IIC驱动代码_3
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的耦合规则
普通数据位的一个周期可分成三段:
- SCL 为低,发送方设置 SDA;
- 等待数据建立时间;
- SCL 为高,接收方采样,SDA 保持稳定;
- 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:
| i | 0x80 >> i | 与 0xA5 的结果 | SDA |
|---|---|---|---|
| 0 | 0x80 | 0x80 | 1 |
| 1 | 0x40 | 0x00 | 0 |
| 2 | 0x20 | 0x20 | 1 |
| 3 | 0x10 | 0x00 | 0 |
| 4 | 0x08 | 0x00 | 0 |
| 5 | 0x04 | 0x04 | 1 |
| 6 | 0x02 | 0x00 | 0 |
| 7 | 0x01 | 0x01 | 1 |
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、延时与返回值语义。