Note 02 of 02: 笔记 02 / 02:Temperature Sampling and Fan Response温度采样与风扇响应

Following sensor selection, asynchronous reading and sending, and peak resets to understand when a temperature sample changes fan output.沿传感器选择、异步读取与发送、温度峰值清空,研究一条温度样本何时影响风扇输出。

Every few seconds the cooler's desktop program pushes a line like CPU62.7 into the Bluetooth link, and the ESP32 decides what the fan should do with it. Getting one line across is easy. Explaining why the fan changed at one particular moment means asking which sensor the number came from, when it was read, and what happened to it on the way. Tracing that single message back through the code turned up more traps than I expected.

散热器的上位机每隔几秒就往蓝牙里塞一行 CPU62.7,ESP32 收到以后决定风扇怎么转。一行字送过去很容易,可要解释风扇为什么偏偏在某个时刻变速,就得追问这个数到底是哪个传感器的、什么时候读到的、路上又被怎么处理过。顺着这一行消息往回翻代码,坑比我想的多。

Sensor Selection传感器的选择

The desktop source uses LibreHardwareMonitor to walk every sensor under the CPU and GPU, and whenever the type is temperature it writes the reading into cpuTemperature or gpuTemperature. A CPU usually exposes several temperature entries, typically one per core plus a package reading, and the loop writes each in turn, every one overwriting the last. The variable is called CPU temperature, yet which entry survives depends on whatever the traversal reaches last. The CPU Temp label in the window is rewritten several times within the same pass, and the eye only catches the final value.

上位机代码用 LibreHardwareMonitor 遍历 CPU 和 GPU 底下的所有传感器,只要类型是温度,就把读数写进 cpuTemperature 或 gpuTemperature。一颗 CPU 往往挂着好几个温度项,通常每个核心一个,再加一个封装温度,这段循环挨个写过去,后一个盖掉前一个。变量叫 CPU 温度,最后留下的是哪一项,全看遍历时谁排在最后。窗口里那行 CPU Temp 标签在同一轮里也被刷了好几遍,人眼只看得到最后一下。

The name, then, no longer says which temperature it holds. To follow CPU Package, the code should pick that entry by name and keep watching the same one. LibreHardwareMonitor's `ISensor` interface exposes Name and Identifier, which are enough for a fixed selection rule. One decimal place is there for display; a fixed source is what makes two successive readings comparable.

变量名里的 CPU 温度因此说不清指的是哪一项。想跟踪 CPU Package,就该指名道姓地把它挑出来,以后一直盯着同一项。LibreHardwareMonitor 的 `ISensor` 接口提供了 Name 和 Identifier,拿它们写一条固定的选择规则就够了。保留一位小数是为了显示,来源固定下来,前后两次读数才有得比。

Read and Send Order读取和发送的先后

With the sensor settled, the next question is whether a fresh reading makes it into this round's message. The desktop timer fires every five seconds, and the callback starts monitoring and sending as two separate Task.Run calls. Monitoring comes first in the source, but nothing makes the sender wait for it. Microsoft's task programming guide is explicit that one task waits for another only through a wait or a continuation. This code sets up neither, so the sender can easily run first and transmit whatever the previous round left in the field.

传感器选定了,还得看新读数赶不赶得上这一次发送。上位机的定时器 5 秒触发一次,回调里用两个 Task.Run 分别启动测温和发送。代码里测温写在前面,两个任务之间却没有任何等待。Microsoft 的任务编程说明讲得很清楚,一个任务要等另一个,得靠等待或延续明确建立依赖。这段程序两样都没有,发送任务完全可能抢先一步,把上一轮留在字段里的温度发出去。

The monitoring side also makes the race lopsided. Every round it creates a new Computer, calls Open() and walks the hardware again, and initializing hardware monitoring takes real time, while the sender only reads a field and writes one serial line. It is not hard to guess which task usually finishes first.

测温这边还让这场赛跑更加一边倒。它每一轮都新建一个 Computer,Open() 一遍再从头遍历,初始化硬件监控本身就要花时间;发送那边只是读个字段、往串口写一行。谁先跑完,大概不用猜。

A set of hypothetical times shows the effect. The field holds 60°C. At 5.000 s both tasks are submitted; at 5.002 s the sender takes 60°C; only at 5.080 s does monitoring write a new 72°C. This round still transmits 60°C. The millisecond gaps are illustration values. What matters is that sending never waits for the current read, and a faster timer would not create that dependency by itself.

拿一组假设的时刻推一遍。字段里原本是 60°C;5.000 秒,两个任务一起提交;5.002 秒,发送任务取走 60°C;5.080 秒,测温任务才写进新的 72°C。这一轮发出去的还是 60°C。几十毫秒的差距是推演用的数,关键在于发送从来不等本轮读取完成,定时器调得再快,这层依赖也不会自己冒出来。

CPU and GPU also share the sending turns, one type every five seconds, so each comes round about every ten seconds. The timer tick, the completed read and the transmission are three separate moments. To fix it I would have each read return a definite sample and hand that sample straight to the sender, instead of letting the sender look up a shared field that another task may be rewriting.

CPU 和 GPU 还要轮流用发送机会,每 5 秒发其中一种,每种大约 10 秒才轮到一次。定时器触发、传感器读完、数值发出,其实是三个不同的时刻。要改这一段,我会让每次读取返回一个确定的样本,再把这个样本直接交给发送,省得发送时回头去翻一个随时可能被别的任务改写的共享字段。

30-Second Peak Window30 秒峰值窗口

At the ESP32 program, the same number goes into two sets of variables. cpuValue and gpuValue hold the latest reading for the OLED, while maxCpuValue and maxGpuValue change only when a higher temperature arrives. About every 30 seconds the main loop puts the larger peak into maxValue and calculates duty for the current mode. A lower new reading can appear on screen immediately while the earlier high one still feeds the next output calculation.

消息到了 ESP32 程序这边,同一个数会分进两组变量。cpuValue 和 gpuValue 存最近收到的值,给 OLED 显示;maxCpuValue 和 maxGpuValue 只有来了更高的温度才更新。主循环每隔约 30 秒把两个峰值里较大的那个存进 maxValue,再按当前模式算占空比。屏幕上可以马上出现一个更低的新读数,之前那个高温却照样参与下一次输出计算。

Take an automatic mode with no one touching the buttons. A window opens at 0 s. The CPU reports 80°C at 5 s and 60°C at both 15 and 25 s, and the GPU stays below that CPU peak. At 30 s the output is calculated from 80°C; the peaks are then cleared and the output holds until the next update. From 15 s onward the screen shows 60°C, yet only at 30 s does the program start setting the fan from 80°C. The temperature has already dropped while the fan keeps pushing, and the program itself arranged that wait.

假设处在自动模式,中途没人碰按键。窗口从 0 秒开始,第 5 秒 CPU 来了 80°C,第 15 秒和第 25 秒都是 60°C,GPU 一直低于这次 CPU 峰值。到第 30 秒算输出,用的还是 80°C;算完峰值清零,输出一直保持到下一次更新。于是屏幕从第 15 秒起就显示 60°C,程序却到第 30 秒才开始按 80°C 设置风扇。温度已经降了,风扇还在使劲,中间这段等待是程序自己安排的。

Buttons and Peak Resets按键与峰值清空

Besides calculating the output, updatePWM() also clears the peaks along the way.

updatePWM() 除了算输出,还顺手把峰值清了。

cpp
dutyCycle =processBluetoothValue(maxValue);
ledcWrite(ledChannel, map(dutyCycle, 0, 100, 0, 255));
maxCpuValue = 0.0;
maxGpuValue = 0.0;

Writing PWM and clearing the temperature peaks live in one function. Searching for its callers turned up the button handler as well as the 30-second update, because every short press ends with a call to it. Changing mode or adjusting frequency therefore wipes the accumulated peaks too. The button path also uses the previously saved maxValue without first collecting the new peaks, and it leaves lastUpdateTime untouched.

写 PWM 和清温度峰值,被放进了同一个函数。再查谁在调用它,除了 30 秒的定时更新,按键处理函数在每次短按的末尾也会调一次。切模式、调频率,都会顺带把攒着的峰值清掉。按键这条路径用的还是上一次存下的 maxValue,没先把正在累计的新峰值取出来,也没重设 lastUpdateTime。

That stretches the timeline one step further. Suppose a frequency button is pressed at 25 s. The retained 80°C disappears, the periodic update still arrives at 30 s, and the calculation now draws on only the few seconds of data received after the press. Reading the constant 30000 on its own suggests a fixed 30-second window. Finding every place that clears it shows how one button press can rewrite what the window contains, and that coupling between functions turned out more interesting to me than the constant itself.

前面的时间线还能再推一步。假设第 25 秒按了一下调频键,刚才保留的 80°C 被清掉,第 30 秒的定时更新照常到来,这时参与计算的,只剩按键之后那几秒收到的数据。单看 30000 这个常数,很容易把它当成固定的 30 秒统计窗口;把所有清零的地方都找出来,才看清一次按键就能改写窗口里装的东西。函数之间的这层牵连,比常数本身有意思多了。

Sample Records and Control Timing样本记录与控制时序

To reorganize this code, I would split writing PWM from closing out a temperature window. Buttons would change the output setting, and an explicit rule would decide whether collection restarts. With a fixed period, a button press keeps the accumulating peaks; if a mode change should start a fresh count, the peaks and the timing origin reset together. Either choice is easy to state, and combining both operations in one function is what makes the choice easy to miss.

要整理这段程序,我会把写 PWM 和结算温度窗口拆成两个操作。按键只管改输出设置,温度统计要不要重新开始,交给一条明确的规则。选固定周期,就让按键保留正在累计的峰值;想在切模式时重新统计,就把峰值和计时起点一起重置。两种安排都说得清,混在一个函数里,该选哪种反而容易被忽略。

Each desktop sample could also carry a little more: sensor identifier, value, read-completion time and a message sequence number, with the ESP32 noting when the message arrived and when its sample was used. Read completion only says when the program got the value, and the sensor may have its own update cycle. A PC timestamp and the ESP32's millis() cannot simply be subtracted either. Matching messages by sequence number and then looking at the waits on each side separately is more reliable than subtracting timestamps from two different clocks.

上位机的样本也可以多带点信息:传感器标识、读数、读取完成的时间和消息序号;ESP32 这边再记下收到消息和采用样本的时刻。读取完成时间只说明程序什么时候拿到这个值,传感器自己可能还有更新周期;电脑的时间和 ESP32 的 millis() 也不能直接相减。先靠序号对上同一条消息,再分别看两端各自等了多久,比硬减两个不同时钟的时间戳靠谱。

With that in place, a temperature that falls while the fan stays high becomes something to trace step by step: which reading was taken, when the message left and arrived, and which peak entered the calculation. The records would show whether those seconds went into task scheduling, sending turns or the peak window, and that is when I would decide which step to speed up first.

这样一来,温度降了而风扇没降,就成了一个能一段一段查下去的问题。先确认读的是哪一项温度,再看这条消息什么时候发出、什么时候收到,最后看哪个峰值进了计算。那几秒钟到底耗在任务之间、发送轮次里还是峰值窗口里,记录里都找得到,到那时再决定先加快哪一步。

09Back to the project回到项目DIY Laptop CoolerDIY 压风式笔记本散热器