stm32—IIC

 1. IIC是什么

IIC(I2C:Inter-Integrated Circuit  集成电路总线)最初由飞利浦(Philips)半导体(后并入NXP)在1982年提出,主要目的是提供一种简单的、成本低廉的串行总线,用于MCU和电视机原件进行通信。它在最初是由一个Master(可以多Master)和可以多达127个Slave在由两根线(SDA,SCL)组成的总线上进行通讯。这样MCU就可以不用再单独和各个设备通讯,而仅仅用两根线就可以和所有外围设备愉快的通信了。这两根线分别是数据线(SDA)和时钟线(SCL)。如此这般,大大节约了芯片引脚数目,方便主板布线,节省了整体成本。良好的设计,慢慢让它得到了广泛应用


IIC也是属于通信的一种,并且也是串行通信(按 bit 收发数据),带数据应答,支持总线挂载多设备(一主多从,多主多从)

半双工:任意时刻只有一个设备可以发送数据,每个设备都有一个唯一的 I2C 地址,用来标识 I2C 设备

同步:有一个时钟信号

        好处:如果传输设备产生中断,传输双方都能定格在暂停的时刻,可以过一段时间再来继续传输,不会对传输造成影响,对时间要求不严格,可以极大的降低单片机对硬件电路的依赖,即使没有硬件电路的支持,也可以很方便的用软件手动翻转电平来实现通信

        

        异步的好处:省一根时钟线,节省资源        缺点:对时间要求严格,对硬件电路的依赖比较严重

总线:可以连接多个设备

IIC属于串行总线通信:

        只有两根线

                一根数据线        SDA:Serial DAta        串行数据线

                一根时钟线        SCL:Serial CLock      串行时钟线

SDA:串行数据线

        数据传输按 bit 位,1 bit 接 1 bit 的在 SDA 线上串行传输

        先传送最高 bit(MSB)

SCL:串行时钟线

        传递时钟信号

        为什么需要时钟线? ===> 用来同步信号用的

        同步:约定好发送数据只能在时钟线低电平时,接收(采样)数据只能在时钟线高电平时

        所以,IIC是半双工通信:因为只有一根数据线SDA,在发送数据的时候就不能接收数据,否则收的数据就是自己发出去的

        IIC通信设备都会挂载在 SDA 和 SCL 总线上,或者说在 SDA 和 SCL 总线上会挂载很多 IIC 设置。那么任意时刻,只能有一个设备向总线上发送数据,但是接收没有限制,都可以收

        为了让数据精准到达(而不是广播的形式发送),我们给 IIC 总线上的每一个设备都给一个唯一的地址,这个地址就是设备地址,用来区分不同的 IIC 设备

2. IIC物理特点

  • 它是一个支持多设备的总线。"总线" 指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机
  • 一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA),一条串行时钟线 (SCL),数据线即用来表示数据,时钟线用于数据收发同步
  • 每个连接到总线的设备都有一个独立的地址(7 / 10bit),主机可以利用这个地址进行不同设备之间的访问
  • 总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平
  • 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线
  • 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式
  • 连接到相同总线的 I2C 数量受到总线的最大电容 400pF 限制

总线仲裁:
        I2C总线上可能在某一时刻有多个主控设备要同时向总线发送START信号,这种情况叫做总线竞争,I2C总线具有多主控能力,可以对发生在SDA线上的总线竞争进行仲裁,决定谁的信号有效,其他的设备就立刻处于一个“监听模式”

其仲裁原则是这样的:

        节点在发送1位数据后,比较总线上所呈现的数据与自己发送的是否一致。是,继续发送;否则,退出竞争。SDA线的仲裁可以保证 I2C 总线系统在多个主节点同时企图控制总线时通信正常进行并且数据不丢失。总线系统通过仲裁只允许一个主节点可以继续占据总线 (建立在线与逻辑上实现的,当总线上只要有一个设备输出低电平,整条总线便处于低电平状态)

3. IIC协议(时序图)

IIC 数据通信的大概流程(时序)图:

  1. 总线空闲(空闲指没有数据通信是总线的状态)                                                                      我们约定:IIC 总线在空闲(ldle)时,SDA 和 SCL 都处于高电平(通过在总线                           上接一个上拉电阻来实现)                                                                              接下来如果有一个设备需要给另一个设备发送数据的话, 就需要一个起始信号 
  2. 起始信号:用来表示我要往总线上发送数据啦!                                                                      SCL 时钟线保持高电平                                                                                                    SDA 数据线从高到低的跳变                                                                                    
    例子:模拟 IIC 的起始信号(在没有I2C控制器的情况下,如C51):    
        /* 空闲 */
        SCL = 1;
        SDA = 1;
        delay();
        /* 起始信号 */
        SDA = 0;
        delay();
    有没有可能两个或两个以上设备同时发送起始信号呢?                                                           有可能,所以需要总线仲裁:决定谁的信号有效                                                                      如果有两个或两个以上的设备同时发送START信号,这个时候,就需要 "总线仲裁",它会决定谁的 START 信号是有效的,其他的设备就立刻处于一个 "监听" 模式                                                                                                                                                    比如:在发送起始信号前,判断 IIC 总线是否空闲                                                 怎么做:                                                                                                                                    time_out  = SCL_T;// 超时时间为一个 SCL 周期                                                          while (SCL == 1 && SDA == 1 && time_out--);                                                 解析:如果一个 SCL 周期内,SCL 和 SDA 都是高电平,那么说明                                          就没有人往总线上发送数据
  3. 发送数据:user data,device data                                                                                        这个数据包含用户真正发送的数据,也包括设备地址(指定通信方)                              因为总线上有多个设备,其中一个发起一个起始信号,表示它要跟总线上的某个设备或多个设备通信                                                                                                                它到底跟谁通信呢?如果不指定,总线上所有设备都可以收到数据                                所以 IIC 协议规定,每个 IIC 总线上的设备都必须有一个 IIC 设备地址(7bits / 10bits),并且,同一个 IIC 总线上的设备地址必须不一样                                                                                                                                                                                                     IIC中数据(包括设备地址)的发送都是按 8bits 进行发送                                               设备的地址 = 7bits + R / W#(读写位,占最低 1bit ---> bit0)                                                  bit0:0    W    表示"我"要给指定地址的设备写入数据                                                    bit0:1    R     表示"我"要从指定地址的设备里读取数据                                     例如:设备 B 的地址是 101 0001,CPU 要发送数据 0x55 给设备A                                           CPU:START  1010  0001  0101  0101                                                                           1010 001:地址    0:写 (发送)   0101 0101:发送的数据                                                                                                                                                                             发送完一个字节(8bits)数据后,对方(接收方)必须要返回一个 ACK(应答位)                                                                                                                                              ACK:在 SDA 数据线上的第 9 个时钟周期,接收方给 SDA 一个低电平                                但是这里存在一个问题,就是如果数据的最后一个 bit 本身就是一个低电平,那么 SDA 线此时的电平状态就是 0,这个时候,不管接收方应答还是不应答,发送方可能都会认为对方应答啦。怎么解决:                                                                                         发送方在发送完 8bits 数据后,一般都会释放 SDA 数据线(SDA = 1)                         在第 9 个时钟周期时,接收方就会给 SDA 一个低电平表示应答(表示我已经收到了)                                                                                                                                             例如:                                                                                                                                       CPU(发送方):STATR   1010 0010            0101  0101                                                      A(接收方):                                    ACK                     ACK                                                                                                                                               

    总结数据的发送规则:

            数据发送其实就是根据要发送的数据的 bit 位 的情况给 SDA 线低电平或高电平            先发送 MSB(最高位)

    发送数据时,更改数据线的要求如下:

            IIC 协议定:                                                                                                                            在 SCL 时钟线低跳变的时候,可以改变 SDA 数据线电平                                                    所以发送是下降沿触发,每个下降沿可以发送 1bits 数据                                          在 SCL 时钟线高电平的时候,SDA 数据线保持稳定                                                            所以接收是上升沿触发,每个上升沿到来,就会去 SDA 上采集 1bits 数据

  4. 停止信号:STOP                                                                                                                      SCL    保持高电平                                                                                                            SDA   从低电平到高电平跳变 

所以一帧 IIC 数据如下:

发送:START + data(7bit addr + 1bit 0W)              + data(8bit data)               + ... + STOP

                                                                  1bit ACK                            1bit ACK

接收:START + data(7bit addr + 1bit 1R)               + data(8bit data)               + ... + STOP

                                                                  1bit ACK                            1bit ACK

有另外一个问题:

        SDA 线一般是谁要发送数据出去就由谁来控制,那么 SCL 时钟线应该由谁来控制?

                谁控制都可以,只要不同时控制,但是很多设备,不具备控制时钟的能力

                因为它可能没有时钟单元(没有时钟输出功能)。所以,在STM32中一般是由CPU作为时钟输出(控制者)

                所以通过谁控制 SCL 线我们为 IIC 通信设备区分不同的角色:

                        IIC 主设备:Master

                                在一次 IIC 通讯过程中,产生 IIC 时钟输出的设备,它控制 IIC 总线的传输速率

                        IIC 从设备:Slave

                                在一次 IIC 通讯过程中,被动接收 IIC 时钟的设备

                细分的话就会有:Master-send   主发        Master-Receive  主收

                                                     就是说时钟提供者既可以收也可以发

                                             Slave-Send    从发        Slave-Receive  从发

                   

        IIC 总线上的时钟频率一般在 几十K hz — 400K hz,频率越低通信速度越慢,但是越稳定,"就低不就高"


IIC 时序图如下:

 

4. IIC模拟时序

在一些芯片上(如:C51)它没有 IIC 总线,也没有 IIC 控制器,那么它能不能和一个 IIC 接口的模块进行通信呢?

        当然可以,我们只需要用两个 GPIO 口来模拟 SDA 和 SCL 即可

/*
    IIC_Send_Start:发送 IIC 起始信号
*/
void IIC_Send_Start(void) {
    /* 空闲 */
    SCL = 1;
    SDA = 1;
    delay(IIC_T); // IIC_T:IIC时钟信号的周期,delay(IIC_T):延时一个时钟周期
    /* 起始 */
    SDA = 0;
    delay(IIC_T);
}

/*
    IIC_Send_Stop:发送 IIC 停止信号
        SCL  保持高电平
        SDA  从低电平到高电平跳变
*/
void IIC_Send_Stop(void) {
    
    SCL = 1;
    SDA = 0;
    delay(IIC_T);
 
    SDA = 1;
    delay(IIC_T);
}

/*
    "Master-Send":主发
    
    IIC_Send_Byte:	将一个字节的数据发送出去
    @ch:要发送的数据,1个字节
    @返回值:
        发送并成功返回1(表示接收方收到回了一个ACK)
        失败返回0(表示接收方没有收到,没有返回ACK)
*/
int IIC_Send_Byte(unsigned char ch) {
    
    /* MSB(最高位先发),并且是在SCL下降沿时发送1bit */
    int i;
    for (i = 7; i >= 0; i--) { // 8个SCL时钟周期发送8bits
        SCL = 0; // 周期开始,下降沿发送
        SDA = (ch >> i) & 0x01;	// 发送一个bit过去
        delay(IIC_T / 2); // 等待约半个时钟周期
        SCL = 1; // 对方在上升沿采集
        delay(IIC_T / 2); // 延时等待一会让对方有时间接收
    }

    /* 发送方在发送完8bits数据后,一般都会释放SDA数据线 */
    /* 同时第9个时钟周期,等待接受方回应一个ACK低电平 */
    SCL = 0; // 第9个周期开始
    SDA = 1; // 释放SDA数据线
    delay(IIC_T / 2); // 等待接收方应答,接收方回复ack(SDA -> 0)
    SCL = 1;
    if (SDA) {
        return 0; // 代表无人应答,发送失败
    } else {
        return 1; // 代表有人应答,发送成功
    }
    delay(IIC_T / 2);
}

