printf调试到串口信息输出,是程序调试的重要方法之一。
51单片机自带串口资源, 需要提前配置好。基本原理:对printf调用的putchar函数重新定向。也就是自定义一个putchar函数
**********************首先需要知道的几个要点:***************************
printf调用前需要加入标准库头文件 #include""
printf原理:根据用户提供的格式化的字符串,可变参数,构造出一个最终输出字符串。之后再调用标准库的putchar函数打印输出信息。
既然是输出到串口, 那么, 至少需要配置最基本的串口功能
**************************************************************************
正文:
配置和头文件.h 并在这两个文件中实现putchar的重新定向。
#ifndef _UART_H
#define _UART_H
void UartDriver(); //串口调用,需要在主循环中调用。负责读写数据,并调用其他函数中的UartAction
void ConfigUART(uint16_t baud); //调用T1 配置波特率的起始重载值TH1,TL1
void UartRxMonitor(uint8_t ms); //ms 时间间隔, 大于该时间认为是数据结束。并将flagFram置一表示帧接收完成。
void UartWrite(uint8_t *buf, uint8_t len); //串口数据写入, buf待发数据,len发送长度,依次发送buf[] 标志位TI = flagTxd
uint8_t UartRead(uint8_t *buf, uint8_t len); //串口数据读取, buf接收指针,设定的len读取长度(可大于接收到的长度),依次写入buf[](interrupt4)并返回更新后的len(接收到的字符长度)
/**printf输出到串口数据部分*************************************
文件的最后附带了重新定向的putchar函数, 一般是被注释掉的,
如果需要printf输出到串口数据, 则恢复该段代码即可。
*/
char putchar(char c); //重新定向putchar
#endif
/*=======================
适用于51单片机的UART串口文件
1.使用前需要配置波特率,在configUart中传递波特率形参, 串口模式只能用8位数据位1位起始位1位停止位, 如需更改在本文件的configUart函数中
发送:直接调用下面的函数
void UartWrite(uint8_t *buf, uint8_t len)
接收:
接收前需要在一个精准的定时器中断服务函数中调用数据接收完整监视函数UartRxMonitor(uint8_t ms),
建议使用1ms的定时器, 在里面直接传递1即可
并需要在while中调用UartDrive, 不断的检测完整接收标志。
接收到的数据,位于UartDrive函数的buf数组中。
如果有需要相关的动作, 应放在该函数中执行。
eg:
extern void UartAction(uint8 *buf, uint8 len);
并将UartAction(uint8 *buf, uint8 len)函数放在UartDrive中来执行。
============================*/
#define _UART_C
#include ""
#include ""
bit flagFrame = 0; //帧接收完成标志,即接收到一帧新数据
bit flagTxd = 0; //单字节发送完成标志,用来替代 TXD 中断标志位
uint8_t cntRxd = 0; //接收字节计数器 只对接收有作用, 接收完一帧完整的数据后(指定数量的字节) 将归零。
uint8_t pdata bufRxd[64]; //接收字节缓冲区
extern void UartAction(uint8_t *buf, uint8_t len);
/* 串口配置函数,baud-通信波特率 */
/* 函数作用:配置串口的基本参数
内容:
1.通过将SCON串行控制寄存器配置为01 01 0000 将串口通讯模式设置为SM0=0 SM1=1的模式1,
即1位起始位,8位数据位1位停止位。同时清零TI和RI(发送和接收的中断标志位)
**********SCON寄存器位分配说明************************
位 : 7 6 5 4 3 2 1 0
符号:SM0 SM1 SM2 REN TB8 RB8 TI RI
*****************************************************
2.配置TMOD 定时器模式寄存器,由于串口默认使用定时器1,因此TMOD &= 0x0F;将高4位清零,同时T0部分不变
接着配置高四位为0010,即M1=1,M0=0的工作模式2
**************TMOD定时器模式寄存器********************
位 : 7 6 5 4 3 2 1 0
符号:GATE C/T M1 M0 GATE C/T M1 M0
高4位用于T1, 第4位用于T0
******************************************
**************TMOD定时器模式M1/M0工作模式********************
M1 M0 工作模式 描述
0 0 0 兼容 8048 单片机的 13 位定时器,THn 的 8 位和 TLn 的 5 位组成一个 13 位定时器
0 1 1 THn 和 TLn 组成一个 16 位的定时器。
1 0 2 8位自动重装模式,定时器溢出后THn重装到TLn中(实际上只有TLn做加1计数,THn值不变,TLn溢出后TFn即置一, 同时将存在THn中的数据重载进TLn中)
1 1 3 禁用定时器1,定时器0变成两个8位定时器
******************************************
3.根据晶振配置波特率,并计算重载值
4. 根据下表,禁止T1中断,使能串口中断ES, 并启动T1.
***************IE 中断使能寄存器位分配****************
位 : 7 6 5 4 3 2 1 0
符号: EA -- ET2 ES ET1 EX1 ET0 EX0
****************************************************
位 符号 描述
7 EA 总中断使能位,相当于总开关
6 -- --
5 ET2 定时器 2 中断使能
4 ES 串口中断使能
3 ET1 定时器 1 中断使能
2 EX1 外部中断 1 使能
1 ET0 定时器 0 中断使能
0 EX0 外部中断 0 使能
*/
void ConfigUART(uint16_t baud)
{
SCON = 0x50; //配置串口为8位数据位,1位停止位和1位起始位模式。 同时将TI,RI清零。
TMOD &= 0x0F; //清零 T1 的控制位
TMOD |= 0x20; //配置 T1 为模式 2
TH1 = 256 - (11059200 / 12 / 32) / baud; //计算 T1 重载值,并将其存在TH1中
TL1 = TH1; //将存在TH1中的数值赋值给TL1,以供其第一次启动用。
ET1 = 0; //禁止 T1 中断
ES = 1; //使能串口中断
TR1 = 1; //启动 T1
}
/* 串口数据写入,即串口发送函数,buf-待发送数据的指针,len-指定的发送长度 */
/* uint8_t *buf传递待发送的数据指针 eg: uint8_t str[8] , 传递&str
uint8_t len传递要发送的字节的长度(数量)
如果是字符串, 则要加一位/0位
发送时利用指针指向要发送的第一个字节的位置,然后*取指(即取该位置上的值) 赋值给SBUF寄存器发送。
接着利用while来监测发送发送完成(发送完成后, 会清空SBUF发送寄存器,同时TI置一(这里传递给flagTxd置一))
发送完成后, 在继续发送下一位。 也就是说, 是一位一位发送的。
*/
void UartWrite(uint8_t *buf, uint8_t len)
{
while (len--) //循环发送所有字节
{
flagTxd = 0; //清零发送标志
SBUF = *buf++; //发送一个字节数据
while (!flagTxd); //等待该字节发送完成
}
}
/* 串口数据读取函数,buf-接收指针,len-指定的读取长度,返回值-实际读到的长度 */
/*
串口接收时被动行为, 在UartConfig函数中使能串口中断后, 当有一个字节的数据(只能是一个字节)被放在SBUF接收寄存器后, 将触发串口中断服务函数, 并进入该函数。
在串口中断服务函数中, 将自动完成完整的帧数据写入临时接收字节缓存区BufRxd数组中的过程。
因此, 在调用该函数前, 应已经完成了写入BufRxd(接收数据缓存)的过程。
调用时:
uint8_t *buf传递接收位置的数据指针 eg: uint8_t str[8] , 传递&str
uint8_t len传递要接收的字节的长度(数量)
cntRxd在中断服务函数中记录接收到字节数量。当接收到的字节数量长度小于指定读取的长度时,
也就是说, 要读取的长度大于接收到的字节数量, 需要对指定的长度进行一下调整。否则, 会读到缓冲区中的随机数据
同时由于有这个机制, 可以将要读取的数据写的比实际大一些, 保证读完全部信息。
接着从字节临时缓存数组中的第0位开始, 依次将数据连续传递出去。
*/
unsigned char UartRead(uint8_t *buf, uint8_t len)
{
uint8_t i;
if (len > cntRxd) //指定读取长度大于实际接收到的数据长度时,
{ //读取长度设置为实际接收到的数据长度
len = cntRxd;
}
for (i = 0; i < len; i++) //拷贝接收到的数据到接收指针上
{
*buf++ = bufRxd[i];
}
cntRxd = 0; //接收计数器清零
return len; //返回实际读取长度
}
/* 串口接收监控,由空闲时间判定帧结束,需在定时中断中调用,ms-定时间隔 */
/*由中断服务函数和串口接收函数可知, 串口接收函数, 必须在中断服务函数将全部完整的一帧数据接收完毕后才能启动
因此,这里需要一个监视函数, 来确定是否已经完整的接收了一帧数据
该函数, 只有在接收到数据后(即中断服务函数中的接收部分,已经开始启动cntRxd用于计数后才会进入)
因为中断服务函数中的cntRxd已经启动,因此肯定是大于0的, 而第一次进入这个函数的时候, cntbkp的初始值为0,因此在最开始的时候, 将cntRxd的值不断的赋给
cntbkp,直到cntRxd不再变化, 此时 cntbkp的值肯定是等于cntRxd, 因此进行下面的判断, 判断是否大于30ms
由于涉及微秒定时, 因此该函数应在T0的中断服务函数中调用。 如果T0的中断服务函数定的是1ms, 那么传递的值就是1,
如果是10微秒, 传递的值就是10 , 即利用定时器的精准定时来在精准的时间里进入UartRxMonitor函数来监视。
*/
void UartRxMonitor(uint8_t ms)
{
static uint8_t cntbkp = 0;
static uint8_t idletmr = 0;
if (cntRxd > 0) //接收计数器大于零时,监控总线空闲时间
{
if (cntbkp != cntRxd) //接收计数器改变,即刚接收到数据时,清零空闲计时
{
cntbkp = cntRxd;
idletmr = 0;
}
else //接收计数器未改变,即总线空闲时,累积空闲时间
{
if (idletmr < 30) //空闲计时小于 30ms 时,持续累加
{
idletmr += ms;
if (idletmr >= 30) //空闲时间达到 30ms 时,即判定为一帧接收完毕
{
flagFrame = 1; //设置帧接收完成标志
}
}
}
}
else
{
cntbkp = 0;
}
}
/* 串口驱动函数,监测数据帧的接收,调度功能函数,需在主循环中调用 */
/*
该函数放在while中不断的检测 是否有完整的数据接收完毕。
如果有, 就将数据存在buf【40】中, 由于接收函数有自己调整长度的能力(详见接收函数部分)
因此这里预留足够大的位置即可。
*/
void UartDriver()
{
uint8_t len;
uint8_t pdata buf[40];
if (flagFrame) //有命令到达时,读取处理该命令
{
flagFrame = 0;
len = UartRead(buf, sizeof(buf)); //将接收到的命令读取到缓冲区中
UartAction(buf, len); //传递数据帧,调用动作执行函数
}
}
/* 串口中断服务函数 */
/*
中断查询序列
*******************************************************
**中断** ** ** 中断 ** 中断 **默认**
**函数**中断名称**中断标志位**使能位**向量地址**优先**
**编号** ** ** ** **级别**
*******************************************************
0 外部中断0 IE0 EX0 0x003 1
1 T0中断 TF0 ET0 0x00B 2
2 外部中断1 IE1 EX1 0x0013 3
3 T1中断 TF1 ET1 0x001B 4
4 Uart中断 TI/RI ES 0x0023 5
5 T2中断 TF2/EXF2 ET2 0x002B 6
*******************************************************
使能UART的中断服务函数后,将会自动监测RI/TI标志位。当被置一的时候, 会自动进入中断服务函数。
发送: 发送的中断服务比较简单, 只要有数据被放在SBUF中, 就会触发发送中断的TI位, 进而将flagTxd置一。
发送行为是主动的。 调用发送函数时UartWrite(),会将要发送的内容依次放在SBUF中。
接收:接收行为是被动的。 首先需要使能串口中断服务( 在ConfigUART()函数中启动),这样只要有数据被放在SBUF的接收寄存器中,
就会触发中断服务函数,并将RI位置一。
SBUF只能放一个字节, 因此,每次触发都需要及时将SBUF中的内容取出去, 否则会被冲掉。每次接收的个单个字节数据, 将利用计数器存在一个临时数组中bufRxd中
单需要注意的是, 计数器不断自加的过程中, 不能大于临时数组的大小,也就是说一帧数据不能太长,字节数不能超过缓冲区的大小。 当一帧数据完整的接收完毕后, cntRxd将会被归0.
*/
void InterruptUART() interrupt 4
{
if (RI) //接收到新字节
{
RI = 0; //清零接收中断标志位
if (cntRxd < sizeof(bufRxd)) //接收缓冲区尚未用完时,
{ //保存接收字节,并递增计数器
bufRxd[cntRxd++] = SBUF;
}
}
if (TI) //字节发送完毕
{
TI = 0; //清零发送中断标志位
flagTxd = 1; //设置字节发送完成标志
}
}
/* *************************************************
print用, 重新定向的putchar函数,
作用: 将printf内容打印到串口并实现输出。
*/
char putchar(char c)
{
SBUF = c;
while(!TI);
TI = 0;
return c;
}
在主程序用加入 并配置好串口的波特率,然后直接用printf打印信息即可
/*****
printf 在51中的实现
基于STC89C52, 串口波特率9600, 晶振:11.0592MHz
*/
#include""
#include""
#include""
void main()
{
ConfigUART(9600);
printf("hallo printf function in ");
while(1);
}