ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

树莓派Pico SPI通信实战:从协议原理到驱动TFT屏幕

树莓派Pico SPI通信实战:从协议原理到驱动TFT屏幕 1. 项目缘起为什么要在Pico上折腾SPI如果你玩过树莓派Pico大概率会先从点亮个LED、读个按键开始然后很快就会发现GPIO不够用了或者想接个屏幕、读个传感器但I2C的速度又跟不上。这时候SPI就该登场了。我手头这个项目核心就是基于Raspberry Pi Pico把SPISerial Peripheral Interface这个通信协议给彻底摸透、用活。你可能在各种教程里见过SPI但很多都是点到为止讲个初始化、发个“Hello World”就结束了。实际用起来从选引脚、配模式、到处理时序冲突和提升稳定性坑多得是。SPI对于Pico这类微控制器来说就像它的高速数据通道。I2C虽然省引脚但它是半双工速度也有限标准模式100kHz快速模式400kHz。而SPI是全双工主频轻松上到几十兆赫兹驱动TFT屏幕、读写SD卡、连接高速ADC/DAC这些活离了SPI还真不好办。Pico的硬件SPI外设功能挺强但官方文档和基础例程往往只展示了冰山一角。这次我们不满足于“点灯”目标是实现一个稳定、高效、能应对复杂场景的SPI通信框架并把这些实战中积累的经验和踩过的坑系统地分享出来。2. 核心概念扫盲SPI协议到底在“约法三章”什么在动手写代码前我们必须把SPI的“游戏规则”搞清楚。很多人觉得SPI比I2C简单因为它没有地址概念但实际上它的灵活性背后藏着不少需要明确约定的细节。2.1 SPI的“四线制”与“三线制”最经典的SPI需要4根线SCK (Serial Clock)时钟线由主机产生从机根据这个时钟来同步收发数据。这是SPI的“指挥棒”。MOSI (Master Out Slave In)主机输出从机输入。数据从主机流向从机。MISO (Master In Slave Out)主机输入从机输出。数据从从机流向主机。SS/CS (Slave Select / Chip Select)片选线低电平有效。主机通过拉低这根线来“选中”要通信的从机。这是SPI多设备连接的关键。注意很多资料包括Pico的SDK会用SPI_RX和SPI_TX来指代MISO和MOSI这是从主机视角命名的。对于主机TX就是MOSI主机发送RX就是MISO主机接收。理解这一点对配置代码很重要。有时候比如只向从机写数据而不需要读回例如驱动只有输入的数字电位器或者使用半双工模式的设备可以省去MISO线变成“三线制”。但全双工通信才是SPI的完全体。2.2 核心参数模式(CPOL, CPHA)与速率这是SPI最容易搞混的地方直接关系到通信能否成功。CPOL (Clock Polarity时钟极性)决定了SCK空闲时的电平。CPOL0SCK空闲时为低电平。CPOL1SCK空闲时为高电平。CPHA (Clock Phase时钟相位)决定了数据在时钟的哪个边沿被采样捕获。CPHA0数据在SCK的第一个边沿如果CPOL0就是上升沿如果CPOL1就是下降沿被采样在相反的边沿变化。CPHA1数据在SCK的第二个边沿被采样在第一个边沿变化。这两个参数组合起来就构成了常见的SPI模式0, 1, 2, 3模式0 (Mode 0)CPOL0 CPHA0。空闲低电平在上升沿采样数据。这是最常用的模式。模式1 (Mode 1)CPOL0 CPHA1。空闲低电平在下降沿采样数据。模式2 (Mode 2)CPOL1 CPHA0。空闲高电平在下降沿采样数据。模式3 (Mode 3)CPOL1 CPHA1。空闲高电平在上升沿采样数据。怎么记我个人的经验是永远以从机设备的数据手册为准在连接一个新的SPI设备如Flash芯片、屏幕驱动IC时第一件事就是去手册里找“SPI mode”或“CPOL/CPHA”的说明。主机Pico的模式必须与从机严格匹配否则读回来的全是乱码。速率 (Baud Rate)SPI的通信时钟频率。Pico的硬件SPI可以设置很高的速率理论可达系统时钟的一半但实际速率受限于从机设备支持的最高速率看手册。导线长度和布线质量线太长或干扰大高速率下数据会出错。Pico的GPIO输出能力。通常在面包板或杜邦线连接时我保守起见会从1MHz或2MHz开始测试稳定后再逐步提高。2.3 数据格式MSB先行还是LSB先行SPI协议本身没有规定先传输字节的最高位(MSB)还是最低位(LSB)。这又是一个需要主从双方约定的参数。绝大多数设备默认是MSB先行但有些设备比如某些型号的RFID读卡器芯片可能是LSB先行。同样需要在从机设备的数据手册中确认。Pico的SPI外设可以灵活配置数据传输顺序。3. Pico的硬件SPI外设与引脚分配实战树莓派Pico有两个硬件SPI控制器spi0和spi1。它们功能相同可以独立工作。硬件SPI的优势是效率高不占用CPU时间在后台自动完成数据的移位和收发CPU可以腾出手来做其他事情。3.1 硬件SPI的固定引脚映射这是第一个容易踩坑的点。Pico的硬件SPI引脚不是任意GPIO都可以的它有固定的“功能复用”映射表。以spi0为例SPI功能GPIO引脚 (spi0)GPIO引脚 (spi1)备注SCK2, 6, 1810, 14必须从这几组里选一个TX (MOSI)3, 7, 1911, 15必须从这几组里选一个RX (MISO)0, 4, 168, 12必须从这几组里选一个CSn1, 5, 179, 13硬件片选可选重要提示上表中的CSn引脚是硬件SPI控制器自带的片选信号线。使用它时控制器会在数据传输前后自动控制该引脚的电平非常方便。但是更多时候我们使用“软件片选”即任意一个GPIO口来手动控制因为一个SPI总线可能挂多个设备每个设备都需要独立的片选线而硬件CSn通常只有一根。3.2 初始化配置代码详解我们使用Pico官方推荐的C/C SDK进行开发。下面是一个初始化spi0的典型代码片段我会逐行加上注释#include hardware/spi.h #include hardware/gpio.h #define SPI_PORT spi0 #define PIN_SPI_SCK 2 #define PIN_SPI_MOSI 3 #define PIN_SPI_MISO 4 #define PIN_SPI_CS 5 // 这里我们使用软件片选所以用一个普通GPIO void spi_init_custom() { // 1. 初始化SPI端口指定速率 // 这里设置SPI波特率为1MHz (1 * 1000 * 1000) // 实际时钟 系统时钟 / (CPHA CPOL?) 不对是系统时钟 / (波特率分频系数)。 // spi_init函数内部会计算合适的分频系数。 spi_init(SPI_PORT, 1000 * 1000); // 2. 设置SPI格式 // 参数依次为SPI端口、数据位宽(8)、SPI模式(CPOL, CPHA)、MSB先行 // 这里设置为模式0MSB先行8位数据。这是最常见配置。 spi_set_format(SPI_PORT, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST); // 3. 初始化GPIO功能复用 // 将特定的GPIO引脚设置为SPI功能而不是普通的输入输出。 gpio_set_function(PIN_SPI_SCK, GPIO_FUNC_SPI); gpio_set_function(PIN_SPI_MOSI, GPIO_FUNC_SPI); gpio_set_function(PIN_SPI_MISO, GPIO_FUNC_SPI); // 4. 初始化片选引脚软件控制 gpio_init(PIN_SPI_CS); gpio_set_dir(PIN_SPI_CS, GPIO_OUT); gpio_put(PIN_SPI_CS, 1); // 初始化为高电平不选中 // 可选5. 提升GPIO驱动能力如果连接线较长或负载较重 // gpio_set_drive_strength(PIN_SPI_MOSI, GPIO_DRIVE_STRENGTH_8MA); // gpio_set_slew_rate(PIN_SPI_MOSI, GPIO_SLEW_RATE_FAST); }为什么这么配置spi_init不仅设置速率还会启用SPI外设的时钟。速率值是一个“期望值”SDK会选择一个最接近的、可行的分频系数。spi_set_format是配置的核心必须与从机设备匹配。数据位宽除了8也可以是4到16之间的值取决于外设支持。GPIO功能复用是关键步骤。如果你忘记调用gpio_set_function即使引脚号对了SPI信号也无法输出到物理引脚上。软件片选初始化为高电平是因为SPI片选通常是低电平有效。先拉高确保设备处于未选中状态。4. 软件片选 vs. 硬件片选灵活性与效率的权衡4.1 软件片选绝对的掌控力就像上面代码展示的我们用一个普通GPIO如GPIO5作为片选。通信流程完全手动控制void write_to_spi_device(uint8_t *data, size_t len) { gpio_put(PIN_SPI_CS, 0); // 拉低片选选中设备 sleep_us(1); // 微小延时等待从机准备就绪视设备而定 spi_write_blocking(SPI_PORT, data, len); // 写入数据 sleep_us(1); // 微小延时保证数据稳定 gpio_put(PIN_SPI_CS, 1); // 拉高片选结束通信 }优点数量无限你可以用任意多个GPIO控制任意多个从机。时序可控可以在片选拉低后、时钟产生前插入必要的延时sleep_us这对某些“慢热”的从机设备是必须的。操作直观代码逻辑清晰便于调试。缺点CPU开销每次通信前后都需要CPU干预来操作GPIO。时序精度如果使用sleep_us其精度受系统滴答中断影响在极高频率或苛刻时序下可能不够精确。4.2 硬件片选解放CPU如果使用SPI控制器自带的CSn引脚如spi0的GPIO1/5/17代码会简化// 假设使用GPIO17作为硬件CS #define PIN_SPI_HW_CS 17 gpio_set_function(PIN_SPI_HW_CS, GPIO_FUNC_SPI); // 在传输数据时只需要调用 spi_write_blocking(SPI_PORT, data, len); // CSn引脚会自动在传输开始时拉低传输结束后拉高。优点自动高效完全由硬件控制无需CPU介入时序精准。代码简洁。缺点数量有限每个SPI控制器通常只有1-2个专用的硬件CSn引脚。灵活性差无法在CS有效期间插入自定义延时也无法在连续传输多段数据时保持CS有效除非使用DMA构造特殊的数据帧。4.3 我的选择建议单一高速设备如果总线只挂一个设备且该设备时序要求严格优先考虑硬件片选。多设备系统99%的情况使用软件片选。它带来的灵活性和可扩展性远大于那一点点CPU开销。在连接SD卡、TFT屏、多个传感器时软件片选是更实际的选择。混合使用我曾在一个项目里对实时性要求最高的ADC芯片用了硬件片选spi0的CS0而对另外两个配置后就不常读写的传感器用了软件片选物尽其用。5. 阻塞、非阻塞与DMA三种传输方式的性能抉择Pico的SDK提供了三种SPI数据传输方式应对不同场景。5.1 阻塞式传输简单可靠我们上面用的spi_write_blocking和spi_read_blocking就是阻塞式的。函数会一直等待直到整个数据传输完成才返回。uint8_t tx_buf[4] {0x01, 0x02, 0x03, 0x04}; uint8_t rx_buf[4] {0}; // 阻塞式写入 spi_write_blocking(SPI_PORT, tx_buf, 4); // 执行到这里时4个字节已经发送完毕。 // 阻塞式读取同时需要发送时钟通常发送0xFF或0x00 spi_read_blocking(SPI_PORT, 0xFF, rx_buf, 4);适用场景初始化配置、单次读写、对实时性要求不高的任务。代码简单不易出错。5.2 非阻塞式传输提高CPU利用率使用spi_write16_read16_blocking这类函数不那是阻塞的。真正的非阻塞需要用到spi_get_hw(SPI_PORT)直接操作底层寄存器或者使用spi_write16_nonblocking等函数配合状态查询。但SDK对非阻塞的支持封装较少更常用的高级方式是DMA。5.3 DMA传输终极性能方案DMADirect Memory Access是微控制器里的“专职快递员”。它可以在不打扰CPU的情况下在内存和外设如SPI之间搬运数据。对于大量、连续的SPI数据传输例如刷新整块TFT屏幕、读写SD卡扇区DMA能极大解放CPU。Pico的SDK提供了DMA与SPI配合的API但配置稍复杂。核心思路是配置DMA通道告诉它源地址内存中的数组、目标地址SPI发送数据寄存器、传输数量。配置SPI使其在发送完一个数据后自动向DMA请求下一个数据。启动DMA传输然后CPU就可以去干别的事了。通过中断或轮询检查DMA传输是否完成。一个简化的DMA发送示例框架#include hardware/dma.h int dma_chan; dma_channel_config c; // 准备要发送的数据 uint8_t screen_buffer[1024]; // ... 填充 screen_buffer ... // 获取一个DMA通道 dma_chan dma_claim_unused_channel(true); // 配置DMA通道 c dma_channel_get_default_config(dma_chan); channel_config_set_transfer_data_size(c, DMA_SIZE_8); // 每次传输8位一个字节 channel_config_set_read_increment(c, true); // 源地址内存递增 channel_config_set_write_increment(c, false); // 目标地址SPI数据寄存器固定 channel_config_set_dreq(c, DREQ_SPI0_TX); // 数据请求来自SPI0的发送端 // 配置DMA从内存(buffer)搬运到外设(spi_get_hw(SPI_PORT)-dr) dma_channel_configure(dma_chan, c, spi_get_hw(SPI_PORT)-dr, // 目标地址SPI数据寄存器 screen_buffer, // 源地址 sizeof(screen_buffer), // 传输数量 true); // 立即开始 // 此时CPU空闲。可以在这里执行其他任务比如准备下一帧数据。 // 等待DMA传输完成 dma_channel_wait_for_finish_blocking(dma_chan); // 传输完成清理DMA通道可选或配置为循环模式DMA使用的经验之谈内存对齐DMA访问的内存地址最好4字节对齐__attribute__((aligned(4)))效率最高。缓冲区管理使用双缓冲区Ping-Pong Buffer是高级技巧。当DMA在发送缓冲区A时CPU填充缓冲区B然后无缝切换可以实现无卡顿的连续数据流这在视频播放或高速数据采集时非常有用。调试难度DMA问题较难调试。一旦配置错误可能 silently fail静默失败。务必从简单例子开始确保基础传输正确再增加复杂度。6. 实战避坑指南那些手册上不会写的细节6.1 上拉电阻的必要性SPI总线需要上拉电阻吗I2C是必须的但SPI通常不需要。因为SPI的主机始终驱动着MOSI和SCK从机在选中时驱动着MISO都是推挽输出有明确的高低电平。但是在以下情况加上拉电阻通常4.7kΩ-10kΩ是明智的总线有多主机可能虽然SPI标准是单主机但某些特殊设计会有。从机设备在未被选中时其MISO引脚处于高阻态Hi-Z。如果这根线悬空容易受到噪声干扰。加上拉可以将其稳定在高电平。片选线(CS)如果可能因为程序跑飞而处于未定义状态加上拉可以确保设备默认不被选中防止总线冲突。我的习惯是在PCB设计时会给SCK、MOSI、MISO和所有软件片选线都预留上拉电阻的位置0Ω电阻或焊盘。在面包板实验时如果通信不稳定首先尝试在SCK和MOSI上加10kΩ上拉到3.3V。6.2 时序匹配等待从机就绪很多SPI设备尤其是那些包含慢速存储单元如EEPROM或复杂操作如ADC转换的在片选拉低后需要一段准备时间tCSS才能响应命令。直接发送命令会导致失败。解决方案在gpio_put(CS, 0)后加一个sleep_us(1)或busy_wait_us(1)。这个延时时间需要查阅从机数据手册的“AC Characteristics”章节。对于大多数现代芯片1微秒足矣但对于一些老式芯片可能需要几微秒甚至几十微秒。6.3 全双工与半双工数据帧处理SPI是全双工主机在发送的同时也在接收。spi_write_blocking函数会忽略接收到的数据。spi_read_blocking需要发送一个虚拟字节如0xFF来产生时钟。很多时候我们需要发送命令并接收响应这是一个“先写后读”的过程。注意在写和读之间片选必须保持有效低电平。错误的做法是写完后拉高CS再拉低CS去读。正确的做法是gpio_put(PIN_CS, 0); sleep_us(1); // 发送命令字节 spi_write_blocking(SPI_PORT, cmd, 1); // 可能需要的延时等待从机处理 sleep_us(10); // 读取响应数据发送0xFF产生时钟 spi_read_blocking(SPI_PORT, 0xFF, rx_buf, resp_len); gpio_put(PIN_CS, 1);6.4 电平转换与长线驱动Pico的GPIO是3.3V电平。如果你要连接一个5V的SPI从机设备注意很多现代SPI设备也是3.3V的绝对不能直接连接否则可能损坏Pico。需要使用电平转换芯片如TXB0104等双向转换器。同样如果SPI总线要走很长的线10cm需要考虑信号完整性问题可能需要在输出端串联一个小电阻22-100Ω来抑制振铃或者使用差分SPI有些高速ADC支持或降低通信速率。6.5 多从机共享总线避免冲突当多个设备共享MOSI、MISO、SCK时必须确保任何时候只有一个设备的MISO输出是有效的即被选中。其他未被选中的设备其MISO引脚必须处于高阻态。这是从机芯片设计时就保证的。你需要担心的只是软件上不要同时选中两个设备。更复杂的情况是有些设备的MISO引脚在没有被选中时可能不是高阻而是输出低电平或高电平这就会与当前被选中的设备冲突。这种情况在选型时就要避免。7. 综合案例驱动一块SPI TFT屏幕让我们用一个实际例子把上面的知识点串起来驱动一块常见的1.8英寸SPI TFT屏幕驱动IC通常是ST7735或ST7789。7.1 硬件连接Pico 3.3V - LCD VCCPico GND - LCD GNDPico GP2 (SPI0 SCK) - LCD SCKPico GP3 (SPI0 MOSI) - LCD SDA (或MOSI)Pico GP4 (SPI0 MISO) - 本例中LCD可能没有MISO不接或接GND。Pico GP5 (任意GPIO) - LCD CS (片选)Pico GP6 (任意GPIO) - LCD DC (数据/命令选择非常重要)Pico GP7 (任意GPIO) - LCD RST (复位)7.2 软件驱动核心这类屏幕的通信协议是“9位SPI”在发送每个字节之前需要先通过DC引脚告诉从机接下来发送的是命令DC0还是数据DC1。void lcd_write_cmd(uint8_t cmd) { gpio_put(PIN_DC, 0); // DC拉低表示命令 gpio_put(PIN_CS, 0); // 选中屏幕 spi_write_blocking(SPI_PORT, cmd, 1); gpio_put(PIN_CS, 1); } void lcd_write_data(uint8_t data) { gpio_put(PIN_DC, 1); // DC拉高表示数据 gpio_put(PIN_CS, 0); spi_write_blocking(SPI_PORT, data, 1); gpio_put(PIN_CS, 1); } void lcd_write_data_bulk(uint8_t *data, uint32_t len) { gpio_put(PIN_DC, 1); gpio_put(PIN_CS, 0); spi_write_blocking(SPI_PORT, data, len); // 使用阻塞传输发送大量数据如图像数据 gpio_put(PIN_CS, 1); }初始化序列屏幕在上电后需要一系列特定的命令和数据进行初始化设置分辨率、颜色格式、扫描方向等。这些序列通常由屏幕厂商提供或者可以从开源驱动库如Adafruit_ST7735中找到。你需要将这些命令和数据用上面的函数依次发送。7.3 性能优化刷新整屏图像160x128像素16位色深需要传输 160 * 128 * 2 40960 字节。用阻塞式spi_write_blocking一次传输CPU会被完全占用几十毫秒。优化方案提升SPI速率在屏幕允许的范围内查手册将SPI时钟提高到20MHz甚至更高。使用DMA将整个帧缓冲区通过DMA发送这是最有效的方法。CPU只在启动DMA和切换缓冲区时介入。局部刷新如果只更新屏幕的一部分只发送那部分区域的数据。通过这个案例你可以看到SPI的配置、软件片选、时序控制、批量数据传输等知识点是如何结合在一起的。掌握了这些你就能驾驭Pico上绝大多数SPI设备了。
返回列表