Files
ua1zbe a1206d6fd4 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)
2026-04-12 00:44:05 +03:00

229 lines
6.9 KiB
C

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