I2C Protocol

I2C combines the best features of SPI and UARTs. With I2C, you can connect multiple slaves to a single master (like SPI) and you can have multiple masters controlling single, or multiple slaves. This is really useful when you want to have more than one microcontroller logging data to a single memory card or displaying text to a single LCD.

Like UART communication, I2C only uses two wires to transmit data between devices:

Introduction to I2C - Single Master Single Slave

SDA (Serial Data) – The line for the master and slave to send and receive data.

SCL (Serial Clock) – The line that carries the clock signal.

Note: in many breakout boards, the SDA line may also be labeled as SDI and the SCL line as SCK.

I2C is a serial communication protocol, so data is transferred bit by bit along a single wire (the SDA line).

Like SPI, I2C is synchronous, so the output of bits is synchronized to the sampling of bits by a clock signal shared between the master and the slave. The clock signal is always controlled by the master.

Basics of the I2C Communication Protocol - Specifications Table


With I2C, data is transferred in messages. Messages are broken up into frames of data. Each message has an address frame that contains the binary address of the slave, and one or more data frames that contain the data being transmitted. The message also includes start and stop conditions, read/write bits, and ACK/NACK bits between each data frame:

Introduction to I2C - Message, Frame, and Bit

Start Condition: The SDA line switches from a high voltage level to a low voltage level before the SCL line switches from high to low.

Stop Condition: The SDA line switches from a low voltage level to a high voltage level after the SCL line switches from low to high.

Address Frame: A 7 or 10 bit sequence unique to each slave that identifies the slave when the master wants to talk to it.

Read/Write Bit: A single bit specifying whether the master is sending data to the slave (low voltage level) or requesting data from it (high voltage level).

ACK/NACK Bit: Each frame in a message is followed by an acknowledge/no-acknowledge bit. If an address frame or data frame was successfully received, an ACK bit is returned to the sender from the receiving device.


I2C doesn’t have slave select lines like SPI, so it needs another way to let the slave know that data is being sent to it, and not another slave. It does this by addressing. The address frame is always the first frame after the start bit in a new message.

The master sends the address of the slave it wants to communicate with to every slave connected to it. Each slave then compares the address sent from the master to its own address. If the address matches, it sends a low voltage ACK bit back to the master. If the address doesn’t match, the slave does nothing and the SDA line remains high.


The address frame includes a single bit at the end that informs the slave whether the master wants to write data to it or receive data from it. If the master wants to send data to the slave, the read/write bit is a low voltage level. If the master is requesting data from the slave, the bit is a high voltage level.


After the master detects the ACK bit from the slave, the first data frame is ready to be sent.

The data frame is always 8 bits long, and sent with the most significant bit first. Each data frame is immediately followed by an ACK/NACK bit to verify that the frame has been received successfully. The ACK bit must be received by either the master or the slave (depending on who is sending the data) before the next data frame can be sent.

After all of the data frames have been sent, the master can send a stop condition to the slave to halt the transmission. The stop condition is a voltage transition from low to high on the SDA line after a low to high transition on the SCL line, with the SCL line remaining high.


1. The master sends the start condition to every connected slave by switching the SDA line from a high voltage level to a low voltage level before switching the SCL line from high to low:

Introduction to I2C - Data Transmission Diagram START CONDITION

2. The master sends each slave the 7 or 10 bit address of the slave it wants to communicate with, along with the read/write bit:

Introduction to I2C - Data Transmission Diagram ADDRESS FRAME

3. Each slave compares the address sent from the master to its own address. If the address matches, the slave returns an ACK bit by pulling the SDA line low for one bit. If the address from the master does not match the slave’s own address, the slave leaves the SDA line high.

Introduction to I2C - Data Transmission Diagram ACK Bit Slave to Master

4. The master sends or receives the data frame:

Introduction to I2C - Data Transmission Diagram Data Frame

5. After each data frame has been transferred, the receiving device returns another ACK bit to the sender to acknowledge successful receipt of the frame:

Introduction to I2C - Data Transmission Diagram ACK Bit Slave to Master

6. To stop the data transmission, the master sends a stop condition to the slave by switching SCL high before switching SDA high:

Introduction to I2C - Data Transmission Diagram Stop Condition


