ADC — 读取内部参考电压
难度: ⭐⭐(需要了解 ADC 和 DMA 概念)
固件库: AT32F403A_407_Firmware_Library V2.2.2
源文件: project/at_start_f403a/examples/adc/current_vref_value_check/src/main.c
这个实验是干嘛的?
每个 MCU 芯片内部都有一个精确的参考电压源(Vrefint),典型值 1.2V。这个电压不随外部供电变化,是芯片出厂时校准好的。
本实验的思路:
- 用 ADC 采集这个内部 1.2V 参考电压
- ADC 采样值会随实际供电电压(VDDA)变化
- 反过来,知道了 ADC 采样值和参考电压,就能算出实际 VDDA
公式:
1
|
VDDA = 1.2V × 4095 / ADC采样值
|
有什么用?
- 电池供电的设备:不用外部电压表就能监测电池电量
- 电源质量检测:发现供电不稳
- ADC 精度校准:知道实际 VDDA 后,其他 ADC 测量更准
硬件准备
不需要任何外部硬件! 内部参考电压是芯片自带的。
你只需要:
- AT32F403A 开发板
- USB 转串口模块(查看 printf 输出)
涉及的知识点
这个实验比较"硬核",同时用到了 ADC 和 DMA 两个外设。先搞懂概念:
ADC(模数转换器)
ADC 把模拟电压转换成数字值。AT32F403A 的 ADC 是 12 位的,意味着:
- 输入 0V → 输出 0
- 输入 VDDA(3.3V)→ 输出 4095(2^12 - 1)
- 输入 1.2V → 输出约 1489(1.2 / 3.3 × 4095)
DMA(直接存储器访问)
ADC 采样完成后,数据存在 ADC 寄存器里。你可以:
- 不用 DMA:CPU 自己去读寄存器(轮询或中断)
- 用 DMA:DMA 硬件自动把数据搬到内存变量,CPU 完全不参与
本实验用 DMA,因为更高效——ADC 每次采样完,DMA 自动把结果搬到 adc1_ordinary_value 变量里。
完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
#include "at32f403a_407_board.h"
#include "at32f403a_407_clock.h"
/* DMA 搬运的目标变量,ADC 采样结果会自动存到这里 */
__IO uint16_t adc1_ordinary_value = 0;
/* ========== DMA 配置 ========== */
static void dma_config(void)
{
dma_init_type dma_init_struct;
/* 使能 DMA1 时钟 */
crm_periph_clock_enable(CRM_DMA1_PERIPH_CLOCK, TRUE);
dma_reset(DMA1_CHANNEL1);
dma_default_para_init(&dma_init_struct);
/* 数据搬运方向:从外设(ADC)到内存 */
dma_init_struct.direction = DMA_DIR_PERIPHERAL_TO_MEMORY;
/* 源地址:ADC 数据寄存器 */
dma_init_struct.peripheral_base_addr = (uint32_t)&(ADC1->odt);
dma_init_struct.peripheral_data_width = DMA_PERIPHERAL_DATA_WIDTH_HALFWORD; // 16位
dma_init_struct.peripheral_inc_enable = FALSE; // 源地址不递增(始终读同一个寄存器)
/* 目标地址:我们的变量 */
dma_init_struct.memory_base_addr = (uint32_t)&adc1_ordinary_value;
dma_init_struct.memory_data_width = DMA_MEMORY_DATA_WIDTH_HALFWORD; // 16位
dma_init_struct.memory_inc_enable = FALSE; // 目标地址不递增(只存一个值)
dma_init_struct.buffer_size = 1; // 每次搬运 1 个数据
dma_init_struct.priority = DMA_PRIORITY_HIGH; // 高优先级
dma_init_struct.loop_mode_enable = TRUE; // 循环模式(反复搬运)
dma_init(DMA1_CHANNEL1, &dma_init_struct);
}
/* ========== ADC 配置 ========== */
static void adc_config(void)
{
adc_base_config_type adc_base_struct;
/* 使能 ADC1 时钟 */
crm_periph_clock_enable(CRM_ADC1_PERIPH_CLOCK, TRUE);
adc_reset(ADC1);
/* ADC 时钟分频:系统时钟 / 4 */
crm_adc_clock_div_set(CRM_ADC_DIV_4);
adc_base_default_para_init(&adc_base_struct);
/* 独立模式(不与其他 ADC 联动) */
adc_combine_mode_select(ADC_INDEPENDENT_MODE);
/* ADC 基础配置 */
adc_base_struct.sequence_mode = FALSE; // 非序列模式(只采一个通道)
adc_base_struct.repeat_mode = FALSE; // 不重复
adc_base_struct.data_align = ADC_RIGHT_ALIGNMENT; // 右对齐
adc_base_struct.ordinary_channel_length = 1; // 只采 1 个通道
adc_base_config(ADC1, &adc_base_struct);
/* 选择通道 17 = 内部参考电压 Vrefint */
adc_ordinary_channel_set(ADC1, ADC_CHANNEL_17, 1, ADC_SAMPLETIME_239_5);
/* 软件触发(不用外部触发源) */
adc_ordinary_conversion_trigger_set(ADC1, ADC12_ORDINARY_TRIG_SOFTWARE, TRUE);
/* 使能 ADC 的 DMA 请求 */
adc_dma_mode_enable(ADC1, TRUE);
/* 使能内部温度传感器和参考电压 */
adc_tempersensor_vintrv_enable(TRUE);
/* 使能 ADC */
adc_enable(ADC1, TRUE);
/* ADC 校准(必须!消除工艺偏差) */
adc_calibration_init(ADC1);
while(adc_calibration_init_status_get(ADC1)); // 等待校准初始化完成
adc_calibration_start(ADC1);
while(adc_calibration_status_get(ADC1)); // 等待校准完成
}
/* ========== 主函数 ========== */
int main(void)
{
nvic_priority_group_config(NVIC_PRIORITY_GROUP_4);
system_clock_config();
at32_board_init();
/* 关闭所有 LED */
at32_led_off(LED2);
at32_led_off(LED3);
at32_led_off(LED4);
/* 初始化串口(用于 printf 输出) */
uart_print_init(115200);
/* 初始化 DMA 和 ADC */
dma_config();
adc_config();
/* 使能 DMA 通道(必须在 ADC 使能之后!) */
dma_channel_enable(DMA1_CHANNEL1, TRUE);
printf("adc1_vref_check \r\n");
while(1)
{
at32_led_toggle(LED2);
delay_sec(1);
/* 软件触发一次 ADC 转换 */
adc_ordinary_software_trigger_enable(ADC1, TRUE);
/* 等待 DMA 搬运完成(转换结束标志) */
while(dma_flag_get(DMA1_FDT1_FLAG) == RESET);
dma_flag_clear(DMA1_FDT1_FLAG);
/* 计算并打印 VDDA 电压 */
printf("vref_value = %f V\r\n", ((double)1.2 * 4095) / adc1_ordinary_value);
}
}
|
代码分段讲解
DMA 配置详解
1
|
dma_init_struct.direction = DMA_DIR_PERIPHERAL_TO_MEMORY;
|
DMA 有 3 种方向:
PERIPHERAL_TO_MEMORY — 外设→内存(读取外设数据,本实验用这个)
MEMORY_TO_PERIPHERAL — 内存→外设(发送数据到外设)
MEMORY_TO_MEMORY — 内存→内存(数据搬运)
1
|
dma_init_struct.peripheral_base_addr = (uint32_t)&(ADC1->odt);
|
源地址是 ADC1 的数据输出寄存器(ODT),ADC 每次采样的结果存在这里。
1
|
dma_init_struct.loop_mode_enable = TRUE;
|
循环模式:DMA 搬完一次后自动重新开始。这样每次 ADC 转换完,DMA 都会自动把新数据搬过来。
ADC 配置详解
1
|
adc_ordinary_channel_set(ADC1, ADC_CHANNEL_17, 1, ADC_SAMPLETIME_239_5);
|
这行配置了 3 个东西:
- 通道 17 — 内部参考电压(Vrefint)固定映射到通道 17
- 序号 1 — 在序列中的位置(这里只采一个通道,所以是 1)
- 采样时间 239.5 个时钟周期 — 采样时间越长精度越高,但速度越慢
1
|
adc_tempersensor_vintrv_enable(TRUE);
|
这行很重要! 必须使能内部参考电压,否则通道 17 读到的是 0。
1
2
|
adc_calibration_init(ADC1);
adc_calibration_start(ADC1);
|
ADC 校准是必须的步骤。芯片制造有工艺偏差,校准可以消除这个偏差。不校准的话,采样值可能偏差几百 LSB。
主循环
1
2
3
4
|
adc_ordinary_software_trigger_enable(ADC1, TRUE); // 触发一次转换
while(dma_flag_get(DMA1_FDT1_FLAG) == RESET); // 等 DMA 搬完
dma_flag_clear(DMA1_FDT1_FLAG); // 清标志位
printf("vref_value = %f V\r\n", ((double)1.2 * 4095) / adc1_ordinary_value);
|
公式:VDDA = 1.2V × 4095 / ADC采样值
举例:如果 ADC 读到 1489,那么 VDDA = 1.2 × 4095 / 1489 ≈ 3.30V
实验现象
串口助手每秒打印一行:
1
2
3
4
5
|
adc1_vref_check
vref_value = 3.302500 V
vref_value = 3.301800 V
vref_value = 3.303200 V
...
|
- 如果供电是 3.3V,读数应该在 3.28~3.32V 附近
- 如果用 USB 供电,可能会看到 3.25~3.35V 的波动(USB 供电不太稳)
- 如果用电池供电,电压会随电量慢慢下降
常见问题
Q: 读到的电压是 0 或者不对?
- 检查是否调用了
adc_tempersensor_vintrv_enable(TRUE) — 不开这个,Vrefint 通道没连接
- 检查是否做了 ADC 校准
- 检查 DMA 是否在 ADC 使能之后才使能
Q: 数值跳动很大?
- 增加采样时间(已经用了 239.5,比较长了)
- 可以做多次采样取平均值
- 检查电源是否稳定
Q: 想读取外部引脚的电压?
- 把
ADC_CHANNEL_17 换成对应的外部通道号(如 ADC_CHANNEL_0 对应 PA0)
- 去掉
adc_tempersensor_vintrv_enable() — 这个只用于内部通道
小结
| 概念 |
说明 |
| Vrefint |
芯片内部 1.2V 精密参考电压 |
| 12 位 ADC |
输出范围 04095,对应 0VVDDA |
| DMA |
硬件自动搬运数据,不占 CPU |
| ADC 校准 |
必须做,消除工艺偏差 |
| 采样时间 |
越长精度越高,速度越慢 |
学了 ADC+DMA 能做什么?
ADC 是连接"物理世界"和"数字世界"的桥梁。温度、光照、电压、声音……所有模拟信号都要靠 ADC 来采集:
传感器数据采集
| 项目 |
原理 |
难度 |
| 温度检测 |
NTC 热敏电阻 + ADC → 查表得温度 |
⭐⭐ |
| 光照检测 |
光敏电阻 + ADC → 判断亮暗 |
⭐ |
| 电池电量 |
分压电阻 + ADC → 读电池电压 |
⭐⭐ |
| 电流检测 |
采样电阻 + 运放 + ADC → 读电流 |
⭐⭐⭐ |
| 声音采集 |
MEMS 麦克风 + ADC → 录音/声控 |
⭐⭐⭐ |
| 心率检测 |
光电传感器 + ADC → 心跳波形 |
⭐⭐⭐ |
控制与调节
| 项目 |
原理 |
难度 |
| 恒温控制 |
温度传感器 + ADC + PID + 加热器 |
⭐⭐⭐ |
| 光控灯 |
光敏电阻 + ADC + 继电器 → 天黑自动开灯 |
⭐⭐ |
| 可调电源 |
电位器 + ADC → 设定输出电压 |
⭐⭐⭐ |
| 示波器 |
高速 ADC + DMA + 显示屏 → 简易示波器 |
⭐⭐⭐⭐ |
| 功率计 |
电压 ADC + 电流 ADC → 计算功率(你正在做的 2UUL!) |
⭐⭐⭐ |
DMA 的重要性
不用 DMA 也能读 ADC,但 DMA 有这些优势:
- 不占 CPU:ADC 采样 100 万次/秒,CPU 完全不用管
- 不丢数据:轮询方式可能来不及读就被覆盖了
- 配合定时器:定时器触发 ADC,DMA 自动搬运,实现精确采样率
核心思路:ADC + DMA = “自动采集系统”。ADC 负责把模拟信号变成数字,DMA 负责搬运数据,CPU 只需要在数据准备好后处理结果。这是所有数据采集系统的基础架构。
下一步:学会了 ADC,可以学 SPI 读写 Flash,用 SPI 接口读写外部 W25Q Flash 芯片。