作为嵌入式软件开发工程师,在敲代码的时候(而不是事后重构),需要在以下细节处时刻保持警惕:
一、写函数时
1. 参数数量控制
c
// ❌ 高耦合信号:参数过多
void config_device(uint8_t addr, uint16_t rate, uint8_t mode,
uint32_t timeout, void (*cb)(void), uint8_t retry);
// ✅ 使用配置结构体
typedef struct {
uint16_t rate;
uint8_t mode;
uint32_t timeout;
uint8_t retry;
} dev_config_t;
void config_device(uint8_t addr, const dev_config_t* cfg, void (*cb)(void));
2. 返回值只暴露必要信息
c
// ❌ 返回内部状态码(耦合上层逻辑)
int32_t sensor_read(uint8_t* data); // 返回-1,-2,-3等错误码
// ✅ 返回bool或简单枚举
bool sensor_read(uint8_t* data); // 成功/失败
// 或
typedef enum { SENSOR_OK, SENSOR_BUSY, SENSOR_TIMEOUT } sensor_status_t;
二、定义变量时
3. 全局变量必须加"防火墙"
c
// ❌ 裸全局变量
uint32_t system_tick; // 任何文件都能改
// ✅ 访问函数
static uint32_t s_system_tick;
uint32_t sys_get_tick(void) { return s_system_tick; }
void sys_tick_update(void) { s_system_tick++; } // 只在中断中调用
4. 文件作用域变量全部static
c
// 每个.c文件开头
static uint8_t s_buffer[256]; // 模块私有
static void (*s_callback)(void); // 其他文件看不到
// 只有明确需要跨文件的才在.h声明
extern const uint32_t FW_VERSION; // 只读常量可例外
三、写头文件时
5. 前向声明优先于#include
c
// timer.h
// ❌ 不必要的包含
#include "event.h"
#include "queue.h"
// ✅ 前向声明
typedef struct event_t event_t; // 只告诉编译器这是个类型
void timer_register(event_t* evt); // 不需要完整定义
6. 不透明指针强制隐藏实现
c
// sensor.h
// ✅ 用户只看到句柄
typedef struct sensor_ctx_t sensor_ctx_t;
sensor_ctx_t* sensor_create(int pin);
bool sensor_read(sensor_ctx_t* ctx, float* out);
void sensor_destroy(sensor_ctx_t* ctx);
// sensor.c
struct sensor_ctx_t {
int pin;
uint8_t i2c_addr;
uint32_t last_read_ms;
// 所有内部细节都藏在这里
};
四、模块间通信时
7. 回调函数不要直接调用业务逻辑
c
// ❌ 回调里做复杂处理
void button_isr(void) {
led_toggle(); // 直接操作LED
buzzer_beep(100); // 直接操作蜂鸣器
send_udp("pressed"); // 直接网络发送
}
// ✅ 回调只设置标志/发消息
void button_isr(void) {
event_post(EVENT_BUTTON_PRESSED); // 通知任务
}
// 任务中处理
void main_task(void) {
if (event_wait(EVENT_BUTTON_PRESSED)) {
led_toggle();
buzzer_beep(100);
send_udp("pressed");
}
}
8. 禁止跨模块访问结构体成员
c
// app.c
// ❌ 直接访问其他模块的私有数据
extern uart_buffer_t g_uart_buf;
memcpy(g_uart_buf.data, src, len);
// ✅ 通过接口
uart_send(src, len);
五、写条件编译时
9. 配置宏集中在单独文件
c
// config.h - 所有配置宏的唯一来源
#define UART_BAUDRATE 115200
#define TASK_STACK_SIZE 1024
// uart.c
// ❌ 分散的宏定义
#ifndef UART_BUFFER_SIZE
#define UART_BUFFER_SIZE 64
#endif
// ✅ 统一包含
#include "config.h"
10. 平台相关代码隔离
c
// 不好:平台代码混杂
void delay_ms(uint32_t ms) {
#ifdef STM32
HAL_Delay(ms);
#elif defined(ESP32)
vTaskDelay(ms / portTICK_PERIOD_MS);
#endif
}
// 好:用独立文件
// hal_delay_stm32.c
// hal_delay_esp32.c
// 链接时选择
六、写循环和条件时
11. 避免长if-else链(内聚性差)
c
// ❌ 一个函数处理多种模式
void process_mode(int mode) {
if (mode == 1) { /* 30行 */ }
else if (mode == 2) { /* 30行 */ }
else if (mode == 3) { /* 30行 */ }
}
// ✅ 函数指针表(内聚到各自模块)
static const mode_handler_t s_handlers[] = {
mode1_process,
mode2_process,
mode3_process,
};
12. 状态机内不要直接调用外部模块
c
// ❌ 状态机直接调用
case STATE_ERROR:
led_set_red();
buzzer_alarm();
log_error();
// ✅ 状态机只改变状态,外部查询执行
case STATE_ERROR:
system_set_state(SYS_STATE_ERROR); // 设置标志
// 主循环中统一处理
七、编码时的自我检查清单
每次敲代码前/后快速过一遍:
| 检查项 | 坏味道 | 改进 |
|---|---|---|
| 这个函数超过50行? | 高 | 拆分 |
| 这个.c文件超过800行? | 高 | 拆分模块 |
| 函数有超过3个参数? | 中 | 用结构体 |
| 头文件包含超过5个其他.h? | 中 | 前向声明 |
| 有非const全局变量? | 高 | 加static+访问函数 |
| 直接访问其他模块的结构体? | 高 | 加接口函数 |
| 中断里做了超过10行操作? | 高 | 只发事件 |
| 模块间有循环依赖? | 致命 | 重新设计 |
八、培养的肌肉记忆
敲#includ前:真的需要完整类型定义?还是只要前向声明?
敲struct {前:这个结构体用户需要看到吗?→ 不需要就放.c里
敲函数前:这个函数应该属于哪个模块?→ 职责不清就拆分
敲static前:这个变量/函数真的需要跨文件访问?→ 默认加static
敲全局变量前:能不能用函数封装?→ 99%的情况能
这些不是死板的规则,而是你在敲键盘的瞬间就应该意识到的设计信号。当你发现自己在"绕过"这些建议时(比如为了省事直接访问其他模块的结构体),就是代码开始腐烂的时刻。