2. C语言基础回顾:数据类型、运算符、流程控制、函数与指针
各位同学,欢迎来到第二讲。说实话,C语言基础这部分,很多人觉得枯燥。但我得说,在医疗嵌入式领域,C语言就是我们的“手术刀”。你用得熟不熟练,直接决定了代码的稳定性和安全性。今天,我带你快速过一遍核心知识点,重点放在那些“坑”上。
2.1 数据类型:别小看这“几字节”
在嵌入式世界里,内存是金子。我见过太多新手,上来就用 int 定义所有变量。结果呢?一个简单的温度传感器数据,用 int 存,白白浪费了2个字节。在资源紧张的MCU上,这可能是压垮骆驼的最后一根稻草。
我个人习惯,先看数据范围,再选类型。比如,一个8位的ADC值,范围0-255,用 uint8_t 就够了。为什么?因为它只占1个字节。
| 类型 | 大小(常见32位MCU) | 范围 | 我常用的场景 |
|---|---|---|---|
uint8_t |
1字节 | 0 ~ 255 | 状态标志、小范围计数 |
uint16_t |
2字节 | 0 ~ 65535 | ADC原始值、PWM占空比 |
uint32_t |
4字节 | 0 ~ 4.29e9 | 时间戳、系统滴答计数 |
float |
4字节 | ±1.18e-38 ~ ±3.4e38 | 滤波后的浮点数据 |
float运算非常慢。我曾经在一个项目中,因为用了大量浮点运算,导致控制周期从1ms拖到了5ms。后来全部改用定点数模拟,才解决了问题。
2.2 运算符:位运算才是嵌入式的主角
普通加减乘除,大家都会。但在嵌入式里,位运算才是真正的“主角”。你想想看,操作寄存器、设置GPIO引脚、打包协议数据,哪样离得开位运算?
举个例子,你想把GPIOB端口的第3脚拉高。用赋值语句写 GPIOB->ODR = 0x0008 ?这会把其他引脚的状态也改了。正确的做法是:
// 只置位第3位,不影响其他位
GPIOB->BSRR = (1 << 3); // 使用BSRR寄存器,原子操作
这里 (1 << 3) 就是位运算。我建议你熟练掌握:&(与)、|(或)、^(异或)、~(取反)、<<(左移)、>>(右移)。
if (x & (x-1) == 0)。这个技巧我在解析CAN报文ID时经常用,效率极高。
2.3 流程控制:别让if-else毁了你的实时性
流程控制,说白了就是让代码“走哪条路”。但这里有个大坑:分支预测失败。在医疗设备里,一个控制循环可能只有几十微秒。如果里面嵌套了多层 if-else,CPU的分支预测器一旦猜错,就要清空流水线,白白浪费好几个时钟周期。
我个人的经验是:
- 优先用switch-case:当判断条件是离散值时,编译器通常会把switch优化成跳转表,比if-else链快得多。
- 把最可能发生的条件放在最前面:比如一个错误检查,正常情况下不会出错,那就把正常逻辑放在if里,错误处理放在else里。
- 避免深层嵌套:超过3层的if-else,就该考虑重构了。用状态机或者查表法替代。
// 不好的写法:深层嵌套
if (state == IDLE) {
if (sensor_ready) {
if (data_valid) {
process_data();
} else {
error_handler(ERR_INVALID);
}
}
}
// 好的写法:提前返回,减少嵌套
if (state != IDLE) return;
if (!sensor_ready) return;
if (!data_valid) {
error_handler(ERR_INVALID);
return;
}
process_data();
2.4 函数:模块化的基石,但小心栈溢出
函数让代码变得清晰、可复用。但在嵌入式里,函数调用是有代价的。每次调用,都要压栈、跳转、返回、弹栈。如果函数被频繁调用(比如在1ms中断里),这个开销就不能忽视。
我曾经在一个项目中,把中断服务函数里的一行代码提取成一个单独的函数。结果中断执行时间从2.1us变成了3.5us。嗯,这就是函数调用的开销。
所以,我的建议是:
- 对于频繁调用的小函数,用
inline关键字建议编译器内联。但注意,这只是“建议”,编译器不一定采纳。 - 注意递归:嵌入式里尽量别用递归。栈空间本来就小,递归深度一深,栈溢出是分分钟的事。我见过一个同事,用递归实现了一个斐波那契数列,结果在STM32上跑,递归到第20层就崩了。
- 函数参数不要太多:超过4个参数,编译器可能会用栈传递,而不是寄存器。这会降低效率。
2.5 指针:C语言的灵魂,也是噩梦的开始
指针,说白了就是“地址”。你操作的不是变量本身,而是它住的地方。在嵌入式里,指针无处不在:访问外设寄存器、动态内存管理、回调函数……
但指针也是最容易出bug的地方。空指针、野指针、数组越界,每一个都能让你的设备死机。在医疗设备里,死机可不是重启一下那么简单。
看一个典型的例子,操作寄存器:
// 定义一个指向GPIOA控制寄存器的指针
volatile uint32_t *gpioa_crl = (uint32_t *)0x40010800;
// 通过指针修改寄存器值
*gpioa_crl |= (1 << 0); // 设置PA0为输出模式
这里 volatile 关键字很重要。它告诉编译器,这个地址的值可能会被硬件修改,不要优化掉。我刚开始学的时候,忘了加 volatile,结果调试了一整天,发现寄存器值怎么都写不进去。其实就是被编译器优化掉了。
再来说说函数指针。在实现状态机或者回调机制时,函数指针非常有用。比如:
// 定义一个函数指针类型
typedef void (*state_handler_t)(void);
// 状态表
state_handler_t state_table[] = {
state_idle,
state_measure,
state_process,
state_error
};
// 执行当前状态
void run_state_machine(uint8_t current_state) {
if (current_state < sizeof(state_table)/sizeof(state_table[0])) {
state_table[current_state](); // 通过指针调用函数
}
}
这种写法,比用 switch-case 更灵活,也更容易扩展。我在设计输液泵的控制逻辑时,就用这种状态表,添加新状态只需要在数组里加一项,不用改主循环代码。
memcpy 来安全拷贝。
小结
好了,这一讲的内容就到这里。C语言基础看似简单,但每个细节都可能在关键时刻“坑”你一把。数据类型选不对,内存爆了;位运算用不好,寄存器写错了;指针没处理好,系统崩了。这些都是我在实际项目中流过的“血泪”。
下一讲,我们会进入更刺激的部分——中断与定时器。那是实时控制的真正核心。做好准备,我们继续前进。