跳到主要内容

模拟 I/O

模拟输入输出让 Arduino 能够处理连续变化的信号,如读取传感器数值或控制 LED 亮度。本章将详细介绍模拟 I/O 的原理和应用。

模拟 vs 数字

数字信号

  • 只有两种状态:高电平(5V)或低电平(0V)
  • 用于开关控制、数字通信
  • 抗干扰能力强

模拟信号

  • 可以在 0V 到 5V 之间连续变化
  • 用于传感器读取、精细控制
  • 更接近自然界的物理量
数字信号:    模拟信号:
│ ┌──┐ │ ╭─╮
5V ─┤ │ │ 5V ─┤ ╱ ╲
│ │ │ │ ╱ ╲
0V ─┴────┘ └── 0V ─┴─╱ ╲──
0 1 0 1 连续变化

模拟输入

Arduino Uno 有 6 个模拟输入引脚(A0-A5),使用 10 位 ADC(模数转换器),可以将 0-5V 电压转换为 0-1023 的数值。

理解 ADC 工作原理

ADC(Analog-to-Digital Converter,模数转换器)将连续的模拟信号转换为离散的数字值。理解 ADC 的工作原理有助于更好地使用模拟输入功能。

分辨率

Arduino Uno 使用 10 位 ADC,意味着可以将输入范围分成 210=10242^{10} = 1024 个离散值:

  • 输入 0V → 数字值 0
  • 输入 5V → 数字值 1023
  • 分辨率 = 5V10234.89mV\frac{5V}{1023} \approx 4.89mV

即 ADC 能分辨的最小电压变化约为 4.89mV。

转换时间

Arduino 的 ADC 完成一次转换约需 100 微秒。这意味着理论上每秒可以进行约 10000 次采样,但实际应用中建议采样率更低以确保稳定性。

ADC 精度的影响因素

实际使用中,ADC 的精度会受到以下因素影响:

  1. 参考电压稳定性:参考电压的波动直接影响测量精度
  2. 电源噪声:电源中的噪声会耦合到 ADC 输入
  3. 输入阻抗:高阻抗信号源可能导致测量误差
  4. 温度漂移:芯片温度变化会影响 ADC 精度

analogRead() 函数

读取模拟引脚的电压值:

int value = analogRead(pin);
  • pin:模拟引脚编号(A0-A5,也可以直接用数字 0-5)
  • 返回值:0-1023(对应 0V-5V)

电压换算公式

Vin=ADC1023×VrefV_{in} = \frac{ADC值}{1023} \times V_{ref}

其中 VrefV_{ref} 是参考电压,默认为 5V。

参考电压选择

使用 analogReference() 函数可以改变 ADC 的参考电压,从而改变测量范围和精度:

analogReference(type);

Arduino Uno 支持的参考电压

类型说明测量范围
DEFAULT默认 5V0-5V
INTERNAL内部 1.1V 参考源0-1.1V
EXTERNALAREF 引脚输入的电压0-AREF 电压

使用内部参考电压提高精度

当测量小电压信号时,使用内部 1.1V 参考可以提高分辨率:

void setup() {
Serial.begin(9600);

// 设置内部 1.1V 参考电压
analogReference(INTERNAL);
}

void loop() {
int rawValue = analogRead(A0);

// 使用 1.1V 参考,分辨率约为 1.07mV
float voltage = rawValue * (1.1 / 1023.0);

Serial.print("电压: ");
Serial.print(voltage * 1000); // 转换为 mV
Serial.println(" mV");

delay(500);
}
重要警告

如果使用 EXTERNAL 参考模式,必须在调用 analogRead() 之前设置,否则可能会损坏芯片。外部参考电压不能超过 5V,且不能为负电压。

使用外部参考电压

当需要精确测量特定范围电压时,可以使用外部参考:

AREF 引脚 ── 稳定的参考电压源(如 2.5V)
void setup() {
// 先设置参考类型,再读取
analogReference(EXTERNAL);
Serial.begin(9600);
}

void loop() {
int rawValue = analogRead(A0);

// 假设外部参考电压为 2.5V
float voltage = rawValue * (2.5 / 1023.0);

Serial.print("电压: ");
Serial.print(voltage);
Serial.println(" V");

delay(500);
}

读取电位器

电位器(可变电阻)是最基础的模拟输入器件:

硬件连接

电位器引脚 1 ── 5V
电位器引脚 2 ── A0(中间引脚,信号输出)
电位器引脚 3 ── GND

