?導讀:《藍橋杯嵌入式組》專欄文章是博主2019年參加藍橋杯的嵌入式組比賽所做的學習筆記,在當年的比賽中,由于忙于準備考研及保研相關工作,博主僅僅參加了當年的省賽,并獲得了省賽一等獎的成績。成績雖談不上最好,但至少問心無愧。如今2021年回頭再看該系列文章,仍然感觸頗多。為了能更好地幫助到單片機初學者,今年特地抽出時間對當年的文章邏輯和結構進行重構,以達到初學者快速上手的目的。需要指出的是,由于本人水平有限,如有錯誤還請讀者指出,非常感謝。那么,接下來讓我們一起開始愉快的學習吧。
“一葉遮目,不見泰山”。不論何事,只有把握事情的總體趨勢,才能做到心中有數。
1個留待補充的程序:
“為了確保不產生丟失數據的情況,開啟 DMA 傳輸模式,將數據傳輸到內存中”嘗試編程
一、ADC入門
1、基礎知識
模數轉換器(Analog To Digital Converter)簡稱 ADC(也可以寫成 A/D),是指將連續變化的模擬信號轉換為離散的數字信號的器件。 ADC 分為積分型、逐次逼近型、并行/串行比較型、Σ-Δ型等多種類型,STM32F103 自帶的 ADC 屬于逐次逼近型。
逐次逼近型 ADC 與天平稱物重非常相似,從高位到低位逐位比較。首先從最重的砝碼開始試放,與被稱物體行進比較,若物體重于砝碼,則該砝碼保留,否則移去,然后用次重砝碼繼續比較,照此一直到最小一個砝碼為止,將所有留下的砝碼重量相加,就得此物體的重量。逐次逼近型 A/D 轉換器,就是將輸入模擬信號與不同的參考電壓作多次比較,使轉換所得的數字量在數值上逐次逼近輸入模擬量對應值。
2、STM32F103RBT6的ADC時鐘及轉換時間
藍橋板載STM32F103RBT6擁有2路12位(0~4096)ADC,ADC掛載在APB2總線上,且ADC最大時鐘不超過14MHz。所以當APB2總線設置為72M,有必要對其分頻再用于ADC,分頻用到的庫函數是:void ADC_ADCCLKConfig(u32 RCC_ADCCLKSource);
ADC的轉換時間 = 采樣時間 + 12.5周期
采樣時間和實際電路有著莫大的關系,但是對于藍橋的板子而言,對于轉換速度沒有太大要求我們一般設置為:ADC_SampleTime_239Cycles5
239.5個周期即可。
3、ADC輸入通道
注意到了兩個ADC模塊,共用了一些通道,不同ADC應用不同通道時,可以同時進行采樣和轉換,但不可以對相同通道同時采樣。
板子對應ADC部分原理圖
PB0管腳可復用為ADC_IN8
ADC1和ADC2兩個通道的都歐克,所以本博客選用了ADC1。
4、ADC的注入組和規則組
在使用 ADC 外部通道時,可以設定為規則組和注入組。規則組就是設定好轉換順序后,按照規則正常轉換;注入組類似于中斷,可以插隊,當觸發信號觸發注入組通道時,優先轉換注入組,轉換完后再繼續轉換規則組;如果正在轉換規則通道期間,注入通道被觸發,當前規則組轉換被復位,注入通道序列被以單次掃描方式轉換,完成轉換后恢復上次被中斷的規則通道轉換。規則組最多可以使用 16 個通道,注入組最多可以使用 4 個通道。
藍橋板子也沒有用到多么復雜,我們就直接使用規則組就好了。
5、ADC的單通道和多通道
當“轉換組”只有一個通道轉換時稱之為單通道模式,當有多個通道按順序轉換時稱之為多通道模式或者掃描模式。
藍橋板子,使用單通道即可。
6、ADC的單次轉換和連續轉換
當規則組或注入組的通道按照設定順序執行一次采轉換后即停止工作,這種模式稱之為單次轉換模式;如果執行完一次轉換后,ADC 沒有停止,而是立即啟動新一輪轉換,這種模式稱之為連續轉換模式。
藍橋板子,使用單次轉換即可。
7、觸發源
觸發啟動轉換:觸發啟動轉換分為兩種方式,分別是軟件觸發和外部事件觸發(外部是相對于 ADC 外設來講),其中外部事件觸發又分為定時器觸發和外部觸發(這里的外部指的是芯片外部信號)。
藍橋板子,使用軟件觸發即可
8、轉換后的數據儲存
①、規則組存儲
在獨立 ADC 模式下,規則組通道轉換完成后,轉換后的數據被存儲在對應的 ADC 規則數據寄存器 ADC_DR 的低 16 位。
雙 ADC 模式是 ADC1 和 ADC2 同時采集某些參數,比如要獲取瞬時功率需要同時采集電壓和電流參數才能準確計算結果,這種場合就必須使用雙 ADC 模式。這種模式下 ADC1所對應的 ADC_DR 的高 16 位存儲 ADC2 的規則數據,低 16 位存儲 ADC1 的規則數據。
由于 ADC 的精度是 12 位,因此在將 ADC 的 12 位數據存入 16 位的數據寄存器中時,可以通過 ADC_CR2 寄存器的 ALIGN 位來選擇數據是左對齊還是右對齊。
規則通道最多有 16 個通道,但規則數據寄存器只有一個,因此當使用多通道轉換時,前一個轉換的通道數據,會被后一個通道轉換的數據覆蓋掉,因此理論上必須在后一個通道轉換完畢之前就把數據取走。
為了確保不產生丟失數據的情況,開啟 DMA 傳輸模式,將數據傳輸到內存中是一個好辦法,這個到時候作為擴展例程寫。
②、注入組存儲
ADC 注入組最多有 4 個通道,每個通道都有對應的數據寄存器 ADC_JDRx(x=1..4),ADC_JDRx 是 32 位的,高 16 位保留,低 16 位用來保存數據,選擇數據是左對齊還是右對齊都由 ALIGN 決定。
9、中斷處理
如果打開相應的中斷,有三種情況可以進入中斷。 (1)規則通道轉換完成中斷
- 轉換數據被存儲在 16 位的 ADC_DR 寄存器中。
- EOC(轉換結束)標志被置位。
- 如果設置了 EOCIE 位,則產生中斷。
(2)注入通道轉換完成中斷
- 轉換數據被存儲在 16 位的 ADC_DRJx 寄存器中。
- JEOC(注入轉換結束)標志被置位。
- 如果設置了 JEOCIE 位,則產生中斷。
(3)模擬看門狗中斷 如果開啟了模擬看門狗中斷,并且設置ADC低閾值ADC_LTR和高閾值ADC_HTR, 當采集到的電壓高于高閾值或者低于低閾值時,就會產生模擬看門狗中斷。
關于ADC中斷這部分,這里也沒有具體涉及到,等日后做題做到了再補充吧。目前也只用到了查詢的方式。
10、ADC校準
ADC 有一個內置自校準模式,可以大幅減小由于內部電容的變化而造成的精準度誤差。通過設置 ADC_CR2 寄存器的 CAL 位啟動校準,一旦校準結束,CAL 位被硬件復位。建議在每次上電時執行一次 ADC 校準,啟動校準前,ADC 必須處于上電狀態(ADON=‘1’)(言外之意必須先使能ADC),至少超過兩個 ADC 時鐘周期。
校準代碼是固定的,參考下面的代碼即可。
11、 使能通道觸發轉換
規則組和注入組觸發方式方式分為軟件觸發和外部觸發,其中外部觸發包括定時器觸發和外部信號觸發。如果設置了規則組或注入組觸發方式,還需要使能相應觸發,保證在觸發到來時啟動轉換。
規則組軟件觸發相應函數為void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
12、轉換結束標志位
看手冊介紹
可以由軟件清除或者讀取數據寄存器清除!
13、總結配置流程
1、初始化 ADC1 通道引腳(內部溫度傳感器和參照電壓通道不需要進行引腳初始化) 2、配置 ADC1 中斷優先級(如果使用 ADC 中斷,本節未使用) 3、使能 ADC1 外設時鐘 4、設置 ADC1 預分頻系數 5、復位 ADC1 6、配置 ADC1 初始化結構體 7、設置規則組轉換順序和采樣時間 8、使能 ADC1 中斷(本節未使用) 9、使能 ADC1 外設 10、校準 ADC1 11、使能 ADC1 軟件觸發或外部觸發轉換 12、編寫中斷服務函數(如果使用 ADC 中斷,本節未使用)
二、主要代碼
main.c
/*******************************************************************************
* 文件名:main.c
* 描 述:
* 作 者:CLAY
* 版本號:v1.0.0
* 日 期: 2019年1月27日
* 備 注:ADC轉換實現電壓測量
*
*******************************************************************************
*/
#include "config.h"
#include "led.h"
#include "key.h"
#include "timer.h"
#include "beep.h"
#include "lcd.h"
#include "stdio.h"
#include "rtc.h"
#include "adc.h"
u8 ADC_Flag = 0;
u8 string[20];
float adc_val;
int main(void)
{
STM3210B_LCD_Init();
LCD_Clear(Blue);
LCD_SetBackColor(Blue);
LCD_SetTextColor(White);
LEDInit();
KeyInit();
BeepInit();
TIM2Init(2000, 72);//定時2ms
ADC1Init();
while(1)
{
KeyDriver();
if(ADC_Flag )
{
ADC_Flag = 0;
adc_val = Get_ADC(8) * 3.3 / 4096;
sprintf((char*)string,"ADC_VAL : %.2f ",adc_val);
LCD_DisplayStringLine(Line2, string);
}
}
}
void KeyAction(int code)
{
if(code == 1)//按下B1,切換燈狀態,蜂鳴器鳴叫0.1s
{
GPIOC->ODR ^= (1<<8);//PC8不斷取反
GPIOD->ODR |= (1<<2);//PD2置1,使能573鎖存器
GPIOD->ODR &= ~(1<<2);//PD2清0,關閉573鎖存器
Beep(100);
}
else if(code == 2)
{
Beep(-1);
}
else if(code == 3)
{
Beep(0);
}
else if(code == 4)
{
}
}
adc.c
#include "adc.h"
void ADC1_IOInit(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//使能PB口時鐘
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;//選中PB0引腳
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模擬輸入
GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化IO
}
void ADC_Cali(void)
{
ADC_ResetCalibration(ADC1);//使能復位校準
while(ADC_GetResetCalibrationStatus( ADC1));//等待復位校準結束
ADC_StartCalibration( ADC1);//開啟AD校準
while(ADC_GetCalibrationStatus(ADC1));//等待校準結束
}
void ADC1Init(void)
{
ADC_InitTypeDef ADC_InitStructure;
ADC1_IOInit();//ADC的GPIO配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//使能ADC時鐘,APB2總線
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//ADC最大時鐘不超過14M,這里盡心了對APB2時鐘進行6分頻=12M
ADC_DeInit(ADC1);//復位ADC
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//ADC獨立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE;//通道模式選擇單通道
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//轉換模式選擇單次轉換模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//規則組觸發轉換方式選擇轉換由軟件觸發啟動
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//數據格式選擇右對齊
ADC_InitStructure.ADC_NbrOfChannel = 1;//通道數目為1
ADC_Init(ADC1,&ADC_InitStructure);//初始化結構體
ADC_Cmd(ADC1, ENABLE);//使能ADC
ADC_Cali();//ADC校準
}
u16 Get_ADC(u8 channel)
{
u16 temp;
ADC_RegularChannelConfig(ADC1, channel, 1, ADC_SampleTime_239Cycles5);//ADC采樣時間239.5周期 總轉換時間=采樣時間+12.5周期
ADC_SoftwareStartConvCmd( ADC1,ENABLE);//使能ADC1軟件觸發轉換,觸發一次ADC轉換
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == 0);//等待ADC轉換完畢
temp = ADC_GetConversionValue( ADC1);//獲取轉換結果,注意此時已經自動清除EOC位
ADC_SoftwareStartConvCmd( ADC1,DISABLE);//失能ADC轉換
return temp;
}
adc.h
#ifndef _ADC_H
#define _ADC_H
#include "config.h"
void ADC1Init(void);
u16 Get_ADC(u8 channel);
#endif
stm32f10x_it.c
extern u8 ADC_Flag;
void TIM2_IRQHandler(void)
{
static u16 tmr500ms = 0;
if(TIM_GetITStatus(TIM2, TIM_FLAG_Update))
{
TIM_ClearITPendingBit(TIM2, TIM_FLAG_Update);
tmr500ms++;
KeyScan();
BeepScan(2);//2ms掃描
if(tmr500ms >= 250)
{
tmr500ms = 0;
ADC_Flag = 1;
}
}
}
三、注意事項
定時器設定500ms讀取一次AD值
結語:以上就是本篇文章的全部內容啦,希望大家可以多多支持我的原創文章。如有錯誤,請及時指正,非常感謝。