From e04fec33f079db4c5d42aa412f7b670784ec1b68 Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Mon, 13 Mar 2023 23:14:32 +0100 Subject: Add WSPR TX to eval-clock-cw-tx --- sw/eval-clock-cw-tx/src/main.rs | 156 +++++++++++++++++++++++++++--------- sw/eval-clock-cw-tx/src/si_clock.rs | 36 +++++---- sw/eval-clock-cw-tx/src/state.rs | 11 ++- sw/eval-clock-cw-tx/src/ui.rs | 4 +- sw/eval-clock-cw-tx/src/usb.rs | 45 +++++++---- 5 files changed, 181 insertions(+), 71 deletions(-) (limited to 'sw/eval-clock-cw-tx/src') diff --git a/sw/eval-clock-cw-tx/src/main.rs b/sw/eval-clock-cw-tx/src/main.rs index b256926..52193b4 100644 --- a/sw/eval-clock-cw-tx/src/main.rs +++ b/sw/eval-clock-cw-tx/src/main.rs @@ -25,6 +25,8 @@ #![no_main] #![no_std] use core::mem::MaybeUninit; +use core::fmt::Write; +use arrayvec::ArrayString; use cortex_m_rt::ExceptionFrame; use cortex_m_semihosting::hprintln; use panic_semihosting as _; @@ -51,12 +53,23 @@ pub mod si_clock; use state::*; -const TICKS_PER_SECOND : u32 = 100; +// One WSPR symbol is 256/375s +const WSPR_SYMBOL_TICKS : u32 = 256; +// TIM2 runs at that rate, and increments TICK_COUNTER +const TICKS_PER_SECOND : u32 = 375; + +// See README.md +const WSPR_SYMBOLS : [u8; 162] = [ + 1, 1, 2, 0, 0, 2, 2, 2, 1, 2, 0, 0, 3, 3, 1, 0, 2, 2, 3, 2, 0, 1, 0, 1, 3, 3, 1, 0, 2, 2, 0, 0, 2, 0, 3, 2, 0, 3, 0, + 1, 2, 0, 2, 0, 2, 0, 3, 2, 1, 1, 0, 2, 3, 3, 0, 3, 0, 2, 2, 3, 1, 0, 3, 0, 2, 2, 0, 1, 3, 2, 3, 0, 3, 2, 3, 0, 1, 0, + 0, 3, 0, 0, 1, 2, 1, 3, 0, 2, 2, 1, 3, 2, 3, 0, 3, 2, 0, 0, 3, 2, 0, 0, 0, 2, 3, 2, 0, 1, 2, 0, 1, 1, 1, 0, 3, 1, 2, + 2, 1, 1, 0, 3, 2, 2, 2, 1, 1, 1, 0, 0, 2, 2, 2, 3, 0, 3, 2, 2, 3, 3, 0, 0, 2, 2, 2, 2, 0, 3, 3, 0, 1, 2, 1, 3, 0, 0, + 0, 1, 1, 2, 2, 0]; struct SharedWithISR { state: State, last_sequence_state_change: u32, - feldhell_ptt: bool, + usb_data_transmit: usb::Transmit, cw_ptt_timestamp: u32, cw_key_out_n: gpio::gpioa::PA15>, ui: ui::UI, @@ -66,7 +79,10 @@ struct SharedWithISR { cw_paddle_ring: gpio::gpiob::PB9>, ptt_out: gpio::gpiob::PB3>, seq_switch: gpio::gpiob::PB5>, - led : gpio::gpiob::PB14>, + led: gpio::gpiob::PB14>, + + wspr_symbol_ix: usize, + wspr_symbol_duration_ticks: u32, } static mut SHARED: MaybeUninit = MaybeUninit::uninit(); @@ -188,13 +204,15 @@ fn main() -> ! { *shared = SharedWithISR { state: State::new(), last_sequence_state_change: 0, - feldhell_ptt: false, + usb_data_transmit: usb::Transmit::None, cw_ptt_timestamp: 0, cw_key_out_n, ui, cw_pwm, cw_keyer: cw::Keyer::new(12, TICKS_PER_SECOND), cw_paddle_tip, cw_paddle_ring, ptt_out, seq_switch, led, + wspr_symbol_ix: 0, + wspr_symbol_duration_ticks: 0, }; si_clock::SiClock::new(i2c_busmanager.acquire_i2c(), 0, shared.state.vfo_display()) @@ -219,32 +237,70 @@ fn main() -> ! { unsafe { pac::NVIC::unmask(pac::Interrupt::TIM2); } let mut last_disp_update_counter = 1; - let mut previous_usb_freq = usb.frequency; let mut previous_vfo = 0; let mut previous_state = SequenceState::Rx; - loop { - let mut update_disp_required = false; + const MESSAGE_LEN: usize = 32; + let mut usb_message: ArrayString:: = ArrayString::new(); + loop { usb.handle(); - if previous_usb_freq != usb.frequency { - previous_usb_freq = usb.frequency; + let (state_copy, update_siclock_required) = cortex_m::interrupt::free(|_cs| { + let shared = unsafe { &mut *SHARED.as_mut_ptr() }; - cortex_m::interrupt::free(|_cs| { - let shared = unsafe { &mut *SHARED.as_mut_ptr() }; - shared.state.set_vfo(usb.frequency); - }); + if shared.wspr_symbol_ix == WSPR_SYMBOLS.len() && shared.wspr_symbol_duration_ticks == WSPR_SYMBOL_TICKS { + usb.clear_transmit(); + shared.wspr_symbol_ix = 0; + shared.wspr_symbol_duration_ticks = 0; + } - update_disp_required = true; - } + let mut update_siclock_required = false; - let state = cortex_m::interrupt::free(|_cs| unsafe { - let shared = SHARED.as_mut_ptr(); - (*shared).feldhell_ptt = usb.is_transmit(); - (*shared).state.clone() + match shared.state.mode { + Mode::CW(_) => { + let vfo = shared.state.vfo_display(); + if previous_vfo != vfo || previous_state != shared.state.sequence_state { + shared.state.set_vfo(vfo); + update_siclock_required = true; + } + previous_vfo = vfo; + } + Mode::FeldHell => { + if let Some(f) = usb.take_frequency() { + shared.state.set_vfo(f); + update_siclock_required = true; + } + }, + Mode::WSPR => { + if shared.state.wspr_freq_offset_updated { + update_siclock_required = true; + } + shared.state.wspr_freq_offset_updated = false; + } + }; + shared.usb_data_transmit = usb.transmit(); + + (shared.state.clone(), update_siclock_required) }); + if update_siclock_required { + let f = state_copy.vfo_siclock() * 100 + + match state_copy.sequence_state { + SequenceState::Tx(SequenceMode::WSPR) => state_copy.wspr_freq_offset_centihz, + _ => 0, + }; + + usb_message.clear(); + if write!(&mut usb_message, "F{}\n", f).is_ok() { + usb.send_message(usb_message.as_bytes()); + } + siclock.set_vfo_centihertz(f); + } + + previous_state = state_copy.sequence_state.clone(); + + let mut update_disp_required = update_siclock_required; let encoder_count : u16 = qei.count(); if encoder_count != last_encoder_count { @@ -262,26 +318,19 @@ fn main() -> ! { update_disp_required = true; } - match (previous_state.clone(), state.sequence_state.clone()) { + match (previous_state.clone(), state_copy.sequence_state.clone()) { (SequenceState::Rx, SequenceState::Switching(_)) => usb.send_transmit(), (SequenceState::Switching(_), SequenceState::Rx) => usb.send_receive(), _ => (), } - let vfo = state.vfo_display(); - if previous_vfo != vfo || previous_state != state.sequence_state { - siclock.set_vfo(state.vfo_siclock()); - } - previous_vfo = vfo; - previous_state = state.sequence_state.clone(); - - if last_disp_update_counter != state.update_disp_counter { + if last_disp_update_counter != state_copy.update_disp_counter { update_disp_required = true; - last_disp_update_counter = state.update_disp_counter; + last_disp_update_counter = state_copy.update_disp_counter; } if update_disp_required { - ui::update_disp(&mut lcd, &state, &mut delay); + ui::update_disp(&mut lcd, &state_copy, &mut delay); } last_encoder_count = encoder_count; @@ -321,14 +370,24 @@ fn TIM2() { } }, Mode::FeldHell => { - shared.feldhell_ptt + shared.usb_data_transmit == usb::Transmit::FELDHELL + } + Mode::WSPR => { + if shared.usb_data_transmit == usb::Transmit::WSPR && + shared.wspr_symbol_ix != WSPR_SYMBOLS.len() && + shared.wspr_symbol_duration_ticks != WSPR_SYMBOL_TICKS { + true + } + else { + false + } } }; let cw_beep = match shared.state.mode { Mode::CW(CWMode::StraightKey) => cw_paddle_tip_low, Mode::CW(CWMode::Iambic) => shared.cw_keyer.tick(*ticks, cw_paddle_tip_low, cw_paddle_ring_low), - Mode::FeldHell => false, // Done in usb.c + _ => false, }; let next_state = match shared.state.sequence_state { @@ -336,11 +395,10 @@ fn TIM2() { shared.ptt_out.set_low(); shared.seq_switch.set_low(); if ptt { - if shared.state.mode == Mode::FeldHell { - SequenceState::Switching(SequenceMode::FeldHell) - } - else { - SequenceState::Switching(SequenceMode::CW) + match shared.state.mode { + state::Mode::WSPR => SequenceState::Switching(SequenceMode::WSPR), + state::Mode::FeldHell => SequenceState::Switching(SequenceMode::FeldHell), + state::Mode::CW(_) => SequenceState::Switching(SequenceMode::CW), } } else { @@ -385,6 +443,30 @@ fn TIM2() { SequenceState::Tx(SequenceMode::FeldHell) => { shared.led.set_low(); }, + SequenceState::Tx(SequenceMode::WSPR) => { + shared.cw_key_out_n.set_low(); + shared.cw_pwm.off(); + shared.led.set_low(); + + if shared.wspr_symbol_duration_ticks < WSPR_SYMBOL_TICKS { + shared.wspr_symbol_duration_ticks += 1; + } + + if shared.wspr_symbol_duration_ticks == WSPR_SYMBOL_TICKS { + if shared.wspr_symbol_ix < WSPR_SYMBOLS.len() { + shared.state.wspr_freq_offset_centihz = WSPR_SYMBOLS[shared.wspr_symbol_ix] as u32 * 100 * 375 / 256; + shared.state.wspr_freq_offset_updated = true; + shared.wspr_symbol_ix += 1; + } + else { + shared.state.wspr_freq_offset_centihz = 0; + } + + if shared.wspr_symbol_ix != WSPR_SYMBOLS.len() { + shared.wspr_symbol_duration_ticks = 0; + } + } + }, _ => { shared.led.set_high(); shared.cw_pwm.off(); diff --git a/sw/eval-clock-cw-tx/src/si_clock.rs b/sw/eval-clock-cw-tx/src/si_clock.rs index 6de4c25..4bd2686 100644 --- a/sw/eval-clock-cw-tx/src/si_clock.rs +++ b/sw/eval-clock-cw-tx/src/si_clock.rs @@ -1,7 +1,7 @@ /* The MIT License (MIT) - Copyright (c) 2020 Matthias P. Braendli + Copyright (c) 2023 Matthias P. Braendli Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -26,10 +26,10 @@ use core::convert::TryInto; use si5351::{Si5351, Si5351Device}; use embedded_hal::blocking::i2c::{WriteRead, Write}; -const REF_CLOCK : u32 = 25_000_000; +const REF_CLOCK_CHZ : u32 = 25_000_000 * 100; const PLL_A_MULT : u32 = 32; -fn gcd(x: u32, y: u32) -> u32 { +fn gcd(x: u64, y: u64) -> u64 { let mut x = x; let mut y = y; while y != 0 { @@ -40,30 +40,32 @@ fn gcd(x: u32, y: u32) -> u32 { x } -fn clock_settings_for_pll(freq: u32, pll: u32) -> (u16, u32, u32) { +// All calculations in centihertz + +fn clock_settings_for_pll(freq: u64, pll: u64) -> (u16, u32, u32) { let a = pll / freq; let b = pll - (a * freq); let gcd = gcd(b, freq); let b = b / gcd; let c = freq / gcd; - (a.try_into().unwrap(), b, c) + (a.try_into().unwrap(), b.try_into().unwrap(), c.try_into().unwrap()) } -fn clock_settings_with_pll_calculation(freq: u32) -> (u16, u8, u32, u32) { - let mut divider : u32 = 900_000_000 / freq; // Calculate the division ratio. 900,000,000 is the maximum internal +fn clock_settings_with_pll_calculation(freq_chz: u32) -> (u16, u8, u32, u32) { + let mut divider : u32 = (100*900_000_000u64 / freq_chz as u64).try_into().unwrap(); // Calculate the division ratio. 900,000,000 is the maximum internal if (divider % 2) == 1 { divider -= 1 // Ensure an even integer division ratio } - let pll_freq = divider * freq; + let pll_freq : u128 = divider as u128 * freq_chz as u128; // mult is an integer that must be in the range 15..90 - let mult = pll_freq / REF_CLOCK; - let l = pll_freq % REF_CLOCK; + let mult = pll_freq / (REF_CLOCK_CHZ as u128); + let l : u32 = (pll_freq % (REF_CLOCK_CHZ as u128)).try_into().unwrap(); let denom = 1048575; - let num = f64::from(l) * f64::from(denom) / f64::from(REF_CLOCK); + let num = f64::from(l) * f64::from(denom) / f64::from(REF_CLOCK_CHZ); (divider.try_into().unwrap(), mult.try_into().unwrap(), num as u32, denom) } @@ -74,7 +76,7 @@ fn set_bfo(siclock: &mut dyn Si5351, freq: u32) -> Result<(), si5351::Error> siclock.set_clock_enabled(si5351::ClockOutput::Clk2, false); } else { - let (a, b, c) = clock_settings_for_pll(freq, PLL_A_MULT * REF_CLOCK); + let (a, b, c) = clock_settings_for_pll(freq.into(), PLL_A_MULT as u64 * REF_CLOCK_CHZ as u64); siclock.setup_multisynth(si5351::Multisynth::MS2, a, b, c, si5351::OutputDivider::Div1)?; siclock.select_clock_pll(si5351::ClockOutput::Clk2, si5351::PLL::A); siclock.set_clock_enabled(si5351::ClockOutput::Clk2, true); @@ -82,7 +84,7 @@ fn set_bfo(siclock: &mut dyn Si5351, freq: u32) -> Result<(), si5351::Error> siclock.flush_clock_control(si5351::ClockOutput::Clk2) } -fn set_vfo(siclock: &mut dyn Si5351, freq: u32) +fn set_vfo_centihertz(siclock: &mut dyn Si5351, freq: u32) { if freq == 0 { siclock.set_clock_enabled(si5351::ClockOutput::Clk0, false); @@ -107,7 +109,7 @@ impl SiClock I2C: WriteRead + Write, { pub fn new(i2c: I2C, bfo: u32, vfo: u32) -> SiClock { - let mut siclock = Si5351Device::new(i2c, false, REF_CLOCK); + let mut siclock = Si5351Device::new(i2c, false); siclock.init(si5351::CrystalLoad::_10).unwrap(); // See freqplan.py for Si5351 frequency plan @@ -118,7 +120,7 @@ impl SiClock siclock.reset_pll(si5351::PLL::A).unwrap(); - set_vfo(&mut siclock, vfo); + set_vfo_centihertz(&mut siclock, vfo * 100); siclock.reset_pll(si5351::PLL::B).unwrap(); @@ -127,8 +129,8 @@ impl SiClock SiClock{siclock} } - pub fn set_vfo(&mut self, freq: u32) { - set_vfo(&mut self.siclock, freq) + pub fn set_vfo_centihertz(&mut self, freq: u32) { + set_vfo_centihertz(&mut self.siclock, freq) } pub fn set_bfo(&mut self, freq: u32) -> Result<(), si5351::Error> { diff --git a/sw/eval-clock-cw-tx/src/state.rs b/sw/eval-clock-cw-tx/src/state.rs index 44c64d2..4016a51 100644 --- a/sw/eval-clock-cw-tx/src/state.rs +++ b/sw/eval-clock-cw-tx/src/state.rs @@ -1,4 +1,4 @@ -const INITIAL_VFO : u32 = 10_100_000; +const INITIAL_VFO : u32 = 10_140_000; // Defines which parameter is changed by the encoder #[derive(Clone, Copy, PartialEq, Eq)] @@ -23,12 +23,14 @@ pub enum CWMode { pub enum Mode { CW(CWMode), FeldHell, + WSPR, } #[derive(PartialEq, Eq, Clone, Copy)] pub enum SequenceMode { CW, FeldHell, + WSPR, } #[derive(Clone, PartialEq, Eq)] @@ -48,19 +50,23 @@ pub struct State { pub sequence_state : SequenceState, pub update_disp_counter : u8, pub cw_wpm : u32, + pub wspr_freq_offset_centihz : u32, + pub wspr_freq_offset_updated : bool, } impl State { pub fn new() -> Self { State { ui_sel : UISelection::VFO, - mode : Mode::CW(CWMode::StraightKey), + mode : Mode::WSPR, vfo_sel : VFOSelection::A, vfo_a : INITIAL_VFO, vfo_b : INITIAL_VFO, sequence_state : SequenceState::Rx, update_disp_counter : 0, cw_wpm : 14, + wspr_freq_offset_centihz : 0, + wspr_freq_offset_updated : false, } } @@ -88,6 +94,7 @@ impl State { } } + pub fn allow_feldhell_keying(&self) -> bool { self.sequence_state == SequenceState::Tx(SequenceMode::FeldHell) } diff --git a/sw/eval-clock-cw-tx/src/ui.rs b/sw/eval-clock-cw-tx/src/ui.rs index 9f3d985..0ee6163 100644 --- a/sw/eval-clock-cw-tx/src/ui.rs +++ b/sw/eval-clock-cw-tx/src/ui.rs @@ -172,7 +172,8 @@ impl UI { let (new_ui_sel, new_filter_shift) = match (state.ui_sel, state.mode) { (UISelection::Mode, Mode::CW(CWMode::StraightKey)) => (UISelection::Mode, Mode::CW(CWMode::Iambic)), (UISelection::Mode, Mode::CW(CWMode::Iambic)) => (UISelection::Mode, Mode::FeldHell), - (UISelection::Mode, Mode::FeldHell) => (UISelection::Mode, Mode::CW(CWMode::StraightKey)), + (UISelection::Mode, Mode::FeldHell) => (UISelection::Mode, Mode::WSPR), + (UISelection::Mode, Mode::WSPR) => (UISelection::Mode, Mode::CW(CWMode::StraightKey)), (_, f) => (UISelection::Mode, f), }; @@ -264,6 +265,7 @@ pub fn update_disp + DelayMs "CWs", Mode::CW(CWMode::Iambic) => "CWp", Mode::FeldHell => "HEL", + Mode::WSPR => "WSP", }; write!(string, "{} ", mode).unwrap(); diff --git a/sw/eval-clock-cw-tx/src/usb.rs b/sw/eval-clock-cw-tx/src/usb.rs index d75c287..b7e8536 100644 --- a/sw/eval-clock-cw-tx/src/usb.rs +++ b/sw/eval-clock-cw-tx/src/usb.rs @@ -52,6 +52,9 @@ static mut USB_DEVICE: Option> = None; * * Set FELDHELL narrow font * fontn + * + * Trigger a WSPR transmission + * wspr */ @@ -75,10 +78,13 @@ pub fn enable_interrupts() { } } +#[derive(PartialEq, Clone, Copy)] +pub enum Transmit { None, FELDHELL, WSPR } + pub struct USBData { font : Font, - pub frequency : u32, - pub transmit : bool, + frequency : Option, + transmit : Transmit, current_str : ArrayVec::, send_ix : usize, @@ -121,15 +127,15 @@ impl USBData { } USBData { - font : Font::Wide, - frequency : 10121000, - transmit : false, - current_str : ArrayVec::new(), - send_ix : 0, + font: Font::Wide, + frequency: None, + transmit: Transmit::None, + current_str: ArrayVec::new(), + send_ix: 0, } } - fn send_once(&mut self, message : &[u8]) { + pub fn send_message(&mut self, message : &[u8]) { cortex_m::interrupt::free(|_cs| { let serial = unsafe { USB_SERIAL.as_mut().unwrap() }; serial.write(message).ok(); @@ -137,11 +143,11 @@ impl USBData { } pub fn send_receive(&mut self) { - self.send_once(b"Switch RX\n"); + self.send_message(b"Switch RX\n"); } pub fn send_transmit(&mut self) { - self.send_once(b"Switch TX\n"); + self.send_message(b"Switch TX\n"); } pub fn handle(&mut self) { @@ -189,12 +195,12 @@ impl USBData { if let Some(m) = in_message { if m.as_str() == "tx\n" { outgoing.try_extend_from_slice(b"TX ok\n").ok(); - self.transmit = true; + self.transmit = Transmit::FELDHELL; } else if m.as_str() == "rx\n" { outgoing.try_extend_from_slice(b"RX ok\n").ok(); self.current_str.clear(); - self.transmit = false; + self.transmit = Transmit::None; } else if m.chars().nth(0).unwrap() == 'm' { self.current_str.clear(); @@ -211,6 +217,10 @@ impl USBData { self.font = Font::Narrow; outgoing.try_extend_from_slice(b"fontn ok\n").ok(); } + else if m.as_str() == "wspr\n" { + outgoing.try_extend_from_slice(b"wspr ok\n").ok(); + self.transmit = Transmit::WSPR; + } else if m.chars().nth(0).unwrap() == 'f' { let mut as_str = ArrayString::::new(); @@ -220,7 +230,7 @@ impl USBData { match u32::from_str_radix(as_str.as_str().trim(), 10) { Ok(val) => { - self.frequency = val; + self.frequency = Some(val); outgoing.try_extend_from_slice(b"f ok\n").ok(); } _ => { @@ -280,7 +290,14 @@ impl USBData { } } - pub fn is_transmit(&self) -> bool { self.transmit } + pub fn take_frequency(&mut self) -> Option { self.frequency.take() } + + pub fn transmit(&self) -> Transmit { self.transmit } + + pub fn clear_transmit(&mut self) { + self.send_message(b"WSPR done\n"); + self.transmit = Transmit::None + } } #[allow(non_snake_case)] -- cgit v1.2.3