目录
1. DMA
1.1 简介
DMA,全称Direct Memory Access,即直接存储器访问。
DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源。
如果没有不通过DMA,CPU传输数据还要以内核作为中转站,例如将ADC采集的数据转移到SRAM中。
而如果通过DMA的话,DMA控制器将获取到的外设数据存储到DMA通道中,然后通过DMA总线与DMA总线矩阵协调,将数据传输到SRAM中,期间不需内核参与。
主要特征:
- 同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推);
- 独立数据源和目标数据区的传输宽度(字节、半字、全字);
- 可编程的数据传输数目:最大为65535;
- 对于大容量的STM32芯片有2个DMA控制器 两个DMA控制器,DMA1有7个通道,DMA2有5个通道。
1.2 存储器映像
计算机系统的五大组成部分:运算器、控制器、存储器、输入设备和输出设备。
其中运算器和控制器合在一起叫CPU。
STM32所有类型的存储器:
类型 | 起始地址 | 存储器 | 用途 |
ROM | 0x0800 0000 | 程序存储器Flash | 存储C语言编译后的程序代码 |
0x1FFF F000 | 系统存储器 | 存储BootLoader,用于串口下载 | |
0x1FFF F800 | 选项字节 | 存储一些独立于程序代码的配置参数 | |
RAM | 0x2000 0000 | 运行内存SRAM | 存储运行过程中的临时变量 |
0x4000 0000 | 外设寄存器 | 存储各个外设的配置参数 | |
0xE000 0000 | 内核外设寄存器 | 存储内核各个外设的配置参数 |
1.3 DMA框图
在看之前我们先需要搞懂一个概念什么是寄存器,寄存器是一种特殊的存储器,寄存器的每一位后面连接着一根导线,可以操作外设电平的状态,完成如操作引脚电平,开关的打开或者关闭,切换数据选择器,当做计数器等的操作:
所以寄存器可以说是连接软件和硬件的桥梁,软件读写寄存器就相当于在控制硬件的执行。
下面我们来看看DMA的框图:
①:DMA总线访问各个存储器;
②:DMA内部的多个通道进行独立的数据转运;
③:仲裁器用于管理多个通道,防止冲突;
④:DMA从设备用于配置DMA参数;
⑤:DMA请求用于硬件触发DMA的数据转运;
1.4 基本结构
上面的图看不懂,没关系,我们总结一下:
我们拆分一下,先看这部分:
可以看出DMA的转运是后方向的,可以外设到内存,也可以内存到外设,我们可以通过函数进行控制:
#define DMA_DIR_PeripheralDST ((uint32_t)0x00000010)
#define DMA_DIR_PeripheralSRC ((uint32_t)0x00000000)
#define IS_DMA_DIR(DIR) (((DIR) == DMA_DIR_PeripheralDST) || \
((DIR) == DMA_DIR_PeripheralSRC))
然后再来看看二者所需的数据:
首先是基地址,也就是两者的起始地址,这两个参数决定数据从哪里来到哪里去,所需函数:
//外设
uint32_t DMA_PeripheralBaseAddr; /*!< Specifies the peripheral base address for DMAy Channelx. */
//存储器
uint32_t DMA_MemoryBaseAddr; /*!< Specifies the memory base address for DMAy Channelx. */
然后是数据宽度,其作用计时指定一次转运要按多大的数据宽度来进行,其可以选择字节(uint8_t),半字(uint16_t),字(uint32_t):
#define DMA_PeripheralDataSize_Byte ((uint32_t)0x00000000)
#define DMA_PeripheralDataSize_HalfWord ((uint32_t)0x00000100)
#define DMA_PeripheralDataSize_Word ((uint32_t)0x00000200)
#define IS_DMA_PERIPHERAL_DATA_SIZE(SIZE) (((SIZE) == DMA_PeripheralDataSize_Byte) || \
((SIZE) == DMA_PeripheralDataSize_HalfWord) || \
((SIZE) == DMA_PeripheralDataSize_Word))
其函数是:
//外设
uint32_t DMA_PeripheralDataSize; /*!< Specifies the Peripheral data width.
This parameter can be a value of @ref DMA_peripheral_data_size */
//存储器
uint32_t DMA_MemoryDataSize; /*!< Specifies the Memory data width.
This parameter can be a value of @ref DMA_memory_data_size */
地址是否自增,作用是决定下次转运是不是要把地址移到下一个位置去,其参数可以选择使能或者失能:
#define DMA_PeripheralInc_Enable ((uint32_t)0x00000040)
#define DMA_PeripheralInc_Disable ((uint32_t)0x00000000)
#define IS_DMA_PERIPHERAL_INC_STATE(STATE) (((STATE) == DMA_PeripheralInc_Enable) || \
((STATE) == DMA_PeripheralInc_Disable))
其函数是:
//外设
uint32_t DMA_PeripheralInc; /*!< Specifies whether the Peripheral address register is incremented or not.
This parameter can be a value of @ref DMA_peripheral_incremented_mode */
//存储器
uint32_t DMA_MemoryInc; /*!< Specifies whether the memory address register is incremented or not.
This parameter can be a value of @ref DMA_memory_incremented_mode */
然后我们看看另一个参数:传输计数器,这个值表示DMA需要转运几次,你可以将其理解为他是一个自减计数器,假如你初始化的值为5,那么每次转运一次计数减1,当减到0的时候,DMA就不会在进行转运了,并且当其减到0,之前自增的地址又会回到起始地址,方便新一轮的转换:
uint32_t DMA_BufferSize; /*!< Specifies the buffer size, in data unit, of the specified Channel.
The data unit is equal to the configuration set in DMA_PeripheralDataSize
or DMA_MemoryDataSize members depending in the transfer direction. */
那么他是怎么进行新一轮的转换呢?这就要靠自动重装器,其作用就是当转运次数归零后,询问是否将转运次数回到最初值,这样如果我们配置为循环模式,DMA计数归零回到起始地址,而自动重装器又将DMA的数据恢复,这样就可以循环:
#define DMA_Mode_Circular ((uint32_t)0x00000020)
#define DMA_Mode_Normal ((uint32_t)0x00000000)
#define IS_DMA_MODE(MODE) (((MODE) == DMA_Mode_Circular) || ((MODE) == DMA_Mode_Normal))
函数:
uint32_t DMA_Mode; /*!< Specifies the operation mode of the DMAy Channelx.
This parameter can be a value of @ref DMA_circular_normal_mode.
@note: The circular buffer mode cannot be used if the memory-to-memory
data transfer is configured on the selected Channel */
然后就是触发机制,主要配置其使能或者失能,使能软件触发,失能硬件触发:
#define DMA_M2M_Enable ((uint32_t)0x00004000)
#define DMA_M2M_Disable ((uint32_t)0x00000000)
#define IS_DMA_M2M_STATE(STATE) (((STATE) == DMA_M2M_Enable) || ((STATE) == DMA_M2M_Disable))
这里需要注意一点软件触发不能和循环一起使用。
因为软件触发就是想将传输计数器清零,但是循环模式我们上面也说了清零后会进行自动重装,因此不能一起使用。
最后就是,使能DMA,也就是开启DMA:
1.5 触发源选择
对于硬件触发我们需要根据不同的触发源,选择不同的通道,软件触发就随便了:
1.6 数据宽度与对齐
根据下表,简单来说就是右对齐,要是目标宽度不够,取最低位(可以参考第四行),要是目标宽度比源端宽度大则高位补零(可以参考第二或者三行):
2. NVIC
2.1 简介
首先,我们需要知道什么是中断?
中断是在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行。
什么是中断优先级?
中断优先级是:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源。
什么是中断嵌套?
中断嵌套是:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。
STM32F1系列包含最多:68个可屏蔽中断通道,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设。
使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级。
2.2 NVIC基本结构
NVIC:嵌套中断向量控制器
统一分配中断优先级,和管理中断
NVIC内核外设,CPU的小助手
米色圈住部分,意思是:一个外设可能会占用多个中断通道,所以这里有“n”条线,NVIC只有一个输出口,NVIC根据每个中断的优先级进行分配中断的先后顺序。
例如:医院叫号,CPU为医生,NVIC进行排号,中断是病人,NVIC根据病人的紧急程度进行排号,找医生。
2.3 NVIC优先级分组
NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级。
抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队。
对于响应优先级,相当于“插队”,此时“1”在看病(程序在进行中),“4”要是比较严重,“4”可以进行插队到“2”前面,但是要等“1”看完(进程走完):
对于抢占优先级(中断嵌套),相当于“1”在看病(程序在进行中),“4”不等“1”看完直接冲到屋内,把“1”推到一边,“4”先看病(进行),“4”先看完,在进行“1”,再依次进行后续的操作:
在FreeRTOS中我们主要使用的是优先级分组4,即所有优先级都是抢占优先级,这样FreeRTOS能完全控制哪些任务或中断可以抢占当前上下文。
2.4 NVIC初始化
NVIC的配置非常简单:中断分组、定义结构体、配置中断线路、配置优先级、使能:
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //选择配置NVIC的EXTI15_10_IRQn线
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 6; //指定NVIC线路的抢占优先级为6
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //指定NVIC线路的响应优先级为0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
3. 程序设计
在 FreeRTOS 中创建一个任务,该任务的作用是用于接收串口发送的数据,我们采用二值信号量进行处理,当串口接收到数据后,DMA将串口接收到的数据直接存到数组中,无需CPU的参与,当串口检测到总线空闲后触发中断,重新计数DMA的值,并发送信号量通知任务处理。
对于二值信号量,入门篇已经详细解释了,这里不在做过多的描述,我们可以将二值信号量理解为一个非0即1的标志位,详细可以看看,上面一篇是概念介绍,下面是API的调用:
FreeRTOS菜鸟入门(十一)·信号量·二值、计数、递归以及互斥信号量的区别·优先级翻转以及继承机制详解_互斥锁和二值信号量-CSDN博客
对于下面我们想要使用的工程,是我们之前移植好的空白工程,介绍中附带有详细移植链接:
准备工作完成了,开始移植。
3.1 中断配置
我们将我们上面工程的Usart.c的代码更改一下,加一些中断处理,增加系统的稳定性,对于中断的配置我们前面也说了:
//配置嵌套向量中断控制器NVIC
static void NVIC_Configuration(void)
{
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ; //选择配置NVIC的USART1_IRQn线
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 6; //指定NVIC线路的抢占优先级为6
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //指定NVIC线路的响应优先级为0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
}
对于这里为什么这样配置,首先对于为什么需要将优先级分组设为4,我们前面也描述了:我们知道响应优先级虽然也能插队,但是其还是需要等1,做完事情才能轮到它:
但是抢占优先级就不同了,抢占优先级表示我就是老大,无论前面谁做什么事情,都往旁边让让,我先来:
而我们FreeRTOS讲究的就是实时响应,这样配置为分组4,用于16个抢占优先级多么完美。不过抢占优先级也不能随便给,我们在FreeRTOSConfig.h文件做了一些配置:
/******************************************************************
FreeRTOS与中断有关的配置选项
******************************************************************/
#ifdef __NVIC_PRIO_BITS
#define configPRIO_BITS __NVIC_PRIO_BITS
#else
#define configPRIO_BITS 4
#endif
//中断最低优先级
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15
//系统可管理的最高中断优先级
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) ) /* 240 */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
首先我们需要了解,我们FreeRTOS需要靠中断进行计数节拍来进行任务的调度,我们硬件外设调用中断不能优先级比系统节拍还高,这样就会造成系统阻塞,这里我们还要知道两个概念:
中断类型 | 优先级 | 作用 |
---|---|---|
SysTick | 通常为 0(最高) | 系统节拍中断,用于任务调度和时间管理。 |
PendSV | configKERNEL_INTERRUPT_PRIORITY (最低) | 上下文切换中断,确保在无其他中断时切换任务。 |
既然SysTick已经设成0了(最高优先级),但是为什么我们还要将可配置的优先级设为最高到5呢?这是因为剩下的几个保留给,不依赖 FreeRTOS 的高实时性中断(如硬件看门狗),这样我们在设置后续的中断优先级,既不会影响系统实施性,又能做到实时中断。
3.2 串口配置
还是在Usart.c文件,找到串口初始化函数USART_Config()做一些修改,其他配置不用变,增加刚刚配置好的中断函数:
// 串口中断优先级配置
NVIC_Configuration();
同时使能空闲中断,用于检测一帧数据接收完成:
// 开启 串口空闲IDEL 中断
USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE);//使能 串口空闲中断(IDLE),用于检测一帧数据接收完成。
然后开启串口接收DMA转运数据:
// 开启串口DMA接收
USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Rx, ENABLE);
完整修改:
//USART GPIO 配置,工作参数配置
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 打开串口GPIO的时钟
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 打开串口外设的时钟
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
// 将USART Tx的GPIO配置为推挽复用模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
// 将USART Rx的GPIO配置为浮空输入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 配置串口的工作参数
// 配置波特率
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
// 配置 针数据字长
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
// 配置停止位
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(DEBUG_USARTx, &USART_InitStructure);
// 串口中断优先级配置
NVIC_Configuration();
// 开启 串口空闲IDEL 中断
USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE);//使能 串口空闲中断(IDLE),用于检测一帧数据接收完成。
// 开启串口DMA接收
USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Rx, ENABLE);
// 使能串口
USART_Cmd(DEBUG_USARTx, ENABLE);
}
3.3 DMA数据转运初始化配置
这个我们可以根据刚才介绍的这张图进行去配置:
先确认三个宏定义:
// 串口对应的DMA请求通道
#define USART_RX_DMA_CHANNEL DMA1_Channel5
// 外设寄存器地址
#define USART_DR_ADDRESS (&DEBUG_USARTx->DR)
// 一次发送的数据量
#define USART_RBUFF_SIZE 1000
开始初始化配置,首先开启时钟,定义结构体,确认传输方向,我们这是想传输串口的数据,因此外设到存储器:
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);// 开启DMA时钟
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,选择由外设到存储器
然后是对二者格式的配置:
外设基地址:
外设基地址:因为我们是想读取串口数据,因此基地址指向串口的数据寄存器地址(如&USART1->DR);
存储器基地址:我们创建一个缓冲区数组,接收数据存放在其中,基地址指向数组的首地址。
char Usart_Rx_Buf[USART_RBUFF_SIZE];
数据宽度:都是字节。
地址是否自增:
外设:因为我们需要从串口的数据寄存器地址取出数据,因此不能自增。
存储器:为了防止数据覆盖,将数据依次存入缓冲区,地址需要自增。
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART_DR_ADDRESS; //外设基地址,给定形参AddrA
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据宽度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址自增,选择失能
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Usart_Rx_Buf; //存储器基地址,内存地址(要传输的变量的指针)
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能,每次转运后,数组移到下一个位置
然后转运次数,模式为循环模式,因为我们的触发方式是外设到存储器,因此将存储器到存储器失能,优先级因为就一个DMA,因此无所谓,这里就给了中等,然后初始化:
DMA_InitStructure.DMA_BufferSize = USART_RBUFF_SIZE; //转运的数据大小(转运次数)
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //模式,选择循环模式
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //存储器到存储器,选择失能
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; //优先级,选择中等
DMA_Init(USART_RX_DMA_CHANNEL, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1
开始时清除标志位,对于标志位的选择我们上面也说了,硬件DMA转运有其特定的通道,这里我们主要使用的是USART1的接收,因此选择通道5(图可以参考上面DMA触发源选择的图),使能DMA:
// 清除DMA所有标志
DMA_ClearFlag(DMA1_FLAG_TC5);
DMA_ITConfig(USART_RX_DMA_CHANNEL, DMA_IT_TE, ENABLE);
// 使能DMA
DMA_Cmd (USART_RX_DMA_CHANNEL,ENABLE);
完整配置:
char Usart_Rx_Buf[USART_RBUFF_SIZE];
void USARTx_DMA_Config(void)
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);// 开启DMA时钟
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,选择由外设到存储器
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART_DR_ADDRESS; //外设基地址,给定形参AddrA
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据宽度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址自增,选择失能
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Usart_Rx_Buf; //存储器基地址,内存地址(要传输的变量的指针)
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能,每次转运后,数组移到下一个位置
DMA_InitStructure.DMA_BufferSize = USART_RBUFF_SIZE; //转运的数据大小(转运次数)
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //模式,选择循环模式
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //存储器到存储器,选择失能
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; //优先级,选择中等
DMA_Init(USART_RX_DMA_CHANNEL, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1
// 清除DMA所有标志
DMA_ClearFlag(DMA1_FLAG_TC5);
DMA_ITConfig(USART_RX_DMA_CHANNEL, DMA_IT_TE, ENABLE);
// 使能DMA
DMA_Cmd (USART_RX_DMA_CHANNEL,ENABLE);
}
3.4 创建二值信号量
因为我们后续使用DMA数据转运需要二值信号量,我们先来到主函数进行创建一下,首先创建一个二值信号量句柄,方便调用:
SemaphoreHandle_t BinarySem_Handle =NULL;
然后找到AppTaskCreate()函数,调用xSemaphoreCreateBinary()函数创建二值信号量:
/* 创建 BinarySem */
BinarySem_Handle = xSemaphoreCreateBinary();
if(NULL != BinarySem_Handle)
printf("BinarySem_Handle二值信号量创建成功!\r\n");
调用完成的函数:
//任务创建函数
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建 BinarySem */
BinarySem_Handle = xSemaphoreCreateBinary();
if(NULL != BinarySem_Handle)
printf("BinarySem_Handle二值信号量创建成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
注意调用头文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Usart.h"
#include "Key.h"
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
/* 标准库头文件 */
#include <string.h>
3.5 DMA数据转运配置
为了防止在操作DMA寄存器时,新数据写入导致配置冲突或数据损坏,我们先暂停DMA转运:
DMA_Cmd(USART_RX_DMA_CHANNEL, DISABLE); // 暂停DMA
DMA_ClearFlag(DMA1_FLAG_TC5); // 清除传输完成标志
USART_RX_DMA_CHANNEL->CNDTR = USART_RBUFF_SIZE; // 重置计数器
DMA_Cmd(USART_RX_DMA_CHANNEL, ENABLE); // 重启DMA
然后由于这是在中断里,我们调用中断释放二值信号量函数:
//给出二值信号量 ,发送接收到新数据标志,供前台程序查询
xSemaphoreGiveFromISR(BinarySem_Handle,&pxHigherPriorityTaskWoken); //释放二值信号量
该宏用于在中断服务函数(ISR) 中检查是否需要立即进行任务切换。如果 pxHigherPriorityTaskWoken 为 pdTRUE,表示有更高优先级的任务因信号量释放而就绪,此时会触发 PendSV 中断,强制退出当前中断后立即执行任务切换。
//如果需要的话进行一次任务切换,系统会判断是否需要进行切换
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
完整配置:
extern SemaphoreHandle_t BinarySem_Handle;//二值信号量句柄
void Uart_DMA_Rx_Data(void)
{
BaseType_t pxHigherPriorityTaskWoken;
// 关闭DMA ,防止干扰
DMA_Cmd(USART_RX_DMA_CHANNEL, DISABLE);
// 清DMA标志位
DMA_ClearFlag( DMA1_FLAG_TC5 );
// 重新赋值计数值,必须大于等于最大可能接收到的数据帧数目
USART_RX_DMA_CHANNEL->CNDTR = USART_RBUFF_SIZE;
DMA_Cmd(USART_RX_DMA_CHANNEL, ENABLE);
/*
xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken);
*/
//给出二值信号量 ,发送接收到新数据标志,供前台程序查询
xSemaphoreGiveFromISR(BinarySem_Handle,&pxHigherPriorityTaskWoken); //释放二值信号量
//如果需要的话进行一次任务切换,系统会判断是否需要进行切换
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
/*
DMA 开启,等待数据。注意,如果中断发送数据帧的速率很快,MCU来不及处理此次接收到的数据,
中断又发来数据的话,这里不能开启,否则数据会被覆盖。有2种方式解决:
1. 在重新开启接收DMA通道之前,将LumMod_Rx_Buf缓冲区里面的数据复制到另外一个数组中,
然后再开启DMA,然后马上处理复制出来的数据。
2. 建立双缓冲,在LumMod_Uart_DMA_Rx_Data函数中,重新配置DMA_MemoryBaseAddr 的缓冲区地址,
那么下次接收到的数据就会保存到新的缓冲区中,不至于被覆盖。
*/
}
3.6 中断服务函数
这个非常简单,为了防止在数据传输的时候被打扰,进入临界段,然后判断是否接收接收到数据,进行DMA转运,然后退出临界区:
/* 声明引用外部队列 & 二值信号量 */
extern SemaphoreHandle_t BinarySem_Handle;
void DEBUG_USART_IRQHandler(void)
{
uint32_t ulReturn;
/* 进入临界段,临界段可以嵌套 */
ulReturn = taskENTER_CRITICAL_FROM_ISR();
if(USART_GetITStatus(DEBUG_USARTx,USART_IT_IDLE)!=RESET)
{
Uart_DMA_Rx_Data(); /* 释放一个信号量,表示数据已接收 */
USART_ReceiveData(DEBUG_USARTx); /* 清除标志位 */
}
/* 退出临界段 */
taskEXIT_CRITICAL_FROM_ISR( ulReturn );
}
3.7 接收任务创建
既然数据转运完数据了,我们肯定需要处理,所以我们创建一个接收任务,当二值信号量释放后(DMA数据转运成功),接收二值信号量,打印接收到的数据。
首先回到主函数我们先创建一个任务句柄,方便对任务的操作:
static TaskHandle_t KEY_Task_Handle = NULL;/* KEY任务句柄 */
创建任务主体,一直等待二值信号量,收到后LED电平翻转,打印数据,清空数据缓冲区:
static void KEY_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
//获取二值信号量 xSemaphore,没获取到则一直等待
xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */
portMAX_DELAY); /* 等待时间 */
if(pdPASS == xReturn)
{
Toggle_LED_R();
printf("收到数据:%s!\r\n",Usart_Rx_Buf);
memset(Usart_Rx_Buf,0,USART_RBUFF_SIZE);/* 清零 */
}
}
}
找到AppTaskCreate()把任务创建了:
/* 创建KEY_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )KEY_Task, /* 任务入口函数 */
(const char* )"KEY_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&KEY_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建KEY_Task任务成功!\r\n");
3.8 完整main函数
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Usart.h"
#include "Key.h"
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
/* 标准库头文件 */
#include <string.h>
/*
* 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
* 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
* 这个句柄可以为NULL。
*/
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
static TaskHandle_t KEY_Task_Handle = NULL;/* KEY任务句柄 */
/********************************** 内核对象句柄 *********************************/
/*
* 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
* 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
* 们就可以通过这个句柄操作这些内核对象。
*
* 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
* 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
* 来完成的
*
*/
SemaphoreHandle_t BinarySem_Handle =NULL;
/******************************* 宏定义 ************************************/
/*
* 当我们在写应用程序的时候,可能需要用到一些宏定义。
*/
extern char Usart_Rx_Buf[USART_RBUFF_SIZE];
//一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */
static void KEY_Task(void* pvParameters);/* KEY_Task任务实现 */
static void All_Function_Init(void);/* 用于初始化板载相关资源 */
int main(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
All_Function_Init();//硬件初始化
while (1)
{
/* 创建AppTaskCreate任务 */
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )1, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */
/* 启动任务调度 */
if(pdPASS == xReturn)
vTaskStartScheduler(); /* 启动任务,开启调度 */
else
return -1;
}
}
//任务创建函数
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建 BinarySem */
BinarySem_Handle = xSemaphoreCreateBinary();
if(NULL != BinarySem_Handle)
printf("BinarySem_Handle二值信号量创建成功!\r\n");
/* 创建KEY_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )KEY_Task, /* 任务入口函数 */
(const char* )"KEY_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&KEY_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建KEY_Task任务成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
static void KEY_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
//获取二值信号量 xSemaphore,没获取到则一直等待
xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */
portMAX_DELAY); /* 等待时间 */
if(pdPASS == xReturn)
{
Toggle_LED_R();
printf("收到数据:%s!\r\n",Usart_Rx_Buf);
memset(Usart_Rx_Buf,0,USART_RBUFF_SIZE);/* 清零 */
}
}
}
//初始化声明
static void All_Function_Init(void)
{
/*
* STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
USART_Config();
//按键初始化
Key_GPIO_Config();
/* DMA初始化 */
USARTx_DMA_Config();
}
3.9 完整Usart函数
#include "Usart.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
char Usart_Rx_Buf[USART_RBUFF_SIZE];//DMA数据存储缓冲区
//配置嵌套向量中断控制器NVIC
static void NVIC_Configuration(void)
{
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ; //选择配置NVIC的USART1_IRQn线
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 6; //指定NVIC线路的抢占优先级为6
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //指定NVIC线路的响应优先级为0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
}
//USART GPIO 配置,工作参数配置
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 打开串口GPIO的时钟
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 打开串口外设的时钟
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
// 将USART Tx的GPIO配置为推挽复用模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
// 将USART Rx的GPIO配置为浮空输入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 配置串口的工作参数
// 配置波特率
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
// 配置 针数据字长
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
// 配置停止位
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(DEBUG_USARTx, &USART_InitStructure);
// 串口中断优先级配置
NVIC_Configuration();
// 开启 串口空闲IDEL 中断
USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE);//使能 串口空闲中断(IDLE),用于检测一帧数据接收完成。
// 开启串口DMA接收
USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Rx, ENABLE);
// 使能串口
USART_Cmd(DEBUG_USARTx, ENABLE);
}
//DMA初始化
void USARTx_DMA_Config(void)
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);// 开启DMA时钟
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,选择由外设到存储器
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART_DR_ADDRESS; //外设基地址,给定形参AddrA
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据宽度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址自增,选择失能
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Usart_Rx_Buf; //存储器基地址,内存地址(要传输的变量的指针)
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能,每次转运后,数组移到下一个位置
DMA_InitStructure.DMA_BufferSize = USART_RBUFF_SIZE; //转运的数据大小(转运次数)
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //模式,选择循环模式
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //存储器到存储器,选择失能
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; //优先级,选择中等
DMA_Init(USART_RX_DMA_CHANNEL, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1
// 清除DMA所有标志
DMA_ClearFlag(DMA1_FLAG_TC5);
DMA_ITConfig(USART_RX_DMA_CHANNEL, DMA_IT_TE, ENABLE);
// 使能DMA
DMA_Cmd (USART_RX_DMA_CHANNEL,ENABLE);
}
extern SemaphoreHandle_t BinarySem_Handle;//二值信号量句柄
void Uart_DMA_Rx_Data(void)
{
BaseType_t pxHigherPriorityTaskWoken;
// 关闭DMA ,防止干扰
DMA_Cmd(USART_RX_DMA_CHANNEL, DISABLE);
// 清DMA标志位
DMA_ClearFlag( DMA1_FLAG_TC5 );
// 重新赋值计数值,必须大于等于最大可能接收到的数据帧数目
USART_RX_DMA_CHANNEL->CNDTR = USART_RBUFF_SIZE;
DMA_Cmd(USART_RX_DMA_CHANNEL, ENABLE);
/*
xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken);
*/
//给出二值信号量 ,发送接收到新数据标志,供前台程序查询
xSemaphoreGiveFromISR(BinarySem_Handle,&pxHigherPriorityTaskWoken); //释放二值信号量
//如果需要的话进行一次任务切换,系统会判断是否需要进行切换
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
/*
DMA 开启,等待数据。注意,如果中断发送数据帧的速率很快,MCU来不及处理此次接收到的数据,
中断又发来数据的话,这里不能开启,否则数据会被覆盖。有2种方式解决:
1. 在重新开启接收DMA通道之前,将LumMod_Rx_Buf缓冲区里面的数据复制到另外一个数组中,
然后再开启DMA,然后马上处理复制出来的数据。
2. 建立双缓冲,在LumMod_Uart_DMA_Rx_Data函数中,重新配置DMA_MemoryBaseAddr 的缓冲区地址,
那么下次接收到的数据就会保存到新的缓冲区中,不至于被覆盖。
*/
}
/***************** 发送一个字节 **********************/
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch)
{
/* 发送一个字节数据到USART */
USART_SendData(pUSARTx,ch);
/* 等待发送数据寄存器为空 */
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
}
/****************** 发送8位的数组 ************************/
void Usart_SendArray( USART_TypeDef * pUSARTx, uint8_t *array, uint16_t num)
{
uint8_t i;
for(i=0; i<num; i++)
{
/* 发送一个字节数据到USART */
Usart_SendByte(pUSARTx,array[i]);
}
/* 等待发送完成 */
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET);
}
/***************** 发送字符串 **********************/
void Usart_SendString( USART_TypeDef * pUSARTx, char *str)
{
unsigned int k=0;
do
{
Usart_SendByte( pUSARTx, *(str + k) );
k++;
} while(*(str + k)!='\0');
/* 等待发送完成 */
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET)
{}
}
/***************** 发送一个16位数 **********************/
void Usart_SendHalfWord( USART_TypeDef * pUSARTx, uint16_t ch)
{
uint8_t temp_h, temp_l;
/* 取出高八位 */
temp_h = (ch&0XFF00)>>8;
/* 取出低八位 */
temp_l = ch&0XFF;
/* 发送高八位 */
USART_SendData(pUSARTx,temp_h);
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
/* 发送低八位 */
USART_SendData(pUSARTx,temp_l);
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
}
///重定向c库函数printf到串口,重定向后可使用printf函数
int fputc(int ch, FILE *f)
{
/* 发送一个字节数据到串口 */
USART_SendData(DEBUG_USARTx, (uint8_t) ch);
/* 等待发送完毕 */
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);
return (ch);
}
///重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
int fgetc(FILE *f)
{
/* 等待串口输入数据 */
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) == RESET);
return (int)USART_ReceiveData(DEBUG_USARTx);
}
3.10 运行结果
完整工程: