任何软件编程都需要有调试信息输出来进行,软件的调试。
在做keil51软件项目时,一般使用UART或者RS232输出调试信息。
那如何输出适合自己项目的调试信息,有下面几个方案可以参考。
1. 重定向打印
首先,需要调试好uart输出接口,可以正常输出字符。
如果只需要打印字符串而不需要打印变量参数,那么就比较简单。只需要像如下这样打印:
- void uart_send_str(char * pstr)
- {
- unsigned int len;
- len = strlen(pstr);
- while(len--)
- {
- uart_send_byte(pstr);
- }
- }
但是,这样设计只能适用简单的设计,如果设计稍微复杂点就需要打印参数的数值。而上面的方式没有办法完成这样的需求。
那么就需要使用keil51库中支持的打印方式,通过重定向打印的接口来实现正常调用库中的printf,sprintf等函数,当然也可以自己实现printf函数。
输入输出库函数的原型声明在头文件STDIO.H中定义,通过8051的串行口工作。如果希望支持其他I/O接口,只需要改动_getkey()和putchar()函数。库中所有其他的I/O支持函数都依赖于这两个函数模块。在使用8051系列单片机的串行口之前,应先对其进行初始化。
函数名及定义 | 功能说明 |
char _getkey(void) | 等待从8051串口读入一个字符并返回读入的字符,这个函数是改变整个输入端口机制时应做修改的唯一一个函数 |
char getchar(void) | 使用_getkey从串口读入字符,并将读入的字符马上传给putchar函数输出,其他与_getkey函数相同 |
char *gets(char *s,int n) | 该函数通过getchar从串口读入一个长度为n的字符串并存入由s指向的数组。输入时一旦检测到换行符就结束字符输入。输入成功时返回传入的参数指针,失败时返回NULL |
char ungetchar(char c) | 将输入字符回送到输入缓冲区,因此下次gets或getchar可用该字符。成功时返回char型值,失败时返回EOF,不能处理多个字符 |
char putchar(char c) | 通过8051串行口输出字符,与函数_getkey一样,这是改变整个输出机制所需要修改的唯一一个函数 |
int printf(const char *fmstr[,argument]...) | 以第一个参数指向字符串制定的格式通过8051串行口输出数值和字符串,返回值为实际输出的字符数 |
int sprintf(char *s,const char *fmstr[,argument]...) | 与printf功能相似,但数据是通过一个指针s送入内存缓冲区,并以ASCII码的形式存储 |
int puts(const char *s) | 利用putchar函数将字符串和换行符写入串行口,错误时返回EOF,否则返回0 |
int scanf(const char *fmstr[,argument]...) | 在格式控制串的控制下,利用getchar函数从串行口读入数据,每遇到一个符合格式控制串fmstr规定的值,就将它按顺序存入由参数指针argument指向的存储单元。其中每个参数都是指针,函数返回所发现并转换的输入项数,错误则返回EOF |
int sscanf(char *s,const char *fmstr[,argument]...) | 与scanf的输入方式相似,但字符串的输入不是通过串行口,而是通过指针s指向的数据缓冲区 |
void vprintf(const char *s,char *fmstr,char *argptr) | 将格式化字符串和数据值输出到由指针s指向的内存缓冲区内。类似于sprintf,但接受一个指向变量表的指针,而不是变量表。返回值为实际写入到输出字符串中的字符数
|
Cx51 User's Guide 中对char putchar(char c)的描述如下
Summary |
|
Description | The putchar function transmits the character c using the serial port. Note
|
Return Value | The putchar routine returns the character output, c. |
现在只考虑打印调试信息,获取字符不用管。在code中加入这段代码就可以实现重定向,但是不要忘记#include <stdio.h>
- char putchar(char c)
- {
- uart_send_bayte(c);
- return c;
- }
参考keil安装文件内的定义function
Keil\C51\LIB\PUTCHAR.C
- /***********************************************************************/
- /* */
- /* PUTCHAR.C: This routine is the general character output of C51. */
- /* You may add this file to a uVision2 project. */
- /* */
- /* To translate this file use C51 with the following invocation: */
- /* C51 PUTCHAR.C <memory model> */
- /* */
- /* To link the modified PUTCHAR.OBJ file to your application use the */
- /* following Lx51 invocation: */
- /* Lx51 <your object file list>, PUTCHAR.OBJ <controls> */
- /* */
- /***********************************************************************/
- #include <reg51.h>
- #define XON 0x11
- #define XOFF 0x13
- /*
- * putchar (full version): expands '\n' into CR LF and handles
- * XON/XOFF (Ctrl+S/Ctrl+Q) protocol
- */
- char putchar (char c) {
- if (c == '\n') {
- if (RI) {
- if (SBUF == XOFF) {
- do {
- RI = 0;
- while (!RI);
- }
- while (SBUF != XON);
- RI = 0;
- }
- }
- while (!TI);
- TI = 0;
- SBUF = 0x0d; /* output CR */
- }
- if (RI) {
- if (SBUF == XOFF) {
- do {
- RI = 0;
- while (!RI);
- }
- while (SBUF != XON);
- RI = 0;
- }
- }
- while (!TI);
- TI = 0;
- return (SBUF = c);
- }
- #if 0 // comment out versions below
- /*
- * putchar (basic version): expands '\n' into CR LF
- */
- char putchar (char c) {
- if (c == '\n') {
- while (!TI);
- TI = 0;
- SBUF = 0x0d; /* output CR */
- }
- while (!TI);
- TI = 0;
- return (SBUF = c);
- }
- /*
- * putchar (mini version): outputs charcter only
- */
- char putchar (char c) {
- while (!TI);
- TI = 0;
- return (SBUF = c);
- }
- #endif
2. 打印函数的设计
如何是C99的编译器可以使用下面的方式来设计调试信息的打印,遗憾的是keil51只支持C89的编译器,不能使用__VA_ARGS__
宏
- #ifdef _DEBUG_
- #define DEBUG 1
- #else
- #define DEBUG 0
- #endif
- #define debug_print(fmt, ...) \
- do { if (DEBUG) fprintf(stderr, "%s:%d:%s(): " fmt, __FILE__, \
- __LINE__, __func__, __VA_ARGS__); } while (0)
或者
- #ifdef _DEBUG_
- #define DEBUG 1
- #else
- #define DEBUG 0
- #endif
- #define debug_print(fmt, ...) \
- do { if (DEBUG) printf("%s:%d:%s(): " fmt, __FILE__, \
- __LINE__, __func__, __VA_ARGS__); } while (0)
v1--单参数宏
- #define DRV_DEBUG 1
- #if DRV_DEBUG
- #define DRV_PRINT(x) printf(x)
- #else
- #define DRV_PRINT(x)
- #endif
这个版本的DRV_PRINT(x)只能输出单变量——纯字符串,
- void foo()
- {
- DRV_PRINT("Driver Initialize Success!");
- }
不需要打印调试信息时,更改DRV_DEBUG宏定义
- #define DRV_DEBUG 0
当然也可以直接这样定义
v2--指定参数宏
- #define DRV_DEBUG 1
- #if DRV_DEBUG
- #define DRV_PRINT(fmt, val1, val2) printf(fmt, val1, val2)
- #else
- #define DRV_PRINT(fmt, val1, val2)
- #endif
如果只需要打印一个变量,第2
个参数用随意值填位,如
- void foo()
- {
- DRV_PRINT("Driver Initialize Success: ver %d !", val1, 2);
- }
类似,如果有4个参数,就:
- void foo()
- {
- DRV_PRINT("Driver Initialize Success: ver %d !", val1, 2, 3, 4);
- }
这种方式使用起来比较麻烦,而且受参数个数的限制,比较笨的方案,但是没办法:(,VxWorks 5.5
内核代码里就是这样干的!
v3--参数数量可变宏
通过下面这种定义,使用的时候需要两个括号,虽然两个括号使用起来有些麻烦,但是比V2的多个参数还是相对好点。
- #define _MSG_LOG_
- #ifdef _MSG_LOG_
- #define LOG(exp) printf exp
- #else
- #define LOG(exp)
- #endif
- void foo()
- {
- LOG(("Driver Initialize Success: ver %d.%d !", 1, 2));
- }
v4--参数数量可变宏,直接使用宏定义为函数
keil51的log打印方式,但是调用宏时不能换行,使用‘\’换行也不行
- #define _MSG_LOG_
- #ifdef _MSG_LOG_
- #define LOG printf
- #else
- #define LOG /##/
- #endif
- void foo()
- {
- LOG("Driver Initialize Success: ver %d.%d !", 1, 2);
- }
每种方式都有优缺点,选择适合自己项目的方式。
3. 调试信息log输出分层
在处理不同的模块,不同的重要程度的信息,我们就需要实现分层的设计。下面试linux的分层级别,不过对于keil51这种级别的设计8种分层过于多,其中的大部分级别是用不到的,
定义出来,也就没有意义,只是作为参考。
linux kernel的日志级别
#define KERN_EMERG "<0>" /* system is unusable */
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */
下面提供两种分层方式,使用v2--指定参数宏
或者v3--参数数量可变宏
都是可以的。
1. 如果分层比较多的可以参考下面的设计方式来做分层设计1
- /*-----------------------------------------------------------------------------
- * message level config
- *-----------------------------------------------------------------------------*/
- #define MSG_EMERG 0x01 //system is unusable
- #define MSG_ERR 0x02 //error conditions
- #define MSG_WARNING 0x04 //warning conditions
- #define MSG_INFO 0x08 // informational
- #define MSG_DEBUG 0x10 // debug-level messages
- #define MSG_ALL 0xFF //All message output
- /*-----------------------------------------------------------------------------
- * Maros name: MSG_PRINT
- * Description: print message through UART port
- * Input: flag MSG_EMERG MSG_ERR ... MSG_DEBUG
- * fmt string same as printf string
- * val[1:4] print variable.if val3,val4 don't have variable,input 3,4 instead of it.
- * Other:
- * example1
- * msg_printf(MSG_DEBUG,"DEBUG: demo_value is %d\n", demo_value,2,3,4);
- * example2
- * msg_printf(MSG_DEBUG,"DEBUG: data1 is %d,data2 is %d,data3 is %d,data4 is %d\n",
- * data1,
- * data2,
- * data3,
- * data4);
- *-----------------------------------------------------------------------------*/
- #ifdef _MSG_ENABLE_
- #define MSG_PRINT(flag, fmt,val1,val2,val3,val4) \
- do{\
- if(flag & _MSG_LEVEL_){\
- printf(fmt,val1,val2,val3,val4);}\
- }while(0)
- #else
- #define MSG_PRINT(flag, fmt, val1, val2, val3, val4)
- #endif
-
2. 如果认为不需要这么多分层那么可以简单的使用下面的方式。
- #define _MSG_ERR_
- #define _MSG_DEBUG_
- #define _MSG_LOG_
- /*-----------------------------------------------------------------------------
- * Maros name:
- * Description: print Assertion through UART port
- * set define _MSG_ERR_, _MSG_DEBUG_, _MSG_LOG_in file IR_sensor.h
- * example: MSG_ERR(("MSG_ERR: output %d\n", val"));
- *-----------------------------------------------------------------------------*/
- #ifdef _MSG_ERR_
- #define ERR(exp) printf exp
- #else
- #define ERR(exp)
- #endif
- #ifdef _MSG_DEBUG_
- #define DBG(exp) printf exp
- #else
- #define DBG(exp)
- #endif
- #ifdef _MSG_LOG_
- #define LOG(exp) printf exp
- #else
- #define LOG(exp)
- #endif
-
4. printf,sprintf,vprintf的选择
在keil51的标准输入输出库<stdio.h>中,打印的函数有上面的三个printf,sprintf,vprintf,vsprintf那么该如何选择使用那个呢?
<Cx51 User's Guide>函数的定义
1. printf 和 sprintf都是可变参数打印
2. vprintf 和 vsprintf需要配合<stdarg.h>
使用。在需要实现自己的打印函数时可以使用。
例如
- #include <stdio.h>
- #include <stdarg.h>
- void error (char *fmt, ...) {
- va_list arg_ptr;
- va_start (arg_ptr, fmt); /* format string */
- vprintf (fmt, arg_ptr);
- va_end (arg_ptr);
- }
- void tst_vprintf (void) {
- int i;
- i = 1000;
- /* call error with one parameter */
- error ("Error: '%d' number too large\n", i);
- /* call error with just a format string */
- error ("Syntax Error\n");
- }
<stdarg.h>说明
名稱 | 描述 | 相容 |
---|---|---|
va_list | 用來保存宏va_arg与宏va_end所需信息 | C89 |
stdarg.h
巨集[编辑]
名稱 | 描述 | 相容 |
---|---|---|
va_start | 使va_list 指向起始的參數 | C89 |
va_arg | 檢索參數 | C89 |
va_end | 釋放va_list | C89 |
va_copy | 拷貝va_list 的內容 | C99 |
使用参考代码
- #include <stdio.h>
- #include <stdarg.h>
- void printargs(int arg1, ...) /* 輸出所有int型態的參數,直到-1結束 */
- {
- va_list ap;
- int i;
- va_start(ap, arg1);
- for (i = arg1; i != -1; i = va_arg(ap, int))
- printf("%d ", i);
- va_end(ap);
- putchar('\n');
- }
- int main(void)
- {
- printargs(5, 2, 14, 84, 97, 15, 24, 48, -1);
- printargs(84, 51, -1);
- printargs(-1);
- printargs(1, -1);
- return 0;
- }
更多的内容参考wiki百科
3. sprintf 和 vsprintf使用起来比较危险,都是输出到buff中的,需要注意buff的大小,以防buff爆掉误写入其他的地方,造成程序崩掉。
对打印的内容做加一些处理,可以使用这两个函数。
例如:
可以在打印error时默认在前面加入"Error: "
- int error(char *fmt, ...)
- {
- int result;
- va_list args;
- va_start(args, fmt);
- sprintf(stderr,"Error: ");
- result = vsprintf(stderr, fmt, args);
- va_end(args);
- return result;
- }
5. ‘\r’ 添加
一般我们在写printf时都是用'\n'来回到下一行的开始,但是在不同的系统'\n'的行为不一致。
参考下面的'\r', '\n'的说明,我们需要自动在'\n'之前加入'\r'.
一般实现方式有两种。
1) 通过putchar方式
参考keil51中的code
- /*
- * putchar (basic version): expands '\n' into CR LF
- */
- char putchar (char c) {
- if (c == '\n') {
- while (!TI);
- TI = 0;
- SBUF = 0x0d; /* output CR */
- }
- while (!TI);
- TI = 0;
- return (SBUF = c);
- }
这样看起来比较清楚点
- char putchar(char c)
- {
- if (c == '\n')
- {
- uart_send_byte('\r');
- }
- uart_send_byte(c);
- return (c);
- }
2) 通过spprintf, svprintf将打印出来的字符串中的 '\n'之前加入'\r'.
- void rlprintf (const char *fmt, ...)
- {
- va_list args;
- U8 byLen,i;
- U8 printbuffer[CONFIG_PRINT_SIZE] = {0};
- va_start (args, fmt); /* For this to work, printbuffer must be larger than * anything we ever want to print. */
- byLen = vsprintf ((S8*)printbuffer, fmt, args);//if the length of fmt > buffer size,vsprintf will return -1
- va_end (args) ;/* Print the string */
- while (i< byLen)
- {
- if (printbuffer[i] == '\n')
- {
- putchar('\r');
- }
- putchar(printbuffer[i]);
- i++;
- }
- }
\r,\n,\r\n的区别
1、\n 软回车:
在Windows 中表示换行且回到下一行的最开始位置。相当于Mac OS 里的 \r 的效果。
在Linux、unix 中只表示换行,但不会回到下一行的开始位置。
2、\r 软空格:在linux、unix 中表示返回到当行的最开始位置。
在Mac OS 中表示换行且返回到下一行的最开始位置,相当于Windows 里的 \n 的效果。
3、\t 跳格(移至下一列)。它们在双引号或定界符表示的字符串中有效,在单引号表示的字符串中无效。
\r\n 一般一起用,用来表示键盘上的回车键,也可只用 \n。
\t表示键盘上的“TAB”键。
就像你使用 enter和shift+enter的区别4、文件中的换行符号:
linux,unix: \r\n
windows : \n
Mac OS : \r
5、常用转义符号的意义:
\n LF或ASCII中的0x0A(10)
\r CR或ASCII中的0x0D(13)
\t 水平制表符-HT或ASCII中的0x09(9)
\\ 反斜杠
\$ 美圆符
\" 双引号
\' 单引号
参考资料: