欢迎光临
我们一直在努力

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

🎯 蓝桥杯单片机学习笔记——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 的数字
}

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

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

  • 🎨 五、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); // 响蜂鸣器

    赞(0)
    未经允许不得转载:171主机测评 » 蓝桥杯单片机学习笔记(六)——PCF8591 完全攻略
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址