1.CIU32L051 DMA的通道映射
由于华大CIU32L051的DMA外设资源有限,DMA只有两个通道可供使用,对应的通道映射图如下:

2.UART对应的引脚分布及其复用映射
CIU32L051对应的UART对应的引脚映射图如下,这里博主为了各位方便查找,就直接全拿进来了:




3.USART1作为无阻塞性收发的串口
根据第二章的图片可以看到,串口1对应的IO口为PA1,PA2,PA11,PA12等等,这里为了方便,博主直接拿usart的例程中的PA11,PA12分别作为USART1_TX,USART1_RX。
对应串口的配置程序如下:
cs
//串口1 GPIO的配置
static void UART1_GPIO_Configure(void)
{
/* GPIO外设时钟使能 */
std_rcc_gpio_clk_enable(RCC_PERIPH_CLK_GPIOA);
std_gpio_init_t usart_gpio_init={0};
/* GPIO引脚配置
PA12 ------> RX
PA11 ------> TX
*/
usart_gpio_init.pin = GPIO_PIN_11|GPIO_PIN_12;
usart_gpio_init.mode = GPIO_MODE_ALTERNATE;//io复用模式
usart_gpio_init.output_type = GPIO_OUTPUT_PUSHPULL;//复用推挽输出
usart_gpio_init.pull = GPIO_PULLUP;//上拉输入
usart_gpio_init.alternate = GPIO_AF1_USART1;//复用串口1
std_gpio_init(GPIOA, &usart_gpio_init);
NVIC_SetPriority(USART1_IRQn, 0);//这里配置串口1的中断配置器,这里可以不需要使用
NVIC_EnableIRQ(USART1_IRQn);
}
//串口1 结构体的配置
static void usart1_init(uint32_t baud)
{
/* USART1时钟使能 */
std_rcc_apb2_clk_enable(RCC_PERIPH_CLK_USART1);
std_usart_init_t usart_init={0};
usart_init.direction = USART_DIRECTION_SEND_RECEIVE;//这里是使能了串口的发送和接收
usart_init.baudrate = baud;//波特率
usart_init.wordlength = USART_WORDLENGTH_8BITS;//数据长度
usart_init.stopbits = USART_STOPBITS_1;//停止位
usart_init.parity = USART_PARITY_NONE;//奇偶校验
usart_init.hardware_flow = USART_FLOWCONTROL_NONE;//无硬件流使能
/* USART初始化 */
if(STD_OK != std_usart_init(USART1,&usart_init))
{
/* 波特率配置不正确处理代码 */
while(1);
}
std_usart_enable(USART1);
}
4.串口1无阻塞性发送的实现及代码实现
根据第一章的内容可以得知,串口1的TX和RX分别对应的是DMA的通道1和通道0。具体如下:

4.1配置串口1 DMA无阻塞性发送的实现
配置代码具体如下:
此处有配置DMA的传输模式为DMA_BLOCK_TRANSFER,具体的解释如下:

cs
/**
* @brief DMA通道1初始化
* @retval 无
*/
static void dma_init(void)
{
std_dma_init_t dma_init_param={0};
/* DMA外设时钟使能 */
std_rcc_ahb_clk_enable(RCC_PERIPH_CLK_DMA);
/* dma_init_param 结构体初始化 */
dma_init_param.dma_channel = DMA_CHANNEL_1;
dma_init_param.dma_req_id = DMA_REQUEST_USART1_TX;//这里是指DMA请求的触发条件
dma_init_param.transfer_type = DMA_BLOCK_TRANSFER;//配置DMA 的传输模式
dma_init_param.src_addr_inc = DMA_SRC_INC_ENABLE;//使能源地址递增
dma_init_param.dst_addr_inc = DMA_DST_INC_DISABLE;//DMA目的地址自增使能或禁止
dma_init_param.data_size = DMA_DATA_SIZE_BYTE;//DMA传输数据宽度,字节、半字或字
dma_init_param.mode = DMA_MODE_NORMAL;//DMA工作模式为单次传输
/* DMA初始化 */
std_dma_init(&dma_init_param);
/* 使能传输完成中断 */
std_dma_interrupt_enable(dma_init_param.dma_channel,DMA_INTERRUPT_TF);//这里使能的传输完成中断,当DMA将对应目标的数据搬运完成后会将传输完成的标记置为1
/* NVIC初始化 */
NVIC_SetPriority(DMA_Channel1_IRQn, 0);
NVIC_EnableIRQ(DMA_Channel1_IRQn);
}
/**
* @brief DMA配置函数 目的是更新DMA传输的数据源和目标地址
* @param source DMA 传输源地址 指的是发送缓冲区的地址
* @param number DMA 传输字符数 这个参数就是指的是你发送缓冲区对应的大小
* @retval 无
*/
void bsp_usart_dma_config(uint8_t *source,uint32_t number)
{
std_dma_config_t dma_config = {0};
/* 配置DMA 源地址、目的地址和传输数据大小,并使能DMA */
dma_config.src_addr = (uint32_t)source;
dma_config.dst_addr = (uint32_t)&USART1->TDR;//目标地址为串口1的发送数据寄存器
dma_config.data_number = number;
dma_config.dma_channel = DMA_CHANNEL_1;
std_dma_start_transmit(&dma_config);
}
void En_Dma_Channel(void)//这里在进行对DMA通道的使能
{
std_dma_enable(DMA_CHANNEL_1);
}
void Dis_Dma_Channel(void)//这里在进行对DMA通道的失能
{
std_dma_disable(DMA_CHANNEL_1);
}
上述代码中,DMA的通道1触发传输的条件是触发了串口1的发送,即当发送缓冲区的数据非空时,DMA将会把发送缓冲区的数据搬运至串口1的发送数据寄存器中,串口则会通过发送数据寄存器将数据转发。
由于,DMA的工作模式设置的是单次传输的模式,由此当DMA传输第一次完成后,需要对对DMA的数据源和目的地址进行更新,并重新使能对应的DMA通道,具体示例如下:
cs
//使用DMA通道1 实现串口非阻塞性发送 这个函数的构造,就相当于是无阻塞性的UART_SENDDATA("DEBUG",strlen("DEBUG"))这种发送函数,只不过原本串口的发送函数是阻塞性的
void UART1_DMA_Send_Buf(uint8_t *buf,size_t len)
{
Dis_Dma_Channel();//更新数据源前需要先关闭DMA对应的通道
bsp_usart_dma_config(buf,len);//此函数则是更新了DMA对应通道的数据源,此函数在之前的配置代码中有对应的详细内容
std_dma_interrupt_enable(DMA_CHANNEL_1,DMA_INTERRUPT_TF); //这里开启对应的传输完成中断,若是对应的中断服务函数中没有关闭传输完成中断,此处内容可以省略
En_Dma_Channel();//重新开启DMA对应的通道
}
//DMA通道1对应的中断服务函数
/*-------------------------------------------functions------------------------------------------*/
/**
* @brief DMA通道中断服务函数
* @retval 无
*/
//传输完成中断
void DMA_Channel1_IRQHandler(void)
{
if(std_dma_get_flag(DMA_FLAG_TF1))
{
std_dma_interrupt_disable(DMA_CHANNEL_1,DMA_INTERRUPT_TF); //这个可以不用关闭
SEGGER_RTT_printf(0, "DMA DATA SEND FINISH\r\n");//这就是RRT的输出的一个调试信息
std_dma_clear_flag(DMA_FLAG_TF1);//清除传输完成的标记
}
}
上述内容是对应的串口 DMA无阻塞性的发送的实现。
4.2串口1 无阻塞性接收的实现
无阻塞性接收数据的实现,需要用到DMA双缓冲区备份。为什么要用双缓冲区备份呢?
理由如下:
由于串口接收到的数据是不定长的数据,使用的DMA传输数据时容易造成可能由于数据过长导致缓冲区溢出的情况,以及在接收完成数据后由于数据处理花费的时间导致数据丢失。
DMA提供了一个传输完成一半的中断提示,由此可以通过数据传输完成一半的时,在中断服务函数中更换DMA的接收缓冲区,将其余的数据转存至到备份区域,这就避免了传输过程中造成的数据丢失问题。
此方式花费了原本双倍的内存换来了更高性能的传输,也避免了传输过程中的数据丢失问题。
具体配置如下:
cs
/**
* @brief DMA配置函数 目的是更新串口1RX对应DMA的数据源和目的地址
* @param distination DMA传输目的地址
* @param number DMA传输字符数
* @retval 无
*/
void USART1_DMA_RX_config(uint8_t *distination,uint32_t number)
{
std_dma_config_t dma_config = {0};
if(distination == buf_hu1)//此处内容用于中断服务函数中更新对应的缓冲区
{
Set_USART1_Buf_Flag();
}
else if(distination == buf_hu2)
{
Clear_USART1_Buf_Flag();
}
/* 配置DMA 源地址、目的地址和传输数据大小,并使能DMA */
dma_config.src_addr = (uint32_t)&USART1->RDR;//串口数据接收寄存器
dma_config.dst_addr = (uint32_t)distination;
dma_config.data_number = number;
dma_config.dma_channel = DMA_CHANNEL_0;
std_dma_start_transmit(&dma_config);
}
/**
* @brief DMA通道0初始化
* @retval 无
*/
void UART1_Dma_RX_init(void)
{
std_dma_init_t dma_init_param = {0};
/* DMA外设时钟使能 */
std_rcc_ahb_clk_enable(RCC_PERIPH_CLK_DMA);
/* dma_init_param 结构体初始化 */
dma_init_param.dma_channel = DMA_CHANNEL_0;//通道0对应串口1的RX
dma_init_param.dma_req_id = DMA_REQUEST_USART1_RX;//dma请求是串口1的接收,意思是当串口1 的RDR寄存器不为空时将RDR寄存器的数据搬运置,缓冲区中
dma_init_param.transfer_type = DMA_BLOCK_TRANSFER;
dma_init_param.src_addr_inc = DMA_SRC_INC_DISABLE;//使能数据源地址递增
dma_init_param.dst_addr_inc = DMA_DST_INC_ENABLE;//使能目标地址递增
dma_init_param.data_size = DMA_DATA_SIZE_BYTE;//一次传输1byte
dma_init_param.mode = DMA_MODE_CIRCULAR;//循环接收,不需要对DMA进行重新配置
/* DMA初始化 */
std_dma_init(&dma_init_param);
USART1_DMA_RX_config(buf_hu1,256);//这设置了串口1对应DMA通道0的缓冲区
/* 使能传输完成中断 */
std_dma_interrupt_enable(DMA_CHANNEL_0,DMA_INTERRUPT_TF);//使能dma通道0对应的接收完成中断
std_dma_interrupt_enable(DMA_CHANNEL_0,DMA_INTERRUPT_TH); //使能dma通道0对应的接收一半中断
/* NVIC初始化 */
NVIC_SetPriority(DMA_Channel0_IRQn, 0);
NVIC_EnableIRQ(DMA_Channel0_IRQn);
}
void Set_USART1_Buf_Flag(void)
{
ATdevs.uart1_buf_Flag = 1;
}
uint8_t Get_USART1_Buf_Flag(void)
{
return ATdevs.uart1_buf_Flag;
}
void Clear_USART1_Buf_Flag(void)
{
ATdevs.uart1_buf_Flag = 0;
}
/*
DMA 双缓冲区实现串口DMA的无阻塞性接收数据
配合DMA的传输一半的中断进行使用
当触发传输一半的中断后,此时需要更换对应的缓冲区,再下次传输一半数据前,是我们处理原本数据的有效时间
要求是MCU的处理速度要比DMA的传输速度要快一半
*/
void DMA_USART_RX_CallBlack(void)
{
std_dma_disable(DMA_CHANNEL_0);
if(Get_USART1_Buf_Flag())
{
memset(buf_hu2,0,USART1_BUF_SIZE);//更换缓冲区前清除需要更换的缓冲区数据
USART1_DMA_RX_config(buf_hu2,USART1_BUF_SIZE);//更新数据源
SEGGER_RTT_printf(0,"dma buf is modify buf_hu2 success\r\n");
}
else
{
memset(buf_hu1,0,USART1_BUF_SIZE);
USART1_DMA_RX_config(buf_hu1,USART1_BUF_SIZE);
SEGGER_RTT_printf(0,"dma buf is modify buf_hu1 success\r\n");
}
std_dma_enable(DMA_CHANNEL_0);
}
上述内容是配置了USART1_RX对应的DMA通道,以及更换数据源的实现。
关于DMA通道0对应的中断服务函数如下:
cs
/*-------------------------------------------functions------------------------------------------*/
/**
* @brief DMA通道中断服务函数
* @retval 无
*/
//传输完成中断
void DMA_Channel0_IRQHandler(void)
{
if(std_dma_get_flag(DMA_FLAG_TF0))
{
std_dma_clear_flag(DMA_FLAG_TF0);
}
if(std_dma_get_flag(DMA_INTERRUPT_TH))//DMA传输一半完成的中断
{
std_dma_clear_flag(DMA_INTERRUPT_TH);
DMA_USART_RX_CallBlack();//传输一半后更换目的地址
}
}
USART1对应的无阻塞性收发初始化内容具体如下:
cs
void Configure_UART1_DMA0_TX_AND_RX(void)
{
dma_init();//这是DMA结构体相关的初始化
UART1_Dma_RX_init();//此处是关于DMA实现的无阻塞性接收的配置
UART1_GPIO_Configure();
usart1_init(115200);
std_usart_dma_tx_enable(USART1);//这里必须要使能
std_usart_dma_rx_enable(USART1);//这里必须要使能
}
5.测试效果
测试环境:
RT-thread NANO系统下,开辟了一个周期为2秒的周期定时器,在周期定时器的回调中进行无阻塞性的发送。
另外,通过STM32F103的最小系统板一直给CIU32L051发送数据。
效果图如下:


由此可见,基于DMA实现的串口无阻塞性收发已经实现。