Interrupt arduino routine to run a slow delay based process - c++

I have an arduino that does mostly data collection and sends it to an ESP8266 over serial. Serial communication to the ESP is not quick as you may know and it depends on a lot of waiting. I have a button and I want to immediately stop any data collection or sending and have it open a door. The door opening takes about 30 seconds. What's the best way to do this?
Not the full code, but it goes something like the below.
Of course this doesn't work because you can't use WHILE or DELAY in an ISR, but I don't know how to restructure it.
attachInterrupt(4 , openadoor, FALLING);
void loop(){
gathersomedata();
senddatatoESP();
if(wait_for_esp_response(2000,"OK")) lightGreenLED();
else lightRedLED();
}
byte wait_for_esp_response(int timeout, const char* term) {
unsigned long t = millis();
bool found = false;
int i = 0;
int len = strlen(term);
while (millis() < t + timeout) {
if (Serial2.available()) {
buffer[i++] = Serial2.read();
if (i >= len) {
if (strncmp(buffer + i - len, term, len) == 0) {
found = true;
break;
}
}
}
}
buffer[i] = 0;
}
void openadoor(){
while (doortimer + dooropentime >= millis() && digitalRead(openbutton) == HIGH && digitalRead(closebutton) == HIGH) {
digitalWrite(DoorOpenRelay, LOW);
}
digitalWrite(DoorOpenRelay, HIGH);
}

TL;DR - see Nick's Answer. :-)
Without the complete code, I can only guess at a few things:
1) You shouldn't wait in an ISR. Even calling millis() is discouraged, as it depends on the Timer0 ISR getting called, which will be prevented as long as you're in your openadoor ISR.
2) In general, the ISR should only do things that are very quick... think microseconds. That's tens to hundreds of instructions, which can be just a few lines of code. Even digitalWrite is almost too slow. If there's more to do, you should just set a volatile flag that is watched in loop. Then loop can do the time-consuming work.
3) Calculating elapsed time must be in this form:
if (millis() - startTime >= DESIRED_TIME)
where startTime is the same type as millis(), a uint32_t:
uint32_t startTime;
You set startTime whereever it's appropriate:
startTime = millis();
This avoids the rollover problem, when millis() rolls over from 232-1 to 0.
4) It looks like you know how to "block" until a certain amount of time has elapsed: the while loop will keep your sketch at that point. If you just change it to an if statement, the Arduino can continue on its way to handle other things.
Because loop happens so quickly, the if statement will check the time very frequently... unless you delay or block somewhere else, like wait_for_esp_response. :-( That while loop should change to an if statement as well. The routine is more like check_for_esp_response.
5) You have to track the state of the door opening and closing process. This is a Finite-State machine problem. Nick has a good description here, too. You can use the enum type to define the states that the door can be in: CLOSED, OPENING, OPENED and CLOSING.
When the OPEN button is pressed, you can look at the state and see if you should start opening it. Then start a timer, turn on the relay and, most importantly, set the state to OPENING. Next time through loop, you can test the state (a switch statement), and for the OPENING case, look at the time to see if it has been long enough. If it has set the state to OPENED. And so on.
If I incorporate all these things into your sketch, it should start to look like this:
volatile bool doorOpenPressed = false;
volatile bool doorClosePressed = false;
static const uint32_t DOOR_OPEN_TIME = 30000UL; // ms
static const uint32_t DOOR_CLOSE_TIME = 30000UL; // ms
static const uint32_t DATA_SAMPLE_TIME = 60000UL; // ms
static uint32_t lastDataTime, sentTime, relayChanged;
static bool waitingForResponse = false;
static uint8_t responseLen = 0;
enum doorState_t { DOOR_CLOSED, DOOR_OPENING, DOOR_OPENED, DOOR_CLOSING };
doorState_t doorState = DOOR_CLOSED;
void setup()
{
attachInterrupt(4 , openadoor, FALLING);
}
void loop()
{
// Is it time to take another sample?
if (millis() - lastDataTime > DATA_SAMPLE_TIME) {
lastDataTime = millis();
gathersomedata();
// You may want to read all Serial2 input first, to make
// sure old data doesn't get mixed in with the new response.
senddatatoESP();
sentTime = millis();
waitingForResponse = true;
responseLen = 0; // ready for new response
}
// If we're expecting a response, did we get it?
if (waitingForResponse) {
if (check_for_esp_response("OK")) {
// Got it!
lightGreenLED();
waitingForResponse = false;
} else if (millis() - sentTime > 2000UL) {
// Too long!
lightRedLED();
waitingForResponse = false;
} // else, still waiting
}
// Check and handle the door OPEN and CLOSE buttons,
// based on the current door state and time
switch (doorState) {
case DOOR_CLOSED:
if (doorOpenPressed) {
digitalWrite(DoorOpenRelay, LOW);
relayChanged = millis();
doorState = DOOR_OPENING;
}
break;
case DOOR_OPENING:
// Has the door been opening long enough?
if (millis() - relayChanged > DOOR_OPEN_TIME) {
digitalWrite(DoorOpenRelay, HIGH);
doorState = DOOR_OPENED;
} else if (!doorOpenPressed && doorClosePressed) {
// Oops, changed their mind and pressed the CLOSE button.
// You may want to calculate a relayChanged time that
// is set back from millis() based on how long the
// door has been opening. If it just started opening,
// you probably don't want to drive the relay for the
// full 30 seconds.
...
}
break;
case DOOR_OPENED:
if (doorClosePressed) {
...
}
break;
case DOOR_CLOSING:
if (millis() - relayChanged > DOOR_CLOSE_TIME) {
...
}
break;
}
}
void openadoor()
{
doorOpenPressed = true;
}
bool check_for_esp_response(const char* term)
{
bool found = false;
if (Serial2.available()) {
// You should make sure you're not running off the end
// of "buffer" here!
buffer[responseLen++] = Serial2.read();
int len = strlen(term);
if (responseLen >= len) {
if (strncmp(buffer + responseLen - len, term, len) == 0) {
found = true;
}
}
}
return found;
}
The key is that you don't block or delay anywhere. loop gets called over and over, and it just checks a few variables. Most of the time, there's nothing to do. But sometimes, based on the state or the current time, it gathers some data, sends it, reads the response, and opens or closes the door. These actions do not interfere with each other, because there are no blocking while loops, only quick checks with if statements.

