PSIF in FreeRTOS
In the previous post, I introduced a PSIF object that, using a receive and a send method, could do the basic functionality of a network interface over a line on one Arduino pin. While this is ok for demonstration purposes, the proposed scheme had a major drawback: it depends on us polling the line frequently to not miss a bit. Any processing we do is subject to running within the duration of a bit, since there is only one single thread.
How do we solve this? The answer is multitasking. We need to make sure that, whatever we do, the PSIF has its opportunity to sample the line (as long as it is not either sending or receiving). If in the future we want a PSiN device to do complex tasks, such as running actual applications, driving screens or actuators, using encryption, etc., we need to consider this. Moreover, programming complex systems will force us to use something like FreeRTOS, where design can be done in modules.
Design
Before doing any coding, let's design how the PSIF will behave. We already made an object that has send and receive methods. But for them to work, we did blocking calls. That means that when we call the function, the microcontroller cannot do anything else, and after some time doing the task, the function returns a result. Well, we will have to modify that. Instead of public functions, we will use FreeRTOS queues. So if a task wants to send something, instead of calling send, it will put the message in the txQueue. Similarily, if a task wants to receive something, it will wait on an rxQueue, where the PSIF will write the packets as they arrive. When I say "the PSIF will write..." I am implying that the PSIF now isn't just an object with passive methods; it now actively runs as a singular process, or task, as it is called in FreeRTOS. So, the new PSIF works as follows:
- We create a PSIF object with the pin and bitrate just like before.
- In the
setup(), where we start everything, we start the PSIF task. - The PSIF task will initialize the TX and RX queues.
- The PSIF task will then do what loop() did in my initial proof-of-concept; only instead of probing for a button, it will probe the TX queue; and instead of showing the message in serial output, it will push it to the RX queue.
Code
The code is available at the repository. In this case, the directory to watch is point_to_pont_freertos.
I'll begin with the main ino file (point_to_pont_freertos.ino). In the code, there are now two new elements: receiverTask and senderTask. Each defines a FreeRTOS task that does what the name implies. The receiverTask polls the RX queue with the receiveAsync method of the PSIF object (which abstracts the queue and I will describe later). Passing a timeout of portMAX_DELAY implies that the task blocks until something arrives from the interface. Meanwhile, other tasks are executed. When something does eventually arrive, the task shows it over serial (like in the proof-of-concept) and goes back to waiting on the queue. Regarding the senderTask, it continuously polls the button and if it finds it pressed, it calls the sendAsync function of PSIF. So basically, the code has taken what was in loop() in the original proof-of-concept, separated receive and transmit, and put them in two loops in independent tasks. But these receive and transmit actions do not actually receive and transmit, they ask the PSIF driver to do it.
Let's go no to the PSIF.h and PSIF.cpp files, what from now on will be called the driver of the PSIF. We have three new important elements:
- The
driverTaskFreeRTOS task: again replicates the behavior ofloop()in the proof-of-concept, but now it receives the order of transmission from the TX queue, and writes whatever was received in the RX queue. Just likeloop(), each iteration starts with asking if there is anything to transmit, transmitting it if necessary, and then sampling the channel for 1/10 th of the bit time, and then proceeding to receive if a signal is detected. - The
receiveAsyncmethod: abstracts the waiting on queue, just for simplifying the API from the outside. It either times out or if something is received, copies it to theoutparameter. - The
sendAsyncmethod: adds a packet to the send queue. Again, an abstraction to make the code look better.
Additionally there are some minor changes, such as the receive and send functions being improved with semaphores to avoid sending and receiving at the same time, or the new Frame class that describes the frame structure of PSiN. This class will be extended in the future once a better LINK layer is defined. There is also a new helper function, called beginDriver, which initializes everything: the queues, the semaphore and the driverTask. Again, this helps simplify the code: we just need to run psif0.beginDriver()in setup() in the main ino file, to have everything running, no need to mess with tasks, queues, etc.
Test
One important aspect of this new design is that it no longer fits in a humble Arduino UNO; the heap sizes . As far as I have tried, it neither does in an Arduino MEGA. So I had to move to the more capable (albeit similarly priced) ESP32. I plan on running it on an STM32 at some point too, to test.
I took two ESP32 devices I had lying around, an M5Stack Core and a Freenove ESP32 S3 WROOM board, and connected them as follows:

The M5Stack Core (left) will act as transmitter, and I have set BUTTON_PIN=39, to use the first button to transmit. Both microcontrollers use pin 5 for the PSIF. The Freenove ESP32 will act as receiver, with its UART port connected to the PC with the Arduino IDE serial monitor on. I have also thrown in a diode + resistor between the line and GND, and a pulldown resistor to keep the line at LOW when idle. Next, I press the button and... :

You can see some debug text, which didn't make it in the uploaded version; and in the bottom, the received packet. Success!
What next?
We now have a working PSIF driver, appropriately isolated from other processes and easy enough to run. In the way we had to ditch the Arduino UNO because of its low memory. A pity since it is quite educational-friendly. But anyway ESP32s don't break the bank.
Now that we have this, we can move to more abstract processes. In telco, "abstract" means the next OSI layer: LINK, where I will introduce functions such as addressing, MAC, flow and error control, etc.
In the process, there will likely be one big redesign of the PHY layer: switching to open-drain output in the PSIF pins and defaulting the line to HIGH. This way, two devices will be able to write in the medium at the same time. Of course in real networks this is not desirable, but PSiN was not built to be efficient, but educational! This way, we can emulate collisions in the line, and then build the algorithms to avoid or compensate them.