目录
通信协议
通信的目的是将一个设备的数据传送到另一个设备,扩展硬件系统
通信协议是制定通信的规则,通信双方按照协议规则进行数据收发
常见的通信协议有以下几种

半双工是只能一端发送,另一端接收,而全双工是两端可以同时发送和接收,发送接收使用的两根线,互不影响。除此之外还有单工,就是只能单向发送,另一端不能发送。
同步通信是发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式。
异步通信是发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。
所以同步是阻塞模式,异步是非阻塞模式。
所以SPI和I2C都是同步通信,因为他们都有时钟线,UART是异步通信方式,他只有两个数据线,发送完数据不会确认你是否接收到。
串口通信概念
串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信。
单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力。
简单双向串口通信有两根通信线(发送端TX和接收端RX)
TX与RX要交叉连接
当只需单向的数据传输时,可以只接一根通信线
当电平标准不一致时,需要加电平转换芯片
因为TX和RX是单端信号,他们的电平是相对GND的,所以这三根线是必须加的。如果通信双方有单独的供电,VCC就不需要单独接在一起,如果一方没有单独供电就需要接在一起。
什么是单端信号:
单端信号是相对于差分信号而言的,单端输入指信号有一个参考端和一个信号端构成,参考端一般为地端。单端信号只使用一根信号线进行传输。由于信号每一点的电压都是相对于地而言的。而在长距离传输时,不同位置地平面的电位可能会有差别,这就导致了信号的误差,即不稳定因素。加之可能会有干扰信号,这就导致了单端信号的抗干扰能力很差。
串口常用的电平标准
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
TTL电平:+3.3V或+5V表示1,0V表示0
RS232电平:-3 ~ -15V表示1,+3 ~ +15V表示0
RS485电平:两线压差+2 ~ +6V表示1,-2 ~ -6V表示0(差分信号)
类似串口这种低压设备使用的都是TTL电平,如果需要其他的电平就需要加电平转换芯片,但是在软件层面都属于串口,所以在程序上没有什么变化。
串口及时序
波特率:串口通信的速率
起始位:标志一个数据帧的开始,固定为低电平
数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
校验位:用于数据验证,根据数据位计算得来
停止位:用于数据帧间隔,固定为高电平
上述规则是串口协议规定的,每个字节都在一个数据帧中。每个帧都有起始位,数据位和停止位。上述图片右边,在数据位中后面还可以加一个奇偶校验位。
串口波特率是串口发送数据的速率,如果接收慢了,就会漏掉某些位,如果接收快了,就会重复接收某些位。所以发送接收双方要约定好速率。
起始位:串口的空闲状态必须是高电平,在传输的时候,必须先发送一个起始位,必须是低电平,打破空闲状态,产生一个下降沿,这个下降沿就是告诉接收设备,这一帧的数据要开始了。
停止位:用于发送完数据后的数据帧间隔,固定为高电平,同时也可以为下一个起始位做准备的。使下一帧数据可以产生下降沿。
校验位:串口使用的是奇偶校验的方法,如果数据出错了可以选择丢弃或者要求重传。可以选择无校验,奇校验和偶校验。无校验就是不进行校验,奇校验就是通过在校验位置1或者0保证数据位的1为奇数个,然后接收方通过验证是否满足奇数个 1 来确定是否传输出错。偶校验同理。但是这种校验方式只能一定程度上避免传输出错。
在STM32中根据数据比特位反转高低电平和接收端都是由USART外设自动完成的,用户不需要管。当然也可以使用定时器模拟这也波形,按照数据帧的要求,给GPIO口置高或者低。模拟接收就是读取RX端的电平,需要注意在接收的时候需要一个外部中断,在起始位的下降沿触发,进入接收状态,并且对齐采样时钟,依次采样八次。
STM32中USART
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器。S就是同步的意思。UART,少一个S的就是异步收发器。但是一般不使用同步功能,所以USART和UART使用起来没什么区别。STM32中USART只是多了个时钟输出而已,只支持时钟输出,不支持时钟输入。所以同步模式更多的是为了兼容别的协议,或者有特殊用途而设计的,并不支持两个USART之间的通信。
USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里。
自带波特率发生器,波特率发生器就是用来配置波特率的,其实就是一个分频器,比如APB2总线给72MHz,然后波特率发生器进行分频,得到我们想要的时钟。最高达4.5Mbits/s。
可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)。
可选校验位(无校验/奇校验/偶校验)。
支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN。同步模式就是多了个时钟CLK输出。
硬件流控制:如果A设备向B设备发数据,发送太快了,此时B没准备好接收可能就造成了数据丢失。如果有硬件流控制,在硬件电路上会多出一根线。如果B没有准备好就置高电平,如果准备好了就置低电平。A接收到了B反馈的准备信号,就只会在B准备好的时候才发数据。如果B没有准备好,数据就不会发送出去。STM32是有硬件流控制的,但是一般不用。
STM32F103C8T6 USART资源: USART1、 USART2、 USART3
USART框图

