搞STM32开发,C语言功底扎不扎实,直接影响到代码质量和开发效率。本文将聚焦于STM32开发中几个高频、实用且容易出错的C语言知识点,通过具体场景帮你巩固基础。
1 位操作
下面我们先讲解几种位操作符,然后通过实例看看它们在单片机开发中的巧妙用法。C语言支持以下六种位操作:

掌握了基本操作符,我们来看看它们在实际编程中的实战技巧。
1.1 在不改变其他位的值的状况下,对某几个位进行设值
这种需求在配置寄存器时简直太常见了。标准做法是两步走:先用 & 操作符对目标位进行清零,再用 | 操作符设置我们需要的值。
例如,要改变 GPIOA 的配置寄存器 CRL 的低4位,可以先执行清零操作:
GPIOA->CRL &= 0XFFFFFFF0F; /* 将第 4~7 位清 0 */

清掉无关位后,再安全地设置新值:
GPIOA->CRL |= 0X00000040; /* 设置相应位的值(4),不改变其他位的值 */

1.2 移位操作提高代码的可读性
位操作配合移位,能让寄存器配置意图一目了然。例如,在SysTick定时器的初始化函数中常看到这样的代码:
SysTick->CTRL |= 1 << 1;
这行代码清晰地表示:将 CTRL 寄存器的第1位(从0开始计数)设置为1。为什么不直接写一个十六进制数呢?对比一下:
SysTick->CTRL |= 0X0002;
虽然两者效果相同,但前者通过 1 << 1 直接指明了操作的是哪一位,意图明确,修改起来也方便(比如想改第2位,直接改数字即可)。后者只是一个魔数,可读性和可维护性都差很多。
1.3 ~按位取反操作使用技巧
按位取反操作符 ~ 在清除特定位时非常好用。例如,关闭SysTick定时器:
SysTick->CTRL &= ~(1 << 0) ; /* 关闭SYSTICK */
这行代码的意思是:只将CTRL寄存器的第0位清零,其他所有位保持不变。如果不用取反,代码会变成这样:
SysTick->CTRL &= 0XFFFFFFFE; /* 关闭SYSTICK */
显然,使用 ~(1 << 0) 的方案更直观,也更容易在后续维护中被理解。
1.4 ^按位异或操作使用技巧
异或操作有一个很棒的特性:与1异或会翻转,与0异或则保持不变。这使得它非常适合用来切换某个位的状态。一个经典应用就是控制LED闪烁:
GPIOB->ODR ^= 1 << 5;
每执行一次这行代码,PB5引脚(假设LED接在此)的输出电平就会翻转一次,从而实现LED的亮灭交替。用这个来实现一个简单的LED心跳灯或状态指示,代码非常简洁。
2 define宏定义
#define 是C语言的预处理指令,用于宏定义。它可以定义常量、简化表达式,极大地提升了代码的可读性和可维护性。其基本格式如下:

其中“标识符”就是你给这个常量起的名字,“字符串”则是它所代表的常数、表达式或格式串。在STM32的工程中,你会频繁见到类似这样的定义:
#define HSE_VALUE 8000000U

这行代码定义了外部高速晶振(HSE)的频率为8MHz,后面的 U 表示这是一个无符号整型数。通过宏定义,我们在代码中就可以直接使用 HSE_VALUE 这个有意义的名字,而不是到处写 8000000 这个魔法数字,后续如果更换晶振,也只需要修改这一处。
3 ifdef条件编译
在开发过程中,我们经常需要根据不同的条件(比如芯片型号、功能模块是否启用)来编译不同的代码段。这时就要用到条件编译。
最常见的形式是:
#ifdef 标识符
程序段1
#else
程序段2
#endif
它的逻辑很简单:如果这个“标识符”已经被定义过了(通常是用 #define 定义的),编译器就编译“程序段1”,否则编译“程序段2”。#else 部分也可以省略:
#ifdef 标识符
程序段1
#endif
在STM32的HAL库中,条件编译被大量使用。比如在 stm32mp1xx_hal_conf.h 这样的配置头文件中,你经常会看到:
#if !defined (HSE_VALUE)
#define HSE_VALUE 24000000U
#endif
这行代码是一个经典的防护性定义:如果之前没有定义过 HSE_VALUE 这个宏,那么就在这里定义它,并将其值设为24000000U(24MHz)。这种写法避免了宏的重复定义错误,也提供了默认值。
顺带一提,这里的 U 代表无符号整型。类似的,UL 代表无符号长整型,F 代表浮点型。加上这些后缀后,编译器在赋值时就不会进行严格的类型检查,直接按指定类型处理数据。
4 extern变量声明
当你的工程包含多个源文件(.c文件)时,如何在A文件中使用B文件中定义的全局变量呢?这就需要 extern 关键字出场了。
extern 可以放在变量或函数声明前,告诉编译器:“这个变量/函数的定义在别的文件里,你编译时去那里找。” 注意,对于同一个变量,extern 声明可以出现多次,但定义只能有一次。
例如,在一个头文件或某个.c文件中,你可能会看到:
extern uint16_t g_usart_rx_sta;
这行代码声明了 g_usart_rx_sta 这个变量,并指出它的定义在别处。那么,在工程的另一个源文件中,你一定能找到它的正确定义:
uint16_t g_usart_rx_sta;
掌握 extern 的用法,是多文件模块化编程的基础。
5 typedef类型别名
typedef 用于为现有的数据类型创建一个新名字(别名)。它的主要作用是简化复杂类型的声明,让代码更清晰,在HAL库中大量用于定义结构体和枚举类型的别名。
假设我们有一个GPIO寄存器结构体,最初是这样定义的:
struct _GPIO
{
__IO uint32_t CRL;
__IO uint32_t CRH;
// ... 其他成员
};
如果要用这个结构体类型定义一个变量,写法比较冗长:
struct _GPIO gpiox; /* 定义结构体变量gpiox */
HAL库中有成百上千个这样的结构体需要定义,每次都写 struct _GPIO 非常麻烦。这时 typedef 就派上用场了:
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
// ... 其他成员
} GPIO_TypeDef;
看,我们为这个匿名结构体类型起了一个别名 GPIO_TypeDef。现在,定义结构体变量就变得简洁明了:
GPIO_TypeDef gpiox;
这里的 GPIO_TypeDef 和最初的 struct _GPIO 完全等价,但前者使用起来方便太多了。这种用法在阅读STM32的库函数和芯片头文件时无处不在,理解了它,你看代码就会顺畅很多。
希望这些结合了STM32实战场景的C语言要点解析,能帮你夯实嵌入式开发的基础。在 云栈社区 有更多关于C/C++深入原理和高级用法的讨论,欢迎前来交流探讨,共同进步。