一、前言
1.1 应用场景
UART是一种应用广泛的通信接口,使用简单,绝大部分MCU都有这种接口。
MCU端如果想接收串口的数据,简单的做法是在“接收寄存器非空”中断中一个字节一个字节的接收,这种方式代码处理起来简单些,一般情况下够用,但是如果接收的数据量非常大就不太合适,此外,这种方式需要CPU频繁处理中断,占用大量CPU时间不说,MCU性能低点还容易丢数据。
这是就需要用DMA来进行批量数据的接收,APM32的大部分UART都配有DMA,利用UART的空闲中断和DMA传输完成中断实现一次中断接收多个数据。
1.2 APM32的相关资源
想用DMA接收前提是改UART支持DMA功能,对于APM32E103来说除了UART5,其他串口都是有DMA功能的。

这次我们用DMA进行多个数据接收,串口中断源只需要用到"空闲中断"即可。DMA传输完成中断只有在传输完特定数量数据后才触发,接收到的数据不足则不触发;而空闲中断则在超过1个字节时间没有收到数据时触发,空闲中断还是很有必要,因为对方发过来的数据往往是随机的。

其中,“总线空闲”(IDLE)正是我们所要使用的关键中断源。手册里对空闲帧的处理机制说明如下:

对应的状态标志位定义如下,当检测到空闲总线时硬件会置位该标志:

这次我们以USART1为例,USART1的RX对应是DMA1的通道5。

二、乒乓操作算法
在用DMA接收数据如果只有一个数组进行缓存,那存在一个问题,在处理这些刚数据时,后面发过来的数据会覆盖这个缓存,导致数据错乱;如果直接在中断服务中先处理数据再开启下一次DMA传输,则很可能因为DMA开晚了导致丢失部分数据。
想解决以上问题,最好就是使用双缓存组成乒乓操作,DMA传输使用一个缓存,数据的解析解析处理使用另一个缓存,每次DMA传输完成修改地址切换成另一个缓存,这样一个缓存用于写入,另一个缓存用于读取,解决了接收速度和解析速度不匹配的问题,这种操作也叫乒乓操作。

弄懂乒乓操作后,下面就来编写代码,先建立一个乒乓数据类型,用来存储该算法需要的所有相关信息。
define MaxBufferSize 100 //单次最大接收长度
typedef struct
{
unsigned char buffer[2][MaxBufferSize+4];
unsigned int length[2];
unsigned char w_index;
unsigned char r_index;
}PingpongDef;
为了防止某些变量默认值不是0而产生一些BUG,编写一个初始化函数进行关键变量清零:
//乒乓缓存初始化
void pingpong_init(void)
{
comBuf.w_index = 0;
comBuf.r_index = 0;
comBuf.length[0] = 0;
comBuf.length[1] = 0;
}
编写缓存的写入函数,实现DMA传输完成中断或串口空闲中断的乒乓操作,写操作主要是要记录此次接收数据长度并切换DMA传输地址。
//乒乓缓存写
//unsigned int len: 本次写入的数据长度
//返回:下次写入的缓存地址
unsigned char *pingpong_write(unsigned int len)
{
comBuf.length[comBuf.w_index] = len;
comBuf.r_index = comBuf.w_index;
comBuf.w_index ^= 0x01;
return comBuf.buffer[comBuf.w_index];
}
编写缓存的读取函数,实现在while(1)中的数据解析前的乒乓操作,读操作时先判断这次的数据长度,如果确实有数据则输出这次数据的长度和指针,给后面代码使用。
//乒乓缓存读
//unsigned int *len:读出的数据长度
//返回:读出的数据地址
unsigned char *pingpong_read(unsigned int *len)
{
unsigned char *p;
*len = comBuf.length[comBuf.r_index];
if (*len == 0)
{
p = NULL;
}
else
{
p = comBuf.buffer[comBuf.r_index];
comBuf.length[comBuf.r_index] = 0;
}
return p;
}
这三个函数属于纯算法,可以用于后面代码调用。
void pingpong_init(void);
unsigned char *pingpong_write(unsigned int len);
unsigned char *pingpong_read(unsigned int *len);
三、UART和DMA的代码
3.1、UART和DMA的初始化
首先是USART1的配置,这部分和正常的USART配置相同。
接着是DMA的配置,也就是 DMA1_Channel5 的配置,外设地址设为USART的基地址+0x04。

