51单片机基础-软件I²C 及其方便应用

第二十七章 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 → SDAP2.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;   // SDA
    sbit 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;
    }
    // 读ACK
    scl_low();
    sda_release();                // 释放SDA,等待从机拉低ACK
    i2c_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/NACK
    scl_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; }      // NACK
    if(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),便于后续平滑切换。

posted @ 2025-12-05 16:22  gccbuaa  阅读(1)  评论(0)    收藏  举报