aboutsummaryrefslogtreecommitdiffstats
path: root/sw/dart-70/src
diff options
context:
space:
mode:
Diffstat (limited to 'sw/dart-70/src')
-rw-r--r--sw/dart-70/src/cw.rs260
-rw-r--r--sw/dart-70/src/log10f.rs91
-rw-r--r--sw/dart-70/src/main.rs511
-rw-r--r--sw/dart-70/src/si_clock.rs137
-rw-r--r--sw/dart-70/src/state.rs129
-rw-r--r--sw/dart-70/src/ui.rs362
6 files changed, 1490 insertions, 0 deletions
diff --git a/sw/dart-70/src/cw.rs b/sw/dart-70/src/cw.rs
new file mode 100644
index 0000000..4656f81
--- /dev/null
+++ b/sw/dart-70/src/cw.rs
@@ -0,0 +1,260 @@
+//! CW output using PWM on PA8, TIM1 CH1
+
+use stm32f1xx_hal::{
+ timer,
+ pac::TIM1,
+};
+
+const CW_MAPPING : [u8; 50] = [ //{{{
+ // Read bits from right to left
+
+ 0b110101, //+ ASCII 43
+ 0b110101, //, ASCII 44
+ 0b1011110, //- ASCII 45
+
+ 0b1010101, //., ASCII 46
+ 0b110110, // / ASCII 47
+
+ 0b100000, // 0, ASCII 48
+ 0b100001, // 1
+ 0b100011,
+ 0b100111,
+ 0b101111,
+ 0b111111,
+ 0b111110,
+ 0b111100,
+ 0b111000,
+ 0b110000, // 9, ASCII 57
+
+ // The following are mostly invalid, but
+ // required to fill the gap in ASCII between
+ // numerals and capital letters
+ 0b10, // :
+ 0b10, // ;
+ 0b10, // <
+ 0b101110, // =
+ 0b10, // >
+ 0b1110011, // ?
+ 0b1101001, //@
+
+ 0b101, // A ASCII 65
+ 0b11110,
+ 0b11010,
+ 0b1110,
+ 0b11,
+ 0b11011,
+ 0b1100,
+ 0b11111,
+ 0b111,
+ 0b10001,
+ 0b1010,
+ 0b11101,
+ 0b100, //M
+ 0b110,
+ 0b1000,
+ 0b11001,
+ 0b10100,
+ 0b1101,
+ 0b1111,
+ 0b10,
+ 0b1011,
+ 0b10111,
+ 0b1001,
+ 0b10110,
+ 0b10010,
+ 0b11100, // Z
+
+ 0b101010, //Start, ASCII [
+ 0b1010111, // SK , ASCII '\'
+]; //}}}
+
+const CW_MACRO : &[u8; 28] = b"CQ DE HB9EGM HB9EGM HB9EGM K";
+
+pub const SIDETONE_FREQ : u32 = 800;
+
+pub struct CWPWM {
+ channel : timer::pwm::PwmChannel<TIM1, { timer::pwm::C1 }>,
+}
+
+impl CWPWM {
+ pub fn new(mut channel: timer::pwm::PwmChannel<TIM1, { timer::pwm::C1 }>) -> Self {
+ channel.enable();
+ channel.set_duty(0);
+ CWPWM { channel }
+ }
+
+ pub fn on(&mut self) {
+ let max = self.channel.get_max_duty();
+ self.channel.set_duty(max / 2);
+ }
+
+ pub fn off(&mut self) {
+ self.channel.set_duty(0);
+ }
+}
+
+#[derive(PartialEq, Eq, Clone, Copy)]
+enum MorseSign {
+ Dot,
+ Dash
+}
+
+impl MorseSign {
+ fn length(&self) -> u32 {
+ match self {
+ Self::Dot => 1,
+ Self::Dash => 3,
+ }
+ }
+
+ fn other(&self) -> Self {
+ match self {
+ Self::Dot => Self::Dash,
+ Self::Dash => Self::Dot,
+ }
+ }
+}
+
+fn other_pressed(sign: MorseSign, dot_pressed: bool, dash_pressed: bool) -> bool {
+ match sign {
+ MorseSign::Dot => dash_pressed,
+ MorseSign::Dash => dot_pressed,
+ }
+}
+
+#[derive(Eq, Clone, Copy)]
+enum KeyerState {
+ Idle,
+ Beep{current: MorseSign, next: Option<MorseSign>},
+ Pause{current: MorseSign, next: Option<MorseSign>},
+ LastPause{next: Option<MorseSign>},
+}
+
+impl PartialEq for KeyerState {
+ fn eq(&self, rhs: &Self) -> bool {
+ match (self, rhs) {
+ (Self::Idle, Self::Idle) => true,
+ (Self::Beep{current : c1, next : _}, Self::Beep{current : c2, next : _}) => c1 == c2,
+ (Self::Pause{current : c1, next : _}, Self::Pause{current : c2, next : _}) => c1 == c2,
+ (Self::LastPause{next : _}, Self::LastPause{next : _}) => true,
+ _ => false,
+ }
+ }
+}
+
+pub struct Keyer {
+ // All durations are in ticks
+ dot_length : u32,
+ state : KeyerState,
+ time_last_state_change : u32,
+}
+
+impl Keyer {
+ pub fn new(wpm : u32, ticks_per_s: u32) -> Keyer {
+ /* PARIS standard: 20 words per minute = dot length of 60 ms, inversely proportional:
+ * 1 wpm = 1200 ms, 2 wpm = 600 ms */
+
+ Keyer{
+ dot_length : 1200 * ticks_per_s / (1000 * wpm),
+ state : KeyerState::Idle,
+ time_last_state_change : 0,
+ }
+ }
+ pub fn set_speed(&mut self, wpm : u32, ticks_per_s: u32) { self.dot_length = 1200 * ticks_per_s / (1000 * wpm) }
+
+ pub fn tick(&mut self, ticks_now: u32, dot_pressed: bool, dash_pressed: bool) -> bool {
+ let mut transmit = false;
+
+ let next_state = match self.state {
+ KeyerState::Idle => {
+ if dot_pressed {
+ transmit = true;
+ KeyerState::Beep{current: MorseSign::Dot, next: None}
+ }
+ else if dash_pressed {
+ transmit = true;
+ KeyerState::Beep{current: MorseSign::Dash, next: None}
+ }
+ else {
+ KeyerState::Idle
+ }
+ },
+ KeyerState::Beep{current, next} => {
+ transmit = true;
+
+ let next = if other_pressed(current, dot_pressed, dash_pressed) {
+ Some(current.other())
+ }
+ else {
+ next
+ };
+
+ if self.time_last_state_change + self.dot_length * current.length() <= ticks_now {
+ KeyerState::Pause{current, next}
+ }
+ else {
+ KeyerState::Beep{current, next}
+ }
+ },
+ KeyerState::Pause{current, next} => {
+ let next = if other_pressed(current, dot_pressed, dash_pressed) {
+ Some(current.other())
+ }
+ else {
+ next
+ };
+
+ if self.time_last_state_change + self.dot_length <= ticks_now {
+ match next {
+ Some(state) => {
+ transmit = true;
+ KeyerState::Beep{current: state, next: None}
+ },
+ None => KeyerState::LastPause{next: None}
+ }
+ }
+ else {
+ KeyerState::Pause{current, next}
+ }
+ },
+ KeyerState::LastPause{next} => {
+ let next = if dot_pressed {
+ Some(MorseSign::Dot)
+ }
+ else if dash_pressed {
+ Some(MorseSign::Dash)
+ }
+ else {
+ next
+ };
+
+ if self.time_last_state_change + self.dot_length <= ticks_now {
+ KeyerState::LastPause{next}
+ }
+ else {
+ match next {
+ Some(MorseSign::Dot) => {
+ transmit = true;
+ KeyerState::Beep{current: MorseSign::Dot, next: None}
+ },
+ Some(MorseSign::Dash) => {
+ transmit = true;
+ KeyerState::Beep{current: MorseSign::Dash, next: None}
+ },
+ None => {
+ KeyerState::Idle
+ },
+ }
+ }
+ },
+ };
+
+ if next_state != self.state {
+ self.time_last_state_change = ticks_now;
+ }
+ self.state = next_state;
+
+ transmit
+ }
+}
+
diff --git a/sw/dart-70/src/log10f.rs b/sw/dart-70/src/log10f.rs
new file mode 100644
index 0000000..108dfa8
--- /dev/null
+++ b/sw/dart-70/src/log10f.rs
@@ -0,0 +1,91 @@
+/* origin: FreeBSD /usr/src/lib/msun/src/e_log10f.c */
+/*
+ * ====================================================
+ * Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
+ *
+ * Developed at SunPro, a Sun Microsystems, Inc. business.
+ * Permission to use, copy, modify, and distribute this
+ * software is freely granted, provided that this notice
+ * is preserved.
+ * ====================================================
+ */
+/*
+ * See comments in log10.c.
+ */
+
+use core::f32;
+
+const IVLN10HI: f32 = 4.3432617188e-01; /* 0x3ede6000 */
+const IVLN10LO: f32 = -3.1689971365e-05; /* 0xb804ead9 */
+const LOG10_2HI: f32 = 3.0102920532e-01; /* 0x3e9a2080 */
+const LOG10_2LO: f32 = 7.9034151668e-07; /* 0x355427db */
+/* |(log(1+s)-log(1-s))/s - Lg(s)| < 2**-34.24 (~[-4.95e-11, 4.97e-11]). */
+const LG1: f32 = 0.66666662693; /* 0xaaaaaa.0p-24 */
+const LG2: f32 = 0.40000972152; /* 0xccce13.0p-25 */
+const LG3: f32 = 0.28498786688; /* 0x91e9ee.0p-25 */
+const LG4: f32 = 0.24279078841; /* 0xf89e26.0p-26 */
+
+#[cfg_attr(all(test, assert_no_panic), no_panic::no_panic)]
+pub fn log10f(mut x: f32) -> f32 {
+ let x1p25f = f32::from_bits(0x4c000000); // 0x1p25f === 2 ^ 25
+
+ let mut ui: u32 = x.to_bits();
+ let hfsq: f32;
+ let f: f32;
+ let s: f32;
+ let z: f32;
+ let r: f32;
+ let w: f32;
+ let t1: f32;
+ let t2: f32;
+ let dk: f32;
+ let mut hi: f32;
+ let lo: f32;
+ let mut ix: u32;
+ let mut k: i32;
+
+ ix = ui;
+ k = 0;
+ if ix < 0x00800000 || (ix >> 31) > 0 {
+ /* x < 2**-126 */
+ if ix << 1 == 0 {
+ return -1. / (x * x); /* log(+-0)=-inf */
+ }
+ if (ix >> 31) > 0 {
+ return (x - x) / 0.0; /* log(-#) = NaN */
+ }
+ /* subnormal number, scale up x */
+ k -= 25;
+ x *= x1p25f;
+ ui = x.to_bits();
+ ix = ui;
+ } else if ix >= 0x7f800000 {
+ return x;
+ } else if ix == 0x3f800000 {
+ return 0.;
+ }
+
+ /* reduce x into [sqrt(2)/2, sqrt(2)] */
+ ix += 0x3f800000 - 0x3f3504f3;
+ k += (ix >> 23) as i32 - 0x7f;
+ ix = (ix & 0x007fffff) + 0x3f3504f3;
+ ui = ix;
+ x = f32::from_bits(ui);
+
+ f = x - 1.0;
+ s = f / (2.0 + f);
+ z = s * s;
+ w = z * z;
+ t1 = w * (LG2 + w * LG4);
+ t2 = z * (LG1 + w * LG3);
+ r = t2 + t1;
+ hfsq = 0.5 * f * f;
+
+ hi = f - hfsq;
+ ui = hi.to_bits();
+ ui &= 0xfffff000;
+ hi = f32::from_bits(ui);
+ lo = f - hi - hfsq + s * (hfsq + r);
+ dk = k as f32;
+ dk * LOG10_2LO + (lo + hi) * IVLN10LO + lo * IVLN10HI + hi * IVLN10HI + dk * LOG10_2HI
+}
diff --git a/sw/dart-70/src/main.rs b/sw/dart-70/src/main.rs
new file mode 100644
index 0000000..425f1c6
--- /dev/null
+++ b/sw/dart-70/src/main.rs
@@ -0,0 +1,511 @@
+/*
+ The MIT License (MIT)
+
+ 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
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+#![no_main]
+#![no_std]
+
+use core::mem::MaybeUninit;
+use cortex_m_rt::ExceptionFrame;
+use cortex_m_semihosting::hprintln;
+use panic_semihosting as _;
+
+use stm32f1xx_hal::{
+ prelude::*,
+ adc,
+ pac,
+ pac::interrupt,
+ i2c,
+ gpio,
+ gpio::PinState,
+ timer::{CounterHz, Timer, Event},
+ qei::QeiOptions,
+};
+
+use hd44780_driver::{Cursor, CursorBlink, Display, DisplayMode, HD44780};
+
+pub mod ui;
+pub mod cw;
+pub mod state;
+pub mod si_clock;
+pub mod log10f;
+
+use state::*;
+
+const TICKS_PER_SECOND: u32 = 100;
+
+struct SharedWithISR {
+ state: State,
+ last_sequence_state_change: u32,
+ cw_ptt_timestamp: u32,
+ cw_key_n: gpio::gpioa::PA15<gpio::Output<gpio::OpenDrain>>,
+ ui: ui::UI,
+ cw_pwm: cw::CWPWM,
+ cw_keyer: cw::Keyer,
+ cw_paddle_tip: gpio::gpiob::PB8<gpio::Input<gpio::PullUp>>,
+ cw_paddle_ring: gpio::gpiob::PB9<gpio::Input<gpio::PullUp>>,
+ en_rx: gpio::gpiob::PB3<gpio::Output<gpio::PushPull>>,
+ en_tx: gpio::gpiob::PB5<gpio::Output<gpio::PushPull>>,
+ mute_spkr: gpio::gpioa::PA2<gpio::Output<gpio::PushPull>>,
+ mute_micn: gpio::gpioa::PA1<gpio::Output<gpio::PushPull>>,
+ led: gpio::gpiob::PB14<gpio::Output<gpio::OpenDrain>>,
+}
+
+static mut SHARED: MaybeUninit<SharedWithISR> = MaybeUninit::uninit();
+static mut CLOCK_TIMER: MaybeUninit<CounterHz<pac::TIM2>> = MaybeUninit::uninit();
+static mut TICK_COUNTER: MaybeUninit<u32> = MaybeUninit::uninit();
+
+fn ticks_now() -> u32 {
+ cortex_m::interrupt::free(|_cs| unsafe { *TICK_COUNTER.as_ptr() })
+}
+
+fn get_state_copy() -> State {
+ cortex_m::interrupt::free(|_cs| unsafe {
+ (*SHARED.as_ptr()).state.clone()
+ })
+}
+
+#[cortex_m_rt::entry]
+fn main() -> ! {
+ let cp = cortex_m::Peripherals::take().unwrap();
+ let dp = pac::Peripherals::take().unwrap();
+
+ let mut flash = dp.FLASH.constrain();
+ let rcc = dp.RCC.constrain();
+ let mut afio = dp.AFIO.constrain();
+ let clocks = rcc.cfgr
+ .use_hse(16.MHz())
+ .sysclk(32.MHz())
+ .pclk1(24.MHz())
+ .adcclk(2.MHz())
+ .freeze(&mut flash.acr);
+ let mut delay = cp.SYST.delay(&clocks);
+
+ delay.delay_ms(200u16);
+
+ let mut gpioa = dp.GPIOA.split();
+ let mut gpiob = dp.GPIOB.split();
+ let mut gpioc = dp.GPIOC.split();
+
+ // Buttons as analog inputs (multi-level)
+ let mic_ptt = gpioa.pa3.into_floating_input(&mut gpioa.crl);
+ let vox_ptt_n = gpioa.pa0.into_pull_up_input(&mut gpioa.crl);
+ let btn0 = gpiob.pb1.into_floating_input(&mut gpiob.crl); // BTN0 Button A
+ let btn1 = gpiob.pb0.into_floating_input(&mut gpiob.crl); // BTN1 Button B
+ let btn2 = gpiob.pb12.into_floating_input(&mut gpiob.crh); // BTN2 Button C
+ let btn3 = gpiob.pb13.into_floating_input(&mut gpiob.crh); // BTN3 Button D
+ let pc15 = gpioc.pc15.into_floating_input(&mut gpioc.crh); // ENC BTN
+ let ui = ui::UI::new(mic_ptt, vox_ptt_n, btn0, btn1, btn2, btn3, pc15);
+
+ let cw_pwm = {
+ let pa8 = gpioa.pa8.into_alternate_push_pull(&mut gpioa.crh); // CW PWM output using TIM1 Ch1
+ let pwm = dp.TIM1.pwm_hz(pa8, &mut afio.mapr, cw::SIDETONE_FREQ.Hz(), &clocks);
+ let channel = pwm.split();
+ cw::CWPWM::new(channel)
+ };
+
+ let cw_paddle_tip = gpiob.pb8.into_pull_up_input(&mut gpiob.crh); // CW paddle tip
+ let cw_paddle_ring = gpiob.pb9.into_pull_up_input(&mut gpiob.crh); // CW paddle ring
+
+ let mut s_meter = gpioa.pa5.into_analog(&mut gpioa.crl);
+ let mut adc2 = adc::Adc::adc2(dp.ADC2, clocks);
+ let mut last_s_meter_update_time = 0;
+
+ // Configure PB14 as output. (LED)
+ let mut led = gpiob.pb14.into_open_drain_output(&mut gpiob.crh);
+ led.set_low();
+
+ let (pa15, pb3, _pb4) = afio.mapr.disable_jtag(gpioa.pa15, gpiob.pb3, gpiob.pb4);
+ let cw_key_n = pa15.into_open_drain_output_with_state(&mut gpioa.crh, PinState::High);
+
+ let en_rx = pb3.into_push_pull_output_with_state(&mut gpiob.crl, PinState::Low);
+ let en_tx = gpiob.pb5.into_push_pull_output_with_state(&mut gpiob.crl, PinState::Low);
+
+ let mute_spkr = gpioa.pa2.into_push_pull_output_with_state(&mut gpioa.crl, PinState::Low);
+ let mute_micn = gpioa.pa1.into_push_pull_output_with_state(&mut gpioa.crl, PinState::Low);
+
+ let c1 = gpioa.pa6;
+ let c2 = gpioa.pa7;
+
+ let qei = Timer::new(dp.TIM3, &clocks)
+ .qei((c1, c2), &mut afio.mapr, QeiOptions::default());
+
+ // Configure I2C2 for display
+ let scl2 = gpiob.pb10.into_alternate_open_drain(&mut gpiob.crh);
+ let sda2 = gpiob.pb11.into_alternate_open_drain(&mut gpiob.crh);
+
+ let i2c2 = i2c::BlockingI2c::i2c2(
+ dp.I2C2,
+ (scl2, sda2),
+ i2c::Mode::Fast {
+ frequency: 400_000.Hz(),
+ duty_cycle: i2c::DutyCycle::Ratio2to1,
+ },
+ clocks,
+ /* start_timeout_us */ 1000,
+ /* start_retries */ 10,
+ /* addr_timeout_us */ 1000,
+ /* data_timeout_us */ 1000,
+ );
+
+ let i2c2_busmanager = shared_bus::BusManagerSimple::new(i2c2);
+
+ const I2C_ADDRESS: u8 = 0b010_0000; // MCP23008, depending on solder bridges
+ let mut lcd = match HD44780::new_i2c_mcp23008(i2c2_busmanager.acquire_i2c(), I2C_ADDRESS, &mut delay) {
+ Ok(lcd) => lcd,
+ Err(_) => panic!("HD44780 init fail"),
+ };
+
+ lcd.reset(&mut delay).unwrap();
+ lcd.clear(&mut delay).unwrap();
+ lcd.set_display_mode(
+ DisplayMode {
+ display: Display::On,
+ cursor_visibility: Cursor::Invisible,
+ cursor_blink: CursorBlink::Off,
+ },
+ &mut delay).unwrap();
+ lcd.set_cursor_pos(0, &mut delay).unwrap();
+ lcd.write_str(" HB9EGM ", &mut delay).unwrap();
+ lcd.set_cursor_pos(40, &mut delay).unwrap();
+ lcd.write_str(" DART-70 2023 ", &mut delay).unwrap();
+ delay.delay_ms(1_500u16);
+
+ // Configure I2C1 to be used for Si5351
+ let scl = gpiob.pb6.into_alternate_open_drain(&mut gpiob.crl);
+ let sda = gpiob.pb7.into_alternate_open_drain(&mut gpiob.crl);
+
+ let i2c = i2c::BlockingI2c::i2c1(
+ dp.I2C1,
+ (scl, sda),
+ &mut afio.mapr,
+ i2c::Mode::Standard {
+ frequency: 100_000.Hz(),
+ },
+ clocks,
+ /* start_timeout_us */ 1000,
+ /* start_retries */ 10,
+ /* addr_timeout_us */ 1000,
+ /* data_timeout_us */ 1000,
+ );
+ let i2c_busmanager = shared_bus::BusManagerSimple::new(i2c);
+
+
+ let mut siclock = {
+ let shared = unsafe { &mut *SHARED.as_mut_ptr() };
+ *shared = SharedWithISR {
+ state : State::new(),
+ last_sequence_state_change : 0,
+ cw_ptt_timestamp : 0,
+ cw_key_n,
+ ui,
+ cw_pwm,
+ cw_keyer : cw::Keyer::new(12, TICKS_PER_SECOND),
+ cw_paddle_tip, cw_paddle_ring, en_rx, en_tx, mute_spkr, mute_micn, led
+ };
+
+ si_clock::SiClock::new(i2c_busmanager.acquire_i2c(), shared.state.bfo(), shared.state.vfo())
+ };
+
+ let mut last_s_meter_value = 0;
+ ui::update_disp(&mut lcd, &get_state_copy(), &mut delay, last_s_meter_value, false);
+
+ let mut last_encoder_count = qei.count();
+
+ {
+ let ticks = unsafe { &mut *TICK_COUNTER.as_mut_ptr() };
+ *ticks = 0;
+ }
+
+ {
+ let timer = unsafe { &mut *CLOCK_TIMER.as_mut_ptr() };
+ *timer = Timer::new(dp.TIM2, &clocks).counter_hz();
+ timer.start(TICKS_PER_SECOND.Hz()).unwrap();
+ timer.listen(Event::Update);
+ }
+
+ unsafe { pac::NVIC::unmask(pac::Interrupt::TIM2); }
+
+ let mut last_disp_update_counter = 1;
+ let mut previous_vfo = 0;
+ let mut previous_bfo = 0;
+
+ loop {
+ let mut update_disp_required = false;
+ let mut bfo_tune_fail = false;
+
+ let state = get_state_copy();
+
+ let encoder_count : u16 = qei.count();
+ if encoder_count != last_encoder_count {
+ let delta = encoder_count.wrapping_sub(last_encoder_count);
+ let delta = if delta > 0x7FFF { delta as i32 - 0x10000 } else { delta as i32 };
+
+ let require_bfo_update = cortex_m::interrupt::free(|_cs| {
+ let shared = unsafe { &mut *SHARED.as_mut_ptr() };
+ let r = shared.ui.update_encoder(&mut shared.state, delta);
+ if let Mode::CW(CWMode::Iambic) = shared.state.mode {
+ shared.cw_keyer.set_speed(shared.state.cw_wpm, TICKS_PER_SECOND)
+ }
+ r
+ });
+
+ if require_bfo_update {
+ bfo_tune_fail = !siclock.set_bfo(state.bfo()).is_ok();
+ }
+ siclock.set_vfo(state.vfo());
+ update_disp_required = true;
+ }
+
+ let bfo = state.bfo();
+ if previous_bfo != bfo {
+ bfo_tune_fail = !siclock.set_bfo(bfo).is_ok();
+ }
+ previous_bfo = bfo;
+
+ let vfo = state.vfo();
+ if previous_vfo != vfo {
+ siclock.set_vfo(vfo);
+ }
+ previous_vfo = vfo;
+
+ let s_meter_adc_value: u16 = adc2.read(&mut s_meter).unwrap();
+ let s_meter_value = s_meter_from_adc(s_meter_adc_value);
+ let t_now = ticks_now();
+ if last_s_meter_update_time + 10 < t_now {
+ update_disp_required |= s_meter_value != last_s_meter_value;
+ last_s_meter_value = s_meter_value;
+ last_s_meter_update_time = t_now;
+ }
+
+ if last_disp_update_counter != state.update_disp_counter {
+ update_disp_required = true;
+ last_disp_update_counter = state.update_disp_counter;
+ }
+
+ if update_disp_required {
+ let state = get_state_copy();
+ ui::update_disp(&mut lcd, &state, &mut delay, s_meter_value, bfo_tune_fail);
+ }
+
+ last_encoder_count = encoder_count;
+
+ cortex_m::asm::wfi();
+ }
+}
+
+fn s_meter_from_adc(adc : u16) -> u8 {
+ // Avoid 0 because of log10
+ let adc = f32::from(if adc == 0 { 1 } else { adc });
+ // ADC is 12-bit, convert to dB full-scale
+ let adc_db = 10f32 * log10f::log10f(adc / 4092f32);
+
+ /* Hand-calibrated lookup table */
+ if adc_db <= -35f32 {
+ 1
+ }
+ else if adc_db <= -20f32 {
+ 4
+ }
+ else if adc_db <= -12f32 {
+ 5
+ }
+ else if adc_db <= -8f32 {
+ 6
+ }
+ else if adc_db <= -7f32 {
+ 7
+ }
+ else {
+ 9
+ }
+}
+
+#[interrupt]
+fn TIM2() {
+ let timer = unsafe { &mut *CLOCK_TIMER.as_mut_ptr() };
+ timer.clear_interrupt(Event::Update);
+
+ let ticks = unsafe { &mut *TICK_COUNTER.as_mut_ptr() };
+ *ticks += 1;
+
+ let mut shared = unsafe { &mut *SHARED.as_mut_ptr() };
+ let button_result = shared.ui.handle_buttons(&mut shared.state);
+
+ if button_result.display_update {
+ shared.state.update_disp_counter += 1;
+ }
+
+ let cw_paddle_tip_low = shared.cw_paddle_tip.is_low();
+ let cw_paddle_ring_low = shared.cw_paddle_ring.is_low();
+
+ if cw_paddle_tip_low || cw_paddle_ring_low {
+ shared.state.send_tone = false;
+ }
+
+ let cw_ptt_delay : u32 = TICKS_PER_SECOND * 800 / 1000;
+ let cw_ptt = shared.state.send_tone ||
+ match shared.state.mode {
+ Mode::CW(_) => {
+ if cw_paddle_tip_low || cw_paddle_ring_low {
+ shared.cw_ptt_timestamp = *ticks;
+ true
+ }
+ else {
+ shared.cw_ptt_timestamp + cw_ptt_delay > *ticks
+ }
+ },
+ _ => false,
+ };
+
+ let cw_beep = shared.state.send_tone ||
+ 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),
+ _ => false,
+ };
+
+ let next_state = match shared.state.sequence_state {
+ SequenceState::Rx => {
+ shared.mute_spkr.set_low();
+ shared.mute_micn.set_low();
+ shared.en_rx.set_high();
+ shared.en_tx.set_low();
+ if button_result.ptt || cw_ptt {
+ SequenceState::MutingSpkr
+ }
+ else {
+ SequenceState::Rx
+ }
+ },
+ SequenceState::MutingSpkr => {
+ shared.mute_spkr.set_high();
+ shared.mute_micn.set_low();
+ shared.en_rx.set_high();
+ shared.en_tx.set_low();
+ if button_result.ptt {
+ SequenceState::SwitchingSSB
+ }
+ else if cw_ptt {
+ SequenceState::SwitchingCW
+ }
+ else {
+ SequenceState::Rx
+ }
+ },
+ SequenceState::SwitchingSSB => {
+ shared.mute_spkr.set_high();
+ shared.en_rx.set_low();
+ shared.en_tx.set_low();
+ shared.mute_micn.set_high();
+
+ if button_result.ptt {
+ SequenceState::TxSSB
+ }
+ else {
+ SequenceState::Rx
+ }
+ },
+ SequenceState::SwitchingCW => {
+ shared.mute_spkr.set_high();
+ shared.en_rx.set_low();
+ shared.en_tx.set_low();
+ shared.mute_micn.set_low();
+ if cw_ptt {
+ SequenceState::TxCW
+ }
+ else {
+ SequenceState::Rx
+ }
+ },
+ SequenceState::TxSSB => {
+ shared.en_rx.set_low();
+ shared.en_tx.set_high();
+
+ if button_result.ptt {
+ SequenceState::TxSSB
+ }
+ else {
+ SequenceState::SwitchingSSB
+ }
+ },
+ SequenceState::TxCW => {
+ shared.en_rx.set_low();
+ shared.en_tx.set_high();
+
+ if cw_ptt {
+ SequenceState::TxCW
+ }
+ else {
+ SequenceState::SwitchingCW
+ }
+ },
+ };
+
+ match shared.state.sequence_state {
+ SequenceState::TxCW => {
+ if cw_beep {
+ shared.led.set_low();
+ shared.cw_pwm.on();
+ shared.cw_key_n.set_low();
+ }
+ else {
+ shared.led.set_high();
+ shared.cw_pwm.off();
+ shared.cw_key_n.set_high();
+ }
+ },
+ _ => {
+ shared.led.set_high();
+ shared.cw_pwm.off();
+ shared.cw_key_n.set_high();
+ },
+ }
+
+ const SWITCHING_DELAY : u32 = TICKS_PER_SECOND * 80 / 1000;
+ if shared.state.sequence_state != next_state &&
+ shared.last_sequence_state_change + SWITCHING_DELAY <= *ticks {
+ shared.state.sequence_state = next_state;
+ shared.last_sequence_state_change = *ticks;
+ }
+}
+
+#[allow(non_snake_case)]
+#[cortex_m_rt::exception]
+unsafe fn HardFault(ef: &ExceptionFrame) -> ! {
+ let periph = unsafe { cortex_m::Peripherals::steal() };
+ let hfsr = periph.SCB.hfsr.read();
+ let cfsr = periph.SCB.cfsr.read();
+
+ hprintln!("Hardfault {:x} {:x} at {:x}\n", hfsr, cfsr, ef.pc());
+ cortex_m::asm::bkpt();
+ loop { }
+}
+
+#[allow(non_snake_case)]
+#[cortex_m_rt::exception]
+unsafe fn DefaultHandler(irqn: i16) {
+ hprintln!("Unhandled exception (IRQn = {})", irqn);
+ cortex_m::asm::bkpt();
+ loop { }
+}
diff --git a/sw/dart-70/src/si_clock.rs b/sw/dart-70/src/si_clock.rs
new file mode 100644
index 0000000..6de4c25
--- /dev/null
+++ b/sw/dart-70/src/si_clock.rs
@@ -0,0 +1,137 @@
+/*
+ The MIT License (MIT)
+
+ Copyright (c) 2020 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
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+use core::convert::TryInto;
+
+use si5351::{Si5351, Si5351Device};
+use embedded_hal::blocking::i2c::{WriteRead, Write};
+
+const REF_CLOCK : u32 = 25_000_000;
+const PLL_A_MULT : u32 = 32;
+
+fn gcd(x: u32, y: u32) -> u32 {
+ let mut x = x;
+ let mut y = y;
+ while y != 0 {
+ let t = y;
+ y = x % y;
+ x = t;
+ }
+ x
+}
+
+fn clock_settings_for_pll(freq: u32, pll: u32) -> (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)
+}
+
+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
+
+ if (divider % 2) == 1 {
+ divider -= 1 // Ensure an even integer division ratio
+ }
+
+ let pll_freq = divider * freq;
+ // 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 denom = 1048575;
+ let num = f64::from(l) * f64::from(denom) / f64::from(REF_CLOCK);
+
+ (divider.try_into().unwrap(), mult.try_into().unwrap(), num as u32, denom)
+}
+
+fn set_bfo(siclock: &mut dyn Si5351, freq: u32) -> Result<(), si5351::Error>
+{
+ if freq == 0 {
+ siclock.set_clock_enabled(si5351::ClockOutput::Clk2, false);
+ }
+ else {
+ let (a, b, c) = clock_settings_for_pll(freq, PLL_A_MULT * REF_CLOCK);
+ 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);
+ }
+ siclock.flush_clock_control(si5351::ClockOutput::Clk2)
+}
+
+fn set_vfo(siclock: &mut dyn Si5351, freq: u32)
+{
+ if freq == 0 {
+ siclock.set_clock_enabled(si5351::ClockOutput::Clk0, false);
+ }
+ else {
+ let (div, mult, num, denom) = clock_settings_with_pll_calculation(freq);
+
+ siclock.setup_pll(si5351::PLL::B, mult, num, denom).unwrap();
+ siclock.setup_multisynth_int(si5351::Multisynth::MS0, div, si5351::OutputDivider::Div1).unwrap();
+ siclock.select_clock_pll(si5351::ClockOutput::Clk0, si5351::PLL::B);
+ siclock.set_clock_enabled(si5351::ClockOutput::Clk0, true);
+ }
+ siclock.flush_clock_control(si5351::ClockOutput::Clk0).unwrap();
+}
+
+pub struct SiClock<I2C> {
+ siclock : Si5351Device<I2C>,
+}
+
+impl<I2C, E> SiClock<I2C>
+ where
+ I2C: WriteRead<Error = E> + Write<Error = E>,
+{
+ pub fn new(i2c: I2C, bfo: u32, vfo: u32) -> SiClock<I2C> {
+ let mut siclock = Si5351Device::new(i2c, false, REF_CLOCK);
+ siclock.init(si5351::CrystalLoad::_10).unwrap();
+
+ // See freqplan.py for Si5351 frequency plan
+ // CLK1 unused
+ siclock.setup_pll_int(si5351::PLL::A, 32).unwrap();
+
+ set_bfo(&mut siclock, bfo).unwrap();
+
+ siclock.reset_pll(si5351::PLL::A).unwrap();
+
+ set_vfo(&mut siclock, vfo);
+
+ siclock.reset_pll(si5351::PLL::B).unwrap();
+
+ siclock.flush_output_enabled().unwrap();
+
+ SiClock{siclock}
+ }
+
+ pub fn set_vfo(&mut self, freq: u32) {
+ set_vfo(&mut self.siclock, freq)
+ }
+
+ pub fn set_bfo(&mut self, freq: u32) -> Result<(), si5351::Error> {
+ set_bfo(&mut self.siclock, freq)
+ }
+}
diff --git a/sw/dart-70/src/state.rs b/sw/dart-70/src/state.rs
new file mode 100644
index 0000000..08826e7
--- /dev/null
+++ b/sw/dart-70/src/state.rs
@@ -0,0 +1,129 @@
+pub const VHF_BAND_EDGE : u32 = 144_000_000;
+pub const VHF_INITIAL_VFO : u32 = 144_300_000;
+pub const VHF_LO : u32 = 114_284_800;
+pub const BFO_LSB : u32 = 6_000_700 + 1_100;
+pub const BFO_USB : u32 = 6_000_700 - 1_100;
+pub const BFO_CW : u32 = 6_000_700 - 1_100;
+
+// Defines which parameter is changed by the encoder
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum UISelection {
+ VFO,
+ RIT,
+ Mode,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum MenuPage {
+ One,
+ Two,
+}
+
+#[derive(Clone, Copy)]
+pub enum VFOSelection {
+ A,
+ B,
+}
+
+#[derive(PartialEq, Eq, Clone, Copy)]
+pub enum CWMode {
+ StraightKey,
+ Iambic,
+}
+
+#[derive(PartialEq, Eq, Clone, Copy)]
+pub enum Mode {
+ LSB,
+ USB,
+ CustomShift(u32),
+ CW(CWMode),
+}
+
+#[derive(Clone, PartialEq, Eq)]
+pub enum SequenceState {
+ Rx,
+ MutingSpkr,
+ SwitchingSSB,
+ TxSSB,
+ SwitchingCW,
+ TxCW,
+}
+
+impl SequenceState {
+ fn apply_rit(&self) -> bool {
+ *self == SequenceState::Rx
+ }
+}
+
+#[derive(Clone)]
+pub struct State {
+ pub menu_page : MenuPage,
+ pub ui_sel : UISelection,
+ pub vfo_a : u32,
+ pub vfo_b : u32,
+ pub vfo_sel : VFOSelection,
+ pub rit : i32,
+ pub mode : Mode,
+ pub send_tone : bool,
+ pub sequence_state : SequenceState,
+ pub update_disp_counter : u8,
+ pub cw_wpm : u32,
+}
+
+impl State {
+ pub fn new() -> Self {
+ State {
+ menu_page : MenuPage::One,
+ ui_sel : UISelection::VFO,
+ vfo_a : VHF_INITIAL_VFO,
+ vfo_b : VHF_INITIAL_VFO,
+ vfo_sel : VFOSelection::A,
+ rit : 0,
+ mode : Mode::USB,
+ send_tone : false,
+ sequence_state : SequenceState::Rx,
+ update_disp_counter : 0,
+ cw_wpm : 14,
+ }
+ }
+
+ pub fn bfo(&self) -> u32 {
+ if self.send_tone {
+ 0
+ }
+ else {
+ match self.mode {
+ Mode::LSB => BFO_LSB,
+ Mode::USB => BFO_USB,
+ Mode::CustomShift(fs) => fs,
+ Mode::CW(_) => match self.sequence_state {
+ SequenceState::SwitchingCW | SequenceState::TxCW => 0,
+ _ => BFO_CW,
+ },
+ }
+ }
+ }
+
+ pub fn vhf_qrg(&self) -> u32 {
+ match self.vfo_sel {
+ VFOSelection::A => self.vfo_a,
+ VFOSelection::B => self.vfo_b,
+ }
+ }
+
+ pub fn if_qrg(&self) -> u32 {
+ self.vhf_qrg() - VHF_LO
+ }
+
+ pub fn vfo(&self) -> u32 {
+ let cw_offset = match self.sequence_state {
+ SequenceState::SwitchingCW | SequenceState::TxCW => 500,
+ _ => 0,
+ };
+
+ let vfo = (self.if_qrg() - self.bfo()) as i32 +
+ if self.sequence_state.apply_rit() { self.rit } else { 0 } +
+ cw_offset;
+ vfo as u32
+ }
+}
diff --git a/sw/dart-70/src/ui.rs b/sw/dart-70/src/ui.rs
new file mode 100644
index 0000000..f607b2b
--- /dev/null
+++ b/sw/dart-70/src/ui.rs
@@ -0,0 +1,362 @@
+/*
+ The MIT License (MIT)
+
+ 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
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+use crate::state::*;
+
+use core::fmt;
+use core::fmt::Write;
+
+use stm32f1xx_hal::{
+ gpio::gpioa::*,
+ gpio::gpiob::*,
+ gpio::gpioc::*,
+ gpio::{Input, PullUp, Floating},
+};
+
+use embedded_hal::blocking::delay::{DelayMs, DelayUs};
+use hd44780_driver::HD44780;
+
+#[derive(PartialEq, Eq, Clone, Copy)]
+struct ButtonState {
+ pub a : bool,
+ pub b : bool,
+ pub c : bool,
+ pub d : bool,
+ pub enc : bool,
+ pub ptt : bool,
+}
+
+const VFO_INCR : i32 = 2;
+const RIT_INCR : i32 = 1;
+const BFO_INCR : i32 = 10;
+
+impl ButtonState {
+ fn edge_detection(&self, old_state : &ButtonState) -> ButtonState {
+ ButtonState {
+ a : !old_state.a && self.a,
+ b : !old_state.b && self.b,
+ c : !old_state.c && self.c,
+ d : !old_state.d && self.d,
+ enc : !old_state.enc && self.enc,
+ ptt : self.ptt, // Don't do edge detection for PTT!
+ }
+ }
+}
+
+impl Default for ButtonState {
+ fn default() -> Self {
+ ButtonState {
+ a : false,
+ b : false,
+ c : false,
+ d : false,
+ enc : false,
+ ptt : false,
+ }
+ }
+}
+
+impl fmt::Display for ButtonState {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}{}{}{}{}{}",
+ if self.a { "A" } else { "a" },
+ if self.b { "B" } else { "b" },
+ if self.c { "C" } else { "c" },
+ if self.d { "D" } else { "d" },
+ if self.enc { "X" } else { "x" },
+ if self.ptt { "P" } else { "p" })
+ }
+}
+
+#[derive(Default)]
+pub struct ButtonResult {
+ pub display_update : bool,
+ pub ptt : bool,
+}
+
+pub struct UI {
+ btn0 : PB1<Input<Floating>>,
+ btn1 : PB0<Input<Floating>>,
+ btn2 : PB12<Input<Floating>>,
+ btn3 : PB13<Input<Floating>>,
+
+ btn_enc : PC15<Input<Floating>>,
+
+ mic_ptt : PA3<Input<Floating>>,
+ _vox_ptt_n : PA0<Input<PullUp>>,
+
+ previous_button_state : ButtonState,
+}
+
+impl UI {
+ pub fn new(
+ mic_ptt: PA3<Input<Floating>>,
+ vox_ptt_n: PA0<Input<PullUp>>,
+ btn0: PB1<Input<Floating>>,
+ btn1: PB0<Input<Floating>>,
+ btn2: PB12<Input<Floating>>,
+ btn3: PB13<Input<Floating>>,
+ btn_enc : PC15<Input<Floating>>) -> UI {
+
+ UI {
+ btn0,
+ btn1,
+ btn2,
+ btn3,
+ btn_enc,
+ mic_ptt,
+ _vox_ptt_n: vox_ptt_n,
+ previous_button_state: ButtonState::default(),
+ }
+ }
+
+ fn read_buttons(&mut self) -> ButtonState {
+ let mut buttons = ButtonState::default();
+
+ buttons.ptt = self.mic_ptt.is_high();
+
+ if self.btn0.is_low() {
+ buttons.a = true;
+ }
+
+ if self.btn1.is_low() {
+ buttons.b = true;
+ }
+
+ if self.btn2.is_low() {
+ buttons.c = true;
+ }
+
+ if self.btn3.is_low() {
+ buttons.d = true;
+ }
+
+ if self.btn_enc.is_low() {
+ buttons.enc = true;
+ }
+
+ buttons
+ }
+
+ pub fn handle_buttons(&mut self, state: &mut State) -> ButtonResult {
+ let mut result = ButtonResult::default();
+ let button_state = self.read_buttons();
+ let button_updates = button_state.edge_detection(&self.previous_button_state);
+ self.previous_button_state = button_state;
+
+ result.ptt = button_updates.ptt;
+
+ match state.menu_page {
+ MenuPage::One => {
+ if button_updates.a {
+ state.vfo_sel = match (state.ui_sel, state.vfo_sel) {
+ (UISelection::VFO, VFOSelection::A) => VFOSelection::B,
+ (UISelection::VFO, VFOSelection::B) => VFOSelection::A,
+ _ => state.vfo_sel.clone(),
+ };
+ state.ui_sel = UISelection::VFO;
+ result.display_update = true;
+ }
+
+ if button_updates.b {
+ state.ui_sel = UISelection::RIT;
+ result.display_update = true;
+ }
+
+ if button_updates.c {
+ let (new_ui_sel, new_filter_shift) = match (state.ui_sel, state.mode) {
+ (UISelection::Mode, Mode::USB) => (UISelection::Mode, Mode::LSB),
+ (UISelection::Mode, Mode::LSB) => (UISelection::Mode, Mode::CW(CWMode::StraightKey)),
+ (UISelection::Mode, Mode::CW(CWMode::StraightKey)) => (UISelection::Mode, Mode::CW(CWMode::Iambic)),
+ (UISelection::Mode, Mode::CW(CWMode::Iambic)) => (UISelection::Mode, Mode::USB),
+ (UISelection::Mode, Mode::CustomShift(_)) => (UISelection::Mode, Mode::USB),
+ (_, f) => (UISelection::Mode, f),
+ };
+
+ state.ui_sel = new_ui_sel;
+ state.mode = new_filter_shift;
+
+ result.display_update = true;
+ }
+
+ if button_updates.d {
+ state.menu_page = MenuPage::Two;
+ result.display_update = true;
+ }
+
+ if button_updates.enc {
+ match state.ui_sel {
+ UISelection::VFO => {},
+ UISelection::RIT => {
+ state.rit = 0;
+ },
+ UISelection::Mode => {
+ state.mode = Mode::USB;
+ },
+ }
+
+ result.display_update = true;
+ }
+ },
+ MenuPage::Two => {
+ state.ui_sel = UISelection::VFO;
+
+ if button_updates.a {
+ state.send_tone = true;
+ }
+
+ if button_updates.b {
+ state.send_tone = false;
+ }
+
+ if button_updates.c {
+ }
+
+ if button_updates.d {
+ state.menu_page = MenuPage::One;
+ result.display_update = true;
+ }
+
+ if button_updates.enc {
+ }
+ },
+ }
+
+ if result.ptt {
+ state.send_tone = false;
+ }
+
+ result
+ }
+
+ // Returns true if bfo must be reprogrammed
+ pub fn update_encoder(&mut self, state: &mut State, counter_delta : i32) -> bool {
+
+ let delta = (17 * counter_delta + 3 * (counter_delta * counter_delta * counter_delta))/20;
+
+ match state.ui_sel {
+ UISelection::VFO => {
+ match state.vfo_sel {
+ VFOSelection::A => {
+ state.vfo_a = (state.vfo_a as i32 + delta * VFO_INCR) as u32;
+ },
+ VFOSelection::B => {
+ state.vfo_b = (state.vfo_b as i32 + delta * VFO_INCR) as u32;
+ },
+ }
+ false
+ },
+ UISelection::RIT => {
+ state.rit = state.rit + delta * RIT_INCR;
+ false
+ },
+ UISelection::Mode => {
+ match state.mode {
+ Mode::CW(CWMode::Iambic) => {
+ let mut new_wpm = state.cw_wpm as i32 + counter_delta / 4;
+ if new_wpm < 1 {
+ new_wpm = 1;
+ }
+
+ if new_wpm > 40 {
+ new_wpm = 40;
+ }
+
+ let wpm = new_wpm as u32;
+ state.cw_wpm = wpm;
+ state.mode = Mode::CW(CWMode::Iambic);
+ false
+ },
+ _ => {
+ let new_bfo = (state.bfo() as i32 + counter_delta * BFO_INCR) as u32;
+ state.mode = Mode::CustomShift(new_bfo);
+ true
+ },
+ }
+ },
+ }
+ }
+}
+
+pub fn update_disp<T: hd44780_driver::bus::DataBus, D: DelayUs<u16> + DelayMs<u8>>(lcd: &mut HD44780<T>, state: &State, delay: &mut D, s_meter_value: u8, bfo_tune_fail: bool)
+{
+ let mut string = arrayvec::ArrayString::<16>::new();
+
+ /* Shorten the QRG to avoid using three digits for nothing */
+ let disp_freq = (state.vhf_qrg() as i32) - (VHF_BAND_EDGE as i32);
+ write!(string, "{:<03}.{:<03} ", disp_freq / 1000, disp_freq % 1000).unwrap();
+
+ /* By default, show RIT, unless UIselection is on mode, and mode is CW Iambic, in which case we show speed */
+ match (state.ui_sel, state.mode) {
+ (UISelection::Mode, Mode::CW(CWMode::Iambic)) => {
+ write!(string, "CW{:<02}", state.cw_wpm).unwrap();
+ },
+ (UISelection::Mode, Mode::CustomShift(shift)) => {
+ write!(string, "{:<04}", shift/10).unwrap();
+ },
+ _ => {
+ write!(string, "{}{:<03}", if state.rit >= 0 { "+" } else { "-" }, state.rit.abs()/10).unwrap();
+ },
+ }
+
+ if string.len() <= 16 - 4 {
+ // Avoids crash when frequency is very negative
+ write!(string, " S{:1}", s_meter_value).unwrap();
+ }
+
+ lcd.set_cursor_pos(0, delay).unwrap();
+ lcd.write_str(&string, delay).unwrap();
+
+ string.clear();
+
+ match state.menu_page {
+ MenuPage::One => {
+ match (bfo_tune_fail, &state.vfo_sel) {
+ (true, _) => write!(string, "VFO!").unwrap(),
+ (false, VFOSelection::A) => write!(string, "VFOa").unwrap(),
+ (false, VFOSelection::B) => write!(string, "VFOb").unwrap(),
+ }
+
+ write!(string, "{}", if state.ui_sel == UISelection::RIT { ">RIT" } else { " RIT" }).unwrap();
+
+ write!(string, "{}", if state.ui_sel == UISelection::Mode { ">" } else { " " }).unwrap();
+
+ let mode = match state.mode {
+ Mode::USB => "USB",
+ Mode::LSB => "LSB",
+ Mode::CustomShift(_) => "IFs",
+ Mode::CW(CWMode::StraightKey) => "CWs",
+ Mode::CW(CWMode::Iambic) => "CWp",
+ };
+
+ write!(string, "{} 1/2", mode).unwrap();
+ },
+ MenuPage::Two => {
+ write!(string, "TUT STOP --- 2/2").unwrap();
+ },
+ }
+
+ lcd.set_cursor_pos(40, delay).unwrap();
+ lcd.write_str(&string, delay).unwrap();
+}