From 9969f3e0ae148446cf20a8157ee3f26f468dca29 Mon Sep 17 00:00:00 2001 From: Romain Boutrois Date: Thu, 17 Sep 2026 14:59:15 +0200 Subject: [PATCH 1/2] Add ELIO Eliobot S3 board Eliobot is an educational robot built around an ESP32-S3 (4MB flash, 2MB PSRAM) with a DRV8833 motor driver, LED matrix eyes, IR obstacle sensors, line sensors and a buzzer. USB VID/PID 0x303A:0x8384 is allocated to Eliobot-S3 CircuitPython in espressif/usb-pids. --- .../espressif/boards/elio_eliobot_s3/board.c | 41 ++++++ .../boards/elio_eliobot_s3/mpconfigboard.h | 26 ++++ .../boards/elio_eliobot_s3/mpconfigboard.mk | 27 ++++ ports/espressif/boards/elio_eliobot_s3/pins.c | 136 ++++++++++++++++++ .../boards/elio_eliobot_s3/sdkconfig | 16 +++ 5 files changed, 246 insertions(+) create mode 100644 ports/espressif/boards/elio_eliobot_s3/board.c create mode 100644 ports/espressif/boards/elio_eliobot_s3/mpconfigboard.h create mode 100644 ports/espressif/boards/elio_eliobot_s3/mpconfigboard.mk create mode 100644 ports/espressif/boards/elio_eliobot_s3/pins.c create mode 100644 ports/espressif/boards/elio_eliobot_s3/sdkconfig diff --git a/ports/espressif/boards/elio_eliobot_s3/board.c b/ports/espressif/boards/elio_eliobot_s3/board.c new file mode 100644 index 00000000000..09b7cd0bb9b --- /dev/null +++ b/ports/espressif/boards/elio_eliobot_s3/board.c @@ -0,0 +1,41 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Romain Boutrois +// +// SPDX-License-Identifier: MIT + +#include "supervisor/board.h" +#include "mpconfigboard.h" +#include "shared-bindings/microcontroller/Pin.h" + +#include "lib/oofatfs/ff.h" +#include "extmod/vfs_fat.h" +#include "py/mpstate.h" +#include "supervisor/filesystem.h" + +void board_init(void) { + mp_import_stat_t stat_b = mp_import_stat("boot.py"); + if (stat_b != MP_IMPORT_STAT_FILE) { + fs_user_mount_t *fs_mount = filesystem_circuitpy(); + FATFS *fatfs = &fs_mount->fatfs; + FIL fs; + UINT char_written = 0; + const byte buffer[] = "import board\nimport storage\n\n# Write options : True = Mass Storage, False = REPL\nstorage.remount(\"/\", False)\n"; + // Create or modify existing boot.py file + f_open(fatfs, &fs, "/boot.py", FA_WRITE | FA_CREATE_ALWAYS); + f_write(&fs, buffer, sizeof(buffer) - 1, &char_written); + f_close(&fs); + // Delete code.py, use main.py + mp_import_stat_t stat_c = mp_import_stat("code.py"); + if (stat_c == MP_IMPORT_STAT_FILE) { + f_unlink(fatfs, "/code.py"); + } + // Create main.py file + const byte buffer2[] = "print(\"Hello World!\")\n"; + f_open(fatfs, &fs, "/main.py", FA_WRITE | FA_CREATE_ALWAYS); + f_write(&fs, buffer2, sizeof(buffer2) - 1, &char_written); + f_close(&fs); + } +} + +// Use the MP_WEAK supervisor/shared/board.c versions of routines not defined here. diff --git a/ports/espressif/boards/elio_eliobot_s3/mpconfigboard.h b/ports/espressif/boards/elio_eliobot_s3/mpconfigboard.h new file mode 100644 index 00000000000..7038c4e4efb --- /dev/null +++ b/ports/espressif/boards/elio_eliobot_s3/mpconfigboard.h @@ -0,0 +1,26 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Romain Boutrois +// +// SPDX-License-Identifier: MIT + +#pragma once + +// Micropython setup + +#define MICROPY_HW_BOARD_NAME "Eliobot" +#define MICROPY_HW_MCU_NAME "ESP32S3" +#define CIRCUITPY_DRIVE_LABEL "ELIOBOT" + +#define MICROPY_HW_NEOPIXEL (&pin_GPIO1) +#define MICROPY_HW_NEOPIXEL_COUNT (8) + +#define DEFAULT_I2C_BUS_SCL (&pin_GPIO9) +#define DEFAULT_I2C_BUS_SDA (&pin_GPIO8) + +#define DEFAULT_SPI_BUS_SCK (&pin_GPIO39) +#define DEFAULT_SPI_BUS_MOSI (&pin_GPIO40) +#define DEFAULT_SPI_BUS_MISO (&pin_GPIO41) + +#define DEFAULT_UART_BUS_RX (&pin_GPIO44) +#define DEFAULT_UART_BUS_TX (&pin_GPIO43) diff --git a/ports/espressif/boards/elio_eliobot_s3/mpconfigboard.mk b/ports/espressif/boards/elio_eliobot_s3/mpconfigboard.mk new file mode 100644 index 00000000000..405b8e872df --- /dev/null +++ b/ports/espressif/boards/elio_eliobot_s3/mpconfigboard.mk @@ -0,0 +1,27 @@ +USB_VID = 0x303a +USB_PID = 0x8384 +USB_PRODUCT = "Eliobot" +USB_MANUFACTURER = "ELIO" +USB_INTERFACE_NAME = "Eliobot" + +IDF_TARGET = esp32s3 + +CIRCUITPY_ESP_FLASH_MODE = qio +CIRCUITPY_ESP_FLASH_FREQ = 80m +CIRCUITPY_ESP_FLASH_SIZE = 4MB + +CIRCUITPY_ESP_PSRAM_SIZE = 2MB +CIRCUITPY_ESP_PSRAM_MODE = qio +CIRCUITPY_ESP_PSRAM_FREQ = 80m + +CIRCUITPY_ESPCAMERA = 0 +CIRCUITPY_BITMAPFILTER = 0 +CIRCUITPY_CODEOP = 0 +CIRCUITPY_PARALLELDISPLAYBUS = 0 + + +# Include these Python libraries in firmware. +FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_NeoPixel +FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_HTTPServer +FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_IRRemote +FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_BLE diff --git a/ports/espressif/boards/elio_eliobot_s3/pins.c b/ports/espressif/boards/elio_eliobot_s3/pins.c new file mode 100644 index 00000000000..8267cb252e6 --- /dev/null +++ b/ports/espressif/boards/elio_eliobot_s3/pins.c @@ -0,0 +1,136 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Romain Boutrois +// +// SPDX-License-Identifier: MIT + +#include "shared-bindings/board/__init__.h" + +static const mp_rom_map_elem_t board_module_globals_table[] = { + CIRCUITPYTHON_BOARD_DICT_STANDARD_ITEMS + + { MP_ROM_QSTR(MP_QSTR_IO0), MP_ROM_PTR(&pin_GPIO0) }, + + // GPIO1 : NeoPixel (WS2812B) + { MP_ROM_QSTR(MP_QSTR_NEOPIXEL), MP_ROM_PTR(&pin_GPIO1) }, + + // GPIO2 : Eyes LED connector (J3) + { MP_ROM_QSTR(MP_QSTR_IO2), MP_ROM_PTR(&pin_GPIO2) }, + { MP_ROM_QSTR(MP_QSTR_EYES), MP_ROM_PTR(&pin_GPIO2) }, + + // GPIO3 : Battery voltage sense (ADC, 1:2 divider) + { MP_ROM_QSTR(MP_QSTR_IO3), MP_ROM_PTR(&pin_GPIO3) }, + { MP_ROM_QSTR(MP_QSTR_BATTERY), MP_ROM_PTR(&pin_GPIO3) }, + { MP_ROM_QSTR(MP_QSTR_VBAT_SENSE), MP_ROM_PTR(&pin_GPIO3) }, + { MP_ROM_QSTR(MP_QSTR_VOLTAGE_MONITOR), MP_ROM_PTR(&pin_GPIO3) }, + + // GPIO4-7 : IR obstacle sensor outputs (TSSP77038) + { MP_ROM_QSTR(MP_QSTR_IO4), MP_ROM_PTR(&pin_GPIO4) }, + { MP_ROM_QSTR(MP_QSTR_IR_OUT_1), MP_ROM_PTR(&pin_GPIO4) }, + + { MP_ROM_QSTR(MP_QSTR_IO5), MP_ROM_PTR(&pin_GPIO5) }, + { MP_ROM_QSTR(MP_QSTR_IR_OUT_2), MP_ROM_PTR(&pin_GPIO5) }, + + { MP_ROM_QSTR(MP_QSTR_IO6), MP_ROM_PTR(&pin_GPIO6) }, + { MP_ROM_QSTR(MP_QSTR_IR_OUT_3), MP_ROM_PTR(&pin_GPIO6) }, + + { MP_ROM_QSTR(MP_QSTR_IO7), MP_ROM_PTR(&pin_GPIO7) }, + { MP_ROM_QSTR(MP_QSTR_IR_OUT_4), MP_ROM_PTR(&pin_GPIO7) }, + + // GPIO8-9 : I2C bus + { MP_ROM_QSTR(MP_QSTR_IO8), MP_ROM_PTR(&pin_GPIO8) }, + { MP_ROM_QSTR(MP_QSTR_SDA), MP_ROM_PTR(&pin_GPIO8) }, + + { MP_ROM_QSTR(MP_QSTR_IO9), MP_ROM_PTR(&pin_GPIO9) }, + { MP_ROM_QSTR(MP_QSTR_SCL), MP_ROM_PTR(&pin_GPIO9) }, + + // GPIO10-14 : Line sensors (J1) + { MP_ROM_QSTR(MP_QSTR_IO10), MP_ROM_PTR(&pin_GPIO10) }, + { MP_ROM_QSTR(MP_QSTR_LINE_1), MP_ROM_PTR(&pin_GPIO10) }, + + { MP_ROM_QSTR(MP_QSTR_IO11), MP_ROM_PTR(&pin_GPIO11) }, + { MP_ROM_QSTR(MP_QSTR_LINE_2), MP_ROM_PTR(&pin_GPIO11) }, + + { MP_ROM_QSTR(MP_QSTR_IO12), MP_ROM_PTR(&pin_GPIO12) }, + { MP_ROM_QSTR(MP_QSTR_LINE_3), MP_ROM_PTR(&pin_GPIO12) }, + + { MP_ROM_QSTR(MP_QSTR_IO13), MP_ROM_PTR(&pin_GPIO13) }, + { MP_ROM_QSTR(MP_QSTR_LINE_4), MP_ROM_PTR(&pin_GPIO13) }, + + { MP_ROM_QSTR(MP_QSTR_IO14), MP_ROM_PTR(&pin_GPIO14) }, + { MP_ROM_QSTR(MP_QSTR_LINE_5), MP_ROM_PTR(&pin_GPIO14) }, + + // GPIO15 : XTAL_32K_P + { MP_ROM_QSTR(MP_QSTR_IO15), MP_ROM_PTR(&pin_GPIO15) }, + + // GPIO16 : XTAL_32K_N + { MP_ROM_QSTR(MP_QSTR_IO16), MP_ROM_PTR(&pin_GPIO16) }, + + // GPIO17 : Buzzer + { MP_ROM_QSTR(MP_QSTR_IO17), MP_ROM_PTR(&pin_GPIO17) }, + { MP_ROM_QSTR(MP_QSTR_BUZZER), MP_ROM_PTR(&pin_GPIO17) }, + + // GPIO18 : Line sensor LED enable + { MP_ROM_QSTR(MP_QSTR_IO18), MP_ROM_PTR(&pin_GPIO18) }, + { MP_ROM_QSTR(MP_QSTR_LINE_LED), MP_ROM_PTR(&pin_GPIO18) }, + + // GPIO19 : USB_D- + // GPIO20 : USB_D+ + + // GPIO21 : VBUS sense (5V present, 1:2 divider) + { MP_ROM_QSTR(MP_QSTR_VBUS), MP_ROM_PTR(&pin_GPIO21) }, + { MP_ROM_QSTR(MP_QSTR_VBUS_SENSE), MP_ROM_PTR(&pin_GPIO21) }, + + // GPIO26-32 : internal flash/PSRAM SPI — not available as GPIO + + // GPIO33 : Line sensor command output (J1) + { MP_ROM_QSTR(MP_QSTR_IO33), MP_ROM_PTR(&pin_GPIO33) }, + { MP_ROM_QSTR(MP_QSTR_LINE_CMD), MP_ROM_PTR(&pin_GPIO33) }, + + // GPIO34 : IR obstacle sensor 38kHz modulation output + { MP_ROM_QSTR(MP_QSTR_IO34), MP_ROM_PTR(&pin_GPIO34) }, + { MP_ROM_QSTR(MP_QSTR_IR_CMD), MP_ROM_PTR(&pin_GPIO34) }, + + // GPIO35-38 : Motor driver (DRV8833) + { MP_ROM_QSTR(MP_QSTR_IO35), MP_ROM_PTR(&pin_GPIO35) }, + { MP_ROM_QSTR(MP_QSTR_BIN2), MP_ROM_PTR(&pin_GPIO35) }, + + { MP_ROM_QSTR(MP_QSTR_IO36), MP_ROM_PTR(&pin_GPIO36) }, + { MP_ROM_QSTR(MP_QSTR_AIN1), MP_ROM_PTR(&pin_GPIO36) }, + + { MP_ROM_QSTR(MP_QSTR_IO37), MP_ROM_PTR(&pin_GPIO37) }, + { MP_ROM_QSTR(MP_QSTR_BIN1), MP_ROM_PTR(&pin_GPIO37) }, + + { MP_ROM_QSTR(MP_QSTR_IO38), MP_ROM_PTR(&pin_GPIO38) }, + { MP_ROM_QSTR(MP_QSTR_AIN2), MP_ROM_PTR(&pin_GPIO38) }, + + // GPIO39-41 : SPI header (J8) via JTAG pins MTCK/MTDO/MTDI + { MP_ROM_QSTR(MP_QSTR_IO39), MP_ROM_PTR(&pin_GPIO39) }, + { MP_ROM_QSTR(MP_QSTR_SCK), MP_ROM_PTR(&pin_GPIO39) }, + + { MP_ROM_QSTR(MP_QSTR_IO40), MP_ROM_PTR(&pin_GPIO40) }, + { MP_ROM_QSTR(MP_QSTR_MOSI), MP_ROM_PTR(&pin_GPIO40) }, + + { MP_ROM_QSTR(MP_QSTR_IO41), MP_ROM_PTR(&pin_GPIO41) }, + { MP_ROM_QSTR(MP_QSTR_MISO), MP_ROM_PTR(&pin_GPIO41) }, + + { MP_ROM_QSTR(MP_QSTR_IO42), MP_ROM_PTR(&pin_GPIO42) }, + + // GPIO43-44 : UART (U0TXD / U0RXD) + { MP_ROM_QSTR(MP_QSTR_IO43), MP_ROM_PTR(&pin_GPIO43) }, + { MP_ROM_QSTR(MP_QSTR_TX), MP_ROM_PTR(&pin_GPIO43) }, + + { MP_ROM_QSTR(MP_QSTR_IO44), MP_ROM_PTR(&pin_GPIO44) }, + { MP_ROM_QSTR(MP_QSTR_RX), MP_ROM_PTR(&pin_GPIO44) }, + + // GPIO45 : SPI chip select (J8) + { MP_ROM_QSTR(MP_QSTR_IO45), MP_ROM_PTR(&pin_GPIO45) }, + { MP_ROM_QSTR(MP_QSTR_SS), MP_ROM_PTR(&pin_GPIO45) }, + + { MP_ROM_QSTR(MP_QSTR_IO46), MP_ROM_PTR(&pin_GPIO46) }, + + { MP_ROM_QSTR(MP_QSTR_I2C), MP_ROM_PTR(&board_i2c_obj) }, + { MP_ROM_QSTR(MP_QSTR_SPI), MP_ROM_PTR(&board_spi_obj) }, + { MP_ROM_QSTR(MP_QSTR_UART), MP_ROM_PTR(&board_uart_obj) }, +}; +MP_DEFINE_CONST_DICT(board_module_globals, board_module_globals_table); diff --git a/ports/espressif/boards/elio_eliobot_s3/sdkconfig b/ports/espressif/boards/elio_eliobot_s3/sdkconfig new file mode 100644 index 00000000000..253a3984eb1 --- /dev/null +++ b/ports/espressif/boards/elio_eliobot_s3/sdkconfig @@ -0,0 +1,16 @@ +# +# Espressif IoT Development Framework Configuration +# +# +# Component config +# +# +# LWIP +# +CONFIG_LWIP_LOCAL_HOSTNAME="ELIOBOT" + +# end of LWIP + +# end of Component config + +# end of Espressif IoT Development Framework Configuration From 2e74a70636591b1a855f886b6f8aa4632a0abe96 Mon Sep 17 00:00:00 2001 From: Romain Boutrois Date: Thu, 17 Sep 2026 19:26:28 +0200 Subject: [PATCH 2/2] Eliobot S3: keep code.py, clarify boot.py and pin comments - board_init() no longer deletes code.py or creates main.py: the Elioblocs editor now writes code.py. - The default boot.py explains why the drive is read-only and how to change it. - GPIO15-16 are free pins (no 32 kHz crystal), and the NeoPixel count anticipates the next board revision. --- .../espressif/boards/elio_eliobot_s3/board.c | 22 +++++++++---------- .../boards/elio_eliobot_s3/mpconfigboard.h | 1 + ports/espressif/boards/elio_eliobot_s3/pins.c | 4 +--- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/ports/espressif/boards/elio_eliobot_s3/board.c b/ports/espressif/boards/elio_eliobot_s3/board.c index 09b7cd0bb9b..a83496500f1 100644 --- a/ports/espressif/boards/elio_eliobot_s3/board.c +++ b/ports/espressif/boards/elio_eliobot_s3/board.c @@ -20,21 +20,19 @@ void board_init(void) { FATFS *fatfs = &fs_mount->fatfs; FIL fs; UINT char_written = 0; - const byte buffer[] = "import board\nimport storage\n\n# Write options : True = Mass Storage, False = REPL\nstorage.remount(\"/\", False)\n"; - // Create or modify existing boot.py file + // Default boot.py: CIRCUITPY is read-only over USB so children can't + // break the robot's files from the drive, while code sent over the + // serial REPL or BLE (Eliobot's web editor) can still write them. + const byte buffer[] = + "import storage\n" + "\n" + "# Eliobot default: the USB drive is read-only, and code sent by\n" + "# the Elioblocs editor can write files. To edit files from the drive\n" + "# instead, comment out the next line.\n" + "storage.remount(\"/\", False)\n"; f_open(fatfs, &fs, "/boot.py", FA_WRITE | FA_CREATE_ALWAYS); f_write(&fs, buffer, sizeof(buffer) - 1, &char_written); f_close(&fs); - // Delete code.py, use main.py - mp_import_stat_t stat_c = mp_import_stat("code.py"); - if (stat_c == MP_IMPORT_STAT_FILE) { - f_unlink(fatfs, "/code.py"); - } - // Create main.py file - const byte buffer2[] = "print(\"Hello World!\")\n"; - f_open(fatfs, &fs, "/main.py", FA_WRITE | FA_CREATE_ALWAYS); - f_write(&fs, buffer2, sizeof(buffer2) - 1, &char_written); - f_close(&fs); } } diff --git a/ports/espressif/boards/elio_eliobot_s3/mpconfigboard.h b/ports/espressif/boards/elio_eliobot_s3/mpconfigboard.h index 7038c4e4efb..7919df48932 100644 --- a/ports/espressif/boards/elio_eliobot_s3/mpconfigboard.h +++ b/ports/espressif/boards/elio_eliobot_s3/mpconfigboard.h @@ -12,6 +12,7 @@ #define MICROPY_HW_MCU_NAME "ESP32S3" #define CIRCUITPY_DRIVE_LABEL "ELIOBOT" +// Current boards have one status NeoPixel; the next revision chains eight. #define MICROPY_HW_NEOPIXEL (&pin_GPIO1) #define MICROPY_HW_NEOPIXEL_COUNT (8) diff --git a/ports/espressif/boards/elio_eliobot_s3/pins.c b/ports/espressif/boards/elio_eliobot_s3/pins.c index 8267cb252e6..79f6e83aa61 100644 --- a/ports/espressif/boards/elio_eliobot_s3/pins.c +++ b/ports/espressif/boards/elio_eliobot_s3/pins.c @@ -60,10 +60,8 @@ static const mp_rom_map_elem_t board_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_IO14), MP_ROM_PTR(&pin_GPIO14) }, { MP_ROM_QSTR(MP_QSTR_LINE_5), MP_ROM_PTR(&pin_GPIO14) }, - // GPIO15 : XTAL_32K_P + // GPIO15-16 : free (no 32 kHz crystal fitted) { MP_ROM_QSTR(MP_QSTR_IO15), MP_ROM_PTR(&pin_GPIO15) }, - - // GPIO16 : XTAL_32K_N { MP_ROM_QSTR(MP_QSTR_IO16), MP_ROM_PTR(&pin_GPIO16) }, // GPIO17 : Buzzer