历史上的今天
返回首页

历史上的今天

今天是:2025年08月20日(星期三)

正在发生

2019年08月20日 | stm32之ADC应用实例(单通道、多通道、基于DMA)

2019-08-20 来源:eefocus

硬件:STM32F103VCT6

开发工具:Keil uVision4

下载调试工具:ARM仿真器

网上资料很多,这里做一个详细的整合。(也不是很详细,但很通俗)。

所用的芯片内嵌3个12位的模拟/数字转换器(ADC),每个ADC共用多达16个外部通道,2个内部通道。


3个:代表ADC1、ADC2、ADC3(下图是芯片固件库的截图)

这里写图片描述

12位:也叫ADC分辨率、采样精度。先来看看二进制的12位可表示0-4095个数,也就是说转换器通过采集转换所得到的最大值是4095,如:“111111111111”=4095,那么我们怎么通过转换器转换出来的值得到实际的电压值呢?如果我们要转换的电压范围是0v-3.3v的话,转换器就会把0v-3.3v平均分成4096份。设转换器所得到的值为x,所求电压值为y。

那么就有:

这里写图片描述

16个外部通道:简单的说就是芯片上有16个引脚是可以接到模拟电压上进行电压值检测的。16个通道不是独立的分配给3个转换器(ADC1、ADC2、ADC3)使用,有些通道是被多个转换器共用的。首先看看16个通道在固件库的宏定义(写代码要看的):

这里写图片描述

到这里大家可能会有疑问,每个通道到底对应哪个引脚呢?下面先给出部分引脚图:

这里写图片描述

16个通道的引脚都在上面的图中,拿其中的一个进行说明:


ADC123_IN10:字母“ADC”不用多说,“123”代表它被3个(ADC1、ADC2、ADC3)转换器共用的引脚,“10”对应刚才那张宏定义图里面的ADC_Channel_10,这样就能找到每个通道对应的引脚了。


2个内部通道:一个是内部温度传感器,一个是内部参考电压。


在某个项目中要用到芯片里面的AD转换器,那么要怎么写应用代码?(以下是代码讲解)


芯片固件的库函数为我们提供了很多封装好的函数,只要运用它提供的函数接口就可以了,宏观上来讲就搞懂两个事情就行了:


初始化(设置用的哪个引脚、单通道、还是多通道同时转换、是否使用DMA等配置)?

怎么让转换器进行一次数据获取?

以下分别讲述三种不同方式(单通道、多通道、基于DMA的多通道采集)的ADC应用实例:


/*单通道的ADC采集*/

void  Adc_Config(void)

{

    /*定义两个初始化要用的结构体,下面给每个结构体成员赋值*/

ADC_InitTypeDef ADC_InitStructure; 

GPIO_InitTypeDef GPIO_InitStructure;

/*

  使能GPIOA和ADC1通道时钟

  注意:除了RCC_APB2PeriphClockCmd还有RCC_APB1PeriphClockCmd,那么该如何选择?

      APB2:高速时钟,最高72MHz,主要负责AD输入,I/O,串口1,高级定时器TIM

      APB1:低速时钟,最高36MHz,主要负责DA输出,串口2、3、4、5,普通定时器TIM,USB,IIC,CAN,SPI

  */

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1, ENABLE );   

    RCC_ADCCLKConfig(RCC_PCLK2_Div6);  //72M/6=12, ADC的采样时钟最快14MHz  

      

    /*配置输入电压所用的PA0引脚*/         

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //GPIO_Mode_AIN:模拟输入(还有其他什么模式?请看下面的附录图1)

GPIO_Init(GPIOA, &GPIO_InitStructure);


ADC_DeInit(ADC1); //复位,将ADC1相关的寄存器设为默认值

ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //工作模式:ADC1和ADC2独立工作模式  (还有其他什么模式?请看下面的附录图2)

ADC_InitStructure.ADC_ScanConvMode = DISABLE; //数模转换工作:扫描(多通道)模式=ENABLE、单次(单通道)模式=DISABLE

ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//数模转换工作:连续=ENABLE、单次=DISABLE

ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //ADC转换由软件触发启动 (还有其他什么模式?请看下面的附录图3)

ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐   除了右就是左:ADC_DataAlign_Left

ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目   范围是1-16

ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADC1的寄存器

 

    /*为啥要设置下面这一步?

     细心的你可以发现上面初始化了一个引脚通道,初始化了一个ADC转换器,但ADC转换器并不知道你用的是哪个引脚吧?

     这一步目的是:设置指定ADC的规则组通道(引脚),设置它们的转化顺序和采样时间

     函数原型:void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, u8 ADC_Channel, u8 Rank, u8 ADC_SampleTime)

     参数1 ADCx:x可以是1或者2来选择ADC外设ADC1或ADC2 

     参数2 ADC_Channel:被设置的ADC通道  范围ADC_Channel_0~ADC_Channel_17

     参数3 Rank:规则组采样顺序。取值范围1到16。

     ADC_SampleTime:指定ADC通道的采样时间值  (取值范围?请看下面的附录图4)

    */

ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5 );  

      

ADC_Cmd(ADC1, ENABLE); //使能指定的ADC  注意:函数ADC_Cmd只能在其他ADC设置函数之后被调用


    /*下面4步按流程走,走完就行*/

ADC_ResetCalibration(ADC1); //重置指定的ADC的校准寄存器

while(ADC_GetResetCalibrationStatus(ADC1)); //等待上一步操作完成

ADC_StartCalibration(ADC1); //开始指定ADC的校准状态

while(ADC_GetCalibrationStatus(ADC1));//等待上一步操作按成

 }


初始化完成之后,在主函数中:


void main(void)

{

  float ADC_ConvertedValue; 

  float ADC_ConvertedValueLocal; 


Adc_Config();

while(1)

{

  ADC_SoftwareStartConvCmd(ADC1, ENABLE); //启动转换

  while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));  //等待转换完成

  

  ADC_ConvertedValue=ADC_GetConversionValue(ADC1); //获取转换结果*ADC_ConvertedValue*

  ADC_ConvertedValueLocal=(float)ADC_ConvertedValue*(3.3/4096);   //计算出实际电压值*ADC_ConvertedValueLocal*

  

  //这里适当加上一些延迟

  //最好连续转换几次 取平均值  这里就省略写了 点到为止

}

}


附录图1-GPIO_Mode值:

这里写图片描述

附录图2-ADC_Mode值:

这里写图片描述

附录图3-ADC_ExternalTrigConv值:

这里写图片描述

附录图4-ADC_SampleTime值:


这里写图片描述

对于一些刚接触stm32的人来说,看了上面的代码可能还会有很多疑问。


为什么要使能时钟?时钟到底设置多少才合适?

对于ADC_GetConversionValue(ADC1)这个函数参数并没有指定那个通道,如果多个通道同时使用CAN1转换器转换时怎么获取每个通道的值?

第一个问题,所有的外设都要使能时钟,时钟源分为外部时钟和内部时钟,外部时钟比如接8MHz晶振,内部时钟就在芯片内部集成,时钟源为所有的时序电路提供基本的脉冲信号。时钟源好比是一颗跳动的心脏,它按照一定的频率在跳动,所有的器官(外设)要跟心脏(时钟源)桥接起来才能工作,但不同的外设需要的频率不同,所以在时钟源跟外设之中常常还会有一些分频器或者倍频器,以实现对频率的衰减或增强。还想了解更多专业的解释可以去研究stm32的时钟树图。


**第二个问题,**回答这个问题那么就等于开始介绍多通道转换怎么实现了,看下图

这里写图片描述

由图理解,一个ADC转换器只能选择转换一个通道,那么对比单通道我们只需做一下改变(以双通道为例):

1.在void Adc_Config(void)函数里面添加:


GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; 

GPIO_Init(GPIOA, &GPIO_InitStructure);


配置多一个IO(PA1)口, 也就是通道1。


2.在void Adc_Config(void)函数里面添加:


ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5 );


先不指定ADC转换通道。


3.在主函数循环里改为:


    while(1)

{

  /*先采集通道1数据*/

  ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5 );

  ADC_SoftwareStartConvCmd(ADC1, ENABLE);

  while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));  

  ADC_ConvertedValue=ADC_GetConversionValue(ADC1); 

  ADC_ConvertedValueLocal=(float)ADC_ConvertedValue*(3.3/4096);  

  

  /*再采集通道2数据*/

  ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5 );

      ADC_SoftwareStartConvCmd(ADC1, ENABLE);

  while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));  

  ADC_ConvertedValue=ADC_GetConversionValue(ADC1); 

  ADC_ConvertedValueLocal=(float)ADC_ConvertedValue*(3.3/4096);  


  //加入适当延时

}


完成以上三步就能把单通道扩展到双通道(或者更多个通道)。不过还有一种基于DMA的多通道转换更加合适。


首先简单介绍DMA,DMA(Direct Memory Access,直接内存存取) ,用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无需CPU干预,节省CPU资源;ADC转换出来的值直接赋值给定义好的变量中。配置好的DMA可以不停地将ADC转换值写到该变量中,在主函数直接判断该变量就知道此时的AD值,也就是说在主函数中不需要调用ADC_GetConversionValue()函数来获取转换值。


DMA跟其他外设一样需要进行配置通道,使能时钟等参数。

下面直接看代码分析:


/*基于DMA的ADC多通道采集*/


volatile uint16 ADCConvertedValue[10][3];//用来存放ADC转换结果,也是DMA的目标地址,3通道,每通道采集10次后面取平均数

         

void DMA_Init(void)

{


    DMA_InitTypeDef DMA_InitStructure;


    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//使能时钟


    DMA_DeInit(DMA1_Channel1);    //将通道一寄存器设为默认值

    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR);//该参数用以定义DMA外设基地址

    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&ADCConvertedValue;//该参数用以定义DMA内存基地址(转换结果保存的地址)

    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//该参数规定了外设是作为数据传输的目的地还是来源,此处是作为来源

    DMA_InitStructure.DMA_BufferSize = 3*10;//定义指定DMA通道的DMA缓存的大小,单位为数据单位。这里也就是ADCConvertedValue的大小

    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//设定外设地址寄存器递增与否,此处设为不变 Disable

    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//用来设定内存地址寄存器递增与否,此处设为递增,Enable

    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//数据宽度为16位

    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//数据宽度为16位

    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //工作在循环缓存模式

    DMA_InitStructure.DMA_Priority = DMA_Priority_High;//DMA通道拥有高优先级 分别4个等级 低、中、高、非常高

    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//使能DMA通道的内存到内存传输

    DMA_Init(DMA1_Channel1, &DMA_InitStructure);//根据DMA_InitStruct中指定的参数初始化DMA的通道


    DMA_Cmd(DMA1_Channel1, ENABLE);//启动DMA通道一

}


下面是ADC的初始化,可以将它与上面的对比一下有啥不同,重复的就不解析了


void Adc_Init(void)