Open the door in the ISR and set a flag. Also store the time when you opened it. Both of those variables should be declared volatile.
Then in your main loop see if:
The flag is set; and
Time is up
If so, close the door (and clear the flag).
May I assume that setting the variables as "volatile" will prevent the compiler optimizing them? If so, then would you mind explaining why you thought this necessary.
Variables modified inside an ISR may change when the compiler does not expect them to. Using volatile tells the compiler to reload such variables from RAM (and not cache them into a register) so it always gets the most up-to-date copy.
Just as an example, say you had a flag set inside an ISR. And in your main (non-ISR) code you had this:
flag = false;
while (!flag)
{ } // wait for flag to be set
The compiler looks at that and thinks "well, flag will never change" and optimizes away the test for it changing. With volatile though, the compiler keeps the test, because it has to keep reloading flag from RAM.

Related

How to count milliseconds on the Raspberry Pi Pico?

I want to count milliseconds on my Pico to make nonblocking code in C++.
Arduino has the millis() function, but after looking though all the example programs and scouring the internet I couldn't find anything like that for the Pico using C++.
This is the BlinkWithoutDelay sketch from the Arduino examples that shows what I want to do:
const int ledPin = LED_BUILTIN;// the number of the LED pin
// Variables will change:
int ledState = LOW; // ledState used to set the LED
// Generally, you should use "unsigned long" for variables that hold time
// The value will quickly become too large for an int to store
unsigned long previousMillis = 0; // will store last time LED was updated
// constants won't change:
const long interval = 1000; // interval at which to blink (milliseconds)
void setup() {
// set the digital pin as output:
pinMode(ledPin, OUTPUT);
}
void loop() {
// here is where you'd put code that needs to be running all the time.
// check to see if it's time to blink the LED; that is, if the difference
// between the current time and last time you blinked the LED is bigger than
// the interval at which you want to blink the LED.
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
// save the last time you blinked the LED
previousMillis = currentMillis;
// if the LED is off turn it on and vice-versa:
if (ledState == LOW) {
ledState = HIGH;
} else {
ledState = LOW;
}
// set the LED with the ledState of the variable:
digitalWrite(ledPin, ledState);
}
}
I decided to go with a repeating timer as suggested by some comments and the examples that used it really difficult to understand for me. So I'll try to explain it the best I can.
*Note that I don't really understand some of the values being passed into functions but I know enough to make this work. Please feel free to correct me
#include "pico/stdlib.h"
bool led_state;
//Function Called by the timer
bool timer_callback( repeating_timer_t *rt )
{
//Blinks the LED
led_state = !led_state;
gpio_put(25, led_state);
return (true);
}
int main(){
//From what I can tell this creates an identifier for the timer
static repeating_timer_t timer;
stdio_init_all();
//GPIO setting (GPIO_25 is the LED pin on the pico)
gpio_init(25);
gpio_set_dir(25, GPIO_OUT);
//Sets a repeating timer to call a function every 1000 milliseconds
//add_repeating_timer_ms( int interval_in_milliseconds, ?&function to call?, ???, ?timer identifier? );
add_repeating_timer_ms( 1000, &timer_callback, NULL, &timer );
while(true){
//Blinks LED briefly at a different interval than the repeating timer
//This is to show that my code isn't blocked
gpio_put(25, !led_state);
sleep_ms(50);
gpio_put(25, led_state);
sleep_ms(450);
}
return 0;
}

