STM32串口收发理论与实践详解

如果不想看的可以直接使用git把我的代码下载出来,里面工程挺全的,后期会慢慢的补注释之类的

码云地址: stm32学习笔记: stm32学习笔记源码

如果不会使用git快速下载可以选择直接下载压缩包或者去看看git的使用

git的使用(下载及上传_git如何下载文件_八月风贼冷的博客-CSDN博客

主要完成的这两个工作

1:串口发送

2:串口收发

本篇为单字节的收发,如果是需要传输modbus通信协议那样的数据包请去这里看多字节的收发

stm32f103串口多字节接收_stm32串口接收多个字节_是小刘不是刘的博客-CSDN博客

目录

1:串口理论部分

1:通信的目的和通讯协议·

 2、串口通信

 3、硬件接线

 4、电平标准

5、串口参数及时序

6、串口时序图

1)发送一个0x55(起始位为低,低位先行0x55最后高位结束)

2)发送两个0x55带停止位1位

 3)加上一个偶校验

 7,USART外设

 8usart硬件框图

9 USART简化框图

 10 字长设置

 11 停止位设置

​12 起始位的侦测

 13 数据位检测

​14 波特率的计算

 15 数据模式

二、代码部分

1,串口发送:

1)GPIO配置

2)串口配置

串口发送数据:

1发送一个Byte

 2发送一个数组

3 发送一个字符串

 4 发送一个数字(比如你想发送123456 这种一串数字

1第一种直接用c语言的库函数sprintf和strcat

2自己写一个转换函数

 5 移植c的printf打印函数

 6 想在多个串口都能打印

2 串口接收

1,轮询标志位RXNE

 2,中断的方式接收


1:串口理论部分

1:通信的目的和通讯协议·

通讯协议有很多,USART、IIC、SPI、CAN这些都是已经约定好了的协议,通过时序图可以看出协议的要求,然后根据这些要求来进行代码的编写。

异步与双工:异步或同步:同步就是两个设备通过时钟线来协议通讯。

                                           异步就是不需要时钟线的参与,一般都有波特率的参与。

                      全双工半双工:全双工设备都有两根通讯线,收发互不影响。

                                                半双工发的时候不能同时接收,收的时候不能同时发。

 2、串口通信

一些设备需要通过stm32的USART外设与其它设备进行通讯,调试设备的时候也能直接通过串口助手打印信息,可以直接打印没有使用屏幕调试那么麻烦,有时候也比打断点直观。

 3、硬件接线

TX与RX需要交叉接TX和RX是相较于地线的电平,所以需要供地,如果两个设备的供地不一样是无法进行通讯的。

比如单片机与电脑通讯,就需要使用一个ch340将USB转为串口通讯通讯,就像上面的第一张图,usb转串口模块,一端接usb一端接单片机的TX和RX就可以实现单片机与串口通讯。

 4、电平标准

上面写了电平标准不一致需要加入电平转换芯片,一般我们使用的就是TTL电平,但是在工业控制一些环境会使用到232或者485标准,小型器件一般只使用TTL。

如果需要485或者232的则需要购买TTL转485或者232的转换芯片。 

5、串口参数及时序

 波特率:每秒传输码元的个数,一个码元在二进制的情况下就是一个bit,波特率与比特率转换公式Rb = RB log2M (其中M是进制数)在m等于2的情况下,波特率等于比特率,所以不用管这两种转换和叫法之类的。

起始位:必须有一个标志位为低来告诉对方我要开始

数据位:看图,可以与校验位分开描述,也可以一起描述(低位先行)

校验位:奇偶校验,通过对数据插入1让其1的个数为奇数或者偶数,读取的时候判断是否为偶数或者奇数个以此来判断数据是否正确。

停止位:固定高

6、串口时序图

1)发送一个0x55(起始位为低,低位先行0x55最后高位结束)

波特率:9600  1秒9600bit 差不多一个bit就是100us左右

起始位:s 低电平标志

结束位:p 高电平标志 

校验位:没有加校验位

2)发送两个0x55带停止位1位

 3)加上一个偶校验

这里1的个数已经为偶数个了,所以校验位为0.

 7,,USART外设

硬件流控制:多加两根线,防止发送过快,多的一根线通过高低电平来告诉主机是否准备好,准备好了才会发送。

 8、usart硬件框图

主要部分为上面的一小部分,下面的只是上面硬件的具象化。

