今天继续我们的STM32的内容学习,我使用的单片机是STM32F103VCT6,通过Keil Array Visualization软件来观测AD采样出来的波形。先来看看本次实验用到的硬件知识。
首先是ADC(Analog-to-Digital Converter)是模拟信号转数字信号的关键组件,用于将来自外部传感器或模拟输入的模拟信号转换为数字形式,以便微处理器或控制器进行数字信号处理。如果大家看我其他文章的话,这个器件就是我们采样需要用到的一个器件。它的功能框图如下。
首先,需要注意的是电压的范围,这也是ADC输入信号的幅值范围[,],这里我用的电压范围是[0,3]。这样就够了。当然好像也有负的。这里就不做讨论了。其次,就是输入通道,也就是你的采样信号加在哪个地方。每个ADC最多有16个通道。ADC1和ADC2都有16个通道。接下来,这张图就是ADC对应通道的IO口。大家可以参考一下。这个只是STM32F103VET6的ADC的IO分配,具体到单片机大家可以去找参考手册。
对应的GPIO口要配置成模拟输入模式,即GPIO_Mode_AIN。我使用的是ADC1的4通道,由上图可知,也就是PA4的端口。代码如下。
void GPIO_Configuration(void)
{
/* 定义 GPIO 初始化结构体 GPIO_InitStructure */
GPIO_InitTypeDef GPIO_InitStructure;
/* 将 PA.4 设置为模拟输入脚 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
接下来就是配置ADC了。这里要根据下面的结构体,慢慢配置。包括工作模式,ADC扫描(单通道还是多通道),转换模式的选择,触发方式的选择,对齐格式的选择(左对齐,右对齐等)、ADC采集通道数。当然我建议大家复制粘贴,毕竟结构体打起来太麻烦了,咱们主要学的是原理。结构体如下。
typedef struct
{
uint32_t ADC_Mode; /*!< Configures the ADC to operate in independent or
dual mode.
This parameter can be a value of @ref ADC_mode */
FunctionalState ADC_ScanConvMode; /*!< Specifies whether the conversion is performed in
Scan (multichannels) or Single (one channel) mode.
This parameter can be set to ENABLE or DISABLE */
FunctionalState ADC_ContinuousConvMode; /*!< Specifies whether the conversion is performed in
Continuous or Single mode.
This parameter can be set to ENABLE or DISABLE. */
uint32_t ADC_ExternalTrigConv; /*!< Defines the external trigger used to start the analog
to digital conversion of regular channels. This parameter
can be a value of @ref ADC_external_trigger_sources_for_regular_channels_conversion */
uint32_t ADC_DataAlign; /*!< Specifies whether the ADC data alignment is left or right.
This parameter can be a value of @ref ADC_data_align */
uint8_t ADC_NbrOfChannel; /*!< Specifies the number of ADC channels that will be converted
using the sequencer for regular channel group.
This parameter must range from 1 to 16. */
}ADC_InitTypeDef;
/* 定义 ADC 初始化结构体 ADC_InitStructure */
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent·;
ADC_InitStructure.ADC_ScanConvMode = DISABLE ;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_CC2 ; // 指定 TIM2 的触发输出作为 ADC 触发源
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
我配置的结构体如上,其他的都无所谓,重要的是这个ADC_ExternalTrigConv的选择。烦的嘞,我也是服了。为什么说它烦,咱们继续写。这里使用的是TIM2外部触发ADC1,捕获比较事件2会产生一个触发信号,使得ADC在上升沿或者是下降沿的触发。也就是TIM2每到捕获比较事件结束后,它就是采一次样。那什么是捕获比较事件?捕获事件:在定时器模块中捕获外部信号的状态变化或时间点。比如,可以捕获外部传感器产生的脉冲信号的上升沿或下降沿;比较事件:通过比较寄存器中的值和计数器的值,触发比较事件。比如,可以在比较寄存器值等于计数器值时触发一个事件。这就需要继续配置我们的TIM2定时器了。配置如下。
void TIM2_Configuration(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
//定时器TIM2初始化
TIM_TimeBaseStructure.TIM_Period = 3515-1; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
TIM_TimeBaseStructure.TIM_Prescaler = 1-1; //设置用来作为TIMx时钟频率除数的预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
TIM_OCInitStructure.TIM_Pulse = TIM_TimeBaseStructure.TIM_Period/2;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //输出极性:TIM输出比较极性低
TIM_OC2Init(TIM2, & TIM_OCInitStructure); //初始化外设TIM2_CH2
TIM_Cmd(TIM2, ENABLE); //使能TIM2
}
这里的OC呢,就是我所说的输出比较事件,竟然我们能用OC,那能不能用IC输入捕获呢?有没有大佬指导一下,孩子没搞出来。O_O
接下来,就是配置DMA,与常用的USART串口通信一致。DMA(Direct Memory Access,直接内存访问)是一种无需MCU干预,在外设和存储器之间或者存储器和存储器之间直接进行数据传输的技术。在传统的数据传输方式中,MCU需要参与每个数据的读取和写入操作,这会消耗大量的MCU资源和时间。而 DMA 控制器可以接管数据传输的任务,直接控制数据在不同存储区域之间的移动,从而大大提高了数据传输的效率,减轻了MCU的负担,使MCU能够同时处理其他任务。换句话说,这个DMA传输数据的时候就是点对点的进行传输,不需要经过MCU,不占用内存。有两个DMA,分别为DMA1和DMA2。DMA1上含有7个通道,DMA2上含有5个通道。其功能框图以及通道的分配图如下。
我用的是ADC1,由上图,所以为DMA1。接下来看看DMA配置,包括外设地址,存储地址,传输方向,传输的数据量,外设是否增量,地址是否增量,外设数据宽度,存储器数据宽度,模式选择,通道优先级,什么到什么的模式。结构体如下。
typedef struct
{
uint32_t DMA_PeripheralBaseAddr; /*!< Specifies the peripheral base address for DMAy Channelx. */
uint32_t DMA_MemoryBaseAddr; /*!< Specifies the memory base address for DMAy Channelx. */
uint32_t DMA_DIR; /*!< Specifies if the peripheral is the source or destination.
This parameter can be a value of @ref DMA_data_transfer_direction */
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. */
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 */
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 */
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 */
uint32_t DMA_Priority; /*!< Specifies the software priority for the DMAy Channelx.
This parameter can be a value of @ref DMA_priority_level */
uint32_t DMA_M2M; /*!< Specifies if the DMAy Channelx will be used in memory-to-memory transfer.
This parameter can be a value of @ref DMA_memory_to_memory */
}DMA_InitTypeDef;
说到这里就差不多了。接下来,给大家看看一个完整的代码。本次实验整体的思路是通过TIM2外部触发ADC启动,经过DMA进行传输,通过仿真软件来看到我们所要的波形。这里以波形为主,至于采样率这些就不去深究了,大家可以自由发挥。我这里的输入信号为0~3V的正弦信号,采样率约为20.48KHz。至于为什么20.48KHz?大家结合FFT想想为什么?之后告诉大家。完整代码如下。
/*******************************************************************************
* 文件名 : main.c
* 作者 : Losingamong
* 时间 : 08/08/2008
* 文件描述 : 主函数
********************************************************************************/
/* 头文件 ------------------------------------------------------------------*/
#include "stm32f10x.h"
#include "stm32f10x_adc.h"
#include "stdio.h"
#include "stm32f10x_dma.h"
#include "stm32f10x_tim.h"
/* 自定义同义关键字 --------------------------------------------------------*/
/* 自定义参数宏 --------------------------------------------------------*/
/* 自定义函数宏 --------------------------------------------------------*/
/* 自定义变量 --------------------------------------------------------*/
#define ADC1_DR_Address ((u32)0x40012400 + 0x4c)
uint16_t AD_Value[512];
uint16_t temp[512];
uint8_t ADC_FLAG;
/* 自定义函数声明 --------------------------------------------------------*/
void RCC_Configuration(void);
void GPIO_Configuration(void);
void ADC_Configuration(void);
void DMA_Configuration(void);
void TIM2_Configuration(void);
/*******************************************************************************
* 函数名 : main
* 函数描述 : Main 函数
* 输入参数 : 无
* 输出结果 : 无
* 返回值 : 无
*******************************************************************************/
int main (void)
{
int i = 0;
/* 设置系统时钟 */
RCC_Configuration();
/* 设置 GPIO 端口 */
GPIO_Configuration();
/* 设置 ADC */
ADC_Configuration();
/* DMA 初始化 */
DMA_Configuration();
/* 配置 TIM2 */
TIM2_Configuration();
while(1)
{
if(ADC_FLAG == 1)
{
for(i = 0; i < 512; i++)
{
temp[i] = AD_Value[i];
}
ADC_FLAG = 0;
DMA_Cmd(DMA1_Channel1, ENABLE);
}
}
}
/*******************************************************************************
* 函数名 : RCC_Configuration
* 函数描述 : 设置系统各部分时钟
* 输入参数 : 无
* 输出结果 : 无
* 返回值 : 无
*******************************************************************************/
void RCC_Configuration(void)
{
/* 使能各个用到的外设时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
}
void TIM2_Configuration(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
//定时器TIM2初始化
TIM_TimeBaseStructure.TIM_Period = 3515-1; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
TIM_TimeBaseStructure.TIM_Prescaler = 1-1; //设置用来作为TIMx时钟频率除数的预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
TIM_OCInitStructure.TIM_Pulse = TIM_TimeBaseStructure.TIM_Period/2;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //输出极性:TIM输出比较极性低
TIM_OC2Init(TIM2, & TIM_OCInitStructure); //初始化外设TIM2_CH2
TIM_Cmd(TIM2, ENABLE); //使能TIM2
}
/*******************************************************************************
* 函数名 : GPIO_Configuration
* 函数描述 : 设置各 GPIO 端口功能
* 输入参数 : 无
* 输出结果 : 无
* 返回值 : 无
*******************************************************************************/
void GPIO_Configuration(void)
{
/* 定义 GPIO 初始化结构体 GPIO_InitStructure */
GPIO_InitTypeDef GPIO_InitStructure;
/* 将 PA.4 设置为模拟输入脚 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
/*******************************************************************************
* 函数名 : ADC_Configuration
* 函数描述 : 初始化并启动 ADC 转换,由 TIM2 外部触发
* 输入参数 : 无
* 输出结果 : 无
* 返回值 : 无
*******************************************************************************/
void ADC_Configuration(void)
{
/* 定义 ADC 初始化结构体 ADC_InitStructure */
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE ;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_CC2 ; // 指定 TIM2 的触发输出作为 ADC 触发源
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
/* 设置 ADC1 使用 8 转换通道,转换顺序 1,采样时间为 55.5 周期 */
ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 1, ADC_SampleTime_55Cycles5);
/* 使能 ADC1 */
ADC_Cmd(ADC1, ENABLE);
/* 复位 ADC1 的校准寄存器 */
ADC_ResetCalibration(ADC1);
/* 等待 ADC1 校准寄存器复位完成 */
while(ADC_GetResetCalibrationStatus(ADC1));
/* 开始 ADC1 校准 */
ADC_StartCalibration(ADC1);
/* 等待 ADC1 校准完成 */
while(ADC_GetCalibrationStatus(ADC1));
/* 使能外部触发 启动 ADC1 转换 */
ADC_ExternalTrigConvCmd(ADC1, ENABLE);
ADC_DMACmd(ADC1,ENABLE);
}
void DMA_Configuration(void)
{
DMA_InitTypeDef DMA_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//DMA1初始化
DMA_DeInit(DMA1_Channel1);
DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address; //ADC1地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&AD_Value; //内存地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //方向(从外设到内存)
DMA_InitStructure.DMA_BufferSize = 512; //传输内容的大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址固定
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址固定
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord ; //外设数据单位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord ; //内存数据单位
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
DMA_ITConfig(DMA1_Channel1,DMA_IT_TC, ENABLE); //使能传输完成中断
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);
DMA_Cmd(DMA1_Channel1,ENABLE);
}
void DMA1_Channel1_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC1)!= RESET)
{
// DMA_Cmd(DMA1_Channel1, DISABLE); // 关闭 DMA1 通道 1 和 ADC1
ADC_FLAG = 1; // ADC1 采集完成标志
DMA_ClearITPendingBit(DMA1_IT_TC1); // 清除中断标志
}
}
结果如下,
今天就说到这里,下次讲刺激的FFT。
欲知后事如何,且听下回分解。OVO........