嵌入式入门教学——C51(下)

  • 嵌入式入门教学汇总:
    • 嵌入式入门教学——C51(上)
    • 嵌入式入门教学——C51(中)
    • 嵌入式入门教学——C51(下)

十三、AT24C02(I2C总线)

1、存储器

  • 嵌入式入门教学——C51(下)_第1张图片
  • RAM、ROM各有优势,所以需要结合使用。
1.1、AT24C02简介
  • AT24C02是一种可以实现掉电不丢失的存储器,可用于保存单片机运行时想要永久保存的数据信息。
  • 存储介质:E2PROM
  • 通讯接口:I2C总线
  • 容量:256字节

2、AT24C02原理图

  • 嵌入式入门教学——C51(下)_第2张图片
  • VDD、VSS:电源(1.8V~5.5V)
  • WE:写使能(低电平有效)
  • SCL、SDA:I2C接口
  • E0、E1、E2:I2C地址

3、I2C总线

3.1、I2C简介
  • 12C总线 (InterIC BUS) 是由Philips公司开发的一种通用数据总线。
  • 两根通信线:SCL (Serial Clock)、SDA(Serial Data )
  • 特点:同步,半双工,带数据应答
  • 通用的I2C总线,可以使各种设备的通信标准统一,对于厂家来说使用成熟的方案可以缩短芯片设计周期、提高稳定性,对于应用着来说,使用通用的通信协议可以避免学习各种各样的自定义协议降低了学习和应用的难度。
3.2、I2C电路规范
  • 所有I2C设备的SCL连在一起,SDA连在一起。
  • 设备的SCL和SDA均要配置成开漏输出模式
    • 【注】开漏输出模式:输出电平只能被拉低,而不能被拉高。开关断开时,处于一种浮空的状态;开关连接时,输出低电平。
    • 嵌入式入门教学——C51(下)_第3张图片
  • SCL和SDA各添加一个上拉电阻(把一个信号通过一个电阻接到电源),阻值一般为4.7KQ左右。
    • 【注】为什么开漏输出又要加上拉电阻?
      • 接上拉电阻是因为I2C通信需要输出高电平的能力。一般开漏输出无法输出高电平,如果在漏极接上拉电阻,则可以进行电平转换。
  • 开漏输出和上拉电阻的共同作用实现了“线与”的功能,此设计主要是为了解决多机通信互相干扰的问题。
  • 嵌入式入门教学——C51(下)_第4张图片嵌入式入门教学——C51(下)_第5张图片
3.3、I2C时序结构
  • 起始条件:SCL高电平期间,SDA从高电平切换到低电平。
  • 终止条件:SCL高电平期间,SDA从低电平切换到高电平。
    • 嵌入式入门教学——C51(下)_第6张图片
  • 发送一个字节(主机->从机):SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。
    • 嵌入式入门教学——C51(下)_第7张图片
  • 接收一个字节(从机->主机):SCL低电平期间,从机将数据位依次放到SDA线上(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA,让从机使用)。
    • 嵌入式入门教学——C51(下)_第8张图片
  • 发送应答:在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。
  • 接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)。
    • 嵌入式入门教学——C51(下)_第9张图片
3.4、I2C数据帧
  • 发送一帧数据:在起始条件开始后,第一个字节一定是发送从机地址+写标志0。从机地址前四位是固定的,后三位是可配置的,即E0,E1,E2。每发送一个字节的数据,主机需要接收从机的应答。
    • 嵌入式入门教学——C51(下)_第10张图片
  • 接受一帧数据:第一个字节一定是发送从机地址+读标志1。第一次发送从机地址后,主机需要接收从机的应答。之后每接收一个字节的数据,主机需要向从机发送应答。
    • 嵌入式入门教学——C51(下)_第11张图片
  • 先发送再接收数据帧:
    • 嵌入式入门教学——C51(下)_第12张图片

4、AT24C02数据帧

  • 字节写:在WORD ADDRESS(字地址)处写入数据DATA。
  • 随机读:读出在WORD ADDRESS处的数据DATA。
  • AT24C02的固定地址为1010,可配置地址本开发板上为000,所以SLAVE ADDRESS+W为0xA0,SLAVE ADDRESS(从机地址)+R为0xA1。
  • 【注】每次写入需要延时5ms,因为AT24C02的写周期为5ms。连续读不用。

5、AT24C02数据存储(LCD1602显示)

  • 内容:按下按键1,Num加1;按下按键2,Num减1;按下按键3,将十六位的Num拆开写入AT24C02存储器中;按下按键4,AT24C02存储器中读出Num。(数据是写入芯片中的,掉电后仍会保留)
  • 新建工程,在工程目录下新建Functions、Objects、Listings文件夹,将延时函数、独立按键和LCD1602模块放入Functions文件夹中。设置生成.hex文件并将其存放到Objects中,将.lst文件存放到Listings中。新建main.c文件,添加Delay.c、Key.c和LCD1602.c到工程中,并设置其引入路径。
  • 代码结构:
    • 嵌入式入门教学——C51(下)_第13张图片
5.1、I2C模块
  • I2C.c
  • #include 
    sbit I2C_SCL=P2^1;
    sbit I2C_SDA=P2^0;
    /**
    	*	@brief	I2C开始
    	* 	@param	无
    	*	@retval	无
    	*/
    void I2C_Start(void){
    	I2C_SDA=1;
    	I2C_SCL=1;
    	I2C_SDA=0;
    	I2C_SCL=0;
    }
    /**
    	*	@brief	I2C停止
    	* 	@param	无
    	*	@retval	无
    	*/
    void I2C_Stop(void){
    	I2C_SDA=0;
    	I2C_SCL=1;
    	I2C_SDA=1;
    }
    /**
    	*	@brief	I2C发送一个字节
    	* 	@param	Byte 要发送的字节
    	*	@retval	无
    	*/
    void I2C_SendByte(unsigned char Byte){
    	unsigned char i;
    	for(i=0;i<8;i++){
    		I2C_SDA=Byte&(0x80>>i);
    		I2C_SCL=1; // 读取
    		I2C_SCL=0;
    	}
    }
    /**
    	*	@brief	I2C接收一个字节
    	* 	@param	无
    	*	@retval	接收到的一个字节数据
    	*/
    unsigned char I2C_ReceiveByte(void){
    	unsigned char i,Byte=0x00;
    	I2C_SDA=1; // 主机释放总线,让从机使用
    	for(i=0;i<8;i++){
    		I2C_SCL=1;
    		if(I2C_SDA)	Byte|=(0x80>>i);
    		I2C_SCL=0;
    	}
    	return Byte;
    }
    /**
    	*	@brief	I2C发送应答
    	* 	@param	AckBit 应答位,0为应答,1为非应答
    	*	@retval	无
    	*/
    void I2C_SendAck(unsigned char AckBit){
    	I2C_SDA=AckBit;
    	I2C_SCL=1;
    	I2C_SCL=0;
    }
    /**
    	*	@brief	I2C接收应答
    	* 	@param	无
    	*	@retval	接收到的应答位,0为应答,1为非应答
    	*/
    unsigned char I2C_ReceiveAck(void){
    	unsigned char AckBit;
    	I2C_SDA=1; // 主机释放总线,让从机使用
    	I2C_SCL=1;
    	AckBit=I2C_SDA;
    	I2C_SCL=0;
    	return AckBit;
    }
  • I2C.h
  • #ifndef __I2C_H__
    #define __I2C_H__
    void I2C_Start(void);
    void I2C_Stop(void);
    void I2C_SendByte(unsigned char Byte);
    unsigned char I2C_ReceiveByte(void);
    void I2C_SendAck(unsigned char AckBit);
    unsigned char I2C_ReceiveAck(void);
    #endif
  • 将I2C总线模块放入Functions文件夹中,添加I2C.c到工程中,并设置其引入路径。 
5.2、AT24C02模块
  • AT24C02.c
  • #include 
    #include "I2C.h"
    #define AT24C02_ADDRESS 0xA0
    /**
    	*	@brief	AT24C02写入一个字节
    	* 	@param	WordAddress 要写入字节的地址
    	* 	@param	Data 要写入的数据	
    	*	@retval	无
    	*/
    void AT24C02_WriteByte(unsigned char WordAddress,Data){
    	I2C_Start();
    	I2C_SendByte(AT24C02_ADDRESS); // 发送从机地址,写
    	I2C_ReceiveAck(); // 接收应答
    	I2C_SendByte(WordAddress); // 发送字地址
    	I2C_ReceiveAck();
    	I2C_SendByte(Data); // 发送数据
    	I2C_ReceiveAck();
    	I2C_Stop();
    }
    /**
    	*	@brief	AT24C02读取一个字节
    	* 	@param	WordAddress 要读出字节的地址
    	*	@retval	读出的数据
    	*/
    unsigned char AT24C02_ReadByte(unsigned char WordAddress){
    	unsigned char Data;
    	I2C_Start();
    	I2C_SendByte(AT24C02_ADDRESS); // 发送从机地址
    	I2C_ReceiveAck(); // 接收应答
    	I2C_SendByte(WordAddress); // 发送字地址
    	I2C_ReceiveAck();
    	I2C_Start();
    	I2C_SendByte(AT24C02_ADDRESS|0x01); // 发送从机地址,读
    	I2C_ReceiveAck(); // 接收应答
    	Data=I2C_ReceiveByte();
    	I2C_SendAck(0); // 发送应答
    	return Data;
    	I2C_Stop();
    }
  • AT24C02.h 
  • #ifndef __AT24C02_H__
    #define __AT24C02_H__
    void AT24C02_WriteByte(unsigned char WordAddress,Data);
    unsigned char AT24C02_ReadByte(unsigned char WordAddress);
    #endif
  • 将AT24C02存储器模块放入Functions文件夹中,添加AT24C02.c到工程中,并设置其引入路径。  
5.3、编写main.c文件
  • #include 
    #include "Delay.h"
    #include "Key.h"
    #include "LCD1602.h"
    #include "AT24C02.h"
    unsigned char KeyNum;
    unsigned int Num; // Num为十六位数据,所以需要拆开存放
    void main(){
    	LCD_Init();
    	LCD_ShowNum(1,1,Num,5);
    	while(1){
    		KeyNum=Key();
    		if(KeyNum==1){
    			Num++;
    			LCD_ShowNum(1,1,Num,5);
    		}
    		if(KeyNum==2){
    			Num--;
    			LCD_ShowNum(1,1,Num,5);
    		}
    		if(KeyNum==3){
    			AT24C02_WriteByte(0,Num%256); // 低八位
    			Delay(5); // 写周期
    			AT24C02_WriteByte(1,Num/256); // 高低位
    			Delay(5);
    			LCD_ShowString(2,1,"Write OK");
    			Delay(1000);
    			LCD_ShowString(2,1,"        ");
    		}
    		if(KeyNum==4){
    			Num=AT24C02_ReadByte(0);
    			Num|=AT24C02_ReadByte(1)<<8;
    			LCD_ShowNum(1,1,Num,5);
    			LCD_ShowString(2,1,"Read OK");
    			Delay(1000);
    			LCD_ShowString(2,1,"        ");
    		}
    	}
    }

