更新了大量文档
This commit is contained in:
parent
ff5028036a
commit
253f391cd5
22
README.md
22
README.md
|
@ -27,9 +27,9 @@
|
||||||
|
|
||||||
本框架主要代码分为**BSP、Module、APP**三层。三层的代码分别存放在同名的三个文件夹中,这三个文件夹存放在根目录下。开发过程中主要编写APP层代码,Module层与BSP层不建议修改。如需添加module(如oled屏幕、其他传感器和外设等),请按照规范编写并联系组长提交commit到dev分支或对应的功能名分支,完善后合并至主分支。在配置git的时候,将自己的`user.name`配置成英文缩写或易懂的nick name。
|
本框架主要代码分为**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层应保持相同,当认为该层可能缺少部分功能或有错误时,请联系组长确认后解决并更新整个框架,**请勿自行修改提交**。 请在你修改/增加的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对应一种外设,当上面两个代码层需要使用某个外设时,这里的文件就是对应的交互接口。
|
- bsp_xxx.c/h:每一个成对的.c/h对应一种外设,当上面两个代码层需要使用某个外设时,这里的文件就是对应的交互接口。
|
||||||
- 注册回调函数与接收:通信类外设模块有的定义了回调函数(函数指针类型),module层的模块需要自行处理接收回调函数,在注册bsp的时候应传入对应参数格式的回调函数指针,使得接收中断发生的时候bsp层可以自行找到对应的上层回调函数进行调用。这也是回调函数设计的初衷:为底层代码调用上层代码提供接口,当特定事件发生的时候完成触发(自行搜索hook函数)。
|
- 注册回调函数与接收:通信类外设模块有的定义了回调函数(函数指针类型),module层的模块需要自行处理接收回调函数,在注册bsp的时候应传入对应参数格式的回调函数指针,使得接收中断发生的时候bsp层可以自行找到对应的上层回调函数进行调用。这也是回调函数设计的初衷:为底层代码调用上层代码提供接口,当特定事件发生的时候完成触发(自行搜索hook函数)。
|
||||||
|
|
||||||
## Module层
|
## Module层
|
||||||
|
|
||||||
- 主要功能:实现对设备的封装,如将IMU、PC、电机等视为一个完整的功能模块,让应用层不需要关心其底层的具体实现,直接使用接口。
|
- 主要功能:实现对设备的封装,如将IMU、PC、电机等视为一个完整的功能模块,让应用层(app)不需要关心其底层的具体实现,直接使用接口。
|
||||||
|
|
||||||
- 文件夹
|
- 文件夹
|
||||||
|
|
||||||
- **注意,module层没有也不需要进行统一初始化**。app层的应用会包含一些模块,因此由app来调用各个模块的init或register函数,只有当一个module被app实例化,这个模块才会存在。
|
- **注意,module层没有也不需要进行统一初始化**。app层的应用会包含一些模块,因此由app来调用各个模块的init()或register()函数,只有当一个module被app实例化,这个模块才会存在。
|
||||||
|
|
||||||
> 命名为init()的初始化一般来说是开发板的独占资源,即有且只有一个这样的模块,无法拥有多个实例,如板载陀螺仪、LED、按键等。命名为register()的模块则可以拥有多个,比如电机。
|
> ~~命名为init()的初始化一般来说是开发板的独占资源,即有且只有一个这样的模块,无法拥有多个实例,如板载陀螺仪、LED、按键等。命名为register()的模块则可以拥有多个,比如电机。~~ legacy support,为了保证代码风格统一,所有接口统一命名为xxxRegister()。
|
||||||
|
|
||||||
- monitor文件夹:实现看门狗功能。提供回调函数和count可选(TODO)
|
|
||||||
|
|
||||||
- algorithm:该层软件库存放位置,这些功能与硬件无关,而是提供通用的数据结构和“算子”以供该层的其他部分调用,主要是算法、控制器、底盘和位姿解算等。
|
- 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文件。
|
||||||
|
|
||||||
务必为模块添加说明文档和使用范例,以及其他需要注意的事项(如果有)。
|
务必为模块添加说明文档和使用范例,以及其他需要注意的事项(如果有)。
|
||||||
|
|
||||||
|
|
|
@ -160,7 +160,7 @@ void StartDefaultTask(void const *argument)
|
||||||
/* init code for USB_DEVICE */
|
/* init code for USB_DEVICE */
|
||||||
MX_USB_DEVICE_Init();
|
MX_USB_DEVICE_Init();
|
||||||
/* USER CODE BEGIN StartDefaultTask */
|
/* USER CODE BEGIN StartDefaultTask */
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL); // 删除默认任务,防止占用CPU
|
||||||
/* USER CODE END StartDefaultTask */
|
/* USER CODE END StartDefaultTask */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,15 +172,15 @@ void StartINSTASK(void const *argument)
|
||||||
{
|
{
|
||||||
// 1kHz
|
// 1kHz
|
||||||
INS_Task();
|
INS_Task();
|
||||||
VisionSend(); // 解算完成后发送视觉数<EFBFBD>?
|
VisionSend(); // 解算完成后发送视觉数据,但是当前的实现不太优雅,后续若添加硬件触发需要重新考虑结构的组织
|
||||||
osDelay(1);
|
osDelay(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StartMOTORTASK(void const *argument)
|
void StartMOTORTASK(void const *argument)
|
||||||
{
|
{
|
||||||
// 若使用HT电机则取消本行注释
|
// 若使用HT电机则取消本行注释,该接口会为注册了的电机设备创建线程
|
||||||
HTMotorControlInit();
|
// HTMotorControlInit();
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
// 500Hz
|
// 500Hz
|
||||||
|
@ -203,7 +203,7 @@ void StartROBOTTASK(void const *argument)
|
||||||
{
|
{
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
// 200Hz
|
// 200Hz-500Hz,若有额外的控制任务如平衡步兵可能需要提升至1kHz
|
||||||
RobotTask();
|
RobotTask();
|
||||||
osDelay(5);
|
osDelay(5);
|
||||||
}
|
}
|
||||||
|
@ -214,7 +214,8 @@ void StartUITASK(void const *argument)
|
||||||
My_UI_init();
|
My_UI_init();
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
Referee_Interactive_task();
|
Referee_Interactive_task(); // 每次给裁判系统发送完一包数据后,挂起一次,防止卡在裁判系统发送中,详见Referee_Interactive_task函数的refereeSend();
|
||||||
|
osDelay(1); // 即使没有任何UI需要刷新,也挂起一次,防止卡在UITask中无法切换
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* USER CODE END Application */
|
/* USER CODE END Application */
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
**应用之间不应该有任何包含关系,它们必须是平行工作的。**而这通过pub-sub的机制实现。module层提供了`message_center`模块,支持发布订阅者的消息订阅机制。以传统的框架为例,负责整车控制的应用和其他应用(或任务)是从属的树状结构,或不同的任务和应用之间通过全局变量传递消息(**请不要使用全局变量!**),而此框架下的不同应用是并行的关系。
|
**应用之间不应该有任何包含关系,它们必须是平行工作的。**而这通过pub-sub的机制实现。module层提供了`message_center`模块,支持发布订阅者的消息订阅机制。以传统的框架为例,负责整车控制的应用和其他应用(或任务)是从属的树状结构,或不同的任务和应用之间通过全局变量传递消息(**请不要使用全局变量!**),而此框架下的不同应用是并行的关系。
|
||||||
|
|
||||||
如果一个应用希望获取另一个应用的数据,那么他应该**订阅**由此此应用发布的话题(事件)。一个应用要把自己希望共享的数据,注册到消息中心,即**发布**。为了区别不同的消息来源(你希望订阅谁的消息?哪一个消息?),可以通过**话题名**进行订阅。也就是说,消息中心作为第三方,管理所有的消息发布者和订阅者,它像报刊亭一样对消息进行中转,使得不同的应用之间不需要包含彼此,更不用全局变量也能共享消息。
|
如果一个应用希望获取另一个应用的数据,那么他应该**订阅**由此此应用发布的话题。一个应用要把自己希望共享的数据,注册到消息中心,即**发布**。为了区别不同的消息来源(你希望订阅谁的消息?哪一个消息?),可以通过**话题名**进行订阅。也就是说,消息中心作为第三方,管理所有的消息发布者和订阅者,它像报刊亭一样对消息进行中转,使得不同的应用之间不需要包含彼此,更不用全局变量也能共享消息。
|
||||||
|
|
||||||
> 更多关于发布-订阅的实现,请参考`modules/message_center`下的文档。
|
> 更多关于发布-订阅的实现,请参考`modules/message_center`下的文档。
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
这是机器人的参数配置文件,必须要针对每个机器人进行修改。包括机器人的尺寸参数和性能参数等。你还需要在这里设定软硬件配置:云台板/底盘板/单板等。这里定义的宏会作为条件编译的决断。
|
这是机器人的参数配置文件,必须要针对每个机器人进行修改。包括机器人的尺寸参数和性能参数等。你还需要在这里设定软硬件配置:云台板/底盘板/单板等。这里定义的宏会作为条件编译的决断。
|
||||||
|
|
||||||
app层共用的状态变量和结构体等也应该定义在这里(例如用于应用之间通信的数据)。记得通信变量要用:
|
app层共用的状态变量和结构体等也应该定义在这里(例如用于应用之间通信的数据)。记得用于通信的变量要用:
|
||||||
```c
|
```c
|
||||||
#pragma pack(1)
|
#pragma pack(1)
|
||||||
typedef struct
|
typedef struct
|
||||||
|
@ -25,7 +25,9 @@ typedef struct
|
||||||
} your_struct;
|
} your_struct;
|
||||||
#pragma pack()
|
#pragma pack()
|
||||||
```
|
```
|
||||||
包裹起来,取消字节对齐以防止出现访问8byte地址而出现错误。
|
包裹起来,取消字节对齐以防止出现访问8-bit地址而出现错误。
|
||||||
|
|
||||||
|
如果你需要其他的通信数据类型或修改模块间通信数据的格式,直接在此处更改即可。
|
||||||
|
|
||||||
## robot_cmd
|
## robot_cmd
|
||||||
|
|
||||||
|
@ -35,6 +37,8 @@ typedef struct
|
||||||
|
|
||||||
robot_cmd工作起来就像一个遥控数据的兼容层,不论数据的来源是视觉上位机/遥控器/键鼠/图传通信链路/ps手柄,最后都会被转化成真实参考输入提供给其他的app。它的任务是将其他来源的数据映射到控制输入上。
|
robot_cmd工作起来就像一个遥控数据的兼容层,不论数据的来源是视觉上位机/遥控器/键鼠/图传通信链路/ps手柄,最后都会被转化成真实参考输入提供给其他的app。它的任务是将其他来源的数据映射到控制输入上。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## gimbal
|
## gimbal
|
||||||
|
|
||||||
以步兵为例,云台应用应当包含两个电机,分别用于驱动yaw和pitch轴(除非你是一个三轴的云台),还有一个imu(开发板一般放在云台上)。gimbal模块会接收robot_cmd发来的控制信息(云台的角度、转速等),并通过电机提供的接口完成电机的参考值设定。gimbal还要把imu的数据反馈给cmd,用于和视觉的通信以及云台状态的判断。
|
以步兵为例,云台应用应当包含两个电机,分别用于驱动yaw和pitch轴(除非你是一个三轴的云台),还有一个imu(开发板一般放在云台上)。gimbal模块会接收robot_cmd发来的控制信息(云台的角度、转速等),并通过电机提供的接口完成电机的参考值设定。gimbal还要把imu的数据反馈给cmd,用于和视觉的通信以及云台状态的判断。
|
||||||
|
@ -63,3 +67,4 @@ robot_cmd工作起来就像一个遥控数据的兼容层,不论数据的来
|
||||||
|
|
||||||
此框架对单开发板/双开发板/多开发板的情况都提供了支持(多板一般只在工程机器人上出现,需要自己在robot_cmd和robot_def增加相应的条件编译选项,robot.c中也不要忘记增加初始化和任务运行函数),目前通过条件编译实现了对单双板的切换。使用双板时,主控板在云台上,连接遥控器和上位机;副板在底盘上,负责底盘的运动控制和与裁判系统的通信。
|
此框架对单开发板/双开发板/多开发板的情况都提供了支持(多板一般只在工程机器人上出现,需要自己在robot_cmd和robot_def增加相应的条件编译选项,robot.c中也不要忘记增加初始化和任务运行函数),目前通过条件编译实现了对单双板的切换。使用双板时,主控板在云台上,连接遥控器和上位机;副板在底盘上,负责底盘的运动控制和与裁判系统的通信。
|
||||||
|
|
||||||
|
当然,你可以为每台不同的机器人进行特化,因为本框架是针对步兵/英雄定制的。
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
在main函数中包含`robot.h`头文件,这是对整车的抽象。将`INStask`,`motortask`,`ledtask`,`monitortask`这四个task加入`freertos.c`中,创建对应的任务,设置合适的任务运行间隔;然后将`robottask`放入freertos.c中,同样以一定的频率运行。 在初始化实时系统之前,在`main()`中调用`RobotInit()`进行整车的初始化。
|
在main函数中包含`robot.h`头文件,这是对整车的抽象。将`INStask`,`motortask`,`ledtask`,`monitortask`这四个task加入`freertos.c`中,创建对应的任务,设置合适的任务运行间隔;然后将`robottask`放入freertos.c中,同样以一定的频率运行。 在初始化实时系统之前,在`main()`中调用`RobotInit()`进行整车的初始化。
|
||||||
|
|
||||||
**关于运行的任务**,INStask的运行频率必须为1kHz,motortask推荐的运行频率为200Hz\~500Hz(详情见module/motor/motor_task.c),在MotorTask内部,对于高实时性要求的电机可以提升到1kHz,不过要注意CAN总线的负载。ledtask的运行频率推荐为1kHz,monitortask的运行频率为1kHz;robottask的运行频率推荐为150Hz以上,应当高于视觉发送的频率,若后续使用插帧,同样应该保证不低过motortask太多。
|
**关于运行的任务**,INStask的运行频率必须为1kHz,motortask推荐的运行频率为200Hz\~1000Hz(详情见module/motor/motor_task.c),在MotorTask内部,对于高实时性要求的电机可以提升到1kHz,不过要注意CAN总线的负载。monitortask的运行频率为100Hz;robottask的运行频率推荐为150Hz以上,应当高于视觉发送的频率,若后续使用插帧,同样应该保证不低过motortask太多。
|
||||||
|
|
||||||
若使用双板,则在`robot_def.h`中给对应的开发板设定宏定义,如底盘板使用`#define CHASSIS_BOARD`,云台板使用`#define GIMBAL_BOARD`;单个开发板控制整车,则定义`#define ONE_BOARD`。在每个应用中,都已经使用编译预处理指令完成条件编译,会自动根据设定的宏切换功能。使用双板的时候,目前板间通信通过CAN完成,因此两个开发板会挂载在一条总线上,在两个开发板对这条总线的其他使用CAN的设备进行配置时注意**不要发生ID冲突**,还要注意**防止负载过大**。
|
若使用双板,则在`robot_def.h`中给对应的开发板设定宏定义,如底盘板使用`#define CHASSIS_BOARD`,云台板使用`#define GIMBAL_BOARD`;单个开发板控制整车,则定义`#define ONE_BOARD`。在每个应用中,都已经使用编译预处理指令完成条件编译,会自动根据设定的宏切换功能。使用双板的时候,目前板间通信通过CAN完成,因此两个开发板会挂载在一条总线上,在两个开发板对这条总线的其他使用CAN的设备进行配置时注意**不要发生ID冲突**,还要注意**防止负载过大**。
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ Robot.c是整个机器人的抽象,其下有4个应用:robot_cmd,gimbal,
|
||||||
|
|
||||||
为了进一步解耦应用之间的关系,这里并没有采用层级结构(或设计模式中所谓的**工厂模式**,即robot_cmd包含其他三个模块),而采用了应用并列的**发布-订阅**机制,四个应用之间没有任何相互包含关系,他们之间的通信通过module层提供的`message_center`实现。每个应用会通过该模块向一些话题(事件)发布一些消息,同时从一些话题订阅消息。如robot_cmd应用会发布其他三个模块的控制信息,同时订阅其他三个模块的反馈信息。其他三个模块会订阅robot_cmd发布的控制信息,同时发布反馈给robot_cmd的信息,他们不需要知道彼此的存在,只是从`message_center`处获取其他应用发布的消息或向自己发布的话题推送消息。
|
为了进一步解耦应用之间的关系,这里并没有采用层级结构(或设计模式中所谓的**工厂模式**,即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耦合,去除顶层代码和底层的关系
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
<p align='right'>neozng1@hnu.edu.cn</p>
|
<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 */
|
/* can instance typedef, every module registered to CAN should have this variable */
|
||||||
typedef struct _
|
typedef struct _
|
||||||
{
|
{
|
||||||
CAN_HandleTypeDef* can_handle;
|
CAN_HandleTypeDef *can_handle; // can句柄
|
||||||
CAN_TxHeaderTypeDef txconf;
|
CAN_TxHeaderTypeDef txconf; // CAN报文发送配置
|
||||||
uint32_t tx_id;
|
uint32_t tx_id; // 发送id
|
||||||
uint32_t tx_mailbox;
|
uint32_t tx_mailbox; // CAN消息填入的邮箱号
|
||||||
uint8_t tx_buff[8];
|
uint8_t tx_buff[8]; // 发送缓存,发送消息长度可以通过CANSetDLC()设定,最大为8
|
||||||
uint8_t rx_buff[8];
|
uint8_t rx_buff[8]; // 接收缓存,最大消息长度为8
|
||||||
uint32_t rx_id;
|
uint32_t rx_id; // 接收id
|
||||||
void (*can_module_callback)(struct _*);
|
uint8_t rx_len; // 接收长度,可能为0-8
|
||||||
void* id;
|
// 接收的回调函数,用于解析接收到的数据
|
||||||
} can_instance;
|
void (*can_module_callback)(struct _ *); // callback needs an instance to tell among registered ones
|
||||||
|
void *id; // 使用can外设的模块指针(即id指向的模块拥有此can实例,是父子关系)
|
||||||
|
} CANInstance;
|
||||||
|
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
|
@ -62,6 +66,7 @@ typedef void (*can_callback)(can_instance*);
|
||||||
|
|
||||||
```c
|
```c
|
||||||
void CANRegister(can_instance* instance, can_instance_config config);
|
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);
|
uint8_t CANTransmit(can_instance* _instance, uint8_t timeout);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,30 @@
|
||||||
# bsp_dwt
|
# 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;
|
||||||
|
```
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,28 @@
|
||||||
|
|
||||||
bsp_log是基于segger RTT实现的日志打印模块。
|
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
|
```c
|
||||||
int printf_log(const char *fmt, ...);
|
int printf_log(const char *fmt, ...);
|
||||||
void Float2Str(char *str, float va);
|
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()`发送日志,可以设置小数点位数以降低带宽开销。
|
或直接通过`%f`格式符直接使用`printf_log()`发送日志,可以设置小数点位数以降低带宽开销。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
# bsp usb
|
# bsp usb
|
||||||
|
|
||||||
简单写点,有待优化.
|
简单写点,有待优化. 目前仅支持虚拟串口通信,暂未开发其他内容.
|
||||||
|
|
||||||
|
注意,为了增加发送完成和接收完成回调,对Inc/usbd_xxxx.h四个文件做了修改,对Src/usbxxx.c也进行了修改。
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# tfminiplus
|
||||||
|
|
||||||
|
北醒激光单点激光雷达模块的简单实现。目前使用iic阻塞通信耗时和速度都太慢,需修改为中断读取,bsp_iic已经提供相应的接口。
|
|
@ -25,6 +25,7 @@ CAN comm是用于CAN多机通信的模块。你不需要关心实现的协议,
|
||||||
CANCommInstance *CANCommInit(CANComm_Init_Config_s* comm_config);
|
CANCommInstance *CANCommInit(CANComm_Init_Config_s* comm_config);
|
||||||
void CANCommSend(CANCommInstance *instance, uint8_t *data);
|
void CANCommSend(CANCommInstance *instance, uint8_t *data);
|
||||||
void *CANCommGet(CANCommInstance *instance);
|
void *CANCommGet(CANCommInstance *instance);
|
||||||
|
uint8_t CANCommIsOnline(CANCommInstance *instance);
|
||||||
```
|
```
|
||||||
|
|
||||||
第一个函数将会初始化一个CANComm实例,返回其指针。使用CANComm进行通信的应用应该保存返回的指针。初始化需要传入一个初始化结构体。请在应用初始化的时候调用该函数。推荐的结构体配置方式如下:
|
第一个函数将会初始化一个CANComm实例,返回其指针。使用CANComm进行通信的应用应该保存返回的指针。初始化需要传入一个初始化结构体。请在应用初始化的时候调用该函数。推荐的结构体配置方式如下:
|
||||||
|
@ -139,4 +140,3 @@ CAN comm的通信协议如下:
|
||||||
|
|
||||||
流程图如下:![未命名文件](../../assets/CANcomm.png)
|
流程图如下:![未命名文件](../../assets/CANcomm.png)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ void DaemonTask()
|
||||||
else if (dins->callback) // 等于零说明超时了,调用回调函数(如果有的话)
|
else if (dins->callback) // 等于零说明超时了,调用回调函数(如果有的话)
|
||||||
{
|
{
|
||||||
dins->callback(dins->owner_id); // module内可以将owner_id强制类型转换成自身类型从而调用特定module的offline callback
|
dins->callback(dins->owner_id); // module内可以将owner_id强制类型转换成自身类型从而调用特定module的offline callback
|
||||||
|
// @todo 为蜂鸣器/led等增加离线报警的功能,非常关键!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,3 +5,8 @@
|
||||||
注意LK电机在使用多电机发送的时候,只支持一条总线上至多4个电机,多电机模式下LK仅支持发送id 0x280为接收ID为0x140+id.
|
注意LK电机在使用多电机发送的时候,只支持一条总线上至多4个电机,多电机模式下LK仅支持发送id 0x280为接收ID为0x140+id.
|
||||||
|
|
||||||
要设置为多电机模式,请通过串口连接电机,并使用该文件夹下的LK motor tool.exe进行配置。
|
要设置为多电机模式,请通过串口连接电机,并使用该文件夹下的LK motor tool.exe进行配置。
|
||||||
|
|
||||||
|
|
||||||
|
## LK的其他电机
|
||||||
|
|
||||||
|
若使用其他LK电机,唯一需要修改的是确定编码器的精度,即LKMotorDecode()部分的速度反馈和编码器反馈解析。
|
|
@ -4,10 +4,8 @@
|
||||||
|
|
||||||
请使用字库软件制作自己的图标和不同大小的ascii码.
|
请使用字库软件制作自己的图标和不同大小的ascii码.
|
||||||
|
|
||||||
|
|
||||||
> 后续尝试移植一些图形库使得功能更加丰富
|
> 后续尝试移植一些图形库使得功能更加丰富
|
||||||
> oled主要作调试和log/错误显示等使用
|
> oled主要作调试和log/错误显示等使用
|
||||||
> 可以提供给视觉和机械的同学调试接口,方便他们通过显示屏进行简单的设置
|
> 可以提供给视觉和机械的同学调试接口,方便他们通过显示屏进行简单的设置
|
||||||
|
|
||||||
|
|
||||||
*可以引入RoboMaster oled,或额外增加一个编码器用于控制oled界面并设定一些功能.*
|
*可以引入RoboMaster oled,或额外增加一个编码器用于控制oled界面并设定一些功能.*
|
|
@ -1,6 +1,6 @@
|
||||||
# 标准命令
|
# 标准命令
|
||||||
|
|
||||||
这是一个体力活,也是一个艺术品。请把不同的控制命令module进行封装,以转化成标准的消息类型,包括云台角度速度,底盘速度,发射频率是否发射等信息,供RobotCMD应用或其他应用使用。
|
这是一个体力活,也是一个艺术品。请把不同的控制命令module进行封装,以转化成标准的消息类型,包括云台角度速度,底盘速度,发射频率是否发射等信息,供RobotCMD应用或其他应用使用。通过这种方式,开发者可以更专注于cmd命令的编写,而不需要为每台机器人/不同的控制器编写命令转换。
|
||||||
|
|
||||||
是否将下面的模块都放到standard_cmd文件夹下?似乎没有必要。
|
是否将下面的模块都放到standard_cmd文件夹下?似乎没有必要。
|
||||||
|
|
||||||
|
@ -13,5 +13,4 @@
|
||||||
## key and mouse
|
## key and mouse
|
||||||
|
|
||||||
## 图传链路
|
## 图传链路
|
||||||
|
图传链路的数据解析似乎和键鼠是同一套协议,不过走的是串口,可能需要额外添加支持模块
|
||||||
这似乎和键鼠是同一套,不过走的是串口,可能需要额外添加支持模块
|
|
|
@ -1,7 +1,9 @@
|
||||||
# univsersal communication
|
# univsersal communication
|
||||||
|
|
||||||
|
@todo
|
||||||
|
|
||||||
unicomm旨在为通信提供一套标准的协议接口,屏蔽底层的硬件差异,使得上层应用可以定制通信协议,包括包长度/可变帧长/帧头尾/校验方式等。
|
unicomm旨在为通信提供一套标准的协议接口,屏蔽底层的硬件差异,使得上层应用可以定制通信协议,包括包长度/可变帧长/帧头尾/校验方式等。
|
||||||
|
|
||||||
不论底层具体使用的是什么硬件接口,硬件的每一帧传输完将数据放在缓冲区里之后,就没有任何区别了。 此模块实际上就是对缓冲区的rawdata进行操作,包括查找帧头,计算包长度,校验错误等。
|
不论底层具体使用的是什么硬件接口,实际上每一帧传输完并把数据放在缓冲区之后,就没有任何区别了。 此模块实际上就是对缓冲区的rawdata进行操作,包括查找帧头,计算包长度,校验错误等。
|
||||||
|
|
||||||
完成之后,可以将module/can_comm移除,把原使用了cancomm的应用迁移到此模块。
|
完成之后,可以将module/can_comm、视觉的通信协议seasky_protocol和master_process等移除,把原使用了cancomm的应用迁移到此模块。
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# vofa
|
||||||
|
|
||||||
|
**除非迫不得已,否则强烈不推荐使用vofa进行调试。应通过bsp_log输出日志,或使用ozone可视化。**
|
43
如何定位bug.md
43
如何定位bug.md
|
@ -1,6 +1,6 @@
|
||||||
# how to locate bug in your code
|
# how to locate bug in your code
|
||||||
|
|
||||||
[TOC]
|
[toc]
|
||||||
|
|
||||||
只讨论运行中的bug(指程序的运行结果不符合你的期望)和异常(直接异常终止)。编译期出现的warning和error不在此范畴,他们都可以通过直接阅读报错信息解决。
|
只讨论运行中的bug(指程序的运行结果不符合你的期望)和异常(直接异常终止)。编译期出现的warning和error不在此范畴,他们都可以通过直接阅读报错信息解决。
|
||||||
|
|
||||||
|
@ -12,9 +12,9 @@
|
||||||
|
|
||||||
**修改之后要保存,编译,再调试/下载**。建议你把自动保存打开,并勤劳地commit。push时注意reset这些调试时产生的commit,合并成一次提交。你也可以新建一个分支用于解决bug,这是git的最佳实践。
|
**修改之后要保存,编译,再调试/下载**。建议你把自动保存打开,并勤劳地commit。push时注意reset这些调试时产生的commit,合并成一次提交。你也可以新建一个分支用于解决bug,这是git的最佳实践。
|
||||||
|
|
||||||
同时注意条件编译的兼容,你是否测试一些应用或模块,却使用了错误的`#define`?
|
同时注意条件编译的兼容,你是否测试一些应用或模块,却使用了错误的 `#define`?
|
||||||
|
|
||||||
完成上面的两步仍然无济于事,开始单步调试吧。在调试的同时,再仔细看看你的代码是否写错变量名和运算符,比如,把`==`写成了`=`,那么判断条件将永远是`true`。在值得怀疑的地方,打开汇编视图,查看你要访问的地址是否正确。一步一步往下的同时,关注调用栈是否符合你的期望,添加必要的变量到调试窗口,看看他们何时发生意想不到的变化。**条件断点**是一个杀手级功能,你可以设置一些条件,让条件满足时程序在此处停下。下面的一些常见问题可能对你的调试有所帮助。
|
完成上面的两步仍然无济于事,开始单步调试吧。在调试的同时,再仔细看看你的代码是否写错变量名和运算符,比如,把 `==`写成了 `=`,那么判断条件将永远是 `true`。在值得怀疑的地方,打开汇编视图,查看你要访问的地址是否正确。一步一步往下的同时,关注调用栈是否符合你的期望,添加必要的变量到调试窗口,看看他们何时发生意想不到的变化。**条件断点**是一个杀手级功能,你可以设置一些条件,让条件满足时程序在此处停下。下面的一些常见问题可能对你的调试有所帮助。
|
||||||
|
|
||||||
如果一切正常(或者应该说你没有发现异常,虽然他确实在那里),试着查看你使用的外设寄存器值。这时候你要用到芯片的数据手册和功能描述手册。在运行的每一步中,对照datasheet的寄存器状态和改变,看看是否符合你期望的程序行为。寄存器分为控制寄存器/状态就寄存器/数据寄存器,参照datasheet就可以明白这些寄存器是控制哪些功能,描述什么状态以及内部是什么数据。
|
如果一切正常(或者应该说你没有发现异常,虽然他确实在那里),试着查看你使用的外设寄存器值。这时候你要用到芯片的数据手册和功能描述手册。在运行的每一步中,对照datasheet的寄存器状态和改变,看看是否符合你期望的程序行为。寄存器分为控制寄存器/状态就寄存器/数据寄存器,参照datasheet就可以明白这些寄存器是控制哪些功能,描述什么状态以及内部是什么数据。
|
||||||
|
|
||||||
|
@ -26,19 +26,19 @@
|
||||||
|
|
||||||
### HardFault_Handler()
|
### 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`一个指针两次也可能导致错误。
|
常见的错误还包括使用未初始化的指针(内部可能时垃圾值,指向未知的地址)和初始化为NULL的指针(指向0x00地址)。`free`一个指针两次也可能导致错误。
|
||||||
|
|
||||||
### 通信外设传递的数据没有进行压缩
|
### 通信外设传递的数据没有进行压缩
|
||||||
|
|
||||||
经典的`__pack()`问题。这种问题大多出现在结构体的传输上。用于通信的结构体请在两端用`#pragma __pack(1)`和`#pragma __pack()`包裹。否则传输时会出现空字节,使得数据和你使用的协议对不上号。
|
经典的 `__pack()`问题。这种问题大多出现在结构体的传输上。用于通信的结构体请在两端用 `#pragma __pack(1)`和 `#pragma __pack()`包裹。否则传输时会出现空字节,使得数据和你使用的协议对不上号。
|
||||||
|
|
||||||
### Delay或定时器卡死永远不跳出
|
### 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超过两天,你在搞笑)。
|
||||||
|
|
||||||
### 静态变量的陷阱
|
### 静态变量的陷阱
|
||||||
|
|
||||||
|
@ -48,15 +48,15 @@ Systick和HAL_Delay以及使用TIM来定时的方法,都需要通过**中断**
|
||||||
|
|
||||||
### 指针越界读写/内存泄漏
|
### 指针越界读写/内存泄漏
|
||||||
|
|
||||||
有时候,你发现你使用的变量值变成了奇怪的数,或者你的程序突然崩溃了,但是你并没有在代码中对这个变量进行过修改。这时候,你应该检查一下你的指针是否越界了。 你也许没有正确的将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中都提供了支持。
|
不同的任务/中断调用了相同的函数,或使用了共享变量/全局变量/函数内的static变量会导致读\&写冲突,也被称作**数据竞争**。这一般只会在任务繁重且中断频率高时出现。要避免这种情况,访问共享变量时应进入临界区(关闭全局中断)或使用**互斥锁**。消息队列也是一个不错的选择,这些功能在FreeRTOS中都提供了支持。
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ long long的范围比float小。无符号和有符号数直接转换可能变成
|
||||||
|
|
||||||
### 宏
|
### 宏
|
||||||
|
|
||||||
换行用 `\` ,注意同一个代码块展开后用花括号`{}`包裹,特别注意宏展开之后是直接的文本替换!!!
|
换行用 `\` ,注意同一个代码块展开后用花括号 `{}`包裹,特别注意宏展开之后是直接的文本替换!!!
|
||||||
|
|
||||||
用已有的宏定义宏并且进行运算时,要将后面的字段用括号包围,因为:
|
用已有的宏定义宏并且进行运算时,要将后面的字段用括号包围,因为:
|
||||||
|
|
||||||
|
@ -85,8 +85,6 @@ long long的范围比float小。无符号和有符号数直接转换可能变成
|
||||||
|
|
||||||
**宏只在当前文件生效**,如果宏放在.c那么对其他的文件是不可见的,这也一般称作私有宏。
|
**宏只在当前文件生效**,如果宏放在.c那么对其他的文件是不可见的,这也一般称作私有宏。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 典型debug案例一
|
## 典型debug案例一
|
||||||
|
|
||||||
这是一个结合了软件和硬件且有多模块耦合的异常。该bug发生在调试平衡步兵的底盘过程当中。
|
这是一个结合了软件和硬件且有多模块耦合的异常。该bug发生在调试平衡步兵的底盘过程当中。
|
||||||
|
@ -114,7 +112,7 @@ static void LKMotorDecode(CANInstance *_instance)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
这是问题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时将引起异常。
|
随后,更新之后的数据被覆写到can instance内部,使得其成员变量改变,包括hcan、txbuf、rxbuf、tx/rxlen等。hcan是HAL定义的can句柄类型,里面保存了指向can状态和控制寄存器的指针以及其他HAL状态信息,然而其值被电机反馈回来的值覆写,之后HAL的接口访问hcan时将引起异常。
|
||||||
|
|
||||||
|
@ -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的发现和定位的尝试
|
### bug的发现和定位的尝试
|
||||||
|
|
||||||
程序的大体情况如下,当时进行轮足式倒立摆机器人的测试,启用了balance.c,在其中注册了4个HT04电机(can1)和2个LK9025电机(can2)。控制报文的发送频率均为500Hz。
|
程序的大体情况如下,当时进行轮足式倒立摆机器人的测试,启用了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和0x280),CAN标准ID放不下。但是查阅CAN specification后发现standar ID可以容纳11位的值,应该不会有问题。于是把过滤器配置为mask模式,让bxCAN控制器接收所有报文(即不进行过滤)。然而还是不奏效,仍然无法收到数据。
|
有了方向之后,进一步对每一个函数都进行单步进入调试,同时时刻监测hcan1的值。然而,这时候出现即使一开始就单步调试也无法进入LK电机解码函数的问题。于是,怀疑是CAN过滤器的配置问题,使得LK电机反馈报文被过滤,检查LK的接收id无误后,认为可能由于LK电机的发送和接收ID都比较大(0x140和0x280),CAN标准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,程序正常运行。
|
修改之后,将LK电机挂载到CAN2上,控制频率回到500Hz,程序正常运行。
|
||||||
|
|
||||||
|
@ -159,8 +155,6 @@ void MotorControlTask()
|
||||||
|
|
||||||
均衡总线负载,调节任务运行时间。
|
均衡总线负载,调节任务运行时间。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 典型debug案例二
|
# 典型debug案例二
|
||||||
|
|
||||||
这仍然是一个CAN总线引发的bug。使用的电机均为DJI电机。当多个电机接入时,会产生反馈值跳变的情况。起初认为**总线负载过高**,(控制频率为500Hz,反馈频率均为1kHz,计算之后得出CAN的负载率接近90%),但将电机减少为一半甚至更少时仍然出现此问题。**单独使用CAN1且仅挂载一个电机则问题消失**,同时使用CAN1和CAN2(不论单个总线挂载几个电机)则问题再次出现。
|
这仍然是一个CAN总线引发的bug。使用的电机均为DJI电机。当多个电机接入时,会产生反馈值跳变的情况。起初认为**总线负载过高**,(控制频率为500Hz,反馈频率均为1kHz,计算之后得出CAN的负载率接近90%),但将电机减少为一半甚至更少时仍然出现此问题。**单独使用CAN1且仅挂载一个电机则问题消失**,同时使用CAN1和CAN2(不论单个总线挂载几个电机)则问题再次出现。
|
||||||
|
@ -170,4 +164,3 @@ void MotorControlTask()
|
||||||
将优先级统一设为5,编译之后重新运行,反馈值正常。
|
将优先级统一设为5,编译之后重新运行,反馈值正常。
|
||||||
|
|
||||||
> “同时使用CAN1和CAN2(不论几个电机)则问题再次出现。” 导致此问题的原因是初始化CAN时按照rxid分配FIFO,因此注册的电机会被交替分配到不同的FIFO,故不论注册了几个电机(只要多于2)、注册到哪条总线都会出现FIFO1中断被FIFO0打断的情况。
|
> “同时使用CAN1和CAN2(不论几个电机)则问题再次出现。” 导致此问题的原因是初始化CAN时按照rxid分配FIFO,因此注册的电机会被交替分配到不同的FIFO,故不论注册了几个电机(只要多于2)、注册到哪条总线都会出现FIFO1中断被FIFO0打断的情况。
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue