196. Мини-проект: термостат на BluePill
Рецепты 🔧 по разделам — это кирпичи: светодиод, АЦП, UART, запись во Flash. Мини-проект — законченный прибор из нескольких кирпичей, со всем, что отличает прибор от примера: он переживает отказ датчика, не щёлкает реле как пулемёт, помнит настройки после выключения и отвечает в консоль.
Первый прибор — термостат: терморезистор на АЦП, реле нагревателя, консоль по UART, настройки во Flash. И главное в устройстве проекта: логика прибора не знает про регистры. Всё железное — в одном файле платформы; поэтому ту же логику можно прогнать на компьютере вместе с моделью комнаты и увидеть, как прибор ведёт себя за четыре часа, ещё не собрав плату.
Файлы проекта — _verify/proj196/: thermo.h, thermo.c (логика),
plat_f103.c (платформа BluePill), sim.c (платформа-модель).
Прошивка собрана для Cortex-M3, модель прогнана на компьютере.
Сверяет _verify/proj196_check.py.
Как это называется в документации
Оригинал Перевод thermostat, bang-bang control термостат, двухпозиционное регулирование hysteresis, deadband гистерезис, зона нечувствительности NTC thermistor, B-constant терморезистор с отрицательным коэффициентом, постоянная B minimum on/off time наименьшее время между переключениями platform layer, hardware abstraction слой платформы: всё, что касается железа host-based test, plant model прогон на компьютере, модель объекта
196.1 Что делает прибор
| Функция | Как |
|---|---|
| измерять температуру | NTC 10 кОм в делителе на PA0, АЦП, 16 отсчётов, таблица с интерполяцией |
| держать уставку | включать нагрев при T ≤ уставка − 0,5 °C, выключать при T ≥ уставка + 0,5 °C |
| беречь реле | переключать не чаще раза в 60 с |
| переживать отказ датчика | нет правдоподобного показания 3 с — нагрев выключить, светодиод PC13, «АВАРИЯ» в консоль |
| слушать консоль | get, set 22.5, hyst 0.3 по USART1, 115 200 бод |
| помнить настройки | последняя страница Flash (0x0800FC00), под CRC-16; испорчены — заводские |
Подключение:
| Вывод BluePill | Куда |
|---|---|
| PA0 | средняя точка делителя: NTC 10 кОм к 3,3 В, 10 кОм к земле |
| PB12 | база транзистора ключа реле (раздел 52), диод на обмотке |
| PC13 | светодиод платы: горит — авария |
| PA9 / PA10 | USART1 TX / RX → переходник USB-UART |
196.2 Устройство программы: логика отдельно от железа
Логика общается с миром только через шесть функций платформы:
int16_t plat_read_temp(void); /* десятые доли °C или TEMP_FAIL */
void plat_relay(int on);
uint32_t plat_now_ms(void);
void plat_puts(const char *s);
int plat_cfg_load(struct thermo_cfg *c); /* 0 — прочитано */
void plat_cfg_save(const struct thermo_cfg *c);
На плате их реализует plat_f103.c на регистрах, на компьютере — sim.c
с моделью комнаты. Логика (thermo.c) одна и та же, ни строчки для
компьютера отдельно. Это тот же приём, что и в разделе 184: модуль, который
можно проверить без платы.
Главный цикл на плате — простой, без ОСРВ: каждые 100 мс thermo_tick,
каждый принятый символ — в thermo_rx. Время — от SysTick, 1 мс.
Логика состояния — три состояния: «ждёт», «нагрев», «авария». Переходы:
- «ждёт» → «нагрев»: T ≤ уставка − гистерезис, и с прошлого переключения прошло 60 с;
- «нагрев» → «ждёт»: T ≥ уставка + гистерезис, и прошло 60 с;
- любое → «авария»: нет правдоподобного показания 3 с — реле выключается сразу, без ожидания 60 с;
- «авария» → «ждёт»: датчик снова отвечает.
196.3 Датчик: таблица вместо логарифма
Сопротивление NTC зависит от температуры по экспоненте, и точная формула требует
логарифма. На Cortex-M3 без FPU это дорого, поэтому температура считается по
таблице из 17 точек (через 5 °C от −20 до 60) с линейной интерполяцией между
ними. Таблицу посчитал ntc_table.py по формуле с постоянной B = 3950:
| °C | −20 | 0 | 25 | 50 | 60 |
|---|---|---|---|---|---|
| код АЦП | 355 | 939 | 2048 | 3014 | 3280 |
- При 25 °C сопротивления равны — делитель пополам, код 2048.
- Наибольшая ошибка интерполяции по всем кодам диапазона — 0,13 °C (у −17,5 °C); в комнатном диапазоне меньше.
- Около 25 °C на градус приходится 45,5 единицы АЦП — десятые доли градуса различимы.
- Код вне таблицы (меньше 355 или больше 3280) функция считает отказом
и возвращает
TEMP_FAIL. Настоящий обрыв даёт код около 0 (NTC стоит к 3,3 В, вход стянут к земле резистором), замыкание — около 4095; коды между ними и краями таблицы — просто температура ниже −20 или выше 60 °C. Для комнаты это авария, но термостату в неотапливаемом гараже так нельзя: там таблицу расширяют вниз, а аварией считают только коды у краёв шкалы.
196.4 Прогон на модели комнаты
Модель: комната остывает к наружным 5 °C с постоянной времени 30 минут,
нагреватель мог бы поднять её до 35 °C; датчик — с шумом ±0,1 °C. Шаг —
100 мс, четыре часа. Сценарий: старт при 15 °C; через час в консоль
get и set 22.5; через два часа датчик пропадает на минуту; в конце —
испорченные настройки и неверные команды.
| Что | Результат |
|---|---|
| до 21 °C от старта | 642 с |
| колебания в установившемся режиме (уставка 21,0) | 20,65…21,35 °C |
| период цикла нагрева | 169 с |
| переключений реле за первый час | 36 |
| реакция на отказ датчика | нагрев выключен через 3,1 с |
| нагреватель включён | 59,6 % времени |
уставка во «Flash» после set 22.5 |
225, CRC сходится |
| после порчи одного бита настроек | уставка 210 — заводская |
Колебания уже заданной полосы ±0,5 °C: реле переключается по зашумлённому показанию, которое то и дело выскакивает к порогу на 0,15 °C раньше: 0,1 — шум, 0,05 — округление до десятых.
Консоль за прогон:
cfg: заводские
t=21.2 set=21.0 ждёт
ok
АВАРИЯ: датчик
датчик в норме
t=22.2 set=22.5 ждёт
Неверные команды (set 99, hyst abc) получили подсказку; строка длиннее
31 символа отброшена с сообщением.
Проверка CRC. Функция thermo_crc16 на строке 123456789 даёт
0x29B1 — контрольное значение CRC-16/CCITT-FALSE: реализация правильная.
196.5 Прошивка для BluePill
Платформа plat_f103.c — на регистрах через заголовок CMSIS для F103:
- тактирование — внутренний HSI 8 МГц, ФАПЧ не нужна;
- USART1: делитель 69 → 115 942 бод, отличие от 115 200 — 0,64 %;
- АЦП: время выборки 239,5 такта, калибровка после включения; повторная запись ADON запускает преобразование;
- настройки: разблокировка Flash двумя ключами, стирание страницы, запись полусловами (раздел 11.5).
Собрано arm-none-eabi-gcc -Os для Cortex-M3, без предупреждений:
| Что | Байт |
|---|---|
| вся прошивка (Flash) | 1968 |
| ОЗУ (переменные) | 64 |
из них логика thermo.c — код |
904 |
| логика — строки сообщений | 248 |
Меньше 2 КБ из 64 — место есть и на журнал измерений, и на дисплей.
196.6 Что улучшить
- Журнал: раз в минуту писать температуру в кольцевой буфер во Flash
(раздел 11.8) и выдавать по команде
log. - ПИ-регулятор вместо гистерезиса — если нагреватель управляется ШИМ, а не реле (разделы 47 и 57).
- Сторожевой таймер (раздел 12): зависла программа при включённом нагреве — сброс; после сброса вывод реле — вход, и ключ закрывается резистором на базе.
- Второй датчик и сравнение показаний: расходятся больше чем на 2 °C — авария.
196.7 Грабли
Реле щёлкает на каждом шуме. Нет гистерезиса или полоса уже шума датчика. В модели шум ±0,1 °C и округление показания до десятых (ещё 0,05 °C) вместе сдвигают пороги на 0,15 °C: нагрев включается уже при 20,65 °C и выключается при 21,35 °C — размах ±0,35 вместо заданных ±0,5.
Реле «залипло» во включённом положении при обрыве датчика. Отказ не распознан: код АЦП при обрыве — край диапазона, его надо считать аварией, а не −20 °C.
Выключение по аварии ждёт 60 секунд. Защиту реле от частых переключений применили и к аварии. Авария выключает сразу.
После перепрошивки — «cfg: заводские». Прошивка со стиранием всей памяти стёрла и страницу настроек. Чтобы их сохранить — стирать только занятые программой страницы.
Настройки «сбились» после пропадания питания при записи. Страница уже стёрта, а новые данные не дописаны. CRC это ловит — будут заводские; надёжнее — две страницы по очереди (как метаданные в разделе 195).
Консоль «зависла». Строка без перевода строки копится; длиннее буфера — выбросить и сказать об этом.
196.8 Итоги
- Термостат = АЦП + реле + консоль + Flash, но главное — поведение при отказах и защита реле.
- Логика отделена от железа шестью функциями платформы — и прогоняется на компьютере с моделью комнаты.
- NTC — по таблице из 17 точек: ошибка не больше 0,13 °C, 45,5 единицы АЦП на градус.
- Модель: 642 с до уставки, колебания 20,65…21,35 °C, цикл 169 с, реакция на отказ датчика 3,1 с, испорченные настройки распознаны по CRC.
- Прошивка BluePill — 1968 байт Flash и 64 байта ОЗУ.
Полные исходники
thermo.h — интерфейс логики прибора и функций платформы.
/* Мини-проект «Термостат»: логика прибора без железа.
* Всё, что касается выводов, датчика, времени и памяти настроек, — в функциях платформы (plat_*),
* которые на плате пишутся поверх регистров, а на компьютере — поверх модели комнаты. */
#ifndef THERMO_H
#define THERMO_H
#include <stdint.h>
#define TEMP_FAIL (-32768) /* датчик не ответил */
#define TEMP_MIN_C10 (-400) /* правдоподобный диапазон, десятые доли градуса */
#define TEMP_MAX_C10 (1250)
/* настройки: хранятся с CRC-16, иначе берутся заводские */
struct thermo_cfg {
int16_t setpoint_c10; /* уставка, 0,1 °C */
int16_t hyst_c10; /* половина полосы гистерезиса, 0,1 °C */
uint16_t min_switch_s; /* реле не переключать чаще, секунд */
uint16_t crc;
};
enum thermo_state { T_IDLE, T_HEAT, T_FAULT };
struct thermo {
struct thermo_cfg cfg;
enum thermo_state state;
int16_t temp_c10;
uint32_t last_switch_ms;
uint32_t last_good_ms;
uint32_t switches;
char line[32];
uint8_t line_len;
};
/* платформа */
int16_t plat_read_temp(void); /* десятые доли °C или TEMP_FAIL */
void plat_relay(int on);
uint32_t plat_now_ms(void);
void plat_puts(const char *s);
int plat_cfg_load(struct thermo_cfg *c); /* 0 — прочитано */
void plat_cfg_save(const struct thermo_cfg *c);
/* логика */
uint16_t thermo_crc16(const void *p, uint32_t n);
void thermo_init(struct thermo *t);
void thermo_tick(struct thermo *t); /* раз в 100 мс */
void thermo_rx(struct thermo *t, char c); /* символ из консоли */
#endif
Дальше: раздел 197 «Мини-проект: частотомер на таймере».