ESP32 Simple button hold for 3 seconds example. Output doesn't seem to make sense?

Hoping someone can see what I'm missing as it's gotta be right there staring at me in the face..
I've got this code (below) set up on an ESP32 to spawn a thread that simply monitors the state of a pin connected to a switch. Essentially this code is supposed to wait for the button to be held for 3 seconds and then do something important. The actual input seems to read fine, but for some reason once I've pressed the button, the button state is stuck for like 15 seconds after un-pressing the switch.
For example,
Press the switch, the actualBtnState reads 1, buttonState reads 1 (after 50us),and btnPressTime increments as expected.
Release switch, actualBtnState reads 0, btnState reads 1, and btnPressTime stops incrementing.
After 50us, expecting to see btnState read 0 and then trigger the else or elseif blocks (depending on how long the button was held). Actual results continue to read btnState = 1 and btnPressTime = [whatever the last held time was] for a solid 15 seconds or more. actuyalBtnState reads correctly at 0 this entire time and for some reason lastDebounceTime keeps incrementing?
I should note that this is part of a much larger project, hence the threading. I also can't seem to print anything within the resetBtnCB function as I immediately get a "guru mediation error kernel panic whatever-the-error-is" error and the esp reboots.
Code:
#include <Arduino.h>
#define BUTTON_PIN 27
// Variables will change:
int buttonState; // the current reading from the input pin
int lastButtonState = LOW; // the previous reading from the input pin
unsigned long lastDebounceTime = 0; // the last time the output pin was toggled
unsigned long debounceDelay = 50; // the debounce time; increase if the output flickers
unsigned long buttonPressTime = 0; //Amount of time the button has been held down
unsigned long actualBtnState = 0; //The actual reading from the pin without noise filtering
void resetBtnCB(void *pvParameters)
{
pinMode(BUTTON_PIN, INPUT);
while (true)
{
// read the state of the switch into a local variable:
int reading = digitalRead(BUTTON_PIN);
actualBtnState = reading;
// If the switch changed, due to noise or pressing:
if (reading != lastButtonState)
{
// reset the debouncing timer
lastDebounceTime = millis();
}
unsigned long timeSinceDebounce = millis() - lastDebounceTime;
if (timeSinceDebounce > debounceDelay)
{
// whatever the reading is at, it's been there for longer than the debounce
// delay, so take it as the actual current state:
buttonState = reading;
if (buttonState == HIGH)
{
buttonPressTime += timeSinceDebounce;
}
else if (buttonPressTime > 300)
{
buttonPressTime = 0;
// SUCCESS! Do something important here as we've held the button for x seconds
}
else
{
buttonPressTime = 0;
}
}
// save the reading. Next time through the loop, it'll be the lastButtonState:
lastButtonState = reading;
vTaskDelay(10);
}
}
void setup()
{
Serial.begin(115200);
xTaskCreate(resetBtnCB, "reset_button", 1024, NULL, 10, NULL);
}
void loop()
{
char debug[512];
sprintf(debug, "button state %u, lastD %u, buttonPressTime %u, actualBtnState %u, lastBtnState %u", buttonState, lastDebounceTime, buttonPressTime, actualBtnState, lastButtonState);
Serial.println(debug);
delay(50);
yield();
}

