当前位置: 首页 > news >正文

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 框图

外部引脚/内部信号 ─> 模拟多路复用器 ─> 采样保持电容 ─> 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 已做)。

例:

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 → … 转。

  • 多通道例子里:

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() 启动。

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

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

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 路):

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 位结果。

小例子:

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

__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(单次)

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

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

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

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 自动触发的。
    你可以重写它们,在里面处理数据(滤波/统计),然后尽快返回。

小例子:

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,通常不会用到半传/满传回调——直接在主循环读变量即可。

__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

#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_resultint 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

#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; // 指定要配置的外设时钟类型为ADCadc_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

#include "sys.h"      // 系统初始化/时钟
#include "delay.h"    // 延时
#include "led.h"      // LED
#include "uart1.h"    // 串口1
#include "adc.h"      // 本例ADC+DMA接口:adc_dma_inituint16_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

#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;                            // 使用ADC1adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;       // 结果右对齐adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;       // 不扫描(单通道)adc_handle.Init.ContinuousConvMode = ENABLE;           // 连续转换(自动重复)adc_handle.Init.NbrOfConversion = 1;                   // 规则通道数=1adc_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);           // 初始化PA1adc_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

#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

#include "adc.h"                          // 本模块头文件ADC_HandleTypeDef adc_handle = {0};       // ADC1 句柄
DMA_HandleTypeDef dma_handle = {0};       // DMA1 通道句柄// ADC高层配置:多通道扫描 + 连续
void adc_config(void)
{adc_handle.Instance = ADC1;                           // 选择ADC1adc_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);                            // 写寄存器并调用MSPHAL_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=CH3gpio_init_struct.Mode = GPIO_MODE_ANALOG;         // 设置为模拟输入HAL_GPIO_Init(GPIOA, &gpio_init_struct);          // 初始化PA0~PA3adc_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 时把“源”当“外设端”)才会启用。


3. 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

http://www.dtcms.com/a/338020.html

相关文章:

  • Python pyzmq 库详解:从入门到高性能分布式通信
  • 学习嵌入式的第二十天——数据结构
  • 【前端面试题】JavaScript 核心知识点解析(第一题到第十三题)
  • 【牛客刷题】 01字符串按递增长度截取转换详解
  • 【MyBatis-Plus】一、快速入门
  • Day17: 数据魔法学院:用Pandas打开奇幻世界
  • MySQL面试题:MyISAM vs InnoDB?聚簇索引是什么?主键为何要趋势递增?
  • 从“换灯节能”到“智能调光”:城市智慧照明技术升级的节能革命
  • LangChain4j (3) :AiService工具类、流式调用、消息注解
  • 吴恩达 Machine Learning(Class 2)
  • 数字时代著作权侵权:一场资本与法律的博弈
  • 「Flink」业务搭建方法总结
  • 嵌入式设备Lwip协议栈实现功能
  • 摔倒检测数据集:1w+图像,yolo标注
  • 02.Linux基础命令
  • 8.18 机器学习-决策树(1)
  • docker部署flask并迁移至内网
  • Zephyr下控制ESP32S3的GPIO口
  • RK3568 NPU RKNN(六):RKNPU2 SDK
  • FlycoTabLayout CommonTabLayout 支持Tab选中字体变大 选中tab的加粗效果首次无效的bug
  • 探索性测试:灵活找Bug的“人肉探测仪”
  • 前端 大文件分片下载上传
  • 宝塔面板多Python版本管理与项目部署
  • excel表格 Vue3(非插件)
  • day25|学习前端js
  • Linux: RAID(磁盘冗余阵列)配置全指南
  • 损失函数与反向传播 小土堆pytorch记录
  • FPGA-Vivado2017.4-建立AXI4用于单片机与FPGA之间数据互通
  • 计算机组成原理(9) - 整数的乘除法运算
  • js计算两个经纬度之间的角度