Thursday, January 22, 2026

Raspberry Pico Performance Comparison Using MAX7219: C vs Micropython


Hi there. In this article, I discussed a few experiments I ran with the Raspberry Pi Pico microcontroller and some interesting results I obtained. It became relatively long, so I covered the results in the middle and left, how I ran the code, as additional reading at the end.


What is Raspberry Pi Pico?

Raspberry Pi Pico (Pico for short) is a very powerful alternative microcontroller from the Raspberry Pi Foundation, despite not being a direct response to Arduino. Arduino is targeting hobbyists, still. In my opinion, Pico requires a bit more technical knowledge. In this article, I demonstrate different programming approaches on Pico and compare their speed. I have some code ready from the previous article: SPI and driving LED display over MAX7219. I continue with this topic in this article. I had previously implemented this just on Arduino, now I'm doing it on Pico.

Arduino has an 8-bit ATmega328P (16 MHz) microcontroller. Pico, in turn has a 32-bit dual core ARM Cortex microcontroller with maximum clock speed of 133 MHz, documented. It boots up at its default speed of 125 MHz. Its clock speed can be changed during runtime programmatically. For example, in battery-operated devices, the clock speed can be slowed down to use it sparingly. I don't know if it has been officially documented later, but I've read that Pico can be operate at 200 MHz in the wild. Since I don't need to parallelize my code, I don't care about the dual core for this case. The Pico also has more RAM and flash memory compared to Arduino, but since my application is CPU intensive, these also don't play a significant role for now.

There is an open e-book titled "Get Started with MicroPython on Raspberry Pi Pico" for newcomers to Pico. The last time I checked, the link on this page was dead, but when you search for the book's title on Google, it can be easily found as .pdf file.

There are alternative methods for developing code for Pico. The first is Arduino IDE, and the nice thing about it is that a C code that works well on Arduino, will also work the same way on Pico very very likely. Statements like pinMode() or digitalWrite() are fully compatible. If your code doesn't have any deeply Arduino-specific parts, it will also work on Pico.

It's also possible to program Pico with (micro)python as a second alternative. For example, following line configures the thirteenth GPIO pin as an output pin and sets it to logical one:

spi_CS = machine.Pin(13, machine.Pin.OUT, 1);

To communicate over SPI in Micropython, simply create an SPI object with the pins to be used for SPI protocol and call the write() method of the object. Micropython is simple, but slow. There is a 660 KB Micropython interpreter for Pico. Run Pico in boot select mode and copy this file (In boot select mode, Pico appears like a USB flash disk on the computer). It's that easy. Pico communicates with the computer over USB using serial protocol (RS232). This way, the microcontroller runs python.

Third alternative is C/C++. Pico has extensive C support and libraries. Large number of platform-specific functions (get_absolute_time(), cyw43_arch_gpio_put(), stdio_init_all() etc.) was quite confusing for me at first. However, there are countless examples on Raspberry's official GitHub account for every topic. Another good resource for C is the book Getting started with Raspberry Pi Pico-series. Pico SDK manual is also quite detailed, but it's not a book you'd want to read - it's 745 pages long. I used the video below when compiling the SDK and the examples.


Assembly is certainly another option, but I have never written Assembly for either Arduino or Pico. Calling inline assembly from within C code is also possible.


How Did I Test?

All tests are based on the counter code without library, which I developed for the previous article. Of course, I wrote Micropython code from scratch, but strictly based on the same algorithm. When this code is executed from the Arduino IDE, it works flawlessly and identically both on Arduino and on Pico. Yet interestingly, other version of this code, which uses a library, doesn't run on Pico. The IDE must be searching for header files in different directories for different platforms. I didn't care about it, as I already have one sketch which runs well on both platforms.

My initial plan was to set and reset an IO pin with each overflow occurring in the 8th bit of first element (line 67) of the a array and find the period of overflows with a frequency meter. But this plan failed right from the start because my frequency meter wasn't working properly. I connected the pin to my oscilloscope, but in this case, it missed some pulses due to its sampling rate. When I added a small delay between the set and reset, I was able to read measurements without any problem. In the image above, there is a 10 ms delay between rising and falling edges of each pulse (100 ms / div, on the top right). The pulse frequency here is 2.182 Hz. When converted to period, it is 458 ms. Since 10 ms of it is the delay, the counter overflows every 448 ms.