Arduino, Run Time Code Error in Debouncing Voltage Time Delay Code

I am Ansh Goel, I was learning Arduino from Udemy. I am beginner to this field. I was creating a code for Debouncing the button to solve the issues of bouncing voltage. But there is error in the code. There is no Compile time error but it is run time error.
I also tried to check the code using the Serial.print() to find the where the error is, then I found that the error is in the second nested if condition. I have also mention where there is the error in the code for ease. There I am not able to get the Serial.print("A") function too to the Serial Monitor.
My main motive is to run the code so that I am able to stop bouncing voltages when a button is pressed using some delay.
It is from line 41
This is the code I used to debounce the button
const int btn_pin = 2;
const int debounce_delay = 50; //ms
// We need to remember the previous button state between loops
int btn_prev = HIGH;
int btn_state = HIGH;
unsigned long last_debounce_time = 0;
// Counter
int counter = 0;
void setup() {
Serial.begin(9600);
// Set up pins
pinMode(btn_pin, INPUT_PULLUP);
pinMode(13, OUTPUT);
}
void loop() {
int btn_read;
// Read current button state
btn_read = digitalRead(btn_pin);
//Remember when the button change state
// If the button was previously HIGH and now LOW, it's been pressed
if ( (btn_prev == HIGH) && (btn_read == LOW )) {
//Store the time it took to take the action for button press
last_debounce_time = millis();
}
//Wait before changing the state of the button
// IN THIS CONDITION THERE IS ERROR SOMEWHERE I AM NOT GETTING IT
if(millis() > (last_debounce_time + debounce_delay)){
if(btn_read != btn_state) {
Serial.println("A");
// Then store the button change value to the global variable
btn_state = btn_read;
if(btn_state == LOW) {
// Increment and print counter
counter++;
Serial.println(counter);
digitalWrite(13,HIGH);
delay(500);
digitalWrite(13,LOW);
delay(500);
}
}
}
// Remember the previous button state for the next loop iteration
btn_prev = btn_state;
}
For testing purposes, this is the circuit design on TinkerCad, that you can check online.
TinkerCad Circuit Design
Please Help me solve the issue, it will be a great help from your side for me.
There are several locations where your code could malfunction:
you're not looping properly for loop
your logic doesn't work
digitalRead doesn't work
print doesn't work
first, remove the debounce check and see if this works:
//if(millis() > (last_debounce_time + debounce_delay)){
to check all other issues, add the following right before the remaining if:
delay so you don't get endless data
print millis, last_debounce_time, debounce_delay and btn_read
end line
then run and press the button. The output will let you know what's the issue

Detecting continuous motion using millis with a PIR sensor

