E2PROM是Electrically Erasable Programmable Read Only Memory的缩写,中文为“电可擦除可编程只读程序存储器”,它的特点顾名思义:带点可擦除、可编程、只读存储器。虽然名为“只读”,但是用户可以更改其中的数据,可通过高于普通电压的作用来对E2PROM进行擦除和重编程,同时E2PROM也具有掉电不丢失的特点。另外,一般而言E2PROM的存储空间都比较小,因此只能存储简单的数据。
串行E2PROM按总线形式分为三种,即I2C总线、Microwire总线及SPI总线三种。这里我们讨论的是带I2C总线接口的E2PROM,以AT24C08为例。
AT24C02/04/08为ATMEL公司生产的系列E2PROM,其内存分别为2048/4096/8192比特,即256/512/1024字节。AT24C08的内部空间划分如下:
内部共分为4个block(块),每个block里又有16个page(页),每个page的大小是16字节。这样,每个block的空间是256字节,每块AT24C08的空间就是1024字节。
典型的双排直插式封装的E2PROM引脚图如图1所示。
其中A0、A1、A2决定了这块E2PROM在I2C总线上的地址,在单主控器单被控器的情况下无需考虑,WP可以视作无意义也无需考虑,VCC和GND分别连接3.3V直流电压源和接地即可,SDA和SCL分别表示数据线和时钟线。在单主单从的应用中只需连接VCC、GND、SDA和SCL4根线。
如需详解请自行阅读相关芯片手册,推荐一个名为alldatasheet的网站,内有大量芯片手册可供免费下载,实为硬件工程师居家旅行烧板写码必备良药。
从数据手册上我们可以发现集成I2C串行总线的AT24C08具有5种不同的读写逻辑,可以完成单片机等主控器和24C08(被控器)之间单字节数据和多字节数据的读写操作。
单片机向被控器E2PROM中写入单个字节数据,逻辑如下。
首先单片机在SDA总线上产生start信号,接着产生7bit的地址信号以及1bit读写标记为,其中地址的前4bit为固定在被控器内部不可更改的序列,每一种类的器件共享一个4bit地址代码,后3bit为确定具体某个芯片的代码,那1bit读写标记位规定为高电平表示“读”、低电平表示“写”。被控器识别SDA总线上的7bit地址和1bit标记位,在发现和自己的地址匹配后向SDA上发送ACK,主控器收到ACK后再向SDA总线上产生具体的8bit地址,也就是要把字节发送到E2PROM的具体哪个位置去。被控器接收到地址字节后再次发送ACK。此时,主控器接收到ACK后会发送1字节的数据,被控器从SDA上依次逐bit读取该字节数据,之后向主控器发送ACK,主控器在接收到ACK后向总线发送stop信号,结束本轮数据传输。
为方便描述,我们把7bit地址和1bit读写标记位称为“控制字节(Control Byte)”,把标记芯片内部地址的字节称为“字地址(Word Address)”,发送的数据就是Data。
主控器向被控器写入单字节数据的总线信号示意如图2所示。
此处有一点需要注意,那就是对A0、A1、A2这3个bit的理解和使用。
上文中说到这3bit表示对总线上芯片地址的识别,在单主单从的模式中无需考虑,而Control Byte的后3bit正好是用来在SDA上寻找从器件的。这样一来似乎在单主单从模式下后3bit可以随意填写,而24C08内部的block却无法区分,因此上述描述不能自洽。而我在实践中得到的信息是这样的:对于24C08而言,Control Byte的后3bit中的最后2bit决定了片内block的选择,Control Byte的后3bit中的第1bit决定了芯片的选择,因此,一条I2C总线上最多只能挂载2片24C08芯片,共计8个block。同理,一条I2C总线上最多只能挂载4片24C04(每片有2个block)或8片24C02(每片有1个block)。
集成I2C总线的E2PROM地址为的前4bit按规定都是1010,这4bit数据是固定在其内部无法改变的。因此,对于24C08来说,不妨假设地址位后3bit的第1bit都是0,那么其中4个block的7bit地址代码分别为:1010000、1010001、1010010和1010011,转换成16进制就是:0x50、0x51、0x52和0x53。
单片机向被控器E2PROM中整页写入数据,逻辑如下。
在被控器向主控器发送第3个ACK表明自己收到1字节收据后,主控器继续发送下一字节数据而不是stop,直到某一时刻主控器发送stop。
虽然页(Page)的大小是16Byte,但是主控器连续发送的数据量不一定非得是一页的数据量,理论上可以连续发送任意多字节的数据。但是有一点需要注意,就是对E2PROM的写入是按页循环的,即当地址超出某页末尾后,下一个待写入字节的地址不会自动转入下一页,而是会回到本页的开头,这样该字节就会覆盖本页开头原有的那一字节数据。
主控器向被控器整页写入数据的总线信号示意如图3所示。
单片机读取被控器当前操作数据(读/写)的地址,逻辑如下。
芯片24C28内部有一个地址计数器,记录了上一个数据访问的地址,不论是读取还是写入。因此,若上一个操作的地址是n,那么么下一步执行读取当前地址所得到的数据就是n+1。当24C08收到Control Byte后,它在SDA总线上生成ACK信号以及8bit地址n+1,主控制器则在SDA上产生not ACK信号示意24C08停止继续传输,最后主控制器产生stop结束此次任务。
主控器读取被控器当前地址的总线信号示意如图4所示。
随机读取并不是真的“随机”,而是读取指定地址是的数据,逻辑如下。
随机读取允许主控器读取被控器任意地址的数据。首先主控器发送start,接着发送Control Byte,注意此时最后一位续写标记bit值应为0,即表示“写”。在被控器应答ACK后,主控器将地址字节发送出去,当被控器再次应答ACK后,主控器再次发送一个start信号以结束“写”任务,并紧跟着发送Control Byte开始“读”任务,并注意这里Control Byte的最后1bit值应当为1,即表示“读”。当被控器第三次应答ACK后,此时被控器向主控器发送刚刚接收到的地址处的数据字节。主控器接收完毕后向被控器发送not ACK示意被控器停止继续传输,最后主控器产生stop信号结束任务。
主控器随机读取被控器数据的总线信号示意如图5所示。
顺序读取是主控器读取某一特定地址及其之后的若干个数据,逻辑如下。
顺序读取前面的逻辑和随机读取一致,直到最后一步:主控器在接收到数据后并不发送not ACK而是发送ACK,这样被控器继续向主控器发送数据,直到主控器产生not ACK为止,最后主控器会在发送not ACK之后发送stop结束任务。
主控器顺序读取被控器数据的总线信号示意如图6所示。
理论上被控器可以向主控器无限次发送数据,但是和PAGE WRITE的情形一样,连续读取也存在循环,只不过读取时逐block循环的,显然比PAGE WEITE的逐页循环大了很多。也就是说,在读取到当前block的最后一个字节的数据后,如果继续读取,那么不会读到下个block首地址的数据,而是会读到本block首地址的数据。
在上一篇博客的成果——函数void i2c_write_single_byte(uint8_t i2c_buff)和uint8_t i2c_read_single_byte(void)——的基础上用C语言实现上述5种读写模式。
首先罗列一下所有需要实现的函数。
#ifndef __E2PROM_24C08_H__
#define __E2PROM_24C08_H__
#include "i2c_master_sim.h"
typedef struct address_to_ctrl_byte
{
uint8_t ctrl_byte;
uint8_t word_addr;
}addr_ctrl_byte_struct;
void i2c_byte_write(uint8_t ctrl_byte,uint8_t word_addr,uint8_t data_byte);
void i2c_page_write(uint8_t ctrl_byte,uint8_t word_addr,uint8_t *source_data_addr,uint8_t data_len);
void i2c_write_within_block(uint8_t ctrl_byte,uint8_t word_addr,uint8_t *source_data_addr,uint16_t data_len);
uint8_t i2c_current_addr_read(uint8_t ctrl_byte);
uint8_t i2c_rand_read(uint8_t ctrl_byte,uint8_t word_addr);
void i2c_sequential_read(uint8_t ctrl_byte,uint8_t word_addr,uint16_t data_len,uint8_t *data_addr_in_master_mem);
addr_ctrl_byte_struct get_eigenbytes(uint16_t address_in_chip);
void i2c_write_within_chip(uint16_t address_in_chip,uint8_t *source_data_addr,uint16_t data_len);
void i2c_read_within_chip(uint16_t address_in_chip,uint8_t *data_addr_in_master_mem,uint16_t data_len);
#endif
我想我有必要对该文件的内容做些讲解。文件中除了5个已经提到的函数外还有4个函数和1个结构体,这里也包含了上一篇博客中提到的“分层”思想。
事实上 Chapter 2 所提到的5种读写方法是不能直接提交给用户的。当用户需要往一块内存中读写数据时,他才不管什么Control Byte、Word Address之类的呢,用户关心的参数只有:起始地址、长度、目标地址三样而已。所以应当将这5种读写方式进一步抽象,抽象成2个API:Read函数和Write函数,而将内部的一些细节全部隐藏,这就是头文件中最后两个函数的功能。
void i2c_write_within_chip(uint16_t address_in_chip,uint8_t *source_data_addr,uint16_t data_len);
void i2c_read_within_chip(uint16_t address_in_chip,uint8_t *data_addr_in_master_mem,uint16_t data_len);
头文件中剩余的函数就是为了将5个API进一步抽象成更高一层的2个API所需要的辅助代码。
基本的5个读写函数已有说明,2个更抽象的API所需要当心的内容也只是每一个Page和Block的首末位置,防止循环、防止覆写和重读、注意内存越界等等,本质上只是二维数组的操作,也很简单。
下面就直接上代码了。
#include "stdlib.h"
#include "math.h"
#include "e2prom_24C08.h"
#include "i2c_master_sim.h"
#define SDA IO_CONFIG_PB0
#define SCL IO_CONFIG_PB1
addr_ctrl_byte_struct get_eigenbytes(uint16_t address_in_chip)
{
addr_ctrl_byte_struct cbs;
if((address_in_chip<0x00) || (address_in_chip>0x3FF))
{
printf("Cross-border error! The range of address_in_chip is 0x000-0x3FF.\n");
exit(EXIT_FAILURE);
}
else
{
if((address_in_chip>=0x00) && (address_in_chip<0x100))
{
cbs.ctrl_byte = 0xA0;
cbs.word_addr = address_in_chip;
}
else if((address_in_chip>=0x100) && (address_in_chip<0x200))
{
cbs.ctrl_byte = 0xA2;
cbs.word_addr = address_in_chip%0x100;
}
else if((address_in_chip>=0x200) && (address_in_chip<0x300))
{
cbs.ctrl_byte = 0xA4;
cbs.word_addr = address_in_chip%0x200;
}
else
{
cbs.ctrl_byte = 0xA6;
cbs.word_addr = address_in_chip%0x300;
}
}
return cbs;
}
// write one byte to e2prom
void i2c_byte_write(uint8_t ctrl_byte,uint8_t word_addr,uint8_t data_byte)
{
i2c_start();
i2c_write_single_byte(ctrl_byte);
if(i2c_read_ack() == 0)
i2c_write_single_byte(word_addr);
else
return;
if(i2c_read_ack() == 0)
i2c_write_single_byte(data_byte);
else
return;
if(i2c_read_ack() == 0)
i2c_stop();
else
return;
}
// write bytes to e2prom (page write)
void i2c_page_write(uint8_t ctrl_byte,uint8_t word_addr,uint8_t *source_data_addr,uint8_t data_len)
{
uint8_t i;
if (data_len<=0)
{
printf("i2c_page_write: data_len should be a positive number.\n");
return;
}
else
{
i2c_start();
i2c_write_single_byte(ctrl_byte);
if(i2c_read_ack() == 0)
i2c_write_single_byte(word_addr);
else
return;
for(i=0;i bolck_size )
{
printf("(B1)i2c_write_within_block:beyond the scope of the current block, JUST RETURN.");
return;
}
// within the current block
else
{
if(data_len <= len_left)
{
printf("(B2)i2c_write_within_block:within the current page.\n");
i2c_page_write(ctrl_byte,word_addr,source_data_addr,data_len); // pointer as function parameter?
while(i2c_ack_check(ctrl_byte));
}
else
{
printf("(B3)i2c_write_within_block:within the current block but beyond the current page.\n");
if( (data_len - len_left)%page_size != 0 )
extra_page = floor( (data_len - len_left)/(float)page_size+1 );
else
extra_page = (data_len - len_left)/(float)page_size;
printf("extra_page = %d.\n data_len = %d.\n len_left = %d.\n",extra_page,data_len,len_left);
// first, write the current page
i2c_page_write(ctrl_byte,word_addr,source_data_addr,len_left);
while(i2c_ack_check(ctrl_byte));
// then, write the following complete page except the last maybe-incomplete page
for (i=1;i> 1);
uint16_t total_mem_left = 1024-256*(4-left_block_num)-word_addr;
uint16_t current_block_mem_left = block_size-word_addr;
// do not beyond current block
if ( (word_addr+data_len) <= block_size )
{
printf("(C1)i2c_write_within_chip:do not beyond current block.\n");
i2c_write_within_block(ctrl_byte,word_addr,source_data_addr,data_len);
}
// the chip itself is not large enough
// just write the chip full and abandon the left part
else if ( data_len > total_mem_left )
{
printf("(C2)i2c_write_within_chip:the chip itself is not large enough.\n");
data_len = total_mem_left;
if( (data_len-current_block_mem_left)%block_size != 0 )
extra_block = floor((data_len-current_block_mem_left)/(float)block_size + 1);
else
extra_block = (data_len-current_block_mem_left)/(float)block_size;
// first, write the current block
i2c_write_within_block(ctrl_byte,word_addr,source_data_addr,current_block_mem_left);
// just write the chip full and abandon the left part
for (i=1;i <= extra_block;i++)
{
i2c_write_within_block(ctrl_byte+0x02*i,0x00,source_data_addr+current_block_mem_left+(i-1)*block_size,block_size);
}
}
// occupy more than one block of memory but not beyond chip's memory range
else
{
printf("(C3)i2c_write_within_chip:occupy more than one block of memory but not beyond chip's memory range.\n");
// judge whether extra_block is intergals or float
if( (data_len-current_block_mem_left)%block_size != 0 )
extra_block = floor((data_len-current_block_mem_left)/(float)block_size + 1);
else
extra_block = (data_len-current_block_mem_left)/(float)block_size;
printf("extra_block = %d.\n",extra_block);
// first, write the current block
i2c_write_within_block(ctrl_byte,word_addr,source_data_addr,current_block_mem_left);
// then, write the following complete block except the last maybe-incomplete block
for (i=1;i> 1);
uint16_t total_mem_left = 1024-256*(4-left_block_num)-word_addr;
uint16_t current_block_mem_left = block_size-word_addr;
// donot beyond current block
if ( (word_addr+data_len) <= block_size )
{
printf("(C1)i2c_read_within_chip:do not beyond current block.\n");
i2c_sequential_read(ctrl_byte,word_addr,data_len,data_addr_in_master_mem);
}
// the chip itself is not large enough
// just read the chip full and abandon the left part
else if (data_len > total_mem_left)
{
printf("(C2)i2c_read_within_chip:the chip itself is not large enough.\n");
data_len = total_mem_left;
if( (data_len-current_block_mem_left)%block_size != 0 )
extra_block = floor((data_len-current_block_mem_left)/(float)block_size + 1);
else
extra_block = (data_len-current_block_mem_left)/(float)block_size;
printf("data_len=%d\n current_block_mem_left=%d\n extra_block=%d\n",data_len,current_block_mem_left,extra_block);
// first, read the current block
i2c_sequential_read(ctrl_byte,word_addr,current_block_mem_left,data_addr_in_master_mem);
// then, read the following complete block till the end
for (i = 1;i <= extra_block;i++)
{
i2c_sequential_read(ctrl_byte+0x02*i,0x00,block_size,(data_addr_in_master_mem+current_block_mem_left+(i-1)*block_size));
}
}
// occupy more than one block of memory but not beyond chip's memory range
else
{
printf("(C3)i2c_read_within_chip:occupy more than one block of memory but not beyond chip's memory range.\n");
if( (data_len-current_block_mem_left)%block_size != 0 )
extra_block = floor((data_len-current_block_mem_left)/(float)block_size + 1);
else
extra_block = (data_len-current_block_mem_left)/(float)block_size;
printf("data_len=%d\n current_block_mem_left=%d\n extra_block=%d\n",data_len,current_block_mem_left,extra_block);
// first, read the current block
i2c_sequential_read(ctrl_byte,word_addr,current_block_mem_left,data_addr_in_master_mem);
// then, read the following complete block till the end
for (i = 1;i < extra_block;i++)
{
i2c_sequential_read(ctrl_byte+0x02*i,0x00,block_size,(data_addr_in_master_mem+current_block_mem_left+(i-1)*block_size));
}
// finally, write the last maybe-incomplete block
i2c_sequential_read(ctrl_byte+0x02*extra_block,0x00,data_len-current_block_mem_left-(extra_block-1)*block_size,
(data_addr_in_master_mem+current_block_mem_left+(extra_block-1)*block_size));
}
printf("i2c_read_within_chip finished.\n");
}
实验成功,单片机可以从E2PROM中读取数据或向其中写入数据。
向E2PROM中连续写入15字节数据的总线波形图如图7所示:
从E2PROM中连续读出256字节数据的串口打印结果(波形图太长无法展示)如图8所示:
注意,E2PROM中的数据位在默认情况下都是高电平,即每个字节都是0xFF,这里是先按照0x00-0xFF的顺序往一个block里写256字节,再全部读出来。该实验可以说明面向用户的2个API是有效的。
转载时务必注明来源及作者。尊重知识产权从我做起。
包含上一篇博客在内的4个C代码文件已上传至网络,欢迎下载,密码是gwd2。