ADC的实现(单通道,多通道,DMA)

1. ADC 介绍

1.1 什么是 ADC?

  • ADC(Analog to Digital Converter):把模拟电压转换为数字码值。

  • F103 为 12 位 SAR(逐次逼近型)ADC ,转换结果范围 0...4095

  • LSB 电压 (理想):V_LSB = Vref / 4096(常用换算也写成 raw/4095 * Vref)。

1.2 ADC 工作原理(逐次逼近型)

  • 采样开关把输入电压充到内部采样电容 上(采样时间由 SamplingTime 决定)。

  • SAR 比较器用二分法逐位逼近,得到 12 位数字结果。

  • 单次转换总时间

其中 f_ADC = PCLK2 / 分频(F1 要求 ≤ 14 MHz)。

例:PCLK2=72 MHz,ADCPCLK2_DIV6 → f_ADC=12 MHz;Sampling=239.5 cycles

单通道最大采样率约 47.6 ksps ;若 4 通道扫描,每通道 ≈ 47.6/4 ≈ 11.9 ksps

1.3 ADC 特性参数(F103 重点)

  • 分辨率:12 位。

  • 参考电压 :通常是 VDD(3.3 V),也可用内部 Vrefint 做标定。

  • 输入通道:外部通道 + 内部温度/参考(CH16/CH17)。

  • 采样时间 :1.5~239.5 cycles 可选。源阻越大 → 采样时间越长更稳。

  • 时钟限制f_ADC ≤ 14 MHz(由 ADCPCLK2 分频得到)。


2. ADC 框图

cpp 复制代码
外部引脚/内部信号 ─> 模拟多路复用器 ─> 采样保持电容 ─> SAR比较器/逻辑 ─> 12位结果寄存器(DR)
                                          ↑
                                  触发(软/硬)、扫描序列(SQR)、采样时间(SMPR)
  • 规则组(Regular):我们最常用的一串转换序列(下面 3 个例子用的都是规则组)。

  • 注入组(Injected):带更高优先级,可在规则组间"插队"。

3. ADC 的一些细节

3.1 输入通道

  • F103 常用映射:

    • CH0--CH7 → PA0--PA7;CH8--CH9 → PB0--PB1;CH10--CH15 → PC0--PC5。

    • CH16=温度、CH17=Vrefint(需置 TSVREFE 使能)。

  • GPIO 必须设为 GPIO_MODE_ANALOGHAL_ADC_MspInit 已做)。

例:

cpp 复制代码
gpio_init_struct.Pin  = GPIO_PIN_1;        // CH1=PA1
gpio_init_struct.Mode = GPIO_MODE_ANALOG;
HAL_GPIO_Init(GPIOA, &gpio_init_struct);

3.2 规则组 / 注入组

  • 规则组HAL_ADC_ConfigChannel()RankSamplingTime

  • 注入组 :有单独的序列和中断,HAL:HAL_ADCEx_InjectedConfigChannel()HAL_ADCEx_InjectedStart(_IT)(此项目未用,了解即可)。

3.3 转换顺序(Rank)

  • 每个被采通道要放进一个 Rank (1...16),硬件按 Rank1 → Rank2 → ... 转。

  • 多通道例子里:

cpp 复制代码
adc_channel_config(&adc_handle, ADC_CHANNEL_0, ADC_REGULAR_RANK_1, ...);
adc_channel_config(&adc_handle, ADC_CHANNEL_1, ADC_REGULAR_RANK_2, ...);
// ...

3.4 触发转换方法

  • ExternalTrigConv

    • ADC_SOFTWARE_START(你用的;软件触发)

    • 或者外部触发(定时器事件等,如 ADC_EXTERNALTRIGCONV_T1_CC1 等)。

  • 连续模式 ContinuousConvMode=ENABLE 时,软件启动一次后自动连续

3.5 转换时间(采样时间选择)

  • SamplingTime 影响充电时间,取值:1.5/7.5/13.5/28.5/41.5/55.5/71.5/239.5 cycles

  • 原则 :源阻 > 几 kΩ 时选更长(示例用 239.5,对光敏电阻分压很合适)。

3.6 中断及事件

  • EOC :规则组转换完成(HAL_ADC_PollForConversion() 轮询或 HAL_ADC_Start_IT() 中断)。

  • JEOC:注入组完成(注入模式才有)。

  • AWD:模拟看门狗(阈值比较,越界触发中断)。

  • DMA:规则组可把 EOC 搬运给 DMA(2、3 例子)。

3.7 校准

  • F1 推荐上电后先 HAL_ADCEx_Calibration_Start()(在 3 份 adc_init/adc_config 里都做了)。

3.8 单次转换 & 连续转换

  • 单次ContinuousConvMode=DISABLE,每次手动 HAL_ADC_Start()(你的例 1)。

  • 连续ENABLE,软触发一次后自动不停(例 2、3 与 DMA 配合)。

3.9 扫描模式

  • 关闭SCAN_DISABLE):单通道(例 1、2)。

  • 开启SCAN_ENABLE):多通道需要,并设置 NbrOfConversion=通道个数(例 3)。

4. ADC 寄存器及库函数介绍

4.1 关键 HAL 函数(规则组)

HAL_ADC_Init(&hadc) --- 初始化 ADC 外设

做什么:

  • hadc.Init 里的初始化字段写进 ADC 寄存器(数据对齐、扫描/连续、触发源、规则组通道数等)。

  • 自动回调 HAL_ADC_MspInit() 完成底层 动作:开 ADC/GPIO 时钟、把引脚设为模拟输入 、配置 ADC 分频(如 RCC_ADCPCLK2_DIV6)。

为什么需要: 只有把"软件配置"写进硬件寄存器,ADC 才会按你的模式工作。

