通信
目录
正在上传…重新上传取消
串行:串口,UART,I2C,SPI,W500(16根数据线)
并行:SPIO,FSMC(16位的并行数据线),W5100
USART全双工,SPI全双工,
有时钟就是同步
对时钟要求比较高。
USART--串口通信
正在上传…重新上传取消
物理层
TTL电平从单片机或者从芯片里面出来的都叫TTL电平,在STM32里用3.3v来表示
RS-232高电平0,低电平是1,高低电平之差是30V
电平转换芯片是232。
霸道有232串口,在指南针可以扩展232串口(网上买)
正在上传…重新上传取消
控制器STM32
协议层
正在上传…重新上传取消
在STM32还有九个位
校验位
正在上传…重新上传取消
串口的功能框图
正在上传…重新上传取消
引脚功能
那些引脚有这些功能呢?
正在上传…重新上传取消
串口重映射
数据寄存器—USART_DR(重要)
串口是全双工的,即TDR与RDR同时收发
配置位有那些常用
具体流程
UE串口使能
TE与RE都打开后数据可以正常的接收了
发送过程(先UE=1,TE=1)
当TXE=1
正在上传…重新上传取消
接收过程先UE =1,TE=1
正在上传…重新上传取消
还有其他的位
正在上传…重新上传取消
正在上传…重新上传取消
这些是和中断有关的位
控制波特率的寄存器
正在上传…重新上传取消
第三节具体的函数
正在上传…重新上传取消
正在上传…重新上传取消
第四节编程中断接收和发送
正在上传…重新上传取消
USB转串口原理图
要安装CH340驱动软件加打开野火工具
正在上传…重新上传取消
正在上传…重新上传取消
第一步,开启时钟,把需要用到的USART和GPIO的时钟打开
第二步GPIO初始化,把TX配置成复用输出,RX配置成输入。TX配置复用推挽输出,RX不配置下拉输入就可以
第三步配置USART,直接使用一个结构体,
第四步如果只是需要发送的功能直接开启USART,初始化就结束了。如果需要接收的功能还需要配置中断,在开启USART之前加上ITConfig和NVIC的代码就可以了。。如果要发送数据调用接收的函数就可以了。如果要调用发送的数据调用发送的函数就可以了。如果要获取发送和接收的状态,就调用获取标注位的函数
补江科大
状态寄存器SR(Status Register)存放各种标志位
数据寄存器DR(Data Register) 存放关键数据
配置寄存器CR(Config Register)存放各种配置参数
这三类寄存器基本上每个外设都有
#include "stm32f10x.h" // Device header
void Serial_Init(void)
{ GPIO_InitTypeDef PA10;
USART_InitTypeDef USAASS;
//开启时钟
RCC_APB2PeriphResetCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOA,ENABLE);
//开启PA10,PA9
PA10.GPIO_Mode=GPIO_Mode_AF_PP;
PA10.GPIO_Pin=GPIO_Pin_9;
PA10.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&PA10);
USAASS.USART_BaudRate=9600;
//不适用模式
USAASS.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
//写模式
USAASS.USART_Mode=USART_Mode_Tx;
//校验位不需要
USAASS.USART_Parity=USART_Parity_No;
//选择一位停止位
USAASS.USART_StopBits=USART_StopBits_1;
//字长
USAASS.USART_WordLength=USART_WordLength_8b;
USART_Init(USART1,&USAASS);
//开启USART1
USART_Cmd(USART1,ENABLE);
}
void Serial_SendByte(uint8_t Byte)
{
//把数据传入串口
USART_SendData(USART1,Byte);
//把数据写入TDR了,写完了之后,等TDR转移到移位寄存器才可以放心。
//如果数据在TDR等待我们在写入数据,就会数据覆盖。所以在发送后我们还需要等待一下标志位
//等待这个位TXE置一
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
//不需要手动清零
}
int main(void)
{
Serial_Init();
Serial_SendByte(0x41);
//就会产生0x41对应的波形,可以通过USB转串口传电脑
while(1)
{
}
}
数据模式
HEX模式,以原始数据形式显示,收到什么数据显示什么数据,
文本模式/字符模式:以原始数据编码后的形式显示,每个数据通过查找字符集显示。
数据发送接收
数据的发送和接收都是原始数据的形式
正在上传…重新上传取消
使用printf必须打开
#include "stm32f10x.h" // Device header
void Serial_Init(void)
{ GPIO_InitTypeDef PA10;
USART_InitTypeDef USAASS;
//开启时钟
RCC_APB2PeriphResetCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOA,ENABLE);
//开启PA10,PA9
PA10.GPIO_Mode=GPIO_Mode_AF_PP;
PA10.GPIO_Pin=GPIO_Pin_9;
PA10.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&PA10);
USAASS.USART_BaudRate=9600;
//不适用模式
USAASS.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
//写模式
USAASS.USART_Mode=USART_Mode_Tx;
//校验位不需要
USAASS.USART_Parity=USART_Parity_No;
//选择一位停止位
USAASS.USART_StopBits=USART_StopBits_1;
//字长
USAASS.USART_WordLength=USART_WordLength_8b;
USART_Init(USART1,&USAASS);
//开启USART1
USART_Cmd(USART1,ENABLE);
}
void Serial_SendByte(uint8_t Byte)
{
//把数据传入串口
USART_SendData(USART1,Byte);
//把数据写入TDR了,写完了之后,等TDR转移到移位寄存器才可以放心。
//如果数据在TDR等待我们在写入数据,就会数据覆盖。所以在发送后我们还需要等待一下标志位
//等待这个位TXE置一
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
//不需要手动清零
}
int main(void)
{
Serial_Init();
Serial_SendByte(0x41);
//sprintf()使用
char sting[100];//定义100个字符
sprintf(sting,'num=%d\r\n',666);
Serial_SendByte(sting);
//就会产生0x41对应的波形,可以通过USB转串口传电脑
while(1)
{
}
}
#include <stdio.h>
//发送数字函数
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for(i=0;i<Length;i++)
{
Serial_SendByte(Array[i]);
}
}
//发送字符串函数
void serial_sendstring(char *string)
{
uint8_t i;
for (i=0; string[i]!='0';i++)
{Serial_SendByte(string[i]);
}
}
//次方函数
uint32_t serial_pow(uint32_t X,uint32_t Y)
{
uint32_t Result=1;
while (Y--)
{
Result*=X;
}
return Result;
}
//发送数字函数
void Serial_sendNumber(uint8_t Number,uint8_t Length)
{
uint8_t i;
for(i=0;i<Length;i++)
{
Serial_SendByte(Number/serial_pow(10,Length-i-1)%10+'0');
//加上字符的0
}
}
//printf函数的移值方法
//要#include<stdio.h>
//主函数输入fputc("num=%d\r\n",666)输出num=666
int fputc(int ch,FILE *f)
{
Serial_SendByte(ch);
return ch;
}
//sprintf如果多个串口想用都可以
//封装sprintf
#include<stdarg.h>
void serial_printf(char *format,...)
{
char sting[100];
va_list arg;
va_start(arg,format);
vsprintf(sting,format,arg);
va_end(arg);
Serial_SendByte(sting);
}
传送乱码解决
我们选的UTF-8编码,所以终发送代串口,汉字会以UTF-8的方式编码
方法一
需要给编译器输入参数 --no-multibyte-chars
方法二
切换为GN2312
但当时文件还是UTF-8,需要重新删除文件再打开文件
USART_FLAG_RXNE 是USART的RXNE标志位
串口发送+接收
读查询方式
#include "stm32f10x.h" // Device header
void Serial_Init(void)
{ GPIO_InitTypeDef PA10;
//定义两个脚可以只用一个结构体,
USART_InitTypeDef USAASS;
//开启时钟
RCC_APB2PeriphResetCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOA,ENABLE);
//开启PA10,PA9
PA10.GPIO_Mode=GPIO_Mode_AF_PP;
PA10.GPIO_Pin=GPIO_Pin_9;
PA10.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&PA10);
PA10.GPIO_Mode=GPIO_Mode_IPU;
PA10.GPIO_Pin=GPIO_Pin_10;
PA10.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&PA10);
USAASS.USART_BaudRate=9600;
//不适用模式
USAASS.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
//写模式+读模式
USAASS.USART_Mode=USART_Mode_Tx|USART_Mode_Rx;
//校验位不需要
USAASS.USART_Parity=USART_Parity_No;
//选择一位停止位
USAASS.USART_StopBits=USART_StopBits_1;
//字长
USAASS.USART_WordLength=USART_WordLength_8b;
USART_Init(USART1,&USAASS);
//开启USART1 对USART来说有查询和中断俩个模式。使用查询就结束了。如果使用中断要开启中断
//查询的流程是在主函数不断的判读RXNE标志位,如果置1,说明收到了数据了,在调用ReciveData,读取寄存器就可以了
USART_Cmd(USART1,ENABLE);
}
void Serial_SendByte(uint8_t Byte)
{
//把数据传入串口
USART_SendData(USART1,Byte);
//把数据写入TDR了,写完了之后,等TDR转移到移位寄存器才可以放心。
//如果数据在TDR等待我们在写入数据,就会数据覆盖。所以在发送后我们还需要等待一下标志位
//等待这个位TXE置一
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
//不需要手动清零
}
uint8_t RxData;
int main(void)
{
Serial_Init();
Serial_SendByte(0x41);
//就会产生0x41对应的波形,可以通过USB转串口传电脑
while(1)
{//读取RXNE位
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET)
{//数据放入RxData
//读RXData自动清除标志位(手册可查USART_SR状态寄存器)
RxData=USART_ReceiveData(USART1);
//展示第一行第一列长度为2
OLED_ShowHexNum(1,1,RxData,2);
}
}
}
读中断查询方式
#include "stm32f10x.h" // Device header
void Serial_Init(void)
{ GPIO_InitTypeDef PA10;
//定义两个脚可以只用一个结构体,
NVIC_InitTypeDef NVIC_InitStructure;
USART_InitTypeDef USAASS;
//开启时钟
RCC_APB2PeriphResetCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOA,ENABLE);
//开启PA10,PA9
PA10.GPIO_Mode=GPIO_Mode_AF_PP;
PA10.GPIO_Pin=GPIO_Pin_9;
PA10.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&PA10);
PA10.GPIO_Mode=GPIO_Mode_IPU;
PA10.GPIO_Pin=GPIO_Pin_10;
PA10.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&PA10);
USAASS.USART_BaudRate=9600;
//不适用模式
USAASS.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
//写模式+读模式
USAASS.USART_Mode=USART_Mode_Tx|USART_Mode_Rx;
//校验位不需要
USAASS.USART_Parity=USART_Parity_No;
//选择一位停止位
USAASS.USART_StopBits=USART_StopBits_1;
//字长
USAASS.USART_WordLength=USART_WordLength_8b;
USART_Init(USART1,&USAASS);
//开启RXNE标志位到NVIC(中断)的输出
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
//NVIC设置
//选择中断组2
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_2);
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);
//然后在中断函数设置 中断函数的名字必须在启动文件找
//开启USART1 对USART来说有查询和中断俩个模式。使用查询就结束了。如果使用中断要开启中断
//查询的流程是在主函数不断的判读RXNE标志位,如果置1,说明收到了数据了,在调用ReciveData,读取寄存器就可以了
USART_Cmd(USART1,ENABLE);
}
uint8_t serial_Rxdata;
uint8_t serial_Rxflag;
void Serial_SendByte(uint8_t Byte)
{
//把数据传入串口
USART_SendData(USART1,Byte);
//把数据写入TDR了,写完了之后,等TDR转移到移位寄存器才可以放心。
//如果数据在TDR等待我们在写入数据,就会数据覆盖。所以在发送后我们还需要等待一下标志位
//等待这个位TXE置一
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
//不需要手动清零
}
uint8_t RxData;
int main(void)
{
uint8_t serial__getRxData(void);
uint8_t serial_getRxflag(void);
Serial_Init();
Serial_SendByte(0x41);
//就会产生0x41对应的波形,可以通过USB转串口传电脑
while(1)
{//读取RXNE位
if(serial_getRxflag()==1)
{//数据放入RxData
//读RXData自动清除标志位(手册可查USART_SR状态寄存器)
RxData=serial__getRxData();
Serial_SendByte(RxData);
//展示第一行第一列长度为2
OLED_ShowHexNum(1,1,RxData,2);
}
}
}
//读后自动清除的功能
uint8_t serial_getRxflag(void)
{
if(serial_Rxflag==1)
{
serial_Rxflag=0;
return 1;
}return 0;
}
uint8_t serial__getRxData(void)
{
return serial_Rxdata;
}
//中断函数的名字找到后重定义,写中断函数
void USART1_IRQHandler(void)
{//读取RXNE位
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET)
{
//如果读取RXNE位就不用清零(表示数据来到移位寄存器),不读就要清
//读取输入
serial_Rxdata=USART_ReceiveData(USART1);
serial_Rxflag=1;
//清除RXNE位
USART_ClearFlag( USART1, USART_IT_RXNE);
}
}
USART串口数据包
数据包作用是:把一个个单独的数据给打包起来,方便我们进行多字节的数据通信。在之前我们写了单个字节的发送和接收。但在实际应用中,我们需要把多个字节打包为一个整体来进行发送。
数据包格式的定义
HEX数据包
比如说我们有一个陀螺仪传感器,需要我们用串口发送数据到STM32,陀螺仪的数据,比如说X轴一个字节,Y轴一个字节,Z轴一个字节。总共三个数据需要连续不断的发送,当你像这样,XYZXYZXYZ连续的发送的时候。就会出现一个问题就是接收方,它不知道这数据哪个对应X,哪个对应Y,哪个对应Z。因为接收方可能会从任意位置开始接收。所以数据会出现错位的现象.这个时候我们需要研究一种方式需要对数据进行分割,把XYZ这一批数据分割开,分成一个个数据包。这样接收的时候就知道了,数据包第一个数据是X,第二个是Y,第三个是Z。这就是数据包的任务,就是把属于同一批的数据进行打包和分割,方便接收方进行识别。关于分割打包的方法可以自己设计。比如说设置在XYZXYZ数据包中,数据包的第一个数据是,也就是X的数据包,他最高位置一,其余数据包最高位置0。当我接收到数据就判断一下最高位,如果是1就是X数据,然后紧跟着的数据就是YZ。实际的例子就是UTF-8的编码方法。但这种数据包分割方法破环了原有的数据,使用起来比较复杂。我们的串口数据包,通常使用的是额外添加包头包尾的方式。比如说下图
第一种是固定包长,也就是每个数据包的长度都固定不变。数据包前面是包头后面是包尾。
第二种是可变包长,含包头包尾。也就是每个数据包的长度可以是不一样的。
他们的格式可以根据需求,自己规定的,也可以是自己买个模块,别的开发者规定的。那这里规定的是比如固定包长这里,一批数据规定有4个字节,在这4个字节之前加一个包头。比如定义0XFF为包头,在4个字节之后,加一个包尾,比如定义0XFE为包尾。当我们接收到0XFF之后,我就知道了一个数据包来了,接着再接收4个字节。就当作数据包的第1,2,3,4个数据,存在一个数组里。最后跟一个包尾,当我们收到0XFE之后就可以置一个标志位,告诉程序,我收到了一个数据包。然后新的数据包过来,再重复之前的过程。这样就可以再一个连续不断的数据流中分割出我们想要的数据包了。
问题一:包头包尾和数据载荷重复的问题
定义0XFF为包头,0XFE为包尾,如果我传输的数据就是0XFE和0XFF怎么办?
如果数据和包头包尾重复可能会引起误判,对于这个问题有以下几个解决方法。
第一种,限制载荷数据的范围。如果可以的话,我们可以在发送的时候,对数据进行限幅,比如XYZ三个数,变化的范围都可以是0~100.那我们可以在载荷中只发送0~100的数据。这样就不会和包头包尾重复了。
第二种,如果无法避免载荷数据和包头包尾重复,那我们就尽量使用固定长度的数据包。只要我们通过包头包尾对齐了数据。我们就可以知道哪个数据是包头包尾,哪个数据是载荷数据。在接收数据时,我们会判断它是不是包头包尾,在接收数据时,我们会判断它是不是确实是包头包尾。用于数据对齐。这样在经过几个数据包的对齐之后,剩下的数据包就不会出现问题了。
第三种,增加包头包尾的数量,并且让它尽量呈现出载荷数据出现不了的状态。比如使用FF,FE为包头,FD,FC为包尾。这样也可以避免载荷数据和包头包尾重复的情况发送。
第二个问题,这个包头包尾并不是全部都需要的。可以只要一个包头,把包尾去掉,这样数据包的格式就是一个包头FF,加4个数据。这样也可以,不过载荷和包头重复问题更严重一些。不确定包头FF和数据FF。
第三个问题,固定包长和可变包长的选择
对于HEX数据包来说,如果载荷出现和包头包尾重复的情况,那就最好选择固定包长,这样可以避免接收错误。如果又会重复,又可以选择可变包长,那么数据容易就乱套了。 ----- 如果载荷不会和可变包头包尾重复,那可以选择可变包长。数据长度像这样,4位,3位等待,1位,10位来回任意变肯定没问题。因为包头包尾是唯一的,只要出现包头,就开始数据包,只要出现包尾就结束数据包,这样就非常灵活了。
第四个问题,各种数据转换成字节流的问题
这里的数据包都是一个字节一个字节组成的,如果想发送16位的整形数据,32位的整型数据,float,double都可以,甚至是结构体,其实都没问题。因为它们内部都是由一个字节一个字节组成的,只需用一个uint8_t指向它,把它们当成一个字节数组发送就可以了
文本数据包
文本数据包和HEX数据包就分别对应了文本模式和HEX模式,在HEX数据包里,数据都是以原始的字节数据本身呈现的,而在文本数据包里,每个字节都经过了一层编码和译码。最终表现出来的就是文本格式,但实际上每个文本字符的背后,其实都还是一个HEX数据
这同样也是固定包长和可变包长两种模式,由于数据译码成为了字符形式,就会存在大量的字符可以作为包头和包尾,可以有效避免载荷和包头包尾重复的问题。比如图中规定的是@作为包头,\r\n也就是换行符作为包尾。在载荷数据中间出现除了包头包尾的任意字符。在包头包尾之间可以出现除了包头包尾的任意字符。这很容易做到,所以文本数据包基本不用担心载荷和包头包尾重复的问题,使用非常灵活。可变包长,各种字母,符号,数字,都可以任意使用。当我们接收到载荷数据之后得到的就是一个字符串,在软件中再对字符串进行操作和判断。就可以实现各种指令控制的功能。而且字符串数据包表达的意义很明显,可以把字符串数据直接打印到串口助手上。什么数据,什么指令一眼就可以看明白。所以这个文本数据包通常会以换行为包尾。
HEX数据包与文本数据包的优劣
HEX数据包 优先是传输最直接,解析数据非常简单。比较适合模块发送原始的数据,比如一些使用串口通信的陀螺仪,温湿度传感器。缺点是灵活性不足,载荷容易和包头包尾重复。
文本数据包,优点是数据直接易理解,非常灵活。比较适合一些输入指令进行人机交互的场合。比如蓝牙模块常用的AT指令,CNC和3D打印机常用的G代码。都是文本数据包的格式。缺点是解析的效率低。比如发送数据100,HEX数据包就是一个字节100,文本数据包就是三个字节的字符。‘1’,‘0’,‘0’。收到后要把字符转成数据,才能得到100