[APM32F4] APM32F427的UART驱动(轮询、中断和DMA均为接收不定长数据)

[复制链接]
88|0
口天土立口 发表于 2025-11-18 19:32 | 显示全部楼层 |阅读模式
本帖最后由 口天土立口 于 2025-11-18 23:04 编辑

#申请原创# #技术资源#

@21小跑堂

1. 外设介绍

12286691c576738d6e.png
APM32F427支持多达6个串口,最大通讯速率为10.5Mbit/s。实际在工业场景中,直接使用串口的TTL电平,通讯距离受限,因此串口搭配485电路是常用的使用场景,也因考虑到抗干扰问题,常用的通讯波特率一般为1152009600

2. 硬件
APM32F427ZG TINY

3. 驱动介绍
以下驱动分为三种方式:轮询、中断和DMA,三种方式均已实现接收不定长串口数据。

串口轮询通讯方式的初始化较为简单,初始化对应的IO和配置串口的通讯参数即可。
  1. /*
  2. * @brief       初始化
  3. *
  4. * @param       None
  5. *
  6. * @retval      None
  7. *
  8. */
  9. void bsp_uart_init(void)
  10. {
  11.     GPIO_Config_T gpioConfig;
  12.     USART_Config_T configStruct;
  13.    
  14.     /* GPIO */
  15.     RCM_EnableAHB1PeriphClock(RCM_AHB1_PERIPH_GPIOA);
  16.     GPIO_ConfigStructInit(&gpioConfig);
  17.     gpioConfig.pin     = GPIO_PIN_9 | GPIO_PIN_10;
  18.     gpioConfig.mode    = GPIO_MODE_AF;
  19.     gpioConfig.otype = GPIO_OTYPE_PP;
  20.     gpioConfig.speed   = GPIO_SPEED_100MHz;
  21.     gpioConfig.pupd    = GPIO_PUPD_NOPULL;
  22.     GPIO_Config(GPIOA, &gpioConfig);
  23.    
  24.     /* TX */
  25.     GPIO_ConfigPinAF(GPIOA, GPIO_PIN_SOURCE_9, GPIO_AF_USART1);
  26.     /* RX */
  27.     GPIO_ConfigPinAF(GPIOA, GPIO_PIN_SOURCE_10, GPIO_AF_USART1);
  28.    
  29.     /* USART */
  30.     RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_USART1);
  31.     USART_Reset(UART_INS);
  32.     USART_ConfigStructInit(&configStruct);
  33.     configStruct.baudRate = 115200;
  34.     configStruct.wordLength = USART_WORD_LEN_8B;
  35.     configStruct.stopBits = USART_STOP_BIT_1;
  36.     configStruct.parity = USART_PARITY_NONE ;
  37.     configStruct.mode = USART_MODE_TX_RX;
  38.     configStruct.hardwareFlow = USART_HARDWARE_FLOW_NONE;
  39.     USART_Config(UART_INS, &configStruct);   
  40.     USART_Enable(UART_INS);
  41. }

轮询方式的发送数据,基于发送为空标志位,不断的查询,发送为空即将新的数据写入串口数据寄存器即可,待全部数据都传入完成,等待发送完成标志置位,即表示所有数据发送完成。
  1. /*
  2. * @brief       发送数据
  3. *
  4. * @param       buf: 数据缓存
  5. *              buf_len: 缓存大小
  6. *
  7. * @retval      None
  8. *
  9. */
  10. void bsp_uart_send(uint8_t *buf, uint16_t buf_len)
  11. {
  12.     uint16_t i = 0;
  13.    
  14.     while (i < buf_len) {
  15.         while (USART_ReadStatusFlag(UART_INS, USART_FLAG_TXBE) == RESET);
  16.         USART_TxData(UART_INS, buf[i++]);
  17.     }
  18.     while (USART_ReadStatusFlag(UART_INS, USART_FLAG_TXC) == RESET);
  19. }

接收数据通过检查接收非空标志判断,接收非空标志置位,则表示有新数据抵达数据寄存器,从数据寄存器取走数据后,标志位将自动清零。而判断一串完整的报文数据接收是否完整,是通过接收空闲标志确定,接收空闲标志为在一定数量的空闲位后依然无新数据到来,则置位接收空闲标志,同时也意味着连续的数据传输已经结束。
  1. /*
  2. * @brief       接收数据
  3. *
  4. * @param       None
  5. *
  6. * @retval      None
  7. *
  8. */
  9. void bsp_uart_recv(void)
  10. {
  11.     /* 接收非空 */
  12.     if (USART_ReadStatusFlag(UART_INS, USART_FLAG_RXBNE) != RESET) {
  13.         if (rx_len < sizeof(rx_buf)) {
  14.             rx_buf[rx_len++] = USART_RxData(UART_INS);
  15.         }
  16.     }
  17.     /* 接收空闲则表示一帧报文结束 */
  18.     if (USART_ReadStatusFlag(UART_INS, USART_FLAG_IDLE) != RESET) {
  19.         rx_complete = 1;
  20.         USART_ClearStatusFlag(UART_INS, USART_FLAG_IDLE);
  21.     }
  22. }

中断方式的串口通讯初始化,只比轮询方式多使能接收非空中断和空闲中断,同时需要使能对应的串口中断号。注意:发送的相关中断需在要发送数据时,才能使能,否则立刻使能发送为空中断,将导致程序立刻进入中断。
  1. /*
  2. * @brief       初始化
  3. *
  4. * @param       None
  5. *
  6. * @retval      None
  7. *
  8. */
  9. void bsp_uart_init(void)
  10. {
  11.     GPIO_Config_T gpioConfig;
  12.     USART_Config_T configStruct;
  13.    
  14.     /* GPIO */
  15.     RCM_EnableAHB1PeriphClock(RCM_AHB1_PERIPH_GPIOA);
  16.     GPIO_ConfigStructInit(&gpioConfig);
  17.     gpioConfig.pin     = GPIO_PIN_9 | GPIO_PIN_10;
  18.     gpioConfig.mode    = GPIO_MODE_AF;
  19.     gpioConfig.otype = GPIO_OTYPE_PP;
  20.     gpioConfig.speed   = GPIO_SPEED_100MHz;
  21.     gpioConfig.pupd    = GPIO_PUPD_NOPULL;
  22.     GPIO_Config(GPIOA, &gpioConfig);
  23.    
  24.     /* TX */
  25.     GPIO_ConfigPinAF(GPIOA, GPIO_PIN_SOURCE_9, GPIO_AF_USART1);
  26.     /* RX */
  27.     GPIO_ConfigPinAF(GPIOA, GPIO_PIN_SOURCE_10, GPIO_AF_USART1);
  28.    
  29.     /* USART */
  30.     RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_USART1);
  31.     USART_Reset(UART_INS);
  32.     USART_ConfigStructInit(&configStruct);
  33.     configStruct.baudRate = 115200;
  34.     configStruct.wordLength = USART_WORD_LEN_8B;
  35.     configStruct.stopBits = USART_STOP_BIT_1;
  36.     configStruct.parity = USART_PARITY_NONE ;
  37.     configStruct.mode = USART_MODE_TX_RX;
  38.     configStruct.hardwareFlow = USART_HARDWARE_FLOW_NONE;
  39.     USART_Config(UART_INS, &configStruct);
  40.     /* 关闭发送为空中断 */
  41.     USART_DisableInterrupt(UART_INS, USART_INT_TXBE);
  42.     /* 使能接收非空中断 */
  43.     USART_EnableInterrupt(UART_INS, USART_INT_RXBNE);
  44.     /* 使能空闲中断 */
  45.     USART_EnableInterrupt(UART_INS, USART_INT_IDLE);   
  46.     /* 使能中断 */   
  47.     NVIC_EnableIRQRequest(USART1_IRQn, 0, 0);
  48.     /* 使能串口 */
  49.     USART_Enable(UART_INS);
  50. }

中断方式的串口发送数据,初始化完发送的基本信息后,使能发送为空中断即可,后续工作将进入中断进行处理。
  1. /*
  2. * @brief       发送数据
  3. *
  4. * @param       buf: 数据缓存
  5. *              buf_len: 缓存大小
  6. *
  7. * @retval      None
  8. *
  9. */
  10. void bsp_uart_send(uint8_t *buf, uint16_t buf_len)
  11. {   
  12.     if ((buf != NULL) && (buf_len > 0)) {
  13.         tx_buf = buf;
  14.         tx_index = 0;
  15.         tx_len = buf_len;
  16.         /* 开启发送为空中断 */
  17.         USART_EnableInterrupt(UART_INS, USART_INT_TXBE);
  18.     }
  19. }

串口中断处理函数内部,执行串口的发送和接收工作。发送为空中断,则将待发送数据不断传入数据寄存器,数据传输完毕后,立刻关闭发送为空中断;
串口中断接收数据与轮询方式的接收类似,只是工作改为在中断内部执行,无需主循环代码查询。
  1. /*
  2. * @brief       串口中断
  3. *
  4. * @param       None
  5. *
  6. * @retval      None
  7. *
  8. */
  9. void USART1_IRQHandler(void)
  10. {
  11.     /* 发送为空 */
  12.     if (USART_ReadIntFlag(UART_INS, USART_INT_TXBE) != RESET) {
  13.         if (tx_index < tx_len) {
  14.             /* 发数据 */
  15.             USART_TxData(UART_INS, tx_buf[tx_index++]);
  16.         } else {
  17.             /* 关闭发送 */
  18.             USART_DisableInterrupt(UART_INS, USART_INT_TXBE);
  19.         }
  20.     }
  21.     /* 接收非空 */
  22.     if (USART_ReadIntFlag(UART_INS, USART_INT_RXBNE) != RESET) {
  23.         if (rx_len < sizeof(rx_buf)) {
  24.             rx_buf[rx_len++] = USART_RxData(UART_INS);
  25.         }
  26.     }
  27.     /* 空闲 */
  28.     if (USART_ReadIntFlag(UART_INS, USART_INT_IDLE) != RESET) {
  29.         /* 清除空闲状态 */
  30.         (void)USART_RxData(UART_INS);
  31.         rx_complete = 1;
  32.     }
  33. }

串口的DMA通讯方式,初始化代码相对较为复杂,但此方式却是最为推荐的通讯方式。
初始化工作相对前面的两种方式,需要分别为串口的接收和发送都初始化一个DMA数据流,因为串口接收的数据量没法确定,所以初始化串口接收的DMA传输数据量固定配置为接收的最大值,也因此串口不能单次接收超过此值的数据量;而串口的发送DMA配置无法确定传输的数据量和传输的存储地址,需等到发送数据时才能确认。
  1. /*
  2. * @brief       初始化
  3. *
  4. * @param       None
  5. *
  6. * @retval      None
  7. *
  8. */
  9. void bsp_uart_init(void)
  10. {
  11.     GPIO_Config_T gpioConfig;
  12.     USART_Config_T configStruct;
  13.     DMA_Config_T dmaConfig;
  14.    
  15.     /* GPIO */
  16.     RCM_EnableAHB1PeriphClock(RCM_AHB1_PERIPH_GPIOA);
  17.     GPIO_ConfigStructInit(&gpioConfig);
  18.     gpioConfig.pin     = GPIO_PIN_9 | GPIO_PIN_10;
  19.     gpioConfig.mode    = GPIO_MODE_AF;
  20.     gpioConfig.otype = GPIO_OTYPE_PP;
  21.     gpioConfig.speed   = GPIO_SPEED_100MHz;
  22.     gpioConfig.pupd    = GPIO_PUPD_NOPULL;
  23.     GPIO_Config(GPIOA, &gpioConfig);
  24.    
  25.     /* TX */
  26.     GPIO_ConfigPinAF(GPIOA, GPIO_PIN_SOURCE_9, GPIO_AF_USART1);
  27.     /* RX */
  28.     GPIO_ConfigPinAF(GPIOA, GPIO_PIN_SOURCE_10, GPIO_AF_USART1);
  29.    
  30.     /* DMA */
  31.     RCM_EnableAHB1PeriphClock(RCM_AHB1_PERIPH_DMA2);
  32.     DMA_ConfigStructInit(&dmaConfig);
  33.     /* DMA -> USART_RX */
  34.     dmaConfig.channel = DMA_CHANNEL_4;
  35.     dmaConfig.peripheralBaseAddr = (uint32_t)&UART_INS->DATA;
  36.     dmaConfig.memoryBaseAddr =  (uint32_t)rx_buf;
  37.     dmaConfig.dir = DMA_DIR_PERIPHERALTOMEMORY;
  38.     dmaConfig.bufferSize = sizeof(rx_buf);
  39.     dmaConfig.peripheralInc = DMA_PERIPHERAL_INC_DISABLE;
  40.     dmaConfig.memoryInc = DMA_MEMORY_INC_ENABLE;
  41.     dmaConfig.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_BYTE;
  42.     dmaConfig.memoryDataSize = DMA_MEMORY_DATA_SIZE_BYTE;
  43.     dmaConfig.loopMode = DMA_MODE_NORMAL;
  44.     dmaConfig.priority = DMA_PRIORITY_LOW;
  45.     dmaConfig.fifoMode = DMA_FIFOMODE_DISABLE;
  46.     dmaConfig.fifoThreshold = DMA_FIFOTHRESHOLD_QUARTER;
  47.     dmaConfig.peripheralBurst = DMA_PERIPHERALBURST_SINGLE;
  48.     dmaConfig.memoryBurst = DMA_MEMORYBURST_SINGLE;
  49.     DMA_Config(RX_DMA, &dmaConfig);
  50.     /* 传输完成中断 */
  51.     DMA_EnableInterrupt(RX_DMA, DMA_INT_TCI**);
  52.     /* 使能中断 */
  53.     NVIC_EnableIRQRequest(DMA2_STR2_IRQn, 0, 0);
  54.    
  55.     /* DMA -> USART_TX */
  56.     dmaConfig.channel = DMA_CHANNEL_4;
  57.     dmaConfig.peripheralBaseAddr = (uint32_t)&UART_INS->DATA;
  58.     dmaConfig.memoryBaseAddr =  0;
  59.     dmaConfig.dir = DMA_DIR_MEMORYTOPERIPHERAL;
  60.     dmaConfig.bufferSize = 0;
  61.     dmaConfig.peripheralInc = DMA_PERIPHERAL_INC_DISABLE;
  62.     dmaConfig.memoryInc = DMA_MEMORY_INC_ENABLE;
  63.     dmaConfig.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_BYTE;
  64.     dmaConfig.memoryDataSize = DMA_MEMORY_DATA_SIZE_BYTE;
  65.     dmaConfig.loopMode = DMA_MODE_NORMAL;
  66.     dmaConfig.priority = DMA_PRIORITY_LOW;
  67.     dmaConfig.fifoMode = DMA_FIFOMODE_DISABLE;
  68.     dmaConfig.fifoThreshold = DMA_FIFOTHRESHOLD_QUARTER;
  69.     dmaConfig.peripheralBurst = DMA_PERIPHERALBURST_SINGLE;
  70.     dmaConfig.memoryBurst = DMA_MEMORYBURST_SINGLE;
  71.     DMA_Config(TX_DMA, &dmaConfig);
  72.    
  73.     /* USART */
  74.     RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_USART1);
  75.     USART_Reset(UART_INS);
  76.     USART_ConfigStructInit(&configStruct);
  77.     configStruct.baudRate = 115200;
  78.     configStruct.wordLength = USART_WORD_LEN_8B;
  79.     configStruct.stopBits = USART_STOP_BIT_1;
  80.     configStruct.parity = USART_PARITY_NONE ;
  81.     configStruct.mode = USART_MODE_TX_RX;
  82.     configStruct.hardwareFlow = USART_HARDWARE_FLOW_NONE;
  83.     USART_Config(UART_INS, &configStruct);
  84.     USART_EnableDMA(UART_INS, USART_DMA_TX_RX);
  85.     /* 使能空闲中断 */
  86.     USART_EnableInterrupt(UART_INS, USART_INT_IDLE);   
  87.     /* 使能发送完成中断 */
  88.     USART_EnableInterrupt(UART_INS, USART_INT_TXC);   
  89.     /* 使能中断 */  
  90.     NVIC_EnableIRQRequest(USART1_IRQn, 0, 0);
  91.     /* 使能串口 */
  92.     USART_Enable(UART_INS);
  93.     /* 接收数据 */
  94.     bsp_uart_recv();
  95. }

DMA的通讯方式,判断接收完整一串报文数据,也是通过空闲标志位确定,而确定具体已经接收了多少数据量,则是通过计算当前DMA接收数据流的剩余可传输数据量来反推得出,处理完毕后,需重新开启接受的DMA,并配置为最大的传输数据量,以确保后续新来的数据量能完整接收。
DMA的发送数据,通过发送完成中断确定全部数据发送完毕。
  1. /*
  2. * @brief       串口中断
  3. *
  4. * @param       None
  5. *
  6. * @retval      None
  7. *
  8. */
  9. void USART1_IRQHandler(void)
  10. {
  11.     uint16_t num = 0;
  12.    
  13.     /* 空闲 */
  14.     if (USART_ReadIntFlag(UART_INS, USART_INT_IDLE) != RESET) {
  15.         /* 清除空闲状态 */
  16.         (void)USART_RxData(UART_INS);
  17.         /* 计算已经传输的数据量 */
  18.         num = sizeof(rx_buf) - DMA_ReadDataNumber(RX_DMA);
  19.         if (num != 0) {
  20.             rx_len = num;
  21.             rx_complete = 1;
  22.         }
  23.         bsp_uart_recv();
  24.     }
  25.     /* 发送完成中断 */
  26.     if (USART_ReadIntFlag(UART_INS, USART_INT_TXC) != RESET) {
  27.         /* 清除空闲状态 */
  28.         USART_ClearIntFlag(UART_INS, USART_INT_TXC);
  29.     }
  30. }

另外,DMA接收的数据流完成中断也需要特意处理,处理方式与串口的空闲中断标志一致,即重新开启接收。
  1. /*
  2. * @brief       DMA中断
  3. *
  4. * @param       None
  5. *
  6. * @retval      None
  7. *
  8. */
  9. void DMA2_STR2_IRQHandler(void)
  10. {
  11.     uint16_t num = 0;
  12.    
  13.     /* USART RX */
  14.     if (DMA_ReadIntFlag(RX_DMA, DMA_INT_TCI**2) != RESET) {
  15.         DMA_ClearIntFlag(RX_DMA, DMA_INT_TCI**2);
  16.         /* 计算已经传输的数据量 */
  17.         num = sizeof(rx_buf) - DMA_ReadDataNumber(RX_DMA);
  18.         if (num != 0) {
  19.             rx_len = num;
  20.             rx_complete = 1;
  21.         }
  22.         bsp_uart_recv();
  23.     }
  24. }

