Cornell University ECE4760
ProtoThreads

Pi Pico RP2040

Protothreads on RP2040
Protothreads is a very light-weight, stackless, threading library written entirely as C macros by Adam Dunkels.
Adam Dunkels' documentation is very good and easy to understand. Summary:

You should read sections 1.3-1.6 of the reference manual (local copy) to see all of the implementation details.
We added:

Version 1.1.2: Pi Pico version fixes long duration timer bug
Use pt_cornell_rp2040_v1_1_2.h
Demo code, protothreads header, whole project is here: ZIP file
See the tables below for the Protothreads Function Summary.
See further below also for a short explaination of the example code structure.
Many examples in the class pages still use version 1.1.1.
Adding the 1.1.2 version and changing the one-line #include should update, but note that
PT_GET_TIME_usec() now returns a long-long int. (uint64_t)


Version 1.1.1: Works but has a timer bug!!!
This version has a timer overflow which sometimes halts a thread after 75 minutes

Do NOT use pt_cornell_rp2040_v1_1_1.h
Demo code, make files, protothreads header all are here: ZIP file
See the tables below for the Protothreads Function Summary.
See further below also for a short explaination of the example code structure.


Protothreads Function Summary
The following table give the syntax for the thread macros and functions written by Adam Dunkels.

Protothreads Functions (from Adam Dunkels) Description
PT_INIT(pt)

Initialize a protothread. This macro inits a protothread, but do not use it directly!. The pt_add() macro below will do this for you!
Note that you will proably never use this macro.

PT_THREAD(name_args)
Declaration of a protothread. name_args The name and arguments of the C function implementing the protothread.
PT_WAIT_THREAD(pt, thread)
Wait until a child protothread completes.
pt
A pointer to the protothread control structure.
thread
The child protothread with arguments
PT_WAIT_UNTIL(pt, condition)
PT_WAIT_WHILE(pt, condition)
Wait the thread and wait until/while condition is true. Conditions which may be set on either core are not safe.
PT_BEGIN(pt)
Declare the start of a protothread inside the C function implementing the protothread.
pt
A pointer to the protothread control structure.
PT_END(pt)
Declare the end of a protothread.
pt
A pointer to the protothread control structure.
PT_EXIT(pt)
This macro causes the protothread to exit. If the protothread was spawned by another protothread, the parent protothread will become unblocked and can continue to run.
pt A pointer to the protothread control structure.
PT_RESTART(pt)
This macro will block and cause the running protothread to restart its execution at the place of the PT_BEGIN()call. pt A pointer to the protothread control structure.
PT_SCHEDULE(f)
This macro shedules a protothread, but do not use it directly!. The scheduler will do this for you! The return value of the function is non-zero if the protothread is running or zero if the protothread has exited.
f The call to the C function implementing the protothread to be scheduled.
Note that you will proably never use this macro.
PT_SPAWN(pt, child, thread)
Spawn a child protothread (example) and block the thread until it exits. Parameters:
pt A pointer to the current protothread control structure.
child A pointer to the child protothread’s control structure.
thread The child protothread with arguments
PT_YIELD(pt)
yield the protothread, thereby allowing other processing to take place in the system.
PT_YIELD_UNTIL(pt, cond)
Yield from the protothread until a condition occurs. Conditions which may be set on either core are not safe.
PT_SEM_INIT(s, c)
This macro initializes a semaphore with a value for the counter. Internally, the semaphores use an "unsigned int" to represent the counter, and therefore the "count" argument should be within range of an unsigned int. s A pointer to the pt_sem struct representing the semaphore. Only use these semaphores for signals between threads running on one core!
PT_SEM_SIGNAL(pt, s)
This macro carries out the "signal" operation on the semaphore. The signal operation increments the counter inside the semaphore, which eventually will cause waiting protothreads to continue executing. pt A pointer to the protothread (struct pt) in which the operation is executed. s A pointer to the pt_sem struct representing the semaphore. Only use these semaphores for signals between threads running on one core!
PT_SEM_WAIT(pt, s)
This macro carries out the "wait" operation on the semaphore. The wait operation causes the protothread to yield while the counter is zero. When the counter reaches a value larger than zero, the protothread will continue. Only use these semaphores for signals between threads running on one core!

