Implementing SENT Interface in MicroPython?

General discussions and questions abound development of code with MicroPython that is not hardware specific.
Target audience: MicroPython Users.
User avatar
sailorXY
Posts: 6
Joined: Mon Jun 14, 2021 11:53 am
Location: Near Potsdam (Germany)

Implementing SENT Interface in MicroPython?

Post by sailorXY » Mon Jun 14, 2021 12:31 pm

Hallo *,

I planned to implement a simulator for the SENT-Interface (Single Edge Nibble Transmission) in Micropython in an OLIMEX STM32-H405 board.

Unfortunately I found out, that the Micropython Main loop is too slow for the bit generation according to the SENT-Spec. I need at least a 9µs low-pulse generation possibility:

Image

(yellow pattern is from a SENT-Sensor - blue pattern from my STM32-H405 board - simple toggle in the main loop)

With a Timer - faster but only periodically pattern are possible. But I need a programmable bit pattern generator with a smallest time unit of 3µs which I can programm freely. Would that be possible with MicroPython - may be with inline assembler?

I tried to toggle my pin simply in the Main-Loop. The measured Low-Time was not below 17µs. Also a Timer with a callback function showed this minimal possible low-time no matter which frequency I set.

Any hints?

kind regards
Mario

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Implementing SENT Interface in MicroPython?

Post by pythoncoder » Mon Jun 14, 2021 1:25 pm

17µs is about what I'd expect. This doc explains how to optimise speed, but as you mentioned you may need to use the inline assembler. This can achieve very high performance. It is also possible to write native C modules, however the learning curve for these is similar to that of the assembler (in my opinion).
Peter Hinch
Index to my micropython libraries.

User avatar
sailorXY
Posts: 6
Joined: Mon Jun 14, 2021 11:53 am
Location: Near Potsdam (Germany)

Re: Implementing SENT Interface in MicroPython?

Post by sailorXY » Mon Jun 14, 2021 2:26 pm

Hi pythoncoder,

thanx for the fast reply. So as a mentioned - inline C or assembler would be possible. So I try to implement the bit generator in C as a Timer callback. Is it shure, that the timer callback will be performed all 3µs when I install it like this:

tim = pyb.Timer(1)
tim.init(freq=333333)
tim.callback(tick)

The callback "tick" then would be an inline C-function with direct memory access to the bitfield to send und with direct GPIO access.

Writing in the bitfield to change the data - can be done from PYTHON side.

What do you think?

kind regards
Mario

User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: Implementing SENT Interface in MicroPython?

Post by Roberthh » Mon Jun 14, 2021 2:41 pm

For test of a decoder I made a python script that creates and sends SENT messages. I used an ESP32 with it's RMT module for the test. Code attached. it includes proper CRC calculation, which was the hard part, given that almost no test data was available.

Code: Select all

from machine import Pin
from esp32 import RMT

# Constants 
data_pin = const(13)  # GPIO13
LOW = const(5)  # Low pulse width in ticks
SYNC = const(56 - LOW)  # SYNC Pulse width
OFFSET = const(12 - LOW)  # Nibble offset
MAXTICK = const(272)  # 56 + 8 * 27 ticks
crc4_tab = (0, 13, 7, 10, 14, 3, 9, 4, 1, 12, 6, 11, 15, 2, 8, 5)
p = Pin(data_pin, Pin.OUT)

def frame(msg, status, idle, pause):
    '''
    Generate the frame pulse train. Arguments:
    msg: value to be sent as 3 byte bytes string
    status: Status value
    idle level: 0  for low, 1 for high
    pause: minimal pause length. 0 for no pause
    '''
# create the pulse sequence
    crc = 5 # Magic start value
# calibration pulse
    data = [LOW, SYNC]
# Status nibble
    data.append(LOW)
    data.append(status + OFFSET) # Status nibble
# Data nibbles
    for v in msg:
        nibble = (v >> 4) & 0xf
        crc = crc4_tab[crc] ^ nibble
        data.append(LOW)
        data.append(nibble + OFFSET)

        nibble = v & 0xf
        crc = crc4_tab[crc] ^ nibble
        data.append(LOW)
        data.append(nibble + OFFSET)
    crc = crc4_tab[crc]  # padding nibble
# CRC nibble
    data.append(LOW)
    data.append(crc + OFFSET) # crc nibble
# Add a pause pulse, if requested
    if pause == 1:
        data.append(LOW)
        data.append(LOW)
    elif pause > 0:
        pause = max(pause, 12)  # Ensure minimal length of 12 ticks
        data.append(LOW)
        data.append((MAXTICK - sum(data)) + (pause - LOW)) # pause pulse
    return data


def send_data(data, idle, tick):
    '''
    Send the data frame(s). Arguments:
    data: list with the tick values
    idle: Idle level of the data line
    tick: tick time in us units
    '''
# set up the interface
    idle = 1 if idle else 0  # map idle value to 0 and 1
    p.value(idle)
    if idle: # if idle is high, the idle level has to be 1. Not supported
             # by the main branch of MicroPython 
        rmt = RMT(0, pin=p, clock_div=8, idle=1)  # 100 ns tick granularity, idle=1
    else:
        rmt = RMT(0, pin=p, clock_div=8)  # 100 ns tick granularity, idle=0 (default)

    tick = int(tick * 10 + 0.49)  # scale to 100 ns granularity
# Mutiply items by the internal tick units
    for i in range(len(data)): 
        data[i] *= tick

# send the pulses
    rmt.write_pulses(data, start=1 - idle)
    rmt.wait_done(timeout=sum(data))  # Wait for the end
    rmt.deinit()


# send a single fast data frame with two data values
def fast(value1, value2=None, status=0, idle=1, pause=12, tick=3):
    '''
    Send a single data frame in fast data format. Arguments:
    value1, value2: values to be sent; integer
    status: Status value
    idle level: 0  for low, 1 for high
    pause: minimal pause length. 0 for no pause
    tick: tick time in us units
    '''
    msg = bytearray(3)
    msg[0] = ((value1 >> 4) & 0xff)
    # msg[1] = ((value1 << 4) & 0xf0) | ((value2 >> 8) & 0x0f)
    # msg[2] = value2 & 0xff
    if value2 is None:
        value2 = value1
    # value2 with the nibbles in opposite order:
    msg[1] = ((value1 << 4) & 0xf0) | (value2 & 0x0f)
    msg[2] = (value2 & 0xf0) | ((value2 >> 8) & 0xf)
    # value 2 is sent in regular order.
    # msg[1] = ((value1 << 4) & 0xf0) | ((value2 >> 8) & 0x0f)
    # msg[2] = value2 & 0xff
    data = frame(msg, status, idle, pause)
    send_data(data, idle, tick)


# send a single secure message frame
def secure(value, counter=0, status=0, idle=1, pause=12, tick=3):
    '''
    Send a single secure message frame. Arguments:
    value: value to be sent; integer
    counter: counter; integer 
    status: Status value
    idle level: 0  for low, 1 for high
    pause: minimal pause length. 0 for no pause
    tick: tick time in us units
    '''
    msg = bytearray(3)
    msg[0] = ((value >> 4) & 0xff)
    msg[1] = ((value & 0x0f) << 4) | ((counter >> 4) & 0x0f)
    msg[2] = ((counter & 0x0f) << 4) | ((~value >> 8) & 0x0f)
    data = frame(msg, status, idle, pause)
    send_data(data, idle, tick)


# send a single fast channel high speed frame
def highspeed(value, status=0, idle=1, pause=12, tick=2.7):
    '''
    Send a fast channel high speed frame. Arguments:
    value: value to be sent; integer
    status: Status value
    idle level: 0  for low, 1 for high
    pause: minimal pause length. 0 for no pause
    tick: tick time in us units
    '''
    msg = bytearray(2)
    msg[0] = ((value >> 5) & 0x70) | ((value >> 6 ) & 0x07)
    msg[1] = ((value << 1) & 0x70) | (value & 0x07)
    data = frame(msg, status, idle, pause)
    send_data(data, idle, tick)


# send a short serial message encoded in a burst of 16 frames
def serial(value, id=0, idle=0, pause=12, tick=3, dummy=True):
    '''
    Send a short serial message with a burst of 16 frames. Arguments:
    value: value to be sent; integer
    id: ID; integer
    idle level: 0  for low, 1 for high
    pause: minimal pause length. 0 for no pause
    tick: tick time in us units
    dummy: Flag telling whether a additional trailing frame will be sent
    '''
    data = []
    crc = 5 # Magic start value
    crc = crc4_tab[crc] ^ (id & 0x0f)
    crc = crc4_tab[crc] ^ ((value >> 4) & 0x0f)
    crc = crc4_tab[crc] ^ (value & 0x0f)
    crc = crc4_tab[crc]  # padding nibble

    value = ((id & 0x0f) << 12) | ((value & 0xff) << 4) | crc
    for i in range(16):
        if i == 0:
            status = ((value >> 13) & 0x04) | 0x08
        else:
            status = ((value >> 13) & 0x04)            
        data += frame(chr(i).encode() + b"\x00\x00", status, idle, pause)
        value <<= 1

    # adding a dummy frame to cope with the SD2000x+ expectation
    if dummy:
        data += frame(b"\xff\x00\x00", 0, idle, pause)

    send_data(data, idle, tick)