/*
     "Master-Receive":主收
    
    IIC_Recv_Byte:从IIC总线上接收一个字节
    @返回值:将接收到的字节返回
*/
unsigned char IIC_Recv_Byte(void) {
    // 接收:先接收到的是最高bit,陆续收到 8个bit,在第九个周期发送ACK
    unsigned char ch = 0;
    int i = 0;
    for (i = 7; i >= 0; i--) {
        SCL = 0; // 给半个周期的低电平,让对方发送数据
        delay(IIC_T / 2);
        SCL = 1; // 拉高准备去采集数据
        if (SDA) {
            ch |= 1 << i; // 如果接收到的是高电平,则将对应bit位置为1
        }
        delay(IIC_T / 2);
    }
    /* 接收到8个bit后,应该要回复应答 */
    SCL = 0;
    delay(t); // 延时一段非常短的时间让对方释放数据线
    SDA = 0; // 回复应答信号ACK
    delay(IIC_T / 2 - t); // 以上操作总共花掉半个时钟周期
    SCL = 1; // 让对方采集应答信号
    delay(IIC_T / 2);
    return ch;
}

/*
    IIC_Write_Data:向指定的IIC设备写入数据
    @addr:7bit的目标IIC设备的地址
    @str:要发送的数据字符串
    @len:要发送的数据字符串的长度
    @返回值:发送成功返回1,失败返回0
*/
int IIC_Write_Data(unsigned char addr, char *str, int len) {
    
    /* 发送起始信号 */
    IIC_Send_Start();					
    /* 发送设备地址 */	
    // bit0->0表示写入数据   bit1-bit7:IIC设备地址
    int ret = IIC_Send_Byte((addr << 1) | 0);
    if (ret == 0) {	// 代表没有应答
        IIC_Send_Stop(); // 发送停止信号
        return 0;
    }

    /* 发送数据 */
    int i;
    for (i = 0; i < len; i++) {
        ret = IIC_Send_Byte(str[i]);
        if (ret == 0) { // 代表没有应答
            IIC_Send_Stop(); // 发送停止信号
            return 0;
        }
    }
    /* 发送停止信号 */
    IIC_Send_Stop();
    return 1;
}

5. STM32F4xx IIC控制器

STM32F4xx 有三个 IIC 控制器,有三条 IIC 总线

IIC 控制器是一个 IIC 的设备,它负责产生 IIC 的时序以及协议逻辑

在STM32F4xx中,CPU 与 IIC 是通过系统总线通信的

如果没有 IIC 控制器,那么 CPU 只能通过 GPIO 口来模拟,即 IIC 的时序、协议逻辑等等都需要软件代码去模拟

        比如:8051 单片机就没有 IIC 控制器,所以,C51 的 IIC 都是通过软件代码模拟实现的


IIC 控制器原理,大概如图:

        CR:Control  Register  控制寄存器
        SR:Status  Register    状态寄存器


STM32F4xx 的 IIC 控制器既可以作为主模式又可以作为从模式:

        作为主模式的主发时序图(7bit):


        作为主模式的主收时序图(7bit):

6. STM32F4xx I2C固件库函数

IIC 控制器 的 SDA 和 SCL 其实都是通过 GPIO 引脚复用功能而来的

(1) 初始化 I2C 引脚
a. 使能 GPIO 分组时钟
    RCC_AHB1PeriphClockCmd();

b. SDA 和 SCL 对应的 GPIO 初始化为复用模式
    GPIO_Init();  【SDA只能配置为输出开漏】

c. 配置 GPIO 复用成什么功能
    GPIO_PinAFConfig();

/*
    SCL和SDA线都需要具有能够输出高低电平的能力
    SCL对应的GPIO口配置为开漏或者推挽都可以,因为根据原理图可知
    SCL和SDA上都已经外接了上拉电阻具有输出高低电平的能力
    但是SDA对应的GPIO口却只能配置为开漏模式,我们知道SDA不仅需要
    具有输出高低电平的能力还要具有获取输入的能力(比如要去获取应答)
    通过中文手册183页<7.3.10输出配置>可知,在GPIO配置为输出模式时,
    实际上输入回路上的施密特触发器是处于开启状态的,也就意味着此时
    GPIO是能够获取输入的,但如果配置为输出推挽,当要实现输入检测时,
    就会受到输出电路没有关闭的影响,因为之前的输出电平是存在的,造成
    输入电路和输出电路的短接等现象,所以只能配置为开漏模式,就算当CPU输
    出1,由于N-MOS管处于关闭状态,IO端口的电平将完全由外部电路决定,因此
    CPU可以在输入数据寄存器中读到外部电路的信号而不是自己输出的1   
*/
(2) 初始化 I2C 控制器
a. 使能 IIC 时钟
    RCC_AHB1PeriphClockCmd();
        or
    RCC_AHB2PeriphClockCmd();

b. 初始化 IIC
    
void I2C_Init(I2C_TypeDef *I2Cx, I2C_InitTypeDef *I2C_InitStruct);

@I2Cx:指定IIC控制器编号
        I2C1、I2C2、I2C3

@I2C_InitStruct:指向I2C初始化信息结构体

    typedef struct
	{
		uint32_t I2C_ClockSpeed; 
			指定IIC总线通信时钟频率
			100K ~ 400K 越低越稳定,但速度也越慢

		uint16_t I2C_Mode;              
			指定IIC模式
				I2C_Mode_I2C		  	I2C模式  <--- 选这个
				I2C_Mode_SMBusDevice  	设备模式
				I2C_Mode_SMBusHost	  	主机模式

		uint16_t I2C_DutyCycle; 
			指定时钟线低电平和高电平的比率
				I2C_DutyCycle_16_9		低电平/高电平 = 16/9  <--- 选这个
				I2C_DutyCycle_2			低电平/高电平 = 2/1

		uint16_t I2C_OwnAddress1;  
			指定I2C控制器的地址,根据I2C_AcknowledgedAddress来定
            一般随便指定,但是要与其它设备不同
			只有在作为从设备时用得到(作为主设备不需要)

		uint16_t I2C_Ack;
			在收到I2C总线的数据时,是否回复ACK  ===> 根据模块手册选择
				I2C_Ack_Enable   	回复
				I2C_Ack_Disable		不回复

		uint16_t I2C_AcknowledgedAddress; 
			指定I2C控制器自身地址长度,是7bits还是10bits的
				I2C_AcknowledgedAddress_7bit		地址是7bits
				I2C_AcknowledgedAddress_10bit		地址是10bits


	} I2C_InitTypeDef;
(3) 配置 I2C 控制器的其他功能
比如:中断等(暂时用不到),其中有一个函数接下来会用到:
    
配置I2C控制器是否产生应答位
void I2C_AcknowledgeConfig(I2C_TypeDef *I2Cx, FunctionalState NewState);

    @I2Cx:指定IIC控制器编号

    @NewState:
        ENABLE:当主机收到一个字节的数据后,会自动发送一个ACK应答位
        DISABLE:当主机收到一个字节的数据后,不会产生ACK应答位
(4) 开启 I2C 控制器
I2C_Cmd();
(5) I2C 总线读写流程
a. 发送起始信号
    void I2C_GenerateSTART(I2C_TypeDef *I2Cx, FunctionalState NewState)
================================================================================
b. 获取指定事件
    
ErrorStatus I2C_CheckEvent(I2C_TypeDef *I2Cx, uint32_t I2C_EVENT)

    @I2Cx:指定I2C控制器编号
        
    @I2C_EVENT:指定要获取的事件

        /* --EV5 */	发送起始信号后的应答事件
        I2C_EVENT_MASTER_MODE_SELECT

        /* --EV6 */ 	发送从地址的应答事件
        I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED	主发模式
        I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED		主收模式

        /* --EV7 */ 	主机可以读数据啦
        I2C_EVENT_MASTER_BYTE_RECEIVED
            
        /* --EV8 */	
        I2C_EVENT_MASTER_BYTE_TRANSMITTING	数据正在发送中

        /* --EV8_2 */
        I2C_EVENT_MASTER_BYTE_TRANSMITTED	数据已经发送完成

        具体要等待的事件根据时序图来找宏

@返回值:
    ERROR		获取的事件未发生
    SUCCESS	    获取的事件已经发生
--------------------------------------------------------------------------------
比如:发送起始信号后需要等待EV5

I2C_GenerateSTART(I2C1, ENABLE);	// 向I2C1发送起始信号
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
================================================================================
c. 发送一个7bits的从设备地址

void I2C_Send7bitAddress(I2C_TypeDef *I2Cx, uint8_t Address, 
                                                uint8_t I2C_Direction)

    @I2Cx:指定I2C控制器编号

    @Address:7bit设备地址,【注意:在填入参数的时候需要左移一位】

    @I2C_Direction:读写模式
        I2C_Direction_Transmitter	发送数据模式
        I2C_Direction_Receiver		接收数据模式
================================================================================
d. 发送数据
    
void I2C_SendData(I2C_TypeDef *I2Cx, uint8_t Data)
			
    @I2Cx:指定I2C控制器编号
		
    @Data:要发送的数据,1个字节
================================================================================
e. 接收数据
    
uint8_t I2C_ReceiveData(I2C_TypeDef *I2Cx)

    @返回值:返回从I2C接收到的1个字节数据
================================================================================
f. 产生停止信号
    void I2C_GenerateSTOP(I2C_TypeDef *I2Cx, FunctionalState NewState)
================================================================================
g. 获取I2C控制器的状态标志
    
FlagStatus I2C_GetFlagStatus(I2C_TypeDef *I2Cx, uint32_t I2C_FLAG)

    @I2Cx:指定I2C控制器编号

    @I2C_FLAG:指定状态标志位
            
        I2C_FLAG_BUSY	// 表示I2C总线是否忙碌
            如果被设置,则表示总线忙碌,不能发送起始信号
            所以在发送起始信号前,需要判断总线是否忙碌

如:
/* 检测I2C总线是否忙绿 */
while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY) == SET);
/* 不忙碌才能发送起始信号 */
================================================================================
h. 清除I2C控制器的状态标志
    void I2C_ClearFlag(I2C_TypeDef *I2Cx, uint32_t I2C_FLAG)

7. AT24C02

在M4开发板有一个采用 IIC 进行通信的 EEPROM 存储器芯片 AT24C02

EEPROM:是一个小容量的存储器芯片,一般只存储 几K 的数据,在实际产品应用中,一般用来存储一些其它模块的 ID,MAC,版本号 ......


所以我们可以验证一下:看是否能写入数据到AT24C02中,再读取出来

AT24C02详情请看数据手册,在中文手册中介绍了:

        AT24C02/AT24C04/AT24C08/AT24C16/AT24C32/AT24C64
        后面那个一直变化的数字表示的是其存储容量

        如:AT24C02的存储容量为2K


数据手册 (<<24C02中文资料.PDF>>)中应该懂的知识点:

a. 器件地址 (IIC 设备地址)

        

        AT24C02:7bits地址

        根据<图8:器件地址>可知24C02的地址为:
                1 0 1 0 A2 A1 A0 R/W

        高四位的地址是固定的,其中A2/A1/A0是器件地址位,由外部输入信号的电平决定
                通过M4原理图可知,M4上的24C02的A0/A1/A2引脚接地

                
                IIC_SCL ----- I2C1_SCL ----- PB8               IIC_SDA ----- I2C1_SDA ----- PA9
                所以M4上的24C02的设备地址为:

                        1 0 1 0 0 0 0 R/W
        为什么AT24C02不把它的 I2C 设备地址写死呢? 为什么要浪费我三个引脚呢?
                写死就有可能与其他设备冲突,因为目前 I2C 设备地址都是由各个厂商来自行定的。 通过 A0/A1/A2 可以设置成不同的设备地址,IIC 中每个设备的地址都必须唯一 


b. 内部存储结构 (大小多少 片内地址)                

        24C02一共 2K bits (2*1024/8 = 256 Bytes),分为 32 Pages,每页 8 Bytes
        每页有自己的页地址(2*2*2*2*2 = 32 ---> 5bits ---> 因为一共有32页)

        每个字节也有字节地址(2*2*2 = 8 ---> 3bits ---> 因为一页一共有8字节)

        所以24C02的存储单元地址为:8bits = 5bits_pd + 3bits_wd
                word_addr(8bit)        b7 b6 b5 b4 b3          b2 b1 b0

                                                         页码                      页内地址


c. AT24C02的读写操作

AT24C02的读写操作可以分为:
    写操作时序:
        1. 字节写(写一个字节)
        2. 页写(写一页)
    
    应答查询:
    
    读操作时序:
        1. 当前地址读
        2. 随机读
        3. 顺序读

写操作时序:

1. 字节写(写一个字节)  // 在手册第8页

举个例子:

        假设需要往 AT24C02 内部字节地址为 0x55 处写一个数据 0xAA

MCU:START  从设备地址(1010 000 0/W)       字节地址(0x55)        数据(0xAA)       STOP

24C02:                                                        A                               A                        A 

2. 页写(写一页 ---> 8Bytes)

        AT24C02一页有8个字节,一次写操作最多可以连续写8个字节

        AT24C02在页写时序时,每写入一个字节后,页内地址会自动+1
        需要注意的是地址仅低 3bit 加 1,所以不能跨页

举个例子:

① 向0x00地址处写入”12345678”

        word_addr  数据

        00000 000   1

        00000 001   2

        00000 010   3

        ...

        00000 111   8

② 向0x03地址处写入”12345678”

        word_addr  数据

        00000 011   1

        00000 100   2

        00000 101   3

        00000 110   4

        00000 111   5

        00000 000   6  ---> 超出页码后,后面写入的数据会覆盖前面的

        00000 001   7

        00000 010   8

举个例子:

        假设需要往AT24C02内部字节地址为0x00处写入数据0x01,0x02 ... 0x08

        那如果我想在0x05—0x0C上写入0x01,0x02 ... 0x08该怎么办呢?

                只能分两次写入,先在0x05地址上写入0x01,0x02,0x03

                再去0x08地址上写入0x04,0x05,0x06,0x07,0x08

3. 应答查询

读操作时序(可以跨页):

        读操作与写操作初始化相同,只是器件地址中的 读 / 写 选择位应为 "1"

        内部地址计数器保存着上次访问(已经操作过了)时最后一个地址加1的值,只要芯片有电,该地址就一直保存

1. 当前地址读

2. 随机读(指定读)

        一般在读取之前,需要写一个字地址(伪写),此时还未操作,内部地址计数器保存着这个地址,表示下次从设备的哪里开始读。如果读之前,不写字节地址,而直接读就会从芯片内部的(word_addr)处开始读     word_addr相当于光标

3. 顺序读

代码实现:

AT24C02.h

#ifndef __AT24C02_H__
#define __AT24C02_H__

#include "stm32f4xx.h"
#include "usart.h"
#include "systick.h"

/* 
	初始化AT24C02的通信端口
		IIC_SCL ----- I2C1_SCL ----- PB8   
		IIC_SDA ----- I2C1_SDA ----- PA9
*/
void AT24C02_Init(void);

/*
    等待某个事件发生(有限等待,不能死等)
        @I2Cx:要等待是哪个总线上的设备
        @I2C_EVENT:要等待的是什么事件
        @timeout:最多等待多久(超时时间,单位us)
        返回值:
			等待的事件发生返回0
			没有发生返回-1
*/
int Wait_IIC_Event(I2C_TypeDef *I2Cx, uint32_t I2C_EVENT, int timeout);

/* 
	用来判断 总线 是否忙碌
		@返回值:
			能通信返回0
			不能通信返回-1
*/
int AT24C02_Is_Busy(void);

/*
	等待 AT24C02 内部写完成
	将数据发送过去之后,24C02需要时间写入到addr指定的内部存储器上
	此时我们可以启动应答查询(24C02手册第9页<3.应答查询>)
		返回值:
				写入完成    0 
				还在写		-1
	
	时序:
			MCU:START   IIC_DEV_ADDR 
		AT24C02:                     A
*/
int Wait_24C02_Finished_To_Write(void);

/*
字节写(写一个字节):
    用来往AT24C02指定的存储空间中写入一个字节的数据
        @addr:指定要写入到AT24C02的哪个存储单元中去
        @data:要写入的数据(一个字节)
        @返回值:
			成功返回0
			失败返回-1

	时序:
		MCU:START 从设备地址(1010 0000/w)   字节地址(0x55)   数据(0xAA)   STOP
	AT24C02:                              A                A            A      
*/
int Write_A_Byte_To_24C02(u8 addr, u8 data);

/*
	用来从AT24C02指定的存储空间中获取一个字节的数据
		@addr:指定要访问的AT24C02的存储单元地址(字地址)
        @返回值:那个字节地址上的数据
*/
u8 Read_A_Byte_From_24C02(u8 addr);

/*
    用来从AT24C02中读取数据
        @addr:字地址,指定从哪个地址开始读取数据
        @data:从24C02中读取出来的数据保存到其指向的空间中去
        @count:要读取的字节数
        @返回值:成功返回读取到的字节数,失败返回-1
*/
int Read_Bytes_From_24C02(u8 addr, u8 *data, int count);

/*
页写:
    用来向AT24C02中写入数据
        @addr:写入数据的起始地址
        @data:要写入的数据
        @count:要写入的字节数
        @返回值:成功返回写入的字节数,失败返回-1
*/
int Write_Bytes_To_24C02(u8 addr, u8 *data, int count);

#endif

AT24C02.c

#include "AT24C02.h"

/*
	初始化 AT24C02 的通信端口
		IIC_SCL ----- I2C1_SCL ----- PB8
		IIC_SDA ----- I2C1_SDA ----- PB9
*/
void AT24C02_Init(void) {
	
	// (1)初始化 I2C 引脚
	// a.使能 GPIO 分组时钟
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
	
	// b.SDA 和 SCL 对应的 GPIO 初始化为复用模式
	GPIO_InitTypeDef g;
	
	g.GPIO_Mode = GPIO_Mode_AF; // 复用功能
	g.GPIO_OType = GPIO_OType_OD; // SDA只能配置成输出开漏
	/*
		SCL和SDA线都需要具有能够输出高低电平的能力
		SCL对应的GPIO口配置为开漏或者推挽都可以,因为根据原理图可知
		SCL和SDA上都已经外接了上拉电阻具有输出高低电平的能力
		但是SDA对应的GPIO口却只能配置为开漏模式,我们知道SDA不仅需要
		具有输出高低电平的能力还要具有获取输入的能力(比如要去获取应答)
		通过中文手册183页<7.3.10输出配置>可知,在GPIO配置为输出模式时,
		实际上输入回路上的施密特触发器是处于开启状态的,也就意味着此时
		GPIO是能够获取输入的,但如果配置为输出推挽,当要实现输入检测时,
		就会受到输出电路没有关闭的影响,因为之前的输出电平是存在的,造成
		输入电路和输出电路的短接等现象,所以只能配置为开漏模式,就算当CPU输
		出1,由于N-MOS管处于关闭状态,IO端口的电平将完全由外部电路决定,因此
		CPU可以在输入数据寄存器中读到外部电路的信号而不是自己输出的1
	*/
    g.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
	g.GPIO_PuPd = GPIO_PuPd_UP; // 引脚外部已经接了一个上拉电阻
	                            // 再配置为上拉可以提高IIC的驱动能力
	g.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &g);
	
	// c.配置 GPIO 复用成什么功能
	GPIO_PinAFConfig(GPIOB, GPIO_PinSource8, GPIO_AF_I2C1);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_I2C1);
	
	// (2)初始化 IIC 控制器
	// a.使能 IIC 时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
	
	// b.初始化IIC
	I2C_InitTypeDef i;
	
    i.I2C_Ack = I2C_Ack_Enable;
    i.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	// 通过24C02说明书可知5V时24C02频率为1MHz,一般为400KHz
    i.I2C_ClockSpeed = 400000;
    i.I2C_DutyCycle = I2C_DutyCycle_16_9;
    i.I2C_Mode = I2C_Mode_I2C;
	
    I2C_Init(I2C1, &i);
	
	// (3)配置 I2C 控制器的其他功能
	// 如:中断配置,ACK配置...
    // I2C_AcknowledgeConfig(I2C1, DISABLE); // 可以设置为不自动回ACK
	
	// (4)开启 I2C 控制器
	I2C_Cmd(I2C1, ENABLE);
}

/*
    等待某个事件发生(有限等待,不能死等)
        @I2Cx:要等待是哪个总线上的设备
        @I2C_EVENT:要等待的是什么事件
        @timeout:最多等待多久(超时时间,单位us)
        返回值:
			等待的事件发生返回0
			没有发生返回-1
*/
int Wait_IIC_Event(I2C_TypeDef *I2Cx, uint32_t I2C_EVENT, int timeout) {
	
    // 当事件没有发生或者没有超时的时候继续等待
    while (I2C_CheckEvent(I2Cx, I2C_EVENT) == ERROR && timeout--) {
        // 每隔1us判断一次事件有没有产生或者有没有超时
        // 所以超时时间为(timeout)us,即这个函数只会等待(timeout)us
        delay_us(1);
    }
    return (timeout == -1) ? -1 : 0;
}

/*
	用来判断 总线 是否忙碌
		@返回值:
			能通信返回0
			不能通信返回-1
*/
int AT24C02_Is_Busy(void) {
	
	// 循环判断20次是否总线繁忙
	int time = 20;
	while (time--) {
		// 发送起始信号
		I2C_GenerateSTART(I2C1, ENABLE);
		if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) {
            // 等待超时,事件EV5(发送起始信号后的应答事件)并未产生
            I2C_GenerateSTOP(I2C1, ENABLE); // 发送停止信号
            continue; // 发起下一轮繁忙判断
        }
		// 总线此时不繁忙,发送 7bit 的设备地址
		// 主要目的是为了测试IIC总线上有没有这个设备以及此设备是否坏了
		I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
		if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, 1000) == 0) {
			// 代表EV6事件(发送从地址的应答事件)发生
			I2C_GenerateSTOP(I2C1, ENABLE);
			return 0;
		}
		I2C_GenerateSTOP(I2C1, ENABLE); // 发送停止信号
	}
	return -1;
}

/*
	等待 AT24C02 内部写完成
	将数据发送过去之后,24C02需要时间写入到addr指定的内部存储器上
	此时我们可以启动应答查询(24C02手册第9页<3.应答查询>)
		返回值:
				写入完成    0 
				还在写		-1
	
	时序:
			MCU:START   IIC_DEV_ADDR
		AT24C02:                     A
*/
int Wait_24C02_Finished_To_Write(void) {
	
    int cnt = 20;
	while (cnt--) {

        // 判断总线是否繁忙
        if (AT24C02_Is_Busy() == -1) {
			// 打印提示信息
            printf("IIC Bus is Busy!\r\n");
            continue;
        }
        // 总线此时不繁忙,发送起始信号
        I2C_GenerateSTART(I2C1, ENABLE);
		
        // 等待事件EV5(发送起始信号后的应答事件)产生
        if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) {
            // 发送停止信号
			I2C_GenerateSTOP(I2C1, ENABLE);
            continue;
        }
		
        // 发送从设备地址(器件地址)
        I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
		
        // 等待事件EV6(发送从地址的应答事件)发生
        if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, 1000) == -1) {
			// 内部写未完成
            I2C_GenerateSTOP(I2C1, ENABLE);
            continue; // 没有应答的话开始下一次判断
        }
		I2C_GenerateSTOP(I2C1, ENABLE);
        return 0; // 有应答代表写入完成
    }
    return -1;
}

/*
字节写(写一个字节)
    用来往AT24C02指定的存储空间中写入一个字节的数据
        @addr:指定要写入到AT24C02的哪个存储单元中去
        @data:要写入的数据(一个字节)
        @返回值:
			成功返回0
			失败返回-1

	时序:
		MCU:START 从设备地址(1010 0000/w)   字节地址(0x55)   数据(0xAA)   STOP
	AT24C02:                              A                A            A      
*/
int Write_A_Byte_To_24C02(u8 addr, u8 data) {
	
	// 1. 先等IIC总线不繁忙(Not Busy)时才能发送数据
	// while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY) == SET);
	// 上面为第一种方式通过标志位去判断总线是否繁忙,但这种方式判定过于简单
	// 实际工程中,一般不会死等(万一要是从设备是坏的就会死循环),而是限时等待
	// 第二种方式就是通过事件去判断总线是否繁忙
	if (AT24C02_Is_Busy() == -1) {
        printf("IIC Bus is Busy!\r\n");
        return -1;
    }

	// 发送起始信号
	I2C_GenerateSTART(I2C1, ENABLE);
	
	// 等待事件 EV5(发送起始信号后的应答事件)产生
	if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) {
		// 发送停止信号
		I2C_GenerateSTOP(I2C1, ENABLE);
		return -1;
	}
	
	// 发送从设备地址(器件地址)
	I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
	
	// 等待事件EV6(发送从地址的应答事件)发生
    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, 1000) == -1) {
		// 发送停止信号
		I2C_GenerateSTOP(I2C1, ENABLE);
        return -1;        
    }
	
	// 发送字地址
    I2C_SendData(I2C1, addr);
	
	// 等待事件EV8_2(数据发送完成)
    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, 1000) == -1) {
		// 发送停止信号
		I2C_GenerateSTOP(I2C1, ENABLE);
        return -1;         
    }
	
	// 发送真正要存储的数据
    I2C_SendData(I2C1, data);
	
	// 等待事件EV8_2(数据发送完成)
    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, 1000) == -1) {
		// 发送停止信号
        I2C_GenerateSTOP(I2C1, ENABLE);
        return -1;         
    }

    // 停止信号
    I2C_GenerateSTOP(I2C1, ENABLE);

    // 将数据发送过去之后,24C02需要时间写入到addr指定的内部存储器上
    // 此时我们可以启动应答查询(24C02手册第9页<3.应答查询>)
	
    int flag = Wait_24C02_Finished_To_Write();

    printf("flag = %d\r\n", flag);
    
    return 0;	
}

/*
	用来从AT24C02指定的存储空间中获取一个字节的数据
		@addr:指定要访问的AT24C02的存储单元地址(字地址)
        @返回值:那个字节地址上的数据
*/
u8 Read_A_Byte_From_24C02(u8 addr) {
	
	// 先等 IIC 总线不繁忙(Not Busy)时才能发送数据
	if (AT24C02_Is_Busy() == -1) {
		printf("IIC Bus is Busy!\r\n");
		return 0;
	}
	
	// 发送起始信号
    I2C_GenerateSTART(I2C1, ENABLE);
	
	// 等待事件EV5(发送起始信号后的应答事件)产生
    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1){
        // 发送停止信号
		I2C_GenerateSTOP(I2C1, ENABLE);
        return 0;
    }
	
	// 发送从设备地址(1010 0000/w)
    I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
	
	// 等待事件EV6(发送从地址的应答事件)发生
    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, 1000) == -1) {
        // 发送停止信号
		I2C_GenerateSTOP(I2C1, ENABLE);
        return 0;        
    }
	
	// 发送读取的数据的地址
	I2C_SendData(I2C1, addr);
	
	// 等待事件EV8_2(数据发送完成)
    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, 1000) == -1) {
		// 发送停止信号
        I2C_GenerateSTOP(I2C1, ENABLE);
        return 0;    
    }
	
	// 发送起始信号
    I2C_GenerateSTART(I2C1, ENABLE);
	
	// 等待事件EV5(发送起始信号后的应答事件)产生
    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) {
        // 发送停止信号
		I2C_GenerateSTOP(I2C1,ENABLE);
        return 0;
    }
	
	// 发送从设备地址(1010 0001/r)
    I2C_Send7bitAddress(I2C1, 0xA1, I2C_Direction_Receiver);
	
	// 等待事件EV7(主机有数据可以读)发生
    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED, 1000) == -1) {
        // 发送停止信号
		I2C_GenerateSTOP(I2C1, ENABLE);
        return 0;
    }
	
	// 读取数据
    unsigned char data = I2C_ReceiveData(I2C1);
	
	// 发送停止信号
    I2C_GenerateSTOP(I2C1, ENABLE);

	
	return data;
}

/*
    用来从AT24C02中读取数据
        @addr:字地址,指定从哪个地址开始读取数据
        @data:从24C02中读取出来的数据保存到其指向的空间中去
        @count:要读取的字节数
        @返回值:成功返回读取到的字节数,失败返回-1
*/
int Read_Bytes_From_24C02(u8 addr, u8 *data, int count) {
	
    // 判断count是否合法
    // 如:从地址0xFF(addr)处读取1(count)个字节,此时count就是非法的
    // 256-addr表示能够读取的最多的字节数
    count = count < (256 - addr) ? count : (256 - addr);
	
    printf("Want To Read %d Bytes!\r\n", count);

    if (AT24C02_Is_Busy() == -1) {
        printf("IIC Bus is Busy!\r\n");
        return -1;
    }

    I2C_GenerateSTART(I2C1, ENABLE);

    // 等待事件EV5(发送起始信号后的应答事件)产生
    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) {
        I2C_GenerateSTOP(I2C1,ENABLE);
        return -1;
    }

    I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);

    // 等待事件EV6(发送从地址的应答事件)发生
    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, 1000) == -1) {
        I2C_GenerateSTOP(I2C1, ENABLE);
        return -1;
    }

    // 发送读取的数据的地址
    I2C_SendData(I2C1, addr);

    // 等待事件EV8_2(数据发送完成)
    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, 1000) == -1) {
        I2C_GenerateSTOP(I2C1, ENABLE);
        return -1;
    }

    I2C_GenerateSTART(I2C1, ENABLE);
	
    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) {
        I2C_GenerateSTOP(I2C1, ENABLE);
        return -1;
    }

    I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Receiver);

    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED, 1000) == -1) {
        I2C_GenerateSTOP(I2C1, ENABLE);
        return -1;
    }

    int i;
    // 先读取count-1个字节,因为最后一个字节的处理不同(最后一个字节不回复)
    for (i = 0; i < count - 1; i++) {
        // 等待事件EV7(主机有数据可以读)发生
        if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED, 1000) == -1) {
            I2C_GenerateSTOP(I2C1, ENABLE);
            return -1;
        }

        data[i] = I2C_ReceiveData(I2C1);
    }

    // 由图可知:最后一个字节不回复,只有当对面没有收到应答,对方才会停止发送数据
    I2C_AcknowledgeConfig(I2C1, DISABLE);

    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED, 1000) == -1) {
        I2C_GenerateSTOP(I2C1, ENABLE);
        return -1;
    }

    data[i++] = I2C_ReceiveData(I2C1);

	I2C_GenerateSTOP(I2C1, ENABLE);
	
	// 开启自动回复
	I2C_AcknowledgeConfig(I2C1, ENABLE);
	
   	return i;
}

