网上搜索STM32串口通信+DMA,绝大多数都是采用串口IDLE中断+DMA的处理方法,俺百思不得其解,为什么RXNE中断就不可以使用DMA的方式了呢?在实验室最近做的一个项目里,采用RXNE+IDLE中断的处理方式。由于STM32的DR寄存器是8位的,所以串口的收发都是以字节为单位进行处理的,当通过串口给单片机发送数据时(比如发送字符串"520"),RXNE收到"5"(占一个字节),触发一次RXNE中断,CPU将"5"从DR寄存器中读取,并赋值给USART_RX_BUF数组,同样"2"、"0"也是CPU重复上述顺序依次赋值给USART_RX_BUF,当处理完"520"之后,触发IDLE中断。
上述过程都是CPU全程参与工作,可以很明显的看出,CPU工作量最集中的时段是RXNE中断,因为串口总是源源不断的收到数据并要及时处理掉,如果处理不及时,会触发USART的ORE报错,之前做过一个实验,如果在RXNE中断处理函数中加入含有延迟操作的函数(例如delay、printf)后,会产生ORE报错(因为只有DR寄存器的数据被读走以后才能允许下一次的数据进入DR,如果DR的数据没有及时读走,会导致串口接收数据的累积,所以产生ORE报错,后续数据会丢失)。
之所以采用DMA,是因为想要最大限度的减轻CPU的工作负担,所以理所当然的想到用DMA来处理RXNE中断,这里还是以发送"520"为例:
一、 配置DMA(挑几个重要的参数说一说)
1. 外设寄存器数据宽度为Byte(DR寄存器为8位),内存单元的寄存器数据宽度为Byte(与DR保持一致)
2. DMA通道缓存大小为CNDTR,意思是一次DMA传输的总字节数。(决定了实际有效传递进入内存的数据)
3. 外设地址(&USART -> DR)、内存地址(USART_RX_BUF(数组名是一个指针))、数据传输方向等等就不说了,按部就班配置就可以了
4. 外设指针增量不使能(因为DR寄存器只是一个单纯的8位存储单元,不是一个数组)
5. 内存空间指针增量使能(因为定义的USART_RX_BUF是一个数组)
二、 CNDTR = 1时RXNE中断+DMA工作过程(串口发送"1024")
1. 串口发送"1"(占一个字节)到DR寄存器,RXNE中断拉起,使能一次DMA传输,DR的内容("5")被传递进USART_RX_BUF[0]中,DMA_ISR寄存器中相应的TC位置1(本次DMA传输完成),RXNE中断结束。
2. 串口发送"0"、"2"、"4"重复上述过程
如果串口打印USART_RX_BUF内容,会显示"4",而不是"1024"!
三、 CNDTR = 2 时RXNE中断+DMA工作过程(串口发送"520")
和CNDTR = 1时的区别在于每完成两个字节的传输才会将DMA_ISR中相应的TC位置1
1. 串口发送"5"到DR寄存器,RXNE中断拉起,使能一次DMA传输,DMA将DR的内容"5"传递进入USART_RX_BUF[0],等待下一个DR寄存器的数据。
2. 串口发送"2"到DR寄存器,DMA将DR的内容"2"传递进入USART_RX_BUF[1],TC位置1,本次DMA传输完成,RXNE中断结束。
3. 串口发送"5"到DR寄存器,RXNE中断拉起,使能一次DMA传输,DMA将DR的内容"5"传递进入USART_RX_BUF[0],等待下一个DR寄存器的数据。
4. 由于串口不再发送数据,程序卡死在RXNE中断中...直到再收到一个数据为止。
如果串口打印USART_RX_BUF内容,会显示"0",而不是"520"!同样的道理,如果用这种方法串口发送"1024",打印的结果会是"24",而不是"1024"!
问题总结:
1. 为什么在RXNE中使用DMA时如果发生DMA等待(类似发生延迟的操作使程序一直卡在RXNE中断)却一直没有发生ORE报错?这里给出我个人的理(猜)解(测):在RXNE中断中使用DMA中断会“旁路”掉RXNE中断,根据中文参考手册,USART_SR寄存器中的RXNE是由硬件置1的,只要串口的数据传递进入DR寄存器中,该位就会置1(如果DMA传输没有完成的话,是不会再次唤醒RXNE中断),但是DMA会及时读取DR寄存器的内容使得RXNE位再次清零,这样就保证了来一个数据就送走一个,不会发生数据堆积导致的ORE报错。
2. RXNE中断+DMA必须要根据CNDTR严格限制输入字符的长度,否则会因为DMA的等待机制造成奇奇怪怪的结果,所以这个办法没有检错纠错能力,鲁棒性差。
3. CNDTR决定了实际有效传递进入内存的数据,正如上面的实例分析,如果CNDTR = 1,发送"1024"最终的结果只能是“4”,CNDTR = 2时,发送"520"最终的结果是0,发送"1024"最终的结果是"24",超过CNDTR规定的长度的数据会覆盖前面的数据!结果以最后一次有效长度之内的数据为准!
现在我算是死心了,还是去学习一下如何使用串口IDLE中断+DMA的方式发送可变长的数据吧...
附dma.c代码:
#include "dma.h"
u16 CNDTR = CNDTR_Count;
//创建 DMA_InitTpyeDef 结构体
DMA_InitTypeDef DMA_InitStruct;
//配置DMA参数并初始化
void DMA_Config(DMA_Channel_TypeDef* DMA_CHx, u32 dir, u32 cpar, u32 cmar, u16 cndtr) //形参是在特定应用中需要格外声明的状态,该函数的作用是配置官方库函数中的 DMA_InitType 结构体
{
//使能DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
//记录传输数据量
CNDTR = cndtr;
//去初始化DMA
DMA_DeInit(DMA_CHx);
//为 DMA_InitTpyeDef 结构体成员赋值
DMA_InitStruct. DMA_PeripheralBaseAddr = cpar;
DMA_InitStruct. DMA_MemoryBaseAddr = cmar;
DMA_InitStruct. DMA_BufferSize = cndtr;
DMA_InitStruct. DMA_Priority = DMA_Priority_Medium;
DMA_InitStruct. DMA_DIR = dir;
DMA_InitStruct. DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct. DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct. DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct. DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct. DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct. DMA_M2M = DMA_M2M_Disable;
//初始化DMA
DMA_Init(DMA_CHx, &DMA_InitStruct);
}
//开启一次DMA传输
void DMA_EnableCmd(DMA_Channel_TypeDef* DMA_CHx)
{
//每次开启DMA之前必须先重装在CNDTR,开启CNDTR之前必须关闭DMA
DMA_Cmd(DMA_CHx, DISABLE);
//重装在CNDTR
DMA_SetCurrDataCounter(DMA_CHx, CNDTR);
//开启本次DMA传输
DMA_Cmd(DMA_CHx, ENABLE);
}
附usart.c代码
#include "sys.h"
#include "usart.h"
#include "stdlib.h"
#include "dma.h"
//
//如果使用ucos,则包括下面的头文件即可.
#if SYSTEM_SUPPORT_OS
#include "includes.h" //ucos 使用
#endif
//
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
_sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
#endif
/*使用microLib的方法*/
/*
int fputc(int ch, FILE *f)
{
USART_SendData(USART1, (uint8_t) ch);
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET) {}
return ch;
}
int GetKey (void) {
while (!(USART1->SR & USART_FLAG_RXNE));
return ((int)(USART1->DR & 0x1FF));
}
*/
#if EN_USART1_RX //如果使能了接收
//串口1中断服务程序
//注意,读取USARTx->SR能避免莫名其妙的错误
u8 USART_RX_BUF[USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.
//接收状态
//bit15, 接收完成标志
//bit14, 接收到0x0d
//bit13~0, 接收到的有效字节数目
u16 USART_RX_STA=0; //接收状态标记
int Cmd;
extern u16 Duty_Count;
extern u16 arr;
extern u16 Duty_Count_Init;
int MyFlag = 0; //超载标志
int DMA_FLAG = 0; //DMA标志
int ORE_FLAG = 0; //ORE标志
void uart_init(u32 bound){
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA时钟
//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
//USART1_RX GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
//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(USART1, &USART_InitStructure); //初始化串口1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE); //开启串口DMA接收
USART_Cmd(USART1, ENABLE); //使能串口1
}
void USART1_IRQHandler(void) //串口1中断服务程序
{
uint16_t IDLE_ClearBit = 0;
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntEnter();
#endif
//**************************************RXNE中断+DMA传输************************************************//
// 这种方法存在局限性:
// 1. 必须指定数据长度,并且必须声明是否添加自动换行功能!
// 2. 鲁棒性很差,没有检错纠错机制
if(USART_GetITStatus(USART1,USART_IT_RXNE) != RESET) //RXNE中断处理过程要快和精简,否则可能造成溢出错误!
{
DMA_EnableCmd(DMA1_Channel5); //使能一次DMA传输
while(1) //等待DMA传输完成
{
if(DMA_GetFlagStatus(DMA1_FLAG_TC5) != RESET) //DMA传输完成
{
USART_RX_STA++;
DMA_ClearFlag(DMA1_FLAG_TC5);
DMA_FLAG++;
break;
}
}
//USART_RX_BUF[USART_RX_STA++] = USART_ReceiveData(USART1); //每一次新的接收都会重新刷新BUF!并清除RXNE标志位!
if(USART_RX_STA > USART_REC_LEN) //RXNE中断处理不能用delay不能用printf,否则会产生ORE报错!
{
USART_RX_STA = 0;
MyFlag = 1;
//memset(USART_RX_BUF,0,sizeof(USART_RX_BUF)); //发送的数据最后要带\r\n
}
}
//**************************************RXNE中断+DMA传输************************************************//
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntExit();
#endif
}
#endif
附main.c代码
#include "led.h"
#include "delay.h" //delay_ms(nms): nms < 1864 !
#include "key.h"
#include "sys.h"
#include "usart.h"
#include "timer.h"
#include "string.h"
#include "servo.h"
#include "dma.h"
#define DMA_SENDBUF_BYTESIZE 2 //定义一次DMA传输的数据量(单位:字节) 不能添加自动换行!
u16 arr = 10000;
//u16 led0pwmval = 1500;
u16 Duty_Count = 1500;
u16 Duty_Count_Init = 1500;
//u8 SendBuf[USART_REC_LEN];
int main(void)
{
// int flag = 0;
u16 psc = 72;
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
uart_init(115200); //串口初始化为115200
//LED_Init(); //LED端口初始化
TIM4_PWM_Init(arr-1,psc-1); //不分频。PWM频率=72000000/10000/72 = 100hz
TIM_SetCompare1(TIM4,Duty_Count);
DMA_Config(DMA1_Channel5, DMA_DIR_PeripheralSRC, (u32)& USART1 -> DR, (u32)USART_RX_BUF, DMA_SENDBUF_BYTESIZE); //USART1接收DMA,使用通道5
while(1)
{
//printf("数据允许宽度为%d,请输入指令:(输入范围20-90)\r\n\r\n",USART_REC_LEN-2);
printf("%s\r\n",USART_RX_BUF);
delay_ms(1000);
delay_ms(1000);
delay_ms(1000);
delay_ms(1000);
delay_ms(1000);
}
}