如何避免SD卡阻塞ESP32 SPI总线以保障ADC定时读取?
ESP32同一SPI总线实现ADC定时读取与SD卡抗卡顿共存方案
针对你在ESP-IDF开发中遇到的同一SPI总线共享ADC和SD卡、需保障ADC定时读取不受SD卡卡顿影响的问题,以下是几种可行的落地方案:
方案1:高优先级任务+SPI事务优先级调度
利用FreeRTOS的任务优先级机制,结合ESP-IDF SPI驱动的事务优先级配置,让ADC读取任务优先抢占总线资源:
任务优先级划分
- 将ADC定时读取任务设为高优先级(比如优先级10),SD卡读写任务设为低优先级(比如优先级5)。高优先级任务会随时抢占低优先级任务的CPU时间,确保ADC读取的定时性。
- 用
vTaskDelayUntil实现ADC的精准定时读取,避免累计误差。
SPI设备优先级配置
在添加SPI设备时,给ADC设备设置更高的事务优先级,确保其SPI请求优先被处理:// ADC设备配置 spi_device_interface_config_t adc_dev_cfg = { .mode = 0, // 匹配ADC的SPI模式 .clock_speed_hz = 8*1000*1000, // 兼容SD卡的时钟(需匹配两者硬件规格) .spics_io_num = GPIO_NUM_15, // ADC的独立CS引脚 .queue_size = 3, .priority = 1, // 事务优先级高于SD卡(SD卡设为0) }; spi_bus_add_device(SPI2_HOST, &adc_dev_cfg, &adc_spi_handle); // SD卡设备配置 spi_device_interface_config_t sd_dev_cfg = { .mode = 0, .clock_speed_hz = 8*1000*1000, .spics_io_num = GPIO_NUM_14, // SD卡的独立CS引脚 .queue_size = 5, .priority = 0, // 低优先级 }; spi_bus_add_device(SPI2_HOST, &sd_dev_cfg, &sd_spi_handle);任务实现示例
// ADC定时读取任务 void adc_read_task(void *arg) { TickType_t last_wake_time = xTaskGetTickCount(); const TickType_t read_interval = pdMS_TO_TICKS(10); // 每10ms读取一次 uint16_t adc_data; spi_transaction_t trans = { .length = 16, // ADC的传输位数(根据型号调整) .rx_buffer = &adc_data, }; while (1) { // 精准定时唤醒 vTaskDelayUntil(&last_wake_time, read_interval); // 执行ADC SPI读取(驱动自动处理总线抢占) esp_err_t ret = spi_device_transmit(adc_spi_handle, &trans); if (ret == ESP_OK) { // 数据上报 send_adc_data_to_server(adc_data); // 将数据发送到SD卡写入队列(非阻塞,避免卡主ADC任务) xQueueSend(sd_write_queue, &adc_data, 0); } } } // SD卡写入任务(低优先级) void sd_write_task(void *arg) { uint16_t data_buf; FILE *sd_file = fopen("/sdcard/adc_log.txt", "a"); if (!sd_file) return; while (1) { // 阻塞等待ADC数据 if (xQueueReceive(sd_write_queue, &data_buf, portMAX_DELAY)) { // 写入SD卡,即使卡顿也不影响ADC任务 fprintf(sd_file, "%d\n", data_buf); // 定期刷新缓存,避免大缓存导致的突发卡顿 fflush(sd_file); } } }
方案2:SPI DMA+硬件定时器触发的ADC读取
将ADC的SPI读取配置为DMA模式,结合硬件定时器触发,让读取过程不占用CPU时间,彻底规避SD卡任务的CPU占用影响:
配置ADC的SPI DMA传输
spi_device_interface_config_t adc_dev_cfg = { .mode = 0, .clock_speed_hz = 10*1000*1000, .spics_io_num = GPIO_NUM_15, .queue_size = 5, .flags = SPI_DEVICE_DMA_RX, // 启用RX DMA }; spi_bus_add_device(SPI2_HOST, &adc_dev_cfg, &adc_spi_handle);用硬件定时器触发ADC DMA读取
利用ESP32的硬件定时器,定时触发SPI DMA读取ADC,读取完成后通过中断回调处理数据上报和队列发送:void adc_dma_done_cb(spi_transaction_t *trans) { uint16_t *adc_data = (uint16_t*)trans->rx_buffer; // 数据上报 send_adc_data_to_server(*adc_data); // 发送到SD卡队列 xQueueSend(sd_write_queue, adc_data, 0); // 释放内存 free(trans->rx_buffer); free(trans); } void adc_timer_init(void) { timer_config_t config = { .divider = 80, // 1MHz计数时钟 .counter_dir = TIMER_COUNT_UP, .counter_en = TIMER_PAUSE, .alarm_en = TIMER_ALARM_EN, .auto_reload = true, }; timer_init(TIMER_GROUP_0, TIMER_0, &config); timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, 10000); // 10ms触发一次 timer_isr_callback_add(TIMER_GROUP_0, TIMER_0, []() { // 触发SPI DMA读取 spi_transaction_t *trans = malloc(sizeof(spi_transaction_t)); memset(trans, 0, sizeof(spi_transaction_t)); trans->length = 16; trans->rx_buffer = malloc(2); trans->user = adc_dma_done_cb; spi_device_queue_trans(adc_spi_handle, trans, 0); }, NULL, 0); timer_start(TIMER_GROUP_0, TIMER_0); }
关键注意事项
- 时钟兼容性:确保SPI总线时钟同时满足ADC和SD卡的规格要求,避免因时钟过高导致某一设备工作异常。
- SD卡卡顿缓解:SD卡的卡顿多来自大块擦写或缓存刷新,建议将SD卡写入拆分为小批量操作,定期调用
fflush或f_sync,避免单次操作占用总线过长时间。 - 总线互斥:ESP-IDF的SPI驱动已内置总线互斥逻辑,无需手动添加互斥锁,只需确保每个设备使用独立的CS引脚。
内容的提问来源于stack exchange,提问作者bbqribs




