🎯 蓝桥杯单片机学习笔记——PCF8591 完全攻略

写在前面:PCF8591 就像是单片机世界里的"翻译官",它能把现实世界的模拟信号(光线、电压)翻译成单片机能懂的数字语言,也能把数字指令翻译成模拟电压输出。掌握它,你就掌握了单片机与现实世界对话的钥匙!🔑


📍 一、设备地址:PCF8591 的"身份证号码"

1.1 地址构成原理

每个 PCF8591 芯片都有一个独特的"身份证号",就像快递包裹上的地址一样,只有写对了地址,数据才能准确送达。

地址结构(8位二进制):

┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│    1    │    0    │    0    │    1    │   A2    │   A1    │   A0    │   R/W   │
└─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
  固定前缀(1001)                          硬件地址(可配置)              读写位

1.2 蓝桥杯开发板默认配置

在蓝桥杯的开发板上,A0、A1、A2 三个引脚都接地(都是 0),所以:

操作类型 二进制地址 十六进制 记忆技巧
写操作 1001 0000 0x90 9️⃣0️⃣ 就像"就要写"的谐音
读操作 1001 0001 0x91 9️⃣1️⃣ 比写操作多 1,表示"读一读"

💡 比喻理解:这就像寄快递,1001 是省份代码(固定的),000 是街道门牌号(硬件决定的),最后一位是告诉快递员你是要"寄件(写)“还是"收件(读)”。


🔌 二、IIC 协议:两根线的"对话艺术"

2.1 IIC 总线的两条"生命线"

IIC(Inter-Integrated Circuit)协议就像两个人用对讲机通话,只需要两根线就能完成复杂的数据传输:

信号线 全称 作用 形象比喻
SCL Serial Clock Line 时钟线,控制数据传输节奏 🎵 指挥家的指挥棒,打拍子控制节奏
SDA Serial Data Line 数据线,双向传输数据 🚚 双向车道,可以来回运货

2.2 主从设备的"君臣关系"

  • 主设备(Master):掌握 SCL 时钟线的控制权,就像皇帝掌握朝政节奏
  • 从设备(Slave):听从主设备的节奏,按时钟信号工作,就像大臣听从皇帝调遣

在蓝桥杯中,单片机是主设备PCF8591 是从设备

2.3 三种关键信号

IIC 通信就像一场正式的会议,需要遵循严格的礼仪:

🟢 起始信号(Start)—— “会议开始!”
SCL 保持高电平时,SDA 从高电平跳变到低电平
就像主持人敲响会议锤:"现在开会!"

时序图:

SDA: ‾‾‾‾‾\______
SCL: ‾‾‾‾‾‾‾‾‾‾‾
     ↑ 起始信号
🔵 应答信号(ACK)—— “收到,明白!”
从设备拉低 SDA,表示"我听到了,继续说"
就像开会时点头示意"嗯,我在听"
🔴 停止信号(Stop)—— “散会!”
SCL 保持高电平时,SDA 从低电平跳变到高电平
就像主持人宣布:"今天的会议到此结束!"

时序图:

SDA: ______/‾‾‾‾‾
SCL: ‾‾‾‾‾‾‾‾‾‾‾
     ↑ 停止信号

🎛️ 三、控制字:告诉 PCF8591 “你要听谁说话”

PCF8591 有 4 个模拟输入通道(AIN0~AIN3),就像一个有 4 个耳朵的机器人,你需要告诉它用哪只耳朵听。

3.1 蓝桥杯常用通道配置

传感器类型 通道编号 控制字 记忆口诀
光敏电阻 AIN1 0x41 4️⃣1️⃣ “是一(AIN1)号光敏”
滑动变阻器 AIN3 0x43 4️⃣3️⃣ “是三(AIN3)号滑动”

3.2 控制字的二进制解析

0x41 为例:

0x41 = 0100 0001
       │││└─┴─ 通道选择:01 = AIN1
       ││└──── 自动增量标志:0 = 不自动切换
       │└───── 模拟输入编程:00 = 四路单端输入
       └────── 使能 DAC 输出:0 = 不使能

💡 小白理解:控制字就像点菜单,0x41 就是告诉服务员"我要 1 号套餐(光敏电阻的数据)"。


🔄 四、A/D 转换:把"模拟世界"翻译成"数字语言"

4.1 转换流程图解

┌─────────────────────────────────────────────────────────┐
│  现实世界的模拟信号(光线强度、电压变化)                  │
└────────────────┬────────────────────────────────────────┘
                 │
                 ▼
         ┌───────────────┐
         │  PCF8591 芯片  │  ← 8位 ADC 转换器
         │   (翻译官)     │
         └───────┬───────┘
                 │
                 ▼
    ┌────────────────────────┐
    │  数字量 (0~255)         │  ← 单片机能理解的数字
    └────────────────────────┘

