最近要使用STM32F103C8T6来做个数字万用表,于是开始学习STM32,要用到32内部的12位ADC
等于是刚刚接触STM32,一切从零开始,现在分享下如何简单的使用ADC
RCC:
这个是用来设置时钟的,比如我可以设置我的系统时钟频率等
TIM:
顾名思义,是timer的缩写,是定时计数器.
RCC 和 TIM的区别:
RCC用来设置我32的系统时钟频率或者是一些其他硬件的时钟频率
而TIM是在某个时钟频率下工作的一个计数器,这个频率可以来自RCC的设置,也可以来自外部
注意,RCC设置频率的来源也可以是外部或者内部(内部不准确,我们一般不用,这也是为什么要外接8MHz晶振的原因),而后产生一个内部时钟频率送给TIM
为什么要说时钟呢?因为我要使ADC的采样率达到最大,也就是1MHz的采样率,而达到这样的采样率就需要设置ADC的时钟频率,ADC最大时钟频率是14MHz。这两者什么关系呢?
后面介绍,总之先知道要达到最大的采样率就需要设置我们的时钟
那么就让我开始来配置RCC吧!
No.1
首先我们应该用外部接的8MHz晶振来做时钟源。外部高速晶振:HSE;内部高速晶振:HSI
用 void RCC_HSEConfig(u32 RCC_HSE) 这个函数来启动,内部参数设置 RCC_HSE_ON
RCC_HSEConfig(RCC_HSE_ON);//开启8MHz外部晶振
然后检测外部高速晶振是否正常启动
用 RCC_WaitForHSEStartUp(); 这个函数, 这个函数返回 SUCCESS 这个参数则代表正常启动
ErrorStatus HSEStartUpStatus;//设置标志位
HSEStartUpStatus = RCC_WaitForHSEStartUp();
if(HSEStartUpStatus == SUCCESS)//若外部晶振正常启动
{
/* Add here PLL ans system clock config */
}
这时候我们要通过PLL锁相环来使 外部接的晶振作为输入,输出另一个稳定频率的时钟信号
即我们要用PLL来进行倍频
这里我们设置PLL输出 = 8MHz * 7 = 56MHz (那就是要进行7倍频)
//RCC_PLLSource_HSE_Div1 意思是 PLL的输入时钟 = HSE时钟频率
//RCC_PLLMul_7 表示 7倍频
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_7);
然后输出
RCC_PLLCmd(ENABLE);//PLL输出使能
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);//等待PLL输出
到目前为止我们得到一个56MHz的时钟频率
No.2
然后我们要利用PLL输出的这个频率作为我们STM32的系统时钟
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);//设置系统时钟为56MHz
while(0x08 != RCC_GetSYSCLKSource());//等待系统时钟被正确设置
这样系统时钟就被我们设置好了
接下来的任务就是要设置AHB时钟了
what?AHB时钟又是个啥?
AHB(Advanced High performance Bus)高级高性能总线 或者我们叫它 系统总线
就是我们要设置STM32内部总线的时钟频率
而且AHB又有高低速之分,也就是说我们要设置两个时钟分别给高速AHB和低速AHB
AHB掌管着DMA时钟,SRAM时钟和FLITF时钟,它并不直接管ADC的时钟,那我们为什么还要设置它呢?先别急,往下看
RCC_HCLKConfig(RCC_SYSCLK_Div1);//设置AHB时钟(HCLK)
RCC_PCLK2Config(RCC_HCLK_Div1);//设置高速AHB时钟 PLCK2为56MHz (最大72MHz)
RCC_PCLK1Config(RCC_HCLK_Div2);//设置低速AHB时钟 PLCK1为28MHz (最大36MHz)
//注:这里面Div1表示一倍分频,也就是不分频。 PLCK2 = HCLK = 56MHz
//Div2表示2倍分频 PLCK1 = HCLK / 2 = 28MHz
总线设置好了我们就终于可以开始设置ADC的时钟频率了
但这里又要提到一个名词APB
APB(Advanced Peripheral Bus)外围总线
这个才是直接管ADC时钟的总线,APB又分APB1 和 APB2
APB1管TIMx (x = 2, 3, 4 ……) WWDG,SPI2, USART2, USAT3, I2C, CAN的时钟
APB2管TIM1, GPIOx, ADC1, ADC2, SPI1, USART1的时钟
很明显我们只需要对APB2的ADC功能进行设置就行
//使能ADC & GPIOA
//这里我用ADC1采样,PA0端口,具体看各个芯片的数据手册
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
现在来设置ADC了!!!
来上函数 void ADC_ADCCLKConfig(u32 RCC_ADCCLKSource)
看看这个参数 RCC_ADCCLKSource: 定义ADCCLK,该时钟源自APB2时钟(PCLK2)
懂了吧,为啥非要对AHB进行设置,ADC的时钟来源于AHB的PLCK2
//RCC_PCLK2_Div4 意思是 ADC时钟 = PCLK2 / 4 = 14MHz
RCC_ADCCLKConfig(RCC_PCLK2_Div4);//ADC最大时钟频率是14MHz
至此,我们就对ADC时钟设置完了,一般来说,我们的STM32系统时钟都是设置的是72MHz,但这里我们为什么非要费那么老劲来设置RCC呢?还是上面这个函数,它的参数一共就4个
Div2 Div4 Div6 Div8 也就是2 4 6 8 分频。
72MHz并不能通过这四个分频得到14MHz最大时钟,所以我们特地设置56MHz,通过4分频,产生14MHz的ADC最大时钟频率。72MHz最大能做到 72 / 6 = 12MHz
来吧,上一份完整的RCC代码
static void RCC_ConfigInitail()
{
ErrorStatus HSEStartUpStatus;
FlagStatus Status;
//RCC配置
RCC_DeInit();//重置
RCC_HSEConfig(RCC_HSE_ON);//外部8MHz晶振启动!
HSEStartUpStatus = RCC_WaitForHSEStartUp();
if(SUCCESS == HSEStartUpStatus)//若启动成功
{
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_7);//56MHz PLL输出
RCC_PLLCmd(ENABLE);//PLL输出使能
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);//等待PLL输出成功
//设置系统时钟56MHz
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
while(0x08 != RCC_GetSYSCLKSource());//等待设置成功
RCC_HCLKConfig(RCC_SYSCLK_Div1);
RCC_PCLK2Config(RCC_HCLK_Div1);//PLCK2 56MHz
RCC_PCLK1Config(RCC_HCLK_Div2);//PLCK1 28MHz
//使能APB2外设时钟 ADC & GPIOA
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div4);//ADC1时钟频率 14MHz
}
}
等有时间再更新后面的
2018 / 4 / 4 更新……
我是分割线
好了,我们现在来说一说ADC的配置吧
ADC的配置就没什么值得说的,它没有定时器难理解
直接上代码,注释就是解释
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
//GPIO配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//GPIO采用模拟输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//对PA0初始化
//因为我使用的是ADC1_IN0就是通道0,而这对应STM32C8T6的PA0口
//对于其它型号的要具体看芯片手册
//ADC配置
ADC_DeInit(ADC1);//重置
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//ADC1和ADC2单独工作,互不影响
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//ADC单次采样,即采样一次就停止
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //ADC单通道采样(ENABLE是多通道扫描)
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//软件触发ADC
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1;//ADC通道转换数目,我们只用一个ADC,那么就是1
//对于多通道采集才使这个值 >= 2, 取值范围是1~16
ADC_Init(ADC1,&ADC_InitStructure);//初始化
ADC_Cmd(ADC1, ENABLE);//使能
//ADC校准
ADC_ResetCalibration(ADC1);//重置ADC校准器
while(ADC_GetResetCalibrationStatus(ADC1));//等待重置结束
ADC_StartCalibration(ADC1);//开始校准
while(ADC_GetCalibrationStatus(ADC1));//等待校准完成
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_1Cycles5);
最后一句话是重点
这句话是重点,看函数名就知道是对ADC通道配置的
这里我们就用一个通道,所以就只写一句,如果有多通道,那么就多写几句,参数变一下即可
要知道ADC转换周期是12.5个ADC时钟周期 而ADC_SampleTime_1Cycles5就是ADC采样时间为
1.5个周期,所以一共12.5 + 1.5 = 14个周期
14MHz / 14 = 1MHz,这样我们就能得到1MHz最大的采样率了
除了1.5以为,STM固件库还给出了其它的一些稀奇古怪的周期
例如:
7.5
13.5
28.5
41.5
55.5
71.5
239.5
呐,就这,ADC最基本的应用就配置完成了
剩下的就是些不痛不痒的代码了
static u16 Get_ADC_Value()
{
ADC_SoftwareStartConvCmd(ADC1, ENABLE);//软件启动,ADC开始转换
while(ADC_GetSoftwareStartConvStatus(ADC1));//等待转换完成
return ADC_GetConversionValue(ADC1);//返回得到的ADC值
}
到目前为止ADC的最简单的配置就完成了
下面是我个人的情况,可以选择性阅读
下面是我实力分析为什么在高采样率的情况下,上面这些普通的代码不适合测AC交流
这么说吧,这是我一开始写的代码,目的是为了采集交流信号(这不废话么)
要求是测得10HZ ~ 100KHZ的交流信号的有效值
于是我想采用计算真.有效值的方法进行计算
真.有效值的计算公式是
exp( 1/T * ∫ f(x)² dt ) (积分区间是0 ~ T)
将之变成离散的信号处理,那么公式就又积分变到求和
exp( 1/n * ∑ f(x)² )(求和的话,那就是从0 ~ n)
那么也就是说我需要采样,一个周期采样n个点,n越多,计算得到的真有效值就越接近真值,跟做个示波器差不多了
如此一来我写了如下的程序
//times表示一个周期采样的个数,即上面说的n
float Get_ADC_EffectiveValue(u8 times)
{
float sum = 0.0;//∑ f(x)²
float temp = 0.0;//得到返回的ADC值
u8 t = 0;
for(t = 0; t < times; t++)
{
//12位采样(0xFFF = 4096),STM32是3.3V供电
//所以 3.3 / 0xFFF ≈ 0.000805664
temp = (Get_ADC_Value() - 2048) * 0.000805664f;
sum += temp * temp;
}
return (float)sqrt(sum / times);
}
这里我是通过电路将AC信号控制在0 ~ 3.3V,也就是原本的AC信号加上一个DC直流偏置
直流偏置 = VCC / 2,所以我要Get_ADC_Value()之后减去直流偏置,得到原来真实的量
这里再补充一个小细节,再0.000805664后加上个f,意思是告诉CPU这是一个float型常量,别给我弄成个double型了,因为STM32是32位CPU,那么也就是说在32位以下的数据计算速度都差不多。
float占4个字节(32位),double占8个字节(64位),很明显处理float要比double快多了,以后建议能用float就尽量用float
那么好了,我的具体方案是这样的,通过TIM的输入捕获功能来捕捉到信号的上升沿,在通过计算差值得到周期T,我一共捕捉11个上升沿,这样我就得到了10个周期,求平均值后使误差减小
然后ADC开始采样,根据得到的周期来确定一个周期我要采样几个点,就这样
然而后面我发现一系列问题,导致我现在否定了这个方案。
来,我们来分析分析
while(1)
{
反复分析;//2333333
}
首先我们的定时计数器是工作在56MHz下的,我设置的可计数值为65535,不分频
那么有可能这个周期比较长,我计数器在从0计数到65535后,还没捕捉到第二个上升沿,那么我的这个计数器就会溢出。不要紧,我设置一个全局变量u8 OverFlow = 0;//记录溢出次数
每当我的计数器溢出的话,OverFlow ++;
我计数的Counter = f_TIM / f_IN
f_TIM表示计数器频率即56MHz,f_IN表示输入信号的频率
假如输入信号频率f_IN = 100KHz,那么Counter = 560,嗯,没什么问题
假如f_IN = 10Hz,那么Counter = 5.6 * 1e6,这个可以么?不会太大么?
OverFlow 最大计数255,所以最大Counter = 255 * 65536,这么看有点看不出来,不是很直观
反过来求一下5.6 * 1e6 / 65536 后向下取整为85,也就是说OverFlow 最大只会计数到85,也不是问题
也有人会问,中断不管么?有时间误差啊!
管什么?最多85 + 1次中断,+1是因为捕获到上升沿。急什么,忽略这个时间误差
假如我只知道f_IN,我想知道我的f_TIM应该设置多少?
这样,我们假设Counter >= 100, 那么我计数器从0计数到1的时间间隔t = T_IN / Counter
也就是最小误差(仪器误差)t <= T_IN /100,嗯,1%不到的误差,可以忽略了。
结论一:即当Counter >= 100 时,可以忽略计算周期的误差
那么我就取Counter >= 100
所以f_TIM >= 100 * f_IN,STM32最大72M时钟频率,所以理论上能精确计算的最大输入信号周期的为720KHz。当然,这不是绝对的,你可以通过输入捕获预分频,来扩大这个值,误差还是 1 / Counter这里我们不做过多讨论
由于我们的Counter最小为560,所以根据结论一 可知,一个周期下,输入捕获计算周期的误差忽略,再加上我还通过取平均来减小误差,使得计算得到的误差更小了
结论二:输入捕获计算周期的误差可以忽略不计,不需要管输入捕获了
接下来我们来分析ADC
(嘿嘿嘿!好玩的来了!)
我们得到了输入信号的周期T后,就可以计算一个周期下,ADC采样的次数了
设一个周期内,ADC采样次数为n
则有n = f_ADC / f_IN
f_ADC为ADC的采样频率,f_IN为信号输入频率
那么我们还采用刚才的思路看看,已知f_IN,取n >= 100
则 f_ADC >= 100 * f_IN
确实,前面说过,n越大,越接近积分得到的结果
但是!!!CPU计算也需要时间啊
看看这段代码
for(t = 0; t < times; t++)// 2n
{
temp = (Get_ADC_Value() - 2048) * 0.000805664f;// 3n
sum += temp * temp;// 3n
}
这时候我们就不得不计算程序的时间复杂度了
我就假设+ - * / 都是一样的计算时间,都是一个执行周期(真正的情况下乘除要比加减慢,更不要说你根本不知道编译器翻译成汇编之后会有多少条指令,我这么算算是少的)
t < times 需要n次判断
t++需要n次计算
我就假设Get_ADC_Value()得到值后存入内存不要时间
Get_ADC_Value() - 2048需要n次计算
* 0.000805664f需要n次计算
赋值到temp需要n次计算
temp * temp需要n次计算
sum + (temp * temp)需要n次计算
赋值到sum需要n次计算
呵呵,一共是8n的时间复杂度
ARM3官方给出的平均计算速度是1.25MIPS/MHz
意思是1MHz频率下,每秒执行1.25M条指令。我们是56MHz主频,那么就是每秒56M * 1.25条指令
执行一条指令需要 1 / (56 * 1.25 * 1e6)
也就是说光是用在计算上的时间就需要
t = 8n / (56 * 1.25 * 1e6) ≈ 0.114n (单位us)
我们来看看n取100时候的情况
t = 11.4us, 而对于100KHz信号来说,100KHz = 10us,呵呵,光计算的时间就差了一个周期!!
对于10KHz的信号也会有10%的误差
只有1KHz以下的信号才不会收到影响…………
连采集到的信号都不对,谈什么求和逼近积分……做梦吧,梦里啥都有……
结论三:普通方法不能求得1KHz以上信号的准确 真有效值
正所谓鱼和熊掌不可兼得……
但天无绝人之路,我还有DMA!!!意不意外,惊不惊喜!没想到吧!(此处自动脑补表情包)
可是我现在还不会……等我学会了再回来更新,就酱,如果本文对你有收获,想收藏就收藏,想点赞就点赞
未经允许,不得转载,谢谢
未经允许,不得转载,谢谢
未经允许,不得转载,谢谢
重要的事情说三遍!!!