本文将包含的内容:
- VESC的FOC代码分析, 从初始化到一个控制周期结束
- VESC的FOC算法抽象, 对比其与TI白皮书上的FOC算法差异(鸽了, 或者我本来就是顺着白皮书的思路去理解FOC流程的)
从代码看VESC的FOC
FOC初始化
- 入口函数,
mcpwm_foc_init
, 刚开始先将系统锁定, 组织任何其它线程切换和中断打乱初始化进程. 然后就开始ADC, DMA, TIM的初始化.
DMA和ADC初始化
- 这两个的初始化连在一起, 直接在
foc_init
中执行
- 使能DMA2和ADC1, 2, 3的外设时钟, 给DMA2 stream 4 分配优先级(5)和dma传输完成后的处理函数(
mcpwm_foc_adc_int_handler
), 这个处理函数十分重要, 主要的FOC计算在里面完成.
- 初始化DMA2 stream4, 主要关注peripheral to memory的地址(
(uint32_t)&ADC_Value
). 并且设置DMA中断为半传输完成触发的模式.
设置为半传输完成的前提是FOC计算中最重要的几个参数排在前面优先传输. VESC进行FOC控制时最重要的是三相采样电流, 这个是排在最前面传输的, 所以可以使用HT.
优点是不必等待全部传输完成就可以处理数据, 数据实时性提高.
中断执行的函数就是之前提到的mcpwm_foc_adc_int_handler
. - 初始化三个采样的ADC, 这里使用了ADC三重模式, 一个ADC(ADC1)作为主ADC, 接收外部信号来触发采样, 剩下两个ADC(ADC2, 3)设置为从ADC, 接收ADC1的触发信号, 和ADC1同时采样.
这种配置的好处是保证了三个ADC永远同时开启采样.
注意到主ADC外部触发是TIM2的CC2中断, 在后文中会作为重要的信号, 暗示ADC采样和PWM生成有严格时序关系.
- DMA和ADC初始化完成.
TIM初始化
- TIM初始化之初先关掉各个TIM. 这里使用到的TIM有3个, 分别是TIM1, TIM2, TIM8. 以下是TIM的资源调用说明:
比较重要的是TIM1, 产生PWM信号; 和TIM2, 触发采样和FOC计算.
- tim初始化的入口函数在DMA和AMC初始化好后执行, 函数名是
timer_reinit
传入一个参数是零矢量频率, 具体说是SVPWM中零矢量出现的频率. 关于各种频率, 之后会有说明. - 以下是初始化步骤
先用同一个句柄初始化TIM1和TIM8, 它们两个输出的是同样性质的PWM, 其中TIM1就是用来输出我们FOC控制用的PWM波形的. 而TIM8是留给潜在的双电机控制.
注意到计数模式是中心对齐模式(TIM_CounterMode_CenterAligned1
), 即向上计数到顶, 触发一次中断, 然后向下计数到0, 触发一次中断. 这样就生成了一个中心对称的波形, 中间是1逻辑, 两边是0逻辑. 我们使用这种模式来输出SVPWM需要的对称波形. 在一个完整的SVPWM波形中, 触发两次中断(中间一次末尾一次), 周期长度是一般PWM波周期的两倍.
如下图, SVPWM需要的波形是中心对称的.
还有一些别的设置, 没什么特别的
TIM2的初始化, 初始化为output compare, PWM1, 三路PWM输出, 实际上并不输出PWM来控制电机, 而是作为一个工具定时器, 通过捕获TIM1生成PWM完成的事件来产生中断, 进而驱动ADC采样和DMA传输, 进而触发中断运行FOC计算函数.
下图为TIM2的Base设置和OC模式设置.
下图为与TIM2有关的触发和中断配置:
- 用上图理清PWM输出与采样的触发关系和时序:
- TIM2中断输出的是
TIM_IT_CC2
, 触发条件是TIM2向上计数到CCR2. TIM_IT_CC2
能够触发ADC采样.- TIM1的
TIM_TRGOSource_Update
事件的触发条件是一个PWM波产生完毕. - TIM1的
TIM_TRGOSource_Update
事件能够重置TIM2的计数值. - CCR2实际设置为很小的数值, 所以一旦*
TIM_TRGOSource_Update
事件送达*, TIM2重置为0, 然后瞬间计数到CCR2, 瞬间触发中断TIM_IT_CC2
, 驱动采样, 以及后续的FOC计算. - 所以PWM生成完毕和采样是紧接着执行的.
其它
- 初始化检查输入电压, 上电后一部分等待时间来源于此.
- 处理故障码 FAULT_CODE
- 设置某种偏置电流, 以及进行偏置电流检测. 电调连接电机上电的时候, 电机会发出微弱的电流嗡嗡声, 也许就是在通电测试这个offset
- 开启三个线程, 分别是FOC辅助定时器, hfi相关计算, pid相关计算. 相关内容我们在其它文章中会讲到.
(又挖坑)
- 绑定画图指令, 在终端可以用foc_tmp命令和foc_plot_hfi_en在上位机画图
- 到此, FOC的初始化结束
采样
前置知识
- 采样的过程由ADC和DMA自动完成, 数据处理部分的代码主要在
mcpwm_foc_adc_int_handler
中. mcpwm_foc_adc_int_handler
函数在DMA传输完成一半之后触发执行, DMA传输在ADC转换完成后执行, 而ADC采样的触发条件是TIM1的PWM输出完成, 所以函数执行频率是pwm周期频率.(但是并非FOC计算频率)- 如前所述, 一个SVPWM波形由两段中心对称的PWM组成, 第一段PWM完成后会触发采样, 但是SVPWM只走完一半, 在一般的应用下此时并不适合FOC计算(HFI和基于硬件的高频控制模式另说). 此时
mcpwm_foc_adc_int_handler
函数会检查电压矢量是否处于v7, 即是否在SVPWM中间段, 来决定是否跳过这次FOC计算. - 关于各种频率, 统一作如下说明:
f_zv
, 零矢量频率, 即v0和v7出现的频率, 是PWM频率, VESC中可设置.- SVPWM频率, 是PWM频率的一半, 因为一对PWM波组成一个SVPWM波.
- 采样频率, 等于PWM频率, 等于f_zv.
- 控制器频率, 等于SVPWM频率, 因为每次PWM产生都触发采样, 但是计算在v7时轮空一次, 等同于一半PWM频率, 也就是SVPWM频率.
- 补充一点: 三相PWM时是v0->v7->v0->v7交替出现, 搞不懂这一点的去看我写的SVPWM的讲解.
- 由此引出整个FOC流程:
- 前半波形生成完成->
- ADC采样->DMA传输->
- 传输中断函数中进行FOC计算->检测到是v7, 不进行FOC计算->
- 后半波形生成完毕->
- ADC采样->DMA传输->
- 传输中断函数中进行FOC计算->检测到是v0, 进行FOC计算和SVPWM计算, 应用占空比->
- 按照新占空比生成新波形->
- 前半波形生成完成…
- 这个流程会在接下来逐渐解释清楚.
代码(mcpwm_foc_adc_int_handler
)
- 从上到下是执行顺序.
- 检查减采样, 可以用宏定义设置跳过一些采样; 判断是不是处在v7向量, 如果是, 接下来跳过本次FOC计算.
- 判断一系列特殊情况下的采样, 直接在里面进行foc计算然后退出, 不是本次的重点
- 用宏定义从DMA传输的目标地址中获取采样电流
并且应用初始化的时候算好的offset
- 根据采样模式, 对采到的电流值进行修正, 有三种策略:
- High Current是用两个小电流计算大电流, 能够突破硬件限制, 采样到更大的电流
- Longest Zero Time是用低占空比的两相的电流来估计高占空比的一相的电流, VESC默认是这个模式.
- All Sensors Combined是三个全用, 此时不存在估计某一相的值, 直接用采样值计算i_α, i_β了
- High Current是用两个小电流计算大电流, 能够突破硬件限制, 采样到更大的电流
- 修正后, 将ADC值转换为电流值, 这样就计算出三相电流ia, ib, ic
- 至此采样部分结束, 进入FOC计算部分.
FOC计算
前置知识: FOC计算是要算什么?
- 完整的FOC流程图如下, 更多内容详见我的 FOC与SVPWM学习 部分
接下来按照1234的顺序, 讲代码是如何执行FOC计算的, 由于有些步骤穿插在一起, 不保证执行顺序的严格, 但是可以保证这些代码是在极短的时间内被统一执行的. - FOC计算分两大种情况, 电调主动驱动和不主动驱动的情况, 由于后者相比前者代码简单得多, 且会跳过一部分前者的步骤, 所以接下来默认的情况是电调主动驱动.
1 当前相位计算(mcpwm_foc_adc_int_handler
)
- 判断是否处于手动覆写相位模式, 即手动改变电角度, 使电调输出定向的磁场. 这种模式一般只在整定过程中会用到.
- 根据
foc_sensor_mode
来选择相位的来源. 需要注意的是即使选择了有感模式, 无感的观测器仍然一直更新, 并且会对外部编码器的数值进行补偿和修正.- 使用编码器的模式:
- 使用霍尔传感器, 也有observer进行修正的环节
- 无感, 使用observer作为相位来源, 有时会进行补偿
- HFI也有一套获取相位的规则, 不展开讲了
- 使用编码器的模式:
- 单独判断一些特殊情况, 例如刹车, 开环等, 开环就不需要得知相位了
- 到此我们得到了电机的相位, 也就是电角度.
2 当前id iq计算, 目标id iq计算(mcpwm_foc_adc_int_handler
, control_current
)
- 我们需要将当前id iq与目标id iq作比较, 产生一个差作为输入, 这样才能进行闭环控制.
- 先说目标电流, 这个简单. 目标有两个来源, 一个是直接控制电流到某个值I, 那么通常就设置id为0, iq为I; 另一个是用PID计算出的电流(速度控制, 位置控制). PID和接口函数的说明不在本文的计划中, 所以我们只要知道有其它线程在跑PID函数, 有时会在外部改变目标值, 每次计算我们只要读取进来就好了.
- 进行当前id iq的计算, 需要先将采样得到的三相电流, 通过clark变换到αβ系, 即i_α, i_β, 这一步其实在计算相位前就执行了
- 得到当前i_α, i_β后, 再通过park变换, 变换到dq坐标系, 即可得到当前id iq, 这一步在函数
control_current
中进行(control_current
被mcpwm_foc_adc_int_handler
调用)
- 产生误差, 输入电流控制器
3 电流控制器(control_current
)
control_current
函数几乎就是一整个PI控制器+SVPWM, 所谓PI控制器是电流控制器的核心, 其功能是根据iq id的error计算应该输出的vd, vq, 既包含闭环控制的参数计算, 又实现了从电流到电压的转换, 这种转换基于对电机的建模, 需要先测量电机的参数: 电阻R, 电感L, 磁通ψ等.- 关于PI部分, 我自己琢磨了一段时间, 总算是有一些门道, 以下是我在学习代码过程中写的注释, 以及PI控制器代码部分:
- 通过以下几点设问来总结我的理解:
- PI控制器的作用是什么?输入输出是什么?
PI控制器将设定电流与真实电流之间的差值转化为要设定的电压. 输入是Iq_err, Id_err, 输出是vq_set, vd_set. - 为什么最终要转化成电压?
因为三相逆变器只能直接控制三相电压. - 我们都知道电压 = 电流*电阻 + 电感*电流变化率, 想要多少电流不是可以直接用这个公式反推出电压电流关系吗? 为什么还需要PI控制器呢?
- 电机是一个动态的和复杂的系统, 以上公式只适用于电机静止不动的情况, 一旦电机开始旋转, 就要考虑反电动势和耦合项(之后会讲)对电流的影响, 即存在不确定的干扰项. 对于变化的目标电流, 系统需要有持续追踪的能力, 所以需要引入一个PI控制器进行自动控制.
- 加入PI控制器进行闭环控制, 不仅能够使系统有一定的追踪输入的能力(系统从type 0变为type 1), 还能通过调整ki和kd使系统满足各种性能指标, 例如响应速度, 稳态误差等. (结合根轨迹法思考)
- 为什么只使用PI控制器, 而不是PID控制器?
电机作为一个开环系统, 本质是一个一阶惯性系统: tf = 1/(Ls+R), 不像二阶系统有振荡和超调的问题. D环节, 也就是微分环节, 主要用于改善二阶以上系统的超调和响应速度问题, 用在一阶惯性系统上, 没有实际效果, 反而可能放大噪声. PI控制器中P环节决定响应速度, I环节消除稳态误差, 已经能够满足性能需求. - 如何调整PI控制器的kp和ki?
VESC中, kp的计算与L成正比, ki的计算与成正比, 因为:- 电感L越大, 电流上升越慢, 需要更大的kp来提高系统的响应速度.
- 电阻R越大, 需要更大的ki来补偿电阻带来的稳态误差.
- PI控制器的作用是什么?输入输出是什么?
- 转换过程中, vd vq是存在耦合的, 换句话说, vd不是单纯只受id影响, vq不是单纯只受iq影响, 而是存在交叉项, 如下图公式所示
其中ωe是电机电转速, ψm是磁通.
对vd和vq进行交叉项的补偿, 能够减少耦合的影响, 举例来说, 单独调整vq, 不会对vd影响太大.
对vq进行反电动势补偿, 在高速运行的情况下控制效果会更好, 因为PI控制器不用对抗这种随速度增大的扰动.
注意到补偿项的计算使用了预测的速度(当前周期的瞬时速度估计), 而不是已有的速度数据((上周期相位-本周期相位)/dt, 本质上是上个周期的平均速度), 所以这是一种前馈补偿, 让未来才能得知的数据作用于当下.
- 补偿之后, 我们就得到了目标vd和vq.
4 SVPWM计算(control_current)
- 接下来最后一步, 是通过park逆变换将目标vd, vq转换为目标v_alpha, v_beta, 然后进行SVPWM计算, 算出具体的目标三相占空比.
- 将vq, vd进行归一化, 然后计算出归一化的v_alpha, v_beta. 归一化的目的是在后续计算占空比时, 不用再进行归一化.
- 进行死区时间补偿(
update_valpha_vbeta
)- 死区时间 deadzone time 是由于mos管在开启和关断时都需要一定的时间, 为了防止上下管同时导通, 需要加入死区时间的保护, 实际输出的电压波形可能并不是设计的电压.
死区时间补偿是通过略微调整实际输出电压, 使最终合成的电压矢量满足理想状态下的要求. 补偿前后的效果比较如下
代码, 相关原理以后再深究.
- 死区时间 deadzone time 是由于mos管在开启和关断时都需要一定的时间, 为了防止上下管同时导通, 需要加入死区时间的保护, 实际输出的电压波形可能并不是设计的电压.
- 进行SVPWM计算(foc_svm)
- 关于SVPWM的详细介绍, 在之前的文章中有, 完全不懂SVPWM的建议先去看看.
- 通过PI控制器和死区时间补偿之后, 我们得到了归一化的目标v_alpha和v_beta, 现在可以进行SVPWM的计算, 然后更新占空比了
- SVPWM计算的第一步, 是用v_alpha和v_beta确定电压矢量所在的扇区
- 然后根据所在扇区不同, 逆变器的三个桥有不同的开关顺序, 按照顺序计算各相通电时间
- 返回各项时间, 时间的衡量方法是计数值, 例如最大计数值是100, 那么某一相40%占空比意味着40%的通电时间, 输出计数值就是40.
- 通过修改TIM1->CCRx的值, 应用占空比, 下次PWM生成, 就会按照这个占空比.
流程总结
- 如前所述, 完整FOC流程为:
- 前半波形生成完成->
- ADC采样->DMA传输->
- 传输中断函数中进行FOC计算->检测到是v7, 不进行FOC计算->
- 后半波形生成完毕->
- ADC采样->DMA传输->
- 传输中断函数中进行FOC计算->检测到是v0, 进行FOC计算和SVPWM计算, 应用占空比->
- 按照新占空比生成新波形->
- 前半波形生成完成…
可以再回头去对照着看一下.