stm32f1/application.c

1326 lines
56 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** firmware for ThermoHybaid MBS 0.2G MBLK002 thermo-cycler replacement controller board
* @file
* @author King Kévin <kingkevin@cuvoodoo.info>
* @copyright SPDX-License-Identifier: GPL-3.0-or-later
* @date 2016-2020
*/
/* standard libraries */
#include <stdint.h> // standard integer types
#include <stdlib.h> // standard utilities
#include <string.h> // string utilities
#include <time.h> // date/time utilities
#include <ctype.h> // utilities to check chars
#include <math.h> // NAN definition
/* STM32 (including CM3) libraries */
#include <libopencmsis/core_cm3.h> // Cortex M3 utilities
#include <libopencm3/cm3/scb.h> // vector table definition
#include <libopencm3/cm3/nvic.h> // interrupt utilities
#include <libopencm3/stm32/gpio.h> // general purpose input output library
#include <libopencm3/stm32/rcc.h> // real-time control clock library
#include <libopencm3/stm32/exti.h> // external interrupt utilities
#include <libopencm3/stm32/rtc.h> // real time clock utilities
#include <libopencm3/stm32/iwdg.h> // independent watchdog utilities
#include <libopencm3/stm32/dbgmcu.h> // debug utilities
#include <libopencm3/stm32/desig.h> // design utilities
#include <libopencm3/stm32/flash.h> // flash utilities
#include <libopencm3/stm32/adc.h> // ADC utilities
#include <libopencm3/stm32/timer.h> // timer utilities
/* own libraries */
#include "global.h" // board definitions
#include "print.h" // printing utilities
#include "usb_cdcacm.h" // USB CDC ACM utilities
#include "terminal.h" // handle the terminal interface
#include "menu.h" // menu utilities
#include "oled_text.h" // utilities to display text on OLED
#include "sensor_max1247.h" // to read the thermistor ADC values
#include "sensor_ds18b20.h" // to read temperature from a DS18B20
/** watchdog period in ms */
#define WATCHDOG_PERIOD 10000
/** set to 0 if the RTC is reset when the board is powered on, only indicates the uptime
* set to 1 if VBAT can keep the RTC running when the board is unpowered, indicating the date and time
*/
#define RTC_DATE_TIME 0
/** number of RTC ticks per second
* @note use integer divider of oscillator to keep second precision
*/
#define RTC_TICKS_SECOND 8
/** RTC time when device is started */
static time_t time_start = 0;
/** @defgroup main_flags flag set in interrupts to be processed in main task
* @{
*/
static volatile uint32_t rtc_internal_tick_flag = 0; /**< set with time when internal RTC ticked */
static volatile bool rtc_internal_second_flag = false; /**< set when a second passed */
/** @} */
#define BED_PIN_393A PB3 /**< pin connected to LN393 output A */
#define BED_PIN_3393 PB4 /**< pin connected to ST339 output 3 */
#define BED_PIN_LK1 PB5 /**< pin connected to link 1 */
#define BED_PIN_LK2 PC14 /**< pin connected to link 2 */
#define BED_PIN_LK3 PC15 /**< pin connected to link 3 */
#define BED_PIN_LK4 PB1 /**< pin connected to link 4 */
#define LID_TEC_CHANNEL 0 /**< PA0/ADC12_CH0 is connected to the 12 kOhm thermistor in the lid heater */
#define LID_HEATER_PIN PA10 /**< pin to optocoupler cathode controlling triac to lid heater */
#define LID_HEATER_TIMER 1 /**< timer connected to lid heater pin */
#define LID_HEATER_CHANNEL 3 /**< timer channel connected to lid heater pin */
#define LID_HEATER_OC TIM_OC3 /**< output compare for timer channel connected to lid heater pin */
#define MBLK019_CH26_PIN PA1 /**< to control TR2/6 for the peltier elements */
#define MBLK019_CH14_PIN PA2 /**< to control TR1/4 for the peltier elements */
#define MBLK019_CH35_PIN PA4 /**< to control TR3/5 for the peltier elements */
#define MBLK019_PRESENCE_PIN PA3 /**< connected to ground when MBLK019 board is present */
#define CONTROL_PLAY_GREEN_LED_PIN PA5 /**< to control green LED of play/pause indicator (active high) */
#define led_cool_on() gpio_set(GPIO_PORT(CONTROL_PLAY_GREEN_LED_PIN), GPIO_PIN(CONTROL_PLAY_GREEN_LED_PIN)) /**< switch green play/pause LED on */
#define led_cool_off() gpio_clear(GPIO_PORT(CONTROL_PLAY_GREEN_LED_PIN), GPIO_PIN(CONTROL_PLAY_GREEN_LED_PIN)) /**< switch green play/pause LED off */
#define CONTROL_PLAY_ORANGE_LED_PIN PA6 /**< to control orange LED of play/pause indicator (active high) */
#define led_heat_on() gpio_set(GPIO_PORT(CONTROL_PLAY_ORANGE_LED_PIN), GPIO_PIN(CONTROL_PLAY_ORANGE_LED_PIN)) /**< switch orange play/pause LED on */
#define led_heat_off() gpio_clear(GPIO_PORT(CONTROL_PLAY_ORANGE_LED_PIN), GPIO_PIN(CONTROL_PLAY_ORANGE_LED_PIN)) /**< switch orange play/pause LED off */
#define CONTROL_POWER_RED_LED_PIN PA7 /**< to control red LED of power indicator (active high) */
#define led_power_on() gpio_set(GPIO_PORT(CONTROL_POWER_RED_LED_PIN), GPIO_PIN(CONTROL_POWER_RED_LED_PIN)) /**< switch power LED on */
#define led_power_off() gpio_clear(GPIO_PORT(CONTROL_POWER_RED_LED_PIN), GPIO_PIN(CONTROL_POWER_RED_LED_PIN)) /**< switch power LED off */
#define CONTROL_PLAY_BUTTON_LED_PIN PB0 /**< to read play/pause button (connected to ground when pressed) */
#define TEC_POWER_YELLOW PB11 /**< pin to choose which power rail to connect to yellow TEC power input (high = VCC, low = GND) */
#define tec_power_yellow_on() gpio_set(GPIO_PORT(TEC_POWER_YELLOW), GPIO_PIN(TEC_POWER_YELLOW)) // allow connecting yellow to VCC
#define tec_power_yellow_off() gpio_clear(GPIO_PORT(TEC_POWER_YELLOW), GPIO_PIN(TEC_POWER_YELLOW)) // allow connect yellow to GND
#define TEC_POWER_ORANGE PB10 /**< pin to choose which power rail to connect to orange TEC power input (high = VCC, low = GND) */
#define tec_power_orange_on() gpio_set(GPIO_PORT(TEC_POWER_ORANGE), GPIO_PIN(TEC_POWER_ORANGE)) // allow connect orange to VCC
#define tec_power_orange_off() gpio_clear(GPIO_PORT(TEC_POWER_ORANGE), GPIO_PIN(TEC_POWER_ORANGE)) // allow connect orange to GND
#define TEC_POWER_PWM PB9 /**< pin to actually let power go through TECs, where PWM can be used (active high) */
#define tec_power_pwm_on() gpio_set(GPIO_PORT(TEC_POWER_PWM), GPIO_PIN(TEC_POWER_PWM)) /**< connect yellow/orange wire as set */
#define tec_power_pwm_off() gpio_clear(GPIO_PORT(TEC_POWER_PWM), GPIO_PIN(TEC_POWER_PWM)) /**< disconnect yellow/orange wire */
#define TEC_POWER_TIMER 4 /**< timer connected to pin */
#define TEC_POWER_CHANNEL 4 /**< timer channel connected to pin */
#define TEC_POWER_OC TIM_OC4 /**< timer output compare connected to pin */
#define HEATSINK_FAN_PIN PA15 /**< pin to switch the nMOS to control the fan cooling the bad heatsink, low to disable, pulled up externally, must be 5V tolerant */
#define heatsink_fan_on() gpio_set(GPIO_PORT(HEATSINK_FAN_PIN), GPIO_PIN(HEATSINK_FAN_PIN)) /**< switch fan on, cooling the bed heat sink */
#define heatsink_fan_off() gpio_clear(GPIO_PORT(HEATSINK_FAN_PIN), GPIO_PIN(HEATSINK_FAN_PIN)) /**< switch fan off, when the bed is not used */
static bool led_power_blink = false; /**< remember we are blinking the power LED */
static bool led_heat_blink = false; /**< remember we are blinking the orange play/pause LED */
static bool led_cool_blink = false; /**< remember we are blinking the green play/pause LED */
const uint8_t channels[] = {ADC_CHANNEL17, ADC_CHANNEL(LID_TEC_CHANNEL)}; /**< voltages to convert (channel 17 = internal voltage reference) */
static bool ds18b20_present = false; /**< if DS18B20 temperature sensor is present */
/** target temperature to be reached by the lid */
static float lid_target = NAN;
/** target temperature to be reached by the bed */
static uint16_t bed_target = 0;
/** the current state of the thermo-cycler */
enum state_e {
STATE_IDLE, /**< doing nothing, waiting for a command */
STATE_SAFE, /**< safe state entered, probably because of an error */
STATE_HEAT, /**< simply heat up bed */
STATE_COOL, /**< simply cool down bed */
STATE_FAN, /**< simply use fan to set to ambient temperature */
STATE_PREPARE, /**< heat up for initialisation/first denaturation */
STATE_INITIALISATION, /**< first (longer) denaturation phase */
STATE_TO_DENATURATION, /**< transition to denaturation phase */
STATE_DENATURATION, /**< denaturation phase */
STATE_TO_ANNEALING, /**< transition to annealing phase */
STATE_ANNEALING, /**< annealing phase */
STATE_TO_EXTENSTION, /**< transition to extension phase */
STATE_EXTENSION, /**< extension phase */
STATE_TO_HOLD, /**< transition to final hold */
STATE_HOLD, /**< final hold */
} state = STATE_IDLE;
/** name of the states */
const char* state_names[] = {
[STATE_IDLE] = "ready",
[STATE_SAFE] = "safe",
[STATE_HEAT] = "heating",
[STATE_COOL] = "cooling",
[STATE_FAN] = "fanning",
[STATE_PREPARE] = ">initialisation",
[STATE_INITIALISATION] = "initialisation",
[STATE_TO_DENATURATION] = ">denaturation",
[STATE_DENATURATION] = "denaturation",
[STATE_TO_ANNEALING] = ">annealing",
[STATE_ANNEALING] = "annealing",
[STATE_TO_EXTENSTION] = ">extension",
[STATE_EXTENSION] = "extension",
// there is an optional final extension
[STATE_TO_HOLD] = ">final hold",
[STATE_HOLD] = "final hold",
};
/** set if an error or anomaly has been encountered */
static char* error = NULL;
size_t putc(char c)
{
size_t length = 0; // number of characters printed
static char last_c = 0; // to remember on which character we last sent
if ('\n' == c) { // send carriage return (CR) + line feed (LF) newline for each LF
if ('\r' != last_c) { // CR has not already been sent
usb_cdcacm_putchar('\r'); // send CR over USB
length++; // remember we printed 1 character
}
}
usb_cdcacm_putchar(c); // send byte over USB
length++; // remember we printed 1 character
last_c = c; // remember last character
return length; // return number of characters printed
}
/** enter in safe state mode
* @note useful when an error occurred or an anomaly has been detected
*/
static void safe_state(void)
{
// this is the safest configuration of TEC switching (in cooling mode it switches all off, in heating mode it only heats mildly the top part
gpio_set(GPIO_PORT(MBLK019_CH26_PIN), GPIO_PIN(MBLK019_CH26_PIN)); // don't sink current (e.g. not powering the opto-coupler/transistor)
gpio_set(GPIO_PORT(MBLK019_CH14_PIN), GPIO_PIN(MBLK019_CH14_PIN)); // don't sink current (e.g. not powering the opto-coupler/transistor)
gpio_set(GPIO_PORT(MBLK019_CH35_PIN), GPIO_PIN(MBLK019_CH35_PIN)); // don't sink current (e.g. not powering the opto-coupler/transistor)
// take control over the H-bridge and switch it off
rcc_periph_clock_enable(GPIO_RCC(TEC_POWER_PWM)); // enable clock for GPIO port peripheral
gpio_set_mode(GPIO_PORT(TEC_POWER_PWM), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO_PIN(TEC_POWER_PWM)); // set pin as output
tec_power_pwm_off(); // switch off power
tec_power_yellow_off(); // connect wire to ground
tec_power_orange_off(); // connect wire to ground
// disable heater lid
rcc_periph_clock_enable(GPIO_RCC(LID_HEATER_PIN)); // enable clock for GPIO port peripheral
gpio_set_mode(GPIO_PORT(LID_HEATER_PIN), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO_PIN(LID_HEATER_PIN)); // set pin back as output open-drain
gpio_set(GPIO_PORT(LID_HEATER_PIN), GPIO_PIN(LID_HEATER_PIN)); // don't sink current, not powering the opto-coupler and triac
heatsink_fan_off(); // bed is not active, so we can stop the fan, since we want to stop drawing power and having spinning things
led_heat_blink = false; // stop blinking LED
led_heat_off(); // switch off LED
led_cool_blink = false; // stop blinking LED
led_cool_off(); // switch off LED
if (error) {
led_power_blink = false; // start blinking red LED to indicate error
oled_text_line("error", 2);
oled_text_update();
}
}
/** get temperature of lid (in °C)
* @return lid temperature
*/
static float lid_temperature(void)
{
// read lid temperature using ADC
ADC_SR(ADC1) = 0; // reset flags
uint16_t adc_values[LENGTH(channels)];
float voltages[LENGTH(channels)];
for (uint8_t i = 0; i < LENGTH(channels); i++) {
adc_start_conversion_regular(ADC1); // start conversion (using trigger)
while (!adc_eoc(ADC1)); // wait until conversion finished
adc_values[i] = adc_read_regular(ADC1); // read voltage value (clears flag)
voltages[i] = adc_values[i] * 1.2 / adc_values[0]; // use 1.2V internal voltage reference to get ADC voltage
}
//return voltages[1];
// convert to °C
// calibrated using a DS18B20 (accuracy = +- 0.5°C), with 12-bit precision
// 2.3491 V = 21.500 C, 0.1337 V = 104.9 °C
return -37.6456 * voltages[1] + 109.933;
}
/** set the power delivered to the lid heater
* @param[in] percent power (e.g. duty cycle) in %
* @note we use % since we control the duty cycle of a 1s period over a triac (e.g. 100 Hz control)
*/
static void lid_power(uint8_t percent)
{
if (STATE_SAFE == state && 0 != percent) {
puts("can't set lid power in safe state\n");
return;
}
if (0 == percent) {
timer_set_oc_value(TIM(LID_HEATER_TIMER), LID_HEATER_OC, 0); // duty cycle to 0%, to switch off heater
} else if (percent >= 100) {
timer_set_oc_value(TIM(LID_HEATER_TIMER), LID_HEATER_OC, UINT16_MAX); // duty cycle to 100%, to switch completely on heater
} else {
timer_set_oc_value(TIM(LID_HEATER_TIMER), LID_HEATER_OC, UINT16_MAX / 100 * percent - 1); // set duty cycle
}
}
/** run PID control for lid temperature */
static void lid_pid(void)
{
// I tried ZieglerNichols method, but it overshoots and oscillates far too much (even with the no overshoot rule)
//#define LID_KU 64
//#define LID_TU 14.96
//#define LID_KP (0.2 * LID_KU)
//#define LID_KI (0.4 * LID_KU / LID_TU)
//#define LID_KD (0.066 * LID_KU * LID_TU)
#define LID_KP 24.0
#define LID_KI 0.0
#define LID_KD 0.0
if (isnan(lid_target)) { // no target has been defined
lid_power(0);
// reinitialise errors
return;
}
static float error_sum = 0.0; // value used for the integral part
static float error_prev = 0.0; // value used for the derivate part
const float error_cur = lid_target - lid_temperature(); // get current error
error_sum += error_cur;
const float error_diff = error_cur - error_prev;
error_prev = error_cur;
float power = LID_KP * error_cur + LID_KI * error_sum + LID_KD * error_diff; // calculate needed power
// enforce limits
if (power < 0.0) {
power = 0.0;
} else if (power > 50.0) { // limit power to 50%, else it gets too warm too quick and could damage the hardware
power = 50.0;
}
lid_power((uint8_t)power); // set power
}
static void tec_power(uint16_t duty_cycle)
{
if (STATE_SAFE == state && 0 != duty_cycle) { // don't allow setting in save state (except switching off)
puts("can't set TEC power in safe state\n");
return;
}
timer_set_oc_value(TIM(TEC_POWER_TIMER), TEC_POWER_OC, duty_cycle); // duty cycle to 0%, to switch off heater
// ensure the fan is on when there is power
if (duty_cycle) {
heatsink_fan_on();
}
}
/** set TEC to heat */
static void tec_heat(void)
{
tec_power(0); // ensure power is off while switching
sleep_ms(2); // wait for the PWM to take effect
tec_power_orange_off(); // connect GND from orange TEC line
tec_power_yellow_on(); // connect VCC to yellow TEC line
// set TEC configuration to heat the top half at max
// the following heating configurations exist (when orange is connected to minus and yellow to plus)
// 26 14 35 top bott A@3V
// -- -- -- heat off 0.6
// ++ -- -- heat heat 0.6
// -- ++ -- heat heat 0.8
// -- -- ++ off heat 0.8
// -- ++ ++ off heat 1.1
// ++ -- ++ off heat 1.1
// ++ ++ -- heat heat 0.8
// ++ ++ ++ off heat 1.5
gpio_clear(GPIO_PORT(MBLK019_CH26_PIN), GPIO_PIN(MBLK019_CH26_PIN)); // sink current, powering the opto-coupler, switching the transistor on
gpio_clear(GPIO_PORT(MBLK019_CH14_PIN), GPIO_PIN(MBLK019_CH14_PIN)); // sink current, powering the opto-coupler, switching the transistor on
gpio_clear(GPIO_PORT(MBLK019_CH35_PIN), GPIO_PIN(MBLK019_CH35_PIN)); // sink current, powering the opto-coupler, switching the transistor on
}
/** set TEC to cool */
static void tec_cool(void)
{
tec_power(0); // ensure power is off while switching
sleep_ms(2); // wait for the PWM to take effect
tec_power_yellow_off(); // connect GND from yellow TEC line
tec_power_orange_on(); // connect VCC to orange TEC line
// set TEC configuration to cool the top half at max
// the following heating configurations exist (when orange is connected to minus and yellow to plus)
// 26 14 35 top bot A@3V
// -- -- -- off off 0.0
// ++ -- -- off heat 0.6
// -- ++ -- off cool 0.8
// -- -- ++ off off 0.0
// -- ++ ++ cool cool 1.5
// ++ -- ++ off heat 1.5
// ++ ++ -- cool off 1.4
// ++ ++ ++ cool heat 2.1
// cool only top at max power
gpio_clear(GPIO_PORT(MBLK019_CH26_PIN), GPIO_PIN(MBLK019_CH26_PIN)); // sink current, powering the opto-coupler, switching the transistor on
gpio_clear(GPIO_PORT(MBLK019_CH14_PIN), GPIO_PIN(MBLK019_CH14_PIN)); // sink current, powering the opto-coupler, switching the transistor on
gpio_set(GPIO_PORT(MBLK019_CH35_PIN), GPIO_PIN(MBLK019_CH35_PIN)); // don't sink current, not powering the opto-coupler, switching the transistor off
// cool top and bottom, sharing the load
//gpio_set(GPIO_PORT(MBLK019_CH26_PIN), GPIO_PIN(MBLK019_CH26_PIN));
//gpio_clear(GPIO_PORT(MBLK019_CH14_PIN), GPIO_PIN(MBLK019_CH14_PIN));
//gpio_clear(GPIO_PORT(MBLK019_CH35_PIN), GPIO_PIN(MBLK019_CH35_PIN));
}
/** read bed top half temperature
* @return temperature in °C
*/
static float bed_tophalf_temperature(void)
{
const uint16_t measurement = sensor_max1247_read(0); // read measured value from corresponding thermistor
// convert to °C
// calibrated using a DS18B20 (accuracy = +- 0.5°C), with 12-bit precision
// 558 = 19.500 °C, 616 = 21.8 °C, 1624 = 50.625 °C, 2850 = 82.250 °C, 2926 = 83.625 °C
return measurement * 0.0270798 + 4.38946;
}
/** read bed bottom half temperature
* @return temperature in °C
*/
static float bed_bothalf_temperature(void)
{
const uint16_t measurement = sensor_max1247_read(1); // read measured value from corresponding thermistor
// convert to °C
// calibrated using a DS18B20 (accuracy = +- 0.5°C), with 12-bit precision
// 549 = 19.312 °C, 12 = 63.3 °C
return measurement * 0.0270798 + 4.38946;
}
/** read bed tube temperature
* @return temperature in °C
*/
static float bed_tube_temperature(void)
{
const uint16_t measurement = sensor_max1247_read(3); // read measured value from corresponding thermistor
// convert to °C
// calibrated using a TP101 with 0.1 °C precision (unknown accuracy but is on par with DS18B20)
// 557 = 19.5 °C, 3221 = 100.9 °C
return measurement * 0.0305556 + 2.48056;
}
/** read heat sink temperature
* @return temperature in °C
*/
static float bed_heatsink_temperature(void)
{
const uint16_t measurement = sensor_max1247_read(2); // read measured value from corresponding thermistor
// convert to °C
// calibrated using a DS18B20 (accuracy = +- 0.5°C), with 12-bit precision
// 557 = 19.312 °C, 1624 = 50.625 °C
return measurement * 0.0273778 + 4.22317;
}
/** run PID control for bed temperature (using the top half) */
static void bed_pid(void)
{
// I tried ZieglerNichols method, but it overshoots and oscillates far too much (even with the no overshoot rule)
//#define LID_KU 64
//#define LID_TU 14.96
//#define LID_KP (0.2 * LID_KU)
//#define LID_KI (0.4 * LID_KU / LID_TU)
//#define LID_KD (0.066 * LID_KU * LID_TU)
#define BED_KP 256
#define BED_KI 0
#define BED_KD 0
if (0 == bed_target) { // no target has been defined
tec_power(0);
// reinitialise errors
return;
}
static int32_t error_sum = 0; // value used for the integral part
static int32_t error_prev = 0; // value used for the derivate part
const uint16_t temperature = sensor_max1247_read(0); // read top half sensor
const int32_t error_cur = adds32_safe(bed_target, -temperature);
error_sum = adds32_safe(error_sum, error_cur);
const int32_t error_diff = adds32_safe(error_cur, -error_prev);
error_prev = error_cur;
const int32_t p = BED_KP * error_cur; // calculate proportional part
const int32_t i = BED_KI * error_sum; // calculate integral part
const int32_t d = BED_KD * error_diff; // calculate derivate part
int32_t power = adds32_safe(adds32_safe(p, i), d); // calculate needed power
if (STATE_COOL == state) {
power = -power;
}
// enforce limits
if (power < 0) {
power = 0;
} else if (power > UINT16_MAX ) {
power = UINT16_MAX;
}
printf("set: %u is: %u %.02f p: %d i: %d d: %d power: %d\n", bed_target, temperature, bed_tophalf_temperature(), p, i, d, power);
tec_power((uint16_t)power); // set power
}
/** set new state
* @param[in] new new state to set
*/
static void set_state(enum state_e new)
{
if (STATE_SAFE == state && STATE_SAFE != new) {
puts("restart controller to exit safe state\n");
return;
}
switch (new) {
case STATE_IDLE: // you can go to idle from any state (except safe)
tec_power(0); // ensure bed is not powered anymore
tec_power_yellow_off(); // disconnect all wires
tec_power_orange_off(); // disconnect all wires
lid_power(0); // ensure lid is not powered
break;
case STATE_SAFE: // go to safe state
safe_state(); // put all peripherals in safe mode
break;
case STATE_HEAT:
heatsink_fan_on(); // switch fan on
tec_power(0); // switch power off before changing mode
tec_heat(); // switch to heating mode
break;
case STATE_COOL:
heatsink_fan_on(); // switch fan on
tec_power(0); // switch power off before changing mode
tec_cool(); // switch to heating mode
break;
case STATE_FAN:
tec_power(0); // ensure bed is not powered anymore
tec_power_yellow_off(); // disconnect all wires
tec_power_orange_off(); // disconnect all wires
lid_power(0); // ensure lid is not powered
heatsink_fan_on(); // switch fan on
break;
case STATE_PREPARE:
if (STATE_IDLE == state) { // only allowed from idle state
bed_target = 95.0; // set target temperature
lid_target = bed_target; // set temperature
heatsink_fan_on(); // switch fan on
tec_power(0); // switch power off
tec_heat(); // switch to heating mode
// TODO set unlimited time
} else { // transition not allowed
set_state(STATE_SAFE);
}
break;
case STATE_INITIALISATION:
if (STATE_PREPARE == state) { // only allowed from state
// temperature and heating is already set
// TODO set time to 5 min
} else { // transition not allowed
set_state(STATE_SAFE);
}
break;
case STATE_TO_DENATURATION:
if (STATE_EXTENSION == state) { // only allowed from state
lid_target = 95.0; // set target temperature
bed_target = 95.0; // set target temperature
// heating is already set
// TODO set unlimited time
} else { // transition not allowed
set_state(STATE_SAFE);
}
break;
case STATE_DENATURATION:
if (STATE_TO_DENATURATION == state) { // only allowed from state
// temperature and heating is already set
// TODO set time to 5 min
} else { // transition not allowed
set_state(STATE_SAFE);
}
break;
case STATE_TO_ANNEALING:
if (STATE_DENATURATION == state) { // only allowed from state
bed_target = 45.0; // set target temperature (5 °C below primer)
lid_target = bed_target; // same temperature
tec_power(0); // switch power off
tec_cool(); // switch to cooling mode
// TODO set unlimited time
} else { // transition not allowed
set_state(STATE_SAFE);
}
break;
case STATE_ANNEALING:
if (STATE_TO_ANNEALING == state) { // only allowed from state
tec_power(0); // switch power off before switching mode
tec_heat(); // switch to heating mode
// temperature is already set
// TODO set time to 30-40 s
} else { // transition not allowed
set_state(STATE_SAFE);
}
break;
case STATE_TO_EXTENSTION:
if (STATE_ANNEALING == state) { // only allowed from state
bed_target = 72.0; // set target temperature
lid_target = bed_target; // set target temperature
// heating is already set
// TODO set unlimited time
} else { // transition not allowed
set_state(STATE_SAFE);
}
break;
case STATE_EXTENSION:
if (STATE_TO_EXTENSTION == state) { // only allowed from state
// temperature and heating is already set
// TODO set time to ~1 min/kb of expected product; 5-10 min on last cycle.
} else { // transition not allowed
set_state(STATE_SAFE);
}
break;
case STATE_TO_HOLD:
if (STATE_EXTENSION == state) { // only allowed from state
bed_target = 10.0; // set target temperature (5 °C below primer)
lid_target = NAN; // we don't need to heat anymore
lid_power(0); // switch power off
tec_power(0); // switch power off
tec_cool(); // switch to cooling mode
// TODO set unlimited time
} else { // transition not allowed
set_state(STATE_SAFE);
}
break;
case STATE_HOLD:
if (STATE_TO_HOLD == state) { // only allowed from state
// nothing to do, we just reached the final hold up
// TODO set time to 30-40 s
} else { // transition not allowed
set_state(STATE_SAFE);
}
break;
default: // unknown new state
error = "unknown state to set";
set_state(STATE_SAFE);
break;
}
// display state
if (new != state && STATE_SAFE != state) {
state = new; // save new state
printf("new state: %s\n", state_names[state]); // show new state
oled_text_line(state_names[state], 1); // set new state
oled_text_update(); // show on display
}
}
/** display available commands
* @param[in] argument no argument required
*/
static void command_help(void* argument);
/** show software and hardware version
* @param[in] argument no argument required
*/
static void command_version(void* argument);
/** show uptime
* @param[in] argument no argument required
*/
static void command_uptime(void* argument);
/** reset board
* @param[in] argument no argument required
*/
static void command_reset(void* argument);
/** switch to DFU bootloader
* @param[in] argument no argument required
*/
static void command_bootloader(void* argument);
/** switch power to TECs
* @param[in] argument pointer to unsigned integer: 0 to power all of, 1 to connect yellow to 12V, 2 to connect orange to 12V
*/
static void command_bed_power(void* argument)
{
if (argument) { // segment has been provided
const int32_t target = *(int32_t*)argument; // get while segment to turn on/off
if (target > UINT16_MAX || target < -INT16_MAX) {
printf("can't set temperature over %u\n", UINT16_MAX);
return;
}
if (0 == target) { // switch off
bed_target = 0; // remember we have no target
set_state(STATE_IDLE); // set new state
} else if (1 == target) { // just use fan
bed_target = 0; // remember we have no target
set_state(STATE_FAN); // set new state
} else if (target > 0) { // heat
bed_target = target; // remember target
set_state(STATE_HEAT); // set new state
} else { // cool
bed_target = -target; // remember target
set_state(STATE_COOL); // set new state
}
}
// print segment status
const bool yellow = gpio_get(GPIO_PORT(TEC_POWER_YELLOW), GPIO_PIN(TEC_POWER_YELLOW));
const bool orange = gpio_get(GPIO_PORT(TEC_POWER_ORANGE), GPIO_PIN(TEC_POWER_ORANGE));
if (!yellow && !orange) {
puts("TEC power off\n");
} else if (orange && !yellow) {
puts("TEC set to cooling\n");
} else if (yellow && !orange) {
puts("TEC set to heating\n");
} else {
puts("unknown TEC power setting\n");
}
}
/** switch transistors to power to TEC segments
* @param[in] argument pointer to unsigned integer: 0 to power all off
*/
static void command_bed_segment(void* argument)
{
if (argument) { // segment has been provided
const int32_t segment = *(int32_t*)argument; // get while segment to turn on/off
switch (segment) {
case 0: // switch all off
gpio_set(GPIO_PORT(MBLK019_CH26_PIN), GPIO_PIN(MBLK019_CH26_PIN));
gpio_set(GPIO_PORT(MBLK019_CH14_PIN), GPIO_PIN(MBLK019_CH14_PIN));
gpio_set(GPIO_PORT(MBLK019_CH35_PIN), GPIO_PIN(MBLK019_CH35_PIN));
break;
case 1:
gpio_clear(GPIO_PORT(MBLK019_CH26_PIN), GPIO_PIN(MBLK019_CH26_PIN));
break;
case -1:
gpio_set(GPIO_PORT(MBLK019_CH26_PIN), GPIO_PIN(MBLK019_CH26_PIN));
break;
case 2:
gpio_clear(GPIO_PORT(MBLK019_CH14_PIN), GPIO_PIN(MBLK019_CH14_PIN));
break;
case -2:
gpio_set(GPIO_PORT(MBLK019_CH14_PIN), GPIO_PIN(MBLK019_CH14_PIN));
break;
case 3:
gpio_clear(GPIO_PORT(MBLK019_CH35_PIN), GPIO_PIN(MBLK019_CH35_PIN));
break;
case -3:
gpio_set(GPIO_PORT(MBLK019_CH35_PIN), GPIO_PIN(MBLK019_CH35_PIN));
break;
default:
printf("unknown segment: %d\n", segment);
break;
}
sleep_ms(1); // wait to take effect
}
// print segment status
printf("CH26: %s CH14: %s CH35: %s\n", \
gpio_get(GPIO_PORT(MBLK019_CH26_PIN), GPIO_PIN(MBLK019_CH26_PIN)) ? "off" : "on", \
gpio_get(GPIO_PORT(MBLK019_CH14_PIN), GPIO_PIN(MBLK019_CH14_PIN)) ? "off" : "on", \
gpio_get(GPIO_PORT(MBLK019_CH35_PIN), GPIO_PIN(MBLK019_CH35_PIN)) ? "off" : "on");
}
/** switch power to lid heater
* @param[in] argument pointer to unsigned integer: 0 to power off, 1 to power on
*/
static void command_lid_power(void* argument)
{
if (NULL == argument) {
puts("provide lid power in %\n");
} else { // segment has been provided
const uint8_t power = *(uint32_t*)argument; // get while segment to turn on/off
lid_power(power);
printf("lip power set to %u %%\n", power);
}
}
/** set lid target temperature
* @param[in] argument pointer to unsigned integer: 0 to power off, else temperature in °C
*/
static void command_lid_temperature(void* argument)
{
if (argument) {
const uint32_t target = *(uint32_t*)argument;
if (0 == target) {
lid_target = NAN;
lid_power(0);
} else {
lid_target = target * 1.0;
}
}
if (isnan(lid_target)) {
puts("no lid target temperature\n");
} else {
printf("lid target temperature: %u °C\n", (uint8_t)lid_target);
}
}
static void command_safe(void* argument)
{
(void)argument; // we won't use the argument
set_state(STATE_SAFE);
printf("in safe state\n");
}
static void command_state(void* argument)
{
(void)argument; // we won't use the argument
puts(state_names[state]);
putc('\n');
if (error) {
printf("last error: %s\n", error);
puts("to clear error, restart controller\n");
} else {
printf("no error\n");
}
}
/** list of all supported commands */
static const struct menu_command_t menu_commands[] = {
{
.shortcut = 'h',
.name = "help",
.command_description = "display help",
.argument = MENU_ARGUMENT_NONE,
.argument_description = NULL,
.command_handler = &command_help,
},
{
.shortcut = 'v',
.name = "version",
.command_description = "show software and hardware version",
.argument = MENU_ARGUMENT_NONE,
.argument_description = NULL,
.command_handler = &command_version,
},
{
.shortcut = 'u',
.name = "uptime",
.command_description = "show uptime",
.argument = MENU_ARGUMENT_NONE,
.argument_description = NULL,
.command_handler = &command_uptime,
},
{
.shortcut = 'r',
.name = "reset",
.command_description = "reset board",
.argument = MENU_ARGUMENT_NONE,
.argument_description = NULL,
.command_handler = &command_reset,
},
{
.shortcut = 'B',
.name = "bootloader",
.command_description = "reboot into DFU bootloader",
.argument = MENU_ARGUMENT_NONE,
.argument_description = NULL,
.command_handler = &command_bootloader,
},
{
.shortcut = 'b',
.name = "bed",
.command_description = "provide power to bed",
.argument = MENU_ARGUMENT_SIGNED,
.argument_description = "[+-temp]",
.command_handler = &command_bed_power,
},
{
.shortcut = 's',
.name = "segment",
.command_description = "TEC segment configuration",
.argument = MENU_ARGUMENT_SIGNED,
.argument_description = "[+-0,1,2,3]",
.command_handler = &command_bed_segment,
},
{
.shortcut = 'L',
.name = "lid_power",
.command_description = "set lid power",
.argument = MENU_ARGUMENT_UNSIGNED,
.argument_description = "%",
.command_handler = &command_lid_power,
},
{
.shortcut = 'l',
.name = "lid",
.command_description = "set lid target temperature",
.argument = MENU_ARGUMENT_UNSIGNED,
.argument_description = "°C",
.command_handler = &command_lid_temperature,
},
{
.shortcut = 's',
.name = "safe",
.command_description = "enter safe state",
.argument = MENU_ARGUMENT_NONE,
.argument_description = NULL,
.command_handler = &command_safe,
},
{
.shortcut = 'e',
.name = "error",
.command_description = "show current state and error",
.argument = MENU_ARGUMENT_NONE,
.argument_description = NULL,
.command_handler = &command_state,
},
};
static void command_help(void* argument)
{
(void)argument; // we won't use the argument
printf("available commands:\n");
menu_print_commands(menu_commands, LENGTH(menu_commands)); // print global commands
}
static void command_version(void* argument)
{
(void)argument; // we won't use the argument
printf("firmware date: %04u-%02u-%02u\n", BUILD_YEAR, BUILD_MONTH, BUILD_DAY); // show firmware build date
printf("device serial: %08x%08x%04x%04x\n", DESIG_UNIQUE_ID2, DESIG_UNIQUE_ID1, DESIG_UNIQUE_ID0 & 0xffff, DESIG_UNIQUE_ID0 >> 16); // not that the half-works are reversed in the first word
}
static void command_uptime(void* argument)
{
(void)argument; // we won't use the argument
const uint32_t uptime = (rtc_get_counter_val() - time_start) / RTC_TICKS_SECOND; // get time from internal RTC
printf("uptime: %u.%02u:%02u:%02u\n", uptime / (24 * 60 * 60), (uptime / (60 * 60)) % 24, (uptime / 60) % 60, uptime % 60);
}
static void command_reset(void* argument)
{
(void)argument; // we won't use the argument
scb_reset_system(); // reset device
while (true); // wait for the reset to happen
}
static void command_bootloader(void* argument)
{
(void)argument; // we won't use the argument
// set DFU magic to specific RAM location
__dfu_magic[0] = 'D';
__dfu_magic[1] = 'F';
__dfu_magic[2] = 'U';
__dfu_magic[3] = '!';
scb_reset_system(); // reset system (core and peripherals)
while (true); // wait for the reset to happen
}
/** process user command
* @param[in] str user command string (\0 ended)
*/
static void process_command(char* str)
{
// ensure actions are available
if (NULL == menu_commands || 0 == LENGTH(menu_commands)) {
return;
}
// don't handle empty lines
if (!str || 0 == strlen(str)) {
return;
}
bool command_handled = false;
if (!command_handled) {
command_handled = menu_handle_command(str, menu_commands, LENGTH(menu_commands)); // try if this is not a global command
}
if (!command_handled) {
printf("command not recognized. enter help to list commands\n");
}
}
/** program entry point
* this is the firmware function started by the micro-controller
*/
void main(void);
void main(void)
{
rcc_clock_setup_in_hse_8mhz_out_72mhz(); // use 8 MHz high speed external clock to generate 72 MHz internal clock
#if DEBUG
// enable functionalities for easier debug
DBGMCU_CR |= DBGMCU_CR_IWDG_STOP; // stop independent watchdog counter when code is halted
DBGMCU_CR |= DBGMCU_CR_WWDG_STOP; // stop window watchdog counter when code is halted
DBGMCU_CR |= DBGMCU_CR_STANDBY; // allow debug also in standby mode (keep digital part and clock powered)
DBGMCU_CR |= DBGMCU_CR_STOP; // allow debug also in stop mode (keep clock powered)
DBGMCU_CR |= DBGMCU_CR_SLEEP; // allow debug also in sleep mode (keep clock powered)
#else
// setup watchdog to reset in case we get stuck (i.e. when an error occurred)
iwdg_set_period_ms(WATCHDOG_PERIOD); // set independent watchdog period
iwdg_start(); // start independent watchdog
#endif
board_setup(); // setup board
usb_cdcacm_setup(); // setup USB CDC ACM (for printing)
puts("\nwelcome to the CuVoodoo MBLK001 thermo-cycler driver\n"); // print welcome message
#if DEBUG
// show reset cause
if (RCC_CSR & (RCC_CSR_LPWRRSTF | RCC_CSR_WWDGRSTF | RCC_CSR_IWDGRSTF | RCC_CSR_SFTRSTF | RCC_CSR_PORRSTF | RCC_CSR_PINRSTF)) {
puts("reset cause(s):");
if (RCC_CSR & RCC_CSR_LPWRRSTF) {
puts(" low-power");
}
if (RCC_CSR & RCC_CSR_WWDGRSTF) {
puts(" window-watchdog");
}
if (RCC_CSR & RCC_CSR_IWDGRSTF) {
puts(" independent-watchdog");
}
if (RCC_CSR & RCC_CSR_SFTRSTF) {
puts(" software");
}
if (RCC_CSR & RCC_CSR_PORRSTF) {
puts(" POR/PDR");
}
if (RCC_CSR & RCC_CSR_PINRSTF) {
puts(" pin");
}
putc('\n');
RCC_CSR |= RCC_CSR_RMVF; // clear reset flags
}
#endif
#if !(DEBUG)
// show watchdog information
printf("setup watchdog: %.2fs", WATCHDOG_PERIOD / 1000.0);
if (FLASH_OBR & FLASH_OBR_OPTERR) {
puts(" (option bytes not set in flash: software wachtdog used, not automatically started at reset)\n");
} else if (FLASH_OBR & FLASH_OBR_WDG_SW) {
puts(" (software watchdog used, not automatically started at reset)\n");
} else {
puts(" (hardware watchdog used, automatically started at reset)\n");
}
#endif
// re-use JTAG pins as GPIO (all pins are used)
rcc_periph_clock_enable(RCC_AFIO); // enable clock for alternate function domain
gpio_primary_remap(AFIO_MAPR_SWJ_CFG_JTAG_OFF_SW_ON, 0); // disable JTAG but keep SWD
// setup RTC
puts("setup internal RTC: ");
// note: the blue pill LSE oscillator is affected when toggling the onboard LED, thus prefer the HSE
rtc_auto_awake(RCC_HSE, 8000000 / 128 / RTC_TICKS_SECOND - 1); // use High Speed External oscillator (8 MHz / 128) as RTC clock (VBAT can't be used to keep the RTC running)
rtc_interrupt_enable(RTC_SEC); // enable RTC interrupt on "seconds"
nvic_enable_irq(NVIC_RTC_IRQ); // allow the RTC to interrupt
time_start = rtc_get_counter_val(); // get start time from internal RTC
puts("OK\n");
puts("setup front panel: ");
rcc_periph_clock_enable(GPIO_RCC(CONTROL_PLAY_GREEN_LED_PIN)); // enable clock for GPIO port peripheral
gpio_clear(GPIO_PORT(CONTROL_PLAY_GREEN_LED_PIN), GPIO_PIN(CONTROL_PLAY_GREEN_LED_PIN)); // switch LED off
gpio_set_mode(GPIO_PORT(CONTROL_PLAY_GREEN_LED_PIN), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO_PIN(CONTROL_PLAY_GREEN_LED_PIN)); // set pin as output push-pull to be able to power LED
rcc_periph_clock_enable(GPIO_RCC(CONTROL_PLAY_ORANGE_LED_PIN)); // enable clock for GPIO port peripheral
gpio_clear(GPIO_PORT(CONTROL_PLAY_ORANGE_LED_PIN), GPIO_PIN(CONTROL_PLAY_ORANGE_LED_PIN)); // switch LED off
gpio_set_mode(GPIO_PORT(CONTROL_PLAY_ORANGE_LED_PIN), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO_PIN(CONTROL_PLAY_ORANGE_LED_PIN)); // set pin as output push-pull to be able to power LED
rcc_periph_clock_enable(GPIO_RCC(CONTROL_POWER_RED_LED_PIN)); // enable clock for GPIO port peripheral
gpio_clear(GPIO_PORT(CONTROL_POWER_RED_LED_PIN), GPIO_PIN(CONTROL_POWER_RED_LED_PIN)); // switch LED off
gpio_set_mode(GPIO_PORT(CONTROL_POWER_RED_LED_PIN), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO_PIN(CONTROL_POWER_RED_LED_PIN)); // set pin as output push-pull to be able to power LED
// play/pause button
rcc_periph_clock_enable(GPIO_RCC(CONTROL_PLAY_BUTTON_LED_PIN)); // enable clock for button
gpio_set_mode(GPIO_PORT(CONTROL_PLAY_BUTTON_LED_PIN), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO_PIN(CONTROL_PLAY_BUTTON_LED_PIN)); // set button pin to input
rcc_periph_clock_enable(RCC_AFIO); // enable alternate function clock for external interrupt
exti_select_source(GPIO_EXTI(CONTROL_PLAY_BUTTON_LED_PIN), GPIO_PORT(CONTROL_PLAY_BUTTON_LED_PIN)); // mask external interrupt of this pin only for this port
gpio_set(GPIO_PORT(CONTROL_PLAY_BUTTON_LED_PIN), GPIO_PIN(CONTROL_PLAY_BUTTON_LED_PIN)); // pull up to be able to detect button push (go low)
exti_set_trigger(GPIO_EXTI(CONTROL_PLAY_BUTTON_LED_PIN), EXTI_TRIGGER_FALLING); // trigger when button is pressed
exti_enable_request(GPIO_EXTI(CONTROL_PLAY_BUTTON_LED_PIN)); // enable external interrupt
nvic_enable_irq(GPIO_NVIC_EXTI_IRQ(CONTROL_PLAY_BUTTON_LED_PIN)); // enable interrupt
puts("OK\n");
puts("setup heating bed pins: ");
rcc_periph_clock_enable(GPIO_RCC(BED_PIN_393A)); // enable clock for GPIO port peripheral
gpio_set_mode(GPIO_PORT(BED_PIN_393A), GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO_PIN(BED_PIN_393A)); // set pin to input to read state
rcc_periph_clock_enable(GPIO_RCC(BED_PIN_3393)); // enable clock for GPIO port peripheral
gpio_set_mode(GPIO_PORT(BED_PIN_3393), GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO_PIN(BED_PIN_3393)); // set pin to input to read state
rcc_periph_clock_enable(GPIO_RCC(BED_PIN_LK1)); // enable clock for GPIO port peripheral
gpio_set(GPIO_PORT(BED_PIN_LK1), GPIO_PIN(BED_PIN_LK1)); // pull up
gpio_set_mode(GPIO_PORT(BED_PIN_LK1), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO_PIN(BED_PIN_LK1)); // set pin to input to read state
rcc_periph_clock_enable(GPIO_RCC(BED_PIN_LK2)); // enable clock for GPIO port peripheral
gpio_set(GPIO_PORT(BED_PIN_LK2), GPIO_PIN(BED_PIN_LK2)); // pull up
gpio_set_mode(GPIO_PORT(BED_PIN_LK2), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO_PIN(BED_PIN_LK2)); // set pin to input to read state
rcc_periph_clock_enable(GPIO_RCC(BED_PIN_LK3)); // enable clock for GPIO port peripheral
gpio_set(GPIO_PORT(BED_PIN_LK3), GPIO_PIN(BED_PIN_LK3)); // pull up
gpio_set_mode(GPIO_PORT(BED_PIN_LK3), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO_PIN(BED_PIN_LK3)); // set pin to input to read state
rcc_periph_clock_enable(GPIO_RCC(BED_PIN_LK4)); // enable clock for GPIO port peripheral
gpio_set(GPIO_PORT(BED_PIN_LK4), GPIO_PIN(BED_PIN_LK4)); // pull up
gpio_set_mode(GPIO_PORT(BED_PIN_LK4), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO_PIN(BED_PIN_LK4)); // set pin to input to read state
if (gpio_get(GPIO_PORT(BED_PIN_LK1), GPIO_PIN(BED_PIN_LK1)) && gpio_get(GPIO_PORT(BED_PIN_LK2), GPIO_PIN(BED_PIN_LK2)) && gpio_get(GPIO_PORT(BED_PIN_LK3), GPIO_PIN(BED_PIN_LK3)) && gpio_get(GPIO_PORT(BED_PIN_LK4), GPIO_PIN(BED_PIN_LK4))) { // nothing is connected
error = "heating bed board not connected"; // set error
puts("KO\n");
} else if (gpio_get(GPIO_PORT(BED_PIN_LK1), GPIO_PIN(BED_PIN_LK1)) && gpio_get(GPIO_PORT(BED_PIN_LK2), GPIO_PIN(BED_PIN_LK2)) && !gpio_get(GPIO_PORT(BED_PIN_LK3), GPIO_PIN(BED_PIN_LK3)) && !gpio_get(GPIO_PORT(BED_PIN_LK4), GPIO_PIN(BED_PIN_LK4))) { // the LK jumper setting is correct
puts("OK\n");
} else { // the jumper setting is unknown
error = "not heating bed board detected"; // set error
puts("KO\n");
}
puts("setup MAX1247 to read bed thermistors: ");
sensor_max1247_setup(); // setup communication with MAX1247 ADC
puts("OK\n");
puts("setup ADC to read lid thermistor: ");
rcc_periph_clock_enable(RCC_ADC1); // enable clock for ADC domain
adc_power_off(ADC1); // switch off ADC while configuring it
adc_set_right_aligned(ADC1); // ensure it is right aligned to get the actual value in the 16-bit register
adc_disable_scan_mode(ADC1); // ensure scan mode is disabled
adc_enable_discontinuous_mode_regular(ADC1, 1); // use discontinuous mode (to go through all channels of the group, one after another)
adc_set_single_conversion_mode(ADC1); // ensure continuous mode is not used (that's not the same as discontinuous)
adc_set_sample_time_on_all_channels(ADC1, ADC_SMPR_SMP_239DOT5CYC); // use 239.5 cycles to sample (17.1 us are required for the internal voltage reference, (239.5 + 12.5) cycles @ 14 MHz max = 18 us)
adc_set_regular_sequence(ADC1, LENGTH(channels), (uint8_t*)channels); // set channel to convert
adc_enable_external_trigger_regular(ADC1, ADC_CR2_EXTSEL_SWSTART); // use software trigger to start the conversion (of the regular group)
adc_enable_temperature_sensor(); // enable internal voltage reference
adc_power_on(ADC1); // switch on ADC
sleep_us(1); // wait t_stab for the ADC to stabilize
adc_reset_calibration(ADC1); // remove previous non-calibration
adc_calibrate(ADC1); // calibrate ADC for less accuracy errors
rcc_periph_clock_enable(RCC_ADC12_IN(LID_TEC_CHANNEL)); // enable clock for GPIO domain for lid thermistor channel
gpio_set_mode(ADC12_IN_PORT(LID_TEC_CHANNEL), GPIO_MODE_INPUT, GPIO_CNF_INPUT_ANALOG, ADC12_IN_PIN(LID_TEC_CHANNEL)); // set lid thermistor channel as analogue input for the ADC
puts("OK\n");
puts("setup lid heater: ");
// verify if it is connected (the pin should be pulled up to 5V)
rcc_periph_clock_enable(GPIO_RCC(LID_HEATER_PIN)); // enable clock for GPIO port peripheral
gpio_clear(GPIO_PORT(LID_HEATER_PIN), GPIO_PIN(LID_HEATER_PIN)); // pull down
gpio_set_mode(GPIO_PORT(LID_HEATER_PIN), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO_PIN(LID_HEATER_PIN)); // set pin as input
sleep_us(100); // let signal settle
if (!gpio_get(GPIO_PORT(LID_HEATER_PIN), GPIO_PIN(LID_HEATER_PIN))) { // signal is not pulled up
error = "power board not connected";
puts("KO\n");
} else { // power board is connected
// set up PWM output
rcc_periph_clock_enable(RCC_TIM_CH(LID_HEATER_TIMER, LID_HEATER_CHANNEL)); // enable clock for GPIO peripheral
gpio_set(TIM_CH_PORT(LID_HEATER_TIMER, LID_HEATER_CHANNEL), TIM_CH_PIN(LID_HEATER_TIMER, LID_HEATER_CHANNEL)); // don't sink current (e.g. not powering the opto-coupler/triac))
gpio_set_mode(TIM_CH_PORT(LID_HEATER_TIMER, LID_HEATER_CHANNEL), GPIO_MODE_OUTPUT_10_MHZ, GPIO_CNF_OUTPUT_ALTFN_OPENDRAIN, TIM_CH_PIN(LID_HEATER_TIMER, LID_HEATER_CHANNEL)); // set pin as output
rcc_periph_clock_enable(RCC_AFIO); // enable clock for alternate function (PWM)
rcc_periph_clock_enable(RCC_TIM(LID_HEATER_TIMER)); // enable clock for timer peripheral
rcc_periph_reset_pulse(RST_TIM(LID_HEATER_TIMER)); // reset timer state
timer_set_mode(TIM(LID_HEATER_TIMER), TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP); // set timer mode, use undivided timer clock, edge alignment (simple count), and count up
// since we are controlling a triac, but we don't know the zero-crossing point, we can only switch on/off on half AC waves, e.g. 100 Hz
timer_set_prescaler(TIM(LID_HEATER_TIMER), 1099 - 1); // set period to 1 Hz ((72E6/(1099)) / 2**16 = 0.9997)
timer_set_period(TIM(LID_HEATER_TIMER), UINT16_MAX); // use the whole range as period, even if we can only control up to 100 Hz
timer_set_oc_value(TIM(LID_HEATER_TIMER), LID_HEATER_OC, 0); // duty cycle to 0%, to switch off heater
timer_set_oc_mode(TIM(LID_HEATER_TIMER), LID_HEATER_OC, TIM_OCM_PWM2); // set timer to generate PWM (heater switched of as long as CNT < CCR)
timer_enable_oc_output(TIM(LID_HEATER_TIMER), LID_HEATER_OC); // enable output to generate the PWM signal
timer_enable_break_main_output(TIM(LID_HEATER_TIMER)); // required to enable timer, even when no dead time is used
timer_set_counter(TIM(LID_HEATER_TIMER), 0); // reset counter
timer_enable_counter(TIM(LID_HEATER_TIMER)); // enable timer
puts("OK\n");
}
puts("setup TEC controller: ");
rcc_periph_clock_enable(GPIO_RCC(MBLK019_CH26_PIN)); // enable clock for GPIO port peripheral
gpio_set(GPIO_PORT(MBLK019_CH26_PIN), GPIO_PIN(MBLK019_CH26_PIN)); // don't sink current (e.g. not powering the opto-coupler/transistor)
gpio_set_mode(GPIO_PORT(MBLK019_CH26_PIN), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO_PIN(MBLK019_CH26_PIN)); // set pin as output open-drain
rcc_periph_clock_enable(GPIO_RCC(MBLK019_CH14_PIN)); // enable clock for GPIO port peripheral
gpio_set(GPIO_PORT(MBLK019_CH14_PIN), GPIO_PIN(MBLK019_CH14_PIN)); // don't sink current (e.g. not powering the opto-coupler/transistor)
gpio_set_mode(GPIO_PORT(MBLK019_CH14_PIN), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO_PIN(MBLK019_CH14_PIN)); // set pin as output open-drain
rcc_periph_clock_enable(GPIO_RCC(MBLK019_CH35_PIN)); // enable clock for GPIO port peripheral
gpio_set(GPIO_PORT(MBLK019_CH35_PIN), GPIO_PIN(MBLK019_CH35_PIN)); // don't sink current (e.g. not powering the opto-coupler/transistor)
gpio_set_mode(GPIO_PORT(MBLK019_CH35_PIN), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO_PIN(MBLK019_CH35_PIN)); // set pin as output open-drain
rcc_periph_clock_enable(GPIO_RCC(MBLK019_PRESENCE_PIN)); // enable clock for GPIO port peripheral
gpio_set(GPIO_PORT(MBLK019_PRESENCE_PIN), GPIO_PIN(MBLK019_PRESENCE_PIN)); // pull up
gpio_set_mode(GPIO_PORT(MBLK019_PRESENCE_PIN), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO_PIN(MBLK019_PRESENCE_PIN)); // set pin to input to read state
if (gpio_get(GPIO_PORT(MBLK019_PRESENCE_PIN), GPIO_PIN(MBLK019_PRESENCE_PIN))) {
error = "MBLK019 not connected";
puts("KO\n");
} else {
puts("OK\n");
}
puts("setup TEC power supply: ");
rcc_periph_clock_enable(GPIO_RCC(TEC_POWER_YELLOW)); // enable clock for GPIO port peripheral
gpio_clear(GPIO_PORT(TEC_POWER_YELLOW), GPIO_PIN(TEC_POWER_YELLOW)); // don't connect wire to VCC
gpio_set_mode(GPIO_PORT(TEC_POWER_YELLOW), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO_PIN(TEC_POWER_YELLOW)); // set pin as output
rcc_periph_clock_enable(GPIO_RCC(TEC_POWER_ORANGE)); // enable clock for GPIO port peripheral
gpio_clear(GPIO_PORT(TEC_POWER_ORANGE), GPIO_PIN(TEC_POWER_ORANGE)); // don't connect wire to VCC
gpio_set_mode(GPIO_PORT(TEC_POWER_ORANGE), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO_PIN(TEC_POWER_ORANGE)); // set pin as output
// set up PWM output
rcc_periph_clock_enable(RCC_TIM_CH(TEC_POWER_TIMER, TEC_POWER_CHANNEL)); // enable clock for GPIO peripheral
gpio_clear(TIM_CH_PORT(TEC_POWER_TIMER, TEC_POWER_CHANNEL), TIM_CH_PIN(TEC_POWER_TIMER, TEC_POWER_CHANNEL)); // don't let power trough
gpio_set_mode(TIM_CH_PORT(TEC_POWER_TIMER, TEC_POWER_CHANNEL), GPIO_MODE_OUTPUT_10_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, TIM_CH_PIN(TEC_POWER_TIMER, TEC_POWER_CHANNEL)); // set pin as output
rcc_periph_clock_enable(RCC_AFIO); // enable clock for alternate function (PWM)
rcc_periph_clock_enable(RCC_TIM(TEC_POWER_TIMER)); // enable clock for timer peripheral
rcc_periph_reset_pulse(RST_TIM(TEC_POWER_TIMER)); // reset timer state
timer_set_mode(TIM(TEC_POWER_TIMER), TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP); // set timer mode, use undivided timer clock, edge alignment (simple count), and count up
// peltier elements can safely be PWMed at 300 Hz to 3000 Hz, we will keep it under 2 kHz to avoid the audible range
timer_set_prescaler(TIM(TEC_POWER_TIMER), rcc_ahb_frequency / 1500000 - 1); // set the clock frequency to 1.5 kHz
timer_set_period(TIM(TEC_POWER_TIMER), UINT16_MAX); // use the whole range as period, even if we can only control up to 100 Hz
timer_set_oc_value(TIM(TEC_POWER_TIMER), TEC_POWER_OC, 0); // duty cycle to 0%, to switch off heater
timer_set_oc_mode(TIM(TEC_POWER_TIMER), TEC_POWER_OC, TIM_OCM_PWM1); // set timer to generate PWM (heater switched of as long as CNT < CCR)
timer_enable_oc_output(TIM(TEC_POWER_TIMER), TEC_POWER_OC); // enable output to generate the PWM signal
timer_enable_break_main_output(TIM(TEC_POWER_TIMER)); // required to enable timer, even when no dead time is used
timer_set_counter(TIM(TEC_POWER_TIMER), 0); // reset counter
timer_enable_counter(TIM(TEC_POWER_TIMER)); // enable timer
puts("OK\n");
puts("setup heat sink fan: ");
// we can't test if it is connected (we only control the MOSFET directly powering the fan)
rcc_periph_clock_enable(GPIO_RCC(HEATSINK_FAN_PIN)); // enable clock for GPIO port peripheral
gpio_clear(GPIO_PORT(HEATSINK_FAN_PIN), GPIO_PIN(HEATSINK_FAN_PIN)); // switch off fan
gpio_set_mode(GPIO_PORT(HEATSINK_FAN_PIN), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO_PIN(HEATSINK_FAN_PIN)); // set pin as output open-drain, gate of nMOS it pulled up externally
puts("OK\n");
puts("setup display: ");
if (oled_text_setup()) { // setup OLED display with default slave address
oled_text_clear(); // clear buffer (else last state is displayed)
oled_text_line("PCR 3000", 0);
oled_text_line(state_names[state], 1); // show state
oled_text_update();
puts("OK\n");
} else {
puts("KO\n");
}
puts("setup DS18B20 temperature sensor: ");
sensor_ds18b20_setup(); // configure 1-Wire bus to read from sensor
if (1 == sensor_ds18b20_number()) { // check number of devices available
sensor_ds18b20_precision(0, 12); // set precision to 12 bits
ds18b20_present = true; // remember the sensor is present (and there is only one)
sensor_ds18b20_convert(0); // start conversion (it takes almost 1 s)
puts("OK\n");
} else {
puts("KO\n");
}
// setup terminal
terminal_prefix = ""; // set default prefix
terminal_process = &process_command; // set central function to process commands
terminal_setup(); // start terminal
if (error && STATE_SAFE != state) { // an error has occurred during initialisation
set_state(STATE_SAFE); // go to safe state
} else {
led_power_on(); // indicate user we are ready
}
// start main loop
bool action = false; // if an action has been performed don't go to sleep
button_flag = false; // reset button flag
led_on(); // indicate user boot completed
while (true) { // infinite loop
iwdg_reset(); // kick the dog
if (user_input_available) { // user input is available
action = true; // action has been performed
led_toggle(); // toggle LED
char c = user_input_get(); // store receive character
terminal_send(c); // send received character to terminal
}
if (button_flag) { // user pressed button
action = true; // action has been performed
sleep_ms(100); // wait a bit to remove noise
if (!gpio_get(GPIO_PORT(CONTROL_PLAY_BUTTON_LED_PIN), GPIO_PIN(CONTROL_PLAY_BUTTON_LED_PIN))) {
puts("button pressed\n");
gpio_toggle(GPIO_PORT(CONTROL_PLAY_ORANGE_LED_PIN), GPIO_PIN(CONTROL_PLAY_ORANGE_LED_PIN));
gpio_toggle(GPIO_PORT(CONTROL_PLAY_GREEN_LED_PIN), GPIO_PIN(CONTROL_PLAY_GREEN_LED_PIN));
}
sleep_ms(100); // wait a bit to remove double trigger
button_flag = false; // reset flag
}
if (rtc_internal_tick_flag) { // the internal RTC ticked
const uint32_t ticks = rtc_internal_tick_flag; // save tick time
rtc_internal_tick_flag = 0; // reset flag
action = true; // action has been performed
if (0 == (ticks % (RTC_TICKS_SECOND / 2))) { // time to blink the LEDs
if (0 == (ticks % RTC_TICKS_SECOND)) { // switch on the LEDs
if (led_power_blink) {
led_power_on();
}
if (led_heat_blink) {
led_heat_on();
}
if (led_cool_blink) {
led_cool_on();
}
} else { // switch off LEDs
if (led_power_blink) {
led_power_off();
}
if (led_heat_blink) {
led_heat_off();
}
if (led_cool_blink) {
led_cool_off();
}
}
}
if (STATE_SAFE != state) {
if (!isnan(lid_target)) {
lid_pid(); // run PID loop for lid heater
}
if (bed_target) {
bed_pid(); // run PID loop for bed heater
}
}
}
if (rtc_internal_second_flag) { // one second has passed
rtc_internal_second_flag = false; // clear flag
action = true; // remember we did something
led_toggle(); // toggle LED (good to indicate if main function is stuck)
// read temperatures
const float lid_temp = lid_temperature();
const float heatsink_temp = bed_heatsink_temperature();
const float top_temp = bed_tophalf_temperature();
const float bot_temp = bed_bothalf_temperature();
const float tube_temp = bed_tube_temperature();
// read bed temperatures
printf("bed: top=%u %.2f, bottom=%u %.02f, sink=%u %.02f, tube=%u %.02f; 393-A=%u; 339-3=%u\n",sensor_max1247_read(0), top_temp, sensor_max1247_read(1), bot_temp, sensor_max1247_read(2), heatsink_temp, sensor_max1247_read(3), tube_temp, gpio_get(GPIO_PORT(BED_PIN_393A), GPIO_PIN(BED_PIN_393A)) ? 1 : 0, gpio_get(GPIO_PORT(BED_PIN_3393), GPIO_PIN(BED_PIN_3393)) ? 1 : 0);
printf("lid: %.04f °C\n", lid_temp);
if (ds18b20_present) {
const float temp = sensor_ds18b20_temperature(0); // get temperature
sensor_ds18b20_convert(0); // start next conversion (since it takes almost 1 s)
printf("DS18B20: %.03f °C\n", temp);
}
// time to check if everything is OK
if (!error && STATE_SAFE != state) { // only check if we are not in the safe state
// check lid temperature
if (lid_temp < 5.0) { // voltage is at the upper limit (3.3V), meaning it is directly connected to the 3.3V pull-up resistor, and the lid thermistor does not pull it to ground
error = "lid thermistor is probably not connected";
} else if (lid_temp > 100.0) {
error = "lid is getting too warm";
}
// check fan
if (STATE_IDLE != state && STATE_SAFE != state) {
heatsink_fan_on(); // ensure the fan is on when heating/cooling bed
} else if (STATE_IDLE == state) {
if (heatsink_temp > 45.0) { // ensure heat sink is not above 50 °C when resting
heatsink_fan_on();
} else { // heat sink is now cold enough to touch
heatsink_fan_off();
}
}
if (heatsink_temp > 80.0) {
error = "heat sink is getting too warm";
}
if (error) { // an error has occurred
set_state(STATE_SAFE); // go to safe state
}
}
}
if (action) { // go to sleep if nothing had to be done, else recheck for activity
action = false;
} else {
__WFI(); // go to sleep
}
} // main loop
}
/** @brief interrupt service routine called when tick passed on RTC */
void rtc_isr(void)
{
static uint32_t tick = RTC_TICKS_SECOND; // this will let us known then a second passed
rtc_clear_flag(RTC_SEC); // clear flag
rtc_internal_tick_flag = rtc_get_counter_val(); // notify to show new time
tick--; // count down ticks
if (0 == tick) { // do the check here to not miss a tick
rtc_internal_second_flag = true; // let main loop know a second passed
tick = RTC_TICKS_SECOND; // reset count down
}
}
/** interrupt service routine called when button is pressed */
void GPIO_EXTI_ISR(CONTROL_PLAY_BUTTON_LED_PIN)(void)
{
exti_reset_request(GPIO_EXTI(CONTROL_PLAY_BUTTON_LED_PIN)); // reset interrupt
button_flag = true; // perform button action
}