修改文档结构,更新TODO list

This commit is contained in:
NeoZng 2023-07-23 23:04:44 +08:00
parent 018b7ba6e6
commit 1969adb949
3 changed files with 281 additions and 251 deletions

237
README.md
View File

@ -1,258 +1,43 @@
# 2023 EC basic-framework # YueLu2022/2023 EC basic_framework-dev
> **代码参考了哈工深南宫小樱战队的框架设计和RMUA官方开源RoboRTS-firmware框架在此鸣谢。**
> 每个bsp/module/application都有对应文档建议阅读之后再看代码&进行开发。框架的搭建思路和讲解视频戳这里:[basic_framework讲解](https://www.bilibili.com/video/BV1Bd4y1E7CN)。
> 开发之前必看的文档:**README.md & VSCode+Ozone使用方法.md** 。开发app层请看application目录下的文档若要开发module以及bsp务必把上层文档也浏览一遍以熟悉接口定义的方式。
> **程序的运行流程和框架所有app/module/bsp的数据流图直接拉到本文档底部。**
此框架为机器人通用设计当前的app层是为步兵设计的。不同的机器人只需要重新编写应用层。在我们的战队仓库中有英雄、工程、哨兵、平衡步兵等兵种可作参考。
此框架在RoboMaster A型开发板的移植也已在组织仓库中提供。
[TOC] [TOC]
## 基本信息和开发规范 本框架设计参考了哈尔滨工业大学深圳南工骁🦅战队的EC_framework以及RoboMaster官方的RoboRTS-firmware。
- **开发方式** - 若无法访问github戳[gitee仓库](https://gitee.com/hnuyuelurm/basic_framework)
- 若gitee内容被屏蔽戳[github仓库](https://github.com/HNUYueLuRM/basic_framework)
本框架使用stm32cubemx生成基于makefile编译系统后期拟修改为cmake+nijna+makefile以提高编译速度对于目前的版本您可以考虑自行安装ccache以提高编译速度使用arm gnu工具链开发利用arm-none-eabi-gcc编译make命令,命令行为mingw32-make > 基于basic_framework打造的C++进阶重构版本[***powerful_framework***](https://gitee.com/hnuyuelurm/powerful_framework)现已发布增加全新的消息交互机制和严格的跨任务数据读写保护采用了现代构建系统CMake+Ninja以追求极致的编译速度各种针对嵌入式的编译优化全开DIY程度进一步提升更有自定义CMSIS-DSP和Eigen等扩展库支持快来加入试用/和我们一起开发吧
> ***==deprecated==***若需使用keil5开发请在stm32cubemx的`project manager`标签页下将工具链改为MDK然后在keil中自行添加所需包含的.c文件和头文件。关于如何在keil下添加dsplib请参考文档。在vscode中也有**KEIL assistant**和**Embedded IDE**插件可供使用。
>
> ***强烈推荐使用VSCode进行开发Ozone进行调试。***
VSCode可通过Cortex-Debug利用OpenOCD进行调试jlink/stlink/dap-link都支持具体的使用方法和环境配置教程在[VSCode+Ozone使用方法](./VSCode+Ozone使用方法.md)中。**请使用UTF-8编码查看\&编辑此项目**。
**此外,本项目中使用到的物理变量值均采用标准单位制**若有特殊需求可以通过module层的`general_def.h`添加物理量转换关系的宏。
- **分层** ## 架构
本框架主要代码分为**BSP、Module、APP**三层。三层的代码分别存放在同名的三个文件夹中这三个文件夹存放在根目录下。开发过程中主要编写APP层代码Module层与BSP层不建议修改。如需添加module如oled屏幕、其他传感器和外设等请按照规范编写并联系组长提交commit到dev分支或对应的功能名分支完善后合并至主分支。在配置git的时候将自己的`user.name`配置成英文缩写或易懂的nick name。
BSP层构建于ST的HAL硬件抽象层之上针对RoboMaster竞赛所用电控外设和模块的特点对其进行了进一步封装Module层是基于bsp的封装打造的各种模块旨在为app层提供**硬件无关的接口**,即应用层不应该出线任何与片上外设相关的代码。
**main.c的位置在**`Src/main.c`
- **代码格式**
在vscode-设置-扩展-C/C++-C_Cpp:style下修改。默认为`Visual Studio`。编写完新的代码后,使用右键-格式化文档(注请勿对cube生成的文件使用此操作)。此操作不会改变文档的内容,但会改变缩进、空行、符号位置等,使代码更加统一、整洁。 ## 开发工具
**在cubemx生成的文件(尤其是main.c和freertos.c)时,务必按照cubemx的提示将用户代码放在usercode注释代码块内,否则重新生成时会被覆盖.**
请保持良好的注释编写习惯建议安装doxygen插件。务必统一在.h文件中为外部接口编写注释并给类型定义编写必要的注释。对于私有函数.c文件中static修饰请在.c文件中进行注释。对于复杂的代码段也请添加注释。
每个功能模块编写完之后,及时添加说明文档。内容参照已有的文档,要进行简短的**总体说明、代码结构、外部接口和类型定义、私有函数和变量,以及使用的说明和范例**。如果有特别需要注意的地方,也请说明。
==**在编写代码的时候注意添加安全检查“treat your users as idiots”**==
- **面向对象设计** ## 设计思想
C语言不存在“成员函数”的概念。为实现类似效果所有按照这一思想构建的函数都会有一个传入参数将结构体对象传入。
- **代码风格:**
函数统一使用**动宾短语**建议不超过4个单词。每个单词首字母大写
```c
void SetMotorControl()
```
变量命名使用下划线命名法,统一小写。尽量不要使用缩写,并注意让变量名本身能够表达其含义: ## 执行顺序与数据流
```c
uint8_t gimbal_recv_cmd;
```
后续可能将指针类型的变量名都加上`ptr_`或`p`前缀。私有变量加上下划线`_`前缀。
在利用`typedef`定义新的类型时,使用单词首字母大写+下划线隔开+定义后缀的方式:
```c
typedef struct
{
float Accel[3];
float Gyro[3];
} IMU_Data_t;
typedef struct ## 如何使用本框架
{
can_instance_config_s can_config;
uint8_t send_data_len;
uint8_t recv_data_len;
} CANComm_Init_Config_s;
typedef struct
{
float *other_angle_feedback_ptr;
float *other_speed_feedback_ptr;
PID_t current_PID; ## 后续计划
PID_t speed_PID;
PID_t angle_PID;
float pid_ref; // 将会作为每个环的输入和输出顺次通过串级闭环
} Motor_Controller_s;
```
数据类型单一、结构不复杂的类型以`_t`后缀结尾表明这是一种数据type复杂的结构体类型使用`_s`结尾表明其功能和内涵多structure。对于某个bsp、module其类型结构体应该称为`xxxInstance`:
```c
typedef struct _
{
CAN_HandleTypeDef *can_handle; // can句柄
CAN_TxHeaderTypeDef txconf; // CAN报文发送配置
uint32_t tx_id; // 发送id
uint32_t tx_mailbox; // CAN消息填入的邮箱号
uint8_t tx_buff[8]; // 发送缓存,最大为8
uint8_t rx_buff[8]; // 接收缓存
uint32_t rx_id; // 接收id
uint8_t rx_len; // 接收长度,可能为0-8
// 接收的回调函数,用于解析接收到的数据
void (*can_module_callback)(struct _ *); // callback needs an instance to tell among registered ones
} CANInstance;
```
## BSP层(Board Sopport Package)
- 主要功能实现对STM HAL的封装功能进一步抽象硬件。
- 在本框架中BSP层与cubeMX初始化有一定程度的耦合若没有在CUBEMX中开启某个外设则在application不能初始化使用了对应外设的module。对该层的修改可能需要使用cube重新生成工程主要是外设的配置通信速度时钟频率和分频数等。该层也是唯一允许直接出现stm32HAL库函数的代码层**在非BSP层编写代码时如需使用HAL_...函数请思考是否有同功能的BSP_...函数**。不过由于ST的HAL已经对硬件进行较高的抽象如以handle_xxx的方式描述一个硬件外设或功能引脚因此即使需要更换开发板必须修改的内容也极少。
- 最简单的(如gpio)仅是对HAL库函数的封装。较为复杂的则会进行一定程度的处理(如can)
**编写和使用指南**
- 补充与修改某款主控对应的BSP层应保持相同当认为该层可能缺少部分功能或有错误时请联系组长确认后解决并更新整个框架**请勿自行修改提交**。 请在你修改/增加的bsp_XXX.md中提供测试用例和使用示范以及任何其他需要注意的事项并在代码必要的地方添加注释。
- 代码移植BSP层也是在不同系列、型号的stm32间执行代码移植时主要需要关注的代码层。向功能更强系列移植一般只需要重配cube而向功能较少的系列移植还需要去掉其不支持的功能。如果仅是对同一型号的开发板进行CUBEMX初始化配置的修改一般只需要给app层的应用重新分配外设和引脚或修改波特率和通信频率等。
- 子文件与文件夹:
- bsp.c/h该层用于bsp基础功能初始化的文件其中`bsp.h`被include至main.c中以实现必须的底层初始化目前需要初始化的bsp只有log和dwt**不同主频的MCU需要修改dwt初始化的参数**。**注意**有些外设如串口和CAN不需要在bsp.c中进行模块层的初始化他们会在module层生成实例即C语言中的结构体并注册到bsp层时自动进行初始化。以此达到提高运行速度避免未使用的模块被加载的问题。
- bsp_xxx.c/h每一个成对的.c/h对应一种外设当上面两个代码层需要使用某个外设时这里的文件就是对应的交互接口。
- 注册回调函数与接收:通信类外设模块有的定义了回调函数(函数指针类型)module层的模块需要自行处理接收回调函数在注册bsp的时候应传入对应参数格式的回调函数指针使得接收中断发生的时候bsp层可以自行找到对应的上层回调函数进行调用。这也是回调函数设计的初衷为底层代码调用上层代码提供接口当特定事件发生的时候完成触发自行搜索hook函数
## Module层
- 主要功能实现对设备的封装如将IMU、PC、电机等视为一个完整的功能模块让应用层app不需要关心其底层的具体实现直接使用接口。
- 文件夹
- **注意module层没有也不需要进行统一初始化**。app层的应用会包含一些模块因此由app来调用各个模块的init()或register()函数只有当一个module被app实例化这个模块才会存在。
> ~~命名为init()的初始化一般来说是开发板的独占资源即有且只有一个这样的模块无法拥有多个实例如板载陀螺仪、LED、按键等。命名为register()的模块则可以拥有多个,比如电机。~~ legacy support为了保证代码风格统一所有接口统一命名为xxxRegister()。
- algorithm:该层软件库存放位置,这些功能与硬件无关,而是提供通用的数据结构和“算子”以供该层的其他部分调用,主要是算法、控制器、底盘和位姿解算等。
- module编写和使用指南
- 初始化:
根据代码对应的函数说明传入对应的配置文件。对于某些需要集中设置的参数一般于模块的头文件中会额外设定一个xxx_config_s的结构体用于初始化的参数传递。如果不需要进行这样的集中设置则是直接传入对应的参数或module结构体中本就存在的成员变量。
- 结构体:
也就是所说的“实例”定义一个module结构体对于app层来说就是拥有某一个功能模块的实例比如一个特定的电机。在对电机进行操作的时候为实现面向对象的功能需要在接口函数中传入该结构体指针。
- 函数:
.c中存放的static函数和static变量相当于这个类的private函数.h中的则相当于public。相似的driver的public函数应较为统一。由于通信格式使用方法等的不同不同通信设备在读取操作、数据格式上可能有所不同这些不同应该在driver的内部处理。**由于C语言没有对象的概念对于通信类的module不同的实例需要在module.c中保存一份指针用于处理数据接收的解析。**
- 封装程度:
app层使用时与底层实现无关。如在使用电机时这个电机的数据该和哪些电机的数据在一个数据包中发送can的过滤器设置均属于应该自动处理的功能通信类的模块应该封装到只有初始化、发送和读取。对于电机则是用于初始化的`register`和发送控制命令`set_control`两个函数和一个实时更新的用于给app层提供该信息的数据结构体电机反馈信息
Module层主要存放的是类型定义和实例指针数组在该层没有进行实例化定义或通过malloc分配空间若在APP层没有实例化则该模块的存在与否不会影响编译后的可执行文件只会占用.c文件中的static变量和代码区的少量内存有些module只会保存每个实例对象的指针在没有初始化的时候仅仅占用一个指针数组的空间。因此基于本框架的其他工程没有必要删除APP层未使用的module文件。
务必为模块添加说明文档和使用范例,以及其他需要注意的事项(如果有)。
> **面向对象小指南:**
>
> 由于C语言没有对象的概念对于需要使用通信的module在其.c文件下都需要保存每个实例的指针在收到消息时(发生回调)遍历所有实例指针找到收到消息的实例。这种处理方式可能会导致实时性下降例如CAN接收时要遍历所有注册了CAN的实例进入module层还需要一次遍历。用C++则可以将对象的this指针和模块的回调函数进行绑定生成一个可调用对象然后再进行CAN的注册使得其不需要module层的遍历。
>
> 考虑在实例中加入一个额外的`void*`域成员成员变量其内容为module层实例的地址。这样CAN收到消息时只需要遍历所有CAN instance对于相同的模块可以在其回调函数内部获取CAN instance的`void*`指针并通过强制类型转换cast成模块的实例结构体指针类型从而访问特定的模块。
> 这实际上是保存“对象”的parent pointer使得实例可以访问拥有自己的实例访问自己的父亲。和回调函数配合就可以防止交叉包含并为底层访问上层内容提供支持。
## APP层(application)
- 功能:实现机器人的控制,对机器人**控制**结构进行抽象。
在完成BSP层和Module层后如果在APP层没有控制代码则代码并无实际功能。换言之BSP层与Module层的存在是为了APP层更简单、更合理、更易于扩展和移植。本框架的初始目标即是实现在APP层仅需思考逻辑并用无关硬件的C语言代码实现即可完成整个机器人的控制。所有需要使用的模块和算法都在Module层提供开发板外设硬件的抽象在bsp层完成。**所有使用到的模块都在APP层初始化**因此不需要module自行初始化。
- APP层按照机械设计结构如云台、发射、底盘、夹爪、抬升、机械臂建立对应的子文件夹在其中完成初始化和相关逻辑功能的编写。还有用于发布指令的云台指令应用和底盘指令应用前者应该包含一个遥控器模块和一个视觉通信模块后者包含裁判系统模块。它们包含的模块都会处理一些指令和控制信息这样还可以方便兼容双板。
- 单双板切换在application的`robot_def.h`中进行,**修改宏定义可以切换开发板的设定模式**。当设定为单板的时候,在`robot.c`中会对gimbalchassisshootrobot_cmd四个应用都进行初始化。对于双板的情况需要将上板配置为gimbal board下板配置为chassis board它们会分别初始化gimbal/shoot/robot_cmd和chassis
- 对于单板的情况所有应用之间的信息交互通过message center完成。而使用双板时需要通过板间通信传递控制信息默认遥控器接收机和pc在云台板裁判系统在底盘板因此需要互发信息。当前通过**条件编译**来控制信息的去向发往message center/接收还是通过can comm发送/接收后续考虑将双板通信纳入message center的实现中根据`robot_def.h`的开发板定义自动处理通信,降低应用层级的逻辑复杂度。
## 文件树
板级支持包的每个组件,每个moduel,以及每个app都有对应的说明文档.
```shell
ROOT:.
│ .gitignore # git版本管理忽略文件
│ .mxproject # CubeMX项目文件
│ basic_framework.ioc # CubeMX初始化配置文件
| debug_ozone.jdebug # ozone debug调试配置和缓存文件
│ LICENSE # 开源协议文件
│ Makefile # 编译管理文件,为make(mingw32-make)命令的目标
│ openocd_dap.cfg # 用于OpenOCD调试使用的配置文件,dap用
│ openocd_jlink.cfg # 用于OpenOCD调试使用的配置文件,jlink用
│ README.md # 本说明文档
│ startup_stm32f407xx.s # F407汇编启动文件
│ stm32.jflash # jlink的烧录的配置文件,一键下载用
│ STM32F407.svd # F407外设地址映射文件,用于调试
│ STM32F407IGHx_FLASH.ld # F407IGH(C板MCU)目标FLASH地址和链接规则,用于编译(作为链接阶段的链接器)
| task.ps1 # powershell脚本,一键编译并进入ozone调试/reset开发板用
│ TODO.md # 项目待完成的任务
│ VSCode+Ozone使用方法.md # 开发环境配置和前置知识介绍
│ 修改HAL配置时文件目录的更改.md # 重新配置CubeMX时的步骤和注意事项
│ 必须做&禁止做.md # 开发必看,规范和要求
| 如何定位bug.md # 开发必看,快速定位bug并进行修复.还提供了一些debug典例
|
├─.vscode
│ launch.json # 调试的配置文件
│ settings.json # 工作区配置文件,根据自己的需要配置
│ tasks.json # 任务配置文件,包括一键编译下载调试等
├─.assets # 说明文档的图片
├─application # 应用层
├─bsp # 板级支持包
├─modules # 模块层
|
├─Src #hal生成的外设初始化源文件
├─Inc #hal生成的外设初始化头文件
├─Drivers #hal driver和cmsis drivers
└─Middlewares # STusb ext , rtos , segger rtt等
```
## BSP/Module/Application介绍
在对应应用、模块和板级支持包文件夹下。每个.c文件或完整的功能模块都有说明文档。在编写新代码时注意按照规范编写说明文档。
## 整体架构
### 软件分层
![image-20221113211942850](.assets/framework.png)
### 运行任务
![image-20230413195155471](.assets/image-20230413195155471.png)
### 初始化流程
~~~mermaid
graph TD
HAL库初始化 --> BSP初始化 --> Application初始化 --> app调用其拥有模块的初始化 --> 启动操作系统
~~~
**注意,应用初始化不得放入其对应任务中,即使是在死循环前,否则可能导致一些需要定时器的任务初始化异常**。
APP会调用其所有的模块的初始化函数注册函数这是因为本框架的设计思想是任何模块在被注册构造/初始化)之前,都是不存在的,当且仅当定义了一个模块结构体(也称实例)的时候,才有一个实体的概念。
main函数唯一需要的函数是app层的`robot.c`中的`RobotInit()`函数它首先会调用BSP初始化然后进行所有应用的初始化每个应用会调用对应模块的初始化一些依赖通信外设的模块会将通信支持相关的bsp进行初始化。初始化结束之后实时系统启动。
### 程序运行流程
![运行](.assets/总程序流程.png)
### 程序数据流
![数据流](.assets/数据流.png)

37
TODO.md
View File

@ -10,6 +10,12 @@
- [ ] 由于我们读写和传递的数据结构都不大,基本不会发生读写时任务切换的情况。典型的数据读写时间都是~μs故没有对数据访问的接口添加互斥锁或关闭全局中断。后续有需求如大量数据复制可以添加。可以新增一个bsp_mutex或者module层的ds提供相应支持。实际上freertos提供了一些供线程任务间进行数据交互的类型和函数请查阅对应文档。 - [ ] 由于我们读写和传递的数据结构都不大,基本不会发生读写时任务切换的情况。典型的数据读写时间都是~μs故没有对数据访问的接口添加互斥锁或关闭全局中断。后续有需求如大量数据复制可以添加。可以新增一个bsp_mutex或者module层的ds提供相应支持。实际上freertos提供了一些供线程任务间进行数据交互的类型和函数请查阅对应文档。
最简单的防止重入或数据竞争的方式是创建一个bool类型值实现互斥访问。当一个线程或中断要访问某个变量时先检查这个变量对应的bool锁是否为1若不为1则赋值为1表明当前有线程访问变量之后可以开始对该变量的操作结束读写后释放锁即赋0。不像osSemaphore或osMessageQueue等只允许在中断中添加消息或释放信号量/互斥量,这种变量锁可以在任意处加锁解锁。
若锁获取失败则直接退出或将当前线程挂起等待下一次唤醒。最常见的情况是一个线程需要从一块数据区或缓冲区读取数据而某个中断会向这个区域写入数据线程在读取的时候很可能会被中断打断。那么中断进入时发现该数据已经被上锁就不会强行写入。为了保证数据的实时性你可以选择将数据存入队列或启动一个新的任务当锁释放时再写入数据。如果数据量较小你不在乎开销则可以使用freerots提供的osMessageQueue或osMessageMailbox。
目前我们在bsp_dwt中添加了一个位锁防止中断中调用DWTGetDeltaT或DWTGetTimeline函数更新DWT维护的时间时打断任务中的相同函数导致计数被重复更新或引起错误的DWT溢出检测。
## BSP ## BSP
### 待完成 ### 待完成
@ -34,7 +40,7 @@
#### bsp_wifi #### bsp_wifi
- [ ] 增加无线网功能,方便调试和测试 - [ ] 增加无线局域网功能,方便调试和测试
--- ---
@ -46,12 +52,6 @@
- [ ] 给每个模块增加调试的条件编译并增加bsp log的输出。或直接在运行时添加log等级输出不同的信息。 - [ ] 给每个模块增加调试的条件编译并增加bsp log的输出。或直接在运行时添加log等级输出不同的信息。
#### buzzer
- [ ] 使用bsp_pwm添加buzzer模块
- [ ] 添加初始化完成时的音乐播放
#### ==servo_motor== #### ==servo_motor==
舵机模块需要预先定义90/180/360连续旋转的电机类型并且能够设定max和min位置。 舵机模块需要预先定义90/180/360连续旋转的电机类型并且能够设定max和min位置。
@ -76,15 +76,7 @@
> 是否需要在module层就和**daemon**模块配合? > 是否需要在module层就和**daemon**模块配合?
- [ ] 增加错误或异常提示音 当前实现为buzzer是单独的module若需要蜂鸣器警报的module可以自行包含buzzer.h以创建不同情况下的警报如电机离线、堵转、遥控器离线等。
- [ ] 增加功能提示音
#### led
> 是否需要和**daemon**模块配合这同时会影响到module层的led_task放在这一并解决。
- [ ] 增加错误或异常流水灯
- [ ] 增加功能点灯
#### BMI088 #### BMI088
@ -92,12 +84,11 @@
#### remote_control #### remote_control
- [ ] 增加长按/短按检测 - [ ] 增加长按/短按检测 (是否有必要?)
#### message_center #### message_center
- [ ] 增加队列剩余信息和数据时间戳的支持 - [ ] 增加队列剩余信息和数据时间戳的支持
- [ ] 增加数据为空的处理
- [ ] 提供直接传递指针的接口? - [ ] 提供直接传递指针的接口?
#### can_comm #### can_comm
@ -108,10 +99,6 @@
- [ ] 将PID的初始化改写为PIDRegister的形式,在controller统一分配内存. - [ ] 将PID的初始化改写为PIDRegister的形式,在controller统一分配内存.
#### user_lib
- [ ] 将所有通用的计算函数和常用函数汇集在此
#### dji_motor #### dji_motor
- [ ] 增加3508和2006的开环零位校准函数 - [ ] 增加3508和2006的开环零位校准函数
@ -142,7 +129,7 @@
#### controller #### controller
- [ ] 增加扰动观测器,可能需要新增模块 - [ ] 增加扰动观测器,可能需要新增模块
- [ ] 增加模型控制器,可能需要新增模块 - [ ] 增加基于模型控制器,可能需要新增模块
#### ws2816 #### ws2816
@ -170,10 +157,10 @@
#### gimbal #### gimbal
- [ ] 增加底盘速度前馈控制 - [x] 增加底盘速度前馈控制
#### chassis #### chassis
- [ ] 根据电机的实际速度计算底盘的真实运动(轮式里程计) - [x] 根据电机的实际速度计算底盘的真实运动(轮式里程计)
- [ ] 若为双板根据IMU的数据对电机实际速度进行融合 - [ ] 若为双板根据IMU的数据对电机实际速度进行融合

View File

@ -0,0 +1,258 @@
# 2023 EC basic-framework
> **代码参考了哈工深南宫小樱战队的框架设计和RMUA官方开源RoboRTS-firmware框架在此鸣谢。**
> 每个bsp/module/application都有对应文档建议阅读之后再看代码&进行开发。框架的搭建思路和讲解视频戳这里:[basic_framework讲解](https://www.bilibili.com/video/BV1Bd4y1E7CN)。
> 开发之前必看的文档:**README.md & VSCode+Ozone使用方法.md** 。开发app层请看application目录下的文档若要开发module以及bsp务必把上层文档也浏览一遍以熟悉接口定义的方式。
> **程序的运行流程和框架所有app/module/bsp的数据流图直接拉到本文档底部。**
此框架为机器人通用设计当前的app层是为步兵设计的。不同的机器人只需要重新编写应用层。在我们的战队仓库中有英雄、工程、哨兵、平衡步兵等兵种可作参考。
此框架在RoboMaster A型开发板的移植也已在组织仓库中提供。
[TOC]
## 基本信息和开发规范
- **开发方式**
本框架使用stm32cubemx生成基于makefile编译系统后期拟修改为cmake+nijna+makefile以提高编译速度对于目前的版本您可以考虑自行安装ccache以提高编译速度使用arm gnu工具链开发利用arm-none-eabi-gcc编译make命令,命令行为mingw32-make
> ***==deprecated==***若需使用keil5开发请在stm32cubemx的`project manager`标签页下将工具链改为MDK然后在keil中自行添加所需包含的.c文件和头文件。关于如何在keil下添加dsplib请参考文档。在vscode中也有**KEIL assistant**和**Embedded IDE**插件可供使用。
>
> ***强烈推荐使用VSCode进行开发Ozone进行调试。***
VSCode可通过Cortex-Debug利用OpenOCD进行调试jlink/stlink/dap-link都支持具体的使用方法和环境配置教程在[VSCode+Ozone使用方法](./VSCode+Ozone使用方法.md)中。**请使用UTF-8编码查看\&编辑此项目**。
**此外,本项目中使用到的物理变量值均采用标准单位制**若有特殊需求可以通过module层的`general_def.h`添加物理量转换关系的宏。
- **分层**
本框架主要代码分为**BSP、Module、APP**三层。三层的代码分别存放在同名的三个文件夹中这三个文件夹存放在根目录下。开发过程中主要编写APP层代码Module层与BSP层不建议修改。如需添加module如oled屏幕、其他传感器和外设等请按照规范编写并联系组长提交commit到dev分支或对应的功能名分支完善后合并至主分支。在配置git的时候将自己的`user.name`配置成英文缩写或易懂的nick name。
BSP层构建于ST的HAL硬件抽象层之上针对RoboMaster竞赛所用电控外设和模块的特点对其进行了进一步封装Module层是基于bsp的封装打造的各种模块旨在为app层提供**硬件无关的接口**,即应用层不应该出线任何与片上外设相关的代码。
**main.c的位置在**`Src/main.c`
- **代码格式**
在vscode-设置-扩展-C/C++-C_Cpp:style下修改。默认为`Visual Studio`。编写完新的代码后,使用右键-格式化文档(注请勿对cube生成的文件使用此操作)。此操作不会改变文档的内容,但会改变缩进、空行、符号位置等,使代码更加统一、整洁。
**在cubemx生成的文件(尤其是main.c和freertos.c)时,务必按照cubemx的提示将用户代码放在usercode注释代码块内,否则重新生成时会被覆盖.**
请保持良好的注释编写习惯建议安装doxygen插件。务必统一在.h文件中为外部接口编写注释并给类型定义编写必要的注释。对于私有函数.c文件中static修饰请在.c文件中进行注释。对于复杂的代码段也请添加注释。
每个功能模块编写完之后,及时添加说明文档。内容参照已有的文档,要进行简短的**总体说明、代码结构、外部接口和类型定义、私有函数和变量,以及使用的说明和范例**。如果有特别需要注意的地方,也请说明。
==**在编写代码的时候注意添加安全检查“treat your users as idiots”**==
- **面向对象设计**
C语言不存在“成员函数”的概念。为实现类似效果所有按照这一思想构建的函数都会有一个传入参数将结构体对象传入。
- **代码风格:**
函数统一使用**动宾短语**建议不超过4个单词。每个单词首字母大写
```c
void SetMotorControl()
```
变量命名使用下划线命名法,统一小写。尽量不要使用缩写,并注意让变量名本身能够表达其含义:
```c
uint8_t gimbal_recv_cmd;
```
后续可能将指针类型的变量名都加上`ptr_`或`p`前缀。私有变量加上下划线`_`前缀。
在利用`typedef`定义新的类型时,使用单词首字母大写+下划线隔开+定义后缀的方式:
```c
typedef struct
{
float Accel[3];
float Gyro[3];
} IMU_Data_t;
typedef struct
{
can_instance_config_s can_config;
uint8_t send_data_len;
uint8_t recv_data_len;
} CANComm_Init_Config_s;
typedef struct
{
float *other_angle_feedback_ptr;
float *other_speed_feedback_ptr;
PID_t current_PID;
PID_t speed_PID;
PID_t angle_PID;
float pid_ref; // 将会作为每个环的输入和输出顺次通过串级闭环
} Motor_Controller_s;
```
数据类型单一、结构不复杂的类型以`_t`后缀结尾表明这是一种数据type复杂的结构体类型使用`_s`结尾表明其功能和内涵多structure。对于某个bsp、module其类型结构体应该称为`xxxInstance`:
```c
typedef struct _
{
CAN_HandleTypeDef *can_handle; // can句柄
CAN_TxHeaderTypeDef txconf; // CAN报文发送配置
uint32_t tx_id; // 发送id
uint32_t tx_mailbox; // CAN消息填入的邮箱号
uint8_t tx_buff[8]; // 发送缓存,最大为8
uint8_t rx_buff[8]; // 接收缓存
uint32_t rx_id; // 接收id
uint8_t rx_len; // 接收长度,可能为0-8
// 接收的回调函数,用于解析接收到的数据
void (*can_module_callback)(struct _ *); // callback needs an instance to tell among registered ones
} CANInstance;
```
## BSP层(Board Sopport Package)
- 主要功能实现对STM HAL的封装功能进一步抽象硬件。
- 在本框架中BSP层与cubeMX初始化有一定程度的耦合若没有在CUBEMX中开启某个外设则在application不能初始化使用了对应外设的module。对该层的修改可能需要使用cube重新生成工程主要是外设的配置通信速度时钟频率和分频数等。该层也是唯一允许直接出现stm32HAL库函数的代码层**在非BSP层编写代码时如需使用HAL_...函数请思考是否有同功能的BSP_...函数**。不过由于ST的HAL已经对硬件进行较高的抽象如以handle_xxx的方式描述一个硬件外设或功能引脚因此即使需要更换开发板必须修改的内容也极少。
- 最简单的(如gpio)仅是对HAL库函数的封装。较为复杂的则会进行一定程度的处理(如can)
**编写和使用指南**
- 补充与修改某款主控对应的BSP层应保持相同当认为该层可能缺少部分功能或有错误时请联系组长确认后解决并更新整个框架**请勿自行修改提交**。 请在你修改/增加的bsp_XXX.md中提供测试用例和使用示范以及任何其他需要注意的事项并在代码必要的地方添加注释。
- 代码移植BSP层也是在不同系列、型号的stm32间执行代码移植时主要需要关注的代码层。向功能更强系列移植一般只需要重配cube而向功能较少的系列移植还需要去掉其不支持的功能。如果仅是对同一型号的开发板进行CUBEMX初始化配置的修改一般只需要给app层的应用重新分配外设和引脚或修改波特率和通信频率等。
- 子文件与文件夹:
- bsp.c/h该层用于bsp基础功能初始化的文件其中`bsp.h`被include至main.c中以实现必须的底层初始化目前需要初始化的bsp只有log和dwt**不同主频的MCU需要修改dwt初始化的参数**。**注意**有些外设如串口和CAN不需要在bsp.c中进行模块层的初始化他们会在module层生成实例即C语言中的结构体并注册到bsp层时自动进行初始化。以此达到提高运行速度避免未使用的模块被加载的问题。
- bsp_xxx.c/h每一个成对的.c/h对应一种外设当上面两个代码层需要使用某个外设时这里的文件就是对应的交互接口。
- 注册回调函数与接收:通信类外设模块有的定义了回调函数(函数指针类型)module层的模块需要自行处理接收回调函数在注册bsp的时候应传入对应参数格式的回调函数指针使得接收中断发生的时候bsp层可以自行找到对应的上层回调函数进行调用。这也是回调函数设计的初衷为底层代码调用上层代码提供接口当特定事件发生的时候完成触发自行搜索hook函数
## Module层
- 主要功能实现对设备的封装如将IMU、PC、电机等视为一个完整的功能模块让应用层app不需要关心其底层的具体实现直接使用接口。
- 文件夹
- **注意module层没有也不需要进行统一初始化**。app层的应用会包含一些模块因此由app来调用各个模块的init()或register()函数只有当一个module被app实例化这个模块才会存在。
> ~~命名为init()的初始化一般来说是开发板的独占资源即有且只有一个这样的模块无法拥有多个实例如板载陀螺仪、LED、按键等。命名为register()的模块则可以拥有多个,比如电机。~~ legacy support为了保证代码风格统一所有接口统一命名为xxxRegister()。
- algorithm:该层软件库存放位置,这些功能与硬件无关,而是提供通用的数据结构和“算子”以供该层的其他部分调用,主要是算法、控制器、底盘和位姿解算等。
- module编写和使用指南
- 初始化:
根据代码对应的函数说明传入对应的配置文件。对于某些需要集中设置的参数一般于模块的头文件中会额外设定一个xxx_config_s的结构体用于初始化的参数传递。如果不需要进行这样的集中设置则是直接传入对应的参数或module结构体中本就存在的成员变量。
- 结构体:
也就是所说的“实例”定义一个module结构体对于app层来说就是拥有某一个功能模块的实例比如一个特定的电机。在对电机进行操作的时候为实现面向对象的功能需要在接口函数中传入该结构体指针。
- 函数:
.c中存放的static函数和static变量相当于这个类的private函数.h中的则相当于public。相似的driver的public函数应较为统一。由于通信格式使用方法等的不同不同通信设备在读取操作、数据格式上可能有所不同这些不同应该在driver的内部处理。**由于C语言没有对象的概念对于通信类的module不同的实例需要在module.c中保存一份指针用于处理数据接收的解析。**
- 封装程度:
app层使用时与底层实现无关。如在使用电机时这个电机的数据该和哪些电机的数据在一个数据包中发送can的过滤器设置均属于应该自动处理的功能通信类的模块应该封装到只有初始化、发送和读取。对于电机则是用于初始化的`register`和发送控制命令`set_control`两个函数和一个实时更新的用于给app层提供该信息的数据结构体电机反馈信息
Module层主要存放的是类型定义和实例指针数组在该层没有进行实例化定义或通过malloc分配空间若在APP层没有实例化则该模块的存在与否不会影响编译后的可执行文件只会占用.c文件中的static变量和代码区的少量内存有些module只会保存每个实例对象的指针在没有初始化的时候仅仅占用一个指针数组的空间。因此基于本框架的其他工程没有必要删除APP层未使用的module文件。
务必为模块添加说明文档和使用范例,以及其他需要注意的事项(如果有)。
> **面向对象小指南:**
>
> 由于C语言没有对象的概念对于需要使用通信的module在其.c文件下都需要保存每个实例的指针在收到消息时(发生回调)遍历所有实例指针找到收到消息的实例。这种处理方式可能会导致实时性下降例如CAN接收时要遍历所有注册了CAN的实例进入module层还需要一次遍历。用C++则可以将对象的this指针和模块的回调函数进行绑定生成一个可调用对象然后再进行CAN的注册使得其不需要module层的遍历。
>
> 考虑在实例中加入一个额外的`void*`域成员成员变量其内容为module层实例的地址。这样CAN收到消息时只需要遍历所有CAN instance对于相同的模块可以在其回调函数内部获取CAN instance的`void*`指针并通过强制类型转换cast成模块的实例结构体指针类型从而访问特定的模块。
> 这实际上是保存“对象”的parent pointer使得实例可以访问拥有自己的实例访问自己的父亲。和回调函数配合就可以防止交叉包含并为底层访问上层内容提供支持。
## APP层(application)
- 功能:实现机器人的控制,对机器人**控制**结构进行抽象。
在完成BSP层和Module层后如果在APP层没有控制代码则代码并无实际功能。换言之BSP层与Module层的存在是为了APP层更简单、更合理、更易于扩展和移植。本框架的初始目标即是实现在APP层仅需思考逻辑并用无关硬件的C语言代码实现即可完成整个机器人的控制。所有需要使用的模块和算法都在Module层提供开发板外设硬件的抽象在bsp层完成。**所有使用到的模块都在APP层初始化**因此不需要module自行初始化。
- APP层按照机械设计结构如云台、发射、底盘、夹爪、抬升、机械臂建立对应的子文件夹在其中完成初始化和相关逻辑功能的编写。还有用于发布指令的云台指令应用和底盘指令应用前者应该包含一个遥控器模块和一个视觉通信模块后者包含裁判系统模块。它们包含的模块都会处理一些指令和控制信息这样还可以方便兼容双板。
- 单双板切换在application的`robot_def.h`中进行,**修改宏定义可以切换开发板的设定模式**。当设定为单板的时候,在`robot.c`中会对gimbalchassisshootrobot_cmd四个应用都进行初始化。对于双板的情况需要将上板配置为gimbal board下板配置为chassis board它们会分别初始化gimbal/shoot/robot_cmd和chassis
- 对于单板的情况所有应用之间的信息交互通过message center完成。而使用双板时需要通过板间通信传递控制信息默认遥控器接收机和pc在云台板裁判系统在底盘板因此需要互发信息。当前通过**条件编译**来控制信息的去向发往message center/接收还是通过can comm发送/接收后续考虑将双板通信纳入message center的实现中根据`robot_def.h`的开发板定义自动处理通信,降低应用层级的逻辑复杂度。
## 文件树
板级支持包的每个组件,每个moduel,以及每个app都有对应的说明文档.
```shell
ROOT:.
│ .gitignore # git版本管理忽略文件
│ .mxproject # CubeMX项目文件
│ basic_framework.ioc # CubeMX初始化配置文件
| debug_ozone.jdebug # ozone debug调试配置和缓存文件
│ LICENSE # 开源协议文件
│ Makefile # 编译管理文件,为make(mingw32-make)命令的目标
│ openocd_dap.cfg # 用于OpenOCD调试使用的配置文件,dap用
│ openocd_jlink.cfg # 用于OpenOCD调试使用的配置文件,jlink用
│ README.md # 本说明文档
│ startup_stm32f407xx.s # F407汇编启动文件
│ stm32.jflash # jlink的烧录的配置文件,一键下载用
│ STM32F407.svd # F407外设地址映射文件,用于调试
│ STM32F407IGHx_FLASH.ld # F407IGH(C板MCU)目标FLASH地址和链接规则,用于编译(作为链接阶段的链接器)
| task.ps1 # powershell脚本,一键编译并进入ozone调试/reset开发板用
│ TODO.md # 项目待完成的任务
│ VSCode+Ozone使用方法.md # 开发环境配置和前置知识介绍
│ 修改HAL配置时文件目录的更改.md # 重新配置CubeMX时的步骤和注意事项
│ 必须做&禁止做.md # 开发必看,规范和要求
| 如何定位bug.md # 开发必看,快速定位bug并进行修复.还提供了一些debug典例
|
├─.vscode
│ launch.json # 调试的配置文件
│ settings.json # 工作区配置文件,根据自己的需要配置
│ tasks.json # 任务配置文件,包括一键编译下载调试等
├─.assets # 说明文档的图片
├─application # 应用层
├─bsp # 板级支持包
├─modules # 模块层
|
├─Src #hal生成的外设初始化源文件
├─Inc #hal生成的外设初始化头文件
├─Drivers #hal driver和cmsis drivers
└─Middlewares # STusb ext , rtos , segger rtt等
```
## BSP/Module/Application介绍
在对应应用、模块和板级支持包文件夹下。每个.c文件或完整的功能模块都有说明文档。在编写新代码时注意按照规范编写说明文档。
## 整体架构
### 软件分层
![image-20221113211942850](.assets/framework.png)
### 运行任务
![image-20230413195155471](.assets/image-20230413195155471.png)
### 初始化流程
~~~mermaid
graph TD
HAL库初始化 --> BSP初始化 --> Application初始化 --> app调用其拥有模块的初始化 --> 启动操作系统
~~~
**注意,应用初始化不得放入其对应任务中,即使是在死循环前,否则可能导致一些需要定时器的任务初始化异常**。
APP会调用其所有的模块的初始化函数注册函数这是因为本框架的设计思想是任何模块在被注册构造/初始化)之前,都是不存在的,当且仅当定义了一个模块结构体(也称实例)的时候,才有一个实体的概念。
main函数唯一需要的函数是app层的`robot.c`中的`RobotInit()`函数它首先会调用BSP初始化然后进行所有应用的初始化每个应用会调用对应模块的初始化一些依赖通信外设的模块会将通信支持相关的bsp进行初始化。初始化结束之后实时系统启动。
### 程序运行流程
![运行](.assets/总程序流程.png)
### 程序数据流
![数据流](.assets/数据流.png)