大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺
队伍名称: 智行·龙卷风
参赛队员: 杨怡,韦炳宇,许泽华
带队教师: 张志明,余有灵
§01 引言
§01 引言
1.1全国大学生智能车竞赛介绍
全国大学生智能汽车竞赛是以智能汽车为研究对象的创意性科技竞赛,是面向全国大学生的一种具有探索性工程实践活动,是教育部倡导的大学生科技竞赛之一。该竞赛以“立足培养,重在参与,鼓励探索,追求卓越”为指导思想,旨在促进高等学校素质教育,培养大学生的综合知识运用能力、基本工程实践能力和创新意识,激发大学生从事科学研究与探索的兴趣和潜能,倡导理论联系实际、求真务实的学风和团队协作的人文精神,为优秀人才的脱颖而出创造条件。
该竞赛过程包括理论设计、实际制作、整车调试、现场比赛等环节,要求学生组成团队,协同工作,初步体会一个工程性的研究开发项目从设计到实现的全过程。竞赛融科学性、趣味性和观赏性为一体,是以迅猛发展、前景广阔的汽车电子为背景,涵盖自动控制、模式识别、传感技术、电子、电气、计算机、机械与汽车等多学科专业的创意性比赛。
1.2全国大学生智能车竞赛基础四轮组介绍
基础四轮组比赛是在PVC赛道上进行,官方比赛赛道示意图如图1.1和图1.2中所示,赛道采用黑色边线和电磁进行导引。
▲ 图1.1 第十六届全国大学生智能车竞赛基础四轮组
▲ 全国大学生智能车竞赛基础四轮组赛场
选手制作的车模完成从车库触发沿着赛道运行两周,然后在返回车库。赛道元素包含多个环岛、岔路、坡道、斑马线、车库等。车模需要连续运行两圈,每次需要分别通过三岔路口的两条岔路。比赛时间从车模驶出车库到重新回到车库为止。如果车模没有能够停止在车库内停车区内,比赛时间加罚五秒钟。为此参赛车模需要无误地判断出赛道元素并顺利通过,以最短的时间完成比赛。
1.3 RT-Thread操作系统介绍
1.3.1 嵌入式实时操作系统
嵌入式实时操作系统(Embedded Real-time Operation System,一般称作RTOS),是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统作出快速响应,并控制所有实时任务协调一致运行的嵌入式操作系统 。其主要特点是提供及时响应和高可靠性,核心是任务调度,任务调度的核心是调度算法。主流的RTOS国外的有μClinux、μC/OS-II、eCos、FreeRTOS等,国内的有RT-Thread、Huawei LiteOS等。
1.3.2 RT-Thread 概述
RT-Thread,全称是 Real Time-Thread,是一款我国具有完全自主知识产权的开源嵌入式实时多线程操作系统,3.1.0 以后的版本遵循 Apache License 2.0 开源许可协议。它的基本属性之一是支持多任务,事实上,多个任务同时执行只是一种错觉,一个处理器核心在某一时刻只能运行一个任务。由于每次对一个任务的执行时间很短、任务与任务之间通过任务调度器进行非常快速地切换(调度器根据优先级决定此刻该执行的任务),就造成了这样一种错觉。在 RT-Thread 系统中,任务是通过线程实现的,RT-Thread 中的线程调度器也就是以上提到的任务调度器。经过了15年的迭代和丰富,伴随着物联网的兴起,现已经成为市面上装机量最大(超6亿台)、开发者数量最多、软硬件生态最好的物联网操作系统之一。
1.3.3 RT-Thread 架构
RT-Thread主要采用C语言编写,容易理解,移植方便,同时具有良好的可剪裁性,其架构图如图1.3中所示。Nano 版本(极简内核)仅需要 3KB Flash、1.2KB RAM 内存资源,适用于资源受限的微控制器(MCU)系统。完整版可以实现直观快速的模块化裁剪,无缝地导入丰富的软件功能包,实现类似 Android 的图形界面及触摸滑动效果、智能语音交互效果等复杂功能,适用于资源丰富的物联网设备。相较于 Linux 操作系统,RT-Thread 体积小,成本低,功耗低、启动快速。除此以外 RT-Thread 还具有实时性高、占用资源小等特点,非常适用于各种资源受限(如成本、功耗限制等)的场合。
▲ 图1.3 RT-Thread系统的架构
其中RT-Thread 内核,是 RT-Thread 的核心部分,包括了多线程及其调度、信号量、邮箱、消息队列、内存管理、定时器等。
1.3.4 RT-Thread与智能车竞赛
对于第十六届智能车竞赛来说,MM32SPIN27、CH32V103由于内存较小,因此主要适配RT-Thread Nano版本的,这样可以减少RAM的开销。RT1064、RT1021、MM32F3277在智能车系统开发过程中使用RT-Thread的好处在于一方面可以充分发挥不同芯片的性能,让智能车跑的更加顺畅,另一方面提供了更高程度的抽象,屏蔽了不同单片机底层硬件细节,使得代码逻辑更为清晰,编写调试效率更高,移植性也更好。
1.4 智能车制作情况与本报告框架
经过近一年的准备,我们在小车机械结构设计、硬件电路设计、软件算法设计等方面都有收获和进展,灵活应用了RT-Thread系统的多线程并发、软定时器、线程间同步——信号量、线程间通信——邮箱、时间片轮转等特性,制作出了结构合理、系统稳定、可以顺利完成比赛任务的基础四轮小车。凭借着稳定的发挥,我们一路冲出校赛,在华东分赛区的预赛与决赛阶段都成功完成任务,取得预赛第12、决赛第7的较好成绩,最终排名为华东赛区基础四轮组第7名,决赛成绩103.007s。
本技术报告的框架大致如图1.4所示。
▲ 图1.4 技术报告框架结构
在第一章对全国大学生智能车竞赛、基础四轮组别、RT-Thread系统进行介绍,简单说明智能车制作的情况和报告框架。第二章介绍了智能车机械结构和基本硬件电路设计,第三章说明基础四轮车软件算法开发的基本流程,将传统大while()+中断模式与基于RT-Thread系统模式开发进行比较,得出后者具有巨大优势的结论,并详细介绍如何利用RT-Thread的多线程管理、定时器、邮箱、信号量等内核特性去更好的完成小车任务,总结部分进一步深入,讲述如何利用模块化思维、系统级的通信方式进行嵌入式任务的开发。第四章介绍RT-Thread操作系统的学习和迁移过程,其中4.3.4小节重点介绍了FinSH的详细移植过程。第五章对本次智能车制作和比赛过程进行总结与反思。
§02 智能车硬件
§02 智能车硬件
智能车的机械结构和控制电路对赛车的性能影响巨大。具备一个好的机械结构平台,车身转向的灵敏性、直道行驶的稳定性、较高速度的抓地性才能得到很好地实现。因此,我们在不违反比赛规则的情况下对小车的结构进行设计和改进以使其具有良好的机械性能。同时,我们在整个系统设计过程中严格按照竞赛规范进行,本着可靠、高效的原则,在满足各个要求的情况下,尽量使设计的电路简单,PCB的效果简洁。
2.1 小车机械结构总体布局
第十六届全国大学生智能车竞赛基础四轮组指定采用新B车模,车架长28.5cm,宽16.5cm,高6.0cm; 底盘采用高强度玻纤板,具有较强的弹性和刚性;前轮调整方式简单,全车滚珠轴承、前后轮轴高度可调。驱动电机为RS540,伺服电机为SD-5舵机;轮胎经过软化剂处理,增强其耐磨性和摩擦力,车模整体质量较轻,车模照片分别如图2.1、图2.2、图2.3所示:
▲ 图2.1 四轮车模俯视图
▲ 图2.2 车模侧视图与前视图
车模机械结构具有如下特点:
- 舵机采用竖直姿态,方便控制;
- 更换新的舵机连接杆,提高舵机控制的灵敏性;
- 元件及电池布局在车身中心部分,提高车子的对称性;
- 对前轮进行调整,在保证直行稳定性的同时具有较好的过弯能力;
- 采用双电感安装板双前瞻设计,一前一后,提高小车的前瞻性;
- 安装支架高度尽量降低,最大程度降低车身重心。
2.2小车组件的安装
2.2.1电路板及电池的安装
车模的驱动板安装在车身的后侧,主板安装在车身中心偏后方,为使得整个车身的重心尽量落在车身中心以减少因速度快而产生甩尾和侧滑的现象,将两个电磁信号板安装与主板左右对称处,电池安装于主板下方偏前处,无线串口和电磁信号板均采用直插式封装,方便拆装。具体如图2.4所示:
▲ 图2.4 四轮组车模电路板及电池安装位置
2.2.2 转向舵机的安装
舵机采用竖式安装的方法,便于调节舵机的位置和控制转向,安装时,为了提高舵机控制的灵敏性,减少其延迟的时间,我们特地更换了新的舵机安装支架,使得舵机的安装位置更加契合要求,同时调整转向连杆的长度等因素,使得转动时的扭矩能够达到最佳的转向效果。
2.2.3 前轮的调节
为了进一步提高车的性能,在保证车身在直行稳定的情况下能够更加轻便的过弯,能够有较大的过弯角度,经过查阅资料和实践的检验,我们最终采用前轮外倾,主销后倾,前轮前束的机械结构 .
(1) 前轮外倾: 是指前轮安装后,其上端向外倾斜,于是前轮的旋转平面与纵向垂直平面间形成一个夹角,称之为前轮外倾角。通过前轮外倾的调节,使得前轮外倾和主销内倾相配合,可以减小主销偏距,使转向轻便。
▲ 图2.5 起来跑外倾示意图
(2) 主销后倾: 在车身纵向平面内,主销轴线上端略向后倾斜,这种现象称为主销后倾。在纵向垂直平面内,主销轴线与垂线之间的夹角叫主销后倾角。通过设置主销后倾,可以保持小车直线行驶时的稳定性,并使小车转弯后能自动回正。一般来说,后倾角越大,车速越高,车轮的稳定性越强。但是后倾角过大会造成转向沉重,所以主销后倾角不宜过大,通过实际的实践体验,我们最终将主销后倾角设置为2°~3°。
▲ 图2.6 主销后倾示意图
(3) 前轮前束: 是指前轮前端面与后端面在汽车横向方向的距离差,也可指车身前进方向与前轮平面之间的夹角,此时也称前束角。通过选择适当的前束角,可使前束引起的侧向力与车轮外倾引起的侧倾推力相互抵消,从而避免了额外的轮胎磨耗和动力的消耗,同时前轮前束还可以保证小车稳定的直线行驶,使转向轮具有自动回正的效果。经过多次尝试,智能车前轮前束的调整如图2.7所示:
▲ 图2.7 前轮前束示意图
▲ 图2.8 调整后的四轮组车模前轮前束
2.2.4 电磁电感安装板的安装与固定
为了使车具有较好的前瞻性,电磁电感安装板应尽量前置,但若距离过长,在车身行驶的过程中便极容易丢线,为了在不丢线的同时尽量提高其前瞻性,我们采用的是长短前瞻结合的方式,当长前瞻丢线时,切换到短前瞻进行控制,这样的好处的前瞻长,留给车的控制时间长,有更长的时间做出反应,提高了赛车的速度上限,同时短前瞻的应用又保证了车在过急弯的时候仍能够检测赛道并继续正常行驶。在具体安装时,我们采用碳素杆进行固定,减少支架的重量对于车身对称性和平衡性的影响。支架后半部分用两个支架安装板固定位置,前半部分从车前再引出两个固定支架,构成三角形结构,提高其稳定性,减少车在行进过程中支架的颠簸,进而提高电感测量数据的稳定性。具体安装分别如图2.9、图2.10所示:
▲ 图2.9 电磁从梦境安装板是示意图
▲ 图1.10 前侧采用三角形结构固定
2.3硬件电路设计
硬件电路系统是智能车运动控制系统的核心组件,为保证智能车稳定运行的基础,需要一个良好、稳定的硬件环境才能使得小车能平稳快速的行驶在比赛赛道上。
2.3.1 单片机系统
核心单片机子系统采用英飞凌半导体公司设计生产的TC264D芯片。该芯片采用双核TriCore架构,最高主频为200MHz,高达2.5MB的闪存与240KB的RAM,完全满足智能车控制的算力需求。为方便使用与后续更换,我们使用了逐飞科技公司生产的TC264单片机系统板,原理图如图 2.11 所示:
2.3.2 电源模块设计
硬件电路的电源由18650锂电池提供(额定电压7.4V,容量2000mAh)。由于不同电路模块中所需要的工作电压和电流量各不相同,所以我们采用了三个稳压电路将电源电压转换成各模块需要的电压。
- 使用MIC29302WU芯片将电源电压转换成6V电压,用于智能车舵机供电。输出电压计算公式为:
Vout=1.240*(R3/R5+1) (1)
▲ 图2.12 直流6V稳压电路原理图
- 使用LM1085输出5V电压,用于蜂鸣器,通信串口供电。LM1085芯片的输入电压为5.5V到10V时,输出电压的典型值为5V。电路如图2.13所示:
▲ 图2.13 直流5V稳压电路原理图
- 使用TPS76833将5V电压转换成3.3V电压,用于单片机、信号放大电路、显示屏等模块的供电。TPS76833芯片的输入电压为2.7V到10V时,输出电压为3.3V。电路如图2.14所示:
▲ 图2.14 直流3.3V稳压电路原理图
2.3.3 信号采集及放大电路模块的设计
对赛道上电磁信号的采集和处理是智能车最重要的模块之一,根据变化的磁场信号做出灵敏的检测对控制智能车在赛道上稳定运行起着至关重要的作用。
智能车比赛赛道铺设有中心电磁引导线,其中通有20kHz,100mA的交变电流。根据麦克斯韦电磁场理论,交变电流会在周围产生交变的电磁场。因此我们采用10mH的工字电感和6.8uF的小温差电容组成串联谐振电路,来实现对20kHz信号的选频和将赛道的电磁信号转换成电压,从而完成对信号的采集。接下来就是对收集到的电压信号进行滤波、放大、整流用于单片机ADC模块转换成数字量。放大电路和电感排布方案如图2.15中所示。
▲ 图2.15 电磁信号放大电路和电感排布方案
在上图的电感排布方案中,水平电感1、7主要用于检测弯道,在小车进入弯道过程中,可以根据两端感应电动势值判断智能车与赛道中心线的偏离方向及偏差量。电感4用于检测赛道中心的电磁线,当智能车偏离赛道中心线不多时,
该电感的变化程度较小,当偏离程度较大时,该电感的感应电动势值突然下降很快,因此可以根据该电感的变化情况更加精确地得出智能车与赛道中心线的偏离程度。电感3、4用于检测岔路。电感2、6主要用于引导智能车入环岛,在环岛路段,靠近环岛的那个竖直电感的感应电动势会比远离环岛的竖直电感的感应电动势大许多,可以使用这两个电感引导智能车进入环岛。
2.3.4 电机驱动电路
电机驱动电路采用双极性PWM全桥电路,可实现电机正反转以及可调占空比控制电机转速。该电路能控制电机正反转运行,具有启动快、调速精度高、动态性能好、调速静差小、调速范围大;能加速、减速、刹车、倒转;能在负载超过设定速度时,提供反向力矩、能克服电机轴承的静态摩擦力,产生非常低的转速等多方面优点。电路原理图如图2.16所示:
▲ 图2.16 双极性PWM全桥电机驱动电路
2.3.5 无线串口模块
采用NRF24L01 2.4GHz无线串口模块,用于主机与单片机之间的实时数据通信,实现实时观察车模的运行情况与无线调参等操作,节约调试时间,提高调试效率。
2.3.6编码器测速模块
我们的智能车使用龙邱智能科技的512 线 mini 型编码器,使用减速齿轮和联轴器加载到小车的动力轮上,进行小车的测速,工作电压范围 3.3V-5V。单片机通过读取编码器脉冲数来实现对智能车速度的测量。
2.3.7 PCB设计与实物图
设计各功能电路的PCB,打样后的PCB设计图和实物照片分别如下图中所示,包括:电路底板(图2.17),电磁信号放大板(图2.18),电机驱动板(图2.19),电磁信号放大板(图2.20)。
▲ 图2.17 电路板的PCB与实物图
▲ 图2.18 电磁信号放大版PCB与实物图
▲ 图2.19 电机驱动板的PCB与实物图
▲ 图2.20 电感安装板的PCB与实物
§03 RT-Thread软件
§03 RT-Thread软件
本章介绍基于RT-Thread的四轮组车模软件设计。
本章首先基于四轮车任务背景介绍软件算法,接下来分析裸机大while()+中断型开发模式的架构,并引出为什么要使用RT-Thread操作系统,后续部分从PID算法着手,相对全面地介绍基于RT-Thread的四轮车软件算法设计的各个方面。
3.1 软件算法背景介绍
四轮车组别的主要任务有数据采集类、信号处理算法类、人机交互类、控制类等,其中:
- 数据采集类任务包括摄像头图像采集、电磁信号采集、小车编码器数据采集等。
- 信号处理算法类任务主要包括图像处理(灰度、二值化),电磁信号处理(均值滤波、卡尔曼滤波、归一化)等。
- 人机交互类任务主要包括图像显示、按键、拨码开关、LED、蜂鸣器、串口通讯等。
- 控制类任务主要包括舵机与电机的控制,通过PID进行闭环计算,输出PWM波进行打角和转速的控制。
- 其他的组别还会有更多的任务,如直立车车身姿态的控制、AI机器视觉等,此处和四轮车组任务无关,不再赘述。
这样一来,一辆四轮车就具备了巡线的基本功能,再加上对特殊赛道元素的正确判断,就能初步具备完赛的能力。
▲ 图3.1 四轮车组别主要任务
3.2 裸机大while()+中断型与RT-Thread系统开发对比分析
3.2.1 裸机大while()+中断模式介绍
传统的裸机大while()+中断模式又称为前后台系统,前台指中断,后台指main()函数里的主循环while(1)。初学编程的时候老师会强调,循环一定要有退出的条件,不可以死循环。但是在嵌入式开发不用操作系统的情况下,一般都是用main函数里while(1)无限循环的方式去编程的。中断可以打断main()函数,保证一定的实时性,而中断也可以被优先级更高的中断打断。
▲ 图3.2 裸机大while() + 中断模式
3.2.2 裸机大while()+中断模式优劣势分析
在嵌入式编程发展的早期,是用裸机大while()+中断模式编程的。优点是上手容易,处理简单任务绰绰有余。
而随着计算性能的提高,嵌入式编程的发展是从简单到复杂、从单任务到多任务,加上物联网的兴起,要处理的任务也是越来越复杂。这种模式渐渐显露出弊端。
- 函数可能变得非常复杂,并且需要很长执行时间。且中断嵌套可能产生不可预测的执行时间和堆栈需求。
- 超级循环和 ISR 之间的数据交换是通过全局共享变量进行的,应用程序的程序员必须确保数据一致性。
- 超级循环可以与系统计时器(硬件计时器)轻松同步,但如果系统需要多种不同的周期时间,则会很难实现。
- 超过超级循环周期的耗时函数需要做拆分,增加软件开销,应用程序难以理解,超级循环使得应用程序变得非常复杂,因此难以扩展。
举个例子,逐飞科技做过这样一个实验。while(1)函数里放两个任务,一个是流水灯,一个是显示屏显示摄像头图像。两个任务分别运行时都非常流畅。但是同时运行时,图像刷新就变得十分卡顿。这是因为流水灯的delay()时间比较长,这段时间内cpu什么也不做,所以图像的刷新频率就显而易见的降了下来。这是一个典型的例子。随着小车工作的不断推进,while(1)里要放的任务可能越来越多,后面的任务不可避免会对前面的任务造成影响。只能按顺序执行,而不能进行有效的优先级区分。如果用中断,首先中断的数量可能会不足(比如tc264D只有4个pit中断),其次中断的嵌套也会使程序变得更加复杂,增加维护的难度。最重要的是,有很多的delay(),在这期间,cpu是什么也不做的,这是巨大的资源浪费,而且也会明显影响一些任务的执行频率。
当然,说了这么多,只用裸机大while()+中断模式能不能把车做好?答案是肯定的,历史上的许多神车都是用这种模式做出来的。但是,对于大多数同学,如果有一种效率更高,优势巨大的方法摆在面前,要不要用?答案也是肯定的。下面探讨一下相对于裸机大while()+中断模式,RT-Thread系统的巨大优势。
3.2.3 RT-Thread系统的优劣势分析
RT-Thread系统在1.3节已有详细介绍。这里着重对比传统裸机大while()+中断模式分析其优劣势。
优势有很多,正好克服了裸机大while()+中断模式的劣势。
更灵活的任务处理和更好的实时性。线程数量不受限制,优先级最大256个。首先RT-Thread系统先天就有着处理复杂任务、多任务并发的属性。可以把不同的任务拆分成不同的线程,根据优先级让系统自动调度,更好地可以对多任务进行区别对待。如果优先级配置得当,不同任务之间相互的影响可以降到最低。显著的优势在于,delay()时会将线程挂起,把cpu使用权交出去,这时候cpu可以处理其他任务,显著提高cpu的使用率。
更方便的模块化开发和团队合作。如果是团队协作开发,那么可以各自写各自的线程,最后汇总、配置优先级启动即可。模块化开发也是用了面向对象的观点,屏蔽了一些底层的实现细节,可以更专注于所要解决的任务上,代码逻辑更加清晰,后续的拓展和维护也相对省力。
可重用性。这个是比较显著的优势。不同的平台编程逻辑可能有很大不同,就智能车而言,不同的组别平台就各有不同,同一个组别每一届的平台也可能会有变化。所以对于许多打算做两年或想换组别的同学来说,就免去了痛苦的从头开始的过程,直接一键无痛移植。对于其他的比赛或项目而言,如果RT-Thread系统对该平台有适配,则熟悉的编程逻辑和风格可以让同学更加游刃有余。
丰富的软件生态。这一优点可能在智能车竞赛中不那么突出,但是如果做物联网的一些比赛,丰富的第三方库会让人拍手称快。也许目前智能车面对的任务还不够复杂,但任务越来越复杂是大趋势,一些复杂的项目也是用RT-Thread系统处理的。通过做智能车熟悉了RT-Thread操作系统,也有利于未来自身嵌入式编程的发展。
劣势则是使用有一定门槛,上手不如大while()+中断模式那么容易。不过花点时间了解一下嵌入式实时操作系统的一些特性,学习一下RT-Thread的内核使用,这个时间的投入还是非常划算的。且由于RT-Thread系统本身比较浅显易懂,再加上逐飞有比较详细的讲解和demo,所以学习和移植过程还是比较顺利的。我从刚开始接触RT-Thread到移植成功,只花了大半天时间,后续的调试又用了一个下午。对于有一点嵌入式开发经验的同学来说,可以预留出一天半时间来初步学习和移植。当然要想精进使用是需要不断学习的。所以同学不要觉得操作系统好像很高大上的样子,不明觉厉。当你开始阅读开发者文档,动手操作以后,可能就会觉得还是比较容易的。
3.3 RT-Thread系统在四轮车的部署方案
将3.1软件算法进一步抽象,总体可以概括为:“一核心,两关键,多辅助”。“一核心”指运动控制器,“两关键”指赛道环境的采集(电磁采集和摄像头图像采集、编码器采集)和控制量的输出(舵机和电机的PWM输出),“多辅助”指的是通过按键、拨码开关、led灯、显示屏、蜂鸣器、串口等一系列手段增强调试效率。使用RT-Thread的线程调度、时钟管理、线程间同步、线程间通信等内核特性,有效解决了裸机大while()加中断形式带来的管理单一、无法处理复杂多任务的执行、调试不方便等痛点,在抽象层面更高的平台上,使得多任务运行更加得心应手,编程逻辑更加清晰,调试手段更加多样灵活。
由于小车速度很快,且感知、决策、控制存在必然的顺序,所以必须保证三者的周期性唤醒及执行的先后顺序。所以用RT-Thread 操作系统提供的软定时器,timer1_pit_entry线程周期运行,周期为1个系统节拍。在定时器入口函数里面完成赛道环境的采集和处理,差比和计算,以及PWM波输出。
显示屏和串口作为display_entry线程,优先级设置为31。并且通过拨码开关决定是否显示。蜂鸣器作为buzzer_entry线程,优先级设置为20。并且通过邮箱决定是否响以及响的时间,邮箱大小为5个字节,采用 FIFO 方式进行线程等待。这里显示屏和串口优先级最低,而蜂鸣器优先级更高,比较合理。
利用FinSH在实际小车运行过程中,我们使用ps命令列出系统中所有线程信息,包括线程优先级、状态、栈的最大使用量等,以此监测智能车的线程运行情况。
下图3.3为大while+中断控制逻辑,3.4为RTT多线程同步操作系统在四轮小车上的部署方案。
▲ 图3.3 大 while +中断算法逻辑
▲ 图3.4 RT-Thread系统算法逻辑
3.4 PID运动控制器
一核心指的是运动控制器,本次比赛我们采用经典控制算法PID控制,如图3.5和图3.6中所示,根据系统输入与预定输出的偏差的大小运用比例、积分、微分计算出一个控制量,将这个控制量输入系统,获得输出量,通过反馈回路再次检测该输出量的偏差,循环上述过程,以使输出达到预定值。
▲ 图3.5 使用PID控制器的经典负反馈算法
▲ 图3.6 使用PID控制器的负反馈控制算法应用
位置式PID算法可由公式(2)表达:
而增量式PID算法可由位置式PID算法推导得到,如公式(3)中所示:
在PID算法中,比例环节P的作用是成比例地反映控制系统的偏差信号e(t),一旦产生偏差,比例控制环节立即产生控制作用以减小偏差。积分环节I的作用是消除静差,提高系统的无差度,在使用积分控制时,常常对积分项进行限幅以减少积分饱和的影响。微分环节D的作用是反映偏差信号的变化趋势,能够在偏差信号变化之前先引入一个有效的早期修正信号来提前修正偏差,加快系统的动作速度,减少调节时间。
位置式PID特点主要是一方面控制的输出与整个过去的状态有关,用到了误差的累加值(即用误差累加代替积分),另一方面公式输出直接对应对象的输出,对系统影响大。
而增量式PID特点有不需要累加误差,控制增量仅与最近几次的误差有关,不容易产生误差累积,仅输出控制量增量,误动作影响小,不会严重影响系统。
两者的主要区别在于,位置式PID控制算法是一种非递推算法,其输出直接控制执行机构,输出的量与执行机构的位置(例如阀门的开关量)一一对应。增量式PID控制算法是一种递推算法,输出的只是控制量的增量,技术输出的增量对应的是本次执行机构位置的增量而不是实际位置。PID控制并不一定要三者都出现,也可以只是PI、PD控制,关键决定于控制的对象。
舵机通常采用位置式PD算法,对于舵机的控制,因为不需要记录之前的误差因此这里将积分环节去掉,这样公式就变为。舵机控制代码实现如下:差因此这里将积分环节去掉,这样公式就变为。舵机控制代码实现如下:
//保存上次误差
last_elect_val = curr_elect_val;
//计算本次误差
curr_elect_val=SET_POSITION-position;
elect_val_delta = curr_elect_val - last_elect_val; //电磁变化率
smotor_duty=(int16)(__skp*curr_elect_val + __skd*elect_val_delta);//进行PD运算
smotor_duty=(int32)limit_ab((int16)smotor_duty, LMAX_DUTY, RMAX_DUTY);//限幅,
pwm_duty(SMOTOR_CHANNEL, SMOTOR_CENTER+smotor_duty);//控制舵机转动
电机通常采用增量式PID算法,因为增量式PID不容易造成电机的正反转切换,对速度的调节更为平滑。电机控制代码实现如下:
ek2=ek1;//保存上上次误差
ek1=ek;//保存上次误差
set_speed=SPEED;
//进行增量式PID计算
out_increment=(int16)(kp*(ek-ek1)+ki*ek+kd*(ek-2*ek2+ek2));//计算增量
out +=out_increment; //输出增量
out =limit(out,GTM_ATOM0_PWM_DUTY_MAX/2 ); //输出限幅,不能超过占空比最大值
motor_duty=(int32)out; //强制转换为整数后赋值给电机占空比变量
if(motor_duty>=0) //前进
{
pwm_duty(MOTOR2_CHANNEL, 0);
pwm_duty(MOTOR1_CHANNEL, motor_duty);
}
else //后退
{
pwm_duty(MOTOR1_CHANNEL, 0);
pwm_duty(MOTOR2_CHANNEL, -motor_duty);
}
(注:这里只展示PID算法部分,变量定义及初始化略)
3.5 电磁信号采集和处理
3.5.1 ADC介绍
模拟数字转换器即A/D转换器,或简称ADC,通常是指一个将模拟信号转变为数字信号的电子元件。通常的模数转换器是将一个输入电压信号转换为一个输出的数字信号。由于数字信号本身不具有实际意义,仅仅表示一个相对大小。故任何一个模数转换器都需要一个参考模拟量作为转换的标准,比较常见的参考标准为最大的可转换信号大小。而输出的数字量则表示输入信号相对于参考信号的大小。
3.5.2 ADC转换过程
ADC转换过程通常有采样(取样),保持,量化,编码四个阶段。
▲ 图3.7 ADC转换过程
(引自https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/adc/adc)
3.5.3 ADC转换分辨率
通常以输出二进制或十进制数字的位数表示ADC分辨率的高低,因为位数越多,量化单位越小,对输入信号的分辨能力就越高。
例如:输入模拟电压的变化范围为0~5 V,输出8位二进制数可以分辨的最小模拟电压为5 V×2-8=20 mV;而输出12位二进制数可以分辨的最小模拟电压为5 V×2-12≈1.22 mV。
智能车ADC转换分辨率由板载ADC的具体型号决定,一般有8bit,12bit等。TC264D一般电磁循迹用8bit足够了。
3.5.4 电磁信号处理
首先要读取电磁信号,然后滤波,归一化处理。
读取电磁信号调用官方的库即可。这里注意adc句柄和通道号要与所画pcb的管脚定义相对应。
rt_adc_read(ADC_0, ADC0_CH0_A0);
滤波方法有很多,常用的有均值滤波,平滑滤波,卡尔曼滤波等等。这里注意的是,不要贪多求全,适合自己的才是最好的。听上去很厉害的滤波方法可能效果不一定那么好,有的方法有可能是不收敛的。所以最好对自己使用的滤波方法心里有数。我们在加上三大滤波方法之后,发现滤波效果反而变差了,所以就只用了基本的均值滤波。其实现代码也很简单。
sum = 0;
for(i=0; i<count; i++)
{
sum += rt_adc_read(ADC_0, ADC0_CH0_A0);
}
sum = sum/count;
归一化处理:每一个场地的电感数据,往往不尽相同,甚至出现极大的偏差,可能是场地地下钢筋等因素的影响,所以归一化是十分必要的。这样可以方便适应不同的赛道环境,使得比赛场上的电感值最接近平时调车时的电感值。归一化思路如下:先把车放在直道上,采集各电感的最大值(使该电感贴近或垂直电磁线)。写一个记录最大值、最小值的数组。进行以下运算。
需要注意的是:如果有限幅操作(1-100),则记录归一化数值时需要把车放在整个赛道上电感值最大的地方(如环岛),否则可能造成电感“饱和”的假象。如果没有限幅操作,则放在直道上读数即可。
for(int i=0;i<7;i++)
{
AD_G_S[i]=( (AD_data[i]-AD_min[i])*1.0/(s_AD_max[i]-AD_min[i])*100 );
if(AD_G_S[i]>100)
AD_G_S[i]=100;
else if(AD_G_S[i]<1)
AD_G_S[i]=1;
}
3.6 利用定时器实现巡线
循迹一般有电磁循迹和摄像头循迹两种方案。前者简单易上手,稳定性高,不容易受到赛道环境的影响。后者获取的信息量更大,上限更高,稳定性较低,受场地阳光等影响较大。由于今年四轮组要求摄像头高度不超过不容易受到赛道环境的影响。后者获取的信息量更大,上限更高,稳定性较低,受场地阳光等影响较大。由于今年四轮组要求摄像头高度不超过不容易受到赛道环境的影响。后者获取的信息量更大,上限更高,稳定性较低,受场地阳光等影响较大。由于今年四轮组要求摄像头高度不超过不容易受到赛道环境的影响。后者获取的信息量更大,上限更高,稳定性较低,受场地阳光等影响较大。由于今年四轮组要求摄像头高度不超过
循迹算法常用的是差比和算法,差比和偏差曲线与理想偏差曲线如图3.8中所示。
▲ 图3.8 差比和偏差曲线与理想偏差曲线
算法基本思路如下:
Position= (a-b)/(a+b)(a和b是左右电感的值)
计算出的数值绝对值大小表示偏离赛道的程度,在一定范围内车模偏离赛道越远计算出来的值越大。下面利用这个数据就可以控制舵机,来使得车模一直沿着赛道中心线前进了。
//差比和
__S_diff= (elect_L - elect_R)*100;
__S_sum= (elect_L + elect_R);
S_position=__S_diff/__S_sum;
//丢线保护
if(__S_sum<__LOSELINE){
if (elect_L >= elect_R) {
pwm_duty(SMOTOR_CHANNEL, SMOTOR_CENTER-L_DUTY);//左打死
}
else {
pwm_duty(SMOTOR_CHANNEL, SMOTOR_CENTER-R_DUTY); //右打死
}
}
else{
//计算本次误差
curr_elect_val=SET_POSITION-position;
}
这样一来就可以实现电磁巡线的效果。
把这一系列电感采集与处理、pid计算、pwm输出放在一起,就可以实现初步的智能小车巡线了。下面展示这一功能的实现过程。放在定时器进程里面运行,来保证执行周期是固定的。创建定时器和创建线程的方法类似,创建线程的方法放在3.4.1部分详细说明。
rt_timer_create("timer1", timer1_pit_entry, RT_NULL, 1, RT_TIMER_FLAG_PERIODIC);
这里timer1表示定时器的名称,限制在8个字符以内。
timer1_pit_entry表示时间到了之后需要执行的函数,可以理解为stm32里的超时回调函数。
RT_NULL表示不需要传递参数,
1表示定时器的超时时间为1个系统tick,系统周期为1毫秒,则1表示1毫秒。为什么要用1个系统tick?这样可以保证定时器足够精确。
RT_TIMER_FLAG_PERIODIC表示定时器以周期运行,如果设置为RT_TIMER_FLAG_ONE_SHOT则只会运行一次。
这些内容可以通过RTT官方文档和API手册来了解。
void timer_pit_init(void)
{
rt_timer_t timer;
//创建一个定时器 周期运行
timer = rt_timer_create("timer1", timer1_pit_entry, RT_NULL, 1, RT_TIMER_FLAG_PERIODIC);
//启动定时器
if(RT_NULL != timer)
{
rt_timer_start(timer);
}
}
创建好定时器以后,就可以编写定时器超时函数了。可以理解为周期性执行的线程,而之前无限次执行的线程一般周期性不能得到保证。
每进函数一次,time++。
if(0 == (time%5)) 保证5个周期进一次。
然后依次执行,电磁信号采集、归一化、舵机PID控制,采集编码器数据和电机的PID控制。
void timer1_pit_entry(void *parameter)
{
static uint32 time;
time++;
if(0 == (time%5))
{
//电磁信号采集、归一化、PID控制
elec_calculate();
//采集编码器数据
encoder_get();
//控制电机转动
motor_control();
}
}
这样小车就基本具备自主巡线的功能,同时拥有舵机的方向环和电机的速度环。
3.7 调试手段
3.7.1 显示屏与低优先级线程
显示屏可以在车上直接看数据,如图3.9中所示,相比串口通信调试更加直观好方便,显示信息更加丰富,由于采用了spi协议,速度也更快。
▲ 图3.9 辅具调试显示屏
显示屏显示作为一个独立的线程,首先需要初始化。初始化由以下几部分组成,创建线程句柄,初始化外设(这个直接调厂商的库函数),然后创建显示线程并启动,优先级设置为31。这里注意的是,我们做车一般32个优先级就够了,0为最高的优先级(不同的厂家对优先级的顺序处理可能不一样,RTT和ST都是0最高)。为什么显示线程的优先级是最后一个呢?这是因为,相对于信号的采集、处理、计算,以及pwm输出等操作,显示部分只影响我们读数,不影响小车的正常运转,所以显示就不那么重要。因此,显示的线程最低,为31。调试过程中发现,显示屏显示和串口传输是非常消耗资源的一件事,如果把这些任务放在中断里则无法保证中断的实时性和周期性。统一放在main()的while(1)里面则许多任务无法区分优先级。
这时候,RT-Thread的线程概念就完美解决了这个问题。把不同的任务放在不同的线程,并分配好相应的优先级。则低优先级任务不会影响高优先级任务的执行,高优先级任务挂起的时候,也可以很好的利用处理器的空闲资源来处理低优先级任务。
下面是线程创建的具体操作。创建好之后,就需要启动显示线程。由于是本文中首次接触线程的创建,所以带有详细的注释讲解。
void display_init(void)
{
// 线程控制块指针,创建线程时的标准操作
rt_thread_t tid;
//lcd初始化,相关外设的初始化
lcd_init();
// 创建动态线程,利用系统的动态内存管理,比较方便,一般都用动态线程
//创建显示线程 优先级设置为31
tid = rt_thread_create("display", // 线程名称
display_entry, // 线程入口函数
RT_NULL, // 线程参数,RT_NULL表示无参,类似于void
256, // 256 个字节的栈空间,留有一定的余量
31, // 线程优先级为31,数值越小,优先级越高,0为最高优先级。
30); // 时间片为30个系统节拍
//启动显示线程
if(RT_NULL != tid)
{
rt_thread_startup(tid);
}
}
在显示线程入口函数处,编写需要的内容即可。我们是通过拨码开关确定显示与否(把拨码开关当成一个普通的io处理即可),然后选择显示原始电感值还是归一化后的电感值。
void display_entry(void *parameter)
{
while(1)
{
if(gpio_get(P14_4)){
if(0){
for(int16 i=0;i<7;i++){
lcd_showuint16(5, i, signals_long[i]);
//lcd_showint8(60,i,send_buff[i+2]);
lcd_showuint16(120,i,signals_short[i]);
}
rt_kprintf("point_num: %d\n", point_sum);
}
if(1){
for(int16 i=0;i<7;i++){
lcd_showuint16(5,i,AD_G_L[i]);
lcd_showuint16(120, i, AD_G_S[i]);
}
if(ShortSmotorFlag==0){
lcd_showstr(120,7," ");
lcd_showuint16(5,7,0);
}
else{
lcd_showstr(5,7," ");
lcd_showuint16(120,7,1);
}
virtual_Osc_Test();
rt_kprintf("point_num: %d\n", point_sum);
}
}
rt_thread_mdelay(10);
}
}
入口函数是在哪,被谁调用的?答案是,入口函数是被系统内核调用的,只需要创建好并启动,系统就会自动调用入口函数。实际上,由于我们的线程是无限循环模式,即限循环模式,即限循环模式,即限循环模式,即
rt_thread_mdelay(10);
这样做的意义在于:在实时操作系统中,线程中不能陷入死循环操作,必须要有让出 CPU 使用权的动作,如循环中调用延时函数或者主动挂起。
而放在while(1)的目的,就是为了让这个线程一直被系统循环调度运行,永不删除。与之相对应的就是顺序执行或有限次循环模式,如简单的顺序语句、do while() 或 for()循环等,此类线程不会循环或不会永久循环,可谓是 “一次性” 线程,一定会被执行完毕。在执行完毕后,线程将被系统自动删除。
3.7.2 蜂鸣器和邮箱的完美配合
在做一些赛道元素判断的时候,我们需要知道到底有没有触发条件。如果调试结果不如预期,就需要知道到底是车模运动的问题,还是判断的问题。而车在跑的过程中,看显示屏是不现实的,串口传输也很不方便。所以在某些元素位置满足条件让蜂鸣器响,是方便高效的调试办法。
和显示屏一样,蜂鸣器也需要创建线程,启动线程,编写入口函数。这里优先级设置为20,比显示屏更高,理由是蜂鸣器需要更及时的响应。
void buzzer_init(void)
{
rt_thread_t tid;
//初始化蜂鸣器所使用的GPIO
gpio_init(BUZZER_PIN, GPO, 0, PUSHPULL);
//创建邮箱
buzzer_mailbox = rt_mb_create("buzzer", 5, RT_IPC_FLAG_FIFO);
//创建蜂鸣器的线程
tid = rt_thread_create("buzzer", buzzer_entry, RT_NULL, 256, 20, 2);
//启动线程
if(RT_NULL != tid)
{
rt_thread_startup(tid);
}
}
如何让蜂鸣器响?这里使用的是有源蜂鸣器,所以只需要给它一个高电平。但是如果给一个高电平就不管了,那么蜂鸣器就会一直响,制造出让人无法忍受的噪音。所以需要先拉高,持续一段时间再拉低。同样的,蜂鸣器也需要无限循环模式。
这里有三种操作蜂鸣器方式的比较:
-
手动操作gpio型。满足条件拉高,否则拉低。这是最朴素最原始的方案。优点简单易行。缺点有二,一是如果此条件满足拉高了,下一个判断条件不满足又拉低了,由于程序执行速度很快以至于我们是听不到蜂鸣器响的。如果只满足条件拉高而不拉低,则全实验室就会充斥着蜂鸣器尖利的啸叫,同行们可能会把你的车子扔出去,当然也可能包括你。二是不同的元素处无法通过蜂鸣器的频率区分出来,响声是一样的,如果误判了,无法确定是哪一部分误判。
-
传统裸机编程delay()型。自己写一个程序,拉高,延时一段时间再拉低。这样做的优点就是克服了第1种方式的缺点。缺点则是delay()的过程,相当于cpu什么也不做,十分浪费资源。且如果delay()时间过长,相当于这段时间内信号也不采集,电机也不驱动,仅仅是为了等蜂鸣器响够一定时间。这是非常可怕的。
-
RT-Thread系统邮箱+挂起型。既保留了第2种方式的所有优点,也克服了上述两种方式所有的缺点。不需要全局变量,每次响多长时间由邮件内容决定,在延时的过程中,其他的程序可以继续执行,当延时结束后,再回来拉低gpio。可以说,RT-Thread系统的优势在这一个小点上已得到充分的展现。
其实现流程是:蜂鸣器入口函数里接收邮箱数据,如果没有数据则持续等待并释放CPU控制权。
void buzzer_entry(void *parameter)
{
uint32 mb_data;
while(1)
{
//接收邮箱数据,如果没有数据则持续等待并释放CPU控制权
rt_mb_recv(buzzer_mailbox, &mb_data, RT_WAITING_FOREVER);
gpio_set(BUZZER_PIN, 1); //打开蜂鸣器
rt_thread_mdelay(mb_data); //延时
gpio_set(BUZZER_PIN, 0); //关闭蜂鸣器
}
}
接收模式是由这个宏定义确认的。
#define RT_WAITING_FOREVER -1 /**< Block forever until get resource. */
在需要响的位置给蜂鸣器发一个邮件,让蜂鸣器响一定时间。由于程序是放在定时器里周期性执行的,所以会实现蜂鸣器以不同的频率鸣响。比如斑马线处是“嘟嘟”,在岔路处是“滴滴”。
if(ForkRoadstate){
forkroadcount++;
// pwm_duty(MOTOR1_CHANNEL, 3000);//减速
//发个消息让蜂鸣器响
rt_mb_send(buzzer_mailbox, BBFORK);
}
3.7.3 摄像头与信号量的保护
同样的,摄像头部分也有类似的操作。这里摄像头就不再单独创建一个线程,而是直接放在main()函数的while(1)里。
为什么要等待摄像头采集完毕才开始处理摄像头图像呢?因为双方使用的数组是同一个,属于公共资源。如果不做这个处理,就有可能出现图像处理的过程中摄像头改写了相应的数组,使得这一张是由前一张的一半和下一张的一半拼接而成,也有可能造成流水线等待的迟滞。所以当摄像头采集完毕之后,会释放信号量,rt_sem_take(camera_sem, RT_WAITING_FOREVER);语句得到信号量之后继续执行下面的语句,否则持续等待并释放CPU控制权。
信号量的英文是semaphore,可以理解成信号旗,只有得到了信号,才做相应的反应。这里的应用可以说非常直观了。
while(1) {
//等待摄像头采集完毕
rt_sem_take(camera_sem, RT_WAITING_FOREVER);
//开始处理摄像头图像
DealGarage();
//处理完成需要将标志位置0
mt9v03x_finish_flag = 0;
//翻转LED,闪灯表示程序正在运行没有死机
time++;
if(time==500){
time=0;
gpio_toggle(P20_8);
gpio_toggle(P20_9);
}
}
这样以后,图像处理就完成了。
这里有个小Tips,就是可以在while(1)里翻转LED的GPIO引脚,闪灯则表示程序正在运行没有死机。
▲ 图3.10 摄像头调试
3.7.4 通过按键释放不同的信号量
利用定时器创建一个周期性按键扫描的线程,并且创建按键的信号量,当按键按下就释放该信号量,在需要使用按键的地方获取信号量即可。这里简单演示一下。初始化的时候先定义一个定时器的句柄 timer1 ,初始化对应的GPIO,创建按键的信号量,定时器设置为入口函数是button_entry,不传参,周期为20个系统节拍(即20ms),模式设置为RT_TIMER_FLAG_PERIODIC即周期性执行。为什么是20ms呢,因为要考虑按键按下的这样一个时间,如果太短,可能两次都是按下的状态,如果太长,则可能两次都是没按下的状态。
void button_init(void)
{
rt_timer_t timer1;
gpio_init(KEY_1, GPI, GPIO_HIGH, PULLUP); // 初始化为GPIO浮空输入 默认上拉高电平
gpio_init(KEY_2, GPI, GPIO_HIGH, PULLUP);
key1_sem = rt_sem_create("key1", 0, RT_IPC_FLAG_FIFO); //创建按键的信号量
key2_sem = rt_sem_create("key2", 0, RT_IPC_FLAG_FIFO);
timer1 = rt_timer_create("button", button_entry, RT_NULL, 20, RT_TIMER_FLAG_PERIODIC);
if(RT_NULL != timer1)
{
rt_timer_start(timer1);
}
}
那么在入口函数这里编写自己需要的功能。一般是检测到按键按下之后并放开,释放一次信号量,然后再需要的位置接收信号量即可。
void button_entry(void *parameter)
{
//保存按键状态
key1_last_status = key1_status;
key2_last_status = key2_status;
//读取当前按键状态
key1_status = gpio_get(KEY_1);
key2_status = gpio_get(KEY_2);
//检测到按键按下之后并放开 释放一次信号量
if(key1_status && !key1_last_status)
{
rt_sem_release(key1_sem);
}
}
3.7.5 虚拟示波器线程与时间片轮转
我们利用山外多功能调试助手的协议做了一个虚拟示波器,如图3.11中所示,可以将采集得到的电感值、计算出的位置和打角、车模运行速度等变量实时打印出来,十分直观。
▲ 图3.11 上位机显示的虚拟示波器图像
▲ 图3.12 利用虚拟示波器和显示屏调试
同样的,这里将示波器也作为一个独立的线程。由于和显示屏的地位一样,所以优先级、大小等设置和显示屏线程是一样的。
void virtual_Osc_init(void)
{
rt_thread_t tid;
//创建示波器线程 优先级设置为31
tid = rt_thread_create("virtual_Osc", virtual_Osc_entry, RT_NULL, 256, 31, 25);
//启动示波器线程
if(RT_NULL != tid)
{
rt_thread_startup(tid);
}
}
同样的,编写线程入口函数,同样采用无限循环模式。
void virtual_Osc_entry(void *parameter)
{
while(1)
{
virtual_Osc_Test();
rt_thread_mdelay(10);
}
}
不同优先级之间的线程可以有所区分,按优先级大小进行调度,那么优先级相同的线程如何调度呢?这里就要引出RT-Thread系统的另一大特色,时间片轮转调度方式,如图3.11中所示。
▲ 图3.13 时间片轮转调度方式
(引自https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/thread/thread)
简单来说,就是同优先级的任务依次循环排队进行,时间片的作用就是保证一定的实时性。比如当线程B的执行时间已经达到了给定的时间片长度,而该任务还没有执行结束,该任务就会被强制挂起,执行下一个任务。等排队的任务都执行结束或时间片结束之后再接着执行线程B。
我们回顾一下显示屏线程的创建。最后一个参数为时间片参数,我们设为30,表示30个系统节拍(即30ms)。
//创建显示线程 优先级设置为31
tid = rt_thread_create("display", display_entry, RT_NULL, 256, 31, 30);
而虚拟示波器线程的时间片则为25个系统节拍,即25ms.
//创建示波器线程 优先级设置为31
tid = rt_thread_create("virtual_Osc", virtual_Osc_entry, RT_NULL, 256, 31, 25);
如果在时间片内执行结束,则语句末的rt_thread_mdelay(10);会释放cpu的使用权,如果超时则会被强制挂起。
这样就很好的利用了时间片轮转调度特性完成了显示屏显示和无线串口虚拟示波器任务的“并行处理”。
3.7.6 使用FinSH实时监测单片机运行情况
FinSH 是 RT-Thread 的命令行组件(shell),提供一套供用户在命令行调用的操作接口,主要用于调试或查看系统信息。它可以使用串口 / 以太网 / USB 等与 PC 机进行通信。在智能车运行中,我们常常需要实时监测单片机的运行情况,包括变量的值、线程运行状态、内存使用情况。通常我们会编写相关函数然后通过无线串口传输在上位机监测单片机的运行情况。这些调试所用的函数往往不便于更新和维护,也会使得程序变得复杂、冗余。所以我们需要一套成熟的命令行系统,而RT-Thread系统的FinSH控制台正好为我们提供了这个功能。
在实际小车运行过程中,我们使用ps命令列出系统中所有线程信息,包括线程优先级、状态、栈的最大使用量等,以此监测智能车的线程运行情况。
▲ 图3.14 使用PS命令检测线程信息
另外,FinSH还提供了自定义命令的功能,使用者根据需要自定义命令。比如,我们编写了如,我们编写了如,我们编写了如,我们编写了
MSH_CMD_EXPORT(stop_car,Car Is Stop);
3.8本章小结
本章将裸机大while()+中断方式与RT-Thread系统做了比较,分析了两者的优劣势,并得出了RT-Thread系统优势巨大的结论。接着将基础四轮小车面临的任务抽象为“一核心”PID控制,“两关键”(环境变量输入和控制量输出),“多辅助”(led、显示屏、蜂鸣器等),详细说明了基于RT-Thread操作系统的基础四轮车的软件开发流程,灵活利用RT-Thread操作系统的线程管理、时钟管理、邮箱、信号量、时间片轮转调度等特性,完成了小车的巡线,以及高效的调试和维护。在3.7.2小节利用蜂鸣器与RT-Thread系统的完美配合初步展现了RT-thread系统的魅力,其可重用性及丰富的软件包支持也将成为物联网时代的弄潮儿。
利用嵌入式实时操作系统开发的重点在于摒弃传统的大while(1)+中断模式前后台思维,拥抱多线程并发、模块化开发的思维。将任务分解成一个一个的线程,合理配置优先级和时间片,从而保证任务的顺利完成。
还要有通信思维的转变。从传统的全局变量标志位思维转变为利用邮箱、信号量、互斥量等全局通信方式通信。这样做最大的好处除了减少全局变量的使用及其带来的一系列问题以外,不仅更加方便,还可以实现在条件询问时条件不满足可以释放cpu使用权,提高cpu利用率。同样的,所有的delay()都不会使得cpu空转,而是会先把cpu释放出来,去处理其他的任务。这样大大提高了cpu资源的使用率。
§04 学习与迁移
§04 学习与迁移
4.1 学习过程
学习过程主要分为三部分,先分析小车裸机中断+大while()模式的架构,然后了解RT-Thread基本的特性,学习RT-Thread的内核部分。最后是迁移部分。
学习路径:通过RTT官方文档和逐飞的开源库进行学习,加以一些参考书作为辅助。
4.1.1学习线程管理
//————————————————————
// @brief 线程1入口函数
// @param parameter 参数
// @return void
// Sample usage:
//------------------------------------------------------------
void thread1_entry (void *parameter)
{
while(1)
{
// 调度器上锁,上锁后将不再切换到其他线程,仅响应中断
rt_enter_critical();
// 以下进入临界区
count += 10; // 计数值+10
// 调度器解锁
rt_exit_critical();
rt_kprintf("thread = %d , count = %d\n", 10, count);
rt_thread_mdelay(1000);
}
}
//------------------------------------------------------------
// @brief 线程创建以及启动
// @param void 空
// @return void
// Sample usage:
//------------------------------------------------------------
int critical_section_example(void)
{
// 线程控制块指针
rt_thread_t tid;
// 创建动态线程
tid = rt_thread_create("thread_10", // 线程名称
thread1_entry, // 线程入口函数
RT_NULL, // 线程参数
256, // 栈空间
5, // 线程优先级为5,数值越小,优先级越高,0为最高优先级。
// 可以通过修改rt_config.h中的RT_THREAD_PRIORITY_MAX宏定义(默认值为8)来修改最大支持的优先级
10); // 时间片
if(tid != RT_NULL) // 线程创建成功
{
// 运行该线程
rt_thread_startup(tid);
}
return 0;
}
// 使用INIT_APP_EXPORT宏自动初始化,也可以通过在其他线程内调用critical_section_example函数进行初始化
INIT_APP_EXPORT(critical_section_example); // 应用初始化
学习完毕,总结:创建一个线程需要做三方面的事,线程入口函数,包括调度器上锁、进入临界区、调度器解锁三个步骤,其中重要代码需要放在临界区;线程创建及启动函数,包括线程控制指针、创建动态线程(入口函数、优先级等),启动线程rt_thread_startup(tid)。然后编译运行,符合实验预期。
4.1.2学习线程间通信——邮箱
邮箱服务是实时操作系统中一种典型的线程间通信方法。举一个简单的例子,有两个线程,线程 1 检测按键状态并发送,线程 2 读取按键状态并根据按键的状态相应地改变 LED 的亮灭。这里就可以使用邮箱的方式进行通信,线程 1 将按键的状态作为邮件发送到邮箱,线程 2 在邮箱中读取邮件获得按键状态并对 LED 执行亮灭操作。
这里的线程 1 也可以扩展为多个线程。例如,共有三个线程,线程 1 检测并发送按键状态,线程 2 检测并发送 ADC 采样信息,线程 3 则根据接收的信息类型不同,执行不同的操作。
线程1负责接收邮件,线程2负责发送邮件。
static char mb_str1[] = "i am a mail!"; // 创建一个字符串
static rt_mailbox_t mb; // 邮箱控制块指针
void thread1_entry (void *parameter);
void thread2_entry (void *parameter);
int mailbox_example (void);
//------------------------------------------------------------
// @brief 线程入口
// @param parameter 参数
// @return void
// Sample usage:
//------------------------------------------------------------
void thread1_entry (void *parameter)
{
char *str;
rt_kprintf("thread1:try to recv a mail.\n");
if(rt_mb_recv(mb, // 邮箱控制块
(rt_ubase_t *)&str, // 接收邮箱的字符串 接收 32bit 大小的邮件
RT_WAITING_FOREVER) // 一直等待
== RT_EOK)
{
rt_kprintf("thread1:get a mail from mailbox.\nthe content :%s\n", str); // 输出接收信息
}
rt_mb_delete(mb); // 删除邮箱
}
//------------------------------------------------------------
// @brief 线程入口
// @param parameter 参数
// @return void
// Sample usage:
//------------------------------------------------------------
void thread2_entry (void *parameter)
{
rt_kprintf("thread2:try to send a mail.\n");
// 这里是使用的方式是将字符串的地址取值 得到 32bit 的地址值发送
rt_mb_send(mb, (rt_uint32_t)&mb_str1); // 发送邮件
}
//------------------------------------------------------------
// @brief 邮箱创建以及启动
// @param void
// @return void
// Sample usage:
//------------------------------------------------------------
int mailbox_example (void)
{
rt_thread_t t1,t2;
// 创建邮箱
mb = rt_mb_create("mb",
4, // 设置 缓冲区为 4 封邮件
RT_IPC_FLAG_FIFO // 先进先出
);
t1 = rt_thread_create(
"thread1", // 线程名称
thread1_entry, // 线程入口函数
RT_NULL, // 线程参数
256, // 栈空间大小
4, // 设置线程优先级
5); // 时间片
if(t1 != RT_NULL) // 线程创建成功
{
rt_thread_startup(t1); // 运行该线程
}
t2 = rt_thread_create(
"thread2", // 线程名称
thread2_entry, // 线程入口函数
RT_NULL, // 线程参数
256, // 栈空间大小
3, // 设置线程优先级
5); // 时间片
if(t2 != RT_NULL) // 线程创建成功
{
rt_thread_startup(t2); // 运行该线程
}
return 0;
}
4.1.3学习线程间同步——信号量
例如一项工作中的两个线程:一个线程从传感器中接收数据并且将数据写到共享内存中,同时另一个线程周期性的从共享内存中读取数据并发送去显示。
信号量工作机制:信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。信号量工作示意图如下图4.1中所示,每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值对应了信号量对象的实例数目、资源数目。假如信号量值为 5,则表示共有 5 个信号量实例(资源)可以被使用,当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。
▲ 图4.1 线程间铜箔-信号量机制
(引自https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/ipc1/ipc1?id=%e4%bf%a1%e5%8f%b7%e9%87%8f)
当选择 RT_IPC_FLAG_FIFO(先进先出)方式时,那么等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量。
camera_sem = rt_sem_create("camera", 0, RT_IPC_FLAG_FIFO);
信号量也能够方便地应用于中断与线程间的同步,例如一个中断触发,中断服务例程需要通知线程进行相应的数据处理。这个时候可以设置信号量的初始值是 0,线程在试图持有这个信号量时,由于信号量的初始值是 0,线程直接在这个信号量上挂起直到信号量被释放。当中断触发时,先进行与硬件相关的动作,例如从硬件的 I/O 口中读取相应的数据,并确认中断以清除中断源,而后释放一个信号量来唤醒相应的线程以做后续的数据处理。例如 FinSH 线程的处理方式,如下图5.2中所示:
▲ 图5.2 FinSH 的中断、线程间同步示意图
(引自https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/ipc1/ipc1?id=%e4%bf%a1%e5%8f%b7%e9%87%8f)
信号量的值初始为 0,当 FinSH 线程试图取得信号量时,因为信号量值是 0,所以它会被挂起。当 console 设备有数据输入时,产生中断,从而进入中断服务例程。在中断服务例程中,它会读取 console 设备的数据,并把读得的数据放入 UART buffer 中进行缓冲,而后释放信号量,释放信号量的操作将唤醒 shell 线程。在中断服务例程运行完毕后,如果系统中没有比 shell 线程优先级更高的就绪线程存在时,shell 线程将持有信号量并运行,从 UART buffer 缓冲区中获取输入的数据。
4.2 迁移准备
4.2.1 小车代码部分分析
整体迁移方案是将原来的单机大while()形式迁移到RTT操作系统下面。
具体的大while()+中断形式代码分析见3.2.
4.2.2 迁移思路
将大while()里面的显示屏、串口分解为两个不同的线程,优先级相同,利用时间片轮转方式进行调度。5ms一次的pit中断用RT-Thread系统的软时钟来替代。用信号量解决摄像头获取和处理图像的关系,用邮箱更好地处理蜂鸣器使用不灵活不方便的问题。
由于小车速度很快,且感知、决策、控制存在必然的顺序,所以必须保证三者的周期性唤醒及执行的先后顺序。所以用RT-Thread 操作系统提供的软定时器,timer1_pit_entry线程周期运行,周期为1个系统节拍。在定时器入口函数里面完成赛道环境的采集和处理,差比和计算,以及PWM波输出。
显示屏和串口作为display_entry线程,优先级设置为31。并且通过拨码开关决定是否显示。蜂鸣器作为buzzer_entry线程,优先级设置为20。并且通过邮箱决定是否响以及响的时间,邮箱大小为5个字节,采用 FIFO 方式进行线程等待。这里显示屏和串口优先级最低,而蜂鸣器优先级更高,比较合理。
迁移思路如图4.3中所示,将前述智能车任务分解为多线程并发、系统自动调度机制下的RT-Thread系统模式。
▲ 图4.3 迁移思路
4.3 迁移过程
4.3.1用信号量处理摄像头获取和处理图像的关系
现象:只有当图像处理结束以后,摄像头才能获取下一张。当采集结束以后,才开始处理。类似于adc转换的采样-保持-量化-编码阶段的关系。之前是使用全局变量的方式通信的,中断里放采集+大while()放处理模式。为什么要用信号量处理摄像头获取和处理图像的关系?
原因有二:一是因为原来用全局变量的方式去进行通信是不太好的习惯,,因为全局变量可能会增加系统的不确定性。所以我们对于全局变量的使用都是持“少用,慎用,不用”的态度。但是中断+大while()编程是离不开全局变量的。这也让我一直十分疑惑,不知道怎么解决这个矛盾的问题。而系统级别的通信方式就解决了这个问题。按照RT-Thread的特性,用信号量处理这个问题就十分方便。二是因为如果询问方式采用了while()等待的方式,那么如果条件不满足,cpu就会一直在while()里空转,直到条件满足。这样可能会造成cpu的无谓等待,降低cpu的利用率。
具体操作过程:
先创建摄像头的信号量,
camera_sem = rt_sem_create("camera", 0, RT_IPC_FLAG_FIFO);
选择 RT_IPC_FLAG_FIFO(先进先出)方式,使得等待线程队列将按照先进
先出的方式排队,先进入的线程将先获得等待的信号量。这样可以保证图像处理的完整性和一贯性。
在while(1)里面轮询,采用无限等待的方式获取信号量,在获得信号量以后说明摄像头采集完毕,这时可以开始处理摄像头图像,
while(1)
{
//等待摄像头采集完毕
rt_sem_take(camera_sem, RT_WAITING_FOREVER);
//开始处理摄像头图像
DealGarage();
//处理完成需要将标志位置0
mt9v03x_finish_flag = 0;
}
在MT9V03X摄像头场中断里,判断图像数组是否使用完毕,如果未使用完毕则不开始采集,避免出现访问冲突。如果图像数组使用完毕,则开启下一轮图像传输。图像传输采用DMA模式。
//---------------------------------------------------------------
// @brief MT9V03X摄像头场中断
// @param NULL
// @return void
// @since v1.0
// Sample usage: 此函数在isr.c中被eru(GPIO中断)中断调用
//---------------------------------------------------------------
void mt9v03x_vsync(void)
{
CLEAR_GPIO_FLAG(MT9V03X_VSYNC_PIN);
mt9v03x_dma_int_num = 0;
if(!mt9v03x_finish_flag)//查看图像数组是否使用完毕,如果未使用完毕则不开始采集,避免出现访问冲突
{
if(1 == link_list_num)
{
//没有采用链接传输模式 重新设置目的地址
DMA_SET_DESTINATION(MT9V03X_DMA_CH, camera_buffer_addr);
}
dma_start(MT9V03X_DMA_CH);
}
}
在MT9V03X摄像头DMA完成中断里面,如果采集完成,就释放摄像头信号量。这时候while(1)里面就开始处理图像了。摄像头部分就结束了。
//------------------------------------------------------------------
// @brief MT9V03X摄像头DMA完成中断
// @param NULL
// @return void
// @since v1.0
// Sample usage: 此函数在isr.c中被dma中断调用
//------------------------------------------------------------------
void mt9v03x_dma(void)
{
extern rt_sem_t camera_sem;
CLEAR_DMA_FLAG(MT9V03X_DMA_CH);
mt9v03x_dma_int_num++;
if(mt9v03x_dma_int_num >= link_list_num)
{
//采集完成
mt9v03x_dma_int_num = 0;
mt9v03x_finish_flag = 1;//一副图像从采集开始到采集结束耗时3.8MS左右(50FPS、188*120分辨率)
rt_sem_release(camera_sem); //释放摄像头信号量
dma_stop(MT9V03X_DMA_CH);
}
}
带来的实际好处:使用RT-Thread的信号量特性,方便地解决中断与线程间同步。因为获取和传输图像是在中断进行的,处理图像是在main()线程里完成的。系统级别的通信方式大大铺开,则全局变量的使用将会大大减少,这样减少了许多安全隐患,也有助于我们养成良好的编程习惯。另一方面,利用系统特性,如果条件不满足会将线程挂起,释放cpu,减少了cpu询问等待的空转时间,提高了cpu的利用率。
4.3.2用邮箱解决蜂鸣器使用不灵活不方便的问题
现象:原来的蜂鸣器两种使用方式,手动拉高拉低GPIO型或者写个函数+delay()型。这两种方案各有缺点。手动拉蜂鸣器GPIO型的缺点有二,一是如果此条件满足拉高了,下一个判断条件不满足又拉低了,由于程序执行速度很快以至于我们是听不到蜂鸣器响的。如果只满足条件拉高而不拉低,则全实验室就会充斥着蜂鸣器尖利的啸叫,同行们可能会把你的车子扔出去,当然也可能包括你。二是不同的元素处无法通过蜂鸣器的频率区分出来,响声是一样的,如果误判了,无法确定是哪一部分误判。手动写个函数最大的缺点在于你。二是不同的元素处无法通过蜂鸣器的频率区分出来,响声是一样的,如果误判了,无法确定是哪一部分误判。手动写个函数最大的缺点在于你。二是不同的元素处无法通过蜂鸣器的频率区分出来,响声是一样的,如果误判了,无法确定是哪一部分误判。手动写个函数最大的缺点在于你。二是不同的元素处无法通过蜂鸣器的频率区分出来,响声是一样的,如果误判了,无法确定是哪一部分误判。手动写个函数最大的缺点在于你。二是不同的元素处无法通过蜂鸣器的频率区分出来,响声是一样的,如果误判了,无法确定是哪一部分误判。手动写个函数最大的缺点在于你。二是不同的元素处无法通过蜂鸣器的频率区分出来,响声是一样的,如果误判了,无法确定是哪一部分误判。手动写个函数最大的缺点在于你。二是不同的元素处无法通过蜂鸣器的频率区分出来,响声是一样的,如果误判了,无法确定是哪一部分误判。手动写个函数最大的缺点在于你。二是不同的元素处无法通过蜂鸣器的频率区分出来,响声是一样的,如果误判了,无法确定是哪一部分误判。手动写个函数最大的缺点在于写个函数+delay()型:
void BEEP()
{
gpio_set(P02_6,1);
systick_delay_ms(STM0, 1);
gpio_set(P02_6,0);
}
为什么要用邮箱+蜂鸣器线程的方式?因为这种方式方式——RT-Thread系统邮箱+挂起型,既保留了第2种方式的所有优点,也克服了上述两种方式所有的缺点。不需要全局变量,每次响多长时间由邮件内容决定,在延时的过程中,其他的程序可以继续执行,当延时结束后,再回来拉低gpio。蜂鸣器入口函数里接收邮箱数据,如果没有数据则持续等待并释放CPU控制权,不会造成cpu空转。
具体操作:
首先是初始化buzzer(蜂鸣器),要做的就是初始化蜂鸣器所使用的GPIO、创建邮箱、创建蜂鸣器的线程、启动线程四步。邮箱大小为5个字节,其实实际过程中只需要传一个字节的数据即可,采用 FIFO 方式进行线程等待。具体的使用方式在3.4.2小节已有详细介绍。这样一来方便调试,也方便在比赛场上随时知道小车所处的状态。
带来的实际好处:上面已经多次提到,这样一方面减少了全局变量的使用,一方面减少了cpu的空转,增加了cpu的利用率,同时使得蜂鸣器的使用更加的灵活方便。
4.3.3用软时钟代替原来的pit中断
为什么要用软定时器?由于小车感知、决策、控制存在必然的顺序,且要保证执行周期是固定的,所以要放在定时器线程里面运行。所以用RT-Thread 操作系统提供的软定时器,timer1_pit_entry线程周期运行,周期为1个系统节拍。在定时器入口函数里面完成赛道环境的采集和处理,差比和计算,以及PWM波输出。
具体操作:
创建一个定时器周期运行,这里周期是一个系统节拍(1ms),可以保证精度。
timer = rt_timer_create("timer1", timer1_pit_entry, RT_NULL, 1, RT_TIMER_FLAG_PERIODIC);
然后在时钟线程里面,加入感知、决策、控制的代码。进一次线程time++,然后用(0 == (time%5))保证5个系统周期。
详细的操作过程在3.6部分已有详细说明。按键和虚拟示波器线程前文已详述,此处不再赘述。
带来的好处:软定时器起到了代替原来PIT中断的作用,更多的是RT-Thread系统整体使用带来的一系列好处。软定时器带来的好处最明显的是可以有许多个定时器线程同时工作,不受硬件平台的限制,也提高了代码可移植性。
4.3.4利用FinSH提高调试效率
现象:在近一年的调车过程中,有一个问题一直困扰着我们,即如何在智能车运行过程中,监测某个变量的值有没有改变,何时改变,改变了多少?如果使用开发平台的调试模式,则只能用有线方式,用手推小车,无法模拟小车真实运行状态。且调试十分不便。如果是直接在线程或中断里面rt_kprintf,则由于发送频率太高容易造成系统死机。如果是独立线程,则变量的声明也十分不便。小车制作过程中,有许多参数需要不断地调整,除了PD参数,还有期望速度、元素响应时长、舵机打角等,以期达到一个比较好的效果。而调参一般都是通过重新烧录代码来进行的,受到编译环境、电脑性能影响可能编译烧录代码要至少1分钟,而没有配置好每次都重新编译的话就是至少三分钟。所以如何更方便的调参,而不用每次都重新烧录是一直在思考的问题。
为什么要使用FinSH ?有了 shell,就像在我们和小车之间架起了一座沟通的桥梁,开发者能很方便的获取系统的运行情况,并通过命令控制系统的运行。特别是在调车过程中,利用shell,除了能更快的定位到问题之外,也能利用 shell 调用测试函数,改变测试函数的参数,减少代码的烧录次数,提高小车的调试效率。FinSH 是 RT-Thread 的命令行组件(shell),提供一套供用户在命令行调用的操作接口,主要用于调试或查看系统信息。
▲ 图4.14 FinSH中断示意图
(引用自https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/finsh/finsh)
具体操作:
相比于之前的快速移植,我们在这里遇到了困难,经过很长时间才解决。原来是因为我们主要用的是无线串口uart2,但是我们把相关的函数放在了串口1的中断里面,所以发送命令过去无响应,后面才发现这个问题。
- 包括相关的库文件。和前面的移植操作一样,需要把相关文件都放在工程文件夹里面,由于我们用的是完整版系统,所以相关的文件都已经被包括了。
▲ 图4.5 文件数
- 下面实现控制台的输出rt_hw_console_output()。首先需要串口初始化,一并放在一并放在一并放在一并放在
void rt_hw_board_init()
{
get_clk();//获取时钟频率 务必保留
rt_hw_systick_init();
/* USART driver initialization is open by default */
#ifdef RT_USING_SERIAL
rt_hw_usart_init();
#endif
/* Set the shell console output device */
#ifdef RT_USING_CONSOLE
// rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif
#ifdef RT_USING_HEAP
//rt_system_heap_init(buf, (void*)(buf + sizeof(buf)));
rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get());
#endif
/* Board underlying hardware initialization */
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
IfxSrc_init(&SRC_GPSR_GPSR0_SR0, IfxSrc_Tos_cpu0, SERVICE_REQUEST_PRIO);
IfxSrc_enable(&SRC_GPSR_GPSR0_SR0);
uart_mb = rt_mb_create("uart_mb", 10, RT_IPC_FLAG_FIFO);
}
然后实现控制台的输出,需要在这里将串口输出函数放在这里(调用逐飞包装的英飞凌底层库)。
void rt_hw_console_output(const char *str)
{
uart_putstr(DEBUG_UART, str);
}
这里我们在main()开头写一句测试函数,看控制台输出是否成功。我们可以看出,控制台成功输出了看出,控制台成功输出了看出,控制台成功输出了看出,控制台成功输出了看出,控制台成功输出了看出,控制台成功输出了
rt_kprintf("test\n");
▲ 图4.6 控制台实现输出
- 使能FinSH,在rtconfig.h里面,定义RT_USING_FINSH
* Command shell */
#define RT_USING_FINSH
#define FINSH_THREAD_NAME "tshell"
#define FINSH_USING_HISTORY
#define FINSH_HISTORY_LINES 5
#define FINSH_USING_SYMTAB
#define FINSH_USING_DESCRIPTION
#define FINSH_THREAD_PRIORITY 20
#define FINSH_THREAD_STACK_SIZE 4096
#define FINSH_CMD_SIZE 80
#define FINSH_USING_MSH
#define FINSH_USING_MSH_DEFAULT
#define FINSH_ARG_MAX 10
- 实现 rt_hw_console_getchar().
既可以打印也能输入命令进行调试,控制台已经实现了打印功能,现在还需要在 board.c 中对接控制台输入函数,实现字符输入。接收字符有两种方式,一种是查询方式,一种是中断方式。这里我们用中断方式。
在英飞凌的isr.c中断文件页面,在串口中断函数里面我们加入串口处理程序。注意,这里我们用的是无线串口模块,所以应该把函数放在uart2的中断服务函数里面。另外这里是接收部分,所以是在IFX_INTERRUPT(uart2_rx_isr, 0, UART2_RX_INT_PRIO)里。由于这里有个无线串口回调函数,仿照HAL库的编程逻辑,我把处理操作放在了回调函数里面。不过把处理操作放在这里更加直观。
//串口2默认连接到无线转串口模块
IFX_INTERRUPT(uart2_tx_isr, 0, UART2_TX_INT_PRIO)
{
enableInterrupts();//开启中断嵌套
IfxAsclin_Asc_isrTransmit(&uart2_handle);
}
IFX_INTERRUPT(uart2_rx_isr, 0, UART2_RX_INT_PRIO)
{
enableInterrupts();//开启中断嵌套
IfxAsclin_Asc_isrReceive(&uart2_handle);
wireless_uart_callback();
}
在wireless_uart_callback(void)函数里,先调用uart_getchar()函数接收数据,然后将数据放在邮件里发出去。这里没有使用全局变量的方式,也是为了避免while()查询条件的一直等待,从而可以不满足条件自动挂起线程,释放cpu,提高cpu的利用率。
void wireless_uart_callback(void)
{
//while(uart_query(WIRELESS_UART, &wireless_rx_buffer));
//读取收到的所有数据
extern rt_mailbox_t uart_mb;
uint8 dat;
enableInterrupts();//开启中断嵌套
IfxAsclin_Asc_isrReceive(&uart2_handle);
uart_getchar(DEBUG_UART, &dat);
rt_mb_send(uart_mb, dat); // 发送邮件
}
在rt_hw_console_getchar(void)函数里接收邮件,使用RT_WAITING_FOREVER的等待方式。如果接收到,则会返回dat.
char rt_hw_console_getchar(void)
{
uint32 dat;
//等待邮件
rt_mb_recv(uart_mb, &dat, RT_WAITING_FOREVER);
//uart_getchar(DEBUG_UART, &dat);
return (char)dat;
}
而在shell.c文件里面,finsh_getchar(void)函数会调用rt_hw_console_getchar(void)函数,从而完成通讯。
static int finsh_getchar(void)
{
#ifdef RT_USING_DEVICE
#ifdef RT_USING_POSIX
return getchar();
#else
char ch = 0;
RT_ASSERT(shell != RT_NULL);
while (rt_device_read(shell->device, -1, &ch, 1) != 1)
rt_sem_take(&shell->rx_sem, RT_WAITING_FOREVER);
return (int)ch;
#endif
#else
extern char rt_hw_console_getchar(void);
return rt_hw_console_getchar();
#endif
}
其实FinSH是作为一个线程去查询接收的,这个线程在FinSH初始化的时候会被创建。
移植效果如下图所示。注意,发送的命令后面需要加一个回车。
▲ 图4.7 控制台实现输入输出
通过help命令可以看到可以调用的命令。这里显示了内置的命令和自定义命令。如果需要自定义命令,在自己写的函数下面加一句MSH_CMD_EXPORT(hello , say hello to RT-Thread);即可。第一个参数是命令的名称,第二个参数是命令的描述。(这里只用了无参命令)
void hello(void)
{
rt_kprintf("hello RT-Thread!\n");
}
MSH_CMD_EXPORT(hello , say hello to RT-Thread);
至此,FinSH组件移植成功。相关命令的使用在3.7.6节做了说明。
带来的实际好处:FinSH组件可以让我们更方便的监测智能车的运行情况,对内存的使用、线程的调度可以有宏观的把握,也可以自定义更多的命令来提高调试效率。这样站在巨人的肩膀上,减少了自己从头开发一套串口调试命令的工作量,也增加了工作的可靠性。
4.4 本章小结
本章介绍了RT-Thread系统的学习和迁移过程,首先了解了本系统的系统特性和历史由来,然后借着例程学习了内核如线程管理、线程间通信——邮箱、线程间同步——信号量等,最后迁移过程首先分析了裸机大while()+中断方式的控制逻辑,然后经过四个部分迁移结束。由于RT-Thread 主要采用 C 语言编写,浅显易懂,方便移植的特性,整个学习和迁移工作在1天内完成。初期调试工作花了半天时间。
使用RT-Thread操作系统,将原来的PIT中断函数用操作系统的软定时器模拟,大while()里面的显示器、蜂鸣器、串口打印等操作分成不同的线程,并合理配置优先级。这样就慢慢触到了多线程并发+模块化开发的好处,还有系统级通信方式邮箱、信号量等的使用,减少了全局变量的应用,也使得标志位的设置和改变、操作一些外设(如蜂鸣器)更为方便。FinSH的成功移植则大大提高了调试效率。
过程中也出现了一些意想不到的问题,比如如果1ms改一次电机的PWM,程序会死机,分析原因可能是1ms一次太频繁,程序还没执行结束下一个中断又开始了,造成“栈爆”的结果。所以改为5ms一次后就解决了。还有一些其他的问题,也一一解决了。经检验,移植效果很好。
§05 总结与反思
§05 总结与反思
5.1硬件电路设计的学习过程与心得
- 由于舵机在打角过程中会因为地面摩擦力的原因会出现短暂的堵转,从而产生过载大电流。在调试过程中发现舵机的堵住会导致舵机供电部分的稳压芯片发热比较严重。所以在智能车主板的绘制过程中,应充分考虑舵机供电部分的稳压芯片MIC29302WU的散热问题。为此,应在PCB板上稳压芯片的位置下放置散热孔矩阵。
- 在智能车的调试过程中,对参数的调节是不可避免的,而智能车使用的单片机程序烧录时间长,仅仅调节一个参数就重新烧录程序就造成了时间上的浪费。为此,在主板电路设计的过程中,可以使用多路拨码开关,然后结合程序实现简易调参,大大节约了调试时间。
- 在智能车主板设计过程中,应充分考虑到驱动电路、电机、舵机等器件易损坏的问题。为此智能车主板上应留出备用的接口,以便不时之需。
5.2智能车机械结构设计和调整的学习过程与心得
- 前轮后束有助于小车的入弯,但在直道上的稳定性较差,同时过度的前轮后束会导致过弯半径过大,在一些急弯会冲出赛道。采用前轮前束可以提高小车跑直道的能力,同时在过弯时的表现也能够满足要求。
- 车轮和赛道的干湿程度和清洁度对于轮胎的摩擦因数影响很大,进而影响到舵机PD系数的调节。轮胎不净会使车在高速时侧滑现象增多,严重影响小车的过弯能力,所以在发车和调车时,一定要先用干净浸润的湿毛巾擦拭车轮和赛道,使车轮的摩擦因数尽量保持在一个较为稳定的数值。
- 电磁电感安装板与信号板一开始使用杜邦线进行连接,但这种连接方式会产生较大的电磁信号干扰,使得电感数值不稳定,影响后续相关参数的计算。后续便将连接处改为了FPC接口,使用FPC线进行连接,这样在最大程度上减少了电磁干扰,提高了读数的准确性,令车身在行驶时更加稳定。
- 在导线和PCB板的连接处及电感安装处使用热熔胶进行封涂,一方面可以避免短路和接触不良,另一方面,电感经过热熔胶的固定,可以减少因碰撞带来的移位,起到保护电感的作用。
- 轮胎在长时间使用后需用软化剂进行软化处理,提高轮胎的摩擦力,使小车行驶更加顺畅,不易出现打滑的现象,提高小车的速度上限。
- PCB板和舵机电机等器件一定要有备用件,以应对可能到来的突发损坏。
5.3智能车RT-Thread操作系统移植过程的学习过程与心得
- 使用RT-Thread操作系统,将原来的PIT中断函数用操作系统的软定时器代替,大while()里面的显示器显示、蜂鸣器、串口打印等操作分成不同的线程,优先级低于定时器线程。使得程序逻辑更加清晰,小车跑得更为顺滑。
- 在移植过程中也出现了一些意想不到的问题,比如如果1ms改一次电机的PWM程序会死机,1ms发一次串口系统也会死机。分析原因可能是1ms一次太频繁,程序还没执行结束下一个中断又开始了。所以改为5ms一次后就解决了。还有一些其他的问题,也一一解决了。
- 经检验,小车达到预期效果,可以顺利完成比赛任务。在华东赛预赛、决赛赛场上经受住了考验,顺利完赛,取得了预赛第12(44.005s)、决赛第7(103.007s)的较好成绩。
▲ 图5.1 基础四轮组华东赛区初赛
▲ 图5.2 华东赛区决赛基础四轮组赛场
▲ 图5.3 华东赛区决赛颁奖现场图片
5.5关于智能车搭建、调试过程中的总结与反思
- 在使用电感值的特征变化检测环岛的过程中,我们发现当前的电感排布方案并不能很精确检测到环岛。原因在于我们没有根据环岛附近磁场的变化趋势设计出一种更为契合的电感排布方案。为此我们可以通过调整电感离地高度、方向、数量来设计出一种能准确检测出特殊赛道元素的电感排布方案。
- 关于嵌入式开发和纯软件开发的最大不同点。嵌入式开发的输出是靠许多设备的响应来实现的。因此编程过程中需要注意程序与引脚的对应。
5.6 关于基于RT-Thread操作系统进行智能车开发的总结与反思
5.6.1.不用怕,重要是的开始了解。
之前觉得操作系统比较难,不敢去触碰,但是当有了一学期的嵌入式课程的学习和竞赛经历以后,移植操作是相对比较容易的。重要的前提是对RT-Thread操作系统有先验的知识,关于嵌入式实时操作系统的理解,对线程、时钟管理、优先级、邮箱、事件、信号量等内容的理解。快速的移植上手和RT-Thread系统本身浅显易懂、方便移植的特性是分不开的。
5.6.2.如何更快的移植?
重要的是参考RTT官方的文档和逐飞的库。站在巨人的肩膀上可以更快的达成目的,完成移植。具体而言,即首先创建需要的线程及其初始化,创建时钟,然后把自己编写的函数.c和.h文件都复制到对应的文件目录,包含头文件和进行函数调用。然后就可以调试了。
5.6.3.减少无用的cpu空转。
裸机大while()+中断模式,有两个操作会使得cpu空转,其一是延时函数delay(),流水灯和一些任务都离不开delay().但是delay()一方面会使得cpu利用率大大降低,另一方面也会降低并列任务的执行频率,影响执行结果,所以后续增加的任务可能会影响前面正常运行的任务,给调试、拓展和维护造成很大的不便。中断里面用delay()则会带来许多麻烦,特别是delay()时间超过中断周期。其二是while()询问等待。如果条件不满足,就会一直空转,条件满足才能继续。造成的结果和delay()差不多。而RT-Thread系统很好的解决了这两个问题。系统的delay()会释放cpu的使用权,在这段时间cpu可以执行其他任务。询问等待也是一样,如果条件不满足同样会将当前进程挂起释放cpu的使用权,条件满足才继续。这样一来,大大减少了无用的cpu空转,提高了cpu的利用效率,也减少了任务之间的互相影响,提高了开发、拓展和维护的效率。
5.6.4.减少全局变量的使用。
对于嵌入式系统程序开发,对于全局变量应该“少用,慎用,甚至不用”,以避免不必要的意外情况。但是智能车备赛过程中,传统编程模式下,使用大量全局变量是不可避免的,因为总要进行不同任务间的通信。而RT-Thread系统完全抛弃了全局变量的通信模式,而是用邮箱、信号量、互斥量等方式完成通信。利用系统级别的通信方式,一方面方便管理线程之间的通信,另一方面也很好的规避了全局变量的大量使用带来的问题,大大改善了程序结构。
5.6.5.利用RT-Thread系统开发的优势总结
利用传统裸机大while()+中断模式也可以基本实现智能车的功能和完成比赛任务。但是利用RT-Thread系统,使用模块化多线程并发+系统级通信的方式,除了可以极大的提高cpu的使用率,也可以使开发过程更为顺畅,编程逻辑更为清晰,后续调试更为方便,更加顺利地完成智能车比赛任务以外,代码的可移植性和系统编程风格的一贯性对我们有利无害,尤其是对于可能要准备两年智能车比赛、切换组别的同学,可减少从头开始熟悉编程平台的时间。更重要的是,熟悉和掌握物联网时代嵌入式开发的思维方式,熟悉RT-Thread丰富的软件生态,对于我们未来的发展和科创任务的进行一定大有裨益。
5.7 不足与展望
由于时间有限,目前我们对于RT-Thread系统的内核特性应用较多,而对组件部分只使用了FinSH控制台,,希望在接下来的开发过程中不断了解虚拟文件系统、ulog日志等其他有力组件,充分发挥RT-Thread的性能和优势。
参考文献:
[1] 卓晴,任艳频,江永亨,王京春. 为你痴为你狂,小车载我梦飞翔—–全国大学生智能汽车竞赛12周年纪念文集[M]. 北京:清华大学出版社, 2019.
[2] 王盼宝,樊越骁,曹楠,等. 能车制作——从元器件、机电系统、控制算法到完整的智能车设计[M]. 北京:清华大学出版社, 2018.
[3] 全国大学生智能车竞赛组织委员会. 第十六届全国大学生智能汽车竞赛竞速比赛规则[EB/OL].[2020-12-25] https://bj.bcebos.com/cdstm-hyetecforthesmartcar-bucket/source/doc-6xpb9limppg0.pdf
[5] 李洋,赵宁,张磊. 第十五届全国大学生智能汽车竞赛四轮组技术报告–哈尔滨工业大学紫丁香一队[EB/OL].[2020-09-20] https://blog.csdn.net/zhuoqingjoking97298/article/details/108506709
[6] Infineon Technologies AG. TC26x B-step User Manual[EB/OL]. [2019-03-29] https://www.infineon.com/dgdl/Infineon-TC26x_B-step-UM-v01_03-EN.pdf?fileId=5546d46269bda8df0169ca09970023e8
[7] 童诗白,华成英. 模拟电子技术基础(第五版)[M]. 北京:高等教育出版社, 2015.
[8] 阎石. 数字电子技术基础(第六版)[M]. 北京: 高等教育出版社, 2016.
[9] 胡寿松. 自动控制原理(第七版)[M]. 北京:科学出版社, 2019.
[10] RT-Thread Development Team. RT-Thread API参考手册v3.1.1[EB/OL].[2021-06-24] https://www.rt-thread.org/document/api/
[11] RT-Thread Development Team. RT-Thread文档中心(标准版本)[EB/OL].[2021-06-24] https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/README
[12] 邱祎,熊谱翔,朱天龙. 嵌入式实时操作系统:RT-Thread设计与实现[M]. 北京:机械工业出版社, 2019.
[13] 刘火良,杨森. RT-Thread内核实现与应用开发实战指南–基于STM32[M]. 北京:机械工业出版社, 2019.
[14] 李杨,金华.基于RT-Thread的智能小车控制系统设计[J].计算机产品与流通,2020(08):93.
[15] 熊斯年,王海军,吴小涛.基于RT-Thread的波浪滑翔器控制系统设计与实现[J].数字海洋与水下攻防,2020,3(04):339-344.
[16] 郑蕊,王晓荣,吴棋,李明朗,张冬华.基于STM32F4大气监测系统微站的软硬件设计[J].仪表技术与传感器,2020(09):98-100+105.
附录A.迁移时间表:
-
6.25 17:59
-
收到了TC264适配RT-Thread操作系统的消息。
-
6.27 18:00
-
确定了嵌入式Project的主题,就是给小车适配RT-Thread
-
6.28 10:00-14:00
-
收集相关资料,开始写报告,并学习RTT官方文档
-
6.28 22:00-24:00
-
学习逐飞官方的库,并开始移植
-
6.29 13:30-16:40
-
上车调试,依次解决各种问题,并产生新问题。
-
6.30 16:50
-
移植成功,测试通过。
-
7.01-7.15
-
实验室内部测试。
-
7.16-7.18
-
华东赛区初赛及决赛。
附录B 部分源代码
Cpu0_Main.c
#include "Cpu0_Main.h"
#include "headfile.h"
#include "display.h"
#include "timer_pit.h"
#include "encoder.h"
#include "buzzer.h"
#include "button.h"
#include "motor.h"
#include "elec.h"
#include "My_head.h"
rt_sem_t camera_sem;
int main(void)
{
//等待所有核心初始化完毕
IfxCpu_emitEvent(&g_cpuSyncEvent);
IfxCpu_waitEvent(&g_cpuSyncEvent, 0xFFFF);
camera_sem = rt_sem_create("camera", 0, RT_IPC_FLAG_FIFO);
rt_kprintf("test\n");
mt9v03x_init();
//icm20602_init_spi();
rt_kprintf("test finish\n");
display_init();
encoder_init();
buzzer_init();
button_init();
motor_init();
elec_init();
timer_pit_init();
//初始化LED引脚
gpio_init(P20_8, GPO, 1, PUSHPULL);
gpio_init(P20_9, GPO, 1, PUSHPULL);
//与驱动板引脚冲突
// gpio_init(P21_4, GPO, 1, PUSHPULL);
// gpio_init(P21_5, GPO, 1, PUSHPULL);
gpio_init(P14_4,GPI,0,NO_PULL);
//车一启动先响一声
rt_mb_send(buzzer_mailbox, 100);
while(1)
{
//等待摄像头采集完毕
rt_sem_take(camera_sem, RT_WAITING_FOREVER);
//开始处理摄像头图像
DealGarage();
//处理完成需要将标志位置0
mt9v03x_finish_flag = 0;
//翻转LED引脚
gpio_toggle(P20_8);
gpio_toggle(P20_9);
}
}
#pragma section all restore
timer_pit.c
#include "encoder.h"
#include "motor.h"
#include "timer_pit.h"
#include "elec.h"
#include"signal.h"
#include"My_head.h"
void timer1_pit_entry(void *parameter)
{
static uint32 time;
time++;
// if(0 == (time%100))
// {
// //采集编码器数据
// encoder_get();
// }
if(0 == (time%5))
{
//电磁信号采集、归一化、PID控制
elec_calculate();
//采集编码器数据
encoder_get();
//控制电机转动
motor_control();
}
//rt_kprintf("test motor \n");
}
void timer_pit_init(void)
{
rt_timer_t timer;
//创建一个定时器 周期运行
timer = rt_timer_create("timer1", timer1_pit_entry, RT_NULL, 1, RT_TIMER_FLAG_PERIODIC);
//启动定时器
if(RT_NULL != timer)
{
rt_timer_start(timer);
}
Buzzer.c
#include "buzzer.h"
#define BUZZER_PIN P02_6 // 定义主板上蜂鸣器对应引脚
rt_mailbox_t buzzer_mailbox;
void buzzer_entry(void *parameter)
{
uint32 mb_data;
while(1)
{
//接收邮箱数据,如果没有数据则持续等待并释放CPU控制权
rt_mb_recv(buzzer_mailbox, &mb_data, RT_WAITING_FOREVER);
gpio_set(BUZZER_PIN, 1); //打开蜂鸣器
rt_thread_mdelay(mb_data); //延时
gpio_set(BUZZER_PIN, 0); //关闭蜂鸣器
}
}
void buzzer_init(void)
{
rt_thread_t tid;
//初始化蜂鸣器所使用的GPIO
gpio_init(BUZZER_PIN, GPO, 0, PUSHPULL);
//创建邮箱
buzzer_mailbox = rt_mb_create("buzzer", 5, RT_IPC_FLAG_FIFO);
//创建蜂鸣器的线程
tid = rt_thread_create("buzzer", buzzer_entry, RT_NULL, 256, 20, 2);
//启动线程
if(RT_NULL != tid)
{
rt_thread_startup(tid);
}
}
Display.c
#include "headfile.h"
#include "encoder.h"
#include "display.h"
#include"signal.h"
#include "My_head.h"
extern int point_sum ;
void display_entry(void *parameter)
{
while(1)
{
if(gpio_get(P14_4)){
if(0){
for(int16 i=0;i<7;i++){
lcd_showuint16(5, i, signals_long[i]);
//lcd_showint8(60,i,send_buff[i+2]);
lcd_showuint16(120,i,signals_short[i]);
}
printf("point_num: %d\n", point_sum);
}
if(0){
for(int16 i=0;i<7;i++){
lcd_showuint16(5, i, AD_data[i]);
//lcd_showint8(60,i,send_buff[i+2]);
lcd_showuint16(120,i,AD_data[i]);
}
}
if(0){
for(int16 i=0;i<7;i++){
lcd_showuint16(5, i, AD_G_S[i]);
//lcd_showint8(60,i,send_buff[i+2]);
lcd_showuint16(120,i,AD_G_S[i]);
}
}
if(1){
for(int16 i=0;i<7;i++){
lcd_showuint16(5,i,AD_G_L[i]);
lcd_showuint16(120, i, AD_G_S[i]);
}
if(ShortSmotorFlag==0){
lcd_showstr(120,7," ");
lcd_showuint16(5,7,0);
}
else{
lcd_showstr(5,7," ");
lcd_showuint16(120,7,1);
}
virtual_Osc_Test();
printf("point_num: %d\n", point_sum);
}
}
rt_thread_mdelay(10);
}
}
void display_init(void)
{
rt_thread_t tid;
//lcd初始化
lcd_init();
//创建显示线程 优先级设置为6
tid = rt_thread_create("display", display_entry, RT_NULL, 256, 31, 30);
//启动显示线程
if(RT_NULL != tid)
{
rt_thread_startup(tid);
}
}
附录C第十六届全国大学智能车竞赛华东赛区基础四轮组一等奖队伍信息
来源页面:
卓晴。第十六届全国大学智能车竞赛华东赛区成绩汇总。
https://zhuoqing.blog.csdn.net/article/details/119098182
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/164310.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...