# kbsp

The Kode Dot as constants: pins, I2C addresses, screen, battery and owners, plus the sdkconfig.board and partitions.csv every firmware builds with.

## What it is

kbsp is the board support package, and it describes the Kode Dot. It holds:

- every GPIO number and I2C address
- the size and shape of the screen
- the supply rails
- the numbers of the battery
- the name of the product
- the table of who owns which device

It also carries two files that every firmware reads when it is built: `sdkconfig.board`, the build options that state the chip and wiring facts, and `partitions.csv`, the map of the 32 MB flash.

kbsp sits at the bottom of the layers and depends on nothing. It describes and never acts: no code in it touches hardware. khal starts the buses with its numbers, the drivers address their chips with its constants, and each Active Object registers in its ownership table.

That gives it a placement test. A file that knows a pin belongs here. A file that knows a part number belongs in kdrv.

A pin number or an I2C address is defined only here. Layer check 3 of `tools/check_layers.sh` fails when a `KBSP_PIN_*`, `KBSP_I2C_ADDR_*` or `KBSP_IOEXP_*` is defined anywhere else (`sdk/design-rules`).

## How it is connected

```mermaid
flowchart LR
  subgraph readers["Who reads kbsp"]
    khal["khal<br/>I2C bus, GPIO, expansion header"]
    kdrv["kdrv<br/>chip drivers"]
    kaos["kaos<br/>kaos_claim()"]
  end
  subgraph kbsp["kbsp"]
    consts["Pins, addresses,<br/>owners"]
    build["sdkconfig.board<br/>partitions.csv"]
  end
  khal --> consts
  kdrv --> consts
  kaos -- "kbsp_claim()" --> consts
  build --> fw["Every firmware build"]
  class kaos ao
  class build store
  class fw app
```

*Who reads kbsp*

The component has two sources in C and everything else is `#define`s. The expansion header tables `KBSP_EXP[]` and `KBSP_EXP_CTRL[]` live in `kbsp_exp.c`, because a table in a header would be copied into every file that includes it. The ownership table lives in `kbsp_owners.c` for the same reason.

### Ownership takes no lock

Ownership is a record. Each owner claims its resources in its initial transition, through `kaos_claim()`. That wrapper calls `kbsp_claim()`, and when a second owner asks for the same resource it prints `kodeOS: <resource> claimed by both <first> and <second>` and stops the boot.

Each slot is an atomic pointer changed by compare-and-swap, so two Active Objects on two cores cannot both win. The table does not stand between an owner and its device. The Active Object keeps the device safe by handling one event at a time.

An owner owns a **whole device**. The TCA6408A expander serves four parts, but it has one input register and one interrupt line, so AO_IoExp owns the chip and everyone else asks it by event.

### The build files follow the board

Every project reads `<sdk>/components/kbsp/sdkconfig.board` **first**, then its own `sdkconfig.defaults`, through `SDKCONFIG_DEFAULTS`. A later file wins, so a project can restate an option, with no warning.

A project spells out two board facts itself, because both are needed before the shared file can be fetched:

- `set(IDF_TARGET esp32p4)` before `project()`.
- `CONFIG_PARTITION_TABLE_CUSTOM_FILENAME`. An app beside the SDK fetched as a component points it at `managed_components/kodediy__kbsp/partitions.csv`.

## Specifications

### The board

The facts about the chip, the glass and the rails.

| Thing | Value |
| --- | --- |
| Product, revision | `KBSP_PRODUCT_NAME` "Kode Dot", `KBSP_BOARD_REV` "DVT". `KBSP_ID_PRODUCT` "KODE-DOT" is what `info` prints as `product=`. `KBSP_ID_PREFIX` "KODE" starts every Kode serial and is the same for every Kode product |
| Main chip | ESP32-P4, two RISC-V cores at 400 MHz. Revision v3.2 on the Kode Dot |
| Flash | 32 MB NOR, GD25Q256E, JEDEC `0xC84019` |
| PSRAM | 32 MB in package, HEX mode, 250 MHz |
| Screen | CO5300 AMOLED, `KBSP_LCD_H_RES` 410 by `KBSP_LCD_V_RES` 502 pixels over MIPI-DSI. Corners are rounded with `KBSP_LCD_CORNER_R_PX` 114 pixels, so anything in a corner is not on the glass |
| DPHY rail | `KBSP_LCD_DPHY_LDO_CHAN` 3 at `KBSP_LCD_DPHY_LDO_MV` 2500, up before any MIPI traffic |
| SD IO rail | `KBSP_SD_LDO_CHAN` 4, the supply of the SDMMC pins. The card's own supply is a pin, `KBSP_PIN_SD_PWR` |
| microSD defaults | `KBSP_SD_WIDTH` 4 lines, `KBSP_SD_FREQ_KHZ` 40000 |
| C5 link | `KBSP_C5_SDIO_WIDTH` 4, `KBSP_C5_SDIO_KHZ` 40000. `kdrv_c5` asserts at compile time that `sdkconfig.board` agrees |
| Crystals | `KBSP_XTAL_MAIN_HZ` 40000000 (X1). `KBSP_XTAL_32K_MHZ` 32768000 (X3), in **milli-hertz**. `KBSP_XTAL_32K_TOL_MHZ` 327680, a window of 1 %, wide on purpose: the chip counts every frequency against X1, so a tighter bound could not be judged on the board |
| Not in kbsp | GPIO0 and GPIO1 carry the 32.768 kHz crystal (`XTAL_32K_N`, `XTAL_32K_P`), so no code may use them as pins |

### The controllers kodeOS owns

The chip routes most controllers to any pin, so which controller drives a device is a board choice. The numbers are ESP-IDF's own: for SPI and the ADC that is one less than the chip's name, so SPI2 is `SPI2_HOST`, 1. Each driver turns its number into the IDF type beside a `_Static_assert` that the chip has it.

| Name | Value | Controller | Owner |
| --- | --- | --- | --- |
| `KBSP_I2C_BOARD` | 0 | I2C0, the board bus | `khal_i2c` |
| `KBSP_SPI_NFC` | 1 | SPI2 | the ST25R3916B |
| `KBSP_I2S_CODEC` | 0 | I2S0 | the ES8311 |
| `KBSP_ADC_EXP` | 0 | ADC1 | the analog positions of J3 |
| `KBSP_ADC_RFID` | 1 | ADC2 | the demodulated 125 kHz line |
| `KBSP_EXP_I2C_ID` | 1 | I2C1 | an app, through `khal_exp_ctrl()` |
| `KBSP_EXP_SPI_ID` | 2 | SPI3 (`SPI3_HOST`) | an app, through `khal_exp_ctrl()` |
| UART0 to UART4 | `KBSP_EXP_UART_LAST` 4 | five UARTs | an app. UART0 is on offer only while the console is not a UART, which is the case on the Kode Dot |

`KBSP_EXP_CTRL_COUNT` is 7 on the board: I2C1, SPI3 and five UARTs. The LP controllers reach only the LP pins and are not offered. The chip has one continuous ADC engine for both units and the RFID driver opens it for each read window, so J3's analog positions are read one sample at a time.

### The I2C bus

One shared bus: SDA on `KBSP_PIN_I2C_SDA` GPIO4, SCL on `KBSP_PIN_I2C_SCL` GPIO5, at `KBSP_I2C_FREQ_HZ` 400000. The chips on it:

| Constant | Address | Chip |
| --- | --- | --- |
| `KBSP_I2C_ADDR_LED` | `0x30` | KTD2026 RGB LED driver (U8) |
| `KBSP_I2C_ADDR_IOEXP` | `0x20` | TCA6408A expander (U24), ADDR tied low |
| `KBSP_I2C_ADDR_IMU` | `0x6A` | QMI8658A (U10), SA0 tied high |
| `KBSP_I2C_ADDR_MAG` | `0x2C` | QMC5883P (U5), a chip of its own on the bus, not behind the IMU |
| `KBSP_I2C_ADDR_CODEC` | `0x18` | ES8311 |
| `KBSP_I2C_ADDR_CHARGER` | `0x6B` | BQ25896 |
| `KBSP_I2C_ADDR_GAUGE` | `0x64` | CW2217 |
| `KBSP_I2C_ADDR_HAPTIC` | `0x5A` | AW86233 (U7), on the antenna PCB. The chip fixes the address |
| `KBSP_I2C_ADDR_TOUCH` | `0x15` | CST820, on the panel module of supplier A |

kbsp has no axis map for the IMU or the magnetometer. The drivers return each part's own axes.

### The expander

The eight pins of the TCA6408A serve the buttons, the motion sensor, the touch controller and the supply of the panel.

- Its interrupt line is `KBSP_PIN_IOEXP_INT` GPIO6, active low, pulled up by R24 (10 kΩ).
- Its RESET is tied high through R117, so a reset of the P4 never resets the expander.
- `KBSP_IOEXP_DIR_INPUTS` is `0x7F`: P0 to P6 are inputs, P7 is an output.

The eight pins:

| Pin | Constant | Net | Pull-up |
| --- | --- | --- | --- |
| P0 | `KBSP_IOEXP_BIT_BTN_BOT` | BUTTON_BOT (SW2), also the QON pin of the BQ25896 | **TBD**: see below |
| P1 | `KBSP_IOEXP_BIT_PAD_UP` | PAD_TOP (SW3) | R39, 10 kΩ |
| P2 | `KBSP_IOEXP_BIT_PAD_LEFT` | PAD_LEFT (SW4) | R40, 10 kΩ |
| P3 | `KBSP_IOEXP_BIT_PAD_DOWN` | PAD_BOTTOM (SW5) | R41, 10 kΩ |
| P4 | `KBSP_IOEXP_BIT_PAD_RIGHT` | PAD_RIGHT (SW6) | R42, 10 kΩ |
| P5 | `KBSP_IOEXP_BIT_IMU_INT1` | INT1 of the QMI8658A, driven push-pull by the chip | none, so the driver makes INT1 a driven output |
| P6 | `KBSP_IOEXP_BIT_TP_INT` | touch interrupt, input | on the panel module |
| P7 | `KBSP_IOEXP_BIT_DSP_PWR` | VCI_EN, **the supply of the display** (R48 holds it low) | none |

All six buttons are SKRMAAE010 and active low. The sixth, `KBSP_PIN_BUTTON_TOP` GPIO35, is the only one on a P4 pin, and the driver also turns on the internal pull-up.

If P7 becomes an input the screen turns off, so AO_IoExp never writes P7 unless AO_Display asks.

**TBD**: the pull-up of BUTTON_BOT and BUTTON_TOP. The validation pin map lists external 10 kΩ resistors R38 to R42 on the five expander buttons, while a read of the schematics puts R38 on BUTTON_TOP and leaves BUTTON_BOT to the 200 kΩ internal pull-up of the charger's QON pin. A measurement on the board would settle it.

### Other pins

The rest of the pins, by part.

| Part | Pins | Notes |
| --- | --- | --- |
| Display and touch reset | `KBSP_PIN_DSP_TP_RST` GPIO7 | Active low. The panel and the touch controller share it |
| microSD | SDMMC slot 0: D0 to D3 on GPIO39 to 42, CLK GPIO43, CMD GPIO44 | Power is `KBSP_PIN_SD_PWR` GPIO22, active low through Q1. Card detect is IO6 of the C5 (`KBSP_C5_IO_SD_CD`) |
| USB-Serial/JTAG | GPIO24 (D-), GPIO25 (D+) | The console and the flasher |
| Magnetic pads | GPIO26, GPIO27 | The second full-speed USB pair, used by the dock console. kbsp names no constant: the PHY has fixed pins |
| Audio, I2S to the ES8311 | MCLK GPIO36, SCLK GPIO34, LRCK GPIO37, DSDIN GPIO38 (P4 to codec), ASDOUT GPIO33 (codec to P4) | The NS4150B amplifier enable is IO4 of the C5 (`KBSP_C5_IO_PA_CTRL`). See the first gotcha |
| Infrared | IO24 (TX) and IO23 (RX) of the C5 | `KBSP_C5_IO_IR_TX`, `KBSP_C5_IO_IR_RX`. The P4 has no infrared pins |
| NFC, ST25R3916B, SPI mode 1 | CS GPIO28, IRQ GPIO29, CLK GPIO30, MOSI GPIO31, MISO GPIO32 | The reader is alone on SPI2 |
| RFID, 125 kHz | OUT GPIO47, PULL GPIO45, CARRIER GPIO46, RF_IN GPIO54 | RF_IN is ADC2 channel 5. PULL rests high through R11 and a high level shunts the detector, so the driver holds it low only while it listens. CARRIER is an input and the P4 never drives it |
| ESP32-C5 | EN GPIO23 (high is running). SDIO: CLK GPIO50, CMD GPIO51, D0 to D3 on GPIO49, 48, 53, 52 | `kdrv_radio` asserts that `CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE` equals `KBSP_PIN_C5_EN`. `KBSP_C5_IO_PWR_EXT` is IO5, the enable of the +3.3V_EXT rail of J3, on unless driven |

The RFID input is on ADC2 because GPIO23 is also ADC1 channel 7, and a read on ADC1 there would reconfigure the enable line of the C5 and stop the radio.

### The expansion header

J3 is a 2 by 10 header. It brings out GPIO8 to GPIO21 as `KBSP_PIN_EXP_1` to `KBSP_PIN_EXP_14`, each through 68 Ω, with the 3.3 V and 5 V rails and ground.

`KBSP_EXP[]` has `KBSP_EXP_COUNT` 14 entries, indexed by position minus one. Each is a `kbsp_exp_t`: `gpio`, `adc_chan` (or `KBSP_EXP_NO_ADC`, `0xFF`) and `lp`.

Every position is digital and matrix-routed. The ADC channel is the GPIO minus 16, and the LP pins are the GPIOs at or below 15. No position is both.

The 14 positions:

| Position | J3 pin | GPIO | Extra | ADC1 channel |
| --- | --- | --- | --- | --- |
| EXP1 | 3 | 8 | LP | none |
| EXP2 | 4 | 9 | LP | none |
| EXP3 | 5 | 10 | LP | none |
| EXP4 | 6 | 11 | LP | none |
| EXP5 | 7 | 12 | LP | none |
| EXP6 | 8 | 13 | LP | none |
| EXP7 | 9 | 14 | LP | none |
| EXP8 | 12 | 21 | ADC | 5 |
| EXP9 | 13 | 19 | ADC | 3 |
| EXP10 | 14 | 20 | ADC | 4 |
| EXP11 | 15 | 17 | ADC | 1 |
| EXP12 | 16 | 18 | ADC | 2 |
| EXP13 | 17 | 15 | LP | none |
| EXP14 | 18 | 16 | ADC | 0 |

The table has no bit for the touch-sensing inputs of the chip, because kbsp has no trusted list of which GPIO each channel uses.

### The battery

The cell is a 1S Li-ion pouch cell, type 603048 at 1000 mAh, on connector J7 with the shunt R75 in series. These numbers belong to the cell and the shunt, so none of them sits in a driver. AO_Power writes the charge profile into the BQ25896 at every boot.

The constants, in `kbsp_cell.h`:

| Constant | Value | Meaning |
| --- | --- | --- |
| `KBSP_CELL_RSENSE_MOHM` | 10 | The shunt that the CW2217 reads. Charge reads positive, the same sign as the charger |
| `KBSP_CELL_R_INT_MOHM` | 180 | Internal resistance, for `OCV = V - I * R_int` |
| `KBSP_CELL_OCV_MV`, `KBSP_CELL_OCV_PCT` | 13 points | The curve from 3300 mV = 0 % to 4200 mV = 100 % |
| `KBSP_CELL_VCUT_MV` | 3000 | Discharge cut-off |
| `KBSP_CELL_VTRIP_MV` | 4280 | Over-voltage trip of the protection board. A reading outside 3000 to 4280 mV is not a battery reading |
| `KBSP_CELL_CAPACITY_MAH` | 1000 | Capacity |
| `KBSP_CELL_VREG_MV` | 4192 | Charge voltage, 8 mV under the pack's 4.20 V |
| `KBSP_CELL_ICHG_MA` | 512 | Fast charge, 0.5 C |
| `KBSP_CELL_IPRECHG_MA`, `KBSP_CELL_ITERM_MA` | 64, 64 | Pre-charge and termination, the chip's floor |
| `KBSP_CELL_ICHG_MAX_MA` | 896 | The most `charge_ma` may ask for |
| `KBSP_CELL_BOOST_LIM_CODE` | 0 | BOOST_LIM code 0 is 0.5 A, which keeps the draw under the pack's peak |
| `KBSP_CELL_BOOST_HOT_CODE` | 1 | BHOT code 1, about 55 °C |
| `KBSP_CELL_BOOST_MIN_MV` | 3600 | Under this, a boost request is refused |
| `KBSP_CELL_BOOST_STOP_MV` | 3300 | Under this, a running boost stops |

All the charge values sit on the grid of the BQ25896's registers, so nothing is rounded on the way in (`sdk/kdrv/bq25896`).

### The owners

`kbsp_res_t` lists 25 resources. The column on the right is who claims it in the SDK.

