51单片机使用I2C总线

现在用到的外设越来越高级了,不得不研究了一下怎么使用I2C总线。下面先介绍I2C总线的基本原理,然后给出51单片机普通口模拟I2C时序进行读写的代码。

===================阶段一:I2C电气特性=================

I2C总线共由两条线路构成,一条为时钟线SCL(也可以写作SCK),一条为数据线SDA。可以同时有多个设备挂在I2C总线上,但其中只能有一个主设备(Master),从设备(Slave)可以有多个。通常MCU作为主设备,各种I2C外设作为从设备,如图:

主、从设备的主要区别在于时钟线SCL的控制权。在I2C总线系统中,只有主设备有权力控制时钟线。

在空闲状态下,由于上拉电阻的作用,SCL和SDA都保持高电平。此时,如果某个设备想发送数据,那么将通过拉低、释放(重新返回高电平)SCL和SDA来表达数据。

===================阶段二:I2C数据表达================

在I2C系统中,传输的数据必须以8位字节的形式传输,高位优先发送。假设数据从主设备发送到从设备,那么,主设备在发送每一位时,都是先将SCL拉低,然后将SDA置为要发送的位,然后将SCL拉高。而从设备在SCL从低电平跳变到高电平时读取SDA的值。假设要发送二进制的01011001,那么电平时序如图:

在每个SCL为高电平时采样SDA的值,就是01011001了。

如果数据从从设备发送到主设备,那么,从设备在每次SCL拉低时将SDA置为要发送的位,主设备在把SCL拉高后读取。

当然,除了传输8位字节以外,I2C还需要一些辅助信号,比如START、STOP和ACK。

START信号表明主设备要开始一次传输了,它的定义是——在SCL为高电平时,SDA发生负跳变。相对应的,STOP信号表明主设备要结束一次传输,它的定义是——在SCL为高电平时,SDA发生正跳变,如图:

由于所有设备都处在同一条总线上,所有信息都相当于广播,因此同时只能有一个设备向另外一个设备传输数据。为了能够指定数据的接收者,I2C总线上的每个从设备都有一个7位的地址,这个地址通常是外设出厂时就预设的。比如ADXL345(三轴加速度传感器)的地址就是0x53,而24C04A(EEPROM)的地址就是0x50。

如果主设备要向一个设备的某个寄存器中写入某个数据,那么总线上传输的数据如下:

(1)主设备发送START信号,表明传输开始;
(2)主设备发送从设备的设备地址与W标记,指定哪个从设备和写操作;
(3)从设备发送ACK响应;
(4)主设备发送寄存器地址,指定哪个寄存器;
(5)从设备发送ACK响应;
(6)主设备发送数据。
(7)从设备发送ACK响应;
(8)主设备发送STOP信号。

其中步骤2中,先发送7位设备地址,再发送一位0。0表示写操作。

如果主设备要从一个设备的某个寄存器中读取某个数据,那么总线上传输的数据如下:

(1)主设备发送START信号,表明传输开始;
(2)主设备发送从设备的设备地址与W标记,指定哪个从设备和写操作;
(3)从设备发送ACK响应;
(4)主设备发送寄存器地址,指定哪个寄存器;
(5)从设备发送ACK响应;
(6)主设备发送START信号;
(7)主设备发送从设备的设备地址与R标记,指定哪个从设备和读操作;
(8)从设备发送数据,主设备接收数据;
(9)主设备发送ACK后再发送STOP信号。

其中步骤2中,先发送7位设备地址,再发送一位1。1表示读操作。

一次完整的i2c交互如图:

===================阶段三:代码实现=================
types.h

#ifndef TYPES_H
#define TYPES_H

typedef char bool;
typedef char int8;
typedef unsigned char uint8;
typedef int int16;
typedef unsigned int uint16;
typedef long int32;
typedef unsigned long uint32;

#define true 1
#define false 0
#define null 0

#endif

i2c.h

/*
本文件定义了i2c总线操作的函数:

向指定设备地址的指定寄存器写入指定的值
void i2c_write(uint8 p_address,uint8 p_register,uint8 p_data)

从指定设备地址的指定寄存器中读取值,返回读取的值
uint8 i2c_read(uint8 p_address,uint8 p_register);

向指定设备地址的指定寄存器开始,连续写入指定长度的数据
void i2c_multi_write(uint8 p_address,uint8 p_register_start,uint8 p_count,uint8* p_buffer);

从指定设备地址的指定寄存器开始,连续读出指定长度的数据
void i2c_multi_read(uint8 p_address,uint8 p_register_start,uint8 p_count,uint8* p_buffer);

如果要修改SCL和SDA的引脚,请到i2c.c中修改I2C_SCL和I2C_SDA宏
如果要修改I2C的速率,请到i2c.c中修改I2C_DELAY宏

*/
#ifndef I2C_H
#define I2C_H

