Hi opcode_x64,
Two days of head bashing, and I got that working. The esp-idf High Level Interrupt documentation needs to be updated, as we now need to add something to CMakeLists.txt to get it to link in the assembly (replace the last word with whatever you put at the bottom of the .S file:
Code: Select all
target_link_libraries(${COMPONENT_LIB} INTERFACE "-u ld_include_highint_hdl")
Beyond that, I've gone a much simpler route than you were thinking. I'll document it here for my future sanity.
I'm talking with a TI ADS8887 SAR ADC, which needs a >500ns conversion start pulse in, >12ns delay, then an SPI transaction to read in the data. I need to synchronise between multiple ADCs, so using SAR's seemed the easiest.
I use an external GPIO as reference clock input, using the rising edge to start an outgoing pulse with assembly. The assembly code also starts an SPI transaction on the the SPI peripheral. When the SPI peripheral is done, it then fires an interrupt to my own handler, which does all of the housekeeping, and passes the data in chunks to another task to be transmitted over TCP.
1) Set up the SPI peripheral as normal for interrupt handling. Run one SPI transaction just to confirm everything works. I run this from CPU1 using "xTaskCreatePinnedToCore()"
Code: Select all
static void my_spi_init(void) {
spi_bus_config_t buscfg = {
...
};
spi_device_interface_config_t devcfg = {
...
};
ESP_ERROR_CHECK( spi_bus_initialize(SPI2_HOST, &buscfg, 1) );
ESP_ERROR_CHECK( spi_bus_add_device(SPI2_HOST, &devcfg, &spi_dev) );
ESP_ERROR_CHECK( spi_device_acquire_bus(spi_dev, portMAX_DELAY) );
// do one transmit first to ensure that the settings are correct
ESP_ERROR_CHECK( spi_device_queue_trans(spi_dev, &spi_trans, portMAX_DELAY ) );
ESP_ERROR_CHECK( spi_device_get_trans_result(spi_dev, &spi_trans, portMAX_DELAY ) );
// from here down is unusual
vTaskDelay(1);
intr_handle_t *spi_int = spi_bus_get_intr(SPI2_HOST);
ESP_ERROR_CHECK( esp_intr_free(spi_int) );
ESP_ERROR_CHECK( esp_intr_alloc(ETS_SPI2_INTR_SOURCE, ESP_INTR_FLAG_IRAM|ESP_INTR_FLAG_LEVEL3, spi2_handler, NULL, &spi2_intr_handle) );
ESP_ERROR_CHECK( esp_intr_enable(spi2_intr_handle) );
vTaskDelete(NULL);
}
2) You'll note the above reaches into the ESP SPI stack, disables the interrupt to their SPI handler "spi_intr()", and then connects up with my own SPI handler.
I added a helper function to esp-idf's spi-master.c in order to get access to the relevant intr_handle_t:
Code: Select all
intr_handle_t spi_bus_get_intr(spi_host_device_t host) {
return spihost[host]->intr;
}
I'm not sure if there's a better way to do that.
I then manually enable the SPI interrupt to my handler:
Code: Select all
static void IRAM_ATTR spi2_handler(void *arg) {
SPI2.slave.trans_done = 0; // reset the register
uint32_t data = SPI_SWAP_DATA_RX(*(SPI2.data_buf), 18);
... do something with the data ...
}
3) Input sampling clock to ESP32 as a GPIO input. Set up a level 5 interrupt, and use assembly code to generate output pulse, and start SPI transaction:
Code: Select all
#include <xtensa/coreasm.h>
#include <xtensa/corebits.h>
#include <xtensa/config/system.h>
#include "freertos/xtensa_context.h"
#include "esp_debug_helpers.h"
#include "esp_private/panic_reason.h"
#include "sdkconfig.h"
#include "soc/soc.h"
#include "soc/dport_reg.h"
#define L5_INTR_STACK_SIZE 12
#define L5_INTR_A2_OFFSET 0
#define L5_INTR_A3_OFFSET 4
#define L5_INTR_A4_OFFSET 8
.data
_l5_intr_stack:
.space L5_INTR_STACK_SIZE
.section .iram1,"ax"
.global xt_highint5
.type xt_highint5,@function
.align 4
.literal .GPIO_STATUS1_W1TC_REG, 0x3FF44058
.literal .GPIO_STATUS1_REG, 0x3FF44050
.literal .GPIO_OUT_W1TS_REG, 0x3FF44008
.literal .GPIO_OUT_W1TC_REG, 0x3FF4400C
.literal .GPIO__NUM_33, (1<<1)
.literal .GPIO__NUM_2, (1<<2)
.literal .SPI_CMD_REG, 0x3FF64000
.literal .SPI_USR, (1<<18)
xt_highint5:
/* save contents of registers A2-A4 */
movi a0, _l5_intr_stack
s32i a2, a0, L5_INTR_A2_OFFSET
s32i a3, a0, L5_INTR_A3_OFFSET
s32i a4, a0, L5_INTR_A4_OFFSET
/* clearing the interrupt status of GPIO_NUM_33 */
l32r a2, .GPIO_STATUS1_W1TC_REG
l32r a3, .GPIO__NUM_33
s32i a3, a2, 0
/* setting GPIO2 to high */
l32r a2, .GPIO_OUT_W1TS_REG
l32r a3, .GPIO__NUM_2
s32i a3, a2, 0
/* busy wait */
movi a4, 50
1:
addi a4, a4, -1
bnez a4, 1b
/* setting GPIO2 to low */
/* l32r a2, .GPIO_OUT_W1TC_REG */
/* l32r a3, .GPIO__NUM_2*/
s32i a3, a2, 4 /* GPIO_OUT_W1TC_REG = GPIO_OUT_W1TS_REG + 4 */
/* SPI2.cmd.usr = 1; */
/* The SPI peripheral takes a few hundred nanoseconds
to start, no need for extra delay */
l32r a2, .SPI_CMD_REG
l32r a3, .SPI_USR
s32i a3, a2, 0
/* This hack doesn't seem needed. Add if double interrupting occuring.
l32r a2, .GPIO_STATUS1_REG
l32i a2, a2, 0
memw*/
/* restore contents of registers A2-A4 */
movi a0, _l5_intr_stack
l32i a2, a0, L5_INTR_A2_OFFSET
l32i a3, a0, L5_INTR_A3_OFFSET
l32i a4, a0, L5_INTR_A4_OFFSET
rsync /* ensure register restored */
/* hand back from interrupt */
rsr a0, EXCSAVE_5
rfi 5
.global ld_include_highint_hdl
ld_include_highint_hdl:
With all that running, it works:
Yellow is input clock (30kHz), blue is output sample pulse, pink is SCLK (1MHz), blue is MISO.
There is a little jitter in the width of the assembly-code generated GPIO output pulse, but this isn't doesn't impact my application. I'm supposing this is caused by CPU0 halting CPU1 temporarily whilst accessing something. Not sure if this is something I can fix.
This is an excellent outcome, and I didn't need to resort to bit-banging SPI in assembly or anything too hideous
I've tested this to 200kHz with a higher frequency SCLK, and the limiting factor was the data transmission (TCP/IP and Ethernet code).