Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32F103 · уровень средний · 8 мин чтения
Открыть в интерактивном справочнике

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; испорчены — заводские

Подключение:

Схема подключения термостата: делитель с NTC на PA0, ключ реле на PB12, USB-UART на PA9 и PA10

Вывод 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 мс.

Логика состояния — три состояния: «ждёт», «нагрев», «авария». Переходы:


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

196.4 Прогон на модели комнаты

Термостат на модели комнаты: температура и включения реле за 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:

Собрано arm-none-eabi-gcc -Os для Cortex-M3, без предупреждений:

Что Байт
вся прошивка (Flash) 1968
ОЗУ (переменные) 64
из них логика thermo.c — код 904
логика — строки сообщений 248

Меньше 2 КБ из 64 — место есть и на журнал измерений, и на дисплей.


196.6 Что улучшить


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

Полные исходники

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 «Мини-проект: частотомер на таймере».

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