#include "types.h"

void i2c_write(uint8 p_address,uint8 p_register,uint8 p_data);

uint8 i2c_read(uint8 p_address,uint8 p_register);

void i2c_multi_write(uint8 p_address,uint8 p_register_start,uint8 p_count,uint8* p_buffer);

void i2c_multi_read(uint8 p_address,uint8 p_register_start,uint8 p_count,uint8* p_buffer);

#endif

i2c.c

#define I2C_SCL P1^0
#define I2C_SDA P1^1
#define I2C_DELAY 10

#include "i2c.h"
#include "reg51.h"

sbit SCL=I2C_SCL;
sbit SDA=I2C_SDA;

static void i2c_delay(uint8 p_loop)
{
    while(p_loop--);
}

static void i2c_start()
{
    SDA=1;
    SCL=1;
    i2c_delay(I2C_DELAY);
    SDA=0;
    i2c_delay(I2C_DELAY);
    SCL=0;
}

static void i2c_stop()
{
    SDA=0;
    SCL=1;
    i2c_delay(I2C_DELAY);
    SDA=1;
    i2c_delay(I2C_DELAY);
}

static void i2c_send_ack(bool p_ack)
{
    SDA=p_ack;
    SCL=1;
    i2c_delay(I2C_DELAY);
    SCL=0;
    i2c_delay(I2C_DELAY);
}

static uint8 i2c_recv_ack()
{
    uint8 t_ack;
    SCL=1;
    i2c_delay(I2C_DELAY);
    t_ack=SDA;
    SCL=0;
    i2c_delay(I2C_DELAY);
    return t_ack;
}

static uint8 i2c_send_byte(uint8 p_data)
{
    uint8 t_i;
    for(t_i=0;t_i<8;t_i++)
    {
        SDA=p_data&0x80;
        p_data<<=1;
        SCL=1;
        i2c_delay(I2C_DELAY);
        SCL=0;
        i2c_delay(I2C_DELAY);
    }
    return i2c_recv_ack();
}

static uint8 i2c_recv_byte()
{
    uint8 t_i;
    uint8 t_data=0;
    SDA=1;
    for(t_i=0;t_i<8;t_i++)
    {
        t_data<<=1;
        SCL=1;
        i2c_delay(I2C_DELAY);
        t_data|=SDA;
        SCL=0;
        i2c_delay(I2C_DELAY);
    }
    return t_data;
}

void i2c_write(uint8 p_address,uint8 p_register,uint8 p_data)
{
    p_address<<=1;
    i2c_start();
    i2c_send_byte(p_address);
    i2c_send_byte(p_register);
    i2c_send_byte(p_data);
    i2c_stop();
}

uint8 i2c_read(uint8 p_address,uint8 p_register)
{
    uint8 t_data;
    i2c_multi_read(p_address,p_register,1,&t_data);
    return t_data;
}

void i2c_multi_write(uint8 p_address,uint8 p_register_start,uint8 p_count,uint8* p_buffer)
{
    uint8 t_i;
    for(t_i=0;t_i<p_count;t_i++)
        i2c_write(p_address,p_register_start+t_i,p_buffer[t_i]);
}

void i2c_multi_read(uint8 p_address,uint8 p_register_start,uint8 p_count,uint8* p_buffer)
{
    uint8 t_i;
    p_address<<=1;
    i2c_start();
    i2c_send_byte(p_address);
    i2c_send_byte(p_register_start);
    i2c_start();
    i2c_send_byte(p_address+1);
    for(t_i=0;t_i<p_count;t_i++)
    {
        p_buffer[t_i]=i2c_recv_byte();
        i2c_send_ack(t_i==p_count-1);
    }
    i2c_stop();
}

=========================阶段四:测试======================

首选把i2c.c中的

#define I2C_SCL P1^0
#define I2C_SDA P1^1

改为

#define I2C_SCL P3^0
#define I2C_SDA P3^1

然后编写main函数如下:

#include "reg51.h"
#include "i2c.h"

void delay_ms(uint16 p_ms)
{
    uint8 t_timer;
    while(p_ms--)
        for(t_timer=0;t_timer<250;t_timer++);
}

void main()
{
    uint8 t_index;
    for(t_index=0;t_index<100;t_index++)
        i2c_write(0x50,t_index,t_index);
    while(1)
    {
        for(t_index=0;t_index<100;t_index++)
        {
            P1=i2c_read(0x50,t_index);
            delay_ms(100);
        }
    }
    while(1);
}

搭建电路图如下:

该示例中,使用P3^0作为SCL,用P3^1作为SDA,先往EEPROM中写入数据,地址为i的地方放入数值i,i=0…99。然后就循环读取每个地址中的数值,并通过P1口显示出来。

运行后,可以看见P1口的数字不断递增,然后回到0。。。