Serial Connection between Raspberry Pi and Raspberry Pico
At the beginning of 2021, I started my robot project. One of its subprojects was to get familiar with the Arduino and C-Programming. The Arduino is unchallenged in terms of sensor and actuator libraries, and unparallelled in the "connect and it works" experience. However, in the middle of the year, I made the decision to switch to using the Raspberry Pico and MicroPython to implement my Robot. This choice is motivated by using one and only one programming language for the Robotics middleware, the SBD and the Microcontroller, as well as to tap into the powerful Python libraries for image recognition that are offered by the RealSense D435 camera.
To get this started, I need to re-learn the essentials of Serial connections between the Raspberry Pico running MicroPython and the Raspberry Pi running Python. This article explains the basics for making a successful connection.
Hardware Connection Options
To connect from the Pi to the Pico, there are three options:
- Direct USB-to-USB
- Direct Tx/Rx Pins
- USB-TTL to Rx/Rx
Since the Raspberry Pico is still relatively young, the technical maturity of the MicroPython stack influences which of these connections can be used.
When you use option A or C you can jump ahead to the examples. If you want to use option B, you need enable the Tx/Rx pins with the steps described in the next section.
How to Enable Tx/Rx Pins on Raspberry Pi
Following this great article, you need to do the following:
Initialize Serial Port via Raspi-Config
$> sudo raspi-config => 3. Interface Option => P6. Serial Port => Would you like a login shell to be accessible over serial? Answer with 'No' => Would you like the serial port hardware to be enabled? Answer with 'Yes'
Disable the
/dev/ttyS0
service$> sudo systemctl stop serial-getty@ttyS0.service $> sudo systemctl disable serial-getty@ttyS0.service
Remove the console from booting
$> sudo nano /boot/cmdline.txt => Remove or comment out a line that says 'console=serial0,115200'
Interestingly, the Pi will still send system messages over the Tx/Rx pins - this might be a particular use case you have been looking for. Here is example output when I shutdown the Pi.
>> [ OK ] Stopped LSB: automatic crash report generation.
>> [ OK ] Stopped User Manager for UID 1000.
>> [ OK ] Stopped Login Service.
>> RPI 4 Model B (0xc03111)
Sending Data from Pi to Pico
Sending data from the Raspberry Pi to the Pico is the same code in all examples, with a minor modification: The port to which the Pico is connected.
Use this boilerplate code:
# sender.py
import time
import serial
ser = serial.Serial(
port='/dev/ttyS0', # Change this according to connection methods, e.g. /dev/ttyUSB0
baudrate = 115200,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=1
)
msg = ""
i = 0
while True:
i+=1
print("Counter {} - Hello from Raspberry Pi".format(i))
ser.write('hello'.encode('utf-8'))
time.sleep(2)
Explanation:
- Line 2: Import the
serial
library that enables the serial connection - Line 4: Create a serial object that is configures exactly as the default Pico UART configuration (baudrate 115200, bytesize 8bits etc.). The
port
section needs to be customized according to the connection methods - see details below - Line 19: To send data, you first
encode
the string data, and thenwrite
it to the serial object
Now let’s examine the receiver side.
Receiving Data: USB to USB Connection
At the time of writing this article, the most recent version of MicroPython running on the Pico is MicroPython v1.16 on 2021-06-18
. This version does not have a build-in library for serial communication over USB.
However, in this Pico Forum thread, a user provides a pure MicroPython implementation that spawns a thread on the 2nd Pico CPU Core which listens actively for incoming bytes over USB. I tried this code in my early Robot prototype and could use it to receive messages via USB on the Pico that commanded my robot to move.
Here is my working example:
# receiver.py / USB => USB
import sys
sys.path.append('/radu')
import os
from bot import Bot
from rusb import USB
from _thread import start_new_thread
from time import sleep_ms
radu = Bot('Radu MK1')
usb = USB()
input_msg = None
bufferSTDINthread = start_new_thread(usb.bufferSTDIN, ())
while True:
input_msg = usb.getLineBuffer()
if input_msg and 'ros_msg' in input_msg:
obj = eval(input_msg)
radu.notify(obj)
sleep_ms(10)
Receiving Data: Tx/Rx Pin Connection
Direct connection between the Tx/Rx pins means:
- Connect Ground: Pi PIN 6 to Pico Pin 3
- Connect Tx to Rx: Pi PIN 8 (GPIO 14) to Pico Pin 1 (GPIO 0)
- Connect Tx to Rx: Pi PIN 10 (GPIO 15) to Pico Pin 2 (GPIO 1)
As explained above, you also need to configure your Raspberry Pi to enable these pins. When completed, use the following code:
# receiver.py / Tx/Rx => Tx/Rx
import os
import machine
from time import sleep
uart = machine.UART(0, 115200)
print(uart)
b = None
msg = ""
while True:
sleep(1)
if uart.any():
b = uart.readline()
print(type(b))
print(b)
try:
msg = b.decode('utf-8')
print(type(msg))
print(">> " + msg)
except:
pass
In this program:
- Line 5: Create an
uart
instance by accessing the Pico’s internalmachine.UART
instance. This comes defined with the same values with which the sender code is configured. Note that the MicroPython documentation also allows you to create an differently configured UART instance, e.g. with a slower baudrate. - Line 12: The condition
uart.any()
returns a positive integer when there is at least one character available on the UART connection. - Line 13: To read data, either use
read(i)
wherei
is the number of characters, orreadline()
to read all characters until a newline is reached. Working with single characters is more efficient and faster, working with complete lines is more reliable if you need to exchange complex information. - Line 16: A
try - except
block starts in which ... - Line 17: ... the received message is decoded and then printed. If there is any error, the program continues without raising the error.
Receiving Data: USB-TTL to Tx/Rx Pin Connection
First, you need to get a USB-TTL adapter, which comes in at least two different chipsets: FT232RL and CH340g.
Insert the adapter on of your Picos USB ports, and then use dmesg
to see how this device is configured.
[326612.390873] usb 1-2: new full-speed USB device number 54 using xhci_hcd
[326612.543888] usb 1-2: New USB device found, idVendor=1a86, idProduct=7523, bcdDevice= 2.64
[326612.543893] usb 1-2: New USB device strings: Mfr=0, Product=2, SerialNumber=0
[326612.543895] usb 1-2: Product: USB Serial
[326612.546221] ch341 1-2:1.0: ch341-uart converter detected
[326612.549000] usb 1-2: ch341-uart converter now attached to ttyUSB0
The last line says ttyUSB0
, so the sender code needs to change as follows:
# sender.py / USB-TTL => Tx/Rx
# ...
ser = serial.Serial(
port='/dev/USB0'
# ...
)
The code for receiving data remains the same as in the direct Tx/Rx connection.
Example Output
Now lets see our code in action.
Executing the sender.py
on the Raspberry Pi outputs this information:
>>>
Counter 1 - Hello from Pi4
Counter 2 - Hello from Pi4
Counter 3 - Hello from Pi4
Counter 4 - Hello from Pi4
And running receiver.py
on the Raspberry Pico show this:
%Run -c $EDITOR_CONTENT
(sysname='rp2', nodename='rp2', release='1.16.0', version='v1.16 on 2021-06-18 (GNU 10.2.0 MinSizeRel)', machine='Raspberry Pi Pico with RP2040')
UART(0, baudrate=115200, bits=8, parity=None, stop=1, tx=0, rx=1, txbuf=256, rxbuf=256, timeout=0, timeout_char=1, invert=None)
<class 'bytes'>
b'hello\n'
<class 'str'>
>> hello
Conclusion
This article explained the three options for an connecting a Raspberry Pi with a Raspberry Pico using (Micro)Python. The first option is USB-to-USB and requires an external MicroPython library to receive data. The second option is to connect Rx/Tx directly from both machines. After some configuration of your Pi, this works stable. The third option is to use a USB-FTL to Tx/Rx. No configuration requires, and it works out of the box.
Considering the source code, on the Pi it’s the pyserial
library: Open a serial connection object with the same configuration as the Pico (baud rate, stop bit) and send encoded data with serial.write()
. On the Pico, its the building machine.UART
object which access either the built-in UARTs or is configurable. Use uart.readline()
to receives bytes, then decode()
the data.