-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
CircuitPython version and board name
Adafruit CircuitPython 10.0.3 on 2025-10-17; Adafruit CircuitPlayground Express with samd21g18Code/REPL
# SPDX-FileCopyrightText: 2025 Tom Hoffman
# SPDX-License-Identifier: MIT
# Modular Playground CircuitPython Euclidian Sequencer
DEV_MODE = True
# We use the gc library to check and manage available RAM during development
if DEV_MODE:
import gc
gc.collect()
print("Starting free bytes (after gc) = " + str(gc.mem_free()))
from micropython import const
import math # need the floor function
import board # helps set up pins, etc. on the board
import digitalio # digital (on/off) output to pins, including board LED.
import neopixel # controls the RGB LEDs on the board
import usb_midi # basic MIDI over USB support
if DEV_MODE:
gc.collect()
print("Free bytes after imports = " + str(gc.mem_free()))
############
# CONSTANTS
_CLOCK_MSG = const(0b11111000)
_LED_COUNT = const(10)
_CLOCK_LIMIT = const(23) # for quarter note
_MAX_VELOCITY = const(5)
_DEFAULT_VELOCITY = const(4)
_NOTE_COUNT = const(4)
_NOTES = const((36, 38, 59, 47))
# variables
channel_out = 1
############
# board setup steps
# set up the red LED
a_button = digitalio.DigitalInOut(board.BUTTON_A)
a_button.direction = digitalio.Direction.INPUT
a_button.pull = digitalio.Pull.DOWN
b_button = digitalio.DigitalInOut(board.BUTTON_B)
b_button.direction = digitalio.Direction.INPUT
b_button.pull = digitalio.Pull.DOWN
switch = digitalio.DigitalInOut(board.SLIDE_SWITCH)
switch.direction = digitalio.Direction.INPUT
switch.pull = digitalio.Pull.UP
def switchIsLeft():
return switch.value
pix = neopixel.NeoPixel(board.NEOPIXEL, 10)
pix.brightness = 0.2
pix.fill((255, 0, 255))
pix.show()
innie = usb_midi.ports[0]
outie = usb_midi.ports[1]
if DEV_MODE:
gc.collect()
print("Free bytes after board setup = " + str(gc.mem_free()))
############
# Objects
class EuclidianSequencer(object):
def __init__(self):
self.steps = 9
self.triggers = 2
self.sequence = []
self.active_step = 0
self.clock_count = 0
def update(self):
'''
Generates a "Euclidian rhythm" where triggers are
evenly distributed over a given number of steps.
Takes in a number of triggers and steps,
returns a list of Booleans.
Based on Jeff Holtzkener's Javascript implementation at
https://medium.com/code-music-noise/euclidean-rhythms-391d879494df
'''
slope = self.triggers / self.steps
result = []
previous = None
for i in range(self.steps):
# adding 0.0001 to correct for imprecise math in CircuitPython.
current = math.floor((i * slope) + 0.001)
result.append(current != previous)
previous = current
self.sequence = result
return self # so you can do method chaining.
def addStep(self):
if self.steps < 10:
self.steps += 1
else:
self.steps = 1
self.triggers = 0
self.active_step = 0
self.update()
#def incrementActiveStep(self):
# pass
# #self.active_step = (self.active_step + 1) % self.steps
class SequencerApp(object):
def __init__(self, sequence):
self.seq = sequence
self.a = a_button.value
self.b = b_button.value
self.switchIsLeft = switch.value
self.note_index = 0
self.velocity_index = 4
self.started = False
self.starting_step = 0
def getRed(self, i):
if i == self.seq.active_step:
return 64
else:
return 0
def getGreen(self, i):
if self.seq.sequence[i]:
return 48
else:
return 0
def getBlue(self, i):
# add velocity calculation
return 16
def updateSequenceDisplay(self):
for i in range(_LED_COUNT):
if i < len(self.seq.sequence):
pix[i] = (self.getRed(i), self.getGreen(i), self.getBlue(i))
else:
pix[i] = (0, 0, 0)
def updateConfigDisplay(self):
pix.fill((8, 8, 8))
def updateNeoPixels(self):
if switchIsLeft():
self.updateConfigDisplay()
else:
self.updateSequenceDisplay()
pix.show
def addTrigger(self):
pass
def checkSwitch(self):
# Return value indicates if NeoPixels need to be updated.
if self.switchIsLeft != switch.value:
self.switchIsLeft = switch.value
return True
else:
return False
def checkA(self):
# Return value indicates if NeoPixels need to be updated.
update = False
if self.a != a_button.value:
if a_button.value:
self.seq.addStep()
update = True
self.a = a_button.value
return update
def getByte(self):
raw = innie.read(1)
if raw != b'':
# convert the byte type to a number
return ord(raw)
else:
return None
def get_msg(self):
b = self.getByte()
if b is None:
return None
else:
return b
def incrementClock(self):
self.clock_count +=1
if self.clock_count >= _CLOCK_LIMIT:
self.clock_count = 0
#self.seq.incrementActiveStep()
#self.updateNeoPixels()
def main(self):
while True:
# These method calls change state and return True or False.
# True indicates we need to call updateNeoPixels()
if (self.checkSwitch() or self.checkA()):
self.updateNeoPixels()
msg = self.get_msg()
if msg is not None:
# if there is a message flip the red led
led.value = not(led.value)
#if msg == _CLOCK_MSG:
# pass
#self.incrementClock()
app = SequencerApp(EuclidianSequencer().update())
app.updateNeoPixels()
if DEV_MODE:
gc.collect()
print("Free bytes after object def and creation = " + str(gc.mem_free()))
app.main()Behavior
MemoryError: memory allocation failed, allocating 208 bytes
or, commenting out line 211:
Starting free bytes (after gc) = 12420
Free bytes after imports = 11764
Free bytes after board setup = 11428
Free bytes after object def and creation = 10580
Description
Even taking into account that a CPX is very memory constrained with CircuitPython, it has always felt completely arbitrary when one is going to hit a MemoryError. It feels like the amount of memory reported by gc is irrelevant, as I have gotten in the habit of watching gc.mem_free() very closely. I just went from having over 10,000 bytes free according to gc, to failing to start at all with a MemoryError by adding an if statement that increments a variable.
Obviously this is not actually random, but I can't figure out how to predict/mitigate the issue.
This is probably the same issue as: #10359
I accept that it is an edge case, as everyone is moving on to more current microcontrollers or using C if they are trying to milk performance out of older ones, but I had hopes I'd be able to eek at least slightly more complex CircuitPython applications on the CPX with my students.
Is this maybe a stack/heap configuration kinda thing? I don't think it is a recent regression as CircuitPython has always done the to me but I keep hoping I can figure out how to work with it. I am motivated to try to do some more detailed debugging if a dev can point me in the right direction.
Additional information
No response