commit 2c87c57651d56beca15a5f3417f0b96ecd5217f5 Author: Paul Walko Date: Thu Feb 5 00:37:55 2026 -0700 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c46ebd --- /dev/null +++ b/.gitignore @@ -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 diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..1bb1df6 --- /dev/null +++ b/CONTEXT.md @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0732a03 --- /dev/null +++ b/Makefile @@ -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 diff --git a/firmware/include/stm32l4xx_hal_conf.h b/firmware/include/stm32l4xx_hal_conf.h new file mode 100644 index 0000000..4177a5b --- /dev/null +++ b/firmware/include/stm32l4xx_hal_conf.h @@ -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 */ diff --git a/firmware/platformio.ini b/firmware/platformio.ini new file mode 100644 index 0000000..77b274a --- /dev/null +++ b/firmware/platformio.ini @@ -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> diff --git a/firmware/src/hal_init.c b/firmware/src/hal_init.c new file mode 100644 index 0000000..c04f5f3 --- /dev/null +++ b/firmware/src/hal_init.c @@ -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); +} diff --git a/firmware/src/hal_init.h b/firmware/src/hal_init.h new file mode 100644 index 0000000..3fa3837 --- /dev/null +++ b/firmware/src/hal_init.h @@ -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 */ diff --git a/firmware/src/main.c b/firmware/src/main.c new file mode 100644 index 0000000..66fb5ed --- /dev/null +++ b/firmware/src/main.c @@ -0,0 +1,185 @@ +#include "stm32l4xx_hal.h" +#include "hal_init.h" +#include "modem_hw.h" +#include "uart_print.h" +#include "packet.h" +#include + +/* ---- 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(); +} diff --git a/firmware/src/modem_hw.c b/firmware/src/modem_hw.c new file mode 100644 index 0000000..7661f17 --- /dev/null +++ b/firmware/src/modem_hw.c @@ -0,0 +1,196 @@ +#include "modem_hw.h" +#include +#include + +#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; +} diff --git a/firmware/src/modem_hw.h b/firmware/src/modem_hw.h new file mode 100644 index 0000000..2f316cb --- /dev/null +++ b/firmware/src/modem_hw.h @@ -0,0 +1,68 @@ +#ifndef MODEM_HW_H +#define MODEM_HW_H + +#include +#include + +/* ---- 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 */ diff --git a/firmware/src/uart_print.c b/firmware/src/uart_print.c new file mode 100644 index 0000000..71974b0 --- /dev/null +++ b/firmware/src/uart_print.c @@ -0,0 +1,27 @@ +#include "uart_print.h" +#include "hal_init.h" +#include +#include +#include + +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"); +} diff --git a/firmware/src/uart_print.h b/firmware/src/uart_print.h new file mode 100644 index 0000000..9cc4458 --- /dev/null +++ b/firmware/src/uart_print.h @@ -0,0 +1,10 @@ +#ifndef UART_PRINT_H +#define UART_PRINT_H + +#include +#include + +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 */ diff --git a/sim/main.c b/sim/main.c new file mode 100644 index 0000000..0d25be2 --- /dev/null +++ b/sim/main.c @@ -0,0 +1,222 @@ +#include +#include +#include +#include + +#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; +} diff --git a/src/channel.c b/src/channel.c new file mode 100644 index 0000000..58b7c63 --- /dev/null +++ b/src/channel.c @@ -0,0 +1,45 @@ +#include "channel.h" +#include +#include +#include + +#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; +} diff --git a/src/channel.h b/src/channel.h new file mode 100644 index 0000000..f1f761d --- /dev/null +++ b/src/channel.h @@ -0,0 +1,18 @@ +#ifndef CHANNEL_H +#define CHANNEL_H + +#include + +/* + * 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 */ diff --git a/src/modem.c b/src/modem.c new file mode 100644 index 0000000..6d36aaf --- /dev/null +++ b/src/modem.c @@ -0,0 +1,160 @@ +#include "modem.h" +#include +#include + +#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 */ +} diff --git a/src/modem.h b/src/modem.h new file mode 100644 index 0000000..3da5b75 --- /dev/null +++ b/src/modem.h @@ -0,0 +1,42 @@ +#ifndef MODEM_H +#define MODEM_H + +#include +#include + +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 */ diff --git a/src/packet.c b/src/packet.c new file mode 100644 index 0000000..983b208 --- /dev/null +++ b/src/packet.c @@ -0,0 +1,100 @@ +#include "packet.h" +#include + +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; +} diff --git a/src/packet.h b/src/packet.h new file mode 100644 index 0000000..4dc6f0e --- /dev/null +++ b/src/packet.h @@ -0,0 +1,51 @@ +#ifndef PACKET_H +#define PACKET_H + +#include +#include + +#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 */