常见字段(都会用到):

  • DataAlignADC_DATAALIGN_RIGHT / LEFT

    • 右对齐 常用(12 位结果在低位,便于直接 0..4095 取值)。
  • ScanConvModeADC_SCAN_DISABLE / ENABLE

    • 单通道关、多通道开(配合 NbrOfConversion 和各 Rank)。
  • ContinuousConvModeDISABLE / ENABLE

    • 单次转换 or 连续不断转换(连续采样时一般配 DMA)。
  • NbrOfConversion1..16

    • 规则组里一共采多少路(多通道时要填总数)。
  • ExternalTrigConvADC_SOFTWARE_START 或定时器触发

    • 软件触发 :代码里 HAL_ADC_Start() 启动。

    • 外部触发:定时器事件来触发采样(做"等间隔采样"很稳)。

小例子(单通道、单次):

cpp 复制代码
hadc1.Instance = ADC1;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.NbrOfConversion = 1;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
HAL_ADC_Init(&hadc1);            // 会回调 HAL_ADC_MspInit 去开时钟/设GPIO为模拟
HAL_ADCEx_Calibration_Start(&hadc1);

HAL_ADC_ConfigChannel(&hadc, &chCfg) --- 把"哪路通道"放到"第几个顺位"

做什么:

  • 某个通道 (如 ADC_CHANNEL_1)放到规则组里的某个 Rank (顺位 1..16),并设置采样时间(SMPRx)。

  • 底层写 SQRx/SMPRx 寄存器。

关键参数:

  • ChannelADC_CHANNEL_0...17(0..15 外部引脚;16 温度;17 参考电压)

  • RankADC_REGULAR_RANK_1...16(转换顺序)

  • SamplingTime1.5/7.5/13.5/28.5/41.5/55.5/71.5/239.5 cycles

    • 源阻大 (如光敏电阻分压)→ 选更长的采样时间(239.5)更稳。

小例子(多通道扫描 4 路):

cpp 复制代码
HAL_ADC_ConfigChannel(&hadc1, &(ADC_ChannelConfTypeDef){
  .Channel = ADC_CHANNEL_0, .Rank = ADC_REGULAR_RANK_1, .SamplingTime = ADC_SAMPLETIME_239CYCLES_5
});
HAL_ADC_ConfigChannel(&hadc1, &(ADC_ChannelConfTypeDef){
  .Channel = ADC_CHANNEL_1, .Rank = ADC_REGULAR_RANK_2, .SamplingTime = ADC_SAMPLETIME_239CYCLES_5
});
// 再配 Rank3、Rank4 ...

轮询路径("实验一")

"我就想偶尔读一下电压,不用 DMA。"

调用顺序:

  1. HAL_ADC_Start(&hadc)

    • 触发规则组开始转换(若是外部触发模式,这里只做使能等待外部事件)。
  2. HAL_ADC_PollForConversion(&hadc, timeout_ms)

    • 轮询EOC标志直到完成或超时。
  3. HAL_ADC_GetValue(&hadc)

    • 读取 DR(数据寄存器)里的 12 位结果。

小例子:

cpp 复制代码
HAL_ADC_Start(&hadc1);
if (HAL_OK == HAL_ADC_PollForConversion(&hadc1, 10)) {
    uint16_t raw = HAL_ADC_GetValue(&hadc1);   // 0..4095
}

适用: 低速、偶发读取;简单但占用 CPU 等待。

DMA 路径("实验二/三")

"我要持续采样、CPU 少管甚至多通道扫描按顺序进数组。"

HAL_ADC_Start_DMA(&hadc, dst, length)

做什么:

  • 启动 ADC + DMA 联动。ADC 每完成一次规则转换,就把结果 通过 DMA 搬到内存

  • dst:目的地址(uint16_t* 或变量地址)。

  • length半字(16bit)个数

    • 单通道:length = 缓冲元素数(比如 1 或 N)

    • 多通道扫描:length = 规则组通道数(比如 4)

为什么是"半字": F103 的 ADC 结果寄存器 DR 是 16 位,1 次结果 = 1 个半字。

配套要求:

  • 必须在初始化时把 DMA 句柄关联给 ADC
cpp 复制代码
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
  • 这样 HAL 才能在内部管理 DMA(启动、停止、中断回调)。

  • DMA 通道要选对固定映射 (F1:ADC1 → DMA1_Channel1),并配:

    • Direction = DMA_PERIPH_TO_MEMORY

    • PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD

    • MemDataAlignment = DMA_MDATAALIGN_HALFWORD

    • PeriphInc = DISABLE(DR 固定)

    • MemInc = ENABLE(数组递增;若目标是单变量就 DISABLE)

    • Mode = DMA_CIRCULAR(循环,不停覆盖)或 DMA_NORMAL(单次)

小例子(单通道持续更新到变量):

cpp 复制代码
uint16_t adc_value;
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adc_value, 1);   // 连续模式+循环DMA时:adc_value 会被不断更新

小例子(4 路扫描进数组):

cpp 复制代码
uint16_t buf[4];
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)buf, 4);  // Rank1→buf[0], Rank2→buf[1], ...

回调:HAL_ADC_ConvCpltCallback / HAL_ADC_ConvHalfCpltCallback

什么时候被调用:

  • DMA 循环模式下,缓冲长度为 N:

    • 传到 N/2 个结果时 → 调 HAL_ADC_ConvHalfCpltCallback()(半传输回调)。

    • 传到 N 个结果时 → 调 HAL_ADC_ConvCpltCallback()(满传输回调)。

  • 这两个回调不用你主动调用 ,是在 DMA IRQ → HAL_DMA_IRQHandler() 里由 HAL 自动触发的。

    你可以重写它们,在里面处理数据(滤波/统计),然后尽快返回。

小例子:

cpp 复制代码
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc)
{
    // 处理 buf[0 .. N/2-1]
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    // 处理 buf[N/2 .. N-1]
}

注意点:

  • 回调是 中断上下文,只做轻量工作或发消息/置标志,重活放到主循环。

  • 如果只是单变量 length=1,通常不会用到半传/满传回调------直接在主循环读变量即可。

注意点:

  • 回调是 中断上下文,只做轻量工作或发消息/置标志,重活放到主循环。

  • 如果只是单变量 length=1,通常不会用到半传/满传回调------直接在主循环读变量即可。

