通信总述
通信:设备与设备之间进行数据交换,比如单片机与电脑(要接USB转串口模块)
通信协议:通信规则,通信双方按照通信协议规则进行数据收发
常见的通信协议:USART,I2C,SPI,CAN,USB
串口通信
电平标准:用高低电平来传输数字1,0,从而传输数据
常用电平标准:TTL标准:3.3或5V为高,0V为低;差分电平标准:RX与TX线电压之差
通信协议
所谓通信协议,我所理解的就是规定好数据帧+波特率
数据帧包括:起始位,数据位,校验位,停止位
数据传输方向:
单工:只有一个信道,数据只能沿一个方向传播
半双工:只有一个信道,数据可以双向传播,但是同一时间只能沿一个方向
全双工:两个信道,分别负责数据的收和发,可以同步进行
时钟:
同步:双方共用同一个波特率发生器,因此发送方发送多个数据时,不能有时间间隔
异步:双方规定好一个波特率,按照此波特率发送数据之后,接收方按照此波特率接收,在发送下一个数据之间,可以隔任意时长,不会出现“赶不上”的情况
USART的通信方式是全双工异步通信
数据帧格式:
起始位S:低电平;停止位P:高电平
数据位八位:低位先行(低位在前,高位在后)
比如传输0x55,那么数据D0~D7为1010 1010
USART
USART是stm32内部集成的片上外设,挂载在APB1和APB2上
通过配置结构体来USART的寄存器
主要的寄存器包括:发送数据寄存器,接收数据寄存器
软件代码实现初始化USART
GPIO初始化
①开启时钟
②输出引脚定义为复用推挽,输入引脚定义为上拉输入
USART初始化
①开启时钟
②定义结构体
USART_InitTypeDef USART_InitStructure
USART_InitStructure.USART_BaudRate = 9600; //波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,发送模式和接收模式均选择
USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要
USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位
USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1
大部分都可以直接通过英文意思明白具体作用
数据接收过程
当一个字符被接收到时,RXEN位被置1,它表明移位寄存器的内容被转移到RDR,也就是说,数据已经被接收并且可以被读出,我们要告诉单片机已经收到了,该执行读出的动作了,那么就需要中断这个操作了,在中断里读出
江科大在这里有一个新的设计,利用中断,自己再构建中断,这样更加灵活
中断初始化代码
/*中断输出配置*/
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //选择配置NVIC的USART1线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
NVIC的配置思路
NVIC的分组,指的是如何分配抢占优先级和响应优先级(分别给几个)
NVIC本质上是一个中断管理器,主要完成两件事,一是给中断分配通道并使能通道,二是给中断分配优先级(抢占和响应)
还要开启相关通道的中断,在这里也就是USART_ITConfig
最后一步是对USART进行使能
USART_cmd(USART1,ENABLE)
这样USART就初始化完毕
因为在编程过程中,层层封装的理念用的很深,所以,对于某一个模块,都是在一个单独的文件中将各个功能都封装好,最后在主文件的大循环中调用API即可
因此,我们还要在Serial中封装其他功能的函数
串口发送与接收
发送单个字节:
需要调用函数
USART_SendData(USARTx,Byte)
单字节发送,需要有一个等待缓冲区(发送移位寄存器)清空的过程,等到发送完成,标志位置1,这点和接收数据有点像,接收完数据,接收数据寄存器也置1
因此需要加此函数
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET)
同理,发送数组,字符串
//发送数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++) //遍历数组
{
Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据
}
}
//发送字符串
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止
{
Serial_SendByte(String[i]); //依次调用Serial_SendByte发送每个字节数据
}
}
发送字符串时,回先把字符编码为十六进制数,再进行发送
如果要发送十进制数,需要把每一位分隔开来,都当做单独的字节单独发送
同时,要从高位向低位发送,因为先发送的回出现在前面
比如1234,如果从低位向高位发送,则会在接收方出现4321这样的情况
void Serial_SendNum(uint16_t Num,uint16_t length)
{
uint16_t Array[length];
uint16_t i;
for(i=0;i<length;i++)
{
Array[length-1-i]=Num%10;
Num=Num/10;
}
for(i=0;i<length;i++)
{
Serial_SendByte(Array[i]+'0');
}
}
//突然发现为什么当时在写的时候在Array[i]后面要加一个'0'
串口发送Hex数据包与文本数据包
Hex数据,也就是十六进制类型的数据
发送Hex数据包,本质上是模仿了USART发送一个字节的通信协议
包头,相当于通信协议中的起始位;字长,相当于数据位;包尾,相当于停止位
而接收整个数据帧的判别过程是单片机完成的(硬件),接收一个数据包则需要我们用软件实现(代码)。这里有点像是I2C教程中的软件实现与硬件实现。
江科大介绍了一个重要的编程思想:状态机
(接收数据帧或者数据包的过程中,才需要各种位的判别)
状态机:设计一个能记住不同状态的机制,在不同的状态执行不同的操作,同时还要进行状态的合理转移
在不同的状态处理不同的事情,同时也要设置状态转移的条件,核心是理清楚逻辑
整个状态机是在中断函数里面完成的
江科大在这里用到了C语言中的一个高级语法:静态变量
关键字:static
在函数中相当于全局变量,只初始化一次,且在函数退出后仍然有效,具有记忆性,如果想要回到最初的状态,需要人为赋值
中断函数如图
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0; //定义表示当前状态机状态的静态变量
static uint8_t pRxPacket = 0; //定义表示当前接收数据位置的静态变量
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断
{
uint8_t RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量
/*使用状态机的思路,依次处理数据包的不同部分*/
/*当前状态为0,接收数据包包头*/
if (RxState == 0)
{
if (RxData == 0xFF) //如果数据确实是包头
{
RxState = 1; //置下一个状态
pRxPacket = 0; //数据包的位置归零
}
}
/*当前状态为1,接收数据包数据*/
else if (RxState == 1)
{
Serial_RxPacket[pRxPacket] = RxData; //将数据存入数据包数组的指定位置
pRxPacket ++; //数据包的位置自增
if (pRxPacket >= 4) //如果收够4个数据
{
RxState = 2; //置下一个状态
}
}
/*当前状态为2,接收数据包包尾*/
else if (RxState == 2)
{
if (RxData == 0xFE) //如果数据确实是包尾部
{
RxState = 0; //状态归0
Serial_RxFlag = 1; //接收数据包标志位置1,成功接收一个数据包
}
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位
}
}