PicoSlave is a dual I2C slave simulator for hardware integration testing
Project description
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.
Features
- linear register space:
- configurable for different word sizes (1, 2, 4)
- read/write accessible from I2C and from USB
- statistics function to count I2C read/write register accesses
- blocker mode to simulate SDA- or SCL-stuck condition
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:
- I2C slave based on vmilea/pico_i2c_slave
- rxi/log.c is used as a submodule for logging
- Using TinyUSB
Installation
Program the Raspberry Pi Pico
To program the firmware onto the PicoSlave, follow these steps:
- obtain the latest PicoSlave firmware build: stable (download) | develop (download)
- 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
- when copying is done, the Raspberry Pi Pico should reboot as
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 (or other functions) 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). | |
function |
0x00 : Reset the interface for no operation and release the IO.0x01..0x7F : I2C slave function. The 7-bit I2C slave address to operate on.0xF1 : Blocker function to hold the SCL and/or SDA signal low to GND . |
|
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/__main__.py -h
python -m picoslave -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
# blocker function
picoslave config -f blocker 0 --signals scl sda # block on both signals
picoslave config -f reset 0 # reset the interface to release the blockage
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()
# blocker function
picoslave.config_blocker(iface=0, scl=True, sda=True) # block on both signals
picoslave.config_blocker(iface=0, scl=True) # release the blockage on sda
picoslave.config_reset(iface=0) # reset the interface to release the blockage
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] [DATA]
[CMD=read] [IFACE] [ADDR] [SIZE=N]
[CMD=write] [IFACE] [ADDR] [SIZE=N] [DATA]
[CMD=clear] [IFACE] [ADDR] [SIZE=0]
[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 | CMD=read : number of bytes to readCMD=stat : number of statistic entries to readCMD=config/write : number of bytes to write (must match len(DATA) ) |
DATA |
N | CMD=config : configuration structure, see "Configuration Data Structures"CMD=write : data to write |
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 . |
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 , to address ADDR . 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. |
Configuration Data Structures
Slave configuration structure
Field | Size | Description |
---|---|---|
SIZE |
2 | 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 |
2 | 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. |
Blocker configuration structure
Field | Size | Description |
---|---|---|
SCL |
1 | hold the SCL signal low to GND |
SDA |
1 | hold the SDA signal low to GND |
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:
- https://hackaday.io/project/177198-pi-pico-picoprobe-and-vs-code/details
- https://www.digikey.be/en/maker/projects/raspberry-pi-pico-and-rp2040-cc-part-2-debugging-with-vs-code/470abc7efb07432b82c95f6f67f184c0
Compiling and deploying picoprobe
picoprobe
(by "raspberrypi") is the firmware which
turns a Raspberry Pi Pico into a programmer for other Raspberry Pi Pico's.
The picoprobe
binary needs to be flashed onto a Raspberry Pi Pico only once.
- latest build (built by us): picoprobe.uf2 (download)
# 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for picoslave-2.0.0.dev1-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 83fe2fc0805658f79c887c5b81f63d9bb4845db9f6dd12bdd6ec30c28de9efff |
|
MD5 | 78a580c6665a0c2197760c056fc9804e |
|
BLAKE2b-256 | de946782d51d1ddb3b40a20a1ffdd04eb91e186d2134cc9495044ad3629ee9a9 |