AT32 ADC — 读取内部参考电压,反算供电电压(ADC+DMA 入门)

AT32F403A ADC 读取内部 Vrefint 参考电压,配合 DMA 自动搬运,反算实际 VDDA 供电电压

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。这个电压不随外部供电变化,是芯片出厂时校准好的。

本实验的思路

  1. 用 ADC 采集这个内部 1.2V 参考电压
  2. ADC 采样值会随实际供电电压(VDDA)变化
  3. 反过来,知道了 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 芯片。

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计