| Resource | What it is | Claimed by |
| --- | --- | --- |
| `KBSP_RES_LED` | KTD2026 at `0x30` | AO_LedMgr |
| `KBSP_RES_PANEL` | MIPI-DSI and the CO5300 | AO_Display |
| `KBSP_RES_TOUCH` | CST820 at `0x15` | AO_Touch |
| `KBSP_RES_IOEXP` | TCA6408A at `0x20` and its interrupt line | AO_IoExp |
| `KBSP_RES_BTN_TOP` | GPIO35, the only button on a P4 pin | AO_Buttons |
| `KBSP_RES_SDMMC` | the microSD slot | AO_Storage |
| `KBSP_RES_FATFS` | the internal `storage` partition | AO_Storage |
| `KBSP_RES_OTA_SLOT` | the inactive kodeOS slot | AO_AppMgr |
| `KBSP_RES_OTADATA` | the `otadata` partition: which firmware starts | AO_Boot |
| `KBSP_RES_APPS_REGION` | the `apps` partition: directory and images | AO_AppMgr |
| `KBSP_RES_APPS_BOOTREQ` | the boot-request sector: which app starts next | AO_Boot |
| `KBSP_RES_APP_PENDING` | the `app_pending` mailbox partition | AO_AppMgr |
| `KBSP_RES_PMIC` | BQ25896 at `0x6B` | AO_Power |
| `KBSP_RES_GAUGE` | CW2217 at `0x64` | AO_Power |
| `KBSP_RES_CONSOLE` | the console transport | AO_Log |
| `KBSP_RES_C5` | the whole ESP32-C5: EN line, link, its pins, its flash | AO_Radio |
| `KBSP_RES_JPEG` | the P4's JPEG codec, one engine | AO_Display, with the mirror |
| `KBSP_RES_PPA` | the P4's pixel accelerator, one engine | AO_Display, with the mirror |
| `KBSP_RES_IR` | the emitters and the receiver, owned whole | AO_Ir |
| `KBSP_RES_NFC` | the ST25R3916B, its IRQ and SPI2 | AO_Nfc |
| `KBSP_RES_AUDIO` | the ES8311, microphone, speaker, amplifier path and I2S with its five pins | AO_Audio |
| `KBSP_RES_IMU` | the QMI8658A and the QMC5883P | AO_Imu |
| `KBSP_RES_HAPTIC` | the AW86233 at `0x5A` | AO_Haptic |
| `KBSP_RES_RFID` | OUT GPIO47, PULL GPIO45, CARRIER GPIO46 and the demodulated input GPIO54 with its ADC | AO_Rfid |
| `KBSP_RES_EXP` | the expansion header, all fourteen positions | the console or an app, whichever asks first |

Some pairs are one resource on purpose. The infrared emitters and the receiver share a housing and the receiver hears the emitter. The IMU and the magnetometer share one owner because the test rows and the haptic row's witness go through one place. The audio amplifier's enable pin belongs to the C5 and is not in the audio resource.

`KBSP_RES_JPEG` and `KBSP_RES_PPA` are claimed only in a build with the screen mirror, `KBSP_RES_C5` only with the radio, and the three AppMgr resources only with AO_AppMgr. The LEDC channels of the RFID carrier are not part of its resource: the driver takes them from `khal_pwm` while the field is up.

### The calls

The four calls of the ownership table.

| Call | What it does |
| --- | --- |
| `kbsp_claim(res, owner, name)` | Atomic claim of one resource. `true` when `owner` holds it now or already; `false` when another owner holds it or an argument is invalid. A second claim by the holder keeps the first name |
| `kbsp_release(res, owner)` | Gives it back, ignored unless `owner` holds it. Only the host test suite calls it |
| `kbsp_owner_name(res)` | Who holds it, `"?"` while it is being named, or `NULL` when free. `ps` prints it |
| `kbsp_res_name(res)` | The resource's own name. Never `NULL`; `"?"` out of range |

### The build file sdkconfig.board

Each option is a fact about the chip or the wiring. A comment on every option names the component that declares it. A project can restate an option in its own `sdkconfig.defaults`.

The options come in five groups, one table each. The first group holds flash, cache and the partition table.

| Option | Value | Why |
| --- | --- | --- |
| `ESPTOOLPY_FLASHSIZE_32MB` | y | The GD25Q256E |
| `BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH` | y | 4-byte addressing, so the cache maps all 32 MB and code runs above 16 MB. Global, and marked experimental by ESP-IDF. A die that rejects it does not boot at all |
| `IDF_EXPERIMENTAL_FEATURES` | y | The gate for the option above |
| `PARTITION_TABLE_CUSTOM`, `PARTITION_TABLE_OFFSET` | y, `0x8000` | A custom table, at the address the Arduino core looks for |
| `MMU_PAGE_SIZE_64KB` | y | An app image mapped in place starts on an MMU page, so `KAPPS_ALIGN` (`0x10000`) is this number |
| `CACHE_L2_CACHE_128KB` | y | 128 KB of L2 cache. 512 KB does not boot |

