Raspberry Pi Pico SBUS Code Help

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

Great that you’ve got this mostly working to your liking.

Any chance you can share the sbus_driver_micropython translated code that now works with a Pi Pico? I’m trying to do something similar and would prefer not to re-invent the wheel.

Thanks!

1 Like

https://github.com/themitch22/sbus_driver_micropython I forked the sbus receiver library with my fixes issues with the modern micropython implementation for the Pi Pico RP2040 UART methods. I used a different main code for my application but it was built on the example code. Let me know if this works for you! note: I still used a discrete logic inverter circuit to invert UART. Also my use case, I had to add a buffer random failsafe flags when the signal was bad.

1 Like

Hello, I have not great expirience in micropython and i have struggle to get my sbus reader useful. Could you share your main code or exprein me how i need to change sbus_driver_example.py to make it working. I know that I should change pyb to machine but i don"t know how to change function pyb.timer

Thanks!

1 Like

GitHub - themitch22/sbus_driver_micropython: a Micropython driver for the SBUS protocol Here’s the updated code I made. I used a transistor and resistors to invert the SBUS signal for the UART to read. Also self.sbus.any() returns a bool which was causing it to never enter the read loop.

In my use I also had a buffer array to prevent failsafe bits from causing issues.

1 Like


I see, but I am saking for this code, I am not sure what I should change to get your result :slight_smile:

1 Like

Sorry I’ll have to find my actual example script and upload it to extract it. I basically used a state machine and flag loop depending on which transmitter switch was triggered.

1 Like

Thank you in advance. I hope you find your code :slight_smile: