3 horizontal lines, burger
3 horizontal lines, burger
3 horizontal lines, burger
3 horizontal lines, burger

3 horizontal lines, burger
Remove all
LOADING ...

Content



    I2C interface for ATmega328 (No libs, just C and registers). Using the MPU6050 accelerometer as an example.

    Clock
    11.12.2025
    /
    Clock
    11.12.2025
    /
    Clock
    25 minutes
    An eye
    179
    Hearts
    0
    Connected dots
    0
    Connected dots
    0
    Connected dots
    0

    What it's about and who it's for

    In this article, I'm going to write about I2C (IIC / 2Wire), a communication interface between chips and sensors on boards.
    I2C (IIC - Inter-Integrated Circuit) is a two-wire serial interface that allows sensors, microcontrollers, and other devices to communicate with each other.
    It's very simple to implement. When developing circuits, only two wires (lines) are required: a clock line (SCL) and a data line (SDA). However, the number of supported "slaves" is limited to 127, and the transfer speed is quite low compared to, say, SPI.
    The maximum transfer speed depends specifically on the devices that use this interface, but the standard is 100 and 400 kHz. Some devices support higher speeds, such as Fast Mode Plus (Fm+): up to 1 MHz (1000 kHz) and High-Speed ​​Mode (Hs-Mode): up to 3.4 MHz (3400 kHz).
    I'd like to understand how this interface works because I need it for my project. I can't say anything about it yet, because I might not even be able to implement it. There are so many variables to consider and a lot to learn.
    But here's the crux of the matter. I have the chip I need, the LIS2DH12TR (accelerometer). There aren't many tutorials for it, really. There's only a datasheet and a register map for it. Or rather, there's one library, but it's for the LIS2DH12TR chip. My project is in pure C, so there isn't one. And I don't plan on rewriting the library.
    Besides, I might want to use a different chip if this one doesn't work, and there might not be anything for it. So, it was decided to figure out the I2C interface at all costs and communicate with the target chip without any libraries. For now, we'll use the MPU6050 and ATmega328P as an example.
    This article isn't about the Wire.h or I2Cdev.h libraries. This article is about registers, simple bitwise math, and the logic behind communicating with peripherals via the I2C interface.
    The goal is to understand how it all works and put it into practice by reading and writing a couple of bytes of information to a couple of registers. Let's get started.

    About I2C in General, Theory

    And I'll start, of course, with the theory. I'll discuss the format of the packets being sent. How these packets move between devices, how these devices understand what to send and where to send it, and, most importantly, how to read these packets—all of this will be covered in this chapter.

    Roles in I2C, the Communication Process

    In order to communicate with other sensors, you need to understand how I2C works in general. And how packets are exchanged. Yes, packets. That's where you can start.
    Each packet, one byte long, is sent one at a time over the SDA line. Moreover, it can travel in both directions. From master to slave and from slave to master. Below I'll explain the terminology used:
    This is the common name for devices that have some kind of control circuit. The one who controls is called the Master, and the one who obeys is called the Slave.
    1. Masteris the device that initiates and terminates transmission. It also generates the synchronization signal.
    2. Slaveis the device addressed by the master.
    3. Transmitter is the device that transmits data to the bus.
    4. Receiver is the device that reads data from the bus.
    Based on the existing roles, we can conclude that communication is possible in two ways. First, there is a master/transmitter, which transmits data to a slave/receiver. Second, there is a master/receiver, which receives data from a slave/transmitter.
    In practice, the roles are constantly changing. I'm getting ahead of myself a bit, but for the master to read anything, it first has to specify (i.e., write the appropriate register), and then switch to the receiver state and begin reading the transmitted data from the slave. But let's take things step by step.
    In this article, I'll only cover two master modes (transmitter and receiver), as I'm not yet able to build the sensors, much less program them. However, I think once you understand the basics of the I2C protocol, you can easily figure out how to operate both as a receiver slave and as a transmitter slave.

    Setting the Bitrate

    Before doing anything, you need to set the clock rate at which communication between the master and slave will occur. The bitrate is stored in a special register, TWBR (Two Wired Bitrate Register).
    The bitrate is not stored as absolute numbers, but as special values. For example, for an ATmega328P microcontroller with a clock rate of 16 MHz, if you need to achieve a transmit clock rate of 400 kHz, you need to set TWBR to 12. Where does 12 come from? From a formula found in the documentation.
    In code it will look like this:
    // Set the scale bit to 1. I don't want the communication frequency to be too low. TWSR = 0x00; // This is the formula (p. 180, page 21.5.2) for calculating the bit rate between Master and Slave 400 kHz. TWBR = (F_CPU - TARGET_FREQUENCY * 16)/TARGET_FREQUENCY*2*pow(4,TWSR);
    In addition to being used to detect errors and the results of certain steps in I2C communication, TWSR also sets the scaling bit.
    Once the bit rate has been initialized, you can begin configuring and preparing for actual communication between devices.

    Transmitter Master Mode

    In this mode, the master device writes data to the previously specified register. What does a typical data exchange between a master and slave look like in this mode? Let's take a look.
    Here are the symbols:
    1. Start - Transmission start condition
    2. Stop - Transmission end condition
    3. Adr - Slave address
    4. RAdr - Register address
    5. +W - Write bit
    6. Ack - Signal received
    7. Data - Data
    Write sequence of one bit.
    MasterStartAdr+W
    RAdr(Data)
    Data
    Stop
    Slave

    Ack
    Ack
    Ack
    Write sequence of two or more bits.
    MasterStartAdr+W
    RAdr(Data)
    Data
    Data
    Stop
    Slave

    Ack
    Ack
    Ack
    Ack
    There will be a lot of registers and bitwise math involved. A description of all the necessary registers can be found in the chapter linked, and the bitwise math will be explained step by step in this chapter.
    After each step of master-slave communication, it's also possible to check the TWSR register for the transmission status code. All codes and how to do this are described in this chapter.
    First, the Start signal is generated. This is done by writing the TWINT, TWSTA, and TWEN bits to the TWCR register. Like this: TWCR = ((1 << TWINT) | (1 << TWSTA) | (1 << TWEN)).
    In this expression (1 << TWINT), we bitwise shift 1 (i.e., 0000 0001) 7 steps to the left. Why 7? Simply TWINT = 7. The result is 1000 0000. The same thing happens with the other expressions (1 << TWSTA) and (1 << TWEN). In the second case, we move the 1 to the 5th bit, and in the third, to the 2nd bit. Then we simply combine (bitwise OR) all the resulting binary numbers. Like this:
    1000 0000
    |
    0010 0000
    |
    0000 0100
    Bitwise OR checks two operands. If at least one of them is 1, the result of the bitwise OR is 1.
    The resulting number should be 1010 0100.
    Next, according to the table, you need to send the device (slave) address and the read/write bit. In this case, write.
    Remember I said that communication between the slave and master occurs in packets of 8 bits each? So, the next packet, according to the I2C interface, is the slave address + read/write mode. Since the read mode takes 1 bit, that leaves 7 bits for the address, or 127 possible addresses for slaves. Well, just in case you were wondering why I2C supports exactly 127 addresses.
    How do you do this? To do this, you need to save the slave address in the TWDR register and append the write bit to the end. Like this: TWDR = (0x68 << 1) | 0. Here, 0x68 is the MPU6050 address specified in the documentation. We bitwise shift it left by one bit. That is, 0110 1000 became 1101 0000 . Then we add the write bit - 0.
    After storing the address and write bit in TWDR, we need to send it to the bus via SDA (more on this in the chapter on connecting I2C). To do this, we write the following bits to TWCR, like this: TWCR = ((1 << TWINT) | (1 << TWEN)). This results in 1000 0100.
    If a slave device with the specified address exists, it intercepts the SDA line and pulls it low (i.e., sends a 0, or Ack signal). If not, the SDA line remains untouched and the NAck signal is received, or a 1.
    After confirming that a slave device with this address exists, a byte of information is sent. The first byte sent is not always the register address. For most sensors and other complex devices, this is the case. The register address is sent first, and then the data to be written. But if it's something simple, like some ADCs, they receive the data immediately. To be sure, you need to check the relevant documentation.
    For example, when working with the MPU6050, you first need to specify the address of the register you want to write to. And only then specify the data you want to write.
    This is how it's done.
    // Save data to a special register TWDR = data; // Update the control register TWCR = ((1 << TWINT) | (1 << TWEN));
    Writing and sending data to a slave
    We repeat the writing (as in the code block) until a Stop or Repeated Start signal is sent. This is done as follows.
    To generate a Stop signal, write the following to the control register: TWCR = ((1 << TWINT) | (1 << TWSTO) | (1 << TWEN)). The result should be 1001 0100.
    To generate a Repeated Start signal, write the following to the control register: TWCR = ((1 << TWINT) | (1 << TWSTA) | (1 << TWEN)). The result should be 1010 0100.
    The Repeated Start signal allows you to switch between modes or between up to 127 slaves without releasing the SDA line.

    Master Receiver Mode

    In this mode, the master device receives data from the slave. How does it do this? And how does it specify what exactly needs to be read? To do this, it first needs to specify the address of the desired register (Transmitter Master Mode), which it specifies by writing to this register. After this, the mode switches to Receiver Master mode. Here's what this sequence looks like in the diagram:
    Here are the legend:
    1. Start - Transmission start condition
    2. Stop - Transmission end condition
    3. Adr - Slave address
    4. RAdr - Register address
    5. +W - Write bit
    6. +R - Read bit
    7. Ack - Signal received
    8. NAck - Signal not received
    9. Data - Data
    Read sequence of one bit.
    MasterStartAdr+W
    RAdr
    StartAdr+R

    NAckStop
    Slave

    Ack
    Ack

    AckData

    Reading sequence of several bits
    MasterStartAdr+W
    RAdr
    StartAdr+R

    Ack
    NackStop
    Slave

    Ack
    Ack

    AckData
    Data

    There will be a lot of registers and bitwise math involved. A description of all the necessary registers can be found in this chapter, and the bitwise math will be explained in this chapter.
    Also, after each step of master-slave communication, it is possible to check the TWSR register for the transmission status code. All codes and how to do this are described in this chapter.
    At the beginning, as in transmission mode, the Start signal is generated. This is done by writing the TWINT, TWSTA, and TWEN bits to the TWCR register. Like this: TWCR = ((1 << TWINT) | (1 << TWSTA) | (1 << TWEN)).
    In this expression (1 << TWINT), we bitwise shift 1 (that is, 0000 0001) 7 steps to the left. Why 7? Simply TWINT = 7. The same thing happens with the other expressions (1 << TWSTA) and (1 << TWEN). In the second case, we shift 1 to the 5th bit, and in the third, to the 2nd bit. Then we simply concatenate (bitwise OR) all the resulting binary numbers. The result should be 1010 0100.
    Next, according to the table, we need to send the device (slave) address and the read/write bit. Since I'm working with an MPU6050, the address will be 0x68 + 0 bit for writing.
    How do I do this? To do this, store the slave address in the TWDR register and append the write bit to the end. Like this: TWDR = (0x68 << 1) | 0. Here, 0x68 is the MPU6050 address specified in the documentation. We bitwise shift it left by one bit. So, 0110 1000 becomes 1101 0000 . Then we add the write bit - 0.
    After storing the address and the read/write bit in TWDR, we need to send it to the bus via SDA (more on this in the chapter on connecting I2C). To do this, we write the following bits to TWCR, like this: TWCR = ((1 << TWINT) | (1 << TWEN)). This gives 1000 0100.
    If a slave device with the specified address exists, it intercepts the SDA line and pulls it low (that is, sends a 0, or Ack signal). If not, the SDA line remains untouched and a NAck signal, or 1, is received.
    After confirming that a slave device with the specified address exists, a byte of information is sent. The first byte sent is not always the register address. For most sensors and other complex devices, this is true. The register address is sent first, and then the data to be written. However, if it's something simple, such as some ADCs, they receive data immediately. To be sure, you need to check the relevant documentation.
    So, when working with the MPU6050, you first need to specify the address of the register you want to read. Send it, and wait for a response.
    The above sequence looks like this:
    // Store the address of the target register on the slave TWDR = addres_of_register; // Update the control register, send a byte with the register address TWCR = ((1 << TWINT) | (1 << TWEN)); // Wait for the Ack signal while (!(TWCR & (1 << TWINT))); // 0x28 is the successful data transmission status with acknowledgement from the slave (ACK bit at the end) if ((TWSR & 0xF8) != 0x28){ // Handle the data transmission error }
    If the data with the register address was sent successfully (an Ack signal was received), then the master needs to be switched to receiver mode. This is done using the Repeated Start signal. It's done exactly the same way as a regular Start signal: TWCR = ((1 << TWINT) | (1 << TWSTA) | (1 << TWEN))
    Next, we send the device (slave) address and the read bit. Since I'm working with an MPU6050, the address will be 0x68 + 1, the read bit. It's done like this: TWDR = (0x68 << 1) | 1.
    TWDR = (0x68 << 1) | 1; // Tell the microcontroller to send data from the TWI Data Register, i.e., from TWDR TWCR = (1 << TWINT) | (1 << TWEN);
    Sending the Slave's address for reading
    After receiving the Ack signal in response, we can begin reading the transmitted data. The master device then sends either an Ack or NAck signal. The master—that's you, my dear reader—chooses which signal to send. It's crucial not to get it wrong here. After all, if you respond incorrectly, the data line from the Slave to the Master will get stuck in an infinite wait loop.
    I'm getting a bit ahead of myself, but checking whether the data has been sent is done by reading the TWCR register. More specifically, when the TWINT bit is cleared (1 -> 0), the internal logic of the TWI(I2C) circuit is used. This is why we must always set it when performing any step in the I2C communication process. The circuit could remove it later.
    Note: when the last byte of data is read, we must send a NAck signal. Otherwise, only an Ack signal. If we need to read the first and last bytes from the Slave register, we immediately send a NAck signal.
    If this is the first and last byte, to receive the data and respond with an NAck signal, we do it like this: TWCR = (1<<TWINT) | (1<<TWEN).
    If this is the first or any other byte, but definitely not the last, and we want to read more after it, we request the data and send an Ack, like this: TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA). This means we specifically set the TWEA bit; more about it in the chapter on registers.
    Then, we need to wait until the data appears in the TWDR register. This is done like this:
    // If this is the last byte the master wants to receive // Then we set the control register accordingly TWCR = (1<<TWINT) | (1<<TWEN); // Wait until a shift occurs in the data register while (!(TWCR & (1<<TWINT))); //Now we can receive the data and, for example, store it in SRAM uint8_t byte = TWDR;
    Once we've finished reading and sent the corresponding NAck signal, we need to stop data transmission, either with a Stop or Repeated Start signal. This is done in the same way as writing:
    To generate a Stop signal, write the following to the control register: TWCR = ((1 << TWINT) | (1 << TWSTO) | (1 << TWEN)). The result should be 1001 0100.
    To generate a Repeated Start signal, write the following to the control register: TWCR = ((1 << TWINT) | (1 << TWSTA) | (1 << TWEN)). The result should be 1010 0100.
    This sequence of actions is necessary when reading data from a connected device via I2C. Yes, it's much more complicated and confusing than writing, but it is what it is.

    Response Codes

    In this chapter, I've collected all possible response codes and their meanings. Since each of them is unique and there are some common codes for different modes, they will all be under one chapter.
    These codes only correspond to the Arduino family of microcontrollers, such as the ATmega8/32/328. Therefore, if you have a different microcontroller, check its documentation; the codes for you should be listed there

    0x8TW_STARTThe communication start condition (Start) was successfully met
    0x10TW_REP_STARTThe Repeated Start condition was successfully met.
    0x18TW_MT_SLA_ACKAdr+W byte was sent and Ack signal was received
    0x20TW_MT_SLA_NACKAdr+W byte was sent and NAck signal was received
    0x28TW_MT_DATA_ACK1 byte of data was sent and an Ack signal was received.
    0x30TW_MT_DATA_NACK1 byte of data was sent and an NAck signal was received.
    0x38TW_MT_ARB_LOST или TW_MR_ARB_LOSTThe slave device at the specified address Adr+W/+R is currently busy communicating with another Master.
    0x40TW_MR_SLA_ACK Adr+R byte was sent and Ack signal was received
    0x48TW_MR_SLA_NACKAdr+R byte was sent and NAck signal was received
    0x50TW_MR_DATA_ACK1 byte of data was received and an Ack signal was sent.
    0x58TW_MR_DATA_NACK1 byte of data was received and an NAck signal was sent.
    Respnse codeMacrosDescription
    To find and verify the response code after each step, we need to read the TWSR register. Here, by each step, I mean the action immediately after clearing the TWINT bit in TWCR. In code, it looks like this: while (!(TWCR & (1 << TWINT)));. This essentially indicates that the state of the TWSR register has changed, and we can check it and find out.
    For example, like this: (TWSR & 0xF8) != 0x18
    In this case, if an incorrect code is returned, we'll enter the procedure and, for example, send a warning via the UART protocol to the console on the developer's computer.
    UART (Universal Asynchronous Receiver/Transmitter) is a simple, two-wire (TX and RX) hardware protocol for serial communication, meaning it sends data one bit at a time without a shared clock, relying on pre-agreed speeds (baud rate) for synchronization. Max connected devices is 2.
    You can also include the util/twi.h header so that you can write more human-friendly names (macros) instead of status codes. In the practical example below, I didn't do this because I wanted you, my reader, to understand and see their explicit use.

    Registers used on the Master side (ATmega328P):

    TWCR (Two Wired Control Register) - a communication control register. This register controls the start and end of communication. It controls and holds the bus busy while a transfer is in progress. It also generates the Ack signal.
    TWDR (Two Wired Data Register) - a register where data is stored. This register stores the data sent by the slave when reading, and the master also writes its own data to it, including the slave's address.
    TWSR (Two Wired Status Register) - a register responsible for monitoring the communication status between the slave and the master. It also reports any errors that occurred during data transmission or reception.
    TWBR (Two Wired Bit Rate Register) - a register that stores the master's clock rate (divisor), allowing you to determine the data transfer frequency.
    More detailed information about the flags in each register can be found in the documentation. I highly recommend doing so. Page 198. Chapter 21.9.

    About connecting slaves via I2C

    Now that we know how data transfer works via this protocol, we need to understand how to connect sensors, probes, and other devices to the microcontroller as slaves so we can communicate and exchange data with them.
    The main advantage over, say, the SPI interface is the number of wires required in the circuit. Only two are needed. This doesn't include the voltage and ground lines, of course. In general, a clock line (SCL) and a data line (SDA) are required. Below is a diagram:
    On AVR devices, such as Arduinos, the pins for communicating with other devices via I2C are labeled A5 and A4. Specifically, for SCL it's A5, and for SDA it's A4.
    By the way, this circuit is valid if you're working with a chip already soldered onto a separate board. But if you're working with a bare chip, you'll also need to connect a pull-up resistor of at least 4.7 kOhm to the 5V, SCL, and SDA lines. Otherwise, nothing will work.

    Let's Talk, or How to Work with Peripherals via I2C

    Preparation (Headers and Structures)

    So, that's a little theory. If you understand the essence of this protocol, it's not that complicated. Let me show you with an example. We'll be communicating with an acceleration and rotation sensor—in short, with an MPU-6050 accelerometer.
    We'll also need to output the data somewhere to see if the code is working at all. You can use the built-in library and the Arduino Serial class, but in the code examples, I'll use my own implementation of the UART interface (I'll write about this interface someday).
    Our goal in this chapter is simply to read data from the gyroscope and accelerometer. This chip also has a temperature sensor, but we'll ignore it and disable it. So, where do we start? Connecting the headers.
    #include <avr/io.h> #include <stdio.h> #include <stdbool.h> #include <math.h> #include "UARTMessanger.h"
    Next, we'll define a structure for storing sensor data. Let's call it MPU6050.
     
    typedef struct { int16_t accel_x; int16_t accel_y; int16_t accel_z; int16_t temp; int16_t gyro_x; int16_t gyro_y; int16_t gyro_z; } MPU6050; MPU6050 getData(){ MPU6050 data; return data; }
    The function for receiving data is currently empty and does nothing. I'll fill it in a bit later, but for now, we're just setting the stage. Note that we're storing the sensor data in int16_t, as the documentation for this sensor's registers indicates two bytes for each axis.
    We've completed the preparatory part, so let's move on to the part where we configure the I2C protocol itself.

    Configuring the I2C protocol + a few abstractions

    Now I'm going to configure the protocol and sensor so that we can exchange data and determine whether we've actually connected to the MPU6050 sensor. To do this, in the setup function, we write:
    void setup(){ // Set the communication speed with the receiver (i.e., on the machine where the code is written) uart_init(57600); // Initialize the I2C interface i2c_init(); // MODE: Write one byte to register 0x6B (On the MPU6500, this is Power Manager register 1) i2c_start(MPU_6050_W_ADDR); i2c_write(0x6B); // This means we'll shift one bit to the left by 3 positions. So, 0000 0001 becomes 0000 1000 i2c_write(0x00 | 1 << 3); // 3 refers to the temperature sensor control bit. Let's assume we don't need it, so we'll assign a 1 to the corresponding bit. location // MODE: Read one byte from register 0x75 (on the MPU6500, this is the WHO_AM_i register) i2c_start(MPU_6050_W_ADDR); i2c_write(0x75); i2c_start(MPU_6050_R_ADDR); uint8_t byte = i2c_read(true); sprintf(uart_buffer, "WHO_AM_I(0x75) - 0x%x\n", byte); uart_transmit_string(uart_buffer); }
    We'll ignore the first line for initializing the UART data exchange interface today and move on. You may have noticed that I've abstracted the I2C interface slightly into four functions: i2c_init, i2c_start, i2c_read, and i2c_write.

    About a small extension to the I2C interface (abstraction)

    And, of course, I'll go over them all. With your permission, I won't describe the interface itself, as I did at the beginning, but you'll find helpful hints here and there throughout the code that should help you understand the process.
    i2c_init does everything described in the chapter on setting the bitrate, plus it sets the TWEN flag in TWCR to activate the interface.
    void i2c_init() { // Set the scale bit to 1. I don't want the communication frequency to be too low. TWSR = 0x00; // This is the formula (p. 180, page 21.5.2) for calculating the bit rate between Master and Slave 400 kHz. TWBR = (F_CPU - TARGET_FREQUENCY * 16)/TARGET_FREQUENCY*2*pow(4,TWSR); // Activate TWI. TWCR = (1<<TWEN); }
    i2c_start sets the condition for starting communication (START, REPEATED START) and sends the address of the device with which communication must be established.
    uint8_t i2c_start(uint8_t address) { // 1. Send a signal that we are ready to begin communicating with the peripheral, the START signal // (1000 0000 | 0010 0000 | 0000 0100) -> This means we are starting the START data transfer. TWCR will automatically clear the TWINT flag, resulting in 0010 0100 (0x24) TWCR = ((1<<TWINT)|(1<<TWSTA)|(1<<TWEN)); // !(0010 0100 & 1000 0000) -> This means TWCR will send a signal to SCL until the TWINT flag is set in the TWCR register while (!(TWCR & (1<<TWINT))); // The byte (data) sending status code is checked. // 0x08 means the master device has successfully sent the START signal and is ready to continue communication. // 0x10 means the master device has successfully sent the REPEATED START signal and is ready to continue communication. if ((TWSR & 0xF8) != 0x08 && (TWSR & 0xF8) != 0x10){ sprintf(uart_buffer, "Init START or REPEATED START error: 0x%x\n", TWSR & 0xF8); uart_transmit_string(uart_buffer); } // 2. Write the device address to the corresponding TWDR register // Since the addresses are 7-bit, we need to shift by one bit and write 0 at the end // So, 0110 1000 became 1101 0000 after the shift // 0 indicates that we are writing (write mode) TWDR = address; // Tell the microcontroller to send data from the TWI Data Register, i.e., from TWDR TWCR = (1 << TWINT) | (1 << TWEN); while (!(TWCR & (1 << TWINT))); // The byte (data) sending status code is checked. Since this is the first transmission, the Slave address is sent. // 0x18 means that the address with the write bit was sent successfully and the Slave appended the ACK bit. // 0x40 means that the address with the read bit was sent successfully and the Slave appended the ACK bit. if ((TWSR & 0xF8) != 0x18 && (TWSR & 0xF8) != 0x40){// 0x18 - SLA+W | 0x40 - SLA+R ACK sprintf(uart_buffer, "Send address error: 0x%x\n", TWSR & 0xF8); uart_transmit_string(uart_buffer); } return TWSR & 0xF8; }
    i2c_write sends a byte of data over the bus and, accordingly, waits for a signal to return (Ack or NAck).
    uint8_t i2c_write(uint8_t data){ // Store data in a special register TWDR = data; // Send data by setting the corresponding TWINT and TWEN flags TWCR = (1<<TWINT) | (1<<TWEN); // Wait until the CPU clears the TWINT flag, which means the data has been sent while (!(TWCR & (1<<TWINT))); // 0x28 is the successful data transmission status with acknowledgement from the Slave (the ACK bit at the end) if ((TWSR & 0xF8) != 0x28){ sprintf(uart_buffer, "Data write error: 0x%x\n", TWSR & 0xF8); uart_transmit_string(uart_buffer); } return 1; }
    i2c_read reads data from the bus and returns it if everything went successfully and no flag appeared in the TWSR register.
    uint8_t i2c_read(bool isLast) { // Indicate that we want to read more data if (!isLast) TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA); // Otherwise, in the last read, we need to clear the TWEA bit to indicate (tell the Slave Transmitter) that we've finished reading else TWCR = (1<<TWINT) | (1<<TWEN); // Wait for the MCU to reset our TWINT flag, which means the data has been received while (!(TWCR & (1<<TWINT))); // 0x50 is the successful status of received data with acknowledgement from the Slave (the ACK bit at the end) // 0x58 is the successful status of received data without acknowledgement from the Slave (the NACK bit at the end) if (((TWSR & 0xF8) != 0x50) && ((TWSR & 0xF8) != 0x58)){ sprintf(uart_buffer, "Data read error: 0x%x\n", TWSR & 0xF8); uart_transmit_string(uart_buffer); } // Return the received data return TWDR; }
    This function also allows you to specify whether this will be the last read or not. If so (the last read), we remove the TWEA flag from TWCR.
    Note how I'm using response statuses. Essentially, all these checks indicate that if anything other than the specified statuses occurs, an error has occurred, and we enter a procedure where we output an error message via the UART interface.

    Let's take a closer look at the initialization code.

    We pass the read address, which we calculated and saved in a macro, to the i2c_start function. Like this:
    #define MPU_6050_W_ADDR 0xD0 // (0x68 << 1) | 0 (Write) #define MPU_6050_R_ADDR 0xD1 // (0x68 << 1) | 1 (Read) #define TARGET_FREQUENCY 400000L // The maximum data exchange rate can be 400 kHz
    Next, we send the address of the register we want to write to. In our case, it's register 0x6B: [i2c_write(0x6B);] This is the register that controls power supply to the sensor. Then, we write the data that should be written to the previously specified address.
    I write the following: [i2c_write(0x00 | 1 << 3);] Where 3 is the bit position at the specified address. What does that even mean?
    Look, we're actually sending 0000 1000. The sleep bit, set to 0, wakes up the sensor, and setting the 3rd bit to 1 disables the temperature sensor.
    And the last thing I do during initialization is read the sensor ID itself, just in case.
    // MODE:Reading one byte from register 0x75 (on the MPU6500 this is the WHO_AM_I register) i2c_start(MPU_6050_W_ADDR); i2c_write(0x75); i2c_start(MPU_6050_R_ADDR); uint8_t byte = i2c_read(true); sprintf(uart_buffer, "WHO_AM_I(0x75) - 0x%x\n", byte); uart_transmit_string(uart_buffer);
    We should receive 0x68 in response, which is the sensor address. If you have this sensor, of course.
    You'll most likely receive 0x70 instead of 0x68. Why? Because you don't have an MPU6050, but an MPU6500, a slightly improved version of the sensor with minimal register differences from the MPU6050. Just know that this is normal and everything is fine with your I2C.
    We've completed the initial setup and verification of the I2C protocol. Note that I didn't call i2c_stop because it's unnecessary. We need to continue reading data from the bus, so there's no point in letting it go. It's exactly the same as with the restart to read the sensor ID. We simply sent a REPEATED START signal, and communication resumed.

    Reading the data from the sensor

    We need to receive fresh data from the sensor, so reading it alone won't do the trick. We'll be constantly reading and updating it. Here's how you can do it in a loop:
    void loop(){ // MODE: Read a sequence of bytes, starting with register 0x3B and ending with 0x48 (14 bytes total) // 0x3B is the accelerometer register for the X axis. Next come accel_y(H,L), accel_z(H,L), temperature(H,L), gyro_x(H,L), gyro_y(H,L), gyro_z(H,L) // NOTE: Values ​​are stored in 2 bytes, the high-order one is H and the low-order one is L // A total of 7 sensors, two bytes each i2c_start(MPU_6050_W_ADDR); // <- START condition i2c_write(0x3B); // Collect data from 7 sensors i2c_start(MPU_6050_R_ADDR); // <- REPEATED START condition MPU6050 data = getData(); // Just outputting the data sprintf(uart_buffer, "accel_x=%d\taccel_y=%d\taccel_z=%d\t", data.accel_x, data.accel_y, data.accel_z); uart_transmit_string(uart_buffer); sprintf(uart_buffer, "temp=%d\t", data.temp); uart_transmit_string(uart_buffer); sprintf(uart_buffer, "gyro_x=%d\tgyro_y=%d\tgyro_z=%d\n", data.gyro_x, data.gyro_y, data.gyro_z); uart_transmit_string(uart_buffer); // I release the bus. After this, the sensor data will be updated, and the next cycle will show new data. // Without releasing the bus, the MPU6500 chip will not update the data in its registers. i2c_stop(); }
    After setting the initial address (0x3B) in write mode, we immediately switch to read mode. The first byte we read will be the byte in register 0x3B. With each read, the sensor's internal counter will increment by 1. Therefore, after reading address 0x3B, we read address 0x3C, and so on until we respond with a NAck signal.
    After successfully reading the entire sequence, we complete the transfer and release the bus using the i2c_stop function. We simply set the corresponding TWSTO flag: TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTO);.
    Why not send a REPEATED START signal and repeat the reading? This is possible, but the sensor data won't be updated, and the old values ​​will remain.
    Let's write a complete implementation of the getData function for receiving data from the sensor.
    MPU6050 getData(){ MPU6050 data; // NOTE: To correctly save data from the MPU6500 registers, // they must be placed into 2 bytes. // To do this, after reading 1 byte from the bus, we place it into an int16_t using a bitwise left shift. // So, [0000 0000 1001 1111] becomes [1001 1111 0000 0000] // Accel X data.accel_x = (int16_t)((i2c_read(false) << 8) | i2c_read(false)); // Accel Y data.accel_y = (int16_t)((i2c_read(false) << 8) | i2c_read(false)); // Accel Z data.accel_z = (int16_t)((i2c_read(false) << 8) | i2c_read(false)); // temp data.temp = (int16_t)((i2c_read(false) << 8) | i2c_read(false)); // Gyro X data.gyro_x = (int16_t)((i2c_read(false) << 8) | i2c_read(false)); // Gyro Y data.gyro_y = (int16_t)((i2c_read(false) << 8) | i2c_read(false)); // Gyro Z // Note that when reading the last byte, the master must send a NACK signal to notify the // slave that reading is complete. Otherwise, we won't be able to initiate the START signals, or in my case, REPEATED START. data.gyro_z = (int16_t)((i2c_read(false) << 8) | i2c_read(true)); return data; }
    Let me start with a note. Since the addresses for each acceleration, temperature, and gyroscope value are stored sequentially, the code is much simpler and we don't have to constantly change the read address. Which is convenient.
    Also, the sensor data is stored in two bytes, so we need to make two consecutive reads and combine them into a single variable with an int16_t integer value. To do this, we shift the first byte forward by half, or 8 bits, so that the data from the second read fits.
    Finally, we simply read by sending a NAck signal to the connected sensor.
    And just like that, we've successfully read data from the sensor using only the built-in registers, documentation, and the internal logic of the devices. Your console output might look something like this:
    Notice that the temperature hasn't changed, even though I touched it. This means we've successfully disabled the temperature sensor. Also, look at WHO_AM_I. It's 0x70. This is normal, as described above.

    In conclusion

    In conclusion, I'd like to say that programming and working with devices this way is much more interesting and, I would say, "more reliable." After all, you won't get confused in your own implementation. Plus, you gain a deeper understanding of interfaces and the operation of microcontrollers and other devices. Another big advantage of this approach is now you can read any datasheet and, with no guide, set up any chip with any microcontroller together.
    I hope I've helped you understand the I2C interface, and now you can connect any device and read its data in minutes.

    Do not forget to share, like and leave a comment :)

    Comments

    (0)

    captcha
    Send
    LOADING ...
    It's empty now. Be the first (o゚v゚)ノ