IIC驱动代码_3

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

067.9.6 IIC驱动代码(3)

/* ==================== main.c ==================== */
#include "reg52.h"
#include <intrins.h>
#include "delay.h"
#include "main.h"
#include "uart.h"

uchar recv;

void main()
{
//	UART_Init();
	while(1)
  {

		Delay_xms(1000);
  }
}


/* ==================== main.h ==================== */
#ifndef _MAIN_H_
#define _MAIN_H_

#include "reg52.h"

typedef unsigned char uchar;
typedef unsigned int uint;

extern uchar recv;

sbit LED1 = P1^0;
sbit LED2 = P1^1;
sbit LED3 = P1^2;
sbit LED4 = P1^3;
sbit BEEP = P1^6;
sbit JDQ1 = P2^0;

sbit IIC_SCL = P1^5;
sbit IIC_SDA = P1^4;

#endif



/* ==================== Delay\delay.c ==================== */
#include "delay.h"
#include <intrins.h>

void Delay_xms(uint xms)	//@11.0592MHz
{
	uchar data i, j;
	while(xms)
	{
		_nop_();
		i = 2;
		j = 199;
		do
		{
			while (--j);
		} while (--i);
    xms--;		
	}
}

void Delay5us(void)	//@11.0592MHz
{
}

/* ==================== Delay\delay.h ==================== */
#ifndef _DELAY_H_
#define _DELAY_H_
#include "main.h"

void Delay_xms(uint xms);
void Delay5us(void);

#endif

/* ==================== IIC\iic.c ==================== */
#include "iic.h"
#include "main.h"
#include "delay.h"

void IIC_Start(void)
{
	IIC_SCL = 1;
	IIC_SDA = 1;
	Delay5us();
	IIC_SDA = 0;
	Delay5us();
	IIC_SCL = 0;
}

void IIC_Stop(void)
{
	IIC_SCL = 1;
	IIC_SDA = 0;
	Delay5us();
	IIC_SDA = 1;
}

void IIC_Send_Byte(unsigned char IIC_Byte)
{
	char i;
	for(i = 0; i<8; i++)
	{		
		IIC_SDA = IIC_Byte & (0x80 >> i);
		IIC_SCL = 1;
		Delay5us();
		IIC_SCL = 0;
		Delay5us();		
	}
}

unsigned char IIC_Recv_Ack(void)
{
	unsigned char ack;
	IIC_SDA = 1;
	IIC_SCL = 1;
	Delay5us();
	ack = IIC_SDA;
	IIC_SCL = 0;
	Delay5us();	
	
	return ack;
}

/* ==================== IIC\iic.h ==================== */
#ifndef _IIC_H_
#define _IIC_H_

void IIC_Start(void);
void IIC_Stop(void);
void IIC_Send_Byte(unsigned char IIC_Byte);
unsigned char IIC_Recv_Ack(void);

#endif



/* ==================== Uart\uart.c ==================== */
#include "uart.h"
#include "main.h"

//串口发送一个字节
void UART_Send_Byte(uchar send_byte)
{
	SBUF = send_byte;
	while(!TI);
	TI=0;
}

//串口发送字符串
void UART_Send_Str(uchar *send_str)
{	
	while(*send_str != '\0') //判断字符串结束符,如果识别到‘\0',则跳出while,结束发送
	{
		UART_Send_Byte(*send_str++);	//本质是一个字节一个字节的发送,
		//发送完一个字节,地址+1,等待发送下一个字节,直到检测到字符串结束符为止
	}	
} 

void UART_Init(void)
{
	SCON = 0x50; 	//串口配成工作方式1
	PCON &= 0x7F; //波特率不加倍
	TMOD &= 0x0f;	
	TMOD |= 0x20;	//定时器1,模式二,自动重装初值,串口按照一定速率发数据,需要用的定时器
	TH1 = 0xFD;	
	TL1 = 0xFD; //@11.0592MH时钟,9600比特率定时器初值
	TR1 = 1; //定时器开始运行   
	ES = 1;  //串口中断打开
	EA=1;					//总中断打开
	recv = 0;
}

/* ==================== Uart\uart.h ==================== */
#ifndef _UART_H_
#define _UART_H_
#include "main.h"

void UART_Send_Byte(uchar send_byte);
void UART_Send_Str(uchar *send_str);
void UART_Init(void);

#endif

一、发送数据

I²C 的最小数据单位是 bit,8 bit 组成 1 byte。总线在 SCL 的每个有效时钟周期传送一位,但 START、STOP 和 ACK 还会占用额外时序,因此“发送 1 字节”不等于只有 8 个 SCL 脉冲。

1.1 数据发送的基本单位与规则

一个字节的位编号如下:

位号: bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
权值:  128   64   32   16    8    4    2    1
顺序:   ①    ②    ③    ④    ⑤    ⑥    ⑦    ⑧

I²C 规定最高位先发送,即 MSB first。发送 0xA5 时:

0xA5 = 1010 0101
发送顺序:1 → 0 → 1 → 0 → 0 → 1 → 0 → 1

1.2 时钟信号与数据发送速率

每一个 SCL 周期完成一位数据的传输:

SCL:____/‾‾‾‾\____
SDA:==== 有效位 =====
       准备  采样

100 kHz 表示每秒最多约 100000 个时钟周期,不表示每秒发送 100000 个有效数据字节。以最简单的“8 位数据+1 位 ACK”计算,一个字节至少需要 9 个时钟;再加地址、控制字节和 START/STOP,实际有效吞吐率更低。

SCL 的速率必须同时满足主机、从机和电路条件。软件延时只设置目标速率,最终还要把指令执行时间、函数调用和中断延迟计算进去。

1.3 SDA与SCL的耦合规则

普通数据位的一个周期可分成三段:

  1. SCL 为低,发送方设置 SDA;
  2. 等待数据建立时间;
  3. SCL 为高,接收方采样,SDA 保持稳定;
  4. SCL 再次为低,才准备下一位。
sequenceDiagram
    participant M as 主机GPIO
    participant B as SCL/SDA总线
    participant S as 从机
    M->>B: SCL保持低,设置SDA
    M->>B: 释放SCL为高
    B->>S: 从机在高电平阶段采样SDA
    M->>B: 拉低SCL
    Note over M,B: 可准备下一位

I²C 不强制所有从机恰好在某一个理想瞬间采样,但规范保证 SCL 高期间数据有效。因此软件必须满足建立时间和保持时间。

1.4 例题1:SDA电平变化规则

核心规则是:传输普通数据时,SCL 高电平期间 SDA 不得改变;SDA 应在 SCL 低电平期间改变。

例外正是 START 和 STOP:

SCL高时 SDA 1→0:START,不是普通数据0
SCL高时 SDA 0→1:STOP,不是普通数据1

这也是为什么错误地在 SCL 高时切换数据,可能导致从机把它误判为一次新的传输或传输结束。

二、发送一个字节数据

2.1 发送字节数据的基本思路

IIC_Send_Byte() 用循环执行 8 次,每次完成“提取一个 bit—设置 SDA—产生一个 SCL 脉冲”。循环变量只需表示 0~7,使用 unsigned char 比有符号 char 的意图更明确,但当前 char i 在本函数范围内也不会溢出。

伪代码为:

for i = 0..7
    取出 bit(7-i)
    在 SCL 低时把该位放到 SDA
    SCL 拉高并保持
    SCL 拉低并保持
结束

2.2 时钟信号与数据线的操作

配套代码的核心循环是:

for(i = 0; i < 8; i++)
{
    IIC_SDA = IIC_Byte & (0x80 >> i);
    IIC_SCL = 1;
    Delay5us();
    IIC_SCL = 0;
    Delay5us();
}

赋给 sbit 时,表达式结果为 0 就输出低;任何非 0 值都按逻辑真输出高。因此掩码结果即使是 0x80、0x40 而不是数值 1,也能得到期望电平。

为把数据建立时间写得更清楚,可将顺序组织为:

IIC_SCL = 0;
IIC_SDA = (IIC_Byte & (0x80 >> i)) ? 1 : 0;
Delay5us();       // 数据在SCL上升前稳定
IIC_SCL = 1;
Delay5us();       // SCL高期间保持数据

配套实现把低电平延时放在上一轮末尾;从连续波形看仍有低电平阶段,但本位 SDA 更新后到 SCL 上升之间只有几条指令的时间。低速 51 上通常可能满足建立时间,显式延时则更便于移植和审查。

2.3 数据发送的顺序与位操作

掩码 0x80 的二进制形式只有 bit7 为 1:

待发数据: b7 b6 b5 b4 b3 b2 b1 b0
掩码:      1  0  0  0  0  0  0  0
按位与:   b7  0  0  0  0  0  0  0

若 b7 为 1,结果非 0,SDA 释放为高;若 b7 为 0,结果为 0,SDA 被拉低。

例如发送 0xA5:

i0x80 >> i与 0xA5 的结果SDA
00x800x801
10x400x000
20x200x201
30x100x000
40x080x000
50x040x041
60x020x000
70x010x011

2.4 移位操作实现数据发送

配套代码移动的是 掩码

0x80 >> 0 = 10000000
0x80 >> 1 = 01000000
0x80 >> 2 = 00100000
...
0x80 >> 7 = 00000001

另一种等价写法是始终检查最高位,然后左移待发数据:

for (i = 0; i < 8; i++)
{
    IIC_SDA = (IIC_Byte & 0x80) ? 1 : 0;
    IIC_Byte <<= 1;
    // 产生一个SCL脉冲
}

两种方案都可以,但不要把右移掩码和左移数据混杂得难以验证。涉及有符号数时,右移行为还可能受符号扩展影响,所以通信字节和掩码宜使用无符号类型。

2.5 例题1: 发送一个字节的代码实现

以 0xA5 为例,完整 8 位波形为:

bit:   7     6     5     4     3     2     1     0
数据:  1     0     1     0     0     1     0     1
SCL: _/‾\___/‾\___/‾\___/‾\___/‾\___/‾\___/‾\___/‾\_
SDA: ‾‾‾‾\_____/‾‾‾\___________/‾‾‾\_____/‾‾‾‾‾‾‾
         高电平窗口内,每一位都保持稳定

函数结束时 SCL 为低,为第 9 个 ACK 时钟提供准备阶段。发送函数本身还不是一个完整事务,调用者仍需组合 START、地址、ACK、数据、ACK 和 STOP。

三、主机接收从机回复的应答信号

3.1 应答信号机制详解

发送方完成 8 位后释放 SDA,接收方在第 9 个时钟回答:

前8个时钟:发送方控制 SDA
第9个时钟:接收方控制 SDA

SDA=0 → ACK:接收方确认本字节
SDA=1 → NACK:没有确认

NACK 不一定只表示“接收失败”。主机读取数据时,常在最后一个字节后主动发送 NACK,表示“我不再继续读了”,随后产生 STOP。因此必须结合传输方向和协议阶段解释。

3.2 代码实现: 接收应答信号

配套代码为:

unsigned char IIC_Recv_Ack(void)
{
    unsigned char ack;

    IIC_SDA = 1;   // 主机释放SDA
    IIC_SCL = 1;   // 第9个时钟进入高电平
    Delay5us();
    ack = IIC_SDA; // 在SCL高时采样
    IIC_SCL = 0;
    Delay5us();

    return ack;
}

执行关系:

主机:释放SDA ──→ 拉高SCL ──→ 采样SDA ──→ 拉低SCL
从机:           若确认则拉低SDA
返回:           0=ACK,1=NACK

当前函数只采样一次,没有等待 SCL 真正变高,也没有超时处理。若从机支持时钟拉伸并把 SCL 保持为低,主机直接向端口写 1 后就采样 SDA 会过早。工程化实现还应读取 SCL、等待线路释放并设置超时。

调用示例应明确处理返回值:

IIC_Send_Byte(address_byte);
if (IIC_Recv_Ack() != 0)
{
    IIC_Stop();
    return IIC_ERROR_NACK;
}

3.3 IC应用与代码移植概述

这里应为 I²C 应用。当前驱动只完成总线基本动作,还没有描述 SSD1306 的地址、控制字节、命令和显存组织。点亮 OLED 仍需补齐:

flowchart LR
    A["I²C基础驱动"] --> B["SSD1306写命令/写数据"]
    B --> C["SSD1306初始化"]
    C --> D["字符、汉字、图片函数"]
    D --> E["main()应用流程"]

移植时应保留自己已经验证过的 IIC_Start()、IIC_Stop()、IIC_Send_Byte()、IIC_Recv_Ack(),把 OLED 示例里对底层函数的调用替换为这些接口;随后核对引脚、地址 0x3C/地址字节 0x78、控制字节 0x00/0x40、延时与返回值语义。