diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f8411d4..ccb4f54 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,18 +19,23 @@ jobs: - name: Cache Rust packages uses: actions/cache@v3 with: - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('Cargo.lock') }} + key: ${{ runner.os }}-build-${{ hashFiles('Cargo.lock') }} path: | ~/.cargo - - name: Build - run: cargo build --verbose --release - name: Install elf2uf2-rs run: | sudo apt-get update - sudo apt-get install -y libudev-dev + sudo apt-get install -y libudev-dev cargo install elf2uf2-rs - - name: Generate uf2 file - run: elf2uf2-rs -v target/thumbv6m-none-eabi/release/subtone + - name: Build + run: | + cargo build --verbose --release --features call-tone + mv target/thumbv6m-none-eabi/release/subtone target/thumbv6m-none-eabi/release/subtone-calltone + cargo build --verbose --release + - name: Generate uf2 files + run: | + elf2uf2-rs -v target/thumbv6m-none-eabi/release/subtone-calltone + elf2uf2-rs -v target/thumbv6m-none-eabi/release/subtone - name: Archive production artifacts uses: actions/upload-artifact@v3 with: @@ -38,3 +43,5 @@ jobs: path: | target/thumbv6m-none-eabi/release/subtone target/thumbv6m-none-eabi/release/subtone.uf2 + target/thumbv6m-none-eabi/release/subtone-calltone + target/thumbv6m-none-eabi/release/subtone-calltone.uf2 diff --git a/Cargo.toml b/Cargo.toml index 0cf5956..e80d8f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "subtone" -version = "0.1.0" +version = "0.2.0" license = "MIT" resolver = "2" @@ -100,3 +100,7 @@ debug-assertions = false incremental = false lto = 'fat' opt-level = 's' + +[features] +default = [] +call-tone = [] diff --git a/Oscilloscope.png b/Oscilloscope.png new file mode 100644 index 0000000..6fc7eff --- /dev/null +++ b/Oscilloscope.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d2318b4102e85552bb978e8dc2fe8b6258385c3334be83c1a86239a892a9d5d +size 59044 diff --git a/README.md b/README.md index e8aaa5b..499206e 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,18 @@ An embedded rust project to generate [CTCSS](https://en.wikipedia.org/wiki/Continuous_Tone-Coded_Squelch_System)-Tones via [PDM](https://en.wikipedia.org/wiki/Pulse-density_modulation) on a cheap [RP2040](https://en.wikipedia.org/wiki/RP2040) board. +Due to PDM (aka Delta-Modulation) a simple rc-filter is sufficient for +DA-conversation. +(eg. 100Ω/3.3µF if not using 1750Hz option or 100Ω/470nF otherwise) +Prefer tantalum caps for better switching supression. ![prototype](Prototype.jpg "Prototype attached to 23cm tranciever") +Waveform Example +---------------- + +![waveform](Oscilloscope.png "Waveform example on oscilloscope") + Schematics ---------- @@ -21,4 +30,6 @@ Usage * Use rotary encoder to select wanted frequency. * Press button short for on/off toggling. -* Press long to store current setting. \ No newline at end of file +* Press long to store current setting. +* With 1750Hz call-tone option on this frequency short button push just emmits + a one second sine pulse and is quiet otherwise. \ No newline at end of file diff --git a/build.rs b/build.rs index be50ac8..d6457ae 100644 --- a/build.rs +++ b/build.rs @@ -82,7 +82,7 @@ fn bmp2bitstr(s: &str) -> String { fn fontset(p: PathBuf) { let sources = [ - "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "space", "dash", "dot", "off", "mem", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "space", "tone", "dot", "off", "mem", ]; let mut bitmaps = "".to_string(); for filename in sources { @@ -139,22 +139,24 @@ fn main() { let pdm = pdm_table(1 << PDM_BITS, i_sine); // 8 cycles in one table for higher frequencies - let pdm_8 = pdm_table(1 << PDM_BITS, |i, s| i_sine(i * 8, s)); + let pdm_8 = pdm_table(1 << PDM_BITS, |i, s| i_sine(i << 3, s)); let clk_div_1hz = 125_000_000.0 / 2.0f32.powi(PDM_BITS as i32); write( out.join("pdm_table.rs"), format!( "\ const PDM_TABLE: [u32; {}] = {:?};\n\ -#[allow(dead_code)]\n\ const PDM8_TABLE: [u32; {}] = {:?};\n\ const CLK_DIV_1HZ: f32 = {};\n\ +#[allow(dead_code)]\n\ +const CLK_DIV_8HZ: f32 = {};\n\ ", pdm.len(), pdm, pdm_8.len(), pdm_8, clk_div_1hz, + clk_div_1hz * 8.0, ), ) .unwrap(); diff --git a/font/tone.bmp b/font/tone.bmp new file mode 100644 index 0000000..8a3d0ae Binary files /dev/null and b/font/tone.bmp differ diff --git a/src/main.rs b/src/main.rs index 5e3d1e3..60ca942 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,12 +38,20 @@ include!(concat!(env!("OUT_DIR"), "/fontmap.rs")); const ADDR_OFFSET: u32 = 2 * 1024 * 1024 - 4096; const FLASH_SIZE: usize = 2 * 1024 * 1024; +#[cfg(not(feature = "call-tone"))] const SUBTONES: [f32; 51] = [ 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9, 114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2, 150.0, 151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5, 203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, 250.3, 254.1, ]; +#[cfg(feature = "call-tone")] +const SUBTONES: [f32; 52] = [ + 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, + 110.9, 114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2, 150.0, 151.4, 156.7, 159.8, + 162.2, 165.5, 167.9, 171.3, 173.8, 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5, + 203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, 250.3, 254.1, 1750.0, +]; const POS: [Point; 5] = [ Point::new(0, 8), @@ -55,12 +63,17 @@ const POS: [Point; 5] = [ #[repr(C)] #[derive(Clone, Copy, PartialEq)] -struct Message { - counter: usize, +struct Message<'a> { + divider: U24F8, + pdm_table: &'a [u32], enabled: bool, } -type Config = Message; +#[derive(Clone, Copy, PartialEq)] +struct Config { + counter: usize, + enabled: bool, +} static mut CORE1_STACK: Stack<4096> = Stack::new(); static EXECUTOR0: StaticCell = StaticCell::new(); @@ -79,19 +92,24 @@ type Display = Ssd1306< BufferedGraphicsMode, >; -// let mut flash = Flash::<_, Async, FLASH_SIZE>::new(flash, dma_1); +macro_rules! send_msg { + ($dividers:ident, $pdm_tables:ident, $counter:expr, $enabled:expr) => { + CHANNEL + .send(Message { + divider: $dividers[$counter], + pdm_table: $pdm_tables[$counter], + enabled: $enabled, + }) + .await; + Timer::after(Duration::from_millis(10)).await; + }; +} + #[inline] -fn read_config(flash: &mut Flash<'_, FLASH, Async, FLASH_SIZE>) -> Config { +fn read_config<'a>(flash: &mut Flash<'a, FLASH, Async, FLASH_SIZE>) -> Config { let ref mut flash_buf = [0u8; size_of::()]; flash.read(ADDR_OFFSET, flash_buf).unwrap(); - let mut cfg = unsafe { *(flash_buf as *const u8 as *const Config) }; - if cfg.counter >= SUBTONES.len() { - cfg.counter = 0; - cfg.enabled = true; - }; - info!("Counter: {}", cfg.counter); - info!("Enabled: {}", cfg.enabled); - cfg + unsafe { *(flash_buf as *const u8 as *const Config) } } #[inline] @@ -111,42 +129,80 @@ fn write_config(flash: &mut Flash<'_, FLASH, Async, FLASH_SIZE>, cfg: Config) { #[inline] fn freq_2_divider(freq: f32) -> U24F8 { - U24F8::from_num(CLK_DIV_1HZ / freq) + #[cfg(not(feature = "call-tone"))] + { + U24F8::from_num(CLK_DIV_1HZ / freq) + } + #[cfg(feature = "call-tone")] + { + if freq < 1000.0 { + U24F8::from_num(CLK_DIV_1HZ / freq) + } else { + U24F8::from_num(CLK_DIV_8HZ / freq) + } + } } #[inline] -fn display_freq(display: &mut Display, counter: usize, enabled: bool) { - let mut v = (SUBTONES[counter] * 10.0_f32) as usize; - let mut z = (v / 1000).clamp(0, 9); - - v %= 1000; - if z == 0 { - CHARS[Font::Fspace as usize] - .translate(POS[0]) - .draw(display) - .unwrap(); +fn freq_2_pdm_table(freq: f32) -> &'static [u32] { + if freq < 1000.0 { + &PDM_TABLE } else { - CHARS[z].translate(POS[0]).draw(display).unwrap() + &PDM8_TABLE } - z = (v / 100).clamp(0, 9); - v %= 100; - CHARS[z].translate(POS[1]).draw(display).unwrap(); - z = (v / 10).clamp(0, 9); - v %= 10; - CHARS[z].translate(POS[2]).draw(display).unwrap(); - z = (v).clamp(0, 9); - if enabled { - CHARS[Font::Fdot as usize] - .translate(POS[3]) - .draw(display) - .unwrap(); +} + +#[inline] +fn display_freq(display: &mut Display, counter: usize, enabled: bool) { + // one decimal place + let mut v = (SUBTONES[counter] * 10.0_f32) as usize; + if v >= 10000 { + let mut leading_zero = true; + for p in 0..5 { + let divider = 10_usize.pow(5 - p); + let z = (v / divider).clamp(0, 9); + v %= divider; + if leading_zero && z == 0 { + CHARS[Font::Fspace as usize] + .translate(POS[p as usize]) + .draw(display) + .unwrap(); + } else { + leading_zero = false; + CHARS[z].translate(POS[p as usize]).draw(display).unwrap() + } + } } else { - CHARS[Font::Foff as usize] - .translate(POS[3]) - .draw(display) - .unwrap(); + let mut z = (v / 1000).clamp(0, 9); + v %= 1000; + if z == 0 { + CHARS[Font::Fspace as usize] + .translate(POS[0]) + .draw(display) + .unwrap(); + } else { + CHARS[z].translate(POS[0]).draw(display).unwrap() + } + z = (v / 100).clamp(0, 9); + v %= 100; + CHARS[z].translate(POS[1]).draw(display).unwrap(); + z = (v / 10).clamp(0, 9); + v %= 10; + CHARS[z].translate(POS[2]).draw(display).unwrap(); + z = (v).clamp(0, 9); + if enabled { + CHARS[Font::Fdot as usize] + .translate(POS[3]) + .draw(display) + .unwrap(); + } else { + CHARS[Font::Foff as usize] + .translate(POS[3]) + .draw(display) + .unwrap(); + } + CHARS[z].translate(POS[4]).draw(display).unwrap(); } - CHARS[z].translate(POS[4]).draw(display).unwrap(); display.flush().unwrap(); } @@ -183,8 +239,11 @@ async fn core0_task( ) { info!("Hello from core 0"); + let dividers = SUBTONES.map(freq_2_divider); + let pdm_tables = SUBTONES.map(freq_2_pdm_table); + let mut flash = Flash::<_, Async, FLASH_SIZE>::new(flash, dma_1); - let Message { + let Config { mut counter, mut enabled, } = read_config(&mut flash); @@ -196,8 +255,21 @@ async fn core0_task( display.init().unwrap(); display.set_brightness(Brightness::DIM).unwrap(); + if counter >= SUBTONES.len() { + counter = 0; + } + loop { - CHANNEL.send(Message { counter, enabled }).await; + #[cfg(not(feature = "call-tone"))] + send_msg!(dividers, pdm_tables, counter, enabled); + #[cfg(feature = "call-tone")] + { + if SUBTONES[counter] < 1000.0 { + send_msg!(dividers, pdm_tables, counter, enabled); + } else { + send_msg!(dividers, pdm_tables, counter, false); + } + } display_freq(display, counter, enabled); match select(enc.wait_for(), button.wait_for_low()).await { @@ -212,15 +284,40 @@ async fn core0_task( } }, Either::Second(_) => { + info!("Button pressed"); Timer::after(DELAY_DEFAULT).await; match with_timeout(Duration::from_millis(750), button.wait_for_high()).await { - Ok(_) => enabled = !enabled, + Ok(_) => { + #[cfg(not(feature = "call-tone"))] + { + enabled = !enabled + } + #[cfg(feature = "call-tone")] + { + if SUBTONES[counter] < 1000.0 { + enabled = !enabled + } else { + // send the tone for just a second + CHARS[Font::Ftone as usize] + .translate(POS[0]) + .draw(display) + .unwrap(); + display.flush().unwrap(); + send_msg!(dividers, pdm_tables, counter, true); + Timer::after(Duration::from_millis(1000)).await; + send_msg!(dividers, pdm_tables, counter, false); + } + } + } Err(_) => { CHARS[Font::Fmem as usize] - .translate(POS[3]) + .translate(POS[if SUBTONES[counter] < 1000.0 { 3 } else { 0 }]) .draw(display) .unwrap(); + // flashing struggles if pdm is disabled? + send_msg!(dividers, pdm_tables, counter, true); write_config(&mut flash, Config { counter, enabled }); + send_msg!(dividers, pdm_tables, counter, enabled); display.flush().unwrap(); Timer::after(Duration::from_millis(750)).await; } @@ -245,10 +342,6 @@ async fn core1_task(pdm_pin: PIN_26, pio_0: PIO0, dma_0: DMA_CH0) { .. } = Pio::new(pio_0, Irqs); let out_pin = common.make_pio_pin(pdm_pin); - let dividers = SUBTONES.map(freq_2_divider); - let Message { counter, enabled } = CHANNEL.recv().await; - let mut current_divider = dividers[counter]; - let prg = pio_asm!( ".origin 0", "set pindirs, 1", @@ -262,36 +355,37 @@ async fn core1_task(pdm_pin: PIN_26, pio_0: PIO0, dma_0: DMA_CH0) { cfg.fifo_join = FifoJoin::TxOnly; cfg.set_out_pins(&[&out_pin]); cfg.set_set_pins(&[&out_pin]); - cfg.clock_divider = current_divider; cfg.shift_out.auto_fill = true; cfg.shift_out.direction = ShiftDirection::Left; + let mut dma_out_ref = dma_0.into_ref(); + let Message { + divider, + mut pdm_table, + enabled, + } = CHANNEL.recv().await; + + cfg.clock_divider = divider; sm.set_config(&cfg); sm.set_enable(enabled); - let mut dma_out_ref = dma_0.into_ref(); - loop { match select( CHANNEL.recv(), - sm.tx().dma_push(dma_out_ref.reborrow(), &PDM_TABLE), + sm.tx().dma_push(dma_out_ref.reborrow(), pdm_table), ) .await { - Either::First(Message { counter, enabled }) => { - info!("Got counter: {}", counter); - if (current_divider.to_bits() != dividers[counter].to_bits()) || !sm.is_enabled() { - info!( - "Counter changed: {} {}", - counter, - dividers[counter].to_bits() as u32 - ); - current_divider = dividers[counter]; - sm.set_enable(false); - cfg.clock_divider = current_divider; - sm.set_config(&cfg); - sm.set_enable(enabled); - } + Either::First(Message { + divider, + pdm_table: pdm, + enabled, + }) => { + sm.set_enable(false); + cfg.clock_divider = divider; + sm.set_config(&cfg); + sm.set_enable(enabled); + pdm_table = pdm; } Either::Second(_) => (), }