最近学习了利用无线串口模块进行单片机与电脑上位机,或者进行单片机与单片机之间的通信,在这里分享一下串口通信的工作原理和通信协议的相关学习笔记。以MK60为例。
串口是一种按位(bit)进行发送和接收字节的通讯方式,即在传输中将1字节(Byte)拆分成8位,用一个端口一位一位地进行传输。
由于在数据传输时占用了一个端口,所以串口的数据线最少只需要一根。使用一根传输线可以实现单工模式(Simplex Communication)通信或者半双工模式(Half Duplex)通信。使用两根传输线可以实现全双工模式(Full Duplex)通信。
单工模式通信的一方固定为发送端,另一方固定为接收端,信息单向传输。
半双工模式通信中信息传输方向是可以改变的,即传输的两个设备在传输中负责发送还是接收是不固定的。半双工模式通信中任一时刻的通信方向是唯一的,当一方为发射端时另一方为接收端,不可以同时进行收发。
全双工模式通信中两根传输线各自负责一个方向的传输,可以实现收发同时进行。
波特率(Baud)就是每秒钟传输的数据位数。它是对传输速率的一种度量。
通常使用的波特率有 600、900、1200、1800、2400、4800、9600、19200、38400、57600、115200 等。
同步通信
进行数据传输时,发送和接收双方要保持完全的同步,要求接收和发送设备必须使用同一时钟。同步通信速度快但是对于软硬件要求较高。
异步通信
异步通信在发送字符时,所发送的字符之间的时隙可以是任意的,传输双方按照一定的协议,使传输数据的每一帧包含“开始位”和“停止位”(或包含“校验位”),每一帧的数据传输总是从“开始位”开始,传输到“停止位”结束。异步通信的实现相对简单,但是传输效率相对同步通信较低。在本文中所使用的的都是异步通信方式。
在单片机中使用异步收发器(Universal Asynchronous Receiver/Transmitters,UART)功能进行实现。
无线串口模块本身作用相当于有线串口通信中的传输线,单片机上的从机和电脑上用的主机端,一般使用的是全双工通信,常见有四个接口,除去VCC和GND的电源接口外,另外两个接口分别为RXD:接收数据,TXD:发送数据,与单片机之间的串口接口交叉相连。
第一次使用串口模块之前需要事先配置好串口模块,购买串口模块时商家一般都会提供相关资料,这里主要提一下配置中需要注意的几个点:
1.电脑端口查看 |
---|
先注意安装驱动,配置时的端口号可以 右键***此电脑—>管理—>设备管理器—>端口*** 查看。 |
2.主从地址设置 |
设置口模块的主从机的地址必须相同,并且注意不要和其他的无线串口使用同一地址。 |
3.波特率设置 |
无线串口模块主从机的波特率以及电脑上位机和单片机设置的波特率必须相同,否则无法正常通信。波特率设置并不是越大越好,根据实际自己能通信的波特率而定,不推荐太大。 |
4.从机设置模式 |
配置从机时需要注意将SET引脚和VCC引脚短接以进入配置模式。 |
单片机的串口可以使用的引脚是固定的,可以查阅数据手册,下面列举的是K60的串口通道。
/********************************** UART ***************************************/
// 模块通道 端口 可选范围 建议
#define UART0_RX PTA15 //PTB16 //PTA1、PTA15、PTB16、PTD6 PTA1不要用(与Jtag冲突)
#define UART0_TX PTA14 //PTB17 //PTA2、PTA14、PTB17、PTD7 PTA2不要用(与Jtag冲突)
#define UART1_RX PTE1 //PTC3、PTE1
#define UART1_TX PTE0 //PTC4、PTE0
#define UART2_RX PTD2 //PTD2
#define UART2_TX PTD3 //PTD3
#define UART3_RX PTE5 //PTB10、PTC16、PTE5
#define UART3_TX PTE4 //PTB11、PTC17、PTE4
#define UART4_RX PTE25 //PTC14、PTE25
#define UART4_TX PTE24 //PTC15、PTE24
#define UART5_RX PTE9 //PTD8、PTE9
#define UART5_TX PTE8 //PTD9、PTE8
UART0 和 UART1 时钟源为内核时钟,UART2~UART5 的时钟源为外设时钟(总线时钟)。在后面的示例中所使用的的都是UART3通道,从机的TX接口连接到PTE5,RX接口连接到PTE4,可以自行根据实际情况修改。
K60的串口在初始化之后就可以直接接收发送了,但在实际的使用中接收数据时容易丢失数据,需要设置串口数据接收的中断,当数据需要接收时就进入中断。
#define UARTn UART3 //使用UART3通道
#define BAUD 57600 //设置波特率为57600
#define UARTn_RX_TX_IRQ UART3_RX_TX_IRQn
#define UARTn_RX_TX_VECTORn UART3_RX_TX_VECTORn
void UART_IRQHandler(void);//中断服务函数声明
void uart_rx_irq_en(UARTn_e uratn)
{
UART_C2_REG(UARTN[uratn]) |= UART_C2_RIE_MASK; //使能UART接收中断
enable_irq((IRQn_t)((uratn << 1) + UART0_RX_TX_IRQn)); //使能IRQ中断
}
void uart_init()
{
uart_init (UARTn, BAUD); //初始化串口UARTn,波特率为BAUD
NVIC_SetPriority(UARTn_RX_TX_IRQn,1); //配置串口中断优先级为1
set_vector_handler(UARTn_RX_TX_VECTORn,UART_IRQHandler); //配置串口中断服务函数UART_IRQHandler
uart_rx_irq_en(UARTn); //使能中断
}
在进行大量数据的收发之前,先进行简单数据的收发来测试,以下程序中实现在上位机中发送一个字符,单片机接收到后再将原数据处理发送回上位机。
void uart_getchar (UARTn_e uratn, char *ch)//接收一个字符
{
while (!(UART_S1_REG(UARTN[uratn]) & UART_S1_RDRF_MASK)); //等待接收满了
*ch = UART_D_REG(UARTN[uratn]); // 获取接收到的8位数据
}
void uart_putchar (UARTn_e uratn, char ch)//发送一个
{
while(!(UART_S1_REG(UARTN[uratn]) & UART_S1_TDRE_MASK)); //等待发送缓冲区空
UART_D_REG(UARTN[uratn]) = (uint8)ch; //发送数据
}
int main() //单片机主函数
{
char ch;
uart_init();//串口初始化
while(1)
{
uart_getchar(UARTn,&ch); //接收一个字符储存到变量ch
ch++;//示例处理,转入'A'输出'B'
uart_putchar(UARTn,ch); //发送字符ch,长数据接收需要在中断中
}
}
程序效果:
在上位机发送字符’A’,单片机返回字符’B’
在实际使用当中,仅仅单字符传输是不够的,所以这里就要用到传输协议了。顾名思义,协议就是传输双方规定好固定传输格式,从而使接收到多个数据时不会混淆,并且当产生传输数据出错时可以检测到甚至纠错。传输协议可以按照自己的上位机或者自己设计。
这是我所使用的匿名上位机的传输协议格式:
“AAAA”+“02”+Pb+X[0]+X[1]+···+X[n] |
---|
AAAA
标志位,标志一帧数据的开始
02
协议代码, 表示此协议为02号协议
Pb
校验位,将n个数据求和之后得出的校验位,检验是否有误
X[n]
数据 ,所要传输的n个数据
下面为示例协议的发送函数,注意:由于传输的数据为16位,而串口一次传8位数据,所以单个数据被拆成两个进行传输。
unsigned short int pst[10]={
0};
void pstInit(void)//所要发送的数据设定,这里传输三姿态角作为示例
{
pst[0]=pitch;
pst[1]=roll;
pst[2]=yaw;
}
/*
*uartn 串口通道
*pst 所要发送的数据
*x 协议代码 此处为2
*/
void Data_SendMc(UARTn_e uartn,unsigned short int *pst,uint8 x)
{
unsigned char _cnt=0; unsigned char sum = 0;
unsigned char data_to_send[23]; //发送缓存
data_to_send[_cnt++]=0xAA;
data_to_send[_cnt++]=0xAA;
data_to_send[_cnt++]=x;
data_to_send[_cnt++]=0;
data_to_send[_cnt++]=(unsigned char)(pst[0]>>8); //高8位
data_to_send[_cnt++]=(unsigned char)pst[0]; //低8位
data_to_send[_cnt++]=(unsigned char)(pst[1]>>8);
data_to_send[_cnt++]=(unsigned char)pst[1];
data_to_send[_cnt++]=(unsigned char)(pst[2]>>8);
data_to_send[_cnt++]=(unsigned char)pst[2];
data_to_send[_cnt++]=(unsigned char)(pst[3]>>8);
data_to_send[_cnt++]=(unsigned char)pst[3];
data_to_send[_cnt++]=(unsigned char)(pst[4]>>8);
data_to_send[_cnt++]=(unsigned char)pst[4];
data_to_send[_cnt++]=(unsigned char)(pst[5]>>8);
data_to_send[_cnt++]=(unsigned char)pst[5];
data_to_send[_cnt++]=(unsigned char)(pst[6]>>8);
data_to_send[_cnt++]=(unsigned char)pst[6];
data_to_send[_cnt++]=(unsigned char)(pst[7]>>8);
data_to_send[_cnt++]=(unsigned char)pst[7];
data_to_send[_cnt++]=(unsigned char)(pst[8]>>8);
data_to_send[_cnt++]=(unsigned char)pst[8];
data_to_send[3] = _cnt-4;
sum = 0;
for(unsigned char i=0;i<_cnt;i++)//校验位计算
sum += data_to_send[i];
data_to_send[_cnt++] = sum;
for(unsigned char i=0;i<_cnt;i++)
uart_putchar (uartn,data_to_send[i] );//数据发送
}
接收数据接收部分将区分单片机发送数据与电脑发送的数据。以十六进制数AAAA为标志位的数据是由单片机发送,以字符串“AAAA”为标志位的是由上位机发送数据。
下面是我在单片机之间传输数据时使用的格式:
(X1,X2,······,Xn,Pb) |
---|
’('
开始位 , 标志一帧数据的开始
Xn
数据 , 所要传输的n个数据
Pb
校验位 , 将n个数据求和之后得出的校验位,检验是否有误
’)'
停止位, 标志一帧数据的结束
#define START '(' //开始位设置
#define FINISH ')' //结束位设置
#define SP ',' //以逗号分隔
unsigned short int pst[10]={
0};
void pstInt(void)//所要发送的数据设定,同向上位机发送
{
pst[0]=pitch;
pst[1]=roll;
pst[2]=yaw;
}
/*
*uartn 串口通道
*pst 所要发送的数据
*x 发送数据个数 示例中为3
*/
void Data_SendMcu(UARTn_e uartn,unsigned short int *pst,uint8 x)
{
unsigned char _cnt=0; unsigned char sum = 0;
unsigned char data_to_send[45]; //发送缓存
data_to_send[_cnt++]=START;
for(unsigned char i=0;i<x;i++)
{
data_to_send[_cnt++]=(unsigned char)(pst[i]>>8); //高8位
data_to_send[_cnt++]=(unsigned char)pst[i]; //低8位
data_to_send[_cnt++]=SP;
}
sum = 0;
for(unsigned char i=0;i<_cnt;i++)//校验位计算
sum += data_to_send[i];
data_to_send[_cnt++]=0;
data_to_send[_cnt++] = sum;
data_to_send[_cnt++] =FINISH;
for(unsigned char i=0;i<_cnt;i++)
uart_putchar (uartn,data_to_send[i] );//数据发送
}
与单片机的发送和与上位机发送类似,由于不受上位机协议限制,我们可以自己设置发送数据的个数,即每一帧传输数据长度可以不一样,不需要用0占位。
typedef enum
{
UART_GET_WAIT, //等待接收状态
UART_GET_ING, //正在接收数据
UART_GET_T, //接收成功
UART_GET_F, //接收失败(校验失败)
}UartGetStatusNode;
typedef struct
{
unsigned short int data_to_Get[45]; //接收缓存
unsigned char n; //上一分隔符位置
unsigned char nn; //当前接收位置
unsigned char t; //接收到数据个数
unsigned short int prt[10]; //接收数据缓存
unsigned short int UartGetCh[10]; //可信数据
}UartInfoNode;
UartGetStatusNode UartGetStatus=UART_GET_WAIT; //接收状态机
UartInfoNode UartInfo; //数据结构体变量
void Data_GetInit(void)//结构体初始化
{
memset(UartInfo.data_to_Get,0,45);
UartInfo.n=0;
UartInfo.nn=0;
UartInfo.t=0;
memset(UartInfo.prt,0,10);
}
void Data_GetMcu(UARTn_e uartn,char ch)
{
switch(UartGetStatus)
{
case UART_GET_WAIT:
if(ch=='(') //找到开始位
{
UartInfo.data_to_Get[UartInfo.nn++]=ch;
UartInfo.n=UartInfo.nn-1;
UartGetStatus=UART_GET_ING;
}
break;
case UART_GET_ING:
if(ch=='(')
UartGetStatus=UART_GET_F;//数据位数错误,接收失败
else if(ch==',') //找到分隔位
{
UartInfo.data_to_Get[UartInfo.nn++]=ch;
if(UartInfo.nn==UartInfo.n+4)
UartInfo.prt[UartInfo.t++]= UartInfo.data_to_Get[UartInfo.nn-3]<<8+UartInfo.data_to_Get[UartInfo.nn-2];//存入待校验数据
else
UartGetStatus=UART_GET_F;//数据位数错误,接收失败
UartInfo.n=UartInfo.nn-1;
}
else if(ch==')')//找到结束位
{
UartInfo.data_to_Get[UartInfo.nn++]=ch;
for(unsigned char i=0;i<UartInfo.nn-3;i++)//进行校验
sum +=UartInfo.data_to_Get[i];
if(sum==UartInfo.data_to_Get[UartInfo.nn-3]<<8+UartInfo.data_to_Get[UartInfo.nn-2])
UartGetStatus=UART_GET_T;//校验通过,数据可信
else
UartGetStatus=UART_GET_F;//检验未通过,数据不可信
}
else
UartInfo.data_to_Get[UartInfo.nn++]=ch;
if(UartInfo.nn>44)
UartGetStatus=UART_GET_F;
break;
case UART_GET_T:
for(unsigned char i=0;i<UartInfo.t;i++) //接收成功,将缓存数据存入UartGetCh
UartInfo.UartGetCh[i]=UartInfo.prt[i];
Data_GetInit();
UartGetStatus=UART_GET_WAIT;
sum=0;
break;
case UART_GET_F: //接收失败,重新查找
Data_GetInit();
UartGetStatus=UART_GET_WAIT;
sum=0;
break;
default:
break;
}
}
void UART_IRQHandler(void)
{
if(UART_S1_REG(UARTN[UARTn]) & UART_S1_RDRF_MASK) //接收数据寄存器满
uart_getchar (UARTn, &ch);
Data_GetMcu(UARTn,ch);//接收处理
if(UART_S1_REG(UARTN[UARTn]) & UART_S1_TDRE_MASK ) //发送数据寄存器空
{
Data_SendMcu(UARTn,pst,x);
}
}
int main()
{
uart_init();
Data_GetInit();
while(1)
{
//对UartInfo.UartGetCh进行处理
}
}
当接收到的数据校验失败的时候,这一帧的数据直接放弃,等待下一次的接收,只有当校验通过的时候才会将缓存数据存入UartGetCh数组当中。