在本次項目中,限於空間要求我們選用了STM32F030F4作為控制芯片。這款MCU不但封裝緊湊,而且自帶的Flash空間也非常有限,所以我們選擇了LL庫實現。在本文中我們將介紹基於LL庫的ADC的DMA采集方式。
1、概述
這次我們使用DMA方式實現對AD的采集,在遺忘我們使用HAL庫和標准庫都做過,這次我們使用LL庫來實現。接下來我們簡單了解一下STM32F030F4中的ADC和DMA。
首先看一看ADC,STM32F030F4是12位的ADC。它有多達19個多路復用通道,允許它測量來自16個外部和2個內部源的信號。各種通道的A/D轉換可采用單通道、連續通道、掃描通道或不連續通道進行。ADC的結果存儲在左對齊或右對齊的16位數據寄存器中。ADC結構圖如下:

這次我們只使用第1路外部輸入。接下來說一說DMA,直接內存訪問(DMA)用於在外設和內存以及內存到內存之間提供高速數據傳輸。DMA可以在沒有任何CPU操作的情況下快速移動數據。這使CPU資源可以用於其他操作。STM32F030F4中的DMA控制器有5個通道,每個通道用於管理來自一個或多個外圍設備的內存訪問請求。它有一個仲裁器來處理DMA請求之間的優先級。DMA結構圖如下:

這次我們也使用DMA的第1通道。
2、ADC配置
在使用之前我們需要對ADC和DMA的相關寄存器驚醒必要的配置,才能實現我們想要的功能。我們來看看ADC需要配置的寄存器。ADC需要注意的寄存器主要有兩個:ADC控制寄存器(ADC_CR)和ADC配置寄存器1(ADC_CFGR1)。首先我們來說說ADC控制寄存器(ADC_CR),器結構如下:

關於ADC控制寄存器(ADC_CR),有幾個設置需要說明一下。
ADCAL:ADC校准,設置該位可以軟件啟動校准,校准完成硬件會復位掉這一位。需要注意的是只有ADC處於失能狀態,軟件對ADCAL的操作才是有效的。也就是說軟件對ADCAL操作時,ADC控制寄存器(ADC_CR)必須是全復位狀態,即ADCAL=0,ADSTART=0,ADSTP=0, ADDIS=0和 ADEN=0。
ADSTART: ADC啟動轉換命令。需要注意只有在ADC已啟用,並且沒有禁用ADC的掛起請求。也就是說ADEN=1和ADDIS=0時,軟件對ADSTART的操作才有效。
ADEN: ADC使能命令。只有在ADC控制寄存器(ADC_CR)處於全復位狀態,即ADCAL=0,ADSTART=0,ADSTP=0,ADDIS=0 和 ADEN=0下,軟件對ADEN的操作才有效。這就有一個問題,如果你使用了ADCAL必須等校准完成,才能使能,否則無效。
接下來我們看一看ADC配置寄存器1(ADC_CFGR1),其結構如下:

關於ADC配置寄存器1(ADC_CFGR1),我們需要關注:CONT(轉換模式)、EXTEN[1:0](外部觸發使能)、DMACFG(DMA訪問配置)、DMAEN(DMA訪問使能)。需要說明的是,這幾個配置都必須在啟動轉換前完成配置,即配置時ADSTART=0。
3、DMA配置
配置了ADC還需要配置DMA才能實現我們的想法。關於DMA的配置我們主要說一下4個寄存器:DMA通道配置寄存器(DMA_CCRx)、DMA通道數據數量寄存器(DMA_CNDTRx)、DMA通道外設地址寄存器(DMA_CPARx)、DMA通道內存地址寄存器(DMA_CMARx)。
首先,我們來看看DMA通道配置寄存器(DMA_CCRx),其結構如下:

對於DMA通道配置寄存器(DMA_CCRx),我們需要關注如下位:MSIZE[1:0](內存大小)、PSIZE[1:0] (外設大小)、MINC(內存的增加模式)、PINC(外設增加模式)、CIRC(循環模式)、DIR(數據傳輸方向)、EN(通道使能)。除通道使能外,其它均可通過初始化函數進行配置。
接下來,我們來看看DMA通道數據數量寄存器(DMA_CNDTRx),其結構如下:

其實DMA通道數據數量寄存器(DMA_CNDTRx)用於配置傳送數據的個數,如果是往內存中寫,就是內存緩沖區的大小,單位與配置寄存器中MSIZE和PSIZE有關。接下來,我們來看一看DMA通道外設地址寄存器(DMA_CPARx),其結構如下:

對於DMA通道外設地址寄存器(DMA_CPARx),就是存儲外設的地址,如果我們的外設是ADC,那就是ADC的地址。最后,我們來看一看DMA通道內存地址寄存器(DMA_CMARx),其結構如下:

對於DMA通道內存地址寄存器(DMA_CMARx),其存儲的就是對應的變量在內存中的地址,就是我們開辟的數據緩存區的首地址。
4、軟件實現
我們已經說明了ADC和DMA的配置,在這一小節,我們將根據我們前面的分析實現代碼。首先來實現ADC的配置代碼。
/* ADC 初始化配置 */
static void ADC_Init_Configuration(void)
{
LL_ADC_InitTypeDef ADC_InitStruct = {0};
LL_ADC_REG_InitTypeDef ADC_REG_InitStruct = {0};
LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
/* ADC相關外設時鍾使能 */
LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_ADC1);
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
/**ADC GPIO 配置:PA0 ------> ADC_IN0 */
GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
GPIO_InitStruct.Mode = LL_GPIO_MODE_ANALOG;
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* ADC DMA初始化 */
LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PRIORITY_LOW);
LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_CIRCULAR);
LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PERIPH_NOINCREMENT);
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MEMORY_INCREMENT);
LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PDATAALIGN_WORD);
LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MDATAALIGN_WORD);
LL_DMA_SetDataLength(DMA1,LL_DMA_CHANNEL_1,ADBufferSize);
LL_DMA_SetPeriphAddress(DMA1,LL_DMA_CHANNEL_1,LL_ADC_DMA_GetRegAddr(ADC1,LL_ADC_DMA_REG_REGULAR_DATA));
LL_DMA_SetMemoryAddress(DMA1,LL_DMA_CHANNEL_1,(uint32_t)ADC_ConvertedValue);
LL_DMA_EnableChannel(DMA1,LL_DMA_CHANNEL_1);
/* 配置ADC通道 */
LL_ADC_REG_SetSequencerChAdd(ADC1, LL_ADC_CHANNEL_0);
/* 配置ADC的全局特性:時鍾、分辨率、數據對齊和轉換次數 */
ADC_InitStruct.Clock = LL_ADC_CLOCK_ASYNC;
ADC_InitStruct.Resolution = LL_ADC_RESOLUTION_12B;
ADC_InitStruct.DataAlignment = LL_ADC_DATA_ALIGN_RIGHT;
ADC_InitStruct.LowPowerMode = LL_ADC_LP_MODE_NONE;
LL_ADC_Init(ADC1, &ADC_InitStruct);
ADC_REG_InitStruct.TriggerSource = LL_ADC_REG_TRIG_SOFTWARE;
ADC_REG_InitStruct.SequencerDiscont = LL_ADC_REG_SEQ_DISCONT_DISABLE;
ADC_REG_InitStruct.ContinuousMode = LL_ADC_REG_CONV_CONTINUOUS;
ADC_REG_InitStruct.DMATransfer = LL_ADC_REG_DMA_TRANSFER_UNLIMITED;
ADC_REG_InitStruct.Overrun = LL_ADC_REG_OVR_DATA_PRESERVED;
LL_ADC_REG_Init(ADC1, &ADC_REG_InitStruct);
LL_ADC_REG_SetSequencerScanDirection(ADC1, LL_ADC_REG_SEQ_SCAN_DIR_FORWARD);
LL_ADC_SetSamplingTimeCommonChannels(ADC1, LL_ADC_SAMPLINGTIME_239CYCLES_5);
LL_ADC_DisableIT_EOC(ADC1);
LL_ADC_DisableIT_EOS(ADC1);
LL_ADC_StartCalibration(ADC1);
while( LL_ADC_IsCalibrationOnGoing(ADC1));
LL_ADC_Enable(ADC1);
LL_ADC_REG_SetDMATransfer(ADC1,LL_ADC_REG_DMA_TRANSFER_UNLIMITED);
LL_ADC_REG_StartConversion(ADC1);
}
其實在ADC的初始化配置中也對DMA作了配置,但DMA還需要對始終和中斷進行配置。
/** DMA 控制器初始化配置 */
static void DMA_Init_Configuration(void)
{
/* DMA 控制器時鍾使能 */
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);
/* DMA1_Channel1_IRQn中斷配置 */
NVIC_SetPriority(DMA1_Channel1_IRQn, 0);
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}
配置后,ADC的寄存器如下:

配置后,DMA的寄存器如下:

其實,到這里ADC采集世紀上已經實現了,DMA已經將數據從ADC讀出來存到了指定的內存區域,后續的處理就很簡單了。
5、總結
我們已經實現了基於LL庫使用DMA方式獲取ADC的數據。下面我們就下載到目標設備並檢測一下結果。測試結果如下:

上圖中,上部是計算完成的物理量值,下部則是DMA寫到內存緩存區的ADC的原始碼值。
歡迎關注:

