【C/C++】CYT4BB7控制中间层开源库

打印 上一主题 下一主题

主题 846|帖子 846|积分 2538

【写在前前前面】开源Gitee堆栈链接:
https://gitee.com/hallo_frank/cyt4-bb7_-control_-middleware_-library.git


【写在前前面】特殊感谢:

  • 逐飞科技CYT4BB7开源库,本项目所有工作均基于逐飞科技CYT4BB7开源库,特此感谢逐飞科技的开发人员,为智能车er们带来了云云全面完整的解决方案。
  • 越野信标组参赛队友及华南理工大学智能车队,在完资本项目的途中,假如缺少你们的资助,硬件上的、软件上的、精力上的,本项目肯定没办法到达这样的完成度,特此感谢这些为本项目提供过各种资助的智能车er们。
  • 华工机器人未来创新实验室SRML库历代的各位缔造者和维护者,本项目copy了SRML多个文件的源码,特此感谢你们很好的轮子,使我的拿来主义旋转。

【写在前面】特殊说明:
本项目中多数文件开头都带有与本人相干的开头,如下:
  1. /*
  2. * @Author: Jae Frank[thissfk@qq.com]
  3. * @Date: 2024-06
  4. * @LastEditors: Jae Frank[thissfk@qq.com]
  5. * @LastEditTime: 2024-06
  6. * @FilePath: xxx.c
  7. * @Description:
  8. *            If you need more information,
  9. * please contact Jae Frank[thissfk@qq.com] to get an access.
  10. * Copyright (c) 2024 by Jae Frank, All Rights Reserved.
  11. */
复制代码
此文件开头标识为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函数开始:
    1. osKernelInitialize();
    2. usrSystemInit();      // 初始化系统
    3. osKernelStart();
    复制代码
    以上三行,第一行是初始化FreeRTOS的内核,不需要深入理解也可以。
    第二行是封装好的初始化系统的函数,这个函数里面先是对系统的GPIO、串口等外设举行初始化,然后对相干外设的FreeRTOS任务举行初始化。
    第三行是开启FreeRTOS的内核调度,从这个函数开始,正式进入了FreeRTOS掌控任务调度的时期,FreeRTOS会根据每个任务的运行频率(由客户设置)和优先级(由客户设置),来判定每个时候的当下应该让哪个任务出于运行状态.
    以上,便是FreeRTOS启动的过程.
  • FreeRTOS任务的创建
    任务的创建一样平常先分配好相干的变量,然后通过以下这句代码实现最终的创建。
    1. defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); // 4. 创建任务(给任务分配运行空间),并将任务句柄变量和任务函数\任务参数表链接起来
    复制代码
    创建一个StartDefaultTask的任务,defaultTask_attributes是这个任务的参数列表,里面包含了这个任务的调用空间巨细\优先级等信息,而defaultTaskHandle是这个任务的句柄(handle的中文翻译),句柄可以理解成是一个令牌,古人使用令牌可以来指挥特定的队伍,同样的,用户想要支配某个任务,就是操纵通过这个任务的句柄来实现.
    上面提到过了任务\任务参数表\任务句柄等概念,那么此处展开说一下这些概念
    任务(任务函数): 这个指的是, 某个任务在运行时要执行的函数, 可以理解成这个任务所要执行的详细行为
    任务参数列表: 这个参数列表, 是一个struct布局体范例的数据, 包含了这个任务的名称字符串name, 任务运行的空间(栈)的巨细stack_size以及任务的优先级priority.
    任务句柄: 当任务创建乐成之后, 用户再要对某个任务举行任何的操作, 都是通过任务句柄即可访问到相对应任务的一切信息.
    下面用一份实例代码,说明详细的、完整的创建过程
    1. #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. 创建任务(给任务分配运行空间),并将任务句柄变量和任务函数\任务参数表链接起来
    2.   osKernelStart();}void StartDefaultTask(void *argument); // 5. 任务函数的界说, 任务中真正执行的事情{ // ...暂时省略不写   }
    复制代码
    不难看出, 从上到下, 重要分为5步

    • 创建任务句柄变量, 但此时只是创建了这个变量, 还没将这个变量和现实的任务实体举行链接
    • 创建任务参数列表变量, 里面会对三个变量举行设置, 这些也就是对任务的设置
    • 创建任务函数的声明, 因为一样平常情况下, 程序员都约定速成在文档的最上面先将函数举行声明, 在文档的最下面再对函数举行界说
    • 创建任务, 通过调用osThreadNew函数, 加载了任务函数和任务参数表创建了任务, 并将这个任务与任务句柄举行了链接
    • 任务函数的界说, 在这个任务中, 到底要做些什么, 就是在任务函数里面举行界说

  • 此工程中FreeRTOS任务的创建过程
    上面提到,在封装好的初始化系统函数usrSystemInit中会初始化相干外设和相干外设的FreeRTOS任务。
    1. /**
    2. * @brief 用户系统初始化
    3. *
    4. */
    5. void usrSystemInit(void) {
    6.   // 外设初始化
    7.   usr_sys.peripheralInit();
    8.   // 任务初始化
    9.   usr_sys.TaskCreate();
    10.   // 车子实例初始化
    11.   car.init();
    12. }
    复制代码
    其中usr_sys是USR_SYSTEM类的一个实例化对象,通过调用类函数peripheralInit来举行外设初始化,调用类函数TaskCreate举行任务初始化。下面看一下类函数TaskCreate的内容。
    1. /**
    2. * @brief 用户任务创建
    3. *
    4. */
    5. void USR_SYSTEM::TaskCreate() {
    6.   // 创建默认任务
    7.   defaultTaskHandle = osThreadNew(defaultTask, NULL, &defaultTask_attributes);
    8.   // 创建显示任务
    9.   oledTaskHandle = osThreadNew(oledTask, NULL, &oledTask_attributes);
    10. #if USE_FS_I6X
    11.   // 创建FS_I6X任务
    12.   remoteCtrlUnitTaskHandle =
    13.       osThreadNew(tskFS_I6X, NULL, &remoteCtrlUnitTask_attributes);
    14. #else
    15.   // 创建ps2任务
    16.   remoteCtrlUnitTaskHandle =
    17.       osThreadNew(ps2Task, NULL, &remoteCtrlUnitTask_attributes);
    18. #endif
    19.   // 创建电机驱动任务
    20.   motorTaskHandle = osThreadNew(motorTask, NULL, &motorTask_attributes);
    21.   // 创建舵机转向任务
    22.   servoTaskHandle = osThreadNew(servoTask, NULL, &servoTask_attributes);
    23. // 创建upperMonitor任务
    24. // upperMonitorTaskHandle =
    25. //     osThreadNew(upperMonitorTask, NULL, &upperMonitorTask_attributes);
    26. // 创建vofa任务
    27. #if !ADC_DATA_VIEW
    28.   vofaTaskHandle = osThreadNew(vofaTask, NULL, &vofaTask_attributes);
    29. #endif
    30. }
    复制代码
    由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。
  数据帧的格式非常简单,然后构建数据帧的布局体信息
  1. /**
  2. * @brief 串口空闲状态判断
  3. *
  4. * @param pfifo
  5. * @return true
  6. * @return false
  7. */
  8. static bool isUartIdle(fifo_struct *pfifo) {
  9.   if (pfifo->init_state == false ||
  10.       (pfifo->head == 0 && pfifo->pre_head == 0)) {
  11.     // 如果没有初始化过这个fifo || 没有数据,退出
  12.     return false;
  13.   }
  14.   if (pfifo->head == pfifo->pre_head) {
  15.     // 如果空闲了
  16.     return true;
  17.   }
  18.   if (pfifo->head < pfifo->pre_head) {
  19.     // 假如超过了数组长度
  20.     return true;
  21.   }
  22.   // 如果在接收状态,也就是head变了,则更新pre_head
  23.   pfifo->pre_head = pfifo->head;
  24.   return false;
  25. }
  26. /**
  27. * @brief debug串口空闲中断处理函数
  28. *
  29. * @param pfifo 要被判断空闲状态的串口fifo
  30. * @param handle_func 空闲状态下要执行的处理函数
  31. */
  32. static void handleWhenIdle(uint8_t uart_id) {
  33.   if (isUartIdle(puart_fifo_s[uart_id])) {
  34.     // 如果空闲,执行处理函数
  35.     if (uart_handle_callback_s[uart_id] != NULL) {
  36.       uart_handle_callback_s[uart_id]((uint8_t *)puart_fifo_s[uart_id]->buffer,
  37.         puart_fifo_s[uart_id]->max -puart_fifo_s[uart_id]->size);
  38.     }
  39.     // clear fifo
  40.     fifo_clear(puart_fifo_s[uart_id]);
  41.   }
  42. }
  43. // **************************** PIT中断函数 ****************************
  44. void pit0_ch0_isr() {
  45.   pit_isr_flag_clear(PIT_CH0);
  46.   // 如果debug串口空闲,则执行处理
  47.   // handleWhenIdle(0); // 串口0空闲时,进行回调处理
  48.   handleWhenIdle(1); // 串口1空闲时,进行回调处理 gps
  49.   handleWhenIdle(2); // 串口2空闲时,进行回调处理 遥控
  50.   handleWhenIdle(3); // 串口3空闲时,进行回调处理 裁判
  51.   handleWhenIdle(4); // 串口4空闲时,进行回调处理 电驱
  52. }
复制代码
其中#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的队列举行数据传递,不外因为项目对这个数据包的要求并不高且本人比力懒,所以此处直接使用了全局变量。
  1. // 串口的fifo
  2. fifo_struct uart0_fifo, uart1_fifo, uart2_fifo, uart3_fifo, uart4_fifo;
  3. // 串口fifo集合
  4. fifo_struct *puart_fifo_s[5] = {&uart0_fifo, &uart1_fifo, &uart2_fifo,&uart3_fifo, &uart4_fifo};
  5. //···//
  6. // 串口中断回调函数集合
  7. uart_handle_callback_t uart_handle_callback_s[5] = {
  8.     refereeSystemUnpack,   // 串口0中断回调函数
  9.     decodeUbxPVT,          // 串口1中断回调函数
  10.     FS_I6X_RxCpltCallback, // 串口2中断回调函数
  11.     refereeSystemUnpack,   // 串口3中断回调函数
  12.     escDataUnpack          // 串口4中断回调函数
  13. };
复制代码
解包函数搞定以后,就将它放入串口停止回调函数集合数组中,按照对应的串口顺序放置,也就是usr_uart.cpp中的uart_handle_callback_s变量中,如下:
  1. // 串口0默认作为调试串口
  2. void uart0_isr(void) {
  3.   if (Cy_SCB_GetRxInterruptMask(get_scb_module(UART_0)) &
  4.       CY_SCB_UART_RX_NOT_EMPTY) { // 串口0接收中断
  5.     // 清除接收中断标志位
  6.     Cy_SCB_ClearRxInterrupt(get_scb_module(UART_0), CY_SCB_UART_RX_NOT_EMPTY);
  7. #if DEBUG_UART_USE_INTERRUPT // 如果开启 debug 串口中断
  8.     uart0_read_byte();
  9. #endif // 如果修改了 DEBUG_UART_INDEX 那这段代码需要放到对应的串口中断去
  10.   } else if (Cy_SCB_GetTxInterruptMask(get_scb_module(UART_0)) &
  11.              CY_SCB_UART_TX_DONE) { // 串口0发送中断
  12.     // 清除接收中断标志位
  13.     Cy_SCB_ClearTxInterrupt(get_scb_module(UART_0), CY_SCB_UART_TX_DONE);
  14.   }
  15. }
复制代码
c/c++混编

在最开始,为了实现面向对象的代码习惯,我是采用了c语言实现面向对象的方法,那时候之所以要这样大费周折,也是因为原来逐飞提供的开源工程中,缺乏对c/c++混编的支持,当时我也是避重就轻,懒得解决这个点,提到了很多报错懒得修改,所以先是采用c实现面向对象。当时的我也记录了使用c语言实现面向对象的方法,感兴趣的可以看下面的截图。

反面对代码举行迭代优化,完善了逐飞开源库中对c/c++混编的支持,也便实现了c/c++代码的混编,也很方便地用上了各种c++语法糖。要实现c/c++混编很紧张的一个知识点就是extern "C",这个是C++提供的一种兼容C语言的机制,这个机制的详细情况就不赘述了,不明白的可以搜刮一下~~(虽然搜刮到的效果一样平常都讲得不明不白,但我着实有点懒得写了)~~。
众所周知,逐飞的开源库和英飞凌提供的SDK都是用C语言实现的,我们下面不妨在逐飞开源库和英飞凌SDK中,挑一些文件来看看他们布局上的差异。
  1. /**
  2. * @brief 在中断时,读一个字节进入fifo
  3. *
  4. * @tparam uart_id
  5. */
  6. template <uint8_t uart_id> void uart_read_byte(void) {
  7.   uint8_t rec_sta = 0;
  8.   if (puart_fifo_s[uart_id]->init_state == true) {
  9.     /* 以下有3种方式读取串口数据 */
  10.     // 1. 一次读取
  11.     // uart_query_byte(DEBUG_UART_INDEX, &debug_uart_data);
  12.     // 2. 有数据才读取
  13.     // debug_uart_data = uart_read_byte(DEBUG_UART_INDEX);
  14.     // 3. 有数据才读取,超时会退出
  15.     uint8_t get_byte;
  16.     rec_sta = uart_read_self_quit((uart_index_enum)uart_id, &get_byte, 1000);
  17.     if (rec_sta) {
  18.       // 如果接收成功
  19.       fifo_write_buffer(puart_fifo_s[uart_id], &get_byte, 1); // 存入 FIFO
  20.     }
  21.   }
  22. }
  23. // 初始化一些中断读字节的函数指针以供isr.c文件使用
  24. void (*uart0_read_byte)(void) = uart_read_byte<0>;
  25. void (*uart1_read_byte)(void) = uart_read_byte<1>;
  26. void (*uart2_read_byte)(void) = uart_read_byte<2>;
  27. void (*uart3_read_byte)(void) = uart_read_byte<3>;
  28. void (*uart4_read_byte)(void) = uart_read_byte<4>;
复制代码
  1. // 函数简介       读取串口接收的数据(whlie等待)
  2. // 参数说明       uart_n          串口模块号 参照 zf_driver_uart.h 内
  3. // uart_index_enum 枚举体定义 参数说明       *dat            接收数据的地址
  4. // 返回参数       uint8           接收的数据
  5. // 使用示例       uint8 dat = uart_read_byte(UART_0);             // 接收 UART_1
  6. // 数据  存在在 dat 变量里 备注信息
  7. //-------------------------------------------------------------------------------------------------------------------
  8. uint8 uart_read_byte(uart_index_enum uart_n) {
  9.   while (Cy_SCB_GetNumInRxFifo(scb_module[uart_n]) == 0)
  10.     ;
  11.   return (uint8)Cy_SCB_ReadRxFifo(scb_module[uart_n]);
  12. }
  13. /**
  14. * @brief 带超时自动退出的串口读取函数
  15. *
  16. * @param uart_n 使用的uart号
  17. * @param dat 当下读到的一个字节
  18. * @param _quit_delay 超时自动退出的cnt阈值
  19. * @return uint8 【0】失败 【1】成功
  20. */
  21. uint8 uart_read_self_quit(uart_index_enum uart_n, uint8_t *dat,
  22.                           uint16_t _quit_delay) {
  23.   uint16_t now_cnt = 0;
  24.   do {
  25.     now_cnt++;
  26.     if (now_cnt >= _quit_delay) {
  27.       return 0;
  28.     }
  29.   } while (Cy_SCB_GetNumInRxFifo(scb_module[uart_n]) == 0);
  30.   *dat = (uint8)Cy_SCB_ReadRxFifo(scb_module[uart_n]);
  31.   return 1;
  32. }
复制代码
有上面两个头文件中,我们不难发现,SDK的头文件中在函数的声明的上面和下面,是被一段东西(extern "C")给夹着的,而逐飞的头文件中是没有这一段的,这显然说明了英飞凌的SDK是做好了给用户举行C/C++混编的预备的,而逐飞大概认为大部分参赛队员不会使用到C++,所以没有做好这个预备,那么想要完善逐飞的开源库也很简单,只需要在每一个要用到的逐飞开源的头文件中,加上这么一段即可,示比方下。
  1. #pragma pack(1)
  2. typedef struct __RefereeInf_t {
  3.   uint8_t head; // 0x66
  4.   uint8_t ctrl_board_index;
  5.   uint8_t check_byte; // ctrl_board_index ^ 0xff
  6.   uint8_t tail;       // 0x88
  7.   LinkageStatus_Typedef link_status = LOST;
  8.   uint8_t pre_ctrl_board_index;
  9.   uint8_t ctrl_is_switch; // 0x01: change, 0x00: no change
  10. } RefereeInf_t;
  11. #pragma pack()
复制代码
当然这也是很

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

渣渣兔

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表