串口通讯主要的就是TX和RX,这里可以看得出来一个是连接到了发送数据寄存器和接收数据寄存器。虽然硬件上TDR和RDR分为两个硬件描述,但是在软件中都为DR寄存器

发送数据:通过TXE标志位来判断是否发送数据寄存器为空,如果为空就向TDR写入数据,这时候硬件会检测移位寄存器是否有数据正在移位,如果没有那TDR的数据就会被全部移到移位寄存器去,比如现在TDR里面的是0x55就是01010101 全部移入发送寄存器,发送寄存器一位一位的向右发送移出,这样也就形成了低位先行。

接收数据:数据接收到的数据存放在接收移位寄存器,判断标志位RXNE,当移位寄存器的数据被移到DR寄存器时置1,表示DR有数据可以读出。

 硬件流控制:两根线RTS和CTS:交叉连接其他设备的这两根线。

可以做到空闲时再发送数据,不会造成数据的拥堵。

9 USART简化框图

简化图基本和整体框图的上半部分一样,首先是配置两个GPIO口为RX或者TX,然后通过库函数配置他的数据位,以及校验位等。

波特率发生器,有时钟输入来算,但是如果使用库编程可以直接写想要的波特率进去就不需要自己去算了。

 10 字长设置

串口字长的设置,可以设置为9位也可以设置为8位,推荐9位1个校验位或者8位没有校验位

这样数据包就是8位,比较舒服。

 11 停止位设置

知道有这几种停止位就行了,只要两个串口配置的是一样的他就能收发。 

12 起始位的侦测

在检测到0后,以波特率的16倍频率进行采样,在一位里进行16次采集,因为有时候会有干扰信号让信号线为0,所以做一个多次采集(会在3,5,7次的时候进行采样与8 9 10次的时候采样),在一段时间内有多个0才能被当做起始信号。如果检测到3个中有1个为1,则会置位一个干扰标志位,但是依旧会判定这个信号为起始信号,若两个为1则判断为噪声之后重新采样。

在第8 9 10的时候为数据中心,之后采集数据也会在第8 9 10位的时候采样。

 13 数据位检测

也会 多次采集,3个数据若两个为1或0,则会认为这个数据位为1或者为0.

14 波特率的计算

波特率=时钟/16div 这里div就是我们需要写入寄存器的值

例子:配置波特率为9600=72m/16div   这里算出来DIV是468.75

我们将得到的数转为二进制写到寄存器就好

整数部分:010001101000  小数部分:11 (0.75×2取整为1.5取1,然后0.5×2区1)

但是使用固件库编程不需要我们自己计算,直接写入想要的波特率的值就行。

 15 数据模式

收发 双方统一数据模式,有ASCll模式和HEX模式,两边不一样就会乱码。

二、代码部分

1,串口发送:

1)GPIO配置

