PCF8591这颗芯片在我做过的多个单片机项目中都扮演着重要角色,特别是在需要模拟量采集和输出的场合。作为一款经典的I2C接口模数转换器,它以极简的外围电路实现了4路ADC输入和1路DAC输出,特别适合51单片机这类资源有限的平台。今天我就结合多年使用经验,从硬件设计到软件驱动,带大家彻底吃透这个"模拟量翻译官"。
提示:本文所有代码示例均基于STC89C52单片机,使用Keil C51编译器,可直接用于蓝桥杯单片机竞赛备赛。
PCF8591的内部结构可以划分为三个关键部分:
模拟输入通道(AIN0-AIN3):
8位ADC核心:
8位DAC输出:

| 参数 | 最小值 | 典型值 | 最大值 | 单位 |
|---|---|---|---|---|
| 供电电压 | 2.5 | 5.0 | 6.0 | V |
| ADC分辨率 | - | 8 | - | bit |
| INL误差 | - | ±1 | ±2 | LSB |
| DNL误差 | - | ±0.5 | ±1 | LSB |
| 转换时间 | - | 100 | 200 | μs |
| I2C速率 | - | - | 100 | kHz |
circuit复制VCC ----+---[10k]---+--- SDA
| |
[4.7k] PCF8591
| |
GND ----+---[10k]---+--- SCL
关键设计注意事项:
在工业现场使用时特别需要注意:
PCF8591的7位I2C地址构成:
code复制1 0 0 1 A2 A1 A0
└──固定前缀──┘ └─可编程─┘
例如A2A1A0=000时,写地址=0x90,读地址=0x91
控制字节(Control Byte)各位定义:
| 位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| 功能 | 模拟输出使能 | 自动增量 | 输入模式 | 通道选择 |
常用配置示例:
c复制// I2C起始信号
void I2C_Start() {
SDA = 1; SCL = 1; Delay5us();
SDA = 0; Delay5us();
SCL = 0; Delay5us();
}
// 发送一个字节
void I2C_SendByte(uint8_t dat) {
for(uint8_t i=0; i<8; i++) {
SDA = (dat & 0x80) ? 1 : 0;
SCL = 1; Delay5us();
SCL = 0; Delay5us();
dat <<= 1;
}
SDA = 1; // 释放总线
}
c复制uint8_t PCF8591_ReadADC(uint8_t channel) {
uint8_t val;
I2C_Start();
I2C_SendByte(0x90); // 写地址
I2C_SendByte(0x40 | channel); // 控制字节
I2C_Start();
I2C_SendByte(0x91); // 读地址
val = I2C_RecvByte(); // 丢弃第一次
I2C_Ack();
val = I2C_RecvByte(); // 实际值
I2C_NAck();
I2C_Stop();
return val;
}
c复制void PCF8591_WriteDAC(uint8_t value) {
I2C_Start();
I2C_SendByte(0x90); // 写地址
I2C_SendByte(0x40); // 使能DAC输出
I2C_SendByte(value); // 输出值
I2C_Stop();
}
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 读取值始终为0 | I2C地址错误 | 检查A0-A2接线 |
| ADC值跳动大 | 电源噪声 | 加强VREF滤波 |
| DAC输出不准 | 负载阻抗过小 | 增加电压跟随器 |
| 通信失败 | 上拉电阻过大 | 减小至4.7kΩ |
c复制#define FILTER_LEN 8
uint8_t filter_buf[FILTER_LEN];
uint8_t MovingAverage(uint8_t new_val) {
static uint8_t index = 0;
uint16_t sum = 0;
filter_buf[index++] = new_val;
if(index >= FILTER_LEN) index = 0;
for(uint8_t i=0; i<FILTER_LEN; i++) {
sum += filter_buf[i];
}
return sum / FILTER_LEN;
}
时序优化:适当缩短I2C时钟周期(最小5μs高电平+5μs低电平)
电源管理:不使用ADC时关闭相关电路(控制字节bit6=0)
c复制void MultiChannel_Read() {
uint8_t adc_values[4];
for(uint8_t ch=0; ch<4; ch++) {
adc_values[ch] = PCF8591_ReadADC(ch);
Delay_ms(10); // 通道切换稳定时间
}
// 数据处理...
}
c复制void Generate_SineWave() {
const uint8_t sine_table[32] = {
128, 152, 176, 198, 218, 234, 245, 253,
255, 253, 245, 234, 218, 198, 176, 152,
128, 103, 79, 57, 37, 21, 10, 2,
0, 2, 10, 21, 37, 57, 79, 103
};
while(1) {
for(uint8_t i=0; i<32; i++) {
PCF8591_WriteDAC(sine_table[i]);
Delay_ms(10); // 控制频率
}
}
}
在实际项目中,我发现PCF8591的温度漂移约为0.5mV/℃,对于高精度应用需要做温度补偿。另外,当同时使用ADC和DAC时,建议先完成所有ADC采集再进行DAC输出,避免模拟电路相互干扰。