Low power interactions with the MSP430 Launchpad push button
So lately i've been doing a lot of programming on the Texas Instrument (TI) MSP430 and launchpad development board[1]. It's been quite interesting programming so close to the hardware. There is a lot of power in being able directly manipulate the behavior of the CPU, memory allocation, etc. It's extremely empowering and gives a little bit of insight into just how much work goes into producing the technology that we take for granted every day.
In the process of learning to program the MSP430, one of the more interesting features is the ability to trigger and respond to interrupts. This meant that constantly polling for changes is no longer necessary- instead, listen for an interrupt and handle it when it's fired. So after a little bit of research, i was able to put this together.
/**
* Toggles an LED using push button
* and interrupts
*/
#include <msp430.h>
#define LED BIT6
#define BUTTON BIT3
/*
* Stops the watchdog timer and
* Calibrates the CPU to 1MHZ
* DO NOT MODIFY
*/
void _init() {
WDTCTL = WDTPW | WDTHOLD; // Stop watchdog timer
//Check calibration constants
if (CALBC1_1MHZ == 0xFF || CALDCO_1MHZ == 0xFF) {
//Trap the CPU in an infinite loop
while (1)
;
}
DCOCTL = 0;
BCSCTL1 = CALBC1_1MHZ;
DCOCTL = CALDCO_1MHZ;
}
int main(void) {
_init();
//Set the LED pin as output
P1DIR |= LED;
//Send data to the LED pin
//In this case to turn it off
P1OUT &= ~LED;
//Enable the pushbutton to
//send interrupts
P1IE |= BUTTON;
//Add pull-up resistor
P1REN |= BUTTON;
//Clear interrupt flag
//This makes sure that an
//interrupt isn't triggered when
//interrupts are enabled
P1IFG &= ~BUTTON;
//Enable interrupts
__enable_interrupt();
//Set the board to low power mode 0
//with global interrupts enabled
__bis_SR_register(LPM0 | GIE);
return 0;
}
//Set function as port 1
//interrupt service routine
#pragma vector=PORT1_VECTOR
__interrupt void Port_1(void){
//Toggle the LED
P1OUT ^= LED;
//Clear the interrupt flag
P1IFG &= ~BUTTON;
//Toggle the interrupt edge
//sensitivity so an interrupt
//is fired when you let go
P1IES ^= BUTTON;
}
While the code with the comments are kind of self explanatory(and it works pretty well), I wanted to go a bit deeper into what the code was actually doing and what registers I was interacting with.
Disabling the watchdog
The _init()
method stops the watchdog timer and calibrates the CPU. The watchdog timer works a bit like a deadman's switch. By default, it reset's the board unless the firmware informs it that everything is working correctly within a specified period.[1:1] This way, if your program enters an infinite loop or get's stuck in some weird way, the watchdog will reset the board.
However in this demo program we don't need to worry about the board getting stuck so we disable the watchdog by changing the value of the watchdog timer control register[1:2] WDTCTL
using the watchdog timer password WDTPW
and the watchdog timer hold WDTHOLD
bits.
WDTCTL = WDTPW | WDTHOLD;
Calibrating the CPU
The CPU can be calibrated to run at a specific frequency by managing the on-board Digitally Controlled Oscillator Clock DCOCLK
[1:3]. A value can be set to the Digitally Controlled Oscillator Control Register DCOCTL
as well as the *Basic Clock System Control Registers BCSCTL1
.
There are calibration constants which are stored on the chip to make calibrating the CPU easier, i'm using CALBC1_1MHZ
and CALDCO_1MHZ
to calibrate my chip to 1MHz. It's important to note that these are simply constants stored in memory and can be erased.
As a precaution it's important to check to make sure that these values have not be en erased (When erased the values are set to
0xFF
) before attempting to execute any code.
If your constants are not found, you could provide values which could be used to manually calibrate or use an external clock source to mitigate the problem. For example[1:4]
if(CALBC1_16MHZ!=0xFF){
//Clear DCL for BCL12
DCOCTL = 0x00;
//Info is intact, use it.
BCSCTL1 = CALBC1_16MHZ;
DCOCTL = CALDCO_16MHZ;
}else{
//Info is missing, guess at a good value.
BCSCTL1 = 0x8f; //CALBC1_16MHZ at 0x10f9
DCOCTL = 0x7f; //CALDCO_16MHZ at 0x10f8
}
In my example however, I just prevent the program from doing anything using an infinite loop.
Configuring the LED and push button
The LED pin can be configured for output by setting the pin direction using the P1DIR
registry with 0 or Low for input and 1 or High for output[1:5]. You can set the state of the pin by writing to the P1OUT
register. The push button is then configured to trigger interrupts by setting the appropriate bit in pin interrupts enabled register P1IE
. This will allow it to trigger an interrupt when it's pressed. A pull up or pull down resistor can be applied using the Pull-up/Pull-down Resistor Enabled register P1REN
. The Interrupt Flag register P1IFG
is also cleared to prevent an interrupt from being triggered when interrupts are enabled.
Low power mode and Interrupts
Enabling interrupts is done using the __enable_interrupt()
function. However this intrinsic function, meaning that it's compiler specific and may not be available on all compilers. An alternative is to use __bis_SR_register(GIE)
which does the same thing.
The MSP430 is touted as being a low power microprocessor. This is because it is capable of operating in several different low power modes. In low power mode different components are turned off in order to save power[1:6]. I selected low power mode 0 LPM0
which turns off the CPU. This is done through __bis_SR_register(LPM0)
.
Since the push button is now capable of triggering interrupts, we need to listen for and respond to them when they fire. We do this by annotating a function with #pragma vector=PORT1_VECTOR
which happens to be the port1 interrupt service routine. While interrupts a convenient way of handling events, it's good practice to do as little as possible in interrupts and avoid lengthy tasks such as loops[1:7].
Once the interrupt is fired, it must be manually acknowledged by clearing the interrupt flag register
P1IFG
. This is done by setting it to 0[1:8].
Lastly, we to toggle the edge sensitivity of the interrupt. This allows us to select which edge of the signal trigger the interrupt. This is done by toggling the appropriate value in the Interrupt Edge Select P1IES
register[1:9]. Selecting the rising edge with a value of 0 (default) will cause the interrupt to fire when the button is pushed, and the falling edge with a value of 1 will trigger the interrupt to fire when the button is released.