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_ANALOG
(HAL_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()
配Rank
和SamplingTime
。 -
注入组 :有单独的序列和中断,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 才会按你的模式工作。
常见字段(都会用到):
-
DataAlign
:ADC_DATAALIGN_RIGHT / LEFT
- 右对齐 常用(12 位结果在低位,便于直接
0..4095
取值)。
- 右对齐 常用(12 位结果在低位,便于直接
-
ScanConvMode
:ADC_SCAN_DISABLE / ENABLE
- 单通道关、多通道开(配合
NbrOfConversion
和各Rank
)。
- 单通道关、多通道开(配合
-
ContinuousConvMode
:DISABLE / ENABLE
- 单次转换 or 连续不断转换(连续采样时一般配 DMA)。
-
NbrOfConversion
:1..16
- 规则组里一共采多少路(多通道时要填总数)。
-
ExternalTrigConv
:ADC_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 寄存器。
关键参数:
-
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 路):
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。"
调用顺序:
-
HAL_ADC_Start(&hadc)
- 触发规则组开始转换(若是外部触发模式,这里只做使能等待外部事件)。
-
HAL_ADC_PollForConversion(&hadc, timeout_ms)
- 轮询EOC标志直到完成或超时。
-
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=ENABLE
,NbrOfConversion=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 ≤ 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
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):
-
时钟与引脚:
-
使能
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 时把"源"当"外设端")才会启用。
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
。