IIC驱动代码_1
065.9.4 IIC驱动代码(1)
/* ==================== 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
一、IIC时序
I²C 软件驱动的目标不是“让 GPIO 随便跳变”,而是让 SCL、SDA 严格满足总线规定。课程把时序和代码放在一起学习,最终需要建立“波形条件—GPIO 语句—总线含义”的一一对应关系。
1.1 IC时序的六个知识点
这里的 IC 应为 I²C。六个核心知识点如下:
| 状态 | 发起者 | 关键条件 |
|---|---|---|
| 空闲 | 所有器件释放总线 | SCL=1,SDA=1 |
| START | 主机 | SCL=1 时 SDA 由 1→0 |
| STOP | 主机 | SCL=1 时 SDA 由 0→1 |
| 主机发送 | 主机驱动 SDA | 8 位,MSB 先发,SCL 高时稳定 |
| 主机接收 | 从机驱动 SDA | 主机释放 SDA 并产生 SCL |
| ACK/NACK | 每字节的接收方 | 第 9 个时钟,低为 ACK、高为 NACK |
“ACK 总由从机产生”只适用于主机写数据阶段。主机读从机数据时,接收方变成主机,所以每个字节后的 ACK/NACK 由主机产生。判断谁负责应答,应看当前字节是谁发送、谁接收。
1.2 IC的代码demo
OLED 示例中的 IIC_Start()、IIC_Stop()、Write_IIC_Byte() 和 IIC_Wait_Ack() 展示了软件模拟 I²C 的基本形态,但学习时不能把函数名当成正确性的保证。应逐句对照规范波形检查:
函数语句 → 引脚电平 → 是否在正确的 SCL 阶段改变 SDA → 总线条件
尤其要检查 SDA 写 1 到底表示“主动输出高”还是“释放线路”,以及应答函数是否真的采样了 SDA。配套 9.2 工程的 IIC_Recv_Ack() 会读取 SDA,比演示工程只产生应答时钟更完整。
1.3 例题1: 软件IIC代码实现
软件 I²C 使用普通 GPIO 模拟总线。经典 51 准双向口写 0 时拉低,写 1 时释放/呈现高电平,适合低速入门实验;电路仍需上拉电阻。
flowchart LR
A["IIC_Start()"] --> B["IIC_Send_Byte(地址)"]
B --> C["IIC_Recv_Ack()"]
C --> D["IIC_Send_Byte(数据)"]
D --> E["IIC_Recv_Ack()"]
E --> F["IIC_Stop()"]
软件实现应同时考虑时序、ACK、超时、引脚模式和中断影响。仅能点亮 OLED 的代码不一定已经覆盖总线异常。
1.4 IC时序图详解
以 100 kHz 为例,一个完整 SCL 周期约 10 µs。代码中的 Delay5us() 分别放在高、低阶段,可形成约 5 µs 高电平加 5 µs 低电平;但 GPIO 赋值、函数调用和循环也耗时,所以真实频率通常低于理想的 100 kHz,应以逻辑分析仪测量为准。
数据准备 接收方采样
SCL ________/‾‾‾‾‾‾‾\________
SDA ----< 有效bit >---------
↑可改变 ↑高电平期间必须稳定
SDA 不是“必须和 SCL 一样快地变化”。如果连续发送多个 1 或多个 0,SDA 可以跨越几个时钟保持不变;真正的规则是每一个 SCL 高电平期间都保持本位数据稳定。
1.5 例题2:IIC时序的起始与停止条件
起始和停止都发生在 SCL 高电平期间,这是它们与普通数据变化的根本区别:
START STOP
SCL ‾‾‾‾‾‾‾‾ SCL ‾‾‾‾‾‾‾‾
SDA ‾‾‾‾\____ SDA ____/‾‾‾‾
1→0 0→1
配套 IIC_Start() 的顺序是先令两线为高,再把 SDA 拉低,最后把 SCL 拉低,符合起始条件。
配套 IIC_Stop() 写成先 SCL=1、再 SDA=0、最后 SDA=1。若调用前 SDA 恰为高,第二步会在 SCL 高时先产生一次 START,第三步才产生 STOP。更稳妥的顺序是:
IIC_SCL = 0;
IIC_SDA = 0;
Delay5us();
IIC_SCL = 1;
Delay5us();
IIC_SDA = 1;
Delay5us();
这样 SDA 的低电平先在 SCL 低时准备好,再产生唯一、清晰的 STOP。
1.6 例题3:IIC时序的数据传输与应答信号
每个字节包含 8 个数据时钟,随后必须留出第 9 个应答时钟:
SCL: _/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_
序号: 1 2 3 4 5 6 7 8 9
SDA: b7 b6 b5 b4 b3 b2 b1 b0 ACK
驱动: └────────发送方─────────┘ └接收方┘
发送方在第 8 位结束后必须释放 SDA,否则接收方无法把线路拉低。配套 IIC_Recv_Ack() 先执行 IIC_SDA=1 释放线路,再令 SCL 为高并采样。返回值为 0 表示 ACK,1 表示 NACK;函数名中的 Recv 是站在主机角度“接收应答”。
二、串口
I²C 的 START/STOP 与 UART 的起始位/停止位名称相似,但电气含义不能混用:
| 对比项 | I²C | 常见 8N1 UART |
|---|---|---|
| 时钟 | 独立 SCL | 无独立时钟 |
| 空闲 | SCL、SDA 都高 | TX 高 |
| 开始 | SCL 高时 SDA 下降 | TX 保持一个起始位低电平 |
| 数据顺序 | 通常 MSB 先发 | 通常 LSB 先发 |
| 每字节应答 | ACK/NACK | 标准 UART 帧没有 ACK |
| 停止 | SCL 高时 SDA 上升 | TX 保持停止位高电平 |
| 设备选择 | 地址 | 通常靠独立链路或上层协议 |
UART 可选奇偶校验,但常见 8N1 中 N 就表示没有校验位。不能把“串口帧一定包含校验”作为通用规则。
三、IIC驱动代码
本节建立独立的 IIC 目录,把总线代码从应用代码中分离。完整工程源代码已经列在标题下方,其中 iic.c/.h 是本章核心,main.h 定义 P1.5 和 P1.4,引入 delay 模块提供时序延时。
3.1 新建工程
推荐的目录关系为:
9.2IIC驱动代码
├─ main.c
├─ main.h
├─ Delay
│ ├─ delay.c
│ └─ delay.h
├─ IIC
│ ├─ iic.c
│ └─ iic.h
└─ Uart
├─ uart.c
└─ uart.h
复制旧工程可以保留芯片型号和编译选项,但也会带入无关源文件、目标文件和旧配置。创建后应检查工程组、Include Paths、参与编译的源文件和输出目录,而不是只改文件夹名称。
3.1.1 新建文件
iic.h 使用包含保护,iic.c 包含所需声明:
#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
iic.c 除了包含 iic.h,还需要知道引脚和延时函数的声明,所以配套工程包含 main.h 与 delay.h。更独立的写法是把 IIC_SCL、IIC_SDA 的配置也移入 iic.h 或单独的板级配置文件,降低总线驱动对 main.h 的耦合。
建立函数框架时,参数和返回值应由行为决定:
- Start、Stop 只产生总线条件,不需要数据参数。
- Send_Byte 接收一个 8 位值。
- Recv_Ack 采样并返回应答状态。
- 工程化接口还可以返回枚举或状态码,把超时、NACK、总线忙区分开。
3.1.2 函数声明
声明放在头文件,定义放在源文件,调用处包含头文件。声明末尾必须有分号,函数定义末尾不能额外加分号。
当前 IIC_Recv_Ack() 返回原始 SDA 电平:
返回 0 → SDA 被接收方拉低 → ACK
返回 1 → SDA 保持高电平 → NACK
调用者必须处理这个返回值,否则即使函数能够读取 ACK,整个程序仍等同于忽略错误。更清晰的接口可以使用带语义的宏或枚举,避免把 0 误认为失败、1 误认为成功。