更新了大量文档

This commit is contained in:
NeoZng 2023-06-03 21:58:21 +08:00
parent ff5028036a
commit 253f391cd5
17 changed files with 141 additions and 74 deletions

View File

@ -27,9 +27,9 @@
本框架主要代码分为**BSP、Module、APP**三层。三层的代码分别存放在同名的三个文件夹中这三个文件夹存放在根目录下。开发过程中主要编写APP层代码Module层与BSP层不建议修改。如需添加module如oled屏幕、其他传感器和外设等请按照规范编写并联系组长提交commit到dev分支或对应的功能名分支完善后合并至主分支。在配置git的时候将自己的`user.name`配置成英文缩写或易懂的nick name。
BSP层构建于ST的HAL硬件抽象层之上。为了方便使用已经将CMSIS相关、HAL库和实时系统、DSP支持等文件都放在`HAL_N_Middlewares`文件夹下包括Cube生成的外设初始化的Inc和Src文件夹
BSP层构建于ST的HAL硬件抽象层之上针对RoboMaster竞赛所用电控外设和模块的特点对其进行了进一步封装Module层是基于bsp的封装打造的各种模块旨在为app层提供**硬件无关的接口**,即应用层不应该出线任何与片上外设相关的代码
**main.c的位置在**`HAL_N_Middlewares/Src/main.c`
**main.c的位置在**`Src/main.c`
- **代码格式**
@ -119,23 +119,21 @@
**编写和使用指南**
- 补充与修改某款主控对应的BSP层应保持相同当认为该层可能缺少部分功能或有错误时请联系组长确认后解决并更新整个框架**请勿自行修改提交**。 请在你修改/增加的bsp_XXX.md中提供测试用例和使用示范以及任何其他需要注意的事项并在代码必要的地方添加注释。
- 代码移植BSP层也是在不同系列、型号的stm32间执行代码移植时主要需要关注的代码层。向功能更强系列移植一般只需要重配cube而向功能较少的系列移植还需要去掉其不支持的功能。如果仅是对同一型号的开发板进行HAL初始化配置的修改一般只需要给app层的应用重新分配外设和引脚或修改波特率和通信频率等。
- 代码移植BSP层也是在不同系列、型号的stm32间执行代码移植时主要需要关注的代码层。向功能更强系列移植一般只需要重配cube而向功能较少的系列移植还需要去掉其不支持的功能。如果仅是对同一型号的开发板进行CUBEMX初始化配置的修改一般只需要给app层的应用重新分配外设和引脚或修改波特率和通信频率等。
- 子文件与文件夹:
- bsp.c/h该层用于bsp基础功能初始化的文件其中.h被include至main.c中以实现整个代码层的初始化。include了该层所有模块的.h并调用各模块的初始化函数。目前需要初始化的bsp只有log和dwt**不同主频的MCU需要修改dwt初始化的参数**。**注意**有些外设如串口和CAN不需要在bsp.c中进行模块层的初始化他们会在module层生成实例即C语言中的结构体并注册到bsp层时自动进行初始化。以此达到提高运行速度避免未使用的模块被加载的问题。
- 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、电机等视为一个完整的功能模块让应用层不需要关心其底层的具体实现直接使用接口。
- 主要功能实现对设备的封装如将IMU、PC、电机等视为一个完整的功能模块让应用层app不需要关心其底层的具体实现,直接使用接口。
- 文件夹
- **注意module层没有也不需要进行统一初始化**。app层的应用会包含一些模块因此由app来调用各个模块的init或register函数只有当一个module被app实例化这个模块才会存在。
- **注意module层没有也不需要进行统一初始化**。app层的应用会包含一些模块因此由app来调用各个模块的init()或register()函数只有当一个module被app实例化这个模块才会存在。
> 命名为init()的初始化一般来说是开发板的独占资源即有且只有一个这样的模块无法拥有多个实例如板载陀螺仪、LED、按键等。命名为register()的模块则可以拥有多个,比如电机。
- monitor文件夹:实现看门狗功能。提供回调函数和count可选TODO
> ~~命名为init()的初始化一般来说是开发板的独占资源即有且只有一个这样的模块无法拥有多个实例如板载陀螺仪、LED、按键等。命名为register()的模块则可以拥有多个,比如电机。~~ legacy support为了保证代码风格统一所有接口统一命名为xxxRegister()。
- algorithm:该层软件库存放位置,这些功能与硬件无关,而是提供通用的数据结构和“算子”以供该层的其他部分调用,主要是算法、控制器、底盘和位姿解算等。
@ -147,7 +145,7 @@
- 结构体:
也就是所说的“实例”定义一个module结构体对于app层来说就是拥有某一个功能模块的实例比如一个特定的电机。在对电机进行操作的时候传入该结构体指针。
也就是所说的“实例”定义一个module结构体对于app层来说就是拥有某一个功能模块的实例比如一个特定的电机。在对电机进行操作的时候为实现面向对象的功能,需要在接口函数中传入该结构体指针。
- 函数:
@ -155,9 +153,9 @@
- 封装程度:
应尽可能使到上层使用时不考虑下层所需的操作。如在使用电机时这个电机的数据该和哪些电机的数据在一个数据包中发送can的过滤器设置均属于应该自动处理的功能接收类的driver应该封装到只有初始化用于初始化的`register`和发送控制命令`set_control`两个函数和一个实时更新的用于给app层提供该信息的数据结构体
app层使用时与底层实现无关。如在使用电机时这个电机的数据该和哪些电机的数据在一个数据包中发送can的过滤器设置均属于应该自动处理的功能通信类的模块应该封装到只有初始化、发送和读取。对于电机,则是用于初始化的`register`和发送控制命令`set_control`两个函数和一个实时更新的用于给app层提供该信息的数据结构体(电机反馈信息)。
Module层主要存放的是类型定义和实例指针数组在该层没有进行实例化定义或通过malloc分配空间若在APP层没有实例化则该模块的存在与否不会影响编译后的可执行文件只会占用初始化和代码区所需的少量内存。module只会保存每个实例对象的指针在没有初始化的时候仅仅占用一个指针数组的空间。因此基于本框架的其他工程没有必要删除APP层未使用的module文件。
Module层主要存放的是类型定义和实例指针数组在该层没有进行实例化定义或通过malloc分配空间若在APP层没有实例化则该模块的存在与否不会影响编译后的可执行文件只会占用.c文件中的static变量和代码区的少量内存有些module只会保存每个实例对象的指针在没有初始化的时候仅仅占用一个指针数组的空间。因此基于本框架的其他工程没有必要删除APP层未使用的module文件。
务必为模块添加说明文档和使用范例,以及其他需要注意的事项(如果有)。

