One of the (many) weird quirks of studying in the University of Cambridge is that our terms only last 8 weeks.

Yes, the number of days we are officially “studying” is less than the number of days we are ostensibly on “holiday”. Of course, it never works that way, and Cambridge is probably one of the only places where going to uni feels exactly like going to secondary school.

Over these long break, we are told that our rooms are going to be used for conference purposes, and that we would have to move all our stuff out to a tiny, cramped storage container. That really got me wondering who on Earth would let scientists or whoever would be coming over for conferences live in my room, which doesn’t even have a level floor because it is an attic room. It’s literally putting up the ugliest bit of my college for show, just doesn’t make sense; and during COVID where there definitely wasn’t any conferences going on, they still wouldn’t let me box my stuff up and leave them in my wardrobe, as they claimed my room would be cleaned, even though it really wasn’t when I returned.

So I decided I had enough, I would verify once and for all if there are any signs of life over these 6-week long breaks. I had the idea of using a PIR sensor to log all human movements in my room. It’s a fun excuse to dabble in some low power design techniquies, as whatever gizmo I come up with would have to be battery powered.

Microcontroller Selection: MSP430

Turns out, there is a lot more that goes on beyond the headline sleep current specifications when selecting a suitable microcontroller for low power applications like this. This post on the excellent Ganssle Group website discusses why a lot of the headline marketing claims on datasheets is probably complete garbage when it comes to designing ultra low power stuff. There is even this white paper by TI where they basically fired shots at Microchip who presumably dissed the MSP430 in their PIC24 marketing material.

Thing is, while a lot of modern microcontrollers have sleep currents in the sub microamp range, which is absolutely mind blowing, it just takes one decoupling capacitor (all of which that are of a reasonable type or price have leakage current in the microamp range) to completely dominate the standby current draw and make these low sleep currents absolutely meaningless.

In addition to that, microcontrollers have many many different sleep modes, some of which don’t even include RAM retention; and figures for sleep current draw depend heavily on things like clock speed and supply voltage and how long the microcontroller takes to wake up and actually start doing useful stuff etc. Point is, it’s not as simple as just seeing a 20nA stop mode current and thinking that a project is going to last 10 years on battery.

There are a few reasons I chose to use an MSP430 for this project:

  • Its headline feature is its low power consumption, so I wanted to see just how good it is
  • It is available in a DIP package
  • It requires minimal external components to run
  • It’s quite limited in terms of its resources, with the MSP430G2452 I went with only having 256B of RAM, which I thought would be a fun challenge and a breath of fresh air from the super powerful modern ARM chips.
  • I had an old LaunchPad lying around which I haven’t touched in ages

It also has some niceties like built in programmable load capacitors to run a watch crystal, very handy for keeping track of time without needing an external, expensive RTC. It also has an internal temperature sensor, so I could log both temperature and activity just because why not.

And perhaps the most important of all: it was in stock, and not too expensive.

Failed SD Card attempt

PetitFat on MSP430

I thought at first that logging to an SD card would be extremely cool, so I set out to port over the PetitFat project to the MSP430 platform, since I couldn’t find anything within 10 seconds of Googling.

It wasn’t particularly difficult, basically what I had to do was to implement the SPI functions to write to and read from the SD card, and the generic sample code handled the rest, including the specifics of the SD card protocol.

In typical hackathon fashion, the SPI was implemented in the crudest way possible, with direct configuration of the 3 relevant USI control registers.

Basic SPI functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <msp430g2452.h>

int SPIInit()
{
//SPI Config
USICTL0 = 0b11101010;
USICTL1 = 0b10000000; //disabled counter interrupt
USICKCTL = 0b10001000; // choose 1MHz SMCLK
P1DIR |= (1 << 5 | 1 << 6); //set mosi and clk to output
return 0;
}
char SPITransfer(char data)
{
USISR = data;
USICNT |= 8;
while ((USICTL1 & 1) == 0);
char res = USISR;
return res;
}

Then it was just a matter of replacing the relevant functions in mmcbbp.c

Implementation of Relevant I/O Functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
...