The following table has the protothread macro extensions and functions
we wrote for the RP2040 and which are included in the Cornell header file.

Added Protothreads functions

Description

 

PT_YIELD_usec(delay_time) Causes the current thread to yield (stop executing) for the delay_time in microseconds. The time is derived from the core timer. Note that this is a non-blocking delay, not an interval timer. Overflows in about 300000 years for 64 bit timer read. (as of version 1.1.2.)
PT_INTERVAL_INIT()
PT_YIELD_INTERVAL(interval_time)

The yield interval macro attempts to make periodic execution of the thread. The interval_time is in microseconds. The time is derived from the core timer. Note that this is a non-blocking interval timer. Overflows in about 300000 years for 64 bit timer read (as of version 1.1.2.). The INIT macro must be called in the scheduled thread before the while(1) loop.
PT_GET_TIME_usec() Returns the current microsecond count on the system tick clock. This macro returns a uint_64 timer count. (as of version 1.1.2.)
PT_SEM_SAFE_INIT(s,c)
This macro initializes a semaphore with a value for the counter. Internally, the semaphores use an "unsigned int" to represent the counter, and therefore the "count" argument should be within range of an unsigned int. s is a pointer to the pt_sem struct representing the semaphore. Use these semaphores for signals between threads running on either core! This macro also grabs lock 25 for use by all defined semaphores.
PT_SEM_SAFE_WAIT(pt,s)
This macro carries out the "wait" operation on the semaphore. The wait operation causes the protothread to yield while the counter is zero. When the counter reaches a value larger than zero, the protothread will continue. pt A pointer to the protothread (struct pt) in which the operation is executed. s A pointer to the pt_sem struct representing the semaphore. Use these semaphores for signals between threads running on either core! Never wait on a semaphore in an ISR.
PT_SEM_SAFE_SIGNAL(pt,s)
This macro carries out the "signal" operation on the semaphore. The signal operation increments the counter inside the semaphore, which eventually will cause waiting protothreads to continue executing. pt A pointer to the protothread (struct pt) in which the operation is executed. s A pointer to the pt_sem struct representing the semaphore. Use these semaphores for signals between threads running on either core! These semaphores are core-safe, but should not be signaled in an ISR.
PT_LOCK_INIT(s,lock_num,lock_state)
Initializes a hardware spin_lock state to either LOCKED or UNLOCKED. s is a variable of type spin_lock_t *. lock_num must be between 26 and 31 and be unique. This macro also grabs lock 24 for use by all defined locks. These locks are global between cores.
PT_LOCK_WAIT(pt,lock_num)
Yields the thread until loc_num is UNLOCKED by another thread on either core, then LOCKs it. Never wait on a lock in an ISR. These locks are global between cores.
PT_LOCK_RELEASE(lock_num)
UNLOCKs lock_num
PT_FIFO_WRITE(data)
Sends one 32-bit word to the other core, and blocks until there is space in the FIFO.
PT_FIFO_READ(fifo_out)
Receives one 32-bit word from the other core, and blocks until there is data in the FIFO.
PT_FIFO_FLUSH
Drain outgoing FIFO for the current core.
Higher level control macros  
pt_add(function_name) ; Add a thread to the current core scheduler queue.
A thread is added on the core on which it runs.
See example code!
pt_schedule_start ;
Starts the scheduler for the current core. A separate scheduler runs on each core. This macro never exits!
serial_read ;
A thread which is spawned to get nonblocking string input from UART0. This function assumes that a human is typing at a terminal and thus supports backspace, termination by enter key and echos characters! String is returned in
pt_serial_in_buffer. If more than one thread can spawn this thread, then there must be semaphore protection. Control returns to the scheduler between every character received. The thread exits to the parent when it receives an <enter>.
serial_write ;
A thread which is spawned to send a string output from UART0. String to be sent in pt_serial_out_buffer.
If more than one thread can spawn this thread, then there must be semaphore protection. Control returns to the scheduler between every character sent. The thread exits to the parent after it sends the entire string (zero terminated).

