程序的调试排错往往会占用整个工作的绝大多数时间,很多高级语言都拥有一整套的调试方法。C 语言虽然比较底层,但在调试程序时,也可以使用我之前推荐的 GDB等调试工具,但很多高手绝对不会仅依赖于调试工具,大多数人还是喜欢在开发时用 assert 来排查错误,而开发结束后,更有一些人会使用更加高效完整的日志库等来监控可能出现的 bug。

C 标准库

C 标准库中自带了 assert.h 这个文件,可以说前人在使用时已经将排错考虑细致了,我们加入这个功能后即可以直接使用 assert 进行调试了,函数声明:void assert(int expression); expression 这可以是一个变量或任何 C 表达式。如果 expression 为 TRUE,assert() 不执行任何动作。如果 expression 为 FALSE,assert() 会在标准错误 stderr 上显示错误消息,并中止程序执行。实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <assert.h>
#include <stdio.h>
int main(void)
{
int a;
printf("请输入一个整数值: ");
scanf("%d", &a);
assert(a >= 10);
printf("输入的整数是: %d\n", a);
}
请输入一个整数值: 2
a: a.c:22: main: Assertion `a >= 10' failed.
[1] 9632 abort (core dumped) ./a

网上查了下,没能找到 assert 标准库的函数原型,于是自己写了个简易版本的 assert :

1
2
3
4
5
6
7
#define assert(expr) ((void)((expr) || (_assert(__FILE__,__LINE__),0)))
void _assert(const char *file, int line)
{
printf("%s:%d", file, line);
exit(0);
}

MCU 自带库 assert 实现

这边我们以国内广泛使用的 stm32 v3.5.0 官方库为例,使用STM32库函数的时候,你会发现带参数的库函数前面都有assert_param语句。

assert_param语句是用于程序开发的时候,调试用的检测语句。默认是不开启的,你可以无视它的存在。但是,当你在调试程序的时候,可以打开这个检测机制,调试完了再关闭。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Exported macro ------------------------------------------------------------*/
#ifdef USE_FULL_ASSERT
/**
* @brief The assert_param macro is used for function's parameters check.
* @param expr: If expr is false, it calls assert_failed function which reports
* the name of the source file and the source line number of the call
* that failed. If expr is true, it returns no value.
* @retval None
*/
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0)
#endif /* USE_FULL_ASSERT */

如果我们想要使用 assert_param 就需要定义加入 #define USE_FULL_ASSERT 语句,另外我们可以发现 assert 判定失败时会调用 assert_failed() 函数,但值得注意的是, ST 的固件库中没有预留这个函数的原型,因此我们需要手动创建这个 assert_failed 函数,以下提供一个可供参考的代码:

1
2
3
4
5
6
7
8
9
void assert_failed(uint8_t* file, uint32_t line)
{
/*
注意:编译器 Build Output 栏是不会报错的,这边使用的是 MCU 数据如何输出,
需要根据具体情况分析,一般 MCU 都会把结果输出到串口。
*/
printf("Wrong parameters value: file %s on line %d\r\n", file, line);
while(1);
}

另外 assert_failed()/_assert 函数基本上是所有判定失败后都需要调用的函数,具体要根据你平台提供的库而定,有些 MCU 平台是预留了 assert_failed 这种函数的,你不需要重新定义,只需要略加补充方法即可。例如 EFM32 提供的 assert 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#if defined(DEBUG_EFM) || defined(DEBUG_EFM_USER)
/* Due to footprint considerations, we only pass file name and line number, */
/* not the assert expression (nor function name (C99)) */
void assertEFM(const char *file, int line);
#define EFM_ASSERT(expr) ((expr) ? ((void)0) : assertEFM(__FILE__, __LINE__))
#else
#define EFM_ASSERT(expr) ((void)(expr))
void assertEFM(const char *file, int line)
{
(void)file; /* Unused parameter */
(void)line; /* Unused parameter */
/* 预留用来定制你需要的操作 */
while (true)
{
}
}

小结

assert 调用基本上大同小异,错误时直接终止程序或者死循环,同时能够提供代码出错位置即可。开发时,不适用 assert 也是可以满足绝大多数需求的,但这时你往往需要自己写语句让错误输出出来,assert 仅是提供了一个更加便捷的操作方式而已。

参考链接:
http://www.runoob.com/cprogramming/c-macro-assert.html
http://www.rationmcu.com/stm32/1508.html


写作时间:21:38-22:17