记录一次使用模拟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]);
}
}