micropython icon indicating copy to clipboard operation
micropython copied to clipboard

rpi2040 gives correct output on logic analizer for 8bit but not for 9bit communication

Open aomanchuria opened this issue 1 year ago • 12 comments

Port, board and/or hardware

rpi2040

MicroPython version

MicroPython v1.23.0 on 2024-06-02; Raspberry Pi Pico with RP2040

Reproduction

import machine
import utime

# Configure UART1 on GPIO8 (TX) and GPIO9 (RX)
uart = machine.UART(1, tx=machine.Pin(8), rx=machine.Pin(9), baudrate=25000, bits=9, parity=0, stop=1)

# Function to send the known pattern
def send_pattern():
    #test bytes
    uart.write(bytes([0x55]))  # 0x55 (binary 01010101)
    uart.write(bytes([0xAA]))  # 0xAA (binary 10101010)
    uart.write("AAA")
    #what I want to actually send
    uart.write(bytes.fromhex('0102'))
    uart.write(bytes.fromhex('0104'))
    uart.write(bytes.fromhex('00F9'))
    #uart.write(b'\xAA')
    #uart.write(b'\xFF')

# Send the known pattern every 50ms
while True:
    send_pattern()
    utime.sleep_ms(50)

Expected behaviour

expected to see those values in the logic analyzer

Observed behaviour

the logic analyzer sees this:

name type start_time duration data error
Async Serial data 0.0087 0.00042 0x0075  
Async Serial data 0.00914 0.00042 0x0029 framing
Async Serial data 0.00966 0.00042 0x0161 framing
Async Serial data 0.0103 0.00042 0x0061  
Async Serial data 0.01074 0.00042 0x002C framing
Async Serial data 0.01126 0.00042 0x0064 framing
Async Serial data 0.0119 0.00042 0x01F9  
Async Serial data 0.05908 0.00042 0x0175 framing
Async Serial data 0.05958 0.00042 0x010A  
Async Serial data 0.06002 0.00042 0x0161 framing

Additional Information

If I change the program and analyzer to use 8bits instead, things look better. I may have some issues there but the 0x55 and 0xAA are picked up for example.

uart = machine.UART(1, tx=machine.Pin(8), rx=machine.Pin(9), baudrate=25000, bits=8, parity=0, stop=1)
name type start_time duration data error
Async Serial data 0.01646 0.00038 0x55 framing
Async Serial data 0.0169 0.00038 0xAA framing
Async Serial data 0.01734 0.00038 0x41 framing
Async Serial data 0.01778 0.00038 0x41 framing
Async Serial data 0.01822 0.00038 0x41 framing
Async Serial data 0.01866 0.00038 0x01  
Async Serial data 0.0191 0.00038 0x02  
Async Serial data 0.01954 0.00038 0x01  
Async Serial data 0.01998 0.00038 0x04  

Code of Conduct

Yes, I agree

aomanchuria avatar Jun 02 '24 16:06 aomanchuria

9 Bit is not supported by the RP2040 MCU. At least an error should be raised. AFAIK, the only port that supports 9 bit is the STM32 port.

robert-hh avatar Jun 02 '24 18:06 robert-hh

fantabulous. Do you have some recommended boards from mamazone for this? This is, I'm not kidding, the 4th board I try to get this very simple task done and some how I get one more face smash at the wall. I have a raspberry py zero lying around, do you think that might do it? I'll go see if I can find it in the mean time.

aomanchuria avatar Jun 02 '24 18:06 aomanchuria

You can get a PyBoard V1.1 from the MicroPython store https://store.micropython.org/product/PYBv1.1 The Adafruit Feather STM32F405 Express should work as well https://www.adafruit.com/product/4382, sold on Amazon as well. If you search for PyBoard on Amazon, you get links to other STM32F405VG boards, which may work as well with the MicroPython firmware. Only that the board names for the Pins will not match.

robert-hh avatar Jun 02 '24 19:06 robert-hh

Just bit-bang your serial, with PIO 25000bauds should be fairly doable.

Blafy avatar Jun 04 '24 12:06 Blafy

this sort of worked:


from machine import Pin
import utime

class UART:
    def __init__(self, tx_pin, baud_rate=2500):
        self.tx_pin = Pin(tx_pin, Pin.OUT)
        self.baud_rate = baud_rate
        self.bit_period_us = int(1000000 / baud_rate)

    def send_data(self, data):
        for byte in data:
            # Start bit
            self.tx_pin.value(0)
            utime.sleep_us(self.bit_period_us)  # Start bit duration

            for _ in range(8):  # 8 data bits
                self.tx_pin.value(byte & 1)
                byte >>= 1
                utime.sleep_us(self.bit_period_us)  # Data bit duration

            # Stop bit
            self.tx_pin.value(1)
            utime.sleep_us(self.bit_period_us)  # Stop bit duration

            # Ensure total byte transmission duration
            utime.sleep_us(self.bit_period_us * 2 - 4)  # Total byte transmission duration

# Example usage:
if __name__ == "__main__":
    uart = UART(tx_pin=8, baud_rate=2500)  # Initialize UART with TX pin connected to GPIO pin 8 and baud rate of 2500
    data = bytearray([102, 104, 0x0F9])  # Data to be transmitted
    uart.send_data(data)  # Send data

aomanchuria avatar Jun 04 '24 13:06 aomanchuria

the timing seems correct: image

but it doesn't look exactly like the original signal from the projector i'm trying to reverse engineer: image

