I2C通信测试
更新于 2026年8月13日
本篇总结
使用硬件 I2C1 驱动板载 AHT20 温湿度传感器和 AT24C16C EEPROM,读取温湿度并验证读写。
整改说明
原例程为自发自收测试,未使用板载 AHT20。本次整改改为驱动实际板载器件 AHT20 + AT24C16C,代码已重写。
本示例演示 W55MH32L 的硬件 I2C1 外设,驱动板载两颗 I2C 器件:AHT20(温湿度传感器,7位地址 0x38)和 AT24C16C(EEPROM,7位地址 0x50)。两者共用 I2C1 总线(PB6=SCL / PB7=SDA)。程序先读取 AHT20 温湿度并打印,再对 EEPROM 写入一字节并读回验证。
前置工具准备
开始本例程前,请确认以下工具已安装:
- WIZ UartTool V1.0:串口收发测试工具:点击下载
硬件连接
本例程使用 I2C1 的 SCL(PB6) 和 SDA(PB7) 引脚连接板载两颗 I2C 器件:AHT20 温湿度传感器(7位地址 0x38)和 AT24C16C EEPROM(7位地址 0x50),两者共用同一条 I2C 总线。PB6/PB7 配置为复用开漏输出并使能内部上拉。USART1 用于输出调试信息。
| 引脚 | 功能 | 方向 | 模式 |
|---|---|---|---|
| PB6 | I2C1_SCL(时钟线) | 开发板 ↔ AHT20/EEPROM | 复用开漏输出 (AF_OD) |
| PB7 | I2C1_SDA(数据线) | 开发板 ↔ AHT20/EEPROM | 复用开漏输出 (AF_OD) |
| PA9 | USART1_TX | 开发板 → PC | 复用推挽输出 (AF_PP) |
| PA10 | USART1_RX | PC → 开发板 | 浮空输入 (IN_FLOATING) |
板载器件地址表:
| 器件 | 7位地址 | 库调用基础地址 | 说明 |
|---|---|---|---|
| AHT20 温湿度传感器 | 0x38 | 0x70 (0x38<<1) | 触发测量 0xAC/0x33/0x00,读6字节 |
| AT24C16C EEPROM | 0x50 | 0xA0 (0x50<<1) | 2KB容量,11位地址,高3位入设备字节 |
地址参数注意
标准库 I2C_Send7bitAddress() 的 Address 参数为「7位地址<<1」,R/W 位由 Direction 决定。读写均传基础地址(0x70/0xA0),靠 Direction(Receiver/Transmitter) 区分方向,不要传读写地址(0x71/0xA1)。
工程结构
工程位于 SDK 内 1.SDK\ModuleDemo\I2C\,核心文件如下:
I2C/
├── USER/
│ ├── main.c ← I2C配置 + AHT20驱动 + AT24C16C读写
│ ├── w55mh32_conf.h ← 外设头文件集中包含
│ ├── w55mh32_it.c/h ← 中断服务函数
│ ├── system_w55mh32.c/h← 系统初始化(时钟配置)
│ └── startup_w55mh32.s ← 启动文件
└── SYSTEM/
└── delay/
└── delay.c/h ← SysTick 延时函数
步骤1:I2C 初始化与 AHT20 温湿度读取
文件:USER/main.c — 配置 PB6(SCL)/PB7(SDA) 为复用开漏输出并使能内部上拉,I2C1 为主机模式、100kHz 时钟、7 位地址、使能 ACK。编写 AHT20 初始化函数(等待 40ms、读状态字检查校准位 bit3、发 0xBE 0x08 0x00)和温湿度读取函数(触发 0xAC 0x33 0x00、等 80ms、读 6 字节换算)。
/* AHT20 7位地址 0x38 << 1 */
#define AHT20_ADDR 0x70
/* AT24C16C 7位地址 0x50 << 1 */
#define AT24C_ADDR_BASE 0xA0
/* I2C1 配置:PB6=SCL / PB7=SDA,100kHz 标准模式 */
void IIC_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
I2C_InitTypeDef I2C_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
/* PB6/PB7: 复用开漏输出 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* 使能内部上拉 */
GPIO_ForcePuPdCmd(GPIOB, ENABLE);
GPIO_ForcePullUpConfig(GPIOB, GPIO_Pin_6);
GPIO_ForcePullUpConfig(GPIOB, GPIO_Pin_7);
I2C_DeInit(I2C1);
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_OwnAddress1 = 0xA0;
I2C_InitStructure.I2C_ClockSpeed = 100000; /* 100kHz */
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_Init(I2C1, &I2C_InitStructure);
I2C_Cmd(I2C1, ENABLE);
I2C_AcknowledgeConfig(I2C1, I2C_Ack_Enable);
}
/* AHT20 初始化:等待40ms,读状态字检查校准位bit3,发0xBE 0x08 0x00 */
uint8_t AHT20_Init(void)
{
uint8_t status;
delay_ms(40); /* AHT20 上电后需等待 40ms 才能通信 */
/* 读取状态字,检查校准使能位(bit3) */
I2C_GenerateSTART(I2C1, ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, AHT20_ADDR, I2C_Direction_Receiver);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
/* 单字节读:进入接收模式后立即 NACK + STOP,再读 DR */
I2C_AcknowledgeConfig(I2C1, I2C_Ack_Disable);
I2C_GenerateSTOP(I2C1, ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
status = I2C_ReceiveData(I2C1);
/* 若未校准,发送初始化命令 0xBE 0x08 0x00 */
if ((status & 0x08) == 0)
{
I2C_GenerateSTART(I2C1, ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, AHT20_ADDR, I2C_Direction_Transmitter);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, 0xBE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_SendData(I2C1, 0x08);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_SendData(I2C1, 0x00);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTOP(I2C1, ENABLE);
delay_ms(10);
}
return 0;
}
/* AHT20 温湿度读取:触发0xAC 0x33 0x00,等80ms,读6字节换算 */
uint8_t AHT20_Read(float *temperature, float *humidity)
{
uint8_t data[6] = {0};
uint8_t i;
uint32_t raw_h, raw_t;
/* 触发测量:0xAC 0x33 0x00 */
I2C_GenerateSTART(I2C1, ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, AHT20_ADDR, I2C_Direction_Transmitter);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, 0xAC);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_SendData(I2C1, 0x33);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_SendData(I2C1, 0x00);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTOP(I2C1, ENABLE);
delay_ms(80); /* 测量等待 */
/* 读 6 字节:状态 + 湿度20bit + 温度20bit */
I2C_AcknowledgeConfig(I2C1, I2C_Ack_Enable);
I2C_GenerateSTART(I2C1, ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, AHT20_ADDR, I2C_Direction_Receiver);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
for (i = 0; i < 6; i++)
{
if (i == 5) /* 最后一字节:NACK + STOP */
{
I2C_AcknowledgeConfig(I2C1, I2C_Ack_Disable);
I2C_GenerateSTOP(I2C1, ENABLE);
}
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
data[i] = I2C_ReceiveData(I2C1);
}
if (data[0] & 0x80) /* bit7=1 表示忙 */
return 1;
raw_h = ((uint32_t)data[1] << 12) | ((uint32_t)data[2] << 4) | (data[3] >> 4);
raw_t = (((uint32_t)data[3] & 0x0F) << 16) | ((uint32_t)data[4] << 8) | data[5];
*humidity = (float)raw_h * 100.0f / 1048576.0f;
*temperature = (float)raw_t * 200.0f / 1048576.0f - 50.0f;
return 0;
}
IIC_Configuration:使能 GPIOB 和 I2C1 时钟,配置 PB6/PB7 为复用开漏输出(GPIO_Mode_AF_OD)并启用内部上拉,初始化 I2C1 为 100kHz 标准模式、7位地址、使能 ACK。
AHT20_Init:上电等待 40ms 后读取状态字,检查校准使能位(bit3)。若未校准则发送初始化命令 0xBE 0x08 0x00。单字节读时序:EV6 后立即 NACK+STOP 再读 DR。
AHT20_Read:发触发测量命令 0xAC 0x33 0x00,等待 80ms,读 6 字节(状态+湿度20bit+温度20bit)。换算公式:湿度 = raw_h × 100 / 2²⁰;温度 = raw_t × 200 / 2²⁰ − 50。多字节读最后一字节前必须 NACK+STOP。
如果你想自定义
- 改通信速率 → 修改
I2C_ClockSpeed,如400000为快速模式 400kHz - 换 I2C 器件 → 修改设备地址宏定义,按目标器件 datasheet 调整读写时序
- 连续读写 → 在发送地址后连续发送/接收多个字节,最后发送 STOP/NACK
- 换引脚 → 使用 I2C2 的 PB10(SCL)/PB11(SDA)
步骤2:AT24C16C 读写与主测试
函数:AT24C_WriteByte() / AT24C_ReadByte() / main() — AT24C16C 容量 2KB,11 位地址,高 3 位放入设备地址字节 bit3-1,低 8 位作为 word address。主函数先读 AHT20 温湿度,再对 EEPROM 写入 0x5A 并读回验证。
/* AT24C16C 写字节:高3位地址放入设备地址 bit3-1 */
void AT24C_WriteByte(uint16_t addr, uint8_t data)
{
uint8_t dev = AT24C_ADDR_BASE | (uint8_t)((addr & 0x0700) >> 7);
I2C_GenerateSTART(I2C1, ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, dev, I2C_Direction_Transmitter);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, (uint8_t)(addr & 0xFF)); /* word address 低8位 */
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_SendData(I2C1, data);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTOP(I2C1, ENABLE);
}
/* AT24C16C 读字节:dummy write 设地址后 restart 读 */
uint8_t AT24C_ReadByte(uint16_t addr)
{
uint8_t dev = AT24C_ADDR_BASE | (uint8_t)((addr & 0x0700) >> 7);
uint8_t val;
/* dummy write: 设置待读地址 */
I2C_GenerateSTART(I2C1, ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, dev, I2C_Direction_Transmitter);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, (uint8_t)(addr & 0xFF));
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
/* restart read */
I2C_GenerateSTART(I2C1, ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, dev, I2C_Direction_Receiver);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
/* 单字节读:NACK + STOP 后读 DR */
I2C_AcknowledgeConfig(I2C1, I2C_Ack_Disable);
I2C_GenerateSTOP(I2C1, ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
val = I2C_ReceiveData(I2C1);
return val;
}
int main(void)
{
RCC_ClocksTypeDef clocks;
float temperature = 0.0f, humidity = 0.0f;
uint8_t write_val = 0x5A, read_val = 0;
delay_init();
UART_Configuration(115200);
RCC_GetClocksFreq(&clocks);
printf("\n");
printf("SYSCLK: %3.1fMhz, HCLK: %3.1fMhz, PCLK1: %3.1fMhz, PCLK2: %3.1fMhz, ADCCLK: %3.1fMhz\n",
(float)clocks.SYSCLK_Frequency / 1000000, (float)clocks.HCLK_Frequency / 1000000,
(float)clocks.PCLK1_Frequency / 1000000, (float)clocks.PCLK2_Frequency / 1000000,
(float)clocks.ADCCLK_Frequency / 1000000);
printf("I2C Test: AHT20 (0x38) + AT24C16C (0x50), I2C1 PB6/PB7.\n");
IIC_Configuration();
/* 1. AHT20 温湿度读取 */
if (AHT20_Init() == 0)
{
printf("AHT20 init OK.\n");
if (AHT20_Read(&temperature, &humidity) == 0)
{
printf("AHT20: Temperature = %.2f C, Humidity = %.2f %%RH\n", temperature, humidity);
}
else
{
printf("AHT20 read failed.\n");
}
}
else
{
printf("AHT20 init failed.\n");
}
/* 2. AT24C16C EEPROM 写读验证 */
printf("\nAT24C16C write 0x%02X to addr 0x0000 ...\n", write_val);
AT24C_WriteByte(0x0000, write_val);
delay_ms(5); /* 页写周期 tWR */
read_val = AT24C_ReadByte(0x0000);
printf("AT24C16C read back from addr 0x0000: 0x%02X\n", read_val);
if (read_val == write_val)
{
printf("AT24C16C write/read verify SUCCESS.\n");
}
else
{
printf("AT24C16C write/read verify FAILED.\n");
}
while (1);
}
AT24C16C 地址处理:16C 容量 2KB,地址 11 位,高 3 位放入设备地址字节 bit3-1(AT24C_ADDR_BASE | (addr & 0x0700) >> 7),低 8 位作为 word address 发送。
读时序:先 dummy write 写入待读地址,再 Restart 切换为读方向。单字节读:EV6 后 NACK+STOP 再读 DR。写后需 delay_ms(5) 等页写周期 tWR。
主函数流程:初始化延时和 UART → 打印时钟频率 → 配置 I2C1 → 读取 AHT20 温湿度 → 对 AT24C16C 写入 0x5A 并读回验证。
编译与下载
Keil MDK 中打开 I2C.uvprojx,F7 编译,确认 0 Error(s)。连接 DEBUG USB 口到 PC,F8 下载。
运行验证
打开 WIZ UartTool V1.0,波特率 115200 / 8N1。下载固件并按 RESET 后,串口首先打印系统时钟信息,然后依次输出 AHT20 温湿度读取结果和 AT24C16C EEPROM 写读验证结果。预期串口输出:
SYSCLK: 72.0Mhz, HCLK: 72.0Mhz, PCLK1: 36.0Mhz, PCLK2: 72.0Mhz, ADCCLK: 72.0Mhz
I2C Test: AHT20 (0x38) + AT24C16C (0x50), I2C1 PB6/PB7.
AHT20 init OK.
AHT20: Temperature = 25.34 C, Humidity = 45.67 %RH
AT24C16C write 0x5A to addr 0x0000 ...
AT24C16C read back from addr 0x0000: 0x5A
AT24C16C write/read verify SUCCESS.
可看到 AHT20 温湿度读取成功,AT24C16C EEPROM 写读验证通过(写入 0x5A,读回 0x5A)。
常见问题
Q: I2C 通信卡在 I2C_CheckEvent 等待循环
检查 SCL/SDA 引脚(PB6/PB7)是否正确连接,内部上拉是否已使能(GPIO_ForcePullUpConfig)。确认设备地址正确(AHT20 为 0x70,AT24C16C 为 0xA0)。用示波器或逻辑分析仪观察 SCL/SDA 波形。
Q: AHT20 读取失败或数据异常
确认 AHT20 上电后等待了 40ms 再通信。检查初始化命令 0xBE 0x08 0x00 是否正确发送。读取时确认等待了 80ms 测量完成。检查状态字 bit7 是否为 1(忙),若忙需延长等待时间。
Q: AT24C16C 读回数据与写入不一致
EEPROM 写入后需延时 5ms 等待页写周期 tWR 再读取。确认 11 位地址处理正确:高 3 位放入设备地址字节 bit3-1,低 8 位作为 word address。确认读时序中的 Repeated START 和 NACK 时序正确。
Q: I2C 速率无法达到 400kHz
确认 I2C_ClockSpeed 设为 400000,I2C_DutyCycle 设为 I2C_DutyCycle_16_9(快速模式占空比)。检查 APB1 时钟频率是否足够(I2C 时钟来自 APB1)。
总结
本文基于 W55MH32L 的硬件 I2C1 控制器实现了板载 AHT20 温湿度传感器和 AT24C16C EEPROM 的驱动:
- I2C1 主机模式:100kHz 标准速率,7 位地址,使能 ACK,PB6(SCL)/PB7(SDA) 复用开漏输出并使能内部上拉
- AHT20 驱动:等待 40ms 上电 → 读状态字检查校准位 → 发 0xBE 0x08 0x00 初始化 → 触发 0xAC 0x33 0x00 测量 → 等 80ms 读 6 字节换算温湿度
- AT24C16C 读写:2KB 容量,11 位地址(高 3 位入设备地址字节),写入 0x5A 并读回验证通过
- 地址参数注意:
I2C_Send7bitAddress()传基础地址(0x70/0xA0),R/W 位由 Direction 决定