2. C语言基础回顾
各位同学,咱们今天聊点实在的。C语言是嵌入式系统的灵魂,这话一点不夸张。我见过太多人,上来就啃RTOS、啃驱动,结果连个指针都搞不明白,最后代码跑飞了都不知道怎么查。说白了,C语言基础不牢,后面全是坑。
这一章,我带大家快速过一遍嵌入式开发中最常用的C语言核心知识点。别嫌啰嗦,这些都是我踩过坑之后才真正理解的东西。
2.1 数据类型——选对类型,省一半内存
嵌入式系统资源有限,RAM和Flash都是按KB算的。你写PC程序,int随便用,没人管你。但在单片机上,一个int占4字节,一个char占1字节,差别就出来了。
我个人习惯,能用uint8_t绝不用int。为什么?因为明确。uint8_t告诉你,这个变量范围0~255,不会出负数,也不会溢出(如果你小心的话)。
核心原则:嵌入式开发中,尽量使用
| 类型 | 大小(常见) | 取值范围 | 我常用的场景 |
|---|---|---|---|
| uint8_t | 1字节 | 0 ~ 255 | 状态标志、小计数器、GPIO电平 |
| uint16_t | 2字节 | 0 ~ 65535 | ADC采样值、PWM占空比 |
| uint32_t | 4字节 | 0 ~ 4.29e9 | 时间戳、大循环计数 |
| float | 4字节 | ±1.2e-38 ~ ±3.4e38 | 传感器数据(温度、电压) |
注意:float在有些低端MCU上不支持硬件浮点运算,用起来很慢。我建议能用整数算就别用浮点。比如温度值,直接放大10倍存成整数,显示时再处理小数点。
2.2 运算符与表达式——别让优先级坑了你
运算符优先级,这东西看着简单,但坑起来真要命。我曾经调试一个I2C通信的bug,调了整整两天,最后发现是 if (a & b == 0) 被解析成了 if (a & (b == 0))。你说冤不冤?
我的建议很简单:拿不准优先级,就加括号。括号不花钱,但能省你两天时间。
嵌入式里常用的几个运算符,我列一下:
- 位运算:&(与)、|(或)、^(异或)、~(取反)、<<(左移)、>>(右移)——寄存器操作全靠它们
- 自增自减:++i 和 i++ 的区别——前置先加后用,后置先用后加
- 三目运算符:? : ——写简单判断时很清爽,但别嵌套太多
// 位运算经典用法:操作寄存器
uint8_t reg = 0x00;
// 置位第3位
reg |= (1 << 3); // reg = 0x08
// 清零第3位
reg &= ~(1 << 3); // reg = 0x00
// 翻转第3位
reg ^= (1 << 3); // reg = 0x08
小技巧:用宏定义给位操作起个名字,代码可读性会好很多。比如 #define SET_BIT(reg, bit) ((reg) |= (1 << (bit)))
2.3 流程控制语句——别写又臭又长的if-else
流程控制,说白了就是让程序按你的想法走。if-else、switch-case、for、while,这些大家都会用。但嵌入式里有个特殊要求:代码要快,要确定。
我见过有人写了一个if-else嵌套了七八层,看得我头皮发麻。其实很多时候,用switch-case或者查表法就能解决。
// 不好的写法:多层if-else
if (state == 0) {
// do something
} else if (state == 1) {
// do something else
} else if (state == 2) {
// ...
}
// 好的写法:switch-case
switch (state) {
case 0:
// do something
break;
case 1:
// do something else
break;
case 2:
// ...
break;
default:
// 错误处理
break;
}
为什么推荐switch?因为编译器会把switch优化成跳转表,执行时间固定,不会像if-else那样一个个判断。在实时性要求高的地方,这点差别很关键。
注意:while(1)是嵌入式主循环的标配,但别忘了在里面加个看门狗喂狗操作,不然程序跑飞了都不知道。
2.4 数组与指针——嵌入式开发的灵魂
指针,C语言的精髓,也是很多人的噩梦。我刚开始学的时候也觉得指针玄乎,后来发现,说白了就是地址。你家的门牌号是地址,指针就是存这个门牌号的变量。
嵌入式里指针用得最多的地方:
- 访问寄存器:把外设寄存器地址强制转换成指针,直接读写
- 传递大块数据:传指针比传整个数组快得多
- 动态内存管理:malloc/free,但嵌入式里慎用
// 直接操作寄存器地址(以STM32的GPIO为例)
#define GPIOA_BASE 0x40020000
#define GPIOA_ODR (*(volatile uint32_t *)(GPIOA_BASE + 0x14))
// 置位PA0
GPIOA_ODR |= (1 << 0);
数组和指针的关系,记住一句话:数组名就是首元素的地址。但数组名是常量,不能自增自减;指针是变量,可以随便指。
避坑指南:我曾经在项目里用了一个野指针,结果程序跑着跑着就死机了。查了三天,发现是指针没有初始化,指向了一个随机地址。从那以后,我每次定义指针都会初始化成NULL,用之前再检查一下。
2.5 函数——模块化的基石
函数,就是把一段逻辑封装起来,给它起个名字,需要的时候调用就行。嵌入式里函数设计有几个要点:
- 函数不宜太长:我一般控制在50行以内,超过就考虑拆分
- 参数不宜太多:超过3个参数,考虑用结构体传参
- 返回值要明确:成功返回0,失败返回错误码,这是行业惯例
// 好的函数设计示例
/**
* @brief 初始化UART
* @param baudrate 波特率
* @return 0成功,-1失败
*/
int uart_init(uint32_t baudrate) {
if (baudrate == 0) {
return -1; // 参数错误
}
// 配置寄存器...
// 设置波特率...
return 0; // 成功
}
另外,嵌入式里函数调用是有开销的。每次调用都要压栈、跳转、返回。如果某个函数被频繁调用(比如中断服务函数),可以考虑用宏或者内联函数来减少开销。
小技巧:用static修饰函数,可以限制它的作用域只在当前文件。这样能避免命名冲突,也方便代码管理。我每个.c文件里都会把内部函数声明成static。
本章知识体系
下面这张图,是我自己总结的C语言基础在嵌入式中的核心逻辑。你看一眼,心里就有数了。
嗯,这一章的内容就这些。C语言基础看着简单,但真正用好、用对,需要大量的实践。我建议你每学一个知识点,都去开发板上跑一跑,看看实际效果。纸上得来终觉浅,绝知此事要躬行。
可可精品资料库