一、项目概述
本项目以意法半导体官方 BLE_HealthThermometer 例程为起点,把原来的“模拟温度数据”彻底替换为 STM32WBA65RI 芯片内部温度传感器经 ADC 真实转换的温度,并通过标准蓝牙 Health Thermometer 服务(0x1809)对外上报;同时新开发了一套 Windows 上位机(C# / WPF),实现蓝牙设备扫描、连接、通知订阅、测量间隔配置、实时数值与曲线展示,用于室内环境温度监测。
改造后的数据通路上,任何一个环节都是真实可测的:温度传感器 → ADC 采样 → 出厂校准换算 → HTS 特征值 → BLE 通知 → 上位机解析与绘图。项目最终交付两部分:可直接烧录的固件工程,以及一套复制即用、无需安装运行时的自包含上位机程序。
1.1 项目目标
使用芯片内部温度传感器(VSENSE 通道)替代例程中的 RTC 计数器伪造值,输出真实温度。
温度分辨率提升到 0.01 ℃,显示量程限定在 0.00 ℃ 至 100.00 ℃ 并保留两位小数。
保持与 Bluetooth SIG Health Thermometer Profile 的协议兼容,手机或电脑通用客户端均可读取。
提供可独立分发的上位机程序,在其他电脑上无需配置开发环境即可使用。
1.2 系统组成
硬件:NUCLEO-WBA65RI 开发板——板载 STM32WBA65RI,集成 BLE 5.4 射频与内部温度传感器。
固件:STM32CubeWBA 固件包 V1.10.0——基于 BLE_HealthThermometer 例程改造,使用 Keil MDK 编译。
协议:Health Thermometer 服务 0x1809——包含温度测量 0x2A1C、中间温度 0x2A1E、测量间隔 0x2A21。
上位机:蓝牙温度计(C# / WPF)——负责蓝牙扫描、连接、订阅、曲线绘制、断线自动重连、接收计数管理与数据导出。
1.3 数据流
芯片内部温度传感器电压 → ADC 采样得到原始码值 → 结合出厂校准值 TS_CAL1 / TS_CAL2 与实时 VREFINT 电压做定点换算 → 得到 0.01 ℃ 单位的温度 → 写入 HTS 温度测量特征 → 蓝牙 Indicate 通知 → 上位机按协议解析 → 界面数值与曲线。
二、STM32WBA65RI 固件项目2.1 工程基础
固件包:STM32Cube_FW_WBA_V1.10.0,工程位于 Projects/NUCLEO-WBA65RI/Applications/BLE/BLE_HealthThermometer。
开发环境:Keil MDK(ARMCLANG V6.24)与 STM32CubeIDE 双工程并存,共用同一套源码,按需选择其一即可。
板载资源:Cortex-M33 内核,BLE 5.4 协议栈使用 ST 的 WPAN 框架,低功耗管理由 stm32_lpm 与 UTIL_TIMER 提供。
原始例程行为:温度值由 RTC 亚秒寄存器加固定偏移伪造,恒在 0.37 ℃ 至 0.40 ℃ 之间,与真实环境无关。
static int32_t HTS_APP_Read_InternalTemperature ( void )
{
int32_t sample_centideg = 0;
int32_t sum_centideg = 0;
uint8_t index;
if(ADCCTRL_RequestIpState(&HTS_TempRequest_Handle, ADC_ON) != ADCCTRL_OK)
{
LOG_INFO_APP("HTS: ADC activation failed\n");
return HTS_TEMPERATURE_DEFAULT_CENTIDEG;
}
for(index = 0; index < HTS_ADC_SAMPLES_NB; index++)
{
if(ADCCTRL_RequestTemperatureValues(&HTS_TempRequest_Handle, &sample_centideg) != ADCCTRL_OK)
{
LOG_INFO_APP("HTS: ADC temperature read failed\n");
(void)ADCCTRL_RequestIpState(&HTS_TempRequest_Handle, ADC_OFF);
return HTS_TEMPERATURE_DEFAULT_CENTIDEG;
}
sum_centideg += sample_centideg;
}
(void)ADCCTRL_RequestIpState(&HTS_TempRequest_Handle, ADC_OFF);
/* Average of the samples, already expressed in the 0.01 °C unit used by
the Health Thermometer service */
sum_centideg = (sum_centideg + (HTS_ADC_SAMPLES_NB / 2)) / HTS_ADC_SAMPLES_NB;
/* Keep the reported temperature within the 0 - 100 °C range */
if(sum_centideg < HTS_TEMPERATURE_MIN_CENTIDEG)
{
sum_centideg = HTS_TEMPERATURE_MIN_CENTIDEG;
}
else if(sum_centideg > HTS_TEMPERATURE_MAX_CENTIDEG)
{
sum_centideg = HTS_TEMPERATURE_MAX_CENTIDEG;
}
else
{
/* Nothing to do */
}
return sum_centideg;
}2.2 从模拟数据到真实 ADC
原始例程中温度值的来源只有一行代码,读数来自 RTC 而非温度传感器:
/* 改造前:伪造值 */measurement = ((HTS_APP_Read_RTC_SSR_SS()) + 37);改造后,温度统一从 ADC 请求,并由应用层做多次采样平均与量程钳位:/* 改造后:内部温度传感器采样 */ADCCTRL_RequestIpState(HTS_TempRequest_Handle, ADCCTRL_STATE_ON);ADCCTRL_RequestTemperatureValues(HTS_TempRequest_Handle, samples, &value);ADCCTRL_RequestIpState(HTS_TempRequest_Handle, ADCCTRL_STATE_OFF);换算不再使用整摄氏度宏,而是按下面的定点公式完成,保留两位小数且不引入浮点开销:Temp(0.01℃) = CAL1_TEMP + (raw × VDD − 3300mV × CAL1)× (CAL2_TEMP − CAL1_TEMP)÷ (3300mV × (CAL2 − CAL1))其中 CAL1 / CAL2 为芯片出厂写入 OTP 的温度校准码值,VDD 由同一控制器采集的 VREFINT 通道实时反推,因此不同芯片、不同供电电压下都能保持一致精度。2.3 关键改动点
System/Config/ADC_Ctrl/adc_ctrl_conf.c / .h:新增应用专用句柄 HTS_TempRequest_Handle,通道为内部温度传感器,采样时间 814.5 周期,为应用层提供独立的 ADC 请求入口。
Common/WPAN/Modules/adc_ctrl.c / .h:新增 ADCCTRL_RequestTemperatureValues(),把原始码值按校准参数定点换算为 0.01 ℃,解决原接口只有整摄氏度的问题。
STM32_WPAN/App/hts_app.c:删除 RTC 伪造逻辑,改为 4 次采样平均并钳位到 0 ℃ 至 100 ℃,向上位机上报真实温度。
STM32_WPAN/App/hts_app.c:新增 0x2A1C 指示确认流控——跟踪每条 Indicate 的确认状态,连续 3 个测量周期未收到确认即主动断开链路,由上位机重连恢复,避免协议栈 BUSY 后数据流永久停摆。
STM32_WPAN/App/hts_app.c + hts.c:在 ACI_GATT_SERVER_CONFIRMATION 事件中解除流控阻塞;补发缓存改用 HTS_OldIndex 索引,发送失败保留记录下周期重试。
Core/Src/main.c:新增 IWDG 独立看门狗(约 4.1 秒超时),主循环喂狗;若上次为看门狗复位则点亮 LD3 并通过串口打印复位原因,便于现场取证。
Core/Src/main.c:TIM17 以 500 毫秒周期翻转 LED1,作为程序运行的直观指示。
STM32_WPAN/App/hts_app.c:新增 3 分钟自动软复位定时器,到时调用 NVIC_SystemReset() 重启系统,规避长期运行后协议栈或应用层可能出现的不可恢复状态。
Common/WPAN/Modules/adc_ctrl.c:所有 ADC 轮询循环增加超时上限,超时返回错误并回退默认温度,替代原来的 Error_Handler 死循环。
蓝牙温度计(新增目录):C# / WPF 上位机与 dist 发布目录,承担设备扫描、连接、订阅与曲线展示。
说明:原有 ADCCTRL_RequestValues() 接口完全未改动,射频温度校准路径继续使用整数换算,两条路径通过 ADC 控制器的请求计数与互斥机制串行执行,互不影响。
2.4 蓝牙服务与协议
Health Thermometer 服务 0x1809:温度测量主服务,客户端据此识别设备类型。
温度测量特征 0x2A1C:属性为 Indicate,数据包含 Flags、温度值,可选时间戳与测量类型。
中间温度特征 0x2A1E:属性为 Notify,仅在客户端订阅时才采样,用于降低空闲功耗。
测量间隔特征 0x2A21:属性为可写,单位为秒,固件接受任意非零值,便于上位机自定义采样节奏。
温度值按 HTS 规范的 IEEE-11073 SFLOAT 编码,固件内部以 0.01 ℃ 为单位计算,上报时按协议要求拆分为尾数与指数,因此上位机解析后可直接得到两位小数。Flags 字段中同时标记了单位(摄氏度 / 华氏度)、是否携带时间戳、测量类型(本次为体温以外的通用温度类型)。2.5 编译、烧录与验证
Keil 编译结果:0 Error(s),0 Warning(s),Program Size: Code=204632,RO-data=12060,RW-data=14016,ZI-data=29192。
烧录:STM32CubeProgrammer 写入起始地址 0x08000000,校验通过后自动复位。
运行验证:串口 115200 连续输出 HTS_APP_Measurement: 2814、2811 等日志,即 28.14 ℃、28.11 ℃,数值随芯片温度缓慢波动。
对照判据:旧固件日志为 37 至 40(即 0.37 ℃ 至 0.40 ℃),若 ADC 读取失败则固定回退为 2500(25.00 ℃),因此日志出现 2814 可确认走的是真实 ADC 链路。
2.6 长期运行稳定性设计
针对“连续运行约 1000 秒后数据停止更新”这一实测问题,固件从发送路径、系统看护与周期复位三个层面做了加固:
指示确认流控:BLE 协议栈规定上一条 Indicate 未被对端确认前会拒绝新的指示并返回 BUSY。应用层跟踪确认状态,未确认时跳过当次发送;连续 3 个周期未确认或协议栈明确返回 BUSY 时,调用 aci_gap_terminate 主动断链,等待上位机自动重连,避免永久停摆
独立看门狗:IWDG 以约 4.1 秒为超时,主循环每次迭代喂狗;任何环节(协议栈、ADC、调度器)卡住超过该时间都会触发硬件复位,复位后通过复位标志点亮 LD3 并输出日志。
定时软复位:程序运行满 3 分钟由定时器触发 NVIC_SystemReset(),主动重启到干净的初始状态;配合上位机的自动重连,用户几乎无感知。
LED 运行指示:LED1 每 500 毫秒翻转一次,现场一眼即可判断固件是否在正常运行。
ADC 超时保护:ADCCTRL 所有硬件轮询均有循环上限,ADC 异常时返回默认值 25.00 ℃ 而不是死等,杜绝单点故障拖死整个系统。
三、上位机 C# 项目
3.1 技术选型
框架:.NET WPF,目标框架 net11.0-windows10.0.19041.0,直接使用系统 Windows.Devices.Bluetooth(WinRT)提供的 GATT 客户端接口,不依赖任何第三方蓝牙库。
优势:无需驱动与额外安装包,Windows 10 及以上系统原生支持;同一套 API 也能直接用于后续扩展其他 GATT 服务。
3.2 代码结构

HtsClient.cs:蓝牙扫描、连接、服务与特征发现、订阅通知、写入测量间隔、按 HTS 规范解析数据。
MainWindow.xaml:界面布局,包含设备列表、操作区、当前温度显示、曲线绘图区与日志区。
MainWindow.xaml.cs:界面逻辑,负责扫描列表刷新、连接与断开、曲线绘制、日志输出。
TemperatureMonitor.csproj:项目配置与发布参数。
dist/ 发布目录:自包含发布产物(TemperatureMonitor.exe 与 .NET 运行时 dll),整个目录复制到其他电脑即可直接运行。
3.3 主要功能
主要代码如下所示:
private void RedrawChart()
{
ChartCanvas.Children.Clear(); double width = ChartCanvas.ActualWidth; double height = ChartCanvas.ActualHeight; if (width < 80 || height < 60)
{
return; }
const double padLeft = 46; const double padRight = 10; const double padTop = 8; const double padBottom = 18; double plotWidth = width - padLeft - padRight; double plotHeight = height - padTop - padBottom; if (plotWidth <= 0 || plotHeight <= 0)
{
return; }
if (_history.Count == 0)
{
var hint = new TextBlock
{
Text = "等待温度数据…",
Foreground = HitBrush,
FontSize = 12 }; Canvas.SetLeft(hint, padLeft); Canvas.SetTop(hint, padTop + plotHeight / 2);
ChartCanvas.Children.Add(hint); return; }
/* 曲线纵轴固定在 0 - 100 °C,与显示量程一致 */
double min = TemperatureReading.MinCelsius; double max = TemperatureReading.MaxCelsius; var gridBrush = GridBrush; var labelBrush = LabelBrush; for (int i = 0; i <= 3; i++)
{
double y = padTop + plotHeight * i / 3.0;
ChartCanvas.Children.Add(new Line
{
X1 = padLeft,
Y1 = y,
X2 = padLeft + plotWidth,
Y2 = y,
Stroke = gridBrush,
StrokeThickness = 1 }); var label = new TextBlock
{
Text = (max - (max - min) * i / 3.0).ToString("F2"),
Foreground = labelBrush,
FontSize = 10 }; Canvas.SetLeft(label, 8); Canvas.SetTop(label, y - 8);
ChartCanvas.Children.Add(label); }
double step = plotWidth / (MaxHistory - 1); /* 曲线从左往右绘制:第 1 个点固定在左边界 */
double startX = padLeft; var points = new PointCollection(); for (int i = 0; i < _history.Count; i++)
{
double x = startX + step * i; double y = padTop + plotHeight * (max - _history[i]) / (max - min); points.Add(new Point(x, y)); }
ChartCanvas.Children.Add(new Polyline
{
Points = points,
Stroke = LineBrush,
StrokeThickness = 2,
StrokeLineJoin = PenLineJoin.Round }); Point last = points[^1]; var marker = new Ellipse
{
Width = 7,
Height = 7,
Fill = LineBrush }; Canvas.SetLeft(marker, last.X - 3.5); Canvas.SetTop(marker, last.Y - 3.5);
ChartCanvas.Children.Add(marker); }}扫描:列出附近所有广播中的蓝牙设备(名称、地址、信号强度),对同一设备做短时间去重,未命名设备同样显示,便于排查。
连接:连接指定设备后自动发现 0x1809 服务与相关特征,订阅 0x2A1C(Indicate)与 0x2A1E(Notify),并把界面所选测量间隔写入 0x2A21。
测量间隔:下拉框提供 1、2、5、10、30、60 秒预设,也可直接键入任意秒数,解析范围 1 至 600 秒,非法输入自动回退为 2 秒。
显示:当前温度以两位小数大字号显示,范围钳位 0.00 ℃ 至 100.00 ℃;曲线纵轴固定为 0、33.33、66.67、100.00 ℃ 四档。
曲线:保留最近 120 个采样点,第 1 个点固定绘制在绘图区左边界,后续点依次向右延伸,满窗后整体左移,形成左进右出的走势。
日志:扫描、连接、订阅、收到数据、错误等事件均带时间戳实时输出,便于现场定位问题。
接收计数与自动清零:状态区实时显示“已接收 N 次”;累计超过 300 条时自动清空图表、日志与历史缓存并从 1 重新计数,防止长期运行后内存无限膨胀。
数据导出:一键把本次连接收到的全部温度记录导出为 UTF-8 CSV 文件,Excel 可直接打开。
断线自动重连:非手动断开(如下位机软复位、链路丢失)后,每 3 秒自动重连上次设备;超过 1 分钟仍未成功则停止并提示手动连接。用户手动点“断开连接”不会触发重连。
状态颜色提示:状态文字在正常时为灰色、已连接为青色、断开或失败为红色,远距离一眼可辨。
连接重试:连接阶段遇到 Windows 蓝牙栈返回的 AccessDenied / Unreachable 等瞬时错误时,自动按递增间隔重试 4 次,覆盖设备刚上电或上一会话未释放的常见场景。
3.4 发布与运行环境
发布命令使用自包含多文件模式,目标机无需安装 .NET 运行时:
dotnet publish -c Release -r win-x64 --self-contained tru
-o dist产物:蓝牙温度计/dist/ 目录,内含 TemperatureMonitor.exe 及全部 .NET 运行时 dll;使用时整个目录一起拷贝,仅拷贝 exe 无法运行。
运行环境:64 位 Windows 10(19041 及以上)或 Windows 11,需具备蓝牙 LE 适配器并已开启系统蓝牙。
采用多文件自包含而非单文件打包的原因:大体积自解压单文件 exe 容易被 Windows Defender / SmartScreen 误判拦截,出现双击无反应;多文件形态无自解压过程,可直接启动。
四、项目开发要点总结
ADC 资源必须共用同一控制器。WBA 系列的射频温度校准本身就占用 ADC,若应用层自行初始化 ADC 会产生抢占与状态残留;复用 ADCCTRL 的请求句柄与互斥机制,是最省事也最稳妥的做法。
精度瓶颈在换算而不是采样。框架提供的温度宏是整摄氏度运算,直接使用会出现“值恒定不变”的假象;改为读取原始码值并自行做定点运算,才能得到 0.01 ℃ 的分辨率。
量程与精度要双端约束。固件端钳位到 0 ℃ 至 100 ℃ 保证上报值合法,上位机端再次钳位与统一格式化,避免异常值把曲线拉爆。
低功耗靠“按需采样”。中间温度特征只有被订阅时才采样,未订阅时仅维持测量定时器,可避免空闲状态下每秒空转 ADC。
WPF 单文件发布有两个坑:一是 DirectWriteForwarder.dll 需要加 IncludeAllContentForSelfExtract 才能正常启动;二是即便解决了启动问题,几十 MB 的自解压 exe 仍可能被 Windows Defender / SmartScreen 静默拦截。本项目最终改为多文件自包含发布,exe 与运行时 dll 同目录分发,最稳妥。WinRT 蓝牙接口对目标框架有要求。必须把目标框架设为 net*-windows10.0.19041.0 及以上,否则 Windows.Devices.Bluetooth 命名空间不可用;目标机还需开启蓝牙。
曲线方向的本质是坐标原点。若按“右对齐”计算起点,点会从右侧向左长;把起点固定在左边界并采用固定长度滑窗,才是符合直觉的左进右出。
验证数据真伪要有特征值。通过串口日志对比改造前后的量级(37 至 40 对比 28.14,以及失败回退值 2500),可以快速判断是真实采样还是回退或旧固件。
内部传感器测的是芯片结温。它通常比环境温度高 2 ℃ 至 5 ℃,做环境监测时应做一次性偏置校准或加散热设计,而不是直接当作气温使用。
BLE Indicate 必须做确认流控。协议栈在上一条指示未确认时会拒绝后续指示,应用层若只打日志不处理,数据流会在某个随机时刻永久停摆;跟踪确认事件并在超时后主动断链重连,是最小代价的自愈方案。
长跑稳定性要“双保险”。硬件看门狗兜住系统级卡死,应用层定时软复位兜住慢性的状态劣化;再让上位机具备自动重连能力,整条链路才能做到无人值守长期运行。
五、使用说明
烧录固件:用 Keil 打开 MDK-ARM/BLE_HealthThermometer.uvprojx,执行 Rebuild 后下载;或用 STM32CubeProgrammer 写入 MDK-ARM 目录下生成的 hex 文件。
上电后开发板开始广播,设备名以 HT_ 开头,并广播 0x1809 服务。
启动上位机:双击 蓝牙温度计/dist/TemperatureMonitor.exe,程序自动开始扫描。
在设备列表中选择 HT_xx,设置所需测量间隔,点击连接;连接成功后界面开始显示温度数值与曲线,日志区同步记录事件。
运行中若下位机重启或链路中断,上位机会自动重连上次设备(状态区红色显示重连进度);超过 1 分钟未连上会停止重试,此时重新点击连接即可。累计接收超过 300 条时界面数据会自动清零重新计数,属正常设计。
六、致谢
感谢意法半导体(STMicroelectronics)提供 STM32CubeWBA 固件包、NUCLEO-WBA65RI 开发板以及 BLE_HealthThermometer 参考例程。正是这套完整的软硬件平台与清晰的工程结构,让本项目可以在真实产品级代码上完成改造,而不必从零搭建蓝牙协议栈。
特别感谢 EEPW 论坛(电子产品世界)举办的本次蓝牙信号探测活动。活动提供的开发板与交流平台,让“信道探测”这类偏底层的蓝牙技术有了动手实践与公开分享的机会;也感谢活动组织者与工作人员的付出。
我要赚赏金