#define INIT_PORT() init_port() /_ Initialize MMC control port (CS/CLK/DI:output, DO:input) _/
#define DLY_US(n) \_\_delay_cycles(16L*n) /* Delay n microseconds _/
#define FORWARD(d) forward(d) /_ Data in-time processing function (depends on the project) \*/

#define CS_H() P2OUT|=1<<3 /_ Set MMC CS "high", using P2.3 for the CS line _/
#define CS_L() P2OUT&=~(1<<3) /_ Set MMC CS "low" _/

...

static void init_port(){
    SPIInit();
    P2DIR |= 1 << 3; //set CS to output
}

...

static
void xmit_mmc (
    BYTE d /_ Data to be sent _/
)
{
    SPITransfer(d);
}

static
    BYTE rcvr_mmc (void)
{
    return (BYTE)(SPITransfer(0xFF));
}

static
void skip_mmc (
    UINT n /_ Number of bytes to skip _/
)
{
    do {
        SPITransfer(0xff);
    } while (--n);
}

static
void release_spi (void)
{
    CS_H();
    rcvr_mmc();
}

Apart from the usual silly mistakes like me connecting the MISO line to ground accidentally and wondering why the MSP430 just wouldn’t wait for the card to respond correctly, the basic functionality worked out of the box.

It was when I tried to do something non-basic that problems arose.

Problems with Running PetitFat on These Low RAM Devices

There are some gotchas with running a FAT filesystem on the 256B RAM of the MSP430, mostly when you try to enable file writing. For starters, PetitFat does not support creating files. So basically you’d have to pre-create empty binary files that PetitFat can then modify. They would also have to be sufficiently large, since PetitFat also cannot expand pre-existing files.

1
2
cd YOUR_DRIVE:/
fsutil file createnew 001 1024

I was able to get writing to this file to work. However, as highlighted in the PetitFat page, only writes on sector boundaries are supported. Basically, this means that you can only write 512B at a time (for SD cards, the sector size is usually set to 512B; in theory you could change that but it is discouraged).

With the MSP430G2452 only having 256B RAM (RAM is required to buffer the data before writing), this quickly becomes really clunky, because at most you could only use up half of the space of a given file, and that is assuming that nothing else uses any RAM at all, which obviously wouldn’t be the case. There are probably ways to work around this such as generating the data to be written on the fly, but that requires substantial modification to the library that I didn’t feel like doing; this was supposed to be a quick and dirty project that I could whip up while juggling all the craziness that this university puts on us…

PlatformIO Static Analysis Issues

I started really coming-a-gutsa when trying to run the AVR test code supplied in the PetitFat samples.

30
char Line[128];		/* Console input buffer */

I allocated a similar buffer on my MSP430, and everything crashed. The program would just loop in place and not move forward…

It was almost as if something had overwritten the program space or stack or PC or whatever, just as when doing binary exploitation…

But, PlatformIO reported that everything was fine (this was under the default release build settings).

I started playing with the size of the char array. Changing it to 64 made everything work again, which really made me suspect that there was some fishy memory shenanigans going on. It is also telling that the RAM usage was only reported as 16 bytes when clearly, there was a 128 byte char array going on.

I tried a bunch of things to get PlatformIO to report the RAM correctly: using volatile to ensure that there wasn’t any weird optimisation going on that broke the static analysis, changing the content of the buffer on the fly, filling the buffer up with 0x41’s with a for loop (C89 is evil in that you cannot declare the initial i in the for loop by the way; no idea why the MSP430 PlatformIO config uses C89) so that it will get filled in during runtime, conditionally filling in this buffer with 0x41’s or 0x42’s to be 100% sure that there isn’t any compile time shenanigans going on…

Nothing could get PlatformIO to report it correctly.

So I tried using the classic malloc() which made it work up to allocations of 128 bytes, however PlatformIO still wouldn’t report the RAM usage correctly.

The program was already too large to build in the debug configuration, which is requried for PlatformIO’s advanced memory analysis to take place, so it was really a dead end at that point.

What was even more alarming is that I tried to set the length of the char array to 512 bytes, clearly too large, and it still compiled correctly without even throwing a warning.

What did work was generating a map file by adding the following lines to platformio.ini, but I wasn’t well versed enough in .elf file structure to understand what was going on properly.

Code Composer Studio Static Analysis Issues

So I tried Code Composer Studio, and was immediately greeted with this with malloc() set to 128 bytes:

I thought I could finally move on from this confusion. Turns out that my relief was short lived because Code Composer Studio also doesn’t do it correctly, as it still allowd me to malloc more than 128 bytes, and the RAM usage still didn’t change.

What was worse is that presumably because Code Composer Studio uses less aggresive optimisation settings by default, the code didn’t work at all, when at least it did with PlatformIO…

I tried looking into the .bss section in the memory map, didn’t see anything suggesting that my buffer was there at all. It turns out that malloc() fails aren’t detected during compile time, and malloc() would just return a null pointer during runtime in situations like this.

I was really confused, and decided that it would probably be best to wrestle with this another day. For the time being it was back to the drawing board.

Also, I need Rust…

Attempt 2: EEPROM

A couple of hot showers and hours of Googling later, I decided to go with an AT24C256 256kb EEPROM IC for the non volatile memory for this project. EEPROM is nice in that it is really low power (quite a bit lower than SD cards during write operations), and it can do bytewise writes, so I can forget about all the buffering data and writing 512 bytes at a time shenanigans. So in my mind, it would make development much much easier.

The downside is that instead of 32GB, I now had 32kB. So I’d have to think more carefully how I structure my data.

Data format

Starting from the requirement of having enough storage for 40-ish days, preferably more so I had a little more leeway just in case the cheap eBay crystal decided to drift way way off, I decided to record a data point every 8*24 = 192 seconds. With the 32768 8-bit readings the EEPROM could store, this works out to be about 72 days, more than enough.

Final implementation

Becsue I was really really pressed for time, I decided to completely throw out all of my fancy ideas; something was really better than nothing.

Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <msp430.h> 
#include <stdint.h>
#include "usi_i2c.h"

/**
 * main.c
 */
volatile int aclkDiv = 0; 
const int divideBy = 24; //second layer prescaler
volatile int count = 0; //how many counts since last transmit
volatile unsigned int address = 0; //EEPROM word address
volatile uint8_t transmit = 0;
uint16_t data[] = {0xA0, 0x00, 0x00, 0x00};

int main(void)
{
    WDTCTL = WDTPW | WDTHOLD;	// stop watchdog timer

    BCSCTL1 = CALBC1_1MHZ; //1MHz
    DCOCTL = CALDCO_1MHZ;  // SMCLK = DCO = 1MHz
    BCSCTL3 |= XCAP1 | XCAP0; //max load capacitance

    P1DIR |= BIT0 | BIT1 | BIT2 | BIT4 | BIT5; // set irrelevant port 1's to output
    P1OUT &= ~(BIT0 | BIT1 | BIT2 | BIT4 | BIT5); //gnd them

    //enable pin change interrupt here
    //    P1OUT |= BIT3;
    //    P1REN |= BIT3; //pullup enable (not needed since it messes with the 20K resistor of the PIR)
    P1IES &= ~BIT3; //lo to hi
    P1IE |= BIT3;
    P1IFG &= ~BIT3;

    P2DIR = 0xff; //set Port 2 to output for unused
    P2OUT = 0; //GND all pins

    i2c_init(USIDIV_2, USISSEL_2); //smclk/4 = 250k

    TACTL = TASSEL_1 | ID_3; //select ACLK, enable interrupts, and DON'T SET TAIE
    TACCTL0 = CCIE; //enable capture/compare interrupt
    TACCR0 = 32767; //overflow in 1 s (period is TACCR + 1)
    TACTL |= MC_1;

    __bis_SR_register(GIE); //enable interrupts globally

    while(1){

        if (transmit != 0 && i2c_done()){ //need to transmit

            data[1] = address >> 8;
            data[2] = address & 0xff;
            data[3] = count;
            transmit = 0;
            count = 0;
            address++; //unsure where it will return from LPM, so do everything before calling i2c send
            if(address > 32767){
                address = 0; //we only have 32767 bytes unfortunately
            }
            i2c_send_sequence(data, 4, 0, LPM0_bits); //REMEMBER TO REMOVE JUMPER

        }

        if(i2c_done()){ //don't kill smclk
            LPM3;
        }
        else{
            LPM0;
        }

    }
	return 0;
}

#pragma vector=TIMER0_A0_VECTOR
__interrupt void Timer_A0 (void){

    aclkDiv++; //increment secondary prescaler

    if (aclkDiv == divideBy){
        transmit = 1; //we should transmit now
        aclkDiv = 0;
        LPM3_EXIT;
    }

}

#pragma vector=PORT1_VECTOR
__interrupt void Port_1(void){

    count++;
    P1IFG &= ~BIT3; //manually clear interrupt flag

}

Clocks

I wanted this thing to be an exercise in low power design, which meant putting the MSP430 into as low a power mode as possible. This meant that I could only use the low frequency ACLK to keep track of time in standby, switching to the faster MCLK upon waking up to handle all the data processing and writing to memory.

I chose to go with a 32768kHz watch crystal, since the MSP430G2452 I used has built in adjustable crystal loading capacitors, and I didn’t want to fiddle around with trying to calibrate the internal very low frequency oscillator, which TI explicitly states is far from precise.

Turns out that the cheap crystals from eBay I bought have a rated… ahem… claimed… load capacitance of 30pF, and the MSP430 can only do a maximum of 12.5pF. Oops. That being said, I did at least measure the ACLK output and my multimeter read 32.76kHz, so we are not too far off. Good enough for now.

As for the main clock, I chose to go with 1MHz. I could have gone faster, but in my mind there was no need anyway, since the speed of the i2C bus would ultimately limit how much time could be spent in the lowest power sleep mode, since it relies on MCLK, and Low Power Mode 3, which I am using, disables everything except ACLK.

Interrupts

Being a low power design, we want the CPU to be off as much as possible. Thus, everything, including the i2C bus, has to be driven by interrupts.

Timer A0

Timer A0 is set to capture/compare mode, with a clock dividsion of 8. Thus, it triggers when the counter value reaches 32767, ie every 8 seconds. In hindsight, setting the capture/compare value to the maximum of 65535 would probably be better for low power since the CPU would then have to wake up half as often. Oh well…

Within the ISR, there is a separate counter which increments each time the ISR is fired. When the counter reaches a preset value (24 in this case), the ISR sets a flag to let the main loop know that it is time to save the current count value to EEPROM. In essence, this is sort of like a prescaler, which is needed since the MSP430G2452’s timer can only divide up to a factor of 8 unlike many other microcontrollers.

Of course, doing it this way is not ideal, since the CPU does have to wake up just solely to increment this prescaler counter, which requires spinning up the 1MHz clock, all of which consumes power.

When the prescaler counter reaches the preset value, LPM3_EXIT is invoked, which resumes program execution at the point right after LPM3 was called, which in our case essentialy resumes the main loop.

GPIO Pin Change Interrupt

A random GPIO is selected to monitor the output of the PIR sensor, incrementing count. Note that GPIO interrupt flags are not cleared automatically, so there is a line in the ISR to do that manually, otherwise the interrupt will just keep firing.

i2C

The i2C implementation is taken from this wonderful GitHub repository .

One thing to note is my variant of the MSP430 only has the more basic USI peripheral. If you are using one that has UCSI, you need to use another library, which I’m sure would not be a problem since most of my initial searches yielded UCSI driver code anyway.

Also, depending on the strength of your pullups, it is very likely that you would need to remove the jumper connecting Pin 1.6 to the Green LED on the LaunchPad, otherwise you might run into issues where the LED pulls SCL too low.

Additional Remarks

I strongly recommend that you change the compiler from its default C89 mode to something newer; not that C89 doesn’t work, it just has too many quirks that drives noobs like myself up the wall.

Also, I decided to build the circuit point-to-point so that I could fit it into the space occupied by a single AA battery. This way, everything is nicely contained within a single 3 cell AA holder.

construction of completed unit

