Friday, August 8, 2014

Reading HMC5983 sensor data asynchronously over SPI

In the current context “asynchronously” mean that reading samples from the magnetometer will take place asynchronously with regards to the main execution thread on one or more interrupt threads. Thus the main thread, which most probably will be our control loop, may go through its calculations completely oblivious to the fact that its operation will be interrupted for a few microseconds to deal with the new data sample arriving from HMC5983. However, when the main thread needs a fresh data sample from the magnetometer, it most probably will be ready for consumption without any unnecessary wait.

If you may recall from the previous post, the HMC_Reset(…) function, invoked as part of initialization, configured HMC5983 for Continuous-Measurement Mode. According to the HMC5983 datasheet: “In continuous-measurement mode, the device continuously performs measurements and places the result in the data register. RDY goes high when new data is placed in all three registers“.  So what is this mysterious RDY that goes high?

From the same datasheet, “RDY - Ready Bit. Set when data is written to all six data registers. Cleared when device initiates a write to the data output registers and after one or more of the data output registers are written to. When RDY bit is cleared it shall remain cleared for >200 μs. DRDY pin can be used as an alternative to the status register for monitoring the device for measurement data.”

So, what is DRDY pin? It is Pin 15 of HMC5983 package: “DRDY – Data Ready Interrupt pin”.  Thus by connecting DRDY pin to one of the available pins on our MCU and associating this pin with the MCU Interrupt controller using Peripheral Pin Select (please see _HMCInitPinMap() function) with proper configuration we may trigger an interrupt and switch control from the main thread to the Interrupt Service Routine (ISR) as soon as magnetometer has a new measurement!

HMC_AsyncStart() function, defined in the HMCSPI_Async.c file, performs this “proper configuration” by simply enabling respective interrupt. It also sets the flag indicating that asynchronous data read from the sensor is enabled (_HMC_Async = 1) and resets to 0 counter of the samples (_HMC_Ready = 0) in asynchronous read buffer. The importance of this counter will become clear shortly. HMC_AsyncStop() function implemented in the same file reverses the process by resetting asynchronous operation flag, discarding samples in the buffer, and disabling interrupt.

Functions HMC_AsyncReadIfReady(…) and HMC_AsyncReadWhenReady(…) provide access to the sensor data accumulated in the asynchronous read buffer. The difference between these functions is that the former returns error code if there is no yet new data in the buffer, while the latter waits until the data becomes available. For the actual data retrieval from the asynchronous buffer both of these functions call internal helper routine _HMCAsyncRead(…). In my code, as you may have noticed, the names of internal functions and variables, which are not exposed to the API caller, are prefixed with “_”.

_HMCAsyncRead(…) function deserves a closer look. This function needs to extract the sample from the asynchronous read buffer and modify (actually, reset to 0) the counter of the samples in this buffer. However, both the buffer and respective counter could also be modified by the interrupt routine – how can we serialize access to these data structures so the main thread (on which _HMCAsyncRead(…) is being executed) and interrupt routine do not step on each other’s toes? Usually Operating Systems provide some primitives to provide thread synchronization – like locks, mutexes, semaphores, etc., but here we work on a “bare metal” – our code IS the Operating System, so we have to implement serialization ourselves.

PIC architecture defines 8 priority levels available for the main thread and user ISRs; typically the main thread runs at the lowest priority of 0, while ISRs are configured to run at higher priorities, which allows them to interrupt main thread. Actually, another way to disable an interrupt routine is to set its priority to 0 so that it will not be able to interrupt the main thread. However the recommended way to implement serialization is to create a so-called “critical section” by temporarily raising priority of the main thread to the level of interrupt routine that we would like to block.

