Harbin Institute of Technology Amateur Radio Club

利用串口打印变量快速调试

==

作者:王浩宇 Qrpucp

目录

一.简单实现

在C语言的学习过程中,我们经常会使用printf函数打印变量来进行程序调试,在单片机中我们同样可以这样操作。首先先来看代码。

serve.h

#ifndef __SERVE_H__
#define __SERVE_H__

/*
datatype
*/

typedef unsigned char uint8_t;
typedef unsigned int uint16_t;
typedef unsigned long uint32_t;

typedef signed char int8_t;
typedef signed int int16_t;
typedef signed long int32_t;

typedef float fp32;

typedef unsigned char bool_t;

/*
funtion:delay
explanation:
    model:stc89
    crystal frequency:11.0592MHz
*/

extern void delay_ms(uint32_t ms);

/*
funtion:for quick debugging
explanation:
        bps:9600
        timer1:mode2
        UART:mode1
        switch:#define _DEBUG_(before #include"serve.h")
        remember to use uart_init before!
*/

#ifdef _DEBUG_

#include <stdio.h>
#include <reg52.h>

#define debug_d8(x)\
    do{\
        TI = 1;\
        printf("%s\n", __FILE__);\
        printf("%s\n", __DATE__);\
        printf("%s\n", __TIME__);\
        printf("%s = %bd\n\n", #x, x);\
        while(!TI);\
        TI = 0;\
    }while(0U)

#define debug_d16(x)\
    do{\     
        TI = 1;\
        printf("%s\n", __FILE__);\
        printf("%s\n", __DATE__);\
        printf("%s\n", __TIME__);\
        printf("%s = %d\n\n", #x, x);\
        while (!TI);\
        TI = 0;\
    } while (0U)

#define debug_d32(x)\
    do{\
        TI = 1;\
        printf("%s\n", __FILE__);\
        printf("%s\n", __DATE__);\
        printf("%s\n", __TIME__);\
        printf("%s = %ld\n\n", #x, x);\
        while(!TI);\
        TI = 0;\
    }while(0U)

#define debug_f32(x)\
    do{\
        TI = 1;\
        printf("%s\n", __FILE__);\
        printf("%s\n", __DATE__);\
        printf("%s\n", __TIME__);\
        printf("%s = %f\n\n", #x, x);\
        while(!TI);\
        TI = 0;\
    }while(0U)

#else

#define debug_d8(x) 
#define debug_d16(x)
#define debug_d32(x)
#define debug_f32(x)

#endif	
			
					
/*
explanation:
        crystal frequency:11.0592MHz
        bps:9600
        timer1:mode2
        UART:mode1
*/

extern void uart_init(void);

#endif

serve.c

#include <reg52.h>
#include "serve.h"

void uart_init(void)
{
    TMOD = 0x20;    
    TH1 = 0xfd;     
    TL1 = 0xfd;     
    TR1 = 1;       
    REN = 1;       
    SM0 = 0;      
    SM1 = 1;       
    EA = 1;         					
    ES = 1;        
}

void delay_ms(uint32_t ms)
{
    uint32_t i,j;
    for( i = ms; i > 0; i--)
    {
        for( j = 110; j > 0; j--);
    }
}

main.c(demo)

#define _DEBUG_
#include <reg52.h>
#include "serve.h"

int main(void)
{
    uint8_t a = 1;
    uint16_t b = 2;
    uint32_t c = 3;
    fp32 d = 4.5;
    uart_init();
    while(1)
    {
        debug_d8(a);
        debug_d16(b);
        debug_d32(c);
        debug_f32(d);
        delay_ms(500);
    }
}

我们在电脑上打开串口助手,就可以看到下面图片这样的效果了(注意时间为代码编译的时间)。 串口打印变量

二.代码解释说明

1.只需要将这三个文件包含进工程,编译下载即可运行。我使用的单片机是STC89C52,晶振频率为11.0592MHz,使用串口1,波特率为9600。如果使用的是其他系列的单片机,只需要更改串口1初始化函数和头文件即可。

2.我是采用宏定义的方式实现打印变量,之所以这样做,是为了方便考虑。在程序中我们想要查看变量值的地方,只需要调用debug函数就可以了,而宏定义开关的优点就是,当我们不想调试时只需要把程序第一行的#define _DEBUG_注释掉即可,而不需要跑到程序里,把所有debug函数删掉。

3.但是串口初始化的函数我并没有使用宏定义,那是因为不少功能都需要串口的初始化,如果我们都写进宏定义里面,显然会造成重复定义。我们只需要把串口初始化的实现放到源文件中,然后在头文件中声明该函数,这样我们想要初始化串口只需要在主函数中调用该函数即可。

4.调用printf函数之前需要先把TI置一,否则程序就会卡在printf函数中,这个和getchar函数的实现有关,具体原因在第三部分说明。

5.头文件的宏定义中,作者使用的是do{...}while(0U)的格式,这样做是代码规范考虑,因为我们在主程序中需要调用debug_d8(x);语句,使用上述格式可以在不破坏每一条小语句的情况下,最后不会多出来一个分号。(不用这种格式,现在的编译器也会自动优化代码,不会报错)

三.重定向printf

高系列的51单片机,具有多个串口,像我们上面那样实现,只能用串口1进行输出,如果想要用其他串口输出变量,我们就需要对printf函数进行重定向。

printf函数文档

从keil的帮助文档里我们可以看到,printf是基于putchar实现的,所以我们只要重新实现putchar,就可以实现printf的重定向,即可以将printf用在其他串口上。


```c
#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判断,putchar函数总是要执行的语句其实只有三句,首先先等待上一个数据发送完毕,将标志位置零以后,再发送下一个数据。这也解释了为什么我们在第一次调用printf函数时要先把TI置1,因为STC单片机复位以后TI的值为0,直接调用printf函数就会一直卡死在while(!TI)里面了。

while(!TI);
TI = 0;
return (SBUF = c);

if(c == '\n')部分是判断是否接收到换行符,如果接收到换行符以后,就会输出CR+LF。SBUF = 0X0d是在输出CR,推测LF是在putchar函数之外输出的。

最后if(RI)部分是软件流控制。当接收端数据缓存区满了以后,就会向发送端发送XOFF标志,发送端接收到XOFF以后停止发送数据。接收端处理完数据以后,会向发送端发送XON标志,表示可以继续发送数据。

使用流控制可以有效的防止数据传输过程中的丢失情况。

分析完官方的putchar函数以后,我们需要自己写一个putchar函数来调用串口3,这里要说明一下,如果我们的工程中包含了putchar函数,编译器会优先使用我们所定义的函数,而不会去使用…/C51/LIB下的putchar函数。

下面是我写的putchar函数,因为没有那么高的要求,所以我并没有使用流控制,其次我的代码是先发送数据,再检测标志位,我认为这样做更符合我们平时的使用习惯。

char putchar(char c)
{
    if (c == '\n')
    {
        S3BUF = 0x0d;
        while (!(S3CON & S3TI));  //等待发送成功
        S3CON &= ~S3TI;
        /* output CR  */
    }
    S3BUF = c;
    while (!(S3CON & S3TI));	//等待发送成功
    S3CON &= ~S3TI;	  //清除发送中断标志
    return c;
}

我们只需要将这个putchar函数包含在工程中,然后初始化串口3,便可以利用printf打印串口了。如果想使用上面的debug函数,还需要对头文件进行更改。