Skip to main content

PicoSlave is a dual I2C slave simulator for hardware integration testing

Project description

Latest Release pipeline status License: MIT

PicoSlave

PicoSlave is a USB controllable I2C slave device that features two independent I2C devices. Each I2C slave works on 256-bytes register spaces, which can be read and modified via a simple USB interface.

PicoSlave is run on the Raspberry Pi Pico board on the RP2040 ARM Cortex-M0+ microcontroller. The two I2C interfaces of the board can be used independently or wired together to operate on the same I2C bus.

Overview

                     ╓┉┉┉┉┉╖
                     ║     ║
               ╭─────║ USB ║─────╮
          (TX) ┥  1  ╙┉┉┉┉┉╜  40 ┝
          (RX) ┥  2 ╔╗        39 ┝
               ┥  3 ╚╝LED     38 ┝
               ┥  4           37 ┝
               ┥  5 ╔══╗      36 ┝
         ┌ SDA ┥  6 ║BS║      35 ┝
    I2C0 ┴ SCL ┥  7 ╚══╝      34 ┝
           GND ┥  8           33 ┝
         ┌ SDA ┥  9 ┏━━━━━━━┓ 32 ┝
    I2C1 ┴ SCL ┥ 10 ┃       ┃ 31 ┝
               ┥ 11 ┃RP2040 ┃ 30 ┝
               ┥ 12 ┃       ┃ 29 ┝
               ┥ 13 ┗━━━━━━━┛ 28 ┝
               ┥ 14           27 ┝
               ┥ 15           26 ┝
               ┥ 16           25 ┝
               ┥ 17           24 ┝
               ┥ 18           23 ┝
               ┥ 19           22 ┝
               ┥ 20   DEBUG   21 ┝
               ╰─────┰──┰──┰─────╯
                     S  G  S
                     W  N  W
                     C  D  D
                     L     I
                     K     O

Dependencies

Python

References

The following sources are used in this project:

Installation

Program the Raspberry Pi Pico

To program the firmware onto the PicoSlave, follow these steps:

  • obtain the latest PicoSlave firmware build
  • disconnect the Raspberry Pi Pico from USB
  • while holding the BOOTSEL button of the board, connect USB to the PC
    • the Raspberry Pi Pico should load as a mass storage device to the system
  • copy the picoslave.uf2 firmware file to the mass storage device
    • when copying is done, the Raspberry Pi Pico should reboot as PicoSlave

Configure the PicoSlave USB device

To grant userspace access to the PicoSlave USB device, the udev rules must be added to the system. Make sure that the user is part of the plugdev user group (default for Ubuntu and Raspberry Pi OS):

sudo cp ./contrib/99-picoslave.rules /etc/udev/rules.d/
sudo udevadm control --reload
sudo udevadm trigger --attr-match=subsystem=usb

Alternatively, the install script ./util/install.sh can be executed (it will ask for sudo privileges).

When installing picoslave as a pip package, the picoslave-install script is added to the path and can be used to install the udev rules as well.

Python picoslave package installation

PicoSlave comes as a python package (picoslave) which can be installed via pip. The package contains a library for operating on PicoSlave devices as well as a CLI for manual operation. The package can be installed from source only, currently:

python -m pip install git+https://gitlab.com/janoskut/picoslave.git

Usage

The PicoSlave allows the following operations to configure the two I2C interfaces slaves operations. All functions are available via the CLI as well as the Python library/package, or via the vendor specific USB interface.

