我这里选用串口一为例来实现,在cubeMX中启用串口1,参数保持默认即可。
在DMA设置选项卡中点击Add按钮添加 USART1_RX ,并在模式设置中选择 Cirular 环形模式,其余参数不变,即启用加串口1的接收DMA功能,并实现环形缓冲。
在Project Manager选项卡(1)中的高级设置(2)中可以设置每个外设所使用的库,我们将所有外设的库全部选择为LL库。
创建一个 UART_DAT 串口数据结构体,存放收发数据缓存指针,收发数据长度,收发标志位。
/* 串口设备数据接收结构体 */
typedef struct
{
uint8_t *pTxBuf; /* 发送缓冲区 */
uint8_t *pRxBuf; /* 接收缓冲区 */
uint16_t LEN; /* 接收到的数据长度 */
uint8_t FLG; /* 接收标志位 */
}UART_DAT;
开辟两个缓存区域用于串口数据的收发,通过 UART1_RX_BUF_SIZE和UART1_TX_BUF_SIZE控制缓冲区大小。
/* 定义串口缓冲区大小,分为发送缓冲区和接收缓冲区 */
#define UART1_TX_BUF_SIZE 1
#define UART1_RX_BUF_SIZE 1*512
/* 定义每个串口结构体变量 */
UART_DAT dat_Uart1 = {0};
static uint8_t g_TxBuf1[UART1_TX_BUF_SIZE+1]; /* 发送缓冲区 */
static uint8_t g_RxBuf1[UART1_RX_BUF_SIZE+1]; /* 接收缓冲区 */
初始化串口结构体,设置串口收发缓存指针。
static void UartVarInit(void)
{
/* 清空结构体 */
memset(dat_Uart1, 0, sizeof(UART_DAT));
/* 串口1 用到的缓存 */
dat_Uart1.pRxBuf = g_RxBuf1;
dat_Uart1.pTxBuf = g_TxBuf1;
}
cubeMX生成的初始化中并不包含DMA的中断处理,也不包含DMA的开启,所以我们这里需要手动配置一下。
先对DMA的接收缓存地址和长度进行配置,串口1的接收DMA对应为DMA1的通道5,DMA方向为外设(串口)到内存。所以我们这里先设置DMA的外设地址为串口1的地址,内存地址设置为串口接收缓冲区,并将缓冲区长度设置为 UART1_RX_BUF_SIZE 的大小,代码如下。
/* 配置外设地址 */
LL_DMA_SetPeriphAddress(DMA1, LL_DMA_CHANNEL_5, LL_USART_DMA_GetRegAddr(USART1));
/* 配置缓冲区地址 */
LL_DMA_SetMemoryAddress(DMA1, LL_DMA_CHANNEL_5, (uint32_t)dat_Uart1.pRxBuf);
/* 配置缓冲区大小 */
LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_5, UART1_RX_BUF_SIZE);
这里也可以使用函数 LL_DMA_ConfigAddresses 完成缓冲区和外设地址的设置。一个函数代替两个函数,不过参数比较多,不如上面整洁。
/* 配置地址 */
LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_5,
LL_USART_DMA_GetRegAddr(USART1),
(uint32_t)dat_Uart1.pRxBuf,
LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_5));
/* 设置缓冲区大小 */
LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_5, UART1_RX_BUF_SIZE);
接下来我们在这里分别启用串口空闲中断、DMA传输完成中断、DMA传输错误中断。
// 启用串口空闲中断
LL_USART_EnableIT_IDLE(USART1);
/* 开启DMA传输完成 传输错误中断 */
LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_5);
LL_DMA_EnableIT_TE(DMA1, LL_DMA_CHANNEL_5);
配置完成后启动DMA通道,并开启串口DMA接收功能。
/* 启动串口接收DMA通道 并启用串口接收DMA */
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_5);
LL_USART_EnableDMAReq_RX(USART1);
/*
*******************************************************************************************
* 函 数 名: InitHardUart
* 功能说明: 配置串口的硬件参数、启用串口中断等功能
* 形 参: 无
* 返 回 值: 无
*******************************************************************************************
*/
static void InitHardUart(void)
{
/* 设置DMA接收缓存和缓存大小 */
LL_DMA_SetPeriphAddress(DMA1, LL_DMA_CHANNEL_5, LL_USART_DMA_GetRegAddr(USART1));
LL_DMA_SetMemoryAddress(DMA1, LL_DMA_CHANNEL_5, (uint32_t)dat_Uart1.pRxBuf);
LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_5, UART1_RX_BUF_SIZE);
// 启用串口空闲中断
LL_USART_EnableIT_IDLE(USART1);
/* 开启DMA传输完成 传输错误中断 */
LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_5);
LL_DMA_EnableIT_TE(DMA1, LL_DMA_CHANNEL_5);
/* 启动串口接收DMA通道 并启用串口接收DMA */
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_5);
LL_USART_EnableDMAReq_RX(USART1);
}
我们通过 LL_USART_IsActiveFlag_IDLE() 函数和 LL_USART_IsEnabledIT_IDLE() 函数来判断串口是否进入空闲中断。
在中断函数内部先清除空闲中断标志位并关闭DMA防止DMA继续接收数据,同时我们使用LL_DMA_GetDataLength() 函数来获取缓存区剩余空间大小,用缓冲区大小和剩余空间大小相减即可获取当前接收到的数据长度。
dat_Uart1.LEN = UART1_RX_BUF_SIZE - LL_DMA_GetDataLength(DMA1, LL_DMA_CHANNEL_5);
这里将末尾置为0对字符串类型的数据进行截断,方便printf类型函数%s参数的打印。
dat_Uart1.pRxBuf[dat_Uart1.LEN] = 0;
在计算完接收长度,并对数据处理后,我们重新设置DMA数据空间大小,并打开DMA接续下一帧数据的接收。
/*
*****************************************************************************************
* 函 数 名: MY_UART_IRQHandler
* 功能说明: 串口中断处理函数
* 可同时处理多个串口的中断事件
* 形 参:USARTx: 串口设备
* 返 回 值: 无
*****************************************************************************************
*/
void MY_UART_IRQHandler(USART_TypeDef *USARTx)
{
/* 判断空闲中断标志位 */
if (LL_USART_IsActiveFlag_IDLE(USARTx) && LL_USART_IsEnabledIT_IDLE(USARTx))
{
// 清除空闲中断标志位
LL_USART_ClearFlag_IDLE(USARTx);
LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_5);
/* LED灯翻转指示数据收发 */
LL_GPIO_TogglePin(GPIOC, LEDB_Pin);
dat_Uart1.LEN = UART1_RX_BUF_SIZE - LL_DMA_GetDataLength(DMA1, LL_DMA_CHANNEL_5);
/* 打开接收完成标志位 并将数据传入接收函数中处理 */
dat_Uart1.FLG = 1;
dat_Uart1.pRxBuf[dat_Uart1.LEN] = 0;
/* 重新设置数据长度并打开DMA,使DMA从头开始接收 */
LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_5, UART1_RX_BUF_SIZE);
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_5);
}
else
{
/* 错误回调函数 */
UART_Error_Callback();
}
}
最后我们将 MY_UART_IRQHandler() 函数放在CubeMX生成的中断函数中。
DMA传输出错的回调函数和中断如果不需要可以不启用,函数如下。
/*
*******************************************************************************************
* 函 数 名: DMA_ReceiveComplete_Callback
* 功能说明: DMA接收完成中断回调函数
* 形 参:无
* 返 回 值: 无
*******************************************************************************************
*/
void DMA_ReceiveComplete_Callback(void)
{
/* 将接收满的数据打印 */
LOG_RTT_INFO("%s", dat_Uart1.pRxBuf);
/* 打印DMA缓冲满 */
LOG_RTT_INFO("DMA data full !");
}
/*
*******************************************************************************************
* 函 数 名: DMA_TransferError_Callback
* 功能说明: DMA传输错误回调函数
* 形 参:无
* 返 回 值: 无
*******************************************************************************************
*/
void DMA_TransferError_Callback(void)
{
/* 打印传输错误日志 */
LOG_RTT_INFO("DMA data transfer failed !");
}
并将两个中断函数放在CubeMX生成的中断函数中,在中断函数中清除中断标志位后再进入回调函数中。
void DMA1_Channel5_IRQHandler(void)
{
/* USER CODE BEGIN DMA1_Channel5_IRQn 0 */
if(LL_DMA_IsActiveFlag_TC5(DMA1))
{
LL_DMA_ClearFlag_TC5(DMA1);
DMA_ReceiveComplete_Callback();
}
else if(LL_DMA_IsActiveFlag_TE5(DMA1))
{
LL_DMA_ClearFlag_GI5(DMA1);
DMA_TransferError_Callback();
}
/* USER CODE END DMA1_Channel5_IRQn 0 */
/* USER CODE BEGIN DMA1_Channel5_IRQn 1 */
/* USER CODE END DMA1_Channel5_IRQn 1 */
}
最后我们在主函数中添加如下代码测试收发,注意在操作标志位时需要关闭全局中断,操作完毕后再打开,防止出现中断和主函数同时写FLG变量。
if (dat_Uart1.FLG)
{
/* 日志打印收到的数据 */
LOG_RTT_INFO("Receive:%s", dat_Uart1.pRxBuf);
/* 将收到的数据返回 */
UartSend(dat_Uart1.pRxBuf, dat_Uart1.LEN);
DISABLE_INT();
dat_Uart1.FLG = 0;
ENABLE_INT();
}
下载到板子上,测试效果如下动图所示。
如果对你有帮助记得点个赞点个关注。