嵌入式C开发编程原则完全指南
从基础原则到工程实践,帮助你写出简洁、可靠、易维护的嵌入式代码
前言
在嵌入式软件开发中,我们面临着独特的挑战:资源受限、实时性要求、硬件直接操作、长期维护等。这些特点使得编程原则的应用变得尤为重要。本文将系统介绍经典的软件工程原则,并结合嵌入式C语言的特点,提供实用的指导建议。
适用范围: 单片机开发、RTOS应用、嵌入式Linux、物联网设备、工控系统
目录
一、核心设计原则
二、代码组织原则
三、可读性与可维护性
四、嵌入式专属考虑
一、过早优化是万恶之源
1.1 原则起源
“Premature optimization is the root of all evil.”
—— Donald Knuth, 1974
这是计算机科学泰斗高德纳(Donald Knuth)在1974年发表的经典论文《Structured Programming with go to Statements》中提出的观点。完整的原文是:
“We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.”
翻译: 在97%的情况下,我们应该忘记那些微小的效率改进:过早优化是万恶之源。但我们不应该错过那关键的3%的优化机会。
1.2 核心思想
开发三步法则(Kent Beck):
-
Make it work(先让它工作)
- 实现功能,保证正确性
- 通过测试验证
- 不考虑性能细节
-
Make it right(再让它正确)
- 重构代码结构
- 提高可读性
- 消除明显问题
-
Make it fast(最后让它快速)
- 基于性能测试数据
- 只优化真正的瓶颈
- 保持代码可维护性
1.3 嵌入式开发中的陷阱
❌ 常见的过早优化陷阱:
/* 陷阱1:为了"节省内存"使用位域 */
typedef struct {
uint8_t flag1 : 1; // 节省了7位,但...
uint8_t flag2 : 1; // 增加了位操作开销
uint8_t flag3 : 1; // 降低了可读性
uint8_t mode : 2; // 难以调试
uint8_t state : 3; // 可移植性差
} OverOptimizedFlags_t; // 省了3字节,值得吗?
/* 陷阱2:手动优化循环 */
for (int i = 0; i < n; i++)
{
array[i] = value; // 清晰直观
}
// 过度优化为:
int i = n;
while (i--) { // 稍快,但可读性差
array[i] = value;
}
/* 陷阱3:宏替代函数 */
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// 问题:参数可能被多次求值
int x = MAX(foo(), bar()); // foo()和bar()可能被调用多次!
✅ 正确的做法:
/* 方案1:先保持简单 */
typedef struct {
bool flag1; // 清晰
bool flag2; // 易读
bool flag3; // 易调试
uint8_t mode; // 可移植
uint8_t state; // 如果内存真的不够,再优化
} SimpleFlags_t;
/* 方案2:使用标准库 */
memset(array, value, n * sizeof(array[0])); // 编译器会优化
/* 方案3:使用内联函数 */
static inline int Max(int a, int b)
{
return (a > b) ? a : b; // 类型安全,无副作用
}
1.4 何时应该优化?
优化的前提条件:
✅ 应该优化:
- 有明确的性能问题(实测数据)
- 找到了真正的瓶颈(profiling工具)
- 优化后仍保持可读性
- 有性能测试对比数据
❌ 不应该优化:
- 没有性能问题就优化
- 基于猜测而非测量
- 牺牲可读性换取微小性能提升
- 为了"可能的未来需求"而优化
嵌入式场景下的判断标准:
| 场景 | 是否优化 | 理由 |
|---|---|---|
| 中断服务函数(ISR) | ✅ 是 | 实时性要求高 |
| 1ms定时任务 | ⚠️ 测量后决定 | 看CPU占用率 |
| 初始化函数 | ❌ 否 | 只执行一次,无需优化 |
| 每秒调用一次的函数 | ❌ 否 | 不是瓶颈 |
| DMA传输设置 | ✅ 是 | 硬件相关,影响系统性能 |
| UI显示更新 | ❌ 否 | 100ms刷新一次,足够快 |
二、KISS原则:保持简单
2.1 原则定义
KISS = Keep It Simple, Stupid
最早由美国海军在1960年提出,核心思想是:大多数系统如果保持简单而不是复杂,会工作得更好。
相关格言:
-
“Simplicity is the ultimate sophistication.” —— Leonardo da Vinci
简单是终极的复杂 -
“Everything should be made as simple as possible, but not simpler.” —— Albert Einstein
凡事应尽可能简单,但不能过于简单 -
“Less is more.” —— Ludwig Mies van der Rohe
少即是多
2.2 函数设计的简单之道
2.2.1 参数数量
规则: 函数参数不超过3-5个
/* ❌ 参数过多 */
void ConfigSensor(
uint8_t addr,
uint8_t mode,
uint16_t threshold,
uint16_t hysteresis,
bool enable_interrupt,
bool enable_filter,
uint8_t filter_level,
uint32_t timeout) // 8个参数,难以记忆!
{
// ...
}
/* ✅ 使用结构体封装 */
typedef struct {
uint16_t threshold;
uint16_t hysteresis;
uint8_t filter_level;
bool enable_interrupt;
bool enable_filter;
} SensorConfig_t;
void ConfigSensor(uint8_t addr, const SensorConfig_t *config)
{
// 只有2个参数,清晰明了
}
嵌入式应用: HAL库就是这样设计的
// STM32 HAL库的设计
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 2个参数
HAL_UART_Transmit(&huart1, data, size, timeout); // 4个参数
2.2.2 函数长度
规则: 一个函数不超过50行(最多100行)
/* ❌ 函数过长 */
void ProcessData(void)
{
// 200行代码
// 做了10件不同的事情
// 难以理解和维护
}
/* ✅ 拆分为多个函数 */
void ProcessData(void)
{
if (!ValidateInput())
{
HandleError();
return;
}
uint8_t *buffer = AllocateBuffer();
if (buffer == NULL)
{
return;
}
ReadData(buffer);
TransformData(buffer);
SendData(buffer);
FreeBuffer(buffer);
}
2.3 控制流的简化
2.3.1 减少嵌套层次
规则: 嵌套不超过3层
/* ❌ 深层嵌套 */
void CheckStatus(void)
{
if (device_ready)
{
if (data_available)
{
if (buffer_not_full)
{
if (checksum_valid)
{
// 终于到达这里!
ProcessData();
}
}
}
}
}
/* ✅ 提前返回(Guard Clauses)*/
void CheckStatus(void)
{
if (!device_ready) return;
if (!data_available) return;
if (buffer_full) return;
if (!checksum_valid) return;
// 主逻辑清晰可见
ProcessData();
}
嵌入式应用:中断服务函数
/* ISR中使用提前返回 */
void EXTI0_IRQHandler(void)
{
/* 检查标志位 */
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) == RESET)
{
return; // 不是我的中断,直接返回
}
/* 清除中断标志 */
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
/* 处理中断 */
g_button_pressed = true;
}
三、奥卡姆剃刀定律:如无必要勿增实体
3.1 原则起源
奥卡姆剃刀(Occam’s Razor)
“Entities should not be multiplied without necessity.”
如无必要,勿增实体。
14世纪哲学家奥卡姆的威廉(William of Occam)提出,意思是:在众多解释中,最简单的往往是正确的。
在编程中的含义:
- 不要添加不必要的抽象层
- 不要引入不必要的复杂性
- 选择最简单的解决方案
3.2 避免过度设计
3.2.1 不必要的抽象层
/* ❌ 过度抽象:3层封装只为读一个寄存器 */
typedef struct {
void (*read)(void*);
void (*write)(void*, uint32_t);
void *context;
} GenericRegister_t;
typedef struct {
GenericRegister_t base;
uint32_t offset;
} SpecificRegister_t;
uint32_t ReadRegister(SpecificRegister_t *reg)
{
return reg->base.read(reg->base.context);
}
/* ✅ 直接访问 */
#define LED_GPIO_PORT GPIOA
#define LED_GPIO_PIN GPIO_PIN_5
void LED_On(void)
{
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_GPIO_PIN, GPIO_PIN_SET);
}
嵌入式原则: 除非需要支持多个硬件平台,否则直接访问更清晰
3.2.2 不必要的设计模式
/* ❌ 过度使用设计模式:单例模式 */
typedef struct {
uint8_t data;
bool initialized;
} Sensor_t;
static Sensor_t *s_instance = NULL;
Sensor_t* Sensor_GetInstance(void)
{
if (s_instance == NULL)
{
s_instance = malloc(sizeof(Sensor_t)); // 嵌入式中避免malloc!
s_instance->initialized = false;
}
return s_instance;
}
/* ✅ 简单的全局变量 */
static Sensor_t s_sensor = {0};
void Sensor_Init(void)
{
s_sensor.data = 0;
// 初始化传感器
}
uint8_t Sensor_Read(void)
{
return s_sensor.data;
}
嵌入式原则: 单片机通常只有一个实例,使用静态全局变量即可
3.3 简化数据结构
/* ❌ 过度设计:为了"灵活性" */
typedef struct Node {
void *data; // 泛型指针
size_t data_size; // 数据大小
int type; // 类型标记
struct Node *next; // 链表
struct Node *prev; // 双向链表
void (*destructor)(void*); // 析构函数
void *user_data; // 用户数据
} GenericNode_t;
/* ✅ 简单直接:只保留需要的 */
typedef struct {
uint8_t data[32]; // 固定大小
uint8_t length; // 实际长度
bool valid; // 有效标志
} DataPacket_t;
四、YAGNI原则:你不会需要它
4.1 原则定义
YAGNI = You Aren’t Gonna Need It
来源于极限编程(XP),核心思想:
不要实现当前不需要的功能。
4.2 常见的YAGNI违反
4.2.1 "将来可能需要"的功能
/* ❌ 提前实现未来功能 */
typedef struct {
uint8_t sensor1_data; // 当前使用
uint8_t sensor2_data; // 当前使用
uint8_t sensor3_data; // 预留,可能以后用
uint8_t sensor4_data; // 预留,可能以后用
uint8_t sensor5_data; // 预留,可能以后用
uint16_t reserved[10]; // 保留字段
} SensorData_t; // 浪费内存!
void ProcessAllSensors(SensorData_t *data)
{
ProcessSensor1(data->sensor1_data);
ProcessSensor2(data->sensor2_data);
// sensor3-5的处理函数还没写,但结构体已经占用了内存
}
/* ✅ 只实现当前需要的 */
typedef struct {
uint8_t sensor1_data;
uint8_t sensor2_data;
} SensorData_t; // 需要时再扩展
4.2.2 "通用化"的陷阱
/* ❌ 过度通用化:支持所有可能的情况 */
void ConfigurePort(
GPIO_TypeDef *port,
uint16_t pin,
uint32_t mode,
uint32_t pull,
uint32_t speed,
uint32_t alternate, // 当前项目根本不用alternate功能
uint32_t options) // 什么options?没人知道
{
// 一大堆复杂的配置逻辑
}
/* ✅ 只实现需要的功能 */
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
void Button_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
4.3 何时违反YAGNI
可以提前设计的情况:
✅ 硬件接口设计
/* PCB已经设计好,预留了4个LED,虽然当前只用2个 */
#define LED1_PIN GPIO_PIN_0
#define LED2_PIN GPIO_PIN_1
#define LED3_PIN GPIO_PIN_2 // 预留
#define LED4_PIN GPIO_PIN_3 // 预留
// 可以定义宏,但不实现不用的函数
✅ 协议预留字段
/* 通信协议需要考虑扩展性 */
typedef struct {
uint8_t header; // 0xAA
uint8_t version; // 版本号,为将来升级预留
uint8_t length;
uint8_t data[64];
uint8_t checksum;
} Protocol_t;
五、DRY原则:不要重复自己
5.1 原则定义
DRY = Don’t Repeat Yourself
来源于《程序员修炼之道》,核心思想:
每个知识点在系统中应该有且只有一个明确的、权威的表示。
5.2 代码重复的类型
5.2.1 完全重复(应该消除)
/* ❌ 相同的业务逻辑重复 */
float ReadSensor1(void)
{
uint16_t adc = HAL_ADC_GetValue(&hadc1);
float voltage = adc * 3.3f / 4096.0f;
return voltage * 100.0f; // 转换为温度
}
float ReadSensor2(void)
{
uint16_t adc = HAL_ADC_GetValue(&hadc2);
float voltage = adc * 3.3f / 4096.0f; // 完全相同的公式
return voltage * 100.0f; // 完全相同的转换
}
/* ✅ 提取公共逻辑 */
float ADC_ToVoltage(uint16_t adc)
{
return adc * 3.3f / 4096.0f;
}
float Voltage_ToTemperature(float voltage)
{
return voltage * 100.0f;
}
float ReadSensor(ADC_HandleTypeDef *hadc)
{
uint16_t adc = HAL_ADC_GetValue(hadc);
float voltage = ADC_ToVoltage(adc);
return Voltage_ToTemperature(voltage);
}
5.2.2 结构相似(权衡是否消除)
/* ⚠️ 结构相似,但上下文不同 */
void Init_UART1(void)
{
__HAL_RCC_USART1_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
void Init_UART2(void)
{
__HAL_RCC_USART2_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_2 | GPIO_PIN_3; // 不同引脚
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART2; // 不同alternate
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
// 这种情况可以保持分开,因为:
// 1. 代码量小(10行左右)
// 2. 硬件配置不同
// 3. 合并会增加参数复杂度
// 4. 分开更清晰
5.3 配置数据的复用
/* ✅ 使用配置表 */
typedef struct {
GPIO_TypeDef *port;
uint16_t pin;
uint8_t active_level; // 0=低电平有效,1=高电平有效
} LED_Config_t;
static const LED_Config_t LED_Configs[] = {
{GPIOA, GPIO_PIN_0, 1}, // LED1
{GPIOA, GPIO_PIN_1, 1}, // LED2
{GPIOB, GPIO_PIN_5, 0}, // LED3,低电平有效
};
void LED_On(uint8_t led_id)
{
const LED_Config_t *cfg = &LED_Configs[led_id];
GPIO_PinState state = cfg->active_level ? GPIO_PIN_SET : GPIO_PIN_RESET;
HAL_GPIO_WritePin(cfg->port, cfg->pin, state);
}
5.4 魔数的消除
/* ❌ 魔数散布在代码中 */
void DelayMs(uint32_t ms)
{
for (uint32_t i = 0; i < ms * 8000; i++) // 8000是什么?
{
__NOP();
}
}
if (temperature > 50) // 50是什么意思?
{
// ...
}
/* ✅ 使用常量 */
#define CPU_FREQ_MHZ 72
#define LOOPS_PER_MS (CPU_FREQ_MHZ * 1000 / 8)
#define TEMP_WARNING_LEVEL 50 // 摄氏度
void DelayMs(uint32_t ms)
{
for (uint32_t i = 0; i < ms * LOOPS_PER_MS; i++)
{
__NOP();
}
}
if (temperature > TEMP_WARNING_LEVEL)
{
TriggerWarning();
}
六、单一职责原则(SRP)
6.1 原则定义
SRP = Single Responsibility Principle
一个模块/函数应该只有一个引起它变化的原因。
Robert C. Martin(Uncle Bob)在SOLID原则中提出。
6.2 函数级别的SRP
/* ❌ 一个函数做太多事情 */
void ProcessSensorData(void)
{
// 1. 读取传感器
uint16_t adc = ADC_Read();
// 2. 转换数据
float value = adc * 3.3f / 4096.0f;
// 3. 滤波
static float buffer[10];
float filtered = MovingAverage(value, buffer, 10);
// 4. 判断范围
if (filtered > 100.0f) {
// 处理超限
}
// 5. 保存到EEPROM
EEPROM_Write(0x1000, (uint8_t*)&filtered, sizeof(filtered));
// 6. 显示
LCD_Display(filtered);
// 7. 发送到上位机
UART_Send((uint8_t*)&filtered, sizeof(filtered));
}
/* ✅ 拆分职责 */
float ReadSensor(void)
{
uint16_t adc = ADC_Read();
return adc * 3.3f / 4096.0f;
}
float FilterValue(float value)
{
static float buffer[10];
return MovingAverage(value, buffer, 10);
}
bool CheckRange(float value)
{
return (value >= 0.0f && value <= 100.0f);
}
void LogValue(float value)
{
EEPROM_Write(0x1000, (uint8_t*)&value, sizeof(value));
}
void DisplayValue(float value)
{
LCD_Display(value);
}
void SendValue(float value)
{
UART_Send((uint8_t*)&value, sizeof(value));
}
// 主流程清晰
void ProcessSensorData(void)
{
float raw = ReadSensor();
float filtered = FilterValue(raw);
if (CheckRange(filtered))
{
LogValue(filtered);
DisplayValue(filtered);
SendValue(filtered);
}
}
6.3 模块级别的SRP
嵌入式应用:驱动模块设计
/* ✅ 每个驱动模块只负责一个硬件 */
// led.c - 只负责LED控制
void LED_Init(void);
void LED_On(uint8_t id);
void LED_Off(uint8_t id);
void LED_Toggle(uint8_t id);
// button.c - 只负责按键检测
void Button_Init(void);
bool Button_IsPressed(uint8_t id);
// uart.c - 只负责串口通信
void UART_Init(uint32_t baudrate);
void UART_Send(const uint8_t *data, uint16_t len);
uint16_t UART_Receive(uint8_t *buffer, uint16_t size);
// sensor.c - 只负责传感器数据采集
void Sensor_Init(void);
float Sensor_Read(void);
七、高内聚低耦合
7.1 原则定义
内聚(Cohesion): 模块内部元素的相关性
耦合(Coupling): 模块之间的依赖程度
目标:
- 高内聚:模块内功能紧密相关
- 低耦合:模块间依赖尽可能少
7.2 降低耦合的方法
7.2.1 避免全局变量
/* ❌ 过度使用全局变量 */
float g_temperature; // 全局变量
float g_humidity;
bool g_sensor_error;
void ReadTemperature(void)
{
g_temperature = ADC_Read() * 0.1f;
}
void ProcessData(void)
{
if (g_temperature > 50.0f) // 依赖全局变量
{
g_sensor_error = true;
}
}
/* ✅ 使用参数和返回值 */
typedef struct {
float temperature;
float humidity;
bool error;
} SensorData_t;
void ReadSensorData(SensorData_t *data)
{
if (data == NULL) return;
data->temperature = ADC_Read() * 0.1f;
data->humidity = ADC_Read() * 0.2f;
data->error = false;
}
void ProcessData(const SensorData_t *data)
{
if (data == NULL) return;
if (data->temperature > 50.0f)
{
TriggerAlarm();
}
}
7.2.2 使用接口隔离
/* ✅ 通过接口访问,而非直接访问全局变量 */
// sensor.c
static SensorData_t s_sensor_data = {0}; // 私有数据
void Sensor_Init(void)
{
memset(&s_sensor_data, 0, sizeof(s_sensor_data));
}
// 只暴露必要的接口
float Sensor_GetTemperature(void)
{
return s_sensor_data.temperature;
}
float Sensor_GetHumidity(void)
{
return s_sensor_data.humidity;
}
bool Sensor_HasError(void)
{
return s_sensor_data.error;
}
7.3 提高内聚的方法
将相关功能组织在一起:
/* ✅ 按功能模块组织 */
// pwm_output.c - PWM输出相关的所有功能
typedef struct {
uint32_t frequency;
uint8_t duty_cycle;
bool enabled;
} PWM_Config_t;
static PWM_Config_t s_pwm_config = {0};
void PWM_Init(void);
void PWM_SetFrequency(uint32_t freq);
void PWM_SetDutyCycle(uint8_t duty);
void PWM_Start(void);
void PWM_Stop(void);
uint32_t PWM_GetFrequency(void);
uint8_t PWM_GetDutyCycle(void);
八、面向接口编程
8.1 C语言中的接口
C语言没有interface关键字,但可以使用:
- 函数指针
- 结构体+函数指针
- 头文件定义接口
8.2 使用函数指针实现多态
/* ✅ 传感器接口定义 */
typedef struct {
void (*init)(void);
float (*read)(void);
const char* name;
} Sensor_Interface_t;
// 温度传感器实现
void TempSensor_Init(void)
{
// 初始化温度传感器
}
float TempSensor_Read(void)
{
return ADC_Read() * 0.1f; // 温度转换
}
const Sensor_Interface_t TempSensor = {
.init = TempSensor_Init,
.read = TempSensor_Read,
.name = "Temperature"
};
// 湿度传感器实现
void HumiSensor_Init(void)
{
// 初始化湿度传感器
}
float HumiSensor_Read(void)
{
return ADC_Read() * 0.2f; // 湿度转换
}
const Sensor_Interface_t HumiSensor = {
.init = HumiSensor_Init,
.read = HumiSensor_Read,
.name = "Humidity"
};
// 通用处理函数
void ProcessSensor(const Sensor_Interface_t *sensor)
{
if (sensor == NULL) return;
sensor->init();
float value = sensor->read();
printf("%s: %.2f\n", sensor->name, value);
}
// 使用
void main(void)
{
ProcessSensor(&TempSensor);
ProcessSensor(&HumiSensor);
}
8.3 HAL抽象层示例
STM32 HAL库就是面向接口编程的典范:
// 所有UART都实现相同的接口
HAL_UART_Transmit(&huart1, data, len, timeout);
HAL_UART_Transmit(&huart2, data, len, timeout);
HAL_UART_Transmit(&huart3, data, len, timeout);
// 所有GPIO都实现相同的接口
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET);
九、自解释的代码
9.1 命名的重要性
好的命名 > 注释
/* ❌ 需要注释才能理解 */
int d; // 延时时间(毫秒)
int t; // 温度
int f; // 标志位
void p(int x) { // 处理数据
// ...
}
/* ✅ 自解释的命名 */
int delay_ms; // 或 delayInMilliseconds
int temperature_celsius;
bool is_sensor_ready;
void ProcessSensorData(int raw_value) {
// ...
}
9.2 魔数的替代
/* ❌ 魔数 */
if (status & 0x08) { // 0x08是什么意思?
// ...
}
for (int i = 0; i < 100; i++) { // 100是什么?
buffer[i] = 0;
}
/* ✅ 使用常量 */
#define STATUS_READY_BIT 0x08
#define BUFFER_SIZE 100
if (status & STATUS_READY_BIT) {
// 清晰明了
}
for (int i = 0; i < BUFFER_SIZE; i++) {
buffer[i] = 0;
}
9.3 避免缩写
/* ❌ 过度缩写 */
void cfg_tmp_sns(void); // 什么意思?
int calc_avg_val(int *arr, int n); // calc? avg? val?
/* ✅ 完整单词 */
void ConfigureTemperatureSensor(void);
int CalculateAverageValue(int *array, int count);
/* ✅ 公认的缩写可以使用 */
int adc_value; // ADC是公认缩写
int uart_baudrate; // UART是公认缩写
int max_temperature; // max/min是常见缩写
十、防御性编程
10.1 输入参数检查
/* ✅ 检查所有指针参数 */
void ProcessData(const uint8_t *data, uint16_t length)
{
// 参数校验
if (data == NULL)
{
return; // 或返回错误码
}
if (length == 0 || length > MAX_DATA_SIZE)
{
return;
}
// 正常处理
for (uint16_t i = 0; i < length; i++)
{
ProcessByte(data[i]);
}
}
10.2 数组边界检查
/* ❌ 没有边界检查 */
void SetValue(int index, int value)
{
g_array[index] = value; // 危险!可能越界
}
/* ✅ 边界检查 */
#define ARRAY_SIZE 10
void SetValue(int index, int value)
{
if (index < 0 || index >= ARRAY_SIZE)
{
// 记录错误
return;
}
g_array[index] = value;
}
10.3 断言的使用
/* ✅ 使用断言检查前置条件 */
#include <assert.h>
void ConfigureClock(uint32_t frequency)
{
// 开发阶段的断言
assert(frequency >= 1000000); // 至少1MHz
assert(frequency <= 72000000); // 不超过72MHz
// 生产环境的检查
if (frequency < 1000000 || frequency > 72000000)
{
// 使用默认值
frequency = 8000000;
}
// 配置时钟
RCC_SetFrequency(frequency);
}
十一、错误处理原则
11.1 返回值vs错误码
/* ✅ 方案1:返回错误码 */
typedef enum {
SENSOR_OK = 0,
SENSOR_ERROR_TIMEOUT,
SENSOR_ERROR_INVALID_DATA,
SENSOR_ERROR_NOT_READY
} SensorError_t;
SensorError_t Sensor_Read(float *out_value)
{
if (out_value == NULL)
{
return SENSOR_ERROR_INVALID_DATA;
}
if (!Sensor_IsReady())
{
return SENSOR_ERROR_NOT_READY;
}
*out_value = ADC_Read() * 0.1f;
return SENSOR_OK;
}
// 使用
float temperature;
SensorError_t err = Sensor_Read(&temperature);
if (err != SENSOR_OK)
{
// 处理错误
HandleError(err);
}
11.2 统一的错误处理
/* ✅ 全局错误码定义 */
typedef enum {
ERR_OK = 0,
ERR_NULL_POINTER,
ERR_INVALID_PARAM,
ERR_TIMEOUT,
ERR_BUSY,
ERR_NOT_READY,
ERR_HARDWARE_FAULT
} ErrorCode_t;
/* 错误处理函数 */
void ReportError(ErrorCode_t error, const char *module)
{
// 记录错误日志
LogError(error, module);
// 根据严重程度处理
if (error == ERR_HARDWARE_FAULT)
{
SystemReset(); // 硬件故障,重启
}
}
11.3 嵌入式错误恢复策略
/* ✅ 超时重试机制 */
#define MAX_RETRY_COUNT 3
ErrorCode_t SendDataWithRetry(const uint8_t *data, uint16_t len)
{
int retry = 0;
ErrorCode_t err;
while (retry < MAX_RETRY_COUNT)
{
err = UART_Transmit(data, len, 1000); // 1秒超时
if (err == ERR_OK)
{
return ERR_OK; // 成功
}
retry++;
Delay(100); // 重试前延时
}
return ERR_TIMEOUT; // 重试失败
}
十二、资源受限下的设计权衡
12.1 内存优化
栈vs堆的选择:
/* ❌ 嵌入式中避免频繁malloc */
void ProcessData(void)
{
uint8_t *buffer = malloc(1024); // 危险!
if (buffer == NULL) return;
// 处理数据
free(buffer);
}
/* ✅ 使用静态内存 */
#define BUFFER_SIZE 1024
static uint8_t s_buffer[BUFFER_SIZE];
void ProcessData(void)
{
memset(s_buffer, 0, BUFFER_SIZE);
// 处理数据
}
12.2 代码空间优化
/* ⚠️ 函数vs宏的权衡 */
// 方案1:函数(推荐)- 可读但占用代码空间
static inline uint16_t GetMax(uint16_t a, uint16_t b)
{
return (a > b) ? a : b;
}
// 方案2:宏 - 节省空间但不安全
#define GET_MAX(a, b) ((a) > (b) ? (a) : (b))
// 嵌入式建议:优先使用inline函数,编译器会优化
十三、实时性与确定性
13.1 避免不确定的时间开销
/* ❌ 中断中执行耗时操作 */
void UART_RxIRQHandler(void)
{
uint8_t data = UART_ReadByte();
// 错误:在中断中处理复杂逻辑
ProcessComplexProtocol(data); // 耗时未知
SaveToFlash(data); // 耗时长
}
/* ✅ 中断只做最小工作 */
#define RX_BUFFER_SIZE 64
static uint8_t s_rx_buffer[RX_BUFFER_SIZE];
static volatile uint16_t s_rx_head = 0;
static volatile uint16_t s_rx_tail = 0;
void UART_RxIRQHandler(void)
{
uint8_t data = UART_ReadByte();
// 只接收数据放入缓冲区
uint16_t next = (s_rx_head + 1) % RX_BUFFER_SIZE;
if (next != s_rx_tail)
{
s_rx_buffer[s_rx_head] = data;
s_rx_head = next;
}
}
// 主循环处理
void main_loop(void)
{
while (s_rx_tail != s_rx_head)
{
uint8_t data = s_rx_buffer[s_rx_tail];
s_rx_tail = (s_rx_tail + 1) % RX_BUFFER_SIZE;
ProcessComplexProtocol(data);
}
}
十四、可移植性设计
14.1 类型的可移植性
/* ❌ 依赖平台的类型 */
int value; // 16位还是32位?
long counter; // 32位还是64位?
char status; // 有符号还是无符号?
/* ✅ 使用标准固定宽度类型 */
#include <stdint.h>
uint32_t value; // 明确32位
int16_t offset; // 明确16位有符号
uint8_t status; // 明确8位无符号
14.2 HAL层抽象
/* ✅ 硬件抽象层设计 */
// hal_gpio.h - 接口定义
typedef enum {
GPIO_PIN_RESET = 0,
GPIO_PIN_SET = 1
} GPIO_PinState_t;
void HAL_GPIO_WritePin(uint8_t pin, GPIO_PinState_t state);
GPIO_PinState_t HAL_GPIO_ReadPin(uint8_t pin);
// hal_gpio_stm32.c - STM32实现
void HAL_GPIO_WritePin(uint8_t pin, GPIO_PinState_t state)
{
if (state == GPIO_PIN_SET)
GPIOA->BSRR = (1 << pin);
else
GPIOA->BRR = (1 << pin);
}
// hal_gpio_esp32.c - ESP32实现
void HAL_GPIO_WritePin(uint8_t pin, GPIO_PinState_t state)
{
gpio_set_level(pin, state);
}
总结与建议
编程原则的优先级
-
首要原则(必须遵守)
- 防御性编程(安全第一)
- 错误处理(可靠性)
- 自解释的代码(可维护性)
-
核心原则(强烈推荐)
- KISS原则
- 单一职责
- 避免过早优化
-
高级原则(视情况应用)
- DRY原则(权衡复杂度)
- 面向接口编程
- 奥卡姆剃刀
嵌入式开发的特殊考虑
| 方面 | 通用软件 | 嵌入式软件 |
|---|---|---|
| 内存 | 充足 | 受限,慎用动态分配 |
| 性能 | 优化次要 | 关键路径必须优化 |
| 实时性 | 无严格要求 | 必须保证确定性 |
| 可移植性 | Nice to have | 常需要跨平台 |
| 代码重复 | 尽量避免 | 适度接受(简单优先) |
实践建议
-
从简单开始
- 先让代码工作
- 再追求优雅
- 最后优化性能
-
持续重构
- 小步迭代
- 保持测试
- 及时清理
-
团队协作
- 统一编码规范
- 代码审查
- 知识分享
-
工具辅助
- 静态分析(Cppcheck)
- 代码格式化(clang-format)
- 版本控制(Git)
延伸阅读
经典书籍
- 《代码大全》(Code Complete)- Steve McConnell
- 《程序员修炼之道》(The Pragmatic Programmer)
- 《重构》(Refactoring)- Martin Fowler
- 《嵌入式C语言设计模式》
编码规范
- MISRA C(汽车行业标准)
- BARR-C(嵌入式C编码标准)
- Linux内核编码规范
- Google C++风格指南
在线资源
- Embedded Artistry Blog
- Interrupt by Memfault
- Stack Overflow嵌入式标签
本文完 ✨
记住:简单、可靠、可维护的代码比聪明的代码更有价值。

1381

被折叠的 条评论
为什么被折叠?



