输入输出与编程 / 显示与总线实践发布 内容更新

SPI 上挂两块设备:屏幕和存储卡为什么会互相干扰?

用同一块ST7789+microSD板先分别验收,再交替写卡、读回与刷屏100轮。解释两个片选、SD上电模式与DMA缓冲所有权,附完整IDF工程和空白记录。

读完能做什么
得到T/C/B三种独立入口的共享总线示例,按顺序完成100轮真实验收并保留时钟、日志与照片。
开始之前
已完成前置教程,能核对GPIO、串口、模块供电并复建固定框架示例。
所需器材
官方 ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,8MB Flash、Octal PSRAM;USB-to-UART 桥接串口115200。、Adafruit PID3787 1.54英寸240×240 ST7789+microSD,2022年11月EYESPI修订;排针已可靠焊接,散排针先由具备条件者焊好并检查、已备份的自有2–32GB FAT32 microSD;额定值按卡厂手册、两只10kΩ电阻、短线、面包板、USB数据线
版本组合
参考板:官方 ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,8MB Flash、Octal PSRAM;USB-to-UART 桥接串口115200。工具/框架:ESP-IDF v5.5.5 / 原生esp_lcd + SDSPI + FatFs
适用范围

官方 ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,8MB Flash、Octal PSRAM;USB-to-UART 桥接串口115200。SPI2_HOST,mode0,SD/TFT工作上限4MHz,未实卡/实屏测试。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/programming-spi.md#p08版本:ESP-IDF v5.5.5 / 原生esp_lcd + SDSPI + FatFs验证日期:
构建 / 配置已通过
docs/esp32-evidence/p08-build.log对象:esp32s3 / N8R8版本:ESP-IDF v5.5.5 / 原生esp_lcd + SDSPI + FatFs验证日期:
仿真已通过
docs/esp32-evidence/displays-host-checks.json#p08对象:同源纯逻辑host fixture;不模拟电气/控制器验证日期:
本站实板待验证
未取得实板;观测记录保持空白,未验证光学、总线或供电。

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 显示与总线实践

先让两块设备分别工作,再讨论共享

屏幕单独能画图、卡单独能存文件,并不能证明它们接到同一条SPI总线上仍正常。共用时钟与数据线以后,上电模式、片选、线负载,以及异步发送期间谁能重写内存,都成为新的条件。

本任务用PID3787板上的TFT和microSD作两块SPI设备,按T、C、B三种模式逐步排除变量。B每轮先写入一条有序记录、刷新并读回核对,再把当前轮次画到屏上,重复100次。没有开发板与卡的实际日志,就只能把这项硬件验收记为待测。

做到这里,你应该看到
你能说出:T只验证显示、C只验证卡,只有B的100轮记录对应共享场景。

参考:[1] ESP-IDF5.5.5:SD与其他SPI设备共享的初始化顺序与负载[2] ESP-IDF5.5.5:SDSPI完整例程[5] Adafruit PID3787:尺寸、控制器与EYESPI修订

02 / 显示与总线实践

一条线共享,两个片选各自空闲高

本例走排针接线,使用排针已可靠焊接的同修订PID3787。随附散排针不等于已焊好;未焊时先由具备工具和焊接条件的人完成并检查,不能将排针松插焊孔当作电气连接。EYESPI仍留空,不在本例切换接口。

使用2022年11月EYESPI修订的Adafruit PID3787:1.54英寸、240×240、ST7789、板载microSD。GPIO是ESP32芯片号,图内J1是官方v1.1排针序号;TE、EYESPI和3V稳压输出不用。3V3接VIN、GND共地,所有MCU信号3.3V。

TFT不回读,SO只承载SD卡的MISO。TC与CC各外加10kΩ上拉到3V3,并在SPI初始化前由GPIO主动置高;外部电阻只是覆盖复位空档,不替代软件控制。线尽量短;4MHz是示例工作配置,未测最高频。

做到这里,你应该看到
断电用排针表逐根核对,尤其TC5、CC13不能合在同一片选。

