Embedded HandbookApps
All sections › XXV. Mini-projects
STM32F103 · level intermediate · 8 min read
Open in the interactive handbook

196. Mini-project: a thermostat on the BluePill

The 🔧 recipes in the sections are building bricks: an LED, the ADC, the UART, writing to Flash. A mini-project is a finished device made of several bricks, with everything that sets a device apart from an example: it survives a sensor failure, does not click its relay like a machine gun, remembers its settings after power-off and answers on the console.

The first device is a thermostat: a thermistor on the ADC, a heater relay, a UART console, settings in Flash. And the main point of the project's structure: the device logic knows nothing about registers. Everything hardware-related is in one platform file; that is why the same logic can be run on a computer together with a model of the room, and you can see how the device behaves over four hours before you have even built the board.

The project files are in _verify/proj196/: thermo.h, thermo.c (logic), plat_f103.c (the BluePill platform), sim.c (the model platform). The firmware is built for Cortex-M3, the model is run on a computer. Checked by _verify/proj196_check.py.

Terms used in the documentation

Term Meaning
thermostat, bang-bang control a thermostat, on/off control
hysteresis, deadband hysteresis, a band where the output does not react
NTC thermistor, B-constant a thermistor with a negative temperature coefficient, the B constant
minimum on/off time the shortest time between switchings
platform layer, hardware abstraction the platform layer: everything that touches the hardware
host-based test, plant model a run on the computer, a model of the controlled object

196.1 What the device does

Function How
measure the temperature a 10 kΩ NTC in a divider on PA0, the ADC, 16 samples, a table with interpolation
hold the setpoint turn heating on at T ≤ setpoint − 0.5 °C, turn it off at T ≥ setpoint + 0.5 °C
protect the relay switch no more often than once every 60 s
survive a sensor failure no plausible reading for 3 s — turn heating off, LED on PC13, АВАРИЯ ("alarm") on the console
listen to the console get, set 22.5, hyst 0.3 over USART1, 115200 baud
remember the settings the last Flash page (0x0800FC00), under a CRC-16; if corrupted — factory defaults

Wiring:

Thermostat wiring: an NTC divider on PA0, the relay switch on PB12, USB-UART on PA9 and PA10

BluePill pin Connected to
PA0 the divider midpoint: a 10 kΩ NTC to 3.3 V, 10 kΩ to ground
PB12 the base of the relay switch transistor (section 52), a diode across the coil
PC13 the board LED: lit means alarm
PA9 / PA10 USART1 TX / RX → a USB-UART adapter

196.2 Program structure: logic separate from hardware

The logic talks to the world only through six platform functions:

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);

On the board they are implemented by plat_f103.c on registers, on the computer by sim.c with the room model. The logic (thermo.c) is one and the same, without a single line written separately for the computer. This is the same technique as in section 184: a module that can be tested without a board.

The main loop on the board is simple, with no RTOS: thermo_tick every 100 ms, every received character goes to thermo_rx. Time comes from SysTick, 1 ms.

The state logic has three states: "idle", "heating", "alarm". Transitions:


196.3 The sensor: a table instead of a logarithm

The resistance of an NTC depends on temperature exponentially, and the exact formula needs a logarithm. On a Cortex-M3 without an FPU that is expensive, so the temperature is computed from a table of 17 points (every 5 °C from −20 to 60) with linear interpolation between them. The table was computed by ntc_table.py from the formula with the constant B = 3950:

°C −20 0 25 50 60
ADC code 355 939 2048 3014 3280

196.4 A run on the room model

The thermostat on the room model: temperature and relay switchings over 4 hours

The model: the room cools towards an outdoor 5 °C with a time constant of 30 minutes, the heater could raise it to 35 °C; the sensor has noise of ±0.1 °C. The step is 100 ms, four hours. The scenario: start at 15 °C; after an hour, get and set 22.5 on the console; after two hours the sensor disappears for a minute; at the end, corrupted settings and invalid commands.

What Result
time from start to 21 °C 642 s
swing in steady state (setpoint 21.0) 20.65…21.35 °C
heating cycle period 169 s
relay switchings in the first hour 36
reaction to a sensor failure heating off after 3.1 s
heater on 59.6 % of the time
setpoint in "Flash" after set 22.5 225, CRC matches
after corrupting one bit of the settings setpoint 210 — factory default

The swing is narrower than the set band of ±0.5 °C: the relay switches on the noisy reading, which every now and then jumps to the threshold 0.15 °C early: 0.1 is noise, 0.05 is rounding to tenths.

The console during the run:

cfg: заводские
t=21.2 set=21.0 ждёт
ok
АВАРИЯ: датчик
датчик в норме
t=22.2 set=22.5 ждёт

Invalid commands (set 99, hyst abc) got a hint; a line longer than 31 characters was discarded with a message.

CRC check. The function thermo_crc16 on the string 123456789 gives 0x29B1 — the check value of CRC-16/CCITT-FALSE: the implementation is correct.


196.5 Firmware for the BluePill

The plat_f103.c platform works on registers through the CMSIS header for the F103:

Built with arm-none-eabi-gcc -Os for Cortex-M3, with no warnings:

What Bytes
whole firmware (Flash) 1968
RAM (variables) 64
of which the thermo.c logic — code 904
logic — message strings 248

Less than 2 KB out of 64 — there is room both for a measurement log and for a display.


196.6 What to improve


196.7 Pitfalls

The relay clicks on every bit of noise. There is no hysteresis, or the band is narrower than the sensor noise. In the model, the noise of ±0.1 °C and the rounding of the reading to tenths (another 0.05 °C) together shift the thresholds by 0.15 °C: heating turns on already at 20.65 °C and turns off at 21.35 °C — a swing of ±0.35 instead of the set ±0.5.

The relay "stuck" in the on position when the sensor wire broke. The failure was not recognised: on an open circuit the ADC code is at the edge of the range, and it must be treated as an alarm, not as −20 °C.

Switching off on alarm waits 60 seconds. The relay protection against frequent switching was also applied to the alarm. An alarm switches off at once.

After reflashing — cfg: заводские ("factory defaults"). Flashing with a full chip erase also erased the settings page. To keep them, erase only the pages occupied by the program.

The settings "got lost" after a power loss during a write. The page was already erased, but the new data had not been written yet. The CRC catches this — you get the factory defaults; more reliable is two pages used in turn (like the metadata in section 195).

The console "hung". A line without a newline keeps accumulating; if it is longer than the buffer, throw it away and say so.


196.8 Summary

Full sources

thermo.h — the interface of the device logic and of the platform functions.

/* Мини-проект «Термостат»: логика прибора без железа.
 * Всё, что касается выводов, датчика, времени и памяти настроек, — в функциях платформы (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

Next: section 197 "Mini-project: a frequency counter on a timer".

Open in the interactive handbook