STM32F103驱动AT24C02 EEPROM的I2C硬件时序与稳定读写实战

1. 项目概述:为什么一个2KB的EEPROM值得花一整天去“较真”

你手头有一块最常见的STM32F103C8T6最小系统板,外接了一个AT24C02——就是那个贴着电路板、只有8个引脚、标着“24C02”字样的小黑片。它容量只有2KB,价格不到一块钱,但偏偏在工业控制面板、智能电表、医疗设备校准参数存储、甚至某些玩具的关机记忆里,它比Flash更可靠、比RAM更持久。可就是这么个小东西,多少人在I2C通信上卡了三天:写进去的数据读出来是乱码,地址算错导致写到隔壁寄存器,时钟拉不起来,示波器上看SCL波形像心电图一样抖……这不是玄学,是I2C协议细节和STM32硬件外设配置之间那几微秒的默契没对上。

我带过不少刚从51单片机转过来的开发者,他们第一反应是“查手册”,结果翻到《STM32F10x参考手册》第23章I2C章节,密密麻麻30页,全是状态机转移图和寄存器位定义;再去看AT24C02数据手册,又是页地址、字地址、当前地址读、随机读、顺序读一堆模式。没人告诉你:真正决定成败的,不是你有没有调通,而是你是否理解“起始信号之后第7个SCL上升沿,SDA必须稳定在有效电平”这个物理约束背后,GPIO输出速度、上拉电阻阻值、总线电容三者如何相互制衡。这篇文章不讲理论推导,只讲我实测下来最稳的接线方式、最不容易出错的初始化顺序、最能暴露问题的调试技巧,以及——为什么用标准库写出来的代码,在某些PCB上跑得飞快,换一块板子就丢包。核心关键词全在这里:STM32F103、I2C、AT24C02、EEPROM读写、硬件时序、上拉电阻计算、ACK检测、页写模式。适合所有正在用F103做实际产品的工程师,也适合准备毕业设计、想把“点亮LED”升级成“存下用户设置”的电子系同学。你不需要会RTOS,不需要懂HAL库底层,只要能看懂寄存器手册里的位定义,就能跟着一步步走通。

2. 硬件连接与电气特性:一根线没接对,后面全是白忙活

2.1 物理层连接:SCL/SDA不是随便拉根线就能通的

AT24C02是标准I2C从设备,支持100kHz(标准模式)和400kHz(快速模式),而STM32F103的I2C外设最高支持400kHz。但速率不是越高越好。我见过太多人一上来就配400kHz,结果在长走线、多器件并联的板子上,SCL边沿严重过冲,SDA被拉低后回弹缓慢,直接导致从机无法识别起始/停止条件。所以第一步,先按最稳妥的100kHz来搭环境。

  • SCL引脚:接STM32的PB6(I2C1_SCL),必须通过上拉电阻接到VCC(3.3V)。不能直接接VCC,也不能悬空。
  • SDA引脚:接STM32的PB7(I2C1_SDA),同样必须通过上拉电阻接到VCC。
  • GND:STM32和AT24C02共地,这是底线,没有商量余地。
  • VCC:AT24C02工作电压是1.8V–5.5V,但F103是3.3V系统,所以直接接3.3V即可。注意:有些廉价模块把VCC接到5V,这会导致AT24C02长期工作在超压状态,数据保持时间大幅缩短,实测半年后部分扇区开始掉数据。
  • A0/A1/A2引脚:这三个地址选择引脚决定了I2C从机地址的低三位。AT24C02默认地址是0x50(二进制01010000),其中高4位固定为0101,低3位由A2/A1/A0决定。比如A2=A1=A0=GND,则地址为0x50;若A2=VCC, A1=A0=GND,则地址为0x51。关键点来了:很多初学者把A0-A2全悬空,以为默认是0,其实悬空引脚电平是不确定的,可能被干扰拉高或拉低,导致地址识别错误,I2C扫描时根本找不到设备。我的做法是:全部下拉到GND,确保地址固定为0x50,后续调试无歧义。

