☰
STM32F103C8T6入门三连
2026/9/30 6:11:54 网站建设 项目流程

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. 硬件:其实就几根线

干什么接哪里
红 LEDPA0(实验一/二)、PA1(实验三)
绿 LEDPB0
蓝 LEDPA1(实验一/二)、PB1(实验三)
板载 LEDPC13,出厂已焊好,不用接线
按键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/CRHGPIO_Init(GPIOA, &结构体);
GPIOA->BSRR = (1<<0);GPIO_SetBits(GPIOA, GPIO_Pin_0);
GPIOA->BRR = (1<<0);GPIO_ResetBits(GPIOA, GPIO_Pin_0);

建工程时最容易忘的两件事:

  1. Include Paths 要把库的inc目录加进去;
  2. 宏定义必须写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 可以直接软件仿真,把引脚电平画成波形:

  1. 魔术棒 → Debug → 选Use Simulator(Dialog DLL 参数要是-pSTM32F103C8);
  2. Ctrl+F5进入调试 → 菜单View → Analysis Windows → Logic Analyzer;
  3. 点Setup...,输入PORTA.0,Display Type 选Bit;
  4. 按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 三个必须记住的点

  1. g_led_run要加volatile
    它在中断里被改、在主循环里被读。不加,编译器可能把它记在"自己脑子里"(寄存器),主循环就永远看不到按键的变化。

  2. 中断里别写耗时操作
    回调里只翻转一个标志,别做延时、别打印,重活留给主循环。

  3. 机械按键必须消抖
    按键按下瞬间电平会抖,不消抖会"一次按下触发好几次"。这里用时间做了一个 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. 总结

  1. 三种方式底层是同一件事:都是去拨 GPIO 那几个寄存器,区别只是"直接拨"还是"用遥控器拨";
  2. 永远先开时钟,这是所有 STM32 外设的通用前提;
  3. 软件延时不精确(本例实测 0.972s),要准就用定时器/SysTick;
  4. 中断比轮询好:不占 CPU、响应快,但要记住volatile、ISR 要短、必须消抖;
  5. 没有示波器时,Keil 逻辑分析仪是看时序的好工具;
  6. 最后别忘了:标准 Blue Pill 没有用户按键,做按键实验要自己接一个。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询