Completed Logger

close up view from another angle

Close up from another angle

Of course, the crystal oscillator is literally flapping around in the breeze, which is almost certainly going to affect its accuracy, but then again, the whole load capacitance error thing mentioned above probably already threw any last sliver of accuracy out the window.

Flaws and Wishlist of Features

This project is really far from perfect.

You know that feeling when you come up with an idea and start researching and working on it excitedly, only to have life get in the way and before you know it you’ve lost interest?

Well, this is one of those projects.

However, I thought that I have wasted enough parts in my career as a hobbyist, so I pushed through it. And I’m glad I did, the end product has its own unique charm, being slapped together in 2 days and still working properly.

Here are some of the things that I wish I had the time to implement, or quirks that only came up as I sat there admiring the build as I packed my room away for winter.

Watchdog Timer

For projects that are intended to run for an extended period of time like this one, it is quite important to have the watchdog timer properly implemented so that there is an additional line of defense to rescue the project if there are any software bugs.

However, I didn’t have the time to look into this properly, since it seems like the watchdog timer also only has a 8x divider, meaning that tickling it constantly would be not be straightforward as something has to run more frequently than once every second, which in this case, nothing is.

More Robust Data Format

To implement a watchdog timer correctly, the code needs to be able to recover from essentially a hard reset, which means that it should pick up where it left off, and not start writing from address 0 each time it runs.

This should be really easy to implement: one could just dedicate the first 2 bytes of the EEPROM to store the EEPROM word address that was last written to, and simply read it back at the start of the program.

Temperature Logging

One of the cool features of the MSP430G2452 is that it has an internal temperature sensor that is factory calibrated, meaning that there are pre-programmed values of the thermistor as read by the internal ADC at 2 different temperatures. Therefore we could just do a quick linear interpolation to get the correct temperature reading. This is needed, because while the temperature sensor is relatively linear, its offset is highly variable.

Or that’s what TI says anyways, I haven’t tested it for myself.

It would have been really awesome, and less boring, if this build also tracked room temperature variations across a day, perhaps something to consider implementing in the next revision.

More Efficient Memory Usage

Right now, the code simply counts how many “movements” are detected within a 192s window, and writes it into the EEPROM. There are a few issues with this.

The AS312/AM312 sensor has quite a long non-adjustable hold period, i.e. if there is constant movement, it stays on. Since we are counting the number of rising edges, the actual count will probably be far, far less than 255. Therefore, it just doesn’t make sense to track any activity the way I’m doing it.

A better way would be to spilt each byte into 2 4-bit numbers and have a max of 15 counts in 3 minutes.

Or perhaps, just do a binary thing: 1 if activity is detected within a window, 0 if otherwise. I think this would be a better way to reperesent human activity anyway.

Doing it this way would also mean that we could take readings more often since we would effectively have 262,144 datapoints. Not that once per 3-minutes-ish isn’t plenty for this purpose.

Conclusion

Well, for now, it is what it is. Hopefully when I return from my vacation the logger will still be there, and there would be meaningful data that I can pull off of it.

Until then, happy tinkering!

Update: I’m Back, and the build was still intact

Unfortunately, someone moved it into a storage cupboard so basically that was the last day it saw the light until I returned.

This also meant that at the very least, nobody opened up that cupboard throughout the entire vacation period, which was semi reassuring.

Short data analysis

Using a Raspberry Pi Pico, I modified the script from the EEPROM verifying jig to read the data out to a serial console. I then used a short Python script to plot this daily activity count graph of sorts.

activity plot

Graph of Activity Count Each Day

I was quite surprised actually that there was no activity even on the fourth day that this thing ran, meaning to say that my room had been cleaned the day I left, much sooner that I expected. This also meant that what followed was super boring: basically nothing at all.

On the bright side, the logger did not run out of battery at all, and when I returned to Cambridge and set the logger on my desk again waiting to take a closer look at it, there was indeed lots of activity. The date when activity resumed was also the actual day I returned, which meant that the crystal worked good enough.

There was also a bunch of space left on my EEPROM by the way, so this thing could easily have gone on for weeks more.