STM32F0 Mini tutorial – Using the I2C peripheral to communicate with a HMC5883L digital compass IC

Well! I’ve had a fair few requests in the past few weeks about how I got the HMC5883L digital compass IC working with my STM32F0 discovery board (as shown in a previous post: https://hsel.co.uk/2014/05/29/hmc5883l-magnetometer/). I’m really not a fan of the I2C implementation on the STM32F0 discovery board as many things don’t seem particularly apparent at first, not forgetting that you have to configure the clock speed using an external calculator – Yuk! For future reference, the STM32F0 I2C speed calculator can be found under the name: STSW-STM32126. Such an easy to remember and descriptive name, I know! A link to the calculator (which may expire): http://goo.gl/Zeep7h.

So onwards with the tutorial! Other than certain parts, the I2C protocol consists of a lot of flag checking to check the current status of the bus. I2C is a really neat protocol that uses two bus lines (SDA – serial data and SCL – serial clock) in an open drain configuration and can address up to 127 devices on the same bus! As I2C is open drain, two pull up resistors are required. Due to this asymmetrical behaviour, an I2C signal will have a much larger rise time than fall time due to bus capacitance and the time taken for it to be charged by these pull up resistors. Certain devices allow for actively driven pull ups though I’ve not come across or used any of these devices yet. The standard bus speeds for I2C are 100kHz and 400kHz though much faster modes are supported by some devices (including the HMC5883L device!) of 3.4Mbps. The fastest sampling rate achievable by the HMC5883L is at 75Hz, meaning if you’re continuously streaming samples from the sensor to the microcontroller, you will require a bit rate of:

6bytes/packet
75 packets/sec
Total data rate: 6*75 = 450bytes/sec
Total bit rate: 450*8 = 3.6kbps

Remember however, the above value is not inclusive of any overhead such as returning the internal register pointer back to the first register, the X value.

Since we’re running at such a slow rate – 100kHz in my case, I’ve used 4.7k resistors for my pull ups. This was mainly because I had them to hand and they strike a good middle ground between power consumption and rise time, along with being a relatively low impedance. At 3.3v, with both SCL and SDA lines low, the maximum current consumption will be roughly 1.4mA (3.3v*2/4700) The STM32F0 is also utilizing the internal analog filter on the I2C lines to help filter any potential noise too in my implementation.

The HMC5883L is a pretty simple device and only has a handful of registers which are really easy to modify to your specification. I’m using a really simple setup with the sensor configured for most generic uses:

  • Sample averaging of 8 to eradicate noisy magnetic sources
  • Output rate of 75Hz (to demonstrate capability – of course 😉
  • No sensor bias
  • A midpoint gain of 440LSb/Gauss
  • Standard speed I2C
  • Continuous measurement mode

The data sheet (http://goo.gl/XMYQ31) is really good and gives a good description of what each value should do. The HMC5883L contains three main registers for setting the parameters within the chip, namely: Config register 1, config register 2 and mode register.

Config register 1 contains the settings for the sample averaging, the output rate and measurement configuration (if you require sensor bias).

Config register 2 contains the settings for the gain of the sensor.

The Mode register contains the settings for high speed I2C mode and the sensor mode.

Code:

Now! Lets get on with some coding. As per usual, we want to declare out top of file parts, the HMC5883 defines, included files and structs.

#include <stm32f0xx_gpio.h>
#include <stm32f0xx_rcc.h>
#include <stm32f0xx_i2c.h>

//HMC5883 declarations
#define HMC_SDA GPIO_Pin_11
#define HMC_SCL GPIO_Pin_10
#define HMC_GPIO GPIOB

#define HMC_SDA_PS GPIO_PinSource11
#define HMC_SCL_PS GPIO_PinSource10
#define HMC_PIN_AF GPIO_AF_1

#define HMC_I2C I2C2

//HMC5883 I2C address, don't ask my about the shift
//as it doesn't work without it!
#define HMCAddr (0x1E<<1)

//HMC5883 internal registers
#define R_Config1 0x00
#define R_Config2 0x01
#define R_Mode 0x02
#define R_Status 0x09
#define R_XRegister 0x03

//Time keeping variable!
volatile uint32_t MSec;

GPIO_InitTypeDef GP;
I2C_InitTypeDef IT;

As you can see, I’ve shifted the address up by 1. My only explanation for this is that the STM32F0 I2C peripheral automatically sets the read or write bit (LSB of the I2C transfer) depending on the value you send to the “TransferHandling” function. Either way, shifting up by 1 sorts the problem!

If you hadn’t got this vibe yet, I’m really not a fan of the STM32F0 I2C implementation as its nowhere near as simple as the arduino “Wire” library (yes, I wouldn’t expect it to be as this is bare m3t4l but they sure don’t make it as easy as the SPI or USART peripheral!).

So prepare yourself for the next two I2C functions. I have used an I2C function to write to registers and read from registers.

void I2C_WrReg(uint8_t Reg, uint8_t Val){

	//Wait until I2C isn't busy
	while(I2C_GetFlagStatus(HMC_I2C, I2C_FLAG_BUSY) == SET);

	//"Handle" a transfer - The STM32F0 series has a shocking
	//I2C interface... Regardless! Send the address of the HMC
	//sensor down the I2C Bus and generate a start saying we're
	//going to write one byte. I'll be completely honest,
	//the I2C peripheral doesn't make too much sense to me
	//and a lot of the code is from the Std peripheral library
	I2C_TransferHandling(HMC_I2C, HMCAddr, 1, I2C_Reload_Mode, I2C_Generate_Start_Write);

	//Ensure the transmit interrupted flag is set
	while(I2C_GetFlagStatus(HMC_I2C, I2C_FLAG_TXIS) == RESET);

	//Send the address of the register we wish to write to
	I2C_SendData(HMC_I2C, Reg);

	//Ensure that the transfer complete reload flag is
	//set, essentially a standard TC flag
	while(I2C_GetFlagStatus(HMC_I2C, I2C_FLAG_TCR) == RESET);

	//Now that the HMC5883L knows which register
	//we want to write to, send the address again
	//and ensure the I2C peripheral doesn't add
	//any start or stop conditions
	I2C_TransferHandling(HMC_I2C, HMCAddr, 1, I2C_AutoEnd_Mode, I2C_No_StartStop);

	//Again, wait until the transmit interrupted flag is set
	while(I2C_GetFlagStatus(HMC_I2C, I2C_FLAG_TXIS) == RESET);

	//Send the value you wish you write to the register
	I2C_SendData(HMC_I2C, Val);

	//Wait for the stop flag to be set indicating
	//a stop condition has been sent
	while(I2C_GetFlagStatus(HMC_I2C, I2C_FLAG_STOPF) == RESET);

	//Clear the stop flag for the next potential transfer
	I2C_ClearFlag(HMC_I2C, I2C_FLAG_STOPF);
}


//We're really fortunate with the HMC5883L as it automatically
//increments the internal register counter with every read
//so we need to set the internal register pointer to the first data
//register (the X value register) and just read the next 6 pieces
//of data, X1, X2, Z1, Z2 Y1, Y2 and
//voila! We have the compass values!
uint8_t I2C_RdReg(int8_t Reg, int8_t *Data, uint8_t DCnt){
	int8_t Cnt, SingleData = 0;

	//As per, ensure the I2C peripheral isn't busy!
	while(I2C_GetFlagStatus(HMC_I2C, I2C_FLAG_BUSY) == SET);

	//Again, start another tranfer using the "transfer handling"
	//function, the end bit being set in software this time
	//round, generate a start condition and indicate you will
	//be writing data to the HMC device.
	I2C_TransferHandling(HMC_I2C, HMCAddr, 1, I2C_SoftEnd_Mode, I2C_Generate_Start_Write);

	//Wait until the transmit interrupt status is set
	while(I2C_GetFlagStatus(HMC_I2C, I2C_FLAG_TXIS) == RESET);

	//Send the address of the register you wish to read
	I2C_SendData(HMC_I2C, (uint8_t)Reg);

	//Wait until transfer is complete!
	while(I2C_GetFlagStatus(HMC_I2C, I2C_FLAG_TC) == RESET);

	//As per, start another transfer, we want to read DCnt
	//amount of bytes. Generate a start condition and
	//indicate that we want to read.
	I2C_TransferHandling(HMC_I2C, HMCAddr, DCnt, I2C_AutoEnd_Mode, I2C_Generate_Start_Read);

	//Read in DCnt pieces of data
	for(Cnt = 0; Cnt<DCnt; Cnt++){
        //Wait until the RX register is full of luscious data!
        while(I2C_GetFlagStatus(HMC_I2C, I2C_FLAG_RXNE) == RESET); 
        //If we're only reading one byte, place that data direct into the 
        //SingleData variable. If we're reading more than 1 piece of data 
        //store in the array "Data" (a pointer from main) 		
        if(DCnt > 1) Data[Cnt] = I2C_ReceiveData(HMC_I2C);
	else SingleData = I2C_ReceiveData(HMC_I2C);
     }

     //Wait for the stop condition to be sent
     while(I2C_GetFlagStatus(HMC_I2C, I2C_FLAG_STOPF) == RESET);

     //Clear the stop flag for next transfers
     I2C_ClearFlag(HMC_I2C, I2C_FLAG_STOPF);

     //Return a single piece of data if DCnt was
     //less than 1, otherwise 0 will be returned.
	return SingleData;
     }
}

Sorry about the awful code formatting, the wordpress code block doesnt allow for very long lines! The code is on my github and included at the bottom anyway 🙂

I hope the code is pretty self explanatory in the commenting so I shall continue on. If you have any questions, just ask!

For time keeping, two pretty standard functions are used – the SysTick interrupt handler function which increments a millisecond variable and a delay function that operates the NOP instruction until x milliseconds have passed.

//Standard systick interrupt handler incrementing a variable named
//MSec (Milliseconds)
void SysTick_Handler(void){
	MSec++;
}

//Standard delay function as described in one
//of my previous tutorials!
//All it does is operate a nop instruction
//until "Time" amount of milliseconds has passed.
void Delay(uint32_t Time){
	volatile uint32_t MSStart = MSec;
	while((MSec-MSStart)<Time) asm volatile("nop");
}

As with standard code structure, next comes main! The main initializes all the hardware components include the GPIO and I2C interfaces. It also sets up the SysTick interrupt handler at 1ms.

Constant variables are used to store the register configurations and hopefully the names are pretty self explanatory! The registers are written to, then a small delay is present before the reading on the device occurs. The HMC5883L is polled through I2C for the data to be ready. Once this data is ready, 6 bytes are read and stored in their corresponding variables, namely XVal, YVal and ZVal.

 

//The main!
int main(void)
{
	//Setup a systick interrupt every 1ms (1/1000 seconds)
	SysTick_Config(SystemCoreClock/1000);
	//Enable GPIOB clock, required for the I2C output
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE);
	//Enable the I2C peripheral clock
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);

	//Set the pins HMC_SDA and HMC_SCL as alternate function GPIO pins
	GP.GPIO_Pin = HMC_SDA | HMC_SCL;
	GP.GPIO_Mode = GPIO_Mode_AF;
	GP.GPIO_OType = GPIO_OType_OD;
	GP.GPIO_Speed = GPIO_Speed_Level_1;
	GPIO_Init(HMC_GPIO, &GP);

	//Configure the pins to the I2C AF
	GPIO_PinAFConfig(HMC_GPIO, HMC_SDA_PS, HMC_PIN_AF);
	GPIO_PinAFConfig(HMC_GPIO, HMC_SCL_PS, HMC_PIN_AF);

	//Setup the I2C struct. The timing variable is acquired
	//from the STM32F0 I2C timing calculator sheet. Pretty
	//standard stuff really, its using the Analog filter
	//to clean up any noisy edges (not really required though
	//if you wish to disable it, you will need a different
	//I2C_Timing value).
	IT.I2C_Ack = I2C_Ack_Enable;
	IT.I2C_AnalogFilter = I2C_AnalogFilter_Enable;
	IT.I2C_DigitalFilter = 0;
	IT.I2C_Mode = I2C_Mode_I2C;
	IT.I2C_OwnAddress1 = 0xAB;
	IT.I2C_Timing = 0x10805E89;
	IT.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	I2C_Init(HMC_I2C, &IT);
	I2C_Cmd(HMC_I2C, ENABLE);

	//Write the configuration registers for the HMC5883L compass.
	const uint8_t SmplAverage = 0b11; //Average 8 samples
	const uint8_t OutputRate = 0b110; //Output at 75Hz (Why not!)
	const uint8_t MeasurementCfg = 0b00; //We don't want any bias on the sensors
	const uint8_t SnsrGain = 0b100; //A gain of 440LSb/Gauss, standard.
	const uint8_t HighSpeedI2C = 0; //We want standard speed I2C.
	const uint8_t SnsrMode = 0b00; //Continuous measurement mode

	//Config 1 register
	I2C_WrReg(R_Config1, (SmplAverage<<5)|(OutputRate<<3)|MeasurementCfg);
	//Config 2 register
	I2C_WrReg(R_Config2, (SnsrGain<<5));
	//Mode register
	I2C_WrReg(R_Mode, (HighSpeedI2C<<7)|SnsrMode);

	//Give the sensor a breather before reading data!
	Delay(10);

	int8_t RecData[6] = {0,0,0,0,0,0};
	int32_t XVal = 0, YVal = 0, ZVal = 0;

	while(1)
	{
		//Read status register, waiting for the data ready flag
		while(!(I2C_RdReg(R_Status, 0, 1) & 1));

		//Receive 6 bytes of data from the value registers and store in
		//RecData variable
		I2C_RdReg(R_XRegister, &RecData[0], 6);

		//Construct the words containing the value data!
		//The HMC5883 is a little odd as it arranges its registers
		//in the order X,Z,Y as opposed to what I suppose you'd
		//expect from the normal order X,Y,Z.
		XVal = (RecData[0]<<8) | RecData[1];
		ZVal = (RecData[2]<<8) | RecData[3];
		YVal = (RecData[4]<<8) | RecData[5];
	}
}

Once again, sorry for the horrible code formatting!

To test whether the sensor was actually working, I used the standard debug mode within Coocox. this mode can be easily activated using the debug icon DebugIcon.

Using the debug mode, I can read the values stored in XVal, YVal and ZVal. By rotating my breadboard with the sensor on, I can view the change in these values and determine the position of local magnetic fields!

Making:

Talking of breadboards, the construction sis really simple requiring few wires, two resistors and I’ve included a 4.7uF capacitor for safe keeping!

P1020451
The initial configuration using the two 4.7k resistors connected to the 3v positive rail.

P1020452
Hopefully a better view of the two 4.7k resistors! They’re connected to PB10 (SCL) and PB11 (SDA)

P1020453
And again!

P1020455
Next, I added the power wires for the HMC5883L sensor. not the best view but its the standard stuff – 3v to 3v, 0v to 0v.

P1020456
And finally, slot the chip in! You can see the 4.7uF power supply smoothing cap too.

Testing:

As stated previously, I did all my testing using the debug feature in Coocox. Its really simple to use, you literally just click the debug icon, wait for it to do its stuff, set a breakpoint and monitor your variables! So in simple steps:

Step 1: Press the debug button and you shall be transported to the magical realm of debugging.
Debug1
The initial debugging screen once the debug icon has been pressed.

Debug2
Once the debug initialization is complete, you will be met with this screen, now to step 2!

Step 2: Set a breakpoint on the bracket after YVal = (RecData[4]<<8) | RecData[5];. This will ensure that all three values are read before the program pauses.

Debug3
Adding a breakpoint is really easy! All you need to do is double click in the light blue region on the left until a little red blob appears. This is your breakpoint. All a breakpoint does is pause the program (or “break” the program) at this point until you then press play again.

Step 3: Now a breakpoint has been set, you can actually start reading in values! If you look down at the bottom right of the screen, you can see the variable names, namely XVal, YVal and ZVal. As you can expect, these are the X, Y and Z values reported by the sensor! The reason RecData is a 32bit hex number is because it is the memory address at which the RecData array starts. To do your first read, press the “Play” button.

Debug4
As you can see, the little red box is the play button! Pressing this will update the data in the bottom right box. To exit debugging mode, press the stop button, two buttons to the right of the play (the bright red square!).

The code can be found on github. Hopefully my commenting is sufficient and will help you develop this HMC5883L sensor into your own project!

https://github.com/pyrohaz/STM32F0-HMC5883_Barebones

If you have any questions, just drop me a comment and I’ll answer asap!

Advertisements

17 responses to “STM32F0 Mini tutorial – Using the I2C peripheral to communicate with a HMC5883L digital compass IC

  1. Just a comment on the shift of the address (#define HMCAddr (0x1E<<1)).
    It explains itself when you look into the reference manual of ST (RM0091 page 647). For 7 bit adressing the bits 1:7 are used for 10 bit addressing the bits 0:9 are used.

    Since you are using 7 bit addressing you need to put the adress 0x1E in bits 1:7 which you do by shifting it one to the left.

    Other than that, thanks for the mini tutorial on I²C

  2. Hello Harris,

    nice work, I also googled around for some I2c examples for the stm32f030 and didn’t find a decent one but your example looks very promising. I tried to use it (not with the HMC5883L but with an Aardvark I2c debugger and an oscilloscope as “slave”). But up to now I don’t see any I2C communication. I am stuck in the I2C_WrReg function, the follwowing condition is always true:
    while(I2C_GetFlagStatus(HMC_I2C, I2C_FLAG_TXIS) == RESET);
    I guess that’s not related to my “wrong” slave interface, any idea where I could start looking?

    best
    Matthias

    • Hey buddy,

      I’ve never actually managed to get the STM32F0xx I2C to work without a device because it won’t go through the steps of having flags cleared etc. and won’t be able to continue the program.You do mention though that your oscilloscope is set up as a slave so I assume that it automatically responds to the corresponding I2C commands.

      I actually just modified this code from the standard peripheral examples so I’m not particularly experienced with I2C. I think there is an updated standard peripheral library so maybe that might be able to help? The documentation on this I2C isn’t brilliant…

      Sorry I couldn’t be of more help!

  3. Hi,
    Thanks for this nice tutorial .

    I think 0x1E <<1 is done as device address send in a I2C protocol is 7bit followed by Read and Write mode bit .

    where 1E I assume is address of Magnetometer …….data sheet of magnetometer requires 0x3C and 0x3D for read and write simultaneously for the same reason

    A nice tutorial by Silicon labs is as below explaining same .

    http://blog.siliconlabs.com/t5/Official-Blog-of-Silicon-Labs/Chapter-10-2-Control-an-accelerometer-over-I2C-Part-2-Initialize/ba-p/164580

    Thanks,
    Chinmay

  4. Hi,
    Just a couple of things. I just want to confirm this is set up with interrupt mode right? If so do you know if it’s possible to set up the f0 with polling mode?
    The reason behind this question is that I am trying to use i2c to communicate between a f7 (master) and f0 (slave) and have already set up the f7 to communicate in polling and tested that it worked.

    • Hi Nicholas,

      This tutorial uses polling mode on the F0 side and would require some further code changes and a state machine for interrupt driven functionality.

      Cheers,

  5. Do you know if the I2C driver in STMF0 peripheral library supports interrupt mode? I would like to use an interrupt driven mechanism if possible. I’m not sure how much CPU load this polling approach induces for frequent low speed transfers. Did you measure this? Thanks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s