通信协议
常见通信协议
传输模式
数据通常是在两个站(点对点)之间进行传输,按照数据流的方向可分为三种传输模式:单工、半双工、全双工。
- 单工通信simplex:只支持信号在一个方向上传输(正向或反向)。
- 半双工通信half-duple:允许信号在两个方向上传输,但某一时刻只允许信号在一个信道上单向传输。
- 全双工full-duplex:允许数据同时在两个方向上传输,即有两个信道,因此允许同时进行双向传输。
同步异步
- 同步通信:收发设备双方会使用一根信号线表示时钟信号。双方会统一规定在时钟信号的上升沿或下降沿对数据线进行采样。
- 异步通信:不使用时钟信号进行数据同步,异步通信在发送字符时,所发送的字符之间的时间间隔可以是任意的。接收端必须时刻做好接收的准备,发送端可以在任意时刻开始发送字符。必须在每一个字符中加入起始位、奇偶校验位、停止位等。某些通讯中还需要双方约定数据的传输速率,以便更好地同步 。适用于短距离、速率不高的情况下。
电平标准
电平标准是数据 1 和数据 0 的表达方式,是传输线缆中人为规定的电压与数据的对应关系,这种关系又将电平信号分为两种,单端信号和差分信号。
单端信号:一根电线承载表示信号的变化电压,而另一根电线连接到通常为接地的参考电压。
- TTL电平:高电平(1)>2.4V,低电平(0)<0.4V。
- RS232电平:-3V~-15V表示1,+3V~+6V表示0。
差分传输:区别于传统的一根信号线一根地线的单端信号传输,差分传输在这两根线上都传输信号,这两个信号的振幅相同,相位相反。在这两根线上的传输的信号就是差分信号。信号接收端比较这两个电压的差值来判断发送端发送的逻辑状态。在电路板上,差分走线必须是等长、等宽、紧密靠近、且在同一层面的两根线。
- RS485电平:两线压差+2~+6V表示1,-2~-6V表示0(差分信号)。
发送数据
stm32中封装了USART(Universal synchronous and asynchronous receiver-transmitter,通用同步和异步收发器)库。
对于各项参数和寄存器允许我们粗略了解就可以收发消息。
配置时钟和gpio
这一行无需多言,要用到USART1,要先挂载到时钟上。
c
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
接下来是挂载外设相关的gpio口。
收发数据只需要用到TX和RX。
c
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//tx
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//rx
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
对于TX所在的A9,因为是作为串口输出,所以配置为GPIO_Mode_AF_PP复用推挽输出。
对于A10,设置为GPIO_Mode_IPU上拉输入。
配置USART
USART_InitStructure只是在配置,USART_Init是使配置生效,USART_Cmd是启动设备。
在TIM定时器中,也是同样的结构。
c
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;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1,&USART_InitStructure);
USART_Cmd(USART1,ENABLE);
- USART_BaudRate波特率:这需要通信双方提前协商好,库函数内部实现了波特率的计算,我们无需关注底层实现。
- USART_HardwareFlowControl硬件流控制:不使用硬件流控制,即不使用RTS(请求发送)和CTS(清除发送)信号进行数据流控制。
- USART_Mode:同时配置为发送和接收模式。
- Parity奇偶校验:这种方式可靠性低,还占用一位,因此填USART_Parity_No不采用校验。
- USART_StopBits:停止位设置为1位。这是每个数据字节之后发送的停止位的数量,1位是标准设置。
- USART_WordLength:8位刚好是一个字节的长度,可能省掉很多工作。
发送数据
c
void Usart1_SendByte(uint8_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}
添加while循环,它等待直到USART1发送寄存器中的数据已经传输完成。
USART_GetFlagStatus是一个库函数,用于检查USART标志状态,而USART_FLAG_TXE表示USART发送缓冲区空闲标志。因此,这行代码会等待直到USART发送缓冲区为空闲,即数据已经传输完毕。
发送多字节数据
在一个Byte的基础上,字符串、字节流、多位数都可以视作多个字符的序列。
c
void Usart1_SendArray(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i++)
{
Usart1_SendByte(Array[i]);
}
}
void Usart1_SendString(char *String)
{
uint8_t i;
for(i = 0; String[i] != '\0';i++)
{
Usart1_SendByte(String[i]);
}
}
//进值转换
uint32_t Usart1_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y--)
{
Result *= X;
}
return Result;
}
void Usart1_SendNum(uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
Usart1_SendByte(Number / Usart1_Pow(10, Length - i - 1) % 10 + '0');
}
}
比如发送数字123
,实际是依次发送'1'
、'2'
、'3'
。
串口通信采用的是最低有效位优先传输,接收方收到的是小端存储的二进制数据。
小端存储指的是一个字节内小端存储,而非整个数据块。
printf重定向
输出的消息可能会被发送到不同的通信接口,我们必须要告诉 printf 消息需要发送到哪一个通信接口上,这个过程一般被称做"重定向"。
如果没有配置输出的位置,那么会导致程序崩溃。
也就是添加这几行:
c
#include "stm32f10x.h" // Device header
#include "Usart.h"
//关闭ARM的半主机模式
#pragma import(__use_no_semihosting_swi)
struct __FILE {
int handle; /* Add whatever you need here */
};
FILE __stdout;
FILE __stdin;
int fputc(int ch, FILE *f) {
Usart1_SendByte(ch);
return ch;
}
void _sys_exit(int return_code) {
}
输出结果
接收数据
接收数据显然要用到中断,因为我不可能周期轮询串口状态:效率太低。
配置中断
中断通道可以在.s文件中查找。
NVIC_InitStructure的作用是启用中断、配置优先级。
c
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority =1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority =1;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1,ENABLE);
- USART_ITConfig:启用了USART1的接收数据寄存器非空中断 (USART_IT_RXNE)。当接收缓冲区非空时,这个中断会触发,表示接收到了新的数据。
- NVIC_InitStructure:用于配置中断控制器NVIC(
Nested Vectored Interrupt Controller
)。 - NVIC_IRQChannelPreemptionPriority:抢占优先级,数值越低,优先级越高
- NVIC_IRQChannelSubPriority:子优先级,抢占优先级相同时比较。
需要在主函数中进行中断优先级分组。
c
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
实现中断处理函数
c
uint8_t Rx_Data;
uint8_t Rx_Flag;
uint8_t Usart1_GetRxFlag()
{
if (Rx_Flag == 1)
{
Rx_Flag = 0;
return 1;
}
return 0;
}
uint8_t Usart1_GetRxData()
{
return Rx_Data;
}
void USART1_IRQHandler()
{
if (USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
{
Rx_Data = USART_ReceiveData(USART1);
Rx_Flag = 1;
USART_ClearITPendingBit(USART1,USART_FLAG_RXNE);
}
}
上面的代码会保留接收到的最后一个字节数据。新数据会覆盖掉Rx_Data。
c
if (Usart1_GetRxFlag())
{
OLED_ShowHexNum(1,9,Usart1_GetRxData(),2);
}
在主函数中通过上面的代码,先读取是否有数据,然后取出,即可访问。
接受字节流/字符串
Rx_Data的数据类型是uint8_t,最多只能存储8位数据。
如果要存储多位,可以使用数组。
对于接收到的数据,为了保证数据的有效性,我们可以约定请求头和请求尾,符合要求才会视为成功接收。
c
void USART1_IRQHandler()
{
if (USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
{
Rx_Data = USART_ReceiveData(USART1);
if (RxState == 0)
{
if (Rx_Data == '@')
{
RxState = 1;
pRxPacket = 0;
}
}
else if (RxState == 1)
{
if (Rx_Data == '\r')
{
RxState = 2;
}
else
{
Rx_Packet[pRxPacket] = Rx_Data;
pRxPacket++;
}
}
else if (RxState == 2)
{
if (Rx_Data== '\n')
{
RxState = 0;
Rx_Packet[pRxPacket] = '\0';
Rx_Flag = 1;
}
}
USART_ClearITPendingBit(USART1,USART_FLAG_RXNE);
}
}
把请求头和请求尾过滤掉,把请求体放到数组中。
读取还是同样的过程,不过现在返回值是char*
指针而非8个比特位。
可以配合串口收发装置验证结果。
c
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
Usart1_Init();
OLED_Init();
LED_Init();
OLED_ShowString(1,1,"Rx_Data:");
while(1)
{
if (Usart1_GetRxFlag())
{
if (strcmp(Rx_Packet,"LED_ON") == 0)
{
LED1_ON();
OLED_ShowString(2,1,"LED_ON_OK");
Usart1_SendString("LED_ON_OK!");
}
else if (strcmp(Rx_Packet,"LED_OFF") == 0)
{
LED1_OFF();
OLED_ShowString(2,1,"LED_OFF_OK");
Usart1_SendString("LED_OFF_OK!");
}
else
{
OLED_ShowString(2,1,Rx_Packet);
Usart1_SendString(Rx_Packet);
}
}
}
}