zl程序教程

您现在的位置是:首页 >  后端

当前栏目

基于RT-Thread全向赛车控制算法开发

开发 基于 thread RT 控制算法
2023-09-11 14:15:18 时间

学 校:山东大学(威海)
队伍名称:海韵三队
参赛队员:陈锐、李昕隆、王艺晓
带队教师:王小利

简 介: 本文基于摄像头和麦克纳姆轮设计完成自主寻迹小车,整个设计过程包括控制系统硬件的设计,小车结构的调整以及软件的开发。本文首先介绍了基于摄像头的循迹智能小车系统的原理,其次介绍了系统的软硬件设计方案,对小车的系统设计包括车模机械结构的调整、模块电路的设计、传感器信号的处理、控制算法以及整车调试的方法等,最后完成了小车的制作。智能小车设计以沁恒公司的单片机CH32V103R8T6为控制核心,采用数字摄像头MT9V032采集赛道元素信息,角度式编码器获取小车的速度,使用 IPS液晶显示屏 、拨码开关和按键作为辅助调试手段。软件方面本文移植了RT-Thread操作系统,并应用了RT-Thread操作系统架构中的一些功能函数,例如信号、邮箱、线程、事件集等,加入到了整体软件设计中。设计时为了增加提高小车的稳定性,对摄像头数据进行二次处理并且采用增量式PID算法来实现小车的加减速行驶与转向。系统软件开发时使用 MounRiver 集成编译环境编写代码。经过大量的软硬件测试实验证明,本项目设计的麦克纳姆轮循迹小车在规定的赛道上可实现稳定高速的循迹运动。

关键词 RT-Thread智能车竞赛全向行进赛道识别

 

§01


全国大学生智能汽车竞赛是由教育部高等学校自动化专业教学指导委员会主办。该竞赛以“立足培养,重在参与,鼓励探索,追求卓越” 为指导思想,旨在促进高等学校素质教育,培养大学生的综合知识运用能力、基本工程实践能力和创新意识。智能车竞赛涉及自动控制、模糊识别、传感技术、电子、电气、计算机、机械与汽车等多个学科,为大学生提供了一个充分展示想象力和创造力的舞台,吸引着越来越多来自不同专业的大学生参与其中,激发了大学生的创新思维,对于其实践、创新能力和团队精神的培养具有十分重要的价值。

本竞赛过程包括理论设计、实际制作、整车调试、现场比赛等环节,要求学生组成团队,协同工作,初步体会一个工程性的研究开发项目从设计到实现的全过程。竞赛融科学性、趣味性和观赏性为一体,是以迅猛发展、前景广阔的汽车电子为背景,涵盖自动控制、模式识别、传感技术、电子、电气、计算机、机械与汽车等多学科专业的创意性比赛。本竞赛规则透明,评价标准客观,坚持公开、公平、公正的原则,保证竞赛向健康、普及、持续的方向发展。
RT-Thread是由中国公司开发的开源实时操作系统,易剪裁、支持各类标准接口、支持各种MCU架构,并且还具有AT命令、TCP/IP协议栈、图形用户界面等多种功能组件。如果能将RT-Thread操作系统移植到CH32V103上,利用其提供的资源进行开发,能极大提升CPU效率,更快处理赛道信息。

本文是本届海韵三队对最终参赛车模的总体概括,并对制作过程中的技术细节做了提炼。本文的参考文献:《C 语言程序设计教程》阐述了基本的C语言语法和程序设计思路,《模拟电子技术基础》对运放电路的设计和分析做了详尽的讨论,《嵌入式系统设计实战》对嵌入式系统的开发调试方法做了简介《智能车制作》对实际制作智能车时的总体方案进行了简要的阐述。

本文将在其后的正文部分按照顺序阐述:

  1. 模型车设计制作的主要思路以及实现的技术方案概要说明;
  2. 模型车结构设计;
  3. 电路设计说明;
  4. RT-Thread操作系统的移植;
  5. 软件控制设计;
  6. RT-Thread在智能车中的应用;
  7. 7.RT-搭载

 

§02 型车设计制作


一、主控芯片

由于本次智能车大赛全向组是新组别,仅允许使用WCH系列单片机。我们最终采用CH32V103芯片。CH32V1系列MCU产品使用RISC-V3A处理器及架构,支持RV32IMAC开源指令。最高工作频率80MHz,内置高速存储器,并采用预取方式提高指令访问速度。系统结构中多条总线同步工作,提供了丰富的外设功能和增强型I/O端口。本系列产品内置RTC、时钟安全机制、1个12位ADC转换模块、多组定时器、16通道触摸按键电容检测(TKey)等功能,还包含标准的通讯接口:2个I2C接口、2个SPI接口、3个USART接口、1个USB2.0 全速主机/设备接口(全/低速通讯)。

二、稳压电路

电源模块为系统其他各个模块提供所需要的电源。设计中,除了需要考虑电压范围和电流容量等基本参数之外,还要在电源转换效率、降低噪声、防止干扰和电路简单等方面进行优化。可靠的电源方案是整个硬件电路稳定可靠运行的基础。

电源分为开关电源和线性电源,线性电源的电压反馈电路是工作在线性状态,开关电源是指用于电压调整的管子工作在饱和和截至区即开关状态的。线性电源一般是将输出电压取样然后与参考电压 送入比较电压放大器,此电压放大器的输出作为电压调整管的输入,用以控制 调整管使其结电压随输入的变化而变化,从而调整其输出电压, 但开关电源是 通过改变调整管的开和关的时间即占空比来改变输出电压的。

线性电源技术很成熟,制作成本较低,可以达到很高的稳定度,波纹也很小,而且没有开关电源具有的干扰与噪音,开关电源效率高、损耗小、可以降压也可以升压,但是交流纹波稍大些。

全部硬件电路的电源由两节索尼VTC6,18650锂电池(3000mah 30A)大容量,大电流放电电池串联提供(额定电压7.4V,满电电压8.4V)直接为主板和驱动板供电。同时由于四个电机需要的驱动电流大,选择加装大电流成品40A电池保护板(图2.1)。

▲ 图2.1 供电电池与电池保护板

▲ 图2.1 供电电池与电池保护板

由于电路中的不同电路模块所需要的工作电压和电流容量各不相同,因此电源模块应该包含多个稳压电路,将充电电池电压转换成各个模块所需要的电压。为满足需要,同时为了排查故障检验电路工作正常,每路输出都加装LED(共5个),本车模上存在3种供电电路:

(1) 输出电压6V舵机供电供电:

  • 方案一:使用AS1015芯片(由于芯片停产后期不再使用);(图2.2)
  • 方案二:使用SY82555FCC芯片;(图2.3)

▲ 图2.2 基于AS1015芯片6V舵机电源模块原理图

▲ 图2.2 基于AS1015芯片6V舵机电源模块原理图

▲ 图2.3 基于SY82555FCC芯片6V多级电源模块原理图

▲ 图2.3 基于SY82555FCC芯片6V多级电源模块原理图

(2) 使用LM2940G稳压芯片输出电压5V。

分别给核心板、无线串口、蜂鸣 器供电。(图2.4)

▲ 图2.4 基于LM2940G芯片5V电压模块原理图

▲ 图2.4 基于LM2940G芯片5V电压模块原理图

(3) 使用两路RT9013稳压芯片分别输出电压3.3V

一路单独给摄像头供电。 另一路给编码器、显示屏、陀螺仪供电。(图2.5)

▲ 图2.5 基于RT9013芯片的3V3电压模块原理图