I am trying to detect when continuous motion has been triggered from a PIR sensor for more than 8 seconds. Here is what I have. When sensor is LOW 'no motion...' is displayed, then short motion fires the first '< 8 sec' IF statement. When sensor returns to LOW - no motion is displayed as it should but then when motion is detected a second time, the code seems to freeze and nothing happens.
unsigned long startMillis;
boolean timingFlag = false;
const int buttonPin = 2;
int buttonState = 0;
void setup() {
pinMode(buttonPin, INPUT);
Serial.begin(19200);
delay(500);
}
void loop() {
buttonState = digitalRead(buttonPin);
if (buttonState == HIGH && millis() - startMillis <= 8000UL)
{
Serial.println("Motion Detected but less than 8");
delay(1000);
//the PIR timed out with in the three seconds so cancel timing
timingFlag = false; //disable timing
}
if (buttonState == LOW)
{
Serial.println("No Motion...");
delay(1000);
timingFlag = true; //enable timing
}
//when nine seconds have gone by with consistant detection do something
if (timingFlag == false && millis() - startMillis >= 9000UL)
{
//There has now been nine seconds of constant PIR detection
Serial.println("Motion Detected and greater than 9 sec");
delay(1000);
//Do Something
}
}
There is one very obvious problem with your current code, as Scheff already mentioned in the comments: You never actually set your startMillis to anything, so they are probably (but not necessarily) always 0.
This means, that the statement if (buttonState == HIGH && millis() - startMillis <= 8000UL) will always be false after 8000 ms (until millis() flows over, after around 50 days* ), so timingFlag will never be reset to false after that. This ultimately leads to your "freezing" situation.
I tried to find a good place to set the startMillis in your code, but I honestly find it a little confusing, so I allowed myself to rewrite your logic, hope you don't mind. (Please note that I also changed the variable names from button to detector, since it seemed more fitting to me):
(This version triggers at the transitions from HIGH to LOW)
// define the threshold, after which an action shall be triggered
const int detectionThreshold = 8000;
const int detectorPin = 2;
unsigned long startTime = 0;
int lastDetectorState = LOW;
void setup() {
pinMode(detectorPin, INPUT);
Serial.begin(19200);
delay(500);
}
void triggerDetectionAction(){
// do whatever needs to be done after 8 seconds of motion in here
}
void loop() {
int currentDetectorState = digitalRead(detectorPin);
// if detector is low, no motion is detected
if( currentDetectorState == LOW ){
// when the detector is LOW, we want to check if the last state was HIGH
// because then we just arrived at the transition from HIGH to LOW =>
// "something was detected" to "there is no longer something detected"
if( lastDetectorState == HIGH ){
// then, we can get the total duration, the detection lasted
unsigned long detectionDuration = millis() - startTime;
// and print it for easier debugging
Serial.print("Detection ended after ");
Serial.print(detectionDuration);
Serial.println(" milliseconds");
// finally, we check if the durations was more than
// or equal to our threshold
if( detectionDuration >= detectionThreshold ){
// and trigger stuff if necessary
triggerDetectionAction();
}
}
// if last detector state was LOW too,
// we don't want to do anything
}else{
// here we wan't to check for the transition of LOW to HIGH,
// so we check our last detector state
if( lastDetectorState == LOW ){
// if we caught the transition,
// set the start time to the current millis
startTime = millis();
// we could also set an indicator LED
// or Serial.print something here
Serial.println("Detection started");
}
// otherwise, we don't wan't to do anything
}
// finally, we save our current state into the last state,
// so we have it available in the next loop
lastDetectorState = currentDetectorState;
// do your other loop stuff here
}
Please note that I couldn't test the code at the time writing, so there may be (syntax) errors
*More about millis and overflow here: https://www.arduino.cc/reference/en/language/functions/time/millis/
Update: This version will trigger immediately when the threshold is reached. It also includes an example how to trigger an action once and every loop after the threshold was reached.
// define the threshold, after which an action shall be triggered
const int detectionThreshold = 8000;
const int detectorPin = 2;
unsigned long startTime = 0;
int lastDetectorState = LOW;
bool actionTriggered = false;
void setup() {
pinMode(detectorPin, INPUT);
Serial.begin(19200);
delay(500);
}
void triggerOnce(){
// this will be called once, when the threshold is reached
}
void triggerEveryLoop(){
// this will be called every loop, after the threshold was reached
// for as long as the detector stays high
}
void loop() {
int currentDetectorState = digitalRead(detectorPin);
if( currentDetectorState == LOW ){
if( lastDetectorState == HIGH ){
// since we want to trigger immediately when the threshold is reached,
// we actually don't need this transition any longer.
// We can still keep it for debugging reasons thought.
// If you don't need this, you can simply remove the entire block
unsigned long detectionDuration = millis() - startTime;
Serial.print("Detection ended after ");
Serial.print(detectionDuration);
Serial.println(" milliseconds");
}
}else{
// Check for LOW => HIGH transition change
if( lastDetectorState == LOW ){
// if we caught the transition,
// set the start time to the current millis
startTime = millis();
// and reset the flag
actionTriggered = false;
// we could also set an indicator LED
// or Serial.print something here
Serial.println("Detection started");
}else{
// otherwise we want to check the duration
unsigned long detectionDuration = millis() - startTime;
// and trigger immediatley when the threshold is reached
if( detectionDuration >= detectionThreshold ){
// as long as it wasn't already triggered before
if( !actionTriggered ){
Serial.println("Threshold reached, triggering");
// now we also need to set a flag, so we know we already triggerd this action once
actionTriggered = true;
triggerOnce();
}
// we can also do something every loop
// this can be handy for e.g. blinking a light or playing a sound or something
triggerEveryLoop();
}
}
}
// finally, we save our current state into the last state,
// so we have it available in the next loop
lastDetectorState = currentDetectorState;
// do your other loop stuff here
}

Arduino: interrupt expensive function and resume another

As a first project I plan on making a teensyduino ambient light with different light modes, which are checked in a big switch statement - now I want to switch from one mode to another by pressing a button.
googling lead me to using interrupts, but there is one point that is not clear - if I press the button during an expensive function, which takes long time and has many variables in use, what happens if i call the main loop from the interrupt, does the remaining state of remain in the ram and leads to a stackoverflow if I do switch too many times or is it cleared.
Here some code:
const int speed = 30 //milliseconds
const int modes = 11; //maximum number of modes
const int red = 15;
const int green = 14;
const int blue = 12;
volatile int mode = 0;
void setup() {
pinMode(red , OUTPUT);
pinMode(green , OUTPUT);
pinMode(blue , OUTPUT);
randomSeed(analogRead(0));
Serial.begin(9600);
attachInterrupt(0,incMode,CHANGE); // 0 -> digital pin 2
}
void loop() {
switch(mode){
case 0:{
Serial.println("powerdown");
setAll(0);
delay(1000);
break;
}
\\...
case modes:{
\\ expensive long function
}
}
}
void blinkAll(int times){
for(int i=1;i <= times;i++){
setAll(255);
delay(speed*17);
setAll(0);
delay(speed*17);
}
}
void setAll(int bright){
analogWrite(red , bright);
analogWrite(green , bright);
analogWrite(blue , bright);
}
void incMode(){
delay(speed);
blinkAll(2); //to indicate mode has changed
mode = (mode+1) % (modes+1); //switch starts with 0 so use "% modes+1"!
Serial.println("mode increased");
//--> loop();
//--> would resume the main loop but lead to a stackoverflow i presume
}
How would I break out of the running function without delay and stack pollution.
I know I could just set the mode and wait until the function has ended, but if I have a mode that takes minutes to end I want to be able to switch from it immediately.
PS.: Though I am using a teensyduino, I will use the arduino tag, and as I don't know what language the arduinio uses the tags c/c++. Please change this if it is not appropriate.
You would eventually overflow the stack if you were to reenter main from the interrupt handler multiple times recursively. Additionally, since you'll still be in the interrupt handler as far as the hardware is concerned, you'll have all kinds of weirdness - in particular, interrupts are blocked when you're already in an interrupt, which means delay() won't work and millis() won't count up, and various other things will be broken as well unless you figure out some way to manually re-enable interrupts.
A better way to solve this would be to make your 'expensive long function' instead be a state machine driven by a cheap, short function that is called very frequently. Your interrupt handler can then simply set a flag that is checked on entry into this function, at which point the current mode (ie, current state machine) is changed.
This approach also makes it easier to define new lighting modes. For example, you could define something like this:
struct phase {
unsigned char r, g, b, delay;
};
unsigned long t_nextPhase;
volatile struct phase *forceMode = NULL;
struct phase *mode = blinkAll;
int nextPhase = 0;
struct phase blinkAll[] = {
{ 255, 255, 255, 17 },
{ 0, 0, 0, 17 },
{ 0, 0, 0, 255 } // loop sentinel
};
void lighting_kernel() {
noInterrupts(); // ensure we don't race with interrupts
if (forceMode) {
mode = forceMode;
forceMode = NULL;
t_nextPhase = millis();
nextPhase = 0;
}
interrupts();
if (t_nextPhase > millis()) {
return;
}
struct phase *cur_phase;
do {
cur_phase = mode[nextPhase++];
if (cur_phase->delay == 255) {
nextPhase = 0;
}
} while (cur_phase->delay == 255);
analogWrite(red , cur_phase->r);
analogWrite(green , cur_phase->g);
analogWrite(blue , cur_phase->b);
t_nextPhase = millis() + cur_phase->delay;
}
Now to define a new lighting mode you just need a new array of colors and times, rather than writing new code. Adding things like color ramps and other such effects is left as an exercise to the reader.