From df9b714af4f98960f03e951e0a71e994be79c006 Mon Sep 17 00:00:00 2001 From: Alessandro Gatti Date: Thu, 12 Mar 2026 13:21:46 +0100 Subject: [PATCH 01/53] docs/library/re: Document non-capturing grouping. This commit updates the documentation for the `re` library, officially documenting non-capturing grouping rules (ie. "(?:...)"). The documentation mistakenly marked that feature as not supported, but is is indeed supported in the current iteration of the regex library. This closes #18900. Signed-off-by: Alessandro Gatti --- docs/library/re.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/library/re.rst b/docs/library/re.rst index b8aeefd90cfa4..47f623c5600c8 100644 --- a/docs/library/re.rst +++ b/docs/library/re.rst @@ -54,6 +54,10 @@ Supported operators and special sequences are: Grouping. Each group is capturing (a substring it captures can be accessed with `match.group()` method). +``(?:...)`` + Non-capturing grouping. Each group is matched using the same rules as + regular grouping, but will not be part of the match object. + ``\d`` Matches digit. Equivalent to ``[0-9]``. @@ -87,7 +91,6 @@ Supported operators and special sequences are: * counted repetitions (``{m,n}``) * named groups (``(?P...)``) -* non-capturing groups (``(?:...)``) * more advanced assertions (``\b``, ``\B``) * special character escapes like ``\r``, ``\n`` - use Python's own escaping instead From 06dbc1f48653badb5488fece192f233fc13dc2a8 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 12 Mar 2026 15:05:36 +1100 Subject: [PATCH 02/53] github: Revert "Run esp32&zephyr daily to keep mstr branch caches hot". This reverts commit 046013a1ffbeccb971b6067ff389ebd0350b9e9c. Looks like since the latest round of GitHub Actions updates, the Cache LRU algorithm is working as designed again. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- .github/workflows/ports_esp32.yml | 4 ---- .github/workflows/ports_zephyr.yml | 4 ---- 2 files changed, 8 deletions(-) diff --git a/.github/workflows/ports_esp32.yml b/.github/workflows/ports_esp32.yml index 87ab6dbe35543..446db794cb43f 100644 --- a/.github/workflows/ports_esp32.yml +++ b/.github/workflows/ports_esp32.yml @@ -12,10 +12,6 @@ on: - 'lib/**' - 'drivers/**' - 'ports/esp32/**' - schedule: - # Scheduled run exists to keep master branch ESP-IDF cache entry hot - # and prevent creating many redundant per-branch cache entries instead. - - cron: "20 0 * * *" concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/ports_zephyr.yml b/.github/workflows/ports_zephyr.yml index 571a443e903f8..330121d1de648 100644 --- a/.github/workflows/ports_zephyr.yml +++ b/.github/workflows/ports_zephyr.yml @@ -12,10 +12,6 @@ on: - 'lib/**' - 'ports/zephyr/**' - 'tests/**' - schedule: - # Scheduled run exists to keep master branch Zephyr cache entry hot - # and prevent creating many redundant per-branch cache entries instead. - - cron: "40 4 * * *" concurrency: group: ${{ github.workflow }}-${{ github.ref }} From d2cda57e9d6a6dedecf7f581c4e4e960d9fdd877 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 12 Mar 2026 10:50:19 +1100 Subject: [PATCH 03/53] rp2/rp2_dma: Disable DMA IRQ before clearing handler function. Both the overall IRQ line and the per-channel IRQ, for good measure. Otherwise, soft reset will remove the handler before the finaliser for the DMA object(s) run and trigger IRQs if the channel is still active. Closes #18765 This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- ports/rp2/rp2_dma.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ports/rp2/rp2_dma.c b/ports/rp2/rp2_dma.c index 471cf24ea2ce5..bb935f3b4846c 100644 --- a/ports/rp2/rp2_dma.c +++ b/ports/rp2/rp2_dma.c @@ -427,6 +427,9 @@ static mp_obj_t rp2_dma_close(mp_obj_t self_in) { uint8_t channel = self->channel; if (channel != CHANNEL_CLOSED) { + // Disable channel IRQ + dma_channel_set_irq0_enabled(channel, false); + // Reset this channel's registers to their default values (zeros). dma_channel_config config = { .ctrl = 0 }; dma_channel_configure(channel, &config, NULL, NULL, 0, false); @@ -441,7 +444,6 @@ static mp_obj_t rp2_dma_close(mp_obj_t self_in) { if (irq) { irq->parent = MP_OBJ_NULL; irq->handler = MP_OBJ_NULL; - dma_channel_set_irq0_enabled(channel, false); } dma_channel_unclaim(channel); self->channel = CHANNEL_CLOSED; @@ -479,7 +481,8 @@ void rp2_dma_init(void) { } void rp2_dma_deinit(void) { - // Remove our interrupt handler. + // Disable and remove our interrupt handler. + irq_set_enabled(DMA_IRQ_0, false); irq_remove_handler(DMA_IRQ_0, rp2_dma_irq_handler); } From 82c6b0e59486496d98d8c401924d1ce6a7b6d523 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 12 Mar 2026 14:20:29 +1100 Subject: [PATCH 04/53] esp32: Only check the lockfile currently used by the build. Small tweak to avoid changes in other targets' lockfiles from printing warnings when building esp32 port. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- ports/esp32/CMakeLists.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ports/esp32/CMakeLists.txt b/ports/esp32/CMakeLists.txt index ee062be203d40..c739368c55027 100644 --- a/ports/esp32/CMakeLists.txt +++ b/ports/esp32/CMakeLists.txt @@ -63,19 +63,20 @@ set(SDKCONFIG_DEFAULTS ${CMAKE_BINARY_DIR}/sdkconfig.combined) include($ENV{IDF_PATH}/tools/cmake/project.cmake) # Generate individual dependencies.lock files based on chip target -idf_build_set_property(DEPENDENCIES_LOCK lockfiles/dependencies.lock.${IDF_TARGET}) +set(LOCKFILE_PATH lockfiles/dependencies.lock.${IDF_TARGET}) +idf_build_set_property(DEPENDENCIES_LOCK ${LOCKFILE_PATH}) # Define the project. project(micropython) # Check for lockfile changes and either warn or error depending on build type message("Checking lockfile contents...") -execute_process(COMMAND git diff --exit-code lockfiles/ WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} +execute_process(COMMAND git diff --exit-code ${LOCKFILE_PATH} WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} RESULT_VARIABLE RES) if (RES) # Maintainer builds (CI or autobuild runs) should fail if this has happened if($ENV{MICROPY_MAINTAINER_BUILD}) - message(FATAL_ERROR "Failing build as lockfiles are dirty (see above). Check that ESP-IDF versions match.") + message(FATAL_ERROR "Failing build as lockfile is dirty (see above). Check that ESP-IDF versions match.") else() message(WARNING "Component lockfile contents have changed (see above). This may be due to building with a different ESP-IDF version. Please mention this output if reporting an issue with MicroPython.") endif() From fe32e1d3a17d4210bbdc342bb2e291910d7bfec9 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 12 Mar 2026 12:00:20 +1100 Subject: [PATCH 05/53] esp32: Drop support for ESP-IDF --- ports/esp32/README.md | 2 +- ports/esp32/esp32_common.cmake | 8 -------- ports/esp32/lockfiles/dependencies.lock.esp32 | 2 +- ports/esp32/lockfiles/dependencies.lock.esp32c2 | 2 +- ports/esp32/lockfiles/dependencies.lock.esp32c3 | 2 +- ports/esp32/lockfiles/dependencies.lock.esp32c5 | 2 +- ports/esp32/lockfiles/dependencies.lock.esp32c6 | 2 +- ports/esp32/lockfiles/dependencies.lock.esp32p4 | 2 +- ports/esp32/lockfiles/dependencies.lock.esp32s2 | 2 +- ports/esp32/lockfiles/dependencies.lock.esp32s3 | 2 +- ports/esp32/machine_bitstream.c | 7 ------- ports/esp32/machine_touchpad.c | 8 -------- ports/esp32/main/idf_component.yml | 3 +-- ports/esp32/modnetwork.h | 4 ++-- ports/esp32/network_wlan.c | 4 ---- ports/esp32/usb_serial_jtag.c | 2 -- 16 files changed, 12 insertions(+), 42 deletions(-) diff --git a/ports/esp32/README.md b/ports/esp32/README.md index b5cd1c2a8c6bb..2cfc09afadf2c 100644 --- a/ports/esp32/README.md +++ b/ports/esp32/README.md @@ -53,7 +53,7 @@ build environment and toolchains needed to build the firmware. The ESP-IDF changes quickly and MicroPython only supports certain versions. The current recommended version of ESP-IDF for MicroPython is v5.5.1. MicroPython -also supports v5.2, v5.2.2, v5.3, v5.4, v5.4.1 and v5.4.2. +also supports v5.3, v5.4, v5.4.1 and v5.4.2. To install the ESP-IDF the full instructions can be found at the [Espressif Getting Started guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/index.html#installation-step-by-step). diff --git a/ports/esp32/esp32_common.cmake b/ports/esp32/esp32_common.cmake index d6a4aedb85c68..5ad7f7e003e81 100644 --- a/ports/esp32/esp32_common.cmake +++ b/ports/esp32/esp32_common.cmake @@ -277,14 +277,6 @@ target_compile_options(${MICROPY_TARGET} PUBLIC target_include_directories(${MICROPY_TARGET} PUBLIC ${IDF_PATH}/components/bt/host/nimble/nimble ) -if (IDF_VERSION VERSION_LESS "5.3") -# Additional include directories needed for private RMT header. -# IDF 5.x versions before 5.3.1 - message(STATUS "Using private rmt headers for ${IDF_VERSION}") - target_include_directories(${MICROPY_TARGET} PRIVATE - ${IDF_PATH}/components/driver/rmt - ) -endif() # Add additional extmod and usermod components. if (MICROPY_PY_BTREE) diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32 b/ports/esp32/lockfiles/dependencies.lock.esp32 index 8ba25c77011c2..e242478083e3d 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32 @@ -30,6 +30,6 @@ direct_dependencies: - espressif/lan867x - espressif/mdns - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32c2 b/ports/esp32/lockfiles/dependencies.lock.esp32c2 index 8a366af342369..a6bbf8d61c837 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32c2 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32c2 @@ -16,6 +16,6 @@ dependencies: direct_dependencies: - espressif/mdns - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32c2 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32c3 b/ports/esp32/lockfiles/dependencies.lock.esp32c3 index 3aa99692d9f35..4f99727f4e8f0 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32c3 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32c3 @@ -16,6 +16,6 @@ dependencies: direct_dependencies: - espressif/mdns - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32c3 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32c5 b/ports/esp32/lockfiles/dependencies.lock.esp32c5 index 2fb130b8e5282..ac6b1d99d915d 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32c5 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32c5 @@ -16,6 +16,6 @@ dependencies: direct_dependencies: - espressif/mdns - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32c5 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32c6 b/ports/esp32/lockfiles/dependencies.lock.esp32c6 index c81806909a631..8dfb4d77bc26e 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32c6 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32c6 @@ -16,6 +16,6 @@ dependencies: direct_dependencies: - espressif/mdns - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32c6 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32p4 b/ports/esp32/lockfiles/dependencies.lock.esp32p4 index aea6ec2cc3607..4582830b5b902 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32p4 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32p4 @@ -88,6 +88,6 @@ direct_dependencies: - espressif/mdns - espressif/tinyusb - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32p4 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32s2 b/ports/esp32/lockfiles/dependencies.lock.esp32s2 index 8717181c10ac7..417b999f85a86 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32s2 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32s2 @@ -32,6 +32,6 @@ direct_dependencies: - espressif/mdns - espressif/tinyusb - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32s2 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32s3 b/ports/esp32/lockfiles/dependencies.lock.esp32s3 index 0b8b8e92bd57b..f0376c9a8d60a 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32s3 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32s3 @@ -32,6 +32,6 @@ direct_dependencies: - espressif/mdns - espressif/tinyusb - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32s3 version: 2.0.0 diff --git a/ports/esp32/machine_bitstream.c b/ports/esp32/machine_bitstream.c index 60addcc15b635..d44f9e71a40ae 100644 --- a/ports/esp32/machine_bitstream.c +++ b/ports/esp32/machine_bitstream.c @@ -95,9 +95,6 @@ static void IRAM_ATTR machine_bitstream_high_low_bitbang(mp_hal_pin_obj_t pin, u /******************************************************************************/ // RMT implementation -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) -#include "rmt_private.h" -#endif #include "driver/rmt_tx.h" #include "driver/rmt_encoder.h" @@ -159,11 +156,7 @@ static bool machine_bitstream_high_low_rmt(mp_hal_pin_obj_t pin, uint32_t *timin // Disable and release channel. check_esp_err(rmt_del_encoder(encoder)); rmt_disable(channel); - #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) - channel->del(channel); - #else rmt_del_channel(channel); - #endif // Cancel RMT output to GPIO pin. esp_rom_gpio_connect_out_signal(pin, SIG_GPIO_OUT_IDX, false, false); diff --git a/ports/esp32/machine_touchpad.c b/ports/esp32/machine_touchpad.c index 88b34d64ff070..61a3bcf83db89 100644 --- a/ports/esp32/machine_touchpad.c +++ b/ports/esp32/machine_touchpad.c @@ -31,14 +31,6 @@ #if SOC_TOUCH_SENSOR_SUPPORTED -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) -#if SOC_TOUCH_VERSION_1 -#define SOC_TOUCH_SENSOR_VERSION (1) -#elif SOC_TOUCH_VERSION_2 -#define SOC_TOUCH_SENSOR_VERSION (2) -#endif -#endif - #if SOC_TOUCH_SENSOR_VERSION == 1 // ESP32 only #include "driver/touch_pad.h" #elif SOC_TOUCH_SENSOR_VERSION == 2 // most ESP32 diff --git a/ports/esp32/main/idf_component.yml b/ports/esp32/main/idf_component.yml index 176e29c3c4862..47ae737b39cc0 100644 --- a/ports/esp32/main/idf_component.yml +++ b/ports/esp32/main/idf_component.yml @@ -19,6 +19,5 @@ dependencies: version: "~1.0.0" rules: - if: "target == esp32" - - if: "idf_version >=5.3" idf: - version: ">=5.2.0" + version: ">=5.3.0" diff --git a/ports/esp32/modnetwork.h b/ports/esp32/modnetwork.h index a68db41a3e353..68260dd19a3bb 100644 --- a/ports/esp32/modnetwork.h +++ b/ports/esp32/modnetwork.h @@ -29,8 +29,8 @@ #include "esp_wifi_types.h" #include "esp_netif.h" -// lan867x component requires newer IDF version -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) && CONFIG_IDF_TARGET_ESP32 +// lan867x component requires Original ESP32 +#if CONFIG_IDF_TARGET_ESP32 #define PHY_LAN867X_ENABLED (1) #else #define PHY_LAN867X_ENABLED (0) diff --git a/ports/esp32/network_wlan.c b/ports/esp32/network_wlan.c index 07b16e91cb941..5433bf862f109 100644 --- a/ports/esp32/network_wlan.c +++ b/ports/esp32/network_wlan.c @@ -769,9 +769,7 @@ static const mp_rom_map_elem_t wlan_if_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_SEC_WPA3_ENT_192), MP_ROM_INT(WIFI_AUTH_WPA3_ENT_192) }, { MP_ROM_QSTR(MP_QSTR_SEC_WPA3_EXT_PSK), MP_ROM_INT(WIFI_AUTH_WPA3_EXT_PSK) }, { MP_ROM_QSTR(MP_QSTR_SEC_WPA3_EXT_PSK_MIXED_MODE), MP_ROM_INT(WIFI_AUTH_WPA3_EXT_PSK_MIXED_MODE) }, - #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) { MP_ROM_QSTR(MP_QSTR_SEC_DPP), MP_ROM_INT(WIFI_AUTH_DPP) }, - #endif #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) { MP_ROM_QSTR(MP_QSTR_SEC_WPA3_ENT), MP_ROM_INT(WIFI_AUTH_WPA3_ENTERPRISE) }, { MP_ROM_QSTR(MP_QSTR_SEC_WPA2_WPA3_ENT), MP_ROM_INT(WIFI_AUTH_WPA2_WPA3_ENTERPRISE) }, @@ -797,8 +795,6 @@ _Static_assert(WIFI_AUTH_MAX == 17, "Synchronize WIFI_AUTH_XXX constants with th _Static_assert(WIFI_AUTH_MAX == 16, "Synchronize WIFI_AUTH_XXX constants with the ESP-IDF. Look at esp-idf/components/esp_wifi/include/esp_wifi_types_generic.h"); #elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) _Static_assert(WIFI_AUTH_MAX == 14, "Synchronize WIFI_AUTH_XXX constants with the ESP-IDF. Look at esp-idf/components/esp_wifi/include/esp_wifi_types_generic.h"); -#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0) -_Static_assert(WIFI_AUTH_MAX == 13, "Synchronize WIFI_AUTH_XXX constants with the ESP-IDF. Look at esp-idf/components/esp_wifi/include/esp_wifi_types.h"); #else #error "Error in macro logic, all supported versions should be covered." #endif diff --git a/ports/esp32/usb_serial_jtag.c b/ports/esp32/usb_serial_jtag.c index 2df7e20086283..86c89385fae81 100644 --- a/ports/esp32/usb_serial_jtag.c +++ b/ports/esp32/usb_serial_jtag.c @@ -36,9 +36,7 @@ #include "freertos/portmacro.h" // Number of bytes in the input buffer, and number of bytes for output chunking. -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) #define USB_SERIAL_JTAG_PACKET_SZ_BYTES (64) -#endif static DRAM_ATTR portMUX_TYPE rx_mux = portMUX_INITIALIZER_UNLOCKED; static uint8_t rx_buf[USB_SERIAL_JTAG_PACKET_SZ_BYTES]; From f625d2ed6c6ae6be44cc2763fe632a43ce216bcf Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 12 Mar 2026 14:15:37 +1100 Subject: [PATCH 06/53] esp32: Fix build for ESP-IDF version 5.3. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- ports/esp32/esp32_common.cmake | 6 +++++- ports/esp32/machine_timer.c | 8 ++++++++ ports/esp32/mpconfigport.h | 4 ++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/ports/esp32/esp32_common.cmake b/ports/esp32/esp32_common.cmake index 5ad7f7e003e81..86ea1aaf4ab27 100644 --- a/ports/esp32/esp32_common.cmake +++ b/ports/esp32/esp32_common.cmake @@ -169,7 +169,6 @@ list(APPEND IDF_COMPONENTS esp_app_format esp_mm esp_common - esp_driver_touch_sens esp_eth esp_event esp_hw_support @@ -198,6 +197,11 @@ list(APPEND IDF_COMPONENTS vfs ) +if($ENV{IDF_VERSION} VERSION_GREATER_EQUAL "5.4") + list(APPEND IDF_COMPONENTS + esp_driver_touch_sens) +endif() + # Provide the default LD fragment if not set if (MICROPY_USER_LDFRAGMENTS) set(MICROPY_LDFRAGMENTS ${MICROPY_USER_LDFRAGMENTS}) diff --git a/ports/esp32/machine_timer.c b/ports/esp32/machine_timer.c index b9cd80f48efed..d953f324b9cd7 100644 --- a/ports/esp32/machine_timer.c +++ b/ports/esp32/machine_timer.c @@ -183,7 +183,15 @@ void machine_timer_enable(machine_timer_obj_t *self) { } timer_ll_enable_counter(self->hal_context.dev, self->index, false); + + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) esp_clk_tree_enable_src(TIMER_CLK_SRC, true); + #elif TIMER_CLK_SRC != SOC_MOD_CLK_APB + // esp_clk_tree_enable_src() is only required on some newer chips where timer + // source clock may not be enabled by default + #error "This chip requires ESP-IDF v5.4 or newer for working Timer." + #endif + #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 5, 0) timer_ll_set_clock_source(self->hal_context.dev, self->index, TIMER_CLK_SRC); timer_ll_enable_clock(self->hal_context.dev, self->index, true); diff --git a/ports/esp32/mpconfigport.h b/ports/esp32/mpconfigport.h index dd8f89c4bc410..a1b4594da5654 100644 --- a/ports/esp32/mpconfigport.h +++ b/ports/esp32/mpconfigport.h @@ -140,8 +140,8 @@ #define MICROPY_PY_MACHINE_I2C (1) #define MICROPY_PY_MACHINE_I2C_TRANSFER_WRITE1 (1) #ifndef MICROPY_PY_MACHINE_I2C_TARGET -// I2C target hardware is limited on ESP32 (eg read event comes after the read) so we only support newer SoCs. -#define MICROPY_PY_MACHINE_I2C_TARGET (SOC_I2C_SUPPORT_SLAVE && !CONFIG_IDF_TARGET_ESP32) +// I2C target hardware is limited on ESP32 (eg read event comes after the read) so we only support newer SoCs & ESP-IDF v5.4 or newer +#define MICROPY_PY_MACHINE_I2C_TARGET (SOC_I2C_SUPPORT_SLAVE && !CONFIG_IDF_TARGET_ESP32 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0)) #define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/esp32/machine_i2c_target.c" #define MICROPY_PY_MACHINE_I2C_TARGET_MAX (2) #endif From bac45e5aafed2eca81b8cbfb85bbda68a64ff513 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 18 Mar 2026 10:50:22 +1100 Subject: [PATCH 07/53] tests/ports/stm32/can: Update pyb.CAN tests for FDCAN. Also rename the prefix from can to pyb_can, in anticipation of machine.CAN tests. Signed-off-by: Angus Gratton --- tests/ports/stm32/can2.py | 25 --- tests/ports/stm32/{can.py => pyb_can.py} | 209 +++++++++--------- .../stm32/{can.py.exp => pyb_can.py.exp} | 55 ++--- tests/ports/stm32/pyb_can2.py | 50 +++++ tests/ports/stm32/pyb_can2.py.exp | 5 + .../ports/stm32/pyb_can_classic_rtr_filter.py | 30 +++ ....exp => pyb_can_classic_rtr_filter.py.exp} | 0 tests/ports/stm32/pyb_can_classic_rx.py | 89 ++++++++ tests/ports/stm32/pyb_can_classic_rx.py.exp | 25 +++ 9 files changed, 315 insertions(+), 173 deletions(-) delete mode 100644 tests/ports/stm32/can2.py rename tests/ports/stm32/{can.py => pyb_can.py} (52%) rename tests/ports/stm32/{can.py.exp => pyb_can.py.exp} (62%) create mode 100644 tests/ports/stm32/pyb_can2.py create mode 100644 tests/ports/stm32/pyb_can2.py.exp create mode 100644 tests/ports/stm32/pyb_can_classic_rtr_filter.py rename tests/ports/stm32/{can2.py.exp => pyb_can_classic_rtr_filter.py.exp} (100%) create mode 100644 tests/ports/stm32/pyb_can_classic_rx.py create mode 100644 tests/ports/stm32/pyb_can_classic_rx.py.exp diff --git a/tests/ports/stm32/can2.py b/tests/ports/stm32/can2.py deleted file mode 100644 index 2ce438f1af971..0000000000000 --- a/tests/ports/stm32/can2.py +++ /dev/null @@ -1,25 +0,0 @@ -try: - from pyb import CAN - - CAN(2) -except (ImportError, ValueError): - print("SKIP") - raise SystemExit - -# Testing rtr messages -bus2 = CAN(2, CAN.LOOPBACK) -while bus2.any(0): - bus2.recv(0) -bus2.setfilter(0, CAN.LIST32, 0, (1, 2), rtr=(True, True), extframe=True) -bus2.setfilter(1, CAN.LIST32, 0, (3, 4), rtr=(True, False), extframe=True) -bus2.setfilter(2, CAN.MASK32, 0, (16, 16), rtr=(False,), extframe=True) -bus2.setfilter(2, CAN.MASK32, 0, (32, 32), rtr=(True,), extframe=True) - -bus2.send("", 1, rtr=True, extframe=True) -print(bus2.recv(0)) -bus2.send("", 2, rtr=True, extframe=True) -print(bus2.recv(0)) -bus2.send("", 3, rtr=True, extframe=True) -print(bus2.recv(0)) -bus2.send("", 4, rtr=True, extframe=True) -print(bus2.any(0)) diff --git a/tests/ports/stm32/can.py b/tests/ports/stm32/pyb_can.py similarity index 52% rename from tests/ports/stm32/can.py rename to tests/ports/stm32/pyb_can.py index 020efae0531ac..e8a8637566888 100644 --- a/tests/ports/stm32/can.py +++ b/tests/ports/stm32/pyb_can.py @@ -7,6 +7,15 @@ from array import array import micropython import pyb +import sys + +# Classic CAN (aka bxCAN) hardware has a different filter API +# and some different behaviours to newer FDCAN hardware +IS_CLASSIC = hasattr(CAN, "MASK16") + +# STM32H7 series has a gold-plated FDCAN peripheral with much deeper TX Queue +# than all other parts +IS_H7 = (not IS_CLASSIC) and "STM32H7" in str(sys.implementation) # test we can correctly create by id (2 handled in can2.py test) for bus in (-1, 0, 1, 3): @@ -25,22 +34,26 @@ can.init(CAN.LOOPBACK, num_filter_banks=14) print(can) -print(can.any(0)) +print("any", can.any(0)) # Test state when freshly created -print(can.state() == can.ERROR_ACTIVE) +print("error_active", can.state() == can.ERROR_ACTIVE) # Test that restart can be called can.restart() # Test info returns a sensible value -print(can.info()) +print("info", can.info()) -# Catch all filter -can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0)) +# Catch all filter (standard IDs) +if IS_CLASSIC: + can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0)) +else: + can.setfilter(0, CAN.MASK, 0, (0, 0), extframe=False) can.send("abcd", 123, timeout=5000) -print(can.any(0), can.info()) +pyb.delay(10) # For FDCAN, needs some time to send +print("any+info", can.any(0), can.info()) print(can.recv(0)) can.send("abcd", -1, timeout=5000) @@ -51,11 +64,16 @@ # Test too long message try: - can.send("abcdefghi", 0x7FF, timeout=5000) + if IS_CLASSIC: + payload = "abcdefghi" # 9 bytes long + else: + # the pyb.CAN API for FDCAN always accepts messages up to 64 bytes and sends as FDCAN + payload = b"x" * 65 + can.send(payload, 0x7FF, timeout=5000) except ValueError: - print("passed") + print("overlong passed") else: - print("failed") + print("overlong failed") # Test that recv can work without allocating memory on the heap @@ -135,7 +153,13 @@ can = CAN(1, CAN.LOOPBACK) # Catch all filter, but only for extframe's -can.setfilter(0, CAN.MASK32, 0, (0, 0), extframe=True) +if IS_CLASSIC: + can.setfilter(0, CAN.MASK32, 0, (0, 0), extframe=True) +else: + # FDCAN manages standard and extframe IDs independently, so need to + # clear the standard ID filter and then set the extended ID filter + can.clearfilter(0, extframe=False) + can.setfilter(0, CAN.MASK, 0, (0, 0), extframe=True) print(can) @@ -144,11 +168,12 @@ except ValueError: print("failed") else: + pyb.delay(10) r = can.recv(0) if r[0] == 0x7FF + 1 and r[4] == b"abcde": - print("passed") + print("extframe passed") else: - print("failed, wrong data received") + print("failed, wrong data received", r) # Test filters for n in [0, 8, 16, 24]: @@ -158,109 +183,40 @@ id_fail = 0b00011010 << n can.clearfilter(0, extframe=True) - can.setfilter(0, pyb.CAN.MASK32, 0, (filter_id, filter_mask), extframe=True) + if IS_CLASSIC: + can.setfilter(0, CAN.MASK32, 0, (filter_id, filter_mask), extframe=True) + else: + can.setfilter(0, CAN.MASK, 0, (filter_id, filter_mask), extframe=True) can.send("ok", id_ok, timeout=3, extframe=True) + pyb.delay(10) if can.any(0): msg = can.recv(0) print((hex(filter_id), hex(filter_mask), hex(msg[0]), msg[1], msg[4])) can.send("fail", id_fail, timeout=3, extframe=True) + pyb.delay(10) if can.any(0): msg = can.recv(0) print((hex(filter_id), hex(filter_mask), hex(msg[0]), msg[1], msg[4])) del can -# Test RxCallbacks -print("==== TEST rx callbacks ====") - -can = CAN(1, CAN.LOOPBACK) -can.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4)) -can.setfilter(1, CAN.LIST16, 1, (5, 6, 7, 8)) - - -def cb0(bus, reason): - print("cb0") - if reason == 0: - print("pending") - if reason == 1: - print("full") - if reason == 2: - print("overflow") - - -def cb1(bus, reason): - print("cb1") - if reason == 0: - print("pending") - if reason == 1: - print("full") - if reason == 2: - print("overflow") - - -def cb0a(bus, reason): - print("cb0a") - if reason == 0: - print("pending") - if reason == 1: - print("full") - if reason == 2: - print("overflow") - - -def cb1a(bus, reason): - print("cb1a") - if reason == 0: - print("pending") - if reason == 1: - print("full") - if reason == 2: - print("overflow") - - -can.rxcallback(0, cb0) -can.rxcallback(1, cb1) - -can.send("11111111", 1, timeout=5000) -can.send("22222222", 2, timeout=5000) -can.send("33333333", 3, timeout=5000) -can.rxcallback(0, cb0a) -can.send("44444444", 4, timeout=5000) - -can.send("55555555", 5, timeout=5000) -can.send("66666666", 6, timeout=5000) -can.send("77777777", 7, timeout=5000) -can.rxcallback(1, cb1a) -can.send("88888888", 8, timeout=5000) - -print(can.recv(0)) -print(can.recv(0)) -print(can.recv(0)) -print(can.recv(1)) -print(can.recv(1)) -print(can.recv(1)) - -can.send("11111111", 1, timeout=5000) -can.send("55555555", 5, timeout=5000) - -print(can.recv(0)) -print(can.recv(1)) - -del can - # Testing asynchronous send print("==== TEST async send ====") can = CAN(1, CAN.LOOPBACK) -can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0)) +# Catch all filter (standard IDs) +if IS_CLASSIC: + can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0)) +else: + can.setfilter(0, CAN.MASK, 0, (0, 0), extframe=False) while can.any(0): can.recv(0) can.send("abcde", 1, timeout=0) -print(can.any(0)) +print("any", can.any(0)) while not can.any(0): pass @@ -270,45 +226,82 @@ def cb1a(bus, reason): can.send("abcde", 2, timeout=0) can.send("abcde", 3, timeout=0) can.send("abcde", 4, timeout=0) - can.send("abcde", 5, timeout=0) + if not IS_H7: + can.send("abcde", 5, timeout=0) + else: + # Hack around the STM32H7's deeper transmit queue by pretending this call failed + # (STM32G4 will fail here, using otherwise the same code, so there is still some test coverage.) + print("send fail ok") except OSError as e: - if str(e) == "16": - print("passed") + # When send() fails Classic CAN raises OSError(MP_EBUSY) (16), FDCAN raises OSError(MP_ETIMEDOUT) (110) + if e.errno == (16 if IS_CLASSIC else 110): + print("send fail ok") else: - print("failed") + print("send fail not ok", e) pyb.delay(500) while can.any(0): print(can.recv(0)) +del can + # Testing rtr messages print("==== TEST rtr messages ====") bus1 = CAN(1, CAN.LOOPBACK) while bus1.any(0): bus1.recv(0) -bus1.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4)) -bus1.setfilter(1, CAN.LIST16, 0, (5, 6, 7, 8), rtr=(True, True, True, True)) -bus1.setfilter(2, CAN.MASK16, 0, (64, 64, 32, 32), rtr=(False, True)) + +if IS_CLASSIC: + # pyb.CAN Classic API allows distinguishing between RTR in the filter + bus1.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4)) + bus1.setfilter(1, CAN.LIST16, 0, (5, 6, 7, 8), rtr=(True, True, True, True)) + bus1.setfilter(2, CAN.MASK16, 0, (64, 64, 32, 32), rtr=(False, True)) +else: + # pyb.CAN FDCAN API does not allow distinguishing RTR in filter args, so + # instead we'll only filter the message IDs where Classic CAN equivalent is + # setting the RTR flag (meaning we're verifying RTR is received correctly, but + # not verifying the missing filter behaviour.) + bus1.setfilter(0, CAN.RANGE, 0, (5, 8)) + bus1.setfilter(1, CAN.MASK, 0, (32, 32)) + + +def print_rtr(msg): + if msg: + # Skip printing msg[3] as this is the filter match index, and the value + # is different between Classic and FDCAN implementations + print(msg[0], msg[1], msg[2], msg[4]) + else: + print(msg) + bus1.send("", 1, rtr=True) -print(bus1.any(0)) +print("any", bus1.any(0)) bus1.send("", 5, rtr=True) -print(bus1.recv(0)) +print_rtr(bus1.recv(0)) bus1.send("", 6, rtr=True) -print(bus1.recv(0)) +print_rtr(bus1.recv(0)) bus1.send("", 7, rtr=True) -print(bus1.recv(0)) +print_rtr(bus1.recv(0)) bus1.send("", 16, rtr=True) -print(bus1.any(0)) +print("any", bus1.any(0)) bus1.send("", 32, rtr=True) -print(bus1.recv(0)) +print_rtr(bus1.recv(0)) + +del bus1 # test HAL error, timeout print("==== TEST errors ====") +# Note: this test requires no other CAN node is attached to the CAN peripheral can = pyb.CAN(1, pyb.CAN.NORMAL) try: - can.send("1", 1, timeout=50) + if IS_CLASSIC: + can.send("1", 1, timeout=50) + else: + # Difference between pyb.CAN on Classic vs FDCAN - Classic waits until the message is sent to the bus, + # FDCAN only times out if the TX queue is full + while True: + can.send("1", 1, timeout=50) except OSError as e: - print(repr(e)) + print("timeout", repr(e)) diff --git a/tests/ports/stm32/can.py.exp b/tests/ports/stm32/pyb_can.py.exp similarity index 62% rename from tests/ports/stm32/can.py.exp rename to tests/ports/stm32/pyb_can.py.exp index bd8f6d60b609e..800b3514c0008 100644 --- a/tests/ports/stm32/can.py.exp +++ b/tests/ports/stm32/pyb_can.py.exp @@ -5,14 +5,14 @@ ValueError 3 CAN(1) True CAN(1, CAN.LOOPBACK, auto_restart=False) -False -True -[0, 0, 0, 0, 0, 0, 0, 0] -True [0, 0, 0, 0, 0, 0, 1, 0] +any False +error_active True +info [0, 0, 0, 0, 0, 0, 0, 0] +any+info True [0, 0, 0, 0, 0, 0, 1, 0] (123, False, False, 0, b'abcd') (2047, False, False, 0, b'abcd') (0, False, False, 0, b'abcd') -passed +overlong passed [42, False, False, 0, ] 0 bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') [42, False, False, 0, ] 4 bytearray(b'1234\x00\x00\x00\x00\x00\x00') [42, False, False, 0, ] 8 bytearray(b'01234567\x00\x00') @@ -26,49 +26,24 @@ ValueError ValueError ==== TEST extframe=True ==== CAN(1, CAN.LOOPBACK, auto_restart=False) -passed +extframe passed ('0x8', '0x1c', '0xa', True, b'ok') ('0x800', '0x1c00', '0xa00', True, b'ok') ('0x80000', '0x1c0000', '0xa0000', True, b'ok') ('0x8000000', '0x1c000000', '0xa000000', True, b'ok') -==== TEST rx callbacks ==== -cb0 -pending -cb0 -full -cb0a -overflow -cb1 -pending -cb1 -full -cb1a -overflow -(1, False, False, 0, b'11111111') -(2, False, False, 1, b'22222222') -(4, False, False, 3, b'44444444') -(5, False, False, 0, b'55555555') -(6, False, False, 1, b'66666666') -(8, False, False, 3, b'88888888') -cb0a -pending -cb1a -pending -(1, False, False, 0, b'11111111') -(5, False, False, 0, b'55555555') ==== TEST async send ==== -False +any False (1, False, False, 0, b'abcde') -passed +send fail ok (2, False, False, 0, b'abcde') (3, False, False, 0, b'abcde') (4, False, False, 0, b'abcde') ==== TEST rtr messages ==== -False -(5, False, True, 4, b'') -(6, False, True, 5, b'') -(7, False, True, 6, b'') -False -(32, False, True, 9, b'') +any False +5 False True b'' +6 False True b'' +7 False True b'' +any False +32 False True b'' ==== TEST errors ==== -OSError(110,) +timeout OSError(110,) diff --git a/tests/ports/stm32/pyb_can2.py b/tests/ports/stm32/pyb_can2.py new file mode 100644 index 0000000000000..62ae935357c7e --- /dev/null +++ b/tests/ports/stm32/pyb_can2.py @@ -0,0 +1,50 @@ +try: + from pyb import CAN + + CAN(2) +except (ImportError, ValueError): + print("SKIP") + raise SystemExit + +# Classic CAN (aka bxCAN) hardware has a different filter API +# and some different behaviours to newer FDCAN hardware +IS_CLASSIC = hasattr(CAN, "MASK16") + +# Setting up each CAN peripheral independently is deliberate here, to catch +# catch cases where initialising CAN2 breaks CAN1 + +can1 = CAN(1, CAN.LOOPBACK) +if IS_CLASSIC: + can1.setfilter(0, CAN.LIST16, 0, (123, 124, 125, 126)) +else: + can1.setfilter(0, CAN.RANGE, 0, (123, 126)) + +can2 = CAN(2, CAN.LOOPBACK) +if IS_CLASSIC: + can2.setfilter(0, CAN.LIST16, 0, (3, 4, 5, 6)) +else: + can2.setfilter(0, CAN.RANGE, 0, (3, 6)) + +# Drain any old messages in RX FIFOs +for can in (can1, can2): + while can.any(0): + can.recv(0) + +for id, can in ((1, can1), (2, can2)): + print("testing", id) + # message1 should only receive on can1, message2 on can2 + can.send("message1", 123) + can.send("message2", 3) + did_recv = False + try: + while True: + res = can.recv(0, timeout=50) + # not printing all of 'res' as the filter index result is different + # on Classic vs FD-CAN + print("rx", res[0], res[4]) + did_recv = True + except OSError: + if not did_recv: + print("no rx!") + +print("done") diff --git a/tests/ports/stm32/pyb_can2.py.exp b/tests/ports/stm32/pyb_can2.py.exp new file mode 100644 index 0000000000000..9696f2fa0108f --- /dev/null +++ b/tests/ports/stm32/pyb_can2.py.exp @@ -0,0 +1,5 @@ +testing 1 +rx 123 b'message1' +testing 2 +rx 3 b'message2' +done diff --git a/tests/ports/stm32/pyb_can_classic_rtr_filter.py b/tests/ports/stm32/pyb_can_classic_rtr_filter.py new file mode 100644 index 0000000000000..90ae28ca9be38 --- /dev/null +++ b/tests/ports/stm32/pyb_can_classic_rtr_filter.py @@ -0,0 +1,30 @@ +try: + from pyb import CAN +except ImportError: + print("SKIP") + raise SystemExit + +if not hasattr(CAN, "LIST16"): + # This test relies on the RTR-aware filters of Classic CAN, + # so needs to be skipped on FDCAN hardware + print("SKIP") + raise SystemExit + +can = CAN(1, CAN.LOOPBACK) +while can.any(0): + can.recv(0) +can.setfilter(0, CAN.LIST32, 0, (1, 2), rtr=(True, True), extframe=True) +can.setfilter(1, CAN.LIST32, 0, (3, 4), rtr=(True, False), extframe=True) +can.setfilter(2, CAN.MASK32, 0, (16, 16), rtr=(False,), extframe=True) +can.setfilter(2, CAN.MASK32, 0, (32, 32), rtr=(True,), extframe=True) + +can.send("", 1, rtr=True, extframe=True) +print(can.recv(0)) +can.send("", 2, rtr=True, extframe=True) +print(can.recv(0)) +can.send("", 3, rtr=True, extframe=True) +print(can.recv(0)) +can.send("", 4, rtr=True, extframe=True) +print(can.any(0)) + +del can diff --git a/tests/ports/stm32/can2.py.exp b/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp similarity index 100% rename from tests/ports/stm32/can2.py.exp rename to tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp diff --git a/tests/ports/stm32/pyb_can_classic_rx.py b/tests/ports/stm32/pyb_can_classic_rx.py new file mode 100644 index 0000000000000..39a5eadca54f0 --- /dev/null +++ b/tests/ports/stm32/pyb_can_classic_rx.py @@ -0,0 +1,89 @@ +try: + from pyb import CAN +except ImportError: + print("SKIP") + raise SystemExit + +if not hasattr(CAN, "LIST16"): + # This test relies on some specific behaviours of the Classic CAN RX FIFO + # interrupt, so needs to be skipped on FDCAN hardware + print("SKIP") + raise SystemExit + +# Test RxCallbacks +print("==== TEST rx callbacks ====") + +can = CAN(1, CAN.LOOPBACK) +can.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4)) +can.setfilter(1, CAN.LIST16, 1, (5, 6, 7, 8)) + + +def cb0(bus, reason): + print("cb0") + if reason == 0: + print("pending") + if reason == 1: + print("full") + if reason == 2: + print("overflow") + + +def cb1(bus, reason): + print("cb1") + if reason == 0: + print("pending") + if reason == 1: + print("full") + if reason == 2: + print("overflow") + + +def cb0a(bus, reason): + print("cb0a") + if reason == 0: + print("pending") + if reason == 1: + print("full") + if reason == 2: + print("overflow") + + +def cb1a(bus, reason): + print("cb1a") + if reason == 0: + print("pending") + if reason == 1: + print("full") + if reason == 2: + print("overflow") + + +can.rxcallback(0, cb0) +can.rxcallback(1, cb1) + +can.send("11111111", 1, timeout=5000) +can.send("22222222", 2, timeout=5000) +can.send("33333333", 3, timeout=5000) +can.rxcallback(0, cb0a) +can.send("44444444", 4, timeout=5000) + +can.send("55555555", 5, timeout=5000) +can.send("66666666", 6, timeout=5000) +can.send("77777777", 7, timeout=5000) +can.rxcallback(1, cb1a) +can.send("88888888", 8, timeout=5000) + +print(can.recv(0)) +print(can.recv(0)) +print(can.recv(0)) +print(can.recv(1)) +print(can.recv(1)) +print(can.recv(1)) + +can.send("11111111", 1, timeout=5000) +can.send("55555555", 5, timeout=5000) + +print(can.recv(0)) +print(can.recv(1)) + +del can diff --git a/tests/ports/stm32/pyb_can_classic_rx.py.exp b/tests/ports/stm32/pyb_can_classic_rx.py.exp new file mode 100644 index 0000000000000..6b848eaeb0b65 --- /dev/null +++ b/tests/ports/stm32/pyb_can_classic_rx.py.exp @@ -0,0 +1,25 @@ +==== TEST rx callbacks ==== +cb0 +pending +cb0 +full +cb0a +overflow +cb1 +pending +cb1 +full +cb1a +overflow +(1, False, False, 0, b'11111111') +(2, False, False, 1, b'22222222') +(4, False, False, 3, b'44444444') +(5, False, False, 0, b'55555555') +(6, False, False, 1, b'66666666') +(8, False, False, 3, b'88888888') +cb0a +pending +cb1a +pending +(1, False, False, 0, b'11111111') +(5, False, False, 0, b'55555555') From 4b339ee7dcf9c6130951671f24033e2769045804 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 18 Mar 2026 15:23:12 +1100 Subject: [PATCH 08/53] stm32/pyb_can: Fix initialising CAN2 clearing CAN1 filters. Closes #18922 Signed-off-by: Angus Gratton --- ports/stm32/pyb_can.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ports/stm32/pyb_can.c b/ports/stm32/pyb_can.c index 0d004ecfcb5e4..2de33f511c8ac 100644 --- a/ports/stm32/pyb_can.c +++ b/ports/stm32/pyb_can.c @@ -273,8 +273,9 @@ static mp_obj_t pyb_can_init_helper(pyb_can_obj_t *self, size_t n_args, const mp #else // Init filter banks for classic CAN. can2_start_bank = args[ARG_num_filter_banks].u_int; + int bank_offs = (self->can_id == 2) ? can2_start_bank : 0; for (int f = 0; f < CAN_MAX_FILTER; f++) { - can_clearfilter(&self->can, f, can2_start_bank); + can_clearfilter(&self->can, f + bank_offs, can2_start_bank); } #endif From a906cfbb4b3c183b4e7605e2487eb2efac55be4d Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 18 Mar 2026 15:24:04 +1100 Subject: [PATCH 09/53] stm32/can: Clarify can_clearfilter() arguments. The function arguments mean totally different things for Classic vs FDCAN hardware, but the argument name wasn't particularly clear for either. This commit shouldn't really change the binary firmware at all. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- ports/stm32/can.c | 4 ++-- ports/stm32/can.h | 5 ++++- ports/stm32/fdcan.c | 15 ++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ports/stm32/can.c b/ports/stm32/can.c index d43d73ad42d94..04514a14f0e11 100644 --- a/ports/stm32/can.c +++ b/ports/stm32/can.c @@ -152,7 +152,7 @@ void can_enable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo, bool e ((enable_msg_received ? CAN_IT_FMP1 : 0) | CAN_IT_FF1 | CAN_IT_FOV1))); } -void can_clearfilter(CAN_HandleTypeDef *can, uint32_t filter_num, uint8_t bank) { +void can_clearfilter(CAN_HandleTypeDef *can, uint32_t filter_num, uint8_t can2_start_bank) { CAN_FilterConfTypeDef filter; filter.FilterIdHigh = 0; @@ -164,7 +164,7 @@ void can_clearfilter(CAN_HandleTypeDef *can, uint32_t filter_num, uint8_t bank) filter.FilterMode = CAN_FILTERMODE_IDMASK; filter.FilterScale = CAN_FILTERSCALE_16BIT; filter.FilterActivation = DISABLE; - filter.BankNumber = bank; + filter.BankNumber = can2_start_bank; HAL_CAN_ConfigFilter(can, &filter); } diff --git a/ports/stm32/can.h b/ports/stm32/can.h index 3422f4180d6df..82cd89bc2f307 100644 --- a/ports/stm32/can.h +++ b/ports/stm32/can.h @@ -90,7 +90,6 @@ typedef enum { bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t prescaler, uint32_t sjw, uint32_t bs1, uint32_t bs2, bool auto_restart); void can_deinit(CAN_HandleTypeDef *can); -void can_clearfilter(CAN_HandleTypeDef *can, uint32_t filter_num, uint8_t bank); int can_receive(CAN_HandleTypeDef *can, can_rx_fifo_t fifo, CanRxMsgTypeDef *msg, uint8_t *data, uint32_t timeout_ms); HAL_StatusTypeDef can_transmit(CAN_HandleTypeDef *hcan, CanTxMsgTypeDef *txmsg, uint8_t *data, uint32_t Timeout); @@ -111,12 +110,16 @@ static inline unsigned can_rx_pending(CAN_HandleTypeDef *can, can_rx_fifo_t fifo return HAL_FDCAN_GetRxFifoFillLevel(can, fifo == CAN_RX_FIFO0 ? FDCAN_RX_FIFO0 : FDCAN_RX_FIFO1); } +void can_clearfilter(CAN_HandleTypeDef *can, uint32_t filter_num, bool is_extid); + #else static inline unsigned can_rx_pending(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { return __HAL_CAN_MSG_PENDING(can, fifo == CAN_RX_FIFO0 ? CAN_FIFO0 : CAN_FIFO1); } +void can_clearfilter(CAN_HandleTypeDef *can, uint32_t filter_num, uint8_t can2_start_bank); + #endif // MICROPY_HW_ENABLE_FDCAN #endif // MICROPY_HW_ENABLE_CAN diff --git a/ports/stm32/fdcan.c b/ports/stm32/fdcan.c index 3034dd38ef272..ad1f04a467083 100644 --- a/ports/stm32/fdcan.c +++ b/ports/stm32/fdcan.c @@ -248,15 +248,12 @@ void can_deinit(FDCAN_HandleTypeDef *can) { } } -void can_clearfilter(FDCAN_HandleTypeDef *can, uint32_t f, uint8_t extid) { - FDCAN_FilterTypeDef filter = {0}; - if (extid == 1) { - filter.IdType = FDCAN_EXTENDED_ID; - } else { - filter.IdType = FDCAN_STANDARD_ID; - } - filter.FilterIndex = f; - filter.FilterConfig = FDCAN_FILTER_DISABLE; +void can_clearfilter(FDCAN_HandleTypeDef *can, uint32_t f, bool is_extid) { + FDCAN_FilterTypeDef filter = { + .IdType = is_extid ? FDCAN_EXTENDED_ID : FDCAN_STANDARD_ID, + .FilterIndex = f, + .FilterConfig = FDCAN_FILTER_DISABLE, + }; HAL_FDCAN_ConfigFilter(can, &filter); } From d1c936db73be96fca5170608280e3f2f4e33f48a Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 18 Mar 2026 15:25:01 +1100 Subject: [PATCH 10/53] stm32: Expose FDCAN2 on board NUCLEO_G474RE. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- ports/stm32/boards/NUCLEO_G474RE/mpconfigboard.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ports/stm32/boards/NUCLEO_G474RE/mpconfigboard.h b/ports/stm32/boards/NUCLEO_G474RE/mpconfigboard.h index 21619d6d0d1cb..6f4fd6df48e5a 100644 --- a/ports/stm32/boards/NUCLEO_G474RE/mpconfigboard.h +++ b/ports/stm32/boards/NUCLEO_G474RE/mpconfigboard.h @@ -86,3 +86,9 @@ #define MICROPY_HW_CAN1_NAME "FDCAN1" #define MICROPY_HW_CAN1_TX (pin_A12) // A12, B9, D1 #define MICROPY_HW_CAN1_RX (pin_A11) // A11, B8, D0 + +#define MICROPY_HW_CAN2_NAME "FDCAN2" +#define MICROPY_HW_CAN2_TX (pin_B13) // B13, B6 +#define MICROPY_HW_CAN2_RX (pin_B12) // B12, B5 + +// Note: This MCU has an FDCAN3 peripheral, but not currently supported in MicroPython From cda49bed278a5a85bccc6c60fe097c3039769d89 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Tue, 16 Dec 2025 17:15:20 +1100 Subject: [PATCH 11/53] py/objlist,stm32,esp32: Add helpers for creating/ensuring list args. Simplifies the pattern of an optional arg which can be a list of at least a certain length, otherwise one is lazily initialised. Modify pyb.CAN and ESP-NOW APIs to use the helper. Note this changes the return type of pyb.CAN.recv() from tuple to list. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- docs/library/pyb.CAN.rst | 6 +-- ports/esp32/modespnow.c | 5 +-- ports/esp8266/modespnow.c | 5 +-- ports/stm32/pyb_can.c | 35 ++++------------- ports/unix/coverage.c | 39 +++++++++++++++++++ py/objlist.c | 19 +++++++++ py/objlist.h | 7 ++++ tests/multi_pyb_can/rx_callback.py.exp | 4 +- tests/multi_pyb_can/rx_filters.py.exp | 12 +++--- tests/ports/stm32/pyb_can.py.exp | 14 +++---- .../stm32/pyb_can_classic_rtr_filter.py.exp | 6 +-- tests/ports/stm32/pyb_can_classic_rx.py.exp | 16 ++++---- tests/ports/unix/extra_coverage.py.exp | 7 ++++ 13 files changed, 110 insertions(+), 65 deletions(-) diff --git a/docs/library/pyb.CAN.rst b/docs/library/pyb.CAN.rst index eb21d8223f26e..139b54f5b1140 100644 --- a/docs/library/pyb.CAN.rst +++ b/docs/library/pyb.CAN.rst @@ -237,7 +237,7 @@ Methods - *list* is an optional list object to be used as the return value - *timeout* is the timeout in milliseconds to wait for the receive. - Return value: A tuple containing five values. + Return value: A list containing five values. - The id of the message. - A boolean that indicates if the message ID is standard or extended. @@ -245,8 +245,8 @@ Methods - The FMI (Filter Match Index) value. - An array containing the data. - If *list* is ``None`` then a new tuple will be allocated, as well as a new - bytes object to contain the data (as the fifth element in the tuple). + If *list* is ``None`` then a new list will be allocated, as well as a new + bytes object to contain the data (as the fifth element in the list). If *list* is not ``None`` then it should be a list object with a least five elements. The fifth element should be a memoryview object which is created diff --git a/ports/esp32/modespnow.c b/ports/esp32/modespnow.c index 725374ea77c55..c7a9c6eb1b913 100644 --- a/ports/esp32/modespnow.c +++ b/ports/esp32/modespnow.c @@ -423,10 +423,7 @@ static mp_obj_t espnow_recvinto(size_t n_args, const mp_obj_t *args) { mp_int_t timeout_ms = ((n_args > 2 && args[2] != mp_const_none) ? mp_obj_get_int(args[2]) : self->recv_timeout_ms); - mp_obj_list_t *list = MP_OBJ_TO_PTR(args[1]); - if (!mp_obj_is_type(list, &mp_type_list) || list->len < 2) { - mp_raise_ValueError(MP_ERROR_TEXT("ESPNow.recvinto(): Invalid argument")); - } + mp_obj_list_t *list = mp_obj_list_ensure(args[1], 2); mp_obj_array_t *msg = MP_OBJ_TO_PTR(list->items[1]); if (mp_obj_is_type(msg, &mp_type_bytearray)) { msg->len += msg->free; // Make all the space in msg array available diff --git a/ports/esp8266/modespnow.c b/ports/esp8266/modespnow.c index 91510a3f9df14..cbe9cdc4d4494 100644 --- a/ports/esp8266/modespnow.c +++ b/ports/esp8266/modespnow.c @@ -304,10 +304,7 @@ static mp_obj_t espnow_recvinto(size_t n_args, const mp_obj_t *args) { size_t timeout_ms = ((n_args > 2 && args[2] != mp_const_none) ? mp_obj_get_int(args[2]) : self->recv_timeout_ms); - mp_obj_list_t *list = MP_OBJ_TO_PTR(args[1]); - if (!mp_obj_is_type(list, &mp_type_list) || list->len < 2) { - mp_raise_ValueError(MP_ERROR_TEXT("ESPNow.recvinto(): Invalid argument")); - } + mp_obj_list_t *list = mp_obj_list_ensure(args[1], 2); mp_obj_array_t *msg = MP_OBJ_TO_PTR(list->items[1]); size_t msg_size = msg->len + msg->free; if (mp_obj_is_type(msg, &mp_type_bytearray)) { diff --git a/ports/stm32/pyb_can.c b/ports/stm32/pyb_can.c index 2de33f511c8ac..e546163f7fcde 100644 --- a/ports/stm32/pyb_can.c +++ b/ports/stm32/pyb_can.c @@ -441,18 +441,7 @@ static MP_DEFINE_CONST_FUN_OBJ_1(pyb_can_state_obj, pyb_can_state); // Get info about error states and TX/RX buffers static mp_obj_t pyb_can_info(size_t n_args, const mp_obj_t *args) { pyb_can_obj_t *self = MP_OBJ_TO_PTR(args[0]); - mp_obj_list_t *list; - if (n_args == 1) { - list = MP_OBJ_TO_PTR(mp_obj_new_list(8, NULL)); - } else { - if (!mp_obj_is_type(args[1], &mp_type_list)) { - mp_raise_TypeError(NULL); - } - list = MP_OBJ_TO_PTR(args[1]); - if (list->len < 8) { - mp_raise_ValueError(NULL); - } - } + mp_obj_list_t *list = mp_obj_list_optional_arg(n_args > 1 ? args[1] : NULL, 8); #if MICROPY_HW_ENABLE_FDCAN FDCAN_GlobalTypeDef *can = self->can.Instance; @@ -657,24 +646,14 @@ static mp_obj_t pyb_can_recv(size_t n_args, const mp_obj_t *pos_args, mp_map_t * can_enable_rx_interrupts(&self->can, fifo, fifo_empty); } - // Create the tuple, or get the list, that will hold the return values + // Create or get the list, that will hold the return values // Also populate the fifth element, either a new bytes or reuse existing memoryview - mp_obj_t ret_obj = args[ARG_list].u_obj; - mp_obj_t *items; - if (ret_obj == mp_const_none) { - ret_obj = mp_obj_new_tuple(5, NULL); - items = ((mp_obj_tuple_t *)MP_OBJ_TO_PTR(ret_obj))->items; + mp_obj_list_t *list = mp_obj_list_optional_arg(args[ARG_list].u_obj, 5); + mp_obj_t *items = list->items; + if (MP_OBJ_FROM_PTR(list) != args[ARG_list].u_obj) { + // If newly allocated, create item 4 items[4] = mp_obj_new_bytes(rx_data, rx_dlc); } else { - // User should provide a list of length at least 5 to hold the values - if (!mp_obj_is_type(ret_obj, &mp_type_list)) { - mp_raise_TypeError(NULL); - } - mp_obj_list_t *list = MP_OBJ_TO_PTR(ret_obj); - if (list->len < 5) { - mp_raise_ValueError(NULL); - } - items = list->items; // Fifth element must be a memoryview which we assume points to a // byte-like array which is large enough, and then we resize it inplace if (!mp_obj_is_type(items[4], &mp_type_memoryview)) { @@ -703,7 +682,7 @@ static mp_obj_t pyb_can_recv(size_t n_args, const mp_obj_t *pos_args, mp_map_t * #endif // Return the result - return ret_obj; + return MP_OBJ_FROM_PTR(list); } static MP_DEFINE_CONST_FUN_OBJ_KW(pyb_can_recv_obj, 1, pyb_can_recv); diff --git a/ports/unix/coverage.c b/ports/unix/coverage.c index f5f3657aca8ba..79ec07f45e1aa 100644 --- a/ports/unix/coverage.c +++ b/ports/unix/coverage.c @@ -548,6 +548,45 @@ static mp_obj_t extra_coverage(void) { mp_printf(&mp_plat_print, "%x%08x\n", (uint32_t)(value_ll >> 32), (uint32_t)value_ll); } + // list argument helpers + { + mp_printf(&mp_plat_print, "# list argument helpers\n"); + + // Create a list to test with + mp_obj_t list_items[] = { mp_const_none, MP_OBJ_NEW_SMALL_INT(77), mp_obj_new_str_from_cstr("hello") }; + size_t list_len = MP_ARRAY_SIZE(list_items); + mp_obj_t list = mp_obj_new_list(list_len, list_items); + + // mp_obj_list_ensure + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + mp_obj_list_ensure(MP_OBJ_NEW_SMALL_INT(-1), 5); // Not a list + nlr_pop(); + } else { + mp_obj_print_exception(&mp_plat_print, MP_OBJ_FROM_PTR(nlr.ret_val)); + } + + if (nlr_push(&nlr) == 0) { + mp_obj_list_ensure(list, list_len + 2); // List shorter than minimum length + nlr_pop(); + } else { + mp_obj_print_exception(&mp_plat_print, MP_OBJ_FROM_PTR(nlr.ret_val)); + } + + mp_obj_list_t *as_ptr = mp_obj_list_ensure(list, list_len); // Acceptable! + mp_printf(&mp_plat_print, "mp_obj_list_ensure same list? %d\n", MP_OBJ_TO_PTR(list) == as_ptr); + + // mp_obj_list_optional_arg() + as_ptr = mp_obj_list_optional_arg(list, list_len); + mp_printf(&mp_plat_print, "mp_obj_list_optional_arg same list? %d\n", MP_OBJ_TO_PTR(list) == as_ptr); + + as_ptr = mp_obj_list_optional_arg(mp_const_none, list_len); + mp_printf(&mp_plat_print, "mp_obj_list_optional_arg new list len %d\n", as_ptr->len); + + as_ptr = mp_obj_list_optional_arg(MP_OBJ_NULL, list_len); + mp_printf(&mp_plat_print, "mp_obj_list_optional_arg new list from NULL len %d\n", as_ptr->len); + } + // runtime utils { mp_printf(&mp_plat_print, "# runtime utils\n"); diff --git a/py/objlist.c b/py/objlist.c index d5aca03396034..41c920511d2d4 100644 --- a/py/objlist.c +++ b/py/objlist.c @@ -520,3 +520,22 @@ mp_obj_t mp_obj_new_list_iterator(mp_obj_t list, size_t cur, mp_obj_iter_buf_t * o->cur = cur; return MP_OBJ_FROM_PTR(o); } + +mp_obj_list_t *mp_obj_list_optional_arg(mp_obj_t arg_in, size_t min_len) { + if (arg_in == MP_OBJ_NULL || arg_in == mp_const_none) { + return MP_OBJ_TO_PTR(mp_obj_new_list(min_len, NULL)); + } else { + return mp_obj_list_ensure(arg_in, min_len); + } +} + +mp_obj_list_t *mp_obj_list_ensure(mp_obj_t in, size_t min_len) { + if (!mp_obj_is_type(in, &mp_type_list)) { + mp_raise_TypeError(NULL); + } + mp_obj_list_t *list = MP_OBJ_TO_PTR(in); + if (list->len < min_len) { + mp_raise_ValueError(NULL); + } + return list; +} diff --git a/py/objlist.h b/py/objlist.h index 1f4c70504c8d9..e72778266a3ba 100644 --- a/py/objlist.h +++ b/py/objlist.h @@ -60,4 +60,11 @@ static inline void mp_obj_list_store(mp_obj_t self_in, mp_obj_t index, mp_obj_t self->items[i] = value; } +// Helper function for pattern of an optional argument which can be a list of a specified size, and is +// allocated on-demand otherwise +mp_obj_list_t *mp_obj_list_optional_arg(mp_obj_t arg_in, size_t min_len); + +// Ensure provided object is a list of minimum length min_len. Raises TypeError & ValueError otherwise. +mp_obj_list_t *mp_obj_list_ensure(mp_obj_t in, size_t min_len); + #endif // MICROPY_INCLUDED_PY_OBJLIST_H diff --git a/tests/multi_pyb_can/rx_callback.py.exp b/tests/multi_pyb_can/rx_callback.py.exp index 3ab197cc10319..2126a3fccb02f 100644 --- a/tests/multi_pyb_can/rx_callback.py.exp +++ b/tests/multi_pyb_can/rx_callback.py.exp @@ -2,8 +2,8 @@ rx0 reason full rx0 reason overflow rxed_spam True -(256, False, False, 0, b'aaaaa') +[256, False, False, 0, b'aaaaa'] any False --- instance1 --- -(85, False, False, 0, b'overflow') +[85, False, False, 0, b'overflow'] any False diff --git a/tests/multi_pyb_can/rx_filters.py.exp b/tests/multi_pyb_can/rx_filters.py.exp index 65fbdf4b1b22b..8016f641524a7 100644 --- a/tests/multi_pyb_can/rx_filters.py.exp +++ b/tests/multi_pyb_can/rx_filters.py.exp @@ -1,13 +1,13 @@ --- instance0 --- 0 -fifo0 (837, False, False, 0, b'') -fifo1 (14080, True, False, 0, b'') +fifo0 [837, False, False, 0, b''] +fifo1 [14080, True, False, 0, b''] 1 -fifo0 (837, False, False, 0, b'\x01\x03') -fifo1 (14081, True, False, 0, b'\xee') +fifo0 [837, False, False, 0, b'\x01\x03'] +fifo1 [14081, True, False, 0, b'\xee'] 2 -fifo0 (837, False, False, 0, b'\x02\x03\x02\x03') -fifo1 (14082, True, False, 0, b'\xee\xee') +fifo0 [837, False, False, 0, b'\x02\x03\x02\x03'] +fifo1 [14082, True, False, 0, b'\xee\xee'] Timed out as expected --- instance1 --- 0 diff --git a/tests/ports/stm32/pyb_can.py.exp b/tests/ports/stm32/pyb_can.py.exp index 800b3514c0008..80f44981231ea 100644 --- a/tests/ports/stm32/pyb_can.py.exp +++ b/tests/ports/stm32/pyb_can.py.exp @@ -9,9 +9,9 @@ any False error_active True info [0, 0, 0, 0, 0, 0, 0, 0] any+info True [0, 0, 0, 0, 0, 0, 1, 0] -(123, False, False, 0, b'abcd') -(2047, False, False, 0, b'abcd') -(0, False, False, 0, b'abcd') +[123, False, False, 0, b'abcd'] +[2047, False, False, 0, b'abcd'] +[0, False, False, 0, b'abcd'] overlong passed [42, False, False, 0, ] 0 bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') [42, False, False, 0, ] 4 bytearray(b'1234\x00\x00\x00\x00\x00\x00') @@ -33,11 +33,11 @@ extframe passed ('0x8000000', '0x1c000000', '0xa000000', True, b'ok') ==== TEST async send ==== any False -(1, False, False, 0, b'abcde') +[1, False, False, 0, b'abcde'] send fail ok -(2, False, False, 0, b'abcde') -(3, False, False, 0, b'abcde') -(4, False, False, 0, b'abcde') +[2, False, False, 0, b'abcde'] +[3, False, False, 0, b'abcde'] +[4, False, False, 0, b'abcde'] ==== TEST rtr messages ==== any False 5 False True b'' diff --git a/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp b/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp index 3339e5cbecaaf..b970a0509feba 100644 --- a/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp +++ b/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp @@ -1,4 +1,4 @@ -(1, True, True, 0, b'') -(2, True, True, 1, b'') -(3, True, True, 2, b'') +[1, True, True, 0, b''] +[2, True, True, 1, b''] +[3, True, True, 2, b''] False diff --git a/tests/ports/stm32/pyb_can_classic_rx.py.exp b/tests/ports/stm32/pyb_can_classic_rx.py.exp index 6b848eaeb0b65..c1c743c42b5be 100644 --- a/tests/ports/stm32/pyb_can_classic_rx.py.exp +++ b/tests/ports/stm32/pyb_can_classic_rx.py.exp @@ -11,15 +11,15 @@ cb1 full cb1a overflow -(1, False, False, 0, b'11111111') -(2, False, False, 1, b'22222222') -(4, False, False, 3, b'44444444') -(5, False, False, 0, b'55555555') -(6, False, False, 1, b'66666666') -(8, False, False, 3, b'88888888') +[1, False, False, 0, b'11111111'] +[2, False, False, 1, b'22222222'] +[4, False, False, 3, b'44444444'] +[5, False, False, 0, b'55555555'] +[6, False, False, 1, b'66666666'] +[8, False, False, 3, b'88888888'] cb0a pending cb1a pending -(1, False, False, 0, b'11111111') -(5, False, False, 0, b'55555555') +[1, False, False, 0, b'11111111'] +[5, False, False, 0, b'55555555'] diff --git a/tests/ports/unix/extra_coverage.py.exp b/tests/ports/unix/extra_coverage.py.exp index 3a46994c23d8d..73393e68b6328 100644 --- a/tests/ports/unix/extra_coverage.py.exp +++ b/tests/ports/unix/extra_coverage.py.exp @@ -115,6 +115,13 @@ deadbeef 0deadbeef c0ffee 000c0ffee +# list argument helpers +TypeError: +ValueError: +mp_obj_list_ensure same list? 1 +mp_obj_list_optional_arg same list? 1 +mp_obj_list_optional_arg new list len 3 +mp_obj_list_optional_arg new list from NULL len 3 # runtime utils TypeError: unsupported type for __abs__: 'str' TypeError: unsupported types for __divmod__: 'str', 'str' From 6768325ea3f310311a94ce593d0459ec2ab01b4e Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 29 Jan 2026 15:27:58 +1100 Subject: [PATCH 12/53] stm32: Add can_get_state() function, use from pyb.CAN. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- ports/stm32/can.c | 19 +++++++++++++++++++ ports/stm32/can.h | 2 ++ ports/stm32/fdcan.c | 24 ++++++++++++++++++++++++ ports/stm32/pyb_can.c | 28 +++------------------------- 4 files changed, 48 insertions(+), 25 deletions(-) diff --git a/ports/stm32/can.c b/ports/stm32/can.c index 04514a14f0e11..5804f60cd2e64 100644 --- a/ports/stm32/can.c +++ b/ports/stm32/can.c @@ -307,6 +307,25 @@ HAL_StatusTypeDef can_transmit(CAN_HandleTypeDef *hcan, CanTxMsgTypeDef *txmsg, } } +can_state_t can_get_state(CAN_HandleTypeDef *can) { + uint32_t esr; + + if (can->State == HAL_CAN_STATE_RESET) { + return CAN_STATE_STOPPED; + } + + esr = can->Instance->ESR; + if (esr & CAN_ESR_BOFF) { + return CAN_STATE_BUS_OFF; + } else if (esr & CAN_ESR_EPVF) { + return CAN_STATE_ERROR_PASSIVE; + } else if (esr & CAN_ESR_EWGF) { + return CAN_STATE_ERROR_WARNING; + } else { + return CAN_STATE_ERROR_ACTIVE; + } +} + // Workaround for the __HAL_CAN macros expecting a CAN_HandleTypeDef which we // don't have in the ISR. Using this "fake" struct instead of CAN_HandleTypeDef // so it's not possible to accidentally call an API that uses one of the other diff --git a/ports/stm32/can.h b/ports/stm32/can.h index 82cd89bc2f307..f2601313cf21a 100644 --- a/ports/stm32/can.h +++ b/ports/stm32/can.h @@ -101,6 +101,8 @@ void can_disable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo); // Interrupt for CAN_INT_MESSAGE_RECEIVED is only enabled if enable_msg_received is set. void can_enable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo, bool enable_msg_received); +can_state_t can_get_state(CAN_HandleTypeDef *can); + // Implemented in pyb_can.c, called from lower layer extern void can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo); diff --git a/ports/stm32/fdcan.c b/ports/stm32/fdcan.c index ad1f04a467083..072cfe3dc4a4d 100644 --- a/ports/stm32/fdcan.c +++ b/ports/stm32/fdcan.c @@ -359,6 +359,30 @@ int can_receive(FDCAN_HandleTypeDef *can, can_rx_fifo_t fifo, FDCAN_RxHeaderType return 0; // success } +can_state_t can_get_state(CAN_HandleTypeDef *can) { + uint32_t psr; + + if (can->State != HAL_FDCAN_STATE_BUSY) { + // The HAL states which map to "stopped" are: + // HAL_FDCAN_STATE_RESET - peripheral never initialised + // HAL_FDCAN_STATE_READY - CAN is stopped (i.e. HAL_FDCAN_CAN_Stop() called) + // HAL_FDCAN_STATE_ERROR - Internal transition timeout, mostly relates to + // low-power states we currently don't use + return CAN_STATE_STOPPED; + } + + psr = can->Instance->PSR; + if (psr & FDCAN_PSR_BO) { + return CAN_STATE_BUS_OFF; + } else if (psr & FDCAN_PSR_EP) { + return CAN_STATE_ERROR_PASSIVE; + } else if (psr & FDCAN_PSR_EW) { + return CAN_STATE_ERROR_WARNING; + } else { + return CAN_STATE_ERROR_ACTIVE; + } +} + static void can_rx_irq_handler(uint can_id, CAN_TypeDef *instance, can_rx_fifo_t fifo) { uint32_t ints, rx_fifo_ints, error_ints; diff --git a/ports/stm32/pyb_can.c b/ports/stm32/pyb_can.c index e546163f7fcde..c499a28197a38 100644 --- a/ports/stm32/pyb_can.c +++ b/ports/stm32/pyb_can.c @@ -408,33 +408,11 @@ static MP_DEFINE_CONST_FUN_OBJ_1(pyb_can_restart_obj, pyb_can_restart); // Get the state of the controller static mp_obj_t pyb_can_state(mp_obj_t self_in) { pyb_can_obj_t *self = MP_OBJ_TO_PTR(self_in); - mp_int_t state = CAN_STATE_STOPPED; if (self->is_enabled) { - CAN_TypeDef *can = self->can.Instance; - #if MICROPY_HW_ENABLE_FDCAN - uint32_t psr = can->PSR; - if (psr & FDCAN_PSR_BO) { - state = CAN_STATE_BUS_OFF; - } else if (psr & FDCAN_PSR_EP) { - state = CAN_STATE_ERROR_PASSIVE; - } else if (psr & FDCAN_PSR_EW) { - state = CAN_STATE_ERROR_WARNING; - } else { - state = CAN_STATE_ERROR_ACTIVE; - } - #else - if (can->ESR & CAN_ESR_BOFF) { - state = CAN_STATE_BUS_OFF; - } else if (can->ESR & CAN_ESR_EPVF) { - state = CAN_STATE_ERROR_PASSIVE; - } else if (can->ESR & CAN_ESR_EWGF) { - state = CAN_STATE_ERROR_WARNING; - } else { - state = CAN_STATE_ERROR_ACTIVE; - } - #endif + return MP_OBJ_NEW_SMALL_INT(can_get_state(&self->can)); + } else { + return MP_OBJ_NEW_SMALL_INT(CAN_STATE_STOPPED); } - return MP_OBJ_NEW_SMALL_INT(state); } static MP_DEFINE_CONST_FUN_OBJ_1(pyb_can_state_obj, pyb_can_state); From 022570445b689bbdd9ee9d9dec861b4b088f998b Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Mon, 15 Dec 2025 17:30:15 +1100 Subject: [PATCH 13/53] extmod,docs: Add generic machine.CAN helpers & docs. API is different to the original machine.CAN proposal, as numerous shortcomings were found during initial implementation. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- docs/library/machine.CAN.rst | 614 ++++++++++++++++++++++++++++ docs/library/machine.rst | 1 + extmod/extmod.cmake | 1 + extmod/extmod.mk | 1 + extmod/machine_can.c | 749 +++++++++++++++++++++++++++++++++++ extmod/machine_can.h | 41 ++ extmod/machine_can_port.h | 188 +++++++++ extmod/modmachine.c | 3 + extmod/modmachine.h | 1 + 9 files changed, 1599 insertions(+) create mode 100644 docs/library/machine.CAN.rst create mode 100644 extmod/machine_can.c create mode 100644 extmod/machine_can.h create mode 100644 extmod/machine_can_port.h diff --git a/docs/library/machine.CAN.rst b/docs/library/machine.CAN.rst new file mode 100644 index 0000000000000..0da26b90167d6 --- /dev/null +++ b/docs/library/machine.CAN.rst @@ -0,0 +1,614 @@ +.. currentmodule:: machine +.. _machine.CAN: + +class CAN -- Controller Area Network protocol +============================================= + +CAN is a two-wire serial protocol used for reliable real-time message delivery +between one or more nodes connected to a common bus. CAN 2.0 was standardised in +ISO-11898, and is now also known as CAN Classic. + +There is also a newer, backwards compatible, protocol named CAN FD (CAN with +Flexible Data-Rate). *The machine.CAN driver does not currently support CAN FD +features, use `pyb.CAN` on stm32 if you need CAN FD*. + +CAN support requires a controller (often an internal microcontroller +peripheral), and an external transceiver to level-shift the signals onto the CAN +bus. + +The ``machine.CAN`` interface is a *low level basic* CAN messaging interface +that abstracts a CAN controller as an outgoing priority queue for sending +messages, an incoming queue for receiving messages, and mechanisms for reporting +errors. + +.. note:: The planned ``can`` and ``aiocan`` micropython-lib modules will be the + recommended way to use CAN with MicroPython. + +Constructor +----------- + +.. class:: CAN(id, *args, **kwargs) + + Construct a CAN controller object of the given id: + + - ``id`` identifies a particular CAN controller object; it is board and port specific. + - All other arguments are passed to :func:`CAN.init`. At least one argument (``bitrate``) + must be provided. + + Future versions of this class may also accept port-specific keyword arguments + here which configure the hardware. Currently no such keyword arguments are + implemented. + + Example + ^^^^^^^ + + Construct and initialise CAN controller 1 with bitrate 500kbps:: + + from machine import CAN + can = CAN(1, 500_000) + +.. Add a table of port-specific keyword arguments here, once they exist + +Methods +------- + +.. method:: CAN.init(bitrate, mode=CAN.MODE_NORMAL, sample_point=75, sjw=1, tseg1=None, tseg2=None) + + Initialise the CAN bus with the given parameters: + + - *bitrate* is the desired bus bit rate in bits per second. + - *mode* is one of the values shown under `can-modes`, indicating the + desired mode of operation. The default is "normal" operation on the bus. + + The next parameters are optional and relate to CAN bit timings. In most cases + you can leave these parameters set to the default values: + + - *sample_point* is an integer percentage of the data bit time. It + specifies the position of the bit sample with respect to the whole nominal + bit time. The CAN driver will calculate parameters accordingly. This + parameter is ignored if *tseg1* and *tseg2* are set. + - *sjw* is the resynchronisation jump width in units of time quanta for + nominal bits; it can be a value between 1 and 4 inclusive for classic CAN. + - *tseg1* defines the location of the sample point in units of time quanta + for nominal bits; it can be a value between 1 and 16 inclusive for classic + CAN. This is the sum of the ``Prop_Seg`` and ``Phase_Seg1`` phases as + defined in the ISO-11898 standard. If this value is set then *tseg2* + must also be set and *sample_point* is ignored. + - *tseg2* defines the location of the transmit point in units of the time + quanta for nominal bits; it can be a value between 1 and 8 inclusive for + classic CAN. This corresponds to ``Phase_Seg2`` in the ISO-11898 standard. + If this value is set then *tseg1* must also be set. + + If these arguments are specified then the CAN controller is configured + correctly for the desired *bitrate* and the specified total number of time + quanta per bit. The *tseg1* and *tseg2* values override the + *sample_point* argument if all of these are supplied. + + .. note:: Individual controller hardware may have additional restrictions on + valid values for these parameters, and will raise a ``ValueError`` + if a given value is not supported. + + .. note:: Specific controller hardware may accept additional optional + keyword parameters for hardware-specific features such as oversampling. + +.. method:: CAN.set_filters(filters) + + Set receive filters in the CAN controller. *filters* can be: + + - ``None`` to accept all incoming messages, or + - ``[]`` or ``()`` to disable all message receiving, or + - An iterable of one or more items defining the filter criteria. Each item + should be a tuple or list with three elements: + + - ``identifier`` is a CAN identifier (int). + - ``bit_mask`` is a bit mask for bits in the CAN identifier field (int). + - ``flags`` is an integer with zero or more of the bits defined in + `can-flags` set. This specifies properties that the incoming message needs + to match. Not all controllers support filtering on all flags, a + ``ValueError`` is raised if an unsupported flag is requested. + + Incoming messages are accepted if the bits masked in ``bit_mask`` match between + the message identifier and the filter ``identifier`` value, and flags set in the + filter match the incoming message. + + If the `CAN.FLAG_EXT_ID` bit is set in flags, the filter matches Extended + CAN IDs only. If the `CAN.FLAG_EXT_ID` bit is not set, the filter matches + Standard CAN IDs only. + + All filters are ORed together in the controller. Passing an empty list or + tuple for the filters argument means that no messages will be received. + + Some CAN controllers require each filter to be associated with only one + receive FIFO. In these cases, the filter items in the argument are allocated + round-robin to the available FIFOs. This driver does not distinguish between + FIFOs in the receive IRQ. + + .. note:: If the caller passes an iterable with more items than + :data:`CAN.FILTERS_MAX`, ``ValueError`` will be raised. + + .. note:: If either the ``identifier`` or the ``bit_mask`` is out of range + for the specified ID type, a ``ValueError`` with reason "invalid id" + will be raised. + + Examples + ^^^^^^^^ + + Receive all incoming messages:: + + can.set_filters(None) + + Receive messages with Standard ID values 0x301 and 0x700 only:: + + can.set_filters(((0x301, 0x7FF, 0), + (0x700, 0x7FF, 0))) + + Receive messages with Standard ID values in range 0x300-0x3FF, and Extended + ID value 0x50700 only:: + + can.set_filters(((0x300, 0x700, 0), + (0x50700, 0x1FFF_FFFF, CAN.FLAG_EXT_ID))) + +.. data:: CAN.FILTERS_MAX + + Constant value that reads the maximum number of supported receive filters + for this hardware controller. + + Note that some controllers may have more complex hardware restrictions on the + number of filters in use (for example, counting Standard and Extended ID + filters independently.) In these cases `CAN.set_filters` may raise a + ``ValueError`` even when the ``FILTERS_MAX`` limit is not exceeded. + +.. method:: CAN.send(id, data, flags=0) + + Copy a new CAN message into the controller's hardware transmit queue to be + sent onto the bus. The transmit queue is a priority queue sorted on CAN + identifier priority (lower numeric identifiers have higher priority). + + - *id* is an integer CAN identifier value. + - *data* is a bytes object (or similar) containing the CAN message data, + or describing a Remote Transmission Request (see below). + - *flags* is an integer with zero or more of the bits defined in + `can-flags` set, specifying properties of the outgoing CAN message + (Extended ID, Remote Transmission Request, etc.) + + If the message is successfully queued for transmit onto the bus, the function + returns an integer in the range ``0`` to `CAN.TX_QUEUE_LEN` (exclusive). This + value is the transmit buffer index where the message is queued to send, and + can be used by the `CAN.cancel_send` function and in `CAN.IRQ_TX` events. + + If the queue is full then the send will fail and ``None`` is returned. + + The send can also fail and return ``None`` if the provided *id* value has + equal priority to an existing message in the transmit queue and the CAN + controller hardware cannot guarantee that messages with the same ID will be + sent onto the bus in the same order they were added to the queue. To queue + the message anyway, pass the value :data:`CAN.FLAG_UNORDERED` flag in + the *flags* argument. This flag indicates that it's OK to send messages with + the same CAN ID onto the bus in any order. + + If the controller is in the "Bus Off" error state or disabled then calling + this function will raise an ``OSError``. + + .. note:: This intentionally low-level implementation is designed so the + caller can establish a software queue of outgoing messages. + + .. important:: The CAN "transmit queue" is not a FIFO queue, it is priority + ordered, and although it can hold up to `CAN.TX_QUEUE_LEN` + items there may be other hardware restrictions on messages + which can be queued at the same time. + + Remote Transmission Requests + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + If the bit `CAN.FLAG_RTR` is set in the *flags* argument then the controller + will send a Remote Transmission Request instead of a message. In this case + the contents of the *data* argument is ignored. The controller will send + a request where the ``DLC`` length field is equal to the length of the + *data* argument. + + Examples + ^^^^^^^^ + + Attempt to send a message with three byte payload ``0a0b0c`` and Standard ID 0x200:: + + can.send(0x200, b"\x0a\x0b\x0c", 0) + + Attempt to send a message with an empty payload and Extended ID 0x180008. + Indicate that the controller can send messages with this ID in any order, in + case other messages are already queued to send with the same ID:: + + can.send(0x180008, b"", can.FLAG_EXT_ID | can.FLAG_UNORDERED) + + Attempt to send a Remote Transmission Request with length 8 bytes and Standard ID 0x555:: + + can.send(0x555, b" " * 8, can.FLAG_RTR) + +.. method:: CAN.recv(arg=None) + + Return a CAN message that has been received by the controller, according to + filters set by :func:`CAN.set_filters`. + + This function takes a single optional argument, if provided then it must be a + list of at least 4 elements where the second element is a `memoryview` object + that refers to a `bytearray` or similar object that has enough capacity to hold + any received CAN message (8 bytes for CAN Classic, 64 bytes for CAN FD). The + provided list will be returned as a successful result, and avoids memory + allocation inside the function. + + If no messages have been received by the CAN controller, this function + returns ``None``. + + .. note:: `CAN.set_filters` must be called before any messages can be received by + the controller. To receive all messages, call ``set_filters(None)``. + + If a message has been received by the CAN controller, this function returns a + list with 4 elements: + + - Index 0 is the CAN ID of the received message, as an integer. + - Index 1 is a memoryview that provides access to the received message data. + + - If *arg* is not provided then this is a `memoryview` holding the bytes + which were received. This `memoryview` is backed by a newly allocated + `bytearray` large enough to hold any received CAN message. This allows + the result to be safely reused as a future *arg*, to save memory allocations. + - If *arg* is provided then the provided `memoryview` will be resized to + hold exactly the bytes which were received. The caller is responsible for + making sure the backing object for the `memoryview` can hold a CAN + message of any length. + + - Index 2 is an integer with zero or more of the bits defined in + `can-flags` set. It indicates metadata about the received message. + - Index 3 is an integer with zero or more of the bits defined in + `can-recv-errors` set. Any non-zero value indicates potential issues when + receiving CAN messages. These flags are reset inside the controller each + time this function returns. + + Remote Transmission Requests + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + If a Remote Transmission Request is received then the bit `CAN.FLAG_RTR` + will be set in Index 2 and the memoryview at Index 1 will contain all + zeroes, with a length equal to the ``DLC`` field of the received request. + + Example + ^^^^^^^ + :: + + can.set_filters(None) # receive all + while True: + res = can.recv() + if res: + can_id, data, flags, errs = res + print("Received", hex(can_id), data.hex(), hex(flags), hex(errs)) + else: + time.sleep_ms(1) # not a good pattern, use the irq instead! + + +.. method:: CAN.irq(handler=None, trigger=0, hard=False) + + Sets an interrupt *handler* function to be called when one or more of the + events flagged in *trigger* has occurred. + + - *handler* is a function to be called when the interrupt event + triggers. The handler must take exactly one argument which is the + :class:`CAN` instance. + + - *trigger* configures the event(s) which can generate an interrupt. + Possible values are a mask of one or more of the following: + + - `CAN.IRQ_RX` event occurs after the CAN controller has received + at least one message into its RX FIFO (meaning that :func:`CAN.recv()` + will return successfully). + - `CAN.IRQ_TX` event occurs after the CAN controller has either + successfully sent a message onto the CAN bus or failed to send a + message. This trigger has additional requirements for the handler, see + `machine_can_irq_flags` for details. + - `CAN.IRQ_STATE` event occurs when the CAN controller has transitioned + into a more severe error state. Call :func:`CAN.state()` to get the + updated state. + + - *hard* if True, a hard interrupt is used. This reduces the delay between + the CAN controller event and the handler being called. Hard interrupt + handlers may not allocate memory; see :ref:`isr_rules`. + + Returns an irq object. If called with no arguments then a previously-configured + irq object is returned. + + See `machine_can_irq_flags` for an example. + +.. method:: CAN.cancel_send(index) + + Request the CAN controller to cancel sending a message onto the bus. + + Argument *index* identifies a single transmit buffer. It should be an + integer in the range ``0`` to ``CAN.TX_QUEUE_LEN`` (exclusive). Generally + this will be a value previously returned by :func:`CAN.send()`. + + The result is ``True`` if a message was pending transmission in this buffer + and transmission was cancelled. + + The result is ``False`` otherwise (either no message was pending transmission + in this buffer, or transmission succeeded already). + + The IRQ event `CAN.IRQ_TX` should be used to determine if a message + was definitely sent or not, but note there are potential race conditions if a + transmission is cancelled and then the same buffer is used to send another + message (especially if the CAN controller IRQ is not "hard"). + +.. method:: CAN.state() + + Returns an integer value indicating the current state of the controller. + The value will be one of the values defined in `can-states`. + + Lower severity error states may automatically clear if the bus recovers, but + the `CAN.STATE_BUS_OFF` state can only be recovered by calling + :func:`CAN.restart()`. + +.. method:: CAN.get_counters(list=None /) + + Returns controller's error counter values. The result is a list of eight + values. If the optional *list* parameter is specified then the provided + list object is updated and returned as the result, to avoid an allocation. + + The list items are: + + - TEC (Transmit Error Counter) value + - REC (Receive Error Counter) value + - Number of times the controller entered the Warning state from the Active state. + - Number of times the controller entered the Error Passive state from the Warning state. + - Number of times the controller entered the Bus Off state from the Error Passive state. + - Total number of pending TX messages in the hardware queue. + - Total number of pending RX messages in the hardware queue. + - Number of times an RX overrun occurred. + + .. note:: Depending on the controller, these values may overflow back to 0 after + a certain value. + + .. note:: If a controller doesn't support a particular counter, it will return + ``None`` for that list element. + +.. method:: CAN.get_timings(list=None /) + + Returns a list of elements indicating the current timings configured in the + CAN controller. This can be used to verify timings for debugging purposes. + The result is a list of six values. If the optional *list* parameter is + specified then the provided list object is updated and returned as the + result, to avoid an allocation. + + The list items are: + + - Exact bitrate used by the controller. May vary from *bitrate* argument + passed to :func:`CAN.init()` due to quantisation to meet hardware + constraints. + - Resynchronisation jump width (SJW) in units of time quanta for nominal + bits. Has the same meaning as the *sjw* parameter of :func:`CAN.init()`. + - Location of the sample point in units of time quanta for nominal bits. Has + the same meaning as the *tseg1* parameter of :func:`CAN.init()`. + - Location of the transmit point in units of time quanta for nominal bits. + Has the same meaning as the *tseg2* parameter of :func:`CAN.init()`. + - CAN FD timing information. ``None`` for controllers which don't support CAN + FD, or if CAN FD is not initialised. Otherwise, a nested list of four + elements corresponding to the items above but applicable to the CAN FD BRS + feature. + - Optional controller-specific timing information. Depending on the + controller this will either be ``None`` if controller doesn't report any, + or it will be a constant length list whose elements are specific to a + particular hardware controller. + + .. note:: If :func:`CAN.init()` has not been called then this function + still returns a result, but the result depends on the controller + internals and may not be accurate. + +.. method:: CAN.restart() + + Causes the controller to exit `STATE_BUS_OFF` without clearing any other + internal state. Also clears some of the error counters (always the number + of times each error state has been entered, possibly TEC and REC depending + on the controller.) + + Calling this function also cancels any messages waiting to be sent. No + `IRQ_TX` interrupts are delivered for these messages. + + Note that this function may or may not cause the controller to exit the + "Error Passive" state, depending whether the controller hardware zeroes + TEC and REC or not. + +.. method:: CAN.deinit() + + De-initialises a previously active CAN instance. All pending messages + (transmit and receive) are dropped and the controller stops interacting on + the bus. To use this instance again, call :func:`CAN.init()`. + + No `IRQ_TX` or `IRQ_RX` interrupts are called in response to calling this + function. + + See also :func:`CAN.restart()`. + +Constants +--------- + +.. data:: CAN.TX_QUEUE_LEN + + Maximum number of CAN messages which can be queued in the outgoing hardware + message queue of the controller. The "transmit buffer indexes" used by + :func:`CAN.send()`, :func:`CAN.cancel_send()` and `machine_can_irq_flags` + will be in this range. + +.. _can-modes: + +Modes +^^^^^ + +These values represent controller modes of operation, as passed to `CAN.init()`. Not all controllers may support all modes. + +Changing the mode of a running controller requires calling `CAN.deinit()` and then calling `CAN.init()` again with the new mode. + +.. data:: CAN.MODE_NORMAL + + The controller is active as a standard CAN network node (will acknowledge + valid messages and may transmit errors depending on its current `State + `). + +.. data:: CAN.MODE_SLEEP + + CAN controller is asleep in a low power mode. Depending on the controller, + this may support waking the controller and transitioning to `CAN.MODE_NORMAL` + if CAN traffic is received. + +.. data:: CAN.MODE_LOOPBACK + + A testing mode. The CAN controller is still connected to the external bus, + but will also receive its own transmitted messages and ignore any ACK errors. + +.. data:: CAN.MODE_SILENT + + CAN controller receives messages but does not interact with the CAN bus + (including sending ACKs, errors, etc.) + +.. data:: CAN.MODE_SILENT_LOOPBACK + + A testing mode that does not require a CAN transceiver to be connected at + all. The CAN controller receives its own transmitted messages without + interacting with the CAN bus at all. The CAN TX and RX pins remain idle. + +.. _can-states: + +States +^^^^^^ + +These values are returned by :func:`CAN.state()` and reflect the error state of the CAN controller: + +.. data:: CAN.STATE_STOPPED + + The controller has not been initialised. + +.. data:: CAN.STATE_ACTIVE + + The controller is active and ``TEC`` and ``REC`` error counters are both below the + warning threshold of 96. See :func:`CAN.get_counters()`. + +.. data:: CAN.STATE_WARNING + + The controller is active but at last one of the ``TEC`` and ``REC`` error counters + are between 96 and 127. See :func:`CAN.get_counters()`. + +.. data:: CAN.STATE_PASSIVE + + The controller is in the "Error Passive" state meaning it no longer transmits active + errors to the bus, but it is otherwise functional. This state is entered when at + least one of the ``TEC`` and ``REC`` error counters is 128 or greater, but + ``TEC`` is less than 255. See :func:`CAN.get_counters()`. + +.. data:: CAN.STATE_BUS_OFF + + The controller is in the Bus-Off state, meaning ``TEC`` error counter is + greater than 255. The CAN controller will not interact with the bus in this + state, and needs to be restarted via :func:`CAN.restart()` to continue. + +.. _can-flags: + +Message Flags +^^^^^^^^^^^^^ + + These values represent metadata about a CAN message. Functions + :func:`CAN.send()`, :func:`CAN.recv()`, and :func:`CAN.set_filters()` either + accept or return an integer value made up of zero or more of these flags bitwise + ORed together. + +.. data:: CAN.FLAG_RTR + + Indicates a message is a remote transmission request. + +.. data:: CAN.FLAG_EXT_ID + + If set, indicates a Message identifier is Extended (29-bit). If not set, + indicates a message identifier is Standard (11-bit). + +.. data:: CAN.FLAG_UNORDERED + + If set in the ``flags`` argument of :func:`CAN.send`, indicates that it's + OK if messages with the same CAN ID are sent in any order onto the bus. + + Otherwise trying to queue multiple messages with the same ID may result in + :func:`CAN.send` failing if the controller hardware can't enforce ordering. + + This flag is never set on received messages, and is ignored by + `CAN.set_filters()`. + +.. _can-recv-errors: + +Receive Error Flags +^^^^^^^^^^^^^^^^^^^ + +The result of :func:`CAN.recv()` includes an integer value made up of zero or +more of these flags bitwise ORed together. If set, these flags indicate +potential general issues with receiving CAN messages. + +.. data:: CAN.RECV_ERR_FULL + +The hardware FIFO where this message was received is full, and additional incoming messages may be lost. + +.. data:: CAN.RECV_ERR_OVERRUN + +The hardware FIFO where this message was received is full, and one or more incoming messages has been lost. + +IRQ values +^^^^^^^^^^ + +.. data:: CAN.IRQ_RX + CAN.IRQ_TX + CAN.IRQ_STATE + + IRQ event triggers. Used with :func:`CAN.irq()` and `machine_can_irq_flags`. + +.. data:: CAN.IRQ_TX_FAILED + CAN.IRQ_TX_IDX_SHIFT + CAN.IRQ_TX_IDX_MASK + + Additional IRQ event flags for `CAN.IRQ_TX`. See `machine_can_irq_flags`. + +.. _machine_can_irq_flags: + +IRQ flags +--------- + +Calling :func:`CAN.irq()` registers an interrupt handler with one or more of the +triggers `CAN.IRQ_RX`, `CAN.IRQ_TX` and `CAN.IRQ_STATE`. + +The function returns an IRQ object, and calling the ``flags()`` function on this +object returns an integer indicating which trigger event(s) triggered the +interrupt. A CAN IRQ handler should call the ``flags()`` function repeatedly +until it returns ``0``. + +When the ``flags()`` function returns with `CAN.IRQ_TX` bit set, the +handler can also check the following flag bits in the result for additional +information about the TX event: + +* ``CAN.IRQ_TX_FAILED`` bit is set if the transmit failed. Usually this will + only happen if :func:`CAN.cancel_send()` was called, although it may also + happen if the controller enters an error state. +* ``CAN.IRQ_TX_MASK << CAN.IRQ_TX_SHIFT`` is a bitmasked region of the flags + value that holds the index of the transmit buffer which generated the event. + This will be an integer in the range ``0`` to `CAN.TX_QUEUE_LEN` (exclusive), + and will match the result of a previous call to `CAN.send()`. + +IRQ_TX Example +^^^^^^^^^^^^^^ +:: + + from machine import CAN + + def irq_send(can): + while flags := can.irq().flags(): + if flags & can.IRQ_TX: + idx = (flags >> can.IRQ_TX_IDX_SHIFT) & can.IRQ_TX_IDX_MASK + success = not (flags & can.IRQ_TX_FAILED) + print("irq_send", idx, success) + + can = CAN(1, 500_000) + can.irq(irq_send, trigger=can.IRQ_TX, hard=True) + +.. important:: If the `CAN.IRQ_TX` trigger is set then the handler **must** + call ``flags()`` repeatedly until it returns ``0``, as shown in + this example. Otherwise, CAN interrupts may not be correctly + re-enabled. diff --git a/docs/library/machine.rst b/docs/library/machine.rst index 31acb74920c49..481820defc16e 100644 --- a/docs/library/machine.rst +++ b/docs/library/machine.rst @@ -259,6 +259,7 @@ Classes machine.I2C.rst machine.I2CTarget.rst machine.I2S.rst + machine.CAN.rst machine.RTC.rst machine.Timer.rst machine.Counter.rst diff --git a/extmod/extmod.cmake b/extmod/extmod.cmake index 9ca869582e026..40ae717a54f6a 100644 --- a/extmod/extmod.cmake +++ b/extmod/extmod.cmake @@ -10,6 +10,7 @@ set(MICROPY_SOURCE_EXTMOD ${MICROPY_EXTMOD_DIR}/machine_adc.c ${MICROPY_EXTMOD_DIR}/machine_adc_block.c ${MICROPY_EXTMOD_DIR}/machine_bitstream.c + ${MICROPY_EXTMOD_DIR}/machine_can.c ${MICROPY_EXTMOD_DIR}/machine_i2c.c ${MICROPY_EXTMOD_DIR}/machine_i2c_target.c ${MICROPY_EXTMOD_DIR}/machine_i2s.c diff --git a/extmod/extmod.mk b/extmod/extmod.mk index 37151ad120e68..961699f089e6b 100644 --- a/extmod/extmod.mk +++ b/extmod/extmod.mk @@ -5,6 +5,7 @@ SRC_EXTMOD_C += \ extmod/machine_adc.c \ extmod/machine_adc_block.c \ extmod/machine_bitstream.c \ + extmod/machine_can.c \ extmod/machine_i2c.c \ extmod/machine_i2c_target.c \ extmod/machine_i2s.c \ diff --git a/extmod/machine_can.c b/extmod/machine_can.c new file mode 100644 index 0000000000000..11577d9da9407 --- /dev/null +++ b/extmod/machine_can.c @@ -0,0 +1,749 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2024-2026 Angus Gratton + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "py/objstr.h" +#include "py/runtime.h" + +#if MICROPY_PY_MACHINE_CAN + +#include +#include +#include "extmod/modmachine.h" +#include "extmod/machine_can.h" +#include "extmod/machine_can_port.h" + +#ifndef MICROPY_HW_NUM_CAN +#error "Port must provide a definition of MICROPY_HW_NUM_CAN" +#endif + +#define CAN_MSG_RTR (1 << 0) +#define CAN_MSG_EXTENDED_ID (1 << 1) +#define CAN_MSG_FD_F (1 << 2) +#define CAN_MSG_BRS (1 << 3) + +#define CAN_CLASSIC_MAX_LEN 8 + +// The port provides implementations of the static machine_can_port.h functions in this file. +#include MICROPY_PY_MACHINE_CAN_INCLUDEFILE + +// Note: not all possible return values of flags() are allowed as triggers +#define CAN_IRQ_ALLOWED_TRIGGERS (MP_CAN_IRQ_TX | MP_CAN_IRQ_RX | MP_CAN_IRQ_STATE) + +// Port can override any of these limits if necessary +#ifndef CAN_TSEG1_MIN +#define CAN_TSEG1_MIN 1 +#endif +#ifndef CAN_TSEG1_MAX +#define CAN_TSEG1_MAX 16 +#endif +#ifndef CAN_TSEG2_MIN +#define CAN_TSEG2_MIN 1 +#endif +#ifndef CAN_TSEG2_MAX +#define CAN_TSEG2_MAX 8 +#endif +#ifndef CAN_SJW_MIN +#define CAN_SJW_MIN 1 +#endif +#ifndef CAN_SJW_MAX +#define CAN_SJW_MAX 4 +#endif + +#if MICROPY_HW_ENABLE_FDCAN +// CAN-FD BRS (Baud Rate Switch) default limits +#ifndef CAN_FD_BRS_TSEG1_MIN +#define CAN_FD_BRS_TSEG1_MIN 1 +#endif +#ifndef CAN_FD_BRS_TSEG1_MAX +#define CAN_FD_BRS_TSEG1_MAX 32 +#endif +#ifndef CAN_FD_BRS_TSEG2_MIN +#define CAN_FD_BRS_TSEG2_MIN 1 +#endif +#ifndef CAN_FD_BRS_TSEG2_MAX +#define CAN_FD_BRS_TSEG2_MAX 16 +#endif +#ifndef CAN_FD_BRS_SJW_MIN +#define CAN_FD_BRS_SJW_MIN 1 +#endif +#ifndef CAN_FD_BRS_SJW_MAX +#define CAN_FD_BRS_SJW_MAX 16 +#endif +#endif // MICROPY_HW_ENABLE_FDCAN + +#ifndef CAN_FILTERS_STD_EXT_SEPARATE +// Set if the hardware maintains separate filter indexes for Standard vs Extended IDs +#define CAN_FILTERS_STD_EXT_SEPARATE 0 +#endif +#ifndef CAN_PORT_PRINT_FUNCTION +// If this is set then the port should define its own function machine_can_print() +#define CAN_PORT_PRINT_FUNCTION 0 +#endif + +// Non port-specific functions follow + +// Calculate baud rate prescaler, and if necessary also find tseg1, tseg2 +// to satisfy sample_point +// +static int calculate_brp(int bitrate_nom, int f_clock, int *tseg1, int *tseg2, int sample_point, bool is_fd_brs) { + bool find_tseg = (*tseg1 == -1); // We need to calculate tseg1, tseg2 + + // Set min/max limits for Classic CAN + int brp_min = CAN_BRP_MIN; + int brp_max = CAN_BRP_MAX; + int tseg1_min = CAN_TSEG1_MIN; + int tseg1_max = CAN_TSEG1_MAX; + int tseg2_min = CAN_TSEG2_MIN; + int tseg2_max = CAN_TSEG2_MAX; + #if MICROPY_HW_ENABLE_FDCAN + // If CAN-FD controller, set min/max limits for CAN-FD BRS + if (is_fd_brs) { + brp_min = CAN_FD_BRS_BRP_MIN; + brp_max = CAN_FD_BRS_BRP_MAX; + tseg1_min = CAN_FD_BRS_TSEG1_MIN; + tseg1_max = CAN_FD_BRS_TSEG1_MAX; + tseg2_min = CAN_FD_BRS_TSEG2_MIN; + tseg2_max = CAN_FD_BRS_TSEG2_MAX; + } + #else + (void)is_fd_brs; + #endif + + int brp_best = brp_min; + int best_bitrate_err = bitrate_nom; // best case deviation from bitrate_nom, start at max + int best_sample_err = 10000; // Units of .01%, start at max + + for (int brp = brp_max; brp >= brp_min; brp--) { + unsigned scaled_clock = f_clock / brp; + + if (find_tseg) { + // Find the total number of time quanta that gets closest to the nominal bitrate + int ts_total = (scaled_clock + bitrate_nom / 2) / bitrate_nom; + + if (ts_total < tseg1_min + tseg2_min + 1 + || ts_total > tseg1_max + tseg2_max + 1) { + // The total time quanta doesn't fit in the allowed range + continue; + } + + int bitrate_err = abs((int)bitrate_nom - (int)(scaled_clock / ts_total)); + + if (bitrate_err > best_bitrate_err) { + // This result is worse than our current best bitrate + continue; + } + + // Look for tseg1 & tseg2 that come closest to the sample point + for (int ts1 = tseg1_min; ts1 <= tseg1_max; ts1++) { + int ts2 = ts_total - 1 - ts1; + if (ts2 >= tseg2_min && ts2 <= tseg2_max) { + int try_sample = 10000 * (1 + ts1) / (1 + ts1 + ts2); // sample point, units of .01% + int try_err = abs(try_sample - sample_point * 100); + // Priorities for selecting the best: + // 1. Smallest bitrate error. + // 2. Smallest sample point error. + // 3. Shorter (i.e. more total) time quanta (meaning if all else is equal, choose lower brp). + if (bitrate_err < best_bitrate_err || try_err <= best_sample_err) { + *tseg1 = ts1; + *tseg2 = ts2; + best_sample_err = try_err; + brp_best = brp; + best_bitrate_err = bitrate_err; + } + } + } + } else { + // tseg1 and tseg2 already set, find brp with the lowest bitrate error + int ts_total = *tseg1 + *tseg2 + 1; + int bitrate_err = abs((int)bitrate_nom - (int)(scaled_clock / ts_total)); + if (bitrate_err <= best_bitrate_err) { + brp_best = brp; + best_bitrate_err = bitrate_err; + } + } + } + + if (best_bitrate_err == bitrate_nom) { + // Didn't find any eligible bitrates + mp_raise_ValueError(MP_ERROR_TEXT("unable to calculate BRP for baudrate")); + } + + return brp_best; +} + +// Check a CAN Message ID value is within the valid range, based on supplied flags +static void id_range_check(mp_uint_t can_id, mp_uint_t flags) { + mp_uint_t max = (flags & CAN_MSG_FLAG_EXT_ID) ? (1 << 29) : (1 << 11); + if (can_id >= max) { + mp_raise_ValueError(MP_ERROR_TEXT("invalid id")); + } +} + +static int machine_can_get_actual_bitrate(machine_can_obj_t *self) { + int f_clock = machine_can_port_f_clock(self); + return f_clock / self->brp / (1 + self->tseg1 + self->tseg2); +} + +static mp_obj_t machine_can_deinit(mp_obj_t self_in) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + // Each CAN instance should have at most one peripheral object + assert(self == MP_STATE_PORT(machine_can_objs)[self->can_idx]); + self->mp_irq_trigger = 0; + self->mp_irq_obj = NULL; + if (self->port) { + machine_can_update_irqs(self); + machine_can_port_deinit(self); + self->port = NULL; + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(machine_can_deinit_obj, machine_can_deinit); + +static void machine_can_init_helper(machine_can_obj_t *self, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_bitrate, ARG_mode, ARG_sample_point, ARG_sjw, ARG_tseg1, ARG_tseg2}; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_bitrate, MP_ARG_INT | MP_ARG_REQUIRED }, + { MP_QSTR_mode, MP_ARG_INT, {.u_int = MP_CAN_MODE_NORMAL} }, + { MP_QSTR_sample_point, MP_ARG_INT, {.u_int = 75} }, + { MP_QSTR_sjw, MP_ARG_INT, {.u_int = CAN_SJW_MIN } }, + { MP_QSTR_tseg1, MP_ARG_INT, {.u_int = -1} }, + { MP_QSTR_tseg2, MP_ARG_INT, {.u_int = -1} }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // Verify arguments + + int bitrate_nom = args[ARG_bitrate].u_int; + + machine_can_mode_t mode = args[ARG_mode].u_int; + if (mode >= MP_CAN_MODE_MAX || !machine_can_port_supports_mode(self, mode)) { + mp_raise_ValueError(MP_ERROR_TEXT("mode")); + } + + int sjw = args[ARG_sjw].u_int; + if (sjw < CAN_SJW_MIN || sjw > CAN_SJW_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("sjw")); + } + + int tseg1 = args[ARG_tseg1].u_int; + int tseg2 = args[ARG_tseg2].u_int; + if ((tseg1 != -1 && (tseg1 < CAN_TSEG1_MIN || tseg1 > CAN_TSEG1_MAX)) + || (tseg1 == -1 && tseg2 != -1)) { + mp_raise_ValueError(MP_ERROR_TEXT("tseg1")); + } + if ((tseg2 != -1 && (tseg2 < CAN_TSEG2_MIN || tseg2 > CAN_TSEG2_MAX)) + || (tseg2 == -1 && tseg1 != -1)) { + mp_raise_ValueError(MP_ERROR_TEXT("tseg2")); + } + + int sample_point = args[ARG_sample_point].u_int; + if (sample_point <= 0 || sample_point >= 100) { + // Probably can make these values more restrictive + mp_raise_ValueError(MP_ERROR_TEXT("sample_point")); + } + + int f_clock = machine_can_port_f_clock(self); + + // This function will also set tseg1 and tseg2 if they are -1 + int brp = calculate_brp(bitrate_nom, f_clock, &tseg1, &tseg2, sample_point, false); + + // Set up the hardware + self->tseg1 = tseg1; + self->tseg2 = tseg2; + self->brp = brp; + self->sjw = sjw; + self->mode = mode; + memset(&self->counters, 0, sizeof(self->counters)); + + if (self->port != NULL) { + machine_can_port_deinit(self); + } + machine_can_port_init(self); +} + +// Raise an exception if the CAN controller is not initialised +static void machine_can_check_initialised(machine_can_obj_t *self) { + // Use self->port being allocated as indicator that controller is initialised + if (self->port == NULL) { + mp_raise_OSError(MP_EINVAL); + } +} + +mp_uint_t machine_can_get_index(mp_obj_t identifier) { + mp_uint_t can_num; + if (mp_obj_is_str(identifier)) { + const char *port = mp_obj_str_get_str(identifier); + if (0) { + #ifdef MICROPY_HW_CAN1_NAME + } else if (strcmp(port, MICROPY_HW_CAN1_NAME) == 0) { + can_num = 1; + #endif + #ifdef MICROPY_HW_CAN2_NAME + } else if (strcmp(port, MICROPY_HW_CAN2_NAME) == 0) { + can_num = 2; + #endif + #ifdef MICROPY_HW_CAN3_NAME + } else if (strcmp(port, MICROPY_HW_CAN3_NAME) == 0) { + can_num = 3; + #endif + } else { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%s) doesn't exist"), port); + } + } else { + can_num = mp_obj_get_int(identifier); + } + if (can_num < 1 || can_num > MICROPY_HW_NUM_CAN) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%d) doesn't exist"), can_num); + } + + // check if the CAN is reserved for system use or not + if (MICROPY_HW_CAN_IS_RESERVED(can_num)) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%d) is reserved"), can_num); + } + + return can_num - 1; +} + +static mp_obj_t machine_can_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { + // check arguments + mp_arg_check_num(n_args, n_kw, 1, MP_OBJ_FUN_ARGS_MAX, true); + + // work out port + mp_uint_t can_idx = machine_can_get_index(args[0]); + + machine_can_obj_t *self = MP_STATE_PORT(machine_can_objs)[can_idx]; + if (self == NULL) { + self = mp_obj_malloc(machine_can_obj_t, &machine_can_type); + self->can_idx = can_idx; + MP_STATE_PORT(machine_can_objs)[can_idx] = self; + } + + mp_map_t kw_args; + mp_map_init_fixed_table(&kw_args, n_kw, args + n_args); + machine_can_init_helper(self, n_args, args, &kw_args); + + return MP_OBJ_FROM_PTR(self); +} + +static mp_obj_t machine_can_init(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + machine_can_init_helper(self, n_args, pos_args, kw_args); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(machine_can_init_obj, 1, machine_can_init); + +void machine_can_deinit_all(void) { + for (int i = 0; i < MICROPY_HW_NUM_CAN; i++) { + mp_obj_t can = MP_OBJ_FROM_PTR(MP_STATE_PORT(machine_can_objs)[i]); + if (can) { + machine_can_deinit(can); + MP_STATE_PORT(machine_can_objs)[i] = NULL; + } + } +} + +static mp_uint_t can_irq_trigger(mp_obj_t self_in, mp_uint_t new_trigger) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + machine_can_check_initialised(self); + self->mp_irq_trigger = new_trigger; + // Call into port layer to update hardware IRQ config + machine_can_update_irqs(self); + return 0; +} + +static mp_uint_t can_irq_info(mp_obj_t self_in, mp_uint_t info_type) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + machine_can_check_initialised(self); + if (info_type == MP_IRQ_INFO_FLAGS) { + // Call into port layer to get current irq flags + return machine_can_port_irq_flags(self); + } else if (info_type == MP_IRQ_INFO_TRIGGERS) { + return self->mp_irq_trigger; + } + return 0; +} + +static const mp_irq_methods_t can_irq_methods = { + .trigger = can_irq_trigger, + .info = can_irq_info, +}; + +static mp_obj_t mp_machine_can_irq(machine_can_obj_t *self, bool any_args, mp_arg_val_t *args) { + machine_can_check_initialised(self); + if (self->mp_irq_obj == NULL) { + self->mp_irq_trigger = 0; + self->mp_irq_obj = mp_irq_new(&can_irq_methods, MP_OBJ_FROM_PTR(self)); + } + + if (any_args) { + // TODO: refactor this into a helper to save some code size + mp_uint_t trigger = args[MP_IRQ_ARG_INIT_trigger].u_int; + mp_uint_t not_supported = trigger & ~CAN_IRQ_ALLOWED_TRIGGERS; + if (not_supported) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("trigger 0x%08x unsupported"), not_supported); + } + + mp_obj_t handler = args[MP_IRQ_ARG_INIT_handler].u_obj; + if (handler != mp_const_none && !mp_obj_is_callable(handler)) { + mp_raise_ValueError(MP_ERROR_TEXT("handler must be None or callable")); + } + + self->mp_irq_obj->handler = handler; + self->mp_irq_obj->ishard = args[MP_IRQ_ARG_INIT_hard].u_bool; + + can_irq_trigger(MP_OBJ_FROM_PTR(self), trigger); + } + + return MP_OBJ_FROM_PTR(self->mp_irq_obj); +} + +static mp_obj_t machine_can_send(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + machine_can_check_initialised(self); + + enum { ARG_id, ARG_data, ARG_flags, /*ARG_context*/ }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_id, MP_ARG_REQUIRED | MP_ARG_INT, {} }, + { MP_QSTR_data, MP_ARG_REQUIRED | MP_ARG_OBJ, {} }, + { MP_QSTR_flags, MP_ARG_INT, {.u_int = 0} }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, + MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + mp_uint_t can_id = args[ARG_id].u_int; + mp_uint_t flags = args[ARG_flags].u_int; + + mp_buffer_info_t data; + mp_get_buffer_raise(args[ARG_data].u_obj, &data, MP_BUFFER_READ); + + if (data.len > machine_can_port_max_data_len(flags)) { + mp_raise_msg(&mp_type_OverflowError, MP_ERROR_TEXT("data too long")); + } + id_range_check(can_id, flags); + + mp_int_t res = machine_can_port_send(self, can_id, data.buf, data.len, flags); + + return res >= 0 ? MP_OBJ_NEW_SMALL_INT(res) : mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(machine_can_send_obj, 3, machine_can_send); + +static mp_obj_t machine_can_cancel_send(mp_obj_t self_in, mp_obj_t arg) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + machine_can_check_initialised(self); + mp_uint_t idx = mp_obj_get_uint(arg); + if (idx >= CAN_TX_QUEUE_LEN) { + mp_raise_type(&mp_type_IndexError); + } + bool res = machine_can_port_cancel_send(self, mp_obj_get_uint(arg)); + return mp_obj_new_bool(res); +} +static MP_DEFINE_CONST_FUN_OBJ_2(machine_can_cancel_send_obj, machine_can_cancel_send); + +static mp_obj_t machine_can_recv(size_t n_args, const mp_obj_t *args) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(args[0]); + machine_can_check_initialised(self); + + uint8_t data[MP_CAN_MAX_LEN]; + size_t dlen = sizeof(data); + mp_uint_t id = 0; + mp_uint_t flags = 0; + mp_uint_t errors = 0; + mp_obj_array_t *data_memoryview; + bool use_arg_as_result = n_args > 1 && args[1] != mp_const_none; + mp_obj_list_t *result = mp_obj_list_optional_arg(use_arg_as_result ? args[1] : mp_const_none, RECV_ARG_LEN); + mp_obj_t *items = result->items; + + // Validate the memoryview item if list passed in arg + if (use_arg_as_result) { + if (!mp_obj_is_type(items[RECV_ARG_DATA], &mp_type_memoryview)) { + mp_raise_TypeError(NULL); + } + } + + // Call the port-specific receive function + if (!machine_can_port_recv(self, data, &dlen, &id, &flags, &errors)) { + return mp_const_none; // Nothing to return + } + + // Create memoryview for the result list, if not passed in + if (!use_arg_as_result) { + // Make the result memoryview large enough for the entire result buffer, not the recv result + // ... it will be resized further down + // (Potentially wasteful, but means the result can be safely reused as arg to a subsequent call) + void *backing_buf = m_malloc(MP_CAN_MAX_LEN); + items[RECV_ARG_DATA] = mp_obj_new_memoryview('B', MP_CAN_MAX_LEN, backing_buf); + } + // TODO: length check any memoryview 'result' that was passed in as arg? + // (may not be possible as I don't think memoryview records the size of its backing buffer) + + data_memoryview = MP_OBJ_TO_PTR(items[RECV_ARG_DATA]); // type of obj was checked already + + if ((flags & CAN_MSG_FLAG_RTR) == 0) { + memcpy(data_memoryview->items, data, dlen); + } else { + // Remote request, return a memoryview of 0s with correct length + memset(data_memoryview->items, 0, dlen); + } + data_memoryview->len = dlen; + + items[RECV_ARG_ID] = MP_OBJ_NEW_SMALL_INT(id); + items[RECV_ARG_FLAGS] = MP_OBJ_NEW_SMALL_INT(flags); + items[RECV_ARG_ERRORS] = MP_OBJ_NEW_SMALL_INT(errors); + + return MP_OBJ_FROM_PTR(result); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(machine_can_recv_obj, 1, 2, machine_can_recv); + +static mp_obj_t machine_can_set_filters(mp_obj_t self_in, mp_obj_t filters) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + machine_can_check_initialised(self); + mp_obj_iter_buf_t iter_buf; + int idx = 0; + #if CAN_FILTERS_STD_EXT_SEPARATE + int idx_ext = 0; + #endif + mp_obj_t filter; + + machine_can_port_clear_filters(self); + + if (filters == mp_const_none) { + // Passing 'None' is shortcut for "allow all" - pass standard IDs + // via filter 0 and extended IDs via filter 1. + machine_can_port_set_filter(self, 0, 0, 0, 0); + machine_can_port_set_filter(self, 1, 0, 0, CAN_MSG_FLAG_EXT_ID); + } else { + // Walk the iterable argument and call machine_can_port_set_filter() + // for each item + filters = mp_getiter(filters, &iter_buf); + while ((filter = mp_iternext(filters)) != MP_OBJ_STOP_ITERATION) { + mp_obj_t *args; + size_t arg_len; + mp_obj_get_array(filter, &arg_len, &args); + if (arg_len != 3) { + mp_raise_ValueError(MP_ERROR_TEXT("filter items must have length 3")); + } + mp_uint_t id = mp_obj_get_int(args[0]); + mp_uint_t mask = mp_obj_get_int(args[1]); + mp_uint_t flags = mp_obj_get_int(args[2]); + + id_range_check(id, flags); + id_range_check(mask, flags); + + // This function is expected to raise an exception if filter cannot be set + machine_can_port_set_filter(self, + #if CAN_FILTERS_STD_EXT_SEPARATE + (flags & CAN_MSG_FLAG_EXT_ID) ? idx_ext++ : idx++, + #else + idx++, + #endif + id, + mask, + flags + ); + } + } + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_2(machine_can_set_filters_obj, machine_can_set_filters); + +static mp_obj_t machine_can_state(mp_obj_t self_in) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (self->port == NULL) { + return MP_OBJ_NEW_SMALL_INT(MP_CAN_STATE_STOPPED); + } + return MP_OBJ_NEW_SMALL_INT(machine_can_port_get_state(self)); +} +static MP_DEFINE_CONST_FUN_OBJ_1(machine_can_state_obj, machine_can_state); + +static mp_obj_t machine_can_get_counters(size_t n_args, const mp_obj_t *args) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(args[0]); + machine_can_counters_t *counters = &self->counters; + mp_obj_list_t *list = mp_obj_list_optional_arg(n_args > 1 ? args[1] : mp_const_none, 8); + machine_can_check_initialised(self); + machine_can_port_update_counters(self); + + // Note: the members of 'counters' are laid out in the same order, + // so compiler should be able to infer some kind of loop here... + list->items[0] = MP_OBJ_NEW_SMALL_INT(counters->tec); + list->items[1] = MP_OBJ_NEW_SMALL_INT(counters->rec); + list->items[2] = MP_OBJ_NEW_SMALL_INT(counters->num_warning); + list->items[3] = MP_OBJ_NEW_SMALL_INT(counters->num_passive); + list->items[4] = MP_OBJ_NEW_SMALL_INT(counters->num_bus_off); + list->items[5] = MP_OBJ_NEW_SMALL_INT(counters->tx_pending); + list->items[6] = MP_OBJ_NEW_SMALL_INT(counters->rx_pending); + list->items[7] = MP_OBJ_NEW_SMALL_INT(counters->rx_overruns); + + return MP_OBJ_FROM_PTR(list); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(machine_can_get_counters_obj, 1, 2, machine_can_get_counters); + +static mp_obj_t machine_can_get_timings(size_t n_args, const mp_obj_t *args) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(args[0]); + mp_obj_list_t *list = mp_obj_list_optional_arg(n_args > 1 ? args[1] : mp_const_none, 6); + + list->items[0] = MP_OBJ_NEW_SMALL_INT(machine_can_get_actual_bitrate(self)); + list->items[1] = MP_OBJ_NEW_SMALL_INT(self->sjw); + list->items[2] = MP_OBJ_NEW_SMALL_INT(self->tseg1); + list->items[3] = MP_OBJ_NEW_SMALL_INT(self->tseg2); + #if MICROPY_HW_ENABLE_FDCAN + mp_obj_list_t *fd_list = mp_obj_list_optional_arg(list->items[4], 4); + fd_list->items[0] = mp_const_none; // TODO: CAN-FD timings support + fd_list->items[1] = mp_const_none; + fd_list->items[2] = mp_const_none; + fd_list->items[3] = mp_const_none; + list->items[4] = MP_OBJ_FROM_PTR(fd_list); + #else + list->items[4] = mp_const_none; + #endif + list->items[5] = machine_can_port_get_additional_timings(self, n_args > 1 ? list->items[5] : mp_const_none); + + return MP_OBJ_FROM_PTR(list); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(machine_can_get_timings_obj, 1, 2, machine_can_get_timings); + + +static mp_obj_t machine_can_restart(mp_obj_t self_in) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + machine_can_check_initialised(self); + machine_can_port_restart(self); + memset(&self->counters, 0, sizeof(self->counters)); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(machine_can_restart_obj, machine_can_restart); + +#if !CAN_PORT_PRINT_FUNCTION +static void machine_can_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + // We don't store the bitrate argument, instead print the real achieved bitrate + int f_clock = machine_can_port_f_clock(self); + int actual_bitrate = machine_can_get_actual_bitrate(self); + + qstr mode; + switch (self->mode) { + case MP_CAN_MODE_NORMAL: + mode = MP_QSTR_MODE_NORMAL; + break; + case MP_CAN_MODE_SLEEP: + mode = MP_QSTR_MODE_SLEEP; + break; + case MP_CAN_MODE_LOOPBACK: + mode = MP_QSTR_MODE_LOOPBACK; + break; + case MP_CAN_MODE_SILENT: + mode = MP_QSTR_MODE_SILENT; + break; + case MP_CAN_MODE_SILENT_LOOPBACK: + default: + mode = MP_QSTR_MODE_SILENT_LOOPBACK; + break; + } + + mp_printf(print, "CAN(%d, bitrate=%u, mode=CAN.%q, sjw=%u, tseg1=%u, tseg2=%u, f_clock=%u)", + self->can_idx + 1, + actual_bitrate, + mode, + self->sjw, + self->tseg1, + self->tseg2, + f_clock); +} +#endif + +// CAN.irq(handler, trigger, hard) +static mp_obj_t machine_can_irq(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + mp_arg_val_t args[MP_IRQ_ARG_INIT_NUM_ARGS]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_IRQ_ARG_INIT_NUM_ARGS, mp_irq_init_args, args); + machine_can_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + bool any_args = n_args > 1 || kw_args->used != 0; + return MP_OBJ_FROM_PTR(mp_machine_can_irq(self, any_args, args)); +} +static MP_DEFINE_CONST_FUN_OBJ_KW(machine_can_irq_obj, 1, machine_can_irq); + +static const mp_rom_map_elem_t machine_can_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&machine_can_init_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&machine_can_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR_irq), MP_ROM_PTR(&machine_can_irq_obj) }, + { MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&machine_can_send_obj) }, + { MP_ROM_QSTR(MP_QSTR_cancel_send), MP_ROM_PTR(&machine_can_cancel_send_obj) }, + { MP_ROM_QSTR(MP_QSTR_recv), MP_ROM_PTR(&machine_can_recv_obj) }, + { MP_ROM_QSTR(MP_QSTR_set_filters), MP_ROM_PTR(&machine_can_set_filters_obj) }, + { MP_ROM_QSTR(MP_QSTR_state), MP_ROM_PTR(&machine_can_state_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_counters), MP_ROM_PTR(&machine_can_get_counters_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_timings), MP_ROM_PTR(&machine_can_get_timings_obj) }, + { MP_ROM_QSTR(MP_QSTR_restart), MP_ROM_PTR(&machine_can_restart_obj) }, + + // Mode enum constants + { MP_ROM_QSTR(MP_QSTR_MODE_NORMAL), MP_ROM_INT(MP_CAN_MODE_NORMAL) }, + { MP_ROM_QSTR(MP_QSTR_MODE_SLEEP), MP_ROM_INT(MP_CAN_MODE_SLEEP) }, + { MP_ROM_QSTR(MP_QSTR_MODE_LOOPBACK), MP_ROM_INT(MP_CAN_MODE_LOOPBACK) }, + { MP_ROM_QSTR(MP_QSTR_MODE_SILENT), MP_ROM_INT(MP_CAN_MODE_SILENT) }, + { MP_ROM_QSTR(MP_QSTR_MODE_SILENT_LOOPBACK), MP_ROM_INT(MP_CAN_MODE_SILENT_LOOPBACK) }, + + // State enum constants + { MP_ROM_QSTR(MP_QSTR_STATE_STOPPED), MP_ROM_INT(MP_CAN_STATE_STOPPED) }, + { MP_ROM_QSTR(MP_QSTR_STATE_ACTIVE), MP_ROM_INT(MP_CAN_STATE_ACTIVE) }, + { MP_ROM_QSTR(MP_QSTR_STATE_WARNING), MP_ROM_INT(MP_CAN_STATE_WARNING) }, + { MP_ROM_QSTR(MP_QSTR_STATE_PASSIVE), MP_ROM_INT(MP_CAN_STATE_PASSIVE) }, + { MP_ROM_QSTR(MP_QSTR_STATE_BUS_OFF), MP_ROM_INT(MP_CAN_STATE_BUS_OFF) }, + + // Message Flag enum constants + { MP_ROM_QSTR(MP_QSTR_FLAG_RTR), MP_ROM_INT(CAN_MSG_FLAG_RTR) }, + { MP_ROM_QSTR(MP_QSTR_FLAG_EXT_ID), MP_ROM_INT(CAN_MSG_FLAG_EXT_ID) }, + { MP_ROM_QSTR(MP_QSTR_FLAG_UNORDERED), MP_ROM_INT(CAN_MSG_FLAG_UNORDERED) }, + + // Receive Error Flag enum constants + { MP_ROM_QSTR(MP_QSTR_RECV_ERR_FULL), MP_ROM_INT(CAN_RECV_ERR_FULL) }, + { MP_ROM_QSTR(MP_QSTR_RECV_ERR_OVERRUN), MP_ROM_INT(CAN_RECV_ERR_OVERRUN) }, + + // IRQ enum constants + { MP_ROM_QSTR(MP_QSTR_IRQ_RX), MP_ROM_INT(MP_CAN_IRQ_RX) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_TX), MP_ROM_INT(MP_CAN_IRQ_TX) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_STATE), MP_ROM_INT(MP_CAN_IRQ_STATE) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_TX_FAILED), MP_ROM_INT(MP_CAN_IRQ_TX_FAILED) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_TX_IDX_SHIFT), MP_ROM_INT(MP_CAN_IRQ_IDX_SHIFT) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_TX_IDX_MASK), MP_ROM_INT(MP_CAN_IRQ_IDX_MASK) }, + + // Other constants + { MP_ROM_QSTR(MP_QSTR_TX_QUEUE_LEN), MP_ROM_INT(CAN_TX_QUEUE_LEN) }, + { MP_ROM_QSTR(MP_QSTR_FILTERS_MAX), MP_ROM_INT(CAN_HW_MAX_FILTER) }, +}; +static MP_DEFINE_CONST_DICT(machine_can_locals_dict, machine_can_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + machine_can_type, + MP_QSTR_CAN, + 0, + make_new, machine_can_make_new, + print, machine_can_print, + locals_dict, &machine_can_locals_dict + ); + +MP_REGISTER_ROOT_POINTER(struct _machine_can_obj_t *machine_can_objs[MICROPY_HW_NUM_CAN]); + +#endif // MICROPY_PY_MACHINE_CAN diff --git a/extmod/machine_can.h b/extmod/machine_can.h new file mode 100644 index 0000000000000..6f2d310367083 --- /dev/null +++ b/extmod/machine_can.h @@ -0,0 +1,41 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2024-2026 Angus Gratton + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef MICROPY_INCLUDED_EXTMOD_MACHINE_CAN_H +#define MICROPY_INCLUDED_EXTMOD_MACHINE_CAN_H + +#include "py/obj.h" + +// machine.CAN support APIs that are called from port-level C code + +// Return the 0-based index of the CAN peripheral based on the name or the +// (1-based) number. +// +// Raises an exception if the identifier is invalid, doesn't exist, or is reserved. +mp_uint_t machine_can_get_index(mp_obj_t identifier); + +void machine_can_deinit_all(void); + +#endif // MICROPY_INCLUDED_EXTMOD_MACHINE_CAN_H diff --git a/extmod/machine_can_port.h b/extmod/machine_can_port.h new file mode 100644 index 0000000000000..58eaca0f5c8d2 --- /dev/null +++ b/extmod/machine_can_port.h @@ -0,0 +1,188 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2024-2026 Angus Gratton + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef MICROPY_INCLUDED_EXTMOD_MACHINE_CAN_PORT_H +#define MICROPY_INCLUDED_EXTMOD_MACHINE_CAN_PORT_H + +#include "py/obj.h" +#include "py/objarray.h" +#include "py/runtime.h" +#include "shared/runtime/mpirq.h" + +// This header is included into both extmod/machine_can.c and port-specific +// machine_can.c implementations and provides shared (static) function +// declarations to both. +// +// In a MicroPython build including this header from port-specific machine_can.c +// include is a no-op (as the file is included directly into +// extmod/machine_can.c). However, including it anyway means that Language +// Servers and IDEs can correctly analyse the machine_can.c file while the +// developer is writing it. + +typedef enum { + MP_CAN_STATE_STOPPED, + MP_CAN_STATE_ACTIVE, + MP_CAN_STATE_WARNING, + MP_CAN_STATE_PASSIVE, + MP_CAN_STATE_BUS_OFF, +} machine_can_state_t; + +typedef enum { + MP_CAN_MODE_NORMAL, + MP_CAN_MODE_SLEEP, + MP_CAN_MODE_LOOPBACK, + MP_CAN_MODE_SILENT, + MP_CAN_MODE_SILENT_LOOPBACK, + MP_CAN_MODE_MAX, +} machine_can_mode_t; + +// CAN IRQ Flags +// (currently the same for all ports) +#define MP_CAN_IRQ_TX (1 << 0) +#define MP_CAN_IRQ_RX (1 << 1) +#define MP_CAN_IRQ_TX_FAILED (1 << 2) +#define MP_CAN_IRQ_STATE (1 << 3) + +// Transmit buffer index is encoded into the irq().flags() response for MP_CAN_IRQ_TX +#define MP_CAN_IRQ_IDX_SHIFT 16 +#define MP_CAN_IRQ_IDX_MASK 0xFF + +#if MICROPY_HW_ENABLE_FDCAN +#define MP_CAN_MAX_LEN 64 +#else +#define MP_CAN_MAX_LEN 8 +#endif + +struct machine_can_port; + +// These values appear in the same order as the result of CAN.get_counters() +typedef struct { + mp_uint_t tec; + mp_uint_t rec; + mp_uint_t num_warning; // Number of "Error Warning" transitions + mp_uint_t num_passive; // Number of "Error Passive" transitions + mp_uint_t num_bus_off; // Number of "Bus-Off" transitions + mp_uint_t tx_pending; + mp_uint_t rx_pending; + mp_uint_t rx_overruns; +} machine_can_counters_t; + +typedef struct _machine_can_obj_t { + mp_obj_base_t base; + mp_uint_t can_idx; + + // Timing register settings + byte tseg1; + byte tseg2; + byte brp; + byte sjw; + + machine_can_mode_t mode; + + mp_irq_obj_t *mp_irq_obj; + uint16_t mp_irq_trigger; + mp_uint_t rx_error_flags; + + // Assumed some of these counters are updated from different port ISRs, etc. and some + // are updated by calling machine_can_port_update_counters() + machine_can_counters_t counters; + + struct machine_can_port *port; +} machine_can_obj_t; + +// Indexes for recv result list +typedef enum { + RECV_ARG_ID, + RECV_ARG_DATA, + RECV_ARG_FLAGS, + RECV_ARG_ERRORS, + RECV_ARG_LEN, // Overall length, not an index +} recv_arg_idx_t; + +#define CAN_STD_ID_MASK 0x7FF +#define CAN_EXT_ID_MASK 0x1fffffff + +// CAN Message Flags +#define CAN_MSG_FLAG_RTR (1 << 0) +#define CAN_MSG_FLAG_EXT_ID (1 << 1) +#define CAN_MSG_FLAG_FD_F (1 << 2) +#define CAN_MSG_FLAG_BRS (1 << 3) +#define CAN_MSG_FLAG_UNORDERED (1 << 4) + +// CAN recv() Error Flags +#define CAN_RECV_ERR_FULL (1 << 0) +#define CAN_RECV_ERR_OVERRUN (1 << 1) +#define CAN_RECV_ERR_ESI (1 << 2) + +// The port must provide implementations of these low-level CAN functions +static int machine_can_port_f_clock(const machine_can_obj_t *self); + +static bool machine_can_port_supports_mode(const machine_can_obj_t *self, machine_can_mode_t mode); + +static void machine_can_port_clear_filters(machine_can_obj_t *self); + +static mp_uint_t machine_can_port_max_data_len(mp_uint_t flags); + +// The extmod layer calls this function in a loop with incrementing filter_idx +// values. It's up to the port how to apply the filters from here, and to raise +// an exception if there are too many. +// +// If the CAN_FILTERS_STD_EXT_SEPARATE flag is set to 1, filter_idx will +// enumerate standard id filters separately to extended id filters (the +// CAN_MSG_FLAG_EXT_ID bit in 'flags' differentiates the type). +static void machine_can_port_set_filter(machine_can_obj_t *self, int filter_idx, mp_uint_t can_id, mp_uint_t mask, mp_uint_t flags); + +// Update interrupt configuration based on the new contents of 'self' +static void machine_can_update_irqs(machine_can_obj_t *self); + +// Return the irq().flags() result. Calling this function may also update the hardware state machine. +static mp_uint_t machine_can_port_irq_flags(machine_can_obj_t *self); + +static void machine_can_port_init(machine_can_obj_t *self); + +static void machine_can_port_deinit(machine_can_obj_t *self); + +static mp_int_t machine_can_port_send(machine_can_obj_t *self, mp_uint_t id, const byte *data, size_t data_len, mp_uint_t flags); + +static bool machine_can_port_cancel_send(machine_can_obj_t *self, mp_uint_t idx); + +static bool machine_can_port_recv(machine_can_obj_t *self, void *data, size_t *dlen, mp_uint_t *id, mp_uint_t *flags, mp_uint_t *errors); + +static machine_can_state_t machine_can_port_get_state(machine_can_obj_t *self); + +static void machine_can_port_restart(machine_can_obj_t *self); + +// Updates values in self->counters (which counters are updated by this function versus from ISRs and the like +// is port specific +static void machine_can_port_update_counters(machine_can_obj_t *self); + +// Hook for port to fill in the final item of the get_timings() result list with controller-specific values +static mp_obj_t machine_can_port_get_additional_timings(machine_can_obj_t *self, mp_obj_t optional_arg); + +// This function is only optionally defined by the port. If macro CAN_PORT_PRINT_FUNCTION is not set +// then a default machine_can_print function will be used. +static void machine_can_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind); + +#endif // MICROPY_INCLUDED_EXTMOD_MACHINE_CAN_PORT_H diff --git a/extmod/modmachine.c b/extmod/modmachine.c index 28b60683b1e58..60107a9020cd7 100644 --- a/extmod/modmachine.c +++ b/extmod/modmachine.c @@ -213,6 +213,9 @@ static const mp_rom_map_elem_t machine_module_globals_table[] = { #if MICROPY_PY_MACHINE_ADC_BLOCK { MP_ROM_QSTR(MP_QSTR_ADCBlock), MP_ROM_PTR(&machine_adc_block_type) }, #endif + #if MICROPY_PY_MACHINE_CAN + { MP_ROM_QSTR(MP_QSTR_CAN), MP_ROM_PTR(&machine_can_type) }, + #endif #if MICROPY_PY_MACHINE_DAC { MP_ROM_QSTR(MP_QSTR_DAC), MP_ROM_PTR(&machine_dac_type) }, #endif diff --git a/extmod/modmachine.h b/extmod/modmachine.h index 53660a7b7ab64..18ee229dc79bd 100644 --- a/extmod/modmachine.h +++ b/extmod/modmachine.h @@ -203,6 +203,7 @@ extern const machine_mem_obj_t machine_mem32_obj; // is provided by a port. extern const mp_obj_type_t machine_adc_type; extern const mp_obj_type_t machine_adc_block_type; +extern const mp_obj_type_t machine_can_type; extern const mp_obj_type_t machine_i2c_type; extern const mp_obj_type_t machine_i2c_target_type; extern const mp_obj_type_t machine_i2s_type; From 6f835b365a9040cd541f6dadbafe04c9a37eb462 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 18 Feb 2026 15:56:13 +1100 Subject: [PATCH 14/53] stm32: Implement index-aware STM32G4 FDCAN HAL TX functions. These are oddly missing from the STM32G4 HAL, but the reference manual describes being able to use them and the implementations seem to work as expected. Note that unlike STM32H7 it doesn't seem like we must use this approach, because HAL_FDCAN_AddMessageToTxFifoQ() does seem to not have the issues with priority inversion seen on the H7. However it's simpler to use the same API for both... Signed-off-by: Angus Gratton Signed-off-by: Angus Gratton --- ports/stm32/fdcan.c | 150 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/ports/stm32/fdcan.c b/ports/stm32/fdcan.c index 072cfe3dc4a4d..ae54273e1510a 100644 --- a/ports/stm32/fdcan.c +++ b/ports/stm32/fdcan.c @@ -65,6 +65,12 @@ // as (SRAMCAN_BASE + FDCAN_MESSAGE_RAM_SIZE - 0x4U) limits the usable number of words to 2559 words. #define FDCAN_MESSAGE_RAM_SIZE (2560 - 1) +#if defined(STM32G4) +// These HAL APIs are not implemented for STM32G4, so we implement them here... +static HAL_StatusTypeDef HAL_FDCAN_AddMessageToTxBuffer(FDCAN_HandleTypeDef *hfdcan, FDCAN_TxHeaderTypeDef *pTxHeader, uint8_t *pTxData, uint32_t BufferIndex); +static HAL_StatusTypeDef HAL_FDCAN_EnableTxBufferRequest(FDCAN_HandleTypeDef *hfdcan, uint32_t BufferIndex); +#endif // STM32G4 + // also defined in _hal_fdcan.c, but not able to declare extern and reach the variable const uint8_t DLCtoBytes[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64}; @@ -461,4 +467,148 @@ void FDCAN2_IT1_IRQHandler(void) { } #endif +#if defined(STM32G4) +// These implementations are copied from stm32h7xx_hal_fdcan.c with modifications for different G4 registers & code formatting + +// *FORMAT-OFF* +// ^^^ Keep original STM HAL code style for easier comparison + +static void FDCAN_CopyMessageToRAM(FDCAN_HandleTypeDef *hfdcan, FDCAN_TxHeaderTypeDef *pTxHeader, uint8_t *pTxData, uint32_t BufferIndex); + +static HAL_StatusTypeDef HAL_FDCAN_AddMessageToTxBuffer(FDCAN_HandleTypeDef *hfdcan, FDCAN_TxHeaderTypeDef *pTxHeader, uint8_t *pTxData, uint32_t BufferIndex) +{ + HAL_FDCAN_StateTypeDef state = hfdcan->State; + + /* Check function parameters */ + assert_param(IS_FDCAN_ID_TYPE(pTxHeader->IdType)); + if (pTxHeader->IdType == FDCAN_STANDARD_ID) + { + assert_param(IS_FDCAN_MAX_VALUE(pTxHeader->Identifier, 0x7FFU)); + } + else /* pTxHeader->IdType == FDCAN_EXTENDED_ID */ + { + assert_param(IS_FDCAN_MAX_VALUE(pTxHeader->Identifier, 0x1FFFFFFFU)); + } + assert_param(IS_FDCAN_FRAME_TYPE(pTxHeader->TxFrameType)); + assert_param(IS_FDCAN_DLC(pTxHeader->DataLength)); + assert_param(IS_FDCAN_ESI(pTxHeader->ErrorStateIndicator)); + assert_param(IS_FDCAN_BRS(pTxHeader->BitRateSwitch)); + assert_param(IS_FDCAN_FDF(pTxHeader->FDFormat)); + assert_param(IS_FDCAN_EFC(pTxHeader->TxEventFifoControl)); + assert_param(IS_FDCAN_MAX_VALUE(pTxHeader->MessageMarker, 0xFFU)); + assert_param(IS_FDCAN_TX_LOCATION(BufferIndex)); + + if ((state == HAL_FDCAN_STATE_READY) || (state == HAL_FDCAN_STATE_BUSY)) + { + /* Check that the selected buffer has an allocated area into the RAM */ + if (POSITION_VAL(BufferIndex) >= CAN_TX_QUEUE_LEN) // Note: Modified for G4 here + { + /* Update error code */ + hfdcan->ErrorCode |= HAL_FDCAN_ERROR_PARAM; + + return HAL_ERROR; + } + + /* Check that there is no transmission request pending for the selected buffer */ + if ((hfdcan->Instance->TXBRP & BufferIndex) != 0U) + { + /* Update error code */ + hfdcan->ErrorCode |= HAL_FDCAN_ERROR_PENDING; + + return HAL_ERROR; + } + else + { + /* Add the message to the Tx buffer */ + FDCAN_CopyMessageToRAM(hfdcan, pTxHeader, pTxData, POSITION_VAL(BufferIndex)); + } + + /* Return function status */ + return HAL_OK; + } + else + { + /* Update error code */ + hfdcan->ErrorCode |= HAL_FDCAN_ERROR_NOT_INITIALIZED; + + return HAL_ERROR; + } +} + +static HAL_StatusTypeDef HAL_FDCAN_EnableTxBufferRequest(FDCAN_HandleTypeDef *hfdcan, uint32_t BufferIndex) +{ + if (hfdcan->State == HAL_FDCAN_STATE_BUSY) + { + /* Add transmission request */ + hfdcan->Instance->TXBAR = BufferIndex; + + /* Return function status */ + return HAL_OK; + } + else + { + /* Update error code */ + hfdcan->ErrorCode |= HAL_FDCAN_ERROR_NOT_STARTED; + + return HAL_ERROR; + } +} + +#define SRAMCAN_TFQ_SIZE (18U * 4U) /* TX FIFO/Queue Elements Size in bytes */ + +// This function is copied 100% as-is from stm32g4xx_hal_fdcan.c, unfortunately +static void FDCAN_CopyMessageToRAM(FDCAN_HandleTypeDef *hfdcan, FDCAN_TxHeaderTypeDef *pTxHeader, uint8_t *pTxData, + uint32_t BufferIndex) +{ + uint32_t TxElementW1; + uint32_t TxElementW2; + uint32_t *TxAddress; + uint32_t ByteCounter; + + /* Build first word of Tx header element */ + if (pTxHeader->IdType == FDCAN_STANDARD_ID) + { + TxElementW1 = (pTxHeader->ErrorStateIndicator | + FDCAN_STANDARD_ID | + pTxHeader->TxFrameType | + (pTxHeader->Identifier << 18U)); + } + else /* pTxHeader->IdType == FDCAN_EXTENDED_ID */ + { + TxElementW1 = (pTxHeader->ErrorStateIndicator | + FDCAN_EXTENDED_ID | + pTxHeader->TxFrameType | + pTxHeader->Identifier); + } + + /* Build second word of Tx header element */ + TxElementW2 = ((pTxHeader->MessageMarker << 24U) | + pTxHeader->TxEventFifoControl | + pTxHeader->FDFormat | + pTxHeader->BitRateSwitch | + pTxHeader->DataLength); + + /* Calculate Tx element address */ + TxAddress = (uint32_t *)(hfdcan->msgRam.TxFIFOQSA + (BufferIndex * SRAMCAN_TFQ_SIZE)); + + /* Write Tx element header to the message RAM */ + *TxAddress = TxElementW1; + TxAddress++; + *TxAddress = TxElementW2; + TxAddress++; + + /* Write Tx payload to the message RAM */ + for (ByteCounter = 0; ByteCounter < DLCtoBytes[pTxHeader->DataLength >> 16U]; ByteCounter += 4U) + { + *TxAddress = (((uint32_t)pTxData[ByteCounter + 3U] << 24U) | + ((uint32_t)pTxData[ByteCounter + 2U] << 16U) | + ((uint32_t)pTxData[ByteCounter + 1U] << 8U) | + (uint32_t)pTxData[ByteCounter]); + TxAddress++; + } +} + +#endif // STM32G4 + +// *FORMAT-ON* #endif // MICROPY_HW_ENABLE_CAN && MICROPY_HW_ENABLE_FDCAN From 6cac2d275d7f8cad0da233014e0f042d01a940f1 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Mon, 15 Dec 2025 17:38:47 +1100 Subject: [PATCH 15/53] stm32: Add machine.CAN implementation. Implemented according to API docs in a parent comment. Adds new multi_extmod/machine_can_* tests which pass when testing between NUCLEO_G474RE, NUCLEO_H723ZG and PYBDV11. This work was mostly funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- ports/stm32/can.c | 372 ++++++++++--- ports/stm32/can.h | 92 +++- ports/stm32/fdcan.c | 304 ++++++++--- ports/stm32/machine_can.c | 489 ++++++++++++++++++ ports/stm32/main.c | 4 + ports/stm32/mpconfigport.h | 19 + ports/stm32/pyb_can.c | 182 ++----- ports/stm32/pyb_can.h | 2 - tests/extmod_hardware/machine_can2.py | 44 ++ tests/extmod_hardware/machine_can2.py.exp | 5 + tests/extmod_hardware/machine_can_timings.py | 60 +++ .../machine_can_01_rxtx_simple.py | 35 ++ .../machine_can_01_rxtx_simple.py.exp | 4 + .../machine_can_02_rx_callback.py | 122 +++++ .../machine_can_02_rx_callback.py.exp | 7 + .../multi_extmod/machine_can_03_rx_filters.py | 103 ++++ .../machine_can_03_rx_filters.py.exp | 49 ++ tests/multi_extmod/machine_can_04_tx_order.py | 170 ++++++ .../machine_can_04_tx_order.py.exp | 8 + .../machine_can_05_tx_prio_cancel.py | 118 +++++ .../machine_can_05_tx_prio_cancel.py.exp | 21 + .../multi_extmod/machine_can_06_remote_req.py | 70 +++ .../machine_can_06_remote_req.py.exp | 8 + .../machine_can_07_error_states.py | 205 ++++++++ .../machine_can_07_error_states.py.exp | 37 ++ .../multi_extmod/machine_can_08_init_mode.py | 102 ++++ .../machine_can_08_init_mode.py.exp | 14 + tests/ports/stm32/pyb_can.py | 4 +- tests/run-tests.py | 1 + tests/target_wiring/PYBx.py | 4 + tests/target_wiring/stm32.py | 7 + 31 files changed, 2367 insertions(+), 295 deletions(-) create mode 100644 ports/stm32/machine_can.c create mode 100644 tests/extmod_hardware/machine_can2.py create mode 100644 tests/extmod_hardware/machine_can2.py.exp create mode 100644 tests/extmod_hardware/machine_can_timings.py create mode 100644 tests/multi_extmod/machine_can_01_rxtx_simple.py create mode 100644 tests/multi_extmod/machine_can_01_rxtx_simple.py.exp create mode 100644 tests/multi_extmod/machine_can_02_rx_callback.py create mode 100644 tests/multi_extmod/machine_can_02_rx_callback.py.exp create mode 100644 tests/multi_extmod/machine_can_03_rx_filters.py create mode 100644 tests/multi_extmod/machine_can_03_rx_filters.py.exp create mode 100644 tests/multi_extmod/machine_can_04_tx_order.py create mode 100644 tests/multi_extmod/machine_can_04_tx_order.py.exp create mode 100644 tests/multi_extmod/machine_can_05_tx_prio_cancel.py create mode 100644 tests/multi_extmod/machine_can_05_tx_prio_cancel.py.exp create mode 100644 tests/multi_extmod/machine_can_06_remote_req.py create mode 100644 tests/multi_extmod/machine_can_06_remote_req.py.exp create mode 100644 tests/multi_extmod/machine_can_07_error_states.py create mode 100644 tests/multi_extmod/machine_can_07_error_states.py.exp create mode 100644 tests/multi_extmod/machine_can_08_init_mode.py create mode 100644 tests/multi_extmod/machine_can_08_init_mode.py.exp create mode 100644 tests/target_wiring/stm32.py diff --git a/ports/stm32/can.c b/ports/stm32/can.c index 5804f60cd2e64..fd03e895f46a3 100644 --- a/ports/stm32/can.c +++ b/ports/stm32/can.c @@ -35,9 +35,81 @@ #if !MICROPY_HW_ENABLE_FDCAN -bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t prescaler, uint32_t sjw, uint32_t bs1, uint32_t bs2, bool auto_restart) { +#if defined(MICROPY_HW_CAN3_TX) +#define NUM_CAN 3 +#elif defined(MICROPY_HW_CAN2_TX) +#define NUM_CAN 2 +#else +#define NUM_CAN 1 +#endif + +static int get_inst_index(CAN_HandleTypeDef *hcan) { + #if defined(MICROPY_HW_CAN1_TX) + if (hcan->Instance == CAN1) { + return 0; + } + #endif + #if defined(MICROPY_HW_CAN2_TX) + if (hcan->Instance == CAN2) { + return 1; + } + #endif + #if defined(MICROPY_HW_CAN3_TX) + if (hcan->Instance == CAN3) { + return 2; + } + #endif + assert(0); // Invalid hcan argument + return 0; +} + +static uint32_t get_tx_irqn(int can_id) { + switch (can_id) { + #if defined(MICROPY_HW_CAN1_TX) + case PYB_CAN_1: + return CAN1_TX_IRQn; + #endif + #if defined(MICROPY_HW_CAN2_TX) + case PYB_CAN_2: + return CAN2_TX_IRQn; + #endif + #if defined(MICROPY_HW_CAN3_TX) + case PYB_CAN_3: + return CAN3_TX_IRQn; + #endif + default: + return -1; + } +} + +int can_get_transmit_finished(CAN_HandleTypeDef *hcan, bool *is_success) { + CAN_TypeDef *instance = hcan->Instance; + uint32_t tsr = instance->TSR; + int result = -1; + + if (tsr & CAN_TSR_RQCP0) { + *is_success = tsr & CAN_TSR_TXOK0; + instance->TSR = CAN_TSR_RQCP0; // This also resets TXOK0, ALST0, TERR0 + result = 0; + } else if (tsr & CAN_TSR_RQCP1) { + *is_success = tsr & CAN_TSR_TXOK1; + instance->TSR = CAN_TSR_RQCP1; // This also resets TXOK1, ALST1, TERR1 + result = 1; + } else if (tsr & CAN_TSR_RQCP2) { + *is_success = tsr & CAN_TSR_TXOK2; + instance->TSR = CAN_TSR_RQCP2; // This also resets TXOK2, ALST2, TERR2 + result = 2; + } + + // Re-enable interrupts, to fire again if any transmit events outstanding + HAL_NVIC_EnableIRQ(get_tx_irqn(get_inst_index(hcan) + 1)); + + return result; +} + +bool can_init(CAN_HandleTypeDef *can, int can_id, can_tx_mode_t tx_mode, uint32_t mode, uint32_t prescaler, uint32_t sjw, uint32_t bs1, uint32_t bs2, bool auto_restart) { CAN_InitTypeDef *init = &can->Init; - init->Mode = mode << 4; // shift-left so modes fit in a small-int + init->Mode = mode; init->Prescaler = prescaler; init->SJW = ((sjw - 1) & 3) << 24; init->BS1 = ((bs1 - 1) & 0xf) << 16; @@ -49,8 +121,11 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca init->RFLM = DISABLE; init->TXFP = DISABLE; + (void)tx_mode; // This parameter is important for initialising FDCAN variant, but not bxCAN + CAN_TypeDef *CANx = NULL; - uint32_t sce_irq = 0; + uint32_t sce_irq; + uint32_t tx_irq = get_tx_irqn(can_id); const machine_pin_obj_t *pins[2]; switch (can_id) { @@ -100,13 +175,18 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca // init CANx can->Instance = CANx; - HAL_CAN_Init(can); + if (HAL_CAN_Init(can) != HAL_OK) { + return false; + } __HAL_CAN_ENABLE_IT(can, CAN_IT_ERR | CAN_IT_BOF | CAN_IT_EPV | CAN_IT_EWG); NVIC_SetPriority(sce_irq, IRQ_PRI_CAN); HAL_NVIC_EnableIRQ(sce_irq); + NVIC_SetPriority(tx_irq, IRQ_PRI_CAN); + HAL_NVIC_EnableIRQ(tx_irq); + return true; } @@ -140,6 +220,10 @@ void can_deinit(CAN_HandleTypeDef *can) { } } +uint32_t can_get_source_freq(void) { + return HAL_RCC_GetPCLK1Freq(); +} + void can_disable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { __HAL_CAN_DISABLE_IT(can, ((fifo == CAN_RX_FIFO0) ? (CAN_IT_FMP0 | CAN_IT_FF0 | CAN_IT_FOV0) : @@ -147,6 +231,23 @@ void can_disable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { } void can_enable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo, bool enable_msg_received) { + uint32_t irq = 0; + if (can->Instance == CAN1) { + irq = (fifo == CAN_RX_FIFO0) ? CAN1_RX0_IRQn : CAN1_RX1_IRQn; + } + #if defined(CAN2) + else if (can->Instance == CAN2) { + irq = (fifo == CAN_RX_FIFO0) ? CAN2_RX0_IRQn : CAN2_RX1_IRQn; + } + #endif + #if defined(CAN3) + else { + irq = (fifo == CAN_RX_FIFO0) ? CAN3_RX0_IRQn : CAN3_RX1_IRQn; + } + #endif + NVIC_SetPriority(irq, IRQ_PRI_CAN); + HAL_NVIC_EnableIRQ(irq); + __HAL_CAN_ENABLE_IT(can, ((fifo == CAN_RX_FIFO0) ? ((enable_msg_received ? CAN_IT_FMP0 : 0) | CAN_IT_FF0 | CAN_IT_FOV0) : ((enable_msg_received ? CAN_IT_FMP1 : 0) | CAN_IT_FF1 | CAN_IT_FOV1))); @@ -214,12 +315,46 @@ int can_receive(CAN_HandleTypeDef *can, can_rx_fifo_t fifo, CanRxMsgTypeDef *msg return 0; // success } -// Lightly modified version of HAL CAN_Transmit to handle Timeout=0 correctly +static HAL_StatusTypeDef can_transmit_common(CAN_HandleTypeDef *hcan, int index, CanTxMsgTypeDef *txmsg, const uint8_t *data) { + hcan->pTxMsg = txmsg; + (void)data; // Not needed here, caller has set it up as &tx_msg->Data + + // Set up the Id + hcan->Instance->sTxMailBox[index].TIR &= CAN_TI0R_TXRQ; + if (hcan->pTxMsg->IDE == CAN_ID_STD) { + assert_param(IS_CAN_STDID(hcan->pTxMsg->StdId)); + hcan->Instance->sTxMailBox[index].TIR |= ((hcan->pTxMsg->StdId << 21) | \ + hcan->pTxMsg->RTR); + } else { + assert_param(IS_CAN_EXTID(hcan->pTxMsg->ExtId)); + hcan->Instance->sTxMailBox[index].TIR |= ((hcan->pTxMsg->ExtId << 3) | \ + hcan->pTxMsg->IDE | \ + hcan->pTxMsg->RTR); + } + + // Set up the DLC + hcan->pTxMsg->DLC &= (uint8_t)0x0000000F; + hcan->Instance->sTxMailBox[index].TDTR &= (uint32_t)0xFFFFFFF0; + hcan->Instance->sTxMailBox[index].TDTR |= hcan->pTxMsg->DLC; + + // Set up the data field + hcan->Instance->sTxMailBox[index].TDLR = (((uint32_t)hcan->pTxMsg->Data[3] << 24) | + ((uint32_t)hcan->pTxMsg->Data[2] << 16) | + ((uint32_t)hcan->pTxMsg->Data[1] << 8) | + ((uint32_t)hcan->pTxMsg->Data[0])); + hcan->Instance->sTxMailBox[index].TDHR = (((uint32_t)hcan->pTxMsg->Data[7] << 24) | + ((uint32_t)hcan->pTxMsg->Data[6] << 16) | + ((uint32_t)hcan->pTxMsg->Data[5] << 8) | + ((uint32_t)hcan->pTxMsg->Data[4])); + + // Request transmit + hcan->Instance->sTxMailBox[index].TIR |= CAN_TI0R_TXRQ; + return HAL_OK; +} + HAL_StatusTypeDef can_transmit(CAN_HandleTypeDef *hcan, CanTxMsgTypeDef *txmsg, uint8_t *data, uint32_t Timeout) { uint32_t transmitmailbox; uint32_t tickstart; - uint32_t rqcpflag = 0; - uint32_t txokflag = 0; hcan->pTxMsg = txmsg; (void)data; // Not needed here, caller has set it up as &tx_msg->Data @@ -232,79 +367,86 @@ HAL_StatusTypeDef can_transmit(CAN_HandleTypeDef *hcan, CanTxMsgTypeDef *txmsg, // Select one empty transmit mailbox if ((hcan->Instance->TSR & CAN_TSR_TME0) == CAN_TSR_TME0) { transmitmailbox = CAN_TXMAILBOX_0; - rqcpflag = CAN_FLAG_RQCP0; - txokflag = CAN_FLAG_TXOK0; } else if ((hcan->Instance->TSR & CAN_TSR_TME1) == CAN_TSR_TME1) { transmitmailbox = CAN_TXMAILBOX_1; - rqcpflag = CAN_FLAG_RQCP1; - txokflag = CAN_FLAG_TXOK1; } else if ((hcan->Instance->TSR & CAN_TSR_TME2) == CAN_TSR_TME2) { transmitmailbox = CAN_TXMAILBOX_2; - rqcpflag = CAN_FLAG_RQCP2; - txokflag = CAN_FLAG_TXOK2; } else { - transmitmailbox = CAN_TXSTATUS_NOMAILBOX; - } - - if (transmitmailbox != CAN_TXSTATUS_NOMAILBOX) { - // Set up the Id - hcan->Instance->sTxMailBox[transmitmailbox].TIR &= CAN_TI0R_TXRQ; - if (hcan->pTxMsg->IDE == CAN_ID_STD) { - assert_param(IS_CAN_STDID(hcan->pTxMsg->StdId)); - hcan->Instance->sTxMailBox[transmitmailbox].TIR |= ((hcan->pTxMsg->StdId << 21) | \ - hcan->pTxMsg->RTR); - } else { - assert_param(IS_CAN_EXTID(hcan->pTxMsg->ExtId)); - hcan->Instance->sTxMailBox[transmitmailbox].TIR |= ((hcan->pTxMsg->ExtId << 3) | \ - hcan->pTxMsg->IDE | \ - hcan->pTxMsg->RTR); - } + return HAL_BUSY; + } - // Set up the DLC - hcan->pTxMsg->DLC &= (uint8_t)0x0000000F; - hcan->Instance->sTxMailBox[transmitmailbox].TDTR &= (uint32_t)0xFFFFFFF0; - hcan->Instance->sTxMailBox[transmitmailbox].TDTR |= hcan->pTxMsg->DLC; - - // Set up the data field - hcan->Instance->sTxMailBox[transmitmailbox].TDLR = (((uint32_t)hcan->pTxMsg->Data[3] << 24) | - ((uint32_t)hcan->pTxMsg->Data[2] << 16) | - ((uint32_t)hcan->pTxMsg->Data[1] << 8) | - ((uint32_t)hcan->pTxMsg->Data[0])); - hcan->Instance->sTxMailBox[transmitmailbox].TDHR = (((uint32_t)hcan->pTxMsg->Data[7] << 24) | - ((uint32_t)hcan->pTxMsg->Data[6] << 16) | - ((uint32_t)hcan->pTxMsg->Data[5] << 8) | - ((uint32_t)hcan->pTxMsg->Data[4])); - // Request transmission - hcan->Instance->sTxMailBox[transmitmailbox].TIR |= CAN_TI0R_TXRQ; - - if (Timeout == 0) { - return HAL_OK; - } + HAL_StatusTypeDef err = can_transmit_common(hcan, transmitmailbox, txmsg, data); + if (err != HAL_OK) { + return err; + } - // Get tick - tickstart = HAL_GetTick(); - // Check End of transmission flag - while (!(__HAL_CAN_TRANSMIT_STATUS(hcan, transmitmailbox))) { - // Check for the Timeout - if (Timeout != HAL_MAX_DELAY) { - if ((HAL_GetTick() - tickstart) > Timeout) { - // When the timeout expires, we try to abort the transmission of the packet - __HAL_CAN_CANCEL_TRANSMIT(hcan, transmitmailbox); - while (!__HAL_CAN_GET_FLAG(hcan, rqcpflag)) { - } - if (__HAL_CAN_GET_FLAG(hcan, txokflag)) { - // The abort attempt failed and the message was sent properly - return HAL_OK; - } else { - return HAL_TIMEOUT; - } + if (Timeout == 0) { + return HAL_OK; + } + + // Get tick + tickstart = HAL_GetTick(); + // Check End of transmission flag + while (!(__HAL_CAN_TRANSMIT_STATUS(hcan, transmitmailbox))) { + // Check for the Timeout + if (Timeout != HAL_MAX_DELAY) { + if ((HAL_GetTick() - tickstart) > Timeout) { + // When the timeout expires, we try to abort the transmission of the packet + bool was_transmitting = can_cancel_transmit(hcan, transmitmailbox); + // Note: there is a small race here where a message that transmits exactly as + // we call can_cancel_transmit() will still look like it failed + if (!was_transmitting) { + // The abort attempt failed and the message was sent properly + return HAL_OK; + } else { + return HAL_TIMEOUT; } } } - return HAL_OK; - } else { - return HAL_BUSY; } + return HAL_OK; +} + +HAL_StatusTypeDef can_transmit_buf_index(CAN_HandleTypeDef *hcan, int index, CanTxMsgTypeDef *txmsg, const uint8_t *data) { + __HAL_CAN_ENABLE_IT(hcan, CAN_IT_TME); + return can_transmit_common(hcan, index, txmsg, data); +} + + +bool can_cancel_transmit(CAN_HandleTypeDef *hcan, int index) { + uint32_t empty_flag, mailbox; + bool result = false; + switch (index) { + case 0: + empty_flag = CAN_FLAG_TME0; + mailbox = CAN_TXMAILBOX_0; + break; + case 1: + empty_flag = CAN_FLAG_TME1; + mailbox = CAN_TXMAILBOX_1; + break; + default: + empty_flag = CAN_FLAG_TME2; + mailbox = CAN_TXMAILBOX_2; + break; + } + if (__HAL_CAN_GET_FLAG(hcan, empty_flag) == 0) { + result = true; + __HAL_CAN_CANCEL_TRANSMIT(hcan, mailbox); + mp_uint_t start = mp_hal_ticks_us(); + while (__HAL_CAN_GET_FLAG(hcan, empty_flag) == 0) { + // we don't expect this to take longer than a few clock cycles, if + // it does then it probably indicates a bug in the driver. However, + // either way we don't want to end up stuck here + mp_uint_t elapsed = mp_hal_ticks_us() - start; + assert(elapsed < 2000); + if (elapsed >= 2000) { + break; + } + } + } + + return result; } can_state_t can_get_state(CAN_HandleTypeDef *can) { @@ -326,6 +468,29 @@ can_state_t can_get_state(CAN_HandleTypeDef *can) { } } +void can_get_counters(CAN_HandleTypeDef *can, can_counters_t *counters) { + CAN_TypeDef *inst = can->Instance; + uint32_t esr = inst->ESR; + counters->tec = esr >> CAN_ESR_TEC_Pos & 0xff; + counters->rec = esr >> CAN_ESR_REC_Pos & 0xff; + counters->tx_pending = 0x01121223 >> ((inst->TSR >> CAN_TSR_TME_Pos & 7) << 2) & 0xf; + counters->rx_fifo0_pending = inst->RF0R >> CAN_RF0R_FMP0_Pos & 3; + counters->rx_fifo1_pending = inst->RF1R >> CAN_RF1R_FMP1_Pos & 3; +} + +// Compatibility shim: call both the pyb.CAN and machine.CAN handlers if necessary, +// allow them to decide which is initialised. +static inline void call_can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo) { + #if MICROPY_PY_MACHINE_CAN + machine_can_irq_handler(can_id, interrupt); + #endif + if (interrupt != CAN_INT_TX_COMPLETE) { + pyb_can_irq_handler(can_id, interrupt, fifo); + } + // TODO: Need to do something to clear the transmit state if pyb.CAN is in use, I think + // (check usage of can_get_transmit_finished() from pyb CAN code) +} + // Workaround for the __HAL_CAN macros expecting a CAN_HandleTypeDef which we // don't have in the ISR. Using this "fake" struct instead of CAN_HandleTypeDef // so it's not possible to accidentally call an API that uses one of the other @@ -336,6 +501,7 @@ typedef struct { static void can_rx_irq_handler(uint can_id, CAN_TypeDef *instance, can_rx_fifo_t fifo) { uint32_t full_flag, full_int, overrun_flag, overrun_int, pending_int; + bool msg_received; const fake_handle_t handle = { .Instance = instance, @@ -347,12 +513,14 @@ static void can_rx_irq_handler(uint can_id, CAN_TypeDef *instance, can_rx_fifo_t overrun_flag = CAN_FLAG_FOV0; overrun_int = CAN_IT_FOV0; pending_int = CAN_IT_FMP0; + msg_received = __HAL_CAN_MSG_PENDING(&handle, CAN_FIFO0); } else { full_flag = CAN_FLAG_FF1; full_int = CAN_IT_FF1; overrun_flag = CAN_FLAG_FOV1; overrun_int = CAN_IT_FOV1; pending_int = CAN_IT_FMP1; + msg_received = __HAL_CAN_MSG_PENDING(&handle, CAN_FIFO1); } bool full = __HAL_CAN_GET_FLAG(&handle, full_flag); @@ -361,39 +529,67 @@ static void can_rx_irq_handler(uint can_id, CAN_TypeDef *instance, can_rx_fifo_t // Note: receive interrupt bits are disabled below, and re-enabled by the // higher layer after calling can_receive() + // Only leave msg_received set if the interrupt is enabled, + // otherwise an CAN_INT_MESSAGE_RECEIVED interrupt is already pending + // and this ISR is being called for another reason + msg_received = msg_received && (handle.Instance->IER & pending_int); + if (full) { __HAL_CAN_DISABLE_IT(&handle, full_int); __HAL_CAN_CLEAR_FLAG(&handle, full_flag); if (!overrun) { - can_irq_handler(can_id, CAN_INT_FIFO_FULL, fifo); + call_can_irq_handler(can_id, CAN_INT_FIFO_FULL, fifo); } } if (overrun) { __HAL_CAN_DISABLE_IT(&handle, overrun_int); __HAL_CAN_CLEAR_FLAG(&handle, overrun_flag); - can_irq_handler(can_id, CAN_INT_FIFO_OVERFLOW, fifo); + call_can_irq_handler(can_id, CAN_INT_FIFO_OVERFLOW, fifo); } - if (!(full || overrun)) { - // Process of elimination, if neither of the above - // FIFO status flags are set then message pending interrupt is what fired. + if (msg_received) { __HAL_CAN_DISABLE_IT(&handle, pending_int); - can_irq_handler(can_id, CAN_INT_MESSAGE_RECEIVED, fifo); + call_can_irq_handler(can_id, CAN_INT_MESSAGE_RECEIVED, fifo); } } -static void can_sce_irq_handler(uint can_id, CAN_TypeDef *instance) { +static void can_sce_irq_handler(int can_id, CAN_TypeDef *instance) { instance->MSR = CAN_MSR_ERRI; // Write to clear ERRIE interrupt uint32_t esr = instance->ESR; if (esr & CAN_ESR_BOFF) { - can_irq_handler(can_id, CAN_INT_ERR_BUS_OFF, 0); + call_can_irq_handler(can_id, CAN_INT_ERR_BUS_OFF, 0); } else if (esr & CAN_ESR_EPVF) { - can_irq_handler(can_id, CAN_INT_ERR_PASSIVE, 0); + call_can_irq_handler(can_id, CAN_INT_ERR_PASSIVE, 0); } else if (esr & CAN_ESR_EWGF) { - can_irq_handler(can_id, CAN_INT_ERR_WARNING, 0); + call_can_irq_handler(can_id, CAN_INT_ERR_WARNING, 0); + } +} + +void can_disable_tx_interrupts(CAN_HandleTypeDef *can) { + __HAL_CAN_DISABLE_IT(can, CAN_IT_TME); +} + +void can_restart(CAN_HandleTypeDef *can) { + CAN_TypeDef *instance = can->Instance; + // This sequence puts the hardware in and out of initialisation mode, + // which is the manual way to leave Bus-Off mode (see RM0090 CAN_MCR bit ABOM) + instance->MCR |= CAN_MCR_INRQ; + while ((instance->MSR & CAN_MSR_INAK) == 0) { + } + instance->MCR &= ~CAN_MCR_INRQ; + while ((instance->MSR & CAN_MSR_INAK)) { } } +static void can_tx_irq_handler(int can_id, CAN_TypeDef *instance) { + // Update mailbox tx state based on any RQCPx flags which are set, + // and then clear the RQCPx flags. + + // TX IRQ is re-enabled by higher layer + HAL_NVIC_DisableIRQ(get_tx_irqn(can_id)); + call_can_irq_handler(can_id, CAN_INT_TX_COMPLETE, 0); +} + #if defined(MICROPY_HW_CAN1_TX) void CAN1_RX0_IRQHandler(void) { IRQ_ENTER(CAN1_RX0_IRQn); @@ -412,6 +608,12 @@ void CAN1_SCE_IRQHandler(void) { can_sce_irq_handler(PYB_CAN_1, CAN1); IRQ_EXIT(CAN1_SCE_IRQn); } + +void CAN1_TX_IRQHandler(void) { + IRQ_ENTER(CAN1_TX_IRQn); + can_tx_irq_handler(PYB_CAN_1, CAN1); + IRQ_EXIT(CAN1_TX_IRQn); +} #endif #if defined(MICROPY_HW_CAN2_TX) @@ -432,6 +634,12 @@ void CAN2_SCE_IRQHandler(void) { can_sce_irq_handler(PYB_CAN_2, CAN2); IRQ_EXIT(CAN2_SCE_IRQn); } + +void CAN2_TX_IRQHandler(void) { + IRQ_ENTER(CAN2_TX_IRQn); + can_tx_irq_handler(PYB_CAN_2, CAN2); + IRQ_EXIT(CAN2_TX_IRQn); +} #endif #if defined(MICROPY_HW_CAN3_TX) @@ -452,6 +660,12 @@ void CAN3_SCE_IRQHandler(void) { can_sce_irq_handler(PYB_CAN_3, CAN3); IRQ_EXIT(CAN3_SCE_IRQn); } + +void CAN3_TX_IRQHandler(void) { + IRQ_ENTER(CAN3_TX_IRQn); + can_tx_irq_handler(PYB_CAN_3, CAN3); + IRQ_EXIT(CAN3_TX_IRQn); +} #endif #endif // !MICROPY_HW_ENABLE_FDCAN diff --git a/ports/stm32/can.h b/ports/stm32/can.h index f2601313cf21a..ff73e1a74d2b4 100644 --- a/ports/stm32/can.h +++ b/ports/stm32/can.h @@ -47,19 +47,48 @@ #define LIST32 (3) #if MICROPY_HW_ENABLE_FDCAN +// Interface compatibility for the classic CAN controller HAL #define CAN_TypeDef FDCAN_GlobalTypeDef #define CAN_HandleTypeDef FDCAN_HandleTypeDef #define CanTxMsgTypeDef FDCAN_TxHeaderTypeDef #define CanRxMsgTypeDef FDCAN_RxHeaderTypeDef + +#define CAN_MODE_NORMAL FDCAN_MODE_NORMAL +#define CAN_MODE_LOOPBACK FDCAN_MODE_EXTERNAL_LOOPBACK +#define CAN_MODE_SILENT FDCAN_MODE_BUS_MONITORING +#define CAN_MODE_SILENT_LOOPBACK FDCAN_MODE_INTERNAL_LOOPBACK + +// FDCAN peripheral has independent indexes for standard id vs extended id filters +#if defined(STM32G4) +#define CAN_HW_MAX_STD_FILTER 28 +#define CAN_HW_MAX_EXT_FILTER 8 +#elif defined(STM32H7) +// The RAM filtering section is configured for 64 x 1 word elements for 11-bit standard +// identifiers, and 31 x 2 words elements for 29-bit extended identifiers. +// The total number of words reserved for the filtering per FDCAN instance is 126 words. +#define CAN_HW_MAX_STD_FILTER 64 +#define CAN_HW_MAX_EXT_FILTER 31 #endif -enum { +// Value reported via machine.CAN.FILTER_MAX, somewhat optimistic as requires using +// the exact numbers of each type of filter. +#define CAN_HW_MAX_FILTER (CAN_HW_MAX_STD_FILTER + CAN_HW_MAX_EXT_FILTER) + +#else + +// CAN1 & CAN2 have 28 filters which can be arbitrarily split, but machine.CAN +// implementation hard-codes to 14/14. CAN3 has 14 independent filters. +#define CAN_HW_MAX_FILTER 14 + +#endif + +typedef enum { CAN_STATE_STOPPED, CAN_STATE_ERROR_ACTIVE, CAN_STATE_ERROR_WARNING, CAN_STATE_ERROR_PASSIVE, CAN_STATE_BUS_OFF, -}; +} can_state_t; typedef enum _rx_state_t { RX_STATE_FIFO_EMPTY = 0, @@ -76,8 +105,31 @@ typedef enum { CAN_INT_ERR_BUS_OFF, CAN_INT_ERR_PASSIVE, CAN_INT_ERR_WARNING, + + CAN_INT_TX_COMPLETE, } can_int_t; +typedef enum { + CAN_TX_FIFO, + CAN_TX_QUEUE, +} can_tx_mode_t; + +// Counter data as used by pyb.CAN.info() and machine.CAN.get_counters() +typedef struct { + unsigned tec; + unsigned rec; + unsigned tx_pending; + unsigned rx_fifo0_pending; + unsigned rx_fifo1_pending; +} can_counters_t; + +#if defined(STM32H7) +#define CAN_TX_QUEUE_LEN 16 +#else +// FDCAN STM32G4, bxCAN +#define CAN_TX_QUEUE_LEN 3 +#endif + // RX FIFO numbering // // Note: For traditional CAN peripheral, the values of CAN_FIFO0 and CAN_FIFO1 are the same @@ -87,12 +139,29 @@ typedef enum { CAN_RX_FIFO1, } can_rx_fifo_t; -bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t prescaler, uint32_t sjw, uint32_t bs1, uint32_t bs2, bool auto_restart); +bool can_init(CAN_HandleTypeDef *can, int can_id, can_tx_mode_t tx_mode, uint32_t mode, uint32_t prescaler, uint32_t sjw, uint32_t bs1, uint32_t bs2, bool auto_restart); void can_deinit(CAN_HandleTypeDef *can); +uint32_t can_get_source_freq(void); + int can_receive(CAN_HandleTypeDef *can, can_rx_fifo_t fifo, CanRxMsgTypeDef *msg, uint8_t *data, uint32_t timeout_ms); + +// Transmit a CAN frame (callee to choose the transmit slot). Used by pyb.CAN only, does not enable TX interrupt +// On FDCAN this function invalidates the 'txmsg' structure if successful. HAL_StatusTypeDef can_transmit(CAN_HandleTypeDef *hcan, CanTxMsgTypeDef *txmsg, uint8_t *data, uint32_t Timeout); +// Tell the controller to copy a CAN frame copied to 'index' and start transmitting +// On FDCAN this function invalidates the 'txmsg' structure if successful. +HAL_StatusTypeDef can_transmit_buf_index(CAN_HandleTypeDef *hcan, int index, CanTxMsgTypeDef *txmsg, const uint8_t *data); + +// Cancel the pending transmission in the specified buffer index. Returns after buffer stops transmitting. +// Result is true if buffer was transmitting, false if not transmitting (or finished transmitting before cancellation) +bool can_cancel_transmit(CAN_HandleTypeDef *hcan, int index); + +// Get the lowest index of a buffer in FAILED or SUCCEEDED state, or -1 if none exists +// Calling this function also re-enables the TX done IRQ for this peripheral +int can_get_transmit_finished(CAN_HandleTypeDef *hcan, bool *is_success); + // Disable all CAN receive interrupts for a FIFO void can_disable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo); @@ -101,14 +170,25 @@ void can_disable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo); // Interrupt for CAN_INT_MESSAGE_RECEIVED is only enabled if enable_msg_received is set. void can_enable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo, bool enable_msg_received); +// Disable all pending TX interrupts (ahead of restart or deinit). Will re-enable n next transmit +void can_disable_tx_interrupts(CAN_HandleTypeDef *can); + can_state_t can_get_state(CAN_HandleTypeDef *can); +void can_get_counters(CAN_HandleTypeDef *can, can_counters_t *counters); + +// Restart controller (clears error states). Caller expected to check controller initialised already. +void can_restart(CAN_HandleTypeDef *can); + // Implemented in pyb_can.c, called from lower layer -extern void can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo); +extern void pyb_can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo); + +// Implemented in machine_can.c, called from lower layer +extern void machine_can_irq_handler(uint can_id, can_int_t interrupt); #if MICROPY_HW_ENABLE_FDCAN -static inline unsigned can_rx_pending(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { +static inline unsigned can_is_rx_pending(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { return HAL_FDCAN_GetRxFifoFillLevel(can, fifo == CAN_RX_FIFO0 ? FDCAN_RX_FIFO0 : FDCAN_RX_FIFO1); } @@ -116,7 +196,7 @@ void can_clearfilter(CAN_HandleTypeDef *can, uint32_t filter_num, bool is_extid) #else -static inline unsigned can_rx_pending(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { +static inline unsigned can_is_rx_pending(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { return __HAL_CAN_MSG_PENDING(can, fifo == CAN_RX_FIFO0 ? CAN_FIFO0 : CAN_FIFO1); } diff --git a/ports/stm32/fdcan.c b/ports/stm32/fdcan.c index ae54273e1510a..a7fcf7330b19c 100644 --- a/ports/stm32/fdcan.c +++ b/ports/stm32/fdcan.c @@ -59,11 +59,11 @@ #define FDCAN_IT_GROUP_BIT_LINE_ERROR (FDCAN_ILS_EPE | FDCAN_ILS_ELOE) #define FDCAN_IT_GROUP_PROTOCOL_ERROR (FDCAN_ILS_ARAE | FDCAN_ILS_PEDE | FDCAN_ILS_PEAE | FDCAN_ILS_WDIE | FDCAN_ILS_BOE | FDCAN_ILS_EWE) #define FDCAN_IT_GROUP_RX_FIFO1 (FDCAN_ILS_RF1NL | FDCAN_ILS_RF1FL | FDCAN_ILS_RF1LL) -#endif // The dedicated Message RAM should be 2560 words, but the way it's defined in stm32h7xx_hal_fdcan.c // as (SRAMCAN_BASE + FDCAN_MESSAGE_RAM_SIZE - 0x4U) limits the usable number of words to 2559 words. #define FDCAN_MESSAGE_RAM_SIZE (2560 - 1) +#endif // STM32H7 #if defined(STM32G4) // These HAL APIs are not implemented for STM32G4, so we implement them here... @@ -72,10 +72,55 @@ static HAL_StatusTypeDef HAL_FDCAN_EnableTxBufferRequest(FDCAN_HandleTypeDef *hf #endif // STM32G4 // also defined in _hal_fdcan.c, but not able to declare extern and reach the variable -const uint8_t DLCtoBytes[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64}; +static const uint8_t DLCtoBytes[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64}; + +#if defined(MICROPY_HW_CAN3_TX) +#error "Support is not yet added for FDCAN CAN3" +#elif defined(MICROPY_HW_CAN2_TX) +#define NUM_CAN 2 +#else +#define NUM_CAN 1 +#endif + +int can_get_transmit_finished(CAN_HandleTypeDef *hcan, bool *is_success) { + // Note: No HAL API available for the below regs, unless we use the HAL's IRQ handler + FDCAN_GlobalTypeDef *instance = hcan->Instance; + + uint32_t enabled_tx = instance->TXBTIE; // Which buffers have TX ints enabled? + uint32_t tx_success = instance->TXBTO & enabled_tx; // Which TX buffers succeeded? + uint32_t tx_cancel = instance->TXBCF & enabled_tx; // Which TX buffers cancelled? + int result = -1; + + for (int i = 0; i < CAN_TX_QUEUE_LEN; i++) { + if (tx_success & (1U << i)) { + *is_success = true; + result = i; + break; + } + if (tx_cancel & (1U << i)) { + *is_success = false; + result = i; + break; + } + } + + if (result != -1) { + // Clear the TX interrupts for this buffer, will re-enable + // when next sending + instance->TXBTIE &= ~(1U << result); + instance->TXBCIE &= ~(1U << result); + } -bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t prescaler, uint32_t sjw, uint32_t bs1, uint32_t bs2, bool auto_restart) { - (void)auto_restart; + // Re-enable transmit interrupts + instance->IE |= (FDCAN_IT_TX_COMPLETE | FDCAN_IT_TX_ABORT_COMPLETE); + + return result; +} + +bool can_init(CAN_HandleTypeDef *can, int can_id, can_tx_mode_t tx_mode, uint32_t mode, uint32_t prescaler, uint32_t sjw, uint32_t bs1, uint32_t bs2, bool auto_restart) { + (void)auto_restart; // FDCAN peripheral doesn't support automatic exit of Bus-Off + + uint32_t fifo_queue_mode = (tx_mode == CAN_TX_FIFO) ? FDCAN_TX_FIFO_OPERATION : FDCAN_TX_QUEUE_OPERATION; FDCAN_InitTypeDef *init = &can->Init; // Configure FDCAN with FD frame and BRS support. @@ -91,15 +136,16 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca init->TransmitPause = DISABLE; init->ProtocolException = ENABLE; + init->StdFiltersNbr = CAN_HW_MAX_STD_FILTER; + init->ExtFiltersNbr = CAN_HW_MAX_EXT_FILTER; + #if defined(STM32G4) init->ClockDivider = FDCAN_CLOCK_DIV1; init->DataPrescaler = 1; init->DataSyncJumpWidth = 1; init->DataTimeSeg1 = 1; init->DataTimeSeg2 = 1; - init->StdFiltersNbr = 28; - init->ExtFiltersNbr = 8; - init->TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION; + init->TxFifoQueueMode = fifo_queue_mode; #elif defined(STM32H7) // The dedicated FDCAN RAM is 2560 32-bit words and shared between the FDCAN instances. // To support 2 FDCAN instances simultaneously, the Message RAM is divided in half by @@ -114,26 +160,24 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca // data field and the specific transmission or reception bits field for control. // The following code configures the different Message RAM sections per FDCAN instance. - // The RAM filtering section is configured for 64 x 1 word elements for 11-bit standard - // identifiers, and 31 x 2 words elements for 29-bit extended identifiers. - // The total number of words reserved for the filtering per FDCAN instance is 126 words. - init->StdFiltersNbr = 64; - init->ExtFiltersNbr = 31; - // The Tx event FIFO is used to store the message ID and the timestamp of successfully // transmitted elements. The Tx event FIFO can store a maximum of 32 (2 words) elements. // NOTE: Events are stored in Tx event FIFO only if tx_msg.TxEventFifoControl is enabled. init->TxEventsNbr = 0; - // Transmission section is configured in FIFO mode operation, with no dedicated Tx buffers. - // The Tx FIFO can store a maximum of 32 elements (or 576 words), each element is 18 words + // The Tx FIFO or Queue can store a maximum of 32 elements (or 576 words), each element is 18 words // long (to support a maximum of 64 bytes data field): - // 2 words header + 16 words data field (to support up to 64 bytes of data). + // 2 words header + 16 words data field (to support up to 64 bytes of data). // The total number of words reserved for the Tx FIFO per FDCAN instance is 288 words. - init->TxBuffersNbr = 0; - init->TxFifoQueueElmtsNbr = 16; + if (tx_mode == CAN_TX_FIFO) { + init->TxBuffersNbr = 0; + init->TxFifoQueueElmtsNbr = CAN_TX_QUEUE_LEN; + } else { + init->TxBuffersNbr = CAN_TX_QUEUE_LEN; + init->TxFifoQueueElmtsNbr = 0; + } + init->TxFifoQueueMode = fifo_queue_mode; init->TxElmtSize = FDCAN_DATA_BYTES_64; - init->TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION; // Reception section is configured to use Rx FIFO 0 and Rx FIFO1, with no dedicated Rx buffers. // Each Rx FIFO can store a maximum of 64 elements (1152 words), each element is 18 words @@ -145,15 +189,14 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca init->RxFifo0ElmtSize = FDCAN_DATA_BYTES_64; init->RxFifo1ElmtsNbr = 24; init->RxFifo1ElmtSize = FDCAN_DATA_BYTES_64; - #endif + #endif // STM32H7 - FDCAN_GlobalTypeDef *CANx = NULL; const machine_pin_obj_t *pins[2]; switch (can_id) { #if defined(MICROPY_HW_CAN1_TX) case PYB_CAN_1: - CANx = FDCAN1; + can->Instance = FDCAN1; pins[0] = MICROPY_HW_CAN1_TX; pins[1] = MICROPY_HW_CAN1_RX; break; @@ -161,7 +204,7 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca #if defined(MICROPY_HW_CAN2_TX) case PYB_CAN_2: - CANx = FDCAN2; + can->Instance = FDCAN2; pins[0] = MICROPY_HW_CAN2_TX; pins[1] = MICROPY_HW_CAN2_RX; break; @@ -183,9 +226,7 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca } } - // init CANx - can->Instance = CANx; - // catch bad configuration errors. + // initialise hardware, catching bad configuration errors. if (HAL_FDCAN_Init(can) != HAL_OK) { return false; } @@ -194,7 +235,9 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca HAL_FDCAN_ConfigGlobalFilter(can, FDCAN_REJECT, FDCAN_REJECT, DISABLE, DISABLE); // The configuration registers are locked after CAN is started. - HAL_FDCAN_Start(can); + if (HAL_FDCAN_Start(can) != HAL_OK) { + return false; + } // Reset all filters for (int f = 0; f < init->StdFiltersNbr; ++f) { @@ -228,29 +271,37 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca // FDCAN IT 1 HAL_FDCAN_ConfigInterruptLines(can, FDCAN_IT_GROUP_RX_FIFO1, FDCAN_INTERRUPT_LINE1); - // Enable error interrupts. RX-related interrupts are enabled via can_enable_rx_interrupts() - HAL_FDCAN_ActivateNotification(can, FDCAN_IT_BUS_OFF | FDCAN_IT_ERROR_WARNING | FDCAN_IT_ERROR_PASSIVE, 0); + // Enable error interrupts for all queue positions. + // (RX-related interrupts are enabled via can_enable_rx_interrupts(), + // and TX-related interrupts are enabled during TX + HAL_FDCAN_ActivateNotification(can, + FDCAN_IT_BUS_OFF | FDCAN_IT_ERROR_WARNING | FDCAN_IT_ERROR_PASSIVE, 0); return true; } void can_deinit(FDCAN_HandleTypeDef *can) { + bool any_enabled = false; HAL_FDCAN_DeInit(can); + if (can->Instance == FDCAN1) { HAL_NVIC_DisableIRQ(FDCAN1_IT0_IRQn); HAL_NVIC_DisableIRQ(FDCAN1_IT1_IRQn); - // TODO check if FDCAN2 is used. - __HAL_RCC_FDCAN_FORCE_RESET(); - __HAL_RCC_FDCAN_RELEASE_RESET(); - __HAL_RCC_FDCAN_CLK_DISABLE(); + #if defined(MICROPY_HW_CAN2_TX) + any_enabled = NVIC_GetEnableIRQ(FDCAN2_IT0_IRQn); + #endif #if defined(MICROPY_HW_CAN2_TX) } else if (can->Instance == FDCAN2) { HAL_NVIC_DisableIRQ(FDCAN2_IT0_IRQn); HAL_NVIC_DisableIRQ(FDCAN2_IT1_IRQn); - // TODO check if FDCAN2 is used. + any_enabled = NVIC_GetEnableIRQ(FDCAN1_IT0_IRQn); + #endif + } + + if (!any_enabled) { + // Only reset FDCAN block and disable clock if all FDCAN units are disabled __HAL_RCC_FDCAN_FORCE_RESET(); __HAL_RCC_FDCAN_RELEASE_RESET(); __HAL_RCC_FDCAN_CLK_DISABLE(); - #endif } } @@ -263,6 +314,36 @@ void can_clearfilter(FDCAN_HandleTypeDef *can, uint32_t f, bool is_extid) { HAL_FDCAN_ConfigFilter(can, &filter); } +uint32_t can_get_source_freq(void) { + // Find CAN kernel clock + #if defined(STM32H7) + switch (__HAL_RCC_GET_FDCAN_SOURCE()) { + case RCC_FDCANCLKSOURCE_HSE: + return HSE_VALUE; + case RCC_FDCANCLKSOURCE_PLL: { + PLL1_ClocksTypeDef pll1_clocks; + HAL_RCCEx_GetPLL1ClockFreq(&pll1_clocks); + return pll1_clocks.PLL1_Q_Frequency; + } + case RCC_FDCANCLKSOURCE_PLL2: { + PLL2_ClocksTypeDef pll2_clocks; + HAL_RCCEx_GetPLL2ClockFreq(&pll2_clocks); + return pll2_clocks.PLL2_Q_Frequency; + } + default: + abort(); // Should be unreachable, macro should return one of the above + } + #elif defined(STM32G4) + // STM32G4 CAN clock from reset is HSE, unchanged by MicroPython + return HSE_VALUE; + #else // G0, and assume other MCUs too. + // CAN1/CAN2/CAN3 on APB1 use GetPCLK1Freq, alternatively use the following: + // can_kern_clk = ((HSE_VALUE / osc_config.PLL.PLLM ) * osc_config.PLL.PLLN) / + // (osc_config.PLL.PLLQ * clk_init.AHBCLKDivider * clk_init.APB1CLKDivider); + return HAL_RCC_GetPCLK1Freq(); + #endif +} + void can_disable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { HAL_FDCAN_DeactivateNotification(can, (fifo == CAN_RX_FIFO0) ? FDCAN_IT_RX_FIFO0_MASK : FDCAN_IT_RX_FIFO1_MASK); } @@ -275,6 +356,19 @@ void can_enable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo, bool e HAL_FDCAN_ActivateNotification(can, ints, 0); } +// Fixup the DataLength field from a byte count to a valid DLC value index (rounding up) +static void encode_datalength(CanTxMsgTypeDef *txmsg) { + // Roundup DataLength to next DLC size and encode to DLC. + size_t len_bytes = txmsg->DataLength; + for (mp_uint_t i = 0; i < MP_ARRAY_SIZE(DLCtoBytes); i++) { + if (len_bytes <= DLCtoBytes[i]) { + txmsg->DataLength = (i << 16); + return; + } + } + assert(0); // DataLength value is invalid +} + HAL_StatusTypeDef can_transmit(CAN_HandleTypeDef *can, CanTxMsgTypeDef *txmsg, uint8_t *data, uint32_t timeout_ms) { uint32_t start = HAL_GetTick(); while (HAL_FDCAN_GetTxFifoFreeLevel(can) == 0) { @@ -289,9 +383,58 @@ HAL_StatusTypeDef can_transmit(CAN_HandleTypeDef *can, CanTxMsgTypeDef *txmsg, u } mp_event_wait_ms(1); } + // Note: this function doesn't set up TX interrupts, because it's only used by pyb.CAN which + // doesn't care about this - machine.CAN calls can_transmit_buf_index() + encode_datalength(txmsg); + return HAL_FDCAN_AddMessageToTxFifoQ(can, txmsg, data); } +HAL_StatusTypeDef can_transmit_buf_index(CAN_HandleTypeDef *hcan, int index, CanTxMsgTypeDef *txmsg, const uint8_t *data) { + uint32_t tx_loc = 1U << index; + + encode_datalength(txmsg); + + HAL_StatusTypeDef err = HAL_FDCAN_ActivateNotification(hcan, FDCAN_IT_TX_COMPLETE | FDCAN_IT_TX_ABORT_COMPLETE, tx_loc); + if (err == HAL_OK) { + // Note: casting away const from data, the HAL implementation still treats 'data' as const + err = HAL_FDCAN_AddMessageToTxBuffer(hcan, txmsg, (void *)data, tx_loc); + } + if (err == HAL_OK) { + err = HAL_FDCAN_EnableTxBufferRequest(hcan, tx_loc); + } + return err; +} + +bool can_cancel_transmit(CAN_HandleTypeDef *hcan, int index) { + FDCAN_GlobalTypeDef *instance = hcan->Instance; + bool result = false; + + if (instance->TXBRP & (1U << index)) { + result = true; + HAL_StatusTypeDef err = HAL_FDCAN_AbortTxRequest(hcan, 1U << index); + assert(err == HAL_OK); // Should only fail if controller not started + if (err != HAL_OK) { + return false; + } + mp_uint_t start = mp_hal_ticks_us(); + + // Wait for the TX buffer to be marked as no longer pending + while ((instance->TXBRP & (1U << index)) != 0) { + // we don't expect this to take longer than a few clock cycles, if + // it does then it probably indicates a bug in the driver. However, + // either way we don't want to end up stuck here + mp_uint_t elapsed = mp_hal_ticks_us() - start; + assert(elapsed < 1000); + if (elapsed >= 1000) { + break; + } + } + } + + return result; +} + int can_receive(FDCAN_HandleTypeDef *can, can_rx_fifo_t fifo, FDCAN_RxHeaderTypeDef *hdr, uint8_t *data, uint32_t timeout_ms) { volatile uint32_t *rxf, *rxa; uint32_t fl; @@ -389,8 +532,48 @@ can_state_t can_get_state(CAN_HandleTypeDef *can) { } } -static void can_rx_irq_handler(uint can_id, CAN_TypeDef *instance, can_rx_fifo_t fifo) { - uint32_t ints, rx_fifo_ints, error_ints; +void can_get_counters(CAN_HandleTypeDef *can, can_counters_t *counters) { + FDCAN_GlobalTypeDef *inst = can->Instance; + uint32_t esr = inst->ECR; + counters->tec = (esr & FDCAN_ECR_TEC_Msk) >> FDCAN_ECR_TEC_Pos; + if (esr & FDCAN_ECR_RP) { + counters->rec = 128; // "at least 128" + } else { + counters->rec = (esr & FDCAN_ECR_REC_Msk) >> FDCAN_ECR_REC_Pos; + } + if (can->Init.TxFifoQueueMode == FDCAN_TX_FIFO_OPERATION) { + counters->tx_pending = inst->TXEFS & 0x7; + } else { + counters->tx_pending = mp_popcount(inst->TXBRP); + } + counters->rx_fifo0_pending = (inst->RXF0S & FDCAN_RXF0S_F0FL_Msk) >> FDCAN_RXF0S_F0FL_Pos; + counters->rx_fifo1_pending = (inst->RXF1S & FDCAN_RXF1S_F1FL_Msk) >> FDCAN_RXF1S_F1FL_Pos; +} + +void can_disable_tx_interrupts(CAN_HandleTypeDef *can) { + HAL_FDCAN_DeactivateNotification(can, FDCAN_IT_TX_COMPLETE | FDCAN_IT_TX_ABORT_COMPLETE); +} + +void can_restart(CAN_HandleTypeDef *can) { + HAL_FDCAN_Stop(can); + HAL_FDCAN_Start(can); +} + +// Compatibility shim: call both the pyb.CAN and machine.CAN handlers if necessary, +// allow them to decide which is initialised. +static inline void call_can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo) { + #if MICROPY_PY_MACHINE_CAN + machine_can_irq_handler(can_id, interrupt); + #endif + if (interrupt != CAN_INT_TX_COMPLETE) { + pyb_can_irq_handler(can_id, interrupt, fifo); + } + // TODO: Need to do something to clear the transmit state if pyb.CAN is in use, I think + // (check usage of can_get_transmit_finished() from pyb CAN code) +} + +static void can_irq_handler(uint can_id, CAN_TypeDef *instance, can_rx_fifo_t fifo) { + uint32_t ints, rx_fifo_ints, error_ints, tx_complete_int; ints = instance->IR & instance->IE; @@ -401,54 +584,57 @@ static void can_rx_irq_handler(uint can_id, CAN_TypeDef *instance, can_rx_fifo_t } error_ints = ints & FDCAN_IT_ERROR_STATUS_MASK; + tx_complete_int = ints & (FDCAN_IT_TX_COMPLETE | FDCAN_IT_TX_ABORT_COMPLETE); + // Disable receive interrupts, re-enabled by higher layer after calling can_receive() + // (Note: can't use __HAL_CAN API as only have a CAN_TypeDef, not CAN_HandleTypeDef) instance->IE &= ~rx_fifo_ints; - instance->IR = rx_fifo_ints | error_ints; + instance->IR = rx_fifo_ints | error_ints | tx_complete_int; if (rx_fifo_ints) { - if (rx_fifo_ints & FDCAN_IT_RX_NEW_MESSAGE_MASK) { - can_irq_handler(can_id, CAN_INT_MESSAGE_RECEIVED, fifo); - } if (rx_fifo_ints & FDCAN_IT_RX_FULL_MASK) { - can_irq_handler(can_id, CAN_INT_FIFO_FULL, fifo); + call_can_irq_handler(can_id, CAN_INT_FIFO_FULL, fifo); } if (rx_fifo_ints & FDCAN_IT_RX_MESSAGE_LOST_MASK) { - can_irq_handler(can_id, CAN_INT_FIFO_OVERFLOW, fifo); + call_can_irq_handler(can_id, CAN_INT_FIFO_OVERFLOW, fifo); + } + if (rx_fifo_ints & FDCAN_IT_RX_NEW_MESSAGE_MASK) { + call_can_irq_handler(can_id, CAN_INT_MESSAGE_RECEIVED, fifo); } } if (error_ints) { uint32_t Psr = instance->PSR; - if (error_ints & FDCAN_IT_ERROR_WARNING) { - if (Psr & FDCAN_PSR_EW) { - can_irq_handler(can_id, CAN_INT_ERR_WARNING, 0); - } - } - if (error_ints & FDCAN_IT_ERROR_PASSIVE) { - if (Psr & FDCAN_PSR_EP) { - can_irq_handler(can_id, CAN_INT_ERR_PASSIVE, 0); - } - } - if (error_ints & FDCAN_IT_BUS_OFF) { + if (error_ints & (FDCAN_IT_ERROR_WARNING | FDCAN_IT_ERROR_PASSIVE | FDCAN_IT_BUS_OFF)) { if (Psr & FDCAN_PSR_BO) { - can_irq_handler(can_id, CAN_INT_ERR_BUS_OFF, 0); + call_can_irq_handler(can_id, CAN_INT_ERR_BUS_OFF, 0); + } else if (Psr & FDCAN_PSR_EP) { + call_can_irq_handler(can_id, CAN_INT_ERR_PASSIVE, 0); + } else if (Psr & FDCAN_PSR_EW) { + call_can_irq_handler(can_id, CAN_INT_ERR_WARNING, 0); } } } + + if (tx_complete_int) { + // Disable TX interrupts until we process these ones + instance->IE &= ~tx_complete_int; + call_can_irq_handler(can_id, CAN_INT_TX_COMPLETE, 0); + } } #if defined(MICROPY_HW_CAN1_TX) void FDCAN1_IT0_IRQHandler(void) { IRQ_ENTER(FDCAN1_IT0_IRQn); - can_rx_irq_handler(PYB_CAN_1, FDCAN1, CAN_RX_FIFO0); + can_irq_handler(PYB_CAN_1, FDCAN1, CAN_RX_FIFO0); IRQ_EXIT(FDCAN1_IT0_IRQn); } void FDCAN1_IT1_IRQHandler(void) { IRQ_ENTER(FDCAN1_IT1_IRQn); - can_rx_irq_handler(PYB_CAN_1, FDCAN1, CAN_RX_FIFO1); + can_irq_handler(PYB_CAN_1, FDCAN1, CAN_RX_FIFO1); IRQ_EXIT(FDCAN1_IT1_IRQn); } #endif @@ -456,13 +642,13 @@ void FDCAN1_IT1_IRQHandler(void) { #if defined(MICROPY_HW_CAN2_TX) void FDCAN2_IT0_IRQHandler(void) { IRQ_ENTER(FDCAN2_IT0_IRQn); - can_rx_irq_handler(PYB_CAN_2, FDCAN2, CAN_RX_FIFO0); + can_irq_handler(PYB_CAN_2, FDCAN2, CAN_RX_FIFO0); IRQ_EXIT(FDCAN2_IT0_IRQn); } void FDCAN2_IT1_IRQHandler(void) { IRQ_ENTER(FDCAN2_IT1_IRQn); - can_rx_irq_handler(PYB_CAN_2, FDCAN2, CAN_RX_FIFO1); + can_irq_handler(PYB_CAN_2, FDCAN2, CAN_RX_FIFO1); IRQ_EXIT(FDCAN2_IT1_IRQn); } #endif diff --git a/ports/stm32/machine_can.c b/ports/stm32/machine_can.c new file mode 100644 index 0000000000000..74821b88d5bca --- /dev/null +++ b/ports/stm32/machine_can.c @@ -0,0 +1,489 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2024-2026 Angus Gratton + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// This file is never compiled standalone, it's included directly from +// extmod/machine_can.c via MICROPY_PY_MACHINE_CAN_INCLUDEFILE. +#include +#include "extmod/machine_can_port.h" +#include "can.h" +#include "py/runtime.h" +#include "py/mperrno.h" +#include "py/mphal.h" +#include "py/gc.h" + +#if MICROPY_HW_ENABLE_FDCAN +#define CAN_BRP_MIN 1 +#define CAN_BRP_MAX 512 +#define CAN_FD_BRS_BRP_MIN 1 +#define CAN_FD_BRS_BRP_MAX 32 +#define CAN_FILTERS_STD_EXT_SEPARATE 1 + +#else // Classic bxCAN +#define CAN_BRP_MIN 1 +#define CAN_BRP_MAX 1024 +#define CAN_FILTERS_STD_EXT_SEPARATE 0 +#endif + +#define TX_EMPTY UINT32_MAX + +struct machine_can_port { + CAN_HandleTypeDef h; + uint32_t tx[CAN_TX_QUEUE_LEN]; // ID stored in each hardware tx buffer, or TX_EMPTY if empty + bool irq_state_pending; + bool error_passive; +}; + +// Convert the port agnostic CAN mode to the ST mode +static uint32_t can_port_mode(machine_can_mode_t mode) { + switch (mode) { + case MP_CAN_MODE_NORMAL: + return CAN_MODE_NORMAL; + case MP_CAN_MODE_SLEEP: + return CAN_MODE_SILENT; // Sleep is not an operating mode for ST's peripheral + case MP_CAN_MODE_LOOPBACK: + return CAN_MODE_LOOPBACK; + case MP_CAN_MODE_SILENT: + return CAN_MODE_SILENT; + case MP_CAN_MODE_SILENT_LOOPBACK: + return CAN_MODE_SILENT_LOOPBACK; + default: + assert(0); // Mode should have been checked already + return CAN_MODE_NORMAL; + } +} + +static int machine_can_port_f_clock(const machine_can_obj_t *self) { + return (int)can_get_source_freq(); +} + +static bool machine_can_port_supports_mode(const machine_can_obj_t *self, machine_can_mode_t mode) { + return mode < MP_CAN_MODE_MAX; +} + +static mp_uint_t machine_can_port_max_data_len(mp_uint_t flags) { + #if MICROPY_HW_ENABLE_FDCAN + if (flags & CAN_MSG_FLAG_FD_F) { + return 64; + } + #endif + return 8; +} + +static void machine_can_port_init(machine_can_obj_t *self) { + if (!self->port) { + self->port = m_new(struct machine_can_port, 1); + } + memset(self->port, 0, sizeof(struct machine_can_port)); + for (int i = 0; i < CAN_TX_QUEUE_LEN; i++) { + self->port->tx[i] = TX_EMPTY; + } + + bool res = can_init(&self->port->h, + self->can_idx + 1, // Convert 0-based index to 1-based 'can_id' for lower layer + CAN_TX_QUEUE, + can_port_mode(self->mode), + self->brp, + self->sjw, + self->tseg1, + self->tseg2, + false); // auto_restart not currently exposed + + if (!res) { + mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("CAN init failed")); + } +} + +static void machine_can_port_cancel_all_tx(machine_can_obj_t *self) { + struct machine_can_port *port = self->port; + can_disable_tx_interrupts(&port->h); + for (int i = 0; i < CAN_TX_QUEUE_LEN; i++) { + can_cancel_transmit(&port->h, i); + port->tx[i] = TX_EMPTY; + } +} + +static void machine_can_port_deinit(machine_can_obj_t *self) { + machine_can_port_cancel_all_tx(self); + can_deinit(&self->port->h); +} + +static mp_int_t machine_can_port_send(machine_can_obj_t *self, mp_uint_t id, const byte *data, size_t data_len, mp_uint_t flags) { + int idx_empty = -1; // Empty transmit buffer, where no later index has the same ID message in it + + // Scan through the current transmit queue to find an eligible buffer for transmit + for (int i = 0; i < CAN_TX_QUEUE_LEN; i++) { + uint32_t tx_id = self->port->tx[i]; + if (tx_id == TX_EMPTY) { + // This slot is empty + if (idx_empty == -1) { + // Still have to keep scanning as we might see a later message with the same ID, + idx_empty = i; + } + } else if (tx_id == id && !(flags & CAN_MSG_FLAG_UNORDERED)) { + // Can't queue a second message with the same ID and guarantee order + + // (Undocumented hardware limitation - CANFD reference suggests + // messages with the same ID are sent in buffer index order but + // testing shows not always the case at least on STM32H7! Unsure if + // also a limitation of bxCAN or STM32G4, but these only have 3 TX + // buffers so inserting in buffer index order is likely to run out + // of buffers relatively quickly anyway...) + + // Note: currently the driver considers a Standard and an Extended + // ID with the same numeric value to be the same ID... could fix + // this, although it's a relatively uncommon case. + return -1; + } + } + + if (idx_empty == -1) { + // No space in transmit queue + return -1; + } + + CanTxMsgTypeDef tx = { + #if MICROPY_HW_ENABLE_FDCAN + .MessageMarker = 0, + .ErrorStateIndicator = FDCAN_ESI_ACTIVE, + .TxEventFifoControl = FDCAN_NO_TX_EVENTS, + .Identifier = id, // Range checked by caller + .IdType = (flags & CAN_MSG_FLAG_EXT_ID) ? FDCAN_EXTENDED_ID : FDCAN_STANDARD_ID, + .TxFrameType = (flags & CAN_MSG_FLAG_RTR) ? FDCAN_REMOTE_FRAME : FDCAN_DATA_FRAME, + .FDFormat = (flags & CAN_MSG_FLAG_FD_F) ? FDCAN_FD_CAN : FDCAN_CLASSIC_CAN, + .BitRateSwitch = (flags & CAN_MSG_FLAG_BRS) ? FDCAN_BRS_ON : FDCAN_BRS_OFF, + .DataLength = data_len, // Converted inside can_transmit_buf_index + #else // Classic + .StdId = (flags & CAN_MSG_FLAG_EXT_ID) ? 0 : id, + .ExtId = (flags & CAN_MSG_FLAG_EXT_ID) ? id : 0, + .IDE = (flags & CAN_MSG_FLAG_EXT_ID) ? CAN_ID_EXT : CAN_ID_STD, + .RTR = (flags & CAN_MSG_FLAG_RTR) ? CAN_RTR_REMOTE : CAN_RTR_DATA, + .DLC = data_len, + #endif + }; + #if !MICROPY_HW_ENABLE_FDCAN + assert(data_len <= sizeof(tx.Data)); // Also checked by caller + memcpy(tx.Data, data, data_len); + #endif + + HAL_StatusTypeDef err = can_transmit_buf_index(&self->port->h, idx_empty, &tx, data); + if (err != HAL_OK) { + return -1; + } + self->port->tx[idx_empty] = id; + + return idx_empty; +} + +static bool machine_can_port_cancel_send(machine_can_obj_t *self, mp_uint_t idx) { + return can_cancel_transmit(&self->port->h, idx); +} + +static bool machine_can_port_recv(machine_can_obj_t *self, void *data, size_t *dlen, mp_uint_t *id, mp_uint_t *flags, mp_uint_t *errors) { + CAN_HandleTypeDef *can = &self->port->h; + CanRxMsgTypeDef msg; + + for (can_rx_fifo_t fifo = CAN_RX_FIFO0; fifo <= CAN_RX_FIFO1; fifo++) { + if (can_receive(can, fifo, &msg, data, 0) == 0) { + // CanRxMsgTypeDef is different for Classic vs FD + #if MICROPY_HW_ENABLE_FDCAN + *flags = ((msg.IdType == FDCAN_EXTENDED_ID) ? CAN_MSG_FLAG_EXT_ID : 0) | + ((msg.RxFrameType == FDCAN_REMOTE_FRAME) ? CAN_MSG_FLAG_RTR : 0); + *id = msg.Identifier; + *dlen = msg.DataLength; // Lower layer has converted to bytes already + #else + *flags = (msg.IDE ? CAN_MSG_FLAG_EXT_ID : 0) | + (msg.RTR ? CAN_MSG_FLAG_RTR : 0); + *id = msg.IDE ? msg.ExtId : msg.StdId; + *dlen = msg.DLC; + #endif + + *errors = self->rx_error_flags; + self->rx_error_flags = 0; + + // Re-enable any interrupts that were disabled in RX IRQ handlers + can_enable_rx_interrupts(can, fifo, self->mp_irq_trigger & MP_CAN_IRQ_RX); + + return true; + } + } + return false; +} + +static void machine_can_update_irqs(machine_can_obj_t *self) { + uint16_t triggers = self->mp_irq_trigger; + + for (can_rx_fifo_t fifo = CAN_RX_FIFO0; fifo <= CAN_RX_FIFO1; fifo++) { + if (triggers & MP_CAN_IRQ_RX) { + can_enable_rx_interrupts(&self->port->h, fifo, true); + } else { + can_disable_rx_interrupts(&self->port->h, fifo); + } + } + + // Note: TX complete interrupt is always enabled to manage the internal queue state +} + +static void machine_can_port_clear_filters(machine_can_obj_t *self) { + #if MICROPY_HW_ENABLE_FDCAN + for (int f = 0; f < CAN_HW_MAX_STD_FILTER; f++) { + can_clearfilter(&self->port->h, f, false); + } + for (int f = 0; f < CAN_HW_MAX_EXT_FILTER; f++) { + can_clearfilter(&self->port->h, f, true); + } + #else + int bank_offs = (self->can_idx == 1) ? CAN_HW_MAX_FILTER : 0; // CAN2 filters index after CAN1 + for (int f = 0; f < CAN_HW_MAX_FILTER; f++) { + can_clearfilter(&self->port->h, f + bank_offs, CAN_HW_MAX_FILTER); + } + #endif +} + +#if MICROPY_HW_ENABLE_FDCAN +static void machine_can_port_set_filter(machine_can_obj_t *self, int filter_idx, mp_uint_t can_id, mp_uint_t mask, mp_uint_t flags) { + int max_idx = (flags & CAN_MSG_FLAG_EXT_ID) ? CAN_HW_MAX_EXT_FILTER : CAN_HW_MAX_STD_FILTER; + if (filter_idx >= max_idx) { + mp_raise_ValueError(MP_ERROR_TEXT("too many filters for this ID type")); + } + if (flags & ~CAN_MSG_FLAG_EXT_ID) { + mp_raise_ValueError(MP_ERROR_TEXT("flags")); // Only supported flag is for extended ID + } + + FDCAN_FilterTypeDef filter = { + .IdType = (flags & CAN_MSG_FLAG_EXT_ID) ? FDCAN_EXTENDED_ID : FDCAN_STANDARD_ID, + // FDCAN counts standard and extended id filters separately, but this is + // already accounted for in filter_idx due to CAN_FILTERS_STD_EXT_SEPARATE. + .FilterIndex = filter_idx, + .FilterType = FDCAN_FILTER_MASK, + // Round-robin between FIFO1 and FIFO0 + .FilterConfig = (filter_idx & 1) ? FDCAN_FILTER_TO_RXFIFO1 : FDCAN_FILTER_TO_RXFIFO0, + .FilterID1 = can_id, + .FilterID2 = mask, + }; + + int r = HAL_FDCAN_ConfigFilter(&self->port->h, &filter); + assert(r == HAL_OK); + (void)r; +} +#else +static void machine_can_port_set_filter(machine_can_obj_t *self, int filter_idx, mp_uint_t can_id, mp_uint_t mask, mp_uint_t flags) { + if (filter_idx >= CAN_HW_MAX_FILTER) { + mp_raise_ValueError(MP_ERROR_TEXT("too many filters")); + } + if (flags & ~CAN_MSG_FLAG_EXT_ID) { + mp_raise_ValueError(MP_ERROR_TEXT("flags")); // Only supported flag is for extended ID + } + + if (self->can_idx == 1) { + filter_idx += CAN_HW_MAX_FILTER; // CAN2 filters index after CAN1 + } + + CAN_FilterConfTypeDef filter = { + .FilterActivation = ENABLE, + .FilterScale = CAN_FILTERSCALE_32BIT, + .FilterMode = CAN_FILTERMODE_IDMASK, + .FilterNumber = filter_idx, + // Apply the filters round-robin to each FIFO, as each filter in bxCAN is + // associated with only one FIFO. + .FilterFIFOAssignment = filter_idx % 2, + .BankNumber = CAN_HW_MAX_FILTER, // Assign same number of filters to CAN2 as CAN1 + }; + + // This somewhat corresponds to STM32 RM Figure 342 "Filter bank scale + // configuration", although the Reference Manual makes 32-bit mask filters look + // a lot more complex than they are, then the ST HAL makes it even more + // complex by only supporting filter configuration via 16-bit halfwords + // which are re-assembled to full words inside the HAL... + if (flags & CAN_MSG_FLAG_EXT_ID) { + filter.FilterIdLow = (can_id << 3) | CAN_ID_EXT; + filter.FilterIdHigh = can_id >> 13; + filter.FilterMaskIdLow = (mask << 3) | CAN_ID_EXT; + filter.FilterMaskIdHigh = mask >> 13; + } else { + filter.FilterIdLow = 0; + filter.FilterIdHigh = can_id << 5; + filter.FilterMaskIdLow = CAN_ID_EXT; // Set to require CAN_ID_EXT unset in message + filter.FilterMaskIdHigh = mask << 5; + } + + int r = HAL_CAN_ConfigFilter(&self->port->h, &filter); + assert(r == HAL_OK); // Params should be verified before passing to HAL + (void)r; +} +#endif // MICROPY_HW_ENABLE_FDCAN + +static machine_can_state_t machine_can_port_get_state(machine_can_obj_t *self) { + // machine_can_port.h defines MP_CAN_STATE_xxx enums, verify they all match + // numerically with stm32 can.h CAN_STATE_xxx enums + MP_STATIC_ASSERT((int)MP_CAN_STATE_STOPPED == (int)CAN_STATE_STOPPED); + MP_STATIC_ASSERT((int)MP_CAN_STATE_ACTIVE == (int)CAN_STATE_ERROR_ACTIVE); + MP_STATIC_ASSERT((int)MP_CAN_STATE_WARNING == (int)CAN_STATE_ERROR_WARNING); + MP_STATIC_ASSERT((int)MP_CAN_STATE_PASSIVE == (int)CAN_STATE_ERROR_PASSIVE); + MP_STATIC_ASSERT((int)MP_CAN_STATE_BUS_OFF == (int)CAN_STATE_BUS_OFF); + return (machine_can_state_t)can_get_state(&self->port->h); +} + +static void machine_can_port_update_counters(machine_can_obj_t *self) { + can_counters_t hw_counters; + struct machine_can_port *port = self->port; + machine_can_counters_t *counters = &self->counters; + + can_get_counters(&port->h, &hw_counters); + + counters->tec = hw_counters.tec; + counters->rec = hw_counters.rec; + counters->tx_pending = hw_counters.tx_pending; + counters->rx_pending = hw_counters.rx_fifo0_pending + hw_counters.rx_fifo1_pending; + + // Other fields in 'counters' are updated from ISR directly +} + +static mp_obj_t machine_can_port_get_additional_timings(machine_can_obj_t *self, mp_obj_t optional_arg) { + return mp_const_none; +} + +static void machine_can_port_restart(machine_can_obj_t *self) { + // extmod layer has already checked CAN is initialised + struct machine_can_port *port = self->port; + machine_can_port_cancel_all_tx(self); + can_restart(&port->h); + port->irq_state_pending = false; +} + +static bool clear_complete_transfer(machine_can_obj_t *self, int *index, bool *is_success) { + *index = can_get_transmit_finished(&self->port->h, is_success); + if (*index == -1) { + return false; + } + self->port->tx[*index] = TX_EMPTY; + + return true; +} + +static mp_uint_t machine_can_port_irq_flags(machine_can_obj_t *self) { + mp_uint_t flags = 0; + CAN_HandleTypeDef *can = &self->port->h; + + if (self->mp_irq_trigger & MP_CAN_IRQ_STATE && self->port->irq_state_pending) { + flags |= MP_CAN_IRQ_STATE; + self->port->irq_state_pending = false; + } + + // Check for RX + if (self->mp_irq_trigger & MP_CAN_IRQ_RX) { + for (can_rx_fifo_t fifo = CAN_RX_FIFO0; fifo <= CAN_RX_FIFO1; fifo++) { + if (can_is_rx_pending(can, fifo)) { + flags |= MP_CAN_IRQ_RX; + } + } + } + + // Check for TX done + if (self->mp_irq_trigger & MP_CAN_IRQ_TX) { + bool is_success = false; + int index; + if (clear_complete_transfer(self, &index, &is_success)) { + flags |= (mp_uint_t)(index << MP_CAN_IRQ_IDX_SHIFT) | MP_CAN_IRQ_TX; + if (!is_success) { + flags |= MP_CAN_IRQ_TX_FAILED; + } + } + } + + return flags; +} + +void machine_can_irq_handler(uint can_id, can_int_t interrupt) { + assert(can_id > 0); + machine_can_obj_t *self = MP_STATE_PORT(machine_can_objs)[can_id - 1]; + if (self == NULL) { + return; // Should only hit this code path if pyb.CAN has enabled interrupt + } + struct machine_can_port *port = self->port; + machine_can_counters_t *counters = &self->counters; + bool call_irq = false; + bool irq_state = false; + + switch (interrupt) { + // RX + case CAN_INT_FIFO_FULL: + self->rx_error_flags |= CAN_RECV_ERR_FULL; + break; + case CAN_INT_FIFO_OVERFLOW: + self->rx_error_flags |= CAN_RECV_ERR_OVERRUN; + counters->rx_overruns++; + break; + case CAN_INT_MESSAGE_RECEIVED: + call_irq = call_irq || (self->mp_irq_trigger & MP_CAN_IRQ_RX); + break; + + // Error states + case CAN_INT_ERR_WARNING: + if (!port->error_passive) { + // Only count entering warning state, not leaving it + counters->num_warning++; + irq_state = true; + } + port->error_passive = false; + break; + case CAN_INT_ERR_PASSIVE: + counters->num_passive++; + port->error_passive = true; + irq_state = true; + break; + case CAN_INT_ERR_BUS_OFF: + counters->num_bus_off++; + irq_state = true; + port->error_passive = false; + break; + + // TX + case CAN_INT_TX_COMPLETE: + if (!(self->mp_irq_trigger & MP_CAN_IRQ_TX)) { + // No TX IRQ, so mark this buffer as free and move on + int index; + bool is_success = false; + clear_complete_transfer(self, &index, &is_success); + } else { + // Otherwise, the slot is marked empty after the irq calls flags() + call_irq = true; + } + break; + + default: + assert(0); // Should be unreachable + } + + if (irq_state && (self->mp_irq_trigger & MP_CAN_IRQ_STATE)) { + self->port->irq_state_pending = true; + call_irq = true; + } + + if (call_irq) { + assert(self->mp_irq_obj != NULL); // Can't set mp_irq_trigger otherwise + mp_irq_handler(self->mp_irq_obj); + } +} diff --git a/ports/stm32/main.c b/ports/stm32/main.c index 6ae8061c4135f..ccf3b6d406004 100644 --- a/ports/stm32/main.c +++ b/ports/stm32/main.c @@ -41,6 +41,7 @@ #include "lib/littlefs/lfs2_util.h" #include "extmod/modmachine.h" #include "extmod/modnetwork.h" +#include "extmod/machine_can.h" #include "extmod/vfs.h" #include "extmod/vfs_fat.h" #include "extmod/vfs_lfs.h" @@ -763,7 +764,10 @@ void stm32_main(uint32_t reset_mode) { #endif #if MICROPY_HW_ENABLE_CAN pyb_can_deinit_all(); + #if MICROPY_PY_MACHINE_CAN + machine_can_deinit_all(); #endif + #endif // MICROPY_HW_ENABLE_CAN #if MICROPY_HW_ENABLE_DAC dac_deinit_all(); #endif diff --git a/ports/stm32/mpconfigport.h b/ports/stm32/mpconfigport.h index 4ffe5752be978..e7cf28accee57 100644 --- a/ports/stm32/mpconfigport.h +++ b/ports/stm32/mpconfigport.h @@ -122,6 +122,14 @@ #ifndef MICROPY_PY_MACHINE_BITSTREAM #define MICROPY_PY_MACHINE_BITSTREAM (1) #endif +#ifndef MICROPY_PY_MACHINE_CAN +#ifdef MICROPY_HW_CAN1_TX +#define MICROPY_PY_MACHINE_CAN (1) +#else +#define MICROPY_PY_MACHINE_CAN (0) +#endif +#endif +#define MICROPY_PY_MACHINE_CAN_INCLUDEFILE "ports/stm32/machine_can.c" #define MICROPY_PY_MACHINE_DHT_READINTO (1) #define MICROPY_PY_MACHINE_PULSE (1) #define MICROPY_PY_MACHINE_PIN_MAKE_NEW mp_pin_make_new @@ -207,6 +215,17 @@ extern const struct _mp_obj_type_t network_lan_type; #define MICROPY_HW_NIC_ETH #endif +// Provide a port-level default of MICROPY_HW_NUM_CAN based on pin definitions +#ifndef MICROPY_HW_NUM_CAN +#if defined(MICROPY_HW_CAN3_TX) +#define MICROPY_HW_NUM_CAN 3 +#elif defined(MICROPY_HW_CAN2_TX) +#define MICROPY_HW_NUM_CAN 2 +#elif defined(MICROPY_HW_CAN1_TX) +#define MICROPY_HW_NUM_CAN 1 +#endif +#endif // MICROPY_HW_NUM_CAN + // extra constants #define MICROPY_PORT_CONSTANTS \ MACHINE_BUILTIN_MODULE_CONSTANTS \ diff --git a/ports/stm32/pyb_can.c b/ports/stm32/pyb_can.c index c499a28197a38..a0b4de73bf912 100644 --- a/ports/stm32/pyb_can.c +++ b/ports/stm32/pyb_can.c @@ -39,6 +39,8 @@ #include "pyb_can.h" #include "can.h" #include "irq.h" +// For some non-port-specific utility functions +#include "extmod/machine_can.h" #if MICROPY_HW_ENABLE_CAN @@ -70,19 +72,12 @@ #define CAN_MAXIMUM_DBS1 (32) #define CAN_MAXIMUM_DBS2 (16) -#define CAN_MODE_NORMAL FDCAN_MODE_NORMAL -#define CAN_MODE_LOOPBACK FDCAN_MODE_EXTERNAL_LOOPBACK -#define CAN_MODE_SILENT FDCAN_MODE_BUS_MONITORING -#define CAN_MODE_SILENT_LOOPBACK FDCAN_MODE_INTERNAL_LOOPBACK - #define CAN1_RX0_IRQn FDCAN1_IT0_IRQn #define CAN1_RX1_IRQn FDCAN1_IT1_IRQn #if defined(CAN2) #define CAN2_RX0_IRQn FDCAN2_IT0_IRQn #define CAN2_RX1_IRQn FDCAN2_IT1_IRQn #endif - -extern const uint8_t DLCtoBytes[16]; #else #define CAN_MAX_FILTER (28) @@ -115,6 +110,7 @@ void pyb_can_deinit_all(void) { pyb_can_obj_t *can_obj = MP_STATE_PORT(pyb_can_obj_all)[i]; if (can_obj != NULL) { pyb_can_deinit(MP_OBJ_FROM_PTR(can_obj)); + MP_STATE_PORT(pyb_can_obj_all)[i] = NULL; } } } @@ -152,45 +148,10 @@ static void pyb_can_print(const mp_print_t *print, mp_obj_t self_in, mp_print_ki } } -static uint32_t pyb_can_get_source_freq() { - uint32_t can_kern_clk = 0; - - // Find CAN kernel clock - #if defined(STM32H7) - switch (__HAL_RCC_GET_FDCAN_SOURCE()) { - case RCC_FDCANCLKSOURCE_HSE: - can_kern_clk = HSE_VALUE; - break; - case RCC_FDCANCLKSOURCE_PLL: { - PLL1_ClocksTypeDef pll1_clocks; - HAL_RCCEx_GetPLL1ClockFreq(&pll1_clocks); - can_kern_clk = pll1_clocks.PLL1_Q_Frequency; - break; - } - case RCC_FDCANCLKSOURCE_PLL2: { - PLL2_ClocksTypeDef pll2_clocks; - HAL_RCCEx_GetPLL2ClockFreq(&pll2_clocks); - can_kern_clk = pll2_clocks.PLL2_Q_Frequency; - break; - } - } - #elif defined(STM32G4) - // STM32G4 CAN clock from reset is HSE, unchanged by MicroPython - can_kern_clk = HSE_VALUE; - #else // G0, F4, F7 and assume other MCUs too. - // CAN1/CAN2/CAN3 on APB1 use GetPCLK1Freq, alternatively use the following: - // can_kern_clk = ((HSE_VALUE / osc_config.PLL.PLLM ) * osc_config.PLL.PLLN) / - // (osc_config.PLL.PLLQ * clk_init.AHBCLKDivider * clk_init.APB1CLKDivider); - can_kern_clk = HAL_RCC_GetPCLK1Freq(); - #endif - - return can_kern_clk; -} - static void pyb_can_get_bit_timing(mp_uint_t baudrate, mp_uint_t sample_point, uint32_t max_brp, uint32_t max_bs1, uint32_t max_bs2, uint32_t min_tseg, mp_int_t *bs1_out, mp_int_t *bs2_out, mp_int_t *prescaler_out) { - uint32_t can_kern_clk = pyb_can_get_source_freq(); + uint32_t can_kern_clk = can_get_source_freq(); mp_uint_t max_baud_error = baudrate / 1000; // Allow .1% deviation const mp_uint_t MAX_SAMPLE_ERROR = 5; // round to nearest 1%, which is the param resolution sample_point *= 10; @@ -279,7 +240,12 @@ static mp_obj_t pyb_can_init_helper(pyb_can_obj_t *self, size_t n_args, const mp } #endif - if (!can_init(&self->can, self->can_id, args[ARG_mode].u_int, args[ARG_prescaler].u_int, args[ARG_sjw].u_int, + mp_uint_t mode = args[ARG_mode].u_int; + #if !MICROPY_HW_ENABLE_FDCAN + mode = mode << 4; // Undo the '>> 4' set when defining the bxCAN Python constants further down in this file + #endif + + if (!can_init(&self->can, self->can_id, CAN_TX_FIFO, mode, args[ARG_prescaler].u_int, args[ARG_sjw].u_int, args[ARG_bs1].u_int, args[ARG_bs2].u_int, args[ARG_auto_restart].u_bool)) { mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%d) init failure"), self->can_id); } @@ -298,45 +264,16 @@ static mp_obj_t pyb_can_make_new(const mp_obj_type_t *type, size_t n_args, size_ mp_arg_check_num(n_args, n_kw, 1, MP_OBJ_FUN_ARGS_MAX, true); // work out port - mp_uint_t can_idx; - if (mp_obj_is_str(args[0])) { - const char *port = mp_obj_str_get_str(args[0]); - if (0) { - #ifdef MICROPY_HW_CAN1_NAME - } else if (strcmp(port, MICROPY_HW_CAN1_NAME) == 0) { - can_idx = PYB_CAN_1; - #endif - #ifdef MICROPY_HW_CAN2_NAME - } else if (strcmp(port, MICROPY_HW_CAN2_NAME) == 0) { - can_idx = PYB_CAN_2; - #endif - #ifdef MICROPY_HW_CAN3_NAME - } else if (strcmp(port, MICROPY_HW_CAN3_NAME) == 0) { - can_idx = PYB_CAN_3; - #endif - } else { - mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%s) doesn't exist"), port); - } - } else { - can_idx = mp_obj_get_int(args[0]); - } - if (can_idx < 1 || can_idx > MP_ARRAY_SIZE(MP_STATE_PORT(pyb_can_obj_all))) { - mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%d) doesn't exist"), can_idx); - } - - // check if the CAN is reserved for system use or not - if (MICROPY_HW_CAN_IS_RESERVED(can_idx)) { - mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%d) is reserved"), can_idx); - } + mp_uint_t can_idx = machine_can_get_index(args[0]); // 0-based index pyb_can_obj_t *self; - if (MP_STATE_PORT(pyb_can_obj_all)[can_idx - 1] == NULL) { + if (MP_STATE_PORT(pyb_can_obj_all)[can_idx] == NULL) { self = mp_obj_malloc(pyb_can_obj_t, &pyb_can_type); - self->can_id = can_idx; + self->can_id = can_idx + 1; // ID is 1-based self->is_enabled = false; - MP_STATE_PORT(pyb_can_obj_all)[can_idx - 1] = self; + MP_STATE_PORT(pyb_can_obj_all)[can_idx] = self; } else { - self = MP_STATE_PORT(pyb_can_obj_all)[can_idx - 1]; + self = MP_STATE_PORT(pyb_can_obj_all)[can_idx]; } if (!self->is_enabled || n_args > 1) { @@ -382,25 +319,7 @@ static mp_obj_t pyb_can_restart(mp_obj_t self_in) { if (!self->is_enabled) { mp_raise_ValueError(NULL); } - CAN_TypeDef *can = self->can.Instance; - #if MICROPY_HW_ENABLE_FDCAN - can->CCCR |= FDCAN_CCCR_INIT; - while ((can->CCCR & FDCAN_CCCR_INIT) == 0) { - } - can->CCCR |= FDCAN_CCCR_CCE; - while ((can->CCCR & FDCAN_CCCR_CCE) == 0) { - } - can->CCCR &= ~FDCAN_CCCR_INIT; - while ((can->CCCR & FDCAN_CCCR_INIT)) { - } - #else - can->MCR |= CAN_MCR_INRQ; - while ((can->MSR & CAN_MSR_INAK) == 0) { - } - can->MCR &= ~CAN_MCR_INRQ; - while ((can->MSR & CAN_MSR_INAK)) { - } - #endif + can_restart(&self->can); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_1(pyb_can_restart_obj, pyb_can_restart); @@ -419,33 +338,19 @@ static MP_DEFINE_CONST_FUN_OBJ_1(pyb_can_state_obj, pyb_can_state); // Get info about error states and TX/RX buffers static mp_obj_t pyb_can_info(size_t n_args, const mp_obj_t *args) { pyb_can_obj_t *self = MP_OBJ_TO_PTR(args[0]); - mp_obj_list_t *list = mp_obj_list_optional_arg(n_args > 1 ? args[1] : NULL, 8); + mp_obj_list_t *list = mp_obj_list_optional_arg(n_args > 1 ? args[1] : mp_const_none, 8); + can_counters_t hw_counters; - #if MICROPY_HW_ENABLE_FDCAN - FDCAN_GlobalTypeDef *can = self->can.Instance; - uint32_t esr = can->ECR; - list->items[0] = MP_OBJ_NEW_SMALL_INT((esr & FDCAN_ECR_TEC_Msk) >> FDCAN_ECR_TEC_Pos); - list->items[1] = MP_OBJ_NEW_SMALL_INT((esr & FDCAN_ECR_REC_Msk) >> FDCAN_ECR_REC_Pos); - list->items[2] = MP_OBJ_NEW_SMALL_INT(self->num_error_warning); - list->items[3] = MP_OBJ_NEW_SMALL_INT(self->num_error_passive); - list->items[4] = MP_OBJ_NEW_SMALL_INT(self->num_bus_off); - uint32_t TXEFS = can->TXEFS; - list->items[5] = MP_OBJ_NEW_SMALL_INT(TXEFS & 0x7); - list->items[6] = MP_OBJ_NEW_SMALL_INT((can->RXF0S & FDCAN_RXF0S_F0FL_Msk) >> FDCAN_RXF0S_F0FL_Pos); - list->items[7] = MP_OBJ_NEW_SMALL_INT((can->RXF1S & FDCAN_RXF1S_F1FL_Msk) >> FDCAN_RXF1S_F1FL_Pos); - #else - CAN_TypeDef *can = self->can.Instance; - uint32_t esr = can->ESR; - list->items[0] = MP_OBJ_NEW_SMALL_INT(esr >> CAN_ESR_TEC_Pos & 0xff); - list->items[1] = MP_OBJ_NEW_SMALL_INT(esr >> CAN_ESR_REC_Pos & 0xff); + can_get_counters(&self->can, &hw_counters); + + list->items[0] = MP_OBJ_NEW_SMALL_INT(hw_counters.tec); + list->items[1] = MP_OBJ_NEW_SMALL_INT(hw_counters.rec); list->items[2] = MP_OBJ_NEW_SMALL_INT(self->num_error_warning); list->items[3] = MP_OBJ_NEW_SMALL_INT(self->num_error_passive); list->items[4] = MP_OBJ_NEW_SMALL_INT(self->num_bus_off); - int n_tx_pending = 0x01121223 >> ((can->TSR >> CAN_TSR_TME_Pos & 7) << 2) & 0xf; - list->items[5] = MP_OBJ_NEW_SMALL_INT(n_tx_pending); - list->items[6] = MP_OBJ_NEW_SMALL_INT(can->RF0R >> CAN_RF0R_FMP0_Pos & 3); - list->items[7] = MP_OBJ_NEW_SMALL_INT(can->RF1R >> CAN_RF1R_FMP1_Pos & 3); - #endif + list->items[5] = MP_OBJ_NEW_SMALL_INT(hw_counters.tx_pending); + list->items[6] = MP_OBJ_NEW_SMALL_INT(hw_counters.rx_fifo0_pending); + list->items[7] = MP_OBJ_NEW_SMALL_INT(hw_counters.rx_fifo1_pending); return MP_OBJ_FROM_PTR(list); } @@ -455,7 +360,7 @@ static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(pyb_can_info_obj, 1, 2, pyb_can_info) static mp_obj_t pyb_can_any(mp_obj_t self_in, mp_obj_t fifo_in) { pyb_can_obj_t *self = MP_OBJ_TO_PTR(self_in); can_rx_fifo_t fifo = mp_obj_get_int(fifo_in); - return mp_obj_new_bool(can_rx_pending(&self->can, fifo) != 0); + return mp_obj_new_bool(can_is_rx_pending(&self->can, fifo) != 0); } static MP_DEFINE_CONST_FUN_OBJ_2(pyb_can_any_obj, pyb_can_any); @@ -522,13 +427,7 @@ static mp_obj_t pyb_can_send(size_t n_args, const mp_obj_t *pos_args, mp_map_t * } else { tx_msg.BitRateSwitch = FDCAN_BRS_ON; } - // Roundup DataLength to next DLC size and encode to DLC. - for (mp_uint_t i = 0; i < MP_ARRAY_SIZE(DLCtoBytes); i++) { - if (bufinfo.len <= DLCtoBytes[i]) { - tx_msg.DataLength = (i << 16); - break; - } - } + tx_msg.DataLength = bufinfo.len; // Converted to DLC encoding inside can_transmit #else tx_msg.DLC = bufinfo.len; uint8_t *tx_data = tx_msg.Data; // Data is uint32_t but holds only 1 byte @@ -602,7 +501,7 @@ static mp_obj_t pyb_can_recv(size_t n_args, const mp_obj_t *pos_args, mp_map_t * // Manage the rx state machine if ((fifo == CAN_RX_FIFO0 && self->rxcallback0 != mp_const_none) || (fifo == CAN_RX_FIFO1 && self->rxcallback1 != mp_const_none)) { - bool fifo_empty = can_rx_pending(&self->can, fifo) == 0; + bool fifo_empty = can_is_rx_pending(&self->can, fifo) == 0; byte *state = (fifo == CAN_RX_FIFO0) ? &self->rx_state0 : &self->rx_state1; switch (*state) { case RX_STATE_FIFO_EMPTY: @@ -879,20 +778,6 @@ static mp_obj_t pyb_can_rxcallback(mp_obj_t self_in, mp_obj_t fifo_in, mp_obj_t *callback = callback_in; } else if (mp_obj_is_callable(callback_in)) { *callback = callback_in; - uint32_t irq = 0; - if (self->can_id == PYB_CAN_1) { - irq = (fifo == CAN_RX_FIFO0) ? CAN1_RX0_IRQn : CAN1_RX1_IRQn; - #if defined(CAN2) - } else if (self->can_id == PYB_CAN_2) { - irq = (fifo == CAN_RX_FIFO0) ? CAN2_RX0_IRQn : CAN2_RX1_IRQn; - #endif - #if defined(CAN3) - } else { - irq = (fifo == CAN_RX_FIFO0) ? CAN3_RX0_IRQn : CAN3_RX1_IRQn; - #endif - } - NVIC_SetPriority(irq, IRQ_PRI_CAN); - HAL_NVIC_EnableIRQ(irq); can_enable_rx_interrupts(&self->can, fifo, true); } return mp_const_none; @@ -953,8 +838,8 @@ static mp_uint_t can_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, i uintptr_t flags = arg; ret = 0; if ((flags & MP_STREAM_POLL_RD) - && ((can_rx_pending(&self->can, 0) != 0) - || (can_rx_pending(&self->can, 1) != 0))) { + && ((can_is_rx_pending(&self->can, 0) != 0) + || (can_is_rx_pending(&self->can, 1) != 0))) { ret |= MP_STREAM_POLL_RD; } #if MICROPY_HW_ENABLE_FDCAN @@ -974,12 +859,15 @@ static mp_uint_t can_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, i // IRQ handler, called from lower layer can.c or fdcan.c in ISR context -void can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo) { +void pyb_can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo) { mp_obj_t callback; pyb_can_obj_t *self; byte *state; self = MP_STATE_PORT(pyb_can_obj_all)[can_id - 1]; + if (self == NULL) { + return; // Should only hit this code path if machine.CAN has enabled interrupt + } if (fifo == CAN_RX_FIFO0) { callback = self->rxcallback0; @@ -1014,7 +902,7 @@ void can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo) { return; default: - return; // Should be unreachable + return; // CAN_INT_TX_COMPLETE is ignored by pyb.CAN } // Run the callback diff --git a/ports/stm32/pyb_can.h b/ports/stm32/pyb_can.h index a82043d7863ed..c548bc9367d35 100644 --- a/ports/stm32/pyb_can.h +++ b/ports/stm32/pyb_can.h @@ -52,7 +52,5 @@ extern const mp_obj_type_t pyb_can_type; void pyb_can_deinit_all(void); void pyb_can_init0(void); -void pyb_can_irq_handler(uint can_id, can_rx_fifo_t fifo, can_int_t interrupt); - #endif #endif diff --git a/tests/extmod_hardware/machine_can2.py b/tests/extmod_hardware/machine_can2.py new file mode 100644 index 0000000000000..0ecced82865c0 --- /dev/null +++ b/tests/extmod_hardware/machine_can2.py @@ -0,0 +1,44 @@ +# Test machine.CAN(1) and machine.CAN(2) using loopback +# +# Single device test, assumes support for loopback and no connections to the CAN pins +# +# This test is ported from tests/ports/stm32/pyb_can2.py + +try: + from machine import CAN + + CAN(2, 125_000) +except (ImportError, ValueError): + print("SKIP") + raise SystemExit + +import time + +# Setting up each CAN peripheral independently is deliberate here, to catch +# catch cases where initialising CAN2 breaks CAN1 + +can1 = CAN(1, 125_000, mode=CAN.MODE_LOOPBACK) +can1.set_filters([(0x100, 0x700, 0)]) + +can2 = CAN(2, 125_000, mode=CAN.MODE_LOOPBACK) +can2.set_filters([(0x000, 0x7F0, 0)]) + +# Drain any old messages in RX FIFOs +for can in (can1, can2): + while can.recv(): + pass + +for id, can in ((1, can1), (2, can2)): + print("testing", id) + # message1 should only receive on can1, message2 on can2 + can.send(0x123, b"message1", 0) + can.send(0x003, "message2", 0) + time.sleep_ms(10) + did_recv = False + while res := can.recv(): + did_recv = True + print(hex(res[0]), bytes(res[1]), res[2], res[3]) + if not did_recv: + print("no rx!") + +print("done") diff --git a/tests/extmod_hardware/machine_can2.py.exp b/tests/extmod_hardware/machine_can2.py.exp new file mode 100644 index 0000000000000..bfb6a5088babd --- /dev/null +++ b/tests/extmod_hardware/machine_can2.py.exp @@ -0,0 +1,5 @@ +testing 1 +0x123 b'message1' 0 0 +testing 2 +0x3 b'message2' 0 0 +done diff --git a/tests/extmod_hardware/machine_can_timings.py b/tests/extmod_hardware/machine_can_timings.py new file mode 100644 index 0000000000000..441059f5da546 --- /dev/null +++ b/tests/extmod_hardware/machine_can_timings.py @@ -0,0 +1,60 @@ +# Test machine.CAN timings results +# +# Single device test, assumes no connections to the CAN pins + +try: + from machine import CAN +except ImportError: + print("SKIP") + raise SystemExit + +import unittest + +from target_wiring import can_args, can_kwargs + + +class TestTimings(unittest.TestCase): + def test_bitrate(self): + for bitrate in (125_000, 250_000, 500_000, 1_000_000): + can = CAN(*can_args, bitrate=bitrate, **can_kwargs) + print(can) + timings = can.get_timings() + print(timings) + # Actual bitrate may not be exactly equal to requested rate + self.assertAlmostEqual(timings[0], bitrate, delta=1_000) + can.deinit() + + def test_sample_point(self): + # Verify that tseg1 and tseg2 are set correctly from the sample_point argument + for sample_point in (66, 75, 95): + can = CAN(*can_args, bitrate=500_000, sample_point=sample_point, **can_kwargs) + _bitrate, _sjw, tseg1, tseg2, _fd, _port = can.get_timings() + print(f"sample_point={sample_point}, tseg1={tseg1}, tseg2={tseg2}") + self.assertAlmostEqual(sample_point / 100, tseg1 / (tseg1 + tseg2), delta=0.05) + can.deinit() + + def test_tseg_args(self): + # Verify that tseg1 and tseg2 are set correctly and sample_point is ignored if these are provided + for tseg1, tseg2 in ((5, 2), (16, 8), (16, 5), (15, 5)): + print(f"tseg1={tseg1} tseg2={tseg2}") + can = CAN( + *can_args, bitrate=250_000, tseg1=tseg1, tseg2=tseg2, sample_point=99, **can_kwargs + ) + bitrate, _sjw, ret_tseg1, ret_tseg2, _fd, _port = can.get_timings() + self.assertEqual(ret_tseg1, tseg1) + self.assertEqual(ret_tseg2, tseg2) + + def test_invalid_timing_args(self): + # Test various kwargs out of their allowed value ranges + with self.assertRaises(ValueError): + CAN(*can_args, bitrate=250_000, tseg1=55, **can_kwargs) + with self.assertRaises(ValueError): + CAN(*can_args, bitrate=500_000, tseg2=9, **can_kwargs) + with self.assertRaises(ValueError): + CAN(*can_args, bitrate=-1, **can_kwargs) + with self.assertRaises(ValueError): + CAN(*can_args, bitrate=500_000, sample_point=101, **can_kwargs) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/multi_extmod/machine_can_01_rxtx_simple.py b/tests/multi_extmod/machine_can_01_rxtx_simple.py new file mode 100644 index 0000000000000..fbc7692926d53 --- /dev/null +++ b/tests/multi_extmod/machine_can_01_rxtx_simple.py @@ -0,0 +1,35 @@ +from machine import CAN +import time + +ID_0 = 0x50 +ID_1 = 0x50333 +FLAGS_1 = CAN.FLAG_EXT_ID # ID_1 is an extended CAN ID + +can = CAN(1, 500_000) +can.set_filters(None) # receive all + + +def print_rx(can_id, data, flags, errors): + print(hex(can_id), bytes(data), hex(flags), hex(errors)) + + +def instance0(): + multitest.next() + + # receive from instance1 first + while not (rx := can.recv()): + time.sleep(0) + print_rx(*rx) + + # now send one + can.send(ID_0, b"1234") + + +def instance1(): + multitest.next() + + can.send(ID_1, b"ABCD", FLAGS_1) + + while not (rx := can.recv()): + time.sleep(0) + print_rx(*rx) diff --git a/tests/multi_extmod/machine_can_01_rxtx_simple.py.exp b/tests/multi_extmod/machine_can_01_rxtx_simple.py.exp new file mode 100644 index 0000000000000..479b6809490ca --- /dev/null +++ b/tests/multi_extmod/machine_can_01_rxtx_simple.py.exp @@ -0,0 +1,4 @@ +--- instance0 --- +0x50333 b'ABCD' 0x2 0x0 +--- instance1 --- +0x50 b'1234' 0x0 0x0 diff --git a/tests/multi_extmod/machine_can_02_rx_callback.py b/tests/multi_extmod/machine_can_02_rx_callback.py new file mode 100644 index 0000000000000..80b13dcd622d6 --- /dev/null +++ b/tests/multi_extmod/machine_can_02_rx_callback.py @@ -0,0 +1,122 @@ +from machine import CAN +import time + +# Test the CAN.IRQ_RX irq handler, including overflow + +rx_overflow = False +rx_full = False +received = [] + +# CAN IDs +ID_SPAM = 0x345 # messages spammed into the receive FIFO +ID_ACK_OFLOW = 0x055 # message the receiver sends after it's seen an overflow +ID_AFTER = 0x100 # message the sender sends after the ACK + +can = CAN(1, 500_000) + + +# A very basic "soft" receiver handler that stores received messages into a global list +def receiver_irq_recv(can): + global rx_overflow, rx_full + + assert can.irq().flags() & can.IRQ_RX # the only enabled IRQ + + can_id, data, _flags, errors = can.recv() + + received.append((can_id, None)) + + # The FIFO is expected not to overflow by itself, wait until 40 messages + # have been received and then block the receive handler to induce an overflow + if len(received) == 40: + assert not rx_overflow # shouldn't have already happened, either + time.sleep_ms(500) + + if not rx_overflow and (errors & CAN.RECV_ERR_OVERRUN): + # expected this should happen on the very next message after + # the one where we slept for 500ms + print("irq_recv overrun", len(received)) + received.clear() # check we still get some messages, see rx_spam print line below + rx_overflow = True + + # also expect the FIFO to be FULL again immediately after overrunning and rx_overflow event + if rx_overflow and (errors & CAN.RECV_ERR_OVERRUN | CAN.RECV_ERR_FULL) == CAN.RECV_ERR_FULL: + rx_full = True + + +# Receiver +def instance0(): + can.irq(receiver_irq_recv, trigger=can.IRQ_RX, hard=False) + + can.set_filters(None) # receive all + + multitest.next() + + while not rx_overflow: + pass # Resume ASAP after FIFO0 overflows + + can.send(ID_ACK_OFLOW, b"overflow") + + # at least one ID_SPAM message should have been received + # *after* we overflowed and 'received' was clear in the irq handler + print("rx_spam", any(r[0] == ID_SPAM for r in received)) + + # wait until the "after" message is received + for n in range(100): + if any(r[0] == ID_AFTER for r in received): + break + time.sleep_ms(10) + + can.irq(None) # disable the IRQ + received.clear() + + # at some point while waiting for ID_AFTER the FIFO should have gotten + # full again + print("rx_full", rx_full) + + # now IRQ is disabled, no new messages should be received + time.sleep_ms(250) + print("len", len(received)) + + +received_ack = False + +# reusing the result buffer so sender_irq_recv can be 'hard' +sender_irq_result = [None, memoryview(bytearray(64)), None, None] + + +def sender_irq_recv(can): + global received_ack + + assert can.irq().flags() & can.IRQ_RX # the only enabled IRQ + + can_id, data, _flags, _errors = can.recv(sender_irq_result) + print("sender_irq_recv", can_id, len(data)) # should be ID_ACK_OFLOW and "overflow" payload + received_ack = True + + +# Sender +def instance1(): + can.irq(sender_irq_recv, CAN.IRQ_RX, hard=True) + + can.set_filters(None) + + multitest.next() + + # Spam out messages until the receiver tells us its RX FIFO is full. + # + # The RX FIFO on the receiver can vary from 3 deep (BXCAN) to 25 deep (STM32H7), + # so we keep sending to it until we see a CAN message on ID_ACK_OFLOW indicating + # the receiver's FIFO has overflowed + while not received_ack: + for i in range(255): + while can.send(ID_SPAM, bytes([i] * 8)) is None and not received_ack: + # Don't overflow the TX FIFO + time.sleep_ms(1) + if received_ack: + break + + # give the receiver some time to make space in the FIFO + time.sleep_ms(200) + + # send the final message, the receiver should get this one + can.send(ID_AFTER, b"aaaaa") diff --git a/tests/multi_extmod/machine_can_02_rx_callback.py.exp b/tests/multi_extmod/machine_can_02_rx_callback.py.exp new file mode 100644 index 0000000000000..8e4e5dc64750a --- /dev/null +++ b/tests/multi_extmod/machine_can_02_rx_callback.py.exp @@ -0,0 +1,7 @@ +--- instance0 --- +irq_recv overrun 41 +rx_spam True +rx_full True +len 0 +--- instance1 --- +sender_irq_recv 85 8 diff --git a/tests/multi_extmod/machine_can_03_rx_filters.py b/tests/multi_extmod/machine_can_03_rx_filters.py new file mode 100644 index 0000000000000..a56acbbe91eff --- /dev/null +++ b/tests/multi_extmod/machine_can_03_rx_filters.py @@ -0,0 +1,103 @@ +from machine import CAN +import time + +# Test for filtering capabilities + +can = CAN(1, 500_000) + +# IDs and filter phases used for the 'single id' part of the test +SINGLE_EXT_ID = (0x1234_5678, 0x1FFF_FFFF, CAN.FLAG_EXT_ID) +SINGLE_STD_ID = (0x505, 0x7FF, 0) +SINGLE_ID_PHASES = [ + ("single ext id", [SINGLE_EXT_ID]), + ("single std id", [SINGLE_STD_ID]), + ("ext+std ids", [SINGLE_EXT_ID, SINGLE_STD_ID]), + ("std+ext ids", [SINGLE_STD_ID, SINGLE_EXT_ID]), # these two should be equivalent + ("accept none", []), + ("accept all", None), + ("accept none again", ()), +] + + +# Receiver +def receiver_irq_recv(can): + assert can.irq().flags() & can.IRQ_RX # the only enabled IRQ + can_id, data, _flags, _errors = can.recv() + print("recv", hex(can_id), data.hex()) + + +def instance0(): + can.irq(receiver_irq_recv, trigger=can.IRQ_RX, hard=False) + + multitest.next() + + # Configure to receive standard frames (in a range), and + # extended frames (in a range). + can.set_filters([(0x300, 0x300, 0), (0x3000, 0x3000, CAN.FLAG_EXT_ID)]) + multitest.broadcast("ready id ranges") + + # Run through the phases of filtering for individual IDs + for phase, filters in SINGLE_ID_PHASES: + multitest.wait("configure " + phase) + if filters and len(filters) > CAN.FILTERS_MAX: + # this check really exists to add test coverage for the FILTERS_MAX constant + print("Warning: Too many filters for hardware!") + can.set_filters(filters) + print("receiver configured " + phase) + multitest.broadcast("ready " + phase) + + multitest.wait("Sender done") + + +def send_messages(messages): + for can_id, payload, flags in messages: + r = can.send(can_id, payload, flags) + if r is None: + print("Failed to send:", hex(can_id), payload.hex()) + time.sleep_ms(5) # avoid flooding either our or the receiver's FIFO + + +# Sender +def instance1(): + multitest.next() + multitest.wait("ready id ranges") + + print("Sending ID ranges...") + for i in range(3): + send_messages( + [ + (0x345, bytes([i, 0xFF] * (i + 1)), 0), + (0x3700 + i, bytes([0xEE] * (i + 1)), CAN.FLAG_EXT_ID), + (0x123, b"abcdef", 0), # matches no filter, expect ACKed but not received + ] + ) + + # Now move on to single ID filtering + + single_id_messages = [ + (0x1234_5678, b"\x01\x02\x03\x04\x05", CAN.FLAG_EXT_ID), # matches ext id + (0x0234_5678, b"\x00\x00", CAN.FLAG_EXT_ID), # no match + (0x678, b"\x00\x01", 0), # no match + (0x505, b"\x06\x07\x08\x09\x0a\x0b", 0), # matches standard id + (0x345, b"\x00\x02", 0), # no match (in prev filter) + (0x1234_5679, b"\x00\x03", CAN.FLAG_EXT_ID), # no match + (0x3705, b"\x00\x04", CAN.FLAG_EXT_ID), # no match (in prev filter) + (0x1234_5678, b"\x01\x02\x03", CAN.FLAG_EXT_ID), # matches ext id + (0x505, b"\x04\x05\x06", 0), # matches standard id + (0x505, b"\x00\x05", CAN.FLAG_EXT_ID), # no match (is ext id) + (0x507, b"\x00\x06", 0), # no match + (0x1334_5678, b"\x00\x07", CAN.FLAG_EXT_ID), # no match + (0x1234_5670, b"\x00\x08", CAN.FLAG_EXT_ID), # no match + ] + + # Send the same list of messages for each phase of the test. + # The receiver will have configured different filters, and the .exp + # file is what selects which messages should be received or not. + for phase, _ in SINGLE_ID_PHASES: + multitest.broadcast("configure " + phase) + multitest.wait("ready " + phase) + print("Sending for " + phase + "...") + send_messages(single_id_messages) + + print("Sender done") + multitest.broadcast("Sender done") diff --git a/tests/multi_extmod/machine_can_03_rx_filters.py.exp b/tests/multi_extmod/machine_can_03_rx_filters.py.exp new file mode 100644 index 0000000000000..d55c0c97d159a --- /dev/null +++ b/tests/multi_extmod/machine_can_03_rx_filters.py.exp @@ -0,0 +1,49 @@ +--- instance0 --- +recv 0x345 00ff +recv 0x3700 ee +recv 0x345 01ff01ff +recv 0x3701 eeee +recv 0x345 02ff02ff02ff +recv 0x3702 eeeeee +receiver configured single ext id +recv 0x12345678 0102030405 +recv 0x12345678 010203 +receiver configured single std id +recv 0x505 060708090a0b +recv 0x505 040506 +receiver configured ext+std ids +recv 0x12345678 0102030405 +recv 0x505 060708090a0b +recv 0x12345678 010203 +recv 0x505 040506 +receiver configured std+ext ids +recv 0x12345678 0102030405 +recv 0x505 060708090a0b +recv 0x12345678 010203 +recv 0x505 040506 +receiver configured accept none +receiver configured accept all +recv 0x12345678 0102030405 +recv 0x2345678 0000 +recv 0x678 0001 +recv 0x505 060708090a0b +recv 0x345 0002 +recv 0x12345679 0003 +recv 0x3705 0004 +recv 0x12345678 010203 +recv 0x505 040506 +recv 0x505 0005 +recv 0x507 0006 +recv 0x13345678 0007 +recv 0x12345670 0008 +receiver configured accept none again +--- instance1 --- +Sending ID ranges... +Sending for single ext id... +Sending for single std id... +Sending for ext+std ids... +Sending for std+ext ids... +Sending for accept none... +Sending for accept all... +Sending for accept none again... +Sender done diff --git a/tests/multi_extmod/machine_can_04_tx_order.py b/tests/multi_extmod/machine_can_04_tx_order.py new file mode 100644 index 0000000000000..204bdafd59da8 --- /dev/null +++ b/tests/multi_extmod/machine_can_04_tx_order.py @@ -0,0 +1,170 @@ +from machine import CAN +import time +from random import seed, randrange + +import micropython + +micropython.alloc_emergency_exception_buf(256) +seed(0) + +# Testing that transmit order obeys the priority ordering + +ID_LOW = 0x500 +ID_HIGH = 0x200 + +NUM_MSGS = 255 + +MSG_LEN = 4 + +can = CAN(1, 500_000) + + +def check_sequence(items, label): + # The full range of NUM_MSGS values should have been received or sent, in + # order, without duplicates + if len(items) != NUM_MSGS: + print(label, "wrong count", len(items), "vs", NUM_MSGS) + if items == list(range(NUM_MSGS)): + print(label, "OK") + else: + print(label, "error:") + print(items) + + +# Receiver + +# lists of received messages, one list per ID +received_low = [] +received_high = [] + + +def irq_recv(can): + while can.irq().flags() & can.IRQ_RX: + can_id, data, flags, _errors = can.recv() + + if can_id == ID_LOW and len(data) == MSG_LEN: + received_low.append(data[0]) + elif can_id == ID_HIGH and len(data) == MSG_LEN: + received_high.append(data[0]) + else: + print("unexpected recv", can_id, data, flags) + + +def instance0(): + can.irq(irq_recv, trigger=can.IRQ_RX, hard=False) + can.set_filters(None) # receive all + + multitest.next() + + multitest.wait("sender done") + check_sequence(received_low, "Low prio received") + check_sequence(received_high, "High prio received") + + +# Sender + +## Messages pending to send +pending_low = list(range(NUM_MSGS)) +pending_high = list(range(NUM_MSGS)) + +# List of the messages currently queued to send +tx_queue = [None] * CAN.TX_QUEUE_LEN + +# Messages sent, recorded in order as [high_prio, val] +sent = [] +for _ in range(NUM_MSGS * 2): + sent.append([None, None]) +num_sent = 0 + + +def irq_send(can): + global num_sent + + while flags := can.irq().flags(): + assert flags & can.IRQ_TX # the only enabled IRQ + + idx = (flags >> can.IRQ_TX_IDX_SHIFT) & can.IRQ_TX_IDX_MASK + success = not (flags & can.IRQ_TX_FAILED) + + if not success: + return # We don't worry about failures here + + if not tx_queue[idx]: + print("bad done", idx, success) + return + + was_high, val = tx_queue[idx] + tx_queue[idx] = None + sent[num_sent][0] = was_high + sent[num_sent][1] = val + num_sent += 1 + + +def instance1(): + # note: this test can pass with hard=True, but in a debug build + # the completion IRQ may race ahead of setting tx_queue[idx], below + can.irq(irq_send, trigger=can.IRQ_TX, hard=False) + data = bytearray(MSG_LEN) + + multitest.next() + + while pending_low or pending_high: + if pending_high: + val = pending_high.pop(0) + data[0] = val + data[1] = 1 + while True: + idx = can.send(ID_HIGH, data) + if idx is None: + continue # keep trying until a queue spot opens up + old = tx_queue[idx] + tx_queue[idx] = (True, val) + if old: + print("error high priority queue race", idx, val, old) + break + + for _ in range(randrange(4)): + # Try and queue many low priority messages (expecting most will fail) + if pending_low: + val = pending_low[0] + data[0] = val + data[1] = 0 + idx = can.send(ID_LOW, data) + if idx is None: + # don't retry indefinitely for low priority messages + continue + + old = tx_queue[idx] + tx_queue[idx] = (False, val) + pending_low.pop(0) + if old is not None: + print("error low priority queue race", idx, val, old) + + print("waiting for tx queue to empty...") + while any(x is not None for x in tx_queue): + pass + + multitest.broadcast("sender done") + + # Check we sent the right number of messages + if num_sent != 2 * NUM_MSGS: + print("Sent %d expected %d" % (num_sent, 2 * NUM_MSGS)) + else: + print("Sent right number of messages") + + # Check the low and high priority messages all arrived in order + sent_low = [val for (prio, val) in sent[:num_sent] if prio == False] + sent_high = [val for (prio, val) in sent[:num_sent] if prio == True] + check_sequence(sent_low, "Low prio sent") + check_sequence(sent_high, "High prio sent") + + # check that high priority message queue items always stayed ahead of the low priority + high_val = -1 + for idx, (prio, val) in enumerate(sent): + if prio: + high_val = val + elif high_val <= val and val < NUM_MSGS - 1: + print( + "Low priority message %d overtook high priority %d at index %d" + % (val, high_val, idx) + ) diff --git a/tests/multi_extmod/machine_can_04_tx_order.py.exp b/tests/multi_extmod/machine_can_04_tx_order.py.exp new file mode 100644 index 0000000000000..a2de6ee27063d --- /dev/null +++ b/tests/multi_extmod/machine_can_04_tx_order.py.exp @@ -0,0 +1,8 @@ +--- instance0 --- +Low prio received OK +High prio received OK +--- instance1 --- +waiting for tx queue to empty... +Sent right number of messages +Low prio sent OK +High prio sent OK diff --git a/tests/multi_extmod/machine_can_05_tx_prio_cancel.py b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py new file mode 100644 index 0000000000000..64756a1a1af2e --- /dev/null +++ b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py @@ -0,0 +1,118 @@ +from machine import CAN +import time + +# Check that cancelling a low priority outgoing message and replacing it with a +# high priority message causes it to be transmitted successfully onto a busy bus + +recv = [] + +ITERS = 5 + +can = CAN(1, 500_000) + + +def irq_recv(can): + global recv_std_id + while can.irq().flags() & can.IRQ_RX: + can_id, data, flags, _errors = can.recv() + assert flags & CAN.FLAG_EXT_ID # test uses all extended IDs + if len(recv) < ITERS: + recv.append(can_id) + + +def instance0(): + can.irq(irq_recv, trigger=can.IRQ_RX, hard=False) + can.set_filters(None) # receive all + + multitest.next() + + # "Babble" medium priority messages onto the bus to prevent + # instance1() from sending anything lower priority than this + while len(recv) < ITERS: + for id in range(0x5000, 0x6000): + can.send(id, b"BABBLE", CAN.FLAG_EXT_ID) + if len(recv) >= ITERS: + break + + print("received", ITERS, "messages") + for can_id in recv: + print(hex(can_id)) # should be the high priority messages from instance1, only + + multitest.wait("sender done") + print("done") + + +last_idx = 0 +total_cancels = 0 +total_sent = 0 + + +def irq_send(can): + global total_cancels, total_sent + + while flags := can.irq().flags(): + assert flags & can.IRQ_TX # the only enabled IRQ + + idx = (flags >> can.IRQ_TX_IDX_SHIFT) & can.IRQ_TX_IDX_MASK + + if flags & can.IRQ_TX_FAILED: + # we should only see failed transmits due to cancels in buffer 'last_idx' + assert idx == last_idx + total_cancels += 1 + else: + # this includes the messages we explicitly send, plus queued low + # priority messages once the receiver stops 'babbling' on the bus + total_sent += 1 + + +def instance1(): + global last_idx + can.irq(irq_send, trigger=can.IRQ_TX, hard=True) + + multitest.next() + + for i in range(ITERS): + # Fill the transmit queue with low priority messages (all extended IDs) + last_idx = 0 + if i < 3: + # For the first 3 iterations, send unique message IDs + id_range = range(0x7000, 0x7FFF) + flags = CAN.FLAG_EXT_ID + else: + # For the last iterations, repeat the same ID but tell controller to ignore + # ordering (allows it to queue more than one despite hardware limitations) + id_range = [0x50000 + i] * CAN.TX_QUEUE_LEN + flags = CAN.FLAG_EXT_ID | CAN.FLAG_UNORDERED + + for id in id_range: + idx = can.send(id, b"LOWPRIO", flags) + if idx is None: + break # send queue is full, stop trying to send + last_idx = idx + + time.sleep_ms(50) # the send queue shouldn't empty as instance0 is "babbling" + + # try and cancel the last message we queued + res = can.cancel_send(last_idx) + print(i, "cancel result", res) + + # send a high priority message, that we expect to go out + idx = can.send(0x500 + i, b"HIPRIO", CAN.FLAG_EXT_ID) + print(i, "send result", idx is not None) + + # make sure this message is sent onto the bus + time.sleep_ms(1) + + multitest.broadcast("sender done") + + # let the entire transmit queue drain, now instance0 should have gone quiet + time.sleep_ms(50) + + print("total cancels", total_cancels) # should equal ITERS + + if total_sent == CAN.TX_QUEUE_LEN - 1 + ITERS: + # expect we send one message for each of ITERS, plus all low priority + # queued messages once instance0 stops babbling on the bus + print("total sent OK") + else: + print("total sent", total_sent, CAN.TX_QUEUE_LEN - 1 + ITERS) diff --git a/tests/multi_extmod/machine_can_05_tx_prio_cancel.py.exp b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py.exp new file mode 100644 index 0000000000000..26bab097c4471 --- /dev/null +++ b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py.exp @@ -0,0 +1,21 @@ +--- instance0 --- +received 5 messages +0x500 +0x501 +0x502 +0x503 +0x504 +done +--- instance1 --- +0 cancel result True +0 send result True +1 cancel result True +1 send result True +2 cancel result True +2 send result True +3 cancel result True +3 send result True +4 cancel result True +4 send result True +total cancels 5 +total sent OK diff --git a/tests/multi_extmod/machine_can_06_remote_req.py b/tests/multi_extmod/machine_can_06_remote_req.py new file mode 100644 index 0000000000000..4a1414c0946ef --- /dev/null +++ b/tests/multi_extmod/machine_can_06_remote_req.py @@ -0,0 +1,70 @@ +from machine import CAN +import time + +# Test CAN remote transmission requests + +ID = 0x750 + +FILTER_ID = 0x101 +FILTER_MASK = 0x7FF + +can = CAN(1, 500_000) + + +def receiver_irq_recv(can): + assert can.irq().flags() & can.IRQ_RX # the only enabled IRQ + can_id, data, flags, _errors = can.recv() + is_rtr = flags == CAN.FLAG_RTR + print( + "recv", + hex(can_id), + is_rtr, + len(data) if is_rtr else bytes(data), + ) + if is_rtr: + # The 'data' response of a remote request should be all zeroes + assert bytes(data) == b"\x00" * len(data) + + +# Receiver +def instance0(): + can.irq(receiver_irq_recv, trigger=can.IRQ_RX, hard=False) + can.set_filters(None) # receive all + + multitest.next() + + multitest.wait("enable filter") + can.set_filters([(FILTER_ID, FILTER_MASK, 0)]) + multitest.broadcast("filter set") + + multitest.wait("done") + + +# Sender +def instance1(): + multitest.next() + + can.send(ID, b"abc", CAN.FLAG_RTR) # length==3 remote request + time.sleep_ms(5) + can.send(ID, b"abc", 0) # regular message using the same ID + time.sleep_ms(5) + can.send(ID, b"abcde", CAN.FLAG_RTR) # length==5 remote request + time.sleep_ms(5) + + multitest.broadcast("enable filter") + multitest.wait("filter set") + + # these two messages should be filtered out + can.send(ID, b"abc", CAN.FLAG_RTR) # length==3 remote request + time.sleep_ms(5) + can.send(ID, b"abc", 0) # regular message using the same ID + time.sleep_ms(5) + + # these messages should be filtered in + can.send(FILTER_ID, b"def", CAN.FLAG_RTR) # length==3 remote request + time.sleep_ms(5) + can.send(FILTER_ID, b"hij", 0) # regular message using the same ID + time.sleep_ms(5) + + multitest.broadcast("done") + print("done") diff --git a/tests/multi_extmod/machine_can_06_remote_req.py.exp b/tests/multi_extmod/machine_can_06_remote_req.py.exp new file mode 100644 index 0000000000000..fe94508ba3da2 --- /dev/null +++ b/tests/multi_extmod/machine_can_06_remote_req.py.exp @@ -0,0 +1,8 @@ +--- instance0 --- +recv 0x750 True 3 +recv 0x750 False b'abc' +recv 0x750 True 5 +recv 0x101 True 3 +recv 0x101 False b'hij' +--- instance1 --- +done diff --git a/tests/multi_extmod/machine_can_07_error_states.py b/tests/multi_extmod/machine_can_07_error_states.py new file mode 100644 index 0000000000000..e7eec513ee169 --- /dev/null +++ b/tests/multi_extmod/machine_can_07_error_states.py @@ -0,0 +1,205 @@ +from machine import CAN +from micropython import const +import time + +# Test that without a second CAN node on the network the controller will +# correctly go into the correct error states, and can then recover. +# +# Note this test depends on no other CAN node being active on the network apart +# from the two test instances. (Although it's OK for extra nodes to be in a +# "listen only" mode where they won't ACK messages.) + +rx_overflow = False +rx_full = False +received = [] + +# CAN IDs +_ID = const(0x100) + +# can.state() result list indexes, as constants +_IDX_TEC = const(0) +_IDX_REC = const(1) +_IDX_NUM_WARNING = const(2) +_IDX_NUM_PASSIVE = const(3) +_IDX_NUM_BUS_OFF = const(4) +_IDX_PEND_TX = const(5) + +can = CAN(1, 500_000) + + +def state_name(state): + for name in dir(CAN): + if name.startswith("STATE_") and state == getattr(CAN, name): + return name + return f"UNKNOWN-{state}" + + +def irq_recv(can): + while can.irq().flags() & can.IRQ_RX: + can_id, data, _flags, errors = can.recv() + print("recv", hex(can_id), data.hex()) + + +# Receiver +def instance0(): + can.irq(irq_recv, trigger=can.IRQ_RX, hard=False) + + can.set_filters(None) # receive all + + multitest.next() + + # Receive at least one CAN message before sender asks us to disable the controller + + multitest.wait("disable receiver") + can.deinit() + print("can deinit()") + multitest.broadcast("receiver disabled") + + # Wait for the sender to tell us to re-enable + + multitest.wait("enable receiver") + # note the irq is no longer active after deinit() + can.init(500_000) + print("can init()") + multitest.broadcast("receiver enabled") + + # Receive CAN messages until the sender asks us to switch to an invalid baud rate + + multitest.wait("switch baud") + can.init(125_000) + print("can switch baud") + multitest.broadcast("switched baud") + + time.sleep_ms(1) + + print("sending bad msg") + # trying to send this frame should introduce more bus errors + idx_bad = can.send(_ID, b"BADBAUD") + + multitest.wait("fix baud") + print("sending cancelling") + print("cancelled", can.cancel_send(idx_bad)) + print("re-init") + can.init(500_000) + multitest.broadcast("fixed baud") + + # Should be receiving CAN messages OK again + + print("done") + + +def irq_sender(can): + while flags := can.irq().flags(): + if flags & can.IRQ_STATE: + print("irq state", can.state()) + if flags & can.IRQ_TX: + print("irq sent", not (flags & can.IRQ_TX_FAILED)) + + +# Sender +def instance1(): + can.irq(irq_sender, CAN.IRQ_STATE | CAN.IRQ_TX, hard=False) + + can.set_filters(None) + + multitest.next() + + print("started", state_name(can.state())) # should be ERROR_ACTIVE + + # Send a single message to the receiver, to verify it's working + can.send(_ID, b"PAYLOAD") + active_counters = can.get_counters() + # print(active_counters) # DEBUG + + multitest.broadcast("disable receiver") + multitest.wait("receiver disabled") + + # Now the receiver shouldn't be ACKing our frames, queue will stay full + # ... we will get the ISR for ERROR_WARNING but it'll go from ERROR_WARNING to ERROR_PASSIVE + # very quickly as all messages are failing + can.send(_ID, b"MORE") + while can.state() in (CAN.STATE_ACTIVE, CAN.STATE_WARNING): + pass + + print(state_name(can.state())) # should be ERROR_PASSIVE now + passive_counters = can.get_counters() + # print(passive_counters) # DEBUG + print("tec increased", passive_counters[_IDX_TEC] > active_counters[_IDX_TEC]) + print("tec over thresh", passive_counters[_IDX_TEC] >= 128) + # we should have counted exactly one ERROR_WARNING and ERROR_PASSIVE transition + print( + "counted warning", + passive_counters[_IDX_NUM_WARNING] == active_counters[_IDX_NUM_WARNING] + 1, + ) + print( + "counted passive", + passive_counters[_IDX_NUM_PASSIVE] == active_counters[_IDX_NUM_PASSIVE] + 1, + ) + print("some pending tx", passive_counters[_IDX_PEND_TX] > 0) + + # Re-enable the receiver which should allow us to go from ERROR_PASSIVE to ERROR_WARNING + multitest.broadcast("enable receiver") + multitest.wait("receiver enabled") + + can.send(_ID, b"MORE") + while can.state() == CAN.STATE_PASSIVE: + pass + + print(state_name(can.state())) # should be ERROR_WARNING now + warning_counters = can.get_counters() + # print(warning_counters) # DEBUG + print("tec decreased", warning_counters[_IDX_TEC] < passive_counters[_IDX_TEC]) + print( + "tec below thresh", warning_counters[_IDX_TEC] < 128 + ) # and should be more than error passive threshold + # error warning count should stay the same, as we went "down" in severity not up + print( + "no new warning", warning_counters[_IDX_NUM_WARNING] == passive_counters[_IDX_NUM_WARNING] + ) + print( + "no new passive", warning_counters[_IDX_NUM_PASSIVE] == passive_counters[_IDX_NUM_PASSIVE] + ) + + # Tell the receiver to change to the wrong baud rate, which should create both RX and TX errorxs + multitest.broadcast("switch baud") + multitest.wait("switched baud") + + # queue another message. This will keep trying to send until we revert back to ERROR_PASSIVE + idx = can.send(_ID, b"YETMORE") + print("queued yetmore", idx is not None) + while can.state() != CAN.STATE_PASSIVE: + pass + + print(state_name(can.state())) # should be ERROR_PASSIVE again + passive_counters = can.get_counters() + # print(passive_counters) # DEBUG + # we can't say for sure which error counter will hit the ERROR_PASSIVE threshold first + print( + "one over thresh", passive_counters[_IDX_TEC] >= 128 or passive_counters[_IDX_REC] >= 128 + ) + print( + "no new warning", passive_counters[_IDX_NUM_WARNING] == warning_counters[_IDX_NUM_WARNING] + ) + print( + "counted passive", + passive_counters[_IDX_NUM_PASSIVE] == warning_counters[_IDX_NUM_PASSIVE] + 1, + ) + + # Note that we can't get all the way to the most severe BUS_OFF error state + # with this test setup, as Bus Off requires more than just "normal" frame + # transmit errors. + + # restarting the controller may cause it to leave its error state, or not, depending + # on the implementation - but it shouldn't cause any recovery issues. Also cancels all pending TX + # (note: have to do this before 'fix baud' or we create a race condition for pending tx) + can.restart() + + # tell the receiver to go back to a valid baud rate + multitest.broadcast("fix baud") + multitest.wait("fixed baud") + + idx_more = can.send(_ID, b"MOREMORE") + time.sleep_ms(50) # irq_sender should fire during this window + print("queued moremore", idx_more is not None) + + print("done") diff --git a/tests/multi_extmod/machine_can_07_error_states.py.exp b/tests/multi_extmod/machine_can_07_error_states.py.exp new file mode 100644 index 0000000000000..158ae63bfbc7a --- /dev/null +++ b/tests/multi_extmod/machine_can_07_error_states.py.exp @@ -0,0 +1,37 @@ +--- instance0 --- +recv 0x100 5041594c4f4144 +can deinit() +can init() +can switch baud +sending bad msg +sending cancelling +cancelled True +re-init +done +--- instance1 --- +started STATE_ACTIVE +irq sent True +irq state 2 +irq state 3 +STATE_PASSIVE +tec increased True +tec over thresh True +counted warning True +counted passive True +some pending tx True +irq sent True +irq sent True +STATE_WARNING +tec decreased True +tec below thresh True +no new warning True +no new passive True +irq state 3 +queued yetmore True +STATE_PASSIVE +one over thresh True +no new warning True +counted passive True +irq sent True +queued moremore True +done diff --git a/tests/multi_extmod/machine_can_08_init_mode.py b/tests/multi_extmod/machine_can_08_init_mode.py new file mode 100644 index 0000000000000..459e6180ab62d --- /dev/null +++ b/tests/multi_extmod/machine_can_08_init_mode.py @@ -0,0 +1,102 @@ +from machine import CAN +from micropython import const +import time + +# instance0 transitions through various modes, instance1 +# listens for various messages (or not) +# +# Note this test assumes no other CAN nodes are connected apart from the test +# instances (or if they are connected they must be in silent mode.) +# +# TODO: This test needs to eventually support the case where modes aren't supported +# on a controller, maybe by printing fake output if the mode switch fails? + +# MODE_NORMAL, MODE_SLEEP, MODE_LOOPBACK, MODE_SILENT, MODE_SILENT_LOOPBACK +can = CAN(1, 500_000, mode=CAN.MODE_NORMAL) + +# While instance0 is in Silent mode, instance1 sends a message with this ID +# that will be retried for 100ms (as instance0 won't ACK). So don't print every one. +_SILENT_RX_ID = const(0x53) +silent_rx_count = 0 + + +def irq_print(can): + global silent_rx_count + while flags := can.irq().flags(): + if flags & can.IRQ_RX: + can_id, data, _flags, errors = can.recv() + if can_id != _SILENT_RX_ID: + print("recv", hex(can_id), bytes(data)) + else: + silent_rx_count += 1 + if flags & can.IRQ_TX: # note: only enabled on instance1 to avoid race conditions + print("send", "failed" if flags & can.IRQ_TX_FAILED else "ok") + + +def reinit_with_mode(mode): + can.deinit() + can.init(bitrate=500_000, mode=mode) + can.irq(irq_print, trigger=can.IRQ_RX, hard=False) + can.set_filters(None) # receive all + + +def instance0(): + multitest.next() + multitest.wait("instance1 ready") + + reinit_with_mode(can.MODE_NORMAL) + print("Normal", "MODE_NORMAL" in str(can)) + can.send(0x50, b"Normal") + time.sleep_ms(100) + + # Skipping MODE_SLEEP as means different things on different hardware + + reinit_with_mode(can.MODE_LOOPBACK) + print("Loopback", "MODE_LOOPBACK" in str(can)) + + # This message should go out to the bus, but will also be received by instance0 itself + can.send(0x51, b"Loopback") + time.sleep_ms(100) + + reinit_with_mode(can.MODE_SILENT) + print("Silent", "MODE_SLIENT" in str(can)) + + # This message shouldn't go out onto the bus + idx = can.send(0x52, b"Silent") + multitest.broadcast("silent") + multitest.wait("silent done") + # we should have received the message from instance1 many times, as instance0 won't have ACKed it + print("silent_rx_count", silent_rx_count > 5) + can.cancel_send(idx) + + reinit_with_mode(can.MODE_SILENT_LOOPBACK) + print("Silent Loopback", "MODE_SILENT_LOOPBACK" in str(can)) + + # This message should be received by instance0 only + idx = can.send(0x54, b"SiLoop") + time.sleep_ms(50) + + reinit_with_mode(can.MODE_NORMAL) + print("Normal again", "MODE_NORMAL" in str(can)) + can.send(0x55, b"Normal2") # should be received by instance1 only, again + multitest.broadcast("normal done") + + +# Receiver +def instance1(): + can.irq(irq_print, trigger=can.IRQ_RX | can.IRQ_TX, hard=False) + can.set_filters(None) # receive all + multitest.next() + + multitest.broadcast("instance1 ready") + + # The IRQ does most of the work on this instance + + multitest.wait("silent") + # Sending this message back, it should fail to send as Silent mode won't ACK it + idx = can.send(0x53, b"Silent2") + time.sleep_ms(20) + can.cancel_send(idx) + multitest.broadcast("silent done") + + multitest.wait("normal done") diff --git a/tests/multi_extmod/machine_can_08_init_mode.py.exp b/tests/multi_extmod/machine_can_08_init_mode.py.exp new file mode 100644 index 0000000000000..b9f0f11ae1065 --- /dev/null +++ b/tests/multi_extmod/machine_can_08_init_mode.py.exp @@ -0,0 +1,14 @@ +--- instance0 --- +Normal True +Loopback True +recv 0x51 b'Loopback' +Silent False +silent_rx_count True +Silent Loopback True +recv 0x54 b'SiLoop' +Normal again True +--- instance1 --- +recv 0x50 b'Normal' +recv 0x51 b'Loopback' +send failed +recv 0x55 b'Normal2' diff --git a/tests/ports/stm32/pyb_can.py b/tests/ports/stm32/pyb_can.py index e8a8637566888..8178d91fe74f1 100644 --- a/tests/ports/stm32/pyb_can.py +++ b/tests/ports/stm32/pyb_can.py @@ -188,13 +188,13 @@ else: can.setfilter(0, CAN.MASK, 0, (filter_id, filter_mask), extframe=True) - can.send("ok", id_ok, timeout=3, extframe=True) + can.send("ok", id_ok, timeout=5, extframe=True) pyb.delay(10) if can.any(0): msg = can.recv(0) print((hex(filter_id), hex(filter_mask), hex(msg[0]), msg[1], msg[4])) - can.send("fail", id_fail, timeout=3, extframe=True) + can.send("fail", id_fail, timeout=5, extframe=True) pyb.delay(10) if can.any(0): msg = can.recv(0) diff --git a/tests/run-tests.py b/tests/run-tests.py index 84daf4cbbf8d3..a5659fff8b36f 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -289,6 +289,7 @@ "extmod/machine_spi_rate.py", "extmod/machine_uart_irq_txidle.py", "extmod/machine_uart_tx.py", + "extmod_hardware/machine_can_timings.py", "extmod_hardware/machine_encoder.py", "extmod_hardware/machine_uart_irq_break.py", "extmod_hardware/machine_uart_irq_rx.py", diff --git a/tests/target_wiring/PYBx.py b/tests/target_wiring/PYBx.py index a419320d90279..a825dbb514109 100644 --- a/tests/target_wiring/PYBx.py +++ b/tests/target_wiring/PYBx.py @@ -8,3 +8,7 @@ uart_loopback_kwargs = {} spi_standalone_args_list = [(1,), (2,)] + +# CAN args assume no connection for single device tests +can_args = (1,) +can_kwargs = {} diff --git a/tests/target_wiring/stm32.py b/tests/target_wiring/stm32.py new file mode 100644 index 0000000000000..da73d7fa9a257 --- /dev/null +++ b/tests/target_wiring/stm32.py @@ -0,0 +1,7 @@ +# Target wiring for non-PyBoard stm32 boards +# +# See PYBx.py for PyBoards + +# CAN args assume no connection for single device tests +can_args = (1,) +can_kwargs = {} From c802a13fb69f55febc199135cdb52cf099d450aa Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 19 Mar 2026 17:45:00 +1100 Subject: [PATCH 16/53] stm32: Fix printing value of pyb.CAN auto_restart on FDCAN hardware. The DAR register field is for auto-retransmit, FDCAN doesn't support automatic restart to clear Bus Off. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- ports/stm32/pyb_can.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ports/stm32/pyb_can.c b/ports/stm32/pyb_can.c index a0b4de73bf912..2d676b23d20f5 100644 --- a/ports/stm32/pyb_can.c +++ b/ports/stm32/pyb_can.c @@ -140,7 +140,7 @@ static void pyb_can_print(const mp_print_t *print, mp_obj_t self_in, mp_print_ki self->can_id, mode, #if MICROPY_HW_ENABLE_FDCAN - (self->can.Instance->CCCR & FDCAN_CCCR_DAR) ? MP_QSTR_True : MP_QSTR_False + MP_QSTR_False // auto_restart not supported on FDCAN hardware #else (self->can.Instance->MCR & CAN_MCR_ABOM) ? MP_QSTR_True : MP_QSTR_False #endif From e74f3d5eaac0249e473b00e49852b5cf308fe1e4 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 2 Mar 2026 23:53:10 +1100 Subject: [PATCH 17/53] stm32/timer: Use HAL macro to determine if TIM is 32-bit. Some MCUs (eg N6) have more timers which are 32-bit, and it's best to use this macro to work that out. Signed-off-by: Damien George --- ports/stm32/timer.c | 6 +----- ports/stm32/timer.h | 5 +++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ports/stm32/timer.c b/ports/stm32/timer.c index a0db596307970..1a2188451ecdc 100644 --- a/ports/stm32/timer.c +++ b/ports/stm32/timer.c @@ -1026,15 +1026,11 @@ static mp_obj_t pyb_timer_make_new(const mp_obj_type_t *type, size_t n_args, siz memset(tim, 0, sizeof(*tim)); tim->base.type = &pyb_timer_type; tim->tim_id = tim_id; - #if defined(STM32L1) - tim->is_32bit = tim_id == 5; - #else - tim->is_32bit = tim_id == 2 || tim_id == 5; - #endif tim->callback = mp_const_none; uint32_t ti = tim_instance_table[tim_id - 1]; tim->tim.Instance = (TIM_TypeDef *)(ti & 0xffffff00); tim->irqn = ti & 0xff; + tim->is_32bit = IS_TIM_32B_COUNTER_INSTANCE(tim->tim.Instance); MP_STATE_PORT(pyb_timer_obj_all)[tim_id - 1] = tim; } else { // reference existing Timer object diff --git a/ports/stm32/timer.h b/ports/stm32/timer.h index 2ba91cf158d96..1abcd1b9b4264 100644 --- a/ports/stm32/timer.h +++ b/ports/stm32/timer.h @@ -26,6 +26,11 @@ #ifndef MICROPY_INCLUDED_STM32_TIMER_H #define MICROPY_INCLUDED_STM32_TIMER_H +// Define this helper macro for MCUs that the HAL misses. +#if defined(STM32L0) +#define IS_TIM_32B_COUNTER_INSTANCE(tim) (false) +#endif + extern TIM_HandleTypeDef TIM5_Handle; extern const mp_obj_type_t pyb_timer_type; From 2d3241f34c8d026dadeedc8f7a85f6361153f196 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 2 Mar 2026 23:54:25 +1100 Subject: [PATCH 18/53] stm32/timer: Expose functions to convert id to reg and enable TIM clock. This functionality already exists in the TIM code, and can be reused by the upcoming PWM implementation. Signed-off-by: Damien George --- ports/stm32/timer.c | 232 +++++++++++++++++++++++--------------------- ports/stm32/timer.h | 2 + 2 files changed, 123 insertions(+), 111 deletions(-) diff --git a/ports/stm32/timer.c b/ports/stm32/timer.c index 1a2188451ecdc..c05c4716d475d 100644 --- a/ports/stm32/timer.c +++ b/ports/stm32/timer.c @@ -149,6 +149,8 @@ TIM_HandleTypeDef TIM6_Handle; #define PYB_TIMER_OBJ_ALL_NUM MP_ARRAY_SIZE(MP_STATE_PORT(pyb_timer_obj_all)) +static const uint32_t tim_instance_table[MICROPY_HW_MAX_TIMER]; + static mp_obj_t pyb_timer_deinit(mp_obj_t self_in); static mp_obj_t pyb_timer_callback(mp_obj_t self_in, mp_obj_t callback); static mp_obj_t pyb_timer_channel_callback(mp_obj_t self_in, mp_obj_t callback); @@ -229,6 +231,124 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { #endif } +TIM_TypeDef *timer_id_to_reg(uint32_t tim_id) { + return (TIM_TypeDef *)(tim_instance_table[tim_id - 1] & 0xffffff00); +} + +void timer_clock_enable(size_t tim_id) { + // enable TIM clock + switch (tim_id) { + #if defined(TIM1) + case 1: + __HAL_RCC_TIM1_CLK_ENABLE(); + break; + #endif + case 2: + __HAL_RCC_TIM2_CLK_ENABLE(); + break; + #if defined(TIM3) + case 3: + __HAL_RCC_TIM3_CLK_ENABLE(); + break; + #endif + #if defined(TIM4) + case 4: + __HAL_RCC_TIM4_CLK_ENABLE(); + break; + #endif + #if defined(TIM5) + case 5: + __HAL_RCC_TIM5_CLK_ENABLE(); + break; + #endif + #if defined(TIM6) + case 6: + __HAL_RCC_TIM6_CLK_ENABLE(); + break; + #endif + #if defined(TIM7) + case 7: + __HAL_RCC_TIM7_CLK_ENABLE(); + break; + #endif + #if defined(TIM8) + case 8: + __HAL_RCC_TIM8_CLK_ENABLE(); + break; + #endif + #if defined(TIM9) + case 9: + __HAL_RCC_TIM9_CLK_ENABLE(); + break; + #endif + #if defined(TIM10) + case 10: + __HAL_RCC_TIM10_CLK_ENABLE(); + break; + #endif + #if defined(TIM11) + case 11: + __HAL_RCC_TIM11_CLK_ENABLE(); + break; + #endif + #if defined(TIM12) + case 12: + __HAL_RCC_TIM12_CLK_ENABLE(); + break; + #endif + #if defined(TIM13) + case 13: + __HAL_RCC_TIM13_CLK_ENABLE(); + break; + #endif + #if defined(TIM14) + case 14: + __HAL_RCC_TIM14_CLK_ENABLE(); + break; + #endif + #if defined(TIM15) + case 15: + __HAL_RCC_TIM15_CLK_ENABLE(); + break; + #endif + #if defined(TIM16) + case 16: + __HAL_RCC_TIM16_CLK_ENABLE(); + break; + #endif + #if defined(TIM17) + case 17: + __HAL_RCC_TIM17_CLK_ENABLE(); + break; + #endif + #if defined(TIM18) + case 18: + __HAL_RCC_TIM18_CLK_ENABLE(); + break; + #endif + #if defined(TIM19) + case 19: + __HAL_RCC_TIM19_CLK_ENABLE(); + break; + #endif + #if defined(TIM20) + case 20: + __HAL_RCC_TIM20_CLK_ENABLE(); + break; + #endif + #if defined(TIM21) + case 21: + __HAL_RCC_TIM21_CLK_ENABLE(); + break; + #endif + #if defined(TIM22) + case 22: + __HAL_RCC_TIM22_CLK_ENABLE(); + break; + #endif + } +} + // Get the frequency (in Hz) of the source clock for the given timer. // On STM32F405/407/415/417 there are 2 cases for how the clock freq is set. // If the APB prescaler is 1, then the timer clock is equal to its respective @@ -694,117 +814,7 @@ static mp_obj_t pyb_timer_init_helper(pyb_timer_obj_t *self, size_t n_args, cons init->RepetitionCounter = 0; #endif - // enable TIM clock - switch (self->tim_id) { - #if defined(TIM1) - case 1: - __HAL_RCC_TIM1_CLK_ENABLE(); - break; - #endif - case 2: - __HAL_RCC_TIM2_CLK_ENABLE(); - break; - #if defined(TIM3) - case 3: - __HAL_RCC_TIM3_CLK_ENABLE(); - break; - #endif - #if defined(TIM4) - case 4: - __HAL_RCC_TIM4_CLK_ENABLE(); - break; - #endif - #if defined(TIM5) - case 5: - __HAL_RCC_TIM5_CLK_ENABLE(); - break; - #endif - #if defined(TIM6) - case 6: - __HAL_RCC_TIM6_CLK_ENABLE(); - break; - #endif - #if defined(TIM7) - case 7: - __HAL_RCC_TIM7_CLK_ENABLE(); - break; - #endif - #if defined(TIM8) - case 8: - __HAL_RCC_TIM8_CLK_ENABLE(); - break; - #endif - #if defined(TIM9) - case 9: - __HAL_RCC_TIM9_CLK_ENABLE(); - break; - #endif - #if defined(TIM10) - case 10: - __HAL_RCC_TIM10_CLK_ENABLE(); - break; - #endif - #if defined(TIM11) - case 11: - __HAL_RCC_TIM11_CLK_ENABLE(); - break; - #endif - #if defined(TIM12) - case 12: - __HAL_RCC_TIM12_CLK_ENABLE(); - break; - #endif - #if defined(TIM13) - case 13: - __HAL_RCC_TIM13_CLK_ENABLE(); - break; - #endif - #if defined(TIM14) - case 14: - __HAL_RCC_TIM14_CLK_ENABLE(); - break; - #endif - #if defined(TIM15) - case 15: - __HAL_RCC_TIM15_CLK_ENABLE(); - break; - #endif - #if defined(TIM16) - case 16: - __HAL_RCC_TIM16_CLK_ENABLE(); - break; - #endif - #if defined(TIM17) - case 17: - __HAL_RCC_TIM17_CLK_ENABLE(); - break; - #endif - #if defined(TIM18) - case 18: - __HAL_RCC_TIM18_CLK_ENABLE(); - break; - #endif - #if defined(TIM19) - case 19: - __HAL_RCC_TIM19_CLK_ENABLE(); - break; - #endif - #if defined(TIM20) - case 20: - __HAL_RCC_TIM20_CLK_ENABLE(); - break; - #endif - #if defined(TIM21) - case 21: - __HAL_RCC_TIM21_CLK_ENABLE(); - break; - #endif - #if defined(TIM22) - case 22: - __HAL_RCC_TIM22_CLK_ENABLE(); - break; - #endif - } + timer_clock_enable(self->tim_id); // set IRQ priority (if not a special timer) if (self->tim_id != 5) { diff --git a/ports/stm32/timer.h b/ports/stm32/timer.h index 1abcd1b9b4264..10aa81bd52c73 100644 --- a/ports/stm32/timer.h +++ b/ports/stm32/timer.h @@ -39,6 +39,8 @@ void timer_init0(void); void timer_tim5_init(void); TIM_HandleTypeDef *timer_tim6_init(uint freq); void timer_deinit(void); +TIM_TypeDef *timer_id_to_reg(uint32_t tim_id); +void timer_clock_enable(size_t tim_id); uint32_t timer_get_source_freq(uint32_t tim_id); void timer_irq_handler(uint tim_id); From dbe6a11670c02bc0f08e6f7c53bed59596b12345 Mon Sep 17 00:00:00 2001 From: Damien George Date: Wed, 11 Feb 2026 11:32:41 +1100 Subject: [PATCH 19/53] stm32/machine_pwm: Implement machine.PWM class. This commit implements the standard `machine.PWM` class on stm32, using the common bindings in `extmod/machine_pwm.c`. Features implemented are: - construct a PWM object from a pin, with automatic selection of TIM instance and channel; - get and set freq, duty_u16 and duty_ns; - optionally invert the output. The PWM objects are static objects (partly in ROM, partly in RAM) so creating a PWM instance on the same pin will return exactly the same object. That's consistent with other peripherals in the stm32 port, and consistent with other PWM implementations (eg rp2). When creating a PWM object on a pin, if that pin has multiple TIM instances then only the first will be selected. A future extension could allow selecting the TIM/channel (eg similar to how ADCBlock allows selecting an ADC). Signed-off-by: Damien George --- ports/stm32/machine_pwm.c | 658 +++++++++++++++++++++++++++++++++++++ ports/stm32/main.c | 3 + ports/stm32/modmachine.h | 1 + ports/stm32/mpconfigport.h | 2 + 4 files changed, 664 insertions(+) create mode 100644 ports/stm32/machine_pwm.c diff --git a/ports/stm32/machine_pwm.c b/ports/stm32/machine_pwm.c new file mode 100644 index 0000000000000..611bb80f46f3a --- /dev/null +++ b/ports/stm32/machine_pwm.c @@ -0,0 +1,658 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * Copyright (c) 2026 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// This file is never compiled standalone, it's included directly from +// extmod/machine_pwm.c via MICROPY_PY_MACHINE_PWM_INCLUDEFILE. + +#include "py/mphal.h" +#include "timer.h" + +// tim is a TIMx instance, ch is one of PWM_CHx +#define CCRx(tim, ch) ((&(tim)->CCR1)[(ch)]) + +enum { + #if defined(TIM1) + TIM1_ENABLED, + #endif + #if defined(TIM2) + TIM2_ENABLED, + #endif + #if defined(TIM3) + TIM3_ENABLED, + #endif + #if defined(TIM4) + TIM4_ENABLED, + #endif + #if defined(TIM5) + TIM5_ENABLED, + #endif + // TIM6, TIM7 have no output channels so aren't used for PWM. + #if defined(TIM8) + TIM8_ENABLED, + #endif + #if defined(TIM9) + TIM9_ENABLED, + #endif + #if defined(TIM10) + TIM10_ENABLED, + #endif + #if defined(TIM11) + TIM11_ENABLED, + #endif + #if defined(TIM12) + TIM12_ENABLED, + #endif + #if defined(TIM13) + TIM13_ENABLED, + #endif + #if defined(TIM14) + TIM14_ENABLED, + #endif + #if defined(TIM15) + TIM15_ENABLED, + #endif + #if defined(TIM16) + TIM16_ENABLED, + #endif + #if defined(TIM17) + TIM17_ENABLED, + #endif + #if defined(TIM18) + TIM18_ENABLED, + #endif + #if defined(TIM19) + TIM19_ENABLED, + #endif + #if defined(TIM20) + TIM20_ENABLED, + #endif + #if defined(TIM21) + TIM21_ENABLED, + #endif + #if defined(TIM22) + TIM22_ENABLED, + #endif + NUM_TIMERS, +}; + +enum { + PWM_CH1 = 0, + PWM_CH2 = 1, + PWM_CH3 = 2, + PWM_CH4 = 3, + NUM_CHANNELS_PER_TIMER, +}; + +enum { + PWM_POLARITY_NORMAL, + PWM_POLARITY_INVERTED, +}; + +typedef struct _pwm_t { + uint8_t tim_id; // TIMx id + uint8_t channel; // one of PWM_CH1-PWM_CH4 +} pwm_t; + +static void pwm_init(pwm_t *pwm) { + // The following code assumes each channel has 8 bits in CCMR1/2. + MP_STATIC_ASSERT(TIM_CCMR1_CC1S_Pos + 8 == TIM_CCMR1_CC2S_Pos); + MP_STATIC_ASSERT(TIM_CCMR2_CC3S_Pos + 8 == TIM_CCMR2_CC4S_Pos); + + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + + // Initialise TIM if it's not already running. + if (!(tim->CR1 & TIM_CR1_CEN)) { + timer_clock_enable(pwm->tim_id); + + // Initialise with: clock division 1, up counter mode, auto-reload buffered. + // ARPE allows to smoothly change the frequency of the timer, and prevents + // long silent periods when a 32-bit timer is used and CNT goes beyond ARR. + tim->CR1 = TIM_CR1_ARPE; + + // Invalidate the frequency so `pwm_freq_is_valid()` works. + tim->ARR = 0; + + #if defined(IS_TIM_REPETITION_COUNTER_INSTANCE) + if (IS_TIM_REPETITION_COUNTER_INSTANCE(tim)) { + tim->RCR = 0; + } + #endif + } + + // Configure PWM mode. + uint32_t reg = 6 << TIM_CCMR1_OC1M_Pos // PWM1 mode + | 1 << TIM_CCMR1_OC1PE_Pos // preload enabled + | 0 << TIM_CCMR1_CC1S_Pos // output mode + ; + uint32_t shift = 8 * (pwm->channel & 1); + if (pwm->channel == PWM_CH1 || pwm->channel == PWM_CH2) { + tim->CCMR1 = (tim->CCMR1 & ~(0xff << shift)) | reg << shift; + } else { + tim->CCMR2 = (tim->CCMR2 & ~(0xff << shift)) | reg << shift; + } + + #if defined(IS_TIM_BREAK_INSTANCE) + // Enable master output if needed. + if (IS_TIM_BREAK_INSTANCE(tim)) { + tim->BDTR |= TIM_BDTR_MOE; + } + #endif +} + +static void pwm_set_polarity(pwm_t *pwm, unsigned int polarity) { + // The following code assumes each channel has 4 bits in CCER. + MP_STATIC_ASSERT(TIM_CCER_CC1P_Pos + 4 == TIM_CCER_CC2P_Pos); + + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + unsigned int shift = 4 * pwm->channel; + if (polarity == PWM_POLARITY_NORMAL) { + tim->CCER &= ~(TIM_CCER_CC1P << shift); + } else { + tim->CCER |= TIM_CCER_CC1P << shift; + } +} + +static void pwm_deinit(pwm_t *pwm) { + // The following code assumes each channel has 4 bits in CCER. + MP_STATIC_ASSERT(TIM_CCER_CC1E_Pos + 4 == TIM_CCER_CC2E_Pos); + + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + + // Disable the normal output. + tim->CCER &= ~(TIM_CCER_CC1E << (4 * pwm->channel)); + + // Disable the TIM peripheral if all channels are disabled. + uint32_t ccer_out_mask = TIM_CCER_CCxE_MASK; + #if defined(TIM_CCER_CCxNE_MASK) + ccer_out_mask |= TIM_CCER_CCxNE_MASK; + #endif + if ((tim->CCER & ccer_out_mask) == 0) { + #if defined(IS_TIM_BREAK_INSTANCE) + if (IS_TIM_BREAK_INSTANCE(tim)) { + tim->BDTR &= ~TIM_BDTR_MOE; + } + #endif + tim->CR1 &= ~TIM_CR1_CEN; + } +} + +static bool pwm_freq_is_valid(pwm_t *pwm) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + return tim->ARR != 0; +} + +// The ARR needs to be configured (eg `pwm_set_freq()`) before this function is called. +static void pwm_start(pwm_t *pwm) { + // The following code assumes each channel has 4 bits in CCER. + MP_STATIC_ASSERT(TIM_CCER_CC1E_Pos + 4 == TIM_CCER_CC2E_Pos); + + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + if (!(tim->CR1 & TIM_CR1_CEN)) { + // Reinitialise the counter and update the registers. + tim->EGR = TIM_EGR_UG; + + // Enable the timer if it's not already running. + tim->CR1 |= TIM_CR1_CEN; + } + + // Enable output on pin. + unsigned int shift = 4 * pwm->channel; + tim->CCER |= TIM_CCER_CC1E << shift; +} + +static uint32_t pwm_get_freq(pwm_t *pwm) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + uint32_t prescaler = tim->PSC; + uint32_t period = tim->ARR; + uint32_t freq = timer_get_source_freq(pwm->tim_id) / ((prescaler + 1) * (period + 1)); + return freq; +} + +// Input freq_hz must be > 0. +// Returns false if freq_hz is too large. +static bool pwm_set_freq(pwm_t *pwm, uint32_t freq_hz) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + + uint32_t source_freq = timer_get_source_freq(pwm->tim_id); + + // Find optimal prescaler and period values for the given frequency: + // - For 32-bit TIM, the prescaler is always 1 to get maximum precision. + // - For 16-bit TIM, find the smallest prescaler with a period that fits 16-bits. + uint32_t prescaler = 1; + uint32_t period = (source_freq + freq_hz / 2) / freq_hz; + if (!IS_TIM_32B_COUNTER_INSTANCE(tim)) { + while (period > 0xffff) { + // If we can divide exactly, do that first. + if (period % 5 == 0) { + prescaler *= 5; + period /= 5; + } else if (period % 3 == 0) { + prescaler *= 3; + period /= 3; + } else { + // May not divide exactly, but loses minimal precision. + prescaler <<= 1; + period >>= 1; + } + } + } + + if (period <= 1) { + // Requested frequency too high. + return false; + } + + // Set the prescaler and period registers. + tim->PSC = prescaler - 1; + tim->ARR = period - 1; + + return true; +} + +static uint16_t pwm_get_duty_u16(pwm_t *pwm) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + uint32_t top = tim->ARR + 1; + uint32_t cc = CCRx(tim, pwm->channel); + return (cc * 65535ULL + top / 2U) / top; +} + +static void pwm_set_duty_u16(pwm_t *pwm, uint32_t duty_u16) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + uint32_t top = tim->ARR + 1; + uint32_t cc = ((uint64_t)duty_u16 * top + 65535ULL / 2) / 65535U; + if (cc > top) { + cc = top; + } + CCRx(tim, pwm->channel) = cc; +} + +static uint32_t pwm_get_duty_ns(pwm_t *pwm) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + uint32_t source_freq = timer_get_source_freq(pwm->tim_id) / (tim->PSC + 1); + uint32_t cc = CCRx(tim, pwm->channel); + return ((uint64_t)cc * 1000000000ULL + source_freq / 2U) / source_freq; +} + +static void pwm_set_duty_ns(pwm_t *pwm, uint32_t duty_ns) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + uint32_t source_freq = timer_get_source_freq(pwm->tim_id) / (tim->PSC + 1); + uint32_t top = tim->ARR + 1; + uint32_t cc = ((uint64_t)duty_ns * source_freq + 1000000000ULL / 2U) / 1000000000ULL; + if (cc > top) { + cc = top; + } + CCRx(tim, pwm->channel) = cc; +} + +/******************************************************************************/ +// Pin helper + +typedef struct _pwm_pin_config_t { + uint8_t timer_id; + uint8_t timer_channel; + uint8_t alt; +} pwm_pin_config_t; + +static bool pin_find_af_for_pwm(mp_hal_pin_obj_t pin, pwm_pin_config_t *cfg) { + for (size_t i = 0; i < pin->num_af; ++i) { + if (pin->af[i].fn == AF_FN_TIM && pin->af[i].type >= AF_PIN_TYPE_TIM_CH1 && pin->af[i].type <= AF_PIN_TYPE_TIM_CH4) { + cfg->timer_id = pin->af[i].unit; + cfg->timer_channel = pin->af[i].type - AF_PIN_TYPE_TIM_CH1; + cfg->alt = pin->af[i].idx; + return true; + } + } + return false; +} + +/******************************************************************************/ +// MicroPython bindings for machine.PWM + +enum { + DUTY_NOT_SET = 0, + DUTY_U16, + DUTY_NS +}; + +typedef struct _machine_pwm_obj_t { + mp_obj_base_t base; + pwm_t pwm; +} machine_pwm_obj_t; + +typedef struct _machine_pwm_state_t { + uint8_t duty_type; + mp_int_t duty; +} machine_pwm_state_t; + +static uint32_t timer_pwm_active = 0; + +static const machine_pwm_obj_t machine_pwm_obj[NUM_TIMERS * NUM_CHANNELS_PER_TIMER] = { + #if defined(TIM1) + {{&machine_pwm_type}, {1, PWM_CH1}}, + {{&machine_pwm_type}, {1, PWM_CH2}}, + {{&machine_pwm_type}, {1, PWM_CH3}}, + {{&machine_pwm_type}, {1, PWM_CH4}}, + #endif + #if defined(TIM2) + {{&machine_pwm_type}, {2, PWM_CH1}}, + {{&machine_pwm_type}, {2, PWM_CH2}}, + {{&machine_pwm_type}, {2, PWM_CH3}}, + {{&machine_pwm_type}, {2, PWM_CH4}}, + #endif + #if defined(TIM3) + {{&machine_pwm_type}, {3, PWM_CH1}}, + {{&machine_pwm_type}, {3, PWM_CH2}}, + {{&machine_pwm_type}, {3, PWM_CH3}}, + {{&machine_pwm_type}, {3, PWM_CH4}}, + #endif + #if defined(TIM4) + {{&machine_pwm_type}, {4, PWM_CH1}}, + {{&machine_pwm_type}, {4, PWM_CH2}}, + {{&machine_pwm_type}, {4, PWM_CH3}}, + {{&machine_pwm_type}, {4, PWM_CH4}}, + #endif + #if defined(TIM5) + {{&machine_pwm_type}, {5, PWM_CH1}}, + {{&machine_pwm_type}, {5, PWM_CH2}}, + {{&machine_pwm_type}, {5, PWM_CH3}}, + {{&machine_pwm_type}, {5, PWM_CH4}}, + #endif + // TIM6, TIM7 have no output channels so aren't used for PWM. + #if defined(TIM8) + {{&machine_pwm_type}, {8, PWM_CH1}}, + {{&machine_pwm_type}, {8, PWM_CH2}}, + {{&machine_pwm_type}, {8, PWM_CH3}}, + {{&machine_pwm_type}, {8, PWM_CH4}}, + #endif + #if defined(TIM9) + {{&machine_pwm_type}, {9, PWM_CH1}}, + {{&machine_pwm_type}, {9, PWM_CH2}}, + {{&machine_pwm_type}, {9, PWM_CH3}}, + {{&machine_pwm_type}, {9, PWM_CH4}}, + #endif + #if defined(TIM10) + {{&machine_pwm_type}, {10, PWM_CH1}}, + {{&machine_pwm_type}, {10, PWM_CH2}}, + {{&machine_pwm_type}, {10, PWM_CH3}}, + {{&machine_pwm_type}, {10, PWM_CH4}}, + #endif + #if defined(TIM11) + {{&machine_pwm_type}, {11, PWM_CH1}}, + {{&machine_pwm_type}, {11, PWM_CH2}}, + {{&machine_pwm_type}, {11, PWM_CH3}}, + {{&machine_pwm_type}, {11, PWM_CH4}}, + #endif + #if defined(TIM12) + {{&machine_pwm_type}, {12, PWM_CH1}}, + {{&machine_pwm_type}, {12, PWM_CH2}}, + {{&machine_pwm_type}, {12, PWM_CH3}}, + {{&machine_pwm_type}, {12, PWM_CH4}}, + #endif + #if defined(TIM13) + {{&machine_pwm_type}, {13, PWM_CH1}}, + {{&machine_pwm_type}, {13, PWM_CH2}}, + {{&machine_pwm_type}, {13, PWM_CH3}}, + {{&machine_pwm_type}, {13, PWM_CH4}}, + #endif + #if defined(TIM14) + {{&machine_pwm_type}, {14, PWM_CH1}}, + {{&machine_pwm_type}, {14, PWM_CH2}}, + {{&machine_pwm_type}, {14, PWM_CH3}}, + {{&machine_pwm_type}, {14, PWM_CH4}}, + #endif + #if defined(TIM15) + {{&machine_pwm_type}, {15, PWM_CH1}}, + {{&machine_pwm_type}, {15, PWM_CH2}}, + {{&machine_pwm_type}, {15, PWM_CH3}}, + {{&machine_pwm_type}, {15, PWM_CH4}}, + #endif + #if defined(TIM16) + {{&machine_pwm_type}, {16, PWM_CH1}}, + {{&machine_pwm_type}, {16, PWM_CH2}}, + {{&machine_pwm_type}, {16, PWM_CH3}}, + {{&machine_pwm_type}, {16, PWM_CH4}}, + #endif + #if defined(TIM17) + {{&machine_pwm_type}, {17, PWM_CH1}}, + {{&machine_pwm_type}, {17, PWM_CH2}}, + {{&machine_pwm_type}, {17, PWM_CH3}}, + {{&machine_pwm_type}, {17, PWM_CH4}}, + #endif + #if defined(TIM18) + {{&machine_pwm_type}, {18, PWM_CH1}}, + {{&machine_pwm_type}, {18, PWM_CH2}}, + {{&machine_pwm_type}, {18, PWM_CH3}}, + {{&machine_pwm_type}, {18, PWM_CH4}}, + #endif + #if defined(TIM19) + {{&machine_pwm_type}, {19, PWM_CH1}}, + {{&machine_pwm_type}, {19, PWM_CH2}}, + {{&machine_pwm_type}, {19, PWM_CH3}}, + {{&machine_pwm_type}, {19, PWM_CH4}}, + #endif + #if defined(TIM20) + {{&machine_pwm_type}, {20, PWM_CH1}}, + {{&machine_pwm_type}, {20, PWM_CH2}}, + {{&machine_pwm_type}, {20, PWM_CH3}}, + {{&machine_pwm_type}, {20, PWM_CH4}}, + #endif + #if defined(TIM21) + {{&machine_pwm_type}, {21, PWM_CH1}}, + {{&machine_pwm_type}, {21, PWM_CH2}}, + {{&machine_pwm_type}, {21, PWM_CH3}}, + {{&machine_pwm_type}, {21, PWM_CH4}}, + #endif + #if defined(TIM22) + {{&machine_pwm_type}, {22, PWM_CH1}}, + {{&machine_pwm_type}, {22, PWM_CH2}}, + {{&machine_pwm_type}, {22, PWM_CH3}}, + {{&machine_pwm_type}, {22, PWM_CH4}}, + #endif +}; + +static machine_pwm_state_t machine_pwm_state[NUM_TIMERS * NUM_CHANNELS_PER_TIMER]; + +static inline machine_pwm_state_t *get_state(machine_pwm_obj_t *pwm) { + size_t pwm_index = pwm - &machine_pwm_obj[0]; + return &machine_pwm_state[pwm_index]; +} + +static void mp_machine_pwm_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + machine_pwm_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_printf(print, "", self->pwm.tim_id, self->pwm.channel + 1); +} + +static void mp_machine_pwm_init_helper(machine_pwm_obj_t *self, + size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_freq, ARG_duty_u16, ARG_duty_ns, ARG_invert }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_freq, MP_ARG_INT, {.u_int = -1} }, + { MP_QSTR_duty_u16, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} }, + { MP_QSTR_duty_ns, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} }, + { MP_QSTR_invert, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} }, + }; + + // Parse the arguments. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + if (args[ARG_invert].u_int != -1) { + pwm_set_polarity(&self->pwm, args[ARG_invert].u_int ? PWM_POLARITY_INVERTED : PWM_POLARITY_NORMAL); + } + if (args[ARG_freq].u_int != -1) { + mp_machine_pwm_freq_set(self, args[ARG_freq].u_int); + } + if (args[ARG_duty_u16].u_int != -1) { + mp_machine_pwm_duty_set_u16(self, args[ARG_duty_u16].u_int); + } + if (args[ARG_duty_ns].u_int != -1) { + mp_machine_pwm_duty_set_ns(self, args[ARG_duty_ns].u_int); + } + if (pwm_freq_is_valid(&self->pwm)) { + pwm_start(&self->pwm); + } +} + +// PWM(pin [, args]) +static mp_obj_t mp_machine_pwm_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + // Check number of arguments + mp_arg_check_num(n_args, n_kw, 1, MP_OBJ_FUN_ARGS_MAX, true); + + // Get pin to connect to PWM. + mp_hal_pin_obj_t pin = mp_hal_get_pin_obj(all_args[0]); + + // Search the given pin's alternate functions for a TIMx_CHy function. + machine_pwm_obj_t *self = NULL; + pwm_pin_config_t cfg; + if (pin_find_af_for_pwm(pin, &cfg)) { + for (size_t i = 0; i < MP_ARRAY_SIZE(machine_pwm_obj); ++i) { + if (machine_pwm_obj[i].pwm.tim_id == cfg.timer_id && machine_pwm_obj[i].pwm.channel == cfg.timer_channel) { + self = (machine_pwm_obj_t *)&machine_pwm_obj[i]; + break; + } + } + } + + if (self == NULL) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("Pin(%q) doesn't have PWM capabilities"), pin->name); + } + + // If inactive, clear out the state (may be set from a previous soft reset cycle). + if (!(timer_pwm_active & (1U << self->pwm.tim_id))) { + timer_pwm_active |= 1U << self->pwm.tim_id; + size_t pwm_base_index = (self - &machine_pwm_obj[0]) & ~(NUM_CHANNELS_PER_TIMER - 1); + for (size_t i = 0; i < 4; ++i) { + machine_pwm_state_t *state = &machine_pwm_state[pwm_base_index + i]; + state->duty_type = DUTY_NOT_SET; + } + } + + // Initialise the TIM and the output driver. + pwm_init(&self->pwm); + if (n_args > 1 || n_kw > 0) { + // Arguments given, so reset the state. + machine_pwm_state_t *state = get_state(self); + state->duty_type = DUTY_NOT_SET; + pwm_set_polarity(&self->pwm, PWM_POLARITY_NORMAL); + } + + // Process the remaining parameters. + mp_map_t kw_args; + mp_map_init_fixed_table(&kw_args, n_kw, all_args + n_args); + mp_machine_pwm_init_helper(self, n_args - 1, all_args + 1, &kw_args); + + // Select PWM function for given pin. + mp_hal_pin_config(pin, MP_HAL_PIN_MODE_ALT, MP_HAL_PIN_PULL_NONE, cfg.alt); + + return MP_OBJ_FROM_PTR(self); +} + +void machine_pwm_deinit_all(void) { + for (size_t i = 0; i < MP_ARRAY_SIZE(machine_pwm_obj); ++i) { + machine_pwm_state_t *state = &machine_pwm_state[i]; + if (state->duty_type != DUTY_NOT_SET) { + state->duty_type = DUTY_NOT_SET; + mp_machine_pwm_deinit((machine_pwm_obj_t *)&machine_pwm_obj[i]); + } + } + timer_pwm_active = 0; +} + +static void mp_machine_pwm_deinit(machine_pwm_obj_t *self) { + pwm_deinit(&self->pwm); +} + +static mp_obj_t mp_machine_pwm_freq_get(machine_pwm_obj_t *self) { + if (pwm_freq_is_valid(&self->pwm)) { + return MP_OBJ_NEW_SMALL_INT(pwm_get_freq(&self->pwm)); + } else { + return MP_OBJ_NEW_SMALL_INT(0); + } +} + +static void mp_machine_pwm_freq_set(machine_pwm_obj_t *self, mp_int_t freq) { + // Check validity and change the frequency of the TIM peripheral. + if (freq <= 0) { + mp_raise_ValueError(MP_ERROR_TEXT("freq too small")); + } + if (!pwm_set_freq(&self->pwm, freq)) { + mp_raise_ValueError(MP_ERROR_TEXT("freq too large")); + } + + // Update the duty cycle of all active channels that use this TIM. + size_t pwm_base_index = (self - &machine_pwm_obj[0]) & ~(NUM_CHANNELS_PER_TIMER - 1); + for (size_t i = 0; i < NUM_CHANNELS_PER_TIMER; ++i) { + machine_pwm_obj_t *obj = (machine_pwm_obj_t *)&machine_pwm_obj[pwm_base_index + i]; + machine_pwm_state_t *state = &machine_pwm_state[pwm_base_index + i]; + if (state->duty_type == DUTY_U16) { + mp_machine_pwm_duty_set_u16(obj, state->duty); + } else if (state->duty_type == DUTY_NS) { + mp_machine_pwm_duty_set_ns(obj, state->duty); + } + } +} + +static mp_obj_t mp_machine_pwm_duty_get_u16(machine_pwm_obj_t *self) { + machine_pwm_state_t *state = get_state(self); + if (state->duty_type != DUTY_NOT_SET && pwm_freq_is_valid(&self->pwm)) { + uint32_t duty_u16 = pwm_get_duty_u16(&self->pwm); + return MP_OBJ_NEW_SMALL_INT(duty_u16); + } else { + return MP_OBJ_NEW_SMALL_INT(0); + } +} + +static void mp_machine_pwm_duty_set_u16(machine_pwm_obj_t *self, mp_int_t duty_u16) { + machine_pwm_state_t *state = get_state(self); + state->duty = duty_u16; + state->duty_type = DUTY_U16; + + if (pwm_freq_is_valid(&self->pwm)) { + pwm_set_duty_u16(&self->pwm, duty_u16); + pwm_start(&self->pwm); + } +} + +static mp_obj_t mp_machine_pwm_duty_get_ns(machine_pwm_obj_t *self) { + machine_pwm_state_t *state = get_state(self); + if (state->duty_type != DUTY_NOT_SET && pwm_freq_is_valid(&self->pwm)) { + return mp_obj_new_int_from_uint(pwm_get_duty_ns(&self->pwm)); + } else { + return MP_OBJ_NEW_SMALL_INT(0); + } +} + +static void mp_machine_pwm_duty_set_ns(machine_pwm_obj_t *self, mp_int_t duty_ns) { + machine_pwm_state_t *state = get_state(self); + state->duty = duty_ns; + state->duty_type = DUTY_NS; + + if (pwm_freq_is_valid(&self->pwm)) { + pwm_set_duty_ns(&self->pwm, duty_ns); + pwm_start(&self->pwm); + } +} diff --git a/ports/stm32/main.c b/ports/stm32/main.c index ccf3b6d406004..17111c6df983e 100644 --- a/ports/stm32/main.c +++ b/ports/stm32/main.c @@ -771,6 +771,9 @@ void stm32_main(uint32_t reset_mode) { #if MICROPY_HW_ENABLE_DAC dac_deinit_all(); #endif + #if MICROPY_PY_MACHINE_PWM + machine_pwm_deinit_all(); + #endif #if MICROPY_PY_MACHINE machine_deinit(); #endif diff --git a/ports/stm32/modmachine.h b/ports/stm32/modmachine.h index 899a29be8ebfe..7e5fa42861e08 100644 --- a/ports/stm32/modmachine.h +++ b/ports/stm32/modmachine.h @@ -31,6 +31,7 @@ void machine_init(void); void machine_deinit(void); void machine_i2s_init0(); +void machine_pwm_deinit_all(void); MP_DECLARE_CONST_FUN_OBJ_VAR_BETWEEN(machine_info_obj); diff --git a/ports/stm32/mpconfigport.h b/ports/stm32/mpconfigport.h index e7cf28accee57..bc9e94902ffd3 100644 --- a/ports/stm32/mpconfigport.h +++ b/ports/stm32/mpconfigport.h @@ -143,6 +143,8 @@ #define MICROPY_PY_MACHINE_I2S_CONSTANT_RX (I2S_MODE_MASTER_RX) #define MICROPY_PY_MACHINE_I2S_CONSTANT_TX (I2S_MODE_MASTER_TX) #define MICROPY_PY_MACHINE_I2S_RING_BUF (1) +#define MICROPY_PY_MACHINE_PWM (1) +#define MICROPY_PY_MACHINE_PWM_INCLUDEFILE "ports/stm32/machine_pwm.c" #define MICROPY_PY_MACHINE_SPI (1) #define MICROPY_PY_MACHINE_SPI_MSB (SPI_FIRSTBIT_MSB) #define MICROPY_PY_MACHINE_SPI_LSB (SPI_FIRSTBIT_LSB) From 094c268ff1fd7057a83f408d890ee5d7252d9e23 Mon Sep 17 00:00:00 2001 From: Damien George Date: Thu, 5 Mar 2026 15:06:12 +1100 Subject: [PATCH 20/53] stm32/machine_pwm: Use heuristic to assign TIMx_CHy to a pin. When assigning a TIMx_CHy to a pin, the second available alternate function is chosen (or the first if there is only one). This gives better overall static allocation of TIM's to pins. On most MCUs (eg F4, F7, H5, H7) picking the second gives TIM5_CH[1-4] for PA0-PA3, and TIM5 is a 32-bit timer. That leaves TIM2 (also usually on PA0-PA3) for other pins that only have TIM2. For STM32G0, STM32L432 and STM32L452 the heuristic is to simply use the first available alternate function because that gives TIM2 (a 32-bit timer) on PA0-PA3. The above heuristic guarantees that PA0-PA3 always get a 32-bit timer on all supported MCUs. Signed-off-by: Damien George --- ports/stm32/machine_pwm.c | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/ports/stm32/machine_pwm.c b/ports/stm32/machine_pwm.c index 611bb80f46f3a..2560e0caf1d5f 100644 --- a/ports/stm32/machine_pwm.c +++ b/ports/stm32/machine_pwm.c @@ -311,6 +311,18 @@ static void pwm_set_duty_ns(pwm_t *pwm, uint32_t duty_ns) { /******************************************************************************/ // Pin helper +// The heuristic to search for a TIMx_CHy alt configuration depends on the MCU. +// Some use "first available" and others use "second available". The scheme is +// chosen to give the best overall assignment of TIMx_CHy to pins which is: +// - deterministic (doesn't depend on order of allocation); +// - maximises the number of unique assignments; +// - guarantees that PA0-PA3 are on a 32-bit timer. +#if defined(STM32G0) || defined(STM32L432xx) || defined(STM32L452xx) +#define TIM_SEARCH_FOR_SECOND (0) +#else +#define TIM_SEARCH_FOR_SECOND (1) +#endif + typedef struct _pwm_pin_config_t { uint8_t timer_id; uint8_t timer_channel; @@ -318,14 +330,29 @@ typedef struct _pwm_pin_config_t { } pwm_pin_config_t; static bool pin_find_af_for_pwm(mp_hal_pin_obj_t pin, pwm_pin_config_t *cfg) { + const pin_af_obj_t *pin_af = NULL; for (size_t i = 0; i < pin->num_af; ++i) { if (pin->af[i].fn == AF_FN_TIM && pin->af[i].type >= AF_PIN_TYPE_TIM_CH1 && pin->af[i].type <= AF_PIN_TYPE_TIM_CH4) { - cfg->timer_id = pin->af[i].unit; - cfg->timer_channel = pin->af[i].type - AF_PIN_TYPE_TIM_CH1; - cfg->alt = pin->af[i].idx; - return true; + #if TIM_SEARCH_FOR_SECOND + // Get the second TIMx_CHy configuration (or first if there's only one). + if (pin_af != NULL) { + pin_af = &pin->af[i]; + break; + } + pin_af = &pin->af[i]; + #else + // Get the first TIMx_CHy configuration. + pin_af = &pin->af[i]; + break; + #endif } } + if (pin_af != NULL) { + cfg->timer_id = pin_af->unit; + cfg->timer_channel = pin_af->type - AF_PIN_TYPE_TIM_CH1; + cfg->alt = pin_af->idx; + return true; + } return false; } From 6e9d35b6889c62b54a192a2c952c862d7c579e2c Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 2 Mar 2026 23:56:10 +1100 Subject: [PATCH 21/53] tests/extmod_hardware/machine_pwm.py: Round expected timing calculation. To be slightly more accurate computing the expected low/high times for the PWM output. Signed-off-by: Damien George --- tests/extmod_hardware/machine_pwm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/extmod_hardware/machine_pwm.py b/tests/extmod_hardware/machine_pwm.py index a756493f0e566..89a4984a9472c 100644 --- a/tests/extmod_hardware/machine_pwm.py +++ b/tests/extmod_hardware/machine_pwm.py @@ -68,8 +68,8 @@ def _test_freq_duty(self, pulse_in, pwm, freq, duty_u16): self.assertLessEqual(duty_error, duty_margin_per_thousand) # Calculate expected timing. - expected_total_us = 1_000_000 // freq - expected_high_us = expected_total_us * duty_u16 // 65535 + expected_total_us = (1_000_000 + freq // 2) // freq + expected_high_us = (expected_total_us * duty_u16 + 65535 // 2) // 65535 expected_low_us = expected_total_us - expected_high_us expected_us = (expected_low_us, expected_high_us) timeout = 2 * expected_total_us From 142f8b96bf53a99b707e13745db8b266d6b9a645 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 2 Mar 2026 23:57:51 +1100 Subject: [PATCH 22/53] tests/extmod_hardware/machine_pwm.py: Add pin settings for stm32 port. Signed-off-by: Damien George --- tests/extmod_hardware/machine_pwm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/extmod_hardware/machine_pwm.py b/tests/extmod_hardware/machine_pwm.py index 89a4984a9472c..f78825795e2b9 100644 --- a/tests/extmod_hardware/machine_pwm.py +++ b/tests/extmod_hardware/machine_pwm.py @@ -42,6 +42,8 @@ ) else: pwm_pulse_pins = (("D0", "D1"),) +elif "pyboard" in sys.platform: + pwm_pulse_pins = ((Pin.cpu.A0, Pin.cpu.A1),) elif "rp2" in sys.platform: pwm_pulse_pins = (("GPIO0", "GPIO1"),) elif "samd" in sys.platform: From 1d5073f609b7640a3764b383a167c4098b13f894 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 3 Mar 2026 01:18:02 +1100 Subject: [PATCH 23/53] extmod/machine_pwm: Fix use of object when pointer is needed. Signed-off-by: Damien George --- extmod/machine_pwm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extmod/machine_pwm.c b/extmod/machine_pwm.c index 0c1834886c4df..eb68bd9671c64 100644 --- a/extmod/machine_pwm.c +++ b/extmod/machine_pwm.c @@ -50,7 +50,7 @@ static void mp_machine_pwm_duty_set_ns(machine_pwm_obj_t *self, mp_int_t duty_ns #include MICROPY_PY_MACHINE_PWM_INCLUDEFILE static mp_obj_t machine_pwm_init(size_t n_args, const mp_obj_t *args, mp_map_t *kw_args) { - mp_machine_pwm_init_helper(args[0], n_args - 1, args + 1, kw_args); + mp_machine_pwm_init_helper(MP_OBJ_TO_PTR(args[0]), n_args - 1, args + 1, kw_args); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_KW(machine_pwm_init_obj, 1, machine_pwm_init); From af31472e3d86e124f8d2af86ffbfaa65736c3786 Mon Sep 17 00:00:00 2001 From: Damien George Date: Wed, 4 Mar 2026 17:03:29 +1100 Subject: [PATCH 24/53] stm32/boards: Disable some features on boards with small flash. This allows the newly-added `machine.PWM` class to fit on these boards, which is arguably more useful than the features disabled in this commit. Signed-off-by: Damien George --- ports/stm32/boards/ESPRUINO_PICO/mpconfigboard.h | 1 + ports/stm32/boards/NUCLEO_L432KC/mpconfigboard.h | 1 + 2 files changed, 2 insertions(+) diff --git a/ports/stm32/boards/ESPRUINO_PICO/mpconfigboard.h b/ports/stm32/boards/ESPRUINO_PICO/mpconfigboard.h index cfc46491e0763..e38f8e6f61d1d 100644 --- a/ports/stm32/boards/ESPRUINO_PICO/mpconfigboard.h +++ b/ports/stm32/boards/ESPRUINO_PICO/mpconfigboard.h @@ -3,6 +3,7 @@ #define MICROPY_EMIT_THUMB (0) #define MICROPY_EMIT_INLINE_THUMB (0) +#define MICROPY_OPT_COMPUTED_GOTO (0) #define MICROPY_PY_BUILTINS_COMPLEX (0) #define MICROPY_PY_SOCKET (0) #define MICROPY_PY_NETWORK (0) diff --git a/ports/stm32/boards/NUCLEO_L432KC/mpconfigboard.h b/ports/stm32/boards/NUCLEO_L432KC/mpconfigboard.h index 0daf4877ff6c2..f38601c5a7d40 100644 --- a/ports/stm32/boards/NUCLEO_L432KC/mpconfigboard.h +++ b/ports/stm32/boards/NUCLEO_L432KC/mpconfigboard.h @@ -12,6 +12,7 @@ #define MICROPY_PY_STM (0) #define MICROPY_PY_PYB_LEGACY (0) #define MICROPY_PY_HEAPQ (0) +#define MICROPY_PY_FRAMEBUF (0) #define MICROPY_HW_ENABLE_RTC (1) #define MICROPY_HW_ENABLE_ADC (1) From 2b64d6d023de0bc44c97ea7b837ed93cc3f35c48 Mon Sep 17 00:00:00 2001 From: Damien George Date: Fri, 6 Mar 2026 11:40:49 +1100 Subject: [PATCH 25/53] stm32/boards/stm32f091_af.csv: Split TIM2_CH1 from TIM2_ETR. So that TIM2_CH1 can be used. Signed-off-by: Damien George --- ports/stm32/boards/stm32f091_af.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ports/stm32/boards/stm32f091_af.csv b/ports/stm32/boards/stm32f091_af.csv index 5e44697776b13..b8fba765e18b7 100644 --- a/ports/stm32/boards/stm32f091_af.csv +++ b/ports/stm32/boards/stm32f091_af.csv @@ -1,6 +1,6 @@ Port ,Pin ,AF0 ,AF1 ,AF2 ,AF3 ,AF4 ,AF5 ,AF6 ,AF7 ,,,,,,,,,ADC , ,AF0 ,AF1 ,AF2 ,AF3 ,AF4 ,AF5 ,AF6 ,AF7 ,,,,,,,,,ADC -PortA,PA0 , ,USART2_CTS ,TIM2_CH1_ETR ,TSC_G1_IO1,USART4_TX , , ,COMP1_OUT,,,,,,,,,ADC1_IN0 +PortA,PA0 , ,USART2_CTS ,TIM2_CH1/TIM2_ETR ,TSC_G1_IO1,USART4_TX , , ,COMP1_OUT,,,,,,,,,ADC1_IN0 PortA,PA1 ,EVENTOUT ,USART2_RTS ,TIM2_CH2 ,TSC_G1_IO2,USART4_RX ,TIM15_CH1N , , ,,,,,,,,,ADC1_IN1 PortA,PA2 ,TIM15_CH1 ,USART2_TX ,TIM2_CH3 ,TSC_G1_IO3, , , ,COMP2_OUT,,,,,,,,,ADC1_IN2 PortA,PA3 ,TIM15_CH2 ,USART2_RX ,TIM2_CH4 ,TSC_G1_IO4, , , , ,,,,,,,,,ADC1_IN3 From 98ab12a4912c9b6e223b33bf3007ad3e2b97bdb2 Mon Sep 17 00:00:00 2001 From: Damien George Date: Fri, 6 Mar 2026 11:41:15 +1100 Subject: [PATCH 26/53] stm32/boards/stm32n657_af.csv: Add TIM alt funcs to PA0-PA3. Signed-off-by: Damien George --- ports/stm32/boards/stm32n657_af.csv | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ports/stm32/boards/stm32n657_af.csv b/ports/stm32/boards/stm32n657_af.csv index c02c658bd7330..b269d0497c646 100644 --- a/ports/stm32/boards/stm32n657_af.csv +++ b/ports/stm32/boards/stm32n657_af.csv @@ -1,7 +1,9 @@ Port ,Pin ,AF0 ,AF1 ,AF2 ,AF3 ,AF4 ,AF5 ,AF6 ,AF7 ,AF8 ,AF9 ,AF10 ,AF11 ,AF12 ,AF13 ,AF14 ,AF15 ,ADC , ,SYS ,LPTIM1/TIM1/2/16/17,LPTIM3/PDM_SAI1/TIM3/4/5/12/15,I3C1/LPTIM2/3/LPUART1/OCTOSPI/TIM1/8,CEC/DCMI/I2C1/2/3/4/LPTIM1/2/SPI1/I2S1/TIM15/USART1,CEC/I3C1/LPTIM1/SPI1/I2S1/SPI2/I2S2/SPI3/I2S3/SPI4/5/6,I2C4/OCTOSPI/SAI1/SPI3/I2S3/SPI4/UART4/12/USART10/USB_PD,SDMMC1/SPI2/I2S2/SPI3/I2S3/SPI6/UART7/8/12/USART1/2/3/6/10/11,LPUART1/SAI2/SDMMC1/SPI6/UART4/5/8,FDCAN1/2/FMC[NAND16]/FMC[NORmux]/FMC[NOR_RAM]/OCTOSPI/SDMMC2/TIM13/14,CRS/FMC[NAND16]/OCTOSPI/SAI2/SDMMC2/TIM8/USB_,ETH[MII/RMII]/FMC[NAND16]/OCTOSPI/SDMMC2/UART7/9/USB_PD,FMC[NAND16]/FMC[NORmux]/FMC[NOR_RAM]/FMC[SDRAM_16bit]/SDMMC1,DCMI/FMC[NAND16]/FMC[NORmux]/FMC[NOR_RAM]/LPTIM5,LPTIM3/4/5/6/TIM2/UART5,SYS ,ADC -PortA,PA0 , , , , , , , , , , , ,SDMMC2_CMD , , , , ,ADC12_INP0/ADC12_INN1 -PortA,PA3 , , , , , ,SPI5_NSS , , , , , , , , , , , +PortA,PA0 , ,TIM2_CH1 ,TIM5_CH1 ,TIM9_CH1 ,TIM15_BKIN , , , , , , ,SDMMC2_CMD , , , , ,ADC12_INP0/ADC12_INN1 +PortA,PA1 , ,TIM2_CH2 ,TIM5_CH2 ,LPTIM3_IN1 ,TIM15_CH1N , , , , , , , , , , , ,ADC12_INP1 +PortA,PA2 , ,TIM2_CH3 ,TIM5_CH3 ,LPTIM3_IN2 ,TIM15_CH1 , , , , , , , , , , , ,ADC12_INP14 +PortA,PA3 , ,TIM16_CH1 , , , ,SPI5_NSS , , , , , , , , , , , PortA,PA5 , , , , , , , , , , , , , , , , ,ADC2_INP18 PortA,PA8 , , , , , , , , , , , , , , , , ,ADC12_INP5 PortA,PA9 , , , , , , , , , , , , , , , , ,ADC12_INP10 From ef2b30b560f5a231745164766c0320228d70290f Mon Sep 17 00:00:00 2001 From: Damien George Date: Thu, 5 Mar 2026 16:07:04 +1100 Subject: [PATCH 27/53] docs/library/machine.PWM: Add alif,stm32 to list of ports with invert. Signed-off-by: Damien George --- docs/library/machine.PWM.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/library/machine.PWM.rst b/docs/library/machine.PWM.rst index c2b606affd675..0fdc4b28a1eec 100644 --- a/docs/library/machine.PWM.rst +++ b/docs/library/machine.PWM.rst @@ -40,7 +40,7 @@ Constructors Setting *freq* may affect other PWM objects if the objects share the same underlying PWM generator (this is hardware specific). Only one of *duty_u16* and *duty_ns* should be specified at a time. - *invert* is available only on the esp32, mimxrt, nrf, rp2, samd and zephyr ports. + *invert* is available only on the alif, esp32, mimxrt, nrf, rp2, samd, stm32 and zephyr ports. Methods ------- From 47871a4276bfd8e8207324223dbdf2c61867fc38 Mon Sep 17 00:00:00 2001 From: Damien George Date: Fri, 6 Mar 2026 11:41:32 +1100 Subject: [PATCH 28/53] docs/library/machine.PWM: Document hardware PWM layout. Signed-off-by: Damien George --- docs/library/machine.PWM.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/library/machine.PWM.rst b/docs/library/machine.PWM.rst index 0fdc4b28a1eec..fdf7d7f4e0d77 100644 --- a/docs/library/machine.PWM.rst +++ b/docs/library/machine.PWM.rst @@ -84,6 +84,22 @@ Methods Specific PWM class implementations ---------------------------------- +On the alif port there are 11 independent PWM blocks with independent +frequencies, and they have 2 outputs each. The underlying counter is +32-bits wide for all 11 PWM blocks. + +On the rp2 port there are 8 independent PWM blocks on RP2040 and 12 on +RP2350, each with independent frequencies, and each with 2 outputs. +The underlying counter is 16-bits wide for all PWM blocks. + +On the stm32 port the number of independent PWM blocks depends on the MCU +and can range between 4 and 19. TIM2 and TIM5 blocks (also TIM3 and TIM4 +blocks on STM32U5 and STM32N6) are 32-bits wide, and the others are +16-bits wide. All MCUs supported by MicroPython have at least one 32-bit +block available, and most have two. MCUs will have pins PA0 through PA3 +assigned to a 32-bit PWM block (except STM32N6 which has a 16-bit PWM +block on PA3). PWM blocks have up to 4 outputs each. + The following concrete class(es) implement enhancements to the PWM class. | :ref:`pyb.Timer for PyBoard ` From 8f24c86263d713dc3a132a301e96d4857978f4e0 Mon Sep 17 00:00:00 2001 From: Damien George Date: Wed, 18 Mar 2026 13:26:08 +1100 Subject: [PATCH 29/53] tests/extmod_hardware/machine_pwm.py: Convert test to use target_wiring. Signed-off-by: Damien George --- tests/extmod_hardware/machine_pwm.py | 51 ++++++++++------------------ tests/run-tests.py | 1 + tests/target_wiring/NUCLEO_WB55.py | 2 ++ tests/target_wiring/PYBx.py | 2 ++ tests/target_wiring/README.md | 14 ++++++++ tests/target_wiring/alif.py | 2 ++ tests/target_wiring/esp32.py | 2 ++ tests/target_wiring/esp8266.py | 2 ++ tests/target_wiring/mimxrt.py | 12 +++++++ tests/target_wiring/rp2.py | 2 ++ tests/target_wiring/samd.py | 2 ++ tests/target_wiring/stm32.py | 5 +++ 12 files changed, 63 insertions(+), 34 deletions(-) diff --git a/tests/extmod_hardware/machine_pwm.py b/tests/extmod_hardware/machine_pwm.py index f78825795e2b9..7d4f82fd2fe74 100644 --- a/tests/extmod_hardware/machine_pwm.py +++ b/tests/extmod_hardware/machine_pwm.py @@ -1,10 +1,7 @@ # Test machine.PWM, frequency and duty cycle (using machine.time_pulse_us). # # IMPORTANT: This test requires hardware connections: the PWM-output and pulse-input -# pins must be wired together (see the variable `pwm_pulse_pins`). - -import sys -import time +# pins must be wired together (see the variable `pwm_loopback_pins`). try: from machine import time_pulse_us, Pin, PWM @@ -12,48 +9,34 @@ print("SKIP") raise SystemExit -import unittest +import machine, sys, time, unittest +from target_wiring import pwm_loopback_pins pwm_freq_limit = 1000000 freq_margin_per_thousand = 0 duty_margin_per_thousand = 0 timing_margin_us = 5 -# Configure pins based on the target. -if "alif" in sys.platform: - pwm_pulse_pins = (("P0_4", "P0_5"),) -elif "esp32" in sys.platform: - pwm_pulse_pins = ((4, 5),) +# Slow MCUs cannot capture short pulses using `time_pulse_us` so limit the maximum PWM +# frequency tested on such targets. +if hasattr(machine, "freq"): + f = machine.freq() + if isinstance(f, tuple): + f = f[0] + if f <= 48_000_000: + pwm_freq_limit = 2_000 + elif f <= 64_000_000: + pwm_freq_limit = 5_000 + +# Tune test parameters based on the target. +if "esp32" in sys.platform: freq_margin_per_thousand = 2 duty_margin_per_thousand = 1 timing_margin_us = 20 elif "esp8266" in sys.platform: - pwm_pulse_pins = ((4, 5),) pwm_freq_limit = 1_000 duty_margin_per_thousand = 3 timing_margin_us = 50 -elif "mimxrt" in sys.platform: - if "Teensy" in sys.implementation._machine: - # Teensy 4.x - pwm_pulse_pins = ( - ("D0", "D1"), # FLEXPWM X and UART 1 - ("D2", "D3"), # FLEXPWM A/B - ("D11", "D12"), # QTMR and MOSI/MISO of SPI 0 - ) - else: - pwm_pulse_pins = (("D0", "D1"),) -elif "pyboard" in sys.platform: - pwm_pulse_pins = ((Pin.cpu.A0, Pin.cpu.A1),) -elif "rp2" in sys.platform: - pwm_pulse_pins = (("GPIO0", "GPIO1"),) -elif "samd" in sys.platform: - pwm_pulse_pins = (("D0", "D1"),) - if "SAMD21" in sys.implementation._machine: - # MCU is too slow to capture short pulses. - pwm_freq_limit = 2_000 -else: - print("Please add support for this test on this platform.") - raise SystemExit # Test a specific frequency and duty cycle. @@ -160,7 +143,7 @@ def test_freq_10000(self): # Generate test classes, one for each set of pins to test. -for pwm, pulse in pwm_pulse_pins: +for pwm, pulse in pwm_loopback_pins: cls_name = "Test_{}_{}".format(pwm, pulse) globals()[cls_name] = type( cls_name, (TestBase, unittest.TestCase), {"pwm_pin": pwm, "pulse_pin": pulse} diff --git a/tests/run-tests.py b/tests/run-tests.py index a5659fff8b36f..59a292ed4b83a 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -291,6 +291,7 @@ "extmod/machine_uart_tx.py", "extmod_hardware/machine_can_timings.py", "extmod_hardware/machine_encoder.py", + "extmod_hardware/machine_pwm.py", "extmod_hardware/machine_uart_irq_break.py", "extmod_hardware/machine_uart_irq_rx.py", "extmod_hardware/machine_uart_irq_rxidle.py", diff --git a/tests/target_wiring/NUCLEO_WB55.py b/tests/target_wiring/NUCLEO_WB55.py index 166f6f8d35ff9..e40d35e225b61 100644 --- a/tests/target_wiring/NUCLEO_WB55.py +++ b/tests/target_wiring/NUCLEO_WB55.py @@ -8,3 +8,5 @@ uart_loopback_kwargs = {} spi_standalone_args_list = [(1,), (2,)] + +pwm_loopback_pins = [("D1", "D0")] diff --git a/tests/target_wiring/PYBx.py b/tests/target_wiring/PYBx.py index a825dbb514109..b36e342e944ea 100644 --- a/tests/target_wiring/PYBx.py +++ b/tests/target_wiring/PYBx.py @@ -12,3 +12,5 @@ # CAN args assume no connection for single device tests can_args = (1,) can_kwargs = {} + +pwm_loopback_pins = [("X1", "X2")] diff --git a/tests/target_wiring/README.md b/tests/target_wiring/README.md index 96c98da8dff7f..c3a7038bb9111 100644 --- a/tests/target_wiring/README.md +++ b/tests/target_wiring/README.md @@ -48,6 +48,20 @@ SPI instances will be created using: for spi_args in spi_standalone_args_list: machine.SPI(*spi_args) +### PWM tests + +PWM tests require a PWM output to be connected in loopback mode to another GPIO pin +that will use `machine.time_pulse_us()` to time the PWM output signal. The variables +are: + + pwm_loopback_pins: list[tuple[Any, Any]] + +The PWM and input Pin instances will be created using: + + for pwm_pin, pulse_pin in pwm_loopback_pins: + machine.PWM(pwm_pin) + machine.Pin(pulse_pin, Pin.IN) + ### Encoder tests Encoder tests require one encoder to be connected in loopback mode to two other GPIO diff --git a/tests/target_wiring/alif.py b/tests/target_wiring/alif.py index cb62ea407200a..708b4591846cb 100644 --- a/tests/target_wiring/alif.py +++ b/tests/target_wiring/alif.py @@ -7,3 +7,5 @@ uart_loopback_kwargs = {} spi_standalone_args_list = [(0,)] + +pwm_loopback_pins = [("P0_4", "P0_5")] diff --git a/tests/target_wiring/esp32.py b/tests/target_wiring/esp32.py index a3d39b5111f44..d94a6f607596f 100644 --- a/tests/target_wiring/esp32.py +++ b/tests/target_wiring/esp32.py @@ -14,6 +14,8 @@ else: spi_standalone_args_list = [(1,), (2,)] +pwm_loopback_pins = [(4, 5)] + encoder_loopback_id = 0 encoder_loopback_out_pins = (4, 12) encoder_loopback_in_pins = (5, 13) diff --git a/tests/target_wiring/esp8266.py b/tests/target_wiring/esp8266.py index ba9bca01c864b..ee0c3020bfbe1 100644 --- a/tests/target_wiring/esp8266.py +++ b/tests/target_wiring/esp8266.py @@ -7,3 +7,5 @@ uart_loopback_kwargs = {} spi_standalone_args_list = [(1,)] + +pwm_loopback_pins = [(4, 5)] diff --git a/tests/target_wiring/mimxrt.py b/tests/target_wiring/mimxrt.py index e1e51ea143df3..2836d88ab9d9f 100644 --- a/tests/target_wiring/mimxrt.py +++ b/tests/target_wiring/mimxrt.py @@ -3,11 +3,23 @@ # Connect: # - UART1 TX and RX, usually D0 and D1 +import sys + uart_loopback_args = (1,) uart_loopback_kwargs = {} spi_standalone_args_list = [()] +if "Teensy" in sys.implementation._machine: + # Teensy 4.x + pwm_loopback_pins = [ + ("D0", "D1"), # FLEXPWM X and UART 1 + ("D2", "D3"), # FLEXPWM A/B + ("D11", "D12"), # QTMR and MOSI/MISO of SPI 0 + ] +else: + pwm_loopback_pins = [("D0", "D1")] + encoder_loopback_id = 0 encoder_loopback_out_pins = ("D0", "D2") encoder_loopback_in_pins = ("D1", "D3") diff --git a/tests/target_wiring/rp2.py b/tests/target_wiring/rp2.py index 4024eb20884c2..c11cbd469049e 100644 --- a/tests/target_wiring/rp2.py +++ b/tests/target_wiring/rp2.py @@ -7,3 +7,5 @@ uart_loopback_kwargs = {"tx": "GPIO0", "rx": "GPIO1"} spi_standalone_args_list = [(0,), (1,)] + +pwm_loopback_pins = [("GPIO0", "GPIO1")] diff --git a/tests/target_wiring/samd.py b/tests/target_wiring/samd.py index 1ee67e8e74940..50c7b087e5119 100644 --- a/tests/target_wiring/samd.py +++ b/tests/target_wiring/samd.py @@ -7,3 +7,5 @@ uart_loopback_kwargs = {"tx": "D1", "rx": "D0"} spi_standalone_args_list = [()] + +pwm_loopback_pins = [("D0", "D1")] diff --git a/tests/target_wiring/stm32.py b/tests/target_wiring/stm32.py index da73d7fa9a257..533431e694795 100644 --- a/tests/target_wiring/stm32.py +++ b/tests/target_wiring/stm32.py @@ -1,7 +1,12 @@ # Target wiring for non-PyBoard stm32 boards # # See PYBx.py for PyBoards +# +# Connect: +# - D0 to D1 (Arduino labels) # CAN args assume no connection for single device tests can_args = (1,) can_kwargs = {} + +pwm_loopback_pins = [("D0", "D1")] From 2ccf78ae194ac86dbfffca1a06ae454b02a09029 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 12 Mar 2026 12:21:45 +1100 Subject: [PATCH 30/53] ci,esp32: Build oldest & newest ESP-IDF versions in CI. Intended to catch problems where new features don't build in old ESP-IDF. Includes major refactor to the GitHub Actions Workflow for esp32 port, including making a reusable workflow for both Code Size and ESP32 build jobs. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- .github/actions/setup_esp32/action.yml | 47 ++++++++++++++++++++++++ .github/workflows/code_size.yml | 29 +++++---------- .github/workflows/ports_esp32.yml | 50 +++++++++++++------------- ports/esp32/README.md | 6 ++++ tools/ci.sh | 21 ++++------- 5 files changed, 95 insertions(+), 58 deletions(-) create mode 100644 .github/actions/setup_esp32/action.yml diff --git a/.github/actions/setup_esp32/action.yml b/.github/actions/setup_esp32/action.yml new file mode 100644 index 0000000000000..42c44cf76ce2e --- /dev/null +++ b/.github/actions/setup_esp32/action.yml @@ -0,0 +1,47 @@ +name: Setup ESP-IDF for CI +description: Install ESP-IDF +inputs: + idf_ver: + required: true + type: string + ccache_key: + required: true + type: string + +runs: + using: "composite" + + steps: + - id: python_ver + name: Read the Python version + run: echo PYTHON_VER=py$(python --version | cut -d' ' -f2) | tee "${GITHUB_OUTPUT}" + shell: bash + + - name: Cached ESP-IDF install + id: cache_esp_idf + uses: actions/cache@v5 + with: + path: | + ./esp-idf/ + ~/.espressif/ + !~/.espressif/dist/ + ~/.cache/pip/ + # Cache is keyed on both IDF version (from the job) and Python version (from the runner) + key: esp-idf-${{ inputs.idf_ver }}-${{ steps.python_ver.outputs.PYTHON_VER }} + + - name: Install ESP-IDF packages + if: steps.cache_esp_idf.outputs.cache-hit != 'true' + env: + IDF_VER: ${{ inputs.idf_ver }} + run: tools/ci.sh esp32_idf_setup + shell: bash + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: esp32-${{ inputs.idf_ver }}-${{ inputs.ccache_key }} + + - name: Enable CCache for ESP-IDF + run: echo "IDF_CCACHE_ENABLE=1" >> ${GITHUB_ENV} + shell: bash + diff --git a/.github/workflows/code_size.yml b/.github/workflows/code_size.yml index 42e7183c4f410..f3238a85d9acf 100644 --- a/.github/workflows/code_size.yml +++ b/.github/workflows/code_size.yml @@ -32,28 +32,17 @@ jobs: - name: Install packages run: tools/ci.sh code_size_setup - # Needs to be kept in synch with ports_esp32.yml - - id: idf_ver - name: Read the ESP-IDF version (including Python version) and set outputs.IDF_VER - run: tools/ci.sh esp32_idf_ver | tee "${GITHUB_OUTPUT}" - - name: Cached ESP-IDF install - id: cache_esp_idf - uses: actions/cache@v5 - with: - path: | - ./esp-idf/ - ~/.espressif/ - !~/.espressif/dist/ - ~/.cache/pip/ - key: esp-idf-${{ steps.idf_ver.outputs.IDF_VER }} - - name: Install ESP-IDF packages - if: steps.cache_esp_idf.outputs.cache-hit != 'true' - run: tools/ci.sh esp32_idf_setup + - name: Find IDF_NEWEST_VER + id: idf_ver + run: | + echo "IDF_VER="$(yq .env.IDF_NEWEST_VER < .github/workflows/ports_esp32.yml) \ + | tee "${GITHUB_OUTPUT}" - - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 + - name: Setup ESP-IDF + uses: ./.github/actions/setup_esp32 with: - key: code_size + idf_ver: ${{ steps.idf_ver.outputs.IDF_VER }} + ccache_key: code_size - name: Build run: tools/ci.sh code_size_build diff --git a/.github/workflows/ports_esp32.yml b/.github/workflows/ports_esp32.yml index 446db794cb43f..56dce3b69d989 100644 --- a/.github/workflows/ports_esp32.yml +++ b/.github/workflows/ports_esp32.yml @@ -17,45 +17,47 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + # Oldest and newest supported ESP-IDF versions, should match ports/esp32/README.md + IDF_OLDEST_VER: &oldest "v5.3" + IDF_NEWEST_VER: &newest "v5.5.1" + jobs: build_idf: strategy: fail-fast: false matrix: + idf_ver: + - *oldest + - *newest ci_func: # names are functions in ci.sh - esp32_build_cmod_spiram_s2 - esp32_build_s3_c3 - esp32_build_c2_c5_c6 - esp32_build_p4 + exclude: + # Exclude some jobs on the oldest IDF version, to save resources + - idf_ver: *oldest + ci_func: esp32_build_c2_c5_c6 + - idf_ver: *oldest + ci_func: esp32_build_p4 runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - # Needs to be kept in synch with code_size.yml - - id: idf_ver - name: Read the ESP-IDF version (including Python version) and set outputs.IDF_VER - run: tools/ci.sh esp32_idf_ver | tee "${GITHUB_OUTPUT}" + # Only the newest IDF version will build the ESP-IDF lockfiles correctly, + # so we need to disable MICROPY_MAINTAINER_BUILD on older versions. + - name: Disable extra checks for older ESP-IDF + id: check_newest_ver + if: ${{ matrix.idf_ver != env.IDF_NEWEST_VER }} + run: echo "MICROPY_MAINTAINER_BUILD=0" >> ${GITHUB_ENV} - - name: Cached ESP-IDF install - id: cache_esp_idf - uses: actions/cache@v5 - with: - path: | - ./esp-idf/ - ~/.espressif/ - !~/.espressif/dist/ - ~/.cache/pip/ - key: esp-idf-${{ steps.idf_ver.outputs.IDF_VER }} - - - name: Install ESP-IDF packages - if: steps.cache_esp_idf.outputs.cache-hit != 'true' - run: tools/ci.sh esp32_idf_setup - - # Needs to be kept in synch with code_size.yml - - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 + - name: Setup ESP-IDF + uses: ./.github/actions/setup_esp32 with: - key: esp32-${{ matrix.ci_func }} + idf_ver: ${{ matrix.idf_ver }} + ccache_key: ${{ matrix.ci_func }} + - - name: Build ci_${{matrix.ci_func }} + - name: Build ci_${{matrix.ci_func }} on ESP-IDF ${{ matrix.idf_ver }} run: tools/ci.sh ${{ matrix.ci_func }} diff --git a/ports/esp32/README.md b/ports/esp32/README.md index 2cfc09afadf2c..85e13904054d1 100644 --- a/ports/esp32/README.md +++ b/ports/esp32/README.md @@ -55,6 +55,12 @@ The ESP-IDF changes quickly and MicroPython only supports certain versions. The current recommended version of ESP-IDF for MicroPython is v5.5.1. MicroPython also supports v5.3, v5.4, v5.4.1 and v5.4.2. + + To install the ESP-IDF the full instructions can be found at the [Espressif Getting Started guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/index.html#installation-step-by-step). diff --git a/tools/ci.sh b/tools/ci.sh index 6bc74bd54ce48..0532c1c7570f8 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -10,7 +10,9 @@ fi ulimit -n 1024 # Fail on some things which are warnings otherwise -export MICROPY_MAINTAINER_BUILD=1 +if [ -z "$MICROPY_MAINTAINER_BUILD" ]; then + export MICROPY_MAINTAINER_BUILD=1 +fi ######################################################################################## # general helper functions @@ -206,20 +208,11 @@ function ci_embedding_build { ######################################################################################## # ports/esp32 -# GitHub tag of ESP-IDF to use for CI, extracted from the esp32 dependency lockfile -# This should end up as a tag name like vX.Y.Z -# (note: This hacky parsing can be replaced with 'yq' once Ubuntu >=24.04 is in use) -IDF_VER=v$(grep -A10 "idf:" ports/esp32/lockfiles/dependencies.lock.esp32 | grep "version:" | head -n1 | sed -E 's/ +version: //') -PYTHON=$(command -v python3 2> /dev/null) -PYTHON_VER=$(${PYTHON:-python} --version | cut -d' ' -f2) - -export IDF_CCACHE_ENABLE=1 - -function ci_esp32_idf_ver { - echo "IDF_VER=${IDF_VER}-py${PYTHON_VER}" -} - function ci_esp32_idf_setup { + if [ -z "$IDF_VER" ]; then + echo "IDF_VER environment variable must be set before running" + return 1 + fi echo "Using ESP-IDF version $IDF_VER" git clone --depth 1 --branch $IDF_VER https://github.com/espressif/esp-idf.git # doing a treeless clone isn't quite as good as --shallow-submodules, but it From e0beace19ff4b6e11d43668153f5e6f03f2d3a68 Mon Sep 17 00:00:00 2001 From: Damien George Date: Wed, 11 Mar 2026 16:42:58 +1100 Subject: [PATCH 31/53] LICENSE,docs: Update copyright year range to include 2026. Signed-off-by: Damien George --- LICENSE | 2 +- docs/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 929a2e97de7bf..28b5239e5fe59 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2025 Damien P. George +Copyright (c) 2013-2026 Damien P. George Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/conf.py b/docs/conf.py index 603543aa18c7e..f80ca97edcaf9 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -72,7 +72,7 @@ # General information about the project. project = "MicroPython" -copyright = "- The MicroPython Documentation is Copyright © 2014-2025, " + micropy_authors +copyright = "- The MicroPython Documentation is Copyright © 2014-2026, " + micropy_authors # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From b1d635fa823e89ffc4c5659af4260bb29e0faf32 Mon Sep 17 00:00:00 2001 From: Alessandro Gatti Date: Tue, 17 Mar 2026 18:42:07 +0100 Subject: [PATCH 32/53] docs/reference/speed_python: Update native emitter limitations. This commit updates the listed limitations of the native emitter in the documentation related to how to increase speed of python code. Context managers are supported, as in functions marked as native can use the `with` statement in regular code. Generators can be used in native functions both on the emitting (ie. `yield `) and on the receiving end. Signed-off-by: Alessandro Gatti --- docs/reference/speed_python.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/reference/speed_python.rst b/docs/reference/speed_python.rst index 9360fd6108027..6382394bd507b 100644 --- a/docs/reference/speed_python.rst +++ b/docs/reference/speed_python.rst @@ -243,12 +243,10 @@ no adaptation (but see below). It is invoked by means of a function decorator: There are certain limitations in the current implementation of the native code emitter. -* Context managers are not supported (the ``with`` statement). -* Generators are not supported. * If ``raise`` is used an argument must be supplied. * The background scheduler (see `micropython.schedule`) is not run during execution of native code. -* On targets with thrteading and the GIL, the GIL is not released during +* On targets with threading and the GIL, the GIL is not released during execution of native code. To mitigate the last two points, long running native functions should call From b3d88cf210d0408b76f7375773ae665896334759 Mon Sep 17 00:00:00 2001 From: Alessandro Gatti Date: Wed, 18 Mar 2026 20:04:15 +0100 Subject: [PATCH 33/53] extmod/nimble/modbluetooth_nimble: Handle port init failures. This commit fixes an issue related to the NimBLE initialisation procedure in low memory environments on ESP32 boards. MicroPython uses at least two different NimBLE stacks across the supported ports, mynewt (imported as an external library), and the one provided by Espressif in their own SDKs. The problem is that these two ports differ in the signature for `nimble_port_init(void)`, with mynewt returning `void`, and Espressif's returning a status code on failure. On ESP32 boards, allocating almost all the available heap and then turning on the Bluetooth stack would trigger a failure in the NimBLE initialisation function that is not handled by the NimBLE integration code, as there's no expectation of a recoverable condition. Since the stack initialisation would progress, then uninitialised memory accesses crash the board. Since we cannot really modify neither mynewt nor Espressif SDKs, the next best thing is to provide two conditional initialisation paths depending on a configuration setting. This would make Espressif ports recover from a failed initialisation whilst retaining the existing behaviour on other ports. This fixes #14293. Signed-off-by: Alessandro Gatti --- extmod/nimble/modbluetooth_nimble.c | 10 ++++++++++ ports/esp32/mpconfigport.h | 1 + 2 files changed, 11 insertions(+) diff --git a/extmod/nimble/modbluetooth_nimble.c b/extmod/nimble/modbluetooth_nimble.c index 5e7030e36fab4..6b000f77f8a53 100644 --- a/extmod/nimble/modbluetooth_nimble.c +++ b/extmod/nimble/modbluetooth_nimble.c @@ -618,7 +618,17 @@ int mp_bluetooth_init(void) { // Initialise NimBLE memory and data structures. DEBUG_printf("mp_bluetooth_init: nimble_port_init\n"); + + // On some ports `nimble_port_init` may return an error code indicating a + // failed initialisation. + #if MICROPY_BLUETOOTH_NIMBLE_PORT_INIT_RETURNS_ERROR + if (nimble_port_init() < 0) { + mp_bluetooth_deinit(); + return MP_EIO; + } + #else nimble_port_init(); + #endif ble_hs_cfg.reset_cb = reset_cb; ble_hs_cfg.sync_cb = sync_cb; diff --git a/ports/esp32/mpconfigport.h b/ports/esp32/mpconfigport.h index a1b4594da5654..97c44c8076f47 100644 --- a/ports/esp32/mpconfigport.h +++ b/ports/esp32/mpconfigport.h @@ -108,6 +108,7 @@ #define MICROPY_PY_BLUETOOTH_ENABLE_PAIRING_BONDING (1) #define MICROPY_BLUETOOTH_NIMBLE (1) #define MICROPY_BLUETOOTH_NIMBLE_BINDINGS_ONLY (1) +#define MICROPY_BLUETOOTH_NIMBLE_PORT_INIT_RETURNS_ERROR (1) #endif // MICROPY_PY_BLUETOOTH #define MICROPY_PY_RANDOM_SEED_INIT_FUNC (esp_random()) From 406356ec8b289ae47bbad1bf9869171a7b13fd1d Mon Sep 17 00:00:00 2001 From: Damien George Date: Sat, 7 Feb 2026 23:11:16 +1100 Subject: [PATCH 34/53] extmod/modlwip: Ensure socket is finalisable if error during creation. Because socket objects have a finaliser they must be created carefully, in case an exception is raised during the population of their members, eg invalid input argument or out-of-memory when allocating additional arrays. Prior to the fix in this commit, the finaliser would crash due to `incoming.udp_raw.array` being an invalid pointer in the following cases: - if a SOCK_RAW was created with a proto argument that was not an integer - if a SOCK_DGRAM or SOCK_RAW was created where the allocation of `lwip_incoming_packet_t` failed - if an integer was passed in for the socket type but it was not one of SOCK_STREAM, SOCK_DGRAM or SOCK_RAW Furthermore, if the allocation of `lwip_incoming_packet_t` failed then it may have led to corruption within lwIP when freeing `socket->pcb.raw` because that PCB was not fully set up with its callbacks. This commit fixes all of these issues by ensuring: - `pcb.tcp` and `incoming.udp_raw.array` are initialised to NULL early on - the proto argument is parsed before allocating the PCB - the allocation of `lwip_incoming_packet_t` occurs befor allocating the PCB - `incoming.udp_raw.array` is checked for NULL in the finaliser code The corresponding test (which already checked most of these causes of failure) has been updated to include a previously-uncovered scenario. Signed-off-by: Damien George --- extmod/modlwip.c | 18 ++++++++++++++---- tests/extmod/socket_badconstructor.py | 5 +++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/extmod/modlwip.c b/extmod/modlwip.c index 4a72a05d84b5a..9365bd492b0b2 100644 --- a/extmod/modlwip.c +++ b/extmod/modlwip.c @@ -384,7 +384,7 @@ static void lwip_socket_free_incoming(lwip_socket_obj_t *socket, bool free_queue pbuf_free(socket->incoming.tcp.pbuf); socket->incoming.tcp.pbuf = NULL; } - } else { + } else if (socket->incoming.udp_raw.array != NULL) { for (size_t i = 0; i < LWIP_INCOMING_PACKET_QUEUE_LEN; ++i) { lwip_incoming_packet_t *slot = &socket->incoming.udp_raw.array[i]; if (slot->pbuf != NULL) { @@ -938,7 +938,12 @@ static void lwip_socket_print(const mp_print_t *print, mp_obj_t self_in, mp_prin static mp_obj_t lwip_socket_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { mp_arg_check_num(n_args, n_kw, 0, 4, false); + // Once the socket is allocated it must be in a valid state to be finalised: + // - `incoming.udp_raw.array` is NULL or a valid heap pointer + // - `pcb` is NULL or a valid lwIP PCB that has been fully initialised lwip_socket_obj_t *socket = mp_obj_malloc_with_finaliser(lwip_socket_obj_t, &lwip_socket_type); + socket->pcb.tcp = NULL; + socket->incoming.udp_raw.array = NULL; socket->timeout = -1; socket->recv_offset = 0; socket->domain = MOD_NETWORK_AF_INET; @@ -946,10 +951,16 @@ static mp_obj_t lwip_socket_make_new(const mp_obj_type_t *type, size_t n_args, s socket->callback = MP_OBJ_NULL; socket->state = STATE_NEW; + // Parse given arguments. + uint8_t socket_proto = 0; + (void)socket_proto; if (n_args >= 1) { socket->domain = mp_obj_get_int(args[0]); if (n_args >= 2) { socket->type = mp_obj_get_int(args[1]); + if (n_args >= 3) { + socket_proto = mp_obj_get_int(args[2]); + } } } @@ -963,18 +974,17 @@ static mp_obj_t lwip_socket_make_new(const mp_obj_type_t *type, size_t n_args, s #if MICROPY_PY_LWIP_SOCK_RAW case MOD_NETWORK_SOCK_RAW: #endif + socket->incoming.udp_raw.array = m_new0(lwip_incoming_packet_t, LWIP_INCOMING_PACKET_QUEUE_LEN); if (socket->type == MOD_NETWORK_SOCK_DGRAM) { socket->pcb.udp = udp_new(); } #if MICROPY_PY_LWIP_SOCK_RAW else { - mp_int_t proto = n_args <= 2 ? 0 : mp_obj_get_int(args[2]); - socket->pcb.raw = raw_new(proto); + socket->pcb.raw = raw_new(socket_proto); } #endif socket->incoming.udp_raw.iget = 0; socket->incoming.udp_raw.iput = 0; - socket->incoming.udp_raw.array = m_new0(lwip_incoming_packet_t, LWIP_INCOMING_PACKET_QUEUE_LEN); break; default: mp_raise_OSError(MP_EINVAL); diff --git a/tests/extmod/socket_badconstructor.py b/tests/extmod/socket_badconstructor.py index 4a9d2668c7f1a..1ea5d750b3e65 100644 --- a/tests/extmod/socket_badconstructor.py +++ b/tests/extmod/socket_badconstructor.py @@ -16,6 +16,11 @@ except TypeError: print("TypeError") +try: + s = socket.socket(socket.AF_INET, 123456) +except OSError: + print("OSError") + try: s = socket.socket(socket.AF_INET, socket.SOCK_RAW, None) except TypeError: From 8a3c9f0bf2daf3d270ad4b15ed290ef74e44ced5 Mon Sep 17 00:00:00 2001 From: Jack Whitham Date: Thu, 8 Jan 2026 19:36:49 +0000 Subject: [PATCH 35/53] extmod/modlwip: Call user callback on newly-received UDP or RAW packet. User callbacks allow code to respond to incoming messages without blocking or polling. User callbacks are optional, and if no callback is registered the code has no effect. The mechanism is the same as for TCP: when a connection is accepted or a TCP packet is received, a user callback is executed. Fixes issue #3594. Signed-off-by: Jack Whitham --- extmod/modlwip.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extmod/modlwip.c b/extmod/modlwip.c index 9365bd492b0b2..0e2df8f32903a 100644 --- a/extmod/modlwip.c +++ b/extmod/modlwip.c @@ -463,6 +463,8 @@ static void udp_raw_incoming(lwip_socket_obj_t *socket, struct pbuf *p, const ip slot->peer_addr = *addr; slot->peer_port = port; socket->incoming.udp_raw.iput = (socket->incoming.udp_raw.iput + 1) % LWIP_INCOMING_PACKET_QUEUE_LEN; + // Notify user callback of the new packet + exec_user_callback(socket); } } From 134bf4d847579b6a38955b344017a48e5db3f918 Mon Sep 17 00:00:00 2001 From: iabdalkader Date: Wed, 18 Mar 2026 21:52:31 +0100 Subject: [PATCH 36/53] alif/irq: Add missing IRQ priorities. Add DMA, NPU and PDM IRQ priorities to irq.h. Signed-off-by: iabdalkader --- ports/alif/irq.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ports/alif/irq.h b/ports/alif/irq.h index 86b739795c173..0aa9774748578 100644 --- a/ports/alif/irq.h +++ b/ports/alif/irq.h @@ -43,11 +43,14 @@ #define IRQ_PRI_MHU NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 0, 0) #define IRQ_PRI_QUIET_TIMING NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 1, 0) #define IRQ_PRI_UART_REPL NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 1, 0) +#define IRQ_PRI_DMA NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 2, 0) #define IRQ_PRI_ADC NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 3, 0) #define IRQ_PRI_CSI NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 5, 0) #define IRQ_PRI_USB NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 7, 0) #define IRQ_PRI_HWSEM NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 8, 0) +#define IRQ_PRI_NPU NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 9, 0) #define IRQ_PRI_GPU NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 10, 0) +#define IRQ_PRI_PDM NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 10, 0) #define IRQ_PRI_GPIO NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 50, 0) #define IRQ_PRI_I2C NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 60, 0) #define IRQ_PRI_RTC NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 100, 0) From 82e44e07e711befd9eb49505c180ed6303e4b836 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 10 Feb 2026 01:57:58 +1100 Subject: [PATCH 37/53] stm32/mpthreadport: Increase minimum thread stack size to 2.5k. 2.25k Seems necessary so it doesn't crash `thread/thread_stacksize1.py`. But 2.5k gives a little extra headroom to make that test actually pass. Signed-off-by: Damien George --- ports/stm32/mpthreadport.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ports/stm32/mpthreadport.c b/ports/stm32/mpthreadport.c index 621b4311bfc34..369171067e50e 100644 --- a/ports/stm32/mpthreadport.c +++ b/ports/stm32/mpthreadport.c @@ -61,8 +61,8 @@ mp_uint_t mp_thread_get_id(void) { mp_uint_t mp_thread_create(void *(*entry)(void *), void *arg, size_t *stack_size) { if (*stack_size == 0) { *stack_size = 4096; // default stack size - } else if (*stack_size < 2048) { - *stack_size = 2048; // minimum stack size + } else if (*stack_size < 2560) { + *stack_size = 2560; // minimum stack size } // round stack size to a multiple of the word size From 702f15ab9800769c76539d760fe7b365d8654595 Mon Sep 17 00:00:00 2001 From: Damien George Date: Sat, 21 Mar 2026 16:04:46 +1100 Subject: [PATCH 38/53] stm32/boards/PYBD_SF2: Free up some space in internal flash. With the recent addition of `machine.PWM` and `machine.CAN`, the internal flash of PYBD_SF3 overflows by about 300 bytes. This commit moves the inline assembler compiler functions from internal to external QSPI flash. That frees up about 3k internal flash, and shouldn't affect performance. Signed-off-by: Damien George --- ports/stm32/boards/PYBD_SF2/f722_qspi.ld | 1 + 1 file changed, 1 insertion(+) diff --git a/ports/stm32/boards/PYBD_SF2/f722_qspi.ld b/ports/stm32/boards/PYBD_SF2/f722_qspi.ld index 354c1919b41a3..8ada037d6e050 100644 --- a/ports/stm32/boards/PYBD_SF2/f722_qspi.ld +++ b/ports/stm32/boards/PYBD_SF2/f722_qspi.ld @@ -50,6 +50,7 @@ SECTIONS .text_ext : { . = ALIGN(4); + *py/emit*(.text.emit_inline_thumb_* .rodata.emit_inline_thumb_*) *lib/btstack/*(.text* .rodata*) *lib/mbedtls/*(.text* .rodata*) *lib/mynewt-nimble/*(.text* .rodata*) From 803a4d77171ff8a994fe0ee7cc88a7e6129cb64a Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 13 Mar 2026 10:32:57 -0500 Subject: [PATCH 39/53] py/objtemplate: Correctly cast qstr literals when printing. qstr literals are of type qstr_short_t (aka uint16_t) for efficiency, but when the type is passed to `mp_printf` it must be cast explicitly to type `qstr`. These locations were found using an experimental gcc plugin for `mp_printf` error checking. Signed-off-by: Jeff Epler --- py/objtemplate.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/py/objtemplate.c b/py/objtemplate.c index 0fc51a78d9a19..86451350a2143 100644 --- a/py/objtemplate.c +++ b/py/objtemplate.c @@ -152,9 +152,9 @@ static mp_obj_t mp_obj_template_make_new(const mp_obj_type_t *type, size_t n_arg static void mp_obj_template_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { (void)kind; mp_obj_template_t *self = MP_OBJ_TO_PTR(self_in); - mp_printf(print, "%q(%q=", MP_QSTR_Template, MP_QSTR_strings); + mp_printf(print, "%q(%q=", (qstr)MP_QSTR_Template, (qstr)MP_QSTR_strings); mp_obj_print_helper(print, self->strings, PRINT_REPR); - mp_printf(print, ", %q=", MP_QSTR_interpolations); + mp_printf(print, ", %q=", (qstr)MP_QSTR_interpolations); mp_obj_print_helper(print, self->interpolations, PRINT_REPR); mp_print_str(print, ")"); } @@ -346,7 +346,7 @@ static mp_obj_t mp_obj_interpolation_make_new(const mp_obj_type_t *type, size_t static void mp_obj_interpolation_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { (void)kind; mp_obj_interpolation_t *self = MP_OBJ_TO_PTR(self_in); - mp_printf(print, "%q(", MP_QSTR_Interpolation); + mp_printf(print, "%q(", (qstr)MP_QSTR_Interpolation); mp_obj_print_helper(print, self->value, PRINT_REPR); mp_print_str(print, ", "); mp_obj_print_helper(print, self->expression, PRINT_REPR); From 74e945752b91798c4c5d9211df59f69fe37b69a4 Mon Sep 17 00:00:00 2001 From: Damien George Date: Thu, 5 Feb 2026 00:49:06 +1100 Subject: [PATCH 40/53] py/modweakref: Implement weakref module with ref and finalize classes. This adds support for the standard `weakref` module, to make weak references to Python objects and have callbacks for when an object is reclaimed by the GC. This feature was requested by PyScript, to allow control over the lifetime of external proxy objects (distinct from JS<->Python proxies). Addresses issue #646 (that's nearly a 12 year old issue!). Functionality added here: - `weakref.ref(object [, callback])` create a simple weak reference with optional callback to be called when the object is reclaimed by the GC - `weakref.finalize(object, callback, /, *args, **kwargs)` create a finalize object that holds a weak reference to an object and allows more convenient callback usage and state change The new module is enabled at the "everything" level. The implementation aims to be as efficient as possible, by adding another bit-per-block to the garbage collector, the WTB (weak table). Similar to the finalizer bit (FTB), if a GC block has its corresponding WTB bit set then a weak reference to that block is held. The details of that weak reference are stored in a global map, `mp_weakref_map`, which maps weak reference to ref/finalize objects, allowing the callbacks to be efficiently found when the object is reclaimed. With this feature enabled the overhead is: - 1/128th of the available memory is used for the new WTB table (eg a 128k heap now needs an extra 1k for the WTB). - Code size is increased. - At garbage collection time, there is a small overhead to check if the collected objects had weak references. This check is the same as the existing FTB finaliser scan, so shouldn't add much overhead. If there are weak reference objects alive (ref/finalize objects) then additional time is taken to call the callbacks and do some accounting to clean up the used weak reference. Signed-off-by: Damien George --- py/gc.c | 118 ++++++++-- py/gc.h | 5 + py/modweakref.c | 314 +++++++++++++++++++++++++ py/mpconfig.h | 5 + py/mpstate.h | 3 + py/py.cmake | 1 + py/py.mk | 1 + py/runtime.c | 4 + tests/ports/unix/extra_coverage.py.exp | 2 +- 9 files changed, 433 insertions(+), 20 deletions(-) create mode 100644 py/modweakref.c diff --git a/py/gc.c b/py/gc.c index 0d4d19ce9e74c..5fe26ef8905c5 100644 --- a/py/gc.c +++ b/py/gc.c @@ -104,14 +104,21 @@ #if MICROPY_ENABLE_FINALISER // FTB = finaliser table byte // if set, then the corresponding block may have a finaliser - #define BLOCKS_PER_FTB (8) - #define FTB_GET(area, block) ((area->gc_finaliser_table_start[(block) / BLOCKS_PER_FTB] >> ((block) & 7)) & 1) #define FTB_SET(area, block) do { area->gc_finaliser_table_start[(block) / BLOCKS_PER_FTB] |= (1 << ((block) & 7)); } while (0) #define FTB_CLEAR(area, block) do { area->gc_finaliser_table_start[(block) / BLOCKS_PER_FTB] &= (~(1 << ((block) & 7))); } while (0) #endif +#if MICROPY_PY_WEAKREF +// WTB = weakref table byte +// if set, then the corresponding block may have a weakref in MP_STATE_VM(mp_weakref_map). +#define BLOCKS_PER_WTB (8) +#define WTB_GET(area, block) ((area->gc_weakref_table_start[(block) / BLOCKS_PER_WTB] >> ((block) & 7)) & 1) +#define WTB_SET(area, block) do { area->gc_weakref_table_start[(block) / BLOCKS_PER_WTB] |= (1 << ((block) & 7)); } while (0) +#define WTB_CLEAR(area, block) do { area->gc_weakref_table_start[(block) / BLOCKS_PER_WTB] &= (~(1 << ((block) & 7))); } while (0) +#endif + #if MICROPY_PY_THREAD && !MICROPY_PY_THREAD_GIL #define GC_MUTEX_INIT() mp_thread_recursive_mutex_init(&MP_STATE_MEM(gc_mutex)) #define GC_ENTER() mp_thread_recursive_mutex_lock(&MP_STATE_MEM(gc_mutex), 1) @@ -138,17 +145,23 @@ static void gc_sweep_free_blocks(void); // TODO waste less memory; currently requires that all entries in alloc_table have a corresponding block in pool static void gc_setup_area(mp_state_mem_area_t *area, void *start, void *end) { // calculate parameters for GC (T=total, A=alloc table, F=finaliser table, P=pool; all in bytes): - // T = A + F + P + // T = A + F + W + P // F = A * BLOCKS_PER_ATB / BLOCKS_PER_FTB + // W = A * BLOCKS_PER_ATB / BLOCKS_PER_WTB // P = A * BLOCKS_PER_ATB * BYTES_PER_BLOCK - // => T = A * (1 + BLOCKS_PER_ATB / BLOCKS_PER_FTB + BLOCKS_PER_ATB * BYTES_PER_BLOCK) + // => T = A * (1 + BLOCKS_PER_ATB / BLOCKS_PER_FTB + BLOCKS_PER_ATB / BLOCKS_PER_WTB + BLOCKS_PER_ATB * BYTES_PER_BLOCK) size_t total_byte_len = (byte *)end - (byte *)start; - #if MICROPY_ENABLE_FINALISER + #if MICROPY_ENABLE_FINALISER || MICROPY_PY_WEAKREF area->gc_alloc_table_byte_len = (total_byte_len - ALLOC_TABLE_GAP_BYTE) * MP_BITS_PER_BYTE / ( MP_BITS_PER_BYTE + #if MICROPY_ENABLE_FINALISER + MP_BITS_PER_BYTE * BLOCKS_PER_ATB / BLOCKS_PER_FTB + #endif + #if MICROPY_PY_WEAKREF + + MP_BITS_PER_BYTE * BLOCKS_PER_ATB / BLOCKS_PER_WTB + #endif + MP_BITS_PER_BYTE * BLOCKS_PER_ATB * BYTES_PER_BLOCK ); #else @@ -157,26 +170,36 @@ static void gc_setup_area(mp_state_mem_area_t *area, void *start, void *end) { area->gc_alloc_table_start = (byte *)start; + // Allocate FTB and WTB blocks if they are enabled. + byte *next_table = area->gc_alloc_table_start + area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE; + (void)next_table; #if MICROPY_ENABLE_FINALISER size_t gc_finaliser_table_byte_len = (area->gc_alloc_table_byte_len * BLOCKS_PER_ATB + BLOCKS_PER_FTB - 1) / BLOCKS_PER_FTB; - area->gc_finaliser_table_start = area->gc_alloc_table_start + area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE; + area->gc_finaliser_table_start = next_table; + next_table += gc_finaliser_table_byte_len; + #endif + #if MICROPY_PY_WEAKREF + size_t gc_weakref_table_byte_len = (area->gc_alloc_table_byte_len * BLOCKS_PER_ATB + BLOCKS_PER_WTB - 1) / BLOCKS_PER_WTB; + area->gc_weakref_table_start = next_table; + next_table += gc_weakref_table_byte_len; #endif + // Allocate the GC pool of heap blocks. size_t gc_pool_block_len = area->gc_alloc_table_byte_len * BLOCKS_PER_ATB; area->gc_pool_start = (byte *)end - gc_pool_block_len * BYTES_PER_BLOCK; area->gc_pool_end = end; + assert(area->gc_pool_start >= next_table); - #if MICROPY_ENABLE_FINALISER - assert(area->gc_pool_start >= area->gc_finaliser_table_start + gc_finaliser_table_byte_len); - #endif - - #if MICROPY_ENABLE_FINALISER - // clear ATB's and FTB's - memset(area->gc_alloc_table_start, 0, gc_finaliser_table_byte_len + area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE); - #else - // clear ATB's - memset(area->gc_alloc_table_start, 0, area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE); - #endif + // Clear ATB's, and FTB's and WTB's if they are enabled. + memset(area->gc_alloc_table_start, 0, + area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE + #if MICROPY_ENABLE_FINALISER + + gc_finaliser_table_byte_len + #endif + #if MICROPY_PY_WEAKREF + + gc_weakref_table_byte_len + #endif + ); area->gc_last_free_atb_index = 0; area->gc_last_used_block = 0; @@ -196,6 +219,12 @@ static void gc_setup_area(mp_state_mem_area_t *area, void *start, void *end) { gc_finaliser_table_byte_len, gc_finaliser_table_byte_len * BLOCKS_PER_FTB); #endif + #if MICROPY_PY_WEAKREF + DEBUG_printf(" weakref table at %p, length " UINT_FMT " bytes, " + UINT_FMT " blocks\n", area->gc_weakref_table_start, + gc_weakref_table_byte_len, + gc_weakref_table_byte_len * BLOCKS_PER_WTB); + #endif DEBUG_printf(" pool at %p, length " UINT_FMT " bytes, " UINT_FMT " blocks\n", area->gc_pool_start, gc_pool_block_len * BYTES_PER_BLOCK, gc_pool_block_len); @@ -310,6 +339,9 @@ static bool gc_try_add_heap(size_t failed_alloc) { #if MICROPY_ENABLE_FINALISER + total_blocks / BLOCKS_PER_FTB #endif + #if MICROPY_PY_WEAKREF + + total_blocks / BLOCKS_PER_WTB + #endif + total_blocks * BYTES_PER_BLOCK + ALLOC_TABLE_GAP_BYTE + sizeof(mp_state_mem_area_t); @@ -556,6 +588,9 @@ void gc_collect_end(void) { } MP_STATE_THREAD(gc_lock_depth) &= ~GC_COLLECT_FLAG; GC_EXIT(); + #if MICROPY_PY_WEAKREF + gc_weakref_sweep(); + #endif } static void gc_deal_with_stack_overflow(void) { @@ -581,12 +616,16 @@ static void gc_deal_with_stack_overflow(void) { // Run finalisers for all to-be-freed blocks static void gc_sweep_run_finalisers(void) { - #if MICROPY_ENABLE_FINALISER + #if MICROPY_ENABLE_FINALISER || MICROPY_PY_WEAKREF + #if MICROPY_ENABLE_FINALISER && MICROPY_PY_WEAKREF + MP_STATIC_ASSERT(BLOCKS_PER_FTB == BLOCKS_PER_WTB); + #endif for (const mp_state_mem_area_t *area = &MP_STATE_MEM(area); area != NULL; area = NEXT_AREA(area)) { assert(area->gc_last_used_block <= area->gc_alloc_table_byte_len * BLOCKS_PER_ATB); // Small speed optimisation: skip over empty FTB blocks size_t ftb_end = area->gc_last_used_block / BLOCKS_PER_FTB; // index is inclusive for (size_t ftb_idx = 0; ftb_idx <= ftb_end; ftb_idx++) { + #if MICROPY_ENABLE_FINALISER byte ftb = area->gc_finaliser_table_start[ftb_idx]; size_t block = ftb_idx * BLOCKS_PER_FTB; while (ftb) { @@ -616,9 +655,26 @@ static void gc_sweep_run_finalisers(void) { ftb >>= 1; block++; } + #endif + #if MICROPY_PY_WEAKREF + byte wtb = area->gc_weakref_table_start[ftb_idx]; + block = ftb_idx * BLOCKS_PER_WTB; + while (wtb) { + MICROPY_GC_HOOK_LOOP(block); + if (wtb & 1) { // WTB_GET(area, block) shortcut + if (ATB_GET_KIND(area, block) == AT_HEAD) { + mp_obj_base_t *obj = (mp_obj_base_t *)PTR_FROM_BLOCK(area, block); + gc_weakref_about_to_be_freed(obj); + WTB_CLEAR(area, block); + } + } + wtb >>= 1; + block++; + } + #endif } } - #endif // MICROPY_ENABLE_FINALISER + #endif // MICROPY_ENABLE_FINALISER || MICROPY_PY_WEAKREF } // Free unmarked heads and their tails @@ -769,6 +825,25 @@ void gc_info(gc_info_t *info) { GC_EXIT(); } +#if MICROPY_PY_WEAKREF +// Mark the GC heap pointer as having a weakref. +void gc_weakref_mark(void *ptr) { + mp_state_mem_area_t *area; + #if MICROPY_GC_SPLIT_HEAP + area = gc_get_ptr_area(ptr); + assert(area); + #else + assert(VERIFY_PTR(ptr)); + area = &MP_STATE_MEM(area); + #endif + + size_t block = BLOCK_FROM_PTR(area, ptr); + assert(ATB_GET_KIND(area, block) == AT_HEAD); + + WTB_SET(area, block); +} +#endif + void *gc_alloc(size_t n_bytes, unsigned int alloc_flags) { bool has_finaliser = alloc_flags & GC_ALLOC_FLAG_HAS_FINALISER; size_t n_blocks = ((n_bytes + BYTES_PER_BLOCK - 1) & (~(BYTES_PER_BLOCK - 1))) / BYTES_PER_BLOCK; @@ -967,6 +1042,11 @@ void gc_free(void *ptr) { FTB_CLEAR(area, block); #endif + #if MICROPY_PY_WEAKREF + // Objects that have a weak reference should not be explicitly freed. + assert(!WTB_GET(area, block)); + #endif + #if MICROPY_GC_SPLIT_HEAP if (MP_STATE_MEM(gc_last_free_area) != area) { // We freed something but it isn't the current area. Reset the diff --git a/py/gc.h b/py/gc.h index 36177633062b2..4679d6dc8632f 100644 --- a/py/gc.h +++ b/py/gc.h @@ -58,6 +58,11 @@ void gc_collect_end(void); // Use this function to sweep the whole heap and run all finalisers void gc_sweep_all(void); +// These functions are used to manage weakrefs. +void gc_weakref_mark(void *ptr); +void gc_weakref_about_to_be_freed(void *ptr); +void gc_weakref_sweep(void); + enum { GC_ALLOC_FLAG_HAS_FINALISER = 1, }; diff --git a/py/modweakref.c b/py/modweakref.c new file mode 100644 index 0000000000000..3360a4e20950d --- /dev/null +++ b/py/modweakref.c @@ -0,0 +1,314 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "py/gc.h" +#include "py/runtime.h" + +#if MICROPY_PY_WEAKREF + +// Macros to obfuscate a heap pointer as a small integer object. +#define PTR_TO_INT_OBJ(ptr) (MP_OBJ_NEW_SMALL_INT(((uintptr_t)ptr) >> 1)) +#define PTR_FROM_INT_OBJ(obj) ((void *)(MP_OBJ_SMALL_INT_VALUE((obj)) << 1)) + +// Macros to convert between a weak reference and a heap pointer. +#define WEAK_REFERENCE_FROM_HEAP_PTR(ptr) PTR_TO_INT_OBJ(ptr) +#define WEAK_REFERENCE_TO_HEAP_PTR(weak_ref) PTR_FROM_INT_OBJ(weak_ref) + +// Macros to manage ref-finalizer linked-list pointers. +// - mp_obj_ref_t is obfuscated as a small integer object so it's not traced by the GC. +// - mp_obj_finalize_t is stored as-is so it is traced by the GC. +#define REF_FIN_LIST_OBJ_IS_FIN(r) (!mp_obj_is_small_int((r))) +#define REF_FIN_LIST_OBJ_TO_PTR(r) ((mp_obj_ref_t *)(mp_obj_is_small_int((r)) ? PTR_FROM_INT_OBJ((r)) : MP_OBJ_TO_PTR((r)))) +#define REF_FIN_LIST_OBJ_FROM_REF(r) (PTR_TO_INT_OBJ((r))) +#define REF_FIN_LIST_OBJ_FROM_FIN(r) (MP_OBJ_FROM_PTR((r))) +#define REF_FIN_LIST_OBJ_TAIL (PTR_TO_INT_OBJ(NULL)) + +// weakref.ref() instance. +typedef struct _mp_obj_ref_t { + mp_obj_base_t base; + mp_obj_t ref_fin_next; + mp_obj_t obj_weak_ref; + mp_obj_t callback; +} mp_obj_ref_t; + +// weakref.finalize() instance. +// This is an extension of weakref.ref() and shares a lot of code with it. +typedef struct _mp_obj_finalize_t { + mp_obj_ref_t base; + size_t n_args; + size_t n_kw; + mp_obj_t *args; +} mp_obj_finalize_t; + +static const mp_obj_type_t mp_type_ref; +static const mp_obj_type_t mp_type_finalize; + +static mp_obj_t ref___del__(mp_obj_t self_in); + +void gc_weakref_about_to_be_freed(void *ptr) { + mp_obj_t idx = WEAK_REFERENCE_FROM_HEAP_PTR(ptr); + mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_weakref_map), idx, MP_MAP_LOOKUP); + if (elem != NULL) { + // Mark element as being freed. + elem->key = mp_const_none; + } +} + +void gc_weakref_sweep(void) { + mp_map_t *map = &MP_STATE_VM(mp_weakref_map); + for (size_t i = 0; i < map->alloc; i++) { + if (map->table[i].key == mp_const_none) { + // Element was just freed, so call all the registered callbacks. + --map->used; + map->table[i].key = MP_OBJ_SENTINEL; + mp_obj_ref_t *ref = REF_FIN_LIST_OBJ_TO_PTR(map->table[i].value); + map->table[i].value = MP_OBJ_NULL; + while (ref != NULL) { + // Invalidate the weak reference. + assert(ref->obj_weak_ref != mp_const_none); + ref->obj_weak_ref = mp_const_none; + + // Call any registered callbacks. + if (ref->callback != mp_const_none) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + if (ref->base.type == &mp_type_ref) { + // weakref.ref() type. + mp_call_function_1(ref->callback, MP_OBJ_FROM_PTR(ref)); + } else { + // weakref.finalize() type. + mp_obj_finalize_t *fin = (mp_obj_finalize_t *)ref; + mp_call_function_n_kw(fin->base.callback, fin->n_args, fin->n_kw, fin->args); + } + nlr_pop(); + } else { + mp_printf(MICROPY_ERROR_PRINTER, "Unhandled exception in weakref callback:\n"); + mp_obj_print_exception(MICROPY_ERROR_PRINTER, MP_OBJ_FROM_PTR(nlr.ret_val)); + } + } + + // Unlink the node. + mp_obj_ref_t *ref_fin_next = REF_FIN_LIST_OBJ_TO_PTR(ref->ref_fin_next); + ref->ref_fin_next = REF_FIN_LIST_OBJ_TAIL; + ref = ref_fin_next; + } + } + } +} + +static mp_obj_t mp_obj_ref_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { + if (type == &mp_type_ref) { + // weakref.ref() type. + mp_arg_check_num(n_args, n_kw, 1, 2, false); + } else { + // weakref.finalize() type. + mp_arg_check_num(n_args, n_kw, 2, MP_OBJ_FUN_ARGS_MAX, true); + } + + // Validate the input object can have a weakref. + void *ptr = NULL; + if (mp_obj_is_obj(args[0])) { + ptr = MP_OBJ_TO_PTR(args[0]); + if (gc_nbytes(ptr) == 0) { + ptr = NULL; + } + } + if (ptr == NULL) { + mp_raise_TypeError(MP_ERROR_TEXT("not a heap object")); + } + + // Create or get the entry in mp_weakref_map corresponding to this object. + mp_obj_t obj_weak_reference = WEAK_REFERENCE_FROM_HEAP_PTR(ptr); + mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_weakref_map), obj_weak_reference, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND); + if (elem->value == MP_OBJ_NULL) { + // This heap object does not have any existing weakref's, so initialise it. + elem->value = REF_FIN_LIST_OBJ_TAIL; + gc_weakref_mark(ptr); + } + + mp_obj_ref_t *self; + if (type == &mp_type_ref) { + // Create a new weakref.ref() object. + self = mp_obj_malloc_with_finaliser(mp_obj_ref_t, type); + // Link this new ref into the list of all refs/finalizers pointing to this object. + // To ensure it will *NOT* be traced by the GC (the user must manually hold onto it), + // store an integer version of the object after any weakref.finalize() objects (so + // the weakref.finalize() objects continue to be traced by the GC). + mp_obj_t *link = &elem->value; + while (REF_FIN_LIST_OBJ_IS_FIN(*link)) { + link = &REF_FIN_LIST_OBJ_TO_PTR(*link)->ref_fin_next; + } + self->ref_fin_next = *link; + *link = REF_FIN_LIST_OBJ_FROM_REF(self); + } else { + // Create a new weakref.finalize() object. + mp_obj_finalize_t *self_fin = mp_obj_malloc(mp_obj_finalize_t, type); + self_fin->n_args = n_args - 2; + self_fin->n_kw = n_kw; + size_t n_args_kw = self_fin->n_args + self_fin->n_kw * 2; + if (n_args_kw == 0) { + self_fin->args = NULL; + } else { + self_fin->args = m_new(mp_obj_t, n_args_kw); + memcpy(self_fin->args, args + 2, n_args_kw * sizeof(mp_obj_t)); + } + self = &self_fin->base; + // Link this new finalizer into the list of all refs/finalizers pointing to this object. + // To ensure it will be traced by the GC, store its pointer at the start of the list. + self->ref_fin_next = elem->value; + elem->value = REF_FIN_LIST_OBJ_FROM_FIN(self_fin); + } + + // Populate the object weak reference, and the callback. + self->obj_weak_ref = obj_weak_reference; + if (n_args > 1) { + self->callback = args[1]; + } else { + self->callback = mp_const_none; + } + + return MP_OBJ_FROM_PTR(self); +} + +static mp_obj_t mp_obj_ref_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) { + mp_obj_ref_t *self = MP_OBJ_TO_PTR(self_in); + if (self->obj_weak_ref == mp_const_none) { + return mp_const_none; + } + if (self->base.type == &mp_type_ref) { + // weakref.ref() type. + return MP_OBJ_FROM_PTR(WEAK_REFERENCE_TO_HEAP_PTR(self->obj_weak_ref)); + } else { + // weakref.finalize() type. + mp_obj_finalize_t *self_fin = MP_OBJ_TO_PTR(self_in); + ref___del__(self_in); + return mp_call_function_n_kw(self_fin->base.callback, self_fin->n_args, self_fin->n_kw, self_fin->args); + } +} + +static mp_obj_t ref___del__(mp_obj_t self_in) { + mp_obj_ref_t *self = MP_OBJ_TO_PTR(self_in); + mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_weakref_map), self->obj_weak_ref, MP_MAP_LOOKUP); + if (elem != NULL) { + for (mp_obj_t *link = &elem->value; REF_FIN_LIST_OBJ_TO_PTR(*link) != NULL; link = &REF_FIN_LIST_OBJ_TO_PTR(*link)->ref_fin_next) { + if (self == REF_FIN_LIST_OBJ_TO_PTR(*link)) { + // Unlink and clear this node. + *link = self->ref_fin_next; + self->ref_fin_next = REF_FIN_LIST_OBJ_TAIL; + self->obj_weak_ref = mp_const_none; + break; + } + } + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(ref___del___obj, ref___del__); + +static mp_obj_t finalize_peek_detach_helper(mp_obj_t self_in, bool detach) { + mp_obj_finalize_t *self = MP_OBJ_TO_PTR(self_in); + if (self->base.obj_weak_ref == mp_const_none) { + return mp_const_none; + } + mp_obj_t tuple[4] = { + MP_OBJ_FROM_PTR(WEAK_REFERENCE_TO_HEAP_PTR(self->base.obj_weak_ref)), + self->base.callback, + mp_obj_new_tuple(self->n_args, self->args), + mp_obj_dict_make_new(&mp_type_dict, 0, self->n_kw, self->args + self->n_args), + }; + if (detach) { + ref___del__(self_in); + } + return mp_obj_new_tuple(MP_ARRAY_SIZE(tuple), tuple); +} + +static mp_obj_t finalize_peek(mp_obj_t self_in) { + return finalize_peek_detach_helper(self_in, false); +} +static MP_DEFINE_CONST_FUN_OBJ_1(finalize_peek_obj, finalize_peek); + +static mp_obj_t finalize_detach(mp_obj_t self_in) { + return finalize_peek_detach_helper(self_in, true); +} +static MP_DEFINE_CONST_FUN_OBJ_1(finalize_detach_obj, finalize_detach); + +static void mp_obj_finalize_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { + if (dest[0] != MP_OBJ_NULL) { + // Store/delete attribute, unsupported. + return; + } + + if (attr == MP_QSTR_alive) { + mp_obj_finalize_t *self = MP_OBJ_TO_PTR(self_in); + dest[0] = mp_obj_new_bool(self->base.obj_weak_ref != mp_const_none); + return; + } else if (attr == MP_QSTR_peek) { + dest[0] = MP_OBJ_FROM_PTR(&finalize_peek_obj); + dest[1] = self_in; + } else if (attr == MP_QSTR_detach) { + dest[0] = MP_OBJ_FROM_PTR(&finalize_detach_obj); + dest[1] = self_in; + } +} + +static const mp_rom_map_elem_t ref_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&ref___del___obj) }, +}; +static MP_DEFINE_CONST_DICT(ref_locals_dict, ref_locals_dict_table); + +static MP_DEFINE_CONST_OBJ_TYPE( + mp_type_ref, + MP_QSTR_ref, + MP_TYPE_FLAG_NONE, + make_new, mp_obj_ref_make_new, + call, mp_obj_ref_call, + locals_dict, &ref_locals_dict + ); + +static MP_DEFINE_CONST_OBJ_TYPE( + mp_type_finalize, + MP_QSTR_finalize, + MP_TYPE_FLAG_NONE, + make_new, mp_obj_ref_make_new, + call, mp_obj_ref_call, + attr, mp_obj_finalize_attr + ); + +static const mp_rom_map_elem_t mp_module_weakref_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_weakref) }, + { MP_ROM_QSTR(MP_QSTR_ref), MP_ROM_PTR(&mp_type_ref) }, + { MP_ROM_QSTR(MP_QSTR_finalize), MP_ROM_PTR(&mp_type_finalize) }, +}; +static MP_DEFINE_CONST_DICT(mp_module_weakref_globals, mp_module_weakref_globals_table); + +const mp_obj_module_t mp_module_weakref = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&mp_module_weakref_globals, +}; + +MP_REGISTER_ROOT_POINTER(mp_map_t mp_weakref_map); +MP_REGISTER_MODULE(MP_QSTR_weakref, mp_module_weakref); + +#endif // MICROPY_PY_WEAKREF diff --git a/py/mpconfig.h b/py/mpconfig.h index d0150ec7b9995..0951651e7d6a3 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -1914,6 +1914,11 @@ typedef time_t mp_timestamp_t; #define MICROPY_PY_THREAD_RECURSIVE_MUTEX (MICROPY_PY_THREAD && !MICROPY_PY_THREAD_GIL) #endif +// Whether to provide the "weakref" module. +#ifndef MICROPY_PY_WEAKREF +#define MICROPY_PY_WEAKREF (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING) +#endif + // Extended modules #ifndef MICROPY_PY_ASYNCIO diff --git a/py/mpstate.h b/py/mpstate.h index 325c12217521f..32d1adb13ed74 100644 --- a/py/mpstate.h +++ b/py/mpstate.h @@ -107,6 +107,9 @@ typedef struct _mp_state_mem_area_t { #if MICROPY_ENABLE_FINALISER byte *gc_finaliser_table_start; #endif + #if MICROPY_PY_WEAKREF + byte *gc_weakref_table_start; + #endif byte *gc_pool_start; byte *gc_pool_end; diff --git a/py/py.cmake b/py/py.cmake index 8b3c857ef4885..c2efab556c475 100644 --- a/py/py.cmake +++ b/py/py.cmake @@ -54,6 +54,7 @@ set(MICROPY_SOURCE_PY ${MICROPY_PY_DIR}/modsys.c ${MICROPY_PY_DIR}/modthread.c ${MICROPY_PY_DIR}/moderrno.c + ${MICROPY_PY_DIR}/modweakref.c ${MICROPY_PY_DIR}/mpprint.c ${MICROPY_PY_DIR}/mpstate.c ${MICROPY_PY_DIR}/mpz.c diff --git a/py/py.mk b/py/py.mk index a8b50b8d24b7e..932c47ef1773f 100644 --- a/py/py.mk +++ b/py/py.mk @@ -204,6 +204,7 @@ PY_CORE_O_BASENAME = $(addprefix py/,\ modsys.o \ moderrno.o \ modthread.o \ + modweakref.o \ vm.o \ bc.o \ showbc.o \ diff --git a/py/runtime.c b/py/runtime.c index d35cf4025f757..618e9b5ae41cf 100644 --- a/py/runtime.c +++ b/py/runtime.c @@ -179,6 +179,10 @@ void mp_init(void) { MP_STATE_VM(usbd) = MP_OBJ_NULL; #endif + #if MICROPY_PY_WEAKREF + mp_map_init(&MP_STATE_VM(mp_weakref_map), 0); + #endif + #if MICROPY_PY_THREAD_GIL mp_thread_mutex_init(&MP_STATE_VM(gil_mutex)); #endif diff --git a/tests/ports/unix/extra_coverage.py.exp b/tests/ports/unix/extra_coverage.py.exp index 73393e68b6328..1c3fe1558f239 100644 --- a/tests/ports/unix/extra_coverage.py.exp +++ b/tests/ports/unix/extra_coverage.py.exp @@ -75,7 +75,7 @@ json machine marshal math os platform random re select socket string struct sys termios time tls -uctypes vfs websocket +uctypes vfs weakref websocket me micropython machine marshal math From c91d09a00dc047bba4a5215bab50dea54ea82138 Mon Sep 17 00:00:00 2001 From: Damien George Date: Fri, 13 Feb 2026 01:11:00 +1100 Subject: [PATCH 41/53] tests/basics: Add tests for weakref.ref and weakref.finalize. Signed-off-by: Damien George --- tests/basics/weakref_finalize_basic.py | 58 ++++++++++++++++++ tests/basics/weakref_finalize_collect.py | 75 +++++++++++++++++++++++ tests/basics/weakref_multiple_refs.py | 34 ++++++++++ tests/basics/weakref_multiple_refs.py.exp | 6 ++ tests/basics/weakref_ref_basic.py | 14 +++++ tests/basics/weakref_ref_collect.py | 68 ++++++++++++++++++++ tests/run-tests.py | 2 + 7 files changed, 257 insertions(+) create mode 100644 tests/basics/weakref_finalize_basic.py create mode 100644 tests/basics/weakref_finalize_collect.py create mode 100644 tests/basics/weakref_multiple_refs.py create mode 100644 tests/basics/weakref_multiple_refs.py.exp create mode 100644 tests/basics/weakref_ref_basic.py create mode 100644 tests/basics/weakref_ref_collect.py diff --git a/tests/basics/weakref_finalize_basic.py b/tests/basics/weakref_finalize_basic.py new file mode 100644 index 0000000000000..792cffacb1385 --- /dev/null +++ b/tests/basics/weakref_finalize_basic.py @@ -0,0 +1,58 @@ +# Test weakref.finalize() functionality that doesn't require gc.collect(). + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# Cannot reference non-heap objects. +for value in (None, False, True, Ellipsis, 0, "", ()): + try: + weakref.finalize(value, lambda: None) + except TypeError: + print(value, "TypeError") + + +# Convert (obj, func, args, kwargs) so CPython and MicroPython have a chance to match. +def convert_4_tuple(values): + if values is None: + return None + return (type(values[0]).__name__, type(values[1]), values[2], values[3]) + + +class A: + def __str__(self): + return "" + + +print("test alive, peek, detach") +a = A() +f = weakref.finalize(a, lambda: None, 1, 2, kwarg=3) +print("alive", f.alive) +print("peek", convert_4_tuple(f.peek())) +print("detach", convert_4_tuple(f.detach())) +print("alive", f.alive) +print("peek", convert_4_tuple(f.peek())) +print("detach", convert_4_tuple(f.detach())) +print("call", f()) +a = None + +print("test alive, peek, call") +a = A() +f = weakref.finalize(a, lambda *args, **kwargs: (args, kwargs), 1, 2, kwarg=3) +print("alive", f.alive) +print("peek", convert_4_tuple(f.peek())) +print("call", f()) +print("alive", f.alive) +print("peek", convert_4_tuple(f.peek())) +print("call", f()) +print("detach", convert_4_tuple(f.detach())) + +print("test call which raises exception") +a = A() +f = weakref.finalize(a, lambda: 1 / 0) +try: + f() +except ZeroDivisionError as er: + print("call ZeroDivisionError") diff --git a/tests/basics/weakref_finalize_collect.py b/tests/basics/weakref_finalize_collect.py new file mode 100644 index 0000000000000..d364be9f62d67 --- /dev/null +++ b/tests/basics/weakref_finalize_collect.py @@ -0,0 +1,75 @@ +# Test weakref.finalize() functionality requiring gc.collect(). + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# gc module must be available if weakref is. +import gc + + +class A: + def __str__(self): + return "" + + +def callback(*args, **kwargs): + print("callback({}, {})".format(args, kwargs)) + return 42 + + +def test(): + print("test basic use of finalize() with a simple callback") + a = A() + f = weakref.finalize(a, callback) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("alive", f.alive) + print("peek", f.peek()) + print("detach", f.detach()) + print("call", f()) + + print("test that a callback is passed the correct values") + a = A() + f = weakref.finalize(a, callback, 1, 2, kwarg=3) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("alive", f.alive) + print("peek", f.peek()) + print("detach", f.detach()) + print("call", f()) + + print("test that calling the finalizer cancels the finalizer") + a = A() + f = weakref.finalize(a, callback) + print(f()) + print(a) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + + print("test that calling detach cancels the finalizer") + a = A() + f = weakref.finalize(a, callback) + print(len(f.detach())) + print(a) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + + print("test that finalize does not get collected before its ref does") + a = A() + weakref.finalize(a, callback) + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("free a") + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + + +test() diff --git a/tests/basics/weakref_multiple_refs.py b/tests/basics/weakref_multiple_refs.py new file mode 100644 index 0000000000000..373cd6bb691f1 --- /dev/null +++ b/tests/basics/weakref_multiple_refs.py @@ -0,0 +1,34 @@ +# Test weakref when multiple weak references are active. +# +# This test has different output to CPython due to the order that MicroPython +# executes weak reference callbacks. + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# gc module must be available if weakref is. +import gc + + +class A: + def __str__(self): + return "" + + +def test(): + print("test having multiple ref and finalize objects referencing the same thing") + a = A() + r1 = weakref.ref(a, lambda r: print("ref1", r())) + f1 = weakref.finalize(a, lambda: print("finalize1")) + r2 = weakref.ref(a, lambda r: print("ref2", r())) + f2 = weakref.finalize(a, lambda: print("finalize2")) + print(r1(), f1.alive, r2(), f2.alive) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + + +test() diff --git a/tests/basics/weakref_multiple_refs.py.exp b/tests/basics/weakref_multiple_refs.py.exp new file mode 100644 index 0000000000000..1f2d366f776fa --- /dev/null +++ b/tests/basics/weakref_multiple_refs.py.exp @@ -0,0 +1,6 @@ +test having multiple ref and finalize objects referencing the same thing + True True +finalize2 +finalize1 +ref2 None +ref1 None diff --git a/tests/basics/weakref_ref_basic.py b/tests/basics/weakref_ref_basic.py new file mode 100644 index 0000000000000..058045f6c33b3 --- /dev/null +++ b/tests/basics/weakref_ref_basic.py @@ -0,0 +1,14 @@ +# Test weakref.ref() functionality that doesn't require gc.collect(). + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# Cannot reference non-heap objects. +for value in (None, False, True, Ellipsis, 0, "", ()): + try: + weakref.ref(value) + except TypeError: + print(value, "TypeError") diff --git a/tests/basics/weakref_ref_collect.py b/tests/basics/weakref_ref_collect.py new file mode 100644 index 0000000000000..8b2d86fb3c79c --- /dev/null +++ b/tests/basics/weakref_ref_collect.py @@ -0,0 +1,68 @@ +# Test weakref.ref() functionality requiring gc.collect(). + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# gc module must be available if weakref is. +import gc + +# Cannot reference non-heap objects. +for value in (None, False, True, Ellipsis, 0, "", ()): + try: + weakref.ref(value) + except TypeError: + print(value, "TypeError") + + +class A: + def __str__(self): + return "" + + +def callback(r): + print("callback", r()) + + +def test(): + print("test basic use of ref() with only one argument") + a = A() + r = weakref.ref(a) + print(r()) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print(r()) + + print("test use of ref() with a callback") + a = A() + r = weakref.ref(a, callback) + print(r()) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print(r()) + + print("test when weakref gets collected before the object it refs") + a = A() + r = weakref.ref(a, callback) + print(r()) + r = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + a = None + + print("test a double reference") + a = A() + r1 = weakref.ref(a, callback) + r2 = weakref.ref(a, callback) + print(r1(), r2()) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print(r1(), r2()) + + +test() diff --git a/tests/run-tests.py b/tests/run-tests.py index 59a292ed4b83a..cb4c9ab0d7c15 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -158,6 +158,8 @@ "webassembly": ( "basics/string_format_modulo.py", # can't print nulls to stdout "basics/string_strip.py", # can't print nulls to stdout + "basics/weakref_ref_collect.py", # requires custom test due to GC behaviour + "basics/weakref_finalize_collect.py", # requires custom test due to GC behaviour "extmod/asyncio_basic2.py", "extmod/asyncio_cancel_self.py", "extmod/asyncio_current_task.py", From 2cca3481f2d3d9c1bae29417d274f6326dfe5746 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 16 Feb 2026 15:16:57 +1100 Subject: [PATCH 42/53] tests/basics: Add test for weakref having exception in callback. Needs a native exp file because native code doesn't print line numbers in the traceback. Signed-off-by: Damien George --- tests/basics/weakref_callback_exception.py | 42 +++++++++++++++++++ .../basics/weakref_callback_exception.py.exp | 12 ++++++ .../weakref_callback_exception.py.native.exp | 8 ++++ tests/run-tests.py | 2 + 4 files changed, 64 insertions(+) create mode 100644 tests/basics/weakref_callback_exception.py create mode 100644 tests/basics/weakref_callback_exception.py.exp create mode 100644 tests/basics/weakref_callback_exception.py.native.exp diff --git a/tests/basics/weakref_callback_exception.py b/tests/basics/weakref_callback_exception.py new file mode 100644 index 0000000000000..df8e5129803f6 --- /dev/null +++ b/tests/basics/weakref_callback_exception.py @@ -0,0 +1,42 @@ +# Test weakref ref/finalize raising an exception within the callback. +# +# This test has different output to CPython due to the way that MicroPython +# prints the exception. + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +import gc + + +class A: + def __str__(self): + return "" + + +def callback(*args): + raise ValueError("weakref callback", args) + + +def test(): + print("test ref with exception in the callback") + a = A() + r = weakref.ref(a, callback) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("collect done") + + print("test finalize with exception in the callback") + a = A() + weakref.finalize(a, callback) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("collect done") + + +test() diff --git a/tests/basics/weakref_callback_exception.py.exp b/tests/basics/weakref_callback_exception.py.exp new file mode 100644 index 0000000000000..c2f7796310c2b --- /dev/null +++ b/tests/basics/weakref_callback_exception.py.exp @@ -0,0 +1,12 @@ +test ref with exception in the callback +Unhandled exception in weakref callback: +Traceback (most recent call last): + File "\.\+weakref_callback_exception.py", line 21, in callback +ValueError: ('weakref callback', (,)) +collect done +test finalize with exception in the callback +Unhandled exception in weakref callback: +Traceback (most recent call last): + File "\.\+weakref_callback_exception.py", line 21, in callback +ValueError: ('weakref callback', ()) +collect done diff --git a/tests/basics/weakref_callback_exception.py.native.exp b/tests/basics/weakref_callback_exception.py.native.exp new file mode 100644 index 0000000000000..a06a35c3b7e94 --- /dev/null +++ b/tests/basics/weakref_callback_exception.py.native.exp @@ -0,0 +1,8 @@ +test ref with exception in the callback +Unhandled exception in weakref callback: +ValueError: ('weakref callback', (,)) +collect done +test finalize with exception in the callback +Unhandled exception in weakref callback: +ValueError: ('weakref callback', ()) +collect done diff --git a/tests/run-tests.py b/tests/run-tests.py index cb4c9ab0d7c15..153424f65f72c 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -158,6 +158,7 @@ "webassembly": ( "basics/string_format_modulo.py", # can't print nulls to stdout "basics/string_strip.py", # can't print nulls to stdout + "basics/weakref_callback_exception.py", # has different exception printing output "basics/weakref_ref_collect.py", # requires custom test due to GC behaviour "basics/weakref_finalize_collect.py", # requires custom test due to GC behaviour "extmod/asyncio_basic2.py", @@ -435,6 +436,7 @@ def detect_target_wiring_script(pyb, args): "micropython/meminfo.py", "basics/bytes_compare3.py", "basics/builtin_help.py", + "basics/weakref_callback_exception.py", "misc/sys_settrace_cov.py", "net_inet/tls_text_errors.py", "thread/thread_exc2.py", From f83f363bb8f59a758334e37648bb09b8cae707d3 Mon Sep 17 00:00:00 2001 From: Damien George Date: Sun, 15 Feb 2026 23:06:47 +1100 Subject: [PATCH 43/53] webassembly/Makefile: Add test//% target. Following a69425b533932bbcac0ef463f9e27f79ff2150e3, this is a convenient way to run a subset of tests. Signed-off-by: Damien George --- ports/webassembly/Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ports/webassembly/Makefile b/ports/webassembly/Makefile index 9a673b757b29c..3dbe24188786b 100644 --- a/ports/webassembly/Makefile +++ b/ports/webassembly/Makefile @@ -130,7 +130,7 @@ OBJ += $(addprefix $(BUILD)/, $(SRC_C:.c=.o)) ################################################################################ # Main targets. -.PHONY: all repl min test test_min +.PHONY: all repl min test test//% test_min all: $(BUILD)/micropython.mjs @@ -150,6 +150,9 @@ min: $(BUILD)/micropython.min.mjs test: $(BUILD)/micropython.mjs $(TOP)/tests/run-tests.py cd $(TOP)/tests && MICROPY_MICROPYTHON_MJS=../ports/webassembly/$< ./run-tests.py -t webassembly +test//%: $(BUILD)/micropython.mjs $(TOP)/tests/run-tests.py + cd $(TOP)/tests && MICROPY_MICROPYTHON_MJS=../ports/webassembly/$< ./run-tests.py -t webassembly -i "$*" + test_min: $(BUILD)/micropython.min.mjs $(TOP)/tests/run-tests.py cd $(TOP)/tests && MICROPY_MICROPYTHON_MJS=../ports/webassembly/$< ./run-tests.py -t webassembly From 6f96d260e655ea87107a916ec8b48d67b5d55195 Mon Sep 17 00:00:00 2001 From: Damien George Date: Sun, 15 Feb 2026 23:07:39 +1100 Subject: [PATCH 44/53] webassembly/variants/pyscript: Enable weakref module and add tests. The webassembly port needs some additional weakref tests due to the fact that garbage collection only happens when Python execution finishes and JavaScript resumes. The `tests/ports/webassembly/heap_expand.py` expected output also needs to be updated because the amount of GC heap got smaller (weakref WTB takes some of the available RAM). Signed-off-by: Damien George --- .../variants/pyscript/mpconfigvariant.h | 1 + tests/basics/weakref_finalize_collect.py | 1 + tests/basics/weakref_multiple_refs.py | 2 + tests/basics/weakref_ref_collect.py | 1 + tests/ports/webassembly/heap_expand.mjs.exp | 46 +++++----- .../webassembly/weakref_finalize_collect.mjs | 86 +++++++++++++++++++ .../weakref_finalize_collect.mjs.exp | 28 ++++++ .../ports/webassembly/weakref_ref_collect.mjs | 69 +++++++++++++++ .../webassembly/weakref_ref_collect.mjs.exp | 16 ++++ 9 files changed, 227 insertions(+), 23 deletions(-) create mode 100644 tests/ports/webassembly/weakref_finalize_collect.mjs create mode 100644 tests/ports/webassembly/weakref_finalize_collect.mjs.exp create mode 100644 tests/ports/webassembly/weakref_ref_collect.mjs create mode 100644 tests/ports/webassembly/weakref_ref_collect.mjs.exp diff --git a/ports/webassembly/variants/pyscript/mpconfigvariant.h b/ports/webassembly/variants/pyscript/mpconfigvariant.h index ed8e812803533..0b77efc4b32bf 100644 --- a/ports/webassembly/variants/pyscript/mpconfigvariant.h +++ b/ports/webassembly/variants/pyscript/mpconfigvariant.h @@ -1,3 +1,4 @@ #define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_FULL_FEATURES) #define MICROPY_GC_SPLIT_HEAP (1) #define MICROPY_GC_SPLIT_HEAP_AUTO (1) +#define MICROPY_PY_WEAKREF (1) diff --git a/tests/basics/weakref_finalize_collect.py b/tests/basics/weakref_finalize_collect.py index d364be9f62d67..f6e7c14843e01 100644 --- a/tests/basics/weakref_finalize_collect.py +++ b/tests/basics/weakref_finalize_collect.py @@ -1,4 +1,5 @@ # Test weakref.finalize() functionality requiring gc.collect(). +# Should be kept in sync with tests/ports/webassembly/weakref_finalize_collect.py. try: import weakref diff --git a/tests/basics/weakref_multiple_refs.py b/tests/basics/weakref_multiple_refs.py index 373cd6bb691f1..400e03a17c7f8 100644 --- a/tests/basics/weakref_multiple_refs.py +++ b/tests/basics/weakref_multiple_refs.py @@ -19,6 +19,8 @@ def __str__(self): def test(): + global r1, r2 # needed for webassembly port to retain references to them + print("test having multiple ref and finalize objects referencing the same thing") a = A() r1 = weakref.ref(a, lambda r: print("ref1", r())) diff --git a/tests/basics/weakref_ref_collect.py b/tests/basics/weakref_ref_collect.py index 8b2d86fb3c79c..0e8db977d7764 100644 --- a/tests/basics/weakref_ref_collect.py +++ b/tests/basics/weakref_ref_collect.py @@ -1,4 +1,5 @@ # Test weakref.ref() functionality requiring gc.collect(). +# Should be kept in sync with tests/ports/webassembly/weakref_ref_collect.py. try: import weakref diff --git a/tests/ports/webassembly/heap_expand.mjs.exp b/tests/ports/webassembly/heap_expand.mjs.exp index 67ebe98e7fe02..4161fc7eaed83 100644 --- a/tests/ports/webassembly/heap_expand.mjs.exp +++ b/tests/ports/webassembly/heap_expand.mjs.exp @@ -1,27 +1,27 @@ -135241312 -135241280 -135241248 -135241216 -135241168 -135241120 -135241040 -135240896 -135240592 -135240064 -135239024 -135236960 +135233568 +135233536 +135233504 +135233472 +135233424 +135233376 +135233296 +135233152 135232848 -135224640 -135208240 -135175456 -135109840 -134978752 -134716592 -135216800 -136217168 -138217984 -142219568 -150222816 +135232320 +135231280 +135229216 +135225104 +135216896 +135200496 +135167712 +135102096 +134971008 +134708848 +135201312 +136186256 +138156160 +142095984 +149975648 1 2 4 diff --git a/tests/ports/webassembly/weakref_finalize_collect.mjs b/tests/ports/webassembly/weakref_finalize_collect.mjs new file mode 100644 index 0000000000000..1e0bc951350db --- /dev/null +++ b/tests/ports/webassembly/weakref_finalize_collect.mjs @@ -0,0 +1,86 @@ +// Test weakref.finalize() functionality requiring gc.collect(). +// Should be kept in sync with tests/basics/weakref_finalize_collect.py. +// +// This needs custom testing on the webassembly port since the GC can only +// run when Python code returns to JavaScript. + +const mp = await (await import(process.argv[2])).loadMicroPython(); + +// Set up. +mp.runPython(` +import gc, weakref + +class A: + def __str__(self): + return "" + +def callback(*args, **kwargs): + print("callback({}, {})".format(args, kwargs)) + return 42 +`); + +console.log("test basic use of finalize() with a simple callback"); +mp.runPython(` + a = A() + f = weakref.finalize(a, callback) + a = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print("alive", f.alive) + print("peek", f.peek()) + print("detach", f.detach()) + print("call", f()) +`); + +console.log("test that a callback is passed the correct values"); +mp.runPython(` + a = A() + f = weakref.finalize(a, callback, 1, 2, kwarg=3) + a = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print("alive", f.alive) + print("peek", f.peek()) + print("detach", f.detach()) + print("call", f()) +`); + +console.log("test that calling the finalizer cancels the finalizer"); +mp.runPython(` + a = A() + f = weakref.finalize(a, callback) + print(f()) + print(a) + a = None + gc.collect() +`); +console.log("(outside Python)"); + +console.log("test that calling detach cancels the finalizer"); +mp.runPython(` + a = A() + f = weakref.finalize(a, callback) + print(len(f.detach())) + print(a) + a = None + gc.collect() +`); +console.log("(outside Python)"); + +console.log("test that finalize does not get collected before its ref does"); +mp.runPython(` + a = A() + weakref.finalize(a, callback) + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print("free a") + a = None + gc.collect() +`); +console.log("(outside Python)"); diff --git a/tests/ports/webassembly/weakref_finalize_collect.mjs.exp b/tests/ports/webassembly/weakref_finalize_collect.mjs.exp new file mode 100644 index 0000000000000..e8087a4ae9bd9 --- /dev/null +++ b/tests/ports/webassembly/weakref_finalize_collect.mjs.exp @@ -0,0 +1,28 @@ +test basic use of finalize() with a simple callback +callback((), {}) +(outside Python) +alive False +peek None +detach None +call None +test that a callback is passed the correct values +callback((1, 2), {'kwarg': 3}) +(outside Python) +alive False +peek None +detach None +call None +test that calling the finalizer cancels the finalizer +callback((), {}) +42 + +(outside Python) +test that calling detach cancels the finalizer +4 + +(outside Python) +test that finalize does not get collected before its ref does +(outside Python) +free a +callback((), {}) +(outside Python) diff --git a/tests/ports/webassembly/weakref_ref_collect.mjs b/tests/ports/webassembly/weakref_ref_collect.mjs new file mode 100644 index 0000000000000..546a851f0ac09 --- /dev/null +++ b/tests/ports/webassembly/weakref_ref_collect.mjs @@ -0,0 +1,69 @@ +// Test weakref.ref() functionality requiring gc.collect(). +// Should be kept in sync with tests/basics/weakref_ref_collect.py. +// +// This needs custom testing on the webassembly port since the GC can only +// run when Python code returns to JavaScript. + +const mp = await (await import(process.argv[2])).loadMicroPython(); + +// Set up. +mp.runPython(` +import gc, weakref + +class A: + def __str__(self): + return "" + +def callback(r): + print("callback", r()) +`); + +console.log("test basic use of ref() with only one argument"); +mp.runPython(` + a = A() + r = weakref.ref(a) + print(r()) + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print(r()) + a = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print(r()) +`); + +console.log("test use of ref() with a callback"); +mp.runPython(` + a = A() + r = weakref.ref(a, callback) + print(r()) + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print(r()) + a = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print(r()) +`); + +console.log("test when weakref gets collected before the object it refs"); +mp.runPython(` + a = A() + r = weakref.ref(a, callback) + print(r()) + r = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + a = None + gc.collect() +`); diff --git a/tests/ports/webassembly/weakref_ref_collect.mjs.exp b/tests/ports/webassembly/weakref_ref_collect.mjs.exp new file mode 100644 index 0000000000000..f903d41702815 --- /dev/null +++ b/tests/ports/webassembly/weakref_ref_collect.mjs.exp @@ -0,0 +1,16 @@ +test basic use of ref() with only one argument + +(outside Python) + +(outside Python) +None +test use of ref() with a callback + +(outside Python) + +callback None +(outside Python) +None +test when weakref gets collected before the object it refs + +(outside Python) From 44d8f70a8ed729f677c264fe16a824cce61d9acd Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 16 Feb 2026 13:22:09 +1100 Subject: [PATCH 45/53] docs/library/weakref: Add documentation for weakref module. Signed-off-by: Damien George --- docs/library/index.rst | 1 + docs/library/weakref.rst | 78 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 docs/library/weakref.rst diff --git a/docs/library/index.rst b/docs/library/index.rst index 0459814527692..fca9cbb53713f 100644 --- a/docs/library/index.rst +++ b/docs/library/index.rst @@ -82,6 +82,7 @@ library. struct.rst sys.rst time.rst + weakref.rst zlib.rst _thread.rst diff --git a/docs/library/weakref.rst b/docs/library/weakref.rst new file mode 100644 index 0000000000000..ffebc91277beb --- /dev/null +++ b/docs/library/weakref.rst @@ -0,0 +1,78 @@ +:mod:`weakref` -- Python object lifetime management +=================================================== + +.. module:: weakref + :synopsis: Create weak references to Python objects + +|see_cpython_module| :mod:`python:weakref`. + +This module allows creation of weak references to Python objects. A weak reference +is a non-traceable reference to a heap-allocated Python object, so the garbage +collector can still reclaim the object even though the weak reference refers to it. + +Python callbacks can be registered to be called when an object is reclaimed by the +garbage collector. This provides a safe way to clean up when objects are no longer +needed. + +**Availability:** the weakref module requires ``MICROPY_PY_WEAKREF`` to be enabled +at compile time. It is enabled on the unix coverage variant and the webassembly +pyscript variant. + +ref objects +----------- + +A ref object is the simplest way to make a weak reference. + +.. class:: ref(object [, callback], /) + + Return a weak reference to the given *object*. + + If *callback* is given and is not ``None`` then, when *object* is reclaimed + by the garbage collector and if the weak reference object is still alive, the + *callback* will be called. The *callback* will be passed the weak reference + object as its single argument. + +.. method:: ref.__call__() + + Calling the weak reference object will return its referenced object if that + object is still alive. Otherwise ``None`` will be returned. + +finalize objects +---------------- + +A finalize object is an extended version of a ref object that is more convenient to +use, and allows more control over the callback. + +.. class:: finalize(object, callback, /, *args, **kwargs) + + Return a weak reference to the given *object*. In contrast to *weakref.ref* + objects, finalize objects are held onto internally and will not be collected until + *object* is collected. + + A finalize object starts off alive. It transitions to the dead state when the + finalize object is called, either explicitly or when *object* is collected. It also + transitions to dead if the `finalize.detach()` method is called. + + When *object* is reclaimed by the garbage collector (or the finalize object is + explicitly called by user code) and the finalize object is still in the alive state, + the *callback* will be called. The *callback* will be passed arguments as: + ``callback(*args, **kwargs)``. + +.. method:: finalize.__call__() + + If the finalize object is alive then it transitions to the dead state and returns + the value of ``callback(*args, **kwargs)``. Otherwise ``None`` will be returned. + +.. method:: finalize.alive + + Read-only boolean attribute that indicates if the finalizer is in the alive state. + +.. method:: finalize.peek() + + If the finalize object is alive then return ``(object, callback, args, kwargs)``. + Otherwise return ``None``. + +.. method:: finalize.detach() + + If the finalize object is alive then it transitions to the dead state and returns + ``(object, callback, args, kwargs)``. Otherwise ``None`` will be returned. From 5c00edcee28491b6961a5db6aa0e5aa856664de4 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 16 Feb 2026 18:46:31 +1100 Subject: [PATCH 46/53] tools/ci.sh: Increase qemu_arm test run timeout. It takes longer now that weakref is enabled in the coverage build. Signed-off-by: Damien George --- tools/ci.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/ci.sh b/tools/ci.sh index 0532c1c7570f8..056f6a6102d96 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -938,9 +938,9 @@ function ci_unix_qemu_arm_build { function ci_unix_qemu_arm_run_tests { # Issues with ARM tests: - # - thread/stress_aes.py takes around 70 seconds + # - thread/stress_aes.py takes around 90 seconds file ./ports/unix/build-coverage/micropython - (cd tests && MICROPY_MICROPYTHON=../ports/unix/build-coverage/micropython MICROPY_TEST_TIMEOUT=90 ./run-tests.py) + (cd tests && MICROPY_MICROPYTHON=../ports/unix/build-coverage/micropython MICROPY_TEST_TIMEOUT=120 ./run-tests.py) } function ci_unix_qemu_riscv64_setup { From 85e8f6189e846d76aded0317089f1736ca1c9d5f Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 24 Mar 2026 23:38:44 +1100 Subject: [PATCH 47/53] lib/micropython-lib: Update submodule to latest. This brings in: - sdcard: Send stop bit after multi-block read/write - sdcard: Compute CRC7 for all SPI commands - sdcard: Add read/write speed test to sdtest - lsm6dsox: Add pedometer support - lsm6dsox: Add pedometer example code - unix-ffi/re: Handle PCRE2_UNSET in group and groups methods - unix-ffi/re: Add tests for empty string match in ffi regex - unix-ffi/machine: Retrieve a unique identifier if one is known - senml/docs: Correct capitalization of 'MicroPython' - unix-ffi/_libc: Extend FreeBSD libc versions range - string: Convert string module to package and import templatelib Signed-off-by: Damien George --- lib/micropython-lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/micropython-lib b/lib/micropython-lib index 6ae440a8a1442..8380c7bb8f9e5 160000 --- a/lib/micropython-lib +++ b/lib/micropython-lib @@ -1 +1 @@ -Subproject commit 6ae440a8a144233e6e703f6759b7e7a0afaa37a4 +Subproject commit 8380c7bb8f9e5e5260e9539156742925e00366b2 From bce854928d49cee88a722630e2df1a5d2517cd2c Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 24 Mar 2026 23:40:05 +1100 Subject: [PATCH 48/53] tests/feature_check/tstring.py: Remove check for string.templatelib. If a port enables t-strings then it is required to have the `string.templatelib` package (at least to run the tests). That's automatically the case if `MICROPY_PY_TSTRINGS` is enabled. If a port freezes in the micropython-lib `string` extension package then the latest version of this package will include the built-in `string.templatelib` classes. So the feature check for t-strings no longer needs to check if they are available. Signed-off-by: Damien George --- tests/feature_check/tstring.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/feature_check/tstring.py b/tests/feature_check/tstring.py index e1d428b4e7b2b..05322b2ae612d 100644 --- a/tests/feature_check/tstring.py +++ b/tests/feature_check/tstring.py @@ -1,8 +1,5 @@ # check whether t-strings (PEP-750) are supported -# TODO remove this check when micropython-lib's string extends ustring -from string.templatelib import Template, Interpolation - a = 1 t = t"a={a}" print("tstring") From 93201ff2d12211ca016a4510d737a5c544a3599a Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 24 Mar 2026 23:59:17 +1100 Subject: [PATCH 49/53] lib/cyw43-driver: Update driver to latest version v1.1.1. Includes a fix to STA teardown to deinit tcpip and clear itf_state. Signed-off-by: Damien George --- lib/cyw43-driver | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cyw43-driver b/lib/cyw43-driver index dd7568229f3bf..055d64274b014 160000 --- a/lib/cyw43-driver +++ b/lib/cyw43-driver @@ -1 +1 @@ -Subproject commit dd7568229f3bf7a37737b9e1ef250c26efe75b23 +Subproject commit 055d64274b014dd7b1c2fc94d26e8a18face7124 From ac48088749853d52124f7175eb9f91e979a17242 Mon Sep 17 00:00:00 2001 From: Matt Trentini Date: Wed, 1 May 2024 14:58:37 +1000 Subject: [PATCH 50/53] rp2/boards/SEEED_XIAO_RP2040: Add XIAO RP2040 board definition. Signed-off-by: Matt Trentini --- ports/rp2/boards/SEEED_XIAO_RP2040/board.json | 20 ++++++++++++++++++ .../SEEED_XIAO_RP2040/mpconfigboard.cmake | 3 +++ .../boards/SEEED_XIAO_RP2040/mpconfigboard.h | 19 +++++++++++++++++ ports/rp2/boards/SEEED_XIAO_RP2040/pins.csv | 21 +++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 ports/rp2/boards/SEEED_XIAO_RP2040/board.json create mode 100644 ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.cmake create mode 100644 ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.h create mode 100644 ports/rp2/boards/SEEED_XIAO_RP2040/pins.csv diff --git a/ports/rp2/boards/SEEED_XIAO_RP2040/board.json b/ports/rp2/boards/SEEED_XIAO_RP2040/board.json new file mode 100644 index 0000000000000..ae7e31be1d5cc --- /dev/null +++ b/ports/rp2/boards/SEEED_XIAO_RP2040/board.json @@ -0,0 +1,20 @@ +{ + "deploy": [ + "../deploy.md" + ], + "docs": "", + "features": [ + "Dual-core", + "External Flash", + "RGB LED", + "USB", + "USB-C" + ], + "images": [ + "seeedstudio_xiao_rp2040.jpg" + ], + "mcu": "rp2040", + "product": "XIAO RP2040", + "url": "https://www.seeedstudio.com/XIAO-RP2040-v1-0-p-5026.html", + "vendor": "Seeed Studio" +} diff --git a/ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.cmake b/ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.cmake new file mode 100644 index 0000000000000..a069458867d2e --- /dev/null +++ b/ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.cmake @@ -0,0 +1,3 @@ +# cmake file for Seeed Studio XIAO RP204 + +set(PICO_BOARD "seeed_xiao_rp2040") diff --git a/ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.h b/ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.h new file mode 100644 index 0000000000000..4291f8c00c3f6 --- /dev/null +++ b/ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.h @@ -0,0 +1,19 @@ +// https://wiki.seeedstudio.com/XIAO-RP2040/ + +#define MICROPY_HW_BOARD_NAME "Seeed Studio XIAO RP2040" +#define MICROPY_HW_FLASH_STORAGE_BYTES (1408 * 1024) + +// No VID/PID defined for the Seeed XIAO RP2040 +// #define MICROPY_HW_USB_VID (0x) +// #define MICROPY_HW_USB_PID (0x) + +// I2C0 +#define MICROPY_HW_I2C0_SCL (7) +#define MICROPY_HW_I2C0_SDA (6) + +// SPI0 +#define MICROPY_HW_SPI0_SCK (2) +#define MICROPY_HW_SPI0_MOSI (3) +#define MICROPY_HW_SPI0_MISO (4) + +// UART0 is, by default, assigned the correct pins (TX=0, RX=1) diff --git a/ports/rp2/boards/SEEED_XIAO_RP2040/pins.csv b/ports/rp2/boards/SEEED_XIAO_RP2040/pins.csv new file mode 100644 index 0000000000000..7ae0ca50bb050 --- /dev/null +++ b/ports/rp2/boards/SEEED_XIAO_RP2040/pins.csv @@ -0,0 +1,21 @@ +D0,GPIO26 +D1,GPIO27 +D2,GPIO28 +D3,GPIO29 +D4,GPIO6 +D5,GPIO7 +D6,GPIO0 +D7,GPIO1 +D8,GPIO2 +D9,GPIO4 +D10,GPIO3 +A0,GPIO26 +A1,GPIO27 +A2,GPIO28 +A3,GPIO29 +NEOPIXEL_POWER,GPIO11 +NEOPIXEL,GPIO12 +LED_R,GPIO17 +LED_G,GPIO16 +LED_B,GPIO25 +LED,GPIO25 From f4d2447174e2bd75551db6bdf58e08046d88a2b5 Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Mon, 16 Mar 2026 15:16:15 -0600 Subject: [PATCH 51/53] esp32/boards/SPARKFUN_THINGPLUS_ESP32C5: Add SF Thing Plus ESP32-C5. Signed-off-by: Dryw Wade --- .../SPARKFUN_THINGPLUS_ESP32C5/board.json | 29 +++++++++++++++++++ .../SPARKFUN_THINGPLUS_ESP32C5/manifest.py | 2 ++ .../mpconfigboard.cmake | 13 +++++++++ .../mpconfigboard.h | 11 +++++++ .../SPARKFUN_THINGPLUS_ESP32C5/pins.csv | 18 ++++++++++++ .../sdkconfig.board | 2 ++ 6 files changed, 75 insertions(+) create mode 100644 ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/board.json create mode 100644 ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/manifest.py create mode 100644 ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.cmake create mode 100644 ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.h create mode 100644 ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/pins.csv create mode 100644 ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/sdkconfig.board diff --git a/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/board.json b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/board.json new file mode 100644 index 0000000000000..2be28659b9e0b --- /dev/null +++ b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/board.json @@ -0,0 +1,29 @@ +{ + "deploy": [ + "../deploy.md" + ], + "deploy_options": { + "flash_offset": "0x2000" + }, + "docs": "", + "features": [ + "BLE", + "Battery Charging", + "External Flash", + "External RAM", + "Feather", + "JST-SH", + "RGB LED", + "USB-C", + "WiFi", + "microSD" + ], + "images": [ + "30678-Thing-Plus-ESP32-C5-Feature.jpg" + ], + "mcu": "esp32c5", + "product": "Thing Plus ESP32-C5", + "thumbnail": "", + "url": "https://www.sparkfun.com/sparkfun-thing-plus-esp32-c5.html", + "vendor": "SparkFun" +} diff --git a/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/manifest.py b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/manifest.py new file mode 100644 index 0000000000000..73446ecac9892 --- /dev/null +++ b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/manifest.py @@ -0,0 +1,2 @@ +include("$(PORT_DIR)/boards/manifest.py") +require("sdcard") diff --git a/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.cmake b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.cmake new file mode 100644 index 0000000000000..216cd664dbe70 --- /dev/null +++ b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.cmake @@ -0,0 +1,13 @@ +set(IDF_TARGET esp32c5) + +set(SDKCONFIG_DEFAULTS + boards/sdkconfig.base + boards/sdkconfig.riscv + boards/sdkconfig.ble + boards/sdkconfig.240mhz + boards/sdkconfig.free_ram + boards/sdkconfig.spiram + boards/SPARKFUN_THINGPLUS_ESP32C5/sdkconfig.board +) + +set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py) diff --git a/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.h b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.h new file mode 100644 index 0000000000000..7c892399da94f --- /dev/null +++ b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.h @@ -0,0 +1,11 @@ +// Board specific definitions for the SparkFun Thing Plus ESP32-C5. + +#define MICROPY_HW_BOARD_NAME "SparkFun Thing Plus ESP32-C5" +#define MICROPY_HW_MCU_NAME "ESP32C5" + +#define MICROPY_HW_I2C0_SCL (24) +#define MICROPY_HW_I2C0_SDA (23) + +#define MICROPY_HW_SPI1_SCK (10) +#define MICROPY_HW_SPI1_MOSI (8) +#define MICROPY_HW_SPI1_MISO (9) diff --git a/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/pins.csv b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/pins.csv new file mode 100644 index 0000000000000..aec57725e78c0 --- /dev/null +++ b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/pins.csv @@ -0,0 +1,18 @@ +ALERT,GPIO0 +BAT_STAT,GPIO6 +SD_DET,GPIO7 +MOSI,GPIO8 +PICO,GPIO8 +MISO,GPIO9 +POCI,GPIO9 +SCK,GPIO10 +TX,GPIO11 +RX,GPIO12 +SDA,GPIO23 +SCL,GPIO24 +CS,GPIO25 +LP,GPIO26 +LED,GPIO27 +RGB_LED,GPIO27 +NEOPIXEL,GPIO27 +BUTTON,GPIO28 diff --git a/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/sdkconfig.board b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/sdkconfig.board new file mode 100644 index 0000000000000..369330682f91a --- /dev/null +++ b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/sdkconfig.board @@ -0,0 +1,2 @@ +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_ESPTOOLPY_FLASHFREQ_80M=y From 2dc2e30d98ee225070e990586526f8e43b3c95a2 Mon Sep 17 00:00:00 2001 From: Matt Trentini Date: Sat, 21 Mar 2026 10:17:08 +1100 Subject: [PATCH 52/53] esp32/boards/SEEED_XIAO_ESP32C6: Add new XIAO board definition. Signed-off-by: Matt Trentini Signed-off-by: Matt Trentini --- .../boards/SEEED_XIAO_ESP32C6/board.json | 24 +++++++++++++++++++ .../SEEED_XIAO_ESP32C6/mpconfigboard.cmake | 8 +++++++ .../boards/SEEED_XIAO_ESP32C6/mpconfigboard.h | 9 +++++++ .../esp32/boards/SEEED_XIAO_ESP32C6/pins.csv | 22 +++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 ports/esp32/boards/SEEED_XIAO_ESP32C6/board.json create mode 100644 ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.cmake create mode 100644 ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.h create mode 100644 ports/esp32/boards/SEEED_XIAO_ESP32C6/pins.csv diff --git a/ports/esp32/boards/SEEED_XIAO_ESP32C6/board.json b/ports/esp32/boards/SEEED_XIAO_ESP32C6/board.json new file mode 100644 index 0000000000000..81e15d4a3e6cf --- /dev/null +++ b/ports/esp32/boards/SEEED_XIAO_ESP32C6/board.json @@ -0,0 +1,24 @@ +{ + "deploy": [ + "../deploy_nativeusb.md" + ], + "deploy_options": { + "flash_offset": "0" + }, + "docs": "", + "features": [ + "Battery Charging", + "BLE", + "External Flash", + "WiFi", + "USB", + "USB-C" + ], + "images": [ + "seeed_xiao_esp32c6.jpg" + ], + "mcu": "esp32c6", + "product": "XIAO ESP32C6", + "url": "https://www.seeedstudio.com/Seeed-Studio-XIAO-ESP32C6-p-5884.html", + "vendor": "Seeed Studio" +} diff --git a/ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.cmake b/ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.cmake new file mode 100644 index 0000000000000..48946f7094530 --- /dev/null +++ b/ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.cmake @@ -0,0 +1,8 @@ +set(IDF_TARGET esp32c6) + +set(SDKCONFIG_DEFAULTS + boards/sdkconfig.base + boards/sdkconfig.riscv + boards/sdkconfig.c6 + boards/sdkconfig.ble +) diff --git a/ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.h b/ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.h new file mode 100644 index 0000000000000..a85f13899868c --- /dev/null +++ b/ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.h @@ -0,0 +1,9 @@ +#define MICROPY_HW_BOARD_NAME "Seeed XIAO ESP32C6" +#define MICROPY_HW_MCU_NAME "ESP32C6" + +#define MICROPY_HW_I2C0_SCL (23) +#define MICROPY_HW_I2C0_SDA (22) + +#define MICROPY_HW_SPI1_MOSI (18) +#define MICROPY_HW_SPI1_MISO (20) +#define MICROPY_HW_SPI1_SCK (19) diff --git a/ports/esp32/boards/SEEED_XIAO_ESP32C6/pins.csv b/ports/esp32/boards/SEEED_XIAO_ESP32C6/pins.csv new file mode 100644 index 0000000000000..aca4bd9948919 --- /dev/null +++ b/ports/esp32/boards/SEEED_XIAO_ESP32C6/pins.csv @@ -0,0 +1,22 @@ +D0,GPIO0 +D1,GPIO1 +D2,GPIO2 +D3,GPIO21 +D4,GPIO22 +D5,GPIO23 +D6,GPIO16 +D7,GPIO17 +D8,GPIO19 +D9,GPIO20 +D10,GPIO18 +A0,GPIO0 +A1,GPIO1 +A2,GPIO2 +LED,GPIO15 +MTDO,GPIO7 +MTDI,GPIO5 +MTCK,GPIO6 +MTMS,GPIO4 +BOOT,GPIO9 +RF_SEL,GPIO14 +RF_POWER,GPIO3 From 47d672590f47ee691f0db13a7208222ce6c24ede Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Sat, 28 Feb 2026 23:11:36 +1100 Subject: [PATCH 53/53] mpremote: Add smart encoding selection for fs_writefile. Automatically detect device capabilities (deflate, base64, bytes.fromhex) and select the best encoding for file transfers. Deflate+base64 is used when the device supports it and data compresses well, base64 alone as a fallback, and repr as the universal fallback. Each capability is probed independently so a missing deflate module does not suppress base64 detection. Signed-off-by: Andrew Leech --- tools/mpremote/mpremote/commands.py | 34 ++--- tools/mpremote/mpremote/compression_utils.py | 85 ++++++++++++ tools/mpremote/mpremote/transport.py | 128 ++++++++++++++++++- 3 files changed, 226 insertions(+), 21 deletions(-) create mode 100644 tools/mpremote/mpremote/compression_utils.py diff --git a/tools/mpremote/mpremote/commands.py b/tools/mpremote/mpremote/commands.py index 4974c71e2e9c3..3f57f67347516 100644 --- a/tools/mpremote/mpremote/commands.py +++ b/tools/mpremote/mpremote/commands.py @@ -4,10 +4,10 @@ import os import sys import tempfile -import zlib import serial.tools.list_ports +from .compression_utils import compress_chunk, DEFLATE_WBITS from .transport import TransportError, TransportExecError, stdout_write_bytes from .transport_serial import SerialTransport from .romfs import make_romfs, VfsRomWriter @@ -682,18 +682,20 @@ def _do_romfs_deploy(state, args): chunk_size = max(chunk_size, rom_min_write) # Detect capabilities of the device to use the fastest method of transfer. - has_bytes_fromhex = transport.eval("hasattr(bytes,'fromhex')") - try: + caps = transport.detect_encoding_capabilities() + has_deflate_io = caps.get("deflate", False) + has_a2b_base64 = caps.get("base64", False) + has_bytes_fromhex = caps.get("fromhex", False) + + # Import encoding modules on device (detection uses hasattr, not exec). + if has_deflate_io: + transport.exec( + "from binascii import a2b_base64\n" + "from io import BytesIO\n" + "from deflate import DeflateIO,RAW" + ) + elif has_a2b_base64: transport.exec("from binascii import a2b_base64") - has_a2b_base64 = True - except TransportExecError: - has_a2b_base64 = False - try: - transport.exec("from io import BytesIO") - transport.exec("from deflate import DeflateIO,RAW") - has_deflate_io = True - except TransportExecError: - has_deflate_io = False # Deploy the ROMFS filesystem image to the device. for offset in range(0, len(romfs), chunk_size): @@ -701,14 +703,12 @@ def _do_romfs_deploy(state, args): romfs_chunk += bytes(chunk_size - len(romfs_chunk)) if has_deflate_io: # Needs: binascii.a2b_base64, io.BytesIO, deflate.DeflateIO. - compressor = zlib.compressobj(wbits=-9) - romfs_chunk_compressed = compressor.compress(romfs_chunk) - romfs_chunk_compressed += compressor.flush() + romfs_chunk_compressed = compress_chunk(romfs_chunk) buf = binascii.b2a_base64(romfs_chunk_compressed).strip() - transport.exec(f"buf=DeflateIO(BytesIO(a2b_base64({buf})),RAW,9).read()") + transport.exec(f"buf=DeflateIO(BytesIO(a2b_base64({buf})),RAW,{DEFLATE_WBITS}).read()") elif has_a2b_base64: # Needs: binascii.a2b_base64. - buf = binascii.b2a_base64(romfs_chunk) + buf = binascii.b2a_base64(romfs_chunk).strip() transport.exec(f"buf=a2b_base64({buf})") elif has_bytes_fromhex: # Needs: bytes.fromhex. diff --git a/tools/mpremote/mpremote/compression_utils.py b/tools/mpremote/mpremote/compression_utils.py new file mode 100644 index 0000000000000..35a3a17e95c56 --- /dev/null +++ b/tools/mpremote/mpremote/compression_utils.py @@ -0,0 +1,85 @@ +# +# This file is part of the MicroPython project, http://micropython.org/ +# +# The MIT License (MIT) +# +# Copyright (c) 2024 Andrew Leech +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import zlib + +# Minimum file size to attempt compression testing (bytes). +# Below this, decompression setup overhead on device isn't worth it. +MIN_COMPRESS_SIZE = 256 + +# Compression ratio threshold: use deflate only if compressed/original < this. +# 0.80 means require at least 20% size reduction. +MIN_COMPRESS_RATIO = 0.80 + +# Chunk sizes for each encoding method. +# Larger chunks = fewer exec() round trips over serial. +DEFLATE_CHUNK_SIZE = 4096 # Compressed chunks are smaller on wire +BASE64_CHUNK_SIZE = 2048 # Still benefits from fewer round trips + +# Device-side decompression window size (512 bytes, minimal RAM). +DEFLATE_WBITS = 9 +# Host-side compression: negative = raw deflate (no zlib header). +DEFAULT_WBITS = -DEFLATE_WBITS + +# Sample size for compression ratio testing. +COMPRESS_SAMPLE_SIZE = 4096 + + +def compress_chunk(data, wbits=DEFAULT_WBITS): + """Compress a single chunk using raw deflate. + + Each chunk is independently compressed/decompressible, which is required + for per-chunk transfer where each exec() call decompresses one chunk. + + Args: + data: Bytes to compress. + wbits: Window bits for deflate. Negative = raw deflate (no header). + Default -9 = 512-byte window, minimal device RAM usage. + + Returns: + Compressed bytes in raw deflate format. + """ + compressor = zlib.compressobj(wbits=wbits) + return compressor.compress(data) + compressor.flush() + + +def test_compression_ratio(data, sample_size=COMPRESS_SAMPLE_SIZE): + """Test compression ratio on a data sample. + + Compresses a prefix of the data to estimate overall compressibility + without processing the entire file. + + Args: + data: The full data to test. + sample_size: Max bytes to sample from start of data. + + Returns: + Ratio of compressed/original size (0.0-1.0+). Lower = better compression. + """ + sample = data[:sample_size] + if not sample: + return 1.0 + compressed = compress_chunk(sample) + return len(compressed) / len(sample) diff --git a/tools/mpremote/mpremote/transport.py b/tools/mpremote/mpremote/transport.py index d7568b281b1be..446d3311a745c 100644 --- a/tools/mpremote/mpremote/transport.py +++ b/tools/mpremote/mpremote/transport.py @@ -24,8 +24,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -import ast, errno, hashlib, os, re, sys +import ast, binascii, errno, hashlib, os, re, sys from collections import namedtuple +from .compression_utils import ( + compress_chunk, + test_compression_ratio, + MIN_COMPRESS_SIZE, + MIN_COMPRESS_RATIO, + DEFLATE_CHUNK_SIZE, + DEFLATE_WBITS, + BASE64_CHUNK_SIZE, +) from .mp_errno import MP_ERRNO_TABLE @@ -151,16 +160,127 @@ def fs_readfile(self, src, chunk_size=256, progress_callback=None): return contents - def fs_writefile(self, dest, data, chunk_size=256, progress_callback=None): + def detect_encoding_capabilities(self): + """Detect available encoding methods on device. Cached after first call.""" + if hasattr(self, "_fs_encoding_caps"): + return self._fs_encoding_caps + + # Two separate eval() calls so that a missing deflate module (which + # raises ImportError inside the dict expression) doesn't prevent + # base64/bytesio from being detected. + try: + caps = self.eval( + "{" + "'bytesio':hasattr(__import__('io'),'BytesIO')," + "'base64':hasattr(__import__('binascii'),'a2b_base64')," + "'fromhex':hasattr(bytes,'fromhex')," + "}" + ) + except TransportExecError: + caps = {} + + try: + has_deflate = self.eval("hasattr(__import__('deflate'),'DeflateIO')") + except TransportExecError: + has_deflate = False + + self._fs_encoding_caps = { + "deflate": has_deflate and caps.get("bytesio") and caps.get("base64"), + "base64": caps.get("base64", False), + "fromhex": caps.get("fromhex", False), + } + + return self._fs_encoding_caps + + def _choose_encoding_for_data(self, data): + """Choose best encoding based on device capabilities and data compressibility. + + Three-tier fallback: + 1. deflate+base64 - if device has deflate/io/binascii and data compresses well + 2. base64 - if device has binascii.a2b_base64 + 3. repr - universal fallback + + Returns: (encoding, ratio) where encoding is 'deflate', 'base64', or 'repr', + and ratio is the compression ratio (float) for deflate, or None otherwise. + """ + caps = self.detect_encoding_capabilities() + + if caps.get("deflate") and len(data) > MIN_COMPRESS_SIZE: + ratio = test_compression_ratio(data) + if ratio < MIN_COMPRESS_RATIO: + return "deflate", ratio + + if caps.get("base64"): + return "base64", None + + return "repr", None + + _VALID_ENCODINGS = ("deflate", "base64", "repr") + + def fs_writefile(self, dest, data, chunk_size=None, progress_callback=None, encoding=None): + """Write data to a file on the device. + + Automatically selects the best encoding based on device capabilities and + data compressibility, with dynamic chunk sizing per encoding method. + + Args: + dest: Destination path on device + data: Bytes to write + chunk_size: Chunk size in bytes, or None for encoding-appropriate default. + progress_callback: Optional callback(written, total) + encoding: Force encoding: 'deflate', 'base64', 'repr', or None (auto-select) + """ if progress_callback: src_size = len(data) written = 0 + # Auto-select encoding based on data compressibility + if encoding is None: + encoding, _ratio = self._choose_encoding_for_data(data) + + if encoding not in self._VALID_ENCODINGS: + raise ValueError( + "encoding must be one of %s, got %r" % (self._VALID_ENCODINGS, encoding) + ) + + # Dynamic chunk sizing: larger chunks = fewer exec() round trips. + # Only auto-size when caller didn't specify. + if chunk_size is None: + if encoding == "deflate": + chunk_size = DEFLATE_CHUNK_SIZE + elif encoding == "base64": + chunk_size = BASE64_CHUNK_SIZE + else: + chunk_size = 256 + try: - self.exec("f=open('%s','wb')\nw=f.write" % dest) + # Setup imports and file handle on device + if encoding == "deflate": + self.exec( + "from binascii import a2b_base64\n" + "from io import BytesIO\n" + "from deflate import DeflateIO,RAW\n" + "f=open('%s','wb')\nw=f.write" % dest + ) + elif encoding == "base64": + self.exec("from binascii import a2b_base64\nf=open('%s','wb')\nw=f.write" % dest) + else: + self.exec("f=open('%s','wb')\nw=f.write" % dest) + while data: chunk = data[:chunk_size] - self.exec("w(" + repr(chunk) + ")") + if encoding == "deflate": + compressed = compress_chunk(chunk) + b64 = binascii.b2a_base64(compressed).strip() + self.exec( + "w(DeflateIO(BytesIO(a2b_base64(%s)),RAW,%d).read())" + % (b64, DEFLATE_WBITS) + ) + elif encoding == "base64": + b64 = binascii.b2a_base64(chunk).strip() + self.exec("w(a2b_base64(%s))" % b64) + else: + self.exec("w(" + repr(chunk) + ")") data = data[len(chunk) :] if progress_callback: written += len(chunk)