参考:[5] Adafruit PID3787:尺寸、控制器与EYESPI修订[6] Adafruit:显示及SD引脚、电源与背光[7] DevKitC-1 v1.1官方排针与模组限制

03 / 显示与总线实践

SD先进入SPI模式,再初始化屏幕

SD卡上电先处于SD模式,尚未转为SPI时,其他SPI通信也可能触发它响应。组合模式先初始化SPI总线、把TFT片选拉高,再调用esp_vfs_fat_sdspi_mount;成功后才建立esp_lcd的显示设备。官方共享指南明确了这一顺序。

SD驱动初始化时自行使用探测低速,host.max_freq_khz=4000限制后续工作时钟;TFT的pclk_hz=4000000。各设备驱动管理自己的SPI事务。本例在单个应用任务中顺序执行;若以后增加并发任务,还需为设备和应用状态另作同步。

参考:[1] ESP-IDF5.5.5:SD与其他SPI设备共享的初始化顺序与负载[2] ESP-IDF5.5.5:SDSPI完整例程[3] ESP-IDF5.5.5:SPI主机、DMA缓冲与事务

04 / 显示与总线实践

总线空闲以外,还要守住一块条带内存

240×240 RGB565整帧为115200字节,但本例按16行条带绘制,只申请7680字节内部DMA内存。esp_lcd_panel_draw_bitmap提交颜色传输后可能先返回;此时缓冲仍归DMA使用。

每条提交后等待on_color_trans_done回调给出的信号量,再重写条带;完整15条完成后才进入下一项卡操作。1秒没等到回调,程序关背光、停止新作业并保留内存/句柄,防止DMA仍访问时释放。这个超时不会硬终止底层事务。

参考:[3] ESP-IDF5.5.5:SPI主机、DMA缓冲与事务[4] ESP-IDF5.5.5:SPI LCD配置与异步传输实现

05 / 显示与总线实践

下载完整工程,在固定SDK中复建

下载中的components/hxdisplay包含原创像素渲染器,main包含模式选择、排他新建文件与读回校验;没有省略驱动适配。P08和D03的显示组件逐字一致,但两者是独立可构建工程。

按E01进入官方IDF5.5.5环境,在本示例目录用下方命令构建;替换为本机桥接串口。sdkconfig.defaults锁8MB/QIO、Octal PSRAM80MHz、UART0/115200。生成的bootloader写入参数可能显示DIO,这是启动头部流程,使用生成flash_args,不手改。完整入口与组件原样见下一节。

代码 / 配置
idf.py -B /tmp/p08-build -D SDKCONFIG=/tmp/p08-build/sdkconfig -D IDF_TARGET=esp32s3 build
idf.py -B /tmp/p08-build -p /dev/cu.YOUR_UART_PORT flash monitor
做到这里,你应该看到
作者已实际构建Exit0;这一步只证明能生成目标固件,未证明接线与卡可用。

参考:[2] ESP-IDF5.5.5:SDSPI完整例程[3] ESP-IDF5.5.5:SPI主机、DMA缓冲与事务

06 / 显示与总线实践

完整入口、记录校验与显示组件

以下文件标签对应下载中的真实相对路径,逐段保存;其他CMake、锁文件和记录模板均在下载中。程序不自动格式化卡,也不覆盖已有P08_xxx.CSV;顺序寻找001至999中第一个空闲名称,卡满或名称耗尽会停止。

