Learning About Interrupts to Make Etch-a-sketch With Arduino

by 3d-reid in Circuits > Arduino

1055 Views, 5 Favorites, 0 Comments

Learning About Interrupts to Make Etch-a-sketch With Arduino

20200517_163318.jpg
etch-a-sketch-nano-diagram.png
image001.jpg
image003.png
image002.jpg
image005.jpg

Mini-Etch-a-sketcher

By: David A.Reid (3D-Reid)

Introduction:

I wanted to explore the lower level interrupt codes on an Arduino board. An Arduino Nano in my case (but this will also work on an Arduino UNO v.3).

I am using the Arduino IDE (www.arduino.cc) as my editor/compiler. In the Editor you are normally limited to utilizing only 2 external interrupts (on pin 2 and pin 3 of the Nano or UNO boards). But, there is another way to gain access to most of the other pins. This feature is not so well documented and what documentation there is, is quite difficult to understand.

So, through the medium of this project I want to explain in much simpler terms how to work with interrupts at a lower level. Lower level means directly addressing the Nano CPU ports, not by high level IDE IRQ commands. The project is building an etch-a sketch type device with 8 colors and a single mini joystick controller. My Background

Just so you know who you are listening to… I have been designing electronics since I was 15, At that time there were not many computers available that didn’t cost the price of a small house. (and certainly none that people would let a 15 year old kid hack on… But, we did have access to an Apple II computer in our school in Houston Texas, USA ( only for the teachers…. but I managed to talk my way into their confidence and was allowed to ‘play’ on it). In 1981 I got my amateur radio license and started building antennas and my own radio transceivers.

For more than 12 years, I ran my own company designing high-speed digital interfacing for companies all over Europe. Then I joined a semiconductor company in Eindhoven and after 10 years I moved to work for a High Tech company in Hengelo in the Netherlands where I have been working for the past 11 years.

OK, enough about me, let’s get down to the nuts and bolts of this project.

Supplies

Parts list:

  1. 1x Arduino Nano v3.0

    The Arduino Nano v3.0 is a small prototyping board. It is similar to the UNO v3, except has a bit less I/O but still uses the ATMEL 328P processor.

  2. 1x 128 X 160 SPI TFT display 7735IC based

    This is the version without the SD card slot, but they a both identical and use the 7735 chip. So displays from Adafruit, AZ Delivery or most other suppliers should work fine.

    Note the board is labelled as though it was an I2C device, but in reality it is an SPI device, they do not make an I2C version.

  3. 1x Blackberry TrackBall module

    I slightly modified the trackball board by pushing the VCC pin up through the board as this allowed me to directly connect the module to the Nano. (otherwise the VCC pin would have been connected to the Reset pin on the Nano and that is not a good idea.

    So, I heated the pin and pushed it though the PCB from the bottom so that the pin sits up from the main board and I could still attach the female-male jumper wire to the module. (A neat solution, I think).

  4. 1x Breadboard

    Just whatever you have lying around.

  5. 9x male-male jumper cables
  6. 1x female-male jumper cable.

Video of the End Result

etch a sketcher video

A Bit of an Explanation

What is an interrupt?

Ok, let’s start at the beginning… you have a program running in the ‘loop()’ on your Nano, it just does what the program instructions tell it to do…but you want to do quite a few things like read a sensor input while updating the display… you CAN do this in your loop():

loop(){
read sensor
display value
}

But, while you are displaying the value (which is usually a slow process involving a lot of CPU cycles) the reading on your sensor changes and you will miss it. Though this is not too critical if you are just measuring the temperature outside… it can be crucial if you are receiving a peak current reading from a temperature sensor on your 3D printer or trying to decode a serial stream coming from your latest sensor. So, you can solve this by using an interrupt, of course. But, you ask, what is an interrupt and how does it work? Well, I am going the explain in mostly non-technical terms. So, your program is running in a loop()…doing predictable what it was programmed to do and from the outside world along comes an event that says “Hey I’m important enough to stop you from doing what you are doing and do what I want first - then you can go back to your routine loop”. You can equate this to a planned trip to the supermarket, your loop() is:

Loop(){
Get list of what is needed
Take enough shopping bags
Drive car to the shop
Buy the stuff on the list
Pack in the bags
Put bags in the car
Drive home
Unpack bags
And, maybe, delete the list
}

But, while you are in the ‘Drive the car to the shop’ step, the telephone rings and your partner ask you to add an item to the list. (this is the interrupt). You hopefully already stopped the car to answer the phone, then you write onto the list the additional item, lest you forget during the rest of the journey. You ‘Buy the stuff on the list’ (which will include the extra item from the phone call) and continue with the loop().

In a microcontroller like the Nano or UNO, it works pretty much the same way…only we have to prepare things first and then wait for some event to happen. This would be the equivalent in the story above of ensuring you had your phone with you and that it was charged up. Not part of the main loop(), but essential if you are open to receiving calls.

The 3 Stages Involved in Setting Up an Interrupt

With most microprocessors, there are basically 3 stages to getting interrupts working ( from any of the 20 usable input pins of the Arduino Nano). If you are not familiar with working on embedded processors, this code will seem strange and will take a bit of time to understand, but once you have mastered it you can unleash the real power in your Arduino projects.

The Arduino also uses 3 stages, but it gets a bit more complicated, because the internal registers are not all directly mapped to the pins we all know and love on the edge of the board.

Stage 1:Selecting PORTD Using the PCIR (PinChangeInterrupt Register)

PCIR_register.png

When you decide to change the contents of registers that are connected with interrupts you should always disable all interrupts, change the registers, then re-enable the interrupts. You disable the interrupts with the following command (CLearInterrupts):

 cli(); 

and when you are finished changing the registers you must use the command (SEtInterrupts)

sei();

to re-enable the interrupts.

Now that is sorted, We are going to be using the PinChangeInterrupts (PCI). This command writes directly to the ATmega328P chip which has 3 8 bit PORTs. Using the PinChangeInterruptRegister (PCIR) we can select which PORTs on the chip can ‘trigger’ the interrupt. This where it gets a bit messy… but I made this table (above) to hopefully clarify it.

The bottom 3 bits of the PCIR determine which PORTs are enabled. If there is a 1 in the bit position, then that PORT is enabled for PinChangeInterrupts.

We want to enable PORTD and this is easiest to do with a binary number. So that we don’t disturb any of the other setting we can use a compound bitwise OR operator (|=) and this will only set the single bit in the register (bit 2):

 PCIR |= 0b00000100; // Enables PORTD Pin Change Interrupts 

Note:

With this type of interrupt we cannot directly detect if the pin changed from LOW to HIGH or HIGH to LOW. Everytime it changes state, we get an interrupt.

For the roller ball application this is actually an improvement, the rollerball when rolled 360 degrees generates 8 pulses, but, because we ‘see’ both the HIGH-LOW transition and the LOW-HIGH transition we get interrupted 16 times in 1 x 360 degrees rotation – so our resolution is twice as good. If we just detected the HIGH states (or the LOW states) we’d only ‘see’ 8 interrupts. However, this also causes a bit of a problem with the ‘BUTTON’, it will give us 2 interrupts, one when we press it and it changes from HIGH to LOW and one when we release it and it changes from LOW to HIGH…So we have to filter the unnecessary one out in our code. See the description of the Button code later in this article.

Stage 2: Setting the Masking for PORTD

PORT_mapping_register.png
PCMSK2_register.png
image005.jpg

Now that the PCIR has been set, the PORTD is enabled and we need to set the bits that relate to the pins on the board. This is done using 3 special registers in the 328P chip. Note that in many cases this is different than the pin on the edge of the board. So we need a table that makes it simple to select the correct MASK bit, to the microcontroller pin to the Nano board pin. For this project, I was lucky that the rollerball board pinout is 1:1 connected to the Nano board via pin 2 to pin 6 and these also are in sequence on the microcontroller…This makes things a bit easier.

There are 3 MASK registers confusingly called PCMSK0, PCMSK1 and PCMSK2 registers – they directly relate to PORTB,PORTC and PORTD. Because we are using PORTD for this project, we need to program PCMSK2. Because this register aligns with the actual pins on the Nano board, it is quite straightforward to program them. But for the other ports (or different boards), you need to check carefully. I always make this table in my source code for the project I am doing, it is good practice to document in a readable manner exactly what you did – as these sorts of commands are a bit cryptic at the best of times.

// Table of interrupt registers// ----------------------------- 
// PORT D (PCINT2/PCMSK2) 
// Port bit Pin PCINT# Function
// PD2 2 PCINT18 BTN 
// PD3 3 PCINT19 RHT 
// PD4 4 PCINT20 LFT 
// PD5 5 PCINT21 DWN 
// PD6 6    PCINT22 UP 
// ----------------------------- 

We can see in the circuit diagram that the rollerball board connections are wired to pins D2 to D6 of the Arduino Nano and these map directly to the movement of the ball and the button state. The direction that the rollerball is moved, creates an asynchronous pulse train on the associated direction pin (right, left, down, up). The pulse train pulses HIGH and LOW at a frequency which depends on how fast the ball is moving.

One cool thing with this trackball board is that it uses Hall-effect for the sensor, this means the output from the board is a pretty clean signal as the Hall effect output is first amplified and then passed through a Schmitt Trigger.

(See the digital scope screen)

This produces an almost perfect switching signal, which triggers our interrupt with no debounce problems.

The following 4 pins (7-10) on the trackball module go to the LED lights under the rollerball (WHT, GRN, RED, BLU). But on my board the RED pin (pin 9) physically is wired to the blue LED and pin 10 ( labeled BLU) actually goes to the RED LED…. Strange, but true.

Setting the Possible Interrupt Sources in the PCMSK2 Register

PCMSK2_bits_pins_register.png

Now, we need to set the bits in the PinChangeMaSK2 (PCMSK2) for PORTD to allow these pins to call the interrupt routine. Every pin that has a 1 in this register can generate a PinCHangeInterrupt (PCI).

So, with the following instruction we allow interrupts to happen only from the pins that are set to 1 in the PCMSK2 register.

PCMSK2 |= 0b01111100; // Used to mask PORT D for the button and 4 directions of the
rollerball (pins 2-6 on the Nano)

Again, use the |= operator to set only the bits we need for our project. Note that bit 0 and bit 1 in PORTD should never be re-allocated. They are pre-configured as TX and RX and are used for communication with your computer. (You have been warned!).

Stage 3: Write the Interrupt Handling Code

So, now the environment is setup and ready to generate interrupts on the pins from the roller ball, but how do we know when one occurred and how do we tell the microprocessor which instructions to execute when an interrupt occurs. Remember that, unlike an external interrupt pin, we don’t know yet which pin caused the interrupt. But that will all come clear in a few paragraphs.

First, how do we code the interrupt routine?

Outside our main loop(), we create a special Interrupt Service Register (ISR) subroutine. The 328P chip has 3 ISRs (one for each of the PORTs), as we are interested in PORTD, we need to write a bit of code for ISR(PCINT2_vect).

The ISR part tells the compiler than this is an Interrupt routine and the PCINT2_vect tells the computer that we want to handle interrupts from PORTD in the code which we write here.

What happens inside the microcontroller chip is: if there is a change of state on any of the configured pins on the chip, it jumps to a special memory location which points to our code (called a vector) and expects to find code to handle the incoming interrupt.

We have to write that code ourselves, but the vector is defined by the chip hardware. Normally, in an ISR you don’t want to spend too many clock cycles doing lots of heavy processing, you want to get in and out fast – so we don’t write a load of display code, or use delay(), or other heavy processing commands like Serial.println inside the interrupt routine. We need to first define some volatile variables at the top of the program. They are called volatile because we need to change the content of them outside the main loop(), but also use them in the main loop().

We define them at the top of the program like this:

// interrupt variables <br>volatile unsigned int BTN_I; 
volatile unsigned int RHT_I; 
volatile unsigned int LFT_I; 
volatile unsigned int DWN_I; 
volatile unsigned int  UP_I; 
volatile unsigned int rollerD; // a variable for the PIND read

I use a naming convention of my own whereby all variables used in interrupt routines are suffixed with an _I and the name of the pin I take off the PCB, if possible. This makes things clearer for me when I come back to look at the code in 3 months time.

OK, now we have some variables that we can use in the interrupt routine, it is time to read the port and determine the state of the individual pins. We do this by using a special read instruction called PIND, there are similar instructions for PORTB and PORTC ( PINB and PINC respectively). PIND works a lot like digitalRead(); but accesses a register inside the 328P chip directly.

Our interrupt code looks like this:

ISR(PCINT2_vect) {

  rollerD = PIND; // immediately read the 8 bits of the port
   BTN_I = (rollerD & B00000100) >> 2;
   RHT_I = (rollerD & B00001000) >> 3;
   LFT_I = (rollerD & B00010000) >> 4;
   DWN_I = (rollerD & B00100000) >> 5;
   UP_I = (rollerD & B01000000) >> 6;
   } // end of ISR subroutine

The idea here is that we read the port immediately after the interrupt then mask off the different bits and shift them to bit_0 then exit the interrupt routine.

Example of the Input Processing

Example_input_processing.png

Let’s look at one entry:

RHT_I = (rollerD & B00001000) >> 3; 

We put into the volatile variable RHT_I the contents of the PIND (via variable rollerD) which is ANDed with the binary 00001000, that should leave us with only bit 3 of interest and then, to make things easier in the main loop(), we shift the whole register 3 places to the right – this puts the value of bit 3 now at bit 0.

Example:

Let’s assume when we read PIND, the pins on port D are 11001010:

(See Example diagram above).

To only get the RIGHT direction interrupt, we mask off bit 3 using and AND operator (&).

Then shift the result 3 place to the right. (Because the 328 processor is an advanced RISC (Reduced Instruction Set Computer) based architecture, this shift does not cost us any further clock cycles,

This instruction is repeatedf or all the bits and assigned to the global variables we defined. That’s all we are going to do in the interrupt.

The _I version of the variables now hold the latest status of each of the direction pins from the roller ball and the state of the button.

​Now, Back to the Main Program Loop

Because we don’t want any extra interrupts while we are still trying to process the previous events, We are going to disable the PCMSK2 mask, then after we are finished we will re-enable it and follow this with a simple delay statement – that will provide a timeslot to catch the following interrupt. The code to disable the interrupt mask is:

cli(); // Clear the interrupt flag register
   PCMSK2 &= 0b10000011; // clear the bits so we don't get disturbed
sei(); // re-enable the interrupts

and, after we have done everything we need to handle we can set the PCMASK to allow interrupts again:

cli();
PCMSK2 |= 0b01111100;
sei();

and then follow that with a short delay:

delay(1);

Let's See What's Happening in the Main Loop()

XOR-truth-table.png

In the main loop, we need to test if each of the particular bits changed, we can do this in many ways, but this is possibly the easiest to understand.

The code for each direction is similar except for the addition or subtraction of the stepsize to the values. I set up 4 variables, x0 and y0 hold the current location of the dot on the screen. x1 and y1 hold the new position data. Because it was physically easier to mount my display upside down, the origin is probably in a dfferent place than you expect. The top right corner of the display is the origin (0,0). Let’s look at the RHT routine. It resides inside the main loop()

if (RHT ^ RHT_I) {<br>	y1 = y0 - stepsize;
	if (y1 < 0)
	{
        y1 = 0;
        y0 = 0;
	}
    x1 = x0;
    TFTscreen.drawLine(x0, y0, x1, y1, myColor[selector]);
    x0 = x1;
    y0 = y1;

The if-statement checks if the old value XOR (^) the new value is equal to 1.

if (RHT ^ RHT_I) {

(see the truth table for an XOR)

If it is equal to 1 then it means that this pin changed state (i.e. the rollerball was moved to the right)…so we have to adjust the position of the line we will draw on the screen later. We do this in the following manner:

We set the variable y1 equal to (y0 minus the stepsize) (minus moves the location to the right by stepsize dots (We set stepsize in the definition variables section at the top of the program.)

#define stepsize1

Then we check that y1 is still valid ( it has to be 0 or greater). If it is less than 0, then set it to 0. 0 is the rightmost side of the screen.

if (y1 < 0)
{ y1 = 0; y0 = 0; }

And we set x1 to equal x0, so we only move in the y direction.

x1 = x0;

Next, we draw a line on the TFT display in the current color.

TFTscreen.drawLine(x0, y0, x1, y1, myColor[selector]);

And, finally we update the cursor position on the screen:

x0 = x1;
y0 = y1;

and close the ‘if RHT’ routine with a close bracket

}

To move left we have a subroutine that ADDs stepsize to the y0 position, note the check is now against 128 which is the leftmost position on the screen. It is also possible to ask the screen how wide it is and use this value as the largest width. This is better programming practice but adds more complication and deviates us from our main goal of this article - mainly learning about interrupts.

The up and down subroutines add or subtract from the x0 variable, and the max check is for 160 instead of 128. (The screen is 160 pixels high).

The button code:

if (BTN ^ BTN_I) {
if (BTN_I == 1){ if (selector <=6){ selector++; } else { selector = 0; } } }

The button code has to be a bit different because we use the button to change the color of the line it is drawing (by changing the variable selector) but if we just relied on a change on the button pin we would always see 2 pulses, one when the button was pushed down, and another when it was released…so we have to filter out one of the two values.

We do this in a simple if-statement.

The first XOR check is the same as the directions – this means we saw a change on the pin (from 1 to 0 as it was pressed, or from 0 to 1 as it was released).

Next we check for only the interrupt flagging a 1 (we released the button) with:

if (BTN_I == 1){

then
we check the selector variable is not greater than our array size (8), if not we increment the variable.

if (selector <=6){
selector++; }

And if it is not in range, set it back to 0.

else {
selector = 0; }

Then we close the if-statement and the subroutine

}
}

Finally,
after we have checked the status and performed the necessary actions for the main loop, we need to update the old values of the pins to the new values… We do this simply, like this:

// update the current state from the last interrupt state, before re-enabling the interrupts.
BTN = BTN_I; RHT = RHT_I; LFT = LFT_I; DWN = DWN_I; UP = UP_I;

To make the LED in the ball change to (almost) the same color as the line, we can use a case statement:

switch (selector) {
case 0: // black - all off v- used to erase bits of the picture. digitalWrite(7,LOW); digitalWrite(8,LOW); digitalWrite(9,LOW); digitalWrite(10,LOW); break; case 1: // white digitalWrite(7,HIGH); digitalWrite(8,LOW); digitalWrite(9,LOW); digitalWrite(10,LOW); break; case 2: // red digitalWrite(7,LOW); digitalWrite(8,LOW); digitalWrite(9,LOW); digitalWrite(10,HIGH); break; case 3: // blue digitalWrite(7,LOW); digitalWrite(8,LOW); digitalWrite(9,HIGH); digitalWrite(10,LOW); break; case 4: // green digitalWrite(7,LOW); digitalWrite(10,LOW); digitalWrite(8,HIGH); digitalWrite(9,LOW); digitalWrite(10,LOW); break; case 5: // yellow digitalWrite(7,HIGH); digitalWrite(8,HIGH); digitalWrite(9,LOW); digitalWrite(10,LOW); break; case 6: // magenta digitalWrite(7,LOW); digitalWrite(8,LOW); digitalWrite(9,HIGH); digitalWrite(10,HIGH); break; case 7: // cyan digitalWrite(7,HIGH); digitalWrite(8,LOW); digitalWrite(9,HIGH); digitalWrite(10,LOW); break; }

And close the loop() with a curly bracket.

}

The Top of the Program- Defining Things

The rest of the code is for setting up the TFT display.

At the top of the program we include the required libraries:

#include <TFT.h>
#include <SPI.h>

Then we define the pins on the Nano that we want to use for the control pins on the TFT display

#define cs A0
#define dc A1 #define rst A2 // Note the backlight for the TFT display can be connected to the 3V3 // output pin on the Nano. This gives a bit of light to the background, but not too much.

We setup the variables for the start position of thecursor and the new position variables.

int x0 = 80;
int y0 = 64; int x1; int y1;

and then the TFT itself

TFT TFTscreen = TFT(cs, dc, rst);

We also need to setup an array for the colors, I chose 8 possible colors, but you can extend this list as much as you like – just don’t forget to change the BTN subroutine to reflect the size of the array. Also, the array starts at 0 so the last value is one less than the actual number.

volatile double myColor[] {0x0000,0xFFFF,0x001F,0xF800,0x07E0,0x07FF,0xF81F, 0xFFE0};
// black, white, blue, red, green, cyan, magenta,yellow

And create the selector variable at the top of the program

volatile int selector=0;

Diving Into Setup()

In the setup() subroutine we have to define a few things about our TFT display:

TFTscreen.begin();
delay(50); // the TFT display takes a little while to initialize TFTscreen.background(0, 0, 0); // set background color to black TFTscreen.stroke(0, 255, 0); // set the text color to green TFTscreen.setTextSize(2); // chooses a large font size // Write the text to the screen TFTscreen.text("Etch", 55, 20); TFTscreen.text("-a-", 60,40); TFTscreen.text("Sketcher", 35,60); TFTscreen.setTextSize(1); TFTscreen.text("D.A.Reid", 0,80); TFTscreen.text("copyright 2020", 75,80);

And that is the complete code description. Because I described it functionally, and not as a linear code chunk, you can copy or download the entire code from the link.

Hope you enjoyed learning about Interrupts with me.

The Source Code for the Etch-a-sketcher

20200517_163229.jpg
20200517_155721.jpg

Hi,

I guess if you don't really care how it works, but just found the end product really cool, that you have jumped directly to this step. Hey! that's OK with me... but... aren't you just a little curious how it works...You never know, you may learn something.

Enjoy.

// Etch-a-sketcher - expriments in PCI interrupts on ATMEL 328 processors.
// David Reid 05-05-2020. // Published as an instructable on www.instructables.com under my user name 3D-reid // // and now the fun begins.... #include // This allows us to access the internal registers of the ATMEL chip // Drivers for the TFT display which is 128 X 160 SPI (even though the board is labelled like an I2C. #include \ #include \ // Color definitions for the TFT (copied from internet) //#define WHITE 0xFFFF //#define BLACK 0x0000 //#define BLUE 0x001F //#define RED 0xF800 //#define GREEN 0x07E0 //#define CYAN 0x07FF //#define MAGENTA 0xF81F //#define YELLOW 0xFFE0 // #define stepsize 1 #define cs A0 #define dc A1 #define rst A2 // SETUP TFT TFT TFTscreen = TFT(cs, dc, rst); ////////////////////////////////////////////////// // Table of interrupt registers // ----------------------------- // PORT D (PCINT2) // Port bit Pin PCINT# Function // PD2 2 PCINT18 BTN // PD3 3 PCINT19 RHT // PD4 4 PCINT20 LFT // PD5 5 PCINT21 DWN // PD6 6 PCINT22 UP // ----------------------------- // PORT B (PCINT0) // Port bit Pin PCINT# Function // PB4 12 PCINT4 EXTBTN1 // ----------------------------- // unsigned integers - cos we might need all 8 bits unsigned int BTN; unsigned int RHT; unsigned int LFT; unsigned int DWN; unsigned int UP; // loads of volitile variables - cos the interrupt routine needs to also // write to these and otherwise the compiler will make them static. volatile unsigned int rollerD; // volatile unsigned int BTN_I; volatile unsigned int RHT_I; volatile unsigned int LFT_I; volatile unsigned int DWN_I; volatile unsigned int UP_I; // myColor[] probably doesn't need to be volatile- but we have a lot of space so why not. volatile double myColor[] {0x0000,0xFFFF,0x001F,0xF800,0x07E0,0x07FF,0xF81F,0xFFE0}; // black,white,blue,red,green,cyan,magenta,yellow // define integers volatile int selector=0; // The first time it is powered up, the selector will increment to 1, (white), // Note, my screen is mounted upside-down so the origin of the display is in the top,roght in portrait mode int x0 = 80; int y0 = 64; int x1; int y1; // for the sake of my sanity, I reference X and Y to the physical screen and not to the logical screen. void setup() { // set the pins going to the LEDs on the trackball to outputd pinMode(7,OUTPUT); pinMode(8,OUTPUT); pinMode(9,OUTPUT); pinMode(10,OUTPUT); // clear all the LEDs digitalWrite(7,LOW); digitalWrite(8,LOW); digitalWrite(9,LOW); digitalWrite(10,LOW); // setup the TFT and display message TFTscreen.begin(); delay(50); // the TFT display takes a little while to initialize TFTscreen.background(0, 0, 0); // set background color to black TFTscreen.stroke(0, 255, 0); // set the text color to green TFTscreen.setTextSize(2); // chooses a large font size // Write the text to the screen TFTscreen.text("Etch", 55, 20); TFTscreen.text("-a-", 60,40); TFTscreen.text("Sketcher", 35,60); TFTscreen.setTextSize(1); TFTscreen.text("D.A.Reid", 0,80); TFTscreen.text("copyright 2020", 75,80); // Disable interrupts while we play with the registers cli(); PCICR |= 0b00000100; // Enable on PORTD Pin Change Interrupts PCMSK2 |= 0b01111100; // PCINT2 Used to mask PORT D for the button and 4 directions of the rollerball (pins 2-6 on the Nano) // ok done, re-enable the interrupts sei(); delay(2000); // this waits 2 seconds while the title screen is showing. TFTscreen.background(0, 0, 0); // then set the background to black TFTscreen.drawPixel(x0, y0, 0xFFFF); // a white dot at the centr of the screen } void loop() { cli(); // Clear the interrupt flag register so that we don't get wrong data in the _I versions of the variables PCMSK2 &= 0b10000011; // clear the bits so we don't get disturbed by extra inputs sei(); // // My thinking behind this way of doing it... // // Each of the inputs below had either a 0 or a 1 in bit0, (Hex 0x00 or 0x01). So I exclusive OR (^) the old value with the updated value (_I version) and print results if it is still 1 // For example, if RHT contains 0 and an interrupt happens (because of this input pin), it does an XOR with the new value which must be a 1 and the result is 1 - (the if-statement is true) // so it knows there was a change on this pin... on the second round the input pin register is 1 and because the interrupt only activates on a change in the state of the pin, the _I version has to be 0 // otherwise, the interrupt would not be triggered. When it is triggered, the value of the _1 is set to 0; So, in the main program loop,I XOR the old pin state with the new updated interrupt (_I) version. // and the answer is 1 XOR 0 = 1 (making the if-statement true). // // Truth table for the circuit and code // ==================================== // old_state state_I result (1 = true 0=false) // 0 1 1 true (this pin changed) // 1 0 1 true (this pin changed) // 0 0 0 false (nothing changed) // 1 1 0 false (nothing changed) // // So, why would you get a false? Simply because something else that is interrupt enabled on the PORT triggered the interrupt - but not our pin. // The beauty of this mechanism is it allows you to see state changes in multiple pins at the sanme time, like rolling diagonal, or rollong while holding the button down. // if (BTN ^ BTN_I) { if (BTN_I == 1){ if (selector <=6){ selector++; } else { selector = 0; } } } if (RHT ^ RHT_I) { y1 = y0 - stepsize; if (y1 < 0) { y1 = 0; y0 = 0; } x1 = x0; TFTscreen.drawLine(x0, y0, x1, y1, myColor[selector]); x0 = x1; y0 = y1; } if (LFT ^ LFT_I) { y1 = y0 + stepsize; if (y1 > 128) { y1 = 128; y0 = 128; } x1 = x0; TFTscreen.drawLine(x0, y0, x1, y1, myColor[selector]); x0 = x1; y0 = y1; } if (DWN ^ DWN_I) { x1 = x0 + stepsize; if (x1 > 160) { x1 = 160; x0 = 160; } y1 = y0; TFTscreen.drawLine(x0, y0, x1, y1, myColor[selector]); x0 = x1; y0 = y1; } if (UP ^ UP_I) { x1 = x0 - stepsize; if (x1 < 0) { x1 = 0; x0 = 0; } y1 = y0; TFTscreen.drawLine(x0, y0, x1, y1, myColor[selector]); x0 = x1; y0 = y1; } // update the current state from the last interrupt state, before re-enabling the interrupts. BTN = BTN_I; RHT = RHT_I; LFT = LFT_I; DWN = DWN_I; UP = UP_I; // update the LED under the button to match the selected color switch (selector) { case 0: // black - all off == used to erase bits of the picture. digitalWrite(7,LOW); digitalWrite(8,LOW); digitalWrite(9,LOW); digitalWrite(10,LOW); break; case 1: // white digitalWrite(7,HIGH); digitalWrite(8,LOW); digitalWrite(9,LOW); digitalWrite(10,LOW); break; case 2: // red digitalWrite(7,LOW); digitalWrite(8,LOW); digitalWrite(9,LOW); digitalWrite(10,HIGH); break; case 3: // blue digitalWrite(7,LOW); digitalWrite(8,LOW); digitalWrite(9,HIGH); digitalWrite(10,LOW); break; case 4: // green digitalWrite(7,LOW); digitalWrite(10,LOW); digitalWrite(8,HIGH); digitalWrite(9,LOW); digitalWrite(10,LOW); break; case 5: // yellow digitalWrite(7,HIGH); digitalWrite(8,HIGH); digitalWrite(9,LOW); digitalWrite(10,LOW); break; case 6: // magenta digitalWrite(7,LOW); digitalWrite(8,LOW); digitalWrite(9,HIGH); digitalWrite(10,HIGH); break; case 7: // cyan digitalWrite(7,HIGH); digitalWrite(8,LOW); digitalWrite(9,HIGH); digitalWrite(10,LOW); break; } // re-enable the interrupts so we can get a new set of values from the port(s) cli(); PCMSK2 |= 0b01111100; sei(); // The delay(1); is the window of opportunity for new interrupts to occur - might not seem // like a lot of time, but actually it is quite long for a 16MHz RISC processor. // (16 MHz = 0.0000625 ms) // so within a delay of 1mS means we can read the port ~1600 times (if it only took 1 clock tick to read...) but it takes 2, so we can read it ~800 times in 1mS. delay(1); } ISR(PCINT2_vect) { // The idea here is that we read the port immedaitely after the interrupt // then mask off the different bits and shift them to bit_0 // then exit the interrupt routine and in the main program, do an XOR, to see if a bit changed, then report this // later this will send a draw/move command to the screen rollerD = PIND; BTN_I = (rollerD & B00000100) >> 2; RHT_I = (rollerD & B00001000) >> 3; LFT_I = (rollerD & B00010000) >> 4; DWN_I = (rollerD & B00100000) >> 5; UP_I = (rollerD & B01000000) >> 6; }