第四章:宏与枚举命名规范
宏定义和枚举,这两个东西在嵌入式代码里太常见了。但说实话,我见过太多项目因为命名混乱而翻车。今天咱们就聊聊,怎么给它们起个好名字。
4.1 宏定义的命名规则
宏定义说白了就是文本替换。它不像变量那样有类型检查,所以命名上要格外小心。
核心原则:宏定义全部大写,单词间用下划线分隔。
/* 好的命名 */
#define SYSTEM_CLOCK_FREQ_HZ 72000000
#define GPIO_PIN_13 13
#define BUFFER_SIZE_MAX 1024
/* 糟糕的命名 */
#define sysclk 72000000 /* 全小写,容易和变量混淆 */
#define GPIO_PIN_13 13 /* 大小写混用 */
#define buffer_size_max 1024 /* 和普通变量风格一样 */
我个人习惯,宏定义的名字要能一眼看出来「这是个宏」。全大写加下划线,就是最好的标识。我在项目中遇到过有人把宏定义写成小写,结果调试时找了半天才发现是宏替换出了问题。
4.2 宏定义的前缀规范
嗯,这里要注意。宏定义容易产生命名冲突。尤其是不同模块之间,同名宏会覆盖,而且编译器还不报错。
我建议给宏定义加模块前缀。你想想看,一个项目几百个宏,没有前缀怎么管理?
/* 模块前缀命名 */
#define TIM2_PRESCALER_VALUE 1000
#define TIM2_AUTO_RELOAD 5000
#define UART1_BAUD_RATE 115200
#define UART1_DATA_BITS 8
/* 功能分类前缀 */
#define GPIO_MODE_INPUT 0x00
#define GPIO_MODE_OUTPUT 0x01
#define GPIO_MODE_AF 0x02
#define STATUS_OK 0
#define STATUS_ERROR -1
#define STATUS_TIMEOUT -2
小技巧:我习惯用模块名缩写做前缀。比如定时器用 TIM_,串口用 UART_,GPIO 用 GPIO_。这样看代码时,一眼就知道这个宏属于哪个模块。
4.3 枚举类型的命名规则
枚举类型和宏定义不同。枚举有类型检查,所以命名风格可以稍微灵活一些。但也不能太随意。
/* 枚举类型命名:首字母大写,驼峰风格 */
typedef enum {
LED_OFF = 0,
LED_ON = 1,
LED_BLINK = 2
} LedState_t;
typedef enum {
MOTOR_STOP = 0,
MOTOR_FORWARD = 1,
MOTOR_BACKWARD = 2
} MotorDirection_t;
为什么枚举类型要用驼峰?说白了,这是为了和宏定义区分开。宏定义全大写,枚举类型首字母大写,变量全小写。三层风格,一目了然。
4.4 枚举值的命名规范
枚举值的命名,我见过两种主流风格。一种是全大写加下划线,另一种是带枚举类型前缀。
/* 风格一:全大写加下划线(推荐) */
typedef enum {
ERROR_CODE_NONE = 0,
ERROR_CODE_TIMEOUT,
ERROR_CODE_OVERRUN,
ERROR_CODE_PARITY
} ErrorCode_t;
/* 风格二:带枚举类型前缀 */
typedef enum {
EC_NONE = 0,
EC_TIMEOUT,
EC_OVERRUN,
EC_PARITY
} ErrorCode_t;
我个人更推荐风格一。为什么?因为全大写加下划线的枚举值,看起来和宏定义很像,但又有区别。宏定义通常不带模块名前缀,而枚举值带枚举类型前缀。这样在代码里看到 ERROR_CODE_TIMEOUT,就知道它来自 ErrorCode_t 枚举。
避坑指南:我曾经在一个项目里看到有人把枚举值写成小写,比如 led_on、led_off。结果和全局变量重名了,编译器报了一堆莫名其妙的错误。从那以后,我坚持枚举值必须大写。
4.5 避免命名冲突的技巧
命名冲突是嵌入式开发的老大难问题。尤其是宏定义,没有作用域限制,一不小心就「撞车」了。
| 冲突类型 | 示例 | 解决方案 |
|---|---|---|
| 宏与宏冲突 | 两个头文件都定义了 BUFFER_SIZE |
加模块前缀,如 UART_BUFFER_SIZE |
| 宏与枚举冲突 | 宏 STATUS_OK 和枚举值 STATUS_OK |
宏用全大写,枚举值带类型前缀 |
| 枚举与枚举冲突 | 两个枚举都有 NONE 值 |
枚举值加枚举类型前缀 |
/* 避免冲突的完整示例 */
/* timer.h */
#define TIMER_MAX_COUNT 1000
typedef enum {
TIMER_STATE_IDLE = 0,
TIMER_STATE_RUNNING,
TIMER_STATE_EXPIRED
} TimerState_t;
/* uart.h */
#define UART_BUFFER_SIZE 256
typedef enum {
UART_STATE_IDLE = 0,
UART_STATE_TRANSMITTING,
UART_STATE_RECEIVING
} UartState_t;
你看,两个模块都有 IDLE 状态,但因为加了模块前缀,完全不会冲突。这就是命名规范的价值所在。
4.6 实际项目中的命名策略
说了这么多理论,咱们看看实际项目怎么用。我参与过一个智能家居项目,代码量大概 10 万行。我们当时的命名策略是这样的:
/* 系统级宏定义 */
#define SYS_CLOCK_FREQ_HZ 72000000
#define SYS_TICK_INTERVAL_US 1000
/* 模块级宏定义 */
#define LED_GPIO_PORT GPIOA
#define LED_GPIO_PIN GPIO_PIN_13
#define TEMP_SENSOR_I2C_ADDR 0x48
/* 枚举定义 */
typedef enum {
APP_STATE_INIT = 0,
APP_STATE_RUNNING,
APP_STATE_SLEEP,
APP_STATE_ERROR
} AppState_t;
typedef enum {
SENSOR_TYPE_TEMPERATURE = 0,
SENSOR_TYPE_HUMIDITY,
SENSOR_TYPE_LIGHT
} SensorType_t;
我的经验:命名规范不是死规矩,要根据项目规模灵活调整。小项目可以简单点,大项目必须严格规范。我见过一个 50 万行的项目,就因为命名混乱,最后不得不重构了三分之一代码。
4.7 总结一下
宏和枚举的命名,说白了就三个要点:
- 宏定义全大写,加模块前缀,避免冲突
- 枚举类型用驼峰,首字母大写,加
_t后缀 - 枚举值全大写,带枚举类型前缀,和宏定义区分开
记住这些规则,你的代码会清晰很多。下次别人看你的代码时,光看名字就知道这是个宏还是枚举,属于哪个模块。这就是专业和业余的区别。