6、秒表(定时器扫描按键和数码管) 

  • 内容:使用数码管显示秒表,按下按键1,秒表开始计时;按下按键2,秒表停止计时;按下按键3,将秒表的时间写入AT24C02存储器中;按下按键4,从AT24C02存储器中读出时间。(重点是使用定时器扫描按键和数码管)
  • 新建工程,在工程目录下新建Functions、Objects、Listings文件夹,将延时函数、I2C总线、AT24C02存储器和定时器0模块放入Functions文件夹中。设置生成.hex文件并将其存放到Objects中,将.lst文件存放到Listings中。新建main.c文件,添加Delay.c、I2C.c、AT24C02.c和Timer0.c到工程中,并设置其引入路径。
  • 代码结构:
    • 嵌入式入门教学——C51(下)_第14张图片
6.1、独立按键模块
  • 因为使用的是定时器去扫描按键,速度可以控制在20ms扫描一次,所以可以不用消抖。需要在独立按键中编写一个函数供定时器调用,故重新编写定时器模块。
  • Key_Timer.c
  • #include 
    /**
    	*	@brief	使用定时器扫描按键,获取独立按键键码
    	* 	@param	无
    	*	@retval	按下按键的键码,范围:0~4,无按键按下时,返回0
    	*/
    unsigned char Key_KeyNumber;
    unsigned char Key(void){
    	unsigned char Temp=0;
    	Temp=Key_KeyNumber;
    	Key_KeyNumber=0;
    	return Temp;
    }
    unsigned char Key_GetState(){ // 定时器每隔20秒扫描一次,所以不用消抖
    	unsigned char KeyNumber=0;
    	if(P3_1==0)	KeyNumber=1;
    	if(P3_0==0)	KeyNumber=2;
    	if(P3_2==0)	KeyNumber=3;
    	if(P3_3==0)	KeyNumber=4;
    	return KeyNumber;
    }
    void Key_Loop(void){ // 在定时器中调用
    	static unsigned char NowState,LastState;
    	LastState=NowState;
    	NowState=Key_GetState();
    	if(LastState==1 && NowState==0){ // 按下按键1并松手
    		Key_KeyNumber=1;
    	}
    	if(LastState==2 && NowState==0){ // 按下按键2并松手
    		Key_KeyNumber=2;
    	}
    	if(LastState==3 && NowState==0){ // 按下按键3并松手
    		Key_KeyNumber=3;
    	}
    	if(LastState==4 && NowState==0){ // 按下按键4并松手
    		Key_KeyNumber=4;
    	}
    }
  • Key_Timer.h 
  • #ifndef __KEY_H__
    #define __KEY_H__
    unsigned char Key(void);
    void Key_Loop(void);
    #endif
    
  • 将新的独立按键模块放入Functions文件夹中,添加Key_Timer.c到工程中,并设置其引入路径。   
6.2、数码管模块
  • 因为使用的是定时器去扫描数码管,速度可以控制在2ms扫描一次,所以不用管数码管多久刷新一次,不需要延时函数。需要在数码管中定义一个函数去控制它显示的内容,故重新编写定时器模块。
  • Nixie_Timer.c
  • #include 
    /**
    	*	@brief	使用定时器扫描数码管,数码管显示函数
    	* 	@param	Location 显示位置 范围:1~6
    	* 	@param	Number 显示数值 范围:0~16
    	*	@retval	无
    	*/
    //段选
    unsigned char NixieTable[]={ // 只显示0~9和黑屏
        0x3F, 0x06, 0x5B, 0x4F,
        0x66, 0x6D, 0x7D, 0x07,
        0x7F, 0x6F, 0x00
    };
    unsigned char Nixie_Buf[9]={0,10,10,10,10,10,10,10,10}; // 显示缓存
    // 修改缓存
    void Nixie_SetBuf(unsigned char Location,Number){
    	Nixie_Buf[Location]=Number;
    }
    void Nixie_Scan(unsigned char Location, int Number){
        P0=0x00; // 段选清零,消影
        switch(Location){
            case 1: P2_4=1; P2_3=1; P2_2=1; break;
            case 2: P2_4=1; P2_3=1; P2_2=0; break;
            case 3: P2_4=1; P2_3=0; P2_2=1; break;
            case 4: P2_4=1; P2_3=0; P2_2=0; break;
            case 5: P2_4=0; P2_3=1; P2_2=1; break;
            case 6: P2_4=0; P2_3=1; P2_2=0; break;
            case 7: P2_4=0; P2_3=0; P2_2=1; break;
            case 8: P2_4=0; P2_3=0; P2_2=0; break;
        }
        P0=NixieTable[Number]; // 段选
    }
    void Nixie_Loop(void){ // 供定时器调用,不能有Delay
    	static unsigned char i=1;
    	Nixie_Scan(i,Nixie_Buf[i]);
    	i++;
    	if(i>=9) i=1;
    }
  • Nixie_Timer.h 
  • #ifndef __NIXIE_H__
    #define __NIXIE_H__
    void Nixie_SetBuf(unsigned char Location,Number);
    void Nixie_Scan(unsigned char Location, int Number);
    void Nixie_Loop(void);
    #endif
  • 将新的数码管模块放入Functions文件夹中,添加Nixie_Timer.c到工程中,并设置其引入路径。