If you recall, HMC_Init(…) function as its first parameter takes the numeric value which defines the priority level (interrupt level) of all the interrupt routines configured in the HMCSPI module. This value is also stored in the static variable _HMC_IL and is made available to all routines in the module. Compiler provided function SET_AND_SAVE_CPU_IPL(current_cpu_ipl, _HMC_IL) stores current priority level in the local variable and sets the current CPU priority to the value stored in _HMC_IL effectively blocking ISRs defined in the HMCSPI module thus creating a “Critical Section”, which protect consistency of the data shared between the main thread and ISRs. Function RESTORE_CPU_IPL(current_cpu_ipl) restores priority to the original level thus terminating the Critical Section.

It is important to maintain the duration of the Critical Section at minimum, so in the Critical section I usually just copy data from shared to some local storage and perform any further data processing outside of the Critical Section.

Now let’s see how the data gets into this asynchronous data buffer that we got to so much trouble to protect. The actual read of the HMC5983 sample data performed by two inter-related interrupt routines - HMC_Interrupt() and SPIInterrupt() implemented in the code file HMCSPI_ISR.c.

If asynchronous processing of the data samples in enabled through the call to HMC_AsyncStart() function, raising edge on the DRDY line will trigger External interrupt and transfer control to HMC_Interrupt() function. First this function checks the availability of the SPI bus (some synchronous operation may be using bus at this time) and, if available, acquires it. This is achieved through the call to inline function _HMC_AcquireBus(). If you recall, the same function is being used by synchronous _HMC_RegRead(…) and _HMC_RegWrite(…) functions, which are usually called on the main thread. To protect against potential collision due to race condition in checking and acquiring bus _HMC_AcquireBus() also implements Critical Section protection.

If SPI bus is busy, HMC_Interrupt() function just return without any further processing – this sample will be missed and reading will restart when the new sample will be made available by the sensor and indicated by raising DRDY line. If the SPI bus was acquired successfully, interrupt function formats the FIFO buffer template, enables SPI interrupt, and pushes formatted FIFO template into the SPI FIFO buffer. This ends processing the DRDY interrupt – the whole processing takes just a few microseconds!

14 microseconds later (transferring 7 bytes over 4 Mbs SPI bus) transmission will be complete and SPI bus will raise SPI interrupt; this interrupt will transfer control to SPIInterrupt() function. This function retrieves bytes from the FIFO read buffer and converts them into the raw sensor measurements for X, Y, and Z axis. Then comes an interesting point – if _HMC_Ready is 0, the newly read sample stored in the asynchronous read buffer _HMC_Sample, otherwise the new sample is added to the previous values in _HMC_Sample buffer. _HMC_Ready is incremented in either case.

The advantage of this approach is that if our control loop runs at lower frequency than sensor’s ODR, the next call to one of the asynchronous read functions will return AVERAGE of the samples accumulated since the previous call. This implements some additional low-pass filtering without much overhead.

Project 13-HMCSPI-Async brings all of this asynchronous functionality together in a small demo program that reads data from the sensor and submits it for logging over the SerialData Logging interface. The output is identical to the one discussed in the previous post and uses the same log file template for parsing.


Now, equipped with the deep understanding of interrupt-driven IO processing we can bravely jump into issues of I2C communication, which will be the topic of the next post.

Wednesday, August 6, 2014

Communicating with HMC5983 Magnetometer over SPI

My new board uses new magnetometer form Honeywell – the HMC5983. This one has serious advantages over its predecessors – automatic temperature compensation, much higher sampling rate up to 220 Hz, and a choice of protocols – I2C (which was present in older sensors) and SPI.

I did a lot of work with I2C in my previous designs and even found a way to talk to multiple devices on the same bus in the interrupt-driven mode, but I found I2C rather slow and too “chatty” and too heavy on the interrupt system as it generates interrupts per each byte transferred. There are several good reviews comparing I2C and SPI protocols and providing some implementation details for each: I²Cvs SPI, SelectingBetween I2C and SPI, Introductionto SPI and many, many more. Anyway, for this project I decided to use SPI to communicate with HMC5983.

SPI is rather simple protocol and, in my implementation, one of the PIC24EP512GP806 SPI modules is dedicated to communication with HMC5983 so I decided against creating a separate SW module to encompass SPI functionality – both SPI bus management routines and functions specific to HMC5983 are integrated in one SW module – HMCSPI available in my code library.

