My experiments with U-Center and the GPS module had shown that the direction indication was not very reliable. Following a link in Kevin Saye's post, I discovered a command that should tell the orientation. But my device did not seem to be generating that.
Reading a bit further, I realised that the compass feature was provided by a HMC5883HMC5883 connected over I2C on a second connector. So I soldered a green wire onto the SCL connector and a blue wire to the SDA. I did a test with a sample from instructables and an Arduino Uno (using 2x10k resistors pulled up to the 3.3v line) and got some readings from the sensor. Checking the HMC5883 datasheet later 2K2 resistors were recommended so I'll reduce my values if there are any issues.
I realised from looking at this code that there is a magnetic deviation for compasses and really my project should compensate for that based on the current position. Perhaps another Azure Function is the best approach for that as it allows for adjustments over time.
So a bit of a problem with connecting the I2C to the Azure Sphere, the MS team have yet to support I2C in their SDK. I'd come across chips before that did not support I2C and knew that the protocol could be implemented in software with a technique called “bitbanging” i.e. you toggle a GPIO to have the desired behaviour. AlaskaResearchCubeSat had a great example bitbang I2C which looked straight forward to port.
I2C Hardware
As mentioned the MT3620 does support I2C, it has three comms modules that can be configured for UART, SPI or I2C. However this is not yet available via the API so I needed to look at the GPIO. For the board to be able to communicate with I2C it needs to be able send and receive on two pins. I did think about using 4 pins wired in pairs but it does not seem that is necessary.
The I2C protocol works by having pull up resistors and each of the devices pulls down the line to signal a zero. The output is left floating when the MCU indicates "high", that means that other devices could pull that line low still. The Azure Sphere seems to support this with the open_drain setting for GPIO_OpenAsOutput.
It also supports reading the pin whilst in this state so hence we can implement a software/bitbanged I2C.
Porting to Azure Sphere
My first pass at porting the Alaska bitbang version is as follows:
i2cbb.h
#pragma once //I2CBus typedef struct { GPIO_Id scl; int sclFd; GPIO_Id sda; int sdaFd; } i2cbus_t; //Function prototypes void i2c_bb_setup(i2cbus_t* bus); short i2c_bb_tx(i2cbus_t* bus, unsigned char addr, const unsigned char *dat, unsigned short len); short i2c_bb_rx(i2cbus_t* bus, unsigned char addr, unsigned char *dest, unsigned short len);
i2cbb.c
#include <applibs/gpio.h> #include <applibs/log.h> #include <stdbool.h> #include <errno.h> #include <string.h> #include <time.h> #include "i2cbb.h" #define BIT0 1 //setup bit bang I2C pins void i2c_bb_setup(i2cbus_t* bus) { //set pins as outputs opendrain bus->sclFd = GPIO_OpenAsOutput(bus->scl, GPIO_OutputMode_OpenDrain, GPIO_Value_High); bus->sdaFd = GPIO_OpenAsOutput(bus->sda, GPIO_OutputMode_OpenDrain, GPIO_Value_High); } //wait for 1/2 of I2C clock period void i2c_bb_hc(void) { const struct timespec clockhalftime = { 0, 50000 }; //wait for 0.05ms nanosleep(&clockhalftime, NULL); } int setSDA(i2cbus_t* bus, GPIO_Value_Type val) { int result = GPIO_SetValue(bus->sdaFd, val); if (result != 0) { Log_Debug("ERROR: Could not set SDA output value: %s (%d).\n", strerror(errno), errno); return -1; } return 0; } int setSCL(i2cbus_t* bus, GPIO_Value_Type val) { int result = GPIO_SetValue(bus->sclFd, val); if (result != 0) { Log_Debug("ERROR: Could not set SCL output value: %s (%d).\n", strerror(errno), errno); return -1; } return 0; } unsigned char getSDA(i2cbus_t* bus) { GPIO_Value_Type val; int result = GPIO_GetValue(bus->sdaFd, &val); if (result != 0) { Log_Debug("ERROR: Could not read SDA value: %s (%d).\n", strerror(errno), errno); } return val == GPIO_Value_High ? 1 : 0; } void i2c_bb_start(i2cbus_t* bus) { //wait for 1/2 clock first i2c_bb_hc(); //pull SDA low setSDA(bus, GPIO_Value_Low); //wait for 1/2 clock for end of start i2c_bb_hc(); } void i2c_bb_stop(i2cbus_t* bus) { //pull SDA low setSDA(bus, GPIO_Value_Low); //wait for 1/2 clock for end of start i2c_bb_hc(); //float SCL setSCL(bus, GPIO_Value_High); //wait for 1/2 clock i2c_bb_hc(); //float SDA setSDA(bus, GPIO_Value_High); //wait for 1/2 clock i2c_bb_hc(); } //send value over I2C return 1 if slave ACKed short i2c_bb_tx_byte(i2cbus_t* bus, unsigned char val) { int i; //shift out bits for (i = 0;i < 8;i++) { //pull SCL low setSCL(bus, GPIO_Value_Low); //check bit if (val & 0x80) { //float SDA setSDA(bus, GPIO_Value_High); } else { //pull SDA low setSDA(bus, GPIO_Value_Low); } //shift val <<= 1; //wait for 1/2 clock i2c_bb_hc(); //float SCL setSCL(bus, GPIO_Value_High); //wait for 1/2 clock i2c_bb_hc(); } //check ack bit //pull SCL low setSCL(bus, GPIO_Value_Low); //float SDA setSDA(bus, GPIO_Value_High); //wait for 1/2 clock i2c_bb_hc(); //float SCL setSCL(bus, GPIO_Value_High); //wait for 1/2 clock i2c_bb_hc(); //sample SDA val = getSDA(bus); //pull SCL low setSCL(bus, GPIO_Value_Low); //return sampled value return !val; } //send value over I2C return 1 if slave ACKed unsigned char i2c_bb_rx_byte(i2cbus_t* bus, unsigned short ack) { unsigned char val; int i; //shift out bits for (i = 0;i < 8;i++) { //pull SCL low setSCL(bus, GPIO_Value_Low); //wait for 1/2 clock i2c_bb_hc(); //float SCL setSCL(bus, GPIO_Value_High); //wait for 1/2 clock i2c_bb_hc(); //shift value to make room val <<= 1; //sample data unsigned char readbit; readbit = getSDA(bus); if (readbit == GPIO_Value_High) { val |= 1; } } //check ack bit //pull SCL low setSCL(bus, GPIO_Value_Low); //check if we are ACKing this byte if (ack) { //pull SDA low for ACK setSDA(bus, GPIO_Value_Low); } else { //float SDA for NACK setSDA(bus, GPIO_Value_High); } //wait for 1/2 clock i2c_bb_hc(); //float SCL setSCL(bus, GPIO_Value_High); //wait for 1/2 clock i2c_bb_hc(); //pull SCL low setSCL(bus, GPIO_Value_Low); //float SDA setSDA(bus, GPIO_Value_High); //return value return val; } short i2c_bb_tx(i2cbus_t* bus, unsigned char addr, const unsigned char *dat, unsigned short len) { short ack; int i; //send start i2c_bb_start(bus); //send address with W bit ack = i2c_bb_tx_byte(bus, (addr << 1)); //send data bytes for (i = 0;i < len && ack;i++) { //transmit next byte ack = i2c_bb_tx_byte(bus, dat[i]); } //transmit stop i2c_bb_stop(bus); //return if slave NACKed return ack; } short i2c_bb_rx(i2cbus_t* bus, unsigned char addr, unsigned char *dest, unsigned short len) { int i; //send start i2c_bb_start(bus); //send address with R bit if (!i2c_bb_tx_byte(bus, (addr << 1) | BIT0)) { //got NACK return error return 0; } //receive data bytes for (i = 0;i < len;i++) { //receive next byte dest[i] = i2c_bb_rx_byte(bus, i == len - 1); } //transmit stop i2c_bb_stop(bus); //return if slave NACKed return 1; }
This still needs testing but it does compile, the identification registers seem good candidates for the read testing as they have fixed values. As is common with most I2C devices the "read" register is selected by sending a single byte with no data.
"To move the address pointer to a random register location, first issue a “write” to that register location with no data byte following the command. For example, to move the address pointer to register 10, send 0x3C 0x0A."
From Honeywell HMC5883L datasheet.
Note that the output is in the form of a vector so that needs converting to a bearing using the X and Y values and Arctan function. The AdaFruit library used by the Instructables article has a good example of that. I'll like wrap the call to the compass up with a separate function so you don't need to see the low level I2C functionality. I quite like the AdaFruit universal sensor approach but that seems a bit overkill to copy here.
Unfortunately I've got a work trip coming up next week so there will be a pause in the project. Knowing my luck the SDK team will have enabled the I2C functionality before I've had a chance to finish this port.
Further reference
Embedded System I2C Tutorial - Embedded Systems Learning Academy
https://courses.engr.illinois.edu/ece437/fa2018/ref/I2C_Serial_Protocol_Tutorial.pdf
Top Comments