二、AT24C02介绍
三、AT24C02读写时序
四、AT24C02驱动步骤
一、IIC总线协议介绍
IIC(Inter-Integrated Circuit)总线协议是一种专为集成电路间通信设计的同步串行半双工通信协议。以下是关于IIC总线协议更为详细的介绍:
-
物理层特性:
- IIC总线只有两根信号线:Serial Data Line (SDA) 和 Serial Clock Line (SCL)。它们都采用开漏输出并带有内部上拉电阻的设计,这意味着总线在没有数据传输时默认处于高电平状态。
- IIC是一个真正的多主从系统,允许多个主设备和多个从设备共享同一总线。
-
通信模式:
- 半双工通信:在同一时间内,数据只能沿单一方向流动,即在一个时间点要么是发送数据,要么是接收数据,不能同时进行。
- 同步通信:数据传输的时序与一个公共的时钟信号SCL严格同步,所有设备遵循同一个时钟节拍进行数据交换。
-
通信流程:
- 起始信号:SCL保持高电平时,SDA从高电平跳变到低电平,标志着一次通信的开始。
- 停止信号:SCL保持高电平时,SDA从低电平跳变到高电平,结束一次通信过程。
- 数据传输:每个数据位都在SCL的一个时钟周期内传输,每一位数据传输过程中,SDA线上的数据必须在SCL的下降沿之前稳定,而在上升沿之后才能变化。
- 应答机制:每传输完一个字节后,接收方通过在第九个时钟周期拉低SDA线来给出ACK(确认)信号,否则发出NACK(非确认)信号。
-
寻址与通信:
- IIC总线上每个从设备都有唯一的地址,主设备通过发送地址帧选择与某个从设备通信。
- 数据传输速率可变,常见标准速率有100kHz、400kHz以及更高速率如1MHz和3.4MHz。
-
设备连接:
- IIC总线允许多个设备挂接,设备间的连接通过简单的串行总线结构,极大地简化了系统的布线复杂度和成本。
总之,IIC总线协议作为一种简洁高效的通信协议,广泛应用于传感器、存储器、微控制器等各种集成芯片间的通信,尤其适用于那些只需要少量线路连接和较低数据速率的应用场景。
1.1、IIC总线结构(拓扑图)
IIC(Inter-Integrated Circuit)总线结构具有以下特点:
-
物理结构:
- IIC总线主要由两条信号线构成,一条是时钟线(Serial Clock,SCL),另一条是数据线(Serial Data,SDA)。这两条线都需要通过上拉电阻连接到电源,确保在没有任何设备驱动总线时,SCL和SDA均为高电平状态,即总线空闲状态为高电平。
-
设备连接:
- IIC总线支持多设备连接,具备多主机系统能力,也就是说在一个总线上可以有一个或多个主设备和多个从设备。每个设备在总线上都有一个唯一的7位或10位地址,以此识别和区分不同的设备。
-
设备数量限制:
- 连接到IIC总线上的设备数量并非无限,而是受到总线最大电容400pF的限制。这是因为IIC总线的速度取决于电容负载,随着连接设备增多,总线电容增大,可能会导致信号传播延迟增加,影响通信速率和稳定性。
-
数据传输速率:
- IIC总线支持多种数据传输速率,具体包括:
- 标准模式:数据传输速率为100 kbit/s;
- 快速模式:数据传输速率为400 kbit/s;
- 高速模式(也称为快速+模式):数据传输速率为3.4 Mbit/s。
- IIC总线支持多种数据传输速率,具体包括:
在实际应用中,IIC总线因其简单易用、占用IO口少、支持热插拔和故障隔离等特点,常被用于各种嵌入式系统中不同器件间的通信,如温度传感器、EEPROM、ADC、DAC等。
1.2、IIC协议的核心特征
IIC协议的核心特征可以简要概括如下:
-
三个关键信号:
- 起始信号(Start Signal):用于标识一次IIC通信的开始,它是通过在SCL为高电平时,SDA由高电平切换到低电平来实现的。
- 停止信号(Stop Signal):用于标识一次IIC通信的结束,它是通过在SCL为高电平时,SDA由低电平切换到高电平来实现的。
- 应答信号(Acknowledgment Signal/Acknowledge):在每个字节传输结束后,从设备通过拉低SDA线来回应主设备,表示已经正确接收了数据。若从设备未应答(即SDA保持高电平),则表示接收错误或无法处理该数据。
-
两个注意事项:
- 数据有效性:在IIC通信中,数据线SDA上的数据必须在SCL时钟信号的下降沿到来之前稳定,上升沿之后才能改变,以确保数据的有效传输。
- 数据传输顺序:数据是以字节为单位进行传输的,一般来说,先是设备地址(包括读写位),然后是数据字节。并且,主设备负责发起通信、控制时钟信号以及管理数据传输的顺序。
-
一个状态:
- 空闲状态(Idle State):在IIC总线没有数据传输时,SCL和SDA均被上拉电阻拉至高电平,此时总线处于空闲状态。任何设备都不能在这段时间内向总线输出低电平,除非它要发起一个新的通信过程(即发送起始信号)。在空闲状态下,总线上的所有设备都应该释放数据线,使其保持高阻抗状态,以免干扰其他设备的通信。
1.3、IIC协议的时序
IIC协议的时序进一步详细说明如下:
-
起始信号(Start Condition, S)
- 在IIC通信开始时,首先要发出起始信号。起始条件发生在SCL线处于高电平期间,SDA线从高电平向低电平发生跳变。这标志着一次新的通信开始,主设备通过这一信号通知总线上的所有从设备准备接收或发送数据。
-
停止信号(Stop Condition, P)
- 在通信结束或中间需要暂停时,主设备会发出停止信号。停止条件同样发生在SCL线为高电平期间,但这时SDA线是从低电平向高电平跳变。这标志着本次通信结束,从设备可以释放总线,等待下一次通信开始。
-
应答信号(Acknowledgement, ACK/NACK)
- 在每次数据传输后(无论是地址还是数据字节),都会有一个应答位(ACK)或非应答位(NACK)。在主设备发出最后一个数据位之后,SCL线会产生第九个时钟脉冲,在这个时钟脉冲的高电平期间,如果从设备正确接收了数据,它会把SDA线拉低,表示发送ACK;反之,如果SDA线仍然保持在上拉电阻作用下的高电平,则表示发送NACK。
-
数据传输细节
- 数据传输是按字节(8位)进行的,而且数据在SCL线的高电平期间必须保持稳定。数据传输顺序是先传输高位(MSB,Most Significant Bit),然后依次传输低位(LSB,Least Significant Bit)。
- 每一位数据的传输过程如下:在SCL线为低电平时,SDA线上的数据位准备好;然后SCL线上升沿到来时,SDA线上数据必须稳定;最后,在SCL线为高电平期间,所有设备都要读取SDA线上的数据位,直到SCL线再次变为低电平,准备下一个数据位的传输。
总结来说,IIC协议通过严格的时序规定保证了通信的可靠性和准确性,尤其是在多设备共用一条总线的情况下,这种有序的时序控制尤为重要。
二、AT24C02介绍
AT24C02是一款基于EEPROM(Electrically Erasable Programmable Read-Only Memory,电可擦可编程只读存储器)技术的非易失性存储器件,由Atmel(现Microchip Technology Inc.)制造。它的主要特点是即使在系统断电后也能保留存储的数据,非常适合用于存储各类微处理器系统的永久性配置信息、校准参数或其他需要长期保持的重要数据。
AT24C02的具体规格如下:
-
容量:拥有2K比特(256字节)的存储空间,每个字节为8位。
-
通信接口:采用I²C(Inter-Integrated Circuit)总线协议进行数据传输,这是一种二线制同步串行总线,通信速率可以从标准模式的100 Kbps扩展到快速模式的400 Kbps。
-
地址分配:AT24C02在I²C总线上具有唯一的7位或10位地址(取决于是否使用总线地址扩展),允许在一个总线上连接多个此类设备。
-
存储操作:AT24C02支持单字节或多字节的读写操作,可通过I²C接口进行页写入(例如一次性写入连续的8个字节),提高了批量数据存储的效率。
-
耐久性与可靠性:该器件具有较高的擦写次数,可以承受多次编程和擦除操作,使用寿命较长。
-
工作电压:AT24C02的工作电压范围较广,典型值为2.5V至5.5V,能够在多种电源条件下稳定工作。
总的来说,AT24C02凭借其小巧的体积、便捷的I²C接口和可靠的非易失性存储特性,被广泛应用于各类电子产品中,用于存储系统配置、用户偏好设置以及其他需要在断电后保持的信息。
三、AT24C02读写时序
AT24C02读写时序详细说明如下:
写操作时序
-
字节写模式:
- 主设备首先通过I²C总线发送从设备地址(包括器件地址和写入标志位)并等待从设备应答。
- 主设备接着发送欲写入数据的目标内存地址,并等待从设备应答。
- 主设备发送待写入的一个字节数据到SDA线,并等待从设备应答。
- 如需写入更多不同地址的数据,主设备需要重新发送目标地址并重复上述过程。
-
页写模式:
- 同样先发送从设备地址和写入标志位,然后发送目标页内第一个数据的地址。
- 主设备发送第一个数据并等待从设备应答。
- 在不更改地址的情况下,主设备可以在同一页内连续发送最多8个字节的数据,地址会自动递增。
- 若写入的数据超出了一页(8个字节)的范围,后续数据将会覆盖前面已写入的数据,而不是进入下一页。
读操作时序
-
当前地址读模式:
- 主设备首先发送从设备地址和读出标志位。
- 从设备返回当前地址的数据,并在读取后自动递增地址。
- 如果主设备再次请求读取,将从递增后的地址开始读取。
-
随机地址读模式:
- 主设备首先发送从设备地址和写入标志位,随后发送欲读取数据的内存地址。
- 主设备再次发送从设备地址,但这次是在地址后加上读出标志位。
- 从设备应答后,主设备可以从指定地址开始读取数据,读取完成后地址自动递增。
-
顺序读模式:
- 主设备首先发送从设备地址和读出标志位,随后发送序列读取的起始地址。
- 主设备不断读取数据,而无需重新发送地址,从设备会在每次应答后自动递增地址。
- 这种模式下,主设备可以通过连续发送ACK(应答)信号来连续读取数据,直到不再需要读取时发送NACK(非应答)信号,结束读取过程。
在所有操作中,I²C协议的起始和停止信号,以及每个字节数据后的应答/非应答机制,都是非常关键的时序元素,确保了数据的正确传输和设备间的同步。
四、AT24C02驱动步骤
4.1、IIC配置步骤
在STM32等嵌入式系统中,使用软件模拟IIC通信时,配置步骤大致如下:
-
使能相关GPIO时钟:
__HAL_RCC_GPIOB_CLK_ENABLE(); // 以GPIOB为例,确保SCL和SDA所在的GPIO端口时钟已使能
-
配置GPIO工作模式:
GPIO_InitTypeDef GPIO_InitStruct; // 初始化GPIO结构体 GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7; // 假设SCL在PB6,SDA在PB7 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // SDA设置为开漏输出模式 GPIO_InitStruct.Pull = GPIO_NOPULL; // 可根据实际情况设置上下拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_STANDARD; // 选择适当的速度等级 // SDA配置 HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // 修改GPIO_InitStruct.Mode,将SCL设置为推挽输出模式 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // SCL配置 HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
-
编写基本信号函数:
- 起始信号(Start):当SCL为高电平时,SDA由高变低。
- 停止信号(Stop):当SCL为高电平时,SDA由低变高。
- 应答信号(ACK):主机在接收到数据后,于第9个SCL时钟周期拉低SDA表示应答。
- 非应答信号(NACK):主机在接收到数据后,于第9个SCL时钟周期保持SDA为高表示非应答。
- 主机发送ACK/NACK:在读取数据后,通过控制SDA在第9个SCL时钟周期的状态来发送应答或非应答信号。
- 从机等待ACK:从机在发送数据后,监听第9个SCL时钟周期SDA的状态以判断是否收到ACK。
-
编写读写函数:
- 读取一字节函数:
iic_read_byte
uint8_t iic_read_byte(I2C_HandleTypeDef *hi2c) { uint8_t data = 0; // ... 实现读取一字节数据的逻辑 ... return data; }
- 发送一字节函数:
iic_send_byte
void iic_send_byte(I2C_HandleTypeDef *hi2c, uint8_t data) { // ... 实现发送一字节数据的逻辑 ... }
在这些函数中,需要注意的是,在发送数据完成后,主机应该释放SDA线,允许从机在应答阶段拉低SDA线,同时也为下一次通信做好准备。
- 读取一字节函数:
实际上,上述描述是基于软件模拟IIC的过程,对于使用STM32CubeMX工具配置的项目,可以直接使用HAL库提供的I2C驱动函数来简化这部分工作,例如使用HAL_I2C_Master_Transmit()
和HAL_I2C_Master_Receive()
来进行读写操作。
4.2、为什么IIC总线SDA建议用开漏模式?
4.3、AT24C02配置步骤
五、编程实战
myiic.c
#include "./BSP/IIC/myiic.h"
#include "./SYSTEM/delay/delay.h"
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_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), 也可以读取外部信号的高低电平 */
}
static void iic_delay(void)
{
delay_us(2);
}
/* 起始信号 */
void iic_start(void)
{
/* SCL为高电平期间, SDA从高电平往低电平跳变*/
IIC_SDA ( 1 );
IIC_SCL ( 1 );
iic_delay( );
IIC_SDA ( 0 );
iic_delay( );
IIC_SCL ( 0 );
iic_delay( ); /* 钳住总线, 准备发送/接收数据 */
}
/* 停止信号 */
void iic_stop(void)
{
/* SCL为高电平期间, SDA从低电平往高电平跳变*/
IIC_SDA ( 0 );
iic_delay( );
IIC_SCL ( 1 );
iic_delay( );
IIC_SDA ( 1 ); /* 发送总线停止信号*/
iic_delay( );
}
/* 等待应答信号 */
uint8_t iic_wait_ack (void) /* return 1:fail 0:succeed*/
{
IIC_SDA (1); /* 主机释放SDA线 */
iic_delay( );
IIC_SCL (1); /* 从机返回ACK*/
iic_delay( );
if ( IIC_READ_SDA ) /* SCL高电平读取SDA状态*/
{
iic_stop(); /* SDA高电平表示从机nack */
return 1;
}
IIC_SCL(0); /* SCL低电平表示结束ACK检查 */
iic_delay( );
return 0;
}
/* 应答信号 */
void iic_ack(void)
{
IIC_SCL (0);
iic_delay( );
IIC_SDA (0); /* 数据线为低电平,表示应答 */
iic_delay( );
IIC_SCL (1);
iic_delay( );
}
/* 非应答信号 */
void iic_nack(void)
{
IIC_SCL (0);
iic_delay( );
IIC_SDA (1); /* 数据线为低电平,表示应答 */
iic_delay( );
IIC_SCL (1);
iic_delay( );
}
/* 发送一个字节数据 */
void iic_send_byte(uint8_t data)
{
for (uint8_t 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线 */
}
/* 读取1字节数据 */
uint8_t iic_read_byte (uint8_t ack)
{
uint8_t receive = 0 ;
for (uint8_t t = 0; t < 8; t++)
{
/* 高位先输出,先收到的数据位要左移 */
receive <<= 1;
IIC_SCL ( 1 );
iic_delay( );
if ( IIC_READ_SDA ) receive++;
IIC_SCL ( 0 );
iic_delay( );
}
if ( !ack ) iic_nack();
else iic_nack();
return receive;
}
myiic.h
#ifndef __MYIIC_H
#define __MYIIC_H
#include "./SYSTEM/sys/sys.h"
/******************************************************************************************/
/* 引脚 定义 */
#define IIC_SCL_GPIO_PORT GPIOB
#define IIC_SCL_GPIO_PIN GPIO_PIN_6
#define IIC_SCL_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 */
#define IIC_SDA_GPIO_PORT GPIOB
#define IIC_SDA_GPIO_PIN GPIO_PIN_7
#define IIC_SDA_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 */
/******************************************************************************************/
/* IO操作 */
#define IIC_SCL(x) do{ x ? \
HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* SCL */
#define IIC_SDA(x) do{ x ? \
HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* SDA */
#define IIC_READ_SDA HAL_GPIO_ReadPin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN) /* 读取SDA */
void iic_init(void);
void iic_start(void);
void iic_stop(void);
uint8_t iic_wait_ack(void);
void iic_ack(void);
void iic_nack(void);
void iic_send_byte(uint8_t data);
uint8_t iic_read_byte (uint8_t ack);
#endif
24cxx.c
#include "./BSP/IIC/myiic.h"
#include "./BSP/24CXX/24cxx.h"
#include "./SYSTEM/delay/delay.h"
void at24c02_init(void)
{
iic_init();
}
void at24c02_write_one_byte(uint8_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);
}
uint8_t at24c02_read_one_byte(uint8_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、发送非应答(获取该地址即可) */
/* 11、发送停止信号 */
iic_stop();
return rec;
}
24cxx.h
#ifndef __24CXX_H
#define __24CXX_H
#include "./SYSTEM/sys/sys.h"
void at24c02_init(void);
void at24c02_write_one_byte(uint8_t addr, uint8_t data);
uint8_t at24c02_read_one_byte(uint8_t addr);
#endif
main.c
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./USMART/usmart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/24CXX/24cxx.h"
int main(void)
{
uint8_t key;
uint8_t i = 0;
uint8_t data = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
key_init(); /* 初始化按键 */
at24c02_init();
while (1)
{
key = key_scan(0);
if (key == KEY1_PRES)
{
at24c02_write_one_byte(100, 66);
printf("write data \r\n");
}
if (key == KEY0_PRES)
{
data = at24c02_read_one_byte(100);
printf("read data:%d \r\n", data);
}
i++;
if (i % 20 == 0)
{
LED0_TOGGLE(); /* 红灯闪烁 */
i = 0;
}
delay_ms(10);
}
}
在排查IIC通信失败的问题时,查看示波器捕捉到的IIC通信波形是最直观的方法之一,但是容易出现误判的情况主要包括以下几点:
-
ACK应答信号的检查:
- 错误情况一:未能正确识别ACK信号。在IIC通信中,每个被传输的数据字节之后都会跟一个ACK应答位。主机发送完一个字节后,会在第九个SCL时钟周期释放SDA线,此时从机应当拉低SDA线表示接收正确。如果示波器上看到的是高电平,那么可能是从机没有应答或者通信线路存在问题。
- 错误情况二:忽略ACK应答位的时机。在捕获波形时,需要确保示波器设置的触发和采集窗口包含了完整的数据传输周期,特别是包含了每个数据传输后的ACK应答位。
-
上拉电阻的影响:
- 错误情况一:忘记连接或选用不当的上拉电阻。在IIC总线中,SCL和SDA线都需通过上拉电阻连接到电源,以维持空闲状态的高电平。如果没有上拉电阻或者电阻值过大,可能导致波形无法正确上升到高电平,甚至在某些情况下无法进行有效的通信。
- 错误情况二:示波器探头接地不良或耦合方式设置错误,可能导致观察到的波形只有半个周期或者幅值不对,看起来像是上拉效果不明显,但实际上是因为探头设置问题。
- 错误情况三:多个上拉电阻并联造成总阻值过大或过小。在多个设备共同连接到同一IIC总线时,各个设备自带的上拉电阻并联,需要确保总的上拉电阻在合理范围内,以保证总线的上升时间和信号质量。
因此,在查看IIC通信波形时,务必注意应答信号的正确与否以及上拉电阻是否合适,同时也要确保示波器的设置正确,这样才能有效地定位通信失败的原因。
源码
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_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;
}
myiic.h
#ifndef __MYIIC_H
#define __MYIIC_H
#include "./SYSTEM/sys/sys.h"
/******************************************************************************************/
/* 引脚 定义 */
#define IIC_SCL_GPIO_PORT GPIOB
#define IIC_SCL_GPIO_PIN GPIO_PIN_6
#define IIC_SCL_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 */
#define IIC_SDA_GPIO_PORT GPIOB
#define IIC_SDA_GPIO_PIN GPIO_PIN_7
#define IIC_SDA_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 */
/******************************************************************************************/
/* IO操作 */
#define IIC_SCL(x) do{ x ? \
HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* SCL */
#define IIC_SDA(x) do{ x ? \
HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* SDA */
#define IIC_READ_SDA HAL_GPIO_ReadPin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN) /* 读取SDA */
/* IIC所有操作函数 */
void iic_init(void); /* 初始化IIC的IO口 */
void iic_start(void); /* 发送IIC开始信号 */
void iic_stop(void); /* 发送IIC停止信号 */
void iic_ack(void); /* IIC发送ACK信号 */
void iic_nack(void); /* IIC不发送ACK信号 */
uint8_t iic_wait_ack(void); /* IIC等待ACK信号 */
void iic_send_byte(uint8_t txd);/* IIC发送一个字节 */
uint8_t iic_read_byte(unsigned char ack);/* IIC读取一个字节 */
#endif
24cxx.c
#include "./BSP/IIC/myiic.h"
#include "./BSP/24CXX/24cxx.h"
#include "./SYSTEM/delay/delay.h"
/**
* @brief 初始化IIC接口
* @param 无
* @retval 无
*/
void at24cxx_init(void)
{
iic_init();
}
/**
* @brief 在AT24CXX指定地址读出一个数据
* @param readaddr: 开始读数的地址
* @retval 读到的数据
*/
uint8_t at24cxx_read_one_byte(uint16_t addr)
{
uint8_t temp = 0;
iic_start(); /* 发送起始信号 */
/* 根据不同的24CXX型号, 发送高位地址
* 1, 24C16以上的型号, 分2个字节发送地址
* 2, 24C16及以下的型号, 分1个低字节地址 + 占用器件地址的bit1~bit3位 用于表示高位地址, 最多11位地址
* 对于24C01/02, 其器件地址格式(8bit)为: 1 0 1 0 A2 A1 A0 R/W
* 对于24C04, 其器件地址格式(8bit)为: 1 0 1 0 A2 A1 a8 R/W
* 对于24C08, 其器件地址格式(8bit)为: 1 0 1 0 A2 a9 a8 R/W
* 对于24C16, 其器件地址格式(8bit)为: 1 0 1 0 a10 a9 a8 R/W
* R/W : 读/写控制位 0,表示写; 1,表示读;
* A0/A1/A2 : 对应器件的1,2,3引脚(只有24C01/02/04/8有这些脚)
* a8/a9/a10: 对应存储整列的高位地址, 11bit地址最多可以表示2048个位置,可以寻址24C16及以内的型号
*/
if (EE_TYPE > AT24C16) /* 24C16以上的型号, 分2个字节发送地址 */
{
iic_send_byte(0XA0); /* 发送写命令, IIC规定最低位是0, 表示写入 */
iic_wait_ack(); /* 每次发送完一个字节,都要等待ACK */
iic_send_byte(addr >> 8);/* 发送高字节地址 */
}
else
{
iic_send_byte(0XA0 + ((addr >> 8) << 1)); /* 发送器件 0XA0 + 高位a8/a9/a10地址,写数据 */
}
iic_wait_ack(); /* 每次发送完一个字节,都要等待ACK */
iic_send_byte(addr % 256); /* 发送低位地址 */
iic_wait_ack(); /* 等待ACK, 此时地址发送完成了 */
iic_start(); /* 重新发送起始信号 */
iic_send_byte(0XA1); /* 进入接收模式, IIC规定最低位是0, 表示读取 */
iic_wait_ack(); /* 每次发送完一个字节,都要等待ACK */
temp = iic_read_byte(0); /* 接收一个字节数据 */
iic_stop(); /* 产生一个停止条件 */
return temp;
}
/**
* @brief 在AT24CXX指定地址写入一个数据
* @param addr: 写入数据的目的地址
* @param data: 要写入的数据
* @retval 无
*/
void at24cxx_write_one_byte(uint16_t addr, uint8_t data)
{
/* 原理说明见:at24cxx_read_one_byte函数, 本函数完全类似 */
iic_start(); /* 发送起始信号 */
if (EE_TYPE > AT24C16) /* 24C16以上的型号, 分2个字节发送地址 */
{
iic_send_byte(0XA0); /* 发送写命令, IIC规定最低位是0, 表示写入 */
iic_wait_ack(); /* 每次发送完一个字节,都要等待ACK */
iic_send_byte(addr >> 8);/* 发送高字节地址 */
}
else
{
iic_send_byte(0XA0 + ((addr >> 8) << 1)); /* 发送器件 0XA0 + 高位a8/a9/a10地址,写数据 */
}
iic_wait_ack(); /* 每次发送完一个字节,都要等待ACK */
iic_send_byte(addr % 256); /* 发送低位地址 */
iic_wait_ack(); /* 等待ACK, 此时地址发送完成了 */
/* 因为写数据的时候,不需要进入接收模式了,所以这里不用重新发送起始信号了 */
iic_send_byte(data); /* 发送1字节 */
iic_wait_ack(); /* 等待ACK */
iic_stop(); /* 产生一个停止条件 */
delay_ms(10); /* 注意: EEPROM 写入比较慢,必须等到10ms后再写下一个字节 */
}
/**
* @brief 检查AT24CXX是否正常
* @note 检测原理: 在器件的末地址写如0X55, 然后再读取, 如果读取值为0X55
* 则表示检测正常. 否则,则表示检测失败.
*
* @param 无
* @retval 检测结果
* 0: 检测成功
* 1: 检测失败
*/
uint8_t at24cxx_check(void)
{
uint8_t temp;
uint16_t addr = EE_TYPE;
temp = at24cxx_read_one_byte(addr); /* 避免每次开机都写AT24CXX */
if (temp == 0X55) /* 读取数据正常 */
{
return 0;
}
else /* 排除第一次初始化的情况 */
{
at24cxx_write_one_byte(addr, 0X55); /* 先写入数据 */
temp = at24cxx_read_one_byte(255); /* 再读取数据 */
if (temp == 0X55)return 0;
}
return 1;
}
/**
* @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++;
}
}
24cxx.h
#ifndef __24CXX_H
#define __24CXX_H
#include "./SYSTEM/sys/sys.h"
#define AT24C01 127
#define AT24C02 255
#define AT24C04 511
#define AT24C08 1023
#define AT24C16 2047
#define AT24C32 4095
#define AT24C64 8191
#define AT24C128 16383
#define AT24C256 32767
/* 开发板使用的是24c02,所以定义EE_TYPE为AT24C02 */
#define EE_TYPE AT24C02
void at24cxx_init(void); /* 初始化IIC */
uint8_t at24cxx_check(void); /* 检查器件 */
uint8_t at24cxx_read_one_byte(uint16_t addr); /* 指定地址读取一个字节 */
void at24cxx_write_one_byte(uint16_t addr,uint8_t data); /* 指定地址写入一个字节 */
void at24cxx_write(uint16_t addr, uint8_t *pbuf, uint16_t datalen); /* 从指定地址开始写入指定长度的数据 */
void at24cxx_read(uint16_t addr, uint8_t *pbuf, uint16_t datalen); /* 从指定地址开始读出指定长度的数据 */
#endif
main.c
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./USMART/usmart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/24CXX/24cxx.h"
/* 要写入到24c02的字符串数组 */
const uint8_t g_text_buf[] = {"STM32 IIC TEST"};
#define TEXT_SIZE sizeof(g_text_buf) /* TEXT字符串长度 */
int main(void)
{
uint8_t key;
uint16_t i = 0;
uint8_t datatemp[TEXT_SIZE];
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
usmart_dev.init(72); /* 初始化USMART */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
key_init(); /* 初始化按键 */
at24cxx_init(); /* 初始化24CXX */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "IIC TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "KEY1:Write KEY0:Read", RED); /* 显示提示信息 */
while (at24cxx_check()) /* 检测不到24c02 */
{
lcd_show_string(30, 130, 200, 16, 16, "24C02 Check Failed!", RED);
delay_ms(500);
lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);
delay_ms(500);
LED0_TOGGLE(); /* 红灯闪烁 */
}
lcd_show_string(30, 130, 200, 16, 16, "24C02 Ready!", RED);
while (1)
{
key = key_scan(0);
if (key == KEY1_PRES) /* KEY1按下,写入24C02 */
{
lcd_fill(0, 150, 239, 319, WHITE); /* 清除半屏 */
lcd_show_string(30, 150, 200, 16, 16, "Start Write 24C02....", BLUE);
at24cxx_write(0, (uint8_t *)g_text_buf, TEXT_SIZE);
lcd_show_string(30, 150, 200, 16, 16, "24C02 Write Finished!", BLUE); /* 提示传送完成 */
}
if (key == KEY0_PRES) /* KEY0按下,读取字符串并显示 */
{
lcd_show_string(30, 150, 200, 16, 16, "Start Read 24C02.... ", BLUE);
at24cxx_read(0, datatemp, TEXT_SIZE);
lcd_show_string(30, 150, 200, 16, 16, "The Data Readed Is: ", BLUE); /* 提示传送完成 */
lcd_show_string(30, 170, 200, 16, 16, (char *)datatemp, BLUE); /* 显示读到的字符串 */
}
i++;
if (i == 20)
{
LED0_TOGGLE(); /* 红灯闪烁 */
i = 0;
}
delay_ms(10);
}
}
六、总结