As usually in my SW modules, header HMCSPI_Profile.h provides definitions required to link the code to specific SPI and Interrupt modules of MCU and define the MCU’s pins dedicated to control and communication with HMC5983. Please note that in this board HMC5983 connected to PIC24EP512GP806 using SPI2 module, which has dedicated pins for SDO, SDI, and SCK. If any other available module would have been used (SPI1, SPI3, or SPI4) we would have to link respective pins to the module using PIC’s Peripheral Pin Select feature that we touched upon in the previous post.

HMCSPI_Local.c and HMCSPI_Local.h allocate space and provide definitions for some variables, flags, and structures shared among the routines of the module. Those variables that may be changed both in the main thread code and in interrupt service routines (ISRs) are tagged as “volatile” to provide compiler with the proper hint. Also please note that there are several vectors defined in these files for Hard and Soft Iron correction – for now they are just initialized; how to fill them with the values specific to the sensor and the board we will discuss in the post devoted to HMC5983 calibration. Vector module implementation consists of one header file Vector.h, which defines Vector structure and provides inline functions implementing various vector operations.

Header HMCSPI_Inlines.h provides definitions for several inline functions that assist with some basic HMC5983 data transformations and SPI bus control. These functions are rather trivial and are well documented with the code comments. Header HMCSPI.h provides HMCSPI API definitions required for external programs to use functionality exposed by this module; this is the only header file that need to be included in the programs that use HMCSPI module.

Prior to using HMCSPI module’s functionality it needs to be initialized using the call to HMC_Init(…) function defined in HMCSPI_Init.c file. Except for the Interrupt Level (priority of the interrupts associated with the module), all other parameter are just being passed to HMC_Reset(…) function, which we will discuss shortly, for sensor configuration. Other than that, HMC_Init(…) performs initialization of some data structures, configures and resets interrupt lines, and initializes and configures SPI HW module.

Every line in this function is provided with comments explaining what is being done. However, it is important to mention that SPI module is configured to enable 8-deep FIFO buffer and the SPI interrupt is configured to take place after all the bytes loaded into FIFO are transmitted. All the data communication with HMC5983 implemented in the module is limited to 7 bytes – 1 byte for register address and 6 bytes of sensors data – two bytes for each X, Y, and Z; individual registers reads and writes are even shorter – from two to four bytes. Thus all required I/O operations can be completed with just one load into the FIFO buffer. If I would have to transmit more than 8 bytes, I probably will trigger interrupt when FIFO is half-empty to add more bytes for data transfer. In this case the situation would be quite similar to the one we discussed in relation to SerialData Logging in the previous post. Anyway, while looking at this function it would be handy to keep open SPI FamilyReference document for this MCU.

At the end of initialization HMC_Init(…) calls HMC_Reset(…) function, defined in the HMCSPI_Reset.c code file, to perform initialization of the magnetometer itself. Parameter for this function are documented and explained in the code. It is important to note that this function calculates and stores conversion factor, based upon the Gain parameter, to be used for converting raw sensor data into the actual strength of magnetic field in Ga.

As part of the reset process sensor is put into the Continuous-Measurement Mode, in which the sensor automatically initiates new measurement at the defined Output Data Rate (ODR parameter). More detailed explanation of register settings required for sensor initialization is provided in the HMC5983 datasheet.

Now that initialization is completed, we can start looking at synchronous communication with the HMC5983 sensor. By “synchronous” in this context I mean that all operations are performed on the main thread and all call for read and write are blocking. At the core of the synchronous communication are 3 internal functions (localized within the module and not exposed to the users of HMCSPI module) - _HMC_IO(…), _HMC_RegRead(…), and _HMC_RegWrite(…). All synchronous I/O functions, including both internal and external – exposed to the API users, are implemented in HMCSPI_Sync.c code file.

_HMC_IO(…) function accepts a byte array, loads it into the FIFO buffer, and then waits until transmission is completed. At the end of transmission it reads received data from the FIFO buffer and returns it to the caller. It also indicate for the sensor that operation is ended by de-assering CS line through the call to CS_Stop() macro. Please note that CS line is active-low, so the CS_Stop() macro actually raises CS line to Vdd. Function does not check the length of the data array, so it is responsibility of the caller to make sure that the buffer provided does not exceeds the FIFO depth.

SPI operation is fully duplex, so from the perspective of a low-level I/O both reads and writes are identical, thus we can use single routine, _HMC_IO(…), for both reads and writes. The differentiation factor between reads and writes is actually the flag in the first transmitted byte. Configuring this byte for respective operation is actually the function of the other two helper functions - HMC_RegRead(…) and _HMC_RegWrite(…).

These two functions are almost identical – the only difference is that one sets the flag in the first byte for Read, and the other one – for Write. The other two fields in the first byte are the sensor’s register address from which we want to read data or into which we want to write, and the “auto-increment” flag. If this flag is set, then for a multi-byte operation the first read (or write) will be from the register identified by the Register Address, but for each subsequent byte the register address will be incremented. Considering, for example, that sensor data is located on HMC5983 in six consecutive 1-byte registers starting at the address 0x03, by setting the “auto-increment” flag and providing address 0x03 in the first byte we can read all six bytes in one multi-byte operation.

As parameters these two functions receive the HMC5983 register address and the data buffer from which to write to the sensor or into which to read sensor data. Functions validate parameters to make sure that the requested amount of data does not exceed the sisze of the FIFO buffer, and then format the data into the template of FIFO buffer.

Next these functions attempt to “acquire” control over the SPI bus – the bus might be busy if the asynchronous (on the interrupt thread) operation is taking place; we will look at the details of asynchronous operation shortly. Anyway, if the bus is busy, _HMC_RegRead(…) and _HMC_RegWrite(…) will wait until it is free, acquire it into exclusive use, and pass the formatted FIFO template buffer to _HMC_IO(…) function to perform actual operation.

Based upon these helper functions multiple register read/write functions are implemented – they are trivial in their nature and are just the wrappers around _HMC_RegRead(…) and _HMC_RegWrite(…) functions. HMC_Reset(…) function, which we discussed earlier, uses register Read/Write functions implemented in HMCSPI_Sync.c code file to perform initialization/configuration of the sensor.

Project 12-HMCSPI-sync brings all of this functionality together in a small demo program that reads data from the sensor and submits it for logging over the Serial Data Logging interface. The data logged includes the timestamp (in microseconds) of the sample, X, Y, and Z values of the magnetic field, and sequential number of the sample. To parse this log I use the XMLconfiguration file for my utility discussed in SerialData Logging post. The timestamp value scaled down to seconds by using the FieldWeight=”0.000001” scale factor. Values for magnetic field are rounded to 3 digits after the decimal point.

This is the fragment of the Excel file that the log file is being converted to:
TS
MX
MY
MZ
Count    
5.017436
41.284
-219.266
322.936
1
5.021849
39.450
-223.853
322.936
2
5.026263
40.367
-222.018
324.771
3
5.030672
43.119
-220.183
323.853
4
5.035085
39.450
-220.183
322.936
5
5.039500
39.450
-221.101
322.936
6
5.043911
41.284
-222.018
324.771
7
5.048328
40.367
-222.018
321.101
8
5.052745
36.697
-221.101
323.853
9
5.057160
41.284
-223.853
326.605
10
5.061575
39.450
-222.018
328.440
11
5.065989
38.532
-221.101
325.688
12
5.070406
41.284
-222.018
322.018
13





At highest ODR HMC5983 provides measurements at a rate of about 220 Hz, which is roughly every 4.5 msec. Using synchronous read routines, which we discussed thus far, we would have to wait a few milliseconds whenever we need to read next measurement from the sensor – and this time would be purely wasted eating into the time allotted for the control loop. The answer to this problem – implement asynchronous communication with HMC5983 using PI24EP512GP806 interrupt system, which will be the topic of the next post.