第七周 PCF8591和AT24C02学习笔记

第七周 学习笔记:PCF8591 模块

核心目标:掌握 PCF8591 通过 I2C 总线进行 AD/DA 转换的基本流程,理解 I2C 总线起始、停止、应答、收发数据等底层时序。


一、模块简介与引脚图

PCF8591 是一款常见的 8 位 A/D、D/A 转换芯片,通过 I2C 总线与单片机通信,常用于采集光敏电阻、滑动变阻器等模拟量。

引脚图

二、I2C 地址与读写位

PCF8591 的器件地址由固定位和硬件地址位组成,最低位决定读写方向:

  • 0:写
  • 1:读

常用地址:

操作 地址
写 0x90
读 0x91

读写位示意图


三、控制字节说明

PCF8591 通过控制字节来选择通道和功能。

控制字节图

常用通道选择表

D1 D0 选择通道
0 0 AIN0
0 1 AIN1
1 0 AIN2
1 1 AIN3

注意事项:

  • 通过 D1、D0 选择通道 1 或通道 3。
  • 当系统进行 D/A 转换时,D6 必须固定为 1,用于开启 DAC 输出功能。

四、I2C 总线启动与停止

启动/停止时序图

1. 总线启动条件

// 总线启动条件
void IIC_Start(void)
{
    SDA = 1;
    SCL = 1;
    IIC_Delay(DELAY_TIME);
    SDA = 0;
    IIC_Delay(DELAY_TIME);
    SCL = 0;	
}

说明:

  • 初始时先拉高 SDA 和 SCL。
  • 延时后拉低 SDA,形成下降沿。
  • 再延时后拉低 SCL,完成启动条件。

2. 总线停止条件

// 总线停止条件
void IIC_Stop(void)
{
    SDA = 0;
    SCL = 1;
    IIC_Delay(DELAY_TIME);
    SDA = 1;
    IIC_Delay(DELAY_TIME);
}

说明:

  • 初始时拉低 SDA,同时拉高 SCL。
  • 延时后拉高 SDA,形成上升沿,完成停止条件。

五、发送应答与等待应答

应答时序图

1. 发送应答

// 发送应答
void IIC_SendAck(bit ackbit)
{
    SCL = 0;
    SDA = ackbit;  					// 0:应答,1:非应答
    IIC_Delay(DELAY_TIME);
    SCL = 1;
    IIC_Delay(DELAY_TIME);
    SCL = 0; 
    SDA = 1;
    IIC_Delay(DELAY_TIME);
}

说明:

  • 先拉低 SCL。
  • 将要发送的应答位放到 SDA 上。
  • 拉高 SCL,让从机读取当前应答位。
  • 延时后拉低 SCL,最后释放 SDA。

2. 等待应答

// 等待应答
bit IIC_WaitAck(void)
{
    bit ackbit;
	
    SCL  = 1;
    IIC_Delay(DELAY_TIME);
    ackbit = SDA;
    SCL = 0;
    IIC_Delay(DELAY_TIME);
    return ackbit;
}

说明:

  • 拉高 SCL。
  • 延时后读取 SDA 电平,即为从机应答位。
  • 读取完成后拉低 SCL。

六、通过 I2C 总线发送和接收数据

数据收发时序图

1. 通过 I2C 总线发送数据

// 通过 I2C 总线发送数据
void IIC_SendByte(unsigned char byt)
{
    unsigned char i;

    for(i = 0; i < 8; i++)
    {
        SCL = 0;
        IIC_Delay(DELAY_TIME);

        if(byt & 0x80)
            SDA = 1;
        else
            SDA = 0;

        IIC_Delay(DELAY_TIME);
        SCL = 1;
        byt <<= 1;
        IIC_Delay(DELAY_TIME);
    }

    SCL = 0;  
}

说明:

  • 先拉低 SCL。
  • 将当前字节的最高位赋值到 SDA。
  • 拉高 SCL,使从机读取该位。
  • 将发送字节左移一位,循环 8 次,实现从高位到低位发送。

2. 从 I2C 总线上接收数据

// 从 I2C 总线上接收数据
unsigned char IIC_RecByte(void)
{
    unsigned char i, da = 0;

    for(i = 0; i < 8; i++)
    {   
        SCL = 1;
        IIC_Delay(DELAY_TIME);

        da <<= 1;
        if(SDA)
            da |= 1;

        SCL = 0;
        IIC_Delay(DELAY_TIME);
    }

    return da;    
}

