summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/dual_hx711.rs75
-rw-r--r--src/main.rs128
-rw-r--r--src/pulse.rs119
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()
+ }
+}