From eb55880296ae946c70958e3a8e2f45c262f22474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?King=20K=C3=A9vin?= Date: Mon, 7 Dec 2020 19:14:00 +0100 Subject: [PATCH] application: working WIP firmware to control thermocycler --- README.md | 182 ++++++++++- application.c | 875 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 970 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 7c9ab91..9f28a24 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,188 @@ To set BOOT0 to 1, apply 3.3 V on R11, between the resistor and the reference de The red LED should stay off while the green LED is on. Now you can remove the read protection (and erase flash), run `rake remove_protection` while a SWD adapter is connected. +ST 339 is a quad voltage comparator. +I don't know what it compares. + +LN 393 is a dual comparator. +I don't know what it compares. + +there is also an external reference (REF1). +this is probably for the MAX1247 (only the MAX1246 has an internal voltage reference). + +MAX1247 is a 4-channel 12-bit ADC. +it is used to measure the temperatures. + +there are four 2.2 kOhm thermistors. +one side is connected to REF1, the other to a MAX1247 channel. +- TH4: on the sink, connected to CH2 through R8 (2702), and COM through R4 +- TH3: in the tube, connected to CH3 through R7 (2702), and COM through R3 +- in the top of the tub heating bed, connected to CH0 +- in the bottom of the tub heating bed, connected to CH1 + +heating block MBLK-008: +01. NC +02. VCC +03. LN393 OUTPUT-A +04. MAX1247 DIN +05. MAX1247 DOUT +06. MAX1247 SCLK +07. MAX1247 nCS +08. ST339 OUTPUT-3 +09. LK1 jumper (missing), other side connected to ground +10. LK2 jumper (missing), other side connected to ground +11. LK3 jumper (present), other side connected to ground +12. LK4 jumper (present), other side connected to ground +13. ground +14. LN393 OUTPUT-A through 10 kOhm resistor (not sure what this is used for, probably not just to pull) + +heated lid, 12 kOhm NTC thermistor, 3-pin (pin 1 has notch): +1. thermistor lead 1 +2. thermistor lead 2 +3. chassis + +thermocouple (e.g. peltier elements) controller board, MBLK019, 2x3 pin plug (IDC numbering): +1. red, VCC +2. red/green, sink to control IC/TR 2/6 +3. red/black, sink to control IC/TR 1/4 +4. red/blue, connected to pin 6 +5. red/brown, sink to control IC/TR 3/5 +6. black, connected to pin 4 + +front control panel, 1x5 connector: +1. ground +2. play/pause indicator, green LED anode +3. play/pause indicator, orange LED anode +4. power indicator, red LED +5. play/pause button, connected to ground when pressed + +fan: +1. red: 12V +2. black: ground + +MBLK-078, power + heater, 2x2 connector: +1. red, 12V +2. black, ground +3. yellow/red, optocoupler anode for triac controlling the lid heater +4. yellow/black, optocoupler cathode for triac controlling the lid heater + +SSD1306 OLED screen: +1. GND +2. VDD +3. SCK +4. SDA + +power for thermocouples +----------------------- + +use a 12V xA power supply. +ideally it would be an adjustable constant current supply (probably what the original power supply was), but using PWM on constant voltage is good enough for thermocouples, even if less efficient. + +I created a custom board with: +- 2 relays to act as h-bridge, allowing to invert the voltage on the thermocouples +- 1 power MOSFET, to PWM the power +- 1 optocoupler, to control the MOSFET +- an IDC 2x5 connector, to re-use to cable + +relay 1: +- COM: thermocouple yellow +- NC: OUT +- NO: 12V dedicated power supply +- VCC: 5V +- GND: ground +- IN: IDC pin 3,4 + +relay 2: +- COM: thermocouple orange +- NC: OUT +- NO: 12V dedicated power supply +- VCC: 5V +- GND: ground +- IN: IDC pin 5,6 + +power nMOS (FQP30N06L, 60V 32A): +- gate: PC817 optocoupler source, pulled low (100 kOhm to ground) +- drain: OUT from relay +- source: ground + +IDC 2x5 connector: +- 1,2: 5V to power the relays and optocoupler +- 3,4: input to relay 1, active low (when sinking) +- 5,6: input to relay 2, active low (when sinking) +- 7,8: input optocoupler controlling MOSFET, active low (when sinking) +- 9,10: ground + +PC817 optocoupler: +- source: power nMOS gate +- drain: 12V dedicated power supply +- anode: 5V though 330 Ohm +- cathode: IDC pin 7,8 + connections =========== -Connect the peripherals the following way (STM32F10X signal; STM32F10X pin; peripheral pin; peripheral signal; comment): - -- *list board to peripheral pin connections* - All pins are configured using `define`s in the corresponding source code. +Connect the peripherals the following way. + +heating block MBLK-008 CO IDC 7x2: +01. NC +02. 3.3V +03. PB3 +04. PB15 SPI2_MOSI +05. PB14 SPI2_MISO +06. PB13 SPI2_SCK +07. PB12 SPI2_NSS +08. PB4 +09. PB5 +10. PC14 +11. PC15 +12. PB1 +13. ground + +heated lid, 12 kOhm NTC thermistor, 3-pin (pin 1 has notch): +1. PA0/ADC1_CH0, pulled up to 5.0V by 10 kOhm resistor +2. ground +3. earth + +thermocouple controller board, MBLK019, 2x3 IDC connector: +1. 3.3 V +2. PA1 +3. PA2 +4. PA3 +5. PA4 +6. ground + +front panel: +1. ground +2. PA5, with 330 Ohm inline resistor +3. PA6, with 330 Ohm inline resistor +4. PA7, with 330 Ohm inline resistor +5. PB0 + +fan: +1. 12V front board power supply +2. power nMOS drain +power nMOS source. ground +power nMOS gate. PA15, pulled up externally to 5V + +MBLK-078, power + heater, 2x2 connector: +1. LM7805 in +2. ground +3. 5V though 330 Ohm +4. PA10 + +SSD1306 OLED screen: +1. ground +2. 3.3 V +3. PB6/I2C1_SCL, pulled up to 3.3V by external 10 kOhm resistor +4. PB7/I2C1_SDA, pulled up to 3.3V by external 10 kOhm resistor + +thermocouple power supply control board. IDC 2x5: +- 1,2: 5V +- 3,4: PB10 +- 5,6: PB11 +- 7,8: PB9 +- 9,10: ground code ==== diff --git a/application.c b/application.c index 1ac0f63..f859262 100644 --- a/application.c +++ b/application.c @@ -11,6 +11,7 @@ #include // string utilities #include // date/time utilities #include // utilities to check chars +#include // NAN definition /* STM32 (including CM3) libraries */ #include // Cortex M3 utilities @@ -24,16 +25,18 @@ #include // debug utilities #include // design utilities #include // flash utilities +#include // ADC utilities +#include // timer utilities /* own libraries */ #include "global.h" // board definitions #include "print.h" // printing utilities -#if !defined(STLINKV2) -#include "uart.h" // USART utilities -#endif #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 @@ -41,16 +44,12 @@ /** 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 */ -#if defined(CORE_BOARD) -#define RTC_DATE_TIME 1 -#else #define RTC_DATE_TIME 0 -#endif /** number of RTC ticks per second * @note use integer divider of oscillator to keep second precision */ -#define RTC_TICKS_SECOND 4 +#define RTC_TICKS_SECOND 8 #if defined(RTC_DATE_TIME) && RTC_DATE_TIME /** the start time from which to RTC ticks count @@ -65,31 +64,362 @@ static time_t time_start = 0; /** @defgroup main_flags flag set in interrupts to be processed in main task * @{ */ -static volatile bool rtc_internal_tick_flag = false; /**< flag set when internal RTC ticked */ +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_CH1 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 */ +// the button on the panel is configured as board button +//#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 = ground, low = 12V), must be 5V tolerant */ +#define tec_power_yellow_on() gpio_clear(GPIO_PORT(TEC_POWER_YELLOW), GPIO_PIN(TEC_POWER_YELLOW)) // sink current, connecting wire to 12V +#define tec_power_yellow_off() gpio_set(GPIO_PORT(TEC_POWER_YELLOW), GPIO_PIN(TEC_POWER_YELLOW)) // don't sink current, disconnecting wire from 12V +#define TEC_POWER_ORANGE PB10 /**< pin to choose which power rail to connect to orange TEC power input (high = ground, low = 12V), must be 5V tolerant */ +#define tec_power_orange_on() gpio_clear(GPIO_PORT(TEC_POWER_ORANGE), GPIO_PIN(TEC_POWER_ORANGE)) // sink current, connecting wire to 12V +#define tec_power_orange_off() gpio_set(GPIO_PORT(TEC_POWER_ORANGE), GPIO_PIN(TEC_POWER_ORANGE)) // don't sink current, disconnecting wire from 12V +#define TEC_POWER_PWM PB9 /**< pin to actually let power go through TECs, where PWM can be used (active low) */ +#define tec_power_pwm_on() gpio_clear(GPIO_PORT(TEC_POWER_PWM), GPIO_PIN(TEC_POWER_PWM)) /**< allow power to go through TECs by sinking current, powering optocoupler, switching on MOSFET, connecting ground */ +#define tec_power_pwm_off() gpio_set(GPIO_PORT(TEC_POWER_PWM), GPIO_PIN(TEC_POWER_PWM)) /**< prevent power to go through TECs by not sinking current, not powering optocoupler, not switching on MOSFET, disconnecting ground */ +#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 double 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 = STATE_IDLE; + +/** 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 -#if !defined(STLINKV2) - uart_putchar_nonblocking('\r'); // send CR over USART -#endif usb_cdcacm_putchar('\r'); // send CR over USB length++; // remember we printed 1 character } } -#if !defined(STLINKV2) - uart_putchar_nonblocking(c); // send byte over USART -#endif 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) +{ + state = STATE_SAFE; // remember we entered safe state + + // 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 power MOSFET 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_OPENDRAIN, GPIO_PIN(TEC_POWER_PWM)); // set pin as output open-drain + tec_power_pwm_off(); // switch off power + + sleep_ms(2); // wait a be before switch relay to prevent arcing + tec_power_yellow_off(); // disconnect 12V from yellow TEC line + tec_power_orange_off(); // disconnect 12V from orange TEC line + sleep_ms(10); // wait for relay to operate + + // 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 + + oled_text_clear(); // clear screen + oled_text_line("safe", 0); // indicate state + oled_text_update(); // display text + + if (error) { + led_power_blink = false; // start blinking red LED to indicate error + oled_text_line("error", 1); + 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.7013 V = 20.500 °C, 0.35925 V = 84.062 °C + return -27.125 * voltages[1] + 93.7727; +} + +/** 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 lib temperature */ +static void lid_pid(void) +{ + // I tried Ziegler–Nichols 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 > 100.0) { + power = 100.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(); // disconnect 12V from orange TEC line + tec_power_yellow_on(); // connect 12V to yellow TEC line + sleep_ms(10); // wait for relay to operate + + // 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(); // disconnect 12V from yellow TEC line + tec_power_orange_on(); // connect 12V to orange TEC line + sleep_ms(10); // wait for relay to operate + + // 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 + 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 +} + +/** 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, 1624 = 50.625, 2850 = 82.250 + // 616 = 21.8 C, 2926 = 83.625 + + return measurement * 0.0273778 + 4.22317; +} + +/** 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 + // + + return measurement * 0.0273778 + 4.22317; // TODO this values are from the bed top half +} + +// bottom 12 = 63.3 °C +// heat sink + +/** run PID control for bed temperature (using the top half) */ +static void bed_pid(void) +{ + // I tried Ziegler–Nichols 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 +} + +// bottom 19.312 = 549 +// sink 19.312 = 557 +// tube 19.312 = 564 + /** display available commands * @param[in] argument no argument required */ @@ -105,13 +435,6 @@ static void command_version(void* argument); */ static void command_uptime(void* argument); -#if RTC_DATE_TIME -/** show date and time - * @param[in] argument date and time to set - */ -static void command_datetime(void* argument); -#endif - /** reset board * @param[in] argument no argument required */ @@ -122,6 +445,120 @@ static void command_reset(void* argument); */ 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 + // turn all off + tec_power(0); // switch power off + tec_power_yellow_off(); // disconnect 12V from yellow TEC line + tec_power_orange_off(); // disconnect 12V from orange TEC line + heatsink_fan_off(); // we can switch the fan off now + sleep_ms(10); // wait for relay to operate + bed_target = 0; // remember we have no target + state = STATE_IDLE; // set new state + } else if (target > 0) { // heat + heatsink_fan_on(); // switch fan on + tec_power(0); // switch power off + tec_heat(); // switch to heating mode + bed_target = target; // remember target + state = STATE_HEAT; // set new state + } else { // cool + heatsink_fan_on(); // switch fan on + tec_power(0); // switch power off + tec_cool(); // switch to heating mode + bed_target = -target; // remember target + state = STATE_COOL; // set new state + } + } + + // print segment status + if (gpio_get(GPIO_PORT(TEC_POWER_ORANGE), GPIO_PIN(TEC_POWER_ORANGE)) && gpio_get(GPIO_PORT(TEC_POWER_YELLOW), GPIO_PIN(TEC_POWER_YELLOW)) && gpio_get(GPIO_PORT(TEC_POWER_PWM), GPIO_PIN(TEC_POWER_PWM))) { + puts("TEC power disconnected\n"); + } else if (0 == gpio_get(GPIO_PORT(TEC_POWER_ORANGE), GPIO_PIN(TEC_POWER_ORANGE))) { + puts("TEC set to cooling\n"); + + } else if (0 == gpio_get(GPIO_PORT(TEC_POWER_YELLOW), GPIO_PIN(TEC_POWER_YELLOW))) { + puts("TEC set to heating\n"); + } +} + +/** 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 + safe_state(); + printf("in safe state\n"); +} + +static void command_state(void* argument) +{ + (void)argument; // we won't use the argument + switch (state) { + case STATE_IDLE: + puts("idle"); + break; + case STATE_SAFE: + puts("safe"); + break; + default: + puts("unknown state"); + break; + } + 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[] = { { @@ -148,16 +585,6 @@ static const struct menu_command_t menu_commands[] = { .argument_description = NULL, .command_handler = &command_uptime, }, -#if RTC_DATE_TIME - { - .shortcut = 'd', - .name = "date", - .command_description = "show/set date and time", - .argument = MENU_ARGUMENT_STRING, - .argument_description = "[YYYY-MM-DD HH:MM:SS]", - .command_handler = &command_datetime, - }, -#endif { .shortcut = 'r', .name = "reset", @@ -167,13 +594,53 @@ static const struct menu_command_t menu_commands[] = { .command_handler = &command_reset, }, { - .shortcut = 'b', + .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 = '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) @@ -197,41 +664,6 @@ static void command_uptime(void* argument) printf("uptime: %u.%02u:%02u:%02u\n", uptime / (24 * 60 * 60), (uptime / (60 * 60)) % 24, (uptime / 60) % 60, uptime % 60); } -#if RTC_DATE_TIME -static void command_datetime(void* argument) -{ - char* datetime = (char*)argument; // argument is optional date time - if (NULL == argument) { // no date and time provided, just show the current day and time - const time_t time_rtc = rtc_get_counter_val() / RTC_TICKS_SECOND + rtc_offset; // get time from internal RTC - const struct tm* time_tm = localtime(&time_rtc); // convert time - const char* days[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}; // the days of the week - printf("date: %s %d-%02d-%02d %02d:%02d:%02d\n", days[time_tm->tm_wday], 1900 + time_tm->tm_year, 1 + time_tm->tm_mon, time_tm->tm_mday, time_tm->tm_hour, time_tm->tm_min, time_tm->tm_sec); - } else { // date and time provided, set it - const char* malformed = "date and time malformed, expecting YYYY-MM-DD HH:MM:SS\n"; - struct tm time_tm; // to store the parsed date time - if (strlen(datetime) != (4 + 1 + 2 + 1 + 2) + 1 + (2 + 1 + 2 + 1 + 2)) { // verify date/time is long enough - printf(malformed); - return; - } - if (!(isdigit((int8_t)datetime[0]) && isdigit((int8_t)datetime[1]) && isdigit((int8_t)datetime[2]) && isdigit((int8_t)datetime[3]) && '-' == datetime[4] && isdigit((int8_t)datetime[5]) && isdigit((int8_t)datetime[6]) && '-' == datetime[7] && isdigit((int8_t)datetime[8]) && isdigit((int8_t)datetime[9]) && ' ' == datetime[10] && isdigit((int8_t)datetime[11]) && isdigit((int8_t)datetime[12]) && ':' == datetime[13] && isdigit((int8_t)datetime[14]) && isdigit((int8_t)datetime[15]) && ':' == datetime[16] && isdigit((int8_t)datetime[17]) && isdigit((int8_t)datetime[18]))) { // verify format (good enough to not fail parsing) - printf(malformed); - return; - } - time_tm.tm_year = strtol(&datetime[0], NULL, 10) - 1900; // parse year - time_tm.tm_mon = strtol(&datetime[5], NULL, 10) - 1; // parse month - time_tm.tm_mday = strtol(&datetime[8], NULL, 10); // parse day - time_tm.tm_hour = strtol(&datetime[11], NULL, 10); // parse hour - time_tm.tm_min = strtol(&datetime[14], NULL, 10); // parse minutes - time_tm.tm_sec = strtol(&datetime[17], NULL, 10); // parse seconds - time_t time_rtc = mktime(&time_tm); // get back seconds - time_rtc -= rtc_offset; // remove start offset - time_start = time_rtc * RTC_TICKS_SECOND + (rtc_get_counter_val() - time_start); // update uptime with current date - rtc_set_counter_val(time_rtc * RTC_TICKS_SECOND); // save date/time to internal RTC - printf("date and time saved: %d-%02d-%02d %02d:%02d:%02d\n", 1900 + time_tm.tm_year, 1 + time_tm.tm_mon, time_tm.tm_mday, time_tm.tm_hour, time_tm.tm_min, time_tm.tm_sec); - } -} -#endif - static void command_reset(void* argument) { (void)argument; // we won't use the argument @@ -295,11 +727,8 @@ void main(void) #endif board_setup(); // setup board -#if !defined(STLINKV2) - uart_setup(); // setup USART (for printing) -#endif usb_cdcacm_setup(); // setup USB CDC ACM (for printing) - puts("\nwelcome to the CuVoodoo STM32F1 example application\n"); // print welcome message + puts("\nwelcome to the CuVoodoo MBLK001 thermo-cycler driver\n"); // print welcome message #if DEBUG // show reset cause @@ -339,27 +768,222 @@ void main(void) } #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: "); -#if defined(BLUE_PILL) || defined(STLINKV2) || defined(BLASTER) // for boards without a Low Speed External oscillator // 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) -#else // for boards with an precise Low Speed External oscillator - rtc_auto_awake(RCC_LSE, 32768 / RTC_TICKS_SECOND - 1); // ensure internal RTC is on, uses the 32.678 kHz LSE, and the prescale is set to our tick speed, else update backup registers accordingly (power off the micro-controller for the change to take effect) -#endif 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 is configured by global + 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 relays: "); + // set as input to check if we are connected + 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)); // pull down + gpio_set_mode(GPIO_PORT(TEC_POWER_YELLOW), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO_PIN(TEC_POWER_YELLOW)); // set pin as input + 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)); // pull down + gpio_set_mode(GPIO_PORT(TEC_POWER_ORANGE), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO_PIN(TEC_POWER_ORANGE)); // set pin as input + rcc_periph_clock_enable(GPIO_RCC(TEC_POWER_PWM)); // enable clock for GPIO port peripheral + gpio_clear(GPIO_PORT(TEC_POWER_PWM), GPIO_PIN(TEC_POWER_PWM)); // pull down + gpio_set_mode(GPIO_PORT(TEC_POWER_PWM), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO_PIN(TEC_POWER_PWM)); // set pin as input + sleep_us(100); // let pin settle + if (0 == gpio_get(GPIO_PORT(TEC_POWER_YELLOW), GPIO_PIN(TEC_POWER_YELLOW)) && 0 == gpio_get(GPIO_PORT(TEC_POWER_ORANGE), GPIO_PIN(TEC_POWER_ORANGE)) && 0 == gpio_get(GPIO_PORT(TEC_POWER_PWM), GPIO_PIN(TEC_POWER_PWM))) { + error = "TEC power controller not connected"; + puts("KO\n"); + } else if (gpio_get(GPIO_PORT(TEC_POWER_YELLOW), GPIO_PIN(TEC_POWER_YELLOW)) && gpio_get(GPIO_PORT(TEC_POWER_ORANGE), GPIO_PIN(TEC_POWER_ORANGE)) && gpio_get(GPIO_PORT(TEC_POWER_PWM), GPIO_PIN(TEC_POWER_PWM))) { // board is connected + // configure as output to control to relays + rcc_periph_clock_enable(GPIO_RCC(TEC_POWER_YELLOW)); // enable clock for GPIO port peripheral + gpio_set(GPIO_PORT(TEC_POWER_YELLOW), GPIO_PIN(TEC_POWER_YELLOW)); // don't sink current (e.g. not power relay to connect to 12V) + gpio_set_mode(GPIO_PORT(TEC_POWER_YELLOW), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO_PIN(TEC_POWER_YELLOW)); // set pin as output open-drain + rcc_periph_clock_enable(GPIO_RCC(TEC_POWER_ORANGE)); // enable clock for GPIO port peripheral + gpio_set(GPIO_PORT(TEC_POWER_ORANGE), GPIO_PIN(TEC_POWER_ORANGE)); // don't sink current (e.g. not power relay to connect to 12V) + gpio_set_mode(GPIO_PORT(TEC_POWER_ORANGE), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO_PIN(TEC_POWER_ORANGE)); // set pin as output open-drain + // set up PWM output + rcc_periph_clock_enable(RCC_TIM_CH(TEC_POWER_TIMER, TEC_POWER_CHANNEL)); // enable clock for GPIO peripheral + gpio_set(TIM_CH_PORT(TEC_POWER_TIMER, TEC_POWER_CHANNEL), TIM_CH_PIN(TEC_POWER_TIMER, TEC_POWER_CHANNEL)); // don't sink current (e.g. not powering the optocoupler/transistor) + gpio_set_mode(TIM_CH_PORT(TEC_POWER_TIMER, TEC_POWER_CHANNEL), GPIO_MODE_OUTPUT_10_MHZ, GPIO_CNF_OUTPUT_ALTFN_OPENDRAIN, 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_PWM2); // 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"); + } else { + error = "TEC power controller wrongly connected"; + puts("KO\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("system ready", 1); + 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 + safe_state(); // 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 @@ -370,16 +994,95 @@ void main(void) } if (button_flag) { // user pressed button action = true; // action has been performed - puts("button pressed\n"); - led_toggle(); // toggle LED - sleep_ms(100); // wait a bit to remove noise and double trigger + sleep_ms(100); // wait a bit to remove noise +#if (0 == BUTTON_PRESSED) + if (!gpio_get(GPIO_PORT(BUTTON_PIN), GPIO_PIN(BUTTON_PIN))) { +#else + if (gpio_get(GPIO_PORT(BUTTON_PIN), GPIO_PIN(BUTTON_PIN))) { +#endif + 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 - rtc_internal_tick_flag = false; // reset flag + 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 == (rtc_get_counter_val() % RTC_TICKS_SECOND)) { // one seond has passed - led_toggle(); // toggle LED (good to indicate if main function is stuck) + 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 bed_temp = bed_tophalf_temperature(); + // read bed temperatures + printf("bed: top=%.2f, bottom=%u, sink=%u %.02f, tube=%u; 393-A=%u; 339-3=%u\n", bed_temp, sensor_max1247_read(1), sensor_max1247_read(2), heatsink_temp, sensor_max1247_read(3), 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: %.02f V\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 5V 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) { + 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 + safe_state(); // go to safe state + } } } if (action) { // go to sleep if nothing had to be done, else recheck for activity @@ -393,6 +1096,12 @@ void main(void) /** @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 = true; // notify to show new time + 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 + } }