Operation Args Description
config Configure the I2C interface for slave operation on the specified I2C slave address on the specified memory. The I2C memory is configured as a 2-dimensional array of length size and width width. Note that an interface that is not yet configured can still already be memory-programmed (read/write/clear).
iface The I2C interface (0 or 1).
addr The 7-bit I2C slave address to operate on. addr=0 deactivates the interface.
[size=256] The number of I2C adressable words in the memory, after which the internal address counter auto-increments. The maximum (and default) size is 256 words.
[width=1] The word size, in bytes. Allowed values are 1 (default), 2 and 4. Note that internally as well as from the USB interface (see read/write), the I2C memory is treated as a 1-dimensional array of size size*width. The word width defines the I2C-addressable sections. Hence when reading (or writing) from an address N from I2C to a memory with width=2, then the first read will yield mem[N*width] and the next read will yield mem[N*width+1]. In terms of endianness, the I2C transmission order can hence be seen as little-endian byte order, as the smaller memory address will be transmitted first.
read Read data from the I2C memory. When reading/writing data from/to the I2C memory, the memory is seen as a 1-dimensional array without respect to word widths and the result is a linear Byte array. Word interpretation has to be done by the API user.
iface The I2C interface (0 or 1).
addr The (raw) address to start reading from.
size The number of Bytes (not words) to read from the given addr.
write Write data into the I2C memory. See read for how the memory is seen as raw, 1-dimensional memory. As an example, with a I2C slave configured for word width=2, in order to have an I2C master read the word ABCD from address 0x24, one would have to write 2 bytes into the raw address 0x48: write(iface, addr=0x48, data=bytes([0xAB, 0xCD])). Note that data can be written into the I2C memory already before the slave is configured to operate on an I2C address.
iface The I2C interface (0 or 1).
addr The (raw) address to start writing data into.
data The data bytes (not words) to write into the given addr.
clear Reset all data in the I2C memory to 0 or a given value.
iface The I2C interface (0 or 1).
value=0x00 The reset value of the I2C memory.
stat Get a read/write statistics report for the selected memory section. The statistics report has a maximum of size entries (see config operation) and has an entry for each addressable I2C memory address (not raw address). It gives a report for how often each I2C memory address has been read or written on the I2C interface. This report can be used to reverse-engineer or mock an I2C slave device in operation, when the internals of the I2C master or the specification of the to-be-mocked I2C slave are not known. The result data is a byte array with size entries, each in the (little-endian) format {#read}{#write}, where #read and #write are numbers of size 4 each.
iface The I2C interface (0 or 1).
addr The I2C memory address (not raw address) to start reading the report for.
size The number of statistics report entries to read (from addr).
reset Reset the PicoSlave MCU. This leads to all I2C configuration and memory to be reset, and also the USB device to be re-enumerated.

CLI Usage

The PicoSlave can be configured using the CLI, which can be run from the installed package, or from source (cloned repository):

# from source
./picoslave/picocli.py -h
python picoslave/picocli.py -h
# from installed package
picoslave -h
picoslave <command> -h

Some example CLI usages:

picoslave scan                       # scan for PicoSlave USB devices
picoslave config 0 0x16              # configure I2C0 for 7-bit address 0x16
picoslave config 1 0x23              # configure I2C1 for 7-bit address 0x23
picoslave write 0 0x10 aabbccddeeff  # write 6 bytes to memory address 0x10 of I2C0 slave
picoslave read 0 0x10 6              # read 6 bytes from memory address 0x10 from 12C0 slave
picoslave clear 0                    # clear the memory of I2C0 slave
picoslave stat 0                     # get a full statistics dump for read/write access to I2C0 slave
picoslave reset                      # reset the PicoSlave USB device

Note that the CLI allows abbreviations for commands, e.g. c for config, etc.

Library Usage

The Python library to access PicoSlave devices can be used in an almost identical way as the CLI:

from picoslave.picoslave import PicoSlave

picoslave = PicoSlave()
picoslave.config(iface=0, slave_address=0x16)
picoslave.write(iface=0, mem_addr=0x10, data=b'aabbccddeeff')
res: bytes = picoslave.read(iface=0, mem_addr=0x10, size=6)
print(' '.join(f'{b:02X}' for b in res))
picoslave.clear(iface=0)
picoslave.statistics(iface=0)
picoslave.reset()

I2C - Raspberry Pi

FIXME

USB Protocol

Wire packet

The top level wire packet wraps the specific packets (host and response) into a simple header, which consists of a length and a checksum:

[LEN] [PAYLOAD] [CRC]
Field Size Description
LEN 4 Length of the packet, including [CRC]
PAYLOAD N Payload data of variable length, see Host packet and Response packet
CRC 2 16-bit CRC-CCITT checksum over [PAYLOAD] initialized with 0x8408

Host packet:

[CMD=config] [IFACE]   [ADDR]   [SIZE=N] [WIDTH]
[CMD=read]   [IFACE]   [ADDR]   [SIZE=N]
[CMD=write]  [IFACE]   [ADDR]   [SIZE=N] [DATA]
[CMD=clear]  [IFACE]   [ADDR]   [SIZE=0] [DATA]
[CMD=stat]   [IFACE]   [ADDR]   [SIZE=N]
[CMD=info]   [IFACE=0] [ADDR=0] [SIZE=0]
[CMD=reset]  [IFACE=0] [ADDR=0] [SIZE=0]
Field Size Description
CMD 1 command to send to the device
IFACE 1 I2C interface number
ADDR 2 7-bit address to assign to I2C interface, or (16 bit) memory address to read/write
SIZE 2 'config': memory size to use (max 256)
'read': number of bytes to read from
'write': number of bytes to write (must match len(DATA))
DATA N data to write
WIDTH 1 I2C memory addressable word width. Allowed are 1, 2 and 4 bytes.

Commands:

CMD Value Description
config 0xA0 configure the slave given with IFACE to operate on the address ADDR, or
deactivate the slave when ADDR=0. SIZE specifies the used memory at
which the auto-increment overflows.
read 0xA1 read SIZE bytes from ADDR from the slave given with IFACE.
write 0xA2 write SIZE bytes of DATA to the slave given with IFACE.
Note that SIZE must match len(DATA).
clear 0xA3 clear all memory and statistics for the given IFACE.
Reset memory to the value given at ADDR.
stat 0xA4 read SIZE many statistics from ADDR. Returns a statistics struct for each address.
info 0xB0 obtain device information from picoslave, see "Info Response"
reset 0xBF reset the target device. Note that IFACE and ADDR are ignored,
but need to be transmitted as 0.

Response packet

[CODE=0] [SIZE=N] [DATA]
[CODE>0] [SIZE=0]
Field Size Description
CODE 0 response or error code
SIZE 2 number of received data bytes in DATA
DATA N received data (optional)

Response Codes:

Code Description
0 OK (no error)
1 CRC_ERROR
2 INVALID_PACKET
3 INVALID_REQUEST
4 INVALID_INTERFACE
5 INVALID_ADDRESS
6 INVALID_SIZE
7 MEMORY_ERROR
8 OPERATION_FAILED

Info Response

With CMD=info the DATA part of the response is a semicolon separated ASCII string with the following segments:

[serial];[firmware];[protocol];[ifaces]
Segment Description
serial the device unique serial
firmware exact firmware version
protocol version of this USB protocol
ifaces number of I2C interfaces

Statistics Response

With CMD=stat the DATA part of the response is structured data of SIZE statistics entries, each corresponding to the memory ADDR requested. A statistics entry has the format:

[READ_CNT][WRITE_CNT]

Where READ_CNT and WRITE_CNT are of size 4 each. They tell how many read/write accesses have been made to that register address.

Development

Toolchain

sudo apt install cmake gcc-arm-none-eabi

Install SDK

git clone https://github.com/raspberrypi/pico-sdk
export PICO_SDK_PATH="$(pwd)/pico-sdk"

Debugging Raspberry Pi Pico

Wiring:

Compiling and deploying picoprobe

picoprobe is the firmware which turns a Raspberry Pi Pico into a programmer for other Raspberry Pi Pico's. The picoprobe binary needs to be compiled and loaded onto a Raspberry Pi Pico only once:

# Picoprobe and picotool
git clone https://github.com/raspberrypi/picoprobe.git --depth=1
cmake -S picoprobe -B picoprobe/build
cmake --build picoprobe/build -j$(nproc)
# hold the BOOTSEL button and connect the Pico USB
# assuming the Pico mounts at `/media/<user>/RPI-RP2`
cp picoprobe/build/picoprobe.uf2 /media/$(whoami)/RPI-RP2

OpenOCD setup

Compile & Install OpenOCD

sudo apt install libtool libusb-1.0-0-dev
git clone "https://github.com/raspberrypi/openocd.git" --branch "picoprobe" --depth=1
cd openocd
./bootstrap
./configure --enable-ftdi --enable-sysfsgpio --enable-bcm2835gpio --enable-picoprobe
make -j$(nproc)
sudo make install

Configure OpenOCD

In the openocd directory:

sudo cp ./contrib/60-openocd.rules /etc/udev/rules.d/
sudo udevadm control --reload
sudo udevadm trigger --attr-match=subsystem=usb

Test OpenOCD

openocd -f interface/picoprobe.cfg -f target/rp2040.cfg
# should show something, but no errors
ctrl+c

Program a binary

openocd -f interface/picoprobe.cfg -f target/rp2040.cfg -c "program build/picoslave.elf verify reset exit"

Or use the OpenOCD helper script picoflash.sh:

# program
./util/picoflash.sh build/picoslave.elf
# or e.g. reset target
./util/picoflash.sh --reset

Or even easier, use the shell tools:

source util/shellutil.sh
flash
reset

Python Development

QA:

pip install -r requirements-dev.txt

flake8 picoslave
pycodestyle picoslave
mypy picoslave --strict

System Testing

-> See ./test/behave/README.md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

picoslave-1.2.0.tar.gz (22.6 kB view hashes)

Uploaded Source

Built Distribution

picoslave-1.2.0-py3-none-any.whl (17.9 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page