记录一次使用模拟IIC配置8051内核单片机为从机

·0·0 分钟阅读
嵌入式开发
#include "CM9T83XX.h"
#include "common.h"
#include "system.h"
#include "gpio.h"
#include "stdio.h"
#include "intrins.h"
#include "communicate.h"
#include "oled.h"

#define IIC_SLAVE_ADDR 0xD0 // 从机地址 (8位写地址, LSB=0; 读时主机发 0xD1)

/* ============== 寄存器映射 (结构体 + union) ==============
 * 用 union 同时支持两种访问方式:
 *   iic_regs.field.version -> 按字段名访问 (可读性好)
 *   iic_regs.bytes[0x01]   -> 按寄存器地址索引 (匹配主机发的 reg_addr)
 * 字段顺序即寄存器地址, 不留空位;如需保留寄存器地址, 加 reserved_x 占位即可
 */
#define IIC_REG_COUNT 16 // 寄存器总数 (按需扩展)

typedef union iic_regs_u {
	struct {
		u8 id;		 // 0x00: 设备 ID
		u8 version;	 // 0x01: 固件版本
		u8 config;	 // 0x02: 配置寄存器
		u8 status;	 // 0x03: 状态寄存器
		u8 data1[12]; // 0x04..0x0F: 用户数据区
	} field;
	u8 bytes[IIC_REG_COUNT]; // 按寄存器地址索引
} iic_regs_t;

/* 寄存器存储:中断里写、主循环里读
 * 单字节访问在 8051 上是原子的, 多字节读请先关中断 */
iic_regs_t iic_regs = {0};

/* ============== 从机引脚 ============== */
#define IIC_SCL PA1 // 从机 SCL 输入引脚
#define IIC_SDA PA3 // 从机 SDA 双向引脚 (开漏)

/* ============== 状态机 ==============
 * ST_IDLE    : 空闲, 等待 START
 * ST_RECV    : 主机写方向, 接收数据字节 (bit_cnt 0..7 数据, 8=收完发ACK, 9=ACK脉冲)
 * ST_TRANSMIT: 主机读方向, 从机发送数据字节 (bit_cnt 0..7 数据, 8=释放SDA等主机ACK)
 *               不检测主机 ACK/NACK, 主机 NACK 后会发 STOP, 下次 START 自然复位
 */
#define ST_IDLE		0
#define ST_RECV		1
#define ST_TRANSMIT 2

static volatile u8 iic_state = ST_IDLE;
static volatile u8 iic_bit_cnt = 0;	  // 0..7 数据位, 8=ACK阶段, 9=ACK脉冲中 (仅 ST_RECV)
static volatile u8 iic_byte_val = 0;
static volatile u8 iic_byte_idx = 0;  // 0=地址字节, 1=寄存器地址, 2+=数据字节
static volatile u8 iic_addr_match = 0;
static volatile u8 iic_rw = 0;		  // 读写位: 0=主机写, 1=主机读 (地址字节 LSB)
static volatile u8 iic_reg_ptr = 0;	  // 当前读写的寄存器地址, 读/写完自动 +1 (支持连续)
static volatile u8 iic_tx_byte = 0;	  // ST_TRANSMIT 中当前正在发送的字节

u8 test_val; // 调试:观察最近一次地址字节

/**
 * @brief 模拟 IIC 从机中断服务: SCL、SDA 外部中断
 *        interrupt 0 = External Interrupt 0, 此芯片 PINTx 共用此向量
 */
void INT3() interrupt 0
{
	// ---- SDA 下降沿:仅当 SCL 为高时才视为 START ----
	if (PINT3IF)
	{
		PINT3IF = 0;
		if (IIC_SCL)
		{
			// START (含 repeated START): 复位状态机, 但保留 iic_reg_ptr
			// (支持 "写寄存器地址 -> repeated START -> 读" 的典型流程)
			iic_state = ST_RECV;
			iic_bit_cnt = 0;
			iic_byte_val = 0;
			iic_byte_idx = 0;
			iic_addr_match = 0;
			iic_rw = 0;
			IIC_SDA = 1; // 释放 SDA, 准备接收
		}
	}

	// ---- SCL 边沿:按状态分派 ----
	if (PINT1IF)
	{
		PINT1IF = 0;

		if (iic_state == ST_RECV)
		{
			if (IIC_SCL) // SCL 上升沿: 采样 SDA 位
			{
				if (iic_bit_cnt < 8)
				{
					iic_byte_val = (u8)((iic_byte_val << 1) | (u8)IIC_SDA);
					iic_bit_cnt++;
				}
				// bit_cnt == 8: ACK 脉冲的上升沿, 不做事
			}
			else // SCL 下降沿
			{
				if (iic_bit_cnt == 8)
				{
					// 8 位收完的下降沿: 拉低 SDA 发 ACK
					if (iic_byte_idx == 0)
					{
						// 地址字节: 屏蔽 LSB 比较 (匹配 0xD0 写 / 0xD1 读)
						if ((iic_byte_val & 0xFE) == IIC_SLAVE_ADDR)
						{
							iic_addr_match = 1;
							iic_rw = iic_byte_val & 0x01; // 0=写, 1=读
							IIC_SDA = 0;				   // ACK
						}
						else
						{
							iic_addr_match = 0; // 不应答
						}
					}
					else if (iic_addr_match && (iic_rw == 0)) // 仅写方向处理数据
					{
						if (iic_byte_idx == 1)
						{
							// 第 1 个数据字节 = 寄存器地址
							iic_reg_ptr = iic_byte_val;
						}
						else
						{
							// 后续字节 = 数据, 按地址递增写入对应寄存器
							if (iic_reg_ptr < IIC_REG_COUNT)
							{
								iic_regs.bytes[iic_reg_ptr] = iic_byte_val;
							}
							iic_reg_ptr++; // 连续写, 地址自增
						}
						IIC_SDA = 0; // ACK
					}
					iic_bit_cnt = 9;
				}
				else if (iic_bit_cnt == 9)
				{
					// ACK 脉冲的下降沿 (9th falling): 准备下一字节
					iic_byte_idx++;
					iic_byte_val = 0;

					// 读方向: 地址字节 ACK 完毕, 切到发送态
					// 关键: 必须在此下降沿立即送出 MSB, 否则主机在下一个上升沿
					// 会采到释放后的高电平 (错的 MSB=1), 后续位整体错位
					if (iic_addr_match && (iic_rw == 1))
					{
						iic_state = ST_TRANSMIT;
						iic_tx_byte = iic_regs.bytes[iic_reg_ptr];
						IIC_SDA = (u8)((iic_tx_byte >> 7) & 0x01); // 立即送 MSB
						iic_bit_cnt = 1; // MSB 已送, 下一位是 bit 6
					}
					else
					{
						// 写方向: 释放 SDA, bit_cnt 清零准备接收下一字节
						IIC_SDA = 1;
						iic_bit_cnt = 0;
					}
				}
			}
		}
		else if (iic_state == ST_TRANSMIT)
		{
			// 从机发送: 只在 SCL 下降沿动作 (主机在上升沿采样)
			// bit_cnt 0..7: 送出 8 位数据 (MSB 先)
			// bit_cnt 8   : 释放 SDA 给主机 ACK, 加载下一字节 (不检测 ACK)
			if (!IIC_SCL)
			{
				if (iic_bit_cnt < 8)
				{
					// 送出位: bit_cnt=0 送 MSB(bit7), bit_cnt=7 送 LSB(bit0)
					IIC_SDA = (u8)((iic_tx_byte >> (7 - iic_bit_cnt)) & 0x01);
					iic_bit_cnt++;
				}
				else if (iic_bit_cnt == 8)
				{
					// 8 位发完: 释放 SDA 给主机 ACK, 加载下一字节
					IIC_SDA = 1;
					iic_reg_ptr++;
					if (iic_reg_ptr >= IIC_REG_COUNT)
						iic_reg_ptr = 0;
					iic_tx_byte = iic_regs.bytes[iic_reg_ptr];
					// 不立即设 bit_cnt=0: 否则 ACK 脉冲下降沿会设下一字节 MSB,
					// 若 MSB=0 拉低 SDA, 干扰主机 STOP/repeated START, 导致状态不同步
					iic_bit_cnt = 9;
				}
				else // bit_cnt == 9: ACK 脉冲下降沿, 不设 SDA, 避免干扰主机
				{
					iic_bit_cnt = 0;
				}
			}
			// 上升沿: 主机在采样, 从机不动作
		}
	}
}

/**
 * @brief 主函数
 */
void main()
{
	CLOCK_INIT();		   // 系统时钟初始化
	WDTC0 = Bin(11100000); // 关闭看门狗

	GPIO_INIT();		   // GPIO 初始化 (含 PA3 开漏配置)
	GPIO_INTERRUPT_INIT(); // PINT1=SCL 双边沿, PINT3=SDA 下降沿
	IIC_SDA = 1;		   // 显式释放 SDA (防止复位后 PADATA bit=0 把 SDA 长期拉低)
	GIE = 1;			   // 开总中断

	Int_OLED_Init();
	Int_OLED_Clear();
	// Int_OLED_ShowStr(0, 0, "Hello World!");

	while (1)
	{
		// 主机写 0x01 寄存器后, 这里会显示其值
		Int_OLED_ShowNum(0, 0, iic_regs.bytes[0x01]);
	}
}