ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于FPGA的永磁同步伺服系统:硬件电流环与SVPWM的Verilog实现

基于FPGA的永磁同步伺服系统:硬件电流环与SVPWM的Verilog实现 前两年做一套高动态响应的永磁同步伺服系统时我用DSP跑FOC算法加SVPWM200MHz主频下电流环算一次要接近20微秒还常被ADC中断、通讯中断来回抢时间。后来索性把整个电流环改成FPGA实现用Verilog写的坐标变换、PI调节器和七段式SVPWM一次性在硬件流水线上跑完同样是20kHz开关频率电流环的计算延迟直接压到几微秒以内系统带宽和响应手感完全是两个级别。这篇文章就把这套基于FPGA的永磁同步伺服系统设计思路完整整理一遍重点拆解硬件电流环和SVPWM这两块配套的Verilog代码框架、定点化细节、仿真验证方法也会一并讲清楚。适合正在做电机控制、打算把FOC从DSP或MCU搬到FPGA的工程师以及想用Verilog从底层验证SVPWM算法的学生朋友。1. 为什么伺服电流环要落到FPGA上一次DSP方案的性能算账1.1 DSP方案遇到的计算瓶颈先算一笔账。伺服系统里电流环通常跑20kHz周期50微秒。TI C2000系列的DSP比如TMS320F28379D主频200MHz做一整套FOC——三相电流采集偏移校正、Clarke变换、Park变换、两路PI调节、反Park变换、SVPWM扇区判断和占空比计算——指令周期大约3000到5000个换算过来是15到25微秒。光计算就占了40%的控制周期再加上ADC采样触发、PWM断点中断、编码器位置更新、通讯中断这些乱七八糟的抢占实际可用的裕量非常紧张。更麻烦的是中断抖动。DSP里中断优先级再高也无法保证每次都在完全相同的时间点上采样和更新PWM。电流环的延迟抖动直接反映成电流相位裕量的损失表现出来就是电机高速运行时电流噪声变大、转矩脉动明显速度环带宽很难往上提。FPGA的方案就没有这个问题。控制算法全部变成组合逻辑和流水线寄存器采样时钟一到数据流过Clarke、Park、PI、SVPWM整条流水线几百个时钟周期就出结果时间确定且极短。同样20kHz的电流环FPGA用100MHz系统时钟一个完整的电流环计算大约花1到2微秒占控制周期不到4%。省下来的计算裕量可以做更高阶的滤波、前馈补偿甚至在同一颗FPGA里挂三四个轴的电流环。1.2 三大环的分工与硬件电流环的边界伺服系统典型的三环结构位置环、速度环、电流环。电流环决定了转矩响应的快速性是内环速度环和位置环在外层响应速度依次降低。在FPGA加DSP的异构架构里通常让FPGA负责最内层、最需要确定性的电流环和PWM生成DSP或软核负责速度环、位置环、逻辑控制和上位机通讯。为什么不是三个环全部塞进FPGA速度和位置环的控制周期慢得多一般1kHz到5kHz计算量小用DSP写C代码维护起来方便得多改个PID参数重新编译一下就行。电流环则不同它每个PWM周期都要更新一次对实时性极度敏感最适合放进硬件流水线。这种分工下DSP只需要周期性地把电流指令Iq_ref、Id_ref通常Id_ref置0写入FPGA的寄存器FPGA完成电流采样、变换、PI调节、SVPWM生成再把电流实际值、母线电压、故障标志这些状态反馈给DSP。接口简洁职责清晰。2. 系统整体架构与坐标变换模块的Verilog落地2.1 模块划分与数据流一套完整的FPGA伺服电流环大致由以下几部分组成模块功能关键要点ADC接口采集两相或三相电流、母线电压多通道同步采样去偏置滑动平均滤波编码器接口解码增量式编码器A/B/Z信号正交解码、计数、Z脉冲零点锁存角度计算机械角转电角度查正余弦表电角度 机械角 × 极对数Clarke变换三相静止abc到两相静止αβ只需要两相电流第三相由KCL推算Park变换静止αβ到旋转dq用角度表查sin/cos电流PI调节器d轴和q轴各一路PI增量式PI、输出限幅、抗积分饱和反Park变换旋转dq到静止αβ电压指令同样需要旋转坐标反变换SVPWM扇区判断、时间计算、三相占空比生成七段式带死区插入寄存器组DSP与FPGA通信接口指令下发、状态回读、参数在线修改数据流顺着一条单向流水线走电流采样值进ClarkeClarke的结果进Parkdq电流进PI调节器PI输出的dq电压进反Parkαβ电压进SVPWM最后变成三相PWM驱动逆变器。放一张数据流简图在脑海里采样值从左进PWM从右出中间全程无反馈循环天然适合用Verilog流水线实现。2.2 Clarke变换的定点化实现Clarke变换把三相静态坐标换算到两相公式是(I_\alpha I_a)(I_\beta (I_a 2I_b) / \sqrt{3})硬件里没有浮点数需要把所有系数换成定点整数。假设电流采样经过12bit ADC和偏置校正后电流量程±10A映射到有符号数[-2048, 2047]1.0对应的定点Q值是4096即Q12格式。那么 (1/\sqrt{3}) 乘以4096约等于2365写作代码// Clarke变换输入i_a、i_b为已去除偏置的12bit有符号电流值 wire signed [15:0] i_alpha; wire signed [15:0] i_beta; wire signed [31:0] sum_ab; assign i_alpha i_a; assign sum_ab { {14{i_a[15]}}, i_a } { {14{i_b[15]}}, i_b }; // 符号扩展相加 assign i_beta (sum_ab * 16sd2365) 12; // 乘1/sqrt(3)再右移12位这里有个容易踩的坑两个12bit有符号数相加可能到14bit必须先做符号扩展到足够的位宽再乘系数否则负数会算错。乘积用32bit中间变量暂存最后算术右移12bit回到Q12格式。Clarke变换本身不消耗什么资源两三个乘法器就够。2.3 Park变换与角度表的生成Park变换把αβ静止坐标转到dq旋转坐标(I_d I_\alpha \cos\theta I_\beta \sin\theta)(I_q -I_\alpha \sin\theta I_\beta \cos\theta)角度θ来自编码器位置计数。增量式编码器每转一圈发固定的脉冲数减去Z脉冲清零值后乘以极对数就得到电角度。工程上一般把一圈机械角度切成4096或8192份作为角度计数基准电角度则按极对数折叠到[0, 360°)区间。sin和cos不必现场计算直接预生成一张表存入ROM。表格密度256或512项足够每项包含sin和cos两个值存成Q14格式1.0对应16384。查表得到角度值后两路乘法加一个加法器// 反Park变换dq电压转αβ电压 // v_d、v_q为PI输出Q12格式sin_theta、cos_theta为Q14格式查表值 wire signed [31:0] prod1 v_d * cos_theta; wire signed [31:0] prod2 v_q * sin_theta; wire signed [31:0] prod3 v_d * sin_theta; wire signed [31:0] prod4 v_q * cos_theta; assign v_alpha (prod1 - prod2) 14; assign v_beta (prod3 prod4) 14;角度表还有一个妙用SVPWM的扇区判断可以不需要繁杂的代数比较直接用角度高位查扇区。电压矢量角度 (phase atan2(U_\beta, U_\alpha))相位的高两位就代表了所在的60度扇区。角度获取可以用CORDIC也可以借助已有的角度表做近似。3. 电流环核心PI调节器与限幅防饱和的硬件设计3.1 增量式PI在流水线里的表达电流环的两路PI调节器输出的是dq轴电压给定。硬件实现通常用增量式PI因为只需要保存上一次的误差和积分累加值寄存器资源最省。公式拆开(e_k ref - feedback)(u_k u_{k-1} K_p(e_k - e_{k-1}) K_i e_k)写成Verilog核心是一个带使能的状态机每个电流环周期触发一次。比例项和积分项可以并行计算最后在加法器里合并module pi_controller #( parameter IW 16, // 输入位宽 parameter KW 16, // 系数位宽 parameter OW 20 // 输出位宽 )( input wire clk, input wire rst_n, input wire en, // 每个PWM周期触发一次 input wire signed [IW-1:0] ref, input wire signed [IW-1:0] fb, output reg signed [OW-1:0] out ); reg signed [IW-1:0] err_d; reg signed [OWKW-1:0] acc; // 积分累加器 wire signed [IW-1:0] err ref - fb; wire signed [IWKW-1:0] pterm err * kp; // 比例项 wire signed [IWKW-1:0] iterm err * ki; // 积分增量 always (posedge clk or negedge rst_n) begin if (!rst_n) begin err_d 0; acc 0; out 0; end else if (en) begin err_d err; acc acc iterm; out pterm acc; // 输出限幅放在外部状态机 end end endmodule这里PI的Kp和Ki必须提前换算成定点数。举个例子一个表贴式PMSM相电感0.8mH相电阻0.5Ω目标电流环带宽1kHz。工程设计上取 (K_p L \times \omega_c)(K_i R \times \omega_c)(\omega_c2\pi\times1000\approx6283rad/s)算出来 (K_p \approx 5.03V/A)(K_i \approx 3141V/(A\cdot s))。电流反馈量程0.5A/LSB误差的Q12值为2048×0.51024则Kp的定点值约是5.03×2048/1024≈10。具体换算比较绕我建议先做代数推演再仿真校准先给一组初值看阶跃响应再微调。3.2 输出限幅必须匹配SVPWM线性区PI输出限幅是整个电流环最容易忽视却最关键的环节。SVPWM在线性调制区内相电压最大幅值是 (U_{dc}/\sqrt{3})。母线60V时最大相电压幅值约34.6V。如果PI的dq电压指令超过这个值SVPWM会进入过调制区实际输出电压不再跟随指令电流环就失去了线性关系甚至引发震荡。所以PI输出限幅值要折算到反Park之前的dq电压范围。最简单的思路把限幅值设成母线电压三分之一附近换成骨折点。实际码子里模块接口输出的v_d、v_q通过饱和处理// 输出限幅saturate_val由SVPWM线性区约束决定 wire signed [OW-1:0] fall_lim -saturate_val; wire signed [OW-1:0] rise_lim saturate_val; always (*) begin if (out rise_lim) out_sat rise_lim; else if (out fall_lim) out_sat fall_lim; else out_sat out; end如果SVPWM内部把v_alpha、v_beta按母线电压归一化成调制度0到1之间的值那么saturate_val对应的就是归一化系数换算后的最大值。这套换算要前后一致否则闭环特性会很奇怪——你说不清到底是PI问题还是归一化问题。3.3 抗积分饱和条件积分与积分回退PI调节器在输出限幅后积分项会持续累积导致饱和恢复时产生超调。硬件里最常用的处理是条件积分法输出没有进入限幅区时才允许累加积分项一旦限幅生效就把积分增量掐掉。改一行代码的事// 条件积分未饱和才允许积分累加 wire is_saturated (out rise_lim) || (out fall_lim); always (posedge clk or negedge rst_n) begin if (!rst_n) acc 0; else if (en !is_saturated) acc acc iterm; end另一个更精细的办法是积分回退把积分项往反方向拉一截动态更快但实际调试中发现条件积分在大多数伺服场景已经够用而且代码可读性高。有条件积分的PI电流阶跃响应基本没有超调这个在仿真波形里能直接看到。4. SVPWM生成扇区判断、马鞍形波的秘密与死区处理4.1 为什么SVPWM输出是马鞍形波SVPWM的名字看着高大上本质是回答一个问题六个开关管能合成六个基础电压矢量和两个零矢量如何在一个PWM周期里用这些离散矢量拼出一个任意方向的平均电压矢量答案是时间加权平均。一个PWM周期里用相邻的两个基础矢量和零矢量各作用一定时间让它们的矢量合成结果等于目标电压矢量。这样做出来的相电压波形不再是纯正弦而是一个马鞍形——这是SVPWM最出名的视觉特征。为什么会有马鞍形因为SVPWM等效于在正弦调制波里注入了三次谐波分量。三次谐波本身不产生转矩却能抬高调制波的波峰压平等效增大了基波电压的利用空间。对比SPWM正弦调制在满调制时相电压幅值最多达到母线电压的一半SVPWM因为加入三次谐波可以把相电压幅值推到 (U_{dc}/\sqrt{3})母线利用率提升了约15%。这也是为什么伺服驱动里普遍选SVPWM而不是SPWM。4.2 七段式SVPWM的扇区判断与作用时间表标准的七段式在一个PWM周期里安排七个矢量段首尾是零矢量中间是目标相邻矢量顺序设计成开关切换次数最少的模式。第一步是扇区判断。已知电压矢量在αβ轴的投影Uα、Uβ可以用代数法判断所在60度扇区。设三个中间变量(U_1 U_\beta)(U_2 \frac{\sqrt{3}}{2}U_\alpha - \frac{1}{2}U_\beta)(U_3 -\frac{\sqrt{3}}{2}U_\alpha - \frac{1}{2}U_\beta)三个量的符号组合编码成N值映射到扇区。Verilog里做符号判断非常方便wire s1 (u_beta 0); wire s2 (sqrt3_div2_scale * u_alpha - half_scale * u_beta 0); wire s3 (-sqrt3_div2_scale * u_alpha - half_scale * u_beta 0); wire [2:0] n {s3, s2, s1}; // 组合编码 // 根据N值查表得到扇区 always (*) begin case (n) 3b011: sector 1; // 扇区I 3b001: sector 2; // 扇区II 3b101: sector 3; // 扇区III 3b100: sector 4; // 扇区IV 3b110: sector 5; // 扇区V 3b010: sector 6; // 扇区VI default: sector 1; endcase end注意N值编码顺序在不同资料里可能不同因为A/B/C的位权分配不一样。以你实际代码里的对照表为准写完之后用仿真把整个360度扫一遍确认扇区切换顺序是1-2-3-4-5-6连续跳变别跳号。第二步是计算基础矢量的作用时间。先算出三个中间变量(X \sqrt{3} U_\beta T_s / U_{dc})(Y (\frac{3}{2}U_\alpha \frac{\sqrt{3}}{2}U_\beta) T_s / U_{dc})(Z (-\frac{3}{2}U_\alpha \frac{\sqrt{3}}{2}U_\beta) T_s / U_{dc})再按扇区查表取T1和T2扇区T1T2IZYIIY-XIII-ZXIV-XZVX-YVI-Y-ZT1和T2求出来后零矢量时间 (T_0 T_s - T_1 - T_2)七段式里零矢量拆成头尾两半。4.3 比较计数值换算与马鞍形波的产生工程里不会真的去算T1、T2的浮点时间而是换成PWM定时器比较寄存器的计数值。设PWM周期计数值为TPR则三相比较值可以统一计算。以扇区I为例三相切换点在载波周期内的位置也就是比较值按如下顺序排布零矢量前半段(C_0 T_0/4)T1段结束(C_1 C_0 T_1/2)T2段结束(C_2 C_1 T_2/2)A、B、C三相在每个扇区的比较值本质上就是把T1、T2、T0按七段顺序分配到三相上。查表能完成不需要每次重新推公式。核心代码如下// 三角载波计数器cnt从0到TPR比较值cma、cmb、cmc直接决定PWM翻转 always (posedge clk) begin case (sector) 1: begin cma cnt_cmp[0]; cmb cnt_cmp[1]; cmc cnt_cmp[2]; end 2: begin cma cnt_cmp[2]; cmb cnt_cmp[0]; cmc cnt_cmp[1]; end // 其余扇区按标准七段式顺序查表 endcase end当载波计数到比较值时对应相的输出翻转就生成了马鞍形的相电压调制波。这里有个容易产生困惑的点看Uα或Uβ的波形是马鞍形但看三相PWM的占空比变化趋势就能看到波峰处的占空比被压平这是SVPWM注入三次谐波的直接表现。仿真时不要只盯正弦电流把调制波拉出来看一眼确认马鞍形状基本就说明SVPWM工作了。4.4 三相互补PWM与死区插入的Verilog实现逆变器的上下桥臂不能同时导通必须插入死区时间。硬件里的标准做法是下降沿直通上升沿延迟。也就是输出从1变0时立即执行从0变1时延迟n个时钟周期。死区周期数按死区时间除以系统时钟周期计算例如死区1微秒、100MHz时钟就是100个时钟周期。// 简单可综合的死区生成上升沿延时dead_cycle个周期 always (posedge clk or negedge rst_n) begin if (!rst_n) begin pwm_a_hi 0; pwm_a_lo 1; end else if (pwm_raw_a 1) begin // 上升沿延迟输出 pwm_a_hi 1b0; pwm_a_lo 1b1; delay_cnt dead_cycle; end else begin if (delay_cnt ! 0) begin delay_cnt delay_cnt - 1b1; pwm_a_hi 1b0; pwm_a_lo 1b1; end else begin pwm_a_hi 1b1; pwm_a_lo 1b0; end end end死区插入会让电流波形在过零点附近出现畸变和毛刺这是正常的不能误判成算法bug。优化手段包括死区补偿、最小脉宽限制这些可以后续再加。5. 仿真验证testbench设计与波形检查的实战经验5.1 一套能跑起来的testbench骨架FPGA电机控制仿真最大的坑是激励不会写。电机是一个带反电动势和非线性的对象你不能只给一个固定三相电流。一个实用的做法不建复杂的电机模型只搭一个简化的RL负载加反电动势把SVPWM输出的电压作用上去再看电流闭环能不能跟踪指令。testbench里要生成几样东西系统时钟、复位、模拟编码器的角度信号、模拟ADC采样的三相电流。模拟编码器最简单的方式是用一个加法器周期性累加得到机械角度再从角度生成A、B、Z脉冲。三相电流可以用理想模型反推给定额定Id、Iq指令经过反Park和Clarke逆变换生成三相正弦电流加上少许谐波和高频噪声作为ADC通道的输入。// testbench: 生成三相理想电流激励 always (posedge clk) begin theta_mech theta_mech step; // 匀速旋转 end // 由theta_e和给定的id、iq反变换出ia、ib、ic assign ia id*cos(theta_e) - iq*sin(theta_e); assign ib id*cos(theta_e - 2.094) - iq*sin(theta_e - 2.094); assign ic id*cos(theta_e 2.094) - iq*sin(theta_e 2.094);注意这是开环验证坐标变换和SVPWM用真正闭环调试PI参数时还是要给一个带反电动势的电机模型否则PI的输出和实际电压之间的物理关系对不上。5.2 波形检查的几个关键位置搭好仿真后至少检查这几处波形由里到外排查Clarke和Park的输出给定正弦三相电流Id应该稳定在给定值Iq稳定在另一个给定值两者解耦干净。PI输出限幅是否触发看输出是否长时间顶在饱和值上如果是检查限幅系数和反馈系数是否匹配。SVPWM的比较值曲线看三相比较值是否连续变化扇区切换处不应有跳变台阶。马鞍形调制波确认相电压调制波的波峰被压平说明三次谐波注入生效。死区时间放大PWM边沿确认上下桥臂之间有足够的死区间隔上升沿延迟生效。仿真建议用开源的Icarus Verilog加GTKWave纯RTL验证足够跑一个20kHz周期的闭环仿真几十毫秒的波形也就几分钟。工程比较大的时候再用Vivado或Quartus自带的仿真器后仿时序更接近真实。5.3 顺带解决ADC采样毛刺滑动窗口滤波的Verilog实现电流采样毛刺在硬件上几乎无法避免尤其是IGBT开关瞬间的di/dt耦合进采样回路。滑动窗口滤波是成本最低、最稳妥的方案。实现思路是维护一个长度为N的FIFO每次新数据进来总和加减最旧数据// 滑窗均值滤波窗口长度WIN_SIZE reg signed [15:0] window[0:WIN_SIZE-1]; reg [7:0] wr_ptr; reg signed [23:0] sum; always (posedge clk) begin sum sum adc_data - window[wr_ptr]; window[wr_ptr] adc_data; wr_ptr wr_ptr 1b1; end assign adc_filtered sum / WIN_SIZE;窗口长度取4到16长度太长会引入相位延迟电流环的带宽会被拖低。我的经验是采样频率20kHz时窗口取4或8就很合适延迟一个到两个采样周期对电流环相位裕度的影响可控。6. 工程排坑位宽、时序约束与外围配合的实战提醒6.1 定点位宽选择的核心原则FPGA里做定点运算位宽选大了浪费资源选小了溢出算错调试时非常隐蔽。我的习惯是电流反馈信号按传感器满量程的1.5倍设计位宽中间乘积累积阶段位宽至少是输入位宽的两倍每次右移回到目标Q格式时做饱和判断不要把溢出值截断进去。举个例子12bit ADC的电流值范围[-2048, 2047]Clarke输出也在这个量级可以安全放进int16。但Park变换要乘Q14的sin值乘积到Q26右移14位回到Q12前必须用32bit中间量。如果用int16接乘积一个负电流乘正余弦很容易就溢出了表现是Id或Iq在角度过零点出现突发尖刺排查起来很费劲。乘法器资源也要规划。Clarke两个乘法、Park四个乘法、SVPWM时间计算三个乘法、PI两路积分各一个乘法一共十几个乘法器。中端FPGA比如Cyclone 10或Artix-7级别内置DSP块通常够用。如果不冗余可以把sin/cos查表改成CORDIC迭代用逻辑资源换乘法器但代码复杂度会高不少。6.2 时序约束与跨时钟域处理FPGA控制板的时钟体系通常有两个域系统时钟比如100MHz和ADC采样时钟可能12MHz或20MHz。ADC数据进入系统时钟域前必须做同步处理否则亚稳态问题在仿真里看不到上板后随机抽风。标准做法是两级触发器同步加握手信号。ADC采样完成信号打两拍再触发采样值锁存。PWM定时器的更新则完全由系统时钟域驱动保证所有控制和PWM在同一时钟域减少跨时钟域路径。时序约束上给时钟加上create_clock约束对SVPWM比较值到PWM输出的路径设一个简单的输出延迟约束。如果不做约束布局布线可能把关键路径压到刚好在时钟沿附近温度一高就出问题。这类问题很难复现上板后抓毛刺抓到怀疑人生不如一开始就把约束建好。6.3 与速度环、上位机的联调技巧FPGA里的电流环调试通过后进入系统联调阶段我建议做到三件事把所有PI参数、限幅值、滤波系数都映射成可读写的寄存器做成脚本在线改参。DSP和FPGA之间用简单的并行总线或者SPI/AXI-Lite接口每次改完参数不用重新综合调试效率高很多。在FPGA里增加一个故障状态寄存器把过流、过压、编码器异常、PWM故障等标志汇总上报。电流环跑飞之前硬件故障标志会先于软件判断给出反应这对保护功率级至关重要。联调时先用很小的电流指令比如额定值的5%跑空载确认电流环稳定后再逐渐加大指令同时观察Id和Iq是否保持解耦。如果负载加上去后Iq指令增大Id还始终稳定在0附近说明Park变换角度和PI参数都正常。最后说一个我自己的调试习惯每次上板前先在仿真里把整个控制周期跑通再在真实硬件上先用开环给定一个固定占空比烧电机看电流波形确认ADC方向和编码器方向都对最后才闭合电流环。很多人跳过这步直接闭环结果方向反了或者偏置没处理电机以最大电流顶到限位轻则烧驱动重则伤人。这个顺序省了我无数次返工真心建议你也这么做。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进