欢迎光临
我们一直在努力

嵌入式系统核心知识讲义——从第一性原理到工程实践

SPI模式0与模式3的时钟极性和相位配置、CPOL、CPHA是什么、怎么配置 硬件与模拟I2C的区别 时序余量 常用的硬件控制器(理解时钟配置和FIFO深度) I2C死锁的原因,用复位机制、软件看门狗怎么解决 写个带DMA的SPI高速刷屏驱动(位带操作、高效传输) flag、temp逻辑 怎么用Doxygen生成文档(结构化注释) 寄存器、位操作、时钟树怎么理顺 I2C时序怎么实现 DMA传输原理 TOS任务调度、、 IPC通信机制 任务优先级 CAN滤波原理 (功能、时序、功耗、稳定性)

一、SPI模式0与模式3:时钟极性与相位

1.1 第一性原理:同步通信的本质

SPI的本质是一个移位寄存器环。主机和从机各有一个8位移位寄存器,通过MOSI(主出从入)和MISO(主入从出)首尾相连。每来一个时钟脉冲,数据就右移一位。8个脉冲后,两个寄存器的内容就完整交换了。

这里有一个物理约束:数据必须在时钟边沿到来之前“站稳” ,这就是建立时间;采样之后还要“站一会儿”,这就是保持时间。

1.2 CPOL与CPHA的物理含义

CPOL定义空闲时SCK的电平:

  • CPOL=0:空闲低电平(像一根“躺平”的线,拉一下才动)

  • CPOL=1:空闲高电平

CPHA定义数据在哪个边沿被采样:

  • CPHA=0:第一个边沿采样

  • CPHA=1:第二个边沿采样

这两个参数组合出四种模式,但实际工程中模式0和模式3最常用,因为它们的采样边沿分别是上升沿和下降沿,而数据切换边沿落在另一个方向上,天然满足建立/保持时间要求。

特性模式0 (CPOL=0, CPHA=0)模式3 (CPOL=1, CPHA=1)
空闲状态 SCK低电平 SCK高电平
数据采样边沿 SCK上升沿 SCK下降沿
数据切换边沿 SCK下降沿 SCK上升沿
首次数据变化 第一个SCK边沿前 第一个SCK边沿后

一个关键洞察:模式0和模式3的数据变化都发生在下降沿,采样都发生在上升沿(模式3的“下降沿切换”是在空闲高电平之后的第一个下降沿)。这意味着从数据线的角度来看,两种模式的波形实际上是一样的,只是时钟空闲电平不同。

1.3 STM32 HAL库配置示例

(代码部分由AI生成)

代码c

// 模式0配置
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0

// 模式3配置
hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; // CPOL=1
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA=1

配置后务必确认主从设备模式一致。W25Q64等SPI Flash通常同时支持模式0和模式3,默认用模式0即可。

二、时序余量:建立时间和保持时间的数学

2.1 第一性原理:为什么高速信号会“读错”

数字电路采样数据时,接收端在时钟边沿附近需要一个“稳定窗口”。这个窗口由两个参数决定:

  • 建立时间(Setup Time, tSU) :数据必须在采样边沿到来前稳定的最短时间。物理原因是接收端内部寄生电容需要时间充电到阈值电压。

  • 保持时间(Hold Time, tTH) :采样边沿之后数据必须继续稳定的最短时间。物理原因是接收端锁存器需要时间完成状态固化。

2.2 时序余量计算公式

(AI生成)

text

建立时间余量 = 半个SCK周期 – 发送端输出延迟(tOD_max) – 从设备建立时间要求(tSU_slave) – PCB走线延迟差
保持时间余量 = 发送端保持时间(tOH_min) – 从设备保持时间要求(tTH_slave) – PCB走线延迟差

余量必须为正,否则通信不可靠。

举个例子:SCK频率1MHz(周期1000ns),发送端tOD_max=20ns,从设备tSU=30ns,PCB延迟差=5ns。则建立余量 = 500 – 20 – 30 – 5 = 445ns,非常充裕。但如果SCK升到20MHz(周期50ns),建立余量 = 25 – 20 – 30 – 5 = -30ns,负值意味着数据还没稳定就被采样了,必然出错。

