Tinymal-B控制教程介绍2-基本框架介绍

TInymal-B默认采用了多任务与多线程的控制框架,基于舵狗与飞控的开发基础搭建!本文介绍了基本的软件架构,以及Linux下相关驱动的简单配置说明

在单片机中我们可以使用计时器中断、软件调度或者直接使用嵌入式操作系统如FreeRTOS或UCOSII的方式来实现不同控制任务的定时运行,由于单片机是单核系统因此就算构建了多线程也仅仅是在某个线程sleep时主动将资源释放而言,而在树莓派中其有多个CPU能实现多核心运行,而且当很多高速运动控制任务与单一CPU内核绑定时其运行的速度和实时性都远远优于单片机的操作系统调度,如下图MOCO-ML机器人控制系统中主要的几个任务其均运行在不同的频率因此我们可以采用Linux多线程或多核多任务的方式来实现,从而提高力控带宽到1Khz满足绝大部分机器人的运动控制频率需求:
如上图所示,越往左的模块其对计算的实时性要求越高,因此我们可以把很多同类调度的软件模块综合在一个大任务中保证其采用单独的CPU运行,但这种方法相比多线程来说任务间的数据交互是一个大问题,在多线程框架中最直接的方式我们可以定义全局变量来完成数据的函数数据的交互,而在多任务框架下每个任务都在不同的内核上运行,数据交互的实时性和数据量都有很严格的要求才能保证机器人的运行。以MIT的四足机器人软件为例其采用LCM来实现进程间的数据通信,LCM(Lightweight Communications and Marshalling,轻量级通信与数据封送库)是一组类库含多种语言如java,c等专门针对实时系统在高带宽和低的延迟的情况下进行消息发送和数据封送处理。它提供了一个发布/订阅消息模型、自动封装/解封代码生成工具含多种编程语言版本。其最初由MIT城市挑战赛小组为DARPA消息传递系统设计。LCM是专为通过局域网连接的tightly-coupled类型系统而设计。 它不适合因特网。LCM研制开发软实时系统它默认允许丢包以减少延时。http://lcm-proj.github.io/
LCM也早已经被广泛应用于自动驾驶中,可以说国外的软件框架发展还是更加完善,很多必要的软件模块均已有成熟的库和相应的包可以依赖,这也是MIT这样大学技术实力最核心的体现。
因此考虑机器人的软硬件控制机制,我们可以把单片机中的控制框架重新修改一下以多进程和多线程的结构来重新组合。首先我们可以将机器人主要功能划分为3个层次,第一个是底层伺服驱动:该部分主要是树莓派与单片机的SPI通讯,由于要保证高速数据传输,因此需要单独分配一个进程,同时增加相应保护;第二层是运动控制层:该部分主要完成对机器人运动状态的估计和反馈控制;第三层是导航规划层:该部分主要完成机器人的路径规划和遥控命令处理,另外很多全局保护也可以放在本层中,则在树莓派中机器人的软件框架如下:
基于LCM能实现数据的轻量化数据的实时交互,但是需要而外安装相应的库来支撑,在Linux系统中另外可以通过共享内存的方式来实现数据交互,共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。
因此我参考网上很多共享内存的例子来实现一个自己的进程间通信接口,首先我们构建伺服层servo_task的共享内存接口,本层中主要首先使用SPI接口读取单片机回传的编码器数据和IMU等运动传感器数据,进一步我们开辟一片简单的内存将其按浮动数的协议差分为4个字节依次存入其中,首先建立写入内存初始化:
结构体重申请了MEM_SIZE个Char型的变量内存,之后在初始化中添加下面的代码其中MEM_SPI为内存ID,读取和写入需要一致,其flag能确定谁在读写该内存区域,下一步在主函数中初始化共享内容主要是确定其ID,并构建对应的结构体:
由于Servo线程主写,因此在主循环中当内容读写标志位为0时,其按协议给结构体数组赋值,这里我将内存划分为两半,前一半为Servo使用后一半为Control使用,因此写完后读取后一半内存数组并按格式解码,最终将标志位赋值为1:
在Control任务中由于其主读取,因此内存读写顺序和Servo相反先读取后写入,注意各自任务仅操作自己对应的内存区域:
这样当两个任务都跑起来时就可以通过共享内存的方式来实现数据互传,当然我实现的方式比较粗糙类似传统应答的模式,但能力有限先保证两个部分功能能够实现,当在两个不同终端中启动servo和control程序则可以看到数据互通成功,通讯频率能保证与SPI接收速率批量,下图中servo节点传输了SPI读取STM32计算的姿态角到control节点,并将其打印:
备注:综上该部分给出了多任务间共享内存的例子,该方法仅以本人个人理解实现共享内存数据互传,当然很明显其存在很多问题比如:采用应答机制因此在SPI收发完成后才能实现一次数据同步,因此内存数据刷新受SPI通讯频率影响;内存同步无法实现浮点数的传输,需要自己重新组码和解码;内存同步仅支持1对1收发;

串口通信

串口通信是树莓派一个重要的功能,可以说在没有使用其他接口时串口是树莓派与外部大多数传感器和模块通信的主要方式,其串口通信的方式与单片机类似,但是Linux下有许多独特的处理,这里我们首先使用serial库来实现,由于其不是Linux自带库需要进行安装,在库目录build下进行cmake和make完成编译,最终用sudo make install的方式将其安装到系统中完成依赖,则树莓派串口读取可以使用两种输入,一是在其外扩排针处直接提供的串口引脚,其与单片机UART6进行连接,当然使用时需要先在树莓派配置中打开串口,另外树莓派的USB口也可以再介入类似虚拟串口的设备时进行串口操作,因此首先使用ls /dev查看具体连接的串口设备是USB还是物理引脚,并在代码中初始化:
Linux下串口的读取不是中断机制,任然是一包读取因此为了避免收发频率造成一包数据被截断的问题,这里还是采用中断状态机处理的方式:
并且在每次接受完毕后进行发送,其发送部分比较类似单片机,在数组赋值完毕后将其余接口绑定并指定发送长度即可:
最终,其界面校验与单片机中一致,我提供了对应的数据处理接口:
备注:树莓派串口操作比较简单,但是其硬件IO的串口波特率只能低于115200,而采用USB转串口则可以很高,在Moco舵狗固件中使用串口实现单片机与树莓派间图像和控制数据传输实现SDK控制,另外Demo中的接收例子是空循环公司UWB模块的接收方式;

 多线程

采用共享内存能实现在多个进程间的数据交互,因此不同进程即不同的独立任务,而在一个任务中我们还可以构建多进程来实现不同处理的周期控制,因此多进程可以理解为我有多个不同的单片机各自跑独立的任务并将其安排在独立的CPU内核中加快计算,如独立完成控制和状态估计,在在每个内核中类似单片机我还可以采用Ucos或FreeRTOS的形式构建多进程,让个部分代码的执行周期更加稳定,计算资源分配更加合理,因此这里使用Linux 自带的pthread库来实现该目的,首先需要在cmakelist文件中添加相关依赖:
之后再主函数中创建我们的线程:
则现在不同的线程中我们可以采用while循环的方式进行代码编写:
备注:多线程管理实际上是Linux代码编程中高级功能,其设计操作系统和数据保护等一系列问题,代码中仅给出了简单的例子;

UDP通讯

在STM32中我们使用USB虚拟串口完成与上位机的通讯,从而帮助我们更方便地监视机器人状态和调试参数,而在树莓派中我们已经采用Linux系统因此可以使用UDP网口来实现更高速更复杂的通讯,这里我们让树莓派作为服务器上位机作为客户端,简单来说UDP的通讯机制是客户端向服务器主动发送数据,客户端在收到数据后对其进行反馈,因此在使用UDP时我们需要先包上对应的头文件:
UDP通讯的关键是二者在一个IP端并且端口号一致,因此为方便每次启动的网络连接这里我们将树莓派IP设定为静态IP,编辑树莓派对应网络配置文件sudo nano/etc/dhcpcd.conf,在最后加上下面的代码,重启后用SSH查看是否能够连接:
# 指定接口 eth0 interface eth0 # 指定静态IP,/24表示子网掩码为 255.255.255.0 static ip_address=192.168.1.20/24 # 路由器/网关IP地址 static routers=192.168.1.1 # 手动自定义DNS服务器 static domain_name_servers=114.114.114.114
因此在树莓派中首先完成socket的配置,其SERV_PORT为最重要的端口号需要与客户端保持一致:
依据UDP通讯机制,首先等待客户端数据:
由于UDP是整包读取,这里我们还是采用中断状态机处理接收数据:
在处理完后进行数据发送,类似串口给数组赋值指定发送长度即可:
之后客户端主发送:
2023-02-11