View File

@ -160,7 +160,7 @@ void StartDefaultTask(void const *argument)
/* init code for USB_DEVICE */
MX_USB_DEVICE_Init();
/* USER CODE BEGIN StartDefaultTask */
vTaskDelete(NULL);
vTaskDelete(NULL); // 删除默认任务,防止占用CPU
/* USER CODE END StartDefaultTask */
}
@ -172,15 +172,15 @@ void StartINSTASK(void const *argument)
{
// 1kHz
INS_Task();
VisionSend(); // 解算完成后发送视觉数<EFBFBD>?
VisionSend(); // 解算完成后发送视觉数据,但是当前的实现不太优雅,后续若添加硬件触发需要重新考虑结构的组织
osDelay(1);
}
}
void StartMOTORTASK(void const *argument)
{
// 若使用HT电机则取消本行注释
HTMotorControlInit();
// 若使用HT电机则取消本行注释,该接口会为注册了的电机设备创建线程
// HTMotorControlInit();
while (1)
{
// 500Hz
@ -203,7 +203,7 @@ void StartROBOTTASK(void const *argument)
{
while (1)
{
// 200Hz
// 200Hz-500Hz,若有额外的控制任务如平衡步兵可能需要提升至1kHz
RobotTask();
osDelay(5);
}
@ -214,7 +214,8 @@ void StartUITASK(void const *argument)
My_UI_init();
while (1)
{
Referee_Interactive_task();
Referee_Interactive_task(); // 每次给裁判系统发送完一包数据后,挂起一次,防止卡在裁判系统发送中,详见Referee_Interactive_task函数的refereeSend();
osDelay(1); // 即使没有任何UI需要刷新,也挂起一次,防止卡在UITask中无法切换
}
}
/* USER CODE END Application */

View File

@ -6,7 +6,7 @@
**应用之间不应该有任何包含关系,它们必须是平行工作的。**而这通过pub-sub的机制实现。module层提供了`message_center`模块,支持发布订阅者的消息订阅机制。以传统的框架为例,负责整车控制的应用和其他应用(或任务)是从属的树状结构,或不同的任务和应用之间通过全局变量传递消息(**请不要使用全局变量!**),而此框架下的不同应用是并行的关系。
如果一个应用希望获取另一个应用的数据,那么他应该**订阅**由此此应用发布的话题(事件)。一个应用要把自己希望共享的数据,注册到消息中心,即**发布**。为了区别不同的消息来源(你希望订阅谁的消息?哪一个消息?),可以通过**话题名**进行订阅。也就是说,消息中心作为第三方,管理所有的消息发布者和订阅者,它像报刊亭一样对消息进行中转,使得不同的应用之间不需要包含彼此,更不用全局变量也能共享消息。
如果一个应用希望获取另一个应用的数据,那么他应该**订阅**由此此应用发布的话题。一个应用要把自己希望共享的数据,注册到消息中心,即**发布**。为了区别不同的消息来源(你希望订阅谁的消息?哪一个消息?),可以通过**话题名**进行订阅。也就是说,消息中心作为第三方,管理所有的消息发布者和订阅者,它像报刊亭一样对消息进行中转,使得不同的应用之间不需要包含彼此,更不用全局变量也能共享消息。
> 更多关于发布-订阅的实现,请参考`modules/message_center`下的文档。
@ -15,7 +15,7 @@
这是机器人的参数配置文件,必须要针对每个机器人进行修改。包括机器人的尺寸参数和性能参数等。你还需要在这里设定软硬件配置:云台板/底盘板/单板等。这里定义的宏会作为条件编译的决断。
app层共用的状态变量和结构体等也应该定义在这里例如用于应用之间通信的数据。记得通信变量要用:
app层共用的状态变量和结构体等也应该定义在这里例如用于应用之间通信的数据。记得用于通信变量要用:
```c
#pragma pack(1)
typedef struct
@ -25,7 +25,9 @@ typedef struct
} your_struct;
#pragma pack()
```
包裹起来取消字节对齐以防止出现访问8byte地址而出现错误。
包裹起来取消字节对齐以防止出现访问8-bit地址而出现错误。
如果你需要其他的通信数据类型或修改模块间通信数据的格式,直接在此处更改即可。
## robot_cmd
@ -35,6 +37,8 @@ typedef struct
robot_cmd工作起来就像一个遥控数据的兼容层不论数据的来源是视觉上位机/遥控器/键鼠/图传通信链路/ps手柄最后都会被转化成真实参考输入提供给其他的app。它的任务是将其他来源的数据映射到控制输入上。
## gimbal
以步兵为例云台应用应当包含两个电机分别用于驱动yaw和pitch轴除非你是一个三轴的云台还有一个imu开发板一般放在云台上。gimbal模块会接收robot_cmd发来的控制信息云台的角度、转速等并通过电机提供的接口完成电机的参考值设定。gimbal还要把imu的数据反馈给cmd用于和视觉的通信以及云台状态的判断。
@ -63,3 +67,4 @@ robot_cmd工作起来就像一个遥控数据的兼容层不论数据的来
此框架对单开发板/双开发板/多开发板的情况都提供了支持多板一般只在工程机器人上出现需要自己在robot_cmd和robot_def增加相应的条件编译选项robot.c中也不要忘记增加初始化和任务运行函数目前通过条件编译实现了对单双板的切换。使用双板时主控板在云台上连接遥控器和上位机副板在底盘上负责底盘的运动控制和与裁判系统的通信。
当然,你可以为每台不同的机器人进行特化,因为本框架是针对步兵/英雄定制的。

View File

@ -10,7 +10,7 @@
在main函数中包含`robot.h`头文件,这是对整车的抽象。将`INStask``motortask``ledtask``monitortask`这四个task加入`freertos.c`中,创建对应的任务,设置合适的任务运行间隔;然后将`robottask`放入freertos.c中同样以一定的频率运行。 在初始化实时系统之前,在`main()`中调用`RobotInit()`进行整车的初始化。
**关于运行的任务**INStask的运行频率必须为1kHzmotortask推荐的运行频率为200Hz\~500Hz详情见module/motor/motor_task.c在MotorTask内部对于高实时性要求的电机可以提升到1kHz不过要注意CAN总线的负载。ledtask的运行频率推荐为1kHzmonitortask的运行频率为1kHzrobottask的运行频率推荐为150Hz以上应当高于视觉发送的频率若后续使用插帧同样应该保证不低过motortask太多。
**关于运行的任务**INStask的运行频率必须为1kHzmotortask推荐的运行频率为200Hz\~1000Hz详情见module/motor/motor_task.c在MotorTask内部对于高实时性要求的电机可以提升到1kHz不过要注意CAN总线的负载。monitortask的运行频率为100Hzrobottask的运行频率推荐为150Hz以上应当高于视觉发送的频率若后续使用插帧同样应该保证不低过motortask太多。
若使用双板,则在`robot_def.h`中给对应的开发板设定宏定义,如底盘板使用`#define CHASSIS_BOARD`,云台板使用`#define GIMBAL_BOARD`;单个开发板控制整车,则定义`#define ONE_BOARD`。在每个应用中都已经使用编译预处理指令完成条件编译会自动根据设定的宏切换功能。使用双板的时候目前板间通信通过CAN完成因此两个开发板会挂载在一条总线上在两个开发板对这条总线的其他使用CAN的设备进行配置时注意**不要发生ID冲突**,还要注意**防止负载过大**。
@ -24,7 +24,7 @@ Robot.c是整个机器人的抽象其下有4个应用robot_cmdgimbal
为了进一步解耦应用之间的关系,这里并没有采用层级结构(或设计模式中所谓的**工厂模式**即robot_cmd包含其他三个模块而采用了应用并列的**发布-订阅**机制四个应用之间没有任何相互包含关系他们之间的通信通过module层提供的`message_center`实现。每个应用会通过该模块向一些话题事件发布一些消息同时从一些话题订阅消息。如robot_cmd应用会发布其他三个模块的控制信息同时订阅其他三个模块的反馈信息。其他三个模块会订阅robot_cmd发布的控制信息同时发布反馈给robot_cmd的信息他们不需要知道彼此的存在只是从`message_center`处获取其他应用发布的消息或向自己发布的话题推送消息。
application在初始化module的时候初始化参数会包含部分bsp的内容但仅仅是外设和引脚的选择以及id设置用于通信的外设需要id设置。实际上当前框架的app层和cubemx初始化部分耦合在配置的时候就必须确定每个外设的作用和归属权一旦cubemx完成设置app层必须按照对应参数设置引脚和并分配module的外设.后续考虑将cubemx和bsp耦合去除顶层代码和底层的关系
application在初始化module的时候初始化参数会包含部分bsp的内容但仅仅是外设和引脚的选择以及id设置用于通信的外设需要id设置。实际上当前框架的app层和cubemx初始化部分耦合在配置的时候就必须确定每个外设的作用和归属权一旦cubemx完成设置app层必须按照对应参数设置引脚和并分配module的外设后续考虑将cubemx和bsp耦合去除顶层代码和底层的关系

View File

@ -2,7 +2,9 @@
<p align='right'>neozng1@hnu.edu.cn</p>
# 请注意使用CAN设备的时候务必保证总线只接入了2个终端电阻开发板一般都有一个6020电机和HT、LK电机也都有终端电阻注意把多于2个的全部断开通过拨码
# 请注意使用CAN设备的时候务必保证总线只接入了2个终端电阻开发板一般都有一个6020电机、c620/c610电调、LK电机也都有终端电阻注意把多于2个的全部断开通过拨码
## 使用说明
@ -23,16 +25,18 @@
/* can instance typedef, every module registered to CAN should have this variable */
typedef struct _
{
CAN_HandleTypeDef* can_handle;
CAN_TxHeaderTypeDef txconf;
uint32_t tx_id;
uint32_t tx_mailbox;
uint8_t tx_buff[8];
uint8_t rx_buff[8];
uint32_t rx_id;
void (*can_module_callback)(struct _*);
void* id;
} can_instance;
CAN_HandleTypeDef *can_handle; // can句柄
CAN_TxHeaderTypeDef txconf; // CAN报文发送配置
uint32_t tx_id; // 发送id
uint32_t tx_mailbox; // CAN消息填入的邮箱号
uint8_t tx_buff[8]; // 发送缓存,发送消息长度可以通过CANSetDLC()设定,最大为8
uint8_t rx_buff[8]; // 接收缓存,最大消息长度为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
void *id; // 使用can外设的模块指针(即id指向的模块拥有此can实例,是父子关系)
} CANInstance;
typedef struct
{
@ -62,6 +66,7 @@ typedef void (*can_callback)(can_instance*);
```c
void CANRegister(can_instance* instance, can_instance_config config);
void CANSetDLC(CANInstance *_instance, uint8_t length); // 设置发送帧的数据长度
uint8_t CANTransmit(can_instance* _instance, uint8_t timeout);
```

View File

@ -1,3 +1,30 @@
# bsp_dwt
DWT是stm32内部的一个"隐藏资源",他的用途是给下载器提供准确的定时,从而为调试信息加上时间戳.并在固定的时间间隔将调试数据发送到你的xxlink上.
DWT是stm32内部的一个"隐藏资源",他的用途是给下载器提供准确的定时,从而为调试信息加上时间戳.并在固定的时间间隔将调试数据发送到你的xxlink上.
## 常用功能
### 计算两次进入同一个函数的时间间隔
```c
static uint32_t cnt;
float deltaT;
deltaT=DWT_GetDeltaT(&cnt);
```
### 计算执行某部分代码的耗时
```c
float start,end;
start=DWT_DetTimeline_ms();
// some proc to go...
for(uint8_t i=0;i<10;i++)
foo();
end = DWT_DetTimeline_ms()-start;
```

View File

@ -11,6 +11,28 @@
bsp_log是基于segger RTT实现的日志打印模块。
推荐使用`bsp_log.h`中提供了三级日志:
```c
#define LOGINFO(format,...)
#define LOGWARNING(format,...)
#define LOGERROR(format,...)
```
分别用于输出不同等级的日志。
**若想启用RTT必须通过`launch.json`的`debug-jlink`启动调试(不论使用什么调试器)。**
注意若你使用的是cmsis-dap和daplink**请在调试任务启动之后再打开`log`任务。**(均在项目文件夹下的.vsocde/task.json中有注释自行查看
在ozone中查看log输出直接打开console调试任务台和terminal调试中断便可看到调试输出。
> 由于ozone版本的原因可能出现日志不换行或没有颜色。
## 自定义输出
你也可以自定义输出格式详见Segger RTT的文档。
```c
int printf_log(const char *fmt, ...);
void Float2Str(char *str, float va);
@ -33,3 +55,6 @@ printf_log("Motor %d met some problem, error code %d!\n",3,1);
```
或直接通过`%f`格式符直接使用`printf_log()`发送日志,可以设置小数点位数以降低带宽开销。

View File

@ -1,3 +1,5 @@
# bsp usb
简单写点,有待优化.
简单写点,有待优化. 目前仅支持虚拟串口通信,暂未开发其他内容.
注意为了增加发送完成和接收完成回调对Inc/usbd_xxxx.h四个文件做了修改对Src/usbxxx.c也进行了修改。

View File

@ -0,0 +1,3 @@
# tfminiplus
北醒激光单点激光雷达模块的简单实现。目前使用iic阻塞通信耗时和速度都太慢需修改为中断读取bsp_iic已经提供相应的接口。

View File

@ -25,6 +25,7 @@ CAN comm是用于CAN多机通信的模块。你不需要关心实现的协议
CANCommInstance *CANCommInit(CANComm_Init_Config_s* comm_config);
void CANCommSend(CANCommInstance *instance, uint8_t *data);
void *CANCommGet(CANCommInstance *instance);
uint8_t CANCommIsOnline(CANCommInstance *instance);
```
第一个函数将会初始化一个CANComm实例返回其指针。使用CANComm进行通信的应用应该保存返回的指针。初始化需要传入一个初始化结构体。请在应用初始化的时候调用该函数。推荐的结构体配置方式如下
@ -139,4 +140,3 @@ CAN comm的通信协议如下
流程图如下:![未命名文件](../../assets/CANcomm.png)

View File

@ -42,6 +42,7 @@ void DaemonTask()
else if (dins->callback) // 等于零说明超时了,调用回调函数(如果有的话)
{
dins->callback(dins->owner_id); // module内可以将owner_id强制类型转换成自身类型从而调用特定module的offline callback
// @todo 为蜂鸣器/led等增加离线报警的功能,非常关键!
}
}
}

View File

@ -4,4 +4,9 @@
注意LK电机在使用多电机发送的时候只支持一条总线上至多4个电机多电机模式下LK仅支持发送id 0x280为接收ID为0x140+id.
要设置为多电机模式请通过串口连接电机并使用该文件夹下的LK motor tool.exe进行配置。
要设置为多电机模式请通过串口连接电机并使用该文件夹下的LK motor tool.exe进行配置。
## LK的其他电机
若使用其他LK电机唯一需要修改的是确定编码器的精度即LKMotorDecode()部分的速度反馈和编码器反馈解析。

View File

@ -4,10 +4,8 @@
请使用字库软件制作自己的图标和不同大小的ascii码.
> 后续尝试移植一些图形库使得功能更加丰富
> oled主要作调试和log/错误显示等使用
> 可以提供给视觉和机械的同学调试接口,方便他们通过显示屏进行简单的设置
*可以引入RoboMaster oled,或额外增加一个编码器用于控制oled界面并设定一些功能.*

View File

@ -1,6 +1,6 @@
# 标准命令
这是一个体力活也是一个艺术品。请把不同的控制命令module进行封装以转化成标准的消息类型包括云台角度速度底盘速度发射频率是否发射等信息供RobotCMD应用或其他应用使用。
这是一个体力活也是一个艺术品。请把不同的控制命令module进行封装以转化成标准的消息类型包括云台角度速度底盘速度发射频率是否发射等信息供RobotCMD应用或其他应用使用。通过这种方式开发者可以更专注于cmd命令的编写而不需要为每台机器人/不同的控制器编写命令转换。
是否将下面的模块都放到standard_cmd文件夹下似乎没有必要。
@ -13,5 +13,4 @@
## key and mouse
## 图传链路
这似乎和键鼠是同一套,不过走的是串口,可能需要额外添加支持模块
图传链路的数据解析似乎和键鼠是同一套协议,不过走的是串口,可能需要额外添加支持模块

View File

@ -1,7 +1,9 @@
# univsersal communication
@todo
unicomm旨在为通信提供一套标准的协议接口屏蔽底层的硬件差异使得上层应用可以定制通信协议包括包长度/可变帧长/帧头尾/校验方式等。
不论底层具体使用的是什么硬件接口,硬件的每一帧传输完将数据放在缓冲区里之后,就没有任何区别了。 此模块实际上就是对缓冲区的rawdata进行操作包括查找帧头计算包长度校验错误等。
不论底层具体使用的是什么硬件接口,实际上每一帧传输完并把数据放在缓冲区之后,就没有任何区别了。 此模块实际上就是对缓冲区的rawdata进行操作包括查找帧头计算包长度校验错误等。
完成之后可以将module/can_comm移除把原使用了cancomm的应用迁移到此模块。
完成之后可以将module/can_comm、视觉的通信协议seasky_protocol和master_process等移除把原使用了cancomm的应用迁移到此模块。

3
modules/vofa/vofa.md Normal file
View File

@ -0,0 +1,3 @@
# vofa
**除非迫不得已否则强烈不推荐使用vofa进行调试。应通过bsp_log输出日志或使用ozone可视化。**

View File

@ -1,6 +1,6 @@
# how to locate bug in your code
[TOC]
[toc]
只讨论运行中的bug指程序的运行结果不符合你的期望和异常直接异常终止。编译期出现的warning和error不在此范畴他们都可以通过直接阅读报错信息解决。
@ -12,9 +12,9 @@
**修改之后要保存,编译,再调试/下载**。建议你把自动保存打开并勤劳地commit。push时注意reset这些调试时产生的commit合并成一次提交。你也可以新建一个分支用于解决bug这是git的最佳实践。
同时注意条件编译的兼容,你是否测试一些应用或模块,却使用了错误的`#define`
同时注意条件编译的兼容,你是否测试一些应用或模块,却使用了错误的 `#define`
完成上面的两步仍然无济于事,开始单步调试吧。在调试的同时,再仔细看看你的代码是否写错变量名和运算符,比如,把`==`写成了`=`,那么判断条件将永远是`true`。在值得怀疑的地方,打开汇编视图,查看你要访问的地址是否正确。一步一步往下的同时,关注调用栈是否符合你的期望,添加必要的变量到调试窗口,看看他们何时发生意想不到的变化。**条件断点**是一个杀手级功能,你可以设置一些条件,让条件满足时程序在此处停下。下面的一些常见问题可能对你的调试有所帮助。
完成上面的两步仍然无济于事,开始单步调试吧。在调试的同时,再仔细看看你的代码是否写错变量名和运算符,比如,把 `==`写成了 `=`,那么判断条件将永远是 `true`。在值得怀疑的地方,打开汇编视图,查看你要访问的地址是否正确。一步一步往下的同时,关注调用栈是否符合你的期望,添加必要的变量到调试窗口,看看他们何时发生意想不到的变化。**条件断点**是一个杀手级功能,你可以设置一些条件,让条件满足时程序在此处停下。下面的一些常见问题可能对你的调试有所帮助。
如果一切正常或者应该说你没有发现异常虽然他确实在那里试着查看你使用的外设寄存器值。这时候你要用到芯片的数据手册和功能描述手册。在运行的每一步中对照datasheet的寄存器状态和改变看看是否符合你期望的程序行为。寄存器分为控制寄存器/状态就寄存器/数据寄存器参照datasheet就可以明白这些寄存器是控制哪些功能描述什么状态以及内部是什么数据。
@ -26,37 +26,37 @@
### HardFault_Handler()
99%是由于野指针和非法内存访问导致的。在HardFault函数内添加一句`asm("bx lr");`, 并在此处加上断点。当代码运行至此处时,选择跳出,程序会跳转回出错之前最后一句执行的语句。
99%是由于野指针和非法内存访问导致的。在HardFault函数内添加一句 `asm("bx lr");`, 并在此处加上断点。当代码运行至此处时,选择跳出,程序会跳转回出错之前最后一句执行的语句。
查看你是否在出错前的最后一次操作中访问了非法地址或使用了已经被析构的指针。 `memcpy`的目标地址和源地址重合也可能引发硬件错误。另外如果使用指针访问了一个非对齐地址请参考__pack()相关的说明这是CMSIS架构中不允许的有些架构可以修改启动文件使其支持例如你有四个uint8类型的数据被存储在0x03-0x07的地址内这时候你通过强制类型转换以float的方式读取这四个字节就会发生非对齐访问。 虽然结构体可以通过`__pack(1)`来压缩,编译器会对结构体变量进行处理,在读取非对齐字段时分别读取拆分的两个部分再进行合并,从而支持非对齐访问;但前述的行为却是未定义的,编译器在编译代码的时候并不知道你会以分开的方式访问这段内存,即使知道,他也无法预测栈上分配的空间是否能对齐。
查看你是否在出错前的最后一次操作中访问了非法地址或使用了已经被析构的指针。 `memcpy`的目标地址和源地址重合也可能引发硬件错误。另外如果使用指针访问了一个非对齐地址请参考__pack()相关的说明这是CMSIS架构中不允许的有些架构可以修改启动文件使其支持例如你有四个uint8类型的数据被存储在0x03-0x07的地址内这时候你通过强制类型转换以float的方式读取这四个字节就会发生非对齐访问。 虽然结构体可以通过 `__pack(1)`来压缩,编译器会对结构体变量进行处理,在读取非对齐字段时分别读取拆分的两个部分再进行合并,从而支持非对齐访问;但前述的行为却是未定义的,编译器在编译代码的时候并不知道你会以分开的方式访问这段内存,即使知道,他也无法预测栈上分配的空间是否能对齐。
常见的错误还包括使用未初始化的指针内部可能时垃圾值指向未知的地址和初始化为NULL的指针指向0x00地址。`free`一个指针两次也可能导致错误。
### 通信外设传递的数据没有进行压缩
经典的`__pack()`问题。这种问题大多出现在结构体的传输上。用于通信的结构体请在两端用`#pragma __pack(1)`和`#pragma __pack()`包裹。否则传输时会出现空字节,使得数据和你使用的协议对不上号。
经典的 `__pack()`问题。这种问题大多出现在结构体的传输上。用于通信的结构体请在两端用 `#pragma __pack(1)` `#pragma __pack()`包裹。否则传输时会出现空字节,使得数据和你使用的协议对不上号。
### Delay或定时器卡死永远不跳出
Systick和HAL_Delay以及使用TIM来定时的方法都需要通过**中断**来更新时间。如其重装载计数器上限为65535当计数达到此值时会触发中断在中断处理函数中增加一次溢出的时间。如果此时有更高优先级的中断或同优先级的中断正在运行且他们耗时很长 or 调用了这些依赖中断的Delay函数那么将会形成**死锁**,永不见天日。 如果中断被关闭,这些计时也无法更新。 这里推荐使用DWT定时器在`bsp_dwt`中实现其重载计数器是64位的按照stm32f407 168MHz的运行频率需要两天多的时间才会发生溢出因此仅依赖其重载计数器计算两次tick的差值就可以实现高精度的定时除非你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()`的时候没有正确设置源地址和目标地址或长度。
有时候,你发现你使用的变量值变成了奇怪的数,或者你的程序突然崩溃了,但是你并没有在代码中对这个变量进行过修改。这时候,你应该检查一下你的指针是否越界了。 你也许没有正确的将void\*指针cast成期望的类型使其访问了不该访问的位置例如一个uint8类型长度为4数组你希望将其转化为float进行读取。但你在声明数组时只分配了3个字节使用float\*访问就会触及未知的第四个字节,第四个位置上存放的可能是其他变量的值,这时候访问其他变量就会出现奇怪的值。 或者,你在 `memset()` `memcpy()`的时候没有正确设置源地址和目标地址或长度。
### 实时系统
中断中使用实时系统的接口时记得使用有`ISR`后缀的版本它们对中断的调用做了特殊处理。所有中断的优先级都是高于RTOS中任务的优先级的。
中断中使用实时系统的接口时记得使用有 `ISR`后缀的版本它们对中断的调用做了特殊处理。所有中断的优先级都是高于RTOS中任务的优先级的。
若期望某个任务以1KHz频率运行你可能会在任务循环外加一个`OS_Delay(1)`但是你的任务执行时间要是超过了1ms系统的调度就会出现异常倘若你还在此任务内认为每次进入的时间间隔是1ms并据此编写了一些依赖周期性的代码那便是大错特错了。
若期望某个任务以1KHz频率运行你可能会在任务循环外加一个 `OS_Delay(1)`但是你的任务执行时间要是超过了1ms系统的调度就会出现异常倘若你还在此任务内认为每次进入的时间间隔是1ms并据此编写了一些依赖周期性的代码那便是大错特错了。
对于实时性和周期性要求高的任务,使用`vTaskDelayUntil()`,这可以获得更高的定时精度。
对于实时性和周期性要求高的任务,使用 `vTaskDelayUntil()`,这可以获得更高的定时精度。
不同的任务/中断调用了相同的函数,或使用了共享变量/全局变量/函数内的static变量会导致读\&写冲突,也被称作**数据竞争**。这一般只会在任务繁重且中断频率高时出现。要避免这种情况,访问共享变量时应进入临界区(关闭全局中断)或使用**互斥锁**。消息队列也是一个不错的选择这些功能在FreeRTOS中都提供了支持。
@ -74,7 +74,7 @@ long long的范围比float小。无符号和有符号数直接转换可能变成
### 宏
换行用 `\` ,注意同一个代码块展开后用花括号`{}`包裹,特别注意宏展开之后是直接的文本替换!!!
换行用 `\` ,注意同一个代码块展开后用花括号 `{}`包裹,特别注意宏展开之后是直接的文本替换!!!
用已有的宏定义宏并且进行运算时,要将后面的字段用括号包围,因为:
@ -85,8 +85,6 @@ long long的范围比float小。无符号和有符号数直接转换可能变成
**宏只在当前文件生效**,如果宏放在.c那么对其他的文件是不可见的这也一般称作私有宏。
## 典型debug案例一
这是一个结合了软件和硬件且有多模块耦合的异常。该bug发生在调试平衡步兵的底盘过程当中。
@ -108,13 +106,13 @@ static void LKMotorDecode(CANInstance *_instance)
// 上面一行应为: 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。
这是问题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时将引起异常。
@ -126,7 +124,7 @@ void MotorControlTask()
DJIMotorControl();
HTMotorControl();
LKMotorControl();
ServeoMotorControl();
@ -135,23 +133,21 @@ void MotorControlTask()
}
```
这是motortask的内容此任务将以500hz的频率运行。在发生bug时我们将4个HT04电机和2个LK MF9025电机全部连接到CAN1上。注意HT04不支持多电机指令因此占用的带宽较大。在`LKMotorControl()`完成参考值计算和CAN发送之后立刻会调用`HTMotorControl()`后者需要连续发送4条报文。而HT和LK电机都会在接收到控制指令之后发送反馈信息报文。由于HT电机的控制在LK电机控制之后立刻执行导致总线被占据LK电机发送的反馈数据仲裁失败无法获得总线占有权使得主机收不到反馈数据。
这是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。
测试时发现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指针被其他的内存访问语句修改。
此时内心有些动摇开始检查硬件连线。我们尝试把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果然能重新接收到数据了。
这时候想起HT电机是不支持多电机控制指令的因此500Hz的控制频率似乎有些过高相当于2ms内要完成2x4+1+2=11次CAN报文的发送。计算1M波特率下最大通信速率果然超出了负载。于是降低 `MotorTask()`的频率为200Hz果然能重新接收到数据了。
继续单步调试,终于发现在`LKMotorDecode()`中通过强制类型转换获取LKMotorInstance的时候用错了变量使得反馈值被写入电机的`CANInstance`内导致hcan指向随机的地址最终造成访问时引发hardfault。
继续单步调试,终于发现在 `LKMotorDecode()`通过强制类型转换获取LKMotorInstance的时候用错了变量使得反馈值被写入电机的 `CANInstance`导致hcan指向随机的地址最终造成访问时引发hardfault。
修改之后将LK电机挂载到CAN2上控制频率回到500Hz程序正常运行。
@ -159,8 +155,6 @@ void MotorControlTask()
均衡总线负载,调节任务运行时间。
# 典型debug案例二
这仍然是一个CAN总线引发的bug。使用的电机均为DJI电机。当多个电机接入时会产生反馈值跳变的情况。起初认为**总线负载过高**控制频率为500Hz反馈频率均为1kHz计算之后得出CAN的负载率接近90%),但将电机减少为一半甚至更少时仍然出现此问题。**单独使用CAN1且仅挂载一个电机则问题消失**同时使用CAN1和CAN2不论单个总线挂载几个电机则问题再次出现。
@ -170,4 +164,3 @@ void MotorControlTask()
将优先级统一设为5编译之后重新运行反馈值正常。
> “同时使用CAN1和CAN2不论几个电机则问题再次出现。” 导致此问题的原因是初始化CAN时按照rxid分配FIFO因此注册的电机会被交替分配到不同的FIFO故不论注册了几个电机只要多于2、注册到哪条总线都会出现FIFO1中断被FIFO0打断的情况。