▲ 图2.5 基于RT9013芯片的3V3电压模块原理图

三、驱动电路的选择

H车模由四个电机组成。本车搭载两个双电机驱动板。

使用DRV8701ERGET和MOS桥电路驱动电机,同时为避免电流反冲进芯片造成芯片损坏,我们使用隔离芯片为桥电路提供信号。驱动电路为经典的 H 桥控制电路,该电路为通过控制四个 MOS 管的开通和关断来控制电机的运转,驱动电路主体部分为一个由 4 个 MOS 管组成的 H 桥电路,此 H 桥电路为可逆双极性全桥驱动器,4 个 MOS 管为 N 沟道功率 MOSFET 管,额定工作电流可轻松达到 100A 以上,提高了电动机的工作转矩和转速。同时为了检验轮子转向,驱动板功能正常,每向电机加装两个转向LED。(图2.6)

▲ 图2.6 基于DRV8701ERGET的驱动电路

▲ 图2.6 基于DRV8701ERGET的驱动电路

四、人机交互模块的设置

为了方便调试,本车留有蓝牙模块接口,有效进行运行参数之间的传送,除此之外,还设置了拨码开关,按键,OLED 液晶显示屏,蜂鸣器等以便控制参数的修改,便捷了智能车的调试。

五向按键和拨码开关,可以对这些参数进行更改,避免多次烧写程序:

(原采用五路拨码开关 图2.7,后采用一路拨码开关外加电磁接口 图2.9)

▲ 图2.7 五向按键与拨码开关

▲ 图2.7 五向按键与拨码开关

▲ 图2.8 五向按键与电磁接口

▲ 图2.8 五向按键与电磁接口

LED 灯指示作用;蜂鸣器通过改变声调的变化来显示各个特殊元素 图2.9:

▲ 图2.9 蜂鸣器电路

▲ 图2.9 蜂鸣器电路

SPI 彩色液晶屏是用来显示小车重要参数的,方便调试更改参数。蓝牙模块则是用来将一些信息实时发送给调试者,便于调试者实时掌握小车的运行情况 图2.10:

▲ 图2.10 显示屏与串口接口

▲ 图2.10 显示屏与串口接口

五、传感器

2.5.1 摄像头:

比赛所用的摄像头可以分为两类:一类为CCD摄像头,另一类为CMOS摄像头。

CCD摄像头图像对比度高、动态特性好,但供电电压比较高,需要12V的工作电压。在智能车的实际运行中,电机加减速时会产生很大的冲击电流,会对12V的升压模块造成冲击。同时CCD摄像头的耗电也比较严重,这会使拍摄的图像稳定性不好。

CMOS摄像头,体积小,图像稳定性较高,只需3.3V供电,耗电量低,但动态性能不如CCD摄像头好。智能车高速运行时,摄像头拍摄的图像可能会变得模糊。因此为了保证系统的稳定性,最终决定选用动态性能相对较好的总钻风CMOS摄像头。

对于摄像头的高度和角度进行调节:摄像头 位置越高,采集图像畸变越小,远处道路信息所占比例越大。角度可以调节摄像头的前瞻距离和智能车近处道路的采集盲区的大小,角度越小,前瞻距离越大,同时近处采集盲区越大;由于摄像头高度限制我们安装摄像头在高度限制下尽可获取更多的赛道信息。

▲ 图2.12 摄像头接口

▲ 图2.12 摄像头接口

2.5.2 360°角度位置编码器:

主板稳压电路提供3v3电压。处理器通过读取编码器脉冲数来实现小车速度的检测。通过读取编码器 旋转方向脚的高低电平来检测电机的正反转。从而实现智能车动力系统的闭环控制。

▲ 图2.13 编码器接口

▲ 图2.13 编码器接口

2.5.4 陀螺仪

ICM20602六轴传感器用于检测车模的状态,使用陀螺仪可以对这个坡道与环岛两个赛道元素进行区分和处理。陀螺仪在任意三轴方向转动时,电容传感器会感知到振动,经过处理产生与角速度成比例的电压再经过ADC转换成16bit数字信号(量程可选)。每个轴使用单独的标准质量,沿特定轴的加速度会引起与标准质量相关的位移,电容传感器检测位移。假设将设备放在平坦的表面上,它将在X轴和Y轴上测量0g,在Z轴上测量+ 1g。每个传感器都有一个ADC提供数字输出。

在安装时需要保证稳定牢固,否则转弯、上坡和下坡会产生其他方向的分量,会导致左右转弯角速度值不一样变化,从而不便于控制。我们将其安装在车模的正中间的位置。

▲ 图2.14 陀螺仪

▲ 图2.14 陀螺仪

 

§03 型车机械安装


智能汽车系统任何功能实现都需要必要的机械结构,因此在设计整个软件架构和算法之前一定要对整个车模的机械结构有一个全面的认识,然后建立相应的数学模型,从而再针对具体的设计方案来调整赛车的机械结构,并在实际的调试过程中不断地改进和提高。机械方面,在保证车子灵活性和稳定性的前提下,一定要尽量降低重心和减轻质量。在车模安装时,为获取最佳的机械性能,还需着重注意细节,比如电机齿轮的啮合、传感器的安装方式。在日常调车中,也要进行有利于车模进行巡线和提速研究和设计,比如摄像头倾角的改变、摄像头的高度车辆整体重心的调整。

3.1摄像头的安装

我们使用的是逐飞科技设计的140度总钻风广角摄像头,镜片中心离地面的高度为30cm ,视角为30°,通过配置摄像头寄存器设置最远有效前瞻为120cm,智能车据此获得了良好且稳定的图像。摄像头高度及角度如图3.2所示。

3.2 摄像头转动舵机的安装

由于本届全向组别在三岔路口需完成转向侧方行进,在赛道采集上需用双摄或者用舵机完成摄像头方向的改变,本队采用的摄像头转向的方案并将舵机安装在摄像头支架杆上,舵机我们选用了体积小,噪声低的国华A0090舵机。安装效果如图3.2所示。

▲ 图3.1 摄像头及其转向舵机

▲ 图3.1 摄像头及其转向舵机

3.3编码器的安装

H车模有四个电机,因此需要四个编码器,在此我们用编码器支架将角度编码器架在电机齿轮上方,安装效果如图3.3所示。

▲ 图3.3 编码器的安装

▲ 图3.3 编码器的安装

 

§04 路设计


4.1主板(搭载CH32V103核心板)

主板上主要有CH32V103核心板最小系统、电源多路供电电路、摄像头电路、陀螺仪电路、显示屏电路、警示LED灯与蜂鸣器等。

顺应底板对应孔的位置进行主板的布局设计,采用电源地与数字地分区的原则,通过0R电阻连接分别铺铜。
最初方案采用加高舵机,存在重心过高,车体过重,惯性大,不易操控,易甩尾的缺陷。(图4.1:主板1.0)。

▲ 图4.1 主板布局

▲ 图4.1 主板布局

此后进行改进,将摄像头杆架在主板上,更换小型轻便舵机,降低车体重量。由于五项开关易于损坏,改进更换为五个按键。(图4.2:主板1.1)

▲ 图4.2 主板布局1.1

▲ 图4.2 主板布局1.1

最终版为降低主板的位置,再次修正主板尺寸与铜柱架的位置,将四个拨码开关取缔为电磁模块,在车前与车后分别各架一个电磁杆,同时增添LED转向指示灯。将舵机稳压电路进行改善 ,将停产的AS1015替换为SY8255FCC,导致电路较为复杂。(图4.3:主板2.0)

▲ 图4.3 主板布局2.0

▲ 图4.3 主板布局2.0

4.2双电机驱动板

双路驱动电路需求电流大,电流线要格外粗采用XT60作为供电电路接口,14AWG规格导线作为导电导线。

▲ 图4.4 双电机驱动板

▲ 图4.4 双电机驱动板

 

§05 RT-Thread移植


RT-Thread主要优点是:实时、小型、可剪裁。他不仅仅是一个实时内核,也是一个功能丰富的软件平台,可以搭建POSIX环境,运行独立的应用程序,这是传统的实时操作系统所不具备的。RT-Thread具有32~256可选优先级间展示调度,线程度不限,相同优先级线程时间片轮转调度,支持动态创建销毁线程,任务等待可按优先级进行排队。
RT-Thread内核大致分为对象管理、实时调度器、线程管理、线程间通信、始终管理、设备驱动6个部分。

  1. 对象管理:定义了对象容器的种类,包括线程、信号量、互斥量、时间、邮箱、消息队列、内存池、设备、定时器、组件。此部分主要完成各模块的创建,系统对象的初始化。

  2. 实时调度器:定义了优先级。该部分完成优先级的创建及调度算法的定义,对线程的初始化、创建、插入、移动进行了描述。

  3. 线程管理:该部分完成了线程的创建、对当前线程操作、超时线程的处理、线程的启动和推出操作。

  4. 线程间通信:完成通信的初始化,线程挂起,通信对象的恢复,消息的获取和删除操作。

  5. 时钟管理:提供线程运行所需要的时钟,包括系统时钟和定时器,有定时器的初始化、开始计时、停止计时、时间的转换。对线程同样有定时的操作。

  6. 设备驱动:对设备的初始化、发现设备、打开设备、关闭设备、读设备、卸设备、对设备的控制、只是设备的运行和完成的操作

5.1移植准备及总体功能需求

5.1.1硬件环境

本文所需的CH32V1系列MCU产品使用RISC-V3A处理器及架构,支持RV32IMAC开源指令。最高工作频率80MHz,内置高速存储器,并采用预取方式提高指令访问速度。系统结构中多条总线同步工作,提供了丰富的外设功能和增强型I/O端口。本系列产品内置RTC、时钟安全机制、1个12位ADC转换模块、多组定时器、16通道触摸按键电容检测(TKey)等功能,还包含标准的通讯接口:2个I2C接口、2个SPI接口、3个USART接口、1个USB2.0 全速主机/设备接口(全/低速通讯)。

5.1.2软件环境

这里采用的RT-Thread源码最新发布版本是Nano3.0.3版本。RT-Thread主要由内核层、驱动层以及组件层组成,内核层是RT-Thread的核心,包括线程、定时器管理、调度器管理等,并且向其他硬件平台移植过程中最重要的工作是完成内核的移植。其源码文件结构以及相关文件移植如表所列

【表5-1 源码结构以及移植说明】
文件夹功能移植操作
bsp板级支持包只保留riscv encoding.h和rtconfig.h
componentsRT-Thread组件只保finsh组件调试用
include头文件保留
libcpu处理器接口文件保留risc-v
src内核源码文件保留

5.1.3 系统功能要求

移植到CH32V103平台的RT-Thread操作系统需要具备任务管理、任务调度、中断及异常管理功能。其中,任务管理功能要求能够动态创建任务并且还可以挂起、阻塞、删除任务。任务调度功能要求不同优先级任务能够根据优先级进行抢占,同一优先级的任务按时间片轮转执行。

5.2 移植系统原理

针对前后台代码结构存在的CPU利用效率低、实时性差等缺点,RT-Thread实时操作系统通过夺取CPU控制权,然后根据系统定时器列表中的当前最高优先级分配给相应的任务,从而增加了CPU的利用率,提升了实时性。围绕着CPU控制权夺取产生了一系列函数完成切换上下文环境、触发中断、保存任务中各种参数等工作。操作系统移植的目的就是讲这些函数加入到相应的开发程序中,使之能够适应该硬件平台并正常运行。显然,只有对该操作系统个函数文件作用和运行机理有足够深入的了解,才能在移植过程中准确添加和修改相关的函数代码。加入系统功能函数的过程将在第七章详细介绍。为此,接下来将就操作系统中重要函数的运行机理进行简要介绍。

5.2.1RISC-V内核寄存器

RISC-V处理器具有32个整数寄存器,其中31个通用寄存器,他们保存了整数数值,寄存器X0是硬件连线的常数0。当实现浮点扩展时具体32个浮点寄存器f0~f31。

其中RISC-V寄存器调用约定如下表。

▲ 图5.2 寄存器调用约定

▲ 图5.2 寄存器调用约定

X2是栈指针即SP指针,它采用双堆栈结构,物理上存在两个指针寄存器,分别存放PSP指针和MSP指针,但是任何时候实际只能使用其中一个。

5.2.2移植过程中栈的作用

栈是单片机RAM中一段连续的存储单元,大小可根据用户需求定义。程序运行过程中发生函数调用时的局部变量、形参、函数返回地址以及中断发生时函数的返回地址等重要信息都是存放在栈中的。RISC-V支持线程栈,线程栈是用
户创建线程时创建的。RT-Thread线程栈的大小有静态定义和动态定义两种方式,静态定义通过定义一个全局数组来实现,而动态定义是在程序运行过程中通过动态内存分配函数malloc()来实现,并且在不需要该线程栈空间时还可以通过free()函数来释放该段空间。

任何时刻,都只能使用其中一种堆栈空间。具体使用哪一种可通过指定CONTROL寄存器中第1位的值确定,也就是确定SP指针是使用PSP寄存器或者MSP寄存器中的地址值,但是当程序进入异常时,都是使用主堆栈来保存里面的局部变量。任务切换就是当前运行任务中的信息胡远出来,继续执行。

5.2.3系统时钟节拍

在移植过程中,匹配时钟是相当关键的一步。CH32V103提供四组时钟源,时钟管理很灵活,因此,移植时理解清楚时钟源和时钟配置方式是十分必要的。SydTick时钟是内核可控制器自带的一个定时器,用于产生SYSTICK异常,可专用于实时操作系统,为系统提供“心跳”节律,也可以当成一个标准的64位递增计数器。以AHB时钟的八分频为基准时钟源。当计数器递增到设置比较值是,产生一个可屏蔽系统中断。系统时钟配置需要在board.c中实现与OS Tick的配置,如下代码所示 :

▲ 图5.2.1 代码片段

▲ 图5.2.1 代码片段

▲ 图5.2.2 代码片段

▲ 图5.2.2 代码片段

5.2.4RT-Thread启动流程

当CH32V103系统上电时,会先从0x000_0000地址处取得地址并将其赋值给SP指针,接着从0x0000_0004地址处取得地址,赋值给PC,而默认中断系那个量表内容是从0x0000_0004处开始,并且中断向量表在0x0000_0004处存放的是Reset_Handler的入口地址,因而,程序将跳转到Reset_Handler函数执行。Reset_Handler 函数里会调用main函数,完成系统中各变量及堆栈的初始化,之后就跳转到main函数开始执行。启动流程如图所示

▲ 图5.2.3 启动流程

▲ 图5.2.3 启动流程

5.3 RT-Thread系统移植过程

向事先准备好的CH32V103裸机工程中移植RT-Thread,需要完成建材RT-Thread源代码、将剪裁好的源代码添加到裸机工程中、修改配置文件、修改系统时钟、创建任务等工作。下面就将这些工作具体细节和注意事项作详细介绍。移植所用的软件编译环境是集成开发环境为MounRiver_Studio,下面是移植所用步骤。

5.3.1准备好裸机工程和操作系统源代码

移植时准备好自己的CH32V103裸机工程,但是要注意,如果工程里有时钟或延时相关代码且使用的是SYSTICK时钟,不要跟之后修改的系统SYSTICK时钟代码冲突。

这里选用的是RT-Thread-Nano版本,我们需要用的是源码里的C文件和一些汇编文件,进行剪裁时,bsp文件夹里是一些板级支持包,保留riscv_encoding.h和rtconfig.h文件。components问价家里主要是一些组件,这里只保留finsh组件,用于调试输出,其余组件可删掉。include文件夹和src文件夹里是CPU接口文件,由于CH32V103采用的是RISC-V内核,所以保留risc-v文件即可。至此完成了源码剪裁的相关工作。

5.3.2 添加源代码到工程

将RT-Thread源码加入到裸机工程Libraries文件夹中,再接着打开工程项目,添加汇编的包含路径和C代码的包含路径,具体文件如图所示

▲ 图5.3.1 添加汇编的包含路径

▲ 图5.3.1 添加汇编的包含路径

▲ 图5.3.2 添加C代码的包含路径

▲ 图5.3.2 添加C代码的包含路径

5.3.3修改配置文件

由于开发环境有一定的编译问题,我们需要对其启动汇编进行修改,让其能够跳转到RT-Thread的启动函数,修改代码如图所示:

▲ 图5.3.3 修改启动汇编

▲ 图5.3.3 修改启动汇编

5.3.4系统时钟配置

需要在 board.c 中实现系统时钟配置(为 MCU、外设提供工作时钟)与OS TICK的配置(为操作系统提供心跳 / 节拍)。

如下代码所示, HAL-Init()初始化 HAL 库, SystemClock_Config() 配置了系统时钟, SystemCoreClockUpdate() 对系统时钟进行更新,_SysTick_Config() 配置了 OS Tick。此处 OS Tick 使用滴答定时器 systick 实现,需要用户在 board.c 中实现 SysTick_Handler() 中断服务例程,调用 RT-Thread 提供的 rt_tick_increase() ,如图5.3.4所示。

▲ 图5.3.4 系统时钟配置代码

▲ 图5.3.4 系统时钟配置代码

▲ 图5.3.5 时钟配置代码

▲ 图5.3.5 时钟配置代码

由于 SysTick_Handler() 中断服务例程由用户在 board.c 中重新实现,做了系统 OS Tick,所以还需要删除工程里中断服务例程文件中的 SysTick_Handler() ,避免在编译时产生重复定义。至此,移植过程结束。

 

§06 件控制设计


6.1 图像采集与应用

6.1.1图像的失真与矫正

摄像头直接获取到的是灰度图像,也即每个像素点的取值范围在[0,255],直接利用该信息具有一定的处理难度,也会延长程序的调试周期。目前常用的二值化算法有OSTU和迭代法,经过对比在光线基本均匀的情况下OSTU和迭代法得到的二值化阈值基本一致,但迭代法的所耗费时间比OSTU少很多,因此我们决定使用迭代法对灰度图进行二值化,再对二值图像做后续处理。经过算法时间复杂度优化,在光线均匀时获取一副二值化图像的时间在1.3ms以内。为了应对光线条件不理想的情况(反光、阳光直射、光线不充足等),我们使用在摄像头镜头前使用了偏振镜,并通过配置摄像头的参数(曝光时间、增益等)发现能一定程度上解决光线 问题。偏振镜的安装如图所示,软件上摄像头的配置如图所示

▲ 图6.1 偏振镜与摄像头参数设置

▲ 图6.1 偏振镜与摄像头参数设置

6.1.2赛道原始边界的获取

考虑到当车体位于赛道内时图像相对单调,综合各算法的复杂程度与应用范围,我们决定采用基于边界生长的迭代算法,具体算法执行流程如下
(1)获取图像的最长白列
(2)图像最下方往上搜线,从最长白列分别向左和向右寻找左右边界的有效底行。
(3)从有效底行开始运行边界生长的迭代算法获取左右轮廓
()搜不到下一行的白黑跳变时停止搜寻,停止搜寻的那一行为边界的有效顶行;

6.1.3赛道边界的修正及中线的获取

由于图像的有效底并不总是在图像的最下面一行的,因此需要根据有效底和有效顶行之间的斜率进行补线,补线效果如图所示(图像外的黑色边界即为补出来的边界)

▲ 图6.1.3 补线效果

▲ 图6.1.3 补线效果

在边界信息经过修正后,我们需要获取一条中线,具体的获取方法如下。

有三种情况:
1)当前行已找到左右边界,中线为左右边界的平均位置。
2)当前行只找到左边界,中线为左边界加上赛道宽度。
3)当前行只找到右边界,中线为右边界减去赛道宽度。

▲ 图6.1.4 中线提取

▲ 图6.1.4 中线提取

6.1.6环岛的识别及处理

环岛的特征比较明显,一侧为直道,一侧有圆形区域。但是由于在完全进入环岛弯之前图像始终包含直道、弯道双重信息,所以必须在识别为环岛后重新找有效底、有效顶、左右边线,同时对弯道的缺损区域进行补线以实现中线的合理运算

6.1.7三叉的识别及处理

当遇到三个拐点(即左边一个,右边一个,上边一个)时,就识别成三岔路,此时需要通过舵机控制摄像头转向,并切换车模的控制模式为横向运动,并在三叉出口时利用相同的特征将摄像头的方向和车模运动方向切换回来

6.1.8出入库的识别及处理

出库前依据液晶按键设定出库方向,识别库尖角和库顶,据此补线出库。入库方向与出库方向一致,识别斑马线和库尖角,据此补线入库。

6.2 控制方案选择

我们使用增量式PID算法控制电机稳定在稳定速度,PD控制差速转向。

在工程实际中,应用最为广泛的调节器控制规律为比例、积分、微分控制,简称PID控制,又称PID调节。PID控制器问世至今已有近70年历史,它以其结构简单、稳定性好、工作可靠、调整方便而成为工业控制的主要技术之一。当被控对象的结构和参数不能完全掌握,或得不到精确的数学模型时,控制理论的其它技术难以采用时,系统控制器的结构和参数必须依靠经验和现场调试来确定,这时应用PID控制技术最为方便。即当我们不完全了解一个系统和被控对象,或不能通过有效的测量手段来获得系统参数时,最适合用PID控制技术。PID控制,实际中也有PI和PD控制。

PID控制器是一种线性控制器,它根据给定值与实际输出值构成控制偏差。将偏差的比例§、积分(I)和微分(D)通过线性组合构成控制量,对被控对象进行控制,故称PID控制器,原理框图如图5.10所示。

▲ 图6.1.5 PID控制器原理框图

▲ 图6.1.5 PID控制器原理框图

在计算机控制系统中,使用的是数字PID控制器,控制规律为:
e ( k ) = r ( k ) − c ( k ) e\left( k \right) = r\left( k \right) - c\left( k \right) e(k)=r(k)c(k)
u ( k ) = K p { e ( k ) + T T j ∑ j = 0 k e ( j ) + T D T [ e ( k ) − e ( k − 1 ) ] } u\left( k \right) = K_p \left\{ {e\left( k \right) + {T \over {T_j }}\sum\limits_{j = 0}^k {e\left( j \right)} + {{T_D } \over T}\left[ {e\left( k \right) - e\left( {k - 1} \right)} \right]} \right\} u(k)=Kp{e(k)+TjTj=0ke(j)+TTD[e(k)e(k1)]}

