Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 6 additions & 14 deletions Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -206,21 +206,13 @@ endmenu

menu "Expansion deck configuration"

config DECK_FORCE
string "Force load specified custom deck driver"
default "none"
help
A colon seperated list of custom drivers to force load or "none".

config DECK_BACKEND_ONEWIRE
bool "Enable OneWire deck discovery backend"
default y
help
Enable the OneWire deck discovery backend that scans for decks with
OneWire memory chips. Disable this if you want to ignore all
OneWire-based deck discovery, which might be useful for debugging.
menu "Deck discovery backends"
source "src/deck/backends/Kconfig"
endmenu

source src/deck/drivers/src/Kconfig
menu "Deck drivers"
source "src/deck/drivers/src/Kconfig"
endmenu

endmenu

Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ PYTHON ?= python3
ifdef WSL_DISTRO_NAME
CLOAD_SCRIPT ?= python.exe -m cfloader
else
CLOAD_SCRIPT ?= $(PYTHON) -m cfloader
CLOAD_SCRIPT ?= uvx --from git+https://github.com/bitcraze/crazyflie-clients-python cfloader
endif

DFU_UTIL ?= dfu-util

CLOAD_CMDS ?=
CLOAD_ARGS ?=
CLOAD_ARGS ?= -w radio://0/70/2M/E7E7E7E7E7

ARCH := stm32f4
SRCARCH := stm32f4
Expand Down
170 changes: 170 additions & 0 deletions docs/functional-areas/deckctrl_protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
title: DeckCtrl protocol specification
page_id: deckctrl_protocol
---

# DeckCtrl protocol specification

The DeckCtrl backend implements an I2C-based deck and control discovery mechanism that uses microcontrollers on deck boards to enable dynamic enumeration of multiple decks.
The microcontroller implementing the deckctrl protocol is exclusively used for that purpose, deck functionality are implemented on
another chip/microcontroller.

## Discovery Sequence

The DeckCtrl backend performs the following sequence during initialization:

### 1. Reset Phase (Address 0x41)

All deck controllers on the bus listen to the reset address. A read operation to this address causes all controllers to reset their I2C configuration and return to default state.

```c
i2cdevReadReg16(I2C1_DEV, 0x41, 0x0000, 2, dummy_buffer);
vTaskDelay(10); // Wait for controllers to restart
```
### 2. Listening Mode (Address 0x42)
After reset, unconfigured deck controllers must be told to enter listening mode. In this state, they monitor the bus for address assignment.
```c
i2cdevReadReg16(I2C1_DEV, 0x42, 0x0000, 2, dummy_buffer);
```

### 3. Enumeration

For each deck to be discovered:

#### 3a. Read CPU ID (Address 0x43, Register 0x1900)

We read the next unconfigured deck with the lowest ID.
The other decks will observe a bus collision and back-off, they will then be ready to be put back in listening mode in step 2.

```c
uint8_t cpu_id[12];
i2cdevReadReg16(I2C1_DEV, 0x43, 0x1900, 12, cpu_id);
```
*Note: This is the core of the discovery protocol, it uses the native anticollision behavior of
I2C to be able to detect decks one by one. I2C detects a collition if it tries to let the open-collector data line high
while another deck is pulling it low. The deck that wanted it high will detect the line low, assume a collision, and back-off*
#### 3b. Assign Address (Address 0x43, Register 0x1800)
Write a unique I2C address to the deck controller. The address range starts at 0x44:
```c
uint8_t deck_address = 0x44 + deck_count;
i2cdevWriteReg16(I2C1_DEV, 0x43, 0x1800, 1, &deck_address);
```

After receiving its address, the deck removes itself from the default addresses and begins responding only to its assigned address.

#### 3c. Read Deck Information (Assigned Address, Register 0x0000)

Read the deck's identification and capability information from its newly assigned address:

```c
uint8_t deck_info[21];
i2cdevReadReg16(I2C1_DEV, deck_address, 0x0000, 21, deck_info);
```
### 4. Repeat
Return to step 2 (listening mode) to discover the next deck. Continue until no more decks respond.
## I2C Address Map
| Address | Purpose | Description |
|---------|---------|-------------|
| 0x41 | Reset | Broadcast reset to all deck controllers |
| 0x42 | Listen | Put unconfigured controllers in listening mode |
| 0x43 | Default | Read CPU ID and assign unique address |
| 0x44-0x4F | Assigned | Individual deck addresses (up to 12 decks) |
The maximum number of decks is configured via `CONFIG_DECK_BACKEND_DECKCTRL_MAX_DECKS`.
## Memory Layout
### Register Map
Once configured, the deck-control behaves like an I2C memory with the following memory map:
| Register | Size | Description |
|----------|------|---------------------------------------------|
| 0x0000 | 21 | Deck identification information (Read-only) |
| 0x0020 | 2016 | ROM partitions |
| 0x1000 | 4 | GPIO |
| 0x1800 | 1 | Address assignment (write-only) |
| 0x1900 | 12 | CPU unique ID (read-only) |
### Deck Information Format (Register 0x0000)
The 21-byte deck information block contains:
| Offset | Size | Field | Description |
|--------|------|-------|-------------|
| 0x00 | 2 | Magic | Magic number 0xBCDC (big-endian) |
| 0x02 | 1 | Major Version | Firmware major version |
| 0x03 | 1 | Minor Version | Firmware minor version |
| 0x04 | 1 | Vendor ID | Deck vendor ID |
| 0x05 | 1 | Product ID | Deck product ID |
| 0x06 | 1 | Board Revision | Board revision character |
| 0x07 | 14 | Product Name | Null-terminated product name string |
#### Magic Number
All valid DeckCtrl decks must return the magic number **0xBCDC** in the first two bytes. This validates that the device is a proper DeckCtrl deck.
Format: Big-endian (0xBC at offset 0, 0xDC at offset 1)
#### Vendor ID and Product ID
The VID/PID pair uniquely identifies the deck type and is used to match with the appropriate deck driver in firmware. These should match the `vid` and `pid` fields in the `DeckDriver` structure.
#### Product Name
A human-readable name for the deck (up to 14 characters plus null terminator). This is used for logging and debugging.
## Backend Context
Each discovered DeckCtrl deck receives a `DeckCtrlContext` structure that contains backend-specific data:
```c
typedef struct deckCtrlContext_s {
uint8_t i2cAddress; // Assigned I2C address (0x44+)
} DeckCtrlContext;
```

Deck drivers can access this context through the `DeckInfo` structure passed to their `init()` function:

```c
void myDeckInit(DeckInfo *info) {
DeckCtrlContext *ctx = (DeckCtrlContext *)info->backendContext;
uint8_t address = ctx->i2cAddress;

// Use address for I2C communication with the deck
}
```
### ROM Partition formats (address 0x0020)
The space is split in partitions with the following format:
| Offset | Size | Field | Description |
|--------|--------------|---------|--------------------------------------------------|
| 0x00 | 2 | Length | Partition full size, 0x0000 if no more partition |
| 0x02 | 4 | Type | Partition type |
| 0x06 | *Length* - 6 | Data | Partition data |
At offset *length* the next partition is starting. A length of 0x0000 denote the end of the partition table.
A length between 1 and 5 included is invalid.
There are no partition type defined yet. This mechanism is designed to allow for future expansion.
### GPIO control
| Offset | Size | Field | Reset value | Description |
|---------|------|-----------|-------------|-------------------------------------------|
| 0x00 | 2 | Direction | 0x00 | GPIO Direction. 0 for input, 1 for output |
| 0x04 | 2 | Value | * | GPIO Value |
14 changes: 12 additions & 2 deletions docs/userguides/deck.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,19 @@ Deck drivers
Decks are enumerated automatically using a modular discovery system that supports multiple backends:

- **OneWire backend**: Reads deck information from One Wire (OW) memory soldered on the deck PCB
- **Forced backend**: Allows compile-time forcing of deck drivers via `CONFIG_DECK_FORCE`
- **DeckCtrl backend**: I2C-based intelligent discovery using deck controller microcontrollers
- **Forced backend**: Allows compile-time forcing of deck drivers via `CONFIG_DECK_FORCE` (for development)

The architecture is extensible: New discovery backends can be added for different communication protocols.
The architecture is extensible: New discovery backends can be added for different communication protocols. Each backend is called sequentially during system initialization to discover all connected decks.

### Discovery backend configuration

Backends can be enabled or disabled via KConfig options:
- `CONFIG_DECK_BACKEND_ONEWIRE` - Enable OneWire backend (typically always enabled)
- `CONFIG_DECK_BACKEND_DECKCTRL` - Enable DeckCtrl backend
- `CONFIG_DECK_BACKEND_DECKCTRL_MAX_DECKS` - Maximum number of DeckCtrl decks to enumerate (default varies by platform)

Discovery runs at startup in the order backends are registered. All enabled backends are queried to build the complete list of connected decks.

The Deck driver API uses a declarative syntax to register deck drivers and initialize them when the proper deck is detected through any of the discovery backends.

