STM32(十二)通过I2C总线向EEPROM(AT24C02 )读写数据的过程
一、概述
(1)背景
- I2C(IIC,Inter-Integrated Circuit)总线是由Philips公司开发的一种简单、双向二线制同步串行总线。
- 它只需要两根线即可在连接于总线上的器件之间传送信息。
- 主器件用于启动总线传送数据,并产生时钟以开放传送的器件,此时任何被寻址的器件均被认为是从器件。
- I2C总线简化了硬件电路PCB布线,降低了系统成本,提高了系统可靠性。因为I2C芯片(如mpu6050、ft5x06等)除了这两根线和少量中断线,与系统再没有连接的线,用户常用IC可以很容易形成标准化和模块化,便于重复利用。
(2)传输方向
- 在总线上主和从、发和收的关系不是恒定的,而取决于此时数据传送方向。
- 如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送。
- 如果主机要接收从器件的数据,首先由主器件寻址从器件.然后主机接收从器件发送的数据,最后由主机终止接收过程。在这种情况下,主机负责产生定时时钟和终止数据传送。
(3)速度
- 连接到相同总线上的IC数量只受总线最大电容的限制,串行的8位双向数据传输位速率在标准模式下可达100Kbit/s,快速模式下可达400Kbit/s,高速模式下可达3.4Mbit/s。
- 总线具有极低的电流消耗.抗高噪声干扰,增加总线驱动器可以使总线电容扩大10倍,传输距离达到15m;兼容不同电压等级的器件,工作温度范围宽。
(4)地址
I2C总线上的每一个设备都可以作为主设备或者从设备,而且每一个设备都会对应一个唯一的地址(地址通过物理接地或者拉高,可以从I2C器件的数据手册得知,如AT24C02芯片,7位地址依次1010xxx, 最低三位可配,如果全部物理接地,则该设备地址为0x50),主从设备之间就通过这个地址来确定与哪个器件进行通信,在通常的应用中,我们把STM32作为主设备,把挂接在总线上的其他设备都作为从设备。
二、AT240C02 EEPROM介绍
(1)特点
- 宽范围的工作电压1.8V~5.5V低电压技术
- 1mA典型工作电流- 1uA典型待机电流·存储器组织结构
- 24C02,256 X8(2K bits)- 24C04,512×8(4K bits)- 24C08,1024 × 8 (8K bits)-24C16,2048 ×8(16K bits)-24C32,4096 X8(32K bits)- 24C64,8192 × 8(64K bits)
- 2线串行接口,完全兼容l2C总线
- I2C时钟频率为1 MHz (5V),400 kHz (1.8V,2.5V,2.7V)施密特触发输入噪声抑制
- 硬件数据写保护
- 内部写周期(最大5 ms)可按字节写
- 页写:8字节页(24C02),16字节页(24C04/08/16),32字节页(24C32/64)可按字节,随机和序列读
- 自动递增地址
- 高可靠性擦写寿命:100万次-数据保持时间:100年
有规格书可知,EEPROM的读写速率是100KHZ.
(2)引脚说明
AT24C02芯片,7位地址依次1010xxx, 最低三位(A0~A2)可配,由原理图可知三个脚物理接地,地址为1010000,即该设备地址为0x50)。
(3)起始和停止时序
数据和时钟线都为高则称总线处在空闲状态。当SCL为高电平时SDA的下降沿(高到低叫做起始条件(START,简写为S),SDA的上升沿(低到高)则叫做停止条件(STOP,简写为P)。参见图5。
(4)数据有效性
SCL高电平时SDA数据有效,低电平时SDA数据交换,数据变换为高电平或者低电平。
(5)应答信号:
三、读写操作
1、写操作
(1).字节写
写操作要求在接收器件地址和ACK应答后,接收8位的字地址。接收到这个地址后EEPROM应答"0",然后是一个8位数据。在接收8位数据后EEPROM应答"0",接着必须由主器件发送停止条件来终止写序列。
此时EEPROM进入内部写周期twR,数据写入非易失性存储器中,在此期间所有输入都无效。直到写周期完成,EEPROM才会有应答(见图9)。
(2)页写
- 24C02器件按8字节/页执行页写,24C04/08/16器件按16字节/页执行页写,24C32/64器件按32字节/页执行页写。
- 页写初始化与字节写相同,只是主器件不会在第一个数据后发送停止条件,而是在EEPROM的ACK以后,接着发送7个(24C02数据。EEPROM收到每个数据后都应答“0”。最后仍需由主器件发送停止条件,终止写序列(见图10)。接收到每个数据后,字地址的低3位(24C02)内部自动加1,高位地址位不变,维持在当前页内。当内部产生的字地址达到该页边界地址时,随后的数据将写入该页的页首。如果超过8个(24C02)数据传送给了EEPROM,字地址将回转到该页的首字节,先前的字节将会被覆盖。
MCU向AT24CT02写数据时,MCU为主机,EEPROM为从机。由上图可知:
- 主机通过SDA先发一个起始信号。
- 发设备地址寻找设备,并设置是写入还是读取。
- 从机回一个Ack应答信号。
- 主机发送要写入的EEPROM片内地址。
- 从机回Ack应答信号。
- 主机写数据到从机,每写一byte,从机回发一个ack应答。
- 主机发送停止信号,结束数据写入。
(3)代码分析:
#include "iic.h" /*软件模拟IIC写数据到EEPROM*/ GPIO_InitTypeDef GPIO_InitStructure; #define SDA_OUT PBout(9) #define SDA_IN PBin(9) #define SCL_OUT PBout(8) //初始化SDL、SCL GPIO void Iic_AT24C02_Init(void) { //1.初始化时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); //2.初始化硬件 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_8;//PB8 PB9 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//输出模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉 GPIO_Init(GPIOB,&GPIO_InitStructure); //空闲状态高电平 SDA_OUT = 1; SCL_OUT = 1; } //设置引脚模式 void sda_pin_mode(GPIOMode_TypeDef mode) { GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;//PB9 GPIO_InitStructure.GPIO_Mode = mode;//模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉 GPIO_Init(GPIOB,&GPIO_InitStructure); } //IIC起始信号 void iic_start(void) { //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); //保证开始是一个高电平 SDA_OUT = 1; SCL_OUT = 1; delay_us(5); //AT24C02的读写速度为100kHZ,一个周期为10us. SDA_OUT = 0;//时钟线为高电平期间,数据线由高到低 delay_us(5); SCL_OUT = 0;//高速总线上的从机,有通信的设备 delay_us(5); } //发送数据 void iic_send_byte(uint8_t d)// 10101110 { int32_t i; //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 0; delay_us(5); for(i=7;i>=0;i--) { if(d & (0x1<<i)) SDA_OUT = 1; else SDA_OUT = 0; delay_us(5); SCL_OUT = 1; delay_us(5);//当前数据是可靠的,告诉从机可以读取数据 SCL_OUT = 0; delay_us(5);//当前数据是不可靠的,正在准备 } } //应答信号 uint8_t iic_wait_ack(void) { uint8_t ack = 0; //保证SDA引脚为输入模式 sda_pin_mode(GPIO_Mode_IN); SCL_OUT = 1; delay_us(5);//当前数据是可靠的,主机可以访问 if(SDA_IN == 0) ack = 0;//有应答 要 else ack = 1;//无应答 不要了 SCL_OUT = 0; delay_us(5); return ack; }
void iic_stop(void) { //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 1; delay_us(5); SDA_OUT = 1;//时钟线为高电平期间,数据线由低到高 delay_us(5); } int32_t at24c02_write(uint8_t word_addr,uint8_t *buf,uint8_t len) { uint8_t ack = 0; uint8_t *p = buf; //发送起始信号 iic_start(); //设备寻址,设备写访问地址为 10100000 = 0xA0 iic_send_byte(0xA0); //等待应答 ack = iic_wait_ack(); if(ack) { printf("devices address failed!\r\n"); return -1; } //告诉从机我要访问的数据存储地址 iic_send_byte(word_addr); //等待应答 ack = iic_wait_ack(); if(ack) { printf("word address failed!\r\n"); return -2; } //连续写数据 while(len--) { //写入数据 iic_send_byte(*p); //等待应答 ack = iic_wait_ack(); if(ack) { printf("data failed!\r\n"); return -3; } p++; } //发送停止信号 iic_stop(); printf("write success!\r\n"); return 0; }
2、读操作
读操作与写操作初始化相同,只是器件地址中的读/写选择位应为"1"。有三种不同的读操作方式:当前地址读,随机读和顺序读。
(1)当前地址读
- 内部地址计数器保存着上次访问时最后一个地址加1的值。只要芯片有电,该地址就一直保存。当读到最后页的最后字节,地址会回转到0;当写到某页尾的最后一个字节,地址会回转到该页的首字节。
- 接收器件地址(读/写选择位为"1")、EEPROM应答ACK后,当前地址的数据就随时钟送出。主器件无需应答"O",但需发送停止条件(见图12)。
2.随机读
随机读需先写一个目标字地址,一旦EEPROM接收器件地址和字地址并应答了ACK,主器件就产生一个重复的起始条件。
然后,主器件发送器件地址(读/写选择位为"1"),EEPROM应答ACK,并随时钟送出数据。主器件无需应答"O",但需发送停止条件(见图13)。
- 主机通过SDA先发一个起始信号。
- 发设备地址寻找设备,并设置是写入(因为要写入读取数据的地址)。
- 写入要读取的地址
- 从机回一个Ack应答信号。
- 主机再发一个起始信号开始读
- 主机发送要写入的EEPROM片内地址(读写位设置为读)。
- 读数据。
- 主机发送停止信号,结束数据读取。
3.顺序读
顺序读可以通过“当前地址读"或“随机读"启动。主器件接收到一个数据后,应答ACK。只要EEPROM接收到ACK,将自动增加字地址并继续随时钟发送后面的数据。若达到存储器地址末尾,地址自动回转到0,仍可继续顺序读取数据。
主器件不应答"0",而发送停止条件,即可结束顺序读操作(见图14)。
#include "iic.h" GPIO_InitTypeDef GPIO_InitStructure; void Iic_AT24C02_Init(void) { //1.初始化时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); //2.初始化硬件 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_8;//PB8 PB9 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//输出模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉 GPIO_Init(GPIOB,&GPIO_InitStructure); //空闲状态高电平 SDA_OUT = 1; SCL_OUT = 1; } void sda_pin_mode(GPIOMode_TypeDef mode) { GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;//PB9 GPIO_InitStructure.GPIO_Mode = mode;//模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉 GPIO_Init(GPIOB,&GPIO_InitStructure); } void iic_start(void) { //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); //保证开始是一个高电平 SDA_OUT = 1; SCL_OUT = 1; delay_us(5); SDA_OUT = 0;//时钟线为高电平期间,数据线由高到低 delay_us(5); SCL_OUT = 0;//高速总线上的从机,有通信的设备 delay_us(5); } void iic_send_byte(uint8_t d)// 10101110 { int32_t i; //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 0; delay_us(5); for(i=7;i>=0;i--) { if(d & (0x1<<i)) SDA_OUT = 1; else SDA_OUT = 0; delay_us(5); SCL_OUT = 1; delay_us(5);//当前数据是可靠的,告诉从机可以读取数据 SCL_OUT = 0; delay_us(5);//当前数据是不可靠的,正在准备 } } uint8_t iic_recv_byte(void) { int8_t i; uint8_t data; //保证SDA引脚为输入模式 sda_pin_mode(GPIO_Mode_IN); //当前数据不可靠,切换数据 SCL_OUT = 0; delay_us(5); for(i=7;i>=0;i--)//MSB 10101111 { SCL_OUT = 1; delay_us(5);//当前数据可靠,读把 if(SDA_IN == 1) { data |= (0x1<<i); } else { data &= ~(0x1<<i); } SCL_OUT = 0; delay_us(5);//当前数据不可靠,切换数据 } return data; } void iic_ack(uint8_t ack) { //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 0; delay_us(5);//当前数据不可靠 //发送高/低电平 SDA_OUT = ack; delay_us(5);//准备数据 SCL_OUT = 1; delay_us(5);//当前数据可靠,然后从机可以访问 SCL_OUT = 0; delay_us(5);//当前数据bu可靠,切换数据 } uint8_t iic_wait_ack(void) { uint8_t ack = 0; //保证SDA引脚为输入模式 sda_pin_mode(GPIO_Mode_IN); SCL_OUT = 1; delay_us(5);//当前数据是可靠的,主机可以访问 if(SDA_IN == 0) ack = 0;//有应答 要 else ack = 1;//无应答 不要了 SCL_OUT = 0; delay_us(5); return ack; } void iic_stop(void) { //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 1; delay_us(5); SDA_OUT = 1;//时钟线为高电平期间,数据线由低到高 delay_us(5); } int32_t at24c02_write(uint8_t word_addr,uint8_t *buf,uint8_t len) { uint8_t ack = 0; uint8_t *p = buf; //发送起始信号 iic_start(); //设备寻址,设备写访问地址为 10100000 = 0xA0 iic_send_byte(0xA0); //等待应答 ack = iic_wait_ack(); if(ack) { printf("devices address failed!\r\n"); return -1; } //告诉从机我要访问的数据存储地址 iic_send_byte(word_addr); //等待应答 ack = iic_wait_ack(); if(ack) { printf("word address failed!\r\n"); return -2; } //连续写数据 while(len--) { //写入数据 iic_send_byte(*p); //等待应答 ack = iic_wait_ack(); if(ack) { printf("data failed!\r\n"); return -3; } p++; } //发送停止信号 iic_stop(); printf("write success!\r\n"); return 0; } int32_t at24c02_read(uint8_t word_addr,uint8_t *buf,uint8_t len) { uint8_t ack = 0; uint8_t *p = buf; //发送开始信号 iic_start(); //设备寻址,设备写访问地址 0xA0 iic_send_byte(0xA0); //等待应答 ack = iic_wait_ack(); if(ack) //1 无应答 0 应答 { printf("devices address failed!\r\n"); return -1; } //告诉从机我要访问的数据存储地址 iic_send_byte(word_addr); //等待应答 ack = iic_wait_ack(); if(ack) //1 无应答 0 应答 { printf("word address failed!\r\n"); return -2; } //再次发送开始信号 iic_start(); //设备寻址,设备读访问地址 0xA1 iic_send_byte(0xA1); //等待应答 ack = iic_wait_ack(); if(ack) //1 无应答 0 应答 { printf("devices address failed!\r\n"); return -3; } //连续接收数据 len = len - 1; while(len--) { *p++ = iic_recv_byte(); //发送应答 iic_ack(0); } //接收最后一个字节 *p = iic_recv_byte(); //发送不应答 iic_ack(1); //发送停止信号 iic_stop(); printf("read success!\r\n"); return 0; }