目录
一、I2C简介
对比串口通信,从全双工转为半双工,有应答,一根线可以同时接多个模块,单片机可以选择与特定模块通信,并且不会相互干扰。
简而言之,I2C为同步、串行、半双工的通信总线协议。


1、为何SDA与SCL使用开漏输出模式?
SDA引脚模式设置,开漏输出,上拉, 这样就不用再设置IO方向了, 开漏输出的时候(=1), 也可以读取外部信号的高低电平。即可同时作为输入输出。
硬件模式:复用开漏输出、既不上拉也不下拉。(快速模式:400 Kbit/s)
软件模拟:推挽输出、配置上拉电阻。
软件模拟:推挽输出、配置上拉电阻。

2、AT24C02通讯地址

根据不同地址访问不同芯片模块,若有同样地址的芯片模块挂载在同一条总线上时,通过修改AD0(举例)引脚上的电平,可以改变芯片地址的最后一位。可通过修改A0、A1、A2同时挂载多个相同地址的芯片模块。
3、软件I2C与硬件I2C的区别
容易用软件模拟。软件I2C,可直接软件模拟,任意引脚均可用,比较灵活。
硬件I2C具有资源限制,需要使用特定的引脚,不同stm32核心板的资源不同。
4、EEPROM是什么?
EEPROM是一种掉电数据不丢失的存储器,常用来存储配置信息,系统重新上电时可以加载。写完需要等5ms。