cpp 复制代码
__HAL_LINKDMA(&adc_handle, DMA_Handle, dma_handle);
//           ^外设句柄   ^外设句柄里的成员名    ^DMA句柄变量

5、把它们放到三个实验里怎么对上

  • 实验一(轮询)

    • HAL_ADC_Init()(会 MSP 配时钟/GPIO/分频),再每次:
      HAL_ADC_Start()HAL_ADC_PollForConversion()HAL_ADC_GetValue()

    • 简单直观,但 CPU 要等。

  • 实验二(单通道 + DMA)

    • HAL_ADC_Init() + HAL_ADC_ConfigChannel() + 配 DMA(通道1、P2M、半字、循环)

    • __HAL_LINKDMA() 绑定

    • HAL_ADC_Start_DMA(&hadc, &adc_result, 1)

    • 连续模式下,adc_result 自动更新;主循环直接读。

  • 实验三(多通道 + DMA)

    • ScanConvMode=ENABLENbrOfConversion=4

    • 依次 HAL_ADC_ConfigChannel() 把 CH0..CH3 放到 RANK1..4

    • DMA 还是通道1、P2M、半字、循环

    • HAL_ADC_Start_DMA(&hadc, buf, 4),数组 [0..3] 依顺序存 Rank1..4 的结果并循环覆盖。

6、常见坑 & 小贴士

  • length 单位 :F1 的 ADC 结果是 16 位,HAL_ADC_Start_DMA(..., length)length=半字个数

  • 采样时间 要按源阻 选,光敏电阻分压用 239.5 cycles 很合适。

  • ADCCLK ≤ 14MHzRCC_ADCPCLK2_DIVx)------太快会影响精度。

  • 连续 + DMA 循环时,数组内容持续被覆盖:要么用回调分段处理,要么主循环里注意拷贝/关中断。

  • 单变量作为 DMA 目的时可把 MemInc=DISABLE(否则地址会递增到未知区域)。

  • 换通道/改序列后记得重新 HAL_ADC_ConfigChannel() ,必要时 HAL_ADC_Stop() 再启动。

DMA 配置要点(配合 ADC)

  • 通道映射(F1 固定)ADC1 → DMA1_Channel1

  • DirectionDMA_PERIPH_TO_MEMORY

  • DataAlignment :ADC DR 是 16 位 → P/M ALIGN = HALFWORD

  • Inc :目的端数组 → MINC_ENABLE;若写入单变量MINC_DISABLE

  • Mode :连续采样建议 DMA_CIRCULAR

  • Priority:中/高,视系统而定。

7. 总结下面3 个实验

例 ① 单通道 + 轮询

  • 关闭扫描、关闭连续、软件触发,每次 Start→Poll→GetValue

  • 适合低速、偶尔采样;CPU忙时不建议。

例 ② 单通道 + DMA

  • 连续模式 + DMA 循环,把结果不停写到变量(或环形缓冲)。

  • 适合持续采样;CPU 只读变量即可。

例 ③ 多通道 + DMA(扫描)

  • 开启扫描,NbrOfConversion=4;DMA 循环把 4 通道结果依序写入数组。

  • 适合多传感器轮询采样;注意数组随时被覆盖,读时做保护(临界区/双缓冲)。


8. 工程建议 & 常见坑

  • ADCCLK ≤ 14 MHz;否则精度掉。

  • 采样时间要足 :高源阻(如光敏电阻分压)→ 用 239.5 cycles

  • Vref 误差 :若要更准,测 Vrefint 做一次标定(或用外部精密参考)。

  • DMA 循环 读取:数组会被不断覆盖,处理时用双缓冲或在回调里复制。

  • 目的端对齐 :ADC 必须用 HALFWORD;长度参数是半字个数

  • 单变量 DMA 目的 :把 MemInc 设为 DISABLE,避免地址溢出。

  • 内部温度/参考 :要置 TSVREFE 使能后才能读(HAL 有封装或手动置位 CR2)。

实验:

实验一:ADC 单通道采集(软件触发 + 轮询)

main.c

cpp 复制代码
#include "sys.h"      // 系统层头文件:提供时钟初始化等系统函数声明
#include "delay.h"    // 延时函数:delay_ms / delay_us
#include "led.h"      // LED 控制:led_init/led1_on/led2_on 等
#include "uart1.h"    // 串口1:uart1_init、printf重定向等
#include "adc.h"      // 本例自定义ADC接口:adc_init、adc_get_result

int main(void)
{
    HAL_Init();                         // 初始化HAL库:配置SysTick为1ms节拍、NVIC分组等
    stm32_clock_init(RCC_PLL_MUL9);     // 配置系统时钟:外部8MHz *9 = 72MHz(可选:RCC_PLL_MUL6/7/8/9...)
    led_init();                         // 初始化LED相关GPIO(输出模式、默认电平)
    uart1_init(115200);                 // 初始化USART1,波特率115200,8N1,打开外设时钟与GPIO复用
    adc_init();                         // 初始化ADC1(见adc.c),包括GPIO模拟、分频、模式、校准
    printf("hello world!\r\n");         // 通过串口打印字符串,验证串口正常

    while(1)                            // 主循环
    {
        // 读取ADC通道1一次(返回0~4095),并按照Vref=3.3V换算电压值
        // 注意:更严谨可除以4095。Vref建议用内部Vrefint校准得到更准确值
        printf("adc result: %f\r\n", (float)adc_get_result(ADC_CHANNEL_1) / 4096 * 3.3);
        delay_ms(500);                  // 500ms采样一次
    }
}

adc.c

cpp 复制代码
#include "adc.h"                         // 本模块头文件:声明外部可见的ADC接口

ADC_HandleTypeDef adc_handle = {0};      // 定义ADC句柄,全局保存配置与状态

