目录
1.课程记录
4:54
写三个模块,封装分层
I2C作为最底层
包括6个函数:
START, STOP
发送一个字节,接收一个字节
发送应答,接收应答
AT24C02调用I2C函数:
两个数据帧
在某个地址下写入数据,
在某个地址下读出数据,
main模块调用AT24C02模块
程序-项目1-AT24C02数据存储
I2C.c
0:7:25
start函数
空闲时
空闲时,SCL=1,SDA=1
上图中1对应 空闲时,SDA=1
2,对应另一种情况, 非空闲,在发送完数据后,接收应答,此时SDA=0,
因此在Start阶段, SDA有两种情况 SDA=1, 或者 SDA=0
//2025.6.28
#include <REGX52.H>
sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;
void I2C_Start(void)
{
I2C_SDA=1;
I2C_SCL=1;
I2C_SDA=0;
I2C_SCL=0;
}
Stop函数
情况1(空闲时)
情况2:
在接收应答后,进入停止阶段
说明 在停止阶段前, SDA可能是0或者1。
//2025.6.28
#include <REGX52.H>
sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;
void I2C_Start(void)
{
I2C_SDA=1;
I2C_SCL=1;
I2C_SDA=0;
I2C_SCL=0;
}
void I2C_Stop(void)
{
I2C_SDA=0;
I2C_SCL=1;
I2C_SDA=1;
}
I2C_SendByte函数
11:14
注意:
在发送字节前,SCL已经为0
void I2C_SendByte(unsigned char Byte)
{
I2C_SDA=Byte&0x80 ;
//Byte(写入的字节数据),
//通过与1000 0000进行与操作,得到最高位数据(因为数据写入是高位在前,低位在后)
I2C_SCL=1; //SCL=1,此时读取SDA的内容,即Byte&0x80
T2C_SCL=0; // 在读取完SDA内容后,SCL=0,不再读取SDA内容,此时允许SDA有数据变化
}
注意:
这里
SCL置1后, 立马SCL置0,
问题: SCL高电平持续时间较短,能否在SCL高电平期间读取到数据?
参考手册内容 24C02.pdf
SCL 在5V电压下频率为 1000khz, 即1mhz, 周期为1us,
单片机IO口翻转速度最快为1us,频率为500khz,
因此上述 SCL=1,SCL=0 ,电平立马拉高然后立马拉低, 其频率小于SCL的极限范围,因此在该时间段可以读取数据
SCL高电平持续时间只要高于0.4us ,即可满足, 现在使用的单片机STC89C52,最短持续时间为1us ,满足要求
注意
14:05
因为一个字节有8位数据,因此需要将上述模块调用8次,并且每调用一次,0x80需要右移一位
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;
}
}
I2C_ReceiveByte函数
接受数据,首先主机释放SDA,即SDA=1
注意:一般规定:仅在SCL高电平,才从SDA获取数据
unsigned char I2C_ReceiveByte(void)
{
unsigned char Byte=0x00;
I2C_SDA=1;
I2C_SCL=1;
if(I2C_SDA) {Byte|=0x80;} //如果I2C_SDA为1,则Byte 第八位置1
I2C_SCL=0;
return Byte;
}
unsigned char I2C_ReceiveByte(void)
{
unsigned char i,Byte=0x00;
I2C_SDA=1;
for(i=0;i<8;i++) //调用该模块8次
{
I2C_SCL=1;
if(I2C_SDA) {Byte|=(0x80>>i);}
I2C_SCL=0;
}
return Byte;
}
应答函数
I2C_SendAck函数
void I2C_SendAck(unsigned char AckBit) //AckBit 应答位,为0代表接收应答
{
I2C_SDA=AckBit;
I2C_SCL=1;
I2C_SCL=0;
}
I2C_ReceiveAck函数
unsigned char I2C_ReceiveAck(void)
{
unsigned char AckBit;
I2C_SDA=1; //主机释放SDA,从机进行控制SDA
I2C_SCL=1;
AckBit=I2C_SDA;
I2C_SCL=0;
return AckBit;
}
个人:
感觉类似看折线图编程,判断SDA,SCL什么时候为1或0
小结
//2025.6.28
#include <REGX52.H>
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++) //调用该模块8次
{
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) //AckBit 应答位,为0代表接收应答
{
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; //主机释放SDA,从机进行控制SDA
I2C_SCL=1;
AckBit=I2C_SDA;
I2C_SCL=0;
return AckBit;
}
AT2C4C02.c
0:24:38
字节写函数
随机读函数
31:40
测试1
AT24C02.c
//2025.6.29
#include <REGX52.H>
#include "I2C.h"
//定义从机地址(也相当于:地址+W) 地址查看硬件手册
#define AT24C02_ADDRESS 0xA0
//字节写函数
void AT24C02_WriteByte(unsigned char WordAddress,Data)
{
unsigned char Ack;
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS);
Ack=I2C_ReceiveAck();
if (Ack==0) P2=0x00; //如果存在-接受应答,则led全亮
}
//随机读函数
unsigned char AT24C02_ReadByte(unsigned char WordAddress)
{
unsigned char Data;
return Data;
}
main.c
//2025.6.28
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "AT24C02.h"
void main()
{
LCD_Init();
LCD_ShowString(1,1,"Hello");
AT24C02_WriteByte(0,0);
while(1)
{
}
}
效果
led全亮。
测试2
main.c
//2025.6.28
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "AT24C02.h"
#include "Delay.h"
unsigned char Data;
void main()
{
LCD_Init();
LCD_ShowString(1,1,"Hello");
//AT24C02_WriteByte 函数的参数:字节地址(范围:0~255),数据(需要写入的数据)
AT24C02_WriteByte(1,101);
Delay(5); // 注意:根据硬件手册,写周期至少需要5ms,如果没有加延时,则不会显示读取的数据,因为没有写入
Data=AT24C02_ReadByte(1);
LCD_ShowNum(2,1,Data,3); //在第2行第1列,显示Data,长度为3
while(1)
{
}
}
效果:
小结
//2025.6.29
#include <REGX52.H>
#include "I2C.h"
//定义从机地址(也相当于:地址+W) 地址查看硬件手册
#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); // AT24C02_ADDRESS为从机地址+W(0xA0),从机地址+R为0xA1
I2C_ReceiveAck();
Data=I2C_ReceiveByte();
I2C_SendAck(1);
I2C_Stop();
return Data;
}
main.c
一个0~255,共256个存储单元
46:33
//2025.6.28
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "AT24C02.h"
#include "Delay.h"
unsigned char KeyNum;
unsigned int Num;
void main()
{
LCD_Init();
LCD_ShowNum(1,1,Num,5); //初始显示默认Num
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) //按键3功能:存入数据
{
AT24C02_WriteByte(0,Num%256); //将Num的低8位存储在地址0
Delay(5);
AT24C02_WriteByte(1,Num/256);//将Num的高8位存储在地址1
Delay(5);//写周期至少需要5ms
LCD_ShowString(2,1,"Write OK");
Delay(1000);
LCD_ShowString(2,1," ");
}
if (KeyNum==4) //按键4功能:从AT24C02芯片读取Num显示在屏幕
{
Num=AT24C02_ReadByte(0);
Num|=AT24C02_ReadByte(1)<<8; //读取Num的高八位,然后左移8位
LCD_ShowNum(1,1,Num,5);
LCD_ShowString(2,1,"Read OK");
Delay(1000);
LCD_ShowString(2,1," ");
}
}
}
这里高位左移8为,挪到高8位,低八位为0,然后与NUM进行或操作,NUM填写入低八位
0:46:47
项目2-秒表(定时器扫描按键数码管)
讲解:
左边main, 右边(从上往下)定时器模块,按键模块,数码管模块
因为按键,数码管模块均需要调用定时器,但是这样存在交叉耦合(耦合性高,不易于代码管理)(即每个模块之间存在相互调用,那么后续更改时一个模块,其他模块也要跟着修改),因此分别在按键,数码管各自写一个中断调用函数
在main模块,规定假设20ms调用一次按键,30ms调用一次数码管
up:让主模块为子模块服务,自称为驱动函数 或者 循环电路函数
个人:还是不太理解
main.c
//2025.6.29
#include <REGX52.H>
//包含之前项目:按键,数码管,时钟,定时器
#include "Timer0.h"
#include "Nixie.h"
#include "Key.h"
#include "Delay.h"
void main()
{
Timer0_Init(); //初始化定时器
while(1)
{
}
}
//每个1s调用一次中断
//定时器中断函数模版
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count; //默认初值为0 //设置静态变量,在该函数内再次调用时,保留上一次的值
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=1000) // 每隔一秒,led8个灯 闪烁一次
{
T0Count=0;
P2=~P2;
}
}
58:47
原来检测按键是否 按下 或者 松开 的原理:
按下:0-->1, 松开:1-->0, 在0与1的过渡之间存在抖动,因此需要延时20ms,在低电平(按下期间)期间,需要一直while(1)死循环, 因此不灵活,时间占用长
使用定时器检测按键是否 按下 或者 松开 的原理:
每隔20ms检测 一次按键状态值, 同时记录上一时刻按键状态值,与当前值进行对比,
0-->1:表示按下, 1-->0:表示松开,
(注意:好像是 按键状态值为0,表示按下,)
按键key.c改造为定时器key
原来
仅依靠Delay函数,while()循环
#include <REGX52.H>
#include "Delay.h"
/**
* @brief 获取独立按键键码
* @param 无
* @retval 按下按键的键码,范围:1~4, 无按键按下时返回值为0
*/
unsigned char Key()
{
unsigned char KeyNumber=0;
if(P3_1==0){Delay(200);while(P3_1==0);Delay(200);KeyNumber=1;}
if(P3_0==0){Delay(200);while(P3_0==0);Delay(200);KeyNumber=2;}
if(P3_2==0){Delay(200);while(P3_2==0);Delay(200);KeyNumber=3;}
if(P3_3==0){Delay(200);while(P3_3==0);Delay(200);KeyNumber=4;}
return KeyNumber;
}
更改版本
#include <REGX52.H>
#include "Delay.h"
unsigned char Key_KeyNumber;
unsigned char Key(void)
{
unsigned char Temp=0; //设置一个中间变量,返回当前按键的值(表面是1~4其中哪个按键按下)
Temp=Key_KeyNumber;
Key_KeyNumber=0; //初始化按键值,等待下一次按键的返回值
return Temp;
}
/**
* @brief 获取独立按键键码
* @param 无
* @retval 按下按键的键码,范围:1~4, 无按键按下时返回值为0
*/
unsigned char Key_GetState()
{
unsigned char KeyNumber=0;
if(P3_1==0){KeyNumber=1;}
// 经过定时器扫描按键状态值(如设定每隔20ms扫描一次),检测按键P3^1引脚状态值为0,说明按键1处于按下状态,
//此时将KeyNumber=1,表明是按键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,如果上一个扫描时刻状态为按下,当前扫描时刻状态为松开,则表明按键1 有一次按键操作发送
{
Key_KeyNumber=1;
}
if(LastState==2 && NowState==0)
{
Key_KeyNumber=2;
}
if(LastState==3 && NowState==0)
{
Key_KeyNumber=3;
}
if(LastState==4 && NowState==0)
{
Key_KeyNumber=4;
}
}
测试:
main.c
//2025.6.29
#include <REGX52.H>
//包含之前项目:按键,数码管,时钟,定时器
#include "Timer0.h"
#include "Nixie.h"
#include "Key.h"
#include "Delay.h"
unsigned char KeyNum;
unsigned char Temp;
void main()
{
Timer0_Init(); //初始化定时器
while(1)
{
KeyNum=Key();
if (KeyNum) Temp=KeyNum;
Nixie(1,Temp); //按键按下,(按键1~4),第一个数码管显示对应按键数字
}
}
//每个1s调用一次中断
//定时器中断函数模版
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count; //默认初值为0 //设置静态变量,在该函数内再次调用时,保留上一次的值
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=20) // 每隔20ms,扫描一次按键状态
{
T0Count=0;
Key_Loop();
}
}
定时器间隔20ms扫描键盘,将按下的按键对应的数字显示在数码管
定时器扫描数码管
原本
Nixie.c
#include <REGX52.H>
#include "Delay.h"
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00
};
void Nixie(unsigned char Location,unsigned char Number)
{
switch(Location) //位选
{
case 1:P2_4=1;P2_3=1;P2_2=1;break; // 4+2+1==7,????,???7????
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];// 段选
Delay(1);
P0=0X00; //段清零 //显示数字,延时1ms,再清零
}
改造后
1:14:10
Nixie_SetBuf()
三个数码管显示相同数字
1:16:00
//2025.6.29
#include <REGX52.H>
//包含之前项目:按键,数码管,时钟,定时器
#include "Timer0.h"
#include "Nixie.h"
#include "Key.h"
#include "Delay.h"
unsigned char KeyNum;
unsigned char Temp;
void main()
{
Timer0_Init(); //初始化定时器
while(1)
{
KeyNum=Key();
if(KeyNum)
{
Nixie_SetBuf(1,KeyNum);
Nixie_SetBuf(2,KeyNum);
Nixie_SetBuf(3,KeyNum);
}
}
}
//每个1s调用一次中断
//定时器中断函数模版
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count1,T0Count2; //默认初值为0 //设置静态变量,在该函数内再次调用时,保留上一次的值
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count1++;
if(T0Count1>=20) // 每隔20ms,扫描一次按键状态
{
T0Count1=0;
Key_Loop();
}
//扫描数码管
T0Count2++;
if(T0Count2>=2) // 每隔2ms,扫描一次数码管
{
T0Count2=0;
Nixie_Loop();
}
}
总结-项目实现
//2025.6.29
#include <REGX52.H>
//包含之前项目:按键,数码管,时钟,定时器
#include "Timer0.h"
#include "Nixie.h"
#include "Key.h"
#include "AT24C02.h"
#include "I2C.h"
#include "Delay.h"
unsigned char KeyNum;
unsigned char Min,Sec,MiniSec;
unsigned char RunFlag;
void main()
{
Timer0_Init(); //初始化定时器
while(1)
{
KeyNum=Key();
if(KeyNum==1) //启动,暂停 键
{
RunFlag=!RunFlag;
}
if(KeyNum==2) //归零
{
Min=0;
Sec=0;
MiniSec=0;
}
if(KeyNum==3) //写入
{
AT24C02_WriteByte(0,Min);
Delay(5);//写入周期
AT24C02_WriteByte(1,Sec);
Delay(5);
AT24C02_WriteByte(2,MiniSec);
Delay(5);
}
if(KeyNum==4) //读取
{
Min=AT24C02_ReadByte(0);
Sec=AT24C02_ReadByte(1);
MiniSec=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,MiniSec/10);
Nixie_SetBuf(8,MiniSec%10);
}
}
void Sec_Loop(void)
{
if(RunFlag) //启动标志
{
MiniSec++;
if(MiniSec>=100) //10*100=1000ms
{
MiniSec=0;
Sec++;
if(Sec>=60)
{
Sec=0;
Min++;
if(Min>=60)
{
Min=0;
}
}
}
}
}
//每个1s调用一次中断
//定时器中断函数模版
void Timer0_Routine() interrupt 1 // 每隔1ms调用一次,(调用下述函数)
{
static unsigned int T0Count1,T0Count2,T0Count3; //默认初值为0 //设置静态变量,在该函数内再次调用时,保留上一次的值
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count1++;
if(T0Count1>=20) // 每隔20ms,扫描一次按键状态
{
T0Count1=0;
Key_Loop();
}
//扫描数码管
T0Count2++;
if(T0Count2>=2) // 每隔2ms,扫描一次数码管
{
T0Count2=0;
Nixie_Loop();
}
//扫描计时
T0Count3++;
if(T0Count3>=10) // 每隔2ms,扫描一次数码管
{
T0Count3=0;
Sec_Loop();
}
}
看来2天半敲完,期间确实有休息5min,学5min,玩10min, 惭愧。
学习小结
1.硬件知识
2.代码模块化编程 (这个很重要,对于编写复杂功能的项目,需要将模块分卡编写)
3.在写项目时,要边写,边测试,把一个个模块确定好,不能都写完了在验证,这样很可能之前代码全部白费。