├── LICENSE ├── analysis ├── .python-version ├── pyproject.toml ├── Untitled.ipynb ├── calipertron.py ├── optimal_frequencies.smt2 ├── parameter_sweep.ipynb ├── design_simulations.ipynb └── analysis.ipynb ├── 2024_11_02_caliper_demo.mp4 ├── .gitignore ├── calipertron-core ├── Cargo.toml ├── src │ ├── bin │ │ └── scratch.rs │ └── lib.rs └── Cargo.lock ├── schema ├── Cargo.toml ├── src │ └── lib.rs └── Cargo.lock ├── firmware ├── .cargo │ └── config.toml ├── src │ └── bin │ │ ├── hello.rs │ │ ├── usb_serial.rs │ │ ├── local.rs │ │ ├── recorder.rs │ │ └── usb_custom.rs ├── Cargo.toml ├── build.rs └── Cargo.lock ├── frontend ├── Cargo.toml └── src │ └── bin │ ├── record_stdout.rs │ ├── stdout.rs │ ├── parameter_sweep.rs │ └── scope.rs └── readme.md /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Kevin Lynagh. -------------------------------------------------------------------------------- /analysis/.python-version: -------------------------------------------------------------------------------- 1 | 3.12.3 2 | -------------------------------------------------------------------------------- /2024_11_02_caliper_demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynaghk/calipertron/HEAD/2024_11_02_caliper_demo.mp4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # python generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # venv 10 | .venv 11 | *.txt -------------------------------------------------------------------------------- /calipertron-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "calipertron-core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | num-traits = { version = "0.2", default-features = false, features = ["libm"] } 8 | -------------------------------------------------------------------------------- /schema/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "schema" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = { version = "1.0", default-features = false, features = ["derive"]} 8 | postcard = "*" 9 | defmt = "0.3.8" 10 | -------------------------------------------------------------------------------- /firmware/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 2 | runner = [ 3 | "probe-rs", 4 | "run", 5 | "--always-print-stacktrace", 6 | "--chip", "STM32F103C8", 7 | "--log-format", "{t} {L} {s}" 8 | ] 9 | 10 | [build] 11 | target = "thumbv7m-none-eabi" 12 | 13 | [env] 14 | DEFMT_LOG = "info" 15 | -------------------------------------------------------------------------------- /frontend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frontend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | schema = { path = "../schema" } 8 | nusb = "0.1" 9 | futures-lite = "2" 10 | egui = {version = "0.28.1" } 11 | eframe = "0.28.1" 12 | egui_plot = "0.28.1" 13 | flume = "0.11.0" 14 | tokio = { version = "1.41", features = ["full"] } -------------------------------------------------------------------------------- /calipertron-core/src/bin/scratch.rs: -------------------------------------------------------------------------------- 1 | use calipertron_core::*; 2 | use core::f32::consts::PI; 3 | 4 | pub fn main() { 5 | let mut accumulator = PositionAccumulator::new(0.0, 0.1); 6 | for position in 0..100 { 7 | let angle = (position as f32 * 0.1 * PI + PI) % (2.0 * PI) - PI; 8 | accumulator.update(angle); 9 | let position = accumulator.get_position(); 10 | println!("Position: {}", position); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /firmware/src/bin/hello.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use defmt::info; 5 | use embassy_executor::Spawner; 6 | use embassy_stm32::Config; 7 | use embassy_time::Timer; 8 | use {defmt_rtt as _, panic_probe as _}; 9 | 10 | #[embassy_executor::main] 11 | async fn main(_spawner: Spawner) -> ! { 12 | let config = Config::default(); 13 | let _p = embassy_stm32::init(config); 14 | 15 | loop { 16 | info!("Hello World!"); 17 | Timer::after_secs(1).await; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /analysis/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "calipertron" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "numpy>=1.26.4", 9 | "matplotlib>=3.8.4", 10 | "ipywidgets>=8.1.2", 11 | "ipykernel>=6.29.4", 12 | "ipympl>=0.9.4", 13 | "plotnine>=0.13.6", 14 | "pandas>=2.2.2", 15 | "seaborn>=0.13.2", 16 | "plotly>=5.23.0", 17 | "nbformat>=5.10.4", 18 | "polars>=1.12.0", 19 | "hvplot>=0.11.1", 20 | ] 21 | -------------------------------------------------------------------------------- /calipertron-core/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.4.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 10 | 11 | [[package]] 12 | name = "calipertron-core" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "num-traits", 16 | ] 17 | 18 | [[package]] 19 | name = "libm" 20 | version = "0.2.8" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" 23 | 24 | [[package]] 25 | name = "num-traits" 26 | version = "0.2.19" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 29 | dependencies = [ 30 | "autocfg", 31 | "libm", 32 | ] 33 | -------------------------------------------------------------------------------- /calipertron-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | use core::f32::consts::PI; 4 | 5 | use num_traits::Float; 6 | pub struct PhaseAccumulator { 7 | pub unwrapped_phase: f32, 8 | last_phase: f32, 9 | hysteresis_threshold: f32, 10 | } 11 | 12 | impl PhaseAccumulator { 13 | pub fn new(initial_phase: f32, hysteresis_threshold: f32) -> Self { 14 | PhaseAccumulator { 15 | unwrapped_phase: 0.0, 16 | last_phase: initial_phase, 17 | hysteresis_threshold, 18 | } 19 | } 20 | 21 | pub fn update(&mut self, new_phase: f32) { 22 | let mut delta = new_phase - self.last_phase; 23 | 24 | // Handle wraparound 25 | if delta > PI { 26 | delta -= 2.0 * PI; 27 | } else if delta < -PI { 28 | delta += 2.0 * PI; 29 | } 30 | 31 | // Apply hysteresis 32 | if delta.abs() > self.hysteresis_threshold { 33 | self.unwrapped_phase += delta; 34 | self.last_phase = new_phase; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /analysis/Untitled.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "a7aa3838-a57c-4a45-bbb3-ca19f1604426", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "%matplotlib ipympl\n", 11 | "from ipywidgets import *\n", 12 | "import numpy as np\n", 13 | "import matplotlib.pyplot as plt\n", 14 | "from calipertron import *\n", 15 | "\n", 16 | "t = np.linspace(0, 4 * PI)\n", 17 | "\n", 18 | "fig = plt.figure()\n", 19 | "ax = fig.add_subplot(1, 1, 1)\n", 20 | "line, = ax.plot(t, adder(t, 0))\n", 21 | "\n", 22 | "def update(position = widgets.FloatSlider(min=0, max=20, step=0.1, value=0)):\n", 23 | " line.set_ydata(adder(t, position))\n", 24 | " fig.canvas.draw_idle()\n", 25 | "\n", 26 | "\n", 27 | "interact(update);" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "id": "5c60a81e-03ec-4afb-9324-9b7bb32946f3", 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [] 37 | } 38 | ], 39 | "metadata": { 40 | "kernelspec": { 41 | "display_name": "Python 3 (ipykernel)", 42 | "language": "python", 43 | "name": "python3" 44 | }, 45 | "language_info": { 46 | "codemirror_mode": { 47 | "name": "ipython", 48 | "version": 3 49 | }, 50 | "file_extension": ".py", 51 | "mimetype": "text/x-python", 52 | "name": "python", 53 | "nbconvert_exporter": "python", 54 | "pygments_lexer": "ipython3", 55 | "version": "3.12.3" 56 | } 57 | }, 58 | "nbformat": 4, 59 | "nbformat_minor": 5 60 | } 61 | -------------------------------------------------------------------------------- /schema/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize, PartialEq, Debug, Clone, defmt::Format)] 6 | pub enum AdcSamplingPeriod { 7 | CYCLES1_5, 8 | CYCLES7_5, 9 | CYCLES13_5, 10 | CYCLES28_5, 11 | CYCLES41_5, 12 | CYCLES55_5, 13 | CYCLES71_5, 14 | CYCLES239_5, 15 | } 16 | 17 | impl AdcSamplingPeriod { 18 | #[allow(non_snake_case)] 19 | pub fn to_Hz(&self) -> f64 { 20 | use AdcSamplingPeriod::*; 21 | let sample_cycles = match self { 22 | CYCLES1_5 => 1.5, 23 | CYCLES7_5 => 7.5, 24 | CYCLES13_5 => 13.5, 25 | CYCLES28_5 => 28.5, 26 | CYCLES41_5 => 41.5, 27 | CYCLES55_5 => 55.5, 28 | CYCLES71_5 => 71.5, 29 | CYCLES239_5 => 239.5, 30 | }; 31 | 32 | let adc_frequency = 12_000_000.; 33 | let adc_sample_overhead_cycles = 12.5; // see reference manual section 11.6 34 | adc_frequency / (sample_cycles + adc_sample_overhead_cycles) 35 | } 36 | } 37 | 38 | #[derive(Serialize, Deserialize, PartialEq, Debug, Clone, defmt::Format)] 39 | #[allow(non_snake_case)] 40 | pub enum Command { 41 | SetFrequency { 42 | frequency_kHz: f64, 43 | adc_sampling_period: AdcSamplingPeriod, 44 | }, 45 | Record, 46 | } 47 | 48 | impl Command { 49 | pub fn serialize<'a>(&self, buf: &'a mut [u8]) -> Result<&'a mut [u8], postcard::Error> { 50 | postcard::to_slice(self, buf) 51 | } 52 | 53 | pub fn deserialize(bs: &[u8]) -> Option { 54 | postcard::from_bytes(bs).ok() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /firmware/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "calipertron" 4 | version = "0.1.0" 5 | authors = ["Kevin J. Lynagh "] 6 | 7 | [dependencies] 8 | schema = { path = "../schema" } 9 | calipertron-core = { path = "../calipertron-core" } 10 | 11 | embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", features = ["defmt", "stm32f103c8", "unstable-pac", "memory-x", "time-driver-any"] } 12 | embassy-sync = { git = "https://github.com/embassy-rs/embassy", features = ["defmt"] } 13 | embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = ["arch-cortex-m", "executor-thread", "defmt", "integrated-timers"] } 14 | embassy-time = { git = "https://github.com/embassy-rs/embassy", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] } 15 | embassy-usb = { git = "https://github.com/embassy-rs/embassy", features = ["defmt"] } 16 | embassy-futures = { git = "https://github.com/embassy-rs/embassy" } 17 | 18 | defmt = "0.3" 19 | defmt-rtt = "0.4" 20 | 21 | cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] } 22 | cortex-m-rt = "0.7.0" 23 | embedded-hal = "0.2.6" 24 | panic-probe = { version = "0.3", features = ["print-defmt"] } 25 | heapless = { version = "0.8", default-features = false } 26 | nb = "1.0.0" 27 | static_cell = "2.0.0" 28 | bytemuck = "1.16.3" 29 | 30 | # need this for arctangent on nostd 31 | num-traits = { version = "0.2", default-features = false, features = ["libm"] } 32 | #log = { version = "0.4" } 33 | 34 | 35 | [profile.dev] 36 | opt-level = "s" 37 | 38 | [profile.release] 39 | debug = 2 40 | lto = true 41 | opt-level = "s" 42 | incremental = false 43 | codegen-units = 1 44 | -------------------------------------------------------------------------------- /analysis/calipertron.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | Pi = np.pi 5 | Shift = 85 * (Pi/180) 6 | #Shift = 2 * PI / 3 7 | 8 | # Number of positive signals on the scale. 9 | N = 3 10 | 11 | def signal(t, idx): 12 | sign = 1 if idx % (2*N) < N else -1 13 | shift = (idx%N) * Shift 14 | return sign*np.sin(t + shift) 15 | 16 | 17 | # Combined signal when the adder is at a given position 18 | def adder(t, position): 19 | position = position % (2*N) 20 | a = position 21 | b = position + N 22 | sum = 0; 23 | # check 3 sets of tracks so we don't need to consider the right side of the adder wrapping around. 24 | for idx in range(3*N): 25 | overlap = 0 26 | # track is entirely within adder 27 | if a <= idx and b >= idx + 1: 28 | overlap = 1 29 | 30 | # left edge of adder only partially overlaps this track 31 | if idx < a < idx + 1: 32 | overlap = idx + 1 - a 33 | 34 | # right edge of adder only partially overlaps this track 35 | if idx < b < idx + 1: 36 | overlap = b - idx 37 | 38 | #print((idx, overlap)) 39 | sum += overlap * signal(t, idx) 40 | return sum 41 | 42 | 43 | def first_zero_crossing(t, y): 44 | sign_changes = np.where(np.diff(y > 0))[0] 45 | 46 | # filter for positive to negative transitions 47 | zero_crossings = t[sign_changes][y[sign_changes] > 0] 48 | return zero_crossings[0] 49 | 50 | 51 | def main(): 52 | 53 | t = np.linspace(0, 8*Pi, 5000) 54 | res = []; 55 | for x in np.linspace(0, 10, 100): 56 | y = adder(t, x) 57 | first_zero = first_zero_crossing(t, y) 58 | res.append([x, first_zero]) 59 | res = np.array(res) 60 | plt.plot(res[:,0], res[:,1]) 61 | plt.show() 62 | 63 | 64 | if __name__ == "__main__": 65 | main() 66 | -------------------------------------------------------------------------------- /frontend/src/bin/record_stdout.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | use schema::Command; 4 | 5 | fn main() { 6 | // Parse command-line argument for frequency 7 | let frequency_kHz = parse_frequency_arg(); 8 | 9 | let di = nusb::list_devices() 10 | .unwrap() 11 | .find(|d| d.vendor_id() == 0xc0de && d.product_id() == 0xcafe) 12 | .expect("device should be connected"); 13 | 14 | eprintln!("Device info: {di:?}"); 15 | 16 | let device = di.open().unwrap(); 17 | 18 | let interface = device.claim_interface(0).unwrap(); 19 | 20 | // Send frequency command to firmware 21 | let endpoint_addr = 1; 22 | let mut out_queue = interface.bulk_out_queue(endpoint_addr); 23 | 24 | send_command(&mut out_queue, Command::SetFrequency { frequency_kHz }); 25 | 26 | send_command(&mut out_queue, Command::Record); 27 | 28 | // Read and print ADC values 29 | let mut queue = interface.bulk_in_queue(0x80 + endpoint_addr); 30 | let transfer_size = 64; 31 | 32 | loop { 33 | while queue.pending() < 1 { 34 | queue.submit(nusb::transfer::RequestBuffer::new(transfer_size)); 35 | } 36 | 37 | let completion = futures_lite::future::block_on(queue.next_complete()); 38 | 39 | let data = completion.data.as_slice(); 40 | for chunk in data.chunks_exact(2) { 41 | if let [low, high] = chunk { 42 | let adc_value = u16::from_le_bytes([*low, *high]); 43 | println!("{}", adc_value); 44 | } 45 | } 46 | queue.submit(nusb::transfer::RequestBuffer::reuse( 47 | completion.data, 48 | transfer_size, 49 | )); 50 | } 51 | } 52 | 53 | fn parse_frequency_arg() -> f64 { 54 | let args: Vec = std::env::args().collect(); 55 | if args.len() != 2 { 56 | eprintln!("Usage: {} ", args[0]); 57 | std::process::exit(1); 58 | } 59 | 60 | match args[1].parse::() { 61 | Ok(freq) if freq >= 0.0 && freq <= 100.0 => freq, 62 | _ => { 63 | eprintln!("Error: Frequency must be a number between 0 and 100 kHz"); 64 | std::process::exit(1); 65 | } 66 | } 67 | } 68 | 69 | fn send_command(out_queue: &mut nusb::transfer::Queue>, command: Command) { 70 | let mut buf = [0u8; 64]; // Assuming MAX_PACKET_SIZE is 64 71 | if let Ok(serialized) = command.serialize(&mut buf) { 72 | out_queue.submit(serialized.into()); 73 | } else { 74 | eprintln!("Error: Failed to serialize command"); 75 | std::process::exit(1); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /frontend/src/bin/stdout.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | use schema::Command; 4 | 5 | fn main() { 6 | // Parse command-line argument for frequency 7 | let frequency_kHz = parse_frequency_arg(); 8 | 9 | let di = nusb::list_devices() 10 | .unwrap() 11 | .find(|d| d.vendor_id() == 0xc0de && d.product_id() == 0xcafe) 12 | .expect("device should be connected"); 13 | 14 | eprintln!("Device info: {di:?}"); 15 | 16 | let device = di.open().unwrap(); 17 | 18 | let interface = device.claim_interface(0).unwrap(); 19 | 20 | // Send frequency command to firmware 21 | let endpoint_addr = 1; 22 | let mut out_queue = interface.bulk_out_queue(endpoint_addr); 23 | send_frequency_command(&mut out_queue, frequency_kHz); 24 | 25 | // Read and print ADC values 26 | let mut queue = interface.bulk_in_queue(0x80 + endpoint_addr); 27 | let transfer_size = 64; 28 | 29 | loop { 30 | while queue.pending() < 1 { 31 | queue.submit(nusb::transfer::RequestBuffer::new(transfer_size)); 32 | } 33 | 34 | let completion = futures_lite::future::block_on(queue.next_complete()); 35 | 36 | let data = completion.data.as_slice(); 37 | for chunk in data.chunks_exact(2) { 38 | if let [low, high] = chunk { 39 | let adc_value = u16::from_le_bytes([*low, *high]); 40 | //println!("ADC value: {} mV", adc_value); 41 | println!("{}", adc_value); 42 | } 43 | } 44 | queue.submit(nusb::transfer::RequestBuffer::reuse( 45 | completion.data, 46 | transfer_size, 47 | )); 48 | } 49 | } 50 | 51 | fn parse_frequency_arg() -> f64 { 52 | let args: Vec = std::env::args().collect(); 53 | if args.len() != 2 { 54 | eprintln!("Usage: {} ", args[0]); 55 | std::process::exit(1); 56 | } 57 | 58 | match args[1].parse::() { 59 | Ok(freq) if freq >= 0.0 && freq <= 100.0 => freq, 60 | _ => { 61 | eprintln!("Error: Frequency must be a number between 0 and 100 kHz"); 62 | std::process::exit(1); 63 | } 64 | } 65 | } 66 | 67 | fn send_frequency_command(out_queue: &mut nusb::transfer::Queue>, frequency_kHz: f64) { 68 | let command = Command::SetFrequency { frequency_kHz }; 69 | let mut buf = [0u8; 64]; // Assuming MAX_PACKET_SIZE is 64 70 | if let Ok(serialized) = command.serialize(&mut buf) { 71 | out_queue.submit(serialized.into()); 72 | } else { 73 | eprintln!("Error: Failed to serialize frequency command"); 74 | std::process::exit(1); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /analysis/optimal_frequencies.smt2: -------------------------------------------------------------------------------- 1 | (define-const kilo Int 1000) 2 | (define-const mega Int 1000000) 3 | 4 | 5 | (declare-const sampling-frequency Real) 6 | (declare-const sampling-period Real) 7 | (assert (= sampling-frequency (/ 1 sampling-period))) 8 | 9 | (declare-const adc-clock-cycles-per-sample Real) 10 | (declare-const adc-clock-frequency Real) 11 | 12 | 13 | (declare-const fft-n Int) 14 | (assert (or 15 | ;; (= fft-n 64) 16 | ;; (= fft-n 128) 17 | (= fft-n 256) 18 | ;; (= fft-n 512) 19 | ;;(= fft-n 1024) 20 | )) 21 | 22 | (declare-const fft-bin-resolution Real) 23 | (assert (= fft-bin-resolution (/ sampling-frequency fft-n))) 24 | 25 | 26 | (declare-const signal-bin-idx Int) 27 | 28 | ;; real signal, so it should be in the first half of the fft bins 29 | (assert (<= 0 signal-bin-idx (/ fft-n 2))) 30 | 31 | 32 | ;;;;;;;;;;;;;;;;;;;;;;;; 33 | ;; PDM signal emission 34 | 35 | (define-const pdm-samples-n Int 132) 36 | (declare-const pdm-signal-frequency Real) 37 | 38 | (assert (< 0 pdm-signal-frequency (* 72 mega))) 39 | 40 | ;; loss function is how far our emitted signal frequency is from an fft bin's 41 | (declare-const loss Real) 42 | (assert (= loss (^ (- (* signal-bin-idx fft-bin-resolution) pdm-signal-frequency) 43 | 2))) 44 | 45 | ;; assuming line noise is in the first bin, make sure our signal isn't 46 | (assert (< 1 (/ pdm-signal-frequency fft-bin-resolution))) 47 | 48 | ;; lets have some extra margin above the nyquyst limit 49 | (assert (> sampling-frequency (* 4 pdm-signal-frequency))) 50 | 51 | 52 | 53 | ;;;;;;;;;;;;; 54 | ;; stm32f103 55 | 56 | (assert (= adc-clock-frequency (* 12 mega))) 57 | (define-const adc-sample-overhead-cycles Real 12.5) 58 | 59 | ;; 11.12.4 ADC sample time register 1 (ADC_SMPR1) 60 | (assert (or 61 | ;; (= adc-clock-cycles-per-sample 1.5) 62 | ;; (= adc-clock-cycles-per-sample 7.5) 63 | ;; (= adc-clock-cycles-per-sample 13.5) 64 | ;; (= adc-clock-cycles-per-sample 28.5) 65 | ;; (= adc-clock-cycles-per-sample 41.5) 66 | ;; (= adc-clock-cycles-per-sample 55.5) 67 | ;; (= adc-clock-cycles-per-sample 71.5) 68 | (= adc-clock-cycles-per-sample 239.5))) 69 | 70 | (assert (= sampling-period (/ (+ adc-clock-cycles-per-sample 71 | adc-sample-overhead-cycles) 72 | adc-clock-frequency))) 73 | 74 | 75 | 76 | 77 | (minimize loss) 78 | 79 | (set-option :pp.decimal true) 80 | (check-sat) 81 | (get-model) 82 | 83 | ;;(eval sampling-frequency) 84 | 85 | 86 | 87 | ;; ( 88 | ;; (define-fun fft-n () Int 89 | ;; 128) 90 | ;; (define-fun mega () Int 91 | ;; 1000000) 92 | ;; (define-fun signal-bin-idx () Int 93 | ;; 1) 94 | ;; (define-fun pdm-signal-frequency () Real 95 | ;; 433.0) 96 | ;; (define-fun pdm-samples-n () Int 97 | ;; 132) 98 | ;; (define-fun kilo () Int 99 | ;; 1000) 100 | ;; (define-fun adc-sample-overhead-cycles () Real 101 | ;; 12.5) 102 | ;; (define-fun fft-bin-resolution () Real 103 | ;; 372.0238095238?) 104 | ;; (define-fun sampling-frequency () Real 105 | ;; 47619.0476190476?) 106 | ;; (define-fun sampling-period () Real 107 | ;; 0.000021) 108 | ;; (define-fun adc-clock-frequency () Real 109 | ;; 12000000.0) 110 | ;; (define-fun adc-clock-cycles-per-sample () Real 111 | ;; 239.5) 112 | ;; (define-fun loss () Real 113 | ;; 3718.0958049886?) 114 | ;; (define-fun /0 ((x!0 Real) (x!1 Real)) Real 115 | ;; (ite (and (= x!0 60.0) (= x!1 372.0238095238?)) 0.16128 116 | ;; (ite (and (= x!0 433.0) (= x!1 372.0238095238?)) 1.163904 117 | ;; 372.0238095238?))) 118 | ;; ) 119 | -------------------------------------------------------------------------------- /firmware/build.rs: -------------------------------------------------------------------------------- 1 | use std::f64::consts::PI; 2 | use std::fs::File; 3 | use std::io::Write; 4 | 5 | fn generate_pdm_bsrr(n_samples: usize) -> String { 6 | let mut output = String::new(); 7 | output.push_str("pub const PDM_SIGNAL: [u32; "); 8 | output.push_str(&n_samples.to_string()); 9 | output.push_str("] = [\n"); 10 | 11 | let n_waves = 8; 12 | 13 | // in PCB schematic v1.1 the pins PA0--PA7 are wired up for signal idx 0,4, 1,5, 2,6, 3,7 14 | let mut wave_idx_to_pin = [0; 8]; 15 | for (pin_idx, wave_idx) in [0, 4, 1, 5, 2, 6, 3, 7].into_iter().enumerate() { 16 | wave_idx_to_pin[wave_idx] = pin_idx; 17 | } 18 | 19 | let mut errors = vec![0.0; n_waves]; 20 | for sample in 0..n_samples { 21 | let mut bsrr = 0u32; 22 | for wave in 0..n_waves { 23 | let phase_offset = 2.0 * PI * (wave as f64) / (n_waves as f64); 24 | let angle = 2.0 * PI * (sample as f64 / n_samples as f64) + phase_offset; 25 | let cosine = angle.cos() as f32; 26 | let normalized_signal = (cosine + 1.0) / 2.0; 27 | 28 | // rescale 29 | let scale = 1.0; 30 | let normalized_signal = (1.0 - scale) / 2.0 + scale * normalized_signal; 31 | 32 | if normalized_signal > errors[wave] { 33 | bsrr |= 1 << wave_idx_to_pin[wave]; // set bit 34 | errors[wave] += 1.0 - normalized_signal; 35 | } else { 36 | bsrr |= 1 << (wave_idx_to_pin[wave] + 16); // reset bit 37 | errors[wave] -= normalized_signal; 38 | } 39 | } 40 | output.push_str(&format!(" {:#034b},\n", bsrr)); 41 | } 42 | 43 | output.push_str("];\n"); 44 | output 45 | } 46 | 47 | fn generate_sine_cosine_table( 48 | signal_frequency: f64, 49 | sampling_frequency: f64, 50 | num_samples: usize, 51 | ) -> String { 52 | let mut output = String::new(); 53 | output.push_str("pub const SINE_COSINE_TABLE: [(f32, f32); "); 54 | output.push_str(&num_samples.to_string()); 55 | output.push_str("] = [\n"); 56 | 57 | for i in 0..num_samples { 58 | let angle = 2.0 * PI * signal_frequency * (i as f64 * (1.0 / sampling_frequency)); 59 | let sine = angle.sin() as f32; 60 | let cosine = angle.cos() as f32; 61 | output.push_str(&format!(" ({:?}, {:?}),\n", sine, cosine)); 62 | } 63 | 64 | output.push_str("];\n"); 65 | output 66 | } 67 | 68 | fn main() { 69 | println!("cargo:rustc-link-arg-bins=--nmagic"); 70 | println!("cargo:rustc-link-arg-bins=-Tlink.x"); 71 | println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); 72 | 73 | let out_dir = std::env::var("OUT_DIR").unwrap(); 74 | let dest_path = std::path::Path::new(&out_dir).join("constants.rs"); 75 | let mut f = File::create(&dest_path).unwrap(); 76 | 77 | let pdm_frequency: u32 = 222_000; 78 | f.write_all(format!("pub const PDM_FREQUENCY: u32 = {:?};\n", pdm_frequency).as_bytes()) 79 | .unwrap(); 80 | 81 | let pdm_length = 128; 82 | let num_samples = 128; 83 | 84 | let signal_frequency = pdm_frequency as f64 / pdm_length as f64; 85 | let adc_frequency = 12_000_000.; 86 | // let adc_sample_cycles = 239.5; 87 | // let adc_sample_cycles = 71.5; 88 | let adc_sample_cycles = 41.5; 89 | let adc_sample_overhead_cycles = 12.5; // see reference manual section 11.6 90 | let sampling_frequency = adc_frequency / (adc_sample_cycles + adc_sample_overhead_cycles); 91 | 92 | f.write_all( 93 | generate_sine_cosine_table(signal_frequency, sampling_frequency, num_samples).as_bytes(), 94 | ) 95 | .unwrap(); 96 | 97 | f.write_all(generate_pdm_bsrr(pdm_length).as_bytes()) 98 | .unwrap(); 99 | 100 | // Tell Cargo to rerun this script if the source file changes 101 | println!("cargo:rerun-if-changed=build.rs"); 102 | } 103 | -------------------------------------------------------------------------------- /frontend/src/bin/parameter_sweep.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | // This binary sweeps the frequency of the PDM signal and records the ADC values to a file. 4 | // Use with "Recorder" firmware. 5 | 6 | use schema::*; 7 | use std::io::{BufWriter, Write}; 8 | use tokio::time::timeout; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<(), Box> { 12 | let file = std::fs::File::create("parameter_sweep.csv")?; 13 | let mut csv_writer = BufWriter::new(file); 14 | writeln!(csv_writer, "pdm_frequency,sampling_frequency,n,sample")?; 15 | 16 | for frequency_kHz in (32..256).step_by(2) { 17 | use AdcSamplingPeriod::*; 18 | for adc_sampling_period in &[ 19 | CYCLES1_5, 20 | CYCLES7_5, 21 | CYCLES13_5, 22 | CYCLES28_5, 23 | CYCLES41_5, 24 | CYCLES55_5, 25 | CYCLES71_5, 26 | CYCLES239_5, 27 | ] { 28 | 'connection: loop { 29 | let Some(di) = nusb::list_devices()? 30 | .find(|d| d.vendor_id() == 0xc0de && d.product_id() == 0xcafe) 31 | else { 32 | continue 'connection; 33 | }; 34 | 35 | let Ok(device) = di.open() else { 36 | continue 'connection; 37 | }; 38 | 39 | let interface = device.claim_interface(0)?; 40 | 41 | let endpoint_addr = 1; 42 | let mut out_queue = interface.bulk_out_queue(endpoint_addr); 43 | 44 | let mut queue = interface.bulk_in_queue(0x80 + endpoint_addr); 45 | let transfer_size = 64; 46 | 47 | send_command( 48 | &mut out_queue, 49 | Command::SetFrequency { 50 | frequency_kHz: frequency_kHz as f64, 51 | adc_sampling_period: adc_sampling_period.clone(), 52 | }, 53 | ); 54 | 55 | send_command(&mut out_queue, Command::Record); 56 | 57 | let num_recorded_packets = 128; 58 | let mut samples = Vec::new(); 59 | for _packet_idx in 0..num_recorded_packets { 60 | if queue.pending() == 0 { 61 | queue.submit(nusb::transfer::RequestBuffer::new(transfer_size)); 62 | } 63 | 64 | let completion = 65 | match timeout(std::time::Duration::from_secs(1), queue.next_complete()) 66 | .await 67 | { 68 | Ok(completion) => completion, 69 | Err(_) => { 70 | println!("Device frozen, please reset"); 71 | continue 'connection; 72 | } 73 | }; 74 | 75 | let data = completion.data.as_slice(); 76 | 77 | // Not sure what's going on here, but after device freezes and we reset it and reconnect, we end up getting some 0 length packets. 78 | // in that case, just start over 79 | if data.len() == 0 { 80 | continue 'connection; 81 | } 82 | 83 | for chunk in data.chunks_exact(2) { 84 | if let [low, high] = chunk { 85 | let adc_value = u16::from_le_bytes([*low, *high]); 86 | samples.push(adc_value); 87 | } 88 | } 89 | queue.submit(nusb::transfer::RequestBuffer::reuse( 90 | completion.data, 91 | transfer_size, 92 | )); 93 | } 94 | 95 | println!( 96 | "Recorded {} samples at {} kHz", 97 | samples.len(), 98 | frequency_kHz 99 | ); 100 | 101 | for (idx, sample) in samples.iter().enumerate() { 102 | writeln!( 103 | csv_writer, 104 | "{},{},{},{}", 105 | frequency_kHz * 1000, 106 | adc_sampling_period.to_Hz(), 107 | idx, 108 | sample 109 | )?; 110 | } 111 | break 'connection; 112 | } 113 | } 114 | } 115 | 116 | csv_writer.flush()?; 117 | Ok(()) 118 | } 119 | 120 | fn send_command(out_queue: &mut nusb::transfer::Queue>, command: Command) { 121 | let mut buf = [0u8; 64]; // Assuming MAX_PACKET_SIZE is 64 122 | if let Ok(serialized) = command.serialize(&mut buf) { 123 | out_queue.submit(serialized.into()); 124 | } else { 125 | eprintln!("Error: Failed to serialize command"); 126 | std::process::exit(1); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /firmware/src/bin/usb_serial.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use defmt::{panic, *}; 5 | use embassy_executor::Spawner; 6 | use embassy_futures::join::join; 7 | use embassy_stm32::gpio::{Level, Output, Speed}; 8 | use embassy_stm32::time::Hertz; 9 | use embassy_stm32::usb::{Driver, Instance}; 10 | use embassy_stm32::{bind_interrupts, peripherals, usb, Config}; 11 | use embassy_time::Timer; 12 | use embassy_usb::class::cdc_acm::{CdcAcmClass, State}; 13 | use embassy_usb::driver::EndpointError; 14 | use embassy_usb::Builder; 15 | use {defmt_rtt as _, panic_probe as _}; 16 | 17 | bind_interrupts!(struct Irqs { 18 | USB_LP_CAN1_RX0 => usb::InterruptHandler; 19 | }); 20 | 21 | bind_interrupts!(struct AdcIrqs { 22 | ADC1_2 => adc::InterruptHandler; 23 | }); 24 | 25 | const MAX_PACKET_SIZE: u8 = 64; 26 | 27 | #[embassy_executor::main] 28 | async fn main(_spawner: Spawner) { 29 | let mut config = Config::default(); 30 | { 31 | use embassy_stm32::rcc::*; 32 | config.rcc.hse = Some(Hse { 33 | freq: Hertz(8_000_000), 34 | mode: HseMode::Oscillator, 35 | }); 36 | config.rcc.pll = Some(Pll { 37 | src: PllSource::HSE, 38 | prediv: PllPreDiv::DIV1, 39 | mul: PllMul::MUL9, 40 | }); 41 | config.rcc.sys = Sysclk::PLL1_P; 42 | config.rcc.ahb_pre = AHBPrescaler::DIV1; 43 | config.rcc.apb1_pre = APBPrescaler::DIV2; 44 | config.rcc.apb2_pre = APBPrescaler::DIV1; 45 | } 46 | let mut p = embassy_stm32::init(config); 47 | 48 | info!("Hello World!"); 49 | 50 | { 51 | // Board has a pull-up resistor on the D+ line; pull it down to send a RESET condition to the USB bus. 52 | // This forced reset is needed only for development, without it host will not reset your device when you upload new firmware. 53 | let _dp = Output::new(&mut p.PA12, Level::Low, Speed::Low); 54 | Timer::after_millis(10).await; 55 | } 56 | 57 | let driver = Driver::new(p.USB, Irqs, p.PA12, p.PA11); 58 | let (vid, pid) = (0xc0de, 0xcafe); 59 | let mut config = embassy_usb::Config::new(vid, pid); 60 | config.max_packet_size_0 = MAX_PACKET_SIZE; 61 | 62 | // Create embassy-usb DeviceBuilder using the driver and config. 63 | // It needs some buffers for building the descriptors. 64 | let mut config_descriptor = [0; 256]; 65 | let mut bos_descriptor = [0; 256]; 66 | let mut control_buf = [0; 7]; 67 | 68 | let mut state = State::new(); 69 | 70 | let mut builder = Builder::new( 71 | driver, 72 | config, 73 | &mut config_descriptor, 74 | &mut bos_descriptor, 75 | &mut [], // no msos descriptors 76 | &mut control_buf, 77 | ); 78 | 79 | let mut class = CdcAcmClass::new(&mut builder, &mut state, MAX_PACKET_SIZE as u16); 80 | let mut usb = builder.build(); 81 | let usb_fut = usb.run(); 82 | 83 | let mut adc = Adc::new(p.ADC1); 84 | let mut pin = p.PB1; 85 | 86 | let fut = async { 87 | loop { 88 | class.wait_connection().await; 89 | info!("Connected"); 90 | //let _ = echo(&mut class).await; 91 | let _ = stream_adc(&mut class, &mut adc, &mut pin).await; 92 | info!("Disconnected"); 93 | } 94 | }; 95 | 96 | // Run everything concurrently. 97 | // If we had made everything `'static` above instead, we could do this using separate tasks instead. 98 | join(usb_fut, fut).await; 99 | } 100 | 101 | struct Disconnected {} 102 | 103 | impl From for Disconnected { 104 | fn from(val: EndpointError) -> Self { 105 | match val { 106 | EndpointError::BufferOverflow => panic!("Buffer overflow"), 107 | EndpointError::Disabled => Disconnected {}, 108 | } 109 | } 110 | } 111 | 112 | async fn echo<'d, T: Instance + 'd>( 113 | class: &mut CdcAcmClass<'d, Driver<'d, T>>, 114 | ) -> Result<(), Disconnected> { 115 | let mut buf = [0; MAX_PACKET_SIZE as usize]; 116 | loop { 117 | let n = class.read_packet(&mut buf).await?; 118 | let data = &buf[..n]; 119 | info!("data: {:x}", data); 120 | class.write_packet(data).await?; 121 | } 122 | } 123 | 124 | use embassy_stm32::adc; 125 | use embassy_stm32::adc::Adc; 126 | use embassy_stm32::peripherals::ADC1; 127 | 128 | async fn stream_adc<'d, T: Instance + 'd>( 129 | class: &mut CdcAcmClass<'d, Driver<'d, T>>, 130 | adc: &mut Adc<'d, ADC1>, 131 | pin: &mut impl embassy_stm32::adc::AdcChannel, 132 | ) -> Result<(), Disconnected> { 133 | let mut vrefint = adc.enable_vref(); 134 | let vrefint_sample = adc.read(&mut vrefint).await; 135 | let convert_to_millivolts = |sample| { 136 | const VREFINT_MV: u32 = 1200; 137 | (u32::from(sample) * VREFINT_MV / u32::from(vrefint_sample)) as u16 138 | }; 139 | 140 | let mut buf = [0u8; MAX_PACKET_SIZE as usize]; 141 | let samples_per_packet = (MAX_PACKET_SIZE as usize) / 2; // 2 bytes per sample 142 | 143 | loop { 144 | for i in 0..samples_per_packet { 145 | let v = adc.read(pin).await; 146 | let mv = convert_to_millivolts(v); 147 | buf[i * 2] = (mv >> 8) as u8; 148 | buf[i * 2 + 1] = mv as u8; 149 | } 150 | class.write_packet(&buf).await?; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /firmware/src/bin/local.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use calipertron_core::*; 5 | 6 | use defmt::*; 7 | use embassy_executor::Spawner; 8 | use embassy_stm32::dma::*; 9 | use embassy_stm32::gpio::{Flex, Input, Level, Output, Speed}; 10 | use embassy_stm32::time::Hertz; 11 | use embassy_stm32::{adc, Config}; 12 | 13 | //use embassy_time::Duration; 14 | use num_traits::Float; 15 | 16 | use {defmt_rtt as _, panic_probe as _}; 17 | 18 | include!(concat!(env!("OUT_DIR"), "/constants.rs")); 19 | const NUM_SAMPLES: usize = SINE_COSINE_TABLE.len(); 20 | 21 | #[embassy_executor::main] 22 | async fn main(_spawner: Spawner) { 23 | let mut config = Config::default(); 24 | { 25 | use embassy_stm32::rcc::*; 26 | config.rcc.hse = Some(Hse { 27 | freq: Hertz(8_000_000), 28 | mode: HseMode::Oscillator, 29 | }); 30 | config.rcc.pll = Some(Pll { 31 | src: PllSource::HSE, 32 | prediv: PllPreDiv::DIV1, 33 | mul: PllMul::MUL9, 34 | }); 35 | config.rcc.sys = Sysclk::PLL1_P; 36 | config.rcc.ahb_pre = AHBPrescaler::DIV1; 37 | config.rcc.apb1_pre = APBPrescaler::DIV2; 38 | config.rcc.apb2_pre = APBPrescaler::DIV1; 39 | } 40 | let p = embassy_stm32::init(config); 41 | 42 | info!("Hello World!"); 43 | 44 | //////////////////////// 45 | // Signal emission setup 46 | 47 | let _pins = [ 48 | Output::new(p.PA0, Level::Low, Speed::Low), 49 | Output::new(p.PA1, Level::Low, Speed::Low), 50 | Output::new(p.PA2, Level::Low, Speed::Low), 51 | Output::new(p.PA3, Level::Low, Speed::Low), 52 | Output::new(p.PA4, Level::Low, Speed::Low), 53 | Output::new(p.PA5, Level::Low, Speed::Low), 54 | Output::new(p.PA6, Level::Low, Speed::Low), 55 | Output::new(p.PA7, Level::Low, Speed::Low), 56 | ]; 57 | 58 | let tim = embassy_stm32::timer::low_level::Timer::new(p.TIM2); 59 | let timer_registers = tim.regs_gp16(); 60 | timer_registers 61 | .cr2() 62 | .modify(|w| w.set_ccds(embassy_stm32::pac::timer::vals::Ccds::ONUPDATE)); 63 | timer_registers.dier().modify(|w| { 64 | // Enable update DMA request 65 | w.set_ude(true); 66 | // Enable update interrupt request 67 | w.set_uie(true); 68 | }); 69 | 70 | tim.set_frequency(Hertz(PDM_FREQUENCY)); 71 | 72 | let start_pdm = || unsafe { 73 | let mut opts = TransferOptions::default(); 74 | opts.circular = true; 75 | 76 | let dma_ch = embassy_stm32::Peripheral::clone_unchecked(&p.DMA1_CH2); 77 | let request = embassy_stm32::timer::UpDma::request(&dma_ch); 78 | 79 | tim.reset(); 80 | 81 | let t = Transfer::new_write( 82 | dma_ch, 83 | request, 84 | &PDM_SIGNAL, 85 | embassy_stm32::pac::GPIOA.bsrr().as_ptr() as *mut u32, 86 | opts, 87 | ); 88 | 89 | tim.start(); 90 | t 91 | }; 92 | 93 | //////////////////////// 94 | // ADC + DMA setup 95 | 96 | let start_adc = |sample_buf| unsafe { 97 | let dma_ch = embassy_stm32::Peripheral::clone_unchecked(&p.DMA1_CH1); 98 | let request = embassy_stm32::adc::RxDma::request(&dma_ch); 99 | let opts = TransferOptions::default(); 100 | 101 | let t = Transfer::new_read( 102 | dma_ch, 103 | request, 104 | embassy_stm32::pac::ADC1.dr().as_ptr() as *mut u16, 105 | sample_buf, 106 | opts, 107 | ); 108 | 109 | // Start ADC conversions 110 | embassy_stm32::pac::ADC1.cr2().modify(|w| w.set_adon(true)); 111 | t 112 | }; 113 | 114 | // just need this to power on ADC 115 | let _adc = adc::Adc::new(p.ADC1); 116 | 117 | // Configure ADC for continuous conversion with DMA 118 | let adc = embassy_stm32::pac::ADC1; 119 | 120 | adc.cr1().modify(|w| { 121 | w.set_scan(true); 122 | w.set_eocie(true); 123 | }); 124 | 125 | adc.cr2().modify(|w| { 126 | w.set_dma(true); 127 | w.set_cont(true); 128 | }); 129 | 130 | // Configure channel and sampling time 131 | adc.sqr1().modify(|w| w.set_l(0)); // one conversion. 132 | 133 | // TODO: this may not be necessary 134 | let mut pb1 = Flex::new(p.PB1); 135 | pb1.set_as_analog(); 136 | 137 | const PIN_CHANNEL: u8 = 9; // PB1 is on channel 9 for STM32F103 138 | adc.sqr3().modify(|w| w.set_sq(0, PIN_CHANNEL)); 139 | adc.smpr2() 140 | .modify(|w| w.set_smp(PIN_CHANNEL as usize, adc::SampleTime::CYCLES41_5)); 141 | 142 | let user_button = Input::new(p.PB14, embassy_stm32::gpio::Pull::None); 143 | 144 | let mut phase_accumulator = PhaseAccumulator::new(0.0, 0.1); 145 | 146 | // 9.4mm spacing across all 8 emission pads on the v1.1 PCB Mitko sent me. 147 | let distance_per_phase_cycle = 9.4; 148 | 149 | let fut_main = async { 150 | loop { 151 | // TODO: I'd rather this be local, but Transfer requires the buffer have the same lifetime as the DMA channel for some reason. 152 | static mut ADC_BUF: [u16; NUM_SAMPLES] = [0u16; NUM_SAMPLES]; 153 | 154 | let adc_buf = unsafe { &mut ADC_BUF[..] }; 155 | let adc_transfer = start_adc(adc_buf); 156 | let mut pdm_transfer = start_pdm(); 157 | // wait for all of the samples to be taken 158 | adc_transfer.await; 159 | pdm_transfer.request_stop(); 160 | 161 | let mut sum_sine: f32 = 0.0; 162 | let mut sum_cosine: f32 = 0.0; 163 | 164 | let adc_buf = unsafe { &ADC_BUF[..] }; 165 | 166 | for i in 0..NUM_SAMPLES { 167 | let (sine, cosine) = SINE_COSINE_TABLE[i]; 168 | sum_sine += adc_buf[i] as f32 * sine; 169 | sum_cosine += adc_buf[i] as f32 * cosine; 170 | } 171 | let phase = sum_sine.atan2(sum_cosine); 172 | 173 | phase_accumulator.update(phase); 174 | info!( 175 | //"Phase: {:06.2} Position: {:06.2}", 176 | "Position: {}mm, Phase: {} ", 177 | phase_accumulator.unwrapped_phase 178 | * (distance_per_phase_cycle / (2.0 * core::f32::consts::PI)), 179 | phase, 180 | ); 181 | 182 | // make sure everything is reset before we continue 183 | pdm_transfer.await; 184 | 185 | /////////////////////// 186 | // handle button press 187 | 188 | if user_button.is_low() { 189 | info!("Button pressed, zeroing"); 190 | phase_accumulator.unwrapped_phase = 0.; 191 | } 192 | } 193 | }; 194 | 195 | fut_main.await 196 | } 197 | -------------------------------------------------------------------------------- /frontend/src/bin/scope.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use egui_plot::{Line, Plot, PlotPoints}; 3 | use flume::{Receiver, Sender}; 4 | use nusb::transfer::{Queue, RequestBuffer}; 5 | use schema::Command; 6 | use std::collections::VecDeque; 7 | use std::sync::{Arc, Mutex}; 8 | use std::thread; 9 | 10 | const MAX_SAMPLES: usize = 1000; 11 | const MAX_PACKET_SIZE: usize = 64; 12 | 13 | fn main() -> Result<(), eframe::Error> { 14 | let di = nusb::list_devices() 15 | .unwrap() 16 | .find(|d| d.vendor_id() == 0xc0de && d.product_id() == 0xcafe) 17 | .expect("device should be connected"); 18 | 19 | eprintln!("Device info: {di:?}"); 20 | 21 | let device = di.open().unwrap(); 22 | let interface = device.claim_interface(0).unwrap(); 23 | 24 | let endpoint_addr = 1; 25 | let in_queue = interface.bulk_in_queue(0x80 + endpoint_addr); 26 | let out_queue = interface.bulk_out_queue(endpoint_addr); 27 | 28 | let samples = Arc::new(Mutex::new(VecDeque::with_capacity(MAX_SAMPLES))); 29 | let samples_clone = Arc::clone(&samples); 30 | let threshold = Arc::new(Mutex::new(None)); 31 | let threshold_clone = Arc::clone(&threshold); 32 | 33 | // Create a flume channel for sending messages to the USB thread 34 | let (tx, rx) = flume::unbounded(); 35 | 36 | // Start USB reading thread 37 | thread::spawn(move || { 38 | usb_reading_thread(in_queue, out_queue, samples_clone, threshold_clone, rx); 39 | }); 40 | 41 | let options = eframe::NativeOptions::default(); 42 | let app = ADCApp { 43 | samples, 44 | threshold, 45 | tx, 46 | frequency_kHz: 1., 47 | }; 48 | eframe::run_native( 49 | "ADC Visualization", 50 | options, 51 | Box::new(|_cc| Ok(Box::new(app))), 52 | ) 53 | } 54 | 55 | fn usb_reading_thread( 56 | mut in_queue: Queue, 57 | mut out_queue: Queue>, 58 | samples: Arc>>, 59 | threshold: Arc>>, 60 | rx: Receiver, // Add this parameter 61 | ) { 62 | let mut triggered = false; 63 | let mut prev_value = 0; 64 | 65 | loop { 66 | // Send any pending commands 67 | if let Ok(command) = rx.try_recv() { 68 | let mut buf = [0u8; MAX_PACKET_SIZE]; 69 | if let Ok(serialized) = command.serialize(&mut buf) { 70 | out_queue.submit(serialized.into()); 71 | } 72 | } 73 | 74 | while in_queue.pending() < 1 { 75 | in_queue.submit(nusb::transfer::RequestBuffer::new(MAX_PACKET_SIZE)); 76 | } 77 | 78 | let completion = futures_lite::future::block_on(in_queue.next_complete()); 79 | let data = completion.data.as_slice(); 80 | 81 | let threshold = *threshold.lock().unwrap(); 82 | let mut samples = samples.lock().unwrap(); 83 | for chunk in data.chunks_exact(2) { 84 | if let [low, high] = chunk { 85 | let adc_value = u16::from_le_bytes([*low, *high]); 86 | 87 | match threshold { 88 | Some(threshold) => { 89 | if triggered { 90 | samples.push_back(adc_value); 91 | if samples.len() >= MAX_SAMPLES { 92 | triggered = false; 93 | } 94 | } else { 95 | if prev_value <= threshold && adc_value > threshold { 96 | triggered = true; 97 | samples.clear(); 98 | } 99 | prev_value = adc_value; 100 | } 101 | } 102 | None => { 103 | samples.push_back(adc_value); 104 | if samples.len() >= MAX_SAMPLES { 105 | samples.pop_front(); 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | in_queue.submit(nusb::transfer::RequestBuffer::reuse( 113 | completion.data, 114 | MAX_PACKET_SIZE, 115 | )); 116 | } 117 | } 118 | 119 | #[allow(non_snake_case)] 120 | struct ADCApp { 121 | frequency_kHz: f64, 122 | samples: Arc>>, 123 | threshold: Arc>>, 124 | tx: Sender, 125 | } 126 | 127 | impl eframe::App for ADCApp { 128 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 129 | egui::SidePanel::left("controls").show(ctx, |ui| { 130 | ui.heading("Controls"); 131 | 132 | let mut threshold = self.threshold.lock().unwrap().unwrap_or(0); 133 | ui.add(egui::Slider::new(&mut threshold, 0..=4000).text("Trigger")); 134 | *self.threshold.lock().unwrap() = if threshold == 0 { 135 | None 136 | } else { 137 | Some(threshold) 138 | }; 139 | 140 | if ui 141 | .add( 142 | egui::Slider::new(&mut self.frequency_kHz, 1.0..=100.0).text("Frequency (kHz)"), 143 | ) 144 | .changed() 145 | { 146 | self.tx 147 | .send(Command::SetFrequency { 148 | frequency_kHz: self.frequency_kHz, 149 | }) 150 | .unwrap() 151 | } 152 | }); 153 | 154 | egui::CentralPanel::default().show(ctx, |ui| { 155 | ui.heading("ADC Values"); 156 | 157 | let samples = self.samples.lock().unwrap(); 158 | let threshold = *self.threshold.lock().unwrap(); // Create a copy and immediately drop the lock 159 | let plot = Plot::new("ADC Plot") 160 | .include_y(0.0) 161 | .include_y(10000.0) 162 | .include_x(MAX_SAMPLES as f64); 163 | plot.show(ui, |plot_ui| { 164 | let points: PlotPoints = samples 165 | .iter() 166 | .enumerate() 167 | .map(|(i, &v)| [i as f64, v as f64]) 168 | .collect(); 169 | let line = Line::new(points); 170 | plot_ui.line(line); 171 | 172 | // Add horizontal line for non-zero threshold 173 | if let Some(threshold) = threshold { 174 | let threshold_line = Line::new(vec![ 175 | [0.0, threshold as f64], 176 | [MAX_SAMPLES as f64, threshold as f64], 177 | ]) 178 | .color(egui::Color32::BLUE) 179 | .name("Trigger Threshold"); 180 | plot_ui.line(threshold_line); 181 | } 182 | }); 183 | }); 184 | 185 | ctx.request_repaint(); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /schema/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "atomic-polyfill" 7 | version = "1.0.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" 10 | dependencies = [ 11 | "critical-section", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.3.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "1.3.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 25 | 26 | [[package]] 27 | name = "byteorder" 28 | version = "1.5.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 31 | 32 | [[package]] 33 | name = "cobs" 34 | version = "0.2.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" 37 | 38 | [[package]] 39 | name = "critical-section" 40 | version = "1.1.2" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" 43 | 44 | [[package]] 45 | name = "defmt" 46 | version = "0.3.8" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "a99dd22262668b887121d4672af5a64b238f026099f1a2a1b322066c9ecfe9e0" 49 | dependencies = [ 50 | "bitflags", 51 | "defmt-macros", 52 | ] 53 | 54 | [[package]] 55 | name = "defmt-macros" 56 | version = "0.3.9" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "e3a9f309eff1f79b3ebdf252954d90ae440599c26c2c553fe87a2d17195f2dcb" 59 | dependencies = [ 60 | "defmt-parser", 61 | "proc-macro-error", 62 | "proc-macro2", 63 | "quote", 64 | "syn 2.0.72", 65 | ] 66 | 67 | [[package]] 68 | name = "defmt-parser" 69 | version = "0.3.4" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "ff4a5fefe330e8d7f31b16a318f9ce81000d8e35e69b93eae154d16d2278f70f" 72 | dependencies = [ 73 | "thiserror", 74 | ] 75 | 76 | [[package]] 77 | name = "hash32" 78 | version = "0.2.1" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" 81 | dependencies = [ 82 | "byteorder", 83 | ] 84 | 85 | [[package]] 86 | name = "heapless" 87 | version = "0.7.17" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" 90 | dependencies = [ 91 | "atomic-polyfill", 92 | "hash32", 93 | "rustc_version", 94 | "serde", 95 | "spin", 96 | "stable_deref_trait", 97 | ] 98 | 99 | [[package]] 100 | name = "lock_api" 101 | version = "0.4.12" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 104 | dependencies = [ 105 | "autocfg", 106 | "scopeguard", 107 | ] 108 | 109 | [[package]] 110 | name = "postcard" 111 | version = "1.0.8" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" 114 | dependencies = [ 115 | "cobs", 116 | "heapless", 117 | "serde", 118 | ] 119 | 120 | [[package]] 121 | name = "proc-macro-error" 122 | version = "1.0.4" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 125 | dependencies = [ 126 | "proc-macro-error-attr", 127 | "proc-macro2", 128 | "quote", 129 | "syn 1.0.109", 130 | "version_check", 131 | ] 132 | 133 | [[package]] 134 | name = "proc-macro-error-attr" 135 | version = "1.0.4" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 138 | dependencies = [ 139 | "proc-macro2", 140 | "quote", 141 | "version_check", 142 | ] 143 | 144 | [[package]] 145 | name = "proc-macro2" 146 | version = "1.0.86" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 149 | dependencies = [ 150 | "unicode-ident", 151 | ] 152 | 153 | [[package]] 154 | name = "quote" 155 | version = "1.0.36" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 158 | dependencies = [ 159 | "proc-macro2", 160 | ] 161 | 162 | [[package]] 163 | name = "rustc_version" 164 | version = "0.4.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 167 | dependencies = [ 168 | "semver", 169 | ] 170 | 171 | [[package]] 172 | name = "schema" 173 | version = "0.1.0" 174 | dependencies = [ 175 | "defmt", 176 | "postcard", 177 | "serde", 178 | ] 179 | 180 | [[package]] 181 | name = "scopeguard" 182 | version = "1.2.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 185 | 186 | [[package]] 187 | name = "semver" 188 | version = "1.0.23" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 191 | 192 | [[package]] 193 | name = "serde" 194 | version = "1.0.204" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" 197 | dependencies = [ 198 | "serde_derive", 199 | ] 200 | 201 | [[package]] 202 | name = "serde_derive" 203 | version = "1.0.204" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" 206 | dependencies = [ 207 | "proc-macro2", 208 | "quote", 209 | "syn 2.0.72", 210 | ] 211 | 212 | [[package]] 213 | name = "spin" 214 | version = "0.9.8" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 217 | dependencies = [ 218 | "lock_api", 219 | ] 220 | 221 | [[package]] 222 | name = "stable_deref_trait" 223 | version = "1.2.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 226 | 227 | [[package]] 228 | name = "syn" 229 | version = "1.0.109" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 232 | dependencies = [ 233 | "proc-macro2", 234 | "unicode-ident", 235 | ] 236 | 237 | [[package]] 238 | name = "syn" 239 | version = "2.0.72" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" 242 | dependencies = [ 243 | "proc-macro2", 244 | "quote", 245 | "unicode-ident", 246 | ] 247 | 248 | [[package]] 249 | name = "thiserror" 250 | version = "1.0.63" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 253 | dependencies = [ 254 | "thiserror-impl", 255 | ] 256 | 257 | [[package]] 258 | name = "thiserror-impl" 259 | version = "1.0.63" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 262 | dependencies = [ 263 | "proc-macro2", 264 | "quote", 265 | "syn 2.0.72", 266 | ] 267 | 268 | [[package]] 269 | name = "unicode-ident" 270 | version = "1.0.12" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 273 | 274 | [[package]] 275 | name = "version_check" 276 | version = "0.9.5" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 279 | -------------------------------------------------------------------------------- /analysis/parameter_sweep.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "a7aa3838-a57c-4a45-bbb3-ca19f1604426", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import numpy as np\n", 11 | "import matplotlib.pyplot as plt\n", 12 | "import seaborn as sns\n", 13 | "import polars as pl" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": null, 19 | "id": "a31396d0", 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "n_pdm_samples = 128\n", 24 | "\n", 25 | "d = pl.read_csv(\"../frontend/parameter_sweep.csv\")\n", 26 | "d" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "id": "786ae8a9", 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "grouped = d.group_by(\"pdm_frequency\", \"sampling_frequency\", maintain_order=True)" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "id": "3e06502e", 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "# Measure phases via correlation. Use a sliding window so we can get many measurements of the phase and thus a sense of the \"noise\"\n", 47 | "def measure_phases(samples, sampling_frequency, signal_frequency, window_size=1024):\n", 48 | " num_windows = len(samples) - window_size + 1\n", 49 | " phases = np.zeros(num_windows)\n", 50 | " t = np.arange(len(samples)) / sampling_frequency\n", 51 | " for i in range(num_windows):\n", 52 | " window = samples[i:i+window_size]\n", 53 | " \n", 54 | " sine_wave = np.sin(2 * np.pi * signal_frequency * t[i:i+window_size])\n", 55 | " cosine_wave = np.cos(2 * np.pi * signal_frequency * t[i:i+window_size])\n", 56 | " correlation_sine = np.sum(window * sine_wave)\n", 57 | " correlation_cosine = np.sum(window * cosine_wave)\n", 58 | " phases[i] = np.arctan2(correlation_sine, correlation_cosine)\n", 59 | "\n", 60 | " return phases" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "id": "9bc7f91f", 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "\n", 71 | "(pdm_frequency, sampling_frequency), g = list(grouped)[50]\n", 72 | "plt.figure()\n", 73 | "plt.plot(g[\"sample\"])\n", 74 | "plt.xlabel(\"Sample Index\")\n", 75 | "\n", 76 | "signal_frequency = g[0, \"pdm_frequency\"] / n_pdm_samples\n", 77 | "sampling_frequency = g[0, \"sampling_frequency\"]\n", 78 | "\n", 79 | "phases = measure_phases(g[\"sample\"].to_numpy(), sampling_frequency, signal_frequency)\n", 80 | "plt.figure()\n", 81 | "plt.plot(phases)\n", 82 | "plt.xlabel(\"Sample Index\")\n", 83 | "plt.ylabel(\"Phase (radians)\")\n", 84 | "plt.title(\"Phase Values for First PDM Frequency\")\n" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": null, 90 | "id": "a29d581c", 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "def calculate_phases(df):\n", 95 | " signal_frequency = df[0, \"pdm_frequency\"] / n_pdm_samples\n", 96 | " sampling_frequency = df[0, \"sampling_frequency\"]\n", 97 | " window_sizes = [128, 256, 512, 1024, 2048]\n", 98 | " df = pl.DataFrame({\n", 99 | " \"pdm_frequency\": df[0, \"pdm_frequency\"],\n", 100 | " \"sampling_frequency\": df[0, \"sampling_frequency\"],\n", 101 | " \"window_size\": window_sizes,\n", 102 | " \"phase\": [pl.Series(measure_phases(df[\"sample\"].to_numpy(), sampling_frequency, signal_frequency, window_size)) for window_size in window_sizes]\n", 103 | " })\n", 104 | " return df\n", 105 | "\n", 106 | "with_phases = d.group_by(\"pdm_frequency\", \"sampling_frequency\", maintain_order=True).map_groups(calculate_phases)\n" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "id": "a36d3aee", 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "# Calculate std dev of phases for each group\n", 117 | "phase_stats = with_phases.select(\n", 118 | " \"pdm_frequency\",\n", 119 | " \"sampling_frequency\", \n", 120 | " \"window_size\",\n", 121 | " pl.col(\"phase\").list.std().alias(\"phase_std\")\n", 122 | ")\n", 123 | "\n", 124 | "# Create figure with subplots stacked vertically\n", 125 | "window_sizes = sorted(phase_stats[\"window_size\"].unique().to_list())\n", 126 | "fig, axes = plt.subplots(len(window_sizes), 1, figsize=(12, 4*len(window_sizes)))\n", 127 | "\n", 128 | "# Get global min and max for y-axis scaling\n", 129 | "y_min = phase_stats[\"phase_std\"].min()\n", 130 | "y_max = phase_stats[\"phase_std\"].max()\n", 131 | "\n", 132 | "# Create a plot for each window size\n", 133 | "for i, window_size in enumerate(window_sizes):\n", 134 | " window_data = phase_stats.filter(pl.col(\"window_size\") == window_size)\n", 135 | " \n", 136 | " sns.lineplot(\n", 137 | " data=window_data.with_columns(\n", 138 | " pdm_frequency=pl.col(\"pdm_frequency\") / 1000,\n", 139 | " sampling_frequency=(pl.col(\"sampling_frequency\") / 1000).round(1)\n", 140 | " ),\n", 141 | " x=\"pdm_frequency\",\n", 142 | " y=\"phase_std\",\n", 143 | " hue=\"sampling_frequency\",\n", 144 | " palette=\"viridis\",\n", 145 | " legend=\"full\" if i == 0 else False, # Only show legend on first plot\n", 146 | " ax=axes[i]\n", 147 | " )\n", 148 | " \n", 149 | " # Modify legend title and align labels for first plot\n", 150 | " if i == 0:\n", 151 | " legend = axes[i].get_legend()\n", 152 | " legend.set_title(\"Sampling Frequency (kHz)\")\n", 153 | " \n", 154 | " axes[i].set_xlabel(\"PDM Frequency (kHz)\")\n", 155 | " axes[i].set_ylabel(\"Phase Standard Deviation (radians)\")\n", 156 | " axes[i].set_title(f\"Phase Variation vs PDM Frequency (Window Size: {window_size})\")\n", 157 | " axes[i].set_ylim(y_min, y_max) # Set same y-axis limits for all subplots\n", 158 | "\n", 159 | "# Adjust spacing between subplots\n", 160 | "plt.tight_layout()" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": null, 166 | "id": "1c3139ae", 167 | "metadata": {}, 168 | "outputs": [], 169 | "source": [ 170 | "# Calculate std dev of phases for each group\n", 171 | "phase_stats = with_phases.select(\n", 172 | " \"pdm_frequency\",\n", 173 | " \"sampling_frequency\", \n", 174 | " \"window_size\",\n", 175 | " pl.col(\"phase\").list.std().alias(\"phase_std\")\n", 176 | ")\n", 177 | "\n", 178 | "# Create figure with subplots stacked vertically\n", 179 | "window_sizes = sorted(phase_stats[\"window_size\"].unique().to_list())\n", 180 | "fig, axes = plt.subplots(len(window_sizes), 1, figsize=(12, 4*len(window_sizes)))\n", 181 | "\n", 182 | "# Get global min and max for y-axis scaling\n", 183 | "y_min = phase_stats[\"phase_std\"].min()\n", 184 | "y_max = phase_stats[\"phase_std\"].max()\n", 185 | "\n", 186 | "# Create a plot for each window size\n", 187 | "for i, window_size in enumerate(window_sizes):\n", 188 | " window_data = phase_stats.filter(pl.col(\"window_size\") == window_size)\n", 189 | " \n", 190 | " # Create scatter plot with lines\n", 191 | " plot = sns.lineplot(\n", 192 | " data=window_data.with_columns(\n", 193 | " pdm_frequency=pl.col(\"pdm_frequency\") / 1000,\n", 194 | " sampling_frequency=(pl.col(\"sampling_frequency\") / 1000).round(1)\n", 195 | " ),\n", 196 | " x=\"pdm_frequency\",\n", 197 | " y=\"phase_std\",\n", 198 | " hue=\"sampling_frequency\",\n", 199 | " palette=\"viridis\",\n", 200 | " legend=\"full\" if i == 0 else False, # Only show legend on first plot\n", 201 | " ax=axes[i]\n", 202 | " )\n", 203 | " \n", 204 | " # Add hover annotations\n", 205 | " annot = axes[i].annotate(\"\", xy=(0,0), xytext=(10,10),\n", 206 | " textcoords=\"offset points\",\n", 207 | " bbox=dict(boxstyle=\"round\", fc=\"w\", ec=\"0.5\", alpha=0.9),\n", 208 | " arrowprops=dict(arrowstyle=\"->\"))\n", 209 | " annot.set_visible(False)\n", 210 | "\n", 211 | " def hover(event):\n", 212 | " if event.inaxes == axes[i]:\n", 213 | " cont, ind = plot.lines[0].contains(event)\n", 214 | " if cont:\n", 215 | " x = event.xdata\n", 216 | " y = event.ydata\n", 217 | " annot.xy = (x, y)\n", 218 | " text = f'PDM Freq: {x:.1f} kHz\\nPhase StdDev: {y:.3f} rad'\n", 219 | " annot.set_text(text)\n", 220 | " annot.set_visible(True)\n", 221 | " fig.canvas.draw_idle()\n", 222 | " else:\n", 223 | " annot.set_visible(False)\n", 224 | " fig.canvas.draw_idle()\n", 225 | " \n", 226 | " # Modify legend title and align labels for first plot\n", 227 | " if i == 0:\n", 228 | " legend = axes[i].get_legend()\n", 229 | " legend.set_title(\"Sampling Frequency (kHz)\")\n", 230 | " \n", 231 | " axes[i].set_xlabel(\"PDM Frequency (kHz)\")\n", 232 | " axes[i].set_ylabel(\"Phase Standard Deviation (radians)\")\n", 233 | " axes[i].set_title(f\"Phase Variation vs PDM Frequency (Window Size: {window_size})\")\n", 234 | " axes[i].set_ylim(y_min, y_max) # Set same y-axis limits for all subplots\n", 235 | "\n", 236 | "# Connect the hover event\n", 237 | "fig.canvas.mpl_connect(\"motion_notify_event\", hover)\n", 238 | "\n", 239 | "# Adjust spacing between subplots\n", 240 | "plt.tight_layout()" 241 | ] 242 | } 243 | ], 244 | "metadata": { 245 | "kernelspec": { 246 | "display_name": "Python 3 (ipykernel)", 247 | "language": "python", 248 | "name": "python3" 249 | }, 250 | "language_info": { 251 | "codemirror_mode": { 252 | "name": "ipython", 253 | "version": 3 254 | }, 255 | "file_extension": ".py", 256 | "mimetype": "text/x-python", 257 | "name": "python", 258 | "nbconvert_exporter": "python", 259 | "pygments_lexer": "ipython3", 260 | "version": "3.12.3" 261 | } 262 | }, 263 | "nbformat": 4, 264 | "nbformat_minor": 5 265 | } 266 | -------------------------------------------------------------------------------- /firmware/src/bin/recorder.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | use schema::*; 4 | 5 | use defmt::*; 6 | use embassy_executor::Spawner; 7 | use embassy_stm32::adc::Adc; 8 | use embassy_stm32::dma::*; 9 | use embassy_stm32::gpio::{Flex, Level, Output, Speed}; 10 | use embassy_stm32::time::Hertz; 11 | use embassy_stm32::{adc, bind_interrupts, peripherals, usb, Config}; 12 | use embassy_time::Timer; 13 | use embassy_usb::driver::{Endpoint, EndpointIn, EndpointOut}; 14 | use embassy_usb::Builder; 15 | use {defmt_rtt as _, panic_probe as _}; 16 | 17 | include!(concat!(env!("OUT_DIR"), "/constants.rs")); 18 | 19 | bind_interrupts!(struct Irqs { 20 | USB_LP_CAN1_RX0 => usb::InterruptHandler; 21 | }); 22 | 23 | const MAX_PACKET_SIZE: u8 = 64; 24 | const SAMPLES_PER_PACKET: usize = (MAX_PACKET_SIZE as usize) / 2; // 2 bytes per sample 25 | const NUM_SAMPLES: usize = SAMPLES_PER_PACKET * 128; 26 | 27 | pub const USB_CLASS_CUSTOM: u8 = 0xFF; 28 | const USB_SUBCLASS_CUSTOM: u8 = 0x00; 29 | const USB_PROTOCOL_CUSTOM: u8 = 0x00; 30 | 31 | #[embassy_executor::main] 32 | async fn main(_spawner: Spawner) { 33 | let mut config = Config::default(); 34 | { 35 | use embassy_stm32::rcc::*; 36 | config.rcc.hse = Some(Hse { 37 | freq: Hertz(8_000_000), 38 | mode: HseMode::Oscillator, 39 | }); 40 | config.rcc.pll = Some(Pll { 41 | src: PllSource::HSE, 42 | prediv: PllPreDiv::DIV1, 43 | mul: PllMul::MUL9, 44 | }); 45 | config.rcc.sys = Sysclk::PLL1_P; 46 | config.rcc.ahb_pre = AHBPrescaler::DIV1; 47 | config.rcc.apb1_pre = APBPrescaler::DIV2; 48 | config.rcc.apb2_pre = APBPrescaler::DIV1; 49 | } 50 | let mut p = embassy_stm32::init(config); 51 | 52 | info!("Hello World!"); 53 | 54 | { 55 | // Board has a pull-up resistor on the D+ line; pull it down to send a RESET condition to the USB bus. 56 | // This forced reset is needed only for development, without it host will not reset your device when you upload new firmware. 57 | let _dp = Output::new(&mut p.PA12, Level::Low, Speed::Low); 58 | Timer::after_millis(10).await; 59 | } 60 | 61 | //////////////////////// 62 | // Signal emission setup 63 | 64 | let _pins = [ 65 | Output::new(p.PA0, Level::Low, Speed::Low), 66 | Output::new(p.PA1, Level::Low, Speed::Low), 67 | Output::new(p.PA2, Level::Low, Speed::Low), 68 | Output::new(p.PA3, Level::Low, Speed::Low), 69 | Output::new(p.PA4, Level::Low, Speed::Low), 70 | Output::new(p.PA5, Level::Low, Speed::Low), 71 | Output::new(p.PA6, Level::Low, Speed::Low), 72 | Output::new(p.PA7, Level::Low, Speed::Low), 73 | ]; 74 | 75 | let tim = embassy_stm32::timer::low_level::Timer::new(p.TIM2); 76 | let timer_registers = tim.regs_gp16(); 77 | timer_registers 78 | .cr2() 79 | .modify(|w| w.set_ccds(embassy_stm32::pac::timer::vals::Ccds::ONUPDATE)); 80 | timer_registers.dier().modify(|w| { 81 | // Enable update DMA request 82 | w.set_ude(true); 83 | // Enable update interrupt request 84 | w.set_uie(true); 85 | }); 86 | 87 | tim.set_frequency(Hertz(100_000)); 88 | 89 | let start_pdm = || unsafe { 90 | let mut opts = TransferOptions::default(); 91 | opts.circular = true; 92 | 93 | let dma_ch = embassy_stm32::Peripheral::clone_unchecked(&p.DMA1_CH2); 94 | let request = embassy_stm32::timer::UpDma::request(&dma_ch); 95 | 96 | tim.reset(); 97 | 98 | let t = Transfer::new_write( 99 | dma_ch, 100 | request, 101 | &PDM_SIGNAL, 102 | embassy_stm32::pac::GPIOA.bsrr().as_ptr() as *mut u32, 103 | opts, 104 | ); 105 | 106 | tim.start(); 107 | t 108 | }; 109 | 110 | //////////////////////// 111 | // USB Setup 112 | 113 | let driver = embassy_stm32::usb::Driver::new(p.USB, Irqs, p.PA12, p.PA11); 114 | let (vid, pid) = (0xc0de, 0xcafe); 115 | let mut config = embassy_usb::Config::new(vid, pid); 116 | config.max_packet_size_0 = MAX_PACKET_SIZE; 117 | config.product = Some("Calipertron"); 118 | 119 | let mut config_descriptor = [0; 256]; 120 | let mut bos_descriptor = [0; 256]; 121 | let mut control_buf = [0; 64]; 122 | 123 | let mut builder = Builder::new( 124 | driver, 125 | config, 126 | &mut config_descriptor, 127 | &mut bos_descriptor, 128 | &mut [], // no msos descriptors 129 | &mut control_buf, 130 | ); 131 | 132 | let mut func = builder.function(USB_CLASS_CUSTOM, USB_SUBCLASS_CUSTOM, USB_PROTOCOL_CUSTOM); 133 | let mut iface = func.interface(); 134 | 135 | let mut iface_alt = iface.alt_setting( 136 | USB_CLASS_CUSTOM, 137 | USB_SUBCLASS_CUSTOM, 138 | USB_PROTOCOL_CUSTOM, 139 | None, 140 | ); 141 | let mut read_ep = iface_alt.endpoint_bulk_out(MAX_PACKET_SIZE as u16); 142 | let mut write_ep = iface_alt.endpoint_bulk_in(MAX_PACKET_SIZE as u16); 143 | drop(func); 144 | 145 | let mut usb = builder.build(); 146 | 147 | let fut_usb = usb.run(); 148 | 149 | //////////////////////// 150 | // ADC + DMA setup 151 | 152 | let start_adc = |sample_buf| unsafe { 153 | let dma_ch = embassy_stm32::Peripheral::clone_unchecked(&p.DMA1_CH1); 154 | let request = embassy_stm32::adc::RxDma::request(&dma_ch); 155 | let opts = TransferOptions::default(); 156 | 157 | let t = Transfer::new_read( 158 | dma_ch, 159 | request, 160 | embassy_stm32::pac::ADC1.dr().as_ptr() as *mut u16, 161 | sample_buf, 162 | opts, 163 | ); 164 | 165 | // Start ADC conversions 166 | embassy_stm32::pac::ADC1.cr2().modify(|w| w.set_adon(true)); 167 | t 168 | }; 169 | 170 | let mut adc = Adc::new(p.ADC1); 171 | 172 | let vrefint_sample = { 173 | let mut vrefint = adc.enable_vref(); 174 | 175 | // give vref some time to warm up 176 | Timer::after_millis(100).await; 177 | 178 | adc.read(&mut vrefint).await as u32 179 | }; 180 | info!("VREFINT: {}", vrefint_sample); 181 | 182 | //let convert_to_millivolts = |sample| (sample as u32 * adc::VREF_INT / vrefint_sample) as u16; 183 | 184 | // Configure ADC for continuous conversion with DMA 185 | let adc = embassy_stm32::pac::ADC1; 186 | 187 | adc.cr1().modify(|w| { 188 | w.set_scan(true); 189 | w.set_eocie(true); 190 | }); 191 | 192 | adc.cr2().modify(|w| { 193 | w.set_dma(true); 194 | w.set_cont(true); 195 | }); 196 | 197 | // Configure channel and sampling time 198 | adc.sqr1().modify(|w| w.set_l(0)); // one conversion. 199 | 200 | // TODO: this may not be necessary 201 | let mut pb1 = Flex::new(p.PB1); 202 | pb1.set_as_analog(); 203 | 204 | const PIN_CHANNEL: u8 = 9; // PB1 is on channel 9 for STM32F103 205 | adc.sqr3().modify(|w| w.set_sq(0, PIN_CHANNEL)); 206 | adc.smpr2() 207 | .modify(|w| w.set_smp(PIN_CHANNEL as usize, adc::SampleTime::CYCLES239_5)); 208 | 209 | ////////////////////////// 210 | // handle commands from host 211 | let fut_commands = async { 212 | // Wait for USB to connect 213 | read_ep.wait_enabled().await; 214 | 215 | // Wait for USB to connect 216 | write_ep.wait_enabled().await; 217 | 218 | info!("Ready"); 219 | 220 | loop { 221 | let mut command_buf = [0u8; MAX_PACKET_SIZE as usize]; 222 | 223 | match read_ep.read(&mut command_buf).await { 224 | Ok(size) => { 225 | if let Some(command) = Command::deserialize(&command_buf[..size]) { 226 | info!("Received command: {:?}", command); 227 | use Command::*; 228 | match command { 229 | SetFrequency { 230 | frequency_kHz, 231 | adc_sampling_period, 232 | } => { 233 | tim.set_frequency(Hertz((frequency_kHz * 1000.) as u32)); 234 | 235 | adc.smpr2().modify(|w| { 236 | w.set_smp( 237 | PIN_CHANNEL as usize, 238 | match adc_sampling_period { 239 | AdcSamplingPeriod::CYCLES1_5 => { 240 | adc::SampleTime::CYCLES1_5 241 | } 242 | AdcSamplingPeriod::CYCLES7_5 => { 243 | adc::SampleTime::CYCLES7_5 244 | } 245 | AdcSamplingPeriod::CYCLES13_5 => { 246 | adc::SampleTime::CYCLES13_5 247 | } 248 | AdcSamplingPeriod::CYCLES28_5 => { 249 | adc::SampleTime::CYCLES28_5 250 | } 251 | AdcSamplingPeriod::CYCLES41_5 => { 252 | adc::SampleTime::CYCLES41_5 253 | } 254 | AdcSamplingPeriod::CYCLES55_5 => { 255 | adc::SampleTime::CYCLES55_5 256 | } 257 | AdcSamplingPeriod::CYCLES71_5 => { 258 | adc::SampleTime::CYCLES71_5 259 | } 260 | AdcSamplingPeriod::CYCLES239_5 => { 261 | adc::SampleTime::CYCLES239_5 262 | } 263 | }, 264 | ) 265 | }) 266 | } 267 | 268 | // would be nice to extract this, but async closures aren't stable yet and no way in hell I'm going to write out the types. 269 | Record => { 270 | // TODO: I'd rather this be local, but Transfer requires the buffer have the same lifetime as the DMA channel for some reason. 271 | static mut ADC_BUF: [u16; NUM_SAMPLES] = [0u16; NUM_SAMPLES]; 272 | 273 | let buf = unsafe { &mut ADC_BUF[..] }; 274 | 275 | // start ADC 276 | let adc_transfer = start_adc(buf); 277 | 278 | // start PDM 279 | let mut pdm_transfer = start_pdm(); 280 | 281 | // wait for all of the samples to be taken 282 | adc_transfer.await; 283 | // TODO: why am I getting errors about multiple mutable borrows --- shouldn't awaiting the adc_transfer above end the borrow? 284 | let buf = unsafe { &mut ADC_BUF[..] }; 285 | 286 | pdm_transfer.request_stop(); 287 | 288 | // now we can send the collected results back to the host 289 | 290 | // for x in buf.iter_mut() { 291 | // *x = convert_to_millivolts(*x); 292 | // } 293 | for c in buf.chunks(SAMPLES_PER_PACKET) { 294 | let r = write_ep.write(bytemuck::cast_slice(c)).await; 295 | if r.is_err() { 296 | error!("USB Error: {:?}", r); 297 | break; 298 | } 299 | } 300 | 301 | // make sure everything is reset before we continue 302 | pdm_transfer.await; 303 | } 304 | } 305 | } else { 306 | error!("Failed to deserialize command"); 307 | } 308 | } 309 | Err(e) => error!("Failed to read USB packet: {:?}", e), 310 | } 311 | } 312 | }; 313 | 314 | // Pinning and using join_array saves 1kB of flash compared to join3. (Presumably reduced code size.) 315 | // embassy_futures::join::join3(fut_commands, fut_usb, fut_stream_adc).await; 316 | 317 | let fut_commands = core::pin::pin!(fut_commands); 318 | let fut_usb = core::pin::pin!(fut_usb); 319 | 320 | let futures: [core::pin::Pin<&mut dyn core::future::Future>; 2] = 321 | [fut_usb, fut_commands]; 322 | embassy_futures::join::join_array(futures).await; 323 | } 324 | -------------------------------------------------------------------------------- /firmware/src/bin/usb_custom.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | use schema::*; 4 | 5 | use defmt::*; 6 | use embassy_executor::Spawner; 7 | use embassy_stm32::adc::Adc; 8 | use embassy_stm32::gpio::{Flex, Level, Output, Speed}; 9 | use embassy_stm32::time::Hertz; 10 | use embassy_stm32::{adc, bind_interrupts, interrupt, peripherals, usb, Config}; 11 | use embassy_time::Timer; 12 | use embassy_usb::driver::{Endpoint, EndpointIn, EndpointOut}; 13 | use embassy_usb::Builder; 14 | 15 | use {defmt_rtt as _, panic_probe as _}; 16 | 17 | bind_interrupts!(struct Irqs { 18 | USB_LP_CAN1_RX0 => usb::InterruptHandler; 19 | }); 20 | 21 | const MAX_PACKET_SIZE: u8 = 64; 22 | const SAMPLES_PER_PACKET: usize = (MAX_PACKET_SIZE as usize) / 2; // 2 bytes per sample 23 | pub const USB_CLASS_CUSTOM: u8 = 0xFF; 24 | const USB_SUBCLASS_CUSTOM: u8 = 0x00; 25 | const USB_PROTOCOL_CUSTOM: u8 = 0x00; 26 | 27 | #[embassy_executor::main] 28 | async fn main(_spawner: Spawner) { 29 | let mut config = Config::default(); 30 | { 31 | use embassy_stm32::rcc::*; 32 | config.rcc.hse = Some(Hse { 33 | freq: Hertz(8_000_000), 34 | mode: HseMode::Oscillator, 35 | }); 36 | config.rcc.pll = Some(Pll { 37 | src: PllSource::HSE, 38 | prediv: PllPreDiv::DIV1, 39 | mul: PllMul::MUL9, 40 | }); 41 | config.rcc.sys = Sysclk::PLL1_P; 42 | config.rcc.ahb_pre = AHBPrescaler::DIV1; 43 | config.rcc.apb1_pre = APBPrescaler::DIV2; 44 | config.rcc.apb2_pre = APBPrescaler::DIV1; 45 | } 46 | let mut p = embassy_stm32::init(config); 47 | 48 | info!("Hello World!"); 49 | 50 | { 51 | // Board has a pull-up resistor on the D+ line; pull it down to send a RESET condition to the USB bus. 52 | // This forced reset is needed only for development, without it host will not reset your device when you upload new firmware. 53 | let _dp = Output::new(&mut p.PA12, Level::Low, Speed::Low); 54 | Timer::after_millis(10).await; 55 | } 56 | 57 | //////////////////////// 58 | // Signal emission setup 59 | 60 | let _pins = [ 61 | Output::new(p.PA0, Level::Low, Speed::Low), 62 | Output::new(p.PA1, Level::Low, Speed::Low), 63 | Output::new(p.PA2, Level::Low, Speed::Low), 64 | Output::new(p.PA3, Level::Low, Speed::Low), 65 | Output::new(p.PA4, Level::Low, Speed::Low), 66 | Output::new(p.PA5, Level::Low, Speed::Low), 67 | Output::new(p.PA6, Level::Low, Speed::Low), 68 | Output::new(p.PA7, Level::Low, Speed::Low), 69 | ]; 70 | 71 | let tim = embassy_stm32::timer::low_level::Timer::new(p.TIM2); 72 | let timer_registers = tim.regs_gp16(); 73 | timer_registers 74 | .cr2() 75 | .modify(|w| w.set_ccds(embassy_stm32::pac::timer::vals::Ccds::ONUPDATE)); 76 | timer_registers.dier().modify(|w| { 77 | // Enable update DMA request 78 | w.set_ude(true); 79 | // Enable update interrupt request 80 | w.set_uie(true); 81 | }); 82 | 83 | tim.set_frequency(Hertz(100_000)); 84 | //tim.start(); 85 | 86 | let _debug_pin = Output::new(p.PB7, Level::Low, Speed::Low); // use SDA as debug pin for scope 87 | unsafe { cortex_m::peripheral::NVIC::unmask(embassy_stm32::pac::Interrupt::TIM2) }; 88 | 89 | static mut DRIVE_N: usize = 0; 90 | #[interrupt] 91 | unsafe fn TIM2() { 92 | embassy_stm32::pac::TIM2.sr().modify(|w| w.set_uif(false)); 93 | DRIVE_N += 1; 94 | if 0 == DRIVE_N % SIGNAL.len() { 95 | embassy_stm32::pac::GPIOB 96 | .bsrr() 97 | .write(|w| w.set_bs(7, true)) 98 | } else { 99 | embassy_stm32::pac::GPIOB 100 | .bsrr() 101 | .write(|w| w.set_br(7, true)) 102 | } 103 | } 104 | 105 | use embassy_stm32::dma::*; 106 | let gpioa = embassy_stm32::pac::GPIOA; 107 | 108 | let mut opts = TransferOptions::default(); 109 | opts.circular = true; 110 | 111 | let request = embassy_stm32::timer::UpDma::request(&p.DMA1_CH2); 112 | let _transfer = unsafe { 113 | Transfer::new_write( 114 | p.DMA1_CH2, 115 | request, 116 | &SIGNAL, 117 | gpioa.bsrr().as_ptr() as *mut u32, 118 | opts, 119 | ) 120 | }; 121 | 122 | //////////////////////// 123 | // USB Setup 124 | 125 | let driver = embassy_stm32::usb::Driver::new(p.USB, Irqs, p.PA12, p.PA11); 126 | let (vid, pid) = (0xc0de, 0xcafe); 127 | let mut config = embassy_usb::Config::new(vid, pid); 128 | config.max_packet_size_0 = MAX_PACKET_SIZE; 129 | config.product = Some("Calipertron"); 130 | 131 | let mut config_descriptor = [0; 256]; 132 | let mut bos_descriptor = [0; 256]; 133 | let mut control_buf = [0; 64]; 134 | 135 | let mut builder = Builder::new( 136 | driver, 137 | config, 138 | &mut config_descriptor, 139 | &mut bos_descriptor, 140 | &mut [], // no msos descriptors 141 | &mut control_buf, 142 | ); 143 | 144 | let mut func = builder.function(USB_CLASS_CUSTOM, USB_SUBCLASS_CUSTOM, USB_PROTOCOL_CUSTOM); 145 | let mut iface = func.interface(); 146 | 147 | let mut iface_alt = iface.alt_setting( 148 | USB_CLASS_CUSTOM, 149 | USB_SUBCLASS_CUSTOM, 150 | USB_PROTOCOL_CUSTOM, 151 | None, 152 | ); 153 | let mut read_ep = iface_alt.endpoint_bulk_out(MAX_PACKET_SIZE as u16); 154 | let mut write_ep = iface_alt.endpoint_bulk_in(MAX_PACKET_SIZE as u16); 155 | drop(func); 156 | 157 | let mut usb = builder.build(); 158 | 159 | let fut_usb = usb.run(); 160 | 161 | //////////////////////// 162 | // ADC + DMA setup 163 | 164 | let mut adc_buffer = [0; 2 * SAMPLES_PER_PACKET]; 165 | let request = embassy_stm32::adc::RxDma::request(&p.DMA1_CH1); 166 | let mut opts = TransferOptions::default(); 167 | opts.half_transfer_ir = true; 168 | let mut adc_rb = unsafe { 169 | ReadableRingBuffer::new( 170 | p.DMA1_CH1, 171 | request, 172 | embassy_stm32::pac::ADC1.dr().as_ptr() as *mut u16, 173 | &mut adc_buffer, 174 | opts, 175 | ) 176 | }; 177 | 178 | let mut adc = Adc::new(p.ADC1); 179 | 180 | let vrefint_sample = { 181 | let mut vrefint = adc.enable_vref(); 182 | 183 | // give vref some time to warm up 184 | Timer::after_millis(100).await; 185 | 186 | adc.read(&mut vrefint).await as u32 187 | }; 188 | info!("VREFINT: {}", vrefint_sample); 189 | 190 | let convert_to_millivolts = |sample| (sample as u32 * adc::VREF_INT / vrefint_sample) as u16; 191 | 192 | // Configure ADC for continuous conversion with DMA 193 | let adc = embassy_stm32::pac::ADC1; 194 | 195 | adc.cr1().modify(|w| { 196 | w.set_scan(true); 197 | w.set_eocie(true); 198 | }); 199 | 200 | adc.cr2().modify(|w| { 201 | w.set_dma(true); 202 | w.set_cont(true); 203 | }); 204 | 205 | // Configure channel and sampling time 206 | adc.sqr1().modify(|w| w.set_l(0)); // one conversion. 207 | 208 | // TODO: this may not be necessary 209 | let mut pb1 = Flex::new(p.PB1); 210 | pb1.set_as_analog(); 211 | 212 | const PIN_CHANNEL: u8 = 9; // PB1 is on channel 9 for STM32F103 213 | adc.sqr3().modify(|w| w.set_sq(0, PIN_CHANNEL)); 214 | adc.smpr2().modify(|w| { 215 | w.set_smp( 216 | PIN_CHANNEL as usize, 217 | adc::SampleTime::CYCLES239_5, 218 | //adc::SampleTime::CYCLES71_5, 219 | ) 220 | }); 221 | 222 | // Start ADC conversions 223 | adc.cr2().modify(|w| w.set_adon(true)); 224 | 225 | //////////////////////// 226 | // Stream ADC data to host 227 | 228 | let fut_stream_adc = async { 229 | // Wait for USB to connect 230 | write_ep.wait_enabled().await; 231 | 232 | // Start handling DMA requests from ADC 233 | adc_rb.start(); 234 | 235 | let mut buf = [0; SAMPLES_PER_PACKET]; 236 | loop { 237 | loop { 238 | let r = adc_rb.read_exact(&mut buf).await; 239 | 240 | if r.is_err() { 241 | error!("ADC_RB error: {:?}", r); 242 | break; 243 | } 244 | 245 | for x in buf.iter_mut() { 246 | *x = convert_to_millivolts(*x); 247 | } 248 | 249 | let r = write_ep.write(bytemuck::cast_slice(&buf)).await; 250 | if r.is_err() { 251 | error!("USB Error: {:?}", r); 252 | break; 253 | } 254 | } 255 | 256 | adc_rb.clear(); 257 | } 258 | }; 259 | 260 | ////////////////////////// 261 | // handle commands from host 262 | let fut_commands = async { 263 | // Wait for USB to connect 264 | read_ep.wait_enabled().await; 265 | 266 | loop { 267 | let mut command_buf = [0u8; MAX_PACKET_SIZE as usize]; 268 | 269 | match read_ep.read(&mut command_buf).await { 270 | Ok(size) => { 271 | if let Some(command) = Command::deserialize(&command_buf[..size]) { 272 | info!("Received command: {:?}", command); 273 | match command { 274 | Command::SetFrequency { frequency_kHz } => { 275 | tim.stop(); 276 | tim.reset(); 277 | 278 | tim.set_frequency(Hertz((frequency_kHz * 1000.) as u32)); 279 | tim.start(); 280 | } 281 | x => { 282 | defmt::todo!("Can't handle: {}", x) 283 | } 284 | } 285 | } else { 286 | error!("Failed to deserialize command"); 287 | } 288 | } 289 | Err(e) => { 290 | error!("Failed to read USB packet: {:?}", e); 291 | } 292 | }; 293 | } 294 | }; 295 | 296 | // Pinning and using join_array saves 1kB of flash compared to join3. (Presumably reduced code size.) 297 | // embassy_futures::join::join3(fut_commands, fut_usb, fut_stream_adc).await; 298 | 299 | let fut_commands = core::pin::pin!(fut_commands); 300 | let fut_usb = core::pin::pin!(fut_usb); 301 | let fut_stream_adc = core::pin::pin!(fut_stream_adc); 302 | 303 | let futures: [core::pin::Pin<&mut dyn core::future::Future>; 3] = 304 | [fut_commands, fut_usb, fut_stream_adc]; 305 | embassy_futures::join::join_array(futures).await; 306 | } 307 | 308 | static SIGNAL: [u32; 132] = [ 309 | 0b00000000010101010000000010101010, 310 | 0b00000000010101010000000010101010, 311 | 0b00000000011010100000000010010101, 312 | 0b00000000011010100000000010010101, 313 | 0b00000000010101010000000010101010, 314 | 0b00000000100101010000000001101010, 315 | 0b00000000011010100000000010010101, 316 | 0b00000000011010100000000010010101, 317 | 0b00000000010101010000000010101010, 318 | 0b00000000100101010000000001101010, 319 | 0b00000000011010100000000010010101, 320 | 0b00000000011010100000000010010101, 321 | 0b00000000100101010000000001101010, 322 | 0b00000000100101010000000001101010, 323 | 0b00000000010110100000000010100101, 324 | 0b00000000011010100000000010010101, 325 | 0b00000000100101010000000001101010, 326 | 0b00000000100101010000000001101010, 327 | 0b00000000010110100000000010100101, 328 | 0b00000000010110100000000010100101, 329 | 0b00000000100101010000000001101010, 330 | 0b00000000101001010000000001011010, 331 | 0b00000000010110100000000010100101, 332 | 0b00000000010110100000000010100101, 333 | 0b00000000100101010000000001101010, 334 | 0b00000000101001010000000001011010, 335 | 0b00000000010110100000000010100101, 336 | 0b00000000010110100000000010100101, 337 | 0b00000000101001010000000001011010, 338 | 0b00000000101001010000000001011010, 339 | 0b00000000010101100000000010101001, 340 | 0b00000000010110100000000010100101, 341 | 0b00000000101001010000000001011010, 342 | 0b00000000101001010000000001011010, 343 | 0b00000000010101100000000010101001, 344 | 0b00000000010101100000000010101001, 345 | 0b00000000101001010000000001011010, 346 | 0b00000000101010010000000001010110, 347 | 0b00000000010101100000000010101001, 348 | 0b00000000010101100000000010101001, 349 | 0b00000000101001010000000001011010, 350 | 0b00000000101010010000000001010110, 351 | 0b00000000010101100000000010101001, 352 | 0b00000000010101100000000010101001, 353 | 0b00000000101010010000000001010110, 354 | 0b00000000101010010000000001010110, 355 | 0b00000000010101010000000010101010, 356 | 0b00000000010101100000000010101001, 357 | 0b00000000101010010000000001010110, 358 | 0b00000000101010010000000001010110, 359 | 0b00000000010101010000000010101010, 360 | 0b00000000010101010000000010101010, 361 | 0b00000000101010010000000001010110, 362 | 0b00000000101010100000000001010101, 363 | 0b00000000010101010000000010101010, 364 | 0b00000000010101010000000010101010, 365 | 0b00000000101010010000000001010110, 366 | 0b00000000101010100000000001010101, 367 | 0b00000000010101010000000010101010, 368 | 0b00000000010101010000000010101010, 369 | 0b00000000101010100000000001010101, 370 | 0b00000000101010100000000001010101, 371 | 0b00000000010101010000000010101010, 372 | 0b00000000010101010000000010101010, 373 | 0b00000000101010100000000001010101, 374 | 0b00000000101010100000000001010101, 375 | 0b00000000100101010000000001101010, 376 | 0b00000000010101010000000010101010, 377 | 0b00000000101010100000000001010101, 378 | 0b00000000101010100000000001010101, 379 | 0b00000000100101010000000001101010, 380 | 0b00000000100101010000000001101010, 381 | 0b00000000101010100000000001010101, 382 | 0b00000000011010100000000010010101, 383 | 0b00000000100101010000000001101010, 384 | 0b00000000100101010000000001101010, 385 | 0b00000000101010100000000001010101, 386 | 0b00000000011010100000000010010101, 387 | 0b00000000100101010000000001101010, 388 | 0b00000000100101010000000001101010, 389 | 0b00000000011010100000000010010101, 390 | 0b00000000011010100000000010010101, 391 | 0b00000000101001010000000001011010, 392 | 0b00000000100101010000000001101010, 393 | 0b00000000011010100000000010010101, 394 | 0b00000000011010100000000010010101, 395 | 0b00000000101001010000000001011010, 396 | 0b00000000101001010000000001011010, 397 | 0b00000000011010100000000010010101, 398 | 0b00000000010110100000000010100101, 399 | 0b00000000101001010000000001011010, 400 | 0b00000000101001010000000001011010, 401 | 0b00000000011010100000000010010101, 402 | 0b00000000010110100000000010100101, 403 | 0b00000000101001010000000001011010, 404 | 0b00000000101001010000000001011010, 405 | 0b00000000010110100000000010100101, 406 | 0b00000000010110100000000010100101, 407 | 0b00000000101010010000000001010110, 408 | 0b00000000101001010000000001011010, 409 | 0b00000000010110100000000010100101, 410 | 0b00000000010110100000000010100101, 411 | 0b00000000101010010000000001010110, 412 | 0b00000000101010010000000001010110, 413 | 0b00000000010110100000000010100101, 414 | 0b00000000010101100000000010101001, 415 | 0b00000000101010010000000001010110, 416 | 0b00000000101010010000000001010110, 417 | 0b00000000010110100000000010100101, 418 | 0b00000000010101100000000010101001, 419 | 0b00000000101010010000000001010110, 420 | 0b00000000101010010000000001010110, 421 | 0b00000000010101100000000010101001, 422 | 0b00000000010101100000000010101001, 423 | 0b00000000101010100000000001010101, 424 | 0b00000000101010010000000001010110, 425 | 0b00000000010101100000000010101001, 426 | 0b00000000010101100000000010101001, 427 | 0b00000000101010100000000001010101, 428 | 0b00000000101010100000000001010101, 429 | 0b00000000010101100000000010101001, 430 | 0b00000000010101010000000010101010, 431 | 0b00000000101010100000000001010101, 432 | 0b00000000101010100000000001010101, 433 | 0b00000000010101100000000010101001, 434 | 0b00000000010101010000000010101010, 435 | 0b00000000101010100000000001010101, 436 | 0b00000000101010100000000001010101, 437 | 0b00000000010101010000000010101010, 438 | 0b00000000010101010000000010101010, 439 | 0b00000000011010100000000010010101, 440 | 0b00000000101010100000000001010101, 441 | ]; 442 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Calipertron 2 | 3 | A lil' exploration in understanding how electronic calipers work. 4 | 5 | See [full background and results](https://kevinlynagh.com/calipertron/). 6 | 7 | 10 | 11 | All of the work in this repo was done with the [v1.1 PCB designed by Mitko](https://github.com/MitkoDyakov/Calipatron/tree/444c72c3e81eab0a2e7ee198f5574062dc1fc510/Hardware/V1.1). 12 | See also his [DIY Digital Caliper](https://hackaday.io/project/194778-diy-digital-caliper) Hackday.io project page. 13 | 14 | ## install 15 | 16 | First [install Rust](https://www.rust-lang.org/tools/install), then: 17 | 18 | rustup target add thumbv7m-none-eabi 19 | cargo install probe-rs-tools 20 | 21 | For Python analysis stuff, [install UV](https://github.com/astral-sh/uv?tab=readme-ov-file#installation). 22 | then run 23 | 24 | cd analysis/ 25 | uv sync 26 | 27 | 28 | ## firmware/ 29 | 30 | Rust firmware for v1.1 PCB based on the Embassy framework. 31 | 32 | Build and flash via STLink: 33 | 34 | cargo run --release --bin local 35 | 36 | Attach to running firmware: 37 | 38 | probe-rs attach --chip STM32F103C8 target/thumbv7m-none-eabi/release/local 39 | 40 | I did all of the development using Rust 1.81 on an M1 Macbook air running MacOS 12.7.6. 41 | 42 | 43 | ## frontend/ 44 | 45 | Parameter sweep: 46 | 47 | cargo run --release --bin parameter_sweep 48 | 49 | 50 | ## Log 51 | 52 | ### Nov 2 - Initial public release, hurray! 53 | 54 | ### Nov 1 - Do proper grid search of params. 55 | 56 | Make frontend parameter_sweep binary robust to the device freezing and needing to be restarted. 57 | 58 | 59 | ### Oct 23 - Do some design simluations to better understand PDM and sampling 60 | 61 | Added a new design_simulations notebook to try and understand whether an analog low-pass filter is necessary to smooth the reflected signal before reading by the ADC, or if all of that will wash out in software. 62 | Seems like it's the latter: Even when we're at the slowest sampling rate we're still getting phase error of just e-14 63 | Interestingly this is less than the e-12 error of the longest sampling rate --- I guess that's acting as a low pass filter itself and helping a bit. 64 | (And/or all of this washes out when correlating across 10 full cycles) 65 | 66 | 67 | ### Oct 18/19 - frequency scan; phase via cross correlation 68 | 69 | Haven't had much luck using Z3 to find optimal FFT settings, search space as I set it up is probably too bug. 70 | 71 | I'm likely going about this the wrong way, anyway --- it's probably much better to just pick the PDM signal frequency to be something that's not a multiple of 50 or 60 Hz line noise, then extract the phase by cross-correlating with the known frequency 72 | 73 | Nice overview of PDM theory https://tomverbeure.github.io/2020/10/04/PDM-Microphones-and-Sigma-Delta-Conversion.html 74 | 75 | --- 76 | 77 | How many periods of the emitted signal should we capture? 78 | Also, does it matter if the number of samples line up with the signal? 79 | I don't think it really does, since we know the signal frequency. 80 | 81 | For simplicity, lets just record a fixed number of samples for each emission frequency. 82 | 83 | 84 | ### Sept 11 85 | 86 | Doing FFT on stm32f103: 87 | 88 | 44.3ms to sample 89 | 138.0ms to sample + 2048 FFT. 90 | 141.0ms to sample + 2048 FFT + arctan phase calculation for 5 bins. 91 | 92 | Presumably takes forever because we don't have a floating point unit. 93 | For better performance we could probably do the DFT only for the target bin. 94 | 95 | Looks like we're split across bins 32 and 33. Need to tweak signal and sampling rates so all energy is centered in one bin. 96 | 97 | 30: 172994200.0 -0.6752128 98 | 31: 440644960.0 -0.7401424 99 | 32: 3207128000.0 -0.7714465 100 | 33: 6121130500.0 2.3279614 101 | 34: 512542240.0 2.2985573 102 | 103 | over 25 measurements without moving the slide, bin 33 phase is 2.33 +/- 0.045. 104 | 105 | Would be nice if it's a bit steadier. 106 | 107 | 108 | ### Sept 9 109 | 110 | Got new PCB scale from Dimitar with correct measurements. 111 | Added new firmware to do "one shot" recording of 32 cycles, rather than trying to do it continously and stream to usb (the phase offset of the initial start was inconsistent here). 112 | 113 | I verified with the python analysis notebook that the results are pretty repeatable. 114 | I.e., multiple measurements at same location give phase offset repeatable to about 0.1 radians. 115 | Moving the scale reliably changes the phase offset. 116 | 117 | Should be straightforward to update the notebook to "poll" continuously and then get a live plot to mess with. 118 | 119 | ### Aug 23/24/25 - signal analysis 120 | Recording coupled signals and trying to decode them in Python. 121 | 122 | Sampling rate is driven by ADC sample time (assuming no DMA overrun / drop out for simplicity) 123 | 124 | 1. System clock (HCLK) = 72 MHz 125 | 2. APB2 clock = 72 MHz 126 | 3. ADC clock = 72 MHz / 6 = 12 MHz (14 MHz max) 127 | 128 | ADC freq (/ (* 18 1e6) 239.5) => 75.156 kHz 129 | ADC freq (/ (* 18 1e6) 71.5) => 251.748 kHz 130 | 131 | PDM array has 132 entries, so assuming that's one period then the sinusoidal drive signal frequency is 132 | (/ 100000 132.0) => 757.57 Hz 133 | (/ 50000 132.0) => 378.79 Hz 134 | 135 | 136 | --- 137 | 138 | I'm not sure how to process the captured signal. 139 | It's not a simple PDM because the amplitude isn't binary. 140 | 141 | Maybe I should try just emitting a single GPIO signal and seeing if I can reconstruct that first? 142 | 143 | Okay, yeah, that definitely looks more like a PDM signal. 144 | So the modulation I'm getting probably really is the desired effect of the signals combining. 145 | I wonder if I should just do a rolling window and calculate variance and then take the minimum of that or something? 146 | 147 | --- 148 | 149 | Verifying the PDM by importing the drive signal into python and analyzing, we match the expected 757.58 drive frequency. Cool. 150 | Though this may be begging the question since the calcs use the drive rate, which is derived assuming the ADC clock stuff above is correct. 151 | Let's see if we can recover it from a single signal 152 | 153 | --- 154 | 155 | If I take a rolling 20-sample window of the high-pass filtered signal, I get a decent looking sine wave. 156 | However, the frequency is 2670 Hz. Where does this come from? 157 | 158 | 159 | Ahh, it's not really a clean frequency. I can fit 10-ish cycles, but not more than that. 160 | Manually playing with the curve fit I can see it's not lining up nicely. 161 | The spectrogram isn't super clean, but there's a peak much higher than the rest at 2668 Hz, basically the same as we saw above. 162 | 163 | 164 | ---- 165 | 166 | Hmm, lets look at the coupled signal with just a single drive PDM. 167 | With 2000Hz PDM cutoff, there's a strong peak at 1385 Hz. 168 | 169 | Maybe the drive timer is actually off? 170 | After all, Embassy is probably doing some rounding or whatever to set the frequency. But by almost 2x? That seems pretty bad. 171 | 172 | Let's try with 10kHz timer (PDM drive rate) 173 | 174 | no real luck. though maybe I'm removing the signal with the low pass filter that I used to eliminate the line noise? 175 | If I look at max power without low pass filter, it's around 84 Hz which is close to the 76 Hz theoretical. 176 | 177 | I should crank up the PDM tick so that the generated signal is well above 50 Hz. 178 | 179 | Going back to the 100kHz tick (757 Hz drive) 180 | 181 | the unfiltered peak is 84 Hz still (???) 182 | AHHH, is that microcontroller noise? Nah, that's 72 MHz. 183 | Hmm, power grid here should be about 50 Hz. 184 | 185 | Okay, did another recording of just line noise and it's still coming out at 83.92 Hz. 186 | So the ADC sample rate must be off. 187 | 188 | Yeah, Claude bullshitted me. Looking at the clock tree it says ADC max is 14 MHz. 189 | let's look at registers with cube debugger 190 | PLLMUL => pll input clock x 9 191 | 192 | HPRE = AHB prescaler, SYSCLK not divided 193 | PPRE2 = APB2 prescaler HCLK not divided 194 | ADCPRE = PCLK2 divided by 6 195 | 196 | Okay, so assuming we're at max SYSCLK of 72 MHz, that gives ADC clock of 12 MHz. 197 | 198 | Adjusting for this, line noise frequency (FFT of raw data) comes out to 58.74 Hz, which is pretty off. 199 | 200 | Hmm, maybe that's correlated noise from shorter sample time. 201 | When I use the longest ADC sampling time, the collected data gives 52.61 Hz. 202 | If I software filter out noise above 100 Hz, then it's 50.10 Hz. 203 | 204 | 205 | Let's collect more driven data using this longer sample time. 206 | 207 | Okay! Peak is at 796 Hz. That's close to 757 Hz drive, I guess. 208 | 209 | What if I do 50? 210 | Hmm, seeing two peaks, biggest at 1195 Hz. 211 | Not sure how this makes sense, since I'm filtering loss pass with 1000 Hz cutoff 212 | 213 | If I tweak the filtering, I can recover 737 Hz. 214 | 215 | GPIO speed shoudln't be an issue, it's set to slowest but that's 2 MHz. 216 | 217 | Taking line noise again with longest sample time, FFT shows peak at 52.61 Hz again. 218 | 219 | I don't think it's a hardware thing, I measured on bluepill too. 220 | Holding the cable really helps. 221 | 222 | --- 223 | 224 | I measured line noise with my scope, it's definietly just 50 Hz. 225 | MCU clock doesn't seem to be a problem, since if I emit a 1kHz signal on the GPIO the scope measures it as 1.00 kHz. 226 | Emitting 50 Hz signal measures as 50.00 Hz. So MCU clock seems aight. 227 | 228 | Possible the issue is related to DMA dropping occasional samples and the larger signal getting out of sync? 229 | If that were the case, I should see freq get "more accurate" with less data. 230 | 231 | Nah, changing the offsets and amonut doesn't move it much, still around 52.6 Hz. 232 | Maybe it's roundoff / numerical error on the python FFT impl? 233 | That seems quite unlikely, but not sure what else it might be. 234 | 235 | --- 236 | 237 | Recording the MCU's generated 50Hz square wave with the ADC and analyzing in the computer gives 52.89 Hz FFT. 238 | 239 | Ah, found the issue! ADC converstion time is sampling duration + a 12.5 cycle overhead. 240 | ADding that in gives me FFT on generated signal of 50.01 Hz. 241 | 242 | --- 243 | 244 | Now looking at a single PDM waveform ticked out at 100 kHz (implied 757.6 Hz signal), high pass above 500 Hz to remove line-noise and low pass under 1000 Hz I see just a single peak at 758 Hz. Nice! 245 | 246 | 247 | ### Aug 6 248 | 249 | Had a chat with Mitko. 250 | Look into "pulse density modulation" for signal generation. Also read color university paper in repo. Mitko will add patent too. 251 | 252 | --- 253 | 254 | Looking more into minimal reproducible example for the hang I've been running into. 255 | 256 | It seems like the issue may be resolved by doing a full power reset when flashing new firmware. 257 | So maybe something about the USB bus is getting corrupted in such a way that causes the later hang? 258 | 259 | I do need to trigger interrupt when USB disconnects --- right now the USB tasks just hang in wfe. 260 | (discovered this via unplugging and then Ctrl-C probe-run, which listed /Users/dev/.cargo/git/checkouts/embassy-9312dcb0ed774b29/46aa206/embassy-executor/src/arch/cortex_m.rs:106:21) 261 | 262 | Managed to catch and debug registers via STM32Cube. 263 | ICSR 264 | ISRPending 1 265 | VectPending 0x024 266 | 267 | 268 | This vect points to USB stuff, but I didn't see any obvious error flags in the USB registers. 269 | 270 | Had another freeze and captured the pending interrupt as 0x01B, which is DMA1_CHANNEL5 271 | 272 | // TODO: possible I need to bind 273 | // DMA1_CHANNEL5 => timer::UpdateInterruptHandler; 274 | 275 | 276 | When the app is running fine, though, and I read this register, it fluctuates between: 277 | 278 | 0x01B 279 | 0x01F 280 | 281 | and also (when USB is being read) 282 | 283 | 0x024 284 | 285 | I'm not sure how Claude knew 0x024 was USB --- I'd like to see this in a datasheet 286 | 287 | the enum values in https://docs.embassy.dev/embassy-stm32/git/stm32f103c8/interrupt/enum.Interrupt.html don't match 288 | 289 | 290 | --- 291 | 292 | - if code is mapped into RAM, it's possible stack could clobber it if it's too big. 293 | debuggers 294 | - uses jetbrains integrated debugger in RustRover. 295 | - espressif has onboard debugger over USB 296 | - set breakpoints, watchpoints etc. 297 | - break on interrupt 298 | 299 | 300 | --- 301 | 302 | crash still occurs when debugger data cables disconnected 303 | 304 | 305 | tried adding my own hardfault handler 306 | 307 | use cortex_m_rt::{exception, ExceptionFrame}; 308 | #[exception] 309 | unsafe fn HardFault(ef: &ExceptionFrame) -> ! { 310 | loop {} 311 | } 312 | 313 | but didn't end up there on the freeze. (all registers point to 0x2100_0000 314 | 315 | port install gdb +multiarch 316 | 317 | break on exception. 318 | 319 | 320 | 321 | ### Aug 5 - debugging 322 | 323 | 324 | https://matrix.to/#/!YoLPkieCYHGzdjUhOK:matrix.org/$dOadSX4X9q9CnQEiKhKyZJr5toI8R8q08fk8qE3VloE?via=matrix.org&via=tchncs.de&via=mozilla.org 325 | 326 | 327 | 328 | 14:49:40 $ /Applications/STMicroelectronics/STM32Cube/STM32CubeProgrammer/STM32CubeProgrammer.app/Contents/MacOs/bin/STM32_Programmer_CLI -c port=SWD 329 | ------------------------------------------------------------------- 330 | STM32CubeProgrammer v2.17.0 331 | ------------------------------------------------------------------- 332 | 333 | ST-LINK SN : 53FF6C064884534937360587 334 | ST-LINK FW : V2J37S7 335 | Board : -- 336 | Voltage : 3.24V 337 | SWD freq : 4000 KHz 338 | Connect mode: Normal 339 | Reset mode : Software reset 340 | Device ID : 0x410 341 | Revision ID : Rev X 342 | Device name : STM32F101/F102/F103 Medium-density 343 | Flash size : 64 KBytes 344 | Device type : MCU 345 | Device CPU : Cortex-M3 346 | BL Version : -- 347 | 348 | 349 | I didn't see anything in errata that seemed sus: https://www.st.com/resource/en/errata_sheet/es0340-stm32f101xcde-stm32f103xcde-device-errata-stmicroelectronics.pdf 350 | 351 | 352 | cargo install cargo-binutils 353 | rustup component add llvm-tools-preview 354 | 355 | I can print out assembly of my program now via 356 | 357 | cargo objdump --bin usb_custom --release -- -d --no-show-raw-insn --print-imm-hex 358 | 359 | that doesn't help me yet, since I don't have any fault handlers pointing to what went wrong. 360 | 361 | 362 | ### Aug 3 - ADC pickup 363 | DMA ring buffer is working. 364 | the duplicates I was getting earlier is because stop disables the circularity on the channel, but start doesn't restore it. 365 | so if you call stop once, you get screwed forever. 366 | 367 | (I should just reset the device if USB ever disconnects) 368 | 369 | based on defmt timing, each loop takes about 600--700us. 370 | awaiting USB causes us to overrun DMA, so I may need to have a larger buffer and do that in a separate task. 371 | 372 | 373 | Ugh. I have no idea what's fucked here. 374 | Even when not trying to send over USB, eventually the ringbuffer freezes somehow and the await loop stops. 375 | 376 | 377 | ugh, maybe it's bad wiring? 378 | just reflashing and fucking about, no real changes and now it's streaming alnog fine. 379 | 380 | // use embassy_stm32::interrupt; 381 | // #[interrupt] 382 | // fn DMA1_CHANNEL1() { 383 | // info!("interrupt"); 384 | // } 385 | 386 | yeah, very intermittent. 387 | Everything seems to be working fine now. this is infuriating. 388 | 389 | 390 | 391 | ### Aug 2 - signal emission and ADC pickup 392 | Claude helped me make a little USB scope visualization. 393 | 394 | For the life of me I can't seem to affect the waveform, though. 395 | The average value seems to change on every reset. 396 | 397 | There's no pulldown, so I thought it might be a floating voltage. 398 | But trying to clear it with 399 | 400 | { 401 | let _adc_pin = Output::new(p.PB1, Level::Low, Speed::VeryHigh); 402 | Timer::after_millis(1000).await; 403 | // dropping returns to high impedence 404 | } 405 | 406 | doesn't affect things. 407 | average value can be anywhere from 800 to 1400. 408 | Though I guess looking at the schematic the floating is occucring before the MCP6S21 amplifier. 409 | 410 | 411 | Let's rule out it's not some USB delay thing by generating a cycling counter. 412 | 413 | Yeah, USB and egui rendering look superfast, no problems there. 414 | 415 | Maybe the ADC isn't actually sampling properly and it's repeating itself? 416 | 417 | Yeah, I think that's what's happening. scrolling through the output file, the readings are super identical. 418 | 419 | 420 | Okay, it's something in embassy ring buffer. 421 | If I have DMA write to a static mut and I just YOLO read from it, the values are definitely change 422 | 423 | from looking at https://github.com/embassy-rs/embassy/blob/a2ea2630f648a9a46dc4f9276d9f5eaf256ae893/embassy-stm32/src/adc/ringbuffered_v2.rs#L122 it seems like I'm holding the ring buffer correctly. hmmm. 424 | 425 | 426 | 427 | ### July 31 - Rust USB ADC data streaming. 428 | Got this working first by porting Embassy USB CDC example, but on MacOS there's some kind of internal buffer that waits to fill up before any results are printed out of the file. 429 | It takes 10+ seconds so is probably a few MB. 430 | 431 | I used Cursor to generate, then had it do a custom USB class protocol thing so I can bypass the CDC_ACM virtual serial port stuff. 432 | 433 | It took a bit of coaxing, but I managed to get something working with a custom reader too. 434 | 435 | Embassy seems to have nice DMA stuff, but it doesn't seem to support F1-series. 436 | https://github.com/embassy-rs/embassy/pull/3116 437 | 438 | I should just give up on this for now and stream naively. 439 | 440 | 441 | 442 | 443 | 444 | ### July 29 - Arudino test 445 | 446 | installed arduino-ide_2.3.2_macOS_arm64 447 | 448 | 449 | followed https://community.st.com/t5/stm32-mcus/how-to-program-and-debug-the-stm32-using-the-arduino-ide/ta-p/608514 450 | 451 | add board manager URL in preferences: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json 452 | 453 | installed STM32 MCU based boards version 2.8.1 454 | 455 | Trying to upload: 456 | 457 | Sketch uses 14896 bytes (45%) of program storage space. Maximum is 32768 bytes. 458 | Global variables use 1632 bytes (15%) of dynamic memory, leaving 8608 bytes for local variables. Maximum is 10240 bytes. 459 | STM32CubeProgrammer not found (STM32_Programmer_CLI). 460 | Please install it or add '/bin' to your PATH environment: 461 | https://www.st.com/en/development-tools/stm32cubeprog.html 462 | 463 | Aight, had to install cube stuff but then the arduino upload worked: 464 | 465 | 466 | Sketch uses 14896 bytes (45%) of program storage space. Maximum is 32768 bytes. 467 | Global variables use 1632 bytes (15%) of dynamic memory, leaving 8608 bytes for local variables. Maximum is 10240 bytes. 468 | Warning: long options not supported due to getopt from FreeBSD usage. 469 | Selected interface: swd 470 | ------------------------------------------------------------------- 471 | STM32CubeProgrammer v2.17.0 472 | ------------------------------------------------------------------- 473 | 474 | ST-LINK SN : 53FF6C064884534937360587 475 | ST-LINK FW : V2J37S7 476 | Board : -- 477 | Voltage : 3.25V 478 | SWD freq : 4000 KHz 479 | Connect mode: Under Reset 480 | Reset mode : Hardware reset 481 | Device ID : 0x410 482 | Revision ID : Rev X 483 | Device name : STM32F101/F102/F103 Medium-density 484 | Flash size : 64 KBytes 485 | Device type : MCU 486 | Device CPU : Cortex-M3 487 | BL Version : -- 488 | 489 | 490 | 491 | Memory Programming ... 492 | Opening and parsing file: Base.ino.bin 493 | File : Base.ino.bin 494 | Size : 14.84 KB 495 | Address : 0x08000000 496 | 497 | 498 | Erasing memory corresponding to segment 0: 499 | Erasing internal memory sectors [0 14] 500 | Download in Progress: 501 | 502 | 503 | File download complete 504 | Time elapsed during download operation: 00:00:01.048 505 | 506 | RUNNING Program ... 507 | Address: : 0x8000000 508 | Application is running, Please Hold on... 509 | Start operation achieved successfully 510 | 511 | 512 | Was also able to get serial monitor working by setting USB Support Generic Serial CDC. 513 | Man, Arduino is nicer than Rust lol. 514 | 515 | 516 | The Arduino serial monitor only showed 100 points at a time (ugh!) but I found https://github.com/hacknus/serial-monitor-rust which worked great. 517 | 518 | uggh, serial monitor isn't actually live. 519 | Seems like it must have some buffer or otherwise be dropping stuff on the floor. 520 | Recording a few seconds and then saving a CSV only gives a 1000-ish data. 521 | 522 | but using minicom 523 | 524 | minicom -D /dev/tty.usbmodem4995277E384B1 -b 115200 -C foo.csv 525 | 526 | gives 10x the data. 527 | 528 | 529 | 530 | ### 2024 July 29 - hardware connection test 531 | Connected via stlink and jtag pins as per https://github.com/MitkoDyakov/BluePillCaliper/blob/main/Hardware/Schematics%20V1.1.pdf 532 | 533 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh 534 | 535 | 536 | $ probe-rs info 537 | Probing target via JTAG 538 | 539 | WARN probe_rs::probe::stlink: send_jtag_command 242 failed: JtagGetIdcodeError 540 | Error identifying target using protocol JTAG: An error with the usage of the probe occurred 541 | 542 | Probing target via SWD 543 | 544 | WARN probe_rs::probe::stlink: send_jtag_command 242 failed: JtagGetIdcodeError 545 | Error identifying target using protocol SWD: An error with the usage of the probe occurred 546 | 547 | 548 | ah, I was reading the schematic incorrectly. Managed to connect: 549 | 550 | $ probe-rs info 551 | Probing target via JTAG 552 | 553 | ARM Chip with debug port Default: 554 | Debug Port: DPv1, DP Designer: ARM Ltd 555 | └── 0 MemoryAP 556 | └── ROM Table (Class 1), Designer: STMicroelectronics 557 | ├── Cortex-M3 SCS (Generic IP component) 558 | │ └── CPUID 559 | │ ├── IMPLEMENTER: ARM Ltd 560 | │ ├── VARIANT: 1 561 | │ ├── PARTNO: Cortex-M3 562 | │ └── REVISION: 1 563 | ├── Cortex-M3 DWT (Generic IP component) 564 | ├── Cortex-M3 FBP (Generic IP component) 565 | ├── Cortex-M3 ITM (Generic IP component) 566 | └── Cortex-M3 TPIU (Coresight Component) 567 | -------------------------------------------------------------------------------- /analysis/design_simulations.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 45, 6 | "id": "a7aa3838-a57c-4a45-bbb3-ca19f1604426", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import numpy as np\n", 11 | "import matplotlib.pyplot as plt\n", 12 | "import scipy.signal\n", 13 | "import plotly.graph_objects as go" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 46, 19 | "id": "e5fdd2a4", 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "def filter(signal, sampling_frequency, cutoff_frequency, type=\"low\"):\n", 24 | " b, a = scipy.signal.butter(6, cutoff_frequency, fs= sampling_frequency, btype=type, analog=False) \n", 25 | " return scipy.signal.filtfilt(b, a, signal)" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 47, 31 | "id": "5c60a81e-03ec-4afb-9324-9b7bb32946f3", 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "adc_frequency = 12_000_000\n", 36 | "adc_sample_cycles = 71.5\n", 37 | "adc_sample_cycles = 239.5\n", 38 | "adc_sample_overhead_cycles = 12.5 # see reference manual section 11.6\n", 39 | "sampling_frequency = adc_frequency / (adc_sample_cycles + adc_sample_overhead_cycles)\n", 40 | "\n", 41 | "pdm_frequency = 100_000" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "id": "82b1133a", 47 | "metadata": {}, 48 | "source": [ 49 | "# PDM signal measured by Mitko\n", 50 | "\n", 51 | "I believe this was taken with a scope from a cheap pair of calipers." 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 48, 57 | "id": "aef240e9", 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "bsrr = [\n", 62 | " 0b00000000010101010000000010101010,\n", 63 | " 0b00000000010101010000000010101010,\n", 64 | " 0b00000000011010100000000010010101,\n", 65 | " 0b00000000011010100000000010010101,\n", 66 | " 0b00000000010101010000000010101010,\n", 67 | " 0b00000000100101010000000001101010,\n", 68 | " 0b00000000011010100000000010010101,\n", 69 | " 0b00000000011010100000000010010101,\n", 70 | " 0b00000000010101010000000010101010,\n", 71 | " 0b00000000100101010000000001101010,\n", 72 | " 0b00000000011010100000000010010101,\n", 73 | " 0b00000000011010100000000010010101,\n", 74 | " 0b00000000100101010000000001101010,\n", 75 | " 0b00000000100101010000000001101010,\n", 76 | " 0b00000000010110100000000010100101,\n", 77 | " 0b00000000011010100000000010010101,\n", 78 | " 0b00000000100101010000000001101010,\n", 79 | " 0b00000000100101010000000001101010,\n", 80 | " 0b00000000010110100000000010100101,\n", 81 | " 0b00000000010110100000000010100101,\n", 82 | " 0b00000000100101010000000001101010,\n", 83 | " 0b00000000101001010000000001011010,\n", 84 | " 0b00000000010110100000000010100101,\n", 85 | " 0b00000000010110100000000010100101,\n", 86 | " 0b00000000100101010000000001101010,\n", 87 | " 0b00000000101001010000000001011010,\n", 88 | " 0b00000000010110100000000010100101,\n", 89 | " 0b00000000010110100000000010100101,\n", 90 | " 0b00000000101001010000000001011010,\n", 91 | " 0b00000000101001010000000001011010,\n", 92 | " 0b00000000010101100000000010101001,\n", 93 | " 0b00000000010110100000000010100101,\n", 94 | " 0b00000000101001010000000001011010,\n", 95 | " 0b00000000101001010000000001011010,\n", 96 | " 0b00000000010101100000000010101001,\n", 97 | " 0b00000000010101100000000010101001,\n", 98 | " 0b00000000101001010000000001011010,\n", 99 | " 0b00000000101010010000000001010110,\n", 100 | " 0b00000000010101100000000010101001,\n", 101 | " 0b00000000010101100000000010101001,\n", 102 | " 0b00000000101001010000000001011010,\n", 103 | " 0b00000000101010010000000001010110,\n", 104 | " 0b00000000010101100000000010101001,\n", 105 | " 0b00000000010101100000000010101001,\n", 106 | " 0b00000000101010010000000001010110,\n", 107 | " 0b00000000101010010000000001010110,\n", 108 | " 0b00000000010101010000000010101010,\n", 109 | " 0b00000000010101100000000010101001,\n", 110 | " 0b00000000101010010000000001010110,\n", 111 | " 0b00000000101010010000000001010110,\n", 112 | " 0b00000000010101010000000010101010,\n", 113 | " 0b00000000010101010000000010101010,\n", 114 | " 0b00000000101010010000000001010110,\n", 115 | " 0b00000000101010100000000001010101,\n", 116 | " 0b00000000010101010000000010101010,\n", 117 | " 0b00000000010101010000000010101010,\n", 118 | " 0b00000000101010010000000001010110,\n", 119 | " 0b00000000101010100000000001010101,\n", 120 | " 0b00000000010101010000000010101010,\n", 121 | " 0b00000000010101010000000010101010,\n", 122 | " 0b00000000101010100000000001010101,\n", 123 | " 0b00000000101010100000000001010101,\n", 124 | " 0b00000000010101010000000010101010,\n", 125 | " 0b00000000010101010000000010101010,\n", 126 | " 0b00000000101010100000000001010101,\n", 127 | " 0b00000000101010100000000001010101,\n", 128 | " 0b00000000100101010000000001101010,\n", 129 | " 0b00000000010101010000000010101010,\n", 130 | " 0b00000000101010100000000001010101,\n", 131 | " 0b00000000101010100000000001010101,\n", 132 | " 0b00000000100101010000000001101010,\n", 133 | " 0b00000000100101010000000001101010,\n", 134 | " 0b00000000101010100000000001010101,\n", 135 | " 0b00000000011010100000000010010101,\n", 136 | " 0b00000000100101010000000001101010,\n", 137 | " 0b00000000100101010000000001101010,\n", 138 | " 0b00000000101010100000000001010101,\n", 139 | " 0b00000000011010100000000010010101,\n", 140 | " 0b00000000100101010000000001101010,\n", 141 | " 0b00000000100101010000000001101010,\n", 142 | " 0b00000000011010100000000010010101,\n", 143 | " 0b00000000011010100000000010010101,\n", 144 | " 0b00000000101001010000000001011010,\n", 145 | " 0b00000000100101010000000001101010,\n", 146 | " 0b00000000011010100000000010010101,\n", 147 | " 0b00000000011010100000000010010101,\n", 148 | " 0b00000000101001010000000001011010,\n", 149 | " 0b00000000101001010000000001011010,\n", 150 | " 0b00000000011010100000000010010101,\n", 151 | " 0b00000000010110100000000010100101,\n", 152 | " 0b00000000101001010000000001011010,\n", 153 | " 0b00000000101001010000000001011010,\n", 154 | " 0b00000000011010100000000010010101,\n", 155 | " 0b00000000010110100000000010100101,\n", 156 | " 0b00000000101001010000000001011010,\n", 157 | " 0b00000000101001010000000001011010,\n", 158 | " 0b00000000010110100000000010100101,\n", 159 | " 0b00000000010110100000000010100101,\n", 160 | " 0b00000000101010010000000001010110,\n", 161 | " 0b00000000101001010000000001011010,\n", 162 | " 0b00000000010110100000000010100101,\n", 163 | " 0b00000000010110100000000010100101,\n", 164 | " 0b00000000101010010000000001010110,\n", 165 | " 0b00000000101010010000000001010110,\n", 166 | " 0b00000000010110100000000010100101,\n", 167 | " 0b00000000010101100000000010101001,\n", 168 | " 0b00000000101010010000000001010110,\n", 169 | " 0b00000000101010010000000001010110,\n", 170 | " 0b00000000010110100000000010100101,\n", 171 | " 0b00000000010101100000000010101001,\n", 172 | " 0b00000000101010010000000001010110,\n", 173 | " 0b00000000101010010000000001010110,\n", 174 | " 0b00000000010101100000000010101001,\n", 175 | " 0b00000000010101100000000010101001,\n", 176 | " 0b00000000101010100000000001010101,\n", 177 | " 0b00000000101010010000000001010110,\n", 178 | " 0b00000000010101100000000010101001,\n", 179 | " 0b00000000010101100000000010101001,\n", 180 | " 0b00000000101010100000000001010101,\n", 181 | " 0b00000000101010100000000001010101,\n", 182 | " 0b00000000010101100000000010101001,\n", 183 | " 0b00000000010101010000000010101010,\n", 184 | " 0b00000000101010100000000001010101,\n", 185 | " 0b00000000101010100000000001010101,\n", 186 | " 0b00000000010101100000000010101001,\n", 187 | " 0b00000000010101010000000010101010,\n", 188 | " 0b00000000101010100000000001010101,\n", 189 | " 0b00000000101010100000000001010101,\n", 190 | " 0b00000000010101010000000010101010,\n", 191 | " 0b00000000010101010000000010101010,\n", 192 | " 0b00000000011010100000000010010101,\n", 193 | " 0b00000000101010100000000001010101, \n", 194 | "]" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": null, 200 | "id": "e46520a2", 201 | "metadata": {}, 202 | "outputs": [], 203 | "source": [ 204 | "pdm_num_samples = len(bsrr)\n", 205 | "pdm_signal_frequency = pdm_frequency / pdm_num_samples\n", 206 | "\n", 207 | "def bsrr_to_gpio_states(bsrr_values):\n", 208 | " num_samples = len(bsrr_values)\n", 209 | " gpio_states = np.zeros((num_samples, 16), dtype=int)\n", 210 | " \n", 211 | " for i, bsrr in enumerate(bsrr_values):\n", 212 | " set_bits = bsrr & 0xFFFF\n", 213 | " reset_bits = (bsrr >> 16) & 0xFFFF\n", 214 | " \n", 215 | " if i > 0:\n", 216 | " gpio_states[i] = gpio_states[i-1]\n", 217 | " \n", 218 | " for j in range(16):\n", 219 | " if set_bits & (1 << j):\n", 220 | " gpio_states[i, j] = 1\n", 221 | " elif reset_bits & (1 << j):\n", 222 | " gpio_states[i, j] = 0\n", 223 | " \n", 224 | " return gpio_states\n", 225 | "\n", 226 | "# Generate GPIO state matrix\n", 227 | "gpio_matrix = bsrr_to_gpio_states(bsrr)\n", 228 | "\n", 229 | "# Print shape of the resulting matrix\n", 230 | "print(f\"GPIO matrix shape: {gpio_matrix.shape}\")\n", 231 | "\n", 232 | "# Optionally, visualize the first few rows of the matrix\n", 233 | "print(\"\\nFirst 5 rows of the GPIO matrix:\")\n", 234 | "print(gpio_matrix[:5])\n" 235 | ] 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "id": "f0a98b66", 240 | "metadata": {}, 241 | "source": [ 242 | "# Generate our own PDM signal" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": 50, 248 | "id": "6ead527a", 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "def generate_pdm_waveform(N):\n", 253 | " t = np.arange(N) / N\n", 254 | " analog_signal = np.sin(2 * np.pi * t)\n", 255 | " \n", 256 | " # Normalize the analog signal to range [0, 1]\n", 257 | " normalized_signal = (analog_signal + 1) / 2\n", 258 | " \n", 259 | " # Generate PDM signal\n", 260 | " pdm_signal = np.zeros(N, dtype=int)\n", 261 | " error = 0\n", 262 | " \n", 263 | " for i in range(N):\n", 264 | " if normalized_signal[i] > error:\n", 265 | " pdm_signal[i] = 1\n", 266 | " error += 1 - normalized_signal[i]\n", 267 | " else:\n", 268 | " pdm_signal[i] = 0\n", 269 | " error -= normalized_signal[i]\n", 270 | " \n", 271 | " return pdm_signal" 272 | ] 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": null, 277 | "id": "cc04a419", 278 | "metadata": {}, 279 | "outputs": [], 280 | "source": [ 281 | "# Let's compare the transcribed and generated PDM signals\n", 282 | "\n", 283 | "transcribed = np.tile(gpio_matrix[:,0], 10)\n", 284 | "generated = np.tile(generate_pdm_waveform(pdm_num_samples), 10)\n", 285 | "\n", 286 | "import plotly.graph_objects as go\n", 287 | "from plotly.subplots import make_subplots\n", 288 | "\n", 289 | "transcribed_filtered = filter(transcribed, pdm_frequency, cutoff_frequency=1000, type=\"low\")\n", 290 | "generated_filtered = filter(generated, pdm_frequency, cutoff_frequency=1000, type=\"low\")\n", 291 | "\n", 292 | "# Create subplots\n", 293 | "fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.1)\n", 294 | "\n", 295 | "# Add trace for transcribed\n", 296 | "fig.add_trace(\n", 297 | " go.Scatter(x=np.arange(len(transcribed)), y=transcribed, mode='lines', name='Transcribed', line=dict(shape='hv')),\n", 298 | " row=1, col=1\n", 299 | ")\n", 300 | "fig.add_trace(\n", 301 | " go.Scatter(x=np.arange(len(transcribed_filtered)), y=transcribed_filtered, mode='lines', name='Transcribed (Filtered)', line=dict(color='red')),\n", 302 | " row=1, col=1\n", 303 | ")\n", 304 | "\n", 305 | "# Add trace for generated\n", 306 | "fig.add_trace(\n", 307 | " go.Scatter(x=np.arange(len(generated)), y=generated, mode='lines', name='Generated', line=dict(shape='hv')),\n", 308 | " row=2, col=1\n", 309 | ")\n", 310 | "fig.add_trace(\n", 311 | " go.Scatter(x=np.arange(len(generated_filtered)), y=generated_filtered, mode='lines', name='Generated (Filtered)', line=dict(color='red')),\n", 312 | " row=2, col=1\n", 313 | ")\n", 314 | "\n", 315 | "# Update layout\n", 316 | "fig.update_layout(\n", 317 | " height=600,\n", 318 | " width=800,\n", 319 | " title_text=\"Comparison of Transcribed and Generated with Low-Pass Filter\",\n", 320 | " showlegend=True\n", 321 | ")\n", 322 | "\n", 323 | "# Update y-axis labels\n", 324 | "fig.update_yaxes(title_text=\"Amplitude\", row=1, col=1)\n", 325 | "fig.update_yaxes(title_text=\"Amplitude\", row=2, col=1)\n", 326 | "\n", 327 | "# Update x-axis label\n", 328 | "fig.update_xaxes(title_text=\"Sample\", row=2, col=1)\n", 329 | "\n", 330 | "# Show the plot\n", 331 | "fig.show()\n" 332 | ] 333 | }, 334 | { 335 | "cell_type": "markdown", 336 | "id": "16251859", 337 | "metadata": {}, 338 | "source": [ 339 | "I'm not sure what signal is in the transcribed PDM measured by Mitko, it seems pretty different from the PDM I generated from a pure sinusoid. " 340 | ] 341 | }, 342 | { 343 | "cell_type": "markdown", 344 | "id": "de8673e2", 345 | "metadata": {}, 346 | "source": [ 347 | "# Phase measurement via cross correlation\n", 348 | "\n", 349 | "The caliper's linear position is derived from the phase of the signal reflected back at us through the scale.\n", 350 | "Do we need an analog low-pass filter to smooth the PDM pulses before we read it via the ADC, or does that all get taken care of in the correlation?" 351 | ] 352 | }, 353 | { 354 | "cell_type": "code", 355 | "execution_count": 52, 356 | "id": "3e06502e", 357 | "metadata": {}, 358 | "outputs": [], 359 | "source": [ 360 | "\n", 361 | "# Measure phases via cross correlation. Use a sliding window so we can get many measurements of the phase and thus a sense of the \"noise\"\n", 362 | "def measure_phases(samples, frequency):\n", 363 | " window_size = 150\n", 364 | " num_windows = len(samples) - window_size + 1\n", 365 | " phases = np.zeros(num_windows)\n", 366 | " t = np.arange(len(samples)) / frequency\n", 367 | "\n", 368 | " for i in range(num_windows):\n", 369 | " window = samples[i:i+window_size]\n", 370 | "\n", 371 | " sine_wave = np.sin(2 * np.pi * frequency * t[i:i+window_size])\n", 372 | " cosine_wave = np.cos(2 * np.pi * frequency * t[i:i+window_size])\n", 373 | " correlation_sine = np.sum(window * sine_wave)\n", 374 | " correlation_cosine = np.sum(window * cosine_wave)\n", 375 | " phases[i] = np.arctan2(correlation_sine, correlation_cosine)\n", 376 | "\n", 377 | " return phases" 378 | ] 379 | }, 380 | { 381 | "cell_type": "code", 382 | "execution_count": 53, 383 | "id": "884d509b", 384 | "metadata": {}, 385 | "outputs": [], 386 | "source": [ 387 | "def resample(samples, original_frequency, sampling_duration, dead_time_duration):\n", 388 | " \"\"\"\n", 389 | " Resample a given set of samples using a continuous method.\n", 390 | " Works for both upsampling and downsampling.\n", 391 | " \n", 392 | " Parameters:\n", 393 | " samples (array-like): The original samples\n", 394 | " original_frequency (float): The frequency of the original samples in Hz\n", 395 | " sampling_duration (float): The duration of sampling for each new sample in seconds\n", 396 | " dead_time_duration (float): The duration of dead time between samples in seconds\n", 397 | " \n", 398 | " Returns:\n", 399 | " numpy.ndarray: The resampled signal\n", 400 | " \"\"\"\n", 401 | " \n", 402 | " # Calculate the new target frequency\n", 403 | " target_frequency = 1 / (sampling_duration + dead_time_duration)\n", 404 | " \n", 405 | " # Calculate the total duration of the original signal\n", 406 | " total_duration = len(samples) / original_frequency\n", 407 | " \n", 408 | " # Calculate the number of points needed for the resampled signal\n", 409 | " num_points = int(total_duration * target_frequency)\n", 410 | "\n", 411 | " # Create an array to hold the resampled signal\n", 412 | " resampled = np.zeros(num_points)\n", 413 | "\n", 414 | " for i in range(num_points):\n", 415 | " # Calculate the start and end times for this sample\n", 416 | " start_time = i * (sampling_duration + dead_time_duration)\n", 417 | " end_time = start_time + sampling_duration\n", 418 | " \n", 419 | " # Convert times to fractional indices in the original sample array\n", 420 | " start_index = start_time * original_frequency\n", 421 | " end_index = end_time * original_frequency\n", 422 | " \n", 423 | " # Calculate the contribution of each original sample to the new sample\n", 424 | " index = np.arange(int(np.floor(start_index)), int(np.ceil(end_index)))\n", 425 | " weights = np.minimum(index + 1, end_index) - np.maximum(index, start_index)\n", 426 | " weights = np.maximum(weights, 0) # Ensure non-negative weights\n", 427 | " \n", 428 | " # Handle edge cases\n", 429 | " if len(index) == 0:\n", 430 | " # We're between two samples, so interpolate\n", 431 | " left_index = int(np.floor(start_index))\n", 432 | " right_index = min(left_index + 1, len(samples) - 1)\n", 433 | " t = start_index - left_index\n", 434 | " resampled[i] = samples[left_index] * (1-t) + samples[right_index] * t\n", 435 | " else:\n", 436 | " # Compute weighted average\n", 437 | " valid_indices = index < len(samples)\n", 438 | " if np.any(valid_indices):\n", 439 | " resampled[i] = np.sum(weights[valid_indices] * samples[index[valid_indices]]) / np.sum(weights[valid_indices])\n", 440 | " else:\n", 441 | " resampled[i] = samples[-1] # Use last sample if we're past the end\n", 442 | "\n", 443 | " return resampled" 444 | ] 445 | }, 446 | { 447 | "cell_type": "code", 448 | "execution_count": 58, 449 | "id": "180a666f", 450 | "metadata": {}, 451 | "outputs": [], 452 | "source": [ 453 | "adc_sample_cycles = 1.5\n", 454 | "#adc_sample_cycles = 12.5\n", 455 | "#adc_sample_cycles = 71.5\n", 456 | "#adc_sample_cycles = 239.5\n", 457 | "sampling_frequency = adc_frequency / (adc_sample_cycles + adc_sample_overhead_cycles)\n", 458 | "\n", 459 | "transcribed_resampled = resample(transcribed, pdm_frequency, sampling_duration=adc_sample_cycles/adc_frequency, dead_time_duration=adc_sample_overhead_cycles/adc_frequency)\n", 460 | "generated_resampled = resample(generated, pdm_frequency, sampling_duration=adc_sample_cycles/adc_frequency, dead_time_duration=adc_sample_overhead_cycles/adc_frequency)" 461 | ] 462 | }, 463 | { 464 | "cell_type": "code", 465 | "execution_count": null, 466 | "id": "043b4564", 467 | "metadata": {}, 468 | "outputs": [], 469 | "source": [ 470 | "# Does this resample fn work as expected?\n", 471 | "# Create a figure for comparing original and resampled signals\n", 472 | "fig = go.Figure()\n", 473 | "\n", 474 | "# Calculate time arrays for original and resampled signals\n", 475 | "original_time = np.arange(len(transcribed)) / pdm_frequency\n", 476 | "resampled_time = np.arange(len(transcribed_resampled)) / sampling_frequency\n", 477 | "\n", 478 | "duration = 0.001\n", 479 | "original_samples = int(duration * pdm_frequency)\n", 480 | "resampled_samples = int(duration * sampling_frequency)\n", 481 | "\n", 482 | "# Plot the original transcribed PWM signal for the first 100 ms\n", 483 | "fig.add_trace(\n", 484 | " go.Scatter(x=original_time[:original_samples], y=transcribed[:original_samples], \n", 485 | " mode='lines', name='Original Transcribed', line=dict(color='blue', shape='hv'))\n", 486 | ")\n", 487 | "\n", 488 | "# Plot the resampled transcribed PWM signal for the first 100 ms\n", 489 | "fig.add_trace(\n", 490 | " go.Scatter(x=resampled_time[:resampled_samples], y=transcribed_resampled[:resampled_samples], \n", 491 | " mode='markers', name='Resampled Transcribed', marker=dict(color='red', size=3))\n", 492 | ")\n", 493 | "\n", 494 | "# Update layout\n", 495 | "fig.update_layout(\n", 496 | " height=600,\n", 497 | " width=800,\n", 498 | " title_text=\"Comparison of Original and Resampled Transcribed PDM Signal\",\n", 499 | " showlegend=True,\n", 500 | " yaxis_title=\"Amplitude\",\n", 501 | " xaxis_title=\"Time (seconds)\",\n", 502 | " xaxis_range=[0, duration]\n", 503 | ")\n", 504 | "\n", 505 | "# Show the plot\n", 506 | "fig.show()\n" 507 | ] 508 | }, 509 | { 510 | "cell_type": "code", 511 | "execution_count": 60, 512 | "id": "fd50f285", 513 | "metadata": {}, 514 | "outputs": [], 515 | "source": [ 516 | "transcribed_resampled_phases = measure_phases(transcribed_resampled, sampling_frequency)\n", 517 | "generated_resampled_phases = measure_phases(generated_resampled, sampling_frequency)\n", 518 | "transcribed_resampled_phases_filtered = filter(transcribed_resampled_phases, sampling_frequency, cutoff_frequency=1000, type=\"low\")\n", 519 | "generated_resampled_phases_filtered = filter(generated_resampled_phases, sampling_frequency, cutoff_frequency=1000, type=\"low\")" 520 | ] 521 | }, 522 | { 523 | "cell_type": "code", 524 | "execution_count": null, 525 | "id": "82ad0efb", 526 | "metadata": {}, 527 | "outputs": [], 528 | "source": [ 529 | "# Create a figure with one plot\n", 530 | "fig = go.Figure()\n", 531 | "\n", 532 | "# Plot transcribed phases\n", 533 | "fig.add_trace(\n", 534 | " go.Scatter(y=transcribed_resampled_phases, mode='lines', name='Transcribed', line=dict(color='blue'))\n", 535 | ")\n", 536 | "fig.add_trace(\n", 537 | " go.Scatter(y=transcribed_resampled_phases_filtered, mode='lines', name='Transcribed (Filtered)', \n", 538 | " line=dict(color='blue', dash='dot'))\n", 539 | ")\n", 540 | "\n", 541 | "# Plot generated phases\n", 542 | "fig.add_trace(\n", 543 | " go.Scatter(y=generated_resampled_phases, mode='lines', name='Generated', line=dict(color='red'))\n", 544 | ")\n", 545 | "fig.add_trace(\n", 546 | " go.Scatter(y=generated_resampled_phases_filtered, mode='lines', name='Generated (Filtered)', \n", 547 | " line=dict(color='red', dash='dot'))\n", 548 | ")\n", 549 | "\n", 550 | "# Update layout\n", 551 | "fig.update_layout(\n", 552 | " height=600,\n", 553 | " width=800,\n", 554 | " title_text=\"Phase Measurement Comparison\",\n", 555 | " showlegend=True,\n", 556 | " yaxis_title=\"Phase (radians)\",\n", 557 | " xaxis_title=\"Window Index\"\n", 558 | ")\n", 559 | "\n", 560 | "# Update y-axis to use scientific notation\n", 561 | "fig.update_yaxes(tickformat=\".2e\")\n", 562 | "\n", 563 | "# Show the plot\n", 564 | "fig.show()" 565 | ] 566 | }, 567 | { 568 | "cell_type": "markdown", 569 | "id": "6cc3b3ab", 570 | "metadata": {}, 571 | "source": [ 572 | "Yeah, it looks like it doesn't matter at all if we run the PDM signal through a low pass filter before correlating it --- everything pretty much returns the expected phase offset of 0." 573 | ] 574 | } 575 | ], 576 | "metadata": { 577 | "kernelspec": { 578 | "display_name": "Python 3 (ipykernel)", 579 | "language": "python", 580 | "name": "python3" 581 | }, 582 | "language_info": { 583 | "codemirror_mode": { 584 | "name": "ipython", 585 | "version": 3 586 | }, 587 | "file_extension": ".py", 588 | "mimetype": "text/x-python", 589 | "name": "python", 590 | "nbconvert_exporter": "python", 591 | "pygments_lexer": "ipython3", 592 | "version": "3.12.3" 593 | } 594 | }, 595 | "nbformat": 4, 596 | "nbformat_minor": 5 597 | } 598 | -------------------------------------------------------------------------------- /analysis/analysis.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 100, 6 | "id": "a7aa3838-a57c-4a45-bbb3-ca19f1604426", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import numpy as np\n", 11 | "import pandas as pd\n", 12 | "import seaborn as sns\n", 13 | "import matplotlib.pyplot as plt\n", 14 | "import scipy.signal\n", 15 | "import plotly.graph_objects as go\n", 16 | "#from calipertron import *" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 101, 22 | "id": "e5fdd2a4", 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "def filter(signal, sampling_frequency, cutoff_frequency, type=\"low\"):\n", 27 | " b, a = scipy.signal.butter(6, cutoff_frequency, fs= sampling_frequency, btype=type, analog=False) \n", 28 | " return scipy.signal.filtfilt(b, a, signal)" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 102, 34 | "id": "aef240e9", 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "bsrr = [\n", 39 | " 0b00000000010101010000000010101010,\n", 40 | " 0b00000000010101010000000010101010,\n", 41 | " 0b00000000011010100000000010010101,\n", 42 | " 0b00000000011010100000000010010101,\n", 43 | " 0b00000000010101010000000010101010,\n", 44 | " 0b00000000100101010000000001101010,\n", 45 | " 0b00000000011010100000000010010101,\n", 46 | " 0b00000000011010100000000010010101,\n", 47 | " 0b00000000010101010000000010101010,\n", 48 | " 0b00000000100101010000000001101010,\n", 49 | " 0b00000000011010100000000010010101,\n", 50 | " 0b00000000011010100000000010010101,\n", 51 | " 0b00000000100101010000000001101010,\n", 52 | " 0b00000000100101010000000001101010,\n", 53 | " 0b00000000010110100000000010100101,\n", 54 | " 0b00000000011010100000000010010101,\n", 55 | " 0b00000000100101010000000001101010,\n", 56 | " 0b00000000100101010000000001101010,\n", 57 | " 0b00000000010110100000000010100101,\n", 58 | " 0b00000000010110100000000010100101,\n", 59 | " 0b00000000100101010000000001101010,\n", 60 | " 0b00000000101001010000000001011010,\n", 61 | " 0b00000000010110100000000010100101,\n", 62 | " 0b00000000010110100000000010100101,\n", 63 | " 0b00000000100101010000000001101010,\n", 64 | " 0b00000000101001010000000001011010,\n", 65 | " 0b00000000010110100000000010100101,\n", 66 | " 0b00000000010110100000000010100101,\n", 67 | " 0b00000000101001010000000001011010,\n", 68 | " 0b00000000101001010000000001011010,\n", 69 | " 0b00000000010101100000000010101001,\n", 70 | " 0b00000000010110100000000010100101,\n", 71 | " 0b00000000101001010000000001011010,\n", 72 | " 0b00000000101001010000000001011010,\n", 73 | " 0b00000000010101100000000010101001,\n", 74 | " 0b00000000010101100000000010101001,\n", 75 | " 0b00000000101001010000000001011010,\n", 76 | " 0b00000000101010010000000001010110,\n", 77 | " 0b00000000010101100000000010101001,\n", 78 | " 0b00000000010101100000000010101001,\n", 79 | " 0b00000000101001010000000001011010,\n", 80 | " 0b00000000101010010000000001010110,\n", 81 | " 0b00000000010101100000000010101001,\n", 82 | " 0b00000000010101100000000010101001,\n", 83 | " 0b00000000101010010000000001010110,\n", 84 | " 0b00000000101010010000000001010110,\n", 85 | " 0b00000000010101010000000010101010,\n", 86 | " 0b00000000010101100000000010101001,\n", 87 | " 0b00000000101010010000000001010110,\n", 88 | " 0b00000000101010010000000001010110,\n", 89 | " 0b00000000010101010000000010101010,\n", 90 | " 0b00000000010101010000000010101010,\n", 91 | " 0b00000000101010010000000001010110,\n", 92 | " 0b00000000101010100000000001010101,\n", 93 | " 0b00000000010101010000000010101010,\n", 94 | " 0b00000000010101010000000010101010,\n", 95 | " 0b00000000101010010000000001010110,\n", 96 | " 0b00000000101010100000000001010101,\n", 97 | " 0b00000000010101010000000010101010,\n", 98 | " 0b00000000010101010000000010101010,\n", 99 | " 0b00000000101010100000000001010101,\n", 100 | " 0b00000000101010100000000001010101,\n", 101 | " 0b00000000010101010000000010101010,\n", 102 | " 0b00000000010101010000000010101010,\n", 103 | " 0b00000000101010100000000001010101,\n", 104 | " 0b00000000101010100000000001010101,\n", 105 | " 0b00000000100101010000000001101010,\n", 106 | " 0b00000000010101010000000010101010,\n", 107 | " 0b00000000101010100000000001010101,\n", 108 | " 0b00000000101010100000000001010101,\n", 109 | " 0b00000000100101010000000001101010,\n", 110 | " 0b00000000100101010000000001101010,\n", 111 | " 0b00000000101010100000000001010101,\n", 112 | " 0b00000000011010100000000010010101,\n", 113 | " 0b00000000100101010000000001101010,\n", 114 | " 0b00000000100101010000000001101010,\n", 115 | " 0b00000000101010100000000001010101,\n", 116 | " 0b00000000011010100000000010010101,\n", 117 | " 0b00000000100101010000000001101010,\n", 118 | " 0b00000000100101010000000001101010,\n", 119 | " 0b00000000011010100000000010010101,\n", 120 | " 0b00000000011010100000000010010101,\n", 121 | " 0b00000000101001010000000001011010,\n", 122 | " 0b00000000100101010000000001101010,\n", 123 | " 0b00000000011010100000000010010101,\n", 124 | " 0b00000000011010100000000010010101,\n", 125 | " 0b00000000101001010000000001011010,\n", 126 | " 0b00000000101001010000000001011010,\n", 127 | " 0b00000000011010100000000010010101,\n", 128 | " 0b00000000010110100000000010100101,\n", 129 | " 0b00000000101001010000000001011010,\n", 130 | " 0b00000000101001010000000001011010,\n", 131 | " 0b00000000011010100000000010010101,\n", 132 | " 0b00000000010110100000000010100101,\n", 133 | " 0b00000000101001010000000001011010,\n", 134 | " 0b00000000101001010000000001011010,\n", 135 | " 0b00000000010110100000000010100101,\n", 136 | " 0b00000000010110100000000010100101,\n", 137 | " 0b00000000101010010000000001010110,\n", 138 | " 0b00000000101001010000000001011010,\n", 139 | " 0b00000000010110100000000010100101,\n", 140 | " 0b00000000010110100000000010100101,\n", 141 | " 0b00000000101010010000000001010110,\n", 142 | " 0b00000000101010010000000001010110,\n", 143 | " 0b00000000010110100000000010100101,\n", 144 | " 0b00000000010101100000000010101001,\n", 145 | " 0b00000000101010010000000001010110,\n", 146 | " 0b00000000101010010000000001010110,\n", 147 | " 0b00000000010110100000000010100101,\n", 148 | " 0b00000000010101100000000010101001,\n", 149 | " 0b00000000101010010000000001010110,\n", 150 | " 0b00000000101010010000000001010110,\n", 151 | " 0b00000000010101100000000010101001,\n", 152 | " 0b00000000010101100000000010101001,\n", 153 | " 0b00000000101010100000000001010101,\n", 154 | " 0b00000000101010010000000001010110,\n", 155 | " 0b00000000010101100000000010101001,\n", 156 | " 0b00000000010101100000000010101001,\n", 157 | " 0b00000000101010100000000001010101,\n", 158 | " 0b00000000101010100000000001010101,\n", 159 | " 0b00000000010101100000000010101001,\n", 160 | " 0b00000000010101010000000010101010,\n", 161 | " 0b00000000101010100000000001010101,\n", 162 | " 0b00000000101010100000000001010101,\n", 163 | " 0b00000000010101100000000010101001,\n", 164 | " 0b00000000010101010000000010101010,\n", 165 | " 0b00000000101010100000000001010101,\n", 166 | " 0b00000000101010100000000001010101,\n", 167 | " 0b00000000010101010000000010101010,\n", 168 | " 0b00000000010101010000000010101010,\n", 169 | " 0b00000000011010100000000010010101,\n", 170 | " 0b00000000101010100000000001010101, \n", 171 | "]" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 1, 177 | "id": "5c60a81e-03ec-4afb-9324-9b7bb32946f3", 178 | "metadata": {}, 179 | "outputs": [ 180 | { 181 | "ename": "NameError", 182 | "evalue": "name 'bsrr' is not defined", 183 | "output_type": "error", 184 | "traceback": [ 185 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 186 | "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", 187 | "Cell \u001b[0;32mIn[1], line 26\u001b[0m\n\u001b[1;32m 17\u001b[0m pdm_frequency \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m100_000\u001b[39m\n\u001b[1;32m 19\u001b[0m \u001b[38;5;66;03m#filename = \"../frontend/50kHz_drive_one_signal_239_5.txt\"\u001b[39;00m\n\u001b[1;32m 20\u001b[0m \u001b[38;5;66;03m#pdm_tick_frequency = 50_000\u001b[39;00m\n\u001b[1;32m 21\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 24\u001b[0m \u001b[38;5;66;03m#filename = \"../frontend/line_noise_bluepill_239_5.txt\"\u001b[39;00m\n\u001b[1;32m 25\u001b[0m \u001b[38;5;66;03m#filename = \"../frontend/50hz_bluepill_239_5.txt\"\u001b[39;00m\n\u001b[0;32m---> 26\u001b[0m pdm_signal_frequency \u001b[38;5;241m=\u001b[39m pdm_frequency \u001b[38;5;241m/\u001b[39m \u001b[38;5;28mlen\u001b[39m(\u001b[43mbsrr\u001b[49m)\n\u001b[1;32m 29\u001b[0m raw \u001b[38;5;241m=\u001b[39m pd\u001b[38;5;241m.\u001b[39mread_csv(filename, header \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m, names \u001b[38;5;241m=\u001b[39m [\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124madc\u001b[39m\u001b[38;5;124m\"\u001b[39m])\n\u001b[1;32m 30\u001b[0m raw[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt\u001b[39m\u001b[38;5;124m'\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mrange\u001b[39m(\u001b[38;5;241m0\u001b[39m, \u001b[38;5;28mlen\u001b[39m(raw))\n", 188 | "\u001b[0;31mNameError\u001b[0m: name 'bsrr' is not defined" 189 | ] 190 | } 191 | ], 192 | "source": [ 193 | "adc_frequency = 12_000_000\n", 194 | "adc_sample_cycles = 1.5\n", 195 | "#adc_sample_cycles = 7.5\n", 196 | "#adc_sample_cycles = 239.5\n", 197 | "adc_sample_overhead_cycles = 12.5 # see reference manual section 11.6\n", 198 | "sampling_frequency = adc_frequency / (adc_sample_cycles + adc_sample_overhead_cycles)\n", 199 | "\n", 200 | "\n", 201 | "#filename = \"../frontend/100kHz_drive.txt\"\n", 202 | "#filename = \"../frontend/100kHz_drive_one_signal.txt\"\n", 203 | "#filename = \"../frontend/10kHz_drive_one_signal.txt\"\n", 204 | "#filename = \"../frontend/line_noise.txt\"\n", 205 | "\n", 206 | "#filename = \"../frontend/100kHz_drive_one_signal_239_5.txt\"\n", 207 | "#filename = \"../frontend/100kHz_drive_239_5.txt\"\n", 208 | "filename = \"../frontend/100kHz_drive_239_5_new_scale.txt\"\n", 209 | "pdm_frequency = 100_000\n", 210 | "\n", 211 | "#filename = \"../frontend/50kHz_drive_one_signal_239_5.txt\"\n", 212 | "#pdm_tick_frequency = 50_000\n", 213 | "\n", 214 | "\n", 215 | "#filename = \"../frontend/line_noise_239_5.txt\"\n", 216 | "#filename = \"../frontend/line_noise_bluepill_239_5.txt\"\n", 217 | "#filename = \"../frontend/50hz_bluepill_239_5.txt\"\n", 218 | "pdm_signal_frequency = pdm_frequency / len(bsrr)\n", 219 | "\n", 220 | "\n", 221 | "raw = pd.read_csv(filename, header = None, names = [\"adc\"])\n", 222 | "raw['t'] = range(0, len(raw))\n", 223 | "raw" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": null, 229 | "id": "6e832871", 230 | "metadata": {}, 231 | "outputs": [], 232 | "source": [ 233 | "offset = 00_000\n", 234 | "length = 100_000\n", 235 | "\n", 236 | "d = raw.to_numpy()[offset:offset+length,0]\n", 237 | "sns.lineplot(x = range(d.shape[0]), y = d)\n", 238 | "plt.show()" 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": null, 244 | "id": "e46520a2", 245 | "metadata": {}, 246 | "outputs": [], 247 | "source": [ 248 | "def bsrr_to_gpio_states(bsrr_values):\n", 249 | " num_samples = len(bsrr_values)\n", 250 | " gpio_states = np.zeros((num_samples, 16), dtype=int)\n", 251 | " \n", 252 | " for i, bsrr in enumerate(bsrr_values):\n", 253 | " set_bits = bsrr & 0xFFFF\n", 254 | " reset_bits = (bsrr >> 16) & 0xFFFF\n", 255 | " \n", 256 | " if i > 0:\n", 257 | " gpio_states[i] = gpio_states[i-1]\n", 258 | " \n", 259 | " for j in range(16):\n", 260 | " if set_bits & (1 << j):\n", 261 | " gpio_states[i, j] = 1\n", 262 | " elif reset_bits & (1 << j):\n", 263 | " gpio_states[i, j] = 0\n", 264 | " \n", 265 | " return gpio_states\n", 266 | "\n", 267 | "# Generate GPIO state matrix\n", 268 | "gpio_matrix = bsrr_to_gpio_states(bsrr)\n", 269 | "\n", 270 | "# Print shape of the resulting matrix\n", 271 | "print(f\"GPIO matrix shape: {gpio_matrix.shape}\")\n", 272 | "\n", 273 | "# Optionally, visualize the first few rows of the matrix\n", 274 | "print(\"\\nFirst 5 rows of the GPIO matrix:\")\n", 275 | "print(gpio_matrix[:5])\n" 276 | ] 277 | }, 278 | { 279 | "cell_type": "code", 280 | "execution_count": null, 281 | "id": "7d43a0f8", 282 | "metadata": {}, 283 | "outputs": [], 284 | "source": [ 285 | "pdm_single = np.tile(gpio_matrix[:,0], 10)\n", 286 | "expected_drive_signal = filter(pdm_single, pdm_frequency, cutoff_frequency=1000, type=\"low\")\n", 287 | "\n", 288 | "t = np.arange(len(pdm_single)) / sampling_frequency\n", 289 | "plt.figure(figsize=(12, 6))\n", 290 | "plt.step(t, pdm_single, 'b-', where='post', label='PDM Signal', alpha=0.5)\n", 291 | "plt.plot(t, expected_drive_signal, 'r-', label='Reconstructed Analog Signal')\n", 292 | "plt.xlabel('Time (s)')\n", 293 | "plt.ylabel('Amplitude')\n", 294 | "plt.title('PDM to Analog Conversion')\n", 295 | "plt.legend()\n", 296 | "plt.grid(True)\n", 297 | "plt.show()" 298 | ] 299 | }, 300 | { 301 | "cell_type": "code", 302 | "execution_count": null, 303 | "id": "79adc144", 304 | "metadata": {}, 305 | "outputs": [], 306 | "source": [ 307 | "# Find the dominant frequency of analog_signal\n", 308 | "from scipy.fft import fft, fftfreq\n", 309 | "\n", 310 | "def analyze_frequency_spectrum(analog_signal, sampling_rate, name, freq_limit=10000):\n", 311 | " n = len(analog_signal)\n", 312 | " fft_result = fft(analog_signal)\n", 313 | " frequencies = fftfreq(n, 1/sampling_rate)\n", 314 | "\n", 315 | " # Find the index of the maximum amplitude in the positive frequency range\n", 316 | " positive_freq_range = frequencies > 0\n", 317 | " max_amplitude_index = np.argmax(np.abs(fft_result[positive_freq_range]))\n", 318 | "\n", 319 | " # Get the dominant frequency\n", 320 | " dominant_frequency = frequencies[positive_freq_range][max_amplitude_index]\n", 321 | "\n", 322 | " # Limit the frequency range\n", 323 | " freq_mask = (frequencies[positive_freq_range] <= freq_limit)\n", 324 | "\n", 325 | " # Plot the frequency spectrum\n", 326 | " import plotly.graph_objects as go\n", 327 | "\n", 328 | " # Create the main trace\n", 329 | " trace = go.Scatter(\n", 330 | " x=frequencies[positive_freq_range][freq_mask],\n", 331 | " y=np.abs(fft_result[positive_freq_range][freq_mask]),\n", 332 | " mode='lines',\n", 333 | " name='Frequency Spectrum'\n", 334 | " )\n", 335 | "\n", 336 | " # Create the vertical line for dominant frequency\n", 337 | " dominant_freq_line = go.Scatter(\n", 338 | " x=[dominant_frequency, dominant_frequency],\n", 339 | " y=[0, np.max(np.abs(fft_result[positive_freq_range][freq_mask]))],\n", 340 | " mode='lines',\n", 341 | " name=f'Dominant Frequency: {dominant_frequency:.2f} Hz',\n", 342 | " line=dict(color='red', dash='dash')\n", 343 | " )\n", 344 | "\n", 345 | " # Create the layout\n", 346 | " layout = go.Layout(\n", 347 | " title=f'Frequency Spectrum of {name}',\n", 348 | " xaxis=dict(title='Frequency (Hz)', range=[0, freq_limit]),\n", 349 | " yaxis=dict(title='Magnitude'),\n", 350 | " showlegend=True,\n", 351 | " legend=dict(orientation=\"h\", yanchor=\"bottom\", y=-0.2, xanchor=\"center\", x=0.5),\n", 352 | " width=800,\n", 353 | " height=500\n", 354 | " )\n", 355 | "\n", 356 | " # Create the figure and add the traces\n", 357 | " fig = go.Figure(data=[trace, dominant_freq_line], layout=layout)\n", 358 | "\n", 359 | " # Show the interactive plot\n", 360 | " fig.show()\n", 361 | "\n", 362 | " return dominant_frequency\n", 363 | "\n", 364 | "# Call the function\n", 365 | "dominant_frequency = analyze_frequency_spectrum(expected_drive_signal, pdm_frequency, \"Expected drive signal\")\n" 366 | ] 367 | }, 368 | { 369 | "cell_type": "markdown", 370 | "id": "74fce272", 371 | "metadata": {}, 372 | "source": [ 373 | "## Let's look at measured data now" 374 | ] 375 | }, 376 | { 377 | "cell_type": "code", 378 | "execution_count": null, 379 | "id": "81f823aa", 380 | "metadata": {}, 381 | "outputs": [], 382 | "source": [ 383 | "#########################################\n", 384 | "# Let's look at our measured data now.\n", 385 | "\n", 386 | "# Apply the high-pass filter to the data\n", 387 | "d_highpass = filter(d, sampling_frequency, cutoff_frequency=500, type=\"high\")\n", 388 | "\n", 389 | "# Define the number of points to plot\n", 390 | "num_points = 1000\n", 391 | "\n", 392 | "# Plot the original and filtered data for comparison using seaborn\n", 393 | "plt.figure(figsize=(12, 6))\n", 394 | "sns.lineplot(x=range(num_points), y=d[:num_points], label='Original')\n", 395 | "sns.lineplot(x=range(num_points), y=d_highpass[:num_points], label='Filtered')\n", 396 | "plt.xlabel('Sample')\n", 397 | "plt.ylabel('ADC Value')\n", 398 | "plt.title(f'Original vs High-Pass Filtered Data (First {num_points} Points)')\n", 399 | "plt.legend()\n", 400 | "plt.show()\n", 401 | "\n", 402 | "# Create a zoomed-in plot of d_filtered\n", 403 | "offset = 0\n", 404 | "length = 300\n", 405 | "plt.figure(figsize=(12, 6))\n", 406 | "sns.lineplot(x=range(offset, offset + length), y=d_highpass[offset:(offset + length)])\n", 407 | "plt.xlabel('Sample')\n", 408 | "plt.ylabel('ADC Value')\n", 409 | "plt.title('Zoomed-in View of Filtered Data')\n", 410 | "plt.show()\n", 411 | "\n" 412 | ] 413 | }, 414 | { 415 | "cell_type": "code", 416 | "execution_count": null, 417 | "id": "840df435", 418 | "metadata": {}, 419 | "outputs": [], 420 | "source": [ 421 | "d_highpass_peak_frequency = analyze_frequency_spectrum(d_highpass, sampling_frequency, \"Highpass filtered signal\")\n", 422 | "\n", 423 | "d_highpass_lowpass = filter(d_highpass, sampling_frequency, cutoff_frequency=1000, type=\"low\")\n", 424 | "d_highpass_lowpass_peak_frequency = analyze_frequency_spectrum(d_highpass_lowpass, sampling_frequency, \"Highpass/lowpass filtered signal\")\n", 425 | "\n", 426 | "d_bandpass = filter(d, sampling_frequency, cutoff_frequency=[500, 1500], type=\"bandpass\")\n", 427 | "d_bandpass_peak_frequency = analyze_frequency_spectrum(d_bandpass, sampling_frequency, \"Bandpass filtered signal\")\n" 428 | ] 429 | }, 430 | { 431 | "cell_type": "code", 432 | "execution_count": null, 433 | "id": "04902f13", 434 | "metadata": {}, 435 | "outputs": [], 436 | "source": [ 437 | "signals = [d, d_highpass, d_highpass_lowpass, d_bandpass]\n", 438 | "signal_names = ['Original', 'High-pass filtered', 'High-pass/Low-pass filtered', 'Band-pass filtered']\n", 439 | "\n", 440 | "from plotly.subplots import make_subplots\n", 441 | "\n", 442 | "fig = make_subplots(rows=len(signals), cols=1, shared_xaxes=True, vertical_spacing=0.05,\n", 443 | " subplot_titles=signal_names)\n", 444 | "\n", 445 | "for i, (s, name) in enumerate(zip(signals, signal_names), start=1):\n", 446 | " fig.add_trace(go.Scatter(\n", 447 | " x=list(range(len(s))),\n", 448 | " y=s,\n", 449 | " mode='lines',\n", 450 | " name=name\n", 451 | " ), row=i, col=1)\n", 452 | "\n", 453 | "fig.update_layout(\n", 454 | " title='Multiple Analog Signals',\n", 455 | " xaxis_title='Sample',\n", 456 | " height=1200, # Increase height to accommodate multiple subplots\n", 457 | " showlegend=False\n", 458 | ")\n", 459 | "\n", 460 | "for i in range(1, len(signals)):\n", 461 | " fig.update_yaxes(title_text='Amplitude', row=i, col=1)\n", 462 | "\n", 463 | "fig.show()" 464 | ] 465 | }, 466 | { 467 | "cell_type": "code", 468 | "execution_count": null, 469 | "id": "d200a920", 470 | "metadata": {}, 471 | "outputs": [], 472 | "source": [ 473 | "import numpy as np\n", 474 | "from scipy.optimize import curve_fit\n", 475 | "import plotly.graph_objects as go\n", 476 | "\n", 477 | "def analyze_sinusoidal_signal(signal, sampling_frequency, target_frequency, name):\n", 478 | " def sinusoidal(t, _A, phi, C):\n", 479 | " return 50 * np.cos(2 * np.pi * target_frequency * t + phi) + C\n", 480 | "\n", 481 | " # Prepare the data\n", 482 | " t = np.arange(len(signal)) / sampling_frequency\n", 483 | " y = signal\n", 484 | "\n", 485 | " # Perform the curve fitting\n", 486 | " initial_guess = [np.std(y), 0, np.mean(y)]\n", 487 | " params, _ = curve_fit(sinusoidal, t, y, p0=initial_guess)\n", 488 | "\n", 489 | " # Extract the fitted parameters\n", 490 | " A, phi, C = params\n", 491 | "\n", 492 | " print(f\"{name} - Fitted amplitude: {A:.4f}\")\n", 493 | " print(f\"{name} - Fitted phase: {phi:.4f} radians\")\n", 494 | " print(f\"{name} - Fitted offset: {C:.4f}\")\n", 495 | "\n", 496 | " # Generate the fitted curve\n", 497 | " y_fit = sinusoidal(t, 50, phi, C)\n", 498 | "\n", 499 | " # Plot the results\n", 500 | " fig = go.Figure()\n", 501 | " fig.add_trace(go.Scatter(x=t, y=y, mode='lines', name=f'{name} - Measured Signal'))\n", 502 | " fig.add_trace(go.Scatter(x=t, y=y_fit, mode='lines', name=f'{name} - Fitted Curve'))\n", 503 | "\n", 504 | " fig.update_layout(\n", 505 | " title=f'{name} - Measured Signal vs Fitted Curve',\n", 506 | " xaxis_title='Time (s)',\n", 507 | " yaxis_title='Amplitude',\n", 508 | " width=1000,\n", 509 | " height=600\n", 510 | " )\n", 511 | "\n", 512 | " fig.show()\n", 513 | " return A, phi, C\n", 514 | "\n", 515 | "analyze_sinusoidal_signal(d_highpass, sampling_frequency, d_highpass_peak_frequency, \"High-pass filtered\")\n", 516 | "analyze_sinusoidal_signal(d_highpass_lowpass, sampling_frequency, d_highpass_lowpass_peak_frequency, \"High-pass/Low-pass filtered\")\n", 517 | "analyze_sinusoidal_signal(d_bandpass, sampling_frequency, d_bandpass_peak_frequency, \"Band-pass filtered\")" 518 | ] 519 | }, 520 | { 521 | "cell_type": "markdown", 522 | "id": "43031c80", 523 | "metadata": {}, 524 | "source": [ 525 | "## Hmm, what if we take FFT of raw signal and try to extract phase?" 526 | ] 527 | }, 528 | { 529 | "cell_type": "code", 530 | "execution_count": null, 531 | "id": "93333bc8", 532 | "metadata": {}, 533 | "outputs": [], 534 | "source": [ 535 | "from scipy.fft import fft, fftfreq\n", 536 | "import plotly.graph_objects as go\n", 537 | "import numpy as np\n", 538 | "\n", 539 | "# Define frequency range constants\n", 540 | "FREQ_MIN = 100\n", 541 | "FREQ_MAX = 1000\n", 542 | "\n", 543 | "def analyze_signal(signal, sampling_frequency, name):\n", 544 | " fft_result = fft(signal)\n", 545 | " frequencies = fftfreq(len(fft_result), 1 / sampling_frequency)\n", 546 | " phases = np.angle(fft_result)\n", 547 | "\n", 548 | " # Limit the frequency range\n", 549 | " mask = (frequencies >= FREQ_MIN) & (frequencies <= FREQ_MAX)\n", 550 | " freq_limited = frequencies[mask]\n", 551 | " phases_limited = phases[mask]\n", 552 | "\n", 553 | " # Calculate FFT magnitude\n", 554 | " magnitude = np.abs(fft_result)\n", 555 | " magnitude_limited = magnitude[mask]\n", 556 | "\n", 557 | " # Find the frequency with the highest magnitude (excluding DC component)\n", 558 | " peak_index = np.argmax(magnitude_limited[1:]) + 1 # +1 to account for excluded DC component\n", 559 | " peak_frequency = freq_limited[peak_index]\n", 560 | " peak_magnitude = magnitude_limited[peak_index]\n", 561 | " peak_phase = phases_limited[peak_index]\n", 562 | "\n", 563 | " return {\n", 564 | " 'name': name,\n", 565 | " 'freq_limited': freq_limited,\n", 566 | " 'phases_limited': phases_limited,\n", 567 | " 'magnitude_limited': magnitude_limited,\n", 568 | " 'peak_frequency': peak_frequency,\n", 569 | " 'peak_magnitude': peak_magnitude,\n", 570 | " 'peak_phase': peak_phase\n", 571 | " }\n", 572 | "\n", 573 | "def plot_multiple_signals(signals_data):\n", 574 | " # Create the phase vs frequency plot\n", 575 | " fig_phase = go.Figure()\n", 576 | " for signal in signals_data:\n", 577 | " fig_phase.add_trace(go.Scatter(x=signal['freq_limited'], y=signal['phases_limited'], mode='lines', name=f\"{signal['name']} Phase\"))\n", 578 | "\n", 579 | " fig_phase.update_layout(\n", 580 | " title=f'Phase vs Frequency ({FREQ_MIN}-{FREQ_MAX} Hz)',\n", 581 | " xaxis_title='Frequency (Hz)',\n", 582 | " yaxis_title='Phase (radians)',\n", 583 | " width=1000,\n", 584 | " height=600\n", 585 | " )\n", 586 | "\n", 587 | " fig_phase.show()\n", 588 | "\n", 589 | " # Create the magnitude vs frequency plot\n", 590 | " fig_magnitude = go.Figure()\n", 591 | " for signal in signals_data:\n", 592 | " fig_magnitude.add_trace(go.Scatter(x=signal['freq_limited'], y=signal['magnitude_limited'], mode='lines', name=f\"{signal['name']} Magnitude\"))\n", 593 | "\n", 594 | " fig_magnitude.update_layout(\n", 595 | " title=f'Magnitude vs Frequency ({FREQ_MIN}-{FREQ_MAX} Hz)',\n", 596 | " xaxis_title='Frequency (Hz)',\n", 597 | " yaxis_title='Magnitude',\n", 598 | " width=1000,\n", 599 | " height=600\n", 600 | " )\n", 601 | "\n", 602 | " fig_magnitude.show()\n", 603 | "\n", 604 | " # Print results for each signal\n", 605 | " for signal in signals_data:\n", 606 | " print(f\"\\nResults for {signal['name']}:\")\n", 607 | " print(f\"Peak frequency: {signal['peak_frequency']:.2f} Hz\")\n", 608 | " print(f\"Peak magnitude: {signal['peak_magnitude']:.2f}\")\n", 609 | " print(f\"Phase at peak frequency: {signal['peak_phase']:.2f} radians\")\n", 610 | "\n", 611 | "\n", 612 | "signals_to_analyze = [\n", 613 | " (d, sampling_frequency, \"Raw analog signal\"),\n", 614 | " (d_bandpass, sampling_frequency, \"Bandpass filtered analog signal\"),\n", 615 | "]\n", 616 | "\n", 617 | "signals_data = [analyze_signal(signal, sf, name) for signal, sf, name in signals_to_analyze]\n", 618 | "\n", 619 | "# Plot and print results for all signals\n", 620 | "plot_multiple_signals(signals_data)\n" 621 | ] 622 | }, 623 | { 624 | "cell_type": "code", 625 | "execution_count": null, 626 | "id": "81ed8382", 627 | "metadata": {}, 628 | "outputs": [], 629 | "source": [ 630 | "# Analyze frequency sweep\n", 631 | "df = pd.read_csv(\"../frontend/frequency_sweep.csv\")\n", 632 | "d = df.to_numpy()\n", 633 | "\n", 634 | "adc_frequency = 12_000_000\n", 635 | "adc_sample_cycles = 239.5\n", 636 | "adc_sample_overhead_cycles = 12.5 # see reference manual section 11.6\n", 637 | "sampling_frequency = adc_frequency / (adc_sample_cycles + adc_sample_overhead_cycles)\n", 638 | "\n", 639 | "unique_frequencies = np.unique(d[:, 0])\n", 640 | "samples_per_frequency = (d[:, 0] == unique_frequencies[0]).sum()\n", 641 | "window_size = 1000 # Adjust this value as needed\n", 642 | "phases_per_frequency = samples_per_frequency - window_size + 1\n", 643 | "results = np.empty((len(unique_frequencies), phases_per_frequency))\n", 644 | "\n", 645 | "for i_frequency, pdm_frequency in enumerate(unique_frequencies):\n", 646 | " samples = d[d[:, 0] == pdm_frequency, 1]\n", 647 | " signal_frequency = pdm_frequency / len(bsrr)\n", 648 | " t = np.arange(samples_per_frequency) / sampling_frequency\n", 649 | "\n", 650 | " for i in range(phases_per_frequency):\n", 651 | " window = samples[i:i+window_size] \n", 652 | " sine_wave = np.sin(2 * np.pi * signal_frequency * t[i:i+window_size])\n", 653 | " cosine_wave = np.cos(2 * np.pi * signal_frequency * t[i:i+window_size])\n", 654 | "\n", 655 | " correlation_sine = np.sum(window * sine_wave)\n", 656 | " correlation_cosine = np.sum(window * cosine_wave)\n", 657 | " phase = np.arctan2(correlation_sine, correlation_cosine)\n", 658 | " results[i_frequency, i] = phase" 659 | ] 660 | }, 661 | { 662 | "cell_type": "code", 663 | "execution_count": null, 664 | "id": "c81483ff", 665 | "metadata": {}, 666 | "outputs": [], 667 | "source": [ 668 | "# Calculate median phase for each frequency\n", 669 | "median_phases = np.median(results, axis=1)\n", 670 | "\n", 671 | "# Convert PDM frequency to kHz for better readability\n", 672 | "pdm_frequency_kHz = unique_frequencies / 1000\n", 673 | "\n", 674 | "import plotly.graph_objects as go\n", 675 | "from plotly.subplots import make_subplots\n", 676 | "\n", 677 | "# Create figure\n", 678 | "fig = make_subplots()\n", 679 | "\n", 680 | "# Scatter plot of all phase values\n", 681 | "for i in range(results.shape[0]):\n", 682 | " fig.add_trace(\n", 683 | " go.Scatter(\n", 684 | " x=[pdm_frequency_kHz[i]] * results.shape[1],\n", 685 | " y=results[i],\n", 686 | " mode='markers',\n", 687 | " marker=dict(color='blue', opacity=0.005),\n", 688 | " name='All phases',\n", 689 | " showlegend=i==0\n", 690 | " )\n", 691 | " )\n", 692 | "\n", 693 | "# Scatter plot of median phases as red dots\n", 694 | "fig.add_trace(\n", 695 | " go.Scatter(\n", 696 | " x=pdm_frequency_kHz,\n", 697 | " y=median_phases,\n", 698 | " mode='markers',\n", 699 | " marker=dict(color='red', size=10),\n", 700 | " name='Median'\n", 701 | " )\n", 702 | ")\n", 703 | "\n", 704 | "# Update layout\n", 705 | "fig.update_layout(\n", 706 | " title='Phase vs PDM Frequency',\n", 707 | " xaxis_title='PDM Frequency (kHz)',\n", 708 | " yaxis_title='Phase (radians)',\n", 709 | " legend_title='Legend',\n", 710 | " height=600,\n", 711 | " width=1200\n", 712 | ")\n", 713 | "\n", 714 | "# Add grid\n", 715 | "fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='lightgray')\n", 716 | "fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='lightgray')\n", 717 | "\n", 718 | "# Show the interactive plot\n", 719 | "fig.show()" 720 | ] 721 | } 722 | ], 723 | "metadata": { 724 | "kernelspec": { 725 | "display_name": "Python 3 (ipykernel)", 726 | "language": "python", 727 | "name": "python3" 728 | }, 729 | "language_info": { 730 | "codemirror_mode": { 731 | "name": "ipython", 732 | "version": 3 733 | }, 734 | "file_extension": ".py", 735 | "mimetype": "text/x-python", 736 | "name": "python", 737 | "nbconvert_exporter": "python", 738 | "pygments_lexer": "ipython3", 739 | "version": "3.12.3" 740 | } 741 | }, 742 | "nbformat": 4, 743 | "nbformat_minor": 5 744 | } 745 | -------------------------------------------------------------------------------- /firmware/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "atomic-polyfill" 19 | version = "1.0.3" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" 22 | dependencies = [ 23 | "critical-section", 24 | ] 25 | 26 | [[package]] 27 | name = "autocfg" 28 | version = "1.3.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 31 | 32 | [[package]] 33 | name = "bare-metal" 34 | version = "0.2.5" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3" 37 | dependencies = [ 38 | "rustc_version 0.2.3", 39 | ] 40 | 41 | [[package]] 42 | name = "bit_field" 43 | version = "0.10.2" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" 46 | 47 | [[package]] 48 | name = "bitfield" 49 | version = "0.13.2" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" 52 | 53 | [[package]] 54 | name = "bitfield" 55 | version = "0.14.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" 58 | 59 | [[package]] 60 | name = "bitflags" 61 | version = "1.3.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 64 | 65 | [[package]] 66 | name = "bitflags" 67 | version = "2.6.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 70 | 71 | [[package]] 72 | name = "bytemuck" 73 | version = "1.16.3" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" 76 | 77 | [[package]] 78 | name = "byteorder" 79 | version = "1.5.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 82 | 83 | [[package]] 84 | name = "calipertron" 85 | version = "0.1.0" 86 | dependencies = [ 87 | "bytemuck", 88 | "calipertron-core", 89 | "cortex-m", 90 | "cortex-m-rt", 91 | "defmt", 92 | "defmt-rtt", 93 | "embassy-executor", 94 | "embassy-futures", 95 | "embassy-stm32", 96 | "embassy-sync", 97 | "embassy-time", 98 | "embassy-usb", 99 | "embedded-hal 0.2.7", 100 | "heapless 0.8.0", 101 | "nb 1.1.0", 102 | "num-traits", 103 | "panic-probe", 104 | "schema", 105 | "static_cell", 106 | ] 107 | 108 | [[package]] 109 | name = "calipertron-core" 110 | version = "0.1.0" 111 | dependencies = [ 112 | "num-traits", 113 | ] 114 | 115 | [[package]] 116 | name = "cfg-if" 117 | version = "1.0.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 120 | 121 | [[package]] 122 | name = "cobs" 123 | version = "0.2.3" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" 126 | 127 | [[package]] 128 | name = "cortex-m" 129 | version = "0.7.7" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9" 132 | dependencies = [ 133 | "bare-metal", 134 | "bitfield 0.13.2", 135 | "critical-section", 136 | "embedded-hal 0.2.7", 137 | "volatile-register", 138 | ] 139 | 140 | [[package]] 141 | name = "cortex-m-rt" 142 | version = "0.7.3" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "ee84e813d593101b1723e13ec38b6ab6abbdbaaa4546553f5395ed274079ddb1" 145 | dependencies = [ 146 | "cortex-m-rt-macros", 147 | ] 148 | 149 | [[package]] 150 | name = "cortex-m-rt-macros" 151 | version = "0.7.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "f0f6f3e36f203cfedbc78b357fb28730aa2c6dc1ab060ee5c2405e843988d3c7" 154 | dependencies = [ 155 | "proc-macro2", 156 | "quote", 157 | "syn 1.0.109", 158 | ] 159 | 160 | [[package]] 161 | name = "critical-section" 162 | version = "1.1.2" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" 165 | 166 | [[package]] 167 | name = "darling" 168 | version = "0.20.10" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 171 | dependencies = [ 172 | "darling_core", 173 | "darling_macro", 174 | ] 175 | 176 | [[package]] 177 | name = "darling_core" 178 | version = "0.20.10" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 181 | dependencies = [ 182 | "fnv", 183 | "ident_case", 184 | "proc-macro2", 185 | "quote", 186 | "strsim", 187 | "syn 2.0.72", 188 | ] 189 | 190 | [[package]] 191 | name = "darling_macro" 192 | version = "0.20.10" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 195 | dependencies = [ 196 | "darling_core", 197 | "quote", 198 | "syn 2.0.72", 199 | ] 200 | 201 | [[package]] 202 | name = "defmt" 203 | version = "0.3.8" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "a99dd22262668b887121d4672af5a64b238f026099f1a2a1b322066c9ecfe9e0" 206 | dependencies = [ 207 | "bitflags 1.3.2", 208 | "defmt-macros", 209 | ] 210 | 211 | [[package]] 212 | name = "defmt-macros" 213 | version = "0.3.9" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "e3a9f309eff1f79b3ebdf252954d90ae440599c26c2c553fe87a2d17195f2dcb" 216 | dependencies = [ 217 | "defmt-parser", 218 | "proc-macro-error", 219 | "proc-macro2", 220 | "quote", 221 | "syn 2.0.72", 222 | ] 223 | 224 | [[package]] 225 | name = "defmt-parser" 226 | version = "0.3.4" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "ff4a5fefe330e8d7f31b16a318f9ce81000d8e35e69b93eae154d16d2278f70f" 229 | dependencies = [ 230 | "thiserror", 231 | ] 232 | 233 | [[package]] 234 | name = "defmt-rtt" 235 | version = "0.4.1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "bab697b3dbbc1750b7c8b821aa6f6e7f2480b47a99bc057a2ed7b170ebef0c51" 238 | dependencies = [ 239 | "critical-section", 240 | "defmt", 241 | ] 242 | 243 | [[package]] 244 | name = "document-features" 245 | version = "0.2.10" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" 248 | dependencies = [ 249 | "litrs", 250 | ] 251 | 252 | [[package]] 253 | name = "embassy-embedded-hal" 254 | version = "0.1.0" 255 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 256 | dependencies = [ 257 | "defmt", 258 | "embassy-futures", 259 | "embassy-sync", 260 | "embassy-time", 261 | "embedded-hal 0.2.7", 262 | "embedded-hal 1.0.0", 263 | "embedded-hal-async", 264 | "embedded-storage", 265 | "embedded-storage-async", 266 | "nb 1.1.0", 267 | ] 268 | 269 | [[package]] 270 | name = "embassy-executor" 271 | version = "0.5.0" 272 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 273 | dependencies = [ 274 | "cortex-m", 275 | "critical-section", 276 | "defmt", 277 | "document-features", 278 | "embassy-executor-macros", 279 | "embassy-time-driver", 280 | "embassy-time-queue-driver", 281 | ] 282 | 283 | [[package]] 284 | name = "embassy-executor-macros" 285 | version = "0.4.1" 286 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 287 | dependencies = [ 288 | "darling", 289 | "proc-macro2", 290 | "quote", 291 | "syn 2.0.72", 292 | ] 293 | 294 | [[package]] 295 | name = "embassy-futures" 296 | version = "0.1.1" 297 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 298 | 299 | [[package]] 300 | name = "embassy-hal-internal" 301 | version = "0.1.0" 302 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 303 | dependencies = [ 304 | "cortex-m", 305 | "critical-section", 306 | "defmt", 307 | "num-traits", 308 | ] 309 | 310 | [[package]] 311 | name = "embassy-net-driver" 312 | version = "0.2.0" 313 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 314 | dependencies = [ 315 | "defmt", 316 | ] 317 | 318 | [[package]] 319 | name = "embassy-net-driver-channel" 320 | version = "0.2.0" 321 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 322 | dependencies = [ 323 | "embassy-futures", 324 | "embassy-net-driver", 325 | "embassy-sync", 326 | ] 327 | 328 | [[package]] 329 | name = "embassy-stm32" 330 | version = "0.1.0" 331 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 332 | dependencies = [ 333 | "bit_field", 334 | "bitflags 2.6.0", 335 | "cfg-if", 336 | "cortex-m", 337 | "cortex-m-rt", 338 | "critical-section", 339 | "defmt", 340 | "document-features", 341 | "embassy-embedded-hal", 342 | "embassy-futures", 343 | "embassy-hal-internal", 344 | "embassy-net-driver", 345 | "embassy-sync", 346 | "embassy-time", 347 | "embassy-time-driver", 348 | "embassy-usb-driver", 349 | "embassy-usb-synopsys-otg", 350 | "embedded-can", 351 | "embedded-hal 0.2.7", 352 | "embedded-hal 1.0.0", 353 | "embedded-hal-async", 354 | "embedded-hal-nb", 355 | "embedded-io", 356 | "embedded-io-async", 357 | "embedded-storage", 358 | "embedded-storage-async", 359 | "futures-util", 360 | "nb 1.1.0", 361 | "proc-macro2", 362 | "quote", 363 | "rand_core", 364 | "sdio-host", 365 | "static_assertions", 366 | "stm32-fmc", 367 | "stm32-metapac", 368 | "vcell", 369 | "volatile-register", 370 | ] 371 | 372 | [[package]] 373 | name = "embassy-sync" 374 | version = "0.6.0" 375 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 376 | dependencies = [ 377 | "cfg-if", 378 | "critical-section", 379 | "defmt", 380 | "embedded-io-async", 381 | "futures-util", 382 | "heapless 0.8.0", 383 | ] 384 | 385 | [[package]] 386 | name = "embassy-time" 387 | version = "0.3.1" 388 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 389 | dependencies = [ 390 | "cfg-if", 391 | "critical-section", 392 | "defmt", 393 | "document-features", 394 | "embassy-time-driver", 395 | "embassy-time-queue-driver", 396 | "embedded-hal 0.2.7", 397 | "embedded-hal 1.0.0", 398 | "embedded-hal-async", 399 | "futures-util", 400 | "heapless 0.8.0", 401 | ] 402 | 403 | [[package]] 404 | name = "embassy-time-driver" 405 | version = "0.1.0" 406 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 407 | dependencies = [ 408 | "document-features", 409 | ] 410 | 411 | [[package]] 412 | name = "embassy-time-queue-driver" 413 | version = "0.1.0" 414 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 415 | 416 | [[package]] 417 | name = "embassy-usb" 418 | version = "0.2.0" 419 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 420 | dependencies = [ 421 | "defmt", 422 | "embassy-futures", 423 | "embassy-net-driver-channel", 424 | "embassy-sync", 425 | "embassy-usb-driver", 426 | "heapless 0.8.0", 427 | "ssmarshal", 428 | "usbd-hid", 429 | ] 430 | 431 | [[package]] 432 | name = "embassy-usb-driver" 433 | version = "0.1.0" 434 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 435 | dependencies = [ 436 | "defmt", 437 | ] 438 | 439 | [[package]] 440 | name = "embassy-usb-synopsys-otg" 441 | version = "0.1.0" 442 | source = "git+https://github.com/embassy-rs/embassy#e89ff7d12904d030c0efe3d92623ef7a208fa5eb" 443 | dependencies = [ 444 | "critical-section", 445 | "embassy-sync", 446 | "embassy-usb-driver", 447 | ] 448 | 449 | [[package]] 450 | name = "embedded-can" 451 | version = "0.4.1" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "e9d2e857f87ac832df68fa498d18ddc679175cf3d2e4aa893988e5601baf9438" 454 | dependencies = [ 455 | "nb 1.1.0", 456 | ] 457 | 458 | [[package]] 459 | name = "embedded-hal" 460 | version = "0.2.7" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" 463 | dependencies = [ 464 | "nb 0.1.3", 465 | "void", 466 | ] 467 | 468 | [[package]] 469 | name = "embedded-hal" 470 | version = "1.0.0" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" 473 | 474 | [[package]] 475 | name = "embedded-hal-async" 476 | version = "1.0.0" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" 479 | dependencies = [ 480 | "embedded-hal 1.0.0", 481 | ] 482 | 483 | [[package]] 484 | name = "embedded-hal-nb" 485 | version = "1.0.0" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" 488 | dependencies = [ 489 | "embedded-hal 1.0.0", 490 | "nb 1.1.0", 491 | ] 492 | 493 | [[package]] 494 | name = "embedded-io" 495 | version = "0.6.1" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" 498 | dependencies = [ 499 | "defmt", 500 | ] 501 | 502 | [[package]] 503 | name = "embedded-io-async" 504 | version = "0.6.1" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" 507 | dependencies = [ 508 | "defmt", 509 | "embedded-io", 510 | ] 511 | 512 | [[package]] 513 | name = "embedded-storage" 514 | version = "0.3.1" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032" 517 | 518 | [[package]] 519 | name = "embedded-storage-async" 520 | version = "0.4.1" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "1763775e2323b7d5f0aa6090657f5e21cfa02ede71f5dc40eead06d64dcd15cc" 523 | dependencies = [ 524 | "embedded-storage", 525 | ] 526 | 527 | [[package]] 528 | name = "encode_unicode" 529 | version = "0.3.6" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 532 | 533 | [[package]] 534 | name = "fnv" 535 | version = "1.0.7" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 538 | 539 | [[package]] 540 | name = "futures-core" 541 | version = "0.3.30" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 544 | 545 | [[package]] 546 | name = "futures-task" 547 | version = "0.3.30" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 550 | 551 | [[package]] 552 | name = "futures-util" 553 | version = "0.3.30" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 556 | dependencies = [ 557 | "futures-core", 558 | "futures-task", 559 | "pin-project-lite", 560 | "pin-utils", 561 | ] 562 | 563 | [[package]] 564 | name = "hash32" 565 | version = "0.2.1" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" 568 | dependencies = [ 569 | "byteorder", 570 | ] 571 | 572 | [[package]] 573 | name = "hash32" 574 | version = "0.3.1" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" 577 | dependencies = [ 578 | "byteorder", 579 | ] 580 | 581 | [[package]] 582 | name = "hashbrown" 583 | version = "0.13.2" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" 586 | dependencies = [ 587 | "ahash", 588 | ] 589 | 590 | [[package]] 591 | name = "heapless" 592 | version = "0.7.17" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" 595 | dependencies = [ 596 | "atomic-polyfill", 597 | "hash32 0.2.1", 598 | "rustc_version 0.4.0", 599 | "serde", 600 | "spin", 601 | "stable_deref_trait", 602 | ] 603 | 604 | [[package]] 605 | name = "heapless" 606 | version = "0.8.0" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" 609 | dependencies = [ 610 | "hash32 0.3.1", 611 | "stable_deref_trait", 612 | ] 613 | 614 | [[package]] 615 | name = "ident_case" 616 | version = "1.0.1" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 619 | 620 | [[package]] 621 | name = "libm" 622 | version = "0.2.8" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" 625 | 626 | [[package]] 627 | name = "litrs" 628 | version = "0.4.1" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 631 | 632 | [[package]] 633 | name = "lock_api" 634 | version = "0.4.12" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 637 | dependencies = [ 638 | "autocfg", 639 | "scopeguard", 640 | ] 641 | 642 | [[package]] 643 | name = "log" 644 | version = "0.4.22" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 647 | 648 | [[package]] 649 | name = "nb" 650 | version = "0.1.3" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" 653 | dependencies = [ 654 | "nb 1.1.0", 655 | ] 656 | 657 | [[package]] 658 | name = "nb" 659 | version = "1.1.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" 662 | 663 | [[package]] 664 | name = "num-traits" 665 | version = "0.2.19" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 668 | dependencies = [ 669 | "autocfg", 670 | "libm", 671 | ] 672 | 673 | [[package]] 674 | name = "once_cell" 675 | version = "1.19.0" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 678 | 679 | [[package]] 680 | name = "panic-probe" 681 | version = "0.3.2" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "4047d9235d1423d66cc97da7d07eddb54d4f154d6c13805c6d0793956f4f25b0" 684 | dependencies = [ 685 | "cortex-m", 686 | "defmt", 687 | ] 688 | 689 | [[package]] 690 | name = "pin-project-lite" 691 | version = "0.2.14" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 694 | 695 | [[package]] 696 | name = "pin-utils" 697 | version = "0.1.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 700 | 701 | [[package]] 702 | name = "portable-atomic" 703 | version = "1.7.0" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" 706 | 707 | [[package]] 708 | name = "postcard" 709 | version = "1.0.8" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" 712 | dependencies = [ 713 | "cobs", 714 | "heapless 0.7.17", 715 | "serde", 716 | ] 717 | 718 | [[package]] 719 | name = "proc-macro-error" 720 | version = "1.0.4" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 723 | dependencies = [ 724 | "proc-macro-error-attr", 725 | "proc-macro2", 726 | "quote", 727 | "syn 1.0.109", 728 | "version_check", 729 | ] 730 | 731 | [[package]] 732 | name = "proc-macro-error-attr" 733 | version = "1.0.4" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 736 | dependencies = [ 737 | "proc-macro2", 738 | "quote", 739 | "version_check", 740 | ] 741 | 742 | [[package]] 743 | name = "proc-macro2" 744 | version = "1.0.86" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 747 | dependencies = [ 748 | "unicode-ident", 749 | ] 750 | 751 | [[package]] 752 | name = "quote" 753 | version = "1.0.36" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 756 | dependencies = [ 757 | "proc-macro2", 758 | ] 759 | 760 | [[package]] 761 | name = "rand_core" 762 | version = "0.6.4" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 765 | 766 | [[package]] 767 | name = "rustc_version" 768 | version = "0.2.3" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" 771 | dependencies = [ 772 | "semver 0.9.0", 773 | ] 774 | 775 | [[package]] 776 | name = "rustc_version" 777 | version = "0.4.0" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 780 | dependencies = [ 781 | "semver 1.0.23", 782 | ] 783 | 784 | [[package]] 785 | name = "schema" 786 | version = "0.1.0" 787 | dependencies = [ 788 | "defmt", 789 | "postcard", 790 | "serde", 791 | ] 792 | 793 | [[package]] 794 | name = "scopeguard" 795 | version = "1.2.0" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 798 | 799 | [[package]] 800 | name = "sdio-host" 801 | version = "0.5.0" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "f93c025f9cfe4c388c328ece47d11a54a823da3b5ad0370b22d95ad47137f85a" 804 | 805 | [[package]] 806 | name = "semver" 807 | version = "0.9.0" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" 810 | dependencies = [ 811 | "semver-parser", 812 | ] 813 | 814 | [[package]] 815 | name = "semver" 816 | version = "1.0.23" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 819 | 820 | [[package]] 821 | name = "semver-parser" 822 | version = "0.7.0" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" 825 | 826 | [[package]] 827 | name = "serde" 828 | version = "1.0.204" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" 831 | dependencies = [ 832 | "serde_derive", 833 | ] 834 | 835 | [[package]] 836 | name = "serde_derive" 837 | version = "1.0.204" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" 840 | dependencies = [ 841 | "proc-macro2", 842 | "quote", 843 | "syn 2.0.72", 844 | ] 845 | 846 | [[package]] 847 | name = "spin" 848 | version = "0.9.8" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 851 | dependencies = [ 852 | "lock_api", 853 | ] 854 | 855 | [[package]] 856 | name = "ssmarshal" 857 | version = "1.0.0" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "f3e6ad23b128192ed337dfa4f1b8099ced0c2bf30d61e551b65fda5916dbb850" 860 | dependencies = [ 861 | "encode_unicode", 862 | "serde", 863 | ] 864 | 865 | [[package]] 866 | name = "stable_deref_trait" 867 | version = "1.2.0" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 870 | 871 | [[package]] 872 | name = "static_assertions" 873 | version = "1.1.0" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 876 | 877 | [[package]] 878 | name = "static_cell" 879 | version = "2.1.0" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "d89b0684884a883431282db1e4343f34afc2ff6996fe1f4a1664519b66e14c1e" 882 | dependencies = [ 883 | "portable-atomic", 884 | ] 885 | 886 | [[package]] 887 | name = "stm32-fmc" 888 | version = "0.3.0" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "830ed60f33e6194ecb377f5d6ab765dc0e37e7b65e765f1fa87df13336658d63" 891 | dependencies = [ 892 | "embedded-hal 0.2.7", 893 | ] 894 | 895 | [[package]] 896 | name = "stm32-metapac" 897 | version = "15.0.0" 898 | source = "git+https://github.com/embassy-rs/stm32-data-generated?tag=stm32-data-e0cfd165fd8fffaa0df66a35eeca83b228496645#bf02c35d063e1823a156468497b448ae3350a255" 899 | dependencies = [ 900 | "cortex-m", 901 | "cortex-m-rt", 902 | ] 903 | 904 | [[package]] 905 | name = "strsim" 906 | version = "0.11.1" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 909 | 910 | [[package]] 911 | name = "syn" 912 | version = "1.0.109" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 915 | dependencies = [ 916 | "proc-macro2", 917 | "quote", 918 | "unicode-ident", 919 | ] 920 | 921 | [[package]] 922 | name = "syn" 923 | version = "2.0.72" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" 926 | dependencies = [ 927 | "proc-macro2", 928 | "quote", 929 | "unicode-ident", 930 | ] 931 | 932 | [[package]] 933 | name = "thiserror" 934 | version = "1.0.63" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 937 | dependencies = [ 938 | "thiserror-impl", 939 | ] 940 | 941 | [[package]] 942 | name = "thiserror-impl" 943 | version = "1.0.63" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 946 | dependencies = [ 947 | "proc-macro2", 948 | "quote", 949 | "syn 2.0.72", 950 | ] 951 | 952 | [[package]] 953 | name = "unicode-ident" 954 | version = "1.0.12" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 957 | 958 | [[package]] 959 | name = "usb-device" 960 | version = "0.3.2" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "98816b1accafbb09085168b90f27e93d790b4bfa19d883466b5e53315b5f06a6" 963 | dependencies = [ 964 | "heapless 0.8.0", 965 | "portable-atomic", 966 | ] 967 | 968 | [[package]] 969 | name = "usbd-hid" 970 | version = "0.8.2" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "e6f291ab53d428685cc780f08a2eb9d5d6ff58622db2b36e239a4f715f1e184c" 973 | dependencies = [ 974 | "serde", 975 | "ssmarshal", 976 | "usb-device", 977 | "usbd-hid-macros", 978 | ] 979 | 980 | [[package]] 981 | name = "usbd-hid-descriptors" 982 | version = "0.8.2" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "0eee54712c5d778d2fb2da43b1ce5a7b5060886ef7b09891baeb4bf36910a3ed" 985 | dependencies = [ 986 | "bitfield 0.14.0", 987 | ] 988 | 989 | [[package]] 990 | name = "usbd-hid-macros" 991 | version = "0.8.2" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "bb573c76e7884035ac5e1ab4a81234c187a82b6100140af0ab45757650ccda38" 994 | dependencies = [ 995 | "byteorder", 996 | "hashbrown", 997 | "log", 998 | "proc-macro2", 999 | "quote", 1000 | "serde", 1001 | "syn 1.0.109", 1002 | "usbd-hid-descriptors", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "vcell" 1007 | version = "0.1.3" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" 1010 | 1011 | [[package]] 1012 | name = "version_check" 1013 | version = "0.9.5" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1016 | 1017 | [[package]] 1018 | name = "void" 1019 | version = "1.0.2" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 1022 | 1023 | [[package]] 1024 | name = "volatile-register" 1025 | version = "0.2.2" 1026 | source = "registry+https://github.com/rust-lang/crates.io-index" 1027 | checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc" 1028 | dependencies = [ 1029 | "vcell", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "zerocopy" 1034 | version = "0.7.35" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1037 | dependencies = [ 1038 | "zerocopy-derive", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "zerocopy-derive" 1043 | version = "0.7.35" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1046 | dependencies = [ 1047 | "proc-macro2", 1048 | "quote", 1049 | "syn 2.0.72", 1050 | ] 1051 | --------------------------------------------------------------------------------