When I started this component, I went through three different designs :
- Learning Curve : With the ultimate objective in mind (and no idea of how to use an Arduino at this point), this project became my Arduino learning curve. In this first "learning" state, it used pull-up or pull-down resistors on a separate circuit board, and the device was actively polling the switches for state changes.
- Sleep Mode : As I started to wonder about power consumption, I found that the Arduino utilized interrupts, and (as a result), could be put to sleep. That lead to a redesign using both pull up resistors AND diodes to trigger a separate interrupt pin. This option was later scrapped due to time constraints and the fact that power consumption dropped to 3mA rather than the original 15mA. That is NOT a lot of power, and the polling was easier.
- Simpler Design : As I progressed in completing the sleep mode, I found that the Arduino INCLUDES on-board pull-up resistors - making them pointless in my initial design. A subsequent test with using the on-board pull-up resistors gave me a fully functional design that required half of the parts I thought I'd end up with.
I toyed with the following schematic, only to find it buggy :
The PROBLEM :
The problem with the above design is that, if any door is closed, the interrupt pin
will be grounded out, meaning someone could open any single door,
leaving another one closed, and the Arduino would never wake up to find
out. Still, it can be done, I just don't have the time to implement it (using some NEG gates and an OR gate across all switches). Because it can be done, I'll discuss what was intended with the above.
The THEORY :
All switches (I'm using magnetic reed switches) have one end tied to the ground pin on the arduino. As a switch is closed, it pulls the corresponding digital pin that it is tied to DOWN. (It's high because when we start, we pretend it's an OUTPUT pin and set it to HIGH.) The diode is to prevent switch one from also pulling switch 2 low because we are tying the switches together into an extra pin that is specific for the INT (interrupt). That is required, since we start the Arduino up, initialize everything, then put it to sleep.
Here's how it was supposed to work. When a door is in it's closed state, the switch for that corresponding door is closed, causing the digital pin for that to be pulled low. As a door opens, the switch opens, and the pull-up resistor for that pulls that input to HIGH. It also causes the state to change on the INT. That INT signals the controller to launch a subroutine that scans all of the pins it's configured to looking for a state change, and then launches the appropriate subroutine (e.g. the "closed()" or "closing()" functions). The interrupt-based code is still available at :
http://svn.silverhawk.net/files/interrupt_alarm.pde
The REAL-LIFE answer : back to "polling"
The solution to the INT problem is to switch back to a "polling" mode. This means that the Arduino doesn't sleep, but the power savings are negligible. It also means that we call the handleInterrupt() function manually. The time I delay is 50ms - which is nearly instantaneous to the human mind due to reactions, etc. It should catch pretty much everything, plus, it allows us to throw in a server (to not just sent notices out, but to be passive in the states, too!), and gives us a little more elbow room.
Onward and Upward :
So, with no interrupts enabled, and with the above description of how it was "supposed" to work, we can surmise that everything pretty much stays the same, but without the INT functionality. Above, we mentioned "closed", "closing", "opened", and "opening". You might be asking what the difference is on closed/closing and opened/opening functions. That comes into play with the garage door code. Using a single magnet embedded into the garage door, and two "switches" connected to the endpoints of the door, when the door is closed, one switch will be closed and the other open, and when it opens, those reverse. It also gave the ability to see when both are open, and the last state was "closed", we must logically have started opening the door. The same is also true if both switches are open and the last state is "open" - we are now "closing" the door. That means that the configuration MUST be able to know the difference between a normal switch, and the stateful garage door "switches". For a garage door, the code would look like :
int previous_state;
int state1 = digitalRead(pin1);
int state2 = digitalRead(pin2);
if ((state1 == LOW) && (state2 == LOW)) {
previous_state = -1;
eventUnknown("Garage Door Sensors both low! Error");
} else if ((previous_state == 0) && (state1 == HIGH) && (state2 == HIGH)) {
eventDoorOpening();
} else if ((previous_state == 2) && (state1 == HIGH) && (tstate2 == HIGH)) {
eventDoorClosing();
} else if (state1 == LOW) {
eventDoorClosed();
} else if (state2 == LOW) {
eventDoorOpened();
}
To implement it, you'd need to define a structure that could include this, since the INT specifications mean that the contents are volatile.
For a regular door, it simple checks the previous state, and if the state has changed, call the corresponding "open()" or "close()" function. Also, by passing a structure to a configuration for what sensor we are looking at, we can suddenly reuse the function (memory is limited on an Arduino).
I wanted this device to be configurable, so I had to read from the SD card on an Ethernet shield (W5100). This was done in the setup function. Since I needed a configuration that was easily modifiable without re-uploading the code to the device, I re-invented the INI wheel. The setup function includes the code :
char config_code[10];
char currentChar;
char config_section[80];
char config_line[356];
char *config_key;
char *config_val;
void setup() {
pinMode(SD_OUTPUT_PIN, OUTPUT);
if (!SD.begin(SD_PIN)) {
return;
}
// open the configuration file. To write, add FILE_WRITE param
myFile = SD.open(config_file_name);
// if the file opened okay, read it. We use an INI-type format
if (myFile) {
Serial.print("Reading from ");
Serial.print(config_file_name);
Serial.print("...");
config_line[0] = '\0';
while (myFile.available()) {
String cl = myFile.read();
cl.toCharArray(config_code,10);
currentChar = atoi(config_code);
config_line[strlen(config_line)+1] = '\0';
if (currentChar == '\n') {
handle_config_line(config_line);
config_line[0] = '\0';
}
config_line[strlen(config_line)+1] = '\0';
if ((currentChar != '\n') && (currentChar != '\r')) {
config_line[strlen(config_line)] = currentChar;
}
}
// close the file:
myFile.close();
Serial.println("done.");
} else {
// if the file didn't open, print an error:
Serial.print("error opening config file ");
Serial.print(config_file_name);
Serial.println("...");
return;
}
}
Note that this requires some extra functions (otherwise, setup would have become extremely ugly).
void handle_setting(char *section,char *key,char *val) {
// put the code in there that would handle your config settings from the INI
};
void handle_config_line(char *line) {
int tmp_remove_comment;
config_val = 0;
while ((line[strlen(line)-1] == '\n') ||
(line[strlen(line)-1] == '\r') ||
(line[strlen(line)-1] == ' ') ||
(line[strlen(line)-1] == '\t')) {
line[strlen(line)-1] = '\0';
}
tmp_remove_comment = 0;
while (tmp_remove_comment < strlen(line)) {
if (line[tmp_remove_comment] == ';') line[tmp_remove_comment] = 0;
tmp_remove_comment++;
}
if (strlen(line) > 0) {
if (line[0] == '[') {
strcpy(config_section,line + 1);
while (config_section[strlen(config_section)-1] == ']') {
config_section[strlen(config_section)-1] = '\0';
}
} else {
config_key = line;
config_val = line;
while ((config_val[0] != '\0') && (config_val[0] != '=')) {
config_val++;
}
if (config_val[0] != '\0') {
config_val[0] = '\0';
config_val++;
handle_setting(config_section,config_key,config_val);
}
}
}
}
Polling doesn't require the following chunk of code! If we get a good interrupt design and need to implement, then, while we are in the setup function, we need to set up the interrupt pins. We do this by enabling the pull-up resistor on the interrupt pin, and then attaching the interrupt function (handleEvent) to that pin :
// set pull-up resistor
digitalWrite(interruptPin,HIGH);
// attach the interrupt
switch (interruptPin) {
case 2 :
attachInterrupt(0,handleEvent,CHANGE);
break;
case 3 :
attachInterrupt(1,handleEvent,CHANGE);
break;
case 18 :
attachInterrupt(5,handleEvent,CHANGE);
break;
case 19 :
attachInterrupt(4,handleEvent,CHANGE);
break;
case 20 :
attachInterrupt(3,handleEvent,CHANGE);
break;
case 21 :
attachInterrupt(2,handleEvent,CHANGE);
break;
};
For each switch, we also need to enable the pull-up resistors :
digitalWrite(pin1,HIGH);
pinMode(pin1,INPUT);
If we were doing the
interrupt driven, we'd have to put the thing to sleep. This makes the main loop() function VERY small and short. In fact, the loop() function is ONLY powering the device down :
void loop() {
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
sleep_mode();
sleep_disable();
}
Since we are
not using interrupts (we're polling, remember?), our loop function will call our "handleEvent()" function manually by doing the following :
void loop() {
delay(50ms);
handleEvent();
}
The interrupt function (
used in both interrupt and polling code sets) is fairly simple :
void handleEvent() {
};
At that point, it's merely a matter of making the handleEvent function check all of the pins it needs to. To see the full code, check out
http://svn.silverhawk.net/files/polling_alarm.ino. Next, we put it all together to test in
Arduino - Assembly and Testing.