UA1ZBE Custom Firmware v1.0

Features:
- VFO mode with direct frequency input
- POCSAG decoder (512/1200 baud) with BCH(31,21) correction
- Full-screen Spectrum analyzer
- FM Radio receiver
- RSSI signal indicator overlay
- Custom boot splash (UA1ZBE / POCSAG pager / build date)

Architecture:
- app/mode.c — mode dispatcher (VFO/POCSAG/Spectrum/FM)
- app/boot_splash.c — 2-second boot splash
- app/pocsag/ — POCSAG decoder + BCH correction
- app/display_rssi.c — RSSI indicator
- main.c — entry point with custom init
- syscalls.c — bare-metal _sbrk stub

Build: arm-none-eabi-gcc -Os -flto -Wall -Werror -Wextra
Size: 57.9KB Flash / 3.6KB RAM

Controls:
- 0-9: Direct frequency input (VFO)
- SK2: POCSAG mode
- SK1: Spectrum analyzer
- 0: FM Radio
- EXIT: Return to VFO
- F/*: Toggle 512/1200 baud (in POCSAG)
This commit is contained in:
2026-04-12 00:44:05 +03:00
parent 5c1229659b
commit a1206d6fd4
3435 changed files with 593221 additions and 2 deletions

542
app/pocsag/pocsag.c Normal file
View File

@@ -0,0 +1,542 @@
/* UA1ZBE Custom Firmware - POCSAG Decoder Implementation
*
* Implements POCSAG decoding from FM discriminator audio samples.
* Uses zero-crossing detection for bit slicing.
*
* Signal chain:
* BK4819 FM demod -> AF output -> ADC sampling
* -> zero-crossing detection -> bit stream -> POCSAG decode
*
* Cortex-M0 optimized: no hardware division in hot path.
* All memory statically allocated.
*/
#include "pocsag.h"
#include "bch31.h"
#include "driver/bk4819.h"
#include "driver/system.h"
#include "driver/st7565.h"
#include "misc.h"
#include "radio.h"
#include <string.h>
/* === Configuration === */
/* Sampling rate for audio (Hz) — must be much higher than max baud rate */
#define POCSAG_SAMPLE_RATE 8000 /* 8 kHz — sufficient for 1200 baud */
/* Threshold for zero-crossing detection (ADC counts) */
#define POCSAG_ZERO_THRESHOLD 2048 /* Midpoint of 12-bit ADC range (0-4095) */
/* Minimum preamble bits to detect (POCSAG spec: 576 bits of 0x55 + 1) */
#define POCSAG_PREAMBLE_MIN_BITS 576
/* BPSK bit extraction: compare sample against threshold */
#define IS_BIT_ONE(sample) ((sample) > POCSAG_ZERO_THRESHOLD)
/* === Global decoder instance === */
pocsag_decoder_t gPocsag;
/* === Internal state for bit slicing === */
/* Samples since last bit decision */
static uint16_t s_sample_counter;
/* Samples per bit for current baud rate */
static uint16_t s_samples_per_bit;
/* Accumulated samples for current bit decision */
static uint32_t s_bit_accumulator;
static uint8_t s_bit_sample_count;
/* Preamble detection: count alternating bits */
static uint16_t s_preamble_alt_count;
static uint8_t s_last_bit;
/* Sync detection shift register (holds last 32 bits) */
static uint32_t s_sync_reg;
/* Bit buffer for assembling 32-bit words */
static uint32_t s_word_reg;
static uint8_t s_word_bits;
/* Batch tracking */
static uint8_t s_batch_words;
/* Address/message assembly */
static uint32_t s_msg_address;
static uint8_t s_msg_func;
static uint8_t s_msg_buf[POCSAG_MSG_MAX_LEN];
static uint8_t s_msg_len;
static bool s_msg_is_alpha;
/* Alpha decoder state */
static uint32_t s_alpha_bits;
static uint8_t s_alpha_bit_count;
/* State */
static pocsag_state_t s_state;
/* Previous save state for restoration */
static uint16_t s_saved_reg_30;
static uint16_t s_saved_reg_47;
static uint16_t s_saved_reg_33;
/* Timing counter — increments each time FeedSample is called (every ~10ms) */
static uint32_t s_tick_counter;
/* Last sync timestamp */
static uint32_t s_last_sync_tick;
/* === Baud rate lookup === */
/* Samples per bit for supported baud rates at 8kHz sample rate */
static uint16_t get_samples_per_bit(uint32_t baud)
{
/* Avoid division: use precomputed values
* 8000 / 512 = 15.625 -> 16
* 8000 / 1200 = 6.667 -> 7 */
if (baud == POCSAG_BAUD_512)
return 16;
if (baud == POCSAG_BAUD_1200)
return 7;
return 7; /* Default to 1200 */
}
/* === Message ring buffer === */
static void msg_push(const pocsag_msg_t *msg)
{
if (gPocsag.msg_count >= POCSAG_MSG_POOL_SIZE) {
/* Buffer full — drop oldest */
gPocsag.msg_read_idx = (gPocsag.msg_read_idx + 1) % POCSAG_MSG_POOL_SIZE;
gPocsag.msg_count--;
}
gPocsag.messages[gPocsag.msg_write_idx] = *msg;
gPocsag.msg_write_idx = (gPocsag.msg_write_idx + 1) % POCSAG_MSG_POOL_SIZE;
gPocsag.msg_count++;
gPocsag.msg_available = true;
}
/* === POCSAG word processing === */
static void process_pocsag_word(uint32_t word)
{
/* Check parity first — quick reject using bit count */
uint32_t v = word;
v = v - ((v >> 1) & 0x55555555U);
v = (v & 0x33333333U) + ((v >> 2) & 0x33333333U);
v = (v + (v >> 4)) & 0x0F0F0F0FU;
v = (v * 0x01010101U) >> 24;
bool parity_ok = (v & 1U) == 0;
/* Try BCH correction */
int corr = bch31_correct(&word);
if (corr < 0 && !parity_ok) {
/* Uncorrectable — discard */
gPocsag.uncorrectable_errors++;
if (s_state == POCSAG_STATE_DATA) {
s_state = POCSAG_STATE_SYNC;
s_batch_words = 0;
}
return;
}
if (corr > 0)
gPocsag.corrected_errors++;
gPocsag.total_words++;
/* Check for sync word */
if (bch31_is_sync(word)) {
s_batch_words = 0;
s_last_sync_tick = s_tick_counter;
return;
}
/* Check for idle word */
if (bch31_is_idle(word)) {
return; /* Skip idle words */
}
/* In DATA state, process the word */
if (s_state != POCSAG_STATE_DATA) {
s_state = POCSAG_STATE_DATA;
s_batch_words = 0;
}
/* Even/odd position in batch determines address vs data */
if (s_batch_words % 2 == 0) {
/* Address word */
s_msg_address = bch31_get_data(word);
s_msg_func = bch31_get_func(word);
s_msg_len = 0;
s_msg_is_alpha = false;
s_msg_buf[0] = '\0';
/* Reset alpha decoder state */
s_alpha_bits = 0;
s_alpha_bit_count = 0;
} else {
/* Data word */
uint32_t data = bch31_get_data(word);
if (s_msg_func == 0) {
/* Numeric pager — data is BCD digits */
uint32_t num = data & 0x000FFFFFU;
char digits[12];
int idx = 0;
if (num == 0) {
digits[idx++] = '0';
} else {
/* Extract digits using precomputed powers of 10 (no division) */
static const uint32_t powers[] = {
1000000000U, 100000000U, 10000000U, 1000000U,
100000U, 10000U, 1000U, 100U, 10U, 1U
};
bool started = false;
for (int p = 0; p < 10; p++) {
uint32_t pwr = powers[p];
uint8_t digit = 0;
while (num >= pwr) {
num -= pwr;
digit++;
}
if (digit > 0 || started || p == 9) {
digits[idx++] = '0' + digit;
started = true;
}
}
}
digits[idx] = '\0';
/* Push message */
if (s_msg_len == 0) {
pocsag_msg_t msg;
memset(&msg, 0, sizeof(msg));
msg.address = s_msg_address;
msg.func = s_msg_func;
msg.is_alpha = false;
msg.timestamp = s_tick_counter;
int copy_len = idx;
if (copy_len > POCSAG_MSG_MAX_LEN - 1)
copy_len = POCSAG_MSG_MAX_LEN - 1;
memcpy(msg.text, digits, copy_len);
msg.text[copy_len] = '\0';
msg.text_len = copy_len;
msg_push(&msg);
}
} else if (s_msg_func == 2) {
/* Alpha pager — 7-bit ASCII, 20 bits per word */
uint32_t bits20 = data & 0x000FFFFFU;
s_alpha_bits = (s_alpha_bits << 20) | bits20;
s_alpha_bit_count += 20;
while (s_alpha_bit_count >= 7 && s_msg_len < POCSAG_MSG_MAX_LEN - 1) {
s_alpha_bit_count -= 7;
uint8_t ch = (s_alpha_bits >> s_alpha_bit_count) & 0x7F;
if (ch == 0x03) {
/* ETX — end of message */
s_msg_buf[s_msg_len] = '\0';
pocsag_msg_t msg;
memset(&msg, 0, sizeof(msg));
msg.address = s_msg_address;
msg.func = s_msg_func;
msg.is_alpha = true;
msg.timestamp = s_tick_counter;
memcpy(msg.text, s_msg_buf, s_msg_len);
msg.text[s_msg_len] = '\0';
msg.text_len = s_msg_len;
msg_push(&msg);
s_msg_len = 0;
s_alpha_bits = 0;
s_alpha_bit_count = 0;
break;
}
s_msg_buf[s_msg_len++] = ch;
}
s_msg_is_alpha = true;
}
}
s_batch_words++;
/* After 32 words, expect new sync */
if (s_batch_words >= 32) {
s_state = POCSAG_STATE_SYNC;
s_batch_words = 0;
}
}
/* === Bit processing === */
static void process_bit(bool bit)
{
gPocsag.total_bits++;
switch (s_state) {
case POCSAG_STATE_IDLE:
case POCSAG_STATE_PREAMBLE:
s_state = POCSAG_STATE_PREAMBLE;
/* Look for alternating pattern: 01010101... (0x55) ending with 1 (0xAB) */
if (s_preamble_alt_count == 0) {
s_last_bit = bit ? 1 : 0;
s_preamble_alt_count = 1;
} else if ((bit ? 1 : 0) != s_last_bit) {
s_preamble_alt_count++;
s_last_bit = bit ? 1 : 0;
} else {
s_preamble_alt_count = 1;
s_last_bit = bit ? 1 : 0;
}
if (s_preamble_alt_count >= POCSAG_PREAMBLE_MIN_BITS) {
/* Preamble detected — look for sync word */
s_state = POCSAG_STATE_SYNC;
s_sync_reg = 0;
s_word_bits = 0;
s_preamble_alt_count = 0;
s_last_sync_tick = s_tick_counter;
}
break;
case POCSAG_STATE_SYNC:
/* Collect bits into 32-bit sync register */
s_sync_reg = (s_sync_reg << 1) | (bit ? 1 : 0);
if (s_sync_reg == 0x7CD21538U) {
/* Sync word found! */
s_state = POCSAG_STATE_DATA;
s_word_reg = 0;
s_word_bits = 0;
s_batch_words = 0;
gPocsag.signal_detected = true;
s_last_sync_tick = s_tick_counter;
}
/* Timeout: ~1000 bits at 1200 baud ≈ 833ms ≈ 83 calls */
if ((s_tick_counter - s_last_sync_tick) > 100) {
s_state = POCSAG_STATE_IDLE;
s_preamble_alt_count = 0;
gPocsag.signal_detected = false;
}
break;
case POCSAG_STATE_DATA:
/* Collect 32-bit words */
s_word_reg = (s_word_reg << 1) | (bit ? 1 : 0);
s_word_bits++;
if (s_word_bits >= 32) {
process_pocsag_word(s_word_reg);
s_word_reg = 0;
s_word_bits = 0;
}
/* Sync loss check */
if ((s_tick_counter - s_last_sync_tick) > 300) {
s_state = POCSAG_STATE_SYNC;
s_sync_reg = 0;
gPocsag.signal_detected = false;
}
break;
}
}
/* === Sample processing (hot path) === */
bool POCSAG_FeedSample(uint16_t audio_sample)
{
s_tick_counter++;
s_sample_counter++;
/* Accumulate samples for bit decision */
s_bit_accumulator += audio_sample;
s_bit_sample_count++;
if (s_bit_sample_count >= s_samples_per_bit) {
/* Make bit decision from averaged sample.
* Avoid division: compare accumulated value against threshold * count.
*
* For 1200 baud (7 samples): threshold * 7 = 2048 * 7 = 14336
* For 512 baud (16 samples): threshold * 16 = 2048 * 16 = 32768 */
uint32_t thresh;
if (gPocsag.baud_rate == POCSAG_BAUD_512)
thresh = 32768;
else
thresh = 14336;
bool bit = (s_bit_accumulator >= thresh);
s_bit_accumulator = 0;
s_bit_sample_count = 0;
/* Process the bit */
process_bit(bit);
return true;
}
return false;
}
void POCSAG_FeedBit(bool bit)
{
process_bit(bit);
}
/* === Public API === */
void POCSAG_Init(uint32_t baud_rate)
{
memset(&gPocsag, 0, sizeof(gPocsag));
gPocsag.baud_rate = baud_rate;
gPocsag.state = POCSAG_STATE_IDLE;
gPocsag.msg_read_idx = 0;
gPocsag.msg_write_idx = 0;
gPocsag.msg_count = 0;
gPocsag.msg_available = false;
s_sample_counter = 0;
s_samples_per_bit = get_samples_per_bit(baud_rate);
s_bit_accumulator = 0;
s_bit_sample_count = 0;
s_preamble_alt_count = 0;
s_last_bit = 0;
s_sync_reg = 0;
s_word_reg = 0;
s_word_bits = 0;
s_batch_words = 0;
s_msg_address = 0;
s_msg_func = 0;
s_msg_len = 0;
s_msg_is_alpha = false;
s_alpha_bits = 0;
s_alpha_bit_count = 0;
s_state = POCSAG_STATE_IDLE;
s_tick_counter = 0;
s_last_sync_tick = 0;
}
void POCSAG_SwitchBaud(uint32_t baud_rate)
{
gPocsag.baud_rate = baud_rate;
s_samples_per_bit = get_samples_per_bit(baud_rate);
s_state = POCSAG_STATE_IDLE;
s_preamble_alt_count = 0;
s_sync_reg = 0;
s_word_reg = 0;
s_word_bits = 0;
s_batch_words = 0;
gPocsag.signal_detected = false;
}
uint32_t POCSAG_GetBaud(void)
{
return gPocsag.baud_rate;
}
bool POCSAG_MessageAvailable(void)
{
return gPocsag.msg_count > 0;
}
bool POCSAG_GetMessage(pocsag_msg_t *msg)
{
if (gPocsag.msg_count == 0)
return false;
*msg = gPocsag.messages[gPocsag.msg_read_idx];
gPocsag.msg_read_idx = (gPocsag.msg_read_idx + 1) % POCSAG_MSG_POOL_SIZE;
gPocsag.msg_count--;
if (gPocsag.msg_count == 0)
gPocsag.msg_available = false;
return true;
}
pocsag_state_t POCSAG_GetState(void)
{
return s_state;
}
bool POCSAG_SignalDetected(void)
{
return gPocsag.signal_detected;
}
void POCSAG_Reset(void)
{
uint32_t baud = gPocsag.baud_rate;
POCSAG_Init(baud);
}
void POCSAG_Process(void)
{
gPocsag.state = s_state;
}
/* === Radio configuration === */
void POCSAG_ConfigureRadio(uint32_t frequency_hz)
{
/* Save current radio state */
s_saved_reg_30 = BK4819_ReadRegister(BK4819_REG_30);
s_saved_reg_47 = BK4819_ReadRegister(BK4819_REG_47);
s_saved_reg_33 = BK4819_ReadRegister(BK4819_REG_33);
/* Set frequency */
BK4819_SetFrequency(frequency_hz);
BK4819_PickRXFilterPathBasedOnFrequency(frequency_hz);
/* Configure for narrow FM (POCSAG is typically narrow FM) */
BK4819_SetFilterBandwidth(BK4819_FILTER_BW_NARROW, true);
/* Turn on RX */
BK4819_RX_TurnOn();
/* Set AF output to FM (demodulated audio) for sampling */
BK4819_SetAF(BK4819_AF_FM);
/* Disable squelch for continuous audio monitoring */
BK4819_SetupSquelch(0, 0, 0, 0, 255, 0);
/* Enable RX link, AF DAC, disc mode */
BK4819_WriteRegister(BK4819_REG_30,
BK4819_REG_30_ENABLE_VCO_CALIB |
BK4819_REG_30_ENABLE_RX_LINK |
BK4819_REG_30_ENABLE_AF_DAC |
BK4819_REG_30_ENABLE_DISC_MODE |
BK4819_REG_30_ENABLE_PLL_VCO |
BK4819_REG_30_ENABLE_RX_DSP);
/* Enable green LED */
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
}
void POCSAG_Stop(void)
{
BK4819_WriteRegister(BK4819_REG_30, s_saved_reg_30);
BK4819_WriteRegister(BK4819_REG_47, s_saved_reg_47);
BK4819_WriteRegister(BK4819_REG_33, s_saved_reg_33);
gPocsag.signal_detected = false;
s_state = POCSAG_STATE_IDLE;
}
/* === State description strings === */
const char *POCSAG_StateString(pocsag_state_t state)
{
static const char *const strs[] = { "IDLE", "SEARCH", "SYNC", "DATA" };
if ((unsigned int)state > 3u) return "???";
return strs[(unsigned int)state];
}