Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 3 Next »

Goal

Facilitate hardware triggering firmware actions.

E.g.

  • Read ADC

  • Interact with GPIO

  • SPI

  • I2C

  • UART

  • CAN messages

  • PWM - later

Requirements

MVP

  • Python library to be accessed via python shell

  • Call functions like

gpio_set/get(port, pin) -> bool # re-init pin every time this is called
adc_read_raw/converted(gpio_address) -> int
spi_exchange(spi_port, mode, tx: list, rx_len, baudrate=None, cs=None) -> rx: list # re-init spi for given port every call
i2c_write(i2c_port, addr, tx: list, reg=None) -> None
i2c_read(i2c_port, addr, rx_len, reg=None) -> rx: list
can_send_raw(id, data: list) -> None
load_dbc(path) -> None  # could be auto-loaded in 'make babydriver'
can_send_msg(msg_name, *data) -> None  # auto checks based on dbc if valid

Details

Two components: python library, and firmware project.

Communication between the two is all encoded as CAN messages.

Unfortunately, our CAN tooling only allows 64 CAN messages, most of which we’d like to use for the car itself. So we’re going to cram everything into one CAN message, SYSTEM_CAN_MESSAGE_BABYDRIVER. This sucks because we can’t use our CAN library’s nice packing/unpacking features, but it is what it is.

General format of the babydriver CAN message:

uint8 id
7 * uint8: message-defined data

That is, the CAN message consists of 8 uint8s, the first of which is a babydriver-specific ID. The other 7 uint8s are generic data fields, the meaning of which is determined by the ID. Babydriver message IDs will be defined in an enum in C and a series of constants in Python.

Any multi-byte fields in a Babydriver CAN message (like using two uint8s to represent a uint16) should be in little-endian order to match the native STM32F0xx byte order. That is, the least significant byte should be specified first and the most significant byte last.

We’ll have a generic status message sent by the firmware project at the end of any operation, allocating the uint8s as follows:

uint8 id = 0
uint8 status
6 * uint8 unused 

The status will be one of the status codes from status.h. The firmware project will send this message to the Python project at the end of every operation to signify that the operation is done and give the status.

More babydriver messages will be specified in tickets and/or on Confluence when they come up. Here are a couple examples.

Implementing gpio_set

The flow for gpio_set will look like this.

Python project sends a babydriver CAN message that looks like:

uint8 id = X  // some constant defined in C/Python
uint8 port    // port of gpio pin to set
uint8 pin     // pin number of gpio pin to set
uint8 state   // 1 or 0 to set to high or low respectively

The Python project then blocks until it sees a status babydriver CAN message or it times out.

The firmware project receives the message, initializes gpio on the pin, sets the appropriate gpio pin, and responds back with a status message indicating success or failure.

Upon receiving the status message, the Python project returns from gpio_set(), raising an exception if the status code was non-zero.

Implementing SPI functions

SPI, I2C, etc are more special because they require transferring more data than can be fit in the 7 data uint8s we’re given. Thus, we need multiple messages.

To start a SPI exchange, the Python project will send these two messages:

Metadata message 1:
uint8 id = X   // some constant
uint8 spi_port // 0 or 1 for SPI_PORT_1 or SPI_PORT_2
uint8 tx_len_low  // low byte of tx_len (a uint16)
uint8 tx_len_high // high byte of tx_len
uint8 rx_len_low  // low byte of rx_len (another uint16)
uint8 rx_len_high // high byte of rx_len
uint8 cs_port // port of chip select gpio pin
uint8 cs_pin  // pin of chip select gpio pin
Metadata message 2:
uint8 id = X  // some constant
4 * uint8 baudrate // a uint32

Note that since we’re representing everything as uint8s, we have to break up multiple-byte fields into their individual bytes and reconstruct them. This sucks, but it is what it is. Again, we use little-endian order to match the native STM32F0xx byte order.

The Python project will then send enough generic “babydriver data” messages (another type of message with just 7 uint8s) to cover tx_len. The firmware project will send back enough generic data messages to cover the rx_len bytes received, then the status message.

Task list

  • setup project structure

    • make new C project

      • init CAN, write main() method, other setup (not registering specific rx handlers)

    • includes a scripts folder with python script

    • setup makefile to allow make babydriver

      • program babydriver

      • puts you in a python shell with baby driver library loaded

  • write simple CAN abstraction layer

    • sending message, waiting for message, initializing CAN

    • have simple representation of can message

    • the pack() function takes in a list of tuples (len_in_bytes, val)

  • setup gpio functions in python

    • refer to this doc for details on flow

  • setup gpio functions in C and create CAN message in codegen-tooling

    • refer to this doc for details

  • etc. for other python and C implementations of firmware functions

  • No labels