diff --git a/Maiskolben_TFT/Maiskolben_TFT.ino b/Maiskolben_TFT/Maiskolben_TFT.ino index f22c917..8bef607 100644 --- a/Maiskolben_TFT/Maiskolben_TFT.ino +++ b/Maiskolben_TFT/Maiskolben_TFT.ino @@ -12,6 +12,12 @@ */ //#define USE_TFT_RESET +/* show splash screen only a short time; to enter options, press power button when turning the maiskolben on. */ +#define FAST_BOOT + +/* display the input voltage (define to true to show, to false to hide */ +#define SHOW_INPUT_VOLTS true + /* * If red is blue and blue is red change this * If not sure, leave commented, you will be shown a setup screen @@ -42,7 +48,7 @@ volatile uint8_t pwm, threshold_counter; volatile int16_t cur_t, last_measured; volatile error_type error = NO_ERROR; error_type error_old; -int16_t stored[3] = {300, 350, 450}, set_t = TEMP_MIN, set_t_old, cur_t_old, target_t; +int16_t stored[3] = { 300, 350, 450 }, set_t = TEMP_MIN, set_t_old, cur_t_old, target_t, old_target_t; double pid_val, cur_td, set_td; uint8_t store_to = 255; p_source power_source, power_source_old = NO_INIT; @@ -72,15 +78,18 @@ TFT_ILI9163C tft = TFT_ILI9163C(TFT_CS, TFT_DC, STBY_NO); #else TFT_ILI9163C tft = TFT_ILI9163C(TFT_CS, TFT_DC); #endif -#define BLACK 0x0000 -#define BLUE 0x001F -#define RED 0xF800 -#define GREEN 0x07E0 -#define CYAN 0x07FF -#define MAGENTA 0xF81F -#define YELLOW 0xFFE0 -#define WHITE 0xFFFF -#define GRAY 0x94B2 + +#define COLOR(r,g,b) ((((r &0xFFu)>>3u)<<11u) + (((g&0xFFu)>>2u)<<5u) + ((b&0xFFu)>>3u)) + +#define BLACK COLOR(0,0,0) +#define BLUE COLOR(0,0,255) +#define RED COLOR(255,20,20) +#define GREEN COLOR(0, 255, 0) +#define CYAN COLOR(0, 255, 255) +#define MAGENTA COLOR(255, 0, 255) +#define YELLOW COLOR(255, 255, 0) +#define WHITE COLOR(255,255,255) +#define GRAY COLOR(144,148,140) PID heaterPID(&cur_td, &pid_val, &set_td, kp, ki, kd, DIRECT); @@ -222,6 +231,9 @@ void setup(void) { if (force_menu) optionMenu(); else { updateRevision(); +#ifdef FAST_BOOT + attachInterrupt(digitalPinToInterrupt(SW_STBY), optionMenu, LOW); +#endif tft.drawBitmap(0, 20, maiskolben, 160, 64, YELLOW); tft.setCursor(20,86); tft.setTextColor(YELLOW); @@ -234,13 +246,18 @@ void setup(void) { tft.setCursor(46,120); tft.print("HW Revision "); tft.print(revision); + +#ifdef FAST_BOOT //Allow Options to be set at startup + delay(200); +#else delay(100); attachInterrupt(digitalPinToInterrupt(SW_STBY), optionMenu, LOW); for (int i = 0; i < 10 && !menu_dismissed; i++) { digitalWrite(HEAT_LED, i % 2); delay(250); } +#endif detachInterrupt(digitalPinToInterrupt(SW_STBY)); } /* @@ -499,7 +516,11 @@ void timer_sw_poll(void) { cnt_off_press = min(201, cnt_off_press+1); } else { if (cnt_off_press > 0 && cnt_off_press <= 100) { - setStandby(!stby); + if (!off) setStandby(!stby); + if (off) { + setStandby(false); + setOff(false); + } } cnt_off_press = 0; } @@ -608,6 +629,37 @@ void printTemp(float t) { tft.print((int)t); } + +const unsigned char* get_battery_symbol(float v_bat, bool charging, uint8_t *red, uint8_t *green, uint8_t *percent_out ) { + uint8_t percent; + if (charging) { + float D_MAX = NUM_CELLS * (MAX_CHARGE_PER_CELL - MIN_VOLTS_PER_CELL); + float delta_v = (v_bat) - (NUM_CELLS * MIN_VOLTS_PER_CELL); + // Ladezustand + percent = min(100,max(0,(delta_v / D_MAX ) * 100.0)); + } else { + float D_MAX = NUM_CELLS * (MAX_VOLTS_PER_CELL - MIN_VOLTS_PER_CELL); + float delta_v = (v_bat) - (NUM_CELLS * MIN_VOLTS_PER_CELL); + // Ladezustand + percent = min(100,max(0,(delta_v / D_MAX ) * 100.0)); + } + + if (red && green) { + *green = ((float) percent) * 2.55; + *red = 255-*green; + } + + if (percent_out) { + *percent_out = percent; + } + + if(percent >= 90) return battery_100; + if(percent >= 75) return battery_75; + if(percent >= 50) return battery_50; + if(percent >= 25) return battery_25; + return battery_0; +} + void display(void) { if (force_redraw) tft.fillScreen(BLACK); int16_t temperature = cur_t; //buffer volatile value @@ -638,6 +690,10 @@ void display(void) { case NO_TIP: tft.print(F("Error: No tip connected\nTip slipped out?")); break; + case FAILED_TO_HEAT: + tft.print(F("Error: Heating\nTemp not increasing.")); + break; + } tft.setTextSize(2); tft.setTextColor(YELLOW, BLACK); @@ -669,17 +725,17 @@ void display(void) { if (stby || stby_layoff) { old_stby = true; tft.setTextColor(YELLOW, BLACK); - tft.print(F("STBY ")); + tft.print(F("STBY ")); } else { old_stby = false; set_t_old = set_t; tft.setTextColor(WHITE, BLACK); - tft.write(' '); + //tft.write(' '); printTemp(set_t); tft.write(247); tft.write(fahrenheit?'F':'C'); - tft.fillTriangle(149, 50, 159, 50, 154, 38, (set_t < TEMP_MAX) ? WHITE : GRAY); - tft.fillTriangle(149, 77, 159, 77, 154, 90, (set_t > TEMP_MIN) ? WHITE : GRAY); + tft.fillTriangle(140, 50, 150, 50, 145, 38, (set_t < TEMP_MAX) ? WHITE : GRAY); + tft.fillTriangle(140, 77, 150, 77, 145, 90, (set_t > TEMP_MIN) ? WHITE : GRAY); } } if (!off) { @@ -722,7 +778,7 @@ void display(void) { if (temperature < TEMP_COLD) { tft.print(F("COLD ")); } else { - tft.write(' '); + //tft.write(' '); printTemp(temperature); tft.write(247); tft.write(fahrenheit?'F':'C'); @@ -741,7 +797,7 @@ void display(void) { tft.setTextColor(YELLOW, BLACK); tft.setCursor(122,5); tft.setTextSize(2); - int power = min(15,v)*min(15,v)/4.8*pwm/255; + int power =v*v/4.8*pwm/255; if (power < 10) tft.write(' '); tft.print(power); tft.write('W'); @@ -776,15 +832,45 @@ void display(void) { power_source_old = power_source; } if (power_source == POWER_CORD) { - /*if (v > v_c3) { - tft.setTextSize(2); - tft.setTextColor(GREEN, BLACK); - tft.setCursor(0,5); - tft.print(v); + uint8_t red = 0; + uint8_t green = 0; + uint8_t percent = 0; + tft.setTextSize(1); + tft.setCursor(30,5); + + if (v > (v_c3+1.5)) { + if (v_c3 > 6.0) { + // on wall power, with battery present. + // we print a cyan symbol to indicate that we're (likely) charging, and a green one if completed. + const unsigned char *symbol = get_battery_symbol(v_c3+0.35, true, nullptr, nullptr, &percent); + uint16_t color = tft.Color565(red, green, 0); + if (percent > 98) { + color = GREEN; + tft.setTextColor(GREEN,BLACK); + tft.print("DC: "); tft.print(v, 1); tft.print("V"); + tft.fillRect(77, 5, 40, 9, BLACK); + } else { + color = CYAN; + tft.setTextColor(CYAN,BLACK); + tft.print("BAT: "); tft.print(v_c3 + 0.25 ,2); tft.print("V"); + tft.fillRect(89, 5, 32, 9, BLACK); + } + tft.drawBitmap(0, 5, symbol, 24, 9 , color, BLACK); + } else { + tft.drawBitmap(0, 5, power_cord, 24, 9 , GREEN, BLACK); + } + } else { + // on battery. + const unsigned char *symbol = get_battery_symbol(v_c3+0.35, false, &red, &green, &percent); + uint16_t color = tft.Color565(red, green, 0); + tft.drawBitmap(0, 5, symbol, 24, 9 , color, BLACK); + tft.setTextColor(WHITE,BLACK); + tft.print("BAT: "); + tft.print(v_c3+0.35, 1); tft.print("V "); - } else {*/ - tft.drawBitmap(0, 5, power_cord, 24, 9, tft.Color565(max(0, min(255, (14.5-v)*112)), max(0, min(255, (v-11)*112)), 0)); - //} + tft.print(percent); + tft.print("% "); + } } else if (power_source == POWER_LIPO || power_source == POWER_CHARGING) { float volt[] = {v_c1, v_c2-v_c1, v_c3-v_c2}; uint8_t volt_disp[] = {max(1,min(16,(volt[0]-3.0)*14.2)), max(1,min(16,(volt[1]-3.0)*14.2)), max(1,min(16,(volt[2]-3.0)*14.2))}; @@ -795,11 +881,11 @@ void display(void) { } } for (uint8_t i = 0; i < 3; i++) { - if (volt[i] < 3.20) { + if (volt[i] < MIN_VOLTS_PER_CELL) { setError(BATTERY_LOW); tft.fillRect(13, 7+14*i, volt_disp[i], 8, blink?RED:BLACK); } else { - tft.fillRect(13, 7+14*i, volt_disp[i], 8, tft.Color565(250-min(250, max(0, (volt[i]-3.4)*1000.0)), max(0,min(250, (volt[i]-3.15)*1000.0)), 0)); + tft.fillRect(13, 7+14*i, volt_disp[i], 8, tft.Color565(250-min(250, max(0, (volt[i]-MIN_VOLTS_PER_CELL)*1000.0)), max(0,min(250, (volt[i]-MIN_VOLTS_PER_CELL)*1000.0)), 0)); } tft.fillRect(13+volt_disp[i], 7+14*i, 17-volt_disp[i], 8, BLACK); } @@ -838,12 +924,19 @@ void display(void) { } void compute(void) { + static int16_t rising_protection_milestone_temperature = 0; + static int16_t rising_protection_timeout = WATCH_TEMP_PERIOD; + static int16_t rising_rebound_timeout = WATCH_TEMP_REBOUND; + static bool rising_protection_target_reached = false; + #ifndef USE_TFT_RESET setStandbyLayoff(!digitalRead(STBY_NO)); //do not measure while heater is active, potential is not neccessary == GND #endif cur_t = getTemperature(); if (off) { target_t = 0; + rising_protection_milestone_temperature = 0; + rising_protection_timeout = WATCH_TEMP_PERIOD; if (cur_t < adc_offset + TEMP_RISE) { threshold_counter = TEMP_UNDER_THRESHOLD; //reset counter } @@ -856,6 +949,53 @@ void compute(void) { if (cur_t-last_measured <= -30 && last_measured != 999) { setError(EXCESSIVE_FALL); //decrease of more than 30 degree is uncommon, short of ring and gnd is possible. } + + // if target_t has been lowered, make sure that we also lower that milestone temperature + if (target_t < rising_protection_milestone_temperature) { + rising_protection_milestone_temperature = target_t; + } + + // if target_t has been changed, timeouts needs to be reset. + if(old_target_t != target_t) { + rising_protection_timeout = WATCH_TEMP_PERIOD ; + rising_rebound_timeout = WATCH_TEMP_REBOUND; + old_target_t = target_t; + } + + // ensure that the temperature is actually rising when it should. + if(target_t - cur_t > WATCH_TEMP_DEACTIVATE ) { + // temperature is lower than setpoint by WATCH_TEMP_DEACTIVATE °C. + if (rising_protection_target_reached) { + // if previously we've been at target, e.g cleaning the tip might drop the temp significantly in a short time. + // so we need to temporarily suspend the protection and also continue it with a lower milestone. + if(rising_rebound_timeout) { rising_rebound_timeout--; } + else { + // rebound timeout expired, arm protection again. + rising_protection_milestone_temperature = cur_t + WATCH_TEMP_INCREASE; + rising_protection_target_reached = false; + } + } else { + // target was previously not reached, see if next milestone has been reached. + if (cur_t >= rising_protection_milestone_temperature + WATCH_TEMP_INCREASE) { + rising_protection_milestone_temperature = cur_t + WATCH_TEMP_INCREASE; // Yes, raise the bar. + rising_protection_timeout = WATCH_TEMP_PERIOD; // and give a new time window. + } else { + rising_protection_timeout--; // milestone not reached. + } + } + } else { + // we are near the target, time to disarm the protection and rearm the rebound timer. + rising_protection_timeout = WATCH_TEMP_PERIOD; + rising_rebound_timeout = WATCH_TEMP_REBOUND; + rising_protection_target_reached = true; + rising_protection_milestone_temperature = target_t - WATCH_TEMP_DEACTIVATE; // in case temperature has been lowered, follow the target. + } + + if(0 == rising_protection_timeout) { + // milestone not reached, E-STOP. + setError(FAILED_TO_HEAT); + } + if (cur_t < adc_offset + TEMP_RISE) { if (threshold_counter == 0) { setError(NOT_HEATING); //temperature is not reached in desired time, short of sensor and gnd too? @@ -872,10 +1012,19 @@ void compute(void) { last_measured = cur_t; heaterPID.Compute(); - if (error != NO_ERROR || off) + + // Power limitation. + // Tips are rated for 40W, do not exceed that. + // note t-hat we have inherently only 50% PWM, as we have it on 10ms and then wait with pwm off for another 10ms. + // Formula: P = 0.5 * ( U^2 / ( R )) * (pwm) + float pwm_max = 2 * PMAX / ((v*v) / 2.4); + if (pwm_max > pid_val) pwm_max = pid_val; + + if (error != NO_ERROR || off) { pwm = 0; - else - pwm = min(255,pid_val*255); + } else { + pwm = min(255, pwm_max * 255); + } analogWrite(HEATER_PWM, pwm); } @@ -915,7 +1064,7 @@ void loop(void) { if (sendNext <= millis()) { sendNext += 100; -#ifndef TEST_ADC +#ifdef TEST_ADC Serial.print(stored[0]); Serial.print(";"); Serial.print(stored[1]); @@ -941,8 +1090,8 @@ void loop(void) { Serial.print(v_c2); Serial.print(";"); Serial.println(v); -#endif Serial.flush(); +#endif display(); } if (Serial.available()) { diff --git a/Maiskolben_TFT/definitions.h b/Maiskolben_TFT/definitions.h index 58c2385..3aa6849 100644 --- a/Maiskolben_TFT/definitions.h +++ b/Maiskolben_TFT/definitions.h @@ -13,21 +13,37 @@ #define TEMP_STBY 150 #define TEMP_COLD (adc_offset + 15) +#define PMAX (40) // max watts to pump into the tip. (note: Weller specifies the tips as 40W; the big RT-11 has 55W) + #define SHUTOFF_ACTIVE #define BOOTHEAT_ACTIVE #define STANDBY_TIMEOUT 240 // seconds without any significant temperature drop, if exceeded it will standby #define OFF_TIMEOUT 900 // seconds in standby before turning off -#define TEMP_RISE 30 //threshold temperature, that must be exceeded delta in given time: +#define TEMP_RISE 30 //threshold temperature, that must be exceeded delta in given time: (from cold to ~70°C) #define TEMP_UNDER_THRESHOLD 80 // x (TIME_COMPUTE_IN_MS + DELAY_BEFORE_MEASURE) #define THRES_MAX_DECEED 2 //max times the threshold temperature may be undercut by the current temperature -//Temperature in degree to rise at least in given time -#define TEMP_MIN_RISE 10 -//Time in that the temperature must rise by the set temperature -#define TEMP_RISE_TIME 1000 +// Protection: Make sure that the tip gets actually hotter when heated. +#define WATCH_TEMP_PERIOD (1500/20) // Time allowed (in measurement cycles) ~20ms) for temperature to raise. (x/20) gives you around x ms. +#define WATCH_TEMP_INCREASE 5 // temperature must raise by this value (°C) +// Rebound: When tip is suddenly cooled (e.g cleaning, or soldering), allow this extra time before re-actviating the protection. +#define WATCH_TEMP_REBOUND (2000/20) // Time, when target has been reached previously, where the temp may drop without re-arming the protection +#define WATCH_TEMP_DEACTIVATE 15 // disarm the protection when the current temperature is close to the target temperature by this temperature (°C) +// voltage ranges for the voltage display. +#define NUM_CELLS (4) +#if (false) // For LiFePO4, set to false. Be sure to read not below +#define MIN_VOLTS_PER_CELL (3.0) // LiIon: Usually discharged until 3.0 V/cell +#define MAX_VOLTS_PER_CELL (3.6) // LiIon: Nominal at 3.6V +#define MAX_CHARGE_PER_CELL (4.2) // When charged, full at 4.2 +#else +// NOTE: LiFePO4 needs an own BMS! Do not use the LiIon charging circuitry that might be on your Maiskolben! Ignoring this warning can cause your cell to explode and may cause fires! +#define MIN_VOLTS_PER_CELL (3.0) // LiFePo in my case, they can go down to 2.0V, my BMS wi4,2ll lockout at 2.1. The 2.75 are margin. +#define MAX_VOLTS_PER_CELL (3.2) // LiFePo: nominal voltage is 3.2V +#define MAX_CHARGE_PER_CELL (3.6) // When charged, full at 3.6V +#endif //#define OLD_PWM // RX 0 @@ -55,12 +71,12 @@ #define VIN A7 #endif -#define kp 0.03 -#define ki 0.00001 +#define kp 0.035 +#define ki 0.0002 #define kd 0.0 #define TIME_COMPUTE_IN_MS 10 -#define TIME_MEASURE_VOLTAGE_IN_MS 200 +#define TIME_MEASURE_VOLTAGE_IN_MS 50 #define TIME_SW_POLL_IN_MS 10 #define DELAY_BEFORE_MEASURE 10 #define DELAY_MAIN_LOOP 10 @@ -96,7 +112,8 @@ typedef enum ERROR_TYPE { NOT_HEATING, NO_TIP, BATTERY_LOW, - USB_ONLY + USB_ONLY, + FAILED_TO_HEAT } error_type; const unsigned char power_cord [] PROGMEM = { @@ -111,6 +128,63 @@ const unsigned char power_cord [] PROGMEM = { 0x00, 0x00, 0xC0 }; +const unsigned char battery_0 [] PROGMEM = { + 0b11111111, 0b11111111, 0b11111100, + 0b10000000, 0b00000000, 0b00000111, + 0b10000000, 0b00000000, 0b00000101, + 0b10000000, 0b00000000, 0b00000101, + 0b10000000, 0b00000000, 0b00000101, + 0b10000000, 0b00000000, 0b00000111, + 0b11111111, 0b11111111, 0b11111100, + 0,0,0, 0,0,0, +}; + + +const unsigned char battery_25 [] PROGMEM = { + 0b11111111, 0b11111111, 0b11111100, + 0b11111100, 0b00000000, 0b00000111, + 0b11111100, 0b00000000, 0b00000101, + 0b11111100, 0b00000000, 0b00000101, + 0b11111100, 0b00000000, 0b00000101, + 0b11111100, 0b00000000, 0b00000111, + 0b11111111, 0b11111111, 0b11111100, + 0,0,0, 0,0,0, +}; + +const unsigned char battery_50 [] PROGMEM = { + 0b11111111, 0b11111111, 0b11111100, + 0b11111111, 0b11100000, 0b00000111, + 0b11111111, 0b11100000, 0b00000101, + 0b11111111, 0b11100000, 0b00000101, + 0b11111111, 0b11100000, 0b00000101, + 0b11111111, 0b11100000, 0b00000111, + 0b11111111, 0b11111111, 0b11111100, + 0,0,0, 0,0,0, +}; + +const unsigned char battery_75 [] PROGMEM = { + 0b11111111, 0b11111111, 0b11111100, + 0b11111111, 0b11111111, 0b00000111, + 0b11111111, 0b11111111, 0b00000101, + 0b11111111, 0b11111111, 0b00000101, + 0b11111111, 0b11111111, 0b00000101, + 0b11111111, 0b11111111, 0b00000111, + 0b11111111, 0b11111111, 0b11111100, + 0,0,0, 0,0,0, +}; + +const unsigned char battery_100 [] PROGMEM = { + 0b11111111, 0b11111111, 0b11111100, + 0b11111111, 0b11111111, 0b11111111, + 0b11111111, 0b11111111, 0b11111111, + 0b11111111, 0b11111111, 0b11111111, + 0b11111111, 0b11111111, 0b11111111, + 0b11111111, 0b11111111, 0b11111111, + 0b11111111, 0b11111111, 0b11111100, + 0,0,0, 0,0,0, +}; + + const unsigned char maiskolben [] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,