Keil51 log设计

任何软件编程都需要有调试信息输出来进行,软件的调试。

在做keil51软件项目时,一般使用UART或者RS232输出调试信息。 

那如何输出适合自己项目的调试信息,有下面几个方案可以参考。

1. 重定向打印

首先,需要调试好uart输出接口,可以正常输出字符。

如果只需要打印字符串而不需要打印变量参数,那么就比较简单。只需要像如下这样打印:

  1. void uart_send_str(char * pstr)
  2. {
  3.     unsigned int len;
  4.     
  5.     len = strlen(pstr);
  6.     while(len--)
  7.     {
  8.         uart_send_byte(pstr);
  9.     }
  10. }

 

但是,这样设计只能适用简单的设计,如果设计稍微复杂点就需要打印参数的数值。而上面的方式没有办法完成这样的需求。

那么就需要使用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
  1. #include <stdio.h>
  2.  
  3. char putchar (
  4.   char c);   /* character to output */
Description

The putchar function transmits the character c using the serial port.

Note

  • This routine is implementation-specific and its function may deviate from that described above. Source code is provide in the LIB folder. You may modify the source to customize this function for your particular hardware environment.
Return Value

The putchar routine returns the character output, c.

 

现在只考虑打印调试信息,获取字符不用管。在code中加入这段代码就可以实现重定向,但是不要忘记#include <stdio.h>

  1. char putchar(char c)
  2. {
  3.     uart_send_bayte(c);
  4.     return c;
  5. }

 参考keil安装文件内的定义function

Keil\C51\LIB\PUTCHAR.C

  1. /***********************************************************************/
  2. /*                                                                     */
  3. /*  PUTCHAR.C:  This routine is the general character output of C51.   */
  4. /*  You may add this file to a uVision2 project.                       */
  5. /*                                                                     */
  6. /*  To translate this file use C51 with the following invocation:      */
  7. /*     C51 PUTCHAR.C <memory model>                                    */
  8. /*                                                                     */
  9. /*  To link the modified PUTCHAR.OBJ file to your application use the  */
  10. /*  following Lx51 invocation:                                         */
  11. /*     Lx51 <your object file list>, PUTCHAR.OBJ <controls>            */
  12. /*                                                                     */
  13. /***********************************************************************/
  14.  
  15. #include <reg51.h>
  16.  
  17. #define XON  0x11
  18. #define XOFF 0x13
  19.  
  20.  
  21. /*
  22.  * putchar (full version):  expands '\n' into CR LF and handles
  23.  *                          XON/XOFF (Ctrl+S/Ctrl+Q) protocol
  24.  */
  25. char putchar (char c)  {
  26.  
  27.   if (== '\n')  {
  28.     if (RI)  {
  29.       if (SBUF == XOFF)  {
  30.         do  {
  31.           RI = 0;
  32.           while (!RI);
  33.         }
  34.         while (SBUF != XON);
  35.         RI = 0; 
  36.       }
  37.     }
  38.     while (!TI);
  39.     TI = 0;
  40.     SBUF = 0x0d;                         /* output CR  */
  41.   }
  42.   if (RI)  {
  43.     if (SBUF == XOFF)  {
  44.       do  {
  45.         RI = 0;
  46.         while (!RI);
  47.       }
  48.       while (SBUF != XON);
  49.       RI = 0; 
  50.     }
  51.   }
  52.   while (!TI);
  53.   TI = 0;
  54.   return (SBUF = c);
  55. }
  56.  
  57.  
  58.  
  59. #if 0         // comment out versions below
  60.  
  61. /*
  62.  * putchar (basic version): expands '\n' into CR LF
  63.  */
  64. char putchar (char c)  {
  65.   if (== '\n')  {
  66.     while (!TI);
  67.     TI = 0;
  68.     SBUF = 0x0d;                         /* output CR  */
  69.   }
  70.   while (!TI);
  71.   TI = 0;
  72.   return (SBUF = c);
  73. }
  74.  
  75.  
  76. /*
  77.  * putchar (mini version): outputs charcter only
  78.  */
  79. char putchar (char c)  {
  80.   while (!TI);
  81.   TI = 0;
  82.   return (SBUF = c);
  83. }
  84.  
  85. #endif

 

 

2. 打印函数的设计

如何是C99的编译器可以使用下面的方式来设计调试信息的打印,遗憾的是keil51只支持C89的编译器,不能使用__VA_ARGS__

  1. #ifdef _DEBUG_
  2. #define DEBUG   1
  3. #else
  4. #define DEBUG   0
  5. #endif
  6.  
  7. #define debug_print(fmt, ...) \
  8.         do { if (DEBUG) fprintf(stderr, "%s:%d:%s(): " fmt, __FILE__, \
  9.         __LINE__, __func__, __VA_ARGS__); } while (0)​ 

或者

  1. #ifdef _DEBUG_
  2. #define DEBUG 1
  3. #else
  4. #define DEBUG 0
  5. #endif
  6. #define debug_print(fmt, ...) \
  7.     do { if (DEBUG) printf("%s:%d:%s(): " fmt, __FILE__, \
  8.     __LINE__, __func__, __VA_ARGS__); } while (0)​

 

 

v1--单参数宏

 和不使用<stdio.h>库,自己写打印字符串输出一样,这个方式没有参考价值。
  1. #define DRV_DEBUG 1
  2. #if DRV_DEBUG
  3.     #define DRV_PRINT(x) printf(x)
  4. #else
  5.     #define DRV_PRINT(x) 
  6. #endif 

这个版本的DRV_PRINT(x)只能输出单变量——纯字符串,

 
  1. void foo()
  2. {
  3.     DRV_PRINT("Driver Initialize Success!");
  4. } 

不需要打印调试信息时,更改DRV_DEBUG宏定义

 
  1. #define DRV_DEBUG 0

当然也可以直接这样定义

 

 

v2--指定参数宏

 
  1. #define DRV_DEBUG 1
  2. #if DRV_DEBUG
  3.     #define DRV_PRINT(fmt, val1, val2) printf(fmt, val1, val2)
  4. #else
  5.     #define DRV_PRINT(fmt, val1, val2) 
  6. #endif 

如果只需要打印一个变量,第2个参数用随意值填位,如

 
  1. void foo()
  2. {
  3.     DRV_PRINT("Driver Initialize Success: ver %d !", val1, 2);
  4. } 

类似,如果有4个参数,就:

 
  1. void foo()
  2. {
  3.     DRV_PRINT("Driver Initialize Success: ver %d !", val1, 2, 3, 4);
  4. } 

这种方式使用起来比较麻烦,而且受参数个数的限制,比较笨的方案,但是没办法:(,VxWorks 5.5内核代码里就是这样干的!

 

v3--参数数量可变宏

 通过下面这种定义,使用的时候需要两个括号,虽然两个括号使用起来有些麻烦,但是比V2的多个参数还是相对好点。

  1. #define _MSG_LOG_
  2.  
  3. #ifdef _MSG_LOG_
  4. #define LOG(exp) printf exp
  5. #else
  6. #define LOG(exp)
  7. #endif 
  8.  
  9. void foo()
  10. {
  11.     LOG(("Driver Initialize Success: ver %d.%d !", 1, 2));
  12. }

 

v4--参数数量可变宏,直接使用宏定义为函数

keil51的log打印方式,但是调用宏时不能换行,使用‘\’换行也不行

  1. #define _MSG_LOG_
  2.  
  3. #ifdef _MSG_LOG_
  4. #define LOG     printf
  5. #else
  6. #define LOG     /##/
  7. #endif 
  8.  
  9. void foo()
  10. {
  11.     LOG("Driver Initialize Success: ver %d.%d !", 1, 2);
  12. }

 

 每种方式都有优缺点,选择适合自己项目的方式。

 

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

  1. /*-----------------------------------------------------------------------------
  2.  * message level config
  3.  *-----------------------------------------------------------------------------*/
  4. #define MSG_EMERG 0x01 //system is unusable
  5. #define MSG_ERR 0x02 //error conditions 
  6. #define MSG_WARNING 0x04 //warning conditions 
  7. #define MSG_INFO 0x08 // informational
  8. #define MSG_DEBUG 0x10 // debug-level messages 
  9. #define MSG_ALL 0xFF //All message output
  10.  
  11. /*-----------------------------------------------------------------------------
  12.  * Maros name: MSG_PRINT
  13.  * Description: print message through UART port
  14.  * Input: flag MSG_EMERG MSG_ERR ... MSG_DEBUG
  15.  * fmt string same as printf string
  16.  * val[1:4] print variable.if val3,val4 don't have variable,input 3,4 instead of it.
  17.  * Other: 
  18.  * example1
  19.  * msg_printf(MSG_DEBUG,"DEBUG: demo_value is %d\n", demo_value,2,3,4);
  20.  * example2
  21.  * msg_printf(MSG_DEBUG,"DEBUG: data1 is %d,data2 is %d,data3 is %d,data4 is %d\n", 
  22.  * data1,
  23.  * data2,
  24.  * data3,
  25.  * data4);
  26.  *-----------------------------------------------------------------------------*/
  27. #ifdef _MSG_ENABLE_
  28. #define MSG_PRINT(flag, fmt,val1,val2,val3,val4) \
  29.                 do{\
  30.                     if(flag & _MSG_LEVEL_){\
  31.                     printf(fmt,val1,val2,val3,val4);}\
  32.                 }while(0)
  33. #else
  34. #define MSG_PRINT(flag, fmt, val1, val2, val3, val4)
  35. #endif
  36.  

2. 如果认为不需要这么多分层那么可以简单的使用下面的方式。

  1. #define  _MSG_ERR_
  2. #define _MSG_DEBUG_
  3. #define _MSG_LOG_
  4.  
  5. /*-----------------------------------------------------------------------------
  6.  * Maros name: 
  7.  * Description: print Assertion through UART port
  8.  * set define _MSG_ERR_, _MSG_DEBUG_, _MSG_LOG_in file IR_sensor.h
  9.  * example: MSG_ERR(("MSG_ERR: output %d\n", val"));
  10.  *-----------------------------------------------------------------------------*/
  11. #ifdef _MSG_ERR_
  12. #define ERR(exp) printf exp
  13. #else
  14. #define ERR(exp)
  15. #endif
  16. #ifdef _MSG_DEBUG_
  17. #define DBG(exp) printf exp
  18. #else
  19. #define DBG(exp)
  20. #endif
  21. #ifdef _MSG_LOG_
  22. #define LOG(exp) printf exp
  23. #else
  24. #define LOG(exp)
  25. #endif
  26.  

 

4. printf,sprintf,vprintf的选择

在keil51的标准输入输出库<stdio.h>中,打印的函数有上面的三个printf,sprintf,vprintf,vsprintf那么该如何选择使用那个呢?

<Cx51 User's Guide>函数的定义

 

1. printf 和 sprintf都是可变参数打印

2. vprintf 和 vsprintf需要配合<stdarg.h>​使用。在需要实现自己的打印函数时可以使用。

例如

  1. #include <stdio.h>
  2. #include <stdarg.h>
  3.  
  4. void error (char *fmt, ...) {
  5.   va_list arg_ptr;
  6.   va_start (arg_ptr, fmt); /* format string */
  7.   vprintf (fmt, arg_ptr);
  8.   va_end (arg_ptr);
  9. }
  10.  
  11. void tst_vprintf (void) {
  12.   int i;
  13.  
  14.   i = 1000;
  15.  
  16.   /* call error with one parameter */
  17.   error ("Error: '%d' number too large\n", i);
  18.  
  19.   /* call error with just a format string */
  20.   error ("Syntax Error\n");
  21. }

 

<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

使用参考代码

  1. #include <stdio.h>
  2. #include <stdarg.h>
  3.  
  4. void printargs(int arg1, ...) /* 輸出所有int型態的參數,直到-1結束 */
  5. {
  6.   va_list ap;
  7.   int i;
  8.  
  9.   va_start(ap, arg1); 
  10.   for (= arg1; i != -1; i = va_arg(ap, int))
  11.     printf("%d ", i);
  12.   va_end(ap);
  13.   putchar('\n');
  14. }
  15.  
  16. int main(void)
  17. {
  18.    printargs(5, 2, 14, 84, 97, 15, 24, 48, -1);
  19.    printargs(84, 51, -1);
  20.    printargs(-1);
  21.    printargs(1, -1);
  22.    return 0;
  23. }

更多的内容参考wiki百科

 

3. sprintf 和 vsprintf使用起来比较危险,都是输出到buff中的,需要注意buff的大小,以防buff爆掉误写入其他的地方,造成程序崩掉。

 对打印的内容做加一些处理,可以使用这两个函数。

例如:

 

 可以在打印error时默认在前面加入"Error: "

  1. int error(char *fmt, ...)
  2. {
  3.     int result;
  4.     va_list args;
  5.     va_start(args, fmt);
  6.     sprintf(stderr,"Error: ");
  7.     result = vsprintf(stderr, fmt, args);
  8.     va_end(args);
  9.     return result;
  10. } 

 

5. ‘\r’ 添加

一般我们在写printf时都是用'\n'来回到下一行的开始,但是在不同的系统'\n'的行为不一致。

参考下面的'\r', '\n'的说明,我们需要自动在'\n'之前加入'\r'.

一般实现方式有两种。

1) 通过putchar方式

参考keil51中的code

  1. /*
  2.  * putchar (basic version): expands '\n' into CR LF
  3.  */
  4. char putchar (char c)  {
  5.   if (== '\n')  {
  6.     while (!TI);
  7.     TI = 0;
  8.     SBUF = 0x0d;                         /* output CR  */
  9.   }
  10.   while (!TI);
  11.   TI = 0;
  12.   return (SBUF = c);
  13. }

这样看起来比较清楚点

  1. char putchar(char c)
  2. {
  3. if (== '\n')
  4. {
  5. uart_send_byte('\r');
  6. }
  7. uart_send_byte(c);
  8. return (c);
  9. }

2) 通过spprintf, svprintf将打印出来的字符串中的 '\n'之前加入'\r'.

  1. void rlprintf (const char *fmt, ...)
  2. {
  3. va_list args;
  4. U8 byLen,i;
  5.     U8 printbuffer[CONFIG_PRINT_SIZE] = {0};
  6.     
  7. va_start (args, fmt); /* For this to work, printbuffer must be larger than * anything we ever want to print. */
  8.     byLen = vsprintf ((S8*)printbuffer, fmt, args);//if the length of fmt > buffer size,vsprintf will return -1
  9. va_end (args) ;/* Print the string */
  10.  
  11.      while (i< byLen)
  12.      {
  13.      if (printbuffer[i] == '\n')
  14.      {
  15.      putchar('\r');
  16.      }
  17.             
  18.          putchar(printbuffer[i]);
  19.          i++;
  20.      }
  21. }

 

\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)
\\  反斜杠
\$  美圆符
\"  双引号
\'  单引号

 

 

参考资料:

 

posted @ 2017-06-29 10:04  jason_leeee  阅读(1714)  评论(1编辑  收藏  举报