diff --git a/lib/i2c.toit b/lib/i2c.toit index 783debb20..5eb907d27 100644 --- a/lib/i2c.toit +++ b/lib/i2c.toit @@ -63,6 +63,12 @@ CONTROLLER-RESULT-OK_ ::= 0 CONTROLLER-RESULT-NACK_ ::= 1 CONTROLLER-RESULT-TIMEOUT_ ::= 2 +validate-target-address_ address/int address-bit-size/int broadcast/bool -> none: + if address-bit-size != 7 and address-bit-size != 10: throw "INVALID_ARGUMENT" + limit := (1 << address-bit-size) - 1 + if not 0 <= address <= limit: throw "INVALID_ARGUMENT" + if broadcast and address-bit-size == 10: throw "INVALID_ARGUMENT" + /** An addressable I2C target. @@ -117,10 +123,7 @@ class Target: --default-response/ByteArray?=null --pull-up/bool=false --broadcast/bool=false: - if address-bit-size != 7 and address-bit-size != 10: throw "INVALID_ARGUMENT" - limit := (1 << address-bit-size) - 1 - if not 0 <= address <= limit: throw "INVALID_ARGUMENT" - if broadcast and address-bit-size == 10: throw "INVALID_ARGUMENT" + validate-target-address_ address address-bit-size broadcast if send-buffer-size <= 0 or receive-buffer-size <= 0: throw "INVALID_ARGUMENT" response := default-response or ByteArray MAX-DEFAULT-RESPONSE-SIZE --initial=0xff if response.size == 0 or response.size > MAX-DEFAULT-RESPONSE-SIZE: @@ -348,10 +351,7 @@ class RegisterTarget: --receive-buffer-size/int=DEFAULT-TARGET-BUFFER-SIZE --pull-up/bool=false --broadcast/bool=false: - if address-bit-size != 7 and address-bit-size != 10: throw "INVALID_ARGUMENT" - address-limit := (1 << address-bit-size) - 1 - if not 0 <= address <= address-limit: throw "INVALID_ARGUMENT" - if broadcast and address-bit-size == 10: throw "INVALID_ARGUMENT" + validate-target-address_ address address-bit-size broadcast if register-address-byte-size != 1 and register-address-byte-size != 2: throw "INVALID_ARGUMENT" register-limit := 1 << (register-address-byte-size * 8) if not 0 < register-count <= register-limit: throw "INVALID_ARGUMENT" @@ -563,9 +563,18 @@ class Bus: test address --timeout-ms/int=100 -> bool: if not 0 <= address <= 0x7f: throw "INVALID_ARGUMENT" if timeout-ms <= 0: throw "INVALID_ARGUMENT" - return perform-controller-operation_ - (: i2c-bus-probe_ resource_ address timeout-ms) - (: i2c-bus-probe-finish_ resource_) + result := false + error := catch --unwind=(: it != DEADLINE-EXCEEDED-ERROR): + result = with-timeout --ms=timeout-ms: + perform-controller-operation_ + (: i2c-bus-probe_ resource_ address timeout-ms) + (: i2c-bus-probe-finish_ resource_) + if error: + // An earlier caller deadline is automatically rethrown by 'catch'. A + // deadline caught here was installed for this probe and is equivalent + // to the driver's hardware timeout. + return false + return result /** Closes this I2C bus. diff --git a/lib/spi.toit b/lib/spi.toit index f46b45298..26e07b3a6 100644 --- a/lib/spi.toit +++ b/lib/spi.toit @@ -577,7 +577,11 @@ class Bus: cycles before the first clock edge. ESP-IDF only supports this option for half-duplex transactions, except for a limited one-cycle case on the classic ESP32. $cs-hold-cycles keeps CS active after the last clock edge. - Both values must be between 0 and 16. + Setup must be between 0 and 16. Hold must be between 0 and 16, except on the + classic ESP32 where the maximum is 15. + + On ESP32, a bus configured with only one of `mosi` and `miso` registers its + devices as half-duplex. A bus with both data pins remains full-duplex. The bus retains the returned device until either the device or the bus is explicitly closed. Dropping the last application reference does not free a @@ -600,6 +604,8 @@ class Bus: if mode < 0 or mode > 3: throw "Argument Error" if not 0 <= cs-setup-cycles <= 16: throw "OUT_OF_RANGE" if not 0 <= cs-hold-cycles <= 16: throw "OUT_OF_RANGE" + if system.architecture == system.ARCHITECTURE-ESP32 and cs-hold-cycles == 16: + throw "OUT_OF_RANGE" cs-num := gpio.to-pin-num_ cs dc-num := gpio.to-pin-num_ dc // For a deprecated gpio.Pin the dc pin is configured here; for the new diff --git a/src/resources/i2c_esp32.cc b/src/resources/i2c_esp32.cc index 10a24c6b0..6301e8788 100644 --- a/src/resources/i2c_esp32.cc +++ b/src/resources/i2c_esp32.cc @@ -19,10 +19,13 @@ #include #include +#ifdef CONFIG_TOIT_ENABLE_I2C_TARGET #include -#include +#endif #include +#ifdef CONFIG_TOIT_ENABLE_I2C_TARGET #include +#endif #include #include "../linked.h" @@ -60,6 +63,8 @@ class I2cResourceGroup : public ResourceGroup { const word kControllerDoneState = 1 << 0; +#ifdef CONFIG_TOIT_ENABLE_I2C_TARGET + const word kTargetReceiveState = 1 << 0; const word kTargetRequestState = 1 << 1; const word kTargetOverflowState = 1 << 2; @@ -76,30 +81,58 @@ class I2cTargetResourceGroup : public ResourceGroup { } }; -class I2cTargetResource : public EventQueueResource { +struct I2cTargetConfig { + int sda; + int scl; + i2c_addr_bit_len_t address_length; + uint16_t address; + uint32_t send_buffer_size; + uint32_t receive_buffer_size; + bool pullup; + bool allow_power_down; + bool broadcast; +}; + +class I2cTargetResourceBase : public EventQueueResource { + public: + I2cTargetResourceBase(ResourceGroup* group, QueueHandle_t event_queue) + : EventQueueResource(group, event_queue) {} + ~I2cTargetResourceBase() override; + + i2c_slave_dev_handle_t handle() const { return handle_; } + GpioPins& owned_pins() { return owned_pins_; } + + esp_err_t initialize_driver( + const I2cTargetConfig& config, + const i2c_slave_event_callbacks_t& callbacks, + void* callback_context); + + protected: + void release_driver(); + + private: + i2c_slave_dev_handle_t handle_ = null; + GpioPins owned_pins_; +}; + +class I2cTargetResource : public I2cTargetResourceBase { public: TAG(I2cTargetResource); I2cTargetResource(I2cTargetResourceGroup* group, - i2c_slave_dev_handle_t handle, QueueHandle_t event_queue, MessageBufferHandle_t receive_buffer) - : EventQueueResource(group, event_queue) - , handle_(handle) + : I2cTargetResourceBase(group, event_queue) , receive_buffer_(receive_buffer) { spinlock_initialize(&spinlock_); } ~I2cTargetResource() override { - ESP_ERROR_CHECK(i2c_del_slave_device(handle_)); + release_driver(); vMessageBufferDeleteWithCaps(receive_buffer_); - vQueueDeleteWithCaps(queue()); - owned_pins_.release(); } - i2c_slave_dev_handle_t handle() const { return handle_; } MessageBufferHandle_t receive_buffer() const { return receive_buffer_; } - GpioPins& owned_pins() { return owned_pins_; } I2C_IRAM_ATTR bool receive_from_isr(const uint8_t* data, size_t length) { BaseType_t higher_was_woken = pdFALSE; @@ -169,26 +202,22 @@ class I2cTargetResource : public EventQueueResource { xQueueSendFromISR(queue(), &payload, higher_was_woken); } - i2c_slave_dev_handle_t handle_; MessageBufferHandle_t receive_buffer_; mutable spinlock_t spinlock_; word pending_event_ = 0; word request_count_ = 0; word dropped_receive_count_ = 0; - GpioPins owned_pins_; }; -class I2cRegisterTargetResource : public EventQueueResource { +class I2cRegisterTargetResource : public I2cTargetResourceBase { public: TAG(I2cRegisterTargetResource); I2cRegisterTargetResource(I2cResourceGroup* group, - i2c_slave_dev_handle_t handle, uint8_t* registers, uint32_t register_count, uint32_t register_address_byte_size) - : EventQueueResource(group, null) - , handle_(handle) + : I2cTargetResourceBase(group, null) , registers_(registers) , register_count_(register_count) , register_address_byte_size_(register_address_byte_size) { @@ -196,9 +225,8 @@ class I2cRegisterTargetResource : public EventQueueResource { } ~I2cRegisterTargetResource() override { - ESP_ERROR_CHECK(i2c_del_slave_device(handle_)); + release_driver(); free(registers_); - owned_pins_.release(); } I2C_IRAM_ATTR void receive_from_isr(const uint8_t* data, size_t length, bool overflow) { @@ -275,7 +303,6 @@ class I2cRegisterTargetResource : public EventQueueResource { } uint32_t register_count() const { return register_count_; } - GpioPins& owned_pins() { return owned_pins_; } private: uint8_t load_register(uint32_t index) const { @@ -304,7 +331,6 @@ class I2cRegisterTargetResource : public EventQueueResource { portEXIT_CRITICAL_ISR(&spinlock_); } - i2c_slave_dev_handle_t handle_; uint8_t* registers_; uint8_t transmit_buffer_[SOC_I2C_FIFO_LEN]; uint32_t register_count_; @@ -313,9 +339,53 @@ class I2cRegisterTargetResource : public EventQueueResource { uint32_t prefetch_pointer_ = 0; mutable spinlock_t spinlock_; word dropped_write_count_ = 0; - GpioPins owned_pins_; }; +I2cTargetResourceBase::~I2cTargetResourceBase() { + ASSERT(handle_ == null); + if (queue() != null) vQueueDeleteWithCaps(queue()); + owned_pins_.release(); +} + +esp_err_t I2cTargetResourceBase::initialize_driver( + const I2cTargetConfig& config, + const i2c_slave_event_callbacks_t& callbacks, + void* callback_context) { + ASSERT(handle_ == null); + i2c_slave_config_t driver_config = { + .i2c_port = -1, + .sda_io_num = static_cast(config.sda), + .scl_io_num = static_cast(config.scl), + .clk_source = I2C_CLK_SRC_DEFAULT, + .send_buf_depth = config.send_buffer_size, + .receive_buf_depth = config.receive_buffer_size, + .slave_addr = config.address, + .addr_bit_len = config.address_length, + .intr_priority = 0, + .flags = { + .allow_pd = config.allow_power_down, + .enable_internal_pullup = config.pullup, + #if SOC_I2C_SLAVE_SUPPORT_BROADCAST + .broadcast_en = config.broadcast, + #endif + }, + }; + i2c_slave_dev_handle_t handle; + esp_err_t err = i2c_new_slave_device(&driver_config, &handle); + if (err != ESP_OK) return err; + handle_ = handle; + return i2c_slave_register_event_callbacks( + handle_, &callbacks, callback_context); +} + +void I2cTargetResourceBase::release_driver() { + if (handle_ == null) return; + ESP_ERROR_CHECK(i2c_del_slave_device(handle_)); + handle_ = null; +} + +#endif // CONFIG_TOIT_ENABLE_I2C_TARGET + class I2cBusResource; class I2cDeviceResource; typedef DoubleLinkedList DeviceList; @@ -489,6 +559,8 @@ PRIMITIVE(init) { return proxy; } +#ifdef CONFIG_TOIT_ENABLE_I2C_TARGET + PRIMITIVE(target_init) { ByteArray* proxy = process->object_heap()->allocate_proxy(); if (proxy == null) FAIL(ALLOCATION_FAILED); @@ -517,6 +589,23 @@ I2C_IRAM_ATTR static bool target_request_handler( return resource->request_from_isr(); } +static bool decode_target_address( + int address_bit_size, + uint16_t address, + i2c_addr_bit_len_t* address_length) { + if (address_bit_size == 7 && address <= 0x7f) { + *address_length = I2C_ADDR_BIT_LEN_7; + return true; + } +#if SOC_I2C_SUPPORT_10BIT_ADDR + if (address_bit_size == 10 && address <= 0x3ff) { + *address_length = I2C_ADDR_BIT_LEN_10; + return true; + } +#endif + return false; +} + PRIMITIVE(target_create) { ARGS(I2cTargetResourceGroup, group, int, sda, @@ -537,15 +626,8 @@ PRIMITIVE(target_create) { } i2c_addr_bit_len_t address_length; - if (address_bit_size == 7 && address <= 0x7f) { - address_length = I2C_ADDR_BIT_LEN_7; - #if SOC_I2C_SUPPORT_10BIT_ADDR - } else if (address_bit_size == 10 && address <= 0x3ff) { - address_length = I2C_ADDR_BIT_LEN_10; - #endif - } else { - FAIL(INVALID_ARGUMENT); - } + if (!decode_target_address( + address_bit_size, address, &address_length)) FAIL(INVALID_ARGUMENT); #if !SOC_I2C_SLAVE_SUPPORT_BROADCAST if (broadcast) FAIL(UNSUPPORTED); @@ -578,50 +660,35 @@ PRIMITIVE(target_create) { [&] { if (!handed_to_resource) vMessageBufferDeleteWithCaps(receive_buffer); } }; - i2c_slave_config_t config = { - .i2c_port = -1, - .sda_io_num = static_cast(sda_num), - .scl_io_num = static_cast(scl_num), - .clk_source = I2C_CLK_SRC_DEFAULT, - .send_buf_depth = send_buffer_size, - .receive_buf_depth = receive_buffer_size, - .slave_addr = address, - .addr_bit_len = address_length, - .intr_priority = 0, - .flags = { - .allow_pd = allow_power_down, - .enable_internal_pullup = pullup, - #if SOC_I2C_SLAVE_SUPPORT_BROADCAST - .broadcast_en = broadcast, - #endif - }, - }; - - i2c_slave_dev_handle_t handle; - esp_err_t err = i2c_new_slave_device(&config, &handle); - if (err == ESP_ERR_NOT_FOUND) FAIL(ALREADY_IN_USE); - if (err != ESP_OK) return Primitive::os_error(err, process); - Defer delete_target { - [&] { if (!handed_to_resource) i2c_del_slave_device(handle); } - }; - - auto resource = _new I2cTargetResource(group, handle, event_queue, receive_buffer); + auto resource = _new I2cTargetResource(group, event_queue, receive_buffer); if (resource == null) FAIL(MALLOC_FAILED); handed_to_resource = true; bool registered = false; Defer delete_resource { [&] { if (!registered) delete resource; } }; + I2cTargetConfig config = { + .sda = sda_num, + .scl = scl_num, + .address_length = address_length, + .address = address, + .send_buffer_size = send_buffer_size, + .receive_buffer_size = receive_buffer_size, + .pullup = pullup, + .allow_power_down = allow_power_down, + .broadcast = broadcast, + }; i2c_slave_event_callbacks_t callbacks = { .on_request = target_request_handler, .on_receive = target_receive_handler, .on_transmit = null, .on_transmit_done = null, }; - err = i2c_slave_register_event_callbacks(handle, &callbacks, resource); + esp_err_t err = resource->initialize_driver(config, callbacks, resource); + if (err == ESP_ERR_NOT_FOUND) FAIL(ALREADY_IN_USE); if (err != ESP_OK) return Primitive::os_error(err, process); err = i2c_slave_set_default_response( - handle, default_response.address(), default_response.length()); + resource->handle(), default_response.address(), default_response.length()); if (err != ESP_OK) return Primitive::os_error(err, process); resource->owned_pins().adopt(reserver); @@ -750,15 +817,8 @@ PRIMITIVE(register_target_create) { if (register_count > addressable_register_count) FAIL(INVALID_ARGUMENT); i2c_addr_bit_len_t address_length; - if (address_bit_size == 7 && address <= 0x7f) { - address_length = I2C_ADDR_BIT_LEN_7; - #if SOC_I2C_SUPPORT_10BIT_ADDR - } else if (address_bit_size == 10 && address <= 0x3ff) { - address_length = I2C_ADDR_BIT_LEN_10; - #endif - } else { - FAIL(INVALID_ARGUMENT); - } + if (!decode_target_address( + address_bit_size, address, &address_length)) FAIL(INVALID_ARGUMENT); #if !SOC_I2C_SLAVE_SUPPORT_BROADCAST if (broadcast) FAIL(UNSUPPORTED); @@ -771,7 +831,7 @@ PRIMITIVE(register_target_create) { ByteArray* proxy = process->object_heap()->allocate_proxy(); if (proxy == null) FAIL(ALLOCATION_FAILED); - uint8_t* registers = unvoid_cast(heap_caps_calloc( + auto registers = unvoid_cast(heap_caps_calloc( register_count, sizeof(uint8_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); if (registers == null) FAIL(MALLOC_FAILED); bool handed_to_resource = false; @@ -783,47 +843,32 @@ PRIMITIVE(register_target_create) { int scl_num = reserver.decode_and_take(scl, &reserve_ok); if (!reserve_ok) FAIL(ALREADY_IN_USE); - i2c_slave_config_t config = { - .i2c_port = -1, - .sda_io_num = static_cast(sda_num), - .scl_io_num = static_cast(scl_num), - .clk_source = I2C_CLK_SRC_DEFAULT, - .send_buf_depth = SOC_I2C_FIFO_LEN, - .receive_buf_depth = receive_buffer_size, - .slave_addr = address, - .addr_bit_len = address_length, - .intr_priority = 0, - .flags = { - .allow_pd = allow_power_down, - .enable_internal_pullup = pullup, - #if SOC_I2C_SLAVE_SUPPORT_BROADCAST - .broadcast_en = broadcast, - #endif - }, - }; - - i2c_slave_dev_handle_t handle; - esp_err_t err = i2c_new_slave_device(&config, &handle); - if (err == ESP_ERR_NOT_FOUND) FAIL(ALREADY_IN_USE); - if (err != ESP_OK) return Primitive::os_error(err, process); - Defer delete_target { - [&] { if (!handed_to_resource) i2c_del_slave_device(handle); } - }; - auto resource = _new I2cRegisterTargetResource( - group, handle, registers, register_count, register_address_byte_size); + group, registers, register_count, register_address_byte_size); if (resource == null) FAIL(MALLOC_FAILED); handed_to_resource = true; bool registered = false; Defer delete_resource { [&] { if (!registered) delete resource; } }; + I2cTargetConfig config = { + .sda = sda_num, + .scl = scl_num, + .address_length = address_length, + .address = address, + .send_buffer_size = SOC_I2C_FIFO_LEN, + .receive_buffer_size = receive_buffer_size, + .pullup = pullup, + .allow_power_down = allow_power_down, + .broadcast = broadcast, + }; i2c_slave_event_callbacks_t callbacks = { .on_request = null, .on_receive = register_target_receive_handler, .on_transmit = register_target_transmit_handler, .on_transmit_done = register_target_transmit_done_handler, }; - err = i2c_slave_register_event_callbacks(handle, &callbacks, resource); + esp_err_t err = resource->initialize_driver(config, callbacks, resource); + if (err == ESP_ERR_NOT_FOUND) FAIL(ALREADY_IN_USE); if (err != ESP_OK) return Primitive::os_error(err, process); resource->owned_pins().adopt(reserver); @@ -877,6 +922,27 @@ PRIMITIVE(register_target_dropped_write_count) { return Smi::from(target->dropped_write_count()); } +#else + +PRIMITIVE(target_init) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_create) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_close) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_receive) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_write) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_set_write_pending) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_set_handler_mode) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_take_request_count) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_dropped_receive_count) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_create) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_close) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_get) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_set) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_read) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_write) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_dropped_write_count) { FAIL(UNIMPLEMENTED); } + +#endif // CONFIG_TOIT_ENABLE_I2C_TARGET + PRIMITIVE(bus_create) { ARGS(I2cResourceGroup, group, int, sda, int, scl, bool, pullup); diff --git a/src/resources/spi_esp32.cc b/src/resources/spi_esp32.cc index d8f8cd382..d2a95f754 100644 --- a/src/resources/spi_esp32.cc +++ b/src/resources/spi_esp32.cc @@ -19,12 +19,19 @@ #include #include +#ifdef CONFIG_TOIT_ENABLE_SPI_TARGET #include -#include -#include #include +#include +#if SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE +#ifdef CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET +#include +#endif #include #include +#endif +#endif +#include #include "../objects_inline.h" #include "../process.h" @@ -53,13 +60,27 @@ static ResourcePool spi_host_devices( #endif ); +static uint8_t* allocate_dma_buffer(size_t size, size_t alignment) { + if (size == 0) return null; + size_t allocation_size = (size + alignment - 1) & ~(alignment - 1); + if (allocation_size < size) return null; + return static_cast(heap_caps_aligned_alloc( + alignment, + allocation_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT)); +} + +#ifdef CONFIG_TOIT_ENABLE_SPI_TARGET + const word kSpiTargetReadyState = 1 << 0; const word kSpiTargetDoneState = 1 << 1; const int64 kSpiTargetTeardownTimeoutUs = 10 * 1000 * 1000; +#ifdef CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET const size_t kSpiBufferTargetMaxTransferSize = 4092; const word kSpiBufferTargetReceivedState = 1 << 2; const word kSpiBufferTargetStoppedState = 1 << 3; const word kSpiBufferTargetArmedState = 1 << 4; +#endif static size_t spi_dma_buffer_alignment(bool dma) { if (!dma) return 4; @@ -159,6 +180,8 @@ class SpiTargetResource : public EventQueueResource { GpioPins owned_pins_; }; +#ifdef CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET + class SpiBufferTargetResource : public EventQueueResource { public: TAG(SpiBufferTargetResource); @@ -232,9 +255,17 @@ class SpiBufferTargetResource : public EventQueueResource { GpioPins owned_pins_; }; -SpiResourceGroup::SpiResourceGroup(Process* process, EventSource* event_source, spi_host_device_t host_device) +#endif // CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET + +#endif // CONFIG_TOIT_ENABLE_SPI_TARGET + +SpiResourceGroup::SpiResourceGroup(Process* process, + EventSource* event_source, + spi_host_device_t host_device, + bool half_duplex) : ResourceGroup(process, event_source) - , host_device_(host_device) {} + , host_device_(host_device) + , half_duplex_(half_duplex) {} SpiResourceGroup::~SpiResourceGroup() { SystemEventSource::instance()->run([&]() -> void { @@ -311,6 +342,8 @@ bool SpiDevice::receive_event(word* data) { return xQueueReceive(queue(), data, 0) == pdTRUE; } +#ifdef CONFIG_TOIT_ENABLE_SPI_TARGET + SpiTargetResource::~SpiTargetResource() { if (initialized_ && operation_in_flight_) { // Process teardown can bypass Target.close. Abort and wait for the driver @@ -382,6 +415,8 @@ void SpiTargetResource::finish_operation() { operation_in_flight_ = false; } +#ifdef CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET + SpiBufferTargetResource::SpiBufferTargetResource( SpiTargetResourceGroup* group, spi_host_device_t host_device, @@ -647,6 +682,10 @@ void SpiBufferTargetResource::signal_from_isr(word event) { if (higher_was_woken == pdTRUE) portYIELD_FROM_ISR(); } +#endif // CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET + +#endif // CONFIG_TOIT_ENABLE_SPI_TARGET + MODULE_IMPLEMENTATION(spi, MODULE_SPI); PRIMITIVE(init) { @@ -707,7 +746,10 @@ PRIMITIVE(init) { } SpiResourceGroup* spi = _new SpiResourceGroup( - process, EventQueueEventSource::instance(), host_device); + process, + EventQueueEventSource::instance(), + host_device, + mosi_num == -1 || miso_num == -1); if (!spi) { SystemEventSource::instance()->run([&]() -> void { FATAL_IF_NOT_ESP_OK(spi_bus_free(capture.host_device)); @@ -725,6 +767,8 @@ PRIMITIVE(init) { return proxy; } +#ifdef CONFIG_TOIT_ENABLE_SPI_TARGET + PRIMITIVE(target_init) { ByteArray* proxy = process->object_heap()->allocate_proxy(); if (proxy == null) FAIL(ALLOCATION_FAILED); @@ -875,15 +919,7 @@ PRIMITIVE(target_close) { return process->null_object(); } -static uint8_t* allocate_dma_buffer(size_t size, size_t alignment) { - if (size == 0) return null; - size_t allocation_size = (size + alignment - 1) & ~(alignment - 1); - if (allocation_size < size) return null; - return static_cast(heap_caps_aligned_alloc( - alignment, - allocation_size, - MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT)); -} +#ifdef CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET SPI_TARGET_ISR_ATTR static void spi_buffer_target_done_callback( spi_slave_transaction_t* transaction) { @@ -1157,6 +1193,20 @@ PRIMITIVE(buffer_target_dropped_receive_count) { return Smi::from(target->dropped_receive_count()); } +#else + +PRIMITIVE(buffer_target_create) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_arm) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_close) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_get) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_set) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_read) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_write) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_receive) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_dropped_receive_count) { FAIL(UNIMPLEMENTED); } + +#endif // CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET + PRIMITIVE(target_transfer_start) { ARGS(SpiTargetResource, resource, Blob, transmit, @@ -1250,6 +1300,25 @@ PRIMITIVE(target_transfer_finish) { return Smi::from(result_size); } +#else + +PRIMITIVE(target_init) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_create) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_close) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_transfer_start) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_transfer_finish) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_create) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_arm) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_close) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_get) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_set) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_read) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_write) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_receive) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_dropped_receive_count) { FAIL(UNIMPLEMENTED); } + +#endif // CONFIG_TOIT_ENABLE_SPI_TARGET + PRIMITIVE(close) { ARGS(SpiResourceGroup, spi); spi->tear_down(); @@ -1317,7 +1386,9 @@ PRIMITIVE(device) { .input_delay_ns = 0, .sample_point = SPI_SAMPLING_POINT_PHASE_0, .spics_io_num = cs_num, - .flags = 0, + .flags = spi->half_duplex() + ? static_cast(SPI_DEVICE_HALFDUPLEX) + : 0u, .queue_size = 1, .pre_cb = null, .post_cb = spi_post_transfer_callback, diff --git a/src/resources/spi_esp32.h b/src/resources/spi_esp32.h index 88c000757..e0b37ffe0 100644 --- a/src/resources/spi_esp32.h +++ b/src/resources/spi_esp32.h @@ -35,7 +35,10 @@ const int kSpiControllerMaxDevicesPerHost = 6; class SpiResourceGroup : public ResourceGroup { public: TAG(SpiResourceGroup); - SpiResourceGroup(Process* process, EventSource* event_source, spi_host_device_t host_device); + SpiResourceGroup(Process* process, + EventSource* event_source, + spi_host_device_t host_device, + bool half_duplex); ~SpiResourceGroup() override; uint32_t on_event(Resource* resource, word data, uint32_t state) override { @@ -43,6 +46,7 @@ class SpiResourceGroup : public ResourceGroup { } spi_host_device_t host_device() { return host_device_; } + bool half_duplex() const { return half_duplex_; } // GPIO pins reserved by this bus (mosi/miso/clock). GpioPins& owned_pins() { return owned_pins_; } @@ -57,6 +61,7 @@ class SpiResourceGroup : public ResourceGroup { private: spi_host_device_t host_device_; + const bool half_duplex_; int device_count_ = 0; GpioPins owned_pins_; }; diff --git a/tests/hw/esp32/i2c-async-controller-shared.toit b/tests/hw/esp32/i2c-async-controller-shared.toit index 839843f4d..96e8812ee 100644 --- a/tests/hw/esp32/i2c-async-controller-shared.toit +++ b/tests/hw/esp32/i2c-async-controller-shared.toit @@ -19,6 +19,7 @@ UART-RX2 ::= Variant.CURRENT.board-connection-pin2 UART-TX2 ::= Variant.CURRENT.board-connection-pin1 I2C-SDA ::= Variant.CURRENT.board-connection-pin3 +SYNC-PIN ::= Variant.CURRENT.board-connection-pin4 I2C-SCL ::= Variant.CURRENT.board-connection-pin5 I2C-SCL-PROBE ::= Variant.CURRENT.board-connection-pin6 @@ -27,6 +28,7 @@ MISSING-ADDRESS ::= 0x71 READY ::= 0xa5 OK ::= 0x5a +SYNC-LOW-SEEN ::= 0xc3 SET ::= 1 RECONFIGURE ::= 2 @@ -47,13 +49,13 @@ main-board1: test-board1: port := uart.Port --rx=UART-RX1 --tx=UART-TX1 --baud-rate=115_200 - expect-equals READY port.in.read-byte + synchronize-controller port if system.architecture == system.ARCHITECTURE-ESP32: test-board1-esp32 port return - bus := i2c.Bus --sda=I2C-SDA --scl=I2C-SCL --frequency=100_000 --pull-up=false + bus := i2c.Bus --sda=I2C-SDA --scl=I2C-SCL --frequency=100_000 --pull-up // Probe completion exercises both DONE and NACK callbacks. Scanning repeats // this over enough transactions to catch stale completion state. @@ -62,6 +64,7 @@ test-board1: found := bus.scan --timeout-ms=5 expect (found.contains ADDRESS) expect-not (found.contains MISSING-ADDRESS) + print "Async I2C: probe and scan complete" missing := bus.device MISSING-ADDRESS expect-throw "I2C_NACK": missing.write #[1] @@ -81,6 +84,7 @@ test-board1: expect-equals initial (registers.read-bytes 0 initial.size) expect-equals (wrapped initial 17 1024) (registers.read-bytes 17 1024) + print "Async I2C: initial register reads complete" into := ByteArray 40: 0xee device.write #[23] @@ -108,6 +112,7 @@ test-board1: done.do: it.get errors.do: | error/any? | if error: throw error + print "Async I2C: contention complete" device.close slow := bus.device ADDRESS --frequency=50_000 @@ -162,6 +167,7 @@ test-board1: reconfigure port REGISTER-10 set-registers port 0 wide expect-equals #[wide[3]] (ten.write-read #[3] 1) + print "Async I2C: address-width collision complete" seven.close ten.close @@ -169,6 +175,7 @@ test-board1: // Prove that the suspended controller task does not prevent another Toit // task from running during successful clock stretching. + print "Async I2C: dynamic clock-stretch recovery" stretched := bus.device ADDRESS --timeout-us=30_000 expected := make-data 17 0xc4 send-command port DYNAMIC-READ [expected, encode-u16 10] @@ -188,6 +195,7 @@ test-board1: // A task deadline aborts the native transaction even when every individual // stretch is shorter than the device's SCL timeout. The bus must be reusable // after the target releases the clock. + print "Async I2C: deadline abort recovery" abortable := bus.device ADDRESS --timeout-us=100_000 send-command port DYNAMIC-READ [#[0x5a], encode-u16 20] expect-throw DEADLINE-EXCEEDED-ERROR: @@ -211,6 +219,26 @@ test-board1: expect (bus.test ADDRESS) abortable.close + // Hold SCL externally before START. The per-device stretch timeout applies + // to SCL pulses after a transaction has started, so use the task deadline + // to abort this bus-not-idle case. Releasing the probe pin lets us verify + // that the same bus and target recover immediately. + print "Async I2C: external SCL deadline recovery" + external-timeout-device := bus.device ADDRESS --timeout-us=2_000 + send-command port STRETCH-CLOCK [encode-u16 10] + expect-throw DEADLINE-EXCEEDED-ERROR: + with-timeout --ms=3: external-timeout-device.read 1 + expect-equals OK port.in.read-byte + expect (bus.test ADDRESS) + external-timeout-device.close + + recovered := bus.device ADDRESS --timeout-us=30_000 + recovered-data := #[0x6b] + send-command port DYNAMIC-READ [recovered-data, encode-u16 1] + expect-equals recovered-data (recovered.read 1) + expect-equals OK port.in.read-byte + recovered.close + // Exercise the controller pull-up configuration and resource reuse after a // large number of asynchronous transactions and errors. bus.close @@ -364,7 +392,7 @@ test-board2: register-target/i2c.RegisterTarget? := make-register-target REGISTER-7 dynamic-target/i2c.Target? := null port := uart.Port --rx=UART-RX2 --tx=UART-TX2 --baud-rate=115_200 - send-byte port READY + synchronize-target port while true: command := port.in.read-byte @@ -400,6 +428,13 @@ test-board2: sleep --ms=(decode-u16 parts[1]) parts[0] send-byte port OK + else if command == STRETCH-CLOCK: + stretcher := gpio.Pin I2C-SCL-PROBE --output --open-drain --value=0 + send-byte port READY + sleep --ms=(decode-u16 parts[0]) + stretcher.set 1 + stretcher.close + send-byte port OK else if command == TIMEOUT-READ: send-byte port READY dynamic-target.serve-read-requests: @@ -420,7 +455,7 @@ test-board2-esp32 -> none: --receive-buffer-size=512 --pull-up port := uart.Port --rx=UART-RX2 --tx=UART-TX2 --baud-rate=115_200 - send-byte port READY + synchronize-target port while true: command := port.in.read-byte @@ -500,6 +535,19 @@ send-byte port/uart.Port value/int -> none: port.out.write-byte value port.out.flush +synchronize-controller port/uart.Port -> none: + ready := gpio.Pin SYNC-PIN --input --pull-up + while ready.get != 0: sleep --ms=1 + send-byte port SYNC-LOW-SEEN + while ready.get != 1: sleep --ms=1 + ready.close + +synchronize-target port/uart.Port -> none: + ready := gpio.Pin SYNC-PIN --output --value=0 + while port.in.read-byte != SYNC-LOW-SEEN: null + ready.set 1 + ready.close + encode-u16 value/int -> ByteArray: return #[value & 0xff, (value >> 8) & 0xff] diff --git a/tests/hw/esp32/i2c-spi-oom-test.toit b/tests/hw/esp32/i2c-spi-oom-test.toit new file mode 100644 index 000000000..d736c717b --- /dev/null +++ b/tests/hw/esp32/i2c-spi-oom-test.toit @@ -0,0 +1,124 @@ +// Copyright (C) 2026 Toit contributors. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +import expect show * +import i2c +import spi +import system + +import .test +import .variants + +expect-oom [block]: + exception := catch block + expect + exception == "ALLOCATION_FAILED" + or exception == "MALLOC_FAILED" + or exception == "OUT_OF_MEMORY" + +main: + run-test: test + +test: + variant := Variant.CURRENT + data := variant.unconnected-pin1 + clock := variant.unconnected-pin2 + select := variant.unconnected-pin3 + + // These deliberately impossible native allocations fail on every primitive + // attempt. If an attempt retains pins, a peripheral, or another native + // resource, the following retry reports ALREADY_IN_USE instead of OOM. + expect-oom: + i2c.Target + --sda=data + --scl=clock + --address=0x42 + --send-buffer-size=(1 << 30) + --receive-buffer-size=64 + target := i2c.Target + --sda=data + --scl=clock + --address=0x42 + target.close + + // Register targets are not supported on the original ESP32. + if system.architecture != system.ARCHITECTURE-ESP32: + expect-oom: + i2c.RegisterTarget + --sda=data + --scl=clock + --address=0x42 + --receive-buffer-size=(1 << 30) + register-target := i2c.RegisterTarget + --sda=data + --scl=clock + --address=0x42 + register-target.close + + expect-oom: + spi.Target + --mosi=data + --clock=clock + --cs=select + --max-transfer-size=(1 << 30) + spi-target := spi.Target + --mosi=data + --clock=clock + --cs=select + --max-transfer-size=64 + spi-target.close + + expect-oom: + spi.BufferTarget + --mosi=data + --clock=clock + --cs=select + --buffer-size=4 + --receive-queue-depth=(1 << 29) + buffer-target := spi.BufferTarget + --mosi=data + --clock=clock + --cs=select + --buffer-size=4 + --receive-queue-depth=2 + buffer-target.close + + test-i2c-controller-oom data clock + system.process-stats --gc + test-spi-controller-oom data clock select + +native-failure-size -> int: + // The source is stored in PSRAM on ESP32-S3, while peripheral transfers + // require internal DMA memory. Classic ESP32 has enough memory for the + // source, but not for a second allocation of the same size. + if system.architecture == system.ARCHITECTURE-ESP32: return 70_000 + return 1_000_000 + +test-i2c-controller-oom sda/int scl/int: + bus := i2c.Bus --sda=sda --scl=scl + device := bus.device 0x42 + buffer := ByteArray native-failure-size + expect-oom: device.read-into buffer + device.close + bus.close + + // Prove that both the driver handle and pins remain reusable. + retry := i2c.Bus --sda=sda --scl=scl + retry-device := retry.device 0x42 + retry-device.close + retry.close + +test-spi-controller-oom mosi/int clock/int cs/int: + bus := spi.Bus --mosi=mosi --clock=clock + device := bus.device --cs=cs --frequency=100_000 + buffer := ByteArray native-failure-size + expect-oom: device.write buffer + device.close + bus.close + + // Prove that both the driver handle and pins remain reusable. + retry := spi.Bus --mosi=mosi --clock=clock + retry-device := retry.device --cs=cs --frequency=100_000 + retry-device.close + retry.close diff --git a/tests/hw/esp32/spi-buffer-target-shared.toit b/tests/hw/esp32/spi-buffer-target-shared.toit index 85f62bfcd..3d898c53d 100644 --- a/tests/hw/esp32/spi-buffer-target-shared.toit +++ b/tests/hw/esp32/spi-buffer-target-shared.toit @@ -36,6 +36,8 @@ SYNC ::= 0xb3 FLAG-USE-MOSI ::= 1 << 0 FLAG-USE-MISO ::= 1 << 1 FLAG-DMA ::= 1 << 2 +FLAG-TRANSMIT-LSB-FIRST ::= 1 << 3 +FLAG-RECEIVE-LSB-FIRST ::= 1 << 4 main-board1: run-test: test-board1 @@ -82,9 +84,46 @@ test-board1: expect-equals #[1, 0x81, 3, 0x91, 0x92, 0x93, 0x94, 0xa5, 0xa5] received expect-equals sent (take-received port) - print "SPI buffer target: receive queue overflow" + print "SPI buffer target: modes and bit order" close-target port device.close + [ + [0, false, false], + [1, false, false], + [2, false, false], + [3, false, false], + [0, true, false], + [0, false, true], + [0, true, true], + ].do: | current/List | + mode/int := current[0] + transmit-lsb-first/bool := current[1] + receive-lsb-first/bool := current[2] + print "SPI buffer target: mode=$mode tx-lsb=$transmit-lsb-first rx-lsb=$receive-lsb-first" + response := pattern 16 (0x15 + mode) + sent = pattern 16 (0x91 + mode) + create-target port + --mode=mode + --buffer-size=16 + --queue-depth=2 + --fill-byte=0xff + --initial=response + --use-mosi + --use-miso + --dma=false + --transmit-lsb-first=transmit-lsb-first + --receive-lsb-first=receive-lsb-first + device = create-device bus --mode=mode --frequency=1_000_000 + expected-response := response.copy + if transmit-lsb-first: reverse-bits-in-place expected-response + expect-equals expected-response (transfer device sent) + expected-receive := sent.copy + if receive-lsb-first: reverse-bits-in-place expected-receive + expect-equals expected-receive (take-received port) + close-target port + device.close + + print "SPI buffer target: receive queue overflow" create-target port --mode=0 --buffer-size=16 @@ -95,12 +134,15 @@ test-board1: --use-miso --dma=false device = create-device bus --mode=0 --frequency=2_000_000 - 5.repeat: | index/int | - data := pattern 16 (0x40 + index) - expect-equals (filled 16 0x5a) (transfer device data) - expect-equals 2 (dropped port) - 3.repeat: | index/int | - expect-equals (pattern 16 (0x40 + index)) (take-received port) + 3.repeat: | round/int | + 5.repeat: | index/int | + data := pattern 16 (0x40 + round * 8 + index) + expect-equals (filled 16 0x5a) (transfer device data) + expect-equals (round + 1) * 2 (dropped port) + 3.repeat: | index/int | + expect-equals + (pattern 16 (0x40 + round * 8 + index)) + (take-received port) print "SPI buffer target: receive wait and repeated re-arm" port.out.write #[RECEIVE] --flush @@ -225,48 +267,49 @@ test-board1: close-target port device.close - [false, true].do: | dma/bool | - suffix := dma ? "dma" : "no-dma" - print "SPI buffer target: active close-$suffix" - create-target port - --mode=0 - --buffer-size=64 - --queue-depth=2 - --fill-byte=0xff - --initial=#[ ] - --use-mosi - --use-miso=false - --dma=dma - device = create-device bus - --mode=0 - --frequency=(dma ? 400_000 : 100_000) - device.with-reserved-bus: - device.transfer #[0x5a] --keep-cs-active - // Closing must not wait for CS to be released by the controller. + 3.repeat: + [false, true].do: | dma/bool | + suffix := dma ? "dma" : "no-dma" + print "SPI buffer target: active close-$suffix" + create-target port + --mode=0 + --buffer-size=64 + --queue-depth=2 + --fill-byte=0xff + --initial=#[ ] + --use-mosi + --use-miso=false + --dma=dma + device = create-device bus + --mode=0 + --frequency=(dma ? 400_000 : 100_000) + device.with-reserved-bus: + device.transfer #[0x5a] --keep-cs-active + // Closing must not wait for CS to be released by the controller. + close-target port + // The target has disconnected its CS input, so this only terminates the + // controller-side reservation. + device.transfer #[0] + device.close + + // Closing must also release the peripheral and pins for a new target. + create-target port + --mode=0 + --buffer-size=8 + --queue-depth=2 + --fill-byte=0xff + --initial=#[ ] + --use-mosi + --use-miso=false + --dma=dma + device = create-device bus + --mode=0 + --frequency=(dma ? 400_000 : 100_000) + recovery := pattern 8 (dma ? 0xd1 : 0x1d) + transfer device recovery + expect-equals recovery (take-received port) close-target port - // The target has disconnected its CS input, so this only terminates the - // controller-side reservation. - device.transfer #[0] - device.close - - // Closing must also release the peripheral and pins for a new target. - create-target port - --mode=0 - --buffer-size=8 - --queue-depth=2 - --fill-byte=0xff - --initial=#[ ] - --use-mosi - --use-miso=false - --dma=dma - device = create-device bus - --mode=0 - --frequency=(dma ? 400_000 : 100_000) - recovery := pattern 8 (dma ? 0xd1 : 0x1d) - transfer device recovery - expect-equals recovery (take-received port) - close-target port - device.close + device.close bus.close port.close @@ -306,6 +349,8 @@ test-board2: --buffer-size=buffer-size --receive-queue-depth=queue-depth --fill-byte=fill-byte + --transmit-lsb-first=((flags & FLAG-TRANSMIT-LSB-FIRST) != 0) + --receive-lsb-first=((flags & FLAG-RECEIVE-LSB-FIRST) != 0) --dma=(flags & FLAG-DMA) != 0 port.out.write #[READY] --flush else if command == WRITE: @@ -352,11 +397,15 @@ create-target port/uart.Port --initial/ByteArray --use-mosi/bool --use-miso/bool - --dma/bool: + --dma/bool + --transmit-lsb-first/bool=false + --receive-lsb-first/bool=false: flags := 0 if use-mosi: flags |= FLAG-USE-MOSI if use-miso: flags |= FLAG-USE-MISO if dma: flags |= FLAG-DMA + if transmit-lsb-first: flags |= FLAG-TRANSMIT-LSB-FIRST + if receive-lsb-first: flags |= FLAG-RECEIVE-LSB-FIRST port.out.write #[CREATE, mode, flags, fill-byte] port.out.little-endian.write-uint32 buffer-size port.out.little-endian.write-uint32 queue-depth @@ -424,3 +473,12 @@ pattern size/int seed/int -> ByteArray: size.repeat: | index/int | result[index] = (seed + index * 37) & 0xff return result + +reverse-bits-in-place bytes/ByteArray -> none: + bytes.size.repeat: | index/int | + value := bytes[index] + reversed := 0 + 8.repeat: + reversed = (reversed << 1) | (value & 1) + value >>= 1 + bytes[index] = reversed diff --git a/tests/hw/esp32/spi-cs-timing-board1.toit b/tests/hw/esp32/spi-cs-timing-board1.toit new file mode 100644 index 000000000..0baaddba8 --- /dev/null +++ b/tests/hw/esp32/spi-cs-timing-board1.toit @@ -0,0 +1,8 @@ +// Copyright (C) 2026 Toit contributors. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +import .spi-cs-timing-shared + +main: + main-board1 diff --git a/tests/hw/esp32/spi-cs-timing-board2.toit b/tests/hw/esp32/spi-cs-timing-board2.toit new file mode 100644 index 000000000..85f8d498b --- /dev/null +++ b/tests/hw/esp32/spi-cs-timing-board2.toit @@ -0,0 +1,8 @@ +// Copyright (C) 2026 Toit contributors. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +import .spi-cs-timing-shared + +main: + main-board2 diff --git a/tests/hw/esp32/spi-cs-timing-shared.toit b/tests/hw/esp32/spi-cs-timing-shared.toit new file mode 100644 index 000000000..91bc68f65 --- /dev/null +++ b/tests/hw/esp32/spi-cs-timing-shared.toit @@ -0,0 +1,135 @@ +// Copyright (C) 2026 Toit contributors. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +import expect show * +import rmt +import spi +import system +import uart + +import .test +import .variants + +RX1 ::= Variant.CURRENT.board-connection-pin1 +TX1 ::= Variant.CURRENT.board-connection-pin2 +RX2 ::= Variant.CURRENT.board-connection-pin2 +TX2 ::= Variant.CURRENT.board-connection-pin1 + +CS ::= Variant.CURRENT.board-connection-pin3 +SCLK ::= Variant.CURRENT.board-connection-pin4 +MOSI ::= Variant.CURRENT.board-connection-pin5 + +READY ::= 0xa1 +DONE ::= 0xa2 +CLOSE ::= 0xa3 + +FREQUENCY ::= 100_000 +CYCLE-US ::= 1_000_000 / FREQUENCY +TRANSFER ::= #[0x12, 0x34, 0x56, 0x78] + +main-board1: + run-test: test-board1 + +test-board1: + port := uart.Port --rx=RX1 --tx=TX1 --baud-rate=115_200 + expect-equals READY port.in.read-byte + bus := spi.Bus --clock=SCLK --mosi=MOSI + + baseline := measure port bus 0 0 + print "SPI CS timing: setup=0 hold=0 low=$(baseline)us" + + 16.repeat: | index/int | + check-timing port bus baseline index + 1 0 + + hold-max := system.architecture == system.ARCHITECTURE-ESP32 ? 15 : 16 + hold-max.repeat: | index/int | + check-timing port bus baseline 0 index + 1 + + if system.architecture == system.ARCHITECTURE-ESP32: + expect-throw "OUT_OF_RANGE": + bus.device + --cs=CS + --frequency=FREQUENCY + --cs-hold-cycles=16 + + port.out.write #[CLOSE] --flush + expect-equals DONE port.in.read-byte + bus.close + port.close + +check-timing port/uart.Port bus/spi.Bus baseline/int setup/int hold/int -> none: + measured := measure port bus setup hold + print "SPI CS timing: setup=$setup hold=$hold low=$(measured)us" + + // ESP-IDF maps a requested hold of zero to one cycle because the hardware + // needs that minimum. All other setup/hold values contribute one full SPI + // clock cycle each. + effective-hold := max 1 hold + expected-delta := (setup + effective-hold - 1) * CYCLE-US + expect (measured - baseline - expected-delta).abs <= 3 + +measure port/uart.Port bus/spi.Bus setup/int hold/int -> int: + port.out.write #[setup, hold] --flush + expect-equals READY port.in.read-byte + device := bus.device + --cs=CS + --frequency=FREQUENCY + --cs-setup-cycles=setup + --cs-hold-cycles=hold + device.write TRANSFER + device.close + expect-equals DONE port.in.read-byte + return port.in.little-endian.read-uint32 + +main-board2: + run-test --background: test-board2 + +test-board2: + port := uart.Port --rx=RX2 --tx=TX2 --baud-rate=115_200 + port.out.write #[READY] --flush + + while true: + setup := port.in.read-byte + if setup == CLOSE: + port.out.write #[DONE] --flush + port.close + return + hold := port.in.read-byte + + cs-probe := rmt.In CS --resolution=1_000_000 --memory-blocks=2 + clock-probe := rmt.In SCLK --resolution=1_000_000 --memory-blocks=2 + try: + cs-probe.start-reading --max-ns=10_000_000 + clock-probe.start-reading --max-ns=10_000_000 + port.out.write #[READY] --flush + + cs-signals := cs-probe.wait-for-data + clock-signals := clock-probe.wait-for-data + cs-low-us := longest-signal-us cs-signals 0 + + high-count := 0 + clock-signals.size.repeat: | i/int | + duration-us := (clock-signals.ns-duration i) / 1_000 + if (clock-signals.level i) == 1 and duration-us != 0: + if 3 <= duration-us <= 7: + high-count++ + else: + print "Ignoring non-clock SCLK-high signal: $(duration-us)us" + expect-equals TRANSFER.size * 8 high-count + expect cs-low-us >= TRANSFER.size * 8 * CYCLE-US + expect cs-low-us < 1_000 + + port.out.write #[DONE] --flush + port.out.little-endian.write-uint32 cs-low-us + port.out.flush + finally: + cs-probe.close + clock-probe.close + +longest-signal-us signals/rmt.Signals level/int -> int: + result := 0 + signals.size.repeat: | i/int | + if (signals.level i) == level: + result = max result ((signals.ns-duration i) / 1_000) + return result diff --git a/tests/hw/esp32/spi-target-shared.toit b/tests/hw/esp32/spi-target-shared.toit index e3dcb4854..00cba29e1 100644 --- a/tests/hw/esp32/spi-target-shared.toit +++ b/tests/hw/esp32/spi-target-shared.toit @@ -3,6 +3,7 @@ // be found in the tests/LICENSE file. import expect show * +import gpio import io import monitor import spi @@ -27,6 +28,7 @@ ABORT-IDLE ::= 0x32 ABORT-ACTIVE ::= 0x33 RESUME ::= 0x34 CLOSE-ACTIVE ::= 0x35 +NONBYTE-TERMINATION ::= 0x36 SYNC ::= 0xa0 READY ::= 0xa1 DONE ::= 0xa2 @@ -80,6 +82,13 @@ test-board1: cases := [] 4.repeat: | mode/int | + cases.add (Case "mode-$(mode)-full-duplex-no-dma" + --frequency=400_000 + --mode=mode + --transmit=(pattern 16 (0x40 + mode)) + --controller-data=(pattern 16 (0x10 + mode)) + --max-transfer-size=64 + --dma=false) [false, true].do: | dma/bool | suffix := dma ? "dma" : "no-dma" if not dma or (mode & 1) == 0: @@ -253,10 +262,11 @@ test-board1: if current.transmit-lsb-first: reverse-bits-in-place expected-controller expect-equals expected-controller controller-result - [false, true].do: | dma/bool | - test-abort port bus ABORT-IDLE dma - test-abort port bus ABORT-ACTIVE dma - test-close-active port bus dma + 3.repeat: + [false, true].do: | dma/bool | + test-abort port bus ABORT-IDLE dma + test-abort port bus ABORT-ACTIVE dma + test-close-active port bus dma // Exercise the controller bus's device ownership and bounded native slots. // The devices deliberately omit CS because no transfers are performed. @@ -270,6 +280,12 @@ test-board1: devices.do: | device/spi.Device | expect-throws "CLOSED": device.write #[0] expect-throws "CLOSED": replacement.write #[0] + + // ESP-IDF's SPI controller API can only issue whole bytes. Drive mode-0 + // clocks directly to verify target completion and reuse when CS rises in + // the middle of a byte. + test-nonbyte-termination port + if is-classic-esp32: [1, 3].do: | mode/int | expect-throws "INVALID_ARGUMENT": @@ -303,6 +319,9 @@ test-board2: if command == CLOSE-ACTIVE: test-close-active-target port continue + if command == NONBYTE-TERMINATION: + test-nonbyte-target port + continue if command != PREPARE: throw "Unknown command: $command" mode := port.in.read-byte @@ -471,6 +490,73 @@ test-close-active-target port/uart.Port -> none: port.out.write result --flush target.close +test-nonbyte-termination port/uart.Port -> none: + print "SPI target: non-byte-aligned CS termination" + cs := gpio.Pin CS --output --value=1 + clock := gpio.Pin SCLK --output --value=0 + mosi := gpio.Pin MOSI --output --value=0 + try: + // Avoid counts congruent to one modulo eight. ESP32-S3's hardware exposes + // an N-1 counter for early termination, making those cases byte-ambiguous + // (one clock is indistinguishable from no clocks at all). + [2, 7, 10, 15, 31].do: | bit-count/int | + port.out.write #[NONBYTE-TERMINATION, bit-count] --flush + expect-equals READY port.in.read-byte + bitbang-mode0 cs clock mosi bit-count + expect-equals (bit-count + 7) / 8 (read-target-result port).size + + // Reuse the same target immediately for a complete two-byte exchange. + port.out.write #[RESUME] --flush + expect-equals READY port.in.read-byte + bitbang-mode0 cs clock mosi 16 + expect-equals 2 (read-target-result port).size + finally: + mosi.close + clock.close + cs.close + +test-nonbyte-target port/uart.Port -> none: + bit-count := port.in.read-byte + target := spi.Target + --mosi=MOSI + --clock=SCLK + --cs=CS + --max-transfer-size=8 + --dma=false + + result := target.transfer #[ ] + --receive-size=8 + --when-armed=: + port.out.write #[READY] --flush + port.out.write #[DONE] --flush + port.out.little-endian.write-uint32 result.size + port.out.write result --flush + expect-equals (bit-count + 7) / 8 result.size + + expect-equals RESUME port.in.read-byte + result = target.transfer #[ ] + --receive-size=8 + --when-armed=: + port.out.write #[READY] --flush + port.out.write #[DONE] --flush + port.out.little-endian.write-uint32 result.size + port.out.write result --flush + expect-equals 2 result.size + target.close + +bitbang-mode0 cs/gpio.Pin clock/gpio.Pin mosi/gpio.Pin bit-count/int -> none: + cs.set 0 + sleep --ms=1 + bit-count.repeat: | index/int | + mosi.set (index & 1) + sleep --ms=1 + clock.set 1 + sleep --ms=1 + clock.set 0 + sleep --ms=1 + sleep --ms=1 + cs.set 1 + prepare-target port/uart.Port current/Case: flags := 0 if current.transmit-lsb-first: flags |= FLAG-TRANSMIT-LSB-FIRST diff --git a/toolchains/esp32/sdkconfig b/toolchains/esp32/sdkconfig index ca03ca9a3..7f15ca670 100644 --- a/toolchains/esp32/sdkconfig +++ b/toolchains/esp32/sdkconfig @@ -2160,6 +2160,9 @@ CONFIG_TOIT_ENABLE_IP=y CONFIG_TOIT_ENABLE_ETHERNET=y CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y +CONFIG_TOIT_ENABLE_I2C_TARGET=y +CONFIG_TOIT_ENABLE_SPI_TARGET=y +CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" CONFIG_RODATA_PADDING=2097152 diff --git a/toolchains/esp32/sdkconfig.defaults b/toolchains/esp32/sdkconfig.defaults index 3eee73be2..96837294c 100644 --- a/toolchains/esp32/sdkconfig.defaults +++ b/toolchains/esp32/sdkconfig.defaults @@ -29,7 +29,6 @@ CONFIG_BTDM_BLE_SCAN_DUPL=n CONFIG_BTDM_BLE_ADV_REPORT_FLOW_CTRL_NUM=50 CONFIG_ADC_CAL_EFUSE_TP_ENABLE=n CONFIG_ADC_CAL_LUT_ENABLE=n -CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y CONFIG_I2S_ISR_IRAM_SAFE=y CONFIG_SPI_MASTER_ISR_IN_IRAM=y CONFIG_SPI_SLAVE_ISR_IN_IRAM=n diff --git a/toolchains/esp32c3/sdkconfig b/toolchains/esp32c3/sdkconfig index f97ddf3d3..93553004e 100644 --- a/toolchains/esp32c3/sdkconfig +++ b/toolchains/esp32c3/sdkconfig @@ -2190,6 +2190,9 @@ CONFIG_TOIT_ENABLE_IP=y CONFIG_TOIT_ENABLE_ETHERNET=y CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y +CONFIG_TOIT_ENABLE_I2C_TARGET=y +CONFIG_TOIT_ENABLE_SPI_TARGET=y +CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" CONFIG_RODATA_PADDING=2097152 diff --git a/toolchains/esp32c3/sdkconfig.defaults b/toolchains/esp32c3/sdkconfig.defaults index 13b5bb5c0..54b0a6e31 100644 --- a/toolchains/esp32c3/sdkconfig.defaults +++ b/toolchains/esp32c3/sdkconfig.defaults @@ -23,7 +23,6 @@ CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="toit" CONFIG_BT_NIMBLE_SVC_GAP_APPEARANCE=0x0 CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=10 CONFIG_BT_CTRL_BLE_MAX_ACT=10 -CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y CONFIG_I2S_ISR_IRAM_SAFE=y CONFIG_SPI_SLAVE_ISR_IN_IRAM=y CONFIG_UART_ISR_IN_IRAM=y diff --git a/toolchains/esp32c6/sdkconfig b/toolchains/esp32c6/sdkconfig index c34104da0..0690c1e35 100644 --- a/toolchains/esp32c6/sdkconfig +++ b/toolchains/esp32c6/sdkconfig @@ -2368,6 +2368,9 @@ CONFIG_TOIT_ENABLE_IP=y CONFIG_TOIT_ENABLE_ETHERNET=y CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y +CONFIG_TOIT_ENABLE_I2C_TARGET=y +CONFIG_TOIT_ENABLE_SPI_TARGET=y +CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" CONFIG_RODATA_PADDING=2097152 diff --git a/toolchains/esp32c6/sdkconfig.defaults b/toolchains/esp32c6/sdkconfig.defaults index a3de65f38..a13e19d68 100644 --- a/toolchains/esp32c6/sdkconfig.defaults +++ b/toolchains/esp32c6/sdkconfig.defaults @@ -23,7 +23,6 @@ CONFIG_BT_NIMBLE_NVS_PERSIST=y CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="toit" CONFIG_BT_NIMBLE_SVC_GAP_APPEARANCE=0x0 CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=10 -CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y CONFIG_I2S_ISR_IRAM_SAFE=y CONFIG_SPI_SLAVE_ISR_IN_IRAM=y CONFIG_UART_ISR_IN_IRAM=y diff --git a/toolchains/esp32p4/sdkconfig b/toolchains/esp32p4/sdkconfig index 8c3b21e90..e6f6cddb7 100644 --- a/toolchains/esp32p4/sdkconfig +++ b/toolchains/esp32p4/sdkconfig @@ -2265,6 +2265,9 @@ CONFIG_TOIT_ENABLE_IP=y CONFIG_TOIT_ENABLE_ETHERNET=y CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y +CONFIG_TOIT_ENABLE_I2C_TARGET=y +CONFIG_TOIT_ENABLE_SPI_TARGET=y +CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET=y CONFIG_TOIT_SPIRAM_HEAP=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" diff --git a/toolchains/esp32p4/sdkconfig.defaults b/toolchains/esp32p4/sdkconfig.defaults index 15e84d50d..eb94e1c6b 100644 --- a/toolchains/esp32p4/sdkconfig.defaults +++ b/toolchains/esp32p4/sdkconfig.defaults @@ -11,7 +11,6 @@ CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_COMPILER_OPTIMIZATION_SIZE=y CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE=y CONFIG_COMPILER_ORPHAN_SECTIONS_PLACE=y -CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y CONFIG_I2S_ISR_IRAM_SAFE=y CONFIG_SPI_SLAVE_ISR_IN_IRAM=y CONFIG_UART_ISR_IN_IRAM=y diff --git a/toolchains/esp32s2/sdkconfig b/toolchains/esp32s2/sdkconfig index 5a9f60813..067be08b1 100644 --- a/toolchains/esp32s2/sdkconfig +++ b/toolchains/esp32s2/sdkconfig @@ -2034,6 +2034,9 @@ CONFIG_TOIT_ENABLE_IP=y CONFIG_TOIT_ENABLE_ETHERNET=y CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y +CONFIG_TOIT_ENABLE_I2C_TARGET=y +CONFIG_TOIT_ENABLE_SPI_TARGET=y +CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET=y # CONFIG_TOIT_SPIRAM_HEAP is not set CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" diff --git a/toolchains/esp32s2/sdkconfig.defaults b/toolchains/esp32s2/sdkconfig.defaults index 354e55502..5e07686e5 100644 --- a/toolchains/esp32s2/sdkconfig.defaults +++ b/toolchains/esp32s2/sdkconfig.defaults @@ -11,7 +11,6 @@ CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_COMPILER_OPTIMIZATION_SIZE=y CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE=y CONFIG_COMPILER_ORPHAN_SECTIONS_PLACE=y -CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y CONFIG_I2S_ISR_IRAM_SAFE=y CONFIG_SPI_SLAVE_ISR_IN_IRAM=y CONFIG_UART_ISR_IN_IRAM=y diff --git a/toolchains/esp32s3/sdkconfig b/toolchains/esp32s3/sdkconfig index 373720333..d3d6ef92c 100644 --- a/toolchains/esp32s3/sdkconfig +++ b/toolchains/esp32s3/sdkconfig @@ -2465,6 +2465,9 @@ CONFIG_TOIT_ENABLE_IP=y CONFIG_TOIT_ENABLE_ETHERNET=y CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y +CONFIG_TOIT_ENABLE_I2C_TARGET=y +CONFIG_TOIT_ENABLE_SPI_TARGET=y +CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET=y CONFIG_TOIT_SPIRAM_HEAP=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" diff --git a/toolchains/esp32s3/sdkconfig.defaults b/toolchains/esp32s3/sdkconfig.defaults index 0b5dd7a62..28fa730eb 100644 --- a/toolchains/esp32s3/sdkconfig.defaults +++ b/toolchains/esp32s3/sdkconfig.defaults @@ -24,7 +24,6 @@ CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="toit" CONFIG_BT_NIMBLE_SVC_GAP_APPEARANCE=0x0 CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=10 CONFIG_BT_CTRL_BLE_MAX_ACT=10 -CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y CONFIG_I2S_ISR_IRAM_SAFE=y CONFIG_SPI_SLAVE_ISR_IN_IRAM=y CONFIG_UART_ISR_IN_IRAM=y diff --git a/toolchains/idf/components/toit/CMakeLists.txt b/toolchains/idf/components/toit/CMakeLists.txt index 9339a19e2..c82e8c4e2 100644 --- a/toolchains/idf/components/toit/CMakeLists.txt +++ b/toolchains/idf/components/toit/CMakeLists.txt @@ -2,9 +2,16 @@ cmake_minimum_required(VERSION 3.11) set(TOIT_BASE_DIR ${COMPONENT_PATH}/../../../..) +set(TOIT_COMPONENT_REQUIRES + "efuse" "esp_adc" "esp_eth" "esp_hw_support" "esp_netif" "esp_rom" + "nvs_flash" "spi_flash" "mbedtls" "bt" "app_update" "ulp" "fatfs") +if(CONFIG_TOIT_ENABLE_SPI_TARGET AND CONFIG_SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE) + list(APPEND TOIT_COMPONENT_REQUIRES "esp_mm") +endif() + # Register without sources, this will create an INTERFACE lib and we can then specify link options later idf_component_register(INCLUDE_DIRS "${TOIT_BASE_DIR}/src" "${TOIT_BASE_DIR}/include" - REQUIRES "efuse" "esp_adc" "esp_eth" "esp_hw_support" "esp_mm" "esp_netif" "esp_rom" "nvs_flash" "spi_flash" "mbedtls" "bt" "app_update" "ulp" "fatfs") + REQUIRES ${TOIT_COMPONENT_REQUIRES}) cmake_policy(SET CMP0079 NEW) @@ -24,6 +31,9 @@ add_subdirectory(${TOIT_BASE_DIR} toit EXCLUDE_FROM_ALL) # Add the include directories from the required components to the imported toit_vm target. target_include_directories(toit_vm PRIVATE "$") +if(CONFIG_TOIT_ENABLE_SPI_TARGET AND CONFIG_SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE) + target_include_directories(toit_vm PRIVATE "$") +endif() # idf cmake does not honor the CMAKE_BUILD_TARGET, so we can not use the CMAKE_C(XX)?_FLAGS. # Add the required flags and defines with target_compile_* commands. diff --git a/toolchains/idf/components/toit/Kconfig b/toolchains/idf/components/toit/Kconfig index 6e57c084a..3b0e8dfe6 100644 --- a/toolchains/idf/components/toit/Kconfig +++ b/toolchains/idf/components/toit/Kconfig @@ -214,6 +214,36 @@ menu "Toit" help ESP-Now support in the Toit binary. + config TOIT_ENABLE_I2C_TARGET + boolean "Enable I2C target support on ESP32" + default "y" + depends on SOC_I2C_SUPPORT_SLAVE + select I2C_ENABLE_SLAVE_DRIVER_VERSION_2 + help + I2C target and register-target support in the Toit binary. + + Disable this option to reduce firmware size when only I2C + controller functionality is needed. + + config TOIT_ENABLE_SPI_TARGET + boolean "Enable SPI target support on ESP32" + default "y" + help + SPI target support in the Toit binary. + + Disable this option to reduce firmware size when only SPI + controller functionality is needed. + + config TOIT_ENABLE_SPI_BUFFER_TARGET + boolean "Enable buffered SPI target support on ESP32" + default "y" + depends on TOIT_ENABLE_SPI_TARGET + help + Autonomous buffered SPI target support in the Toit binary. + + Disable this option to reduce firmware size while retaining + the transaction-at-a-time SPI target. + config TOIT_SPIRAM_HEAP boolean "Enable Toit heap in SPIRAM" default "y"