代码 / 配置
// ---- main/Record.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
#include <stdio.h>
#include <string.h>
inline uint32_t record_hash(const char* s){uint32_t h=2166136261u;while(*s){h^=uint8_t(*s++);h*=16777619u;}return h;}
inline bool make_record(unsigned n,char* out,size_t cap){
 char plain[24];int k=snprintf(plain,sizeof plain,"P08,%03u",n);if(k<0||size_t(k)>=sizeof plain)return false;
 int len=snprintf(out,cap,"%s,%08lX\n",plain,(unsigned long)record_hash(plain));return len>0&&size_t(len)<cap;
}
inline bool verify_record(unsigned n,const char* line){char expected[48];return make_record(n,expected,sizeof expected)&&strcmp(line,expected)==0;}
// ---- main/Console.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <unistd.h>
#include <fcntl.h>
#include "driver/uart.h"
#include "esp_vfs_dev.h"
inline void init_console(){
 ESP_ERROR_CHECK(uart_driver_install(UART_NUM_0,256,0,0,nullptr,0));
 esp_vfs_dev_uart_use_driver(UART_NUM_0);
 int flags=fcntl(STDIN_FILENO,F_GETFL);fcntl(STDIN_FILENO,F_SETFL,flags|O_NONBLOCK);
}
inline char console_key(){char c=0;return read(STDIN_FILENO,&c,1)==1?c:0;}
// ---- components/hxdisplay/Graphics.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
#include <stdio.h>
#include <string.h>
namespace hx {
struct View { enum Page { TEST, SENSOR, PROGRESS } page=TEST; unsigned rotation=0,seq=0; bool valid=false; int temperature_mC=0,humidity_mPermille=0; };
// Original 5x7 uppercase/digit bitmap font, one row per byte (5 significant bits).
inline const uint8_t* glyph(char c) {
 struct Entry { char c; uint8_t row[7]; };
 static const Entry f[]={
 {'0',{14,17,19,21,25,17,14}},{'1',{4,12,4,4,4,4,14}},{'2',{14,17,1,2,4,8,31}},
 {'3',{30,1,1,14,1,1,30}},{'4',{2,6,10,18,31,2,2}},{'5',{31,16,16,30,1,1,30}},
 {'6',{14,16,16,30,17,17,14}},{'7',{31,1,2,4,8,8,8}},{'8',{14,17,17,14,17,17,14}},{'9',{14,17,17,15,1,1,14}},
 {'A',{14,17,17,31,17,17,17}},{'B',{30,17,17,30,17,17,30}},{'C',{14,17,16,16,16,17,14}},
 {'D',{30,17,17,17,17,17,30}},{'E',{31,16,16,30,16,16,31}},{'F',{31,16,16,30,16,16,16}},
 {'G',{14,17,16,23,17,17,14}},{'H',{17,17,17,31,17,17,17}},{'I',{14,4,4,4,4,4,14}},
 {'J',{7,2,2,2,2,18,12}},{'K',{17,18,20,24,20,18,17}},{'L',{16,16,16,16,16,16,31}},
 {'M',{17,27,21,21,17,17,17}},{'N',{17,25,21,19,17,17,17}},{'O',{14,17,17,17,17,17,14}},
 {'P',{30,17,17,30,16,16,16}},{'Q',{14,17,17,17,21,18,13}},{'R',{30,17,17,30,20,18,17}},
 {'S',{15,16,16,14,1,1,30}},{'T',{31,4,4,4,4,4,4}},{'U',{17,17,17,17,17,17,14}},
 {'V',{17,17,17,17,17,10,4}},{'W',{17,17,17,21,21,27,17}},{'X',{17,17,10,4,10,17,17}},
 {'Y',{17,17,10,4,4,4,4}},{'Z',{31,1,2,4,8,16,31}},{'.',{0,0,0,0,0,6,6}},
 {'-',{0,0,0,31,0,0,0}},{'+',{0,4,4,31,4,4,0}},{'/',{1,1,2,4,8,16,16}},
 {'%',{25,25,2,4,8,19,19}},{',',{0,0,0,0,6,4,8}},{' ',{0,0,0,0,0,0,0}}
 };
 for(const auto& e:f)if(e.c==c)return e.row;
 static const uint8_t unknown[7]={31,17,1,2,4,0,4};return unknown;
}
inline bool ink(int x,int y,const char* text,int left,int top,int scale=2) {
 x-=left;y-=top;if(x<0||y<0||y>=7*scale)return false;
 unsigned i=unsigned(x/(6*scale));if(i>=strlen(text)||x%(6*scale)>=5*scale)return false;
 return glyph(text[i])[y/scale] & (1<<(4-(x%(6*scale))/scale));
}
inline uint16_t pixel(const View& v,int x,int y) {
 constexpr uint16_t W=0xffff,R=0xf800,G=0x07e0,B=0x001f,K=0;
 if(x==0||y==0||x==239||y==239)return W;
 char title[32],line[32];
 snprintf(title,sizeof title,"R%u %s",v.rotation,v.page==View::TEST?"RGB TEST":v.page==View::SENSOR?"SHT40":"SPI SHARED");
 if(ink(x,y,title,12,20))return W;
 if(ink(x,y,"0",4,4,1)||ink(x,y,"1",229,4,1)||ink(x,y,"2",229,229,1)||ink(x,y,"3",4,229,1))return W;
 if(v.page==View::TEST){
  if(y>=50&&y<130)return x<80?R:x<160?G:B;
  if(ink(x,y,"240X240",12,155)||ink(x,y,"RGB565 LE",12,182))return W;
  const char* gap=v.rotation==0?"GAP 0,80":v.rotation==1?"GAP 80,0":"GAP 0,0";
  return ink(x,y,gap,12,208)?W:K;
 }
 if(v.page==View::PROGRESS){
  snprintf(line,sizeof line,"PASS %03u/100",v.seq);
  if(ink(x,y,line,12,80)||ink(x,y,"WRITE READ",12,125)||ink(x,y,"SPI DONE",12,170))return W;
  return K;
 }
 if(!v.valid)return (ink(x,y,"NO SAMPLE",12,80)||ink(x,y,"CHECK I2C",12,125))?W:K;
 unsigned t=v.temperature_mC<0?unsigned(-v.temperature_mC):unsigned(v.temperature_mC);
 snprintf(line,sizeof line,"T %c%u.%u C",v.temperature_mC<0?'-':'+',t/1000,(t%1000)/100);
 if(ink(x,y,line,12,80))return W;
 snprintf(line,sizeof line,"RH %u.%u %%",unsigned(v.humidity_mPermille)/1000,(unsigned(v.humidity_mPermille)%1000)/100);
 return ink(x,y,line,12,125)?W:K;
}
struct Rotation { bool swap,mx,my; int gx,gy; };
inline Rotation rotation(unsigned r){switch(r&3){case 0:return {false,true,true,0,80};case 1:return {true,false,true,80,0};case 2:return {false,false,false,0,0};default:return {true,true,false,0,0};}}
}
// ---- components/hxdisplay/Tft.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include "esp_err.h"
#include "driver/spi_master.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_ops.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "Graphics.h"
namespace hx {
constexpr int SCK=12,MOSI=11,MISO=10,TCS=5,CCS=13,DC=4,RST=6,BL=7;
constexpr spi_host_device_t HOST=SPI2_HOST;
esp_err_t init_bus();
class Tft {
 public:
  esp_err_t init();
  esp_err_t set_rotation(unsigned r);
  esp_err_t draw(const View& v);
  void backlight(bool on);
 private:
  esp_lcd_panel_handle_t panel_=nullptr;
  esp_lcd_panel_io_handle_t io_=nullptr;
  SemaphoreHandle_t done_=nullptr;
  uint16_t* strip_=nullptr;
};
}
// ---- components/hxdisplay/Tft.cpp ----
// SPDX-License-Identifier: MIT
#include "Tft.h"
#include <initializer_list>
#include "driver/gpio.h"
#include "esp_heap_caps.h"
#include "esp_lcd_panel_vendor.h"
namespace hx {
esp_err_t init_bus(){
 for(int pin:{TCS,CCS}){ESP_ERROR_CHECK(gpio_set_level(gpio_num_t(pin),1));ESP_ERROR_CHECK(gpio_set_direction(gpio_num_t(pin),GPIO_MODE_OUTPUT));}
 ESP_ERROR_CHECK(gpio_set_level(gpio_num_t(BL),0));ESP_ERROR_CHECK(gpio_set_direction(gpio_num_t(BL),GPIO_MODE_OUTPUT));
 spi_bus_config_t cfg{};cfg.sclk_io_num=SCK;cfg.mosi_io_num=MOSI;cfg.miso_io_num=MISO;
 cfg.quadwp_io_num=-1;cfg.quadhd_io_num=-1;cfg.max_transfer_sz=240*16*2;
 return spi_bus_initialize(HOST,&cfg,SPI_DMA_CH_AUTO);
}
static bool done_cb(esp_lcd_panel_io_handle_t,esp_lcd_panel_io_event_data_t*,void* ctx){
 BaseType_t wake=pdFALSE;xSemaphoreGiveFromISR((SemaphoreHandle_t)ctx,&wake);return wake==pdTRUE;
}
void Tft::backlight(bool on){gpio_set_level(gpio_num_t(BL),on);}
esp_err_t Tft::init(){
 done_=xSemaphoreCreateBinary();strip_=(uint16_t*)heap_caps_malloc(240*16*2,MALLOC_CAP_DMA|MALLOC_CAP_INTERNAL);
 if(!done_||!strip_)return ESP_ERR_NO_MEM;
 esp_lcd_panel_io_spi_config_t io{};io.cs_gpio_num=TCS;io.dc_gpio_num=DC;io.spi_mode=0;io.pclk_hz=4000000;io.trans_queue_depth=1;io.lcd_cmd_bits=8;io.lcd_param_bits=8;io.on_color_trans_done=done_cb;io.user_ctx=done_;
 esp_err_t e=esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)HOST,&io,&io_);if(e!=ESP_OK)return e;
 esp_lcd_panel_dev_config_t dev{};dev.reset_gpio_num=RST;dev.rgb_ele_order=LCD_RGB_ELEMENT_ORDER_RGB;dev.data_endian=LCD_RGB_DATA_ENDIAN_LITTLE;dev.bits_per_pixel=16;
 if((e=esp_lcd_new_panel_st7789(io_,&dev,&panel_))!=ESP_OK)return e;
 if((e=esp_lcd_panel_reset(panel_))!=ESP_OK)return e;
 if((e=esp_lcd_panel_init(panel_))!=ESP_OK)return e;
 if((e=esp_lcd_panel_invert_color(panel_,true))!=ESP_OK)return e;
 if((e=set_rotation(0))!=ESP_OK)return e;
 return esp_lcd_panel_disp_on_off(panel_,true);
}
esp_err_t Tft::set_rotation(unsigned r){
 auto q=rotation(r);esp_err_t e=esp_lcd_panel_swap_xy(panel_,q.swap);if(e!=ESP_OK)return e;
 if((e=esp_lcd_panel_mirror(panel_,q.mx,q.my))!=ESP_OK)return e;
 return esp_lcd_panel_set_gap(panel_,q.gx,q.gy);
}
esp_err_t Tft::draw(const View& v){
 for(int y=0;y<240;y+=16){
  for(int j=0;j<16;++j)for(int x=0;x<240;++x)strip_[j*240+x]=pixel(v,x,y+j);
  esp_err_t e=esp_lcd_panel_draw_bitmap(panel_,0,y,240,y+16,strip_);if(e!=ESP_OK)return e;
  if(xSemaphoreTake(done_,pdMS_TO_TICKS(1000))!=pdTRUE){backlight(false);return ESP_ERR_TIMEOUT;}
 }
 backlight(true);return ESP_OK;
}
}
// ---- main/main.cpp ----
// SPDX-License-Identifier: MIT
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_vfs_fat.h"
#include "driver/sdspi_host.h"
#include "sdmmc_cmd.h"
#include "Tft.h"
#include "Record.h"
#include "Console.h"

static hx::Tft screen;
static void stop(const char* stage){screen.backlight(false);ESP_LOGE("P08","STOP %s; no more SPI jobs; inspect log then RESET",stage);}
static FILE* new_file(char* path,size_t cap){
 for(unsigned n=1;n<=999;++n){
  snprintf(path,cap,"/sdcard/P08_%03u.CSV",n);
  int fd=open(path,O_CREAT|O_EXCL|O_RDWR,0600);
  if(fd>=0){FILE* f=fdopen(fd,"w+");if(!f)close(fd);return f;}
  if(errno!=EEXIST)return nullptr;
 }
 errno=EEXIST;return nullptr;
}
static bool store_and_verify(FILE* f,unsigned n){
 char wanted[48],got[48];if(!make_record(n,wanted,sizeof wanted))return false;
 long offset=ftell(f);if(offset<0)return false;
 if(fwrite(wanted,1,strlen(wanted),f)!=strlen(wanted))return false;
 if(fflush(f)!=0||fsync(fileno(f))!=0)return false;
 if(fseek(f,offset,SEEK_SET)!=0||!fgets(got,sizeof got,f)||!verify_record(n,got))return false;
 return fseek(f,0,SEEK_END)==0;
}
extern "C" void app_main(){
 init_console();ESP_ERROR_CHECK(hx::init_bus());
 ESP_LOGI("P08","READY SPI2 SCK12 MOSI11 MISO10 TFT_CS5 SD_CS13 MODE0 4MHz");
 ESP_LOGI("P08","Select T=tft only (remove card first), C=card only, B=both; one run per RESET");
 char mode=0;
 while(!mode){char c=console_key();if(c=='T'||c=='t')mode='T';if(c=='C'||c=='c')mode='C';if(c=='B'||c=='b')mode='B';vTaskDelay(pdMS_TO_TICKS(10));}
 bool use_card=mode!='T',use_tft=mode!='C';
 sdmmc_card_t* card=nullptr;FILE* f=nullptr;char path[64]={};
 if(use_card){
  // Other CS is already HIGH; initialise the SD card into SPI mode first.
  sdmmc_host_t host=SDSPI_HOST_DEFAULT();host.slot=hx::HOST;host.max_freq_khz=4000;
  sdspi_device_config_t dev=SDSPI_DEVICE_CONFIG_DEFAULT();dev.host_id=hx::HOST;dev.gpio_cs=gpio_num_t(hx::CCS);
  esp_vfs_fat_sdmmc_mount_config_t cfg{};cfg.format_if_mount_failed=false;cfg.max_files=4;cfg.allocation_unit_size=16384;
  esp_err_t e=esp_vfs_fat_sdspi_mount("/sdcard",&host,&dev,&cfg,&card);
  if(e!=ESP_OK){ESP_LOGE("P08","mount %s",esp_err_to_name(e));stop("card init; no format attempted");return;}
  f=new_file(path,sizeof path);if(!f){stop("new file (full card/path/999 names)");return;}
  ESP_LOGI("P08","new file %s; existing files kept",path);
 }
 if(use_tft){
  esp_err_t e=screen.init();if(e!=ESP_OK){if(f)fclose(f);stop("TFT init");return;}
  hx::View card_view;if(screen.draw(card_view)!=ESP_OK){if(f)fclose(f);stop("test-card SPI transfer");return;}
  vTaskDelay(pdMS_TO_TICKS(2000));
 }
 hx::View v;v.page=hx::View::PROGRESS;
 unsigned passed=0;
 for(unsigned n=1;n<=100;++n){
  int64_t start=esp_timer_get_time();
  if(use_card&&!store_and_verify(f,n)){ESP_LOGE("P08","record %u failed",n);fclose(f);stop("write/flush/readback");return;}
  int64_t stored=esp_timer_get_time();v.seq=n;
  if(use_tft&&screen.draw(v)!=ESP_OK){if(f)fclose(f);stop("TFT transfer timeout; DMA buffer retained");return;}
  ++passed;
  ESP_LOGI("P08","mode=%c pass=%u/100 card_us=%lld tft_us=%lld",mode,passed,(long long)(stored-start),(long long)(esp_timer_get_time()-stored));
  vTaskDelay(pdMS_TO_TICKS(50));
 }
 if(f&&fclose(f)!=0){stop("close");return;}
 if(use_card){esp_err_t e=esp_vfs_fat_sdcard_unmount("/sdcard",card);if(e!=ESP_OK){stop("unmount");return;}}
 ESP_LOGI("P08","DONE mode=%c records/transfers=%u; logical readback/SPI completion only, inspect real screen separately",mode,passed);
}
07 / 显示与总线实践

T、C、B分别运行,留下100轮和时钟记录

每次上电/RESET后只选一种模式,保留完整日志。先备份卡,示例只接受事先在电脑准备好的FAT32,不会因挂载失败替你格式化。不要热插拔。

把实际卡型号、供电、线长、4MHz工作上限、每轮card_us/tft_us、最后DONE和照片写入空observations.csv;这些栏没有作者实测值。SD文件读回只证明当前软件路径返回相同数据,不能承诺突然断电后的文件完整性。

  1. 断电拔下SD,启动串口发T;应先有RGB卡再PASS001…100和DONE mode=T,肉眼查颜色/边框。
  2. 断电插入已备份FAT32卡,启动发C;TFT不初始化且背光低,卡应创建新CSV并有100条校验与DONE mode=C。
  3. 前两项实际通过后启动发B;SD先mount,每轮写入→fflush/fsync→读回→TFT完成,直到pass=100/100及DONE mode=B。
  4. 正常卸载后断电取卡,电脑独立读取CSV,核对001–100与100条;另看屏幕PASS100、完整边框,附照片和原日志。
做到这里,你应该看到
没有B模式实物100轮证据时,hardware维度保持pending。

参考:[1] ESP-IDF5.5.5:SD与其他SPI设备共享的初始化顺序与负载[2] ESP-IDF5.5.5:SDSPI完整例程

08 / 显示与总线实践

只在组合时失败,先查新增加的条件

T失败优先查VIN、BL、TC/DC/RST、固定窗口和RGB字节序;C失败先看mount错误、FAT32、CC13/SO10、卡插接与电源。开文件失败还可能是卡满、文件系统错误或999个名称已占用,不应清空卡来掩盖原因。

各自正常、B失败时,检查两路CS是否真的独立、启动有没有先mount卡、有没有其他库重新初始化SPI,再查线长和负载。降频可以作为另一次有记录的对照;重新构建与记录,不能把建议写成已解决。

主机fixture用同一Record.h执行100条临时文件记录、错序、损坏与长度边界,已经通过;它没有SPI、FAT卡扇区或TFT模型。超时先保留日志,再断电修改接线、RESET恢复。需要复刷时走F06的BOOT/RESET流程,不以关闭欠压检测修复。

前置回看P06的总线排错思路和E01的构建环境;下一篇D03细查同一屏幕的色卡、方向和偏移。

参考:[1] ESP-IDF5.5.5:SD与其他SPI设备共享的初始化顺序与负载[3] ESP-IDF5.5.5:SPI主机、DMA缓冲与事务

BACK TO THE SOURCE

资料来源与版本说明

固定版本源与具体模块资料逐项核对;构建与主机逻辑检查通过,所有实板观察待测。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. ESP-IDF5.5.5:SD与其他SPI设备共享的初始化顺序与负载
  2. ESP-IDF5.5.5:SDSPI完整例程
  3. ESP-IDF5.5.5:SPI主机、DMA缓冲与事务
  4. ESP-IDF5.5.5:SPI LCD配置与异步传输实现
  5. Adafruit PID3787:尺寸、控制器与EYESPI修订
  6. Adafruit:显示及SD引脚、电源与背光
  7. DevKitC-1 v1.1官方排针与模组限制
  8. Adafruit ST7789 1.11.0:240×240窗口与方向