22
Investigation of I2C Slave Code on Arduino NOTE – this is my first attempt at writing I2C code, or writing ATMega assembly code. The code was written for this investigation, and is largely untested and probably flakey. Use at your own risk. Summary This note describes an investigation into the performance of an Arduino as an I2C slave, using different software solutions. The investigation was primarily an exercise to learn about the different tools and debugging approaches for the 8-bit AVR microcontrollers and communications. It also provided useful insight into the operation of the I2C protocol, the AVR implementation of the I2C protocol, and the operation of the Wire library. Please note – this is not about which approach is better or worse. The aim was to understand & quantify the relative advantages & disadvantages of each approach. Question When using an Arduino as an I2C slave, what are the advantages / disadvantages of coding the I2C routines: 1. In C code and using the Wire library (using the Arduino environment) 2. In C code using hand-coded I2C routines (using the Arduino environment) 3. In Assembler (using Atmel Studio 7, so bare metal - no Arduino environment). Use Case The use case for the I2C slave device will impact the relative advantages of the above coding approaches. In this use case, the aim is to automatically keep a block of memory synchronised between the I2C master & slave. The synchronisation occurs in the background, and is transparent to applications running on either device. In particular – neither device is blocked by the sync process, and the I2C comms should operate as close to max speed as possible. This use case is not particularly suited to the philosophy behind the Wire library. In this trial, the data sync process comprised: - triggered once per second - copy two bytes of data from Master to Slave - copy two bytes of data from Slave to Master Equipment Used Slave device: Arduino Nano or Arduino Mini (i.e. ATMega328P, 5V, 16MHz) Logic Analyser: Saleae clone device – 8 Channel, 24MHz capture, streamed to SigRok / PulseView

Investigation of I2C Slave Code on Arduino

  • Upload
    others

  • View
    6

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Investigation of I2C Slave Code on Arduino

Investigation of I2C Slave Code on Arduino

NOTE – this is my first attempt at writing I2C code, or writing ATMega assembly code. The code was written for thisinvestigation, and is largely untested and probably flakey. Use at your own risk.

Summary

This note describes an investigation into the performance of an Arduino as an I2C slave, using different software solutions.

The investigation was primarily an exercise to learn about the different tools and debugging approaches for the 8-bit AVR microcontrollers and communications.

It also provided useful insight into the operation of the I2C protocol, the AVR implementation of theI2C protocol, and the operation of the Wire library.

Please note – this is not about which approach is better or worse. The aim was to understand & quantify the relative advantages & disadvantages of each approach.

Question

When using an Arduino as an I2C slave, what are the advantages / disadvantages of coding the I2C routines:

1. In C code and using the Wire library (using the Arduino environment)2. In C code using hand-coded I2C routines (using the Arduino environment)3. In Assembler (using Atmel Studio 7, so bare metal - no Arduino environment).

Use Case

The use case for the I2C slave device will impact the relative advantages of the above coding approaches.In this use case, the aim is to automatically keep a block of memory synchronised between the I2C master & slave. The synchronisation occurs in the background, and is transparent to applications running on either device.In particular – neither device is blocked by the sync process, and the I2C comms should operate as close to max speed as possible.

This use case is not particularly suited to the philosophy behind the Wire library.

In this trial, the data sync process comprised:- triggered once per second- copy two bytes of data from Master to Slave- copy two bytes of data from Slave to Master

Equipment Used

Slave device: Arduino Nano or Arduino Mini (i.e. ATMega328P, 5V, 16MHz)Logic Analyser: Saleae clone device – 8 Channel, 24MHz capture, streamed to SigRok / PulseView

Page 2: Investigation of I2C Slave Code on Arduino

Measurement

The code timings were measured in two places:1. Main loop not executing, and2. I2C routines executing.

The difference between the two is the overhead imposed by the libraries, compiler & interrupt handling normally invisible to the developer.

The main code loop was implemented as a tight loop toggling pin D2. In C, this gave a square pulsestream with a cycle time of 0.75uSec, i.e. an on-time (or off-time) of 0.375uSec or 6 CPU cycles.This pulse stream was monitored on the logic analyser, and the ‘Time out of main loop’ calculated as the time between the D2 transitions either side of the interrupt, minus 0.375uSec

The handling routine code was measured by toggling pin D3 on entering the routine, and toggling D3 again immediately before returning from the routine.

Summary of Results

The results showed that:- Assembly code was approx 2x as fast as dedicated C code (which should scale with data size)- Assembly code was approx 3x as fast as C & Wire library code (which should scale with data size)

In addition, the Wire library has a significant overhead of 20us Master-Write or 40us Master-Read (copying data between buffers, initialising, etc.) which will not scale linearly with data size. The Master-Read did copy 32 bytes into the Wire buffers – slightly mean as only 2 bytes were actually tranferred back to the Master.

The Wire library is the only approach which shows significant clock stretching on the I2C bus.

The Wire library added clock stretching:- 41us of clock stretching during its response to SLA+R,- 6us of clock stretching during slave write operation

Other notes

The trace diagrams show some unexplained pauses in main loop execution – probably due to other Arduino background interrupts such as Timer 0 or Timer 1 or extra main-loop code. Given the consistency of I2C timing results, I assume they are not impacted.

Assembler Hand-Coded C C and Arduino Libraries.Time out of Main Loop Time in Interrupt Code Time out of Main Loop Time in Interrupt Code Time out of Main Loop Time in Interrupt Code

Functions: Clock Cycles uSec Clock Cycles uSec Clock Cycles uSec Clock Cycles uSec Clock Cycles uSec Clock Cycles uSec

SLA+W 40 2.5 28 1.774 61 3.825 18 1.125 94 5.875Master Write +ACK 40 2.5 29 1.823 80 5 37 2.333 128 8Master Write + NACK 40 2.5 28 1.75 79 4.925 36 2.25 128 8Reading data from buffer 328 20.5 112 7.03Total 120 7.5 85 5.347 220 13.75 91 5.708 678 42.375 112 7.03

SLA+R 40 2.5 29 1.833 81 5.042 38 2.375 768 48 605 37.833Master Read + ACK 40 2.5 29 1.83 79 4.958 36 2.25 129 8.083Master Read + NACK 36 2.25 24 1.5 67 4.167 24 1.5 102 6.375Total 116 7.25 82 5.163 227 14.167 98 6.125 999 62.458 605 37.833

Page 3: Investigation of I2C Slave Code on Arduino

Result for Wire Library

Advantages:- Simplest code. Quickest to write, debug, and reuse.- Less than 1 page of code, total.- Robust code, intended to be safe / hassle-free for inexperienced developers

- Each I2C bus event takes about 8us out of main-loop processing to handle

Disadvantages:- Slowest to execute, added clock stretching- Rigid design – e.g. copying data between buffers whether this is requried by the app or not

See Appendix A for code.

The timing below does illustrate:- how the I2C Slave communications / Wire library code works,- when internal Wire library code runs (interrupt driven) and when the Wire callbacks are invoked,- which parts of the Wire processing are / aren’t visible to the developer- and how much processing overhead is consumed by the I2C code.

Analyser trace for the Wire library communications.

C and Arduino Libraries.Time out of Main Loop Time in Interrupt Code

Functions: Clock Cycles uSec Clock Cycles uSec

SLA+W 94 5.875Master Write +ACK 128 8Master Write + NACK 128 8Reading data from buffer 328 20.5 112 7.03Total 678 42.375 112 7.03

SLA+R 768 48 605 37.833Master Read + ACK 129 8.083Master Read + NACK 102 6.375Total 999 62.458 605 37.833

Page 4: Investigation of I2C Slave Code on Arduino

Result for C Code

Advantages- 50% faster than wire library, and avoid Wire overheads (e.g. copying buffers)- No clock stretching by slave, so comms operating at full speed (100kbits/sec)- Takes about 5us out of the main loop to process each I2C event- Consistent timings- C is easier to write / debug / reuse than assembler

- 6 pages of (commented) code.

Disadvantages- not as robust & well-tested as Wire library.- not as easy to use / reuse as Wire library- 100% slower than assembler- C imposes overhead in calling / returning from interrupts

Code approach:- uses SWITCH statement on TWSR status value to execute state-specific code.

See Appendix B for the code

Analyser trace for the C code I2C comms:

Hand-Coded CTime out of Main Loop Time in Interrupt Code

Functions: Clock Cycles uSec Clock Cycles uSec

SLA+W 61 3.825 18 1.125Master Write +ACK 80 5 37 2.333Master Write + NACK 79 4.925 36 2.25Reading data from bufferTotal 220 13.75 91 5.708

SLA+R 81 5.042 38 2.375Master Read + ACK 79 4.958 36 2.25Master Read + NACK 67 4.167 24 1.5Total 227 14.167 98 6.125

Page 5: Investigation of I2C Slave Code on Arduino

Result for Assembler Code

Advantages:- 2x faster than C code, and 3x faster than Wire library- No additional timing overhead (compared to Wire library)- No clock stretching, so comms operating at full speed (100kbits/sec)- Takes about 2.5uSec out of the main loop to process each I2C event- Timings are consistent

Disadvantages- Approx 10 pages of (commented) code- Tricky to write / debug / reuse (approx 12 hours to debug!!)- Makes exclusive use of some resources (e.g. X register) to avoid stack operations – so probaby can’t integrate with C code without losing some efficiency- Did I mention that it was time consuming to debug

Code Approach- Use upper 5 bits of TWSR status as lower byte of jump value, which gives us 8 program words toexecute state-specific code (including jump out) in a 256-word jump table.

See Appendix C for Assembly Code.

Analyser trace for assembler code of I2C Slave:

AssemblerTime out of Main Loop Time in Interrupt Code

Functions: Clock Cycles uSec Clock Cycles uSec

SLA+W 40 2.5 28 1.774Master Write +ACK 40 2.5 29 1.823Master Write + NACK 40 2.5 28 1.75Reading data from bufferTotal 120 7.5 85 5.347

SLA+R 40 2.5 29 1.833Master Read + ACK 40 2.5 29 1.83Master Read + NACK 36 2.25 24 1.5Total 116 7.25 82 5.163

Page 6: Investigation of I2C Slave Code on Arduino

Appendix A – Wire library code for I2C Slave

NOTE – this is my first attempt at writing I2C code, or writing ATMega assembly code. The code was written for this investigation, and is largelyuntested and probably flakey. Use at your own risk.

(Comments pretty much not required – the code documents itself. A testament to the simplicity of the Wire library approach).

#include <Wire.h>

// Blocks of data to be synchronised with I2C Master.// 32 bytes is max that Wire can handle. // This is a bit mean, as we are only sending / receiving 2 bytes.

