STM32F103学习笔记(8)—— DMA+串口空闲中断接收数据
1.前言DMA:直接存储器访问,优点是有DMA总线进行数据接收,不会占用CPU资源。在我目前负责的项目,之前是使用串口接收中断来判断是否完成数据接收,项目的坑货前任最开始的方法是在串口接收中断里面清除标志,在微系统的数据处理里面对标志位++,判断如果两次没进到接收中断就认为接收数据完成。最开始一帧数据只有77个字节,数据量较少,处理和接收都比较快。因为客户要求增加15个汉字,协议修改了,一次传输3
1. 前言
DMA:直接存储器访问,优点是有DMA总线进行数据接收,不会占用CPU资源。
在我目前负责的项目,之前是使用串口接收中断来判断是否完成数据接收,项目的坑货前任最开始的方法是在串口接收中断里面清除标志,在微系统的数据处理里面对标志位++,判断如果两次没进到接收中断就认为接收数据完成。
最开始一帧数据只有77个字节,数据量较少,处理和接收都比较快。
因为客户要求增加15个汉字,协议修改了,一次传输300+字节,接收中断和数据处理函数耗时较长,可能收到第一帧正在处理时候第二帧就发出来了,在处理完之后再去接收第二帧收到的是不完整的数据。在这个坑上面我改了好多次时间问题,让上位机改变发送时间间隔,从最开始的100ms到后来的500ms,各种修改。我师父每次听到我说改上位机时间就骂我,让我用帧头帧尾判断。
当前的协议只用了一个字节来告诉我一帧数据,一个十六进制的字节最大只能表示255,因此在我接受这一帧数据时候需要先进行多步判断计算,然后得出帧尾位置,再用帧尾来判断是否传输结束。在串口接收中断里面进行多步判断导致接收数据紊乱,因此只能否定这种接收方式。
师傅建议我改成DMA接收方式,于是有了这篇文章。
修改前中断如下:

修改前处理如下(微系统,任务队列,定时运行此方法,因此该方法的运行时间受到其他方法运行时间的影响,所以此方法中的标志位不能快速++,也不能快速解锁串口接收,最终导致接收中断数据紊乱):

2. DMA相关概念
看到我这篇文章时候想必大家都看过一些DMA的简介,这里主要讲述DMA使用,因此不会讲太多DMA概念相关知识。
STM32大容量产品有两个DMA通道,DMA1和DMA2,我这里用的是DMA1。
DMA1有7个通道,可以映射到不同的外设,如ADC、I2C、SPI、TIM等。
通过下图我们可以看到UART5没有DMA映射,而我的憨憨前任画板子时候把数据接收放在了UART5,因此需要飞线把串口1和串口5的位置换一下。
(发出去的机器只能通过修改协议把数据长度用两个字节表示以减少计算量来减少串口中断延时,保证数据接收准确)

通过下图我们可以看到串口1的接收映射到DMA1的通道5,因此后续代码针对DMA1的通道5进行讲述

3. 串口初始化
串口初始化和普通串口初始化一样,如果有同学对串口设置还有疑问的请翻到我前面的串口相关文章查阅。
使用DMA时候初始化唯一和普通串口初始化不同的地方是要加上这一句,开启串口空闲中断。
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);//开启串口空闲中断 ***非常重要***

4. 串口中断
代码如下
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 clearTag;
// if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
if((USART_GetITStatus(USART1, USART_IT_IDLE) != RESET))
{
// USART_ClearITPendingBit(USART1, USART_IT_IDLE);
clearTag=USART1->DR;//软件序列清除IDLE标志位
MYDMA_Config();
}
}
与接收中断一样,空闲中断也是读取空闲中断寄存器判断是否可以进入中断

本章节增加了点新知识,之前清除中断标志位都是使用函数来清除。而实际上中断里面的标志位寄存器在读取之后就会被清除置零,因此随便声明一个变量,用这个变量去读一次相关的寄存器就可以了。

清除完空闲标志位之后,初始化一下DMA配置,这样会让接收数组从0开始存,否则会继续从后面追加存储。这个地方应该可以直接通过寄存器来清零,但是我现在不会用就不写了,后面学会了在加上来。DMA初始化方法在后面。
后面发现只需要重置传输数据量就可以了,也就是简单的重置一个寄存器,毕竟重置初始化方法比重置一个寄存器要消耗的资源多得多。更改后的程序作为优化写在后面。
MYDMA_Config();
5. DMA初始化
代码如下
#include "dma.h"
#include "usart.h"
u8 RX[306];
void MYDMA_Config()
{
DMA_Channel_TypeDef* DMA_CHx;
u16 DMA1_MEM_LEN;
u32 cpar;
u32 cmar;
DMA_CHx=DMA1_Channel5; //通道号
cpar=(u32)&USART1->DR; //源地址
cmar=(u32)℞ //目的地址
DMA1_MEM_LEN=306; //传输数据大小、同时也是DMA缓存大小
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA传输
DMA_DeInit(DMA_CHx); //将DMA的通道1寄存器重设为通道5
DMA_InitStructure.DMA_PeripheralBaseAddr = cpar; //DMA外设基地址
DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA内存基地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,从内存读取发送到外设
DMA_InitStructure.DMA_BufferSize = DMA1_MEM_LEN; //DMA通道的DMA缓存的大小
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_Circular; //工作在正常缓存模式
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; //DMA通道 x拥有中优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
DMA_Init(DMA_CHx, &DMA_InitStructure); //根据DMA_InitStruct中指定的参数初始化DMA的通道USART1_Tx_DMA_Channel所标识的寄存器
DMA_SetCurrDataCounter(DMA_CHx,DMA1_MEM_LEN);
DMA_Cmd(DMA_CHx, ENABLE);
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
}
原谅我在前面写了一堆乱七八糟的定义,因为是直接用原子的代码,他的函数接口里面写了很多定义,需要从主函数传进来。
我这里为了简化代码直接写死,同时为了让大家看清楚哪些是需要自己定义的变量,在前面写了很多的定义。
接收数组全局变量放在外面主要是为了调试时候用。
如下图,我们需要4个自定义的变量来初始化DMA,作用在后面的注释中写的很清楚了。

需要注意的有这个,数据传输方向,这个是从外设到内存。因为我们是把数据从串口传到内存数组里面,因此用这个

可以看到他的头文件里面定义了两种模式,还有一种是从内存到外设,这个可以用在从内存把数据放到存储芯片如24C02或者SD内存卡里面。

还有个注意的地方是这个:
外设地址寄存器不变,是因为串口中断一次只接收一个字节的数据,取出来上一个之后又会把下一个收到的数据放在这个里面,因此他的地址是不需要变化的,也就是地址寄存器不变。
内存地址递增,因为内存地址给的是一个数组地址,数组里面连续存储的数据地址是递增变化的。如果使用地址不变,则会将数据一直放在数组的第一位。

至于这几个设置,通用的设置应该都可以了

这个是告诉DMA要传多少个数据,可以在中断里面得到这个数据判断本次传输多少个数据。

这个是开启UART到DMA的映射,我之前程序运行DMA一直收不到数据就是没开这个的原因。

6. 优化
优化后中断代码如下
u16 countNum;
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 clearTag;
// if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
if((USART_GetITStatus(USART1, USART_IT_IDLE) != RESET))
{
// USART_ClearITPendingBit(USART1, USART_IT_IDLE);
countNum=DMA1_Channel5->CNDTR; //获取剩余应该传输的字节数
clearTag=USART1->DR;//软件序列清除IDLE标志位
DMA_Cmd(DMA1_Channel5,DISABLE); //要关闭之后再设置,否则设置无法生效
DMA_SetCurrDataCounter(DMA1_Channel5,306); //重设长度想当于重置接收数组下标为0
DMA_Cmd(DMA1_Channel5,ENABLE);
}
}
如图,使用上面的方法直接重置传输长度之后,间隔1ms发送数据,数组没有被覆盖,说明本方法想当于前面的重新初始化DMA方法。

剩余数据量,如图,设置一次传输306字节,我们发了13个字节,发现13+291=304,和预计的不一样,仔细看发现串口助手自动在、发送的数据后面拼接了0D 0A,因此13+291+2=306正确。

7. 全部代码
main.c
#include "stm32f10x.h"
#include "delay.h"
#include "led.h"
#include "pwm.h"
#include "usart.h"
#include "dma.h"
extern u8 UartLock;
int main(void)
{
delay_init();
ledInit();
uart_init(115200);
MYDMA_Config();
u16 t;
u16 len;
while(1)
{
LED0=!LED0;//闪烁LED,提示系统正在运行.
delay_ms(500);
}
}
usart.c
#ifndef __USART_H
#define __USART_H
#include "stdio.h"
#include "sys.h"
#define USART_REC_LEN 200 //定义最大接收字节数 200
#define EN_USART1_RX 1 //使能(1)/禁止(0)串口1接收
extern u8 USART_RX_BUF[USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.末字节为换行符
extern u16 USART_RX_STA; //接收状态标记
//如果想串口中断接收,请不要注释以下宏定义
void uart_init(u32 bound);
#endif
usart.c
#include "sys.h"
#include "usart.h"
#include "dma.h"
u8 UartLock=0;
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0)
{
}
//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
#endif
#if EN_USART1_RX //如果使能了接收
//串口1中断服务程序
//注意,读取USARTx->SR能避免莫名其妙的错误
u8 USART_RX_BUF[USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.
//接收状态
//bit15, 接收完成标志
//bit14, 接收到0x0d
//bit13~0, 接收到的有效字节数目
u16 USART_RX_STA=0; //接收状态标记
void uart_init(u32 bound) {
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA时钟
//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
//USART1_RX GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10
//Usart1 NVIC 配置
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化NVIC寄存器
//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//串口波特率
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); //初始化串口1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
USART_Cmd(USART1, ENABLE); //使能串口1
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);//开启串口空闲中断 ***非常重要***
}
u16 countNum;
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 clearTag;
// if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
if((USART_GetITStatus(USART1, USART_IT_IDLE) != RESET))
{
// USART_ClearITPendingBit(USART1, USART_IT_IDLE);
countNum=DMA1_Channel5->CNDTR; //获取剩余应该传输的字节数
clearTag=USART1->DR;//软件序列清除IDLE标志位
DMA_Cmd(DMA1_Channel5,DISABLE); //要关闭之后再设置,否则设置无法生效
DMA_SetCurrDataCounter(DMA1_Channel5,306); //重设长度想当于重置接收数组下标为0
DMA_Cmd(DMA1_Channel5,ENABLE);
}
}
#endif
dma.h
#ifndef __DMA_H
#define __DMA_H
#include "sys.h"
//void MYDMA_Config(DMA_Channel_TypeDef*DMA_CHx,u32 cpar,u32 cmar,u16 cndtr);//配置DMA1_CHx
void MYDMA_Config(void);
#endif
dma.c
#include "dma.h"
#include "usart.h"
u8 RX[306];
void MYDMA_Config()
{
DMA_Channel_TypeDef* DMA_CHx;
u16 DMA1_MEM_LEN;
u32 cpar;
u32 cmar;
DMA_CHx=DMA1_Channel5; //通道号
cpar=(u32)&USART1->DR; //源地址
cmar=(u32)℞ //目的地址
DMA1_MEM_LEN=306; //传输数据大小、同时也是DMA缓存大小
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA传输
DMA_DeInit(DMA_CHx); //将DMA的通道1寄存器重设为通道5
DMA_InitStructure.DMA_PeripheralBaseAddr = cpar; //DMA外设基地址
DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA内存基地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,从内存读取发送到外设
DMA_InitStructure.DMA_BufferSize = DMA1_MEM_LEN; //DMA通道的DMA缓存的大小
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_Circular; //工作在正常缓存模式
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; //DMA通道 x拥有中优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
DMA_Init(DMA_CHx, &DMA_InitStructure); //根据DMA_InitStruct中指定的参数初始化DMA的通道
USART1_Tx_DMA_Channel所标识的寄存器
DMA_Cmd(DMA_CHx, ENABLE);
DMA_SetCurrDataCounter(DMA_CHx,DMA1_MEM_LEN);
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
}
以上。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)