主控:STM32F103C8T6
在进行速度控制之前,我们首先需要进行速度采样,这里参见这篇博文
这里不细说电机驱动模块的选型和使用,而是说一个常见的误区。我们驱动电机要使用两路PWM,一般是一路给PWM信号,一路是纯低电平。但这其实是不好的,正确的做法是一路给PWM,另一路给纯高电平。此时PWM的占空比越低,电机的速度越快。
如果大家使用的是类似于A4950或者DRV8870这样的电机驱动芯片,它们的数据手册中都会有这样的描述
这是DRV8870的,明确说明了PWM加高电平是最佳控制方式。

这是A4950的,用曲线图的方式说明了PWM加高电平时电流会更加稳定。
此外,如果使用PWM加高电平的控制方式,在设置速度为0时,两路信号都会输出纯高电平,此时会触发芯片的刹车模式,使得电机快速停止转动,避免我们制作的小车出现刹不住车的情况。
PID的原理就不赘述了,我们直接看代码。
现在我们已经在定时器中断中完成了电机的速度采样,得到了电机的速度,接下来我们需要进行PID计算,并输出相应占空比的PWM给电机。
但是在此之前,我们需要编写PID的计算函数和进行相关初始化,下面是代码
PID.h部分
#ifndef _PID_H_
#define _PID_H_
#include "stm32f1xx.h"
#include "encoder.h"
#include <stdio.h>
#include "control.h"
//PID三个参数的值
#define KP_speed 2
#define KI_speed 0
#define KD_speed 0
typedef struct _PID//PID参数结构体
{
float kp,ki,kd;
float err,lastErr;
float integral,maxIntegral; //积分值
float output,maxOutput;
}PID;
void PID_Init(void);
float Speed_PID_Realize(PID* pid,float target,float feedback);//一次PID计算
PID.c部分
#include "pid.h"
PID pid_speed;
/**********************************
* 功能:PID结构体参数初始化
* 输入:无
* 返回:无
* *******************************/
void PID_Init(void)//PID参数初始化
{
pid_speed.err = 0;
pid_speed.integral = 0;
pid_speed.maxIntegral = 1000;
pid_speed.maxOutput = __HAL_TIM_GetAutoreload(&PWM_TIM);
pid_speed.lastErr = 0;
pid_speed.output = 0;
pid_speed.kp = KP_speed;
pid_speed.ki = KI_speed;
pid_speed.kd = KD_speed;
}
/****************************************
* 作用:速度环PID计算
* 参数:PID参数结构体地址;目标值;反馈值
* 返回值:无
* ****************************************/
float Speed_PID_Realize(PID* pid,float target,float feedback)//一次PID计算
{
pid->err = target - feedback;
if(pid->err < 0.3 && pid->err > -0.3) pid->err = 0;//pid死区
pid->integral += pid->err;
if(pid->ki * pid->integral < -pid->maxIntegral) pid->integral = -pid->maxIntegral / pid->ki;//积分限幅
else if(pid->ki * pid->integral > pid->maxIntegral) pid->integral = pid->maxIntegral / pid->ki;
if(target == 0) pid->integral = 0; // 刹车时清空i
pid->output = (pid->kp * pid->err) + (pid->ki * pid->integral) + (pid->kd * (pid->err - pid->lastErr));//全量式PID
//输出限幅
if(target >= 0)//正转时
{
if(pid->output < 0) pid->output = 0;
else if(pid->output > pid->maxOutput) pid->output = pid->maxOutput;
}
else if(target < 0)//反转时
{
if(pid->output < -pid->maxOutput) pid->output = -pid->maxOutput;
else if(pid->output > 0) pid->output = 0;
}
pid->lastErr = pid->err;
if(target == 0) pid->output = 0; // 刹车时直接输出0
return pid->output;
}
这里的速度环代码在一般的PID上加了点东西。首先是PID死区,即err值很小时认为err=0,让速度发生很小的抖动时,PID输出不会变化,避免大幅度震荡的产生;其次是当目标值为0的时候让积分部分和输出同时等于0,使刹车更加迅速。
PID_Init()函数需要放在main.c的循环之前,Speed_PID_Realize()函数需要放在定时器中断的电机测速部分后面。
所以现在的定时器中断函数如下
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)//定时器回调函数,用于计算速度和PID计算
{
if(htim->Instance==GAP_TIM.Instance)//间隔定时器中断,是时候计算速度了
{
/**********************************电机测速************************************/
motor1.direct = __HAL_TIM_IS_TIM_COUNTING_DOWN(&ENCODER_TIM);//如果向上计数(正转),返回值为0,否则返回值为1
motor1.totalCount = COUNTERNUM_1 + motor1.overflowNum * RELOADVALUE_1;//一个周期内的总计数值等于目前计数值加上溢出的计数值
if(motor1.lastCount - motor1.totalCount > 19000) // 在计数值溢出时进行防溢出处理
{
motor1.overflowNum++;
motor1.totalCount = COUNTERNUM_1 + motor1.overflowNum * RELOADVALUE_1;//一个周期内的总计数值等于目前计数值加上溢出的计数值
}
else if(motor1.totalCount - motor1.lastCount > 19000) // 在计数值溢出时进行防溢出处理
{
motor1.overflowNum--;
motor1.totalCount = COUNTERNUM_1 + motor1.overflowNum * RELOADVALUE_1;//一个周期内的总计数值等于目前计数值加上溢出的计数值
}
motor1.speed = (float)(motor1.totalCount - motor1.lastCount) / (4 * MOTOR_SPEED_RERATIO * PULSE_PRE_ROUND) * 3000;//算得每秒多少转,除以4是因为4倍频
motor1.speed = Speed_Low_Filter(motor1.speed,speed_Record);
motor1.lastCount = motor1.totalCount; //记录这一次的计数值
/***************************PID速度环**********************************/
motor_Out = Speed_PID_Realize(&pid_speed,Target_Speed,motor1.speed);
//Target_Speed是目标速度,自行定义就好
if(motor_Out >= 0)
{
__HAL_TIM_SetCompare(&MOTOR1_TIM, MOTOR1_CHANNEL_FORWARD, 1000);
__HAL_TIM_SetCompare(&MOTOR1_TIM, MOTOR1_CHANNEL_BACKWARD, 1000-motor_Out);
}
else
{
__HAL_TIM_SetCompare(&MOTOR1_TIM, MOTOR1_CHANNEL_BACKWARD, 1000);
__HAL_TIM_SetCompare(&MOTOR1_TIM, MOTOR1_CHANNEL_FORWARD, 1000+motor_Out);
}
/**********************************************************************/
}
}
如果觉得定时器中断函数看起来很乱,可以将测速和PID分别封装成函数,定时器中断负责调用函数即可。
现在我们就已经能实现电机的速度环控制了,剩下的就是进行PID调参了。
这里需要说明一下,我给电机测速加上了平均滤波,PID参数在滤波和不滤波的情况下会有比较大的区别
电机速度环比较好调,我的调参经验是这样:
为了更好地进行PID调参,我们最好使用能画曲线的串口上位机,这里推荐VOFA+,使用VOFA+进行PID调参可以看这里
速度环调得好的效果应该是这样的:

上图中,绿线是电机的实际速度,红线是电机的目标速度。我们主要检查以下几点:
如果电机到正反最大转速所需时间较短,有一点点超调,没有震荡;到静止时所需时间很短,且没有震荡,那么就说明速度环调好了。
下图展示了速度环调好后电机速度稳定和停下来所需的时间

电机速度稳定需要60ms左右,而停下来只需要20ms左右
当然不同电机不同环境下情况会不一样,我这里只是一个参考。
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h