Rpi Pico suddenly stops multithreading with WS2812

RP2040 based microcontroller boards running MicroPython.
Target audience: MicroPython users with an RP2040 boards.
This does not include conventional Linux-based Raspberry Pi boards.
Post Reply
mdibit
Posts: 2
Joined: Sun Apr 18, 2021 5:05 pm

Rpi Pico suddenly stops multithreading with WS2812

Post by mdibit » Sun Apr 18, 2021 7:23 pm

Description of the problem :
1. I wrote a program (IDE=Thonny) to drive some WS2812 LEDs, copying the sample program on the MicroPython book. It works.
2. I modified it to create some different figures on the LED. OK
3. I added a communication routine to receive from a Raspberry PI some commands to choose which figure to activate. The corresponding figure is activated. It's OK and it runs for hours (but obviously, during this phase the communication can't go on)
4. Then, in order to manage the LEDs while the communication is going on, I moved this routine to a second thread whose only task is to receive commands ; it then sends the received data to the first thread by means of global area and a flag.
This newly arranged situation works FOR SOMETIME (6-7 new commands) but suddenly stops and there is no way to gain access to the program in Thonny (no ^C etc) environment. You can only power off/on or reset through the RUN pin. I thought there could be interference between comms and threading so I canceled the communication and wrote a simple thread which simulates periodically (3 sec) to have received data from the comms and sends them to the first thread.
Again, after a while, it stops.
To quickly detect the problem I suggest to use the instruction :
DataToRead[1] = ord('3')
which is the command to see a "LED moving around". Usually the Pico stops running after 5-6 "LED back and forth".
If you substitute
DataToRead[1] = ord('1')
which is the command to turn every LED on with a single colour, in this case the system looks to be running for some minutes but anyway it stops, again without giving the possibility to halt it to debug
Thanks everybody
Michele

Programs running (forever) without thread :

Code: Select all

import array, machine, utime
from machine import Pin, UART
import _thread
from rp2 import PIO, StateMachine, asm_pio

# Configure the number of WS2812 LEDs and GPIO driver
NUM_LEDS = 14
GPIO_NUM = 4

BLACK = (0, 0, 0)
RED = (255, 0, 0)
YELLOW = (255, 255, 0)
GREEN = (0, 255, 0)
ORANGE = (255, 127, 0)
BLUE = (0, 0, 255)
PURPLE = (180, 0, 255)
WHITE = (255, 255, 255)
COLORS = (BLACK, RED, ORANGE, YELLOW, GREEN, BLUE, PURPLE, WHITE)

STX=2
ETX=3
NAK=21

global DataToRead
DataToRead = bytearray('0000')            # Area shared between main task and thread
global DataToReadReady
DataToReadReady= 0                        # Flag telling there are data to read in shared area

@asm_pio(sideset_init=PIO.OUT_LOW, out_shiftdir=PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
def ws2812():
    T1 = 2
    T2 = 5
    T3 = 3
    wrap_target()
    label("bitloop")
    out(x, 1)               .side(0)    [T3 - 1]
    jmp(not_x, "do_zero")   .side(1)    [T1 - 1]
    jmp("bitloop")          .side(1)    [T2 - 1]
    label("do_zero")
    nop()                   .side(0)    [T2 - 1]
    wrap()


# Create the StateMachine with the ws2812 program, outputting on pin
sm = StateMachine(0, ws2812, freq=8_000_000, sideset_base=Pin(GPIO_NUM))

# Start the StateMachine, it will wait for data on its FIFO.
sm.active(1)

# Display a pattern on the LEDs via an array of LED RGB values (ogni elemento = INT = 4 bytes)
ar = array.array("I", [0 for _ in range(NUM_LEDS)])

##########################################################################
brightness = 255

def pixels_show():
    """
    Output "ar" array, after dimmering according to brightness
    """
    dimmer_ar = array.array("I", [0 for _ in range(NUM_LEDS)])
    for i,c in enumerate(ar):
        r = int(((c >> 8) & 0xFF) * brightness)
        g = int(((c >> 16) & 0xFF) * brightness)
        b = int((c & 0xFF) * brightness)
        dimmer_ar[i] = (g<<16) + (r<<8) + b
    sm.put(dimmer_ar, 8)
    utime.sleep_ms(10)

def set_a_pixel(i, color):
    """
    set a pixel (LED) after the required color
    """
    ar[i] = (color[1]<<16) + (color[0]<<8) + color[2]

def all_switched_on(color):
    """
    every pixel with the same colour
    """
    for i in range(len(ar)):
        set_a_pixel(i, color)
    pixels_show()

def roll_1_LED(color, wait):
    # roll/shift a LED back and forth (choice of colour)
    for ii in range(1, NUM_LEDS):
        set_a_pixel(ii, color)
        utime.sleep(wait)
        set_a_pixel(ii-1, BLACK)
        pixels_show()
    for ii in range(NUM_LEDS-2, -1,-1):
        set_a_pixel(ii, color)
        utime.sleep(wait)
        set_a_pixel(ii+1, BLACK)
        pixels_show()
    set_a_pixel(0,BLACK)
    pixels_show()

#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Second thread section (Here not used as a thread):
# This is a simulation of the original program which is communicating on a serial line to a master device.
# Receives information to turn on the WS2812 LEDs in different ways.
# The received data are tranferred to the first thread through a global area
# IN THE SIMULATION, every 4 seconds, it generates a DataToRead buffer to send a command tothe first thread
# the data are arranged in a 4 bytes array (ASCII) :
# byte 0 = notused
# byte 1 = command ( eg. '1'= switch LEDs on with same color, '3' = shift (roll) an LED )
#            (other commands are not useful to understand the problem)
# byte 2 = color
# byte 3 = brightness
# After tranferring the command to the first thread, the second one sleeps for 3 second, before generating a new command,
# so that they work alternating.
# N.B. This simulation shows that the problem doesn't come from the serial communication.
# Maybe the problem comes from the co-existence of a second thread together with the State Machine
# If the program is run without the thread, i.e by calling the GestSeriale_thread() from the main program,
# the program runs witjout stopping. See Prova_NO_Thread

colorethread = 0
def GestSeriale_thread():
    global DataToRead, DataToReadReady, colorethread

    colorethread = (colorethread % 7) + 1   #cycle on 8 colors, from red to black 
    DataToRead[0] = ord('A')
    DataToRead[1] = ord('3')
    DataToRead[2] = colorethread + 0x30
    DataToRead[3] = ord('2')
    DataToReadReady +=1               # a new set of data ready in DataToRead
    utime.sleep(1)
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


# Startup = switch off every LED
color= 0
brightness= 0
all_switched_on(COLORS[color])
pixels_show()
#_thread.start_new_thread(GestSeriale_thread, ())

# First thread / MAIN program = permanently waiting for a command from the second thread, who received it through the serial line 0
# the true original program

while (True):
    GestSeriale_thread()
    if (DataToReadReady >0):
        DataToReadReady -=1
        print(DataToRead)
        color= DataToRead[2] & 0xF             # quindi imposta colore e luminosita
        brightness= (DataToRead[3] & 0xF)/10
        if (DataToRead[1]==ord('1')):          # e poi scegli il programma
            all_switched_on(COLORS[color])
        elif (DataToRead[1]==ord('2')):
            arcobaleno()
        elif (DataToRead[1]==ord('3')):
            roll_1_LED(COLORS[color], 0.05)
        elif (DataToRead[1]==ord('4')):
            sfuma_arcobaleno()
        elif (DataToRead[1]==ord('5')):
            scorri_1_colore(COLORS[color], 0.05)
    #utime.sleep_ms(200)
Programs running (and then STOPPING) with thread :

Code: Select all

import array, machine, utime
from machine import Pin, UART
import _thread
from rp2 import PIO, StateMachine, asm_pio

# Configure the number of WS2812 LEDs and GPIO driver
NUM_LEDS = 14
GPIO_NUM = 4

BLACK = (0, 0, 0)
RED = (255, 0, 0)
YELLOW = (255, 255, 0)
GREEN = (0, 255, 0)
ORANGE = (255, 127, 0)
BLUE = (0, 0, 255)
PURPLE = (180, 0, 255)
WHITE = (255, 255, 255)
COLORS = (BLACK, RED, ORANGE, YELLOW, GREEN, BLUE, PURPLE, WHITE)

STX=2
ETX=3
NAK=21

global DataToRead
DataToRead = bytearray('0000')            # Area shared between main task and thread
global DataToReadReady
DataToReadReady= 0                        # Flag telling there are data to read in shared area

@asm_pio(sideset_init=PIO.OUT_LOW, out_shiftdir=PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
def ws2812():
    T1 = 2
    T2 = 5
    T3 = 3
    wrap_target()
    label("bitloop")
    out(x, 1)               .side(0)    [T3 - 1]
    jmp(not_x, "do_zero")   .side(1)    [T1 - 1]
    jmp("bitloop")          .side(1)    [T2 - 1]
    label("do_zero")
    nop()                   .side(0)    [T2 - 1]
    wrap()


# Create the StateMachine with the ws2812 program, outputting on pin
sm = StateMachine(0, ws2812, freq=8_000_000, sideset_base=Pin(GPIO_NUM))

# Start the StateMachine, it will wait for data on its FIFO.
sm.active(1)

# Display a pattern on the LEDs via an array of LED RGB values (ogni elemento = INT = 4 bytes)
ar = array.array("I", [0 for _ in range(NUM_LEDS)])

##########################################################################
brightness = 255

def pixels_show():
    """
    Output "ar" array, after dimmering according to brightness
    """
    dimmer_ar = array.array("I", [0 for _ in range(NUM_LEDS)])
    for i,c in enumerate(ar):
        r = int(((c >> 8) & 0xFF) * brightness)
        g = int(((c >> 16) & 0xFF) * brightness)
        b = int((c & 0xFF) * brightness)
        dimmer_ar[i] = (g<<16) + (r<<8) + b
    sm.put(dimmer_ar, 8)
    utime.sleep_ms(10)

def set_a_pixel(i, color):
    """
    set a pixel (LED) after the required color
    """
    ar[i] = (color[1]<<16) + (color[0]<<8) + color[2]

def all_switched_on(color):
    """
    every pixel with the same colour
    """
    for i in range(len(ar)):
        set_a_pixel(i, color)
    pixels_show()

def roll_1_LED(color, wait):
    # roll/shift a LED back and forth (choice of colour)
    for ii in range(1, NUM_LEDS):
        set_a_pixel(ii, color)
        utime.sleep(wait)
        set_a_pixel(ii-1, BLACK)
        pixels_show()
    for ii in range(NUM_LEDS-2, -1,-1):
        set_a_pixel(ii, color)
        utime.sleep(wait)
        set_a_pixel(ii+1, BLACK)
        pixels_show()
    set_a_pixel(0,BLACK)
    pixels_show()

#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Second thread section :
# This is a simulation of the original program which is communicating on a serial line to a master device.
# Receives information to turn on the WS2812 LEDs in different ways.
# The received data are tranferred to the first thread through a global area
# IN THE SIMULATION, every 4 seconds, it generates a DataToRead buffer to send a command tothe first thread
# the data are arranged in a 4 bytes array (ASCII) :
# byte 0 = notused
# byte 1 = command ( eg. '1'= switch LEDs on with same color, '3' = shift (roll) an LED )
#            (other commands are not useful to understand the problem)
# byte 2 = color
# byte 3 = brightness
# After tranferring the command to the first thread, the second one sleeps for 4 second, before generating a new command,
# so that they work alternating.
# N.B. This simulation shows that the problem doesn't come from the serial communication.
# Maybe the problem comes from the co-existence of a second thread together with the State Machine
# If the program is run without the thread, i.e by calling the GestSeriale_thread() from the main program,
# the program runs witjout stopping. See Prova_NO_Thread

colorethread = 0
def GestSeriale_thread():
    global DataToRead, DataToReadReady, colorethread
    while True:
        colorethread = (colorethread % 7) + 1    #cycle on 8 colors, from red to black 
        DataToRead[0] = ord('A')
        DataToRead[1] = ord('3')
        DataToRead[2] = colorethread + 0x30
        DataToRead[3] = ord('2')
        DataToReadReady +=1               # a new set of data ready in DataToRead
        utime.sleep(3)
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


# Startup = switch off every LED
color= 0
brightness= 0
all_switched_on(COLORS[color])
pixels_show()
_thread.start_new_thread(GestSeriale_thread, ())

# First thread / MAIN program = permanently waiting for a command from the second thread, who received it through the serial line 0
# the true original program

while (True):
    if (DataToReadReady >0):
        DataToReadReady -=1
        print(DataToRead)
        color= DataToRead[2] & 0xF             # quindi imposta colore e luminosita
        brightness= (DataToRead[3] & 0xF)/10
        if (DataToRead[1]==ord('1')):          # e poi scegli il programma
            all_switched_on(COLORS[color])
        elif (DataToRead[1]==ord('2')):
            arcobaleno()
        elif (DataToRead[1]==ord('3')):
            roll_1_LED(COLORS[color], 0.05)
        elif (DataToRead[1]==ord('4')):
            sfuma_arcobaleno()
        elif (DataToRead[1]==ord('5')):
            scorri_1_colore(COLORS[color], 0.05)
    utime.sleep_ms(200)

User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: Rpi Pico suddenly stops multithreading with WS2812

Post by Roberthh » Sun Apr 18, 2021 8:02 pm

There is a problem with Pico, dual thread mode and memory management. The code may either stop or behave otherwise weird. Nothing related to the actual code itself. At the moment, the only choice you have is to use single thread only or to rewrite the program to allocate all memory at the beginning and avoid re-allocation. I hope this will be fixed soon.

A second problem seem to exist with dual thread and interrupts, if the interrupt handler is at the second thread.

mdibit
Posts: 2
Joined: Sun Apr 18, 2021 5:05 pm

Re: Rpi Pico suddenly stops multithreading with WS2812

Post by mdibit » Fri Apr 23, 2021 4:21 pm

Thanks! Now I know that I followed the right path by re-writing the program in a single thread. Just to let you know, since the main program is continuously writing the LEDs and waiting some msecs before setting a new config, I wrote an alias for the "sleep" function in order to manage the serial line while spending time and then preparing the recv buffer as if received by the second thread.
I works perfectly

Post Reply