From cdf2a5026e45ac2b65001ed41cc67d86590a681a Mon Sep 17 00:00:00 2001 From: Tomasz Kramkowski Date: Thu, 16 Oct 2025 19:33:17 +0100 Subject: Driver example --- src/dual_hx711.rs | 75 ++++++++++++++++++++++++++++++++ src/main.rs | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/pulse.rs | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 322 insertions(+) create mode 100644 src/dual_hx711.rs create mode 100644 src/main.rs create mode 100644 src/pulse.rs (limited to 'src') diff --git a/src/dual_hx711.rs b/src/dual_hx711.rs new file mode 100644 index 0000000..13ec247 --- /dev/null +++ b/src/dual_hx711.rs @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2025 Tomasz Kramkowski +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Driver for reading from two HX711 ADCs with a shared clock. +//! +//! (Datasheet)[https://cdn.sparkfun.com/assets/b/f/5/a/e/hx711F_EN.pdf] +//! +//! Existing implementations could not handle a combined setup. + +use embassy_stm32::timer::GeneralInstance4Channel; +use embedded_hal::digital::InputPin; +use embedded_hal_async::digital::Wait; + +use crate::pulse::Pulse; + +/// Dual HX711 driver +pub struct DualHx711<'a, T: GeneralInstance4Channel, A: InputPin + Wait, B: InputPin + Wait> { + clock: Pulse<'a, T>, + a: A, + b: B, +} + +/// Sign extend a two's complement number stored in the low 24 bits of a u32 to +/// an i32 +fn convert_c24_to_i32(n: u32) -> i32 { + ((n << 8) as i32) >> 8 +} + +impl< + 'a, + T: GeneralInstance4Channel, + E, + A: InputPin + Wait, + B: InputPin + Wait, + > DualHx711<'a, T, A, B> +{ + /// Create a new driver from clock and data pins + /// + /// Clock pin must be a `Pulse` configured to produce an appropriately timed + /// clock signal. Refer to the HX711 datasheet for more details. + /// + /// Currently only implements CH.A, Gain:128 mode + pub fn new(clock: Pulse<'a, T>, a: A, b: B) -> Self { + Self { clock, a, b } + } + + /// Read the next available conversion from both HX711s + pub async fn read(&mut self) -> Result<(i32, i32), E> { + self.a.wait_for_low().await?; + self.b.wait_for_low().await?; + let mut val = (0, 0); + for _ in 0..24 { + self.clock.trigger_and_wait().await; + val.0 <<= 1; + val.0 |= self.a.is_high()? as u32; + val.1 <<= 1; + val.1 |= self.b.is_high()? as u32; + } + // Only implement CH.A, Gain:128 mode + self.clock.trigger_and_wait().await; + Ok((convert_c24_to_i32(val.0), convert_c24_to_i32(val.1))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn convert_twos_complement() { + assert_eq!(convert_c24_to_i32(0), 0); + assert_eq!(convert_c24_to_i32(0x7fffff), 8388607); + assert_eq!(convert_c24_to_i32(0x800000), -8388608); + assert_eq!(convert_c24_to_i32(0xffffff), -1); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..dda8d22 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2025 Tomasz Kramkowski +// SPDX-License-Identifier: GPL-3.0-or-later + +#![no_main] +#![no_std] + +mod dual_hx711; +mod pulse; + +use embassy_stm32::{bind_interrupts, peripherals, timer}; +use {defmt_rtt as _, panic_probe as _}; + +use rtic_monotonics::systick::prelude::*; + +systick_monotonic!(Mono); + +bind_interrupts!(struct Irqs { + TIM3 => timer::UpdateInterruptHandler; +}); + +#[rtic::app(device = embassy_stm32, peripherals = true, dispatchers = [SDIO, USART6])] +mod app { + use crate::pulse::CountingMode; + + use super::*; + use embassy_stm32::{ + exti::ExtiInput, + gpio::{Level, OutputOpenDrain, OutputType, Pull, Speed}, + time::*, + timer::simple_pwm::PwmPin, + Config, + }; + use pulse::Pulse; + #[shared] + struct Shared {} + #[local] + struct Local {} + + #[init] + fn init(cx: init::Context) -> (Shared, Local) { + defmt::trace!("init"); + + embassy_stm32::pac::FLASH.acr().modify(|w| { + w.set_prften(true); + w.set_icen(true); + w.set_dcen(true) + }); + + let mut config = Config::default(); + { + use embassy_stm32::rcc::*; + config.rcc.hsi = false; + config.rcc.hse = Some(Hse { + freq: mhz(25), + mode: HseMode::Oscillator, + }); + config.rcc.pll_src = PllSource::HSE; + config.rcc.pll = Some(Pll { + prediv: PllPreDiv::DIV13, + mul: PllMul::MUL104, + divp: Some(PllPDiv::DIV2), + divq: None, + divr: None, + }); + config.rcc.ahb_pre = AHBPrescaler::DIV1; + config.rcc.apb1_pre = APBPrescaler::DIV2; + config.rcc.apb2_pre = APBPrescaler::DIV1; + config.rcc.sys = Sysclk::PLL1_P; + } + + let p = embassy_stm32::init(config); + + defmt::trace!("embassy_stm32::init done"); + + Mono::start(cx.core.SYST, 100_000_000); + + let led = OutputOpenDrain::new(p.PC13, Level::High, Speed::Medium); + + blink::spawn(led).ok(); + + // Create the clock pin + let tim3_ch3_pin = PwmPin::new(p.PB0, OutputType::OpenDrain); + let tim3_pulse = Pulse::new( + p.TIM3, + None, + None, + Some(tim3_ch3_pin), + None, + khz(500), + CountingMode::EdgeAlignedUp, + Irqs, + ); + + // Data pins + let left_lc = ExtiInput::new(p.PB8, p.EXTI8, Pull::None); + let right_lc = ExtiInput::new(p.PB9, p.EXTI9, Pull::None); + + // Spawn test task + test::spawn(tim3_pulse, left_lc, right_lc).ok(); + + (Shared {}, Local {}) + } + + #[task()] + async fn test( + _cx: test::Context, + clk: Pulse<'static, peripherals::TIM3>, + a: ExtiInput<'static>, + b: ExtiInput<'static>, + ) { + // Create driver from peripherals + let mut scales = dual_hx711::DualHx711::new(clk, a, b); + + // Keep reading forever + loop { + let (a, b) = scales.read().await.unwrap(); + defmt::info!("scales read: {}, {}", a, b); + } + } + + #[task()] + async fn blink(_cx: blink::Context, mut led: OutputOpenDrain<'static>) { + loop { + led.toggle(); + Mono::delay(1.secs()).await; + } + } +} diff --git a/src/pulse.rs b/src/pulse.rs new file mode 100644 index 0000000..0fbb409 --- /dev/null +++ b/src/pulse.rs @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2024-2025 Tomasz Kramkowski +// SPDX-License-Identifier: GPL-3.0-or-later + +use embassy_stm32::{ + interrupt::typelevel::{Binding, Interrupt}, + pac::timer::vals, + time::Hertz, + timer::{ + low_level::{self, mode, OutputCompareMode, OutputPolarity, Timer}, + simple_pwm::PwmPin, + Ch1, Ch2, Ch3, Ch4, Channel, GeneralInstance4Channel, UpdateInterruptHandler, + }, + Peri, +}; + +/// The counting mode setting for the timer +pub enum CountingMode { + EdgeAlignedUp, + EdgeAlignedDown, +} + +impl From for low_level::CountingMode { + fn from(value: CountingMode) -> Self { + match value { + CountingMode::EdgeAlignedUp => low_level::CountingMode::EdgeAlignedUp, + CountingMode::EdgeAlignedDown => low_level::CountingMode::EdgeAlignedDown, + } + } +} + +/// A clock pulse wrapper driver based on STM32 4ch Timers +pub struct Pulse<'d, T: GeneralInstance4Channel> { + inner: Timer<'d, T, mode::WithIrq>, +} + +impl<'d, T: GeneralInstance4Channel> Pulse<'d, T> { + /// Create a new Pulse driver from the timer, pin channels, and + /// configuration. + pub fn new( + tim: Peri<'d, T>, + ch1: Option>, + ch2: Option>, + ch3: Option>, + ch4: Option>, + period: Hertz, + counting_mode: CountingMode, + irq: impl Binding> + Copy + 'd, + ) -> Self { + let this = Self { + inner: Timer::new_with_interrupt(tim, irq), + }; + + this.inner.set_counting_mode(counting_mode.into()); + + // Configure OPM + this.inner.regs_gp16().cr1().modify(|r| r.set_opm(true)); + + this.inner.set_frequency(period); + + this.inner.enable_outputs(); + + // Calculate 50% duty + let half_duty = this.inner.get_max_compare_value().div_ceil(2); + + // Enable channels + [ + (Channel::Ch1, ch1.is_some()), + (Channel::Ch2, ch2.is_some()), + (Channel::Ch3, ch3.is_some()), + (Channel::Ch4, ch4.is_some()), + ] + .iter() + .filter_map(|(channel, present)| present.then_some(channel)) + .for_each(|&channel| { + // In OPM, Mode1 + ActiveLow and Mode2 + ActiveHigh are identical + this.inner + .set_output_compare_mode(channel, OutputCompareMode::PwmMode1); + this.inner + .set_output_polarity(channel, OutputPolarity::ActiveLow); + + // Configure duty cycle + this.inner.set_output_compare_preload(channel, true); + this.inner.set_compare_value(channel, half_duty); + + this.inner.enable_channel(channel, true); + }); + + // Initialise the counter registers + { + let regs = this.inner.regs_core(); + // Disable update interrupt generation + regs.cr1().modify(|r| r.set_urs(vals::Urs::COUNTER_ONLY)); + // Force shadow registers to update + regs.egr().write(|r| r.set_ug(true)); + // Re-enable interrupt generation + regs.cr1().modify(|r| r.set_urs(vals::Urs::ANY_EVENT)); + } + + // NVIC Configuration + T::UpdateInterrupt::unpend(); + unsafe { T::UpdateInterrupt::enable() }; + + this + } + + /// Trigger the pulse on all channel pins, wait for it to finish + pub async fn trigger_and_wait(&mut self) { + // TODO: Needed? + self.inner.clear_update_interrupt(); + self.inner.start(); + self.inner.wait_for_update().await; + } +} + +impl<'d, T: GeneralInstance4Channel> Drop for Pulse<'d, T> { + fn drop(&mut self) { + T::UpdateInterrupt::disable() + } +} -- cgit v1.2.3-70-g09d2