ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

GD32H759 RT-Thread SPI驱动实战:从寄存器到ST7789与W25Q64

GD32H759 RT-Thread SPI驱动实战:从寄存器到ST7789与W25Q64 1. 为什么在 GD32H759 上跑 RT-Thread 还要死磕 SPI拿到 GD32H759 这块片子的时候我第一反应是外设资源真够猛的主频拉到 600MHz各种定时器、串口、SPI 一应俱全。但真正把它塞进工控项目里跑起来才发现事情没那么简单。工控场景对通信稳定性的要求跟消费电子完全不是一个量级现场电磁环境复杂线缆动辄一两米偶尔还要跟变频器、伺服驱动器共处一个电柜。这种条件下SPI 作为板级高速总线反而成了最容易出幺蛾子的环节。这篇内容主要聊的是 GD32H759 搭配 RT-Thread 操作系统时SPI 外设的驱动配置、设备框架对接、以及实际挂载 ST7789 这类显示模组和 W25Q64 这类存储芯片时的完整实操过程。适合已经上手过 GD32 或者 STM32 系列、对 RT-Thread 设备驱动模型有基本了解、但一到具体外设配置就卡壳的嵌入式开发者。我会把寄存器层面的配置逻辑、RT-Thread SPI 设备框架的注册流程、DMA 传输的坑、片选信号的软硬之争这些内容全部拆开讲清楚。先说结论GD32H7xx 系列的 SPI 外设跟 GD32F1/F3 系列相比寄存器结构有变化时钟树也更复杂直接照搬老代码大概率跑不通。而 RT-Thread 的 SPI 设备框架虽然封装得不错但如果你不理解它底层怎么调用 HAL 或者标准外设库出问题的时候根本无从下手。所以这篇内容的核心思路是先把硬件层的 SPI 时序和 GD32H759 的 SPI 寄存器组讲透再往上对接 RT-Thread 的驱动框架最后用 ST7789 和 W25Q64 两个实际案例把整个链路串起来。2. SPI 协议核心要点与 GD32H759 外设特性拆解2.1 SPI 时序的四个模式到底怎么选SPI 协议本身不复杂四根线SCK、MOSI、MISO、CS。但真正让新手头疼的是 CPOL 和 CPHA 这两个参数组合出来的四种模式。我见过太多人在这上面翻车现象就是数据读出来全是 0xFF 或者 0x00查半天代码没问题最后发现是模式选错了。CPOL 决定时钟空闲状态是高还是低CPHA 决定数据在时钟的哪个边沿采样。具体组合如下模式CPOLCPHA空闲电平采样边沿典型器件Mode 000低第一个边沿上升ST7789、W25Q64Mode 101低第二个边沿下降部分 ADCMode 210高第一个边沿下降较少见Mode 311高第二个边沿上升部分传感器ST7789 和 W25Q64 都用 Mode 0这也是最常用的模式。但注意有些 SPI Flash 在高速读取时对模式有特殊要求比如 W25Q64 在 Quad 模式下时序会变这个后面细说。注意选错模式不会烧片子但会让你怀疑人生。建议先用逻辑分析仪抓一下 SCK 和 MOSI 的波形对照器件手册确认采样边沿。2.2 GD32H759 的 SPI 外设有什么不一样GD32H759 的 SPI 外设相比 GD32F103 那代主要变化在时钟源选择和 FIFO 深度上。F103 的 SPI 时钟直接来自 APB2分频系数就是简单的 2/4/8/16 这种。但 H759 的 SPI 时钟源可以选 APB 或者 PLL 分频出来的高速时钟这意味着你可以让 SPI 跑得更快但也更容易因为时钟配置错误导致通信失败。具体来说GD32H759 的 SPI 挂载在 APB2 上APB2 的时钟频率取决于系统时钟配置。假设系统跑 600MHzAPB2 分频后可能是 150MHz 或者 120MHz。SPI 的波特率分频寄存器 SPI_CTL0 中的 PSC 位可以设置 2 到 256 的分频。计算公式是SPI 时钟 APB2 时钟 / PSC 值比如 APB2 跑 150MHzPSC 设为 8那 SPI 时钟就是 18.75MHz。ST7789 的 SPI 接口最高支持大概 62.5MHzW25Q64 标准模式支持 104MHz所以 18.75MHz 完全够用。但如果你要跑更高速度就得注意 PCB 走线和线缆长度了。另外 H759 的 SPI 有 32 字节的 FIFO这个在连续传输大量数据时非常有用。F103 只有单字节缓冲每发一个字节都要等 TXE 标志CPU 占用率高。H759 的 FIFO 可以让 CPU 一次性写入多个字节然后 DMA 或者中断慢慢搬效率提升明显。2.3 硬件片选和软件片选的取舍CS 片选信号的处理方式有两种硬件片选和软件片选。硬件片选是 SPI 外设自动控制 CS 引脚传输开始拉低结束拉高。软件片选是你自己用 GPIO 控制。GD32H759 的 SPI 支持硬件片选但实际项目中我几乎不用。原因有几个第一硬件片选只能控制一个 CS 引脚如果你挂多个从设备还是得用软件片选第二硬件片选的时序有时候跟器件手册要求对不上比如 ST7789 要求 CS 拉低后至少等 10ns 才能发第一个时钟硬件片选可能不满足这个延时第三调试的时候软件片选更灵活你可以手动控制 CS 来抓波形。所以我的建议是一律用软件片选。把 SPI 的 NSS 引脚配置成普通 GPIO 输出在传输前后手动拉低拉高。这样虽然多几行代码但可控性完全不一样。实操心得软件片选的时候CS 拉低和第一个 SCK 边沿之间最好加几个 NOP 延时尤其是线缆较长的时候。我一般加 1 微秒左右的延时用rt_hw_us_delay(1)就行。3. RT-Thread SPI 设备框架对接实操3.1 RT-Thread 的 SPI 设备模型长什么样RT-Thread 的 SPI 设备框架分三层SPI 总线设备、SPI 从设备、SPI 消息。总线设备对应 MCU 的一个 SPI 外设比如 SPI0、SPI1。从设备对应挂在总线上的具体芯片比如 ST7789、W25Q64。消息则是每次传输的数据包包含发送缓冲区、接收缓冲区、长度和片选引脚。注册流程是这样的先实现底层 SPI 外设的初始化、配置、收发函数然后调用rt_spi_bus_register注册总线。接着用rt_spi_bus_attach_device把从设备挂到总线上指定片选引脚。最后应用层用rt_spi_transfer或者rt_spi_send这些 API 来收发数据。听起来简单但实际配置的时候有几个关键点容易出错。第一个是rt_spi_configure里的模式设置第二个是片选引脚的申请和释放第三个是 DMA 传输时的缓冲区对齐问题。3.2 GD32H759 SPI 底层驱动移植要点RT-Thread 官方有 GD32 的 BSP 包但 H759 这个型号比较新可能没有现成的 SPI 驱动。我的做法是参考 GD32F4 的驱动然后根据 H759 的寄存器手册改。底层驱动需要实现这几个函数static rt_err_t gd32_spi_init(struct rt_spi_device *device); static rt_err_t gd32_spi_configure(struct rt_spi_device *device, struct rt_spi_configuration *cfg); static rt_uint32_t gd32_spi_xfer(struct rt_spi_device *device, struct rt_spi_message *message);init函数里要做的事情包括使能 SPI 时钟、配置 SCK/MOSI/MISO 引脚为复用功能、配置 CS 引脚为普通 GPIO、复位 SPI 外设、设置 SPI 为主机模式、设置数据宽度为 8 位、设置 NSS 为软件模式。configure函数里根据cfg-mode设置 CPOL 和 CPHA根据cfg-max_hz计算分频系数根据cfg-data_width设置数据帧格式。xfer函数是核心它要处理message链表对每个消息先拉低 CS然后循环发送/接收数据最后拉高 CS。如果启用了 DMA这里还要配置 DMA 通道。注意GD32H759 的 SPI 引脚复用功能跟 F4 不一样查手册的时候别翻错章节。SPI0 的 SCK 在 PA5MOSI 在 PA7MISO 在 PA6跟 F4 一样但 SPI1 和 SPI2 的引脚分布有变化。3.3 设备注册和挂载的完整代码假设我们用 SPI0CS 用 PA4挂载 ST7789 和 W25Q64 两个设备。代码大概长这样#include rtthread.h #include rtdevice.h #include gd32_spi.h static struct rt_spi_device st7789_device; static struct rt_spi_device w25q64_device; int rt_hw_spi_init(void) { /* 注册 SPI0 总线 */ gd32_spi_bus_register(SPI0, spi0); /* 挂载 ST7789CS 用 PA4 */ rt_spi_bus_attach_device(st7789_device, st7789, spi0, (void *)GPIO_PIN_4); /* 挂载 W25Q64CS 用 PA15 */ rt_spi_bus_attach_device(w25q64_device, w25q64, spi0, (void *)GPIO_PIN_15); return RT_EOK; } INIT_DEVICE_EXPORT(rt_hw_spi_init);这里有个细节rt_spi_bus_attach_device的第四个参数是 CS 引脚类型是void *。在 GD32 的驱动里这个参数会被转换成 GPIO 引脚号。但不同 BSP 的实现可能不一样有的用引脚编号有的用端口和引脚组合的结构体。移植的时候一定要看底层驱动怎么解析这个参数。4. ST7789 显示模组驱动实战4.1 ST7789 的 SPI 通信特点ST7789 是一颗 240x320 的 TFT 液晶驱动芯片SPI 接口支持 Mode 0 和 Mode 3。它有一个 DC 引脚用来区分命令和数据DC 拉低表示发的是命令DC 拉高表示发的是数据。这个引脚不参与 SPI 时序但必须跟 CS 配合好。ST7789 的 SPI 写时序是这样的CS 拉低DC 设置好然后在 SCK 上升沿采样 MOSI 数据。每个字节高位先发。写命令的时候先发命令字节然后如果需要参数再发参数字节。写数据的时候直接发数据字节。初始化 ST7789 需要发一系列命令包括软复位、睡眠退出、像素格式设置、显示方向设置、伽马校正等。这些命令参数在数据手册里都有但不同厂家的模组可能略有差异最好找卖家要初始化代码。4.2 用 RT-Thread SPI 框架驱动 ST7789在 RT-Thread 里驱动 ST7789我一般会封装一层简单的 APIstatic void st7789_write_cmd(rt_uint8_t cmd) { rt_pin_write(ST7789_DC_PIN, PIN_LOW); rt_spi_send(st7789_device, cmd, 1); } static void st7789_write_data(rt_uint8_t data) { rt_pin_write(ST7789_DC_PIN, PIN_HIGH); rt_spi_send(st7789_device, data, 1); } static void st7789_write_buf(rt_uint8_t *buf, rt_uint32_t len) { rt_pin_write(ST7789_DC_PIN, PIN_HIGH); rt_spi_send(st7789_device, buf, len); }注意rt_spi_send每次调用都会拉低再拉高 CS如果你要连续发多个字节最好用rt_spi_transfer或者自己控制 CS。比如初始化的时候发完命令马上发参数中间 CS 不能拉高否则 ST7789 会认为一次传输结束了。我的做法是在st7789_write_cmd里不控制 CS而是让上层函数统一控制。比如static void st7789_init(void) { rt_pin_write(ST7789_CS_PIN, PIN_LOW); st7789_write_cmd(0x01); /* 软复位 */ rt_thread_mdelay(120); st7789_write_cmd(0x11); /* 退出睡眠 */ rt_thread_mdelay(120); st7789_write_cmd(0x3A); /* 像素格式 */ st7789_write_data(0x55); /* 16位色 */ /* ... 其他命令 ... */ rt_pin_write(ST7789_CS_PIN, PIN_HIGH); }但这样又跟 RT-Thread SPI 框架的 CS 管理冲突了。因为rt_spi_send内部会操作 CS。解决办法是在rt_spi_configure的时候把 CS 引脚设为软件控制然后在应用层手动控制 CS调用rt_spi_send的时候传RT_NULL作为 CS 参数不行RT-Thread 的 API 不支持这样。实际可行的方案是在底层驱动的xfer函数里判断 message 的cs_take标志。如果cs_take为 1就拉低 CS如果cs_release为 1就拉高 CS。这样上层可以用rt_spi_transfer的 message 链表来控制 CS 的保持。或者更简单粗暴不用 RT-Thread 的 SPI 框架直接操作寄存器。但这样就失去了设备框架的好处不推荐。我最后采用的方案是修改底层驱动的xfer函数让它支持 CS 保持。具体来说在rt_spi_message结构体里cs_take和cs_release是控制 CS 的。如果上层调用rt_spi_send框架会自动设置cs_take1, cs_release1。如果调用rt_spi_transfer并手动构造 message可以设置cs_take1, cs_release0来保持 CS。struct rt_spi_message msg1, msg2; msg1.send_buf cmd; msg1.recv_buf RT_NULL; msg1.length 1; msg1.cs_take 1; msg1.cs_release 0; msg1.next msg2; msg2.send_buf data; msg2.recv_buf RT_NULL; msg2.length 1; msg2.cs_take 0; msg2.cs_release 1; msg2.next RT_NULL; rt_spi_transfer_message(st7789_device, msg1);这样就能在一次 CS 有效期内发完命令和参数。4.3 刷屏性能优化与 DMA 配置ST7789 刷全屏是 240x320x2 153600 字节。如果 SPI 时钟跑 18.75MHz理论上需要 153600 * 8 / 18750000 ≈ 65ms。但实际用rt_spi_send逐字节发CPU 占用率会很高而且中间会有间隔实际刷屏时间可能超过 100ms。用 DMA 可以大幅提升效率。GD32H759 的 SPI0 支持 DMA 请求映射到 DMA0 的通道 3发送和通道 2接收。配置 DMA 的时候要注意源地址是内存缓冲区目标地址是 SPI 数据寄存器传输方向是内存到外设数据宽度是字节传输完成后要清除 DMA 标志要等 SPI 的 TRANS 标志置位后才能拉高 CSRT-Thread 的 SPI 框架支持 DMA 吗官方框架本身不直接支持但底层驱动可以实现。我的做法是在xfer函数里判断数据长度如果超过 32 字节就启用 DMA否则用中断或者轮询。DMA 配置的关键代码static void spi_dma_send(rt_uint8_t *buf, rt_uint32_t len) { dma_channel_disable(DMA0, DMA_CH3); dma_deinit(DMA0, DMA_CH3); dma_init_struct.direction DMA_MEMORY_TO_PERIPHERAL; dma_init_struct.memory_addr (rt_uint32_t)buf; dma_init_struct.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_init_struct.memory_width DMA_MEMORY_WIDTH_8BIT; dma_init_struct.periph_addr (rt_uint32_t)SPI_DATA(SPI0); dma_init_struct.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.periph_width DMA_PERIPHERAL_WIDTH_8BIT; dma_init_struct.priority DMA_PRIORITY_HIGH; dma_init(DMA0, DMA_CH3, dma_init_struct); dma_transfer_number_config(DMA0, DMA_CH3, len); dma_channel_enable(DMA0, DMA_CH3); spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); while(dma_flag_get(DMA0, DMA_CH3, DMA_FLAG_FTF) RESET); dma_flag_clear(DMA0, DMA_CH3, DMA_FLAG_FTF); spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); }实操心得DMA 传输的时候缓冲区最好用rt_malloc分配并且确保地址对齐。如果缓冲区在栈上DMA 传输期间函数返回了栈被覆盖数据就乱了。我踩过这个坑现象是屏幕花屏查了一天才发现是栈变量被 DMA 用了。5. W25Q64 SPI Flash 读写与文件系统挂载5.1 W25Q64 的命令集和读写流程W25Q64 是 8MB 的 SPI NOR Flash支持标准 SPI、Dual SPI 和 Quad SPI。标准 SPI 下读数据用 0x03 命令页编程用 0x02扇区擦除用 0x20整片擦除用 0xC7。写之前必须先擦除NOR Flash 只能把 1 写成 0不能把 0 写成 1。读数据的流程拉低 CS发 0x03发 24 位地址然后连续读数据。地址是 3 个字节高位在前。读的时候 MISO 线上会返回数据SCK 继续跑就行。写数据的流程先发写使能命令 0x06然后拉低 CS发 0x02发 24 位地址然后发数据。数据不能跨页一页 256 字节。如果跨页了地址会回卷到页首覆盖之前的数据。所以写之前要算好页边界。擦除的流程先发写使能 0x06然后发 0x20 加 24 位地址等待擦除完成。擦除一个扇区 4KB 大概需要 45ms擦除整片 8MB 需要几十秒。擦除期间可以读状态寄存器 0x05 的 BUSY 位来判断是否完成。5.2 在 RT-Thread 上挂载 LittleFS 或 FatFSW25Q64 挂到 RT-Thread 上之后可以跑 LittleFS 或者 FatFS。LittleFS 更适合 NOR Flash因为它有磨损均衡和掉电保护。FatFS 更通用但需要额外的磨损均衡层。挂载 LittleFS 的步骤实现rt_mtd_nor_device的读、写、擦除函数注册 MTD 设备用dfs_mount挂载文件系统读函数直接调 SPI 读命令写函数要先擦除再写擦除函数按扇区擦。注意 LittleFS 的块大小要跟 Flash 的扇区大小匹配W25Q64 的扇区是 4KB所以 LittleFS 的块大小也设 4KB。static rt_err_t w25q64_read(rt_mtd_nor_device_t dev, rt_off_t offset, rt_uint8_t *buf, rt_size_t size) { rt_pin_write(W25Q64_CS_PIN, PIN_LOW); rt_uint8_t cmd 0x03; rt_spi_send(w25q64_device, cmd, 1); rt_uint8_t addr[3]; addr[0] (offset 16) 0xFF; addr[1] (offset 8) 0xFF; addr[2] offset 0xFF; rt_spi_send(w25q64_device, addr, 3); rt_spi_recv(w25q64_device, buf, size); rt_pin_write(W25Q64_CS_PIN, PIN_HIGH); return RT_EOK; }写函数要先发写使能然后发页编程命令最后等待写完成。擦除函数类似发扇区擦除命令后等待。注意W25Q64 的写使能命令每次写之前都要发因为写完成后写使能位会自动清除。如果你连续写多个页每页之前都要发 0x06。5.3 SPI Flash 读写的性能瓶颈和优化W25Q64 标准 SPI 读速度受限于 SCK 频率。18.75MHz 下读 1MB 数据需要 1MB * 8 / 18.75MHz ≈ 447ms。如果用 Quad SPI四根数据线并行速度可以翻四倍。但 Quad SPI 需要额外的引脚而且时序更复杂。在工控场景里如果只是存配置参数和日志标准 SPI 完全够用。如果要存大量数据比如图像或者音频建议用 Quad SPI 或者换用 SD 卡。另一个优化点是减少擦除次数。NOR Flash 的擦除寿命大概 10 万次如果频繁写小数据很容易把某个扇区写坏。LittleFS 有磨损均衡但也不能完全避免。我的做法是把频繁写的日志放到 RAM 里缓冲攒够一个扇区再一次性写入。6. 常见问题排查与避坑指南6.1 SPI 通信失败的典型原因速查现象可能原因排查方法读出来全是 0xFFMISO 没接好、模式选错、CS 没拉低检查接线、用逻辑分析仪抓波形读出来全是 0x00MOSI 没接好、从设备没供电检查供电、测量 MOSI 电压数据偶尔错位时钟太快、线缆太长、干扰降低 SCK 频率、缩短线缆、加屏蔽CS 拉低后无响应CS 引脚配置错误、从设备损坏换一个从设备试试、检查 GPIO 配置DMA 传输花屏缓冲区被覆盖、DMA 配置错误用静态缓冲区、检查 DMA 通道映射6.2 逻辑分析仪抓 SPI 波形的技巧逻辑分析仪是调 SPI 的利器。抓波形的时候注意几点采样率要至少是 SCK 频率的 4 倍否则波形会失真。触发条件设成 CS 下降沿这样能抓到完整的传输过程。解码的时候选 SPI 协议设置好 CPOL 和 CPHA就能直接看到数据字节。我用的是一款 8 通道 24MHz 的逻辑分析仪抓 18.75MHz 的 SPI 有点勉强但看时序关系足够了。如果要抓更高速的 SPI得用 100MHz 以上的分析仪。实操心得抓波形的时候先把 SCK 频率降到 1MHz 左右确认通信正常后再逐步提高。这样能快速定位是时序问题还是信号完整性问题。6.3 RT-Thread SPI 框架的常见坑第一个坑是rt_spi_configure的调用时机。必须在rt_spi_bus_attach_device之后调用否则配置不生效。而且每次切换从设备的时候都要重新配置因为 SPI 总线的模式可能不同。第二个坑是片选引脚的申请。rt_spi_bus_attach_device的 CS 参数如果是引脚号底层驱动需要调用rt_pin_mode把它设成输出。如果忘了这一步CS 引脚可能还是默认的浮空输入根本拉不动。第三个坑是 DMA 和中断的冲突。如果 SPI 用了 DMA就不要在中断里调用rt_spi_transfer因为 DMA 传输是异步的中断里等 DMA 完成会阻塞其他中断。我的做法是在线程里做 SPI 传输中断里只发信号量。第四个坑是多线程访问 SPI 总线。RT-Thread 的 SPI 框架本身没有互斥锁如果两个线程同时调用rt_spi_send数据会交错。解决办法是在应用层加互斥锁或者用rt_spi_take_bus和rt_spi_release_bus来保护。rt_spi_take_bus(st7789_device); rt_spi_send(st7789_device, buf, len); rt_spi_release_bus(st7789_device);6.4 工控现场的抗干扰经验工控现场最大的敌人是电磁干扰。SPI 线缆如果跟电机线捆在一起通信失败率会飙升。我的经验是SPI 线缆尽量短最好不超过 10cm用双绞线或者屏蔽线屏蔽层单端接地SCK 线上串一个 22 欧姆的电阻减少振铃从设备的电源加 0.1uF 和 10uF 电容滤波如果条件允许用差分 SPI 或者隔离 SPI还有一点GD32H759 的 IO 口驱动能力有限如果线缆较长可能需要加缓冲器。74HC245 或者 74LVC245 都可以注意方向控制。7. 从寄存器到应用层的完整链路回顾7.1 初始化顺序和依赖关系整个 SPI 链路的初始化顺序不能乱配置系统时钟确保 APB2 时钟正确使能 GPIO 和 SPI 时钟配置 SCK、MOSI、MISO 为复用功能配置 CS 为普通 GPIO 输出复位 SPI 外设配置 SPI 参数模式、分频、数据宽度使能 SPI注册 RT-Thread SPI 总线挂载从设备配置从设备参数如果顺序错了比如先使能 SPI 再配置 GPIOSPI 可能会输出错误的电平。我遇到过因为 GPIO 配置晚了SCK 上电瞬间输出高电平导致从设备进入错误状态。7.2 中断和 DMA 的优先级配置GD32H759 的中断优先级分组要跟 RT-Thread 的调度器配合。RT-Thread 用 PendSV 做上下文切换优先级最低。SPI 中断的优先级要高于 PendSV但低于系统滴答定时器。DMA 中断的优先级可以设得低一点因为 DMA 传输不紧急。具体配置nvic_irq_enable(SPI0_IRQn, 1, 0); nvic_irq_enable(DMA0_Channel3_IRQn, 2, 0);优先级分组用NVIC_PRIGROUP_PRE2_SUB2这样抢占优先级有 4 级子优先级有 4 级。7.3 实际项目中的参数配置参考以 ST7789 为例我的配置是SPI 时钟18.75MHz模式Mode 0数据宽度8 位片选软件控制PA4DC 引脚PA3复位引脚PA2背光引脚PA1W25Q64 的配置SPI 时钟18.75MHz模式Mode 0数据宽度8 位片选软件控制PA15两个设备共用 SPI0通过不同的 CS 引脚区分。切换设备的时候先拉高当前设备的 CS再拉低目标设备的 CS然后重新配置 SPI 参数如果模式不同。注意ST7789 和 W25Q64 都是 Mode 0所以不需要重新配置模式。但如果挂了一个 Mode 3 的传感器切换的时候必须调rt_spi_configure。8. 写在最后的一些个人体会SPI 这个东西说简单也简单四根线一接就能通。说复杂也复杂时序、模式、片选、DMA、中断、多线程每一个环节都能让你调半天。我在 GD32H759 上跑 RT-Thread 的 SPI 驱动前前后后改了三四版才稳定下来。第一版用轮询CPU 占用率太高第二版用中断发现中断里不能等太久第三版用 DMA又遇到缓冲区对齐问题第四版才把 CS 控制、DMA 传输、多线程保护都理顺。如果你也在调 SPI我的建议是先用最简单的轮询方式把通信跑通确认时序和模式没问题然后再逐步加 DMA 和中断。不要一上来就搞最复杂的方案出了问题你都不知道是哪一层的问题。逻辑分析仪一定要有抓波形比看代码快十倍。还有RT-Thread 的 SPI 框架虽然方便但底层驱动的实现质量参差不齐移植的时候一定要对着手册逐行检查。最后分享一个小技巧如果 SPI 通信偶尔出错但找不到原因可以在 CS 拉低之后、第一个 SCK 之前加一个短延时有时候能解决一些莫名其妙的时序问题。这个延时不用太长几百纳秒到几微秒都行具体看从设备的要求。我在 ST7789 上加了 1 微秒延时之后花屏概率明显降低。
返回列表