6.2、编写main.c文件
  • #include 
    #include "AT24C02.h"
    #include "Key_Timer.h"
    #include "Nixie_Timer.h"
    #include "Timer0.h"
    #include "Delay.h"
    unsigned char KeyNum;
    unsigned char Min,Sec,MinSec;
    unsigned char RunFlag;
    void main(){
    	Timer0Init();
    	while(1){
    		KeyNum=Key();
    		if(KeyNum==1){
    			RunFlag=!RunFlag;
    		}
    		if(KeyNum==2){
    			Min=0,Sec=0,MinSec=0;
    		}
    		if(KeyNum==3){ // 写入存储器
    			AT24C02_WriteByte(0,Min);
    			Delay(5); // 写周期
    			AT24C02_WriteByte(1,Sec);
    			Delay(5);
    			AT24C02_WriteByte(2,MinSec);
    			Delay(5);
    		}
    		if(KeyNum==4){ // 从存储器读出
    			Min=AT24C02_ReadByte(0);
    			Sec=AT24C02_ReadByte(1);
    			MinSec=AT24C02_ReadByte(2);
    		}
    		Nixie_SetBuf(1,Min/10); // 分钟的十位
    		Nixie_SetBuf(2,Min%10); // 分钟的个位
    		Nixie_SetBuf(3,11); // 杠-
    		Nixie_SetBuf(4,Sec/10); // 秒钟的十位
    		Nixie_SetBuf(5,Sec%10); // 秒钟的个位
    		Nixie_SetBuf(6,11); // 杠-
    		Nixie_SetBuf(7,MinSec/10); // 100为1秒
    		Nixie_SetBuf(8,MinSec%10);
    	}
    }
    void Sec_Loop(void){
    	if(RunFlag){
    		MinSec++; // 到100加1秒
    		if(MinSec>=100){
    			MinSec=0;
    			Sec++;
    			if(Sec>=60){
    				Sec=0;
    				Min++;
    				if(Min>=60) Min=0;
    			}
    		}
    	}
    }
    void Timer0_Routine() interrupt 1{
    	static unsigned int T0Count1,T0Count2,T0Count3;
    	TL0 = 0x18;		//设置定时初值
    	TH0 = 0xFC;		//设置定时初值
    	T0Count1++;
    	if(T0Count1>=20){ // 20ms
    		T0Count1=0;
    		Key_Loop(); // 独立键盘
    	}
    	T0Count2++;
    	if(T0Count2>=2){ // 2ms
    		T0Count2=0;
    		Nixie_Loop(); // 数码管
    	}
    	T0Count3++;
    	if(T0Count3>=10){ // 10ms
    		T0Count3=0;
    		Sec_Loop(); // 秒表计时
    	}
    }

待续。。。

你可能感兴趣的:(嵌入式,51单片机)