提示:不要用万用表“通断档”去测SCL/SDA是否连通——那是测短路的。要用二极管档或电压档,确认上拉电阻两端有3.3V压降,且SDA在空闲时确实是高电平(约3.2V以上)。

2.2 上拉电阻的计算:不是“10K就行”,而是要算出来

上拉电阻R_p的选择,本质是在“上升时间”和“驱动电流”之间找平衡。太小(如2.2K),MCU的IO口需要灌入过大电流,长期运行发热,且总线电容大时上升沿变缓;太大(如100K),上升时间过长,违反I2C时序要求,尤其在100kHz下,SCL高电平时间最小为4.7μs,SDA上升时间必须远小于此值。

计算公式如下(来自I2C总线规范):

$$ t_r = 0.8473 \times R_p \times C_b $$

其中:

  • $t_r$ 是SDA/SCL信号上升时间(单位:秒)
  • $R_p$ 是上拉电阻(单位:欧姆)
  • $C_b$ 是总线电容(单位:法拉)

对于100kHz标准模式,最大允许上升时间为1000ns(1μs)。实测一块双面板、走线长度<10cm、挂载1个AT24C02的典型场景,$C_b$ 约为100pF(0.0000000001F)。

代入公式: $$ 1000 \times 10^{-9} = 0.8473 \times R_p \times 100 \times 10^{-12} $$ 解得: $$ R_p \approx 11.8K\Omega $$

所以4.7K–10K是安全区间,我固定用4.7K。原因很简单:F103的IO口在3.3V下,最大灌电流为20mA,当SDA被从机拉低时,流经上拉电阻的电流为 $3.3V / 4.7K\Omega \approx 0.7mA$,远低于极限值,IO口温升几乎为零;同时4.7K在100pF负载下,理论上升时间约 $0.8473 \times 4700 \times 100 \times 10^{-12} \approx 400ns$,完全满足1μs要求。

注意:如果你的板子上还挂了其他I2C设备(如OLED、温湿度传感器),总线电容会叠加。每增加一个SOIC-8封装的芯片,电容约增加5–10pF;PCB走线每厘米增加约1–2pF。此时必须重新计算,宁可选小一点的电阻(如3.3K),也不要冒险用10K。

2.3 电源去耦与布线禁忌:那些藏在示波器波形里的“幽灵”

AT24C02对电源噪声极其敏感。我曾遇到一个案例:同一份固件,在实验室面包板上100%成功,焊接到正式PCB后,写入失败率高达30%。用示波器抓VCC,发现纹波峰峰值达150mV,频率集中在10MHz附近——这是DC-DC开关电源的辐射干扰,通过电源平面耦合到AT24C02的VCC引脚,导致内部逻辑紊乱。

解决方案只有两个:

  1. 在AT24C02的VCC和GND引脚之间,紧贴芯片焊一颗100nF X7R陶瓷电容。这是硬性规定,不能省略,不能用10μF电解电容代替(高频响应差)。
  2. I2C总线走线必须远离高速信号线。绝对禁止与USB差分线、SPI时钟线、RF天线馈线平行布线超过5mm。如果空间紧张,必须用地线隔离带隔开,且SCL/SDA走线长度尽量相等(差分思想),减少信号偏斜。

实操心得:在PCB打样前,用尺子量一下SCL和SDA从MCU到AT24C02的走线长度。如果相差超过2mm,就在稍短的那根线上加一段蛇形走线。别笑,这真的影响ACK响应时间——我亲眼见过因长度差3mm,导致某批次芯片在-20℃低温下ACK丢失。

3. STM32F103 I2C外设初始化:寄存器级配置的“黄金顺序”

3.1 为什么不能照抄标准库的I2C_Init()函数?

ST的标准外设库(StdPeriph_Lib)里,I2C_Init()函数把时钟配置、GPIO初始化、I2C使能全打包在一起。这在教学Demo里很友好,但在真实项目中,它隐藏了一个致命细节:I2C外设时钟使能(RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE))必须在GPIO时钟使能(RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE))之后,且必须在GPIO模式配置(GPIO_Init())完成之后,才能执行I2C使能(I2C_Cmd(I2C1, ENABLE))。顺序错一个,I2C_CR1寄存器的PE位(Peripheral Enable)就可能被硬件忽略,外设永远处于复位态。

我拆解过标准库源码,它的初始化流程是:

  1. 开GPIO时钟 → 配置PB6/PB7为开漏输出 → 开I2C时钟 → 调用I2C_Init() → 最后I2C_Cmd(ENABLE)

这看似合理,但问题出在“配置PB6/PB7为开漏输出”这一步。F103的GPIOB端口复位后,默认是浮空输入模式。当你执行GPIO_Init()把PB6设为开漏输出时,如果此时I2C时钟还没开,PB6的复用功能(AFIO)根本没激活,IO口只会按普通GPIO行为输出,SCL线会一直被拉低(因为开漏输出+上拉电阻=高电平,但初始状态是浮空,上拉电阻把它拉高了?不对!开漏输出的“高电平”其实是高阻态,靠上拉电阻实现,所以初始状态是高电平。但问题在于:在I2C外设未使能时,PB6的复用功能未开启,它只是个普通IO,你给它设开漏输出,它确实会输出高阻,但SCL线上电瞬间,由于没有I2C硬件控制,这条线就是个悬空的高阻节点,极易受干扰。一旦有干扰脉冲把它误拉低,I2C外设上电后检测到SCL为低,就会认为总线被占用(BUSY flag置位),死锁。

所以我的初始化顺序是教科书式的“四步铁律”:

  1. 开GPIOB时钟(RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE))
  2. 配置PB6/PB7为开漏输出,且初始输出为高电平(GPIO_WriteBit(GPIOB, GPIO_Pin_6|GPIO_Pin_7, Bit_SET);然后GPIO_Init()设Mode为GPIO_Mode_Out_OD,Speed为GPIO_Speed_50MHz)
  3. 开I2C1时钟(RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE))
  4. 配置I2C寄存器,最后才使能外设(I2C_Cmd(I2C1, ENABLE))

实操心得:在第2步配置完GPIO后,用万用表电压档量一下PB6和PB7对地电压,必须是3.2V以上。如果只有0.5V,说明IO口被意外拉低了,检查原理图是否有其他器件挂在同一条线上,或者PCB焊接短路。

3.2 关键寄存器配置详解:CCR、TRISE、OAR1一个都不能错

I2C初始化的核心是三个寄存器:I2C_CCR(时钟控制)、I2C_TRISE(上升时间)、I2C_OAR1(自身地址)。很多人只改CCR,忘了TRISE,结果在不同主频下表现不一。

  • I2C_CCR(Clock Control Register):决定SCL频率。公式为: $$ f_{SCL} = \frac{f_{PCLK1}}{2 \times (CCR + 1)} \quad \text{(标准模式)} $$ F103的PCLK1(APB1总线时钟)默认是36MHz(HSE=8MHz,PLL=9倍频)。要得到100kHz SCL,代入得: $$ CCR = \frac{36 \times 10^6}{2 \times 100 \times 10^3} - 1 = 179 $$ 所以I2C_CCR = 179;。注意:CCR是16位寄存器,最高位D15是F/S位(Fast/Standard mode),标准模式必须清零。

  • I2C_TRISE(TRISE Register):设定最大上升时间,单位是PCLK1周期。公式为: $$ TRISE = (t_{rise_max} \times f_{PCLK1}) + 1 $$ 对于100kHz,$t_{rise_max}=1000ns$,$f_{PCLK1}=36MHz$,则: $$ TRISE = (1000 \times 10^{-9} \times 36 \times 10^6) + 1 = 36 + 1 = 37 $$ 所以I2C_TRISE = 37;。这个值必须精确,它直接影响硬件对起始/停止条件的采样窗口。

  • I2C_OAR1(Own Address Register 1):F103作为主机时,这个寄存器其实不用配置(主机不响应地址),但必须写0。很多教程留空或写默认值,会导致I2C_CR1的ACK位异常。我的做法是显式写I2C_OAR1 = 0x0000;,清空所有位。

初始化代码片段(纯寄存器操作,无库依赖):

// 1. 开GPIOB时钟 RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; // 2. 配置PB6/PB7为开漏输出,初始高电平 GPIOB->CRH &= ~(GPIO_CRH_MODE6 | GPIO_CRH_CNF6 | GPIO_CRH_MODE7 | GPIO_CRH_CNF7); GPIOB->CRH |= GPIO_CRH_MODE6_1 | GPIO_CRH_CNF6_0 | GPIO_CRH_MODE7_1 | GPIO_CRH_CNF7_0; // 50MHz开漏 GPIOB->BSRR = GPIO_BSRR_BS6 | GPIO_BSRR_BS7; // PB6/PB7置高(高阻态,靠上拉变高) // 3. 开I2C1时钟 RCC->APB1ENR |= RCC_APB1ENR_I2C1EN; // 4. 配置I2C寄存器 I2C1->CR2 = 36; // PCLK1=36MHz,用于计算CCR I2C1->OAR1 = 0x0000; // 主机模式,清空地址 I2C1->CCR = 179; // 100kHz标准模式 I2C1->TRISE = 37; // 上升时间37个PCLK1周期 I2C1->CR1 = I2C_CR1_PE; // 仅使能外设,不开启应答(主机不用ACK)

3.3 状态机与标志位:读懂I2C_SR1/2才是调试的灵魂

I2C的状态判断不靠延时,全靠轮询I2C_SR1(Status Register 1)和I2C_SR2(Status Register 2)的标志位。新手常犯的错误是:看到I2C_SR1_SB(Start Bit)置位,就立刻往I2C_DR写地址,结果总线冲突。因为SB置位只表示起始条件已发出,但硬件需要时间让SCL稳定,此时必须等待I2C_SR1_ADDR(Address Sent)或I2C_SR1_TXE(Transmit Data Register Empty)就绪。

完整的“写一个字节”状态流是:

  1. I2C_SR1_SB == 1→ 写从机地址+写方向(0x50<<1 | 0x00 = 0xA0)到I2C_DR
  2. I2C_SR1_ADDR == 1→ 读一次I2C_SR2(清除ADDR标志),然后写数据到I2C_DR
  3. I2C_SR1_BTF == 1(Byte Transfer Finished)→ 表示字节已发送并收到ACK,可以发停止或继续发

而“读一个字节”的流程更复杂,涉及两次起始:

  1. 第一次起始+写地址(0xA0)+写内存地址 → 触发从机内部地址指针
  2. 第二次起始+读地址(0xA0<<1 | 0x01 = 0xA1)→ 从机开始输出数据
  3. I2C_SR1_RXNE == 1→ 从I2C_DR读数据

常见陷阱:在读操作中,当你要读最后一个字节时,必须在读取I2C_DR之前,先向I2C_CR1写I2C_CR1_ACK清零(NACK),再发停止。否则从机会继续输出下一个地址的数据,而你没地方存,造成溢出。这个细节,90%的网上例程都错了。

4. AT24C02读写全流程:从单字节到页写的实战代码与避坑指南

4.1 单字节写入:为什么“写完就读”会失败?

AT24C02的写入不是即时生效的。当你发送一个字节到指定地址后,芯片内部会启动一个“写周期”(Write Cycle),典型时间为5ms(最大10ms)。在此期间,它不会响应任何I2C请求,SCL会被它拉低(Clock Stretching),直到内部擦写完成。如果你在写入命令后立刻发起读操作,主机会在SCL被拉低时超时,返回错误。

