Introduction: Expand Your Arduino's I/O With an I2C Slave Device
Foreword
Have you ever found whilst creating your Arduino Uno project you only just run out of available I/O?
So what are my options you may ask?
- Using the I2C interface you could add extra I/O chips (PCF8574, MCP23017, MCP3422/3/4 etc.) but this can be quite wasteful and result in a complex solution,
- Trade up to a bigger Arduino (Mega2560/Due) assuming it fulfills your I/O needs.
Well, if like me you have a few spare Arduino Unos hanging around there is a third way. Ever considered connecting together two Arduinos as Master and Slave via I2C?
Read on...
Introduction
As described above, this Instructable details how to use an Arduino Uno configured as an I2C Slave in order to extend the I/O available in the Slave to that of the Master.
What parts do I need?
To build the circuit depicted in the image above you will need the following parts;
- 3 off 10K resistor
- 2 off 470R resistor
- 2 off LEDs
- 1 off SPST button
- 1 off 10K potentiometer
- 2 off Arduino Uno R3
- 1 off Prototype breadboard
- Various interconnection wires
What skills do I need?
- A minimal grasp of electronics,
- Knowledge of Arduino and it's IDE,
- A little patience
.
Topics covered
- Brief overview of the circuit,
- Brief overview of the software,
- How to test your I2C Slave device,
- Shrinking your design,
- Conclusion,
- References used.
.
Disclaimer
As always, you use these instructions at your own risk and they come unsupported.
Step 1: Circuit Overview
The circuit couldn't be simpler.
All you need to do is connect up the Master and Slave devices as shown above, remembering to link common grounds together.
A4 on the master is connected to A4 of the slave. The same is true for A5.
Here;
A4 is I2C SDA
A5 is I2C SCL
The remaining circuit on the prototype board is designed to work with the example software I wrote detailed in the next steps.
.
Note 1 : In the diagram I have not shown any pull ups connected between A4 and Vcc, A5 and Vcc, the circuit as shown will work given the Arduino has internal pull ups of around 20K (in parallel between the two Arduinos this gives approx 10K). However, it is always best practice to include the pull ups.
I have included the circuit diagram of the Arduino Uno R3 below to show there are no explicit 10K resistors attached.This is primarily due to the dual use of the A4/A5 lines as analogue inputs and I2C SDA/SCL respectively.
Note 2 : Power the Arduinos from the same USB hub. This is to ensure both devices have the same ground reference point and prevent any circulating currents when commoned and also stop any odd ADC readings.
.
For more information on I2C see 'Arduino I2C LCD Driver Library and PackMan' : 'I2C_LCD_With_Arduino.pdf'
Attachments
Step 2: Software Overview
Preamble
To successfully compile this source code you will need the following library;
Wire.h
- By : Arduino Core
- Purpose : Gives I2C functionality.
- From : https://www.arduino.cc/en/Reference/Wire
Code Overview
SetUp
See Pic 1 above, on startup the software initialises the I/O of the Slave device and sets the I2C address using the #define SLAVE_ADDR 8.
Initial values are as follows;
- For Digital I/O, all lines are set as inputs,
- For PWM outputs, all are set off,
- For Analogue inputs, all values are set as 0.
Next, all values in the register array bRegisterArray[] are set to 0 (other than Digital I/O = ALL_IN, DDR=B00111111).
Finally the call back functions to allow the I2C Master to read and write from/to the Slave device are registered.
- Wire.onReceive(receiveEvent); // register event
- Wire.onRequest(requestEvent); // register event
Loop
Once startup has completed the code then loops indefinitely. During this looping the following action occurs;
- Do PWM : Values from bRegisterArray[] entries 0x00 ... 0x05 are written to PWMs 3, 5, 6, 9, 10 and 11 respectively (Pic 2 above),
- Do Digital I/O : Depending upon the state of the Data Direction Register (DDR) bRegisterArray[] entry 0x06 the corresponding Digital I/O Bit is either read from the Pin and Written to the Data Register (DR) bRegisterArray[] entry 0x07 as in the case if it were an input, or alternatively the converse is true if it were an output. Currently Digital pins 2, 4, 7, 8, 12 and 13 are mapped to bRegisterArray[] register bits 0...5 respectively. Note : If corresponding DDR bit is '1' then pin is treated as an Input, if '0' then it becomes and output (Pic 3 above),
- Do Analogue I/Ps : Values from Analogue Inputs A0 ... A3 are written to bRegisterArray[] entries 0x08 ... 0x0F respectively. Note : The Arduino Uno uses a 10 bit ADC giving 0...1023, meaning the ADC value must be split across 2 bytes in the bRegisterArray[]. Hence the use of ANALOGUE_IN_0/3_LOW and ANALOGUE_IN_0/3_HIGH constants (Pic 4 above).
Asynchronous Event Handling
During execution of the main loop as mentioned above, communications with the I2C Slave are handled via the routines receiveEvent and requestEvent. These routines access the shared memory space in bRegisterArray[].
Each entry in this array has specific meaning;
- Pulse Width Modulation - PWM, Addresses 0x00 ... 0x05
- Data Direction Register - DDR, Address 0x06
- Data Register - DR, Address 0x07
- Analogue inputs A0 ... A3. Address 0x08 ... 0x0F. Paired as A0 = 0x08 - 0x09, A1 = 0x0A - 0x0B, A2 = 0x0C - 0x0D, A3 = 0x0E - 0x0F. Paired as low 8 bits abd hi 2 bits, which make up the value 0 ... 1023.
Once an update is made to any of these registers on the next successive execution of the loop the changes will be picked up.
.
Writing to the Slave device from the Master : receiveEvent
In order to write to the Slave the Master must first set the address of the target register followed by a second write of the data. For example;
- Wire.beginTransmission(SLAVE_ADDR);
- // Point to DDR
- Wire.write(DDR);
- error = Wire.endTransmission();
followed by;
- Wire.beginTransmission(SLAVE_ADDR);
- // X, X, P13=O/P, P12=O/P, P8=O/P, P7=O/P, P4=O/P, P2=O/P
- Wire.write(B00000000);
- error = Wire.endTransmission();
Or they can be combined;
- // X, X, P13=O/P, P12=O/P, P8=O/P, P7=O/P, P4=O/P, P2=O/P
- Wire.beginTransmission(SLAVE_ADDR);
- Wire.write(DDR);
- Wire.write(B00000000);
- error = Wire.endTransmission();
Reading from the Slave device by the Master : requestEvent
In order to read from the Slave device the Master must first set the address of the target register followed by a read of the data. For example;
Reading 1 byte
- Wire.beginTransmission(SLAVE_ADDR);
- // Point to DR
- Wire.write(DR);
- error = Wire.endTransmission();
- Wire.requestFrom((int)SLAVE_ADDR, (int)1); // request 1 byte from slave device #addr
- value = (char) Wire.read();
Reading more than one byte
- Wire.beginTransmission(SLAVE_ADDR);
- // Point to ANALOGUE_IN_0_LOW
- Wire.write(ANALOGUE_IN_0_LOW);
- error = Wire.endTransmission();
- Wire.requestFrom((int)SLAVE_ADDR, (int)1); // request 1 byte from slave device #addr
- value1 = (char) Wire.read(); // Read from ANALOGUE_IN_0_LOW register
- Wire.requestFrom((int)SLAVE_ADDR, (int)1); // request next byte from slave device #addr
- value2 = (char) Wire.read(); // Read from ANALOGUE_IN_0_HIGH register
.
Note : When bytes are read from or written to the Slave device, the internal pointer 'bRegisterPointer' to the bRegisterArray[] array is automatically incremented. Once it reaches the upper most limit of the array it will wrap to the beginning position. ie bRegisterPointer = 0;
Attachments
Step 3: Testing Your I2C Slave Device
In order to test the Slave I wrote the code below. Once this sketch is loaded on the Master open the Serial monitor and you will receive one second updates of the value of the digital I/O on the Slave along with the value from ADC A0.
During execution the programme reads the voltage value of the Pot via A0 and writes this 0...1023 value scaled to 0...255 to Slave PWM 3 to control the brightness of Led 1 (See Pic 1 above).
Once the value of the Slave pot exceeds 2.5v Led 2 is also turned on.
Note : The one second updates are implemented via a call to delay(1000);, as this is a blocking call it will slow down the response of the change in pot position to Led 1 intensity change.
Attachments
Step 4: Shrinking Your Design
Finally, if you feel the Arduino PCB is too cumbersome you could always reduce the form factor of your design and follow the above circuit.
Here the Arduino design has been cut down to 'bare bones' and uses the ATMega328P microcontroller programmed via the bootloader and an FTDI device.
See Instructable 'Programming the ATTiny85, ATTiny84 and ATMega328P : Arduino As ISP' for further details.
.
I also included a copy of the Arduino Uno pinout for reference.
Step 5: Conclusion
In conclusion the use of a second Arduino (ATMega328P) to expand your I/O can be very handy especially if you need a combination of I/O (DI, DO, PWM, AI) and are limited for space. In general I found the response of this design consistent and reliable.
However, if speed of response is required this is not an optimal distribution of I/O, given the Slave unit needs to be polled via the I2C bus running at 100KHz. In this case it would be better to assign 'real time' I/O to the Master and any 'slow time' I/O to the Slave.
Step 6: References
Arduino R3 Pinout
Arduino Uno R3 Circuit Diagram
Arduino official pin mapping
Details on omitting the 10K pull ups on I2C lines
Good source of information on I2C Slaves
Arduino Wire Library Reference
Arduino ADC
.
.
.