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。
对应串口的配置程序如下:
//串口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;//复用串口1std_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,具体的解释如下:
/**
* @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通道,具体示例如下:
//使用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的接收缓冲区,将其余的数据转存至到备份区域,这就避免了传输过程中造成的数据丢失问题。
此方式花费了原本双倍的内存换来了更高性能的传输,也避免了传输过程中的数据丢失问题。
具体配置如下:
/**
* @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的RXdma_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;//一次传输1bytedma_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对应的中断服务函数如下:
/*-------------------------------------------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对应的无阻塞性收发初始化内容具体如下:
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实现的串口无阻塞性收发已经实现。