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}