STM32串口空闲中断配合DMA接收数据

目录

前言 

一 DMA和空闲中断

1.1 空闲中断

1.2 DMA

​ 二 代码实现

2.1 利用结构体实现快速替换引脚

2.2 GPIO初始化

2.3 串口及中断配置

2.4 DMA配置

2.5 串口中断服务函数

结尾


前言 

        废话不多话,直接贴上驱动代码:

#include "stm32f10x.h"                  // Device header
#include "stdio.h"
#include "stdbool.h"

typedef struct 
{
	uint32_t 				rccGpio;
	uint32_t 				rccUart;
	uint32_t				rccDma;
	DMA_Channel_TypeDef*	dmaChannel;
	GPIO_TypeDef* 			gpio;
	USART_TypeDef*			uartNo;
	uint16_t 				uartDmaReq;
	uint16_t 				txPin;
	uint16_t 				rxPin;
	IRQn_Type				irq;
}UsartPara_t;

static UsartPara_t g_uartInfo = 
{
	RCC_APB2Periph_GPIOB, RCC_APB1Periph_USART3,RCC_AHBPeriph_DMA1,
	DMA1_Channel3, GPIOB, USART3, USART_DMAReq_Rx, GPIO_Pin_10, GPIO_Pin_11, USART3_IRQn, 
};

#define USART3_DATA_ADDR	(USART3_BASE + 0x04)

#define MAX_BUF_SIZE		20
static uint8_t g_rcvDataBuf[MAX_BUF_SIZE];

static void GpioInit(void)
{
	/*时钟使能*/
	RCC_APB2PeriphClockCmd(g_uartInfo.rccGpio, ENABLE);
	/*引脚配置*/
	GPIO_InitTypeDef gpioStruct;
	gpioStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	gpioStruct.GPIO_Pin = g_uartInfo.txPin;		//tx
	gpioStruct.GPIO_Speed = GPIO_Speed_10MHz;
	GPIO_Init(g_uartInfo.gpio , &gpioStruct);
	
	gpioStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	gpioStruct.GPIO_Pin = g_uartInfo.rxPin;		//rx
	gpioStruct.GPIO_Speed = GPIO_Speed_10MHz;
	GPIO_Init(g_uartInfo.gpio , &gpioStruct);
}

static void UartInit(uint32_t baudRate)
{
	/*时钟使能*/
	RCC_APB1PeriphClockCmd(g_uartInfo.rccUart, ENABLE);
	/*串口配置*/
	USART_InitTypeDef uartStruct;
	uartStruct.USART_BaudRate  = baudRate;
	uartStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	uartStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
	uartStruct.USART_Parity = USART_Parity_No;		
	uartStruct.USART_StopBits = USART_StopBits_1;	
	uartStruct.USART_WordLength = USART_WordLength_8b;
	USART_Init(g_uartInfo.uartNo, &uartStruct);
	/*中断输出配置*/
	USART_ITConfig(g_uartInfo.uartNo, USART_IT_IDLE, ENABLE);	//开启串口接收数据的空闲中断
	USART_DMACmd(g_uartInfo.uartNo, g_uartInfo.uartDmaReq, ENABLE);
	/*NVIC中断分组,整个工程只能分组一次*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;					
	NVIC_InitStructure.NVIC_IRQChannel = g_uartInfo.irq;		
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;		
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;		
	NVIC_Init(&NVIC_InitStructure);							
	/*串口使能*/
	USART_Cmd(g_uartInfo.uartNo, ENABLE);
}

static void UartDmaInit(void)
{
	/* 使能DMA时钟;*/
	RCC_AHBPeriphClockCmd(g_uartInfo.rccDma, ENABLE);
	/* 复位DMA通道;*/
	DMA_DeInit(g_uartInfo.dmaChannel);
	/*配置DMA*/
	DMA_InitTypeDef DMA_InitStruct;
	DMA_StructInit (&DMA_InitStruct);
	DMA_InitStruct.DMA_BufferSize = MAX_BUF_SIZE;						//配置数据传输最大次数
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;						//配置传输方向,指定外设为数据源
	DMA_InitStruct.DMA_M2M  = DMA_M2M_Disable;
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)g_rcvDataBuf;			//配置数据目的地址
	DMA_InitStruct.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte;	//配置目的数据传输位宽
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;				//配置目的地址是固定的还是增长的
	DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
	DMA_InitStruct.DMA_PeripheralBaseAddr = USART3_DATA_ADDR;			//配置数据源地址
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//配置源数据传输位宽
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;		//配置源地址是固定的还是增长的
	DMA_InitStruct.DMA_Priority = DMA_Priority_High;					//配置DMA通道优先级
	DMA_Init(g_uartInfo.dmaChannel, &DMA_InitStruct);
	/*DMA使能*/
	DMA_ClearFlag(DMA1_FLAG_TC3);										//清除通道x的传输完成标志
	DMA_Cmd(g_uartInfo.dmaChannel, ENABLE);								//DMA使能
}

/**
***********************************************************
* @brief	串口驱动初始化
* @param
* @return 
***********************************************************
*/
void UartDrvInit(void)
{
	GpioInit();
	UartInit(9600);
	UartDmaInit();
}

/**
***********************************************************
* @brief 串口3中断服务函数
* @param
* @return 
***********************************************************
*/
void USART3_IRQHandler(void)
{
//	uint16_t tmpData = 0;
	if (USART_GetITStatus(g_uartInfo.uartNo, USART_IT_IDLE) != RESET)
	{
		//由软件序列清除IDLE(先读USART_SR,然后读USART_DR)。
		USART_ClearITPendingBit(g_uartInfo.uartNo, USART_IT_IDLE);
		USART_ReceiveData(g_uartInfo.uartNo);
//		tmpData = g_uartInfo.uartNo->SR;						//第一步,读取SR状态寄存器,清除IDLE标志位
//		tmpData = g_uartInfo.uartNo->DR;						//第二步,读取DR数据寄存器,清除IDLE标志位
		if (PACKET_DATA_LEN == (MAX_BUF_SIZE - DMA_GetCurrDataCounter(g_uartInfo.dmaChannel)))
		{
			g_packetRcvFlag = true;
		}
		DMA_Cmd(g_uartInfo.dmaChannel, DISABLE);				//失能DMA,避免破坏数据。
		DMA_SetCurrDataCounter(g_uartInfo.dmaChannel,MAX_BUF_SIZE);//重新配置DMA和DMA的接收长度
		DMA_Cmd(g_uartInfo.dmaChannel, ENABLE);					//使能DMA
	}
}

/**
***********************************************************
* @brief printf函数默认打印输出到显示器,如果要输出到串口,
		 必须重新实现fputc函数,将输出指向串口,称为重定向
* @param
* @return 
***********************************************************
*/
int fputc(int ch, FILE *f)
{
	USART_SendData(g_uartInfo.uartNo, (uint8_t)ch);
	while (RESET == USART_GetFlagStatus(g_uartInfo.uartNo, USART_FLAG_TXE));
	return ch;
}

一 DMA和空闲中断

1.1 空闲中断

        普通中断,接收一包数据的过程中,每来一个字节就要产生一次中断,如果一包数据有7个字节,那接收这一包数据就要产生7次中断。这样就会频繁的打断CPU的主流程。下面是普通串口接收中断的流程图:

        DMA和串口空闲中断就是要解决这样的问题,接收一包数据只需要进入一次中断就可以了。

        所谓的空闲中断就是依靠的空闲位,在一包数据中,字符帧与字符帧之间的空闲位可以忽略不计,很短;但是对于一包数据与一包数据之间的空闲位则不能忽略了,很长。当检测到RX引脚空闲时间(高电平)超过传输一个字符帧所需的时间,并且IDLEIE位被设置,就会产生一个空闲中断。

        假如串口波特率为9600,代表1S可以传输9600bits,那么传输1bits所需要的时间就是10^6/9600, 那么传输一个字符帧所需的时间就是10^7/9600 = 1.041ms。也就是说只要RX空闲时间>1.041ms就会产生空闲中断。

1.2 DMA

        那么普通串口接收中断,接收一个字符帧后,产生中断,把这个字符数据从USART_DR数据寄存器中搬运到我们程序中写的数组里面。那么,使用串口空闲中断,一次性就接收一包数据,是怎么把它存储到内存数组中的呢?这就要用到ARM32位单片机的另一个功能模块叫做DMA(直接存储器访问)

        下面这张图是基于GD32单片机的思维图,但是并不妨碍用于STM32的分析,原理都是一样的,串口外设的数据寄存器每来一个字节,通过DMA将这个字节的数据自动的搬运到内存的数组当中,不需要CPU的干预,只需要对DMA初始化配置就可以了。

        要配置传输方向,是从外设到内存还是内存到外设,还是….

        要配置数据源所在的地址和数据目的地址,比如本例中就是从串口数据寄存器(数据源地址)传输到内存(数组地址)。

        要配置传输的位宽,是8位还是16位或32位,由于串口数据寄存器USART_DR固定是8位,所以配置成8位就可以了。

        固定地址和增量地址的意思是,比如本例是从串口的数据寄存器传输数据到内存数组,那我串口数据寄存器的地址肯定是不变的啊,所以源地址配置为固定地址;而内存数组,就是我程序中写的那个数组,它的每一个元素的地址都是不同的啊,所以我的目的地址一定配置为增量地址。

         STM32的DMA1有7个通道,DMA2有5个通道,具体信息可以去查看用户手册10.3.7节。

DMA1的通道3不仅可以给USART3_RX使用,也可以给TIM1_CH2使用,假如我们在程序中同时使能了DMA1的通道3给这两个外设使用,当同时要搬运数据的时候,谁的优先级更高就先去搬谁的数据。

 二 代码实现

2.1 利用结构体实现快速替换引脚

        利用结构体把驱动需要用到的东西全写在这里面,这样,以后更换引脚的时候,只需要更改这里面的参数和驱动的一部分函数和中断服务函数就可以了,十分方便。

typedef struct 
{
	uint32_t 				rccGpio;
	uint32_t 				rccUart;
	uint32_t				rccDma;
	DMA_Channel_TypeDef*	dmaChannel;
	GPIO_TypeDef* 			gpio;
	USART_TypeDef*			uartNo;
	uint16_t 				uartDmaReq;
	uint16_t 				txPin;
	uint16_t 				rxPin;
	IRQn_Type				irq;
}UsartPara_t;

static UsartPara_t g_uartInfo = 
{
	RCC_APB2Periph_GPIOB, RCC_APB1Periph_USART3,RCC_AHBPeriph_DMA1,
	DMA1_Channel3, GPIOB, USART3, USART_DMAReq_Rx, GPIO_Pin_10, GPIO_Pin_11, USART3_IRQn, 
};

2.2 GPIO初始化

        使能时钟的时候,需要注意你用的外设属于APB1还是APB2还是AHB;

        tx引脚配置成复用推挽输出,因为使用的是它的串口TX而不是普通IO功能,如果配置成普通推挽输出那就出错了。

        rx配置成浮空输入是因为

  • 浮空输入可以检测到高电平(逻辑1)和低电平(逻辑0),并在未接收数据时保持高阻抗状态,减少功耗。
  • 浮空输入引脚在没有数据时不会影响总线状态,避免了对其他设备的干扰。
  • static void GpioInit(void)
    {
    	/*时钟使能*/
    	RCC_APB2PeriphClockCmd(g_uartInfo.rccGpio, ENABLE);
    	/*引脚配置*/
    	GPIO_InitTypeDef gpioStruct;
    	gpioStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    	gpioStruct.GPIO_Pin = g_uartInfo.txPin;		//tx
    	gpioStruct.GPIO_Speed = GPIO_Speed_10MHz;
    	GPIO_Init(g_uartInfo.gpio , &gpioStruct);
    	
    	gpioStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    	gpioStruct.GPIO_Pin = g_uartInfo.rxPin;		//rx
    	gpioStruct.GPIO_Speed = GPIO_Speed_10MHz;
    	GPIO_Init(g_uartInfo.gpio , &gpioStruct);
    }

    2.3 串口及中断配置

            /*时钟使能*/

            USART3是APB1的外设,如果你使用的是其它串口,记得检查时钟使能函数与外设对应。

             /*串口配置*/

            波特率看你项目的需要配置。

            硬件控制流以后用到在细说。

            模式设置为接收和发送都使能。

            没有奇偶校验位,正经人谁还用奇偶校验,埋汰。

            停止位设置为1位。

            一个字符帧中的数据位数为8位,对应串口数据寄存器位数。

             /*中断输出配置*/

            启用串口空闲中断。

            使能串口DMA接收请求。

            /*NVIC配置*/

            中断分组一个工程只能分一次组,写在这其它地方就不用写了。

    static void UartInit(uint32_t baudRate)
    {
    	/*时钟使能*/
    	RCC_APB1PeriphClockCmd(g_uartInfo.rccUart, ENABLE);
    	/*串口配置*/
    	USART_InitTypeDef uartStruct;
    	uartStruct.USART_BaudRate  = baudRate;
    	uartStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    	uartStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    	uartStruct.USART_Parity = USART_Parity_No;		
    	uartStruct.USART_StopBits = USART_StopBits_1;	
    	uartStruct.USART_WordLength = USART_WordLength_8b;
    	USART_Init(g_uartInfo.uartNo, &uartStruct);
    	/*中断输出配置*/
    	USART_ITConfig(g_uartInfo.uartNo, USART_IT_IDLE, ENABLE);	//开启串口接收数据的空闲中断
    	USART_DMACmd(g_uartInfo.uartNo, g_uartInfo.uartDmaReq, ENABLE);
    	/*NVIC中断分组,整个工程只能分组一次*/
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
    	/*NVIC配置*/
    	NVIC_InitTypeDef NVIC_InitStructure;					
    	NVIC_InitStructure.NVIC_IRQChannel = g_uartInfo.irq;		
    	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			
    	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;		
    	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;		
    	NVIC_Init(&NVIC_InitStructure);							
    	/*串口使能*/
    	USART_Cmd(g_uartInfo.uartNo, ENABLE);
    }

    2.4 DMA配置

            这个代码里写的比较清晰了,DMA_StructInit (&DMA_InitStruct);是为了把结构体初始化一下,避免出现有成员没有赋值的情况。

            DMA_ClearFlag(DMA1_FLAG_TC3);清除通道X传输完成标志。在用户手册10.4.2那一节可以清楚地看到DMA中断标志清除寄存器(DMA_IFCR),bit[27:0]是有效的,28/4刚好就是四个通道,参数DMA1_FLAG_TC3就是通道3的CTCIF位,也就是bit9置1。

    #define USART3_DATA_ADDR	(USART3_BASE + 0x04)
    #define MAX_BUF_SIZE		20
    static uint8_t g_rcvDataBuf[MAX_BUF_SIZE];
    
    static void UartDmaInit(void)
    {
    	/* 使能DMA时钟;*/
    	RCC_AHBPeriphClockCmd(g_uartInfo.rccDma, ENABLE);
    	/* 复位DMA通道;*/
    	DMA_DeInit(g_uartInfo.dmaChannel);
    	/*配置DMA*/
    	DMA_InitTypeDef DMA_InitStruct;
    	DMA_StructInit (&DMA_InitStruct);
    	DMA_InitStruct.DMA_BufferSize = MAX_BUF_SIZE;						//配置数据传输最大次数
    	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;						//配置传输方向,指定外设为数据源
    	DMA_InitStruct.DMA_M2M  = DMA_M2M_Disable;
    	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)g_rcvDataBuf;			//配置数据目的地址
    	DMA_InitStruct.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte;	//配置目的数据传输位宽
    	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;				//配置目的地址是固定的还是增长的
    	DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
    	DMA_InitStruct.DMA_PeripheralBaseAddr = USART3_DATA_ADDR;			//配置数据源地址
    	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//配置源数据传输位宽
    	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;		//配置源地址是固定的还是增长的
    	DMA_InitStruct.DMA_Priority = DMA_Priority_High;					//配置DMA通道优先级
    	DMA_Init(g_uartInfo.dmaChannel, &DMA_InitStruct);
    	/*DMA使能*/
    	DMA_ClearFlag(DMA1_FLAG_TC3);										//清除通道x的传输完成标志
    	DMA_Cmd(g_uartInfo.dmaChannel, ENABLE);								//DMA使能
    }

    2.5 串口中断服务函数

            查手册25.6.1,可以知道状态寄存器(USART_SR)的bit4:

    当检测到总线空闲时,该位被硬件置位。如果USART_CR1中的IDLEIE为’1’,则产生中断。

            要清除这个中断标志位:

    由软件序列清除该位(先读USART_SR,然后读USART_DR)。

            代码中被我注释掉的部分就是用来清除空闲中断标志位的,当然下面这两句代码是我学习别人的做法,也实现了清除标志位需要的两步操作,只是我不太能理解原因。        USART_ClearITPendingBit(g_uartInfo.uartNo, USART_IT_IDLE);
    USART_ReceiveData(g_uartInfo.uartNo);这也是我能理解的方式。

            DMA_GetCurrDataCounter(g_uartInfo.dmaChannel);这个函数用来返回当前DMA通道中剩余数据单元的数量,在DMA的配置中已经设置数据传输最大次数为MAX_BUF_SIZE,即20,假设一包数据有PACKET_DATA_LEN个数据帧,那么用最大次数减去函数返回的值不就是已经传输的单元数量了吗, 如果得到的结果与PACKET_DATA_LEN相等说明一包数据接收完了。

    /**
    ***********************************************************
    * @brief 串口3中断服务函数
    * @param
    * @return 
    ***********************************************************
    */
    void USART3_IRQHandler(void)
    {
    //	uint16_t tmpData = 0;
    	if (USART_GetITStatus(g_uartInfo.uartNo, USART_IT_IDLE) != RESET)
    	{
    		//由软件序列清除IDLE(先读USART_SR,然后读USART_DR)。
    		USART_ClearITPendingBit(g_uartInfo.uartNo, USART_IT_IDLE);
    		USART_ReceiveData(g_uartInfo.uartNo);
    //		tmpData = g_uartInfo.uartNo->SR;						//第一步,读取SR状态寄存器,清除IDLE标志位
    //		tmpData = g_uartInfo.uartNo->DR;						//第二步,读取DR数据寄存器,清除IDLE标志位
    		if (PACKET_DATA_LEN == (MAX_BUF_SIZE - DMA_GetCurrDataCounter(g_uartInfo.dmaChannel)))
    		{
    			//g_packetRcvFlag = true;
    		}
    		DMA_Cmd(g_uartInfo.dmaChannel, DISABLE);				//失能DMA,避免破坏数据。
    		DMA_SetCurrDataCounter(g_uartInfo.dmaChannel,MAX_BUF_SIZE);//重新配置DMA和DMA的接收长度
    		DMA_Cmd(g_uartInfo.dmaChannel, ENABLE);					//使能DMA
    	}
    }

    结尾

    我也是小白上路,写的东西若是有不对的地方希望大家在评论区指出,如果这篇文章对您有帮助,这是我的荣幸,希望能得到您的关注和点赞,收藏。谢谢。

    作者:Winter、rui

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32串口空闲中断配合DMA接收数据

    发表回复