I2C
最近移植 RT-Thread + FM24CL64 ,发送地址报错 NACK;排查下来,软硬件都有小问题;趁此机会,整理下I2C相关。
文末有简单的模拟 I2C 代码,单纯为了深入理解 I2C 协议,注释仅为个人理解。
I2C协议的灵魂,【上拉电阻】 和 【开漏输出】;由此带来 “线与” 特性,可实现多主、仲裁等。
FM24CL64,地址0xA0,读出指定位置(就从起始位置读起吧 0x0000)后的16字节数据。
那么读的逻辑就是...,好不好用不知道,对比上图很合理。
uint8_t byte[16] = {0};
i2c_start(); i2c_send_byte(0xA0); i2c_wait_ack(); i2c_send_byte(0x00); i2c_wait_ack(); i2c_send_byte(0x00); i2c_wait_ack(); i2c_start(); i2c_send_byte(0xA1); i2c_wait_ack(); for (i = 0; i < 16; i++) { byte[i] = i2c_read_byte(); if (15 != i) i2c_ack(); else i2c_nack(); } i2c_stop();
模拟 I2C
#include <stdint.h> #include "stm32f1xx_hal.h" #define SCL_H() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET) #define SCL_L() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET) #define SDA_H() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET) #define SDA_L() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET) #define SCL_R() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_6) #define SDA_R() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) void fm24cl64_read(void); /* * Mr.Zhen,BlackGloves * 2021.12.03 **/ void udelay(uint8_t us) { int i; for (i = 18*us; i > 0; i--) ; } void i2c_start(void) { SCL_H(); SDA_H(); udelay(2); SDA_L(); udelay(3); } void i2c_stop(void) { SCL_H(); SDA_L(); udelay(2); SDA_H(); udelay(3); } void i2c_send_byte(uint8_t byte) { uint8_t i; // MSB SCL_L(); for (i = 0; i < 8; i++) { if (byte & 0x80) SDA_H(); else SDA_L(); udelay(5); SCL_H(); udelay(5); SCL_L(); //紧跟 SCL_H, byte 发送过程中,防止让出SCL后被其他设备占用. if (i == 7) { SDA_H(); //最后 1bit Master 释放 SDA SCL SCL_H(); //顺序很重要,先SDA后SCL; //假设最后 1bit SDA=L, 先释放SCL的可能后果是,其他设备虽占用总线,但SDA_H()无效 } byte <<= 1; } } uint8_t i2c_read_byte(void) { uint8_t i; uint8_t val = 0; /* MSB */ for (i = 0; i < 8; i++) { val <<= 1; SCL_H(); //Master 让出总线, slave 占有总线,并将数据输出到 SDA, 完成后 slave 让出总线; udelay(5); if (SDA_R()) val++; SCL_L(); //master 占有总线, slave 得到消息(Master已获取上一bit数据)后释放 SDA, 并准备下一个bit的数据, //待拿到总线控制权后第一时间将数据输出到 SDA if (i != 7) //master 第8个时钟后半段的低电平期间, 要抉择是 ack 还是 nack, 提前输出到 SDA. udelay(5); //这样, master第9个时钟起始的上升沿就可以判断出 SDA 的状态 //slave 根据ack / nack 进行后续操作, 这样可充分利用SCL时钟. } return val; } uint8_t i2c_wait_ack(void) { uint8_t ret; SDA_H(); SCL_H(); //确保 master 让出总线; i2c_send_byte 最后1bit会释放总线,这2行也可考虑删除. udelay(5); //给足 slave 反应时间, 等待 ack if (SDA_R()) ret = 1; else ret = 0; // ACK SCL_L(); //1.时序; 2.通知 slave,释放 SDA; 3.master 占有总线,准备发送数据; return ret; } void i2c_ack(void) { SDA_L(); //接收完1byte数据后,第8个时钟后半段的低电平期间,拉低SDA,准备ACK信号. udelay(5); SCL_H(); //SCL上升沿,SDA=L udelay(5); SCL_L(); udelay(5); //第9个周期完 SDA_H(); SCL_H(); //master 释放 SDA SCL } void i2c_nack(void) { SDA_H(); //假设 master 中途提前发送 nack, 那么 slave 不再准备数据; udelay(5); //后面 master 读到的全是1, SDA高阻态么. SCL_H(); udelay(5); SCL_L(); udelay(5); SDA_H(); SCL_H(); } void fm24cl64_read(void) { int i; uint8_t byte[16] = {0}; if (SDA_R() && SCL_R()) { i2c_start(); i2c_send_byte(0xA0); i2c_wait_ack(); i2c_send_byte(0x00); i2c_wait_ack(); i2c_send_byte(0x00); i2c_wait_ack(); i2c_start(); i2c_send_byte(0xA1); i2c_wait_ack(); for (i = 0; i < 16; i++) { byte[i] = i2c_read_byte(); if (i != 15) i2c_ack(); else i2c_nack(); } i2c_stop(); } }