stm32 DMA接收串口数据实现与深度分析

前言

本文实现一个用DMA接收串口数据的功能,并对实现过程进行深度分析。参考的原始代码是正点原子的【资料盘】->【2,标准例程-HAL库版本】->【实验5 串口实验】。

代码实现

usart.c部分代码:

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"


/* 如果使用os,则包括下面的头文件即可. */
#if SYS_SUPPORT_OS
#include "includes.h" /* os 使用 */
#endif

/******************************************************************************************/
/* 加入以下代码, 支持printf函数, 而不需要选择use MicroLIB */

#if (__ARMCC_VERSION >= 6010050)            /* 使用AC6编译器时 */
__asm(".global __use_no_semihosting\n\t");  /* 声明不使用半主机模式 */
__asm(".global __ARM_use_no_argv \n\t");    /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式 */

#else
/* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
#pragma import(__use_no_semihosting)

struct __FILE
{
    int handle;
    /* Whatever you require here. If the only file you are using is */
    /* standard output using printf() for debugging, no file handling */
    /* is required. */
};

#endif

/* 不使用半主机模式,至少需要重定义_ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式 */
int _ttywrch(int ch)
{
    ch = ch;
    return ch;
}

/* 定义_sys_exit()以避免使用半主机模式 */
void _sys_exit(int x)
{
    x = x;
}

char *_sys_command_string(char *cmd, int len)
{
    return NULL;
}


/* FILE 在 stdio.h里面定义. */
FILE __stdout;

/* MDK下需要重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
int fputc(int ch, FILE *f)
{
    while ((USART_UX->SR & 0X40) == 0);     /* 等待上一个字符发送完成 */

    USART_UX->DR = (uint8_t)ch;             /* 将要发送的字符 ch 写入到DR寄存器 */
    return ch;
}
/******************************************************************************************/


UART_HandleTypeDef g_uart1_handle;  /* UART句柄 */

/**
 * @brief       串口X初始化函数
 * @param       baudrate: 波特率, 根据自己需要设置波特率值
 * @note        注意: 必须设置正确的时钟源, 否则串口波特率就会设置异常.
 *              这里的USART的时钟源在sys_stm32_clock_init()函数中已经设置过了.
 * @retval      无
 */
void usart_init(uint32_t baudrate)
{
    /*UART 初始化设置*/
    g_uart1_handle.Instance = USART_UX;                                       /* USART_UX */
    g_uart1_handle.Init.BaudRate = baudrate;                                  /* 波特率 */
    g_uart1_handle.Init.WordLength = UART_WORDLENGTH_8B;                      /* 字长为8位数据格式 */
    g_uart1_handle.Init.StopBits = UART_STOPBITS_1;                           /* 一个停止位 */
    g_uart1_handle.Init.Parity = UART_PARITY_NONE;                            /* 无奇偶校验位 */
    g_uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;                      /* 无硬件流控 */
    g_uart1_handle.Init.Mode = UART_MODE_TX_RX;                               /* 收发模式 */
    HAL_UART_Init(&g_uart1_handle);                                           /* HAL_UART_Init()会使能UART1 */
}

/**
 * @brief       UART底层初始化函数
 * @param       huart: UART句柄类型指针
 * @note        此函数会被HAL_UART_Init()调用
 *              完成时钟使能,引脚配置,中断配置
 * @retval      无
 */
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    GPIO_InitTypeDef gpio_init_struct;

    if (huart->Instance == USART_UX)                            /* 如果是串口1,进行串口1 MSP初始化 */
    {
        USART_TX_GPIO_CLK_ENABLE();                             /* 使能串口TX脚时钟 */
        USART_RX_GPIO_CLK_ENABLE();                             /* 使能串口RX脚时钟 */
        USART_UX_CLK_ENABLE();                                  /* 使能串口时钟 */

        gpio_init_struct.Pin = USART_TX_GPIO_PIN;               /* 串口发送引脚号 */
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;                /* 复用推挽输出 */
        gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;          /* IO速度设置为高速 */
        HAL_GPIO_Init(USART_TX_GPIO_PORT, &gpio_init_struct);
                
        gpio_init_struct.Pin = USART_RX_GPIO_PIN;               /* 串口RX脚 模式设置 */
        gpio_init_struct.Mode = GPIO_MODE_AF_INPUT;    
        HAL_GPIO_Init(USART_RX_GPIO_PORT, &gpio_init_struct);   /* 串口RX脚 必须设置成输入模式 */
        
#if USART_EN_RX
       HAL_NVIC_EnableIRQ(USART_UX_IRQn);                      /* 使能USART1中断通道 */
       HAL_NVIC_SetPriority(USART_UX_IRQn, 3, 3);              /* 组2,最低优先级:抢占优先级3,子优先级3 */
       __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);              /* 使能UART总线空闲中断 */
#endif
    }
}

/**
 * @brief       串口X中断服务函数
                注意,读取USARTx->SR能避免莫名其妙的错误
 * @param       无
 * @retval      无
 */
void USART_UX_IRQHandler(void)
{
    if (__HAL_UART_GET_FLAG(&g_uart1_handle, UART_FLAG_IDLE) != RESET)       /* UART总线空闲中断 */
    {
        //dma_set_next_buf();                         						 /* 清除UART总线空闲中断 */
        dma_update_recvnum();
        __HAL_UART_CLEAR_IDLEFLAG(&g_uart1_handle);                          /* 清除UART总线空闲中断 */
    }

}

main.c部分代码

#include "./stm32f1xx_it.h"
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"

DMA_HandleTypeDef rx_dam_handle;

typedef struct dma_recv_buff
{
    struct dma_recv_buff* next_ptr;     //指向下一个缓冲结构体
    uint8_t buff[56];                   //接收数据缓冲区
    unsigned int recv_num;              //缓冲区中接收到的字节数
}DMA_RECV_BUFF;

DMA_RECV_BUFF dma_buf_one;
DMA_RECV_BUFF dma_buf_two;
DMA_RECV_BUFF dma_buf_three;

DMA_RECV_BUFF* dma_buf_now;

void dma_rx_init()
{
 
    __HAL_RCC_DMA1_CLK_ENABLE();                      /* DMA1时钟使能 */
    
    /* Tx DMA配置 */
    rx_dam_handle.Instance = DMA1_Channel5;                          /* USART1_TX使用的DMA通道为: DMA1_Channel5 */
    rx_dam_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;             /* 外设到存储器模式 */
    rx_dam_handle.Init.PeriphInc = DMA_PINC_DISABLE;                 /* 外设非增量模式 */
    rx_dam_handle.Init.MemInc = DMA_MINC_ENABLE;                     /* 存储器增量模式 */
    rx_dam_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;    /* 外设数据长度:8位 */
    rx_dam_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;       /* 存储器数据长度:8位 */
    rx_dam_handle.Init.Mode = DMA_NORMAL;                            /* DMA模式:正常模式 */
    rx_dam_handle.Init.Priority = DMA_PRIORITY_MEDIUM;               /* 中等优先级 */

    HAL_DMA_Init(&rx_dam_handle);
    __HAL_LINKDMA(&g_uart1_handle, hdmarx, rx_dam_handle);           /* 将DMA与USART1联系起来(发送DMA) */
	HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);
}

void dma_buff_init()
{
	dma_buf_one.next_ptr = &dma_buf_two;
    memset(dma_buf_one.buff, 0, 56);
    dma_buf_one.recv_num = 0;

    dma_buf_two.next_ptr = &dma_buf_three;
    memset(dma_buf_two.buff, 0, 56);
    dma_buf_two.recv_num = 0;

    dma_buf_three.next_ptr = &dma_buf_one;
    memset(dma_buf_three.buff, 0, 56);
    dma_buf_three.recv_num = 0;

    dma_buf_now = &dma_buf_one;
}

void dma_update_recvnum()
{
    dma_buf_now->recv_num = 56 - __HAL_DMA_GET_COUNTER(&rx_dam_handle);
}

void dma_set_next_buf()
{

	HAL_UART_DMAStop(&g_uart1_handle);

    dma_buf_now->recv_num = 56 - __HAL_DMA_GET_COUNTER(&rx_dam_handle);
    dma_buf_now = dma_buf_now->next_ptr;
    dma_buf_now->recv_num = 0;
	
	HAL_UART_Receive_DMA(&g_uart1_handle, (uint8_t *)dma_buf_now->buff, 56);
}

int main(void)
{
    HAL_Init();                             /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);     /* 设置时钟为72Mhz */
    delay_init(72);                         /* 延时初始化 */
    usart_init(115200);                     /* 串口初始化为115200 */
    dma_rx_init();
    dma_buff_init();
	HAL_UART_Receive_DMA(&g_uart1_handle, (uint8_t *)dma_buf_now->buff, 56);    
	
    while (1)
    {
        printf("\r\n %x \r\n", __HAL_DMA_GET_COUNTER(&rx_dam_handle));
		
        for (int i = 0; i < 56; i++)
        {
            printf(" %x ", dma_buf_one.buff[i]);
        }
        printf(" \r\n dma_buf_one.recv_num  %d \r\n", dma_buf_one.recv_num);
        for (int i = 0; i < 56; i++)
        {
            printf(" %x ", dma_buf_two.buff[i]);
        }
		printf(" \r\n dma_buf_two.recv_num  %d \r\n", dma_buf_two.recv_num);
        for (int i = 0; i < 56; i++)
        {
            printf(" %x ", dma_buf_three.buff[i]);
        }
        printf(" \r\n dma_buf_three.recv_num  %d \r\n", dma_buf_three.recv_num);
		
		printf("\r\nDMA1->ISR    %x  \r\n",DMA1->ISR);
        printf("\r\nDMA1->ISR addr    %x  \r\n",(uint32_t)&(DMA1->ISR));
        printf("\r\nDMA1->IFCR    %x  \r\n",DMA1->IFCR);
		printf("\r\nDMA1_Channel5->CCR    %x  \r\n",DMA1_Channel5->CCR);
		printf("\r\nDMA1_Channel5->CNDTR    %x  \r\n",DMA1_Channel5->CNDTR);
        printf("\r\nDMA1_Channel5->CPAR    %x  \r\n",DMA1_Channel5->CPAR);
        printf("\r\nDMA1_Channel5->CMAR    %x  \r\n",DMA1_Channel5->CMAR);

        printf("\r\nUSART1->SR    %x  \r\n",USART1->SR);
        printf("\r\nUSART1->CR1    %x  \r\n",USART1->CR1);
        printf("\r\nUSART1->CR2    %x  \r\n",USART1->CR2);
        printf("\r\nUSART1->CR3    %x  \r\n",USART1->CR3);
        delay_ms(1000);
    }
}



/* UART DMA 接收半满中断 */
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
    //dma_set_next_buf();
    dma_update_recvnum();
}

/* UART DMA 接收全满中断 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    dma_set_next_buf();  
}


void DMA1_Channel5_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&rx_dam_handle);
}

代码实现功能介绍

以上代码实现功能是通过DMA自动接收并保存串口数据,其中定义了一个结构体:

typedef struct dma_recv_buff
{
    struct dma_recv_buff* next_ptr;     //指向下一个缓冲结构体
    uint8_t buff[56];                   //接收数据缓冲区
    unsigned int recv_num;              //缓冲区中接收到的字节数
}DMA_RECV_BUFF;

用来管理接受到的串口数据,其中next_ptr指向下一个缓冲结构体的指针,使得多个结构体组成一个环形链表,buff为接受数据的缓冲区域,recv_num表示缓冲区中接收到了多少字节的数据。
代码中定义了

DMA_RECV_BUFF dma_buf_one;
DMA_RECV_BUFF dma_buf_two;
DMA_RECV_BUFF dma_buf_three;

三个消息缓冲。在函数void dma_buff_init()中将他们串联起来形成一个环形链表。当dma_buf_one中存满数据后,将继续在dma_buf_two中保存数据,dma_buf_two存满后在dma_buf_three中保存数据,dma_buf_three存满后继续在dma_buf_one中保存数据,以此类推。
dma_update_recvnum()函数用于更新当前正在接受数据的缓冲区中接受到了多少数据。
dma_set_next_buf()函数用于更新当前缓冲区接受数据量,并且设置一个新的保存数据的缓冲区。
在串口的空闲中断

void USART_UX_IRQHandler(void)
{
    if (__HAL_UART_GET_FLAG(&g_uart1_handle, UART_FLAG_IDLE) != RESET)       /* UART总线空闲中断 */
    {
        dma_update_recvnum();
        __HAL_UART_CLEAR_IDLEFLAG(&g_uart1_handle);                          /* 清除UART总线空闲中断 */
    }
}

和DMA的半满中断

void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
    dma_update_recvnum();
}

中调用dma_update_recvnum()更新接受数据量。
在DMA的全满中断

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    dma_set_next_buf();  
}

中调用dma_set_next_buf()来设置下一个缓冲区。
代码运行起来效果如下:
在这里插入图片描述

原理分析

想让DMA自动传输数据,DMA会问我们4个问题怎么传?传多少?从哪来?到哪去?,这就对应着DMA每个通道的4个寄存器CCRCNTRCPARCMAR

CCR

主要用于告诉DMA数据传输方向(从外设到内存还是从内存到外设),是否开启全满中断或半满中断,以及打开通道开始传输。

CNTR

写入的时候用于设置DMA传输字节数,读取的时候用于表示DMA中剩余的待传输字节数据。该寄存器只有在CCR寄存器的bit0位为0(通道不工作)是才能写入。所以每次设置新的缓冲区要先调用HAL_UART_DMAStop()再调用HAL_UART_Receive_DMA()

CPAR

表示外设地址寄存器,本文代码外设是UASRT1,因此该寄存器设置为(uint32_t)&USART1->DR

CMAP

表示存储器地址。在本文代码中就是DMA_RECV_BUFF中的buff

串口通过DMA接收数据时序图如下:
在这里插入图片描述
表示当串口接收到一帧数据后,RXNE会被置位,这时DMA开始从寄存器USART_DR 读取这帧数据,同时读操作会自动将RXNE清零。当接收完CNTR寄存器中设置的传输字节数后,DMA的TCIP位会被置位,产生全满中断。
USART的寄存器在DMA传输过程中比较重要的是CR3寄存器,其中bit6置位表示使能接收时的DMA模式。所以可以看到本文代码在运行时打印的CR3数值为0x41。
具体寄存器说明可以在【STM32F10xxx参考手册】查找。

运行过程分析

DMA1_Channel5_IRQHandler()函数修改成:

void DMA1_Channel5_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&rx_dam_handle);
    printf("DMA1_Channel5_IRQHandler\r\n");
}

实验可以发现,在DMA出现全满或者半满时,都会打印出DMA1_Channel5_IRQHandler,可见DMA产生中断时是先进入DMA1_Channel5_IRQHandler()函数,至于是全满中断还半满中断在HAL_DMA_IRQHandler(&rx_dam_handle)函数中进行判断并调用响应的HAL_UART_RxHalfCpltCallback()函数或HAL_UART_RxCpltCallback()函数进行处理。
如果注释掉HAL_DMA_IRQHandler(&rx_dam_handle);那么在出现半满或全满时会一直打印DMA1_Channel5_IRQHandler可见HAL_DMA_IRQHandler(&rx_dam_handle);中有清除中断标志的操作。

结束语

如果觉得内容还不错就点个关注吧,感谢您的支持!有什么想法欢迎在评论区讨论。

  • 12
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值