aomanchuria avatar Jun 04 '24 13:06 aomanchuria

Over in raspberry pi land, I got this to work but the timing seems really unstable, the waveform looks exactly as it should be but some times the timing is 400us, some times its 530us so you get framing errors:

import pigpio
import time

# Set up GPIO mode
pi = pigpio.pi()  # Use default BCM numbering

# UART settings
BAUD_RATE = 2500  # Change baud rate to 2500 bps
BIT_PERIOD = 400000/ BAUD_RATE  # Time for one bit in microseconds
START_BIT = 0
STOP_BIT = 1
pin = 14

# Data to send
DATA = [
    (0x02, 1),
    (0x04, 1),
    (0xF9, 0)
]

# Function to send a single byte
def send_byte(byte, parity_bit, pin):
    pi.set_mode(pin, pigpio.OUTPUT)

    # Start bit
    pi.write(pin, START_BIT)
    time.sleep(BIT_PERIOD / 1000000)  # Sleep for the bit period in microseconds

    # Data bits
    for i in range(8):
        bit = (byte >> i) & 0x01
        pi.write(pin, bit)
        time.sleep(BIT_PERIOD / 1000000)  # Sleep for the bit period in microseconds

    # Parity bit
    pi.write(pin, parity_bit)
    time.sleep(BIT_PERIOD / 1000000)  # Sleep for the bit period in microseconds

    # Stop bit
    pi.write(pin, STOP_BIT)
    time.sleep(BIT_PERIOD / 1000000)  # Sleep for the bit period in microseconds

try:
    while True:
        # Send data on pins 8, 10, and 27 (BCM numbering)
        #for pin in [14,8, 10, 27]:
        
        for byte, parity_bit in DATA:
            send_byte(byte, parity_bit, pin)
            #time.sleep(0.000681)  # Sleep for the bit period in microseconds
            #time.sleep(0.000450)  # Sleep for the bit period in microseconds
        time.sleep(0.371)  # Sleep for the bit period in microseconds 
except KeyboardInterrupt:
    pass

# Clean up GPIO
pi.stop()

aomanchuria avatar Jun 04 '24 13:06 aomanchuria

Using GPIO for UART has no reliable timing. The suggestion was to use the PIO state machine, which runs at exact timing. An example for TX is here: https://github.com/raspberrypi/pico-micropython-examples/blob/master/pio/pio_uart_tx.py You can easily expand that to any bit length. Data is supplied to the state machine by sm.put, which may be a word of up to 32 bit. A parity bit could be added to the bit pattern before pushing them to the state machine. Alternative PIO code for RX canbe found at https://github.com/raspberrypi/pico-examples/blob/master/pio/uart_rx

Even if that is using C code, you can take the PIO script from the uart_rx.pio file.

robert-hh avatar Jun 04 '24 15:06 robert-hh

Robert-hh, you're so cool! Look I got it to work! its a baby step, I gotta see if the projector actually gets fooled so simply, but man this gives me the tool to replicate the responses the lamp is supposed to give. Thanks for the tip!

from machine import Pin, Timer
from rp2 import PIO, StateMachine, asm_pio

UART_BAUD = 2500
PIN_BASE = 8
NUM_UARTS = 1

# Define the hexadecimal values to be sent
hex_values = [0x102, 0x104, 0x0F9]

# Assembly code for UART transmission
@asm_pio(sideset_init=PIO.OUT_HIGH, out_init=PIO.OUT_HIGH, out_shiftdir=PIO.SHIFT_RIGHT)
def uart_tx():
    # Block with TX deasserted until data available
    pull()
    # Initialise bit counter, assert start bit for 8 cycles
    set(x, 8)  .side(0)       [7]
    # Shift out 9 data bits, 9 execution cycles per bit
    label("bitloop")
    out(pins, 1)              [7]
    jmp(x_dec, "bitloop")
    # Assert stop bit for 8 cycles total (incl 1 for pull())
    nop()      .side(1)       [6]

# Initialize StateMachine for UART
sm = StateMachine(
    0, uart_tx, freq=9 * UART_BAUD, sideset_base=Pin(PIN_BASE), out_base=Pin(PIN_BASE)
)
sm.active(1)

# Function to print the hexadecimal values
def pio_uart_hex(sm, hex_values):
    for hex_val in hex_values:
        sm.put(hex_val)
        # Insert a slight delay between bytes
        for _ in range(1550):  # Adjust this number for the desired delay
            pass

# Print the hexadecimal values from the UART
pio_uart_hex(sm, hex_values)

# Function to repeat the transmission every 200ms
def repeat_send(timer):
    pio_uart_hex(sm, hex_values)

# Initialize timer for repeating transmission
tim = Timer(-1)
tim.init(period=275, mode=Timer.PERIODIC, callback=repeat_send)

image

aomanchuria avatar Jun 05 '24 04:06 aomanchuria

I had to play around with the delays 1550 gave me around the correct delay between bytes. 275 gave me around the correct delay between messages.

aomanchuria avatar Jun 05 '24 04:06 aomanchuria

best of all, you plug it and it runs, no 2 minutes booting up delay from a lazy raspberry pi :)

aomanchuria avatar Jun 05 '24 04:06 aomanchuria

Glad that it works. To be honest, the hint for using PIO came from @Blafy. I just pointed to suitable examples.

robert-hh avatar Jun 05 '24 06:06 robert-hh