说明:

  • 先拉高 SCL。
  • 将接收字节左移一位。
  • 将当前 SDA 电平写入接收字节的最低位。
  • 拉低 SCL,循环 8 次,实现从高位到低位接收。

小提示:da 变量使用前应先初始化为 0,避免初值不确定影响结果。


七、AD 转换流程

AD 转换时序图

AD 读取函数

unsigned char Ad_Read(unsigned char addr)
{
    unsigned char temp;

    IIC_Start();                  // 1. 发送起始信号
    IIC_SendByte(0x90);           // 2. 发送写地址
    IIC_WaitAck();                // 3. 等待应答

    IIC_SendByte(addr);           // 4. 发送控制字节,指定要读取的通道
    IIC_WaitAck();                // 5. 等待应答

    IIC_Start();                  // 6. 再次发送起始信号
    IIC_SendByte(0x91);           // 7. 发送读地址
    IIC_WaitAck();                // 8. 等待应答

    temp = IIC_RecByte();         // 9. 读取转换数据字节

    IIC_SendAck(1);               // 10. 发送非应答
    IIC_Stop();                   // 11. 发送停止信号

    return temp;
}

使用示例

dat2 = Ad_Read(0x41);  // 读取光敏电阻值,对应 AIN1
dat  = Ad_Read(0x43);  // 读取滑动变阻器值,对应 AIN3

注意事项:

  • 读取完成后,在发送停止信号之前,必须先发送非应答信号。
  • PCF8591 的 AD 转换具有一次延迟,当前读取到的通常是上一次转换的结果。如果需要实时性较高的数据,建议连续读取两次或先空读一次丢弃旧值。

八、DA 转换流程

DA 转换时序图

DA 输出函数

void Da_Write(unsigned char dat)
{
    IIC_Start();                  // 发送起始信号
    IIC_SendByte(0x90);           // 发送写地址
    IIC_WaitAck();                // 等待应答

    IIC_SendByte(0x41);           // 发送控制字节,开启 DAC 功能
    IIC_WaitAck();                // 等待应答

    IIC_SendByte(dat);            // 发送要转换的数字量:0~255
    IIC_WaitAck();                // 等待应答

    IIC_Stop();                   // 发送停止信号
}

注意事项:

  • 进行 D/A 转换时,必须发送 0x41。
  • 控制字节的 D6 位必须为 1,用于开启 DAC 功能。
  • dat 取值范围为 0~255,对应模拟输出电压。

九、如何解决蜂鸣器和继电器的电流声

蜂鸣器控制函数

void Beep(unsigned char flag)
{
    static unsigned char temp = 0x00;      // 缓存当前 P0 状态
    static unsigned char temp_old = 0xff;  // 记录上一次状态

    if(flag)
        temp |= 0x40;      // 蜂鸣器对应 P0.6,置 1
    else
        temp &= ~0x40;     // 蜂鸣器对应 P0.6,清 0

    if(temp != temp_old)
    {
        P0 = temp;
        P2 = P2 & 0x1f | 0xa0;   // 打开锁存器
        P2 &= 0x1f;               // 关闭锁存器
        temp_old = temp;          // 更新旧状态
    }
}

继电器控制函数

void Relay(unsigned char flag)
{
    static unsigned char temp = 0x00;
    static unsigned char temp_old = 0xff;

    if(flag)
        temp |= 0x10;      // 继电器对应 P0.4
    else
        temp &= ~0x10;

    if(temp != temp_old)
    {
        P0 = temp;
        P2 = P2 & 0x1f | 0xa0;
        P2 &= 0x1f;
        temp_old = temp;
    }
}

说明:

  • 蜂鸣器地址位为 0x40,继电器地址位为 0x10。
  • 两个函数结构完全相同,只是控制位不同。
  • 使用 static 变量记录上一次状态,只有状态发生变化时才刷新 P0 和锁存器。
  • 这样可以避免蜂鸣器和继电器被反复刷新,从而消除电流声或抖动现象。

十、学习小结与易错点

易错点 正确做法
忘记发送非应答 接收最后一个字节后,先发送非应答,再发送停止信号
AD 读取值不准确 PCF8591 存在一次转换延迟,建议连续读取两次或先空读一次
DA 转换无输出 控制字节 D6 位必须为 1,示例中使用 0x41
蜂鸣器/继电器有电流声 使用状态缓存,状态变化时才更新 P0 和锁存器
I2C 地址混淆 写地址为 0x90,读地址为 0x91