# send an enhanced serial message with 12 or 16 bit in a 18 frame burst 
def enhanced(value, id=0, long=False, idle=0, pause=12, tick=3, dummy=True):
    '''
    Send an enhanced serial message with a burst of 18 frames. Arguments:
    value: value to be sent; integer
    id: ID; integer
    long: True for 16 bit, False for 12 bit,
    idle level: 0  for low, 1 for high
    pause: minimal pause length. 0 for no pause
    tick: tick time in us units
    dummy: Flag telling whether am additional trailing frame will be sent
    '''
    # Pack the data into two bit fields for bit2 and bit3
    if long:
        bit3 = ((id & 0x0f) << 6) | ((value >> 11) & 0x1e) | 0x400
    else:
        bit3 = ((id & 0xf0) << 2) | ((id & 0x0f) << 1)
    bit2 =  value & 0xfff

    # calculate the CRC bitwise
    polynome = 0b011001  # (x^6 +) x^4 + x^3 + 1  (1 is x^0 -> lowest bit set, x^6 is implicit)
    uprmask  = 0b100000  # Masking the upper bit of the crc
    bitmask = 0x800
    crc = 0x15 # Magic start value
    for i in range(15):  # 24 data bits + 6 implicit zero bits
        crc = ((crc << 1) | ((bit2 & bitmask) != 0)) ^ (polynome if (crc & uprmask) else 0)
        crc = ((crc << 1) | ((bit3 & bitmask) != 0)) ^ (polynome if (crc & uprmask) else 0)
        bitmask >>= 1
    crc &= 0x3f

    # add the crc to bit2 and "enhanced serial" flag to bit3
    bit2 |= (crc << 12)  # crc sixbit
    bit3 |= 0x3f000  # ones sixbit

    # create the frame sequence
    data = []
    for i in range(18):
        status = ((bit2 >> 15) & 0x04) | ((bit3 >> 14) & 0x08)
        data += frame(chr(i).encode() + b"\x00\x00", status, idle, pause)
        bit2 <<= 1
        bit3 <<= 1

    # adding a dummy frame to cope with the SD2000x+ expectation
    if dummy:
        data += frame(b"\xff\x00\x00", 0, idle, pause)
 
    send_data(data, idle, tick)

User avatar
sailorXY
Posts: 6
Joined: Mon Jun 14, 2021 11:53 am
Location: Near Potsdam (Germany)

Re: Implementing SENT Interface in MicroPython?

Post by sailorXY » Mon Jun 14, 2021 3:21 pm

Thanx Roberthh,

unfortunately my self-compiled OLIMEX-H405 port (STM32) has not yet the RMT module. May be I can add this?
But I think not - its an other processor...

But nevertheless thanx for the code - I have the same challange with the CRC ;-)

best regards
Mario

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Implementing SENT Interface in MicroPython?

Post by pythoncoder » Tue Jun 15, 2021 10:46 am

RMT is an ESP32 hardware component so cannot be implemented on STM.
Peter Hinch
Index to my micropython libraries.

User avatar
sailorXY
Posts: 6
Joined: Mon Jun 14, 2021 11:53 am
Location: Near Potsdam (Germany)

Re: Implementing SENT Interface in MicroPython?

Post by sailorXY » Tue Jun 15, 2021 11:20 am

Hi Peter,

I already found this out. What do you think about my implementation idea according the bit-generation:

"Is it shure, that the timer callback will be performed all 3µs when I install it like this:

tim = pyb.Timer(1)
tim.init(freq=333333)
tim.callback(tick)

The callback "tick" then would be an inline C-function with direct memory access to the bitfield to send und with direct GPIO access.

Writing in the bitfield to change the data - can be done from PYTHON side."

I first make a try with direkt GPIO access in MicroPython in the "tick" callback - may be it is still sufficiant...

best regards
Mario

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Implementing SENT Interface in MicroPython?

Post by pythoncoder » Thu Jun 17, 2021 3:48 pm

I have no idea whether this is feasible - I think you're breaking new ground here. It depends on whether there is a significant overhead in calling a C function which was specified in Python as an ISR. Otherwise you may need to figure out how, in C, to allocate a function to an interrupt. I've often wondered about doing this kind of thing, but I've yet to try it.
Peter Hinch
Index to my micropython libraries.

User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: Implementing SENT Interface in MicroPython?

Post by Roberthh » Thu Jun 17, 2021 4:21 pm

I do not expect it to work at that frequency. The time to call a python function on a F407 is about 4 µs.

User avatar
sailorXY
Posts: 6
Joined: Mon Jun 14, 2021 11:53 am
Location: Near Potsdam (Germany)

Re: Implementing SENT Interface in MicroPython?

Post by sailorXY » Mon Jun 21, 2021 10:22 am

I tried it one time again with direct register access of the GPIO in the ISR. The low-time goes to 15,20µs - which is not sufficiant.

The problem would be the slow ISR-call on Python level as you mentioned - I think. So it makes no difference if the ISR itselfes is native C or not.
So - I switch to complete C-Implementation...

May be I simply introduce a Python command/object in the STM port of the MicroPython interpreter? I built my image from source - so it would be possible. What do you think? Is it simple to make own Python-callable functions - completely embedded in the Interpreter with own C-Code?
I only need the Bit-Generator on C-Level. It needs to toggle a bit-array out with correct timing.

thanx for your hints so far...

Post Reply