嵌入式现代C++工程实践——第11篇:HAL_GPIO_WritePin与TogglePin —— 让引脚动起来

张开发
2026/4/16 7:11:59 15 分钟阅读

分享文章

嵌入式现代C++工程实践——第11篇:HAL_GPIO_WritePin与TogglePin —— 让引脚动起来
嵌入式现代C工程实践——第11篇HAL_GPIO_WritePin与TogglePin —— 让引脚动起来仓库已经开源仍然在持续建设中喜欢的话点个⭐相关的链接如下https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP静态网页直接阅览https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/承接上一篇引脚配置好了时钟开了推挽输出就绪。现在就差最后一步——告诉引脚输出高电平或输出低电平。这就是HAL_GPIO_WritePin()和HAL_GPIO_TogglePin()的工作。我们的目标经过前面几篇的努力GPIOC的时钟已经使能了PC13也配好了推挽输出模式。引脚现在已经站好军姿等待命令了。但我们还没给它下达过任何指令——所以LED到现在还是不亮的。这一篇我们就来解决最后一步怎么让引脚输出我们想要的电平。HAL_GPIO_WritePin —— 直接控制引脚电平这是HAL库提供的最基本的引脚控制函数我们先看它的完整签名voidHAL_GPIO_WritePin(GPIO_TypeDef*GPIOx,uint16_tGPIO_Pin,GPIO_PinState PinState);三个参数我们在前面的文章中都已经见过现在我们把它们放在一起理解。第一个参数GPIO_TypeDef *GPIOx是端口指针告诉HAL你要操作哪个端口——GPIOA、GPIOB还是GPIOC。第二个参数uint16_t GPIO_Pin是引脚位掩码指出具体哪个引脚。第三个参数GPIO_PinState PinState只有两种取值GPIO_PIN_SET高电平值为1和GPIO_PIN_RESET低电平值为0。对于我们的Blue Pill板载LEDPC13低电平有效点亮LED需要输出低电平熄灭LED需要输出高电平// 点亮LED —— PC13输出低电平HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET);// 熄灭LED —— PC13输出高电平HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET);这里要注意一个容易搞混的地方我们说点亮LED对应的是GPIO_PIN_RESET低电平而不是直觉上的GPIO_PIN_SET。这是因为Blue Pill的PC13 LED电路是低电平有效的——这在第3篇推挽、开漏与PC13中已经详细分析过了。如果你顺手把SET和RESET写反了LED的行为就会完全反过来——“亮变成灭”“灭变成亮”。不过话说回来这不影响程序运行只是逻辑上的颠倒。BSRR寄存器——原子操作的幕后功臣HAL_GPIO_WritePin的底层实现非常精巧值得我们深入看一看。它操作的不是ODROutput Data Register而是BSRRBit Set/Reset Register。BSRR的设计是ARM Cortex-M系列的一大亮点// HAL_GPIO_WritePin 的实现简化版voidHAL_GPIO_WritePin(GPIO_TypeDef*GPIOx,uint16_tGPIO_Pin,GPIO_PinState PinState){if(PinState!GPIO_PIN_RESET){GPIOx-BSRRGPIO_Pin;// 低16位设置}else{GPIOx-BSRR(uint32_t)GPIO_Pin16U;// 高16位清除}}BSRR是一个32位只写寄存器它的设计非常巧妙。低16位bit0到bit15用来设置对应的ODR位——写1到bit13就是把ODR的bit13设为1输出高电平。高16位bit16到bit31用来清除对应的ODR位——写1到bit29即bit13左移16位就是把ODR的bit13清为0输出低电平。以PC13为例GPIO_PIN_13的值是0x2000第13位为1。当我们需要输出高电平时写GPIOC-BSRR 0x2000这会设置ODR的第13位为1。当我们需要输出低电平时写GPIOC-BSRR 0x2000 16 0x20000000这会清除ODR的第13位为0。为什么不用ODR直接写因为ODR是16位可读写寄存器如果用读-改-写的方式修改某一个位在读取和写回之间如果发生了中断中断处理函数可能也修改了同一个端口的另一位——写回时就会覆盖中断的修改。BSRR通过写1生效的设计避免了这个问题设置和清除是两个独立的位域写操作是原子的不需要读-改-写三步。这意味着即使多个中断同时操作同一个端口的不同引脚也不会互相干扰。HAL_GPIO_TogglePin —— 翻转引脚电平有时候我们不需要关心当前电平是什么只需要把它翻转——高变低、低变高。这时候用HAL_GPIO_TogglePin更方便voidHAL_GPIO_TogglePin(GPIO_TypeDef*GPIOx,uint16_tGPIO_Pin);它只有两个参数——端口和引脚不需要指定目标电平。底层实现也很直接voidHAL_GPIO_TogglePin(GPIO_TypeDef*GPIOx,uint16_tGPIO_Pin){GPIOx-ODR^GPIO_Pin;// 异或操作翻转对应位}异或操作XOR的特性是与0异或保持不变与1异或翻转。所以ODR ^ GPIO_PIN_13只会翻转ODR的第13位其他位不受影响。⚠️ 注意与BSRR不同TogglePin的读-改-写操作不是原子的。如果在读取ODR和写回之间发生了中断而中断处理函数也修改了同一个端口的其他引脚理论上可能会出问题。不过对于LED闪烁这种简单场景完全不用担心——LED不需要原子性保证。HAL_Delay —— 时间的来源LED闪烁需要延时我们用的是HAL_Delay()HAL_Delay(500);// 延时500毫秒HAL_Delay的实现依赖于SysTick定时器。SysTick是Cortex-M3内核内置的24位递减计数器它的时钟源是HCLK在我们的配置中是64MHz。HAL_Init()会把SysTick配置为每1ms产生一次中断每次中断时一个名为uwTick的全局计数器加1。HAL_Delay()就是通过查询这个计数器来判断是否经过了指定的毫秒数。这就是为什么main.cpp中必须先调用HAL_Init()——没有它SysTick没有被配置HAL_Delay()根本不工作你的程序会卡在延时函数里永远不出来。完整的C风格LED闪烁程序现在我们把前面所有HAL API组合起来写一个完整的C风格LED闪烁程序。这是整个系列中纯HAL方式的完整展示也是后续C重构的起点#includestm32f1xx_hal.h/* 时钟配置HSI - PLL - 64MHz */voidSystemClock_Config(void){RCC_OscInitTypeDef osc{0};osc.OscillatorTypeRCC_OSCILLATORTYPE_HSI;osc.HSIStateRCC_HSI_ON;osc.PLL.PLLStateRCC_PLL_ON;osc.PLL.PLLSourceRCC_PLLSOURCE_HSI_DIV2;osc.PLL.PLLMULRCC_PLL_MUL16;HAL_RCC_OscConfig(osc);RCC_ClkInitTypeDef clk{0};clk.ClockTypeRCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;clk.SYSCLKSourceRCC_SYSCLKSOURCE_PLLCLK;clk.AHBCLKDividerRCC_SYSCLK_DIV1;clk.APB1CLKDividerRCC_HCLK_DIV2;clk.APB2CLKDividerRCC_HCLK_DIV1;HAL_RCC_ClockConfig(clk,FLASH_LATENCY_2);}/* LED初始化使能时钟 配置PC13为推挽输出 */voidled_init(void){__HAL_RCC_GPIOC_CLK_ENABLE();GPIO_InitTypeDef g{0};g.PinGPIO_PIN_13;g.ModeGPIO_MODE_OUTPUT_PP;g.PullGPIO_NOPULL;g.SpeedGPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOC,g);}/* LED点亮PC13输出低电平 */voidled_on(void){HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET);}/* LED熄灭PC13输出高电平 */voidled_off(void){HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET);}intmain(void){HAL_Init();SystemClock_Config();led_init();while(1){led_on();HAL_Delay(500);led_off();HAL_Delay(500);}}我们来逐段理解这个程序。首先是SystemClock_Config()它配置系统时钟到64MHz——HSI8MHz内部振荡器经过PLL倍频/2 × 16 64MHz作为SYSCLK然后AHB不分频、APB1二分频到32MHz、APB2不分频保持64MHz。这段代码对应我们项目中system/clock.cpp的setup_system_clock()方法。接下来是led_init()它做了两件事先调用__HAL_RCC_GPIOC_CLK_ENABLE()唤醒GPIOC的时钟这是第4篇讲过的第一大坑然后把PC13配置为推挽输出、无上下拉、低速。这个函数和我们项目中gpio.hpp的setup()方法做的是完全一样的事情。最后是led_on()和led_off()分别调用HAL_GPIO_WritePin输出低电平和高电平。注意led_on()传的是GPIO_PIN_RESET低电平因为Blue Pill的PC13 LED是低电平有效的。主函数main()的逻辑很直接初始化HAL库和时钟初始化LED引脚然后在无限循环中交替点亮和熄灭LED每次间隔500ms。编译和烧录如果你是跟着env_setup系列一路走来的编译和烧录应该已经很熟悉了mkdirbuildcdbuild cmake..makemakeflash如果你用的是我们项目中的CMakeLists.txt编译完成后会自动显示固件大小text data bss dec hex filename 1234 120 4 1358 54e stm32_demo.elf烧录成功后你应该看到Blue Pill板上的LED以1秒为周期500ms亮500ms灭稳定闪烁。如果LED完全没有反应排查顺序是第一确认ST-Link连接正常SWDIO、SWCLK、GND三线第二确认时钟配置正确用调试器读RCC_CFGR寄存器第三确认GPIOC时钟已使能读RCC_APB2ENR的bit4第四确认PC13已配置为输出读GPIOC_CRH的[23:20]位。我们走到了哪一步到这里HAL库的三个核心GPIO API我们都掌握了__HAL_RCC_GPIOx_CLK_ENABLE()开时钟、HAL_GPIO_Init()配引脚、HAL_GPIO_WritePin()/HAL_GPIO_TogglePin()控电平。用这三个API已经足够控制LED闪烁了。但如果你回头看看上面的代码会发现一个问题这段代码跟PC13硬绑定了。GPIOC、GPIO_PIN_13、__HAL_RCC_GPIOC_CLK_ENABLE()这三个常量分散在三个不同的函数中。如果要把LED换到PA0上你需要改三个地方——而且必须三个都改对漏一个就不工作。下一篇我们就来分析这种C风格写法的问题看看它是怎么一步步走到难以维护的为后续的C重构做铺垫。相关阅读现代Qt开发教程新手篇1.1——QObject 与元对象系统 - 相似度 100%现代Qt开发教程新手篇1.2——信号与槽 - 相似度 100%通用GUI编程技术——图形渲染实战二十八——图像格式与编解码PNG/JPEG全掌握 - 相似度 100%

更多文章