I2C use on the STM32F103R Medium Density Devices

I2C or Inter Integrated Circuit ( sometimes just referred to as just two-wire interface ) is a great method of individually communicating with over 100 devices on only 2 wires. However I2C as a whole requires a reasonable amount of protocol overhead so its not the fastest method of communicating with outside peripherals. But it is very good at reducing wiring on space critical designs due to the fact that only 2 data lines are required to enter the processor.

The principles of operation for I2C are relatively simple and is described well here :

http://www.robot-electronics.co.uk/acatalog/I2C_Tutorial.html

Its recommended that unless your familiar with I2C to read the above page before continuing.

Assuming your were or are now are familiar with I2C you will know the basic protocol for reading an I2C device is as follows:

  1. Send a start sequence
  2. Send left hand aligned device address with LSB low – Write mode
  3. Send the address of Internal register of the device – this doesn’t need to be left aligned
  4. Send a start sequence again – called a repeated start
  5. Send left hand aligned device address with LSB high – Read mode
  6. Clock in the data from the device
  7. Send the stop sequence.

Simple right? Well from my experience nothing is ever simple on the STM32 so I will go over how I set mine up and hopefully give you an insight as to setting up your own peripheral.

When I was learning how to use this peripheral I was trying to communicate with a HMC5843 so this is what I will use for the example. Allot of things differ between different I2C devices so the method I used to communicate with the HMC5843 may differ to that of another I2C but the principle stays the same. I will try to cover all bases so you can modify the code to suit your needs.

The HMC5843 is a three axis compass/magnetometer made by Honeywell and available from Sparkfun:

http://www.sparkfun.com/products/9371

Addressing

This section is mainly for those of you who skimmed the over tutorial link , if you truly are confident with the principles of operation then skip this part.

All addresses sent to an I2C device are 8bit, some devices including the STM32 support a 10bit addressing method, but this wont be covered here. All addresses for read/write requests sent to I2C devices must be left hand aligned thus allowing the right most bit or LSB free to be used as a directional identifier.

For example the HMC3843 has an I2C address of 0x1E or 0b00011110 but if we want to send a request to read from our device we have to send it the address 0x3D or 0b00111101 and to send it a request to write we send it 0x3C or 0b00111100. Note the last bit is cleared in the write request and set in the read request.

To my knowledge all I2C device addresses are fixed to that specific device and cannot be changed so check before buying more than one to go on a single line.

Wiring in the device

Again if you know I2C well skip this section, otherwise read on.

The STM32 doesn’t push and pull the communication lines high and low but instead just pulls them down and releases them, the same goes for all other I2C controllers. Because of this 2 pull-up resistors are required to bring the lines back up to VCC once the STM32 releases them. I personally used 5Kohm resisters on both lines which worked well at the speeds I was reading at.

Setting up the I2C peripheral

The I2C peripheral on the STM32 is relatively simple to configure compared to many of its other peripherals. Most of it can be figured out just from reading the STM libraries but the most important things to consider are:

That you have your two pins GPIOB 6 & 7 which are your SCL & SDA lines configured as alternate function open drain so they can pull down the lines.

The speed must relatively slow, in my example I have the speed set to 50khz but I2C will run at much higher speeds. Also seeing as the master (the stm32) controls the clock you don’t have to worry about matching it to the slave device as it will just follow the clock pulses regardless. Providing it can keep up of course..

The Own address confused me for a while but it turns out that before the STM32 sends its start sequence it sits listening to the I2C lines waiting for its address, the address you set here. I wont be covering how to set the STM32 as a I2C slave so this will just be left as 0x00.

This part will be carried out within the “ I2C_Setup“ function outlined below.

Writing to registers

Most of the time before you can get anything useful out of your I2C device you will need to write to it first to tell it what you want it to do. The HMC5843 is no exception and you need to write a 0x00 to register 0x02 to make it go into continuous conversion mode. This then allows you to just pull the data from the device as and when. The obvious drawback of this is an increase in power consumption.

This is done in the exact way the tutorial outlines:

  1. Send a start sequence
  2. Send 0x3C – this is the 0x1E address but left-hand aligned
  3. Send 0x02 – one of the internal configuration registers
  4. Send 0x00 – Clear the bits within the register
  5. Send the stop sequence.