这就是为什么高速SPI需要缩短走线、降低从设备侧输入电容、甚至使用源同步时钟。

三、硬件I2C与模拟I2C的区别

3.1 第一性原理:谁在“打拍子”

I2C本质上是一种开漏总线,SCL和SDA线上各有一个上拉电阻,任何设备只能把线拉低,不能主动拉高。这种设计的数学本质是:总线的状态 = 所有设备输出的逻辑“与”。

硬件I2C:MCU内部有专用I2C外设电路,时钟由硬件分频器产生,起始/停止条件、ACK/NACK由状态机自动处理。CPU只需配置寄存器。

模拟I2C:用两个普通GPIO,通过代码控制引脚电平翻转来“手工”产生时序。CPU必须逐位参与。

3.2 核心对比

维度硬件I2C模拟I2C
CPU占用 低,传输过程无需干预 高,逐位循环
时序精度 硬件生成,不受中断影响 受中断/主频影响,可能有抖动
引脚灵活性 固定引脚 任意GPIO
兼容性 对非标准从设备可能无法适配 时序可完全自定义
调试难度 需查看寄存器状态 逻辑分析仪看波形即可
移植性 依赖MCU厂商实现 代码逻辑清晰,跨平台容易

模拟I2C在实际工程中更常用,原因有二:一是引脚灵活,PCB布线方便;二是某些MCU的硬件I2C存在已知Bug,需要软件workaround,反而增加复杂度。

3.3 模拟I2C的时序核心

(代码部分由AI生成)

c

// 起始条件:SCL高时,SDA从高变低
void i2c_start(void) {
SDA_OUT(); SDA_H(); SCL_H(); delay_us(4);
SDA_L(); delay_us(4); SCL_L(); // 拉低SCL准备传输
}

// 发送一个字节
void i2c_send_byte(uint8_t byte) {
SDA_OUT();
for (int i = 0; i < 8; i++) {
SCL_L();
if (byte & 0x80) SDA_H(); else SDA_L();
byte <<= 1; delay_us(2);
SCL_H(); delay_us(2); // 上升沿从设备采样
}
SCL_L(); // 释放SCL等待ACK
}

四、I2C死锁:原因与恢复机制

4.1 第一性原理:互等的“囚徒困境”

I2C死锁的本质是两个状态机互相等待。具体场景:

主机发送完8位数据后,在第9个时钟周期拉低SCL,等待从机ACK。从机检测到SCL低电平后,将SDA拉低作为ACK。此时如果主机异常复位,SCL被释放为高电平。从机继续拉低SDA等待SCL变低以结束ACK——但主机复位后检测到SDA为低,认为总线被占用,等待SDA变高。主机等从机放SDA,从机等主机拉SCL,两者永远互等。

用数学语言说:这是一个两个进程互相持有对方需要的资源并等待的经典死锁模型。

4.2 恢复方法

软件恢复(最常用) :主机复位后检测SDA是否为低,如果是,则控制SCL产生9个时钟脉冲。从机在第9个脉冲后会完成被挂起的ACK操作并释放SDA。

(代码部分由AI生成)

c

void i2c_bus_recovery(void) {
GPIO_InitTypeDef gpio;
// 将SCL和SDA临时切换为开漏输出
gpio.GPIO_Mode = GPIO_Mode_Out_OD;
// … 配置GPIO …

if (SDA_READ() == 0) { // 检测到死锁
for (int i = 0; i < 9; i++) {
SCL_L(); delay_us(5);
SCL_H(); delay_us(5);
}
// 产生停止条件
SDA_L(); delay_us(5);
SCL_H(); delay_us(5);
SDA_H(); delay_us(5);
}
// 恢复GPIO到I2C功能
}

硬件措施:

  • 选择带复位引脚的I2C从器件,主复位时同步复位从机

  • 用MOS管控制从设备电源,主复位时切断从设备电源再恢复

五、DMA传输原理与FIFO深度

5.1 第一性原理:CPU是“大脑”,DMA是“搬运工”

