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

228
app/pocsag/bch31.c Normal file
View File

@@ -0,0 +1,228 @@
/* UA1ZBE Custom Firmware - BCH(31,21) Decoder for POCSAG
*
* POCSAG uses a shortened BCH(31,21) code:
* - 31-bit codeword (n=31)
* - 21 data bits (k=21)
* - 10 parity/check bits (n-k=10)
* - Can correct up to 2 bit errors per codeword
*
* Generator polynomial: g(x) = x^10 + x^9 + x^8 + x^6 + x^5 + x^3 + 1
* = 0x72D (binary: 111 0010 1101)
*
* The full 32-bit word includes:
* - Bit 31: even parity bit (bit 0 of the word after inversion)
* - Bits 30-0: 31-bit BCH codeword
*
* This implementation avoids hardware division (Cortex-M0 has no DIV unit).
* All operations use shifts and XOR (GF(2) arithmetic).
*/
#include "pocsag.h"
#include <stdint.h>
#include <stdbool.h>
/* Generator polynomial for BCH(31,21): x^10 + x^9 + x^8 + x^6 + x^5 + x^3 + 1 */
#define BCH31_GEN_POLY 0x72DU /* 11100101101 binary */
/* POCSAG sync word (32-bit) */
#define POCSAG_SYNC_WORD 0x7CD21538U
/* POCSAG idle word */
#define POCSAG_IDLE_WORD 0x7A89C197U
/* Parity check mask for even parity */
#define BCH31_PARITY_MASK 0x80000000U
/*
* Calculate syndrome of a 31-bit BCH codeword.
* The syndrome is the remainder of dividing the received word by g(x).
* If syndrome == 0, the word is valid (no errors or undetectable errors).
*
* word: 31-bit codeword (bits 30:0, parity bit excluded)
* Returns: 10-bit syndrome value
*/
static uint16_t bch31_syndrome(uint32_t word)
{
uint32_t reg = word & 0x7FFFFFFFU; /* Mask to 31 bits */
int i;
/* Polynomial division in GF(2) using shift-and-XOR */
/* We process from MSB to LSB, XORing with generator when MSB is 1 */
for (i = 30; i >= 10; i--) {
if (reg & ((uint32_t)1 << i)) {
reg ^= (BCH31_GEN_POLY << (i - 10));
}
}
/* The remainder is in the lower 10 bits */
return (uint16_t)(reg & 0x03FFU);
}
/*
* Check even parity of a 32-bit word.
* Returns true if parity is correct (even number of 1-bits).
*/
static bool bch31_check_parity(uint32_t word)
{
/* Count set bits using a lookup-free method (no division needed) */
uint32_t v = word;
v = v - ((v >> 1) & 0x55555555U);
v = (v & 0x33333333U) + ((v >> 2) & 0x33333333U);
v = (v + (v >> 4)) & 0x0F0F0F0FU;
v = (v * 0x01010101U) >> 24; /* Sum of all bytes */
return (v & 1U) == 0; /* Even parity = even number of 1-bits */
}
/*
* Find error position from syndrome.
* For single-bit errors, the syndrome directly maps to the error position.
* For double-bit errors, we need more complex correction.
*
* syndrome: 10-bit syndrome value
* Returns: bit position (0-30) if single error, 0 if no error,
* or a special value for double errors.
*/
static int bch31_find_single_error(uint16_t syndrome)
{
if (syndrome == 0)
return -1; /* No error */
/* For single-bit errors, syndrome = x^i mod g(x) for error at position i.
* We try each position by computing the expected syndrome.
* This avoids division — just shift and XOR. */
uint32_t test_syn = 1; /* Start with x^0 mod g(x) = 1 */
for (int i = 0; i < 31; i++) {
if (test_syn == syndrome)
return i; /* Error at position i */
/* Multiply by x in GF(2^10 / g(x)):
* Shift left; if bit 10 is set, XOR with generator */
test_syn <<= 1;
if (test_syn & 0x0400U) { /* Bit 10 set */
test_syn ^= BCH31_GEN_POLY;
}
test_syn &= 0x03FFU; /* Keep 10 bits */
}
return -2; /* Not a single-bit error */
}
/*
* Attempt to correct double-bit errors using syndrome decoding.
* For a (31,21) BCH code with d_min=5, we can correct up to 2 errors.
*
* This uses a simplified approach: try all pairs of error positions.
* For performance on Cortex-M0, we use a precomputed approach.
*
* word: pointer to the 32-bit word (will be modified in place if corrected)
* Returns: 0 = no error, 1 = single error corrected,
* 2 = double error corrected, -1 = uncorrectable
*/
int bch31_correct(uint32_t *word)
{
uint32_t data = *word;
/* Step 1: Check parity */
bool parity_ok = bch31_check_parity(data);
/* Extract 31-bit codeword (strip parity bit 31) */
uint32_t codeword = data & 0x7FFFFFFFU;
/* Step 2: Calculate syndrome */
uint16_t syn = bch31_syndrome(codeword);
if (syn == 0 && parity_ok) {
/* No errors detected */
return 0;
}
/* Step 3: Try single-bit error correction */
int err_pos = bch31_find_single_error(syn);
if (err_pos >= 0) {
/* Single-bit error at position err_pos */
codeword ^= ((uint32_t)1 << err_pos);
/* Fix parity bit too */
*word = codeword | ((uint32_t)bch31_check_parity(codeword) << 31);
return 1;
}
/* Step 4: Try double-bit error correction
*
* For double errors at positions i and j:
* syndrome S = x^i + x^j (mod g(x))
*
* We use a brute-force search over all pairs (i, j) where i > j.
* For 31 bits, this is 31*30/2 = 465 pairs — acceptable.
*/
if (err_pos == -2) {
/* Precompute all single-error syndromes */
uint16_t single_syn[31];
uint32_t test = 1;
for (int i = 0; i < 31; i++) {
single_syn[i] = (uint16_t)test;
test <<= 1;
if (test & 0x0400U)
test ^= BCH31_GEN_POLY;
test &= 0x03FFU;
}
/* Search for pair (i, j) where syn_i XOR syn_j == syn */
for (int i = 1; i < 31; i++) {
for (int j = 0; j < i; j++) {
if ((single_syn[i] ^ single_syn[j]) == syn) {
/* Found double error at positions i and j */
codeword ^= ((uint32_t)1 << i);
codeword ^= ((uint32_t)1 << j);
*word = codeword | ((uint32_t)bch31_check_parity(codeword) << 31);
return 2;
}
}
}
}
/* Step 5: If syndrome != 0 but we couldn't find error positions,
* check if it might still be valid (parity might catch it).
* POCSAG spec says words with uncorrectable errors should be discarded.
*/
return -1; /* Uncorrectable */
}
/*
* Extract 21 data bits from a corrected 32-bit POCSAG word.
* Returns the 21-bit data value.
*/
uint32_t bch31_get_data(uint32_t word)
{
/* Data bits are in positions 30:10 of the 31-bit codeword
* (bit 31 is parity, bits 9:0 are check bits)
* So data = bits [30:10] = (word >> 10) & 0x1FFFFF */
return (word >> 10) & 0x001FFFFFU;
}
/*
* Get function bits from a POCSAG address word.
* Address words have function code in bits 11:10 of the data portion.
*/
uint8_t bch31_get_func(uint32_t word)
{
/* Function bits are data bits [11:10] = bits [21:20] of full word */
return (uint8_t)((word >> 20) & 0x03U);
}
/*
* Check if a word is a sync word.
*/
bool bch31_is_sync(uint32_t word)
{
return word == POCSAG_SYNC_WORD;
}
/*
* Check if a word is an idle word.
*/
bool bch31_is_idle(uint32_t word)
{
return word == POCSAG_IDLE_WORD;
}

25
app/pocsag/bch31.h Normal file
View File

@@ -0,0 +1,25 @@
/* UA1ZBE Custom Firmware - BCH(31,21) header */
#ifndef APP_POCSAG_BCH31_H
#define APP_POCSAG_BCH31_H
#include <stdint.h>
#include <stdbool.h>
/* Correct errors in a POCSAG 32-bit word in place.
* Returns: 0 = no error, 1 = single corrected,
* 2 = double corrected, -1 = uncorrectable */
int bch31_correct(uint32_t *word);
/* Extract 21 data bits from corrected word */
uint32_t bch31_get_data(uint32_t word);
/* Get 2-bit function code */
uint8_t bch31_get_func(uint32_t word);
/* Check if word is sync word */
bool bch31_is_sync(uint32_t word);
/* Check if word is idle word */
bool bch31_is_idle(uint32_t word);
#endif

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];
}

150
app/pocsag/pocsag.h Normal file
View File

@@ -0,0 +1,150 @@
/* UA1ZBE Custom Firmware - POCSAG Decoder
*
* Decodes POCSAG paging signals from FM discriminator output.
* Supports 512 and 1200 baud rates with dynamic switching.
*
* POCSAG Protocol:
* - Preamble: 0xAAAAAAAB (at least 576 bits of alternating 1010... ending with 1)
* - Sync word: 0x7CD21538
* - Data words: 32-bit (21 data + 10 BCH + 1 parity)
* - BCH(31,21) code, corrects up to 2 errors per word
*
* Architecture:
* - Samples audio from BK4819 discriminator (REG_69 AF output or FSK FIFO)
* - Zero-crossing detection for bit slicing
* - State machine: IDLE -> PREAMBLE -> SYNC -> DATA
* - Ring buffer for decoded messages
* - Zero malloc — all static allocation
*/
#ifndef APP_POCSAG_POCSAG_H
#define APP_POCSAG_POCSAG_H
#include <stdbool.h>
#include <stdint.h>
/* POCSAG baud rates */
#define POCSAG_BAUD_512 512
#define POCSAG_BAUD_1200 1200
/* Maximum message length in bytes (arbitrary, fits in static buffer) */
#define POCSAG_MSG_MAX_LEN 64
/* Maximum messages in ring buffer */
#define POCSAG_MSG_POOL_SIZE 8
/* Decoded message structure */
typedef struct {
uint32_t address; /* Address (21-bit frame address) */
uint8_t func; /* Function code (2 bits) */
char text[POCSAG_MSG_MAX_LEN]; /* Decoded text (alpha pager) */
uint8_t text_len; /* Length of decoded text */
bool is_alpha; /* true = alpha pager, false = numeric */
uint32_t timestamp; /* System tick when received */
} pocsag_msg_t;
/* Decoder state machine states */
typedef enum {
POCSAG_STATE_IDLE = 0,
POCSAG_STATE_PREAMBLE,
POCSAG_STATE_SYNC,
POCSAG_STATE_DATA
} pocsag_state_t;
/* Decoder context — all static, no malloc */
typedef struct {
/* Configuration */
uint32_t baud_rate; /* Current baud rate: 512 or 1200 */
/* State machine */
pocsag_state_t state;
/* Bit accumulation */
uint32_t shift_reg; /* Shift register for incoming bits */
uint8_t bit_count; /* Number of bits collected in shift_reg */
uint8_t preamble_count; /* Consecutive alternating bits in preamble */
/* Sync detection */
uint8_t sync_word_count; /* Number of sync words found in a row */
/* Word collection */
uint8_t batch_count; /* Words in current batch (0-16) */
uint8_t word_in_batch; /* Current word position in batch */
/* Message assembly */
uint32_t current_address;
uint8_t current_func;
uint8_t msg_data[POCSAG_MSG_MAX_LEN];
uint8_t msg_len;
bool msg_is_alpha;
/* Ring buffer of decoded messages */
pocsag_msg_t messages[POCSAG_MSG_POOL_SIZE];
volatile uint8_t msg_read_idx;
volatile uint8_t msg_write_idx;
volatile uint8_t msg_count;
/* Statistics */
uint32_t total_bits;
uint32_t total_words;
uint32_t corrected_errors;
uint32_t uncorrectable_errors;
/* Flag for new message available */
volatile bool msg_available;
/* Signal active flag */
bool signal_detected;
} pocsag_decoder_t;
/* Global decoder instance */
extern pocsag_decoder_t gPocsag;
/* === API === */
/* Initialize decoder, set baud rate */
void POCSAG_Init(uint32_t baud_rate);
/* Switch baud rate dynamically (512 <-> 1200) */
void POCSAG_SwitchBaud(uint32_t baud_rate);
/* Get current baud rate */
uint32_t POCSAG_GetBaud(void);
/* Feed audio sample (12-bit ADC value from discriminator)
* Call this from the 10ms timeslice or a dedicated sampling timer.
* Returns true if a bit was detected. */
bool POCSAG_FeedSample(uint16_t audio_sample);
/* Feed a single bit (after external bit-slicing) */
void POCSAG_FeedBit(bool bit);
/* Check if a decoded message is available (non-blocking) */
bool POCSAG_MessageAvailable(void);
/* Pop the next decoded message from the ring buffer */
bool POCSAG_GetMessage(pocsag_msg_t *msg);
/* Get decoder state (for display) */
pocsag_state_t POCSAG_GetState(void);
/* Get signal detection status */
bool POCSAG_SignalDetected(void);
/* Reset decoder to idle state */
void POCSAG_Reset(void);
/* Process decoder state machine (call from main loop) */
void POCSAG_Process(void);
/* Configure BK4819 for POCSAG reception on given frequency */
void POCSAG_ConfigureRadio(uint32_t frequency_hz);
/* Stop POCSAG reception, restore normal RX */
void POCSAG_Stop(void);
/* Get state description string */
const char *POCSAG_StateString(pocsag_state_t state);
#endif /* APP_POCSAG_POCSAG_H */