4. 代码优化技巧:代码风格、内存优化、执行效率优化
各位做电机控制的同行,咱们今天聊点实在的。代码优化这事儿,说白了就是三个维度:让人看得懂、让机器跑得快、让内存用得省。我做了十几年嵌入式,见过太多「能跑就行」的代码,结果后期维护时把自己坑得欲哭无泪。嗯,咱们一个一个说。
4.1 代码风格:写给未来的自己看
你想想看,一段代码写出来,真正运行的时间可能只有几个月,但被人阅读的时间可能是好几年。我个人习惯是:把代码当成写情书一样认真。当然,是写给单片机的情书。
4.1.1 命名规范
我在项目中遇到过最头疼的事,就是看到变量名叫 a、b、tmp。你猜 tmp 是存电流值还是存角度值?猜对了算我输。
- 全局变量加前缀:
g_u16MotorSpeed(g表示全局,u16表示类型) - 局部变量用匈牙利命名法:
u16AdcValue - 函数名用动词开头:
CalcFocCurrent()、SetPwmDuty()
// 不好的风格
int a, b, c;
void func1() {
a = readADC();
b = a * 2;
c = b + 100;
}
// 好的风格
int g_i16MotorCurrent;
void UpdateCurrentLoop() {
int i16AdcRaw = ReadAdcChannel(ADC_CH_CURRENT);
g_i16MotorCurrent = i16AdcRaw * CURRENT_SCALE_FACTOR;
g_i16MotorCurrent += CURRENT_OFFSET;
}
4.1.2 注释的艺术
我曾经见过一段代码,注释比代码还长,但全是废话:「这里加1是因为要加1」。嗯,这种注释还不如不写。好的注释应该解释「为什么」,而不是「是什么」。
- 函数头部写清楚:输入、输出、副作用
- 关键算法处写原理:比如「这里用查表法代替sin计算,节省200个时钟周期」
- 不要注释显而易见的代码:
i++; // i加1这种纯属浪费硬盘空间
4.2 内存优化:在钢丝上跳舞
做电机控制,MCU的RAM通常就几K到几十K。你想想看,一个浮点数组就占4个字节,要是存100个点,400字节就没了。所以内存优化是必修课。
4.2.1 变量类型选择
| 数据类型 | 占用字节 | 适用场景 |
|---|---|---|
| uint8_t | 1 | 状态标志、小范围计数 |
| int16_t | 2 | 电流值、速度值(Q格式) |
| float | 4 | PID参数、角度值 |
| double | 8 | 能不用的尽量别用 |
我个人习惯:能用 int16_t 就别用 float。电机控制里很多量其实可以用 Q15 或 Q31 格式表示,精度完全够用,但内存省一半。
4.2.2 常量与查表法
电机控制里经常用到 sin/cos 函数。你想想看,每次调用标准库的 sin() 函数,那计算量可不小。我的做法是:预计算好一个正弦表,运行时直接查表。
// 预计算正弦表,256点
const int16_t g_sinTable[256] = {
0, 804, 1608, ... // 预计算好的值
};
// 查表获取sin值
int16_t GetSinValue(uint8_t u8Index) {
return g_sinTable[u8Index & 0xFF];
}
这样做的优点:
- 省去浮点运算,速度提升明显
- 表格存在Flash里,不占RAM
- 精度可控:256点查表,角度分辨率1.4度,对大多数电机控制够用了
4.3 执行效率优化:每一微秒都很珍贵
电机控制的核心是电流环和速度环,通常电流环要跑在10kHz以上。也就是说,每100微秒就要完成一次完整的计算。你想想看,这时间有多紧张。
4.3.1 循环展开
我刚开始做电机控制时,写了个 for 循环处理三相电流,结果发现每次循环都要判断条件、更新索引,白白浪费了好几个时钟周期。后来改成手动展开:
// 不推荐:循环方式
for (int i = 0; i < 3; i++) {
g_Current[i] = ReadAdc(i);
g_Current[i] = g_Current[i] * SCALE_FACTOR;
}
// 推荐:手动展开
g_Current[0] = ReadAdc(0) * SCALE_FACTOR;
g_Current[1] = ReadAdc(1) * SCALE_FACTOR;
g_Current[2] = ReadAdc(2) * SCALE_FACTOR;
别看就省了几个指令,在10kHz的电流环里,每个微秒都很珍贵。
4.3.2 使用位运算代替乘除法
电机控制里经常要做乘除运算。比如把电流值除以2,或者乘以4。你想想看,用除法指令要几十个时钟周期,但用移位操作只要1个周期。
a / 2改成a >> 1a * 4改成a << 2a % 8改成a & 0x07
// 不好的写法
int16_t i16HalfCurrent = i16Current / 2;
int16_t i16QuadCurrent = i16Current * 4;
// 好的写法
int16_t i16HalfCurrent = i16Current >> 1;
int16_t i16QuadCurrent = i16Current << 2;
4.3.3 减少函数调用开销
在中断服务函数里,我建议尽量少调用函数。每次函数调用都要压栈、跳转、返回,这些开销在高速中断里是致命的。
- 把频繁调用的短函数写成宏或内联函数
- 中断里只做最核心的计算,其他事情放到主循环里做
- 能用全局变量传参就别用函数参数传递
4.4 知识体系总览
下面这张图是我自己整理的代码优化知识体系,你可以对照着看看自己哪些地方还需要加强。
4.5 实战经验总结
说了这么多,其实核心就一句话:代码是写给人看的,顺便给机器执行。我见过太多追求极致优化但完全不可读的代码,最后维护成本高得吓人。
- 先保证功能正确
- 再保证代码可读
- 最后才考虑性能优化
记住,过早优化是万恶之源。先让代码跑起来,再用 profiler 工具找出真正的瓶颈,然后针对性地优化。我曾经在某个项目里,花了两天优化一个函数,结果发现它只占 CPU 时间的 0.1%。嗯,从那以后我再也不做这种傻事了。
公众号:蓝海资料掘金营,微信deep3321