4. 代码优化技巧:代码风格、内存优化、执行效率优化

各位做电机控制的同行,咱们今天聊点实在的。代码优化这事儿,说白了就是三个维度:让人看得懂、让机器跑得快、让内存用得省。我做了十几年嵌入式,见过太多「能跑就行」的代码,结果后期维护时把自己坑得欲哭无泪。嗯,咱们一个一个说。

4.1 代码风格:写给未来的自己看

你想想看,一段代码写出来,真正运行的时间可能只有几个月,但被人阅读的时间可能是好几年。我个人习惯是:把代码当成写情书一样认真。当然,是写给单片机的情书。

4.1.1 命名规范

我在项目中遇到过最头疼的事,就是看到变量名叫 abtmp。你猜 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 格式表示,精度完全够用,但内存省一半。

避坑指南:我曾经在某个项目里,把所有变量都定义成 float,结果编译出来 RAM 超了 200 字节。后来改成 Q15 格式,不仅内存够了,运算速度还快了 30%。嗯,从那以后我再也不敢乱用 float 了。

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 >> 1
  • a * 4 改成 a << 2
  • a % 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 知识体系总览

下面这张图是我自己整理的代码优化知识体系,你可以对照着看看自己哪些地方还需要加强。

电机控制代码优化知识体系 代码风格 内存优化 执行效率 命名规范:匈牙利命名法 注释原则:解释为什么 代码排版:对齐与缩进 变量类型:int16_t优先 查表法:预计算存Flash Q格式:定点数代替浮点 循环展开:减少判断开销 位运算:移位代替乘除 内联函数:减少调用开销 核心原则:可读性 + 低内存 + 高效率 三者平衡,根据项目需求取舍 代码优化不是一蹴而就的,需要在实际项目中不断积累经验

4.5 实战经验总结

说了这么多,其实核心就一句话:代码是写给人看的,顺便给机器执行。我见过太多追求极致优化但完全不可读的代码,最后维护成本高得吓人。

我的优化优先级:
  1. 先保证功能正确
  2. 再保证代码可读
  3. 最后才考虑性能优化

记住,过早优化是万恶之源。先让代码跑起来,再用 profiler 工具找出真正的瓶颈,然后针对性地优化。我曾经在某个项目里,花了两天优化一个函数,结果发现它只占 CPU 时间的 0.1%。嗯,从那以后我再也不做这种傻事了。

最后分享一个小技巧:每次写完代码,隔一周再回来看。如果连你自己都看不懂,那就说明需要重构了。相信我,这个习惯能帮你省下无数调试时间。

公众号:蓝海资料掘金营,微信deep3321