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.
Hi can you interface your hmc5983 with esp32. In a way it can send sensor data reader by the BLE esp32 client. And the information can then be published onto a webserver. Any help is appreciated.
ReplyDelete