1. 引言
在嵌入式开发的学习道路上,从裸机点灯到驱动外设,再到理解中断与总线协议,是一条必经的进阶之路。本文以 ARM 平台 + I2C 中断驱动 + DHT11 温湿度传感器 为主线,记录一次完整的嵌入式学习与实战总结,涵盖硬件原理、驱动框架、中断机制、协议解析与代码实现,希望能为同样在路上的读者提供一份可参考的路线图。
2. 学习目标与整体框架
本次学习围绕三个核心关键词展开:
- ARM:以 ARM Cortex-M 系列(如 STM32F103)为硬件平台,理解其内核架构、中断控制器(NVIC)与 GPIO 复用。
- I2C 中断驱动:掌握 I2C 总线协议,并学会用中断方式(而非轮询)驱动 I2C 外设,提升 CPU 利用率。
- DHT11:一款单总线数字温湿度传感器,通过自定义时序协议与主机通信,是理解时序驱动与位级解析的绝佳案例。
整体学习路径如下:
#mermaid-svg-9wh4nqZmI6vTcnp9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9wh4nqZmI6vTcnp9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9wh4nqZmI6vTcnp9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9wh4nqZmI6vTcnp9 .error-icon{fill:#552222;}#mermaid-svg-9wh4nqZmI6vTcnp9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9wh4nqZmI6vTcnp9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9wh4nqZmI6vTcnp9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9wh4nqZmI6vTcnp9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9wh4nqZmI6vTcnp9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9wh4nqZmI6vTcnp9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9wh4nqZmI6vTcnp9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9wh4nqZmI6vTcnp9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9wh4nqZmI6vTcnp9 .marker.cross{stroke:#333333;}#mermaid-svg-9wh4nqZmI6vTcnp9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9wh4nqZmI6vTcnp9 p{margin:0;}#mermaid-svg-9wh4nqZmI6vTcnp9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9wh4nqZmI6vTcnp9 .cluster-label text{fill:#333;}#mermaid-svg-9wh4nqZmI6vTcnp9 .cluster-label span{color:#333;}#mermaid-svg-9wh4nqZmI6vTcnp9 .cluster-label span p{background-color:transparent;}#mermaid-svg-9wh4nqZmI6vTcnp9 .label text,#mermaid-svg-9wh4nqZmI6vTcnp9 span{fill:#333;color:#333;}#mermaid-svg-9wh4nqZmI6vTcnp9 .node rect,#mermaid-svg-9wh4nqZmI6vTcnp9 .node circle,#mermaid-svg-9wh4nqZmI6vTcnp9 .node ellipse,#mermaid-svg-9wh4nqZmI6vTcnp9 .node polygon,#mermaid-svg-9wh4nqZmI6vTcnp9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9wh4nqZmI6vTcnp9 .rough-node .label text,#mermaid-svg-9wh4nqZmI6vTcnp9 .node .label text,#mermaid-svg-9wh4nqZmI6vTcnp9 .image-shape .label,#mermaid-svg-9wh4nqZmI6vTcnp9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-9wh4nqZmI6vTcnp9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9wh4nqZmI6vTcnp9 .rough-node .label,#mermaid-svg-9wh4nqZmI6vTcnp9 .node .label,#mermaid-svg-9wh4nqZmI6vTcnp9 .image-shape .label,#mermaid-svg-9wh4nqZmI6vTcnp9 .icon-shape .label{text-align:center;}#mermaid-svg-9wh4nqZmI6vTcnp9 .node.clickable{cursor:pointer;}#mermaid-svg-9wh4nqZmI6vTcnp9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9wh4nqZmI6vTcnp9 .arrowheadPath{fill:#333333;}#mermaid-svg-9wh4nqZmI6vTcnp9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9wh4nqZmI6vTcnp9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9wh4nqZmI6vTcnp9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9wh4nqZmI6vTcnp9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9wh4nqZmI6vTcnp9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9wh4nqZmI6vTcnp9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9wh4nqZmI6vTcnp9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9wh4nqZmI6vTcnp9 .cluster text{fill:#333;}#mermaid-svg-9wh4nqZmI6vTcnp9 .cluster span{color:#333;}#mermaid-svg-9wh4nqZmI6vTcnp9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9wh4nqZmI6vTcnp9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9wh4nqZmI6vTcnp9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-9wh4nqZmI6vTcnp9 .icon-shape,#mermaid-svg-9wh4nqZmI6vTcnp9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9wh4nqZmI6vTcnp9 .icon-shape p,#mermaid-svg-9wh4nqZmI6vTcnp9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9wh4nqZmI6vTcnp9 .icon-shape .label rect,#mermaid-svg-9wh4nqZmI6vTcnp9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9wh4nqZmI6vTcnp9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9wh4nqZmI6vTcnp9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9wh4nqZmI6vTcnp9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ARM 平台基础
GPIO 与中断系统
I2C 协议与中断驱动
DHT11 单总线时序
综合实战:数据采集与显示
3. ARM 平台与开发环境准备
3.1 硬件平台
本次实验选用 STM32F103C8T6(ARM Cortex-M3 内核),搭配:
- I2C1 总线:用于连接 EEPROM(AT24C02)等标准 I2C 设备,验证中断驱动。
- 普通 GPIO(如 PA0):用于连接 DHT11 数据引脚。
- 串口(USART1):用于打印采集到的温湿度数据。
3.2 软件环境
- 集成开发环境:Keil MDK 或 STM32CubeIDE。
- 固件库:标准外设库(SPL)或 HAL 库。
- 调试工具:ST-Link / J-Link,串口调试助手。
建议先跑通一个 GPIO 点灯例程,再逐步引入中断与总线协议,避免一步到位带来的挫败感。
4. 中断系统:嵌入式驱动的灵魂
4.1 什么是中断
中断是 CPU 处理异步事件的机制。当外设(如定时器、串口、I2C)发生特定事件时,会向 CPU 发出中断请求,CPU 暂停当前任务,跳转到对应的中断服务函数(ISR)处理,处理完毕后再返回原任务继续执行。
4.2 ARM 中断控制器(NVIC)
在 Cortex-M 内核中,NVIC(嵌套向量中断控制器) 负责中断的使能、优先级管理与响应。每个外设中断都有独立的 IRQn 编号,通过库函数配置即可:
c
/* 使能 I2C1 事件中断 */
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = I2C1_EV_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
4.3 中断 vs 轮询
| 方式 | CPU 占用 | 实时性 | 适用场景 |
|---|---|---|---|
| 轮询 | 高(持续查询标志位) | 一般 | 简单、低速外设 |
| 中断 | 低(事件触发才处理) | 高 | 高速、多任务场景 |
学习心得:中断让 CPU 从「忙等」中解放出来,是嵌入式系统从「裸机」走向「实时操作系统」的关键一步。
5. I2C 总线协议与中断驱动实战
5.1 I2C 协议基础
I2C(Inter-Integrated Circuit)是一种两线制串行总线:
- SCL:时钟线,由主机控制。
- SDA:数据线,双向传输。
通信流程包括:起始条件(START)、地址帧、数据帧、应答位(ACK/NACK)、停止条件(STOP)。
5.2 中断方式驱动 I2C
相比轮询方式,中断驱动 I2C 的核心思路是:发起传输后立即返回,在中断服务函数中根据事件标志推进状态机。
以 HAL 库为例,中断发送数据的核心代码如下:
c
/* 主机以中断方式向从机地址 0xA0 发送 2 字节数据 */
uint8_t pData[2] = {0x12, 0x34};
HAL_I2C_Master_Transmit_IT(&hi2c1, 0xA0, pData, 2);
对应的中断回调函数:
c
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
if (hi2c == &hi2c1)
{
/* 传输完成,置标志位供主循环处理 */
g_i2c_tx_done = 1;
}
}
5.3 中断驱动的状态机设计
实际项目中,一次完整的 I2C 中断读写往往需要多个步骤,建议用状态机管理:
c
typedef enum {
I2C_IDLE,
I2C_START,
I2C_SEND_ADDR,
I2C_SEND_DATA,
I2C_STOP,
I2C_DONE
} I2C_State_t;
学习心得:中断驱动看似复杂,但一旦掌握「事件 + 状态机」的思维,就能优雅地处理各种异步外设,也为后续学习 RTOS 的事件驱动模型打下基础。
6. DHT11 温湿度传感器:单总线时序解析
6.1 DHT11 简介
DHT11 是一款数字温湿度传感器,采用**单总线(Single-Wire)**通信,仅需一根数据线即可完成双向通信。测量范围:湿度 20%--90% RH,温度 0--50℃,精度一般,但胜在便宜、简单,非常适合入门。
6.2 通信时序
DHT11 的通信由主机发起,时序如下:
- 主机拉低数据线至少 18ms,然后释放(拉高),启动通信。
- DHT11 响应:先拉低 80us,再拉高 80us,表示准备好发送数据。
- 随后连续发送 40 位数据(5 字节):湿度整数、湿度小数、温度整数、温度小数、校验和。
- 每位数据通过高电平持续时间区分:26--28us 表示「0」,70us 表示「1」。
6.3 位级读取代码实现
读取一位的核心逻辑(基于 GPIO 中断或定时器捕获):
c
uint8_t DHT11_ReadBit(void)
{
uint8_t bit = 0;
/* 等待高电平开始 */
while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_PIN) == GPIO_PIN_RESET);
/* 延时 40us 后采样 */
Delay_us(40);
if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_PIN) == GPIO_PIN_SET)
{
bit = 1; /* 高电平持续较长,表示 '1' */
}
/* 等待高电平结束 */
while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_PIN) == GPIO_PIN_SET);
return bit;
}
6.4 数据校验
接收完 40 位数据后,需校验:
c
if ((hum_int + hum_dec + temp_int + temp_dec) == checksum)
{
/* 数据有效 */
}
else
{
/* 数据错误,丢弃本次读取 */
}
学习心得:DHT11 的时序解析让我深刻理解了「时序就是协议」这句话。没有硬件 SPI/I2C 控制器时,用 GPIO 模拟时序是嵌入式开发的必备技能。
7. 综合实战:I2C 中断 + DHT11 数据采集系统
7.1 系统设计
将两部分结合,构建一个完整的数据采集系统:
- I2C 中断驱动:负责与 EEPROM 通信,保存温湿度历史数据。
- DHT11:负责实时采集温湿度。
- 串口:打印数据,便于观察。
7.2 主程序流程
c
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();
while (1)
{
/* 1. 读取 DHT11 温湿度 */
if (DHT11_ReadData(&hum, &temp) == DHT11_OK)
{
/* 2. 通过 I2C 中断方式写入 EEPROM */
uint8_t buf[2] = {temp, hum};
HAL_I2C_Mem_Write_IT(&hi2c1, EEPROM_ADDR, 0x00, buf, 2);
/* 3. 串口打印 */
printf("Temp: %d.%d C, Hum: %d.%d %%\r\n",
temp / 10, temp % 10, hum / 10, hum % 10);
}
HAL_Delay(2000); /* DHT11 采样间隔建议 >= 1s */
}
}
7.3 中断回调完善
在 I2C 写完成回调中,可以继续发起读操作,实现「写后读」验证:
c
void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
if (hi2c == &hi2c1)
{
/* 写完成,接着发起读验证 */
HAL_I2C_Mem_Read_IT(&hi2c1, EEPROM_ADDR, 0x00, g_read_buf, 2);
}
}
8. 踩坑记录与调试心得
8.1 常见问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| I2C 总线卡死 | SCL/SDA 未加上拉电阻 | 外接 4.7kΩ 上拉电阻 |
| DHT11 一直无响应 | 起始信号时序不满足 | 确保低电平时间 >= 18ms |
| 数据校验失败 | 位读取延时不准 | 用定时器或 DWT 精确延时 |
| 中断不触发 | NVIC 未使能或优先级配置错误 | 检查 NVIC 配置与中断标志清除 |
8.2 调试技巧
- 逻辑分析仪:抓取 I2C 与单总线波形,直观对比时序。
- 串口打印:在关键节点打印状态标志,快速定位卡在哪一步。
- 分步验证:先单独验证 I2C 读写 EEPROM,再单独验证 DHT11,最后联调。