扫码加入

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

做项目的时候,被这几个C语言问题摆了一道!

03/18 14:27
330
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

我是老温,一名热爱学习的嵌入式工程师,关注我,一起变得更加优秀!

作为一名被嵌入式开发毒打了十几年的菜鸟,有时候总自信地以为对C语言的基础用法了如指掌,直到有一天项目调试遇到坑了,才深刻地意识到:

嵌入式环境下使用C语言编程,坑的从来不在于复杂的语法,而在于,C语言如何和硬件、时序、内存高度配合,让设备运行得更加高效和稳定。

有时候,一句不起眼的代码就可能会导致设备死机或者内存数据错乱,甚至有可能会造成硬件损坏,嵌入式编程的核心是操控硬件,很多代码都可能直接操作外设寄存器,这让C语言的一些特性变成了致命的坑。

下面结合我的实际情况,列举几个我以前在开发过程中遇到过的坑,供各位老铁参考。

坑一:隐式类型转换导致数值溢出。

这个坑出现在我调试串口波特率配置的时候,当时需要配置某款单片机UART波特率,而根据单片机的数据手册:波特率 = 系统时钟 / (16 * (USART_BRR寄存器值 + 1))。

假设系统时钟是72MHz,目标的波特率是9600,计算出来的USART_BRR寄存器值应该是468,(72000000/(16*9600) - 1 = 468.75,取整468)
代码如下,表面看貌似没啥问题,但串口始终无法正常通信,打印出来的都是乱码。

#include "stm32f10x.h"
void UART_Init(void) {    // 省略时钟使能、GPIO配置等代码    uint8_t brr = 72000000 / (16 * 9600) - 1; // 错误代码:uint8_t类型溢出    USART1->BRR = brr; // 配置波特率寄存器}

排查了好一会才发现,问题是出现在数据类型的转换上面,uint8_t是8位无符号数,最大值是255,而计算出来468已经超出其范围导致数据溢出,实际写入寄存器的值是468-256=212,波特率严重偏离!

如何避坑?关键在于我们配置硬件寄存器或者计算的时候,必须要明确变量类型和取值范围,避免隐式类型转换。

首先要查看寄存器的数据宽度(如USART_BRR是16位寄存器),选择用对应宽度的变量类型(uint16_t),其次,计算前先明确结果的大致范围,必要时进行显式类型转换,避免溢出,修改后的代码如下。

void UART_Init(void) {    // 省略其他代码    uint16_t brr = (uint16_t)(72000000 / (16 * 9600) - 1); // 显式转换,选用16位变量,避免溢出    USART1->BRR = brr; // 正确配置波特率寄存器}

坑二:指针操作越界,破坏寄存器

指针是C语言的灵魂,也是嵌入式编程最容易踩坑的点之一。

在嵌入式编程开发里面,指针可以通过地址映射的方式来操作硬件寄存器,但一旦指针越界,不仅会导致数据错乱,还有可能意外地修改其他寄存器的值,引发硬件异常。

在某个项目里面,需要通过指针来操作某一块外部RAM,用来临时存储传感器的数据,运行后设备频繁死机,甚至出现GPIO口出现电平异常,具体代码如下:

#define EXTERNAL_RAM_ADDR 0x60000000 // 外部RAM起始地址#define DATA_LEN 100 // 数据长度
void Data_Store(uint16_t* data) {    uint8_t i;    uint16_t* ram_ptr = (uint16_t*)EXTERNAL_RAM_ADDR;    for (i = 0; i <= DATA_LEN; i++) { // 错误:循环条件多了等号,导致指针越界(访问到DATA_LEN下标)        ram_ptr[i] = data[i];    }}

经过排查后发现,循环条件是i <= DATA_LEN,导致了指针访问到的地址是0x60000000+100*2=0x600000C8的位置,而这个地址刚好又是某个GPIO寄存器的地址,指针越界操作导致误改了GPIO的配置值,导致硬件异常死机。

避坑思路是,必须严格规定指针的访问范围。一是明确指针指向的内存或者寄存器地址范围,避免越界。二是循环操作的时候,仔细检查循环条件(如i<DATA_LEN而非i<=DATA_LEN)。

三是尽量使用数组下标代替指针直接偏移,降低指针越界的风险,四是对于硬件寄存器地址,建议使用宏定义进行明确的范围界定,避免直接写实际地址。

修改后的代码如下:

#define EXTERNAL_RAM_ADDR 0x60000000 // 外部RAM起始地址#define DATA_LEN 100 // 数据长度
void Data_Store(uint16_t* data) {    uint8_t i;    uint16_t* ram_ptr = (uint16_t*)EXTERNAL_RAM_ADDR;    // 严格控制循环范围,避免越界(仅访问0~DATA_LEN-1下标)    for (i = 0; i < DATA_LEN; i++) {         ram_ptr[i] = data[i];    }}

坑三:中断服务函数的隐蔽错误

嵌入式设备经常依赖中断处理实时事件(如定时器中断或外部中断),中断服务函数(ISR)的编写有严格的要求,一些看似无关的代码,也有可能导致中断响应延迟、时序错乱,甚至是死循环。

我曾经试过在定时器中断里面添加printf进行调试信息打印,用于查看中断的执行次数,运行后发现设备的实时性严重下降,并且传感器的数据出现大量丢包,代码如下:

uint32_t count = 0;// 定时器2中断服务函数void TIM2_IRQHandler(void) {    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {        count++;        printf("中断执行次数:%dn", count); // 错误:printf为阻塞式调用,耗时过长,影响中断时序        TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除中断标志位    }}

出现上述问题的主要原因是,printf函数属于阻塞式调用,执行过程会占用CPU资源,而定时器中断是1ms,printf的执行时间远远超过1ms,所以导致中断无法及时响应,造成传感器的数据无法及时处理,出现丢包。

如何避坑?中断服务函数主要的编写原则是“快进快出”,避免在中断里面执行耗时操作!

一是不使用printf函数或延时函数等阻塞式调用;二是中断里面只处理核心逻辑(如清除中断标志位、更新标志位);三是合理设置中断优先级,避免低优先级中断阻塞高优先级中断。

修改后的代码如下:

uint32_t count = 0;uint8_t flag = 0; // 中断标志位,用于主循环处理耗时操作// 定时器2中断服务函数(快进快出)void TIM2_IRQHandler(void) {    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {        count++;        flag = 1; // 置位标志位,主循环处理printf等耗时操作        TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 快速清除中断标志位    }}// 主循环int main(void) {    // 省略初始化代码(定时器、中断、GPIO等)    while (1) {        if (flag) {            printf("中断执行次数:%dn", count); // 主循环中执行耗时操作,不影响中断时序            flag = 0; // 清除标志位,等待下一次中断触发        }        // 其他业务逻辑    }}

最后总结就是,嵌入式C语言的坑,本质上都是对细节的忽视(忽视类型范围,忽视内存边界,忽视中断时序,等等)。

嵌入式编程开发,既要懂C语言语法,也要懂硬件特性,既要追求代码简洁,更要注重代码的安全性和稳定性。

那些让我们容易栽跟头的问题,最终会让我们在嵌入式编程道路上不断成长.

在往后写bug的日子里,多一次排查,多一分谨慎,就会少一次踩坑,才能让嵌入式设备稳定运行,也能让自己少走弯路。

相关推荐