@@ -290,97 +290,97 @@ void display_process(therm_settings_t* s
ssd1306_drawlogo();
if(set->val.temp_units == TEMP_UNITS_FAHRENHEIT)
ssd1306_drawstring("Fahrenheit", 1, 60);
else
ssd1306_drawstring("Celsius ", 1, 60);
ssd1306_drawstring("Press to accept", 3, 40);
// Button handler
if(SW_BTN_PRESSED) {
status->state = STATE_SETTEMPOFFSET;
}
else if(!HAL_GPIO_ReadPin(SW_UP)) {
set->val.temp_units = TEMP_UNITS_FAHRENHEIT;
else if(!HAL_GPIO_ReadPin(SW_DOWN)) {
set->val.temp_units = TEMP_UNITS_CELSIUS;
// Event Handler
// N/A
} break;
case STATE_SETTEMPOFFSET:
{
// Write text to OLED
// [ therm :: set temp offset ]
// [ g = 12 ]
ssd1306_drawstring("Temp Cal Offset", 0, 40);
char tempstr[6];
itoa(set->val.temp_offset, tempstr, 10);
ssd1306_drawstring("O=", 1, 45);
ssd1306_drawstring(" ", 1, 57);
ssd1306_drawstring(tempstr, 1, 57);
flash_save(set);
status->state = STATE_IDLE;
else {
user_input_signed((int16_t*)(&set->val.temp_offset));
user_input_signed(&set->val.temp_offset);
case STATE_PREHEAT:
// [ therm : preheating brew ]
// [ 30 => 120 C ]
ssd1306_drawstring("Preheating...", 0, 0);
//ssd1306_drawlogo();
draw_setpoint(status);
status->pid_enabled = 1;
status->setpoint = set->val.setpoint_brew;
user_input((uint16_t*)&set->val.setpoint_brew);
if(status->temp >= status->setpoint) {
status->state = STATE_MAINTAIN;
case STATE_MAINTAIN:
// [ therm : ready to brew ]
ssd1306_drawstring("Preheated!", 0, 0);
@@ -522,49 +522,55 @@ void display_process(therm_settings_t* s
if(last_state != status->state) {
// Clear screen on state change
goto_mode = MODE_HEAT;
trigger_drawsetpoint = 1;
ssd1306_clearscreen();
// Last buttonpress
sw_btn_last = sw_btn;
sw_up_last = sw_up;
sw_down_last = sw_down;
sw_left_last = sw_left;
sw_right_last = sw_right;
static int32_t temp_last = 43002;
static int32_t setpoint_last = 10023;
// Draw current setpoint on display
static void draw_setpoint(therm_status_t* status) {
// FIXME: need to do this when switching modes too
if(status->temp != temp_last || trigger_drawsetpoint) {
char tempstr[3];
itoa_fp(status->temp, status->temp_frac, tempstr);
ssd1306_drawstringbig(" ", 3, 0);
ssd1306_drawstringbig(tempstr, 3, 0);
if(trigger_drawsetpoint)
ssd1306_drawstringbig(">", 3, 74);
if(status->setpoint != setpoint_last || trigger_drawsetpoint) {
itoa(status->setpoint, tempstr, 10);
ssd1306_drawstringbig(" ", 3, 90);
ssd1306_drawstringbig(tempstr, 3, 90);
trigger_drawsetpoint = 0;
setpoint_last = status->setpoint;
temp_last = status->temp;
void display_startup_screen() {
ssd1306_drawstring("therm v0.2", 1, 40);
ssd1306_drawstring("protofusion.org/therm", 3, 0);
// vim:softtabstop=4 shiftwidth=4 expandtab
#ifndef DISPLAY_H
#define DISPLAY_H
#include <stdlib.h>
#include "stm32f0xx_hal.h"
#include "states.h"
#include "ssd1306.h"
#include "stringhelpers.h"
#include "config.h"
#include "syslib.h"
#include "flash.h"
#include "gpio.h"
#ifdef MAX31865_RTD_SENSOR
#include "max31865.h"
#endif
void display_startup_screen();
void display_process(therm_settings_t* set, therm_status_t* status);
// Increase on each press, and increase at a fast rate after duration elapsed of continuously holding down... somehow...
static uint32_t change_time_reset = 0;
// Increment/decrement unsigned variable with up/down buttons
void user_input(uint16_t* to_modify)
if(CHANGE_ELAPSED) {
if(!HAL_GPIO_ReadPin(SW_UP) ) {
CHANGE_RESET;
(*to_modify)++;
else if(!HAL_GPIO_ReadPin(SW_DOWN) && (*to_modify) > 0) {
(*to_modify)--;
// Increment/decrement signed variable with up/down buttons
void user_input_signed(int16_t* to_modify)
void user_input_signed(int32_t* to_modify)
// TODO: Bounds check on int16_t
//fixme: need to cast to 16/32 bits correctly
if (*to_modify < 32768)
if (*to_modify >= -32768)
// Initialize GPIO
void gpio_init(void)
GPIO_InitTypeDef GPIO_InitStruct;
/* GPIO Ports Clock Enable */
__GPIOF_CLK_ENABLE();
__GPIOA_CLK_ENABLE();
__GPIOB_CLK_ENABLE();
__SPI1_CLK_ENABLE();
//////////////////
// PORT F //
// PORTF OUTPUT
// Configure GPIO pin : PF0 [Power LED]
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
// PORTF UNUSED
// Configure GPIO pin : PF1
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
// PORT A //
// PORT A OUTPUT
// Configure GPIO pins : (SSR+ CS_OLED RES D/C CS_MAX)
GPIO_InitStruct.Pin = GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_15;
#ifndef GPIO_H
#define GPIO_H
#include <inttypes.h>
#include "stm32f0xx_hal_conf.h"
#define CHANGE_PERIOD_MS 100
#define CHANGE_ELAPSED (HAL_GetTick() - change_time_reset) > CHANGE_PERIOD_MS
#define CHANGE_RESET change_time_reset = HAL_GetTick()
void user_input(uint16_t* to_modify);
void user_input_signed(int16_t* to_modify);
void user_input_signed(int32_t* to_modify);
void gpio_init(void);
#include "pid.h"
#ifdef MAX31855_TC_SENSOR
#include "max31855.h"
#else
#include "spi.h"
#include "display.h"
#include "usb_device.h"
#include "usbd_cdc_if.h"
therm_settings_t set;
therm_status_t status;
pid_state_t pid_state;
int main(void)
// Initialize HAL
hal_init();
// Configure the system clock
systemclock_init();
// Unset bootloader option bytes (if set)
// FIXME this was never getting called. Try again sometime.
//bootloader_unset();
// Init GPIO
gpio_init();
// Init USB (TODO: Handle plugged/unplugged with external power)
MX_USB_DEVICE_Init();
// set.val.usb_plugged =
// USB startup delay
HAL_Delay(500);
HAL_GPIO_WritePin(LED_POWER, 1);
// Enter into bootloader if up button pressed on boot
if(!HAL_GPIO_ReadPin(SW_UP))
bootloader_enter();
// Init SPI busses
spi_init();
// Init RTD chip
max31865_config(spi_get());
// Init OLED over SPI
ssd1306_init();
// Startup screen
display_startup_screen();
// Default status
status.temp = 0;
status.temp_frac = 0;
status.state_resume = 0;
status.state = STATE_IDLE;
status.setpoint = 70;
status.pid_enabled = 0;
pid_init(&pid_state);
// Go to brew instead of idle if configured thusly
if(set.val.boottobrew)
status.state = STATE_PREHEAT;
HAL_Delay(1000);
// Restore settings from flash memory
flash_restore(&set);
HAL_Delay(2000);
// Soft timers
uint32_t last_ssr_on = 0;
uint32_t last_vcp_tx = 0;
uint32_t last_led = 0;
uint32_t last_pid = 0;
int16_t ssr_output = 0; // Duty cycle of ssr, 0 to SSR_PERIOD
// Main loop
while(1)
// Process sensor inputs
if(HAL_GetTick() - last_led > 400)
last_led = HAL_GetTick();
if((HAL_GetTick() - last_pid > PID_PERIOD))
max31855_readtemp(spi_get(), &set, &status); // Read MAX31855
max31865_readtemp(spi_get(), &set, &status);
if(status.pid_enabled)
// Get ssr output for next time
int16_t power_percent = pid_update(set.val.k_p, set.val.k_i, set.val.k_d, status.temp, status.temp_frac, status.setpoint, &set, &status);
int16_t power_percent = pid_update(&set, &status, &pid_state);
//power-percent is 0-1000?
ssr_output = power_percent; //(((uint32_t)SSR_PERIOD * (uint32_t)10 * (uint32_t)100) * power_percent) / (uint32_t)1000000;
// put ssr output on display
ssd1306_drawstring(" ", 0, 90); //fixme: this is bad, but I can't get the old digits to clear otherwise
itoa(ssr_output, tempstr, 10);
ssd1306_drawstring(tempstr, 0, 90);
ssr_output = 0;
last_pid = HAL_GetTick();
// Kill SSR once the desired on-time has elapsed
if(HAL_GetTick() - last_ssr_on > ssr_output || ssr_output <= 0)
HAL_GPIO_WritePin(SSR_PIN, 0);
HAL_GPIO_WritePin(LED_POWER, 0);
// Every 200ms, set the SSR on unless output is 0
if(HAL_GetTick() - last_ssr_on > SSR_PERIOD)
// Only support heating (ssr_output > 0) right now
if(ssr_output > 0)
HAL_GPIO_WritePin(SSR_PIN, 1);
last_ssr_on = HAL_GetTick();
// Make sure everything is off
// Transmit temperature over USB-CDC on a regulat basis
// Transmit temperature over USB-CDC on a regular basis
if(HAL_GetTick() - last_vcp_tx > VCP_TX_FREQ)
// Print temp to cdc
char tempstr[16];
itoa_fp(status.temp, status.temp_frac, tempstr);
uint8_t numlen = strlen(tempstr);
tempstr[numlen] = '\r';
tempstr[numlen+1] = '\n';
// if(set.val.usb_plugged)
// CDC_Transmit_FS(tempstr, numlen+2);
// while(CDC_Transmit_FS("\r\n", 2) == USBD_BUSY);
last_vcp_tx = HAL_GetTick();
// Run state machine
display_process(&set, &status);
@@ -8,104 +8,98 @@
#define MAX31865_REG_HFAULT_THRESH_MSB 0x03
#define MAX31865_REG_HFAULT_THRESH_LSB 0x04
#define MAX31865_REG_LFAULT_THRESH_MSB 0x05
#define MAX31865_REG_LFAULT_THRESH_LSB 0x06
#define MAX31865_REG_FAULTSTATUS 0x07
#define MAX31865_REG_WRITE_MODIFIER 0x80
// Fields
#define MAX31865_CONF_VBIAS (1<<7)
#define MAX31865_CONF_AUTO_CONVERT (1<<6)
#define MAX31865_CONF_1SHOT (1<<5)
#define MAX31865_CONF_3WIRE (1<<4)
#define MAX31865_CONF_FAULT_NOACTION 0b0000
#define MAX31865_CONF_FAULT_AUTODELAY 0b0100
#define MAX31865_CONF_FAULT_MANUALELAY1 0b1000
#define MAX31865_CONF_FAULT_MANUALELAY2 0b1100
#define MAX31865_CONF_FAULT_CLEAR (1<<1)
#define MAX31865_CONF_50_60HZ_FILTER (1<<0)
void max31865_config(SPI_HandleTypeDef* hspi1)
uint8_t config = 0x00;
config |= MAX31865_CONF_VBIAS;
config |= MAX31865_CONF_AUTO_CONVERT;
config |= MAX31865_CONF_FAULT_CLEAR;
uint8_t reg = (MAX31865_REG_WRITE_MODIFIER | MAX31865_REG_CONFIG);
// Assert CS
HAL_GPIO_WritePin(MAX_CS, 0);
HAL_SPI_Transmit(hspi1, ®, 1, 100);
HAL_SPI_Transmit(hspi1, &config, 1, 100);
// Release CS
HAL_GPIO_WritePin(MAX_CS, 1);
void max31865_clear_errors(SPI_HandleTypeDef* hspi1) {
max31865_config(hspi1);
// Grab temperature reading from MAX31865
void max31865_readtemp(SPI_HandleTypeDef* hspi1, therm_settings_t* set, therm_status_t* status)
///////////////////////////////////
// This is duplicated from MAX31855, update for MAX31865 registers/etc
// TODO: Set configuration register based on params in config.h (2-wire, 4-wire, etc RTD). This is register 0x00.
// 2-wire RTC or 2-wire (duh) NTC thermistor will be the only options
// Need option for resistance of RTD
// These options should be stored in the set structure and should be menu-selectable
// TODO: Set RTD ref resistance in set struct
uint8_t regh = MAX31865_REG_RTD_MSB;
uint8_t rxdatah[1] = {0x00};
uint8_t rxdatal[1] = {0x00};
HAL_SPI_Transmit(hspi1, ®h, 1, 100);
HAL_SPI_Receive(hspi1, rxdatah, 1, 100);
HAL_SPI_Receive(hspi1, rxdatal, 1, 100);
// Assemble data array into one var
uint16_t adc_count = ((rxdatah[0] & 0x7F) << 8) | rxdatal[0];
if((rxdatah[0] & 0x80) && !set->val.ignore_error) {
uint8_t reg = MAX31865_REG_FAULTSTATUS;
uint8_t data[1] = {0x00};
HAL_SPI_Receive(hspi1, data, 1, 100);
status->error_code = data[0];
HAL_Delay(400); // FIXME: remove?
status->state_resume = status->state;
status->state = STATE_TC_ERROR;
status->temp = 0;
status->temp_frac = 0;
// Convert to Fahrenheit
// PID implementation
// TODO: Make struct that has the last_temp and i_state in it, pass by ref. Make struct that has other input values maybe.
static int16_t last_pid_temp = 0;
static uint8_t last_pid_temp_frac = 0;
static int32_t i_state = 0;
int16_t pid_update(uint16_t k_p, uint16_t k_i, uint16_t k_d, int16_t temp, uint8_t temp_frac, int16_t setpoint, therm_settings_t* set, therm_status_t* status)
void pid_init(pid_state_t* state)
state->i_state = 0;
state->last_pid_temp = 0;
state->last_pid_temp_frac = 0;
int16_t pid_update(therm_settings_t* set, therm_status_t* status, pid_state_t *state)
// Calculate instantaneous error
int16_t error = setpoint - temp; // TODO: Use fixed point fraction
int16_t error = status->setpoint - status->temp; // TODO: Use fixed point fraction
// Proportional component
int32_t p_term = k_p * error;
int32_t p_term = set->val.k_p * error;
// Error accumulator (integrator)
i_state += error;
state->i_state += error;
// to prevent the iTerm getting huge from lots of
// error, we use a "windup guard"
// (this happens when the machine is first turned on and
// it cant help be cold despite its best efforts)
// not necessary, but this makes windup guard values
// relative to the current iGain
int32_t windup_guard_res = set->val.windup_guard / k_i;
int32_t windup_guard_res = set->val.windup_guard / set->val.k_i;
// Calculate integral term with windup guard
if (i_state > windup_guard_res)
i_state = windup_guard_res;
else if (i_state < -windup_guard_res)
i_state = -windup_guard_res;
if (state->i_state > windup_guard_res)
state->i_state = windup_guard_res;
else if (state->i_state < -windup_guard_res)
state->i_state = -windup_guard_res;
int32_t i_term = k_i * i_state;
int32_t i_term = set->val.k_i * state->i_state;
// Calculate differential term (slope since last iteration)
int32_t d_term = (k_d * (status->temp - last_pid_temp));
int32_t d_term = (set->val.k_d * (status->temp - state->last_pid_temp));
// Save temperature for next iteration
last_pid_temp = status->temp;
last_pid_temp_frac = status->temp_frac;
state->last_pid_temp = status->temp;
state->last_pid_temp_frac = status->temp_frac;
int16_t result = p_term + i_term - d_term;
// Put out tenths of percent, 0-1000.
if(result > 1000)
result = 1000;
else if(result < -1000)
result = -1000;
// Return feedback
return result;
#ifndef PIDS_H
#define PIDS_H
int16_t pid_update(uint16_t k_p, uint16_t k_i, uint16_t k_d, int16_t temp, uint8_t temp_frac, int16_t setpoint, therm_settings_t* set, therm_status_t* status);
typedef struct {
int16_t last_pid_temp;
uint8_t last_pid_temp_frac;
int32_t i_state;
} pid_state_t;
void pid_init(pid_state_t* state);
int16_t pid_update(therm_settings_t* set, therm_status_t* status, pid_state_t* state);
Status change: