为什么嵌入式代码规范如此重要?

嵌入式开发常面临资源受限(RAM/Flash小)、硬件耦合度高、团队协作频繁等挑战。混乱的命名、缺失的注释、冗长的函数会让代码在数月后变得难以理解,甚至引发难以排查的bug。规范不是束缚,而是为代码建立“可读性契约”,让每一位开发者都能快速定位问题、安全修改功能。

命名规范:让名字自解释

1. 变量命名:类型+用途

  • 全局变量:使用g_前缀,如g_sysTickCount
  • 局部变量:小驼峰,如adcValue
  • 指针变量:加p_前缀,如p_buffer
  • 布尔变量:用is/has/enable开头,如isTimerRunning
// 反例
int n; // 含义不明
uint32_t t; // 是时间?计数?

// 正例
static uint32_t g_sysTickCount; // 系统节拍计数
uint16_t adcValue;              // ADC采样值
bool isUartReady;               // UART是否就绪

2. 函数命名:动词+对象

  • 模块前缀:如uart_timer_flash_
  • 动作明确:uart_init()timer_start()flash_erase_sector()
  • 返回值语义化:uart_read_byte()返回int(-1表示错误),而非uint8_t
// 反例
void process(void); // 处理什么?
int get(void);      // 获取什么?

// 正例
void uart_init(uint32_t baudrate);
int uart_read_byte(uint8_t *data); // 返回0成功,-1失败

3. 宏与常量:全大写+下划线

  • 宏定义:#define LED_ON 1
  • 枚举常量:typedef enum { STATE_IDLE, STATE_RUNNING } State_t;
  • 避免魔法数字,使用有意义的常量。
#define ADC_CHANNEL_TEMP   0
#define ADC_CHANNEL_BATT   1
#define TIMEOUT_MS         1000

注释规范:解释为什么,而不是什么

1. 文件头注释

每个源文件开头应有版权、作者、日期、功能描述。

/**
 * @file    uart_driver.c
 * @brief   UART底层驱动,支持中断收发
 * @author  Zhang San
 * @date    2025-03-01
 * @note    依赖:stm32f4xx_hal.h
 */

2. 函数注释

说明功能、参数、返回值、注意事项,尤其对硬件操作。

/**
 * @brief  初始化UART1,8N1格式
 * @param  baudrate: 波特率,如9600, 115200
 * @retval 0成功,-1参数错误
 */
int uart_init(uint32_t baudrate);

3. 关键代码注释

  • 解释复杂算法或硬件时序。
  • 说明为什么这样写,而非逐行翻译。
// 等待发送完成,否则可能丢失数据(参考手册第12.3节)
while (!(UART1->SR & UART_SR_TC));

4. 避免无效注释

// 反例
int a = 0; // 将a赋值为0

// 正例
int retryCount = 0; // 重试次数,超过3次则报错

可维护性实践:让代码易于修改和移植

1. 模块化与信息隐藏

  • 每个外设或功能独立成.c/.h文件。
  • 头文件只暴露必要接口,内部静态函数用static修饰。
  • 使用#ifndef防止重复包含。
// uart_driver.h
#ifndef UART_DRIVER_H
#define UART_DRIVER_H

#include <stdint.h>

int uart_init(uint32_t baudrate);
int uart_send_byte(uint8_t data);
int uart_receive_byte(uint8_t *data);

#endif

2. 使用typedef简化复杂类型

typedef struct {
    uint32_t baudrate;
    uint8_t  data_bits;
    uint8_t  stop_bits;
    uint8_t  parity;
} UART_Config_t;

void uart_init(const UART_Config_t *config);

3. 避免硬编码硬件地址

使用寄存器映射或宏定义,便于移植。

// 反例
*(volatile uint32_t *)0x40011000 |= 0x01;

// 正例
#define GPIOA_CRL   ((volatile uint32_t *)0x40010800)
#define GPIO_PIN0   (1 << 0)
GPIOA_CRL[0] |= GPIO_PIN0;

4. 错误处理与断言

  • 函数入口检查参数,非法值返回错误码。
  • 关键假设使用assert()(调试阶段)。
int uart_send_byte(uint8_t data) {
    if (uart_is_busy()) {
        return -1; // 忙,返回错误
    }
    // 发送逻辑
    return 0;
}

5. 代码风格统一

  • 缩进:4个空格,不用Tab。
  • 大括号:K&R风格(左大括号不换行)。
  • 每行不超过80字符,便于阅读。
void timer_isr(void) {
    if (g_sysTickCount < UINT32_MAX) {
        g_sysTickCount++;
    }
}

完整示例:一个规范的LED控制模块

// led.h
#ifndef LED_H
#define LED_H

#include <stdint.h>

#define LED_ON  1
#define LED_OFF 0

typedef enum {
    LED_RED = 0,
    LED_GREEN,
    LED_BLUE
} LedId_t;

void led_init(void);
void led_set(LedId_t id, uint8_t state);
void led_toggle(LedId_t id);

#endif

// led.c
#include "led.h"
#include "stm32f4xx.h" // 假设使用STM32

static void led_hw_set(LedId_t id, uint8_t state);

void led_init(void) {
    // 使能GPIO时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
    // 配置PD12-14为输出
    GPIOD->MODER &= ~(GPIO_MODER_MODER12 | GPIO_MODER_MODER13 | GPIO_MODER_MODER14);
    GPIOD->MODER |= (GPIO_MODER_MODER12_0 | GPIO_MODER_MODER13_0 | GPIO_MODER_MODER14_0);
    // 初始化为灭
    led_set(LED_RED, LED_OFF);
    led_set(LED_GREEN, LED_OFF);
    led_set(LED_BLUE, LED_OFF);
}

void led_set(LedId_t id, uint8_t state) {
    if (id > LED_BLUE) {
        return; // 参数错误
    }
    led_hw_set(id, state);
}

void led_toggle(LedId_t id) {
    if (id > LED_BLUE) {
        return;
    }
    // 读取当前状态并翻转
    uint8_t current = (GPIOD->ODR >> (12 + id)) & 1;
    led_hw_set(id, current ? LED_OFF : LED_ON);
}

static void led_hw_set(LedId_t id, uint8_t state) {
    uint16_t pin = (GPIO_PIN_12 << id); // 假设GPIO_PIN_12已定义
    if (state == LED_ON) {
        GPIOD->BSRR = pin;
    } else {
        GPIOD->BSRR = (uint32_t)pin << 16;
    }
}

注意事项与常见陷阱

  • 命名一致性:团队内统一风格,避免混用uartUART
  • 注释不要过度:只注释有深度的逻辑,避免逐行注释。
  • 避免全局变量滥用:全局变量增加耦合,尽量用静态变量+访问函数。
  • 头文件自包含:每个.h应能独立编译,包含所需依赖。
  • 版本控制:代码中不要出现#if 0注释掉的代码,用git管理历史。
  • 静态分析:使用cppcheckPC-Lint检查潜在问题。

总结

嵌入式代码规范不是一蹴而就,而是持续迭代的过程。从命名、注释到模块化设计,每一步都在提升代码的可维护性。良好的规范能减少调试时间,让团队协作更顺畅,也让你的代码在硬件升级后依然易于复用。建议从今天开始,逐步应用这些实践到你的项目中。