36. 单通道ADC过采样采集
一、ADC单通道过采样
ADC 过采样技术,是利用 ADC 多次采集的方式,来提高 ADC 精度,采样速度每提高 4 倍,采样精度提高 1bit,同时,ADC 采样速度降低 4 倍,如提高 4bit 精度,需要 256 次采集才能得出 1 次数据,相当于 ADC 速度慢了 256 倍。理论上只要 ADC 足够块,我们可以无限提高 ADC 精度,但实际上 ADC 并不是无限块的,而且由于 ADC 性能限制,并不是位数无限提交结果就越好,需要根据自己的实际需求和 ADC 的实际性能来权衡。
根据要增加的分辨率位数计算过采样频率方程如下:
其中,\(f_{os}\) 是过采样频率,w 是希望增加的分辨率位数,\(f_{s}\) 是初始采样频率要求。
如果我们希望 12 位分辨率的 ADC 提高 4 位分辨率,采样频率就要提高 256 倍,即需要 256 次采集才能得到一次 16 位分辨率的数据。然后将这 256 次采集结果求和,求和的结果再右移 4 位,就得到提高分辨率后的结果。
提高 N 位分辨率,需要右移 N 位。
二、ADC对应的通道引脚
通道号 | ADC1 | ADC2 | ADC3 |
---|---|---|---|
通道 0 | PA0 | PA0 | PA0 |
通道 1 | PA1 | PA1 | PA1 |
通道 2 | PA2 | PA2 | PA2 |
通道 3 | PA3 | PA3 | PA3 |
通道 4 | PA4 | PA4 | PF6 |
通道 5 | PA5 | PA5 | PF7 |
通道 6 | PA6 | PA6 | PF8 |
通道 7 | PA7 | PA7 | PF9 |
通道 8 | PB0 | PB0 | PF10 |
通道 9 | PB1 | PB1 | PF3 |
通道 10 | PC0 | PC0 | PC0 |
通道 11 | PC1 | PC0 | PC1 |
通道 12 | PC2 | PC2 | PC2 |
通道 13 | PC3 | PC3 | PC3 |
通道 14 | PC4 | PC3 | PF4 |
通道 15 | PC5 | PC5 | PF5 |
三、单通道ADC过采样采集步骤
3.1、使能ADC对应的GPIO时钟
使用 ADC 时钟:
#define __HAL_RCC_ADC1_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_ADC1EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_ADC1EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_ADC2_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_ADC2EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_ADC2EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_ADC3_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_ADC3EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_ADC3EN);\
UNUSED(tmpreg); \
} while(0U)
使能对应的 GPIO 时钟:
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOB_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOC_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOF_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOFEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOFEN);\
UNUSED(tmpreg); \
} while(0U)
3.2、配置ADC工作参数
ADC 的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_ADC_Init(ADC_HandleTypeDef *hadc);
形参 hdac 是 ADC_HandleTypeDef 结构体类型指针变量,其定义如下:
typedef struct
{
ADC_TypeDef *Instance; // ADC寄存器基地址
ADC_InitTypeDef Init; // ADC参数初始化结构体变量
__IO uint32_t NbrOfCurrentConversionRank; // 当前转换等级的ADC数
DMA_HandleTypeDef *DMA_Handle; // DMA配置结构体
HAL_LockTypeDef Lock; // ADC锁定对象
__IO uint32_t State; // ADC工作状态
__IO uint32_t ErrorCode; // ADC错误代码
} ADC_HandleTypeDef;
Instance:指向 ADC 寄存器基地址。实际上这个基地址 HAL 库已经定义好了,可以选择范围如下:
#define ADC1 ((ADC_TypeDef *) ADC1_BASE)
#define ADC2 ((ADC_TypeDef *) ADC2_BASE)
#define ADC3 ((ADC_TypeDef *) ADC3_BASE)
Init:ADC初始化结构体,用于配置 ADC 的工作参数。
typedef struct
{
uint32_t ClockPrescaler; // 设置预分频系数,即PRESC[3:O]位
uint32_t Resolution; // 配置ADC的分辨率
uint32_t DataAlign; // 数据对齐方式
uint32_t ScanConvMode; // 扫描模式
uint32_t EOCSelection; // 转换完成标志位
FunctionalState ContinuousConvMode; // 是否连续转换
uint32_t NbrOfConversion; // 规格转换序列数目
FunctionalState DiscontinuousConvMode; // 不连续采样模式
uint32_t NbrOfDiscConversion; // 不连续采样通道
uint32_t ExternalTrigConv; // ADC外部触发源选
uint32_t ExternalTrigConvEdge; // ADC外部触发极性
FunctionalState DMAContinuousRequests; // DMA连续请求转换
} ADC_InitTypeDef;
ClockPrescaler:ADC 预分频系数选择,可选的分频系数为 2、4、6、8。
#define ADC_CLOCK_SYNC_PCLK_DIV2 0x00000000U
#define ADC_CLOCK_SYNC_PCLK_DIV4 ((uint32_t)ADC_CCR_ADCPRE_0)
#define ADC_CLOCK_SYNC_PCLK_DIV6 ((uint32_t)ADC_CCR_ADCPRE_1)
#define ADC_CLOCK_SYNC_PCLK_DIV8 ((uint32_t)ADC_CCR_ADCPRE)
Resolution:配置 ADC 的分辨率,可选的分辨率有 12 位、10 位、8 位和 6 位。分辨率越高,转换数据精度越高,转换时间也越长;反之分辨率越低,转换数据精度越低,转换时间也越短。
#define ADC_RESOLUTION_12B 0x00000000U
#define ADC_RESOLUTION_10B ((uint32_t)ADC_CR1_RES_0)
#define ADC_RESOLUTION_8B ((uint32_t)ADC_CR1_RES_1)
#define ADC_RESOLUTION_6B ((uint32_t)ADC_CR1_RES)
DataAlign:转换结果数据对齐模式,可选右对齐 ADC_DataAlign_Right 或者左对齐ADC_DataAlign_Left。一般我们选择右对齐模式。
#define ADC_DATAALIGN_RIGHT 0x00000000U
#define ADC_DATAALIGN_LEFT ((uint32_t)ADC_CR2_ALIGN)
ScanConvMode:配置是否使用扫描。如果是单通道转换使用 DISABLE,如果是多通道转换使用 ENABLE。
typedef enum
{
DISABLE = 0U,
ENABLE = !DISABLE
} FunctionalState;
EOCSelection:指定转换结束时是否产生 EOS 中断或事件标志,可选值如下:
#define ADC_EOC_SEQ_CONV 0x00000000U
#define ADC_EOC_SINGLE_CONV 0x00000001U
#define ADC_EOC_SINGLE_SEQ_CONV 0x00000002U /*!< reserved for future use */
ContinuousConvMode:配置自动连续转换还是单次转换。使用 ENABLE 配置为使能自动连续转换;使用 DISABLE 配置为单次转换,转换一次后停止需要手动控制才重新启动转换。
typedef enum
{
DISABLE = 0U,
ENABLE = !DISABLE
} FunctionalState;
NbrOfConversion:设置常规转换通道数目,范围是:1 ~ 16。
DiscontinuousConvMode:配置是否使用不连续的采样模式,比如要转换的通道有 1、2、5、7、8、9,那么第一次触发会进行通道 1 与通道 2,下次触发就是转换通道 5 与通道 7,这样不连续的转换,依次类推。此参数只有将 ScanConvMode 使能,还有 ContinuousConvMode失能的情况下才有效,不可同时使能。
typedef enum
{
DISABLE = 0U,
ENABLE = !DISABLE
} FunctionalState;
NbrOfDiscConversion:不连续采样通道数。
ExternalTrigConv:外部触发方式的选择,如果使用软件触发,那么外部触发会关闭。
#define ADC_EXTERNALTRIGCONV_T1_CC1 0x00000000U
#define ADC_EXTERNALTRIGCONV_T1_CC2 ((uint32_t)ADC_CR2_EXTSEL_0)
#define ADC_EXTERNALTRIGCONV_T1_CC3 ((uint32_t)ADC_CR2_EXTSEL_1)
#define ADC_EXTERNALTRIGCONV_T2_CC2 ((uint32_t)(ADC_CR2_EXTSEL_1 | ADC_CR2_EXTSEL_0))
#define ADC_EXTERNALTRIGCONV_T2_CC3 ((uint32_t)ADC_CR2_EXTSEL_2)
#define ADC_EXTERNALTRIGCONV_T2_CC4 ((uint32_t)(ADC_CR2_EXTSEL_2 | ADC_CR2_EXTSEL_0))
#define ADC_EXTERNALTRIGCONV_T2_TRGO ((uint32_t)(ADC_CR2_EXTSEL_2 | ADC_CR2_EXTSEL_1))
#define ADC_EXTERNALTRIGCONV_T3_CC1 ((uint32_t)(ADC_CR2_EXTSEL_2 | ADC_CR2_EXTSEL_1 | ADC_CR2_EXTSEL_0))
#define ADC_EXTERNALTRIGCONV_T3_TRGO ((uint32_t)ADC_CR2_EXTSEL_3)
#define ADC_EXTERNALTRIGCONV_T4_CC4 ((uint32_t)(ADC_CR2_EXTSEL_3 | ADC_CR2_EXTSEL_0))
#define ADC_EXTERNALTRIGCONV_T5_CC1 ((uint32_t)(ADC_CR2_EXTSEL_3 | ADC_CR2_EXTSEL_1))
#define ADC_EXTERNALTRIGCONV_T5_CC2 ((uint32_t)(ADC_CR2_EXTSEL_3 | ADC_CR2_EXTSEL_1 | ADC_CR2_EXTSEL_0))
#define ADC_EXTERNALTRIGCONV_T5_CC3 ((uint32_t)(ADC_CR2_EXTSEL_3 | ADC_CR2_EXTSEL_2))
#define ADC_EXTERNALTRIGCONV_T8_CC1 ((uint32_t)(ADC_CR2_EXTSEL_3 | ADC_CR2_EXTSEL_2 | ADC_CR2_EXTSEL_0))
#define ADC_EXTERNALTRIGCONV_T8_TRGO ((uint32_t)(ADC_CR2_EXTSEL_3 | ADC_CR2_EXTSEL_2 | ADC_CR2_EXTSEL_1))
#define ADC_EXTERNALTRIGCONV_Ext_IT11 ((uint32_t)ADC_CR2_EXTSEL)
#define ADC_SOFTWARE_START ((uint32_t)ADC_CR2_EXTSEL + 1U)
ExternalTrigConvEdge:外部触发极性选择,如果使用外部触发,可以选择触发的极性,可选有禁止触发检测、上升沿触发检测、下降沿触发检测以及上升沿和下降沿均可触发检测。
DMAContinuousRequests:指定 DMA 请求是否以一次性模式执行(当达到转换次数时,DMA 传输停止)或在连续模式下(DMA 传输无限制,无论转换的数量)。注:在连续模式下,DMA 必须配置为循环模式。否则,当达到 DMA 缓冲区最大指针时将触发溢出。注意:当常规组和注入组都没有转换时(禁用 ADC,或启用 ADC,没有连续模式或可以启动转换的外部触发器),必须修改此参数。该参数可设置为 “启用” 或 “禁用”。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
typedef enum
{
HAL_OK = 0x00U, // 成功
HAL_ERROR = 0x01U, // 错误
HAL_BUSY = 0x02U, // 忙碌
HAL_TIMEOUT = 0x03U // 超时
} HAL_StatusTypeDef;
3.3、ADC底层初始化
HAL 库中,提供 HAL_GPIO_Init()
函数用于配置 GPIO 功能模式,初始化 GPIO。该函数的声明如下:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
该函数的第一个形参 GPIOx 用来指定端口号,可选值如下:
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
第二个参数是 GPIO_InitTypeDef 类型的结构体变量,用来设置 GPIO 的工作模式,其定义如下:
typedef struct
{
uint32_t Pin; // 引脚号
uint32_t Mode; // 模式设置
uint32_t Pull; // 上下拉设置
uint32_t Speed; // 速度设置
uint32_t Alternate; // 复用功能设置
}GPIO_InitTypeDef;
成员 Pin 表示 引脚号,范围:GPIO_PIN_0 到 GPIO_PIN_15。
#define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */
#define GPIO_PIN_1 ((uint16_t)0x0002) /* Pin 1 selected */
#define GPIO_PIN_2 ((uint16_t)0x0004) /* Pin 2 selected */
#define GPIO_PIN_3 ((uint16_t)0x0008) /* Pin 3 selected */
#define GPIO_PIN_4 ((uint16_t)0x0010) /* Pin 4 selected */
#define GPIO_PIN_5 ((uint16_t)0x0020) /* Pin 5 selected */
#define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected */
#define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */
#define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */
#define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */
#define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */
#define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */
#define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */
#define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */
#define GPIO_PIN_14 ((uint16_t)0x4000) /* Pin 14 selected */
#define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */
成员 Mode 是 GPIO 的 模式选择,有以下选择项:
#define GPIO_MODE_ANALOG MODE_ANALOG // 模式功能
成员 Pull 用于 配置上下拉电阻,有以下选择项:
#define GPIO_NOPULL 0x00000000U // 无上下拉
#define GPIO_PULLUP 0x00000001U // 上拉
#define GPIO_PULLDOWN 0x00000002U // 下拉
3.4、配置ADC通道
在 HAL 库中,通过 HAL_ADC_ConfigChannel()
函数来设置配置 ADC 的通道,根据需求设置通道、序列、采样时间和校准配置单端输入模式或差分输入模式等。
HAL_StatusTypeDef HAL_ADC_ConfigChannel(ADC_HandleTypeDef *hadc, ADC_ChannelConfTypeDef *sConfig);
形参 hdac 是 ADC_HandleTypeDef 结构体类型指针变量。
形参 sConfig 是 ADC_ChannelConfTypeDef 结构体类型指针变量,定义如下:
typedef struct
{
uint32_t Channel; // ADC转换通道
uint32_t Rank; // ADC转换顺序
uint32_t SamplingTime; // ADC采样周期
uint32_t Offset; // ADC偏移量
} ADC_ChannelConfTypeDef;
Channel:ADC 转换通道,范围:0 ~ 19。
Rank:在常规转换中的常规组的转换顺序,可以选择 1 ~ 16。
SamplingTime:ADC 的采样周期,最大 480 个 ADC 时钟周期,要求采样周期尽量大,以减少误差。
#define ADC_SAMPLETIME_3CYCLES 0x00000000U
#define ADC_SAMPLETIME_15CYCLES ((uint32_t)ADC_SMPR1_SMP10_0)
#define ADC_SAMPLETIME_28CYCLES ((uint32_t)ADC_SMPR1_SMP10_1)
#define ADC_SAMPLETIME_56CYCLES ((uint32_t)(ADC_SMPR1_SMP10_1 | ADC_SMPR1_SMP10_0))
#define ADC_SAMPLETIME_84CYCLES ((uint32_t)ADC_SMPR1_SMP10_2)
#define ADC_SAMPLETIME_112CYCLES ((uint32_t)(ADC_SMPR1_SMP10_2 | ADC_SMPR1_SMP10_0))
#define ADC_SAMPLETIME_144CYCLES ((uint32_t)(ADC_SMPR1_SMP10_2 | ADC_SMPR1_SMP10_1))
#define ADC_SAMPLETIME_480CYCLES ((uint32_t)ADC_SMPR1_SMP10)
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
3.5、配置DMA
3.5.1、使能DMA时钟
使能对应的 DMA 时钟。
#define __HAL_RCC_DMA2_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_DMA2EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_DMA2EN);\
UNUSED(tmpreg); \
} while(0U)
3.5.2、配置DMA工作参数
DMA 的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);
形参 hdma 是 DMA_HandleTypeDef 结构体类型指针变量,其定义如下:
typedef struct __DMA_HandleTypeDef
{
DMA_Stream_TypeDef *Instance; // 寄存器基地址
DMA_InitTypeDef Init; // DAM通信参数
HAL_LockTypeDef Lock; // DMA锁对象
__IO HAL_DMA_StateTypeDef State; // DMA传输状态
void *Parent; // 父对象状态,HAL库处理的中间变量
void (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma); // DMA传输完成回调
void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); // DMA一半传输完成回调
void (* XferM1CpltCallback)( struct __DMA_HandleTypeDef * hdma); // DMA传输完整的Memory1回调
void (* XferM1HalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); // DMA传输半完全内存回调
void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma); // DMA传输错误回调
void (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma); // DMA传输中止回调
__IO uint32_t ErrorCode; // DMA存取错误代码
uint32_t StreamBaseAddress; // DMA通道基地址
uint32_t StreamIndex; // DMA通道索引
}DMA_HandleTypeDef;
Instance:是用来 设置寄存器基地址。ADC1
使用 DMA2_Stream0
中的 DMA_CHANNEL_0
或 DMA2_Stream4
中的 DMA_CHANNEL_0
。ADC2
使用 DMA2_Stream2
中的 DMA_CHANNEL_1
或 DMA2_Stream3
中的 DMA_CHANNEL_1
。ADC3
使用 DMA2_Stream0
中的 DMA_CHANNEL_2
或 DMA2_Stream1
中的 DMA_CHANNEL_2
。
#define DMA2_Stream0 ((DMA_Stream_TypeDef *) DMA2_Stream0_BASE)
#define DMA2_Stream1 ((DMA_Stream_TypeDef *) DMA2_Stream1_BASE)
#define DMA2_Stream2 ((DMA_Stream_TypeDef *) DMA2_Stream2_BASE)
#define DMA2_Stream3 ((DMA_Stream_TypeDef *) DMA2_Stream3_BASE)
#define DMA2_Stream4 ((DMA_Stream_TypeDef *) DMA2_Stream4_BASE)
Parent:是 HAL 库处理中间变量,用来 指向 DMA 通道外设句柄。
StreamBaseAddress 和 StreamIndex 是 数据流基地址 和 索引号,这个是 HAL 库处理的时候会自动计算,用户无需设置。
Init,它是 DMA_InitTypeDef 结构体类型变量,该结构体定义如下:
typedef struct
{
uint32_t Channel; // 传输通道
uint32_t Direction; // 传输方向
uint32_t PeriphInc; // 外设地址是否自增
uint32_t MemInc; // 内存地址是否自增
uint32_t PeriphDataAlignment; // 外设数据大小
uint32_t MemDataAlignment; // 存储器数据大小
uint32_t Mode; // 传输模式
uint32_t Priority; // DMA优先级
uint32_t FIFOMode; // FIFO模式开启或者禁止
uint32_t FIFOThreshold; // FIFO阈值选择
uint32_t MemBurst; // 存储器突发模式
uint32_t PeriphBurst; // 外设突发模式
}DMA_InitTypeDef;
成员 Channel 用来 指定传输通道,可选值如下:
#define DMA_CHANNEL_0 0x00000000U /*!< DMA Channel 0 */
#define DMA_CHANNEL_1 0x02000000U /*!< DMA Channel 1 */
#define DMA_CHANNEL_2 0x04000000U /*!< DMA Channel 2 */
#define DMA_CHANNEL_3 0x06000000U /*!< DMA Channel 3 */
#define DMA_CHANNEL_4 0x08000000U /*!< DMA Channel 4 */
#define DMA_CHANNEL_5 0x0A000000U /*!< DMA Channel 5 */
#define DMA_CHANNEL_6 0x0C000000U /*!< DMA Channel 6 */
#define DMA_CHANNEL_7 0x0E000000U /*!< DMA Channel 7 */
成员 Direction 用来 指定传输方向,可选值如下:
#define DMA_PERIPH_TO_MEMORY 0x00000000U // 外设到内存
成员 PeriphInc 用来 设置外设地址是否自增,可选值如下:
#define DMA_PINC_DISABLE 0x00000000U /*!< Peripheral increment mode disable */
成员 MemInc 用来 设置内存地址是否自增,可选值如下:
#define DMA_MINC_ENABLE ((uint32_t)DMA_SxCR_MINC) /*!< Memory increment mode enable */
成员 PeriphDataAlignment 用来 外设数据大小,可选值如下:
#define DMA_PDATAALIGN_BYTE 0x00000000U /*!< Peripheral data alignment: Byte */
#define DMA_PDATAALIGN_HALFWORD ((uint32_t)DMA_SxCR_PSIZE_0) /*!< Peripheral data alignment: HalfWord */
#define DMA_PDATAALIGN_WORD ((uint32_t)DMA_SxCR_PSIZE_1) /*!< Peripheral data alignment: Word */
成员 MemDataAlignment 用来 存储器数据大小,可选值如下:
#define DMA_MDATAALIGN_BYTE 0x00000000U /*!< Memory data alignment: Byte */
#define DMA_MDATAALIGN_HALFWORD ((uint32_t)DMA_SxCR_MSIZE_0) /*!< Memory data alignment: HalfWord */
#define DMA_MDATAALIGN_WORD ((uint32_t)DMA_SxCR_MSIZE_1) /*!< Memory data alignment: Word */
成员 Mode 用来 设置传输模式,可选值如下:
#define DMA_NORMAL 0x00000000U // 普通模式
#define DMA_CIRCULAR ((uint32_t)DMA_SxCR_CIRC) // 循环模式
#define DMA_PFCTRL ((uint32_t)DMA_SxCR_PFCTRL) // 外设流控模式
成员 Priority 用来 DMA 优先级,可选值如下:
#define DMA_PRIORITY_LOW 0x00000000U /*!< Priority level: Low */
#define DMA_PRIORITY_MEDIUM ((uint32_t)DMA_SxCR_PL_0) /*!< Priority level: Medium */
#define DMA_PRIORITY_HIGH ((uint32_t)DMA_SxCR_PL_1) /*!< Priority level: High */
#define DMA_PRIORITY_VERY_HIGH ((uint32_t)DMA_SxCR_PL) /*!< Priority level: Very High */
成员 FIFOThreshold 用来 设置是否开启 FIFO,可选值如下:
#define DMA_FIFOMODE_DISABLE 0x00000000U /*!< FIFO mode disable */
#define DMA_FIFOMODE_ENABLE ((uint32_t)DMA_SxFCR_DMDIS) /*!< FIFO mode enable */
成员 FIFOMode 用来 设置 FIFO 的阈值,可选值如下:
#define DMA_FIFO_THRESHOLD_1QUARTERFULL 0x00000000U /*!< FIFO threshold 1 quart full configuration */
#define DMA_FIFO_THRESHOLD_HALFFULL ((uint32_t)DMA_SxFCR_FTH_0) /*!< FIFO threshold half full configuration */
#define DMA_FIFO_THRESHOLD_3QUARTERSFULL ((uint32_t)DMA_SxFCR_FTH_1) /*!< FIFO threshold 3 quarts full configuration */
#define DMA_FIFO_THRESHOLD_FULL ((uint32_t)DMA_SxFCR_FTH) /*!< FIFO threshold full configuration */
成员 MemBurst 用来 设置存储器突发模式,可选值如下:
#define DMA_MBURST_SINGLE 0x00000000U
#define DMA_MBURST_INC4 ((uint32_t)DMA_SxCR_MBURST_0)
#define DMA_MBURST_INC8 ((uint32_t)DMA_SxCR_MBURST_1)
#define DMA_MBURST_INC16 ((uint32_t)DMA_SxCR_MBURST)
成员 PeriphBurst 用来 设置外设突发模式,可选值如下:
#define DMA_PBURST_SINGLE 0x00000000U
#define DMA_PBURST_INC4 ((uint32_t)DMA_SxCR_PBURST_0)
#define DMA_PBURST_INC8 ((uint32_t)DMA_SxCR_PBURST_1)
#define DMA_PBURST_INC16 ((uint32_t)DMA_SxCR_PBURST)
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
3.6、连接DMA和外设
HAL 库为了处理各类外设的 DMA 请求,在调用相关函数之前,需要调用一个宏定义标识符,来连接 DMA 和外设句柄。
#define __HAL_LINKDMA(__HANDLE__, __PPP_DMA_FIELD__, __DMA_HANDLE__) \
do{ \
(__HANDLE__)->__PPP_DMA_FIELD__ = &(__DMA_HANDLE__); \
(__DMA_HANDLE__).Parent = (__HANDLE__); \
} while(0U)
其中 __HANDLE__
是外设初始化句柄。__PPP_DMA_FIELD__
是外设句柄结构体的成员变量名。__DMA_HANDLE__
是 DMA初始化句柄。
在 HAL 库中,任何一个可以使用 DMA 的外设,它的初始化结构体句柄都会有一个 DMA_HandleTypeDef 指针类型的成员变量,是 HAL 库用来做相关指向的。__PPP_DMA_FIELD__
就是 DMA_HandleTypeDef 结构体指针类型。这句话的含义就是把 __HANDLE__
句柄的成员变量 __PPP_DMA_FIELD__
和 DMA 句柄 __DMA_HANDLE__
连接起来,是纯软件处理,没有任何硬件操作。
3.7、使能DMA中断
3.7.1、设置中断优先级分组
HAL_NVIC_SetPriorityGrouping() 函数是设置中断优先级分组函数。其声明如下:
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);
其中,参数 PriorityGroup 是 中断优先级分组号,可以选择范围如下:
#define NVIC_PRIORITYGROUP_0 0x00000007U /*!< 0 bits for pre-emption priority
4 bits for subpriority */
#define NVIC_PRIORITYGROUP_1 0x00000006U /*!< 1 bits for pre-emption priority
3 bits for subpriority */
#define NVIC_PRIORITYGROUP_2 0x00000005U /*!< 2 bits for pre-emption priority
2 bits for subpriority */
#define NVIC_PRIORITYGROUP_3 0x00000004U /*!< 3 bits for pre-emption priority
1 bits for subpriority */
#define NVIC_PRIORITYGROUP_4 0x00000003U /*!< 4 bits for pre-emption priority
0 bits for subpriority */
这个函数在一个工程里基本只调用一次,而且是在程序 HAL 库初始化函数里面已经被调用,后续就不会再调用了。因为当后续调用设置成不同的中断优先级分组时,有可能造成前面设置好的抢占优先级和响应优先级不匹配。如果调用了多次,则以最后一次为准。
3.7.2、设置中断优先级
HAL_NVIC_SetPriority() 函数是设置中断优先级函数。其声明如下:
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);
其中,参数 IRQn 是 中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。
typedef enum
{
DMA2_Stream0_IRQn = 56, /*!< DMA2 Stream 0 global Interrupt */
DMA2_Stream1_IRQn = 57, /*!< DMA2 Stream 1 global Interrupt */
DMA2_Stream2_IRQn = 58, /*!< DMA2 Stream 2 global Interrupt */
DMA2_Stream3_IRQn = 59, /*!< DMA2 Stream 3 global Interrupt */
DMA2_Stream4_IRQn = 60, /*!< DMA2 Stream 4 global Interrupt */
} IRQn_Type;
参数 PreemptPriority 是 抢占优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。
参数 SubPriority 是 响应优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。
3.7.3、使能中断
HAL_NVIC_EnableIRQ() 函数是中断使能函数。其声明如下:
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);
其中,参数 IRQn 是 中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。
3.8、使能DMA传输完成中断
#define __HAL_DMA_ENABLE_IT(__HANDLE__, __INTERRUPT__) (((__INTERRUPT__) != DMA_IT_FE)? \
((__HANDLE__)->Instance->CR |= (__INTERRUPT__)) : ((__HANDLE__)->Instance->FCR |= (__INTERRUPT__)))
其中,__HANDLE__
是 DMA 句柄,__INTERRUPT__
是中断标志,传输完成中断是 __DMA_IT_TC
。
3.9、编写DMA中断服务函数
每开启一个中断,就必须编写其对应的中断服务函数,否则将导致死机(CPU 将找不到中断服务函数)。中断服务函数接口厂家已经在 startup_stm32f407xx.s 中写好了。
void DMA2_Stream0_IRQHandler(void);
void DMA2_Stream1_IRQHandler(void);
void DMA2_Stream2_IRQHandler(void);
void DMA2_Stream3_IRQHandler(void);
void DMA2_Stream4_IRQHandler(void);
3.10、开启DMA传输
HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
形参 hdma 是 DMA 句柄,SrcAddress 是 源地址,DstAddress 是 目的地址,DataLength 是要 传输的数据长度。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
3.11、使用DMA传输ADC读取的数据
配置好 ADC 通道之后,通过 HAL_ADC_Start()
函数启动 AD 转换器。
HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef *hadc, uint32_t *pData, uint32_t Length);
形参 hdac 是 ADC_HandleTypeDef 结构体类型指针变量。
形参 pData 是 保存读取的 ADC 值的指针。
形参 Length 是 要读取的长度。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
四、原理图
使用短接片可以将 STM_ADC(PA5)连接在电位器(R_ADC)上(开发板上 STM_ADC 对应的丝印为 ADC,R_ADC 对应的丝印是 R),调节电位器即可改变电压,通过 ADC 转换即可检测此电压值。
五、程序源码
5.1、ADC初始化
ADC 初始化函数:
ADC_HandleTypeDef g_adc1_handle;
/**
* @brief ADC初始化函数
*
* @param hadc ADC句柄
* @param ADCx ADC寄存器基地址
* @param numOfConversion 常规转换的通道数
* @param continuousConvMode ADC是否连续转换, ENABLE:连续转换, DISABLE:单次转换
* @param dmaContinuousRequests DMA是否连续请求, ENABLE:使用DMA, DISABLE:不使用DMA
*/
void ADC_Init(ADC_HandleTypeDef *hadc, ADC_TypeDef *ADCx,uint32_t numOfConversion, FunctionalState continuousConvMode, FunctionalState dmaContinuousRequests)
{
hadc->Instance = ADCx; // ADC寄存器基地址
hadc->Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; // ADC时钟分频因子
hadc->Init.Resolution = ADC_RESOLUTION_12B; // 12位模式
hadc->Init.DataAlign = ADC_DATAALIGN_RIGHT; // 数据右对齐
hadc->Init.EOCSelection = DISABLE; // 转换完成标志位
hadc->Init.ScanConvMode = ENABLE; // 配置是否使用扫描
hadc->Init.ContinuousConvMode = continuousConvMode; // ADC是否自动连续转换
hadc->Init.NbrOfConversion = numOfConversion; // 配置常规转换通道
hadc->Init.DiscontinuousConvMode = DISABLE; // 是否使用不连续的采样模式
hadc->Init.NbrOfDiscConversion = 0; // 不连续采样通道数
hadc->Init.ExternalTrigConv = ADC_SOFTWARE_START; // 外部触发方式的选择
hadc->Init.DMAContinuousRequests = dmaContinuousRequests; // 是否开启DMA连续转换
HAL_ADC_Init(hadc);
}
ADC 底层初始化函数:
/**
* @brief ADC底层初始化函数
*
* @param hadc ADC句柄
*/
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (hadc->Instance == ADC1)
{
__HAL_RCC_ADC1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_5; // PA5引脚设置为ADC1的通道5引脚
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; // 模拟功能
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不使用上下拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}
ADC 通道配置函数:
/**
* @brief ADC通道配置函数
*
* @param hadc ADC句柄
* @param channel ADC通道,可选值: ADC_CHANNEL_0 ~ ADC_CHANNEL_18
* @param rank 采样序列
* @param samplingTime 采样时间,可选值:ADC_SAMPLETIME_xCYCLES,x: [3,15,28,56,112,144,480]
*/
void ADC_ConfigChannel(ADC_HandleTypeDef *hadc, uint32_t channel, uint32_t rank, uint32_t samplingTime)
{
ADC_ChannelConfTypeDef ADC_ChannelConfStruct = {0};
ADC_ChannelConfStruct.Channel = channel; // ADC通道
ADC_ChannelConfStruct.Rank = rank; // 采样序列
ADC_ChannelConfStruct.SamplingTime = samplingTime; // 采样时间
HAL_ADC_ConfigChannel(hadc, &ADC_ChannelConfStruct);
}
5.2、DMA初始化
DMA 外设到存储器间的数据传输初始化函数:
DMA_HandleTypeDef g_dma_handle;
/**
* @brief DMA外设到存储器间的数据传输初始化函数
*
* @param dma_stream DMA数据流,可选值: DMA1_Stream0 ~ DMA1_Stream7, DMA2_Stream0 ~ DMA2_Stream7
* @param channel DMA通道,可选值: DMA_CHANNEL_0 ~ DMA_CHANNEL_7
* @param dataLength 数据长度,可选值: [8, 16, 32]
* @param mode DMA工作模式,可选值: [DMA_NORMAL, DMA_CIRCULAR, DMA_PFCTRL]
* @param priority DMA通道优先级,可选值: [DMA_PRIORITY_LOW, DMA_PRIORITY_MEDIUM, DMA_PRIORITY_HIGH, DMA_PRIORITY_VERY_HIGH]
*/
void DMA_PeripheralToMemory_Init(DMA_Stream_TypeDef *dma_stream, uint32_t channel, uint8_t dataLength, uint32_t mode, uint32_t priority)
{
if ((uint32_t)dma_stream > (uint32_t)DMA2) // 得到当前stream是属于DMA2还是DMA1
{
__HAL_RCC_DMA2_CLK_ENABLE();
}
else
{
__HAL_RCC_DMA1_CLK_ENABLE();
}
g_dma_handle.Instance = dma_stream; // 数据流选择
g_dma_handle.Init.Channel = channel; // DMA通道选择
g_dma_handle.Init.Direction = DMA_PERIPH_TO_MEMORY; // 外设到存储器
g_dma_handle.Init.PeriphInc = DMA_PINC_DISABLE; // 外设非增量模式
g_dma_handle.Init.MemInc = DMA_MINC_ENABLE; // 存储器增量模式
switch (dataLength)
{
case 8:
g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; // 外设地址长度
g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; // 存储器地址长度
break;
case 16:
g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 外设地址长度
g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; // 存储器地址长度
break;
case 32:
g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; // 外设地址长度
g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; // 存储器地址长度
break;
default:
break;
}
g_dma_handle.Init.Mode = mode; // DMA模式
g_dma_handle.Init.Priority = priority; // DMA通道优先级
g_dma_handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE; // 关闭FIFO模式
g_dma_handle.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL; // FIFO阈值配置
g_dma_handle.Init.MemBurst = DMA_MBURST_SINGLE; // 存储器突发单次传输
g_dma_handle.Init.PeriphBurst = DMA_PBURST_SINGLE; // 外设突发单次传输
HAL_DMA_Init(&g_dma_handle);
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}
DMA2 的 Stream0 中断函数:
uint8_t dma_is_finished = 0;
/**
* @brief DMA2的Stream0中断函数
*
*/
void DMA2_Stream0_IRQHandler(void)
{
if (DMA2->LISR & (1 << 5)) // DMA2_Stream0传输完成标志
{
dma_is_finished = 1; // 标记DMA传输完成
DMA2->LIFCR |= (1 << 5); // 清除DMA2_Stream0传输完成标志
}
}
使能一次 ADC 的 DMA 传输函数:
/**
* @brief 使能一次ADC的DMA传输函数
*
* @param hadc ADC句柄
* @param hdma DMA句柄
* @param ndtr DMA传输的次数
*/
void ADC_DMA_Enable(ADC_HandleTypeDef *hadc, DMA_HandleTypeDef *hdma, uint16_t ndtr)
{
__HAL_ADC_DISABLE(hadc); // 先关闭ADC
__HAL_DMA_DISABLE(hdma); // 关闭DMA传输
hdma->Instance->NDTR= ndtr; // 重设DMA传输数据量
__HAL_DMA_ENABLE(hdma); // 开启DMA传输
__HAL_ADC_ENABLE(hadc); // 重新启动ADC
hadc->Instance->CR2 |= 1 << 30; // 启动规则转换通道
}
5.3、main()函数
main() 函数:
#define ADC_OVERSAMPLE_TIMES 256 // ADC过采样次数,从12位提高到16位需要采样256次
#define ADC_DMA_BUFFER_SIZE ADC_OVERSAMPLE_TIMES * 10 // 累计过采样10次
uint16_t g_adc_dma_buffer[ADC_DMA_BUFFER_SIZE] = {0};
int main(void)
{
uint32_t sum = 0, value = 0;
double result = 0;
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
UART_Init(&g_usart1_handle, USART1, 115200);
ADC_Init(&g_adc1_handle, ADC1, 1, ENABLE, ENABLE);
ADC_ConfigChannel(&g_adc1_handle, ADC_CHANNEL_5, 1, ADC_SAMPLETIME_3CYCLES);
// 使用DMA2的Stream0的Channel0
DMA_PeripheralToMemory_Init(DMA2_Stream0, ADC_CHANNEL_0, 16, DMA_NORMAL, DMA_PRIORITY_HIGH);
__HAL_LINKDMA(&g_adc1_handle, DMA_Handle, g_dma_handle); // 链接DMA与ADC1
__HAL_DMA_ENABLE_IT(&g_dma_handle, DMA_IT_TC); // TCIE=1,使能传输完成中断
HAL_DMA_Start_IT(&g_dma_handle, (uint32_t)&ADC1->DR, (uint32_t)g_adc_dma_buffer, 0);
HAL_ADC_Start_DMA(&g_adc1_handle, (uint32_t *)g_adc_dma_buffer, 0);
ADC_DMA_Enable(&g_adc1_handle, &g_dma_handle, ADC_DMA_BUFFER_SIZE);
while (1)
{
if (dma_is_finished)
{
sum = 0;
for (uint32_t i = 0; i < ADC_DMA_BUFFER_SIZE; i++)
{
sum += g_adc_dma_buffer[i];
}
value = sum / (ADC_DMA_BUFFER_SIZE / ADC_OVERSAMPLE_TIMES); // 过采样10次的平均值
value >>= 4; // 右移12位,从12位变成16位
printf("value: %ld\r\n", value);
result = (double)value * 3.3 / 65536;
printf("result: %.2lf\r\n", result);
dma_is_finished = 0;
ADC_DMA_Enable(&g_adc1_handle, &g_dma_handle, ADC_DMA_BUFFER_SIZE);
}
HAL_Delay(1000);
}
return 0;
}
· 百万级群聊的设计实践
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期