capsules_extra/
hc_sr04.rs

1// Licensed under the Apache License, Version 2.0 or the MIT License.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// Copyright Tock Contributors 2024.
4
5//! HC-SR04 Ultrasonic Distance Sensor.
6//!
7//! Product Link: [HC-SR04 Product Page](https://www.sparkfun.com/products/15569)
8//! Datasheet: [HC-SR04 Datasheet](https://www.handsontec.com/dataspecs/HC-SR04-Ultrasonic.pdf)
9//!
10//! HC-SR04 ultrasonic sensor provides a very low-cost and easy method of distance measurement. It measures distance using sonar,
11//! an ultrasonic (well above human hearing) pulse (~40KHz) is transmitted from the unit and distance-to-target is determined by
12//! measuring the time required for the echo return. This sensor offers excellent range accuracy and stable readings in an easy-to-use
13//! package.
14
15use core::cell::Cell;
16
17use kernel::hil::gpio;
18use kernel::hil::sensors::{self, Distance, DistanceClient};
19use kernel::hil::time::Alarm;
20use kernel::hil::time::{AlarmClient, ConvertTicks};
21use kernel::utilities::cells::OptionalCell;
22use kernel::ErrorCode;
23
24/// Maximum duration for the echo pulse to be measured in milliseconds.
25// As specified in the datasheet:
26// https://www.handsontec.com/dataspecs/HC-SR04-Ultrasonic.pdf,
27// the maximum time for the echo pulse to return is around 23 milliseconds
28// for a maximum distance of approximately 4 meters under standard temperature
29// and pressure conditions, but we use 38 milliseconds to account for variations
30// in real-world conditions. We use a slightly higher the value to account for
31// possible variations in measurement.
32pub const MAX_ECHO_DELAY_MS: u32 = 50;
33
34/// Speed of sound in air in mm/s.
35// The speed of sound is approximately 343 meters per second, which
36// translates to 343,000 millimeters per second. This value is used
37// to calculate the distance based on the time it takes for the echo
38// to return.
39pub const SPEED_OF_SOUND: u32 = 343000;
40
41#[derive(Copy, Clone, PartialEq)]
42/// Status of the sensor.
43pub enum Status {
44    /// Sensor is idle.
45    Idle,
46
47    /// Sending ultrasonic pulse.
48    TriggerPulse,
49
50    /// Interrupt on the rising edge.
51    EchoStart,
52
53    /// Interrupt on the falling edge.
54    EchoEnd,
55}
56
57/// HC-SR04 Ultrasonic Distance Sensor Driver
58pub struct HcSr04<'a, A: Alarm<'a>> {
59    trig: &'a dyn gpio::Pin,
60    echo: &'a dyn gpio::InterruptPin<'a>,
61    alarm: &'a A,
62    start_time: Cell<u64>,
63    state: Cell<Status>,
64    distance_client: OptionalCell<&'a dyn sensors::DistanceClient>,
65}
66
67impl<'a, A: Alarm<'a>> HcSr04<'a, A> {
68    /// Create a new HC-SR04 driver.
69    pub fn new(
70        trig: &'a dyn kernel::hil::gpio::Pin,
71        echo: &'a dyn kernel::hil::gpio::InterruptPin<'a>,
72        alarm: &'a A,
73    ) -> HcSr04<'a, A> {
74        // Setup and return struct.
75        HcSr04 {
76            trig,
77            echo,
78            alarm,
79            start_time: Cell::new(0),
80            state: Cell::new(Status::Idle),
81            distance_client: OptionalCell::empty(),
82        }
83    }
84}
85
86impl<'a, A: Alarm<'a>> Distance<'a> for HcSr04<'a, A> {
87    /// Set the client for distance measurement results.
88    fn set_client(&self, distance_client: &'a dyn DistanceClient) {
89        self.distance_client.set(distance_client);
90    }
91
92    /// Start a distance measurement.
93    fn read_distance(&self) -> Result<(), ErrorCode> {
94        if self.state.get() == Status::Idle {
95            self.state.set(Status::TriggerPulse);
96            self.trig.set();
97
98            // Setting the alarm to send the trigger pulse.
99            // According to the HC-SR04 datasheet, a 10 µs pulse should be sufficient
100            // to trigger the measurement. However, in practical tests, using this
101            // 10 µs value led to inaccurate measurements.
102            // We have chosen to use a 1 ms pulse instead because it provides stable
103            // operation and accurate measurements, even though it is slightly longer
104            // than the datasheet recommendation. While this adds a small delay to the
105            // triggering process, it does not significantly affect the overall performance
106            // of the sensor.
107            self.alarm
108                .set_alarm(self.alarm.now(), self.alarm.ticks_from_ms(1));
109            Ok(())
110        } else {
111            Err(ErrorCode::BUSY)
112        }
113    }
114
115    /// Get the maximum distance the sensor can measure in mm
116    fn get_maximum_distance(&self) -> u32 {
117        // The maximum distance is determined by the maximum pulse width the sensor can detect.
118        // As specified in the datasheet: https://www.handsontec.com/dataspecs/HC-SR04-Ultrasonic.pdf,
119        // the maximum measurable distance is approximately 4 meters.
120        // Convert this to millimeters.
121        4000
122    }
123
124    /// Get the minimum distance the sensor can measure in mm.
125    fn get_minimum_distance(&self) -> u32 {
126        // The minimum distance is determined by the minimum pulse width the sensor can detect.
127        // As specified in the datasheet: https://www.handsontec.com/dataspecs/HC-SR04-Ultrasonic.pdf,
128        // the minimum measurable distance is approximately 2 cm.
129        // Convert this to millimeters.
130        20
131    }
132}
133
134impl<'a, A: Alarm<'a>> AlarmClient for HcSr04<'a, A> {
135    /// Handle the alarm event.
136    fn alarm(&self) {
137        match self.state.get() {
138            Status::TriggerPulse => {
139                self.state.set(Status::EchoStart); // Update status to waiting for echo.
140                self.echo.enable_interrupts(gpio::InterruptEdge::RisingEdge); // Enable rising edge interrupt on echo pin.
141                self.trig.clear(); // Clear the trigger pulse.
142                self.alarm.set_alarm(
143                    self.alarm.now(),
144                    self.alarm.ticks_from_ms(MAX_ECHO_DELAY_MS),
145                ); // Set alarm for maximum echo delay.
146            }
147            // Timeout for echo pulse.
148            Status::EchoStart => {
149                self.state.set(Status::Idle); // Update status to idle.
150                if let Some(distance_client) = self.distance_client.get() {
151                    // NOACK indicates that no echo was received within the expected time.
152                    distance_client.callback(Err(ErrorCode::NOACK));
153                }
154            }
155            _ => {}
156        }
157    }
158}
159
160impl<'a, A: Alarm<'a>> gpio::Client for HcSr04<'a, A> {
161    /// Handle the GPIO interrupt.
162    fn fired(&self) {
163        // Convert current ticks to microseconds using `ticks_to_us`,
164        // which handles the conversion based on the timer frequency.
165        let time = self.alarm.ticks_to_us(self.alarm.now()) as u64;
166        match self.state.get() {
167            Status::EchoStart => {
168                let _ = self.alarm.disarm(); // Disarm the alarm.
169                self.state.set(Status::EchoEnd); // Update status to waiting for echo end.
170                self.echo
171                    .enable_interrupts(gpio::InterruptEdge::FallingEdge); // Enable falling edge interrupt on echo pin.
172                self.start_time.set(time); // Record start time when echo received.
173            }
174            Status::EchoEnd => {
175                let end_time = time; // Use a local variable for the end time.
176                self.state.set(Status::Idle); // Update status to idle.
177                let duration = end_time.wrapping_sub(self.start_time.get()) as u32; // Calculate pulse duration.
178                if duration > MAX_ECHO_DELAY_MS * 1000 {
179                    // If the duration exceeds the maximum distance, return an error indicating invalid measurement.
180                    // This means that the object is out of range or no valid echo was received.
181                    if let Some(distance_client) = self.distance_client.get() {
182                        distance_client.callback(Err(ErrorCode::INVAL));
183                    }
184                } else {
185                    // Calculate distance in millimeters based on the duration of the echo.
186                    // The formula for calculating distance is:
187                    // Distance = (duration (µs) * SPEED_OF_SOUND (mm/s)) / (2 * 1_000_000), where
188                    // - `duration` is the time taken for the echo to travel to the object and back, in microseconds,
189                    // - SPEED_OF_SOUND is the speed of sound in air, in millimeters per second.
190                    // We divide by 2 because `duration` includes the round-trip time (to the object and back),
191                    // and we divide by 1,000,000 to convert from microseconds to seconds.
192                    //
193                    // To avoid using 64-bit arithmetic (u64), we restructure this equation as:
194                    // ((SPEED_OF_SOUND / 1000) * duration) / (2 * 1000).
195                    // This rearrangement reduces the scale of intermediate values, keeping them within u32 limits:
196                    // - SPEED_OF_SOUND is divided by 1000, reducing it to 343 (in mm/ms), and
197                    // - duration remains in microseconds (µs).
198                    // The final division by 2000 adjusts for the round trip and scales to the correct unit.
199                    //
200                    // This form is less intuitive, but it ensures all calculations stay within 32-bit size (u32).
201                    // Given the HC-SR04 sensor's maximum `duration` of ~23,000 µs (datasheet limit), this u32 approach
202                    // is sufficient for accurate distance calculations without risking overflow.
203                    let distance = ((SPEED_OF_SOUND / 1000) * duration) / (2 * 1000);
204                    if let Some(distance_client) = self.distance_client.get() {
205                        distance_client.callback(Ok(distance));
206                    }
207                }
208            }
209            _ => {}
210        }
211    }
212}