51单片机快速入门之 模拟 I2C 用精准中断来控制
首先复习一下51单片机快速入门之定时器和计数器(含中断基础)
再看看之前的I2C操作 51单片机快速入门之 IIC I2C通信
定时器/计数器是51单片机中用于实现精确延时的硬件资源。通过配置定时器的初始值和工作模式,可以实现不同长度的延时。
例如,如果使用12MHz晶振,一个机器周期为1μs,可以通过设置定时器的初值来实现精确的延时。
// 假设需要延时1ms
void delay_1ms()
{
unsigned int i;
TMOD |= 0x01; // 设置定时器0为工作方式1
TH0 = 0xFC; // 设置初值,1ms延时
TL0 = 0x18;
TR0 = 1; // 启动定时器
while(!TF0); // 等待定时器溢出
TR0 = 0; // 关闭定时器
TF0 = 0; // 清除溢出标志
}
软件延时是通过循环语句实现的,虽然不如定时器精确,但在不需要极高精度的场合也可以使用。为了提高精度,可以使用
_NOP_()
函数,它是C51编译器提供的一个空操作指令,执行时间为1个机器周期。
#include <intrins.h>
// 假设需要延时10μs
void delay_10us(unsigned char t)
{
do
{
_nop_(); _nop_(); _nop_(); _nop_(); // 4个_NOP_()指令,共4μs
_nop_(); _nop_(); _nop_(); _nop_(); // 另外4μs
}
while(--t);
}
下面开始写代码:
#include <STC89C5xRC.H>sbit SCL=P2^0; //时钟线
sbit SDA=P2^1; //数据线
sbit DQ=P2^4;//读取按键
void delay_2us(); // 延时函数声明
void startI2C();//开始信号
void stopI2C();//停止信号
bit ack; //用于存储ACK信息
void ack_i2c(); //用于处理ACK信息
void SendByte(unsigned char date);//发送字节数据void main()
{startI2C();//开始信号SendByte(0xA2);//发送从机地址ack_i2c();SendByte(0x00);//设置数据需要保存在哪个地方 地址!ack_i2c();SendByte(0X66);//发送 数据ack_i2c();stopI2C();//发送停止信号while(1); // 防止程序重复运行
}void startI2C() //开始信号{SDA=1; //拉高SDA,delay_2us(); //延时2usSCL=1; //拉高SCL 初始化delay_2us(); //延时2usdelay_2us(); //延时2usSDA=0; //拉低SDA 使其进入下降沿delay_2us(); //延时2usSCL=0; //拉低SCL准备接收数据delay_2us(); //延时2usdelay_2us(); //延时2usdelay_2us(); //延时2us}void stopI2C() //停止信号
{SDA=0; //拉低SDA准备进入上升沿delay_2us(); //延时2usSCL=1;//拉高SCL让其 波形长度领先delay_2us(); //延时2usdelay_2us(); //延时2usSDA=1;delay_2us(); //延时2us/*SCL领先 SDA 此时进入上升沿 占用 现在SCL与SDA波长相等*/}void SendByte(unsigned char date)//发送字节数据
{unsigned char j; //创建一个变量用于传输for(j=0; j<8; j++) {if(date<<j&0x80)/*每次往左移动j 变量位 这里加()是防止意外错误 再 和 0x80 1000 0000 进行与运算AXB */{SDA=1; //当其最高位为1时, 结果是1 SDA 拉高} else {SDA=0; //当其最高位为0时,结果是0 SDA 拉低}delay_2us(); //延时2usSCL=1;//拉高以发送现在SDA状态 到从机delay_2us(); //延时2usdelay_2us(); //延时2usSCL=0; //拉低开始接收下一个数据delay_2us(); //延时2usdelay_2us(); //延时2usdelay_2us(); //延时2us} SDA=1;//拉高预防因数据最后一位是0 SDA=0 而导致 进入停止信号SCL=1;//拉高以发送现在SDA状态 到从机 SDA=1;//拉高进入ack检测delay_2us(); //延时2usif(SDA==0) { //从机发来ACK响应ack=1; //表接收到ACK响应 SCL=0;delay_2us(); //延时2usdelay_2us(); //延时2usdelay_2us(); //延时2us} else {stopI2C();}}void ack_i2c()
{if(ack==1) {ack=0; //满足条件初始化} else {while(1); //不满足条件阻塞程序不让其往后执行}}void delay_2us()
{TMOD=0x10;//设置使用定时器1 0001 0000 不需要io触发 断开引脚输入 16位模式TH1 = 255; // 因为65534 / 256 = 255 存放高8位 256x256=65536TL1 = 254; // 因为65534 % 256 = 存放低8位 初值计算 255*256+254=65,534/*65536-65534=2 us12MHz 晶振振荡 12分频之后,为1MHz 当其从0-65536 时,需要65536μs 微秒 */EA=1;//打开总中断ET1=1;//允许定时器1中断TR1=1;//打开定时器1while(!TF1);//判断是否溢出/*感叹号 ! 是逻辑非运算符如果 TF1 是 1(定时器1溢出), 则 !TF1 是 0(假)。 向中断发送了请求如果 TF1 是 0(定时器1未溢出),则 !TF1 是 1(真)。一旦 TF1 变为1(溢出),!TF1 就变为0,循环结束*/TR1=0;//关闭定时器1TF1=0;//初始化为未溢出状态}
写入24c02c 运行效果(单字节写入):
- 开始信号
- 7从机地址和 写入0
- 从机回复ACK
- 数据存储地址
- 从机回复 ACK
- 数据
- 从机回复ACK
- 停止信号
数据读取代码(选择读取):
- 开始信号
- 7从机地址和 写入0 伪写操作
- 从机回复ACK
- 数据存储地址
- 从机回复ACK
- 开始信号
- 7从机地址和 读取1 读取操作
- 从机回复ACK
- 获取从机发来的8位数据
- 拉高SDA 主机发送NACK
- 停止信号
读取数据并控制P1 寄存器 应用 以验证效果 ,代码如下
#include <STC89C5xRC.H>sbit SCL=P2^0; //时钟线
sbit SDA=P2^1; //数据线
sbit DQ=P2^4;//读取按键
void delay_2us(); // 延时函数声明
void startI2C();//开始信号
void stopI2C();//停止信号
bit ack; //用于存储ACK信息
void ack_i2c(); //用于处理ACK信息
void SendByte(unsigned char date);//发送字节数据
unsigned char D1,C1;//D1用于发送8位脉冲,C1用于保存读取到的数据
void REI2C();//读取8位函数
void main()
{startI2C();//开始信号SendByte(0xA2);//发送从机地址ack_i2c();SendByte(0x00);//设置数据需要保存在哪个地方 地址!ack_i2c();SendByte(0X66);//发送 数据ack_i2c();stopI2C();//发送停止信号while(DQ==1); //当其为高电平时,表示按钮未按下阻塞程序startI2C();//开始信号SendByte(0xA2);//发送 伪写入从机地址ack_i2c();SendByte(0x00);//设置要读取的 数据存储地址ack_i2c();startI2C();//开始信号SendByte(0xA3);//发送 读取从机地址ack_i2c();REI2C();P1 = 0x00; // 先将 P1 置为 0 P1 = C1; // 再赋值 ack_i2c();stopI2C();//停止信号while(1); // 防止程序重复运行
}void startI2C() //开始信号{SDA=1; //拉高SDA,delay_2us(); //延时2usSCL=1; //拉高SCL 初始化delay_2us(); //延时2usdelay_2us(); //延时2usSDA=0; //拉低SDA 使其进入下降沿delay_2us(); //延时2usSCL=0; //拉低SCL准备接收数据delay_2us(); //延时2usdelay_2us(); //延时2usdelay_2us(); //延时2us}void stopI2C() //停止信号
{SDA=0; //拉低SDA准备进入上升沿delay_2us(); //延时2usSCL=1;//拉高SCL让其 波形长度领先delay_2us(); //延时2usdelay_2us(); //延时2usSDA=1;delay_2us(); //延时2us/*SCL领先 SDA 此时进入上升沿 占用 现在SCL与SDA波长相等*/}void SendByte(unsigned char date)//发送字节数据
{unsigned char j; //创建一个变量用于传输for(j=0; j<8; j++) {if(date<<j&0x80)/*每次往左移动j 变量位 这里加()是防止意外错误 再 和 0x80 1000 0000 进行与运算AXB */{SDA=1; //当其最高位为1时, 结果是1 SDA 拉高} else {SDA=0; //当其最高位为0时,结果是0 SDA 拉低}delay_2us(); //延时2usSCL=1;//拉高以发送现在SDA状态 到从机delay_2us(); //延时2usdelay_2us(); //延时2usSCL=0; //拉低开始接收下一个数据delay_2us(); //延时2usdelay_2us(); //延时2usdelay_2us(); //延时2us} SDA=1;//拉高预防因数据最后一位是0 SDA=0 而导致 进入停止信号SCL=1;//拉高以发送现在SDA状态 到从机 SDA=1;//拉高进入ack检测delay_2us(); //延时2usif(SDA==0) { //从机发来ACK响应ack=1; //表接收到ACK响应 SCL=0;delay_2us(); //延时2usdelay_2us(); //延时2usdelay_2us(); //延时2us} else {stopI2C();}}void ack_i2c()
{if(ack==1) {ack=0; //满足条件初始化} else {while(1); //不满足条件阻塞程序不让其往后执行}}void REI2C()
{ SDA=1;//初始化SDA for(D1=0;D1<8;D1++)
{SCL=1;//读取当前SDA if(SDA==1)
{C1=C1 | 1<<D1; /* |与运算A+B 运行一个循环之后 C1此时为1 0000 0001 1为0000 0001 当 D1=1时 左移一位 0000 0010
0000 0001
0000 0010
----------0000 0011 C1此时就为这个值 D1=2时 位移两位 0000 0100 0000 00110000 0100此时C1=0000 0111 注意每次移位都只是 对0000 0001 操作 ! 谨防思维混乱*/
}//因为D1是递增的,所以如果 当D1=2 时 if不满足 下次 D1=3 就会多出一个0 SCL=0; //拉低开始接收下一个数据delay_2us(); //延时2usdelay_2us(); //延时2usdelay_2us(); //延时2us}
SDA=1;//拉高预防因数据最后一位是0 SDA=0 而导致 进入停止信号SCL=1;//拉高以发送现在SDA状态 到从机 SDA=1;//拉高进入ack检测delay_2us(); //延时2usif(SDA==0) { //从机发来ACK响应ack=1; //表接收到ACK响应 SCL=0;delay_2us(); //延时2usdelay_2us(); //延时2usdelay_2us(); //延时2us} else {stopI2C();}}void delay_2us()
{TMOD=0x10;//设置使用定时器1 0001 0000 不需要io触发 断开引脚输入 16位模式TH1 = 255; // 因为65534 / 256 = 255 存放高8位 256x256=65536TL1 = 254; // 因为65534 % 256 = 存放低8位 初值计算 255*256+254=65,534/*65536-65534=2 us12MHz 晶振振荡 12分频之后,为1MHz 当其从0-65536 时,需要65536μs 微秒 */EA=1;//打开总中断ET1=1;//允许定时器1中断TR1=1;//打开定时器1while(!TF1);//判断是否溢出/*感叹号 ! 是逻辑非运算符如果 TF1 是 1(定时器1溢出), 则 !TF1 是 0(假)。 向中断发送了请求如果 TF1 是 0(定时器1未溢出),则 !TF1 是 1(真)。一旦 TF1 变为1(溢出),!TF1 就变为0,循环结束*/TR1=0;//关闭定时器1TF1=0;//初始化为未溢出状态}
运行效果!可以看到我们读取到了之前存入的 0x66 数据!
0110 0110 注意看P1的状态