This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

The Emperor’s new Watch

This post is dedicated to Alesea Schiopu, whom without her encouragement none of this would happen.

We thank you, the customer service department and the whole family of TI for all your help and caring!

 

The Emperor’s new Watch

If you happened to seriously play with TI’s fantastic ez430-Chronos, chances that sooner than later you’ve noticed that the accelerometer’s Z-axis reading is strange. I did. Being a Chronos rookie and all I thought there’s probably something I missed and so I hurried to scavenge Google’s results on the subject and around it. I found out a broad spectrum of related questions but somehow they were almost always deflected and drowned inside a chatter of other issues, mostly protocol related ones. Celebrating my (u8)7169 post on e2e, let me use it as an excuse for a light crash intro to Chronos plus tips.

There are actually two kinds of accelerometers. One measuring “coordinate acceleration” while the other measures “proper acceleration”.  Coordinate Accelerometers are measuring an object’s actual change of (displacement and therefore) velocity within space, and so are tightly related to POSITIONING, while Proper Accelerometers are actually no more than miniature weighing scales, constantly measuring the weight (i.e. exerted force) of some embedded tiny mass to deduce GRAVITATION. Since a free-falling object will constantly increase its free-falling speed in the presence of “gravitational field”, those terms are equivalent. On Earth, such a free-falling object will accelerate its speed by 9.81 meters per every second of additional falling, which is known as “g”. Since F=ma (force equal to mass times acceleration), by knowing the force (i.e. weight) and the mass of the object, the acceleration can be deduced. So (while on Earth) a mass “resting” on weighing scales will show 1g, while a mass “hanging-from” and pulling rather than pushing on the scales will show a -1g. Naturally, letting both the mass and scales free-fall, or placing them where no gravity is present, or positioning them “on their side” perpendicularly to the direction of gravity will register no weight, and therefore 0g. That’s as far as the theory.

In most of the accelerometer MEMS (micro-electro-mechanical-systems) that specific weighing scale is known as the ‘Z’ channel, measuring the g-force exerted from top to bottom (LCD facing up). I think MEMS are a real miracle of miniaturization. You might not believe it but when the CMA3000-D0X datasheet says that “the sensing element consists of … acceleration sensitive mass... Acceleration will cause a capacitance change that will be then converted into a voltage change …” it really means it! The tiny weighing scale is a compartment in which a tiny mass is suspended on seams (think of them as “springs”), where the displacement of those in relation to each other changes an internal capacitance, electrically changed to equivalent voltage further converted using an 8-bit analog-to-digital to 1 out of 255 possible values spanning the ‘g’ range for the axis. To differentiate between positive and negative, the variable is “split” at its biggest power of 2 (a.k.a. 2’s complement convention), giving 1..127 for positives and 255..128 for negatives, so that:

abs(uval) = (uval < 128) ? uval : 256-uval  (or ~uval +1);    [and]    IsNeg(uval) = (uval & BIT7);

The datasheet tells us that the accelerometer can be configured to work at either +-2g or +-8g range. The resolution for +-2g would be then 4g/255steps ~= 0.016 or 16mg which inversely would be about 62 steps per g (or 1000mg), but I’m not completely sure why, on p.6 the datasheet says 17mg (as an Integer of 56 steps) then on p.14 it goes to define it as 18mg (as a Round of 56 steps) and maybe that’s why you never get a full 1g reading at rest. Following the same logic the resolution for the +-8g range is 4 times coarser. The Chronos is fixed at the +-2g range as can be seen from AS_RANGE defined in “driver/vti_as.c” (‘as’ probably stands for Acceleration Sensor), although it could be probably smarter to “ramp up” to the broader range once the edges are hit and returning when the levels are low.

It is only so much fun to know the face up to face down rotation level. The accelerometer is actually a compound triplet of similar weighing scales, each fabricated and positioned perpendicularly to both others, supplying an 8-bit value following the same logic. You can think of them all as the three walls enclosing one corner in a cube. Since there is only one force working on all 3 weighs at all time (gravitation), their readouts would always reflect how much of this force is being exerted on each, i.e. their tilt. Look at p.23, facing up and leveled - only Z channel “feels” full (1g) gravitation. Since X and Y are perpendicular to gravitation’s direction they will readout as zero. Tilting North and South will affect the X “plane” and tilting East and West will affect the Y plane.

The lowest level function worth paying attention to is as_get_data() located at the same .c file. This one is being regularly called throughout the code with a parameter of a pointer to a 3 byte array which it fills with the raw (0..255) X,Y,Z values respectively. The array regularly used for that as a global throughout the code is sAccel.xyz. sAccel declared in “logic/acceleration.c” and defined in its respective .h file.

The same .c file also includes the important function display_acceleration(), which when in acceleration (top-line) display is responsible for nicely showing any of the selected X,Y,Z values (which are all updated, ready and waiting inside sAccel.xyz). Let’s skim through:

The three ‘update’ cases DISPLAY_LINE_UPDATE_FULL, DISPLAY_LINE_UPDATE_PARTIAL, and DISPLAY_LINE_CLEAR are common to the other display function as well and respectively correspond to entering the mode for the first time, steady state update mode, and exiting from the mode.

In steady state, sAccel.view_style (cycled by the ‘UP’ key) switches which byte of the XYZ will the ‘raw_data’ byte get, and correspondingly which of ‘X’, ‘Y’ or ‘Z’ character will be displayed at LCD_SEG_L1_3 which means Line1 (top) Position3 (out of the top 3, 2, 1, 0). The scarily long function convert_acceleration_value_to_mgrav() just takes that ‘raw_data’ byte, strips it of a negative sign, then uses the bitmapped array ‘mgrav_per_bit’ (from the table in p.14) to simply aggregate each bit by its corresponding ‘mg’ value into the local variable ‘accel_data’  then divides it by 10 – can you guess why? (hint: the top line can show up to 4 characters). To mitigate the frantic nature of the sensor readouts, only a 20% contribution from the current value is being averaged with 80% of the previous “moving average” (sAccel.data) and then saved for next cycle. This allows for a slower LPF-like convergence. The value is then converted to a string using the itoa() function and printed to the character ARRAY specified by LCD_SEG_L1_2_0, meaning Line1 from char pos2 to char pos0. Based on acceleration_value_is_positive() which simply checks BIT7 the small UP or DOWN arrow symbols are being turned on and off.

To sum it up, when entering the display for the first time, the “moving average” sAccel.data is cleared, the sensor module is started (while not in use it’s turned off to conserve power), the initial display axis (Y) is set, and last but not least, the decimal point (DP) is turned on just so the 000 to 200 (after the above division by 10) would appear as 0.00 to 2.00 – it is done only once since it should stay as long as acceleration mode is on. When exiting the mode, the sensor module is stopped, and the UP, DOWN and DP symbols are cleared. I skipped the timeout issues for the time being. You are welcome to experiment.

So what does it all got to do with strange values coming from the Z axis?

Four Three… Oh yeah! Placing the Chronos at a facing up, leveled position (let’s call it neutral from now on, shall we?) I got a bizarre approximate 0.4 value. Seeking an immediate psychological relief I tried to convince myself that it is calibrated so that 1g is read when the LCD is facing my head while the watch is being worn on my hand at an average degree – what? Complete nonsense… I obviously couldn’t live with this for more than a minute or so, then I plunged into the code to check whether it’s coming right from the raw value (you can see that at the test() mode as well), and when verified I checked Google.

‘iggarpe’ was one of the only few and the first who dared vocalizing the issue as soon as Dec 15th, 2010 with no resolution, just to be asked again recently on Sep 7th, 2011 by ‘Markus Artner’ and not to be answered.

Well, I didn’t know what to think. The datasheet suggested maximum errors equivalent to 150mg and I was nagging the technical support about that. I do however still strongly believe that similarly to the altimeter sensor, the acceleration sensor should allow the user manipulation of its non-volatile calibration information to be able and solve any offset problem regardless of its origin and especially if such a problem is hard to fix like a bad soldering in a whole batch.

Encouraged by Alesea to look again for a solution in the e2e forum I finally stumbled at an older post from Jul 12th, 2010 by ‘Gloin’ who describes how when releasing the backplate the raw ‘Z’ values are correct at 55 (remember the 56 steps per 1g?) and then drop to 25 with the plate screwed on.

Of course… direct mechanical pressure on the ‘Z’ weigh changes the axis value. How come an immediate relation between those posts values wasn’t realized is a puzzle I leave for you to solve…  

So does it have any ill effect? Is it model specific? Are all like that? Should the screws be kept little loose?

Forget it. Do you know how I solved it? I’ve replaced the CR2032 battery with a thinner CR2025. True, it won’t last as long but if you care about the “mint” integrity of the PCB you’ll probably take the call (or hack an LR2025 mod). FYI, the battery coding is ‘CRddtt’ where ‘dd’ is the diameter and ‘tt’ the thickness, both in millimeters.

Anyhow, there are times when you’ll want to recalibrate your offset values either because they are out of sync or you’ll want to start from a different zero reference.

Here’s a quick and dirty beep-rich calibration feature add-on, and first its auxiliary functions.

void PatWatchDog()

{

#ifdef USE_WATCHDOG

                                                // Service watchdog

                                                WDTCTL = WDTPW + WDTIS__512K + WDTSSEL__ACLK + WDTCNTCL;

#endif

}

I just grabbed it from ‘main.c’ – WDTSSEL__ACLK selects the ACLK (32.768kHz crystal) timer source, and WDTIS__512K is the timer count for resetting. 512/32 = 16 seconds to “pat” the dog or else… or else!!

u8 babs(u8 bval)

{

                        return (bval < 128) ? bval : (u8)((u16)256-bval);

}

This one will return the “byte Absolute value” of a byte.

u8 approx(u8 val1, u8 val2, u8 maxdiff)

{

                        return (babs(babs(val1)-babs(val2)) <= maxdiff);

}

This one will return logic true if the bytes ‘val1’ and ‘val2’ are within ‘maxdiff’ difference (inclusive).

u8 elapsed(u8 secs)

{

static s32 startst = 0;

                        if (!secs) goto reset_it;

                        else

                        {

                                                if (!startst) startst = sTime.system_time;

                                                else if ((sTime.system_time - startst) >= secs) goto reset_it;

                        }

                        return FALSE;

                        reset_it:

                        startst = 0;

                        return TRUE;

}

This one is cute. It returns logic true or false depending on whether the specified number of seconds has elapsed since it was called. If you’re not going to call it at least until it returns true and resets, you’ll have to reset it manually by calling it with zero as a parameter. Finally, here’s the calibration function:

#define MINNEARCNT 5000

#define MAXCLBDIFF 3

void mx_acceleration(u8 line)

{

u8 i;

u8 lxyz[3];

u8 near;

u16 cntgood;

 

                        clear_line(LINE1);

                        display_chars(LCD_SEG_L1_3_0, (u8*)"CLB ", SEG_ON);

                        for (i=0; i<5; i++)

                        {

                                                PatWatchDog();

                                                display_char(LCD_SEG_L1_0,'5'-i,SEG_ON);

                                                start_buzzer((i<4) ? 2 : 10, 1500, 1500, (i<4) ? 6 : 4);

                                                while(!elapsed(1));

                        }

                        memset(sAccel.offset, 0, 3);

                        as_get_data(sAccel.xyz);

                        memcpy(lxyz,sAccel.xyz,3);

                        cntgood = 0;

                        while(!elapsed(10))

                        {

                                                PatWatchDog();

                                                as_get_data(sAccel.xyz);

                                                near = 1;

                                                for (i=0; i<3; i++)

                                                {

                                                                        near = near && approx(sAccel.xyz[i],lxyz[i],MAXCLBDIFF);

                                                }

                                                cntgood = (near) ? cntgood+1 : 0;

                                                if (cntgood == MINNEARCNT) break;

                                                display_chars(LCD_SEG_L1_3_0, itoa(cntgood,4,0), SEG_ON);

                        }

                        display_chars(LCD_SEG_L1_3_0, (cntgood == MINNEARCNT) ? (u8*)"DONE" : (u8*)"EROR", SEG_ON);

                        if (cntgood == MINNEARCNT)

                        {

                                                lxyz[2] -= 200; //200 is u8 of -56 which is about -1g readout in 2g resolution (1000/18 per lsb).

                                                memcpy(sAccel.offset, lxyz, 3);

                                                for (i=0; i<3; i++)

                                                {

                                                                        PatWatchDog();

                                                                        start_buzzer(1, (i<2) ? 15000 : 30000, (i<2) ? 15000 : 2000, 5);

                                                                        while(!elapsed(1));

                                                }

                        }

                        else

                        {

                                                PatWatchDog();

                                                start_buzzer(3, 1500, 1500, 6);

                                                while(!elapsed(3));

                        }

}

