diff options
author | Matthias P. Braendli <matthias.braendli@mpb.li> | 2023-02-25 11:17:03 +0100 |
---|---|---|
committer | Matthias P. Braendli <matthias.braendli@mpb.li> | 2023-02-25 11:17:03 +0100 |
commit | 2240e0b6683bcb69b1f477f4449de9cc17314ccd (patch) | |
tree | ab38c9e75a649faaa466578f0f6fc0b511e3607d /sw/dart-70/src | |
parent | 719fcaf1e0b69eefde2c7c9a572d4c807f8bc7ca (diff) | |
download | picardy-2240e0b6683bcb69b1f477f4449de9cc17314ccd.tar.gz picardy-2240e0b6683bcb69b1f477f4449de9cc17314ccd.tar.bz2 picardy-2240e0b6683bcb69b1f477f4449de9cc17314ccd.zip |
Create dart-70 firmware
Diffstat (limited to 'sw/dart-70/src')
-rw-r--r-- | sw/dart-70/src/cw.rs | 260 | ||||
-rw-r--r-- | sw/dart-70/src/log10f.rs | 91 | ||||
-rw-r--r-- | sw/dart-70/src/main.rs | 511 | ||||
-rw-r--r-- | sw/dart-70/src/si_clock.rs | 137 | ||||
-rw-r--r-- | sw/dart-70/src/state.rs | 129 | ||||
-rw-r--r-- | sw/dart-70/src/ui.rs | 362 |
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(); +} |