From fb08787c741ab537ef8c31e10bf3d1338c835136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Valenti=C4=87?= Date: Thu, 6 Aug 2026 15:35:34 +0200 Subject: [PATCH 1/3] Added BBCtrl support and combined lib for esp32s3 devices with CH340 chips --- .../boards/CM_-CombinedS3CH340/board.json | 18 ++++ .../esp32/boards/CM_-CombinedS3CH340/board.md | 3 + .../boards/CM_-CombinedS3CH340/manifest.py | 3 + .../CM_-CombinedS3CH340/mpconfigboard.cmake | 9 ++ .../CM_-CombinedS3CH340/mpconfigboard.h | 9 ++ .../CM_-CombinedS3CH340/sdkconfig.board | 7 ++ .../esp32/boards/CM_ButterBotCtrl/board.json | 16 +++ ports/esp32/boards/CM_ButterBotCtrl/board.md | 3 + .../esp32/boards/CM_ButterBotCtrl/manifest.py | 3 + .../CM_ButterBotCtrl/mpconfigboard.cmake | 9 ++ .../boards/CM_ButterBotCtrl/mpconfigboard.h | 9 ++ .../boards/CM_ButterBotCtrl/sdkconfig.board | 9 ++ ports/esp32/libraries/ButterBotCtrl/Pins.py | 100 ++++++++++++++++++ ports/esp32/libraries/ButterBotCtrl/_LED.py | 20 ++++ .../esp32/libraries/ButterBotCtrl/__init__.py | 43 ++++++++ ports/esp32/main.c | 2 +- ports/esp32/mphalport.c | 2 +- ports/esp32/usb.c | 2 +- 18 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 ports/esp32/boards/CM_-CombinedS3CH340/board.json create mode 100644 ports/esp32/boards/CM_-CombinedS3CH340/board.md create mode 100644 ports/esp32/boards/CM_-CombinedS3CH340/manifest.py create mode 100644 ports/esp32/boards/CM_-CombinedS3CH340/mpconfigboard.cmake create mode 100644 ports/esp32/boards/CM_-CombinedS3CH340/mpconfigboard.h create mode 100644 ports/esp32/boards/CM_-CombinedS3CH340/sdkconfig.board create mode 100644 ports/esp32/boards/CM_ButterBotCtrl/board.json create mode 100644 ports/esp32/boards/CM_ButterBotCtrl/board.md create mode 100644 ports/esp32/boards/CM_ButterBotCtrl/manifest.py create mode 100644 ports/esp32/boards/CM_ButterBotCtrl/mpconfigboard.cmake create mode 100644 ports/esp32/boards/CM_ButterBotCtrl/mpconfigboard.h create mode 100644 ports/esp32/boards/CM_ButterBotCtrl/sdkconfig.board create mode 100644 ports/esp32/libraries/ButterBotCtrl/Pins.py create mode 100644 ports/esp32/libraries/ButterBotCtrl/_LED.py create mode 100644 ports/esp32/libraries/ButterBotCtrl/__init__.py diff --git a/ports/esp32/boards/CM_-CombinedS3CH340/board.json b/ports/esp32/boards/CM_-CombinedS3CH340/board.json new file mode 100644 index 00000000000..d31a7c7a239 --- /dev/null +++ b/ports/esp32/boards/CM_-CombinedS3CH340/board.json @@ -0,0 +1,18 @@ +{ + "vendor": "CircuitMess", + "product": "ESP32S3 CH340 Devices", + "thumbnail": "", + "url": "https://circuitmess.com/", + "images": [], + + "docs": "", + "mcu": "esp32s3", + "features": [ + "USB-C", + "BLE", + "WiFi" + ], + "deploy": [ + "../deploy_s3.md" + ] +} diff --git a/ports/esp32/boards/CM_-CombinedS3CH340/board.md b/ports/esp32/boards/CM_-CombinedS3CH340/board.md new file mode 100644 index 00000000000..576ea80450f --- /dev/null +++ b/ports/esp32/boards/CM_-CombinedS3CH340/board.md @@ -0,0 +1,3 @@ +The following files are daily firmware for ESP32-based boards without external SPIRAM. + +This firmware is compiled using ESP-IDF v4.x. Some older releases are also provided that are compiled with ESP-IDF v3.x. diff --git a/ports/esp32/boards/CM_-CombinedS3CH340/manifest.py b/ports/esp32/boards/CM_-CombinedS3CH340/manifest.py new file mode 100644 index 00000000000..2459bfc2f06 --- /dev/null +++ b/ports/esp32/boards/CM_-CombinedS3CH340/manifest.py @@ -0,0 +1,3 @@ +include("$(PORT_DIR)/boards/manifest.py") + +package("ButterBotCtrl", base_path="$(PORT_DIR)/libraries") diff --git a/ports/esp32/boards/CM_-CombinedS3CH340/mpconfigboard.cmake b/ports/esp32/boards/CM_-CombinedS3CH340/mpconfigboard.cmake new file mode 100644 index 00000000000..5b1523ed3d7 --- /dev/null +++ b/ports/esp32/boards/CM_-CombinedS3CH340/mpconfigboard.cmake @@ -0,0 +1,9 @@ +set(IDF_TARGET esp32s3) + +set(SDKCONFIG_DEFAULTS + boards/sdkconfig.base + boards/sdkconfig.ble + boards/CM_-CombinedS3CH340/sdkconfig.board +) + +set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py) diff --git a/ports/esp32/boards/CM_-CombinedS3CH340/mpconfigboard.h b/ports/esp32/boards/CM_-CombinedS3CH340/mpconfigboard.h new file mode 100644 index 00000000000..93ef87da50f --- /dev/null +++ b/ports/esp32/boards/CM_-CombinedS3CH340/mpconfigboard.h @@ -0,0 +1,9 @@ +#define MICROPY_HW_BOARD_NAME "CircuitMess ESP32S3 CH340 Devices" +#define MICROPY_HW_MCU_NAME "ESP32S3" + +#define MICROPY_PY_MACHINE_DAC (0) + +// Enable UART REPL for modules that have an external USB-UART and don't use native USB. - set to 1 to enable +// These devices route USB-C through an onboard CH340 to UART0; the S3's native USB pins +// (GPIO19/20) are repurposed as regular GPIOs. +#define MICROPY_HW_ENABLE_UART_REPL (1) diff --git a/ports/esp32/boards/CM_-CombinedS3CH340/sdkconfig.board b/ports/esp32/boards/CM_-CombinedS3CH340/sdkconfig.board new file mode 100644 index 00000000000..4de2f2099a5 --- /dev/null +++ b/ports/esp32/boards/CM_-CombinedS3CH340/sdkconfig.board @@ -0,0 +1,7 @@ +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_ESPTOOLPY_FLASHFREQ_80M=y +CONFIG_ESPTOOLPY_AFTER_NORESET=y + +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_ESPTOOLPY_FLASHSIZE_8MB= +CONFIG_ESPTOOLPY_FLASHSIZE_16MB= diff --git a/ports/esp32/boards/CM_ButterBotCtrl/board.json b/ports/esp32/boards/CM_ButterBotCtrl/board.json new file mode 100644 index 00000000000..5e57121abd2 --- /dev/null +++ b/ports/esp32/boards/CM_ButterBotCtrl/board.json @@ -0,0 +1,16 @@ +{ + "vendor": "CircuitMess", + "product": "Butter Bot Controller", + "thumbnail": "", + "url": "https://circuitmess.com/", + "images": [], + + "docs": "", + "mcu": "esp32s3", + "features": [ + "USB-C" + ], + "deploy": [ + "../deploy_s3.md" + ] +} \ No newline at end of file diff --git a/ports/esp32/boards/CM_ButterBotCtrl/board.md b/ports/esp32/boards/CM_ButterBotCtrl/board.md new file mode 100644 index 00000000000..3d6a9113540 --- /dev/null +++ b/ports/esp32/boards/CM_ButterBotCtrl/board.md @@ -0,0 +1,3 @@ +The following files are daily firmware for ESP32-based boards without external SPIRAM. + +This firmware is compiled using ESP-IDF v4.x. Some older releases are also provided that are compiled with ESP-IDF v3.x. \ No newline at end of file diff --git a/ports/esp32/boards/CM_ButterBotCtrl/manifest.py b/ports/esp32/boards/CM_ButterBotCtrl/manifest.py new file mode 100644 index 00000000000..a7942cf7e1a --- /dev/null +++ b/ports/esp32/boards/CM_ButterBotCtrl/manifest.py @@ -0,0 +1,3 @@ +include("$(PORT_DIR)/boards/manifest.py") + +package("ButterBotCtrl", base_path="$(PORT_DIR)/libraries") \ No newline at end of file diff --git a/ports/esp32/boards/CM_ButterBotCtrl/mpconfigboard.cmake b/ports/esp32/boards/CM_ButterBotCtrl/mpconfigboard.cmake new file mode 100644 index 00000000000..2a8235ecd3d --- /dev/null +++ b/ports/esp32/boards/CM_ButterBotCtrl/mpconfigboard.cmake @@ -0,0 +1,9 @@ +set(IDF_TARGET esp32s3) + +set(SDKCONFIG_DEFAULTS + boards/sdkconfig.base + boards/sdkconfig.ble + boards/CM_ButterBotCtrl/sdkconfig.board +) + +set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py) \ No newline at end of file diff --git a/ports/esp32/boards/CM_ButterBotCtrl/mpconfigboard.h b/ports/esp32/boards/CM_ButterBotCtrl/mpconfigboard.h new file mode 100644 index 00000000000..c41bb203643 --- /dev/null +++ b/ports/esp32/boards/CM_ButterBotCtrl/mpconfigboard.h @@ -0,0 +1,9 @@ +#define MICROPY_HW_BOARD_NAME "CircuitMess Butter Bot Controller" +#define MICROPY_HW_MCU_NAME "ESP32S3" + +#define MICROPY_PY_MACHINE_DAC (0) + +// Enable UART REPL for modules that have an external USB-UART and don't use native USB. - set to 1 to enable +// Butter Bot Controller has no native USB (GPIO19/D- is used as the battery calibration +// reference); its USB-C routes through an onboard CH340 to UART0. +#define MICROPY_HW_ENABLE_UART_REPL (1) \ No newline at end of file diff --git a/ports/esp32/boards/CM_ButterBotCtrl/sdkconfig.board b/ports/esp32/boards/CM_ButterBotCtrl/sdkconfig.board new file mode 100644 index 00000000000..cb7820bf043 --- /dev/null +++ b/ports/esp32/boards/CM_ButterBotCtrl/sdkconfig.board @@ -0,0 +1,9 @@ +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_ESPTOOLPY_FLASHFREQ_80M=y +CONFIG_ESPTOOLPY_AFTER_NORESET=y + +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_ESPTOOLPY_FLASHSIZE_8MB= +CONFIG_ESPTOOLPY_FLASHSIZE_16MB= +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/noota_4MB.csv" \ No newline at end of file diff --git a/ports/esp32/libraries/ButterBotCtrl/Pins.py b/ports/esp32/libraries/ButterBotCtrl/Pins.py new file mode 100644 index 00000000000..7bd5fbc917f --- /dev/null +++ b/ports/esp32/libraries/ButterBotCtrl/Pins.py @@ -0,0 +1,100 @@ +from micropython import const + + +class Pins: + LED_WIFI: int = const(0) + LED_POWER: int = const(1) + LED_BATTLOW: int = const(2) + LED_BIG: int = const(3) + + BTN_MANOVER: int = const(4) + BTN_POKE: int = const(5) + BTN_SHUTUP: int = const(6) + BTN_SUMMON: int = const(7) + BTN_JOY: int = const(8) + + JOY_V: int = const(9) + JOY_H: int = const(10) + + TFT_SCK: int = const(11) + TFT_SDA: int = const(12) + TFT_DC: int = const(13) + TFT_RST: int = const(14) + LED_BACKLIGHT: int = const(15) + + PIN_BATT: int = const(16) + PIN_VREF: int = const(17) + + Rev1Map = { + LED_WIFI: const(7), + LED_POWER: const(8), + LED_BATTLOW: const(9), + LED_BIG: const(46), + + BTN_MANOVER: const(10), + BTN_POKE: const(11), + BTN_SHUTUP: const(18), + BTN_SUMMON: const(13), + BTN_JOY: const(21), + + JOY_V: const(4), + JOY_H: const(5), + + TFT_SCK: const(14), + TFT_SDA: const(15), + TFT_DC: const(16), + TFT_RST: const(17), + LED_BACKLIGHT: const(45), + + PIN_BATT: const(6), + PIN_VREF: const(19), + } + + def __init__(self, revision): + self.currentMap = None + if revision == 0: + self.currentMap = self.Rev1Map + else: + print("Unknown revision", revision) + + def get(self, pin: int) -> int: + if not pin in self.currentMap: + print("Pin", pin, "not in map") + return -1 + + return self.currentMap[pin] + + +class Buttons: + ManualOverride: int = const(0) + Poke: int = const(1) + ShutUp: int = const(2) + Summon: int = const(3) + Joystick: int = const(4) + + def __init__(self, pins: Pins): + # Maps Buttons [0-4] to their respective GPIO pins + self.Pins: [int] = const([ + pins.get(Pins.BTN_MANOVER), pins.get(Pins.BTN_POKE), pins.get(Pins.BTN_SHUTUP), + pins.get(Pins.BTN_SUMMON), pins.get(Pins.BTN_JOY) + ]) + + def get_pins_array(self) -> [int]: + return self.Pins + + +class LEDs: + BigGreen: int = const(0) + Wifi: int = const(1) + Power: int = const(2) + BatteryLow: int = const(3) + + def __init__(self, pins: Pins): + # Maps LEDs to their respective GPIO pins + self.pins: [int] = const([ + pins.get(Pins.LED_BIG), pins.get(Pins.LED_WIFI), pins.get(Pins.LED_POWER), + pins.get(Pins.LED_BATTLOW) + ]) + + def get_pins_array(self) -> [int]: + return self.pins \ No newline at end of file diff --git a/ports/esp32/libraries/ButterBotCtrl/_LED.py b/ports/esp32/libraries/ButterBotCtrl/_LED.py new file mode 100644 index 00000000000..2a2709dae55 --- /dev/null +++ b/ports/esp32/libraries/ButterBotCtrl/_LED.py @@ -0,0 +1,20 @@ +from CircuitOS import SingleLED + + +class LED: + def __init__(self, pins: [int]): + self.leds = [SingleLED(pin) for pin in pins] + + def set(self, led: int, value: int): + """ + @param led: LED from LEDs + @param value: value from 0 to 100 + """ + if led < 0 or led >= len(self.leds): + return + + self.leds[led].set(value) + + def set_all(self, value: int): + for led in self.leds: + led.set(value) \ No newline at end of file diff --git a/ports/esp32/libraries/ButterBotCtrl/__init__.py b/ports/esp32/libraries/ButterBotCtrl/__init__.py new file mode 100644 index 00000000000..ab8aa550cdf --- /dev/null +++ b/ports/esp32/libraries/ButterBotCtrl/__init__.py @@ -0,0 +1,43 @@ +from machine import SPI, Pin, Signal, ADC +from CircuitOS import InputGPIO, Display, PanelST7735_128x128, SliderADC, Sliders +from .Pins import * +from ._LED import LED + +pins = Pins(0) +btn_pins = Buttons(pins) +led_pins = LEDs(pins) + +spiTFT: SPI = SPI(1, baudrate=40000000, polarity=0, phase=0, sck=Pin(pins.get(Pins.TFT_SCK)), + mosi=Pin(pins.get(Pins.TFT_SDA))) + +blPin = Pin(pins.get(Pins.LED_BACKLIGHT), mode=Pin.OUT, value=True) +backlight = Signal(blPin, invert=True) + +# GPIO19 high injects the 2.5V calibration reference onto the battery ADC node - keep it off +vref = Pin(pins.get(Pins.PIN_VREF), mode=Pin.OUT, value=False) + +buttons = InputGPIO(btn_pins.get_pins_array(), inverted=True) + +joystick_x = SliderADC(pins.get(Pins.JOY_H), min=0, max=4096, ema_a=0.05, width=ADC.WIDTH_12BIT) +joystick_y = SliderADC(pins.get(Pins.JOY_V), min=0, max=4096, ema_a=0.05, width=ADC.WIDTH_12BIT) +joystick = Sliders([joystick_x, joystick_y]) + +leds = LED(led_pins.pins) + +dc = Pin(pins.get(Pins.TFT_DC), Pin.OUT) +reset = Pin(pins.get(Pins.TFT_RST), Pin.OUT) +panel = PanelST7735_128x128(spiTFT, dc=dc, reset=reset, rotation=0, + rotations=[(0x00, 128, 128, 0, 0)]) +display = Display(panel) + + +def begin(): + panel.init() + + display.fill(Display.Color.Black) + display.commit() + + backlight.on() + + buttons.scan() + joystick.scan() \ No newline at end of file diff --git a/ports/esp32/main.c b/ports/esp32/main.c index 43caf6e9663..4c71096a3c2 100644 --- a/ports/esp32/main.c +++ b/ports/esp32/main.c @@ -92,7 +92,7 @@ void mp_task(void *pvParameter) { #endif #if CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG usb_serial_jtag_init(); - #elif CONFIG_USB_OTG_SUPPORTED + #elif CONFIG_USB_OTG_SUPPORTED && !MICROPY_HW_ENABLE_UART_REPL usb_init(); #endif #if MICROPY_HW_ENABLE_UART_REPL diff --git a/ports/esp32/mphalport.c b/ports/esp32/mphalport.c index 538b3e405c9..6df84c765ab 100644 --- a/ports/esp32/mphalport.c +++ b/ports/esp32/mphalport.c @@ -127,7 +127,7 @@ void mp_hal_stdout_tx_strn(const char *str, size_t len) { } #if CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG usb_serial_jtag_tx_strn(str, len); - #elif CONFIG_USB_OTG_SUPPORTED + #elif CONFIG_USB_OTG_SUPPORTED && !MICROPY_HW_ENABLE_UART_REPL usb_tx_strn(str, len); #endif #if MICROPY_HW_ENABLE_UART_REPL diff --git a/ports/esp32/usb.c b/ports/esp32/usb.c index 2e417755c9a..8413163db32 100644 --- a/ports/esp32/usb.c +++ b/ports/esp32/usb.c @@ -28,7 +28,7 @@ #include "py/mphal.h" #include "usb.h" -#if CONFIG_USB_OTG_SUPPORTED && !CONFIG_ESP_CONSOLE_USB_CDC && !CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG +#if CONFIG_USB_OTG_SUPPORTED && !CONFIG_ESP_CONSOLE_USB_CDC && !CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG && !MICROPY_HW_ENABLE_UART_REPL #include "esp_timer.h" #ifndef NO_QSTR From d001f823391ed95c76b50e93828cb36494c08754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Valenti=C4=87?= Date: Fri, 7 Aug 2026 14:19:02 +0200 Subject: [PATCH 2/3] Added ButterBot support --- .../boards/CM_-CombinedS3CH340/manifest.py | 1 + ports/esp32/boards/CM_ButterBot/board.json | 16 +++ ports/esp32/boards/CM_ButterBot/board.md | 3 + ports/esp32/boards/CM_ButterBot/manifest.py | 3 + .../boards/CM_ButterBot/mpconfigboard.cmake | 9 ++ .../esp32/boards/CM_ButterBot/mpconfigboard.h | 12 ++ .../esp32/boards/CM_ButterBot/sdkconfig.board | 9 ++ ports/esp32/libraries/ButterBot/Motors.py | 116 ++++++++++++++++++ ports/esp32/libraries/ButterBot/Pins.py | 58 +++++++++ ports/esp32/libraries/ButterBot/_LED.py | 22 ++++ ports/esp32/libraries/ButterBot/__init__.py | 39 ++++++ 11 files changed, 288 insertions(+) create mode 100644 ports/esp32/boards/CM_ButterBot/board.json create mode 100644 ports/esp32/boards/CM_ButterBot/board.md create mode 100644 ports/esp32/boards/CM_ButterBot/manifest.py create mode 100644 ports/esp32/boards/CM_ButterBot/mpconfigboard.cmake create mode 100644 ports/esp32/boards/CM_ButterBot/mpconfigboard.h create mode 100644 ports/esp32/boards/CM_ButterBot/sdkconfig.board create mode 100644 ports/esp32/libraries/ButterBot/Motors.py create mode 100644 ports/esp32/libraries/ButterBot/Pins.py create mode 100644 ports/esp32/libraries/ButterBot/_LED.py create mode 100644 ports/esp32/libraries/ButterBot/__init__.py diff --git a/ports/esp32/boards/CM_-CombinedS3CH340/manifest.py b/ports/esp32/boards/CM_-CombinedS3CH340/manifest.py index 2459bfc2f06..9fed784de4e 100644 --- a/ports/esp32/boards/CM_-CombinedS3CH340/manifest.py +++ b/ports/esp32/boards/CM_-CombinedS3CH340/manifest.py @@ -1,3 +1,4 @@ include("$(PORT_DIR)/boards/manifest.py") package("ButterBotCtrl", base_path="$(PORT_DIR)/libraries") +package("ButterBot", base_path="$(PORT_DIR)/libraries") diff --git a/ports/esp32/boards/CM_ButterBot/board.json b/ports/esp32/boards/CM_ButterBot/board.json new file mode 100644 index 00000000000..0a0fbac92f1 --- /dev/null +++ b/ports/esp32/boards/CM_ButterBot/board.json @@ -0,0 +1,16 @@ +{ + "vendor": "CircuitMess", + "product": "Butter Bot", + "thumbnail": "", + "url": "https://circuitmess.com/", + "images": [], + + "docs": "", + "mcu": "esp32s3", + "features": [ + "USB-C" + ], + "deploy": [ + "../deploy_s3.md" + ] +} diff --git a/ports/esp32/boards/CM_ButterBot/board.md b/ports/esp32/boards/CM_ButterBot/board.md new file mode 100644 index 00000000000..576ea80450f --- /dev/null +++ b/ports/esp32/boards/CM_ButterBot/board.md @@ -0,0 +1,3 @@ +The following files are daily firmware for ESP32-based boards without external SPIRAM. + +This firmware is compiled using ESP-IDF v4.x. Some older releases are also provided that are compiled with ESP-IDF v3.x. diff --git a/ports/esp32/boards/CM_ButterBot/manifest.py b/ports/esp32/boards/CM_ButterBot/manifest.py new file mode 100644 index 00000000000..7a7cdfe49b4 --- /dev/null +++ b/ports/esp32/boards/CM_ButterBot/manifest.py @@ -0,0 +1,3 @@ +include("$(PORT_DIR)/boards/manifest.py") + +package("ButterBot", base_path="$(PORT_DIR)/libraries") diff --git a/ports/esp32/boards/CM_ButterBot/mpconfigboard.cmake b/ports/esp32/boards/CM_ButterBot/mpconfigboard.cmake new file mode 100644 index 00000000000..cf36e22f96f --- /dev/null +++ b/ports/esp32/boards/CM_ButterBot/mpconfigboard.cmake @@ -0,0 +1,9 @@ +set(IDF_TARGET esp32s3) + +set(SDKCONFIG_DEFAULTS + boards/sdkconfig.base + boards/sdkconfig.ble + boards/CM_ButterBot/sdkconfig.board +) + +set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py) diff --git a/ports/esp32/boards/CM_ButterBot/mpconfigboard.h b/ports/esp32/boards/CM_ButterBot/mpconfigboard.h new file mode 100644 index 00000000000..f0307ce9e49 --- /dev/null +++ b/ports/esp32/boards/CM_ButterBot/mpconfigboard.h @@ -0,0 +1,12 @@ +#define MICROPY_HW_BOARD_NAME "CircuitMess Butter Bot" +#define MICROPY_HW_MCU_NAME "ESP32S3" + +#define MICROPY_PY_MACHINE_DAC (0) + +#define MICROPY_HW_I2C0_SCL (48) +#define MICROPY_HW_I2C0_SDA (47) + +// Enable UART REPL for modules that have an external USB-UART and don't use native USB. - set to 1 to enable +// Butter Bot has no native USB (GPIO19/20 are used as the accelerometer and motor base board +// interrupts); its USB-C routes through an onboard CH340 to UART0. +#define MICROPY_HW_ENABLE_UART_REPL (1) diff --git a/ports/esp32/boards/CM_ButterBot/sdkconfig.board b/ports/esp32/boards/CM_ButterBot/sdkconfig.board new file mode 100644 index 00000000000..381e92aef45 --- /dev/null +++ b/ports/esp32/boards/CM_ButterBot/sdkconfig.board @@ -0,0 +1,9 @@ +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_ESPTOOLPY_FLASHFREQ_80M=y +CONFIG_ESPTOOLPY_AFTER_NORESET=y + +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_ESPTOOLPY_FLASHSIZE_8MB= +CONFIG_ESPTOOLPY_FLASHSIZE_16MB= +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/noota_4MB.csv" diff --git a/ports/esp32/libraries/ButterBot/Motors.py b/ports/esp32/libraries/ButterBot/Motors.py new file mode 100644 index 00000000000..a995d62a3c6 --- /dev/null +++ b/ports/esp32/libraries/ButterBot/Motors.py @@ -0,0 +1,116 @@ +import time +import ustruct +from machine import I2C, Timer +from micropython import const + + +class Motor: + Left = const(0) + Right = const(1) + + +class Motors: + REG_IDENTIFY = 0x00 + REG_RESET = 0x01 + REG_MOTOR_L = 0x02 + + # The base board stops both motors if it doesn't receive a motor command within 500ms + # (dead-man switch), so the current speeds are re-sent every 250ms while any motor is running. + RESEND_PERIOD = 250 + + # Setup time needed between a reset and the following command [ms] + RESET_DELAY = 200 + + def __init__(self, bus: I2C, addr: int = 0x69, timer_id: int = 0): + self.bus = bus + self.addr = addr + self.timer = Timer(timer_id) + self.__values = [0] * 2 + + def begin(self) -> bool: + try: + ident = self.bus.readfrom_mem(self.addr, self.REG_IDENTIFY, 1)[0] + except OSError: + print("Motor base board not responding on address", self.addr) + return False + + if ident != self.addr: + print("Motor base board ID mismatch: expected", self.addr, ", got", ident) + return False + + self.timer.deinit() + self.__values = [0, 0] + + # Reset is a bare command byte with no register data + self.bus.writeto(self.addr, bytes([self.REG_RESET])) + time.sleep_ms(self.RESET_DELAY) + + return True + + def set(self, motor: int, value: int): + if motor < 0 or motor >= 2: + return + + value = min(max(value, -100), 100) + if value == self.__values[motor]: + return + + self.__values[motor] = value + self.__send() + + def get(self, motor: int): + if motor < 0 or motor >= 2: + return 0 + + return self.__values[motor] + + def set_left(self, value: int): + self.set(Motor.Left, value) + + def set_right(self, value: int): + self.set(Motor.Right, value) + + def get_left(self): + return self.get(Motor.Left) + + def get_right(self): + return self.get(Motor.Right) + + def set_all(self, val: int | [int] | (int, int)): + if type(val) == int: + self.set_all((val, val)) + return + + if type(val) == tuple or type(val) == list: + if len(val) != 2: + return + + values = [min(max(v, -100), 100) for v in val] + if values == self.__values: + return + + self.__values = values + self.__send() + + def get_all(self): + return tuple(self.get(i) for i in range(2)) + + def stop_all(self): + self.set_all((0, 0)) + + def __send(self): + self.__write() + + if self.__values[Motor.Left] == 0 and self.__values[Motor.Right] == 0: + self.timer.deinit() + else: + self.timer.init(mode=Timer.PERIODIC, period=self.RESEND_PERIOD, callback=self.__resend) + + def __resend(self, timer): + self.__write() + + def __write(self): + # Motor direction is inverted between the user-facing API and the base board. + # Both motor registers are written in a single sequential transfer. + data = ustruct.pack(" int: + if not pin in self.currentMap: + print("Pin", pin, "not in map") + return -1 + return self.currentMap[pin] + + +class Buttons: + Power: int = const(0) + + def __init__(self, pins: Pins): + # Maps Buttons [0] to their respective GPIO pins + self.Pins: [int] = const([ + pins.get(Pins.BTN_POWER) + ]) + + def get_pins_array(self) -> [int]: + return self.Pins + + +class LEDs: + Status: int = const(0) + + def __init__(self, pins: Pins): + # Maps LEDs to their respective TCA9555 expander pins + self.pins: [int] = const([ + Pins.EXP_LED + ]) + + def get_pins_array(self) -> [int]: + return self.pins diff --git a/ports/esp32/libraries/ButterBot/_LED.py b/ports/esp32/libraries/ButterBot/_LED.py new file mode 100644 index 00000000000..08f5e4f51ee --- /dev/null +++ b/ports/esp32/libraries/ButterBot/_LED.py @@ -0,0 +1,22 @@ +from CircuitOS import PCA95XX + + +class LED: + def __init__(self, expander: PCA95XX, pins: [int]): + self.expander = expander + self.pins = pins + + def set(self, led: int, value: int): + """ + @param led: LED from LEDs + @param value: value from 0 to 100. The LED is on the TCA9555 expander and can't be + dimmed - any value above 0 turns it on + """ + if led < 0 or led >= len(self.pins): + return + + self.expander.pin_write(self.pins[led], value > 0) + + def set_all(self, value: int): + for pin in self.pins: + self.expander.pin_write(pin, value > 0) diff --git a/ports/esp32/libraries/ButterBot/__init__.py b/ports/esp32/libraries/ButterBot/__init__.py new file mode 100644 index 00000000000..3f6190d6049 --- /dev/null +++ b/ports/esp32/libraries/ButterBot/__init__.py @@ -0,0 +1,39 @@ +from machine import Pin, I2C +from CircuitOS import InputGPIO, PCA95XX +from .Pins import * +from .Motors import Motors, Motor +from ._LED import LED + +pins = Pins(0) +btn_pins = Buttons(pins) +led_pins = LEDs(pins) + +# GPIO4 is the power latch - driving it low powers the device off. Keep it hi-Z (input, no pulls) +pwdn = Pin(pins.get(Pins.PIN_PWDN), mode=Pin.IN, pull=None) + +i2c = I2C(0, sda=Pin(pins.get(Pins.I2C_SDA)), scl=Pin(pins.get(Pins.I2C_SCL))) + +pca9555 = PCA95XX(i2c, 0x20) +pca9555.begin() + +leds = LED(pca9555, led_pins.pins) + +# The button is active-high with external biasing +buttons = InputGPIO(btn_pins.get_pins_array(), inverted=False) + +motors = Motors(i2c) + + +def begin(): + pca9555.pin_mode(Pins.EXP_LED, Pin.OUT) + leds.set_all(0) + + # The speaker amp's SD/mode pin floats after the expander reset - drive it low to mute + pca9555.pin_mode(Pins.EXP_SD_MODE, Pin.OUT) + pca9555.pin_write(Pins.EXP_SD_MODE, False) + + motors.begin() + + motors.set(Motor.Left, 100) + + buttons.scan() From b8fabfefa8063abbc2e61fa9c063c5a742f7c30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Valenti=C4=87?= Date: Fri, 7 Aug 2026 16:05:52 +0200 Subject: [PATCH 3/3] ButterBot fix: added InputGPIO argument to disable pulls --- ports/esp32/libraries/ButterBot/__init__.py | 6 ++---- ports/esp32/libraries/CircuitOS/_Input.py | 8 ++++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ports/esp32/libraries/ButterBot/__init__.py b/ports/esp32/libraries/ButterBot/__init__.py index 3f6190d6049..faa4b4400e0 100644 --- a/ports/esp32/libraries/ButterBot/__init__.py +++ b/ports/esp32/libraries/ButterBot/__init__.py @@ -18,8 +18,8 @@ leds = LED(pca9555, led_pins.pins) -# The button is active-high with external biasing -buttons = InputGPIO(btn_pins.get_pins_array(), inverted=False) +# The button is active-high with external biasing - internal pulls would load the node, so leave them off +buttons = InputGPIO(btn_pins.get_pins_array(), inverted=False, pull=None) motors = Motors(i2c) @@ -34,6 +34,4 @@ def begin(): motors.begin() - motors.set(Motor.Left, 100) - buttons.scan() diff --git a/ports/esp32/libraries/CircuitOS/_Input.py b/ports/esp32/libraries/CircuitOS/_Input.py index 6d8692bbc3c..d93509407a2 100644 --- a/ports/esp32/libraries/CircuitOS/_Input.py +++ b/ports/esp32/libraries/CircuitOS/_Input.py @@ -102,14 +102,18 @@ def LH(pin: Pin): class InputGPIO(Input): - def __init__(self, pins: [int], inverted: bool = False): + def __init__(self, pins: [int], inverted: bool = False, pull: int = -1): super().__init__(len(pins)) self.pins = [] self.signals = [] + # pull: -1 selects the pull automatically from 'inverted', None disables pulls (externally biased inputs) + if pull == -1: + pull = Pin.PULL_UP if inverted else Pin.PULL_DOWN + for i in range(len(pins)): - pin = Pin(pins[i], mode=Pin.IN, pull= Pin.PULL_UP if inverted else Pin.PULL_DOWN) + pin = Pin(pins[i], mode=Pin.IN, pull=pull) signal = Signal(pin, invert=inverted) self.pins.append(pin)