修改doc存放位置,增加代码静态检查,完成了README文档,修复了.assets图片分辨率低的问题

This commit is contained in:
NeoZng
2023-07-25 16:12:38 +08:00
parent fa4a78a7a1
commit cdb1d209fc
22 changed files with 210 additions and 81 deletions

69
.Doc/Bug_Report.md Normal file
View File

@@ -0,0 +1,69 @@
# 异常报告
已知可能出现的bug将会列在此处并指明修复期限和任务执行者。
使用中遇到的bug和错误放在此处。参照下列格式
## 标题用简短的一句话描述
### 出现问题的application/module/bsp
描述你的使用方法,应该贴上图片或代码块,以及硬件连线等
### 尝试解决的方案
你的尝试,以及猜测可能的错误
### 如何复现问题
问题能否稳定复现?描述复现方法等
### 紧急程度
这里用⭐表示。最大5颗⭐。
如果不修复,会有何种其他牵连情况发生?
---
不同的问题用 --- 分隔开
你还可以使用Stepsize插件在代码出现问题可能出现问题的地方添加issues并详尽描述。或在gitee上增加issues。
当然,最快的方法是在群里提问。
## 使用LK电机并挂载在hcan2上时会出现HardFault
> 已修复此问题。修复日志请查看当前目录下的“如何定位bug.md”。
使用MF9025v2电机,并将其配置在CAN2上。经过一次LKMotorControl第二次进入时hcan->instance会在HAL_CAN_Add_Tx_Message()结束时被未知的语句修改成奇怪的值造成HardFault
### 尝试解决的方案
单步调试无果在HAL_CAN_Add_Tx_Message()返回的那一步hcan->instance会莫名其妙变成0hcan2也会被修改到一个0x8000xxx的地址上(hcan是HAL库自定的全局变量)
### 如何复现问题
使用LK电机并将其挂载在CAN2上连接电机后直接运行。在第二次进入MotorTAsk中的LKMotorControl时于检查空闲CAN邮箱时由于hcan2被修改访问CAN2外设状态时会访问野指针导致HardFault。
### 紧急程度
⭐⭐⭐⭐⭐
## 总线挂载多个电机后,pitch和yaw的GM6020电机出现编码器反馈值跳动
> 已修复详细信息见“如何定位bug.md”
CAN1总线挂载5个电机,4\*3508+1\*6020,控制报文发送频率为500Hz,电机的反馈频率皆为1kHz.云台在控制时会出现突然跳动.添加到Ozone graph查看发现ECD(编码器)值在静止状态下也会出现突然抖动,并且幅度超过4000.但不会出现超过编码器反馈值范围的值.
### 尝试解决的方案
若使用单个6020电机,不会出现此问题. 曾认为是指针越界导致`motor_measure->ecd`值被修改, 需要进一步观察其他反馈值是否出现问题. 且反馈值始终在编码器范围之内.
### 如何复现问题
同时启用CAN1和CAN2并在单条CAN总线上挂载超过5个电机.
### 紧急程度
⭐⭐⭐

166
.Doc/TODO.md Normal file
View File

@@ -0,0 +1,166 @@
# Work To be done & optimized
- **待完成**:不完成可能导致整车功能不完整
- **待优化**:对已有的功能进行性能提高/模块解耦/可维护性增强
- **待添加**:不紧急的/锦上添花的功能
**==标为黄色高亮的代表紧急程度高。==**
## assorted
- [ ] 由于我们读写和传递的数据结构都不大,基本不会发生读写时任务切换的情况。典型的数据读写时间都是~μ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_pwm
- [ ] 是否允许修改预分频计数器?
### 待添加
#### bsp_iic
- [ ] 添加10位地址的支持
#### bsp_blueteetch
- [ ] 增加蓝牙功能,方便调试和测试
#### bsp_wifi
- [ ] 增加无线局域网功能,方便调试和测试
---
## Module
### 待完成
- [ ] 为键鼠/遥控器/ps手柄/视觉上位机等各种控制器提供一套统一的接口,把发来数据转化为标准的控制数据,包括底盘速度云台角度等等
- [ ] 给每个模块增加调试的条件编译并增加bsp log的输出。或直接在运行时添加log等级输出不同的信息。
#### ==servo_motor==
舵机模块需要预先定义90/180/360连续旋转的电机类型并且能够设定max和min位置。
- [x] 编写舵机模块(待测试和优化)
#### imu
- [ ] 完善bmi088模块和算法的交互添加异步量测更新的SO3上的IEKF
#### ==master_machine==
- [ ] 增加IMU数据的时间戳
- [ ] 增加加速度计数据
- [ ] 重构seasky protocol的接口
- [ ] 增加数据未更新的处理
### 待优化
#### buzzer
> 是否需要在module层就和**daemon**模块配合?
当前实现为buzzer是单独的module若需要蜂鸣器警报的module可以自行包含buzzer.h以创建不同情况下的警报如电机离线、堵转、遥控器离线等。
#### BMI088
- [ ] 完善和SO3 IEKF的交互增加异步任务的唤醒和数据传递IMU中断唤起任务
#### remote_control
- [ ] 增加长按/短按检测 (是否有必要?)
#### message_center
- [ ] 增加队列剩余信息和数据时间戳的支持
- [ ] 提供直接传递指针的接口?
#### can_comm
- [ ] 增加can_comm数据未更新的处理
#### controller
- [ ] 将PID的初始化改写为PIDRegister的形式,在controller统一分配内存.
#### dji_motor
- [ ] 增加3508和2006的开环零位校准函数
- [ ] 为实例增加低通滤波系数变量,使不同电机有不同的配置
#### LKmotor
- [ ] 正反转标志位设置,需要修改反馈量和pid计算
#### HTmotor
- [ ] 正反转标志位设置,需要修改反馈量和pid计算
### 待添加
#### unicomm
- [ ] 完成初版构建
#### step_motor
- [ ] 增加步进电机模块
#### referee_communication
- [ ] 增加裁判系统多机通信功能
#### controller
- [ ] 增加扰动观测器,可能需要新增模块
- [ ] 增加基于模型的控制器,可能需要新增模块
#### ws2816
- [ ] 通过bsp_pwm添加支持
---
## APP
### 待完成
#### all app
- [ ] 增加调试的条件编译,使得在没有连接其他应用时也可以假装有那些应用而正常调试运行
#### ==shoot==
- [ ] 增加卡弹检测和反转
### 待优化
#### robot_cmd
- [ ] 优化消息发布和接收性能若皆为异步并在robottask中执行实际上可以只传递指针
#### gimbal
- [x] 增加底盘速度前馈控制
#### chassis
- [x] 根据电机的实际速度计算底盘的真实运动(轮式里程计)
- [ ] 若为双板根据IMU的数据对电机实际速度进行融合

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
# 利用Ozone进行model-based PID tunning
Ozone的实时变量可视化监测(示波器)功能可以很好地帮助我们观察控制器在时域的表现,典型的有上升时间、超调量和稳态时间等。
## 调试顺序
先内环,后外环。若有已知的外部扰动如阻力、重力等可以在**保持kp不变**的情况下添加积分环节,并查看达到稳态时积分的输出,该输出值可以作为**前馈**作用通过feedforward_ptr一同送入下一个串级控制器。

166
.Doc/如何定位bug.md Normal file
View File

@@ -0,0 +1,166 @@
# how to locate bug in your code
[toc]
只讨论运行中的bug指程序的运行结果不符合你的期望和异常直接异常终止。编译期出现的warning和error不在此范畴他们都可以通过直接阅读报错信息解决。
## Debug方法论
**首先**请阅读module和bsp的标准使用示例检查你的代码和示例是否有所不同。如果你正在编写一个新的模块务必使用**增量式编程**的方法构建你的模块,每完成一部分的功能就进行单元测试,看看是否符合你的期望。**千万不要一口气写完**,这时候再来测试,你就无从下手了。
如果确认软件没有问题先别急着单步调试检查一下硬件连接是否正确包括CAN的H和L串口的TX RX以及电源和GND。 如果你的硬件有多个相同的接口但是使用不同的功能,你得好好看看是否插错接口了。 软件中对hdevicehcan/hspi/htim等的设定是否和CUBEMX中配置的一致 模块的id和地址是否写错。
**修改之后要保存,编译,再调试/下载**。建议你把自动保存打开并勤劳地commit。push时注意reset这些调试时产生的commit合并成一次提交。你也可以新建一个分支用于解决bug这是git的最佳实践。
同时注意条件编译的兼容,你是否测试一些应用或模块,却使用了错误的 `#define`
完成上面的两步仍然无济于事,开始单步调试吧。在调试的同时,再仔细看看你的代码是否写错变量名和运算符,比如,把 `==`写成了 `=`,那么判断条件将永远是 `true`。在值得怀疑的地方,打开汇编视图,查看你要访问的地址是否正确。一步一步往下的同时,关注调用栈是否符合你的期望,添加必要的变量到调试窗口,看看他们何时发生意想不到的变化。**条件断点**是一个杀手级功能,你可以设置一些条件,让条件满足时程序在此处停下。下面的一些常见问题可能对你的调试有所帮助。
如果一切正常或者应该说你没有发现异常虽然他确实在那里试着查看你使用的外设寄存器值。这时候你要用到芯片的数据手册和功能描述手册。在运行的每一步中对照datasheet的寄存器状态和改变看看是否符合你期望的程序行为。寄存器分为控制寄存器/状态就寄存器/数据寄存器参照datasheet就可以明白这些寄存器是控制哪些功能描述什么状态以及内部是什么数据。
如果你的数据是连续或规则的记得使用Ozone可视化工具配合条件断点。
> 也可以让copilot帮帮你选中你认为可能出错的代码选择copilot brush-》debug
## 一些常见问题
### HardFault_Handler()
99%是由于野指针和非法内存访问导致的。在HardFault函数内添加一句 `asm("bx lr");`, 并在此处加上断点。当代码运行至此处时,选择跳出,程序会跳转回出错之前最后一句执行的语句。
查看你是否在出错前的最后一次操作中访问了非法地址或使用了已经被析构的指针。 `memcpy`的目标地址和源地址重合也可能引发硬件错误。另外如果使用指针访问了一个非对齐地址请参考__pack()相关的说明这是CMSIS架构中不允许的有些架构可以修改启动文件使其支持例如你有四个uint8类型的数据被存储在0x03-0x07的地址内这时候你通过强制类型转换以float的方式读取这四个字节就会发生非对齐访问。 虽然结构体可以通过 `__pack(1)`来压缩,编译器会对结构体变量进行处理,在读取非对齐字段时分别读取拆分的两个部分再进行合并,从而支持非对齐访问;但前述的行为却是未定义的,编译器在编译代码的时候并不知道你会以分开的方式访问这段内存,即使知道,他也无法预测栈上分配的空间是否能对齐。
常见的错误还包括使用未初始化的指针内部可能时垃圾值指向未知的地址和初始化为NULL的指针指向0x00地址`free`一个指针两次也可能导致错误。
### 通信外设传递的数据没有进行压缩
经典的 `__pack()`问题。这种问题大多出现在结构体的传输上。用于通信的结构体请在两端用 `#pragma __pack(1)``#pragma __pack()`包裹。否则传输时会出现空字节,使得数据和你使用的协议对不上号。
### Delay或定时器卡死永远不跳出
Systick和HAL_Delay以及使用TIM来定时的方法都需要通过**中断**来更新时间。如其重装载计数器上限为65535当计数达到此值时会触发中断在中断处理函数中增加一次溢出的时间。如果此时有更高优先级的中断或同优先级的中断正在运行且他们耗时很长 or 调用了这些依赖中断的Delay函数那么将会形成**死锁**,永不见天日。 如果中断被关闭,这些计时也无法更新。 这里推荐使用DWT定时器`bsp_dwt`中实现其重载计数器是64位的按照stm32f407 168MHz的运行频率需要两天多的时间才会发生溢出因此仅依赖其重载计数器计算两次tick的差值就可以实现高精度的定时除非你delay超过两天你在搞笑
### 静态变量的陷阱
注意,函数内的静态变量只会在程序启动的时候初始化一次,之后不论多少次重入,都会保存上一次修改的值。
然而,如果静态变量被放在头文件里,则每个包含该头文件的其他源文件,都会拥有一份自己的备份,这些备份之间是互不影响的(详见编译期的头文件展开)。 千万不要认为静态变量是全局的,它只是在当前文件内有效。
### 指针越界读写/内存泄漏
有时候,你发现你使用的变量值变成了奇怪的数,或者你的程序突然崩溃了,但是你并没有在代码中对这个变量进行过修改。这时候,你应该检查一下你的指针是否越界了。 你也许没有正确的将void\*指针cast成期望的类型使其访问了不该访问的位置例如一个uint8类型长度为4数组你希望将其转化为float进行读取。但你在声明数组时只分配了3个字节使用float\*访问就会触及未知的第四个字节,第四个位置上存放的可能是其他变量的值,这时候访问其他变量就会出现奇怪的值。 或者,你在 `memset()``memcpy()`的时候没有正确设置源地址和目标地址或长度。
### 实时系统
中断中使用实时系统的接口时记得使用有 `ISR`后缀的版本它们对中断的调用做了特殊处理。所有中断的优先级都是高于RTOS中任务的优先级的。
若期望某个任务以1KHz频率运行你可能会在任务循环外加一个 `OS_Delay(1)`但是你的任务执行时间要是超过了1ms系统的调度就会出现异常倘若你还在此任务内认为每次进入的时间间隔是1ms并据此编写了一些依赖周期性的代码那便是大错特错了。
对于实时性和周期性要求高的任务,使用 `vTaskDelayUntil()`,这可以获得更高的定时精度。
不同的任务/中断调用了相同的函数,或使用了共享变量/全局变量/函数内的static变量会导致读\&写冲突,也被称作**数据竞争**。这一般只会在任务繁重且中断频率高时出现。要避免这种情况,访问共享变量时应进入临界区(关闭全局中断)或使用**互斥锁**。消息队列也是一个不错的选择这些功能在FreeRTOS中都提供了支持。
### 中断
中断不要放入太复杂的逻辑和运算。使用标志位或将要处理的数据转移到队列中,于任务中检查标志位,判断是否要进行数据的处理。否则可能出现中断过于复杂/频繁使得通信出现overrun error
强烈建议初始化时不要使用和中断相关的功能,若你在中断过程中访问了一些尚未初始化的变量,就很大概率会出现前述的野指针/越界等问题。 非要使用,请添加一个标志位,并让中断判断初始化是否完成。**当前框架在初始化机器人的时候关闭了全局中断所以千万不要使用中断和与symstick/HALtick有关的延时**
### 强制类型转换
long long的范围比float小。无符号和有符号数直接转换可能变成负数。**应该在一切可能表达运算顺序错误的地方加上括号以明确操作的顺序,即使你知道不同运算符的优先级。** 使用移位操作要注意你的变量类型,请不要相信编译器的临时变量生成,务必加上类型转换。
例如:`#define MY_MACRO_NUMBER 3.0f`,使用宏定义一些带小数点的数据时记得加上.0或f后缀干脆两个都加。无符号定义要加u后缀。
### 宏
换行用 `\` ,注意同一个代码块展开后用花括号 `{}`包裹,特别注意宏展开之后是直接的文本替换!!!
用已有的宏定义宏并且进行运算时,要将后面的字段用括号包围,因为:
```c
#define YOUR_DEF SOMETHING
// 宏通过空格来解析替换字段,YOUR_DEF空格后的第一个字段当作替换文本
```
**宏只在当前文件生效**,如果宏放在.c那么对其他的文件是不可见的这也一般称作私有宏。
## 典型debug案例一
这是一个结合了软件和硬件且有多模块耦合的异常。该bug发生在调试平衡步兵的底盘过程当中。
### 引发bug的原因
1. 指针在强制类型转换中变成了错误的类型,使得指向的内存地址被错误地修改
2. CAN总线负载过大导致电机反馈消息丢失
这里是发生bug的代码片段:
```c
static void LKMotorDecode(CANInstance *_instance)
{
static LKMotor_Measure_t *measure;
static uint8_t *rx_buff;
rx_buff = _instance->rx_buff;
measure = &((LKMotorInstance *)_instance)->measure; // 通过caninstance保存的id获取对应的motorinstance
// 上面一行应为: measure = &(((LKMotorInstance *)_instance->id)->measure);
measure->last_ecd = measure->ecd;
measure->ecd = ...
// ....
}
```
这是问题1的出处。can instance中保存了父指针即拥有该instance的LKMotorInstance。这里想通过强制类型转换将 `void*`类型的 `id`转换成电机的instance指针类型并访问其measure成员变量以从CAN反馈的报文中更新量测值。然而却直接将can instance转换成motor instance。
随后更新之后的数据被覆写到can instance内部使得其成员变量改变包括hcan、txbuf、rxbuf、tx/rxlen等。hcan是HAL定义的can句柄类型里面保存了指向can状态和控制寄存器的指针以及其他HAL状态信息然而其值被电机反馈回来的值覆写之后HAL的接口访问hcan时将引起异常。
第二个问题则不是显式存在的:
```c
void MotorControlTask()
{
DJIMotorControl();
HTMotorControl();
LKMotorControl();
ServeoMotorControl();
StepMotorControl();
}
```
这是motortask的内容此任务将以500hz的频率运行。在发生bug时我们将4个HT04电机和2个LK MF9025电机全部连接到CAN1上。注意HT04不支持多电机指令因此占用的带宽较大。在 `LKMotorControl()`完成参考值计算和CAN发送之后立刻会调用 `HTMotorControl()`后者需要连续发送4条报文。而HT和LK电机都会在接收到控制指令之后发送反馈信息报文。由于HT电机的控制在LK电机控制之后立刻执行导致总线被占据LK电机发送的反馈数据仲裁失败无法获得总线占有权使得主机收不到反馈数据。
### bug的发现和定位的尝试
程序的大体情况如下当时进行轮足式倒立摆机器人的测试启用了balance.c在其中注册了4个HT04电机can1和2个LK9025电机can2。控制报文的发送频率均为500Hz。
测试时发现9025电机可以接收到mcu发送的控制指令并响应但是mcu始终无法获得反馈值`LKMotorInstance->measure`的所有成员变量一直是零。由于CAN是总线架构电机能接收到数据说明通信正常。HT04电机也可以正常控制并收到反馈信息。在 `LKMotorDecode()`函数中添加断点发现能够成功进入1~2次随后便引发HardFault。
此时内心有些动摇开始检查硬件连线。我们尝试把LK电机也挂载到CAN1总线上。开始单步调试发现LK电机可以正常接收一次反馈报文之后就进入 `Hardfault_handler()`。HT和DJI电机均无此问题。进一步进行每条指令的调试发现在成功接收到一次报文之后接收报文指的是can发生中断并在处理函数中调用LK电机的解码函数我们并没有查看measure值是否刷新实际上这时候反馈值仍然为零进入该电机的控制报文发送时通过在 `Hardfault_handler()`中添加汇编语句 `asm("bx lr")`,即跳转到最后一次执行的指令,发现访问 `hcan->state`会引起硬件错误。遇到这种情况说明发生了越界访问或使用了野指针。检查hcan的值发现是一个非常大的地址。因此怀疑hcan指针被其他的内存访问语句修改。
有了方向之后进一步对每一个函数都进行单步进入调试同时时刻监测hcan1的值。然而这时候出现即使一开始就单步调试也无法进入LK电机解码函数的问题。于是怀疑是CAN过滤器的配置问题使得LK电机反馈报文被过滤检查LK的接收id无误后认为可能由于LK电机的发送和接收ID都比较大0x140和0x280CAN标准ID放不下。但是查阅CAN specification后发现standar ID可以容纳11位的值应该不会有问题。于是把过滤器配置为mask模式让bxCAN控制器接收所有报文即不进行过滤。然而还是不奏效仍然无法收到数据。
这时候想起HT电机是不支持多电机控制指令的因此500Hz的控制频率似乎有些过高相当于2ms内要完成2x4+1+2=11次CAN报文的发送。计算1M波特率下最大通信速率果然超出了负载。于是降低 `MotorTask()`的频率为200Hz果然能重新接收到数据了。
继续单步调试,终于发现在 `LKMotorDecode()`通过强制类型转换获取LKMotorInstance的时候用错了变量使得反馈值被写入电机的 `CANInstance`导致hcan指向随机的地址最终造成访问时引发hardfault。
修改之后将LK电机挂载到CAN2上控制频率回到500Hz程序正常运行。
### 解决方案
均衡总线负载,调节任务运行时间。
# 典型debug案例二
这仍然是一个CAN总线引发的bug。使用的电机均为DJI电机。当多个电机接入时会产生反馈值跳变的情况。起初认为**总线负载过高**控制频率为500Hz反馈频率均为1kHz计算之后得出CAN的负载率接近90%),但将电机减少为一半甚至更少时仍然出现此问题。**单独使用CAN1且仅挂载一个电机则问题消失**同时使用CAN1和CAN2不论单个总线挂载几个电机则问题再次出现。
**单步调试发现反馈值并未因指针越界而被纂改**。仔细检查代码的计算发现并未出错打开Ozone查看反馈值曲线发现确实偶发跳变但跳变值并未超出反馈值范围即即使发生跳变值仍然在**正常范围内**,因此不像是总线负载过大导致数据帧错误或指针越界修改的随机值。加入多个电机同时查看反馈值,**发现反馈跳变之后会和另一电机的反馈值相同**呈现“你跳到我我跳到你”的图景。怀疑CAN中断被**重入**即一个中断未完成时另一个CAN报文到来打断了当前的中断并执行了**相同的反馈解码函数**。但CAN1和CAN2的中断优先级均为5因此不可能打断彼此。打开CubeMX查看初始化配置发现两个CAN的FIFO0和FIFO1中断优先级不同分别是5和6。则FIFO1的溢出中断会被FIFO0打断且我们在电机的解码函数中使用了一些**静态变量**用于存储触发接收中断的电机报文的相关信息,故而新进入的中断覆写了之前中断的静态变量值,使得之前中断在恢复之后存储了前者的值,导致自身反馈错误。
将优先级统一设为5编译之后重新运行反馈值正常。
> “同时使用CAN1和CAN2不论几个电机则问题再次出现。” 导致此问题的原因是初始化CAN时按照rxid分配FIFO因此注册的电机会被交替分配到不同的FIFO故不论注册了几个电机只要多于2、注册到哪条总线都会出现FIFO1中断被FIFO0打断的情况。

View File

@@ -0,0 +1,27 @@
# MUST & MUSTNOTMUSTNOT
## 禁止过度摸鱼
提供工作效率!
## 禁止在临界区使用延时,这会导致因中断关闭使得定时器无法进入中断更新时间,进而卡死系统
除非你使用的是基于计数寄存器差值的延时方法,或阻塞式的for延时。
**若有必要,应该使用`bsp_dwt.h`提供的接口。
## 若任务耗时较长导致可能出现数据读写被中断或更高优先级的任务打断,请为你的数据添加锁
若同时要求数据的实时性考虑将低优先级线程设置为由高优先级任务唤醒或添加位互斥锁而不是osMutex确保中断也可以使用
## 禁止图方便直接将电机/电调连接在开发板的xt30接口上否则电机的反电动势可能烧毁开发板
后续考虑增加一个xt30转接器其上实现隔离电路再连接开发板充当分电板。
## 请给你编写的bsp和module提供详细的文档和使用示例并为接口增加安全检查
用于调试的条件编译和若有可能log输出也是必须的。
另外“treat your user as idot
## NO WARNING
makefile中已经启用了`-Werror`选项所有的warning都会被视为error别妄图带着warning通过编译

View File

@@ -0,0 +1,256 @@
# 2023 EC basic-framework
> 每个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`。编写完新的代码后,使用`右键-格式化``shift+alt+f`(请勿对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文件或完整的功能模块都有说明文档。在编写新代码时注意按照规范编写说明文档。
## 整体架构
### 软件分层
<img src="../.assets/image-20230725153133419.png" style="zoom:67%;" />
### 运行任务
<img src="../.assets/image-20230725152433502.png" style="zoom: 50%;" />
### 初始化流程
~~~mermaid
graph TD
HAL库初始化 --> BSP初始化 --> Application初始化 --> app调用其拥有模块的初始化 --> 启动操作系统
~~~
**注意,应用初始化不得放入其对应任务中,即使是在死循环前,否则可能导致一些需要定时器的任务初始化异常**。
APP会调用其所有的模块的初始化函数注册函数这是因为本框架的设计思想是任何模块在被注册构造/初始化)之前,都是不存在的,当且仅当定义了一个模块结构体(也称实例)的时候,才有一个实体的概念。
main函数唯一需要的函数是app层的`robot.c`中的`RobotInit()`函数它首先会调用BSP初始化然后进行所有应用的初始化每个应用会调用对应模块的初始化一些依赖通信外设的模块会将通信支持相关的bsp进行初始化。初始化结束之后实时系统启动。
### 程序运行流程
![](../.assets/image-20230725153635454.png)
### 程序数据流
![](../.assets/dataflow.svg)

View File

@@ -0,0 +1,93 @@
# 让VSCode变成你熟悉的形状
VSCode的各种配置如快捷键、高亮颜色、主题、界面形状和位置等也包括各种插件的设置可以通过在`ctrl+,`打开的设置页面修改更好的方式是你已经尝试过的——通过xxx.json文件进行配置。VSCode的配置系统同样通过`.json`文件完成且存在继承和覆盖的关系。VSCode的底层配置`setting.json`是针对整个VSCode进行设置的而工作区目录下创建的`.vscode/setting.json`则可以覆盖底层配置。了解了基本的配置结构后,这里将介绍一些入手必备的设置和推荐安装的插件。
[TOC]
## 快捷键
## 提高效率
## 代码高亮
## 终端工具
## 插件
> 学习一个插件的使用最好的方式是阅读插件的wiki和说明文档而不是在搜索引擎里面搜索询问ChatGPT或者Copilot Chat也是一个比较好的办法初级的问题它们几乎不会犯错。
- **Better C++ Syntax**
用于静态解析C++代码为intellisense以及language server提供代码高亮和完整的智能提示选项。
- **Blockman**
为代码分块。不同的作用域会被浅色背景边框包围同时高亮当前focus的代码块方便在大段代码中定位程序控制流。
- **Bookmarks**
为代码添加书签可以在左侧tab页中跳转到对应位置方便阅读代码时往复查看也有助于阅读理解。右键点击代码行号左侧即打断点的地方可以添加书签或带label的书签。
- **C/C++ Snippets**
为基本的语句(关键字)提供代码补全,如输入`for`自动生成下面的代码:
```c
for (size_t i = 0; i < count; i++)
{
/* code */
}
```
补全之后按下tab会进入不同的位置方便进一步修改snippet。你也可以在VSCode中自定义常用的snippet补全。
- **Code Issue Manager**
可以在代码的任意地方插入“便签”和“评注”支持多人协作。是一个比注释更好的TODO list和注意事项提醒。插入的issue支持markdown格式。
- **Doxygen Documention Generator**
为你的代码生成doxygen文档格式的注释。同时也支持一些基本的注释块生成。输入`/**`再按下回车会根据当前注释的位置自动生成合适的注释块。本框架中的注释均通过该插件生成。
- **Github Copilot / Copilot Labs / Copilot Chat**
体验大模型的威力。尤其是Copilot Chat非常适合为你提供一门语言的入门级咨询。
- **Gitlens**
方便地通过图形化的方式管理你的git仓库。
- **HexEditor & Hex Hover Converter**
以十六进制编辑文件 & 鼠标悬停在任意数值类型上时自动提供2/8/16进制的转换。
- **Live Share**
和你的伙伴一起coding。
- **SonarLint**
VSCode上最强大的静态检查工具在VSCode自动静态检查的基础上提供更严格的代码建议尽可能降低出错的可能。其中也包含了不同语言的最佳实践。
cmake可以通过添加`DCMAKE_EXPORT_COMPILE_COMMANDS=True` 的指令直接在cmakelists中通过`set()`设定也可以makefile则通过Makefile Tools插件设置生成`compile_commands.json`的路径。