(这里只写了我得配置,可以自己去GPIO的文件看看具体有哪些参数

首先开启两个引脚,并且开启对应的时钟线

引脚配置:mode这个查一下引脚配置,Tx输出配置为推挽复用输出,Rx配置为浮空或者上拉

                  GPIO配置查一下引脚表:只使用了输入输出,所以只配置输入输出。 

               速率:随便选一个吧(还有就是输入配不配速率都没用

2)串口配置

(这里只写了我得配置,可以自己去串口的文件看看具体有哪些参数

1.USART_BaudRate:波特率配置:这个和电脑通讯,所以和电脑配置的一样就行没什么特殊要求,但是如果是和其他设备通讯,得先看一下设备得速率。

2.HardwareFlowControl:硬件设备流上面写过,通过两根线来控制要不要接收这里没有使用所以直接选不使用。

3.USART_Mode:模式配置,这里讲输入输出都开启。

4.USART_Parity:奇偶效验位,这里我选择了不开

5.USART_StopBits:停止位,配置为1位

6.USART_WordLength:数据位8位(前面说过,如果配置8位就不要校验位,9位就开一个校验位,因为8位数据传输会舒服一点。

最后调用USART_Cmd();使能啊,不然串口没有时钟,动不了的。

void Usart_Config(void)
{
	 USART_InitTypeDef USART_InitStruct;
	 GPIO_InitTypeDef GPIO_InitStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_9;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IN_FLOATING;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_10;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	
	USART_InitStruct.USART_BaudRate=115200;
	USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
	USART_InitStruct.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
	USART_InitStruct.USART_Parity=USART_Parity_No;
	USART_InitStruct.USART_StopBits=USART_StopBits_1;
	USART_InitStruct.USART_WordLength=USART_WordLength_8b;
	USART_Init(USART1,&USART_InitStruct);
	
	USART_Cmd(USART1,ENABLE);
}

串口发送数据:

1、发送一个Byte

调用SendData()函数:这个函数会讲输入的数据与0x01FF按位与写入到DR寄存器

之后判断TXE标志位:这个标志位前面写了是用来判断发送数据寄存器是否被移到了移位寄存器,然后判断这个TXE标志位是否被置为1,因为被置为1则代表数据寄存器的数据发送完成。

循环判断是否等于0,如果为0就一直循环

//main.c
int main(void)
{
	Usart_Config();
	Send_Byte(0xff);
    Send_Byte('A');
  while(1)
	{	

	}	
}
//Usart.c
void Send_Byte(u8 Byte)
{
	 USART_SendData(USART1,Byte);
	 
	 while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
}

然后在main.c里面调用Usart函数完成配置,发送0xff到串口,这里将电脑的串口配置的和代码一样,波特率115200,校验位无,数据为8,停止位1。

接收模式配置为HEX模式:这样才能收到我们发送的0xFF,若想接收ascll则直接发送字符

 2、发送一个数组

输入参数为:数组,长度

循环打印数组内的值,一般显示方式都为HEX模式。拿这个打印字符没必要吧= =,一般就是一些协议规定了发送的值之类的。

//main.c
int main(void)
{
	u8 arr[]={0x11,0x22,0x33,0x44};
		
	Usart_Config();
	

	Send_Array(arr,4);
  while(1)
	{	

	}	
}

//Usart.c
void Send_Array(u8* arr,u8 len)
{
	u8 i=0;
	for(i=0;i<len;i++)
	{
		 Send_Byte(arr[i]);
	}
}

3 发送一个字符串

传入一个字符串,指针的++是受指针类型影响的,这里char类型++一次跳跃一个字节,如果是int类型就会一次跳跃4个字节,不熟悉的话可以先去看看指针的帖子。

//main.c
int main(void)
{
	u8 arr[]={'a','b',0x33,0x44};		
	Usart_Config();
	Send_String("string");
//	Send_Byte(0xff);
	//Send_Array(arr,4);
  while(1)
	{	

	}	
}
//Usart.c
void Send_String(char* string)
{
	u8 i=0;
	for(i=0;string[i]!='\0';i++)
	{
		Send_Byte(string[i]);
	}
}

 

 4 发送一个数字(比如你想发送123456 这种一串数字

发送数字其实并不是直接可以吧整个数字发送过去,如果你发送一个数字过去会被转义为一个字符,假如你发33在ascll模式下就会把他转义为 ! ,所以我们不能直接打印数字,只能将数字转为字符打印,

1,第一种直接用c语言的库函数sprintf和strcat

这样就能直接输入一个数了

用sprintf 把数据以字符的形式填入到string数组里面:这里就直接是字符了,这函数初始化字符串也是很好用的

然后使用strcat连接给他一个换行

//main.c
int main(void)
{
	u8 arr[]={'a','b',0x33,0x44};
		
	Usart_Config();
//	Send_String("string");
//	Send_Byte(0xff);
	//Send_Array(arr,4);
  Usart_Send_32bit_Data(66666);
  while(1)
	{	

	}	
}
//usart.c
#include <string.h>
#include <stdio.h>
void Usart_Send_32bit_Data(uint32_t value)
{
	  char strings[8];
	  char Enter[3]="\r\n";
	  sprintf(strings,"%d",value);
	  strcat(strings,Enter);
	  Send_String(strings);
}

 这就直接将数据以字符串的形式打印出来了,

2、自己写一个转换函数

将一个数字的每一位拆分出来,再与0的ascll地址相加,就能将每一位都打印出来

然后就是如何拆分每个位出来,这个很简单,一般c语言的算法题最开始就会让我们分别打印出123的个位十位和百位,我们就直接采用除后取余

比如123就除以100=1.23 然后对10取余就是1 ,123除以10=12对10取余 就余2 ,然后123除以1=123 再除10取余=3  这时候就把123取出来了。

 然后就是函数的设计了,因为输入的数据是不定长的,所以我们肯定是不可以手动让他除以几的,这时候我们就需要一个记录长度的参数输入了(因为我们需要知道这个数现在该除以多少,需要用这个数据来计算)。

所以这里的函数设计肯定就是数字+长度的传入了,void Send_Number(int num,u8 len)

这里数字和长度有了也能计算他应该除以多少了,毕竟摸10都是固定的。就比如100就是10的2次方,10就是10的1次方,1就是10的0次方,然后咋们就开始凑公式嘛,说好听点就是经验公式= =

//main.c
Send_Number(111111,6);

//usart.c
int Send_Pow(int x,int y)
{
	 int Result=1;
	 while(y--)
	 {
		 Result *= x;
	 }
	 return Result;
}

void Send_Number(int num,u8 len) //123
{
	  u8 i=0;
	  for(i=0;i<len;i++)
		{
			Send_Byte(num / Send_Pow(10,len-i-1) %10 +'0'); //2 1 0
		}
}

这里我们自己写一个平方函数,当然也能直接使用库里面的pow函数,然后往里面凑公式,之后将分离出来的数字+‘0’,加了一个字符0,这就是加了一个0的地址偏移量,将其在字符0的基础上+数字之后得到ascll,这里可以看图,我们可以直接写0x30也能写‘0’。

这样我们就能成功发送数字11111了

 5 移植c的printf打印函数

首先重新定义fputc函数到串口,printf是调用的fputc,这里我们将内部改为发送到串口就行

主函数直接打印就行,记得引用头文件

//main.c
printf("hello");

//usart.c
#include <stdio.h>
int fputc(int ch,FILE *f)
{
	 Send_Byte(ch);
	 return ch;
}

 6 想在多个串口都能打印

这里我们需要重定向一个 sprintf函数,一样的直接调用这个函数就行,如果用的其他串口你就把函数改个名字定义到其他串口去。

可变参的参考资料,这里我直接粘了正点原子的写法把发送Byte直接改写为了发送string’

C语言可变参数详解_c可变参数_陈 洪 伟的博客-CSDN博客

#include <stdarg.h>
void Usart1_Printf(char *fmt,...)
{
	char string[100];
  va_list ap;
	va_start(ap,fmt);
	vsprintf(string,fmt,ap);
	va_end(ap);
	Send_String(string);
}

2 串口接收

1,轮询标志位RXNE

读数据寄存器非空的时候就代表可以读出数据,这时候我们读取DR寄存器就能得到数据

想知道我们有没有接收成功,我们就拿一个变量把发送的参数接收回来,然后再发送出去,可以看出来我们是接收成功了的。

uint8_t RxData;
int main(void)
{
	//u8 arr[]={'a','b',0x33,0x44};
		
	Usart_Config();
  while(1)
	{	
			//轮询标志位
		  if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET)
			{
				 RxData=USART_ReceiveData(USART1);
				 Send_Byte(RxData);
			}
	}	
}

 2,中断的方式接收

首先开启中断的通道,串口中断 ,这里不清楚的可以参考一下以前写的外部中断

stm32f103中断的使用_八月风贼冷的博客-CSDN博客

这里因为是串口中断,所以不用选择引脚和配置EXTI外部中断了,开启RXNE标志位到NVIC的输出

USART_ITConfig函数的功能是使能或者失能指定的USART串口中断,这里砸门得先使能其中断。

USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);

之后就是配置NVIC了首先配置中断组,这个东西说过,一个工程只生效一个,可以考虑直接写在主函数开头,这里配置为中断组2,不明白的优先级组或者优先级的可以先去看看之前的那篇中断(因为再写一遍就篇幅有点长了)

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

 然后配置NVIC初始化结构体(老朋友了,先把参数点出来,然后赋值)

这里分别是,中断源,然后使能,抢占优先级 响应优先级  (越大优先级越高)

这里我们得去stm32f103.h找到对应的中断通道这里有串口1中断,我们复制了粘进去。

然后使能,中断优先级和抢占随便写一个就行,因为只有这一个中断

	NVIC_InitTypeDef  NVIC_InitStruct;
	NVIC_InitStruct.NVIC_IRQChannel=USART1_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=2;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority=2;

然后就是写一个中断了,这个东西不能自己命名哦记住去hd文件找一下

 找到相关的中断名,然后写一个中断,这里和刚刚的轮询代码是一样的,但是我们是写在了中断里面,只有这个中断的事件被触发了之后才会执行这个代码,减少了cpu的占有用,最后结果和刚刚轮询接收标志位效果是一样的。

uint8_t RxData;

void USART1_IRQHandler(void)
{
		  if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET)
			{
				 RxData=USART_ReceiveData(USART1);
				 Send_Byte(RxData);
			}
}

 

物联沃分享整理
物联沃-IOTWORD物联网 » STM32串口收发理论与实践详解

发表评论