4.2 读取步骤详解(六步走战略)

unsigned char Ad_Read(unsigned char addr)
{
    unsigned char temp;
    
    // 【第1步】发送起始信号 —— "喂,PCF8591,醒醒!"
    I2CStart();
    
    // 【第2步】发送写地址 0x90 —— "我要给你下命令了"
    I2CSendByte(0x90);
    I2CWaitAck();  // 等待 PCF8591 点头:"嗯,我在听"
    
    // 【第3步】发送控制字 —— "请帮我读取 XX 通道的数据"
    I2CSendByte(addr);  // 比如 0x41(光敏)或 0x43(滑动)
    I2CWaitAck();
    
    // 【第4步】重新发送起始信号 —— "现在切换到接收模式"
    I2CStart();
    
    // 【第5步】发送读地址 0x91 —— "把数据告诉我吧"
    I2CSendByte(0x91);
    I2CWaitAck();
    
    // 【第6步】接收数据 —— "收到!"
    temp = I2CReceiveByte();
    I2CSendAck(1);  // 发送非应答信号:"够了,不用再说了"
    
    // 【第7步】发送停止信号 —— "通话结束,挂了"
    I2CStop();
    
    return temp;  // 返回 0~255 的数字
}

🎭 戏剧化理解:这就像打电话订外卖:

  1. 拨通电话(起始信号)
  2. “喂,是XX餐厅吗?”(发送 0x90)
  3. “我要点一份宫保鸡丁”(发送控制字)
  4. 挂断后重新拨打(重新起始)
  5. “请问我的外卖做好了吗?”(发送 0x91)
  6. “好了,马上送”(接收数据)
  7. “谢谢,再见”(停止信号)

🎨 五、D/A 转换:把"数字指令"变成"模拟电压"

5.1 转换原理

PCF8591 的 DAC(数模转换器)就像一个"调光旋钮",你给它一个 0~255 的数字,它就输出对应的 0~5V 电压。

换算公式:

输出电压 = (输入数值 / 255) × 5V
输入数值 输出电压 应用场景
0 0V 完全关闭 LED
51 1V 微弱亮度
102 2V 中等亮度
153 3V 较亮
204 4V 很亮
255 5V 最大亮度

5.2 写入函数详解

void Da_Write(unsigned char value)
{
    // 【第1步】起始信号
    I2CStart();
    
    // 【第2步】发送写地址
    I2CSendByte(0x90);
    I2CWaitAck();
    
    // 【第3步】发送 DAC 使能控制字
    I2CSendByte(0x40);  // 0x40 表示启用 DAC 输出
    I2CWaitAck();
    
    // 【第4步】发送要转换的数值(0~255)
    I2CSendByte(value);
    I2CWaitAck();
    
    // 【第5步】停止信号
    I2CStop();
}

💡 生活比喻:这就像用遥控器调节空调温度,你按"+"号(发送数值),空调就会调整到对应温度(输出电压)。


📦 六、完整底层驱动代码详解

6.1 iic.h 头文件(接口声明)

#ifndef __IIC_H_
#define __IIC_H_

// ========== 基础 IIC 协议函数 ==========
void I2CStart(void);              // 发送起始信号
void I2CStop(void);               // 发送停止信号
void I2CSendByte(unsigned char byt);     // 发送一个字节
unsigned char I2CReceiveByte(void);      // 接收一个字节
unsigned char I2CWaitAck(void);          // 等待应答信号
void I2CSendAck(unsigned char ackbit);   // 发送应答信号

// ========== PCF8591 专用函数 ==========
unsigned char Ad_Read(unsigned char addr);  // AD 转换读取
void Da_Write(unsigned char value);         // DA 转换写入

#endif

6.2 iic.c 实现文件(核心代码)

#include "iic.h"
#include "reg52.h"
#include "intrins.h"

#define DELAY_TIME 10

// 定义 IIC 引脚
sbit sda = P2 ^ 1;  // 数据线
sbit scl = P2 ^ 0;  // 时钟线

// ========== 延时函数(微调时序) ==========
static void I2C_Delay(unsigned char n)
{
    do {
        _nop_();_nop_();_nop_();_nop_();_nop_();
        _nop_();_nop_();_nop_();_nop_();_nop_();
        _nop_();_nop_();_nop_();_nop_();_nop_();  
    } while(n--);       
}

// ========== 起始信号 ==========
void I2CStart(void)
{
    sda = 1;  // 先拉高数据线
    scl = 1;  // 再拉高时钟线
    I2C_Delay(DELAY_TIME);
    sda = 0;  // 数据线下降沿 → 起始信号
    I2C_Delay(DELAY_TIME);
    scl = 0;  // 拉低时钟线,准备传输    
}

// ========== 停止信号 ==========
void I2CStop(void)
{
    sda = 0;  // 先拉低数据线
    scl = 1;  // 再拉高时钟线
    I2C_Delay(DELAY_TIME);
    sda = 1;  // 数据线上升沿 → 停止信号
    I2C_Delay(DELAY_TIME);
}

// ========== 发送一个字节(高位先发) ==========
void I2CSendByte(unsigned char byt)
{
    unsigned char i;
    for(i=0; i<8; i++) {
        scl = 0;  // 拉低时钟,准备数据
        I2C_Delay(DELAY_TIME);
        
        // 根据最高位设置 SDA
        if(byt & 0x80) {
            sda = 1;
        } else {
            sda = 0;
        }
        I2C_Delay(DELAY_TIME);
        
        scl = 1;  // 拉高时钟,从设备读取数据
        byt <<= 1;  // 左移一位,准备下一位
        I2C_Delay(DELAY_TIME);
    }
    scl = 0;  // 传输完成,拉低时钟
}

// ========== 接收一个字节 ==========
unsigned char I2CReceiveByte(void)
{
    unsigned char da = 0;
    unsigned char i;
    
    for(i=0; i<8; i++) {   
        scl = 1;  // 拉高时钟,读取数据
        I2C_Delay(DELAY_TIME);
        da <<= 1;  // 左移一位,准备接收新位
        if(sda) {
            da |= 0x01;  // 如果 SDA 为高,最低位置 1
        }
        scl = 0;  // 拉低时钟
        I2C_Delay(DELAY_TIME);
    }
    return da;    
}

// ========== 等待应答信号 ==========
unsigned char I2CWaitAck(void)
{
    unsigned char ackbit;
    scl = 1;  // 拉高时钟
    I2C_Delay(DELAY_TIME);
    ackbit = sda;  // 读取应答位(0=应答,1=非应答)
    scl = 0;
    I2C_Delay(DELAY_TIME);
    return ackbit;
}

// ========== 发送应答信号 ==========
void I2CSendAck(unsigned char ackbit)
{
    scl = 0;
    sda = ackbit;  // 0=应答,1=非应答
    I2C_Delay(DELAY_TIME);
    scl = 1;
    I2C_Delay(DELAY_TIME);
    scl = 0; 
    sda = 1;  // 释放 SDA
    I2C_Delay(DELAY_TIME);
}

// ========== AD 读取函数 ==========
unsigned char Ad_Read(unsigned char addr)
{
    unsigned char temp;
    
    I2CStart();
    I2CSendByte(0x90);  // 写地址
    I2CWaitAck();
    I2CSendByte(addr);  // 控制字(选择通道)
    I2CWaitAck();
    
    I2CStart();  // 重新起始
    I2CSendByte(0x91);  // 读地址
    I2CWaitAck();
    temp = I2CReceiveByte();
    I2CSendAck(1);  // 发送非应答信号
    I2CStop();
    
    return temp;
}

// ========== DA 写入函数 ==========
void Da_Write(unsigned char value)
{
    I2CStart();
    I2CSendByte(0x90);
    I2CWaitAck();
    I2CSendByte(0x40);  // DAC 使能控制字
    I2CWaitAck();
    I2CSendByte(value);  // 要转换的数值
    I2CWaitAck();
    I2CStop();
}

🎯 七、实战应用案例

7.1 读取光敏电阻值

#include "iic.h"
#include "Seg.h"  // 假设你有数码管显示函数

void main()
{
    unsigned char light_value;
    
    while(1) {
        // 读取光敏电阻(通道 AIN1)
        light_value = Ad_Read(0x41);
        
        // 在数码管上显示(0~255)
        Seg_Buf[0] = light_value / 100 % 10;
        Seg_Buf[1] = light_value / 10 % 10;
        Seg_Buf[2] = light_value % 10;
        
        Delay_ms(100);  // 延时 100ms
    }
}

7.2 同时读取多个传感器(重要!)

⚠️ 注意:PCF8591 有一个"数据滞后"特性,第一次读取的是上一次转换的结果。

错误示范:

light = Ad_Read(0x41);  // 读到的可能是上次的数据
slide = Ad_Read(0x43);  // 读到的可能是光敏的数据

正确做法(读两次,取第二次):

Ad_Read(0x41);  // 第一次读取,丢弃
light = Ad_Read(0x41);  // 第二次读取,这才是真实数据

Ad_Read(0x43);  // 第一次读取,丢弃
slide = Ad_Read(0x43);  // 第二次读取,这才是真实数据

💡 比喻理解:这就像问别人"你吃了吗?",第一次回答的可能是上顿饭的情况,第二次才是这顿饭的真实情况。


🔊 八、继电器与蜂鸣器控制(附加功能)

8.1 控制原理

蓝桥杯开发板上的继电器和蜂鸣器通过 74HC573 锁存器控制,需要特定的操作步骤。

控制流程:

1. 准备数据 → P0 口
2. 打开锁存器 → P2 = 0xA0(Y5 译码)
3. 锁存数据 → P2 = 0x1F(关闭所有译码)

8.2 继电器控制函数

void Relay(unsigned char flag)
{
    static unsigned char temp = 0x00;
    static unsigned char temp_Old = 0xFF;
    
    if(flag) {
        temp |= 0x10;   // 第 4 位置 1(继电器在 P0.4)
    } else {
        temp &= ~0x10;  // 第 4 位清 0
    }
    
    // 只有状态改变时才操作(避免频繁刷新)
    if(temp != temp_Old) {
        P0 = ~temp;  // 蓝桥杯板子是低电平有效
        P2 = (P2 & 0x1F) | 0xA0;  // 打开 Y5 锁存器
        P2 &= 0x1F;  // 关闭所有锁存器
        temp_Old = temp;
    }
}

// 使用示例
Relay(1);  // 打开继电器
Relay(0);  // 关闭继电器

8.3 蜂鸣器控制函数

void Beep(unsigned char flag)
{
    static unsigned char temp = 0x00;
    static unsigned char temp_Old = 0xFF;
    
    if(flag) {
        temp |= 0x40;   // 第 6 位置 1(蜂鸣器在 P0.6)
    } else {
        temp &= ~0x40;  // 第 6 位清 0
    }
    
    if(temp != temp_Old) {
        P0 = ~temp;
        P2 = (P2 & 0x1F) | 0xA0;
        P2 &= 0x1F;
        temp_Old = temp;
    }
}

// 使用示例
Beep(1);  // 蜂鸣器响
Beep(0);  // 蜂鸣器停

8.4 位操作详解

// 假设 temp = 0b00000000

temp |= 0x10;   // 或运算,第 4 位置 1
// 结果:0b00010000

temp &= ~0x10;  // 与非运算,第 4 位清 0
// 结果:0b00000000

💡 比喻理解

  • |= 就像"开灯",不管原来是什么状态,都把指定位置打开
  • &= ~ 就像"关灯",不管原来是什么状态,都把指定位置关闭

📊 九、常见问题与解决方案

问题 1:读取的数据总是 0 或 255

可能原因:

  • IIC 时序不对(延时太短或太长)
  • 硬件连接问题(SDA/SCL 接反)
  • 没有上拉电阻(IIC 需要上拉)

解决方案:

// 增加延时时间
#define DELAY_TIME 20  // 原来是 10,改成 20 试试

问题 2:切换通道后数据不对

原因: PCF8591 的"数据滞后"特性

解决方案:

// 读两次,取第二次
Ad_Read(0x41);  // 丢弃
light = Ad_Read(0x41);  // 真实数据

问题 3:DA 输出电压不准确

检查清单:

  • 是否发送了正确的控制字(0x40)
  • 输入数值是否在 0~255 范围内
  • 用万用表测量 AOUT 引脚电压

📚 十、知识点总结

知识点 关键内容 记忆口诀
设备地址 写 0x90,读 0x91 “就要写,就要读”
IIC 信号 起始、应答、停止 “起承转合”
控制字 光敏 0x41,滑动 0x43 “是一光,是三滑”
AD 转换 0~255 对应模拟量 “数字翻译官”
DA 转换 0~255 对应 0~5V “电压调节器”
数据滞后 读两次取第二次 “第一次是旧闻”

🎉 结语

掌握 PCF8591,你就打开了单片机与模拟世界对话的大门。记住:

IIC 协议是语言,PCF8591 是翻译官,你的代码是指挥棒!

加油,蓝桥杯的勇士们!愿你们在比赛中所向披靡!🏆


📌 快速查询表

// 常用代码片段
light = Ad_Read(0x41);  // 读光敏
slide = Ad_Read(0x43);  // 读滑动
Da_Write(128);          // 输出 2.5V
Relay(1);               // 开继电器
Beep(1);                // 响蜂鸣器
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