[SOLVED] Problem using interrupt pin change with a joystick

I made a simple remote using an arduino nano, an nrf24L01 module and a 3 axis joystick. The joystick uses 3 analog pins, 1 for the X axis, 1 for the Y and 1 for click. As it is a remote, it works on battery and stays off most of the time. Therefore I want it to stay asleep and to wake up as soon as any bouton is used. There is the code:

#define CLIENT_ADDRESS 1
#define SERVER_ADDRESS 2
#define DEBUG 1

#include <avr/sleep.h>
#include <RHReliableDatagram.h>
#include <RH_NRF24.h>
#include <SPI.h>
#include "DebugUtils.h"

RH_NRF24 driver;
RHReliableDatagram manager(driver, CLIENT_ADDRESS);

const byte XPIN = A1;
const byte YPIN = A2;
const byte CPIN = A3;
const byte LEDPIN = 2;

const unsigned int wakeTimeout = 3000;
unsigned long previousWakeTimeout;

ISR (PCINT1_vect) { sleep_disable(); DEBUG_PRINT("*"); } // handle pin change interrupt for A0 to A5

void setup () 
{
    Serial.begin(9600); delay(100);
    pinMode(LEDPIN, OUTPUT);
    pinMode(XPIN, INPUT);
    pinMode(YPIN, INPUT);
    pinMode(CPIN, INPUT);

    digitalWrite(LEDPIN, LOW);
    DEBUG_PRINTLN("CLIENT");
    if (!manager.init()) DEBUG_PRINTLN("init failed");

    PCMSK1=(1<<PCINT9)|(1<<PCINT10)|(1<<PCINT11); //want pins A1 to A3
    PCIFR|= bit (PCIF1); // clear any outstanding interrupts
    PCICR|= bit (PCIE1); // enable pin change interrupts for A0 to A5

    DEBUG_PRINT("Idle x;y value: ("); DEBUG_PRINT(analogRead(XPIN)); DEBUG_PRINT(";"); DEBUG_PRINT(analogRead(YPIN)); DEBUG_PRINTLN(")");
}

void loop () 
{
    if (millis() - previousWakeTimeout >= wakeTimeout)
    {
        previousWakeTimeout = millis();
        DEBUG_PRINTLN("SLEEP"); delay(100);
        set_sleep_mode (SLEEP_MODE_PWR_DOWN);
        sleep_mode ();
    }
    readInput();
}

void readInput()
{
    if (digitalRead(CPIN) == 0) { rf_sendString("Click"); }
    else if (analogRead(YPIN) > 700) { rf_sendString("Down"); }
    else if (analogRead(YPIN) < 400) { rf_sendString("Up"); }
    else if (analogRead(XPIN) > 700) { rf_sendString("Left"); }
    else if (analogRead(XPIN) < 400) {  rf_sendString("Right"); }
}

void rf_sendString(char str[RH_NRF24_MAX_MESSAGE_LEN])
{
    previousWakeTimeout = millis();
    uint8_t rf_data[RH_NRF24_MAX_MESSAGE_LEN];
    strcpy((char*)rf_data, str); 
    digitalWrite(LEDPIN, HIGH);
    manager.sendtoWait(rf_data, sizeof(rf_data), SERVER_ADDRESS);
    digitalWrite(LEDPIN, LOW); 
    //delay(70);
    
    DEBUG_PRINT((char*)rf_data);
    DEBUG_PRINT(" ("); DEBUG_PRINT(analogRead(XPIN)); DEBUG_PRINT(";"); DEBUG_PRINT(analogRead(YPIN)); DEBUG_PRINTLN(")");
}

Strangely, it works fine when the joystick is clicked, or pushed Up or Right, but does not work for Left nor Down. Here's the serial output, where the interrupt is represented by an asterisk (*) and the X,Y shown in parantheses:

CLIENT
Idle x;y value: (513;518)
**Down (513;737)
Down (513;1023)
Down (513;1023)
Down (513;518)
*Up (*513;*0)
Up (*513;*0)
Up (*513;*0)
Up (513;518)
*Left (1023;518)
Left (1023;518)
Left (1023;518)
Left (1022;518)
Left (513;518)
*Right (0;*518)
*Right (0;*518)
*Right (513;517)

Notice how the interrupt is triggered for all Up and Right gesture, but not for Down and Left, altought they use the same pins. I suspect that it might have something to do with the type of change, as Up and Right are decreasing values, and Down and Left are increasing values. Any hint would be greatly appreciated!

Pin-change is a digital function. It is only interested in the digital level HIGH or LOW. It can't see that the analog input which was LOW is now LOWER.

If it's just a wakeup, then a quick waggle left-right is probably OK to wake the machine up. If you actually want to read 'left' without first going right or pressing another button, then you have work to do.

A comparator is able to compare two analog voltages and change a digital output as a result. Most (all?) Arduino chips do have an analog comparator module. It is rarely used and there aren't any simple Arduino functions to make it easy to use. The comparator can be used to generate an interrupt. But there's only one comparator. I don't even know if it could be applied to the X and Y inputs at the same time.

You could also add an external comparator. Something that will give a change on a digital pin when the joystick goes left or down. I'm thinking that it might be possible to add a couple of resistors to tap off that signal and push it to the other side of the high/low decision point, so that a LOW to LOWER transition pulls another pin from HIGH to LOW.

Thank you for your great advices. I tried the analogComp library without success. I read differents informations and wasn't sure if AIN0 and AIN1 are tied to either D6 and D7 or A6 and A7 on the atmega 328p. According to the datasheet, I think it is the analog ports. Anyway, even if I don't add any wires, by activating the ACSR flag the interrupt is always triggered (#), like so (I reversed Up and Down values from the initial code):

SLEEP
*###########Right (381;513)
**##########Down (518;513)
*##########Up (518;519)
##########Left (518;513)
##########Up (518;513)
##########Left (518;513)
##########Up (518;513)
*##########Right (518;513)
**##########Down (518;513)
##########Down (518;513)
**##########Right (518;513)
**##########Down (517;513)
*
SLEEP

Unfortunatly, the analog compare interrupt is only triggered when the IC is awake. This is the (stripped) code with ACSR enabled;

#include <avr/sleep.h>
const byte XPIN = A1;
const byte YPIN = A2;
const byte CPIN = A3;
const unsigned int wakeTimeout = 5000;
unsigned long previousWakeTimeout;

ISR (PCINT1_vect) { sleep_disable(); Serial.print("*"); } // handle pin change interrupt for A0 to A5
ISR (ANALOG_COMP_vect) { sleep_disable(); Serial.print("#"); }

void setup () 
{
    Serial.begin(9600); delay(100);
    pinMode(XPIN, INPUT);
    pinMode(YPIN, INPUT);
    pinMode(CPIN, INPUT);
    
    //Disable the digital input buffers to avoid excessive current at midrange signal levels
    DIDR1 = (1<<AIN1D) | (1<<AIN0D);

    // Setup the voltage comparator
    ACSR = 
    (0<<ACD) |   // Analog Comparator: Enabled
    (1<<ACI) |   // Analog Comparator Interrupt Flag: Clear Pending Interrupt
    (1<<ACIE); //|   // Analog Comparator Interrupt: Enabled
/*
    (0<<ACBG) |   // Analog Comparator Bandgap Select: AIN0 is applied to the positive input
    (0<<ACO) |   // Analog Comparator Output: Off
    (0<<ACIC) |   // Analog Comparator Input Capture: Disabled
    (1<<ACIS1) | (1<ACIS0);   // Analog Comparator Interrupt Mode: Comparator Interrupt on Rising Output Edge
*/
    PCMSK1=(1<<PCINT9)|(1<<PCINT10)|(1<<PCINT11); //add interrupt of pins A1, A2, A3
    PCIFR|= bit (PCIF1); // clear any outstanding interrupts
    PCICR|= bit (PCIE1); // enable pin change interrupts for A0 to A5

    Serial.print("Idle x;y value: ("); Serial.print(analogRead(XPIN)); Serial.print(";"); Serial.print(analogRead(YPIN)); Serial.println(")");
}

void loop () 
{
    if (millis() - previousWakeTimeout >= wakeTimeout)
    {
        previousWakeTimeout = millis();
        Serial.println(""); Serial.println("SLEEP"); delay(50);
        set_sleep_mode (SLEEP_MODE_PWR_DOWN);
        sleep_mode ();
    }
    readInput();
}

void readInput()
{
    if (digitalRead(CPIN) == 0) { rf_sendString("Click"); }
    else if (analogRead(YPIN) > 700) { rf_sendString("Up"); }
    else if (analogRead(YPIN) < 400) { rf_sendString("Down"); }
    else if (analogRead(XPIN) > 700) { rf_sendString("Left"); }
    else if (analogRead(XPIN) < 400) {  rf_sendString("Right"); }
}

void rf_sendString(char str[10])
{
    previousWakeTimeout = millis();
    Serial.print(str);
    Serial.print(" ("); Serial.print(analogRead(XPIN)); Serial.print(";"); Serial.print(analogRead(YPIN)); Serial.println(")");
    delay(100);
}

Finally, I noticed that:
XPIN + GND = Right + *Interrupt
XPIN + VCC = Left
YPIN + GND = Down + *Interrupt
YPIN + VCC = Up

When enabled, altough the analog comparator is triggered everytime, it won't get the IC out of sleep. I also tried to power the joystick with an external source and it did not help. Worst case scenario, I'll add a tilt switch. But I would really like to get it to work properly, or at least understand what is going on!


EDIT: I found out that

When the arduino is in SLEEP_MODE_PWR_DOWN the only way to wake it is with either a watchdog timer interrupt, a level interrupt on pins 2 or 3, or a Pin Change interrupt (see ATmega328 datasheet Table 10-1, including note 3, on pg. 38).

(Arduino Playground - HomePage)

and

AC can be a wake up source in idle mode by generating an interrupt.

(http://www.avrfreaks.net/forum/analog-comparator-wakeup-source)

So in the end the best solution might be to add a watdog interrupt to wake the arduino 10 times a second or so.. What do you think?

There's a lot of different levels of "sleep". Almost all of them will stop power to the analog subsystem, so analog-compare won't work. You just have to find a sleep command that won't turn off that subsystem.

Waking up enough to do an analogRead() several times per second is probably a useful alternative.

I've looked into the sleep modes and from what I understood the analog comparator would need to be in IDLE mode, which is the least efficient.

So I replaced the PCI by a Watchdog Timer Interrupt that wake the IC every 125ms. It is also useful to receive data from the nrf24l01 without using the IRQ pin.

I cannot test the current consumption for the moment, but the timer behave as expected. I used MCUSR &= ~(1<<WDRF); from an example of the datasheet, but I have seen it set to 0 in others.

#define WDPS_16MS   (0<<WDP3 )|(0<<WDP2 )|(0<<WDP1)|(0<<WDP0)
#define WDPS_32MS   (0<<WDP3 )|(0<<WDP2 )|(0<<WDP1)|(1<<WDP0)
#define WDPS_64MS   (0<<WDP3 )|(0<<WDP2 )|(1<<WDP1)|(0<<WDP0)
#define WDPS_125MS  (0<<WDP3 )|(0<<WDP2 )|(1<<WDP1)|(1<<WDP0)
#define WDPS_250MS  (0<<WDP3 )|(1<<WDP2 )|(0<<WDP1)|(0<<WDP0)
#define WDPS_500MS  (0<<WDP3 )|(1<<WDP2 )|(0<<WDP1)|(1<<WDP0)
#define WDPS_1S     (0<<WDP3 )|(1<<WDP2 )|(1<<WDP1)|(0<<WDP0)
#define WDPS_2S     (0<<WDP3 )|(1<<WDP2 )|(1<<WDP1)|(1<<WDP0)
#define WDPS_4S     (1<<WDP3 )|(0<<WDP2 )|(0<<WDP1)|(0<<WDP0)
#define WDPS_8S     (1<<WDP3 )|(0<<WDP2 )|(0<<WDP1)|(1<<WDP0)

#define CLIENT_ADDRESS 1
#define SERVER_ADDRESS 2

#include <avr/sleep.h>
#include <RHReliableDatagram.h>
#include <RH_NRF24.h>
#include <SPI.h>

RH_NRF24 driver;
RHReliableDatagram manager(driver, CLIENT_ADDRESS);

const byte XPIN = A1;
const byte YPIN = A2;
const byte ZPIN = A3;
const byte RFLED = 7;
const byte IRLED = 6;

ISR (WDT_vect) { sleep_disable(); }

void setup () 
{
    pinMode(XPIN, INPUT);
    pinMode(YPIN, INPUT);
    pinMode(ZPIN, INPUT);
    pinMode(RFLED, OUTPUT);
    digitalWrite(RFLED, LOW);
    manager.init(); //nrf24

    MCUSR &= ~(1<<WDRF); //Clear Watchdog System Reset Flag (WDRF) in MCU Status Register (MCUSR)
    WDTCSR = (1<<WDCE)|(1<<WDE); //Set WD Change Enable (WDCE) and WD Enable (WDE) in WD Timer Control Register (WDTCSR) (enter configuration mode)
    WDTCSR = (1<<WDIE)|WDPS_125MS; //Set WD Interrupt Enable (WDIE) and WD Pause
}

void loop ()
{
    set_sleep_mode (SLEEP_MODE_PWR_DOWN);
    sleep_mode ();
    rf_listen();
    read_input();
}

void read_input()
{
    if (digitalRead(ZPIN) == 0) { rf_sendString("Click"); }
    else if (analogRead(YPIN) > 700) { rf_sendString("Up"); }
    else if (analogRead(YPIN) < 400) { rf_sendString("Down"); }
    else if (analogRead(XPIN) > 700) { rf_sendString("Left"); }
    else if (analogRead(XPIN) < 400) {  rf_sendString("Right"); }
}

void rf_sendString(char str[RH_NRF24_MAX_MESSAGE_LEN])
{
    digitalWrite(RFLED, HIGH);
    uint8_t rf_data[RH_NRF24_MAX_MESSAGE_LEN];
    strcpy((char*)rf_data, str); 
    manager.sendtoWait(rf_data, sizeof(rf_data), SERVER_ADDRESS);
    digitalWrite(RFLED, LOW); 
}

void rf_listen()
{
    if (manager.available())
    {
        uint8_t rf_buf[RH_NRF24_MAX_MESSAGE_LEN];
        uint8_t len = sizeof(rf_buf);
        uint8_t from;
        if (manager.recvfromAck(rf_buf, &len, &from))
        {
            if (!strcmp((char*)rf_buf, "tv"))
            {
                //Blink IR LED
            }
        }
    }
}