Embedded HandbookПриложения
Все разделы › XI. Датчики: подключение, схемы, драйверы
STM32F103, GD32VF103 · уровень продвинутый · 16 мин чтения
Открыть в интерактивном справочнике

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 Нагрев и измерение по очереди

TS100 под IronOS: жало с нагревателем и термопарой, ключ и усилитель, внутри STM32F103 TIM3 открывает ключ, TIM2 кончает нагрев каналом CH4 и запускает АЦП каналом CH1, задача PID задаёт ширину импульса на следующий период; внизу период быстрого режима 95,5 мс: нагрев до 85,8 мс, пауза 4,7 мс, измерение

Такт 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 Итоги

Дальше: раздел 104б «ИК-термометр MLX90614 по SMBus: температура объекта без контакта, PEC, излучательная способность и поле зрения» — температура без касания: MLX90614 ловит излучение предмета, а точность решают излучательная способность и поле зрения.

Открыть в интерактивном справочнике