Merge pull request #358 from dhiltonp/watts

Changes the PID to work in watts instead of a raw PWM output, making the output automatically compensated for voltage and much, much smoother.
This commit is contained in:
Ben V. Brown
2018-10-30 11:25:04 +11:00
committed by GitHub
9 changed files with 196 additions and 133 deletions

View File

@@ -82,6 +82,7 @@ public:
static void drawCheckbox(bool state) {
drawSymbol((state) ? 16 : 17);
}
static void debugNumber(int32_t val);
static void drawSymbol(uint8_t symbolID);//Used for drawing symbols of a predictable width
static void drawArea(int16_t x, int8_t y, uint8_t wide, uint8_t height,
const uint8_t* ptr); //Draw an area, but y must be aligned on 0/8 offset

View File

@@ -117,9 +117,8 @@ enum TipType {
uint16_t lookupTipDefaultCalValue(enum TipType tipID);
uint16_t getHandleTemperature();
uint16_t getTipRawTemp(uint8_t instant);
uint16_t getTipRawTemp(uint8_t refresh);
uint16_t getInputVoltageX10(uint16_t divisor);
uint16_t getTipInstantTemperature();
uint8_t getTipPWM();
void setTipPWM(uint8_t pulse);
uint16_t ctoTipMeasurement(uint16_t temp);
@@ -129,8 +128,8 @@ uint16_t tipMeasurementToF(uint16_t raw);
void seekQC(int16_t Vx10,uint16_t divisor);
void setCalibrationOffset(int16_t offSet);
void setTipType(enum TipType tipType, uint8_t manualCalGain);
uint32_t calculateTipR(uint8_t useFilter);
int16_t calculateMaxVoltage(uint8_t useFilter, uint8_t useHP);
uint32_t calculateTipR();
int16_t calculateMaxVoltage(uint8_t useHP);
void startQC(uint16_t divisor); // Tries to negotiate QC for highest voltage, must be run after
// RToS
// This will try for 12V, failing that 9V, failing that 5V

View File

@@ -0,0 +1,41 @@
/*
* history.hpp
*
* Created on: 28 Oct, 2018
* Authors: Ben V. Brown, David Hilton
*/
#ifndef HISTORY_HPP_
#define HISTORY_HPP_
#include <stdint.h>
// max size = 127
template <class T=uint16_t, uint8_t SIZE=15>
struct history {
static const uint8_t size = SIZE;
T buf[size];
int32_t sum;
uint8_t loc;
void update(T const val) {
// step backwards so i+1 is the previous value.
loc = (size+loc-1) % size;
sum -= buf[loc];
sum += val;
buf[loc] = val;
}
T operator[] (uint8_t i) const {
// 0 = newest, size-1 = oldest.
i = (i+loc) % size;
return buf[i];
}
T average() const {
return sum / size;
}
};
#endif /* HISTORY_HPP_ */

View File

@@ -0,0 +1,23 @@
/*
* Power.hpp
*
* Created on: 28 Oct, 2018
* Authors: Ben V. Brown, David Hilton
*/
#include "stdint.h"
#include <history.hpp>
#ifndef POWER_HPP_
#define POWER_HPP_
const uint8_t hz = 32;
const uint8_t oscillationPeriod = 3.5 * hz;
extern history<uint16_t, oscillationPeriod> milliWattHistory;
int32_t tempToMilliWatts(int32_t rawTemp, uint16_t mass, uint8_t rawC);
void setTipMilliWatts(int32_t mw);
uint8_t milliWattsToPWM(int32_t milliWatts, uint8_t divisor);
int32_t PWMToMilliWatts(uint8_t pwm, uint8_t divisor);
#endif /* POWER_HPP_ */

View File

@@ -7,6 +7,7 @@
#include <string.h>
#include <OLED.hpp>
#include <stdlib.h>
#include "Translation.h"
#include "cmsis_os.h"
@@ -231,6 +232,20 @@ void OLED::printNumber(uint16_t number, uint8_t places) {
print(buffer);
}
void OLED::debugNumber(int32_t val) {
if (abs(val) > 99999) {
OLED::print(" OoB"); // out of bounds
return;
}
if (val >= 0) {
OLED::drawChar(' ');
OLED::printNumber(val, 5);
} else {
OLED::drawChar('-');
OLED::printNumber(-val, 5);
}
}
void OLED::drawSymbol(uint8_t symbolID) {
// draw a symbol to the current cursor location
setFont(2);

View File

@@ -643,26 +643,14 @@ static void settings_displayTipModel(void) {
static void calibration_displaySimpleCal(void) {
printShortDescription(18, 5);
}
static void dotDelay() {
for (uint8_t i = 0; i < 20; i++) {
getTipRawTemp(1); // cycle through the filter a fair bit to ensure we're stable.
OLED::clearScreen();
OLED::setCursor(0, 0);
for (uint8_t x = 0; x < i / 4; x++)
OLED::print(".");
OLED::refresh();
osDelay(50);
}
}
static void setTipOffset() {
setCalibrationOffset(0); // turn off the current offset
dotDelay();
// If the thermocouple at the end of the tip, and the handle are at
// equalibrium, then the output should be zero, as there is no temperature
// differential.
int32_t offset = 0;
uint32_t offset = 0;
for (uint8_t i = 0; i < 15; i++) {
offset += getTipRawTemp(1);
// cycle through the filter a fair bit to ensure we're stable.
@@ -697,7 +685,7 @@ static void calibration_enterSimpleCal(void) {
OLED::refresh();
osDelay(200);
waitForButtonPress();
dotDelay(); // cycle the filter a bit
// Now take the three hot measurements
// Assume water is boiling at 100C
uint32_t RawTipHot = getTipRawTemp(0) * 10;

View File

@@ -117,28 +117,16 @@ uint16_t lookupTipDefaultCalValue(enum TipType tipID) {
#endif
}
uint16_t getTipRawTemp(uint8_t instant) {
static int64_t filterFP = 0;
uint16_t getTipRawTemp(uint8_t refresh) {
static uint16_t lastSample = 0;
const uint8_t filterBeta = 7; // higher values smooth out more, but reduce responsiveness
if (instant == 1) {
uint16_t itemp = getTipInstantTemperature();
filterFP = (filterFP << filterBeta) - filterFP;
filterFP += (itemp << 9);
filterFP = filterFP >> filterBeta;
uint16_t temp = itemp;
itemp += lastSample;
itemp /= 2;
lastSample = temp;
return itemp;
} else if (instant == 2) {
filterFP = (getTipInstantTemperature() << 8);
return filterFP >> 9;
} else {
return filterFP >> 9;
if (refresh) {
lastSample = getTipInstantTemperature();
}
return lastSample;
}
uint16_t getInputVoltageX10(uint16_t divisor) {
// ADC maximum is 32767 == 3.3V at input == 28.05V at VIN
// Therefore we can divide down from there
@@ -332,7 +320,7 @@ void startQC(uint16_t divisor) {
QCMode = 0;
}
// Get tip resistance in milliohms
uint32_t calculateTipR(uint8_t useFilter) {
uint32_t calculateTipR() {
// We inject a small current into the front end of the iron,
// By measuring the Vdrop over the tip we can calculate the resistance
// Turn PA0 into an output and drive high to inject (3.3V-0.6)/(6K8+Rtip)
@@ -346,27 +334,19 @@ uint32_t calculateTipR(uint8_t useFilter) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);// Set low first
setTipPWM(0);
vTaskDelay(1);
uint32_t offReading = getTipInstantTemperature();
uint32_t offReading = getTipRawTemp(1);
for (uint8_t i = 0; i < 24; i++) {
if (useFilter == 0) {
vTaskDelay(1); // delay to allow it too stabilize
offReading += getTipInstantTemperature();
} else {
offReading += getTipRawTemp(0);
}
vTaskDelay(1); // delay to allow it to stabilize
offReading += getTipRawTemp(1);
}
// Turn on
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);// Set low first
vTaskDelay(1);// delay to allow it too stabilize
vTaskDelay(1); // delay to allow it too stabilize
uint32_t onReading = getTipInstantTemperature();
for (uint8_t i = 0; i < 24; i++) {
if (useFilter == 0) {
vTaskDelay(1); // delay to allow it too stabilize
onReading += getTipInstantTemperature();
} else {
onReading += getTipRawTemp(0);
}
vTaskDelay(1); // delay to allow it to stabilize
onReading += getTipRawTemp(1);
}
uint32_t difference = onReading - offReading;
@@ -391,11 +371,11 @@ static unsigned int sqrt32(unsigned long n) {
g |= c;
}
}
int16_t calculateMaxVoltage(uint8_t useFilter, uint8_t useHP) {
int16_t calculateMaxVoltage(uint8_t useHP) {
// This measures the tip resistance, then it calculates the appropriate
// voltage To stay under ~18W. Mosfet is "9A", so no issues there
// QC3.0 supports up to 18W, which is 2A @9V and 1.5A @12V
uint32_t milliOhms = calculateTipR(useFilter);
uint32_t milliOhms = calculateTipR();
// Check no tip
if (milliOhms > 10000)
return -1;

View File

@@ -3,6 +3,8 @@
#include <gui.hpp>
#include <main.hpp>
#include "LIS2DH12.hpp"
#include <history.hpp>
#include <power.hpp>
#include "Settings.h"
#include "Translation.h"
#include "cmsis_os.h"
@@ -36,7 +38,7 @@ int main(void) {
HAL_Init();
Setup_HAL(); // Setup all the HAL objects
HAL_IWDG_Refresh(&hiwdg);
setTipPWM(0); // force tip off
setTipMilliWatts(0); // force tip off
FRToSI2C::init(&hi2c1);
OLED::initialize(); // start up the LCD
OLED::setFont(0); // default to bigger font
@@ -87,6 +89,7 @@ int main(void) {
while (1) {
}
}
void printVoltage() {
OLED::printNumber(getInputVoltageX10(systemSettings.voltageDiv) / 10, 2);
OLED::drawChar('.');
@@ -547,8 +550,10 @@ static void gui_solderingMode(uint8_t jumpToSleep) {
if (systemSettings.detailedSoldering) {
OLED::setFont(1);
OLED::print(SolderingAdvancedPowerPrompt); // Power:
OLED::printNumber(getTipPWM(), 3);
OLED::print("%");
OLED::printNumber(milliWattHistory[0] / 1000, 2);
OLED::drawChar('.');
OLED::printNumber(milliWattHistory[0] / 100 % 10, 1);
OLED::drawChar('W');
if (systemSettings.sensitivity && systemSettings.SleepTime) {
OLED::print(" ");
@@ -735,7 +740,7 @@ void startGUITask(void const *argument __unused) {
uint8_t tempWarningState = 0;
bool buttonLockout = false;
bool tempOnDisplay = false;
getTipRawTemp(2); // reset filter
getTipRawTemp(1); // reset filter
OLED::setRotation(!(systemSettings.OrientationMode & 1));
uint32_t ticks = xTaskGetTickCount();
ticks += 400; // 4 seconds from now
@@ -796,11 +801,10 @@ void startGUITask(void const *argument __unused) {
OLED::setFont(0);
OLED::displayOnOff(true); // turn lcd on
#ifdef MODEL_TS80
//Here we re-check for tip presence
if (idealQCVoltage < 90)
idealQCVoltage = calculateMaxVoltage(1,
systemSettings.cutoutSetting);// 1 means use filtered values rather than do its own
seekQC(idealQCVoltage,systemSettings.voltageDiv);
//Here we re-check for tip presence
if (idealQCVoltage < 90)
idealQCVoltage = calculateMaxVoltage(systemSettings.cutoutSetting);
seekQC(idealQCVoltage,systemSettings.voltageDiv);
#endif
gui_solderingMode(0); // enter soldering mode
buttonLockout = true;
@@ -819,7 +823,7 @@ void startGUITask(void const *argument __unused) {
currentlyActiveTemperatureTarget = 0; // ensure tip is off
uint16_t tipTemp = tipMeasurementToC(getTipRawTemp(1)); // This forces a faster update rate on the filtering
uint16_t tipTemp = tipMeasurementToC(getTipRawTemp(0));
if (tipTemp < 50) {
if (systemSettings.sensitivity) {
@@ -918,37 +922,16 @@ void startPIDTask(void const *argument __unused) {
* struct
*
*/
setTipPWM(0); // disable the output driver if the output is set to be off
#ifdef MODEL_TS100
for (uint8_t i = 0; i < 50; i++) {
osDelay(10);
getTipRawTemp(1); // cycle up the tip temp filter
HAL_IWDG_Refresh(&hiwdg);
}
#else
// On the TS80 we can measure the tip resistance before cycling the filter a
// bit
idealQCVoltage = 0;
idealQCVoltage = calculateMaxVoltage(0, systemSettings.cutoutSetting);
// Rapidly cycle the filter to help converge
HAL_IWDG_Refresh(&hiwdg);
for (uint8_t i = 0; i < 50; i++) {
osDelay(11);
getTipRawTemp(1); // cycle up the tip temp filter
}
HAL_IWDG_Refresh(&hiwdg);
setTipMilliWatts(0); // disable the output driver if the output is set to be off
#ifdef MODEL_TS80
idealQCVoltage = calculateMaxVoltage(systemSettings.cutoutSetting);
#endif
int32_t rawC = ctoTipMeasurement(100) - ctoTipMeasurement(101); // 1*C change in raw.
currentlyActiveTemperatureTarget = 0; // Force start with no output (off). If in sleep / soldering this will
// be over-ridded rapidly
int32_t integralCount = 0;
int32_t derivativeLastValue = 0;
// be over-ridden rapidly
// REMEBER ^^^^ These constants are backwards
// They act as dividers, so to 'increase' a P term, you make the number
// smaller.
history<int16_t> tempError = {{0}, 0, 0};
const int32_t itermMax = 100;
pidTaskNotification = xTaskGetCurrentTaskHandle();
for (;;) {
if (ulTaskNotifyTake(pdTRUE, 50)) {
@@ -956,69 +939,54 @@ void startPIDTask(void const *argument __unused) {
// This is a call to block this thread until the ADC does its samples
uint16_t rawTemp = getTipRawTemp(1); // get instantaneous reading
if (currentlyActiveTemperatureTarget) {
// Compute the PID loop in here
// Because our values here are quite large for all measurements (0-32k
// ~= 66 counts per C) P I & D are divisors, so inverse logic applies
// (beware)
// Cap the max set point to 450C
if (currentlyActiveTemperatureTarget > ctoTipMeasurement(450)) {
currentlyActiveTemperatureTarget = ctoTipMeasurement(450);
}
int32_t rawTempError = currentlyActiveTemperatureTarget
- rawTemp;
// As we get close to our target, temp noise causes the system
// to be unstable. Use a rolling average to dampen it.
// We overshoot by roughly 1/2 of 1 degree Fahrenheit.
// This helps stabilize the display.
tempError.update(currentlyActiveTemperatureTarget - rawTemp + rawC/4);
int32_t ierror = (rawTempError
/ ((int32_t) systemSettings.PID_I));
// Now for the PID!
int32_t milliWattsOut = 0;
integralCount += ierror;
// P term - total power needed to hit target temp next cycle.
// thermal mass = 1690 milliJ/*C for my tip.
// = Watts*Seconds to raise Temp from room temp to +100*C, divided by 100*C.
// divided by 8 to let I term dominate near set point.
const uint16_t mass = 1690 / 8;
int32_t milliWattsNeeded = tempToMilliWatts(tempError.average(), mass, rawC);
milliWattsOut += milliWattsNeeded;
if (integralCount > (itermMax / 2))
integralCount = itermMax / 2; // prevent too much lead
else if (integralCount < -itermMax)
integralCount = itermMax;
// I term - energy needed to compensate for heat loss.
// We track energy put into the system over some window.
// Assuming the temp is stable, energy in = energy transfered.
// (If it isn't, P will dominate).
milliWattsOut += milliWattHistory.average();
int32_t dInput = (rawTemp - derivativeLastValue);
/*Compute PID Output*/
int32_t output = (rawTempError
/ ((int32_t) systemSettings.PID_P));
if (((int32_t) systemSettings.PID_I))
output += integralCount;
if (((int32_t) systemSettings.PID_D))
output -= (dInput / ((int32_t) systemSettings.PID_D));
if (output > 100) {
output = 100; // saturate
} else if (output < 0) {
output = 0;
}
if (currentlyActiveTemperatureTarget < rawTemp) {
output = 0;
integralCount = 0;
derivativeLastValue = 0;
}
setTipPWM(output);
derivativeLastValue = rawTemp; // store for next loop
// D term - use sudden temp change to counter fast cooling/heating.
// In practice, this provides an early boost if temp is dropping
// and counters extra power if the iron is no longer losing temp.
// basically: temp - lastTemp
// Unfortunately, our temp signal is too noisy to really help.
setTipMilliWatts(milliWattsOut);
} else {
setTipPWM(0); // disable the output driver if the output is set to be off
integralCount = 0;
derivativeLastValue = 0;
setTipMilliWatts(0);
}
HAL_IWDG_Refresh(&hiwdg);
} else {
if (currentlyActiveTemperatureTarget == 0) {
setTipPWM(0); // disable the output driver if the output is set to be off
integralCount = 0;
derivativeLastValue = 0;
setTipMilliWatts(0);
}
}
}
}
#define MOVFilter 8
void startMOVTask(void const *argument __unused) {
OLED::setRotation(true);

View File

@@ -0,0 +1,48 @@
/*
* power.cpp
*
* Created on: 28 Oct, 2018
* Authors: Ben V. Brown, David Hilton
*/
#include <power.hpp>
#include <Settings.h>
#include <hardware.h>
const uint8_t tipResistance = 87;
const uint8_t maxPWM = 255;
history<uint16_t, oscillationPeriod> milliWattHistory = {{0}, 0, 0};
int32_t tempToMilliWatts(int32_t rawTemp, uint16_t mass, uint8_t rawC) {
// mass is in milliJ/*C, rawC is raw per degree C
int32_t milliJoules = mass * rawTemp / rawC;
return milliJoules * hz;
}
void setTipMilliWatts(int32_t mw) {
int32_t output = milliWattsToPWM(mw, systemSettings.voltageDiv / 10);
setTipPWM(output);
uint16_t actualMilliWatts = PWMToMilliWatts(output, systemSettings.voltageDiv / 10);
milliWattHistory.update(actualMilliWatts);
}
uint8_t milliWattsToPWM(int32_t milliWatts, uint8_t divisor) {
int32_t v = getInputVoltageX10(divisor); // 1000 = 10v
int32_t availableMilliWatts = v*v / tipResistance;
int32_t pwm = maxPWM * milliWatts / availableMilliWatts;
if (pwm > maxPWM) {
pwm = maxPWM;
} else if (pwm < 0) {
pwm = 0;
}
return pwm;
}
int32_t PWMToMilliWatts(uint8_t pwm, uint8_t divisor) {
int32_t v = getInputVoltageX10(divisor);
return pwm * (v*v / tipResistance) / maxPWM;
}