电子说
一、背景
前些年,为了给学习单片机编程的学生提供一个方便使用的控制素材,我设计了一个轮式驱动单元,其最大特点就是将电机驱动和码盘反馈集成到一起,用几个TTL电平线就可以驱动,和舵机的驱动类似,这样使用时可以根据喜好、需要随意选择核心板驱动
可以用它构建不同驱动方式的小车底盘,最简单的一种驱动方式就是使用一个轮式驱动单元加一个舵机实现的“单轮驱动舵机转向小车”,它的运动方式和现实世界中的电动叉车类似
因这种驱动方式小车的转向和行走两个主要控制元素相互独立,从学习编程角度考虑,相对于两轮差分驱动方式更为简单、容易。
当时目标是降低学习门槛,故选了Arduino作为控制器。为便于接线,选择了市场上的一种 Nano 扩展板,其外形和UNO一样,只是将每个IO都配了一个地线和电源端,用杜邦线连接十分方便
后来觉得Arduino Nano控制器资源太少,考虑改用主流的 STM32F103C8T6核心板,但Nano扩展板的构思不错,边参考它自己设计了一块 STM32F103C8核心板的扩展板
由于大环境影响,国产OS被重视,在RTOS领域,RT-Thread无疑是国产系统中的佼佼者。故萌生了在小车平台上尝试一下的念头,既然有可以方便使用的STM32F103C8扩展板,又有和STM32F103C8核心板兼容的STM32F411CE核心板,小车的控制也应该提升一下,跑跑我们自己的RTOS。
下面将完整地记录实施过程,期望能对想选用 RT-Thread 开发产品的朋友有帮助,因为小车控制相对实时性要求较高,有真实的多任务需求,不同于HMI(人机界面)类应用,用事件驱动即可,没有那么强的实时性、并发性要求。
二、目标
按实时多任务的思路,基于 RT-Thread,完成小车驱动,并可以通过串口(蓝牙透传)操纵小车运动。
因为RT-Thread 的特色是有丰富的组件和软件包可以扩展,但要享用这些,必须是标准版;如果用Nano版,和FreeRTOS区别不大。故选择RT-Thread标准版。
三、实施过程
3.1 总体构思及任务设计
我做项目通常是先构思程序框架,将所需完成的功能合理拆解,根据功能确定框架。
选用成熟的RTOS,只是有了多任务实现的手段和工具,程序框架还是需要自己设计。
对于基于RTOS的程序而言,首先要设计的就是任务(在RT-Thread中为线程),根据RTOS所提供的工具,结合自己想要实现的功能,合理划分任务,创建相应的线程,并确定线程之间的交互方式及内容,从而完成一个基于RTOS的多任务控制程序。
任务划分的首要目标是相对独立,即所构建的任务是完整的,有清晰的输入消息和输出结果,其工作只是对输入进行相应处理,给出输出,中间过程不受其它因素所牵制。
其次是任务可一次完成,中间没有长时间的等待操作,任务通常是处在等待输入的状态,收到输入后,即刻完成对输入的处理,输出结果,之后再次回到等待输入状态。
这样设计主要是因为:所谓多任务操作,表面上看每个任务都在连续执行,实质上MCU还是分时处理各个任务,只是通过RTOS在后台切换。
RTOS调度的方式决定了各个任务的分配时间和响应速度,各家RTOS的调度方式虽有不同,但都遵循一个原则:即处于等待状态的任务不分配运行时间,这样才能使程序达到最佳效率。
使用RTOS的等候消息函数,就是告知OS,我目前“没事”,在等新的消息。
基于这种方式构建任务除了保证程序的执行效率和实时性外,还便于调试。可人为注入消息触发任务执行,观察输出结果是否符合设计即可。而且可以用多模块方式,一个任务一个模块,由多人分别编写、合作完成。
基于RTOS编程,考虑到其多任务的特点,通常是先做一个基础框架,再根据不同的场景设计相应的任务。
所谓“基础框架”,就是结合单片机应用的特点,将一些通用需求纳入,这样在每次做新的项目时以此为基础,增加新的功能即可。
基础框架包含:
1)串口命令接收:作为人机交互通道,代替以往通过按键实现的程序操控手段。操作命令的产生可以用PC、手机之类有丰富交互手段的设备,比实体按键更为灵活、直观、丰富。
2)串口数据发送:作为人机交互通道,代替以往通过显示器实现的信息输出,同样可以用PC、手机之类的设备接收后显示,界面设计远比显示屏灵活、丰富、随心所欲。之所以将收、发分开,是考虑到输出信息需要服务于所有任务,以便功能设计更为合理。
3)调试信息输出任务:在没有或不能使用IDE调试手段时,需要在程序中输出相应的调试信息,以实现 Debug ;不过,RT-Thread 内置Finsh 十分完善,故选用RT-Thread则不再需要设计此任务,这是 RT-Thread 优点之一。
4)看护任务:利用RTOS的信息交互机制,周期性地与各任务交换信息,当出现不应答时,说明对应的任务工作异常,可以做相应的操作。这样处理无需复位,导致其它任务被非正常中断,增加了程序的可靠性。即便不处理,也能利用调试信息输出及时发现是哪个任务异常,以便消除隐患。
5)主应用任务:前面几个任务属于框架的基础任务,未涉及程序需要执行的实际功能,主应用任务就是实现具体功能的核心。设计主应用任务是考虑到一般来说,程序均有一个核心的部分,用于管理、协调一些子功能,使得程序运行有序;它相当于一个管理者。这个在抽象的框架设计时只完成了信息交互,在具体到特定的需求时再完善设计。
6)其它任务:相当于执行者,完成特定的功能。同上,这个任务在框架设计时只是完成和主应用任务的信息交互,具体实施时再详细设计。
在本项目实施中,需要完成的功能如下:
1、接受串口操作命令,解析并执行。具体而言就是2个命令:按指定速度前进或后退指定距离、转向角度。
2、电机驱动,实现调速和走指定距离。
3、舵机驱动,完成指定转向角度。
据此,考虑设置两个执行任务:电机驱动、舵机驱动,以及一个主应用任务,负责接受、解析操作命令,将相应操作信息发送给相应的执行任务。
舵机的操作方式按道理无需单设为一个任务,因为它自身闭环,无需程序去处理反馈和修正。但考虑到逻辑上的独立性,以及未来用多个舵机和轮式驱动单元构成的全向小车驱动,舵机转向构建为一个独立的任务有助于程序的可维护性和可扩展性。
构建如下应用任务:
1)主应用任务:解析所设计的操作命令,将命令参数提取后发送给相应的执行任务,并应答。
2)电机驱动:接收执行参数实现PWM电机驱动,并根据码盘反馈实现调速和行走距离控制,反馈当前运行状态给主应用任务。
3)舵机驱动:接收执行参数,执行舵机操作;基于舵机特性,反馈舵机当前状态(正在运行、已到位)。
3.2 任务(线程)间交互设计
一个任务常常需要接收多个消息,而且是来自不同的任务;为了实现将这些消息通过一个等待函数获取,RTOS提供了一个方便的手段:事件组;即将若干消息汇总在一起,一个消息对事件的一位;而且等待的方式也很灵活,可以是“与”的关系,即所关注的各个消息都出现才触发;也可以是“或”的关系,即出现任意一个消息就触发。
事件组虽然解决了等待多个消息的问题,但所传递的内容往往不够,一个消息有时包含许多参数,如通讯命令的内容。
为传递消息内容,选择了RTOS的另一个工具:邮箱。
邮箱只传送一个字(4字节),很多时候也不够,我习惯于用邮箱传递存放数据的指针,将要传送的消息定义为一个数据结构,通过指针传送,这样比较灵活。
曾经用过队列传输,但需要预先确定队列项长度,很难一次规划到位,后期修改比较麻烦,故改用邮箱。
通过事件组和邮箱的配合,基本上可以实现任务间的消息交互。
3.3 任务(线程)详细设计
3.3.1 串口接收任务
串口是作为单片机的操作输入通道,取代传统模式下的按键操作。通过串口命令操作单片机远比设计实体按键灵活方便,而且硬件资源占用也少。
此任务要实现对串口命令的接收和初步解析,将命令内容转发给主应用任务处理。
核心是能可靠的监测和接收符合通讯协议的命令帧。
通讯协议的定义也是串口命令的重点,要考虑的简洁性和可扩展性的平衡,同时要兼顾应用场景的需求。
目前协议是参考 ROS (机器人操作系统)中的ROS Serial 协议设计的。因为小车有无线通讯的需求,故在协议中有相应的通讯地址,以便在一个通道上实现多机通讯。
串口通讯协议如下:
字符格式: 115200 8 N 1
帧格式:(借鉴 ROS 的 ROS Serial 协议)
0xFF 0xFE(2字节帧同步字) 帧长L 帧长H 帧长校验和 目标地址 源地址 帧数据区 帧校验和
其中:
帧同步字 —— 2字节特征字,暂定为0xFF 0xFE,借鉴的ROS Serial协议。
帧长 —— 帧数据区数据字节数,不含帧校验和、目标地址和源地址;先低后高,最大支持65535字节,实际不一定需要,但有可能超过256字节,所以用2字节。
帧长校验和 —— 2字节帧长算数和取反,取最低字节。借鉴ROS Serial协议
目标地址 —— 接收方的通讯地址, 1字节。
源地址 —— 发送此帧的通讯地址,1字节,应答时使用。
帧数据区 —— 通讯数据,字节数为帧长
帧校验和 —— 数据区所有字节的算数和取反,取最低字节。如果数据长度为0,则CS为0xFF。
按此设计,最短的帧为 8字节,数据区无数据,可作为心跳帧或命令应答帧。
帧的传输方向由两个地址确定,无需再设计上、下行(应答帧)标志。
帧数据区定义如下:
Key: 操作命令,1字节
Len: 数据长度,2字节,先低后高,单位 - 字节
Val[Len] :N 字节数据
至于 Val的数据如何定义,取决于 Key,可以定义为结构、数据、或者更复杂的数据,也可以简单的定义为字节、字、整形。
串口数据帧的可靠接收源于可靠的帧提取方式,因为有可能使用无线通讯(串口接无线透传模块即可),就存在串口接收到的数据并非都是有效的、应该收的,需要从接收的数据流中检出发给自己的数据帧,不能根据数据绝对位置提取。
对于从连续数据流中检出一段符合要求的数据,使用滑窗比较方式较为可靠。
由于上述协议定义的是变长帧,无法对整个数据帧进行滑窗比较,只能基于协议,找到帧头的特征后,再对整个数据帧接收,之后再通过校验判断此帧是否正确。
此处所用的帧头特征为:同步字、帧长格式(2字节+校验)、目标地址。
除数据帧的可靠接收外,在串口命令接收任务中,还设计了两个操作命令:
读内存、写内存
目的是为了在小车运行过程中检测特定变量的值,从而发现程序出现的问题,类似于在IDE环境下设置断点,停下程序后检查内存变量值。
这种方式可以在程序运行中实现,不影响程序运行,更为真实。
将需要监测的变量设计为全局变量或静态变量,在编译产生的 map 文件中可以查到相应的地址,通过读内存操作可随时观察变量的变化。
由于STM32的内存是线性的,其RAM、ROM、硬件工作寄存器均在一个地址空间,因此还可以通过读内存功能监测MCU相应硬件的工作状态(读取相应寄存器值),以确定初始化是否正常,运行是否正确。
这个功能作为调试信息输出的补充,可以使调试手段更加丰富。调试信息输出需要预先嵌入一段代码,而读内存操作可以随时使用,只要对象不是动态变量。
写内存功能也是作为调试手段的补充,可以通过串口命令修改程序中的相应变量,从而激励程序执行所需的操作。
读、写命令定义如下:
命令1(0x01):读内存操作
0x01 0x05 0x00 数据地址(4byte,L-H)读字节数(1byte)
读取从数据地址开始的N字节数据,用于调试及故障远程诊断。
应答内容:
0x01 lenL lenH 数据地址(4byte,L-H)读字节数(1byte)数据(N字节)
数据长度len为 读字节数+5
命令2(0x02):写内存数据
0x02 lenL lenH 数据地址(4byte,L-H)写字节数(1byte)数据(N字节)
从数据地址开始写 N 字节数据,用于调试,及临时性的参数设置,需要保护,以免引起程序崩溃。
应答内容:
0x02 0x05 0x00 数据地址(4byte,L-H)实际写字节数(1byte)
如果写失败(所写地址不在允许范围内容,或长度超过设定),则实际写字节数为 0。
3.3.2串口发送任务
在传统的单片机系统中,通常会设计显示屏、至少是LED数码管作为信息输出手段;但目前多数单片机系统已不需要这样设计,通过串口输出信息,使用PC、手机这类显示功能完善的设备作为单片机系统信息输出的呈现手段,比LED数码管、LCD屏更为直观、灵活、美观,而且占用硬件资源极少。
因程序框架是多任务方式,理论上各个任务都有输出信息的需求,故将串口发送功能独立设计为一个任务,可以服务于所有任务。
为减少内存消耗,发送数据传递消息只传输存放指针,发送任务根据数据结构定义,取出要发送的数据。串口发送速度和内存操作相比慢很多,要发送的数据放置在各自任务中,在每次需要发送前,需要确定上次数据是否取走,以避免数据覆盖,导致发送数据错误。
3.3.3看护任务
看护任务的设计目前只是示意性的,没有实质的恢复处理。因为恢复处理需要根据具体功能确定,没有统一的方法。
但编好一个处理框架,后续如果需要增加相应的处理会方便一些。
作为看护任务,除了通过和各任务交互,以确定任务是否在正常运行外,还顺带完成了运行指示功能。
多数单片机系统虽无需显示器,但工作状态指示通常都有,可直观的反映系统是否在正常运行,一般是通过LED的闪烁变化呈现。
此处参考FreeRTOS的异常指示方式设计了LED显示功能,正常时,LED等间隔闪烁,当发现某个任务异常时,按任务顺序会出现间断闪烁。具体方式为:
将一个完整的显示周期定为10次闪烁,正常时一个周期闪烁10次。
如果是1号任务异常,则一个周期闪烁1次,其余9次对应暗状态;如果是2号,一个周期闪2次、暗8次,以此类推,最多可以支持9个任务的异常指示。
3.3.4 主应用任务
此处主应用任务(后面简称:主任务)完成:
1)解析串口接收任务发来的操作命令,根据命令将参数发给电机驱动和舵机驱动任务。
2)定时读取电机和舵机工作状态,以便反馈给操作者。
根据前述设计目标,定义操作命令如下:
A) 命令3:读工作状态命令(Key = 3,Len = 0)
0x03 0x00 0x00
应答内容:
0x03 0x09 0x00 电机工作参数(2字节)电机运行状态(2字节)电机供电电压(1字节)电机供电电流(2字节)舵机操作角度(1字节)舵机当前状态(1字节)
电机工作参数:PWM值或速度值
电机运行状态:剩余运行时间或距离
舵机操作角度:-90 ~ +90
舵机当前状态:运行、到位两个状态
B) 命令4:PWM方式定时运行(Key = 4,Len = 5,Val:2字节电机 PWM,2字节运行时间,1字节舵机操作角度)
0x04 0x05 0x00 电机PWM(2字节) 2字节运行时间 1字节舵机角度
电机PWM:为 2 字节有符号数,-100% ~ 100%,正数前进,负数倒退,0 - 惰行,127 - 刹车,-128 - 无效PWM,不操作
运行时间:单位:秒
舵机角度:1字节有符号数。-90 ~ +90
应答内容:(同读状态命令)
C)命令5:PWM方式定距离运行(Key = 5,Len = 5,Val:2字节电机 PWM, 2字节运行距离,1字节舵机角度)
0x05 0x05 0x00 电机PWM(2字节) 运行距离(2字节) 舵机角度(1字节)
电机PWM: (同上)
运行距离:2字节无符号数,单位mm
舵机角度:同上
应答内容:(同读状态命令)
D)命令6:速度方式定时运行(Key = 6,Len = 5,Val:2字节运行速度, 2字节运行时间,1字节舵机角度)
0x06 0x05 0x00 运行速度(2字节) 运行时间(2字节) 舵机角度(1字节)
电机速度: 2 字节有符号数,单位:mm/s,正数前进,负数倒退,0 - 惰行,32767 - 刹车,-32768 - 无效速度,不操作
运行时间:2字节无符号数,单位 秒
舵机角度:同前
应答内容:(同读状态命令)
E)命令7:速度方式定距离运行(Key = 6,Len = 5,Val:2字节运行速度, 2字节运行距离,1字节舵机角度)
0x06 0x05 0x00 运行速度(2字节) 运行距离(2字节) 舵机角度(1字节)
电机速度:(同上)
运行距离:2字节无符号数,单位 mm
舵机角度:同前
应答内容:(同读状态命令)
主任务从串口接收任务获取上述操作命令,解析后,将相应的工作参数转发给电机驱动和舵机驱动任务。
读状态命令则由主任务完成,为提高响应速度,两个驱动任务定时将自身的工作状态反馈给主任务,主任务接收并保存,以便随时响应读状态命令。
主任务等待的消息为:串口任务发来的命令、电机驱动及舵机驱动反馈的状态、看护任务发来的询问。
输出的信息为:发给电机驱动及舵机驱动的解析后命令参数,发给串口发送任务的应答信息。
3.3.5 电机驱动任务
任务需要完成:
1)PWM方式驱动电机
2)通过码盘反馈实现测速
3)通过PID算法实现调速
4)通过码盘反馈实现行走距离控制
5)通过计时器实现定时运行控制
轮式驱动单元的电机驱动设计了4种工作状态:
前进、后退、惰行、刹车。
前进、后退不难理解,惰行和刹车需要说明一下:
惰行 —— 是指在停止PWM信号后,电机驱动H桥使电机线圈处于开路状态,电机转子会由于惯性,继续转动到机械阻力使其停止。
刹车 —— 是指在停止PWM信号后,电机驱动H桥使电机线圈处于短路状态,电机转子由于惯性转动产生的感应电势形成电流,产生阻力,和机械阻力共同作用使其停止。
在精确控制行走距离时,应该运用刹车功能,降低惯性产生的误差。
平时待命状态,使其处于惰行状态,可以轻松的用手转动轮子。
为了对应这两种状态,在PWM命令参数中增加了刹车、惰行。
硬件上电机由3根信号线控制,2根控制线实现电机的四个状态,一根PWM线控制电机功率。需要启用RTT的PIN组件及PWM组件。
轮式驱动单元设计是作为编程学习素材,故刻意降低成本,码盘是用轮毂上的齿实现,分辨率较低,为达到测速所需的精度,在算法上做了优化(增加了编程的挑战)。
基于正常转动的特征:相邻两个脉冲的周期不会突变(排除干扰造成的脉冲丢失、抖动等异常状况)。
拟用计数和测周期两种方式组合,实现倍频,以提高分辨率。处理方式为:
在测速周期内,一方面对脉冲计数,得到完整脉冲的计数值。同时测量每个脉冲的周期,保存上一个脉冲的周期值(也可以保留前几个脉冲的平均值,以提高可靠性),为计算非完整脉冲做准备。
当测速周期到时,读取当前脉冲周期测量“已计量的时间“,除保存的“前一脉冲周期值”,即可得到非完整脉冲值 0. XX ,从而提高分辨率。
图示如下:
通过这种方式,基本满足了测速的需要。
测速和行走距离控制都需要启用IO中断。
因为测速和PID计算需要定时,但计时要求比不高,故直接使用OS的tick计时。
电机驱动任务等待的消息:主任务发来的参数、Tick定时唤醒信号。通过Tick定时唤醒实现周期性处理,如测速、PID调速、定时运行。
输出为:电机的物理运转、工作状态反馈
3.3.6 舵机驱动任务
舵机驱动比较简单,输出周期为20ms的脉冲信号,脉冲宽度从1.5 ~ 2.5ms可调。
基于PWM组件即可实现。
舵机自身有闭环控制。但为了能反馈舵机当前状态,主要是为了告知舵机是否转到指定角度,需要做一些处理。
因舵机自身无反馈信号,只能根据舵机参数(响应速度),通过计算当前角度到指定角度转动需要多少时间,适当放宽后计时处理,计时完成则说明转到;为连续控制提供方便。
舵机任务等待的消息为:主任务发来的参数、Tick唤醒消息。
因本身舵机完成计时精度不高,用Tick计时即可。
舵机任务输出为:舵机物理运动、工作状态反馈
3.4 基于RT-Thread 实现过程
3.4.1 工程创建
选择STM32F411CE 芯片创建标准 RT-Thread 项目:
系统版本4.1.1;芯片支持包版本0.2.3;工具链(编译器)版本 10.2.1。
对生成的main函数略作修改,输出7次信息后停止输出。
编译通过,可以下载执行:
说明RTT的编程基础已有,软、硬件环境已打通,可以着手根据上述设计编写程序。
因轮式驱动单元当初设计是模块化构思,即可以用一个或多个构成不同驱动方式的小车底盘,为后续用多个轮子和舵机做其它驱动方式的小车方便,故此处用C++模式,以便用类的方式简化后续的编程,增加程序的可维护性、可扩展性。
在 RT-Thread Studio 环境下,用C++编程,除了在RT-Thread Setting中选择C++外,如果用多文件编程,需将应用的C程序后缀改为cpp,否则编译出错。
之前在两轮差分小车驱动上尝试过使用 RT-Thread,这次以那个程序为基础修改。
3.4.2 硬件资源分配
串口命令端口:
USART1(PA9、PA10)默认给 Finsh使用。
将USART2(PA2、PA3)作为串口命令及反馈端口。
电机驱动:
PWM控制端(CT1):PB6,使用 PWM4(Timer4)通道1(T4CH1)
工作状态控制端(CT2、CT3):PB5(CT2)、PB4(CT3)
脉冲反馈输入端:PA12,中断方式处理
电机电压采集端:PA0,使用ADC1,通道0
电机电流采集端:PA1,使用ADC1,通道1
原来程序是驱动差分小车的,控制两个电机,分别对应左、右两侧。为了便于日后将程序扩展应用到多轮驱动小车,将电机变量标识从左右侧改为1、2……。目前只有一个电机,故定义为“1”。
为了实现倍频测速,启用Timer3,作为硬件计时器,获取ns级计时。
舵机驱动:
因为舵机的控制脉冲周期为20ms,和电机不同,故需要另用一个定时器实现。
基于STM32F411CE的芯片引脚分配,考虑到后期可能需要驱动三轮全向小车,选择Timer2作为舵机驱动用定时器,仍然使用PWM模式。
目前驱动一个舵机,使用PA15(T2CH1),即PWM2的通道1。
参考电机驱动方式,构建舵机驱动类,完成:
1)将角度转换为脉冲宽度
2)进行角度非线性修正,弥补舵机的偏差
3)根据舵机操作的运行角度,用延时方式指示舵机工作状态。
3.4.3 编程
之前首次尝试 RT-Thread 是做了一个两轮差分驱动的小车程序,在上面完成了电机驱动、测速、调速,以及一直想实现的PID自整定;控制效果不错,基本达到了预期。
这次以两轮差分驱动的程序为基础,修改为舵机转向单轮驱动小车的驱动程序,电机驱动部分基本照搬,增加了舵机驱动部分。
因为后面还想尝试使用 RT-Thread 驱动3轮全向小车,所以保留了电机驱动用类的方式,同时将舵机驱动也做成类,这样后面做三轮小车就很方便了。
同样,为方便测试,基于以往的程序,修改为此处所需的PC端测试程序(基于Processing编写):
全部0条评论
快来发表一下你的评论吧 !