速通串口通信
- 串口通信可以说是蓝桥杯单片机考察的最难的一个模块了,如果你是参加第十六届蓝桥杯的选手,可以在比赛前花上一点时间学习以下串口通信的函数实现,以防省赛突然蹦出串口通信来,机会是留给有准备的人的。
- 直接点击下方链接跳转到串口初始化函数开始学习即可,前面的寄存器不学对比赛没有影响。
串口初始化
一、串行口控制寄存器SCON
1.RI
RI的功能简单来说就是接收中断请求标志位:数据接收完成时由硬件置为1,需手动清零。
2.TI
TI的功能是发送中断请求标志位:数据发送完成时由硬件置为1,需手动清零。
3.SM0、SM1
对于SM0、SM1只需要知道当SM0、SM1=01时,串口工作在方式一(8位UART,波特率可变)。
4.REN
允许/禁止串行中断接收控制位,REN置1时允许接收中断,置0时则反之。
5.其余位
其余位与串口中断没有直接关系,统一置为0即可。
综上所述,对于SCON的配置为:SCON = 0x50;
二、电源控制器PCON
只需知道SMOD置0即可,其他不用管。
三、数据缓存器SBUF
如果读者有学过计算机组成原理这门课,那肯定对SBUF寄存器不陌生吧,在此只需知道SBUF为数据的临时中转站即可,无需过多了解。
- 串行发送时,CPU向SBUF写入数据,此时99H表示发送缓存SBUF。
- 串行接收时:CPU从SBUF读取数据,此时99H表示接收缓存SBUF。
- 数据发送:把数据扔进SBUF后,内核会自动将数据发送,内容发送完毕,TI标志位置1。
- 数据接收:内核从串口接收到一个完整数据后,会将RI标志置1,用户用SBUF读取即可。
四、中断允许寄存器IE
五、串口初始化函数
如果你还没理解前面的寄存器功能是没有关系的,速成只需要会使用ISP对串口进行配置即可,配置如下图所示:
定时器时钟配置1T还是12T目测没有太大影响,最好设置12T模式
再将复制后的代码进行以下处理:
void Uart1_Init(void) //9600bps@12.000MHz
{
SCON = 0x50; //8位数据,可变波特率
AUXR |= 0x01; //串口1选择定时器2为波特率发生器
AUXR |= 0x04; //定时器时钟1T模式
T2L = 0xC7; //设置定时初始值
T2H = 0xFE; //设置定时初始值
AUXR |= 0x10; //定时器2开始计时
ES = 1;
EA = 1;
}
六、字符串发送函数
1.方法一:不引用头文件(不推荐)
//单片机回复上位机(单个字节)
void SendByte(unsigned char dat)
{
SBUF = dat;
while(!TI);//死循环,等待字节发送
TI = 0;//字节发送完成后TI会置1,计时清0
}
//单片机回复上位机(字符串)
//写法一:如果你刚开始学习C语言,写法一比较好理解
void SendString(unsigned char *dat, unsigned char i)
{
unsigned char j;
for (j = 0; j < i; j++) // 遍历数组中的所有元素
{
SendByte(dat[j]); // 发送当前字节
}
}
//写法二
void SendString(unsigned char *dat)
{
while(*dat != '\0')//当发送数组未到达终止符时
SendByte(*dat++);//发送该字节,发送完成后数组往后移位
}
2.方法二:引用头文件
在stdio.h
头文件中,已经声明了putchar
和sprintf
这两个函数。
putchar
函数需要用户自己定义,在底层函数中格式如下:
extern char putchar(char ch)
{
SBUF = ch; // 将ch写入SBUF,发出数据
while (TI == 0);// 等待发送完成
TI = 0; // 清除发送完成标志
return ch;
}
该函数是标准库函数 printf 的底层实现依赖。在嵌入式系统中,printf 会调用 putchar 将字符逐个发送到串口。
所以你要单片机回复上位机某个字符串时,只需按照以下方式编写代码即可。
//串口处理函数
void UartProc()
{
//处理数据溢出代码
if(//上位机发送数据)
printf("HELLO!");
}
七、串口中断函数
//全局变量定义
idata unsigned char uart_rec[10];//数据缓存数组
idata unsigned char uart_rec_index; //数据缓存数组索引
void Uart1Server() interrupt 4
{
if(RI == 1)//串口数据接收完成
{
uart_rec[uart_rec_index] = SBUF;//读取数据缓存区的数据
uart_rec_index++;//移位
RI = 0;//重新接收
}
}
八、防止数据溢出判断
上面写的代码存在一个致命的缺点就是没有考虑数据溢出,也就是说,当数据接收数组存满后,再发送串口数据会导致溢出,所以还需要加入溢出判断。
memset
函数
调用该函数需要引入头文件#include <string.h>
函数调用格式:memset(*ptr,value,size)
*ptr
:要改变的数组
value
:改变后的值
size
:设置的字节数
例如:memset(arr,0,3);
表示将数组arr的第0位到第2位的数据赋值为0
也就是说,该函数等于以下代码:unsigned char i; for(i = 0; i < 3; i++) arr[i] = 0;
/*全局变量声明区*/
idata unsigned char uart_rec[10];//数据缓存存放数组
idata unsigned char uart_rec_index;//数据缓存存放数组索引
idata unsigned char systick;//滴答计时器
idata bit uart_flag;//处理数据标志位
//Uart初始化以及发送字节函数省略
void UartProc()//串口处理数据
{
//当未开始串口通信时,不执行该函数
if(!uart_rec_index)
return;
if(systick >= 10)//当滴答计时器计时10ms时,进行一次数据处理
{
systick = uart_flag = 0;
//串口执行操作(自行编写)
//执行完操作后,开始清除缓存区数据
memset(uart_rec,0,uart_rec_index);//清空缓存区
uart_rec_index = 0;//重置索引值
}
}
void Uart1_Server() interrupt 4
{
if(RI == 1)//接收到数据时
{
uart_flag = 1;//有数据传入,准备处理数据
uart_rec[uart_rec_index] = SBUF;
uart_rec_index++;
RI = 0;
if(uart_rec_index > 10)
{
memset(uart_rec,0,10);
uart_rec_index = 0;//防止数据溢出
}
}
}
void Timer1_Init(void) //1毫秒@12.000MHz
{
AUXR &= 0xBF; //定时器时钟12T模式
TMOD &= 0x0F; //设置定时器模式
TL1 = 0x18; //设置定时初始值
TH1 = 0xFC; //设置定时初始值
TF1 = 0; //清除TF1标志
TR1 = 1; //定时器1开始计时
ET1 = 1; //使能定时器1中断
EA = 1;
}
void Timer1_Isr(void) interrupt 3
{
if(uart_flag)//数据接收时,滴答计时器开始计时
systick++;
}
九、完整代码及应用
模块化编程,将UartInit()
和putchar()
函数放进Uart.c
文件中,并在Uart.h
中声明,在main.c
中引入头文件Uart.h
。
1.Uart.h
#ifndef __Uart_H__
#define __Uart_H__
void UartInit(void);
extern char putchar(char ch);
#endif
2.Uart.c
#include <STC15F2K60S2.H>
void Uart1_Init(void) //9600bps@12.000MHz
{
SCON = 0x50; //8位数据,可变波特率
AUXR |= 0x01; //串口1选择定时器2为波特率发生器
AUXR |= 0x04; //定时器时钟1T模式
T2L = 0xC7; //设置定时初始值
T2H = 0xFE; //设置定时初始值
AUXR |= 0x10; //定时器2开始计时
EA = 1;
ES = 1;
}
extern char putchar(char ch)
{
SBUF = ch;
while(!TI);//等待数据发送
TI = 0;
return ch;
}
3.main.c
#include <STC15F2K60S2.H> // 单片机寄存器专用头文件
#include <string.h>
#include <stdio.h>
#include "Init.h"
#include "Uart.h" // 串口底层驱动专用头文件
#include <intrins.h>
idata unsigned char uart_rec[10];
idata unsigned char uart_rec_index;
idata unsigned char systick;
idata bit uart_flag;
void Timer1_Init(void) //1毫秒@12.000MHz
{
AUXR &= 0xBF; //定时器时钟12T模式
TMOD &= 0x0F; //设置定时器模式
TL1 = 0x18; //设置定时初始值
TH1 = 0xFC; //设置定时初始值
TF1 = 0; //清除TF1标志
TR1 = 1; //定时器1开始计时
ET1 = 1; //使能定时器1中断
}
void Timer1_Isr(void) interrupt 3
{
if(uart_flag)
systick++;
}
void UartProc()
{
if(!uart_rec_index)
return;
if(systick >= 10)
{
systick = uart_flag = 0;
//逻辑函数
//清除数据
memset(uart_rec,0,uart_rec_index);
uart_rec_index = 0;
}
}
void UartServer() interrupt 4
{
if(RI == 1)
{
uart_flag = 1;
uart_rec[uart_rec_index] = SBUF;
uart_rec_index++;
RI = 0;
if(uart_rec_index > 10)
{
memset(uart_rec,0,10);
uart_rec_index = 0;//防止数据溢出
}
}
}
void main()
{
SystemInit();
Timer1_Init();
Uart1_Init();
while(1)
{
UartProc();
}
}
4.应用
应用1:上位机发送指定内容时单片机才回复
上位机向串口发送字符串ok,单片机收到后向上位机回复HELLO
只需在串口数据处理函数更改即可
void UartProc()
{
if(!uart_rec_index)
return;
if(systick >= 10)
{
systick = uart_flag = 0;
//逻辑函数
if(uart_rec[0] == 'o' && uart_rec[1] == 'k')
{
printf("HELLO");
}
//清除数据
memset(uart_rec,0,uart_rec_index);
uart_rec_index = 0;
}
}
串口现象:
应用2:单片机回复完成后换行
上位机向串口发送字符串ok,单片机收到后向上位机回复HELLO,并自动换行回复WORLD!
在末尾加上换行符\n
即可
void UartProc()
{
if(!uart_rec_index)
return;
if(systick >= 10)
{
systick = uart_flag = 0;
//逻辑函数
if(uart_rec[0] == 'o' && uart_rec[1] == 'k')
{
printf("HELLO\n");
printf("WORLD!\n");
}
//清除数据
memset(uart_rec,0,uart_rec_index);
uart_rec_index = 0;
}
}
串口现象: