使用DMA传输实现单片机高效串口转发——以STM32系列为例
Date | Author | Version | Note |
---|---|---|---|
2023.08.06 | Dog Tao | V1.0 | 1. 完成了文档的撰写。 |
文章目录
- 使用DMA传输实现单片机高效串口转发——以STM32系列为例
- 应用场景
- 实现流程
- 源码示例
- 串口与中断配置
- DMA外设配置
- DMA发送数据函数
- 串口中断服务函数
- DMA中断服务函数
- Modbus协议代码
应用场景
在许多现实应用场景中,例如工业自动化控制、嵌入式通信设备等领域,单片机需要实时地从一个串口读取数据,并转发到另一个串口。如果使用常规的轮询或中断方法来完成这样的任务,会消耗大量的CPU资源,效率较低。此时如果采用DMA(直接存储器访问)进行串口数据转发则可以具备很多优势,例如降低数据转发延时、减轻CPU的运行负载、提高系统的实时性等。通过串口转发也可以实现多个不同通讯形式(例如无线传输与有线传输)、不同通讯协议(例如自定协议与Modbus协议)、不同通讯参数(例如两个设备分别具备不同波特率)的设备通讯中转。
直接存储器访问(DMA,Direct Memory Access)是一种允许外设或内存直接与其他外设或内存交换数据,而不需要通过CPU进行中介处理的技术。DMA可以有效提高整体系统效率,因为它允许数据传输的同时,CPU仍可以执行其他任务。STM32的DMA系统是一项强大的功能,允许高效的数据传输,同时减轻了CPU的负担。其灵活的配置选项和与多种外设的兼容性使其适用于许多应用,从简单的数据复制到复杂的外设管理。正确使用DMA可以显著提高STM32微控制器的性能和功能。
From STM32F103 datasheet:
The flexible 7-channel general-purpose DMA is able to manage memory-to-memory, peripheral-to-memory and memory-to-peripheral transfers. The DMA controller supports circular buffer management avoiding the generation of interrupts when the controller reaches the end of the buffer. Each channel is connected to dedicated hardware DMA requests, with support for software trigger on each channel. Configuration is made by software and transfer sizes between source and destination are independent.The DMA can be used with the main peripherals: SPI, I2C, USART, general-purpose and advanced-control timers TIMx and ADC.
From STM32F407 datasheet:
The devices feature two general-purpose dual-port DMAs (DMA1 and DMA2) with 8 streams each. They are able to manage memory-to-memory, peripheral-to-memory and memory-to-peripheral transfers. They feature dedicated FIFOs for APB/AHB peripherals, support burst transfer and are designed to provide the maximum peripheral bandwidth (AHB/APB). The two DMA controllers support circular buffer management, so that no specific code is needed when the controller reaches the end of the buffer. The two DMA controllers also have a double buffering feature, which automates the use and switching of two memory buffers without requiring any special code. Each stream is connected to dedicated hardware DMA requests, with support for software trigger on each stream. Configuration is made by software and transfer sizes between source and destination are independent. The DMA can be used with the main peripherals: SPI and I2S, I2C, USART, General-purpose, basic and advanced-control timers TIMx, DAC, SDIO, Camera interface (DCMI) and ADC.
实现流程
使用单片机实现串口转发可以分为两种主要的模式:直接转发模式与选择转发模式。直接转发模式是指单片机从一个串口中接收到的数据不经CPU的判断与处理,直接通过DMA传输从另个一串口发送出去。间接转发模式是指单片机从一个串口中接收到的数据需经过CPU的判断与处理,选择性的将部分数据或者修改后的数据通过DMA传输从另个一串口发送出去。
直接转发模式的核心实现过程为:对于接收数据的DMA通道,将串口的数据寄存器地址设置为源地址,并设置一个内存地址为目标地址。对于发送数据的DMA通道,将之前设置的内存地址设置为源地址,将另一个串口的数据寄存器地址设置为目标地址。
间接转发模式由于CPU的恰当介入而具备更好的灵活性与多场景的适应性,因此得到更为广泛的应用。以USART1与USART3为例,间接转发的主要实现流程为:
-
初始化串口:初始化USART1和USART3,配置波特率、数据位、停止位、奇偶校验等。
-
配置USART1用于中断接收和DMA转发:启用USART1的接收中断功能,并配置相关NVIC。选择适当的DMA通道,关联USART1的发送功能。设置DMA源地址(例如缓冲区)和目标地址(USART3的数据发送寄存器)。配置DMA的大小、方向、优先级、模式等。
-
配置USART3用于中断接收和DMA转发:与USART1类似,配置USART3以使用中断进行接收,并选择适当的DMA通道用于发送。设置DMA源地址(例如缓冲区)和目标地址(USART1的数据发送寄存器)。配置DMA的大小、方向、优先级、模式等。
-
启用USART和DMA:启用USART1、USART3以及相关的DMA通道。
-
中断服务程序处理:在USART1的中断服务程序中,读取接收到的数据,并触发与USART3关联的DMA传输。在USART3的中断服务程序中,读取接收到的数据,并触发与USART1关联的DMA传输。
-
错误处理和同步:监视DMA和USART的错误标志,并采取适当措施响应任何潜在问题。根据需要,实现缓冲区管理和同步机制,以确保数据的完整性和时序。
源码示例
以STM32F407的USART1与USART3双向互发为例,展示核心功能实现的源码。其中部分自定外设配置函数(例如USART_ConfigNVIC
, USART_ConfigPort
等)来自笔者自定的HAL库。
示例代码中,本机为Modbus-RTU从机设备,其USART1为Modbus-RTU/无线433MHz通讯口,USART3为RS485通讯口(485总线上连接多台Modbus-RTU从机设备)。单片机从USART1中接收到Modbus-RTU请求报文之后,会首先判断从机地址是否为本机,如果从机地址为本机地址,则进行正常的报文回复处理。如果从机地址不是本机地址,则通过USART3/485端口进行数据转发。接收到来自USART3/485端口上对应从机的回复后,再通过USART1/无线433MHz通讯端口将报文进一步封装后发送到主机。
通过此方法,可以实现一对一的无线通讯与一对多的Modbus/RS485的混合组网。其通讯系统示意图如下所示:
串口与中断配置
void NVIC_Config()
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
USART_ConfigNVIC(1, 0, 0);
USART_ConfigNVIC(2, 0, 0);
USART_ConfigNVIC(3, 0, 0);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream7_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Stream3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
void USART_Config()
{
USART_ConfigPort(1, 115200, WordLength_8b, StopBits_1, Parity_No);
USART_ConfigPort(2, 115200, WordLength_8b, StopBits_1, Parity_No);
USART_ConfigPort(3, 115200, WordLength_8b, StopBits_1, Parity_No);
// 使能串口发送完成中断
// USART_ITConfig(USART1, USART_IT_TC, ENABLE);
// USART_ITConfig(USART2, USART_IT_TC, ENABLE);
// USART_ITConfig(USART3, USART_IT_TC, ENABLE);
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);
USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);
USART_Config_DMA();
// 初始化串口接收缓冲区
USART_RevInitAll();
}
DMA外设配置
void USART_Config_DMA()
{
//配置USART1_TX-Stream: DMA-2 Stream-7 Channel-4
DMA_InitTypeDef DMA_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); //开启DMA时钟
DMA_DeInit(DMA2_Stream7);
while(DMA_GetCmdStatus(DMA2_Stream7) != DISABLE){} //等待stream可配置,即DMAy_SxCR.EN变为0
DMA_InitStructure.DMA_Channel = DMA_Channel_4; //从8个channel中选择一个
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&USART1->DR; //外设地址
DMA_InitStructure.DMA_Memory0BaseAddr = (u32)SendBuff; //存储器0地址,双缓存模式还要使用M1AR
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral; //存储器到外设模式
DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE; //数据传输量,以外设数据项为单位
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址保持不变
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据位宽:8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据位宽:8位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //普通模式(与循环模式对应)
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //中等优先级
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; //禁止FIFO模式
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; //单次传输
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; //单次传输
DMA_ITConfig(DMA2_Stream7, DMA_IT_TC, ENABLE);
// DMA_ITConfig(DMA2_Stream7, DMA_IT_TE, ENABLE);
DMA_Init(DMA2_Stream7, &DMA_InitStructure);
//配置USART3_TX-Stream: DMA-1 Stream-3 Channel-4
// DMA_InitTypeDef DMA_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE); //开启DMA时钟
DMA_DeInit(DMA1_Stream3);
while(DMA_GetCmdStatus(DMA1_Stream3) != DISABLE){} //等待stream可配置,即DMAy_SxCR.EN变为0
DMA_InitStructure.DMA_Channel = DMA_Channel_4; //从8个channel中选择一个
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&USART3->DR; //外设地址
DMA_InitStructure.DMA_Memory0BaseAddr = (u32)SendBuff; //存储器0地址,双缓存模式还要使用M1AR
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral; //存储器到外设模式
DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE; //数据传输量,以外设数据项为单位
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址保持不变
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据位宽:8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据位宽:8位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //普通模式(与循环模式对应)
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //中等优先级
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; //禁止FIFO模式
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; //单次传输
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; //单次传输
DMA_ITConfig(DMA1_Stream3, DMA_IT_TC, ENABLE);
// DMA_ITConfig(DMA1_Stream3, DMA_IT_TE, ENABLE);
DMA_Init(DMA1_Stream3, &DMA_InitStructure);
}
DMA发送数据函数
由于USART3是RS485协议传输,需要选择收发状态。本文源码中,通过RS485_CTRL_ADDR
的值实现收发转换。在发送数据前,先将RS485_CTRL_ADDR
置1。在DMA中断服务函数中(发送完成中断),将RS485_CTRL_ADDR
置0,恢复RS485的数据接收状态。
void USART1_DMA_SendData(uint8_t *tx_buffer,uint16_t length)
{
// DMA_InitTypeDef DMA_InitStructure;
DMA_Cmd(DMA2_Stream7, DISABLE); //关闭DMA通道
DMA_SetCurrDataCounter(DMA2_Stream7, (uint16_t)length); //设置传输字节数
DMA2_Stream7->CR |= (1 << 10); //发送DMA流的地址不自增
DMA2_Stream7->M0AR = (uint32_t)tx_buffer; //设置接收和发送的内存地址
DMA_Cmd(DMA2_Stream7, ENABLE); //打开DMA通道
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口1的DMA发送
// while( DMA_GetFlagStatus(DMA2_Stream7, DMA_FLAG_TCIF7) == RESET); //等待传输完成
// DMA_Cmd(DMA2_Stream7, DISABLE); //关闭DMA通道
// DMA_ClearFlag(DMA2_Stream7, DMA_FLAG_TCIF7); //清除DMA传输完成标志
}
void USART3_DMA_SendData(uint8_t *tx_buffer,uint16_t length)
{
// DMA_InitTypeDef DMA_InitStructure;
DMA_Cmd(DMA1_Stream3, DISABLE); //关闭DMA通道
DMA_SetCurrDataCounter(DMA1_Stream3, (uint16_t)length); //设置传输字节数
DMA1_Stream3->CR |= (1 << 10); //发送DMA流的地址不自增
DMA1_Stream3->M0AR = (uint32_t)tx_buffer; //设置接收和发送的内存地址
DMA_Cmd(DMA1_Stream3, ENABLE); //打开DMA通道
USART_DMACmd(USART3,USART_DMAReq_Tx,ENABLE); //使能串口1的DMA发送
// while( DMA_GetFlagStatus(DMA1_Stream3, DMA_FLAG_TCIF3) == RESET); //等待传输完成
// DMA_Cmd(DMA1_Stream3, DISABLE); //关闭DMA通道
// DMA_ClearFlag(DMA1_Stream3, DMA_FLAG_TCIF7); //清除DMA传输完成标志
}
void USART1_WSN32_SendData(uint8_t *tx_buffer, uint16_t length)
{
static uint8_t data_temp[300];
memcpy(data_temp, MB_CommParam.MB_PreTrans_Data, MB_CommParam.MB_PreTrans_Num);
memcpy(data_temp + MB_CommParam.MB_PreTrans_Num, tx_buffer, length);
USART1_DMA_SendData(data_temp, length + MB_CommParam.MB_PreTrans_Num);
// USART_SendData(USART1, MB_CommParam.MB_PreTrans_Data, MB_CommParam.MB_PreTrans_Num);
// USART_SendData(USART1, tx_buffer, length);
}
void USART3_RS485_SendData(uint8_t *tx_buffer, uint16_t length)
{
if(tx_buffer[0] == MB_CommParam.MB_SlaveAddr)
{
// 本机地址,不发送
return;
}
*RS485_CTRL_ADDR = 1;
// delay_ms(1);
USART3_DMA_SendData(tx_buffer, length);
// 恢复RS485控制信号为接收状态的操作放到DMA发送完成中断中
// vtaskDelay(100);
// *RS485_CTRL_ADDR = 0;
}
串口中断服务函数
示例代码中,本机为Modbus-RTU从机设备,其USART1为Modbus-RTU/无线433MHz通讯串口,USART3为RS485通讯串口(485总线上连接多台Modbus-RTU从机设备)。因此,MB_CommParam.MB_PortNum
的值为1。
USART1: 在串口接收中断USART_IT_RXNE
的服务函数中调用Modbus-RTU协议的数据接收函数pxMBFrameCBByteReceived
。
USART3: 在串口接收中断USART_IT_RXNE
的服务函数中往FIFO队列缓冲中添加接收到的数据。在串口空闲中断USART_IT_IDLE
的服务函数中判断数据接收完成并实现数据转发的操作。
void USART1_IRQHandler(void)
{
/**
* 如果使能串口接收中断,那么ORE为1时也会产生中断。
* 在应用中对ORE标志进行处理,当判断发生ORE中断的时候,
* 我们再读一次USART_DR的值,
* 这样如果没有新的Overrun 溢出事件发生的时候,ORE会被清除,
* 然后程序就不会因为ORE未被清除而一直不断的进入串口中断
*/
if (USART_GetFlagStatus(USART1, USART_FLAG_ORE) != RESET)
{
USART_ReceiveByte(USART1);
}
if (MB_CommParam.MB_PortNum == 1)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
pxMBFrameCBByteReceived();
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
else if (USART_GetITStatus(USART1, USART_IT_TC) != RESET)
{
pxMBFrameCBTransmitterEmpty();
USART_ClearITPendingBit(USART1, USART_IT_TC);
}
else if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)
{
// 串口接收完数据后空闲必须清除空闲标志位。
// 通过读串口DR寄存器里的值来清除IDLE标志位,否则将一直触发空闲中断
uint16_t data_temp = USART1->DR; // 先读取接收缓存中数据,清除空闲标志位
data_temp = USART1->SR;
}
else
{
}
}
else
{
/* 省略无关代码 */
}
}
void USART3_IRQHandler(void)
{
if (USART_GetFlagStatus(USART3, USART_FLAG_ORE) != RESET)
{
USART_ReceiveByte(USART3);
}
if (MB_CommParam.MB_PortNum == 3)
{
/* 省略无关代码 */
}
else
{
if (USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
{
USART_ClearITPendingBit(USART3, USART_IT_RXNE);
USART_WriteFIFO(2, USART_ReceiveByte(USART3)); // 将接收到的数据添加到FIFO缓冲区
}
else if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) // 串口空闲(数据接收完成)时,转发数据到USART1
{
uint16_t data_temp = USART3->DR; // 先读取接收缓存中数据,清除空闲标志位
data_temp = USART3->SR;
if (IsEnablePortForwarding != 0)
{
// USART3 数据接收完成后,转发数据到串口1
USART3_RevBuffer_Handler(USART1_WSN32_SendData);
// USART3_RevBuffer_Handler(USART2_RS232_SendData);
}
}
else if (USART_GetITStatus(USART3, USART_IT_TC) != RESET)
{
USART_ClearITPendingBit(USART3, USART_IT_TC);
// do something
}
else
{
}
}
}
DMA中断服务函数
在DMA中断服务函数中(发送完成中断),将RS485_CTRL_ADDR
置0,恢复RS485的数据接收状态。
void DMA2_Stream7_IRQHandler(void) // USART-1-TX DMA
{
// 判断是否为DMA发送完成中断
if (DMA_GetFlagStatus(DMA2_Stream7, DMA_FLAG_TCIF7) == SET)
{
DMA_Cmd(DMA2_Stream7, DISABLE); // 关闭DMA通道
DMA_ClearFlag(DMA2_Stream7, DMA_FLAG_TCIF7);
}
}
void DMA1_Stream3_IRQHandler(void) // USART3-TX DMA
{
// 判断是否为DMA发送完成中断
if (DMA_GetFlagStatus(DMA1_Stream3, DMA_FLAG_TCIF3) == SET)
{
DMA_Cmd(DMA1_Stream3, DISABLE); // 关闭DMA通道
DMA_ClearFlag(DMA1_Stream3, DMA_FLAG_TCIF3);
// delay_ms(1);
delay_us(500);
*RS485_CTRL_ADDR = 0; // 恢复RS485的数据接收状态
}
}
Modbus协议代码
Modbus-RTU协议通过移植freemodbus库实现,笔者在此库中增加了报文接收的函数指针:
/// @brief When modbus-RTU ADU received, this function will be called.
extern RTU_ADU_ReceivedHandler_Type RTU_ADU_ReceivedHandler;
因此,可以设计一个回调函数USART3_RS485_SendData
(已在上文提供实现源码)注册给RTU_ADU_ReceivedHandler
指针,实现非本机地址的modbus请求指令通过USART3转发。
User_Init
函数首先通过读取两个拨码开关的值来判断当前设备的功能设定,如果处于无线通讯状态(主机与本机一对一)则使能串口转发功能。通过将不是本机地址的Modbus报文通过RS485总线发送出去,再将接收到的RS485回复数据通过无线通讯转发到主机,则可以实现多机通讯与无线/有线混合组网。
void User_Init()
{
// 读取拨码开关的拨码值
DevAddr_Val = Debug_GetDipSwitchValue(GPIO_Array_DevAddr, 4, 0);
SigChan_Val = Debug_GetDipSwitchValue(GPIO_Array_SigChan, 4, 0);
if ((DevAddr_Val != 0) && (SigChan_Val != 0)) // The current work mode is WSN32
{
IsEnablePortForwarding = 1;
}
else
{
IsEnablePortForwarding = 0;
}
// 初始化Modbus四种寄存器
User_MB_InitRegs();
if(IsEnablePortForwarding != 0)
{
RTU_ADU_ReceivedHandler = USART3_RS485_SendData; // 注册回调函数,处理接收到Modbus报文事件
}
}