44class Rangefinder :
55
66 _DEFAULT_RANGEFINDER_INSTANCE = None
7+ _instances = []
8+ _timer = None
9+ _current_index = 0
710
811 @classmethod
912 def get_default_rangefinder (cls ):
@@ -14,12 +17,36 @@ def get_default_rangefinder(cls):
1417 cls ._DEFAULT_RANGEFINDER_INSTANCE = cls ()
1518 return cls ._DEFAULT_RANGEFINDER_INSTANCE
1619
20+ @classmethod
21+ def _start_shared_timer (cls ):
22+ """
23+ Start the shared timer if it isn't already running.
24+ One timer cycles through all rangefinder instances to prevent acoustic crosstalk.
25+ """
26+ if cls ._timer is None :
27+ cls ._timer = Timer (- 1 )
28+ cls ._timer .init (mode = Timer .PERIODIC , period = 60 , callback = cls ._timer_callback )
29+
30+ @classmethod
31+ def _timer_callback (cls , _t ):
32+ """
33+ Shared timer callback that triggers one rangefinder per tick,
34+ cycling through all instances in round-robin order.
35+ """
36+ if not cls ._instances :
37+ return
38+ instance = cls ._instances [cls ._current_index ]
39+ instance ._do_ping ()
40+ cls ._current_index = (cls ._current_index + 1 ) % len (cls ._instances )
41+
1742 def __init__ (self , trigger_pin : int | str = "RANGE_TRIGGER" , echo_pin : int | str = "RANGE_ECHO" , timeout_us :int = 500 * 2 * 30 ):
1843 """
1944 A non-blocking class for using the HC-SR04 Ultrasonic Rangefinder.
2045 The sensor range is between 2cm and 4m.
21- Measurements are taken continuously in the background using a timer
46+ Measurements are taken continuously in the background using a shared timer
2247 and pin IRQ, so distance() returns immediately with the most recent value.
48+ When multiple rangefinders exist, they are pinged sequentially to prevent
49+ acoustic crosstalk.
2350 Timeouts will return a MAX_VALUE (65535) instead of raising an exception.
2451
2552 :param trigger_pin: The number of the pin on the microcontroller that's connected to the ``Trig`` pin on the HC-SR04.
@@ -38,29 +65,40 @@ def __init__(self, trigger_pin: int|str = "RANGE_TRIGGER", echo_pin: int|str = "
3865 # Init echo pin (in)
3966 self .echo = Pin (echo_pin , mode = Pin .IN , pull = None )
4067
68+ # Cache time functions as instance attributes for use in hard IRQ context,
69+ # where global module lookups are not allowed
70+ self ._ticks_us = time .ticks_us
71+ self ._ticks_diff = time .ticks_diff
72+
4173 self .cms = self .MAX_VALUE
4274 self ._echo_start = 0
4375 self ._waiting_for_echo = False
4476 self ._trigger_time = 0
4577 self ._first_reading_done = False
4678
79+ # Pre-bind methods as instance attributes so the shared timer callback
80+ # can call them without creating bound method objects (which would
81+ # allocate memory in hard IRQ context)
82+ self ._do_ping = self ._trigger_ping
83+ self ._do_echo = self ._echo_handler
84+
4785 # Register echo pin IRQ for both rising and falling edges
48- self .echo .irq (trigger = Pin .IRQ_RISING | Pin .IRQ_FALLING , handler = self ._echo_handler )
86+ self .echo .irq (trigger = Pin .IRQ_RISING | Pin .IRQ_FALLING , handler = self ._do_echo )
4987
50- # Start a virtual timer to periodically send trigger pulses
51- # 60ms period matches the HC-SR04 recommended minimum cycle time
52- self ._timer = Timer (- 1 )
53- self ._timer .init (mode = Timer .PERIODIC , period = 60 , callback = self ._trigger_ping )
88+ # Register this instance and start the shared timer
89+ Rangefinder ._instances .append (self )
90+ Rangefinder ._start_shared_timer ()
5491
55- def _trigger_ping (self , t ):
92+ def _trigger_ping (self ):
5693 """
57- Timer callback that sends a trigger pulse to the HC-SR04.
94+ Send a trigger pulse to the HC-SR04.
95+ Called by the shared timer callback.
5896 Only ~15us of work per call (negligible blocking).
5997 Also detects timeouts from previous measurements.
6098 """
6199 if self ._waiting_for_echo :
62100 # Check if previous measurement timed out
63- if time . ticks_diff ( time . ticks_us (), self ._trigger_time ) > self .timeout_us :
101+ if self . _ticks_diff ( self . _ticks_us (), self ._trigger_time ) > self .timeout_us :
64102 self .cms = self .MAX_VALUE
65103 self ._waiting_for_echo = False
66104 self ._first_reading_done = True
@@ -74,7 +112,7 @@ def _trigger_ping(self, t):
74112 self ._trigger .value (1 )
75113 self ._delay_us (10 )
76114 self ._trigger .value (0 )
77- self ._trigger_time = time . ticks_us ()
115+ self ._trigger_time = self . _ticks_us ()
78116 self ._waiting_for_echo = True
79117
80118 def _echo_handler (self , pin ):
@@ -85,11 +123,11 @@ def _echo_handler(self, pin):
85123 """
86124 if pin .value () == 1 :
87125 # Rising edge - echo pulse started
88- self ._echo_start = time . ticks_us ()
126+ self ._echo_start = self . _ticks_us ()
89127 else :
90128 # Falling edge - echo pulse ended
91129 if self ._waiting_for_echo :
92- pulse_time = time . ticks_diff ( time . ticks_us (), self ._echo_start )
130+ pulse_time = self . _ticks_diff ( self . _ticks_us (), self ._echo_start )
93131 if pulse_time > 0 :
94132 # Sound speed 343.2 m/s = 0.034320 cm/us = 1cm per 29.1us
95133 # Divide by 2 because pulse travels to target and back
@@ -114,6 +152,6 @@ def _delay_us(self, delay:int):
114152 Custom implementation of time.sleep_us(), used to get around the bug in MicroPython where time.sleep_us()
115153 doesn't work properly and causes the IDE to hang when uploading the code
116154 """
117- start = time . ticks_us ()
118- while time . ticks_diff ( time . ticks_us (), start ) < delay :
155+ start = self . _ticks_us ()
156+ while self . _ticks_diff ( self . _ticks_us (), start ) < delay :
119157 pass
0 commit comments