左上角TX和RX就是串口的收发引脚,下面一点SW_RX,IRDA_OUT,IRDA_IN是智能卡和IrDA通信的引脚。
上面灰色框中,发送数据寄存器(TDR)和接收数据寄存器(RDR)共用一个地址,这里和51单片机的SBUF是一样的。在程序上只表现为一个寄存器即数据寄存器DR。但实际硬件中分成了两个寄存器,一个用于发送TDR,另一个用于接收RDR。
然后往下看,下面是两个移位寄存器,一个用于发送,一个用于接收发送。发送移位寄存器 的作用就是把一个字节的数据一位一位的移除去,正好对应串口协议的波形的数据位。这两个寄存器是怎么工作的呢?举个例子,比如你在某时刻给TDR
写入了0X55这个数据,在寄存器里就是二进制存储01010101。那么此时硬件检测到你写入数据了,他就会检查当前数据寄存器是不是有数据正在移位,如果没有,这个01010101就会立刻全部移动到发送移位寄存器准备发送。当数据从TDR移动到移位寄存器时,会置一个标志位,叫TXE
(TX Empty),发送给寄存器空,我们检查这个标志位,如果置 1 了,我们就可以在TDR写入下一个数据了。注意,当TXE标志位置 1 时,数据其实还没有发送数据,只要数据从TDR转移到发送移位寄存器了,TXE就会置 1 ,我们就可以写入新的数据了,然后发送移位寄存器就会在下面的发送器控制 的驱动下向右移位,然后一位一位的把数据输出到TX引脚。这里向右移位,刚好和串口协议规定的低位先行是一致的。TDR会在移位寄存器移动完成后再次转移TDR的数据。
接收移位寄存器 在接收器控制 的控制下开始接收数据,是从高位向低位这个方向移动的。当接收完成后会转移到RDR中,此时也会置一个标志位叫RXNE
(RX Not Empty)接收数据寄存器非空。检测到RXNE置 1 后就可以把数据读走了。
可以发现在左边就有上面介绍的硬件数据流控。nRTS(Request To Send)是请求发送,是输出脚,即告诉对方当前能不能收。nCTS(Clear To Send)是清除发送,是输入脚。也就是用于接收别人nRTS的信号的。前面加n意思是低电平有效。用法就是将自己的RTS接到对方的CTS,当能接收的时候,RTS就置0,对方CTS接收到后就可以一直发。当处理不过来时,就置 1 ,对方就会停止发送。
右边还要SCLK,产生同步时钟信号,是配合发送移位寄存器输出的。发送寄存器每移位一次,同步时钟电平就跳变一个周期。,时钟告诉对方,我移出去一位数据了。
中间的唤醒单元是实现串口挂在多设备,串口一般是点对点的通信,点对点只支持两个设备互相通信,是两个设备之间通信。唤醒单元可以用来实现多设备的功能,在这里可以给串口分配一个地址(在上面的USART地址处),当发送指定地址时,此设备唤醒开始工作。当发送别的设备地址时,别的设备就唤醒工作。这也就实现了多设备的串口通信。
下面中断输出控制,中断申请位就是状态寄存器里的各种标志位。状态寄存器这里有两个标志位比较重要。就是上面说的TXE和RXNE。
最下面的就是波特率发生器,波特率发生器其实就是分频器。 APB时钟进行分频,得到发送和接收移位的时钟。时钟输入是fPCLKx
,x等于 1 或2,USART1挂载在APB2,所以就是PCLK2的时钟一般是72MHz。其他的USART都挂载在APB1,所以是PCLK1的时钟一般是36MHz。之后,这个时钟进行一个分频除以一个USART Div的分频系数。 USART Div里面就是右边的一个数值,并且分为了整数部分和小数部分,因为有些波特率用72兆除一个整数的可能除不尽会有误差,所以这里分频系数是支持小数点后四位的分频就更加精准了。之后分频完之后还要再除16,得到发送器时钟和接收器时钟通向控制部分。右边如果TE为 1 就是发送使能了,发送部分的波特率就有效。如果RE为 1 就是接收器使能了,接收部分的波特率就有效。
USART基本结构

