【2021年国赛】F题-送药智能小车
资料下载
一、赛题分析
1.1. 题目资料
原始题目
数字字模
1.2. 题目分析
结构相关
- 小车长×宽×高不大于 25cm×20cm×25cm,只能使用普通车轮。在转弯时不能碰到走廊边缘,所以小车要尽可能小,越小越不容易碰到走廊边缘。
- 药品重约 200g。
硬件相关
- 两个小车均由电池供电。
- 两车之间要能无线通讯。
- 外界不能有任何附加电路与控制装置。
- 要有一个 RGB 灯(通过不同的 PWM 值控制来切换颜色)或者直接三个红,黄,绿灯。
- 作品应能适应无阳光直射的自然光照明及顶置多灯照明环境,测试时不得有特殊照明条件要求。
软件相关
- 每次测试开始时,只允许按一次复位键,装载药品后即刻启动运送时间记时,卸载药品后即刻启动返回时间记时。不能人工干预,测试项目只能测试一次。
- 在药房处识别病房号的时间不超过 20s,发挥部分 1 中自选暂停点处的小车 2 与小车 1 的车头投影外沿中心点的红实线距离不小于 70cm。
- 有任何一个指示灯处于点亮状态的小车必须处于停止状态,运送过程中不允许在同一走廊上错车或超车。
功能(得分)相关
- 近端病房号位置固定不变,中部病房和远端病房号(3-8 号)在测试时随机设定。
- 检测到药品装载完成后就开始自动运送。
- 单个小车运送药品到指定的近端病房并返回到药房,运送时间和返回时间均小于 20s,超时扣分。
基本要求
- 根据走廊上的标识信息自动识别,寻径将药品送到指定病房,投影要在门口区域内,
- 到了指定病房后,点亮红色指示灯,等待卸载药品。
- 人工卸掉药品后,小车自动熄灭红色指示灯,开始返回。
- 自动返回药房,点亮绿色指示灯。
发挥部分
- 两个小车协同运送到同一指定的中部病房,小车 1 到达病房后等待卸载药品,小车 2 识别病房房号装载药品后启动运送,到达自选暂停点后暂停,点亮黄色指示灯,等待小车 1 卸载;小车 1 卸载药品后开始返回,同时控制小车 2 熄灭黄色指示灯,继续运送。(从小车 2 启动运送开始,到小车 1 返回药房且小车 2 到达病房的总时间越短越好,不计算小车 2 黄灯亮时的暂停时间,不超过 60s)
- 两个小车协同运送到不同的远端病房送、取药品,小车 1 送药,小车 2 取药。小车 1 识别病房号装载药品后开始运送,小车 2 在药房处识别病房号等待小车 1 的取药开始指令;小车 1 到达病房后卸载药品,开始返回,同时向小车 2 发送启动取药指令;小车 2 收到取药指令后开始启动,到达病房后停止,亮红色指示灯(从小车 1 返回开始,到小车 1 返回到药房且小车 2 到达取药病房的总时间越短越好,不超过 60s)
- 其他
1.3. 如何制作送药小车
从上面的题目我们可以分析出,完成题目的基础要求有这些:
主控
主控当然是用 立创·梁山派开发板 了,他用的芯片是 GD32F470ZGT6,主频 240、内部 SDRAM 512K。对于智能车比赛来说是足够用了。
说明
- 板载 4 个 led,可以用来做状态指示。
- 核心板上有两个按键可以用来设置参数。
- 一路 SDIO-TF 卡槽,如果需要记录调试数据,可以移植文件系统,把要记录的数据以 CSV 格式写入 sd 卡中,需要查看调试数据时把 sd 卡中的文件拷入到电脑里,用 EXCEl 或者 Python 做数据分析
- 一路 SPI Flash,可以用来存储需要掉电保存的数据以及各种关键数据。
- SWD+串口调试接口,可以通过这个接口直接烧录调试,并且带一个串口方便调试。 更多信息可以去立创开发板官网查看 航模电池需要给小车核心板供电,但用的航模 3S 电池是不能直接供给开发版的,开发版需要的电压是 5V,所以需要一个降压芯片。常用的降压模块可就多了去了,比如 MP1584EN,LM2596,TPS5430 等。这里我们为了全国产化就选用国产 SGMICRO(圣邦微)的 SGM6132,最大电流 3A,输入范围 4.5V-28.5V。
驱动小车前进和转弯的动力装置(只能用普通车轮)
驱动方式选择: 两轮驱动+万向轮||牛眼轮
- 后驱+前置舵机转向方式(就是平常路上开的那种小轿车的结构,适用于路线直角转弯比较少的地方,相信大家也知道汽车过直角弯有多麻烦,这种对转弯半径要求较高,并且还需要考虑后两轮差速问题)
- 四轮驱动+差速转弯(比如最近比亚迪新出的仰望 U8,他的轮子由四个电机独立控制,每一个电机可以单独控制轮子实现正转和反转,当左侧两个轮子和右侧两个轮子以相反的方向转动时,就可以实现原地转向)
- 两轮驱动+万向轮||牛眼轮(对于这个比赛我认为用两个轮子+一个或两个从动轮就够用了,而且轮子电机少,做出来的小车尺寸也更小,不容易压到题目中说的走廊边缘。这个的转弯就是给两个电机不同的转动方向就行了)
电机选择: 直流减速有刷电机
- 直流减速有刷电机
优点 缺点 - 转速范围广,转速高,扭矩大。
- 控制简单,使用方便。
- 价格低廉。- 需要外接减速器,体积较大。
- 转速波动较大,精度不高。
- 不适合高精度运动控制。- 步进电机
优点 缺点 - 转动精度高,不需要编码器。
- 可以实现高精度定位和运动控制。
- 适合低速高精度运动。- 转矩小,转速慢。
- 需要驱动器,控制复杂。
- 价格相对较高。- FOC 无刷电机
优点 缺点 - 转速范围广,转速高,扭矩大。
- 转速精度高,控制精度高。
- 能够实现高效能、低噪音的运行。- 硬件成本相对较高。
- 控制复杂,需要配合专门的控制器使用。
- 对控制算法的要求较高。那我就选择普通的直流减速有刷电机了,要自带编码器,因为编码器可以提供电机转速和位置信息,这些信息对于小车的控制非常重要。可以帮助智能车更加精确地控制电机的旋转速度和位置,可以提供反馈,提高小车的可靠性和稳定性。
用步进电机也是不错的选择,适合中低速,只要电流足够并且扭矩不超过额定值,设置好细分,你给一个脉冲他就转一定的角度,甚至可以不用编码器就可以稳定转速,相对来说步进电机的驱动器一般都比较大,要注意布局。
用 FOC 电机就更好了,高中低速都适合,但是对控制算法要求很高,对驱动电路要求也高,大佬可以用。
针对直流电机的选型,要注意转速和额定电压。可以确定了轮子直径后再来确定电机的转速。
轮胎直径为6.5cm,周长=0.065*3.14=0.2041米,走1米的距离需要转4.90圈,也就是如果每秒转4.90圈,全速1m/s需要 300转/分钟,全速2m/s需要 600转/分钟,全速3m/s需要 900转/分钟 轮胎直径为8.5cm,周长=0.085*3.14=0.2669米,走1米的距离需要转3.75圈,也就是如果每秒转3.75圈,全速1m/s需要 225转/分钟,全速2m/s需要 450转/分钟,全速3m/s需要 675转/分钟
直流电机电机驱动选择: AT8870
用直流减速电机的话电机驱动就比较简单了,常用的直流电机驱动有以下这些:
- L298N,经久不衰,相信每一个刚开始接触智能车比赛或者循迹小车的人都知道。它可以 5V 和 12V 供电,是一个双 H 桥电机驱动,可以控制两个直流电机或者一个步进电机。他有一个非常大的散热片,当 12V 供电时,他还可以对外输出 5V 给外部设备供电。
- 自己搭建 H 桥驱动电路,一般都是要驱动的电机功率很高以及制作者水平很高。对于这个送药小车比赛来说,用成品的电机驱动芯片就够了
- TB6612,体积小巧,市面上有很多基于这个芯片的模块,用的人多,不容易出问题
- RZ7899,国产电机驱动,主要用在玩具,阀门电机驱动,电磁门锁驱动等。在网上搜索相关资料时发现有些帖子说在使用过程中会出现芯片烧毁的现象,有可能是其内部没有做死区控制引起的。而且查询其 datasheet 也没有发现对输入 PWM 频率的限制和说明,网友一般都建议在 1khz 以下。
- AT8870,这款的 datasheet 已说明有死区控制,网上评价比较高,且其最大 PWM 输入频率可以高达 100Khz,这就意味着电机控制会很顺滑,不会出现异常的噪音。对于一般的电机控制速度来说,PWM 信号最少也得来个 1Khz 以上吧。
轮子选择: 软质橡胶
尽量选择轮子的外部材料为软质橡胶,接触面积越大越不容易打滑,在小车启动和停止的时候更不容偏移。不过考虑到小车尺寸 25cm×20cm×25cm 的要求,在满足抓地力的情况下小车轮子与地面接触面越小越不容易压到走廊黑线。轮子的直径越小对电机扭矩的要求就越小。在选择的时候自由取舍。
检测地图中红线以及检测门口区域(黑色)
视觉识别: K210
- Openmv(现在最火的嵌入式机器视觉模块,使用 MicroPython 驱动,可以把它当成一个可编程摄像头)
- K210(是国内厂家勘智研发的一款采用 RISC-V 处理器架构,典型设备功耗 1w,算力1TOPS 的 AI 芯片,自带 SRAM,可以在本地完成数据的处理和存储,国内厂家也根据 openmv 的 IDE 重新制作了 Canmv IDE 和 Maixpy IDE)
- 树莓派,卡片个人电脑,可以在上面直接运行 OpenCV,进行循迹和数字识别,性能比上面两个都强得多,但是开发难度也是最大的,它可以用很高清的摄像头,用它来参赛简直可以说是降维打击。 这几个都可以使用 Python 编程,前两个上手比较容易,这第一个和第二个文档资料比较丰富。
针对巡线,一般来说就是要找到图像区域中想要的色块,不管是灰度阈值还是 LAB 阈值,都需要提前获取你想要找到的对应颜色的阈值,尽量选取能和比赛时环境差多不的情况下的阈值。这个阈值的调节可以参考他们对应 IDE 里面的机器视觉中的阈值编辑器,可以很方便的更改阈值。
初学的话可以先运行一下他们各自的找色块例程,熟悉一下如何使用,然后学习怎么划分 ROI,感兴趣区,让处理器只在你要求的区域寻找色块。
这里我们就选择国产的 K210 了,他在性能上比 openmv 强,但是他的软件库要比 openmv 落后不少,有些能在 openmv 上执行的语句在 K210 上是执行不了的,不对对于找色块对它来说就是小菜一碟。了解了如何获取到色块中心坐标后,就再运行一下他提供的串口例程然后修改它让他可以传输色块的中心坐标,可以用串口助手在电脑上收到 k210 发送过来的色块坐标信息后基本就可以了。
光电二极管灰度传感器
光电二极管灰度传感器(主要是利用光电二极管对光线的敏感性,通过测量物体表面反射的光线强度来确定物体表面的灰度值,探测到不同颜色地面时会产生不同的电压值,电压值会随被测面的颜色变化而变化,所以理论上可以识别任意颜色)
- 模拟式(模拟式需要单片机自带 AD 采集(模拟量采集),采集到的信息更准确,需要在单片机端自己设置阈值来进行判断当前识别的颜色)
- 数字式(他就是基于模拟式,一般会自带一个处理芯片或者比较器,通过手动调整电路板上的旋转电阻器来设置灵敏度,针脚直接输出的就是高低电平)
红外反射传感器
只适用于黑线识别,调整非常麻烦,基本上是不能用,虽然他简单,不易受光线影响,但是他是无法区分白和红的,而本次题目中是红线
线性CCD
线性CCD可以说他是只有一个像素的摄像头,是一个一维的摄像头,他可以很容易识别红色与白色。
小车底板和固定药品装置,固定摄像头机构,供电电池,紧急开关
小车底板一般常用的板材
- 亚克力板
- 碳纤板
- 玻纤板
- 电木板
- 铝合金板
我们普通的电路板都是 FR-4 玻纤板,1.6mm 板厚对于做这种小车来说强度是足够的,而且加工也挺方便的,画个电路板板框,里面再加一些孔位和一些导线,直接发到嘉立创等收货就行了,如果想用亚克力板的话也可以用嘉立创 EDA 专业版画面板,然后去立创商城下单,把面板当成小车底板用。在设计的时候可以多打一些孔,方便后面调整各个部件的位置。如果你的预算充足的话,也可以用铝合金板,现在三维猴可以接 CNC 订单。
固定药品装置
题目中要求是一次性装置约 200g 药品,但没有要求测量药品重量,所以只要能检测到药品有没有装上就行了。可以使用接近开关传感器或者直接用轻触开关,不怕麻烦的话也可以直接用称重传感器。
既然选了用砝码来替代药品,就得设计一个装载砝码的打印结构,防止小车运动时造成砝码脱落。设计时要注意 3D 文件的壁厚不要太薄,强度不够容易破裂,画完后导出 STL 文件后直接到三维猴下单就行了,一般这种不长时间在太阳下使用的结构件,直接选最便宜的树脂材料就行。
固定摄像头装置
这个一般也是用 3D 打印来制作,考虑到远端病房有四个数字并排放置的情况,如果摄像头不动的话很有可能是识别不全的。所以还需要加一个舵机使得摄像头可以左右偏转。为了小车运行的稳定,小车的重心应该尽量靠近两个驱动轮中间,所以 k210 主板和摄像头是分开的,摄像头架子上就是一个单独的 OV2640 摄像头和 FPC 延长板。对于这个重量,用 5V 供电的塑料齿银燕舵机 ES08A II 也就够了。
因为提前也不清楚摄像头哪个角度跟合适,所以需要这个固定摄像头的装置也可以上下手动调节,一般我们可以在 3d 模型上开出圆弧形状的槽或者圆弧阵列防止的螺丝孔。这个对于精准角度的要求不高,但是固定好确定好角度之后就应该是不变的,方便后续数字识别的采集和选线。
供电电池
考虑到要驱动直流减速电机,所以除了要考虑容量也要考虑放电倍率,一般选用航模电池就足够了,一般常用的电机都是 12v 驱动,所以这里可以选择一个 3s 航模锂电池。
紧急开关
为了防止调试小车时出现参数设置错误导致失控从而造成伤车伤人,可以加一个急停开关,出现异常的情况时拍一下急停开关让小车断电。
用来识别任务目标房号和走廊上房号的数字识别模块
因为题中是确定的印刷体,所以比较简单的方法是模板匹配,他是将采集到的数字与模板的像素点做对比运算来计算相似率的。
我们上面已经选取了 K210 来做巡线,它自带 KPU(通用的神经网络处理器),也非常适合用来作数字识别。我使用的 IDE 是 MaixPy,要采集的数据集图像尺寸为 224*224(这是目前最常用的网络数据大小),为了让结果更准确,所以要训练的数据集要尽可能多,每种数字的数据量尽可能相等。但 k210 的算力也是有限的,他所能运行的模型也是有限的,对于 k210:运行 c 代码时最大可以加载 6MB 左右的模型,运行 maixpy 的最小固件时,能加载 3MB 左右的模型,运行 maixpy 的最大固件时,能够加载约 2MB 左右的模型。
K210模型训练步骤
采集数据集
因为我们运算是在 K210 本地的,图像来源也是 K210 自带的摄像头,为了让结果更准确,可以用 python 编写一个 K210 的拍照程序,采集固定的 224*224 图像并保存到 TF 卡里面。最好是搭好小车框架后,选好摄像头的安装位置和角度,将拍照程序保存到 K210 的运行 sd 卡里面。当按下按钮的时候就进行当前图像的保存。
对数据集进行标注
用读卡器把 TF 卡里面采集到的训练集保存到电脑上,设置好标签,在这里标签就是 1,2,3,4,5,6,7,8。用 labellmg 进行图像的标注,每种标签最少得来 100 张。
开始训练
训练可以选在线的也可以用本地的,但是如果像是电赛这种时间很紧张的比赛,为了防止到时候千军万马过独木桥,导致你的训练任务一直在排队,最好先设置好自己的本地环境。已经有一个开源的大佬只做了一个免安装环境的 Mx-yolo,安装好后,选好训练图片文件夹地址和训练标签文件夹地址,设置训练次数,Alpha,Batch Size,是否开启数据增强。
- 模型训练的次数需要在保证模型收敛的前提下进行合理的选择。如果训练次数过少,可能会导致模型欠拟合,无法很好地拟合训练数据。如果训练次数过多,可能会导致模型过拟合,无法很好地泛化到新的数据。因此,需要根据具体的问题和数据集进行调整。一般来说,可以通过观察模型在训练集和验证集上的表现来确定合适的训练次数。当模型在验证集上的表现不再提升时,可以停止训练,以避免过拟合。
- Alpha 主要影响所生成模型的大小,选 0.25 模型大小约 219k,选 0.5 模型大小约 831k,选 0.75 模型大小约 1.85M,选 1.0 模型大小约 3.24M。当运行 maixpy 的最大固件时,k210 最大只能加载约 2MB 左右的模型,所以选 0.75 就行了。
- Batch Size 一般选 8 就行了。这个参数是指每次迭代训练时,所选取的样本数。Batch Size 的大小会影响模型训练的速度和稳定性。较大的 Batch Size 可以加快模型训练的速度,因为每次迭代处理的样本数量更多。但是,如果 Batch Size 过大,可能会导致内存不足或显存不足的问题。此外,较大的 Batch Size 还可能导致模型过度拟合训练数据。较小的 Batch Size 可以提高模型训练的稳定性,因为每次迭代处理的样本数量较少,可以更好地避免过拟合。但是,较小的 Batch Size 也会导致训练时间变长,因为需要更多次迭代才能处理完所有样本。
- 数据增强建议开启,他主要是做了一些随机裁剪,随机旋转,缩放,翻转等。
查看结果报告和测试结果
训练完之后看一下测试结果,测试样例是否已经被正确识别了。它还会给一张测试报告,确保模型损失已经到了0.1以下。在测试报告中,epoch代表训练次数,model loss是模型损失,train代表训练集,是用来训练模型的数据集。valid代表验证集,是用来评估模型性能和调整超参数的数据集。这两条线越来越往下就是越来越好了。
在k210上面运行
用mx-yoloV4训练出来的数据会自动转化为K210的Kmodel,也会给出测试程序。把boot.py,lables.txt,mx.kmodel一起放到K210的sd卡里,重新上电就可以运行了。
用来指示小车工作状态的的 LED 灯
说明
题目中要求能显示红,绿,黄灯,那就直接去立创商城找一个 RGY 三色 LED 灯,我找了一个,编号:C2897364。查询手册我们可以知道以下信息:
- Reverse Voltage: 5V(最大反向电压,超过这个就会坏)
- Forward Current: (正向电流)
- R: 25mA
- G: 25mA
- Y: 25mA
- Forward Voltage: (正向电压)
- R: 2.0V
- G: 3.3V
- Y: 2.0V 为了让每种颜色的灯的最高亮度基本保持一致,我们需要确定限流电阻,计算公式为 R=(供电电压-正向电压)/正向电流。 限流电阻用贴片电阻,0402 电阻功率为 1/16W,即 0.0625W,0603 电阻功率为 1/10W,即 0.1W。不能小马拉大车,容易把马累死。 计算之后可以知道要选择下面这几个电阻:
- R:电阻值:120Ω,功率 0.075 -贴片电阻 0603 以上-立创商城编号:C22787
- G:电阻值:68Ω,功率 0.0425 -贴片电阻 0402 以上-立创商城编号:C25254
- Y:电阻值:120Ω,功率 0.075 -贴片电阻 0603 以上-立创商城编号:C22787
用于双车通讯的无线模块
常用的无线通讯模块
- WiFi 模块:WiFi 模块可以实现高速无线网络连接,但是其功耗较高,不适合低功耗应用。
- 蓝牙模块:蓝牙模块具有低功耗、短距离通信等特点,但是其传输速率较慢,适用于数据量较小的场景。
- Zigbee 模块:Zigbee 模块具有低功耗、低成本、自组网等特点,适用于物联网应用。
- LoRa 模块:LoRa 模块具有长距离、低功耗、低成本等特点,适用于物联网应用。
- NB-IoT 模块:NB-IoT 模块具有低功耗、广覆盖等特点,适用于物联网应用。
在室内使用的话,wifi 和蓝牙都不错,而且这个智能小车比赛过程中两个小车的距离也不会太远,对于数据量的要求也不高,只需要简单传输一些数字识别结果和指令就够了。
我就选蓝牙模块了,国内沁恒的蓝牙芯片都挺不错的,CH9143 是一个主从一体的蓝牙芯片,他支持蓝牙-串口-USB 三通,也就是说在连个小车上各放一个,他们配对连接上之后你就把他正常当串口用就行了。它的配对方式也挺方便的,只需要把两个芯片同时上电,他们就可以进行配对了。
在调试的时候也可以把一个模块连到电脑上,小车上那个可以就可以通过串口输出数据到电脑上了,远程串口挺方便调试的。
其他附加
- 可以加一个无源蜂鸣器来进行提示和报警,当电池电压过低或者出现错误时进行蜂鸣器报警。
- 可以加一个六轴或者九轴的姿态传感器,通过姿态解算可以得到当前航向角,可以让小车在一定时间内保持车头朝向不变,这样就算车轮打滑小车的车头朝向也不会大变。但是这个方法对姿态传感器的数据要求和算法处理的要求也很高。
- 预留灰度传感器的接口,别万一识别巡线效果拉了,硬着头皮也走不下去就G了,多给自己留条后路。
- 加一个 CAN 芯片,如果你想用 can 芯片控制电机的话就可以用了,大疆的 robomaster 系列电机和市面上的一些高性能电机都是可以用 CAN 通讯来控制的
- 加一个 sbus 电路,参加完比赛做个小遥控车也不错。
二、电路设计
本项目一共设计了6个板子:
0_小车扩展板:
最主要的板子,驱动电机,舵机,板载国产姿态传感器和磁力计,CAN电路,蜂鸣器等。1_外置蓝牙模块:
用国产蓝牙模块做双车通讯2_梁山派-K210固定板:
结构件,承载梁山派和K210开发板3_小车底板:
结构件4_RGY灯板:
为了满足题目要求,显示小车工作状态5_摄像头补光板:
在环境亮度太低时给摄像头补光,也用于固定摄像头位置
代码仓库:https://gitee.com/lcsc/medical_car
2.1. 全国产化
为什么要做全国产化呢?这个也算是之前的疫情给了个机会,疫情开始那几年STM32都涨疯了,疫情期间全球供应链受到了很大冲击。很多人都说显卡涨价涨的太离谱了,其实疫情那几年,就涨价幅度来说,芯片的涨价甩显卡几条街。
所以我们要开始注重自主创新和技术独立,现在中国的很多行业都在推全国产元件,既可以提高国内的技术水平,也可以降低对外部技术的依赖,提升整体竞争力。希望大家在工作学习中也可以多用用国产元件,当然并不是盲目的支持,当前许多芯片与国外技术差距确实还不小,也不要走极端,不能当二极管。国内的芯片能满足项目需求就支持它,不能的话用国外的也无可厚非。
2.2. 引脚分配
在进行电路设计最开始的开始,最重要的首先就是引脚分配,引脚分配的不好很有可能造成明明资源很丰富的 MCU 被搞得没几个接口可以用,或者造成布线异常麻烦。良好的引脚分配可以有效决定电路板上各个元件的位置和连接方式。
结合梁山派原理图和GD32F470的Datasheet中的2.6.2中的硬件定义就可以获得下面这张分配表,因为GD32并没有推出类似Cubemx这种的图形化配置工具,所以目前只能手动记录了,因为排针引出的部分引脚是被开发板的SPI Flash,LED,SDIO等占用的,部分会连接上下拉电阻。所以一定要注意分配时不要粗心大意。连接到屏幕FPC座的引脚,只要你不使用屏幕就可以使用。
P1的引脚功能定义说明
P2的引脚功能定义说明
我选择的时候是先按照UART,SPI,正交编码器接口,PWM,IIC(优先硬件,软件的也可以),CAN,ADC,LED,KEY,灰度传感器接口 这个顺序来选的,具体怎么选还是要看你自己对这些资源的重要性排序。
具体的引脚分配:
具体分配可以查看原理图的最终定稿
UART
- USART2(RX:PB11;TX:PB10) --K210
- USART5(RX:PG9;TX:PG14) --无线模块
- USART1(RX:PD6;TX:PD5)--遥控备用
- UART3(RX:PA1)--SBUS预留
SPI
- SPI3
- CS0(PE4)
- CS1(PC13)
- SCK(PE2)
- MOSI(PE6)
- MISO(PE5)
IIC
- I2C0(SCL:PB6;SDA:PB7)
- 模拟I2C(SCL:PG6 ;SDA:PG7 )
PWM
- TIMER8_CH0(PA2)--电机M1-IN1
- TIMER8_CH1(PA3)-电机M1-IN2
- TIMER11_CH0(PB14)-电机M2-IN1
- TIMER11_CH1(PB15)-电机M2-IN2
- TIMER7_CH0(PC6)-舵机1
- TIMER7_CH1(PC7)-舵机2
- TIMER12_CH0(PA6) --蜂鸣器
- TIMER0_CH0(PA8)-备用
Encoder
- TIMER2_CH0(PB4) -电机M1-Encoder A
- TIMER2_CH1(PB5) -电机M1-Encoder B
- TIMER1_CH0(PB8) -电机M2-Encoder A
- TIMER1_CH1(PB9) -电机M2-Encoder B
ADC
- ADC01_IN4(PA4)
- ADC012_IN11(PC1)
- ADC01_IN7(PA7)
- ADC01_IN14(PC4)
LED
- LED-R(PD3)
- LED-G(PG10)
- LED-Y(PA15)
KEY
- 碰撞开关 KEY0 (PG12)
- KEY1(PG11)
- KEY2(PG13)
- KEY3(PB3)
CAN
- CAN0(RX:PD0;TX:PD1)
还可利用引脚(灰度传感器备用)
- PD4
- PB1
- PB0
- PB12
- PB13
- PF10
- PC5
2.3. 电源电路设计
选用国产圣邦微电子的SGM6132,3A, 28.5V, 1.4MHz Step-Down Converter.
参考官方的典型应用来设计原理图:
这个芯片有一个比较好的一点就是留出了一个使能引脚,你要用的话就可以控制芯片的工作状态,不用的话不连它,他也可以自动启动。
一般这种电源芯片,官方都会给出参考原理图,SGM6132的datasheet第一页左下角他就给出了典型应用。不过是3.3V的。我们继续往下找到第10页可以找到典型应用电路,上面有各个参数的推荐选型的数值。
这里我要的是5V输出,结合上面就可以知道具体元件选型参数,都不用自己计算了。要是自己算阻值的话有一个要注意的地方就是电阻阻值不要超过他的最大值,比如这个芯片的R2最大值就是100kΩ,选的太大它可能就不能工作了。最简单可靠的还是用官方推荐的图来设计。
这里的阻容以及一体成型金属电感都是选用国产厂家生产的。
其他的XT30接口,自恢复保险丝,拨动开关电路,TVS过压保护等选型就比较简单了。XT30公头制造的出名的厂家也没几个,随便选都可以。
考虑到两个直流减速电机在启动时的瞬时电流会很大就算给每个电机分2A吧,再给开发板留1A,所以这里选择的是最大5A的自恢复保险丝。
这里选择的拨动开关,最大可以过6A的电流,妥妥的足够用了。
加入TVS保护电路。
他的工作原理是,当工作在正常电压下时,TVS保护电路的电阻很大,不会对电路产生影响;但在瞬态电压出现时,TVS保护电路的电阻会迅速变小,将电压限制在设定的安全范围内,从而保护电子设备不受损坏。不过锂电池的电压是非常稳定纯净的,这个保护还是主要调试时用开关电源供电时防止电压超标。
2.4. 电机驱动电路设计
一个AT8870可以驱动一个直流电机正反转,我们要驱动两个直流电机,所以这里要用两个驱动芯片。他的手册是全中文的,重点要看的参数主要是主供电要求,逻辑供电要求,逻辑输入要求,连续输出电流,峰值输出电流,最大PWM频率等。
了解上面以上参数后我们就可以选电机了,要确保电机的工作电流电压要在电机驱动芯片的工作范围以下,我选的是轮趣科技的MG513P20_12V ,这个电机减速比是20,工作电压是12V,额定电流是0.36A,堵转电流是3.2A。我们所用的AT8870峰值输出电流是3.6A,那还是有可能在长时间堵转的情况下烧毁的,所以我们需要设置驱动芯片的保护,这款芯片的ISEN引脚可以设置芯片的电流控制电流(这个电阻的取值和参考电压以及要设置的电流有关)。详细介绍可以看AT8870的datasheet的第8页,有详细的介绍和公式。在上面原理图中我就选择0.15欧姆了。代表参考电压为3.3V时,目标电流为2.2A。
再有就是我们看一下快衰减和慢衰减说的是什么,这里的快慢指的是电流,而不是电机转动的速度。
由于电机是感性负载,当断开电机两端的电压时,电流会产生反向电动势。这可能会对驱动芯片造成损坏。因此,要让电机停下来,除了断开电源,还需要建立一个续流回路来释放电机中的能量。慢衰减相当于加在电机(感性原件)两端电压消失,将电机两端正负短接。快衰减相当于加在电机(感性原件)两端电压消失,将电机两端快速接上与驱动电流相反的电流。
近期发现立创商城上面AT8870停止进货了,那可以选一个更好的,AT8236,最大峰值驱动输出可达6A,连续输出驱动可达4A,还是PINtoPIN的,可以直接替换,实际焊接的时候直接替换就好了。
2.5. 姿态传感器选型及设计
姿态传感器是一种测量物体姿态的传感器,他可以通过测量物体的加速度,角速度等参数来计算出物体的姿态信息。最出名的就是mpu6050了,他是InvenSense 公司推出的全球首款整合性 6 轴运动处理组件,应美盛公司已经被日本的TDK公司买了,现在出的最新的IMU都是ICM开头的,比如ICM-42688,可以看一下这个网页的介绍,mpu开头的型号和icm-2x开头的型号已经不被推荐了(有些都已经停产了)。科学技术发展很快,这些传感器的制造工艺和精度也在飞速提升。所以大家学习或者做产品的话最好都是用新产品,电子产品都是买新不买旧嘛,更新换代太快了。传感器不好用是会难为死软件算法的。
一般想要获取姿态信息有以下几种方式,第一种最简单的就是直接买模块,模块内部做姿态解算,你只需要直接处理姿态信息就行了,一般都是串口传过来的。第二种是IMU芯片内部自带解算算法,比如MPU6050自带DMP库,在MCU端配置好之后就能利用芯片自带的DMP加速来进行解算。第三种就是只能获取IMU的原始数据,然后再MCU内部自己进行解算,这个的难度最大,花的时间不够很有可能搞的一地鸡毛,获取到的数据也没法用,但同样这种学到的东西也最多。
虽然国内厂家生产的六轴姿态传感器和国外还有不小的差距,但是它基本没有供货风险,而且价格也挺实惠的。可以打开立创商城搜索,可以看到国内销量最高的两款是QMI8658A和QMI8658C,尽量选A型号,他是C型号的升级版本,有更低的噪声。他的封装也是和国外大厂的部分型号PINtoPIN的,后续可以替换测试其他厂家的imu。
参考QMI8658A的datasheet中第八页(现在立创商城下面的PDF预览已经可以查看整个手册了),他给出了三线SPI,四线SPI,IIC的应用图。查看手册中的表15,可以了解这款芯片SPI最高时钟支持15Mhz,IIC时钟最高支持400Khz。为了在单位时间内获得更多的数据,就选用SPI协议了。
2.6. 磁力传感器选型及设计
姿态传感器是分为6轴和9轴的,9轴的就是带磁力计的,但是目前国产的好像还没有九轴的姿态传感器。
在姿态解算中,磁力计可以提供重要的地磁信息,帮助确定设备的方向和位置。如果没有磁力计,只使用加速度计和陀螺仪来解算姿态,那么在某些情况下,例如设备在静止状态下或只在水平面上运动时,无法确定设备的方向。而通过使用磁力计,可以测量地球磁场,并确定设备的方向和位置,从而实现更准确的姿态解算。此外,磁力计还可以帮助校正陀螺仪的漂移误差,提高姿态解算的精度。
国产磁力计的性能现在还挺不错了,我看到部分飞控上都开始使用QMC5883L了。现在上海矽睿推出了更新的QMC5883P,他的量程比之前大了很多。
参考他的datasheet可以了解到它最高支持400Khz的IIC速率,不支持SPI,结合第七页的建议外部连接可以设计出以下原理图。
2.7. 蜂鸣器选型及设计
蜂鸣器可以将电信号转化为声音信号,可以向用户提供声音反馈或者警报信号。
蜂鸣器从构造类型上有电磁式和电压式两种,从驱动方式上来说有无源(由外部方波驱动)和有源(由内部驱动,外部给电就行)两种。按封装方式的不同也可以分插针式和贴片式。那么就开始打开立创商城开始选型吧,还是选国产的,一般来说电磁式蜂鸣器的动作电压可以比较低。
我选择的是无源电磁式贴片蜂鸣器,工作电压2-4v,频率4000Hz,这里的频率是指他在这个频率下的声音最响。
D2在这里的主要作用是保护这个驱动的MOS管,因为蜂鸣器和电机一样是一个感性元件,也就是说它的电流是不能瞬变的。必须有一个续流二极管提供续流。如果没有这个续流二极管,停止给蜂鸣器供电的时候在蜂鸣器两端会有反向感应电动势,产生高达几十V的尖峰电压,很有可能损坏驱动电路。
R26:限流电阻,防止电流太大损坏芯片的PWM输出引脚。R4就是一个简单的下拉电阻了。
2.8. 按键及药物检测开关设计
这个就是很平常的按键设计,要注意的一点是在原理图转PCB的时候要注意核对封装,不要把引脚连错了。
我们通常用的按键内部都是机械弹性开关,当它按下弹起的时候,机械触点会因为弹性作用而在闭合和断开的瞬间伴随着一连串的抖动。这种抖动会导致输入信号在高低电位之间弹跳,产生不正确的输入。
这里面电阻的作用是限流(害怕初学者不小心给设置成推挽输出了),在这里要注意的是要在芯片内部设置一个下拉。电容作用是硬件去抖,不过软件上还是需要软件消抖,硬件去抖只能改善不能消除。
这里的按键改为了碰撞开关,设计一个3D打印结构,当药物放下的时候使碰撞开关闭合从而起到检测药物是否安装成功的作用。
- 编号:C87120,也叫行程开关,鼠标里面的微动按键其实就是这个,鼠标里面的都是没有那个大贴片的。
2.9. CAN芯片选型及设计
控制器局域网总线(CAN,Controller Area Network)是一种用于实时应用的串行通讯协议总线,是德国BOSCH(博世)公司研发的一种串行通讯协议总线它可以使用双绞线来传输信号,是世界上应用最广泛的现场总线之一。之前主要用于汽车中各种不同元件之间的通讯。
打开立创商城开始选取国产can芯片,这里我选择的是 SIT(芯力特)的SIT65HVD230DR,3.3V供电,速率最高1Mbps。
VREF引脚是Vcc/2基准输出引脚,没用到就不连。Rs是模式选择引脚,强下拉至 GND=高速模式;强上拉至 VCC =低功耗模式;通过 10kΩ 至 100kΩ 电阻下拉至 GND =斜率控制模式。现在还没有什么要求就直接接地设置为高速模式。
其他连线参考手册就可以知道,注意这里的TXD,和RXD和串口的一般标注方式有点不一样的,RX接R,TX接D。注意CAN总线是需要两个120Ω的终端电阻,在CAN总线的两端必须连接终端电阻才可以正常工作,电阻的作用是为了匹配总线阻抗,提高数据通信的可靠性。
这个120欧姆不是算出来的,是测出来的,汽车上常用的典型双绞线测出来的特征阻抗就是约120欧姆。
2.10. SBUS电路设计
SBUS是一种串行通讯协议,通常用来将遥控器和被控制器连接。可以通过单个信道传输多个通道的数据。可以把它理解为串口协议,采用100k波特率,数据八位,两位停止位,偶校验。因为它采用的是反向电平输出,所以必须要硬件取反,除非你的芯片串口外设自带硬件取反。
三、PCB布局
在制作PCB的时候虽然大家都追求一版成功,但是一版成功可遇不可求。就算失败了也不要泄气。犯错也是学习的过程,只要不重复犯同样的错误就够了。
3.1. 预布局
原理图画完之后转为PCB,第一步就是要进行预布局,布局的好坏很大程度上决定了布线的难易。
为了和梁山派的大小保持一致,所以我们先把梁山派的板框和定位孔坐标复制过来,然后确定一下两个40P排母座子的坐标,因为想在背面放置GH1.25带锁扣卧式连接座,所以这里的排母需要选贴片的不然背面没法放座子。
然后就是先把尺寸面积比较大的元件先摆好,这里连接件比较大的有XT30电源接口,大电流拨动开关,两个电机连接座,两路舵机接口。
这样其实不太好,12V电源的跨度太大了,从下面跨到上面的电机驱动芯片,但我也没想出什么好方法放置。接下来就是姿态传感器和磁力计了,为了姿态传感器更能反应板子的姿态,所以尽量把他放到正中心,磁力计就放到姿态传感器旁边。理论上来说一般这种有电机运动震动的工况,IMU应该单独一个板子再做一个减震的,但出于成本和难度考量,就先直接放底板上吧,下图红圈中的左边是姿态传感器,右边是磁力计。其中上下的两条开槽主要有两个作用,一个是降低板材变形对姿态传感器的应力影响,一个是降低DCDC降压和电机驱动芯片工作时产生的干扰。
接下来就是各种连接座子和按键的布局了,到背面了。GH1.25连接座子和侧按按键肯定是要靠近板边的,为了防止侧按按键被误触,所以设计为了只有用指甲才能按下,按钮头缩回去了一点。
其他元件的布局就采用就近原则了,比如说电机驱动芯片就靠近电机座子放置,DC-DC电路就靠近XT30电源接口放置,其他的蜂鸣器,CAN芯片等就看哪个位置方便放就放到哪里,CAN芯片也要尽量靠近CAN信号输出座。
3.2. DC-DC降压
电源可以说是一个电路的心脏,一个不稳定的电源会让系统没法正常工作。在布局的时候一般要注意以下几点:
- 尽量缩小回路面积,减少电磁干扰。
- 将输入和输出回路分开布局,减少串扰。
- 将输入和输出滤波电容尽量靠近功率器件,减小瞬态电压峰值。
- 将地线布局宽阔,减小地回路的串扰。
- 将高频元件(如电感、滤波电容等)布局紧密,减小元件之间的串扰。
- 尽量避免元件之间的交叉布局,减少互相干扰。
- 对于高功率电路,需要考虑散热问题,保证器件工作在安全温度范围内。
- 最好不要用导线,都用区域填充来走线。
当然有些要求可能没法完美满足,尽量达到就行。
典型的两个:
SW回路越小越好
反馈电阻距离越靠近芯片引脚越好
布线这种仁者见仁智者见智,每个人都有不一样的想法。不管黑猫白猫,能抓到老鼠就是好猫。
3.3. 电机驱动电路
电机是大功率用电器,尤其是堵转的时候能达到2,3A的电流。千万不能芯片都还没烧坏呢,电路板的铜皮直接被烧断了。理论上来说1OZ的铜皮(厚度约为0.035mm)当宽度为1mm时,能走过的最大电流约为2A。我们自己用的话可以保守点,就算它1mm能过1.8A吧,所以对于电机的功率线最少也,2mm宽起步,如果是内层的话,内层铜厚一般为0.5OZ需要更厚。条件允许的情况下多来点余量。
一般布线电机驱动都是尽量走最上下两层,铜皮厚度够,散热也够好。直接用实心填充来布线。
3.4. 其他
除了CAN芯片信号的输出要注意走差分线以外,基本没什么需要注意的,走线尽量短一点,用电器的电源线要满足电流要求,比信号线要粗点。线宽,线距,过孔,字符等要求要满足嘉立创制板的工艺要求,不满足的话可能就要加钱了。
最后的最后,在投板前一定要跑过DRC检测,在平时布线的时候最好也时不时跑一跑DRC,早发现问题早治疗。
我走完线它长这个样子:
四、小车组装
基于梁山派的21年电赛送药小车组装文档
4.1. 组装底板
拿出小车底板,轮子,电机,电机架,万向轮,M3*28+6铜柱,螺栓-M3x5
4.2. 安装上板
拿出小车底板,各种3D打印件,螺栓-M3*5,固定用的铜柱,灯板,舵机等
4.3. 安装梁山派与K210并组装
连线相关:
- 灯板连接到小车扩展板上GH1.25的5PIN座子(LED_G,LED_Y,LED_R,GND,+5V).
- K210连接到梁山派的UART2.
- 外部蓝牙模块连接到梁山派的UART5.
- 外部轻触开关(检测药物安放状态)连接到梁山派的KEY0.
- 舵机连接到梁山派的PWM0.
- 两个电机连接到小车扩展板的两个电机座,小车前进方向左边电机连接到小车扩展板下面的座子,前进方向右边的电机连接到小车扩展板上面的座子.
接线示意图如下所示
五、软件工具与调试工具介绍
5.1. 软件工具
5.1.1. KEIL(MDK)
Keil公司是一家专门从事嵌入式系统开发工具的公司,它开发了一个名为MDK (Microcontroller Development Kit) 的嵌入式开发工具套件。MDK主要用于基于ARM Cortex-M微控制器的嵌入式应用程序开发。MDK提供了一套完整的开发环境,包括编译器、调试器、运行时库和中间件组件。
MDK的主要组件包括:
- μVision IDE:这是MDK的集成开发环境,包括代码编辑器、项目管理器、集成调试器等功能。μVision IDE支持语法高亮、代码补全、代码导航等功能,以帮助开发者提高编程效率。
- ARM C/C++编译器:MDK包含了一个基于ARM架构优化的C/C++编译器。它支持标准C11和C++14,并提供了针对ARM Cortex-M微控制器的优化选项,以实现高性能和低功耗的嵌入式应用程序。
- μVision调试器:这是一个功能强大的源代码级调试器,支持单步调试、断点设置、内存查看和修改等功能。它还支持多种调试接口,如J-Link、ULINK等,以方便连接不同的硬件设备。
- CMSIS (Cortex Microcontroller Software Interface Standard):这是一套用于ARM Cortex-M微控制器的标准软件接口,包括CMSIS-CORE(处理器核心接口)、CMSIS-DSP(数字信号处理库)、CMSIS-RTOS(实时操作系统接口)等组件。CMSIS旨在简化软件重用、降低学习成本,促进嵌入式系统开发的规范化。
- 中间件组件:MDK还提供了一些常用的中间件组件,如TCP/IP协议栈、USB设备和主机库、文件系统库等,以帮助开发者快速构建嵌入式应用程序。
现在已经有社区版了。大家可以去官网注册个账号获取社区版,没有代码大小限制,可供电子爱好者、学生等群体非商业免费评估和使用。
5.1.2. VOFA+
VOFA+是一款能够直观且简洁地将字节流转换为多通道数据的软件。它支持十六进制浮点数据和CSV格式字符串流。通过拖拽操作,能够动态添加控件并将数据绑定到这些控件上,从而实现传感器数据的可视化。
简单来说,是一款非常好用的串口调试工具,单片机按特定规则发送数据就可以在VOFA+上面生成曲线。方便调试。
5.1.3. MobaXterm
MobaXterm是一个强大的终端模拟器,集成了多种网络工具和功能。它适用于Windows操作系统,为系统管理员、开发人员和网络工程师提供了一个全面的工具集。MobaXterm支持SSH、Telnet、RDP、VNC、FTP、SFTP等多种协议,同时提供了会话管理、文件传输、远程桌面和其他功能。
对于嵌入式系统开发和调试,我们主要用到MobaXterm的串口(Serial)功能。可以轻松地与嵌入式设备上的串口进行交互,发送和接收数据。
以下是通过串口交互的简要步骤:
- 启动MobaXterm:首先启动MobaXterm。在主界面上,可以看到会话列表、终端窗口以及本地和远程文件系统。
- 创建串口会话:单击“Session”按钮,在弹出的会话管理器窗口中选择“Serial”。打开一个新的串口会话配置窗口。
- 配置串口参数:在串口会话配置窗口中,设置串口参数,如串口号(COMx)、波特率、数据位、停止位和奇偶校验等。这些参数需要与梁山派上的串口设置相匹配。完成设置后,单击“OK”按钮创建串口会话。
- 开始串口交互:MobaXterm将打开一个新的终端窗口,并自动连接到指定的串口。你可以在此窗口中与嵌入式设备进行交互,发送命令和接收响应。
- 会话管理:在MobaXterm的主界面上,你可以管理已创建的串口会话。例如,你可以保存会话配置、编辑会话参数、断开和重新连接等。
用这个软件主要是使用RT-Thread的Finsh控制台指令,方便调试和获取信息,免费版就足够我们用了。RT-Thread的Finsh(Shell)控制台是一个轻量级的命令行界面,用于与RTOS进行交互。通过Finsh控制台,用户可以执行各种命令以监控和控制系统,例如查看线程信息、操作文件系统、调试设备驱动等。
5.1.4. Visual Studio Code
Visual Studio Code(简称VSCode)是一个免费、开源的代码编辑器,由Microsoft开发。它适用于多种平台,包括Windows、macOS和Linux。可以安装Keil Assistant扩展来进行MDK代码的编辑。配置好之后很好用。
5.1.5. GIT
下载地址:https://git-scm.com/
Git是一种分布式版本控制系统,它主要用于跟踪和管理文件(通常是代码文件)的变化。简单来说,Git就像是一个记录文件变更历史的时间机器,它能帮助我们在项目中高效地协作和管理代码。
- 版本控制:Git可以记录文件的每次变更,生成一个可追溯的历史记录。这样,我们可以方便地查看文件的变更历史,比较不同版本之间的差异,并在需要时回滚到之前的版本。
- 分支管理:Git允许我们创建分支,可以在一个独立的副本上进行开发和修改,而不会影响主分支(一般是master或main)。有助于实现功能开发、错误修复等任务的并行处理,并在需要时将它们合并回主分支。
- 团队协作:Git是一个分布式系统,这意味着每个团队成员都可以在本地计算机上拥有完整的项目副本。成员可以在本地进行开发、提交更改,然后将这些更改推送到远程仓库,以便其他团队成员拉取并查看。Git提供了一套机制来解决潜在的合并冲突。
- 备份和恢复:使用Git,可以将项目托管在远程仓库(国外有GitHub、GitLab或Bitbucket等,国内有gitee,华为开源平台,Coding等),这有助于项目的备份和数据恢复。如果我们的本地计算机发生故障,可以轻松地从远程仓库恢复项目。
5.1.6. SOURCETREE
这个软件提供了直观的图形用户界面,使用户可以更轻松地使用Git。并提供了一系列功能,包括代码提交、分支管理、代码比较和合并等。让我们能够通过直观的图形界面而不是命令行来管理仓库,降低了GIT学习成本。
5.1.7. ENV
下载地址:https://www.rt-thread.org/document/site/#/development-tools/env/env
Env 是 RT-Thread 推出的开发辅助工具,针对基于 RT-Thread 操作系统的项目工程,提供编译构建环境、图形化系统配置及软件包管理功能。其内置的 menuconfig 提供了简单易用的配置剪裁工具,可对内核、组件和软件包进行自由裁剪,使系统以搭积木的方式进行构建。其内置的scons也支持直接编译。
5.2. 硬件工具
5.2.1. TTL转USB
单片机的串口信息需要传递给电脑就需要这种串口工具,国产的CH343,国外的FT232芯片的都挺不错的,直接在淘宝购买这些芯片制作的工具就行了,有时间有兴趣的可以自己制作,当然部分下载仿真器也集成了串口,就很方便使用了。
5.2.2. 下载仿真器
常用的有CMSIS-DAP(梁山派自带的下载调试器也属于DAP),PWLINK2(也属于DAP,带一个虚拟串口,9.9顺丰包邮,还是很香的),Jlink(应该是目前最强的调试器,价格也是最贵的),ST-link也可以用,但是在高版本keil上可能会检测连接的MCU是否为标准STM32设备。用一个稳定的下载器能节省很多调试时间,目前也出现了一些无线调试器,对小车调试来说很方便,可以在运动中调试,但是价格也比较高。有多少预算办多少事,在能力范围内选择合适自己的就行了。
5.2.3. 逻辑分析仪
可以测量数字信号的电平,时序,频率等,比如调试SPI时,如果确认焊接没有问题,但是数据一直不对,很有可能你以为的不一定是真实的。比如调试国产IMU,用SPI接口时数据不对,这种时候就需要用到逻辑分析仪查看实际的SPI信号电平和频率是否正常。使用逻辑分析仪我们可以进行多通道的信号捕获,时序分析,协议分析,触发过滤等。
5.2.4. 示波器
他可以测量直流交流信号的电压幅度,周期,频率等,在调试时可以用来观察信号波形并捕获异常情况。比如测试PWM波形,正交编码器波形,电源纹波和噪声等。
六、编译文档
6.1. 准备工作
Env 是 RT-Thread 推出的开发辅助工具,针对基于 RT-Thread 操作系统的项目工程,提供编译构建环境、图形化系统配置及软件包管理功能。
其内置的 menuconfig 提供了简单易用的配置剪裁工具,可对内核、组件和软件包进行自由裁剪,使系统以搭积木的方式进行构建。
Env 工具包含了 RT-Thread 源代码开发编译环境和软件包管理系统。
- 从 RT-Thread 官网下载 Env 工具。
- 在电脑上装好 git,软件包管理功能需要 git 的支持。git 的下载地址为https://git-scm.com/downloads,根据向导正确安装 git,并将 git 添加到系统环境变量。
- 注意在工作环境中,所有的路径都不可以有中文字符或者空格。
6.2. 用MDK编译
了解过 RT-Thread ENV 开发工具
- 克隆仓库到本地。
- 进入2_Code目录,使用 ENV 工具执行 pkgs --update下载缺失的软件包。
- 使用 ENV 工具执行 scons --target=mdk5更新MDK5工程文件。
- 打开2_Code目录下的project.uvprojx即可正常编译使用了。
从来没用过RT-Thread
- 去RT-Thread文档中心了解了解。
- 进入仓库的发行版,下载附件2_code.zip。
- 解压后用MDK打开目录下的project.uvprojx即可正常编译使用了。
6.3. 用ENV编译
进入仓库的2_Code目录,进入ENV工具,输入scons就开始编译了。
2_Code目录下的rtthread.elf就是编译后的文件。
七、建立RT-Thread工程模板
RT-Thread是一个开源的实时操作系统(RTOS),主要用于嵌入式设备。建立一个RT-Thread模板,主要有以下几个步骤:
1. 获取RT-Thread源代码:首先,从RT-Thread的GitHub仓库中克隆源代码。在命令行中执行以下命令:
创建一个文件夹,确保你以安装好git工具,命令行中输入下面的指令。
- 能顺畅访问github的:git clone https://github.com/RT-Thread/rt-thread.git
- 不能顺畅访问github的:git clone https://gitee.com/rtthread/rt-thread.git
2. 选择开发板的BSP:RT-Thread支持多种开发板和处理器架构。在rt-thread/bsp
目录下,你可以找到各种已经支持的开发板。找到适合你的开发板,或者选择一个接近你的硬件平台的开发板作为参考,在这里,我们就进入bsp->gd32->arm->gd32470z-lckfb
。确保已经安装好 Env 工具。首先输入scons
进行一下编译检查,然后输入scons --dist
可以生成一个发布工程,就能随意拷贝到任何目录运行了。
3.烧写和调试:打开目录下的project.uvprojx,编译测试,并使用下载仿真器(例如CMSIS-DAP、J-Link等)将生成的二进制文件烧写到立创梁山派上。接下来,就可以对RT-Thread进行调试。
现在,就已经建立了一个RT-Thread模板。可以在此基础上开发嵌入式应用程序了,添加自定义的线程、设备驱动和组件。在开发过程中,可能需要参考RT-Thread的文档和示例代码,以便更好地了解其功能和使用方法。
初学者可以先看RT-Thread内核入门指南学一下。
八、Finsh控制台的使用
8.1. 简介
RT-Thread控制台是我非常喜欢的一个功能,他可以让你和嵌入式设备产生交互,可以用来调试和查看系统信息。就有点像平时Windows的cmd命令或者linux的控制台中断(Console)。
在立创梁山派中,用uart0来和PC机通讯,用串口默认参数115200-8-1-N,按下复位按键后就能看到RT-Thread的输出信息了:
\ | /
- RT - Thread Operating System
/ | \ 4.1.1 build Jun 26 2023 17:28:18
2006 - 2022 Copyright by RT-Thread team
2
3
4
在计算机发展的早期,图形系统出现之前,没有鼠标,甚至没有键盘。那时候人们如何与计算机交互呢?最早期的计算机使用打孔的纸条向计算机输入命令,编写程序。后来随着计算机的不断发展,显示器、键盘成为计算机的标准配置,但此时的操作系统还不支持图形界面,计算机先驱们开发了一种软件,它接受用户输入的命令,解释之后,传递给操作系统,并将操作系统执行的结果返回给用户。这个程序像一层外壳包裹在操作系统的外面,所以它被称为 shell。
嵌入式设备通常需要将开发板与 PC 机连接起来通讯,常见连接方式包括:串口、USB、以太网、Wi-Fi 等。一个灵活的 shell 也应该支持在多种连接方式上工作。有了 shell,就像在开发者和计算机之间架起了一座沟通的桥梁,开发者能很方便的获取系统的运行情况,并通过命令控制系统的运行。特别是在调试阶段,有了 shell,开发者除了能更快的定位到问题之外,也能利用 shell 调用测试函数,改变测试函数的参数,减少代码的烧录次数,缩短项目的开发时间。
FinSH 是 RT-Thread 的命令行组件(shell),正是基于上面这些考虑而诞生的,FinSH 的发音为 [ˈfɪnʃ]。读完本章,我们会对 FinSH 的工作方式以及如何导出自己的命令到 FinSH 有更加深入的了解。
更多请查看RT-Thread官方文档中对Finsh的介绍。
Finsh控制台也是一个单独的线程。
8.2. 检查小车的数据情况
连接线连接好后,打开 03_软件工具与调试工具 介绍里面的MobaXterm,先测试电机状态信息是否正常。
- 输入 mcn list,查看话题是否正常刷新。
- 输入 mcn echo encoder_m1_topic,控制台将会持续显示编码器数据。
- 转动小车前进方向右边的轮子,数值将会变化,并且因为有PID速度环的存在,电机会抵抗这个旋转。
输入ctrl+z就可以退出循环打印话题数据了。同理,输入其他话题名称就可以查看相应的话题数据是否正常了。
8.3. 制作Finsh控制台命令
一个最简单的自定义的 msh 命令如下,随意放在哪个.c函数里面都可以:
void hello(void)
{
rt_kprintf("hello RT-Thread!\n");
}
MSH_CMD_EXPORT(hello , say hello to RT-Thread);
2
3
4
5
6
按送药小车的使用来说,比如配置好了电机PWM,想在运行中改变PWM的值,通过串口传入参数就可以修改了,如下面的代码块所示:
/**
- @brief 电机的控制室命令,可以通过控制台输入命令来临时改变PWM值驱动电机转动
- @note None
- @param argc:发给当前函数 命令行 总的参数的个数,他的值永远>=1。
argv: 是个字符串数组,用来存放指向字符串参数的指针数组,每一个元素指向一个以空格为分割的参数。
-如argv[0]指向程序运行的函数名称。
-如argv[1]指向解析出来的第一个参数,argv[2]指向再接下来的一个参数。
- @retval None
*/
static void motor_test(int argc, char**argv)
{
int16_t pwm_value = 0;
/* 检查输入的变量是否有两个 */
if (argc < 3)
{
rt_kprintf("Please input'motor_test <motor1|motor2> <value(-1000 ~ +1000)>'\n");
return;
}
if (!rt_strcmp(argv[1], "motor1"))
{
pwm_value = atoi(argv[2]);
motor1_pwm_value_set(pwm_value);
}
else if (!rt_strcmp(argv[1], "motor2"))
{
pwm_value = atoi(argv[2]);
motor2_pwm_value_set(pwm_value);
}
else
{/* 输入的是其他内容 */
rt_kprintf("Please input'motor_test <motor1|motor2> <value(-1000 ~ +1000)>'\n");
}
}
//导出命令到控制台
MSH_CMD_EXPORT(motor_test, motor test sample : motor_test motor1|motor2 in1|in2 puls);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
上面这段代码是一个用于测试电机控制的函数,通过控制台输入命令来临时改变 PWM(脉宽调制)值,从而驱动电机转动。它接收两个参数:argc
和 argv
。argc
表示传递给当前函数的命令行参数的个数,argv
是一个字符串数组,用于存放指向各个命令行参数的指针。
首先检查是否输入了两个参数,如果没有输入两个参数,则提示用户输入正确的命令格式。接下来,函数根据第一个参数(argv[1]
)判断要控制的电机是 motor1 还是 motor2,并将第二个参数(argv[2]
)转换为整数,作为 PWM 值。然后,根据选择的电机调用相应的设置 PWM 值的函数(motor1_pwm_value_set()
或 motor2_pwm_value_set()
)。如果输入的参数不符合预期,会提示用户输入正确的命令格式。
最后一行代码 MSH_CMD_EXPORT(motor_test, motor test sample : motor_test motor1|motor2 in1|in2 puls);
将 motor_test
函数导出到控制台,让我们可以在控制台中直接输入命令调用该函数。
九、如何计算轮子转速
推荐查看资料:编码器计数原理与电机测速原理——多图解析
在这里,我们选用的电机是直流减速电机带霍尔编码器接口,GD32也带硬件正交编码器。
9.1. 简要介绍
正交编码器(Quadrature Encoder)是一种常用于测量机械设备(比如电机、滑台,伸缩杆等)旋转或线性运动的速度和位置的传感器。它具有两个输出信号通道A和B,它们分别输出正交相位差90度的脉冲信号。通过测量这些脉冲信号,可以获得设备的运动速度和方向。关于正交编码器测速,主要有三种方法:M法、T法和M/T法。
- M法(脉冲计数法):M法是一种基于脉冲计数的测速方法。在一个固定的时间间隔内,记录编码器输出的脉冲数量,然后将脉冲数量除以时间间隔以获得脉冲频率。最后,将脉冲频率转换为实际的速度值。
优点 | 缺点 |
- 实现简单,只需一个计数器和一个定时器。 - 对于低速运动,可以获得较高的分辨率。 | - 对于高速运动,可能会出现计数溢出的问题。 - 测速结果受定时器时间间隔影响,较长的时间间隔可能导致较低的测速分辨率。 |
- T法(周期测量法):T法是一种基于脉冲周期测量的测速方法。通过测量两个相邻脉冲之间的时间间隔,然后将时间间隔倒数以获得脉冲频率。最后,将脉冲频率转换为实际的速度值。
优点 | 缺点 |
- 对于高速运动,可以获得较高的测速分辨率。 - 测速结果实时性较好。 | - 实现相对复杂,需要使用输入捕获功能或外部中断来测量脉冲间隔。 - 对于低速运动,可能会出现测量精度较低的问题。 |
- M/T法(脉冲计数与周期测量相结合的方法):M/T法结合了M法和T法的特点,通过同时测量脉冲计数和周期来计算速度。在低速运动时,采用M法获得较高的测速分辨率;在高速运动时,采用T法获得较高的测速实时性。根据实际速度或脉冲频率切换测速方法。
优点 | 缺点 |
- 适用于宽速度范围的测量,兼具M法和T法的优点。 - 既能获得较高的测速分辨率,又能保证较好的实时性。 | - 实现相对复杂,需要同时使用计数器、定时器和输入捕获功能或外部中断。 |
M法、T法和M/T法都是常用的正交编码器测速方法,它们各有优缺点。根据实际应用需求,可以选择合适的测速方法以获得最佳的测速性能。
9.2. 用伪代码实现测速
宏定义及公共变量
#include "gd32fxxx.h"
#define MOTOR_PULSE_PER_REVOLUTION 13
#define MOTOR_REDUCTION_RATIO 20
#define MOTOR_WHEEL_STRAIGHT_MM 64.0
#define TIME_INTERVAL_MS 20
#define PULSE_TO_METER_FACTOR (MOTOR_WHEEL_STRAIGHT_MM * 3.1415926 / (1000 * MOTOR_PULSE_PER_REVOLUTION * MOTOR_REDUCTION_RATIO * 4))
volatile uint32_t pulse_count = 0;
volatile uint32_t last_pulse_time = 0;
2
3
4
5
6
7
8
9
10
11
1. M法(脉冲计数法)
void encoder_init(void) {
// 初始化硬件正交编码器功能,使能四倍频
}
void timer_init(void) {
// 初始化定时器,设置固定的时间间隔为20ms
// 配置中断优先级和使能中断
}
void TIMERx_IRQHandler(void) { // 定时器中断处理函数
// 检查定时器更新中断标志
if (timer_interrupt_flag_get(TIMERx, TIMER_INT_FLAG_UP) != RESET) {
// 计算速度,单位为m/s
float speed = (float)pulse_count / (TIME_INTERVAL_MS / 1000.0) * PULSE_TO_METER_FACTOR;
// 重置脉冲计数
pulse_count = 0;
// 清除定时器更新中断标志
timer_interrupt_flag_clear(TIMERx, TIMER_INT_FLAG_UP);
}
}
int main(void) {
// 初始化编码器
encoder_init();
// 初始化定时器
timer_init();
while (1) {
// 处理其他任务或进入低功耗模式
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2. T法(周期测量法)
****void encoder_init(void) {
// 初始化硬件正交编码器功能,使能四倍频
// 初始化输入捕获,用于测量编码器A通道的脉冲周期
// 配置中断优先级和使能中断
}
void TIMERx_IRQHandler(void) { // 输入捕获中断处理函数
// 检查输入捕获中断标志
if (timer_interrupt_flag_get(TIMERx, TIMER_INT_FLAG_CCx) != RESET) {
// 读取当前脉冲时间
uint32_t current_pulse_time = timer_channel_capture_value_register_read(TIMERx, TIMER_CH_x);
// 计算脉冲周期
uint32_t pulse_period = current_pulse_time - last_pulse_time;
// 更新上一次脉冲时间
last_pulse_time = current_pulse_time;
// 计算速度,单位为m/s
float speed = 1.0 / pulse_period * PULSE_TO_METER_FACTOR;
// 清除输入捕获中断标志
timer_interrupt_flag_clear(TIMERx, TIMER_INT_FLAG_CCx);
}
}
int main(void) {
// 初始化编码器
encoder_init();
while (1) {
// 处理其他任务或进入低功耗模式
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
3. M/T法
void encoder_init(void) {
// 初始化硬件正交编码器功能,使能四倍频
// 初始化输入捕获,用于测量编码器A通道的脉冲周期
// 配置中断优先级和使能中断
}
void timer_init(void) {
// 初始化定时器,设置固定的时间间隔为20ms
// 配置中断优先级和使能中断
}
void TIMERx_IRQHandler(void) { // 输入捕获中断处理函数
// 检查输入捕获中断标志
if (timer_interrupt_flag_get(TIMERx, TIMER_INT_FLAG_CCx) != RESET) {
// 读取当前脉冲时间
uint32_t current_pulse_time = timer_channel_capture_value_register_read(TIMERx, TIMER_CH_x);
// 计算脉冲周期
uint32_t pulse_period = current_pulse_time - last_pulse_time;
// 更新上一次脉冲时间
last_pulse_time = current_pulse_time;
// 增加脉冲计数
pulse_count++;
// 清除输入捕获中断标志
timer_interrupt_flag_clear(TIMERx, TIMER_INT_FLAG_CCx);
}
}
void TIMERy_IRQHandler(void) { // 定时器中断处理函数
// 检查定时器更新中断标志
if (timer_interrupt_flag_get(TIMERy, TIMER_INT_FLAG_UP) != RESET) {
// 计算速度,单位为m/s
float speed = (float)pulse_count / (float)pulse_period * PULSE_TO_METER_FACTOR;
// 重置脉冲计数
pulse_count = 0;
// 清除定时器更新中断标志
timer_interrupt_flag_clear(TIMERy, TIMER_INT_FLAG_UP);
}
}
int main(void) {
// 初始化编码器
encoder_init();
// 初始化定时器
timer_init();
while (1) {
// 处理其他任务或进入低功耗模式
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
9.3. 实际使用的程序
实际使用中我用的是M法测速,详情请看2_code->applications->bsp_encoder.c
文件。
- 在文件开头引用了一些头文件,并定义了一些宏和结构体,包括编码器状态结构体(encoder_state_t)和uMCN的主题定义(encoder_m1_topic、encoder_m2_topic)。
- 在
encoder_gpio_init
函数中,对编码器的GPIO进行初始化,包括设置引脚的工作模式和复用功能。 - 在
encoder_timer_init
函数中,对编码器的定时器进行初始化。这些定时器用于计算编码器的计数值和测量速度。函数中分别初始化了两个编码器(M1和M2)的定时器,并配置了输入捕获通道和编码器模式。还启用了定时器中断,并将相应的中断处理函数激活。 encoder_count_timer_init
函数用于初始化一个用于计数的定时器。5.M1_TIMER_IRQHANDLER
和M2_TIMER_IRQHANDLER
是编码器定时器的中断处理函数。在中断处理函数中,根据定时器的计数值和重载值来判断编码器的溢出情况,并更新编码器的计数值。然后清除定时器的中断标志位。ENCODER_COUNT_TIMER_IRQHANDLER
是计数定时器的中断处理函数。在该函数中,根据定时器的中断标志位进行编码器计数值的更新,并计算编码器的速度。最后,清除计数定时器的中断标志位。set_encoder_struct_to_default
函数用于将编码器状态结构体初始化为默认值。encoder_topic_echo
函数是一个用于打印编码器状态的回调函数。它通过调用mcn_copy_from_hub
函数从主题中获取编码器状态,并打印相关信息。encoder_init
函数是编码器的初始化函数。在该函数中,调用了前面提到的各个初始化函数,并将编码器状态结构体初始化为默认值。然后,通过调用mcn_advertise
函数将回调函数注册为主题的广播函数,以便在接收到编码器状态更新时能够打印相关信息。- 最后的
INIT_APP_EXPORT
就是让GD32启动时,自动进行编码器的初始化,这是RT-Thread一个很方便的功能。
十、电机/舵机/蜂鸣器的驱动
10.1. 啥是PWM?
PWM(Pulse Width Modulation,脉宽调制)是一种在嵌入式系统中常用的技术,它可以用来模拟模拟信号,控制设备的功率输出或者实现对设备的精确控制。PWM信号是一种类似于方波的信号,具有固定的频率,但脉冲宽度(占空比)可以调整。在一定频率下,我们可以通过调整这个占空比来改变他的有效电压,在一定程度上可以实现D/A转换(数字量转模拟量,不过一般来说都是用DAC,立创梁山派自带DAC)。
- 频率(Frequency):指PWM信号在一秒内循环的次数。单位是赫兹(Hz)。
- 周期(Period):指一个完整的PWM信号的时间长度,与频率成反比。单位是秒(s)。
- 脉宽(Pulse Width):指PWM信号中高电平(通常为1)的时间长度。单位是秒(s)或毫秒(ms)。
- 占空比(Duty Ratio):表示在一个完整的PWM信号周期内,高电平(通常为1)所占的时间比例。占空比 = (脉宽 / 周期)x 100%。
- 上升沿(Rising Edge):PWM信号从低电平跳变到高电平的瞬间,通常用来作为触发事件。
- 下降沿(Falling Edge):PWM信号从高电平跳变到低电平的瞬间,也常被用作触发事件。
- 正脉冲宽度(Positive Pulse Width):PWM信号中高电平的持续时间。
- 负脉冲宽度(Negative Pulse Width):PWM信号中低电平的持续时间。
在嵌入式系统中,PWM的应用场景非常广泛,例如:
PWM应用
- 电机控制:通过调整PWM的占空比,可以精确控制直流电机的转速。占空比越高,电机转速越快;占空比越低,电机转速越慢。
- LED亮度调节:通过调整PWM的占空比,可以实现对LED灯的亮度调节。占空比越高,LED灯越亮;占空比越低,LED灯越暗。
举个例子:假设我们有一个LED灯,想实现亮度调节。我们可以生成一个PWM信号,频率为1kHz(周期为1ms),然后通过调整占空比来实现LED灯的亮度调节。如果占空比为100%,那么LED灯将一直处于亮的状态;如果占空比为50%,那么LED灯将以1ms为周期,半亮半暗;如果占空比为0%,那么LED灯将一直处于熄灭状态。通过不断调整占空比,就可以实现LED灯的亮度调节。如果PWM信号的频率过低,我们可能会感觉到它在闪烁。所以,在设计PWM驱动的LED灯时,一般会选择较高的频率来避免可见的闪烁。通常我们是看不到它闪烁的,这主要是因为两个原因:第一个是PWM信号的频率够高,第二个是人眼的视觉暂留效应。即当光源瞬间消失时,我们还能在极短的时间内感知到光源。这种效应导致人眼对快速闪烁的光源产生平滑的感觉。当PWM频率足够高时,视觉暂留效应会使我们感觉到LED灯的亮度是连续的。
10.2. 手机如何控制屏幕亮度?
我们平时一直摸的手机LCD屏幕大部分就是用PWM来调节的。在智能手机屏幕中,PWM调光和DC调光是两种常见的屏幕亮度调节技术。它们的主要区别在于亮度调节的实现方式。
PWM调光: 如之前所说,PWM(脉宽调制)调光是通过调整占空比来控制屏幕亮度的。在这种方法中,屏幕背光源会周期性地开启和关闭。占空比越高,背光源亮的时间越长,屏幕亮度越高;占空比越低,背光源亮的时间越短,屏幕亮度越低。
DC调光: DC(直流)调光是通过调整屏幕背光源的电流来实现亮度调节的。在这种方法中,背光源会一直保持开启状态,但电流的大小会改变,从而调整屏幕亮度。
这两种调光技术各有优缺点:
PWM调光优缺点
- 优点:能实现较高的亮度范围和对比度,通常在高亮度下表现更好。
- 缺点:在低频率下,PWM调光可能导致屏幕闪烁,对部分人来说可能引起眼睛疲劳和不适。此外,对于快速拍照或录像时,PWM调光可能导致出现条纹或闪烁的现象。
DC调光优缺点
- 优点:在低亮度下表现更好,因为屏幕背光源一直保持开启状态,不会出现闪烁现象,对眼睛更友好。
- 缺点:在高亮度下,对比度和亮度范围可能不如PWM调光好。此外,DC调光可能导致背光源的寿命降低和能耗略高。
各种技术的选择取决于手机制造商的考虑和市场需求。有一些高端手机是用的混合调光,在高亮度的模式下用DC调光,亮度低于一定值后用PWM调光。
10.3. 获取定时器的频率
要想输出确定PWM频率的波,首先要知道当前所用定时器的时钟频率,查看GD32F4xx用户手册中的图4-2.时钟树:
送药小车项目中,用的是外部时钟,APB1和APB2配置如下所示:
//截取至system_gd32f4xx.c
system_clock_240m_25m_hxtal();
/* AHB = SYSCLK */
RCU_CFG0 |= RCU_AHB_CKSYS_DIV1;
/* APB2 = AHB/2 */
RCU_CFG0 |= RCU_APB2_CKAHB_DIV2;
/* APB1 = AHB/4 */
RCU_CFG0 |= RCU_APB1_CKAHB_DIV4;
2
3
4
5
6
7
8
9
10
可知AHB为240Mhz,定时器的时钟有两个路线,一路定时器的时钟从APB1过来,又因为上面这个TIMERSEL时钟源选择这个位,所以APB1下时钟频率为120MHz,也就是TIMER1,2,3,4,5,6,11,12,13是120MHz。另一路定时器时钟从APB2过来,TIMER0,7,8,9,10时钟频率为240MHz。
10.4. 电机驱动的PWM配置
代码中这里用的是定时器的输出PWM功能,选用EAPWM(边沿对齐PWM),EAPWM 的周期(频率)由 TIMERx_CAR 寄存器值决定,占空比由 TIMERx_CHxCV 寄存器值决定。
上图中的Cx OUT就是对应的引脚输出的PWM波了,在PWM MODE0模式下,当CNT(计数器值)小于CHxVAL引脚输出低电平,当CNT(计数器值)大于CHxVAL引脚就输出高电平。
该项目所用到的电机驱动芯片AT8870或者AT8236最高可以接收100KHz的频率。所以配置PWM频率时不能超过100KHz。
首先我们需要知道当前所用定时器的时钟频率,在这个项目中,要驱动两个电机,每个电机驱动需要两路PWM值。电机驱动1用的是定时器8,结合上面的基础知识,他的时钟频率是240MHz。电机驱动2用的是定时器11。他的时钟频率是120MHz。分别都是用的相应定时器的通道1和通道2,详情看程序沃。
在下面的代码中,预分频(prescaler)和周期(period)都是从0开始计数的,所以一分频就是0,四分频是3,写成下面的形式只是为了表示更直观。定时器周期(period)是定时器计数的最大值,当计数器的值到达这个值时会重新开始计数。一般的电机驱动PWM频率20KHz就够用,这里我就都配置成60KHz了。
//定时器8时钟频率配置关键代码
timer_param_type.period = 1000-1; //60kHz
timer_param_type.prescaler = 4-1; //240Mhz/4=60MHz
//定时器11时钟频率配置关键代码
timer_param_type.period = 1000-1; //60kHz
timer_param_type.prescaler = 2-1; //120Mhz/2=60MHz
2
3
4
5
6
7
在下面的代码中,配置了定时器的通道0,关键的配置设置了ocpolarity = TIMER_OC_POLARITY_HIGH;,timer_channel_output_pulse_value_config(M1_MOTOR_TIMER, TIMER_CH_0, 1000);相当于把占空比设置为了100%。相当于把这个引脚拉高了。当电机驱动的两个引脚都为高电平时,会进入刹车模式,这时候转动车轮能感觉到明显的阻力。
timer_channel_output_struct_para_init(&timer_oc_init_struct);
timer_oc_init_struct.ocpolarity = TIMER_OC_POLARITY_HIGH;
timer_oc_init_struct.outputstate = TIMER_CCX_ENABLE;
timer_oc_init_struct.outputnstate = TIMER_CCXN_DISABLE;
timer_oc_init_struct.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
timer_oc_init_struct.ocidlestate = TIMER_OC_IDLE_STATE_LOW;
timer_oc_init_struct.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
timer_channel_output_config(M1_MOTOR_TIMER, TIMER_CH_0, &timer_oc_init_struct);
timer_channel_output_pulse_value_config(M1_MOTOR_TIMER, TIMER_CH_0, 1000);
timer_channel_output_mode_config(M1_MOTOR_TIMER,TIMER_CH_0,TIMER_OC_MODE_PWM0);
timer_channel_output_shadow_config(M1_MOTOR_TIMER,TIMER_CH_0,TIMER_OC_SHADOW_DISABLE);
2
3
4
5
6
7
8
9
10
11
12
其他通道的配置也和这个一样。
详情请看2_code->applications->bsp_motor.c文件。
代码简要介绍:
motor_pwm_gpio_init():
用于初始化电机驱动的PWM控制引脚。该函数通过配置引脚的模式和复用功能,将引脚设置为PWM输出模式。motor_timer_init():
用于初始化电机驱动的硬件定时器。该函数通过配置定时器的参数,包括时钟分频、计数方向、周期和占空比等,设置定时器为PWM模式。motor1_in1_pwm_pulse_set()、motor1_in2_pwm_pulse_set()、motor2_in1_pwm_pulse_set()、motor2_in2_pwm_pulse_set():
分别用于设置电机1和电机2的PWM脉宽值。这些函数通过配置定时器的通道和脉冲值,控制对应引脚的PWM输出。motor1_pwm_value_set()、motor2_pwm_value_set():
用于调节电机1和电机2的PWM值,即控制电机的速度。通过调整对应引脚的PWM脉宽值来实现速度调节。motor_test():
用于通过控制台输入命令来临时改变PWM值驱动电机转动。根据输入的命令和参数,调用相应的函数来控制电机的运动。
10.5. 舵机的PWM配置
什么是舵机?
舵机(Servo Motor)是一种特殊的电机,它可以精确地控制旋转角度。舵机广泛应用于模型、航空模型、遥控车和其他需要精确角度控制的领域。 舵机的工作原理主要基于脉冲宽度调制(PWM)信号。PWM信号由高电平和低电平组成,其中高电平的时长称为脉冲宽度。通过调节脉冲宽度,可以控制舵机的旋转角度。也有通过串口通讯来控制的舵机,会比较贵。 舵机通常由电机、减速器、电子电路和输出轴组成。舵机的基础知识:
- 工作原理:舵机内部有一个电机驱动减速器转动,减速器的输出轴与电位器相连,电位器会随着输出轴的转动而发生变化,变化的电位器信号被反馈给驱动电路,电路会根据电位器的信号调整电机的转速,使输出轴停在设定的位置上。
- 控制信号:舵机的控制信号通常是一个 PWM 脉冲信号,信号的周期为 20ms,脉宽为 1ms 到 2ms 之间,其中 1ms 表示输出角度为最小值,2ms 表示输出角度为最大值,1.5ms 表示输出角度为中间值。
- 输出角度:舵机的输出角度通常为 0 到 180 度之间,不同的舵机有不同的输出角度范围。
- 扭矩:舵机的扭矩是指舵机输出轴能够承受的最大力矩,不同的舵机有不同的扭矩大小。
- 电源电压:小功率舵机的电源电压一般为 4.8V 到 6V 之间,不同的舵机有不同的电源电压范围,这次用的ES08A II舵机就是5V供电的。
具体代码配置
定时器7的时钟频率是240MHz,在下面的代码中,舵机需要的周期是20ms也就是50Hz
//定时器7时钟频率配置关键代码
timer_param_type.period = 1200-1; //50Hz
timer_param_type.prescaler = 4000-1; //240Mhz/4000=60000Hz
2
3
PWM的通道配置和上面电机驱动的配置一样。 设置具体的脉宽就是调用timer_channel_output_pulse_value_config(SERVO_TIMER, TIMER_CH_0, SERVO_FIND_LINE_ANGLE);
来改变舵机转动的角度。
详情请看
2_code->applications->bsp_servo.c
文件。
- 首先,包含了一些头文件,包括stdio.h、rtthread.h、board.h等,这些头文件提供了所需的库函数和定义。
- 然后定义了两个静态函数:
servo_pwm_gpio_init()
和servo_timer_init()
。这些函数用于初始化舵机的GPIO和定时器。 servo_pwm_gpio_init()
函数中,首先使能了舵机的GPIO时钟,然后配置了GPIO的模式和复用功能。最后,设置了输出选项,包括输出类型、输出速度等。servo_timer_init()
函数中,首先使能了舵机所使用的定时器的时钟。然后进行了定时器的初始化设置,包括计数方向、计数模式、计数周期、预分频等参数。接着,配置了定时器的通道输出模式和脉冲值,并设置了定时器的输出极性和空闲状态。servo1_pwm_pulse_set()
和servo2_pwm_pulse_set()
函数分别用于设置舵机1和舵机2的脉冲宽度。这些函数通过调用timer_channel_output_pulse_value_config()
函数来设置定时器的通道输出脉冲值,从而控制舵机的角度。servo_test()
函数是一个用于测试舵机控制的命令函数。通过命令行输入参数来选择要控制的舵机和脉冲宽度,然后调用相应的设置函数来控制舵机。- 最后,定义了一个
servo_init()
函数,用于初始化舵机控制。该函数调用了servo_pwm_gpio_init()
和servo_timer_init()
函数来进行初始化操作。
10.6. 蜂鸣器的PWM配置
什么是蜂鸣器?
蜂鸣器是一种常见的声音输出设备,可以发出各种声音或音调。它们广泛应用于家用电器、电子设备、汽车、安全系统等领域。以下是蜂鸣器的主要种类及其使用场景。 蜂鸣器主要分为两大类:有源蜂鸣器和无源蜂鸣器。
有源蜂鸣器(Active Buzzer)
- 有源蜂鸣器内部集成了一个振荡电路,当直接接入电源时就可以发出声音。由于内部已经集成了振荡电路,有源蜂鸣器的控制相对简单,只需要提供一个恒定的电源电压即可。但是,这种蜂鸣器的音调和音量调节较为有限。
无源蜂鸣器(Passive Buzzer)
- 与有源蜂鸣器不同,无源蜂鸣器没有内置振荡电路。要使无源蜂鸣器发声,需要提供一个外部的交流信号(如方波或PWM信号)。这种蜂鸣器的优点是可以通过调整外部信号的频率和占空比来实现更丰富的音调和音量控制。
我这里选用的无源蜂鸣器,他的驱动频率是4000Hz。
具体的代码配置
定时器12的时钟频率是240MHz,在下面的代码中,无源蜂鸣器需要的发声最佳频率是4000Hz。
//定时器12时钟频率配置关键代码
timer_param_type.period = 1000-1; //4000Hz
timer_param_type.prescaler = 30-1; //120Mhz/30=4000000Hz
2
3
PWM的通道配置和上面电机驱动的配置一样。
详情请看
2_code->applications->bsp_beep.c
文件。
beep_pwm_gpio_init
函数:该函数用于初始化蜂鸣器的GPIO引脚。具体操作包括使能蜂鸣器所在的GPIO时钟、配置GPIO引脚为复用功能、设置GPIO输出选项等。beep_timer_init
函数:该函数用于初始化蜂鸣器的定时器(timer)。具体操作包括使能蜂鸣器定时器所在的时钟、重置定时器、配置定时器参数(如计数模式、时钟分频、计数方向、周期和预分频值等)、初始化定时器、配置定时器通道输出参数(如极性、输出使能、闲置状态等)、使能定时器等。beep_pwm_pulse_set
函数:该函数用于设置蜂鸣器的脉冲宽度。具体操作是通过配置定时器通道的脉冲值来控制蜂鸣器的输出。beep
函数:该函数用于控制蜂鸣器发出声音。具体操作是设置蜂鸣器的脉冲宽度为500,延时一段时间后再将脉冲宽度设置为0,实现蜂鸣器的开关控制。beep_test
函数:该函数是一个用于测试的函数,用于通过命令行输入参数来设置蜂鸣器的脉冲宽度。具体操作是通过输入参数获取脉冲宽度值,然后调用beep_pwm_pulse_set
函数设置蜂鸣器的脉冲宽度。beep_init
函数:该函数用于初始化蜂鸣器。具体操作是调用beep_pwm_gpio_init
函数和beep_timer_init
函数来初始化蜂鸣器的GPIO引脚和定时器。INIT_BOARD_EXPORT(beep_init)
:这是一个用于在系统初始化阶段自动执行的宏,将beep_init
函数注册为系统初始化函数,以便在系统启动时自动初始化蜂鸣器。
十一、电机PID速度与位置环调试
11.1. 什么是PID?
PID(Proportional-Integral-Derivative)控制是一种广泛应用于工业和嵌入式系统的控制算法。它通过比较期望的设定点和实际的过程变量之间的差异(称为误差)来调整控制系统的输出。PID控制器结合了比例(P)、积分(I)和微分(D)三种控制方式,以实现对系统的精确控制。
PID控制的三个主要组成部分:
- 比例控制(P):比例控制是根据误差的大小来调整控制器输出。误差越大,输出调整幅度越大。这种控制方式有助于快速地减小误差,但可能导致系统在设定点附近来回波动,无法消除静差。
- 积分控制(I):积分控制是根据误差累积的总量来调整控制器输出。当系统存在持续的静差时,积分控制有助于消除这种误差,使系统最终达到设定点。但过度的积分控制可能导致系统响应变慢或发生过冲。
- 微分控制(D):微分控制是根据误差变化的速度来调整控制器输出。当误差变化速度较快时,微分控制可以抑制系统过冲和振荡。但微分控制对噪声敏感,可能导致输出不稳定。
线性PID算法可以用下式表示:
其中
- Kp是比例控制
- Ki是积分控制
- Kd是微分控制
- e是误差
- t:目前时间
- 丆:是积分变数
上面的式子是连续型的PID公式,但是在嵌入式系统中是不可能连续的,所有的都是数字的离散量。针对PID计算来说,都是间隔一定时间去计算的,改变的只是间隔时间的大小。
离散型的位置式PID和增量式PID公式是将连续时间域的PID公式转换为离散时间域,以便于在数字控制系统中实现。以下是离散型的位置式PID和增量式PID公式以及解释。
11.2. 离散型位置式PID公式
离散型位置式PID(绝对式PID)公式如下:
u[k] = Kp * e[k] + Ki * T * Σe[k] + Kd * (e[k] - e[k-1]) / T
其中,u[k]
是第k
个采样时刻的控制器输出,e[k]
是第k
个采样时刻的误差(设定点与过程变量之差),Kp
、Ki
和Kd
分别是比例、积分和微分系数,T
表示采样时间间隔。
在离散型位置式PID控制器中,比例项直接由当前误差计算,积分项由误差累加求和并乘以采样时间间隔,微分项由当前误差和上一个误差的差分除以采样时间间隔计算。
11.3. 离散型增量式PID公式
离散型增量式PID(速度式PID)公式如下:
Δu[k] = Kp * (e[k] - e[k-1]) + Ki * T * e[k] + Kd * (e[k] - 2 * e[k-1] + e[k-2]) / T
其中,Δu[k]
是第k
个采样时刻的控制器输出增量,其他符号含义与上面的离散型位置式PID相同。
在离散型增量式PID控制器中,比例项由当前误差和上一个误差的差来计算,积分项由当前误差乘以采样时间间隔计算,微分项由当前误差、上一个误差和再上一个误差的差分除以采样时间间隔计算。这种控制器适用于需要避免输出突变和过冲的场景。
11.4. 怎么调节PID呢?
如果某个 P,I,D 参数偏离了原点的 PID 值,控制效果就会变化。控制效果与各个参数的规律如图所示。截取自:链接
当然,如果你能记录PID的曲线的话,就可以导入matlab进行自动调节pid参数,网上有挺多教程,大家可以搜一搜。这里我们就只介绍靠经验来调节了(简称瞎鼓捣)。
比较常用的有试错法,这个是一种经验性的方法,通过尝试不同的PID参数值,找到合适的参数设置。基本步骤如下:
- 首先,将I和D参数设置为0,只保留P参数。
- 增加P参数,直到系统的响应时间变得合适。过高的P参数会导致系统振荡,过低的P参数会导致系统响应慢。
- 接着,增加I参数,以减小稳态误差。适当调整I参数,直到系统达到满意的性能。
- 最后,增加D参数,以降低系统的超调量和提高响应速度。适当调整D参数,直到系统达到满意的性能。
假设我们需要控制一个直流减速电机的速度,为了达到某个设定值。我们将使用PID控制器来调节电机的驱动PWM,来达到所需的速度。
简要步骤如下:
- 初始化参数:首先将Kp,Ki,Kd全部设置为0,这样PID控制器不产生任何控制作用。
- 调节比例增益Kp:逐渐增加Kp的值,直到系统开始出现持续的振荡(速度在设定值附近上下波动)。此时记录下Kp的值。
- 调节积分增益Ki:将Kp设置为刚刚Kp值的一半,然后逐渐增加Ki的值,直到系统的静态误差(速度与设定值的差距)变得很小。过大的Ki值可能导致系统响应过慢或振荡,此时需要适当减小Ki值。
- 调节微分增益Kd:保持当前的Kp和Ki值不变,逐渐增加Kd的值,直到系统的动态性能满足要求(例如,要求速度在设定值附近的波动范围和响应时间)。过大的Kd值可能导致系统响应过于敏感或噪声敏感,此时需要适当减小Kd值。
- 微调参数:以上步骤得到的Kp,Ki,Kd值可能仍然不是最优解,因此还需要根据实际系统的性能要求进行微调。通常情况下,可以先调整Kp和Ki,然后再调整Kd。在调整过程中,需要关注系统的稳定性、阻尼、响应速度、过冲等性能指标。
11.5. 三个参数影响什么?
这里就以恒温热水壶来举例说明了。
Kp(比例增益)
比例项是误差与控制器输出之间的线性关系。增大Kp会使系统响应速度更快,但可能会导致更大的过冲和振荡。减小Kp可以减少过冲和振荡,但会降低响应速度,甚至导致系统无法达到设定点(静态误差)。
以恒温热水壶为例,如果比例增益Kp设置得较大,当温度偏离设定点时,恒温热水壶会迅速地增加或减少加热功率。这可能导致温度在设定点附近发生过冲和振荡。而较小的Kp值可以减少过冲和振荡,但恒温器对温度偏差的响应速度会变慢。
Ki(积分增益)
积分项考虑了误差的累积效应,可以消除静态误差。增大Ki可以加快误差积分,使系统更快地达到设定点,但过大的Ki会导致积分过程过于敏感,引起系统的振荡和不稳定。
在上面恒温热水壶示例中,如果存在静态误差(即使加热持续了很久,温度仍无法达到设定点),可以通过增大积分增益Ki来消除这种静态误差,让水壶的加热功率再大一些。然而,过大的Ki值可能会导致恒温器对温度波动过于敏感,从而引起系统的振荡。
Kd(微分增益)
微分项关注误差的变化速度,可以预测系统的未来行为。增大Kd可以提高系统的阻尼性能,减小过冲和振荡,但过大的Kd会导致系统对噪声过于敏感,引入不稳定性。
在恒温热水壶示例中,如果系统在接近设定点时出现过冲和振荡,可以通过增大微分增益Kd来减小这些现象。微分项可以预测系统的未来行为,从而提前调整加热功率。然而,过大的Kd值可能会使恒温器对温度噪声过于敏感,从而影响系统的稳定性。
11.6. 什么是串级PID?
串级PID控制是将多个PID控制器按照层级结构连接起来的控制策略。在串级PID控制中,一个PID控制器的输出被用作另一个PID控制器的输入。这种通常用于具有多个互相影响的控制变量的系统,可以提高系统的性能和稳定性。
以平衡小车为例,平衡小车一般是一个具有两个控制目标的系统:保持车体竖直和控制车体的位置。我们可以使用串级PID控制来实现这两个目标。
平衡小车的串级PID控制可以分为两个层级:
- 内环控制: 内环控制负责保持车体竖直。这个环节的PID控制器接收车体的倾角和倾角速度作为输入,并输出一个控制信号,用于调整驱动电机的转速,以达到保持车体竖直的目的。倾角控制是一个快速响应的过程,因此内环控制器需要具有较高的响应速度。
- 外环控制: 外环控制负责控制车体的位置。这个环节的PID控制器接收车体的实际位置和目标位置作为输入,并输出一个目标倾角。这个目标倾角被用作内环控制器的设定点。位置控制是一个较慢的过程,因此外环控制器的响应速度可以较低。
- 在平衡小车的串级PID控制中,首先需要调整内环控制器的参数,以保证车体能够迅速地恢复到竖直状态。接下来,调整外环控制器的参数,以实现对车体位置的控制。在调整参数的过程中,需要关注小车的稳定性、响应速度、过冲等性能指标。
串级PID控制可以提高系统的稳定性,他可以确保外环期望的值可以稳定执行,比如说如果平衡小车只有外环控制的话,他输出的PWM值太小的话很有可能会导致小车根本无法克服摩擦力运动,如果他把这个速度值给到内环控制后,就由内环来确保小车以这个速度来执行。在送药小车的电机控制中,速度环就是内环,外环可以是位置环,寻红线环,角度环等。
11.7. 实现PID的程序
在这里实现了位置式PID的程序,它在仓库里的2_code->applications->module->pid
里面。
.H文件
#ifndef _POSITIONAL_PID_H
#define _POSITIONAL_PID_H
#include "stdint.h"
typedef enum {
PID_DISABLE, /* PID失能 */
PID_ENABLE, /* PID使能 */
} positional_pid_status;
typedef struct _positional_pid_params_t{
char control;
float kp;
float ki;
float kd;
float target;
float measure;
float last_measure;
float error;
float last_error;
float p_out;
float i_out;
float d_out;
float output;
float last_output;
float output_max;
float output_min;
float integral_limit;
float dead_zone;
void (*positional_pid_params_init)(
struct _positional_pid_params_t *positional_pid, float kp, float ki,
float kd, float dead_zone, float output_max, float output_min);
void (*positional_pid_set_value)(
struct _positional_pid_params_t *positional_pid, float kp, float ki,
float kd);
void (*positional_pid_control)(
struct _positional_pid_params_t *positional_pid,
positional_pid_status status);
} positional_pid_params_t;
void positional_pid_init(positional_pid_params_t *positional_pid, float kp,
float ki, float kd,
float dead_zone, float output_max, float output_min);
float positional_pid_compute(positional_pid_params_t *positional_pid,
float target, float measure);
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
这个是位置式PID的C语言实现。上面定义了一个名为positional_pid_params_t
的结构体,用于存储 PID 控制器的参数和状态信息。具体包括:
- 控制器使能标志(
control
)当其值为PID_ENABLE时,表示控制器处于启用状态,会根据目标值和测量值计算输出;当其值为PID_DISABLE时,表示控制器处于禁用状态,输出为 0。 - 比例、积分、微分增益(
kp
、ki
、kd
)分别表示 PID 控制器的比例增益、积分增益和微分增益。这些增益参数决定了控制器对误差的反应程度。 - 目标值和实际测量值(
target
、measure
、last_measure
)target表示期望的目标值,measure表示实际测量到的当前值,last_measure表示上一次测量到的值。这些值用于计算误差,误差是PID想要减小的值。 - 当前误差和上一次误差(
error
、last_error
)error表示当前误差,即目标值与测量值之差,last_error表示上一次的误差。这些误差值用于计算 PID 控制器的比例、积分、微分输出。 - 比例、积分、微分输出(
p_out
、i_out
、d_out
)分别表示 PID 控制器的比例输出、积分输出和微分输出。这些输出值是根据当前误差、增益参数以及之前的误差计算得到的。 - 控制器输出和上一次输出(
output
、last_output
)output表示控制器的总输出,它是比例输出、积分输出和微分输出的和;last_output表示上一次的总输出。这些值在控制器使能时会返回给调用函数。 - 输出最大值和最小值(
output_max、output_min
)分别表示控制器输出的最大值和最小值。这些值用于限制控制器的输出范围,防止因过大的输出导致系统不稳定。 - 积分限幅(
integral_limit
)表示积分限幅,用于避免积分饱和现象,在c程序中还没使用到, - 死区(
dead_zone
)表示死区大小。当误差的绝对值小于死区大小时,控制器输出为 0。这有助于减小微小误差对控制器输出的影响。 - 初始化函数指针(
positional_pid_params_init
) - 设定 PID 参数的函数指针(
positional_pid_set_value
) - 控制 PID 的函数指针(
positional_pid_control
)
.C文件
#include "positional_pid.h"
#include <math.h>
#define ABS(x) ((x > 0) ? x : -x)
static void positional_pid_params_init(positional_pid_params_t *positional_pid,
float kp, float ki, float kd,
float dead_zone, float output_max,
float output_min)
{
// 初始化 PID 参数
positional_pid->kp = kp;
positional_pid->ki = ki;
positional_pid->kd = kd;
// 初始化死区、输出上限和输出下限
positional_pid->dead_zone = dead_zone;
positional_pid->output_max = output_max;
positional_pid->output_min = output_min;
// 初始化目标值和输出值
positional_pid->target = 0;
positional_pid->output = 0;
}
static void positional_pid_set_value(positional_pid_params_t *positional_pid,
float kp, float ki, float kd)
{
// 设置 PID 参数
positional_pid->kp = kp;
positional_pid->ki = ki;
positional_pid->kd = kd;
}
float positional_pid_compute(positional_pid_params_t *positional_pid,
float target, float measure)
{
if (positional_pid->control == PID_ENABLE)
{
// 设置目标值和测量值
positional_pid->target = target;
positional_pid->measure = measure;
// 计算误差
positional_pid->error =
positional_pid->target - positional_pid->measure;
if (ABS(positional_pid->error) > positional_pid->dead_zone)
{
// 计算比例项
positional_pid->p_out = positional_pid->kp * positional_pid->error;
// 计算积分项
positional_pid->i_out += positional_pid->ki * positional_pid->error;
// 计算微分项
positional_pid->d_out =
positional_pid->kd
* (positional_pid->error - positional_pid->last_error);
// 计算总输出
positional_pid->output = positional_pid->p_out
+ positional_pid->i_out
+ positional_pid->d_out;
}
// 限制输出在输出上限和输出下限之间
if (positional_pid->output > positional_pid->output_max)
{
positional_pid->output = positional_pid->output_max;
}
if (positional_pid->output < (positional_pid->output_min))
{
positional_pid->output = positional_pid->output_min;
}
// 更新上一次测量值、输出值和误差值
positional_pid->last_measure = positional_pid->measure;
positional_pid->last_output = positional_pid->output;
positional_pid->last_error = positional_pid->error;
return positional_pid->output;
}
else
{
return 0.0f;
}
}
void positional_pid_control(positional_pid_params_t *positional_pid,positional_pid_status status)
{
// 控制 PID 的使能状态
positional_pid->control = status;
}
void positional_pid_init(positional_pid_params_t *positional_pid, float kp,
float ki, float kd,
float dead_zone, float output_max, float output_min)
{
// 初始化 PID 控制器
positional_pid->positional_pid_params_init = positional_pid_params_init;
positional_pid->positional_pid_set_value = positional_pid_set_value;
positional_pid->positional_pid_control = positional_pid_control;
// 调用初始化函数设置参数
positional_pid->positional_pid_params_init(
positional_pid, kp, ki, kd, dead_zone, output_max, output_min);
// 默认使能 PID 控制器
positional_pid->control = PID_ENABLE;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
positional_pid_params_init
初始化 PID 控制器的参数.传入的参数值会被分别赋值到相应的结构体成员变量中,包括增益值、死区大小、输出上限和下限等。positional_pid_set_value
设置 PID 控制器的增益参数。传入的参数值(kp
、ki
、kd
)会被分别赋值到相应的结构体成员变量中。positional_pid_compute
计算 PID 控制器的输出。是 PID 控制器的核心计算逻辑。首先,根据传入的目标值target
和实际测量值measure
,计算当前误差error
和上一次误差last_error
。然后,根据误差和增益参数,分别计算比例输出、积分输出和微分输出。接下来,将这三个输出相加得到总输出,再根据输出限制范围(output_max
和output_min
)对输出进行限幅。最后,将一些必要的值存储到结构体中,以便下次计算使用,并返回当前输出。positional_pid_control
用于控制 PID 控制器的启用和禁用状态。传入的参数control
表示控制器的状态。如果设置为PID_DISABLE
,则将输出设置为 0。positional_pid_init
初始化 PID 控制器,设置函数指针和调用初始化函数(将函数指针分别指向相应的函数实现)并默认使能PID控制器。
11.8. 实现单电机的速度和位置环
这个问题是一个非常关键的问题。这是因为小车电机的速度直接影响了小车的行驶速度和精度,而行驶速度和精度又是小车比赛中的关键因素之一。如果小车电机的速度不稳定,那么小车的行驶速度和精度就会受到影响,导致小车无法在比赛中达到最佳表现。因此,在进行智能车比赛之前,需要先解决小车电机速度问题,保证小车电机的速度稳定、准确,并且能够根据需要进行调整。并不是说给一个确定的电压或者确定的PWM值就能让电机保持到一个准确的转速,比如每个电机的绕线,轴承,机械性能,负载能力,各个接触点的摩擦力这些都有差别,小车轮子所受到的摩擦力的不同也会影响小车的前进速度。
结合之前的文档06如何计算小车轮子转速.md和07电机驱动-舵机-蜂鸣器PWM配置.md我们现在已经可以获取到轮子转速和改变PWM值从而驱动电机转动。通过编码器我们可以进行电机的转速测量,通过改变PWM值我们可以让电机提高或者降低转速。在这个条件下,我们的目标值是让小车电机稳定在一个转速,测量值是小车当前由编码器测量得到的实际速度,输出值是电机的PWM值。通过调节PID的三个参数,让目标值变化时,实际值可以又快又准的接近目标值。
简单来说,PID就是测量出实际值,设定一个目标值,输出一个驱动值,让实际值尽量和目标值靠近。
具体实现在仓库里的
2_code->applications-pid_thread.c
里面。
简要步骤如下:
引入必要的头文件。
定义需要的速度调节PID变量:positional_pid_params_t motor2_speed_pid;和实际的电机速度float motor1_speed_target = 0.0f;
通过uMCN订阅发布机制申明编码器1的信息:MCN_DECLARE(encoder_m1_topic);
创建一个PID线程,初始化速度环的PID配置:
c// 速度环配置 // motor1 速度环 pid 参数初始化 positional_pid_init(&motor1_speed_pid, 125, 25, 80, 0.01, 1000, -1000); // 修改motor1 速度环 kp ki kd motor1_speed_pid.positional_pid_set_value(&motor1_speed_pid, 125, 25, 80);
1
2
3
4
5开启一个软件定时器,用固定频率去计算PID的输出值,在送药小车中我用的间隔是20ms,也就是以50Hz的频率去计算。
c//从uMCN获取编码器信息 if (mcn_poll(encoder1_nod)) { mcn_copy(MCN_HUB(encoder_m1_topic), encoder1_nod, &encoder1_value); } // 计算电机1的PWM输出值 pwm_value = (int16_t)positional_pid_compute( &motor1_speed_pid, motor1_speed_target, encoder1_value.speed); // 应用电机1的PWM值,实际给电机驱动的PWM波 motor1_pwm_value_set(pwm_value);
1
2
3
4
5
6
7
8
9
10
11
速度环就到这里,位置环和上面的类似,也要定义相应的PID变量,初始化PID参数,在开启的软件定时器中需要再加入以下代码:
位置环依靠编码器累积下来的数值确定轮子具体转了多少个脉冲,根据这个脉冲来计算小车电机转过的位置。位置环的PID,测量值就实际的编码器脉冲,目标值是想要控制电机选择多少脉冲,输出值是速度环的速度。
position_result_motor1_speed_target = (positional_pid_compute(
&motor1_position_pid, (float)motor1_position_target,
(float)position1_measurement));
2
3
11.9. 怎样进行调试呢?
可以做一个finsh控制台指令来在电机运行时改变PID的值:类似car_find_lines_pid.Kp = atof(argv[3]);
下面这样:
void pid_test(int argc, char **argv)
{
static float temp_speed;
static uint32_t temp_delay_ms = 10;
// pid_test speed forward|backward|left|right time
/* 检查输入的变量是否有四个 */
if (argc < 4)
{
rt_kprintf("Please input'pid_test speed <forward|backward|left|right> "
"speed_value <time>'\n");
return;
}
if (!rt_strcmp(argv[1], "speed"))
{
if (!rt_strcmp(argv[2], "forward"))
{
motor1_speed_target = atof(argv[3]);
motor2_speed_target = atof(argv[3]);
rt_thread_mdelay(atoi(argv[4]));
motor1_speed_target = 0.0f;
motor2_speed_target = 0.0f;
}
else if (!rt_strcmp(argv[2], "backward"))
{
motor1_speed_target = -atof(argv[3]);
motor2_speed_target = -atof(argv[3]);
rt_thread_mdelay(atoi(argv[4]));
motor1_speed_target = 0.0f;
motor2_speed_target = 0.0f;
}
else if (!rt_strcmp(argv[2], "left"))
{
motor1_speed_target = atof(argv[3]);
motor2_speed_target = -atof(argv[3]);
rt_thread_mdelay(atoi(argv[4]));
motor1_speed_target = 0.0f;
motor2_speed_target = 0.0f;
}
else if (!rt_strcmp(argv[2], "right"))
{
motor1_speed_target = -atof(argv[3]);
motor2_speed_target = atof(argv[3]);
rt_thread_mdelay(atoi(argv[4]));
motor1_speed_target = 0.0f;
motor2_speed_target = 0.0f;
}
}
if (!rt_strcmp(argv[1], "position"))
{
if (!rt_strcmp(argv[2], "go"))
{
car_go_position(atoi(argv[3]));
temp_speed = 0;
while (temp_speed <= atof(argv[4]))
{
temp_speed += 0.01f;
set_car_position_max_speed(temp_speed);
rt_thread_mdelay(temp_delay_ms);
}
}
else if (!rt_strcmp(argv[2], "spin"))
{
car_spin_position(atoi(argv[3]));
}
}
if (!rt_strcmp(argv[1], "angle"))
{
if (!rt_strcmp(argv[2], "pid"))
{
car_find_lines_pid.Kp = atof(argv[3]);
car_find_lines_pid.Ki = atof(argv[4]);
car_find_lines_pid.Kd = atof(argv[5]);
car_find_lines_pid.MaxOut = atof(argv[6]);
}
else if (!rt_strcmp(argv[2], "absolute"))
{
turn_target = atof(argv[3]);
if (turn_target >= 180)
{
turn_target = 180;
}
else if (turn_target <= -180)
{
turn_target = -180;
}
}
else if (!rt_strcmp(argv[2], "run"))
{
raw_target_speed = atof(argv[3]);
rt_thread_mdelay(atoi(argv[4]));
raw_target_speed = 0;
}
}
else
{ /* 输入的是其他内容 */
rt_kprintf("Please input'pid_test speed <forward|backward|left|right> "
"speed_value <time>'\n");
}
}
MSH_CMD_EXPORT(pid_test, pid test sample
: servo_test<servo1 | servo2><pulse(32 - 150)>);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
当然,想快一点的话就直接用MDK进入硬件仿真,直接把具体的变量添加到Watch窗口进行修改,下面就直接用进入硬件调试直接修改变量的方法来调试。
以调试单电机速度环为例
这里的测量值是小车当前由编码器测量得到的实际速度,输出值是电机的PWM值。通过调节PID的三个参数,让目标值变化时,实际值可以又快又准的接近目标值。
我们可以在pid线程中加入如下代码,通过串口打印出轮子1的设定速度和正交编码器1测出来的轮子转速。
while (1)
{
mcn_copy(MCN_HUB(encoder_m1_topic), encoder1_nod, &encoder1_value);
rt_kprintf("motor1:%f,%f\n",raw_target_speed,encoder1_value.speed);
rt_thread_mdelay(20);
}
2
3
4
5
6
在MDK中,把motor1_speed_pid
,raw_target_speed
添加到Watch变量中,这样我们就可以实时修改了。
打开在第五章中介绍的软件VOFA+
,选择数据引擎为FireWater
,数据接口为串口
,串口号选择你具体使用的串口号
,波特率选115200
,点击左上角的小蓝点就可以打开了.
然后在左侧的控件中拖拽一个波形图到上面的tab里面,在控件上面右键选择如下图所示。
接下来就是通过MDK改变motor1_speed_pid里面的Kp,Ki,Kd和raw_target_speed这几个变量了。
下面的动图是我改变raw_target_speed的结果,I0是目标值,I1是测量值。
速度环调试完后再调位置环:
可以在pid线程中加入如下代码,通过串口打印出轮子1的目标位置和正交编码器1测出来的累积脉冲。
while (1)
{
rt_kprintf("motor1:%d,%d\n",motor1_position_target,position1_measurement);
rt_thread_mdelay(20);
}
2
3
4
5
在MDK中,把motor1_position_pid
,motor1_position_target
添加到Watch变量中,这样我们就可以实时修改了。
和上面调节速度环一样打开VOFA+开始调试。接下来就是通过MDK改变motor1_position_pid
里面的Kp,Ki,Kd和raw_target_speed
这几个变量了。
下面的动图是我改变raw_target_speed
的结果,I0是目标值,I1是测量值。
可以看到他到达目标值的速度有点慢,这是因为电机转动快了会导致轮子打滑,这里我用了比较保守的值,设置了motor1_position_pid
的output_max
为3,也就是说这个轮子速度最快是3m/s,当然这个值在你自己调试的时候可以适量加大,目前我选择的这个电机,在PWM最高时可以达到八点几米每秒。当然这里我设置的Kp是偏小的,主要也是为了让轮子的速度变化小一点,平缓一些,防止打滑情况的出现。
十二、国产IMU移植及姿态解算
12.1. 什么是IMU?
IMU(惯性测量单元,Inertial Measurement Unit) 是一种测量设备,用于测量物体的加速度、角速度和磁场等数据。IMU通常包括加速度计、陀螺仪和磁力计(九轴IMU才有)。
- 加速度计:用来测量物体在各个方向上的加速度。加速度计可以检测到重力加速度,因此也可以用来判断物体的姿态(比如我们手机通过知道它是竖直还是水平放置来切换屏幕的显示方向)。加速度计本质上来说测量的是力而不是加速度。只不过是因为加速度会恰好产生惯性力,这个惯性力被IMU里面的MEMS结构测量。
- 陀螺仪:用来测量物体围绕各个轴旋转的速度。陀螺仪能告诉我们物体的旋转情况,例如我们转动手机时,陀螺仪可以检测到这个动作。
- 磁力计(可选):用来测量地球磁场的强度和方向。磁力计可以帮助我们判断物体相对于地球的方向,类似于指南针。当然,在地球南北极等地方,由于磁力线与地球重力方向相重合,他也就没用了。
推荐学习链接:imu_guide
12.2. IMU能做什么
典型的应用场景有以下这些:
- 航空航天:在飞行器、卫星和火箭等领域,IMU用于测量和控制飞行器的姿态,以确保正确的导航和稳定性。
- 无人机:无人机依赖IMU来测量其姿态,以实现稳定的飞行控制。IMU还可以与GPS等其他传感器结合,提供更准确的导航和定位信息。
- 机器人:移动机器人、自动导航车辆和机械臂等机器人需要知道他自己在空间中的姿态和位置,IMU可以提供这些信息来实现精确的运动控制和导航。
- 智能手机和平板电脑:IMU用于检测设备的移动和旋转,从而实现屏幕旋转、游戏控制、运动识别等功能。
- 虚拟现实(VR)和增强现实(AR):在VR和AR系统中,IMU用于追踪用户的头部和手部动作,实现身临其境的沉浸式体验,目前这种是低成本的解决方案,高大上一点的都用空间追踪或者视觉识别。
- 运动分析和健康监测:IMU可以安装在运动员身上,用于测量运动过程中的姿态、速度和加速度等参数,从而分析运动员的动作和技能。同时,IMU也可以用于健康监测,如步行计数、跑步速度等。最常见的的就是各种手环手表了,目前有一些运动鞋在鞋垫下面也安装了这种检测运动状态的部件,可以测量步频,步幅等运动状态信息。
- 导航系统:IMU与GPS等其他导航传感器结合,可以实现更高精度的定位和导航,这在室内导航、隧道和城市高楼间等GPS信号遮挡的环境中尤为重要。比如小车在过隧道时GPS信号就没有了,这时候通过IMU的计算来实现短时间内的位置更新。
- 汽车安全:IMU在汽车安全系统中的应用包括电子稳定控制系统(ESC)和防抱死刹车系统(ABS),通过监测车辆姿态的变化,可以实时调整刹车和动力,提高行车安全性。
IMU的性能和价格差距可以非常大,主要取决于应用场景的要求和预算。主要是下面这几个关键指标: - 精度:不同应用场景对IMU的精度要求不同。例如,航空航天、高精度导航和高级机器人领域需要极高的精度,而智能手机和消费级VR/AR设备则可以接受较低的精度。高精度的IMU通常价格较高。
- 稳定性:稳定性指的是IMU在一段时间内,输出数据的准确性和一致性。在某些关键应用中,如航空航天和无人机,稳定性至关重要。高稳定性的IMU具有更高的价格。
- 更新率:不同应用场景对IMU数据更新率的要求不同。例如,高速运动控制和VR/AR应用可能需要更高的更新率,以实现更流畅的动作跟踪。高更新率的IMU价格更高。
- 功耗:对于便携式设备和电池供电系统,功耗是一个重要的考虑因素。低功耗的IMU可以延长设备的使用时间,但会牺牲一定的性能。低功耗且高性能的IMU通常价格较高。
- 尺寸:根据应用场景的空间限制,可能需要不同尺寸的IMU。在某些紧凑型设备中,需要更小的IMU,当然在其他应用中,尺寸可能不是关键因素。微型化的高性能IMU价格一般会更高。
根据上面这些因素,IMU的价格可以从几美元(用于消费级设备,如智能手机和游戏控制器)到数千甚至数万美元(用于航空航天和高精度导航等关键应用)不等。
类别 | 成本 | 稳定性 | 性能参数 | 应用领域 |
---|---|---|---|---|
消费级 | 低(10美元以下) | 较低 | 精度:较低 更新率:较低至中等 功耗:较低 尺寸:较小 | 智能手机 游戏控制器 消费级VR/AR设备 |
工业级 | 中等(几十至数百美元) | 中等 | 精度:中等 更新率:中等至高 功耗:中等 尺寸:中等 | 机器人 无人机 汽车安全系统/室内导航 |
航空级 | 高(数千至数万美元) | 高 | 精度:高 更新率:高 功耗:中等至高 尺寸:中等至大 | 航空航天 高精度导航 精密测量和监测系统 |
12.3. 国产IMU介绍
为了配合梁山派的全国产化,这里打开立创商城进入姿态传感器/陀螺仪目录,第一栏的 品牌/产地 里面可以看到一个 QST(上海矽睿)这个是国产的,他家目前最新的IMU是QMI8658A,就选这个了。
QMI8658是一款低噪声、高带宽的六轴惯性测量单元(IMU),包含一个三轴陀螺仪和一个三轴加速计,采用2.5 x 3.0 x 0.86 mm 14-pin LGA 封装。支持多种通讯接口:I3C、I2C和SPI。可满足惯性导航高精度低功耗的要求,即使在低速率情况下的数据输出也可保持高精度。
打开他的数据手册,打开第二页,可以了解到他的关键参数有以下这些:
QMI8658A是一款6D MEMS惯性测量单元(IMU),具有低噪声、宽带宽和内置运动协处理器(没找到实际代码,不清楚是不是确实支持)。它适用于消费级和工业级应用。
关键参数
- 陀螺仪噪声:低至13 mdps/√Hz(陀螺仪噪声是指测量角速度时产生的随机误差,这个数值表示在每平方根赫兹的频带宽度内,陀螺仪输出的角速度噪声的均方根(RMS)值是13 mdps。陀螺仪噪声越低,得到的角速度测量结果就越准确,从而提高姿态估计的准确性。)
- 加速度计噪声:低至150µg/√Hz(加速度计噪声是指测量线性加速度时产生的随机误差,这个数值表示在每平方根赫兹的频带宽度内,加速度计输出的线性加速度噪声的均方根(RMS)值是150µg。加速度计噪声越低,得到的线性加速度测量结果就越准确,从而提高运动跟踪的准确性。)
- 通信接口:支持MIPI™ I3C, I2C, 以及3线或4线SPI
- 陀螺仪和加速度计具有可编程数据率和滤波器的数字信号处理
- 尺寸:2.5 x 3.0 x 0.86 mm,14-pin LGA封装
- 1536字节的FIFO缓冲区,可降低系统功耗
- 集成计步器、敲击、任意运动、无运动、显著运动检测功能
- 陀螺仪测量范围:±16°/s至±2048°/s;加速度计测量范围:±2 g至±16 g
- 低功耗模式,用于有效的电源管理
- 可编程采样率和滤波器
- 集成温度传感器
- 广泛的工作温度范围:-40°C至85°C
应用领域
- 智能手机
- 游戏控制器、遥控器和指向设备
- 机器人吸尘器
- 电动自行车和滑板车
- 蓝牙耳机
- 汽车安全系统
- 玩具
作为对比,世界上第一款集成了6轴IMU-MPU6050的陀螺仪噪声是5 mdps/√Hz,加速度计噪声是400μg/√Hz。在手册里,可以看到最初版本是2010年11月发布的。而国产IMU-QMI8658A是2022年1月发布的。
最出名的IMU就是mpu6050了,他是InvenSense 公司推出的全球首款整合 6 轴运动处理的组件,应美盛公司已经被日本的TDK公司买了,现在出的最新的IMU都是ICM开头的,比如ICM-42688,可以看一下这个网页的介绍,mpu开头的型号和icm-2x开头的型号已经不被推荐了(有些都已经停产了)。科学技术发展很快,这些传感器的制造工艺和精度也在飞速提升。所以大家学习或者做产品的话最好都是用新产品,电子产品都是买新不买旧嘛,更新换代太快了。传感器不好用是会难为死软件算法的。
我们可以看一下TDK官方网站的选型表,最新的ICM-42688-P的陀螺仪噪声2.8mdps/√Hz,加速度计噪声是(XY: 65, Z: 70)/√Hz。国产的QMI8658和国外差距确实还不小,我们选型的时候主要还是看使用场景和预算,选国产的好处就是价格便宜而且几乎没有断供风险嘛。不满足使用要求的话使用国外的IMU也无可厚非。
12.4. IMU移植到梁山派上
这些传感器厂家都会自己做一个驱动的,我们可以从厂家的驱动基础上改。但是我没有在他们的官网找到QMI8658A驱动,是在淘宝卖评估板的下面找到了驱动代码。
具体代码在
2_Code->applications->sensor->qmi8658
里面。
这个IMU是支持IIC和SPI的,为了能获得更多的数据量,我在画电路板的时候就选择了SPI通讯。具体可以去立创开源平台查看。
这里我把接口文件单独成了一个文件qmi8658x_port.c
,这里实现的函数是库文件qmi8658.c
需要用到的通讯接口,具体代码如下:
#include <stdio.h>
#include <rtthread.h>
#include <board.h>
#include <gd32f4xx_libopt.h>
#include <qmi8658x_port.h>
void spi_gpio_config(void)
{
// SPI CS gpio
rcu_periph_clock_enable(SPI_CS_GPIO_CLK);
gpio_mode_set(SPI_CS_GPIO_PORT, GPIO_MODE_OUTPUT, SPI_CS_GPIO_PUPD,
SPI_CS_GPIO_PIN);
gpio_output_options_set(SPI_CS_GPIO_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_25MHZ,
SPI_CS_GPIO_PIN);
// SPI SCK gpio init
rcu_periph_clock_enable(SPI_SCK_GPIO_CLK);
gpio_mode_set(SPI_SCK_GPIO_PORT, GPIO_MODE_AF, SPI_SCK_GPIO_PUPD,
SPI_SCK_GPIO_PIN);
gpio_af_set(SPI_SCK_GPIO_PORT, SPI_SCK_GPIO_ALT_FUNC, SPI_SCK_GPIO_PIN);
// SPI MISO gpio init
rcu_periph_clock_enable(SPI_MISO_GPIO_CLK);
gpio_mode_set(SPI_MISO_GPIO_PORT, GPIO_MODE_AF, SPI_MISO_GPIO_PUPD,
SPI_MISO_GPIO_PIN);
gpio_af_set(SPI_MISO_GPIO_PORT, SPI_MISO_GPIO_ALT_FUNC, SPI_MISO_GPIO_PIN);
// SPI MOSI gpio init
rcu_periph_clock_enable(SPI_MOSI_GPIO_CLK);
gpio_mode_set(SPI_MOSI_GPIO_PORT, GPIO_MODE_AF, SPI_MOSI_GPIO_PUPD,
SPI_MOSI_GPIO_PIN);
gpio_af_set(SPI_MOSI_GPIO_PORT, SPI_MOSI_GPIO_ALT_FUNC, SPI_MOSI_GPIO_PIN);
SET_SPI_NSS_HIGH();
}
void spi_config(void)
{
spi_parameter_struct spi_init_struct;
rcu_periph_clock_enable(RCU_SPI3);
/* configure SPI3 parameter */
spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX;
spi_init_struct.device_mode = SPI_MASTER;
spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT;
spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE;
spi_init_struct.nss = SPI_NSS_SOFT;
spi_init_struct.prescale = SPI_PSC_16;
spi_init_struct.endian = SPI_ENDIAN_MSB;
spi_init(SPI3, &spi_init_struct);
}
uint8_t spi_send_byte(uint8_t byte)
{
/* loop while data register in not emplty */
while (RESET == spi_i2s_flag_get(SPI3, SPI_FLAG_TBE))
;
/* send byte through the SPI3 peripheral */
spi_i2s_data_transmit(SPI3, byte);
/* wait to receive a byte */
while (RESET == spi_i2s_flag_get(SPI3, SPI_FLAG_RBNE))
;
/* return the byte read from the SPI bus */
return (spi_i2s_data_receive(SPI3));
}
uint8_t spi_flash_read_byte(void) { return (spi_send_byte(0xff)); }
uint8_t qst_imu_spi_write(uint8_t reg, uint8_t value)
{
spi_send_byte(reg & 0x7f);
spi_send_byte(value);
return 0;
}
uint8_t qst_imu_spi_write_bytes(uint8_t reg, uint8_t *value, uint8_t len)
{
uint8_t *p = value;
static uint32_t i = 0;
for (i = 0; i < len; i++)
{
qst_imu_spi_write(reg, *p++);
}
return 0;
}
uint8_t qst_imu_spi_read(uint32_t reg, uint8_t value)
{
spi_send_byte(reg | 0x80);
return spi_flash_read_byte();
}
uint8_t qst_imu_spi_read_bytes(uint8_t reg, void *buffer, uint8_t len)
{
static uint32_t i = 0;
uint8_t *p = buffer;
spi_send_byte(reg | 0x80);
for (i = 0; i < len; i++)
{
*p++ = spi_flash_read_byte();
}
return 0;
}
int sensor_spi_init(void)
{
spi_gpio_config();
spi_config();
spi_enable(SPI3);
return RT_EOK;
}
INIT_BOARD_EXPORT(sensor_spi_init);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
想要读写国产IMU-QMI8658A,实现上面的这些接口就可以了,下面分段解释下上面的代码:
- 首先,包含了一些必要的头文件,例如
stdio.h
,rtthread.h
(实时操作系统RT-Thread)以及IMU传感器的接口头文件qmi8658x_port.h
等。 spi_gpio_config()
用于配置SPI接口所需的GPIO引脚。这包括CS(片选),SCK(时钟),MISO(主设备输入,从设备输出)和MOSI(主设备输出,从设备输入)引脚。然后,还设置了GPIO速度和复用功能。spi_config()
用于配置SPI接口的参数。这里使用的是SPI3接口,配置为全双工的主模式,数据帧大小为8位,查询QMI8658A的数据手册,翻到对SPI通讯介绍(Figure 18. SPI 4-Wire Single Byte Read and Write (Mode 0 and Mode 3)
),可以了解到,他只支持SPI的Mode0和Mode3。所以这里选择的时钟极性和相位为CPOL=0和CPHA=0,也就是SPI的Mode0模式,软件NSS模式,预分频系数为16,以及大端模式。spi_send_byte()
用于通过SPI接口发送一个字节,并接收并返回从SPI总线读取的一个字节。它等待传输缓冲区为空,发送一个字节,然后等待接收缓冲区非空并读取一个字节。spi_flash_read_byte()
是一个简单的包装函数,用于读取一个字节,实际上调用spi_send_byte(0xff)。发送的这个0xff主要用来产生SPI时钟,这样QMI8658返回来的数据才有意义。qst_imu_spi_write()
用于向指定的寄存器地址写入一个字节。它首先发送寄存器地址(最高位为0,表示写操作),然后发送要写入的值。这里0x7f的二进制是01111111
,任何一个数和他相与最高位都会变成0。qst_imu_spi_write_bytes()
用于向指定的寄存器地址连续写入多个字节。它循环调用qst_imu_spi_write()
函数以完成这个任务。qst_imu_spi_read()
用于从指定的寄存器地址读取一个字节。它首先发送寄存器地址(最高位为1,表示读操作),然后调用spi_flash_read_byte()
函数以获取返回的值。这里0x80
的二进制是10000000
,任何一个数和他相或最高位都会变成1。qst_imu_spi_read_bytes()
用于从指定的寄存器地址连续读取多个字节。它首先发送寄存器地址(最高位为1,表示读操作),然后循环调用spi_flash_read_byte()
将读取的字节保存到缓冲区。sensor_spi_init()
用于初始化SPI接口。它首先调用spi_gpio_config()
函数配置GPIO引脚,然后调用spi_config()
函数配置SPI参数,最后使能SPI3。这个函数直接返回RT_EOK,表示初始化成功。- 最后,
INIT_BOARD_EXPORT(sensor_spi_init)
宏用于将sensor_spi_init()
函数导出,使其在系统启动时自动执行,完成对QMI8658A的SPI接口初始化。
12.5. 什么是姿态解算?
姿态解算是一种计算物体在三维空间中的方向和角度的过程。通俗地说,姿态解算就是通过一些传感器(如陀螺仪、加速度计和磁力计)获取的数据,计算物体在空间中的朝向(即其姿态)。
我们生活中的产品比如无人机就用到了姿态解算。最常用的姿态解算算法是基于四元数和滤波算法的组合。四元数在表示旋转方面具有很好的数值稳定性和避免万向节锁的优点,这对于无人机的连续、平滑旋转至关重要。同时,由于无人机的动态特性和环境噪声,需要使用滤波算法来整合多种传感器数据(如陀螺仪、加速度计和磁力计),以提高姿态估计的准确性和鲁棒性。
扩展卡尔曼滤波(EKF)和互补滤波(CF)是在无人机控制中常用的滤波算法。EKF能够处理非线性系统,适用于结合加速度计、陀螺仪和磁力计数据,以获得更准确的姿态估计。然而,EKF的计算复杂度较高,可能不适合资源受限的嵌入式系统,立创梁山派的主控芯片GD32F470计算EKF是没问题的。相比之下,互补滤波是一种简单易实现的方法,通过加权平均加速度计和陀螺仪数据来估计物体的姿态。尽管互补滤波在某些情况下可能不如EKF准确,但其优点是计算简单,实时性好,适合无人机控制。还有一种滤波算法是卡尔曼滤波,它是一种线性滤波算法,用于估计系统的状态。在姿态解算中,卡尔曼滤波可以结合加速度计和陀螺仪数据,减少误差并提高姿态估计的准确性。
选择适用的姿态解算算法需要根据无人机的具体应用、硬件条件和性能要求来权衡。在实际使用中,还需要注意传感器的误差和噪声,通过校准和滤波等方法来提高姿态估计的准确性。
移植开源的Fusion实现姿态解算
克隆Fusion 仓库,查看他的Examples里面的 简单示例 ,可以知道,如果我们只有一个六轴IMU,那么把下面代码中gyroscope
和accelerometer
替换成IMU实际采集到的数据,宏定义SAMPLE_PERIOD
改为实际的运行间隔就可以了。
#include "../../Fusion/Fusion.h"
#include <stdbool.h>
#include <stdio.h>
#define SAMPLE_PERIOD (0.01f) // replace this with actual sample period
int main() {
FusionAhrs ahrs;
FusionAhrsInitialise(&ahrs);
while (true) { // this loop should repeat each time new gyroscope data is available
const FusionVector gyroscope = {0.0f, 0.0f, 0.0f}; // replace this with actual gyroscope data in degrees/s
const FusionVector accelerometer = {0.0f, 0.0f, 1.0f}; // replace this with actual accelerometer data in g
FusionAhrsUpdateNoMagnetometer(&ahrs, gyroscope, accelerometer, SAMPLE_PERIOD);
const FusionEuler euler = FusionQuaternionToEuler(FusionAhrsGetQuaternion(&ahrs));
printf("Roll %0.1f, Pitch %0.1f, Yaw %0.1f\n", euler.angle.roll, euler.angle.pitch, euler.angle.yaw);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在实际的21年电赛送药小车工程中,具体姿态解算在att_thread.c文件中。
/**
- @brief 姿态解算软件定时器回调函数
- @note None
- @param parameter
- @retval None
*/
static void att_thread_timer_callback(void *parameter)
{
qmi8658_read_xyz(acc, gyro);
accelerometer.array[0] = (float)((acc[0]) / 9.807f);
accelerometer.array[1] = (float)((acc[1]) / 9.807f);
accelerometer.array[2] = (float)((acc[2]) / 9.807f);
gyroscope.array[0] =
(float)((gyro[0]) * (180.0f / 3.14159265358979323846f));
gyroscope.array[1] =
(float)((gyro[1]) * (180.0f / 3.14159265358979323846f));
gyroscope.array[2] =
(float)((gyro[2]) * (180.0f / 3.14159265358979323846f));
FusionAhrsUpdateNoMagnetometer(&ahrs, gyroscope, accelerometer,
SAMPLE_PERIOD);
const FusionEuler euler =
FusionQuaternionToEuler(FusionAhrsGetQuaternion(&ahrs));
fusion_data.euler.euler.pitch = euler.angle.pitch;
fusion_data.euler.euler.roll = euler.angle.roll;
fusion_data.euler.euler.yaw = euler.angle.yaw;
fusion_data.quaternion.quaternion.q0 = ahrs.quaternion.element.w;
fusion_data.quaternion.quaternion.q1 = ahrs.quaternion.element.x;
fusion_data.quaternion.quaternion.q2 = ahrs.quaternion.element.y;
fusion_data.quaternion.quaternion.q3 = ahrs.quaternion.element.z;
mcn_publish(MCN_HUB(fusion_topic), &fusion_data);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
可以看到,IMU的数据在传给fusion做解算前做了单位换算,这是因为Fusion要的陀螺仪数据单位是degrees/s
,加速度计数据单位是g
。
后面的欧拉角 Pitch(俯仰角)、Roll(横滚角)和Yaw(偏航角) 则表示了物体的姿态信息,以无人机作为举例:
- Pitch(俯仰角):表示物体绕x轴的旋转角度。当物体的前端向上抬起时,俯仰角为正;当物体的前端向下抬起时,俯仰角为负。例如,在无人机中,改变俯仰角可以使无人机上升或下降。
- Roll(横滚角):表示物体绕y轴的旋转角度。当物体的右侧向上抬起时,横滚角为正;当物体的左侧向上抬起时,横滚角为负。例如,在无人机中,改变横滚角可以使无人机向左或向右侧倾斜。
- Yaw(偏航角):表示物体绕z轴的旋转角度。当物体的前端向左旋转时,偏航角为正;当物体的前端向右旋转时,偏航角为负。例如,在无人机中,改变偏航角可以使无人机在水平面内绕自身中心旋转。
再后面的四元数 q0, q1, q2, q3 是表示物体旋转的四元数。四元数是一种扩展的复数,用于描述和计算三维空间中的旋转。四元数具有四个分量(q0, q1, q2, q3),其中q0是标量部分,q1、q2和q3是向量部分。四元数相对于欧拉角具有以下优点:
- 避免万向节锁:欧拉角在某些特定姿态下会出现万向节锁现象,导致某个旋转自由度丢失(在某些特定姿态下,两个旋转轴可能会重合,导致一个旋转自由度丢失)。四元数则没有这个问题,让得旋转表示更加稳定和连续。
- 更高的数值稳定性:四元数在计算旋转时具有更好的数值稳定性,避免了欧拉角在接近临界值时的数值不稳定现象。
- 简化旋转计算:四元数可以通过简单的数学运算实现旋转的组合、插值和求逆等操作,计算效率高。
十三、小车角度环的调试与实现
13.1. 如何让小车90度转向
首先,在 第十二章、国产IMU移植及姿态解算
里面我们可以获取到小车的姿态信息,对应的转向信息就是Yaw(偏航角)。
而在 第十一章、电机PID速度与位置环调试
中我们也可以准确控制小车轮子的转速和转多少个脉冲(也就是转几圈)了。
这样实现小车的转向就有了两种方案了。一种是用Yaw(偏航角)作为测量值,想要转的角度作为目标值,用PID驱动小车以一定的速度去旋转来达到目标值。
第二种是在实现小车轮子位置环的基础上,控制两边轮子同时向不同的方向旋转一定的圈数(编码器脉冲),在小车轮子不打滑的情况下转过的角度就是一个确定的值,在送药小车的地图中,旋转基本都是90度的,所以这种实现方法会很简单也容易调试。
13.2. 角度环实现90度转向
在 pid_thread.c
中实现:
创建相应的PID控制参数的结构体:
positional_pid_params_t car_angle_ring_pid;
初始化控制结构体并修改kp ki kd:
//角度环配置
//角度环 pid 参数初始化
positional_pid_init(&car_angle_ring_pid, 0.015, 0, 0, 0.1, 2, -2);
//修改 角度环 kp ki kd
car_angle_ring_pid.positional_pid_set_value(&car_angle_ring_pid, 0.015, 0,
0);
2
3
4
5
6
在PID的软件定时器回调函数中计算两个轮子需要的速度:
// 小车角度环
if (car_angle_ring_enable_flag == 1)
{
static float error = 0;
error = turn_target - fusion_data.euler.euler.yaw;
// 处理角度突变
if (error > 180.0f)
error -= 360.0f;
else if (error < -180.0f)
error += 360.0f;
angle_ring_speed_target = positional_pid_compute(
&car_angle_ring_pid, error, fusion_data.euler.euler.yaw);
}
else
{
turn_target = fusion_data.euler.euler.yaw;
angle_ring_speed_target = 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
打开角度环后他就会保持Yaw为0,如下面这个图一样:
下面是设定他靠角度环旋转90度的效果:
13.3. 位置环实现90度转向
还是在 pid_thread.c
中实现: 在 08_直流减速电机PID速度环与位置环调试
已经可以让轮子转固定的脉冲(圈数)了,直接把对应的目标值加给电机1和电机2的位置目标就可以了。
// 位置式旋转,负数左转,正数右转。
void car_spin_position(int32_t _position_target)
{
motor1_position_target += -_position_target; motor2_position_target -= _position_target;
}
2
3
4
5
在实际实现中,如下面的gif图所示: