# Arduino

Run a sketch from the Arduino IDE on a Kode Dot as a kodeOS app: the Kode Dot board entry, what Upload does, the pin names and what a sketch can use.

## What it is

A sketch built for the Kode Dot is a complete ESP-IDF image, the same kind of file as any other kodeOS app, so kodeOS needs no special code for Arduino. The **Kode Dot** board entry makes the IDE build it for the right chip and upload it to the right place. You pick the board, write `setup()` and `loop()`, press Upload and open the Serial Monitor.

The entry is not in the Boards Manager. The upstream Arduino-ESP32 core has its own `kodedot` entry, for an ESP32-S3 board. The entry for the Kode Dot comes from a git clone of `arduino-esp32` in your sketchbook, with the `kodedot` block of `boards.txt` and the folder `variants/kodedot/` (the pin map and the code specific to the Kode Dot). The board's FQBN, which only `arduino-cli` needs, is `espressif:esp32:kodedot`: the vendor `espressif` is the name of the directory that holds the clone.

While a sketch runs it owns every peripheral. kodeOS does not run beside it. To return to kodeOS, hold BOT and LEFT together for about one second.

## How it is connected

```mermaid
flowchart LR
  subgraph pc["On your computer"]
    ide["Arduino IDE<br/>Upload"] --> es["esptool_py_app_only"]
  end
  subgraph board["On the Kode Dot"]
    mb["app_pending<br/>0x1A00000"]
    am["AO_AppMgr<br/>adopts at boot"]
    reg["apps region"]
    boot["AO_Boot<br/>ALWAYS request"]
    bl["Bootloader<br/>ARMED, jump"]
    mb --> am
    am -- "install" --> reg
    am -- "request" --> boot
    boot --> bl
  end
  es -- "one write" --> mb
  class ide ext
  class mb,reg store
  class am,boot ao
```

*What Upload does*

### Upload writes the sketch and nothing else `upload.tool=esptool_py_app_only` and `build.flash_offset=0x1A00000` give one esptool call that writes one file at one address with `--flash-size 32MB`. There is no bootloader at `0x2000`, no partition table at `0x8000` and no `boot_app0.bin`, so Upload cannot touch kodeOS, and the entry has no `EraseFlash` menu.

### What happens at the next boot

At the next boot AO_AppMgr installs the image into the apps region and verifies the copy. A sketch with the name of an installed app replaces it. If the region is full it verifies the image in the mailbox first and then evicts: the old copy of that sketch first, then the oldest apps. Then AO_AppMgr launches the sketch with a **sticky** request (`internals/boot-model`).

The sticky request is why the sketch survives the Serial Monitor: opening the port is a reset on the bootloader's keep list.

The sketch keeps running after:

- the Serial Monitor opening the port;
- the debugger's `reset halt`;
- a software restart;
- a panic's reboot;
- a deep-sleep wake.

The Kode Dot returns to kodeOS after:

- a power-on (BOT and LEFT);
- a hardware watchdog;
- a hardware brownout reset;
- the next Upload.

A sketch that crashes in `setup()` restarts again and again until Upload or BOT and LEFT, because the system does not tell a deliberate restart from a crash. A second Upload while a sketch runs works with no button: the bootloader finds the mailbox full and starts kodeOS, which adopts the new image.

Back in kodeOS the sketch is an installed app. Apps shows it as a loose row at the root, with no folder, named after the sketch. Starting it from its row is a one-shot run like any other app, so the Serial Monitor opening the port returns to kodeOS then. Only mailbox adoption writes a sticky request.

### The launcher names a sketch by its `.ino`

kodeOS identifies an app by the `project_name` in `esp_app_desc_t`, and the core's prebuilt libraries give every sketch the same one, `arduino-lib-builder`. Without a fix, two sketches would be one app.

`variants/kodedot/kode_app_desc.cpp` defines a strong `esp_app_desc` that overrides the libraries' weak one. `project_name` is the sketch name without the extension (the entry passes `-DKODE_SKETCH_NAME="{build.project_name}"`), `version` is the core's, and `date` and `time` are those of the first build in that sketch's build folder, which kodeOS never compares.

All other fields are copied from the libraries' descriptor, because the bootloader checks some of them. The manifest stays empty, since a sketch cannot declare `KODE_APP_MANIFEST`.

### A flash guard lets the sketch write its own data

The sketch runs from the `apps` region, a data partition. ESP-IDF aborts every flash write when `esp_ota_get_running_partition()` finds no app partition. The entry adds three link wraps in `compiler.c.elf.extra_flags`, implemented in `variants/kodedot/kode_flash_guard.cpp`:

| Wrapped symbol | Answer | Effect |
| --- | --- | --- |
| `esp_ota_get_running_partition` | A synthetic partition describing the sketch's own image | A write to `nvs`, `storage` or anywhere outside the sketch is allowed. A write into the running image is refused. `Preferences`, `EEPROM`, `FFat` and `LittleFS` work |
| `esp_ota_get_next_update_partition` | `NULL` | A sketch has no OTA slot. `Update.begin()` fails cleanly with `UPDATE_ERROR_NO_PARTITION`. Nothing a sketch does reaches the kodeOS slots. The rollback check of `initArduino()` also finds no OTA state and marks nothing |
| `esp_image_verify` | The real call below 16 MB; above it, an answer from `esp_image_get_metadata()` | `ESP.getSketchSize()` and `ESP.getSketchMD5()` work for a sketch that reaches the 16 MB mark. The SHA check is skipped there because the bootloader verified the image at adoption and at boot |

## Specifications

### The board entry

| Thing | Value |
| --- | --- |
| Chip and clock | `build.mcu=esp32p4`, `build.f_cpu=400000000L` |
| Flash | `flash_size=32MB`, `flash_mode=dio`, 80 MHz |
| Serial | `usb_mode=1`, `cdc_on_boot=1`: `Serial` is the USB-Serial/JTAG, the same port as the console |
| Upload | Tool `esptool_py_app_only`, 921600 baud, offset `0x1A00000`, `upload.maximum_size=6287360` (6 MB less one 4 KB sector) |
| Defines | `-DBOARD_HAS_PSRAM -DKODE_SKETCH_NAME="{build.project_name}"`. `build.board=KODE_DOT`, so `#ifdef ARDUINO_KODE_DOT` marks code only the Kode Dot runs |
| Menus | Only Core Debug Level. PartitionScheme, PSRAM, FlashMode, FlashSize and ChipVariant are not choices on this board |
| `platform.txt` | `tools.esptool_py_app_only.path` must be `{runtime.platform.path}/tools/esptool`, not the tool-registry variable, which resolves to nothing in a clone |
| Partition table | The core's prebuilt libraries find `nvs` and `phy_init` because the table is at `0x8000` (`internals/boot-model`) |

### Pin names

These come from `variants/kodedot/pins_arduino.h` and match the board's pin map (`sdk/kbsp`).

| Name | Value |
| --- | --- |
| `Serial` | USB-Serial/JTAG |
| `Serial0`, `TX`/`RX`, `SOC_TX0`/`SOC_RX0` | UART0 on J3: transmit on GPIO8 (J3 pin 3), receive on GPIO9 (J3 pin 4). The C5 is flashed through this pair |
| `Serial1` | `TX1` and `RX1` are `-1`, so `begin()` needs explicit pins. The core's own default, GPIO10 and GPIO11, would drive the C5's download strap on J3 |
| `Wire`, `SDA`, `SCL` | The internal I2C bus every on-board chip uses: GPIO4 and GPIO5, 400 kHz |
| `KODE_IOEXP_INT` | 6, the expander's active-low interrupt |
| `SPI`: `SCK`, `MOSI`, `MISO`, `SS`, `NFC_IRQ` | 30, 31, 32, 28, 29: the NFC reader |
| `A0` to `A5` | GPIO16 to GPIO21, the header pins with ADC1 |
| `EXP1` to `EXP14` | The J3 positions in connector order: 8, 9, 10, 11, 12, 13, 14, 21, 19, 20, 17, 18, 15, 16. `PIN3` to `PIN18` are the same pins by J3 pin number |
| `BUTTON_TOP` | 35, the only button on a GPIO. The other five are behind the expander at `0x20`, on P0 to P4, and a sketch reads them over `Wire` |
| SD card | `BOARD_HAS_SDMMC` slot 0; `SDMMC_CLK` 43, `SDMMC_CMD` 44, `SDMMC_D0` to `D3` 39 to 42; IO domain on the internal LDO channel 4; the card's own supply on GPIO22, active low. A bare `SD_MMC.begin()` powers the card itself. The core reads `BOARD_HAS_SDMMC`, and `BOARD_HAS_SD_SDMMC` does nothing |
| RFID | `RFID_PULL` 45, `RFID_CARRIER` 46 (input only), `RFID_OUT` 47, `RFID_RF_IN` 54 (ADC2 channel 5). R11 holds `RFID_PULL` high at idle, and high stops the receiver, so a sketch that reads tags drives it low first |
| `DISPLAY_RST`, `C5_EN` | 7 (shared with touch) and 23. The panel is MIPI-DSI and gets no pin defines |
| Not defined | `LED_BUILTIN`, `IR_TX` and `IR_RX`. The RGB LED is a KTD2026 on `Wire`, and both infrared lines are IOs of the C5 |