公式中:
k:采样序号,k=0,1,2,3
c(k):第k次实际输出值;
e(k):第k次偏差;
e(k-1):第k-1次偏差
K p K_p Kp:比例系数
T I T_I TI:积分时间常数
T D T_D TD:微分时间常数
T:采样周期
r(k):第k次给定值
u(k):第k次输出控制量

简单说来,PID控制器各校正环节的作用如下:

  • 比例环节:及时成比例地反映控制系统的偏差信号,偏差一旦产生,控制器立即产生控制作用,以减少偏差。
  • 积分环节:主要用于消除静差,提高系统的无差度。积分作用的强弱取决于积分时间常数,越大,积分作用越弱,反之则越强。
  • 微分环节:能反映偏差信号的变化趋势(变化速率),并能在该偏差信号变得太大之前,在系统中引入一个有效的早期修正信号,从而加快系统的动作速度,减小调节时间。

数字PID控制算法通常分为位置式PID控制算法和增量式PID控制算法。

6.2.1位置式PID

位置式PID中,由于计算机输出的u (k) 直接去控制执行机构(如阀门),u(k)的值和执行机构的位置(如阀门开度)是一一对应的,所以通常称公式2为位置式PID控制算法。

位置式PID控制算法的缺点是:由于全量输出,所以每次输出均与过去的状态有关,计算时要对过去e(k)进行累加,计算机工作量大;而且因为计算机输出的u(k)对应的是执行机构的实际位置,如计算机出现故障,u(k)的大幅度变化,会引起执行机构位置的大幅度变化,这种情况往往是生产实践中不允许的,在某些场合,还可能造成严重的生产事故。因而产生了增量式PID 控制的控制算法,所谓增量式PID 是指数字控制器的输出只是控制量的增量△u(k)。

6.2.2增量式PID

当执行机构需要的是控制量的增量(例如:驱动步进电机)时,可由式2推导出提供增量的PID控制算式。由式2可以推出式3,式2减去式3可得式4。


公式4称为增量式PID控制算法,可以看出由于一般计算机控制系统采用恒定的采样周期T,一旦确定了KP、TI 、TD,只要使用前后三次测量值的偏差,即可由式4求出控制增量。

增量式PID具有以下优点:

  1. 由于计算机输出增量,所以误动作时影响小,必要时可用逻辑判断的方法关掉。

  2. 手动/自动切换时冲击小,便于实现无扰动切换。此外,当计算机发生故障时,由于输出通道或执行装置具有信号的锁存作用,故能保持原值。

  3. 算式中不需要累加。控制增量△u(k)的确定仅与最近k次的采样值有关,所以较容易通过加权处理而获得比较好的控制效果。

6.2.3差速转向的 PID 控制算法

对于转向的闭环控制,我们采用了位置式 PID 控制算法,根据往届的技术资料和实际测试,经过反复测试,我们选择的 PID 调节策略是:

  1. 将积分项系数置零,我们发现相比稳定性和精确性,舵机在这种随动系统中对动态响应性能的要求更高。更重要的是,在 KI 置零的情况下,我们通过合理调节 Kp,发现车能够在直线高速行驶时仍能保持车身非常稳定,没有振荡,基本没有必要使用 KI 参数;

  2. 微分项系数 KD 使用定值,原因是舵机在一般赛道中都需要较好的动态响应能力;

  3. 对Kp,我们使用了在程序中具体代码如下:

K p = ( e − ∣ e r r o r ∣ − 1 e − ∣ e r r o r ∣ + 1 ⋅ 0.5 + 0.5 ) ⋅ b a s k p K_p = \left( {{{e^{ - \left| {error} \right|} - 1} \over {e^{ - \left| {error} \right|} + 1}} \cdot 0.5 + 0.5} \right) \cdot bas_{kp} Kp=(eerror+1eerror10.5+0.5)baskp

其中,error是中心位置与中心值的偏差,bas_kp为基准Kp

经不断调试,最终我们选择了一组PD参数,得到了较为理想的转向控制效果。

▲ 图6.1.6 终点偏差和动态Kp值的函数曲线

▲ 图6.1.6 终点偏差和动态Kp值的函数曲线

6.2.4驱动电机的控制算法

在对两种PID算法进行筛选后,我们最终选择了增量式PID作为电机控制方案。经过参数调节,实际电机响应如图所示。

▲ 图6.1.7 电机响应波形图

▲ 图6.1.7 电机响应波形图

▲ 图6.1.8 电机响应波形图

▲ 图6.1.8 电机响应波形图

 

§07 RT-Thread应用


本章讨论RT-Thread操作系统功能在智能车中的应用 。

所有的线程、IPC、设备创建的时候,都会通过链表被挂载在对象容器中 。rrtthread内核对象类型有很多种,主要有线程类型、信号量类型、互斥量类型、时间类型、邮箱类型、消息队列类型、定时器类型等等。

下图给出本次搭载rtthread操作系统的智能车内核对象

▲ 图7.1 内核对象示意图

▲ 图7.1 内核对象示意图

由图可知,本次使用了七个线程,六个信号量,事件集里的两个关联型事件,一个邮箱和两个定时器,消息队列和互斥量没有用到。关于线程、信号量、事件集、邮箱、定时器的使用将在下文进行介绍

7.1 自动初始化

在原本的智能车裸机开发中,初始化一些硬件设备和软件部分如图7.1.1所示,这样的显式调用初始化函数,有时可能多达十几到几十个,看起来非常非常繁杂。

▲ 图7.1.1 初始化代码

▲ 图7.1.1 初始化代码

▲ 图7.1.2 自动初始化代码

▲ 图7.1.2 自动初始化代码

使用rtthread操作系统的自动初始化如图7.1.2所示。将初始化分为硬件设备初始化和软件线程及定时器初始化。这样,使用一个宏,初始化函数就会被自动初始化,不用在其他地方显式调用初始化函数 。对比裸机开发,代码瞬间清晰很多。

下面对自动初始化的原理进行说明

查看源码,INIT_APP_EXPORT的宏定义是:

#define INIT_APP_EXPORT(fn)      INIT_EXPORT(fn, "6") 

INIT_EXPORT(fn, level) 表示这个函数 fn 现在属于哪个初始化 level 段, 由 SECTION(".rti_fn."level) 进行定义
而 SECTION(x) 的宏定义是:

#define SECTION(x)               __attribute__((section(x))) 

attribute((section(“name”)))作用 :将作用的函数或数据放入指定名为"name"的输入段中。

以上就是整个的宏定义。作用就是将函数 fn 的地址赋给一个 __rt_init_fn 的指针,然后放入相应 level 的数据段中。所以函数使用自动初始化宏导出后,这些数据段中就会存储指向函数的指针。

7.2 线程分配及其应用

7.2.1 线程

线程时实现任务的载体,它是Rt-thread操作系统中最基本的调度单位,它描述了一个任务执行的运行环境,也描述了这个任务所处的优先等级线程由三部分组成:线程代码(函数)、线程控制块、线程堆栈。

线程控制块由结构体 struct rt_thread 表示,它是操作系统用于管理线程的一个数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等。

线程控制块中的 entry 是线程的入口函数,它是线程实现预期功能的函数。

线程的入口函数由用户设计实现,一般有无限循环模式和有限次循环模式,本智能车系统只使用到了无限循环模式。