void adc_init(void)
{
    adc_handle.Instance = ADC1;                         // 选择硬件实例:ADC1(F103有ADC1/ADC2/ADC3)
    adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;    // 数据右对齐(常用;另选:ADC_DATAALIGN_LEFT 左对齐)
    adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;    // 扫描模式禁用(只转换一个通道;另选:ADC_SCAN_ENABLE)
    adc_handle.Init.ContinuousConvMode = DISABLE;       // 非连续(单次)转换(另选:ENABLE 连续转换)
    adc_handle.Init.NbrOfConversion = 1;                // 规则组转换个数=1(多通道扫描时设置为通道数)
    adc_handle.Init.DiscontinuousConvMode = DISABLE;    // 不连续模式禁用(另选:ENABLE,配合NbrOfDiscConversion)
    adc_handle.Init.NbrOfDiscConversion = 0;            // 不连续模式下每段的转换数(1~8);此处无效
    adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 外部触发源选择:软件触发
    // 可选外部触发(不同芯片略有差异):
    // ADC_EXTERNALTRIGCONV_T1_CC1/T1_CC2/T1_CC3/T2_CC2/T3_TRGO/T4_CC4/EXT_IT11_TIM8_TRGO 等

    HAL_ADC_Init(&adc_handle);                          // 初始化ADC寄存器,内部会回调HAL_ADC_MspInit完成底层配置
    HAL_ADCEx_Calibration_Start(&adc_handle);           // 启动ADC校准(F1建议上电后做一次以减小偏差)
}

// HAL会在HAL_ADC_Init中回调此函数完成底层初始化
void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)
{
    if(hadc->Instance == ADC1)                          // 判断是否为ADC1实例
    {
        RCC_PeriphCLKInitTypeDef adc_clk_init = {0};    // 定义RCC外设时钟配置结构体并置零
        GPIO_InitTypeDef gpio_init_struct = {0};        // 定义GPIO初始化结构体并置零
        
        __HAL_RCC_ADC1_CLK_ENABLE();                    // 使能ADC1外设时钟(APB2)
        __HAL_RCC_GPIOA_CLK_ENABLE();                   // 使能GPIOA时钟(PA引脚属于GPIOA)

        gpio_init_struct.Pin = GPIO_PIN_1;              // 选择PA1(对应ADC通道1)
        gpio_init_struct.Mode = GPIO_MODE_ANALOG;       // 设置为模拟输入模式(禁止数字输入/输出及上下拉)
        HAL_GPIO_Init(GPIOA, &gpio_init_struct);        // 执行GPIO初始化(对PA1生效)
        
        adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; // 指定要配置的外设时钟类型为ADC
        adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;    // 设置ADC时钟=APB2时钟/6(≤14MHz)
        // 可选:RCC_ADCPCLK2_DIV2 / DIV4 / DIV6 / DIV8 ------ 根据速度与精度折中选择
        HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);              // 应用上述ADC时钟分频设置
    }
}

// 规则组通道配置:把具体通道放入某个Rank并设置采样时间
void adc_channel_config(ADC_HandleTypeDef* hadc, uint32_t ch, uint32_t rank, uint32_t stime)
{
    ADC_ChannelConfTypeDef adc_ch_config = {0};         // 通道配置结构体清零
    
    adc_ch_config.Channel = ch;                         // 指定通道:ADC_CHANNEL_x(0~15外部;16温度;17 Vrefint)
    adc_ch_config.Rank = rank;                          // 指定规则序号:ADC_REGULAR_RANK_1~16(决定转换顺序)
    adc_ch_config.SamplingTime = stime;                 // 指定采样时间:见下方可选说明
    // 可选采样时间枚举:
    // ADC_SAMPLETIME_1CYCLE_5 / 7CYCLES_5 / 13CYCLES_5 / 28CYCLES_5 /
    // 41CYCLES_5 / 55CYCLES_5 / 71CYCLES_5 / 239CYCLES_5
    // 源阻越大(例如光敏电阻分压),采样时间应越长以保证采样电容充分充电
    
    HAL_ADC_ConfigChannel(hadc, &adc_ch_config);        // 写入SQR/SMPR寄存器,完成通道加入与采样时间配置
}

// 读取指定通道一次(软件触发 -> 轮询转换完成 -> 读DR)
uint32_t adc_get_result(uint32_t ch)
{
    adc_channel_config(&adc_handle, ch, ADC_REGULAR_RANK_1, ADC_SAMPLETIME_239CYCLES_5); // 将通道ch放到Rank1,采样239.5周期
    
    HAL_ADC_Start(&adc_handle);                         // 启动规则组转换(单次,因为Continuous=DISABLE)
    HAL_ADC_PollForConversion(&adc_handle, 10);         // 轮询等待转换完成,超时10ms(返回HAL_OK则完成)
    return (uint16_t)HAL_ADC_GetValue(&adc_handle);     // 读取12位转换结果(右对齐)
}

实验二:ADC 单通道 + DMA 连续采样(ADC1→变量)

main.c

cpp 复制代码
#include "sys.h"      // 系统初始化/时钟
#include "delay.h"    // 延时
#include "led.h"      // LED
#include "uart1.h"    // 串口1
#include "adc.h"      // 本例ADC+DMA接口:adc_dma_init

uint16_t adc_result = 0;                   // DMA循环将ADC结果(半字)不断写到此变量

int main(void)
{
    HAL_Init();                            // HAL初始化(SysTick等)
    stm32_clock_init(RCC_PLL_MUL9);        // 72MHz主频
    led_init();                            // LED初始化
    uart1_init(115200);                    // 串口1初始化
    adc_dma_init((uint32_t  *)&adc_result);// 初始化ADC1+DMA:目标地址=adc_result,长度=1(见adc.c)
    printf("hello world!\r\n");            // 打印开机信息

    while(1)                               // 主循环
    { 
        // adc_result 会被 DMA 不断更新,这里直接读取并换算电压
        printf("adc result: %f\r\n", (float)adc_result / 4096 * 3.3);
        delay_ms(500);                     // 0.5s打印一次
    }
}

adc.c

cpp 复制代码
#include "adc.h"                           // 声明本模块接口

ADC_HandleTypeDef adc_handle = {0};        // ADC1 句柄
DMA_HandleTypeDef dma_handle = {0};        // DMA1 通道句柄(用于ADC)

// ADC高层配置:单通道 + 连续转换
void adc_config(void)
{
    adc_handle.Instance = ADC1;                            // 使用ADC1
    adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;       // 结果右对齐
    adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;       // 不扫描(单通道)
    adc_handle.Init.ContinuousConvMode = ENABLE;           // 连续转换(自动重复)
    adc_handle.Init.NbrOfConversion = 1;                   // 规则通道数=1
    adc_handle.Init.DiscontinuousConvMode = DISABLE;       // 不连续禁用
    adc_handle.Init.NbrOfDiscConversion = 0;               // 无效
    adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发(连转仅第一次需要)
    HAL_ADC_Init(&adc_handle);                             // 应用上述配置
    HAL_ADCEx_Calibration_Start(&adc_handle);              // 校准
}

// HAL回调:底层时钟/引脚/分频配置
void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)
{
    if(hadc->Instance == ADC1)                             // 仅处理ADC1
    {
        RCC_PeriphCLKInitTypeDef adc_clk_init = {0};       // RCC外设时钟配置结构体
        GPIO_InitTypeDef gpio_init_struct = {0};           // GPIO初始化结构体
        
        __HAL_RCC_ADC1_CLK_ENABLE();                       // 使能ADC1时钟
        __HAL_RCC_GPIOA_CLK_ENABLE();                      // 使能GPIOA时钟
        
        gpio_init_struct.Pin = GPIO_PIN_1;                 // PA1=通道1输入
        gpio_init_struct.Mode = GPIO_MODE_ANALOG;          // 模拟输入模式
        HAL_GPIO_Init(GPIOA, &gpio_init_struct);           // 初始化PA1
        
        adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; // 选择ADC外设时钟配置
        adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;    // ADCCLK=PCLK2/6(≤14MHz)
        HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);              // 写入分频设置
    }
}

// DMA配置:ADC1 → DMA1_Channel1(固定映射)
void dma_config(void)
{
    __HAL_RCC_DMA1_CLK_ENABLE();                           // 使能DMA1时钟
    dma_handle.Instance = DMA1_Channel1;                   // 选择通道1(ADC1固定使用)
    dma_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;      // 方向:外设→内存(ADC->内存)
    
    // 目的端(内存)配置:
    dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; // 半字对齐(ADC DR 是16位)
    dma_handle.Init.MemInc = DMA_MINC_ENABLE;                   // 目的地址自增(若目标是单变量可改为DISABLE)
    
    // 源端(外设)配置:
    dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 半字对齐(与ADC DR一致)
    dma_handle.Init.PeriphInc = DMA_PINC_DISABLE;                  // 外设地址不自增(固定DR)
    
    dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM;             // 通道优先级(LOW/MEDIUM/HIGH/VERY_HIGH)
    dma_handle.Init.Mode = DMA_CIRCULAR;                        // 循环模式(持续覆盖目标地址/数组)
    HAL_DMA_Init(&dma_handle);                                  // 写DMA相关寄存器
    
    __HAL_LINKDMA(&adc_handle, DMA_Handle, dma_handle);         // 将DMA句柄挂接到ADC句柄(adc_handle.DMA_Handle)
}

// 通道配置工具函数:将通道加入规则组并设置采样时间
void adc_channel_config(ADC_HandleTypeDef* hadc, uint32_t ch, uint32_t rank, uint32_t stime)
{
    ADC_ChannelConfTypeDef adc_ch_config = {0};           // 通道配置结构体清零
    
    adc_ch_config.Channel = ch;                           // 指定通道(ADC_CHANNEL_1 等)
    adc_ch_config.Rank = rank;                            // 指定规则序号(RANK_1)
    adc_ch_config.SamplingTime = stime;                   // 设置采样时间(例如239.5周期)
    HAL_ADC_ConfigChannel(hadc, &adc_ch_config);          // 写入寄存器生效
}

// 对外一键初始化:配置ADC->通道->DMA,并启动ADC+DMA
void adc_dma_init(uint32_t *mar)
{
    adc_config();                                         // 配置ADC1为连续转换
    adc_channel_config(&adc_handle, ADC_CHANNEL_1,        // 选择通道1(PA1)
                       ADC_REGULAR_RANK_1, ADC_SAMPLETIME_239CYCLES_5); // Rank1,长采样更稳
    dma_config();                                         // 配置DMA1_Channel1(外设->内存,循环)
    
    HAL_ADC_Start_DMA(&adc_handle, mar, 1);               // 启动ADC+DMA:目的地址mar,长度=1(单位:半字)
    // 说明:若 mar 指向数组且希望连续写入多个元素,可以把长度改为元素个数
}

实验三:ADC 多通道扫描 + DMA(ADC1→数组)

main.c

cpp 复制代码
#include "sys.h"      // 系统初始化
#include "delay.h"    // 延时
#include "led.h"      // LED
#include "uart1.h"    // 串口1
#include "adc.h"      // 本例ADC+DMA接口

uint16_t adc_result[4] = {0};            // 存放4个通道的结果,DMA循环依次写入[0..3]

int main(void)
{
    HAL_Init();                           // HAL初始化
    stm32_clock_init(RCC_PLL_MUL9);       // 72MHz主频
    led_init();                           // LED初始化
    uart1_init(115200);                   // 串口1初始化
    adc_dma_init((uint32_t  *)&adc_result);// 初始化ADC扫描+DMA,目标地址为adc_result数组
    printf("hello world!\r\n");           // 打印开机信息

    while(1)                              // 主循环
    { 
        // 打印四个通道(Rank1~Rank4)的电压值(假定Vref=3.3V)
        printf("通道0电压: %f\r\n", (float)adc_result[0] / 4096 * 3.3);
        printf("通道1电压: %f\r\n", (float)adc_result[1] / 4096 * 3.3);
        printf("通道2电压: %f\r\n", (float)adc_result[2] / 4096 * 3.3);
        printf("通道3电压: %f\r\n\r\n", (float)adc_result[3] / 4096 * 3.3);
        delay_ms(500);                     // 0.5s打印一次(期间数组会被DMA不断覆盖)
    }
}

adc.c

cpp 复制代码
#include "adc.h"                          // 本模块头文件

ADC_HandleTypeDef adc_handle = {0};       // ADC1 句柄
DMA_HandleTypeDef dma_handle = {0};       // DMA1 通道句柄

// ADC高层配置:多通道扫描 + 连续
void adc_config(void)
{
    adc_handle.Instance = ADC1;                           // 选择ADC1
    adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;      // 右对齐
    adc_handle.Init.ScanConvMode = ADC_SCAN_ENABLE;       // 启用扫描(多通道)
    adc_handle.Init.ContinuousConvMode = ENABLE;          // 连续转换:SQR列表循环转换
    adc_handle.Init.NbrOfConversion = 4;                  // 规则组通道总数=4(Rank1~Rank4)
    adc_handle.Init.DiscontinuousConvMode = DISABLE;      // 不连续禁用
    adc_handle.Init.NbrOfDiscConversion = 0;              // 无效
    adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;// 软件触发(连转仅首次)
    HAL_ADC_Init(&adc_handle);                            // 写寄存器并调用MSP
    HAL_ADCEx_Calibration_Start(&adc_handle);             // 校准
}

// HAL回调:ADC1底层时钟/引脚/分频
void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)
{
    if(hadc->Instance == ADC1)                            // 仅处理ADC1
    {
        RCC_PeriphCLKInitTypeDef adc_clk_init = {0};      // RCC外设时钟配置
        GPIO_InitTypeDef gpio_init_struct = {0};          // GPIO初始化结构体
        
        __HAL_RCC_ADC1_CLK_ENABLE();                      // 使能ADC1时钟
        __HAL_RCC_GPIOA_CLK_ENABLE();                     // 使能GPIOA时钟
        
        gpio_init_struct.Pin = GPIO_PIN_0 | GPIO_PIN_1 |  // PA0=CH0, PA1=CH1,
                               GPIO_PIN_2 | GPIO_PIN_3;   // PA2=CH2, PA3=CH3
        gpio_init_struct.Mode = GPIO_MODE_ANALOG;         // 设置为模拟输入
        HAL_GPIO_Init(GPIOA, &gpio_init_struct);          // 初始化PA0~PA3
        
        adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; // 选择ADC外设时钟
        adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;    // ADCCLK=PCLK2/6(≤14MHz)
        HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);              // 应用分频设置
    }
}

// DMA配置:ADC1 → DMA1_Channel1,循环把4个半字写入数组
void dma_config(void)
{
    __HAL_RCC_DMA1_CLK_ENABLE();                          // 开DMA1时钟
    dma_handle.Instance = DMA1_Channel1;                  // 选择通道1(ADC1固定)
    dma_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;     // 外设→内存
    
    // 目的端(内存)参数:数组按半字递增
    dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; // 半字对齐(uint16_t)
    dma_handle.Init.MemInc = DMA_MINC_ENABLE;                    // 目的地址递增(数组)
    
    // 源端(外设)参数:固定地址、半字宽
    dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 半字对齐(ADC DR)
    dma_handle.Init.PeriphInc = DMA_PINC_DISABLE;                  // 外设地址固定(DR)
    
    dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM;                // 通道优先级
    dma_handle.Init.Mode = DMA_CIRCULAR;                           // 循环模式:每次扫描结果覆盖数组
    HAL_DMA_Init(&dma_handle);                                     // 写DMA配置
    
    __HAL_LINKDMA(&adc_handle, DMA_Handle, dma_handle);            // 挂接DMA到ADC句柄
}

// 通道配置:把CH0~CH3依次放进Rank1~Rank4,均使用长采样时间
void adc_channel_config(ADC_HandleTypeDef* hadc, uint32_t ch, uint32_t rank, uint32_t stime)
{
    ADC_ChannelConfTypeDef adc_ch_config = {0};           // 通道配置结构体清零
    
    adc_ch_config.Channel = ch;                           // 指定通道(ADC_CHANNEL_0/1/2/3)
    adc_ch_config.Rank = rank;                            // 指定顺位(RANK_1..4)
    adc_ch_config.SamplingTime = stime;                   // 指定采样时间(这里均为239.5 cycles)
    HAL_ADC_ConfigChannel(hadc, &adc_ch_config);          // 写入SQR/SMPR
}

// 一键初始化:配置ADC扫描顺序 + DMA目标数组,并启动
void adc_dma_init(uint32_t *mar)
{
    adc_config();                                         // 配置ADC为扫描+连续
    adc_channel_config(&adc_handle, ADC_CHANNEL_0,        // 第1个转换:CH0→Rank1→写入数组[0]
                       ADC_REGULAR_RANK_1, ADC_SAMPLETIME_239CYCLES_5);
    adc_channel_config(&adc_handle, ADC_CHANNEL_1,        // 第2个转换:CH1→Rank2→写入数组[1]
                       ADC_REGULAR_RANK_2, ADC_SAMPLETIME_239CYCLES_5);
    adc_channel_config(&adc_handle, ADC_CHANNEL_2,        // 第3个转换:CH2→Rank3→写入数组[2]
                       ADC_REGULAR_RANK_3, ADC_SAMPLETIME_239CYCLES_5);
    adc_channel_config(&adc_handle, ADC_CHANNEL_3,        // 第4个转换:CH3→Rank4→写入数组[3]
                       ADC_REGULAR_RANK_4, ADC_SAMPLETIME_239CYCLES_5);
    dma_config();                                         // 配置DMA循环写4个半字
    
    HAL_ADC_Start_DMA(&adc_handle, mar, 4);               // 启动ADC+DMA:目标mar为uint16_t[4],长度=4(半字个数)
}

四、ADC 与 DMA:配置步骤 & 原理(工作机制)

A. ADC(F103,12-bit SAR)