虽然结构上看着有四个寄存器,但是实际上只有一个DR寄存器可以使用。写DR进行发送,读DR进行接收。
波特率发生器
在STM32串口开始采样的时候将一个时序分为16份对起始位进行检测,避免噪声。
在起始位已经对齐了采样时钟,在采样的时候,会在第8,9,10位进行采样。这样采样是将噪声考虑进去了,如果不全为1或者0,说明产生了噪声,就按照2:1的方式来。此时就会将噪声标志位NE置1。
发送器和接收器的波特率由波特率寄存器BRR里的DIV确定
计算公式:波特率 =
多个16就是上面说的有个16倍的采样时钟。
STM32相关库函数
大部分函数都是见过的,一眼就知道怎么使用
c
void USART_DeInit(USART_TypeDef* USARTx);
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
void USART_StructInit(USART_InitTypeDef* USART_InitStruct);
void USART_ClockInit(USART_TypeDef* USARTx, USART_ClockInitTypeDef* USART_ClockInitStruct);
void USART_ClockStructInit(USART_ClockInitTypeDef* USART_ClockInitStruct);
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);
USART_ClockInit
和USART_ClockStructInit
是用来配置同步时钟输出的。
USART_SendData
发送数据,USART_ReceiveData
接收数据。
USART_ReceiveData
,读取发送过来的数据。
配置
c
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//STM32串口1的发送端,使用复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;//波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//是否使用硬件流控制
USART_InitStructure.USART_Mode = USART_Mode_Tx;//接收还是发送,如果二者都需要就或起来
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);
}
void Serial_SentByte(uint8_t Byte)
{
USART_SendData(USART1, Byte);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
使用printf
使用printf前需要打开工程选项,勾选MicroLIB
。
MicroLIB是Keil为嵌入式平台优化的一个精简库。
还需要对printf进行重定向,将printf函数打印的东西输出到串口。因为printf函数默认打印到屏幕,但是单片机没有屏幕。
需要包含stdio.h
,然后重定向fputc
函数。
c
int fputc(int ch, FILE* f)
{
Serial_SentByte(ch);
return ch;
}
重定向fputc跟printf有什么关系呢?
fputc是printf函数的底层,printf函数打印的时候就是不断的调用fputc函数一个个打印的。所以重定向一下fputc函数就会打印到串口了。
c
#include "Config.h"
#include <stdio.h>
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//STM32串口1的发送端,使用复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;//波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//是否使用硬件流控制
USART_InitStructure.USART_Mode = USART_Mode_Tx;//接收还是发送,如果二者都需要就或起来
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);
}
void Serial_SentByte(uint8_t Byte)
{
USART_SendData(USART1, Byte);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
int fputc(int ch, FILE* f)
{
Serial_SentByte(ch);
return ch;
}
c
int main(void)
{
OLED_Init();
Serial_Init();
//Serial_SentByte(0x41);
printf("Num = %d\r\n", 666);
char string[100];
sprintf(string, "Num = %d\r\n", 666);
//然后可以使用其他串口在打印
while(1)
{
}
}
重定向后,相当于将printf里面的字符串一个一个提取出来打印。因为我们使用的是Serial_SentByte重定向的,所以打印时会转为ASCII码,但是以字符接收就又会转换回字符。
接收
接收需要在初始化一下另一个引脚,并且可以选择查询或者中断接收。
如果选择中断就需要再配置一下中断,如果选择查询方式就一直查询RXNE
标志位。
串口初始化代码为
c
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//STM32串口1的发送端,使用复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//STM32串口1的接收端,使用上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA, &GPIO_InitStructure);
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);
}
main.c:
c
int main(void)
{
OLED_Init();
Serial_Init();
while(1)
{
if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET)
{
uint8_t data = USART_ReceiveData(USART1);
OLED_ShowHexNum(1, 1, data, 2);
}
}
}
中断方式:
需要加入代码来开启中断:
c
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
然后配置NVIC
c
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
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_DR(即数据寄存器,即执行了USART_ReceiveData
)后,RXNE会自动置0,所以我们读取数据后就不需要再进行清零了。
还需要写中断函数:
整体配置代码就是:
c
#include "Config.h"
uint8_t data;
uint8_t rx_flag;
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//STM32串口1的发送端,使用复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//STM32串口1的接收端,使用上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA, &GPIO_InitStructure);
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_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
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);
}
void Serial_SentByte(uint8_t Byte)
{
USART_SendData(USART1, Byte);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
int fputc(int ch, FILE* f)
{
Serial_SentByte(ch);
return ch;
}
uint8_t Get_RX_Flag()
{
if(rx_flag == 1)
{
rx_flag = 0;
return 1;
}
else
{
return 0;
}
}
uint8_t Get_USART1_Data()
{
return data;
}
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
data = USART_ReceiveData(USART1);
rx_flag = 1;
}
}