/*
页写:
    用来向AT24C02中写入数据
        @addr:写入数据的起始地址
        @data:要写入的数据
        @count:要写入的字节数
        @返回值:成功返回写入的字节数,失败返回-1
*/
int Write_Bytes_To_24C02(u8 addr, u8 *data, int count) {
	
    // 当前已写入的字节数
    int bytes = 0;

    // 判断count是否合法
    count = count < (256 - addr) ? count : (256 - addr);

page_write:
    if (AT24C02_Is_Busy() == -1) {
        printf("IIC Bus is Busy!\r\n");
        return -1;
    }

	I2C_GenerateSTART(I2C1, ENABLE);

    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_MODE_SELECT, 1000) == -1) {
        I2C_GenerateSTOP(I2C1, ENABLE);
        return -1;
    }

	I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); 

    if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, 1000) == -1) {
        I2C_GenerateSTOP(I2C1, ENABLE);
        return -1;
    }

    // 发送字节地址
	I2C_SendData(I2C1, addr);

    if(Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, 1000) == -1) {
        I2C_GenerateSTOP(I2C1, ENABLE);
        return -1;
    }

    int i;
    // 发送数据
    // 计算出当前写入的位置addr距离该页的末尾有多少个字节
    // 8 - 页内地址(addr的低3bit) 0000 0111
    int page_bytes = 8 - (addr & 0x7);
    // 开始写入当前页
    for (i = 0; i < page_bytes && bytes < count; i++) {
        I2C_SendData(I2C1, data[bytes]);
        if (Wait_IIC_Event(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, 1000) == -1) {
            I2C_GenerateSTOP(I2C1, ENABLE);
            return -1;
        }
        bytes++;
    }
    
    I2C_GenerateSTOP(I2C1, ENABLE);

    if (bytes < count) {
        // 还没有写完,此时需要跨页了(addr的高5bit需要+1,低5bit从0开始)
        addr = ((addr >> 3) + 1) << 3;

        int flag = Wait_24C02_Finished_To_Write();
        printf("flag = %d\r\n", flag);
	
        goto page_write;
    }
    
    int flag = Wait_24C02_Finished_To_Write();
    printf("flag = %d\r\n", flag);
    
    return bytes;
}

main.c

#include "stm32f4xx.h"
#include "systick.h"
#include "usart.h"
#include "AT24C02.h"

int main(void) {
	
	USART1_Init(9600);
	
	AT24C02_Init();
	
//	while (1) {
//	
//		Write_A_Byte_To_24C02(0x50, 0x80);
//		
//		printf("0x%02x\r\n", Read_A_Byte_From_24C02(0x50));
//		
//		delay_ms(3000);
//	}
	
//	while (1) {
//		Write_A_Byte_To_24C02(0xFB, 0x80);
//		Write_A_Byte_To_24C02(0xFD, 0x90);
//		Write_A_Byte_To_24C02(0xFF, 0x70);

//		unsigned char data[10] = {0};
//		int n = Read_Bytes_From_24C02(0xF8, data, 10);
//		printf("n == %d\r\n", n);
//		for (int i = 0; i < n; i++) {
//			printf("data[%d] : 0x%02x\r\n", i, data[i]);
//		}

//		delay_ms(3000);
//	}

	while (1) {
		unsigned char buf[10] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09};
		Write_Bytes_To_24C02(0x0F, buf, 10);

		unsigned char data[10] = {0};
		int n = Read_Bytes_From_24C02(0x0F, data, 10);

		for (int i = 0; i < n; i++) {
			printf("data[%d] : 0x%02x\r\n", i, data[i]);
		}
		
		delay_ms(3000);
    }
}
### STM32 IIC (I²C) 接口使用教程 #### 初始化配置 为了使STM32能够通过IIC协议与外部设备通信,初始化过程至关重要。首先需定义并准备用于传输的数据缓冲区: ```c // 要写入到24c02的字符串数组 const uint8_t TEXT_Buffer[] = "Elite STM32 IIC TEST"; #define SIZE sizeof(TEXT_Buffer) ``` 这段代码展示了如何声明一个常量字符数组`TEXT_Buffer`以及计算其大小以便后续操作[^1]。 #### 设备地址解析 当涉及到具体硬件连接时,了解目标从设备的具体地址非常重要。对于大多数基于IIC标准设计的产品而言,7位地址由两部分组成:前四位表示器件类型,后三位则作为用户可编程的选择位(A0, A1, A2),允许在同一总线上挂载多达八个相同型号的组件[^2]。 #### 编程实例 下面给出一段简单的程序片段来展示如何利用HAL库实现向指定EERPOM位置写入数据的功能: ```c #include "stm32f1xx_hal.h" void EEPROM_WriteByte(uint16_t DevAddress,uint16_t MemAddress,uint8_t Data){ HAL_I2C_Mem_Write(&hi2c1,DevAddress,MEMADDRESS,MemAddSize,Data,SIZE,10); } ``` 上述函数接收三个参数——设备地址(`DevAddress`)、内存地址(`MemAddress`)和待写入的数据字节(`Data`),并通过调用`HAL_I2C_Mem_Write()`完成实际的操作。这里假设已经完成了必要的外设初始化工作,并且指定了正确的IIC端口号(如`&hi2c1`)。 #### 数据读取流程 同样地,在需要获取存储器中的信息时,则应采用相应的读方法: ```c uint8_t EEPROM_ReadByte(uint16_t DevAddress,uint16_t MemAddress){ uint8_t temp; HAL_I2C_Mem_Read(&hi2c1,DevAddress,MEMADDRESS,MemAddSize,&temp,SIZE,10); return temp; } ``` 此段代码实现了单个字节级别的读取功能,返回值即为所请求地址处的内容。注意这里的API签名几乎完全一致于之前提到过的写入版本,只是内部处理逻辑有所区别而已。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值