赞
踩
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<data_len;i++) { if(i2c_read_ack() == 0) i2c_write_single_byte(*(source_data_addr+i)); else return; } if(i2c_read_ack() == 0) i2c_stop(); else return; } printf("i2c_page_write finished.\n"); } // read current address uint8_t i2c_current_addr_read(uint8_t ctrl_byte) { uint8_t data; i2c_start(); i2c_write_single_byte(ctrl_byte); if(i2c_read_ack() == 0) { data = i2c_read_single_byte(); i2c_send_nack(); i2c_stop(); return data; } else return 0; } // read one byte from eeprom uint8_t i2c_rand_read(uint8_t ctrl_byte,uint8_t word_addr) { uint8_t data; i2c_start(); i2c_write_single_byte(ctrl_byte); if(i2c_read_ack() == 0) i2c_write_single_byte(word_addr); else return 0; if(i2c_read_ack() == 0) i2c_start(); else return 0; i2c_write_single_byte((ctrl_byte+1)); if(i2c_read_ack() == 0) { data = i2c_read_single_byte(); i2c_send_nack(); i2c_stop(); return data; } else return 0; } // read sequential bytes from eeprom // it is the ADDRESS that is transferred, but not DATA! //uint32_t i2c_sequential_read(uint8_t ctrl_byte,uint8_t word_addr,uint16_t data_num) void i2c_sequential_read(uint8_t ctrl_byte,uint8_t word_addr,uint16_t data_len,uint8_t *data_addr_in_master_mem) { uint16_t i=0; if(data_len<=0) { printf("i2c_sequential_read: 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; if(i2c_read_ack() == 0) i2c_start(); else return; i2c_write_single_byte((ctrl_byte+0x01)); if(i2c_read_ack() == 0) *data_addr_in_master_mem = i2c_read_single_byte(); else return; for(i=1;i<data_len;i++) { i2c_send_ack(); // master send ACK *(data_addr_in_master_mem + i) = i2c_read_single_byte(); } i2c_send_nack(); // master send NACK i2c_stop(); } printf("i2c_sequential_read finished.\n"); } // memory_write within one single block void i2c_write_within_block(uint8_t ctrl_byte,uint8_t word_addr,uint8_t *source_data_addr,uint16_t data_len) { uint8_t page_size = 0x10; uint16_t bolck_size = 0x100; uint8_t extra_page = 0; uint8_t i = 0; uint8_t page_offset = word_addr % page_size; uint8_t len_left = page_size - page_offset; // beyond the scope of the current block if( (word_addr + data_len) > 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<extra_page;i++) { i2c_page_write(ctrl_byte,(word_addr+len_left+(i-1)*page_size),(source_data_addr+len_left+(i-1)*page_size),page_size); while(i2c_ack_check(ctrl_byte)); } // finally, write the last maybe-incomplete page i2c_page_write(ctrl_byte,(word_addr+len_left+(extra_page-1)*page_size),(source_data_addr+len_left+(extra_page-1)*page_size), (data_len-len_left-(extra_page-1)*page_size)); while(i2c_ack_check(ctrl_byte)); } } printf("i2c_write_within_block finished.\n"); } /******************** the following are 2 highest API:Write/Read in chip********************/ // memory_write within one 24c08 Chip void i2c_write_within_chip(uint16_t address_in_chip,uint8_t *source_data_addr,uint16_t data_len) { uint16_t block_size = 0x100; uint8_t extra_block = 0; uint8_t i = 0; addr_ctrl_byte_struct Scb; Scb = get_eigenbytes(address_in_chip); uint8_t ctrl_byte = Scb.ctrl_byte; uint8_t word_addr = Scb.word_addr; uint8_t left_block_num = 4 - ((ctrl_byte & 0x06) >> 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<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); } // finally, write the last maybe-incomplete block i2c_write_within_block(ctrl_byte+0x02*extra_block,0x00, source_data_addr+current_block_mem_left+(extra_block-1)*block_size, data_len-current_block_mem_left-(extra_block-1)*block_size); } printf("i2c_write_within_chip finished.\n"); } void i2c_read_within_chip(uint16_t address_in_chip,uint8_t *data_addr_in_master_mem,uint16_t data_len) { uint16_t block_size = 0x100; uint8_t extra_block = 0; uint8_t i = 0; addr_ctrl_byte_struct Scb; Scb = get_eigenbytes(address_in_chip); uint8_t ctrl_byte = Scb.ctrl_byte; uint8_t word_addr = Scb.word_addr; uint8_t left_block_num = 4 - ((ctrl_byte & 0x06) >> 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。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。