9. ADC 采集
📝本节您将了解 ADC(模数转换)的工作原理和关键参数,学习如何配置 MSPM0G3519 的 ADC 外设,实现模拟电压的数字化采集与换算。
🏆本章目标
1️⃣ 理解 ADC 的采样原理和分辨率概念
2️⃣ 了解 MSPM0G3519 的 ADC 外设资源
3️⃣ 通过 SysConfig 配置单通道 ADC 采集
4️⃣ 实现电压采集并通过串口输出换算结果
9.1 ADC 基本概念
ADC(Analog-to-Digital Converter,模数转换器)将连续的模拟电压信号转换为离散的数字值,供 MCU 进行处理和计算。
现实世界的物理量(温度、光照、压力等)通常是模拟信号,传感器将其转换为模拟电压后,需要 ADC 将电压值数字化才能被程序处理。
数字化过程
ADC 将模拟电压转换为数字值的过程可以分为三步:
- 采样(Sampling):以固定时间间隔对模拟电压进行"拍照",截取瞬时电压值
- 量化(Quantization):将采样到的电压值映射到最接近的离散数字等级
- 编码(Encoding):将量化后的等级表示为二进制数字,供 MCU 读取
类比理解
ADC 的工作原理,和一支数字体温计一模一样:
- 你每隔 10 秒测一次体温 → 采样(定时测量)
- 体温计只能显示到小数点后一位,36.784°C 被归为 36.7°C → 量化(归到最近的刻度)
- 屏幕上显示出数字 "36.7" → 编码(变成数字)
假设体温计只有 10 个档位:0°C=35.0、1°C=35.5……这样就能用 0~9 的数字表示温度了。MSPM0 的 ADC 有 4096 个档位(12-bit),把 0~3.3V 的电压归到这 4096 个刻度上,每格约 0.8mV。

核心参数
| 参数 | 说明 |
|---|---|
| 分辨率 | ADC 输出的数字位数。12-bit ADC 输出范围 0~4095 |
| 参考电压(Vref) | ADC 量程的上限电压。通常为 VDD(3.3V) |
| 采样率 | 每秒完成转换的次数(SPS,Samples Per Second) |
| 量化精度 | 最小可分辨电压 = Vref / 2^n |
电压换算公式
实际电压 = ADC原始值 × Vref / (2^分辨率 - 1)对于 12-bit ADC,Vref = 3.3V:
实际电压 = ADC值 × 3.3 / 4095| ADC 原始值 | 对应电压 |
|---|---|
| 0 | 0V |
| 2048 | ≈ 1.65V |
| 4095 | 3.3V |
量化精度 = 3.3V / 4095 ≈ 0.806mV(即最小可分辨约 0.8mV 的电压变化)。
9.2 MSPM0G3519 ADC 资源
MSPM0G3519 内置两个 12-bit SAR ADC 模块:
| ADC 模块 | 分辨率 | 通道数 | 说明 |
|---|---|---|---|
| ADC0 | 12-bit | 多通道 | 主 ADC 模块 |
| ADC1 | 12-bit | 多通道 | 辅助 ADC 模块 |
支持特性:
- 12-bit 分辨率(0~4095)
- 多种触发源:软件触发、定时器触发、事件触发
- 单次转换和连续转换模式
- 多通道自动扫描
- 内置温度传感器通道
- 支持 DMA 自动数据搬运
参考电压
| 参考源 | 电压 | 说明 |
|---|---|---|
| VDDA | 3.3V | 芯片模拟供电电压,默认参考 |
| 内部参考 | 1.4V / 2.5V | 内部精密参考,精度更高 |
| 外部参考 | 用户提供 | 通过 VREF 引脚输入 |
本教程使用 VDDA(3.3V)作为参考电压。
9.3 SysConfig 配置 ADC
9.3.1 添加 ADC 外设
- 打开
.syscfg文件 - 左侧面板找到 ADC12 选项卡
- 点击 ADD 添加 ADC 实例
9.3.2 配置参数
| 参数 | 设置值 | 说明 |
|---|---|---|
| Name | ADC12_0 | 实例名称 |
| ADC12 Peripheral | ADC0 | 选择 ADC0 |
| Conversion Resolution | 12 Bits | 12 位分辨率 |
| Trigger Source | Software | 软件触发转换 |
| Conversion Mode | Single | 单次转换模式 |
| Sample Period Source | Sampling Timer 0 | 采样周期来源,对应采样时间 0 |
| Desired Sample Time 0 | 125 us | 采样保持时间,值越大越稳定但转换越慢 |
| VREFP / VREFM | VDDA / VSSA | 参考电压正/负端(默认值,即 3.3V) |
| Power Down Mode | Manual(默认) | 转换完成后自动掉电,需代码重新使能 |
9.3.3 配置通道
| 参数 | 设置值 | 说明 |
|---|---|---|
| Channel | Channel2(ADC0 通道 2,对应 PA25) | 选择对应的模拟输入通道 |
| Input Pin | PA25 | 空闲模拟引脚(SysConfig 自动过滤不可用 GPIO) |
9.3.4 配置中断(必选)
必选
本教程三个实验(9.5 和 9.6)均通过中断获取转换结果,此步必须勾选。若未勾选,代码中的中断服务函数和 NVIC_EnableIRQ 将无法编译。
| 参数 | 设置值 |
|---|---|
| Interrupt Enable | Enable |
| MEM0 Result Loaded Interrupt | Enable |
保存并编译。此时编译烧录的只是模板默认代码,仅用于验证配置无误,后续实验会替换 main.c。
引脚注意
ADC 输入引脚必须是具有模拟功能的引脚。不是所有 GPIO 都支持 ADC 功能,请参考 MSPM0G3519 数据手册中的引脚复用表。SysConfig 会自动过滤不支持的引脚。