正确的单字节写流程(带超时保护):

  1. 发送起始信号
  2. 发送从机地址(0xA0)+ 等待ACK
  3. 发送内存地址(高8位,AT24C02是2Kbit=256字节,地址范围0x00–0xFF,只需1字节地址)
  4. 发送要写入的数据字节 + 等待ACK
  5. 发送停止信号
  6. 延时至少10ms(保险起见,我用Delay_ms(15))
  7. 此时才能进行下一次操作

代码实现(精简版):

uint8_t AT24C02_WriteByte(uint8_t addr, uint8_t data) { uint32_t timeout = 0; // 1. 起始 I2C1->CR1 |= I2C_CR1_STA; while (!(I2C1->SR1 & I2C_SR1_SB)) { if (++timeout > 10000) return 1; // 超时 } // 2. 发送从机地址+写 I2C1->DR = 0xA0; while (!(I2C1->SR1 & I2C_SR1_ADDR)) { if (++timeout > 10000) return 1; } (void)I2C1->SR2; // 清ADDR标志 // 3. 发送内存地址 I2C1->DR = addr; while (!(I2C1->SR1 & I2C_SR1_TXE)) { if (++timeout > 10000) return 1; } // 4. 发送数据 I2C1->DR = data; while (!(I2C1->SR1 & I2C_SR1_BTF)) { if (++timeout > 10000) return 1; } // 5. 停止 I2C1->CR1 |= I2C_CR1_STO; Delay_ms(15); // 等待写周期完成 return 0; // 成功 }

实操心得:不要用for(i=0;i<15000;i++)这种空循环做延时,F103主频变化时误差巨大。必须用SysTick定时器做毫秒级延时,或者用usleep()(如果用了FreeRTOS)。我测试过,用空循环在72MHz下延时15ms,实际可能是12ms或18ms,刚好卡在AT24C02写周期的临界点,导致偶发失败。

4.2 单字节读取:两次起始的“心跳”节奏

读取比写入更考验时序把控。核心是理解AT24C02的“当前地址读”(Current Address Read)和“随机读”(Random Read)的区别:

  • 当前地址读:不指定地址,直接发读地址(0xA1),从机从上次写入后的地址指针开始输出。适用于连续读,但首次上电指针位置不确定,慎用。
  • 随机读:先发写地址(0xA0)+ 指定内存地址(定位指针),再发起始+读地址(0xA1),从机从指定地址开始输出。这才是可靠做法。

随机读的完整流程:

  1. 起始 → 发0xA0(写)→ 发内存地址addr → 等待ACK
  2. 再次起始(不是停止!)→ 发0xA1(读)→ 等待ACK
  3. 读I2C_DR→ 得到数据
  4. 发送停止

关键点在于第2步的“再次起始”。很多初学者在第一步写地址完成后,习惯性发停止,再发新起始,这会导致从机地址指针重置,读出来的不是你要的地址。

代码实现:

uint8_t AT24C02_ReadByte(uint8_t addr, uint8_t *data) { uint32_t timeout = 0; // 第一次起始:写地址定位 I2C1->CR1 |= I2C_CR1_STA; while (!(I2C1->SR1 & I2C_SR1_SB)) { if (++timeout > 10000) return 1; } I2C1->DR = 0xA0; while (!(I2C1->SR1 & I2C_SR1_ADDR)) { if (++timeout > 10000) return 1; } (void)I2C1->SR2; I2C1->DR = addr; // 发送要读的内存地址 while (!(I2C1->SR1 & I2C_SR1_TXE)) { if (++timeout > 10000) return 1; } // 第二次起始:切换到读模式 I2C1->CR1 |= I2C_CR1_STA; while (!(I2C1->SR1 & I2C_SR1_SB)) { if (++timeout > 10000) return 1; } I2C1->DR = 0xA1; // 读地址 while (!(I2C1->SR1 & I2C_SR1_ADDR)) { if (++timeout > 10000) return 1; } (void)I2C1->SR2; // 读数据(此时BTF会置位,RXNE也会置位) while (!(I2C1->SR1 & I2C_SR1_RXNE)) { if (++timeout > 10000) return 1; } *data = I2C1->DR; I2C1->CR1 |= I2C_CR1_STO; // 停止 return 0; }

4.3 页写模式(Page Write):效率提升16倍的关键

AT24C02的页大小是16字节。这意味着,你可以在一次写操作中,连续发送最多16个字节,它们会被自动写入从指定地址开始的连续内存中。这比单字节写快得多——因为省去了15次起始/停止/地址传输的开销。

页写规则:

  • 起始 → 发0xA0 → 发起始地址(比如0x00)→ 发16个字节 → 停止
  • 地址不能跨页。如果起始地址是0x0F,你发16个字节,第16个会写到0x1E,没问题;但如果起始是0x10,第16个会到0x1F,也没问题;但起始是0x11,第16个会到0x20,这就跨页了(0x10–0x1F是一页),芯片会自动把0x20之后的数据写回本页开头(0x11),造成覆盖。所以页写前必须检查:(addr + len) & 0xF0必须等于addr & 0xF0,即高4位地址相同。

页写函数(带地址越界检查):

uint8_t AT24C02_PageWrite(uint8_t addr, uint8_t *buf, uint8_t len) { if (len == 0 || len > 16) return 1; if ((addr & 0xF0) != ((addr + len - 1) & 0xF0)) return 1; // 跨页检查 uint32_t timeout = 0; I2C1->CR1 |= I2C_CR1_STA; while (!(I2C1->SR1 & I2C_SR1_SB)) { if (++timeout > 10000) return 1; } I2C1->DR = 0xA0; while (!(I2C1->SR1 & I2C_SR1_ADDR)) { if (++timeout > 10000) return 1; } (void)I2C1->SR2; I2C1->DR = addr; while (!(I2C1->SR1 & I2C_SR1_TXE)) { if (++timeout > 10000) return 1; } for (uint8_t i = 0; i < len; i++) { I2C1->DR = buf[i]; while (!(I2C1->SR1 & I2C_SR1_TXE)) { if (++timeout > 10000) return 1; } } I2C1->CR1 |= I2C_CR1_STO; Delay_ms(15); return 0; }

实操心得:页写后必须延时15ms,不能因为“写了多个字节就延时更久”。AT24C02的写周期是针对整页的,不是每个字节单独计时。我实测过,写1个字节和写16个字节,内部擦写时间几乎一样,都是5–10ms。

5. 调试与故障排查:示波器看不到的“软故障”怎么抓?

5.1 常见问题速查表:从现象反推根源

现象最可能原因快速验证方法解决方案
I2C扫描找不到设备(0x50)A0/A1/A2悬空;上拉电阻未接;VCC未供;SCL/SDA接反用万用表测PB6/PB7对地电压,应为3.2V;测AT24C02 VCC是否3.3V将A0-A2全部下拉到GND;确认上拉电阻焊接;检查电源网络
写入后读出全是0xFF写入后未延时,就读;地址指针错乱;AT24C02写保护引脚WP接地用逻辑分析仪抓波形,看写操作后是否有足够空闲时间;读0x00地址确认严格加入15ms延时;用随机读模式;确认WP引脚为高电平(AT24C02 WP=H时写保护)
读取数据偶尔错位(如读0x00得到0x01)SCL/SDA走线长度差太大;总线电容超标;电源纹波大示波器抓SCL上升沿,看是否过缓(>500ns);测VCC纹波加粗地线;缩短走线;增加100nF去耦电容;换小上拉电阻
连续读取时,第2个字节开始全为0x00读操作中未发第二次起始,而是发了停止再起始;从机地址错逻辑分析仪看I2C波形,确认第二次起始信号是否存在严格按“写地址→起始→读地址”流程;检查0xA0/0xA1是否写错
程序跑着跑着I2C卡死(SR1_BUSY始终为1)从机在写周期中被强行访问;SCL被外部拉低未释放用万用表测PB6对地电压,若为0V则SCL被锁死断电重启;在每次I2C操作前加BUSY检测,超时则强制复位I2C外设

5.2 逻辑分析仪实战:一眼看穿ACK丢失

没有示波器?一个20元的Saleae Logic 8通道逻辑分析仪就能救命。把通道0接PB6(SCL),通道1接PB7(SDA),设置采样率1MHz,触发条件设为“SCL下降沿”,捕获一段写操作。

正常波形特征:

  • 起始信号:SCL高时,SDA从高→低
  • 地址字节:8个SCL周期,每个周期SDA稳定,第9个周期SDA为低(ACK)
  • 数据字节:同上,第9个周期SDA为低(ACK)
  • 停止信号:SCL高时,SDA从低→高

如果第9个周期SDA为高,就是NACK,说明:

  • 从机地址错(0x50写成0x51)
  • 从机没上电(VCC=0)
  • 从机写保护开启(WP=0)

我曾用这招5分钟定位一个“神隐BUG”:客户板子上AT24C02的WP引脚被PCB设计误连到GND,导致所有写操作都被拒绝,但读操作正常,现象是“能读不能写”,查了两天没找到,逻辑分析仪一抓,第9个周期SDA全是高电平,立刻锁定WP。

5.3 “软故障”的终极武器:I2C总线复位术

当I2C总线被某个设备拉死(SCL或SDA持续为低),标准方法是发9个时钟脉冲,强制从机释放总线。F103没有专用复位引脚,但可以用GPIO模拟:

void I2C1_BusReset(void) { // 把PB6/PB7配置为普通推挽输出 GPIOB->CRH &= ~(GPIO_CRH_MODE6 | GPIO_CRH_CNF6 | GPIO_CRH_MODE7 | GPIO_CRH_CNF7); GPIOB->CRH |= GPIO_CRH_MODE6_0 | GPIO_CRH_CNF6_0 | GPIO_CRH_MODE7_0 | GPIO_CRH_CNF7_0; // 先确保SDA高,SCL高 GPIOB->BSRR = GPIO_BSRR_BS6 | GPIO_BSRR_BS7; Delay_us(5); // 发9个SCL脉冲 for (int i = 0; i < 9; i++) { GPIOB->BSRR = GPIO_BSRR_BR6; // SCL低 Delay_us(5); GPIOB->BSRR = GPIO_BSRR_BS6; // SCL高 Delay_us(5); } // 检查SDA是否被释放(应为高) if ((GPIOB->IDR & GPIO_IDR_IDR7) == 0) { // SDA仍被拉低,总线硬件故障 } // 恢复I2C功能 GPIOB->CRH &= ~(GPIO_CRH_MODE6 | GPIO_CRH_CNF6 | GPIO_CRH_MODE7 | GPIO_CRH_CNF7);

相关新闻

Linux DNS服务实战:从Bind安装到Unknown host故障排查

Linux DNS服务实战:从Bind安装到Unknown host故障排查

前几天帮某公司排查一个内网域名解析故障&#xff0c;应用一直报“Unknown host”&#xff0c;折腾了大半天。后来复盘发现&#xff0c;问题既不在网络&#xff0c;也不在服务进程&#xff0c;而是 DNS 服务器上“监听地址”和“查询权限”的语义没理清。那次之后&#xff0c;我…

2026/10/21 2:11:47 阅读更多 →
Qt桌面端地图开发:QWebEngineView加载百度/谷歌地图与双向通信实战

Qt桌面端地图开发:QWebEngineView加载百度/谷歌地图与双向通信实战

简介&#xff1a;这份资源面向需要在Qt桌面应用中嵌入在线地图的C开发者&#xff0c;重点解决百度地图与谷歌地图的加载与交互问题。包内共25个文件&#xff0c;以cpp源码、h头文件、html页面、ui界面文件及pro工程文件为主&#xff0c;另含编译生成的obj、exe、pdb等中间产物&…

2026/10/21 2:11:47 阅读更多 →
LM4040AIM3-3.0+T微功耗3V基准芯片选型与设计实战

LM4040AIM3-3.0+T微功耗3V基准芯片选型与设计实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 2:11:47 阅读更多 →

最新新闻

MALA幅度感知线性注意力如何高效改造YOLO26

MALA幅度感知线性注意力如何高效改造YOLO26

最近在忙目标检测模型的落地优化&#xff0c;把精力主要放在了YOLO26的注意力机制改造上。试过不少主流的注意力模块&#xff0c;要么计算量上去了推理变慢&#xff0c;要么就是涨点不明显&#xff0c;白折腾。后来读到一篇当时挂在学术预印平台上的新工作&#xff0c;核心是提…

2026/10/21 3:33:25 阅读更多 →
Kettle ETL实战:为什么老工具在混乱数据场景中依然不可替代

Kettle ETL实战:为什么老工具在混乱数据场景中依然不可替代

1. 为什么今天还在用Kettle做ETL&#xff1f;一个被低估的“老派”工具的真实价值很多人看到“Kettle”这个词&#xff0c;第一反应是&#xff1a;这玩意儿不是十年前就该淘汰了吗&#xff1f;现在不都上Airflow、Dagster、dbt&#xff0c;或者直接云厂商的DataFlow、Glue、Dat…

2026/10/21 3:33:25 阅读更多 →
用pgzrun零基础实现克隆人游戏:Python游戏开发入门实践

用pgzrun零基础实现克隆人游戏:Python游戏开发入门实践

1. 项目概述&#xff1a;为什么一个“克隆人”游戏值得用pgzrun重做&#xff1f;最近在某高校的编程入门工作坊里&#xff0c;我带学生做了一个小项目——用Python复刻经典街机游戏《阿肯色克隆人》&#xff08;Arkansas Clone&#xff09;&#xff0c;但没碰Pygame原生API&…

2026/10/21 3:33:25 阅读更多 →
MapLibre Native 地图样式切换指南:3 种方式让地图不重启应用就换外观

MapLibre Native 地图样式切换指南:3 种方式让地图不重启应用就换外观

MapLibre Native 地图样式切换指南&#xff1a;3 种方式让地图不重启应用就换外观 【免费下载链接】maplibre-native MapLibre Native - Interactive vector tile maps for iOS, Android and other platforms. 项目地址: https://gitcode.com/GitHub_Trending/ma/maplibre-na…

2026/10/21 3:33:25 阅读更多 →
REA 逆向工程快速上手:把 REA 装进 AI 编码助手,跑通第一次二进制分析

REA 逆向工程快速上手:把 REA 装进 AI 编码助手,跑通第一次二进制分析

REA 逆向工程快速上手&#xff1a;把 REA 装进 AI 编码助手&#xff0c;跑通第一次二进制分析 【免费下载链接】rea Reverse engineer anything with agents, from app behavior down to native binaries. 项目地址: https://gitcode.com/GitHub_Trending/rea2/rea REA&…

2026/10/21 3:33:25 阅读更多 →
51单片机贪吃蛇实战:12864驱动、EEPROM掉电保存与嵌入式分层设计

51单片机贪吃蛇实战:12864驱动、EEPROM掉电保存与嵌入式分层设计

简介&#xff1a;本资源是一套面向大学本科课程设计的51单片机贪吃蛇游戏完整开发方案&#xff0c;适用于单片机原理与应用、嵌入式系统实践等课程学习与项目实训。资源包含Proteus仿真工程&#xff08;.pdsprj&#xff09;、Keil C51源码&#xff08;.c/.a51&#xff09;、编译…

2026/10/21 3:32:25 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →