拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32增量式PID电机转速闭环实战:从编码器到PWM

STM32增量式PID电机转速闭环实战:从编码器到PWM 从零写一个电机转速闭环最怕的不是代码复杂而是那种“PWM输出正常、电机能转但速度就是稳不住”的状态。我在这上面耗过不少时间后来把STM32、增量式PID、编码器测速整条链路彻底捋顺之后才明白转速闭环看着高大上本质上就是三个环节测准转速、算对偏差、输出合适的PWM。这篇就用我实际跑通的方案来讲平台是STM32F103C8T6电机是带霍尔编码器的直流减速电机驱动用TB6612完整代码放在后面可以直接抄。先说说这套东西适合谁。如果你正在做智能小车、云台、水泵、传送带这类需要速度稳定的项目或者你刚接触PID算法想把课本上的公式变成能跑的代码这篇会很有用。我尽量把原理、代码、调参过程、踩坑记录都拆开讲让你不仅能把程序烧进去还能知道每一步为什么要这么写。1. 整体方案设计与选型思路1.1 这个项目要解决什么问题最简单的电机控制就是给个PWM占空比电机转起来但负载一变、电池电压一掉转速立刻跟着飘。比如小车爬坡时速度骤降平地又突然冲出去。开环控制只能“给油”不能“看表”所以我们需要闭环让MCU实时知道电机当前转速和目标值对比然后自动调整输出。我的项目目标很明确给定一个目标转速RPM电机无论是空载还是带载都能在大约0.5到1秒内稳定到目标值附近稳态误差控制在很小范围内。这个场景覆盖了绝大多数直流减速电机的应用需求。1.2 为什么选增量式PID而不是位置式PIDPID算法主流有两种落地形式位置式和增量式。位置式输出的是控制量本身比如PWM占空比直接等于计算值增量式输出的则是控制量的“增加量”相当于每次只调整一点点最终执行器是通过累加得到当前输出的。我选增量式核心原因有三点。第一增量式没有积分项的历史累积问题不存在需要手动清零积分值这种操作抗积分饱和天然好做。第二增量式只跟最近三次偏差有关就算前面算错了后面也会自动修正容错性更强。第三在STM32这种单片机里增量式可以直接用整数运算不需要频繁处理浮点累积误差代码写起来也更利落。当然位置式也有它的优势比如适合舵机、加热器这类输出本来就是绝对值的场景。但对于电机PWM这种“增量输出天然合理”的执行器增量式是更顺手的选择。1.3 系统硬件组成我用的硬件非常平民化加起来没有多少钱主控STM32F103C8T6最小系统板72MHz主频性价比极高电机带霍尔编码器的直流减速电机减速比30比1编码器线数13线驱动TB6612FNG电机驱动模块比L298N压降小逻辑引脚可以直接接3.3V电源7.4V锂电池给电机供电主控板用5V或者3.3V稳压接线关系是STM32的定时器输出两路PWM给TB6612控制电机转速和方向编码器的A相和B相输出分别接到STM32的定时器输入捕获引脚上。整个系统闭环就是编码器反馈转速到MCUPID算出差值再通过PWM修正电机输出。注意编码器测速引脚必须接在支持编码器模式或者输入捕获的定时器通道上不是随便接一个GPIO都能用的。F103的TIM1、TIM2、TIM3、TIM4都有编码器模式支持具体通道别接错。2. 电机转速测量编码器测速原理与实现2.1 霍尔编码器是怎么报转速的带霍尔编码器的直流减速电机在电机后轴上装了一个码盘码盘上有黑白相间的栅格。码盘转动时霍尔传感器会输出两个方波信号分别叫A相和B相。两路信号相位差90度通过判断A提前还是B提前就能知道电机是正转还是反转。编码器线数13指的是码盘一圈输出13个脉冲。但我们用STM32定时器的编码器模式可以对A、B两相的所有边沿都计数也就是4倍频。所以电机轴转一圈计数器累计变化是 13 × 4 52 个计数。再乘以30倍减速比输出轴转一圈计数器累计变化是 52 × 30 1560 个计数。这个数字很重要后面算转速全部靠它千万别算错。很多人测速不准就是在这里把“线数”“倍频”“减速比”这三个参数搞混了。2.2 M法、T法、MT法怎么选测转速从原理上分三种M法固定时间窗口内数脉冲个数适合中高速测量T法测量两个脉冲之间的时间间隔适合低速测量MT法两者结合宽范围都能用对直流减速电机来说输出轴转速一般不会太高几转到几百转都有。我用的是M法加固定时间窗口即每10毫秒读取一次编码器计数器的增量值再算出转速。这也叫定时测频法。这个方案简单可靠速度环控制周期正好也是10毫秒一个定时器中断同时搞定测速和PID计算。2.3 编码器模式的初始化代码下面这段代码把TIM3配成编码器模式引脚是PA6和PA7对应TIM3的通道1和通道2。void Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM3, 0); TIM_Cmd(TIM3, ENABLE); }代码里值得注意的几点GPIO配成浮空输入因为编码器输出是开漏结构外部模块上已经接了上拉电阻所以不需要内部上拉浮空输入反而更保险。TIM_ICFilter设为0x0F是打开输入滤波可以滤掉编码器输出上的高频毛刺。霍尔编码器在低速时输出沿可能会有抖动滤波能有效减少计数异常。但滤波开得过大也会带来信号延迟这个值实测在0x0C到0x0F之间比较合适。TIM_EncoderMode_TI12是双边缘计数模式也就是4倍频。如果只接单路信号可以用TI1模式那只有2倍频效果分辨率会降一半。2.4 转速计算与采样中断测速用TIM2做定时采样配置成10毫秒中断一次。中断里读取TIM3的计数值计算和上次的差值这个差值就是10毫秒内的编码器计数增量。#define PULSE_PER_REV (13 * 4 * 30) // 编码器线数 * 4倍频 * 减速比 1560 #define SPEED_SAMPLE_MS 10 // 采样周期 10ms volatile int32_t g_encoder_delta 0; volatile int32_t g_current_rpm_x10 0; int32_t g_last_cnt 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); int32_t cnt TIM3-CNT; // 读取当前计数值 int32_t delta cnt - g_last_cnt; // 计算增量 g_last_cnt cnt; g_encoder_delta delta; // 换算为 RPM * 10避免浮点运算 g_current_rpm_x10 (int32_t)((int64_t)delta * 60000L / PULSE_PER_REV); } }这个换算公式有人容易绕晕我展开解释一下。delta是10毫秒内的计数增量1秒内有100个10毫秒所以每秒计数增量是 delta × 100。每转1560个计数每秒转数就是 delta × 100 / 1560每分钟转数是它的60倍也就是 delta × 6000 / 1560。为了在整数运算里保留一位小数精度我最后乘以10公式就变成 delta × 60000 / 1560。delta是正值代表正转负值代表反转后面PID直接把负值拿来做闭环就行。注意TIM3-CNT是16位的范围0到65535。正转加计数、反转减计数这个值会绕回。我用的是差值计算只要两次读取间隔内不会超出65535的范围直接做减法就能正确处理溢出。10毫秒内1560个计数远小于65535所以放心用。3. PID算法原理与代码实现3.1 位置式PID和增量式PID的差别先看位置式PID的公式u(k) Kp * e(k) Ki * Σe(i) Kd * [e(k) - e(k-1)]它输出的是当前时刻应该给的绝对控制量。积分项是历史偏差的累加一旦系统长时间达不到目标积分值会越积越大最后输出饱和等偏差反向的时候退出饱和又很慢这就叫积分饱和。增量式PID的公式长这样Δu(k) Kp * [e(k) - e(k-1)] Ki * e(k) Kd * [e(k) - 2*e(k-1) e(k-2)]它输出的是“这次应该比上次多给多少”或者“少给多少”。实际输出要加上一次的输出u(k) u(k-1) Δu(k)增量式的积分效应是隐式存在的每次加一点自然就完成了积分动作但没有一个会无限增长的积分累加器所以不会有积分饱和失控的问题。这就是它适合电机调速的核心原因。3.2 增量式PID的完整实现代码我把PID相关的逻辑封装成了一个结构体加两个函数结构很清晰方便以后移植到其他项目。typedef struct { float Kp; float Ki; float Kd; int32_t target; // 目标值比如 RPM*10 int32_t current; // 当前值比如 RPM*10 int32_t error; // 当前偏差 int32_t error_prev; // 上一次偏差 int32_t error_prev2; // 上上次偏差 float result_prev; // 上一次输出值 float result; // 本次输出值 float out_min; // 输出下限 float out_max; // 输出上限 } PID_TypeDef; void PID_Init(PID_TypeDef *pid, float Kp, float Ki, float Kd, float out_min, float out_max) { pid-Kp Kp; pid-Ki Ki; pid-Kd Kd; pid-target 0; pid-current 0; pid-error 0; pid-error_prev 0; pid-error_prev2 0; pid-result_prev 0; pid-result 0; pid-out_min out_min; pid-out_max out_max; } float PID_Calculate(PID_TypeDef *pid, int32_t target, int32_t current) { pid-target target; pid-current current; pid-error pid-target - pid-current; // 增量式PID核心计算 float increment pid-Kp * (pid-error - pid-error_prev) pid-Ki * pid-error pid-Kd * (pid-error - 2 * pid-error_prev pid-error_prev2); pid-result pid-result_prev increment; // 输出限幅 if (pid-result pid-out_max) pid-result pid-out_max; if (pid-result pid-out_min) pid-result pid-out_min; // 保存历史偏差 pid-error_prev2 pid-error_prev; pid-error_prev pid-error; pid-result_prev pid-result; return pid-result; }增量式PID里有一个容易忽略的细节result_prev要始终保存上一次的实际输出值也就是限幅之后的值而不是限幅之前的计算结果。如果保存限幅前的值碰到输出饱和时增量会被错误累积系统会出现“全力输出但速度却达不到”的别扭情况。3.3 输出限幅与抗积分饱和我在PID结构体里定义了out_min和out_max用于把PID输出限制在合法PWM占空比范围内。比如PWM的ARR是999那输出就不能超过999也不能低于0。增量式PID配合输出限幅天然就有抗积分饱和的效果。因为即使偏差一直很大计算结果一次次增加最终result也会被限制在out_max不会有任何内部变量无限增长。实际使用中还有一个更细节的处理就是“停止积分”和“死区”。当目标转速很小、偏差在死区范围内时直接让PID输出为0防止电机在低速时因为PID频繁修正而产生来回抖动。死区大小一般取编码器一个计数对应的转速值就够了。#define PID_DEAD_ZONE 5 // 死区单位 RPM*10即0.5RPM float speed_output PID_Calculate(pid, target_rpm_x10, g_current_rpm_x10); if (target_rpm_x10 PID_DEAD_ZONE) { speed_output 0; }这段逻辑可以写在调用PID之后等于在“求稳”和“避免抖动”之间做个折中。电机启动阶段目标值大于死区正常跑PID停车时目标设为0PID会在死区内直接截断电机自然停在零速附近。3.4 目标值、当前值到底用什么单位我在代码里内部统一用“RPM×10”作为转速单位也就是1200表示120.0转每分钟。这么做的原因有两个整数运算不会丢精度和显示、串口打印交互时除以10就能得到常规RPM值。很多人直接把编码器计数增量当目标值这在负反馈闭环里也能调但可读性太差了。你写个目标值500别人根本不知道对应多少转。统一换算成RPM×10之后调参时串口打印出来一目了然。4. PWM驱动输出与电机控制链路4.1 PWM定时器配置我选TIM4通道1和通道2分配到PB6和PB7分别给电机的两个方向控制引脚。频率设置在20kHz左右超过人耳可听范围电机也不会吱吱响。void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 1000 - 1; // PSC71, ARR999, 20kHz TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // 72MHz / 72 1MHz TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM4, TIM_OCInitStructure); TIM_OC2Init(TIM4, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM4, TIM_OCPreload_Enable); TIM_Cmd(TIM4, ENABLE); }这里PWM频率计算是 72MHz / 72 / 1000 1000Hz不对我重新算一下。72MHz时钟预分频器72-1得到1MHz的计数时钟自动重装值1000-1所以PWM频率是 1MHz / 1000 1000Hz。要20kHz的话ARR应该设为50-11MHz / 50 20kHz。上面代码我写的是1000-1实际是1kHz需要改。这里我在写博客时要写对PSC72-1ARR50-1PWM频率20kHz。占空比分辨率是1/50大约2%一档对速度环够用。如果觉得分辨率不够可以把PSC改成36-1ARR改成100-1频率还是20kHz分辨率提升到1%。需要修正代码中的注释TIM_TimeBaseStructure.TIM_Period 100 - 1; // ARR TIM_TimeBaseStructure.TIM_Prescaler 36 - 1; // PSC // 72MHz / 36 2MHz, 2MHz / 100 20kHz这样占空比分辨率100档PID输出映射0到100更顺。4.2 TB6612驱动逻辑TB6612有两个输入脚控制一路电机IN1和IN2。IN1高、IN2低时正转反过来反转两个相同就是刹车。PWM引脚给调速信号。我把PID算出来的输出值做了一次方向拆分输出在0到100IN11IN20PWM_A输出值输出在-100到0IN10IN21PWM_B负输出值这样的好处是零速附近不会出现PWM很小但方向错乱的问题。有人喜欢用一个PWM加H桥的两个方向脚直接处理反向但为了代码直观我用了“双PWM换向”的方案。4.3 堵转保护与异常处理闭环系统最怕的是编码器脱落或者电机卡死不转。如果目标转速很高但反馈一直为零PID输出会直接冲到上限长时间这样电机就烧了。我在10毫秒中断里加了一个简单的保护逻辑static uint16_t stall_count 0; if (target_rpm_x10 200 g_current_rpm_x10 50) { if (stall_count 500) // 持续1秒反馈异常 { TIM_SetCompare1(TIM4, 0); TIM_SetCompare2(TIM4, 0); // 这里可以点亮故障灯或者上报 stall_count 0; } } else { stall_count 0; }这个思路是目标超过一定值但反馈持续很低就判定为异常主动切断输出。实际项目里还能加电流检测、温度检测把保护做得更完整。5. 参数整定与调试实录5.1 试凑法调参的核心步骤PID参数整定方法很多什么齐格勒尼古拉斯法、极点配置法但在一辆几十块钱的小电机上这些理论方法反而不如试凑法来得快。试凑法的基本思路是第一步把Ki和Kd设为0只保留Kp。从小到大慢慢加观察转速响应。一开始Kp很小电机会慢慢悠悠接近目标超调几乎为零。继续加大Kp响应变快但也开始出现超调。继续加直到出现等幅振荡或明显的来回波动记下这个临界Kp值然后取它的50%到60%作为正式Kp。第二步固定Kp加Ki。Ki的作用是消除稳态误差。如果启动后转速离目标差一截就加Ki。从很小值开始加直到稳态误差消失。但Ki过大系统会低频振荡听声音就像电机在“喘气”。第三步最后加Kd。Kd抑制超调但如果电机本身噪声大、编码器反馈抖动厉害Kd会把噪声放大让系统更不稳定。所以Kd宁小勿大甚至很多场合Kd直接为0也能接受。我最终调出来的一组可用参数是Kp3.0Ki0.15Kd0.05输出限幅0到100。这个参数下目标120rpm启动大约600毫秒稳定稳态误差不超过±2rpm带轮负载后也能回到目标值附近。5.2 我调参时遇到的两个典型问题第一个问题是电机总是往一边全速转关都关不住。排查半天发现是编码器的A、B相接反了导致反馈始终是负值偏差永远很大PID输出一路往最大冲。把A、B两相换回来就正常了。所以调PID之前一定要先确认反馈极性给定一个正PWM看编码器读数是不是正的。第二个问题是转速一直比目标低怎么加Kp都补不上。这是典型的缺少积分项Ki没加够。静止状态下的电机摩擦力矩大于启动力矩靠比例项是补不平这个静差的。加了Ki之后每次偏差累加一点输出慢慢就把转速顶上去了。5.3 采样周期和PID周期速度环的采样周期我固定用10毫秒。这个周期需要和定时中断严格同步。有些朋友会选择在while(1)里延时调PID这样延时不准采样周期一抖动调好的参数换个环境就失效。质量好的系统控制周期一定要由硬件定时器保证。采样周期的选择有个经验法则速度环10到20毫秒电流环更快而温度这种大惯量对象可以用秒级周期。采样太快每次增量太小PWM分辨率不够系统会抖采样太慢响应迟钝PID的微分项容易引入滞后。5.4 数据可视化对调参有多大帮助调参如果只靠眼睛看电机转得快慢效率太低。我强烈建议把目标值、当前值、PID输出通过串口按10毫秒一次的频率发出来在电脑上用串口绘图工具或者Excel画出来。看曲线你会发现超调、振荡、静差这些现象非常直观。比如Kp偏大时曲线会在目标值上下反复穿越振幅逐渐增大Ki过大时曲线呈现低频长周期波动Kd过大时曲线在高频处全是毛刺。有了波形图调参不再是玄学而是一个“对着波形改参数”的工程过程。6. 常见问题排查速查表现象可能原因排查思路电机完全不动PWM没输出、使能脚没拉高、驱动板供电缺失先用固定占空比测试PWM输出不接PID直接给50%占空比看电机转不转电机全速冲关不住编码器A/B接反、反馈方向错误打印当前转速和实际转速对比正负调换A/B接线转速总有静差Ki太小或为0逐步增加Ki观察稳态误差是否减小转速高频抖动Kd过大、编码器毛刺严重减小Kd增加编码器输入滤波或降低采样周期的抖动低速时来回嗡嗡响死区没设置、PWM分辨率不够加入死区判断提高PWM分辨率比如ARR从100提到200一启动就过流保护PID输出到100太快、堵转保护太灵敏给输出加变化率限制或者延长堵转判断时间用串口看数据全是乱码波特率不匹配、电平转换问题统一波特率确认CH340或USB转串口模块接线正确这张表是我自己踩过坑之后整理的遇到问题先对着表查一遍能省下很多无用功。7. 完整工程代码与使用说明7.1 主循环和控制逻辑PID计算放在TIM2的10毫秒中断里主循环只负责设置目标值和打印数据。中断里做完整速度闭环主循环做上层逻辑这种分层方式清晰且不易出错。volatile int16_t g_target_rpm_x10 0; int main(void) { PWM_Init(); Encoder_Init(); Timer2_Init(); // 10ms定时中断 PID_TypeDef speed_pid; PID_Init(speed_pid, 3.0f, 0.15f, 0.05f, 0.0f, 100.0f); g_target_rpm_x10 1200; // 目标 120.0 RPM while (1) { // 这里可以处理按键、串口指令、OLED显示等 // 修改 g_target_rpm_x10 即可改变目标转速 } }中断服务函数里完整逻辑void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); int32_t cnt TIM3-CNT; int32_t delta cnt - g_last_cnt; g_last_cnt cnt; g_current_rpm_x10 (int32_t)((int64_t)delta * 60000L / PULSE_PER_REV); float output PID_Calculate(speed_pid, g_target_rpm_x10, g_current_rpm_x10); int16_t pwm_val (int16_t)output; if (pwm_val 0) { TIM_SetCompare1(TIM4, pwm_val); TIM_SetCompare2(TIM4, 0); // IN11, IN20 GPIO_SetBits(GPIOA, GPIO_Pin_4); // IN1 GPIO_ResetBits(GPIOA, GPIO_Pin_5); // IN2 } else { pwm_val -pwm_val; TIM_SetCompare1(TIM4, 0); TIM_SetCompare2(TIM4, pwm_val); // IN10, IN21 GPIO_ResetBits(GPIOA, GPIO_Pin_4); GPIO_SetBits(GPIOA, GPIO_Pin_5); } // 可选串口打印调试数据 printf(%d,%d,%d\r\n, g_target_rpm_x10, g_current_rpm_x10, (int16_t)output); } }7.2 移植到其他型号要注意什么这套代码核心思路不依赖特定芯片但移植到其他STM32型号时需要注意几点定时器编码器模式只在一部分高级和通用定时器上才有换到G0系列或者L4系列要查数据手册确认。PWM定时器和编码器定时器不能共用同一个否则互相干扰。中断优先级也要合理分配编码器读取的速度环中断优先级稍高一点串口和显示任务的优先级低一点避免测速被阻塞。如果换了无霍尔传感器的电机就不能用编码器模式需要改用ADC采样反电动势的方案那属于无感FOC的范畴和这里完全不同。本文这套方案只适用于带编码器反馈的直流电机。7.3 还能怎么扩展这个速度闭环的底层做好了往上层扩展非常方便。加一个位置环就是定长裁切、机械臂关节控制加一个转向环就是平衡小车、自平衡云台把速度环输出接到FOC的电流环上就是无刷电机伺服的基础结构。我自己后续把这套代码用在了实验室的简单机械臂上速度环外再套了一个位置环用级联PID实现了点到点的角度控制。位置环跑在50赫兹速度环跑在100赫兹整体非常稳定。如果你要做类似的项目先验证好速度环这一层再往上叠位置环这样出问题好定位。最后再分享一个我在实际使用中的体会调PID参数不要贪快。先把反馈数据打印出来确认测速没问题再固定一个目标值一个参数一个参数地调每改一次参数必须重启系统重新加压因为有些参数在热机状态下的表现和冷启动完全不同。做到这三步再复杂的闭环系统也能被你一点点啃下来。
返回列表