### sdkconfig.board: memory

PSRAM and where the big buffers go.

| Option | Value | Why |
| --- | --- | --- |
| `SPIRAM`, `SPIRAM_MODE_HEX`, `SPIRAM_SPEED_250M` | y | 32 MB in HEX mode at 250 MHz, which needs a die of revision v3.0 or newer. A marginal PSRAM bus corrupts data and does not refuse |
| `SPIRAM_IGNORE_NOTFOUND` | y | Boot without PSRAM when it does not answer. IDF then sizes the Wi-Fi and lwIP buffers as if there were none |
| `SPIRAM_MALLOC_ALWAYSINTERNAL` | 0 | A plain `malloc()` takes PSRAM. Internal RAM is asked for by capability |
| `SPIRAM_MEMTEST` | n | No walk of 32 MB at every boot. `test component=psram` tests it instead |
| `ESP_HOSTED_MEMPOOL_PREFER_SPIRAM` | y | The 88 KB of esp-hosted transport pools in PSRAM |
| `BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL` | y | NimBLE's 32 KB of buffers in PSRAM |
| `MBEDTLS_EXTERNAL_MEM_ALLOC` | y | TLS buffers in PSRAM: a session holds about 30 KB of the internal pool |

### sdkconfig.board: the radio link and Bluetooth

The wiring of the C5 and the BLE host.

| Option | Value | Why |
| --- | --- | --- |
| `ESP_HOSTED_*` link lines | SDIO slot 1, CMD 51, CLK 50, D0 49, D1 48, D2 53, D3 52, reset 23, 40 MHz, 4-bit | The wiring of the C5 link. 40 MHz is the validated maximum of the PCB. They do nothing until a firmware compiles esp-hosted in |
| `ESP_HOSTED_ENABLE_GPIO_EXPANDER` | y | Reaches the C5's own pins by RPC, for the SD card detect |
| `ESP_HOSTED_SLAVE_RESET_ONLY_IF_NECESSARY` | y | esp-hosted resets the C5 only when the SDIO card does not answer, so a parked C5 can stay up |
| `BT_ENABLED`, `BT_NIMBLE_ENABLED`, `ESP_HOSTED_ENABLE_BT_NIMBLE` | y | The BLE host on the P4, controller on the C5. In this file because `BT_NIMBLE_ENABLED` is a choice that no option can select |
| `BT_NIMBLE_NVS_PERSIST`, `BT_NIMBLE_CRYPTO_STACK_MBEDTLS` | y | Bonds survive a reboot. Pairing needs P-256 ECDH, which the default backend fails |
| `BT_NIMBLE_LOG_LEVEL_WARNING`, `BT_NIMBLE_MSYS_1_BLOCK_COUNT` | y, 24 | NimBLE's INFO lines would loop through the BLE console. The default 12 mbufs is fewer than one `help` listing needs |

### sdkconfig.board: console, clock, tick and logging

The console, the CPU and RTC clocks, the scheduler tick and the log levels.

| Option | Value | Why |
| --- | --- | --- |
| `ESP_CONSOLE_USB_SERIAL_JTAG` | y | The console is the native USB. The pins of UART0 go to the audio codec |
| `ESP_DEFAULT_CPU_FREQ_MHZ_400` | y | 400 MHz, stated so a later default cannot move it |
| `RTC_CLK_SRC_EXT_CRYS` | y | The RTC runs on the 32.768 kHz crystal X3, not the internal RC |
| `FREERTOS_HZ` | 1000 | A 1 ms scheduler tick |
| `ESP_MAIN_TASK_STACK_SIZE` | 8192 | `kode_init()` starts every owner Active Object on the main task, and QP/C runs each initial transition on it |
| `LCD_DSI_ISR_CACHE_SAFE` | y | The DSI interrupt keeps running while the flash cache is off, which a long erase causes |
| `MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY` | y | A chain that reaches a root of the bundle through a cross-signature verifies. Costs about 700 bytes per handshake |
| `BOOTLOADER_LOG_LEVEL_NONE`, `LOG_MAXIMUM_LEVEL_DEBUG` | y | A silent bootloader. DEBUG lines of ESP-IDF stay compiled in for `log_level` |

### sdkconfig.board: LVGL

The graphics library's allocator, timing, draw threads and colour format.

| Option | Value | Why |
| --- | --- | --- |
| `LV_USE_CUSTOM_MALLOC` | y | LVGL allocates from PSRAM through `kui_mem.c`, with no fallback |
| `LV_DEF_REFR_PERIOD` | 16 | One pass per 16.8 ms refresh of the panel |
| `LV_OS_FREERTOS`, `LV_DRAW_SW_DRAW_UNIT_CNT`, `LV_DRAW_THREAD_STACK_SIZE`, `LV_DRAW_THREAD_PRIO` | y, 2, 32768, 3 | Two draw threads, one per core, with 32 KB stacks in PSRAM for the Lottie rasteriser |
| `LV_COLOR_FORMAT_RGB565`, `LV_COLOR_MIX_ROUND_OFS` | y, 128 | The CO5300 cannot run 410 by 502 at 24 bits per pixel. Colour mixes round half up |
| `LV_DRAW_SW_SHADOW_CACHE_SIZE`, `LV_USE_QRCODE`, `LV_USE_OBJ_NAME` | 128, y, y | The blurred-corner cache, the QR code of the empty launcher, and object names |

### sdkconfig.board: what it leaves out

`CONFIG_ESP_HOSTED_ENABLED` is absent on purpose. The component installs a constructor that claims the SDMMC controller before `app_main`, so a firmware that read this file would lose the microSD slot without asking for a radio. `KODEOS_RADIO` selects it. `CONFIG_XTAL_FREQ_40` is absent because the chip forces it.

Two lines are commented out for a die older than v3.0: `ESP32P4_SELECTS_REV_LESS_V3` and `ESP32P4_REV_MIN_100`. Turning them on also makes `SPIRAM_SPEED_250M` stop existing, and moves the CPU default to 360 MHz.

### The partition table

The bootloader is at `0x2000` and the table at `0x8000`. The table fits in one 4 KB sector, and `nvs` starts at `0xB000` so a table that grows never moves it. Executable code is not confined below 16 MB, because the cache maps 32-bit addresses, so the `apps` region runs from the last OS slot up to the FAT.

The ten partitions of `partitions.csv`:

| Name | Type | Subtype | Offset | Size | Holds |
| --- | --- | --- | --- | --- | --- |
| `nvs` | data | nvs | `0xB000` | `0x10000` | Settings, and `Preferences` of Arduino sketches |
| `otadata` | data | ota | `0x1B000` | `0x2000` | Which OTA slot starts. kodeOS alone writes it |
| `phy_init` | data | phy | `0x1D000` | `0x1000` | RF calibration data |
| `factory` | app | factory | `0x20000` | `0x400000` | The rescue copy of kodeOS, written by cable only |
| `ota_0` | app | ota_0 | `0x420000` | `0x400000` | Update slot of kodeOS |
| `ota_1` | app | ota_1 | `0x820000` | `0x400000` | Update slot of kodeOS |
| `coredump` | data | coredump | `0xC20000` | `0x20000` | The last panic, in ELF format. Below 16 MB because the panic handler writes it with the cache off |
| `apps` | data | `0x40` | `0xC40000` | `0xBC0000` | Directory, boot request and installed app images (`internals/apps-region`) |
| `storage` | data | fat | `0x1800000` | `0x200000` | The internal FAT volume, wear-levelled |
| `app_pending` | data | `0x41` | `0x1A00000` | `0x600000` | The development mailbox. Non-executable: the bootloader never boots it |

The regions end where the next begins: `apps` ends at `0x1800000`, `storage` at `0x1A00000` and `app_pending` at `0x2000000`, the end of the 32 MB. The offset of `apps` is fixed, so installed images keep their addresses across a table change.

### The files

The files of the component.

| File | What it does |
| --- | --- |
| `board/include/kbsp.h` | Umbrella header: includes `kbsp_pins.h`, `kbsp_board.h` and `kbsp_cell.h` |
| `board/include/kbsp_pins.h` | Every GPIO, I2C address and expander bit |
| `board/include/kbsp_board.h` | The product name and revision, the glass, the rails, the links, the crystals and the controllers |
| `board/include/kbsp_cell.h` | The battery: shunt, charge profile, boost limits and the OCV curve |
| `board/include/kbsp_identity.h` | `KBSP_ID_PRODUCT` and `KBSP_ID_PREFIX`. Not pulled in by `kbsp.h` |
| `exp/include/kbsp_exp.h` | The `kbsp_exp_t` type, `KBSP_EXP[]` and the controllers an app may take |
| `exp/src/kbsp_exp.c` | The table of the 14 positions and the list of controllers, with a compile-time check that a UART console is UART0 |
| `owners/include/kbsp_owners.h` | `kbsp_res_t` and the four calls |
| `owners/src/kbsp_owners.c` | The table of owners: one atomic token and one atomic name per resource |
| `sdkconfig.board` | The build options of the chip and the wiring |
| `partitions.csv` | The flash map above |
| `CMakeLists.txt` | Registers the two sources and the three include folders |

