dmazzella / uble

Lightweight Bluetooth Low Energy driver written in pure python for micropython
MIT License
85 stars 7 forks source link

Framing error #7

Closed waleckaw closed 4 years ago

waleckaw commented 4 years ago

Apologies, this is not an issue with your library

I am writing an application the STM32 B-L475E-IOT01A discovery board that uses the SPBTLE-RF BlueNRG chip as well.

I have been dealing with the following issue for a few days now. If you ever encountered a similar issue, I am wondering what you did to fix it.

The Issue:

I am having trouble interfacing with the B-L475E-IOT01A's on-board SPBTLE-RF bluetooth chip.

I am able to send header packets inquiring about the the bluetooth chip's write and read buffers. I have confirmed that I am doing this correctly, because I get responses in the correct format: 0x02 0x?? 0x00 0x?? 0x00. The ?? bytes always give me the correct number of available writable bytes and bytes to be read, respectively.

I am also able to read messages from the bluetooth chip, as I always receive the expected packet when I perform a chip reset: 0x04 0xff 0x03 0x01 0x00 0x01, and after sending an HCI command packet I am able to poll for and read the responses described below.

The issue that I am having is that, whenever I check that the write buffer is available and attempt to send a full write message such as ...

header = 0x0A 0x00 0x00 0x00 0x00 (check write buffer... only sends rest of message if write buffer > 0) HCI command packet = 0x1003 (example: Read_Local_Supported_Features) param total length = 0x00 (i have also tried not including this byte at all and raising the chip select one byte sooner instead) params: none

... I always receive the same HCI event packet in response, no matter the contents of my HCI command packet's body:

response: 0x04 0x10 0x01 0x00

This implies that I am having a hardware framing error with my SPI, and I have no idea what may be causing that. As I just described, the rest of the processes work fine, so the SPI bus seems to be functional in most cases.

Note: My send an receive methods are almost identical in structure to yours

If you can offer me any advice, I would appreciate it immensely. Otherwise, thank you for the awesome library!

dmazzella commented 4 years ago

Hi, can you give me an example of code that I can verify?

P.S. I'm sorry if I answer with such delays, but I keep this library in my spare time and lately it's been less and less.

dmazzella commented 4 years ago

Close for no, reopen it if you need help.

waleckaw commented 4 years ago

Apologies, I am not sure how I missed your response!

dmazella_imitation_BLESPItest.txt

In the file I attached (which I ran from the REPL), I attempted to copy your SPI code almost line-for-line, and was met with the same issue I described above. As you can see, I did have to port my code to fit the microcontroller I was using, the STM32 B-L475E-IOT01A, which contains an SPBTLE-RF on board.

dmazzella commented 4 years ago

no problem, I just tried your code and the problem is in not sending the reset command. As you can see without the reset, the result of the script is identical to yours:

➜  micropython (master) ✗ python3 -B tools/pyboard.py --device /dev/tty.usbmodem3167398C35352 /Users/damianomazzella/Downloads/ww.py
bytearray(b'\x04\x10\x01\x00')
➜  micropython (master) ✗ 

Adding the reset, the behavior is correct:

➜  micropython (master) ✗ python3 -B tools/pyboard.py --device /dev/tty.usbmodem3167398C35352 /Users/damianomazzella/Downloads/ww.py
bytearray(b'\x04\xff\x03\x01\x00\x01')
➜  micropython (master) ✗ 

This is the code I tried:

# hail mary BLE SPI test

import machine
import pyb
import utime
import micropython
from micropython import const

from machine import Pin

# spi = machine.SPI(3, baudrate=8000000, polarity=0, phase=0,
#                   bits=8, firstbit=machine.SPI.MSB)
spi = machine.SPI(2, baudrate=8000000, polarity=0)

#CS = Pin('D13', Pin.OUT_PP, pull=Pin.PULL_NONE)
CS = machine.Pin('B12', machine.Pin.OUT_PP)
CS.on()

#RS = Pin('A8', Pin.OUT_PP, pull=Pin.PULL_NONE)
RS = machine.Pin('B9', machine.Pin.OUT_PP)
RS.on()

_READ_HEADER_MASTER = b'\x0B\x00\x00\x00\x00'
_WRITE_HEADER_MASTER = b'\x0A\x00\x00\x00\x00'
HCI_READ_PACKET_SIZE = const(128)

# IR = Pin('E6', Pin.IN, pull=Pin.PULL_DOWN)
IR = machine.Pin('B8', machine.Pin.IN, machine.Pin.PULL_DOWN)

class CSContext(object):

    def __init__(self, pin):
        self._pin = pin

    def __enter__(self):
        # Assert CS line
        self._pin.off()

    def __exit__(self, exc_type, exc_value, traceback):
        # Release CS line
        self._pin.on()
        # return all(map(lambda x: x is None, [exc_type, exc_value, traceback]))

def write(retry=5, header=b'\x10\x03', param=0):
    """
    Write packet to BlueNRG-MS module
    """
    _rw_header_slave = bytearray(len(_WRITE_HEADER_MASTER))
    result = None
    while retry:
        with CSContext(CS):
            # Exchange header
            spi.write_readinto(
                _WRITE_HEADER_MASTER,
                _rw_header_slave
            )
            rx_write_bytes = _rw_header_slave[1]
            rx_read_bytes = (
                _rw_header_slave[4] << 8
            ) | _rw_header_slave[3]
            if _rw_header_slave[0] == 0x02 and (
                    rx_write_bytes > 0 or rx_read_bytes > 0):
                # SPI is ready
                if header:
                    # avoid to write more data that size of the buffer
                    if rx_write_bytes >= len(header):
                        result = bytearray(len(header))
                        spi.write_readinto(header, result)
                        if param:
                            rx_write_bytes -= len(header)
                            # avoid to read more data that size of the
                            # buffer
                            if len(param) > rx_write_bytes:
                                tx_bytes = rx_write_bytes
                            else:
                                tx_bytes = len(param)
                            result = bytearray(tx_bytes)
                            spi.write_readinto(param, result)
                            break
                        else:
                            break
                    else:
                        break
                else:
                    break
            else:
                utime.sleep_us(50)
        retry -= 1

    return result

def read(size=HCI_READ_PACKET_SIZE, retry=5):
    """
    Read packet from BlueNRG-MS module
    """
    result = None
    _rw_header_slave = bytearray(len(_WRITE_HEADER_MASTER))
    while retry:
        with CSContext(CS):
            # Exchange header
            spi.write_readinto(
                _READ_HEADER_MASTER,
                _rw_header_slave
            )
            rx_read_bytes = (
                _rw_header_slave[4] << 8
            ) | _rw_header_slave[3]
            if _rw_header_slave[0] == 0x02 and rx_read_bytes > 0:
                # SPI is ready
                # avoid to read more data that size of the buffer
                if rx_read_bytes > size:
                    rx_read_bytes = size
                data = b'\xFF' * rx_read_bytes
                result = bytearray(rx_read_bytes)
                spi.write_readinto(data, result)
                break
            else:
                utime.sleep_us(50)
        retry -= 1

    # Add a small delay to give time to the BlueNRG to set the IRQ pin low
    # to avoid a useless SPI read at the end of the transaction
    utime.sleep_us(150)
    return result

def run():

    # reset
    RS.off()
    utime.sleep_us(5)
    RS.on()
    utime.sleep_us(5)

    write()  # retry=5, header=b'\x10\x03', param=0)
    while True:
        utime.sleep_us(12)
        if (IR.value() == 1):
            hi = read(size=HCI_READ_PACKET_SIZE, retry=5)
            print(hi)
            break
        utime.sleep_us(25)

run()

Let me know how it goes. best regards, D.

waleckaw commented 4 years ago

Ok, I probably should have mentioned in my previous post that I always reset the BlueNRG-MS module before running the code. My mistake.

When I run the code you posted, I get the correct initialization response like you did, but all of my following commands are met with the 0x04 0x10 0x01 0x00 error code. Below is an example with a few further modifications I added to continuously send commands to the chip. As you can see, I am trying to send the HCI_Read_Local_Supported_Features command. I get the same response if I send HCI_Read_Local_Supported_Commands, HCI_Read_Remote_Version_Information, or HCI_Read_Local_Version_Information as defined on page 7 in the blueNRG-MS document found here: [https://www.st.com/content/ccc/resource/technical/document/user_manual/6d/a1/5b/6c/dc/ab/48/76/DM00162667.pdf/files/DM00162667.pdf/jcr:content/translations/en.DM00162667.pdf]. I have tried many other commands, but those are 4 that I believe should work without any initialization other than a reset.

code:

def run():
    RS.off()
    utime.sleep_us(5)
    RS.on()
    utime.sleep_us(5)

    write() # HCI_Read_Local_Supported_Features
    while True: 
        utime.sleep_us(12)
        if (IR.value() == 1):
            hi = read(size=HCI_READ_PACKET_SIZE, retry=5)
            print(hi)
            utime.sleep(2)
            write() # HCI_Read_Local_Supported_Features

run()

and here is the output from the REPL:

from BLEProj.test import dmzBLESPI
bytearray(b'\x04\xff\x03\x01\x00\x01')
bytearray(b'\x04\x10\x01\x00')
bytearray(b'\x04\x10\x01\x00')
bytearray(b'\x04\x10\x01\x00')
bytearray(b'\x04\x10\x01\x00')
bytearray(b'\x04\x10\x01\x00')
bytearray(b'\x04\x10\x01\x00')
bytearray(b'\x04\x10\x01\x00')
bytearray(b'\x04\x10\x01\x00')
dmazzella commented 4 years ago

with your example:

# hail mary BLE SPI test

import machine
import pyb
import ustruct
import utime
import micropython
from micropython import const

from machine import Pin

# spi = machine.SPI(3, baudrate=8000000, polarity=0, phase=0,
#                   bits=8, firstbit=machine.SPI.MSB)
spi = machine.SPI(2, baudrate=8000000, polarity=0)

#CS = Pin('D13', Pin.OUT_PP, pull=Pin.PULL_NONE)
CS = machine.Pin('B12', machine.Pin.OUT_PP)
CS.on()

#RS = Pin('A8', Pin.OUT_PP, pull=Pin.PULL_NONE)
RS = machine.Pin('B9', machine.Pin.OUT_PP)
RS.on()

_READ_HEADER_MASTER = b'\x0B\x00\x00\x00\x00'
_WRITE_HEADER_MASTER = b'\x0A\x00\x00\x00\x00'
HCI_READ_PACKET_SIZE = const(128)

# IR = Pin('E6', Pin.IN, pull=Pin.PULL_DOWN)
IR = machine.Pin('B8', machine.Pin.IN, machine.Pin.PULL_DOWN)

class CSContext(object):

    def __init__(self, pin):
        self._pin = pin

    def __enter__(self):
        # Assert CS line
        self._pin.off()

    def __exit__(self, exc_type, exc_value, traceback):
        # Release CS line
        self._pin.on()
        # return all(map(lambda x: x is None, [exc_type, exc_value, traceback]))

def write(retry=5, header=b'', param=0):
    """
    Write packet to BlueNRG-MS module
    """
    _rw_header_slave = bytearray(len(_WRITE_HEADER_MASTER))
    result = None
    while retry:
        with CSContext(CS):
            # Exchange header
            spi.write_readinto(
                _WRITE_HEADER_MASTER,
                _rw_header_slave
            )
            rx_write_bytes = _rw_header_slave[1]
            rx_read_bytes = (
                _rw_header_slave[4] << 8
            ) | _rw_header_slave[3]
            if _rw_header_slave[0] == 0x02 and (
                    rx_write_bytes > 0 or rx_read_bytes > 0):
                # SPI is ready
                if header:
                    # avoid to write more data that size of the buffer
                    if rx_write_bytes >= len(header):
                        result = bytearray(len(header))
                        spi.write_readinto(header, result)
                        if param:
                            rx_write_bytes -= len(header)
                            # avoid to read more data that size of the
                            # buffer
                            if len(param) > rx_write_bytes:
                                tx_bytes = rx_write_bytes
                            else:
                                tx_bytes = len(param)
                            result = bytearray(tx_bytes)
                            spi.write_readinto(param, result)
                            break
                        else:
                            break
                    else:
                        break
                else:
                    break
            else:
                utime.sleep_us(50)
        retry -= 1

    return result

def read(size=HCI_READ_PACKET_SIZE, retry=5):
    """
    Read packet from BlueNRG-MS module
    """
    result = None
    _rw_header_slave = bytearray(len(_WRITE_HEADER_MASTER))
    while retry:
        with CSContext(CS):
            # Exchange header
            spi.write_readinto(
                _READ_HEADER_MASTER,
                _rw_header_slave
            )
            rx_read_bytes = (
                _rw_header_slave[4] << 8
            ) | _rw_header_slave[3]
            if _rw_header_slave[0] == 0x02 and rx_read_bytes > 0:
                # SPI is ready
                # avoid to read more data that size of the buffer
                if rx_read_bytes > size:
                    rx_read_bytes = size
                data = b'\xFF' * rx_read_bytes
                result = bytearray(rx_read_bytes)
                spi.write_readinto(data, result)
                break
            else:
                utime.sleep_us(50)
        retry -= 1

    # Add a small delay to give time to the BlueNRG to set the IRQ pin low
    # to avoid a useless SPI read at the end of the transaction
    utime.sleep_us(150)
    return result

def run():

    # reset
    RS.off()
    utime.sleep_us(5)
    RS.on()
    utime.sleep_us(5)

    # wait event "device ready"
    while True:
        if IR.value() and read() == b'\x04\xff\x03\x01\x00\x01':
            break

    # HCI_Read_Local_Version_Information: OGF: 0x04 OCF: 0x01 OPCODE: 0x1001
    param = b''
    ogf = 0x04
    ocf = 0x01
    opcode = ustruct.pack("<H", (ocf & 0x03ff) | (ogf << 10)) # OPCODE
    header = ustruct.pack("<B3s", 0x01, opcode)
    print(header, param)
    write(header=header, param=param)
    while True:
        if IR.value():
            hi = read()
            print("-------", hi)
            break

    # HCI_Read_Local_Supported_Features: OGF: 0x04 OCF: 0x03 OPCODE: 0x1003
    param = b''
    ogf = 0x04
    ocf = 0x03
    opcode = ustruct.pack("<H", (ocf & 0x03ff) | (ogf << 10)) # OPCODE
    header = ustruct.pack("<B3s", 0x01, opcode)
    print(header, param)
    write(header=header, param=param)
    while True:
        if IR.value():
            hi = read()
            print("-------", hi)
            break

run()

expected output:

➜  micropython (master) ✗ python3 -B tools/pyboard.py --device /dev/tty.usbmodem3167398C35352 /Users/damianomazzella/Downloads/test_uble.py
b'\x01\x01\x10\x00' b''
------- bytearray(b'\x04\x0e\x0c\x01\x01\x10\x00\x07\x071\x070\x00#\x00')
b'\x01\x03\x10\x00' b''
------- bytearray(b'\x04\x0e\x0c\x01\x03\x10\x00\x00\x00\x00\x00`\x00\x00\x00')
➜  micropython (master) ✗ 

Please try it and give me feedback.

waleckaw commented 4 years ago

It worked! I can't believe it. The issue was definitely the header format. For example, when I told you I sent the HCI_Read_Local_Supported_Features command, I was sending the header in a simple bytearray as it was given in the spec: OpCode = 0x1003. I can see now that, when the header gets packed into a ustruct, its formatting changes and it becomes 0x0103. I am not sure where or if that ever gets clarified in the BlueNRG-MS spec I linked above.

I can't thank you enough, especially because you mentioned this is an old project. Without your help I doubt I ever would have ever solved this problem. I am a relative newcomer when it comes to embedded python, and never would have thought to use a struct. Many thanks!

dmazzella commented 4 years ago

I'm glad that everything works.

Best regards, D.