重新开启接收,需先关闭DMA的使能,重新配置为接收最大传输数据量后,再使能串口接收的DMA数据流。
  1. /*
  2. * @brief       接收数据
  3. *
  4. * @param       None
  5. *
  6. * @retval      None
  7. *
  8. */
  9. void bsp_uart_recv(void)
  10. {   
  11.     DMA_Disable(RX_DMA);
  12.     DMA_ConfigDataNumber(RX_DMA, sizeof(rx_buf));
  13.     DMA_Enable(RX_DMA);
  14. }

串口的DMA发送需每次开启发送前都配置新的数据存储地址和数据数量,因为这些信息在应用层中,不同的处理逻辑传入的值都是不同的。另外,DMA数据流的错误标志需即时清除,否则影响DMA的重新使能。
  1. /*
  2. * @brief       发送数据
  3. *
  4. * @param       buf: 数据缓存
  5. *              buf_len: 缓存大小
  6. *
  7. * @retval      None
  8. *
  9. */
  10. void bsp_uart_send(uint8_t *buf, uint16_t buf_len)
  11. {   
  12.     if ((buf != NULL) && (buf_len > 0)) {
  13.         uint32_t flag = DMA_INT_FEI**7 | DMA_INT_DMEI**7 | DMA_INT_TEI**7 | DMA_INT_HTI**7 | DMA_INT_TCI**7;
  14.         DMA_ClearIntFlag(TX_DMA, flag);
  15.         DMA_Disable(TX_DMA);
  16.         DMA_ConfigDataNumber(TX_DMA, buf_len);
  17.         TX_DMA->M0ADDR = (uint32_t)buf;
  18.         USART_ClearIntFlag(UART_INS, USART_INT_TXC);
  19.         DMA_Enable(TX_DMA);
  20.     }
  21. }

优劣势:
轮询方式:代码简单,利于理解,但需要及时查询状态,否则溢出,且发数据期间严重占用内核;
中断方式:代码简单,利于理解,收发数据期间可释放内核,但频繁中断,占用内核,且大量其他中断存在的场景可能影响数据收发的连续性;
DMA方式:收发数据不占用内核,且中断产生较少,收发数据能保证连续性,但初始化代码复杂;


从使用经验上推荐,最推荐的是DMA方式,而中断方式在大量中断的场景下要保证收发数据的绝对连续性,需要把串口的中断优先级提升到非常高的位置,至于轮询方式,只有非常简单的应用场景才能考虑使用,一般不建议使用。

4. 测试
本次测试的目的是将接收到的数据再发出来,如下代码为轮询方式,相当简洁,而中断和DMA方式,则连bsp_uart_recv()函数都不需要调用。
  1. // 应用初始化
  2. void app_init(void)
  3. {
  4.     bsp_uart_init();
  5. }

  6. // 应用任务
  7. void app_task(void)
  8. {
  9.     /* 接收数据 */
  10.     bsp_uart_recv();
  11.     /* 接收完成再发出来 */
  12.     if (bsp_uart_is_rx_complete() != 0) {
  13.         bsp_uart_send(bsp_uart_get_rx_buf(), bsp_uart_get_rx_len());
  14.         bsp_uart_reset_recv();
  15.     }
  16. }

如下图的测试结果:串口完整接收到数据后,只间隔100us就立刻发出。
39802691c589e744e2.png

5. 驱动移植说明
驱动的移植只需要修改bsp_uart_init函数即可,改为对应的IO和串口外设号,同时DMA的数据流需对应手册修改。

6. 详细代码
轮询、中断、DMA方式
Poll.zip (6.55 MB, 下载次数: 0)
Interrupt.zip (6.56 MB, 下载次数: 0)
DMA.zip (6.57 MB, 下载次数: 0)


您需要登录后才可以回帖 登录 | 注册

本版积分规则

34

主题

64

帖子

0

粉丝
快速回复 在线客服 返回列表 返回顶部