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.

1 comment:

  1. 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