一、IIC总线技术概述
1.1 IIC总线的发展历史
IIC(Inter-Integrated Circuit)总线,又称I²C总线,是由飞利浦半导体公司(现为恩智浦NXP)在1980年代开发的一种串行通信总线。最初设计目的是为电视机内的集成电路提供简单的控制接口,后来因其简洁性和高效性在各种嵌入式系统中得到广泛应用。
随着技术的发展,IIC总线标准经历了多次更新:
1982年:首次推出,标准模式速度100kbps
1992年:推出快速模式(400kbps)
1998年:推出高速模式(3.4Mbps)
2007年:推出超快速模式(5Mbps)
1.2 IIC总线的基本特性
IIC总线具有以下显著特点:
两线制结构:仅需串行数据线(SDA)和串行时钟线(SCL)两根信号线
多主多从架构:支持多个主设备和多个从设备连接在同一总线上
地址寻址:每个从设备有唯一的7位或10位地址
半双工通信:同一时间只能进行发送或接收
速率可调:支持从标准模式到高速模式多种传输速率
硬件简单:无需复杂的接口电路,节省PCB空间和成本
1.3 IIC总线的电气特性
IIC总线采用开漏输出结构,需要外接上拉电阻(通常为4.7kΩ)。这种设计具有以下优势:
实现了"线与"逻辑,便于总线仲裁
允许不同电压等级的器件在同一总线上通信
提高了总线的抗干扰能力
工作电压范围通常为1.8V-5V,具体取决于器件规格。总线电容限制一般为400pF,超过此值需要考虑使用总线缓冲器。
二、EEPROM存储器技术
2.1 EEPROM的基本原理
EEPROM(Electrically Erasable Programmable Read-Only Memory)是一种非易失性存储器,其主要特点包括:
数据掉电不丢失
可以字节为单位进行擦写
擦写寿命有限(通常10万-100万次)
读取速度较快,写入速度相对较慢
EEPROM通过浮栅晶体管存储数据,利用F-N隧穿效应实现电子注入和释放,从而改变存储单元的阈值电压来表示"0"和"1"。
2.2 EEPROM的分类
根据接口类型,EEPROM主要分为:
并行EEPROM:数据总线宽度通常为8位,速度快但引脚多
串行EEPROM:包括IIC接口、SPI接口等,节省引脚资源
根据容量大小,常见的EEPROM有:
小容量:1Kbit-64Kbit(如24C01-24C64)
中容量:128Kbit-512Kbit
大容量:1Mbit及以上
2.3 IIC接口EEPROM的特点
IIC接口EEPROM具有以下优势:
引脚精简:通常只需SCL、SDA、VCC、GND和WP(写保护)五个引脚
易于扩展:通过地址引脚可扩展多个器件
低功耗:工作电流小,适合电池供电设备
标准化接口:与各种MCU兼容性好
常见的IIC EEPROM系列包括Microchip的24系列、ST的M24系列等,容量从1Kbit到1Mbit不等。
三、IIC总线协议详解(摘自正点原子)
stm32f407探索者开发板V3 — 正点原子资料下载中心 1.0.0 文档
起始信号:
当 SCL 为高电平期间, SDA 由高到低的跳变。起始信号是一种电平跳变时序信号,而不是
一个电平信号。该信号由主机发出,在起始信号产生后,总线就处于被占用状态,准备数据传
输。
停止信号:
当 SCL 为高电平期间, SDA 由低到高的跳变。停止信号也是一种电平跳变时序信号,而不
是一个电平信号。该信号由主机发出,在停止信号发出后,总线就处于空闲状态。
应答信号:
发送器每发送一个字节,就在时钟脉冲 9 期间释放数据线,由接收器反馈一个应答信号。
应答信号为低电平时,规定为有效应答位( ACK 简称应答位),表示接收器已经成功地接收了
该字节;应答信号为高电平时,规定为非应答位( NACK ),一般表示接收器接收该字节没有成
功。
观察上图标号③就可以发现,有效应答的要求是从机在第 9 个时钟脉冲之前的低电平期间
将 SDA 线拉低,并且确保在该时钟的高电平期间为稳定的低电平。如果接收器是主机,则在它
收到最后一个字节后,发送一个 NACK 信号,以通知被控发送器结束数据发送,并释放 SDA
线,以便主机接收器发送一个停止信号。
数据有效性:
IIC 总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在
时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。数据在 SCL 的上
升沿到来之前就需准备好。并在下降沿到来之前必须稳定。
主机向从机读取数据的操作,一开始的操作与写操作有点相似,观察两个图也可以发现,
都是由主机发出起始信号,接着发送从机地址 +1( 读操作 ) 组成的 8bit 数据,从机接收到数据验
证是否是自身的地址。 那么在验证是自己的设备地址后,从机就会发出应答信号,并向主机返
回 8bit 数据,发送完之后从机就会等待主机的应答信号。假如主机一直返回应答信号,那么从
机可以一直发送数据,也就是图中的( n byte + 应答信号)情况,直到主机发出非应答信号,从
机才会停止发送数据。
四、IIC读取eeprom实战讲解
4.1 软件模拟IIC
4.11 初始化IIC 的GPIO,既然是软件模拟IIC,开发板所有的引脚都能使用;
void I2C_EE_Init()
{
GPIO_InitTypeDef gpio_init_struct;
__HAL_RCC_GPIOB_CLK_ENABLE();
gpio_init_struct.Pin = GPIO_PIN_8;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD;//开漏
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
gpio_init_struct.Pull = GPIO_PULLUP;//上拉
HAL_GPIO_Init(GPIOB,&gpio_init_struct);
gpio_init_struct.Pin = GPIO_PIN_9;
HAL_GPIO_Init(GPIOB,&gpio_init_struct);
IIC_Stop();
}
4.12 IIC起始结束时序
void IIC_Start(void)
{
SCL_HIGH;
SDA_HIGH;
Delay_I2c(4);
SDA_LOW;
Delay_I2c(4);
SCL_LOW;
Delay_I2c(4);//起始信号
}
void IIC_Stop(void)
{
SDA_LOW;
Delay_I2c(4);
SCL_HIGH;
Delay_I2c(4);
SDA_HIGH;
Delay_I2c(4);//停止信号
}
4.13 IIC发送时序
void IIC_Send_Byte(uint8_t data)
{
uint8_t i;
for(i = 0 ;i < 8 ;i++)
{
if((data & 0x80) >> 7)//高位先发送
{
SDA_HIGH;
}
else
{
SDA_LOW;
}
Delay_I2c(4);
SCL_HIGH;
Delay_I2c(4);
SCL_LOW;
data <<= 1;//下一位
}
SDA_HIGH;
}
4.14 IIC读时序
uint8_t IIC_Recevie_Byte(uint8_t ack)
{
uint8_t i,data = 0;
for(i = 0 ;i < 8 ;i++)
{
data <<= 1;//高位先输出
SCL_HIGH;
Delay_I2c(4);
if(SDA_READ())
{
data ++;
}
SCL_LOW;
Delay_I2c(4);
}
if(!ack)
{
IIC_noack();
}
else
{
IIC_ack();
}
return data;
}
4.15 应答信号
首先先释放 SDA ,把电平拉高,延时等待从机操作 SDA 线,然后主机拉高时 钟线并延时,确保有充足的时间让主机接收到从机发出的 SDA 信号,这里使用的是 IIC_READ_SDA 宏定义去读取,根据 IIC 协议,主机读取 SDA 的值为低电平,就表示“应答信 号”;读到 SDA 的值为高电平,就表示“非应答信号”。在这个等待读取的过程中加入了超时判 断,假如超过这个时间没有接收到数据,那么主机直接发出停止信号,跳出循环,返回等于 1 的变量。在正常等待到应答信号后,主机会把 SCL 时钟线拉低并延时,返回是否接收到应答信 号。
uint8_t IIC_Wait_ack(void)
{
uint8_t waittime = 0;
uint8_t rack = 0;
SDA_HIGH;
Delay_I2c(4);
SCL_HIGH;
Delay_I2c(4);
while(SDA_READ())
{
waittime++;
if(waittime > 250)
{
IIC_Stop();
rack = 1;
break;
}
}
SCL_LOW;
Delay_I2c(4);
return rack;
}
void IIC_noack()//非应答
{
SDA_HIGH;
Delay_I2c(4);
SCL_HIGH;
Delay_I2c(4);
SCL_LOW;
Delay_I2c(4);
}
void IIC_ack()//应答
{
SDA_LOW;
Delay_I2c(4);
SCL_HIGH;
Delay_I2c(4);
SCL_LOW;
Delay_I2c(4);
SDA_HIGH;
Delay_I2c(4);
}
4.16 IIC读写EEPROM驱动代码
4.161 IIC写操作
void IIC_Write_Byte(uint8_t addr,uint8_t data)
{
IIC_Start();//发生起始信号
IIC_Send_Byte(0xA0);//最低位0,表示写入
IIC_Wait_ack();//每次发完等待应答
IIC_Send_Byte(addr >> 8);//高字节
IIC_Wait_ack();
IIC_Send_Byte(addr % 256);//低字节
IIC_Wait_ack();
IIC_Send_Byte(data);//发生一字节
IIC_Wait_ack();
IIC_Stop();//停止条件
HAL_Delay(10);
}
4.162 IIC读操作
uint8_t IIC_Read_Byte(uint8_t addr)
{
uint8_t data = 0;
IIC_Start();//起始信号
IIC_Send_Byte(0xA0);//写入地址
IIC_Wait_ack();
IIC_Send_Byte(addr >> 8);
IIC_Wait_ack();
IIC_Send_Byte(addr % 256);
IIC_Wait_ack();
IIC_Start();
IIC_Send_Byte(0xA1);//1代表读
IIC_Wait_ack();
data = IIC_Recevie_Byte(0);//接受一字节
IIC_Stop();
return data;
}
附.h代码:
#define SCL_LOW HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_RESET)
#define SCL_HIGH HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_SET)
#define SDA_LOW HAL_GPIO_WritePin(GPIOB,GPIO_PIN_9,GPIO_PIN_RESET)
#define SDA_HIGH HAL_GPIO_WritePin(GPIOB,GPIO_PIN_9,GPIO_PIN_SET)
#define SDA_READ() HAL_GPIO_ReadPin(GPIOB ,GPIO_PIN_9)
4.2 硬件IIC(参考野火)
22. I2C—读写EEPROM — [野火]STM32 HAL库开发实战指南——基于野火F4系列开发板 文档 (embedfire.com)
硬件IIC与软件IIC不一样,必须用IIC引脚;
4.21 初始化
I2C_HandleTypeDef hi2c1;
/* I2C1 init function */
void MX_I2C1_Init(void)
{
/* USER CODE BEGIN I2C1_Init 0 */
/* USER CODE END I2C1_Init 0 */
/* USER CODE BEGIN I2C1_Init 1 */
/* USER CODE END I2C1_Init 1 */
hi2c1.Instance = I2C1;//IIC1
hi2c1.Init.ClockSpeed = 400000;//速率
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;/*指定时钟占空比,可选low/high = 2:1及16:9模式*/
hi2c1.Init.OwnAddress1 = 0; /*指定自身的I2C设备地址1,可以是7-bit或者10-bit*/
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;/*指定地址的长度模式,可以是7bit模式或者10bit模式 \*/
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;/*设置双地址模式 \*/
hi2c1.Init.OwnAddress2 = 0;/*指定自身的I2C设备地址2,只能是 7-bit \*/
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;/*指定广播呼叫模式 \*/
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;/*指定禁止时钟延长模式*/
HAL_I2C_Init(&hi2c1);
/* USER CODE BEGIN I2C1_Init 2 */
/* USER CODE END I2C1_Init 2 */
}
void HAL_I2C_MspInit(I2C_HandleTypeDef* i2cHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USER CODE BEGIN I2C1_MspInit 0 */
/* USER CODE END I2C1_MspInit 0 */
__HAL_RCC_GPIOB_CLK_ENABLE();
/**I2C1 GPIO Configuration
PB6 ------> I2C1_SCL
PB7 ------> I2C1_SDA
*/
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;//复用开漏
GPIO_InitStruct.Pull = GPIO_NOPULL;//上拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* I2C1 clock enable */
__HAL_RCC_I2C1_CLK_ENABLE();
/* USER CODE BEGIN I2C1_MspInit 1 */
/* USER CODE END I2C1_MspInit 1 */
}
4.22 向EEPROM写入一个字节的数据
uint32_t I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr)//数据,地址
{
HAL_StatusTypeDef status = HAL_OK;
status = HAL_I2C_Mem_Write(&hi2c1, 0xA0, (uint16_t)
WriteAddr, I2C_MEMADD_SIZE_8BIT, pBuffer, 1, 100);//IIC1,写操作,地址,字节大小,数据,数据大小,超时时间
/* Check the communication status */
if (status != HAL_OK) {
/* Execute user timeout callback */
//I2Cx_Error(Addr);
}
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY) {
}
/* Check if the EEPROM is ready for a new operation */
while (HAL_I2C_IsDeviceReady(&hi2c1, 0xA0,
300, 300) == HAL_TIMEOUT);//等待IIC已经就绪
/* Wait for the end of the transfer */
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY) {
}
return status;
}
4.23 EEPROM的页写入
// 向 I2C EEPROM 进行页写入操作
// pBuffer: 指向要写入的数据缓冲区的指针
// WriteAddr: 要写入的起始地址
// NumByteToWrite: 要写入的字节数
// 返回值: HAL 状态类型,用于表示操作的状态
uint32_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr,
uint8_t NumByteToWrite)
{
// 初始化状态为 HAL_OK,表示操作成功
HAL_StatusTypeDef status = HAL_OK;
// 调用 HAL_I2C_Mem_Write 函数向 EEPROM 写入数据
// &hi2c1 是 I2C 句柄,0XA0 是 EEPROM 的设备地址
// WriteAddr 是要写入的起始地址,I2C_MEMADD_SIZE_8BIT 表示地址大小为 8 位
// (uint8_t*)(pBuffer) 是要写入的数据缓冲区,NumByteToWrite 是要写入的字节数
// 100 是超时时间
status=HAL_I2C_Mem_Write(&hi2c1, 0XA0,WriteAddr,
I2C_MEMADD_SIZE_8BIT, (uint8_t*)(pBuffer),NumByteToWrite, 100);
// 等待 I2C 总线准备好进行下一次操作
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY) {
}
// 检查 EEPROM 是否准备好进行新的操作
// HAL_I2C_IsDeviceReady 函数用于检查设备是否准备好
// 0xA0 是 EEPROM 的设备地址,300 是重试次数,300 是每次重试的超时时间
while (HAL_I2C_IsDeviceReady(&hi2c1, 0xA0,
300, 300) == HAL_TIMEOUT);
// 等待传输结束,确保 I2C 总线处于就绪状态
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY) {
}
// 返回操作的状态
return status;
}
4.24 多字节写入
// 向 I2C EEPROM 进行缓冲区写入操作
// pBuffer: 指向要写入的数据缓冲区的指针
// WriteAddr: 要写入的起始地址
// NumByteToWrite: 要写入的字节数
void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr,
uint16_t NumByteToWrite)
{
// 定义变量用于存储页数、剩余字节数、地址偏移量和计数器
uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;
// 计算起始地址相对于页大小的偏移量
Addr = WriteAddr % EEPROM_PAGESIZE;
// 计算当前页剩余可写入的字节数
count = EEPROM_PAGESIZE - Addr;
// 计算要写入的总页数
NumOfPage = NumByteToWrite / EEPROM_PAGESIZE;
// 计算写入所有页后剩余的字节数
NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;
// 如果起始地址是页大小对齐的
if (Addr == 0) {
// 如果要写入的字节数小于页大小
if (NumOfPage == 0) {
// 直接调用页写入函数写入剩余的字节数
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
// 如果要写入的字节数大于页大小
else {
// 循环写入所有页
while (NumOfPage--) {
// 调用页写入函数写入一页数据
I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE);
// 更新写入地址
WriteAddr += EEPROM_PAGESIZE;
// 更新数据缓冲区指针
pBuffer += EEPROM_PAGESIZE;
}
// 如果还有剩余的字节数
if (NumOfSingle!=0) {
// 调用页写入函数写入剩余的字节数
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
}
// 如果起始地址不是页大小对齐的
else {
// 如果要写入的字节数小于页大小
if (NumOfPage== 0) {
// 直接调用页写入函数写入剩余的字节数
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
// 如果要写入的字节数大于页大小
else {
// 减去当前页可写入的字节数
NumByteToWrite -= count;
// 重新计算要写入的总页数
NumOfPage = NumByteToWrite / EEPROM_PAGESIZE;
// 重新计算写入所有页后剩余的字节数
NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;
// 如果当前页还有可写入的字节数
if (count != 0) {
// 调用页写入函数写入当前页剩余的字节数
I2C_EE_PageWrite(pBuffer, WriteAddr, count);
// 更新写入地址
WriteAddr += count;
// 更新数据缓冲区指针
pBuffer += count;
}
// 循环写入所有页
while (NumOfPage--) {
// 调用页写入函数写入一页数据
I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE);
// 更新写入地址
WriteAddr += EEPROM_PAGESIZE;
// 更新数据缓冲区指针
pBuffer += EEPROM_PAGESIZE;
}
// 如果还有剩余的字节数
if (NumOfSingle != 0) {
// 调用页写入函数写入剩余的字节数
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
}
}
附.h代码:
/* USER CODE BEGIN Includes */
#define EEPROM_PAGESIZE 8
/* USER CODE END Includes */
extern I2C_HandleTypeDef hi2c1;
/* USER CODE BEGIN Private defines */
void I2C_EE_Init(void);
uint8_t I2C_Test(void);
uint32_t I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr);
uint32_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr,
uint8_t NumByteToWrite);
void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr,
uint16_t NumByteToWrite);
uint32_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr, uint16_t NumByteToRead);
五. 总结(IIC软件模拟与硬件IIC区别)
硬件电路
硬件 IIC:需要使用芯片内部专门的 IIC 硬件模块,该模块具有独立的时钟发生器、数据寄存器等硬件电路,通过特定的引脚(SCL 时钟线和 SDA 数据线)与外部设备连接。
软件模拟 IIC:利用单片机的普通 I/O 引脚来模拟 IIC 的通信时序,不需要特定的硬件模块,只需要将普通 I/O 引脚配置为输出模式来发送数据和时钟信号,或配置为输入模式来接收数据。
通信速度
硬件 IIC:通信速度通常较快,因为硬件模块是专门为 IIC 通信设计的,能够高效地处理数据的发送和接收,并且可以支持较高的时钟频率。
软件模拟 IIC:速度相对较慢,由于是通过软件程序来模拟通信时序,程序需要执行一系列的指令来控制 I/O 引脚的电平变化,这会花费一定的时间,限制了通信的速度。
代码复杂度
硬件 IIC:使用硬件 IIC 时,芯片厂商通常会提供相应的驱动库或寄存器操作手册,开发者只需要按照规定的方式配置寄存器,即可实现 IIC 通信。代码相对简洁,且不需要过多关注底层的通信时序细节。
软件模拟 IIC:需要开发者自己编写代码来模拟 IIC 的起始信号、停止信号、数据传输、应答信号等各种时序,代码量较大,且需要对 IIC 协议有深入的理解,以确保时序的正确性。
灵活性
硬件 IIC:硬件 IIC 的引脚通常是固定的,一旦确定了使用的硬件 IIC 模块,其对应的引脚就不能再用于其他功能,灵活性较差。如果硬件设计已经确定,后期很难更改 IIC 引脚的分配。
软件模拟 IIC:可以根据实际需求灵活选择任意的普通 I/O 引脚来模拟 IIC 通信,在硬件设计上更加灵活。如果需要更改引脚分配,只需要修改软件代码中的引脚定义即可,而不需要对硬件进行重新设计。
稳定性
硬件 IIC:由于是硬件电路实现,其稳定性较高,能够准确地按照 IIC 协议的规范进行通信,不容易受到软件干扰或其他因素的影响。在复杂的系统中,硬件 IIC 能够可靠地与多个 IIC 设备进行通信。
软件模拟 IIC:稳定性相对较差,容易受到程序中其他代码的影响,例如在模拟 IIC 时序的过程中,如果程序被其他中断打断,可能会导致时序出现错误,从而影响通信的稳定性。
在实际应用中,对于通信速度要求较高、对稳定性要求严格且 IIC 设备数量较多的情况,通常优先选择硬件 IIC;而对于资源有限、对灵活性要求较高或通信速度要求不高的场合,可以考虑使用软件模拟 IIC。
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/m0_75187370/article/details/147099306
|
|