## Gotchas

> **WARNING**
>
> **The audio pins are swapped against the schematic.** The firmware drives MCLK on GPIO36 and ASDOUT on GPIO33. The DVT schematic and the validation pin map have them the other way round. GPIO36 is a boot strap of the P4, and the output of the codec must not drive it. The swap is rework KD-DVT-002, and a DVT board without it needs the rework before the audio path works.

> **WARNING**
>
> **A generated `sdkconfig` beats a new default.** After a project is configured, its `sdkconfig` holds concrete values, and a changed default in `sdkconfig.board` does nothing until that file is deleted. A new option is picked up only at a CMake configure, so run `idf.py reconfigure` and check with `grep CONFIG_<NAME> sdkconfig`. Delete `sdkconfig` too after changing `CONFIG_PARTITION_TABLE_CUSTOM_FILENAME` or `CONFIG_RTC_CLK_SRC_EXT_CRYS`.

> **DANGER**
>
> **Three lines of `sdkconfig.board` fail without a message.** Leave `BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH` alone, because a die that rejects it does not boot and the bootloader prints nothing. Keep `SPIRAM_SPEED_250M`: a project that declares PSRAM as octal hangs at the first use of it with no output, and a marginal bus corrupts a pixel or a file byte days before anyone looks here. Never raise `CACHE_L2_CACHE_128KB`, because 512 KB does not boot.

> **NOTE**
>
> **A panel with a CST916E starts with no screen and no touch.** Supplier B's panel module carries a CST916E at `0x5A`, which is also the AW86233's address on the antenna PCB. kodeOS has no driver for the CST916E. `kdrv_display` takes an answer at `0x15` as proof that a panel is fitted, and without one it logs `no panel assembly` and leaves the DSI alone.

## Example code

Read the board facts from kbsp, then ask the ownership table who holds the LED and the battery charger. After `kode_init()` returns, the log shows the screen size and the two owners, AO_LedMgr and AO_Power.

```c
/*
 * board_facts.c - 2026-10-28 - from Kode with love
 *
 * Logs the screen size and the I2C address of the charger from kbsp, then asks
 * the ownership table who owns the status LED and the BQ25896. On a Kode Dot
 * the log reads "screen 410 x 502", "charger at 0x6B", "LED owner: AO_LedMgr"
 * and "charger owner: AO_Power".
 */
#include "kode.h"
#include "kbsp.h"
#include "kbsp_owners.h"

#define BOARD_TAG "board"

/* Bring the system up, then log what kbsp says about the board. */
void app_main(void)
{
    kode_init();

    KLOG_N(BOARD_TAG, "screen %u x %u", (unsigned)KBSP_LCD_H_RES, (unsigned)KBSP_LCD_V_RES);
    KLOG_N(BOARD_TAG, "charger at 0x%02X", (unsigned)KBSP_I2C_ADDR_CHARGER);

    /* A resource nobody claimed has no owner and answers NULL. */
    const char *led_owner = kbsp_owner_name(KBSP_RES_LED);
    const char *charger_owner = kbsp_owner_name(KBSP_RES_PMIC);

    if (led_owner != NULL) {
        KLOG_N(BOARD_TAG, "LED owner: %s", led_owner);
    }
    if (charger_owner != NULL) {
        KLOG_N(BOARD_TAG, "charger owner: %s", charger_owner);
    }
}
```

Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. **TBD**: a run on a Kode Dot.

## In the SDK

- [`components/kbsp`](https://github.com/kodediy/kode-sdk/tree/main/components/kbsp): the whole layer, with `sdkconfig.board` and `partitions.csv` at its root.
- [`kbsp_pins.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kbsp/board/include/kbsp_pins.h): every pin and address.
- [`kbsp_owners.c`](https://github.com/kodediy/kode-sdk/blob/main/components/kbsp/owners/src/kbsp_owners.c): the ownership table.
