第2章 嵌入式C语言基础:数据类型、运算符、流程控制、函数、指针、结构体
各位同学,欢迎来到第二章。
说实话,C语言是咱们嵌入式开发的“吃饭家伙”。你想想看,从点亮一个LED到控制发动机喷油,底层跑的都是C代码。我见过不少新人,上来就啃RTOS、啃CAN协议栈,结果连个指针都搞不明白,最后调试到崩溃。
这一章,咱们就把地基打牢。我会把我在车载ECU开发中踩过的坑、积累的经验,都揉碎了讲给你听。
2.1 数据类型:选对类型,省一半内存
嵌入式开发里,内存是金贵的。一个单片机可能只有几KB的RAM,你浪费一个字节,可能就少了一个功能。
2.1.1 基本数据类型
标准C有char、int、float、double。但在嵌入式里,我强烈建议你只用
| 类型 | 位数 | 范围 | 我常用的场景 |
|---|---|---|---|
| uint8_t | 8 | 0 ~ 255 | 状态标志、小计数器 |
| uint16_t | 16 | 0 ~ 65535 | ADC采样值、PWM占空比 |
| uint32_t | 32 | 0 ~ 4.29e9 | 时间戳、里程累计 |
| int32_t | 32 | -2^31 ~ 2^31-1 | 温度差值、速度偏差 |
| float | 32 | ±1.18e-38 ~ ±3.4e38 | PID系数、滤波值 |
2.1.2 枚举与联合体
枚举让代码可读性翻倍。比如定义ECU工作模式:
typedef enum {
ECU_MODE_INIT = 0,
ECU_MODE_NORMAL,
ECU_MODE_DIAG,
ECU_MODE_ERROR
} EcuMode_t;
你看,比用0、1、2、3清晰多了吧?
联合体(union)在解析CAN报文时特别好用。我曾经用union把一个uint32_t拆成4个字节,省去了移位操作:
typedef union {
uint32_t raw;
struct {
uint8_t byte0;
uint8_t byte1;
uint8_t byte2;
uint8_t byte3;
} bytes;
} CanData_t;
2.2 运算符:别让优先级坑了你
运算符优先级是C语言里最容易出bug的地方之一。我见过最经典的错误:
// 错误写法
if (status & MASK == 0x01) // 实际被解析为:if (status & (MASK == 0x01))
// 正确写法
if ((status & MASK) == 0x01)
为什么会这样?因为==的优先级高于&。记住一条铁律:拿不准优先级,就加括号。没人会笑话你括号多,但bug会。
2.2.1 位运算:嵌入式工程师的必修课
在ECU开发中,寄存器操作几乎全是位运算。设置一个GPIO引脚:
// 设置第3位为1
GPIO->BSRR = (1U << 3);
// 清除第3位
GPIO->BRR = (1U << 3);
// 读取第5位
uint8_t bit = (GPIO->IDR >> 5) & 0x01;
我个人习惯把常用的位操作封装成宏:
#define SET_BIT(reg, bit) ((reg) |= (1U << (bit)))
#define CLR_BIT(reg, bit) ((reg) &= ~(1U << (bit)))
#define GET_BIT(reg, bit) (((reg) >> (bit)) & 0x01)
2.3 流程控制:状态机的灵魂
ECU软件本质上就是一个巨大的状态机。上电初始化、自检、正常运行、故障处理……每个阶段都有不同的行为。
2.3.1 if-else vs switch-case
分支少用if-else,分支多用switch-case。但要注意:switch-case里的变量必须是整型或枚举。我曾经看到有人用float做switch,编译直接报错。
switch (ecuState) {
case ECU_MODE_INIT:
initPeripherals();
break;
case ECU_MODE_NORMAL:
runControlLoop();
break;
case ECU_MODE_ERROR:
enterSafeState();
break;
default:
// 兜底处理,防止意外状态
resetECU();
break;
}
2.3.2 循环:while、for、do-while
在嵌入式里,循环常用于轮询、延时、数据搬运。比如等待外设就绪:
// 等待ADC转换完成,超时保护
uint32_t timeout = 1000;
while (!(ADC->SR & ADC_SR_EOC)) {
if (--timeout == 0) {
// 超时处理
handleAdcTimeout();
break;
}
}
注意那个超时保护。没有它,一旦ADC卡死,你的程序就永远停在那里了。这在汽车上是绝对不允许的。
2.4 函数:模块化的基石
函数让代码可复用、可测试。在ECU开发中,我习惯把每个功能模块拆成独立的.c和.h文件。比如:
- adc.c / adc.h —— ADC驱动
- can.c / can.h —— CAN通信
- pwm.c / pwm.h —— PWM输出
- control.c / control.h —— 控制算法
2.4.1 函数设计原则
一个函数只做一件事。比如:
// 不好的设计:一个函数既读温度又控制风扇
void readTempAndControlFan(void);
// 好的设计:职责分离
uint16_t readTemperature(void);
void controlFan(uint16_t temp);
你想想看,如果哪天要改风扇控制逻辑,你只需要改controlFan(),完全不影响温度读取。这就是模块化的好处。
2.4.2 静态函数与全局函数
在.c文件里,只在本文件使用的函数加static。这能防止命名冲突,也告诉阅读者:这个函数是内部实现,别乱调用。
// adc.c
static void adc_hardwareInit(void) {
// 初始化ADC寄存器
}
void adc_init(void) {
adc_hardwareInit();
// 其他初始化
}
2.5 指针:C语言的灵魂
指针是C语言最强大的特性,也是新人最容易翻车的地方。说白了,指针就是地址。你通过地址直接操作内存。
2.5.1 指针基础
uint8_t data = 0x55;
uint8_t *ptr = &data; // ptr保存了data的地址
*ptr = 0xAA; // 通过指针修改data的值
// 现在data变成了0xAA
在嵌入式里,指针最常见的用途是访问外设寄存器。比如:
// 将0x40020000地址映射为GPIO寄存器结构体指针
#define GPIOA ((GPIO_TypeDef *)0x40020000)
// 然后就可以用结构体方式访问寄存器
GPIOA->ODR |= (1U << 5); // PA5输出高电平
2.5.2 函数指针:回调机制的基石
函数指针在ECU开发中常用于中断回调、定时器回调。比如:
// 定义函数指针类型
typedef void (*TimerCallback_t)(void);
// 注册回调函数
void timer_registerCallback(TimerCallback_t cb) {
// 保存回调地址
}
// 使用示例
void myTimerHandler(void) {
// 定时器触发时执行
}
timer_registerCallback(myTimerHandler);
你看,这样就把定时器的触发逻辑和具体处理逻辑解耦了。想换处理函数?换个参数就行。
2.6 结构体:组织数据的利器
结构体把相关的数据打包在一起。在ECU里,一个传感器的所有信息都可以用一个结构体表示:
typedef struct {
uint16_t rawValue; // 原始ADC值
int16_t temperature; // 转换后的温度值(单位0.1℃)
uint8_t status; // 传感器状态
uint32_t timestamp; // 采样时间戳
} SensorData_t;
2.6.1 结构体指针与传参
在函数间传递结构体时,永远用指针。为什么?因为结构体可能很大,传值会导致栈溢出。我见过一个新人把包含1024字节数组的结构体传值,结果栈直接爆了。
// 正确做法:传指针
void processSensorData(SensorData_t *data) {
// 处理数据
data->temperature = convertAdcToTemp(data->rawValue);
}
// 调用
SensorData_t sensor;
processSensorData(&sensor);
2.6.2 结构体对齐:看不见的内存浪费
结构体成员在内存中不是紧密排列的。编译器为了访问效率,会在成员之间插入填充字节。比如:
typedef struct {
uint8_t a; // 1字节
uint32_t b; // 4字节
uint16_t c; // 2字节
} BadStruct_t; // 实际占用12字节!
优化方法:按成员大小从大到小排列:
typedef struct {
uint32_t b; // 4字节
uint16_t c; // 2字节
uint8_t a; // 1字节
} GoodStruct_t; // 实际占用8字节,省了4字节
小结
这一章的内容,说白了就是嵌入式C的“内功心法”。数据类型让你精准控制内存,运算符让你操作硬件,流程控制让你构建状态机,函数让你模块化,指针让你直接操作内存,结构体让你组织数据。
这些基础打牢了,后面学RTOS、学CAN协议、学AUTOSAR,都会轻松很多。下一章,咱们聊聊嵌入式开发中最重要的调试手段——串口和日志。到时候见。