RPI Pico serial behaviour with CTS/RTS
Hi,
I am converting the 'bottom' layer of a project from Arduino to use a Pico 2 instead.
The upper layer ('Host') is an Rpi3B+ for development and will then go back to a Jetson Nano - all that just for the 'big picture'
At the bottom layer of this the two halves communicate with MQTT messages and use a serial connection between these boards.
And just to add a challenge I decided to convert the Arduino C/C++ code to MicroPython - what could possibly go wrong?
The basic serial-2-serial connection was easy enough but when it was bolted back into the 'real program' it became apparent that the link/code was not good enough for so many messages and of increasing length so I reverted back to class modules and their test harnesses
Messages FROM the Pico to the Rpi3 seem to survive OK.
However messages from the Rpi3 TO the Pico were being chopped up - mostly partial messages received. I revised the interface to include known start and end characters so I could see things.
Several days later I decided that I would have to add hardware flow control (on the PICO its DIY if you want software flow control)
This was easy to implement on the Pico but quite the effort on the Rpi3 to get the settings to stick across a reboot.
Now cutting to the chase.. From the attached image I can see that at some point in the transmission the RTS line goes High and the flow of characters from the RPI stop.
That is how I understand flow control should work. Then a bit later RTS drops and the flow continues.
(I conclude from this behaviour that the flow control setup from each end is correct)
My problem seems to be that the Pico does not 'wait' rather it delivers each bit as whole message and of course they dont pass muster because one has no tail and one has no head.
This is the first time I have played with CTS/RTS flow control so I have obviously got something wrong in my understanding.
I am hoping some of you could explain how flow control should be treated from the program's perspective. ie what does 'wait for the receive buffer to be emptied' actually mean I should do?
The receive logs below show 2 messages fail and and one succeeds.
Note: 300 bytes has been selected for this test to try and find where the edge case is.
############# Receiving side (Pico)
The code has 4 modules - 2 on the pico end and 2 on the Rpi end. 1 the class and one the driver.
Pico ClassDriver / Test Harness
Hi,
I am converting the 'bottom' layer of a project from Arduino to use a Pico 2 instead.
The upper layer ('Host') is an Rpi3B+ for development and will then go back to a Jetson Nano - all that just for the 'big picture'
At the bottom layer of this the two halves communicate with MQTT messages and use a serial connection between these boards.
And just to add a challenge I decided to convert the Arduino C/C++ code to MicroPython - what could possibly go wrong?
The basic serial-2-serial connection was easy enough but when it was bolted back into the 'real program' it became apparent that the link/code was not good enough for so many messages and of increasing length so I reverted back to class modules and their test harnesses
Messages FROM the Pico to the Rpi3 seem to survive OK.
However messages from the Rpi3 TO the Pico were being chopped up - mostly partial messages received. I revised the interface to include known start and end characters so I could see things.
Several days later I decided that I would have to add hardware flow control (on the PICO its DIY if you want software flow control)
This was easy to implement on the Pico but quite the effort on the Rpi3 to get the settings to stick across a reboot.
Now cutting to the chase.. From the attached image I can see that at some point in the transmission the RTS line goes High and the flow of characters from the RPI stop.
That is how I understand flow control should work. Then a bit later RTS drops and the flow continues.
(I conclude from this behaviour that the flow control setup from each end is correct)
My problem seems to be that the Pico does not 'wait' rather it delivers each bit as whole message and of course they dont pass muster because one has no tail and one has no head.
This is the first time I have played with CTS/RTS flow control so I have obviously got something wrong in my understanding.
I am hoping some of you could explain how flow control should be treated from the program's perspective. ie what does 'wait for the receive buffer to be emptied' actually mean I should do?
The receive logs below show 2 messages fail and and one succeeds.
Note: 300 bytes has been selected for this test to try and find where the edge case is.
Code:
############# Sending side (Rpi3)Host_ser Serial port open baud= 230400 and timeout = 0.05 ***** Starting RPI Serial TestHarness2 / (RPI-T3) mainThrottle = 0.001 (RPI-T3) sending Host_ser sending len=303 mess3 = b'\x1eUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\x1e\n' (RPI-T3) sending UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUHost_ser sending len=8 mess3 = b'\x1e \x1e\n' (RPI-T3) sending Host_ser sending len=303 mess3 = b'\x1eUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\x1e\n' (RPI-T3) sending UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUHost_ser sending len=8 mess3 = b'\x1e \x1e\n' (repeats)Code:
PICO_ser Serial port open baud= 230400 and timeout = 0***** Starting Pico Serial TestHarness2 / (Pico-T3) mainThrottle = 0.1PICO_ser messageBytes is len = 274PICO_ser messageBytes = b'\x1eUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU'PICO_ser rc=1 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>PICO_ser messageBytes is len = 274PICO_ser messagesBytes = b'\x1eUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU'(Pico-T3) rc=1 = Full message did not arrive (corruption or timeout or buffer overflow?)(Pico-T3) rc=1 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<PICO_ser messageBytes is len = 303PICO_ser messageBytes = b'\x1eUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\x1e\n'PICO_ser returning messageList = ['UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU'](Pico-T3) have 1 response(s)= ['UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU']PICO_ser messageBytes is len = 278PICO_ser messageBytes = b'\x1eUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU'PICO_ser rc=1 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>PICO_ser messageBytes is len = 278PICO_ser messagesBytes = b'\x1eUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU'(Pico-T3) rc=1 = Full message did not arrive (corruption or timeout or buffer overflow?)(Pico-T3) rc=1 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<Pico Class
Code:
# serial-2-serial has 2 parts 'TestHarness' and (dumb) Serial_in_out# and Serial_in_out has several flavours, Pico, RPi, Jetson...from machine import UART, Pin # we need to import UART to use itimport timeclass SerialInOut: ########## Class Attributes (shared by all instances of the class) # Record seperator (RS) = 0x1E = 30 decimal RS = '\x1E' # chr(30) Type = <class 'str'> RSb = b'\x1E' # Type = <class 'bytes'> NL = '\x0A' # one end calls this NL and one LF but that is the same char(10) == \x0A NLb = b'\x0A' # MAXMsgLen = 4000 G_MyName = "Pico SerialInOut" G_MyTag = "PICO_ser " ## Constructor(s) ********************************************************************* # WARNING for normal Python (RPI) timeout is 'maximum time to wait for the requested number of bytes to be received' # for microPython (Pico) timeout is 'time (in milliseconds) to wait for the FIRST CHARACTER to arrive' def __init__(self, UartNo=0, TXPinNo=16, RXPinNo=17, baudrate=57600, timeout=0, CTSPinNo=18, RTSPinNo=19, port="tester"): ########## instance Attributes self.serNum = UartNo self.port = port #NA for Pico self.baudrate = baudrate #self.timeoutSec = timeout # Rpi3 wants sec self.timeoutMs = timeout # Pico wants ms self.txPin = Pin(TXPinNo) self.rxPin = Pin(RXPinNo) self.CTSPin = Pin(CTSPinNo) self.RTSPin = Pin(RTSPinNo) ########## Constructor actions # Step 1. initialize the UART try: self.ser = UART(self.serNum, baudrate = self.baudrate , tx = self.txPin, rx = self.rxPin, timeout = self.timeoutMs, cts = self.CTSPin, rts = self.RTSPin ) self.ser.init(rxbuf = 512, # default was 256 # timeout_char = 1000, no effect flow = UART.RTS | UART.CTS ) print(f"{self.G_MyTag} Serial port open baud= {self.baudrate} and timeout = {self.timeoutMs} ") print(f"{self.ser}") except: print(f"{self.G_MyTag} Error opening Serial / UART on pins {self.txPin}: {self.txPin}") raise # Give the serial port time to initialize time.sleep(0.5) ########## Class Methods def send(self, message): mess2 = self.RS + message + self.RS + self.NL mess3 = mess2.encode('utf-8') # split out so we can print it 1st print (f"{self.G_MyTag}sending mess3 = {mess3} ") self.ser.write(mess3) # Encode the string to bytes # self.ser.write(mess2.encode('utf-8')) # Encode the string to bytes and send in one step # READ # returns a 'rc' also # 0 = OK # 1 = Full message did not arrive (corruption or timeout or buffer overflow?) # 2 = Fail - 1st byte is not a RS # 3 = Fail - last byte is not a RS # and returns the message list or an empty list if there is nothing waiting or there is an error def receive(self): rc = 0 # if self.ser.in_waiting > 0: # Rpi if self.ser.any(): # Pico #messageBytes = self.ser.read_until() # RPI was (self.MAXMsgLen) messageBytes = self.ser.readline() # Pico was (self.MAXMsgLen) # print (f"{self.G_MyTag} messageBytes is of type {type(messageBytes)} ") # <class 'bytes'> print (f"{self.G_MyTag} messageBytes is len = {len(messageBytes)} ") # 1993 Next one was 4486! print (f"{self.G_MyTag} messageBytes = {messageBytes} ") # stop compiler errors/warnings if messageBytes is None: return rc, [] # validation # did the full message arrive including the NL? if messageBytes[-1] != self.NLb[0]: # RSb[0]: rc = 1 #print(" Full message did not arrive (corruption or timeout or buffer overflow?) ") elif messageBytes[0] != self.RSb[0]: rc = 2 #print(" Fail: 1st byte is not a RS") # This is important - we can receive just the tail of a message elif messageBytes[-2] != self.RSb[0]: rc = 3 #print(" Fail: last byte is not a RS") if rc != 0: print (f"{self.G_MyTag} rc={rc} >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") print (f"{self.G_MyTag} messageBytes is len = {len(messageBytes)} ") # 1993 Next one was 4486! #print (f"{self.G_MyTag} messagesBytes!r = {messageBytes!r} ") # !r : Applies repr() to the value, providing a developer-focused representation useful for debugging. print (f"{self.G_MyTag} messagesBytes = {messageBytes} ") self.ser.read() # Pico try and purge the buffer # self.ser.reset_input_buffer() # RPI try and purge the buffer return rc, [] messages = messageBytes[:-1].decode('utf-8') # , in pico there is no option > "errors='ignore' " # this .decode() removes the byte string format; strip() removes all common leading and trailing whitespace # print (f" messages = {messages} ") tempList = messages.split(self.RS) # this will create empty items - especially at <end> <start> sequences messageList = list(filter(None, tempList)) #delete all empty items print(f"{self.G_MyTag} returning messageList = {messageList}") return rc, messageList else: return rc, []Code:
# 2026/02/04 V3 enabling hardware flow control (CTS/RTS)# (just pass the pin numbers in)#from serial_in_out_RPi import SerialInOutfrom serial_in_out_pico import SerialInOutfrom machine import Timerimport time# ================================================================# === Call-back Functions for Threads# ================================================================ ############################################################################## # handle_burst_mode_trigger # just the variable to trigger the output ##############################################################################def handle_burst_mode_trigger(t): # Pico has parm 't'#def handle_burst_mode_trigger(): # Rpi has no Parm global G_doSendBurst G_doSendBurst = True # ================================================================# === Initialize# ================================================================G_MyName = "Pico Serial TestHarness3"G_MyTag = "(Pico-T3) "# instantiate a SerialInOut object# for baudrate choose from 115200, 230400, 460800 (obviously both end have to be the same)test_uart = SerialInOut(UartNo=0, TXPinNo=16, RXPinNo=17, baudrate=230400, CTSPinNo=18, RTSPinNo=19) # for Pico need to specify which Uart and pins# test_uart = SerialInOut(baudrate=230400, port='/dev/serial0') # for RPi-3 "/dev/ttyUSB0 is the USB-2-serial breakout, /dev/serial0 is via pins 14/15"port='/dev/serial0'# test_uart = SerialInOut(baudrate=230400, port='/dev/ttyUSB0' ) # for PCmsg2Send = ['First', #300 char "300UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU" ] # pause time - make it different for each end to prove, almost, that multiple messages will queue up#pauseTime = 5 #in secondsbatch_sent = FalsemainThrottle = 0.01 #in seconds (at 0.01 the 'swamp' of messages was handled. At 0.1 and even 0.02 they were truncated)######## items concerned with sending the output messages in a 'burst'G_doSendBurst = FalseG_timer_send_every = 10000 # millisec. How ofter we send our batch of messages out# PicoburstTimer = Timer(-1)## disable following 'burstTimer.init()' line to suppress outgoing messagesburstTimer.init(mode=Timer.PERIODIC, period=G_timer_send_every, callback=handle_burst_mode_trigger)# RPI# burstTimer = threading.Timer(G_timer_send_every, handle_burst_mode_trigger) # RPI / normal python# burstTimer.daemon = True# # disable following 'burstTimer.start' line to suppress outgoing messages# burstTimer.start() # RPI / normal pythonNoResponsesReceivedCount = 0# receive return-code decodes RC_decode = { 0: "OK", # - and Message is in 2nd return var 1: "Full message did not arrive (corruption or timeout or buffer overflow?) ", 2: "Fail - 1st byte is not a RS ", 3: "Fail - last byte is not a RS ", 4: "Fail - ", 5: "Fail - ", 6: "Fail - ",}print(f"***** Starting {G_MyName} / {G_MyTag} mainThrottle = {mainThrottle} ")# ================================================================# === Main Code Loop ===# ================================================================try: while True: if G_doSendBurst: for m in msg2Send: test_uart.send(m) # Send message to the other device (Pico or RPi or Jetson) print(f"{G_MyTag}sending {m}") G_doSendBurst = False # now just listen a lot - quickly rc, respList = test_uart.receive() if rc != 0: print(f"{G_MyTag} rc={rc} = {RC_decode.get(rc)}") print (f"{G_MyTag} rc={rc} <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<") # The childSerial will return an empty list if there is an error if respList: # list is NOT empty print(f"{G_MyTag}have {len(respList)} response(s)= {respList}") print(f"{G_MyTag} interveening 'No responses' received = {NoResponsesReceivedCount} ") NoResponsesReceivedCount = 0 else: NoResponsesReceivedCount += 1 time.sleep(mainThrottle) # Wait for 'n' secondsexcept KeyboardInterrupt: print("Program stopped by user")Statistics: Posted by jc508 — Thu Feb 05, 2026 7:26 am