Hello,
I am trying to create a PID cooling system for a work project. The details of the hardware are as follows:
- RocketScream Mini Ultra Pro v3 (Arduino Zero ATSAMD21G18A-AU)
- Zero onboard RTC
- Adafruit thermocouple amplifier module MAX31856
- Two coil relays switched by D7 and D8 simultaneously (two solenoids for failure mitigation) through low-side transistor switches.
I am using the SAMD_timerinterrupt library for millisecond interrupt control and the AutoPID library for interval calculations.
The timing of solenoid opening and closing is controlled by two ISRs - first, an ISR attached to the onboard RTC fires every 10 seconds that the clock ticks, then resets that alarm 10 seconds into the future. The RTC ISR checks the current injection interval requested by the PID controller. If the interval is above a minimum value, the RTC ISR sets another ISR with the shutSolenoids() function attached on a hardware timer which triggers after the interval has elapsed. The ISR then runs shootSolenoids() if the ISR was successfully attached.
My problem arises during frequent cases when the post-interval ISR does not fire. Or, at least, none of the flags it should be changing are changed.
I have written a very undesirable "overshootManager" function which checks the state of the output pins after they are supposed to have closed. Semi-regularly, this overshoot manager will find that the digital pins are still HIGH when they should be LOW. This is not a problem with my coil relays sticking shut, as they are not connected to any load, and the digitalRead() confirms they are still internally assigned HIGH. The LED on the relay indicates that they are still receiving power.
#define TIMER_INTERRUPT_DEBUG 0
#define _TIMERINTERRUPT_LOGLEVEL_ 0
#define USING_TIMER_TC3 false
#define USING_TIMER_TC4 false
#define USING_TIMER_TC5 false
#define USING_TIMER_TCC true // Handles solenoid closing
#define USING_TIMER_TCC1 false
#define USING_TIMER_TCC2 false
#include "SAMDTimerInterrupt.h"
#include <Wire.h> // for I2C communication
#include <RTCZero.h> // for RTC
#include <Adafruit_MAX31856.h> // for thermocouple amplifier
#include <AutoPID.h> // PID lib
#define TC1_CS_PIN 3 // Gas temperature
#define PRIME_PIN 7 // Primary solenoid
#define INTLK_PIN 8 // Interlock solenoid
#define OUTPUT_MIN -255
#define OUTPUT_MAX 0
#define KP 8
#define KI 0
#define KD 0.3
double temp1, setPoint, outputVal, interval;
int TStart = 20; // Initial target degrees celsius
int dT = -1; // Change in C per hour
int holdDelay = 0; // Minutes to hold initial temperature
#define pwmPeriod 10 // injection interval; seconds
uint32_t start, now;
SAMDTimer ITimer0(TIMER_TCC);
AutoPID PID(&temp1, &setPoint, &outputVal, OUTPUT_MIN, OUTPUT_MAX, KP, KI, KD);
Adafruit_MAX31856 maxthermo1 = Adafruit_MAX31856(TC1_CS_PIN);
RTCZero rtc;
volatile bool flagOpen = false;
volatile bool flagClose = false;
volatile uint32_t lastInterval;
volatile uint32_t openTime;
volatile uint32_t closeTime;
void setup() {
SerialUSB.begin(9600);
delay(1000);
SerialUSB.println("Connected");
pinMode(PRIME_PIN, OUTPUT);
pinMode(INTLK_PIN, OUTPUT);
rtc.begin();
maxthermo1.begin();
maxthermo1.setThermocoupleType(MAX31856_TCTYPE_T);
maxthermo1.setConversionMode(MAX31856_CONTINUOUS);
PID.setTimeStep(500);
now = rtc.getEpoch();
start = now;
rtc.setAlarmEpoch(now + (pwmPeriod - 1));
rtc.enableAlarm(rtc.MATCH_SS);
rtc.attachInterrupt(manageSolenoids);
}
void loop() {
updateTemperature();
now = rtc.getEpoch();
updateSetpoint(); // Update setpoint based on temp profile and current time
PID.run(); // Update PID evaluation based on setpoint
interval = pwmPeriod * 0.7 * outputVal/OUTPUT_MIN; // Max inject interval is 70% of cycle
overshootManager(); // Check and correct failed solenoid close
// Debugging ISR timing
if (flagOpen) {
SerialUSB.print("Inject processed at "); SerialUSB.print(openTime);
SerialUSB.print(", For duration "); SerialUSB.println(lastInterval);
flagOpen = false;
}
if (flagClose) {
SerialUSB.print("Close processed at "); SerialUSB.print(closeTime);
SerialUSB.print(", real duration "); SerialUSB.println(closeTime - openTime);
flagClose = false;
}
}
void updateSetpoint() {
uint32_t elapsed = (((now - start) >= holdDelay*60) ? (now - (start + holdDelay*60)) : 0);
double TTarget = TStart + ((double)elapsed * dT / 3600);
setPoint = ((TTarget > -195) ? TTarget : -195); // Clamp setPoint to -195 if TTarget is lower
}
void updateTemperature() {
if (maxthermo1.conversionComplete()) {
temp1 = maxthermo1.readThermocoupleTemperature();
}
// We might handle failure to read temperature here. Not a problem currently.
}
void manageSolenoids() {
// Interrupt called by RTC alarm every PWM_PERIOD seconds.
// Sets new RTC interrupt for next evaluation period.
uint32_t epoch = rtc.getEpoch();
rtc.setAlarmEpoch(epoch + (pwmPeriod - 1));
uint32_t intervalMS = interval*1000;
// Open solenoids if inject interval >50ms (filter short injections)
// and set SAMD hardware timer interrupt to close after inject interval milliseconds.
if (intervalMS > 50) {
if (ITimer0.attachInterruptInterval_MS(intervalMS, shutSolenoids)) {
lastInterval = intervalMS;
shootSolenoids();
}
}
}
void shootSolenoids() {
digitalWrite(PRIME_PIN, HIGH);
digitalWrite(INTLK_PIN, HIGH);
flagOpen = true;
openTime = millis();
}
void shutSolenoids() {
digitalWrite(PRIME_PIN, LOW);
digitalWrite(INTLK_PIN, LOW);
ITimer0.disableTimer();
flagClose = true;
closeTime = millis();
}
void overshootManager() {
// Weird problem with interrupt failing to close solenoids...
uint32_t getNow = millis();
if ((getNow - lastInterval - 10) > openTime) { // Output pins should have been shut by now
if (digitalRead(PRIME_PIN) == HIGH) { // But in some cases, they're not
SerialUSB.println("Warning - overshoot detected");
shutSolenoids();
}
}
}
Here is a sample of the Serial output:
12:28:51.667 -> Inject processed at 86811, For duration 1254
12:28:52.890 -> Close processed at 88066, real duration 1255
12:29:01.569 -> Inject processed at 96773, For duration 1260
12:29:02.834 -> Warning - overshoot detected
12:29:02.834 -> Close processed at 98184, real duration 1411
12:29:11.561 -> Inject processed at 106736, For duration 1273
12:29:12.914 -> Close processed at 108010, real duration 1274
As you can see, the fourth line indicates the shutSolenoids function had not fired some time after it should have, so the overshootManager detected a failure to close. This is also shown by the discrepancy between the target duration of 1260ms but the real duration of 1411ms.
I'm not sure if I've created a race condition somehow, but the "Inject processed" message cannot appear unless the system confirms the ISR has been attached to ITimer0. Since shutSolenoids() is directly attached to this timer, I can't imagine why it would be failing to fire.