在实际应用中,STM32的串口通信都是以数据包格式进行收发,这个数据包一般都包含包头和包尾,表示一个数据包。源代码在文末给出
数据包格式:
固定长度,含包头包尾
可变包长,含包头包尾
问题1:当数据包传输时,里面有数据与包头包尾重复怎么办?
1:设置限幅,包头包尾设置为数据包无法超过的16进制数
2:如果无法避免重复,那么就采用固定长度,含包头包尾的包格式
3:增加包头包尾的数据,增强包头包尾的唯一性,例如可以设置两位包头:0xFF与0xFD,判断包头时,需要同时判断第二位是否为0xFD,包尾同理。
问题2:包头包尾是否可以去掉一个?
是可以的,包头包尾并不是全都需要,例如当采用固定包长进行数据包收发时候,这时就可以只有一个包头,当程序检测到包头后开始接收数据,收够4个字节后,置标志位,一个数据包接收完成。但是这种方法存在很大的弊端,例如当设置包头为0xFF的时候,这时传输4个0xFF的时,程序就可能分不清哪个是包头。
问题3:如何发送字节流:
在STM32中,数据包都是以一个字节一个字节进行发送的,当需要发送16位,或32位的数据时,如何发送?例如float,double,甚至是结构体。STM32支持这种发送方式,这种16位或32位的数据类型内部都是由多个字节组成的,发送时只需要使用uint8_t指针指向它,把它当成一个字节数组发送就可以发送整个数据。
总结:
若数据包载荷不会和包头包尾重复,那么就可以选择可变包长,相反就选择固定包长。可变包长的灵活性很强,可以选择任意的数据包长度,发送16位或32位的变量时候使用uint8_t指针指向它,就可以发送整个数据。
HEX数据包与文本数据包:
HEX数据包发送的就是16进制数据,而文本数据包是将数字译码后的数据,通常以换行作为包尾,文本载荷数据是一个字符串。
HEX数据包传输最直接,解析数据非常简单,比较适合模块发送原始数据,例如传感器等,缺点是载荷容易和包头包尾重复,文本数据包数据适合输入指令进行人机交互,例如蓝牙模块的AT指令,缺点是解析效率低,例如发送数字100,文本数据就需要占用3个字节,而HEX只需要1个字节空间。 根据实际场景选择数据。
数据包发送:
HEX数据包:
定义字符数组进行发送
文本数据包发送:
定义字符串进行发送
数据包接收:
每收到一个字节,程序都会进入一次中断,因此每拿到一个字节的数据都是独立的过程,对数据包来说,主要有包头,载荷,包尾这三种数据,那么需要设计一种能记住不同状态的程序和机制,在不同状态执行不同的操作,同时还要进行状态的合理转移,这种程序设计就叫做状态机(State Machine)。
HEX数据包接收:
如下图状态转移图:
首先设置状态变量s = 0;表示等待包头,当接收到数据进入中断时,中断程序进行判断状态接收到的数据是否为包头,是的话将s置1,表示接收数据,这样在下一次中断时就会进行数据接收,并且还要在程序中设置一个数组,当接收到载荷的长度后,表示数据接收完成,这时将s置2,表示等待包尾,如果没有问题,那么下一次收到的数据就是包尾数据,这时将s置0,这样就完成了一次接收数据的循环。
如果数据和包头重复,那么就说明数据判断错误,那么接收完成后包尾的位置就可能不是FE,这时就可以进入重复等待包尾的状态,直到接收到真正的包尾,这样能预防数据和包头重复的错误。
真正设计串口通信时,尽量避免包头与数据重复。
文本数据包接收:
如下图状态转移图:
与HEX不同的是,在s = 1的时候,每次接收数据都要判断是否为\r如果是,则不接受数据并进入下一个状态等待包尾\n,这里有两个包尾,如果只有一个包尾为\r的话那么当接收到\r时可以直接回到s=0了。
电路连接:
连接显示屏与触摸模块,显示屏的SCL在B10,SDA在B11,触摸模块的输出引脚在A1(触摸时输出高电平)。
编写代码:
Serial.c
#include "stm32f10x.h" // Device header
#include "DELAY.h"
#include "OLED.h"
#include "Serial.h"
uint8_t RxData;
uint8_t KeyNum;
int main() {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//下拉输入IPD,上拉输入IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//这一行可以不写,在输入模式下这一行不起作用
GPIO_Init(GPIOA, &GPIO_InitStructure);
OLED_Init();
Serial_Init();
OLED_ShowString(1, 1, "TxData:");
OLED_ShowString(3, 1, "RxData:");
Serial_TxPacket[0] = 0x01;
Serial_TxPacket[1] = 0x02;
Serial_TxPacket[2] = 0x03;
Serial_TxPacket[3] = 0x04;
while(1){
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) == 1){//按键按下,这一位为1
while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) == 1);//如果一直不松手那么程序就卡在这里
KeyNum = 1;//返回按键的值
}
if(KeyNum == 1) {
Serial_TxPacket[0] ++;
Serial_TxPacket[1] ++;
Serial_TxPacket[2] ++;
Serial_TxPacket[3] ++;
Serial_SendPacket();
OLED_ShowHexNum(2,1, Serial_TxPacket[0], 2);//RxPacket数组是同时读写的数组,
OLED_ShowHexNum(2,4, Serial_TxPacket[1], 2);
OLED_ShowHexNum(2,7, Serial_TxPacket[2], 2);
OLED_ShowHexNum(2,10, Serial_TxPacket[3], 2);
KeyNum = 0;
}
if(Serial_GetRxFlag() == 1) {
OLED_ShowHexNum(4,1, Serial_RxPacket[0], 2);//RxPacket数组是同时读写的数组,
//当读取速度慢的时候,后面的数据可能会刷新为下一个数据包的数据,可能会造成数据冲突
OLED_ShowHexNum(4,4, Serial_RxPacket[1], 2);
//若在这里(读取的过程中)进入中断,那么数组的数据就可能被覆盖
OLED_ShowHexNum(4,7, Serial_RxPacket[2], 2);
OLED_ShowHexNum(4,10, Serial_RxPacket[3], 2);
}
}
}
Serial.h
#ifndef __SERIAL_H
#define __SERIAL_H
#include <stdio.h>
extern uint8_t Serial_TxPacket[];//外部可调用,如果模块里有数组需要外部调用,一般使用Get,Set函数进行封装,使用指针传递
extern uint8_t Serial_RxPacket[];
void Serial_Init();
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
void Serial_Printf(char* format,...);
uint8_t Serial_GetRxFlag();
void Serial_SendPacket();
#endif
main.c
#include "stm32f10x.h" // Device header
#include "DELAY.h"
#include "OLED.h"
#include "Serial.h"
uint8_t RxData;
uint8_t KeyNum;
int main() {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//下拉输入IPD,上拉输入IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//这一行可以不写,在输入模式下这一行不起作用
GPIO_Init(GPIOA, &GPIO_InitStructure);
OLED_Init();
Serial_Init();
OLED_ShowString(1, 1, "TxData:");
OLED_ShowString(3, 1, "RxData:");
Serial_TxPacket[0] = 0x01;
Serial_TxPacket[1] = 0x02;
Serial_TxPacket[2] = 0x03;
Serial_TxPacket[3] = 0x04;
while(1){
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) == 1){//按键按下,这一位为1
while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) == 1);//如果一直不松手那么程序就卡在这里
KeyNum = 1;//返回按键的值
}
if(KeyNum == 1) {
Serial_TxPacket[0] ++;
Serial_TxPacket[1] ++;
Serial_TxPacket[2] ++;
Serial_TxPacket[3] ++;
Serial_SendPacket();
OLED_ShowHexNum(2,1, Serial_TxPacket[0], 2);//RxPacket数组是同时读写的数组,
OLED_ShowHexNum(2,4, Serial_TxPacket[1], 2);
OLED_ShowHexNum(2,7, Serial_TxPacket[2], 2);
OLED_ShowHexNum(2,10, Serial_TxPacket[3], 2);
KeyNum = 0;
}
if(Serial_GetRxFlag() == 1) {
OLED_ShowHexNum(4,1, Serial_RxPacket[0], 2);//RxPacket数组是同时读写的数组,
//当读取速度慢的时候,后面的数据可能会刷新为下一个数据包的数据,可能会造成数据冲突
OLED_ShowHexNum(4,4, Serial_RxPacket[1], 2);
//若在这里(读取的过程中)进入中断,那么数组的数据就可能被覆盖
OLED_ShowHexNum(4,7, Serial_RxPacket[2], 2);
OLED_ShowHexNum(4,10, Serial_RxPacket[3], 2);
}
}
}
程序现象:
触摸开关触摸一次,发送一个数据包,在串口助手中,发送区域填写FF 12 34 56 78 FE发送数据,STM32接收到数据。