news 2026/5/20 10:44:04

STM32 I2C通信协议详解——标准库函数实现(通讯协议总结一)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 I2C通信协议详解——标准库函数实现(通讯协议总结一)

一、I2C协议基础原理

1.1 什么是I2C

I2C(Inter-Integrated Circuit)是由飞利浦(现NXP)开发的同步、半双工串行总线。仅需两根信号线:

  • SCL(Serial Clock):时钟线,主机产生。
  • SDA(Serial Data):数据线,双向。

核心特性:多主从支持、7/10位地址、半双工、总线速率最高3.4Mbps。

1.2 物理层:开漏输出与上拉电阻

SCL与SDA必须配置为开漏输出,外部接4.7kΩ(3.3V系统)或2.2kΩ(400kHz高速)上拉电阻。开漏+上拉实现了:

  • 电平兼容(不同电压域设备可共总线);
  • “线与”逻辑(任一设备拉低即低电平,支持多主仲裁与从机应答);
  • 热插拔安全。

为什么不能用推挽?若主机推挽输出高电平,从机无法拉低SDA来应答或发送数据——总线冲突。

1.3 时序核心

事件定义
起始STARTSCL高电平时,SDA下降沿
停止STOPSCL高电平时,SDA上升沿
数据有效SCL高期间SDA稳定,低期间允许变化
应答ACK接收方在第9个SCL将SDA拉低(ACK=0)
非应答NACK第9个SCL保持高电平(NACK=1),结束传输

1.4 帧格式

START → 从机地址(7bit) + R/W(1bit) → ACK → 数据1 → ACK → … → 数据N → ACK/NACK → STOP

读写位:0=主机写,1=主机读。例如AT24C02地址0x50,写为0xA0,读为0xA1。

1.5 速率选择

标准模式100kHz,快速模式400kHz,快速+模式1MHz,高速模式3.4MHz。总线上所有设备必须支持选定速率


二、硬件接线

2.1 连接示意图

  • 所有设备的SCL/SDA并联;
  • 必须外接上拉电阻,即使内部有弱上拉;
  • 推荐阻值:100kHz → 4.7kΩ;400kHz → 2.2kΩ。

2.2 STM32引脚配置(F103系列)

I2C外设SCLSDA复用功能
I2C1PB6PB7GPIO_Mode_AF_OD
I2C2PB10PB11GPIO_Mode_AF_OD

注意:必须配置为复用开漏输出。


三、软件实现(标准库,可直接使用)

开发环境:MDK-ARM,STM32F10x_StdPeriph_Lib_V3.5.0
示例硬件:STM32F103C8T6 + AT24C02 EEPROM(0xA0写,0xA1读)

3.1 GPIO与I2C初始化

#include"stm32f10x.h"#defineI2C_SPEED400000// 400kHz快速模式#defineI2C1_SLAVE_ADDR0xA0// AT24C02写地址(7位0x50左移1位)#defineI2C_TIMEOUT_MAX0x0000FFFFstaticvoidI2C1_GPIO_Init(void){GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6|GPIO_Pin_7;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_OD;// 复用开漏GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructure);}voidI2C1_Init(void){I2C_InitTypeDef I2C_InitStructure;I2C1_GPIO_Init();RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);I2C_InitStructure.I2C_Mode=I2C_Mode_I2C;I2C_InitStructure.I2C_DutyCycle=I2C_DutyCycle_2;I2C_InitStructure.I2C_OwnAddress1=0x00;I2C_InitStructure.I2C_Ack=I2C_Ack_Enable;I2C_InitStructure.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;I2C_InitStructure.I2C_ClockSpeed=I2C_SPEED;I2C_Init(I2C1,&I2C_InitStructure);I2C_Cmd(I2C1,ENABLE);}

3.2 底层时序操作(关键修正)

/** * @brief 发送起始条件(同时支持初始起始和重复起始) * 已移除BUSY等待,避免重复起始时死锁 * @retval 0:成功 1:超时 */staticuint8_tI2C1_Start(void){uint32_ttimeout=I2C_TIMEOUT_MAX;I2C_ClearFlag(I2C1,I2C_FLAG_AF|I2C_FLAG_ARLO|I2C_FLAG_BERR);I2C_GenerateSTART(I2C1,ENABLE);while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)){if(--timeout==0)return1;}return0;}staticvoidI2C1_Stop(void){I2C_GenerateSTOP(I2C1,ENABLE);}/** @brief 发送单字节数据(仅用于数据,不用于地址) */staticuint8_tI2C1_SendByte(uint8_tdata){uint32_ttimeout=I2C_TIMEOUT_MAX;I2C_SendData(I2C1,data);while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)){if(--timeout==0)return1;}return0;}/** @brief 等待地址发送完成并清除ADDR标志 */staticuint8_tI2C1_WaitAck(void){uint32_ttimeout=I2C_TIMEOUT_MAX;while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)){if(--timeout==0)return1;}return0;// I2C_CheckEvent会读SR1+SR2,自动清除ADDR}/** @brief 接收一个字节,ack=1发送ACK,ack=0发送NACK */staticuint8_tI2C1_ReceiveByte(uint8_tack){uint32_ttimeout=I2C_TIMEOUT_MAX;if(ack)I2C_AcknowledgeConfig(I2C1,ENABLE);elseI2C_AcknowledgeConfig(I2C1,DISABLE);while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)){if(--timeout==0)return0;}returnI2C_ReceiveData(I2C1);}

3.3 AT24C02 读写接口

/** @brief 单字节写入:向EEPROM内部地址addr写入data */uint8_tAT24C02_WriteByte(uint8_taddr,uint8_tdata){if(I2C1_Start())return1;I2C_SendData(I2C1,I2C1_SLAVE_ADDR|0x00);// 写地址if(I2C1_WaitAck())gotoerr;I2C_SendData(I2C1,addr);// EEPROM内部地址if(I2C1_WaitAck())gotoerr;if(I2C1_SendByte(data))gotoerr;// 数据字节if(I2C1_WaitAck())gotoerr;I2C1_Stop();for(volatileuint32_ti=0;i<50000;i++);// 等待内部写入周期return0;err:I2C1_Stop();return1;}/** @brief 单字节读取:从EEPROM内部地址addr读出数据 */uint8_tAT24C02_ReadByte(uint8_taddr,uint8_t*data){// 伪写:设置内部地址if(I2C1_Start())return1;I2C_SendData(I2C1,I2C1_SLAVE_ADDR|0x00);if(I2C1_WaitAck())gotoerr;I2C_SendData(I2C1,addr);if(I2C1_WaitAck())gotoerr;// 重复起始+读if(I2C1_Start())gotoerr;I2C_SendData(I2C1,I2C1_SLAVE_ADDR|0x01);if(I2C1_WaitAck())gotoerr;*data=I2C1_ReceiveByte(0);// NACKI2C1_Stop();return0;err:I2C1_Stop();return1;}/** @brief 页写入(注意AT24C02每页8字节,跨页需自行拆分) */uint8_tAT24C02_WriteBuffer(uint8_taddr,uint8_t*buf,uint8_tlen){if(I2C1_Start())return1;I2C_SendData(I2C1,I2C1_SLAVE_ADDR|0x00);if(I2C1_WaitAck())gotoerr;I2C_SendData(I2C1,addr);if(I2C1_WaitAck())gotoerr;for(uint8_ti=0;i<len;i++){if(I2C1_SendByte(buf[i]))gotoerr;if(I2C1_WaitAck())gotoerr;}I2C1_Stop();for(volatileuint32_ti=0;i<50000;i++);return0;err:I2C1_Stop();return1;}/** @brief 连续读取 */uint8_tAT24C02_ReadBuffer(uint8_taddr,uint8_t*buf,uint8_tlen){if(I2C1_Start())return1;I2C_SendData(I2C1,I2C1_SLAVE_ADDR|0x00);if(I2C1_WaitAck())gotoerr;I2C_SendData(I2C1,addr);if(I2C1_WaitAck())gotoerr;if(I2C1_Start())gotoerr;I2C_SendData(I2C1,I2C1_SLAVE_ADDR|0x01);if(I2C1_WaitAck())gotoerr;for(uint8_ti=0;i<len;i++){if(i==(len-1))buf[i]=I2C1_ReceiveByte(0);// 最后一个字节发NACKelsebuf[i]=I2C1_ReceiveByte(1);// 中间发ACK}I2C1_Stop();return0;err:I2C1_Stop();return1;}/** @brief 检测设备是否在线 */uint8_tI2C1_DeviceDetect(uint8_tslaveAddr){uint8_tret;if(I2C1_Start())return1;I2C_SendData(I2C1,slaveAddr|0x00);uint32_ttimeout=I2C_TIMEOUT_MAX;while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)){if(--timeout==0){ret=1;gotoend;}}ret=0;// 收到ACK,设备在线end:I2C1_Stop();returnret;}

3.4 总线死锁恢复

/** * @brief 强制恢复I2C总线:SCL发送9个脉冲释放SDA * 使用前会禁用I2C,操作后需重新调用I2C1_Init() */voidI2C1_BusReset(void){GPIO_InitTypeDef GPIO_InitStructure;uint8_ti;I2C_Cmd(I2C1,DISABLE);// 临时将SCL(PB6)配置为推挽输出GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructure);for(i=0;i<9;i++){GPIO_ResetBits(GPIOB,GPIO_Pin_6);for(volatileuint16_td=0;d<10;d++);GPIO_SetBits(GPIOB,GPIO_Pin_6);for(volatileuint16_td=0;d<10;d++);}// 恢复复用开漏,重新初始化I2CI2C1_GPIO_Init();I2C_Cmd(I2C1,ENABLE);}

3.5 main函数示例

intmain(void){uint8_twdata=0x55,rdata=0x00;uint8_tbuf_w[8]={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};uint8_tbuf_r[8];I2C1_Init();if(I2C1_DeviceDetect(I2C1_SLAVE_ADDR)==0){// 设备在线,继续操作}else{while(1);// 错误处理}AT24C02_WriteByte(0x00,wdata);for(volatileuint32_ti=0;i<100000;i++);AT24C02_ReadByte(0x00,&rdata);// rdata应为0x55AT24C02_WriteBuffer(0x10,buf_w,8);for(volatileuint32_ti=0;i<100000;i++);AT24C02_ReadBuffer(0x10,buf_r,8);// buf_r应等于buf_wwhile(1);}

四、调试建议

  1. 检查上拉电阻:用万用表测SCL/SDA对VCC是否接入4.7kΩ/2.2kΩ。
  2. 逻辑分析仪:抓取波形,核对START、地址、ACK、数据、STOP。
  3. 常见故障
    • 无波形:时钟未开或引脚未复用。
    • 无ACK:地址错误(0x50与0xA0混淆)或上拉缺失。
    • 卡死在WaitAck:从机未应答,检查硬件连接。
    • 数据全0xFF:读时序错误,确认重复起始后地址正确发送。

以上代码已通过逻辑分析仪验证时序,可直接用于项目。如有特定芯片需求,仅需修改I2C1_SLAVE_ADDR宏定义即可。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/20 10:43:03

终极Alienware控制指南:如何用开源工具轻松管理灯光和风扇

终极Alienware控制指南&#xff1a;如何用开源工具轻松管理灯光和风扇 【免费下载链接】alienfx-tools Alienware systems lights, fans, and power control tools and apps 项目地址: https://gitcode.com/gh_mirrors/al/alienfx-tools 厌倦了臃肿的AWCC软件占用系统资…

作者头像 李华
网站建设 2026/5/20 10:41:43

066.模型鲁棒性提升:数据增强、对抗训练等防御策略

一、从产线误检说起 上周产线反馈了一个诡异问题:同一个检测模型,白天准确率99.2%,夜间灯光稍暗时直接掉到74%。查了半天发现,训练集全是日光灯下的标准光照图片,模型压根没见过昏暗环境。这让我想起三年前做安防项目时,摄像头稍微有点雾气,YOLO就把行人框到天上去了—…

作者头像 李华
网站建设 2026/5/20 10:41:38

张可盈优雅灵动亮相《无限超越班》毕业盛典 颜值演技俱佳狂揽口碑

昨日&#xff0c;综艺《无限超越班4》举办毕业典礼红毯直播&#xff0c;以“不设限致成长”为主题&#xff0c;聚焦演员们一路以来的成长和突破。其中&#xff0c;青年演员张可盈选择“逃婚新娘”的红毯设定&#xff0c;一身白纱灵动亮相十分惹眼。在采访互动环节更是展现出真诚…

作者头像 李华
网站建设 2026/5/20 10:40:40

Django 从 0 到 1 打造完整电商平台:Admin 后台管理与数据初始化

IT策士 10余年一线大厂经验&#xff0c;专注 IT 思维、架构、职场进阶。我也会在其它平台持续发布最新文章&#xff0c;助你少走弯路。大家好&#xff0c;我是IT策士。前面三节课我们把模型建好、索引加好、迁移理顺了&#xff0c;数据库里现在还是“空荡荡”的。今天我们就来做…

作者头像 李华
网站建设 2026/5/20 10:40:39

别再只改Device了!STM32从F103ZET6换到C4,Keil里这3个地方不改必报错

STM32芯片更换避坑指南&#xff1a;Keil中3个关键配置详解 从STM32F103ZET6切换到F103C4这类"大改小"的操作&#xff0c;远不止在Device里换个型号那么简单。很多开发者第一次操作时都会遇到各种莫名其妙的编译错误&#xff0c;最常见的就是ADC中断报错和宏定义冲突。…

作者头像 李华
网站建设 2026/5/20 10:37:40

面试官:Zookeeper 的典型应用场景有哪些?你能说上来几个?

在线 Java 面试刷题&#xff08;已更新239题&#xff0c;图文并茂&#xff09;&#xff1a;https://www.quanxiaoha.com/java-interview面试考察点核心能力理解&#xff1a;面试官不仅仅是想知道你背了几个场景名&#xff0c;更是想知道你是否理解 ZooKeeper 的底层能力&#x…

作者头像 李华