Skip to content

Adds an LED_pulse feature using ISR to devices/grid_maze.py #150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions devices/grid_maze.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ def __init__(self, n_exp):
self.audio = Audio_output(1)
self.audio.set_volume = self._set_volume
self.timer = pyb.Timer(hw.available_timers.pop())
self.pulse_timer = None # only ask for a timer if we're going to need one
self.stop_pulse = False

# Initialise pins used for pokes that are directly connected to mazehub.

Expand Down Expand Up @@ -90,13 +92,73 @@ def LED_on(self, loc):
self.write_bit(exp_n, 1, "GPIO", 2 * (port - 1), True)

def LED_off(self, loc):
if self.pulse_timer is not None: # If there's a timer delegate led off
self.stop_pulse = True
return None
exp_n, port = self.map[loc]
if exp_n == -1:
self.hub_pokes[port]["LED"].off()
else:
self.write_bit(exp_n, 1, "GPIO", 2 * (port - 1), False)

def LED_pulse(self, loc, freq, duty_cycle=50, n_pulses=False):
# NOTE: (1) pulsing an LED takes a lot of CPU, and (ii) in this version
# of the code, when a LED is pusling, you CANNOT use other LEDs or
# solenoids - though in principle the code can be updated for that.
# Mouse critical flicker fusion frequency is estimated to be 14Hz; see
# https://www.sciencedirect.com/science/article/pii/S0168010218303882

# Turn on pulsed output with specified frequency and duty cycle.
if self.pulse_timer is None:
self.pulse_timer = pyb.Timer(hw.available_timers.pop())
self.fm = int(100 / next(x for x in (50, 25, 20, 10, 5, 1) if duty_cycle % x == 0))
self.off_ind = int(duty_cycle / 100 * self.fm)
self.i = 0
self.pulse_timer.init(freq=freq*self.fm)
exp_n, port = self.map[loc]
reg_value = self.read_register(exp_n, 1, "GPIO", 2)

# In ISR we cannot allocate memory, and we need to be fast to prevent
# problems, see:
# https://docs.micropython.org/en/latest/reference/isr_rules.html
# Therefore we pre-allocate a buffer for led on and off, and just write
# this in the callback, which is fast, doesn't allocate memory, etc.
# However, this means that other code that touches the reg_values will
# be overwritten by this tight loop, unless they update the LED_buf
# variuables themselves.
if exp_n == 0:
self.LED_buf_off = reg_value.to_bytes(2, "little")
self.LED_buf_on = (reg_value | 1 << (2 * (port - 1))).to_bytes(2, "little")
self.pulse_timer.callback(lambda _: self._LED_pulse_callback(exp_n, port))

@micropython.native
def _LED_pulse_callback(self, exp_n, port):
self.i += 1
if self.i == self.off_ind or self.stop_pulse:
# LED_off but use the buffer to avoid memory allocation in tight loop
if exp_n == -1:
self.hub_pokes[port]["LED"].off()
else:
self.I2C[1].mem_write(self.LED_buf_off, self.addr[exp_n], self.reg_addr["GPIO"], timeout=15)
# Stop from the callback (plus run rest of it) to avoid race conditions.
if self.stop_pulse:
self.pulse_timer.deinit()
self.stop_pulse = False
elif self.i == self.fm:
# LED_on but use the buffer to avoid memory allocation in tight loop
self.i = 0
if exp_n == -1:
self.hub_pokes[port]["LED"].on()
else:
self.I2C[1].mem_write(self.LED_buf_on, self.addr[exp_n], self.reg_addr["GPIO"], timeout=15)

def SOL_on(self, loc):
# This error should never be raised BUT there's a possibility of a race
# condition. I have never seen it given how the rest of the code is
# structured. This _can_ be fixed but I'm delaying fixing this to this
# being an actual problem.
if self.stop_pulse:
pyControlError("race condition: fix LED_off happening after SOL_off.")
exp_n, port = self.map[loc]
if exp_n == -1:
self.hub_pokes[port]["SOL"].on()
Expand Down Expand Up @@ -135,6 +197,8 @@ def _run_start(self):
def _run_stop(self):
# Turn off outputs
self.timer.deinit()
if self.pulse_timer is not None:
self.pulse_timer.deinit()
for exp_n in range(self.n_exp): # Turn off outputs on expanders.
self.write_register(exp_n, 0, "GPIO", int("01" * 8, 2)) # Set speaker enable pins high (disable speakers).
self.write_register(exp_n, 1, "GPIO", 0) # Set LED and SOL pins low.
Expand Down