转自: https://blog.csdn.net/renjiankun/article/details/80513666
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Renjiankun/article/details/80513666
首先感谢以下博客的博主提供的参考公式:https://blog.csdn.net/pengzhihui2012/article/details/52228822?locationNum=6
首先在本设计中采用的步进电机控制方案为,单片机+16位定时器比较匹配中断(最好是16位及其以上)+步进电机驱动+42步进电机。较高的定时器精度能够实现更好的控制。
在步进电机控制中往往需要对步进电机进行加减速控制,以达到平缓启停或者达到较高转速而不失步停转的目的,而在加减速控制中控制方法有两类:
1.查表法;
查表法简单来说就是通过曲线公式预先计算出加速过程的各个点,再将该点转化为定时器的比较匹配值,载入数组中,查询数组值即可达到加减速的目的。优点是运算速度快,占用较少的CPU资源,缺点也很明显。 1.占用较大的存储空间,一般加速的点数都在300-2000点(细分更高的画可能会更高),若想获得更平滑的效果,点数甚至更高,这将会占用大量的单片机内存或者程序存储空间,如果系统支持一般推荐将数组保存在单片机的程序存储空间,以节省宝贵的Ram资源,例如在Arduino uno 中,若直接采样2000点放到数组里内存直接爆满(328的运行内存2K....)!,好在他提供了 PROGMEM 的操作方式,可以将数组保存到程序存储空间。再用 OCR1A = pgm_read_word_near(&AccStep[acc_count]);将数组读出。具体实现方法文后有详细说明。2.更改速度、加速度等不方便,每次更改速度都需要重新生成一次表格,加速度的值更是难以设置,对于我目前的水平是这样的,应该是可以通过算法增大或者缩放加减速表格的,貌似开源3D打印固件Marlin中是这样的。
2.实时生成法;
实时生成法,可能会要求更高的CPU计算能力,比较出名的算法是AVR446:Linear speed control of stepper motor,里面提供了详细的计算以及详细的实现方法,加速过程中实时计算下一个比较匹配值,以实现加减速的实时控制,优点挺多,控制加减速度,速度等参数更加方便,因为可以通过设定参数实时计算出来,缺点就是比较考验单片机的运算能力,但在AVR446提到的算法中也能在运算能力较低的单片机中实现。具体AVR446的实现将在另一个文章中说明。
加速过程实现方法曲线一般有梯型曲线法以及S(Sigmoid)曲线法,其他接触过的还有修正正弦曲线法(用在机械臂的轨迹规划中),梯形曲线法一般通过加速度公式(S = att/2)直接求解,S曲线法则是通过SigMoid函数变形后求解。
本文章主要介绍SigMoid函数用以步进电机的控制方法。
1.基础知识:
步进电机速度计算,做过步进电机控制都知道步进电机的速度跟脉冲频率是直接挂钩的,单片机每发出一个脉冲,步进电机运行一步(转过一个步距角),步距角与步进驱动细分挂钩,例如常用42步进电机步距角参数是1.8°/step,假设通过步进驱动细分后,细分为2,则电机实际每脉冲将运行1.8/2 = 0.9 °。单片机输出脉冲一般通过比较匹配中断的方式使脉冲引脚发出脉冲,则可以计算出单片机发出脉冲的时间间隔为(运行一步的时间) = 比较匹配值 * (1/ 定时器计数频率 ),那这样我们知道了路程(步距角),时间(定时器频率及比较匹配值),就可以计算速度了,但是我们需要将角度换算一下采用弧度制(我在设计的时候采用了 弧度制,且AVR446中采用的也是弧度制,这里是为了统一),rad = π/180×角度,这里我们就能算出1rad ≈ 57.3°,那我们的 步距角 = (π / 180) x (1.8/div) div是步进细分数。设角速度为1rad/s 则 他等于 57.3°/s = (57.3/360)*60/min = 9.55r/min。具有以上知识后就可以将转速(r/min)转换到定时器的比较匹配值了,例如:在我的设计中单片机定时器的计数频率为250Khz、我希望电机运行在300r/min,步进驱动不细分,则有(1.8x(π/180))/(OCR/250000)=300/9.55 所以OCR = (Fcnt * Math.PI) / (100 * Div * (StepSpeed / 9.55))其中Fcnt是定时器计数频率,Div是驱动细分,StepSpeed是转速。,其中,OCR下的实际电机频率为 Fcnt/OCR ,请自行推导一下,强化记忆。
2.SigMoid 曲线
曲线的原型是:
其值域是0~1,因此需要进行变形以便于使用,具体变形请参考文章开头的博客,这里不再赘述,仅做简单描述,可能形式略有不同但它们都是一样的。
关于中心点(a,b)对称的变形公式:
y = 2b / 1+E^(4k(a-x))
参数说明
中心点 (a,b)
过中心点时变化速率 k
y最大值是2b b
改变a可以改变中心点的位置 a
其中需要注意的是,一般步进电机启动不会从0开始,而从某个频率开始启动,所以Y需要加上一个启动频率,正如文章前面博客中的公式一样,请各位自行理解体会里边的变形方法。
本设计采用VB.NET(.NEF FRAMEWORK 4.6.2)实现上位机生成一个加速数组,数组值就为比较匹配值,直接复制就能使用,界面设计如下:
设定好各个参数,点击生成,就能在右边的文本框中显示一个生成的数组,(生成速度受点数影响,技术太渣 还不会解决这个文本框显示过慢的问题),点击导出就能将文本框内容导出为.txt文件,方便使用。
示例,假设步进电机步距角为1.8°,步进驱动的细分为8,启动频率500Hz,计数器频率为250Khz,期望速度为500r/min,加速步数为1000.则有以下运行结果;
VB.NET核心代码实现如下:
Dim img As New Bitmap(500, 500)
Private Sub GeneratAcc_Click(sender As Object, e As EventArgs) Handles GeneratAcc.Click
Dim G = Graphics.FromImage(img)
'Dim G As Graphics = PictureBox1.CreateGraphics '定义picturebox 绘图
Dim Gpen As New Pen(Color.Black, 1) '定义笔参数
Dim cnt As Integer = 0 '循环数
Dim OldX As Int32 = 0 '保存前一次绘图坐标
Dim OldY As Int32 = 500
Dim tmpx As Single 'X轴缩放因子
Dim tmpy As Single 'Y轴缩放因子
dataTextBox1.Text = "" '清零数组显示
Fcnt = Val(TimFreBox.Text) * 1000 '获取定时器计数频率 单位Hz
MinFre = Val(StartFreBox.Text) '最小启动频率
Steps = Val(AccStepBox.Text) '加减速步数
Div = Val(DivBox.Text) '获取驱动细分
StepSpeed = Val(MaxSpeedBox.Text) '获取最大速度
Fle = Val(FleBox.Text) '设置加速区间大小
num = Steps / Val(NumBox.Text) '设置曲线对称系数
MaxFre = (Fcnt * Math.PI) / (100 * Div * (StepSpeed / 9.55)) '求出最大速度时匹配寄存器的值
'MaxFre = (1.8 * Math.PI * Fcnt * StepSpeed) / (180 * 9.55 * Div)
MaxFre = Fcnt / MaxFre '求出设定的最大速度匹配值下的频率
SpeedFre.Text = MaxFre & " Hz"
tmpy = 500 / MaxFre '求出缩放因子
tmpx = 500 / Steps
Dim mydata(Steps) As String
'求解并绘出曲线
For cnt = 0 To Steps
Fre = MinFre + ((MaxFre - MinFre) / (1 + Math.E ^ (-Fle * (cnt - num) / num))) '计算曲线频率数据
mydata(cnt) = Math.Round(Fcnt / Fre) 'Convert.ToInt32(Fcnt / Fre) '转化为OCR匹配值
G.DrawLine(Gpen, OldX, OldY, cnt * tmpx, 500 - (Fre * tmpy)) '画线
OldX = cnt * tmpx '保存前一次绘图坐标
OldY = 500 - (Fre * tmpy)
PictureBox1.Image = img
Next
'保存数组格式
dataTextBox1.Text = "#define ACC_STEP_NUM " & Steps & vbCrLf & "unsigned short AccStep[ACC_STEP_NUM] = {"
'dataTextBox1.Text =
'保存数组格式
ProgressBar1.Maximum = Steps
ProgressBar1.Visible = True
For cnt = 0 To Steps
If (cnt Mod 10) = 0 Then '每10个数据一行
dataTextBox1.Text += vbCrLf
End If
If cnt = Steps Then
'dataTextBox1.Text += mydata(cnt)
dataTextBox1.Text += "};"
Else
dataTextBox1.Text += mydata(cnt) & ","
End If
ProgressBar1.Value = cnt
Next
ProgressBar1.Visible = False
End Sub
在Arduino Uno(ATmega328)上的运行示例:
如同前面提到的,大数组不应该直接放到内存,而是放在程序存储空间中,以免Ram不足,在设计中采用16位定时器1计数频率为250Khz,CTC模式,加减速曲线对称,数组太长不贴。
#define ARR_MAX 3000
const unsigned short AccStep[ARR_MAX] PROGMEM = {*******} ;//生成的加减速数组
#define ACCEL 1 //电机运行状态标志位
#define DECEL 2
#define RUN 3
#define STOP 0
long step_count; //步数计数
int acc_count; //加速计数
int dcc_count; //减速计数
uint8_t flag = STOP;//开始时的状态
void SetSteps(long steps); //设定电机步数及方向 正值代表正传 负值代表反转
void StepRun(void); //启动电机
void SetSteps(long steps)
{
if(steps<0)
{
digitalWrite(2,0); //设定电机运行方向
step_count = -steps; //赋值步数
}
else
{
digitalWrite(2,1);
step_count = steps;
}
}
void StepRun(void)
{
TCCR1B = (1<<WGM12)|(0<<CS12)|(1<<CS11)|(1<<CS10); //16M / 250kHZ
OCR1A = 15; //随便开始一次中断
flag = ACCEL; //进入加速状态
}
/*定时器1 初始化*/
void Timer1Init(void)
{
TCCR1A = 0;
TCCR1B &= ~((1<<WGM12)|(0<<CS12)|(1<<CS11)|(1<<CS10));
TIMSK1 = (1<<OCIE1A);
OCR1A = 15;
sei();
}
/*初始化*/
void setup() {
// put your setup code here, to run once:
pinMode(2,OUTPUT);
pinMode(3,OUTPUT);
Timer1Init();
}
/*主循环*/
void loop() {
// put your main code here, to run repeatedly:
SetSteps(-16000);
StepRun();
delay(5000);
}
/*OCR1A 比较匹配中断 CTC 模式*/
ISR(TIMER1_COMPA_vect)
{
switch(flag) //查询状态
{
case STOP :{
TCCR1B &= ~((1<<WGM12)|(0<<CS12)|(1<<CS11)|(1<<CS10));
acc_count =0;
step_count = 0;
dcc_count = 0;
}break;
case ACCEL :{
acc_count++;
step_count--;
digitalWrite(3,1); //输出一个脉冲
digitalWrite(3,0);
OCR1A = pgm_read_word_near(&AccStep[acc_count]);//查表
if(acc_count == ARR_MAX-1) flag = RUN;
}break;
case RUN :{
step_count--;
digitalWrite(3,1);
digitalWrite(3,0);
if(step_count == ARR_MAX-1)
{
flag = DECEL;
dcc_count = ARR_MAX-1;
}
}break;
case DECEL :{
dcc_count--;
step_count--;
digitalWrite(3,1);
digitalWrite(3,0);
OCR1A = pgm_read_word_near(&AccStep[dcc_count]);
if(acc_count == 0 || step_count==0) flag = STOP;
}break;
}
}
编译结果:
可以看到,数组已经不占用可怜的Ram了。
这个算法生产的加减速表用起来还可以,就是中间加速过程有点太猛了,有点像直接甩上去一样,可能是我设置的速度太高了,加速步数太短,在低速时,运行状态还算不错。
学无止境,到最后怎么运用到各个项目中,还的需要大量的实践。
对步进电机控制,机械臂正逆解感兴趣的朋友可以留言一起交流,搞点事情什么的。
每错!这是我第一篇博客,记录一下学到的一些知识。
2018.7.28 更新:需要注意的是,这里仅仅计算出了每个离散加速点的频率,实际使用中不可能每个频率点只发出一个脉冲,否则就像没有加速一样,解决的办法就是根据实际情况来调整每个频点发送的脉冲数,低频到高频脉冲数最好相应的也由少变多,这样可以改善加速效果。感谢各位评论让我发现不足的地方!
2018.8.06 更新: 看到回复中许多人对数组的使用方式存在疑问,并且之前我个人也存在误区认为每个频段只发 一个脉冲,实际上细想下来并不科学,例如我细分很大,5000,1.8度步距角,那么我转1.8度需要5000步,我加速点2000步 那加速 路程极短, 加速时间极短,那显然不符合逻辑, 查阅 一些资料发现,在加速时应该在每一个频段停留一段时间,或者说,每个频段多发一些脉冲,低频时脉冲少,高频时脉冲多,这里给大家提供几个思路,用来解决每个频段应该发多少脉冲的问题。首先我们根据以下数据生成曲线:步距角1.8°,细分8,定时器计数频率250K,启动频率600HZ ,最大转速500r/min,加速步数为600步。
第一种方法:将600个频段划分为10个部分,这10个部分的频段脉冲数分别为{5,5,10,10,20,20,30,30,40,40},这样每读取 一个频段就按该频段的步数发脉冲,当然这是一个想法、思路。需要注意的是,如果启动频率过低,或者低频时发出的脉冲太多将会影响加速时间,加速效果不好,所以应当合理选择启动频率和合理设计低频时的脉冲数。
第二种方法:这种方法是一种实时计算的方法,我目前正在测试,效果感觉良好,思路很简单,我添加一个加速时间的条件,然后将时间按自己需求离散为这个频段时间间隔,根据这个时间间隔以及频率可以轻易计算出这个频段的步数,比如我目前加速步数是600步,那么将加速时间设置为600ms,显然为了满足600ms的加速时间我可以简单暴力的将每个频段的持续时间划分为1ms,由已知条件计数频率以及当前频段的比较匹配值即可计算,按照以下公式即可,注意时间单位的换算。
那么根据我的已知条件,我的每个频段的脉冲为 = 250.0f / (float)OCR。具体看以下加速度段程序实现。
case ACCEL :{
if(cstep==0)
{
cccv = 250.0f / (float)AccStep[acc_count];
cstep = (cccv <= 1) ? 1 : ceil(cccv);
OCR1A = AccStep[acc_count];
acc_count++;
if(acc_count==ARR_MAX-1) flag = RUN;
}
else
{
digitalWrite(3,1);
step_count--;
cstep--;
digitalWrite(3,0);
}
}break;
舍入函数看个人需求,ceil 或者floor 或者四舍五入,此算法解决频段脉冲数问题,并且需要给定加速时间,限制加速时间,算法还有优化空间,欢迎讨论,具体思路是高频段时间停留更久(目前是每个频段停留1ms,当然计算是有误差的,毕竟只能算出整数,控制加速时间在设定值附近吧),另外一个主意的问题是,在数组生成的最后几组中可能最高转速也包含在里边,根据需要可以适当修改数组,减少不必要的加速段。
20190125更新:实际上在中断里引入浮点运算是不可取的,在几个月前我已经找到了更好的计算每个频段应该发多少个脉冲的算法,只包含一个加法和比较,有时间再更新上来吧。
软件:https://download.csdn.net/download/renjiankun/10452200
测试视频:https://v.youku.com/v_show/id_XMzc2NzkyODg0OA==.html?spm=a2h3j.8428770.3416059.1
————————————————
版权声明:本文为CSDN博主「Renjiankun」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/renjiankun/article/details/80513666