STM32f103串口通信详解
原理分析
首先,我们从串口通信的物理层和协议层来分别分析。
- 物理层
对于串口通信的物理层的标准变化有很多种,在这儿,我主要是讲解 RS-232 标准。
这儿是以 RS-232 标准的常见设备通信结构图:
由图可以看到,两个信号的 DB9 接口,通过串口信号线连接在一起,信号线是使用的 RS-232 标准传输数据信号。但是 RS-232 标准信号不能直接被控制器识别使用,所以我们需要使用一个电平转换芯片转换成 TTL 标准信号,才能实现通信。
为什么需要转换呢? RS-232 与 TTL 有什么区别呢?
上图是两种标准电平信号的区别。
上图是两种电平标准表示同一个信号的对比图。
对于 DB9 这种串口线我就不过多介绍了,我们只需要了解到下图的连线方式就可以了。
这儿要记住, RXD 与 TXD是交叉连接的。 - 协议层
串口通信的数据包又发送设备通过自身的 TXD 接口传输到接收设备的 RXD 接口,在串口通讯的协议层中,数据结构又下图所示,起始位,数据位,校验位,停止位。
波特率
由于异步通信之中是没有时钟信号的,所以需要在两个设备之间约定好波特率(及每个码元的长度)。
通讯的起始和停止信号
串口通讯的一个数据包从起始信号开始,到停止信号结束,起始信号由一个逻辑 0 表示,停止信号可以由 0.5 , 1 , 1.5 , 或者 2 来表示( 2 个位),只要两台设备约定一致就行。
有效数据
起始位之后就是有效数据(主体数据),一般 5 , 6 , 7 , 8 位长。
数据校验
由于数据通信容易受到外部干扰而导致数据偏差,可以通过加入校验位来解决,一般来说,有奇偶校验, 01 校验,或者无校验。
奇校验要求有效数据加上校验位中 1 的个数位奇数,而偶校验就要求为偶数, 0 校验校验位总位为 0 , 1 校验校验位总为 1。
现在开始介绍 STM32 的 USART 原理
接下来我们来具体分析这张图:
功能引脚
从这儿我们可以看到,在 STM32 中,它有这么一些功能引脚:
TX :发送数据的引脚
RX:接收数据的引脚
SW_RX:数据接收引脚,只用于单线和智能卡模式,属于内部引脚,没有具体的外部引脚
nRTS:请求以发送, n 代表低电平有效。如果使能这个引脚,当准备接收数据时就会变成低电平,当接收寄存器已经满了,就会置高。
nCTS:清除以发送,低电平有效,若使能,当发送数据之前会检测一下,若是低电平,则可以发送数据,若是高电平,则在当前数据发送完后停止发送。
SCLK:发送器时钟输出引脚,仅适用于同步模式
因为每个芯片对应的 USART 引脚不一样,所以需要去芯片资料自行查阅。
数据寄存器
在 STM32 中 USART 的数据寄存器只有低九位有效用来存放接收和发送的数据,并且第九位数据是否有效取决于 USART 控制寄存器决定。
发送和接收的数据都是放在数据寄存器中,所以数据寄存器实际上包含了两个寄存器,一个是用于发送的可写寄存器 TDR ,一个是用于接收的可读 RDR,当进行读写操作时,数据都是放在这个数据寄存器当中。
TDR 和 RDR 都是介于系统总线和移位寄存器之间。串行通信是一个位一个位传输的, 发送时把 TDR 内容转移到发送移位寄存器,然后把移位寄存器数据每一位发送出去,接收 时把接收到的每一位顺序保存在接收移位寄存器内然后才转移到 RDR。
控制器
当 USART_CR1 寄存器发送使能置 1 ,也就是 TE 置 1 时,启动数据发送(此时可以往数据寄存器里面写入数据。),发送移位寄存器的 数据会在 TX 引脚输出,当 RE 置 1 ,使能 USART 接收, RX 口开始搜索起始位,当确定到起始位后,将接收到的数据放到数据寄存器里,并把 RXNE 位置 1 。如果是同步模式, SCLK 也会输出时钟信号。
停止位时间长短可以通过 USART 控制寄存器2控制。
小数波特率的生成
USART 的接收好和发送使用同一个波特率,计算公式为:
f(CK) 为 USART 时钟,USARTDIV是一个存放波特率的寄存器
校验控制
在 STM32F103 系列的 USART 还支持奇偶校验位,当使用奇偶校验时,数据位加上校验位一共 9 位,此时的 CR1 寄存器 M 位需要置 1 ,设置为 9 位数据位。
中断控制
当然,对于串口通信中,有多个串口中断请求事件,分别代表了各种状态下的情况。
所有 USART 的控制都在下列寄存器里。
状态寄存器 SR
数据寄存器 DR
波特比例寄存器 BRR
控制寄存器 CR1
控制寄存器 CR2
控制寄存器 CR3
保护时间和预分频寄存器 GTPR
此上,就是所有关于 STM32F103 的 USART 的所有寄存器,接下来来实操固件库函数吧!
代码
#define DEBUG_USART_IRQ USART1_IRQn //宏定义中断源,方便移植,增强可读性。
#define DEBUG_USART_TX_GPIO_PORT GPIOA //宏定义TX的GPIO。
#define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_9 //宏定义TX口的引脚。
#define DEBUG_USART_RX_GPIO_PORT GPIOA //宏定义RX的GPIO。
#define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_10 //宏定义RX口的引脚。
#define DEBUG_USART_BAUDRATE 115200 //宏定义波特率。
#define DEBUG_USARTx USART1 //宏定义串口。
static void NVIC_Configuration(void) //这儿是USART的中断配置,static代表限制了此函数只能在本文件之中使用。
{
NVIC_InitTypeDef NVIC_InitStructure; //定义一个NVIC的结构体。
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置优先级分组。
NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ; //配置USART为中断源
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //选择中断主优先级。
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //配置子优先级。
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能中断。
NVIC_Init(&NVIC_InitStructure); //初始化NVIC结构体。
}
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //定义一个GPIO结构体。
USART_InitTypeDef USART_InitStructure; //定义一个USART结构体。
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE); // 打开串口GPIO的时钟。
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE); // 打开USART的时钟。
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN; //配置发送引脚。
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //模式选择推挽复用模式。
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //配置引脚速度。
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure); //初始化发送引脚。
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN; //配置接收引脚。
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //模式选择浮空输入模式。
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure); //初始化接收引脚。
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE; //配置波特率。
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //配置数据字长。
USART_InitStructure.USART_StopBits = USART_StopBits_1; //配置停止位。
USART_InitStructure.USART_Parity = USART_Parity_No ; //配置校验位。
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //关闭硬件流控制。
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //配置工作模式为接收和发送。
USART_Init(DEBUG_USARTx, &USART_InitStructure); //初始化USART结构体。
NVIC_Configuration(); //串口中断优先级配置。
USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE); //使能串口接收中断。
USART_Cmd(DEBUG_USARTx, ENABLE); //使能串口。
}
以上,就是串口的所有配置了,此时串口已经打开,我们需要怎么来使用呢?可以直接使用固件库里面的函数。
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch) //发送一个字节的数据
{
USART_SendData(pUSARTx, ch); //发送一个字节数据到USART的数据寄存器,进行发送。
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET); //等待发送数据寄存器为空,数据发送出去后才退出发送函数。
}
void Usart_SendArray( USART_TypeDef * pUSARTx, uint8_t *array, uint16_t num) //发送一个八位数据的数组。
{
uint8_t i; //定义一个数来记录数组个数。
for(i=0; i<num; i++) //i记录发送数据组数,当数组全部发送完成,退出循环。
{
Usart_SendByte(pUSARTx, array[i]); //每次发送一个字节数据到USART数据寄存器。
}
while(USART_GetFlagStatus(pUSARTx, USART_FLAG_TC)==RESET); //等待发送完成后退出函数。
}
void Usart_SendString( USART_TypeDef * pUSARTx, char *str) //发送字符串。
{
unsigned int k=0; //定义一个数,记录字符串长度。
do
{
Usart_SendByte(pUSARTx, *(str + k)); //字符是有ASCII表表达,所以每一个字符都是一个八位的数据。
k++; //记录数据已发送长度。
} while(*(str + k)!='\0'); //判断下一个字符是否为'\0'。
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET); //等待发送完成。
}
void Usart_SendHalfWord( USART_TypeDef * pUSARTx, uint16_t ch) //发送16位数据。
{
uint8_t temp_h, temp_l; //将16位数据拆分为两个8位数据。
temp_h = (ch&0xFF00)>>8; //取出高八位。
temp_l = ch&0xFF; //取出低八位。
USART_SendData(pUSARTx,temp_h); //发送高八位。
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET); //等待高8位发送完成。
USART_SendData(pUSARTx,temp_l); //发送低八位。
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET); //等待低8位发送完成。
}
以上就是串口的发送函数了,读取数据就要去中断里面读取,中断函数全部在 stm32f10x_it.c 文件里。
#define DEBUG_USART_IRQHandler USART1_IRQHandler //宏定义串口中断函数名字。
void DEBUG_USART_IRQHandler(void)
{
uint8_t ucTemp; //定义一个8位数据来保存数据。
if(USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE)!=RESET) //确认数据寄存器不为空,读取数据。
{
ucTemp = USART_ReceiveData(DEBUG_USARTx); //读取数据,可以使用全局变量来保存。
}
}
以上,就是关于串口的发送和读取了,不过,对于写习惯了 C 语言的人来说,更加习惯用 C 语言里面的输入输出吧,那串口可以使用这个来进行发送和接收数据吗?当然可以!只需要将函数重定向就可以了。
int fputc(int ch, FILE *f) //重定向c库函数printf到串口,重定向后可使用printf函数。
{
USART_SendData(DEBUG_USARTx, (uint8_t) ch); //发送一个字节数据到串口。
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET); //等待发送完毕。
return (ch);
}
int fgetc(FILE *f) //重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数。
{
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) == RESET); //等待串口输入数据。
return (int)USART_ReceiveData(DEBUG_USARTx);
}
是不是很简单,相信大家都已经学会了 STM32 的串口使用了吧。
谢谢大家的阅读!!!