When started, it gives you 5 seconds to place the watch in the wished neutral position. Then it will get an XYZ reading and allow a maximum of 10 seconds to perform 5000 consecutive reads with particular values within 3 steps (about 2% of 1g) of the initial readout. It will display ‘donE’ as soon as it’s successful, or ‘Eror’ if not.

 

We’ll set it to be automatically called by a long press of the ‘star’ button while in the acceleration display.

To do it, first add:

extern void mx_acceleration(u8 line);

to ‘acceleration.h’, and in ‘menu.c’ within the ‘menu_L1_Acceleration’ const, replace the ‘dummy’ function with our new ‘mx_acceleration’. As you can probably understand, the menus are following a simple state machine convention, where each menu also points to the next menu item (linked list).

Done. How to get there is taken care by the ‘mx_function’ handling within wakeup_event()  in ‘main.c’.

If you’re already at that, take a look at init_global_variables() which is where you prepare and tweak any global and startup values. If you are getting tired of pressing star, star, star… after a reset just to get to the acceleration display, just change the ‘ptrMenu_L1’ to, well you know already.

So… everything works? If you’ve been really following up and implementing you should get the compiler crying for not having a member called ‘offset’ in structure ‘sAccel’. What are you waiting for? Go there and add a 3 byte array similar to the xyz. This one would hold the XYZ offsets once the calibration is done. Depending on your environment you might also need to include <string.h> for the memset() function.

To actually put the offset to work, modify as_get_data() in ‘vti_as.c’ so that the corresponding offsets are reduced from the raw ones. If all works well then after calibrating in a neutral position, x and y should readout about 0g at rest while z should readout about (though probably less than) 1g.

Now I have to briefly touch an important issue. Up until now we regarded the accelerometer as a device that gives its 3-axis tilt in relation to gravity, though you should also remember that MOVING the accelerometer inevitably exerts weighing force (in a direction opposite to the movement), and therefore gives a valid acceleration readout that can be used to derive velocity and displacement. In this sense, what was our limitation, i.e. the fixation of the ‘Z’ axis is now serving as a reference anchor. That said however, there are inherent “sensory” limitations, e.g. a spinning-top-like motion will give no readouts at all, and a centripetal (satellite-like) motion will be interpreted as moving in a “straight line”.

Now that you probably completely got the hang of it, I recommend you to fly through the datasheet and then hit Google on the subject of accelerometers and create some nifty projects.

Here’s a suggestion, add short beeps to the acceleration axis readout, such that the more you deviate from 0g the faster the beeps. You can use different pitch to differentiate between positive and negative readouts, and next time you have to hang a shelf you can just place your watch on it and follow the sound cues until it is leveled (and quiet). Hey, it can even be dubbed as an “idiot-detector” party gag :)

Enjoy,

r15

  • A true coordinate accelerometer instantly measures the direction and distance from Felicity, California USA. See:

    http://www.roadsideamerica.com/story/2036

    It is two-dimensional and no need for z-axis. See:

    http://www.alaska.net/~clund/e_djublonskopf/Flatearthsociety.htm

     

  • Nice post. It covers the generation of the axis data well and also gives a good explanation and solution of the weird Z-axis problem.

    One thing I was missing and what would really give the sensor readings a meaning would be the discussion of how to eliminate the gravity offset form the readings, so that only acceleration relative to surface (or earth core, to be exact) is detected and shown.

    This means, placing the watch upside down on the table should still give a triple-0 reading since the watch is not accelerated (you'll need a glass table to check :) ). While this mode wouldn't be suitable for a tilt sensor, it would be for movement tracking. Which would be the more useful approach for a watch (which is usually kept on the wrist and moved and turned around all the time).

    However, the math is quite complex (you already noted it when talking about spinning and circular movement), and some things cannot be done with just one 3-axis sensor (you need 6 axis readings to properly detect rotations and tell them from accelerations, mounted as three 2-axis sensors.) And I fear, the MSP isn't powerful enough to do the necessary math with the required speed.

**Attention** This is a public forum