CPU做数据搬运的代价极高:取指令、译码、执行、写回,每条指令消耗多个时钟周期。而DMA的本质是一个专用的硬件状态机,它有自己的地址计数器和数据缓冲寄存器,能直接在总线上完成“从源地址读→写入目标地址→地址递增→计数器递减”的循环,不经过CPU的指令流水线。

5.2 总线仲裁

DMA和CPU共享系统总线,需要仲裁器分配访问权。典型的循环仲裁算法确保CPU至少获得一半的总线带宽。在STM32中,DMA请求有软件优先级(低/中/高/极高)和硬件优先级(通道号越低越高)。

5.3 FIFO深度与突发传输

DMA的FIFO是一个临时缓冲区。STM32的每个DMA Stream有4字(4×32bit)的FIFO。FIFO阈值决定了“攒多少数据才触发一次总线传输”:

FIFO阈值行为
1/4(1字) 每收到1个字就搬一次,总线频繁但延迟低
1/2(2字) 攒2个字搬一次
3/4(3字) 攒3个字搬一次
满(4字) 攒满4个字搬一次,总线效率最高

突发传输(Burst) 是配合FIFO使用的:当FIFO积累到阈值后,DMA可以一次性以突发方式在总线上传输多个数据,减少总线仲裁开销。FIFO阈值必须是突发数据量的整数倍。

六、写一个带DMA的SPI高速刷屏驱动

6.1 系统架构

(AI生成)

text

[帧缓冲区 SRAM] –(DMA)–> [SPI TX寄存器] –(SPI硬件)–> [LCD显示屏]

CPU只在开始时配置,传输中完全解放

6.2 位带操作简介

STM32的位带(Bit-Band)机制将SRAM和外设区的每个位映射到一个独立的32位地址。对别名地址的读写,硬件自动转换为对原始位的读-改-写操作:

(代码部分由AI生成)

c

#define BITBAND(addr, bit) ((0x22000000 + ((addr – 0x20000000) * 32) + (bit * 4)))

// 使用示例:直接设置GPIO某一位,无需读-改-写
#define LED_PIN BITBAND(0x20000000, 5) // SRAM中某变量的第5位
*(volatile uint32_t *)LED_PIN = 1;

6.3 DMA+SPI刷屏代码

(代码部分由AI生成)

c

// 全局帧缓冲区(位带可访问)
uint16_t frame_buffer[LCD_WIDTH * LCD_HEIGHT] __attribute__((aligned(4)));

void lcd_dma_flush(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
// 1. 设置LCD窗口
lcd_set_window(x1, y1, x2, y2);

// 2. 发送写GRAM命令
lcd_write_cmd(0x2C);

// 3. 启动DMA传输
uint32_t pixel_count = (x2 – x1 + 1) * (y2 – y1 + 1);

// 配置DMA:内存→SPI数据寄存器
DMA1_Stream3->PAR = (uint32_t)&SPI1->DR; // 外设地址
DMA1_Stream3->M0AR = (uint32_t)frame_buffer; // 内存地址
DMA1_Stream3->NDTR = pixel_count; // 传输数量
DMA1_Stream3->CR = DMA_SxCR_CHSEL_0 | // 通道选择
DMA_SxCR_MINC | // 内存地址递增
DMA_SxCR_DIR_0 | // 内存→外设
DMA_SxCR_TCIE; // 传输完成中断
DMA1_Stream3->CR |= DMA_SxCR_EN; // 使能DMA

// 4. 使能SPI的DMA发送请求
SPI1->CR2 |= SPI_CR2_TXDMAEN;

// CPU现在可以去处理其他任务,传输完成后DMA触发中断
}

// DMA传输完成中断
void DMA1_Stream3_IRQHandler(void) {
if (DMA1->HISR & DMA_HISR_TCIF3) {
DMA1->HIFCR = DMA_HIFCR_CTCIF3; // 清除标志
SPI1->CR2 &= ~SPI_CR2_TXDMAEN; // 关闭SPI DMA请求
lcd_flush_complete_callback(); // 通知上层
}
}

实测中,使用DMA+SPI刷一块240×320的屏,传输时间约等于 像素数 × 16bit ÷ SPI速率,CPU占用接近于零。