According to my observation, the tenth LED remained lit for approx. one second and I made all my calculations based on this, in the previous article. Now, if the period of the eighth LED is 448 ms, then the period of the ninth LED is 892 ms, so the tenth LED remains on for 892 ms. This is not bad for a rough estimate, based on just observation.

On the other hand, it doesn't make any sense actually, to add some delay when performing a time-based measurement. Therefore, I printed the time passing between two consecutive overflows on the second element of the a array from the serial port. This is 114.486 seconds on average. If the first array element overflows 256 times at 892 ms intervals, it yields 114.176 seconds. There is a 310 ms difference between the calculation and the experiment. This could be due to the temperature difference in the room between the days I took the measurements, because Arduino's oscillator isn't very precise enough, or it could be the overhead of serial communication, which is more likely. Arduino cannot do Serial.println() asynchronously, and the baud rate is also slow. Still, 2.7‰ is an acceptable error. Therefore, I decided to read the timings from serial port.

I removed #ifdef from the eighth line to initialize serial port unconditionally. New code is available on GitHub. I used millis() function for time measurement (line 69). Unlike the old version, there is an if block on the eightieth line. Here, the value of StartTime1 is subtracted from the current tick count, and the elapsed time is written to the serial port.

At this point, I discovered something very interesting happening with Arduino IDE. For some reason, the loop duration changes depending on the parity of the number written to the LED matrix. Moreover, the duration changes positively in Arduino and negatively in Pico. The graph below show the duration values in Arduino:

For example, the peak in the middle appears at 127, then the value suddenly drops down at 128. There is another drop between 63 and 64, on the middle way from 127 to the zero point, and very similar one between 191 and 192. The biggest drop is between 255 and 256, on the right side. On the graph, it can be clearly seen that other smaller fractures always have 2n pattern. No idea why. Let's look at the Pico's graph:

This graph seems like to be the mirror image of the previous one, with the duration decreasing as the number of ones increases. Similar fractures to the first graph can be seen in the opposite direction around 64, 128, 192 and 256. Also no idea, why Pico behaves differently.

I mentioned that I translated Python code directly from C. spiwrite() sends command-value pairs to MAX7219. After initializing the pins, I'll be using for SPI, I created an object by calling the SPI method with these pins (line 19). Then I initialized MAX7219, in the same way as I did on Arduino. I wrote the tick count to StartTime1 and sent the timings to the computer using print() for each overflow in Z[1].

I greatly based C code on pico-examples/spi/max7219_32x8_spi/max7219_32x8_spi.c but simplified it a lot. This example uses default pin definitions for SPI from "hardware/spi.h" (like PICO_DEFAULT_SPI_SCK_PIN, PICO_DEFAULT_SPI_CSN_PIN etc). I created my own definitions and used them (lines 12, 13, 14). I kept the MAX7219 command definitions, cs_select() and write_register() functions, as they are in the original code. In this code, StartTime1 variable on the line 48, is written by get_absolute_time() at the beginning of main(). This should actually happen right before the infinite loop, but it doesn't make any significant change in the results. One important thing to note here, is that Pico has two SPI peripherals inside the chip. This means that you can connect two SPI buses at most to the Pico at the same time (without diving deep into the advanced topics like bit-banging or programmable IO). And these peripherals are connected to multiple GPIO pins. You can see more clearly, what I mean, on the Pico pinout diagram:


I can only use SPI0 peripheral with pins labeled SPI0. E.g. GP0, GP1, GP2 and GP3 (Pins 1, 2, 4, 5). If I'm going to connect an SPI device to SPI1, I can connect it via GP10, GP11, GP12 and GP13 (Pins 14, 15, 16, 17), like I did in my code. And I have to specify which circuitry I'm using when initializing SPI. In the original code spi_default is spi0. Since I'm using spi1, I had to call spi_init() with spi1 on the forty-fifth line. The fact that there are so many details even just for SPI is a result of C being so close to the hardware, but that pays off for the user in terms of speed.

MAX7219 initialization section is between the lines 64 and 70 of the code. The following block is the same as the Arduino sketch, with a single difference that the value is issued to SPI bus via write_register() function.


Test Results

As the main scope of this article is not how I run the codes, I will explain it separately after the results.

First, the results from Arduino as a baseline for efficiency and clock speed. 282 overflow events that occurred on Arduino took 114.486 seconds on average. That's the time to count up to 216 or the period of seventeenth LED being on. Since there are 64 LEDs in total, there are 64 - 17 = 47 bits left. The time required for all of them to light up is 114.486s * 247 = 1.611 * 1016 s = 5.106 * 108 years (511 million). My estimation in the previous article was 5.709 * 108 years.

This is similar to the wheat and chessboard problem. According to the story, the inventor of chess demonstrates his invention to the Indian king. The king likes it very much and says typical "Ask me for anything you want". The inventor asks for one grain of wheat to be placed on the first square and twice as many grains on each subsequent square. Even though the king initially thinks it was a simple request, the amount of wheat the inventor wanted is exactly

This amount of wheat grains is equivalent to 1600 times the annual wheat production under today's conditions. By the way, there are various versions of this story and I recounted it as I remember it from my high school math teacher. The key point is that exponential functions can grow to unimaginable levels. The period of the first LED is 1.75 ms (which is actually long time for a processor), may seem short, but its 264 times is incredibly big.

Let me get back to my topic. Running the same code on Pico from within the Arduino IDE takes an average of 14.658 seconds. Pico runs 7.8125 times faster (125 MHz / 16 MHz) in terms of CPU clock speed. 14.658s * 7.8125 = 114.516s. When normalized by the clock speed, there is only a 30 ms difference between Arduino and Pico. Which means, in terms of CPU efficiency, these are almost identical results. The time required for all of the LEDs to light up is 14.658s * 247 = 2.063 * 1015 s = 6.537 * 107 (65.3 million) years.

Same algorithm runs slower with Micropython than with any other platform. One occurence per 128.641 seconds on average. This is 8.78 times slower than the code run in Arduino IDE. This proves that Micropython is very inefficient on a low-level platform such as a microcontroller. I obtained these results after setting SPI speed to 1 MHz. In my first attempt, when I accidentally set the speed to 400 KHz, the same loop took an average of 138.291 seconds and 142.800 seconds on the next day. Baud rate clearly affects the overall speed, but the slowness is definitely program-related. Otherwise, increasing the baud rate by 2.5, would have had more than a %7 improvement. No need to do any calculation for when these results will reach 264.

From my point of view, the most exciting aspects were the timings in C. Pico can count upto 65 536 in 1.656 seconds with C. This means, C compiler produces a code, that runs 8.85 times faster than even Arduino IDE, the second fastest competitor. BTW, there is no difference between using UART or USB serial port for output. 1.656s * 247 = 2.331 * 1014 s = 7.385 * 106 (7.385 million) years. Which is just a bit less than the time a supercomputer would need to calculate 42, the answer to the ultimate question of life, the universe and everything, according to The Hitchhiker's Guide to the Galaxy.


Appendix: Running Code on Raspberry Pico

The three programming approaches, I mentioned in this article (Arduino IDE, Micropython, C) have one thing in common: The Bootsel button of Pico. When you press and hold this button while connecting the Pico to USB, it boots up in Bootloader mode and appears on the computer as a 128 MB USB flash drive. When you copy a .uf2 file, the executable file format of Pico, to this drive, it automatically recognizes and start running it, if it is in correct format. The cross compiler generates such a .uf2 file. This can be our code written in C, or the Micropython interpreter.

To set up Arduino IDE work environment, you should connect Pico to USB while pressing down the Bootsel button. Then, in the IDE, click "Select other board and port..." from the drop-down menu on the green bar. In the next window, search for Pico under Boards and select the board that is not marked as "Deprecated". When Pico is connected to the computer, it creates /dev/ttyACM0 device, which should be also selected as port in IDE. After doing that, the IDE will ask, if you want to download Pico cross compiler and its libraries. Clicking yes will complete the installation and you can test it with Blink sketch (File -> Examples -> 01. Basics -> Blink). If this works, Pico is also ready to run other sketches.

To work with Micropython, you'd first need the interpreter. As I mentioned earlier, this is a file with .uf2 extension. The link to this file is on Raspberry's official website. The documentation says, that the .htm file shown when Pico is in bootloader mode redirects to the page, where the interpreter is and the .txt file contains the model information. But it didn't work for me. When you copy the correct .uf2 file, the disk disappears; when you copy wrong one, nothing happens.

Second, you'd need a Python IDE. Thonny IDE is generally recommended for Linux and Windows. While preparing this article, version 4.1.6 was available via my package manager's official repository. But running Thonny under Linux wasn't very straightforward from my experience. I first installed it with the package manager, but got the following error:

RuntimeError: There is no current event loop in thread 'MainThread'.


This is definitely not a permission issue, because I later downloaded the .tar.gz file from GitHub, extracted it and ran it without any error. Even more strangely, when I ran thonny again, which is installed by the package manager, it worked more or less OK, despite giving the same error. The executable file installed by the package manager and the file extracted from the .tar.gz are different binaries. It's probably related to the compilation parameters, or the package manager is unable to create some config files properly.

If Pico has started running the Micropython interpreter, python prompt comes directly from python running on Pico via serial port, if the correct board and port has been selected from the bottom right corner of thonny. This means, you can enter a python command here and run it directly on Pico. The output of the command will be sent back to thonny over USB. Even if there is no monitor connected, since the stdout of Pico is the serial port, the print command will write to serial port and this can be viewed in thonny. I wrote "correct board and port has been selected" above, because thonny can also use the python interpreter on the computer. If this is selected, the output will come from local python. In the upper part of thonny, where the text box is, python scripts can be written, run and saved. While opening or saving a file, thonny asks the user whether to use the computer storage or Pico's. In other words, files can also be saved to and read from the board. Moreover, if the file is named main.py, it is automatically run by Micropython as soon as power applied to the board. The script can be run on the selected platform by pressing the green play button on the upper bar and stopped by pressing the red stop button. Since there are no example code included with thonny, I have added a simple blink code below:

from machine import Pin
import time

led = Pin(25, Pin.OUT)

while True:
    led.on()
    time.sleep(0.5)
    led.off()
    time.sleep(0.5)

After talking about thonny in such detail, I must also add that you don't actually need thonny to run python code on Pico. I already mentioned that the communication between the computer and Pico happens via RS232 protocol. So you can open a session in minicom (in Windows, Hyperterminal if it still exists or putty) with the command sudo minicom -D /dev/ttyACM0 -b 115200 to access the python interpreter on Pico. If I recall correctly, there are also keyboard shortcuts for saving and running codes, but precisely for this reason I prefer thonny, so I don't have to keep all these shortcuts in my mind. I also leave a quite detailed video for thonny here:



Finally, running C/C++ code is actually easiest. You transfer the .uf2 file, generated by the cross compiler, to Pico and it simply runs. The hard part is installing prerequisites of the SDK and compiling it. For this, I installed following packages.

  1. arm-none-eabi-gcc-cs.x86_64
  2. arm-none-eabi-gcc-cs-c++.x86_64
  3. arm-none-eabi-newlib.noarch
  4. arm-image-installer.noarch

However, I've been compiling various packages on my machine for years, that's why I already have bunch of devel packages, libraries and compiler tools installed on my computer. Therefore, these are actually the bare minimum and other packages might be also needed. For example, cmake is also a requirement, but I already have it.

I cloned Pico SDK and Pico Examples from Github and set the PICO_SDK_PATH environment variable to the path where I downloaded the SDK. Then I ran cmake -S . && make -j4 in pico-examples directory. As there are many small and independent code examples, parallel make significantly speeds the compilation up. The recommended way is actually creating a build/ directory and directing the output of the compilation there. I compiled it in a quick and dirty way. After the compilation is finished, a .uf2 file is generated for each code example. The best starting point is blink/blink.uf2, as always, can be copied to Pico to try out.