Getting Arduino AVR Disassembly with Platformio

You can get a cpp-source-interleaved disassembly listing, when compiling for Arduino AVR under Platformio.

You add these to platformio.ini:

build_flags =
    -save-temps=obj
    -fverbose-asm

Build_type = debug

Then get the c source interleaved disassembly with:

avr-objdump -S firmware.elf > disassembly.txt

A sample of the disassembly output:

...

void loop() 
{
  // Read adc and save result in read-only slave register 1
  i2c_registers[I2C_REG_1] = myadc_read();
     cc2:	0e 94 46 06 	call	0xc8c	; 0xc8c <_Z10myadc_readv>
     cc6:	90 93 21 01 	sts	0x0121, r25	; 0x800121 <i2c_registers+0x3>
     cca:	80 93 20 01 	sts	0x0120, r24	; 0x800120 <i2c_registers+0x2>

  // Spin for 100ms
  delay(100);
     cce:	64 e6       	ldi	r22, 0x64	; 100
     cd0:	70 e0       	ldi	r23, 0x00	; 0
     cd2:	80 e0       	ldi	r24, 0x00	; 0
     cd4:	90 e0       	ldi	r25, 0x00	; 0
     cd6:	0e 94 b5 03 	call	0x76a	; 0x76a <delay>
     cda:	08 95       	ret

...

Arduino I2C Slave Peripheral

Cognizant of a need, I submit this, hoping to add to and improve, the publicly available instruction, on how to layout and code Arduino I2C slave devices. Suggested here is an organisational paradigm, for the coding of I2C slave peripherals under Arduino, and demonstrating this with a Platformio built example.

Structure

On an Atmel328PB microcontroller, we emulate a set of 16-bit peripheral registers, of which each has an 8-bit address. Programmed like common silicon I2C devices, registers can be individually configured for read-only, write-only, or read-write visibility, as seen from the perspective of the I2C master.

/*****************************************************************************
 *
 *                              Global Variables
 *
 *****************************************************************************/
// An array to store register values
int i2c_registers[I2C_NUM_REGS] = {0};

// I2C session persistent register address
uint8_t registerAddr = 0xFF;

In developing an I2C slave application, peripheral functionality must be implemented as data source and data sink calls, and then ingeniously coupled to the above mentioned, emulated device registers.

For very fast running application code, coupling takes place within an I2C interrupt service context. These application calls occur on-the-fly, as wired I2C communication ensues. Link-in too cycle consuming an application call here, and I2C communication timing may be delayed. This, sufficiently that I2C communications becomes non-standards compliant, and fails.

To avoid this, for slower running application code, data source and data sink linkage to emulated peripheral registers, takes place outside I2C service interrupt context, nominally in, or under the task loop scope. During mid access to an emulated register here, it is possible that I2C ISR service routines may be invoked, leading to data corruption. To prevent this, a pair of memory access macros are provided. Employing these in task loop peripheral register data manipulation, ensures atomicity in their access.

I2C Call-back Functions

During program setup, the I2C bus under configuration, is assigned a device address, and call-back routines for the service of wired I2C bus transactions. When an I2C-inbound write occurs, data is consumed and/ or is stored in a register, with code inside the receiveEvent() callback. When an I2C-inbound read occurs, data is application generated, or is retrieved from a register, inside the requestEvent() ISR.

  Wire.begin(I2C_ADDRESS);      // Initialize I2C communication as a slave
  Wire.onReceive(receiveEvent); // Register the receive event handler
  Wire.onRequest(requestEvent); // Register the request event handler

The unpopulated receiveEvent() call-back

An unpopulated receiveEvent() ISR services wired I2C register storage instructions. Not yet linked to any application functionality, it merely provides write access to peripheral register memory.

/*****************************************************************************
 *
 *                               receiveEvent()
 *
 *****************************************************************************/
// Event handler for receiving data from the master, to write to a register
void receiveEvent(int numBytes) 
{ 
  // Read the requested register address
  registerAddr = Wire.read(); 
  
  if (numBytes == 3) 
  {
    // Read the data to write
    uint16_t value = Wire.read() ;        // value LSB
    value |= Wire.read() << 8;            // value MSB

    switch (registerAddr)
    {
      // Comment-out any read-only registers to prevent master write
      case I2C_REG_0:
      case I2C_REG_1: 
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

    I2C_WRITE_DEBUG(registerAddr, value)
  }
}

The unpopulated requestEvent() call-back

An unpopulated requestEvent() ISR services wired I2C register retrieval instructions. Not yet linked to any application functionality, it merely provides read access to peripheral register memory.

/*****************************************************************************
 *
 *                               requestEvent()
 *
 *****************************************************************************/
// Event handler for responding to requests for register contents, from the master
void requestEvent() 
{
  uint16_t value = 0;

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_0:
    case I2C_REG_1:
    case I2C_REG_2:
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

  // Send the data read from
  Wire.write(value & 0xFF); // send LSB
  Wire.write(value >> 8);   // send MSB

  I2C_READ_DEBUG(registerAddr, value);

  return;
}

Read-ability and write-ability

So, we may see from the above, in the unpopulated ISR pair, an I2C master has the ability to write a value to one of a number of memory-only registers, and then read it back from there verbatim. We’ll see later, when we come to adding applications, such simultaneous read-ability and write-ability may not be desirable.

To make a peripheral register read-only, we remove the receiveEvent() ISRs ability to service peripheral register write requests. We do this by commenting-out service routine switch cases that involve the register.

void receiveEvent(int numBytes) 
{

...

    switch (registerAddr)
    {
      // Comment out any read-only registers to prevent master write
      case I2C_REG_0:
      //case I2C_REG_1:  // Make register 1 read-only
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

...

To make a peripheral register write-only, we remove the requestEvent() ISRs ability to service peripheral register read requests. Again, we do this by commenting-out service routine switch cases that involve the register.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_0:
    case I2C_REG_1:
    // case I2C_REG_2:  // Make register 2 write-only
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

Application linkage inside loop()

Now that we have our peripheral registers configured, variously as read and write, read-only, or write-only, we come to linking these to application code. This involves the data sink and data source routines, that you will develop for your peripheral.

Remember, all access to peripheral registers outside the I2C interrupt service context, must employ the atomic register access macros, I2C_ATOMIC_REG_RD() and I2C_ATOMIC_REG_WR(). Again, this is vital to prevent the data corruption that occurs, when I2C ISRs are invoked, during peripheral register access. The macros momentarily ‘lock-out’ the ISRs.

/************************************************************************
 *
 *                                loop()
 *
 ************************************************************************/
void loop() 
{
  {
    uint16_t value = 0;

    // Read adc, and atomically save val in read-only slave register 1
    value = myadc_read();
    // i2c_registers[I2C_REG_1] = value; <- not atomic, instead...
    I2C_ATOMIC_REG_WR(i2c_registers[I2C_REG_1], value);

    // Retrieve contents of slave register 0 atomically, and consume
    // value = i2c_registers[I2C_REG_0]; <- not atomic, instead...
    I2C_ATOMIC_REG_RD(value, i2c_registers[I2C_REG_0]);
    myservo_set_pos(value);
  }

  // Other application tasks.

}

In the above, we see that a logically read-only peripheral register, I2C_REG_1, is fed samples from an onboard ADC, using an application call to myadc_read(). A remotely wired, I2C master may read the stored register data, with calls such as wiringPi’s wiringPiI2CReadReg16().

As well, above, we have an either read and write, or write-only configured peripheral register, I2C_REG_0, feeding position data to an attached servo. It does this via an application call to myservo_set_pos(). A remotely wired, I2C master may write the servo position data, with calls such as wiringPi’s wiringPiI2CWriteReg16().

On-demand, vs polled register access

The above mentioned approach to linking application data sink and source functionality, to underlying peripheral register memory, from outside I2C ISR service context, can be wasteful. Significant cpu cycles are consumed, in the application either constantly polling peripheral registers for incoming data, or frequently updating peripheral registers for fresh outgoing data.

To increase application efficiency, an alternate user code linkage solution exists, for on-demand exchange of data with peripheral registers. This involves application data source and data sink function calls, from within I2C ISR service context.

As mentioned previously, application calls made inside either of the I2C ISR call-backs, must be very brief, so as not to disrupt wired I2C communication. From the servo and ADC application calls dealt with previously, only the myservo_set_pos() call is suitable. The myadc_read() function involves lengthy over-sampling, and any attempt to link it ‘on-demand’, from with an ISR, will break associated I2C transactions.

Application linkage inside the I2C ISRs

Here we examine an alternative approach to linking an I2C slave peripheral’s servo position. Data is exchanged only when requested to be, by a wired I2C bus transaction. The I2C receiveEvent() ISR handles incoming data, and we merely write that ‘value’ data to the servo, using the necessarily fast myservo_set_pos() application call.

void receiveEvent(int numBytes) 
{

...

    switch (registerAddr)
    {
      // Comment out any read-only registers to prevent master write
      case I2C_REG_0:
      {
        myservo_set_pos(value);
        i2c_registers[registerAddr] = value;
        break;
      }
      //case I2C_REG_1:  // Read-only
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

    I2C_WRITE_DEBUG(registerAddr, value)
  }
}

...

In this case, we have chosen to make the servo peripheral register both readable, and writeable. To accomplish this, we must also store the incoming data in the underlying peripheral register, as shown above.

In the case where we wanted to make the application’s servo peripheral register write only, we would not store the incoming position data in any underlying peripheral register, or, at the very least, make the underlying register write only. This is again accomplished, by commenting-out the appropriate switch statement case, inside the requestEvent() ISR.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    //case I2C_REG_0:  // Make servo register 0 write-only
    case I2C_REG_1:
    case I2C_REG_2:
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

For the final case, we examine how outgoing application data may be linked to peripheral register request, on-demand, from within I2C ISR context. For this, we will read an attached switch, with an imaginary application call, myswitch_get_posn(). We know that the call will be fast enough not to break wired I2C communication, and that logically the operation must be read-only.

The I2C requestEvent() ISR handles outgoing data, and we merely service a request for the switch position, with data from the myswitch_get_posn() application call.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_2:
    {
      value = myswitch_get_posn();
      break;
    }
    case I2C_REG_0:
    case I2C_REG_1:
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

As seen above, the switch peripheral register is logically read only, so stored peripheral register memory is not involved. To disable any attempt to write a peripheral register, making it read only, we comment-out it’s associated entry in the receiveEvent() I2C ISR.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_0:
    case I2C_REG_1:
    // case I2C_REG_2:  // Make switch register 2 write-only
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

Improving polled access efficiency

/************************************************************************
 *
 *                                loop()
 *
 ************************************************************************/
void loop() 
{
  {
    uint16_t value = 0;

    if (myservo_changed())
    {
      // Retrieve contents of slave register 0 atomically, and consume
      I2C_ATOMIC_REG_RD(value, i2c_registers[I2C_REG_0]);
      myservo_set_changed(0);

      // From here on, receiveEvent() can store new register data,
      // and the snapshot 'value' can be processed, even if exhaustively
      myservo_set_pos(value);
    }
  }

  // Other application tasks.

}
void receiveEvent(int numBytes) 
{

...

    switch (registerAddr)
    {
      // Comment out any read-only registers to prevent master write
      case I2C_REG_0:
      {
        i2c_registers[registerAddr] = value;
        myservo_set_changed(1);
        break;
      }
      case I2C_REG_1:
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

    I2C_WRITE_DEBUG(registerAddr, value)
  }
}

...

Well, that’s how the I2C slave peripheral model works, how to constrain access to it’s underlying peripheral registers, and how to link-in application code. Application linkage methods were shown, as by polling inside loop(), and by on-demand calls inside ISR context.

Just remember to keep application calls short in the ISRs, and to use atomic register access macros outside them.

Associated Files:

The attached Arduino I2C slave demonstration code for this example, runs on an AT328PB. It reads the position of an ADC-connected potentiometer, and stores this data in a 16-bit I2C register numbered 1. It reads data in a 16-bit I2C register numbered 0, and sets the corresponding position of an attached servo.

Example C code for a wiringPi-installed, Raspberry Pi master is also provided, which remotely reads the potentiometer value, and writes a proportionate value to the I2C slave peripheral’s servo control register.

Demonstration code, of the suggested organisational paradigm, for an Arduino I2C slave peripheral. I2C_slave_model.zip

LiquidCrystal_I2C

The LiquidCrystal_I2C library is a modified version of the standard LiquidCrystal library as found on the Arduino website.

HD44780 controlled LCD, fitted with Chinese PCF8574 I2C backpack.
HD44780 controlled LCD, fitted with Chinese PCF8574 I2C backpack.

This library is intended to be used when a parallel HD44780 compatible LCD is controlled over I2C using a Chinese PCF8574 extender, as sold on eBay.

Chinese PCF8574 I2C to parallel LCD backpack
Chinese PCF8574 I2C to parallel LCD backpack.

Be aware that the Chinese PCF8574 extender is available in two versions, the PCF8574 and the PCF8574A, the only difference between the two is the I2C base address. See the documentation for details.

Library Download.

Arduino code library: LiquidCrystal_I2C.zip

C8_CO2_5K

The C8 CO2 5K is a digital NDIR CO2 sensor, manufactured by Shenzhen Shenchen Technology Co., Ltd.

C8 NDIR CO2 Sensor and Output.

The C8_CO2_5K is a high-precision, low-cost NDIR CO2 sensor that is perfect for a variety of applications, including:

  • Indoor air quality monitoring
  • Greenhouse monitoring
  • Research and development
  • And more!

Associated Files:

English Language Datasheet – C8-CO2-5KV1-4-data-sheets.pdf
Chinese Language Datasheet – C8二氧化碳传感器产品规格书.pdf
Arduino Code Library – C8_CO2_5K.zip

Buy Factory Direct (no affiliation).

Visit – Shenzhen Shenchen Technology Co., Ltd.

or – http://www.iot-sc.com/