第二十七章 51单片机配置 I²C 及其简单应用
1. 导入
绝大多数传统 51(如 AT89C52)没有片上 I²C 外设,但可用“两线开漏 + 软件时序(bit-bang)”稳定地实现 I²C 主机,连接 EEPROM、IO 扩展、温度传感器、OLED 等。本章给出一套通用、可复用的软件 I²C 驱动(支持 7 位地址、ACK/NACK、重复起始),并配套几个最常用的小应用:总线扫描、PCF8574 输出、LM75 温度读取与 AT24C02 简化读写。
说明:
- 少数 STC 增强型 51(如 STC8/12/15 系列)带硬件 I²C,建议优先用硬件外设;本章主线以“软件 I²C”适配所有 51。
- I²C 物理层要求“开漏 + 上拉”,51 口写 1 相当于“释放高阻”,写 0 拉低输出,正好可模拟开漏。
2. 硬件要点
- 总线信号:
SDA(数据)、SCL(时钟),两线并联所有器件。 - 上拉电阻:SDA/SCL 各接 4.7kΩ(到 VCC 3.3V/5V,按系统电压),线短噪声小更稳。
- 电平兼容:5V MCU 连接 3.3V 器件时,优先全 3.3V 供电或加电平转换/强上拉;许多 3.3V 器件输入可容忍 5V 通过限流分压,但请查手册。
- 典型接法(可按需换口):
P2.0 → SDA,P2.1 → SCL,并各接上拉。
3. I²C 时序与开漏实现(关键点)
- 起始(START):SCL=1 时,SDA 从 1→0。
- 停止(STOP):SCL=1 时,SDA 从 0→1。
- 写 1 位:主机在 SCL=0 期间准备 SDA,SCL 拉 1 让从机采样。
- 读 1 位:主机在 SCL=1 期间采样 SDA。
- ACK/NACK:每传 1 字节后第 9 个时钟,从机拉 SDA=0 表示 ACK;主机读时需在第 9 个时钟拉 SDA(0=ACK、1=NACK)反馈给从机。
4. 软件 I²C 驱动(通用可复用)
#include <reg52.h>#include <intrins.h>/* --- 引脚定义(按需修改端口/引脚) --- */sbit I2C_SDA = P2^0; // SDAsbit I2C_SCL = P2^1; // SCL/* --- 微延时:决定SCL频率。可适当增减 _nop_() 数控制速度 --- */static void i2c_delay(void) {_nop_(); _nop_(); _nop_(); _nop_();_nop_(); _nop_(); _nop_(); _nop_();}/* --- 线控制:1=释放(上拉为高),0=拉低(开漏模拟) --- */static void sda_release(void){ I2C_SDA = 1; }static void sda_low(void){ I2C_SDA = 0; }static void scl_release(void){ I2C_SCL = 1; }static void scl_low(void){ I2C_SCL = 0; }/* --- 起始/停止 --- */void i2c_start(void){sda_release(); scl_release(); i2c_delay();sda_low(); i2c_delay();scl_low(); i2c_delay();}void i2c_stop(void){sda_low(); i2c_delay();scl_release(); i2c_delay();sda_release(); i2c_delay();}/* --- 写1字节:返回0=收到ACK,1=收到NACK --- */unsigned char i2c_write_byte(unsigned char dat){unsigned char i;for(i=0;i<8;i++){scl_low();if(dat & 0x80) sda_release(); else sda_low();i2c_delay();scl_release(); // 上升沿从机采样i2c_delay();dat <<= 1;}// 读ACKscl_low();sda_release(); // 释放SDA,等待从机拉低ACKi2c_delay();scl_release(); // 第9个时钟i2c_delay();// 采样ACK位{ unsigned char nack = (I2C_SDA ? 1 : 0);scl_low(); i2c_delay();return nack; } // 0=ACK,1=NACK}/* --- 读1字节:ack=1发送ACK,ack=0发送NACK --- */unsigned char i2c_read_byte(unsigned char ack){unsigned char i, dat=0;sda_release(); // 释放SDA,准备输入for(i=0;i<8;i++){dat <<= 1;scl_low(); i2c_delay();scl_release(); i2c_delay();if(I2C_SDA) dat |= 1;}// 第9个时钟发送ACK/NACKscl_low();if(ack) sda_low(); else sda_release();i2c_delay();scl_release(); i2c_delay();scl_low(); i2c_delay();sda_release();return dat;}/* --- 设备寻址(7位地址):dir=0写,1读。返回0=ACK --- */unsigned char i2c_address7(unsigned char addr7, unsigned char dir){unsigned char a8 = (addr7 << 1) | (dir ? 1 : 0);return i2c_write_byte(a8); // 0=ACK,1=NACK}/* --- 通用寄存器读写(带重复起始) --- */unsigned char i2c_write_reg8(unsigned char addr7, unsigned char reg, unsigned char val){i2c_start();if(i2c_address7(addr7, 0)) { i2c_stop(); return 1; } // NACKif(i2c_write_byte(reg)) { i2c_stop(); return 2; }if(i2c_write_byte(val)) { i2c_stop(); return 3; }i2c_stop();return 0;}unsigned char i2c_read_reg8(unsigned char addr7, unsigned char reg, unsigned char *pval){i2c_start();if(i2c_address7(addr7, 0)) { i2c_stop(); return 1; }if(i2c_write_byte(reg)) { i2c_stop(); return 2; }// 重复起始 + 读i2c_start();if(i2c_address7(addr7, 1)) { i2c_stop(); return 3; }*pval = i2c_read_byte(0); // NACK 结束i2c_stop();return 0;}/* --- 连续读(多字节) --- */unsigned char i2c_read_buf(unsigned char addr7, unsigned char reg, unsigned char *buf, unsigned char len){unsigned char i;if(!len) return 0;i2c_start();if(i2c_address7(addr7, 0)) { i2c_stop(); return 1; }if(i2c_write_byte(reg)) { i2c_stop(); return 2; }i2c_start();if(i2c_address7(addr7, 1)) { i2c_stop(); return 3; }for(i=0;i<len;i++){buf[i] = i2c_read_byte( (i < (len-1)) ? 1 : 0 ); // 中间ACK,最后NACK}i2c_stop();return 0;}/* --- 简易毫秒延时(演示用) --- */void delay_ms(unsigned int ms){unsigned int i,j;for(i=0;i<ms;i++) for(j=0;j<125;j++);}
要点:
- 51 口写 1 即“释放高阻”,配上拉电阻呈高电平;写 0 拉低。
i2c_write_byte必须读取从机 ACK;i2c_read_byte最后一个字节要回 NACK 结束。i2c_address7按 7 位地址 + R/W 位发送地址帧。
5. 通用小工具
5.1 I²C 总线扫描(找设备地址)
/* 返回探测到的设备个数;用串口/显示打印即可 */
unsigned char i2c_scan(unsigned char *found, unsigned char maxn){
unsigned char cnt=0, addr;
for(addr=0x03; addr<=0x77; addr++){ // 合法7位地址范围
i2c_start();
if(i2c_address7(addr, 0) == 0){ // 有ACK
if(cnt < maxn) found[cnt] = addr;
cnt++;
}
i2c_stop();
delay_ms(1);
}
return cnt;
}
6. 简单应用示例
6.1 PCF8574(I/O 扩展,输出点亮 LED)
- 7 位地址:0x20~0x27(由 A2/A1/A0 引脚决定,全部接地为 0x20)
- 写 1 字节即可将 8 路 I/O 输出为对应电平(高有效/低有效取决于负载接法)
#define PCF8574_ADDR 0x20 // A2-A0=000
void pcf8574_write(unsigned char val){
i2c_start();
if(i2c_address7(PCF8574_ADDR, 0)) { i2c_stop(); return; }
i2c_write_byte(val);
i2c_stop();
}
void demo_pcf8574_blink(void){
unsigned char v = 0xFE; // 低有效点亮(假设LED到GND)
while(1){
pcf8574_write(v);
v = (v << 1) | (v >> 7);delay_ms(200);}}
若 PCF8574 接按键输入,直接读 1 字节即可(PCF8574 除输出也可用作输入,读回引脚电平)。
6.2 LM75(数字温度传感器,9-bit,0.5°C/LSB)
- 7 位地址:0x48~0x4F(A2/A1/A0),默认 0x48
- 温度寄存器地址 0x00,读取 2 字节(MSB→LSB),取高 9 位为有符号温度,单位 0.5°C
#define LM75_ADDR 0x48
bit lm75_read_temp_c10(int *tc10){ // 输出单位=0.1摄氏度
unsigned char buf[2];
if(i2c_read_buf(LM75_ADDR, 0x00, buf, 2)) return 0;
// 9-bit: buf[0] = MSB, buf[1] 高1位为 LSB bit0
// 组成有符号9位值:T9..T1(0.5°C/LSB)
// 先拼为16位,再右移7位得到9位有符号
{
int raw = ((int)buf[0] << 8) | buf[1];
raw >>= 7; // 保留符号
// 每LSB=0.5°C => ×5 转 0.1°C
*tc10 = raw * 5;
}
return 1;
}
示例打印(伪代码):
void demo_lm75_print(void){
int t; // 0.1°C
if(lm75_read_temp_c10(&t)){
// 串口/液晶显示:t/10,t%10
// printf("T=%d.%dC\r\n", t/10, (t<0?-(t%10):t%10));
}
delay_ms(500);
}
6.3 AT24C02(2Kb EEPROM)简化读写
- 7 位基地址:
0x50 | (A2..A0),常见 A2~A0=0 → 0x50 - 写页:最多 8 字节,跨页会回卷;单字节写后需等待写周期(
510ms)
#define AT24C02_ADDR 0x50
bit at24c02_write_byte(unsigned char mem_addr, unsigned char val){
i2c_start();
if(i2c_address7(AT24C02_ADDR, 0)) { i2c_stop(); return 0; }
if(i2c_write_byte(mem_addr)) { i2c_stop(); return 0; }
if(i2c_write_byte(val)) { i2c_stop(); return 0; }
i2c_stop();
delay_ms(10); // 写周期
return 1;
}
bit at24c02_read_byte(unsigned char mem_addr, unsigned char *pval){
return (i2c_read_reg8(AT24C02_ADDR, mem_addr, pval) == 0);
}
7. 主程序示例(汇总)
- 初始化:无专用初始化,只需保证 SDA/SCL 口空闲为 1。
- 先扫描总线,打印发现的地址;
- 再运行一个你关心的 Demo(PCF8574/LM75/AT24C02)。
/* 可选:简易串口输出,用于总线扫描打印 */
void uart_init(void){
TMOD |= 0x20; TH1=0xFD; TL1=0xFD; TR1=1; SCON=0x50; EA=1; ES=0;
}
void putc(char c){ SBUF=c; while(!TI); TI=0; }
void puts(const char* s){ while(*s) putc(*s++); }
void put_hex7(unsigned char a){ // 打印7位地址
const char hx[]="0123456789ABCDEF";
putc('0'); putc('x');
putc(hx[(a>>4)&0xF]); putc(hx[a&0xF]);
}
void main(void){
unsigned char found[16];
unsigned char n, i;
// 口线释放为高(开漏)
I2C_SDA = 1; I2C_SCL = 1;
uart_init();
puts("I2C Scan:\r\n");
n = i2c_scan(found, sizeof(found));
if(n==0) puts("No device.\r\n");
else{
puts("Found: ");
for(i=0;i<n;i++){ put_hex7(found[i]); putc(' '); }
puts("\r\n");
}
// 试跑 PCF8574 灯流水(如总线存在 0x20)
demo_pcf8574_blink();
// 或周期读取 LM75 温度
// while(1) demo_lm75_print();
// 或读写 AT24C02
// at24c02_write_byte(0x00, 0x55);
// { unsigned char v; at24c02_read_byte(0x00, &v); /* 显示v */ }
}
8. 调试与排错
- 总线不响应/全 NACK:
- SDA/SCL 接反或未上拉;设备未上电/未共地;地址不对(7 位 vs 8 位混淆)。
- 偶发死锁(SCL 被拉低):
- 设备在位传输中断电导致卡总线;主机上电后可尝试手动输出 9 个 SCL 脉冲释放总线,再发 STOP。
- 读写错误/乱码:
- 时序太快或线太长;延时增大;检查 ACK 处理是否正确(最后一字节需 NACK)。
- EEPROM 写不进去:
- 未等待写周期(须 5~10ms);写跨页导致“回卷”;WP 引脚被置高写保护。
9. 进阶说明(硬件 I²C 的简述)
- 若使用带硬件 I²C 的增强型 51(如 STC8 系列),可直接配置 I²C 外设(开启模块、设置速率、主模式、启用 START/STOP/发送/接收指令并轮询状态位/ACK)。不同芯片寄存器命名各异,请以数据手册或官方库为准。
- 硬件 I²C 优点:时序精准、CPU 占用低、速率更高(400kHz+);劣势:需要了解寄存器状态机。
- 本章的软件 I²C API 与硬件 I²C 上层接口可保持一致(如
i2c_read_reg8/i2c_write_reg8),便于后续平滑切换。