代码

const int POT_PIN = A0;

void setup() {
Serial.begin(9600);
}

void loop() {
int rawValue = analogRead(POT_PIN);
float voltage = rawValue * (5.0 / 1023.0);

Serial.print("原始值: ");
Serial.print(rawValue);
Serial.print(" | 电压: ");
Serial.print(voltage);
Serial.println(" V");

delay(100);
}

光敏电阻读取光线

光敏电阻(LDR)的阻值随光线强度变化:

硬件连接

5V ── LDR ── A0 ── 10kΩ 电阻 ── GND

代码

const int LDR_PIN = A0;

void setup() {
Serial.begin(9600);
}

void loop() {
int lightValue = analogRead(LDR_PIN);

// 将读数映射为亮度等级
int brightnessLevel = map(lightValue, 0, 1023, 0, 100);

Serial.print("光线强度: ");
Serial.print(brightnessLevel);
Serial.println("%");

delay(500);
}

读取温度传感器(LM35)

LM35 温度传感器输出电压与温度成正比(10mV/°C):

硬件连接

LM35 VCC ── 5V
LM35 OUT ── A0
LM35 GND ── GND

代码

const int TEMP_PIN = A0;

void setup() {
Serial.begin(9600);
}

void loop() {
int reading = analogRead(TEMP_PIN);

// 转换为电压
float voltage = reading * 5.0 / 1023.0;

// LM35: 10mV = 1°C
float temperature = voltage * 100;

Serial.print("温度: ");
Serial.print(temperature);
Serial.println(" °C");

delay(1000);
}

模拟输出(PWM)

Arduino 使用 PWM(脉冲宽度调制)模拟模拟输出。通过快速切换高低电平,改变平均电压。

PWM 原理

占空比 25%:     占空比 50%:     占空比 75%:
┌┐ ┌──┐ ┌───┐
││ │ │ │ │
│└──────── │ └──── │ └──
│ │ │
└─────────── └─────────── └────────
低亮度 中亮度 高亮度

analogWrite() 函数

输出 PWM 信号:

analogWrite(pin, value);
  • pin:PWM 引脚(D3、D5、D6、D9、D10、D11)
  • value:0-255(0 表示完全关闭,255 表示完全开启)

LED 亮度控制

硬件连接

Arduino D9 (PWM) ── LED 正极
└── 220Ω 电阻 ── GND

代码

const int LED_PIN = 9;

void setup() {
// PWM 引脚不需要 pinMode 设置
}

void loop() {
// 渐亮
for (int i = 0; i <= 255; i++) {
analogWrite(LED_PIN, i);
delay(10);
}

// 渐暗
for (int i = 255; i >= 0; i--) {
analogWrite(LED_PIN, i);
delay(10);
}
}

电位器控制 LED 亮度

结合模拟输入和 PWM 输出:

const int POT_PIN = A0;
const int LED_PIN = 9;

void setup() {
Serial.begin(9600);
}

void loop() {
// 读取电位器值(0-1023)
int potValue = analogRead(POT_PIN);

// 映射到 PWM 范围(0-255)
int brightness = map(potValue, 0, 1023, 0, 255);

// 设置 LED 亮度
analogWrite(LED_PIN, brightness);

Serial.print("电位器: ");
Serial.print(potValue);
Serial.print(" | 亮度: ");
Serial.println(brightness);

delay(50);
}

控制舵机

舵机使用 PWM 信号控制角度(通常 0-180°):

#include <Servo.h>

Servo myServo;
const int SERVO_PIN = 9;
const int POT_PIN = A0;

void setup() {
myServo.attach(SERVO_PIN);
Serial.begin(9600);
}

void loop() {
int potValue = analogRead(POT_PIN);

// 将 0-1023 映射到 0-180 度
int angle = map(potValue, 0, 1023, 0, 180);

myServo.write(angle);

Serial.print("角度: ");
Serial.println(angle);

delay(15); // 给舵机足够时间转动
}

综合示例:自动夜灯

根据环境光线自动调节 LED 亮度:

const int LDR_PIN = A0;
const int LED_PIN = 9;

// 校准值(根据实际环境调整)
const int DARK_THRESHOLD = 300; // 低于此值认为是黑暗
const int BRIGHT_THRESHOLD = 800; // 高于此值认为是明亮

void setup() {
Serial.begin(9600);
}

void loop() {
int lightLevel = analogRead(LDR_PIN);

// 根据光线强度计算 LED 亮度
// 环境越暗,LED 越亮
int ledBrightness;

if (lightLevel <= DARK_THRESHOLD) {
ledBrightness = 255; // 全亮
} else if (lightLevel >= BRIGHT_THRESHOLD) {
ledBrightness = 0; // 关闭
} else {
// 线性映射
ledBrightness = map(lightLevel, DARK_THRESHOLD, BRIGHT_THRESHOLD, 255, 0);
}

analogWrite(LED_PIN, ledBrightness);

Serial.print("光线: ");
Serial.print(lightLevel);
Serial.print(" | LED亮度: ");
Serial.println(ledBrightness);

delay(100);
}

综合示例:简易温度计

使用 LM35 和多个 LED 显示温度范围:

const int TEMP_PIN = A0;
const int LED_COLD = 8; // 蓝色 LED - 低温
const int LED_NORM = 9; // 绿色 LED - 正常
const int LED_HOT = 10; // 红色 LED - 高温

void setup() {
Serial.begin(9600);
pinMode(LED_COLD, OUTPUT);
pinMode(LED_NORM, OUTPUT);
pinMode(LED_HOT, OUTPUT);
}

void loop() {
float temperature = readTemperature();

// 根据温度点亮不同 LED
if (temperature < 20) {
digitalWrite(LED_COLD, HIGH);
digitalWrite(LED_NORM, LOW);
digitalWrite(LED_HOT, LOW);
} else if (temperature < 30) {
digitalWrite(LED_COLD, LOW);
digitalWrite(LED_NORM, HIGH);
digitalWrite(LED_HOT, LOW);
} else {
digitalWrite(LED_COLD, LOW);
digitalWrite(LED_NORM, LOW);
digitalWrite(LED_HOT, HIGH);
}

Serial.print("温度: ");
Serial.print(temperature);
Serial.println(" °C");

delay(1000);
}

float readTemperature() {
int reading = analogRead(TEMP_PIN);
float voltage = reading * 5.0 / 1023.0;
return voltage * 100;
}

高级技巧

平滑传感器读数

使用移动平均减少传感器噪声:

简单移动平均

const int SENSOR_PIN = A0;
const int NUM_SAMPLES = 10;

void setup() {
Serial.begin(9600);
}

void loop() {
int average = getSmoothedReading();

Serial.print("平滑值: ");
Serial.println(average);

delay(100);
}

int getSmoothedReading() {
long sum = 0;
for (int i = 0; i < NUM_SAMPLES; i++) {
sum += analogRead(SENSOR_PIN);
delayMicroseconds(100); // 短暂延时让 ADC 稳定
}
return sum / NUM_SAMPLES;
}

环形缓冲区平均

更高效的平滑方法,不需要每次重新采集所有样本:

const int SENSOR_PIN = A0;
const int BUFFER_SIZE = 10;

int buffer[BUFFER_SIZE];
int bufferIndex = 0;
bool bufferFilled = false;

void setup() {
Serial.begin(9600);

// 初始化缓冲区
for (int i = 0; i < BUFFER_SIZE; i++) {
buffer[i] = analogRead(SENSOR_PIN);
}
}

void loop() {
// 添加新读数到缓冲区
buffer[bufferIndex] = analogRead(SENSOR_PIN);
bufferIndex = (bufferIndex + 1) % BUFFER_SIZE;

// 计算平均值
long sum = 0;
for (int i = 0; i < BUFFER_SIZE; i++) {
sum += buffer[i];
}
int average = sum / BUFFER_SIZE;

Serial.print("当前值: ");
Serial.print(buffer[(bufferIndex - 1 + BUFFER_SIZE) % BUFFER_SIZE]);
Serial.print(" | 平滑值: ");
Serial.println(average);

delay(100);
}

指数移动平均

权重偏向最新数据的平滑方法,响应更快:

const int SENSOR_PIN = A0;
const float ALPHA = 0.1; // 平滑系数(0-1,越小越平滑)

float smoothedValue = 0;

void setup() {
Serial.begin(9600);
smoothedValue = analogRead(SENSOR_PIN); // 初始化为第一个读数
}

void loop() {
int rawValue = analogRead(SENSOR_PIN);

// 指数移动平均公式
smoothedValue = ALPHA * rawValue + (1 - ALPHA) * smoothedValue;

Serial.print("原始值: ");
Serial.print(rawValue);
Serial.print(" | 平滑值: ");
Serial.println((int)smoothedValue);

delay(50);
}

