diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/dual_hx711.rs | 75 | ||||
| -rw-r--r-- | src/main.rs | 128 | ||||
| -rw-r--r-- | src/pulse.rs | 119 | 
3 files changed, 322 insertions, 0 deletions
| 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 <tomasz@kramkow.ski> +// 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<Error = E> + Wait, +        B: InputPin<Error = E> + 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 <tomasz@kramkow.ski> +// 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<peripherals::TIM3>; +}); + +#[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 <tomasz@kramkow.ski> +// 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<CountingMode> 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<PwmPin<'d, T, Ch1>>, +        ch2: Option<PwmPin<'d, T, Ch2>>, +        ch3: Option<PwmPin<'d, T, Ch3>>, +        ch4: Option<PwmPin<'d, T, Ch4>>, +        period: Hertz, +        counting_mode: CountingMode, +        irq: impl Binding<T::UpdateInterrupt, UpdateInterruptHandler<T>> + 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() +    } +} | 
