ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

FindStar星图计划:02 多路 LED 流水灯:位操作,以及直接写寄存器有多快

FindStar星图计划:02 多路 LED 流水灯:位操作,以及直接写寄存器有多快

星图计划 · C8T6 卷 02
上一篇点亮一颗灯,这一篇让我们来把多个灯跑起来

上一篇只点亮一颗 LED,代码四行。

这一篇要点亮六颗,让它们依次亮过去,看起来像水在流。

你大概会觉得:这不就是把上一篇的代码复制六遍吗?

确实可以,而且我第一版就是这么写的。12 行,每颗灯两行,写完手臂发酸,但灯确实跑了。逻辑分析仪上六个通道依次错开,周期 1.202 秒,和理论值几乎一致。

问题不在灯亮不亮。问题在于——第 7 颗灯的时候,你打算怎么办?

先搞清楚这颗灯是高电平亮,还是低电平亮

上一篇有个让人别扭的地方:PC13 是拉低才亮,代码里写GPIO_PIN_RESET才是"开"。

这一篇的代码反过来:

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_SET);//开

SET才是开。同一块板子,同样的 LED,极性怎么反了?

不是代码写错了,是接法不一样:

接法电流路径点亮条件典型场景
阳极接 3.3V,阴极接引脚3.3V → LED → 引脚 → 内部到地引脚输出低电平蓝色药丸板的板载 PC13
引脚 → 限流电阻 → LED → 地引脚 → 电阻 → LED → 地引脚输出高电平自己外接的 LED

所以每次换硬件,第一件事不是写代码,是确认极性。

判断方法很简单:CubeMX 里把初始电平设成LOW,下载后如果上电就亮,说明是低电平点亮;如果上电全灭、写SET才亮,就是高电平点亮。

这篇用 PA0–PA5 接六颗 LED,高电平点亮,每颗串一个 330Ω 限流电阻。

顺便算一下电流:红色 LED 正向压降约 1.9V,(3.3−1.9)/330 ≈ 4.2mA。六颗全亮 25mA,对 STM32 来说很安全。如果你图省事不接电阻,或者用 100Ω,六颗加起来可能超过 100mA,那就不是"能不能亮"的问题,是"能亮多久"的问题了。

CubeMX 配置

工具链和上一篇一样(CubeMX + HAL),只有 GPIO 部分变了,所以只列会出错的项:

位置配置项值说明
SYS → DebugDebugSerial Wire不配这项,第二次下载就失联
PinoutPA0–PA5GPIO_Output六颗灯
GPIOGPIO output levelLow上电全灭,避免一上电就闪一下
GPIOGPIO modeOutput Push Pull推挽,能主动输出高低电平
GPIOMaximum output speedLow驱动 LED 用最低档就够,边沿慢一点反而干扰小

让我们先看看效果吧

这是纯 GPIO 的项目,丢进 Proteus 跑一遍最快——GPIO 的仿真模型是可靠的(等到后面的 CAN、USB 就不是了),它能在半分钟内告诉你"逻辑到底写没写错":

同一时刻只有一颗亮,六颗轮流过去,和预期一致。

第一版:老老实实复制六遍

while(1){/* USER CODE BEGIN 3 */// LED1 开和关HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_SET);HAL_Delay(200);HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET);// LED2 开和关HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_SET);HAL_Delay(200);HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_RESET);// ...LED3 到 LED6 同理}

十二行,六组,每组只差一个数字。

让我们上逻辑分析仪:

六个通道依次错开 200ms,单个通道:

  • 脉宽 W =0.2004 s
  • 周期 T =1.202 s
  • 频率 f =0.8321 Hz

理论值:每颗亮 200ms,六颗轮一遍 1.2s,频率 0.8333Hz。实测差 2ms,这 2ms 就是六次函数调用和六次HAL_Delay的累积。

灯是对的,代码是脏的。

第二版:一行管一颗

把重复的部分交给循环,把"第几颗"交给位移:

while(1){/* USER CODE BEGIN 3 */for(inti=0;i<6;i++){HAL_GPIO_WritePin(GPIOA,(uint16_t)(1u<<i),GPIO_PIN_SET);HAL_Delay(200);HAL_GPIO_WritePin(GPIOA,(uint16_t)(1u<<i),GPIO_PIN_RESET);}}

代码从 12 行变成 5 行,而且第 7 颗灯只需要把6改成7。

关键是这个1u << i:

i二进制十六进制对应引脚
00000 00010x0001PA0
10000 00100x0002PA1
20000 01000x0004PA2
50010 00000x0020PA5

所谓"点亮第 i 颗灯",在硬件层面就是把一个 1 挪到第 i 位。HAL 库里所有GPIO_PIN_x宏,本质上就是这些数的别名——GPIO_PIN_0是0x0001,GPIO_PIN_5是0x0020。所以1u << i和GPIO_PIN_0~5完全等价,只是前者能算出来。

那个u不能省。写成1 << i时,1是int,有符号。这里i最大是 5,不会出事;但如果你写1 << 31,在有符号整数上左移到符号位是未定义行为,不同编译器给你不同结果。养成写1u的习惯,成本是一个字符。

读数:W = 0.2s,T = 1.2s,f = 0.8333Hz。

比第一版短了 2ms。这 2ms 是性能提升吗?不是。
在这个量程的逻辑分析仪上,2ms 大概是一个像素宽——那是我用鼠标放光标的手抖。

第三版:直接写寄存器

HAL 库写得再顺,本质还是函数调用。想跳过中间层,就直接改ODR寄存器:

uint8_tpattern=0x01;// 注意:必须在 while 外面初始化while(1){/* USER CODE BEGIN 3 */GPIOA->ODR=(GPIOA->ODR&0xFFC0)|(pattern&0x3F);HAL_Delay(200);pattern=((pattern<<1)|(pattern>>5))&0x3F;// 6 位循环左移}

这段只有三行,但每一处都值得看:

& 0x3F—— 0x3F 是0011 1111,正好覆盖 PA0–PA5 六位。

pattern & 0x3F—— 保证写进去的只有低六位。

GPIOA->ODR & 0xFFC0—— 这一项最容易漏。ODR 是 16 位寄存器,管着 PA0–PA15。如果你直接写GPIOA->ODR = pattern,PA6–PA15 会被全部清零。今天这六个引脚没接东西,看不出问题;等你哪天在 PA8 上接了蜂鸣器,就会发现"流水灯一跑,蜂鸣器就哑了",然后花一晚上找原因。

(pattern << 1) | (pattern >> 5)—— 六位循环左移。<< 1把整体左移,最高位(bit5)被挤出去;>> 5把那个 bit5 捡回来放到 bit0。两次操作拼起来正好转一圈。

读数还是 W = 0.2s,T = 1.2s。

三张图放在一起,肉眼几乎找不出区别。

所以,直接写寄存器到底快多少

答案是:这个项目里测不出来。

算一下就明白:

  • 72MHz 主频,一个时钟周期约 13.9ns。
  • 一次HAL_GPIO_WritePin包含函数调用、参数入栈、if/else分支判断、写 BSRR,大约十几个到几十个周期,算它 0.3µs。(顺带说明:库函数里那句assert_param在默认配置下是空宏,不产生任何指令——它只在打开USE_FULL_ASSERT时才真正检查参数。)
  • 六个 LED 一轮,HAL 版比直写版多花的时间,量级在1–2µs。
  • 而一个周期是1.2s。

1.2 秒里有 2 微秒,占比 0.00017%。逻辑分析仪 6 秒量程、屏幕一千像素宽,一像素约等于 6 毫秒。这点差异连一个像素的万分之三都不到。

所以网上那些"直写寄存器快 10 倍"的说法,在你现在这个场景里是完全正确的废话——快 10 倍的确实是一个 0.3µs 的操作,但你的瓶颈是HAL_Delay(200),它才是那 200,000µs。

那什么时候这种差异会咬人?

场景时序要求为什么 HAL 版本会出问题
WS2812 灯带0.4µs 与 0.8µs 区分 0 和 1函数调用开销正好在这个量级,时序直接跑飞
软件 SPI 刷屏SCK 周期几微秒每个边沿都走一次函数调用,刷新率被拖垮
软件 PWM 调光中断里翻转引脚中断服务函数里每一微秒都要抢

到这三个场景,函数调用从"无关紧要"变成"主要矛盾"。那时候不是优化,是不这么做根本做不出来。

所以这一篇真正的收获不是"寄存器更快",而是:你知道有这么一条路,并且知道它什么时候才值得走。在 200ms 的尺度上纠结那几个时钟周期,是典型的过早优化。

踩过的坑

1.pattern没有初始化

第一版直写寄存器的代码里如果漏了初始化,那么上电第一帧是随机图案——这是因为pattern是局部变量,初值来自栈上的残留数据。必须在while外面写uint8_t pattern = 0x01;。

2.PA2 / PA3 可能与串口打架

Blue Pill 上 USART2 的 TX 是 PA2、RX 是 PA3,正好落在这六个引脚里。这一篇还没用串口,所以没事;但如果你的 CubeMX 工程里提前开了 USART2,PA2/PA3 会被外设占用,LED 就不受你控制。遇到"这两颗灯怎么都不亮",先去看 Pinout 视图里这两个脚是不是变绿了。

下一步

灯会跑了,但它只会自己跑——你没有任何办法在中途改变它。

下一篇(03 按键控制 LED)解决这件事:让板子第一次"听见"你的操作。听起来简单,但按键是嵌入式里第一个真正会骗人的东西——你按下一次,它可能报好几次。


星图计划 · C8T6 卷 · FindStar 寻星工作室 · 基于 STM32F103C8T6 最小系统板,工具链为 STM32CubeMX + HAL 库,所有代码与波形均在实机上验证。

返回列表