1、选题背景
最近刚入坑,看了半个多月的入门视频并动手了一些简单的实验,但看工程项目的代码总是很费劲,便想以一个有难度的课题来进一步入门嵌入式开发。这个选题充分使用了STM32的各种片上外设,包括定时器、ADC模/数变换、GPIO口和DMA的使用,配合外部资源如LCD屏幕来展示结果。
2、实验介绍
实验目标:
从一个简单的实验出发,逐步加大难度,增加单片机资源的使用,做一个有意思且有意义的学习课题。注:本实验的全部例程使用库函数进行编写,文中提到的模式关键字均为库函数所定义。本实验对硬件部分不过多关注,仅在必要时稍作解释。
首先理解为什么ADC需要和DMA进行配合,ADC本身可以独立工作,并通过中断服务程序获取每次采样的值。但这仅是基本功能,通常只能采样单通道的信号,每次转换完毕后存储到特定寄存器内(每次转换结果会覆盖上一次的值)并触发中断,频繁地中断又会消耗资源、浪费时间。而配合DMA传输,可以采集多路信号,可以等待采集了大量数据传入内存,一次性进行处理。
下面通过模块的不同工作模式来探讨ADC和DMA的配合工作。
对于ADC模块,由于注入通道不会产生DMA请求,因此以下主要介绍规则通道的一些工作模式(所谓规则通道,即ADC的输入口中稳定工作的部分,区别于注入通道类似中断的方式进行采样):
(1)扫描模式(ADC_ScanConvMode)。控制是否使用多通道,启用后:ADC每次转换完一个通道,会继续去到下一个通道进行采样转换;否则仅转换当前通道。启用后通常需要配合DMA进行数据存储,否则在进入下一个通道转换后的值会覆盖上一次转换的结果(ADC规则通道的数据寄存器只有一个)。
(2)双ADC模式(ADC_Mode)。增强型的STM32芯片通常具有三个ADC外设,其中ADC1和ADC3都可以使用DMA进行传输,ADC2虽然没有直接与DMA相连,但可以与ADC1配合,在双ADC模式下共同工作并使用DMA传输,提高工作效率。因此双模式主要针对ADC1和ADC2,二者既可以相互独立工作,也可以主从配合工作,以下模式适用于规则通道:
- 独立模式(ADC_Mode_Independent),两个ADC相互独立工作,通常在仅使用一个ADC时使用;
- 同步规则模式(ADC_Mode_RegSimult),ADC1和ADC2同时转换规则通道,但不能是同一个规则通道,即同一时间内两个ADC不能转换同一个规则通道,转换结果存储为32位(各占高低16位)。
- 快速交叉模式(ADC_Mode_FastInterl),外部触发启动,ADC2先工作进行采样,延迟7个ADC时钟周期后ADC1启动进行采样,此模式通常仅采样一个规则通道,能加快采样率。要注意采样时间不要小于7个ADC时钟周期,避免重复采样(如下图)。
- 慢速交叉模式(ADC_Mode_SlowInterl),与快速交叉模式类似,ADC1、ADC2轮流工作,但均需要等待上一次采样结束14个ADC时钟周期后才启动。该模式会比快速交叉模式的采样速度慢一些,但比独立模式还是快不少,同样要注意采样时间不要小于14个ADC时钟周期。
(3)连续转换模式(ADC_ContinuousConvMode)。控制是否连续转换,使能后通道会进行连续的转换,在一轮转换后(即所有通道转换完一遍),又从第一个通道重新开始下一轮;否则通道在结束一轮转换后停止工作。
该配置与下面要介绍的外部触发的配置息息相关:
- 在配置了外部触发(尤其是定时器触发,如ADC_ExternalTrigConv_T2_CC2)后,每次相关事件发生时会触发ADC的转换,此时若仍然连续转换,便失去了外部触发的意义。但也有例外,即IO口的触发,通常仅触发一次,相当于一次使能操作,便可以设置连续转换。外部触发方式的优点是容易控制,更容易符合特定场景需求;
- 如果没有设置外部触发(ADC_ExternalTrigConv_None),则由ADC内部时钟和寄存器配合转换工作。在连续转换模式下,其采样率远远大于普通定时器触发,但缺点也很明显,过快的采样速度要求后续的工作也有超高的处理速度,会给CPU带来巨大的运算负担,在一些计算复杂度较高的场景并不适用。
(4)触发方式(ADC_ExternalTrigConv)。即如何触发ADC的转换工作,共有以下8中选择:
其中前6种都是定时器触发,而第八种则由软件控制使能(仅需使能一次),本实验使用定时器触发,选择的是TIM2_CC2事件(ADC_ExternalTrigConv_T2_CC2)。要注意,使用外部触发方式时,只有上升沿才能开启转换,这涉及到后面TIM的配置。
对于DMA模块,在和ADC协同工作时,主要有以下几点需要注意:
(1)工作模式(DMA_Mode)。DMA的工作模式有两种选择,区别在于是否连续工作:
- 正常缓存模式(DMA_Mode_Normal),在该模式下,每次使能仅进行一轮传输,一旦计数器的值减至0时,DMA传输会停止。想要再次传输,必须重新设置计数器CNDTR的数值;
- 循环工作模式(DMA_Mode_Circular),主要处理连续的数据传输,每进行一轮传输,计数器会被自动恢复为初始值,同时DMA传输进入下一轮,非常适合同ADC多通道(扫描)模式一起工作。
(2)DMA通道。下面展示了DMA1的所有通道,为了和ADC1连上,需要设置好DMA1的通道1(DMA1_Channel1)。
注:ADC3可以使用DMA2进行传输,在这里没有展示。而ADC2天生不与DMA发生直接关系,只能配合ADC1进行主从传输。
(3)DMA中断。DMA的中断是这个实验的桥梁,连接着用户和外设,我们需要在中断里处理数据,包括计算和展示数据等,但这样很浪费资源。有一种更好的做法是,基于UCOS系统,在中断服务程序中通过事件标志的方式通知数据处理相关的任务及时运行。
对于通用定时器的配置,在前面说到ADC需要外部的上升沿触发,那么普通的方波就可以满足这一需求,同时也要具有稳定的周期。先来看一下官方的输出比较模式介绍:
其中翻转模式(TIM_OCMode_Toggle,011)和两个PWM模式都可以满足需求,这两种模式都具有“比较”和“翻转”的能力,其它模式如000、100和101只能强制高或低电平,001和010仅适用特殊配置和场景,都不满足需求。
到这里核心的配置就基本完成了!
涉及到的知识脑图:
3、代码详解
(1)ADC配置
ADC_InitTypeDef ADC_InitStructure;
/* 使能ADC1时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
/* 双ADC模式选择,可以是独立或同步规则模式 */
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
/* 扫描模式,多通道 */
ADC_InitStructure.ADC_ScanConvMode = ENABLE;
/* 单次转换,不连续转换 */
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
/* 使用定时器2的CC2触发 */
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_CC2;
/* 使用内部时钟和寄存器触发 */
// ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
/* 右对齐,左对齐会向左产生四位的位移 */
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
/* 采样通道数量 */
ADC_InitStructure.ADC_NbrOfChannel = 2;
ADC_Init(ADC1, &ADC_InitStructure);
/* 配置ADC时钟(12MHz) */
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
/* ADC通道配置,包括通道、采样顺序和采样周期 */
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 2, ADC_SampleTime_239Cycles5);
/* 使能ADC1 */
ADC_Cmd(ADC1, ENABLE);
/* 复位ADC1校准寄存器 */
ADC_ResetCalibration(ADC1);
/* 等待复位完成 */
while (ADC_GetResetCalibrationStatus(ADC1));
/* 进行ADC1校准 */
ADC_StartCalibration(ADC1);
/* 等待校准完成 */
while (ADC_GetCalibrationStatus(ADC1));
/* 使能外部触发,使用外部触发必须要做 */
ADC_ExternalTrigConvCmd(ADC1, ENABLE);
/* 使能ADC发送数据到DMA总线,必须要做 */
ADC_DMACmd(ADC1, ENABLE);
(2)DMA配置
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
/* 复位DMA1通道1,可做可不做 */
DMA_DeInit(DMA1_Channel1);
/* 外设地址,即ADC1的数据寄存器,数据长度为16位 */
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)(&(ADC1->DR));
/* 内存地址,自定义数组并取首地址,注意要用16位,与ADC数据对称 */
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&DMA_ADC_PeripheralBaseAddr[0];
/* 外设作为传输源,即ADC->DMA */
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
/* 一轮传输的数据数量,最好和内存数组长度对应 */
DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
/* 外设地址在传输过程中不自增,因为ADC规则通道的数据寄存器只有一个 */
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
/* 内存地址传输过程自增 */
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
/* 传输的数据都是16位的,即半字(字,不是字节) */
DMA_InitStructure.DMA_PeripheralDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
/* Normal模式单次传输,Circular模式可以自动重装循环传输 */
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
/* DMA任务优先级 */
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
/* 非内存->内存模式 */
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
/* 使能DMA1通道1 */
DMA_Cmd(DMA1_Channel1, ENABLE);
/* 使能传输完毕中断,这样才能接收到该中断 */
DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
(3)TIM配置
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
/* 定时器基本配置,使用TIM2去触发,故初始化TIM2 */
/* 分频系数71,系统主频72M */
TIM_TimeBaseStructure.TIM_Prescaler = 71;
TIM_TimeBaseStructure.TIM_Period = 1000;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
/* 定时器输出比较配置,PWM1输出 */
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
/* 输出比较的值,即TIM2_CCR2 */
TIM_OCInitStructure.TIM_Pulse = 5000;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
TIM_OC2Init(TIM2, &TIM_OCInitStructure);
/* 使能TIM2 */
TIM_Cmd(TIM2, ENABLE);
/* 使能TIM2内部时钟 */
TIM_InternalClockConfig(TIM2);
/* 使能TIM2在CCR2上的预装载寄存器 */
TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable);
/* 失能TIM2更新事件 */
TIM_UpdateDisableConfig(TIM2, DISABLE);
(4)GPIO配置
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/* 设置GPIOA的0号引脚为模拟输入 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_Init(GPIOA, &GPIO_InitStructure);
(5)中断配置
NVIC_InitTypeDef NVIC_InitStructure;
/* 设置中断分组为2 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 设置DMA1通道1的中断 */
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn;
/* 设置抢占优先级 */
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
/* 设置次优先级 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
/* 使能中断 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
(6)中断服务程序
这一部分的代码比较自由,根据个人喜好处理数据
void DMA1_Channel1_IRQHandler(void)
{
OS_CPU_SR cpu_sr;
/* 进入临界区 */
OS_ENTER_CRITICAL();
OSIntEnter();
OS_EXIT_CRITICAL();
/* 判断中断是否是DMA传输完成中断 */
if(DMA_GetFlagStatus(DMA1_IT_TC1))
{
/* 失能DMA1通道1,以防在中断过程中发生DMA传输 */
DMA_Cmd(DMA1_Channel1, DISABLE);
/* 如果DMA设置的是normal模式,需要手动重装计数器 */
// DMA1_Channel1->CNDTR = BUFFER_SIZE;
/* 清除中断标志位 */
DMA_ClearITPendingBit(DMA1_IT_TC1);
// DMA_ClearFlag(DMA1_FLAG_TC1);
/* 重新使能DMA1通道1,开始工作 */
DMA_Cmd(DMA1_Channel1, ENABLE);
}
OSIntExit();
}
以上这些代码仅仅为配置代码,实际需要根据不同程序的需求进行调整,尤其是使能指令的顺序。
4、总结
本文在编写的过程中,出现过很多纰漏和错误,我相信文中肯定还有很多不够严谨和细节的地方,欢迎指正和讨论,笔者单片机入门小白,多来一点批评和指导鞭策我吧^_^
在最后,想给看到的同学说句话,学任何东西都要讲究方法,并坚持,看到了别人的博文,不论有多详细都仅仅是启发,最好自己回头去看看文档资料,并做做实验,能坚持的可以总结出来输出新的博文,带着批判的眼神去审视自己写的内容,才能扎实并精益求精。