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:
228
app/pocsag/bch31.c
Normal file
228
app/pocsag/bch31.c
Normal 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
25
app/pocsag/bch31.h
Normal 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
542
app/pocsag/pocsag.c
Normal 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
150
app/pocsag/pocsag.h
Normal 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 */
|
||||
Reference in New Issue
Block a user