The I2C addresses are defined as names:

| Name | Address | Chip |
| --- | --- | --- |
| `KODE_I2C_IMU` | `0x6A` | QMI8658A |
| `KODE_I2C_MAG` | `0x2C` | QMC5883P |
| `KODE_I2C_PMIC` | `0x6B` | BQ25896 |
| `KODE_I2C_GAUGE` | `0x64` | CW2217 |
| `KODE_I2C_LED` | `0x30` | KTD2026 |
| `KODE_I2C_IOEXP` | `0x20` | TCA6408A |
| `KODE_I2C_CODEC` | `0x18` | ES8311 |
| `KODE_I2C_HAPTIC` | `0x5A` | AW86233 (antenna PCB) |
| `KODE_I2C_TOUCH` | `0x15` | CST820 |

### What works and what does not

| Thing | State |
| --- | --- |
| `Serial`, `Wire`, `BUTTON_TOP`, `analogReadMilliVolts()` on A0 to A5 and GPIO54 | Work |
| `Preferences`, `EEPROM`, `FFat`, `LittleFS` | Work through the flash guard. `LittleFS.begin()` and `FFat.begin()` default to partition labels `spiffs` and `ffat`, which this table does not have, so name the partition: `FFat.begin(false, "/ffat", 10, "storage")` |
| `SD_MMC.begin()` | Works |
| `ESP.getSketchSize()` | Returns the exact image length |
| Wi-Fi and BLE | Not available with the core's prebuilt libraries: they carry esp-hosted configured for an ESP32-C6 slave, and the radio here is an ESP32-C5. The variant names the Kode Dot's SDIO pins (CLK 50, CMD 51, D0 to D3 49, 48, 53, 52, reset on `C5_EN`) so `WiFi.begin()` does not drive the pins of the development board. **TBD**: a run with libraries built for the C5 with `esp32-arduino-lib-builder` |
| A raw `esp_partition_write` into the sketch's own image | Aborts. The guard's refusal becomes an abort under `DANGEROUS_WRITE_ABORTS` |
| `spi_flash_mmap()` or `esp_partition_mmap()` at or above `0x1000000` | Returns `ESP_ERR_INVALID_ARG`. Read with `esp_partition_read()` or `esp_flash_read()` instead |
| Standard Blink | Does not compile: no `LED_BUILTIN` |
| A scan of `Wire` on a Kode Dot with the antenna PCB | Answers at every address of the table above, and an acknowledge at `0x7E` that none of those chips explains |

### Debugging

Start Debugging works as on any board the core supports. OpenOCD attaches over the built-in USB-JTAG and GDB stops on the first line of `setup()`. Stop and a plain detach leave the sketch running. A JTAG reset returns to the sketch only while a sticky request is in force, and Upload arms one.

| Thing | Value |
| --- | --- |
| The check | The OpenOCD script (`kodedot-debug.cfg`) compares the app descriptor of the running image with that of the `.bin` you debug before it resets. A mismatch ends the session with `Kode Dot: the chip is not running this build of the sketch; press Upload, then Start Debugging again` and the board keeps running what it ran |
| Breakpoints | Hardware only. The flash probe cannot describe the apps region, so the script turns off OpenOCD's memory map and GDB trusts the ELF. Two of each core's three triggers may be watchpoints |
| Several boards | OpenOCD attaches to the first USB-JTAG it finds, not the one Upload wrote to. Keep one Kode Dot plugged in |
| Flash writes | Never write the flash of the Kode Dot with OpenOCD. The stub in the core's build addresses erase and write with 24 bits, so a write for the mailbox at 28 MB lands 16 MB lower |

## Gotchas

> **DANGER**
>
> **A core older than the 6 MB mailbox uploads where kodeOS does not look.** The
> Kode Dot entry of arduino-esp32 4.0.0-RC1 and earlier writes the sketch at
> `0x1C00000` with a 4 MB limit. On a Kode Dot with the 6 MB mailbox at
> `0x1A00000`, that address is 2 MB into the mailbox, so kodeOS finds no image
> at the mailbox's start and the sketch never runs. Use a core whose
> `boards.txt` says `kodedot.build.flash_offset=0x1A00000`.

> **DANGER**
>
> **Tools → Burn Bootloader wipes the board.** It does not use the app-only tool. It erases the whole flash, kodeOS and the apps region and the settings included, and writes nothing back. A sketch never needs it. Recover with one full cable flash (`internals/building-and-flashing`).

> **WARNING**
>
> **After an Upload, opening the port answers with the sketch's console, not kodeOS's.** The adopted image holds an `ARMED` request and the port opening is a reset on the keep list. `idf.py flash` right after a sketch does the same. Hold BOT and LEFT together to clear it.

> **WARNING**
>
> **The Kode Dot forgot its settings after a sketch ran.** The sketch shares the `nvs` partition with kodeOS, and `initArduino()` erases an NVS it finds full or of a version it does not know. Nothing else is damaged, and kodeOS starts with defaults (`internals/settings`).

> **NOTE**
>
> **A crashing sketch writes the same core dump at every reboot.** The core's ESP32-P4 libraries use `CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH` like kodeOS, so the dump lands in the `coredump` partition. `info` reports `coredump=present owner=other task=- pc=-`, because the core's libraries checksum with CRC32 and kodeOS checks SHA-256 only.

## Example code

Set up the core once, then upload a sketch.

1. Clone the core into the sketchbook and fetch its tools (about 9 GB): `mkdir -p ~/Documents/Arduino/hardware/espressif && cd ~/Documents/Arduino/hardware/espressif && git clone https://github.com/espressif/arduino-esp32.git esp32 && cd esp32/tools && python3 get.py`. **TBD**: the repository and branch that carry the `kodedot` entry and variant.
2. Restart the Arduino IDE. Open Tools → Board → ESP32 Arduino (in sketchbook) and choose **Kode Dot**.
3. Connect the Kode Dot over USB-C. In Tools → Port choose the port shown as **ESP32 Family Device** (the USB numbers `0x303A:0x1001` belong to Espressif's USB-Serial/JTAG), and pick **Kode Dot** for it once.
4. Open the sketch below and press Upload. The board resets, kodeOS adopts the sketch and it starts.
5. Open Tools → Serial Monitor. One line arrives every 500 ms and the sketch keeps running.
6. Hold BOT and LEFT together for about one second. kodeOS counts the boot as the return from an app, so Apps is on the glass with the sketch in focus.

The sketch prints the uptime, the top button and whether the battery gauge answers, over the real internal bus.

```cpp
/*
 * arduino_status.ino - 2026-10-28 - from Kode with love
 *
 * Runs on the Kode Dot as an app, with the board set to Kode Dot in the
 * Arduino IDE. Twice a second it prints the uptime, the state of the top
 * button and whether the battery gauge answers on the internal I2C bus.
 * In the Serial Monitor you see one line every 500 ms, and the button value
 * changes while you hold the top button.
 */
#include <Wire.h>

/* The battery gauge sits on the internal bus; 1 means it acknowledged. */
static int gaugeAnswers()
{
  Wire.beginTransmission(KODE_I2C_GAUGE);
  if ( Wire.endTransmission() == 0 )
  {
    return 1;
  }
  return 0;
}

/* Serial is the USB-C port; Wire uses the board's own SDA and SCL. */
void setup()
{
  Serial.begin(115200);
  Wire.begin(SDA, SCL, 400000);
  pinMode(BUTTON_TOP, INPUT_PULLUP);
}

/* Print one status line every half second. */
void loop()
{
  Serial.printf("uptime=%lu ms top_button=%d gauge=%d\n",
                (unsigned long)millis(), digitalRead(BUTTON_TOP), gaugeAnswers());
  delay(500);
}
```

**TBD**: this example has not been compiled or run on a Kode Dot yet.

## In the SDK

- [`examples/arduino_hello`](https://github.com/kodediy/kode-sdk/tree/main/examples/arduino_hello): the example sketch, which blinks the LED over `Wire`.
- [`components/kdrv/apps/include/kapps_fmt.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kdrv/apps/include/kapps_fmt.h): the mailbox, the intent word and the request states the upload relies on.
- [`bootloader_components/main/bootloader_start.c`](https://github.com/kodediy/KodeOS/blob/main/bootloader_components/main/bootloader_start.c): the closed list of resets that keep a sketch running.
