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_ANALOG
(HAL_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()
配Rank
和SamplingTime
。注入组:有单独的序列和中断,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 才会按你的模式工作。
常见字段(都会用到):
DataAlign
:ADC_DATAALIGN_RIGHT / LEFT
右对齐常用(12 位结果在低位,便于直接
0..4095
取值)。
ScanConvMode
:ADC_SCAN_DISABLE / ENABLE
单通道关、多通道开(配合
NbrOfConversion
和各Rank
)。
ContinuousConvMode
:DISABLE / ENABLE
单次转换 or 连续不断转换(连续采样时一般配 DMA)。
NbrOfConversion
:1..16
规则组里一共采多少路(多通道时要填总数)。
ExternalTrigConv
:ADC_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 寄存器。
关键参数:
Channel
:ADC_CHANNEL_0…17
(0..15 外部引脚;16 温度;17 参考电压)Rank
:ADC_REGULAR_RANK_1…16
(转换顺序)SamplingTime
:1.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。”
调用顺序:
HAL_ADC_Start(&hadc)
触发规则组开始转换(若是外部触发模式,这里只做使能等待外部事件)。
HAL_ADC_PollForConversion(&hadc, timeout_ms)
轮询EOC标志直到完成或超时。
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=ENABLE
,NbrOfConversion=4
依次
HAL_ADC_ConfigChannel()
把 CH0..CH3 放到 RANK1..4DMA 还是通道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 ≤ 14MHz(
RCC_ADCPCLK2_DIVx
)——太快会影响精度。连续 + DMA 循环时,数组内容持续被覆盖:要么用回调分段处理,要么主循环里注意拷贝/关中断。
单变量作为 DMA 目的时可把
MemInc=DISABLE
(否则地址会递增到未知区域)。换通道/改序列后记得重新
HAL_ADC_ConfigChannel()
,必要时HAL_ADC_Stop()
再启动。
DMA 配置要点(配合 ADC)
通道映射(F1 固定):
ADC1 → DMA1_Channel1
。Direction:
DMA_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):
时钟与引脚:
使能
ADC1
与对应 GPIO 时钟;通道引脚设为
GPIO_MODE_ANALOG
;配置
ADCCLK=PCLK2/div
(≤14 MHz)。
ADC 句柄参数:
DataAlign
(LEFT/RIGHT)ScanConvMode
(多通道=ENABLE,单通道=DISABLE)ContinuousConvMode
(连续/单次)NbrOfConversion
(规则通道数)DiscontinuousConvMode/NbrOfDiscConversion
(可将规则组拆段执行)ExternalTrigConv
(硬件触发源或软件触发)
通道与采样时间:
HAL_ADC_ConfigChannel()
设置Channel/Rank/SamplingTime
采样时间越长→输入高阻/源阻大时更稳。
启动方式:
轮询:
HAL_ADC_Start
→HAL_ADC_PollForConversion
→HAL_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),支持地址递增、循环模式、传输完成/半传输中断。
实现步骤:
开 DMA 时钟;选对通道(F1 固定映射:ADC1→DMA1_Channel1)。
配置方向/对齐/自增/模式/优先级(见上注释)。
把 DMA 句柄挂到外设句柄:
__HAL_LINKDMA(&adc_handle, DMA_Handle, dma_handle)
。启动:
轮询:
HAL_DMA_Start
+HAL_DMA_PollForTransfer
与外设:调用外设的
HAL_xxx_Start_DMA
(如HAL_ADC_Start_DMA
)
循环采样:对接收场景常用
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
Channel
:ADC_CHANNEL_0~17
(F1:0~15=外部通道;16=温度;17=Vrefint)Rank
:ADC_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->DR
、ADCx->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 初始化参数并调用 MSPHAL_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/TCHAL_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
。