Files
uv-k1-k5v3-firmware-custom/App/apps/beam/beam_app.c
T

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 */
}