Because I2C uses addressing, multiple slaves can be controlled from a single master. With a 7 bit address, 128 (27) unique address are available. Using 10 bit addresses is uncommon, but provides 1,024 (210) unique addresses. To connect multiple slaves to a single master, wire them like this, with 4.7K Ohm pull-up resistors connecting the SDA and SCL lines to Vcc:

Introduction to I2C - Single Master Multiple Slaves


Multiple masters can be connected to a single slave or multiple slaves. The problem with multiple masters in the same system comes when two masters try to send or receive data at the same time over the SDA line. To solve this problem, each master needs to detect if the SDA line is low or high before transmitting a message. If the SDA line is low, this means that another master has control of the bus, and the master should wait to send the message. If the SDA line is high, then it’s safe to transmit the message. To connect multiple masters to multiple slaves, use the following diagram, with 4.7K Ohm pull-up resistors connecting the SDA and SCL lines to Vcc:

Introduction to I2C - Multiple Masters Multiple Slaves 2


There is a lot to I2C that might make it sound complicated compared to other protocols, but there are some good reasons why you may or may not want to use I2C to connect to a particular device:


  • Only uses two wires
  • Supports multiple masters and multiple slaves
  • ACK/NACK bit gives confirmation that each frame is transferred successfully
  • Hardware is less complicated than with UARTs
  • Well known and widely used protocol


  • Slower data transfer rate than SPI
  • The size of the data frame is limited to 8 bits
  • More complicated hardware needed to implement than SPI

containers and Iterators in C++


