资讯详情

资讯详情

嵌入式系统第一性原理:SPI、I2C、DMA与GPIO工程实战指南

1. 从第一性原理重新理解嵌入式系统1.1 为什么“点灯”之后反而更迷茫很多人学嵌入式的路径都差不多买一块STM32F103C8T6最小系统板装好Keil5或者CubeMX跟着教程点亮一颗LED然后学GPIO输入输出、外部中断、定时器、串口再往后学I2C、SPI、DMA最后发现自己能跑例程但一旦脱离教程就不知道从哪下手。这个阶段我经历过也见过太多人卡在这里。问题不在于你不够努力而在于学习顺序本身是“自底向上堆外设”缺少一条从“芯片到底怎么工作”到“工程怎么落地”的主线。嵌入式系统的第一性原理其实就三件事数据从哪里来、数据怎么流动、数据怎么被处理。GPIO是数据的最简形态串口/I2C/SPI是数据流动的通道DMA是让数据流动不占用CPU的机制中断是处理异步事件的入口而RTOS或裸机调度决定了这些事件谁先谁后。把这条主线想清楚你会发现STM32也好、ESP32也好、国产GD32也好外设寄存器的名字可能不同但底层逻辑是同一套。这也是我写这篇讲义的核心出发点不按外设手册的顺序讲而是按“一个嵌入式工程师真正需要理解什么”的顺序讲。1.2 这篇讲义适合谁、解决什么问题如果你已经能独立点亮LED、能看懂基本的C语言指针和结构体但面对一个真实项目比如用STM32F103通过SPIDMA读取一颗传感器芯片的数据时不知道从哪开始那这篇内容就是写给你的。我会把SPI、I2C、DMA、GPIO、中断这些热搜词背后的实际工程问题拆开讲包括CubeMX怎么配、代码怎么写、参数怎么算、坑在哪里。如果你是完全零基础建议先补一下GPIO和串口的基础操作再回来看这篇。因为下面很多内容会直接涉及寄存器配置和时序分析没有基础会看得比较吃力。2. 核心通信协议的第一性原理拆解2.1 SPI协议为什么它快快在哪里SPISerial Peripheral Interface的本质是一个环形移位寄存器。主设备和从设备各有一个移位寄存器时钟沿到来时主设备移出一位、从设备移入一位同时从设备也移出一位、主设备移入一位。一个时钟周期完成一位双向交换这就是SPI全双工的根本原理。SPI有四根线SCLK、MOSI、MISO、CS。SCLK由主设备产生CS用来选中从设备。SPI的速度取决于SCLK频率STM32F103的SPI1最高可以到18MHzAPB272MHz分频系数最小为2SPI2/SPI3最高到9MHzAPB136MHz。实际能跑多快取决于从设备芯片的时序要求和你PCB走线的信号完整性。SPI有四种模式由CPOL时钟极性和CPHA时钟相位组合决定模式CPOLCPHA采样时刻Mode 000第一个时钟沿上升沿采样Mode 101第二个时钟沿下降沿采样Mode 210第一个时钟沿下降沿采样Mode 311第二个时钟沿上升沿采样选哪个模式不是你能决定的而是从设备芯片手册规定的。比如W25Q64 SPI Flash支持Mode 0和Mode 3而很多ADC芯片只支持Mode 1或Mode 2。配错了模式读回来的数据全是0xFF或者0x00这是新手最常见的坑之一。注意SPI的片选信号有硬件片选和软件片选两种方式。硬件片选由SPI外设自动控制CS引脚软件片选则是你用GPIO手动拉低拉高。大多数情况下建议用软件片选因为一颗SPI总线上挂多个从设备时硬件片选的管理反而不灵活。CubeMX里把NSS设为Software然后自己指定一个GPIO作为CS引脚这是最常用的做法。2.2 I2C协议开漏输出加两根线凭什么能挂128个设备I2CInter-Integrated Circuit只有两根线SCL和SDA。它的核心设计思想是开漏输出加外部上拉电阻。开漏输出的意思是引脚只能主动拉低不能主动拉高。拉高靠的是外部上拉电阻。这样设计的好处是多个设备可以同时挂在同一根线上任何一个设备拉低总线总线就是低电平不会出现两个设备一个输出高一个输出低导致短路的情况。I2C的通信过程比SPI复杂得多。起始条件SCL高时SDA由高变低、停止条件SCL高时SDA由低变高、7位地址加1位读写位、ACK/NACK应答、数据字节传输每一步都有严格的时序要求。标准模式100kHz快速模式400kHz高速模式3.4MHz。STM32F103的I2C外设支持标准模式和快速模式。I2C上拉电阻的选择是一个经常被忽略但非常关键的问题。上拉电阻太大上升沿太慢高速通信时波形还没拉高就被拉低了通信失败上拉电阻太小灌电流太大可能超过引脚的最大灌电流能力STM32F103单个引脚最大25mA。经验值3.3V系统、100kHz用4.7kΩ400kHz用2.2kΩ到4.7kΩ具体要看总线电容。总线电容越大上拉电阻要越小。如果总线上挂了8个设备每个设备引脚电容约10pF加上PCB走线电容总电容可能到100pF以上这时候4.7kΩ可能就不够了。提示I2C通信失败时第一件事是用示波器看SCL和SDA的波形。如果上升沿明显变缓先换小一点的上拉电阻试试。如果波形正常但通信还是失败检查地址是否正确——很多芯片的7位地址在手册里写的是8位格式最低位是读写位别搞混了。2.3 SPI和I2C的应用场景对比对比项SPII2C线数4根SCLK/MOSI/MISO/CS2根SCL/SDA速度高可达数十MHz低通常100k-400kHz全双工是否多设备每个设备一根CS地址寻址一根总线挂多个典型应用Flash、LCD、ADC、无线模块EEPROM、传感器、RTC、IO扩展硬件复杂度较高引脚多较低引脚少选型逻辑很简单要速度选SPI要省引脚选I2C。比如你要读一颗SPI Flash存日志用SPI你要接一个温度传感器、一个EEPROM、一个IO扩展芯片用I2C更合适因为三颗芯片只需要两根线。3. DMA让数据搬运不占用CPU3.1 DMA到底解决了什么问题假设你用STM32F103的SPI以1MHz的时钟读取一颗传感器每次读2个字节每秒读1000次。如果不用DMACPU需要等待SPI发送完成、等待SPI接收完成、把数据从DR寄存器搬到内存缓冲区。每次操作可能消耗几十个时钟周期1000次就是几万个时钟周期。72MHz的CPU几万个周期听起来不多但如果同时还有串口要处理、定时器要响应、LCD要刷新CPU很快就不够用了。DMADirect Memory Access的作用就是在外设和内存之间直接搬运数据不需要CPU参与。你只需要告诉DMA源地址在哪里、目标地址在哪里、搬多少、搬完之后要不要中断。然后DMA控制器自己完成搬运CPU该干嘛干嘛。STM32F103有2个DMA控制器DMA1有7个通道DMA2有5个通道。每个通道可以配置为外设到内存、内存到外设、内存到内存三种方向。SPI1的RX和TX分别对应DMA1的Channel2和Channel3I2C1的RX和TX对应DMA1的Channel7和Channel6。这些对应关系在参考手册的DMA请求映射表里有CubeMX会自动帮你配好。3.2 CubeMX配置SPIDMA的完整流程以STM32F103C8T6通过SPI1DMA读取一颗SPI传感器为例CubeMX里的配置步骤如下第一步配置时钟。HSE选外部晶振8MHzPLL倍频到72MHzAPB2不分频72MHzAPB1分频系数236MHz。第二步配置SPI1。Mode选Full-Duplex MasterHardware NSS Signal选Disable用软件片选Prescaler选872MHz/89MHzCPOL和CPHA根据从设备手册选Data Size选8位First Bit选MSB。第三步配置DMA。在SPI1的DMA Settings里点Add选SPI1_RXMode选Circular或NormalData Width选BytePriority选Medium。如果需要同时发送再加一个SPI1_TX。第四步配置一个GPIO作为CS引脚。选一个空闲引脚设为GPIO_Output初始电平High。第五步配置NVIC。如果DMA用Normal模式使能DMA通道的中断优先级设低一点比如1或2因为DMA中断不是紧急事件。生成代码后HAL库会自动初始化SPI和DMA。你只需要在业务代码里调用uint8_t rx_buf[10]; HAL_SPI_Receive_DMA(hspi1, rx_buf, 10);但这里有一个关键点HAL_SPI_Receive_DMA是异步的调用之后立刻返回数据还没收完。你需要在DMA传输完成回调函数里处理数据void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { // 数据已经收到rx_buf里了可以处理了 } }3.3 DMA双缓冲和空闲中断的实战用法DMA的Normal模式是搬完指定数量就停Circular模式是搬完自动从头开始。串口接收场景下Circular模式配合空闲中断IDLE是非常经典的用法DMA一直往缓冲区里搬数据不需要CPU干预当一帧数据接收完毕串口总线空闲触发IDLE中断你在中断里计算收到了多少数据然后处理。STM32F103的串口IDLE中断和DMA配合的代码大致如下// 启动DMA接收 HAL_UART_Receive_DMA(huart1, rx_buf, BUF_SIZE); __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); // IDLE中断处理 void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); uint16_t remaining __HAL_DMA_GET_COUNTER(huart1.hdmarx); uint16_t received BUF_SIZE - remaining; // 处理rx_buf中前received个字节 } HAL_UART_IRQHandler(huart1); }DMA双缓冲Double Buffer是更高级的用法适合高速连续数据流。原理是准备两块缓冲区A和BDMA搬A的时候CPU处理B搬B的时候CPU处理A交替进行。STM32F103的DMA不支持硬件双缓冲但可以用两个DMA通道或者手动切换目标地址来模拟。STM32F4/F7/H7的DMA支持硬件双缓冲配置更简单。注意DMA传输完成中断和串口空闲中断的优先级要合理安排。如果DMA中断优先级太高可能打断串口空闲中断的处理如果太低高速数据流下可能来不及处理。一般建议串口空闲中断优先级高于DMA传输完成中断。4. GPIO与板级电路装配的工程细节4.1 GPIO的八种模式到底怎么选STM32的GPIO有八种模式输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、开漏复用、推挽复用。很多人配GPIO的时候凭感觉选结果出现各种奇怪问题。选型逻辑其实很清晰普通LED驱动推挽输出。推挽能主动输出高和低驱动能力强。I2C的SCL和SDA开漏复用输出。因为I2C需要线与功能必须开漏。SPI的SCLK/MOSI推挽复用输出。SPI不需要线与推挽速度更快。SPI的MISO输入浮空或输入上拉。如果从设备在CS无效时MISO高阻建议输入上拉避免浮空干扰。ADC采集模拟输入。模拟输入模式下数字输入通道关闭减少干扰。按键输入输入上拉或输入下拉取决于按键另一端接VCC还是GND。外部中断输入输入上拉或输入下拉不能浮空否则可能反复触发中断。提示GPIO的翻转速度也要注意。STM32F103的GPIO有2MHz、10MHz、50MHz三档输出速度。速度越高EMI辐射越大功耗也越高。普通LED用2MHz就够了SPI时钟用50MHz。配错了不会立刻出问题但EMI测试的时候可能会超标。4.2 板级电路装配中容易忽略的细节嵌入式系统板级电路装配不是把芯片焊上去就完事了。以下几个细节在实际项目中经常被忽略去耦电容的放置。每颗芯片的电源引脚旁边都要放一个100nF的陶瓷电容越近越好。这个电容的作用是滤除高频噪声如果放得离芯片太远走线电感会抵消电容的效果。另外每块板子的电源入口处要放一个10uF到100uF的电解电容或钽电容用来滤除低频纹波。晶振的负载电容。STM32外部晶振需要匹配负载电容典型值是20pF。但实际值要根据晶振手册的负载电容规格来算CL (C1 * C2) / (C1 C2) Cstray其中Cstray是PCB走线的寄生电容一般3pF到5pF。如果负载电容配错了晶振可能起振困难或者频率偏移。BOOT引脚的处理。STM32F103的BOOT0和BOOT1决定了启动模式。正常从Flash启动时BOOT0接GNDBOOT1任意。如果BOOT0悬空可能偶尔进入ISP模式导致程序不运行。所以BOOT0一定要接一个10kΩ下拉电阻到GND。复位电路。NRST引脚内部有上拉电阻但建议外部再加一个100nF电容到GND和一个10kΩ上拉到VCC组成RC复位电路。这样上电时NRST会保持一段低电平确保芯片完成复位。SWD调试接口。SWDIO和SWCLK两根线一定要引出来最好做成4针或5针的排针VCC、GND、SWDIO、SWCLK、可选NRST。没有调试接口程序出问题的时候只能靠串口打印效率极低。4.3 一个真实的板级问题排查记录我之前做过一个基于STM32F103的数字温湿度计I2C接口的温湿度传感器OLED显示蜂鸣器报警。板子焊好之后OLED能显示但温湿度读数一直是0。用逻辑分析仪抓I2C波形发现SCL和SDA都有波形但传感器不应答。排查过程先换了一颗传感器问题依旧。然后查上拉电阻原理图上写的是10kΩ实测也是10kΩ。把上拉电阻换成4.7kΩ问题依旧。最后用示波器看电源纹波发现3.3V电源上有大约200mV的高频纹波频率和OLED刷新频率一致。原来是OLED刷新时电流突变导致电源波动传感器工作不稳定。解决方法在传感器的电源引脚旁边加了一个10uF的钽电容和一个100nF的陶瓷电容问题解决。这个案例说明I2C通信失败不一定是I2C本身的问题电源质量、上拉电阻、总线电容、地址配置都有可能。5. 从裸机到工程实践的进阶路径5.1 裸机程序的典型结构一个典型的STM32裸机程序结构如下int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_SPI1_Init(); MX_USART1_UART_Init(); // 初始化外设 Sensor_Init(); OLED_Init(); while (1) { // 主循环任务 Sensor_Read(); OLED_Display(); HAL_Delay(100); } } // 中断回调 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { // 处理SPI数据 } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // 处理串口数据 }这种结构适合简单项目但任务多了之后主循环会变得很长实时性无法保证。比如HAL_Delay(100)会阻塞CPU 100ms这期间串口数据可能丢失。5.2 状态机裸机程序进阶的必经之路解决主循环阻塞问题的经典方法是状态机。把每个任务拆成多个状态主循环里轮流执行每个任务的一个状态不阻塞。typedef enum { STATE_IDLE, STATE_READ_SENSOR, STATE_WAIT_SENSOR, STATE_DISPLAY, } SystemState; SystemState state STATE_IDLE; while (1) { switch (state) { case STATE_IDLE: Sensor_StartRead(); state STATE_WAIT_SENSOR; break; case STATE_WAIT_SENSOR: if (Sensor_IsDone()) { state STATE_DISPLAY; } break; case STATE_DISPLAY: OLED_Display(); state STATE_IDLE; break; } // 其他任务 UART_Process(); Key_Scan(); }状态机的好处是每个任务都不会阻塞主循环可以快速轮询。缺点是状态多了之后代码会变得复杂这时候就需要RTOS了。5.3 RTOS和裸机怎么选对比项裸机RTOS实时性依赖主循环周期任务优先级抢占代码复杂度简单项目容易复杂项目难复杂项目结构清晰内存占用小需要额外RAM和Flash调试难度低中高适用场景单一任务、简单控制多任务、通信协议栈、GUISTM32F103C8T6只有20KB RAM和64KB Flash跑FreeRTOS没问题但跑Linux不可能。如果你的项目需要同时处理串口协议、SPI传感器、LCD显示、按键、低功耗管理建议上FreeRTOS。如果只是读个传感器、控制个继电器裸机状态机就够了。提示用FreeRTOS的时候中断服务函数里调用的API必须是FromISR结尾的版本比如xQueueSendFromISR而不是xQueueSend。普通版本在中断里调用可能导致任务调度异常。6. 常见问题与排查技巧实录6.1 SPI通信失败排查清单现象可能原因排查方法读回全0xFFMISO未连接或从设备未选中检查CS引脚是否拉低MISO是否接对读回全0x00从设备未上电或时钟未输出示波器看SCLK是否有波形数据错位CPOL/CPHA模式配错对照从设备手册确认模式高速时数据错误SCLK太快或走线太长降低分频系数缩短走线偶尔出错电源噪声或CS时序问题加去耦电容检查CS建立/保持时间6.2 I2C通信失败排查清单现象可能原因排查方法无应答地址错误或设备未上电用逻辑分析仪看地址字节SCL被拉低从设备时钟拉伸检查从设备是否支持时钟拉伸上升沿太慢上拉电阻太大换小电阻减小总线电容通信不稳定电源纹波或地线干扰加去耦电容检查地平面总线死锁主设备复位时从设备还在拉低SDA发送9个时钟脉冲解锁6.3 DMA配置常见错误错误一DMA传输完成但数据不对。原因通常是Data Width配错了。SPI数据寄存器是16位但如果你只用了8位模式DMA的Data Width要选Byte不是Half-Word。配成Half-Word的话每次搬2个字节数据就乱了。错误二DMA传输完成中断不触发。检查NVIC里DMA通道的中断是否使能检查DMA的传输完成中断标志是否清除。HAL库的HAL_DMA_IRQHandler会自动清除标志但如果你自己写了中断服务函数要手动清除。错误三DMA和CPU同时访问同一块内存。DMA搬数据到缓冲区的时候CPU不要去读那块缓冲区否则可能读到一半新一半旧的数据。解决方法是用双缓冲或者等DMA传输完成中断触发后再读。错误四DMA传输完成但SPI还在忙。SPI的DMA传输完成中断触发时SPI的最后一个字节可能还在移位寄存器里没发完。这时候如果立刻拉高CS最后一个字节可能丢失。解决方法是在DMA传输完成中断里等SPI的BSY标志清除再拉高CS。void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { while (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_BSY)); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); }6.4 实操心得几个让我少走弯路的习惯第一逻辑分析仪比示波器更实用。调试I2C、SPI、串口的时候逻辑分析仪能直接解码协议告诉你地址、数据、ACK对不对。几百块钱的8通道逻辑分析仪比几千块的示波器在数字通信调试上更好用。第二每个外设单独测试。不要一次性把SPI、I2C、DMA、中断全配好再调试。先调通GPIO再调通SPI再加DMA再加中断。每加一个功能就验证一次出问题的时候容易定位。第三保留一个串口打印通道。不管项目多简单留一个串口用来打印调试信息。printf重定向到串口出问题的时候打印关键变量比单步调试快得多。第四CubeMX生成的代码要理解再改。CubeMX生成的初始化代码是可靠的但业务代码要自己写。不要直接在CubeMX生成的代码里改而是在USER CODE BEGIN和END之间写这样重新生成代码的时候不会丢失。第五版本管理从第一天开始。用Git管理代码每次调通一个功能就提交一次。嵌入式调试经常需要回退到上一个能工作的版本没有版本管理会非常痛苦。7. 从项目实战反推知识体系7.1 一个SPIDMA读取传感器的完整案例假设有一颗SPI接口的传感器手册要求Mode 0SCLK最高10MHz每次读取需要先发送一个命令字节然后读取2个数据字节。用STM32F103的SPI1DMA实现完整流程如下第一步CubeMX配置SPI1为Full-Duplex MasterPrescaler89MHzCPOL0CPHA0Data Size8bitMSB FirstNSS Software。第二步配置DMA1 Channel2为SPI1_RXNormal模式Byte宽度DMA1 Channel3为SPI1_TXNormal模式Byte宽度。第三步配置一个GPIO为CS初始High。第四步代码实现uint8_t tx_buf[3] {0x01, 0x00, 0x00}; // 命令2个空字节 uint8_t rx_buf[3]; void Sensor_Read(void) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive_DMA(hspi1, tx_buf, rx_buf, 3); } void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { while (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_BSY)); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // rx_buf[1]和rx_buf[2]就是传感器数据 uint16_t value (rx_buf[1] 8) | rx_buf[2]; } }这个案例的关键点SPI是全双工的发送命令的同时也在接收数据。所以tx_buf里命令后面的两个字节是空字节0x00目的是产生时钟让从设备把数据移出来。rx_buf[0]是发送命令时从设备返回的无效数据rx_buf[1]和rx_buf[2]才是有效数据。7.2 知识体系的扩展方向掌握了GPIO、SPI、I2C、DMA、中断之后下一步可以往这些方向扩展通信协议栈Modbus RTU、CAN总线、USB Device。这些协议在工业控制和消费电子里非常常见。CAN总线一般用中断接收高速数据流可以用DMA接收。电源管理低功耗模式Sleep/Stop/Standby、电池电量检测、DC-DC电路设计。基于STM32的数字电源方案比如四开关Buck-Boost双向升降压是电源工程师和嵌入式工程师的交叉领域。RTOS和中间件FreeRTOS、FatFS文件系统、LWIP网络协议栈、OTA升级。STM32 OTA一般通过串口、CAN或无线模块如ESP8266实现核心思路是BootloaderApp双区设计。电机控制PWM、编码器接口、FOC算法。I2C编码器和SPI编码器在机器人关节控制里很常见。信号处理ADC采样、DMA搬运、FFT分析。DMA双缓冲在高速ADC采样里是标配。嵌入式系统的知识体系是网状的不是线性的。你不需要把每个方向都学透但要知道每个方向解决什么问题需要的时候能快速捡起来。这也是第一性原理的价值底层逻辑不变上层应用千变万化理解了底层学新东西就快。我个人在实际项目中的体会是嵌入式工程师的核心竞争力不在于会多少种芯片而在于能不能快速定位问题、能不能在资源受限的条件下做出合理的取舍。一颗STM32F10320KB RAM、64KB Flash、72MHz主频能做的事情远超很多人的想象。关键是你得知道每一行代码在做什么每一个配置项为什么这么选。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →