init commit

This commit is contained in:
2026-02-05 00:37:55 -07:00
commit 2c87c57651
19 changed files with 1727 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
# Build
*.o
*.elf
*.bin
*.hex
*.map
*.lst
build/
firmware/.pio/
# Sim artifacts
cat-radio-sim
*.wav
# IDE
.vscode/
.idea/
*.swp
*~
# OS
.DS_Store
Thumbs.db
+218
View File
@@ -0,0 +1,218 @@
# CAT-Radio - Cave Adapted Telephone
Open source through-the-earth digital text messaging system. FOSS clone of Cave-Link.
## Overview
| Parameter | Value |
|-----------|-------|
| Frequency | 20-140 kHz (VLF), selectable |
| Mode | Digital text, FSK modulated |
| Baud rate | 10-100 baud (frequency dependent) |
| TX power | 10-30W |
| Target range | 1000m+ through rock |
| Error handling | CRC-16 + ARQ (retry until success) |
| Relay | Store & forward |
## Why This Works
Lower frequency = deeper rock penetration.
Skin depth formula: δ ≈ 503 × √(ρ/f) meters
For 100 Ωm resistivity rock:
- 87 kHz → ~170m skin depth (Nicola voice system)
- 40 kHz → ~350m skin depth
- 20 kHz → ~500m skin depth
Digital + checksums = bad signal just means slower transmission, not errors.
## Hardware
### MCU
**STM32L476RG** (NUCLEO-L476RG dev board)
| Spec | Value |
|------|-------|
| Core | Cortex-M4 + FPU |
| Clock | 80 MHz |
| RAM | 128 KB |
| ADC | 12-bit, 5 Msps |
| DAC | 12-bit, 2 channels |
| Power | Ultra low (µA in sleep) |
### Display + Input
| Part | Notes |
|------|-------|
| ILI9341 2.4" LCD | 320x240, SPI interface |
| 4x4 membrane keypad | T9-style text input |
| HC-05 Bluetooth | Optional phone interface |
### Test Equipment
| Part | Purpose |
|------|---------|
| SDRplay RSP1A | View TX signal, debug, spectrum analysis |
### Antenna Options
**Earth Electrodes (best range):**
- Two copper/steel stakes, 30-50cm long
- 50-100m wire between them
- Low ground resistance critical
- Surface: lay out wire in field
- Underground: clip to metal, wet cracks, pools
**Loop Antenna (portable RX):**
- 0.5m diameter, collapsible frame
- 80-100 turns, 0.5mm² enameled wire
- ~50 mH inductance
- Tuned with ~300 pF variable cap
- Electrostatically shielded
## Firmware Architecture
```
┌─────────────────────────────────────┐
│ Application │
│ - Message compose/display │
│ - T9 text input │
│ - Menu system │
│ - Store & forward logic │
├─────────────────────────────────────┤
│ Protocol │
│ - Packet framing │
│ - CRC-16-CCITT │
│ - ARQ (ACK/retry) │
│ - Addressing │
├─────────────────────────────────────┤
│ Modem │
│ - FSK modulator (DAC + DMA) │
│ - FSK demodulator (Goertzel) │
│ - Bit sync / clock recovery │
│ - Carrier detect │
├─────────────────────────────────────┤
│ HAL / Drivers │
│ - ADC + DMA │
│ - DAC + DMA │
│ - UART (debug, BT) │
│ - SPI (display) │
│ - GPIO (keypad) │
└─────────────────────────────────────┘
```
## Packet Format
```
┌────────┬────────┬──────┬────────┬─────────────────┬───────┐
│Preamble│Sync │ Len │ Header │ Payload │ CRC │
│ 16 bits│ 16 bits│8 bits│ 32 bits│ 0-64 bytes │16 bits│
└────────┴────────┴──────┴────────┴─────────────────┴───────┘
0xAAAA 0x2D4B N ... message CRC-16
```
**Header:**
- Dst (1 byte): destination address, 0xFF = broadcast
- Src (1 byte): source address
- Seq (1 byte): sequence number for ARQ
- Flags (1 byte): ACK, REQ_ACK, RELAY, URGENT
## FSK Modem Specs
| Parameter | Value |
|-----------|-------|
| Center frequency | 40 kHz (default) |
| Mark (1) | center + 50 Hz |
| Space (0) | center - 50 Hz |
| Baud rate | 50 baud |
| Modulation | Continuous-phase 2-FSK |
| Demodulation | Goertzel algorithm |
## Dev Plan
### Phase 1: Modem Proof of Concept
1. Set up STM32CubeIDE
2. Generate 40 kHz sine on DAC
3. View on SDR
4. Add FSK modulation
5. Implement Goertzel demodulator
6. Loopback test (DAC → wire → ADC)
### Phase 2: Packet Protocol
1. Implement packet framing
2. Add CRC-16
3. Test encode/decode
4. Add ARQ state machine
### Phase 3: User Interface
1. Wire up ILI9341 display
2. Wire up 4x4 keypad
3. Implement T9 input
4. Message display/history
### Phase 4: RF Hardware
1. Build RX preamp
2. Build TX amplifier (10W)
3. Build antenna matching network
4. Test with loop antenna
5. Test with earth electrodes
### Phase 5: Field Testing
1. Short range through-air test
2. Through-rock test (shallow)
3. Deep cave test
4. Range optimization
## Parts List
**DigiKey:**
| Item | Price |
|------|-------|
| NUCLEO-L476RG | $14.85 |
**Amazon/AliExpress:**
| Item | Price |
|------|-------|
| ILI9341 2.4" SPI LCD | ~$10 |
| 4x4 membrane keypad | ~$3 |
| HC-05 Bluetooth module | ~$5 |
**SDRplay:**
| Item | Price |
|------|-------|
| RSP1A | ~$110 |
**Total: ~$145**
**Phase 1 Test BOM (DigiKey):**
| Item |
|------|
| 40 kHz ultrasonic transducers x2 (muRata MA40S4S/R) |
| TL072 dual op-amp |
| Breadboard |
| Jumper wire kit (M-M) |
| Through-hole ceramic capacitor variety pack |
| Through-hole resistor variety pack |
**Later (RF hardware):**
| Item | Notes |
|------|-------|
| TL072 / LF353 op-amps | RX preamp |
| IRLZ44N MOSFETs | TX PA |
| Matching transformer | Antenna interface |
| Wire, stakes | Antennas |
## References
- [Cave-Link](https://www.cavelink.com/cl3x_neu/index.php/en/)
- [Nicola System](https://www.caverescue.org.uk/nicolaradio/)
- [3496 Hz DQ Receiver](https://radiolocation.tripod.com/DQ_Construction/DQRX.htm)
- [Arduino Cave Radio (GitHub)](https://github.com/adam-sampson/Arduino-cave-radio)
- [Through-the-Earth Communications (Wikipedia)](https://en.wikipedia.org/wiki/Through-the-earth_communications)
- [Skin Depth Theory](https://em.geosci.xyz/content/maxwell1_fundamentals/harmonic_planewaves_homogeneous/skindepth.html)
## License
TBD - likely GPLv3 or MIT
+17
View File
@@ -0,0 +1,17 @@
CC = gcc
CFLAGS = -Wall -Wextra -O2 -std=c99
LDFLAGS = -lm
TARGET = cat-radio-sim
SRCS = sim/main.c src/modem.c src/packet.c src/channel.c
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $@ $(SRCS) $(LDFLAGS)
clean:
rm -f $(TARGET) cat-radio-sim.wav
+78
View File
@@ -0,0 +1,78 @@
#ifndef STM32L4XX_HAL_CONF_H
#define STM32L4XX_HAL_CONF_H
/* ---- Oscillator values ---- */
#define HSE_VALUE 8000000U
#define HSE_STARTUP_TIMEOUT 100U
#define MSI_VALUE 4000000U
#define LSE_VALUE 32768U
#define LSE_STARTUP_TIMEOUT 5000U
#define HSI_VALUE 16000000U
#define LSI_VALUE 32000U
#define EXTERNAL_SAI1_CLOCK_VALUE 0U
#define EXTERNAL_SAI2_CLOCK_VALUE 0U
/* ---- HAL module selection ---- */
#define HAL_MODULE_ENABLED
#define HAL_CORTEX_MODULE_ENABLED
#define HAL_RCC_MODULE_ENABLED
#define HAL_GPIO_MODULE_ENABLED
#define HAL_DMA_MODULE_ENABLED
#define HAL_DAC_MODULE_ENABLED
#define HAL_ADC_MODULE_ENABLED
#define HAL_TIM_MODULE_ENABLED
#define HAL_UART_MODULE_ENABLED
#define HAL_PWR_MODULE_ENABLED
#define HAL_FLASH_MODULE_ENABLED
/* ---- Prefetch / caches ---- */
#define PREFETCH_ENABLE 1U
#define INSTRUCTION_CACHE_ENABLE 1U
#define DATA_CACHE_ENABLE 1U
/* ---- SysTick ---- */
#define TICK_INT_PRIORITY 15U
#define USE_RTOS 0U
/* ---- Include the required HAL headers ---- */
#ifdef HAL_RCC_MODULE_ENABLED
#include "stm32l4xx_hal_rcc.h"
#endif
#ifdef HAL_GPIO_MODULE_ENABLED
#include "stm32l4xx_hal_gpio.h"
#endif
#ifdef HAL_DMA_MODULE_ENABLED
#include "stm32l4xx_hal_dma.h"
#endif
#ifdef HAL_CORTEX_MODULE_ENABLED
#include "stm32l4xx_hal_cortex.h"
#endif
#ifdef HAL_ADC_MODULE_ENABLED
#include "stm32l4xx_hal_adc.h"
#endif
#ifdef HAL_DAC_MODULE_ENABLED
#include "stm32l4xx_hal_dac.h"
#endif
#ifdef HAL_TIM_MODULE_ENABLED
#include "stm32l4xx_hal_tim.h"
#endif
#ifdef HAL_UART_MODULE_ENABLED
#include "stm32l4xx_hal_uart.h"
#endif
#ifdef HAL_PWR_MODULE_ENABLED
#include "stm32l4xx_hal_pwr.h"
#endif
#ifdef HAL_FLASH_MODULE_ENABLED
#include "stm32l4xx_hal_flash.h"
#endif
/* ---- Assert ---- */
/* #define USE_FULL_ASSERT 1U */
#ifdef USE_FULL_ASSERT
#define assert_param(expr) ((expr) ? (void)0U : assert_failed((uint8_t *)__FILE__, __LINE__))
void assert_failed(uint8_t *file, uint32_t line);
#else
#define assert_param(expr) ((void)0U)
#endif
#endif /* STM32L4XX_HAL_CONF_H */
+11
View File
@@ -0,0 +1,11 @@
[env:nucleo_l476rg]
platform = ststm32
board = nucleo_l476rg
framework = stm32cube
monitor_speed = 115200
build_flags =
-I../src
-Iinclude
build_src_filter =
+<*>
+<../../src/packet.c>
+233
View File
@@ -0,0 +1,233 @@
#include "hal_init.h"
/* ---- Handle instances ---- */
TIM_HandleTypeDef htim6;
DAC_HandleTypeDef hdac1;
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_dac1_ch1;
DMA_HandleTypeDef hdma_adc1;
UART_HandleTypeDef huart2;
/* ---- System clock: HSE(8MHz) -> PLL -> 80 MHz ---- */
static void clock_init(void)
{
RCC_OscInitTypeDef osc = {0};
osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
osc.HSEState = RCC_HSE_BYPASS; /* Nucleo uses ST-Link MCO as HSE */
osc.PLL.PLLState = RCC_PLL_ON;
osc.PLL.PLLSource = RCC_PLLSOURCE_HSE;
osc.PLL.PLLM = 1; /* 8 MHz / 1 = 8 MHz */
osc.PLL.PLLN = 20; /* 8 * 20 = 160 MHz VCO */
osc.PLL.PLLP = RCC_PLLP_DIV7;
osc.PLL.PLLQ = RCC_PLLQ_DIV2;
osc.PLL.PLLR = RCC_PLLR_DIV2; /* 160 / 2 = 80 MHz */
HAL_RCC_OscConfig(&osc);
RCC_ClkInitTypeDef clk = {0};
clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clk.AHBCLKDivider = RCC_SYSCLK_DIV1;
clk.APB1CLKDivider = RCC_HCLK_DIV1;
clk.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_4);
}
/* ---- GPIO ---- */
static void gpio_init(void)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
/* PA4 = DAC1_OUT1 (analog) */
gpio.Pin = GPIO_PIN_4;
gpio.Mode = GPIO_MODE_ANALOG;
gpio.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &gpio);
/* PA0 = ADC1_IN5 (analog) */
gpio.Pin = GPIO_PIN_0;
gpio.Mode = GPIO_MODE_ANALOG;
gpio.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &gpio);
/* PA2 = USART2_TX, PA3 = USART2_RX (AF7) */
gpio.Pin = GPIO_PIN_2 | GPIO_PIN_3;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pull = GPIO_PULLUP;
gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
gpio.Alternate = GPIO_AF7_USART2;
HAL_GPIO_Init(GPIOA, &gpio);
}
/* ---- TIM6: 100 kHz trigger for DAC + ADC ---- */
static void tim6_init(void)
{
__HAL_RCC_TIM6_CLK_ENABLE();
htim6.Instance = TIM6;
htim6.Init.Prescaler = 0;
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = 799; /* 80 MHz / 800 = 100 kHz */
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_Base_Init(&htim6);
TIM_MasterConfigTypeDef master = {0};
master.MasterOutputTrigger = TIM_TRGO_UPDATE;
master.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim6, &master);
}
/* ---- DMA ---- */
static void dma_init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
/* DMA1 Channel3 — DAC1 CH1 */
hdma_dac1_ch1.Instance = DMA1_Channel3;
hdma_dac1_ch1.Init.Request = DMA_REQUEST_6; /* DAC1_CH1 */
hdma_dac1_ch1.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_dac1_ch1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_dac1_ch1.Init.MemInc = DMA_MINC_ENABLE;
hdma_dac1_ch1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_dac1_ch1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_dac1_ch1.Init.Mode = DMA_CIRCULAR;
hdma_dac1_ch1.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_dac1_ch1);
__HAL_LINKDMA(&hdac1, DMA_Handle1, hdma_dac1_ch1);
HAL_NVIC_SetPriority(DMA1_Channel3_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn);
/* DMA1 Channel1 — ADC1 */
hdma_adc1.Instance = DMA1_Channel1;
hdma_adc1.Init.Request = DMA_REQUEST_0; /* ADC1 */
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR;
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_adc1);
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}
/* ---- DAC1 CH1 on PA4 ---- */
static void dac_init(void)
{
__HAL_RCC_DAC1_CLK_ENABLE();
hdac1.Instance = DAC1;
HAL_DAC_Init(&hdac1);
DAC_ChannelConfTypeDef cfg = {0};
cfg.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE;
cfg.DAC_Trigger = DAC_TRIGGER_T6_TRGO;
cfg.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE;
cfg.DAC_ConnectOnChipPeripheral = DAC_CHIPCONNECT_DISABLE;
cfg.DAC_UserTrimming = DAC_TRIMMING_FACTORY;
HAL_DAC_ConfigChannel(&hdac1, &cfg, DAC_CHANNEL_1);
}
/* ---- ADC1 IN5 on PA0 ---- */
static void adc_init(void)
{
__HAL_RCC_ADC_CLK_ENABLE();
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV1;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
hadc1.Init.LowPowerAutoWait = DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.NbrOfConversion = 1;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIG_T6_TRGO;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
hadc1.Init.DMAContinuousRequests = ENABLE;
hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
hadc1.Init.OversamplingMode = DISABLE;
HAL_ADC_Init(&hadc1);
ADC_ChannelConfTypeDef ch = {0};
ch.Channel = ADC_CHANNEL_5;
ch.Rank = ADC_REGULAR_RANK_1;
ch.SamplingTime = ADC_SAMPLETIME_6CYCLES_5;
ch.SingleDiff = ADC_SINGLE_ENDED;
ch.OffsetNumber = ADC_OFFSET_NONE;
HAL_ADC_ConfigChannel(&hadc1, &ch);
HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
}
/* ---- USART2: 115200 8N1 on PA2/PA3 ---- */
static void uart_init(void)
{
__HAL_RCC_USART2_CLK_ENABLE();
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart2);
}
/* ---- Public API ---- */
void hal_init_all(void)
{
HAL_Init();
clock_init();
gpio_init();
dma_init();
tim6_init();
dac_init();
adc_init();
uart_init();
}
void hal_dac_start(uint16_t *buf, uint32_t len)
{
HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t *)buf, len,
DAC_ALIGN_12B_R);
HAL_TIM_Base_Start(&htim6);
}
void hal_dac_stop(void)
{
HAL_DAC_Stop_DMA(&hdac1, DAC_CHANNEL_1);
}
void hal_adc_start(uint16_t *buf, uint32_t len)
{
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)buf, len);
}
void hal_adc_stop(void)
{
HAL_ADC_Stop_DMA(&hadc1);
HAL_TIM_Base_Stop(&htim6);
}
/* ---- IRQ handlers ---- */
void DMA1_Channel3_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_dac1_ch1);
}
void DMA1_Channel1_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_adc1);
}
+23
View File
@@ -0,0 +1,23 @@
#ifndef HAL_INIT_H
#define HAL_INIT_H
#include "stm32l4xx_hal.h"
/* Peripheral handles — declared in hal_init.c */
extern TIM_HandleTypeDef htim6;
extern DAC_HandleTypeDef hdac1;
extern ADC_HandleTypeDef hadc1;
extern DMA_HandleTypeDef hdma_dac1_ch1;
extern DMA_HandleTypeDef hdma_adc1;
extern UART_HandleTypeDef huart2;
/* Full peripheral init sequence */
void hal_init_all(void);
/* DMA start/stop helpers */
void hal_dac_start(uint16_t *buf, uint32_t len);
void hal_dac_stop(void);
void hal_adc_start(uint16_t *buf, uint32_t len);
void hal_adc_stop(void);
#endif /* HAL_INIT_H */
+185
View File
@@ -0,0 +1,185 @@
#include "stm32l4xx_hal.h"
#include "hal_init.h"
#include "modem_hw.h"
#include "uart_print.h"
#include "packet.h"
#include <string.h>
/* ---- DMA buffers ---- */
static uint16_t dac_buf[MODEM_DMA_BUF_LEN];
static uint16_t adc_buf[MODEM_DMA_BUF_LEN];
/* ---- Modem state machines ---- */
static modem_tx_state_t tx_state;
static modem_rx_state_t rx_state;
/* ---- Flags set from ISR, processed in main loop ---- */
static volatile int tx_half_ready;
static volatile int tx_full_ready;
static volatile int rx_half_ready;
static volatile int rx_full_ready;
static volatile int rx_done_flag;
/* ---- DMA callbacks ---- */
void HAL_DAC_ConvHalfCpltCallbackCh1(DAC_HandleTypeDef *hdac)
{
(void)hdac;
tx_half_ready = 1;
}
void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac)
{
(void)hdac;
tx_full_ready = 1;
}
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
(void)hadc;
rx_half_ready = 1;
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
(void)hadc;
rx_full_ready = 1;
}
/* ---- Main ---- */
int main(void)
{
hal_init_all();
uart_printf("\r\n=== Cat-Radio STM32 Loopback Test ===\r\n");
uart_printf("Hello from Cat-Radio firmware!\r\n");
/* ---- Build test packet ---- */
packet_t pkt;
memset(&pkt, 0, sizeof(pkt));
pkt.header.dst = 0x01;
pkt.header.src = 0x02;
pkt.header.seq = 0x00;
pkt.header.flags = 0x00;
const char *msg = "Hello, Cat-Radio!";
pkt.payload_len = (uint8_t)strlen(msg);
memcpy(pkt.payload, msg, pkt.payload_len);
uint8_t frame[PACKET_MAX_FRAME];
size_t frame_len = packet_encode(&pkt, frame, sizeof(frame));
uart_printf("TX frame (%u bytes): ", (unsigned)frame_len);
uart_print_hex(frame, frame_len);
/* ---- Init modem state machines ---- */
modem_tx_init(&tx_state, frame, frame_len);
modem_rx_init(&rx_state);
/* Pre-fill the entire DAC buffer before starting DMA */
modem_tx_fill_buffer(&tx_state, &dac_buf[0]);
modem_tx_fill_buffer(&tx_state, &dac_buf[MODEM_HALF_BUF_LEN]);
/* Clear flags */
tx_half_ready = 0;
tx_full_ready = 0;
rx_half_ready = 0;
rx_full_ready = 0;
rx_done_flag = 0;
/* Start ADC DMA first (so it's ready when DAC starts outputting) */
hal_adc_start(adc_buf, MODEM_DMA_BUF_LEN);
/* Start DAC DMA (also starts TIM6, which triggers both DAC and ADC) */
hal_dac_start(dac_buf, MODEM_DMA_BUF_LEN);
uart_printf("DMA started. Waiting for loopback...\r\n");
/* ---- Main processing loop ---- */
uint32_t timeout_start = HAL_GetTick();
uint32_t timeout_ms = 30000; /* 30 second timeout */
while (!rx_done_flag) {
/* Process TX half-buffer requests */
if (tx_half_ready) {
tx_half_ready = 0;
modem_tx_fill_buffer(&tx_state, &dac_buf[0]);
}
if (tx_full_ready) {
tx_full_ready = 0;
modem_tx_fill_buffer(&tx_state, &dac_buf[MODEM_HALF_BUF_LEN]);
}
/* Process RX half-buffer requests */
if (rx_half_ready) {
rx_half_ready = 0;
if (modem_rx_process_buffer(&rx_state, &adc_buf[0]))
rx_done_flag = 1;
}
if (rx_full_ready) {
rx_full_ready = 0;
if (modem_rx_process_buffer(&rx_state, &adc_buf[MODEM_HALF_BUF_LEN]))
rx_done_flag = 1;
}
/* Timeout check */
if ((HAL_GetTick() - timeout_start) > timeout_ms) {
uart_printf("TIMEOUT: No frame detected after %lu ms\r\n",
timeout_ms);
break;
}
/* Sleep until next interrupt */
if (!rx_done_flag && !tx_half_ready && !tx_full_ready &&
!rx_half_ready && !rx_full_ready)
__WFI();
}
/* ---- Stop DMA ---- */
hal_adc_stop();
hal_dac_stop();
/* ---- Decode received frame ---- */
if (rx_state.phase == RX_DONE && rx_state.data_len > 0) {
uart_printf("RX raw data (%u bytes): ", (unsigned)rx_state.data_len);
uart_print_hex(rx_state.data, rx_state.data_len);
packet_t rx_pkt;
int rc = packet_decode(rx_state.data, rx_state.data_len, &rx_pkt);
if (rc == 0 && rx_pkt.crc_ok) {
uart_printf("CRC: OK\r\n");
uart_printf("Payload (%u bytes): ", rx_pkt.payload_len);
/* Print as string */
char payload_str[PACKET_MAX_PAYLOAD + 1];
memcpy(payload_str, rx_pkt.payload, rx_pkt.payload_len);
payload_str[rx_pkt.payload_len] = '\0';
uart_printf("%s\r\n", payload_str);
/* Verify content */
if (rx_pkt.payload_len == pkt.payload_len &&
memcmp(rx_pkt.payload, pkt.payload, pkt.payload_len) == 0) {
uart_printf("\r\n*** PASS: Loopback test successful! ***\r\n");
} else {
uart_printf("\r\n*** FAIL: Payload mismatch ***\r\n");
}
} else {
uart_printf("CRC: FAIL (rc=%d, crc_ok=%d)\r\n", rc,
rc == 0 ? rx_pkt.crc_ok : -1);
uart_printf("\r\n*** FAIL: CRC error ***\r\n");
}
} else {
uart_printf("\r\n*** FAIL: No frame detected ***\r\n");
}
/* Halt */
while (1)
__WFI();
}
/* ---- SysTick handler (HAL needs this) ---- */
void SysTick_Handler(void)
{
HAL_IncTick();
}
+196
View File
@@ -0,0 +1,196 @@
#include "modem_hw.h"
#include <math.h>
#include <string.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
/* ================================================================
* TX — streaming modulator
* ================================================================ */
void modem_tx_init(modem_tx_state_t *tx, const uint8_t *frame, size_t frame_len)
{
tx->frame = frame;
tx->frame_len = frame_len;
tx->byte_idx = 0;
tx->bit_idx = 7;
tx->phase = 0.0;
tx->done = 0;
}
void modem_tx_fill_buffer(modem_tx_state_t *tx, uint16_t *buf)
{
if (tx->done) {
/* Output mid-scale silence */
for (int i = 0; i < MODEM_HALF_BUF_LEN; i++)
buf[i] = 2048;
return;
}
/* Current bit determines frequency */
int b = (tx->frame[tx->byte_idx] >> tx->bit_idx) & 1;
float freq = b ? MODEM_FREQ_MARK : MODEM_FREQ_SPACE;
double phase_inc = 2.0 * M_PI * (double)freq / (double)MODEM_SAMPLE_RATE;
for (int i = 0; i < MODEM_HALF_BUF_LEN; i++) {
/* sin -> [-1, 1], map to [0, 4095] for 12-bit DAC */
float s = sinf((float)tx->phase);
buf[i] = (uint16_t)(((s + 1.0f) / 2.0f) * 4095.0f);
tx->phase += phase_inc;
}
/* Keep phase in [0, 2*PI) */
tx->phase = fmod(tx->phase, 2.0 * M_PI);
/* Advance to next bit */
if (tx->bit_idx == 0) {
tx->bit_idx = 7;
tx->byte_idx++;
if (tx->byte_idx >= tx->frame_len)
tx->done = 1;
} else {
tx->bit_idx--;
}
}
/* ================================================================
* RX — streaming demodulator with Goertzel
* ================================================================ */
static float goertzel_mag(const float *samples, size_t n,
float target_freq, float sample_rate)
{
float k = target_freq * (float)n / sample_rate;
float w = 2.0f * (float)M_PI * k / (float)n;
float coeff = 2.0f * cosf(w);
float s0 = 0.0f, s1 = 0.0f, s2 = 0.0f;
for (size_t i = 0; i < n; i++) {
s0 = samples[i] + coeff * s1 - s2;
s2 = s1;
s1 = s0;
}
return s1 * s1 + s2 * s2 - coeff * s1 * s2;
}
void modem_rx_init(modem_rx_state_t *rx)
{
memset(rx, 0, sizeof(*rx));
rx->phase = RX_HUNT;
rx->expected_bytes = -1;
rx->last_bit = -1;
}
int modem_rx_process_buffer(modem_rx_state_t *rx, const uint16_t *buf)
{
if (rx->phase == RX_DONE)
return 1;
/* Convert uint16_t ADC samples to float [-1, 1] */
static float fbuf[MODEM_HALF_BUF_LEN];
for (int i = 0; i < MODEM_HALF_BUF_LEN; i++)
fbuf[i] = ((float)buf[i] / 2048.0f) - 1.0f;
/* Goertzel to decide mark or space */
float mag_mark = goertzel_mag(fbuf, MODEM_HALF_BUF_LEN,
MODEM_FREQ_MARK, MODEM_SAMPLE_RATE);
float mag_space = goertzel_mag(fbuf, MODEM_HALF_BUF_LEN,
MODEM_FREQ_SPACE, MODEM_SAMPLE_RATE);
int bit = (mag_mark >= mag_space) ? 1 : 0;
/* Check if there's any meaningful signal */
float max_mag = (mag_mark > mag_space) ? mag_mark : mag_space;
rx->total_bits++;
if (max_mag < 1.0f) {
/* Very low energy — likely no signal */
rx->idle_count++;
if (rx->idle_count > 100) {
/* Timeout — no signal for too long */
if (rx->phase != RX_HUNT) {
rx->phase = RX_DONE;
return 1;
}
}
/* In HUNT mode with no signal, just skip */
if (rx->phase == RX_HUNT)
return 0;
} else {
rx->idle_count = 0;
}
switch (rx->phase) {
case RX_HUNT:
/* Look for alternating bits (preamble = 0xAA = 10101010) */
if (rx->last_bit >= 0 && bit != rx->last_bit) {
rx->preamble_count++;
} else if (rx->last_bit >= 0) {
/* Not alternating — reset if we haven't found enough preamble,
* otherwise transition to sync search since this might be
* the start of the sync word */
if (rx->preamble_count >= 8) {
/* We had good preamble, now start looking for sync */
rx->phase = RX_SYNC;
rx->shift_reg = 0;
/* Process this bit in SYNC state below */
rx->shift_reg = (rx->shift_reg << 1) | (uint16_t)bit;
rx->last_bit = bit;
return 0;
}
rx->preamble_count = 0;
}
rx->last_bit = bit;
break;
case RX_SYNC:
rx->shift_reg = (rx->shift_reg << 1) | (uint16_t)bit;
if (rx->shift_reg == 0x2D4B) {
/* Sync word found — transition to DATA */
rx->phase = RX_DATA;
rx->data_len = 0;
rx->bit_count = 0;
rx->current_byte = 0;
rx->expected_bytes = -1;
}
/* Safety: if we've been in SYNC too long without finding sync word */
if (rx->total_bits > 1000 && rx->phase == RX_SYNC) {
/* Fall back to HUNT */
rx->phase = RX_HUNT;
rx->preamble_count = 0;
rx->last_bit = -1;
}
break;
case RX_DATA:
rx->current_byte = (rx->current_byte << 1) | (uint8_t)bit;
rx->bit_count++;
if (rx->bit_count == 8) {
if (rx->data_len < RX_MAX_DATA_BYTES)
rx->data[rx->data_len] = rx->current_byte;
rx->data_len++;
rx->bit_count = 0;
rx->current_byte = 0;
/* First data byte is the length field */
if (rx->data_len == 1) {
uint8_t length = rx->data[0];
/* expected total bytes = length(1) + data(length) + crc(2) */
rx->expected_bytes = 1 + length + 2;
}
if (rx->expected_bytes > 0 &&
(int)rx->data_len >= rx->expected_bytes) {
rx->phase = RX_DONE;
return 1;
}
}
break;
case RX_DONE:
return 1;
}
return 0;
}
+68
View File
@@ -0,0 +1,68 @@
#ifndef MODEM_HW_H
#define MODEM_HW_H
#include <stddef.h>
#include <stdint.h>
/* ---- Constants ---- */
#define MODEM_SAMPLE_RATE 100000.0f
#define MODEM_BAUD_RATE 50.0f
#define MODEM_SAMPLES_PER_BIT 2000
#define MODEM_CENTER_FREQ 40000.0f
#define MODEM_SHIFT 100.0f
#define MODEM_FREQ_MARK (MODEM_CENTER_FREQ + MODEM_SHIFT / 2.0f) /* 40050 Hz */
#define MODEM_FREQ_SPACE (MODEM_CENTER_FREQ - MODEM_SHIFT / 2.0f) /* 39950 Hz */
/* DMA buffer: 4000 samples total (two halves of 2000) */
#define MODEM_DMA_BUF_LEN 4000
#define MODEM_HALF_BUF_LEN 2000
/* ---- TX state machine ---- */
typedef struct {
const uint8_t *frame;
size_t frame_len;
size_t byte_idx;
int bit_idx; /* 7 down to 0 */
double phase; /* continuous phase accumulator */
int done;
} modem_tx_state_t;
void modem_tx_init(modem_tx_state_t *tx, const uint8_t *frame, size_t frame_len);
/* Fill 2000 uint16_t DAC samples (one bit). Called from DMA ISR. */
void modem_tx_fill_buffer(modem_tx_state_t *tx, uint16_t *buf);
/* ---- RX state machine ---- */
typedef enum {
RX_HUNT, /* looking for alternating preamble bits */
RX_SYNC, /* accumulating bits looking for 0x2D4B */
RX_DATA, /* receiving data bytes per length field */
RX_DONE, /* frame received (or failure) */
} modem_rx_phase_t;
#define RX_MAX_DATA_BYTES 128
typedef struct {
modem_rx_phase_t phase;
/* Bit shift register for sync detection */
uint16_t shift_reg;
int preamble_count; /* # good alternating bits seen */
/* Data accumulation */
uint8_t data[RX_MAX_DATA_BYTES];
size_t data_len;
int bit_count; /* bits accumulated in current byte */
uint8_t current_byte;
int expected_bytes; /* -1 until length byte received */
int last_bit; /* for preamble alternation check */
/* Timeout */
int idle_count; /* consecutive bits with no energy */
int total_bits; /* total bits processed */
} modem_rx_state_t;
void modem_rx_init(modem_rx_state_t *rx);
/* Process 2000 uint16_t ADC samples (one bit period). Called from DMA ISR.
* Returns 1 when a complete frame has been received. */
int modem_rx_process_buffer(modem_rx_state_t *rx, const uint16_t *buf);
#endif /* MODEM_HW_H */
+27
View File
@@ -0,0 +1,27 @@
#include "uart_print.h"
#include "hal_init.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
void uart_printf(const char *fmt, ...)
{
char buf[256];
va_list ap;
va_start(ap, fmt);
int len = vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (len > 0) {
if ((size_t)len >= sizeof(buf))
len = sizeof(buf) - 1;
HAL_UART_Transmit(&huart2, (uint8_t *)buf, (uint16_t)len, HAL_MAX_DELAY);
}
}
void uart_print_hex(const uint8_t *data, size_t len)
{
for (size_t i = 0; i < len; i++)
uart_printf("%02X ", data[i]);
uart_printf("\r\n");
}
+10
View File
@@ -0,0 +1,10 @@
#ifndef UART_PRINT_H
#define UART_PRINT_H
#include <stddef.h>
#include <stdint.h>
void uart_printf(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
void uart_print_hex(const uint8_t *data, size_t len);
#endif /* UART_PRINT_H */
+222
View File
@@ -0,0 +1,222 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "../src/modem.h"
#include "../src/packet.h"
#include "../src/channel.h"
/* ---- Minimal WAV writer ---- */
static void write_wav(const char *filename, const float *samples,
size_t num_samples, float sample_rate)
{
FILE *f = fopen(filename, "wb");
if (!f) {
fprintf(stderr, "Error: cannot open %s for writing\n", filename);
return;
}
uint32_t sr = (uint32_t)sample_rate;
uint16_t channels = 1;
uint16_t bits = 16;
uint32_t byte_rate = sr * channels * bits / 8;
uint16_t block_align = channels * bits / 8;
uint32_t data_size = (uint32_t)(num_samples * block_align);
uint32_t chunk_size = 36 + data_size;
/* RIFF header */
fwrite("RIFF", 1, 4, f);
fwrite(&chunk_size, 4, 1, f);
fwrite("WAVE", 1, 4, f);
/* fmt sub-chunk */
fwrite("fmt ", 1, 4, f);
uint32_t fmt_size = 16;
uint16_t audio_fmt = 1; /* PCM */
fwrite(&fmt_size, 4, 1, f);
fwrite(&audio_fmt, 2, 1, f);
fwrite(&channels, 2, 1, f);
fwrite(&sr, 4, 1, f);
fwrite(&byte_rate, 4, 1, f);
fwrite(&block_align, 2, 1, f);
fwrite(&bits, 2, 1, f);
/* data sub-chunk */
fwrite("data", 1, 4, f);
fwrite(&data_size, 4, 1, f);
for (size_t i = 0; i < num_samples; i++) {
float s = samples[i];
if (s > 1.0f) s = 1.0f;
if (s < -1.0f) s = -1.0f;
int16_t val = (int16_t)(s * 32767.0f);
fwrite(&val, 2, 1, f);
}
fclose(f);
printf("WAV written: %s (%zu samples, %.0f Hz)\n",
filename, num_samples, sample_rate);
}
/* ---- Hex dump helper ---- */
static void print_hex(const char *label, const uint8_t *data, size_t len)
{
printf("%s (%zu bytes):\n ", label, len);
for (size_t i = 0; i < len; i++) {
printf("%02X ", data[i]);
if ((i + 1) % 16 == 0 && i + 1 < len)
printf("\n ");
}
printf("\n");
}
/* ---- Main ---- */
int main(int argc, char *argv[])
{
const char *message = "Hello, Cat-Radio!";
float snr_db = 100.0f; /* effectively no noise */
int use_noise = 0;
int write_wav_file = 0;
/* Parse arguments */
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--snr") == 0 && i + 1 < argc) {
snr_db = (float)atof(argv[++i]);
use_noise = 1;
} else if (strcmp(argv[i], "--wav") == 0) {
write_wav_file = 1;
} else {
message = argv[i];
}
}
printf("=== CAT-Radio Simulator ===\n\n");
printf("Message: \"%s\"\n", message);
if (use_noise)
printf("Channel SNR: %.1f dB\n", snr_db);
else
printf("Channel: clean (no noise)\n");
printf("\n");
/* ---- Step 1: Packet encode ---- */
packet_t tx_pkt;
memset(&tx_pkt, 0, sizeof(tx_pkt));
tx_pkt.header.dst = 0xFF; /* broadcast */
tx_pkt.header.src = 0x01;
tx_pkt.header.seq = 0x00;
tx_pkt.header.flags = 0x00;
size_t msg_len = strlen(message);
if (msg_len > PACKET_MAX_PAYLOAD)
msg_len = PACKET_MAX_PAYLOAD;
memcpy(tx_pkt.payload, message, msg_len);
tx_pkt.payload_len = (uint8_t)msg_len;
uint8_t frame[PACKET_MAX_FRAME];
size_t frame_len = packet_encode(&tx_pkt, frame, sizeof(frame));
print_hex("Encoded frame", frame, frame_len);
printf("\n");
/* ---- Step 2: Modulate ---- */
modem_config_t cfg = modem_default_config();
size_t num_samples = modem_modulated_samples(&cfg, frame_len);
float *samples = (float *)malloc(num_samples * sizeof(float));
if (!samples) {
fprintf(stderr, "Error: cannot allocate sample buffer (%zu samples)\n",
num_samples);
return 1;
}
size_t produced = modem_modulate(&cfg, frame, frame_len, samples, num_samples);
printf("Modulated: %zu samples (%.3f seconds at %.0f Hz)\n\n",
produced, (float)produced / cfg.sample_rate, cfg.sample_rate);
/* ---- Optional: write WAV ---- */
if (write_wav_file)
write_wav("cat-radio-sim.wav", samples, produced, cfg.sample_rate);
/* ---- Step 3: Channel ---- */
if (use_noise) {
channel_add_noise(samples, produced, snr_db);
printf("Channel: added AWGN at %.1f dB SNR\n\n", snr_db);
}
/* ---- Step 4: Demodulate ---- */
uint8_t rx_frame[PACKET_MAX_FRAME];
size_t rx_len = modem_demodulate(&cfg, samples, produced,
rx_frame, sizeof(rx_frame));
free(samples);
printf("Demodulated: %zu bytes recovered\n", rx_len);
if (rx_len > 0)
print_hex("Recovered frame", rx_frame, rx_len);
printf("\n");
if (rx_len == 0) {
printf("RESULT: FAIL — no frame detected by demodulator\n");
return 1;
}
/* ---- Step 5: Packet decode ---- */
packet_t rx_pkt;
memset(&rx_pkt, 0, sizeof(rx_pkt));
int rc = packet_decode(rx_frame, rx_len, &rx_pkt);
if (rc != 0) {
printf("RESULT: FAIL — packet framing error\n");
return 1;
}
printf("Packet decode:\n");
printf(" CRC: %s\n", rx_pkt.crc_ok ? "OK" : "FAIL");
printf(" Dst: 0x%02X\n", rx_pkt.header.dst);
printf(" Src: 0x%02X\n", rx_pkt.header.src);
printf(" Seq: %u\n", rx_pkt.header.seq);
printf(" Flags: 0x%02X\n", rx_pkt.header.flags);
printf(" Payload: %u bytes\n", rx_pkt.payload_len);
if (rx_pkt.payload_len > 0) {
char recovered[PACKET_MAX_PAYLOAD + 1];
memcpy(recovered, rx_pkt.payload, rx_pkt.payload_len);
recovered[rx_pkt.payload_len] = '\0';
printf(" Message: \"%s\"\n", recovered);
}
printf("\n");
/* ---- Compare ---- */
int match = (rx_pkt.crc_ok &&
rx_pkt.payload_len == tx_pkt.payload_len &&
memcmp(rx_pkt.payload, tx_pkt.payload, tx_pkt.payload_len) == 0);
/* Bit error stats on raw frame (before packet decode) */
size_t cmp_len = rx_len < frame_len ? rx_len : frame_len;
/* Compare only the data portion (skip preamble+sync which aren't in rx_frame) */
size_t bit_errors = 0;
size_t total_bits = 0;
/* rx_frame corresponds to frame starting at byte 4 (after preamble+sync) */
for (size_t i = 0; i < cmp_len && (i + 4) < frame_len; i++) {
uint8_t diff = rx_frame[i] ^ frame[i + 4];
for (int b = 0; b < 8; b++)
if (diff & (1 << b))
bit_errors++;
total_bits += 8;
}
printf("Bit errors: %zu / %zu", bit_errors, total_bits);
if (total_bits > 0)
printf(" (BER: %.2e)", (double)bit_errors / total_bits);
printf("\n");
if (match)
printf("RESULT: PASS — message recovered successfully!\n");
else
printf("RESULT: FAIL — message mismatch or CRC error\n");
return match ? 0 : 1;
}
+45
View File
@@ -0,0 +1,45 @@
#include "channel.h"
#include <math.h>
#include <stdlib.h>
#include <time.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
/* Box-Muller transform: generate a standard normal random variable */
static float randn(void)
{
float u1 = ((float)rand() + 1.0f) / ((float)RAND_MAX + 1.0f);
float u2 = ((float)rand() + 1.0f) / ((float)RAND_MAX + 1.0f);
return sqrtf(-2.0f * logf(u1)) * cosf(2.0f * (float)M_PI * u2);
}
void channel_add_noise(float *samples, size_t num_samples, float snr_db)
{
static int seeded = 0;
if (!seeded) {
srand((unsigned)time(NULL));
seeded = 1;
}
/* Measure signal power */
double sig_power = 0.0;
for (size_t i = 0; i < num_samples; i++)
sig_power += (double)samples[i] * (double)samples[i];
sig_power /= num_samples;
/* Noise power from SNR */
double noise_power = sig_power / pow(10.0, snr_db / 10.0);
float noise_std = (float)sqrt(noise_power);
for (size_t i = 0; i < num_samples; i++)
samples[i] += noise_std * randn();
}
void channel_attenuate(float *samples, size_t num_samples, float atten_db)
{
float scale = powf(10.0f, -atten_db / 20.0f);
for (size_t i = 0; i < num_samples; i++)
samples[i] *= scale;
}
+18
View File
@@ -0,0 +1,18 @@
#ifndef CHANNEL_H
#define CHANNEL_H
#include <stddef.h>
/*
* Add white Gaussian noise to a sample buffer at the given SNR (in dB).
* Modifies samples in-place.
*/
void channel_add_noise(float *samples, size_t num_samples, float snr_db);
/*
* Apply flat attenuation (in dB, positive value = loss) to a sample buffer.
* Modifies samples in-place.
*/
void channel_attenuate(float *samples, size_t num_samples, float atten_db);
#endif /* CHANNEL_H */
+160
View File
@@ -0,0 +1,160 @@
#include "modem.h"
#include <math.h>
#include <string.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
modem_config_t modem_default_config(void)
{
modem_config_t cfg;
cfg.center_freq = 40000.0f; /* 40 kHz */
cfg.shift = 100.0f; /* 100 Hz total shift */
cfg.baud_rate = 50.0f; /* 50 baud */
cfg.sample_rate = 192000.0f; /* 192 kHz */
return cfg;
}
size_t modem_modulated_samples(const modem_config_t *cfg, size_t data_len)
{
size_t samples_per_bit = (size_t)(cfg->sample_rate / cfg->baud_rate);
return data_len * 8 * samples_per_bit;
}
size_t modem_modulate(const modem_config_t *cfg,
const uint8_t *data, size_t data_len,
float *out_samples, size_t max_samples)
{
float freq_mark = cfg->center_freq + cfg->shift / 2.0f; /* 1 */
float freq_space = cfg->center_freq - cfg->shift / 2.0f; /* 0 */
size_t samples_per_bit = (size_t)(cfg->sample_rate / cfg->baud_rate);
double phase = 0.0;
size_t idx = 0;
for (size_t i = 0; i < data_len; i++) {
for (int bit = 7; bit >= 0; bit--) {
int b = (data[i] >> bit) & 1;
float freq = b ? freq_mark : freq_space;
double phase_inc = 2.0 * M_PI * freq / cfg->sample_rate;
for (size_t s = 0; s < samples_per_bit; s++) {
if (idx >= max_samples)
return idx;
out_samples[idx++] = (float)sin(phase);
phase += phase_inc;
}
/* Keep phase in [0, 2π) to avoid precision loss */
phase = fmod(phase, 2.0 * M_PI);
}
}
return idx;
}
/* ---- Goertzel algorithm ---- */
static float goertzel_mag(const float *samples, size_t n, float target_freq,
float sample_rate)
{
float k = target_freq * (float)n / sample_rate;
float w = 2.0f * (float)M_PI * k / (float)n;
float coeff = 2.0f * cosf(w);
float s0 = 0.0f, s1 = 0.0f, s2 = 0.0f;
for (size_t i = 0; i < n; i++) {
s0 = samples[i] + coeff * s1 - s2;
s2 = s1;
s1 = s0;
}
/* Return magnitude squared (no need for sqrt) */
return s1 * s1 + s2 * s2 - coeff * s1 * s2;
}
size_t modem_demodulate(const modem_config_t *cfg,
const float *samples, size_t num_samples,
uint8_t *out_data, size_t max_data)
{
float freq_mark = cfg->center_freq + cfg->shift / 2.0f;
float freq_space = cfg->center_freq - cfg->shift / 2.0f;
size_t spb = (size_t)(cfg->sample_rate / cfg->baud_rate);
if (num_samples < spb)
return 0;
size_t total_bits = num_samples / spb;
/*
* Phase 1: Decode all bit windows from the raw stream.
* We'll search for preamble + sync in the decoded bits afterward.
*/
size_t raw_bits_cap = total_bits;
/* Stack allocation would be risky for large buffers; use simple VLA-like
* approach. For a simulator this is fine. */
uint8_t *raw_bits = (uint8_t *)__builtin_alloca(raw_bits_cap);
size_t raw_count = 0;
/* Try multiple sub-sample offsets to find best bit sync.
* In a clean (or noisy but preamble-present) signal, the preamble
* edge alignment gives us the best offset. We try a handful of
* offsets and pick the one that gives us a successful decode. */
/* For speed we first try offset=0 (ideal for our own modulator). */
size_t offsets_to_try[] = {0, spb / 4, spb / 2, 3 * spb / 4};
int num_offsets = 4;
for (int oi = 0; oi < num_offsets; oi++) {
size_t offset = offsets_to_try[oi];
raw_count = 0;
for (size_t pos = offset; pos + spb <= num_samples; pos += spb) {
float m_mark = goertzel_mag(samples + pos, spb, freq_mark, cfg->sample_rate);
float m_space = goertzel_mag(samples + pos, spb, freq_space, cfg->sample_rate);
raw_bits[raw_count++] = (m_mark >= m_space) ? 1 : 0;
}
/* Phase 2: Search for preamble (0xAA = 10101010) then sync word
* (0x2D 0x4B) in the bit stream. */
/* We look for at least 8 bits of alternating 1-0 pattern followed
* by the 16-bit sync word 0x2D4B. */
uint16_t sync_word = 0x2D4B;
for (size_t bi = 0; bi + 16 <= raw_count; bi++) {
/* Check for sync word at position bi */
uint16_t word = 0;
for (int j = 0; j < 16; j++)
word = (word << 1) | raw_bits[bi + j];
if (word != sync_word)
continue;
/* Verify some preamble bits before sync (at least 8 bits of 0xAA) */
if (bi >= 8) {
int preamble_ok = 1;
for (size_t p = bi - 8; p + 1 < bi; p++) {
if (raw_bits[p] == raw_bits[p + 1]) {
preamble_ok = 0;
break;
}
}
if (!preamble_ok)
continue;
}
/* Found sync — remaining bits are the data frame */
size_t data_start = bi + 16;
size_t bits_remaining = raw_count - data_start;
size_t bytes_available = bits_remaining / 8;
size_t out_len = bytes_available < max_data ? bytes_available : max_data;
for (size_t b = 0; b < out_len; b++) {
uint8_t byte = 0;
for (int j = 0; j < 8; j++)
byte = (byte << 1) | raw_bits[data_start + b * 8 + j];
out_data[b] = byte;
}
return out_len;
}
}
return 0; /* No frame detected */
}
+42
View File
@@ -0,0 +1,42 @@
#ifndef MODEM_H
#define MODEM_H
#include <stddef.h>
#include <stdint.h>
typedef struct {
float center_freq; /* Hz — center between mark and space */
float shift; /* Hz — total shift (mark = center + shift/2) */
float baud_rate; /* symbols per second */
float sample_rate; /* samples per second */
} modem_config_t;
/* Default config: 40 kHz center, 100 Hz shift, 50 baud, 192 kHz sample rate */
modem_config_t modem_default_config(void);
/*
* FSK modulate a byte array into a float sample buffer.
* Continuous-phase 2-FSK. Mark (1) = center + shift/2, Space (0) = center - shift/2.
*
* out_samples : caller-allocated buffer (use modem_modulated_samples() to size it)
* returns : number of samples written
*/
size_t modem_modulate(const modem_config_t *cfg,
const uint8_t *data, size_t data_len,
float *out_samples, size_t max_samples);
/* How many samples will modem_modulate() produce for data_len bytes? */
size_t modem_modulated_samples(const modem_config_t *cfg, size_t data_len);
/*
* FSK demodulate a float sample buffer back into bytes.
* Uses Goertzel algorithm, preamble detection, and bit-sync.
*
* out_data : caller-allocated buffer
* returns : number of bytes recovered (0 if no valid frame detected)
*/
size_t modem_demodulate(const modem_config_t *cfg,
const float *samples, size_t num_samples,
uint8_t *out_data, size_t max_data);
#endif /* MODEM_H */
+100
View File
@@ -0,0 +1,100 @@
#include "packet.h"
#include <string.h>
uint16_t crc16_ccitt(const uint8_t *data, size_t len)
{
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= (uint16_t)data[i] << 8;
for (int j = 0; j < 8; j++) {
if (crc & 0x8000)
crc = (crc << 1) ^ 0x1021;
else
crc <<= 1;
}
}
return crc;
}
size_t packet_encode(const packet_t *pkt,
uint8_t *out_frame, size_t max_frame)
{
size_t payload_len = pkt->payload_len;
if (payload_len > PACKET_MAX_PAYLOAD)
payload_len = PACKET_MAX_PAYLOAD;
size_t frame_len = PACKET_OVERHEAD + payload_len;
if (frame_len > max_frame)
return 0;
size_t i = 0;
/* Preamble */
out_frame[i++] = PACKET_PREAMBLE_0;
out_frame[i++] = PACKET_PREAMBLE_1;
/* Sync word */
out_frame[i++] = PACKET_SYNC_0;
out_frame[i++] = PACKET_SYNC_1;
/* Length byte: header(4) + payload */
uint8_t length = 4 + (uint8_t)payload_len;
out_frame[i++] = length;
/* Header */
out_frame[i++] = pkt->header.dst;
out_frame[i++] = pkt->header.src;
out_frame[i++] = pkt->header.seq;
out_frame[i++] = pkt->header.flags;
/* Payload */
memcpy(&out_frame[i], pkt->payload, payload_len);
i += payload_len;
/* CRC over length + header + payload */
uint16_t crc = crc16_ccitt(&out_frame[4], 1 + 4 + payload_len);
out_frame[i++] = (crc >> 8) & 0xFF; /* CRC high */
out_frame[i++] = crc & 0xFF; /* CRC low */
return i;
}
int packet_decode(const uint8_t *raw_frame, size_t frame_len,
packet_t *pkt)
{
/* raw_frame starts at the length byte (after preamble + sync) */
if (frame_len < 7) /* length(1) + header(4) + crc(2) minimum */
return -1;
uint8_t length = raw_frame[0];
if (length < 4)
return -1;
uint8_t payload_len = length - 4;
if (payload_len > PACKET_MAX_PAYLOAD)
return -1;
/* Check we have enough bytes: length(1) + header(4) + payload + crc(2) */
size_t needed = 1 + 4 + payload_len + 2;
if (frame_len < needed)
return -1;
/* Header */
pkt->header.dst = raw_frame[1];
pkt->header.src = raw_frame[2];
pkt->header.seq = raw_frame[3];
pkt->header.flags = raw_frame[4];
/* Payload */
pkt->payload_len = payload_len;
memcpy(pkt->payload, &raw_frame[5], payload_len);
/* CRC check: over length + header + payload */
size_t crc_data_len = 1 + 4 + payload_len;
uint16_t crc_calc = crc16_ccitt(raw_frame, crc_data_len);
uint16_t crc_recv = ((uint16_t)raw_frame[crc_data_len] << 8)
| raw_frame[crc_data_len + 1];
pkt->crc_ok = (crc_calc == crc_recv) ? 1 : 0;
return 0;
}
+51
View File
@@ -0,0 +1,51 @@
#ifndef PACKET_H
#define PACKET_H
#include <stddef.h>
#include <stdint.h>
#define PACKET_PREAMBLE_0 0xAA
#define PACKET_PREAMBLE_1 0xAA
#define PACKET_SYNC_0 0x2D
#define PACKET_SYNC_1 0x4B
#define PACKET_MAX_PAYLOAD 64
/* Preamble(2) + Sync(2) + Length(1) + Header(4) + Payload(0-64) + CRC(2) */
#define PACKET_OVERHEAD 11
#define PACKET_MAX_FRAME (PACKET_OVERHEAD + PACKET_MAX_PAYLOAD)
typedef struct {
uint8_t dst;
uint8_t src;
uint8_t seq;
uint8_t flags;
} packet_header_t;
typedef struct {
packet_header_t header;
uint8_t payload[PACKET_MAX_PAYLOAD];
uint8_t payload_len;
int crc_ok; /* set by packet_decode */
} packet_t;
/*
* Encode a packet into a framed byte array ready for modulation.
* Returns the total frame length written to out_frame.
*/
size_t packet_encode(const packet_t *pkt,
uint8_t *out_frame, size_t max_frame);
/*
* Decode a framed byte array (starting after preamble + sync detection)
* into a packet struct. Verifies CRC and sets pkt->crc_ok.
* raw_frame should point to the length byte (first byte after sync word).
* Returns 0 on success, -1 on framing error.
*/
int packet_decode(const uint8_t *raw_frame, size_t frame_len,
packet_t *pkt);
/* CRC-16-CCITT (polynomial 0x1021, init 0xFFFF) */
uint16_t crc16_ccitt(const uint8_t *data, size_t len);
#endif /* PACKET_H */