初学51单片机之I2C总线与E2PROM以及UART简单实例应用
这是I2C的系列的第三篇,这篇主要是写一个简单的程序来实践一下相关的内容。前面博主写过一个电子密码锁的程序初学51单片机之简易电子密码锁及PWM应用扩展_51单片机设计电子密码锁-CSDN博客
本篇主要是在此基础上修改下程序,让密码存储在E2PROM中,并且可以通过UART串口通信在线修改E2PROM存储的密码。
简单的介绍下程序的功能:
1:笔者的开发版在烧录该程序后一上电,除了数字键可以使能外其他按键是没有功能的。因此需要输入相应的密码才能使能其他按键的功能。
2:程序的密码是存放在24C02这个E2PROM器件“非易失区”的4个存储地址中(0x00,0x01,0x02,0x03),以密码2024为例,它是以字符形式‘2’、‘0’、‘2’、‘4’分别按顺序存入的。由于密码锁的写法有点问题,该程序目前只支持4位的密码,
3:当输入正确的密码后,键盘的其它按键功能就能使用。在设置好倒计时比如10秒,按下entel键倒计时开始,10s后LED小灯就会被点亮(当然这个秒表还不是非常准确,没仔细做时间补偿),蜂鸣器蜂鸣。按下ESC会复位。
4:通过UART串口,可以通过输入命令使能蜂鸣器。“buzz on”、"buzz off"能分别打开和关闭蜂鸣器。“reset password ” 命令能修改24C02存储的数据,以输入命令“reset password 2024”为例,通信软件接收收区会显示命令语句reset password 2024,并且液晶上会显示“2024”,同时修改了E2PROM密码存储区的数据,修改后的密码只会在下次开机启动后使能。在线修改密码上电后就可以修改。如果UART传输未定义命令,会在接收区显示字符串“bad command”用以提醒输入错误的指令。
5:在键盘输入错误密码后。数码管基本显示的是65526这几个数字,这时板子无法再继续输入密码。需要重启板子才能再次尝试输入密码。
6:该程序实现了UART串口通信,I2C通信的基本用法。对于UART串口通信,前面笔者有一篇博文使用"!"号作为命令结束的通用标识,本篇采用的是另外一种。它是基于对总线空闲时间的监控来确定数据帧是否传输接收。(注意这个数据帧不是单指一个字节的数据帧也可能是多个字节一起构成一段数据帧),这个时间本案是30ms,即UART总线上空闲了30ms,就确认一段数据帧结束。并开始处理数据帧命令。
7:这个密码锁的写法是笔者之前写的,个人觉得不行,太麻烦。个人觉得把键盘输入的键码转换为字节方式存入数组中,然后和E2PROM中的存储的字节比较会快一点。
8:程序的主干来自开发版老师,一些功能扩展是笔者自己写的。这个程序还是初版,有些功能还能优化一下的,比如该程序目前只支持密码修改,按密码时的长短键要求还无法通过UART修改,目前该程序按密码的时候都是短键使能。
看代码:
main.c
#include <reg52.h>
sbit BUZZ = P1^6;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
sbit KEY_IN_1 = P2^4;
sbit KEY_IN_2 = P2^5;
sbit KEY_IN_3 = P2^6;
sbit KEY_IN_4 = P2^7;
sbit KEY_OUT_1 = P2^3;
sbit KEY_OUT_2 = P2^2;
sbit KEY_OUT_3 = P2^1;
sbit KEY_OUT_4 = P2^0;
unsigned char code LedChar[] = { //数码管显示字符转换表
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char LedBuff[7] = { //数码管+独立LED显示缓冲区
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
unsigned char code KeyCodeMap[4][4] = { //矩阵按键编号到标准键盘键码的映射表
{ 0x31, 0x32, 0x33, 0x26 }, //数字键1、数字键2、数字键3、向上键
{ 0x34, 0x35, 0x36, 0x25 }, //数字键4、数字键5、数字键6、向左键
{ 0x37, 0x38, 0x39, 0x28 }, //数字键7、数字键8、数字键9、向下键
{ 0x30, 0x1B, 0x0D, 0x27 } //数字键0、ESC键、 回车键、 向右键
};
unsigned char KeySta[4][4] = { //全部矩阵按键的当前状态
{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}
};
pdata unsigned long KeyDownTime[4][4]= {
{0, 0, 0, 0},{0, 0, 0, 0},{0, 0, 0, 0},{0, 0, 0, 0}
};
bit enBuzz = 0; //蜂鸣器使能标记
bit flag1s = 0; //1s定时标志
bit flagStart = 0; //倒计时启动标志
bit EntelLongPress = 0; //Entel长按标志
bit LongPress = 0; //长按标志
bit Locksta = 0; //按键转换状态防出错标志
bit PasswordLock = 0; //使能定时器键盘标志
bit KeyLock = 1; //使能密码键盘标志
bit PressMark = 0; //长按标记
unsigned char PressStyle = 0;//按键方式
unsigned int backword = 0; //键盘密码值
unsigned char T0RH = 0; //T0重载值高字节
unsigned char T0RL = 0; //T0重载值低字节
unsigned char CountDown = 0; //倒计时计数器
unsigned int keybuf = 0;//E2prom储存的密码
extern void UartDriver();
extern void ConfigUART(unsigned int baud);
extern void UartRxMonitor(unsigned char ms);
extern void UartWrite(unsigned char *buf, unsigned char len);
extern void InitLcd1602();
extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str);
extern void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len);
unsigned int DataConversion(unsigned char addr);
extern void E2Read(unsigned char* buf,unsigned char addr,unsigned char len );
extern void E2Write(unsigned char* buf,unsigned char addr, unsigned char len);
void ConfigTimer0(unsigned int ms); //定时器0初值设定函数
void ShowNumber(unsigned long num); //倒计时调整时间,数码管显示函数
void KeyDriver();
void Password(unsigned int Num);
void main()
{
unsigned int keybuf = 0;
InitLcd1602(); //初始化液晶
EA = 1;
ENLED = 0;
ADDR3 = 1;
ConfigUART(9600); //配置波特率为9600
keybuf = DataConversion(0x00);
ConfigTimer0(2); //定时2ms
ShowNumber(0); //数码管显示0
while(1)
{
KeyDriver(); //调用按键驱动函数
Password(keybuf);
UartDriver(); //调用串口驱动
if(flagStart && flag1s) //倒计时启动且1秒定时到达时,处理倒计时
{
flag1s = 0;
if(CountDown > 0) //倒计时未到0时,计时器递减
{
CountDown--; //
ShowNumber(CountDown); //刷新倒计时数字显示
if(CountDown == 0)
{
enBuzz = 1; //启动蜂鸣器
LedBuff[6] = 0x00; //点亮独立LED;
}
}
}
}
}
/* 内存比较函数,比较两个指针所指向的内存数据是否相同,
ptr1-待比较指针1,ptr2-待比较指针2,len-待比较长度
返回值-两段内存数据完全相同时返回1,不同返回0 */
bit CmpMemory(unsigned char *ptr1, unsigned char *ptr2, unsigned char len)
{
while (len--)
{
if (*ptr1++ != *ptr2++) //遇到不相等数据时即刻返回0
{
return 0;
}
}
return 1; //比较完全部长度数据都相等则返回1
}
/* 串口动作函数,根据接收到的命令帧执行响应的动作
buf-接收到的命令帧指针,len-命令帧长度 */
void UartAction(unsigned char *buf, unsigned char len)
{
unsigned char i;
unsigned char j;
unsigned char pdata keybuf[4];
unsigned int pdata PasswordBuf = 0;
unsigned char code cmd0[] = "buzz on"; //开蜂鸣器命令
unsigned char code cmd1[] = "buzz off"; //关蜂鸣器命令
unsigned char code cmd2[] = "reset password "; //字符串显示命令
unsigned char code cmdLen[] = { //命令长度汇总表
sizeof(cmd0)-1, sizeof(cmd1)-1, sizeof(cmd2)-1,//去掉字符串结束符
};
unsigned char code *cmdPtr[] = { //命令指针汇总表
&cmd0[0], &cmd1[0], &cmd2[0],
};
for (i=0; i<sizeof(cmdLen); i++) //遍历命令列表,查找相同命令
{
if (len >= cmdLen[i]) //首先接收到的数据长度要不小于命令长度
{
if (CmpMemory(buf, cmdPtr[i], cmdLen[i])) //比较相同时退出循环
{
break;
}
}
}
switch (i) //循环退出时i的值即是当前命令的索引值
{
case 0:
enBuzz = 1; //开启蜂鸣器
break;
case 1:
enBuzz = 0; //关闭蜂鸣器
break;
case 2:
buf[len] = '\0'; //为接收到的字符串添加结束符
LcdShowStr(0, 0, buf+cmdLen[2]); //显示命令后的字符串
i = len - cmdLen[2]; //计算有效字符个数
if (i < 16) //有效字符少于16时,清除液晶上的后续字符位
{
LcdAreaClear(i, 0, 16-i);
}
for(j = 0; j<4;j++) //约定是设置4位密码,因此输入密码的时候要注意长度,输入5位密码也只使能前4位
{
keybuf[j] = *(buf+cmdLen[2]+j);//把串口写入的密码存入数组
}
E2Write(keybuf,0x00,sizeof(keybuf));//往0x00地址写入密码
break;
default: //未找到相符命令时,给上机发送“错误命令”的提示
UartWrite("bad command.\r\n", sizeof("bad command.\r\n")-1);
return;
}
buf[len++] = '\r'; //有效命令被执行后,在原命令帧之后添加
buf[len++] = '\n'; //回车换行符后返回给上位机,表示已执行
UartWrite(buf, len);
}
/* 取出E2PROM 0x00 0x01地址的值转换为16进制的数作为密码 addr为起始地址该密码是4位数 */
unsigned int DataConversion(unsigned char addr)
{
// unsigned char tmp;
unsigned int key;
unsigned char buf[4];//定义一个数组存储密码
pdata unsigned int str[4]; //注意这个数组一定要用int
E2Read(buf,addr,sizeof(buf));//由该函数buf[4]数组已经取得E2PROM 0x00 0x01 0x02 0x03地址的值分别
//存储在buf[4] 数组中
str[0] = (buf[0] - '0')*1000;
str[1] = (buf[1] - '0')*100;
str[2] = (buf[2] - '0')*10;
str[3] = (buf[3] - '0')*1;
key = str[0] + str[1] + str[2] +str[3] ;
return key;
}
/*配置并启动T0,ms-T0定时时间 */
void ConfigTimer0(unsigned int ms)
{
unsigned long tmp; //临时变量
tmp = 11059200 / 12; //每秒机器周期数
tmp = (tmp * ms)/1000; //计算传递实参的机器周期数
tmp = 65536 - tmp ; //设置定时器重载初值
tmp = tmp +28; //初值补偿
T0RH = (unsigned char)(tmp >> 8); //初值高低字节分离
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清零定时器0控制位
TMOD |= 0x01; //选择定时器0的工作模式
TH0 = T0RH; //定时器0高低字节赋值
TL0 = T0RL;
ET0 = 1; //定时器0中断使能
TR0 = 1; //使能定时器0
}
/*将一个无符号长整型的数字显示到数码管伤,num位待显示数字 */
void ShowNumber(unsigned long num)
{
signed char i;
unsigned char buf[6]; //把长整形数,每个进制位上的数转化成十进制的数共6个存入数组
for(i = 0; i <6; i++)
{
buf[i] = num %10;
num = num / 10;
}
for(i = 5;i >=1;i--) //从高位起,遇到0转换为0xff(不显示),遇到非零则退出循环
{
if(buf[i] == 0 )
LedBuff[i] = 0xFF; // 作用:高位是零则不显示
else
break;
}
for(; i >= 0; i--) //剩余低位都如实转换成数码管要显示的数
{
LedBuff[i] = LedChar[buf[i]];
}
}
/* 按键动作函数,根据密码锁键码执行相应的操作,passcode 为按键键码 */
void PasswordAction(unsigned char passcode)
{
static unsigned char cnt = 0;
static unsigned int buf[4] = {0,0,0,0};
static unsigned i = 0xFF;
if(KeyLock == 1) //为1可以输入密码,为0密码输入屏蔽
{
ShowNumber(passcode - 0x30);
if(passcode >= 0x30 && passcode <= 0x39 | passcode == 0x1B)
{ //确定输入的是数字键
cnt++;
switch(cnt)
{
case 1: buf[0] = (passcode - 0x30)*1000 ; i = i << 1 | PressMark; break;
case 2: buf[1] = (passcode - 0x30)*100 ; i = i << 1 | PressMark; break;
case 3: buf[2] = (passcode - 0x30)*10 ; i = i << 1 | PressMark; break;
case 4: buf[3] = (passcode - 0x30)*1 ; i = i << 1 | PressMark; break;
default: break;
}
}
cnt &= 0x03;
if(cnt == 0)
{
PressStyle = i & 0x0F;
backword = (buf[0]+buf[1]+buf[2]+buf[3]);
buf[0] = 0;
buf[1] = 0;
buf[2] = 0;
buf[3] = 0;
i = 0xFF;
}
if(passcode == 0x1B) //初始化键
{
buf[0] = 0;
buf[1] = 0;
buf[2] = 0;
buf[3] = 0;
cnt = 0;
i = 0xFF;
}
}
}
/* 密码设置函数 */
void Password(unsigned int Num)
{
if(Num == backword && PressStyle == 0x00)//按键方式目前都是短键
PasswordLock = 1;
}
/* 按键动作函数,根据键码执行相应的操作,keycode 为按键键码 */
void KeyAction(unsigned char keycode)
{
if(PasswordLock == 1) //为1使能定时操作,为0屏蔽按键操作
{
KeyLock = 0;
if(keycode == 0x26) //向上键,倒计时设定值每按一下加1
{
if(CountDown < 9999) //最大计数9999
{
// LongPress = 0;
CountDown++;
ShowNumber(CountDown);
}
}
else if (keycode == 0x28) //向下键 倒计时设定值递减
{
if(CountDown >1) //最小计时1s
{
// LongPress = 0;
CountDown--;
ShowNumber(CountDown);
}
}
else if(keycode == 0x0D) //回车键 ,启动倒计时
{
if(EntelLongPress | Locksta == 1)
{
flagStart = 0;
EntelLongPress = 0;
LongPress = 0;
}
else
{
flagStart = 1;
LongPress = 0;
}
}
else if(keycode == 0x1B) //ESC 键 取消倒计时
{
LongPress = 0;
enBuzz = 0;
LedBuff[6] = 0xFF;
flagStart = 0;
CountDown = 0;
ShowNumber(0);
}
}
}
/*按键驱动函数,检测按键动作,调度相应动作函数,需要在主函数中调用 */
void KeyDriver()
{
unsigned char i,j,cnt;
static unsigned char pdata backup[4][4] = { //按键值备份,保存前一次的值
{1,1,1,1},
{1,1,1,1},
{1,1,1,1},
{1,1,1,1}
};
static unsigned long pdata TimeThr[4][4] = { //快速输入执行的时间阈值
{500,500,500,500},
{500,500,500,500},
{500,500,500,500},
{500,500,500,500}
};
for(i = 0; i<4; i++) //循环扫描4*4的矩阵按键
{
for(j = 0; j<4; j++)
{
if(backup[i][j] != KeySta[i][j]) //按键动作检查
{
if(PasswordLock)
{
if(backup[i][j] == 0 && LongPress == 0 ) //前态如果是0那么现态是1,开关从按住弹起
{
if( Locksta == 0)
KeyAction(KeyCodeMap[i][j]); //调用按键动作函数
Locksta = 0;
}
if(backup[i][j] == 0 && LongPress == 1 ) //前态如果是0那么现态是1,开关从按住弹起
{
LongPress = 0;
}
}
if(KeyLock == 1)//锁住密码锁
{
if(backup[i][j] == 0 && PressMark == 0) //前态如果是0那么现态是1,开关从按住弹起
{
PasswordAction(KeyCodeMap[i][j]); //调用密码锁按键动作
}
if(backup[i][j] == 0 && PressMark == 1)
{
PressMark = 0;
}
}
backup[i][j] = KeySta[i][j]; //刷新前一次备份值
cnt = 0;
}
if(KeyDownTime[i][j] > 0) //检测执行快速输入
{
if(KeyDownTime[i][j] >= TimeThr[i][j])
{
if(KeyLock ==1) //密码锁键盘使能
{
if( PressMark == 0)
{
PressMark = 1;
PasswordAction(KeyCodeMap[i][j]); //调用密码锁按键动作
}
TimeThr[i][j] += 100; //时间阈值增加200ms,以准备下一次执行
}
if(PasswordLock == 1)
{
LongPress = 1; //达到阈值时执行一次动作
KeyAction(KeyCodeMap[i][j]); //调用按键动作函数
TimeThr[i][j] += 100; //时间阈值增加200ms,以准备下一次执行
cnt++;
if(cnt >= 11)
{
cnt = 0;
if(i == 3 && j == 2) //注意entel键是3行2列,不是4行3列因为第1行一1列是0,0
{
EntelLongPress = 1;
Locksta = 1; //按键锁标志防止弹起进入短按函数
}
}
}
}
}
else // 按键弹起时复位阈值时间
{
TimeThr[i][j] = 500; // 恢复1s的初始阈值时间
//矩阵函数cnt = 0复位语句语句不能放在该处
// 每4次中断才能扫描到一次对应按住的按键
} //其他时间一直进入的都是else函数,把它的比较阈值赋值为500,因此cnt=0 不能放在这个位置。
}
}
}
/*按键扫描函数 ,需要在定时中断中调用 */
void KeyScan()
{
unsigned char i;
static unsigned char keyout = 0;
static unsigned char keybuf[4][4] = {
{0xFF,0xFF,0xFF,0xFF},
{0xFF,0xFF,0xFF,0xFF},
{0xFF,0xFF,0xFF,0xFF},
{0xFF,0xFF,0xFF,0xFF},
};
//将一行的4个按键值移入缓冲区
keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1;
keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2;
keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3;
keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4;
//消抖后更新按键状态
for(i = 0; i < 4; i++)
{
if((keybuf[keyout][i] & 0x0F) == 0x00)
{//连续4次烧苗值为0,即4x4ms内都是按下状态时,可以认为按键已稳定的按下
KeySta[keyout][i] = 0;
KeyDownTime[keyout][i] += 4;//按下的持续时间累加
}
else if((keybuf[keyout][i] & 0x0F) == 0x0F)
{ //连续4次扫描值为1,即4x4ms内都是弹起状态时,可认为按键已稳定的弹起
KeySta[keyout][i] = 1;
KeyDownTime[keyout][i] = 0;//按下的持续时间清零
}
}
keyout++; //输出索引递增
keyout &= 0x03; //索引值逢4归0
switch(keyout) //根据索引,释放当前输出引脚,拉低下次的输出引脚
{
case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break;
case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break;
case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break;
case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break;
default: break;
}
}
/* LED动态扫描函数,需要在定时中断中调用 */
void LedScan()
{
static unsigned char i = 0; //动态扫描索引
P0 = 0xFF; //消除鬼影
P1 = (P1 & 0xF8) | i; // 0xF8 = 1111 1000,位选索引值赋值到P1口低3位
P0 = LedBuff[i]; //缓冲区中索引位置的数据送到P0口
if(i < 6) //索引递增循环,遍历整个缓冲区
i++;
else
i = 0;
}
/* T0中断服务函数,完成数码管、按键扫描与定时 */
void interruptTimer0() interrupt 1
{
static unsigned int tmr1s = 0; //1秒定时器
TH0 = T0RH;
TL0 = T0RL;
UartRxMonitor(2); //串口接收监控
if(enBuzz)
BUZZ = ~BUZZ; //蜂鸣器发声处理
else //驱动蜂鸣器发声
BUZZ = 1;
LedScan(); //关闭蜂鸣器
KeyScan(); //LED 扫描显示
if(flagStart) //按键扫描
{ //倒计时启动时处理1秒定时
tmr1s++;
if(tmr1s >= 500)
{
tmr1s = 0;
flag1s = 1;
}
}
else
{
tmr1s = 0; //倒计时未启动时1秒定时器始终归零
}
}
Uart.c
#include <reg52.h>
bit flagFrame = 0; //帧接收完成标志,即接收到一帧新数据
bit flagTxd = 0; //单字节发送完成标志,用来替代TXD中断标志位
unsigned char cntRxd = 0; //接收字节计数器
unsigned char pdata bufRxd[64]; //接收字节缓冲区
extern void UartAction(unsigned char *buf, unsigned char len);
/* 串口配置函数,baud-通信波特率 */
void ConfigUART(unsigned int baud)
{
SCON = 0x50; //配置串口为模式1
TMOD &= 0x0F; //清零T1的控制位
TMOD |= 0x20; //配置T1为模式2
TH1 = 256 - (11059200/12/32)/baud; //计算T1重载值
TL1 = TH1; //初值等于重载值
ET1 = 0; //禁止T1中断
ES = 1; //使能串口中断
TR1 = 1; //启动T1
}
/* 串口数据写入,即串口发送函数,buf-待发送数据的指针,len-指定的发送长度 */
void UartWrite(unsigned char *buf, unsigned char len)
{
while (len--) //循环发送所有字节
{
flagTxd = 0; //清零发送标志
SBUF = *buf++; //发送一个字节数据
while (!flagTxd); //等待该字节发送完成
}
}
/* 串口数据读取函数,buf-接收指针,len-指定的读取长度,返回值-实际读到的长度 */
unsigned char UartRead(unsigned char *buf, unsigned char len)
{
unsigned char i;
if (len > cntRxd) //指定读取长度大于实际接收到的数据长度时,
{ //读取长度设置为实际接收到的数据长度
len = cntRxd;
}
for (i=0; i<len; i++) //拷贝接收到的数据到接收指针上
{
*buf++ = bufRxd[i];
}
cntRxd = 0; //接收计数器清零
return len; //返回实际读取长度
}
/* 串口接收监控,由空闲时间判定帧结束,需在定时中断中调用,ms-定时间隔 */
void UartRxMonitor(unsigned char ms)
{
static unsigned char cntbkp = 0;
static unsigned char idletmr = 0;
if (cntRxd > 0) //接收计数器大于零时,监控总线空闲时间
{
if (cntbkp != cntRxd) //接收计数器改变,即刚接收到数据时,清零空闲计时
{
cntbkp = cntRxd;
idletmr = 0;
}
else //接收计数器未改变,即总线空闲时,累积空闲时间
{
if (idletmr < 15) //空闲计时小于30ms时,持续累加
{
idletmr += ms;
if (idletmr >= 15) //空闲时间达到30ms时,即判定为一帧接收完毕,该程序是2ms进入一次T0中断
{
flagFrame = 1; //设置帧接收完成标志
}
}
}
}
else
{
cntbkp = 0;
}
}
/* 串口驱动函数,监测数据帧的接收,调度功能函数,需在主循环中调用 */
void UartDriver()
{
unsigned char len;
unsigned char pdata buf[40];
if (flagFrame) //有命令到达时,读取处理该命令
{
flagFrame = 0;
len = UartRead(buf, sizeof(buf)); //将接收到的命令读取到缓冲区中
UartAction(buf, len); //传递数据帧,调用动作执行函数
}
}
/* 串口中断服务函数 */
void InterruptUART() interrupt 4
{
if (RI) //接收到新字节
{
RI = 0; //清零接收中断标志位
if (cntRxd < sizeof(bufRxd)) //接收缓冲区尚未用完时,
{ //保存接收字节,并递增计数器
bufRxd[cntRxd++] = SBUF;
}
}
if (TI) //字节发送完毕
{
TI = 0; //清零发送中断标志位
flagTxd = 1; //设置字节发送完成标志
}
}
I2C.c
# include<reg52.h>
# include<intrins.h>
# define I2CDelay() {_nop_();_nop_();_nop_();_nop_();}
// # define I2CDelay() {_nop_();}
sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;
/* 产生总线起始信号 */
void I2CStart()
{
I2C_SDA = 1; //首先确保SDA,SCL都是高电平
I2C_SCL = 1;
I2CDelay();
I2C_SDA = 0; //先拉低SDA
I2CDelay();
I2C_SCL = 0; //再拉低SCL
}
/* 产生总线停止信号 */
void I2CStop()
{
I2C_SCL = 0; //首先确保SDA,SCL都是低电平
I2C_SDA = 0;
I2CDelay();
I2C_SCL = 1; //先拉高SCL的电平
I2CDelay();
I2C_SDA = 1; //再拉高SDA的电平
I2CDelay();
}
/*I2C总线写操作,dat为待写入字节,返回值为从机的应答位的值 */
bit I2CWrite(unsigned char dat)
{
bit ack; //用于暂存应带位的值
unsigned char mask; //用于探测字节内一位值的掩码变量
for(mask = 0x80; mask != 0; mask >>= 1)//从高位依次进行
{
if((mask&dat) == 0)
I2C_SDA = 0;
else
I2C_SDA = 1; //通过上述语句把dat的8位电平信息从最高位开始依次发出
I2CDelay();
I2C_SCL = 1;
I2CDelay();
I2C_SCL = 0; //再拉低SCL,完成一个位周期
}
I2C_SDA = 1; //8位数据发送完后,主机释放SDA,以检测从机应答
I2CDelay();
I2C_SCL = 1; //拉高SCL
ack = I2C_SDA;//读取此时的SDA的值,即为从机的应答值
I2CDelay();
I2C_SCL = 0; //再拉低SCL完成应答位,并保持住总线
return(~ack); //应答值取反符合通常的逻辑;0 = 不纯在
//或忙或写入失败,1 = 纯在且空闲或者写入成功
}
/* I2C总线读操作,并发送非应答信号,返回值为读到的字节 */
unsigned char I2CReadNAK()
{
unsigned char mask;
unsigned char dat;
I2C_SDA = 1; //首先确保主机释放SDA
for(mask = 0x80; mask != 0; mask >>= 1) //从高位到低位依次进行
{
I2CDelay();
I2C_SCL = 1; //拉高SCL
if(I2C_SDA == 0) //读取SDA的值
dat &= ~mask; //为0时,dat中对应位清零
else
dat |= mask; //为1时,dat中对应位置1
I2CDelay();
I2C_SCL = 0; //再拉低SCL,以使从机发送下一位
}
I2C_SDA = 1; //8位数据发送完后,拉高SDA,发送非应答信号
I2CDelay();
I2C_SCL = 1; //拉高SCL
I2CDelay();
I2C_SCL = 0; //再拉低SCL完成非应答位,并保持住总线
return dat;
}
/* I2C总线操作,并发送应答信号,返回值为读到的字节 */
unsigned char I2CReadACK()
{
unsigned char mask;
unsigned char dat;
I2C_SDA = 1;
for(mask = 0x80; mask != 0; mask >>= 1)
{
I2CDelay();
I2C_SCL = 1;
if(I2C_SDA == 0)
dat &= ~mask;
else
dat |= mask;
I2CDelay();
I2C_SCL = 0;//再拉低SCL,以使从机发送出下一位
}
I2C_SDA = 0; //8位数据发送完后,拉低SDA。发送应答信号
I2CDelay();
I2C_SCL = 1; //拉高SCL
I2CDelay();
I2C_SCL = 0; //再拉低SCL完成应答,并保持住总线
return dat;
}
E2PROM.c
# include<reg52.h>
extern void I2CStart();
extern void I2CStop();
extern unsigned char I2CReadACK();
extern unsigned char I2CReadNAK();
extern bit I2CWrite(unsigned char dat);
/*E2读取函数,buf为数据接收指针,addr为E2中的起始地址,len为读取长度 ,这是一个读取相应地址并写入buf数组 的程序 */
void E2Read(unsigned char* buf,unsigned char addr,unsigned char len )
{
do{
I2CStart(); //用寻址操作查询当前是否可以进行读写操作
if(I2CWrite(0x50<<1)) //应答则跳出循环,非应答则进行下一次查询
{
break;
}
I2CStop();
}while(1);
I2CWrite(addr); //写入起始地址
I2CStart(); //发送重复启动信号
I2CWrite((0x50<<1) | 0x01);//寻址器件后续为读操作
while(len > 1) //连续读取len-1个字节
{
*buf++ = I2CReadACK(); //最后字节前为读取操作+应答
len--;
}
*buf = I2CReadNAK(); //最后一个字节为读操作+非应答
I2CStop();
}
/* E2写入函数,buf为数据指针,addr为E2中的起始地址,len为写入长度 */
void E2Write(unsigned char* buf,unsigned char addr, unsigned char len)
{
while(len > 0)
{ //等待上次写入操作完成
do{ //用寻址操作查询当前是否可以进行读写操作
I2CStart();
if(I2CWrite(0x50 << 1)) //应答则跳出循环,非应答则进行下一次查询
{
break;
}
I2CStop();
}while(1);
//按页写入模式连续写入字节
I2CWrite(addr); //写入起始地址
while(len > 0)
{
I2CWrite(*buf++); //写入一个字节数据
len--; //待写入长度计数递减
addr++; //E2地址递增
if((addr&0x07) == 0)//检查地址是否到达页边界,24C02每页8字节
{ //所以检测低3位是否为0即可
break; //到达页边界时,跳出循环,结束本次写操作
}
}
I2CStop();
}
}
Lcd1602.c
#include <reg52.h>
#define LCD1602_DB P0
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;
/* 等待液晶准备好 */
void LcdWaitReady()
{
unsigned char sta;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do {
LCD1602_E = 1;
sta = LCD1602_DB; //读取状态字
LCD1602_E = 0;
} while (sta & 0x80); //bit7等于1表示液晶正忙,重复检测直到其等于0为止
}
/* 向LCD1602液晶写入一字节命令,cmd-待写入命令值 */
void LcdWriteCmd(unsigned char cmd)
{
LcdWaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}
/* 向LCD1602液晶写入一字节数据,dat-待写入数据值 */
void LcdWriteDat(unsigned char dat)
{
LcdWaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}
/* 设置显示RAM起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */
void LcdSetCursor(unsigned char x, unsigned char y)
{
unsigned char addr;
if (y == 0) //由输入的屏幕坐标计算显示RAM的地址
addr = 0x00 + x; //第一行字符地址从0x00起始
else
addr = 0x40 + x; //第二行字符地址从0x40起始
LcdWriteCmd(addr | 0x80); //设置RAM地址
}
/* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针 */
void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str)
{
LcdSetCursor(x, y); //设置起始地址
while (*str != '\0') //连续写入字符串数据,直到检测到结束符
{
LcdWriteDat(*str++);
}
}
/* 区域清除,清除从(x,y)坐标起始的len个字符位 */
void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len)
{
LcdSetCursor(x, y); //设置起始地址
while (len--) //连续写入空格
{
LcdWriteDat(' ');
}
}
/* 初始化1602液晶 */
void InitLcd1602()
{
// LcdWriteCmd(0x38); //16*2显示,5*7点阵,8位数据接口
// LcdWriteCmd(0x0C); //显示器开,光标关闭
// LcdWriteCmd(0x06); //文字不动,地址自动+1
// LcdWriteCmd(0x01); //清屏
LcdWriteCmd(0x38);//0x38 = 0011 1000 16*2显示,5*7点阵,8位数据接口
LcdWriteCmd(0x08);//显示关闭
LcdWriteCmd(0x01);//清屏
LcdWriteCmd(0x06);//0x04 = 0000 0100 文字不动,地址自动加1
LcdWriteCmd(0x0C);//显示器开 ,光标关闭
}
贴个相关操作视频
UART和I2C通信综合应用_哔哩哔哩_bilibili
UART串口工作
简易的流程图: https://docs.qq.com/s/sUMg3jiBzjcUANe_68SHnq
只是UART工作的流程图不是全程序的,笔者自己编的,凑活看吧。