原理 :逐次逼近型 ADC;规则组(Regular)定义一串要采的通道(SQR1~3),每个通道有采样时间(SMPRx)。一次转换=采样阶段(采样电容充电)+转换阶段(12位逼近)。
实现步骤(HAL):

  1. 时钟与引脚

    • 使能 ADC1 与对应 GPIO 时钟;

    • 通道引脚设为 GPIO_MODE_ANALOG

    • 配置 ADCCLK=PCLK2/div(≤14 MHz)。

  2. ADC 句柄参数

    • DataAlign(LEFT/RIGHT)

    • ScanConvMode(多通道=ENABLE,单通道=DISABLE)

    • ContinuousConvMode(连续/单次)

    • NbrOfConversion(规则通道数)

    • DiscontinuousConvMode/NbrOfDiscConversion(可将规则组拆段执行)

    • ExternalTrigConv(硬件触发源或软件触发)

  3. 通道与采样时间

    • HAL_ADC_ConfigChannel() 设置 Channel/Rank/SamplingTime

    • 采样时间越长→输入高阻/源阻大时更稳。

  4. 启动方式

    • 轮询:HAL_ADC_StartHAL_ADC_PollForConversionHAL_ADC_GetValue

    • DMA:HAL_ADC_Start_DMA(adc, dest, len)(连续或扫描配合 DMA_CIRCULAR

校准HAL_ADCEx_Calibration_Start() 上电后做一次,可改善偏差。
电压换算V = raw/4095 * Vref;若要更准,需测量 Vrefint 做标定。

B. DMA(F1,DMA1/2 Channel)

原理 :DMA 控制器根据通道配置,自动把"源地址→目的地址"搬运指定个数的数据(按 BYTE/HALFWORD/WORD),支持地址递增、循环模式、传输完成/半传输中断。
实现步骤

  1. 开 DMA 时钟选对通道(F1 固定映射:ADC1→DMA1_Channel1)。

  2. 配置方向/对齐/自增/模式/优先级(见上注释)。

  3. 把 DMA 句柄挂到外设句柄__HAL_LINKDMA(&adc_handle, DMA_Handle, dma_handle)

  4. 启动

    • 轮询:HAL_DMA_Start + HAL_DMA_PollForTransfer

    • 与外设:调用外设的 HAL_xxx_Start_DMA(如 HAL_ADC_Start_DMA

  5. 循环采样 :对接收场景常用 DMA_CIRCULAR,缓冲区会被持续覆盖。

五、". / = 后面还能选哪些参数?作用是什么?"

1) ADC_HandleTypeDef.Init 主要字段

  • DataAlign:

    • ADC_DATAALIGN_RIGHT(常用;读低位)

    • ADC_DATAALIGN_LEFT(左对齐;读高位)

  • ScanConvMode: ADC_SCAN_DISABLE / ENABLE(单/多通道)

  • ContinuousConvMode: DISABLE / ENABLE(单次/连续)

  • NbrOfConversion: 1~16(规则通道数量)

  • DiscontinuousConvMode: DISABLE / ENABLE(把规则组拆段执行,常与外部触发配合)

  • NbrOfDiscConversion: 1~8(不连续每段个数)

  • ExternalTrigConv(F1 可选硬件触发源,或软件触发):

    • ADC_SOFTWARE_START

    • ADC_EXTERNALTRIGCONV_T1_CC1 / T1_CC2 / T1_CC3 / T2_CC2 / T3_TRGO / T4_CC4 / EXT_IT11_TIM8_TRGO 等(不同芯片略有差异)

  • MSP 分频

    • RCC_ADCPCLK2_DIV2 / DIV4 / DIV6 / DIV8(保证 ADCCLK ≤ 14 MHz)

2) ADC_ChannelConfTypeDef

  • ChannelADC_CHANNEL_0~17(F1:0~15=外部通道;16=温度;17=Vrefint)

  • RankADC_REGULAR_RANK_1 ~ _16(决定写入 SQR 序列的位置)

  • SamplingTime

    • ADC_SAMPLETIME_1CYCLE_5 / 7CYCLES_5 / 13CYCLES_5 / 28CYCLES_5 / 41CYCLES_5 / 55CYCLES_5 / 71CYCLES_5 / 239CYCLES_5

    • 采样电容充电时间;源阻高/精度要求高→选长一点

3) DMA_InitTypeDef

  • Direction: DMA_PERIPH_TO_MEMORY / DMA_MEMORY_TO_PERIPH / DMA_MEMORY_TO_MEMORY

  • PeriphInc: DMA_PINC_DISABLE / ENABLE

  • MemInc: DMA_MINC_DISABLE / ENABLE

  • PeriphDataAlignment: DMA_PDATAALIGN_BYTE / HALFWORD / WORD

  • MemDataAlignment: DMA_MDATAALIGN_BYTE / HALFWORD / WORD

  • Mode: DMA_NORMAL / DMA_CIRCULAR

  • Priority: DMA_PRIORITY_LOW / MEDIUM / HIGH / VERY_HIGH

1.Direction --- 传输方向

作用:告诉 DMA"谁是外设端、谁是内存端"。这会影响"地址自增""数据宽度"等含义。

  • DMA_PERIPH_TO_MEMORY(外设→内存)

    场景:ADC 接收USART/SPI/RX 等。

    典型:外设地址固定 (DR 寄存器),内存地址递增到缓冲区。

  • DMA_MEMORY_TO_PERIPH(内存→外设)

    场景:USART/SPI/TX 、把一段缓冲发出去。

    典型:外设地址固定 (DR),内存地址递增

  • DMA_MEMORY_TO_MEMORY(内存↔内存)

    场景:大块拷贝/填充

    典型:源/目的都递增 ;或做"常数填充"时源不递增、目递增。

    ⚠️ 注:在部分 F1 上,M2M 不支持循环模式(以参考手册为准)。

小贴士:调用 HAL_DMA_Start(hdma, src, dst, length) 时,参数顺序始终是源地址、目标地址 ,与 Direction 保持一致。


2.PeriphInc --- 外设端地址自增

作用 :每搬运一次后,外设端地址是否 + 数据宽度。

  • DMA_PINC_DISABLE(常用)

    多数外设只有一个数据寄存器(如 USARTx->DRADCx->DR),地址固定,应禁用。

  • DMA_PINC_ENABLE

    很少用。只有在外设端是一片地址连续的寄存器/缓冲(或做 M2M 时把"源"当"外设端")才会启用。


  1. MemInc --- 内存端地址自增

作用 :每搬运一次后,内存端地址是否 + 数据宽度。

  • DMA_MINC_ENABLE(常用)

    数组 里写/从数组读(RX/TX 缓冲、ADC 采样数组)。

  • DMA_MINC_DISABLE

    一个固定变量 里写(如 ADC 连续采样只想覆盖同一个 uint16_t 变量);或做"常数填充"(源固定、目的递增)。


4.PeriphDataAlignment --- 外设端数据宽度

作用 :外设端一次传输的数据单位(字节/半字/字)。

  • DMA_PDATAALIGN_BYTE(8 位)
    USART/一般 SPI 的数据寄存器默认 8 位。

  • DMA_PDATAALIGN_HALFWORD(16 位)
    ADC 的 DR 是 16 位;SPI 若配置 16 位帧也选这个。

  • DMA_PDATAALIGN_WORD(32 位)

    少数外设/内存映射需要;常见度低。

⚠️ 必须与外设实际数据宽度匹配,否则会溢出/错位。


5.MemDataAlignment --- 内存端数据宽度

作用 :内存端一次传输的数据单位(字节/半字/字)。

  • DMA_MDATAALIGN_BYTE(8 位)

    uint8_t 数组搬运。

  • DMA_MDATAALIGN_HALFWORD(16 位)

    uint16_t 数组或变量搬运(ADC 最常用)。

  • DMA_MDATAALIGN_WORD(32 位)

    uint32_t 数组搬运(速度更高,但地址必须 4 字节对齐)。

允许"外设端宽度"和"内存端宽度"不同,但地址必须按各自宽度对齐;F1 不带 FIFO,宽度不匹配会降低效率或出错。


6.Mode --- 传输模式

作用 :到达 length 后是否自动"重装"并继续。

  • DMA_NORMAL(单次)

    传满就停;适合一次性搬运、单次 TX、单次采样。

  • DMA_CIRCULAR(循环)

    计数清零后自动重新装载 初始计数和地址,继续搬运。

    场景:ADC 连续采样到循环缓冲串口不停接收

    • MemInc=ENABLE:在数组里循环写入(配合半传/满传回调做环形处理)。

    • MemInc=DISABLE:一直覆盖同一个变量(只保留最新值 )。

      ⚠️ M2M 在许多 F1/早期系列不支持循环(查 RM)。


7.Priority --- DMA 通道优先级

作用:多通道并发时的仲裁先后(非 NVIC 中断优先级)。

  • DMA_PRIORITY_LOW / MEDIUM / HIGH / VERY_HIGH
    RX/实时性强 (如高速 UART RX、ADC 采样)设高;

    发数据/非关键链路可设低。

    只有当多条 DMA 同时争用总线时才体现出差别。

4) 常用 HAL 函数(作用简述)

  • ADC

    • HAL_ADC_Init():写入 ADC 初始化参数并调用 MSP

    • HAL_ADC_ConfigChannel():配置 SQR/SMPR(通道/顺序/采样时间)

    • HAL_ADCEx_Calibration_Start():校准

    • HAL_ADC_Start() / HAL_ADC_Stop():开始/停止规则转换

    • HAL_ADC_PollForConversion():轮询等待转换结束

    • HAL_ADC_GetValue():取 12 位结果

    • HAL_ADC_Start_DMA(adc, dst, len):ADC+DMA 启动(dst 为半字数组/变量,len 为半字个数)

  • DMA

    • HAL_DMA_Init() / HAL_DMA_DeInit():通道配置/反配

    • HAL_DMA_Start() / HAL_DMA_Start_IT():启动一次搬运(可开中断)

    • HAL_DMA_PollForTransfer():轮询等待 HT/TC

    • HAL_DMA_Abort(_IT):终止

    • __HAL_LINKDMA():将 DMA 句柄挂入外设句柄

    • __HAL_DMA_GET_COUNTER():读剩余计数(CNDTR)


六、实战提示(精度与稳定性)

  • Vref 不是绝对 3.3V :要更准,用 Vrefint 标定或用外部参考。

  • 采样时间选择 :光敏电阻+分压器源阻较高,采样时间选长(如 239.5 cycles)能让采样电容充分充电。

  • ADCCLK 频率:F1 要 ≤14 MHz,超了会精度下降。

  • DMA 循环:多通道+循环时,数组会被不断覆盖;处理数据时注意临界区或使用双缓冲。

  • MemInc 设置 :单变量目的地建议 MINC_DISABLE,数组则 MINC_ENABLE

相关推荐
anghost1504 小时前
基于单片机的防酒驾系统设计
单片机·嵌入式硬件·毕业设计·流程图
lepton_yang4 小时前
Zephyr下控制ESP32S3的GPIO口
linux·嵌入式硬件·esp32·zephyr
AI+程序员在路上4 小时前
单片机驱动LCD显示模块LM6029BCW
c语言·单片机·嵌入式硬件
XINVRY-FPGA5 小时前
10CL016YF484C8G Altera FPGA Cyclone
嵌入式硬件·网络协议·fpga开发·云计算·硬件工程·信息与通信·fpga
Hero_11276 小时前
学习Stm32 的第一天
stm32·嵌入式硬件·学习
ye150127774559 小时前
DC6v-36V转3.2V1A恒流驱动芯片WT7017
单片机·嵌入式硬件·其他
scilwb19 小时前
RoboCon考核题——scilwb
单片机
点灯小铭20 小时前
基于STM32单片机智能RFID刷卡汽车位锁桩设计
stm32·单片机·汽车·毕业设计·课程设计
bai5459361 天前
STM32 软件I2C读写MPU6050
stm32·单片机·嵌入式硬件