提高测量精度的方法

过采样技术

通过多次采样平均可以提高有效分辨率。每增加 1 位分辨率需要 4 次采样:

  • 增加 1 位:41=44^1 = 4 次采样
  • 增加 2 位:42=164^2 = 16 次采样
  • 增加 3 位:43=644^3 = 64 次采样
// 通过 16 次过采样,将 10 位 ADC 提升到 12 位
unsigned int readADC12Bit(int pin) {
unsigned long sum = 0;

for (int i = 0; i < 16; i++) {
sum += analogRead(pin);
}

// 右移 2 位(除以 4),得到 12 位结果
return sum >> 2; // 范围 0-4095
}

void setup() {
Serial.begin(9600);
}

void loop() {
unsigned int value = readADC12Bit(A0);

// 12 位分辨率:5V / 4095 ≈ 1.22mV
float voltage = value * (5.0 / 4095.0);

Serial.print("12位值: ");
Serial.print(value);
Serial.print(" | 电压: ");
Serial.print(voltage * 1000);
Serial.println(" mV");

delay(500);
}

去除异常值

使用中位数滤波去除突发噪声:

const int SENSOR_PIN = A0;
const int NUM_SAMPLES = 5;

int medianFilter() {
int samples[NUM_SAMPLES];

// 采集样本
for (int i = 0; i < NUM_SAMPLES; i++) {
samples[i] = analogRead(SENSOR_PIN);
delayMicroseconds(100);
}

// 冒泡排序
for (int i = 0; i < NUM_SAMPLES - 1; i++) {
for (int j = 0; j < NUM_SAMPLES - i - 1; j++) {
if (samples[j] > samples[j + 1]) {
int temp = samples[j];
samples[j] = samples[j + 1];
samples[j + 1] = temp;
}
}
}

// 返回中位数
return samples[NUM_SAMPLES / 2];
}

void setup() {
Serial.begin(9600);
}

void loop() {
int filtered = medianFilter();
Serial.println(filtered);
delay(100);
}

改变 PWM 频率

Arduino 默认 PWM 频率约为 490Hz(D5、D6 为 980Hz),某些应用需要调整:

// 注意:这会改变定时器设置,可能影响其他功能

// 设置 D9、D10 的 PWM 频率为 62.5kHz
void setup() {
// 将 Timer1 设置为模式 5(快速 PWM,8位)
TCCR1A = _BV(COM1A1) | _BV(COM1B1) | _BV(WGM10);
TCCR1B = _BV(CS10) | _BV(WGM12); // 无预分频
}

void loop() {
analogWrite(9, 128); // D9 输出 50% 占空比
analogWrite(10, 64); // D10 输出 25% 占空比
}

PWM 频率对照表

引脚定时器默认频率可调范围
D5, D6Timer0980Hz修改会影响 millis()
D9, D10Timer1490Hz31Hz - 62.5kHz
D3, D11Timer2490Hz31Hz - 32kHz

模拟输入阻抗考虑

Arduino 模拟输入的输入阻抗约为 100MΩ,但信号源阻抗过高会导致测量误差:

  • 信号源阻抗 < 10kΩ:直接连接,误差可忽略
  • 信号源阻抗 10kΩ-100kΩ:可能产生微小误差
  • 信号源阻抗 > 100kΩ:需要使用电压跟随器

使用运算放大器作为电压跟随器:

高阻抗信号源 ──┬── 运放 +输入端

GND

运放输出 ── Arduino 模拟输入
运放 -输入端 ── 连接到输出端(电压跟随器配置)

常见问题

1. 模拟读数不稳定

原因

  • 电源噪声
  • 传感器信号弱
  • ADC 参考电压不稳定

解决方法

  • 添加去耦电容(0.1μF)
  • 使用移动平均算法
  • 使用外部参考电压

2. PWM 输出有噪音

原因

  • PWM 频率过低,能听到蜂鸣声
  • 负载电流过大

解决方法

  • 提高 PWM 频率
  • 添加滤波电路
  • 使用驱动电路

3. 模拟输入精度不够

Arduino 的 10 位 ADC 提供约 4.9mV 的分辨率。如需更高精度:

  • 使用外部 ADC 芯片(如 ADS1115,16 位)
  • 使用运算放大器放大信号
  • 降低参考电压提高分辨率

下一步

掌握了模拟 I/O 后,我们将学习串口通信,这是 Arduino 与电脑、其他设备交互的重要方式。