init commit
This commit is contained in:
+23
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 */
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 */
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 */
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 */
|
||||||
Reference in New Issue
Block a user