尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32热敏电阻温度采集报警:ADC到OLED与串口全链路解析

STM32热敏电阻温度采集报警:ADC到OLED与串口全链路解析 简介这是一套完整的STM32热敏电阻温度监测工程源码面向嵌入式初学者和开发者解决从传感器采集到显示、报警、串口输出的全流程问题。工程共231个文件压缩包约6.33MB文件类型以c和h源码为主包含标准外设库驱动、OLED显示驱动、ADC采集配置以及工程编译文件另有o、crf等中间产物和axf、map等生成文件便于了解编译与链接结构。目前已有843人学习下载。通过学习这一工程可以掌握热敏电阻阻值变化经ADC转换为数字温度的算法学会用SPI或I2C驱动OLED实时显示温度利用GPIO控制蜂鸣器实现超限报警并通过UART把温度数据发送到串口调试助手。源码模块清晰、注释明确适合直接移植到智能家居、工业环境监测或竞赛实训中是一份能动手复现并扩展的实用资料。1. STM32 温度采集报警热敏电阻、OLED、蜂鸣器和串口这条链路从头调把热敏电阻、OLED、蜂鸣器接到 STM32 上硬件连线用不了多少时间真正的复杂度全在数据链路里热敏电阻不能直接怼 ADC 引脚要先搭分压OLED 的 I2C 地址写错一位就是白屏蜂鸣器在阈值附近反复启停比不响更恼人printf 不做重定向的话串口调试助手只会收到空白或乱码。这条链路每一步看起来都有现成例程但拼在一起时采样时间、滞回区间、数据帧格式这些参数才是决定项目能不能稳定跑起来的关键。这篇文章顺着数据流把整条链路拆开从热敏电阻分压采集、B 值换算温度到 OLED 实时显示、蜂鸣器阈值报警最后通过 USART 把温度和原始 ADC 值送到串口调试助手每段都给出 HAL 库代码和参数依据。做毕设或者桌面温度计这类小工具直接对照着把它变成稳定可交付的代码。2. 热敏电阻分压与 ADC 换算从电阻值到浮点温度2.1 分压电路设计热敏电阻不能直连 ADCSTM32 的 ADC 只能测量电压而热敏电阻的物理量是阻值所以第一步是把电阻变化转换成电压变化最常见也最稳妥的做法是串联分压NTC 热敏电阻一端接 3.3V另一端接到 ADC 引脚同时引脚再接一个固定电阻到 GND中间抽头就是 ADC 采样点。NTC 的特点是温度升高阻值下降于是这个分压点的电压会随温度升高而上升方向直观。选型上网上最常见、例程最多的是 25°C 下标称 10kΩ、B 值为 3950 的 NTC固定电阻也配 10kΩ。这个组合的好处是 25°C 时 NTC 阻值恰好等于固定电阻分压点电压处于 3.3V 的中点 1.65V整个量程内可获得相对均衡的灵敏度。分压公式是 V_adc 3.3 × R_fixed / (R_ntc R_fixed)可以用一段 Python 把不同温度下的预期电压先算出来接线之前心里就有数。# 用 B 值方程估算各温度下 NTC 阻值和分压电压 import math R0 10000.0 # 25°C 标称阻值 B 3950.0 # 热敏指数NTC 厂家给出 Rf 10000.0 # 分压固定电阻 VCC 3.3 T0 298.15 # 25°C 对应的开尔文温度 def r_ntc(t): tk t 273.15 return R0 * math.exp(B * (1.0 / tk - 1.0 / T0)) for t in [0, 10, 25, 40, 60, 80]: r r_ntc(t) v VCC * Rf / (r Rf) # NTC 在上、固定电阻在下 print(fT{t:3d}C R{r:8.1f} V_adc{v:.3f}V)这段代码里 R0 是 25°C 时的 NTC 阻值B 是热敏指数决定了阻值随温度变化的曲线形态。运行后可以看到 0°C 时分压约 0.85V25°C 时 1.65V80°C 时约 2.90V整个范围落在 ADC 量程中段不会出现低位溢出。把 NTC 和固定电阻的位置对调电压方向会反转后续换算公式也要跟着改。分压电阻的精度直接影响温度准确性固定电阻建议不要用 5% 的普通色环电阻至少选 1% 金属膜有条件直接上 0.1%。B 值的离散度也要关注同样标 10k 3950 的传感器实际 B 值可能偏差 ±1%在 100°C 附近换算误差会被放大到 2°C 以上。B 值25°C 到 50°C 阻值变化趋势常见应用场景3380下降到约 4.2kΩ家电、空调温度探头3950下降到约 3.6kΩ3D 打印、工业模块、毕设最常见4100下降到约 3.5kΩ汽车电子、高温测点选传感器时确认好 B 值代码里的常量要跟实物一致否则中低温段误差不大高温段偏差会越来越明显。2.2 ADC 采样配置为什么采样时间要拉到 239.5 周期以 STM32F103C8T6 为例ADC1 的通道 0 对应 PA0 引脚。CubeMX 里配置很简单PA0 设为 Analog 模式ADC1 使能分辨率 12 位。真正影响读数质量的是 Sampling Time 这个参数务必选 239.5 Cycles对应代码里的ADC_SAMPLETIME_239CYCLES_5。原因在 NTC 的高内阻特性低温时 NTC 阻值可能到 30kΩ 以上ADC 内部采样电容需要在采样阶段完成充电充电时间常数由源阻抗和内部电容共同决定。采样时间如果只给 1.5 周期电容还没充满就进入转换阶段读到的电压会明显偏低而且温度越低偏低越严重。239.5 周期在 ADC 时钟 12MHz 下约 20µs对这个量级的源阻抗已经足够。这一条直接决定采样数据是否可信是整套代码里最不该省的一步。// ADC1 通道 0(PA0) 单通道采样配置 void ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_DISABLE; // 单通道模式 hadc1.Init.ContinuousConvMode DISABLE; // 每次显式触发转换 hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; // 数据右对齐低 12 位有效 hadc1.Init.NbrOfConversion 1; HAL_ADC_Init(hadc1); sConfig.Channel ADC_CHANNEL_0; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; // 关键长采样时间 HAL_ADC_ConfigChannel(hadc1, sConfig); } uint32_t ADC1_ReadRaw(void) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); // 等待转换完成超时 10ms return HAL_ADC_GetValue(hadc1); }配置里ContinuousConvMode设为 DISABLE 是因为我们想在主循环里按节奏主动采一次而不是让 ADC 自己连续跑这样便于控制采样频率。PollForConversion的超时参数给 10ms 足够正常转换在几十微秒内完成。手动改成了长采样时间后如果温度读数仍然跳就要往硬件连接方向排查而不是继续加采样时间。2.3 用 B 值公式换算还是查表插值ADC 原始值只是 0 到 4095 的整数要先还原成电压再反推 NTC 阻值最后用 B 值公式换算成温度。B 值公式由 NTC 的定义式反解而来R R0 × exp(B × (1/T - 1/T0))其中 T0 是 298.15K对应 25°C。反解后得到 T B × T0 / (T0 × ln(R/R0) B)注意所有温度都要先换算成开尔文最后再减掉 273.15。#include math.h // 由 NTC 阻值换算温度单位 °C float R_to_Temp(float r) { const float B 3950.0f; // NTC 的 B 值 const float R0 10000.0f; // 25°C 标称阻值 const float T0 298.15f; // 25°C 开尔文 float tk B * T0 / (T0 * logf(r / R0) B); return tk - 273.15f; }logf是自然对数需要包含 math.h。在 Keil 工程里建议勾选 Target 选项里的 Use MicroLIB否则浮点 printf 和数学函数的链接体积会明显膨胀F103 这类芯片的 Flash 很快就吃紧。F103 的 Cortex-M3 内核不带硬件 FPU浮点运算由软件完成但一次温度换算只有几十微秒测温场景每秒采十几次完全没有压力。对应到完整链路从 ADC 原始值到温度这样串起来float read_temperature(void) { uint32_t raw ADC1_ReadRaw(); // 0~4095 float v_adc (float)raw / 4095.0f * 3.3f; // 还原采样点电压 float r_ntc 10000.0f * (3.3f - v_adc) / v_adc; // 固定电阻在 GND 侧 return R_to_Temp(r_ntc); }这段代码里3.3f用的是 VDDA 标称值实际开发板 VDDA 不一定是精确的 3.300V这也是系统误差的主要来源之一后面校准时用万用表实测修正。查表插值是另一条路线预先算出 0 到 100°C 每 1°C 对应的 ADC 值或电阻值运行时先二分查找落在哪个区间再做线性插值。查表法不依赖浮点库执行时间确定适合对实时性有要求或者不想引入 log 函数的裸机工程缺点是换一种 NTC 型号就要重新生成整张表。原型验证阶段用 B 值公式更快定型后用查表法更稳两条路线各有适用场景。换算方式代码量典型精度换传感器成本适用阶段B 值公式5 行25°C 附近 0.3~0.5°C高温偏差放大只改 B 值常量快速原型、通用演示查表插值约 50 行取决于表密度可做到 0.1°C重新生成表格和烧录批量定型、传感器型号固定3. 用 HAL 库点亮 OLED、接通蜂鸣器报警3.1 0.96 寸 I2C OLEDHAL 库驱动代码的最小闭环0.96 寸 OLED 有 I2C 和 SPI 两个版本。I2C 版只要 SCL、SDA、VCC、GND 四根线占用两个 IO适合对刷新率要求不高的温度显示SPI 版刷新快但要多接 CS、DC、RESET 三根控制线连线复杂度上了一个台阶。这款项目显示内容只有一行温度和一两个状态字符I2C 版本就够用驱动芯片绝大多数是 SSD1306。接线按 I2C1 的默认引脚走SCL 接 PB6SDA 接 PB7VCC 接 3.3VGND 共地。I2C 地址一般是 0x3C如果模块上 SA0 焊盘被拉高地址会变成 0x3D白屏时优先检查这个。HAL 库的HAL_I2C_Mem_Write需要的是 8 位地址格式也就是把 7 位地址左移一位代码里写0x3C 1这是新手最容易卡住的地方。// SSD1306 写命令与写数据Mem_Write 的第一个字节是控制字节 static void Oled_WriteCmd(uint8_t cmd) { HAL_I2C_Mem_Write(hi2c1, 0x3C 1, 0x00, I2C_MEMADD_SIZE_8BIT, cmd, 1, 10); } static void Oled_WriteData(uint8_t dat) { HAL_I2C_Mem_Write(hi2c1, 0x3C 1, 0x40, I2C_MEMADD_SIZE_8BIT, dat, 1, 10); } // 初始化序列核心是打开 charge pump 并退出睡眠 void Oled_Init(void) { const uint8_t init[] { 0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40, 0x8D, 0x14, 0x20, 0x00, 0xA1, 0xC8, 0xDA, 0x12, 0x81, 0xCF, 0xD9, 0xF1, 0xDB, 0x40, 0xA4, 0xA6, 0xAF }; for (uint8_t i 0; i sizeof(init); i) Oled_WriteCmd(init[i]); }初始化序列里0x8D 0x14是打开内部电荷泵SSD1306 没有这一步屏幕不会亮0xA8 0x3F设置驱动路数为 64 行0x20 0x00选水平寻址模式0xAF最后打开显示。很多 HAL 库驱动 OLED 代码把这一整段封装成OLED_Init()出错概率也集中在这里。显示温度时OLED 是 128x64 点阵显存共 1024 字节每 8 个点纵向组成一个字节。显示字符串就是先把字符字模按列拷贝进显存缓冲区然后整帧刷到屏幕上。为做查询方便一般把字模按 6x8 点阵存放调用时逐字节拷贝// 在指定页(每页 8 行像素)显示字符串随后整帧刷新 void Oled_ShowString(uint8_t page, uint8_t col, char *s) { extern uint8_t frame[1024]; // 显存缓冲区 while (*s) { for (uint8_t i 0; i 6; i) // 从 6x8 字模表取第 col 个字模字节写入对应位置 frame[page * 128 col * 6 i] font6x8[(*s - 32) * 6 i]; s; col; } // 刷新整帧 Oled_WriteCmd(0x21); Oled_WriteCmd(0); Oled_WriteCmd(127); Oled_WriteCmd(0x22); Oled_WriteCmd(0); Oled_WriteCmd(7); for (uint16_t i 0; i 1024; i) { uint8_t b frame[i]; HAL_I2C_Mem_Write(hi2c1, 0x3C 1, 0x40, I2C_MEMADD_SIZE_8BIT, b, 1, 10); } }主循环里一行代码完成温度显示显示刷新频率保持在 10Hz 左右就够了人眼不会感到迟滞同时避免 I2C 刷屏占用太多总线时间。3.2 蜂鸣器驱动电路有源和无源蜂鸣器的接法差异蜂鸣器按有无内部振荡源分成有源和无源两类。有源蜂鸣器内部自带振荡电路通电即响无源蜂鸣器需要外部给 2 到 4kHz 的方波才会发声。报警场景建议优先无源蜂鸣器因为它的音调和节奏完全由程序控制能做出「短促报警」和「持续报警」的区分有源蜂鸣器只能以固定频率一直响报警辨识度差一些。常用 STM32 开发板自带的蜂鸣器模块通常已经集成三极管驱动但如果外接裸蜂鸣器就不能直接用 GPIO 去推STM32 引脚输出电流有限带不动蜂鸣器工作电流必须加一级三极管放大。典型接法是用 NPN 三极管 S8050GPIO 通过 1kΩ 基极限流电阻接到基极蜂鸣器串联在电源和集电极之间发射极直接接地。连接节点去向说明PB11kΩ → S8050 基极限流保护 GPIO 和三极管S8050 发射极GND低侧驱动蜂鸣器正极5V 或 3.3V按蜂鸣器标称电压选择蜂鸣器负极S8050 集电极三极管导通后形成回路续流二极管反向并联蜂鸣器两端1N4148吸收反向电动势GPIO 输出高电平时三极管导通蜂鸣器通电发声这是最常见的低侧驱动电路。无源蜂鸣器是感性负载断开瞬间会产生反电动势不加续流二极管容易在集电极产生高压尖峰时间长了可能击穿三极管。无源蜂鸣器的报警逻辑可以做成非阻塞翻转方波用HAL_GetTick()做时间基准每 200ms 翻转一次电平形成「响 200ms、停 200ms」的节奏// 无源蜂鸣器驱动按 200ms 节奏断续报警 void Buzzer_Update(uint8_t alarm, uint32_t now) { static uint32_t last_toggle 0; static uint8_t state 0; if (!alarm) { HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_RESET); state 0; return; } if (now - last_toggle 200) { // 200ms 翻转一次形成断续音 last_toggle now; state ^ 1; HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, state ? GPIO_PIN_SET : GPIO_PIN_RESET); } }这段函数不给方波频率给的是报警节奏。直接用HAL_Delay(200)也能实现类似效果但主循环里还要刷 OLED、发串口阻塞延时会让屏幕刷新和串口输出都出现卡顿所以非阻塞方式才是这个场景的正解。3.3 报警逻辑滞回阈值与非阻塞鸣叫配合报警阈值常见的写法是温度超过 40°C 就报警、低于 40°C 就关闭但这种单阈值逻辑有个明显的坑温度在 39.8 到 40.2 之间波动时蜂鸣器会反复开启和关闭听起来像是接触不良。解决办法是加滞回区间报警触发点和释放点分开设本项目用触发 40°C、释放 37°C 的区间做 3°C 滞回。#define TEMP_ALARM_ON 40.0f // 进入报警阈值 #define TEMP_ALARM_OFF 37.0f // 退出报警阈值略低于触发点 uint8_t Buzzer_Check(float temp) { static uint8_t alarm 0; if (temp TEMP_ALARM_ON) alarm 1; // 达到触发点进入报警 if (temp TEMP_ALARM_OFF) alarm 0; // 降到释放点才解除 return alarm; }static变量保存上一次的报警状态函数被反复调用时才能记住当前是不是已经处于报警中。滞回带的宽度设置要考虑传感器噪声一般噪声不超过 1°C3°C 留足了余量同时不会让报警反应显得迟钝。这个状态机还能继续扩展比如把报警状态机从二态改成「正常-预警-报警」三态OLED 上分别显示不同提示串口输出里也多一个状态字段为后续上位机联动留下空间。4. 串口发送到调试助手printf 重定向与数据帧设计4.1 printf 重定向让 USART1 变成调试助手能读的输出口CubeMX 里把 USART1 配置为异步模式波特率 115200数据位 8停止位 1无校验对应 PA9 的 TX 和 PA10 的 RX。硬件连接上如果调试助手这边用的是 USB 转 TTL 模块STM32 的 TX 接模块的 RXRX 接模块的 TX同时两边必须共地否则串口收数据会断断续续。标准 C 库的 printf 默认输出到 stdout而 stdout 在裸机工程里没有实现所以要把输出重定向到 USART1。HAL 库里只需要重写 fputc 这一个函数#include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 100); return ch; }fputc收到一个字符就通过 HAL_UART_Transmit 发一个字符超时时间给 100ms 足够。重定向完成之后printf 的所有输出都会走 USART1串口调试助手直接就能收到文字。要注意 Keil 工程里必须勾选 Target 选项中的 Use MicroLIB否则完整的 C 标准库 printf 会把浮点格式化函数的体积带大在 F103 上容易遇到 Flash 不足而且 MicroLIB 的 printf 才支持%.1f这类浮点格式。如果不打算引入 printf也可以直接用 HAL_UART_Transmit 发送格式化后的字符串效果一样char buf[32]; int n sprintf(buf, temp%.1f\r\n, temp); HAL_UART_Transmit(huart1, (uint8_t *)buf, n, 100);这种方式不依赖标准输出重定向逻辑更加裸露直观。另外如果用的是 ST-Link 自带的虚拟串口Windows 驱动没有正确安装时设备管理器里会出现黄色感叹号串口助手也打不开对应端口先解决驱动再查程序。4.2 数据帧格式选择文本、逗号分隔和 JSON 各有什么适用场景数据到了串口调试助手这一步格式决定了后面连接的方便程度。只是人眼观察纯文本就够以后要接上位机或者服务端就需要结构化一些的帧格式。格式示例优点适用场景纯文本T25.6C ALARM0人眼直接读排错直观临时调试逗号分隔25.6,2048,0短小Excel 或脚本易分割简单记录、采集JSON{temp:25.6,raw:2048,alarm:0}自描述上位机解析不依赖字段位置对接上位机、服务器人眼观察用纯文本最直接SSCOM、XCOM 这类常用串口调试助手直接以 ASCII 显示字段一清二楚。逗号分隔格式最省带宽适合长时间记录后导入表格工具处理。JSON 帧虽然长一点但每个字段自带语义上位机用现成的 JSON 库解析即可以后增加风速、湿度等字段也不需要改协议。推荐在主循环里按固定周期发送 JSON 帧字段包含温度、原始 ADC 值和报警状态一次把传感器信息带全printf({\temp\:%.1f,\raw\:%lu,\alarm\:%u}\r\n, temp, raw, alarm);注意字符串里的双引号要用反斜杠转义帧尾必须是\r\n串口调试助手靠回车换行来区分一帧的结束不加的话所有数据会粘连在一行里后续做数据回放时很难拆分。发送频率控制在 5 到 10Hz 即可115200 波特率下每秒发送的数据量远低于信道容量频繁发送只会增加上位机解析压力。4.3 边界检测与完整主循环别让传感器故障伪装成温度ADC 满量程值 4095 或者接近 0 的值按温度公式算出来会是极端温度但实际对应的是接线故障。回顾分压结构NTC 开路时抽头被固定电阻拉到 GNDADC 读到接近 0固定电阻短路时抽头接近 VCCADC 读到接近 4095。正常测温范围内 ADC 值不会走到这两个极端因此可以做传感器健康检查放在温度换算之前。// 传感器异常判断0 表示异常1 表示正常 uint8_t temp_sensor_check(uint32_t raw) { if (raw 100 || raw 4050) return 0; return 1; }阈值留了 100 和 4050 的余量避免把低温高压的场景误判为故障。异常时就不该继续走温度换算流程OLED 显示错误提示串口输出传感器错误让调试的人立刻知道是接线而不是温度。完整主循环把这一整套逻辑串起来OLED 显示和蜂鸣器报警各司其职while (1) { uint32_t raw ADC1_ReadRaw(); if (!temp_sensor_check(raw)) { Oled_ShowString(0, 0, SENSOR ERR); printf(sensor_error\r\n); } else { float temp read_temperature(); char line[16]; sprintf(line, T%.1fC, temp); Oled_ShowString(0, 0, line); uint8_t alarm Buzzer_Check(temp); Buzzer_Update(alarm, HAL_GetTick()); printf({\temp\:%.1f,\raw\:%lu,\alarm\:%u}\r\n, temp, raw, alarm); } HAL_Delay(100); }主循环里HAL_Delay(100)控制整体节奏为 10Hz每次循环采集一次 ADC、更新一次 OLED、发送一次串口帧。温度采集不属于高速场景10Hz 足够让串口助手里看到平滑变化曲线还能给 I2C 刷屏和 printf 留出充足时间。5. 稳定输出温度滤波、冰水校准与故障巡检5.1 滑动平均窗口取多大才合适ADC 单次采样的随机噪声通常有几到十几个 LSB直接送进温度公式会让 OLED 上的小数位来回跳动。常见做法是滑动平均连续采 8 次取平均窗口更新方式比简单累加平均更省内存。窗口太大会让温度响应变慢报警触发也随之延迟8 次在 10Hz 采样下引入约 800ms 延迟用在阈值判断上完全能接受。#define WINDOW_SIZE 8 uint32_t adc_buf[WINDOW_SIZE]; uint8_t adc_idx 0; uint32_t adc_sum 0; // 每次采集后把新值滑入窗口 adc_sum - adc_buf[adc_idx]; // 移除最旧的值 adc_buf[adc_idx] raw; // 写入新值 adc_sum adc_buf[adc_idx]; // 累加 adc_idx (adc_idx 1) % WINDOW_SIZE; raw adc_sum / WINDOW_SIZE; // 平滑后的 raw把这段逻辑放在temp_sensor_check之前还是之后有讲究推荐先做异常判断再做平滑否则一次故障采样的极端值会把窗口内的平均值拉偏。注意这里用的是整数运算避免浮点除法引入额外的 CPU 开销。ADC 值平滑之后再做温度换算高压侧和低压侧的噪声都会被压低串口助手里看到的曲线就干净很多。5.2 冰水校准和电源基准修正整条链路跑通后第一件要做的事是验证温度准不准冰水混合物是最便宜的标准温度源。冰块加少量水在标准大气压下稳定在 0°C 附近把 NTC 探头完全浸入等 30 秒热平衡后看串口调试助手的读数。如果显示 1 到 2°C 的偏差优先检查两个源头。第一个是固定电阻精度10kΩ 的 5% 电阻实际阻值可能落在 9.5 到 10.5kΩ 之间直接导致分压曲线系统性偏移。把固定电阻换成 0.1% 金属膜电阻这类偏差基本消失。第二个是 VDDA 实际电压代码里3.3f是标称值开发板上的 3.3V 经过 LDO 后通常有 1% 到 2% 的误差。用万用表实测 VDDA 对 GND 的电压例如实际为 3.28V把换算代码里的常量替换成实测值误差能显著收敛。再用一个高温点验证曲线形态手心温度约 33°C 到 35°C握紧 NTC 等读数稳定观察从 0°C 到体温区间内的测量线性度。如果中间段偏差仍然超过 0.5°C多半是 B 值不匹配需要更换 NTC 型号或者调整 B 值常量。串口帧里的 raw 字段在调试期间不要省掉它能帮助区分是传感器问题还是换算问题。标完冰水 0°C 和手心温度两个点把 3.3f 常量按实测电压替换这条链路的误差通常就能收进 ±0.5°COLED 上的温度值和串口助手里的曲线就都能交付了。本文还有配套的精品资源点击获取
返回列表