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(); //延时2us
SCL=1; //拉高SCL 初始化
delay_2us(); //延时2us
delay_2us(); //延时2us
SDA=0; //拉低SDA 使其进入下降沿
delay_2us(); //延时2us
SCL=0; //拉低SCL准备接收数据
delay_2us(); //延时2us
delay_2us(); //延时2us
delay_2us(); //延时2us
}
void stopI2C() //停止信号
{
SDA=0; //拉低SDA准备进入上升沿
delay_2us(); //延时2us
SCL=1;//拉高SCL让其 波形长度领先
delay_2us(); //延时2us
delay_2us(); //延时2us
SDA=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(); //延时2us
SCL=1;//拉高以发送现在SDA状态 到从机
delay_2us(); //延时2us
delay_2us(); //延时2us
SCL=0; //拉低开始接收下一个数据
delay_2us(); //延时2us
delay_2us(); //延时2us
delay_2us(); //延时2us
}
SDA=1;//拉高预防因数据最后一位是0 SDA=0 而导致 进入停止信号
SCL=1;//拉高以发送现在SDA状态 到从机
SDA=1;//拉高进入ack检测
delay_2us(); //延时2us
if(SDA==0) { //从机发来ACK响应
ack=1; //表接收到ACK响应
SCL=0;
delay_2us(); //延时2us
delay_2us(); //延时2us
delay_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=65536
TL1 = 254; // 因为65534 % 256 = 存放低8位 初值计算 255*256+254=65,534
/*65536-65534=2 us
12MHz 晶振振荡 12分频之后,为1MHz 当其从0-65536 时,需要65536μs 微秒 */
EA=1;//打开总中断
ET1=1;//允许定时器1中断
TR1=1;//打开定时器1
while(!TF1);//判断是否溢出
/*感叹号 ! 是逻辑非运算符
如果 TF1 是 1(定时器1溢出), 则 !TF1 是 0(假)。 向中断发送了请求
如果 TF1 是 0(定时器1未溢出),则 !TF1 是 1(真)。
一旦 TF1 变为1(溢出),!TF1 就变为0,循环结束
*/
TR1=0;//关闭定时器1
TF1=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(); //延时2us
SCL=1; //拉高SCL 初始化
delay_2us(); //延时2us
delay_2us(); //延时2us
SDA=0; //拉低SDA 使其进入下降沿
delay_2us(); //延时2us
SCL=0; //拉低SCL准备接收数据
delay_2us(); //延时2us
delay_2us(); //延时2us
delay_2us(); //延时2us
}
void stopI2C() //停止信号
{
SDA=0; //拉低SDA准备进入上升沿
delay_2us(); //延时2us
SCL=1;//拉高SCL让其 波形长度领先
delay_2us(); //延时2us
delay_2us(); //延时2us
SDA=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(); //延时2us
SCL=1;//拉高以发送现在SDA状态 到从机
delay_2us(); //延时2us
delay_2us(); //延时2us
SCL=0; //拉低开始接收下一个数据
delay_2us(); //延时2us
delay_2us(); //延时2us
delay_2us(); //延时2us
}
SDA=1;//拉高预防因数据最后一位是0 SDA=0 而导致 进入停止信号
SCL=1;//拉高以发送现在SDA状态 到从机
SDA=1;//拉高进入ack检测
delay_2us(); //延时2us
if(SDA==0) { //从机发来ACK响应
ack=1; //表接收到ACK响应
SCL=0;
delay_2us(); //延时2us
delay_2us(); //延时2us
delay_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 0011
0000 0100
此时C1=0000 0111
注意每次移位都只是 对0000 0001 操作 ! 谨防思维混乱
*/
}//因为D1是递增的,所以如果 当D1=2 时 if不满足 下次 D1=3 就会多出一个0
SCL=0; //拉低开始接收下一个数据
delay_2us(); //延时2us
delay_2us(); //延时2us
delay_2us(); //延时2us
}
SDA=1;//拉高预防因数据最后一位是0 SDA=0 而导致 进入停止信号
SCL=1;//拉高以发送现在SDA状态 到从机
SDA=1;//拉高进入ack检测
delay_2us(); //延时2us
if(SDA==0) { //从机发来ACK响应
ack=1; //表接收到ACK响应
SCL=0;
delay_2us(); //延时2us
delay_2us(); //延时2us
delay_2us(); //延时2us
} else {
stopI2C();
}
}
void delay_2us()
{
TMOD=0x10;//设置使用定时器1 0001 0000 不需要io触发 断开引脚输入 16位模式
TH1 = 255; // 因为65534 / 256 = 255 存放高8位 256x256=65536
TL1 = 254; // 因为65534 % 256 = 存放低8位 初值计算 255*256+254=65,534
/*65536-65534=2 us
12MHz 晶振振荡 12分频之后,为1MHz 当其从0-65536 时,需要65536μs 微秒 */
EA=1;//打开总中断
ET1=1;//允许定时器1中断
TR1=1;//打开定时器1
while(!TF1);//判断是否溢出
/*感叹号 ! 是逻辑非运算符
如果 TF1 是 1(定时器1溢出), 则 !TF1 是 0(假)。 向中断发送了请求
如果 TF1 是 0(定时器1未溢出),则 !TF1 是 1(真)。
一旦 TF1 变为1(溢出),!TF1 就变为0,循环结束
*/
TR1=0;//关闭定时器1
TF1=0;//初始化为未溢出状态
}
运行效果!可以看到我们读取到了之前存入的 0x66 数据!
0110 0110 注意看P1的状态