折腾小米二代温度湿度计

笔者有一款小米二代温度湿度计,从后盖能看到编号为:LYWSD03MMC。拆解后的主 板和屏幕如下图:

mi-thermometer-board-and-lcd.jpg

Figure 1: 小米二代温度湿度计里的LCD屏幕和主板

主板一面主要是芯片和元件,另一面主要是测试点:

mi-thermometer-board-chips-side.jpg

Figure 2: 主板主要是芯片和元件的一面

mi-thermometer-board-test-points-side.jpg

Figure 3: 主板主要是测试点的一面

U3为主控芯片,芯片上的丝印为:TLSR8251F512ET24 。在Telink官网能找到该芯 片的规格书。在规格书2.1.1提到:

Both register and SRAM address can be accessed (read or write) via debugging interface (SWS/SWM, SPI/I2C/USB interface).

这其中的SWS/SWM吸引了我的注意。通过规格书1.6.3配合7.1.1.1能看出来芯片的 Pin 4默认功能是SWS,通过目测配合万用表能测出来测试点P14连着芯片的Pin 4; 那么很可能只要有能够跟SWS接口通迅的设备,就能对芯片进行读写。在网购平台 搜索Telink能搜索到有关的烧录器,但是本着折腾就要折腾地极端点的想法,决定 先搜索一下能否用别的看起来更极客一些的烧录器。

在Marginalia搜索一番,发现了这个有关逆向小米手环6的博客:

https://rbaron.net/blog/2021/07/06/Reverse-engineering-the-M6-smart-fitness-band

小米手环6使用的主控芯片是Telink TLSR8232,跟我的温度计上的芯片一样有用作 调试的SWS接口。作者rbaron想更加了解SWS接口使用的协议,但是在网上没办法找 到多少描述协议的资料,不过rbaron找到了pvvx在小米温度计上折腾出来的成果:

https://github.com/pvvx/ATC_MiThermometer

并且在一个pvvx的一个仓库中找到了一个描述了SWS接口的协议的文件:

https://github.com/pvvx/TlsrTools/blob/master/DOCs/SWM_S.pdf

在pvvx的仓库中我也找了我感兴趣的东西——TlsrTools825x:

https://github.com/pvvx/TlsrTools825x

TlsrTools825x的目的是利用STM32F103C8T6作TLSR825x烧录器的主控芯片,利用 STM32的SPI功能配合DMA来模拟SWire协议。TlsrTools825x的仓库中有一个为 Windows系统准备的GUI客户端:

https://github.com/pvvx/TlsrTools825x/blob/3749af2811bd316e510753919ffde4634020126c/bin/TlsrTool.exe

但是笔者不太乐意特地准备一个Windows系统。作者rbaron写了一份 基于Python的cli客户端,那么只需要我基于这个客户端调整一下应该就能给 TLSR8251使用了。

笔者认为rbaron在博客中已经很好的演示了SWS接口使用的协议——Swire了,但是笔 者依然打算在这里简单的介绍一下TLSR8251使用的变体版的SWire协议;rbaron在 博客中提到TLSR8251使用了SWire协议的一种变体:

It’s worth noting that there exists at least one variation of the SWire protocol. In the other variant, the master sends 3 bytes of addr after the START byte, instead of only two bytes in our SWire protocol. The 3-byte variant is employed, for example, in Telink’s TLSR8251 SoCs, used in the Xiaomi thermometer we mentioned above.

主要信息来源于TlsrTools仓库内的SWire协议文件和rbaron的博客。Swire协议是 一个单线协议,沟通就在一根线上完成。当线上是空闲时,电平为高。 Bit 1 的信号为连续4个时间单位的低电平和1个时间单位的高电平, Bit 0 的信号为 连续1个时间单位的低电平和4个时间单位的高电平。下面这张图截取自逻辑分析仪 采集到的从TlsrTools825x发送的一段SWire信号:

swire-bit-0-1-signals.jpg

Figure 4: SWire Bit 0/1 信号

一个字节的信息由连续的1个bit的 cmd 、8个bit的数据和最后些许时间单位的 低电平表示的 end 组成。SWire协议中有两个特殊的字节: START 和 END 字节,这两个字节用来表示读写指令的开始与结束,只有这两个特殊字节的 cmd 位会是1,其它字节的 cmd 位都是0。下面这个例子就展示了SWire协议中的 START 字节,这个字节的 cmd 位为1,字节数据为 0x5a :

swire-byte-signal.jpg

Figure 5: SWire START 字节信号

然后就是读写命令。读写指令都由一个 START 字节表示开始,用一个 END 字 节表示结束。 START 字节的 cmd 位为1,数据位的值为 0x5a 。 END 字 节的 cmd 位为1,数据位的值为 0xff 。在 START 字节之后,需要给出要 进行读写的内存地址,在TlsrTools仓库内的SWire协议文件中,使用两个数据字节 表示地址的高8位和底8位。而对于我这款设备里的TLSR8251,地址则是由三个数据 字节组成,分别对应高8位、中8位和低8位。地址的字节之后则是特殊字节 RW_ID ,如果是一个读取指令, RW_ID 中的 bit7 就得是1,而如果是一个 写入指令, RW_ID 中的 bit7 就需要是0。而 bit6~bit0 则表示为SWire中 的 slave ID 。之后的情况就会根据指令是读取还是写入变得不同。如果是写 入,那么主设备就需要继续发送要写入的数据,然后用 END 字节表示沟通结 束。简单描述一下就是这个样子:

|START|AddrH|AddrM|AddrL|RW_ID|Write_data|...|END|

这里给出一个对地址 0xb2 写入 0x08 的变体版SWire写入指令的信号:

variant-swire-write-operation-signal.jpg

Figure 6: 变体版SWire的写入指令信号,对地址 0xb2 写入 0x08

如果操作是读取,那么在 RW_ID 字节之后,主设备发送一个时间单位的低电平 来表示主设备开始等从设备的信息,当随后的高电平时间超过了一个时间单位的低 电平,从设备就会发送8bits数据和1个时间单位的低电平,如果主设备想继续读接 下来的数据,需要重复操作;发送一个时间单位的低电平然后等从设备发送信息。 想结束沟通的话可以直接发送 END 字节。简单描述一下就是:

|START|AddrH|AddrM|AddrL|RW_ID|slave_data|...|END|

而 slave_data 是:

|master pulldown|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|slave pulldown|

这里也给出一个从地址 0x80007e 读取2个字节的信号 0x62 和 0x55 :

variant-swire-read-operation-signal.jpg

Figure 7: 变体版SWire的读取指令信号,从地址 0x80007e 读取2字节数据

这就是TLSR8251的SWire协议了。

我尝试运行和读了一下TlsrTools825x中的编程器的代码,在这里简单介绍一下使 用的方法。在接上设备之后,编程器会被识别为一个串口设备,我们就可以直接对 串口设备进行写入和读出。编程器从主机接收到一个字节串后,编程器会先判断这 个字节串代表的指令是否是对编程器本身进行的操作;如果字节串是想对编程器进 行操作,那么就会运行指令指定的代码,而如果不是,那就会直接将字节串转换成 对应的Swire信号然后发送。比如仓库中的这段代码表示,如果字节串的第1个字节 是 0x55 ,第2个字节是 0x00 的话,编程器就会拉低GPIO PB0的电压。而如 果第1个字节不是 0x55 ,那么就会直接转换字节串为SWire信号然后发送。

TlsrTools825x的代码仓库中的README文件介绍了怎么使用烧录器;先将 STM32F103C8T6 Blue Pill开发板按照图中方式跟温度计连接,然后需要 将 GPIO PB0 配置为push-pull模式。但是笔者不乐意特地准备一个Windows设 备;也不想花时间去找出让编程器配置GPIO的字节串是什么,因此笔者直接偷懒通 过逆汇编找出编程器的二进制固件中对应初始化 GPIO B0 的代码,然后用二进 制编辑器GNU poke修改了它,使 GPIO PB0 直接被初始化为push-pull模式。具 体细节可以查看这篇博客:

https://zhml.dev/blogs/2026-09-08-use-gnu-poke-to-avoid-compile-cn/index.html

然后是对rbaron的基于python的tlsr82-debugger-client.py进行一些修改。源码 在这个链接,可以下载下来跟rbaron写的对比一下,关注一下有哪些修改,代码许 可证是MIT协议:

./tlsr82-debugger-client.py

接下来把设备连接起来:

mi-thermometer-connect-with-stm32-blue-pill.jpg

Figure 8: 跟小米温度计连接好的STM32 Blue Pill开发板

运行代码

python tlsr82-debugger-client.py --serial-port /dev/ttyACM0 --sws-speed 8 get_soc_id

能得到 SOC ID: 0x5562 。命令 get_soc_id 从地址 0x00007e 读取2字 节。在Telink文件DS-TLSR8251-E14中的Figure 2-2能看到 0x00007e 对应的寄 存器跟 System control 有关,但是文件中并没有描述 System control 寄存 器的信息。但是根据pvvx的代码和rbaron的博客看来,读出来的 0x5562 表示这 块芯片是 TLSR825x 。

运行

python tlsr82-debugger-client.py --serial-port /dev/ttyACM0 --sws-speed 8 dump_ram ram.bin

然后 hd ram.bin 就能看到很多数据能跟规格书中描述的寄存器数据对上,比如:

00000000  1f 5c 00 01 5a f1 00 00  00 11 05 00 00 01 3b 01  |.\..Z.........;.|

能够跟规格书中表格 Table 7-5 Register configuration for I2C 的默认值对上。

运行这段指令就能把存储内的代码读出来:

python tlsr82-debugger-client.py --serial-port /dev/ttyACM0 --sws-speed 8 dump_flash flash.bin

用 strings flash.bin 就能翻出一些稍微有趣的字符串:

Authentication
disconnect
para_value_get
Cus1 Data Count
Data Read
LYWSD03MMC
Version
standard
Time
Batt
Data Count
mi.miot.ble
Cus1 Index
Cus1 Temperature and Humidity
comfortable temp and humi
ota_data
set conn interval

LYWSD03MMC 也刚好是笔者这个设备的编号。

然后在查找怎么为TLSR8251编译代码的时候发现Telink貌似并没有遵循代码许可将 工具链中的GNU C编译器和GNU binutils的源码分发出来。按照官方步骤,使用 Docker将链接为 ghcr.io/telink-semi/telink-dev-tc32 的image下载下来,在 其中的路径 /opt/toolchain/tc32/bin/ 就能看到编译好的为TC32内核芯片准备 的GNU C编译器和GNU binutils的二进制文件。而GNU C编译器和GNU binutils本身 就是以GPL协议发放的,GPL协议要求基于GPL协议发布的代码做出的修改也应该以 源码形式发布。但是在笔者写到这里之前,也就是2026年9月20日之前,笔者在官 方网站和Telink github帐号下并没有搜索或查找到官方发布的针对TC32内核芯片 的编译器和binutils的源代码。看起来在7年前就有开发者尝试寻找源码无果,然 后尝试对TC32工具链进行逆向分析:

https://github.com/rgov/Ghidra_TELink_TC32/

接下来就尝试烧录在这个代码仓库中的固件吧,根据README描述这份固件做出了不 少优化:

https://github.com/pvvx/ATC_MiThermometer

笔者的设备编号为 LYWSD03MMC ,对应的固件名就是 ATC_vNN.bin 。运行指 令把固件给烧录进去:

python tlsr82-debugger-client.py --serial-port /dev/ttyACM0 --sws-speed 8 write_flash ATC_v58.bin

烧录完成后就能通过蓝牙连接了,在 GNU/Linux 主机上运行 bluetoothctl 进入蓝牙操作界面然后运行 scan on 进行扫描,设备名字类似 ATC_F02AED 。 连接后在 menu gatt 中用 select-attribute 选择 UUID 为 0x1f1f 的 Characteristic 。运行 notify on 后根据表格运行对应的指令:

https://github.com/pvvx/ATC_MiThermometer#control-function-id-when-connected

比如运行 write 0x01 就能得到设备名称。运行 write 0x55 能读取设备配置:

55 58 87 10 0a 06 28 04 a9 31 31 00 b4 00 UX....(..11...

从第3个字节 87 开始就是设备配置信息,和代码中的 _cfg_t 类型相对应:

https://github.com/pvvx/ATC_MiThermometer/blob/3e4f5ca2c68ca184a80578c64dd969a87de03d03/src/app.h#L53-L143

如果运行 write 0x55 0x97 就能将 temp_F_or_C 配置为1,温度显示就会是 用华氏度而非默认的摄氏度:

mi-thermometer-temp-F.jpg

Figure 9: 被设置为使用华氏度显示的温度计