Sigmoid位置环规划:Q16定点数在STM32F103上的FOC实现
2026/9/23 7:21:28 网站建设 项目流程

1. 位置环规划为什么值得单独拎出来讲

搞过FOC的人都有一个共识:电流环是根基,速度环是骨架,位置环才是真正决定“手感”的那一层。很多朋友调电流环调得挺顺,波形也漂亮,一到位置环就发现要么响应慢半拍,要么到位时抖一下,要么走个小距离来回蹭。问题往往不在PID参数本身,而在于位置指令是怎么给出来的

传统做法无非两种:阶跃给定和梯形/S形速度规划。阶跃给定响应最快,但加速度理论上无穷大,机械冲击大,到位后容易振荡;梯形规划稍微好点,但加速度突变点依然存在,对皮带、丝杆、联轴器这些柔性传动不友好。S形规划能解决加速度突变,但实现起来参数多、计算量大,在STM32F103这种资源有限的片子上跑起来有点吃力。

我这次要聊的方案,是用一个Sigmoid函数直接生成位置环的平滑过渡曲线。它的核心优势是:一个公式、两个参数、天然平滑、计算量小,而且非常适合用Q16定点数在MCU上实现。整套东西我在STM32F103RCT6驱动无刷电机的平台上实测过,从位置指令生成到最终转子运动,全程没有出现明显的抖动和过冲。

这篇文章适合谁看?如果你正在做PMSM/BLDC的FOC控制,已经跑通了电流环和速度环,正在为位置环的平滑性发愁;或者你对定点数运算在电机控制里的落地感兴趣,想找一个具体案例来理解Q格式的用法,那这篇内容应该能给你一些直接能抄的思路。我会从数学原理讲到Q16定点实现,再到代码结构和实测现象,尽量把每个“为什么”都说清楚。

2. Sigmoid函数到底凭什么能用来做位置规划

2.1 从生物神经元到电机位置曲线

Sigmoid函数的标准形式是:

[ \sigma(x) = \frac{1}{1 + e^{-x}} ]

它的形状是一条从0平滑上升到1的S形曲线,中间陡、两头缓。这个特性最早是在神经网络里当激活函数用的,因为它能把任意实数映射到(0,1)区间,而且导数连续。

把它搬到位置规划上,思路就很直接了:把归一化的时间映射到Sigmoid的自变量,输出就自然是一条从0到1的平滑位置曲线。假设我们要从位置A走到位置B,总行程Δ = B - A,那么任意时刻的位置可以写成:

[ pos(t) = A + \Delta \cdot \sigma(k \cdot (t - t_0)) ]

其中k控制曲线的陡峭程度,也就是“加速快慢”;t0是曲线中心点对应的时间。t从0到T的过程中,pos从A平滑过渡到B。

2.2 为什么它比梯形和S形规划更省事

我列一个对比表,把三种常见位置规划方式放在一起看:

规划方式加速度连续性参数数量计算量实现难度适用场景
阶跃给定不连续(冲击)0极低极低点位要求极低
梯形速度加速度突变3(vmax, acc, dec)通用点位
S形速度加速度连续5+(加加速度限制)中高中高高精度柔性传动
Sigmoid位置加速度连续2(k, T)资源受限MCU

关键点在于:Sigmoid曲线的一阶导和二阶导都是连续的,意味着速度和加速度都不会突变。而它的计算只需要一次指数运算,如果用定点数查表或者近似,连指数运算都能省掉。

2.3 参数k和T分别控制什么

很多人第一次用Sigmoid做规划,容易把k和T搞混。我用实际调试的经验来说:

  • T决定总过渡时间。T越大,整个运动越慢,但曲线形状不变。
  • k决定曲线的“陡峭度”。k越大,中间段越陡,起步和停止越突然;k越小,曲线越平缓,加速越柔和。

有一个经验公式可以参考:当k·T ≈ 10~14时,曲线两端已经足够平缓,起点和终点的速度接近零。如果k·T太小,比如小于6,起点速度不为零,会有明显的“弹射感”;如果太大,比如超过20,中间段几乎变成阶跃,失去平滑意义。

我在STM32F103上实测,对于一个小型云台电机,行程90度,T取0.3s,k取40,k·T=12,效果比较理想。这个值不是固定的,要根据负载惯量和机械柔性来调。

注意:k和T不是独立的,真正影响曲线形状的是它们的乘积。调参时先定T满足节拍要求,再调k满足平滑度要求。

3. Q16定点数:为什么不用浮点,又怎么落地

3.1 STM32F103的浮点困境

STM32F103RCT6是Cortex-M3内核,没有硬件浮点单元。所有float运算都是软件模拟,一次浮点乘法可能要几十个周期,一次指数运算更是上百周期。在电流环这种20kHz的场合,浮点还能勉强撑住;但位置环如果每1ms算一次,还要做指数运算,CPU负载会明显上升。

更麻烦的是,浮点运算在中断里执行时间不确定,可能导致控制周期抖动。对于电机控制这种对时序敏感的场景,定点数是更稳妥的选择。

3.2 Q16格式的基本规则

Q16的意思是:用一个32位有符号整数,低16位表示小数部分,高16位表示整数部分。实际值 = 整数存储值 / 65536。

比如:

  • 1.0 存储为 65536
  • 0.5 存储为 32768
  • 0.1 存储为 6554(0.1×65536≈6553.6,取整)
  • -2.5 存储为 -163840

Q16的精度是1/65536≈0.0000153,对于位置规划来说完全够用。范围是-32768到32767,对于归一化到0~1的Sigmoid输出,绰绰有余。

3.3 Q16下的乘法与溢出处理

两个Q16数相乘,结果是Q32格式,需要右移16位才能回到Q16。这里有个坑:如果直接乘,中间结果可能溢出32位。比如两个接近1.0的Q16数相乘,65536×65536=4294967296,已经超过int32的最大值2147483647。

解决办法有两种:

  1. 用int64中间变量,乘完再右移。STM32F103支持64位运算,但速度慢。
  2. 先右移再乘,牺牲一点精度。比如(a>>8)*(b>>8)>>0,相当于Q8×Q8=Q16。

我在实际代码里用的是第一种,因为位置环频率不高(1kHz),64位运算的开销可以接受。如果要在更高频率下跑,可以用第二种,但要注意精度损失。

3.4 Sigmoid的定点近似

直接算e^{-x}在定点上很麻烦。我的做法是查表+线性插值

  • 把x的范围(比如-8到8)分成256段,每段存一个Q16的Sigmoid值。
  • 查表时根据x找到相邻两个表项,做线性插值。

这样一次Sigmoid计算只需要几次乘加和一次查表,在STM32F103上大概几十个周期就能完成。表的大小是256×4字节=1KB,对于RCT6的256KB Flash来说毫无压力。

如果你想要更高精度,可以分512段或1024段,但1KB的表已经能把误差控制在0.001以内,对位置环来说足够了。

实操心得:查表法的精度不仅取决于表长,还取决于x的范围。如果x超出表范围,直接取端点值。因为Sigmoid在|x|>8时已经非常接近0或1,误差可以忽略。

4. 从公式到代码:位置环规划的完整实现

4.1 整体控制结构

先理清楚位置环在整个FOC里的位置。我的结构是三层串级:

  • 位置环(1kHz):输入目标位置和当前位置,输出目标速度。
  • 速度环(5kHz):输入目标速度和当前速度,输出目标电流q轴分量。
  • 电流环(20kHz):输入目标电流和采样电流,输出SVPWM占空比。

位置环的输出不是直接给速度环,而是经过Sigmoid规划后的位置指令再差分得到速度前馈。这样速度环跟踪的是一条平滑的速度曲线,而不是阶跃速度指令。

具体来说,每个位置环周期做这几件事:

  1. 计算当前规划时间t。
  2. 用Sigmoid函数算出归一化位置σ。
  3. 实际位置指令 = 起始位置 + 行程×σ。
  4. 位置指令差分得到速度前馈。
  5. 位置误差 = 位置指令 - 实际位置,经过P控制器得到速度修正。
  6. 最终速度指令 = 速度前馈 + 速度修正。

4.2 Q16定点Sigmoid查表实现

先定义表长和范围:

#define SIGMOID_TABLE_SIZE 256 #define SIGMOID_X_MIN (-8.0f) #define SIGMOID_X_MAX (8.0f) #define SIGMOID_X_STEP ((SIGMOID_X_MAX - SIGMOID_X_MIN) / (SIGMOID_TABLE_SIZE - 1)) static const int32_t sigmoid_table[SIGMOID_TABLE_SIZE] = { ... };

表的生成可以在PC上算好,直接以常量数组形式放进代码。每个表项是Q16格式的Sigmoid值。

查表函数:

int32_t sigmoid_q16(int32_t x_q16) { // 把Q16的x转换成表索引 // x_q16 / 65536.0f 得到实际x // (x - X_MIN) / X_STEP 得到浮点索引 int32_t x_int = x_q16 >> 16; // 整数部分 int32_t x_frac = x_q16 & 0xFFFF; // 小数部分 // 计算索引,这里用定点运算避免浮点 // index = (x - X_MIN) / X_STEP // 预先算好 1/X_STEP 的Q16值 int32_t index_q16 = (x_q16 - (int32_t)(SIGMOID_X_MIN * 65536)) * INV_STEP_Q16 >> 16; int32_t idx = index_q16 >> 16; int32_t frac = index_q16 & 0xFFFF; if (idx < 0) return sigmoid_table[0]; if (idx >= SIGMOID_TABLE_SIZE - 1) return sigmoid_table[SIGMOID_TABLE_SIZE - 1]; // 线性插值 int32_t y0 = sigmoid_table[idx]; int32_t y1 = sigmoid_table[idx + 1]; return y0 + (((y1 - y0) * frac) >> 16); }

这段代码里,INV_STEP_Q16是1/X_STEP的Q16表示。整个函数没有一次浮点运算,全部是整数乘加和移位。

4.3 位置规划器的状态机

位置规划器需要管理一次运动的生命周期:

typedef enum { PLAN_IDLE, PLAN_RUNNING, PLAN_DONE } plan_state_t; typedef struct { plan_state_t state; int32_t start_pos_q16; int32_t delta_pos_q16; int32_t k_q16; int32_t T_q16; int32_t t_q16; int32_t dt_q16; int32_t pos_cmd_q16; int32_t vel_ff_q16; } pos_planner_t;

启动一次规划:

void planner_start(pos_planner_t *p, int32_t target_pos_q16) { p->start_pos_q16 = current_pos_q16; p->delta_pos_q16 = target_pos_q16 - current_pos_q16; p->t_q16 = 0; p->state = PLAN_RUNNING; }

每个周期更新:

void planner_update(pos_planner_t *p) { if (p->state != PLAN_RUNNING) return; p->t_q16 += p->dt_q16; if (p->t_q16 >= p->T_q16) { p->pos_cmd_q16 = p->start_pos_q16 + p->delta_pos_q16; p->vel_ff_q16 = 0; p->state = PLAN_DONE; return; } // 计算Sigmoid自变量:k * (t - T/2) int32_t x_q16 = (p->k_q16 * (p->t_q16 - (p->T_q16 >> 1))) >> 16; int32_t sigma_q16 = sigmoid_q16(x_q16); int32_t new_pos = p->start_pos_q16 + ((p->delta_pos_q16 * sigma_q16) >> 16); // 速度前馈 = (new_pos - old_pos) / dt p->vel_ff_q16 = ((new_pos - p->pos_cmd_q16) << 16) / p->dt_q16; p->pos_cmd_q16 = new_pos; }

这里有个细节:速度前馈用差分计算,而不是对Sigmoid求导。差分更简单,而且和位置指令严格一致,不会出现前馈和指令不匹配的问题。

4.4 参数整定的实操步骤

我调这套东西的步骤是这样的:

  1. 先定T。根据机械节拍要求,比如要求0.5秒内走完90度,那T就取0.5秒。注意T是过渡时间,不是总时间,实际到位时间会略大于T。
  2. 初选k。让k·T≈10,比如T=0.5,k=20。
  3. 观察起步和停止。如果起步有冲击,减小k;如果中间段太慢,增大k。
  4. 微调位置环P。位置环的P增益不要太大,因为前馈已经提供了主要的速度指令。P的作用只是消除稳态误差。
  5. 看电流波形。如果q轴电流在起步和停止时有尖峰,说明加速度还是太大,继续减小k。

我实测的一组参数:T=0.3s,k=40,位置环P=8,速度环PI=0.5/0.01,电流环PI=1.2/0.005。这套参数下,90度行程的到位时间约0.35秒,超调小于0.5度,电流峰值约为额定值的1.5倍。

常见问题:如果发现位置始终到不了目标,检查Sigmoid表的范围是否覆盖了k·T/2。当k·T=12时,x的最大值约为6,在-8到8范围内没问题。但如果k·T超过16,x会超出表范围,导致端点值被截断,位置出现偏差。

5. 实测现象与排查记录

5.1 起步抖动:多半是k太大

第一次跑的时候,我用的是k=80,T=0.2s,k·T=16。结果电机起步时“咯噔”一下,声音很明显。用示波器看q轴电流,起步瞬间有一个尖峰。

原因分析:k·T=16时,Sigmoid曲线在起点附近的变化率仍然较大,对应的加速度不为零。虽然理论上Sigmoid的导数在无穷远处才为零,但实际工程中要求起点加速度足够小。

解决:把k降到40,k·T=8,起步明显柔和。但k·T=8时,曲线两端不够平缓,停止时又有点慢。最后折中取k=50,T=0.25,k·T=12.5,效果比较平衡。

5.2 到位振荡:位置环P太大

平滑了起步之后,发现到位时电机在目标位置附近来回蹭,幅度大概1度左右。一开始以为是速度环的问题,调了半天没效果。

后来把位置环P从20降到8,振荡消失。原因是:Sigmoid规划已经提供了速度前馈,位置环P只需要修正很小的跟踪误差。P太大反而会把前馈的平滑性破坏掉。

5.3 定点溢出:乘法顺序很关键

在计算delta_pos_q16 * sigma_q16时,如果delta_pos_q16比较大(比如对应360度),sigma_q16接近65536,乘积可能超过int32范围。

我的处理是:先把delta_pos_q16右移8位,乘完再左移8位。这样损失一点精度,但避免了溢出。对于位置控制来说,1/256的精度损失完全可以接受。

int32_t delta_shifted = p->delta_pos_q16 >> 8; int32_t pos_offset = (delta_shifted * sigma_q16) >> 8;

5.4 常见问题速查表

现象可能原因排查方向解决方法
起步冲击大k太大或T太小看k·T是否>14减小k或增大T
到位振荡位置环P太大观察位置误差符号减小P,依赖前馈
位置到不了Sigmoid表范围不够检查k·T/2是否>8扩大表范围或减小k
运动中间卡顿查表插值精度不够看表长是否<128增加表长到256或512
速度前馈噪声大差分周期太短检查dt是否<0.5ms增大dt或加低通滤波
定点溢出乘法中间结果超32位检查大数相乘先移位再乘

5.5 一个容易被忽略的细节:初始位置对齐

位置环规划的前提是当前转子位置已知。对于有感FOC,编码器或霍尔能提供绝对位置,没问题。但对于无感FOC,转子初始位置检测本身就有误差,位置环规划再平滑,起点错了后面全错。

我的做法是:在位置环使能之前,先做一次转子初始位置检测,把检测结果作为位置环的起始位置。如果检测误差较大,可以在低速下先跑一段速度环,让观测器收敛后再切位置环。

6. 还能怎么扩展:几个实用的改进方向

6.1 变参数Sigmoid:起步柔、中间快、停止稳

固定k的Sigmoid曲线是对称的,但实际机械往往希望起步更柔、中间更快、停止更稳。可以把k做成随时间变化的:

  • 前1/3段用较小的k,让起步柔和。
  • 中间1/3段用较大的k,加快中间段。
  • 后1/3段再用较小的k,让停止平稳。

实现上就是根据t/T的值查不同的k,或者用分段函数。代价是参数变多,调参复杂度上升。对于大多数应用,固定k已经够用。

6.2 前馈加加速度限制

Sigmoid规划的速度前馈是平滑的,但加速度前馈(速度前馈的差分)在起点和终点仍然有较小突变。如果机械对加加速度(jerk)敏感,可以对速度前馈再做一次Sigmoid平滑,相当于两级串联。这样加加速度也连续,但响应会变慢。

6.3 多段连续运动的位置拼接

如果要做连续轨迹,比如从A到B再到C,可以在第一段规划还没完全结束时就开始第二段。关键是保证位置和速度在拼接点连续。我的做法是:在PLAN_DONE之前,如果收到新目标,就把当前位置作为新起点,重新启动规划。由于Sigmoid在终点速度接近零,拼接时的速度不连续很小,实测几乎看不出来。

6.4 在FPGA上实现定点Sigmoid

如果位置环要在FPGA上跑,Q16定点数同样适用。FPGA的优势是可以做全并行查表和插值,一个时钟周期就能出一个Sigmoid值。表可以放在Block RAM里,256×16bit只占一小块。对于多轴联动,每个轴一个独立的规划器,互不干扰。

我在实际项目里对比过:STM32F103软件查表,一次Sigmoid计算约2微秒;FPGA硬件查表,一次约20纳秒。对于1kHz的位置环,两者都绰绰有余,但FPGA能支持更高的环路频率。

6.5 和MPTA控制的配合

如果电机要跑MPTA(最大转矩电流比),位置环输出的速度指令最终会变成iq和id的参考值。Sigmoid规划只负责位置和速度的平滑,不改变MPTA的电流分配逻辑。两者是正交的,可以独立调试。

我个人在实际操作中的体会是:位置环的平滑性,七分靠规划,三分靠PID。规划做得好,PID参数可以放得很宽,系统依然稳定;规划做得糙,PID再精细也补不回来。Sigmoid加Q16这套组合,最大的价值就是把复杂的轨迹规划压缩成了一个公式和一张表,在资源受限的MCU上也能跑出接近伺服驱动器的运动品质。

最后再分享一个小技巧:调参时把位置指令、实际位置、速度前馈、q轴电流四个量同时用串口传到上位机画波形。光看电机转是看不出问题的,波形一摆出来,哪里不平滑、哪里有过冲,一目了然。我习惯用1kHz的采样率传,波特率115200,一帧8个字节,四个量刚好32字节,传一秒钟的数据也就3.6KB,上位机用Python的matplotlib实时画,调试效率比盲调高十倍不止。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询