• 正文
  • 相关推荐
申请入驻 产业图谱

嵌入式中通常应避免哪些编程实践?

03/23 09:20
410
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好,我是杂烩君。

今天咱们聊点实在的。做嵌入式开发,坑实在是太多了。有些代码看着挺顺眼,跑起来却像个定时炸弹。今天给大家盘点一下那些应该尽量避免的编程实践。

1. 不检查函数返回值

这个太常见了。I2C读写、UART发送、内存分配,这些函数的返回值很多人直接忽略。

HAL_I2C_Master_Transmit(&hi2c1, addr, data, len, 100);
// 万一发送失败呢?

如果外设通信失败,代码继续往下执行,后面的操作都会基于错误的假设。这种bug在现场重现时,往往是一堆连锁反应,真正的问题源头反而被掩盖了。

正确做法:检查返回值,出错要有处理逻辑。在主循环或任务里可以加重试,用定时器延时而非阻塞 delay;裸机可用 HAL_Delay,跑 RTOS 则用 vTaskDelay 让出 CPU

HAL_StatusTypeDef status = HAL_ERROR;
for (int retry = 0; retry < 3 && status != HAL_OK; retry++) 
{
    status = HAL_I2C_Master_Transmit(&hi2c1, addr, data, len, 100);
    if (status != HAL_OK) 
    {
        HAL_Delay(10);  // 裸机。若用 RTOS 则 vTaskDelay(pdMS_TO_TICKS(10));
    }
}
if (status != HAL_OK) 
{
    handle_i2c_error();
}

2. 任务栈配太小

RTOS任务栈配太小,某个任务一跑就 HardFault。

RTOS 下每个任务有独立栈。新手常按「够用就行」给 128、256 字节,结果该任务调用层级一深、局部变量一大,直接栈溢出,表现就是某任务一执行就 HardFault,排查半天才发现是栈爆了。

// 危险:栈可能不够
xTaskCreate(task_handler, "Task", 128, NULL, 1, NULL);  

// 更危险:递归、大局部数组、printf 等都很吃栈
void task_handler(void* pv) 
{
    char buf[200];   
    sprintf(buf, "...");  
    parse_protocol();     // 可能还有多层调用
}

正确做法:先给任务栈留足余量,再用栈水位检测(如 FreeRTOS 的 uxTaskGetStackHighWaterMark)观察实际占用,逐步收紧。有递归、printf、大数组的任务尤其要多留。

相关文章:FreeRTOS 任务栈:翻车原因、定位方法与防范技巧

3. 死锁

两任务互相等锁,系统直接卡死。

任务 A 先拿 mutex1 再拿 mutex2,任务 B 先拿 mutex2 再拿 mutex1。某一时刻 A 拿着 1 等 2,B 拿着 2 等 1,谁也等不到,全系统卡死。锁顺序不一致是死锁的典型原因。

// 任务 A
xSemaphoreTake(mutex1, portMAX_DELAY);
xSemaphoreTake(mutex2, portMAX_DELAY);  // 若 B 已拿 mutex2,A 死等
// ... 操作 ...
xSemaphoreGive(mutex2);
xSemaphoreGive(mutex1);

// 任务 B:顺序相反,埋下死锁隐患
xSemaphoreTake(mutex2, portMAX_DELAY);
xSemaphoreTake(mutex1, portMAX_DELAY);  // 若 A 已拿 mutex1,B 死等

正确做法:全项目统一锁顺序(如按地址排序:先低地址后高地址),所有任务按同一顺序加锁。或者用 xSemaphoreTake(..., 0) 非阻塞尝试,拿不到就释放已持有的锁、重试,避免永久阻塞。

4. 在中断里调用阻塞型 API

RTOS,在中断里调用阻塞型 API —— 直接挂死或断言。

FreeRTOS 的 xQueueSendxSemaphoreTakevTaskDelay 这些都会阻塞,绝对不能在 ISR 里调用。ISR 里必须用带 FromISR 后缀的版本,且不能带阻塞超时。

// 错误!在 UART 中断里这样写会挂死或触发 configASSERT
void UART_IRQHandler(void) {
    uint8_t byte = read_uart_byte();
    xQueueSend(rx_queue, &byte, 0);  // 错误:阻塞型 API
}

