目录
一、引言
二、所用软件
1、串口调试工具
2、虚拟串口软件
3、Keil μVision5
三、软件设置
1、串口调试助手软件设置
2、虚拟串口软件设置
3、Keil C51设置
A、调试(.ini)文件建立
B、Debug调试引入虚拟串口设置
C、COM1和COM2虚拟串口连接配对原理图
四、仿真实例介绍
1、UART配置程序段
2、发送程序段
3、中断接收程序段
4、延时程序段
5、P3初始化程序段
6、MAIN函数段
五、仿真结果
六、已调程序
七、软件使用提示
新近发现用Proteus配Keil for C51编译器调试UART串口收发通讯,发送没有问题,接收总是有问题,输入RXD的波形、波特率以及串口配置都是对的,但AT89C51就是不接收数据,RI不置位,也不能进入串口中断4程序,同样的程序移到Keil μVision5里,调试就妥妥的,收发自如,网上也看了别人的帖子和文章,大都是用Proteus自带的Virtual Termimal看到收到调试助手借助虚拟串口发的数据,但并不代表AT89C51真真收到了数据,拿网上贴出来的程序测试,也同样不能进入接收中断程序,RI不置位,严重怀疑Proteus的AT89C51模块有小问题,有同样问题或解决了该问题的高手,请回帖讨论。这里只实例介绍一下Keil仿真AT89C51串口收发数据的详细过程,相互学习,共同进步。
SSCOM是一款很不错的串口调试软件,收发数据行云流水不卡顿,就是界面略显花哨,不过还是要感谢开发者,不停地迭代更新,给大家带来的惊喜连连,在这里就顺带声援支持一下。
Virtual Serial Port Driver 是由 Electronic Team 开发的软件包,其功能如同 Windows机器上的虚拟 COM 端口仿真器。 该软件可以创建虚拟 COM 端口,能使用虚拟零调制解调器电缆成对连接,所创建的每个虚拟串口都将与串行应用程序进行通信,就好像它们是实际物理端口一样。
Keil C51编译器自1988年引入市场以来成为事实上的行业标准,并支持超过500种8051变种。Keil公司在2005年被ARM收购,通过这次收购,Keil将能更好地向高速发展的32位微控制器市场提供完整的解决方案,同时继续在μVision环境下支持8051和C16x编译器。
SSCOM在本例中设置:1)9600波特率,10位一帧,0起始位+8位数据+1停止位,无校验;2)加时间戳和分包显示,分包以大于等于一段完整的收发数据为宜;3)视程序设计定时重复发送数据给Keil中预设的虚拟串口。
VSPD设置分3步:1)选好配对虚拟串口;2)点击添加虚拟串口对;3)生成配对好的虚拟串口对,一发一收,就像实际连接在一起的串口鸳鸯对。
1)先在Keil项目文件夹目录下面建立一个“.ini”文件,第一行写入指定AT89C51侧的虚拟串口号,并配置好串口(如波特率9600等信息);2)第二行指定AT89C51侧的虚拟串口输入输出(本例为COM1),写好后保存文件(本例起名为debug_C51.ini)。两行语句模式如下:
MODE COMx baudrate, parity, databits, stopbits
ASSIGN COMx
1)点击μVision5的魔法棒;2)在Debug页面把上面A步建立好的debug_C51.ini添加进来。
串口参数配置函数,本例配置为9600波特率,1位停止位,8位数据位,无校验;定时器Timer1以定时模式工作在方式2:8位常数自动装入定时器;允许串口接收数据REN=1,打开串口中断ES=1及总中断EA=1,具体看下面代码。
/* 串口参数配置函数,这里配置为9600波特率,1位停止位,8位数据位,无校验 */
void uart_init(void)
{
TMOD = 0x20; //Timer1以定时模式工作在方式2:8位常数自动装入定时器/计数器
SCON = 0x40; //SM0=0,SM1=1=1,方式1,10位UART "0 D0~D7 1",波特率可变
REN = 1; //允许串口接收数据位
TH1 = 0xFD; //9600波特率:晶振的频率/(12*(256-初值))=波特率*32
TL1 = 0xFD; //方式2的TH1,TL1是相等的,TL1自动重装TH1初值
PCON = 0x00; //SMOD=0波特率不加倍
IE = 0x90; //允许总中断,允许串口中断,禁止其他中断
PS = 0; //设置串行口中断优先级
TR1 = 1; //当GATE=0,TR1置“1” 启动定时器1
}
字符串发送函数被调用后,该函数再进一步调用字符发送函数。
/* send_char函数, ch为待发送的字符 */
void send_char(uint8_t ch)
{
SBUF = ch; //SBUF是指串行口同地址的两个缓冲寄存器,一个是发送寄存器,一个是接收寄存器,
while (!TI); //等待一个字符(一帧)发送完毕
TI = 0; //TI软件清零,等待下一次发送后的置位
}
/* send_string函数, *str为待发送的字符串首元素地址 */
void send_string(uint8_t *str)
{
while (*str != '\0') //字符串里的数据发送不到结尾
{
send_char(*str); //调用字符发送函数
str++; // 指针指向下一个待发字符
}
}
从COM1接收外部串口调试助手通过虚拟串口COM2发来的数据,保存进RXD_Buf字符数组中,添加好字符串结束字符'\0',并把正确的接收状态赋给Buf_Flag标志,如果输入超过32个字节,标记为溢出BufOverflow,正确接收带结束符\r\n或\n\r数据的话,Buf_Flag标记为BufReady。
/*串口中断接收数据函数 */
void uart_interrupt(void) interrupt 4
// 中断默认优先级:0外部中断0 > 1定时/计数器0 > 2外部中断1 > 3定时/计数器1 > 4串行中断;
{
static uint8_t n = 0; //一次性接收输入字符串字符个数计数,静态变量只在编译时赋一次初值
if (TI==1) //如果是发送进入中断,中断直接返回
{
TI = 0; //TI清0
return;
}
if (RI==1) //RI置位表示一帧数据接收完毕,RI必须用软件清0
{
RI = 0; //中断标志位清零
ES = 0; //关闭串口中断,预防而已,防止下面程序执行时被打断
if (n < BufSize - 1) // 接收缓冲区未溢出
{
RXD_Buf[n++] = SBUF; //读入的数据依次放入RXD_Buf中
if (((RXD_Buf[n - 2] == R_SYM) && (RXD_Buf[n - 1] == N_SYM))
\||((RXD_Buf[n - 2] == N_SYM) && (RXD_Buf[n - 1] == R_SYM)))
// 判断是否接收到结束符(回车 "\r\n"或者"\r\n")
{
Buf_Flag = BufReady; // 成功接收到带结束标志符的一串字符
RXD_Buf[n] = '\0'; // 在字符串末尾补字符数组(串)结束标识
n = 0; //n清0
}
}
else // 接收缓冲区溢出
{
Buf_Flag = BufOverflow; // 串口接收标志:溢出
n = 0; //n清0
}
ES = 1; //打开串口中断
}
}
延时函数,没什么好解释的。
/* 延时函数 */
void delay(uint16_t i)
{
uint8_t j;
for(;i>0;i--)
for(j=0;j<125;j++)
;
}
这段代码非必须
/* P3串口初始化函数 */
void uart_P3_init(void)
{
P3 = 0x0F; //P3.0 P3.1用作串口的RXD和TXD
}
声明自己是虚拟串口COM1,打个招呼之后,等待输入的数据就绪标记BufReady,没输入的话Buf_Flag = BufEmpty,提示通过串口提示助手通过COM2输入内容,只提示一次,有输入之后,把输入的数据再返还给COM2,并提示是你刚输入的内容,如果一次性输入超过32Byte,提示溢出标记BufOverflow。
/* main 主函数*/
void main()
{
uint8_t i = 0;
Buf_Flag = BufEmpty; //接收缓冲区标志赋初值
uart_P3_init(); //初始化P3口
uart_init(); //初始化串口
ES = 0; //发送前关闭串口中断,以防发送完每个字符都没必要地进入中断程序
send_string("I am AT89C51 Serial COM1!\r\n"); //发送字符串
ES = 1; //发送完打开串口中断
while (1)
{
if (Buf_Flag == BufEmpty) //如果还没有收到COM2的数据,发送请求输入提示
{
ES = 0; //发送前关闭串口中断,以防发送完每个字符都没必要地进入中断程序
send_string("\r\nPlease input form Serial Assistant(<32Bytes)!\r\n");
ES = 1; //发送完打开串口中断
Buf_Flag = WaitInput; //请求输入提示只发送一次
}
else if (Buf_Flag == BufReady) //如果收到并存入了RXD_Buf中
{
ES = 0; //发送前关闭串口中断
send_string("Your Input: "); //提示你刚才输入了什么
send_string(RXD_Buf); // 把接收到的字符串发送回去
i = 0; //i计数清零
while (i < BufSize) //接收缓冲区清零
{
RXD_Buf[i++] = 0; //清零RXD_Buf
}
Buf_Flag = BufEmpty; //串口接收标志:空
ES = 1; //处理完打开串口中断
}
else if (Buf_Flag == BufOverflow)
{
ES = 0; //发送前关闭串口中断
send_string("ERROR:RXD_Buf is full!\r\n"); // 提示缓冲区溢出
delay(2000); //按需忽略之后再从COM2输入的内容
i = 0; //i计数清零
while (i < BufSize) //接收缓冲区清零
{
RXD_Buf[i++] = 0; //清零RXD_Buf
}
Buf_Flag = BufEmpty; //串口接收标志:空
ES = 1; //处理完打开串口中断
}
}
}
串口调试助手收发标注如下,实心的菱形表示串口助手收到的内容,空心的菱形表示串口助手发出的内容,关键设置也在下面的截图中做了标注。
上面的图是停下了串口调试助手翻回去截的图,动态的截屏(闪电GIF)如下:
完整的程序代码如下,头文件reg52.h没有贴出来。
/* Main.c file generated by Baoyufei
* UART AT89C51 Frequency 11.0592MHz
* receive data and tranfer them back
* Created: Saturday April 19, 2022
* Processor: AT89C51
* Compiler: Keil μVision5
* Version: v1.0
*/
#include
#include
// 类型重定义,便于移植
typedef unsigned char uint8_t;
typedef unsigned int uint16_t;
// 定义串口接收标志枚举类型
enum
{
BufEmpty= 0, // 接收缓冲区为空
BufOverflow , // 接收缓冲区溢出
BufReady, // 成功接收到结束标志符
WaitInput
} Buf_Flag; // 串口接收标志
#define BufSize 32 // 定义接收缓冲区大小为 32字节
// 定义结束符为回车符 "\r(回车符)+\n(换行符)"
#define R_SYM '\r'
#define N_SYM '\n'
// 外部可见函数声明
void uart_P3_init(void);
void uart_init(void);
void send_char(uint8_t ch);
void send_string(uint8_t *str);
void delay(uint16_t i);
// 定义串口接收缓冲区
uint8_t RXD_Buf[BufSize] = {0};
/* main 主函数*/
void main()
{
uint8_t i = 0;
Buf_Flag = BufEmpty; //接收缓冲区标志赋初值
uart_P3_init(); //初始化P3口
uart_init(); //初始化串口
ES = 0; //发送前关闭串口中断,以防发送完每个字符都没必要地进入中断程序
send_string("I am AT89C51 Serial COM1!\r\n"); //发送字符串
ES = 1; //发送完打开串口中断
while (1)
{
if (Buf_Flag == BufEmpty) //如果还没有收到COM2的数据,发送请求输入提示
{
ES = 0; //发送前关闭串口中断,以防发送完每个字符都没必要地进入中断程序
send_string("\r\nPlease input form Serial Assistant(<32Bytes)!\r\n");
ES = 1; //发送完打开串口中断
Buf_Flag = WaitInput; //请求输入提示只发送一次
}
else if (Buf_Flag == BufReady) //如果收到并存入了RXD_Buf中
{
ES = 0; //发送前关闭串口中断
send_string("Your Input: "); //提示你刚才输入了什么
send_string(RXD_Buf); // 把接收到的字符串发送回去
i = 0; //i计数清零
while (i < BufSize) //接收缓冲区清零
{
RXD_Buf[i++] = 0; //清零RXD_Buf
}
Buf_Flag = BufEmpty; //串口接收标志:空
ES = 1; //处理完打开串口中断
}
else if (Buf_Flag == BufOverflow)
{
ES = 0; //发送前关闭串口中断
send_string("ERROR:RXD_Buf is full!\r\n"); // 提示缓冲区溢出
delay(2000); //按需忽略之后再从COM2输入的内容
i = 0; //i计数清零
while (i < BufSize) //接收缓冲区清零
{
RXD_Buf[i++] = 0; //清零RXD_Buf
}
Buf_Flag = BufEmpty; //串口接收标志:空
ES = 1; //处理完打开串口中断
}
}
}
/* 延时函数 */
void delay(uint16_t i)
{
uint8_t j;
for(;i>0;i--)
for(j=0;j<125;j++)
;
}
/* 串口参数配置函数,这里配置为9600波特率,1位停止位,8位数据位,无校验 */
void uart_init(void)
{
TMOD = 0x20; //Timer1以定时模式工作在方式2:8位常数自动装入定时器/计数器
SCON = 0x40; //SM0=0,SM1=1=1,方式1,10位UART "0 D0~D7 1",波特率可变
REN = 1; //允许串口接收数据位
TH1 = 0xFD; //9600波特率:晶振的频率/(12*(256-初值))=波特率*32
TL1 = 0xFD; //方式2的TH1,TL1是相等的,TL1自动重装TH1初值
PCON = 0x00; //SMOD=0波特率不加倍
IE = 0x90; //允许总中断,允许串口中断,禁止其他中断
PS = 0; //设置串行口中断优先级
TR1 = 1; //当GATE=0,TR1置“1” 启动定时器1
}
/* P3串口初始化函数 */
void uart_P3_init(void)
{
P3 = 0x0F; //P3.0 P3.1用作串口的RXD和TXD
}
/* send_char函数, ch为待发送的字符 */
void send_char(uint8_t ch)
{
SBUF = ch; //SBUF是指串行口同地址的两个缓冲寄存器,一个是发送寄存器,一个是接收寄存器,
while (!TI); //等待一个字符(一帧)发送完毕
TI = 0; //TI软件清零,等待下一次发送后的置位
}
/* send_string函数, *str为待发送的字符串首元素地址 */
void send_string(uint8_t *str)
{
while (*str != '\0') //字符串里的数据发送不到结尾
{
send_char(*str); //调用字符发送函数
str++; // 指针指向下一个待发字符
}
}
/*串口中断接收数据函数 */
void uart_interrupt(void) interrupt 4
// 中断默认优先级:0外部中断0 > 1定时/计数器0 > 2外部中断1 > 3定时/计数器1 > 4串行中断;
{
static uint8_t n = 0; //一次性接收输入字符串字符个数计数,静态变量只在编译时赋一次初值
if (TI==1) //如果是发送进入中断,中断直接返回
{
TI = 0; //TI清0
return;
}
if (RI==1) //RI置位表示一帧数据接收完毕,RI必须用软件清0
{
RI = 0; //中断标志位清零
ES = 0; //关闭串口中断,预防而已,防止下面程序执行时被打断
if (n < BufSize - 1) // 接收缓冲区未溢出
{
RXD_Buf[n++] = SBUF; //读入的数据依次放入RXD_Buf中
if (((RXD_Buf[n - 2] == R_SYM) && (RXD_Buf[n - 1] == N_SYM))
\||((RXD_Buf[n - 2] == N_SYM) && (RXD_Buf[n - 1] == R_SYM)))
// 判断是否接收到结束符(回车 "\r\n"或者"\r\n")
{
Buf_Flag = BufReady; // 成功接收到带结束标志符的一串字符
RXD_Buf[n] = '\0'; // 在字符串末尾补字符数组(串)结束标识
n = 0; //n清0
}
}
else // 接收缓冲区溢出
{
Buf_Flag = BufOverflow; // 串口接收标志:溢出
n = 0; //n清0
}
ES = 1; //打开串口中断
}
}
已调试程序如下:
本实例使用软件纯属调试所需,绝无商业宣传意图;
另外提醒,商业用途请大家使用正版软件,尊重知识版权。