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.

No comments:

Post a Comment