更新了大量文档

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

@@ -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打断的情况。