7.2.2线程的创建及相应线程的作用

  • main线程:摄像头数据采集线程,采集成功后发送事件1给sweep线程

  • sweep线程:图像处理线程,包含赛道左右边界的获取与特殊元素的判断及处理

  • spped线程:速度决策线程, 获得期望速度,作为速度环的输入

  • display线程:人机交互调参线程,用于调参和显示当前赛道图像

  • buzzer线程:蜂鸣器线程,内置邮箱,用于赛车遇到不同特殊元素时发出不同时长的声响,方便人工判断。

  • tshell线程:命令行外壳线程,它是系统自带的线程,属于RT-Thread Finsh组件,提供了一套供用户在命令行的操作接口,主要用于调试、查看系统信息。在大部分嵌入式系统中,一般开发调试都使用硬件调试器和 printf 日志打印,在有些情况下,这两种方式并不是那么好用。比如对于 RT-Thread 这个多线程系统,我们想知道某个时刻系统中的线程运行状态、手动控制系统状态。如果有一个 shell,就可以输入命令,直接相应的函数执行获得需要的信息,或者控制程序的行为。这无疑会十分方便。

  • tidle线程:空闲线程,是系统自带的线程,它是系统线程中一个比较特殊的线程,具有最低的优先级,当系统中无其他线程可运行时,调度器将调度到空闲线程。空闲线程通常是一个死循环,永远不被挂起。

▲ 图7.2.1 软件设计流程图

▲ 图7.2.1 软件设计流程图

7.2.3线程栈大小及优先级的分配

想要观察各个线程的栈的使用情况,使用putty是一个不错的选择,但是在我们初步分配好各个线程的优先级和栈的大小后,使用putty登录操作系统时发现始终无法输入命令,在上网查找相关资料后发觉可能是tshell线程优先级太低的原因导致,经过调整后将tshell的优先级调到最高,如图所示

调整完成后,成功进入msh命令界面。由于CH32V103芯片的RAM资源只有20K,因此合理分配各个线程的栈的大小是非常有必要的,经过长时间的调试我们找到了适合各个线程的栈的大小,最终结果如图7.2.2

由图中可知,除了系统自带的线程tshell和tidle,我将自己创建的线程栈的最大使用率都控制在了70%到85%附近,最大化地利用了芯片的资源。值得一提的是,由于sweep线程是图像处理线程,需要保存的数据非常多,因此肯定需要分配特别多的栈,这一点我们开始分配栈时并没有注意,只是盲目地控制它的最大使用率,结果导致程序莫名的崩溃。仔细思考后,发觉使用putty看到的仅仅是智能车没跑起来时线程栈的使用情况,当智能车跑起来后sweep线程才会真正的运行,因此需要给sweep线程预留足够的栈空间,才能确保线程真正运行起来时不会崩溃,经过调整后我们将车没跑起来前sweep线程栈的最大使用率控制在11%附近,成功地让智能车完成了比赛任务。

关于线程优先级高低的分配将在7.6节事件集中进行说明。

7.3信号量的应用

临界资源或临界区是指在同一时刻只允许一个进程或线程访问,并且只有当占有该资源的进程释放了该资源后,才能被其他进程使用。因此需要设计一种机制保障进程间的通信,使得不同的进程能够知道临界资源的使用情况,当某个进程占据了临界资源时,应该告知其他进程该资源已经被占用,避免其他进程错误的访问和使用临界资源。信号量即是一种进程间通信的方法,它可用于线程间同步,它使用一个整形变量来累计唤醒次数,供以后使用,当信号量只有两种状态时,表示该资源只能被唤醒一次或者占用一次,此时对临界资源的访问就是互斥的。

智能车应用中,使用信号量时用到了三个API函数,即信号量的动态创建、获取与释放。

7.3.1信号量相关函数的介绍

动态创建信号量的函数如下

其中,函数第一个参数是创建的信号量的名称;第二个参数是初始化可用示例的数目;第三个参数flag用于决定当信号量不可用时多个线程等待信号量的排列方式,有RT_IPC_FLAG_FIFO和RT_IPC_FLAG_PRIO两种,若使用前者则等待的线程将采用先进先出的方式排队,先进入等待列表的线程将优先获得信号量;若使用后者则高优先级的等待线程将优先获得信号量。函数返回的是信号量结构体对象指针。

信号量获取函数如下:

其中,第一个参数是信号量对象的指针,第二参数是时间参数。当线程使用该函数获取信号量时,若信号量的值value大于0,线程将获得信号量,获得信号量后函数立即返回,并且将信号量的值减一,当value为0时,信号量所指向的共享资源不可用。申请该信号量的线程,将根据time参数进行相应的动作,time为0时时立即返回,time大于0时按照time值进行时间等待,time的时间是以系统的滴答时钟为基础单位的。当time为负数时(RT_WAITING_FOREVER宏定义是-1),线程将永远在信号量上进行等待(挂起),直到信号量在其他线程或中断里被释放。 需要注意的是,信号量获取函数只能在线程中被调用,不能在中断中被调用,因为中断一般是有周期性运行的需求的,若在中断中使用take则可能会导致中断挂起而使中断里的程序无法执行。

信号量释放函数如下:

量的值value将+1,即可用资源的数目+1.

7.3.2基于信号量的摄像头数组资源访问互斥

智能车应用开发中,摄像头采集到的图像数组就属于临界资源,它是多线程存在时必须互斥访问的资源,若处理摄像头数据的算法占用非常多的时间,而摄像头DMA采集间隔很短,会造成DMA和CPU同时去竞争访问摄像头数组,以至于进了硬件中断hard_fault,造成单片机死机。此时需要创建一个信号量,从而保证DMA和CPU不会同时竞争访问摄像头数组。

首先在main线程里创建信号量,然后进入循环,执行take函数,等待摄像头资源的释放,等待过程中main线程会被挂起,系统将调度其他线程。当获取信号量成功后,向事件集发送事件1,触发图像处理线程,此过程请参照事件集部分 号量的释放函数位于摄像头DMA采集图像成功时使用,如图所示

7.3.3基于信号量的人机交互页面设计

智能车调试时,经常需要对一些参数进行加减修改,因此为了便于调试,本文设计了一个基于RT-Thread信号量的人机交互系统。

1)创建按键信号量

2)创建一个按键定时器,让其每20ms读取以下按键状态,保证其实时性

3)编写一个回调函数,使其能够实现对按键状态的读取;


4)编写一个按键反馈函数,当按键按下时释放信号量并发送邮箱数据给蜂鸣器,以达到按下按键时有蜂鸣器反馈的目的;

5)编写按键操作函数,当按键按下时,读取按键信号量并对目标参数进行参数值加减操作,最后将其写入进flash中保存参数

6)人机交互页面设计
首先我们编写了一个显示屏显示线程,用于显示参数及图像

图7.3.12
在交互页面中,我们设计了发车,显示摄像头参数以及参数调节页,实际效果如图所示

图7.3.13第一页菜单

图7.3.14debug页调参

图7.3.15图像显示
7.4邮箱的应用
RT-Thread操作系统的邮箱用于线程间通信,特点是开销较低,效率较高。
7.4.1邮箱相关函数的介绍
初始化与脱离

创建与删除

发送邮件

接受邮件

7.4.2基于邮箱的蜂鸣器反馈系统
基于邮箱的操作我们将邮箱用于蜂鸣器反馈系统,使其能够及时反馈赛道信息和按键状态。
1) 首先创建邮箱命名为buzzer_mailbox,

2) 创建蜂鸣器线程并编写蜂鸣器入口函数

3) 我们希望当车驶入特定赛道元素时,或者按键按下时,有蜂鸣器反馈声音,

使用putty查看当前邮箱使用情况,可以看到buzzer邮箱

图7.4.1邮箱使用情况
7.5软件定时器应用
软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器的基础上(系统滴答定时器)。软件定时器使系统能够不受树木限制的定时器服务。
RT-Thread操作系统提供的软件定时器,以系统节拍(OS TICK)的时间长度为定时单位,提供了基于系统节拍整数倍的定时能力,即定时数值是OS TICK
的整数倍。
当软件定时器所设定的定时时间到了后,会调用用户设置的定时器timeout回调函数,用户需要定时运行的程序会在回调函数中得到处理
7.5.1软件定时器相关函数介绍
初始化与脱离

创建与删除

启动定时器

停止定时器

7.5.2基于定时器的速度环控制及按键调参设计
由于速度控制需要周期性实时控制,按键调参时也需要实时性检测按键状态,于是本文用定时器功能来实现速度环控制和按键调参。
7.5.2.1速度环定时器
1)创建速度环定时器 时间为1ms

2)编写速度环回调函数,实现期望速度的分配(详细代码参考timer_pit.c)

7.5.2.2按键检测定时器
1)创建按键检测定时器,时间为20ms

2)编写按键检测回调函数,让其每20ms检测一次按键状态

7.6 事件集
事件集是线程间同步的机制之一,一个事件集可以包含多个事件,利用事件集可以完成一对多,多对多的线程间同步。
RT-Thread中的事件集用一个32位无符号整型变量来表示,变量中的一个位代表一个事件,线程通过“逻辑与”或“逻辑或”与一个或多个事件建立关联形成一个事件组合。
事件的“逻辑或”也称为是独立型同步,指的是线程与任何事件之一发 生
同步,只要有一个事件发生,即满足条件;
事件的“逻辑与”也称为是关联型同步,指的是线程与若干事件都发生同步,只有这些事件全部发生,才满足条件。

图7.6.1事件集
7.6.1 事件集相关函数介绍
智能车应用中,使用事件集时用到了三个API函数,即事件集的动态创建函数、发送事件函数与接受事件函数。
动态创建事件集的函数如下

其中name是事件集的名称,flag决定了事件未触发时线程的等待方式,有RT_IPC_FLAG_FIFO和RT_IPC_FLAG_PRIO两种,若使用前者则等待的线程将采用先进先出的方式排队,先进入等待列表的线程将优先获得事件;若使用后者则高优先级的等待线程将优先获得事件。该函数的返回值是事件对象的句柄
发送事件函数如下

其中第一个参数表示我们向那一个事件集发送事件,第二个参数是我们发送的事件
接受事件函数如下

其中第一个参数是事件集对象的指针,即我们要接受那一个事件集的事件;第二个参数是表示我们对那个事件集哪一个位感兴趣,是某些bit的组合;第三个参数是事件的信息标记;有RT_EVENT_FLAG_AND(表示set参数的某几个事件都发生这个函数才能被唤醒),RT_EVENT_FLAG_OR(表示set参数有其中一个事件发生这个函数才能被唤醒),RT_EVENT_FLAG_CLEAR(表示函数唤醒后会清除set参数中的位,即把事件清零);第四个参数是时间参数,若事件未发生,则线程会挂起相应的时间(t>=0时),若事件参数为负数(RT_WAITING_FOREVER宏定义是-1)则线程将一直挂起直到事件被触发。
7.6.2事件集的应用
在我们创建的智能车系统中,使用到了两个事件:事件标志1(#define EVENT_FLAG1 (1<<1))和事件标志2(#define EVENT_FLAG2 (1<<2))。首先在自动初始化中创建事件集如下

经过大量的调试,我们发现用来接收事件的线程的优先级最好要比用来发送事件的线程优先级要高,因为这样可以保证事件触发与接收的顺序按我们的预想执行。
由于速度决策是在图像处理之后执行,而图像处理是在摄像头接收到图像后执行,所以我们需要先让speed线程(速度决策线程)挂起,以等待事件2的触发,再让sweep(图像处理线程)挂起,以等待事件1触发,再进入main线程 ,当main线程的take函数获取到摄像头信号量时,立即发送事件1,随后进行线程调度,由于sweep线程之前已经在等待事件1,所以会开始执行图像处理并清除事件1的标志,图像处理完成后发送事件2,线程调度到speed线程执行速度决策程序并清除事件2标志,然后才会调度到显示线程和蜂鸣器线程而不影响程序的主体。
由此可以形成一个循环,让搭载Rt-thread操作系统的智能车稳定地完赛。因此线程优先级的分配如图7.6.2

图7.6.2 线程优先级配置

 

§08 RT-Thread性能


本章给出了搭载RT-Thread操作系统与裸机运行的对比 。

8.1裸机运行局限性

1)并发性:程序并发工作效率低
在写裸机软件时,不可避免的在主程序中会有一个超级大的while(1)循环,这里面包含了整个智能车寻迹运行的所有业务逻辑。因为每个业务逻辑里面都有delay这样的循环等待函数,这样导致了所有的业务逻辑几乎是串行起来工作的。这个时候CPU就会有很多时间都浪费在了延时函数里,一直在空转,导致软件的并发效率非常差
2)实时性:功能复杂的情况下,实时性无法保证
在编写裸机软件中,如果代码变得庞大以后,可以想象到,主函数中那么大的一个while(1)循环,代码耦合严重,到处是delay延时,要保证实时性几乎是不可能的。

8.2 RT-Thread操作系统带来的优势

1)模块化
移植了RT-Thread操作系统以后,整个软件的工作被拆分成了由多个线程任务来构成,每个线程都有自己独立的运行空间,几线程堆栈,这个时候每个线程独立自主运行,互不干扰,模块化程度得到很好的提高。
2)并发性
从并发的角度来看,各个线程在使用delay、事件等待这类函数式,会自动的让出CPU给其他有需要的线程,不仅书写delay延时函数操的心少了,整个CPU的利用率也得饿到了提高,最终提升并发性。
3)实时性
RT-Thread操作系统中,各个线程可以设置不同的优先级,重要的线程可以设为高优先级,不重要的线程可以减低优先级,做好全局的统筹规划后,整个软件的实时性也能得到保证。
4)有效保护临界资源
智能车应用开发中,摄像头采集到的图像数组就属于临界资源,它是多线程存在时必须互斥访问的资源,若处理摄像头数据的算法占用非常多的时间,而摄像头DMA采集间隔很短,会造成DMA和CPU同时去竞争访问摄像头数组,以至于进了硬件中断hard_fault,造成单片机死机。若使用RT-Thread操作系统中的信号量,就可以保证DMA和CPU不会同时竞争访问摄像头数组而造成系统实际。 5)程序结构的清晰化
裸机开发中往往程序都是堆在一起顺序执行的,而使用了RT-Thread操作系
统能有效地划分出一个个程序结构,让不同的功能的程序在不同的线程里运行,提升了代码执行的效率,加快了小车的速度

8.3 上位机测试裸机与搭载RT-Thread操作系统运行情况

8.3.1 对比图像处理和速度决策所需时间
裸机下的代码如下,测试原理:获取到摄像头图像后开始计时,运行完一次图像处理和速度决策后停止计时,并发送一次经历的时间time给上位机

得到的结果如下(纵轴的单位是us),可知裸机下运行一次图像处理和速度决策的时间在10000us即10ms左右

图8.3.2图像处理时间
搭载Rt-thread操作系统下的发送获取一幅图像的时间time的代码如下,原理是:在获取到摄像头信号量之前开始计时,图像处理线程中的程序和速度决策中的程序运行完成后停止计时,并发送一次数据给上位机

