前言
在STM32学习中,串口调试对于我们调试程序至关重要,所以写了一个串口+DMA接发的回显功能模板,方便以后调试程序用。
1 用法介绍
串口加环形队列基本满足普通单片机的数据收发处理,实现起来简单可靠,但实际项目中会出现多个串口连用同时又希望数据收发过程不要占用太多CPU时间,这时DMA就有大用了。
总体方案为:开启STM32F1的串口1收发DMA功能,接收采用DMA传输完成中断函数将每一个字节压入环形队列。main函数进程中实现对数据的DMA发送,DMA发送完成队列实现对中断标志的清空。
2 DMA
-
DMA(Direct Memory Access)直接存储器访问
-
DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源(外设一般指外设的数据寄存器DR;存储器指的运行内存SRAM和程序存储器Flash,是我们存储变量数组和程序代码的地方)
-
12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)
-
每个通道都支持软件触发和特定的硬件触发(外设到存储器因为一般有时机触发,所以选择硬件触发;存储器到存储器选择软件触发)
-
STM32F103C8T6 DMA资源:DMA1(7个通道)
DMA基本结构
- 数据转运有两大站点:外设寄存器站点和存储器站点(可以实现外设到存储器和存储器到存储器的数据转运方法)
- 站点参数:
1.起始地址(从哪里来,到哪里去)
2.数据宽度(指定一次转运按多大的数据宽度进行)
3.地址是否自增(一次转运完成以后,地址是不是自增)
- 传输计数器:记录总共需要转运几次
- 自动重装器:计数器减到0以后,是否自动恢复到计数初始值
- DMA触发控制:M2M置1时软件触发;置0时硬件触发。软件触发和循环模式不能同时使用,会导致DMA一直在循环运行
DMA请求
DMA请求映像可知,USART1_TX对应通道4;USART1_RX对应通道5。
3 软件代码
main.c
#include "stm32f10x.h"
#include "usart1.h"
void STM32_Tx_task() //发送任务函数 STM32---->PC串口
{
USART1_printf("STM32---->PC串口\r\n");
USART1_printf("this is message from STM32!\r\n");
USART1_printf("\r\n");
}
void STM32_Rx_task() //接收任务函数 STM32<----PC串口
{
int i=0;
for(i=0;i<2;i++)
{
if(UART1[i].u1rx1IsRev == 1)
{
UART1[i].u1rx1IsRev =0;
USART1_printf("STM32<----PC串口\r\n");
DMA_USART1_Tx_Data(UART1[i].u1rxbuf, UART1[i].u1rxSize);
USART1_printf("\r\n");
}
}
}
int main(void)
{
NVIC_Configuration();
Initial_UART1(115200);
STM32_Tx_task();
while (1)
{
STM32_Rx_task();
}
}
uart1.c
串口1初始化
//串口1初始化
void Initial_UART1(unsigned long baudrate)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//时钟设置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//USART1_RX GPIOA.10
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 8; //抢占优
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure);
//USART 初始化设置
USART_InitStructure.USART_BaudRate = baudrate;
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1; //一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No; //无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART1, &USART_InitStructure);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); //开启检测串口空闲状态中断
USART_Cmd(USART1, ENABLE);
DMA1_USART1_Init();
}
- 串口初始化与串口收发基本一样,多了一个串口空闲状态中断:当串口没有数据发送或接收时,可以触发空闲状态中断。
- 空闲中断的作用在于方便接收不定长的数据
串口1的DMA初始化
void DMA1_USART1_Init(void)
{
DMA_InitTypeDef DMA1_Init;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE); //开启DMA1时钟
DMA_DeInit(DMA1_Channel5); //将DMA的通道5寄存器重设为缺省值
//DMA_USART1_RX USART1->RAM的数据传输
DMA1_Init.DMA_PeripheralBaseAddr = (u32)(&USART1->DR); //外设基地址
DMA1_Init.DMA_MemoryBaseAddr = (u32)UART1[0].u1rxbuf; //存储器基地址
DMA1_Init.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,从外设读取到内存
DMA1_Init.DMA_BufferSize = USART1_MAX_RX_LEN; //DMA通道的DMA缓存的大小
DMA1_Init.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变
DMA1_Init.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增
DMA1_Init.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据宽度为8位
DMA1_Init.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据宽度为8位
DMA1_Init.DMA_Mode = DMA_Mode_Normal; //工作在正常模式
DMA1_Init.DMA_Priority = DMA_Priority_High; //DMA通道拥有高优先级
DMA1_Init.DMA_M2M = DMA_M2M_Disable; //DMA通道没有设置为内存到内存传输
DMA_Init(DMA1_Channel5,&DMA1_Init); //对DMA通道5进行初始化
//DMA_USART1_TX RAM->USART1的数据传输
DMA_DeInit(DMA1_Channel4); //将DMA的通道4寄存器重设为缺省值
DMA1_Init.DMA_PeripheralBaseAddr = (u32)(&USART1->DR); //外设基地址
DMA1_Init.DMA_MemoryBaseAddr = (u32)USART1_TX_BUF; //存储器及地址
DMA1_Init.DMA_DIR = DMA_DIR_PeripheralDST; //数据传输方向,从内存发送到外设
DMA1_Init.DMA_BufferSize = USART1_MAX_TX_LEN; //DMA通道的DMA缓存的大小
DMA1_Init.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设地址寄存器不变
DMA1_Init.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增
DMA1_Init.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据宽度为8位
DMA1_Init.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //内存数据宽度为8位
DMA1_Init.DMA_Mode = DMA_Mode_Normal; //工作在正常模式
DMA1_Init.DMA_Priority = DMA_Priority_High; //DMA通道拥有高优先级
DMA1_Init.DMA_M2M = DMA_M2M_Disable; //DMA通道没有设置为内存到内存传输
DMA_Init(DMA1_Channel4,&DMA1_Init); //对DMA通道4进行初始化
//DMA1通道5 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel5_IRQn; //NVIC通道设置
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3 ; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure);
//DMA1通道4 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel4_IRQn; //NVIC通道设置
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3 ; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure);
DMA_ITConfig(DMA1_Channel5,DMA_IT_TC,ENABLE); //开USART1 Rx DMA中断
DMA_ITConfig(DMA1_Channel4,DMA_IT_TC,ENABLE); //开USART1 Tx DMA中断
DMA_Cmd(DMA1_Channel5,ENABLE); //使DMA通道5停止工作
DMA_Cmd(DMA1_Channel4,DISABLE); //使DMA通道4停止工作
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); //开启串口DMA发送
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); //开启串口DMA接收
}
- DMA初始化:
1.开启DMA1的时钟
2.配置USART1->RAM的数据传输通道和RAM->USART1的数据传输通道
3.开启DMA通道4和通道5的传输完成标志位(DMA_IT_TC)到NVIC的输出,同时配置DMA通道4和通道5的NVIC。
4.使能USART1的DMA发送和接收功能,允许通过DMA直接将数据发送到USART。
处理DMA1通道5的接收完成中断
//处理DMA1 通道5的接收完成中断
void DMA1_Channel5_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC5)!= RESET) //DMA接收完成标志
{
DMA_ClearITPendingBit(DMA1_IT_TC5); //清除中断标志
USART_ClearFlag(USART1,USART_FLAG_TC); //清除USART1标志位
DMA_Cmd(DMA1_Channel5, DISABLE ); //关闭USART1 RX DMA1 所指示的通道
UART1[witchbuf1].u1rxSize = USART1_MAX_RX_LEN;
UART1[witchbuf1].u1rx1IsRev = 1;
witchbuf1 = witchbuf1 == 0?1:0;
DMA1_Channel5->CMAR=(u32)UART1[witchbuf1].u1rxbuf;
DMA1_Channel5->CNDTR = USART1_MAX_RX_LEN; //DMA通道的DMA缓存的大小
DMA_Cmd(DMA1_Channel5, ENABLE); //使能USART1 RX DMA1 所指示的通道
}
}
- 1.if(DMA_GetITStatus(DMA1_IT_TC5)!= RESET):这行代码检查DMA1通道5的传输完成(TC,Transfer Complete)中断是否被触发。如果这个中断被触发,DMA_GetITStatus函数会返回一个非零值(例如 SET),否则返回 RESET。
- 2.DMA_ClearITPendingBit(DMA1_IT_TC5):清除DMA1通道5的传输完成中断标志位。这是为了防止再次进入这个中断服务程序。
- 3.USART_ClearFlag(USART1,USART_FLAG_TC):清除USART1的相关标志位。这可能是为了准备下一次的数据传输或接收。
- 4.DMA_Cmd(DMA1_Channel5, DISABLE ):关闭USART1 TX的DMA通道,即停止DMA传输。
- 5.切换接收缓冲区和相关的标志位:
- UART1[witchbuf1].u1rxSize = USART1_MAX_RX_LEN;:设置接收缓冲区的大小为最大值。
- UART1[witchbuf1].u1rx1IsRev = 1;:设置一个标志位,用来指示缓冲区是否反转。
- witchbuf1 = witchbuf1 == 0?1:0;:切换接收缓冲区的索引。
- 6.DMA1_Channel5->CMAR=(u32)UART1[witchbuf1].u1rxbuf;:设置DMA的内存地址,即新的接收缓冲区的地址。
- 7.DMA1_Channel5->CNDTR = USART1_MAX_RX_LEN;:设置DMA传输的数据量,即最大接收长度。
- 8.DMA_Cmd(DMA1_Channel5, ENABLE);:重新开启USART1 TX的DMA通道,开始新的DMA传输。
处理DMA1通道4的发送完成中断
//DMA1通道4中断
void DMA1_Channel4_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC4)!= RESET) //DMA发送完成标志
{
DMA_ClearITPendingBit(DMA1_IT_TC4); //清除中断标志
USART_ClearFlag(USART1,USART_FLAG_TC); //清除串口1的标志位
DMA_Cmd(DMA1_Channel4, DISABLE ); //关闭USART1 TX DMA1 所指示的通道
USART1_TX_FLAG=0; //USART1发送标志(关闭)
}
}
串口1中断函数
//串口1中断函数
void USART1_IRQHandler(void)
{
u8 USART1_RX_LEN = 0; //接收数据长度
if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) //串口1空闲中断
{
USART_ReceiveData(USART1); //清除串口1空闲中断IDLE标志位
USART_ClearFlag(USART1,USART_FLAG_TC); //清除USART1标志位
DMA_Cmd(DMA1_Channel5, DISABLE ); //关闭USART1 RX DMA1 所指示的通道
USART1_RX_LEN = USART1_MAX_RX_LEN - DMA1_Channel5->CNDTR; //获得接收到的字节数
UART1[witchbuf1].u1rxSize = USART1_RX_LEN;
UART1[witchbuf1].u1rx1IsRev = 1;
witchbuf1 = witchbuf1 == 0?1:0;
DMA1_Channel5->CMAR=(u32)UART1[witchbuf1].u1rxbuf;
DMA1_Channel5->CNDTR = USART1_MAX_RX_LEN; //DMA通道的DMA缓存的大小
DMA_Cmd(DMA1_Channel5, ENABLE); //使能USART1 RX DMA1 所指示的通道
}
USART_ClearITPendingBit(USART1,USART_IT_ORE); //清除USART1_ORE标志位
}
- u8 USART1_RX_LEN = 0;:定义一个8位无符号整数变量USART1_RX_LEN,用于存储接收到的数据长度。
- if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET);:检查USART1的中断标志位是否被设置。这里检查的是空闲中断(IDLE)标志位。如果该标志位被设置,表示USART1处于空闲状态。
- USART_ReceiveData(USART1);:清除USART1的空闲中断标志位。这行代码的作用是清除IDLE标志位,以便于下一次中断的产生。PS:产生空闲中断后,先读SR,再读DR可以清除空闲中断标志位。
- USART_ClearFlag(USART1,USART_FLAG_TC);:清除USART1的传输完成(TC)标志位。这个标志位用于指示USART1的数据传输已经完成。
- DMA_Cmd(DMA1_Channel5, DISABLE );:关闭USART1 TX的DMA通道。这里禁用DMA通道,以停止数据传输。
- USART1_RX_LEN = USART1_MAX_RX_LEN - DMA1_Channel5->CNDTR;:计算实际接收到的字节数。这里通过从最大接收长度中减去DMA当前未传输的字节数来获得实际接收长度。
- 更新UART1数组中的接收缓冲区和相关标志位,并重新配置DMA通道以开始新的数据传输:
1.UART1[witchbuf1].u1rxSize = USART1_RX_LEN;:设置接收缓冲区的大小为实际接收长度。
2.UART1[witchbuf1].u1rx1IsRev = 1;:设置一个标志位,可能是用来指示缓冲区中的数据是否反转。
3.witchbuf1 = witchbuf1 == 0?1:0;:切换接收缓冲区的索引。
- DMA1_Channel5->CMAR=(u32)UART1[witchbuf1].u1rxbuf;:设置DMA的内存地址,即新的接收缓冲区的地址。
- DMA1_Channel5->CNDTR = USART1_MAX_RX_LEN;:设置DMA传输的数据量,即最大接收长度。
- DMA_Cmd(DMA1_Channel5, ENABLE);:重新开启USART1 TX的DMA通道,开始新的DMA传输。
- USART_ClearITPendingBit(USART1,USART_IT_ORE);:清除USART1的溢出错误(ORE)标志位。这行代码的作用是清除可能出现的溢出错误标志位,确保下一次数据传输的正确性。
DMA发送数据函数
//DMA发送应用源码
void DMA_USART1_Tx_Data(u8 *buffer, u32 size)
{
while(USART1_TX_FLAG); //等待上一次发送完成(USART1_TX_FLAG为1即还在发送数据)
USART1_TX_FLAG=1; //USART1发送标志(启动发送)
DMA1_Channel4->CMAR = (uint32_t)buffer; //设置要发送的数据地址
DMA1_Channel4->CNDTR = size; //设置要发送的字节数目
DMA_Cmd(DMA1_Channel4, ENABLE); //开始DMA发送
}
- CMAR 是“内存地址寄存器”(Channel Memory Address Register)。这个寄存器用于存储DMA传输的目标内存地址。
- CNDTR 是“传输计数寄存器”(Channel Number of Data Register)。这个寄存器用于存储DMA传输的数据块大小。
-例如:DMA_USART1_Tx_Data(UART1.u1rxbuf, UART1.u1rxSize);该语句是将USART1接收到的数据通过DMA发送给USART1,即实现了测试中的回显功能函数。
uart1.h
#include "stdio.h"
#include "sys.h"
#include "delay.h"
#include "string.h"
#include <stdarg.h>
#define USART1_MAX_TX_LEN 1000 //最大发送缓存字节数
#define USART1_MAX_RX_LEN 1000 //最大接收缓存字节数
typedef struct
{
u8 u1rxbuf[USART1_MAX_RX_LEN];
u8 u1rx1IsRev;
u16 u1rxSize;
}UART1_DEF;
extern u8 USART1_TX_BUF[USART1_MAX_TX_LEN]; //发送缓冲区
extern u8 witchbuf1; //标记当前使用的是哪个缓冲区,0,使用u1rxbuf;1,使用u2rxbuf;
extern u8 USART1_TX_FLAG;
extern u8 USART1_RX_FLAG;
extern UART1_DEF UART1[2];
void Initial_UART1(unsigned long baudrate);
void DMA1_USART1_Init(void);
void DMA_USART1_Tx_Data(u8 *buffer, u32 size);
void USART1_printf(char *format, ...);
#endif
4 实验现象
- 第一段信息:STM32通过串口1往串口助手发送数据,串口助手显示接收到的数据
- 第二段信息:串口助手通过串口1往STM32发送数据,串口助手显示发送的数据和STM32接收到的数据回显至串口助手。
总结
利用串口+DMA+双缓存的方案可以大大提高数据传输效率、降低CPU工作量、提高系统稳定性、支持实时处理。是非常值得掌握的一种串口收发方法。