第七周 学习笔记: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使用不同地址,避免数据覆盖;读取地址必须与写入地址一致。
六、注意事项
- 地址对齐:
addr应为 8 的倍数,否则可能发生跨页回卷。 - 单次写入长度:一次最多写入 8 字节;超过 8 字节需要分多次写入。
- 地址分配:多个变量存储时,地址应错开,避免相互覆盖。
- 数据类型大小:51 单片机中
unsigned char为 1 字节,int通常为 2 字节。 - 溢出问题:
unsigned char范围为 0~255,示例中a += 10多次累加可能溢出,必要时改用unsigned int。 - 图片路径:本文图片使用本地绝对路径,建议将图片放入 md 同目录下,改为相对路径,如
assets/at24c02.png。