9.4 ADC 相关函数
常用 ADC DriverLib 函数
启动 ADC 转换:
void DL_ADC12_startConversion(ADC12_Regs *adc);触发一次 ADC 转换(软件触发模式下)。
检查转换是否完成:
需要先使能外设级中断(不开 NVIC):
void DL_ADC12_enableInterrupt(ADC12_Regs *adc, uint32_t interruptMask);DL_ADC12_enableInterrupt(ADC12_0_INST, DL_ADC12_INTERRUPT_MEM0_RESULT_LOADED);然后通过轮询中断索引判断转换是否完成:
DL_ADC12_IIDX DL_ADC12_getPendingInterrupt(const ADC12_Regs *adc);while (DL_ADC12_getPendingInterrupt(ADC12_0_INST) != DL_ADC12_IIDX_MEM0_RESULT_LOADED);读取转换结果:
uint16_t DL_ADC12_getMemResult(const ADC12_Regs *adc, DL_ADC12_MEM_IDX idx);返回 12-bit 原始 ADC 值(0~4095)。
使能/禁止 ADC 转换:
void DL_ADC12_enableConversions(ADC12_Regs *adc);
void DL_ADC12_disableConversions(ADC12_Regs *adc);2
最小 ADC 采集流程:
SYSCFG_DL_init(); // 1. 初始化(含 ADC 配置)
DL_ADC12_startConversion(ADC12_0_INST); // 2. 启动转换
// 等待转换完成(中断或轮询中断标志)
result = DL_ADC12_getMemResult(ADC12_0_INST, DL_ADC12_MEM_IDX_0); // 3. 读取结果
DL_ADC12_enableConversions(ADC12_0_INST); // 4. 重新使能,准备下次转换2
3
4
5
9.5 实验一:单通道电压采集
三个实验共用配置
本节(9.5.1、9.5.2)和 9.6 共三个实验,全部复用 9.3 配置好的同一个工程(ADC12_0 + PA25 + UART_0),SysConfig 只需配置这一次,后续不再改动。
每个实验的操作流程都一样:
- 替换
main.c中的全部代码(下文代码块会完整给出) - 编译 → 烧录 → 串口验证
推荐做法:实验一完成后,在 CCS 中右键工程 → Copy → Paste 复制一份,改名为 adc_voltmeter,再替换其中的 main.c,这样每个实验各留一份完整工程,方便回看。
前置依赖
本实验的电压值通过串口输出,需要在 .syscfg 中同时配置 ADC12 和 UART(UART 配置参考第 5 章串口通信)。
硬件接线
本实验使用 PA25 作为 ADC 输入引脚(该引脚为排针引出,非板载外设)。用杜邦线连接:
- 测试 0V:PA25 接 GND → 串口显示约
0.000 V - 测试 3.3V:PA25 接 3.3V → 串口显示约
3.300 V - 测试可调电压:PA25 接电位器(10kΩ)中点,电位器两端接 3.3V 和 GND
严禁超压
PA25 输入电压严禁超过 3.3V(如误接 5V 或面包板电源),超出会损坏 ADC 引脚。
9.5.1 软件触发 + 标志位轮询
最基础的 ADC 使用方式:软件触发转换,中断通知完成(通过标志位轮询等待),读取结果。
以下代码完整替换 main.c 的全部内容,工程里其他文件(SysConfig 生成的 .c/.h、targetConfig 等)一律不动。
#include "ti_msp_dl_config.h"
#include <stdio.h>
volatile bool gCheckADC;
volatile uint16_t gAdcResult;
void uart_send_string(const char *str)
{
while (*str) {
while (DL_UART_Main_isBusy(UART_0_INST));
DL_UART_Main_transmitData(UART_0_INST, *str++);
}
}
int main(void)
{
SYSCFG_DL_init();
NVIC_EnableIRQ(ADC12_0_INST_INT_IRQN);
gCheckADC = false;
char buf[64];
while (1) {
DL_ADC12_startConversion(ADC12_0_INST);
// 轮询等待中断标志
while (!gCheckADC);
// 读取结果
gAdcResult = DL_ADC12_getMemResult(ADC12_0_INST, DL_ADC12_MEM_IDX_0);
gCheckADC = false;
// 每次转换后需重新使能(SDK Manual 掉电模式要求)
DL_ADC12_enableConversions(ADC12_0_INST);
// 换算为电压(mV)
uint32_t voltage_mv = (uint32_t)gAdcResult * 3300 / 4095;
// 通过串口输出
sprintf(buf, "ADC: %u, Voltage: %lu.%03lu V\r\n",
gAdcResult, voltage_mv / 1000, voltage_mv % 1000);
uart_send_string(buf);
delay_cycles(16000000); // 500ms 采集一次
}
}
void ADC12_0_INST_IRQHandler(void)
{
switch (DL_ADC12_getPendingInterrupt(ADC12_0_INST)) {
case DL_ADC12_IIDX_MEM0_RESULT_LOADED:
gCheckADC = true;
break;
default:
break;
}
}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
电压换算说明
使用整数运算避免浮点数:
adc_raw * 3300 / 4095得到毫伏值- 输出时将毫伏值拆分为整数部分和小数部分
避免浮点的原因:Cortex-M0+ 没有硬件浮点单元(FPU),浮点运算由软件模拟,效率低且代码体积大。
9.5.2 中断方式(低功耗等待)
更高效的方式:启动转换后 CPU 进入低功耗睡眠(__WFI),转换完成后由中断唤醒并通知。
本实验与 9.5.1 共用同一份 SysConfig 配置,仅将主循环的"忙等待"改为 __WFI() 低功耗等待。以下代码完整替换 main.c 的全部内容。
#include "ti_msp_dl_config.h"
#include <stdio.h>
volatile bool gCheckADC;
volatile uint16_t gAdcResult;
void uart_send_string(const char *str)
{
while (*str) {
while (DL_UART_Main_isBusy(UART_0_INST));
DL_UART_Main_transmitData(UART_0_INST, *str++);
}
}
int main(void)
{
SYSCFG_DL_init();
NVIC_EnableIRQ(ADC12_0_INST_INT_IRQN);
gCheckADC = false;
char buf[64];
while (1) {
DL_ADC12_startConversion(ADC12_0_INST);
// __WFI 进入低功耗睡眠,中断到来时自动唤醒
while (!gCheckADC) {
__WFI();
}
// 读取结果
gAdcResult = DL_ADC12_getMemResult(ADC12_0_INST, DL_ADC12_MEM_IDX_0);
gCheckADC = false;
DL_ADC12_enableConversions(ADC12_0_INST);
uint32_t voltage_mv = (uint32_t)gAdcResult * 3300 / 4095;
sprintf(buf, "ADC: %u, Voltage: %lu.%03lu V\r\n",
gAdcResult, voltage_mv / 1000, voltage_mv % 1000);
uart_send_string(buf);
delay_cycles(16000000);
}
}
void ADC12_0_INST_IRQHandler(void)
{
switch (DL_ADC12_getPendingInterrupt(ADC12_0_INST)) {
case DL_ADC12_IIDX_MEM0_RESULT_LOADED:
gCheckADC = true;
break;
default:
break;
}
}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
9.6 实验二:简易电压表
结合 ADC 采集和串口输出,实现一个简易电压表:持续采集并以格式化方式显示电压值。
本实验不改变接线和外设配置,仍复用 9.3 的 SysConfig 配置,仅在 9.5 的基础上新增均值滤波,降低读数抖动。以下代码完整替换 main.c 的全部内容。
#include "ti_msp_dl_config.h"
#include <stdio.h>
volatile bool gCheckADC;
volatile uint16_t gAdcResult;
void uart_send_string(const char *str)
{
while (*str) {
while (DL_UART_Main_isBusy(UART_0_INST));
DL_UART_Main_transmitData(UART_0_INST, *str++);
}
}
// 单次 ADC 采集(中断 + 标志位等待)
uint16_t adc_read_once(void)
{
DL_ADC12_startConversion(ADC12_0_INST);
while (!gCheckADC);
gAdcResult = DL_ADC12_getMemResult(ADC12_0_INST, DL_ADC12_MEM_IDX_0);
gCheckADC = false;
DL_ADC12_enableConversions(ADC12_0_INST);
return gAdcResult;
}
// 简单均值滤波(采集 N 次取平均)
uint16_t adc_read_average(uint8_t samples)
{
uint32_t sum = 0;
for (uint8_t i = 0; i < samples; i++) {
sum += adc_read_once();
}
return (uint16_t)(sum / samples);
}
int main(void)
{
SYSCFG_DL_init();
NVIC_EnableIRQ(ADC12_0_INST_INT_IRQN);
gCheckADC = false;
char buf[64];
uart_send_string("=== Tianqiaoxing Voltmeter ===\r\n");
while (1) {
// 采集 16 次取平均,降低噪声
uint16_t adc_avg = adc_read_average(16);
uint32_t voltage_mv = (uint32_t)adc_avg * 3300 / 4095;
sprintf(buf, " Voltage: %lu.%03lu V (ADC raw: %u)\r\n",
voltage_mv / 1000, voltage_mv % 1000, adc_avg);
uart_send_string(buf);
delay_cycles(16000000); // 500ms 更新一次
}
}
void ADC12_0_INST_IRQHandler(void)
{
switch (DL_ADC12_getPendingInterrupt(ADC12_0_INST)) {
case DL_ADC12_IIDX_MEM0_RESULT_LOADED:
gCheckADC = true;
break;
default:
break;
}
}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
软件滤波
ADC 原始值通常存在一定噪声波动。简单的均值滤波(多次采样取平均)可以有效降低随机噪声。采样次数越多滤波效果越好,但响应速度越慢。
常用滤波方式:
- 均值滤波:N 次采样取平均,简单有效
- 中值滤波:N 次采样取中间值,抗脉冲干扰
- 滑动平均:保持最近 N 次采样的滑动窗口平均值
9.7 ADC 使用注意事项
注意
输入电压不能超过 Vref(3.3V):超压可能损坏 ADC 引脚。如需测量高于 3.3V 的电压,需外接分压电阻。
模拟输入阻抗:ADC 采样电容需要足够时间充电。如果外部信号源阻抗过高(>10kΩ),需增加采样保持时间或加缓冲运放。
噪声隔离:模拟引脚尽量远离数字信号走线。PCB 布局时注意模拟地和数字地的处理。
单次 vs 连续模式:
- 单次模式:每次需手动触发,适合低频采样
- 连续模式:自动重复采样,适合高频采样或配合 DMA
分压电阻计算(测量高于 3.3V 的电压):
V_adc = V_in × R2 / (R1 + R2) 例如:测量 0~12V,选 R1=27kΩ, R2=10kΩ V_adc_max = 12 × 10 / (27+10) = 3.24V < 3.3V ✓1
2
3
4
9.8 知识总结
| 内容 | 说明 |
|---|---|
| ADC 功能 | 模拟电压 → 数字值 |
| 分辨率 | 12-bit(0~4095) |
| 参考电压 | VDDA = 3.3V |
| 量化精度 | 约 0.8mV |
| 电压换算 | V = ADC × 3300 / 4095(mV) |
| 触发方式 | 软件触发(本章演示)/ 定时器触发 / 事件触发 |
| 降噪 | 多次采样取平均 |
掌握 ADC 后,你可以测量各种模拟传感器的输出(温度、光照、电位器、电池电压等)。下一章将学习 DAC 输出——ADC 的逆操作,将数字值转换为模拟电压。