The example code defines some protothreads structures, then starts six threads, three on each core.
Note that the two schedulers are completely independent. Communication between threads on
different cores must be by (protected) global variables, FIFO, safe semaphores, and/or locks. Semaphores,
locks, and FIFO init operations should be done before threads are started.
Spin locks and the FIFOs are part of the RP2040 SIO single-cycle core communication subsystem.

Core 0 main:

  1. inits the serial interface and prints a message.
    Assumes a serial connection on gpio 0 (transmit) an gpio 1 (receive).
    If you delete this, then Protothreads will still run but without any serial communication.
  2. inits locks and semaphores
  3. flushes the core 0 to core 1 FIFO
  4. launches core 1 main
  5. adds 3 threads to the core 0 scheduler queue
  6. starts the core 0 scheduler

Core 1 main (launched by core 0):

  1. flushes the core 1 to core 0 FIFO
  2. adds 3 threads to the core 1 scheduler queue
  3. starts the core 1 scheduler

The threads:

  1. Three threads on core0:
    1. Serial i/o test . Ths thread reads two numbers, adds, print the sum. It also uses the FIFO to send numbers to core 1 (which sums them) then prints result. It also also prints error count for semaphone and lock. Protothreads assumes uart0 on gpio 0 and 1. It supports <enter> and <backspace>keys.
    2. The usual blinky of the on-board LED and prints if there are lock alternation errors.
    3. One of Two threads which execute in alternation using two safe semaphores. This code is testing core-safe semaphore (other thread is on core 1). This thread increments a lock-protected counter.
  2. Three threads on core1
    1. Toggle an gpio pin with a small random timing variation.
    2. One of Two threads which execute in alternation using two safe semaphores ---- note that this code is testing core-safe semaphore (other is on core 0). This thread decrements a lock-protected counter. he timing of this thread is randomized to test for dead-lock, or failure to alternate.
    3. wait for core 0 to send two numbers, adds them , sends back the sum using the FIFO.

The two alternating threads increment and decrement a counter, so that if they truely alternate the counter should be either 0 or 1. The blinky thread on core 0 watches the count and prints a messaage if there is an error. The code was tested for over 500 million thread swaps with no errors. The syntax for using the safe semaphores is the same as the default semaphores, except that the tag SAFE is added into the macro definition The LOCK macros use spinlock 24 to insure atomic access (plus the specific spin-LOCK that you specify) The SEM macros use spinlock 25 to insure atomic access Do not use ANY spin-lock below number 26!

With just two alernating threads (one on each core), thread rate is about 440 thousand/sec with safe semaphores and locked counter.

Debugging ProtoThreads

If your program just sits there and does nothing:
-- Did you write each thread to include at least one YIELD (or YIELD_UNTIL, or YIELD_TIME_msec) in the while(1) loop? Remember that the UART input spawned
thread yields betwen eacy typed character. Humans are sooo slow.
-- Did you schedule the threads?
-- Does the board have correct Vdd?

If your program reboots about a few times/sec:
-- Did you exit from any scheduled thread? This blows out the stack.
-- Did you turn on any interrupt source and fail to write an ISR for that source?
-- Did you divide by zero? A divide by zero is an untrapped exception.
-- Did you write to a non-existant memory location, perhaps by using a negative (or very large) array index.
A write to a non-existant memory location is an untrapped exception.

If your thread state variables seem to change on their own:
--- Did you define any automatic local variable?
Local variables in a thread should be static.
-- Are your global arrays big enough the clobber the stack in high memory?
You should be able to use over 200 kbytes.


Version 1.1: Works with the Pi Pico. (depricated)
Use pt_cornell_rp2040_v1.1.h
Demo code, make files, protothreads header all are here: ZIP file
See the tables below for the Protothreads Function Summary.
See below also for a short explaination of the example code structure.

 


Copyright Cornell University October 7, 2023