51单片机 I2C 通讯
前提条件
- 已完成51单片机串口通讯
了解 I2C
I2C
总线全程 Inter IC Bus, 常用于芯片间通信, 比如说从AT24C02
存储芯片中读取数据. 会使用到两根通信线 SCL
(Serial Clock), SDA
(Serial Data).
I2C 接线
- 所有
I2C
设备的SCL
连在一起,SDA
连在一起 - 所有
I2C
设备的SCL
和SDA
均要配置成开漏输出模式 - 在
SCL
和SDA
上各添加一个上拉电阻, 阻值一般为4.7kΩ - 开漏输出和上拉电阻主要是为了解决多机通信互相干扰的问题
https://www.ti.com/lit/an/sbaa565/sbaa565.pdf?ts=1725775105679
I2C 时序
- 起始条件
S
:SCL
高电平期间,SDA
从高电平切换为低电平. - 终止条件
P
:SCL
高电平期间,SDA
从低电平切换为高电平.https://www.ti.com/lit/an/sbaa565/sbaa565.pdf?ts=1725775105679
- 发生数据
SD
: 主设备取出待发送数据(8bit)的最高位, 将SDA
设置为该值. 而后拉高SCL
再拉低SCL
, 从设备会在SCL
的下降沿读取SDA
的值. 所以在SCL
线高电平期间,SDA
的值必须保持稳定, 毕竟此时从设备正在读取数据. 重复这一过程8次, 即可发送 8bit 数据.https://www.ti.com/lit/an/sbaa565/sbaa565.pdf?ts=1725775105679
- 接收数据
RD
: 主设备先将SDA
设置为1以释放SDA
. 然后主设备拉高SCL
再拉低SCL
创造一个时钟脉冲, 在这个脉冲期间, 从设备会把它要发出的值写在SCL
上, 所以在这个脉冲期间主设备还需要读取SDA
的值. 创建8个脉冲, 读取8次SDA
, 就能拼接出一个完整的 byte(8bit) - 接收应答
RA
: 时序图切片与数据传输相同, 可以理解为数据传输的第9位. 主设备每发送一个 byte 后, 从设备会向主设备发送一个应答位(1bit), 0 表示成功收到数据 ACK(Acknowledge), 1 表示没有收到数据或通信结束 NAK(Not Acknowledge) - 发送应答
SA
: 时序图切片与数据传输相同, 可以理解为数据传输的第9位. 主设备每收到一个 byte 后, 需要先从设备发送一个应答位(1bit), 0 表示成功收到数据 ACK(Acknowledge), 1 表示没有收到数据或通信结束 NAK(Not Acknowledge)
注意! 上述简写字母为作者自定义, 用来代表时序图的切片, 不是标准的简写.
例子1, 发送一帧数据.
- 起始
- 发送数据. 内容为从设备地址, 数据大小 1byte. 其中前7位为设备地址, 最后一位为发送/接受标志位. 例如, 对于
AT24C02
芯片来说- 设备地址的前4位为固定值 0b1010
- 后3位由芯片引脚 A0, A1, A2 所接的电平高低决定, 假设它们都接了 GND, 那它们就是 0b000
- 观察数据手册发现, 时序图中 R/W 的 W 上划了一根横线, 所以代表 1 是 Read, 0 是 Write. 由于是写入, 所以值为 0b0
- 所以第一个 byte 的具体值为 0b10100000
- 接收应答
- 发送数据, 接收应答 ... 发送数据, 接收应答
- 终止
例子2, 接受一帧数据
- 起始
- 发送数据. 内容为从设备地址, 数据大小 1byte. 其中前7位为设备地址, 最后一位为发送/接受标志位.
AT24C02
芯片来说, 由于是读操作, 所以最后一位是 1, 因为观察数据手册发现, 时序图中 R/W 的 W 上划了一根横线, 这意味着代表 1 是 Read, 0 是 Write. - 接收应答
- 接收数据, 发送应答 ... 接收数据, 发生应答
- 终止
读写 AT24C02 芯片
效果展示
- 在串口监视器中发送'r', 单片机从
AT24C02
读取数据, 打印出原始数据. - 在串口监视器中发送'w', 单片机向
AT24C02
写入数据. - 在串口监视器中发送'r', 单片机从
AT24C02
读取数据, 打印读到的数据, 该数据应与写入的数据相同. - 将单片机断电, 然后再上电.
- 在串口监视器中发送'r', 单片机从
AT24C02
读取数据, 打印读到的数据, 该数据应与上一次读出的数据相同.
参考资料
工作流程(写入)
向 AT24C02 写入. 数据手册中流程图如下
转译为我们自定义符号的示意图
工作流程(读取)
从 AT24C02 读取. 数据手册中流程图如下
转译为我们自定义符号的示意图
代码编写
c
#include <stdint.h>
#include <stdio.h>
#include "uart.h"
#include "at24c02.h"
#include "delay.h"
uint8_t count = 0;
// 中断号为4的原因: STC89C52RC数据手册, 第6章 中断系统
void uart_isr(void) __interrupt (4) {
if (RI == 1) { // 如果是接收中断
RI = 0; // 清除接收标志
char received_data = SBUF; // 读取接收到的数据
uint8_t storage_address = 0x02;
count ++;
if(received_data == 'w') {
at24c02_write(storage_address, count);
delay_ms(6); // 写入需要 5ms
char str[20];
sprintf(str, "Data saved, %d", count);
uart_print(str);
return;
}
if(received_data == 'r') {
uint8_t data = at24c02_read(storage_address);
char str[20];
sprintf(str, "Data retrieved, %d", data);
uart_print(str);
}
}
}
void main(void) {
uart_init();
while (1) {
}
}
c
#include "at24c02.h"
uint8_t at24c02_read(uint8_t address) {
i2c_start();
i2c_send(AT24C02_WRITE_ADDRESS);
i2c_receive_ack();
i2c_send(address);
i2c_receive_ack();
i2c_start();
i2c_send(AT24C02_READ_ADDRESS);
i2c_receive_ack();
uint8_t data = i2c_receive();
i2c_send_ack(I2C_NO_ACK);
i2c_stop();
return data;
}
void at24c02_write(uint8_t address, uint8_t data) {
i2c_start();
i2c_send(AT24C02_WRITE_ADDRESS);
i2c_receive_ack();
i2c_send(address);
i2c_receive_ack();
i2c_send(data);
i2c_receive_ack();
i2c_stop();
}
c
#include "i2c.h"
void i2c_start(void) {
I2C_SDA = 1;
I2C_SCL = 1;
I2C_SDA = 0; // Start condition: SDA goes low when SCL is high
I2C_SCL = 0;
}
void i2c_stop(void) {
I2C_SDA = 0;
I2C_SCL = 1;
I2C_SDA = 1; // Stop condition: SDA goes high when SCL is high
}
void i2c_send(uint8_t data) {
for (uint8_t i = 0; i < 8; i++) {
I2C_SDA = data & (0x80 >> i); // Send each bit
// 注意! 从设备读取 SDA 状态需要时间, 所以 SCL 需要保持一段时间的高电平
// 查看 AT24C02 数据手册得该芯片最大 SCL 时钟频率为 400kHz, 0.4us一个周期.
// 51 单片机时钟频率为 11059200 Hz, 11us一个周期.
// 所以, 单片机将 SCL 置高后马上置低, 少说了过了 11us, 足够从设备读取 SCL 数据了.
// 但是对于频率更高的单片机, 就可能需要延时增加 SCL 的时长了.
I2C_SCL = 1; // Clock high
I2C_SCL = 0; // Clock low
}
}
uint8_t i2c_receive(void) {
uint8_t data = 0;
I2C_SDA = 1; // Release SDA to read data
for (uint8_t i = 0; i < 8; i++) {
I2C_SCL = 1;
data = (data << 1) | I2C_SDA; // Read each bit from SDA
I2C_SCL = 0;
}
return data;
}
void i2c_send_ack(I2C_ACK_BIT ack_bit){
I2C_SDA = ack_bit; // Set ack bit in SDA
I2C_SCL = 1; // Slave device start reading SDA
I2C_SCL = 0; // Slave device stop reading SDA
}
I2C_ACK_BIT i2c_receive_ack(void){
I2C_SDA = 1; // Release SDA
I2C_SCL = 1; // Start reading SDA
I2C_ACK_BIT ack_bit = I2C_SDA; // Read ack bit from SDA
I2C_SCL = 0; // Stop reading SDA
return ack_bit;
}
完整代码 codes/demo207-51-i2c