I2C EEPROM
I2C
I2C(Inter-Integrated Circuit)总线是由 PHILIPS 公司开发的两线式 串行总线,用于连接微控制器及其外围设备。
I2C 结构
I2C 只有两根双向信号线,一根是 SDA 数据线,一根是 SCL 时钟线。
主机:启动数据传送并产生时钟信号的设备;
从机:被主机寻址的器件;
多主机:同时有多于一个主机尝试控制总线但不破坏传输;
主模式:用 I2CNDAT 支持自动字节计数的模式; 位 I2CRM,I2CSTT,I2CSTP 控制数据的接收和发送;
从模式:发送和接收操作都是由 I2C 模块自动控制的;
仲裁:是一个在有多个主机同时尝试控制总线但只允许其中一个控制总线并 使传输不被破坏的过程;
同步:两个或多个器件同步时钟信号的过程;
发送器:发送数据到总线的器件;
接收器:从总线接收数据的器件。
I2C 协议
①时钟信号周期性的高电平低电平。只有时钟信号为低电平时,数据信号才能变化。
②起始和停止信号:SDA高→低和低→高。
③发送器件传输完一个字节8位数据后,后面必须紧跟一个ACK/NACK校验位,判断接受是否完成,数据传送是否可以继续。
④总线寻址方式
从机有自己的地址,总线寻址采用7或10位的寻址位数。如7位:位定义D0表示数据传送方向位(是主机从从机读取数据?还是主机向从机写数据?代表这个从机是接收器还是发送器),D7~D1是从机地址位。
主机发送一个地址后,所有从机前7位地址和主机比较,如果相同再判断从机是接收还是发送器。
从机地址包含固定位和可编程位,可编程位决定了这个部件可以最多有多少个接入总线。如4位固定位,3位可编程位,说明2^3=8个最多可以接入总线。
⑤数据传输
起始信号 S+7位从机地址+数据方向位+ACK+数据+NACK+终止信号。如果主机还想要新的数据传送,可以不终止,继续发出起始信号向另一从机寻址。
数据方向位=0:主机向从机发送数据。主机一直发到从机返回 NACK 为止。
数据方向位=1:从机向主机发送数据,发送到主机返回 NACK 为止。
AT24C02
是一个2k位串行CMOS,主板上的主板上的一块可读写的并行或串行FLASH芯片。该芯片有 I2C 接口,是个从机。而且有写保护功能,其中写入的数据断电不丢失。
我们可以通过单片机的模拟 IIO 功能,将数据写入该芯片永久存储,下次断电时也能访问。
创建多文件工程
下面编写的程序要求:
设计一个系统,可以写入、读取 AT24C02 中的数据,并将其中的数据用数码管显示出来。
这个系统涉及之前学过的按键、数码管信息,还涉及新的 AT24C02 的使用,三部分代码。由于内容太多,所以这次我们创建一个多文件系统,以后也会用这个系统模板更好的管理文件。
本项目主要包含:
App 文件夹:存储各类函数
Obj 文件夹:存放编译产生的 hex 文件、列表清单等。
Public 文件:存放公共文件,如延时、变量重定义。
User:存放 main.c 等主函数文件。
创建步骤:
①新建一个项目文件夹,在该项目文件夹中新建以上四个文件夹。
②在 Keil 中新建项目,选中这个项目文件夹。选择不复制 StartUp 代码。
③点击魔术棒右边的三色方块,创建三个分组 Group。这里创建的分组是一个逻辑上的分组,并不是指选中这三个具体的文件夹。与三个文件夹起名相同是为了方便管理.
④编写程序.
清楚了我们的需求,我们想一下要写那些代码,放在什么地方。
公共内容:public 中。
使用按钮代码:App中。
使用数码管代码:App中。
使用 AT24C02 代码:App 中。
调用以上几部分内容代码:main 中。
把普中部分代码复制到单片机中,大意:按键1将当前数据写入芯片,按键2读取芯片中存储的数据,按键3将当前数据+1,按键4将当前数据清零。
key部分代码:装有key_scan函数,用于识别当前按下的是四个按钮中的哪一个并返回。
//key.h
#ifndef _key_H
#define _key_H
#include "reg52.h"
//定义独立按键控制脚
sbit KEY1=P3^1;
sbit KEY2=P3^0;
sbit KEY3=P3^2;
sbit KEY4=P3^3;
//定义 LED1 控制脚
sbit LED1=P2^0;
sbit LED2=P2^1;
sbit LED3=P2^2;
sbit LED4=P2^3;
//使用宏定义独立按键按下的键值
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4
#define KEY_UNPRESS 0
u8 key_scan(u8 mode);
#endif
//key.c
#include "public.h"
#include "key.h"
u8 key_scan(u8 mode)
{
static u8 key=1;
if(mode)key=1;//连续扫描按键
if(key==1&&(KEY1==0||KEY2==0||KEY3==0||KEY4==0))//任意按键按下
{
delay(1000);//消抖
key=0;
if(KEY1==0)
return KEY1_PRESS;
else if(KEY2==0)
return KEY2_PRESS;
else if(KEY3==0)
return KEY3_PRESS;
else if(KEY4==0)
return KEY4_PRESS;
}
else if(KEY1==1&&KEY2==1&&KEY3==1&&KEY4==1) //无按键按下
{
key=1;
}
return KEY_UNPRESS;
}
smg.c(我的命名是display.c,也代表数码管?),传入数码管要显示的数字信息(第一个参数是要显示的数字数组,如0,0,7.第二个参数是从左往右数第几位开始显示数字,从0开始。如要显示007,因为一共8位,0~4位都不显示,所以从第五位开始显示)。
/display.h
#ifndef _display_H
#define _display_H
#include "reg52.h"
#include "public.h"
sbit LSA = P2 ^ 2;
sbit LSB = P2 ^ 3;
sbit LSC = P2 ^ 4;
void smg_display(u8 dat[], u16 index);
#endif
display.c
#include "public.h"
#include "display.h"
#define Display P0
unsigned char gsmg_code[17] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07,
0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71};
#define SMG_A_DP_PORT P0 // 使用宏定义数码管段码口
// dat:要传入的数据集合
// index:起始从哪一位开始显示数字。0则显示0~7 8位数字;7则只显示第7位数字。也能让函数知道数组的长度
void smg_display(u8 dat[], u16 index)
{
while (1)
{
int i;
// 位选
for (i = index; i < 8; i++)
{
switch (i)
{
case 0:
LSC = 1;LSB = 1;LSA = 1;
break;
case 1:
LSC = 1;LSB = 1;LSA = 0;
break;
case 2:
LSC = 1;LSB = 0;LSA = 1;
break;
case 3:
LSC = 1;LSB = 0;LSA = 0;
break;
case 4:
LSC = 0;LSB = 1;LSA = 1;
break;
case 5:
LSC = 0;LSB = 1;LSA = 0;
break;
case 6:
LSC = 0;LSB = 0;LSA = 1;
break;
case 7:
LSC = 0;LSB = 0;LSA = 0;
break;
default:break;
}
SMG_A_DP_PORT = dat[i - index];
delay(100); // 延时一段时间,等待显示稳定
SMG_A_DP_PORT = 0x00; // 消音
}
}
}
iic:定义i2c总线的一些方法。如开始等待接收数据、结束关闭、ack、nack、wait ack、读取写入数据等方法。
/iic.h
#ifndef _iic_H
#define _iic_H
#include "public.h"
//定义EEPROM控制脚
sbit IIC_SCL=P2^1;//SCL时钟线
sbit IIC_SDA=P2^0;//SDA数据线
//IIC所有操作函数
void iic_start(void); //发送IIC开始信号
void iic_stop(void); //发送IIC停止信号
void iic_write_byte(u8 txd); //IIC发送一个字节
u8 iic_read_byte(u8 ack); //IIC读取一个字节
u8 iic_wait_ack(void); //IIC等待ACK信号
void iic_ack(void); //IIC发送ACK信号
void iic_nack(void); //IIC不发送ACK信号
#endif
///iic.c
#include "public.h"
#include "iic.h"
void iic_start()
{
IIC_SCL = 1; // SCL为高电平时,SDA的数据才有效
IIC_SDA = 1;
delay(10);
IIC_SDA = 0; // SCL SDA 都由高变低,是起始标志
delay(10);
IIC_SCL = 0;
}
void iic_stop()
{
IIC_SCL = 1;
IIC_SDA = 0;
delay(10);
IIC_SDA = 1;
delay(10);
}
void iic_ack()
{
IIC_SCL = 0;
IIC_SDA = 0;
delay(10);
IIC_SCL = 1;
delay(10);
IIC_SCL = 0;
}
void iic_nack()
{
IIC_SCL = 0;
IIC_SDA = 1;
delay(10);
IIC_SCL = 1;
delay(10);
IIC_SCL = 0;
}
u8 iic_wait_ack()
{
u16 time_temp = 0;
IIC_SCL = 1;
while (IIC_SDA)
{ // 等待数据变成0
if (++time_temp > 100)
{
iic_stop();
return 1;
}
}
IIC_SCL = 0;
return 0;
}
u8 iic_read_byte(u8 ack)
{
u8 i = 0, receive = 0;
for (i; i < 8; i++)
{
IIC_SCL = 0;
delay(1);
IIC_SCL = 1;
receive <<= 1;
if (IIC_SDA)
receive += 1;
delay(1);
}
if (!ack)
iic_nack();
else
iic_ack();
return receive;
}
void iic_write_byte(u8 dat)
{
u8 i = 0;
IIC_SCL = 0;
for (i; i < 8; i++)
{
if ((dat & 0x80) > 0)
IIC_SDA = 1;
else
IIC_SDA = 0;
dat <<= 1;
delay(1);
IIC_SCL = 1;
delay(1);
IIC_SCL = 0;
delay(1);
}
}
24c02:主要就是芯片调用iic来写入或取出数据的函数。看上去倒没有多少自己的东西。
/24c02.h
#ifndef _24c02_H
#define _24c02_H
#include "public.h"
void at24c02_write_one_byte(u8 addr, u8 dat);
u8 at24c02_read_one_byte(u8 addr);
#endif
/24c02.c
#include "public.h"
#include "24c02.h"
#include "iic.h"
void at24c02_write_one_byte(u8 addr, u8 dat){
iic_start();
iic_write_byte(0xA0);//写命令
iic_wait_ack();
iic_write_byte(addr);//发送写地址
iic_wait_ack();
iic_write_byte(dat);
iic_wait_ack();
iic_stop();//停止条件
delay(1);
}
u8 at24c02_read_one_byte(u8 addr){
u8 temp;
iic_start();
iic_write_byte(0xA0);//写命令
iic_wait_ack();
iic_write_byte(addr);//发送写地址
iic_wait_ack();
iic_write_byte(0xA1);//进入接收模式
iic_wait_ack();
temp=iic_read_byte(0);//读取条件
iic_stop();//停止条件
return temp;
}
public.h和.c里面就是装了u8 u16 和 delay 函数的定义。不用多说。
main.c:
#include <reg52.h>
#include <public.h>
#include <key.h>
#include <display.h>
#include <24c02.h>
#define EEPROM_ADDRESS 0//数据存入EEPROM 的起始地址
void main(){
u8 key_temp=0;
u8 save_value=0;//每次刚打开单片机时,起始值都=0
u8 save_buf[3];
while(1){
key_temp=key_scan(0);//单词扫描按键获取当前按键
switch(key_temp){//按键1:写入
case KEY1_PRESS:
at24c02_write_one_byte(EEPROM_ADDRESS,save_value);
break;
case KEY2_PRESS:
save_value=at24c02_read_one_byte(EEPROM_ADDRESS);
break;
case KEY3_PRESS:
if(save_value<255)save_value++;
break;
case KEY4_PRESS:
save_value=0;
break;
default:
break;
}
save_buf[0]=save_value/100;
save_buf[1]=save_value/10%10;
save_buf[2]=save_value%10;
smg_display(save_buf,5);
}
}
其实写完main.c发现了一个地方,就是数码管展示函数每次传入的长度都是3,也就是0~255都是显示3位,高位补0.只有个位就是00几,有个位和十位就是0几几。当时我觉得那这个参数要不要也没啥必要啊。数码管展示函数说不定也能改的更简洁一点。但是后来想到代码的拓展性,这个数码管函数只是针对这次案例永远会只显示5位,说不定其他例子就不一样了。所以不改也好。
运行效果:
一开始运行,数码管显示当前数字000.
按下按钮1,会把当前数字写入 AT24C02 芯片。
按下按钮2,会读取 AT24C02 中存储的数字显示出来。断电也能保存。
按下按钮3,当前数字会+1.
按下按钮4,当前数字会清0.