蓝桥杯单片机学习笔记(六)——PCF8591 完全攻略
本文介绍了蓝桥杯单片机中PCF8591模块的使用方法,主要内容包括: PCF8591的地址配置:蓝桥杯开发板默认写地址0x90,读地址0x91 IIC通信协议:通过SCL时钟线和SDA数据线进行通信,包含起始信号、应答信号和停止信号 控制字设置:0x41对应AIN1(光敏电阻),0x43对应AIN3(滑动变阻器) A/D转换流程:6步完成模拟信号到数字信号的转换 D/A转换功能:通过发送0-255
🎯 蓝桥杯单片机学习笔记——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)