STM32串口通信详解
一.stm32串口的简介
串口是一种应用十分广泛的通讯接口,可以实现两个设备的相互通讯。
在我们学习的过程中我们会发现串口有usart还有uart这些说法,一开始我以为是少打了一个字母,后来才发现不是这样的。处理器和外部设备通讯有两种方式:串行通讯和并行通讯。而串行数据通讯有两种基本的方式,一种是同步通讯还有一种是异步通讯。uart使用的是异步的通讯方式,usart则是在uart的基础上增加了时钟。
注意:简单双向串口通信有两根通信线(发送端TX和接收端RX)TX与RX要交叉连接
二.USART简介
USART 是 Universal Synchronous/Asynchronous Receiver/Transmitter 的缩写,即通用同步/异步收发传输器。
串口通讯的基本参数:
·波特率:串口通讯的速率
·起始位:标志一个数据帧的开始,且固定是低电平
·数据位:数据帧的有效载荷,1为高电平,0为低电平
·校验位: 用于数据验证,根据数据位计算而来(奇偶校验)
·停止位:用于数据帧间隔,固定为高电平
注意:这里我们 可以看到一个为8位数据一个为9位数据,他们的校验位就是一个用无校验,一个用奇偶校验。
奇偶检验知识点:UART串口校验方式(无校验、奇偶校验、固定校验)_uart校验位-CSDN博客
三.stm32的外设USART
USART是STM32内部集成的硬件外设,可以根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可以自动接收RX引脚的数据帧时序,拼接成一个字节数据,存放在数据寄存器里。
stm32内USART的接收发送框图:
stm32的波特率发生器:
USART的波特率由波特率寄存器的BRR里的DIV确定
波特率的计算公式: 波特率 = fPCLK2/1 / (16 * DIV)
四.USART的应用
在这里我们主要介绍USART的三种应用,串口基本发送,DMA发送,中断收发。
stm32中有多个串口收发引脚,他们对应的功能也不一样,在数据手册里面有介绍。
void USART1_Init (uint32_t baudrate); // 初始化串口的GPIO、通信参数配置、中断优先级; (波特率可设、8位数据、无校验、1个停止位)
uint8_t USART1_GetBuffer (uint8_t* buffer, uint8_t* cnt); // 获取接收到的数据
void USART1_SendData (uint8_t* buf, uint8_t cnt); // 通过中断发送数据,适合各种数据
void USART1_SendString (char* stringTemp); // 通过中断发送字符串,适合字符串,长度在256个长度内的
void USART1_SendStringForDMA (char* stringTemp) ; // 通过DMA发送数据,适合一次过发送数据量特别大的字符串,省了占用中断的时间
/******************************************************************************
* 函 数: vUSART1_Init
* 功 能: 初始化USART1的GPIO、通信参数配置、中断优先级
* (8位数据、无校验、1个停止位)
* 参 数: uint32_t baudrate 通信波特率
* 返回值: 无
******************************************************************************/
void USART1_Init(uint32_t baudrate)
{
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 时钟使能
RCC->APB2ENR |= RCC_APB2ENR_USART1EN; // 使能USART1时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // 使能GPIOA时钟
// GPIO_TX引脚配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // TX引脚,配置为复用推挽工作模式
GPIO_Init (GPIOA, &GPIO_InitStructure);
// GPIO_RX引脚配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING ; // RX引脚,由于板子上为一主多从电路,故选择复用开漏模式
GPIO_Init (GPIOA, &GPIO_InitStructure);
// 中断配置
NVIC_InitStructure .NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure .NVIC_IRQChannelPreemptionPriority=2 ; // 抢占优先级
NVIC_InitStructure .NVIC_IRQChannelSubPriority = 2; // 子优先级
NVIC_InitStructure .NVIC_IRQChannelCmd = ENABLE; // IRQ通道使能
NVIC_Init(&NVIC_InitStructure);
//USART 初始化设置
USART_DeInit(USART1);
USART_InitStructure.USART_BaudRate = baudrate; // 串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 字长为8位数据格式
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(USART1, &USART_InitStructure); // 初始化串口
USART_ITConfig(USART1, USART_IT_TXE , DISABLE );
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接受中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // 使能空闲中断
USART_Cmd(USART1, ENABLE); // 使能串口, 开始工作
USART1->SR = ~(0x00F0); // 清理中断
xUSART.USART1InitFlag =1; // 标记初始化标志
xUSART.USART1ReceivedNum =0; // 接收字节数清零
printf("\r\r\r=========== 魔女开发板 STM32F103 外设初始报告 ===========\r");
printf("USART1初始化配置 接收中断、空闲中断, 发送中断\r");
}
/******************************************************************************
* 函 数: USART1_IRQHandler
* 功 能: USART1的接收中断、空闲中断、发送中断
* 参 数: 无
* 返回值: 无
*
******************************************************************************/
static uint8_t U1TxBuffer[256] ; // 用于中断发送:环形缓冲区,256个字节
static uint8_t U1TxCounter = 0 ; // 用于中断发送:标记已发送的字节数(环形)
static uint8_t U1TxCount = 0 ; // 用于中断发送:标记将要发送的字节数(环形)
void USART1_IRQHandler(void)
{
static uint16_t cnt=0; // 接收字节数累计:每一帧数据已接收到的字节数
static uint8_t RxTemp[U1_RX_BUF_SIZE]; // 接收数据缓存数组:每新接收1个字节,先顺序存放到这里,当一帧接收完(发生空闲中断), 再转存到全局变量:xUSART.USARTxReceivedBuffer[xx]中;
// 接收中断
if(USART1->SR & (1<<5)) // 检查RXNE(读数据寄存器非空标志位); RXNE中断清理方法:读DR时自动清理;
{
if((cnt>=U1_RX_BUF_SIZE))//||(xUSART.USART1ReceivedFlag==1)) // 判断1: 当前帧已接收到的数据量,已满(缓存区), 为避免溢出,本包后面接收到的数据直接舍弃.
{ // 判断2: 如果之前接收好的数据包还没处理,就放弃新数据,即,新数据帧不能覆盖旧数据帧,直至旧数据帧被处理.缺点:数据传输过快于处理速度时会掉包;好处:机制清晰,易于调试
USART1->DR; // 读取数据寄存器的数据,但不保存.主要作用:读DR时自动清理接收中断标志;
return;
}
RxTemp[cnt++] = USART1->DR ; // 把新收到的字节数据,顺序存放到RXTemp数组中;注意:读取DR时自动清零中断位;
}
// 空闲中断, 用于配合接收中断,以判断一帧数据的接收完成
if(USART1->SR & (1<<4)) // 检查IDLE(空闲中断标志位); IDLE中断标志清理方法:序列清零,USART1 ->SR; USART1 ->DR;
{
xUSART.USART1ReceivedNum = 0; // 把接收到的数据字节数清0
memcpy(xUSART.USART1ReceivedBuffer, RxTemp, U1_RX_BUF_SIZE); // 把本帧接收到的数据,存放到全局变量xUSART.USARTxReceivedBuffer中, 等待处理; 注意:复制的是整个数组,包括0值,以方便字符串数据
xUSART.USART1ReceivedNum = cnt; // 把接收到的字节数,存放到全局变量xUSART.USARTxReceivedNum中;
cnt=0; // 接收字节数累计器,清零; 准备下一次的接收
memset(RxTemp ,0, U1_RX_BUF_SIZE); // 接收数据缓存数组,清零; 准备下一次的接收
USART1 ->SR; USART1 ->DR; // 清零IDLE中断标志位!! 序列清零,顺序不能错!!
}
// 发送中断
if ((USART1->SR & 1<<7) && (USART1->CR1 & 1<<7)) // 检查TXE(发送数据寄存器空)、TXEIE(发送缓冲区空中断使能)
{
USART1->DR = U1TxBuffer[U1TxCounter++]; // 读取数据寄存器值;注意:读取DR时自动清零中断位;
if(U1TxCounter == U1TxCount )
USART1->CR1 &= ~(1<<7); // 已发送完成,关闭发送缓冲区空置中断 TXEIE
}
}
/******************************************************************************
* 函 数: vUSART1_GetBuffer
* 功 能: 获取UART所接收到的数据
* 参 数: uint8_t* buffer 数据存放缓存地址
* uint8_t* cnt 接收到的字节数
* 返回值: 0_没有接收到新数据, 1_接收到新数据
******************************************************************************/
uint8_t USART1_GetBuffer(uint8_t* buffer, uint8_t* cnt)
{
if(xUSART.USART1ReceivedNum) // 判断是否有新数据
{
memcpy(buffer, xUSART.USART1ReceivedBuffer, xUSART.USART1ReceivedNum ); // 把新数据复制到指定位置
memset(xUSART.USART1ReceivedBuffer ,0, U1_RX_BUF_SIZE); // 接收数据缓存数组,清零; 准备下一次的接收
*cnt = xUSART.USART1ReceivedNum; // 把新数据的字节数,存放指定变量
xUSART.USART1ReceivedNum = 0; // 接收标记置0
return 1; // 返回1, 表示接收到新数据
}
return 0; // 返回0, 表示没有接收到新数据
}
/******************************************************************************
* 函 数: vUSART1_SendData
* 功 能: UART通过中断发送数据,适合各种数据类型
* 【适合场景】本函数可发送各种数据,而不限于字符串,如int,char
* 【不 适 合】注意环形缓冲区容量256字节,如果发送频率太高,注意波特率
* 参 数: uint8_t* buffer 需发送数据的首地址
* uint8_t cnt 发送的字节数 ,限于中断发送的缓存区大小,不能大于256个字节
* 返回值:
******************************************************************************/
void USART1_SendData(uint8_t* buf, uint8_t cnt)
{
for(uint8_t i=0; i<cnt; i++)
U1TxBuffer[U1TxCount++] = buf[i];
if((USART1->CR1 & 1<<7) == 0 ) // 检查发送缓冲区空置中断(TXEIE)是否已打开
USART1->CR1 |= 1<<7;
}
/******************************************************************************
* 函 数: vUSART1_SendString
* 功 能: UART通过中断发送输出字符串,无需输入数据长度
* 【适合场景】字符串,长度<=256字节
* 【不 适 合】int,float等数据类型
* 参 数: char* stringTemp 需发送数据的缓存首地址
* 返回值: 元
******************************************************************************/
void USART1_SendString(char* stringTemp)
{
u16 num=0; // 字符串长度
char* t=stringTemp ; // 用于配合计算发送的数量
while(*t++ !=0) num++; // 计算要发送的数目,这步比较耗时,测试发现每多6个字节,增加1us,单位:8位
USART1_SendData((u8*)stringTemp, num+1); // 调用函数完成发送,num+1:字符串以0结尾,需多发一个:0
}
/******************************************************************************
* 函 数: vUSART1_SendStringForDMA
* 功 能: UART通过DMA发送数据,省了占用中断的时间
* 【适合场景】字符串,字节数非常多,
* 【不 适 合】1:只适合发送字符串,不适合发送可能含0的数值类数据; 2-时间间隔要足够
* 参 数: char strintTemp 要发送的字符串首地址
* 返回值: 无
******************************************************************************/
void USART1_SendStringForDMA(char* stringTemp)
{
static u8 Flag_DmaTxInit=0; // 用于标记是否已配置DMA发送
u32 num = 0; // 发送的数量,注意发送的单位不是必须8位的
char* t =stringTemp ; // 用于配合计算发送的数量
while(*t++ !=0) num++; // 计算要发送的数目,这步比较耗时,测试发现每多6个字节,增加1us,单位:8位
while(DMA1_Channel4->CNDTR > 0); // 重要:如果DMA还在进行上次发送,就等待; 得进完成中断清标志,F4不用这么麻烦,发送完后EN自动清零
if( Flag_DmaTxInit == 0) // 是否已进行过USAART_TX的DMA传输配置
{
Flag_DmaTxInit = 1; // 设置标记,下次调用本函数就不再进行配置了
USART1 ->CR3 |= 1<<7; // 使能DMA发送
RCC->AHBENR |= 1<<0; // 开启DMA1时钟 [0]DMA1 [1]DMA2
DMA1_Channel4->CCR = 0; // 失能, 清0整个寄存器, DMA必须失能才能配置
DMA1_Channel4->CNDTR = num; // 传输数据量
DMA1_Channel4->CMAR = (u32)stringTemp; // 存储器地址
DMA1_Channel4->CPAR = (u32)&USART1->DR; // 外设地址
DMA1_Channel4->CCR |= 1<<4; // 数据传输方向 0:从外设读 1:从存储器读
DMA1_Channel4->CCR |= 0<<5; // 循环模式 0:不循环 1:循环
DMA1_Channel4->CCR |= 0<<6; // 外设地址非增量模式
DMA1_Channel4->CCR |= 1<<7; // 存储器增量模式
DMA1_Channel4->CCR |= 0<<8; // 外设数据宽度为8位
DMA1_Channel4->CCR |= 0<<10; // 存储器数据宽度8位
DMA1_Channel4->CCR |= 0<<12; // 中等优先级
DMA1_Channel4->CCR |= 0<<14; // 非存储器到存储器模式
}
DMA1_Channel4->CCR &= ~((u32)(1<<0)); // 失能,DMA必须失能才能配置
DMA1_Channel4->CNDTR = num; // 传输数据量
DMA1_Channel4->CMAR = (u32)stringTemp; // 存储器地址
DMA1_Channel4->CCR |= 1<<0; // 开启DMA传输
}
// printf //
/******************************************************************************
* 功 能: printf函数支持代码
* 【特别注意】加入以下代码, 使用printf函数时, 不再需要选择use MicroLIB
* 参 数:
* 返回值:
* 备 注: 魔女开发板团队 资料存放Q群:1126717453 最后修改_2020年07月15日
******************************************************************************/
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#pragma import(__use_no_semihosting)
struct __FILE { int handle; }; // 标准库需要的支持函数
FILE __stdout; // FILE 在stdio.h文件
void _sys_exit(int x) { x = x; } // 定义_sys_exit()以避免使用半主机模式
int fputc(int ch, FILE *f) // 重定向fputc函数,使printf的输出,由fputc输出到UART, 这里使用串口1(USART1)
{
#if 1 // 方式1-使用常用的poll方式发送数据,比较容易理解,但等待耗时大
while((USARTx_DEBUG->SR & 0X40)==0); // 等待上一次串口数据发送完成
USARTx_DEBUG->DR = (u8) ch; // 写DR,串口1将发送数据
return ch;
#else // 方式2-使用queue+中断方式发送数据; 无需像方式1那样等待耗时,但要借助已写好的函数、环形缓冲
uint8_t c[1]={(uint8_t)ch};
if(USARTx_DEBUG == USART1) vUSART1_SendData (c, 1);
if(USARTx_DEBUG == USART2) vUSART2_SendData (c, 1);
if(USARTx_DEBUG == USART3) vUSART3_SendData (c, 1);
if(USARTx_DEBUG == UART4) vUART4_SendData (c, 1);
if(USARTx_DEBUG == UART5) vUART5_SendData (c, 1);
return ch;
#endif
}
作者:俊昭喜喜里