mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-07 21:47:32 +00:00
328 lines
9.3 KiB
C
328 lines
9.3 KiB
C
/* Copyright 2026 Armel F4HWN
|
|
* Licensed under the Apache License, Version 2.0.
|
|
*
|
|
* BEAM overlay app. Packet version 2 remains wire-compatible with the
|
|
* resident implementation: one selected VFO is sent and a received VFO is
|
|
* stored in the first free memory channel.
|
|
*
|
|
* Requires APP_CAP_BEAM2: the resident bridge only tunes the channel and
|
|
* converts it to/from app_beam_channel_t; this app drives the BK4819 FSK modem
|
|
* itself, from register sequences stored in its assets.
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include "../app_api.h"
|
|
#include "beam_assets.h" /* generated by gen_assets.py */
|
|
|
|
#define PACKET_MAGIC 0xBEA5u
|
|
#define PACKET_VERSION 2u
|
|
#define PACKET_START 0xABCDu
|
|
#define PACKET_END 0xDCBAu
|
|
#define PACKET_WORDS 36u
|
|
#define STORE_WORDS 18u /* 72 bytes, 4-byte aligned for the payload */
|
|
|
|
/* BK4819 registers and REG_02 interrupt flags (mirror driver/bk4819-regs.h). */
|
|
enum {
|
|
REG_02 = 0x02, /* interrupt status */
|
|
REG_0B = 0x0B, /* FSK status: bit 4 = CRC error */
|
|
REG_0C = 0x0C, /* bit 0 = interrupt pending */
|
|
REG_5F = 0x5F, /* FSK data FIFO */
|
|
};
|
|
#define IRQ_FSK_FIFO_ALMOST_FULL (1u << 12)
|
|
#define IRQ_FSK_RX_FINISHED (1u << 13)
|
|
|
|
/* Largest register sequence (asset_read into a stack buffer). */
|
|
#define SEQ_MAX_LEN SEQ_RX_LEN
|
|
_Static_assert(SEQ_RESET_LEN <= SEQ_MAX_LEN && SEQ_SETUP_LEN <= SEQ_MAX_LEN &&
|
|
SEQ_TX_LOAD_LEN <= SEQ_MAX_LEN && SEQ_TX_GO_LEN <= SEQ_MAX_LEN &&
|
|
SEQ_TX_END_LEN <= SEQ_MAX_LEN, "SEQ_MAX_LEN too small");
|
|
|
|
enum {
|
|
STATUS_READY = 0,
|
|
STATUS_TX_WAIT,
|
|
STATUS_TX_DONE,
|
|
STATUS_RX_WAIT,
|
|
STATUS_RX_SAVED,
|
|
STATUS_RX_FULL,
|
|
STATUS_ERROR,
|
|
};
|
|
|
|
typedef struct {
|
|
uint16_t magic;
|
|
uint8_t version;
|
|
uint8_t pad;
|
|
app_beam_channel_t channel;
|
|
} beam_payload_t;
|
|
|
|
_Static_assert(sizeof(beam_payload_t) == 48u,
|
|
"BEAM v2 wire payload layout changed");
|
|
|
|
/* All the state in one struct: Thumb-1 code then reaches every field from a
|
|
* single base address (one literal per function instead of one per global).
|
|
* The packet buffer lives on app_main's stack, outside the 4 KiB overlay; the
|
|
* overlay itself is zeroed by the loader at each launch. */
|
|
struct globals {
|
|
uint8_t mode, status;
|
|
uint8_t fsk_index; /* next RX FIFO word of the packet */
|
|
bool receiving, running, dirty;
|
|
uint16_t copiedChannel;
|
|
const app_api_t *api;
|
|
uint32_t *packet_store; /* STORE_WORDS words, on app_main's stack */
|
|
};
|
|
static struct globals g;
|
|
#define A (g.api)
|
|
|
|
static uint16_t *packet(void) { return (uint16_t *)g.packet_store; }
|
|
static beam_payload_t *payload(void) { return (beam_payload_t *)&packet()[2]; }
|
|
|
|
static void clear_bytes(void *ptr, uint8_t count)
|
|
{
|
|
uint8_t *p = (uint8_t *)ptr;
|
|
while (count--) *p++ = 0;
|
|
}
|
|
|
|
static uint16_t crc16(const void *buffer, uint16_t size)
|
|
{
|
|
const uint8_t *data = (const uint8_t *)buffer;
|
|
uint16_t crc = 0;
|
|
while (size--) {
|
|
crc ^= (uint16_t)*data++ << 8;
|
|
for (uint8_t bit = 0; bit < 8u; bit++)
|
|
crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u)
|
|
: (uint16_t)(crc << 1);
|
|
}
|
|
return crc;
|
|
}
|
|
|
|
static void obfuscate(void)
|
|
{
|
|
uint16_t key[8]; /* obfuscation key, from the assets */
|
|
uint16_t *p = packet();
|
|
A->asset_read(OBFUSCATION, key, sizeof(key));
|
|
for (uint8_t i = 0; i < 32u; i++)
|
|
p[i + 1u] ^= key[i & 7u];
|
|
}
|
|
|
|
/* Run one register sequence: (register, value) word pairs, a SEQ_DELAY
|
|
* register meaning "wait value ms". */
|
|
static void bk_seq(uint16_t offset, uint16_t len)
|
|
{
|
|
uint16_t seq[SEQ_MAX_LEN / 2u];
|
|
A->asset_read(offset, seq, len);
|
|
for (const uint16_t *w = seq; w < seq + len / 2u; w += 2) {
|
|
if (w[0] == SEQ_DELAY)
|
|
A->delay_ms(w[1]);
|
|
else
|
|
A->bk_write((uint8_t)w[0], w[1]);
|
|
}
|
|
}
|
|
#define SEQ(name) bk_seq(name, name##_LEN)
|
|
|
|
/* Key up, send one packet (BK4819_SendFSKData), then back to RX. */
|
|
static void fsk_send(const uint16_t *p)
|
|
{
|
|
A->tx_set_params();
|
|
SEQ(SEQ_TX_LOAD);
|
|
for (uint8_t i = 0; i < PACKET_WORDS; i++)
|
|
A->bk_write(REG_5F, p[i]);
|
|
SEQ(SEQ_TX_GO);
|
|
/* TX-finished poll ceiling in 5 ms ticks: ~3 per word plus margin. */
|
|
for (uint16_t t = PACKET_WORDS * 3u + 100u; t && !(A->bk_read(REG_0C) & 1u); t--)
|
|
A->delay_ms(5);
|
|
SEQ(SEQ_TX_END);
|
|
A->tx_end();
|
|
}
|
|
|
|
static void fsk_rx_start(void)
|
|
{
|
|
g.fsk_index = 0;
|
|
SEQ(SEQ_RX);
|
|
}
|
|
|
|
/* Drain the RX FIFO into the packet; APP_BEAM_RX_* once complete. */
|
|
static uint8_t fsk_rx_poll(uint16_t *p)
|
|
{
|
|
while (A->bk_read(REG_0C) & 1u) {
|
|
A->bk_write(REG_02, 0);
|
|
const uint16_t irq = A->bk_read(REG_02);
|
|
if (irq & (IRQ_FSK_FIFO_ALMOST_FULL | IRQ_FSK_RX_FINISHED)) {
|
|
uint8_t words = (irq & IRQ_FSK_RX_FINISHED)
|
|
? (uint8_t)(PACKET_WORDS - g.fsk_index) : 4u;
|
|
while (words--) {
|
|
const uint16_t word = A->bk_read(REG_5F);
|
|
if (g.fsk_index < PACKET_WORDS)
|
|
p[g.fsk_index++] = word;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (g.fsk_index < PACKET_WORDS)
|
|
return APP_BEAM_RX_WAIT;
|
|
|
|
const uint16_t status = A->bk_read(REG_0B);
|
|
fsk_rx_start();
|
|
return (status & 0x0010u) ? APP_BEAM_RX_ERROR : APP_BEAM_RX_READY;
|
|
}
|
|
|
|
static void draw(void)
|
|
{
|
|
/* T_STATE: READY in TX / RX mode, then one entry per later status. */
|
|
char state[T_STATE_STRIDE];
|
|
const uint8_t index = g.status ? (uint8_t)(g.status + 1u) : g.mode;
|
|
A->asset_read(T_STATE + index * T_STATE_STRIDE, state, sizeof(state));
|
|
A->beam_draw(state);
|
|
A->blit_status();
|
|
A->blit_full();
|
|
}
|
|
|
|
static void stop_rx(void)
|
|
{
|
|
if (g.receiving) {
|
|
SEQ(SEQ_RESET);
|
|
g.receiving = false;
|
|
}
|
|
}
|
|
|
|
static void send_packet(void)
|
|
{
|
|
uint16_t *p = packet();
|
|
clear_bytes(g.packet_store, STORE_WORDS * 4u);
|
|
p[0] = PACKET_START;
|
|
payload()->magic = PACKET_MAGIC;
|
|
payload()->version = PACKET_VERSION;
|
|
A->beam_get(&payload()->channel);
|
|
p[34] = crc16(&p[1], 2u + 64u);
|
|
p[35] = PACKET_END;
|
|
obfuscate();
|
|
|
|
g.status = STATUS_TX_WAIT;
|
|
draw();
|
|
fsk_send(p);
|
|
g.status = STATUS_TX_DONE;
|
|
for (uint8_t i = 0; i < 3u; i++) {
|
|
A->play_tone(880, 60);
|
|
if (i < 2u) A->delay_ms(20);
|
|
}
|
|
g.dirty = true;
|
|
}
|
|
|
|
static void start(void)
|
|
{
|
|
stop_rx();
|
|
/* A channel save is committed only after app_main() returns. Do not let a
|
|
second RX overwrite that pending save; exit and relaunch to receive more. */
|
|
if (g.mode && g.copiedChannel != 0xFFFFu)
|
|
return;
|
|
A->beam_prepare();
|
|
SEQ(SEQ_SETUP);
|
|
if (!g.mode) {
|
|
send_packet();
|
|
return;
|
|
}
|
|
fsk_rx_start();
|
|
g.receiving = true;
|
|
g.status = STATUS_RX_WAIT;
|
|
g.dirty = true;
|
|
}
|
|
|
|
static bool valid_packet(void)
|
|
{
|
|
uint16_t *p = packet();
|
|
if (p[0] != PACKET_START || p[35] != PACKET_END)
|
|
return false;
|
|
obfuscate();
|
|
if (p[34] != crc16(&p[1], 2u + 64u))
|
|
return false;
|
|
return payload()->magic == PACKET_MAGIC && payload()->version == PACKET_VERSION;
|
|
}
|
|
|
|
static void poll_rx(void)
|
|
{
|
|
if (!g.receiving)
|
|
return;
|
|
const uint8_t result = fsk_rx_poll(packet());
|
|
if (result == APP_BEAM_RX_WAIT)
|
|
return;
|
|
if (result == APP_BEAM_RX_ERROR || !valid_packet()) {
|
|
g.status = STATUS_ERROR;
|
|
A->backlight_on();
|
|
g.dirty = true;
|
|
return;
|
|
}
|
|
|
|
g.copiedChannel = A->beam_save(&payload()->channel);
|
|
stop_rx();
|
|
g.status = g.copiedChannel == 0xFFFFu ? STATUS_RX_FULL : STATUS_RX_SAVED;
|
|
A->backlight_on();
|
|
g.dirty = true;
|
|
}
|
|
|
|
static void key_press(uint8_t key)
|
|
{
|
|
switch (key) {
|
|
case APP_KEY_UP:
|
|
case APP_KEY_DOWN:
|
|
stop_rx();
|
|
g.mode ^= 1u;
|
|
g.status = STATUS_READY;
|
|
g.dirty = true;
|
|
break;
|
|
case APP_KEY_MENU:
|
|
start();
|
|
break;
|
|
case APP_KEY_EXIT:
|
|
g.running = false;
|
|
break;
|
|
case APP_KEY_PTT:
|
|
break;
|
|
default:
|
|
A->play_tone(500, 60);
|
|
break;
|
|
}
|
|
}
|
|
|
|
__attribute__((section(".text.entry"), used))
|
|
void app_main(const app_api_t *api)
|
|
{
|
|
uint32_t store[STORE_WORDS]; /* the packet: 4-byte aligned, not in the overlay */
|
|
A = api;
|
|
g.packet_store = store;
|
|
/* TX mode, READY and not receiving: zeroed (.bss) */
|
|
g.copiedChannel = 0xFFFFu;
|
|
g.running = true;
|
|
g.dirty = true;
|
|
A->backlight_on();
|
|
|
|
uint8_t previous = APP_KEY_INVALID;
|
|
uint8_t batteryTicks = 0;
|
|
while (g.running) {
|
|
const uint8_t key = A->get_key();
|
|
if (key == APP_KEY_SAVER) {
|
|
previous = APP_KEY_INVALID;
|
|
} else if (key == APP_KEY_WAKE) {
|
|
previous = APP_KEY_INVALID;
|
|
g.dirty = true;
|
|
} else {
|
|
if (key != previous && key != APP_KEY_INVALID) {
|
|
A->backlight_on();
|
|
key_press(key);
|
|
}
|
|
previous = key;
|
|
}
|
|
|
|
poll_rx();
|
|
if (g.dirty) { draw(); g.dirty = false; }
|
|
A->delay_ms(10);
|
|
A->backlight_update();
|
|
if (++batteryTicks >= 50u) {
|
|
batteryTicks = 0;
|
|
A->battery_sample();
|
|
g.dirty = true;
|
|
}
|
|
}
|
|
|
|
SEQ(SEQ_RESET); /* stop the FSK modem (RX or idle) before returning */
|
|
}
|