// 正确:用 FromISR 版本,且根据需要决定是否触发任务切换
void UART_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint8_t byte = read_uart_byte();
    xQueueSendFromISR(rx_queue, &byte, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

5. 魔法数字满天飞

看这段代码:

if(status == 0x03) 
{
    reg = 0x1F;
    delay(10);
} 
else if(status == 0x05) 
{
    reg = 0x3F;
    delay(20);
}

0x03代表什么?0x1F又是什么意思?10和20为什么是这个值?三个月后你自己都看不懂。

正确做法:用宏定义或者枚举给这些数字起个有意义的名字。

#define SENSOR_MODE_ACTIVE   0x03
#define SENSOR_MODE_STANDBY  0x05
#define CONFIG_REG_ACTIVE    0x1F
#define CONFIG_REG_STANDBY   0x3F
#define DELAY_ACTIVE_MS      10
#define DELAY_STANDBY_MS     20

if(status == SENSOR_MODE_ACTIVE) 
{
    reg = CONFIG_REG_ACTIVE;
    delay(DELAY_ACTIVE_MS);
} 
elseif(status == SENSOR_MODE_STANDBY) 
{
    reg = CONFIG_REG_STANDBY;
    delay(DELAY_STANDBY_MS);
}

6. 忽略编译器警告

先说说最普遍的一个问题。

很多中大型项目,代码模块比较多,很多人编译代码时,看到满屏的警告跟没看见一样,只要编译不报错了就觉得万事大吉。这种做法在嵌入式开发里,简直是给自己埋雷。

来看个例子:

uint8_t data_buffer[100];
uint16_t index = 300;

data_buffer[index] = 0x55;

编译器可能会提示“possible loss of data”或者“conversion from ‘uint16_t’ to ‘uint8_t’ may change value”,但很多人选择无视。结果呢?index被截断成44,写到了完全错误的位置。这种bug在调试时能把人折磨疯。

正确做法:把警告级别调到最高(比如-Wall -Wextra),所有警告都要处理掉。实在无法消除的,也要明确知道原因并加上注释说明。

7. 文件描述符泄漏

嵌入式 Linux,文件描述符泄漏 ,跑着跑着 "Too many open files"。

open() 失败返回 -1 不检查就继续用、open() 成功但忘了 close(),都会出问题。设备节点、socket、普通文件反复打开不关,文件描述符迟早耗尽,报 "Too many open files",进程直接废掉。

// 不推荐:打开不关、失败不检查
int fd = open("/dev/ttyS0", O_RDWR);
write(fd, data, len);        
// 没有 close(fd),每次调用泄漏一个 fd

正确做法:检查 open() 返回值,失败则处理错误并返回;成功则确保所有路径都有 close(),用 goto 集中清理,或封装成 with 风格的函数。

int fd = open("/dev/ttyS0", O_RDWR);
if (fd < 0) 
{
    perror("open");
    return -1;
}
if (write(fd, data, len) != (ssize_t)len) 
{
    close(fd);
    return -1;
}
close(fd);

8. signal handler处理不当

嵌入式 Linux,信号处理函数里调用非 async-signal-safe 函数 —— 可能死锁或崩溃。

在 signal handler 里,你只能调用 POSIX 规定的 async-signal-safe 函数。printfmallocpthread_mutex_lock 等都不在其中,调用即未定义行为,可能死锁、崩溃,或者覆盖 errno。

// 危险!printf、malloc 等在信号处理函数里不可用
void sig_handler(int sig) 
{
    printf("received signal %dn", sig);   // 可能死锁
    char* buf = malloc(64);                // 可能破坏堆
    sprintf(buf, "sig %d", sig);
    // ...
}

正确做法:handler 里只做最少的事,比如写一个 volatile sig_atomic_t 标志,或调用 write() 往 fd 写几个字节。复杂逻辑放到主循环或专门线程里,轮询该标志后再处理。

volatile sig_atomic_t g_signal_received = 0;
void sig_handler(int sig) 
{
    g_signal_received = sig;  // 仅设置标志,安全
}

9. 忽略 read/write 的返回值

嵌入式 Linux,忽略 read/write 的返回值与短读短写 —— 数据丢一半。

read()write() 可能一次只读/写部分数据,返回值才是实际字节数。不检查就假设「一次搞定」,在管道、socket、某些驱动下会丢数据或写不完整。

// 错误:假设一次 read 读完
char buf[256];
read(fd, buf, sizeof(buf));    // 可能只读到 50 字节
process_packet(buf);           // 按完整包处理,数据不完整

正确做法:循环读写直到满/尽,并处理 EINTR、EAGAIN 等。write() 失败或短写要重试或处理错误;read() 要累加直至收齐或遇到 0/EOF。

ssize_t total = 0;
while (total < len) 
{
    ssize_t n = read(fd, buf + total, len - total);
    if (n <= 0) 
    { 
        /* 处理错误或 EOF */ 
        break; 
    }
    total += n;
}

10. 滥用动态内存分配

malloc和free在PC编程里是家常便饭,但在嵌入式系统里,这俩函数是要慎重对待的。

某项目里大量使用动态内存分配,程序跑了三天开始随机死机。查了半天,原来是内存碎片导致malloc失败返回NULL,代码里还没做判空处理。

嵌入式系统的内存本来就有限,频繁申请释放会产生碎片。更关键的是,malloc的执行时间是不确定的,这在实时系统中是个大忌。

正确做法:能用静态分配的用静态分配。真需要动态管理的,考虑用内存池(memory pool)或者预分配一个大数组自己管理。

// 不推荐
void* ptr = malloc(size);
free(ptr);

// 推荐 - 静态分配
static uint8_t buffer[256];
static uint8_t buffer_used = 0;

11. 中断服务程序里干活太多

中断ISR应该是快进快出的。有些人把大量计算、甚至延时都放在ISR里,导致其他中断响应延迟,或者主循环得不到执行。

void USART1_IRQHandler(void) 
{
    uint8_t data = USART1->DR;
    // 下面这些操作不应该在中断里做
    process_protocol(data);        // 协议解析
    save_to_buffer(data);          // 保存数据
    update_display(data);          // 更新显示
}

正确做法:ISR里只做最必要的事,比如接收数据放入缓冲区,设置一个标志位,把耗时操作放到主循环里处理。

void USART1_IRQHandler(void) 
{
    uint8_t data = USART1->DR;
    // 只做接收和标记
    rx_buffer[rx_index++] = data;
    data_ready_flag = 1;
}

int main(void) 
{
    while(1) 
    {
        if(data_ready_flag) 
        {
            data_ready_flag = 0;
            // 在这里处理数据
            process_received_data();
        }
    }
}

12. 全局变量满天飞

全局变量确实方便,但滥用全局变量会让代码耦合度极高。你在中断里改了某个全局变量,主循环在用它,另一个模块也在用它,最后谁改的、什么时候改的根本理不清。

正确做法:能用局部变量用局部变量,模块间通信用函数参数和返回值,或者用get/set接口封装。

// 不推荐
uint32_t system_tick;

// 推荐
static uint32_t system_tick;

uint32_t get_system_tick(void) 
{
    return system_tick;
}

void set_system_tick(uint32_t tick) 
{
    system_tick = tick;
}

13. 忽视volatile关键字

这是个经典问题。编译器优化有时候会坑人,特别是在处理硬件寄存器和中断共享变量时。

uint8_t flag = 0;

void ISR(void) 
{
    flag = 1;
}

void main(void) 
{
    while(!flag) 
    {
        // 编译器可能优化成 while(1)
    }
}

不加volatile的话,编译器可能认为flag在主循环里永远不会被改变,直接把while(!flag)优化成死循环。

正确做法:所有可能被中断或硬件改变的量,都要加上volatile。

volatile uint8_t flag = 0;

14. 不写注释,或者写无意义的注释

有些代码注释是这样的:

i++; // i加1

这种注释还不如不写。而有些代码根本没有注释,几个月后作者自己都看不懂。

正确做法:注释要说明“为什么这么做”,而不是“做了什么”。复杂的算法、特殊处理的原因、硬件相关的坑,这些地方一定要写清楚注释。

// 等待100us,因为传感器上电后需要稳定时间才能读取有效数据
delay_us(100);

总结

嵌入式软件开发,资源受限、实时性要求高、调试手段有限,这就要求我们在写代码时更加严谨。

说到底,好的嵌入式代码追求的是:确定性、可维护性、鲁棒性。避开这些不良实践,代码质量就能上一个台阶。

自检清单

发版前,不妨对着下面几条过一遍:

    [ ] 关键外设调用(I2C、SPI、UART)是否检查了返回值?[ ] 任务栈是否足够?[ ] 多锁是否统一了加锁顺序?[ ] ISR 里是否只调用了 FromISR 系列 API?[ ] 魔法数字?[ ] 编译时是否还有未处理的警告?[ ] 文件描述符泄漏?open 的 fd 是否都及时 close?[ ] 信号处理函数里是否只做了最小操作?[ ] read/write 是否处理了短读短写?[ ] 是否还在用 malloc/free 或类似的动态分配?[ ] 中断里是否只做了必要的事,把耗时逻辑放到主循环?[ ] 中断/多任务共享的变量是否加了 volatile?[ ] 特殊逻辑是否加了「为什么」的注释?

上面这些,你中过几条?如果还有别的坑,欢迎评论区补充

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

本公众号专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,公众号内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!