IIC驱动代码_1

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

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
主机发送主机驱动 SDA8 位,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 误认为成功。