FreeRTOS实战(八)·移植STM32实现双ADC采集DMA转运数据

目录

1.  ADC

1.1  简介

1.2  逐次逼近型ADC(ADC0809)

1.3  ADC框图(STM32)

1.4  ADC基本结构

1.4.1  GPIO口配置

1.4.2  转换器配置

1.4.3  采样时钟选择

1.4.4  转换模式选择

1.4.4.1  单次非扫描模式

1.4.4.2  单次扫描模式

1.4.4.3  连续非扫描模式

1.4.4.4  连续扫描模式

1.4.4.5  配置

1.4.5  触发控制

1.4.6  数据对齐

1.4.7  使能

1.4.8  校准

1.4.9  读取

2.  DMA

2.1  简介

2.2  存储器映像

2.3  DMA框图

2.4  基本结构

2.5  触发源选择

2.6  数据宽度与对齐

3.  程序设计

3.1  初始化GPIO口

3.2  初始化DMA配置

3.3  初始化ADC1配置

3.4  初始化ADC2配置

3.5  初始化文件配置

3.6  任务创建

3.7  完整main函数

3.8  运行结果


        介绍都是重复的介绍,主要让每个章节独立出来,不用每次附上一堆链接跳来跳去,比较麻烦,这里如果前面看过了可以直接跳过先关外设介绍,直接看程序设计的地方。

1.  ADC

1.1  简介

        ADC全称是(Analog-to-Digital Converter)模拟-数字转换器,一般我们把模拟信号(Analog signal) 用A来进行简写,数字信号(digital signal) 用D来表示。主要用于将连续传输的模拟信号转换为数字信号,便于数字系统(如中央处理器CPU、微控制器MCU等)对传输信息进行快速处理和分析。

         模拟信号是指用连续变化的物理量所表达的信息,如温度、湿度、压力、电压、电流等。ADC模块所采集的模拟信号是连续变化的电压或电流信号,其数值在一定范围内连续变化,如下所示:

        在单片机的使用中,我们可以通过ADC的转换将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁。

         ADC采样的实现方式主要有两种:外接采样芯片控制核心内部的采样模块。以前的一些芯片是外挂ADC芯片,不过现在许多MCU和DSP内部集成了ADC模块。我们使用的STM32内部集成了ADC功能,不过我们还是来了解一下外挂芯片的原理,方便理解。

1.2  逐次逼近型ADC(ADC0809)

        我们先来了解一下ADC0809的工作原理。

        首先是通道选择开关,通过通道选择开关,选择(IN0~IN7)其中的一路,输入到这个点进行转换,对于0809只有8路选择,但是STM32有18个输入通道,可测量16个外部和2个内部信号源(一会下面介绍):

        然后是地址锁存和译码,这里你想要选择哪一路,需要将通道号放到(ADDA、ADDB、ADDC)这三个脚上,然后通过ALE给出锁存信号,对应的通道选择开关就会自动拨好了:

        比较器是一个电压比较器,他可以判断两个输入信号电压的大小关系,其中通道选择开关输入的是待测的电压,另一端是DAC(数模转换器)的电压:

        这里待测电压是未知的,但是DAC所出的电压是我们通过数模转换来的,其编码是已知的,我们通过比较器进行比较,当待测电压大于DAC所示电压,我们就调大DAC的值,如果待测电压小于DAC所示电压,我们就调小DAC的值,直到待测电压等于DAC所示电压,这样DAC的输入数据就是待测电压的数据,我们就可以知道待测电压的编码(类似于二分法)。

        得出待测电压的编码通过三态锁存缓冲器进行输出,8位就有8根线,12位就有12根:

1.3  ADC框图(STM32)

        STM32F1系列其芯片内部有多达18个通道,可测量16个外部和2个内部信号源:

        通道检测数据进入搭配模拟多路开关,模拟多路开关将数据输出的“模拟至数字转换器”(这里的作用类似于上面介绍的逐次逼近ADC),将转换数据传至数据寄存器,取出结果:

        对于这里,我们可以将注入通道或者规则通道比作饭店上菜的菜单,注入通道的通道相当于菜单上的菜,菜单上有菜(有数据),厨房做菜(注入通道寄存器接受数据),菜单上有多少菜做多少,从上往下一道一道做(数据逐次注入);

        规则通道有16个通道(也就是菜单上能写16个),但是规则通道数据寄存器只有一个(可以理解为能够上菜的桌子太小只能放一个菜),所以每当有新菜,就会将之前的菜端走,只能保存最后一个数据(桌子太小只能放最后一个菜,无论你前面吃没吃都给端走了),这里就需要搭配DMA来使用(作用就是当接收到一个数据之后,将这个数据挪到其他地方去,防止被覆盖):

        这里相当于ADC0809的START信号(开始转换信号),对于STM32的ADC触发ADC开始转换的信号又两种:软件触发和硬件触发。

        软件触发就是在程序中手动调用一条代码,就可以启动转换。

        硬件触发如下,由于ADC需要经过一个固定时间转换一次,因此我们需要通过定时器进行控制时间,但是若是实时采集数据,会频繁进入定时器,频繁进入中断,而中断又有优先级,若是多处地方需要频繁进入中断,这将会导致程序卡死,不过对于这种需要频繁进入中断,并且中断之进行简单的操作,一般会有硬件的支持:

1.4  ADC基本结构

1.4.1  GPIO口配置

        上面的有点乱感觉不理解,没关系,我们有简化版本:

        首先是输入通道的选择:

        引脚的选择: 

通道

ADC1

ADC2

ADC3

通道0

PA0

PA0

PA0

通道1

PA1

PA1

PA1

通道2

PA2

PA2

PA2

通道3

PA3

PA3

PA3

通道4

PA4

PA4

PF6

通道5

PA5

PA5

PF7

通道6

PA6

PA6

PF8

通道7

PA7

PA7

PF9

通道8

PB0

PB0

PF10

通道9

PB1

PB1

通道10

PC0

PC0

PC0

通道11

PC1

PC1

PC1

通道12

PC2

PC2

PC2

通道13

PC3

PC3

PC3

通道14

PC4

PC4

通道15

PC5

PC5

通道16

温度传感器

通道17

内部参考电压

        假如我们选择PA0作为ADC的通道,那么我们先初始化GPIO口:

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟

	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA0引脚初始化为模拟输入

1.4.2  转换器配置

        然后是ADC转换器的配置,前面我们也说了ADC有规则组和注入组两种转换器,规则组有16个菜单但是只能上最后一个菜,注入组有只有4个菜单但是能全部上桌,看自己需求配置:

        这里我们用规则组,前面也说了我们使用的是PA0的口,那么这里我们使用ADC1的通道0,采样顺序我们设置为1,采样时间我们选择ADC_SampleTime_55Cycles5:

	/*规则组通道配置*/
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);		//规则组序列1的位置,配置为通道0
宏定义采样时间(周期)适用场景
ADC_SampleTime_1Cycles51.5高速、低阻抗信号(如直流电压)
ADC_SampleTime_7Cycles57.5中等速度、中等阻抗
ADC_SampleTime_13Cycles513.5一般模拟信号
ADC_SampleTime_28Cycles528.5较高阻抗信号
ADC_SampleTime_41Cycles541.5高阻抗信号(如传感器)
ADC_SampleTime_55Cycles555.5高阻抗、高精度需求
ADC_SampleTime_71Cycles571.5极高阻抗(如热电偶)
ADC_SampleTime_239Cycles5239.5超长采样(极低噪声需求)

1.4.3  采样时钟选择

        对于ADC的采样时间的了解,在了解前我们先了解一下,AD转换的步骤,其分为:采样,保持,量化,编码。

        对于采样是将连续的模拟信号在时间域上进行离散化的过程。它通过在特定的时间点上获取信号的值,将模拟波形切分为一系列的离散时间片。这些时间片的大小通常与原波形的特征值相匹配,但由于只在有限的时间点上进行测量,部分信息可能会丢失。这种信息的丢失被称为“抽样失真”。

        保持是采集模拟信号后,需花时间将其转化为数字信号,为了给后续的量化编码过程提供一个稳定值,需用保持电路对取得的模拟信号进行电压保持。此过程可通过并联电容的方式实现。输入的连续模拟信号经过采样与保持后将得到一个时间上离散的模拟信号样本集合。

        而这个采样时间就是我们上面设置的时间,这个时间主要为了防止例如尖峰等波动造成影响,对于STM32中ADC采样的总时间:

T_{CONV}=采样时间+12.5个ADC周期

其中采样时间就是我们设置的时间

12.5个ADC周期:是因为STM32是12位逐次逼近型,所以有12个周期,而另外0.5个采样走起是为了例如程序运行时间等给做的一个缓冲。

举个例子:

当ADCCLK=14MHz,采样时间为1.5个ADC周期

T_{CONV}=1.5+12.5=14个ADC周期=1us

这个也是为什么STM32的ADC采样频率最大到1us转换时间

        而对于ADCCLK的设置,最大到14MHz,我们可以通过:

	/*设置ADC时钟*/
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);						//选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz

        对于其中的参数配置:

#define RCC_PCLK2_Div2                   ((uint32_t)0x00000000)
#define RCC_PCLK2_Div4                   ((uint32_t)0x00004000)
#define RCC_PCLK2_Div6                   ((uint32_t)0x00008000)
#define RCC_PCLK2_Div8                   ((uint32_t)0x0000C000)
#define IS_RCC_ADCCLK(ADCCLK) (((ADCCLK) == RCC_PCLK2_Div2) || ((ADCCLK) == RCC_PCLK2_Div4) || \
                               ((ADCCLK) == RCC_PCLK2_Div6) || ((ADCCLK) == RCC_PCLK2_Div8))

        这里有人会说如果我们将参数设为:RCC_PCLK2_Div2或者RCC_PCLK2_Div4不是超过14Mhz了吗,为什么最大只能到14MHz,那是因为这样设置就会超频了,速度虽然更快了,但是稳定性方面就不敢保证了。

        量化是数字信号在时间和幅值上都是离散的,量化是将采样电压转化为离散电平的近似过程。常用的量化方法有只舍不入和四舍五入。量化过程中会产生量化误差,它是一种无法消除的原理性误差。ADC的位数越高,离散电平之间的差值越小,量化误差也会越小。以参考电压3.3V的12位ADC采样模块为例,输入模拟电压与量化后产生的数值之间的关系如下:

        编码为方便数字信号数据的传输与存储,需要将量化得到的十进制数字信号转换成二进制编码。常用的编码方式有二进制编码、格雷编码、调制编码和二进制补码编码等。

1.4.4  转换模式选择

        STM32有四种选择模式,分别是:单次非扫描模式、单次扫描模式、连续非扫描模式以及连续扫描模式。

1.4.4.1  单次非扫描模式

        只有第一个序列1的位置有效,在其中选择转换的通道,例如选择通道2,这样ADC就会对通道2进行模数转换,过一会转换完成,将其放到数据寄存器里,将EOC标志位置1,表示转换完成:

1.4.4.2  单次扫描模式

        其与非扫描模式不同的是,非扫描模式只使用了序列1,而扫描模式将下面序列使用了起来,在结构体初始化时,需要初始化通道数目:

1.4.4.3  连续非扫描模式

        这个和单次转换非扫描模式有些类似,但是不同的是,这个在转换结束后不需要停止,可以直接进行下一次转换,不需要多次触发:

1.4.4.4  连续扫描模式

        这个就是在单次转换扫描模式基础上,使其连续触发:

1.4.4.5  配置

        首先是上面四种模式的配置,我们可以通过开启和关闭一下两个函数进行调整:

	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;		//连续转换,失能,每转换一次规则组序列后停止
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;			//扫描模式,失能,只转换规则组的序列1这一个位置

        假如我们采用单次非扫描,那么我们就需要将通道数设为1:

	ADC_InitStructure.ADC_NbrOfChannel = 1;					//通道数,为1,仅在扫描模式下,才需要指定大于1的数,在非扫描模式下,只能是1

        由于这里我们只使用了PA0,只是一个通道,因此选择单通道模式,对于这一块的配置:

ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;		//模式,选择独立模式,即单独使用ADC1

        其他的模式如下:

/* ADC(模数转换器)工作模式配置宏定义 */

#define ADC_Mode_Independent                       ((uint32_t)0x00000000)// 独立模式:每个ADC独立工作,不与其他ADC同步
#define ADC_Mode_RegInjecSimult                    ((uint32_t)0x00010000)// 规则组和注入组同步模式:规则通道和注入通道同时转换
#define ADC_Mode_RegSimult_AlterTrig               ((uint32_t)0x00020000)// 规则组同步 + 交替触发模式:多个ADC同步采样规则通道,并使用交替触发
#define ADC_Mode_InjecSimult_FastInterl            ((uint32_t)0x00030000)// 注入组同步 + 快速交替模式:注入通道同步采样,并结合快速交替采样
#define ADC_Mode_InjecSimult_SlowInterl            ((uint32_t)0x00040000)// 注入组同步 + 慢速交替模式:注入通道同步采样,并结合慢速交替采样
#define ADC_Mode_InjecSimult                       ((uint32_t)0x00050000)// 注入组同步模式:多个ADC的注入通道同步采样
#define ADC_Mode_RegSimult                         ((uint32_t)0x00060000)// 规则组同步模式:多个ADC的规则通道同步采样
#define ADC_Mode_FastInterl                        ((uint32_t)0x00070000)// 快速交替模式:ADC之间快速交替采样(适用于高频率采样)
#define ADC_Mode_SlowInterl                        ((uint32_t)0x00080000)// 慢速交替模式:ADC之间慢速交替采样(适用于低频率采样)
#define ADC_Mode_AlterTrig                         ((uint32_t)0x00090000)// 交替触发模式:多个ADC通过交替触发信号进行采样

/* 检查ADC模式是否有效的宏 */
// 判断给定的MODE参数是否是上述合法的ADC模式之一
#define IS_ADC_MODE(MODE) (((MODE) == ADC_Mode_Independent) || \
                           ((MODE) == ADC_Mode_RegInjecSimult) || \
                           ((MODE) == ADC_Mode_RegSimult_AlterTrig) || \
                           ((MODE) == ADC_Mode_InjecSimult_FastInterl) || \
                           ((MODE) == ADC_Mode_InjecSimult_SlowInterl) || \
                           ((MODE) == ADC_Mode_InjecSimult) || \
                           ((MODE) == ADC_Mode_RegSimult) || \
                           ((MODE) == ADC_Mode_FastInterl) || \
                           ((MODE) == ADC_Mode_SlowInterl) || \
                           ((MODE) == ADC_Mode_AlterTrig))

1.4.5  触发控制

        对于STM32的触发,前面我们也说了有软件和硬件两种:

ADC1和ADC2用于规则通道的外部触发:

        配置如下: 

	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//外部触发,使用软件触发,不需要外部触发

        是STM32F1系列的一些触发源:

/* ADC(模数转换器)外部触发源配置宏定义 */

// ============= 适用于ADC1和ADC2的触发源 =============
// TIM1(定时器1)的CC1(捕获/比较通道1)事件触发
#define ADC_ExternalTrigConv_T1_CC1                ((uint32_t)0x00000000)
// TIM1的CC2(捕获/比较通道2)事件触发
#define ADC_ExternalTrigConv_T1_CC2                ((uint32_t)0x00020000)
// TIM2的CC2(捕获/比较通道2)事件触发
#define ADC_ExternalTrigConv_T2_CC2                ((uint32_t)0x00060000)
// TIM3的TRGO(触发输出)事件触发
#define ADC_ExternalTrigConv_T3_TRGO               ((uint32_t)0x00080000)
// TIM4的CC4(捕获/比较通道4)事件触发
#define ADC_ExternalTrigConv_T4_CC4                ((uint32_t)0x000A0000)
// 外部中断线11或TIM8的TRGO事件触发
#define ADC_ExternalTrigConv_Ext_IT11_TIM8_TRGO    ((uint32_t)0x000C0000)

// ============= 适用于ADC1/2/3的通用触发源 =============
// TIM1的CC3(捕获/比较通道3)事件触发
#define ADC_ExternalTrigConv_T1_CC3                ((uint32_t)0x00040000)
// 不使用外部触发(软件触发模式)
#define ADC_ExternalTrigConv_None                  ((uint32_t)0x000E0000)

// ============= 仅适用于ADC3的专用触发源 =============
// TIM3的CC1(捕获/比较通道1)事件触发
#define ADC_ExternalTrigConv_T3_CC1                ((uint32_t)0x00000000)
// TIM2的CC3(捕获/比较通道3)事件触发
#define ADC_ExternalTrigConv_T2_CC3                ((uint32_t)0x00020000)
// TIM8的CC1(捕获/比较通道1)事件触发
#define ADC_ExternalTrigConv_T8_CC1                ((uint32_t)0x00060000)
// TIM8的TRGO(触发输出)事件触发
#define ADC_ExternalTrigConv_T8_TRGO               ((uint32_t)0x00080000)
// TIM5的CC1(捕获/比较通道1)事件触发
#define ADC_ExternalTrigConv_T5_CC1                ((uint32_t)0x000A0000)
// TIM5的CC3(捕获/比较通道3)事件触发
#define ADC_ExternalTrigConv_T5_CC3                ((uint32_t)0x000C0000)

/* 检查外部触发源是否有效的宏 */
// 判断给定的REGTRIG参数是否是上述合法的ADC触发源之一
#define IS_ADC_EXT_TRIG(REGTRIG) (((REGTRIG) == ADC_ExternalTrigConv_T1_CC1) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T1_CC2) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T1_CC3) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T2_CC2) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T3_TRGO) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T4_CC4) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_Ext_IT11_TIM8_TRGO) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_None) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T3_CC1) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T2_CC3) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T8_CC1) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T8_TRGO) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T5_CC1) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T5_CC3))

1.4.6  数据对齐

        我们知道STM32的ADC采集数据是12位的,但是我们的数据要存到数据寄存器,但是数据寄存器是16位的,这就需要我们将数据进行对齐了,有两种方法:左对齐和右对齐。

数据右对齐:

数据左对齐:

        一个是高位补零,一个是低位补零,一般情况下我们都是使用数据右对齐,这样读取出来的数据就是原值,而如果要是使用左对齐,因为左移了四位,对于二进制而言,就需要将最终数据除以16(2的4次方),才能得到原值。

这里可能会有问,既然右对齐还要多操作一步,那么其存在的意义是干什么呢?对于右对齐适用于精度比较高的数据,左对齐适用于不需要我们进行高精度的东西,我们可直接去高8位数据,舍弃后面的四位,这样不需要这个高的精度。

        配置:

	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//数据对齐,选择右对齐

        这个参数就比较少了,一个左,一个右,看需求:

#define ADC_DataAlign_Right                        ((uint32_t)0x00000000)
#define ADC_DataAlign_Left                         ((uint32_t)0x00000800)
#define IS_ADC_DATA_ALIGN(ALIGN) (((ALIGN) == ADC_DataAlign_Right) || \
                                  ((ALIGN) == ADC_DataAlign_Left))

1.4.7  使能

        这没啥,就ADC使能,用哪个使能哪个:

	/*ADC使能*/
	ADC_Cmd(ADC1, ENABLE);									//使能ADC1,ADC开始运行

1.4.8  校准

        ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差。

        启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期。

        建议在每次上电后执行一次校准。

/* ADC校准流程 */
// 1. 复位ADC1的校准寄存器(启动校准复位)
ADC_ResetCalibration(ADC1);		// 固定流程,内部有专用校准电路会自动执行校准操作

// 2. 等待校准复位完成(硬件会自动清除标志位)
while (ADC_GetResetCalibrationStatus(ADC1) == SET); // 循环检测直到复位完成

// 3. 启动ADC1的校准过程
ADC_StartCalibration(ADC1);		// 开始实际校准过程

// 4. 等待校准完成
while (ADC_GetCalibrationStatus(ADC1) == SET);	// 循环检测直到校准完成

1.4.9  读取

        上面已经完成了ADC的初始化操作,那么我们如何进行数据的读取呢?STM32给我们封装了三个函数,首先:

ADC_SoftwareStartConvCmd(ADC1, ENABLE);

        其作用是在开始前调用软件触发ADC1开始一次转换(无需外部触发信号),调用后,ADC硬件会自动开始采样和转换。


ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)

        ADC转换完成标志位,其中 ADC_FLAG_EOC 转换结束标志位,硬件自动置位。若其返回值为 RESET 表示标志位未就绪(STM32库中通常定义为0),在裸机开发中我们可以通过while()循环去阻塞式等待,来完成检测ADC是否完成采集,如:

while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);

        但是如果我们要将其运用到FreeRTOS当中,这种方法是不行的,首先循环会持续占用CPU,直到ADC转换完成,这样的阻塞会阻止其他任务运行,破坏实时性,而我们使用FreeRTOS的初衷就是实时操作,这样肯定是不行的。

        那么我们要使用什么方法呢?我们可以通过中断来实现,通过任务通知来传递数据,具体实现方法下面实例进行讲解。


ADC_GetConversionValue(ADC1);

        从ADC数据寄存器中读取12位转换结果(STM32的ADC分辨率为12位时,范围0~4095),读取后,EOC标志位会自动清除。

2.  DMA

2.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个通道。

2.2  存储器映像

        计算机系统的五大组成部分:运算器、控制器、存储器、输入设备和输出设备。

        其中运算器和控制器合在一起叫CPU。

STM32所有类型的存储器:

2.3  DMA框图

        在看之前我们先需要搞懂一个概念什么是寄存器,寄存器是一种特殊的存储器,寄存器的每一位后面连接着一根导线,可以操作外设电平的状态,完成如操作引脚电平,开关的打开或者关闭,切换数据选择器,当做计数器等的操作:

所以寄存器可以说是连接软件和硬件的桥梁,软件读写寄存器就相当于在控制硬件的执行。

下面我们来看看DMA的框图:

①:DMA总线访问各个存储器;

②:DMA内部的多个通道进行独立的数据转运;

③:仲裁器用于管理多个通道,防止冲突;

④:DMA从设备用于配置DMA参数;

⑤:DMA请求用于硬件触发DMA的数据转运;

2.4  基本结构

        上面的图看不懂,没关系,我们总结一下:

        我们拆分一下,先看这部分:

        可以看出DMA的转运是后方向的,可以外设到内存,也可以内存到外设,我们可以通过函数进行控制:

        然后再来看看二者所需的数据:

        首先是基地址,也就是两者的起始地址,这两个参数决定数据从哪里来到哪里去,所需函数:

//外设
  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:

2.5  触发源选择

        对于硬件触发我们需要根据不同的触发源,选择不同的通道,软件触发就随便了:

2.6  数据宽度与对齐

        根据下表,简单来说就是右对齐,要是目标宽度不够,取最低位(可以参考第四行),要是目标宽度比源端宽度大则高位补零(可以参考第二或者三行):

3.  程序设计

        非常简单的功能,在FreeRTOS中创建两个任务,一个用来接收DMA转运数据,并进行处理,一个用来挂起和恢复接收任务。对于ADC的我们采用双ADC采集模式。

        对于下面我们想要使用的工程,是我们之前移植好的空白工程,介绍中附带有详细移植链接:

基于STM32F1系列FreeRTOS移植模版资源-CSDN文库

        准备工作完成了,开始移植。

3.1  初始化GPIO口

        为了方便代码维护,我们创建一些宏定义,想要更改引脚直接对宏定义进行更改即可,这里我们进行双ADC采集,所以定义两个GPIO:

#define    ADCx_1_GPIO_APBxClock_FUN        RCC_APB2PeriphClockCmd
#define    ADCx_1_GPIO_CLK                  RCC_APB2Periph_GPIOC  
#define    ADCx_1_PORT                      GPIOC
#define    ADCx_1_PIN                       GPIO_Pin_1

#define    ADCx_2_GPIO_APBxClock_FUN        RCC_APB2PeriphClockCmd
#define    ADCx_2_GPIO_CLK                  RCC_APB2Periph_GPIOC  
#define    ADCx_2_PORT                      GPIOC
#define    ADCx_2_PIN                       GPIO_Pin_4

        GPIO口初始化:

static void ADCx_GPIO_Config(void)
{	
	// 配置 ADC IO 引脚模式
	// 必须为模拟输入
	GPIO_InitTypeDef GPIO_InitStructure;//初始化结构体

	//开启GPIO时钟
	ADCx_1_GPIO_APBxClock_FUN ( ADCx_1_GPIO_CLK, ENABLE );
	//等价于RCC_APB2PeriphClockCmd( ADC_GPIO_CLK, ENABLE );
	GPIO_InitStructure.GPIO_Pin = ADCx_1_PIN;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//在模拟输入模式下速度设置不影响功能,设置不设置都一样
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_Init(ADCx_1_PORT, &GPIO_InitStructure);// 初始化 ADC IO			

	ADCx_1_GPIO_APBxClock_FUN ( ADCx_2_GPIO_CLK, ENABLE );
	GPIO_InitStructure.GPIO_Pin = ADCx_2_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_Init(ADCx_2_PORT, &GPIO_InitStructure);	
}

3.2  初始化DMA配置

        这个我们可以根据刚才介绍的这张图进行去配置:

        开始初始化配置,首先开启时钟,定义结构体,确认传输方向,我们这是想传输串口的数据,因此外设到存储器,并且开始前先复位一下DMA,清除残存数据:

	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);// 开启DMA时钟

	/*DMA初始化*/
	DMA_InitTypeDef DMA_InitStructure;											//定义结构体变量

	DMA_DeInit(ADC_DMA_CHANNEL);// 复位DMA控制器

	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;							//数据传输方向,选择由外设到存储器

然后是对二者格式的配置:

外设基地址:

        外设基地址:因为我们是想读取ADC的数据,因此基地址指向ADC的数据寄存器地址,如(uint32_t)(&( ADC1->DR)),这里有个细节,我们使用的是双ADC采集,那么外设基地址为什么只有ADC1的呢,那是因为,当我们将ADC配置成单ADC模式的时候,只使用了低16位(DATA[15:0]),而如果使用双ADC模式,将会调用ADC2DATA[15:0]进行存储ADC2的通道数据,可以翻看一下数据手册:

        存储器基地址:我们创建一个缓冲区数组,接收数据存放在其中,基地址指向缓冲区地址。

数据宽度:选择字,上面我们也说了,当配置成双ADC的时候会采集ADC1数据寄存器的数据,而此时的数据是32位。

地址是否自增:

        外设:因为我们需要从ADC的数据寄存器地址取出数据,因此不能自增。

        存储器:防止覆盖自增。

	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&( ADCx_1->DR));			      //外设基地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;	    //外设数据宽度
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;			          //外设地址自增,选择失能

	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&ADC_ConvertedValue;			      //存储器基地址,内存地址(要传输的变量的指针)
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;			        //存储器数据宽度
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;						            //存储器地址自增

        然后转运次数,模式为循环模式,因为我们的触发方式是外设到存储器,因此将存储器到存储器失能,优先级因为就一个DMA,因此无所谓,这里就给了中等,然后初始化:

	DMA_InitStructure.DMA_BufferSize = NOFCHANEL;						                            //转运的数据大小(转运次数)
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;							            	  //模式,选择循环模式
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;							    	            //存储器到存储器,选择失能
	DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;					            //优先级,选择中等
	DMA_Init(ADC_DMA_CHANNEL, &DMA_InitStructure);							                //将结构体变量交给DMA_Init,配置DMA1的通道11

        然后使能DMA:

	// 使能DMA
	DMA_Cmd (ADC_DMA_CHANNEL,ENABLE);

        完整:

//DMA初始化配置
static void ADC_DMA_Config(void)
{
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);// 开启DMA时钟

	/*DMA初始化*/
	DMA_InitTypeDef DMA_InitStructure;											//定义结构体变量

	DMA_DeInit(ADC_DMA_CHANNEL);// 复位DMA控制器

	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;							//数据传输方向,选择由外设到存储器

	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&( ADCx_1->DR));			      //外设基地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;	    //外设数据宽度
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;			          //外设地址自增,选择失能

	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&ADC_ConvertedValue;			      //存储器基地址,内存地址(要传输的变量的指针)
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;			        //存储器数据宽度
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;						            //存储器地址自增

	DMA_InitStructure.DMA_BufferSize = NOFCHANEL;						                            //转运的数据大小(转运次数)
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;							            	  //模式,选择循环模式
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;							    	            //存储器到存储器,选择失能
	DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;					            //优先级,选择中等
	DMA_Init(ADC_DMA_CHANNEL, &DMA_InitStructure);							                //将结构体变量交给DMA_Init,配置DMA1的通道11

	// 使能DMA
	DMA_Cmd (ADC_DMA_CHANNEL,ENABLE);
}

3.3  初始化ADC1配置

        因为有两个ADC我们分别配置一下,首先也是为了方便维护,我们也可以创建一些宏定义:

#define    ADCx_1                           ADC1
#define    ADCx_1_APBxClock_FUN             RCC_APB2PeriphClockCmd
#define    ADCx_1_CLK                       RCC_APB2Periph_ADC1

#define    ADCx_1_CHANNEL                   ADC_Channel_11

#define    ADCx_2                           ADC2
#define    ADCx_2_APBxClock_FUN             RCC_APB2PeriphClockCmd
#define    ADCx_2_CLK                       RCC_APB2Periph_ADC2

#define    ADC_DMA_CHANNEL               DMA1_Channel1

        我们根据这张图进行配置:

        GPIO口上面初始化过了,然后配置转换器,首先是对时钟的配置,打开时钟,配置分频系数:

	// 打开ADC时钟
	ADCx_1_APBxClock_FUN ( ADCx_1_CLK, ENABLE );

	RCC_ADCCLKConfig(RCC_PCLK2_Div8); // 配置ADC时钟为PCLK2的8分频,即9MHz

        配置转换顺序和转换时间,顺序就1个,给谁都无所谓,给个1吧,转换时间我们给239.5:

	ADC_RegularChannelConfig(ADCx_1, ADCx_1_CHANNEL, 1, ADC_SampleTime_239Cycles5);	// 配置 ADC 通道转换顺序和采样时间

        然后初始化ADC结构体,对结构体的成员进行设置,里面的参数可以查找上面介绍,我们这里将其配置为双ADC模式,不适用外部触发,软件开启,结果右对齐:

	ADC_InitTypeDef ADC_InitStructure;//初始化结构体

	ADC_InitStructure.ADC_Mode = ADC_Mode_RegSimult;// 只使用一个ADC,属于独立模式
	ADC_InitStructure.ADC_ScanConvMode = ENABLE ; // 禁止扫描模式,多通道才要,单通道不需要
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;// 连续转换模式
  ADC_InitStructure.ADC_NbrOfChannel = NOFCHANEL;	// 转换通道1个
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;// 不用外部触发转换,软件开启即可
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;// 转换结果右对齐	
	ADC_Init(ADCx_1, &ADC_InitStructure);// 初始化ADC

        使能DMA转运请求,开启ADC转换:

	// 使能ADC DMA 请求
	ADC_DMACmd(ADCx_1, ENABLE);

	ADC_Cmd(ADCx_1, ENABLE);// 开启ADC ,并开始转换

        校验:

	// 初始化ADC 校准寄存器  
	ADC_ResetCalibration(ADCx_1);
	// 等待校准寄存器初始化完成
	while(ADC_GetResetCalibrationStatus(ADCx_1));
	
	// ADC开始校准
	ADC_StartCalibration(ADCx_1);
	// 等待校准完成
	while(ADC_GetCalibrationStatus(ADCx_1));

        由于没有采用外部触发,所以使用软件触发ADC转换,又因为连续转换,因此后续会直接转换,不用在管他了:

	// 由于没有采用外部触发,所以使用软件触发ADC转换 
	ADC_SoftwareStartConvCmd(ADCx_1, ENABLE);

        完整代码:

//ADC1初始化配置
static void ADCx_1_Mode_Config(void)
{
	// 打开ADC时钟
	ADCx_1_APBxClock_FUN ( ADCx_1_CLK, ENABLE );

	RCC_ADCCLKConfig(RCC_PCLK2_Div8); // 配置ADC时钟为PCLK2的8分频,即9MHz
	ADC_RegularChannelConfig(ADCx_1, ADCx_1_CHANNEL, 1, ADC_SampleTime_239Cycles5);	// 配置 ADC 通道转换顺序和采样时间

	ADC_InitTypeDef ADC_InitStructure;//初始化结构体

	ADC_InitStructure.ADC_Mode = ADC_Mode_RegSimult;// 只使用一个ADC,属于独立模式
	ADC_InitStructure.ADC_ScanConvMode = ENABLE ; // 禁止扫描模式,多通道才要,单通道不需要
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;// 连续转换模式
  ADC_InitStructure.ADC_NbrOfChannel = NOFCHANEL;	// 转换通道1个
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;// 不用外部触发转换,软件开启即可
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;// 转换结果右对齐	
	ADC_Init(ADCx_1, &ADC_InitStructure);// 初始化ADC

	// 使能ADC DMA 请求
	ADC_DMACmd(ADCx_1, ENABLE);

	ADC_Cmd(ADCx_1, ENABLE);// 开启ADC ,并开始转换
	
	// 初始化ADC 校准寄存器  
	ADC_ResetCalibration(ADCx_1);
	// 等待校准寄存器初始化完成
	while(ADC_GetResetCalibrationStatus(ADCx_1));
	
	// ADC开始校准
	ADC_StartCalibration(ADCx_1);
	// 等待校准完成
	while(ADC_GetCalibrationStatus(ADCx_1));
	
	// 由于没有采用外部触发,所以使用软件触发ADC转换 
	ADC_SoftwareStartConvCmd(ADCx_1, ENABLE);
}

3.4  初始化ADC2配置

        过程和上面一样,就只是改了一下通道,这里直接把完整代码放出来,可以查看上面步骤自己写一下:

//ADC2初始化配置
static void ADCx_2_Mode_Config(void)
{
	// 打开ADC时钟
	ADCx_1_APBxClock_FUN ( ADCx_2_CLK, ENABLE );

	RCC_ADCCLKConfig(RCC_PCLK2_Div8); // 配置ADC时钟为PCLK2的8分频,即9MHz
	ADC_RegularChannelConfig(ADCx_2, ADCx_2_CHANNEL, 1, ADC_SampleTime_239Cycles5);	// 配置 ADC 通道转换顺序和采样时间

	ADC_InitTypeDef ADC_InitStructure;//初始化结构体

	ADC_InitStructure.ADC_Mode = ADC_Mode_RegSimult;// 只使用一个ADC,属于独立模式
	ADC_InitStructure.ADC_ScanConvMode = ENABLE ; // 扫描模式,多通道才要,单通道不需要
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;// 连续转换模式
  ADC_InitStructure.ADC_NbrOfChannel = NOFCHANEL;	// 转换通道1个
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;// 不用外部触发转换,软件开启即可
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;// 转换结果右对齐	
	ADC_Init(ADCx_2, &ADC_InitStructure);// 初始化ADC

	// 使能ADC DMA 请求
	ADC_DMACmd(ADCx_2, ENABLE);

	ADC_Cmd(ADCx_2, ENABLE);// 开启ADC ,并开始转换
	
	// 初始化ADC 校准寄存器  
	ADC_ResetCalibration(ADCx_2);
	// 等待校准寄存器初始化完成
	while(ADC_GetResetCalibrationStatus(ADCx_2));
	
	// ADC开始校准
	ADC_StartCalibration(ADCx_2);
	// 等待校准完成
	while(ADC_GetCalibrationStatus(ADCx_2));
	
	// 由于没有采用外部触发,所以使用软件触发ADC转换 
	ADC_SoftwareStartConvCmd(ADCx_1, ENABLE);
}

3.5  初始化文件配置

        就是将上面四个文件集中起来,方便管理:

//初始化文件
void ADCx_Init(void)
{
	ADCx_GPIO_Config();
	ADC_DMA_Config();
	ADCx_1_Mode_Config();
	ADCx_2_Mode_Config();
}

3.6  任务创建

        首先,我们之前也说了需要创建两个任务,那么就是两个任务句柄:

static TaskHandle_t Test_Task_Handle = NULL;/* 接收任务句柄 */
static TaskHandle_t KEY_Task_Handle = NULL;/* KEY任务句柄 */

        获取一下ADC采集存储数据的全局变量,方便任务提取数据,以及创建一个用于保存计算值:

// ADC1转换的电压值通过MDA方式传到SRAM
extern __IO uint32_t ADC_ConvertedValue[NOFCHANEL];

// 局部变量,用于保存转换计算后的电压值 	 
float ADC_ConvertedValueLocal[NOFCHANEL*2];     

        然后读取任务的创建,我们知道STM32F1系列输入电压范围:0~3.3V,转换结果范围:0~4095,那么我们是不是可以得到计算公式:

最终电压值 = ( 测得数字信号量 / 4095 ) * 3.3V

static void Test_Task(void* parameter)
{	
  uint16_t temp0=0 ,temp1=0;
  
  while (1)
  {
    // 取出ADC1数据寄存器的高16位,这个是ADC2的转换数据
		temp0 = (ADC_ConvertedValue[0]&0XFFFF0000) >> 16;
		// 取出ADC1数据寄存器的低16位,这个是ADC1的转换数据
		temp1 = (ADC_ConvertedValue[0]&0XFFFF);	
		
		ADC_ConvertedValueLocal[0] =(float) temp0/4096*3.3;
		ADC_ConvertedValueLocal[1] =(float) temp1/4096*3.3;
		
		printf("\r\nADCx_1 value = %f V \r\n", ADC_ConvertedValueLocal[1]);
		printf("ADCx_2 value = %f V \r\n", ADC_ConvertedValueLocal[0]);
  
    vTaskDelay(1000);   /* 延时500个tick */
  }
}

        然后是挂起和恢复任务的创建:

//KEY_Task任务主体
static void KEY_Task(void* parameter)
{	
  while (1)
  {
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
    {/* K1 被按下 */
		  printf("\r\n按键KEY1按下!\r\n");
      printf("挂起Test_Task任务!\r\n");
      vTaskSuspend(Test_Task_Handle);/* 挂起Test_Task任务 */
      printf("挂起Test_Task任务成功!\r\n");
    } 
    if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
    {/* K2 被按下 */
	    printf("\r\n按键KEY2按下!\r\n");
      printf("恢复Test_Task任务!\r\n");
      vTaskResume(Test_Task_Handle);/* 恢复Test_Task任务! */
      printf("恢复Test_Task任务成功!\r\n");
    }
    vTaskDelay(20);/* 延时20个tick */
  }
}

        然后找到AppTaskCreate()将两个任务创建:

  /* 创建Test_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )Test_Task, /* 任务入口函数 */
                        (const char*    )"Test_Task",/* 任务名字 */
                        (uint16_t       )512,   /* 任务栈大小 */
                        (void*          )NULL,	/* 任务入口函数参数 */
                        (UBaseType_t    )2,	    /* 任务的优先级 */
                        (TaskHandle_t*  )&Test_Task_Handle);/* 任务控制块指针 */
  if(pdPASS == xReturn)
    printf("创建Test_Task任务成功!\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"); 

3.7  完整main函数

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

#include "LED.h"
#include "Usart.h"
#include "Key.h"  
#include "ADC.h"

/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

/* 
 * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
 * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
 * 这个句柄可以为NULL。
 */
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
static TaskHandle_t Test_Task_Handle = NULL;/* 接收任务句柄 */
static TaskHandle_t KEY_Task_Handle = NULL;/* KEY任务句柄 */

/********************************** 内核对象句柄 *********************************/
/*
 * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
 * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
 * 们就可以通过这个句柄操作这些内核对象。
 *
 * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
 * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
 * 来完成的
 * 
 */

/******************************* 全局变量声明 ************************************/
// ADC1转换的电压值通过MDA方式传到SRAM
extern __IO uint32_t ADC_ConvertedValue[NOFCHANEL];

// 局部变量,用于保存转换计算后的电压值 	 
float ADC_ConvertedValueLocal[NOFCHANEL*2];       


/******************************* 宏定义 ************************************/

//一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */

static void Test_Task(void* pvParameters);/* Test_Task任务实现 */
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();           //进入临界区

  /* 创建Test_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )Test_Task, /* 任务入口函数 */
                        (const char*    )"Test_Task",/* 任务名字 */
                        (uint16_t       )512,   /* 任务栈大小 */
                        (void*          )NULL,	/* 任务入口函数参数 */
                        (UBaseType_t    )2,	    /* 任务的优先级 */
                        (TaskHandle_t*  )&Test_Task_Handle);/* 任务控制块指针 */
  if(pdPASS == xReturn)
    printf("创建Test_Task任务成功!\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();            //退出临界区
}

//Test_Task任务主体,方法一
static void Test_Task(void* parameter)
{	
  uint16_t temp0=0 ,temp1=0;
  
  while (1)
  {
    // 取出ADC1数据寄存器的高16位,这个是ADC2的转换数据
		temp0 = (ADC_ConvertedValue[0]&0XFFFF0000) >> 16;
		// 取出ADC1数据寄存器的低16位,这个是ADC1的转换数据
		temp1 = (ADC_ConvertedValue[0]&0XFFFF);	
		
		ADC_ConvertedValueLocal[0] =(float) temp0/4096*3.3;
		ADC_ConvertedValueLocal[1] =(float) temp1/4096*3.3;
		
		printf("\r\nADCx_1 value = %f V \r\n", ADC_ConvertedValueLocal[1]);
		printf("ADCx_2 value = %f V \r\n", ADC_ConvertedValueLocal[0]);
  
    vTaskDelay(1000);   /* 延时500个tick */
  }
}

//KEY_Task任务主体
static void KEY_Task(void* parameter)
{	
  while (1)
  {
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
    {/* K1 被按下 */
		  printf("\r\n按键KEY1按下!\r\n");
      printf("挂起Test_Task任务!\r\n");
      vTaskSuspend(Test_Task_Handle);/* 挂起Test_Task任务 */
      printf("挂起Test_Task任务成功!\r\n");
    } 
    if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
    {/* K2 被按下 */
	    printf("\r\n按键KEY2按下!\r\n");
      printf("恢复Test_Task任务!\r\n");
      vTaskResume(Test_Task_Handle);/* 恢复Test_Task任务! */
      printf("恢复Test_Task任务成功!\r\n");
    }
    vTaskDelay(20);/* 延时20个tick */
  }
}


//初始化声明
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();

	// ADC 初始化
	ADCx_Init();
}



3.8  运行结果

         至此完整工程完成。

完整工程:

基于STM32移植FreeRTOS实现双ADC进行DMA数据转运资源-CSDN文库

FreeRTOS菜鸟入门系列_时光の尘的博客-CSDN博客

FreeRTOS实战系列_时光の尘的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时光の尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值