Skip to content

MemoryErrors feel unpredictable and arbitrary on Circuit Playground / SAMD21Β #10693

@tom-hoffman

Description

@tom-hoffman

CircuitPython version and board name

Adafruit CircuitPython 10.0.3 on 2025-10-17; Adafruit CircuitPlayground Express with samd21g18

Code/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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions