1.串口通信编程
STC-ISP串口助手的使用:
文本模式和HEX模式的区别:文本模式就是那些可打印的字符。HEX模式就是这些可打印字符对应的16进制。它们都对应相同的ASCII码(用十进制表示)。
- 很多小白在程序编写完成后调试时会搞不清楚到底是程序问题还是上位机传送的数据问题,比如在习题3中,到底写成cmd == 1 还是 cmd == '1' 需要简单考虑一下。
代码心得:
- 当我们想让串口中打印的字符串换行,并且对齐时,需要在字符串后面加上\r\n,注意\r和\n缺一不可,它不像我们写C语言程序时只用转义字符'\n'代表换行,这里是回车加换行的意思。
1、发送中断请求标志位TI:(习题1中使用了TI)
- 使用原因:在习题1中,我们通过不断往SBUF寄存器写入1个字符的方法来输出字符串,但是发现在给SBUF连续写入字符时没有延时的话PC端不能显示一整个字符串,只能显示一个字符。回忆我们上一节刚刚讲过串行移位寄存器。串口在输出数据时,先往8位的输出数据缓冲寄存器SBUF中写入数据(1个字符),然后再放到串行输出移位寄存器中,由它将二进制数顺序发送给PC。我们需要注意的是,我们在用单片机给PC发送数据时,单片机先把数据扔到SBUF中,然后经过移位,串口数据移位的过程是需要时间的,所以我们不能以非常快的速度把数据一股脑扔到SBUF中。那么每往SUBF写入一个字符后,隔多少时间进行下一次写入比较合适呢(延时有点多的话,比如说10ms,那么PC上打印的字符串会出现刷屏的现象)?答:查阅手册,我们发现有比延时更好用的东西,就是SCON寄存器的发送中断请求标志位TI,可以控制串口数据的移位时间。
- 原理:目前不使用串口的中断,而是利用了串口中断触发的特点(硬件底层的电气特性),也就是串行发送数据的第8位结束后硬件会自动将TI“置1”。回忆超声波测距传感器demo,我们可以用空循环体等待串行发送数据第8位结束的方法来间接表示一位数据的发送时间,最后软件复位TI。这样就不需要知道你串行发送完一个字节数据需要多少时间,就可以解决PC上打印的字符串刷屏的现象。
习题1(向PC发送字符串):【项目工程文件夹】
- 思路:
全局变量: 1.sfr指令直接找到AUXR寄存器: sfr AUXR = 0X8E; //因为AUXR没有在reg52.h中声明
1. 调用API2. 初始化串口: UartInit(); 2. while死循环,每隔一秒通过串口给PC发送一个字符串 2.1 调用API1,软件延时1s: Delay1000ms(); 2.2 调用API4,给输出数据缓冲寄存器SBUF发送一条字符串: sendString("hello shuaige\r\n");
/* 一级函数:f1、f2、f4 */ f1. 封装软件延时1s的API,用于每隔一秒给串口缓冲寄存器发送字符串: void Delay1000ms(); f2. 封装初始化串口的API: void UartInit(void); f2.1 禁用ALE信号: AUXR = 0X01; f2.2 让串口以方式1工作(8位UART,可变波特率),并允许串口接收: SCON = 0x50; f2.3 让定时器1以8位重载工作模式工作: TMOD &= 0xDF; TMOD |= 0x20; f2.4 根据波特率为9600,波特率不翻倍,设置定时器1的初值: TH1 = 0xFD; TL1 = 0xFD; f2.5 定时器开始数数: TR1 = 1; f4. 封装给PC发送字符串的API: void sendString(char *str); //形参是字符串的地址 f4.1 定义一个字符指针变量p用来保存字符串首地址: char *p = str; f4.2 while循环,控制循环的变量是*p,当*p != '\0' 时,进入循环,进行单个字符的发送 f4.2.1 通过指针间接访问字符串字符,再调用API3. 发送单个字符: sendByte(*p); f4.2.2 修改循环变量p的值,让指针p偏移: p++;
/* 二级函数:f3 */ f3. 封装定时给PC发送一个字符的API: void sendByte(char data_msg); //形参是字符值 f3.1 往SBUF寄存器中写入字符data_msg: SBUF = data_msg; f3.2 根据串口发送中断触发位TI,利用空循环体暂停程序: while(TI == 0); f3.3 程序复位TI: TI = 0;
- 代码:
#include "reg52.h" #include "intrins.h" sfr AUXR = 0x8E; /* API1. 软件延时1s,用于每隔一秒给串口缓冲寄存器发送字符串 */ void Delay1000ms(); /* API2. 初始化串口 */ void UartInit(void); /* API3. 软件延时10ms,用于给串行移位寄存器移动数据预留时间(弃用) */ void Delay10ms(); /* API3. 通过串口定时给PC发送一个字符 */ void sendByte(char data_msg); /* API4. 通过串口给PC发送一个字符串 */ void sendString(char *str); void main(void) { UartInit(); while(1){ Delay1000ms(); sendString("hello shuaige\r\n"); } } void Delay1000ms() //@11.0592MHz { unsigned char i, j, k; _nop_(); i = 8; j = 1; k = 243; do { do { while (--k); } while (--j); } while (--i); } void UartInit(void) //9600bps@11.0592MHz { AUXR = 0x01; SCON = 0x50; //8位UART,允许串口接收 TMOD &= 0xDF; TMOD |= 0x20; //定时器8位重载工作模式 TH1 = 0xFD; TL1 = 0xFD; //9600波特率初值 TR1 = 1; } void Delay10ms() //@11.0592MHz { unsigned char i, j; i = 18; j = 235; do { while (--j); } while (--i); } void sendByte(char data_msg) { SBUF = data_msg; // Delay10ms(); while(TI == 0); TI = 0; } void sendString(char *str) { char *p = str; while(*p != '\0'){ sendByte(*p); p++; } }
2、接收中断请求标志位RI:
- 使用原因:我们上一节刚刚讲过串行移位寄存器。串口在接收数据时,PC先将二进制数顺序发送给串行输入移位寄存器,再由它把数据顺序移动到输入数据缓冲寄存器SBUF中,最后串口从缓冲区接收到数据。同样的,数据经过移位是耗费时间的。所以我们想知道PC什么时候已经完成“通过串口将数据输入给单片机”这件事。而用“接收中断请求标志位RI”来帮助单片机判断是否接收到1字节数据。
- 原理:目前不使用串口的中断,而是利用了串口中断触发的特点(硬件底层的电气特性),也就是串行接收数据的第8位结束后硬件会自动将RI“置1”。所以我们可以用查询法来判断一个字符是否已经通过串行移位寄存器移动到SBUF寄存器,最后软件复位RI。
习题2(PC发送字符指令给单片机):PC通过串口点亮单片机LED灯【项目工程文件夹】
- tip1【SCON寄存器的REN“置1”后单片机才能接收数据】:我们在串口初始化函数中已经设置好了
- 思路:在习题1的基础上修改代码
全局变量 | 增加 1. sbit指令找到P3这个I/O口组的第7位P3^7,也就是D5这个LED: sbit ledD5 = P3^7;
main函数 | 修改 1. 调用API2. 初始化串口: UartInit(); 2. 让D5先灭: ledD5 = 1; 3. while死循环,每隔一秒通过串口给PC发送一个字符串,并且从PC接收一个字符数据 3.1 调用API1,软件延时1s: Delay1000ms(); 3.2 调用API4,给输出数据缓冲寄存器SBUF发送一条字符串: sendString("hello shuaige\r\n"); 3.3 用“接收中断请求标志位RI”来帮助单片机判断是否接收到1字节数据,判据是RI == 1 3.3.1 如果是,说明已经接收到1位数据 从输入数据缓冲寄存器SBUF中接收数据,保存在变量cmd中: cmd = SBUF; 如果cmd是'o',则点亮D5,如果cmd是'C',则关闭D5; 3.4 在接受到1字节数据后,程序复位RI: RI = 0;
- 代码:
#include "reg52.h" #include "intrins.h" sfr AUXR = 0x8E; sbit ledD5 = P3^7; /* API1. 软件延时1s,用于每隔一秒给串口缓冲寄存器发送字符串 */ void Delay1000ms(); /* API2. 初始化串口 */ void UartInit(void); /* API3. 通过串口给PC发送一个字符 */ void sendByte(char data_msg); /* API4. 通过串口给PC发送一个字符串 */ void sendString(char *str); void main(void) { char cmd; UartInit(); ledD5 = 1; while(1){ Delay1000ms(); sendString("hello shuaige\r\n"); if(RI == 1){ cmd = SBUF; if(cmd == 'o'){ ledD5 = 0; }else if(cmd == 'c'){ ledD5 = 1; } } RI = 0; } } void Delay1000ms() //@11.0592MHz { unsigned char i, j, k; _nop_(); i = 8; j = 1; k = 243; do { do { while (--k); } while (--j); } while (--i); } void UartInit(void) //9600bps@11.0592MHz { AUXR = 0x01; SCON = 0x50; //8位UART,允许串口接收 TMOD &= 0xDF; TMOD |= 0x20; //定时器8位重载工作模式 TH1 = 0xFD; TL1 = 0xFD; //9600波特率初值 TR1 = 1; } void sendByte(char data_msg) { SBUF = data_msg; // Delay10ms(); while(TI == 0); TI = 0; } void sendString(char *str) { char *p = str; while(*p != '\0'){ sendByte(*p); p++; } }
2.串口中断
1、为什么需要串口中断?:
- 在习题2中,经过测试可以发现电脑通过串口点亮单片机的LED有点延迟,这是因为我们让串口处于全双工通信状态下时,每隔一秒无脑给PC发送数据的同时又要对从PC那里接收到的数据进行判断,这导致PC的数据即使已经被保存在输入数据缓冲寄存器SBUF中了,但是单片机因为还在数数(软件延时)没有马上去取这个数据。
- 类比于感应开关盖垃圾桶项目中利用外部中断记住振动传感器的信号,我们需要在单片机串口接收到PC的数据后,立马挂起串口发送的程序,先去响应接收中断,再进行串口发送。
- 你可能在有点工作经验的人口中听到“心跳包”这个词,或者hearbeat,意思就是让单片机每隔1秒给PC(或者服务器、上位机)发送一个字符或者一个字符串的心跳包,代表单片机“没死”。然后让我们的具体业务放在另一个“线程”当中。
2、串口中断配置:
- 串口中断触发方式:SCON寄存器的TI 和 RI,分别是串口发送中断请求标志(transfer interrupt) 和 串口接收中断请求标志(receive interrupt)。另外我们可以发现串口中断程序没有细分为发送中断还是接收中断,因此需要人为的在中断处理程序根据用串口中断触发方式进行判断。
- 串口中断号: 4 ,查阅51芯片手册第6.1小节的表6-1。
- 串口中断开关:先在51芯片手册中找到中断系统结构图示意图(6.1小节),发现串口对应的中断开关有ES和EA,再在手册中查找ES(enable serial),我们发现它是IE寄存器的第4位,是串行口1中断允许位
习题3(PC串口中断发送字符给单片机):【项目工程文件夹】
- 思路:在习题2的基础上修改
全局变量 | 增加 1. 定义一个用于接收PC通过串口发送来的字符的全局变量cmd: char cmd; //cmd的传递路线为:SBUF ——> 串口中断(中断4)
main函数 | 修改 1. 调用API2. 初始化串口: UartInit(); 2. 让D5先灭: ledD5 = 1; 3. while死循环,每隔一秒通过串口给PC发送一个字符串 3.1 调用API1,软件延时1s: Delay1000ms(); 3.2 调用API4,给输出数据缓冲寄存器SBUF发送一条字符串: sendString("hello shuaige\r\n");
中断: 中断4: 封装串口中断的中断服务程序, void Uart_Routine() interrupt 4 4.1 中断处理程序中,对于接收中断的响应,判据是RI == 1 4.1.1 在接受到1字节数据后,程序复位RI: RI = 0; 4.1.2 从输入数据缓冲寄存器SBUF中接收数据,保存在变量cmd中: cmd = SBUF; 4.1.3 对指令cmd进行判断: 如果cmd是'o',则点亮D5,如果cmd是'C',则关闭D5; 4.2 中断处理程序中,对于发送中断的响应,判据是TI == 1 暂时不做任何事情
/* 一级函数:f1、f2、f4 */ | 修改f2 f2. 封装初始化串口的API: void UartInit(void); | 增加开启串口中断的语句 f2.1 禁用ALE信号: AUXR = 0X01; f2.2 让串口以方式1工作(8位UART,可变波特率),并允许串口接收: SCON = 0x50; f2.3 让定时器1以8位重载工作模式工作: TMOD &= 0xDF; TMOD |= 0x20; f2.4 根据波特率为9600,波特率不翻倍,设置定时器1的初值: TH1 = 0xFD; TL1 = 0xFD; f2.5 定时器开始数数: TR1 = 1; f2.6 开启串口中断: EA = 1; ES = 1;
- 代码:
#include "reg52.h" #include "intrins.h" sfr AUXR = 0x8E; sbit ledD5 = P3^7; char cmd; /* API1. 软件延时1s,用于每隔一秒给串口缓冲寄存器发送字符串 */ void Delay1000ms(); /* API2. 初始化串口 */ void UartInit(void); /* API3. 通过串口给PC发送一个字符 */ void sendByte(char data_msg); /* API4. 通过串口给PC发送一个字符串 */ void sendString(char *str); void main(void) { UartInit(); ledD5 = 1; while(1){ Delay1000ms(); sendString("hello shuaige\r\n"); } } void Uart_Routine() interrupt 4 { /* 中断处理程序中,对于接收中断的响应 */ if(RI == 1){ RI = 0; cmd = SBUF; if(cmd == 'o'){ ledD5 = 0; }else if(cmd == 'c'){ ledD5 = 1; } } /* 中断处理程序中,对于发送中断的响应 */ if(TI == 1){ // 暂时不做任何事情 } } void Delay1000ms() //@11.0592MHz { unsigned char i, j, k; _nop_(); i = 8; j = 1; k = 243; do { do { while (--k); } while (--j); } while (--i); } void UartInit(void) //9600bps@11.0592MHz { AUXR = 0x01; SCON = 0x50; //8位UART,允许串口接收 TMOD &= 0xDF; TMOD |= 0x20; //定时器8位重载工作模式 TH1 = 0xFD; TL1 = 0xFD; //9600波特率初值 TR1 = 1; EA = 1; ES = 1; //开启串口中断 } void sendByte(char data_msg) { SBUF = data_msg; // Delay10ms(); while(TI == 0); TI = 0; } void sendString(char *str) { char *p = str; while(*p != '\0'){ sendByte(*p); p++; } }
3.串口支持单词型指令控制
1、可能遇到的复杂指令:今后可能会遇到几种数据格式,一些模块将这些指令通过串口传递给单片机
- open 或 close
- cmd-open 或 cmd-close
- c:1:open 或 c:0:close
2、一种串口指令处理方法:
- 程序中用字符数组存储串口接收到的字符串指令。
- 以第1种数据格式的指令为例,数据缓冲寄存器SBUF读取到的字符串存放在我们定义的字符数组中,但是顺序不一定从第0位开始存放。所以字符串指令“open”或“close”的位置是不确定的,因此不能用strcmp()函数来判断。
- 比如我们只关心字符串中有没有子串"op"或"en",或者有没有子串"cl"或"se"。所以我们使用strstr()函数。
总结:这种处理方法对于上述指令够用了,但是处理复杂指令时可能会出错。想要完美的解决这一问题需要比较厉害的C语言功底,写出来的代码量也会比较大。文章来源:https://www.toymoban.com/news/detail-441486.html
习题4(PC发送字符串指令给单片机):【项目工程文件夹】文章来源地址https://www.toymoban.com/news/detail-441486.html
- 思路:在习题3的基础上修改
宏定义 | 增加 1. 定义符号常量len,用它代表用来接收串口数据的字符数组的长度: #define len 12 全局变量 | 增加 1. 定义一个用于接收串口数据的字符数组cmd[]: char cmd[len]; //cmd[]的传递路线为:SBUF ——> 串口中断(中断4)
中断 | 修改中断4 中断4: 封装串口中断的中断服务程序, void Uart_Routine() interrupt 4 4.1 定义一个静态全局区的静态变量,用来表示数组cmd的下标: static int i = 0; 4.2 中断处理程序中,对于接收中断的响应,判据是RI == 1 4.2.1 在接受到1字节数据后,程序复位RI: RI = 0; 4.2.2 从输入数据缓冲寄存器SBUF中接收数据,保存在数组cmd的第i个元素中: cmd[i] = SBUF; 4.2.3 偏移数组下标: i++; 4.2.4 判断字符数组cmd是否存满了,判据是 i == len 4.2.4.1 如果是,那么需要从头开始存放: i = 0; 4.2.4.2 否则,那么什么也不做,继续往下执行 //内在逻辑:由于cmd长度的限制,当字符串指令超过len时,我们需要覆盖掉原先的字符 4.2.5 判断当前字符串cmd中是否含有子串"en",判据是strstr(cmd,"en")!=NULL 4.2.5.1 如果是,说明我们大概率读到了open,代表点亮LED 点亮D5这个LED: ledD5 = 0; 既然已经完成命令,我们希望SBUF中的数据能够从头开始存放新的指令: i = 0; 清空cmd字符串的内容: memset(cmd, '\0', len); 4.2.5.2 否则,如果字符串cmd中含有子串"se",说明我们大概率读到了close 熄灭D5这个LED: ledD5 = 1; 既然已经完成命令,我们希望SBUF中的数据能够从头开始存放新的指令: i = 0; 清空cmd字符串的内容: memset(cmd, '\0', len); 4.3 中断处理程序中,对于发送中断的响应,判据是TI == 1 暂时不做任何事情
- 代码:
#include "reg52.h" #include "intrins.h" #include <string.h> #define len 12 sfr AUXR = 0x8E; sbit ledD5 = P3^7; char cmd[len]; /* API1. 软件延时1s,用于每隔一秒给串口缓冲寄存器发送字符串 */ void Delay1000ms(); /* API2. 初始化串口 */ void UartInit(void); /* API3. 通过串口给PC发送一个字符 */ void sendByte(char data_msg); /* API4. 通过串口给PC发送一个字符串 */ void sendString(char *str); void main(void) { UartInit(); ledD5 = 1; while(1){ Delay1000ms(); sendString("hello shuaige\r\n"); } } void Uart_Routine() interrupt 4 { static int i = 0; //静态全局区的变量 /* 中断处理程序中,对于接收中断的响应 */ if(RI == 1){ RI = 0; cmd[i] = SBUF; i++; if(i == len){ i = 0; } if(strstr(cmd,"en")!=NULL){ ledD5 = 0; i = 0; memset(cmd,'\0',len); }else if(strstr(cmd,"se")!=NULL){ ledD5 = 1; i = 0; memset(cmd,'\0',len); } } /* 中断处理程序中,对于发送中断的响应 */ if(TI == 1){ // 暂时不做任何事情 } } void Delay1000ms() //@11.0592MHz { unsigned char i, j, k; _nop_(); i = 8; j = 1; k = 243; do { do { while (--k); } while (--j); } while (--i); } void UartInit(void) //9600bps@11.0592MHz { AUXR = 0x01; SCON = 0x50; //8位UART,允许串口接收 TMOD &= 0xDF; TMOD |= 0x20; //定时器8位重载工作模式 TH1 = 0xFD; TL1 = 0xFD; //9600波特率初值 TR1 = 1; EA = 1; ES = 1; //开启串口中断 } void sendByte(char data_msg) { SBUF = data_msg; // Delay10ms(); while(TI == 0); TI = 0; } void sendString(char *str) { char *p = str; while(*p != '\0'){ sendByte(*p); p++; } }
到了这里,关于串口全双工通信与串口中断的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!