Expand Down
1 change: 1 addition & 0 deletions src/deck/api/Kbuild
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ obj-y += deck_constants.o
obj-y += deck_digital.o
obj-y += deck_spi3.o
obj-y += deck_spi.o
obj-$(CONFIG_DECK_BACKEND_DECKCTRL) += deckctrl_gpio.o
125 changes: 125 additions & 0 deletions src/deck/api/deckctrl_gpio.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* DeckCtrl GPIO backend for deck initialization and control
*
* These functions are intended to be called from deck drivers to
* power, initialize and control the bootloader state of decks
*
* copyright (C) 2025 Bitcraze AB
*/

#include <deck.h>
#include <string.h>

#include "deckctrl_gpio.h"
#include "deckctrl.h"
#include "i2cdev.h"

#include "FreeRTOS.h"
#include "semphr.h"

#define DEBUG_MODULE "DECKCTRL_GPIO"
#include "debug.h"


static bool get_i2c_address(DeckInfo* info, uint8_t* address) {
if (info->backendContext == NULL || strcmp(info->discoveryBackend->name, "deckctrl") != 0) {
return false;
}
*address = ((DeckCtrlContext*)info->backendContext)->i2cAddress;
return true;
}

bool deckctrl_gpio_set_direction(DeckInfo* info, DeckCtrlGPIOPin pin, bool output) {
if (pin >= DECKCTRL_GPIO_PIN_MAX) {
return false;
}

uint8_t i2c_address;
if (!get_i2c_address(info, &i2c_address)) {
return false;
}

char buffer[2];
// Read current direction register (16-bit)
if (!i2cdevReadReg16(I2C1_DEV, i2c_address, DECKCTRL_GPIO_DIRECTION_REG, 2, buffer)) {
return false;
}

// Convert bytes to 16-bit value (little endian)
uint16_t direction_reg = buffer[0] | (buffer[1] << 8);

// Set or clear the bit for this pin
if (output) {
direction_reg |= (1 << pin);
} else {
direction_reg &= ~(1 << pin);
}

DEBUG_PRINT("Setting GPIO pin %d direction to %s (reg=0x%04x)\n", pin, output ? "output" : "input", direction_reg);

// Convert back to bytes and write
buffer[0] = direction_reg & 0xFF;
buffer[1] = (direction_reg >> 8) & 0xFF;

bool result = i2cdevWriteReg16(I2C1_DEV, i2c_address, DECKCTRL_GPIO_DIRECTION_REG, 2, buffer);

return result;
}

bool deckctrl_gpio_write(DeckInfo* info, DeckCtrlGPIOPin pin, bool value) {
if (pin >= DECKCTRL_GPIO_PIN_MAX) {
return false;
}

uint8_t i2c_address;
if (!get_i2c_address(info, &i2c_address)) {
return false;
}

char buffer[2];
// Read current value register (16-bit)
if (!i2cdevReadReg16(I2C1_DEV, i2c_address, DECKCTRL_GPIO_VALUE_REG, 2, buffer)) {
return false;
}

// Convert bytes to 16-bit value (little endian)
uint16_t value_reg = buffer[0] | (buffer[1] << 8);

// Set or clear the bit for this pin
if (value) {
value_reg |= (1 << pin);
} else {
value_reg &= ~(1 << pin);
}

// Convert back to bytes and write
buffer[0] = value_reg & 0xFF;
buffer[1] = (value_reg >> 8) & 0xFF;

return i2cdevWriteReg16(I2C1_DEV, i2c_address, DECKCTRL_GPIO_VALUE_REG, 2, buffer);
}

bool deckctrl_gpio_read(DeckInfo* info, DeckCtrlGPIOPin pin, bool* value) {
if (pin >= DECKCTRL_GPIO_PIN_MAX || value == NULL) {
return false;
}

uint8_t i2c_address;
if (!get_i2c_address(info, &i2c_address)) {
return false;
}

char buffer[2];
// Read current value register (16-bit)
if (!i2cdevReadReg16(I2C1_DEV, i2c_address, DECKCTRL_GPIO_VALUE_REG, 2, buffer)) {
return false;
}

// Convert bytes to 16-bit value (little endian)
uint16_t value_reg = buffer[0] | (buffer[1] << 8);

// Extract the bit for this pin
*value = (value_reg & (1 << pin)) != 0;

return true;
}
4 changes: 3 additions & 1 deletion src/deck/backends/Kbuild
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
obj-y += deck_backend_onewire.o
obj-$(CONFIG_DECK_BACKEND_ONEWIRE) += deck_backend_onewire.o

# Only compile forced backend when DECK_FORCE is set to something other than "none"
ifneq ($(CONFIG_DECK_FORCE),"none")
ifneq ($(CONFIG_DECK_FORCE),"")
obj-y += deck_backend_forced.o
endif
endif

obj-$(CONFIG_DECK_BACKEND_DECKCTRL) += deck_backend_deckctrl.o
Loading