前言
本文实现一个用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个寄存器CCR
、CNTR
、CPAR
、CMAR
。
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);
中有清除中断标志的操作。
结束语
如果觉得内容还不错就点个关注吧,感谢您的支持!有什么想法欢迎在评论区讨论。