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.