This part will be carried out within the Init_sensor() function outlined below

Reading registers

All EEPROM devices have a set of registers in which to store their configuration data and sensor data. In the case of the HMC5843 it has a total of 12 registers. For now we only care about the ones we want to read from which are 0x03 to 0x08. These registers store all of the converted data from the sensors in 8 bit chunks. Seeing as I2C can only deal with 8 bits of data at a time we will need to read all of the registers and stick them together to form useful data. This will become more clearer later in the code.

The procedure outlined above for reading an I2C device is a generic way of reading a single register into your program. However allot of devices, this one included have an internal address pointer which increments every-time a register is read. This is good as it means we can read the device with less protocol overhead and therefore much faster.

This part can be the most tricky overall as its very device dependant it took me a wile to realise the HMC5843 required a NACK bit to be sent at the end of reading before it would allow for a new set of data to be reloaded. Be aware when reading from other devices that the code I give will most likely not work out of the box.

This part will be carried out within the “Receive()” function outlined below.

The Code

The code relies entirely on the STM32 libraries and at the time of writing they were at version 3.5.0

Also note the code has 2 defines:


#define I2C1_SLAVE_ADDRESS7 0x1E //address for magnetometer
#define I2C_SPEED 50000 //50Khz speed for I2C


void I2C_Setup(void)
{

    GPIO_InitTypeDef  GPIO_InitStructure;
    I2C_InitTypeDef  I2C_InitStructure;

    /*enable I2C*/
    I2C_Cmd(I2C1,ENABLE);

    /* I2C1 clock enable */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);

    /* I2C1 SDA and SCL configuration */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    /*SCL is pin06 and SDA is pin 07 for I2C1*/

    /* I2C1 configuration */
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStructure.I2C_OwnAddress1 = 0x00;
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_InitStructure.I2C_ClockSpeed = I2C_SPEED ;
    I2C_Init(I2C1, &I2C_InitStructure);

}


void init_sensor(void)
{

    /* initiate start sequence */
    I2C_GenerateSTART(I2C1, ENABLE);
    /* check start bit flag */
    while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_SB));
    /*send write command to chip*/
    I2C_Send7bitAddress(I2C1, I2C1_SLAVE_ADDRESS7<<1, I2C_Direction_Transmitter);
    /*check master is now in Tx mode*/
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
    /*mode register address*/
    I2C_SendData(I2C1, 0x02);
    /*wait for byte send to complete*/
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    /*clear bits*/
    I2C_SendData(I2C1, 0x00);
    /*wait for byte send to complete*/
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    /*generate stop*/
    I2C_GenerateSTOP(I2C1, ENABLE);
    /*stop bit flag*/
    while(I2C_GetFlagStatus(I2C1, I2C_FLAG_STOPF));

}


void Receive(u8 Address, u8 Register)
{
    u8 XMSB,XLSB,YMSB,YLSB,ZMSB,ZLSB; /* variables to store temporary values in */

    /*left align address*/
    Address = Address<<1;
    /*re-enable ACK bit incase it was disabled last call*/
    I2C_AcknowledgeConfig(I2C1, ENABLE);
    /* Test on BUSY Flag */
    while (I2C_GetFlagStatus(I2C1,I2C_FLAG_BUSY));
    /* Enable the I2C peripheral */
/*======================================================*/
    I2C_GenerateSTART(I2C1, ENABLE);
    /* Test on start flag */
    while (!I2C_GetFlagStatus(I2C1,I2C_FLAG_SB));
    /* Send device address for write */
    I2C_Send7bitAddress(I2C1, Address, I2C_Direction_Transmitter);
    /* Test on master Flag */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
    /* Send the device's internal address to write to */
    I2C_SendData(I2C1,Register);
    /* Test on TXE FLag (data sent) */
    while (!I2C_GetFlagStatus(I2C1,I2C_FLAG_TXE));
/*=====================================================*/
      /* Send START condition a second time (Re-Start) */
    I2C_GenerateSTART(I2C1, ENABLE);
    /* Test start flag */
    while (!I2C_GetFlagStatus(I2C1,I2C_FLAG_SB));
    /* Send address for read */
    I2C_Send7bitAddress(I2C1, Address, I2C_Direction_Receiver);
    /* Test Receive mode Flag */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
    /* load in all 6 registers */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
    XMSB = I2C_ReceiveData(I2C1);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
    XLSB = I2C_ReceiveData(I2C1);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
    YMSB = I2C_ReceiveData(I2C1);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
    YLSB = I2C_ReceiveData(I2C1);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
    ZMSB = I2C_ReceiveData(I2C1);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
    ZLSB = I2C_ReceiveData(I2C1);
    
    /*enable NACK bit */
    I2C_NACKPositionConfig(I2C1, I2C_NACKPosition_Current);
    I2C_AcknowledgeConfig(I2C1, DISABLE);
    
    /* Send STOP Condition */
    I2C_GenerateSTOP(I2C1, ENABLE);
    while(I2C_GetFlagStatus(I2C1, I2C_FLAG_STOPF)); // stop bit flag
    
    /*sort into 3 global variables*/
    X = ((XMSB<<8) | XLSB);
    Y = ((YMSB<<8) | YLSB);
    Z = ((ZMSB<<8) | ZLSB);

}

So in my case the function will be called as Receive(0x1E,0x03); because the device address is 0x1E and the beginning variable register is 0x03.

You should also be able to see for the most part how the original sequence for reading an I2C device is followed in the code.

The Receive function shown above will read all 6 registers from the HMC5843 and combine them into 3 global variables. Because its a 3 axis magnetometer it stores each individual axis into 2 registers so its necessary to read a minimum of 2 registers to get 1 axis of measurement.

As mentioned earlier the magnetometer has an inbuilt address pointer that will increment through all the variable registers then reset back to position 3. This means that the code in-between the equals signs can in-fact be completely removed and you will still read all the registers the same way.

I left the write part in to show how you can read specific registers in certain I2C devices without having to roll through all of the registers in turn.

This entry was posted in Micromouse. Bookmark the permalink.

20 Responses to I2C use on the STM32F103R Medium Density Devices

  1. Darren Cawthorne says:

     

    Some further things to note i should have probably included are that its very easy to get the sensor to lock up. if you dont clock out all of the data or do something its not expecting then it will just hold onto the SDA line untill its happy or its data is clocked out.
    As you would have noticed the code is full of While tests, these are necessary to stop the processor from blitzing through its execution and not waiting for the sensor to respond. However if the sensor holds onto the SDA line then the stm32 will just sit within its while loop forever more. including a timeout facility within these loops that returns an error condition after a wile would be a good idea.
    To clear the sensor you can simply unpower it or maybe include some code in the setup function to clock out any remaining data and then generate a stop condition.

    Some further things to note that I should have probably included are that its very easy to get the sensor to lock up. if you dont clock out all of the data or do something its not expecting then it will just hold onto the SDA line untill its happy or its data is clocked out.
    As you would have noticed the code is full of While tests, these are necessary to stop the processor from blitzing through its execution and not waiting for the sensor to respond. However if the sensor holds onto the SDA line then the stm32 will just sit within its while loop forever more. including a timeout facility within these loops that returns an error condition after a wile would be a good idea.
    To clear the sensor you can simply unpower it or maybe include some code in the setup function to clock out any remaining data and then generate a stop condition.

  2. Hamed says:

    It is very useful, are you using gyro in Decimus 2?

  3. peteh says:

    There is an ASDXRS610 mounted on this mouse. It is used for all the turns.

  4. Marcos says:

    Where’s the code ?

  5. Peter Harrison says:

    Sorry. Don’t know what happened there. Is that fixed for you now?

  6. Dave Fletcher says:

    Your read address appears to be wrong, under (restart). Or I’m missing where you add or OR with 1.

  7. Dave Fletcher says:

    You may also want to consider adding:-
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // Enable GPIOB Clock

  8. Dave Fletcher says:

    There is also a missing write sequence (Data to Reg) below line 82.

  9. Darren Cawthorne says:

    Dave,

    On line 65 I left hand align the address and the function “I2C_Send7bitAddress(I2C1, Address, I2C_Direction_Transmitter);” will set the LSB bit to 0 or 1 depending on the last argument which in this case is set to transmitter. This function is part of the standard STM I2C libraries.

    Yes good point I should have added the line to turn on the GPIOB clock, I shall amend this.

    On line 82 the code isn’t meant to write anything to the registers it is simply meant to tell the sensor which register I wish to read. The 2nd function “init_Sensor()” includes code on how to actually modify the register contents.

  10. Dave Fletcher says:

    Darren,

    Thanks for the feedback, I wasnt aware of the first point thanks for pointing it out. (I should have read the library). I still dont understand why I made the third point, I must have been confused between read and write.

    Regards,
    Dave.

  11. avcs says:

    what is “Register” @ line 80?

  12. Darren Cawthorne says:

    Sorry that was a defined value that i didn’t include in the code. It is the address of the register in which you wish to read from which in this case is 0x03 the most significant bit of the X axis. The rest of the registers containing the axis data are then stored directly after this and the device increments the address each time it is read. Therefore you need only state the register address once then clock out the rest.

  13. avcs says:

    thanks, it’s my bad; my question was stupid. “Register” is just one of the input to the function Receive(), I somehow slipped there. Anyways my concern is that “while(I2C_GetFlagStatus(I2C1, I2C_FLAG_STOPF));” after generating STOP command, is it required? This is for stop detection in Slave mode!!

  14. Mumin YILDIRIM says:

    Respect++
    As beginners, it is hard to use the standard pheripheral libraries by ourselves alone. Someone has to guide because library comments are unclear and there is not enough documentation.
    Thanks a lot 🙂
    With a little modification, it works for HMC6352
    Are there any ‘detailed’ datasheets for STM32F10x series? What’s on the STM web site seem superficial.

  15. Palthiya Maanu says:

    #define I2C1_SLAVE_ADDRESS7 0x1E //address for magnetometer. What about Accelerometer Address

  16. Darren Cawthorne says:

    I cant honestly remember the exact address of the accelerator i was using then but all devices are different anyway. Just call “void Receive(u8 Address, u8 Register)” giving the address of the device you are using which you can find in its datasheet.

  17. Arctangent says:

    Is it necessary to call this function receive in a timer? Or it’s default is at continuous conversion mode. And how could you read all these values from one address?

  18. Sachin Singh says:

    tried this code on STM32VLDiscovery STM32F100RB but it does not generate a start flag
    while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_SB));
    code hangs here

  19. Peter Harrison says:

    Sorry – this is not my code so I am unable to help. Perhaps Darren will see this.

  20. Darren Cawthorne says:

    Wow now your testing me! I haven’t looked at this code in many years.

    The purpose of testing for the start flag is simply to stall the core long enough to give the peripheral time to actually do it.

    If the flag is not being set then the start action is probably not being generated on the line properly, have you witnessed it happening on a scope or analyser?

    if its not being generated then my guess is one of two things:

    1> The peripheral simply isn’t setup correctly, the code I posted is obviously for a different processor but the peripheral ‘should’ still setup correctly. However if that peripheral comes out on a different set of pins then it will need to be changed. without digging into the datasheet I wouldn’t know.

    2> Following onto the pin setup if your hardware is not correct such that the pins are being pulled up too strongly or shorted hi then the peripheral may detect that it cannot drop both of the lines in order to generate the start flag. Its worth setting up a small alternate pin wiggle loop and checking on a scope that both of the lines are going up and down as expected. Id suggest removing the target I2C device for this as it may get the wrong idea and start clock stretching your CLK line.

    2.1> better still for this test write something like :

    while (1)

    {
    I2C_GenerateSTART(I2C1, ENABLE);

    DelayMs(1) ;

    I2C_GenerateSTOP(I2C1, ENABLE);
    }

    The I2C stop bit pattern is a mirror of the start bit so it should be easy to spot them both on a scope or better still a logic analyser.

    Also sorry for the delayed response, I read soon as it arrived then Christmas got in the way!

    Hope this helps,

    Darren.

Leave a Reply