diff --git a/boards/partitions-16MiB.csv b/boards/partitions-16MiB.csv new file mode 100644 index 00000000..427e8805 --- /dev/null +++ b/boards/partitions-16MiB.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags + +nvs,data,nvs,0x9000,0x6000 +phy_init,data,phy,0xF000,0x1000 +factory,app,factory,0x10000,0x350000 +vfs,data,fat,0x360000,0xCA0000 diff --git a/builder/esp32.py b/builder/esp32.py index 727ad0e1..dc1323fb 100644 --- a/builder/esp32.py +++ b/builder/esp32.py @@ -169,6 +169,7 @@ def get_espidf(): board = None skip_partition_resize = False partition_size = -1 +partition_source = None flash_size = 4 oct_flash = False @@ -304,6 +305,7 @@ def common_args(extra_args): global ccache global skip_partition_resize global partition_size + global partition_source global flash_size global board_variant global optimize_size @@ -401,6 +403,12 @@ def common_args(extra_args): type=int, action='store' ) + esp_argParser.add_argument( + 'PART_SRC', + dest='partition_source', + default=None, + action='store' + ) esp_argParser.add_argument( '--optimize-size', dest='optimize_size', @@ -473,6 +481,17 @@ def common_args(extra_args): if custom_board_path is None: skip_partition_resize = esp_args.skip_partition_resize partition_size = esp_args.partition_size + partition_source = esp_args.partition_source + if partition_source is not None: + if not os.path.isabs(partition_source): + partition_source = os.path.join( + SCRIPT_DIR, partition_source + ) + partition_source = os.path.abspath(partition_source) + if not os.path.isfile(partition_source): + esp_argParser.error( + f'PART_SRC does not name a file: {partition_source}' + ) flash_size = esp_args.flash_size optimize_size = esp_args.optimize_size ota = esp_args.ota @@ -1462,6 +1481,7 @@ def revert_custom_board(): def compile(*args): # NOQA global PORT global flash_size + global partition_source env, cmds = setup_idf_environ() env['IDF_COMPONENT_MANAGER'] = '1' @@ -1474,14 +1494,19 @@ def compile(*args): # NOQA args = build_sdkconfig(*args) if custom_board_path is None: - - if partition_size == -1: - p_size = 0x25A000 + if partition_source is not None: + shutil.copyfile( + partition_source, + f'{SCRIPT_DIR}/build/partitions.csv' + ) else: - p_size = partition_size + if partition_size == -1: + p_size = 0x25A000 + else: + p_size = partition_size - partition = Partition(p_size) - partition.save() + partition = Partition(p_size) + partition.save() update_main() update_mpthreadport() @@ -1511,9 +1536,14 @@ def compile(*args): # NOQA revert_files('esp32') sys.exit(ret_code) + if partition_source is not None: + revert_files('esp32') + sys.exit(ret_code) + if ( not skip_partition_resize and partition_size == -1 and + partition_source is None and custom_board_path is None ): for pattern in ( diff --git a/ext_mod/CAN/CAN_frame.c b/ext_mod/CAN/CAN_frame.c new file mode 100644 index 00000000..aa32b969 --- /dev/null +++ b/ext_mod/CAN/CAN_frame.c @@ -0,0 +1,205 @@ +#include "CAN_frame.h" + +#include "py/objarray.h" +#include "py/runtime.h" + +#include "CAN_node.h" + +static void twai_frame_validate_id(uint32_t id, uint8_t flags) { + const uint32_t max_id = (flags & TWAI_FLAG_EXTENDED) ? 0x1fffffffU : 0x7ffU; + if (id > max_id) { + mp_raise_ValueError(MP_ERROR_TEXT("CAN id out of range")); + } +} + +static uint32_t twai_frame_get_id(mp_obj_t value) { + mp_int_t id = mp_obj_get_int(value); + if (id < 0) { + mp_raise_ValueError(MP_ERROR_TEXT("CAN id out of range")); + } + return (uint32_t)id; +} + +static uint8_t twai_frame_get_dlc(mp_obj_t value) { + mp_int_t dlc = mp_obj_get_int(value); + if (dlc < 0 || dlc > TWAI_FRAME_DATA_LEN) { + mp_raise_ValueError(MP_ERROR_TEXT("DLC must be from 0 to 8")); + } + return (uint8_t)dlc; +} + +static uint8_t twai_frame_get_flags(mp_obj_t value) { + mp_int_t flags = mp_obj_get_int(value); + if (flags < 0 || (flags & ~TWAI_FLAG_MASK)) { + mp_raise_ValueError(MP_ERROR_TEXT("invalid CAN flags")); + } + return (uint8_t)flags; +} + +static void twai_frame_check_editable(const twai_frame_obj_t *self) { + if (twai_frame_is_in_use(self)) { + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("frame is in use")); + } +} + +static mp_obj_t twai_frame_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_id, ARG_dlc, ARG_flags }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_id, MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_dlc, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_flags, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // mp_obj_new_bytearray() copies its input with memcpy(), so it must not + // receive NULL. Allocate the bytearray before the Frame so the Frame is + // not an unrooted GC allocation while another MicroPython allocation runs. + static const uint8_t initial_data[TWAI_FRAME_DATA_LEN] = {0}; + mp_obj_t data = mp_obj_new_bytearray(TWAI_FRAME_DATA_LEN, initial_data); + twai_frame_obj_t *self = mp_obj_malloc(twai_frame_obj_t, type); + self->id = twai_frame_get_id(MP_OBJ_NEW_SMALL_INT(args[ARG_id].u_int)); + self->dlc = twai_frame_get_dlc(MP_OBJ_NEW_SMALL_INT(args[ARG_dlc].u_int)); + self->flags = twai_frame_get_flags(MP_OBJ_NEW_SMALL_INT(args[ARG_flags].u_int)); + twai_frame_validate_id(self->id, self->flags); + self->data = data; + atomic_init(&self->in_use, false); + self->owner = NULL; + twai_frame_prepare_tx(self); + return MP_OBJ_FROM_PTR(self); +} + +static void twai_frame_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + (void)kind; + twai_frame_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_printf(print, "Frame(id=0x%lx, dlc=%u, flags=0x%x)", (unsigned long)self->id, self->dlc, self->flags); +} + +static mp_int_t twai_frame_get_buffer(mp_obj_t self_in, mp_buffer_info_t *bufinfo, mp_uint_t flags) { + twai_frame_obj_t *self = MP_OBJ_TO_PTR(self_in); + (void)flags; + uint8_t *data_buf = twai_frame_data_buf(self); + if (data_buf == NULL) { + mp_raise_ValueError(MP_ERROR_TEXT("Frame data must remain 8 bytes")); + } + bufinfo->buf = data_buf; + bufinfo->len = TWAI_FRAME_DATA_LEN; + bufinfo->typecode = 'B'; + return 0; +} + +static mp_obj_t twai_frame_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) { + twai_frame_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (value != MP_OBJ_SENTINEL) { + twai_frame_check_editable(self); + } + return mp_obj_subscr(self->data, index, value); +} + +static void twai_frame_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { + twai_frame_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (dest[0] == MP_OBJ_NULL) { + if (attr == MP_QSTR_id) { + dest[0] = mp_obj_new_int_from_uint(self->id); + } else if (attr == MP_QSTR_dlc) { + dest[0] = MP_OBJ_NEW_SMALL_INT(self->dlc); + } else if (attr == MP_QSTR_flags) { + dest[0] = MP_OBJ_NEW_SMALL_INT(self->flags); + } else if (attr == MP_QSTR_data) { + dest[0] = self->data; + } else if (attr == MP_QSTR_in_use) { + dest[0] = mp_obj_new_bool(twai_frame_is_in_use(self)); + } else { + dest[1] = MP_OBJ_SENTINEL; + } + return; + } + + if (dest[1] == MP_OBJ_NULL) { + return; + } + twai_frame_check_editable(self); + if (attr == MP_QSTR_id) { + uint32_t id = twai_frame_get_id(dest[1]); + twai_frame_validate_id(id, self->flags); + self->id = id; + } else if (attr == MP_QSTR_dlc) { + self->dlc = twai_frame_get_dlc(dest[1]); + } else if (attr == MP_QSTR_flags) { + uint8_t flags = twai_frame_get_flags(dest[1]); + twai_frame_validate_id(self->id, flags); + self->flags = flags; + } else { + // data and in_use are deliberately read-only attributes. + dest[1] = MP_OBJ_SENTINEL; + return; + } + twai_frame_prepare_tx(self); + dest[0] = MP_OBJ_NULL; +} + +static mp_obj_t twai_frame_release(mp_obj_t self_in) { + twai_frame_obj_t *self = MP_OBJ_TO_PTR(self_in); + twai_node_release_rx_frame(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(twai_frame_release_obj, twai_frame_release); + +static const mp_rom_map_elem_t twai_frame_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_release), MP_ROM_PTR(&twai_frame_release_obj) }, +}; +static MP_DEFINE_CONST_DICT(twai_frame_locals_dict, twai_frame_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + twai_frame_type, + MP_QSTR_Frame, + MP_TYPE_FLAG_NONE, + make_new, twai_frame_make_new, + print, twai_frame_print, + buffer, twai_frame_get_buffer, + subscr, twai_frame_subscr, + attr, twai_frame_attr, + locals_dict, &twai_frame_locals_dict + ); + +bool twai_frame_try_acquire(twai_frame_obj_t *frame) { + bool expected = false; + return atomic_compare_exchange_strong(&frame->in_use, &expected, true); +} + +void twai_frame_release_from_driver(twai_frame_obj_t *frame) { + atomic_store(&frame->in_use, false); +} + +bool twai_frame_is_in_use(const twai_frame_obj_t *frame) { + return atomic_load(&frame->in_use); +} + +uint8_t *twai_frame_data_buf(twai_frame_obj_t *frame) { + mp_obj_array_t *data = MP_OBJ_TO_PTR(frame->data); + return data->len == TWAI_FRAME_DATA_LEN ? data->items : NULL; +} + +bool twai_frame_prepare_tx(twai_frame_obj_t *frame) { + uint8_t *data_buf = twai_frame_data_buf(frame); + if (data_buf == NULL) { + return false; + } + frame->native_frame.header.id = frame->id; + frame->native_frame.header.dlc = frame->dlc; + frame->native_frame.header.ide = (frame->flags & TWAI_FLAG_EXTENDED) != 0; + frame->native_frame.header.rtr = (frame->flags & TWAI_FLAG_RTR) != 0; + frame->native_frame.header.fdf = false; + frame->native_frame.header.brs = false; + frame->native_frame.header.esi = false; + frame->native_frame.buffer = data_buf; + frame->native_frame.buffer_len = frame->dlc; + return true; +} + +void twai_frame_finish_rx(twai_frame_obj_t *frame) { + frame->id = frame->native_frame.header.id; + frame->dlc = frame->native_frame.header.dlc; + frame->flags = (frame->native_frame.header.ide ? TWAI_FLAG_EXTENDED : 0) + | (frame->native_frame.header.rtr ? TWAI_FLAG_RTR : 0); +} diff --git a/ext_mod/CAN/CAN_frame.h b/ext_mod/CAN/CAN_frame.h new file mode 100644 index 00000000..4db019c4 --- /dev/null +++ b/ext_mod/CAN/CAN_frame.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include "py/obj.h" +#include "esp_twai_types.h" + +typedef struct _twai_node_obj_t twai_node_obj_t; + +#define TWAI_FRAME_DATA_LEN (8) +#define TWAI_FLAG_EXTENDED (0x01) +#define TWAI_FLAG_RTR (0x02) +#define TWAI_FLAG_MASK (TWAI_FLAG_EXTENDED | TWAI_FLAG_RTR) + +typedef struct _twai_frame_obj_t { + mp_obj_base_t base; + mp_obj_t data; + uint32_t id; + uint8_t dlc; + uint8_t flags; + atomic_bool in_use; + twai_node_obj_t *owner; + twai_frame_t native_frame; +} twai_frame_obj_t; + +extern const mp_obj_type_t twai_frame_type; + +bool twai_frame_try_acquire(twai_frame_obj_t *frame); +void twai_frame_release_from_driver(twai_frame_obj_t *frame); +bool twai_frame_is_in_use(const twai_frame_obj_t *frame); +uint8_t *twai_frame_data_buf(twai_frame_obj_t *frame); +bool twai_frame_prepare_tx(twai_frame_obj_t *frame); +void twai_frame_finish_rx(twai_frame_obj_t *frame); diff --git a/ext_mod/CAN/CAN_node.c b/ext_mod/CAN/CAN_node.c new file mode 100644 index 00000000..39e5ee48 --- /dev/null +++ b/ext_mod/CAN/CAN_node.c @@ -0,0 +1,404 @@ +#include "CAN_node.h" + +#include + +#include "py/mperrno.h" +#include "py/runtime.h" +#include "freertos/FreeRTOS.h" +#include "freertos/idf_additions.h" +#include "freertos/queue.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_twai_onchip.h" + +#define TWAI_TX_TASK_STACK_SIZE (3072) +#define TWAI_TX_TASK_PRIORITY (tskIDLE_PRIORITY + 1) + +static const char *TAG = "twai"; + +static void twai_raise_esp_err(esp_err_t err) { + mp_raise_OSError(err); +} + +static mp_obj_tuple_t *twai_pool_tuple(mp_obj_t pool_in, const char *name) { + size_t len; + mp_obj_t *items; + mp_obj_get_array(pool_in, &len, &items); + if (len == 0) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%s must not be empty"), name); + } + for (size_t i = 0; i < len; ++i) { + if (mp_obj_get_type(items[i]) != &twai_frame_type) { + mp_raise_TypeError(MP_ERROR_TEXT("pool entries must be Frame objects")); + } + for (size_t j = 0; j < i; ++j) { + if (items[i] == items[j]) { + mp_raise_ValueError(MP_ERROR_TEXT("pool contains duplicate Frame")); + } + } + } + return MP_OBJ_TO_PTR(mp_obj_new_tuple(len, items)); +} + +static bool twai_tuple_has_frame(const mp_obj_tuple_t *pool, const twai_frame_obj_t *frame) { + for (size_t i = 0; i < pool->len; ++i) { + if (MP_OBJ_TO_PTR(pool->items[i]) == frame) { + return true; + } + } + return false; +} + +bool twai_node_frame_is_rx_member(const twai_node_obj_t *self, const twai_frame_obj_t *frame) { + return twai_tuple_has_frame(MP_OBJ_TO_PTR(self->rx_pool), frame); +} + +static bool IRAM_ATTR twai_on_rx_done(twai_node_handle_t handle, const twai_rx_done_event_data_t *edata, void *user_ctx) { + (void)edata; + twai_node_obj_t *self = user_ctx; + twai_frame_obj_t *frame = NULL; + BaseType_t task_woken = pdFALSE; + + // The node driver has already read and released the hardware FIFO before + // invoking this callback. receive_from_isr only copies its internal frame. + if (xQueueReceiveFromISR(self->rx_free_queue, &frame, &task_woken) != pdTRUE) { + atomic_fetch_add(&self->dropped_rx_count, 1); + return task_woken == pdTRUE; + } + + uint8_t *data_buf = twai_frame_data_buf(frame); + if (data_buf == NULL) { + atomic_fetch_add(&self->dropped_rx_count, 1); + xQueueSendFromISR(self->rx_free_queue, &frame, &task_woken); + return task_woken == pdTRUE; + } + frame->native_frame.buffer = data_buf; + frame->native_frame.buffer_len = TWAI_FRAME_DATA_LEN; + if (twai_node_receive_from_isr(handle, &frame->native_frame) != ESP_OK) { + atomic_fetch_add(&self->dropped_rx_count, 1); + xQueueSendFromISR(self->rx_free_queue, &frame, &task_woken); + return task_woken == pdTRUE; + } + + twai_frame_finish_rx(frame); + atomic_store(&frame->in_use, true); + if (xQueueSendFromISR(self->rx_ready_queue, &frame, &task_woken) != pdTRUE) { + atomic_store(&frame->in_use, false); + atomic_fetch_add(&self->dropped_rx_count, 1); + xQueueSendFromISR(self->rx_free_queue, &frame, &task_woken); + } + return task_woken == pdTRUE; +} + +static bool IRAM_ATTR twai_on_tx_done(twai_node_handle_t handle, const twai_tx_done_event_data_t *edata, void *user_ctx) { + (void)handle; + twai_node_obj_t *self = user_ctx; + mp_obj_tuple_t *pool = MP_OBJ_TO_PTR(self->tx_pool); + for (size_t i = 0; i < pool->len; ++i) { + twai_frame_obj_t *frame = MP_OBJ_TO_PTR(pool->items[i]); + if (&frame->native_frame == edata->done_tx_frame) { + twai_frame_release_from_driver(frame); + break; + } + } + return false; +} + +static bool IRAM_ATTR twai_on_error(twai_node_handle_t handle, const twai_error_event_data_t *edata, void *user_ctx) { + (void)handle; + (void)edata; + twai_node_obj_t *self = user_ctx; + atomic_fetch_add(&self->error_count, 1); + return false; +} + +static bool IRAM_ATTR twai_on_state_change(twai_node_handle_t handle, const twai_state_change_event_data_t *edata, void *user_ctx) { + (void)handle; + twai_node_obj_t *self = user_ctx; + if (edata->new_sta == TWAI_ERROR_BUS_OFF) { + atomic_fetch_add(&self->bus_off_count, 1); + } + return false; +} + +static void twai_tx_task(void *arg) { + twai_node_obj_t *self = arg; + for (;;) { + twai_frame_obj_t *frame = NULL; + if (xQueueReceive(self->tx_request_queue, &frame, portMAX_DELAY) != pdTRUE) { + continue; + } + if (frame == NULL) { + break; + } + if (!twai_frame_prepare_tx(frame) || twai_node_transmit(self->node, &frame->native_frame, 0) != ESP_OK) { + twai_frame_release_from_driver(frame); + } + } + self->tx_task = NULL; + vTaskDelete(NULL); +} + +static void twai_stop_tx_task(twai_node_obj_t *self) { + if (self->tx_task == NULL) { + return; + } + twai_frame_obj_t *stop = NULL; + xQueueSend(self->tx_request_queue, &stop, portMAX_DELAY); + while (self->tx_task != NULL) { + vTaskDelay(1); + } +} + +static bool twai_all_frames_idle(const mp_obj_tuple_t *pool) { + for (size_t i = 0; i < pool->len; ++i) { + if (twai_frame_is_in_use(MP_OBJ_TO_PTR(pool->items[i]))) { + return false; + } + } + return true; +} + +static void twai_log_counters(const twai_node_obj_t *self) { + uint_fast32_t error_count = atomic_load(&self->error_count); + uint_fast32_t bus_off_count = atomic_load(&self->bus_off_count); + uint_fast32_t dropped_rx_count = atomic_load(&self->dropped_rx_count); + if (error_count || bus_off_count || dropped_rx_count) { + ESP_LOGI(TAG, "TWAI counters: error=%" PRIuFAST32 " bus_off=%" PRIuFAST32 " dropped_rx=%" PRIuFAST32, + error_count, bus_off_count, dropped_rx_count); + } +} + +static mp_obj_t twai_node_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_tx, ARG_rx, ARG_bitrate, ARG_rx_pool, ARG_tx_pool }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_tx, MP_ARG_REQUIRED | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_rx, MP_ARG_REQUIRED | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_bitrate, MP_ARG_REQUIRED | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_rx_pool, MP_ARG_REQUIRED | MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_tx_pool, MP_ARG_REQUIRED | MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + if (args[ARG_bitrate].u_int <= 0) { + mp_raise_ValueError(MP_ERROR_TEXT("bitrate must be positive")); + } + + mp_obj_tuple_t *rx_pool = twai_pool_tuple(args[ARG_rx_pool].u_obj, "rx_pool"); + mp_obj_tuple_t *tx_pool = twai_pool_tuple(args[ARG_tx_pool].u_obj, "tx_pool"); + for (size_t i = 0; i < rx_pool->len; ++i) { + if (twai_tuple_has_frame(tx_pool, MP_OBJ_TO_PTR(rx_pool->items[i]))) { + mp_raise_ValueError(MP_ERROR_TEXT("a Frame cannot be in both pools")); + } + } + + twai_node_obj_t *self = mp_obj_malloc(twai_node_obj_t, type); + self->node = NULL; + self->rx_free_queue = xQueueCreate(rx_pool->len, sizeof(twai_frame_obj_t *)); + self->rx_ready_queue = xQueueCreate(rx_pool->len, sizeof(twai_frame_obj_t *)); + self->tx_request_queue = xQueueCreate(tx_pool->len, sizeof(twai_frame_obj_t *)); + if (self->rx_free_queue == NULL || self->rx_ready_queue == NULL || self->tx_request_queue == NULL) { + mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("unable to create TWAI queues")); + } + + self->rx_pool = MP_OBJ_FROM_PTR(rx_pool); + self->tx_pool = MP_OBJ_FROM_PTR(tx_pool); + self->rx_pool_len = rx_pool->len; + self->tx_pool_len = tx_pool->len; + self->tx_task = NULL; + self->started = false; + atomic_init(&self->error_count, 0); + atomic_init(&self->bus_off_count, 0); + atomic_init(&self->dropped_rx_count, 0); + + for (size_t i = 0; i < rx_pool->len; ++i) { + twai_frame_obj_t *frame = MP_OBJ_TO_PTR(rx_pool->items[i]); + if (frame->owner != NULL || twai_frame_is_in_use(frame)) { + mp_raise_ValueError(MP_ERROR_TEXT("Frame already belongs to a Node")); + } + xQueueSend(self->rx_free_queue, &frame, 0); + } + for (size_t i = 0; i < tx_pool->len; ++i) { + twai_frame_obj_t *frame = MP_OBJ_TO_PTR(tx_pool->items[i]); + if (frame->owner != NULL || twai_frame_is_in_use(frame)) { + mp_raise_ValueError(MP_ERROR_TEXT("Frame already belongs to a Node")); + } + } + + twai_onchip_node_config_t config = { + .io_cfg = { + .tx = (gpio_num_t)args[ARG_tx].u_int, + .rx = (gpio_num_t)args[ARG_rx].u_int, + .quanta_clk_out = GPIO_NUM_NC, + .bus_off_indicator = GPIO_NUM_NC, + }, + .bit_timing = {.bitrate = (uint32_t)args[ARG_bitrate].u_int}, + .data_timing = {}, + .fail_retry_cnt = 3, + .tx_queue_depth = tx_pool->len, + .intr_priority = 0, + .flags = {}, + }; + esp_err_t err = twai_new_node_onchip(&config, &self->node); + if (err != ESP_OK) { + twai_raise_esp_err(err); + } + twai_event_callbacks_t callbacks = { + .on_tx_done = twai_on_tx_done, + .on_rx_done = twai_on_rx_done, + .on_state_change = twai_on_state_change, + .on_error = twai_on_error, + }; + err = twai_node_register_event_callbacks(self->node, &callbacks, self); + if (err != ESP_OK) { + twai_node_delete(self->node); + self->node = NULL; + twai_raise_esp_err(err); + } + for (size_t i = 0; i < rx_pool->len; ++i) { + ((twai_frame_obj_t *)MP_OBJ_TO_PTR(rx_pool->items[i]))->owner = self; + } + for (size_t i = 0; i < tx_pool->len; ++i) { + ((twai_frame_obj_t *)MP_OBJ_TO_PTR(tx_pool->items[i]))->owner = self; + } + return MP_OBJ_FROM_PTR(self); +} + +static mp_obj_t twai_node_start(mp_obj_t self_in) { + twai_node_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (self->started) { + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("TWAI node already started")); + } + if (xTaskCreatePinnedToCore(twai_tx_task, "twai_tx", TWAI_TX_TASK_STACK_SIZE, self, + TWAI_TX_TASK_PRIORITY, (TaskHandle_t *)&self->tx_task, 0) != pdPASS) { + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("failed to create TWAI task")); + } + esp_err_t err = twai_node_enable(self->node); + if (err != ESP_OK) { + twai_stop_tx_task(self); + twai_raise_esp_err(err); + } + self->started = true; + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(twai_node_start_obj, twai_node_start); + +static mp_obj_t twai_node_send(mp_obj_t self_in, mp_obj_t frame_in) { + twai_node_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (!self->started) { + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("TWAI node is not started")); + } + if (mp_obj_get_type(frame_in) != &twai_frame_type) { + mp_raise_TypeError(MP_ERROR_TEXT("send expects a Frame")); + } + twai_frame_obj_t *frame = MP_OBJ_TO_PTR(frame_in); + if (!twai_tuple_has_frame(MP_OBJ_TO_PTR(self->tx_pool), frame)) { + mp_raise_ValueError(MP_ERROR_TEXT("Frame is not in this Node's tx_pool")); + } + if (twai_frame_data_buf(frame) == NULL) { + mp_raise_ValueError(MP_ERROR_TEXT("Frame data must remain 8 bytes")); + } + if (!twai_frame_try_acquire(frame)) { + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("frame is in use")); + } + if (xQueueSend(self->tx_request_queue, &frame, 0) != pdTRUE) { + twai_frame_release_from_driver(frame); + mp_raise_OSError(MP_EAGAIN); + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_2(twai_node_send_obj, twai_node_send); + +static mp_obj_t twai_node_recv(mp_obj_t self_in) { + twai_node_obj_t *self = MP_OBJ_TO_PTR(self_in); + twai_frame_obj_t *frame = NULL; + if (xQueueReceive(self->rx_ready_queue, &frame, 0) != pdTRUE) { + return mp_const_none; + } + return MP_OBJ_FROM_PTR(frame); +} +static MP_DEFINE_CONST_FUN_OBJ_1(twai_node_recv_obj, twai_node_recv); + +static mp_obj_t twai_node_pending(mp_obj_t self_in) { + twai_node_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_int_from_uint(uxQueueMessagesWaiting(self->rx_ready_queue)); +} +static MP_DEFINE_CONST_FUN_OBJ_1(twai_node_pending_obj, twai_node_pending); + +static mp_obj_t twai_node_deinit(mp_obj_t self_in) { + twai_node_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (self->node == NULL) { + return mp_const_none; + } + if (!twai_all_frames_idle(MP_OBJ_TO_PTR(self->rx_pool)) || !twai_all_frames_idle(MP_OBJ_TO_PTR(self->tx_pool)) + || uxQueueMessagesWaiting(self->rx_ready_queue) != 0 || uxQueueMessagesWaiting(self->tx_request_queue) != 0) { + mp_raise_OSError(MP_EBUSY); + } + twai_stop_tx_task(self); + if (self->started) { + esp_err_t err = twai_node_disable(self->node); + if (err != ESP_OK) { + twai_raise_esp_err(err); + } + self->started = false; + } + twai_event_callbacks_t callbacks = {}; + twai_node_register_event_callbacks(self->node, &callbacks, NULL); + twai_log_counters(self); + esp_err_t err = twai_node_delete(self->node); + if (err != ESP_OK) { + twai_raise_esp_err(err); + } + self->node = NULL; + mp_obj_tuple_t *rx_pool = MP_OBJ_TO_PTR(self->rx_pool); + mp_obj_tuple_t *tx_pool = MP_OBJ_TO_PTR(self->tx_pool); + for (size_t i = 0; i < self->rx_pool_len; ++i) { + ((twai_frame_obj_t *)MP_OBJ_TO_PTR(rx_pool->items[i]))->owner = NULL; + } + for (size_t i = 0; i < self->tx_pool_len; ++i) { + ((twai_frame_obj_t *)MP_OBJ_TO_PTR(tx_pool->items[i]))->owner = NULL; + } + vQueueDelete(self->rx_free_queue); + vQueueDelete(self->rx_ready_queue); + vQueueDelete(self->tx_request_queue); + self->rx_free_queue = NULL; + self->rx_ready_queue = NULL; + self->tx_request_queue = NULL; + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(twai_node_deinit_obj, twai_node_deinit); + +void twai_node_release_rx_frame(twai_frame_obj_t *frame) { + twai_node_obj_t *self = frame->owner; + if (self == NULL || !twai_node_frame_is_rx_member(self, frame)) { + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("Frame is not an RX frame")); + } + if (!twai_frame_is_in_use(frame)) { + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("Frame is not pending")); + } + // Clear in_use before publishing to the ISR. Publishing first would let + // an RX interrupt reuse the Frame and then have this function clear the + // new frame's in_use flag. + twai_frame_release_from_driver(frame); + if (xQueueSend(self->rx_free_queue, &frame, 0) != pdTRUE) { + atomic_store(&frame->in_use, true); + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("RX free pool is full")); + } +} + +static const mp_rom_map_elem_t twai_node_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_start), MP_ROM_PTR(&twai_node_start_obj) }, + { MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&twai_node_send_obj) }, + { MP_ROM_QSTR(MP_QSTR_recv), MP_ROM_PTR(&twai_node_recv_obj) }, + { MP_ROM_QSTR(MP_QSTR_pending), MP_ROM_PTR(&twai_node_pending_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&twai_node_deinit_obj) }, +}; +static MP_DEFINE_CONST_DICT(twai_node_locals_dict, twai_node_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + twai_node_type, + MP_QSTR_Node, + MP_TYPE_FLAG_NONE, + make_new, twai_node_make_new, + locals_dict, &twai_node_locals_dict + ); diff --git a/ext_mod/CAN/CAN_node.h b/ext_mod/CAN/CAN_node.h new file mode 100644 index 00000000..efbf0d51 --- /dev/null +++ b/ext_mod/CAN/CAN_node.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include + +#include "py/obj.h" +#include "esp_twai.h" +#include "freertos/queue.h" +#include "freertos/task.h" + +#include "CAN_frame.h" + +typedef struct _twai_node_obj_t { + mp_obj_base_t base; + twai_node_handle_t node; + QueueHandle_t rx_free_queue; + QueueHandle_t rx_ready_queue; + QueueHandle_t tx_request_queue; + volatile TaskHandle_t tx_task; + mp_obj_t rx_pool; + mp_obj_t tx_pool; + size_t rx_pool_len; + size_t tx_pool_len; + atomic_uint_fast32_t error_count; + atomic_uint_fast32_t bus_off_count; + atomic_uint_fast32_t dropped_rx_count; + bool started; +} twai_node_obj_t; + +extern const mp_obj_type_t twai_node_type; + +bool twai_node_frame_is_rx_member(const twai_node_obj_t *self, const twai_frame_obj_t *frame); +void twai_node_release_rx_frame(twai_frame_obj_t *frame); diff --git a/ext_mod/CAN/examples/lamp_control.py b/ext_mod/CAN/examples/lamp_control.py new file mode 100644 index 00000000..429effc6 --- /dev/null +++ b/ext_mod/CAN/examples/lamp_control.py @@ -0,0 +1,335 @@ +"""Six-lamp LVGL panel using the new pool-based ``CAN`` API. + +Copy this file to the device and run it after flashing the firmware that +contains ``ext_mod/CAN``. It retains the CAN message layout used by the old +application: + +* TX ID 0x001, payload ``[value, register]`` +* RX ID 0x101, first payload byte is the six-lamp state bitmap + +It is safe to execute this file again in the same REPL: the previous node is +drained and deinitialised first. Before an actual MicroPython soft reboot +(Ctrl-D), run ``shutdown_can()``. The current node API has no automatic +soft-reboot cleanup hook. +""" + +import gc +gc.collect() + +import time +import CAN +from micropython import const +import lvgl as lv +import lcd_bus +import rgb_display +import task_handler + + +# --------------------------------------------------------- +# 1. TWAI setup (new node/Frame API) +# --------------------------------------------------------- + +PHYSICAL_ADDR = const(0x01) +CAN_STATE_ID = const(0x0101) +CAN_TX_PIN = const(15) +CAN_RX_PIN = const(16) +CAN_BITRATE = const(50000) + +# RX frames stay owned by the application until release() is called. Eight +# entries absorb short bursts while LVGL's 50-ms timer is between callbacks. +CAN_RX_POOL_SIZE = const(8) +CAN_TX_POOL_SIZE = const(4) + + +def _deinit_idle_can_node(node, rx_pool, tx_pool): + # Ready RX frames are still inside the node queue. Release every one so + # Node.deinit() can perform its strict idle check. + while True: + old_frame = node.recv() + if old_frame is None: + break + old_frame.release() + + for _ in range(25): + busy = False + for old_frame in rx_pool + tx_pool: + if old_frame.in_use: + busy = True + break + if not busy: + break + time.sleep_ms(10) + + if busy: + raise RuntimeError("TWAI Frame is still in use; release it and retry") + node.deinit() + + +def _stop_previous_can_node(): + """Allow re-running this script in one REPL without leaking the node.""" + old_node = globals().get("can_node") + if old_node is not None: + _deinit_idle_can_node( + old_node, + globals().get("can_rx_pool", ()), + globals().get("can_tx_pool", ()), + ) + + +_stop_previous_can_node() + +# The Frame objects are allocated once. Node keeps strong references to both +# pools; the TX picker below uses only a Frame whose driver ownership ended. +can_rx_pool = tuple([CAN.Frame() for _ in range(CAN_RX_POOL_SIZE)]) +can_tx_pool = tuple([CAN.Frame() for _ in range(CAN_TX_POOL_SIZE)]) +can_node = CAN.Node( + CAN_TX_PIN, + CAN_RX_PIN, + CAN_BITRATE, + rx_pool=can_rx_pool, + tx_pool=can_tx_pool, +) +can_node.start() +print("CAN Ready.") + + +def shutdown_can(): + """Drain/release RX Frames and deinitialise TWAI before Ctrl-D.""" + global can_node + _deinit_idle_can_node(can_node, can_rx_pool, can_tx_pool) + # A deinitialised Node no longer owns valid FreeRTOS queues. Do not let a + # later script re-run try to drain that object again. + can_node = None + + +def _idle_tx_frame(): + for frame in can_tx_pool: + if not frame.in_use: + return frame + return None + + +def send_reg_write(addr, reg_addr, value): + """Submit a non-blocking write with the original wire format.""" + frame = _idle_tx_frame() + if frame is None: + print("TWAI send skipped: all TX Frames are busy") + return False + + try: + frame.flags = 0 + frame.id = addr + frame.dlc = 2 + frame.data[0] = value + frame.data[1] = reg_addr + can_node.send(frame) + return True + except OSError as exc: + # EAGAIN means the non-blocking software TX queue is full. + print("TWAI send failed:", exc) + except (RuntimeError, ValueError) as exc: + print("TWAI frame setup failed:", exc) + return False + + +# --------------------------------------------------------- +# 2. LCD hardware configuration +# --------------------------------------------------------- + +WIDTH = const(1024) +HEIGHT = const(600) + +try: + if lv.is_initialized(): + lv.deinit() + time.sleep_ms(50) +except Exception as exc: + print("LVGL deinit warning:", exc) + +lv.init() + +bus = lcd_bus.RGBBus( + hsync=46, vsync=3, de=5, pclk=7, + data0=14, data1=38, data2=18, data3=17, data4=10, + data5=39, data6=0, data7=45, data8=48, data9=47, data10=21, + data11=1, data12=2, data13=42, data14=41, data15=40, + freq=12000000, + hsync_front_porch=40, hsync_back_porch=140, hsync_pulse_width=20, + vsync_front_porch=12, vsync_back_porch=20, vsync_pulse_width=3, + hsync_idle_low=False, vsync_idle_low=False, de_idle_high=False, + pclk_idle_high=False, pclk_active_low=False, +) + +BUFFER_SIZE = 1024 * 60 * 2 +buf1 = bus.allocate_framebuffer(BUFFER_SIZE, lcd_bus.MEMORY_SPIRAM) + +disp = rgb_display.RGBDisplay( + data_bus=bus, display_width=WIDTH, display_height=HEIGHT, + frame_buffer1=buf1, frame_buffer2=None, + color_space=lv.COLOR_FORMAT.RGB565, rgb565_byte_swap=False, +) +disp.init() +try: + disp.set_power(True) + disp.set_backlight(6) +except Exception: + pass + + +# --------------------------------------------------------- +# 2.5. Release TP_RST via CH422G before starting the touch IC +# --------------------------------------------------------- + +try: + from machine import I2C as _MachineI2C, Pin as _Pin + + _ch422g_i2c = _MachineI2C(0, sda=_Pin(8), scl=_Pin(9), freq=100000) + _ch422g_i2c.writeto(0x24, bytes([0x01])) + _ch422g_i2c.writeto(0x38, bytes([0xFF])) + time.sleep_ms(100) + try: + _ch422g_i2c.deinit() + except Exception: + pass + del _ch422g_i2c + print("CH422G touch reset released.") +except Exception as exc: + print("CH422G reset warning:", exc) + + +# --------------------------------------------------------- +# 3. GT911 touch, SDA=8/SCL=9 +# --------------------------------------------------------- + +touch_indev = None +try: + from i2c import I2C + import gt911 + + I2C_BUS = I2C.Bus( + host=1, + scl=const(9), + sda=const(8), + freq=400000, + use_locks=False, + ) + TOUCH_DEVICE = I2C.Device(I2C_BUS, dev_id=gt911.I2C_ADDR, reg_bits=gt911.BITS) + touch_indev = gt911.GT911(TOUCH_DEVICE) + print("Touch (GT911) initialized OK.") +except Exception as exc: + print("Touch init failed:", exc) + + +# --------------------------------------------------------- +# 4. GUI +# --------------------------------------------------------- + +scr = lv.screen_active() +scr.set_style_bg_color(lv.color_hex(0x050814), 0) + +lamps = [] +labels = [] +lamp_states = [False] * 6 +positions = [ + (150, 150), (512, 150), (874, 150), + (150, 450), (512, 450), (874, 450), +] + +for i in range(6): + x, y = positions[i] + obj = lv.obj(scr) + obj.set_size(180, 180) + obj.set_pos(x - 90, y - 90) + obj.set_style_radius(9999, 0) + obj.set_style_bg_color(lv.color_hex(0x333333), 0) + obj.add_flag(lv.obj.FLAG.CLICKABLE) + obj.set_style_bg_color(lv.color_hex(0x555555), lv.PART.MAIN | lv.STATE.PRESSED) + + lbl = lv.label(obj) + lbl.set_text("LAMP {}\nOFF".format(i + 1)) + lbl.center() + lamps.append(obj) + labels.append(lbl) + + +def update_gui_from_state(state_byte): + for i in range(6): + is_on = bool((state_byte >> i) & 1) + if lamp_states[i] != is_on: + lamp_states[i] = is_on + if is_on: + lamps[i].set_style_bg_color(lv.color_hex(0x00FF66), 0) + labels[i].set_text("LAMP {}\nON".format(i + 1)) + else: + lamps[i].set_style_bg_color(lv.color_hex(0x333333), 0) + labels[i].set_text("LAMP {}\nOFF".format(i + 1)) + + +# --------------------------------------------------------- +# 5. Drain all queued CAN frames from LVGL's timer context +# --------------------------------------------------------- + +def check_can_bus_cb(_timer): + try: + while True: + frame = can_node.recv() + if frame is None: + break + try: + if (frame.id == CAN_STATE_ID + and not (frame.flags & CAN.RTR) + and frame.dlc >= 1): + update_gui_from_state(frame.data[0]) + finally: + # RX pool capacity is restored only by release(). + frame.release() + except Exception as exc: + print("TWAI receive warning:", exc) + + +lv.timer_create(check_can_bus_cb, 50, None) + + +# --------------------------------------------------------- +# 6. REPL and touch controls +# --------------------------------------------------------- + +current_local_state = 0x00 + + +def lamp_on(lamp_id): + global current_local_state + if 1 <= lamp_id <= 6: + lamp_mask = 1 << (lamp_id - 1) + send_reg_write(PHYSICAL_ADDR, 0x01, lamp_mask) + current_local_state |= lamp_mask + update_gui_from_state(current_local_state) + + +def lamp_off(lamp_id): + global current_local_state + if 1 <= lamp_id <= 6: + lamp_mask = 1 << (lamp_id - 1) + send_reg_write(PHYSICAL_ADDR, 0x02, lamp_mask) + current_local_state &= ~lamp_mask + update_gui_from_state(current_local_state) + + +def on_lamp_touched(event): + obj = event.get_target_obj() + try: + lamp_id = lamps.index(obj) + 1 + except ValueError: + return + if lamp_states[lamp_id - 1]: + lamp_off(lamp_id) + else: + lamp_on(lamp_id) + + +for lamp_obj in lamps: + lamp_obj.add_event_cb(on_lamp_touched, lv.EVENT.CLICKED, None) + + +print("System Ready! You can type lamp_on(1) or tap the screen.") +th = task_handler.TaskHandler() diff --git a/ext_mod/CAN/micropython.cmake b/ext_mod/CAN/micropython.cmake new file mode 100644 index 00000000..ace6627c --- /dev/null +++ b/ext_mod/CAN/micropython.cmake @@ -0,0 +1,19 @@ +# TWAI user module for the ESP-IDF node-based driver. + +add_library(usermod_CAN INTERFACE) + +# The ESP32 port builds its main component after loading user modules, so this +# makes the node-based driver a direct dependency of MicroPython. +list(APPEND IDF_COMPONENTS esp_driver_twai) + +target_sources(usermod_CAN INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/modCAN.c + ${CMAKE_CURRENT_LIST_DIR}/CAN_frame.c + ${CMAKE_CURRENT_LIST_DIR}/CAN_node.c +) + +target_include_directories(usermod_CAN INTERFACE + ${CMAKE_CURRENT_LIST_DIR} +) + +target_link_libraries(usermod INTERFACE usermod_CAN) diff --git a/ext_mod/CAN/modCAN.c b/ext_mod/CAN/modCAN.c new file mode 100644 index 00000000..cbfb8443 --- /dev/null +++ b/ext_mod/CAN/modCAN.c @@ -0,0 +1,21 @@ +#include "py/obj.h" +#include "py/runtime.h" + +#include "CAN_frame.h" +#include "CAN_node.h" + +static const mp_rom_map_elem_t CAN_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_CAN) }, + { MP_ROM_QSTR(MP_QSTR_Frame), MP_ROM_PTR(&twai_frame_type) }, + { MP_ROM_QSTR(MP_QSTR_Node), MP_ROM_PTR(&twai_node_type) }, + { MP_ROM_QSTR(MP_QSTR_EXTENDED), MP_ROM_INT(TWAI_FLAG_EXTENDED) }, + { MP_ROM_QSTR(MP_QSTR_RTR), MP_ROM_INT(TWAI_FLAG_RTR) }, +}; +static MP_DEFINE_CONST_DICT(CAN_module_globals, CAN_module_globals_table); + +const mp_obj_module_t mp_module_CAN = { + .base = {&mp_type_module}, + .globals = (mp_obj_dict_t *)&CAN_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_CAN, mp_module_CAN); diff --git a/ext_mod/micropython.cmake b/ext_mod/micropython.cmake index 50051f6f..68a9b8ea 100644 --- a/ext_mod/micropython.cmake +++ b/ext_mod/micropython.cmake @@ -3,6 +3,7 @@ if(ESP_PLATFORM) include(${CMAKE_CURRENT_LIST_DIR}/esp32_components.cmake) include(${CMAKE_CURRENT_LIST_DIR}/spi3wire/micropython.cmake) + include(${CMAKE_CURRENT_LIST_DIR}/CAN/micropython.cmake) if(DEFINED ENV{LEDSTRIP}) include(${CMAKE_CURRENT_LIST_DIR}/led_strip/micropython.cmake)