sentry_gimbal_hzz/如何定位bug.md

9.2 KiB
Raw Blame History

how to locate bug in your code

只讨论运行中的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后缀。

换行用 \ ,注意同一个代码块展开后用花括号{}包裹,特别注意宏展开之后是直接的文本替换!!!

用已有的宏定义宏并且进行运算时,要将后面的字段用括号包围,因为:

#define YOUR_DEF SOMETHING 
// 宏通过空格来解析替换字段,YOUR_DEF空格后的第一个字段当作替换文本

宏只在当前文件生效,如果宏放在.c那么对其他的文件是不可见的这也一般称作私有宏。