class MyContainer{
    const int* array;
    int size;
    class Iterator{
        const int *obj;
        Iterator(const int* obj):obj(obj){

        bool operator !=(Iterator other){
            cout<<"call != \n";
                return false;
            return true;

        Iterator operator++(){
            cout<<"call ++ \n";
            return *this;

        const int& operator*()const {
            cout<<"call * \n";
            return *obj;

    template <int i>
    MyContainer(int const (&obj)[i]):array(obj),size(i){


    Iterator begin(){
        MyContainer::Iterator iterator(this->array);
        cout<<"call begin\n";
        return iterator;

    Iterator end() {
        MyContainer::Iterator iterator(this->array+size);
        cout << "call begin\n";
        return iterator;


int main(int argc,char* argv[]){
    MyContainer mc({1,2,3,4,5,6});
    for(const auto& vv:mc){

    return 0;

simple forward iterator and the result is

call begin
call !=
call *
call ++
call !=
call *
call ++
call !=
call *
call ++
call !=
call *
call ++
call !=
call *
call ++
call !=
call *
call ++
call !=

Iterator categoryPropertiesContainer
Forward iterator++It, It++, *Itunordered associative container
 It == It2, It != It2std::forward_list
Bidirectional iterator--It, It--ordered associative container
Random access iteratorIt[i]std::array
 It+= n, It-= nstd::vector
 It+n, It-nstd::deque
 It < It2, It <= It2, It > It2 
 It >= It2 
Global functionDescription
std::begin(cont)Returns a begin iterator to the container cont.
std::end(cont)Returns an end iterator to the container cont.
std::rbegin(cont)Returns a reverse begin iterator to the container cont.
std::rend(cont)Returns a reverse end iterator to the container cont.
std::cbegin(cont)Returns a constant begin iterator to the container cont.
std::cend(cont)Returns a constant end iterator to the container cont.
std::crbegin(cont)Returns a reverse constant begin iterator to the container cont.
std::crend(cont)Returns a reverse constant end iterator to the container cont.
std::prev(it)Returns an iterator, which points to a position before it
std::next(it)Returns an iterator, which points to a position after it.
std::distance(fir, sec)Returns the number of elements between fir and sec.
std::advance(it, n)Puts the iterator it n positions further.
general functions
// iteratorUtilities.cpp
#include <iterator>
using std::cout;

std::unordered_map<std::string, int> myMap{{"Rainer", 1966}, {"Beatrix", 1966},
                                           {"Juliette", 1997}, {"Marius", 1999}};

for (auto m: myMap) cout << "{" << m.first << "," << m.second << "} ";
     // {Juliette,1997},{Marius,1999},{Beatrix,1966},{Rainer,1966}

auto mapItBegin= std::begin(myMap);
cout << mapItBegin->first << " " << mapItBegin->second; // Juliette 1997

auto mapIt= std::next(mapItBegin);
cout << mapIt->first << " " << mapIt->second;            // Marius 1999
cout << std::distance(mapItBegin, mapIt);                 // 1

std::array<int, 10> myArr{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (auto a: myArr) std::cout << a << " ";               // 0 1 2 3 4 5 6 7 8 9

auto arrItEnd= std::end(myArr);
auto arrIt= std::prev(arrItEnd);
cout << *arrIt << std::endl;                             // 9

std::advance(arrIt, -5);
cout << *arrIt;                                          // 4


The table shows you the constructors and destructors of a container. A std:vector stands for the rest of them.

Defaultstd::vector<int> vec1
Rangestd::vector<int> vec2(vec1.begin(), vec1.end())
Copystd::vector<int> vec3(vec2)
Copystd::vector<int> vec3= vec2
Movestd::vector<int> vec4(std::move(vec3))
Movestd::vector<int> vec4= std::move(vec3)
Sequence (Initializer list)std::vector<int> vec5 {1, 2, 3, 4, 5}
Sequence (Initializer list)std::vector<int> vec5= {1, 2, 3, 4, 5}
Delete elementsvec5.clear()

Because std::array is generated at compile time, there are a few things that are special. std::array has no move constructor and can neither be created with a range nor with an initialiser list. However, you can initialize a std::array with an aggregate initialisation. Also, std::array has no method for removing its elements.

#include <map>
#include <unordered_map>
#include <vector>
using namespace std;

vector<int> vec= {1, 2, 3, 4, 5, 6, 7, 8, 9};
map<string, int> m= {{"bart", 12345}, {"jenne", 34929}, {"huber", 840284} };
unordered_map<string, int> um{m.begin(), m.end()};

for (auto v: vec) cout << v << " "; // 1 2 3 4 5 6 7 8 9
for (auto p: m) cout << p.first << "," << p.second << " ";
                                    // bart,12345 huber,840284 jenne,34929
for (auto p: um) cout << p.first << "," << p.second << " ";
                                    // bart,12345 jenne,34929 huber,840284

vector<int> vec2= vec;
cout << vec.size() << endl; // 9
cout << vec2.size() << endl; // 9

vector<int> vec3= move(vec);
cout << vec.size() << endl; // 0
cout << vec3.size() << endl; // 9

cout << vec3.size() << endl; // 0


#include <map>
#include <set>
#include <vector>
using namespace std;

vector<int> intVec{1, 2, 3, 4, 5, 6, 7, 8, 9};
map<string, int> str2Int = {{"bart", 12345}, 
                            {"jenne", 34929}, {"huber", 840284}};
set<double> douSet{3.14, 2.5};

cout << intVec.empty() << endl;  // false
cout << str2Int.empty() << endl; // false
cout << douSet.empty() << endl;  // false

cout << intVec.size() << endl;  // 9
cout << str2Int.size() << endl; // 3
cout << douSet.size() << endl;  // 2

cout << intVec.max_size() << endl;  // 4611686018427387903
cout << str2Int.max_size() << endl; // 384307168202282325
cout << douSet.max_size() << endl;  // 461168601842738790


template<class InputIterator, class OutputIterator>
  OutputIterator copy (InputIterator first, InputIterator last, OutputIterator result)
  while (first!=last) {
    *result = *first;
    ++result; ++first;
  return result;


cont.begin() and cont.end()Pair of iterators to iterate forward.
cont.cbegin() and cont.cend()Pair of iterators to iterate const forward.
cont.rbegin() and cont.rend()Pair of iterators to iterate backward.
cont.crbegin() and cont.crend()Pair of iterators to iterate const backward.

assign new elements
You can assign new elements to existing containers or swap two containers. For the assignment of a container cont2 to a container cont, there exists the copy assignment cont= cont2 and the move assignment cont= std::move(cont2). A special form of assignment is the one with an initialiser list: cont= {1, 2, 3, 4, 5}. That’s not possible for std::array, but you can instead use the aggregate initialisation. The function swap exists in two forms. You have it as a method cont(swap(cont2)) or as a function template std::swap(cont, cont2).

comparison operators
Containers support the comparison operators ==!=<><=>=. The comparison of two containers happens on the elements of the containers. If you compare associative containers, their key is compared. Unordered associative containers support only the comparison operator == and !=.

Sequential Container

Implementationstatic arraydynamic arraysequence of arraysdoubled linked listsingle linked list
Accessrandomrandomrandomforward and backwardforward
Optimized for insert and delete at end: O(1)begin and end: O(1)begin and end: O(1)begin(1)
    arbitrary: O(1)arbitrary: O(1)
Memory reservation yesnonono
Release of memory shrink_to_fitshrink_to_fitalwaysalways
Strengthno memory allocation; minimal memory requirements95% solutioninsertion and deletion at the begin and endinsertion and deletion at an arbitrary positionfast insertion and deletion; minimal memory requirements
Weaknessno dynamic memoryInsertion and deletionInsertion and deletionno random accessno random access
 memory allocationat an arbitrary position: O(n)at an arbitrary position: O(n)  

std::vector is a homogeneous container, for which it’s length can be adjusted at runtime. std::vector needs the header <vector>. As it stores its elements contiguously in memory, std::vector support pointer arithmetic.


std::deque, which consists of a sequence of arrays, is quite similar to std::vectorstd::deque need the header <deque>. The std::deque has three additional methods deq.push_front(elem)deq.pop_front() and `deq.emplace_front(args… ) to add or remove elements at its beginning.


std::list is a doubled linked list. std::list needs the header <list>.

Although it has a similar interface to std::vector or std::dequestd::list is quite different to both of them. That’s due to its structure.

Associative Container

Associative containerSortedAssociated valueMore identical keysAccess time

Adaptors for Containers


#include <stack>
std::stack<int> myStack;

std::cout << myStack.empty() << std::endl;   // true
std::cout << myStack.size() << std::endl;    // 0

std::cout << myStack.top() << std::endl;     // 3

while (!myStack.empty()){ 
  std::cout << myStack.top() << " ";
}                                            // 3 2 1

std::cout << myStack.empty() << std::endl;   // true
std::cout << myStack.size() << std::endl;    // 0


#include <queue>
std::queue<int> myQueue;

std::cout << myQueue.empty() << std::endl;    // true
std::cout << myQueue.size() << std::endl;     // 0

std::cout << myQueue.back() << std::endl;     // 3
std::cout << myQueue.front() << std::endl;    // 1

while (!myQueue.empty()){
  std::cout << myQueue.back() << " ";
  std::cout << myQueue.front() << " : ";
}                                             // 3 1 : 3 2 : 3 3

std::cout << myQueue.empty() << std::endl;    // true
std::cout << myQueue.size() << std::endl;     // 0


#include <queue>
std::priority_queue<int> myPriorityQueue;

std::cout << myPriorityQueue.empty() << std::endl;   // true
std::cout << myPriorityQueue.size() << std::endl;    // 0

std::cout << myPriorityQueue.top() << std::endl;     // 3

while (!myPriorityQueue.empty()){
  std::cout << myPriorityQueue.top() << " ";
}                                                    // 3 2 1

std::cout << myPriorityQueue.empty() << std::endl;   // true
std::cout << myPriorityQueue.size() << std::endl;    // 0

std::priority_queue<std::string, std::vector<std::string>,
                    std::greater<std::string>> myPriorityQueue2;


while (!myPriorityQueue2.empty()){
  std::cout << myPriorityQueue2.top() << " ";
}                                // . Only for purpose testing

back_inserter + front_inserter + inserter

#include <iterator>
std::deque<int> deq{5, 6, 7, 10, 11, 12};
std::vector<int> vec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

std::copy(std::find(vec.begin(), vec.end(), 13),
          vec.end(), std::back_inserter(deq));

for (auto d: deq) std::cout << d << " ";
                    // 5 6 7 10 11 12 13 14 15

std::copy(std::find(vec.begin(), vec.end(), 8),
std::find(vec.begin(), vec.end(), 10),
std::find(deq.begin(), deq.end(), 10)));d
for (auto d: deq) std::cout << d << " ";
                    // 5 6 7 8 9 10 11 12 13 14 15

std::copy(vec.rbegin()+11, vec.rend(),
for (auto d: deq) std::cout << d << " ";
                    // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15