目录
一. 引言
STM32结合串口通信可以实现数据的传输和控制,是嵌入式系统中的重要技术之一。本实验旨在通过STM32的串口通信接口进行数据的传输和控制,实现串口通信的基本功能和使用方法。通过本实验,可以掌握STM32串口通信的基本原理和实现方法,理解串口通信协议和同步/异步通信方式的特点和应用场景,加深对STM32串口通信的理解和应用能力,为后续嵌入式系统开发中的串口通信应用打下基础。
二. 串口通信基础知识
2.1、串口基本原理简介
串口通信是一种通过串行接口进行数据传输的通信方式,它基于串行通信原理,将数据以位的形式逐个发送和接收。在串口通信中,数据被组织成数据帧的形式进行传输,包括起始位、数据位、校验位和停止位等部分,波特率决定了数据传输的速度,发送方和接收方必须以相同的波特率进行通信,为了使通信顺利进行,需要定义一套串口协议,规定数据的传输格式、起始和结束方式以及校验方法等。
2.2、串口通信协议(如RS-232, RS-485)
原生串口协议:原生串口通信通常使用TTL电平(Transistor-Transistor Logic,晶体管—晶体管逻辑电平),其中逻辑1通常表示高电平(3.3V或5V),逻辑0通常表示低电平(0V),使用异步串行通信方式,定义了数据的格式、起始位、停止位、数据位数以及波特率等。
RS-232:RS-232是一种较为常见的串口通信协议,用于在数据通信设备之间进行数据传输,RS-232通信使用负逻辑电平表示逻辑1,正逻辑电平表示逻辑0。典型的电压范围是+12V至-12V,但实际上可以容忍更大的波动范围,RS-232定义了一个点对点的串行通信接口,并且通常使用异步通信方式,也规定了数据的格式、起始位、停止位、数据位数以及校验方式等。
RS-485:RS-485适用于多点和远距离通信,使用差分信号传输,即使用两根信号线进行数据传输,一条线传输正信号,另一条线传输负信号。典型的电压范围是-7V至+12V,支持多主机和多从机的半双工或全双工通信方式。在定义了数据的格式和校验方式的基础上,还规定了总线上设备的驱动方式、数据冲突检测和解决机制等。
三. 串口功能框图
这里将串口功能框图分成四个部分进行解释。
首先是引脚部分,我们最常用的就是RX和TX 引脚,分别是接收和发送引脚;SCLK是时钟引脚,通常在同步通信时使用;nCTS和nRTS引脚会在硬件流控模式中需要,nCTS是允许发送,nRTS是请求发送,均为低电平有效;IRDA_OUT和IRDA_IN分别是IrDA模式下的数据输出和输入。
其次是数据寄存器部分,发送和接收的数据都由这里进行解析。
发送过程:当发送数据寄存器从CPU或者DMA读取数据之后,将其转移到发送移位寄存器中,数据转移完之后,TXE标志位被置位,代表数据已经转移完毕,可以接收下一组数据了,当发送移位寄存器将数据一位一位的通过串口的TX引脚传输到外部设备后,TC标志位被置位,表示数据发送完成;
接收过程:当外部设备向串口发数据时,会先经过RX引脚进入,传到接收移位寄存器,然后传给RDR寄存器,当RDR寄存器将数据完全转到DR寄存器时RXNE被置位,表示收到数据,可以读出数据。
这整个过程中串口使能UE、发送使能TE以及接收使能RE都应该被置位。
紧接着我们来看控制部分,主要通过控制寄存器CR1、CR2、CR3来进行控制,通过CR1寄存器来配置串口的字长、校验位、唤醒方式、发送使能和接收使能等;通过CR2寄存器来控制串口的数据停止位、LIN模式、时钟极性和时钟相位等;CR3寄存器来控制硬件流控制模式、智能卡模式以及DMA使能发送和DMA使能接收,这部分内容需要通读数据手册,了解这些寄存器的每个位配置的功能就能够实现想要的控制效果。
最后时波特率部分,串口的波特率是由波特比率寄存器(USART_BRR)来进行计算和设置的,通过写入波特率参数,这个寄存器会自动计算整数部分和小数部分写入寄存器,并同时同步给接收器和发送器,波特率的计算公式如图
是时钟频率,波特率是我们设定的参数(如9600、115200等),USART_BRR寄存器会算出USARTDIV,然后分整数部分和小数部分写入寄存器。
四.源码分析
首先依旧按照惯例,在本地文件夹内创建USART.c和USART.h文件,然后到keil5里面将文件添加到对应的组里,切记要将头文件路径设置,不然找不到头文件报错,以下程序部分代码来自野火的例程。
USART.c
#include "USART.h"
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 打开串口GPIO的时钟
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 打开串口外设的时钟
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
// 将USART Tx的GPIO配置为推挽复用模式
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);
// 将USART Rx的GPIO配置为浮空输入模式
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_Cmd(DEBUG_USARTx, ENABLE);
}
/***************** 发送一个字符 **********************/
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch)
{
/* 发送一个字节数据到USART */
USART_SendData(pUSARTx,ch);
/* 等待发送数据寄存器为空 */
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
}
/***************** 发送字符串 **********************/
void Usart_SendString( USART_TypeDef * pUSARTx, char *str)
{
unsigned int k=0;
do
{
Usart_SendByte( pUSARTx, *(str + k) );
k++;
} while(*(str + k)!='\0');
/* 等待发送完成 */
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET)
{
}
}
/***************** 发送一个16位数 **********************/
void Usart_SendHalfWord( USART_TypeDef * pUSARTx, uint16_t ch)
{
uint8_t temp_h, temp_l;
/* 取出高八位 */
temp_h = (ch&0XFF00)>>8;
/* 取出低八位 */
temp_l = ch&0XFF;
/* 发送高八位 */
USART_SendData(pUSARTx,temp_h);
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
/* 发送低八位 */
USART_SendData(pUSARTx,temp_l);
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
}
///重定向c库函数printf到串口,重定向后可使用printf函数
int fputc(int ch, FILE *f)
{
/* 发送一个字节数据到串口 */
USART_SendData(DEBUG_USARTx, (uint8_t) ch);
/* 等待发送完毕 */
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);
return (ch);
}
///重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
int fgetc(FILE *f)
{
/* 等待串口输入数据 */
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) == RESET);
return (int)USART_ReceiveData(DEBUG_USARTx);
}
首先分析函数USART_Config()
,它的功能是配置并初始化一个USART,先定义了两个结构体,GPIO_InitTypeDef GPIO_InitStructure;
和 USART_InitTypeDef USART_InitStructure;
分别用于GPIO的初始化和USART的初始化。紧接着打开串口外设和GPIO的时钟,分别向两个结构体中写参数,GPIO结构体中将USART Tx的GPIO配置为推挽复用模式,速度为50MHz,USART Rx的GPIO配置为浮空输入模式,速度为50MHz,然后调用GPIO初始化函数。串口结构体中将串口配置波特率、数据字长、停止位、校验位、硬件流控制和工作模式等内容,然后调用函数进行串口初始化,最后使能串口,为数据传输做准备。
接下来具体来分析串口是如何进行初始化的,选中串口初始化函数,按F12或右键点击跳转到定义处,USART_Init()函数首先进行参数合法性检查,传入参数都在有效范围内再进行下一步操作,否则会终止运行。
参数合法性检查完毕之后进行串口配置,首先配置的是USART的停止位参数,从CR2寄存器中读取原有设置,清除特定的位(STOP[13:12]),然后根据初始化结构体的值重新设置这些位,最后将最终的设置写回到CR2寄存器中。
/*---------------------------- USART CR2 Configuration -----------------------*/
tmpreg = USARTx->CR2;
/* Clear STOP[13:12] bits */
tmpreg &= CR2_STOP_CLEAR_Mask;
/* Configure the USART Stop Bits, Clock, CPOL, CPHA and LastBit ------------*/
/* Set STOP[13:12] bits according to USART_StopBits value */
tmpreg |= (uint32_t)USART_InitStruct->USART_StopBits;
/* Write to USART CR2 */
USARTx->CR2 = (uint16_t)tmpreg;
然后配置USART的字长度、校验位和模式,同上述操作一样,首先从CR1寄存器中读取原有设置,清除特定的位(M, PCE, PS, TE和RE),然后根据初始化结构体的值重新设置这些位,最后将最终的设置写回到CR1寄存器中。
M | 定义了数据字的长度 0:一个起始位,8个数据位,n个停止位; |
PCE | 检验控制使能 0:禁止校验控制; |
PS | 校验选择 0:偶校验; |
TE | 发送使能 0:禁止发送; |
RE | 接收使能 0:禁止接收; |
之后来到控制寄存器CR3的配置,这主要用于配置USART的硬件流控制,STM32的硬件流控制(Hardware Flow Control)主要适用于USART1、USART2和USART3。那什么是硬件流控制呢?硬件流控制是用来解决数据拥塞和丢失问题的一种方法。当接收方不能接收更多的数据时,发送方就会停止发送数据,这就是所谓的流控制(Flow Control)。而硬件流控制是一种通过硬件设备进行流控制的方法。在硬件流控制中,通常使用专门的硬件设备来监测数据的传输状态。当数据的传输速度过快,以至于接收方无法处理时,硬件设备就会通知发送方暂停发送数据。反之,当接收方能够接收更多的数据时,硬件设备就会通知发送方继续发送数据。
/*---------------------------- USART CR3 Configuration -----------------------*/
tmpreg = USARTx->CR3;
/* Clear CTSE and RTSE bits */
tmpreg &= CR3_CLEAR_Mask;
/* Configure the USART HFC -------------------------------------------------*/
/* Set CTSE and RTSE bits according to USART_HardwareFlowControl value */
tmpreg |= USART_InitStruct->USART_HardwareFlowControl;
/* Write to USART CR3 */
USARTx->CR3 = (uint16_t)tmpreg;
最后进行波特率配置,通过配置USART_BRR寄存器来实现。
/*---------------------------- USART BRR Configuration -----------------------*/
/* Configure the USART Baud Rate -------------------------------------------*/
RCC_GetClocksFreq(&RCC_ClocksStatus);
if (usartxbase == USART1_BASE)
{
apbclock = RCC_ClocksStatus.PCLK2_Frequency;
}
else
{
apbclock = RCC_ClocksStatus.PCLK1_Frequency;
}
/* Determine the integer part */
if ((USARTx->CR1 & CR1_OVER8_Set) != 0)
{
/* Integer part computing in case Oversampling mode is 8 Samples */
integerdivider = ((25 * apbclock) / (2 * (USART_InitStruct->USART_BaudRate)));
}
else /* if ((USARTx->CR1 & CR1_OVER8_Set) == 0) */
{
/* Integer part computing in case Oversampling mode is 16 Samples */
integerdivider = ((25 * apbclock) / (4 * (USART_InitStruct->USART_BaudRate)));
}
tmpreg = (integerdivider / 100) << 4;
/* Determine the fractional part */
fractionaldivider = integerdivider - (100 * (tmpreg >> 4));
/* Implement the fractional part in the register */
if ((USARTx->CR1 & CR1_OVER8_Set) != 0)
{
tmpreg |= ((((fractionaldivider * 8) + 50) / 100)) & ((uint8_t)0x07);
}
else /* if ((USARTx->CR1 & CR1_OVER8_Set) == 0) */
{
tmpreg |= ((((fractionaldivider * 16) + 50) / 100)) & ((uint8_t)0x0F);
}
/* Write to USART BRR */
USARTx->BRR = (uint16_t)tmpreg;
可以看到该代码首先获取时钟频率,然后判断使用的USART类型,如果是串口1,就使用PCLK2的时钟频率,最大为72MHz,否则使用PCLK1的时钟频率,最大为36MHz。根据USART的过采样模式计算整数部分。过采样模式有两种,8样本和16样本。在8样本模式下,整数部分是APB时钟频率除以2乘以波特率;在16样本模式下,整数部分是APB时钟频率除以4乘以波特率。整数部分除以100,然后右移4位,得到的是整数部分的100倍。然后从整数部分100倍中减去这个值,就得到了小数部分。根据整数和小数部分,设置USART的BRR(Baud Rate Registers)寄存器,这个寄存器的值决定了USART的波特率。在8样本模式下,(小数部分*8+50)/100,最后取低5位;在16样本模式下,(小数部分*16+50)/100,最后取低12位,到此就完成串口的初始化配置,可以看到整个配置过程中对控制寄存器CR1、CR2、CR3以及波特率寄存器USART_BRR进行了配置,结合上面的串口功能框图更容易理解。
关于数据的传输函数,只需要关注数据寄存器中几个标志位的值的变化,对其进行读取判断数据是否已经发送完成或者接收完成。这里面还用到将C库函数重定向到串口进行数据传输,具体来说,通过重定向,可以将C标准库中的输入输出函数(如printf
,scanf
等)的数据流重定向到串口,使得我们可以在串口中观察和发送数据,这在调试程序、观察系统运行状态、以及在无文件系统环境中进行数据传输等方面非常有用。
TXE | 发送数据寄存器为空被置位 |
TC | 发送完成后置位 |
RXNE | 接收数据寄存器不为空时置位 |
USART.h
#ifndef __USART_H
#define __USART_H
#include "stm32f10x.h"
#include <stdio.h>
#define DEBUG_USARTx USART2
#define DEBUG_USART_CLK RCC_APB1Periph_USART2
#define DEBUG_USART_APBxClkCmd RCC_APB1PeriphClockCmd
#define DEBUG_USART_BAUDRATE 115200
#define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOA)
#define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd
#define DEBUG_USART_TX_GPIO_PORT GPIOA
#define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_2
#define DEBUG_USART_RX_GPIO_PORT GPIOA
#define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_3
#define DEBUG_USART_IRQ USART2_IRQn
#define DEBUG_USART_IRQHandler USART2_IRQHandler
void USART_Config(void);
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch);
void Usart_SendString( USART_TypeDef * pUSARTx, char *str);
void Usart_SendHalfWord( USART_TypeDef * pUSARTx, uint16_t ch);
#endif
五. 串口通信实验
5.1 串口通信测试
main.c
#include "stm32f10x.h"
#include "led.h"
#include "USART.h"
int main(void)
{
USART_Config();
Usart_SendByte(DEBUG_USARTx,'A');
printf("成功接收到字符\n\n\n\");
Usart_SendString(DEBUG_USARTx,"这是串口1接收实验\n");
printf("成功接收到字符串\n\n\n\");
}
5.2 串口控制LED
main.c
#include "stm32f10x.h"
#include "led.h"
#include "USART.h"
int main(void)
{
char ch;
LED_Init();
USART_Config();
while(1)
{
ch=getchar();
printf("接收到字符:%c\n",ch);
switch(ch)
{
case '1':
LED_ON;
break;
case '0':
LED_OFF;
break;
}
}
}
led.c
#include "led.h"
// LED初始化函数
void LED_Init()
{
// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
// 设置GPIOB的第0位
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
// 设置为推挽输出模式
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
// 输出速度为50MHz
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
// 初始化GPIOB
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 设置GPIOB的第0位为高电平,LED为熄灭状态
GPIO_SetBits(GPIOB, GPIO_Pin_0);
}
// 延迟函数
void Delay(uint32_t count)
{
volatile uint32_t i;
for(i=0; i<count; i++);
}
led.h
#ifndef __LED_H
#define __LED_H
#include "stm32f10x.h"
void LED_Init(void);
void Delay(uint32_t count);
#define LED_ON GPIO_ResetBits(GPIOB, GPIO_Pin_0);
#define LED_OFF GPIO_SetBits(GPIOB, GPIO_Pin_0);
#endif /* __LED_H */
六. 总结
本文讲述了串口通信的基本原理和操作方法,对串口功能框图进行了阐述,并成功进行了串口通信测试和LED灯控制实验,为后续的嵌入式系统开发提供了有益的参考。还可以进一步探索STM32D的串口通信应用,如实现多个设备之间的通信和控制,或者通过串口进行数据采集和传输等。