七、flag与temp逻辑:状态机的艺术

7.1 第一性原理:为什么需要flag

flag的本质是把“过去发生的事件”编码成一个持久化的状态位。CPU是顺序执行的,但外部事件(按键按下、数据到达、超时)是异步发生的。flag就像一个“信箱”,事件发生者在里面留一张纸条,主循环来取。

7.2 典型模式

(代码部分由AI生成)

c

volatile uint8_t data_ready_flag = 0;

// 中断服务函数中
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) {
rx_buffer[rx_index++] = USART1->DR;
if (rx_index >= EXPECTED_LEN) {
data_ready_flag = 1; // 设置flag
}
}
}

// 主循环中
void main_loop(void) {
if (data_ready_flag) {
data_ready_flag = 0; // 清除flag
process_data(rx_buffer); // 处理数据
}
}

关键原则:flag的置位和清除必须考虑原子性。如果中断可能在任何时刻发生,用 __disable_irq() / __enable_irq() 保护,或者用位带操作确保单条指令完成。

八、Doxygen生成文档

8.1 第一性原理:为什么需要结构化注释

代码本身就是文档,但代码只表达“怎么做”,不表达“为什么这么做”、“参数约束是什么”、“返回值在什么情况下有效”。Doxygen的注释块是一种嵌入在代码中的结构化元数据,既能被人阅读,也能被工具提取生成HTML/PDF。

8.2 注释风格

(代码部分由AI生成)

c

/**
* @file spi_driver.h
* @brief SPI DMA刷屏驱动接口
* @author Your Name
* @date 2026-09-13
*/

/**
* @brief 向LCD指定区域刷入帧缓冲数据
* @param x1 起始列坐标(0~239)
* @param y1 起始行坐标(0~319)
* @param x2 结束列坐标(0~239)
* @param y2 结束行坐标(0~319)
* @retval 0 成功
* @retval -1 参数越界
* @note 调用前需确保帧缓冲区数据已填充完毕
* @warning 本函数非阻塞,传输完成通过回调通知
*/
int lcd_dma_flush(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);

常用命令:@param、@retval、@note、@warning、@code/@endcode、@see。生成文档只需在项目根目录运行 doxygen Doxyfile。

九、寄存器、位操作与时钟树

9.1 时钟树的第一性原理

MCU内部有多个时钟源(HSI、HSE、PLL),各外设挂在不同总线上(AHB、APB1、APB2)。外设要工作,必须先“开闸放水”——使能对应的时钟。

c

// STM32F4示例:使能SPI1时钟
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; // 打开SPI1的时钟阀门

时钟树的核心逻辑:HSE(外部晶振)→ PLL倍频 → SYSCLK → AHB分频 → APB分频 → 外设时钟。理解时钟树的关键是画一张图,标出每个分频器和选择器。

9.2 寄存器位操作

(代码部分由AI生成)

c

// 置位某位(不影响其他位)
REG |= (1 << BIT_POS);

// 清零某位
REG &= ~(1 << BIT_POS);

// 读取某位
if (REG & (1 << BIT_POS)) { … }

// 修改多位
REG = (REG & ~MASK) | (NEW_VALUE & MASK);

十、FreeRTOS:任务调度、IPC与优先级

10.1 第一性原理:单核上的“伪并行”

FreeRTOS的本质是在单个CPU核上,通过快速切换任务上下文,制造“多个任务同时运行”的幻觉。物理事实是:任何时刻只有一个任务占用CPU。调度器的工作就是决定“下一个时刻谁上”。

10.2 抢占式调度

FreeRTOS采用基于优先级的全抢占式调度:高优先级任务就绪时,立即打断低优先级任务。同优先级任务通过时间片轮转共享CPU。

任务状态迁移:

(AI生成)

text

创建 → 就绪 → 运行 → 阻塞(等待事件/延时)→ 就绪

挂起(对调度器不可见)

10.3 IPC通信机制

机制用途数学本质
队列(Queue) 任务间传数据 有界缓冲区,生产者-消费者模型
二值信号量 事件通知 一个bit的状态,0=无事件,1=有事件
计数信号量 资源计数 整数计数器,P操作减1,V操作加1
互斥量(Mutex) 保护共享资源 带优先级继承的二值信号量

10.4 优先级反转

优先级反转是一个经典的实时系统问题:低优先级任务L持有互斥锁 → 高优先级任务H就绪,需要同一把锁,被阻塞 → 中优先级任务M就绪,抢占L → M执行期间,H无法获得锁,间接被M“阻塞”。

解决方案:优先级继承。当H等待L持有的锁时,L的优先级被临时提升到H的优先级,使M无法抢占L。L释放锁后,优先级恢复。

(代码部分由AI生成)

c

// 创建互斥量(自动支持优先级继承)
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();

// 任务中使用
xSemaphoreTake(xMutex, portMAX_DELAY);
// … 访问共享资源 …
xSemaphoreGive(xMutex);

十一、CAN滤波原理

11.1 第一性原理:广播总线上“只要我关心的”

CAN是广播总线,每个节点都能收到所有帧。如果每个节点都处理所有帧,CPU会被大量无用中断淹没。滤波器的本质是在硬件层做“条件匹配”,只把匹配的帧放入接收FIFO,不匹配的直接丢弃。

11.2 ACR/AMR机制

滤波器由两个寄存器配合:

  • ACR(验收代码寄存器) :期望的ID值

  • AMR(验收屏蔽寄存器) :哪些位需要匹配

规则:AMR=0的位,ACR必须与ID完全一致才接收;AMR=1的位,ID对应位为0或1都接收。

举例:ACR=0b11101111,AMR=0b00000000,则ID必须精确等于0b11101111。如果AMR=0b00010000,则ID的第4位可以为0或1,其余位必须匹配。

11.3 位定时与同步

CAN是异步通信,每个位的采样点位置由位定时参数决定。一个位时间分为四个段:同步段、传播段、相位缓冲段1、相位缓冲段2。采样点位于Phase_Seg1和Phase_Seg2之间。

SJW(同步跳转宽度) 决定了每次重同步时可以调整的最大TQ数,一般为1~4个TQ。采样点通常设置在位时间的75%~87.5%处,以补偿传播延迟和振荡器误差。

十二、实际应用建议

12.1 通信接口选型

场景推荐方案理由
高速外设(LCD、Flash) SPI + DMA 速率可达几十MHz,DMA解放CPU
多设备低速总线(传感器、EEPROM) I2C(模拟) 引脚灵活,多设备共享总线
多节点可靠通信(汽车、工业) CAN 差分信号抗干扰,硬件滤波
点对点调试/日志 UART 最简单,成本最低

12.2 调试方法论

  • SPI通信失败:首先用示波器看SCK是否有波形,再看MOSI数据是否在正确边沿变化。确认主从CPOL/CPHA一致。

  • I2C死锁:在复位后先执行总线恢复程序再初始化。用逻辑分析仪抓SDA/SCL波形判断是否死锁。

  • DMA不工作:检查时钟是否使能、通道映射是否正确、传输完成标志是否清除。

  • FreeRTOS任务不调度:检查栈大小是否足够(栈溢出会HardFault)、优先级配置是否合理、是否在中断中调用了非FromISR版本的API。

12.3 功耗优化

  • 不需要的外设时钟及时关闭(RCC->APBxENR &= ~…)

  • 使用DMA代替CPU轮询,CPU可以进入低功耗模式

  • 合理使用MCU的Sleep/Stop/Standby模式,唤醒源配置正确

  • 外设引脚未使用时配置为模拟输入,避免悬空导致的漏电流

12.4 稳定性建议

  • 所有中断服务函数尽量短,耗时操作交给任务处理

  • 共享变量访问加临界区保护

  • 看门狗喂狗时间要留足余量,避免正常工作时误复位

  • 通信协议加校验(CRC/校验和),重要数据加重传机制

月落乌啼霜满天,江枫渔火对愁眠。——张继《枫桥夜泊》

赞(0)
未经允许不得转载:171主机测评 » 嵌入式系统核心知识讲义——从第一性原理到工程实践
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址