常规的串口使用是这样的:先配置基本的GPIO和串口,然后调用发送和接收函数,如果需要中断,可以根据情况配置发送中断和接收中断。
比如:
//PB10:UT3_TX
//PB11:UT3_RX
void lcd_usart_init(uint32_t bound)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); //使能USART3时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能GPIOB时钟
//USART3_TX GPIOB.10
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //PB.10
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB.10
//USART3_RX GPIOB.11
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;//PB11
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB.11
//Usart3 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化NVIC寄存器
//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//串口波特率
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(USART3, &USART_InitStructure); //初始化串口3
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);//开启串口接收中断
// USART_ITConfig(USART3, USART_IT_TC, ENABLE);//开启串口接收中断
USART_Cmd(USART3, ENABLE); //使能串口3
}
void USART3_IRQHandler(void)
{
if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
{
printf("get a data\r\n");
}
if(USART_GetITStatus(USART3, USART_IT_TXE) != RESET)
{
printf("send a data\r\n");
}
}
关于串口中断,有几个问题不太明白。
第一,发送函数USART_SendData(USART_TypeDef* USARTx, uint16_t Data)和接收函数USART_ReceiveData(USART_TypeDef* USARTx),都是一次发送1个字节和接收1个字节吗?
第二,中断,是每发送完和接收完1个字节后产生吗?
第三,发送中断标志位有两个USART_IT_TXE和USART_IT_TC,二者有何异同?
先解决这三个问题,然后再继续串口+DMA。
关于串口发送和接收的中断,手册里有三种类型:
─ 发送数据寄存器空
─ 发送完成
─ 接收数据寄存器满
先看下数据寄存器
可以看到,数据寄存器只有8:0位是有用的,其他都保留。
我一开始想的是只有7:0,刚好一个字节,但是这里多了一位,为什么?
数据寄存器 (USART_DR) 只有低 9 位有效,并且第 9 位数据是否有效要取决于 USART控制寄存器1(USART_CR1) 的 M 位设置,当 M 位为 0 时表示 8 位数据字长,当 M 位为 1 表示 9位数据字长(最后一位为奇偶校验位),我们一般使用 8 位数据字长,即无奇偶校验。
根据以上内容,结合USART_SendData(USART_TypeDef* USARTx, uint16_t Data)和接收函数USART_ReceiveData(USART_TypeDef* USARTx)的源码,可以知道,这两个函数都是一次发送或者接收1个字节。
另外,每发送完1个字节,就会产生“发送数据寄存器空”和“发送完成”中断,每接收完1个字节,就会产生“接收数据寄存器满”中断。
这里有个问题,就是“发送数据寄存器空”和“发送完成”中断二者有何区别?
首先要知道,都是针对1个字节来说的。
其中,USART_IT_TXE
是在TDR寄存器为空时产生的中断标志位;USART_IT_TC
是在DR寄存器发送完最后一个位时产生的中断标志位。手册里的说法是,TXE置位,意味着TDR的数据移位到DR寄存器,并已启动发送,此时TDR寄存器为空,可以发送下一字节数据到TDR寄存器,并且不会覆盖之前DR寄存器的内容。
这里有个重点问题,容易错。
我在进行串口发送数据时,没有判断这个标志位,直接连续发送,结果数据全乱了,这就是因为没有判断TDR寄存器为空,数据发送过快,导致TDR数据寄存器中的数据位混在一起了,产生了溢出。
所以,需要判断再发送下一个字节
USART_SendData(USART3, 0x5A); while(USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET){}; USART_SendData(USART3, 0xA5); while(USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET){}; USART_SendData(USART3, 0x07); while(USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET){}; USART_SendData(USART3, 0x10); while(USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET){}; USART_SendData(USART3, 0x70); while(USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET){}; ……
上面只是说明,常规写法如下
for(uint8_t i = 0; i < allProtocolLength; i++) { USART_SendData(USART3, sendData[i]); while(USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET){}; }
千万不要在不做任何判断的情况下连续发数据。
现象和解决方式参考这篇文章,讲的很详细
STM32库函数USART_SendData的缺陷和解决方法-文章-单片机-STM32 - 畅学电子网
回到USART_IT_TC标志位,表示发送完成,当包含有数据的一帧发送完成后,并且TXE=1时,由硬件将该位置“1” ,也就是说,当TDR寄存器的数据被赋值到移位寄存器的时候,TXE会置位,当移位寄存器中的数据发完之后,并且,TDR寄存器依然为空,则TC会置位。
一般,只要TXE置位,就可以继续发送下一个字节数据了。
另外,还有个问题,是否要手动清除该标志位?答案是不用。
当TDR寄存器中的数据被硬件转移到移位寄存器的时候,TXE位被硬件置1,对USART_DR的写操作,该位自动清零。所以,不必手动清除。
以上就是串口的基本用法,串口收发数据时,很少会用到发送中断和接收中断。
发送一般都是直接发,接收时也是一个字节一个字节去接收,速度不快。
所以,很多情况下,串口都会结合DMA一起使用,提高效率。
关于DMA的基本内容可直接查阅相关手册,此处仅记录重点内容及DMA+串口的常见用法。
DMA在初始化时,会设置一个缓冲区buffer_size,当缓冲区满的时候,就会产生中断,最常见的就是传输完成TC中断(可以不关心中断)。
到底要不要用DMA的中断呢?
一般来说,发送可用可不用(可直接用串口发送),发送时,可以通过设置缓冲区大小,让DMA在传输完设定长度的字节数据后停止。
但是接收时,也要设定这个缓冲区的大小,只有填满这个缓冲区的时候,才会产生中断(这一点待确认),问题是,我不知道接收的字节数到底是多少,缓冲设小了数据收不完整,缓冲设大了,又没那么多数据可接收,这样,就不会触发接收中断(待确认),就算能触发,也会多出一些没用的数据。
……
有以下几种思路:
DMA直接发送(单次)+DMA直接接收(单次);
DMA发送中断+DMA接收中断;
串口直接发送+循环DMA接收;
结合空闲中断;
……
那么,到底要如何合理地使用串口+DMA呢?
了解什么是空闲中断
在串口的状态寄存器里面,有个IDLE标志
位说明如下:
这里的总线空闲,指的是串口线处于空闲状态。
可以参考这个问答:
串口的空闲中断和普通中断相比有什么优势
关于空闲中断,记录如下重点内容:
串口中断标志有很多,接收完成、发送完成、CTS、过载错误、噪声错误和空闲等,每个中断标志代表的功能不一样。
普通的有接收中断和发送中断,即每接收或者发送一个字节,就产生一个中断,在接收比较长的数据时会频繁地进中断,可能数据会来不及处理。而空闲中断是一帧数据接收结束后收到一个字节的空闲帧才中断,空闲中断配合DMA可以很好的实现不定长数据接收,出现空闲标志时,认为一帧报文发送完毕,然后进行报文分析。
空闲中断是接收到一个数据以后,接收停顿超过一字节时间认为桢收完,总线空闲中断是在检测到在接收数据后,数据总线上一个字节的时间内,没有再接到数据后发生。也就是RXNE位被置位之后,才开始检测,只被置位一次,除非再次检测到RXNE位被置位,然后才开始检测下一次的总线空闲。一次RXNE位被置位只进行一次。
具体用法参考下面三篇文章:
STM32学习之串口采用DMA收发数据:需要利用状态机加DMA加串口_暮尘依旧的博客-CSDN博客
STM32F103 串口DMA + 空闲中断 实现不定长数据收发_stm32f103空闲中断_夏夜晚风_的博客-CSDN博客
STM32F103 串口 +DMA中断实现数据收发_stm32f103 dma中断_夏夜晚风_的博客-CSDN博客
几点验证
要发的字节数10个,存在一个50字节大小的数组里。
经验证,缓存要设置比实际传输字节数少1,才会置位发送标志位,比如这里要设置9,但是设置为9,数据就发不完整了。奇怪。不知道哪里出了问题。
首先能确认,发送时,缓存要大于等于实际发送的数据字节数,要不数据发不完整,缓存一满就会停止DMA传输。
如果不是已经发生事件跳到中断里,就不要直接用if来判断标志位有没有置位。
在中断里直接if是因为标志位已经发生了,现在要判断是哪种中断标志。
如果就是要判断标志位有没有发生,要放到while里,而不是用if,或者在while(1)里if判断,因为如果你直接if判断,说不定还没来得及置位,但是因为if不满足条件直接就跳过了,后面就算再置位也不会进行判断了。
比如,这样就不对
这样比较合理
这样调整之后,发送缓存只要大于等于要发送的数据,就能保证数据发送没问题,并且也会置位发送标志位,所以,应该是发送完了或者缓冲区满了都会置位标志位吧,具体以后再慢慢研究吧。
另外,仔细想了想,发送完成之后再开启接收,和发送方的数据不太匹配,可能会漏接收,或者出现其他问题,这一点不太合理。
直接这样吧。
DMA_ClearFlag和DMA_ClearITPendingBit效果是一样的。
前者常在非中断中使用,后者常在中断中使用。
我的实践方案总结
串口+DMA涉及的东西比较多,两个都能普通收发,都能中断。
比如,串口普通收,串口普通发,串口中断收,串口中断发,DMA普通收,DMA普通发,DMA中断收,DMA中断发,再加上个空闲中断。
其中,我们要明白一点,使用DMA只是不用CPU来运输数据,而是使用DMA控制器来运输数据,但是,串口那边的功能并不影响,寄存器还是一样的。所以,DMA可以不用中断,而是通过串口的中断来判断数据有没有收发完,串口才是最知道它自己的数据有没有发完和收完的。
发送的时候,因为DMA可以指定传输的长度,所以和串口中断的作用是差不多的。但是,在接收时,就推荐使用串口来判断,因为接受时,DMA并不知道串口数据有没有接收完。
发送和接收数据都是由DMA完成,但是判断有没有完成,就需要选择了。
我在实践中是这样选择的,发送完成由DMA发送完成中断来判断,接收完成由串口空闲中断来判断,串口接收完成中断是每个字节产生一个中断,需要一个字节一个字节地去判断,但是空闲中断是接收完一帧数据后再去统一处理,效率高很多。
一个字节一个字节接收时,需要在中断里判断有没有接收到帧头,之后的数据才会保存到数组里,而空闲中断是先接收完一帧数据,然后再去处理。
我们初始化时就开启DMA接收,然后设置空闲中断,发生空闲中断时,就表示一帧数据接收完成了。DMA开启后,有数据就搬,没数据就等待。选择DMA的正常模式,则来一次数据,搬一次,就停了,即DMA只传输一次。如果当传输完一次后,还想再传输下一次,就需要重启DMA接收,依然从头开始存起。如果是循环模式,就会一直继续接收,此时地址是不断增长并循环的。
以下给出方案中这部分代码。
串口初始化
//LCD串口 //PB10:UT3_TX //PB11:UT3_RX //单片机串口3接到LCD芯片的串口1(仅串口1支持协议解析) void lcd_usart_init(uint32_t bound) { //GPIO端口设置 GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); //使能USART3时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能GPIOB时钟 //USART3_TX GPIOB.10 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //PB.10 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB.10 //USART3_RX GPIOB.11 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;//PB11 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入 GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB.11 //USART 初始化设置 USART_InitStructure.USART_BaudRate = bound;//串口波特率 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(USART3, &USART_InitStructure); //初始化串口3 USART_DMACmd(USART3,USART_DMAReq_Tx, ENABLE); //使能串口3的DMA发送 USART_DMACmd(USART3, USART_DMAReq_Rx, ENABLE); //使能串口3的DMA接收 USART_Cmd(USART3, ENABLE); //使能串口3 /* 串口中断配置 */ NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级 NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn; // 串口3中断 NVIC_Init(&NVIC_InitStructure); // 嵌套向量中断控制器初始化 //使能串口空闲中断 //接收一帧数据产生 USART_IT_IDLE 空闲中断 USART_ITConfig(USART3, USART_IT_IDLE, ENABLE); }
DMA发送初始化
//初始化串口3的DMA发送功能 void usart3_dma_send_init() { DMA_InitTypeDef DMA_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; //开启DMA1时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //串口3发送DMA初始化,DMA1通道2 DMA_DeInit(DMA1_Channel2); //将DMA1的通道2寄存器重设为缺省值 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART3->DR; //DMA外设基地址 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)sendData; //DMA内存基地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //数据传输方向,从内存读取发送到外设 DMA_InitStructure.DMA_BufferSize = 0; //DMA通道的DMA缓存的大小,初始化为0不发送 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度为8位 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常模式,一次传输后自动结束 DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DMA通道 x拥有中优先级 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输 DMA_Init(DMA1_Channel2, &DMA_InitStructure); DMA_Cmd(DMA1_Channel2, DISABLE);//初始化时禁止DMA发送 //配置DMA发送中断,发送完成后,清除标志位即可 NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能 NVIC_Init(&NVIC_InitStructure); // 嵌套向量中断控制器初始化 //开启DMA1通道2的传输中断,用来判断发送完成 DMA_ITConfig(DMA1_Channel2, DMA_IT_TC, ENABLE); }
DMA接收初始化
//初始化串口3的DMA接收功能 void usart3_dma_receive_init() { DMA_InitTypeDef DMA_InitStructure; //开启DMA1时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //串口3接收DMA初始化,DMA1通道3 DMA_DeInit(DMA1_Channel3); //将DMA1的通道3寄存器重设为缺省值 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART3->DR; //DMA外设基地址 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)receiveData; //DMA内存基地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,从外设读取发送到内存 DMA_InitStructure.DMA_BufferSize = sizeof(receiveData); //DMA通道的DMA缓存的大小 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度为8位 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常模式,一次传输后自动结束 DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DMA通道 x拥有中优先级 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输 DMA_Init(DMA1_Channel3, &DMA_InitStructure); //发送是主动的,但是接收是被动的,上电就要打开,等着接收HMI的数据 DMA_Cmd(DMA1_Channel3, ENABLE);//初始化时即开启接收 } //开启一次DMA发送 void USART3_DMA_SEND_Enable(uint16_t buffer_size) { DMA_Cmd(DMA1_Channel2, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel2, buffer_size);//DMA通道的DMA缓存的大小 DMA_Cmd(DMA1_Channel2, ENABLE); }
开启一次DMA发送
//开启一次DMA发送 void USART3_DMA_SEND_Enable(uint16_t buffer_size) { DMA_Cmd(DMA1_Channel2, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel2, buffer_size);//DMA通道的DMA缓存的大小 DMA_Cmd(DMA1_Channel2, ENABLE); }
开启一次DAM接收
//开启一次DMA接收 void USART3_DMA_RECEIVE_Enable() { //先将接收的内存部分数据清0 memset(receiveData, 0, sizeof(receiveData)); DMA_Cmd(DMA1_Channel3, ENABLE); }
DMA发送完成的中断函数
//DMA1发送完成的中断函数 void DMA1_Channel2_IRQHandler() { if(DMA_GetITStatus(DMA1_IT_TC2) != RESET) { sendCFlag = 1; DMA_ClearITPendingBit(DMA1_IT_TC2); // 清除传输完成中断标志位 DMA_Cmd(DMA1_Channel2, DISABLE); // 关闭DMA发送 } }
串口的空闲中断
//串口3的空闲中断处理函数 //判断DMA数据是不是接收完 void USART3_IRQHandler(void) { uint8_t clear; if(USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) // 空闲中断 { clear = USART3->SR; // 清除空闲中断 clear = USART3->DR; // 清除空闲中断 receiveCFlag = 1; // 置接收标志位 //空闲中断产生,但是DMA后续可能还有数据,不关心 //所以主动关闭DMA接收, DMA_Cmd(DMA1_Channel3, DISABLE); } }
数据准备好后,就开启发送
…… USART3_DMA_SEND_Enable(allProtocolLength);//开启DMA传输进行数据发送
空闲中断里接收完成标志位置位后就开始处理数据
//主函数 int main(void) { system_init(); while(1)//整体的逻辑就是,串口只要有数据来,就会DMA搬运并置位接收完成标志位 { //主循环判断标志位然后处理数据,处理完成后再次开启DMA接收 if(receiveCFlag && IS_DATA_OK()) { printf("ok\r\n"); receiveCFlag = 0;//取消接收完成标志位 //开启DMA接收,为下一次接收做准备 USART3_DMA_RECEIVE_Enable(); } } }
DMA和串口遗漏内容补充
摘自数据手册。
串口的发送和接收寄存器地址是一样的&USARTx->DR