使用STM32F103和STM32F401CCU6對雙軸搖桿(兩個電壓通道)進行ADC采樣並通過DMA讀取數值
STM32 ADC(模數轉換)工作模式
單次轉換模式
In Single Conversion mode, the ADC does one conversion. This mode is started either by setting the ADON bit in the ADC_CR2 register (for a regular channel only) or by an external trigger (for a regular or injected channel), while the CONT bit is 0. Once the conversion of the selected channel is complete:
If a regular channel was converted:
– The converted data is stored in the 16-bit ADC_DR register
– The EOC (End Of Conversion) flag is set
– and an interrupt is generated if the EOCIE is set.
If an injected channel was converted:
– The converted data is stored in the 16-bit ADC_DRJ1 register
– The JEOC (End Of Conversion Injected) flag is set
– and an interrupt is generated if the JEOCIE bit is set.
The ADC is then stopped.
連續轉換模式
In continuous conversion mode, ADC starts another conversion as soon as it finishes one. This mode is started either by an external trigger or by setting the ADON bit in the ADC_CR2 register, while the CONT bit is 1. After each conversion:
If a regular channel was converted:
– The converted data is stored in the 16-bit ADC_DR register
– The EOC (End Of Conversion) flag is set
– An interrupt is generated if the EOCIE is set.
If an injected channel was converted:
– The converted data is stored in the 16-bit ADC_DRJ1 register
– The JEOC (End Of Conversion Injected) flag is set
– An interrupt is generated if the JEOCIE bit is set.
掃描模式
This mode is used to scan a group of analog channels. A single conversion is performed for each channel of the group. After each end of conversion, the next channel of the group is converted automatically. If the CONT bit is set, conversion does not stop at the last selected group channel but continues again from the first selected group channel.
When using scan mode, DMA bit must be set and the direct memory access controller is used to transfer the converted data of regular group channels to SRAM after each update of the ADC_DR register. The injected channel converted data is always stored in the ADC_JDRx registers.
非連續模式
This mode is enabled by setting the DISCEN bit in the ADC_CR1 register. It can be used to convert a short sequence of n conversions (n <=8) which is a part of the sequence of conversions selected in the ADC_SQRx registers. The value of n is specified by writing to the DISCNUM[2:0] bits in the ADC_CR1 register.
When an external trigger occurs, it starts the next n conversions selected in the ADC_SQRx registers until all the conversions in the sequence are done. The total sequence length is defined by the L[3:0] bits in the ADC_SQR1 register.
讀取ADC結果的幾種方式
The Polling Method
It’s the easiest way in code in order to perform an analog to digital conversion using the ADC on an analog input channel. However, it’s not an efficient way in all cases as it’s considered to be a blocking way of using the ADC. As in this way, we start the A/D conversion and wait for the ADC until it completes the conversion so the CPU can resume processing the main code.
中斷模式
The interrupt method is an efficient way to do ADC conversion in a non-blocking manner, so the CPU can resume executing the main code routine until the ADC completes the conversion and fires an interrupt signal so the CPU can switch to the ISR context and save the conversion results for further processing.
However, when you’re dealing with multiple channels in a circular mode or so, you’ll have periodic interrupts from the ADC that are too much for the CPU to handle. This will introduce jitter injection and interrupt latency and all sorts of timing issues to the system. This can be avoided by using DMA.
DMA方式
Lastly, the DMA method is the most efficient way of converting multiple ADC channels at very high rates and still transfers the results to the memory without CPU intervention which is so cool and time-saving technique.
STM32F103C8T6的代碼實現
管腳與ADC的映射關系
- PA0:7 ADC1_IN0:7
- PB0 ADC1_IN8
- PB1 ADC1_IN9
實現兩個通道電壓采集到DMA
- 確定要采集的信號通道數量, 每個信號通道要保留的采樣數, 比如下面的例子中是2個通道, 每個通道4個采樣
- 根據上面的數量得到
ARRAYSIZE, 聲明用於DMA的內存變量__IO uint16_t ADCConvertedValue[ARRAYSIZE] - 初始化時鍾: ADC1, GPIOA, DMA1
- 初始化GPIOA用於采集的兩個pin
- 初始化ADC1
- 初始化DMA1
代碼
#include <stdio.h>
#include "timer.h"
#include "usart.h"
#define ARRAYSIZE 2*4
__IO uint16_t ADCConvertedValue[ARRAYSIZE];
void RCC_Configuration(void)
{
/* ADCCLK = PCLK2/4 */
RCC_ADCCLKConfig(RCC_PCLK2_Div4);
/* Enable peripheral clocks ------------------------------------------------*/
/* Enable DMA1 clock */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
/* Enable ADC1 and GPIOC clock */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
}
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* Configure PA.00 (ADC Channel0) as analog input -------------------------*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
int main(void)
{
SystemInit();
Systick_Init();
USART_Configuration();
/* System clocks configuration ---------------------------------------------*/
RCC_Configuration();
/* GPIO configuration ------------------------------------------------------*/
GPIO_Configuration();
/* ADC1 configuration ------------------------------------------------------*/
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
//We will convert multiple channels
ADC_InitStructure.ADC_ScanConvMode = ENABLE;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
//right 12-bit data alignment in ADC data register
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
// Set it to the number of channels
ADC_InitStructure.ADC_NbrOfChannel = 2;
ADC_Init(ADC1, &ADC_InitStructure);
/* ADC1 regular channel0 configuration, rank decides the order in ADCConvertedValue, start from 1 */
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_41Cycles5);
/* ADC1 regular channel1 configuration */
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_41Cycles5);
/* Enable ADC1 DMA */
ADC_DMACmd(ADC1, ENABLE);
/* Enable ADC1 */
ADC_Cmd(ADC1, ENABLE);
/* Enable ADC1 reset calibration register */
ADC_ResetCalibration(ADC1);
/* Check the end of ADC1 reset calibration register */
while(ADC_GetResetCalibrationStatus(ADC1));
/* Start ADC1 calibration */
ADC_StartCalibration(ADC1);
/* Check the end of ADC1 calibration */
while(ADC_GetCalibrationStatus(ADC1));
/* Start ADC1 Software Conversion */
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
/* DMA1 channel1 configuration ----------------------------------------------*/
DMA_InitTypeDef DMA_InitStructure;
DMA_DeInit(DMA1_Channel1);
// ADC1_DR_Address ((uint32_t)0x4001244C)
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&(ADC1->DR);
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADCConvertedValue;
/* Direction:
DMA_DIR_PeripheralSRC:from peripheral,
DMA_DIR_PeripheralDST:to peripheral
*/
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
/* Specifies the buffer size, in data unit, of the specified Stream.
The data unit is equal to the configuration set in DMA_PeripheralDataSize
or DMA_MemoryDataSize members depending in the transfer direction.
Set it to the number of channels
*/
DMA_InitStructure.DMA_BufferSize = ARRAYSIZE;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
// Specifies whether the memory address register should be incremented or not
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;
// Priority among DMA channels
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
// From Memory to Memory
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
/* Enable DMA1 channel1 */
DMA_Cmd(DMA1_Channel1, ENABLE);
while(1) {
for (u8 i = 0; i < ARRAYSIZE; i++) {
printf("%d ", *(ADCConvertedValue + i));
}
printf("\r\n");
Systick_Delay_ms(500);
}
}
void ADC1_IRQHandler(void)
{
ADC_ClearITPendingBit(ADC1, ADC_IT_EOC);
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
}
STM32F401CCU6的代碼實現
只有1個16通道ADC. One 12-bit analog-to-digital converter is embedded and shares up to 16 external channels, performing conversions in the single-shot or scan mode. In scan mode, automatic conversion is performed on a selected group of analog inputs. The ADC can be served by the DMA controller. An analog watchdog feature allows very precise monitoring of the converted voltage of one, some or all selected channels. An interrupt is generated when the converted voltage is outside the programmed thresholds.
To synchronize A/D conversion and timers, the ADCs could be triggered by any of TIM1, TIM2, TIM3, TIM4 or TIM5 timer.
管腳與ADC的映射關系
- PA0:7 ADC1_IN0:7
- PB0 ADC1_IN8
- PB1 ADC1_IN9
- PC0:5 ADC1_IN10:15
因為F401CCU6的PC口只有PC13,PC14,PC15, 所以可以用的ADC只有ADC1_IN0 - IN9
STM32F4的ADC1與DMA的映射
根據STM32F2/F4/F7的DMA參考手冊, 這個系列的芯片中DMA1與DMA2各有8個Stream(Stream0 - Stream7), 分別對應着不同的外設, 其中ADC1對應的是DMA2的Stream0和Stream4, 在代碼中必須使用這兩個, 否則DMA不起作用
實現兩個通道電壓采集到DMA的代碼
#include <stdio.h>
#include "config.h"
#include "led.h"
#include "timer.h"
#include "uart.h"
#define ARRAYSIZE 2*4
__IO uint16_t ADCConvertedValue[ARRAYSIZE];
void RCC_Configuration(void)
{
/* Enable ADCx, DMA and GPIO clocks ****************************************/
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC1, DISABLE);
}
void GPIO_Configuration(void)
{
/* Configure ADC1 Channel0,1 pin as analog input ******************************/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
int main(void)
{
Systick_Init();
USART1_Init();
LED_Init();
/* System clocks configuration ---------------------------------------------*/
RCC_Configuration();
/* GPIO configuration ------------------------------------------------------*/
GPIO_Configuration();
/* DMA2 Stream0 channel0 configuration **************************************/
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_Channel = DMA_Channel_0;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR);
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)ADCConvertedValue;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
DMA_InitStructure.DMA_BufferSize = ARRAYSIZE;
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_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA2_Stream0, &DMA_InitStructure);
DMA_Cmd(DMA2_Stream0, ENABLE);
/* ADC Common Init **********************************************************/
ADC_CommonInitTypeDef ADC_CommonInitStructure;
ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
// 預分頻4分頻, ADCCLK=PCLK2/4=84/4=21Mhz,ADC時鍾最好不要超過36Mhz
ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4;
// DMA使能 (DMA傳輸下要設置使能)
ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
//兩個采樣階段之間的延遲x個時鍾
ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
ADC_CommonInit(&ADC_CommonInitStructure);
/* ADC1 Init ****************************************************************/
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
ADC_InitStructure.ADC_ScanConvMode = ENABLE;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
//ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfConversion = 2;
ADC_Init(ADC1, &ADC_InitStructure);
/* ADC1 regular channel0,1 configuration **************************************/
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_56Cycles);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_56Cycles);
/* Enable DMA request after last transfer (Single-ADC mode) */
ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE);
/* Enable ADC1 DMA */
ADC_DMACmd(ADC1, ENABLE);
/* Enable ADC1 */
ADC_Cmd(ADC1, ENABLE);
ADC_SoftwareStartConv(ADC1);
while(1) {
LED_On();
for (u8 i = 0; i < ARRAYSIZE; i++) {
float a = (*(ADCConvertedValue + i) - 2048) * 512 /2048;
printf("% 5d ", (int)a);
}
printf("\r\n");
LED_Off();
Systick_Delay_ms(200);
}
}
參考
- 這篇寫得很詳細 https://deepbluembedded.com/stm32-adc-tutorial-complete-guide-with-examples/
- 可能有用的代碼 https://github.com/Itachihi/stm32-adc/blob/master/readvb.c
- 可能有用的代碼 ContinuousConvMode https://github.com/mnectnky/STM32F103-One-ADC-MultiChannel-Analog-Read/blob/main/main.c
- 可能有用的代碼 ContinuousConvMode https://github.com/klesogor/ADC/blob/master/ADC.c
- 有用的代碼 DMA, adc部分在main.c https://github.com/daedaleanai/stm32f103_adc2serial/blob/main/main.c
- 有用的代碼 DMA, adc部分在main.c https://github.com/MrLuuuu/STM32F103ZET6_UartPrint/blob/master/USER/main.c
- 用stm32f103做的電容電感測試儀, 注入型, adc部分在main.c https://github.com/MilanGb/STM32F103-LC-R-meter/blob/main/main.c
- https://community.st.com/s/question/0D50X00009XkZ4F/adc-with-multiple-channels-settings
- This one is helpful https://embedds.com/multichannel-adc-using-dma-on-stm32/
- 這篇 https://blog.csdn.net/weixin_45456099/article/details/110669752
- 直接中斷輸出到串口 https://www.it610.com/article/1296571998083817472.htm
- STM32F2/F4/F7 DMA參考 https://www.st.com/resource/en/application_note/dm00046011-using-the-stm32f2-stm32f4-and-stm32f7-series-dma-controller-stmicroelectronics.pdf
