heater_controller.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. #include "heater_controller.h"
  2. #include "diagnostic_queue.h"
  3. #include "integer_arithmetic.h"
  4. #include "numeric_conversion.h"
  5. #include "program_memory.h"
  6. #include "shot_control.h"
  7. #include "telemetry_reports.h"
  8. namespace meCoffee {
  9. namespace {
  10. constexpr int16_t kMinimumUsableAggregate = 100;
  11. constexpr uint16_t kMinimumMappedAmplitude = 80;
  12. int16_t signedSetpointForMode(const FirmwareState& state) {
  13. return state.alternateModeSelection == 0 ? state.settings.brewSetpoint : state.settings.steamSetpoint;
  14. }
  15. int32_t lowerAccumulatorBound(const SettingsValues& settings) {
  16. return signedDivide(100 * static_cast<int32_t>(settings.integralAccumulatorMinimum), settings.integralGain);
  17. }
  18. int32_t upperAccumulatorBound(const SettingsValues& settings) {
  19. return signedDivide(100 * static_cast<int32_t>(settings.integralAccumulatorMaximum), settings.integralGain);
  20. }
  21. int32_t clampAccumulatorCandidate(const SettingsValues& settings, int32_t candidate) {
  22. const int32_t lowerBound = lowerAccumulatorBound(settings);
  23. const int32_t afterLower = candidate > lowerBound ? candidate : lowerBound;
  24. const int32_t upperBound = upperAccumulatorBound(settings);
  25. return afterLower < upperBound ? afterLower : upperBound;
  26. }
  27. uint16_t mapDemandToAmplitudeCached(HeaterState* heater, uint16_t demand, uint16_t filteredPhase) {
  28. if (!heater->hasMappedAmplitude || heater->mappedDemand != demand || heater->mappedPhase != filteredPhase) {
  29. heater->mappedAmplitude = mapDemandToAmplitude(demand, filteredPhase);
  30. heater->mappedDemand = demand;
  31. heater->mappedPhase = filteredPhase;
  32. heater->hasMappedAmplitude = true;
  33. }
  34. return heater->mappedAmplitude;
  35. }
  36. void writeHeaterAmplitudeThroughWrapper(FirmwareState* state, uint16_t demand) {
  37. uint16_t mappedAmplitude = mapDemandToAmplitudeCached(&state->heater, demand, state->mainsPhase.filteredTicksPerHalfCycle);
  38. if (mappedAmplitude < kMinimumMappedAmplitude) {
  39. mappedAmplitude = 0;
  40. }
  41. setGroupAmplitude(&groupForRole(&state->outputChannels, OutputGroupRole::Boiler), mappedAmplitude);
  42. }
  43. void reportHeaterEnableChange(FirmwareState* state, bool shouldEnable) {
  44. enqueueDiagnosticProgramMemoryText(&state->diagnosticQueue,
  45. shouldEnable ? MECOFFEE_PROGRAM_TEXT("pid 65536 0 0 0") : MECOFFEE_PROGRAM_TEXT("pid 0 0 0 0"));
  46. }
  47. } // namespace
  48. uint16_t computeDemandFromTerms(int32_t proportional, int32_t integral, int32_t derivative, int32_t feedForward) {
  49. const uint32_t wrappedSum = static_cast<uint32_t>(proportional) + static_cast<uint32_t>(integral) +
  50. static_cast<uint32_t>(derivative) + static_cast<uint32_t>(feedForward);
  51. const int32_t signedSum = static_cast<int32_t>(wrappedSum);
  52. int32_t clamped = signedSum;
  53. if (clamped < 0) {
  54. clamped = 0;
  55. } else if (clamped > 65535) {
  56. clamped = 65535;
  57. }
  58. return static_cast<uint16_t>(clamped >> 6);
  59. }
  60. uint16_t mapDemandToAmplitude(uint16_t demand, uint16_t filteredPhase) {
  61. const float scaledDemand = static_cast<float>(demand) * 3476.47998046875f;
  62. const uint32_t floorOfScaledDemand = convertFloatToUnsigned32(scaledDemand, nullptr);
  63. const int32_t first = static_cast<int16_t>(static_cast<uint16_t>(floorOfScaledDemand >> 10));
  64. const uint32_t demandWide = demand;
  65. const uint32_t second = (demandWide * demandWide * 2303u) >> 20;
  66. const uint32_t residual = static_cast<uint32_t>(first) - second - 2396u;
  67. const int32_t demandTimesResidual = static_cast<int32_t>(demandWide * residual);
  68. const uint32_t intermediate = static_cast<uint32_t>(signedDivideBy1024(demandTimesResidual)) + 1418u;
  69. const int32_t phaseTimesIntermediate = static_cast<int32_t>(static_cast<uint32_t>(filteredPhase) * intermediate);
  70. return static_cast<uint16_t>(signedDivide(phaseTimesIntermediate, 1608));
  71. }
  72. void initializeHeaterController(FirmwareState* state, uint32_t currentMilliseconds) {
  73. HeaterState& heater = state->heater;
  74. heater = HeaterState{};
  75. heater.integralAccumulator = kInitialIntegralAccumulator;
  76. heater.previousAggregate = kInitialPreviousAggregate;
  77. heater.periodTimerBaseline = currentMilliseconds;
  78. }
  79. void updateControllerPeriodTimer(FirmwareState* state, uint32_t foregroundSnapshotMilliseconds) {
  80. HeaterState& heater = state->heater;
  81. if (static_cast<uint32_t>(foregroundSnapshotMilliseconds - heater.periodTimerBaseline) > state->settings.controllerUpdatePeriod) {
  82. heater.isPeriodElapsed = true;
  83. heater.periodTimerBaseline = foregroundSnapshotMilliseconds;
  84. } else {
  85. heater.isPeriodElapsed = false;
  86. }
  87. }
  88. void resetIntegralAccumulator(FirmwareState* state) {
  89. const SettingsValues& settings = state->settings;
  90. const uint16_t setpoint = static_cast<uint16_t>(settings.brewSetpoint);
  91. const uint16_t measurement = static_cast<uint16_t>(state->sensor.firstChannelAggregate);
  92. const uint16_t difference = static_cast<uint16_t>(setpoint - measurement);
  93. const uint16_t span = static_cast<uint16_t>(settings.integralAccumulatorMaximum - settings.integralAccumulatorMinimum);
  94. uint32_t candidate = unsignedDivide(static_cast<uint32_t>(difference) * span, setpoint);
  95. candidate += settings.integralAccumulatorMinimum;
  96. candidate *= 100u;
  97. candidate = unsignedDivide(candidate, settings.integralGain);
  98. state->heater.integralAccumulator = clampAccumulatorCandidate(settings, static_cast<int32_t>(candidate));
  99. }
  100. void runHeaterController(FirmwareState* state, uint32_t foregroundSnapshotMilliseconds) {
  101. HeaterState& heater = state->heater;
  102. const SettingsValues& settings = state->settings;
  103. OutputGroup& boiler = groupForRole(&state->outputChannels, OutputGroupRole::Boiler);
  104. const int16_t aggregate = state->sensor.firstChannelAggregate;
  105. if (aggregate < kMinimumUsableAggregate) {
  106. setGroupEnabled(&boiler, false);
  107. return;
  108. }
  109. if (state->alternateModeSelection != 0 && heater.bypassSelector == 0) {
  110. setGroupAmplitude(&boiler, 0);
  111. const bool shouldEnable = aggregate < settings.steamSetpoint;
  112. if (shouldEnable != (boiler.isEnabled != 0)) {
  113. reportHeaterEnableChange(state, shouldEnable);
  114. }
  115. setGroupEnabled(&boiler, shouldEnable);
  116. return;
  117. }
  118. if (heater.isPeriodElapsed) {
  119. const int16_t setpoint = signedSetpointForMode(*state);
  120. const int16_t measurement = state->sensor.firstChannelAggregate;
  121. const int16_t error = static_cast<int16_t>(static_cast<uint16_t>(setpoint) - static_cast<uint16_t>(measurement));
  122. heater.proportionalTerm = static_cast<int32_t>(error) * static_cast<int32_t>(settings.proportionalGain);
  123. bool isIntegrationFrozen = false;
  124. if (heater.shotBeginMarker != 0) {
  125. if (heater.shotEndMarker == 0) {
  126. isIntegrationFrozen = true;
  127. } else if (foregroundSnapshotMilliseconds < heater.shotEndMarker + heater.retainedLockDuration) {
  128. isIntegrationFrozen = true;
  129. } else {
  130. heater.shotEndMarker = 0;
  131. heater.shotBeginMarker = 0;
  132. }
  133. }
  134. const uint16_t warmThreshold = static_cast<uint16_t>(static_cast<uint16_t>(setpoint) - (static_cast<uint16_t>(setpoint) >> 4));
  135. if (!isIntegrationFrozen && static_cast<uint16_t>(measurement) > warmThreshold) {
  136. const int32_t candidate = static_cast<int32_t>(static_cast<uint32_t>(heater.integralAccumulator) + static_cast<uint32_t>(error));
  137. heater.integralAccumulator = clampAccumulatorCandidate(settings, candidate);
  138. }
  139. heater.integralTerm = signedDivide(
  140. static_cast<int32_t>(static_cast<uint32_t>(settings.integralGain) * static_cast<uint32_t>(heater.integralAccumulator)), 100);
  141. const int16_t delta = static_cast<int16_t>(heater.previousAggregate - static_cast<uint16_t>(measurement));
  142. const int32_t derivativeBase = static_cast<int32_t>(delta) * static_cast<int32_t>(settings.derivativeGain);
  143. const uint32_t factor = (heater.shotBeginMarker != 0 && delta < 0) ? 2u : 1u;
  144. heater.derivativeTerm = static_cast<int32_t>(static_cast<uint32_t>(derivativeBase) * factor);
  145. heater.previousAggregate = static_cast<uint16_t>(measurement);
  146. heater.demand = computeDemandFromTerms(heater.proportionalTerm, heater.integralTerm, heater.derivativeTerm, heater.feedForwardTerm);
  147. enqueueDiagnosticCallback(&state->diagnosticQueue, reportHeaterTermsFrame);
  148. if (settings.continuousHeaterControlEnabled != 0) {
  149. writeHeaterAmplitudeThroughWrapper(state, heater.demand);
  150. setGroupEnabled(&boiler, true);
  151. } else {
  152. setGroupAmplitude(&boiler, 0);
  153. }
  154. }
  155. if (settings.continuousHeaterControlEnabled != 0) {
  156. const uint32_t sampleScale = state->sensor.firstRangeFlag == 0 ? 11000u : 22000u;
  157. const uint16_t continuousMeasurement = static_cast<uint16_t>((static_cast<uint32_t>(state->sensor.firstRing[0]) * sampleScale) >> 10);
  158. const int16_t setpoint = signedSetpointForMode(*state);
  159. const int16_t error = static_cast<int16_t>(static_cast<uint16_t>(setpoint) - continuousMeasurement);
  160. heater.proportionalTerm = static_cast<int32_t>(error) * static_cast<int32_t>(settings.proportionalGain);
  161. heater.demand = computeDemandFromTerms(heater.proportionalTerm, heater.integralTerm, heater.derivativeTerm, heater.feedForwardTerm);
  162. writeHeaterAmplitudeThroughWrapper(state, heater.demand);
  163. } else {
  164. const uint16_t onTime = static_cast<uint16_t>(5u * heater.demand);
  165. setGroupEnabled(&boiler, static_cast<uint32_t>(foregroundSnapshotMilliseconds - heater.periodTimerBaseline) < onTime);
  166. }
  167. if (isInputQualified(state->shotInput)) {
  168. const uint32_t shotDuration = foregroundSnapshotMilliseconds - state->shotInput.beginTimestamp;
  169. const uint16_t pressureDemand = computePressureDemand(settings, shotDuration);
  170. setGroupAmplitude(&groupForRole(&state->outputChannels, OutputGroupRole::Pump),
  171. mapDemandToAmplitude(pressureDemand, state->mainsPhase.filteredTicksPerHalfCycle));
  172. }
  173. }
  174. void beginShotFeedForward(FirmwareState* state, uint32_t currentMilliseconds) {
  175. HeaterState& heater = state->heater;
  176. heater.shotBeginMarker = currentMilliseconds;
  177. heater.shotEndMarker = 0;
  178. const int32_t product = static_cast<int32_t>(65535u * static_cast<uint32_t>(state->settings.shotFeedForwardPercentage));
  179. const float quotient = static_cast<float>(product) / 100.0f;
  180. heater.feedForwardTerm = convertFloatToSigned32(quotient);
  181. }
  182. void endShotFeedForward(FirmwareState* state, uint32_t currentMilliseconds) {
  183. HeaterState& heater = state->heater;
  184. if (heater.shotBeginMarker != 0) {
  185. heater.shotEndMarker = currentMilliseconds;
  186. heater.retainedLockDuration = static_cast<uint32_t>(state->settings.shotFeedForwardLockDuration) * 1000u;
  187. }
  188. heater.feedForwardTerm = 0;
  189. }
  190. uint16_t computePressureDemand(const SettingsValues& settings, uint32_t durationMilliseconds) {
  191. const int16_t delta = static_cast<int16_t>(settings.pressureProfileEnd - settings.pressureProfileStart);
  192. const float denominator = static_cast<float>(settings.pressureProfileDuration) * 1000.0f;
  193. const float ratio = static_cast<float>(durationMilliseconds) / denominator;
  194. const float value = ratio * static_cast<float>(delta) + static_cast<float>(static_cast<int16_t>(settings.pressureProfileStart));
  195. const uint16_t scaledValue = static_cast<uint16_t>(convertFloatToUnsigned32(value, nullptr));
  196. const float scaled = static_cast<float>(scaledValue) * 1024.0f / 100.0f;
  197. if (scaled > 1023.0f) {
  198. return 1023;
  199. }
  200. return static_cast<uint16_t>(convertFloatToUnsigned32(scaled, nullptr));
  201. }
  202. } // namespace meCoffee