十一、程序编写错误

1.数码管显示电压进行强制类型转换时,忘记考虑char类型的溢出现象。

错误程序:

Seg_Buf[6] = (unsigned char)(Voltage_Output*100) / 10 % 10;
Seg_Buf[7] = (unsigned char)(Voltage_Output*100) % 10;

应改为:

Seg_Buf[6] = (unsigned int)(Voltage_Output*100) / 10 % 10;
Seg_Buf[7] = (unsigned int)(Voltage_Output*100) % 10;

2.用复循环实现互斥点亮;忘记输出电压值

DA_Write(Voltage_Output*51);//实时输出电压值
unsigned char i;
for(i=0;i<2;i++)
	ucLed[i] = (i == Seg_Mode);

AT24C02 模块学习笔记

一、模块简介

AT24C02 是基于 I2C 总线的 2Kbit(256 字节)EEPROM,常用于蓝桥杯单片机题目中需要掉电保存数据的场景。

二、硬件结构

  • A0、A1、A2:片选地址引脚,通常接地固定为 0。
  • 器件地址:AT24C02 的 7 位器件地址为 1010 A2 A1 A0,当 A0~A2 接地时,器件地址为 0x50。
  • R/W:I2C 地址字节最低位,0 表示写,1 表示读。
    • 写地址:0xA0
    • 读地址:0xA1

三、写入函数

void EEPROM_Write(unsigned char *pData, unsigned char addr, unsigned char num);

参数说明

参数 含义 说明
pData 数据指针 待写入数据的首地址。数组传数组名;单个变量传 &变量
addr 起始地址 EEPROM 中要写入的首地址,必须为 8 的倍数
num 字节数 一次写入的字节数,最多 8 字节

示例:

  • unsigned char 类型(1 字节)最多写 8 个;
  • int 类型在 51 单片机中通常为 2 字节,最多写 4 个;
  • 若使用 4 字节 int 环境,则最多写 2 个。

注意

AT24C02 内部按 8 字节一页写入。若起始地址不是 8 的倍数,写满一页后会回卷到本页开头,可能造成数据覆盖。

四、读取函数

void EEPROM_Read(unsigned char *pData, unsigned char addr, unsigned char num);

参数与写入函数相同,只是数据方向变为从 EEPROM 读出到单片机内存。

五、使用示例

/* 变量声明 */
unsigned char dat[2] = {30, 60};   // 待保存的数组
unsigned char a = 200;             // 单个待保存变量

/* 按键处理 */
switch(Key_Down)
{
    case 19:
        a += 10;
        break;
    case 18:
        dat[1] -= 10;
        break;
    case 17:
        EEPROM_Write(dat, 0, 2);    // 将 dat 写入 EEPROM 地址 0~1
        break;
    case 16:
        EEPROM_Write(&a, 8, 1);     // 将 a 写入 EEPROM 地址 8
        break;
}

/* 数码管显示 */
Seg_Buf[0] = a / 100 % 10;
Seg_Buf[1] = a / 10 % 10;
Seg_Buf[2] = a % 10;

Seg_Buf[6] = dat[1] / 10;
Seg_Buf[7] = dat[1] % 10;

/* 主函数 */
void main()
{
    EEPROM_Read(&a, 8, 1);          // 上电从 EEPROM 地址 8 读回 a
    // 其他初始化代码
    // ...
}

说明:示例中 dat 与 a 使用不同地址,避免数据覆盖;读取地址必须与写入地址一致。

六、注意事项

  1. 地址对齐:addr 应为 8 的倍数,否则可能发生跨页回卷。
  2. 单次写入长度:一次最多写入 8 字节;超过 8 字节需要分多次写入。
  3. 地址分配:多个变量存储时,地址应错开,避免相互覆盖。
  4. 数据类型大小:51 单片机中 unsigned char 为 1 字节,int 通常为 2 字节。
  5. 溢出问题:unsigned char 范围为 0~255,示例中 a += 10 多次累加可能溢出,必要时改用 unsigned int。
  6. 图片路径:本文图片使用本地绝对路径,建议将图片放入 md 同目录下,改为相对路径,如 assets/at24c02.png。