模拟 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,意味着可以将输入范围分成 个离散值:
- 输入 0V → 数字值 0
- 输入 5V → 数字值 1023
- 分辨率 =
即 ADC 能分辨的最小电压变化约为 4.89mV。
转换时间
Arduino 的 ADC 完成一次转换约需 100 微秒。这意味着理论上每秒可以进行约 10000 次采样,但实际应用中建议采样率更低以确保稳定性。
ADC 精度的影响因素
实际使用中,ADC 的精度会受到以下因素影响:
- 参考电压稳定性:参考电压的波动直接影响测量精度
- 电源噪声:电源中的噪声会耦合到 ADC 输入
- 输入阻抗:高阻抗信号源可能导致测量误差
- 温度漂移:芯片温度变化会影响 ADC 精度
analogRead() 函数
读取模拟引脚的电压值:
int value = analogRead(pin);
pin:模拟引脚编号(A0-A5,也可以直接用数字 0-5)- 返回值:0-1023(对应 0V-5V)
电压换算公式:
其中 是参考电压,默认为 5V。
参考电压选择
使用 analogReference() 函数可以改变 ADC 的参考电压,从而改变测量范围和精度:
analogReference(type);
Arduino Uno 支持的参考电压
| 类型 | 说明 | 测量范围 |
|---|---|---|
DEFAULT | 默认 5V | 0-5V |
INTERNAL | 内部 1.1V 参考源 | 0-1.1V |
EXTERNAL | AREF 引脚输入的电压 | 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 位: 次采样
- 增加 2 位: 次采样
- 增加 3 位: 次采样
// 通过 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, D6 | Timer0 | 980Hz | 修改会影响 millis() |
| D9, D10 | Timer1 | 490Hz | 31Hz - 62.5kHz |
| D3, D11 | Timer2 | 490Hz | 31Hz - 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 与电脑、其他设备交互的重要方式。