#include "stm32f0xx_hal.h" #include "config.h" #include "states.h" #include "ssd1306.h" #include "eeprom_min.h" #include "gpio.h" #include "spi.h" #include "stringhelpers.h" #include "display.h" #include "usb_device.h" #include "usbd_cdc_if.h" // Prototypes // Move to header file void process(); void restore_settings(); void save_settings(); void save_setpoints(); void SystemClock_Config(void); therm_settings_t set; therm_status_t status; // Globalish setting vars SPI_HandleTypeDef hspi1; static __IO uint32_t TimingDelay; void deinit(void) { HAL_DeInit(); } volatile int i=0; int main(void) { /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* Configure the system clock */ SystemClock_Config(); /* Unset bootloader option bytes (if set) */ void bootloader_unset(void); /* Initialize all configured peripherals */ init_gpio(); MX_USB_DEVICE_Init(); // USB startup delay HAL_Delay(1000); HAL_GPIO_WritePin(LED_POWER, 1); // TODO: Awesome pwm of power LED // Configure 1ms SysTick (change if more temporal resolution needed) //RCC_ClocksTypeDef RCC_Clocks; //RCC_GetClocksFreq(&RCC_Clocks); //SysTick_Config(RCC_Clocks.HCLK_Frequency / 1000); // Init SPI busses init_spi(); // Init OLED over SPI ssd1306_Init(); ssd1306_clearscreen(); // Default settings set.boottobrew = 0; set.temp_units = TEMP_UNITS_CELSIUS; set.windup_guard = 1; set.k_p = 1; set.k_i = 1; set.k_d = 1; set.ignore_tc_error = 0; set.setpoint_brew = 0; set.setpoint_steam = 0; // Default status status.temp = 0; status.temp_frac = 0; status.state_resume = 0; status.state = STATE_IDLE; status.setpoint = 0; status.pid_enabled = 0; // Load settings (if any) from EEPROM restore_settings(); if(set.boottobrew) status.state = STATE_PREHEAT_BREW; // Go to brew instead of idle if configured thusly // Startup screen ssd1306_DrawString("therm v0.1", 1, 40); ssd1306_DrawString("protofusion.org/therm", 3, 0); HAL_Delay(1500); ssd1306_clearscreen(); // Main loop while(1) { // Process sensor inputs process(); // Run state machine display_process(&set, &status); } } /** System Clock Configuration */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct; RCC_ClkInitTypeDef RCC_ClkInitStruct; RCC_PeriphCLKInitTypeDef PeriphClkInit; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI48; RCC_OscInitStruct.HSI48State = RCC_HSI48_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE; HAL_RCC_OscConfig(&RCC_OscInitStruct); RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI48; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_1); PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USB; PeriphClkInit.UsbClockSelection = RCC_USBCLKSOURCE_HSI48; HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit); __SYSCFG_CLK_ENABLE(); } void update_temp() { // Assert CS HAL_GPIO_WritePin(MAX_CS, 0); uint8_t rxdatah[1] = {0x00}; uint8_t rxdatal[1] = {0x00}; HAL_SPI_Receive(&hspi1, rxdatah, 1, 100); HAL_SPI_Receive(&hspi1, rxdatal, 1, 100); // Release CS HAL_GPIO_WritePin(MAX_CS, 1); // Assemble data array into one var uint16_t temp_pre = rxdatal[0] | (rxdatah[0]<<8); if(temp_pre & 0b0000000000000010) { ssd1306_clearscreen(); //ssd1306_DrawString("Fatal Error", 3, 35); HAL_Delay(100); status.state = STATE_TC_ERROR; status.temp = 0; status.temp_frac = 0; } else if(temp_pre & 0b0000000000000001 && !set.ignore_tc_error) { status.state_resume = status.state; status.state = STATE_TC_ERROR; status.temp = 0; status.temp_frac = 0; } else { if(status.state == STATE_TC_ERROR) { status.state = status.state_resume; ssd1306_clearscreen(); } uint8_t sign = status.temp >> 15;// top bit is sign temp_pre = temp_pre >> 2; // Drop 2 lowest bits status.temp_frac = temp_pre & 0b11; // get fractional part status.temp_frac *= 25; // each bit is .25 a degree, up to fixed point temp_pre = temp_pre >> 2; // Drop 2 fractional bits int8_t signint; if(sign) { signint = -1; } else { signint = 1; } // Convert to Fahrenheit if(set.temp_units == TEMP_UNITS_FAHRENHEIT) { status.temp = signint * ((temp_pre*100) + status.temp_frac); status.temp = status.temp * 1.8; status.temp += 3200; status.temp_frac = status.temp % 100; status.temp /= 100; status.temp += set.temp_offset; } // Use Celsius values else { status.temp = temp_pre * signint; status.temp += set.temp_offset; } } } // PID implementation // TODO: Make struct that has the last_temp and i_state in it, pass by ref. Make struct that has other input values maybe. int16_t last_pid_temp = 0; uint8_t last_pid_temp_frac = 0; int16_t i_state = 0; int16_t update_pid(uint16_t k_p, uint16_t k_i, uint16_t k_d, int16_t temp, uint8_t temp_frac, int16_t setpoint) { // Calculate instantaneous error int16_t error = (int16_t)setpoint - (int16_t)temp; // TODO: Use fixed point fraction // Proportional component int16_t p_term = k_p * error; // Error accumulator (integrator) i_state += error; // to prevent the iTerm getting huge despite lots of // error, we use a "windup guard" // (this happens when the machine is first turned on and // it cant help be cold despite its best efforts) // not necessary, but this makes windup guard values // relative to the current iGain int16_t windup_guard_res = set.windup_guard / k_i; // Calculate integral term with windup guard if (i_state > windup_guard_res) i_state = windup_guard_res; else if (i_state < -windup_guard_res) i_state = -windup_guard_res; int16_t i_term = k_i * i_state; // Calculate differential term (slope since last iteration) int16_t d_term = (k_d * (status.temp - last_pid_temp)); // Save temperature for next iteration last_pid_temp = status.temp; last_pid_temp_frac = status.temp_frac; int16_t result = p_term + i_term - d_term; // Put out tenths of percent, 0-1000. if(result > 1000) result = 1000; else if(result < -1000) result = -1000; // Return feedback return result; } uint32_t last_ssr_on = 0; uint32_t last_vcp_tx = 0; uint32_t last_led = 0; int16_t ssr_output = 0; // Duty cycle of ssr, 0 to SSR_PERIOD // Process things void process() { update_temp(); // Read MAX31855 // TODO: Add calibration offset (linear) uint32_t ticks = HAL_GetTick(); if(ticks - last_led > 400) { HAL_GPIO_TogglePin(LED_POWER); last_led = ticks; } // Every 200ms, set the SSR on unless output is 0 if((ticks - last_ssr_on > SSR_PERIOD)) { if(status.pid_enabled) { // Get ssr output for next time int16_t power_percent = update_pid(set.k_p, set.k_i, set.k_d, status.temp, status.temp_frac, status.setpoint); //power-percent is 0-1000 ssr_output = power_percent; //(((uint32_t)SSR_PERIOD * (uint32_t)10 * (uint32_t)100) * power_percent) / (uint32_t)1000000; } else { ssr_output = 0; } // Only support heating (ssr_output > 0) right now if(ssr_output > 0) { char tempstr[6]; itoa(ssr_output, tempstr, 10); ssd1306_DrawString(tempstr, 0, 90); HAL_GPIO_WritePin(SSR_PIN, 1); last_ssr_on = ticks; } } // Kill SSR after elapsed period less than SSR_PERIOD if(ticks - last_ssr_on > ssr_output || ssr_output == 0) { HAL_GPIO_WritePin(SSR_PIN, 0); } if(ticks - last_vcp_tx > VCP_TX_FREQ) { // Print temp to cdc char tempstr[16]; itoa_fp(status.temp, status.temp_frac, tempstr); uint8_t numlen = strlen(tempstr); tempstr[numlen] = '\r'; tempstr[numlen+1] = '\n'; CDC_Transmit_FS(tempstr, numlen+2); // while(CDC_Transmit_FS("\r\n", 2) == USBD_BUSY); last_vcp_tx = ticks; } } void save_settings() { /* Minimal_EEPROM_Unlock(); // Try programming a word at an address divisible by 4 Minimal_EEPROM_ProgramWord(EEPROM_BASE_ADDR + EEPROM_ADDR_BOOTTOBREW, boottobrew); Minimal_EEPROM_ProgramWord(EEPROM_BASE_ADDR + EEPROM_ADDR_WINDUP_GUARD, windup_guard); Minimal_EEPROM_ProgramWord(EEPROM_BASE_ADDR + EEPROM_ADDR_K_P, k_p); Minimal_EEPROM_ProgramWord(EEPROM_BASE_ADDR + EEPROM_ADDR_K_I, k_i); Minimal_EEPROM_ProgramWord(EEPROM_BASE_ADDR + EEPROM_ADDR_K_D, k_d); Minimal_EEPROM_ProgramWord(EEPROM_BASE_ADDR + EEPROM_ADDR_UNITS, temp_units); Minimal_EEPROM_Lock(); */ } void save_setpoints() { /* Minimal_EEPROM_Unlock(); Minimal_EEPROM_ProgramWord(EEPROM_BASE_ADDR + EEPROM_ADDR_BREWTEMP, setpoint_brew); Minimal_EEPROM_ProgramWord(EEPROM_BASE_ADDR + EEPROM_ADDR_STEAMTEMP, setpoint_steam); Minimal_EEPROM_Lock(); */ } // TODO: Make a struct that has all settings in it. Pass by ref to this func in a library. void restore_settings() { /* Minimal_EEPROM_Unlock(); while(Minimal_FLASH_GetStatus()==FLASH_BUSY); boottobrew = (*(__IO uint32_t*)(EEPROM_BASE_ADDR + EEPROM_ADDR_BOOTTOBREW)); while(Minimal_FLASH_GetStatus()==FLASH_BUSY); windup_guard = (*(__IO uint32_t*)(EEPROM_BASE_ADDR + EEPROM_ADDR_WINDUP_GUARD)); while(Minimal_FLASH_GetStatus()==FLASH_BUSY); k_p = (*(__IO uint32_t*)(EEPROM_BASE_ADDR + EEPROM_ADDR_K_P)); while(Minimal_FLASH_GetStatus()==FLASH_BUSY); k_i = (*(__IO uint32_t*)(EEPROM_BASE_ADDR + EEPROM_ADDR_K_I)); while(Minimal_FLASH_GetStatus()==FLASH_BUSY); k_d = (*(__IO uint32_t*)(EEPROM_BASE_ADDR + EEPROM_ADDR_K_D)); while(Minimal_FLASH_GetStatus()==FLASH_BUSY); setpoint_brew = (*(__IO uint32_t*)(EEPROM_BASE_ADDR + EEPROM_ADDR_BREWTEMP)); while(Minimal_FLASH_GetStatus()==FLASH_BUSY); setpoint_steam = (*(__IO uint32_t*)(EEPROM_BASE_ADDR + EEPROM_ADDR_STEAMTEMP)); while(Minimal_FLASH_GetStatus()==FLASH_BUSY); temp_units = (*(__IO uint32_t*)(EEPROM_BASE_ADDR + EEPROM_ADDR_UNITS)); Minimal_EEPROM_Lock(); */ } // vim:softtabstop=4 shiftwidth=4 expandtab