
简介本资源是一套基于STM32F103微控制器与TMP102高精度数字温度传感器的I²C通信完整实现方案面向嵌入式初学者及STM32项目开发者解决传感器驱动开发、I²C底层协议调试与温度数据读取等典型工程问题。压缩包共146个文件包含38个头文件.h定义寄存器与接口、36个源文件.c涵盖STM32标准外设库如stm32f10x_i2c.c、stm32f10x_rcc.c、编译中间产物.o/.d/.crf及可执行镜像.axf/.hex辅以Keil工程配置.uvprojx/.uvoptx、启动脚本keilkilll.bat和说明文档.txt整体大小为3.64MB。已有984人学习下载代码经实测运行稳定配套简明说明文档清晰标注初始化流程、寄存器配置要点与温度换算逻辑便于快速移植到同类I²C传感器项目中。 前两天在调试一块温控板主控是STM32F103板载的传感器是一颗TMP102通信走IIC。现象很典型温度值大部分时间正常但偶尔会突然跳成0xFFF8这种明显不对的数据而且持续时间不固定。查了一下午最后发现是配置寄存器里的扩展模式位被程序里某段代码意外改掉了——这个坑让我决定把TMP102配合STM32用IIC驱动的完整经验整理出来。这篇文章适合所有正在用或准备用TMP102的人尤其是刚接触IIC协议、第一次读数字温度传感器、或者遇到读不到数据读数跳变问题的朋友。我会从选型逻辑讲到底层时序再到实际调试踩坑尽量把每个为什么都讲透而不是甩一堆代码让你自己猜。1. 为什么选TMP102而不是DHT11和18B20选型逻辑1.1 与DHT11、18B20的对比TMP102是TI出的一款IIC接口数字温度传感器SOT563封装比一粒米还小。工作电压1.4~3.6V默认12位分辨率下精度是±0.5℃-25℃~85℃范围典型应用电流只有10μA左右关断模式下不到1μA。这些参数放在低功耗、小体积的场景里几乎是为电池设备量身定做的。很多人一提到温度采集第一反应是DHT11或者DS18B20。这两颗传感器本身没问题但对项目需求的适配性差别非常大。我列个对比表大家看得更直观项目TMP102DS18B20DHT11接口IIC可挂多设备单总线1-Wire单总线自定义时序精度±0.5℃典型±0.5℃-10~85±2℃实际经常更差分辨率12位/13位可选9~12位可配置8位最低电压1.4V3.0V3.3V封装SOT563极小TO-92/贴片常见封装较大功耗10μA级别1mA级别转换时0.5mA级别采样速率最快4ms9位模式最快93.75ms9位1秒一次DHT11的精度和采样速度摆在那里做环境温湿度显示还行稍微有点控制逻辑的项目基本不够用。DS18B20的精度和TMP102相当但单总线协议在代码实现上比IIC更麻烦尤其是多设备挂载时ROM搜索命令的时序折腾起来相当费时间。TMP102走的是标准IIC地址可配置一条总线上挂4片轻轻松松代码写起来也规整得多。1.2 什么场景下TMP102是最优解从我自己的项目经验看满足下面任何一种情况TMP102都值得优先考虑第一系统里本来就有IIC总线。比如主板上已经挂了OLED、加速度计或者RTC芯片TMP102直接挂上去就行不需要额外占用IO口也不需要再单独维护一套单总线时序代码。第二低功耗场景。TMP102的关断模式配合单次触发One-Shot转换可以做到醒来测一下、测完睡觉非常适合电池供电的IoT传感器节点。第三小体积需求。SOT563封装打板时几乎不占地贴在PCB边角或者柔性板上都很方便。第四精密控温场景。比如锂电池保护板、恒温晶振补偿、工业传感器温漂校准0.5℃的精度加上13位扩展模式下0.03125℃的分辨率完全够用。反过来如果只需要一根线、温度变化范围大、现场环境恶劣DS18B20可能更皮实如果是消费级温湿度计DHT11便宜够用。选型没有绝对的最好只有适合当前项目。2. 硬件连接里最容易翻车的三个细节IIC地址、上拉电阻、电平匹配2.1 器件地址怎么确定TMP102的7位IIC地址是1001 A2 A1 A0。A2、A1、A0是三个地址引脚接GND表示0接V表示1。三个引脚全接地时7位地址是0x48左移一位后写地址是0x90读地址是0x91。如果想在同一条总线上挂多片就把不同引脚的接线组合换一下。可选的7位地址一共8种组合实际TMP102支持4个这里要澄清一下TMP102的A2引脚在SOT563封装里其实是固定接GND的只有A1和A0可以外部配置所以实际能用的是4个地址0x48~0x4B。手册标注的A2引脚在这个封装中不可用我见过有人试图通过A2扩展地址结果焊了半天没反应白折腾。地址引脚必须明确接GND或V绝对不能悬空。悬空状态下的电平不确定可能这次上电是0下次上电是1导致设备时而能通信时而找不到。我排查过不少偶发通信失败的板子最后都是芯片引脚悬空惹的祸。另外TMP102的地址是不带内部上下拉的别指望省那两颗电阻。2.2 上拉电阻取值与计算IIC总线是开漏结构SCL和SDA必须有外部上拉电阻否则总线根本拉不高。STM32虽然内部有上拉但内部上拉阻值通常在30kΩ~50kΩ对于IIC来说太大了边沿非常慢400kHz高速模式下基本没法稳定通信。实测下来STM32内部上拉跑100kHz勉强能用但抗干扰能力差偶尔丢数据。正规做法是外部加上拉电阻。上拉电阻的取值有两个约束太小了灌电流大太大了上升沿慢。工程上推荐值很简单总线速率上拉电阻推荐值适用场景100kHz标准模式10kΩ线短、设备少、布线干净400kHz快速模式4.7kΩ常规板卡推荐默认值1MHz快速模式2.2kΩ线长、设备多、电容大如果总线电容比较大比如线长超过20cm、总线上挂了4~5个设备4.7k可能都不够需要降到2.2k甚至1k。计算公式是Rp(max)约等于tr / (0.8473 × Cbus)tr是允许的最大上升时间Cbus是总线总电容。实际调板子时不用算太细拿示波器看波形上升沿如果出现明显的圆弧就换小一点的阻值。2.3 电源与去耦TMP102工作电压1.4~3.6V和STM32的3.3V电平直接对接没问题。但要注意上拉电阻接的电源必须和TMP102的供电一致。如果TMP102供电是1.8V上拉却接了3.3V那IIC引脚会被上拉到3.3V超过芯片V电压长期运行可能损坏芯片。供电引脚必须在靠近V的位置放去耦电容我习惯放0.1μF并一个1μF一个滤高频噪声一个应对瞬态电流。别小看这步温度传感器本身是模拟器件电源噪声会直接反映在转换结果上。我遇到过读数跳动0.3℃左右的情况排查到最后就是去耦电容没贴补上之后数据立刻稳了。还有一个细节SDA和SCL不要走太长的平行线尤其不要贴着高频信号线走。IIC是低速总线但长线或者交叉干扰依然会造成波形畸变导致ACK丢失、数据错位。短走线、包地或者加串联电阻都能改善信号质量。3. 软件方案硬件IIC和软件模拟IIC的取舍3.1 硬件外设IIC的典型坑STM32的硬件IIC外设口碑很极端尤其是F1系列。官方勘误表列了一堆问题I2C模块在某些时序组合下会卡在Busy状态或者发出错误的起始条件。我早期用F103的硬件IIC读TMP102就遇到过偶发卡死的情况SCL不再产生时钟SDA保持高电平。网上搜一圈无数人遇到同样的问题解决方案无非是软件复位外设发送总线清除序列治标不治本。不是说STM32的硬件IIC完全不能用。F4、H7系列的I2C外设已经修正了很多问题配合HAL库用起来也不难。但在产品开发阶段尤其是刚接触IIC的新手我建议优先用软件模拟IIC。软件模拟IIC的优势非常明显引脚随意选不受硬件复用的限制时序完全可控调试时可以放慢速度抓波形代码逻辑透明出问题能一步步跟进。代价是占用CPU但对于读温度传感器这种低频操作影响微乎其微。3.2 软件模拟IIC核心代码这是我在项目里一直用的软件IIC基础库基于STM32标准库改成HAL库也非常容易。宏定义部分把引脚抽出来换板子时只需要改这里#define SCL_GPIO GPIOB #define SCL_PIN GPIO_Pin_6 #define SDA_GPIO GPIOB #define SDA_PIN GPIO_Pin_7 #define SCL_H() GPIO_SetBits(SCL_GPIO, SCL_PIN) #define SCL_L() GPIO_ResetBits(SCL_GPIO, SCL_PIN) #define SDA_H() GPIO_SetBits(SDA_GPIO, SDA_PIN) #define SDA_L() GPIO_ResetBits(SDA_GPIO, SDA_PIN) #define SDA_READ() GPIO_ReadInputDataBit(SDA_GPIO, SDA_PIN)初始化时注意SDA要配置成开漏输出这样读输入时直接切换模式就行不会因为输出寄存器状态影响读结果。SCL用推挽输出即可void IIC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin SCL_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(SCL_GPIO, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin SDA_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(SDA_GPIO, GPIO_InitStructure); SCL_H(); SDA_H(); }起始和停止时序是IIC的基础。起始条件是SCL高电平时SDA产生一个下降沿停止条件是SCL高电平时SDA产生一个上升沿。代码实现时注意顺序void IIC_Start(void) { SDA_H(); SCL_H(); delay_us(5); SDA_L(); delay_us(5); SCL_L(); } void IIC_Stop(void) { SDA_L(); SCL_H(); delay_us(5); SDA_H(); delay_us(5); }发送一个字节时数据在SCL低电平期间变化高电平期间保持稳定。这是IIC协议的核心规则写代码时严格执行就不会有问题。接收字节则是主机不驱动SDA从机往总线发数据主机在SCL高电平期间采样void IIC_SendByte(uint8_t dat) { for (uint8_t i 0; i 8; i) { if (dat 0x80) SDA_H(); else SDA_L(); dat 1; delay_us(2); SCL_H(); delay_us(5); SCL_L(); delay_us(2); } SDA_H(); // 释放SDA读取从机ACK delay_us(2); SCL_H(); delay_us(5); SCL_L(); delay_us(2); } uint8_t IIC_RecvByte(void) { uint8_t dat 0; SDA_H(); // 主机释放总线 for (uint8_t i 0; i 8; i) { dat 1; SCL_H(); delay_us(5); if (SDA_READ()) dat | 0x01; SCL_L(); delay_us(5); } return dat; }ACK处理上我习惯在IIC_SendByte里自动读取从机应答但返回值嫌啰嗦就没写。实际项目里可以加一个返回值用于判断设备是否存在。接收数据时最后一个字节要回一个NACK告诉从机别发了然后再发停止信号。3.3 我的选型建议开发阶段用软件模拟IIC量产阶段如果要省CPU或者引脚复用紧张再切硬件IIC。这套思路帮我避了很多坑因为软件模拟IIC的每一步时序都是自己写的出了问题顺着代码走一遍就能定位。而硬件IIC一旦卡死分析手段非常有限大概率是复位外设或者重新初始化GPIO。至于有人问软件模拟IIC能不能用DMA答案是基本不能。DMA是给硬件外设用的软件模拟本身就是靠CPU翻转IO没有DMA参与的空间。TMP102读取一次温度就两个字节数据量太小DMA带来的收益远小于复杂度开销。热词里iic的dma传输通常是针对片上Flash、外部存储这类大块连续读写场景温度传感器完全没必要。4. TMP102寄存器配置与经典读写时序拆解4.1 寄存器总览与地址映射TMP102共有4个16位寄存器通过指针寄存器来切换。通信时先写一个字节指定寄存器地址然后再读写数据。寄存器名称指针地址读写属性作用温度寄存器0x00只读存放最近一次温度转换结果配置寄存器0x01读写控制模式、分辨率、报警参数TLOW寄存器0x02读写低温阈值触发ALERTTHIGH寄存器0x03读写高温阈值触发ALERT另外还有一个只读的器件ID寄存器地址是0x0F固定返回0x54TMP102或0x55TMP102A不同批次可能有差异。我每次初始化时会读一下这个ID用来确认IIC通信正常这是个很好的自检手段。4.2 读温度的完整时序读温度是最常用的操作时序是标准的IIC写指针重起始读数据。先发起始信号然后发送写地址0x90接着发送寄存器指针0x00把内部的指针指到温度寄存器。然后重新发送起始信号发送读地址0x91连续读两个字节。第二个字节读完要回NACK再发停止信号。完整代码float TMP102_ReadTemp(void) { uint8_t buf[2]; int16_t raw; IIC_Start(); IIC_SendByte(0x90); // 器件写地址 0x481 IIC_SendByte(0x00); // 指向温度寄存器 IIC_Start(); // 重起始切换为读 IIC_SendByte(0x91); // 器件读地址 buf[0] IIC_RecvByte(); // 读高字节回ACK IIC_SendAck(0); buf[1] IIC_RecvByte(); // 读低字节 IIC_SendAck(1); // 最后一字节回NACK IIC_Stop(); raw (buf[0] 8) | buf[1]; raw 4; // 12位精度去掉低4位 return raw * 0.0625f; }注意IIC_SendAck这个函数我上面没给它的实现很简单主机将SDA拉低表示ACK释放SDA表示NACK然后在SCL上打一个时钟脉冲即可。字节顺序也是个隐藏坑。TMP102的数据是MSB在前也就是高字节先发。很多传感器比如某些加速度计是LSB在前写惯了一种容易惯性犯错误。如果读回来的buf[0]和buf[1]顺序搞反了温度值会完全错乱且毫无规律。4.3 配置寄存器位定义与扩展模式配置寄存器是16位很多人在初始化时只写一个0x00就完事实际上里面每个位都有意义。常用的位定义如下位名称功能说明15OS/ALERT兼作单次转换触发与告警标志14~13R1/R0转换速率/分辨率折中12~11F1/F0告警故障队列防止噪声误触发10POLALERT引脚极性0低有效1高有效9TM告警模式0比较模式1中断模式8SD关断模式1进入低功耗4EM扩展模式1开启13位分辨率3~0保留保持默认初始化配置寄存器的代码void TMP102_Init(void) { IIC_Start(); IIC_SendByte(0x90); // 写地址 IIC_SendByte(0x01); // 配置寄存器指针 IIC_SendByte(0x60); // 高字节13位扩展模式 IIC_SendByte(0x00); // 低字节 IIC_Stop(); }0x60换算成二进制是0110 0000对应bit14~13为11。等一下我上面说R1/R0是转换速率折中这里怎么设置为13位扩展模式实际上TMP102的分辨率控制有两个层面R1/R0决定了转换时间与有效位数的折中而EM位bit4决定是否启用13位扩展输出。实际项目里最常见的做法是让R1/R0保持默认00把EM位置1就得到13位、0.03125℃分辨率的输出。上面代码里0x60更多是演示写法我实际项目里一般直接写IIC_SendByte(0x00); // 高字节R1R000默认12位 IIC_SendByte(0x10); // 低字节bit41开启EM扩展模式这样就不会混乱。R1/R0的具体组合不同批次手册描述略有出入非特殊需求保持默认即可。5. 数据换算与实测校准从原始码到实际温度5.1 12位/13位数据格式换算TMP102温度寄存器里的数据最高位是符号位剩余位是温度数值。12位精度下低4位恒为013位扩展模式下低3位恒为0。换算公式是12位模式温度 原始值 × 0.0625 ℃/LSB 13位模式温度 原始值 × 0.03125 ℃/LSBC语言实现时把读到的两个字节拼成int16_t然后右移去掉无效低位直接乘对应系数float TMP102_ConvTemp12(uint8_t msb, uint8_t lsb) { int16_t raw (msb 8) | lsb; raw 4; // 去掉低4位 return raw * 0.0625f; } float TMP102_ConvTemp13(uint8_t msb, uint8_t lsb) { int16_t raw (msb 8) | lsb; raw 3; // 去掉低3位 return raw * 0.03125f; }注意这里的int16_t类型很关键。如果不带符号负温度就全乱了。比如-0.25℃在12位模式下原始码是0xFFFC作为有符号数是-4乘以0.0625得到-0.25。如果当成无符号数65532乘出来就是4095.75℃完全不可用。5.2 负温度与精度处理TMP102支持-40℃~125℃的测量范围负温度的处理就是靠符号位。摄氏温度在0℃以下时温度寄存器的值是负数用补码表示。代码层面只要坚持用int16_t类型乘完系数就是正确的带符号温度不需要额外判断符号位。我之前看到有人写代码时手动判断最高位再取绝对值加负号绕了一大圈结果还容易在边界上出错。直接用有符号数是最简单可靠的做法。5.3 实测校准过程与温度补偿TMP102出厂时已经校准过通常直接读就能用。但如果追求更高精度或者板子布局导致传感器靠近发热源可以自己做一次两点校准。最简单的校准方法把板子放到恒温环境比如用冰水混合物0℃和沸水100℃注意海拔要修正做两个参考点读出TMP102的原始值然后建立一阶线性修正修正值 原始值 × 增益 偏移实际工程中我更推荐用一颗已校准的参考温度计在25℃和60℃两个温度点用恒温槽或者精密温箱记录多组TMP102读数然后做线性拟合。拟合代码不用写在单片机里在电脑上用Excel或Python算好两个系数写死在固件里就行。以下是我在某块板子上的实测数据参考温度℃TMP102读数℃偏差0.00.060.0625.325.1-0.240.039.9-0.160.560.80.380.280.60.4这个偏差基本在规格书标称的±0.5℃以内。如果偏差呈现明显线性趋势比如低温偏正、高温偏负说明增益需要调整如果整体平移那就是偏移量的问题。6. 调试实录我在这块板子上踩过的坑与排查链路6.1 现象一SDA被拉死总线锁死第一次焊好板子上电示波器一看SDA始终是低电平SCL有时钟但总线完全没响应。排查思路是这样的先把除了TMP102之外的所有IIC设备摘掉确认是不是TMP102把SDA拉低了。结果摘掉后SDA还是低基本排除从机问题。再看软件的初始化顺序发现GPIO初始化时先配了SDA开漏输出但那时SCL还没有初始化完成GPIO状态不确定产生了类似起始条件的状态导致总线上某个设备误判进入异常状态。解决方法是修改初始化顺序先把所有相关GPIO配好SDA和SCL都拉高然后再开外设时钟或者配置其它引脚。同时我在起始函数的起始处加了SDA_H()和SCL_H()确保每次打开通信前总线处于空闲状态。6.2 现象二读回来永远是0xFFFF0xFFFF意味着总线上所有读到的位都是1可能的原因有三个设备没上电、地址不对、或者根本不在总线上。排查链路是先查供电万用表量V引脚确认有3.3V。再查地址TMP102默认0x48但A0引脚如果悬空读回地址可能飘忽不定。用IIC扫描函数扫一下0x48~0x4B看看设备在哪个地址出现。如果扫描也扫不到就得上示波器抓波形。抓SDA和SCL看起始条件是否正确、地址字节波形对不对、第九个时钟上有没有ACK的拉低动作。我见过一个板子地址字节是完全正确的但SCL信号幅度只有1.5V导致TMP102不认查了一圈发现是SCL上拉了10k电阻到3.3V但走线太长分布电容太大上升沿变成圆弧电平到不了高电平阈值。换4.7k上拉后问题消失。6.3 现象三温度值偶发跳变这是最折磨人的一种问题。前面我提到项目里配置寄存器被意外改写导致EM位被置1就是这类问题的典型。当EM位被意外改变时数据格式从12位变成13位读出来的温度就会突然跳变但又不是完全错乱因为温度和数值之间仍存在线性关系只是斜率变了。排查这类问题第一步是确认寄存器确实被改了。读回配置寄存器和写入的初始值比较如果bit4从0变成了1说明有代码路径在往配置寄存器里写数据。我们当时查到的原因是某段中断处理函数中一个数组越界写操作恰好命中了IIC发送缓冲区导致配置数据被错误发出。这类问题非常隐蔽光靠物理排查没用需要仔细审查所有可能往总线上发数据的代码路径。6.4 ALERT中断引脚的进阶玩法TMP102的ALERT引脚在SOT563封装中有时没引出来选型时要确认好封装版本可以在温度超限时触发中断而不是让主控一遍遍轮询。配置好THIGH和TLOW寄存器后TMP102会自动比较当前温度与阈值超过范围时ALERT引脚输出有效电平极性由POL位决定。把ALERT接到STM32的外部中断引脚上温度一超限就进中断主控在中断里读取状态、做保护动作。这种事件驱动的方式比轮询省CPU响应也更及时适合做温控保护逻辑。我用类似方法做过电池过温保护效果很好。如果项目里还有其它中断驱动的外设这套思路可以复用。6.5 多设备共存的额外提醒一条IIC总线上如果同时挂了OLED、TMP102和加速度计每个设备的地址要错开上拉电阻取值要照顾到所有设备的输入电容。设备多了之后总线电容变大上升沿变慢这时候可以把上拉从4.7k降到2.2k并且尽量缩短走线。另外不同设备的通信速率可能不同以最慢的为准或者每次通信前重新初始化时序都能避免兼容性问题。最后分享一个调试习惯手边常备一个逻辑分析仪几十块钱的那种就行抓IIC波形时比示波器方便得多。把软件模拟IIC的每个延时调大10倍什么起始、停止、ACK、数据位全都看得清清楚楚。等时序逻辑验证无误再把延时调回正常值跑实际项目。我后来调试新板卡凡是涉及IIC的第一步都是用这个办法确认底层时序再往上写应用代码省下的时间远比学逻辑分析仪的那点成本多。本文还有配套的精品资源点击获取