This week, I played around with an ultrasonic range finder and wrote a little library for it in Rust. The outcome is shown in the image above. This system measures the distance between myself and Ferris.

I used an ultrasonic sensor from Maxbotix to measure distance. This project monitors the sensor’s output and shows the measured distance on a LED display.

Here are two new things I learned from this project:

  • How to read microcontroller’s timer counter value
  • How to convert u32 (sensor reading) to &str (for the display) in a no-std environment where format! macro is not available

Ingredients

Hardware

Crates

Ultrasonic Sensor

Maxbotix ultrasonic sensor measures distance and outputs the reading through PWM or Analog. For this project, I read pulse width with a timer.

The output from the sensor looks like the screenshot below. The pulse width represents distance. You can learn more about pulse width based distance calculation at Maxbotix website.

Reading the pulse width is straightforward. Basically, I observe the state of the input pin and read the timer counter value. When read method is called, it goes like this:

  • Wait while the pin is low
  • Reset the counter
  • Wait while the pin is high
  • Read the counter value and return it to the caller

Implementation

Three Sensor Models

The sensor comes in 3 models: LV, XL, and HR. They all work the same way. Just different resolutions. To calculate the distance, we use the model-specific scale factor.

  • LV: 147uS/inch
  • XL: 58uS/cm
  • HR: 1uS/mm

I define an enum called Model like this:

impl Model {
    /// scale factor
    fn factor(self) -> u32 {
        match self {
            Model::LV => 147,
            Model::XL => 58,
            Model::HR => 1,
        }
    }
    /// unit
    fn unit(self) -> &str {
        match self {
            Model::LV => "\"",
            Model::XL => "cm",
            Model::HR => "mm",
        }
    }
}

Distance measurement using timer counter

I wrote a module called maxsonar to measure the pulse width by reading a timer’s counter value.

Here is the struct for the sensor. It takes a Timer (concrete TIM2 of the HAL crate), Model, and generic T: InputPin.

pub struct MaxSonar<T> {
    timer: TIM2,
    model: Model,
    pin: T,
}

impl<T, E> MaxSonar<T>
where
    T: InputPin<Error = E>,
    E: core::fmt::Debug,
{
    pub fn new(timer: TIM2, model: Model, pin: T, sysclk: Hertz) -> Self {
        // Configure timer for 1Mhz
        let rcc = unsafe { &(*RCC::ptr()) };
        rcc.apb1enr.modify(|_, w| w.tim2en().set_bit());
        let psc = (sysclk.0 / 1_000_000) as u16;
        timer.psc.write(|w| w.psc().bits(psc - 1));
        timer.egr.write(|w| w.ug().set_bit());
        // Start MaxSonar
        let mut sonar = MaxSonar { timer, model, pin };
        sonar.start();
        sonar
    }

    fn start(&mut self) {
        self.timer.cnt.reset();
        self.timer.cr1.write(|w| w.cen().set_bit());
    }
}

It looks like stm32f4xx-hal doesn’t implement a method to read the current timer value. So, I directly access TIM2’s cnt register in my read method.

pub fn read(&mut self) -> u32 {
    while self.pin.is_low().unwrap() {}
    self.timer.cnt.reset();
    while self.pin.is_high().unwrap() {}
    self.timer.cnt.read().bits() / self.model.factor()
}

As you can see, I do these:

  • Wait while the pin is low
  • Reset the counter
  • Wait while the pin is high
  • Read the counter value

I then calculate the distance by dividing the counter value by the scale factor. self.model.factor() returns the scale factor of the chosen model. In this project, I use Model::LV. So, the factor is 147.

format! in a no-std environment

Great. I now have the distance as u32. I just need to convert it to &str to show it on my LED display. Here is the definition of the display driver’s write_str method:

pub fn write_str(&mut self, text: &str) -> Result<(), E> 

In a std environment, I would just call format! to make a &str. But, apparently I cannot do that in a no-std environment. After a struggle to find a way to convert u32 to &str, I managed to achieve that with heapless crate. With heapless::String and write! macro, I can create formatted texts.

use core::fmt::Write;
use heapless::consts::*;
use heapless::String;

let mut data = String::<U8>::new();

loop {
    let distance = sonar.read();
    let _ = write!(data, "{:7}{}", distance, sonar.unit());
    max6955.write_str(&data).unwrap();
    data.clear();
}

sonar.unit() returns the unit for the chosen model. So, it is " here.