【写在前前前面】开源Gitee堆栈链接:
https://gitee.com/hallo_frank/cyt4-bb7_-control_-middleware_-library.git
【写在前前面】特殊感谢:
- 逐飞科技CYT4BB7开源库,本项目所有工作均基于逐飞科技CYT4BB7开源库,特此感谢逐飞科技的开发人员,为智能车er们带来了云云全面完整的解决方案。
- 越野信标组参赛队友及华南理工大学智能车队,在完资本项目的途中,假如缺少你们的资助,硬件上的、软件上的、精力上的,本项目肯定没办法到达这样的完成度,特此感谢这些为本项目提供过各种资助的智能车er们。
- 华工机器人未来创新实验室SRML库历代的各位缔造者和维护者,本项目copy了SRML多个文件的源码,特此感谢你们很好的轮子,使我的拿来主义旋转。
【写在前面】特殊说明:
本项目中多数文件开头都带有与本人相干的开头,如下:
- /*
- * @Author: Jae Frank[thissfk@qq.com]
- * @Date: 2024-06
- * @LastEditors: Jae Frank[thissfk@qq.com]
- * @LastEditTime: 2024-06
- * @FilePath: xxx.c
- * @Description:
- * If you need more information,
- * please contact Jae Frank[thissfk@qq.com] to get an access.
- * Copyright (c) 2024 by Jae Frank, All Rights Reserved.
- */
复制代码 此文件开头标识为VSCode插件设置好的自动插入,所以文件开头带有此标识不代表此文件的工作全部出自本人,因为本项目中另有大部分工作来自「孟杨」同学,更不代表本人否认或者想要陵犯他人的工作成果,有些文件中本来的开头标识大概被本人误删,如果需要补充可以与本人联系更新,对此造成的误解与不便,敬请谅解!
本项目重要特点有三:
- 移植了FreeRTOS操作系统,使用任务调度机制比裸机跑程序更有上风。
- 通过定时器停止实现了类似STM32中空闲停止的机制,实现串口数据的不定长接收,接收一些模块的数据时,不需要轮询查找帧头帧尾,更节省cpu资源。
- 使用c/c++混编实现了面向对象编程,相比面向过程编程,更自然直观也更解耦,并且也能用上c++的语法糖,编程开发更便捷。
本项目的其他小特点:
- 移植u8g2实现非线性菜单,纵享丝滑Q弹。
- 各种外设均由自己的独立任务举行驱动和数据收罗,AAARobot类的实例化对象只通过对应外设的数据布局体来访问相应的信息,充分将底层外设和机器人控制逻辑层解耦。
- 在硬件停止处理函数中举行了软件重启,当由于奇怪的情况进入硬件停止后可以自动重启继续实现控制
- ……
以上小特点均懒得码字不赘述,详细实现可以参考源码,下面只对重要特点举行详细先容。
FreeRTOS
这个工程中,给M0移植的是FreeRTOS的v1版本的源码,给M7移植的是v2的源码,源码都来自于cubemx给M0和M7天生的FreeRTOS源码
因为从FreeRTOS官网下载的源码移植后无法正常工作,并且v2版本的源码给M0核没办法正常工作,所以以上的版本安排会这么抽象
- FreeRTOS的调度过程
对于FreeRTOS调度过程,只需要知道FreeRTOS的启动过程即可,其余部分只求使用的话,不需要太深入相识
从main_cm7_0.c中的main函数开始:
- osKernelInitialize();
- usrSystemInit(); // 初始化系统
- osKernelStart();
复制代码 以上三行,第一行是初始化FreeRTOS的内核,不需要深入理解也可以。
第二行是封装好的初始化系统的函数,这个函数里面先是对系统的GPIO、串口等外设举行初始化,然后对相干外设的FreeRTOS任务举行初始化。
第三行是开启FreeRTOS的内核调度,从这个函数开始,正式进入了FreeRTOS掌控任务调度的时期,FreeRTOS会根据每个任务的运行频率(由客户设置)和优先级(由客户设置),来判定每个时候的当下应该让哪个任务出于运行状态.
以上,便是FreeRTOS启动的过程.
- FreeRTOS任务的创建
任务的创建一样平常先分配好相干的变量,然后通过以下这句代码实现最终的创建。
- defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); // 4. 创建任务(给任务分配运行空间),并将任务句柄变量和任务函数\任务参数表链接起来
复制代码 创建一个StartDefaultTask的任务,defaultTask_attributes是这个任务的参数列表,里面包含了这个任务的调用空间巨细\优先级等信息,而defaultTaskHandle是这个任务的句柄(handle的中文翻译),句柄可以理解成是一个令牌,古人使用令牌可以来指挥特定的队伍,同样的,用户想要支配某个任务,就是操纵通过这个任务的句柄来实现.
上面提到过了任务\任务参数表\任务句柄等概念,那么此处展开说一下这些概念
任务(任务函数): 这个指的是, 某个任务在运行时要执行的函数, 可以理解成这个任务所要执行的详细行为
任务参数列表: 这个参数列表, 是一个struct布局体范例的数据, 包含了这个任务的名称字符串name, 任务运行的空间(栈)的巨细stack_size以及任务的优先级priority.
任务句柄: 当任务创建乐成之后, 用户再要对某个任务举行任何的操作, 都是通过任务句柄即可访问到相对应任务的一切信息.
下面用一份实例代码,说明详细的、完整的创建过程
- #include "cmsis_os.h"// 启动任务osThreadId_t defaultTaskHandle; // 1. 创建好任务句柄const osThreadAttr_t defaultTask_attributes = { // 2.创建任务参数列表 .name = "defaultTask", // 给这个任务的名字/描述信息 .stack_size = 128 * 4, // 给这个任务安排的栈空间,一样平常在iar中,假如任务运行上溢出这个栈,就会进入硬件停止死循环 .priority = (osPriority_t)osPriorityRealtime, // 给这个任务安排的优先级};void StartDefaultTask(void *argument); // 3. 创建任务函数的声明void main(){ osKernelInitialize(); defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); // 4. 创建任务(给任务分配运行空间),并将任务句柄变量和任务函数\任务参数表链接起来
- osKernelStart();}void StartDefaultTask(void *argument); // 5. 任务函数的界说, 任务中真正执行的事情{ // ...暂时省略不写 }
复制代码 不难看出, 从上到下, 重要分为5步
- 创建任务句柄变量, 但此时只是创建了这个变量, 还没将这个变量和现实的任务实体举行链接
- 创建任务参数列表变量, 里面会对三个变量举行设置, 这些也就是对任务的设置
- 创建任务函数的声明, 因为一样平常情况下, 程序员都约定速成在文档的最上面先将函数举行声明, 在文档的最下面再对函数举行界说
- 创建任务, 通过调用osThreadNew函数, 加载了任务函数和任务参数表创建了任务, 并将这个任务与任务句柄举行了链接
- 任务函数的界说, 在这个任务中, 到底要做些什么, 就是在任务函数里面举行界说
- 此工程中FreeRTOS任务的创建过程
上面提到,在封装好的初始化系统函数usrSystemInit中会初始化相干外设和相干外设的FreeRTOS任务。
- /**
- * @brief 用户系统初始化
- *
- */
- void usrSystemInit(void) {
- // 外设初始化
- usr_sys.peripheralInit();
- // 任务初始化
- usr_sys.TaskCreate();
- // 车子实例初始化
- car.init();
- }
复制代码 其中usr_sys是USR_SYSTEM类的一个实例化对象,通过调用类函数peripheralInit来举行外设初始化,调用类函数TaskCreate举行任务初始化。下面看一下类函数TaskCreate的内容。
- /**
- * @brief 用户任务创建
- *
- */
- void USR_SYSTEM::TaskCreate() {
- // 创建默认任务
- defaultTaskHandle = osThreadNew(defaultTask, NULL, &defaultTask_attributes);
- // 创建显示任务
- oledTaskHandle = osThreadNew(oledTask, NULL, &oledTask_attributes);
- #if USE_FS_I6X
- // 创建FS_I6X任务
- remoteCtrlUnitTaskHandle =
- osThreadNew(tskFS_I6X, NULL, &remoteCtrlUnitTask_attributes);
- #else
- // 创建ps2任务
- remoteCtrlUnitTaskHandle =
- osThreadNew(ps2Task, NULL, &remoteCtrlUnitTask_attributes);
- #endif
- // 创建电机驱动任务
- motorTaskHandle = osThreadNew(motorTask, NULL, &motorTask_attributes);
- // 创建舵机转向任务
- servoTaskHandle = osThreadNew(servoTask, NULL, &servoTask_attributes);
- // 创建upperMonitor任务
- // upperMonitorTaskHandle =
- // osThreadNew(upperMonitorTask, NULL, &upperMonitorTask_attributes);
- // 创建vofa任务
- #if !ADC_DATA_VIEW
- vofaTaskHandle = osThreadNew(vofaTask, NULL, &vofaTask_attributes);
- #endif
- }
复制代码 由FreeRTOS任务的创建所提到的内容, 不难看出, 这函数一次性将多少个任务的创建任务这一步给完成了。
在这个项目中,整体的调用过程如下:
main函数->usrSystemInit函数->通过usr_sys类对象调用 TaskCreate类函数->完成所有外设任务的创建任务步调。
串口空闲处理实现不定长接收
不定长接收对于串口信息接收有很大的资助,借助不定长接收可以将接收到的信息分成不同长度的数据帧,随后便能很便捷地实现解包处理。
在STM32中, 串口可以开启空闲停止(IDLE_INTERRUPT), 开启了空闲停止的话, 硬件会自动识别串口接收数据的情况, 一样平常串口外设发送完一段数据后, 需要一段短停息顿之后才能发送下一段数据, 故在两段数据之间, 会有一个比力短暂的停顿, 硬件接收完一个字节数据(一段数据的末了一个字节)后, 有一个短时间没有信息到来, 则会判定为这一段数据(约定俗成下会称这一段数据为一帧数据)已经接收完毕, 然后触发空闲停止, 在停止中, 用户即可通过一定的手段来判定这一帧数据一共有多少个字节, 这便是俗称的"不定长接收".
在cyt4887逐飞开发的代码框架中, 我并没有找到空闲停止的开启方法, 英飞凌的sdk中, 我也没有找到, 网上谷歌也没有这款芯片空闲停止的相干描述. 但根据以上所说的原理, 只要使用一个定时器停止, 提供串口外设的空闲判定工作, 也是能实现"空闲停止"的, 但显然这时候就不是停止了, 称为"空闲处理"更为适当.
注: 通过STM32实现这种手动判定的原理可以参考https://blog.csdn.net/yychuyu/article/details/134768431的4.3节
在此工程中, 重要是仿照上述链接的4.3节, 魔改了逐飞的fifo布局体和逐飞的串口读取函数, 来实现不定长接收的工程
使用不定长停止举行串口信息解包
前面说到,使用不定长停止对于串口信息的解包处理有很大资助,在此处对这个说明举行解释。以对裁判系统的信息解包为例子。首先查看裁判系统的解包规则。
发送的信标指令格式如下:
① 信标指令数据使用UART串行通讯协议,波特率115200,停止位1,8位数据位,无校验位;
② 信标指令间隔为100ms;
③ 一帧信标指令由4字节构成;
④ 信标指令的第一个字节buf1为帧头:0x66;
⑤ 信标指令的第二个字节buf2为当前正在工作的信标灯控制板的序号,范围:0x00-0x08;
⑥ 信标指令的第三个字节buf3为校验位:buf3 = buf2 ^ 0xff; (^为异或);
⑦ 信标指令的第四个字节buf4为帧尾:0x88。
数据帧的格式非常简单,然后构建数据帧的布局体信息
- /**
- * @brief 串口空闲状态判断
- *
- * @param pfifo
- * @return true
- * @return false
- */
- static bool isUartIdle(fifo_struct *pfifo) {
- if (pfifo->init_state == false ||
- (pfifo->head == 0 && pfifo->pre_head == 0)) {
- // 如果没有初始化过这个fifo || 没有数据,退出
- return false;
- }
- if (pfifo->head == pfifo->pre_head) {
- // 如果空闲了
- return true;
- }
- if (pfifo->head < pfifo->pre_head) {
- // 假如超过了数组长度
- return true;
- }
- // 如果在接收状态,也就是head变了,则更新pre_head
- pfifo->pre_head = pfifo->head;
- return false;
- }
- /**
- * @brief debug串口空闲中断处理函数
- *
- * @param pfifo 要被判断空闲状态的串口fifo
- * @param handle_func 空闲状态下要执行的处理函数
- */
- static void handleWhenIdle(uint8_t uart_id) {
- if (isUartIdle(puart_fifo_s[uart_id])) {
- // 如果空闲,执行处理函数
- if (uart_handle_callback_s[uart_id] != NULL) {
- uart_handle_callback_s[uart_id]((uint8_t *)puart_fifo_s[uart_id]->buffer,
- puart_fifo_s[uart_id]->max -puart_fifo_s[uart_id]->size);
- }
- // clear fifo
- fifo_clear(puart_fifo_s[uart_id]);
- }
- }
- // **************************** PIT中断函数 ****************************
- void pit0_ch0_isr() {
- pit_isr_flag_clear(PIT_CH0);
- // 如果debug串口空闲,则执行处理
- // handleWhenIdle(0); // 串口0空闲时,进行回调处理
- handleWhenIdle(1); // 串口1空闲时,进行回调处理 gps
- handleWhenIdle(2); // 串口2空闲时,进行回调处理 遥控
- handleWhenIdle(3); // 串口3空闲时,进行回调处理 裁判
- handleWhenIdle(4); // 串口4空闲时,进行回调处理 电驱
- }
复制代码 其中#pragma pack(1)和#pragma pack()是为了让布局体以一个字节单位举行字节对齐,这样整个布局体便可以像一个数组那样紧凑分列,方便我们的后续操作。
再然后构造解包函数,也就是串口不定长停止的停止回调函数。函数的详细流程是,传入了串口FIFO的缓存区buf和接收到的长度len,首先举行长度判定,假如长度不正确,直接返回false;然后使用memcpy函数将buf中len个字节的数据拷贝到tem_ref_info中,因为布局体是像数组一样紧凑分列的,所以举行这个操作不会有问题,假如布局体按默认的4字节举行字节对齐,那么像这样去使用memcpy函数举行拷贝的话有大概是不正确的。末了,对tem_ref_info的head、tail、和check_byte举行检验,假如全部正确,再把数据拷贝到referee_inf这个专门用于存放裁判系统数据的全局变量中。当然这样写入全局变量不是太规范的做法,前面已经移植了FreeROTS,那么应该使用好FreeRTOS的队列举行数据传递,不外因为项目对这个数据包的要求并不高且本人比力懒,所以此处直接使用了全局变量。
- // 串口的fifo
- fifo_struct uart0_fifo, uart1_fifo, uart2_fifo, uart3_fifo, uart4_fifo;
- // 串口fifo集合
- fifo_struct *puart_fifo_s[5] = {&uart0_fifo, &uart1_fifo, &uart2_fifo,&uart3_fifo, &uart4_fifo};
- //···//
- // 串口中断回调函数集合
- uart_handle_callback_t uart_handle_callback_s[5] = {
- refereeSystemUnpack, // 串口0中断回调函数
- decodeUbxPVT, // 串口1中断回调函数
- FS_I6X_RxCpltCallback, // 串口2中断回调函数
- refereeSystemUnpack, // 串口3中断回调函数
- escDataUnpack // 串口4中断回调函数
- };
复制代码 解包函数搞定以后,就将它放入串口停止回调函数集合数组中,按照对应的串口顺序放置,也就是usr_uart.cpp中的uart_handle_callback_s变量中,如下:
- // 串口0默认作为调试串口
- void uart0_isr(void) {
- if (Cy_SCB_GetRxInterruptMask(get_scb_module(UART_0)) &
- CY_SCB_UART_RX_NOT_EMPTY) { // 串口0接收中断
- // 清除接收中断标志位
- Cy_SCB_ClearRxInterrupt(get_scb_module(UART_0), CY_SCB_UART_RX_NOT_EMPTY);
- #if DEBUG_UART_USE_INTERRUPT // 如果开启 debug 串口中断
- uart0_read_byte();
- #endif // 如果修改了 DEBUG_UART_INDEX 那这段代码需要放到对应的串口中断去
- } else if (Cy_SCB_GetTxInterruptMask(get_scb_module(UART_0)) &
- CY_SCB_UART_TX_DONE) { // 串口0发送中断
- // 清除接收中断标志位
- Cy_SCB_ClearTxInterrupt(get_scb_module(UART_0), CY_SCB_UART_TX_DONE);
- }
- }
复制代码 c/c++混编
在最开始,为了实现面向对象的代码习惯,我是采用了c语言实现面向对象的方法,那时候之所以要这样大费周折,也是因为原来逐飞提供的开源工程中,缺乏对c/c++混编的支持,当时我也是避重就轻,懒得解决这个点,提到了很多报错懒得修改,所以先是采用c实现面向对象。当时的我也记录了使用c语言实现面向对象的方法,感兴趣的可以看下面的截图。
反面对代码举行迭代优化,完善了逐飞开源库中对c/c++混编的支持,也便实现了c/c++代码的混编,也很方便地用上了各种c++语法糖。要实现c/c++混编很紧张的一个知识点就是extern "C",这个是C++提供的一种兼容C语言的机制,这个机制的详细情况就不赘述了,不明白的可以搜刮一下~~(虽然搜刮到的效果一样平常都讲得不明不白,但我着实有点懒得写了)~~。
众所周知,逐飞的开源库和英飞凌提供的SDK都是用C语言实现的,我们下面不妨在逐飞开源库和英飞凌SDK中,挑一些文件来看看他们布局上的差异。
- /**
- * @brief 在中断时,读一个字节进入fifo
- *
- * @tparam uart_id
- */
- template <uint8_t uart_id> void uart_read_byte(void) {
- uint8_t rec_sta = 0;
- if (puart_fifo_s[uart_id]->init_state == true) {
- /* 以下有3种方式读取串口数据 */
- // 1. 一次读取
- // uart_query_byte(DEBUG_UART_INDEX, &debug_uart_data);
- // 2. 有数据才读取
- // debug_uart_data = uart_read_byte(DEBUG_UART_INDEX);
- // 3. 有数据才读取,超时会退出
- uint8_t get_byte;
- rec_sta = uart_read_self_quit((uart_index_enum)uart_id, &get_byte, 1000);
- if (rec_sta) {
- // 如果接收成功
- fifo_write_buffer(puart_fifo_s[uart_id], &get_byte, 1); // 存入 FIFO
- }
- }
- }
- // 初始化一些中断读字节的函数指针以供isr.c文件使用
- void (*uart0_read_byte)(void) = uart_read_byte<0>;
- void (*uart1_read_byte)(void) = uart_read_byte<1>;
- void (*uart2_read_byte)(void) = uart_read_byte<2>;
- void (*uart3_read_byte)(void) = uart_read_byte<3>;
- void (*uart4_read_byte)(void) = uart_read_byte<4>;
复制代码- // 函数简介 读取串口接收的数据(whlie等待)
- // 参数说明 uart_n 串口模块号 参照 zf_driver_uart.h 内
- // uart_index_enum 枚举体定义 参数说明 *dat 接收数据的地址
- // 返回参数 uint8 接收的数据
- // 使用示例 uint8 dat = uart_read_byte(UART_0); // 接收 UART_1
- // 数据 存在在 dat 变量里 备注信息
- //-------------------------------------------------------------------------------------------------------------------
- uint8 uart_read_byte(uart_index_enum uart_n) {
- while (Cy_SCB_GetNumInRxFifo(scb_module[uart_n]) == 0)
- ;
- return (uint8)Cy_SCB_ReadRxFifo(scb_module[uart_n]);
- }
- /**
- * @brief 带超时自动退出的串口读取函数
- *
- * @param uart_n 使用的uart号
- * @param dat 当下读到的一个字节
- * @param _quit_delay 超时自动退出的cnt阈值
- * @return uint8 【0】失败 【1】成功
- */
- uint8 uart_read_self_quit(uart_index_enum uart_n, uint8_t *dat,
- uint16_t _quit_delay) {
- uint16_t now_cnt = 0;
- do {
- now_cnt++;
- if (now_cnt >= _quit_delay) {
- return 0;
- }
- } while (Cy_SCB_GetNumInRxFifo(scb_module[uart_n]) == 0);
- *dat = (uint8)Cy_SCB_ReadRxFifo(scb_module[uart_n]);
- return 1;
- }
复制代码 有上面两个头文件中,我们不难发现,SDK的头文件中在函数的声明的上面和下面,是被一段东西(extern "C")给夹着的,而逐飞的头文件中是没有这一段的,这显然说明了英飞凌的SDK是做好了给用户举行C/C++混编的预备的,而逐飞大概认为大部分参赛队员不会使用到C++,所以没有做好这个预备,那么想要完善逐飞的开源库也很简单,只需要在每一个要用到的逐飞开源的头文件中,加上这么一段即可,示比方下。
- #pragma pack(1)
- typedef struct __RefereeInf_t {
- uint8_t head; // 0x66
- uint8_t ctrl_board_index;
- uint8_t check_byte; // ctrl_board_index ^ 0xff
- uint8_t tail; // 0x88
- LinkageStatus_Typedef link_status = LOST;
- uint8_t pre_ctrl_board_index;
- uint8_t ctrl_is_switch; // 0x01: change, 0x00: no change
- } RefereeInf_t;
- #pragma pack()
复制代码 当然这也是很 |