{

    ADC_InitTypeDef ADC_InitStructure;

    GPIO_InitTypeDef GPIO_InitStructure;

    /*3个IO口的配置(PA0、PA1、PA2)*/

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;

    GPIO_Init(GPIOA, &GPIO_InitStructure);

    

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;

    GPIO_Init(GPIOA, &GPIO_InitStructure);


    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;

    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /*IO和ADC使能时钟*/

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1|RCC_APB2Periph_GPIOA,ENABLE);

    RCC_ADCCLKConfig(RCC_PCLK2_Div6);


    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;

    ADC_InitStructure.ADC_ScanConvMode = ENABLE;

    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续转换

    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;

    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;

    ADC_InitStructure.ADC_NbrOfChannel = 3;

    ADC_Init(ADC1, &ADC_InitStructure);

    

    ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_239Cycles5);//通道一转换结果保存到ADCConvertedValue[0~10][0]

ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_239Cycles5););//通道二转换结果保存到ADCConvertedValue[0~10][1]

ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_239Cycles5); );//通道三转换结果保存到ADCConvertedValue[0~10][2]


  

    ADC_DMACmd(ADC1, ENABLE);//开启ADC的DMA支持

    ADC_Cmd(ADC1, ENABLE);


    ADC_ResetCalibration(ADC1);

    while(ADC_GetResetCalibrationStatus(ADC1));

    ADC_StartCalibration(ADC1);

    while(ADC_GetCalibrationStatus(ADC1));


}


做完这两步,ADCConvertedValue数组的值就会随输入的模拟电压改变而改变,在主函数中最好取多几次的平均值,再通过公式换算成电压单位。下面是主函数:


int main(void)

{

int sum;

u8 i,j;

float ADC_Value[3];//用来保存经过转换得到的电压值

ADC_Init();

DMA_Init();

ADC_SoftwareStartConvCmd(ADC1, ENABLE);//开始采集


while(1)

{

for(i=0;i<3;i<++)

{

sum=0;

for(j=0;j<10;j++)

{

sum+=ADCConvertedValue[j][i];

}

ADC_Value[i]=(float)sum/(10*4096)*3.3;//求平均值并转换成电压值

//打印(略)

}

//延时(略)

}

}


ADCConvertedValue的定义用了volatile修饰词,因为这样可以保证每次的读取都是从绝对地址读出来的值,不会因为被会编译器进行优化导致读取到的值不是实时的AD值。


最后提醒一下,接线测试的时候记得接上基准电压,就是VREF+和VREF-这两个引脚。如果不想外接线测试就将内部通道的电压读出来,这样就不用配置IO口了。


水平有限,仅供参考,错误之处以及不足之处还望多多指教。

推荐阅读

史海拾趣

BALLUFF公司的发展小趣事

BALLUFF公司的历史可以追溯到1921年,由格布哈德·巴鲁夫(Gebhard Balluff)创建的一家机械修理厂开始。这家修理厂起初规模较小,但巴鲁夫凭借其对机械技术的精湛理解和不懈追求,逐渐将业务发展壮大。他通过不断的技术创新和产品升级,使得修理厂逐渐转型为一家专注于生产精密部件、车件和铣件的企业。这一转变不仅奠定了BALLUFF公司的工业基础,也为其日后在传感器领域的崛起打下了坚实基础。

CQR SECURITY公司的发展小趣事

CQR SECURITY公司在追求商业成功的同时,也积极履行社会责任。公司定期举办网络安全宣传活动,提高公众的网络安全意识。同时,CQR还积极参与社会公益事业,为弱势群体提供网络安全支持和帮助。这种积极履行社会责任的举措不仅提升了CQR的品牌形象,还为公司赢得了社会的广泛赞誉。

这些故事虽然虚构,但反映了电子安全公司可能经历的一些普遍发展路径和挑战。希望这些故事能够为您提供一些启发和参考。如果需要更多关于特定公司的信息,建议您查阅相关新闻报道、行业分析报告或公司官网等渠道。

Fair Rite公司的发展小趣事

到了1955年,Fair Rite的产品开始被广泛应用于娱乐电子行业。公司成功开发出适合电视和无线电设备使用的铁氧体元件,为当时的家庭娱乐生活提供了可靠的技术支持。随着技术的不断进步和市场需求的增长,Fair Rite逐渐扩大了产品线,并开始进入其他领域。

Cavium Networks公司的发展小趣事

随着通信网络的不断升级和转型,Cavium Networks 看到了市场的巨大潜力。在2008年,尽管全球经济风暴肆虐,但 Cavium Networks 依然保持着强劲的增长势头。同年11月,公司宣布收购 W&W Communications 公司,这一举措进一步拓展了 Cavium Networks 的业务范围,增强了其在网络通信领域的竞争力。通过收购,Cavium Networks 获得了更多的技术资源和市场份额,为其后续的发展提供了有力的支持。

淩志比高公司的发展小趣事

随着国内市场的逐渐饱和,淩志比高公司开始将目光投向海外市场。公司制定了国际化战略,积极拓展海外市场,与多家国际知名企业建立了合作关系。通过不断的市场拓展和品牌推广,淩志比高逐渐在国际市场上获得了认可。

EFC [Electronic Film Capacitors, Inc.]公司的发展小趣事

在电子薄膜电容器领域,EFC公司一直以其技术创新而闻名。公司创始人李博士带领着一支由资深工程师组成的研发团队,不断挑战技术极限。某年,他们成功研发出一种新型材料,这种材料不仅大大提高了电容器的性能,还降低了制造成本。这一突破性的创新使EFC公司在市场上脱颖而出,赢得了大量客户的青睐。

问答坊 | AI 解惑

新建楼宇防雷电设计

根据5月16日颁布实施的《厦门市建设项目防雷工程设计审核规定(试行)》(以下简称《规定》(试行)),今后所有新建、改建、扩建建设项目在设计审核和竣工验收时,建设方都必须向审核、验收部门提供单独列出的防雷设计专篇。昨日上午,市气象局召 ...…

查看全部问答>

成批量的买件,你习惯去哪里

设计好一套电路板,需要购买器件,大家一般都去哪里买啊,网上还是当地电子市场。派睿,艾睿还是其他的,大家说说啊…

查看全部问答>

nrf905在凌阳61上的问题!!大家帮帮忙!急啊

#include \"SPCE061A.H\" #include \"math.h\" #include \"bit.h\"   //位操作            1.8.4版 #define  uchar  unsigned char #define LED     & ...…

查看全部问答>

SMbus 总线与 I2C总线有什么区别?

同一个设备EEPROM,接在I2C总线上,和连在SM bus上,写驱动时有什么差别吗? 以前写过I2C主控制器的驱动, 现在要写一个SM bus控制器的驱动,二者有什么差别啊? …

查看全部问答>

MMU地址映射问题

各位大侠好! 小弟最近在调试wince5.0的bootloader时遇到了问题,是这样的:cpu是ATMEL9261(926ej-s),在startup.s中没有启用MMU之前程序运行很好!但是当执行完mcr      p15, 0, r1, c1, c0, 0 后,灯也没亮,程序就飞了,请大家赐教 ...…

查看全部问答>

晶振的问题

我用了一个有源的晶阵 是5v电压供电的那一种 输出是ttl电平 可是我下面接了一个非门 可是没有输出啊 是不是驱动能力不构 如何提高呢 感谢啊…

查看全部问答>

关于CCS的限制问题

我的仿真器附带光盘上有一个CCSv4,上面说是32K限制版,这个32K指的是存储器的大小吗?假如我想使用TMS320F28035中的128K的Flash,能用吗?…

查看全部问答>

单电源边双电源?

单电源变双电源的原理是什么?能给个电路吗 6v电池到正负5v的? 谢谢…

查看全部问答>

用51单片机进行控制射进简易计算器

通过Proteus与keil C的方针联调设计一个简易计算器,计算器可实现6位整数的四则运算,由LED数码显示运算数据以及结果,其中出发运算无需实现小数部分。   QQ 261246707欢迎交流 现在编程方面有一些问题 希望大家给点帮助…

查看全部问答>

日企PCB专业LAYOUT工作经验6年,求兼职

本人日企PCB专业LAYOUT工作经验6年,有2-10层基板级设计经验,有丰富EMC、SI,EMI,ESD,PI设计经验,现求兼职PCB LAYOUT,有意请:ximigac@163.com…

查看全部问答>