volatile byte i2cReadBuffer[32];byte i2cWriteBuffer[32];

void setup() { pinMode(2, OUTPUT); // Configure pins pinMode(3, OUTPUT); i2cWriteBuffer[0] = 0x12; // Put debug data into the buffers i2cWriteBuffer[1] = 0x34; Wire.begin(0x10); // Initialise the I2C comms Wire.onReceive(receiveEvent); // Register events for Master send & Slave receive. Wire.onRequest(requestEvent); }

void receiveEvent(uint8_t numBytes){ PIND |= 1<<3; // Start handling I2C code

uint8_t bufferPosition = 0; while (Wire.available() > 0) // Keep reading until no more data. No check on 32 byte limit. i2cReadBuffer[bufferPosition++] = Wire.read();

PIND |= 1<<3; // Stop handling I2C code}

void requestEvent(){ PIND |= 1<<3; // Start handling I2C code

Wire.write(&i2cWriteBuffer[0], 32); // Write 32 bytes of data (a bit mean – only need 2)

PIND |= 1<<3; // Stop handling I2C code}

void loop() { PIND |= 1<<2; // Toggle Pin D2 so that we can see when the main loop is / isn’t runing}

Page 7: Investigation of I2C Slave Code on Arduino

Appendix B – C Code for I2C Slave

NOTE – this is my first attempt at writing I2C code, or writing ATMega assembly code. The code was written for this investigation, and is largelyuntested and probably flakey. Use at your own risk.

/** Code to manage a slave I2C * * First part is code, written in C, * but written to be easily translated into ASM * * Second part is setup() and loop() to demo the I2C code * using switches & LEDs to share memory with an I2C Master. */

/************************************************************************************* * * I2C Slave Address * ************************************************************************************/

#define S_ADDR 0x10#define SLA_R ((S_ADDR<<1) | 0x01)#define SLA_W (S_ADDR<<1)

/************************************************************************************* * * I2C Data Buffer * ************************************************************************************/

/** Or we could define this one bigger, to match the Master buffer? */#define I2C_BUFFER_SIZE 4

/** How the buffer will be segmented: */#define I2C_START_WR_POSITION 0#define I2C_MAX_WR_POSITION 1#define I2C_START_RD_POSITION 2#define I2C_MAX_RD_POSITION 3

/** Define I2C buffer */uint8_t i2cBuffer[I2C_BUFFER_SIZE];uint8_t i2cWritePosition; // Write is Master->Slave (i.e. writing to our shared memory)uint8_t i2cReadPosition; // Read is Slave->Master (i.e. reading from our shared memory)

/************************************************************************************* * * I2C Control Commands * ************************************************************************************/

// Don't know if 'READY' needs TWINT or not. It seems not!#define I2C_CR_READY (1<<TWEA) | (1<<TWEN) | (1<<TWIE)#define I2C_CR_CONT_ACK (1<<TWINT) | (1<<TWEA) | (1<<TWEN) | (1<<TWIE)#define I2C_CR_CONT_NACK (1<<TWINT) | (1<<TWEN) | (1<<TWIE)

/************************************************************************************* * * I2C Initialisation Code *

Page 8: Investigation of I2C Slave Code on Arduino

************************************************************************************/

void initI2CSlave(){ /** Set slave address */ TWAR = (S_ADDR <<1); TWAMR = 0; // All zero means test address against ALL bits. /** Clear the status register (prescaler bits), so that we don't need to mask it later. */ TWSR = 0;

/** Reset our i2cBuffer pointers */ i2cWritePosition = I2C_START_WR_POSITION; i2cReadPosition = I2C_START_RD_POSITION; /** Set I2C status flags */ TWCR = I2C_CR_CONT_ACK;}

ISR(TWI_vect){ i2cSlaveInterrupt();}

/************************************************************************************* * * I2C Interrupt Code * ************************************************************************************/

/** Handle I2C interrupt */void i2cSlaveInterrupt(){ // Toggle Pin D3 PIND |= 1<<3; uint8_t i2cStatus = (TWSR); // Don't bother to mask out the prescalar bits. uint8_t temp;

switch (i2cStatus) { case 0x00 : // Bus error - go to 'waiting for START' i2cWritePosition = I2C_START_WR_POSITION; i2cReadPosition = I2C_START_RD_POSITION;

// temp = I2C_CR_READY; // No TWINT temp = I2C_CR_CONT_ACK; // Send a TWINT to clear the error? TWCR = temp; // It's up to the Master to send a STOP

PIND |= 1<<3; return;

case 0x60 : // SLA_W has been received, ACK sent temp = I2C_CR_CONT_ACK; // Carry on, with ACK TWCR = temp; PIND |= 1<<3;

Page 9: Investigation of I2C Slave Code on Arduino

return;

case 0x68 : // Lost arbitration, SLA_W received, ACK sent. temp = I2C_CR_CONT_ACK; // Carry on, with ACK TWCR = temp; PIND |= 1<<3; return;

case 0x70 : // General call received, ACK sent temp = I2C_CR_CONT_ACK; // Carry on, with ACK TWCR = temp; PIND |= 1<<3; return;

case 0x78 : // Lost arbitration, general call received, ACK sent temp = I2C_CR_CONT_ACK; // Carry on, with ACK TWCR = temp; PIND |= 1<<3; return;

case 0x80 : // SLA+W, Data received, ACK sent. /** Last time I sent an ACK, so I am happy to receive this byte. */ temp = TWDR; // Get the data i2cBuffer[i2cWritePosition] = temp; // Into the buffer i2cWritePosition++; // and get ready for another READ

temp = I2C_CR_CONT_ACK; if (i2cWritePosition == I2C_MAX_WR_POSITION) temp = I2C_CR_CONT_NACK;

TWCR = temp; PIND |= 1<<3; return;

case 0x88 : // SLA+W, Data received, NACK sent temp = TWDR; // Get the data i2cBuffer[i2cWritePosition] = temp; // Into the buffer // Don't bother incrementing readPosition

/** RJMP reset() */ i2cWritePosition = I2C_START_WR_POSITION; // Reset i2cReadPosition = I2C_START_RD_POSITION; temp = I2C_CR_CONT_ACK; // Set up ACK ready for next START TWCR = temp; PIND |= 1<<3; return;

case 0x90 : // General call, Data received, ACK sent /** RJMP case 0x80 */ /** Last time I sent an ACK, so I am happy to receive this byte. */ temp = TWDR; // Get the data i2cBuffer[i2cWritePosition] = temp; // Into the buffer i2cWritePosition++; // and get ready for another READ

Page 10: Investigation of I2C Slave Code on Arduino

temp = I2C_CR_CONT_ACK; if (i2cWritePosition >= I2C_MAX_WR_POSITION) // If the next write is our last temp = I2C_CR_CONT_NACK; // Then respond to it with a NACK

TWCR = temp; PIND |= 1<<3; return;

case 0x98 : // General call, Data received, NACK sent /** RJMP case 0x88 */ temp = TWDR; // Get the data i2cBuffer[i2cWritePosition] = temp; // Into the buffer // Don't bother incrementing writePosition

/** RJMP reset() */ i2cWritePosition = I2C_START_WR_POSITION; // Reset i2cReadPosition = I2C_START_RD_POSITION;

temp = I2C_CR_CONT_ACK; // Set up ACK ready for next START TWCR = temp; // When we send a NACK, we stop detecting I2C events such as STOP or RESTART // The next thing we will interrupt on is SLA+R/W with our address. PIND |= 1<<3; return;

case 0xA0 : // STOP or REPEATED START received. /** RJMP reset() */ i2cWritePosition = I2C_START_WR_POSITION; // Reset i2cReadPosition = I2C_START_RD_POSITION; temp = I2C_CR_CONT_ACK; // Set up ACK ready for next START TWCR = temp; PIND |= 1<<3; return;

case 0xA8 : // SLA_R has been received, ACK sent /** We sent an ACK, so we must have at least one byte of data to send */ temp = i2cBuffer[i2cReadPosition]; // Buffer[] -> I2C TWDR = temp; if (i2cReadPosition < I2C_MAX_RD_POSITION) i2cReadPosition++; // Next read position from our buffer

temp = I2C_CR_CONT_ACK; // Our ACK/NACK is irrelevant. TWCR = temp; // Master manages ACK/NACK PIND |= 1<<3; return;

case 0xB0 : // Arbitration lost, SLA_R received, ACK sent /** RJMP case 0xA8 */ /** We sent an ACK, so we must have at least one byte of data to send */ temp = i2cBuffer[i2cReadPosition]; // Buffer[] -> I2C TWDR = temp; if (i2cReadPosition < I2C_MAX_RD_POSITION) i2cReadPosition++; // Next read position from our buffer

temp = I2C_CR_CONT_ACK; // Our ACK/NACK is irrelevant. TWCR = temp; // Master manages ACK/NACK PIND |= 1<<3;

Page 11: Investigation of I2C Slave Code on Arduino

return;

case 0xB8 : // Data transmitted, ACK received. /** RJMP case 0xA8 */ /** We sent an ACK, so we must have at least one byte of data to send */ temp = i2cBuffer[i2cReadPosition]; // Buffer[] -> I2C TWDR = temp; if (i2cReadPosition < I2C_MAX_RD_POSITION) i2cReadPosition++; // Next read position from our buffer

temp = I2C_CR_CONT_ACK; // Our ACK/NACK is irrelevant. // Unless we want to take advantage of state 0xC8 TWCR = temp; // Master manages ACK/NACK PIND |= 1<<3; return;

case 0xC0 : // Data transmitted, NACK received /** RJMP reset() */ i2cWritePosition = I2C_START_WR_POSITION; // Reset i2cReadPosition = I2C_START_RD_POSITION; temp = I2C_CR_CONT_ACK; // Set up ACK ready for next START TWCR = temp; PIND |= 1<<3; return;

case 0xC8 : // Data transmitted with TWEA=0, ACK received. /** This is when Master is reading data from us (so Master controls ACK/NACK) * We have no way to indicate 'no more data' * but we cleared the TWEA (ACK enable) anyway (would send NACK, but we aren't doing ACK/NACK) * So Slave sends 0xFF to master and waits - with TWEA clear. * We won't respond to a SLA+R/W with TWEA clear. This feels unreliable. * We will have to detect 0xC0 when the Master finally sends a NACK, or 0xA0 STOP/RESTART * to trigger us to set TWEA again? * What is the alternative? Send same data again, with TWEA enabled, until master stops requesting data * If I don't get a STOP or NAK, and I get a START (i.e. RESTART) then I want to respond to it. */ temp = I2C_CR_CONT_ACK; // Set up ACK ready for next START TWCR = temp; PIND |= 1<<3; return;

case 0xF8 : // Write to TWCR while TWINT low /** We are interrupt driven, so shoudn't ever get here. But... */ PIND |= 1<<3; return; } // End of switch (i2cStatus)}

Page 12: Investigation of I2C Slave Code on Arduino

/************************************************************************************* * * Basic Loop to test Slave software. * ************************************************************************************/

/** Main loop toggles D02 as fast as it can * Interrupt routine toggles D03 on entry & exit. */

void setup() { /** Set up D2 & D3 as outputs for the logic analyser to track the code. */

pinMode(02, OUTPUT); pinMode(03, OUTPUT); /** Set up the I2C Slave */ initI2CSlave();}

void loop() { // Toggle pin D2 PIND |= 1<<2;}

Page 13: Investigation of I2C Slave Code on Arduino

Appendix C – Assembly Code for I2C Slave

NOTE – this is my first attempt at writing I2C code, or writing ATMega assembly code. The code was written for this investigation, and is largelyuntested and probably flakey. Use at your own risk.

(Please ignore inconsistent annotation styles).

I2C_Asm.asm

; Assembly code to manage I2C interface (SLAVE); Use case: share a block of memory invisibly with a slave device.; Call i2cSync() every 10ms or so, to initiate sync. (every 1 second in this example);

#include "./i2c_macros.asm"#include "./i2c_variables.asm"#include "./i2c_interrupt_vectors.asm"#include "./i2c_interrupt_code.asm"#include "./i2c_initialise.asm"

; Main Looploop:

SBI PIND, 5 // DEBUG - Toggle D5

// Actually there was a bit more code in this main loop, but it’s not relevant to the example,// so it has been removed for clarity.// However, it did affect the main loop timings.

rjmp loop

i2c_macros.asm

/** Macro to set the Stack Pointer to end of ram * Input Parameters: none */.macro SET_STACK LDI R16, LOW(RAMEND) OUT SPL, R16 LDI R16, HIGH(RAMEND) OUT SPH, R16.endmacro

Page 14: Investigation of I2C Slave Code on Arduino

i2c_Variables.asm

.DSEG

.ORG 0x200; Defines where our data starts in RAM; The i2c buffer (aka shared memory) needs to be byte-aligned so that it starts with low byte = 0x00

; This allows us to access up to 256 bytes of buffer without worrying about the high byte.

.EQU i2cWriteBufferLength = 2 ; Size of 'Write' buffer - minimum 1 byte

.EQU i2cReadBufferLength = 2 ; Size of 'Read' buffer - minimum 1 byte

.EQU i2cBufferLength = i2cWriteBufferLength + i2cReadBufferLength

i2cBuffer : .BYTE i2cBufferLength; Block of memory which we share via I2C.

.EQU i2cWriteStart = 0 ; Offset into i2cBuffer for start of write buffer

.EQU i2cWriteEnd = i2cWriteStart + i2cWriteBufferLength - 1 ; End of write buffer

.EQU i2cReadStart = i2cWriteEnd + 1 ; Start of read buffer

.EQU i2cReadEnd = i2cReadStart + i2cReadBufferLength - 1 ; End of read buffer

.EQU i2cReady = 0

.EQU i2cBusySLAW = 1

.EQU i2cBusySLAR = 2

.EQU i2cBusyGenW = 3

.EQU i2cBusyGenR = 4

.EQU i2cError = 255

.EQU i2cErrorOnBus = 254

.EQU i2cErrorReadOverrun = 253

.EQU i2cErrorTWINTClear = 252

.EQU i2cErrorInvalidTWSR = 251

#define I2C_CR_READY ((1<<TWEA) | (1<<TWEN) | (1<<TWIE))#define I2C_CR_CONT_ACK ((1<<TWINT) | (1<<TWEA) | (1<<TWEN) | (1<<TWIE))#define I2C_CR_CONT_NACK ((1<<TWINT) | (1<<TWEN) | (1<<TWIE))

/** REGISTERS */

; Reserve some registers to speed things up; Low 16 registers (R0-R15) - can't use immediate commands

.DEF zero = R0 ; Use for ADC XH / YH / ZH

.DEF arg0 = R1 ; Use to pass arguement(s) during function call / return

.DEF arg1 = R2 ; Use to pass arguement(s) during function call / return

; Used by softwarePWM - can swap out to RAM and re-use if required..DEF MaskPORTB = R3 ; Not critical to store this bitmask in registers .DEF MaskPORTC = R4 ; Lets see how much space we have..DEF MaskPORTD = R5

; Reserved for use within interrupt routines only.; Only one interrupt level - leave interrupts disabled during interrupt routines.

.DEF currentPWMPin = R12 ; Reserved for use within softPWM INTERRUPTS

.DEF firstPWMPin = R13

.DEF intTemp1 = R14

.DEF tempSREG = R15

; High 10 registers (R16-R25) - can use immediate commands.DEF temp0 = R16 ; Used outside of INTERRUPTS in main loop.DEF temp1 = R17.DEF temp2 = R18.DEF temp3 = R19

; Used within INTERRUPTS

.DEF intTemp = R23 ; Use within interrupt routine only; Also hold i2cCurrentPosition when NOT in the interrupt.

; Top 6 registers (R26-R31) - use for indirect addressing everywhere!

Page 15: Investigation of I2C Slave Code on Arduino

i2c_interrupt_vectors.asm

.cseg

.org 0 rjmp start

.org 0x030rjmp i2cInterrupt

Page 16: Investigation of I2C Slave Code on Arduino

i2c_interrupt_code.asm

/** This code jump table (i2cJumpTable) MUST start in a memory location where the low byte = 0 * as we need all 256 jump locations (strictly, we need 32 contiguous groups of 8-word code blocks) * We will call it by setting: * - ZH = High(i2cJumpTable) * - ZL = TWSR & 0xF8 (or don't bother with the mask if we know the prescale bits = 0) * - so every 8 words will be the code to handle a specific I2C interrupt state * - i.e. the code MUST fit within 8 words. * * Before jumping to the table, X will be configured to point to the i2cBuffer current location * the i2cBuffer is basically our shared memory - both READ and WRITE. * - XH = High(i2cBuffer), which never changes (buffer is byte-aligned and <256 bytes) * - XL = Low(i2cBuffer), basically our index into the buffer * at the moment - I am not using X anywhere else, so it is dedicated to i2cInterrupt and doesn't need to be push/popped. * * At the end of the table code: * - load a 'state' value into GPIOR2 if you want to set the state * (could remove this to speed up the code/free up a register) * (or if I need an extra register, change the rjmp to i2cSetState to store that value in RAM) * - load the TWCR value into intTemp (there isn't room to do write it to TWCR within the 8-word jump table, so it is done 'at the end') * - rjmp to i2cAfterJump * * i2cAfterJump then manages finishing off after the jump table * - writes the intTemp value into TWCR (because there wasn't room to do that within the jump table code) * - restore Z from the stack * - restore the status register and RETI. * (or jump to i2cNoWriteRETI if we don’t need to write TWCR – makes things a bit quicker) * * Store our (enumerated) status in GPIOR2 for debugging / monitoring purposes, as GPIOR2 is easy/cheap to access * and assume GPIOR2 is only used for the I2C interrupt code. */

.ORG (0x100) // Base address of i2c interrupt jump table

i2cJumpTable : // 0x00 - Bus Error// RESET

LDI intTemp, i2cErrorOnBus // Set i2c state to 'Bus Error'OUT GPIOR2, intTemp

LDI intTemp, I2C_CR_CONT_ACK // Setting TWINT to continue, and ACK so that we respond to our SLArjmp i2cAfterJump

/**********************************************************************************************/.ORG (i2cJumpTable) + 0x08 // Unused - SLAVE shouldn't ever call this.

LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0x10 // Unused - SLAVE shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0x18 // Unused - SLAVE shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0x20 // Unused - SLAVE shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0x28 // Unused - SLAVE shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0x30 // Unused - SLAVE shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0x38 // Unused - SLAVE shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'

Page 17: Investigation of I2C Slave Code on Arduino

OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0x40 // Unused - SLAVE shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0x48 // Unused - SLAVE shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0x50 // Unused - SLAVE shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0x58 // Unused - SLAVE shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

/**********************************************************************************************/

.ORG (i2cJumpTable) + 0x60 // SLA_W received, ACK sent// Set up write buffer & Continue

LDI intTemp, i2cBusySLAW // Update the state to SLA+WOUT GPIOR2, intTemp

LDI XL, i2cWriteStart // Set buffer pointer to start of write buffer

LDI intTemp, I2C_CR_CONT_ACK // Get ready to ACK the next byte writtenCPI XL, i2cWriteEndBRLO i2c_0x60_Jmp // If XL < end of the write buffer, use the ACKLDI intTemp, I2C_CR_CONT_NACK // ELSE NACK the next byte written

i2c_0x60_Jmp :rjmp i2cAfterJump

.ORG (i2cJumpTable) + 0x68 // Lost arb, SLA_W received, ACK sent// Continue

/** Copy of 0x60 - quicker than RJMP */ LDI intTemp, i2cBusySLAW // Update the state to SLA+WOUT GPIOR2, intTemp

LDI XL, i2cWriteStart // Set buffer pointer to start of write buffer

LDI intTemp, I2C_CR_CONT_ACK // Get ready to ACK the next byte writtenCPI XL, i2cWriteEndBRLO i2c_0x68_Jmp // If XL < end of the write buffer, use the ACKLDI intTemp, I2C_CR_CONT_NACK // ELSE NACK the next byte written

i2c_0x68_Jmp :rjmp i2cAfterJump

.ORG (i2cJumpTable) + 0x70 // General call received, ACK sent// Continue

/** Copy of 0x60 - quicker than RJMP */ LDI intTemp, i2cBusyGenW // Update the state to General Call +WOUT GPIOR2, intTemp

LDI XL, i2cWriteStart // Set buffer pointer to start of write buffer

LDI intTemp, I2C_CR_CONT_ACK // Get ready to ACK the next byte writtenCPI XL, i2cWriteEndBRLO i2c_0x70_Jmp // If XL < end of the write buffer, use the ACKLDI intTemp, I2C_CR_CONT_NACK // ELSE NACK the next byte written

i2c_0x70_Jmp :rjmp i2cAfterJump

.ORG (i2cJumpTable) + 0x78 // Lost arb, general call received, ACK sent// Continue

/** Copy of 0x60 - quicker than RJMP */ LDI intTemp, i2cBusyGenW // Update the state to General Call +WOUT GPIOR2, intTemp

Page 18: Investigation of I2C Slave Code on Arduino

LDI XL, i2cWriteStart // Set buffer pointer to start of write buffer

LDI intTemp, I2C_CR_CONT_ACK // Get ready to ACK the next byte writtenCPI XL, i2cWriteEndBRLO i2c_0x78_Jmp // If XL < end of the write buffer, use the ACKLDI intTemp, I2C_CR_CONT_NACK // ELSE NACK the next byte written

i2c_0x78_Jmp :rjmp i2cAfterJump

.ORG (i2cJumpTable) + 0x80 // SLA+W rx, data rx, ACK sent// We can only be here if we were happy to rx this byte (ACK)// So write to buffer & continue.

LDS intTemp, (TWDR) /* 2 */ // Load the data into current buffer write position.ST X+, intTemp // and increment buffer pointer.

LDI intTemp, I2C_CR_CONT_ACK // Get ready to ACK next sent byteCPI XL, i2cWriteEndBRLO i2c_0x80_Jmp // If XL < end of the write buffer, use the ACKLDI intTemp, I2C_CR_CONT_NACK // ELSE NACK next sent byte

i2c_0x80_Jmp :rjmp i2cAfterJump

.ORG (i2cJumpTable) + 0x88 // SLA+W rx, data rx, NACK sent// This is the last byte we will receive// So write to buffer & continue.

LDS intTemp, (TWDR) /* 2 */ // Load the data into current buffer write position.ST X+, intTemp // incr pointer so that it's the same whether we end due to Slave NACK,

// or Master STOP.

LDI intTemp, i2cReady // Reset our state to 'Ready'OUT GPIOR2, intTemp

LDI intTemp, I2C_CR_CONT_ACK // Get ready to ACK on SLA+W/R

rjmp i2cAfterJump

.ORG (i2cJumpTable) + 0x90 // General call, data rx, ACK sent// We can only be here if we were happy to rx this byte (ACK)

/** Same as 0x80 */ // So write to buffer & continue.LDS intTemp, (TWDR) /* 2 */ // Load the data into current buffer write position.ST X+, intTemp // and increment buffer pointer.

LDI intTemp, I2C_CR_CONT_ACK // Get ready to ACK next sent byteCPI XL, i2cWriteEndBRLO i2c_0x90_Jmp // If XL < end of the write buffer, use the ACKLDI intTemp, I2C_CR_CONT_NACK // ELSE NACK next sent byte

i2c_0x90_Jmp :rjmp i2cAfterJump

.ORG (i2cJumpTable) + 0x98 // General call, data rx, NACK sent// This is the last byte we will receive

/** Same as 0x88 */ // So write to buffer & continue.LDS intTemp, (TWDR) /* 2 */ // Load the data into current buffer write position.ST X+, intTemp // incr pointer so that it's the same whether we end due to Slave NACK,

// or Master STOP.

LDI intTemp, i2cReady // Reset our state to 'Ready'OUT GPIOR2, intTemp

LDI intTemp, I2C_CR_CONT_ACK // Get ready to ACK on SLA+W/R

rjmp i2cAfterJump

.ORG (i2cJumpTable) + 0xA0 // STOP or REPEATED START received// Reset

LDI intTemp, i2cReady // Reset our state to 'Ready'OUT GPIOR2, intTemp

LDI intTemp, I2C_CR_CONT_ACK // Get ready to ACK

Page 19: Investigation of I2C Slave Code on Arduino

rjmp i2cAfterJump

.ORG (i2cJumpTable) + 0xA8 // SLA+R received, ACK sent// Initialise reading & continue

LDI intTemp, i2cBusySLAR // Set our state to 'SLA+R'OUT GPIOR2, intTemp

LDI XL, i2cReadStart // Set our buffer pointer to the start of the READ buffer.

LD intTemp, X+ // Load the byte from the current buffer position into TWDRSTS (TWDR), intTemp /* 2 */ // and increment the current buffer position

LDI intTemp, I2C_CR_CONT_ACK // Always set ACK on reads from our buffer, as Master controls ACK/NACK anyway.

rjmp i2cAfterJump

.ORG (i2cJumpTable) + 0xB0 // Arb lost, SLA+R received, ACK sent// Initialise reading & continue

/** Same as 0xA8 */LDI intTemp, i2cBusySLAR // Set our state to 'SLA+R'OUT GPIOR2, intTemp

LDI XL, i2cReadStart // Set our buffer pointer to the start of the READ buffer.

LD intTemp, X+ // Load the byte from the current buffer position into TWDRSTS (TWDR), intTemp /* 2 */ // and increment the current buffer position

LDI intTemp, I2C_CR_CONT_ACK // Always set ACK on reads from our buffer, as Master controls ACK/NACK anyway.

rjmp i2cAfterJump

.ORG (i2cJumpTable) + 0xB8 // Data sent to master, ACK received// Continue reading from buffer & sending

LD intTemp, X // Load the byte from the current buffer position into TWDRSTS (TWDR), intTemp /* 2 */ // without incrementing the current buffer position (yet)

LDI intTemp, I2C_CR_CONT_ACK // Always set ACK on reads from our buffer, as Master controls ACK/NACK anyway.

CPI XL, i2cReadEndBRLO i2c_0xB8_Jmp // IF XL < end of the read buffer, just ACKINC XL // ELSE move to the next read locaiton

i2c_0xB8_Jmp :rjmp i2cAfterJump // and then ACK

.ORG (i2cJumpTable) + 0xC0 // Data sent to master, NACK received// Reset

LDI intTemp, i2cReady // Set i2c state to 'Ready'OUT GPIOR2, intTemp

LDI intTemp, I2C_CR_CONT_ACK // Setting TWINT to continue, and ACK so that we respond to our SLArjmp i2cAfterJump

.ORG (i2cJumpTable) + 0xC8 // Data sent to master while TWEA=0, ACK received// So we have no more data, but the master is still pulling data from us// We set up NACK, but the master is driving ACK/NACK, not us.// This shouldn't happen, as we're not setting NACK during SLA+R transfers.

// ResetLDI intTemp, i2cErrorReadOverrun // Set i2c state to 'Error - Read Overrun'OUT GPIOR2, intTemp

LDI intTemp, I2C_CR_CONT_ACK // Setting TWINT to continue, and ACK so that we respond to our SLArjmp i2cAfterJump

/**********************************************************************************************/.ORG (i2cJumpTable) + 0xD0 // Unused - shouldn't ever call this.

LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0xD8 // Unused - shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'

Page 20: Investigation of I2C Slave Code on Arduino

OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0xE0 // Unused - shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0xE8 // Unused - shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

.ORG (i2cJumpTable) + 0xF0 // Unused - shouldn't ever call this.LDI intTemp, i2cErrorInvalidTWSR // Set i2c state to 'Error - Invalid TWSR'OUT GPIOR2, intTemprjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

/**********************************************************************************************/

.ORG (i2cJumpTable) + 0xF8 // Accessing TWCR while TWINT is not set// Shouldn't happen as we're interrupt driven.// Continue.

LDI intTemp, i2cErrorTWINTClear // Set i2c state to 'Error - TWINT clear'OUT GPIOR2, intTemp

rjmp i2cNoWriteRETI // No point in trying to write to TWCR !!

/** This interrupt can be called after 1x 100kHz clk (e.g. after asserting START) * but normally will be called after asserting nearer 10x 100kHz clocks (sending or receiving a byte) * * Response time: not timing-critical, as the ATMega automatically stretches the clock until we respond * but the longer we wait, the slower the comms. */

i2cInterrupt :

/** Push all registers that we will overwrite onto the stack. * At the moment - X is reserved for I2C, so I don't have to PUSH & POP it. */

SBI PIND, 6 // DEBUG - Toggle D6

IN tempSREG, SREG // Save SREG as well

PUSH ZL // Save Z, as we are going to use it for the jump table.PUSH ZH

LDI ZH, HIGH(i2cJumpTable) // Set up Z to point to our jump tableLDS ZL, TWSR // with the low byte read from the i2c interrupt status

// Mask not required because the prescaler bits are zero

IJMP // i2c-state dependent jump.LDI intTemp, I2C_CR_CONT_ACK

i2cAfterJump :STS TWCR, intTemp // Write the i2c control register

i2cNoWriteRETI :

POP ZHPOP ZL

OUT SREG, tempSREG

SBI PIND, 6 // DEBUG - Toggle D6RETI

Page 21: Investigation of I2C Slave Code on Arduino

i2c_initialise.asm

#ifndef F_CPU#define F_CPU 16000000#endif

#define F_I2C 100000#define I2C_PRESCALER 1#define TWSR_VAL 0#define TWBR_VAL ((((F_CPU / F_I2C) / I2C_PRESCALER) - 16) / 2)

#define SL_ADDR 0x10

/** Start of execution */

/**************************************************** * * Basic Initialisation of the microprocessor * **************************************************/

start:CLR zero ; Set register R0 (zero) =0

SET_STACK ; Invoke our macro to set the stack pointer.

/**************************************************** * * Initialise PINS * **************************************************/

initPins :LDI temp0, 0xFC ; 1=Output. So make D2-D7 outputs.OUT DDRD, temp0

LDI temp0, 0x00 ; All of PORT C are inputs.OUT DDRC, temp0

LDI temp0, 0x00 ; All of PORT B are inputs (D8-D15)OUT DDRB, temp0

/**************************************************** * * Initialise our I2C System * **************************************************/

initI2C :LDI temp0, TWBR_VAL // Load the bit-rate register with the pre-calculated value for 100kHzSTS TWBR, temp0

STS TWSR, zero // Set the prescaler to zero (x1)

LDI temp0, (SL_ADDR << 1) // Set the I2C addressSTS TWAR, temp0

STS TWAMR, zero // Set the I2C address mask - detect on all address bits

LDI temp0, I2C_CR_READY // Initialise the I2C systemSTS TWCR, temp0

LDI XH, HIGH(i2cBuffer) // X will point to the i2cBuffer// If we don't use it anywhere else,// then we will never set it anywhere else.

/** Add some junk data to our 'READ' buffer - for debugging */LDI temp0, 0x3CSTS (i2cBuffer), temp0

LDI temp0, 0x5CSTS (i2cBuffer+1), temp0

Page 22: Investigation of I2C Slave Code on Arduino

LDI temp0, 0x7CSTS (i2cBuffer+2), temp0

LDI temp0, 0xC5STS (i2cBuffer+3), temp0

initPost:SEI // And switch interrupts on! Doh!!