Raspberry Pi Pico SBUS Code Help

I am trying to read data from an R/C receiver over SBUS which is an inverted signal to UART.

I started with this library which is meant for a pyboard so the micropython implementation doesn’t work the same.

With @Evan_Lott help I have been able to get the code translated to run without errors, but no data is received.

Without having to use an inverter IC, there is a C function to invert the GPIO but I’m not sure how to implement this with the micropython.

The Pi Pico has Programmable IO which allows you to run assembly code to handle IO.

This is way too low level for me and I’m asking for help from actual programmers here.

I’m willing to pay/trade for your time if you think you can make it work.

1 Like

Doesn’t the SBUS require an inverter? The documentation would suggest you need this…

1 Like

Just to analyze that I confirmed the differences and yes they do appear to be inverted. I am basically going to make that circuit but with a SN74LS04N inverter I have here.

I was able to understand what the library is doing after a long session with @redslashace

2 Likes

The SN74LS04N is a 5 Vdc part.
The 3.3 Vdc part is SN74LVC04A
Hex inverter - make sure you don’t leave any of the inputs floating - ie not connected.
Single inverter may work for your project - SN74LVC1G04
datasheet pdfs at ti.com

2 Likes

thanks, I noticed in the datasheet. Good idea with a single inverter, I prefer through hole so it’s easy to breadboard but when making a custom PCB for this I would prefer to surface mount. If I can find one transistor in the billion in E-lab, I could do the schematic pictured above.

According to the rp2040 chip data sheet, peripheral inputs and outputs can be inverted. Micropython also supports inline assembly, so that’s the hack we use here enable input inversion.

The idea is to set up the UART port first, then set up the GPIO inversion. If the inversion is set up first, it’s likely that the UART setup will overwrite the inversion.

  • setup UART port
  • invert UART input in GPIO

Reversing the above order might work, and would be an interesting experiment.

Disclaimer: This is my first foray into ARM assembler, so beware the newbie curse…
Update Fixed some assembler syntax errors (replaced + with , for offsets, removed # for literals)

Following example is starting point, from section 10.2 of this link
http://docs.micropython.org/en/latest/pyboard/tutorial/assembler.html

@micropython.asm_thumbdef led_on():
    movwt(r0, stm.GPIOA)
    movw(r1, 1 << 13)
    strh(r1, [r0, stm.GPIO_BSRRL])

Reference for ARM Thumb inline assembler

https://micropython-docs-esp32.readthedocs.io/en/esp32_doc/reference/asm_thumb2_index.html

Set GPIO pin to inverting input

# r0 has GPIP pin # (0-29) to invert input
@micropython.asm_thumb
def inv_gpio_in(r0):
    ; multiply pin * 8    ; offset to regs for that pin
    add(r0, r0, r0)
    add(r0, r0, r0)
    add(r0, r0, r0)
    add(r0, r0, 4)       ; offset to control register
    movwt(r1, 0x40014000) ; base address of GPIO registers
    add(r0, r0, r1)       ; r0 = final address of pin regs start
    ldr(r1, [r0, 4])     ; get control reg value
    ; Only want to change bits 17:16 in GPIO ctrl reg,
    ; so retain all but those bits
    ; NOTE: bits 17:16 should already be clear, but we do so here
    ;       for completeness
    movwt(r2, 0x30000)    ; mask for bits 17:16
    bic(r1, r2)           ; clear bits 17:16
    ; set bits 17:16 = 01 to invert GPIO input
    movwt(r2, 0x10000)    ; mask for bits 17:16    
    orr(r1, r2)
    str(r1, [r0, 4])     ; save new control reg value
    mov(r0, r1)           ; return the new control reg value

Less assembler, more python

The above assembler code demonstrates why assembler is used only when necessary. The programmer must manage every little detail.

Really, all that’s needed is the ability to peek (read) and poke (write) memory locations. Since we know the addresses of the GPIO control registers (see below references), writing code to invert the input pin is much shorted in python. Here it is, using peek/poke to access the the control register.

Note: This could be done with a 16-bit read & write to only access the upper half of the control register. But since the endianess wasn’t clear to me I changed it to access the full word, eliminating any question.

Little & big endian code included below, but commented out. The endianess could be determined empirically by picking an endianess and writing a 16 bit value to a register, then reading it back as a 32 bit value and see which half was updated by the 16 bit write.

# python version of inverting gpio in, using peek/poke
def inv_gpio_in( pin_num):
    gpio_ctrl_addr = 0x40014000 + pin_num*8 + 4;
    mem_poke32(gpio_ctrl_addr, (mem_peek32(gpio_ctrl_addr) & 0xFFFCFFFF) | 0x10000

    ; little endian 16 bit access
    ;mem_poke16(gpio_ctrl_addr+2, (mem_peek16(gpio_ctrl_addr+2) & 0xFFFC) | 0x1

    ; big endian 16 bit access
    ;mem_poke16(gpio_ctrl_addr, (mem_peek16(gpio_ctrl_addr) & 0xFFFC) | 0x1

8, 16, 32 bit memory peek/read & poke/write functions

The following micropython functions can be used to access 8, 16, or 32 bit memory values

# read 32 bits from memory
# r0 = address
@micropython.asm_thumb
def mem_peek32(r0):
    ldr(r0, [r0, 0]) 

# write 32 bits to memory
# r0 = address
# r1 = data
@micropython.asm_thumb
def mem_poke32(r0, r1):
    str(r1, [r0, 0]) 

# read 16 bits from memory
# r0 = address
@micropython.asm_thumb
def mem_peek16(r0):
    ldrh(r0, [r0, 0]) 

# write 16 bits to memory
# r0 = address
# r1 = data
@micropython.asm_thumb
def mem_poke16(r0, r1):
    strh(r1, [r0, 0]) 

# read 8 bits from memory
# r0 = address
@micropython.asm_thumb
def mem_peek16(r0):
    ldrb(r0, [r0, 0]) 

# write 8 bits to memory
# r0 = address
# r1 = data
@micropython.asm_thumb
def mem_poke16(r0, r1):
    strb(r1, [r0, 0]) 

RPi 2040 GPIO registers

Following is from RP2040 data sheet

The inversion of GPIO input or outputs is controlled by GPIOx_CTRL register, were x=GPIO pin number.

GPIO Register address mapping
This is the memory map for GPIOs 0-29 in the user IO bank

1 Like

I’ve never worked with micropython, but surely an embedded language has a way to access registers directly that doesn’t stoop to writing your own inline assembly?

http://docs.micropython.org/en/v1.8.2/esp8266/reference/speed_python.html details a really neat module (viper) that lets you declare pointers to arbitrary memory locations in python. Perhaps it’s been brought over to the rp2040?

It’d also be nice if the rp2040 had cmsis-like headers so you didn’t need to encode the location and structure of the gpio registers in your program… But maybe that’s asking too much.

2 Likes

I was surprised that my google searches for an MP peek/poke/read/write memory came up empty. I’m guessing there must be and I just couldn’t dig out the info. So out came the assembly hacksaw…

Beside, it was a lot of fun and I learned something. Always nice to have assembly capability for quick n dirty stuff.

2 Likes

Ahhhh…assembler. Brings back many fond memories :crazy_face:
If you can do this in the code - most efficient way to get it done.No extra hrdwr needed, no board mods, no increased pwr needed. QED

Just fyi - the transistor in the above ckt can be a 2N2222.
A FET version of the ckt can use a 2N7000… No base resistor needed - drive gate direct.

1 Like

Wow I appreciate the help with this project, especially @urbite! I did find an appropriate 74HC04N and was able to properly invert the sbus to uart.

After a bunch of troubleshooting I found my code issue. @Evan_Lott helped sticking out with me learning about this with me.

Finally data being read from the Frsky receiver on the pi pico with sbus.

4 Likes

Congrats @themitch22 for getting things working!

Turns out that the stm module in the STM32 port of micropython does have memory read/write functionality. The memory access functionality is implemented as arrays: mem8, mem16, mem32, for 8, 16, and 32 bit data, respectively.

Tying this back to implementing memory access functionality using inline assembler, we first define the inline assembler function peek32. Then, function peek32 and array stm.mem32 are used to read 4 words starting at address 0. Will they return the same results?

>>> @micropython.asm_thumb
... def peek32(r0):
...     ldr(r0, [r0,0])

>>> [hex(peek32(x)) for x in range(0,16,4)]
['0x2001fff8', '0x805021d', '0x8048937', '0x8048925']

>>> [hex(stm.mem32[x]) for x in range(0,16,4)]
['0x2001fff8', '0x805021d', '0x8048937', '0x8048925']

Both implementations return the same results, as expected hoped.

This example was run on an STM32F446 Nucleo board with micropython v1.14.0

>>> import os
>>> print(os.uname())
(sysname='pyboard', nodename='pyboard', release='1.14.0', version='v1.14 on 2021-02-02', machine='NUCLEO-F446RE with STM32F446xx')

I am happy you found help with this.

It reminds me of the Common Room of old.

1 Like

Finally got an RPi Pico to play with. Turns out that the 8, 16, and 32 bit memory access arrays, mem8, mem16, mem32, in the stm module are also in the machine module for both the stm and rpi pico python builds. I’m guessing this is the case for all ARM-based micropythons. So no need for assembler to twiddle control registers, just peek n poke.

>>> help('modules')
__main__          gc                uasyncio/event    ujson
_boot             machine           uasyncio/funcs    uos
_onewire          math              uasyncio/lock     urandom
_rp2              micropython       uasyncio/stream   ure
_thread           onewire           ubinascii         uselect
_uasyncio         rp2               ucollections      ustruct
builtins          uarray            uctypes           usys
ds18x20           uasyncio/__init__ uhashlib          utime
framebuf          uasyncio/core     uio               uzlib
Plus any modules on the filesystem

>>> import uos
>>> dir(uos)
['__class__', '__name__', 'remove', 'VfsLfs2', 'chdir', 'getcwd', 'ilistdir', 'listdir', 'mkdir', 'mount', 'rename', 'rmdir', 'stat', 'statvfs', 'umount', 'uname']
>>> uos.uname()
(sysname='rp2', nodename='rp2', release='1.14.0', version='v1.14 on 2021-02-05 (GNU 9.3.0 MinSizeRel)', machine='Raspberry Pi Pico with RP2040')

>>> import machine
>>> dir(machine)
['__class__', '__name__', 'ADC', 'I2C', 'PWM', 'PWRON_RESET', 'Pin', 'SPI', 'SoftI2C', 'SoftSPI', 'Timer', 'UART', 'WDT', 'WDT_RESET', 'bootloader', 'deepsleep', 'disable_irq', 'enable_irq', 'freq', 'idle', 'lightsleep', 'mem16', 'mem32', 'mem8', 'reset', 'reset_cause', 'soft_reset', 'time_pulse_us', 'unique_id']
>>> [hex(machine.mem32[x]) for x in range(0,16,4)]
['0x20041f00', '0xef', '0x35', '0x31']

Also, micropython appears to be coded as a little endian machine, as illustrated by the following code snippet. I believe this is programmable in the ARM architecture.

>>> [hex(machine.mem8[x]) for x in range(0,4)]
['0x0', '0x1f', '0x4', '0x20']
>>> hex(machine.mem32[0])
'0x20041f00'

# Just for fun, access the bytes high-to-low to match 32-bit output visually
>>> [hex(machine.mem8[x]) for x in range(3,-1, -1)]
['0x20', '0x4', '0x1f', '0x0']
4 Likes

Updating the function to invert a GPIO input pin

# put this at top of code, or before mem functions are used
import machine

# python version of setting gpio input polarity, using memory access array
def set_gpio_in_pol( pin_num, polarity):
    gpio_ctrl_addr = 0x40014000 + pin_num*8 + 4
    pol_mask = polarity << 16
    machine.mem32[ gpio_ctrl_addr ] = (machine.mem32[ gpio_ctrl_addr ] & 0xFFFCFFFF) | pol_mask 

# set input pin to inverting
def set_gpio_in_inv( pin_num):
    set_gpio_in_pol( pin_num, 1)

# set input pin to non-inverting
def set_gpio_in_ninv( pin_num):
    set_gpio_in_pol( pin_num, 0)

Now, let’s test with the pico LED. Appears to be working. Setting inversion in GPIO control register causes GPIO read value to flip without changing the output value.

>>> from machine import Pin
>>> led = Pin(25, Pin.OUT)
# Set LED pin = 1 and read/confirm
>>> led.value(1)
>>> led.value()
1
# Invert LED pin input value, then read it - value is inverted
>>> set_gpio_in_inv(25)
>>> led.value()
0
# Remove inversion on LED pin input 
>>> set_gpio_in_ninv(25)
# LED value read = 1, so inversion is gone
>>> led.value()
1

3 Likes

With input pin inversion functionality working, we add output pin inversion for completeness. To avoid duplication of code, let’s refactor a bit.

# put this at top of code, or before mem functions are used
from machine import mem32

# python version of setting gpio input or output polarity, using memory access array
# direction: 0=output, 1=input
def set_gpio_inout_pol( pin_num, direction, polarity ):
    gpio_ctrl_addr = 0x40014000 + 8*pin_num + 4
    pol_value = polarity << 8 + 8*direction
    pol_mask = ~(3 << (8 + 8*direction))
    mem32[ gpio_ctrl_addr ] = (mem32[ gpio_ctrl_addr ] & pol_mask) | pol_value 

# set pin input to inverting
def set_gpio_in_inv( pin_num ):
    set_gpio_inout_pol( pin_num, 1, 1)

# set pin input to non-inverting
def set_gpio_in_ninv( pin_num ):
    set_gpio_inout_pol( pin_num, 1, 0)

# set pin output to inverting
def set_gpio_out_inv( pin_num ):
    set_gpio_inout_pol( pin_num, 0, 1)

# set pin output to non-inverting
def set_gpio_out_ninv( pin_num ):
    set_gpio_inout_pol( pin_num, 0, 0)

Now, let’s test inversion on input and output with the pico LED.

>>> from machine import Pin
>>> led = Pin(25, Pin.OUT)

# Set LED pin in and out to non-inverting
>>> set_gpio_out_ninv(25)
>>> set_gpio_in_ninv(25)

# Set LED pin to both states and read/confirm no inversion
>>> led.value(0)
>>> led.value()
0
>>> led.value(1)
>>> led.value()
1

# Enable inversion in pin output. Readback confirms inversion *without* setting value
# NOTE: LED turns OFF, because 1 (LED=ON) was last value written, but it's now inverted by GPIO logic
>>> set_gpio_out_inv(25)
>>> led.value()
0

# LED turns after the next command, again due to output inversion
>>> led.value(0)
>>> led.value()
1

# Enable inversion also in pin input.
>>> set_gpio_in_inv(25)

# Readback data should match output value written because **both input and output inversions are enabled**
# NOTE: Because output inversion is still enabled, LED is ON when 0 is written to pin
>>> led.value(0)
>>> led.value()
0

>>> led.value(1)
>>> led.value()
1

Appears to be working. Setting input or output pin inversion in GPIO control register causes GPIO input and output values to flip without changing the output value.

1 Like

We now have the ability to invert a GPIO pin before it drives an on-board peripheral, or to invert a peripheral output before it drives a GPIO pin. It would be nice to be able to get the ‘override’ status of a GPIO pin. The term override comes from the names of these bit fields in the pico RP2040 datasheet, as shown in these clips from the GPIO control register definition.

image
image

One can see that there are overrides for the output enable and interrupt of the GPIO, in addition to the in and out. In this case a pair of bits is being extracted from a 32-bit word, but we envision a more general case where it is desired to extract any number of bits starting at any bit position. So we implement this core extraction functionality, then wrap it as needed to extract the GPIO override bits.

# put this at top of code, or before mem functions are used
from machine import mem32

# extract any number of bits from an integer data item
def extract_bits( data, start_pos, num_bits ):
    return (data >> start_pos) & ((1<<num_bits)-1)

# extra bits from any 32-bit memory location
def extract_bits_mem32( addr, start_pos, num_bits ):
    return extract_bits( mem32[addr], start_pos, num_bits )

# extra bits from any GPIO control register
def extract_bits_gpioctrl( pin_num, start_pos, num_bits ):
    return extract_bits_mem32( 0x40014000 + 8*pin_num + 4, start_pos, num_bits )

# get gpio input override setting
def get_gpio_in_over( pin_num ):
    return extract_bits_gpioctrl( pin_num, 16, 2 )

# get gpio output override setting
def get_gpio_out_over( pin_num ):
    return extract_bits_gpioctrl( pin_num, 8, 2 )

With a framework in place, extracting the override output enable and interrupt override GPIO settings is a matter of defining a few extra wrapper functions where only the bit position is changed.

Assuming the set_gpio_xxx functions and imports defined previously are available, let’s confirm that our getter functions are working. We use our trusty LED pin as the test item.

# set and read back GPIO pin in inversions
>>> set_gpio_in_ninv(25)
>>> get_gpio_in_over(25)
0
>>> set_gpio_in_inv(25)
>>> get_gpio_in_over(25)
1

# set and read back GPIO pin out inversions
>>> set_gpio_out_ninv(25)
>>> get_gpio_out_over(25)
0
>>> set_gpio_out_inv(25)
>>> get_gpio_out_over(25)
1

We could refactor inversion override setter functions, building up those functions in a corresponding hierarchical manner. But the horse is way beyond dead.

3 Likes

So I tried to implement this best I could but it doesn’t seem to invert the UART RX pin.


from machine import UART, Pin
from invertGPIO import *
import array

class SBUSReceiver:
    def __init__(self, uart_port):
        set_gpio_in_inv(5) #inverting SBUS to UART input pin 5
        self.sbus = UART(uart_port, 100000, tx = Pin(4), rx = Pin(5), bits=8, parity=0, stop=2)

and including this in another py file

def set_gpio_inout_pol( pin_num, direction, polarity ):
    gpio_ctrl_addr = 0x40014000 + 8*pin_num + 4
    pol_value = polarity << 8 + 8*direction
    pol_mask = ~(3 << (8 + 8*direction))
    mem32[ gpio_ctrl_addr ] = (mem32[ gpio_ctrl_addr ] & pol_mask) | pol_value 

# set pin input to inverting
def set_gpio_in_inv( pin_num ):
    set_gpio_inout_pol( pin_num, 1, 1)

it still works like inversion or non-inversion is even happening with my inverter IC, so not what I expected.

I wish I understood the PIO well enough to do the inversion with the PIO as a state machine, it should be a simple operation.

Try reversing the order of inverting the pin and initializing the UART. The UART init probably overwrites the invert bit in the pin control register.

class SBUSReceiver:
    def __init__(self, uart_port):
        self.sbus = UART(uart_port, 100000, tx = Pin(4), rx = Pin(5), bits=8, parity=0, stop=2)
        set_gpio_in_inv(5) #inverting SBUS to UART input pin 5
1 Like

I’ll try that. I found on the raspberry pi forum thread I posted, the latest release of micropython allows an invert keyword for uart()

So I made a custom PCB with the help of @redslashace in KiCAD. I ended up going with a transistor inverter circuit instead of inverting the pin in the firmware, I found out that the latest MicroPython version enabled the “invert” keyword in the UART() init. It was just simpler to do it with a common P2N222 and allows the receiver to get 5v and 3.3v for the GPIO.

It uses two Panasonic opto-isolated mosfet SSR’s per channel like the PWM relays we used in Subzero previously: Micro PWM Switch - ServoCity.

Ordered it on JLCPCB.com and got 5 boards in about a week, 2oz copper for higher current output. I’m blown away with how easy it is to go from KiCAD to fully produced board in a few days.

I still have to work on using timers and interrupts to keep the packet updating every 0.3milliseconds and the LED triggering when the channel > 1000 threshold.

6 Likes