104а. Паяльник на STM32: разбор IronOS — термопара в жале, ПИД и USB Power Delivery
В разделе 104 термопару читала готовая микросхема: MAX31855 сама усиливает микровольты, сама меряет холодный спай и отдаёт градусы по SPI. В жале паяльника такой микросхемы нет. Термопара там — это стык двух металлов прямо у нагревателя, и провода у неё с нагревателем общие. Пока по ним идёт ток нагрева, мерить нечего; мерить можно только в паузе.
Здесь разобрана открытая прошивка IronOS — та, что стоит на паяльниках TS100, TS80, Pinecil и других. Смотрим, как она делит период ШИМ на нагрев и измерение, как пересчитывает код АЦП в градусы, каким регулятором держит температуру, как выбирает напряжение у зарядки USB PD и что делает при поломке. Настоящие функции прошивки собраны на компьютере и прогнаны против модели жала: нагрев с 25 до 320 °C, нагрузка, вынутое жало, закороченная термопара.
Опоры: исходники IronOS (github.com/Ralim/IronOS, снимок 2e1f8c2,
версия v2.23) — пути ниже даны от каталога source/Core;
справочник RM0008 по тактированию F103. Термопара и холодный спай —
раздел 104, регулятор — раздел 47, таймер и ШИМ — раздел 13, АЦП
и инжектированные каналы — раздел 14, FreeRTOS — разделы 24–34.
Лицензия: IronOS распространяется по GPL-3.0. Фрагменты кода ниже — короткие цитаты с указанием файла; копии нужных файлов лежат рядом с проверкой вместе с текстом лицензии.
Как это называется в документации
Оригинал Перевод tip жало heater нагреватель cold junction compensation (CJC) поправка на холодный спай op-amp gain stage каскад усиления на ОУ injected channels инжектированные каналы АЦП lookup table таблица пересчёта thermal runaway тепловой разгон power data object (PDO) предложение источника USB PD sleep / shutdown сон / выключение keep awake pulse импульс «не засыпай» для пауэрбанка
104а.1 Что за прошивка и на чём работает
IronOS начиналась как замена заводской прошивки TS100, теперь это общая
прошивка для десятка паяльников и двух нагревательных плиток. Какой
микроконтроллер стоит в каком приборе, видно по Makefile: для
каждой модели там свои флаги компилятора и каталог платы в BSP/.
| Прибор | Микроконтроллер | Питание (по README) | Каталог платы |
|---|---|---|---|
| Miniware TS100 | STM32F103 | DC | BSP/Miniware |
| Miniware TS80, TS80P | STM32F103 | QC; у TS80P ещё PD | BSP/Miniware |
| Miniware TS101 | STM32F103 (клон, пишет Hardware.md) | DC, PD | BSP/Miniware |
| Sequre S60, S60P, S99, плитка T55 | STM32F103 | PD; у S99 ещё DC | BSP/Sequre |
| Miniware MHP30 (плитка) | STM32F103 | PD | BSP/MHP30 |
| Pine64 Pinecil V1 | GD32VF103 (RISC-V) | DC, QC, PD | BSP/Pinecil |
| Pine64 Pinecil V2 | BL706 (RISC-V) | DC, QC, PD | BSP/Pinecilv2 |
Всё, что не зависит от платы, общее: Threads/PIDThread.cpp (регулятор),
Threads/MOVThread.cpp (акселерометр), Drivers/TipThermoModel.cpp
(градусы), Src/power.cpp (ватты в ШИМ), Drivers/USBPD.cpp (выбор
напряжения). Плата даёт таймеры, АЦП, таблицу термопары и константы
в своём configuration.h. Работает всё под FreeRTOS с тиком 1000 Гц.
Дальше разбираем TS100: у него STM32F103, открытая схема и самый простой тракт. Где Pinecil устроен иначе, это оговорено.
Можно ли собрать прошивку без сети. Нет. Штатный make -k model=TS100
firmware-EN в образе с arm-none-eabi-gcc 12.2.1 собрал 122 объектных
файла и остановился на трёх вещах: генератору настроек нужен модуль
Python yaml, генератору шрифтов и переводов — bdflib, а четырём
драйверам акселерометров — заголовок <array> из стандартной библиотеки
C++ для ARM, которой в образе нет. Без сети их не поставить, так что
образа прошивки нет. Файлы регулятора собраны теми же флагами (без
-flto, чтобы в объектном файле был машинный код):
| Файл | Код (.text), байт |
|---|---|
Threads/PIDThread.cpp |
760 |
BSP/Miniware/BSP.cpp |
500 |
BSP/Miniware/ThermoModel.cpp |
424 |
Drivers/TipThermoModel.cpp |
248 |
Src/power.cpp |
220 |
Drivers/Utils.cpp |
130 |
Из 424 байт ThermoModel.cpp 408 — таблица на 51 точку.
104а.2 Нагрев и измерение по очереди
Такт F103 в TS100 — от внутреннего генератора HSI: 8 МГц / 2 × 16 = 64 МГц. Шина APB1 поделена на 16, до 4 МГц, а таймерам на ней RM0008 удваивает частоту, если делитель шины не 1 — отсюда 8 МГц у TIM2 и TIM3. Такт АЦП — 64 / 6 = 10,67 МГц.
Весь цикл «нагрев — пауза — измерение» ведёт TIM2 (BSP/Miniware/BSP.cpp):
static void switchToFastPWM(void) {
// 10Hz
infastPWM = true;
totalPWM = powerPWM + tempMeasureTicks + holdoffTicks;
htimADC.Instance->ARR = totalPWM;
htimADC.Instance->CCR1 = powerPWM + holdoffTicks;
htimADC.Instance->PSC = 2690;
}
powerPWM = 255, пауза holdoffTicks и окно измерения
tempMeasureTicks — по 14 тиков. Тик таймера — 2691 / 8 МГц = 336,4 мкс,
период — 284 тика, 95,5 мс, 10,47 Гц. В начале периода обработчик
переполнения включает нагрев, канал CH4 со значением pwm его
выключает, так что нагрев занимает от 0 до 255 тиков, до 85,8 мс. Канал
CH1 на 269-м тике (90,5 мс) через TRGO запускает инжектированную очередь
АЦП1: четыре измерения входа PB0 по 28,5 такта, всего 4 × (28,5 + 12,5) =
164 такта, 15,4 мкс. Между концом самого длинного нагрева и измерением
остаётся 14 тиков, 4,71 мс: комментарий в Setup.cpp говорит, что выходу
усилителя нужно 4 мс, чтобы успокоиться после выключения. Прерывание
«очередь готова» будит задачу PID, та считает новую мощность, и новая
ширина импульса вступает в силу со следующего периода.
Чем чаще мерить, тем меньше времени остаётся на нагрев. Поэтому режимов
два. Медленный: делитель 5380, период 270 тиков по 672,6 мкс = 181,6 мс,
5,51 Гц, пауза те же 4,71 мс, а нагрев может занять 94,4 % периода вместо
89,8 %. Src/power.cpp уходит в медленный режим, когда ширина импульса
больше 144, и возвращается в быстрый ниже 112 — гистерезис, чтобы
не дёргаться. В прогоне ниже при разгоне жала прошивка сменила режим
дважды: в медленный на первом же цикле и обратно около 320 °C; за первые
3 с она померила жало 17 раз.
Ключ нагревателя в TS100 открывает не сам вывод таймера. TIM3 гонит на
PB4 меандр 50 %, а он, по комментарию в Setup.cpp, идёт на затвор через
конденсатор — постоянный уровень через конденсатор не проходит. TIM2 лишь
включает и выключает этот меандр. Вторая страховка — счётчик
PWMSafetyTimer: задача PID при каждом вызове ставит его в 20, каждый
период таймера убавляет, а на нуле нагрев не включается. Если задача
зависла, жало остывает через 20 периодов — 1,91 с в быстром режиме
и 3,63 с в медленном.
104а.3 От кода АЦП к градусам
Термопара в жале даёт мало: на 320 °C при ручке 25 °C — около 6924 мкВ по
таблице прошивки. Усилитель на ОУ поднимает это в 1 + 750 кОм / 2,37 кОм
раз. Четыре 12-битных измерения складываются и сдвигаются на бит влево,
так что полная шкала — 32 768 (ADC_MAX_READING), это 3,3 В. Дальше
Drivers/TipThermoModel.cpp:
uint32_t rawInputmVX10 = (rawADC * vddRailmVX10) / (ADC_MAX_READING);
uint32_t valueuV = rawInputmVX10 * 100; // shift into uV
// Now to divide this down by the gain
valueuV /= OP_AMP_GAIN_STAGE;
Из микровольт вычитается смещение CalibrationOffset — 900 мкВ по
умолчанию, сумма смещения ОУ и подтяжки на входе. Потом таблица
BSP/Miniware/ThermoModel.cpp: 51 пара «микровольты — градусы» от 0 до
500 °C через 10 °C, для жал типа Hakko T12 (0 мкВ — 0 °C, 7609 мкВ —
320 °C, 12 575 мкВ — 500 °C). Между точками — прямая, за последней —
продолжение последнего отрезка: 13 000 мкВ дают 517 °C.
Холодный спай здесь — ручка. У TS100 в ней датчик TMP36, и его градусы
просто прибавляются к градусам из таблицы. Это грубее, чем в разделе 104:
складывают градусы, а не термо-ЭДС. Документация IronOS
(Documentation/Temperature.md) это признаёт: остаток ошибки считают
постоянным смещением, и около комнатной температуры показание может
прыгать. Смещение подстраивает калибровка при включении (CJC.cpp):
если жало и ручка отличаются меньше чем на 10 °C, прошивка берёт
16 измерений и записывает их среднее в микровольтах как новое смещение.
Настоящий код прошивки, собранный на компьютере, на модели усилителя и АЦП (ручка 25 °C, смещение 900 мкВ):
| Жало | Код АЦП | Сумма | Прошивка насчитала | По таблице на самом деле | Показание |
|---|---|---|---|---|---|
| 100 °C | 1066 | 8528 | 1809 мкВ | 1807 мкВ | 100 °C |
| 200 °C | 1921 | 15 368 | 3982 мкВ | 3976 мкВ | 200 °C |
| 320 °C | 3082 | 24 656 | 6932 мкВ | 6924 мкВ | 320 °C |
| 400 °C | 3969 | 31 752 | 9187 мкВ | 9174 мкВ | 400 °C |
| 430 °C | 4095 | 32 760 | 9507 мкВ | 10 040 мкВ | 411 °C |
Две вещи видны сразу. Во-первых, OP_AMP_GAIN_STAGE считается
препроцессором в целых: 1 + 750000 / 2370 = 317, а по резисторам 317,46.
Прошивка завышает микровольты на 0,15 %, на 320 °C — на 8 мкВ, меньше
градуса. Во-вторых, одна единица суммы — 100,7 мкВ на выходе усилителя
и 0,318 мкВ на термопаре: вот почему встроенному АЦП хватает точности,
которой в разделе 104 не было, — всю работу делает усилитель.
Зато шкала коротка. При 430 °C АЦП уже в потолке (4095), и прошивка
видит 411 °C. getTipMaxInC() при ручке 25 °C возвращает 410 °C, и
задача PID режет уставку до этого значения: выбрать в меню можно
до 450 °C, а держать TS100 будет не выше 410.
104а.4 Регулятор: интегратор с утечкой вместо ПИД
В Threads/PIDThread.cpp два регулятора. Настоящий ПИД включает
TIP_CONTROL_PID, и в этом снимке он определён только для Pinecil V2:
Kp 40, Ki 6, Kd 200 (configuration_Pinecilv2.h). Все остальные приборы,
и TS100 в том числе, работают на интеграторе с утечкой:
sum = (sum * (100 - (inertia / rate))) / 100;
// Add the new value x integration interval ( 1 / rate)
sum += (gain * val) / rate;
На вход идёт val = 65 × (уставка − показание), 65 — TIP_THERMAL_MASS
в десятых ватта на градус в секунду; inertia тоже 65, gain — 2,
rate — сколько раз в секунду вызван регулятор, целым: 1000 / 95 = 10
в быстром режиме, 1000 / 181 = 5 в медленном. Выход — мощность в десятых
ватта, зажатая в ± предел мощности.
Если раскрыть целые числа, в быстром режиме за цикл сумма умножается на 94/100 и к ней прибавляется 13 × ошибка. Утечка 6 % за цикл не даёт интегралу копиться: в установившемся режиме сумма равна 13 / 0,06 = 216,7 десятых ватта на градус ошибки, то есть 21,67 Вт/°C. В медленном — × 87/100 и 26 × ошибка, 20,00 Вт/°C. По сути это П-регулятор с большим усилением и сглаживанием по времени, а не интеграл. Автор объясняет выбор в комментарии «Sandman note»: сразу после импульса нагреватель горячее жала, мгновенное показание завышено, и регулятор с утечкой ведёт себя как П вдали от уставки и почти как И около неё.
Отсюда же неустранимая ошибка, как в разделе 47.2. Чтобы держать 5,9 Вт, нужно 0,27 °C недогрева, а градусы в прошивке целые. В прогоне жало стояло на 319…320 °C, в среднем 319,69.
Модель жала для прогона. Её параметры — наши допущения, а не данные
TS100: два узла — нагреватель с термопарой (0,25 Дж/°C) и тело жала
(1,44 Дж/°C); в сумме 1,69 Дж/°C — столько, по комментарию в power.hpp,
автор намерил на своём жале. Между узлами 1 К/Вт — чтобы тело догоняло
нагреватель за 3–5 с, как пишет Temperature.md. Потери в воздух
0,02 Вт/К, питание 24 В, нагреватель 7,5 Ом, ручка 25 °C. Таймер, АЦП
и вызовы регулятора — как в разделе 104а.2.
| Прогон, уставка 320 °C | Интегратор (TS100) | ПИД с константами Pinecil V2 |
|---|---|---|
| Показание впервые 320 °C | 5,99 с | 7,10 с |
| Пик показания | 323 °C (+3) | 322 °C (+2) |
| В полосе ±2 °C после | 6,36 с | 10,32 с |
| Тело жала в 5 °C от своего предела | 8,98 с | 10,41 с |
| Показание за 50…60 с | 319…320, среднее 319,69 | 319…320, среднее 319,38 |
| Смен режима ШИМ | 2 | 20 |
| Нагрузка 0,1 Вт/К на 5 с: минимум показания | 317 °C | 313 °C |
| Под нагрузкой, среднее за последние 2 с | 318,48 °C при 30,9 Вт | 313,48 °C при 29,5 Вт |
Держит регулятор температуру нагревателя, а не кончика: в установившемся режиме тело жала в модели на 5 °C холоднее (314,8 °C при 319,7 °C на нагревателе) — ровно на потери 5,9 Вт × 1 К/Вт. Под нагрузкой разница растёт: минимум показания 317 °C, минимум тела — 290,3 °C. Это свойство любого паяльника с датчиком у нагревателя. ПИД Pinecil V2 на тракте TS100 показан только для сравнения: IronOS так TS100 не собирает. После снятия нагрузки интегратор не вышел из полосы ±2 °C ни разу, ПИД вернулся в неё к 37,16 с — через 2,16 с.
104а.5 Мощность, ШИМ и USB PD
Регулятор выдаёт ватты, а таймеру нужна ширина импульса. Src/power.cpp
считает, сколько даст жало при импульсе во весь период:
uint32_t availableWattsX10 = (v * v) / tipResistance;
и умножает на долю периода, доступную нагреву:
availableWattsX10 = availableWattsX10 * powerPWM;
availableWattsX10 /= totalPWM;
При 24 В и жале 7,5 Ом это 692 десятых ватта в быстром режиме и 728 в медленном. Нужная ширина — пропорция:
pwm = (powerPWM * x10Watts) / availableW10(sample);
5 Вт — 18 из 255, 10 Вт — 36, 30 Вт — 110, 69,2 Вт и больше — 255. Сверху мощность режут настройка «предел мощности» (у TS100 по умолчанию выключена), договорённость с источником, а у S99 ещё и нагрев ручки; в прогоне предел равен тем же 692.
У паяльников без понижающего преобразователя ток в импульсе всегда V / R,
ШИМ меняет только среднее (Documentation/Hardware.md). Поэтому источнику
USB PD нельзя просить напряжение, при котором V / R больше его тока.
У TS80P, TS101, MHP30 и обоих Pinecil (POW_PD 1 в configuration.h)
переговоры ведёт микросхема FUSB302 по I²C (адрес 0x22 в USBPD.cpp)
и библиотека usb-pd того же автора; в снимке это подмодуль git, и его
каталог пуст. У Sequre вместо неё готовые приёмники HUB238 или FS2711
(POW_PD_EXT). Правило выбора для FUSB302 — в Drivers/USBPD.cpp:
int min_resistance_ohmsx10 = voltage_mv / current_a_x100;
if (voltage_mv > 0) {
if (voltage_mv <= (USB_PD_VMAX * 1000)) {
if (voltage_mv <= 20000 || (pd_mode != usbpdMode_t::NO_DYNAMIC)) {
if (min_resistance_ohmsx10 <= tipResistance) {
Из подходящих фиксированных предложений берётся самое высокое
напряжение. Режим PD по умолчанию — 0, NO_DYNAMIC: только фиксированные
напряжения и не выше 20 В, даже если USB_PD_VMAX у платы 28 (Pinecil V2,
TS101). В режиме DEFAULT к сопротивлению жала добавляется 0,5 Ом
запаса и разрешаются PPS и EPR. Два примера для жала 7,5 Ом, правило
пересказано на Python:
| Блок питания | 20 В: мВ / (10 мА) | Выбрано | Мощность и ток |
|---|---|---|---|
| 45 Вт: 5 В 3 А, 9 В 3 А, 15 В 3 А, 20 В 2,25 А | 20000 / 225 = 88 > 75 | 15 В | 30,0 Вт, 2,00 А |
| 65 Вт: 5 В 3 А, 9 В 3 А, 15 В 3 А, 20 В 3,25 А | 20000 / 325 = 61 ≤ 75 | 20 В | 53,3 Вт, 2,67 А |
При старте задача PID ждёт окончания переговоров до 3 с и только потом начинает греть.
104а.6 Защиты, сон и экран
Тепловой разгон. detectThermalRunaway() в PIDThread.cpp следит
за двумя признаками. Первый: если нагрев идёт больше 20 с, а разница
между наибольшим и наименьшим показанием меньше 3 °C, счётчик
heaterThermalRunawayCounter растёт на 1 — один раз за всё время работы,
дальше проверка отключается. Второй: если нагрев выключен, а сумма АЦП
выше ADC_MAX_READING - 8, счётчик растёт каждый цикл. Раз в секунду
счётчик убывает на 1. Больше 8 — нагрев выключается, а режим пайки
(Soldering.cpp) показывает экран теплового разгона. Отдельно нагрев
гасится, если сумма АЦП выше 0x7FFF - 32.
Жало вынуто. Подтяжка на входе уводит усилитель в потолок, показание
улетает вверх. isTipDisconnected() считает жало вынутым, если
показание выше getTipMaxInC() - 5. В прогоне жало «вынули» на 30-й
секунде: первое же показание было 411 °C, выше уставки, и регулятор снял
нагрев на том же цикле.
Сон и выключение. Задача MOVThread.cpp раз в 100 мс читает
акселерометр — прошивка сама опознаёт, какой стоит: MMA8652FC, LIS2DH12,
BMA223, SC7A20 или MSA301. Каждое чтение сравнивается со средним восьми
последних; если сумма отклонений по трём осям больше порога, это
движение. Порог — 1500 + 9 × 200 − 200 × чувствительность, при
чувствительности 7 по умолчанию — 1900. Без движения и кнопок 50 с —
сон: уставка падает до 150 °C (или до рабочей, если та ниже). Через
10 мин без движения — выключение нагрева.
Экран — SSD1306 по I²C, адрес 0x3C; у TS100 это 96 × 16 точек на программной шине, у TS101 и приборов Sequre — 128 × 32.
104а.7 Грабли
Уставка 320 °C, на экране 319. Регулятор с утечкой держит мощность только за счёт ошибки: 5,9 Вт — это 0,27 °C недогрева, а градусы целые. В прогоне среднее 319,69 °C.
Выше 410 °C TS100 не греет. Шкала АЦП кончается: при ручке 25 °C
getTipMaxInC() = 410 °C, и уставку режут до неё, хотя меню даёт 450.
Показание 320, а припой плавится хуже. Термопара у нагревателя. В модели тело жала на 5 °C холоднее без нагрузки, а под нагрузкой 0,1 Вт/К его минимум — 290,3 °C при минимуме показания 317 °C.
Закороченная термопара не ловится. Если термопара даёт 0 мкВ, прошивка
видит температуру ручки. В прогоне показание 60 с стояло на 25 °C,
ширина импульса — 255, счётчик разгона так и остался 0: наибольшее
показание при нагреве не больше наименьшего, и проверка разницы не
срабатывает. Сон не спасёт: уставка сна 150 °C всё равно выше
показания 25 °C (Sleep.cpp). Нагрев снимет выключение через 10 мин
без движения или сам паяльщик.
Проверка «АЦП в потолке» на TS100 молчит. Четыре 12-битных измерения
со сдвигом дают не больше 4095 × 8 = 32 760, а условие — строго больше
ADC_MAX_READING - 8, то есть больше тех же 32 760. В прогоне с вынутым
жалом сумма стояла на 32 760, счётчик остался 0. Нагрев всё равно
снимается, но другим путём: показание выше уставки.
Зарядка на 45 Вт, а паяльник берёт 15 В. 20 В × 2,25 А для жала 7,5 Ом не годится: пиковый ток 20 / 7,5 = 2,67 А больше 2,25 А. Нужен блок с 3 А на 20 В.
Прошивка не собирается из архива без сети. Подмодуль usb-pd пуст,
генераторам нужны yaml и bdflib, а драйверам акселерометров —
стандартная библиотека C++ для ARM.
Показание прыгает около комнатной температуры. Холодный спай учтён
сложением градусов ручки, остаток ошибки считается постоянным
(Temperature.md); ниже 100 °C это заметно.
104а.8 Итоги
- IronOS — открытая прошивка (GPL-3.0) для TS100, TS80(P), TS101, Sequre, MHP30 на STM32F103 и Pinecil на GD32VF103 и BL706.
- Нагрев и измерение идут по одним проводам по очереди: TIM2 делит период 95,5 мс (быстрый режим) или 181,6 мс (медленный) на нагрев до 255 тиков, паузу 4,71 мс и инжектированный АЦП.
- Код АЦП → микровольты (усиление 317, 0,318 мкВ на единицу) → таблица из 51 точки → плюс градусы ручки. Предел TS100 — 410 °C.
- Регулятор TS100 — интегратор с утечкой: 21,67 Вт/°C в быстром режиме, держит 319…320 °C при уставке 320. ПИД (Kp 40, Ki 6, Kd 200) — только у Pinecil V2.
- На модели жала: 320 °C за 5,99 с, пик +3 °C, полоса ±2 °C с 6,36 с.
- Ватты → ШИМ через V² / R: при 24 В и 7,5 Ом полная ширина — 69,2 Вт. USB PD берёт самое высокое напряжение, где V / I не больше сопротивления жала: блок 45 Вт — 15 В, блок 65 Вт — 20 В.
- Защиты: счётчик теплового разгона (больше 8 — стоп), страховка 20 периодов, вынутое жало; сон через 50 с до 150 °C, выключение через 10 мин. Закороченную термопару прошивка не замечает.
Дальше: раздел 104б «ИК-термометр MLX90614 по SMBus: температура объекта без контакта, PEC, излучательная способность и поле зрения» — температура без касания: MLX90614 ловит излучение предмета, а точность решают излучательная способность и поле зрения.