
1. 项目概述这不是芯片封装课而是一场“硅基交通系统”的实操推演你手里的开发板那块指甲盖大小的黑色芯片不是凭空长出来的。它从沙漠里挖出的石英砂开始经历提纯、拉晶、光刻、蚀刻、封装……最终变成能跑代码的MCU。但真正让这块芯片“活起来”的从来不是它内部的CPU多快而是它能不能和外界——另一块芯片、一个传感器、一块显示屏、甚至另一个MCU——说上话。SPI、I2C这两个缩写词就是嵌入式世界里最基础、最频繁、也最容易被轻视的“片间语言”。它们不是炫技的高速接口而是每天要跑几百次、几千次、甚至几万次的“通勤地铁”——不快但必须准不 flashy但绝不能掉线。这个标题《从沙子到车辙4.2从片内到片间-SPI、I2C》“沙子”是物理起点“车辙”是功能终点而“4.2”这个编号暗示它是一套系统性知识图谱中的关键一环。它刻意把“片内”芯片内部总线比如AMBA AHB/APB和“片间”芯片与芯片之间的物理连线并列就是在提醒你别再只盯着寄存器手册里那个漂亮的框图了。那个框图里CPU、DMA、Flash、RAM之间画着粗壮的总线箭头看起来很美可一旦你把芯片焊到PCB上用示波器探头戳一下SCL引脚看到的是一串抖动的、带毛刺的方波那一刻你才真正踏入了“片间通信”的真实战场。这里没有抽象的协议栈只有电压、时序、上拉电阻、走线长度、电源噪声以及你焊锡烙铁留下的每一个微小失误。我做过不下二十个基于STM32和ESP32的传感器融合项目从温湿度、气压、IMU到OLED显示、SD卡存储。每一次调试失败有七成概率不是算法错了而是SPI的CPOL/CPHA配反了或者I2C的上拉电阻选大了导致上升沿太慢被主控误判为“超时”。这种问题不会报错它只会让你的程序在某个特定温度下、某次特定的读取操作后静默地卡死。所以这篇内容不是教你怎么调通一个例程而是带你亲手“拆解”SPI和I2C这两条“车辙”看清它的轨距时序、枕木电平标准、信号灯ACK/NACK、甚至路边的维修站错误恢复机制。它适合所有已经能点亮LED、会写GPIO翻转但一碰到外设驱动就查资料、抄代码、然后陷入无尽调试循环的开发者。如果你还在问“为什么我的MPU6050读不出数据”那接下来的内容就是你该补上的那一课。2. 核心思路拆解为什么非得是SPI和I2C而不是别的2.1 片内与片间的本质鸿沟速度、距离与成本的三角博弈理解SPI和I2C首先要认清一个残酷的现实片内通信和片间通信是两种完全不同的物理世界。这不是工程师偷懒随便定的协议而是由物理定律和商业成本共同画下的楚河汉界。片内On-chip所有模块都在同一块硅片上通过金属互连层连接。距离是微米级μm信号传播延迟可以忽略不计皮秒ps量级供电极其稳定噪声极低。所以片内总线可以追求极致带宽比如ARM的AXI总线轻松跑几百MHz用复杂的握手机制如READY/VALID来榨干每一丝性能。它像城市核心区的地下高铁轨道精准、信号强、速度快但造价高昂只服务于核心枢纽。片间Inter-chip两块芯片可能相隔几厘米甚至十几厘米靠PCB上的铜线连接。距离是厘米级cm信号传播延迟变成了纳秒ns量级这已经和数字信号的边沿时间rise/fall time在一个数量级了。更麻烦的是PCB走线是天线会耦合电源噪声、其他信号串扰芯片的IO口驱动能力有限长线会带来严重的阻抗失配和反射不同芯片的工艺、电压域、IO电平标准3.3V vs 1.8V还可能不一致。它像城市间的城际铁路轨道PCB走线不可能像地下高铁那么完美站点芯片的接驳标准电平、时序必须足够宽容才能保证列车数据天天准点、不脱轨。这个鸿沟直接决定了片间协议的设计哲学必须牺牲一部分理论带宽换取极高的鲁棒性、极简的硬件开销和极广的兼容性。这就是SPI和I2C存在的根本逻辑。2.2 SPI一根“独裁者”的高速专线SPISerial Peripheral Interface的设计思想非常直白我要和你高速对话那就给你一条专属的、不受干扰的、全双工的通道。它不搞民主协商主控Master说了算。硬件结构SPI需要至少4根线SCLK时钟由Master单向驱动、MOSIMaster Out Slave In数据从Master发给Slave、MISOMaster In Slave Out数据从Slave发给Master、SS/CSSlave Select片选由Master控制低电平有效。每增加一个Slave就要多一根独立的SS线。这就像给每个下属都配了一条直达老板办公室的专用电话线。为什么是4线这是速度与复杂度的黄金平衡点。全双工意味着MOSI和MISO可以同时收发理论带宽是SCLK频率的两倍实际受制于器件性能。SCLK由Master统一提供彻底消除了时钟同步的难题——Slave不需要自己生成时钟只要乖乖跟着Master的节拍走就行。SS线则确保了“专机专用”避免多个Slave在同一时刻抢着说话总线冲突。核心优势速度最快、协议最简单、实时性最好。STM32的SPI外设最高能跑到80MHz这意味着理论数据率可达160Mbps80MHz * 2。对于Flash、SD卡、高速ADC/DAC这类对吞吐量敏感的设备SPI是唯一选择。它的“独裁”设计恰恰是高速场景下最可靠的保障。核心代价引脚资源消耗巨大。每加一个SPI设备就要多占一根IO口作为SS。一个8位MCUIO口本就不富裕接上3个SPI设备SS线就占了3个加上SCLK/MOSI/MISO共7根几乎一半的IO没了。这就像城市里修太多专用快车道反而挤占了普通道路资源。2.3 I2C一群“议会制”成员的共享街道I2CInter-Integrated Circuit的设计哲学与SPI截然相反资源宝贵大家共用一条路但必须有严格的交通规则。它是为了解决“引脚荒”而生的。硬件结构I2C只需要2根线SCLSerial Clock Line时钟线和SDASerial Data Line数据线。最关键的是这两根线都是开漏Open-Drain输出必须通过外部上拉电阻接到电源。这意味着任何设备Master或Slave都可以把线“拉低”但都不能主动“推高”“推高”只能靠上拉电阻完成。这就像一条双向单车道所有车辆设备都有权踩刹车拉低但谁也不能强行按喇叭推高让别人让路。为什么是开漏上拉这是I2C协议的灵魂所在。它天然支持多主控Multi-Master和总线仲裁Arbitration。当两个Master同时想发数据它们都会尝试在SDA上写“1”即释放总线靠上拉电阻拉高或“0”即主动拉低。如果一个Master写“1”另一个写“0”那么总线实际呈现“0”。写“1”的Master会立刻检测到SDA没被拉高预期是1读到是0就知道自己输了立刻停止发送把总线让给写“0”的Master。整个过程毫秒级完成无需软件干预。这就像十字路口的车辆都遵守“谁先到谁先过”的潜规则自动避让。核心优势引脚极度节省、支持多设备、支持多主控、有完善的地址寻址和错误处理机制ACK/NACK。一个I2C总线上理论上可以挂载128个设备7位地址实际受限于总线电容。对于温湿度、光照、气压等低速、多点采集的传感器网络I2C是绝对的王者。它的“议会制”设计让系统扩展变得异常灵活。核心代价速度相对较慢、协议更复杂、对硬件上拉电阻、走线电容更敏感。标准模式Standard-mode只有100kHz快速模式Fast-mode400kHz高速模式High-speed mode3.4MHz。而且总线上的所有设备其SDA/SCL引脚的输入电容会累加总电容越大上拉电阻拉高所需时间越长上升沿就越慢这直接限制了最高通信速率。这就是为什么I2C总线不能无限长、不能挂太多设备的根本原因。2.4 选型决策树你的项目到底该坐哪趟车面对一个新项目如何在SPI和I2C之间做选择我总结了一个三步决策树这是我踩过坑之后提炼出的血泪经验第一步看“乘客”是谁如果“乘客”是高速、单向或半双工的数据流比如从Flash读取固件、向OLED写入一帧图像、从高速ADC采样那么SPI是唯一答案。I2C的带宽瓶颈会让你抓狂。如果“乘客”是低速、小包、双向的控制指令或状态查询比如读取一个温湿度值几十字节、配置一个传感器寄存器、查询一个EEPROM的状态那么I2C是更优雅的选择。它省下的IO口可能就是你后续加一个Wi-Fi模块的关键。第二步看“车站”有多少如果你的系统里同类型设备只有一个比如就一块SD卡且对速度要求苛刻SPI的“专线”优势就能发挥到极致。如果你的系统里同类型设备有多个比如4个温湿度传感器分布在电路板四个角或者不同类型设备很多温湿度、气压、IMU、EEPROM那么I2C的“共享街道地址寻址”模式会让你的PCB布线清爽十倍软件架构也清晰得多。SPI在这种场景下SS线会像蜘蛛网一样铺满整个板子。第三步看“路况”是否复杂如果你的PCB是紧凑的4层板走线短电源干净那么两种协议都能跑得很稳。如果你的PCB是双面板走线长或者需要跨板连接比如主控板和传感器板用排线连接那么I2C的开漏设计对噪声的容忍度更高而SPI的高速信号在长线上极易产生反射和串扰调试起来会非常痛苦。此时宁可牺牲一点速度也要选I2C。记住没有“最好”的协议只有“最合适”的协议。一个优秀的嵌入式工程师不是背熟所有协议手册的人而是能在项目伊始就根据物理约束、性能需求和未来扩展性果断拍板选型并为这个选择承担起所有后续调试责任的人。3. 核心细节解析SPI与I2C的“交通规则”逐条拆解3.1 SPI的四大时序参数CPOL、CPHA、MSB/LSB与帧格式SPI的协议看似简单但它的“简单”背后藏着四个决定性的开关任何一个拨错通信就会彻底失效。它们不是可选项而是每个SPI设备数据手册里白纸黑字写明的硬性要求。CPOLClock Polarity时钟极性定义了SCLK在空闲状态IDLE时的电平。CPOL 0空闲时SCLK为低电平0。时钟脉冲从低到高上升沿开始。CPOL 1空闲时SCLK为高电平1。时钟脉冲从高到低下降沿开始。提示你可以把CPOL想象成交通信号灯的“默认状态”。红灯停CPOL1空闲高电平绿灯行CPOL0空闲低电平。设备上电后SCLK线就一直维持在这个“默认状态”直到Master开始发脉冲。CPHAClock Phase时钟相位定义了数据是在时钟的哪个边沿被采样读取在哪个边沿被改变写入。CPHA 0数据在第一个边沿即SCLK从IDLE态变化的那个边沿被采样在第二个边沿被改变。CPHA 1数据在第二个边沿被采样在第一个边沿被改变。注意这是最容易混淆的点。以CPOL0, CPHA0为例SCLK空闲为低。第一个边沿是上升沿低-高此时Slave采样MOSI上的数据第二个边沿是下降沿高-低此时Master将下一个bit放到MOSI上。整个过程数据是在SCLK的“上升沿”被锁进Slave的移位寄存器。CPOL和CPHA组合起来就形成了SPI的四种工作模式Mode 0 ~ Mode 3。绝大多数常见的SPI Flash如W25Q系列和OLED如SSD1306使用的是Mode 0 (CPOL0, CPHA0)。而一些老式的音频编解码器Codec可能使用Mode 3 (CPOL1, CPHA1)。你永远不能假设必须查手册我曾经因为没查清一个MPU6050的SPI模式它其实支持SPI但默认是I2C硬是调了两天最后发现只是CPOL配反了一改就通。MSB/LSB First最高/最低位优先这决定了一个字节8-bit的数据是先发最高位Bit7还是先发最低位Bit0。这通常由SPI外设的寄存器配置决定和CPOL/CPHA无关。绝大多数设备默认是MSB First。如果你在示波器上看到数据波形和预期不符除了时序也要检查这个。帧格式Frame Format一个SPI“帧”通常包含一个完整的字节8-bit或字16-bit数据。但有些设备如某些ADC要求一次传输16个bit其中前3个bit是命令后13个bit是数据。这就需要SPI外设支持可变长度的帧或者由软件手动拼接。STM32的SPI外设支持8/16-bit帧长而ESP32的SPI驱动则更灵活可以通过API指定任意bit数。3.2 I2C的物理层与电气特性上拉电阻不是随便选的如果说SPI的难点在于“时序匹配”那么I2C的难点就在于“物理层搭建”。它的开漏设计让上拉电阻成了整个系统的“心脏”。上拉电阻Pull-up Resistor它的作用是当所有设备都“松开”SDA/SCL线时将线拉高到VCC电平。它的阻值选择是一个典型的“鱼与熊掌”问题阻值太小如1kΩ上拉能力强上升沿快可以支持更高的通信速率。但缺点是当任何一个设备把线拉低时流过电阻的电流会很大I VCC / R造成不必要的功耗还可能超过设备IO口的灌电流Sink Current能力导致IO口损坏。阻值太大如100kΩ功耗极小但上拉能力弱上升沿会非常缓慢导致信号在“高电平”区域停留时间过长无法满足I2C协议对上升时间tr的要求从而被Master误判为“总线忙”或“超时”。如何计算合适的上拉电阻这需要查阅两个关键参数VOLOutput Low Voltage设备在灌入最大电流IOL时其IO口能保证的最低输出电压。例如一个IO口标称“IOL 3mA, VOL 0.4V”意思是当它灌入3mA电流时输出电压不会高于0.4V。总线电容Cb这是所有挂在总线上的设备SDA/SCL引脚的输入电容Cin之和再加上PCB走线本身的分布电容通常按1-2pF/cm估算。一个典型的I2C总线电容在100-400pF之间。计算公式如下Rmin (VCC - VOL) / IOL Rmax tr / (0.8473 * Cb)其中tr是I2C协议规定的最大上升时间Standard-mode: 1000ns, Fast-mode: 300ns。举个实例VCC3.3V, VOL0.4V, IOL3mA, Cb200pF, 目标是Fast-mode (tr300ns)。Rmin (3.3 - 0.4) / 0.003 ≈ 967ΩRmax 300e-9 / (0.8473 * 200e-12) ≈ 1.77kΩ所以上拉电阻应该在1kΩ到1.8kΩ之间。实践中我会首选2.2kΩ因为它在Rmax之上一点点能保证足够的裕量同时功耗依然可控3.3V/2200Ω ≈ 1.5mA。这是一个经过无数次验证的“安全甜点”。总线电容Cb的实战测量法与其费劲去查所有器件的手册不如用万用表的电容档直接测量PCB上SCL和GND之间的电容。这个值就是Cb的上限。如果测出来是350pF那你基本就告别Fast-mode了老老实实用Standard-mode100kHz吧。3.3 I2C的协议层地址、ACK、NACK与STOP条件I2C的协议层是一套精巧的“握手艺术”它确保了在共享总线上数据能准确无误地送达目标。7位地址7-bit Address每个I2C设备都有一个唯一的7位地址范围是0x00到0x7F128个。这个地址通常由设备的硬件引脚A0, A1, A2决定或者由软件配置。例如一个常见的AT24C02 EEPROM其地址的高4位固定为1010b低3位由A0-A2引脚电平决定所以它可以有8个不同地址0x50-0x57。注意数据手册里写的地址通常是左移一位后的8位格式7位地址 1位R/W位编程时一定要看清楚。START与STOP条件这是I2C的“交通灯”。START条件是SCL为高时SDA从高变低。STOP条件是SCL为高时SDA从低变高。这两个条件只有Master可以发起。它们标志着一次完整通信的开始和结束。任何设备在检测到START后都会监听接下来的地址检测到STOP后则知道本次通信已结束。ACK与NACKAcknowledgement这是I2C的“确认回执”。每次传输完一个字节8-bit后接收方无论是Master还是Slave都必须在第9个SCL周期内将SDA拉低ACK或保持高NACK。Slave ACK表示它成功收到了地址或数据并且准备好接收下一个字节。Slave NACK表示它“忙”比如正在处理内部操作无法接收新数据或者地址不匹配在地址阶段NACK或者数据缓冲区已满在数据阶段NACK。Master收到NACK就必须立即停止传输发出STOP。Master ACK/NACK在读取数据时Master在接收完最后一个字节之前会发送ACK表示“还要”在接收完最后一个字节后会发送NACK表示“够了”然后发出STOP。实操心得当你用逻辑分析仪抓到I2C波形却看不到数据时第一件事就是放大看ACK位。如果每个字节后面都是NACK那90%的可能是地址错了或者Slave根本没上电/没焊好。我见过太多次因为一个虚焊的GND导致整个I2C总线瘫痪而现象就是Master永远收不到ACK。4. 实操过程详解从零开始手把手调通一个I2C传感器4.1 硬件准备一块面包板就是你的实验室我们以最常见的BME280温湿度气压传感器I2C接口为例用一块STM32F103C8T6俗称“蓝 pill”开发板来实操。这个组合成本极低但足以覆盖所有核心知识点。BME280模块市面上有多种模块核心是BME280芯片。它有4个引脚VCC3.3V、GND、SCL、SDA。注意BME280是3.3V器件绝对不能接5V很多廉价模块会自带3.3V LDO但也有直接把VCC引出的务必确认。上拉电阻根据前面的计算我们选用4.7kΩ的贴片电阻比2.2kΩ稍大是为了在面包板这种高接触电阻的环境下提供更稳定的上拉。将两个4.7kΩ电阻一端分别接到SCL和SDA线上另一端都接到3.3V电源。连接方式BME280 VCC → STM32 3.3VBME280 GND → STM32 GNDBME280 SCL → STM32 PB6 (I2C1_SCL)BME280 SDA → STM32 PB7 (I2C1_SDA)提示STM32F103的PB6/PB7是I2C1的默认引脚硬件上已经做了开漏配置无需额外设置。但如果你用的是其他引脚比如PA9/PA10就必须在代码里手动配置为开漏输出Open-Drain。4.2 软件初始化HAL库的“魔法”与陷阱我们使用ST官方的HAL库因为它封装了底层细节但也隐藏了一些关键配置容易踩坑。// 1. 初始化I2C外设 hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 100000; // Standard-mode: 100kHz hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; // 标准占空比 hi2c1.Init.OwnAddress1 0; // 主控不需要自己的地址 hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; // 7位地址 hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 0; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(hi2c1) ! HAL_OK) { Error_Handler(); }这段代码里ClockSpeed 100000是最常被忽略的陷阱。HAL库的HAL_I2C_Init()函数会根据这个值自动计算并配置I2C的时序寄存器TRISE和CCR。但它的计算是基于一个预设的APB1总线频率通常是36MHz。如果你的系统时钟配置RCC没有正确设置或者你修改了APB1的分频系数那么HAL库计算出的CCR值就会是错的导致SCL波形严重失真。解决方案在调用HAL_I2C_Init()之前务必用SystemCoreClockUpdate()更新系统时钟变量或者更稳妥的做法是直接查看stm32f1xx_hal_i2c.c源码找到I2C_SetTiming()函数用示波器实测SCL波形手动微调hi2c1.Init.ClockSpeed的值直到波形完美为止。4.3 地址扫描找到你的“迷路的设备”在发送任何数据之前你必须确认BME280真的在线并且地址正确。最可靠的方法是写一个简单的“I2C地址扫描器”。void I2C_Scan(void) { uint8_t addr; HAL_StatusTypeDef ret; for (addr 0x08; addr 0x78; addr) { // 0x00-0x07是保留地址 ret HAL_I2C_IsDeviceReady(hi2c1, (uint16_t)(addr1), 3, 10); if (ret HAL_OK) { printf(Found device at 0x%02X\r\n, addr); } } }这个函数会遍历所有可能的7位地址0x08到0x77对每个地址发送一个START地址READ请求并等待ACK。如果收到ACK就说明该地址上有设备。运行这个函数你应该能看到类似这样的输出Found device at 0x76这说明BME280的地址是0x76。这个地址是由BME280的SDO引脚电平决定的SDO悬空或接高地址为0x76SDO接地地址为0x77。这一步至关重要它是你和设备建立信任的第一步。如果什么都扫不到那问题一定出在硬件上检查电源、GND、上拉电阻、焊接点。4.4 寄存器读写与BME280的第一次“对话”BME280的数据手册里有一张长长的寄存器映射表。我们要做的就是按照I2C协议向它写入配置然后从它读取数据。步骤1软复位Soft Reset向寄存器0xE0写入0xB6让BME280回到初始状态。uint8_t cmd_reset[2] {0xE0, 0xB6}; HAL_I2C_Master_Transmit(hi2c1, (0x761), cmd_reset, 2, 100); HAL_Delay(10); // 等待复位完成步骤2配置传感器写入0xF2, 0xF4, 0xF5这三个寄存器分别控制湿度、温度/压力的过采样Oversampling和滤波器。我们将其全部设为“跳过”Skip以获得最快的响应速度。uint8_t config_data[7] {0xF2, 0x01, 0xF4, 0x25, 0xF5, 0xA0, 0x80}; // 详细含义见手册 HAL_I2C_Master_Transmit(hi2c1, (0x761), config_data, 7, 100);步骤3读取校准数据24个字节BME280的精度依赖于内部的24字节校准参数。我们必须先读取它们才能进行后续的温度/压力计算。uint8_t calib_buf[24]; HAL_I2C_Mem_Read(hi2c1, (0x761), 0x88, I2C_MEMADD_SIZE_8BIT, calib_buf, 24, 100);步骤4读取原始数据3个字节向寄存器0xF7发起一次读取会连续读出0xF7~0xFA共4个字节分别是压力3字节和温度3字节的原始值MSB, LSB, XLSB。uint8_t raw_data[8]; HAL_I2C_Mem_Read(hi2c1, (0x761), 0xF7, I2C_MEMADD_SIZE_8BIT, raw_data, 8, 100); uint32_t press_raw (raw_data[0] 12) | (raw_data[1] 4) | (raw_data[2] 4); uint32_t temp_raw (raw_data[3] 12) | (raw_data[4] 4) | (raw_data[5] 4);实操心得HAL_I2C_Mem_Read()这个函数是HAL库为I2C内存映射设备如EEPROM、传感器提供的高级API。它内部会自动完成“发送地址寄存器偏移”和“接收数据”两个步骤比手动调用HAL_I2C_Master_Transmit()和HAL_I2C_Master_Receive()要简洁得多。但它的前提是设备必须支持“内存映射”访问即先发一个地址再读数据。BME280支持所以用它没问题。但如果你对接的是一个不支持内存映射的老式设备就必须手动分两步走。4.5 数据计算把原始数字变成真实世界BME280返回的press_raw和temp_raw只是ADC的原始计数值必须经过复杂的补偿计算才能得到真实的摄氏度和百帕hPa。温度计算这是一个包含多项式的浮点运算涉及dig_T1,dig_T2,dig_T3等校准参数。公式在数据手册的“Compensation formulas”章节有详细描述。核心思想是先用校准参数对原始值进行线性补偿再用二次项进行非线性修正。压力计算同样复杂需要dig_P1到dig_P9共9个参数。为了不让你迷失在数学符号里我直接给出一个经过验证的、精简版的C语言实现仅展示温度部分int32_t t_fine; // 全局变量用于存储精细温度 int32_t compensate_temperature(int32_t adc_T) { int32_t var1, var2, T; var1 ((((adc_T3) - ((int32_t)calib.dig_T11))) * ((int32_t)calib.dig_T2)) 11; var2 (((((adc_T4) - ((int32_t)calib.dig_T1)) * ((adc_T4) - ((int32_t)calib.dig_T1))) 12) * ((int32_t)calib.dig_T3)) 14; t_fine var1 var2; T (t_fine * 5 128) 8; return T; // 单位0.01°C }调用这个函数传入temp_raw返回值就是温度值单位是0.01°C所以除以100就是摄氏度。你会发现一个简单的printf(Temp: %d.%02d C\r\n, T/100, T%100);就能在串口看到真实的温度了。注意事项这些计算涉及大量32位整数运算对MCU的算力有一定要求。STM32F103完全OK但如果你用的是8位AVR单片机可能就需要启用编译器的-O2优化或者考虑用查表法LUT来加速。5. 常见问题与排查技巧实录那些年我们一起踩过的坑5.1 “找不到设备”硬件排查的黄金四步法这是I2C新手遇到的第一个、也是最普遍的问题。不要急着骂代码先按顺序检查这四点电源与地Power Ground用万用表直流电压档测量BME280的VCC和GND引脚确认电压是稳定的3.3V误差±0.1V。再测量STM32的3.3V引脚确认它没被拉低。这是90%问题的根源。我曾因为一个松动的杜邦线让VCC在3.3V和0V之间跳变导致扫描器永远找不到设备。**