Firmware for the RPi Pico for Oscilloscope Renderer
The Firmware handles data and pushes it to the DACs resulting in a signal for the oscilloscope to be display on.
The RPi Pico is connected via USB to a host machine which provide the image. The Pico can run in two modes: Raw (Signal Data is generated on the host and just passed through) amd Instruction (The Pico recv instructions to generated the signal on the fly and the outputs it)
I'm using an RP2040 on a Raspberry Pico. Code is written in C/C++ with the PicoSDKwith FreeRTOS. One of the rp2040's PIO cores is used in combination with a DMA-Channel to archive higher data rate.
Documentation is provided via doxygen and can be found here
This Project is developed with VSCode
The VSCode project comes pre-configured is self contained (FreeRTOS, PicoSDK). But the appropriate tools like gcc, gdb and openocd need to be installed. You could also use docker to build the project
For this you only need docker to be installed
Building the container
docker build -t rpi_pico_build ./firmware/docker
Running the docs: firmware
docker run -v "./":/data --rm rpi_pico_build "/data/firmware/docker/build_docs.sh"
Running the build: firmware
docker run -v "./":/data --rm rpi_pico_build "/data/firmware/docker/build_firmware.sh"
sudo apt install build-essential cmake gcc-arm-none-eabi gdb-multiarch
(install command for ubuntu or debian based distributions)
(I once had an issue with debug in gdb-multiarch, which seems to gone now. But if you have any problems it helps building gdb-multiarch from sources. I'm using v12.2)
You also need to install openocd
which is done by compiling from sources:
git clone https://github.com/raspberrypi/openocd.git
cd openocd
./bootstrap
./configure --enable-picoprobe
make -j4
sudo make install
For Syntax-highlight and debugging you need to load the rpiPico-code-profile
.
Debugging and Programming is done via a SWD, I used a Raspberry Pi Debug Probe to do so.
But a Pico Probe
or any other SWD-Debugger should work to. You just need to edit the firmware/scripts/launch_openocd.sh
to fit your debugger.
Building is done with the CMake: build main
VSCode-task
Flashing can be USB: Just press the BOOTSEL Button on the Pico and connect the RPi-Pico via USB. It should now show up as a USB-drive. You can now move the .uf2 file form the firmware/build/bin folder. The Pico should now restart and run the uploaded binary.
OR
Flashing can be one via SWD (recommended). You need to connect your Debugger to debug pins of the Pico (see pinout). Then you need to start openocd via the Pico: launch openocd
VSCode-Task. The Pico can now be flashed with the Pico: flash main
-Task (This task automatically builds
everything before flashing)
The Data/Buffer Flow is concert how buffer move with the RP2040, without the need of dynamic allocation. The Flow is based FreeRTOS's Queues.
There are two type of buffers:
frameBuffer_t
: These hold samples that are sent to DACsinstructionBuffer_t
: These hold instruction that are used to generate a signal
All buffers are preallocated and are only passed around by pointer, because they are to large (around 8kB in case of the frame_buffer) to be copied. frameBuffer_t
/instructionBuffer_t
struct only holds information of about the size and a pointer to the frame/instruction buffer.
The Systems main buffer-cycles:
The IO-handler (e.g USB or UART handler) requests a instructionBuffer_t
via dac_acquireInstructionBufferPointer()
from the g_unusedInstructionBufferQueue
and submits the fill instructionBuffer_t
via dac_submitInstructions()
to the g_instructionBufferQueue
.
The gen_processingTaskFunction()
takes one instructionBuffer_t
r (if available) and generates the output signal with this. The now used instructionBuffer_t
is submitted to g_unusedInstructionBufferQueue
for the IO-handler to be picket up and filled again
The Structure/Format of a instruction is documented here
The gen_processingTaskFunction()
(aka the Signal Generator) request one unused frameBuffer_t
from the g_unusedFrameBufferQueue
. The Signal Generator now fills this frameBuffer_t
with the signal generated by the instruction it got. This fill buffer is then submitted to the g_frameBufferQueue
.
The isrDma()
Interrupt services routine is trigger on completion of the transfer of data to the PIO sm. On Trigger, the next_buf is submitted to the DMA, an stored as the current frameBuffer_t
. The now old frameBuffer_t
is then pushed to the g_unusedFrameBufferQueue
, which is then refilled by the gen_processingTaskFunction()
again.
Then the isrDma()
pulls one frameBuffer_t
from the g_frameBufferQueue
.
This is not done on every cycle but on every FRAME_REPEAT
nth cycle (can be configured in the dacConfig.h). There for every frameBuffer_t
is repeated FRAME_REPEAT
-times. This is done, to have a less flickery-image on the output-oscilloscope.
In Case of an empty g_frameBufferQueue
the current frame is repeated (after the FRAME_REPEAT
) until one new frame becomes available.