5、I2C仲裁机制?
I2C 仲裁机制,总线具有 线“与”(Wired-AND)的逻辑功能,即只要有一个节点发送低电平时,总线上就表现为低电平;当所有节点都表现为高点平时,总线才表现为高电平。简单说,它遵循“低电平优先”的原则,即谁先发送低电平谁就会掌握对总线的控制权。SDA只允许一个主节点占据总线。
正是由于这种“线”与逻辑功能,当多个节点同时发送时钟信号时,在总线上表现的是统一的时钟信号。这就是SCL的同步原理。
SDA仲裁与SCL时钟同步处理过程没有先后关系,而是同步进行的。
二、配置文件及相关函数
1、myiic.c
#include "./BSP/IIC/myiic.h"
#include "./SYSTEM/delay/delay.h"
/**
* @brief 初始化IIC
* @param 无
* @retval 无
*/
void iic_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
IIC_SCL_GPIO_CLK_ENABLE(); /* SCL引脚时钟使能 */
IIC_SDA_GPIO_CLK_ENABLE(); /* SDA引脚时钟使能 */
gpio_init_struct.Pin = IIC_SCL_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 快速 */
HAL_GPIO_Init(IIC_SCL_GPIO_PORT, &gpio_init_struct);/* SCL */
gpio_init_struct.Pin = IIC_SDA_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD; /* 开漏输出 */
HAL_GPIO_Init(IIC_SDA_GPIO_PORT, &gpio_init_struct);/* SDA */
/* SDA引脚模式设置,开漏输出,上拉, 这样就不用再设置IO方向了, 开漏输出的时候(=1), 也可以读取外部信号的高低电平 */
iic_stop(); /* 停止总线上所有设备 */
}
/**
* @brief IIC延时函数,用于控制IIC读写速度
* @param 无
* @retval 无
*/
static void iic_delay(void)
{
delay_us(2); /* 2us的延时, 读写速度在250Khz以内 */
}
/**
* @brief 产生IIC起始信号
* @param 无
* @retval 无
*/
void iic_start(void)
{
IIC_SDA(1);
IIC_SCL(1);
iic_delay();
IIC_SDA(0); /* START信号: 当SCL为高时, SDA从高变成低, 表示起始信号 */
iic_delay();
IIC_SCL(0); /* 钳住I2C总线,准备发送或接收数据 */
iic_delay();
}
/**
* @brief 产生IIC停止信号
* @param 无
* @retval 无
*/
void iic_stop(void)
{
IIC_SDA(0); /* STOP信号: 当SCL为高时, SDA从低变成高, 表示停止信号 */
iic_delay();
IIC_SCL(1);
iic_delay();
IIC_SDA(1); /* 发送I2C总线结束信号 */
iic_delay();
}
/**
* @brief 等待应答信号到来
* @param 无
* @retval 1,接收应答失败
* 0,接收应答成功
*/
uint8_t iic_wait_ack(void)
{
uint8_t waittime = 0;
uint8_t rack = 0;
IIC_SDA(1); /* 主机释放SDA线(此时外部器件可以拉低SDA线) */
iic_delay();
IIC_SCL(1); /* SCL=1, 此时从机可以返回ACK */
iic_delay();
while (IIC_READ_SDA) /* 等待应答 */
{
waittime++;
if (waittime > 250)
{
iic_stop();
rack = 1;
break;
}
}
IIC_SCL(0); /* SCL=0, 结束ACK检查 */
iic_delay();
return rack;
}
/**
* @brief 产生ACK应答
* @param 无
* @retval 无
*/
void iic_ack(void)
{
IIC_SDA(0); /* SCL 0 -> 1 时 SDA = 0,表示应答 */
iic_delay();
IIC_SCL(1); /* 产生一个时钟 */
iic_delay();
IIC_SCL(0);
iic_delay();
IIC_SDA(1); /* 主机释放SDA线 */
iic_delay();
}
/**
* @brief 不产生ACK应答
* @param 无
* @retval 无
*/
void iic_nack(void)
{
IIC_SDA(1); /* SCL 0 -> 1 时 SDA = 1,表示不应答 */
iic_delay();
IIC_SCL(1); /* 产生一个时钟 */
iic_delay();
IIC_SCL(0);
iic_delay();
}
/**
* @brief IIC发送一个字节
* @param data: 要发送的数据
* @retval 无
*/
void iic_send_byte(uint8_t data)
{
uint8_t t;
for (t = 0; t < 8; t++)
{
IIC_SDA((data & 0x80) >> 7); /* 高位先发送 */
iic_delay();
IIC_SCL(1);
iic_delay();
IIC_SCL(0);
data <<= 1; /* 左移1位,用于下一次发送 */
}
IIC_SDA(1); /* 发送完成, 主机释放SDA线 */
}
/**
* @brief IIC读取一个字节
* @param ack: ack=1时,发送ack; ack=0时,发送nack
* @retval 接收到的数据
*/
uint8_t iic_read_byte(uint8_t ack)
{
uint8_t i, receive = 0;
for (i = 0; i < 8; i++ ) /* 接收1个字节数据 */
{
receive <<= 1; /* 高位先输出,所以先收到的数据位要左移 */
IIC_SCL(1);
iic_delay();
if (IIC_READ_SDA)
{
receive++;
}
IIC_SCL(0);
iic_delay();
}
if (!ack)
{
iic_nack(); /* 发送nACK */
}
else
{
iic_ack(); /* 发送ACK */
}
return receive;
}
2、24cxx.c
(1)写一个字节
void at24cxx_write_one_byte(uint16_t addr,uint8_t data) /* 指定地址写入一个字节 */
{
/* 1、发送起始信号 */
iic_start();
/* 2、发送通信地址(写操作地址) */
iic_send_byte(0xA0);
/* 3、等待应答信号 */
iic_wait_ack();
/* 4、发送内存地址 */
iic_send_byte(addr);
/* 5、等待应答信号 */
iic_wait_ack();
/* 6、发送写入数据 */
iic_send_byte(data);
/* 7、等待应答信号 */
iic_wait_ack();
/* 8、发出停止信号 */
iic_stop();
/* 等待EEPROM写入完成 */
delay_ms(10);
}
(2)读一个字节
uint8_t at24cxx_read_one_byte(uint16_t addr) /* 指定地址读取一个字节 */
{
uint8_t rec = 0;
/* 1、发送起始信号 */
iic_start();
/* 2、发送通信地址(写操作地址) */
iic_send_byte(0xA0);
/* 3、等待应答信号 */
iic_wait_ack();
/* 4、发送内存地址 */
iic_send_byte(addr);
/* 5、等待应答信号 */
iic_wait_ack();
/* 6、发送起始信号 */
iic_start();
/* 7、发送通讯地址(读通讯地址) */
iic_send_byte(0xA1);
/* 8、等待应答信号 */
iic_wait_ack();
/* 9、等待接收数据 */
rec = iic_read_byte(0);
/* 10、发送非应答nack(上述0)(即获取该地址而已) */
/* 11、发送停止信号 */
iic_stop();
return rec;
}
(3)连续读写(均是在读写一个字节的基础上实现)。
/**
* @brief 在AT24CXX里面的指定地址开始读出指定个数的数据
* @param addr : 开始读出的地址 对24c02为0~255
* @param pbuf : 数据数组首地址(读的数据放的位置)
* @param datalen : 要读出数据的个数
* @retval 无
*/
void at24cxx_read(uint16_t addr, uint8_t *pbuf, uint16_t datalen)
{
while (datalen--)
{
*pbuf++ = at24cxx_read_one_byte(addr++);
}
}
/**
* @brief 在AT24CXX里面的指定地址开始写入指定个数的数据
* @param addr : 开始写入的地址 对24c02为0~255
* @param pbuf : 数据数组首地址
* @param datalen : 要写入数据的个数
* @retval 无
*/
void at24cxx_write(uint16_t addr, uint8_t *pbuf, uint16_t datalen)
{
while (datalen--)
{
at24cxx_write_one_byte(addr, *pbuf);
addr++;
pbuf++;
}
}

写时序,每次发完一个字节注意需要等待应答
读时序,第一次起始信号先写地址,第二次起始信号读,从机返回 word address的数据
发送不成功波形检验,首先查看是否有应答ACK,第二查看波形的波峰是否过低导致被识别为低电平。
正点原子例程考虑了7位与10位收发,if进行判断AT24C16,分两个字节判断。