STM32F103C8T6 入门三连:三种方式点亮流水灯 + 按键暂停
一句话概括:同一个流水灯,用三种"姿势"做出来——寄存器 → 标准外设库 → HAL库+中断。
这篇尽量说人话,适合刚上手 STM32 的同学快速过一遍。
摘要:本文用三种方式点亮同一个流水灯——寄存器、标准外设库、HAL 库 + 按键中断,并实现按键暂停/恢复。实验一直接操作寄存器,理解芯片底层"开关面板";实验二用库函数封装,注意软件延时实测约 0.972s 并不精确;实验三引入中断机制,实现"暂停后从原地继续",并提醒标准 Blue Pill 板载按键其实是复位键,需外接按键。文末附 Keil 逻辑分析仪看波形的方法与常见踩坑点。
目录
- 1. 硬件:其实就几根线
- 2. 实验一:寄存器
- 2.1 原理
- 2.2 核心代码
- 2.3 结果
- 3. 实验二:标准外设库
- 3.1 库函数其实就是换了个名字
- 3.2 重点:软件延时到底准不准?
- 3.3 没有示波器?用 Keil 逻辑分析仪看波形
- 4. 实验三:HAL 库 + 按键中断
- 4.1 中断 = 门铃
- 4.2 "暂停后从原地继续"是怎么做到的
- 4.3 三个必须记住的点
- 4.4 最大的坑:板子"自带的按键"其实是复位键
- 4.5 结果
- 6. 总结
| 用什么 | 一句话理解 | 代码量 | |
|---|---|---|---|
| 实验一 | 直接写寄存器 | 直接去按芯片的"开关面板" | 少 |
| 实验二 | 标准外设库 | 把开关面板包装成"遥控器" | 中 |
| 实验三 | HAL 库 + 外部中断 | 换个通用遥控器,再装个"门铃" | 多 |
1. 硬件:其实就几根线
| 干什么 | 接哪里 |
|---|---|
| 红 LED | PA0(实验一/二)、PA1(实验三) |
| 绿 LED | PB0 |
| 蓝 LED | PA1(实验一/二)、PB1(实验三) |
| 板载 LED | PC13,出厂已焊好,不用接线 |
| 按键 | PA0 + GND(实验三) |
2. 实验一:寄存器
2.1 原理
芯片里有一排排"开关面板",每个面板就是一个寄存器。想让灯亮,就是去拨这些开关。
有个大坑:这些面板默认是断电的,得先去"总闸"把这个端口的电送上,不然你拨开关一点反应都没有。这个总闸就是RCC_APB2ENR。
所以流程就三步:
① 送电 → RCC_APB2ENR 打开 GPIOA / GPIOB / GPIOC ② 定方向 → GPIOx_CRL / CRH 把引脚设成"输出" ③ 拨开关 → GPIOx_BSRR / BRR 输出高/低电平"输出"怎么设?每个引脚占 4 个小位:
- 低 2 位 = 速度(2MHz / 10MHz / 50MHz)
- 高 2 位 = 模式(推挽 / 开漏)
要让 LED 能亮,就设成通用推挽输出。比如 50MHz 推挽 =0b0011=0x3。
为什么要用 BSRR/BRR,而不是直接写 ODR?
因为它们只认"1":想置高就往 BSRR 写 1,想拉低就往 BRR 写 1,写 0 完全没影响。
好处是不会误碰同一个端口上的其它引脚(不用"读一遍、改一下、再写回去"这么麻烦)。
延时怎么办?用芯片内核自带的小闹钟SysTick:让它每 1 毫秒响一次,响 1000 次就是 1 秒。不占定时器、不用写中断,省事。
2.2 核心代码
/* ① 送电 */RCC->APB2ENR|=(1UL<<2)|(1UL<<3)|(1UL<<4);/* GPIOA|GPIOB|GPIOC *//* ② 设成推挽输出: 先清掉这 4 位, 再写上 0x3 */GPIOA->CRL&=~(0xFUL<<0);GPIOA->CRL|=(0x3UL<<0);/* PA0 */GPIOB->CRL&=~(0xFUL<<0);GPIOB->CRL|=(0x3UL<<0);/* PB0 *//* ③ 点亮 / 熄灭 (写 1 有效, 写 0 不影响) */GPIOA->BSRR=(1UL<<0);/* PA0 置高 = 点亮 */GPIOA->BRR=(1UL<<0);/* PA0 拉低 = 熄灭 */主循环:
while(1){for(i=0;i<4;i++){led_on(&g_leds[i]);delay_ms(1000);/* 亮 1 秒 */led_off(&g_leds[i]);}}写 CRL/CRH 一定要**“先清零那 4 位,再或上目标值”**,否则会把同一个端口上其它引脚的设置冲掉。
2.3 结果
wangde
3. 实验二:标准外设库
3.1 库函数其实就是换了个名字
底层做的事和实验一一模一样,只是把"拨开关"包装成了好记的函数:
| 寄存器写法 | 库函数写法 |
|---|---|
RCC->APB2ENR |= (1<<2)|(1<<3)|(1<<4); | RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|GPIOB|GPIOC, ENABLE); |
| 手工算 CRL/CRH | GPIO_Init(GPIOA, &结构体); |
GPIOA->BSRR = (1<<0); | GPIO_SetBits(GPIOA, GPIO_Pin_0); |
GPIOA->BRR = (1<<0); | GPIO_ResetBits(GPIOA, GPIO_Pin_0); |
建工程时最容易忘的两件事:
- Include Paths 要把库的
inc目录加进去; - 宏定义必须写
STM32F10X_MD,USE_STDPERIPH_DRIVER,少一个就报一堆"函数未定义"。
3.2 重点:软件延时到底准不准?
实验二用的是"空循环数数"来延时(不是 SysTick)。数得准不准,全看编译器脸色。
用 Keil 反汇编一看,循环一次要 7 个机器周期;循环 10000 次,再加点开销,一共约70012 个周期。
72MHz 下:
70012 ÷ 72,000,000 ≈ 0.972 毫秒所以你以为的"1 秒",其实是:
| 名义上 | 实际上 |
|---|---|
| delay_ms(1) | ≈ 0.972 ms |
| delay_ms(1000) | ≈0.972 秒 |
大概短了 3%。而且只要你把 Keil 的优化等级从 -O0 调到 -O1/-O2,循环被优化后时间会明显变化。
3.3 没有示波器?用 Keil 逻辑分析仪看波形
Keil 可以直接软件仿真,把引脚电平画成波形:
- 魔术棒 → Debug → 选Use Simulator(Dialog DLL 参数要是
-pSTM32F103C8); Ctrl+F5进入调试 → 菜单View → Analysis Windows → Logic Analyzer;- 点
Setup...,输入PORTA.0,Display Type 选Bit; - 按
F5跑几秒再停,波形就出来了。
从波形上就能读出来:高电平宽约 0.97 秒,完整周期约 3.89 秒(因为 4 只灯轮流,一只灯要等另外 3 只各亮一次)。
踩坑提醒:一定要用
DARMSTM.DLL这个"STM32 器件模型",别用通用的DCM.DLL——通用模型不仿真 GPIO,你会看到一条直线。
4. 实验三:HAL 库 + 按键中断
4.1 中断 = 门铃
轮询就像你每隔几秒跑到门口看一眼有没有人——白费力气,还耽误干活。
中断就像装了个门铃——平时该干嘛干嘛,有人按铃你再去开门。
按键接 PA0,按下时引脚从高变低(下降沿),芯片立刻"打断"主程序去处理,处理完再回来接着跑。
处理流程:
按下按键(PA0 出现下降沿) → EXTI0 中断线 → NVIC 中断控制器 → EXTI0_IRQHandler() 清标志 → HAL_GPIO_EXTI_Callback() 你写的处理代码4.2 "暂停后从原地继续"是怎么做到的
关键:把 1 秒拆成 1000 个 1 毫秒的小节拍,暂停时就不往下数。
staticvoidled_delay_ms(uint32_tms){while(ms>0U){HAL_Delay(1U);/* 走 1 毫秒 */if(g_led_run!=0U)ms--;/* 只有在"运行"状态才消耗时间 */}}按键中断里只干一件事——把g_led_run取反:
voidHAL_GPIO_EXTI_Callback(uint16_tGPIO_Pin){if(GPIO_Pin!=KEY_Pin)return;if(HAL_GetTick()-last_tick<200U)return;/* 200ms 消抖 */last_tick=HAL_GetTick();g_led_run=!g_led_run;/* 暂停 <-> 恢复 */}4.3 三个必须记住的点
g_led_run要加volatile
它在中断里被改、在主循环里被读。不加,编译器可能把它记在"自己脑子里"(寄存器),主循环就永远看不到按键的变化。中断里别写耗时操作
回调里只翻转一个标志,别做延时、别打印,重活留给主循环。机械按键必须消抖
按键按下瞬间电平会抖,不消抖会"一次按下触发好几次"。这里用时间做了一个 200ms 的过滤。
4.4 最大的坑:板子"自带的按键"其实是复位键
标准 Blue Pill(STM32F103C8T6 最小系统板)上只有 1 个按键,就是复位键,接在 NRST 上。
- 按下去 =复位单片机,程序从头跑,不是普通 IO,做不了暂停/恢复;
- 如果你的板子有两个按键(第二个常印着
KEY/S2/WK_UP),那才是用户键,一般在PA0。
怎么办?在面板板上外接一个轻触按键就行,就两根线:
PA0 ──[ 按键 ]── GND不用加电阻(用芯片内部上拉)。四脚按键记住一句:一只脚接 PA0,另一只"对角"的脚接 GND。
手边没按键?拿一根杜邦线,一头插 GND,另一头去轻碰 PA0 排针,每碰一下等于按一次(有消抖,手抖不会误触发)。
如果你的板子是"按下接 3.3V"(高电平有效),改一个数字就行:
#defineKEY_ACTIVE_HIGH0/* 0=按下接地(低电平有效) 1=按下接3.3V(高电平有效) */4.5 结果
微信视频2026-09-29_183115_191
6. 总结
- 三种方式底层是同一件事:都是去拨 GPIO 那几个寄存器,区别只是"直接拨"还是"用遥控器拨";
- 永远先开时钟,这是所有 STM32 外设的通用前提;
- 软件延时不精确(本例实测 0.972s),要准就用定时器/SysTick;
- 中断比轮询好:不占 CPU、响应快,但要记住
volatile、ISR 要短、必须消抖; - 没有示波器时,Keil 逻辑分析仪是看时序的好工具;
- 最后别忘了:标准 Blue Pill 没有用户按键,做按键实验要自己接一个。