news 2026/9/30 3:19:53

STM32F103 入门实战:流水灯、蜂鸣器与传感器代码的结构化理解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103 入门实战:流水灯、蜂鸣器与传感器代码的结构化理解

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(),它做的事情可以归纳为三步:

voidMX_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_tled_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 中)voidMX_TIM2_Init(void){TIM_ClockConfigTypeDef sClockSourceConfig={0};TIM_MasterConfigTypeDef sMasterConfig={0};htim2.Instance=TIM2;htim2.Init.Prescaler=7199;// 72MHz / 7200 = 10kHzhtim2.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 中重写)voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef*htim){staticuint16_tled_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 完全空闲}

对比小结:

对比维度HAL_Delay()阻塞式定时器中断驱动
CPU 利用率延时期间空转,浪费等待期间可执行其他任务
多任务支持差,难以同时响应按键好,主循环可自由处理其他逻辑
代码复杂度简单,易上手稍复杂,需理解中断与回调
时序精度依赖 SysTick,受中断影响由硬件定时器保证,更精确
适用场景入门学习、单任务多任务、实时性要求高的工程

下面用一张 Mermaid 时序图直观对比两种方案下 CPU 的占用情况。横轴为时间,纵轴标注「主循环任务」与「定时器中断」两条泳道,色块表示 CPU 正在执行的内容:

定时器中断主循环任务定时器中断主循环任务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 配置为输出模式,用于发送起始信号)voidDHT11_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);}// 第二步:发送起始信号并切换为输入模式voidDHT11_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_tDHT11_ReadByte(void){uint8_tdata=0;for(inti=0;i<8;i++){// 等待低电平结束(每位数据以 50us 低电平开始)while(HAL_GPIO_ReadPin(DHT11_Port,DHT11_Pin)==GPIO_PIN_RESET);// 高电平持续时间决定 0 或 1:// - 26~28us 表示逻辑 0// - 70us 表示逻辑 1delay_us(40);// 采样点:高电平开始后 40usif(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);}returndata;}// 第四步:数据校验与结果输出voidDHT11_ReadData(uint8_t*humidity,uint8_t*temperature){uint8_tdata[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(inti=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 时序)voiddelay_us(uint32_tus){// 以 DWT 为例:使能 DWT 周期计数器,实现精确微秒延时CoreDebug->DEMCR|=CoreDebug_DEMCR_TRCENA_Msk;DWT->CYCCNT=0;DWT->CTRL|=DWT_CTRL_CYCCNTENA_Msk;uint32_tstart=DWT->CYCCNT;uint32_tticks=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 表示三类实验的代码结构对比:

STM32F103 代码结构

时钟与 GPIO 初始化

流水灯:写引脚 + 延时

蜂鸣器:写引脚 / PWM + 频率

传感器:触发 + 读取 + 解析

while(1) 循环

核心结论:流水灯和蜂鸣器是"输出型"外设,代码结构简单、同构;传感器是"输入型"外设,多出"读取与解析"环节。但它们的初始化部分完全一致,都遵循"时钟 → 模式 → 初始状态"的模板。掌握了这个模板,你就能快速读懂任何 STM32 外设的初始化代码。

7. 常见问题与调试建议

7.1 灯不亮 / 蜂鸣器不响

  • 检查 GPIO 初始化是否被调用(MX_GPIO_Init()是否在main()中执行);
  • 检查引脚号是否与硬件接线一致;
  • 检查电平逻辑:有些板子是低电平点亮,有些是高电平点亮。

7.2 传感器读数异常

  • 确认采样间隔大于 1 秒;
  • 检查起始信号时序是否准确(延时是否被优化掉);
  • 用示波器或逻辑分析仪观察数据线波形,这是最有效的排查手段。

7.3 延时不准

  • HAL_Delay()依赖 SysTick 中断,确认中断未被关闭;
  • 需要更精确的延时或并行任务时,改用定时器。

8. 总结

通过流水灯、蜂鸣器和传感器三个实验,我们实际上掌握了 STM32F103 开发的最小知识闭环:

  1. GPIO 初始化模板:时钟 → 模式 → 初始状态,适用于所有外设;
  2. 输出型外设(流水灯、蜂鸣器):写引脚 + 延时/频率控制;
  3. 输入型外设(传感器):触发 → 读取 → 解析 → 输出,多出时序协议与校验环节。

这三个实验的价值不在于"让灯亮起来"本身,而在于让你建立起"初始化与业务分离、外设分输入输出、主循环驱动一切"的嵌入式思维。带着这个框架去读任何 STM32 代码,你都会觉得清晰很多。

下一步建议:尝试把流水灯改为定时器中断驱动(非阻塞),再把传感器数据通过串口打印出来——这两个练习能帮你从"会抄代码"进阶到"会写代码"。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 3:19:41

Vue项目VSCode配置指南:Volar、ESLint与Prettier协同原理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 3:19:37

Python pygame飞机大战:游戏循环与碰撞检测实战

1. 从一条弹幕说起&#xff1a;为什么我劝你先用 Python 写个飞机大战说个真事儿。前段时间帮一个学弟看简历&#xff0c;他说自己"精通 Python"&#xff0c;结果面试官让他现场写个对象在屏幕上动起来&#xff0c;他憋了二十分钟没写出来。问题出在哪&#xff1f;不…

作者头像 李华
网站建设 2026/9/30 3:19:21

分布式事务实战:从2PC到TCC、SAGA与本地消息表的选型与落地

分布式事务这个话题&#xff0c;只要做过订单、库存、支付这类交易链路的人&#xff0c;迟早都会撞上。我最早接触它是在一单“订单创建减库存”的业务改造里&#xff0c;单体应用拆成订单服务和库存服务&#xff0c;数据库一拆&#xff0c;原本一个本地事务能搞定的事情&#…

作者头像 李华
网站建设 2026/9/30 3:19:17

Qt配置OpenCV保姆级教程:从环境搭建到图像显示

很多人把Qt和OpenCV配在一起&#xff0c;是想快速做一个带界面的图像处理小工具。思路没问题&#xff0c;但真正动手的时候&#xff0c;光一个版本匹配问题就能劝退一半人。我见过不少朋友卡在“OpenCV下载好了、Qt也装完了&#xff0c;但在.pro里一写路径就报错”这一步&#…

作者头像 李华
网站建设 2026/9/30 3:19:09

网络安全系统运维方案实战:连通性、性能与监控管理落地指南

简介&#xff1a;这份文档资料面向企业IT运维人员、网络管理员及安全服务从业者&#xff0c;提供一套完整的网络安全系统运维服务方案&#xff0c;帮助解决网络连通性、性能与监控管理三大核心运维难题。资源包共1个doc文件&#xff0c;约63KB&#xff0c;内容以方案文本与作业…

作者头像 李华
网站建设 2026/9/30 3:18:51

运维转网安是技能树分叉:优势、岗位选择与落地路线

干了这么多年运维&#xff0c;身边转行的同事一抓一大把&#xff0c;有的去做云计算&#xff0c;有的去做DevOps&#xff0c;但聊下来转得最顺、反馈最好的&#xff0c;十个里有七八个都去了网络安全方向。一开始我也挺纳闷&#xff0c;运维和网安虽说都沾个“网”字&#xff0…

作者头像 李华