一、背景
前面已完成了硬件激活、编程环境选择、软件框架构建,并且基于软件框架尝试了基于 RTOS 编程,为在此软硬件环境下学习单片机编程做了充分的准备。
从本篇开始,开始以其为载体,一步步完善其功能、性能,并基于其具备的能力去完成一些有趣的任务,在这个过程中,逐步掌握用单片机解决实际问题的技能。
掌上单片机实验室实质是一个小车,小车可以衍生出丰富的学习素材。
首先,让其按照需要运动就涉及到很多内容,如:直流电机驱动、运动反馈、PID调速、堵转保护等,以及两轮差分驱动方式下的直线行走,需要用到很多单片机知识。
在这些完成后,还可以基于其可动性,去设计、完成一些有趣的任务,如按特定轨迹行走、避障、“搬运”物体等,这些又将蕴含很多学习素材,可以外延到更多的知识范畴。
可以说,小车是一道有丰富发挥空间的单片机学习应用题。
既然是小车,第一步就要使其按需运动,本篇就着手实现此功能。
二、需求
因小车是作为学习平台,考虑到学习成本,使用的是最便宜的130直流电机,性能较低,用 PWM 调功方式很难得到期望的转速,阻力稍有变化将会改变转速。为使小车能按需要行走,控制车轮转速是必须的。
而控制转速首先需要测速!同样由于成本原因,小车码盘是在轮毂上直接做的,只有60齿,分辨率太低,尤其是用于PID调速时,计算周期如果设计为20ms,最快时才几个脉冲,无法满足计算需求,因此需要通过算法提高测速分辨率。
综合而言,驱动小车运动的需求如下:
1、可以方便的驱动电机
2、可实现满足调速要求的测速
3、可控制车轮转速
4、可控制车轮行走距离
三、设计
3.1 驱动任务构思
前面虽然实现了电机的驱动,能让小车动起来,但那个只是为了硬件测试,功能设计比较简单。
此处要实现的驱动是为了满足后续小车的行为可控,能根据需要完成相应的轨迹运动,速度、距离可控,能完善、可靠的执行相应的操作命令,且易于和其它任务交互,协同实现期望的功能。
按面向对象的思路,拟将两个车轮的驱动作为对象,将小车运动的相关功能均纳入,以其为基础构建一个任务:驱动任务。
驱动任务应包含:
1)电机驱动
2)测速
3)调速
4)距离控制
5)左、右轮配合实现运动轨迹控制
6)电机驱动保护
7)当前电机工作状态反馈
上述功能的罗列顺序是基于相互依存关系的,按上述顺序实现各个功能,驱动任务应能满足需求。
因为要驱动两个电机,功能一样,故考虑使用类方式实现电机驱动相关功能,以简化编程。
3.2 电机驱动
电机驱动在前面做测试任务时已经完成,此处从实际使用角度再优化一下即可。
电机驱动电路支持电机的4个工作状态:前进、后退、惰行、刹车。
前进、后退不难理解,惰行和刹车需要说明一下:
惰行 —— 是指在停止 PWM 信 后,电机驱动H桥使电机线圈处于开路状态,电机转子会由于惯性,继续转动到机械阻力使其停止。
刹车 —— 是指在停止 PWM 信 后,电机驱动H桥使电机线圈处于短路状态,电机转子由于惯性转动产生的感应电势形成电流,产生阻力,和机械阻力共同作用使其停止。
在精确控制行走距离时,应运用刹车功能,降低惯性产生的误差。
平时待命状态,可以使其处于惰行状态,可以轻松的用手转动轮子。
测试任务中设计的电机驱动函数,使用PWM百分比作为驱动信 ,范围为 -100 ~ 100,负数对应后退,正对应前进,0 对应停止。
补充定义:
127 —— 对应刹车
0 —— 对应惰行
此外,增加对PWM频率的初始化,因为STM32Duino支持改变PWM频率,不过是哪些IO有效,有待测试确定。
同时,STM32Duino的 PWM 输出在内部要和 PWM 引脚对应的硬件定时器关联,所以在选择两个电机的 PWM 引脚时,需要考虑用哪个定时器,最好使用一个定时器的两个通道,这样可以少占用硬件定时器。
根据STM32F103C8 资料,目前电机驱动用引脚需要调整为:
左侧 CT1、2、3 使用 PB6、PB5、PB4;右侧不变。
这样PWM驱动使用 PB6、PB7, 对应 TIM4 的CH1、CH2。原来的驱动函数略作修改即可。
3.3 测速
测速是调速的基础,通常使用合适分辨率的编码器即可方便实现。但此小车在设计时,主要考虑到降低成本,以减轻学生负担,故只是在轮毂上设计了遮光齿,用光电检测实现简易的转动反馈。
目前的一圈是 60 齿,如果测速周期为10 ~ 20ms,对应最高转速(电机空载转速9800转/分,减速比1:48)3.4转/秒,每个测速周期才计数 2 ~ 4 个脉冲;如果用计数方式测速,明显速度分辨率太低。而周期方式测速由于周期随速度变化,不方便作为调速信息,故一般不采用。
因此,如果要使用此信 作为调速的反馈信 ,实现PID转速控制,必须采用倍频技术,将脉冲分辨率至少提高100倍。
基于正常转动的特征:相邻两个脉冲的周期不会突变(排除干扰造成的脉冲丢失、抖动等异常状况)。
拟用计数和测周期两种方式组合,实现倍频,以提高分辨率。具体处理方式为:
在测速周期内,一方面对脉冲计数,得到完整脉冲的计数值。同时测量每个脉冲的周期,保存上一个脉冲的周期值(也可以保留前几个脉冲的平均值,以提高可靠性),为计算非完整脉冲做准备。
当测速周期到时,读取当前脉冲周期测量“已计量的时间“,除保存的“前一脉冲周期值”,即可得到非完整脉冲值 0. XX ,从而提高分辨率。
图示如下:
注意:下一测速周期开始后的第一个脉冲计数不应该加1,而是加(1- 0. XX)。
用此方法理论上可以提高到很高分辨率,但由于转动部分的阻力不均匀,导致脉冲周期会有波动,所以过高倍频没有实际意义,尤其是在速度变化过程中。在速度稳定时可以适当提高倍数,以提高控制的精度。
在编程处理中,要避免非完整脉冲计算时产生大于 1 的结果。
为减少浮点运算,简化算法,将脉冲计数单位扩大,每个完整脉冲计数值为256,相当于256倍频,这样用左移一个字节即可实现完整脉冲数的变换。
在目前程序框架中,转动脉冲计数仍用中断完成,脉冲周期测量尝试直接在中断中读取当前系统计时值计算得到,利用Arduino 的 micros()函数实现,取代通常读取硬件计时器,尝试一下,看看是否可行,这样可以弱化程序对硬件的依赖。
测速周期通过设计为 5ms 的倍数,故非完整脉冲的测量在 Tick 中断中实现,同样利用 micros()函数。使用中注意 micro()返回值溢出处理,按 4 字节应该约 71 分钟溢出。
注意:因为Tick从 1ms 修改为 5ms,Arduino 的系统时间均因此而改变,micros()、millis() 函数单位因此扩大了 5 倍,变为 5us、5ms,delay() 函数也一样。
3.4 调速
调速通常使用 PID 算法, Arduino 资源中有很多分享的库,此处尝试 PID_V2,这是在原来Arduino推荐的 PID 库基础上完善的库。
3.5 距离控制
这个比较简单,基于轮毂上的码盘即可,但由于码盘精度较低,故距离控制精度也不高,大约是3.5mm(轮子直径65mm,一圈脉冲60个)。
运动轨迹等功能后一步再实施,这一步先完成这些。
为了能操作上述功能,必须有相应的控制协议,才能通过串口命令操作。
3.6 控制命令定义
取消原来的测试命令,改为:
1) 命令3:读工作状态命令(Key = 3,Len = 0)
0x03 0x00 0x00
应答内容:
0x03 0x0E 0x00 左侧电机工作参数(2字节) 右侧电机工作参数(2字节)左侧剩余运行参数(2字节)右侧剩余运行参数(2字节) 左侧供电电压(1字节)右侧供电电压(1字节)左侧供电电流(2字节)右侧供电电流(2字节)
2)命令4:PWM方式定时运行(Key = 4,Len = 8,Val:2字节左侧 PWM,2字节右侧 PWM,2字节左侧时间,2字节右侧时间)
0x04 0x08 0x00 左侧电机PWM(2字节) 右侧电机PWM(2字节)2字节左侧时间 2字节右侧时间
PWM 为 2 字节有符 数,-100 ~ 100,正数前进,负数倒退,0 – 惰行,127 – 刹车,-128 – 无效PWM,不操作
时间单位:秒
应答内容:
0x04 0x0E 0x00 左侧电机PWM(2字节) 右侧电机PWM(2字节)左侧剩余运行时间(2字节)右侧剩余运行时间(2字节) 左侧供电电压(1字节)右侧供电电压(1字节)左侧供电电流(2字节)右侧供电电流(2字节)
3)命令5:PWM方式定距离运行(Key = 5,Len = 8,Val:2字节左侧 PWM,2字节右侧 PWM,2字节左侧距离,2字节右侧距离)
0x05 0x08 0x00 左侧电机PWM(2字节) 右侧电机PWM(2字节)2字节左侧距离 2字节右侧距离
PWM 为 2 字节有符 数,-100 ~ 100,正数前进,负数倒退,0 – 惰行,127 – 刹车,-128 – 无效PWM,不操作
距离单位:mm
应答内容:
0x05 0x0E 0x00 左侧电机PWM(2字节) 右侧电机PWM(2字节)左侧剩余运行时间(2字节)右侧剩余运行时间(2字节) 左侧供电电压(1字节)右侧供电电压(1字节)左侧供电电流(2字节)右侧供电电流(2字节)
4)命令6:速度方式定时运行(Key = 6,Len = 8,Val:2字节左侧速度,2字节右侧速度,2字节左侧时间,2字节右侧时间)
0x06 0x08 0x00 左侧电机速度(2字节) 右侧电机速度(2字节)2字节左侧时间 2字节右侧时间
速度为 2 字节有符 数,单位:mm/s,正数前进,负数倒退,0 – 惰行,32767 – 刹车,-32768 – 无效速度,不操作
时间单位:秒
应答内容:
0x06 0x0E 0x00 左侧电机速度(2字节) 右侧电机速度(2字节)左侧剩余运行时间(2字节)右侧剩余运行时间(2字节) 左侧供电电压(1字节)右侧供电电压(1字节)左侧供电电流(2字节)右侧供电电流(2字节)
5)命令7:速度方式定距离运行(Key = 7,Len = 8,Val:2字节左侧速度,2字节右侧速度,2字节左侧距离,2字节右侧时间距离)
0x07 0x08 0x00 左侧电机速度(2字节) 右侧电机速度(2字节)2字节左侧距离 2字节右侧距离
速度为 2 字节有符 数,单位:mm/s,正数前进,负数倒退,0 – 惰行,32767 – 刹车,-32768 – 无效速度,不操作
距离单位:mm
应答内容:
0x07 0x0E 0x00 左侧电机速度(2字节) 右侧电机速度(2字节)左侧剩余运行距离(2字节)右侧剩余运行距离(2字节) 左侧供电电压(1字节)右侧供电电压(1字节)左侧供电电流(2字节)右侧供电电流(2字节)
四、实施
两侧电机的驱动是一样的,利用 Arduino 支持 C++ 的特性,定义一个电机驱动类,将电机驱动相关的操作封装在一起。
因 PID 调速使用的是库,它也是定义为类,故未纳入到自己定义的电机驱动类中。
驱动类定义如下:
使用时实例化为左右两个电机驱动即可:
驱动类需要完成:
1)驱动电机:输入带符 的PWM,转换为电机驱动的信 。
2)测速,包含两个部分,脉冲计数和转速计算:
脉冲中断服务中调用脉冲计数函数,一是完成测速周期中的脉冲计数,二是测得脉冲周期:
中断函数暂时放置在任务中,因为还有一些任务级的操作,如距离控制。
转速计算是基于测速周期中的脉冲计数,以及脉冲周期,通过上述倍频算法得到转速值,此处是256倍频,是为了简化计算,用左移8位代替了乘法:
3)为了实现堵转保护,先实现电机供电电压和电流的检测:
用类的方式编写,感觉比函数逻辑更清晰。
做上述修改后,任务间的信息交互重新定义:
上述基础工作完成后,编写驱动任务。
驱动任务框架和原来的测试任务类似,主要有三部分:
1)接收主任务发来的消息,并根据操作命令启动相应的操作
2)每个Tick 唤醒后执行相应的处理,在此包含:速度计算、电机电压电流采集、当前操作命令的执行(如定时运转的计时处理,以及PID调速处理)
3)定时将驱动任务的工作状态发送给主任务。
PID 调速使用第三方分享的库,实际上只是在定速运行状态下,每次将当前速度送给PID计算函数处理,得到后续电机驱动的 PWM 值。其余和正常运行没有区别,其核心是用于计算的三个系数。
这一步只是将PID调速功能加入的到电机驱动过程中,但还未实现真正的调速,因为三个参数需要根据小车的实际特性调整,这个过程有相当的技巧。目前随意设置了几个数值,所以在定速运行时,转速呈震荡状态,下一步实现参数的整定(确定PID参数的过程称之为“整定”)。
同前,单片机端程序修改后,配套的PC端辅助程序也需要相应修改。程序逻辑不变,只是根据修改后的协议,调整相应的参数处理及数据显示即可,修改后的PC程序界面如下:
至此,以实用为目标的电机驱动任务基本完成,还差PID调速的完善以及堵转保护。
PID 参数整定以往我是做了一个速度输出,并且 PC 端对应做了谱图功能,根据图形手工计算确定相应的参数。
这次我想尝试一下“自整定”,Arduino 提供了相应的库,看看效果如何。下一步的练习就是它了。
五、结语
因为这是学习编程,其核心是如何将现实世界的需求转化为程序代码;而构建代码的完整过程很难描述,最佳方式就是一步步地呈现代码的变化,通过前后比较,逐步悟出代码是如何从无到有的。
基于程序框架编写使得这个变化过程更清晰。
这一步重新编写测试任务,将其变为驱动任务;主任务只做了很少的改变,其余任务未变。
对于学习者而言,这样就很容易比较程序前后的变化。
这既是一个分享的过程,也是自身提高的过程;因为我是非科班出身,编程技巧是在工作中不断学习、摸索的,是随着技术的进步而提升的。在后续过程中,我会不断尝试新的方式,不一定都正确、合理,但可以供大家参考;一种方法是否好,至少要有人去尝试,就算是抛砖引玉吧。
——————————
文中程序下载:
链接:https://pan.baidu.com/s/1Mkh3xlxebdqSBJkT3ilzBA
提取码:xw82
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!