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.
Related
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();
}
First I'd like to say I'm very new to c++, that's why I'm using the Arduino core and libraries on ESP32, and I would like to apologize for the dumpster fire you're about to see below.
Simply making a custom keyboard with buttons and encoders. When booted, select one of two modes: blekeyboard or ble midi control surface.
The button works in both modes but the encoder only works in whichever mode is declared last. (so in this script order, both the encoder and button in mode 1 blekeyboard works, while only the button works in mode 2.)
What did I do wrong and what can I do? Any suggestions regarding the problem or the overall script is welcome.
Thank you in advance.
#include <Arduino.h>
#include <BleKeyboard.h>
BleKeyboard bleKeyboard;
#define ESP32
#include <encoder.h>
#include <Control_Surface.h>
#include <MIDI_Interfaces/BluetoothMIDI_Interface.hpp>
BluetoothMIDI_Interface midi;
const int usermodebutton1 = 2;
const int usermodebutton2 = 0;
int usermode = 0;
// ---------------------- mode 2 MIDI Input Elements ------------------------ //
using namespace MIDI_Notes;
NoteButton csButton1 = {
2,
note(C, 4),
};
CCRotaryEncoder csEnc1 = {
{26, 25}, // pins
MCU::V_POT_1, // MIDI address (CC number + optional channel)
1, // optional multiplier if the control isn't fast enough
};
// -------------------------- mode 1 blekeyboard --------------------------- //
int kbutton1 = 2;
int kbutton1State;
int keyInterval = 400000;
Encoder kencoder1(25, 26);
int encInterval = 5000;
TickType_t currentTime;
TickType_t previousTime;
long enc1_oldPos = -999;
// ============================================================================= //
void setup()
{
pinMode(usermodebutton1, INPUT_PULLUP);
pinMode(usermodebutton2, INPUT_PULLUP);
Serial.begin(115200);
Serial.println("");
Serial.println("select mode:");
// ----------------------------------------------------------------------------- //
while (true)
{
if (digitalRead(usermodebutton1) == LOW)
{
usermode = 1;
Serial.println("mode 1 selected");
break;
}
if (digitalRead(usermodebutton2) == LOW)
{
usermode = 2;
Serial.println("mode 2 selected");
break;
}
delay(1000);
}
// ----------------------------------------------------------------------------- //
if (usermode == 1)
{
Serial.println("setup mode 1");
Serial.println("Starting BLE work...");
bleKeyboard.begin();
pinMode(kbutton1, INPUT_PULLUP);
previousTime = 0;
}
if (usermode == 2)
{
Serial.println("setup mode 2");
Serial.println("Control Surface BLE starting...");
RelativeCCSender::setMode(relativeCCmode::TWOS_COMPLEMENT);
Control_Surface.begin(); // Initialize Control Surface
}
}
// ============================================================================= //
void loop()
{
while (usermode == 1)
{
while (bleKeyboard.isConnected())
{
// mode 1 encoders
long enc1_newPos = kencoder1.read();
currentTime = esp_timer_get_time();
if (enc1_newPos < enc1_oldPos && currentTime - previousTime > encInterval)
{
enc1_oldPos = enc1_newPos;
previousTime = currentTime;
// bleKeyboard.write(KEY_MEDIA_VOLUME_DOWN);
Serial.print("enc1: ");
Serial.println(enc1_newPos);
}
if (enc1_newPos > enc1_oldPos && currentTime - previousTime > encInterval)
{
enc1_oldPos = enc1_newPos;
previousTime = currentTime;
// bleKeyboard.write(KEY_MEDIA_VOLUME_UP);
Serial.print("enc1: ");
Serial.println(enc1_newPos);
}
// mode 1 keys
kbutton1State = digitalRead(kbutton1);
if (kbutton1State == LOW && currentTime - previousTime > keyInterval)
{
previousTime = currentTime;
Serial.println("button 1 pressed");
bleKeyboard.print("1");
}
}
}
while (usermode == 2)
{
Control_Surface.loop(); // Refresh all elements
}
}
First of all, your code needs general tidying up. Do not instantiate an object after the #include or #define section.
Avoid doing this:
#include <BleKeyboard.h>
BleKeyboard bleKeyboard;
#include <MIDI_Interfaces/BluetoothMIDI_Interface.hpp>
BluetoothMIDI_Interface midi;
I generally organise my code as:
// Include libraries
#include <lib1.h>
#include <lib2.h>
...
// Macros
#define SOMETHING_FUNNY value // see the Macro name in capitals?
#define SOMETHING_USEFUL anothervalue
...
/*
* Variables Section.
* Also, I usually categorize my variables section by type: floats, integers, chars, etc. If I ever use booleans I pack them in a section called 'flags'. Also, if a boolean is used inside an interrupt it should be volatile
*/
Type var_name1 = initial_value; // the initaliztion is optional
AnotherType var_name2;
...
// Object instantiation
ClassName object1;
AnotherClassName object2 = new AnotherClassName(constructor_parameters);
...
//Setup Function
void setup(){
// Serial Port initialization goes first
Serial.begin(baudrate);
// Initialization routines --> use functions!
}
void loop(){
// Check for states of your FSM and call the respective function
}
// Functions definitions
output type myFunction1(args){
// Routine
}
...
Second, your code shouts to me 'Finite-States Machines', which is basically what you are implementing but not in a conventional way. Please read Nick Gammon's amazing tutorial on FSMs. This way, you will enumerate your states and trigger actions or events depending on the user's selection or hardware inputs without blocking the flow of the code.
Now, I assume that you have read both the .cpp files from your two libraries and checked if there are no conflicts between them? Also, which encoder.h library are you using? It looks like Paul Stoffregen's library, is it? I ask because his library is heavily based on interrupts and it was coded with some ASM blocks that might be not optimized for the ESP32, but please don't quote me on that. As enhzflep said:
I'd suspect there to be a problem with clashing interrupts.
and I could not agree more on that. If you are using that library, there is a Macro that could save your life:
#define ENCODER_DO_NOT_USE_INTERRUPTS
used in this example from the library's repo
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
}
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.
As usual, I know how to bypass this problem with some ugly patch work, but I want to make it elegant: I would like to make a small wrapper for motors commanded by an Arduino, which would unfortunately mean writing a distinct interrupt routine for each instance of motor because it has to modify the right step counter (member variable of the motor class) to determine its rate. However those functions would obviously have the same processing...
My question is: how can I determine which counter to modify in a unique interrupt routine?
Here is what I have so far, I'd like to hide interrupts from the user.
class Motor {
volatile int counter;
unsigned long lastUpdate; //Last timestamp update (for calculating the rate)
static const unsigned int RESOLUTION = 1024; //Number of steps in one rev
static const unsigned int RPMS_TO_RPM = 60000; //Convers rev/ms to rpm
public:
Motor() : counter(0)
{
lastUpdate = millis();
}
void encoderInput(bool pinA, bool pinB)
{
counter += (pinA ^ pinB)*(-1)+!(pinA ^ pinB);
}
int getRate() {
int ret = float(counter)/RESOLUTION/(millis() - lastUpdate)*RPMS_TO_RPM;
lastUpdate = millis();
counter = 0;
return ret;
}
};
/* Example:
* Motor motor1;
*
* void motor1_isr(void) {
* motor1.encoderInput(PIN_A, PIN_B);
* }
*
* void setup() {
* attachInterrupt(PIN_I, motor1_isr, CHANGE);
* Serial.begin(9600);
* }
*
* void loop() {
* Serial.println(motor1.getRate());
* delay(1000);
* }
*/
Thanks for your help, I think it would be useful to other people as well once it's done with :)
Regards,
Mister Mystère
You are facing a fundamental problem: an ISR is called with no data. In other words, the ISR function is not provided any data that indicates its source of the interrupt.
The basic approach is that the person writing the code provides the linkage between input and action by hardcoding a function. The tricky approach you are looking for is a method by which the ISR can figure out what the source was.
So you want to have N motors with 2N inputs from quadrature encoders. A single interrupt handler is attached to all the input pins on a CHANGE condition. The same handler gets called for all N motors. It can figure out which Motor to update by comparing the input pins to the values the last time it was called. If the input pins changed, then call that motor. Here is a psuedo code
Motor motor1;
Motor motor2;
onSomethingChanged() {
static int in1aprev, in1bprev;
static int in2aprev, in2bprev;
int in1a, in1b;
int in2a, in2b;
in1a = digitalRead(....
same for other in's
if( (in1a!=in1alast) || (in1b!=in1blast)) {
motor1.encoderInput(in1a,in1b);
in1alast = in1a;
in1blast = in1b;
}
if( (in2a!=in2alast) || (in2b!=in1blast)) {
motor2.encoderInput(in2a,in2b);
in1a2ast = in2a;
in1b2ast = in2b;
}
return;
}
Not so long ago this type of function would be handled in an entire chip (see programmable interrupt controller ). The chip implements all the logic to trigger and capture the source of the interrupt. The main CPU just gets a trigger "something happened". The handler polls the chip to ask "what happened".
Having offered this method, I'm not sure I would recommend. You cannot hide what is going on with your code. The motor must be physically wired to the correct pins. You have consumed a scarce resource -- you have to tell people.