第四章:宏与枚举命名规范

宏定义和枚举,这两个东西在嵌入式代码里太常见了。但说实话,我见过太多项目因为命名混乱而翻车。今天咱们就聊聊,怎么给它们起个好名字。

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_onled_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 后缀
  • 枚举值全大写,带枚举类型前缀,和宏定义区分开

记住这些规则,你的代码会清晰很多。下次别人看你的代码时,光看名字就知道这是个宏还是枚举,属于哪个模块。这就是专业和业余的区别。