arduino-tnc

git clone https://git.bradleygannon.com/arduino-tnc.git
Log | Files | Refs

commit 3e10c5cd7da55cb8bc3542d973f8334a8262b562
parent 76a7d242c117b572f995d520d46e6c0ab0dfe2ae
Author: Bradley Gannon <bradley@bradleygannon.com>
Date:   Tue, 26 May 2026 22:14:28 -0400

Implement partial draft receiver

Diffstat:
MCargo.lock | 7+++++++
MCargo.toml | 1+
Msrc/main.rs | 67+++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/rx.rs | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
4 files changed, 153 insertions(+), 20 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -41,6 +41,7 @@ dependencies = [ "heapless", "nb 1.1.0", "panic-halt", + "trig-const", "ufmt", ] @@ -863,6 +864,12 @@ dependencies = [ ] [[package]] +name = "trig-const" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffec2fe2ed1e10dac46d8530afb395cae635fa02ef03d7fff1c211e94f9059fc" + +[[package]] name = "ufmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -18,6 +18,7 @@ embedded-hal = "1.0" heapless = "0.9.2" nb = "1.1.0" panic-halt = "1.0.0" +trig-const = "0.4.0" ufmt = "0.2.0" [dependencies.arduino-hal] diff --git a/src/main.rs b/src/main.rs @@ -11,22 +11,34 @@ use core::hint::spin_loop; use arduino_hal::clock::MHz16; use arduino_hal::hal::usart::{Event, Usart0}; -use arduino_hal::Adc; +use arduino_hal::{pac, Adc}; +use avr_device::generic::Periph; use avr_device::interrupt::{self, Mutex}; use kiss::{KissAction, KissTncState}; use panic_halt as _; use rx::Receiver; use tx::Transmitter; +type TC0 = Periph<pac::tc0::RegisterBlock, 53>; +type TC1 = Periph<pac::tc1::RegisterBlock, 54>; + struct Tnc { state: TncState, serial: Option<Usart0<MHz16>>, receiver: Receiver, transmitter: Transmitter, kiss_state: KissTncState, + sample_timer: Option<TC0>, + symbol_timer: Option<TC1>, } impl Tnc { + // 52 * 8 = 416 cycles; at 16 MHz this gives a period of 26 µs (about 38460 Hz) + const SAMPLE_TIMER_COMPARE: u8 = 52; + // this should theoretically be 13_333 cycles, but in practice we have to go a little faster to + // account for overhead; this value seems to work well + const SYMBOL_TIMER_COMPARE: u16 = 13_300; + const fn default() -> Tnc { Tnc { state: TncState::Receiving, @@ -34,6 +46,8 @@ impl Tnc { receiver: Receiver::default(), transmitter: Transmitter::default(), kiss_state: KissTncState::Idle, + sample_timer: None, + symbol_timer: None, } } pub(crate) fn handle_usart_byte(&mut self, byte: u8) { @@ -73,7 +87,21 @@ fn TIMER0_COMPA() { interrupt::free(|cs| { let mut tnc = TNC.borrow(cs).borrow_mut(); match tnc.state { - TncState::Receiving => tnc.receiver.sample_update(), + TncState::Receiving => { + tnc.receiver.sample_update(); + if tnc.receiver.is_at_symbol_transition() { + if let Some(ref mut symbol_timer) = tnc.symbol_timer { + symbol_timer + .tcnt1() + .write(|w| w.set(Tnc::SYMBOL_TIMER_COMPARE / 2)); + } + } + if let Some(byte) = tnc.receiver.get_output() { + if let Some(ref mut serial) = tnc.serial { + let _ = ufmt::uwriteln!(serial, "0x{:02x}", byte); + } + } + } TncState::Transmitting => { tnc.transmitter.sample_update(); if tnc.transmitter.is_done() { @@ -114,8 +142,27 @@ fn main() -> ! { let mut serial = arduino_hal::default_serial!(dp, pins, 57600); serial.listen(Event::RxComplete); + let sample_timer = dp.TC0; + sample_timer.tccr0a().write(|w| w.wgm0().ctc()); + sample_timer.tccr0b().write(|w| w.cs0().prescale_8()); + sample_timer + .ocr0a() + .write(|w| w.set(Tnc::SAMPLE_TIMER_COMPARE)); + sample_timer.timsk0().write(|w| w.ocie0a().set_bit()); + + let symbol_timer = dp.TC1; + symbol_timer + .tccr1b() + .write(|w| w.wgm1().set(0b01).cs1().direct()); + symbol_timer + .ocr1a() + .write(|w| w.set(Tnc::SYMBOL_TIMER_COMPARE)); + symbol_timer.timsk1().write(|w| w.ocie1a().set_bit()); + interrupt::free(|cs| { let mut tnc = TNC.borrow(cs).borrow_mut(); + tnc.sample_timer = Some(sample_timer); + tnc.symbol_timer = Some(symbol_timer); tnc.receiver .initialize(Adc::new(dp.ADC, Default::default()), pins.a0); tnc.transmitter.initialize( @@ -127,22 +174,6 @@ fn main() -> ! { ); }); - let dac_timer = dp.TC0; - dac_timer.tccr0a().write(|w| w.wgm0().ctc()); - dac_timer.tccr0b().write(|w| w.cs0().prescale_8()); - // 52 * 8 = 416 cycles; at 16 MHz this gives a period of 26 µs (about 38460 Hz) - dac_timer.ocr0a().write(|w| w.set(52)); - dac_timer.timsk0().write(|w| w.ocie0a().set_bit()); - - let symbol_timer = dp.TC1; - symbol_timer - .tccr1b() - .write(|w| w.wgm1().set(0b01).cs1().direct()); - // this should theoretically be 13_333 cycles, but in practice we have to go a little faster to - // account for overhead; this value seems to work well - symbol_timer.ocr1a().write(|w| w.set(13_300)); - symbol_timer.timsk1().write(|w| w.ocie1a().set_bit()); - unsafe { avr_device::interrupt::enable() }; loop { diff --git a/src/rx.rs b/src/rx.rs @@ -1,3 +1,5 @@ +use core::f32::consts::TAU; + use arduino_hal::{ hal::port::PC0, port::{ @@ -6,27 +8,119 @@ use arduino_hal::{ }, Adc, }; +use trig_const::cos; + +#[derive(PartialEq, Clone, Copy)] +enum Symbol { + Mark, + Space, +} + +struct GoertzelFilter { + coeff: f32, + s0: f32, + s1: f32, + s2: f32, + energy: f32, +} + +impl GoertzelFilter { + const fn new(frequency: f32, sample_rate: f32) -> Self { + GoertzelFilter { + coeff: 2. * cos((TAU * frequency / sample_rate) as f64) as f32, + s0: 0., + s1: 0., + s2: 0., + energy: 0., + } + } + fn update(&mut self, x: f32) { + self.s0 = x + self.coeff * self.s1 - self.s2; + self.s2 = self.s1; + self.s1 = self.s0; + self.energy = self.s1 * self.s1 + self.s2 * self.s2 - self.coeff * self.s1 * self.s2; + } + fn energy(&self) -> f32 { + self.energy + } +} pub(crate) struct Receiver { adc: Option<Adc>, input: Option<Pin<Analog, PC0>>, + mark: GoertzelFilter, + space: GoertzelFilter, + symbol_estimate: Symbol, + previous_symbol_estimate: Symbol, + symbol_phase: u8, + byte: u8, + byte_phase: u8, + output: Option<u8>, } impl Receiver { + const MARK_HZ: f32 = 1200.; + const SPACE_HZ: f32 = 2200.; + const SAMPLE_RATE_HZ: f32 = 38400.; + const SYMBOL_RATE_HZ: f32 = 1200.; + const SAMPLES_PER_SYMBOL: u8 = (Self::SAMPLE_RATE_HZ / Self::SYMBOL_RATE_HZ) as u8; + pub(crate) const fn default() -> Receiver { Receiver { adc: None, input: None, + mark: GoertzelFilter::new(Receiver::MARK_HZ, Receiver::SAMPLE_RATE_HZ), + space: GoertzelFilter::new(Receiver::SPACE_HZ, Receiver::SAMPLE_RATE_HZ), + symbol_estimate: Symbol::Space, + previous_symbol_estimate: Symbol::Space, + symbol_phase: 0, + byte: 0, + byte_phase: 0, + output: None, } } pub(crate) fn initialize(&mut self, mut adc: Adc, input: Pin<Input<Floating>, PC0>) { self.input = Some(input.into_analog_input(&mut adc)); self.adc = Some(adc); } + pub(crate) fn is_at_symbol_transition(&self) -> bool { + self.symbol_phase == 0 + } + pub(crate) fn get_output(&self) -> Option<u8> { + self.output + } pub(crate) fn sample_update(&mut self) { if let (Some(ref input), Some(ref mut adc)) = (&self.input, &mut self.adc) { - let _new_sample = input.analog_read(adc); + let new_sample = input.analog_read(adc) as f32 / 512. - 0.5; + self.mark.update(new_sample); + self.space.update(new_sample); + let new_symbol_estimate = if self.mark.energy() > self.space.energy() { + Symbol::Mark + } else { + Symbol::Space + }; + if new_symbol_estimate != self.symbol_estimate { + self.symbol_phase = 0; + } else { + self.symbol_phase = self.symbol_phase.saturating_add(1) % Self::SAMPLES_PER_SYMBOL; + } + self.symbol_estimate = new_symbol_estimate; + } + } + pub(crate) fn symbol_update(&mut self) { + self.byte <<= 1; + if self.symbol_estimate == self.previous_symbol_estimate { + self.byte += 1; + } + self.byte_phase = (self.byte_phase + 1) % 8; + if self.byte == 0x7e { + self.byte_phase = 0; + } + if self.byte_phase == 0 { + self.output = Some(self.byte); + } else { + self.output = None; } + self.previous_symbol_estimate = self.symbol_estimate; } - pub(crate) fn symbol_update(&mut self) {} }