图8.3.3上位机代码
结果如下图所示(纵轴的单位是us),可知Rt-thread操作系统下运行完一次图像处理和速度决策的时间在7000us即7ms左右

图8.3.4图像处理时间
综上所述,使用Rt-thread操作系统的多线程时,经历一次图像处理和速度决策的时间是裸机下的70%(7/10),提高了程序运行的效率,缩短了更新一次目标速度的时间间隔,可以让小车更快地根据赛道情况改变自身的速度以至于提升了小车的速度上限。
8.3.2对比获取一幅图像的时间
裸机中的代码如图所示

图8.3.5上位机测试代码
变量time代表的是获取一幅图像的时间,每获取一幅图像后发送一次数据给上位机,得到的结果如图所示(纵轴的单位是us),可知裸机下获取一幅图像的时间在1400us即1.4MS左右。

图8.3.6 上位机测试结果
搭载Rt-thread操作系统下的发送获取一幅图像的时间time的代码如下,原理是获取到摄像头信号量后立即发送一次数据

图8.3.7上位机测试代码
结果如图所示(纵轴的单位是us),可知Rt-thread操作系统下获取一幅摄像头图像的时间在1000us即1ms左右。

图8.3.8上位机测试结果
综上所述,使用信号量后获取一幅图像的时间是裸机下的71.4%(1000/1400 ),节省下的时间可以更早地进行图像处理和速度决策(因为等待图像获取成功之前单片机可能处于死循环的空闲状态,浪费了时间),使全向小车的转向更加灵敏,侧面提升了小车的速度。

 

§09 结与展望


9.1 工作总结

全向行进小车是一种多变量、非线性、强耦合的系统,是实时性要求非常高的典型装置。本系统主要是以risc-v为核心的CH32V103的全向小车为平台,深入研究了 RT-Thread操作系统的软件架构和工作机制,并对两者进行了结合,本系统主要工作总结如下:

  1. 对全向行进小车的硬件系统进行了详细的设计,采用CH32V103作为主控制器.MT9V032模块采集赛道图像,角度式编码器获取小车的速度,使用 IPS液晶显示屏 、拨码开关和按键作为辅助调试手段。

  2. 阅读RT-Thread操作系统的实用手册和相关技术文档.了解了 RT-Thread操作系统的基本架构硬件支持方式,对RT-Thread 操作系统在risc-v的移植做了相关的说明.阐述了线程间的通信方式和线程的调度,给出了整个系统的软件流程图。

  3. 通过收集相关资料和长时间的开发与调试,分析了完成赛题任务需要的图像处理算法以及电机pid响应算法。

  4. 创新性地将Rt-thread操作系统中的线程、信号量、事件集、邮箱、软件定时器应用在了全向行进小车系统上,验证了RT-Thread软件应用符合科学原理与工程技术要求。

  5. 测试比较了使用Rt-thread的多线程和裸机下程序的运行周期和获取一幅图像的时间,验证了Rt-thread在提升程序运行效率的科学性与可行性。

  6. 成功通过Rt-thread操作系统完成了在CH32V103芯片上的软件应用开发,在实现全向行进小车出入库、前进、转弯、进出环岛、横向通过三叉路段的比赛任务要求的同时保证了小车的快速性及鲁棒性的需求。

9.2工作展望

  1. 本文设计的图像处理算法还有很大的提升空间,比如在赛道元素判断的100%准确和更加精准的速度决策方面还可以进行改进。

  2. 本文设计的控制算法方面还待加强.普通的增量式PID算法对电机响应速度的提升是有限的.如果能加入模糊自动调整的PID算法,可以让电机的响应更加快速,从而提升小车的速度和稳定性。

  3. 本设计中采用的是RT-Thread比较直观和简单的系统设计.并没有充分、完美地发挥其在嵌入式领域的优势,作为一款能够应用于工业控制中的实时操作系统,如果能应用在医疗器械、汽车电子等对实时性要求更高、更复杂的领域,就更能发挥出 RT-Thread操作系统的优势,更能展示出RT-Thread操作系统相较于裸机运行的优越性。

■ 参考文献

[1]王艳颖,王珍,郭丽环.直流电动机传递函数测定的实验研究[J].实验技术与管理,2008(08):38-40.
[2] 童诗白,华成英.模拟电子技术基础[M].北京. 高等教育出版社.2000
[3] 李玲,桂玮珍,刘莲.C 语言程序设计教程.北京.人民邮电出版社.2010
[4] 王宜怀,曹金华.嵌入式系统设计实战.北京.北京航空航天大学出版社.2011
[5] 王盼宝.智能车制作.北京.清华大学出版社.2017
[6]王兆滨,韩鹏程.MSP432的RT-Thread操作系统移植[J].单片机与嵌入式系统应用,2021,21(05):39-42.
[7]朱志国.RT-Thread操作系统在STM32中移植的研究[J].计算机光盘软件与应用,2012,15(22):119-120.

附录A:核心算法子程序源代码

主函数源代码 
#include "headfile.h" 
#include "display.h" 
#include "timer_pit.h" 
#include "encoder.h" 
#include "buzzer.h" 
#include "button.h" 
#include "motor.h" 
#include "RG_Type_Define.h" 
#include  "RG_Data_Define.h" 
#include  "camera.h" 
#include  "control.h" 
#include <rtconfig.h> 
#include <rtdef.h> 
#include  "foundation_sweeping.h" 
#include  "get_speed.h" 
#define EVENT_FLAG1 (1<<1) 
#define EVENT_FLAG2 (1<<2) 
 
//rt_event_t event;//事件控制块  已在foundation_sweeping.c中定义 
rt_sem_t camera_sem;//摄像头信号量 
int hardware_init(void)//硬件设备初始化 
{ 
    pwm_init(PWM2_CH1_A0, 100,2550);// 舵机pwm初始化,一开始让舵机转向正前方 
    ips114_init();//屏幕初始化 
    mt9v03x_init();//摄像头初始化 
    icm20602_init_spi();//陀螺仪初始化 
    encoder_init();//编码器初始化 
    motor_init();//电机PWM初始化 
    return 0; 
} 
int software_init(void)//软件线程或定时器初始化 
{ 
    check_value(); 
    event = rt_event_create("event", RT_IPC_FLAG_FIFO);//事件集的创建 
    buzzer_init();//蜂鸣器线程初始化 在buzzer.c文件中 
    display_init();//显示线程初始化   在display.c文件中 
    button_init();//按键检测定时器初始化 在button.c文件中 
    timer_pit_init();//速度环定时器初始化化  在timer_pit.c文件中 
    sweep_init();//赛道左右边界获取、特殊元素处理及中线获取线程    当识别到特殊元素时,会发送邮件(内容是发声时长)到蜂鸣器邮箱中,  让蜂鸣器响不同的时间,以区分不同的元素 
    get_speed_init()  ; //速度决策线程(获得速度环的输入) 
    return 0; 
} 
INIT_APP_EXPORT(software_init); 
INIT_APP_EXPORT(hardware_init); 
int main(void) 
{ 
    camera_sem = rt_sem_create("camera", 0, RT_IPC_FLAG_FIFO); 
    while(1) 
    { 
        rt_sem_take(camera_sem, RT_WAITING_FOREVER);  //等待摄像头采集完毕 
        rt_event_send(event, EVENT_FLAG1);//若摄像头采集一幅图像完毕,则触发事件1,左右边界获取及特殊元素处理线程开始运行,函数在foundation_sweeping.c中 
        rt_thread_mdelay(5);           //线程调度 
    } 
}