ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32热敏电阻测温实战:分压采样、OLED显示与串口报警设计

STM32热敏电阻测温实战:分压采样、OLED显示与串口报警设计 简介基于STM32与热敏电阻的温控监测方案源码包面向嵌入式初学者及STM32开发者解决温度采集、OLED显示、超限报警及串口数据上报的综合实践需求。资源共231个文件以C语言源文件、头文件、工程配置文件为主同时包含编译输出文件如.o、.axf、.map等压缩包大小6.33MB覆盖ADC采样、I2C/SPI驱动OLED、GPIO控制蜂鸣器、UART串口通信等关键模块。已有843人学习下载适合入门到进阶的嵌入式项目参考。通过阅读源代码可掌握外设初始化、中断处理、传感器数据转换与多任务调度思路并可直接基于现有工程修改阈值与显示界面快速搭建自己的温度监测原型。1. STM32 热敏电阻测温从 NTC 分压采样到串口报警的系统拆解当时要做一个冷链箱温度监控的原型需求并不高一个温度显示、一个超温报警、一个上位机窗口能看到实时温度。我直接用 STM32F103C8T6 作为主控外扩一颗 10k NTC 热敏电阻、0.96 寸 I2C 接口 OLED 和有源蜂鸣器就搭出了完整链路。NTC 阻值变化通过固定电阻分压进入 ADC温度结果同时送往 OLED 和串口调试助手超过阈值后 GPIO 拉高驱动蜂鸣器。对刚接触嵌入式的工程师来说这个项目覆盖了 ADC 采样、I2C 显示和 UART 通信三个常用外设对有经验的开发者来说可以借此对比不同 NTC 拟合算法和报警判断状态机的取舍。2. NTC 分压电路与 STM32 外设选型ADC 通道、OLED I2C 与蜂鸣器 GPIO2.1 NTC 热敏电阻的分压采样原理负温度系数热敏电阻的阻值会随温度升高而下降但 STM32 的 ADC 只能识别电压不能直接识别电阻。常见做法是把 NTC 和一个固定电阻串联组成分压电路ADC 采集两个电阻中间的连接点。以 10k NTC、10k 固定电阻、3.3V 供电为例25℃ 时 NTC 阻值接近 10kΩ分压点电压约 1.65V温度升高后 NTC 阻值降低分压点电压也随之下降。工程里一般把 NTC 放在上端、固定电阻放在下端好处是接地端始终是固定电阻NTC 引线不管怎么接采集电压都保持在 0VCC 区间内。需要明确的是NTC 本身是非线性元件不能按线性关系去推算温度线缆较长时还要考虑接触电阻和导线压降。对普通室内测温场景固定电阻选 1% 精度即可功率 0.125W 以上温度漂移控制在 50ppm 内避免固定电阻自身受热后影响分压比例。下面记录这个项目最关键的分压参数参数取值说明VCC3.3VSTM32 ADC 参考电压NTC 额定阻值10kΩ 25℃常用 B3950固定电阻 R10kΩ分压桥臂精度 1%ADC 分辨率12bit量程 0~4095采样通道ADC1_IN0PA0 引脚固定电阻取值决定了 ADC 在目标温度区间内的分辨率。测温范围若是 20~50℃10kΩ 电阻能让电压变化分布在这个区间中段ADC 转换结果不会过早饱和也不会在小信号区间损失精度。2.2 OLED 屏幕的 I2C 接口与地址选择项目里的 OLED 采用 I2C 驱动常见的 0.96 寸 SSD1306 模块默认 I2C 地址是 0x3C少数厂商会做成 0x3D。模块的 SCL、SDA 分别接到 STM32 的 I2C1 引脚典型连接是 PB6、PB7。I2C 总线一般需要外接 4.7kΩ 上拉电阻到 3.3V如果模块 PCB 上已经带上拉可以省掉。项目文件里同时出现 OLED.c 和 stm32f10x_i2c.c说明驱动是基于标准外设库的 I2C 实现而不是用 GPIO 模拟时序。引脚分配建议如下功能STM32 引脚外设OLED SCLPB6I2C1_SCLOLED SDAPB7I2C1_SDA蜂鸣器控制PA1GPIO 推挽输出NTC 分压点PA0ADC1_IN0UART1 TXPA9USART1_TXUART1 RXPA10USART1_RXOLED 的 I2C 速率不要设得太高SSD1306 手册虽然支持 400kHz但驱动线较长时建议降到 100kHz。尤其在使用杜邦线连接开发板和 OLED 模块时线间电容和干扰容易导致 I2C 应答超时现象是初始化阶段屏幕无显示或偶尔花屏。2.3 蜂鸣器驱动电路与有源/无源选择蜂鸣器报警部分的代码实现很直接但电路设计有两个容易忽略的点第一开发板引脚直接接蜂鸣器电流会超过 GPIO 输出能力第二无源蜂鸣器需要 PWM 信号才能发声有源蜂鸣器只需高电平。这个项目的功能描述只写了“控制蜂鸣器发出声音”说明大概率使用有源蜂鸣器GPIO 输出高电平即可。实际项目里我会加一只 NPN 三极管比如 S8050GPIO 通过 1kΩ 电阻接到基极集电极接蜂鸣器负端蜂鸣器正端接 3.3V并在蜂鸣器两端反并联一个二极管吸收关断瞬间的感应电动势。初始化代码如下GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_2MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_1); // 蜂鸣器报警 GPIO_ResetBits(GPIOA, GPIO_Pin_1); // 停止报警这段代码把 PA1 配置为推挽输出速度为 2MHz。蜂鸣器属于低频器件2MHz 足够还能减少 EMI。后面的GPIO_SetBits和GPIO_ResetBits分别拉高、拉低控制电平。如果蜂鸣器接在三极管的集电极高电平开启就是正逻辑代码里的“报警”和“停止”语义保持一致。2.4 整体连接与供电清单接线时要注意OLED、蜂鸣器和 NTC 分压电路共用 3.3V 电源因为 ADC 参考电压也取自 3.3V电源波动会直接影响测温值。若系统由 USB 串口模块供电我会在电源入口并联 100μF 电解电容和 0.1μF 陶瓷电容让 OLED 刷新时的瞬时电流变化不影响 ADC 采样结果。四条链路接到同一块主控之后可以先不做软件联调用万用表测量 NTC 分压点电压常温下应在 1.5~1.8V 之间。如果电压为 0V 或 3.3V说明 NTC 或固定电阻接触不良后面程序写得再好也读不到有效结果。硬件连接确认无误后再进入代码实现阶段寄存器配置将基于这套引脚定义展开。3. 核心代码实现ADC 采集、热敏电阻温度换算与 OLED 字符串刷新3.1 ADC 初始化与单通道采样配置在标准外设库工程里ADC 操作集中在stm32f10x_adc.c。使用 ADC1 的通道 0 时需要先把 PA0 设置为模拟输入并打开 ADC1 时钟。这里用连续转换模式加轮询读取代码简单且足够满足每秒采样一次的需求如果后续要同时采样多路传感器可以改成扫描模式加 DMA但项目文件里没有出现 DMA 文件说明当前方案不需要。void ADC_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode ENABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_Cmd(ADC1, ENABLE); }ADC_DataAlign_Right表示 12 位数据右对齐读到的值范围是 0~4095换算电压时用 4095 而不是 4096。ADC_SampleTime_239Cycles5是可选采样时间中最长的一档NTC 分压点内阻不低长采样时间可以降低阻抗匹配带来的误差。连续转换模式下ADC 会不停刷新结果寄存器主循环直接使用ADC_GetConversionValue(ADC1)取得最新值即可。3.2 NTC 温度换算从 ADC 值到摄氏温度NTC 温度计算有两种主流方式第一种是查表法把温度点和对应 ADC 值预先算好运行时二分查找第二种是 B 值公式法计算量稍大但点连续。项目使用 10k NTC 和 B3950B 值公式已经能覆盖 0~85℃ 的常规范围误差在 0.5℃ 左右。要求更高时可以用 Steinhart-Hart 方程需要厂家提供的 A、B、C 系数算法结构类似。下面是我常用的 B 值换算函数#include math.h #define VREF 3.3f #define ADC_RES 4095.0f #define R_FIXED 10000.0f #define R25 10000.0f #define B_CONST 3950.0f #define T0 298.15f float NTC_GetTemperatureC(uint16_t adc_value) { float voltage (float)adc_value / ADC_RES * VREF; float r_ntc (VREF - voltage) * R_FIXED / voltage; // NTC 在上端 float kelvin 1.0f / ( 1.0f / T0 logf(r_ntc / R25) / B_CONST ); return kelvin - 273.15f; }逻辑说明先用 ADC 原始值得到分压点电压voltage再根据电路结构反推r_ntc。这里的R_FIXED是 10kΩ 下端电阻r_ntc是上端 NTC 阻值。B 值方程标准写法是1/T 1/T0 (1/B) * ln(R/R25)T0取 298.15K最后减 273.15 得到摄氏温度。如果计算结果为负数或超过 100优先检查r_ntc是否卡在 0 或无穷大通常对应分压点短路或断路。需要注意logf依赖浮点数学库Keil 工程里要勾选 MicroLIB否则浮点计算和printf会明显增大代码体积GCC 工具链下则需要链接时加上-lm参数。3.3 OLED 显示刷新与字符串格式化OLED 驱动在OLED.c中一般提供OLED_Init、OLED_Clear、OLED_ShowString、OLED_ShowNum等函数。显示温度只需要把浮点数拆成整数部分和小数部分再按字符坐标输出。0.96 寸 128x64 屏如果使用 16x16 字体一屏能放 4 行推荐第一行显示温度第二行显示报警状态。extern void OLED_ShowString(uint8_t x, uint8_t y, char *str, uint8_t size); extern void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size); char line[16]; float temp_c NTC_GetTemperatureC(ADC_GetConversionValue(ADC1)); int int_part (int)temp_c; int dec_part (int)((temp_c - int_part) * 10); if (dec_part 0) dec_part -dec_part; sprintf(line, %d.%d C, int_part, dec_part); OLED_ShowString(16, 2, line, 16); OLED_ShowString(16, 4, temp_c 35.0f ? ALARM! : NORMAL, 16);OLED 是慢速外设I2C 一帧最多写几十字节刷新一屏内容需要数毫秒。如果放在定时器中断里频繁调用会拖慢主循环。常见做法是把显示函数放进while(1)主循环用 200ms 延时或软件标志位控制更新频率既能看到温度变化又不会影响串口发送。OLED 的 I2C 地址若为 0x3D需要在驱动文件里修改地址宏否则初始化时屏幕没有反应但 I2C 总线上不一定报错。3.4 UART 串口发送与调试助手对接串口部分对应stm32f10x_usart.c时钟初始化在stm32f10x_rcc.c中完成。使用 USART1 时TX 和 RX 分别映射到 PA9、PA10波特率设 115200。PC 端连接使用 USB 转 TTL 模块常见的是 CH340 或 FTDI 芯片。初始化后串口调试助手需要按下面的参数配置参数值波特率115200数据位8奇偶校验无停止位1流控制无发送字符串用sprintf组织好之后再逐字节写入 USART 数据寄存器。注意不能直接连续调用USART_SendData否则会丢字节需要检查发送数据寄存器空标志和发送完成标志。void UART_SendString(USART_TypeDef *USARTx, char *str) { while (*str) { while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) RESET); USART_SendData(USARTx, (uint8_t)*str); str; } while (USART_GetFlagStatus(USARTx, USART_FLAG_TC) RESET); }USART_FLAG_TXE表示数据寄存器可以写入新字节USART_FLAG_TC表示当前字节已经全部发送出去。前者保证不丢数据后者保证最后一字节不会在关闭串口时被截断。PC 端收到TEMP:xx.x后串口调试助手会按波特率实时刷新显示方便记录和观察温度曲线。4. 阈值报警状态机与串口调试助手数据协议设计4.1 阈值判定与滞回比较温度报警不能只看瞬间值否则在 35℃ 附近来回抖动时蜂鸣器会反复启停。常规做法是设置两个阈值动作温度 35℃释放温度 32℃。温度达到 35℃ 后进入报警状态下降到 32℃ 以下才解除两者之间形成 3℃ 的滞回区间。这个状态机逻辑同样适用于风扇控制、加热器保护和冷链箱温度告警。typedef enum { STATE_WAIT_TEMPERATURE_RISE, STATE_WAIT_TEMPERATURE_DROP } AlertState; static AlertState alert_state STATE_WAIT_TEMPERATURE_RISE; static uint8_t alert_flag 0; void Alert_Update(float temper) { switch (alert_state) { case STATE_WAIT_TEMPERATURE_RISE: if (temper 35.0f) { alert_flag 1; GPIO_SetBits(GPIOA, GPIO_Pin_1); alert_state STATE_WAIT_TEMPERATURE_DROP; } break; case STATE_WAIT_TEMPERATURE_DROP: if (temper 32.0f) { alert_flag 0; GPIO_ResetBits(GPIOA, GPIO_Pin_1); alert_state STATE_WAIT_TEMPERATURE_RISE; } break; } }状态迁移逻辑先处于等待升温状态温度超过 35℃ 后拉高 GPIO 并切换到等待降温状态。此后只有温度掉到 32℃ 以下状态机才会回到等待升温状态。这样的写法比散落的 if 嵌套更清晰也方便后续加入报警延时时间、重复报警次数等复杂条件。4.2 串口数据帧格式设计与解析建议调试助手接收原始TEMP:28.5字符串也能工作但真实项目里更推荐定义帧头、数据和校验字段。例如温度帧采用逗号分隔$TMP,28.5,1*5A1表示报警状态5A是异或校验。上位机解析时能识别传感器掉线、主控重启和真实温度跳变校验也能避免串口干扰导致错误数据进入统计报表。发送端代码uint8_t CheckSum(const char *data, uint16_t len) { uint8_t sum 0; for (uint16_t i 0; i len; i) sum ^ (uint8_t)data[i]; return sum; } void SendTemperatureFrame(float temper, uint8_t alarm_state) { char frame[32]; uint8_t len (uint8_t)sprintf(frame, $TMP,%.1f,%d*, temper, alarm_state); uint8_t crc CheckSum(frame, len); sprintf(frame len, %02X\r\n, crc); UART_SendString(USART1, frame); }异或校验把$TMP,28.5,0这段字符逐字节异或结果以两位十六进制形式放在*后面。解析端收到完整一行后先找到*号再解析*后的两位校验值与相同内容重复计算的结果比对。%.1f把温度保留一位小数帧长不会超过 32 字节缓冲区不会溢出。排查串口问题时可以参考下面的对应关系现象可能原因排查位置无任何数据显示串口未打开、CH340 未驱动、TX/RX 接反设备管理器、TTL 电平顺序显示乱码波特率不一致、晶振频率配置错误核对 115200、确认 8MHz 晶振温度固定 0.0NTC 分压点接地了查 ADC 通道和电阻虚焊温度固定 99.9NTC 开路分压电压接近 3.3V查 NTC 两端引线4.3 主循环调度与采样周期控制项目文件里出现了stm32f10x_tim.c说明工程里多半用定时器或 SysTick 产生时间基准。NTC 是慢变信号1 秒采样一次足够OLED 200ms 刷新一次能保证视觉连续报警状态机则放在温度更新后执行。我一般用 SysTick 产生 1ms tick再扩展出两个软件标志位volatile uint8_t time_1s_flag 0; volatile uint8_t time_200ms_flag 0; void SysTick_Handler(void) { static uint32_t tick 0; tick; if (tick % 200 0) time_200ms_flag 1; if (tick % 1000 0) time_1s_flag 1; }SysTick 中断里每 1000ms 置位一次time_1s_flag主循环检测到标志位后执行一轮采样、显示、报警和串口发送。使用标志位而不是在中断里直接调用外设函数可以避免 I2C 和串口在中断上下文中阻塞太久降低数据竞争风险。如果改用定时器 TIM2 或 TIM4思路完全一致只需要把中断回调入口改到对应的定时器中断服务函数。5. 现场调参排错ADC 噪声、OLED 不亮和串口乱码的定位方法5.1 OLED 初始化失败排查顺序OLED 不亮时先检查模块供电和 I2C 地址再用逻辑分析仪观察 SCL、SDA 波形。如果始终看不到时钟翻转说明初始化函数没有执行到通常是 RCC 里没有开启 I2C1 时钟。网上不少教程用 STM32 HAL 库点亮 OLEDAPI 名是HAL_I2C_Mem_Write本项目里则是标准外设库的I2C_SendData和I2C_GenerateSTART排查思路一致只是函数入口不同。部分 OLED 模块的 0x3C 地址可以通过背面电阻切换地址不对时屏幕不会亮总线时序上也只能看到 NACK。5.2 ADC 数据抖动的软件滤波ADC 原始值常有 ±20 以内的波动对应温度约 0.1~0.3℃。用滑动平均可以压到 ±5代码实现非常简单#define ADC_FILTER_N 8 uint16_t ADC_GetFilteredValue(void) { uint32_t sum 0; for (uint8_t i 0; i ADC_FILTER_N; i) sum ADC_GetConversionValue(ADC1); return (uint16_t)(sum / ADC_FILTER_N); }取 8 次采样求平均是成本和效果的折中。把ADC_FILTER_N加到 32温度曲线会更平滑但更新周期变长。对电机干扰明显的工业环境在 NTC 分压点并联一个 1μF 陶瓷电容再配合软件滤波效果会比单纯增大滤波次数更明显。5.3 串口乱码与 CH340 驱动匹配串口调试助手显示乱码时先确认两边波特率都是 115200再看开发板晶振。很多 STM32 最小系统板用 8MHz 晶振但也有部分板子焊的是 12MHz 晶振系统初始化如果按 8MHz 配置实际波特率会偏离PC 端就出现乱码。解决办法是在system_stm32f10x.c中把外部高速晶振频率改为 12MHz或者直接换用 8MHz 晶振。PC 端方面CH340 和 FTDI 串口驱动都比较常见按目标板对应芯片安装驱动不要同时安装多个版本否则设备管理器可能出现感叹号。5.4 NTC 热敏电阻对照表与 B 值偏差最后回到热敏电阻本身。如果只从元件盒里随意拿一颗标称 10k 的 NTCB 值可能落在 3435 到 3950 之间在 100℃ 时同样 ADC 值测出的温度差会超过 3℃。更可靠的做法是用万用表实测 25℃ 时阻值再核对供应商提供的 NTC 热敏电阻对照表确认该阻值对应温度是否准确。对项目里 35℃ 报警阈值来说B3950 的 10k NTC 是性价比最高的选择如果要做高精度仪表级测温可以换成 100k NTC 配高精度电阻并用恒流源代替简单分压。最后在校准环节上0℃ 和 50℃ 恒温槽各采一个点做两点补偿就能把系统误差控制在满足工程需要的范围内。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进