TL;DR(太长不看版):本文面向刚接触 STM32F103 的开发者,用流水灯、蜂鸣器和传感器三个经典实验,帮你建立一套可复用的嵌入式代码理解框架。核心观点是:所有外设初始化都遵循"时钟 → 模式 → 初始状态"的统一模板;流水灯和蜂鸣器属于"输出型"外设,代码骨架同构(写引脚 + 延时/频率控制);传感器属于"输入型"外设,多出"触发 → 读取 → 解析 → 输出"的链路。读完你就能快速读懂任何 STM32 外设的初始化与主循环代码,为后续学习串口、定时器中断和 FreeRTOS 打好基础。
1. 引言
对于刚接触 STM32F103 的开发者来说,流水灯、蜂鸣器和传感器是三个最经典的入门实验。它们看似简单,却几乎覆盖了嵌入式开发的核心要素:GPIO 输出控制、定时器延时、外部中断、ADC 采集以及传感器数据的读取与处理。
本文不打算逐行贴出完整工程(那会淹没重点),而是从代码构成与基本结构的角度,帮你建立一套可复用的理解框架。读完你会明白:为什么流水灯和蜂鸣器的代码骨架几乎一样?传感器代码又为什么多出"初始化—读取—转换—输出"这几步?理解了这些,后续学习串口、定时器中断、FreeRTOS 都会顺畅很多。
2. 开发环境与工程结构
在分析代码之前,先明确我们面对的是怎样的工程。STM32F103 的开发通常有两种主流方式:
- 标准外设库(StdPeriph_Lib):经典、资料多,适合学习寄存器与库函数的对应关系。
- HAL 库(STM32CubeMX 生成):图形化配置,代码可读性好,适合快速上手和工程化开发。
无论用哪种库,一个完整的工程都包含以下固定部分:
Project/
├── Core/
│ ├── Inc/ # 头文件(main.h、gpio.h、tim.h 等)
│ └── Src/ # 源文件(main.c、gpio.c、tim.c 等)
├── Drivers/
│ ├── CMSIS/ # 内核相关定义
│ └── STM32F1xx_HAL_Driver/ # HAL 库源码
└── main.c # 主函数入口
理解这个结构很重要:外设的"初始化"和"使用"是分离的。初始化代码通常由 CubeMX 生成在 gpio.c、tim.c 等文件中,而业务逻辑写在 main.c 的 while(1) 循环里。这也是初学者最容易困惑的地方——明明在 main.c 里没看到引脚配置,为什么灯能亮?因为配置在 MX_GPIO_Init() 里被调用了。
3. 流水灯代码的结构化理解
3.1 初始化阶段
流水灯的本质是多个 GPIO 引脚按顺序输出高低电平。初始化代码的核心是 MX_GPIO_Init(),它做的事情可以归纳为三步:
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 1. 使能 GPIO 端口时钟
__HAL_RCC_GPIOC_CLK_ENABLE();
// 2. 配置引脚模式:推挽输出、无上下拉、低速
GPIO_InitStruct.Pin = LED1_Pin | LED2_Pin | LED3_Pin | LED4_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
// 3. 初始状态:全部熄灭(高电平熄灭,取决于硬件接法)
HAL_GPIO_WritePin(GPIOC, LED1_Pin | LED2_Pin | LED3_Pin | LED4_Pin, GPIO_PIN_SET);
}
这段代码的结构可以抽象为**“时钟 → 模式 → 初始状态”**三步曲。几乎所有 GPIO 外设(按键、蜂鸣器、传感器使能脚)的初始化都是这个套路。
3.2 主循环中的流水逻辑
流水效果的核心在 while(1) 循环里,常见写法有两种:
写法一:逐个点亮(最直观)
while (1)
{
HAL_GPIO_WritePin(GPIOC, LED1_Pin, GPIO_PIN_RESET); // 亮
HAL_Delay(200);
HAL_GPIO_WritePin(GPIOC, LED1_Pin, GPIO_PIN_SET); // 灭
HAL_GPIO_WritePin(GPIOC, LED2_Pin, GPIO_PIN_RESET);
HAL_Delay(200);
// … 依次类推
}
写法二:移位法(更优雅,适合多灯)
uint16_t led_mask = 0x0001; // 从第一盏灯开始
while (1)
{
HAL_GPIO_WritePin(GPIOC, LED_ALL_Pin, GPIO_PIN_SET); // 先全灭
HAL_GPIO_WritePin(GPIOC, led_mask, GPIO_PIN_RESET); // 点亮当前位
HAL_Delay(200);
led_mask <<= 1; // 左移一位
if (led_mask > 0x0008) led_mask = 0x0001; // 循环回第一盏
}
理解要点:流水灯代码的骨架是"写引脚 → 延时 → 换下一个"。这里的 HAL_Delay(200) 是阻塞式延时,CPU 在延时期间空转。这是入门阶段可以接受的,但当你需要同时处理多个任务(比如流水灯的同时还要响应按键)时,就要改用定时器中断或非阻塞延时了——这是后话。
写法三:定时器中断驱动(非阻塞,进阶推荐)
当需要同时处理多个任务时,HAL_Delay() 的阻塞特性就成了瓶颈。改用定时器中断后,CPU 在等待期间可以继续执行其他逻辑(如响应按键、刷新 OLED),流水灯的节奏完全由硬件定时器驱动。
// 1. 定时器初始化回调(CubeMX 生成在 tim.c 中)
void MX_TIM2_Init(void)
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 7199; // 72MHz / 7200 = 10kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 1999; // 10kHz / 2000 = 5Hz,即 200ms 中断一次
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
}
// 2. 启动定时器并开启中断(在 main() 中调用)
HAL_TIM_Base_Start_IT(&htim2);
// 3. 中断回调函数(HAL 库弱函数,在 main.c 中重写)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint16_t led_mask = 0x0001;
if (htim->Instance == TIM2)
{
HAL_GPIO_WritePin(GPIOC, LED_ALL_Pin, GPIO_PIN_SET); // 先全灭
HAL_GPIO_WritePin(GPIOC, led_mask, GPIO_PIN_RESET); // 点亮当前位
led_mask <<= 1; // 左移一位
if (led_mask > 0x0008) led_mask = 0x0001; // 循环回第一盏
}
}
// 4. 主循环:不再需要任何延时,可以处理其他任务
while (1)
{
// 响应按键、刷新 OLED、读取传感器……CPU 完全空闲
}
对比小结:
| CPU 利用率 | 延时期间空转,浪费 | 等待期间可执行其他任务 |
| 多任务支持 | 差,难以同时响应按键 | 好,主循环可自由处理其他逻辑 |
| 代码复杂度 | 简单,易上手 | 稍复杂,需理解中断与回调 |
| 时序精度 | 依赖 SysTick,受中断影响 | 由硬件定时器保证,更精确 |
| 适用场景 | 入门学习、单任务 | 多任务、实时性要求高的工程 |
下面用一张 Mermaid 时序图直观对比两种方案下 CPU 的占用情况。横轴为时间,纵轴标注「主循环任务」与「定时器中断」两条泳道,色块表示 CPU 正在执行的内容:
定时器中断主循环任务定时器中断主循环任务#mermaid-svg-IbNI2vQavJ5llHLC{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-IbNI2vQavJ5llHLC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IbNI2vQavJ5llHLC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IbNI2vQavJ5llHLC .error-icon{fill:#552222;}#mermaid-svg-IbNI2vQavJ5llHLC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IbNI2vQavJ5llHLC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IbNI2vQavJ5llHLC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IbNI2vQavJ5llHLC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IbNI2vQavJ5llHLC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IbNI2vQavJ5llHLC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IbNI2vQavJ5llHLC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IbNI2vQavJ5llHLC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IbNI2vQavJ5llHLC .marker.cross{stroke:#333333;}#mermaid-svg-IbNI2vQavJ5llHLC svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IbNI2vQavJ5llHLC p{margin:0;}#mermaid-svg-IbNI2vQavJ5llHLC .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IbNI2vQavJ5llHLC text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-IbNI2vQavJ5llHLC .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IbNI2vQavJ5llHLC .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-IbNI2vQavJ5llHLC .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-IbNI2vQavJ5llHLC .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-IbNI2vQavJ5llHLC #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-IbNI2vQavJ5llHLC .sequenceNumber{fill:white;}#mermaid-svg-IbNI2vQavJ5llHLC #sequencenumber{fill:#333;}#mermaid-svg-IbNI2vQavJ5llHLC #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-IbNI2vQavJ5llHLC .messageText{fill:#333;stroke:none;}#mermaid-svg-IbNI2vQavJ5llHLC .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IbNI2vQavJ5llHLC .labelText,#mermaid-svg-IbNI2vQavJ5llHLC .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-IbNI2vQavJ5llHLC .loopText,#mermaid-svg-IbNI2vQavJ5llHLC .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-IbNI2vQavJ5llHLC .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IbNI2vQavJ5llHLC .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-IbNI2vQavJ5llHLC .noteText,#mermaid-svg-IbNI2vQavJ5llHLC .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-IbNI2vQavJ5llHLC .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IbNI2vQavJ5llHLC .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IbNI2vQavJ5llHLC .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IbNI2vQavJ5llHLC .actorPopupMenu{position:absolute;}#mermaid-svg-IbNI2vQavJ5llHLC .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-IbNI2vQavJ5llHLC .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IbNI2vQavJ5llHLC .actor-man circle,#mermaid-svg-IbNI2vQavJ5llHLC line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-IbNI2vQavJ5llHLC :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}HAL_Delay() 阻塞式定时器中断驱动(非阻塞)点亮 LED1(写引脚)1HAL_Delay(200) 空转等待2点亮 LED2(写引脚)3HAL_Delay(200) 空转等待4点亮 LED3(写引脚)5HAL_Delay(200) 空转等待6点亮 LED1(写引脚)7执行其他任务(按键/OLED/传感器)8200ms 到,触发中断9点亮 LED2(写引脚)10执行其他任务(按键/OLED/传感器)11200ms 到,触发中断12点亮 LED3(写引脚)13执行其他任务(按键/OLED/传感器)14
读图要点:上半部分(灰色区域)中,主循环在 HAL_Delay(200) 期间完全空转,CPU 被白白占用;下半部分(绿色区域)中,主循环只在写引脚瞬间占用 CPU,其余时间片都可用于响应按键、刷新 OLED 等业务,流水节奏完全由定时器中断驱动。
理解要点:定时器中断方案的本质是把"延时等待"从主循环中剥离出来,交给硬件定时器去计时,到点后通过中断回调自动切换下一盏灯。主循环因此被解放出来,可以专心处理其他业务。这也是从"会抄代码"走向"会写代码"的关键一步——理解了中断驱动模型,后续学习 FreeRTOS 的任务调度会轻松很多。
4. 蜂鸣器代码的结构化理解
蜂鸣器与流水灯在代码结构上几乎完全一致,区别只在于:
- 蜂鸣器通常只有一个 GPIO 引脚;
- 有源蜂鸣器(自带振荡源)只需给高低电平即可发声;
- 无源蜂鸣器需要PWM 方波驱动,才能发出不同频率的声音。
4.1 有源蜂鸣器:与流水灯同构
// 初始化:与 LED 完全相同的三步曲
// 主循环:按节奏翻转电平即可产生"滴滴"声
while (1)
{
HAL_GPIO_WritePin(BUZZER_Pin, GPIO_PIN_RESET); // 响
HAL_Delay(100);
HAL_GPIO_WritePin(BUZZER_Pin, GPIO_PIN_SET); // 停
HAL_Delay(100);
}
4.2 无源蜂鸣器:引入 PWM
无源蜂鸣器需要定时器产生 PWM 信号。初始化代码多了一个 MX_TIM3_Init(),结构变为:
// 1. GPIO 初始化(同上)
// 2. 定时器初始化:设置预分频和自动重载值,决定 PWM 频率
// 3. 配置 PWM 通道并启动
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
// 主循环:通过修改比较寄存器改变占空比,从而控制音量
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);
理解要点:蜂鸣器代码的骨架是"GPIO 初始化(+ 定时器初始化)→ 输出控制 → 延时/频率控制"。它与流水灯共享同一套 GPIO 初始化模板,多出来的定时器部分,本质上是把"延时控制"升级为"频率控制"。
5. 传感器代码的结构化理解
传感器(以常见的 DHT11 温湿度传感器为例)的代码结构与前面两个有本质区别。它不再是简单的"输出控制",而是**“输入读取 → 数据解析 → 结果输出”**的完整链路。
5.1 传感器代码的通用四段式结构
// 第一步:初始化(GPIO 配置为输出模式,用于发送起始信号)
void DHT11_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 1. 使能 GPIO 端口时钟
__HAL_RCC_GPIOB_CLK_ENABLE();
// 2. 配置引脚为推挽输出模式(初始用于发送起始信号)
GPIO_InitStruct.Pin = DHT11_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速,保证时序边沿陡峭
HAL_GPIO_Init(DHT11_Port, &GPIO_InitStruct);
// 3. 初始状态:拉高,保持总线空闲
HAL_GPIO_WritePin(DHT11_Port, DHT11_Pin, GPIO_PIN_SET);
}
// 第二步:发送起始信号并切换为输入模式
void DHT11_Start(void)
{
// 1. 主机拉低总线至少 18ms,作为起始信号
HAL_GPIO_WritePin(DHT11_Port, DHT11_Pin, GPIO_PIN_RESET);
HAL_Delay(20); // 拉低 20ms(要求 ≥18ms)
// 2. 拉高总线,释放总线控制权
HAL_GPIO_WritePin(DHT11_Port, DHT11_Pin, GPIO_PIN_SET);
delay_us(30); // 拉高 30us(要求 20~40us)
// 3. 切换为输入模式,等待 DHT11 响应
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DHT11_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 输入模式,读取传感器数据
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉,保证空闲时为高电平
HAL_GPIO_Init(DHT11_Port, &GPIO_InitStruct);
}
// 第三步:读取数据(逐位读取 40 位数据)
uint8_t DHT11_ReadByte(void)
{
uint8_t data = 0;
for (int i = 0; i < 8; i++)
{
// 等待低电平结束(每位数据以 50us 低电平开始)
while (HAL_GPIO_ReadPin(DHT11_Port, DHT11_Pin) == GPIO_PIN_RESET);
// 高电平持续时间决定 0 或 1:
// – 26~28us 表示逻辑 0
// – 70us 表示逻辑 1
delay_us(40); // 采样点:高电平开始后 40us
if (HAL_GPIO_ReadPin(DHT11_Port, DHT11_Pin) == GPIO_PIN_SET)
{
data |= (0x80 >> i); // 高电平仍为高 → 逻辑 1
}
// 等待该位高电平结束,进入下一位
while (HAL_GPIO_ReadPin(DHT11_Port, DHT11_Pin) == GPIO_PIN_SET);
}
return data;
}
// 第四步:数据校验与结果输出
void DHT11_ReadData(uint8_t *humidity, uint8_t *temperature)
{
uint8_t data[5] = {0}; // 40 位数据:湿度整/小、温度整/小、校验和
// 1. 等待 DHT11 响应信号(低电平 80us + 高电平 80us)
while (HAL_GPIO_ReadPin(DHT11_Port, DHT11_Pin) == GPIO_PIN_SET); // 等待拉低
while (HAL_GPIO_ReadPin(DHT11_Port, DHT11_Pin) == GPIO_PIN_RESET); // 等待拉高
while (HAL_GPIO_ReadPin(DHT11_Port, DHT11_Pin) == GPIO_PIN_SET); // 等待数据开始
// 2. 连续读取 5 个字节(40 位)
for (int i = 0; i < 5; i++)
{
data[i] = DHT11_ReadByte();
}
// 3. 校验和验证:前 4 字节之和的低 8 位应等于第 5 字节
if ((uint8_t)(data[0] + data[1] + data[2] + data[3]) == data[4])
{
*humidity = data[0]; // 湿度整数部分
*temperature = data[2]; // 温度整数部分
}
else
{
// 校验失败:返回无效值,由调用方决定是否重试
*humidity = 0xFF;
*temperature = 0xFF;
}
}
// 微秒级延时(基于 DWT 或定时器,HAL_Delay 最小精度为 1ms,无法满足 DHT11 时序)
void delay_us(uint32_t us)
{
// 以 DWT 为例:使能 DWT 周期计数器,实现精确微秒延时
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000); // 72MHz → 每微秒 72 个时钟周期
while ((DWT->CYCCNT – start) < ticks);
}
5.2 主循环中的调用方式
while (1)
{
DHT11_Start();
DHT11_ReadData(&humidity, &temperature);
// 将结果通过串口打印或显示在 OLED 上
HAL_Delay(1000); // 传感器采样间隔通常需要 1s 以上
}
理解要点:传感器代码的骨架是"初始化 → 触发 → 读取 → 解析 → 输出"。与流水灯/蜂鸣器最大的不同在于:
- 数据流向相反:从"写引脚"变为"读引脚";
- 引入了时序协议:需要精确测量高低电平的持续时间来解析数据;
- 引入了校验机制:防止读取错误;
- 引入了采样间隔:传感器本身有响应时间限制。
6. 三类代码的统一抽象
把三个实验放在一起对比,你会发现它们其实共享同一个底层骨架:
┌─────────────────────────────────────────────────┐
│ STM32F103 外设代码通用骨架 │
├─────────────────────────────────────────────────┤
│ 1. 时钟使能(RCC) │
│ 2. GPIO 初始化(模式/速度/上下拉) │
│ 3. (可选)定时器/ADC/中断等外设初始化 │
│ 4. 主循环: │
│ ├─ 流水灯:写引脚 → 延时 → 换下一个 │
│ ├─ 蜂鸣器:写引脚/PWM → 频率控制 │
│ └─ 传感器:触发 → 读取 → 解析 → 输出 │
└─────────────────────────────────────────────────┘
用 Mermaid 表示三类实验的代码结构对比:
#mermaid-svg-peNo7lVkcWOgWSJV{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-peNo7lVkcWOgWSJV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-peNo7lVkcWOgWSJV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-peNo7lVkcWOgWSJV .error-icon{fill:#552222;}#mermaid-svg-peNo7lVkcWOgWSJV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-peNo7lVkcWOgWSJV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-peNo7lVkcWOgWSJV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-peNo7lVkcWOgWSJV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-peNo7lVkcWOgWSJV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-peNo7lVkcWOgWSJV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-peNo7lVkcWOgWSJV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-peNo7lVkcWOgWSJV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-peNo7lVkcWOgWSJV .marker.cross{stroke:#333333;}#mermaid-svg-peNo7lVkcWOgWSJV svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-peNo7lVkcWOgWSJV p{margin:0;}#mermaid-svg-peNo7lVkcWOgWSJV .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-peNo7lVkcWOgWSJV .cluster-label text{fill:#333;}#mermaid-svg-peNo7lVkcWOgWSJV .cluster-label span{color:#333;}#mermaid-svg-peNo7lVkcWOgWSJV .cluster-label span p{background-color:transparent;}#mermaid-svg-peNo7lVkcWOgWSJV .label text,#mermaid-svg-peNo7lVkcWOgWSJV span{fill:#333;color:#333;}#mermaid-svg-peNo7lVkcWOgWSJV .node rect,#mermaid-svg-peNo7lVkcWOgWSJV .node circle,#mermaid-svg-peNo7lVkcWOgWSJV .node ellipse,#mermaid-svg-peNo7lVkcWOgWSJV .node polygon,#mermaid-svg-peNo7lVkcWOgWSJV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-peNo7lVkcWOgWSJV .rough-node .label text,#mermaid-svg-peNo7lVkcWOgWSJV .node .label text,#mermaid-svg-peNo7lVkcWOgWSJV .image-shape .label,#mermaid-svg-peNo7lVkcWOgWSJV .icon-shape .label{text-anchor:middle;}#mermaid-svg-peNo7lVkcWOgWSJV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-peNo7lVkcWOgWSJV .rough-node .label,#mermaid-svg-peNo7lVkcWOgWSJV .node .label,#mermaid-svg-peNo7lVkcWOgWSJV .image-shape .label,#mermaid-svg-peNo7lVkcWOgWSJV .icon-shape .label{text-align:center;}#mermaid-svg-peNo7lVkcWOgWSJV .node.clickable{cursor:pointer;}#mermaid-svg-peNo7lVkcWOgWSJV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-peNo7lVkcWOgWSJV .arrowheadPath{fill:#333333;}#mermaid-svg-peNo7lVkcWOgWSJV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-peNo7lVkcWOgWSJV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-peNo7lVkcWOgWSJV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-peNo7lVkcWOgWSJV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-peNo7lVkcWOgWSJV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-peNo7lVkcWOgWSJV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-peNo7lVkcWOgWSJV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-peNo7lVkcWOgWSJV .cluster text{fill:#333;}#mermaid-svg-peNo7lVkcWOgWSJV .cluster span{color:#333;}#mermaid-svg-peNo7lVkcWOgWSJV 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-peNo7lVkcWOgWSJV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-peNo7lVkcWOgWSJV rect.text{fill:none;stroke-width:0;}#mermaid-svg-peNo7lVkcWOgWSJV .icon-shape,#mermaid-svg-peNo7lVkcWOgWSJV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-peNo7lVkcWOgWSJV .icon-shape p,#mermaid-svg-peNo7lVkcWOgWSJV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-peNo7lVkcWOgWSJV .icon-shape .label rect,#mermaid-svg-peNo7lVkcWOgWSJV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-peNo7lVkcWOgWSJV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-peNo7lVkcWOgWSJV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-peNo7lVkcWOgWSJV :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
STM32F103 代码结构
时钟与 GPIO 初始化
流水灯:写引脚 + 延时
蜂鸣器:写引脚 / PWM + 频率
传感器:触发 + 读取 + 解析
while(1) 循环
核心结论:流水灯和蜂鸣器是"输出型"外设,代码结构简单、同构;传感器是"输入型"外设,多出"读取与解析"环节。但它们的初始化部分完全一致,都遵循"时钟 → 模式 → 初始状态"的模板。掌握了这个模板,你就能快速读懂任何 STM32 外设的初始化代码。
7. 常见问题与调试建议
7.1 灯不亮 / 蜂鸣器不响
- 检查 GPIO 初始化是否被调用(MX_GPIO_Init() 是否在 main() 中执行);
- 检查引脚号是否与硬件接线一致;
- 检查电平逻辑:有些板子是低电平点亮,有些是高电平点亮。
7.2 传感器读数异常
- 确认采样间隔大于 1 秒;
- 检查起始信号时序是否准确(延时是否被优化掉);
- 用示波器或逻辑分析仪观察数据线波形,这是最有效的排查手段。
7.3 延时不准
- HAL_Delay() 依赖 SysTick 中断,确认中断未被关闭;
- 需要更精确的延时或并行任务时,改用定时器。
8. 总结
通过流水灯、蜂鸣器和传感器三个实验,我们实际上掌握了 STM32F103 开发的最小知识闭环:
这三个实验的价值不在于"让灯亮起来"本身,而在于让你建立起"初始化与业务分离、外设分输入输出、主循环驱动一切"的嵌入式思维。带着这个框架去读任何 STM32 代码,你都会觉得清晰很多。
下一步建议:尝试把流水灯改为定时器中断驱动(非阻塞),再把传感器数据通过串口打印出来——这两个练习能帮你从"会抄代码"进阶到"会写代码"。