外设地址不增,储存地址自增,数据大小都设置为字节,循环模式设置为禁止,内存地址设为之前的乒乓缓存的第1个数组,内存大小也就是DMA单次最大传输长度,这里设为100,传输方向设为从外设到内存。
最后使能USART的DMA接收功能,使能DMA通道,使能DMA的传输完成中断,使能串口的空闲中断。
//UART + DMA初始化
void uart_dma_init(void)
{
GPIO_Config_T GPIO_ConfigStruct;
USART_Config_T USART_ConfigStruct;
RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOA | RCM_APB2_PERIPH_USART1);
//USART config
GPIO_ConfigStruct.mode = GPIO_MODE_AF_PP;
GPIO_ConfigStruct.pin = GPIO_PIN_9;
GPIO_ConfigStruct.speed = GPIO_SPEED_10MHz;
GPIO_Config(GPIOA, &GPIO_ConfigStruct);
GPIO_ConfigStruct.mode = GPIO_MODE_IN_FLOATING;
GPIO_ConfigStruct.pin = GPIO_PIN_10;
GPIO_ConfigStruct.speed = GPIO_SPEED_10MHz;
GPIO_Config(GPIOA, &GPIO_ConfigStruct);
USART_ConfigStruct.baudRate = UartBaudrate;
USART_ConfigStruct.hardwareFlow = USART_HARDWARE_FLOW_NONE;
USART_ConfigStruct.mode = USART_MODE_TX_RX;
USART_ConfigStruct.parity = USART_PARITY_NONE;
USART_ConfigStruct.stopBits = USART_STOP_BIT_1;
USART_ConfigStruct.wordLength = USART_WORD_LEN_8B;
USART_Config(USART1, &USART_ConfigStruct);
USART_Enable(USART1);
//DMA config
RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_DMA1);
DMA_Reset(DMA1_Channel4);
DMA_Reset(DMA1_Channel5);
DMA_Config_T DMA_ConfigStruct;
DMA_ConfigStruct.peripheralBaseAddr = (uint32_t)(USART1_BASE + 0x04);
DMA_ConfigStruct.peripheralInc = DMA_PERIPHERAL_INC_DISABLE;
DMA_ConfigStruct.memoryInc = DMA_MEMORY_INC_ENABLE;
DMA_ConfigStruct.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_BYTE;
DMA_ConfigStruct.memoryDataSize = DMA_MEMORY_DATA_SIZE_BYTE;
DMA_ConfigStruct.loopMode = DMA_MODE_NORMAL;
DMA_ConfigStruct.M2M = DMA_M2MEN_DISABLE;
DMA_ConfigStruct.priority = DMA_PRIORITY_HIGH;
//USART1_RX DMA1_CH5
DMA_ConfigStruct.memoryBaseAddr = (uint32_t)comBuf.buffer[0];;
DMA_ConfigStruct.bufferSize = MaxBufferSize;
DMA_ConfigStruct.dir = DMA_DIR_PERIPHERAL_SRC;
DMA_Config(DMA1_Channel5, &DMA_ConfigStruct);
USART_EnableDMA(USART1, USART_DMA_RX);
DMA_Enable(DMA1_Channel5); //使能DMA通道
DMA_EnableInterrupt(DMA1_Channel5, DMA_INT_TC);
USART_EnableInterrupt(USART1, USART_INT_IDLE);
NVIC_EnableIRQRequest(USART1_IRQn, 0, 1);
NVIC_EnableIRQRequest(DMA1_Channel5_IRQn, 0, 0);
}
3.2 中断服务函数的处理
在两个中断服务函数中还要实现数据的接收操作,首先是串口空闲中断,进入串口空闲中断后,先清除空闲中断标志,然后读取DMA的传输长度,接着获取下一个待切换的缓存地址,重新配置长度并开启DMA通道,中断中的代码尽量少,避免执行时间过长而错过数据。
//串口接收空闲中断
void USART1_IRQHandler(void)
{
if (USART_ReadStatusFlag(USART1, USART_FLAG_IDLE) != RESET)
{
USART_ClearIntFlag(USART1, USART_INT_IDLE);
USART_RxData(USART1);
DMA1_Channel5->CHCFG_B.CHEN = DISABLE;
unsigned short len = MaxBufferSize - DMA1_Channel5->CHNDATA; //获取本次DMA传输长度
unsigned char *p = pingpong_write(len);
DMA1_Channel5->CHMADDR = (unsigned int)p;
DMA1_Channel5->CHNDATA = MaxBufferSize;
DMA1_Channel5->CHCFG_B.CHEN = ENABLE;
}
}
DMA传输完成中断的操作和串口空闲中断差不多,不同的只是前面是清除DMA的中断标志。
//DMA接收传输完成中断
void DMA1_Channel5_IRQHandler(void)
{
if (DMA_ReadIntFlag(DMA1_INT_FLAG_TC5) != RESET)
{
DMA_ClearIntFlag(DMA1_INT_FLAG_TC5);
DMA1_Channel5->CHCFG_B.CHEN = DISABLE;
unsigned short len = MaxBufferSize - DMA1_Channel5->CHNDATA; //获取本次DMA传输长度
unsigned char *p = pingpong_write(len);
DMA1_Channel5->CHMADDR = (unsigned int)p;
DMA1_Channel5->CHNDATA = MaxBufferSize;
DMA1_Channel5->CHCFG_B.CHEN = ENABLE;
}
}
四、批量数据接收测试
上面已经实现乒乓写入操作,剩下的就在while(1)中实现乒乓的读取操作,例如对数据进行解析。这里只演示数据的接收数量,把本次接收的数据量和总计接收的数据量通过printf打印出来。
//接收数据的处理..
void buffer_process(void)
{
static unsigned int total_len = 0;
unsigned int cur_len;
unsigned char *buf;
buf = pingpong_read(&cur_len);
if (buf)
{
total_len += cur_len;
printf("%d/%d\r\n", cur_len, total_len); //打印输出本次读取长度和总长度
}
}
最后用串口助手发送一些数据进行测试,每一次都能收数据量都正确。
最重要的测试是发送一个文件,模拟批量数据的发送,以便能测试APM32的串口接收能力。发送一个64KB的文件,最终接收到的数据总数是65536,刚好不多不少。

评论区
登录后即可参与讨论
立即登录