本帖最后由 口天土立口 于 2026-2-1 19:42 编辑
#技术资源# #申请原创#
@21小跑堂
1. 外设介绍
G32M3101支持有2个串口,最大通讯速率为2.5Mbit/s。实际在工业场景中,直接使用串口的TTL电平,通讯距离受限,因此串口搭配485电路是常用的使用场景,也因考虑到抗干扰问题,常用的通讯波特率一般为115200和9600。
2. 硬件 G32M3101K8T6
3. 驱动介绍 以下驱动分为三种方式:轮询、中断和DMA,三种方式均已实现接收不定长串口数据。
串口轮询通讯方式的初始化较为简单,初始化对应的IO和配置串口的通讯参数即可。 - /*
- * @brief 初始化
- *
- * @param None
- *
- * @retval None
- *
- */
- void bsp_uart_init(void)
- {
- DDL_GPIO_InitTypeDef GPIO_InitStruct;
- DDL_USART_InitTypeDef USART_InitStruct;
-
- /* 使能时钟 */
- DDL_SCU_Unlock();
- DDL_SCU_EnableAHBPeripheralClock(DDL_SCU_AHB_PERIPHERAL_GPIOA);
- DDL_SCU_EnableAPBPeripheralClock(DDL_SCU_APB_PERIPHERAL_UART0);
- DDL_SCU_Lock();
-
- /* 配置GPIO */
- /* PA7 -> UART0_TX
- * PA8 -> UART0_RX
- */
- DDL_GPIO_StructInit(&GPIO_InitStruct);
- GPIO_InitStruct.Pin = DDL_GPIO_PIN_7 | DDL_GPIO_PIN_8;
- GPIO_InitStruct.Mode = DDL_GPIO_MODE_ALTERNATE;
- GPIO_InitStruct.Drive = DDL_GPIO_DRIVE_HIGH;
- GPIO_InitStruct.OutputType = DDL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = DDL_GPIO_PULL_NO;
- GPIO_InitStruct.Alternate = DDL_GPIO_AF_0;
- DDL_GPIO_LockKey(GPIOA, DDL_GPIO_LOCK_DISABLE);
- DDL_GPIO_Init(GPIOA, &GPIO_InitStruct);
- DDL_GPIO_SetPinInputMode(GPIOA, DDL_GPIO_PIN_8, DDL_GPIO_INPUT_ENABLE);
- DDL_GPIO_LockKey(GPIOA, DDL_GPIO_LOCK_ENABLE);
-
- /* USART */
- DDL_USART_DeInit(UART_INS);
- DDL_USART_StructInit(&USART_InitStruct);
- USART_InitStruct.BaudRate = 115200;
- USART_InitStruct.DataWidth = DDL_USART_DATAWIDTH_8B;
- USART_InitStruct.StopBits = DDL_USART_STOPBITS_1;
- USART_InitStruct.Parity = DDL_USART_PARITY_NONE ;
- USART_InitStruct.TransferDirection = DDL_USART_DIRECTION_TX_RX;
- DDL_USART_Init(UART_INS, &USART_InitStruct);
- DDL_USART_Enable(UART_INS);
- }
轮询方式的发送数据,基于发送为空标志位,不断的查询,发送为空即将新的数据写入串口数据寄存器即可,待全部数据都传入完成,等待发送完成标志置位,即表示所有数据发送完成。 - /*
- * @brief 发送数据
- *
- * @param buf: 数据缓存
- * buf_len: 缓存大小
- *
- * @retval None
- *
- */
- void bsp_uart_send(uint8_t *buf, uint16_t buf_len)
- {
- uint16_t i = 0;
-
- DDL_USART_ClearFlag_TC(UART_INS);
- while (i < buf_len) {
- while (DDL_USART_IsActiveFlag_TXE(UART_INS) == 0);
- DDL_USART_TransmitData8(UART_INS, buf[i++]);
- }
- while (DDL_USART_IsActiveFlag_TC(UART_INS) == 0);
- }
接收数据通过检查接收非空标志判断,接收非空标志置位,则表示有新数据抵达数据寄存器,从数据寄存器取走数据后,标志位将自动清零。而判断一串完整的报文数据接收是否完整,是通过接收空闲标志确定,接收空闲标志为在一定数量的空闲位后依然无新数据到来,则置位接收空闲标志,同时也意味着连续的数据传输已经结束。空闲标志是实现不定长数据接收的关键。 - /*
- * @brief 接收数据
- *
- * @param None
- *
- * @retval None
- *
- */
- void bsp_uart_recv(void)
- {
- /* 接收非空 */
- if (DDL_USART_IsActiveFlag_RXNE(UART_INS) != 0) {
- if (rx_len < sizeof(rx_buf)) {
- rx_buf[rx_len++] = DDL_USART_ReceiveData8(UART_INS);
- }
- }
- /* 接收空闲则表示一帧报文结束 */
- if (DDL_USART_IsActiveFlag_IDLE(UART_INS) != 0) {
- rx_complete = 1;
- DDL_USART_ClearFlag_IDLE(UART_INS);
- }
- }
中断方式的串口通讯初始化,只比轮询方式多使能接收非空中断和空闲中断,同时需要使能对应的串口中断号。 注意:发送的相关中断需在要发送数据时,才能使能,否则立刻使能发送为空中断,将导致程序立刻进入中断。 - /*
- * @brief 初始化
- *
- * @param None
- *
- * @retval None
- *
- */
- void bsp_uart_init(void)
- {
- DDL_GPIO_InitTypeDef GPIO_InitStruct;
- DDL_USART_InitTypeDef USART_InitStruct;
-
- /* 使能时钟 */
- DDL_SCU_Unlock();
- DDL_SCU_EnableAHBPeripheralClock(DDL_SCU_AHB_PERIPHERAL_GPIOA);
- DDL_SCU_EnableAPBPeripheralClock(DDL_SCU_APB_PERIPHERAL_UART0);
- DDL_SCU_Lock();
-
- /* 使能中断 */
- NVIC_SetPriority(UART0_IRQn, 0);
- NVIC_EnableIRQ(UART0_IRQn);
-
- /* 配置GPIO */
- /* PA7 -> UART0_TX
- * PA8 -> UART0_RX
- */
- DDL_GPIO_StructInit(&GPIO_InitStruct);
- GPIO_InitStruct.Pin = DDL_GPIO_PIN_7 | DDL_GPIO_PIN_8;
- GPIO_InitStruct.Mode = DDL_GPIO_MODE_ALTERNATE;
- GPIO_InitStruct.Drive = DDL_GPIO_DRIVE_HIGH;
- GPIO_InitStruct.OutputType = DDL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = DDL_GPIO_PULL_NO;
- GPIO_InitStruct.Alternate = DDL_GPIO_AF_0;
- DDL_GPIO_LockKey(GPIOA, DDL_GPIO_LOCK_DISABLE);
- DDL_GPIO_Init(GPIOA, &GPIO_InitStruct);
- DDL_GPIO_SetPinInputMode(GPIOA, DDL_GPIO_PIN_8, DDL_GPIO_INPUT_ENABLE);
- DDL_GPIO_LockKey(GPIOA, DDL_GPIO_LOCK_ENABLE);
-
- /* USART */
- DDL_USART_DeInit(UART_INS);
- DDL_USART_StructInit(&USART_InitStruct);
- USART_InitStruct.BaudRate = 115200;
- USART_InitStruct.DataWidth = DDL_USART_DATAWIDTH_8B;
- USART_InitStruct.StopBits = DDL_USART_STOPBITS_1;
- USART_InitStruct.Parity = DDL_USART_PARITY_NONE ;
- USART_InitStruct.TransferDirection = DDL_USART_DIRECTION_TX_RX;
- DDL_USART_Init(UART_INS, &USART_InitStruct);
- /* 关闭发送为空中断 */
- DDL_USART_DisableIT_TXE(UART_INS);
- /* 使能接收非空中断 */
- DDL_USART_EnableIT_RXNE(UART_INS);
- /* 使能空闲中断 */
- DDL_USART_EnableIT_IDLE(UART_INS);
- /* 使能串口 */
- DDL_USART_Enable(UART_INS);
- bsp_uart_reset_recv();
- }
中断方式的串口发送数据,初始化完发送的基本信息后,使能发送为空中断即可,后续工作将进入中断进行处理。 - /*
- * @brief 发送数据
- *
- * @param buf: 数据缓存
- * buf_len: 缓存大小
- *
- * @retval None
- *
- */
- void bsp_uart_send(uint8_t *buf, uint16_t buf_len)
- {
- if ((buf != 0) && (buf_len > 0)) {
- tx_buf = buf;
- tx_index = 0;
- tx_len = buf_len;
- /* 开启发送为空中断 */
- DDL_USART_EnableIT_TXE(UART_INS);
- }
- }
串口中断处理函数内部,执行串口的发送和接收工作。发送为空中断,则将待发送数据不断传入数据寄存器,数据传输完毕后,立刻关闭发送为空中断; 串口中断接收数据与轮询方式的接收类似,只是工作改为在中断内部执行,无需主循环代码查询。 - /*
- * @brief 串口中断
- *
- * @param None
- *
- * @retval None
- *
- */
- void UART0_IRQHandler(void)
- {
- /* 发送为空 */
- if (DDL_USART_IsActiveFlag_TXE(UART_INS) != 0) {
- if (tx_index < tx_len) {
- /* 发数据 */
- DDL_USART_TransmitData8(UART_INS, tx_buf[tx_index++]);
- } else {
- /* 关闭发送为空中断 */
- DDL_USART_DisableIT_TXE(UART_INS);
- }
- }
- /* 接收非空 */
- if (DDL_USART_IsActiveFlag_RXNE(UART_INS) != 0) {
- if (rx_len < sizeof(rx_buf)) {
- rx_buf[rx_len++] = DDL_USART_ReceiveData8(UART_INS);
- }
- }
- /* 空闲 */
- if (DDL_USART_IsActiveFlag_IDLE(UART_INS) != 0) {
- /* 清除空闲状态 */
- DDL_USART_ClearFlag_IDLE(UART_INS);
- rx_complete = 1;
- }
- }
串口的DMA通讯方式,初始化代码相对较为复杂,但此方式却是最为推荐的通讯方式。 初始化工作相对前面的两种方式,需要分别为串口的接收和发送都初始化一个DMA通道,因为串口接收的数据量没法确定,所以初始化串口接收的DMA传输数据量固定配置为接收的最大值,也因此串口不能单次接收超过此值的数据量;而串口的发送DMA配置无法确定传输的数据量和传输的存储地址,需等到发送数据时才能确认。 - /*
- * @brief 初始化
- *
- * @param None
- *
- * @retval None
- *
- */
- void bsp_uart_init(void)
- {
- DDL_GPIO_InitTypeDef GPIO_InitStruct;
- DDL_USART_InitTypeDef USART_InitStruct;
- DDL_DMA_InitTypeDef DMA_InitStruct;
-
- /* 使能时钟 */
- DDL_SCU_Unlock();
- DDL_SCU_EnableAHBPeripheralClock(DDL_SCU_AHB_PERIPHERAL_GPIOA);
- DDL_SCU_EnableAHBPeripheralClock(DDL_SCU_AHB_PERIPHERAL_DMA1);
- DDL_SCU_EnableAPBPeripheralClock(DDL_SCU_APB_PERIPHERAL_UART0);
- DDL_SCU_Lock();
-
- /* 配置GPIO */
- /* PA7 -> UART0_TX
- * PA8 -> UART0_RX
- */
- DDL_GPIO_StructInit(&GPIO_InitStruct);
- GPIO_InitStruct.Pin = DDL_GPIO_PIN_7 | DDL_GPIO_PIN_8;
- GPIO_InitStruct.Mode = DDL_GPIO_MODE_ALTERNATE;
- GPIO_InitStruct.Drive = DDL_GPIO_DRIVE_HIGH;
- GPIO_InitStruct.OutputType = DDL_GPIO_OUTPUT_PUSHPULL;
- GPIO_InitStruct.Pull = DDL_GPIO_PULL_NO;
- GPIO_InitStruct.Alternate = DDL_GPIO_AF_0;
- DDL_GPIO_LockKey(GPIOA, DDL_GPIO_LOCK_DISABLE);
- DDL_GPIO_Init(GPIOA, &GPIO_InitStruct);
- DDL_GPIO_SetPinInputMode(GPIOA, DDL_GPIO_PIN_8, DDL_GPIO_INPUT_ENABLE);
- DDL_GPIO_LockKey(GPIOA, DDL_GPIO_LOCK_ENABLE);
-
- /* DMA */
- DDL_DMA_StructInit(&DMA_InitStruct);
- /* DMA -> USART_RX */
- DMA_InitStruct.PeriphOrM2MSrcAddress = (uint32_t)&UART_INS->DATA;
- DMA_InitStruct.MemoryOrM2MDstAddress = (uint32_t)rx_buf;
- DMA_InitStruct.Direction = DDL_DMA_DIRECTION_PERIPH_TO_MEMORY;
- DMA_InitStruct.Mode = DDL_DMA_MODE_NORMAL;
- DMA_InitStruct.PeriphOrM2MSrcIncMode = DDL_DMA_PERIPH_NOINCREMENT;
- DMA_InitStruct.MemoryOrM2MDstIncMode = DDL_DMA_MEMORY_INCREMENT;
- DMA_InitStruct.PeriphOrM2MSrcDataSize = DDL_DMA_PDATAALIGN_BYTE;
- DMA_InitStruct.MemoryOrM2MDstDataSize = DDL_DMA_MDATAALIGN_BYTE;
- DMA_InitStruct.NbData = sizeof(rx_buf);
- DMA_InitStruct.Peripheral = DDL_DMA_PERIPHERAL_5;
- DMA_InitStruct.Priority = DDL_DMA_PRIORITY_LOW;
- DMA_InitStruct.FIFOMode = DDL_DMA_FIFOMODE_DISABLE;
- DMA_InitStruct.FIFOThreshold = DDL_DMA_FIFOTHRESHOLD_1_4;
- DMA_InitStruct.MemBurst = DDL_DMA_MBURST_SINGLE;
- DMA_InitStruct.PeriphBurst = DDL_DMA_PBURST_SINGLE;
- DDL_DMA_Init(DMA1, DDL_DMA_CHANNEL_1, &DMA_InitStruct);
- /* 使能传输完成中断 */
- DDL_DMA_EnableIT_TC(DMA1, DDL_DMA_CHANNEL_1);
- /* 使能中断 */
- NVIC_SetPriority(DMA_Channel1_IRQn, 0);
- NVIC_EnableIRQ(DMA_Channel1_IRQn);
-
- /* DMA -> USART_TX */
- DMA_InitStruct.PeriphOrM2MSrcAddress = (uint32_t)&UART_INS->DATA;
- DMA_InitStruct.MemoryOrM2MDstAddress = 0;
- DMA_InitStruct.Direction = DDL_DMA_DIRECTION_MEMORY_TO_PERIPH;
- DMA_InitStruct.Mode = DDL_DMA_MODE_NORMAL;
- DMA_InitStruct.PeriphOrM2MSrcIncMode = DDL_DMA_PERIPH_NOINCREMENT;
- DMA_InitStruct.MemoryOrM2MDstIncMode = DDL_DMA_MEMORY_INCREMENT;
- DMA_InitStruct.PeriphOrM2MSrcDataSize = DDL_DMA_PDATAALIGN_BYTE;
- DMA_InitStruct.MemoryOrM2MDstDataSize = DDL_DMA_MDATAALIGN_BYTE;
- DMA_InitStruct.NbData = 0;
- DMA_InitStruct.Peripheral = DDL_DMA_PERIPHERAL_5;
- DMA_InitStruct.Priority = DDL_DMA_PRIORITY_LOW;
- DMA_InitStruct.FIFOMode = DDL_DMA_FIFOMODE_DISABLE;
- DMA_InitStruct.FIFOThreshold = DDL_DMA_FIFOTHRESHOLD_1_4;
- DMA_InitStruct.MemBurst = DDL_DMA_MBURST_SINGLE;
- DMA_InitStruct.PeriphBurst = DDL_DMA_PBURST_SINGLE;
- DDL_DMA_Init(DMA1, DDL_DMA_CHANNEL_0, &DMA_InitStruct);
-
- /* USART */
- DDL_USART_DeInit(UART_INS);
- DDL_USART_StructInit(&USART_InitStruct);
- USART_InitStruct.BaudRate = 115200;
- USART_InitStruct.DataWidth = DDL_USART_DATAWIDTH_8B;
- USART_InitStruct.StopBits = DDL_USART_STOPBITS_1;
- USART_InitStruct.Parity = DDL_USART_PARITY_NONE ;
- USART_InitStruct.TransferDirection = DDL_USART_DIRECTION_TX_RX;
- DDL_USART_Init(UART_INS, &USART_InitStruct);
- /* 使能DMA收发 */
- DDL_USART_EnableDMAReq_RX(UART_INS);
- DDL_USART_EnableDMAReq_TX(UART_INS);
- /* 使能空闲中断 */
- DDL_USART_EnableIT_IDLE(UART_INS);
- /* 使能发送完成中断 */
- DDL_USART_EnableIT_TC(UART_INS);
- /* 使能中断 */
- NVIC_SetPriority(UART0_IRQn, 0);
- NVIC_EnableIRQ(UART0_IRQn);
- /* 使能串口 */
- DDL_USART_Enable(UART_INS);
- /* 接收数据 */
- bsp_uart_recv();
- }
DMA的通讯方式,判断接收完整一串报文数据,也是通过空闲标志位确定,而确定具体已经接收了多少数据量,则是通过计算当前DMA接收通道的剩余可传输数据量来反推得出,处理完毕后,需重新开启接受的DMA通道,并配置为最大的传输数据量,以确保后续新来的数据量能完整接收。 DMA的发送数据,通过发送完成中断确定全部数据发送完毕。 - /*
- * @brief 串口中断
- *
- * @param None
- *
- * @retval None
- *
- */
- void UART0_IRQHandler(void)
- {
- uint16_t num = 0;
-
- /* 空闲 */
- if (DDL_USART_IsActiveFlag_IDLE(UART_INS) != 0) {
- /* 清除空闲状态 */
- DDL_USART_ClearFlag_IDLE(UART_INS);
- /* 计算已经传输的数据量 */
- num = sizeof(rx_buf) - DDL_DMA_GetDataLength(DMA1, DDL_DMA_CHANNEL_1);
- if (num != 0) {
- rx_len = num;
- rx_complete = 1;
- }
- bsp_uart_recv();
- }
- /* 发送完成中断 */
- if (DDL_USART_IsActiveFlag_TC(UART_INS) != 0) {
- /* 清除空闲状态 */
- DDL_USART_ClearFlag_TC(UART_INS);
- }
- }
另外,DMA接收的数据流完成中断也需要特意处理,处理方式与串口的空闲中断标志一致,即重新开启接收。 - /*
- * @brief DMA中断
- *
- * @param None
- *
- * @retval None
- *
- */
- void DMA_Channel0_IRQHandler(void)
- {
- uint16_t num = 0;
-
- /* USART RX */
- if (DDL_DMA_IsActiveFlag_TC1(DMA1) != 0) {
- DDL_DMA_ClearFlag_TC1(DMA1);
- /* 计算已经传输的数据量 */
- num = sizeof(rx_buf) - DDL_DMA_GetDataLength(DMA1, DDL_DMA_CHANNEL_1);
- if (num != 0) {
- rx_len = num;
- rx_complete = 1;
- }
- bsp_uart_recv();
- }
- }
重新开启接收,需先关闭DMA的通道使能,重新配置为接收最大传输数据量后,再使能串口接收的DMA通道。 - /*
- * @brief 接收数据
- *
- * @param None
- *
- * @retval None
- *
- */
- void bsp_uart_recv(void)
- {
- DDL_DMA_DisableChannel(DMA1, DDL_DMA_CHANNEL_1);
- DDL_DMA_SetDataLength(DMA1, DDL_DMA_CHANNEL_1, sizeof(rx_buf));
- DDL_DMA_EnableChannel(DMA1, DDL_DMA_CHANNEL_1);
- }
串口的DMA发送需每次开启发送前都配置新的数据存储地址和数据数量,因为这些信息在应用层中,不同的处理逻辑传入的值都是不同的。 - /*
- * @brief 发送数据
- *
- * @param buf: 数据缓存
- * buf_len: 缓存大小
- *
- * @retval None
- *
- */
- void bsp_uart_send(uint8_t *buf, uint16_t buf_len)
- {
- if ((buf != 0) && (buf_len > 0)) {
- DDL_DMA_DisableChannel(DMA1, DDL_DMA_CHANNEL_0);
- DDL_DMA_SetDataLength(DMA1, DDL_DMA_CHANNEL_0, buf_len);
- DDL_DMA_SetMemoryAddress(DMA1, DDL_DMA_CHANNEL_0, (uint32_t)buf);
- DDL_USART_ClearFlag_TC(UART_INS);
- DDL_DMA_EnableChannel(DMA1, DDL_DMA_CHANNEL_0);
- }
- }
优劣势: 轮询方式:代码简单,利于理解,但需要及时查询状态,否则溢出,且发数据期间严重占用内核; 中断方式:代码简单,利于理解,收发数据期间可释放内核,但频繁中断,占用内核,且大量其他中断存在的场景可能影响数据收发的连续性; DMA方式:收发数据不占用内核,且中断产生较少,收发数据能保证连续性,但初始化代码复杂; 从使用经验上推荐,最推荐的是DMA方式,而中断方式在大量中断的场景下要保证收发数据的绝对连续性,需要把串口的中断优先级提升到非常高的位置,至于轮询方式,只有非常简单的应用场景才能考虑使用,一般不建议使用。
4. 测试 本次测试的目的是将接收到的数据再发出来,如下代码为轮询方式,相当简洁,而中断和DMA方式,则连bsp_uart_recv()函数都不需要调用。 - // 外设初始化
- void bsp_init(void)
- {
- bsp_uart_init();
- }
- // 应用任务
- void app_task(void)
- {
- /* 接收数据 */
- bsp_uart_recv();
- /* 接收完成再发出来 */
- if (bsp_uart_is_rx_complete() != 0) {
- bsp_uart_send(bsp_uart_get_rx_buf(), bsp_uart_get_rx_len());
- bsp_uart_reset_recv();
- }
- }
如下图的测试结果:串口完整接收到数据后,只间隔不到100us就立刻发出。
5. 驱动移植说明 驱动的移植只需要修改bsp_uart_init函数即可,改为对应的IO和串口外设号,同时DMA的通道需对应手册修改。
6.详细代码
DMA.zip
(2.53 MB, 下载次数: 0)
|