commit 3e10c5cd7da55cb8bc3542d973f8334a8262b562
parent 76a7d242c117b572f995d520d46e6c0ab0dfe2ae
Author: Bradley Gannon <bradley@bradleygannon.com>
Date: Tue, 26 May 2026 22:14:28 -0400
Implement partial draft receiver
Diffstat:
| M | Cargo.lock | | | 7 | +++++++ |
| M | Cargo.toml | | | 1 | + |
| M | src/main.rs | | | 67 | +++++++++++++++++++++++++++++++++++++++++++++++++------------------ |
| M | src/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) {}
}