Sunday, April 28, 2013

Programming ATtiny

One of the unique things with the Cosa platform is that the full range of integrated classes can be used for all Arduino boards and ATtiny/ATmega processors. Cosa allows code to be developed on a standard Arduino board and more or less reconfigured and compiled for an ATtiny. No extra cores are necessary. 

With todays technology it is both easy and cheap to program micro controllers. The Cosa framework together with the Arduino IDE makes it possible to achieve a very fast prototype turn-around development loop. From code change to test on the ATtinyX5 (ATtiny25/45/85) typically within a minute. 

Fig.1: Arduino DIY Programmer Shield
This blog post describes the basic steps to compile, run and debug Cosa example sketches for the ATtiny. The Cosa Soft UART (Soft::UART) transmitter makes it possible to reuse all the Cosa IOStream support directly for the ATtiny and run trace output over the Soft UART without modification of sketch code. The implementation only requires a single output pin.  

Fig.2: The ATtiny breadboard version of CosaButton example sketch
Before starting you will need some equipment. Other than the ATtiny you will need a Programmer. If you have available an Arduino and a breadboard you can use them for a Programmer. More frequently programming of ATtiny devices may require either investing in an ISP or building a DIY Programmer Shield from a prototype shield and a universal ZIF socket (component cost approx. $5-6 on eBay). The available LEDs on the prototype shield may be used for the Programmers state. The soldering required is very basic. See below.

Fig.3: DIY Programmer Shield wiring
A ZIF socket allows you to connect and disconnect the chip without having to power down the Arduino. Just insert the ATtiny into the ZIF socket and fasten it by pulling down the lever, then upload the sketch as you would for an Arduino, unfasten the chip and move it to your breadboard test circuit or PCB prototyping board.

The Cosa ISP sketch is part of the Cosa example tools code and may be found in the Cosa example menu (File>Sketchbook>hardware>Cosa>Tools>CosaISP). The Cosa core contains support for ATtiny (ATtinyX4, ATtinyX5 and ATtinyX61). 

When using a new ATtiny for the first time it is important to burn the "bootloader", i.e. set the fuse bits. If you forget this the sketches will not run correctly. Especially the Cosa Watchdog/RTC timer will not work correctly.

Fig.4: Burn ATtiny45/85 Bootloader (Fuse bits)
Insert the chip into the ZIF socket, select the "Serial Port" for the Cosa ISP in the Tools menu, select the ATtiny45/861/85 in the board menu, and then select "Burn Bootloader". You only need to do this once per chip. Programming is now simply inserting the chip into the ZIF socket, uploading through the Arduino Programmer to the ATtiny and then removing the chip and inserting it into the breadboard or socket on a PCB.

I would recommend that you always get the sketch running first on a standard Arduino and then port the sketch to the ATtiny. When porting it is important to fully understand the footprint of the sketch and reduce it to the available memory on the ATtiny. This might require trimming away to get within the limited memory size both program and data memory. Especially the amount of data and stack memory (SRAM) used. Remember that the ISRs must be able to execute "on top" of the deepest function call in the sketch. The Arduino IDE reports the program memory size and will not build if the sketch is too large.

Debugging on the ATtiny is even more difficult than on the Arduino. Most applications will need all pins and hardware units. Cosa provides serial output from the ATtiny using a single pin and a simple software UART transmitter. It is implemented with the serial output support, OutputPin::write() method, and mimics an UART output port.

namespace Soft {
  class UART : public IOStream {
public:
  ... 
  virtual int putchar(char c)
  {
    // Add start and stop bits and write to output pin with given pulse width

    uint16_t d = ((0xff00 | c) << 1);
    m_pin.write(d, m_bits + m_stops + 1, m_us);
    return (c);
  }

  ...
};
};
 
A synchronized delay loop is used instead of a timer and interrupt service routine to keep the memory footprint low and not allocate other hardware resources. Special care must be taken to avoid affecting the sketch timing as interrupt handling is turned off during the transmission of a character. At baudrate of 9600 that is in the range of 1 milli-second.

On the ATtiny you should/must alway place literal strings (constants) in program memory. The Trace support macros in Cosa does this for you but when using the IOStream always use PSTR() together with IOStream::print_P() and IOStream::printf_P().

Fig. 5: USB to UART module
A USB to UART module can be used to establish a connection to the Arduino IDE serial monitor on your host machine. These module are very cheap (approx. $2 on eBay) and work nicely as they contain internal buffers for the input and output UART data stream and can power your breadboard circuit with both 5V and 3.3V (check current limits). You could also connect to one of the extra UART ports on an Arduino Mega. Please remember that only serial output is currently available. No code changes are necessary to achieve trace output from the ATtiny. And only a single output pin is required. 

It is also possible to use an LCD for sketch and trace output. The Cosa PCD8544 LCD driver is available for the ATtiny. There is a reduced version of the system font (5x7) to keep program memory footprint down. Replacing Soft UART output with the LCD is made easy as both are Cosa IOStream::Device's. In many cases only a single line of code needs changing.

In the Cosa example sketch folder there are a few demonstration sketches specifically for the ATtiny85. You can find them in the folder named Tiny. The CosaTinyReceiver is especially interesting as it uses the LCD driver together with the Virtual Wire Interface Receiver. The sketch will wait for messages from an Arduino and/or ATtiny running the CosaVWIsender sketch and display the measurements together with some statistics, battery status and tick count. 

Fig.6: CosaTinyDHT11 and CosaTinyReceiver with PCD8544 LCD
Let us take a look at a full example sketch. The below sketch acts as a wireless DHT11 sensor and reports humidity and temperature readings back to the CosaTinyReceiver or CosaVWIreceiver sketches. With the support in Cosa the code is very straight forward. 

// Allocate instances and bind to pins
DHT11 sensor(Board::D1);
VirtualWireCodec codec;
VWI::Transmitter tx(Board::D2, &codec);
const uint16_t SPEED = 4000;
...
// Message with DHT11 reading
struct msg_t {
  uint16_t nr;
  int16_t humidity;
  int16_t temperature;
};
...
void setup()
{
  // Start watchdog and virtual wire interface
  Watchdog::begin();
  VWI::begin(SPEED);
  tx.begin();
}
...
void loop()
{
  // Read sensor and send message every two seconds
  static uint16_t nr = 0;
  SLEEP(2);
  msg_t msg;
  if (!sensor.read(msg.humidity, msg.temperature)) return;
  msg.nr = nr++;
  tx.send(&msg, sizeof(msg));
  tx.await();
}

Both 1-Wire and the Virtual Wire Interface (VWI/RF433) are available together with drivers for InterruptPin, ExternalInterruptPin, EEPROM, DHT11/22 (Temperature/Humidity), TSOP4838 (IR Receiver), NEXA Remote Receiver (RF433), HCSR04 (Ultrasonic Range Module), Soft UART Transmitter, PCD8544 LCD and the Cosa Object-Oriented Pin handling. So there is plenty to get started with.

More details on Arduino as an ISP may be found here.  

[Update: 2013-06-05]

The Cosa now supports ATtinyX4, ATtinyX5 and ATtinyX61. A SPI master driver is now implemented (USI), and the SPI drivers (ADXL345 and NRF24L01P) are available for ATtiny. 

A TWI slave device is also available. This allows ATtiny's to be used as slave units on the TWI bus. This could be for a standard Arduino or even a Raspberry Pi and may be used to bridge other interfaces such as OneWire and VirtualWire. 

All LCD drivers work with ATtiny.

[Update: 2013-07-16]

The USI based I2C slave and master device driver for ATtiny has now been completed and device drivers are being ported.

Fig.7: ATtiny85 running LCD device driver benchmark (CosaLCDspeed).
The TWI master device driver allows ATtiny85 to connect to an LCD (HD44780) using an I2C IO expander. 

Fig.8: ATtiny84 acting as a Virtual LCD device (I2C slave device)
Also a Virtual LCD device driver (VLCD) has been introduced. It allows higher performance I2C LCD connection and may be used with for instance an ATtiny84 running the 4-bit parallel implementation of the LCD device driver. In fig.8 above the Arduino Nano connects to a Virtual LCD device on the I2C bus. The device is implemented with an ATtiny84 running LCD device driver in 4-bit parallel access mode. 

[Update 2014-01-05]
The Cosa Tools directory (in examples) contains an implementation of the STK500v1 protocol and may be used as an ISP alternative. There is also a TWI scanner to help debug TWI drivers. 

[Update 2014-03-01]
The Cosa Nucleo support for multi-tasking may be used with ATtiny. Please see the Nucleo example sketches. They include a variant of the blink sketch that will run three threads; one for the LED, one for LED controller that changes the blink frequency and the main thread in the background. The main thread will also power down while waiting for the next watchdog tick. 

Saturday, April 27, 2013

Enhanced Virtual Wire Interface

This post presents the final design of the Cosa Virtual Wire Interface. It now includes many of the features discussed in the previous posts. The main enhancement is reliable wireless communication on cheap RF433 transmitter and receiver modules with node addressing, message filtering, auto-retransmission and support for message dispatch. As before this Cosa sub-system may be used with ATtinyX5 to create ultra tiny and low power sensor devices.

The first section of the post describes the modifications to the Cosa VWI Receiver and Transmitter classes, adding addressing and message tags. The second section describes the new VWI::Transceiver class which provides message acknowledgement with auto-retransmission and reduces much of the complexity of designing and implementing wireless message protocols.

To achieve this the original Virtual Wire Interface (VWI) has been extended. A new overloaded send() method is introduced to allow gathering of multiple buffers (iovec_t). This makes implementation of communication protocols much easier and reduces message memory buffer copying.

To improve performance of retransmission a new resend() method is also introduced. This method will simple retransmit the message already encoded in the transmission buffer. There are also access methods for the next message sequence counter as this will be needed later for retransmission. Below is the updated VWI::Transmitter API.

 Fig.1: Enhanced VWI Transmitter Member Functions

The enhanced mode is enabled by providing a 32-bit node address when calling VWI::begin(). Below is the setup() from the example sketch CosaVWItempsensor.ino to give an idea how this works. 

// Connect RF433 transmitter to Arduino/D9
VirtualWireCodec codec;
VWI::Transmitter tx(Board::D9, &codec);

const uint16_t SPEED = 4000;
const uint32_t ADDR = 0xC05a0002;

...
void setup()
{
  ...
  // Start the Virtual Wire Interface/Transmitter
  VWI::begin(ADDR, SPEED);
  tx.begin();
  ...
}


The blue section is the only change needed to enable addressing from the sensor node, i.e. the new address parameter. The loop() of the temperature sensor will read two 1-Wire Thermometers and send the values together with a reading of the power supply voltage. The message is defined as a struct (sample_t) and given a message type tag, SAMPLE_CMD. This is used as an extra parameter to the send() method and provides the receiver with information about what type of message the payload contains. See the blue sections below.

// Message from the device; temperature and voltage reading
const uint8_t SAMPLE_CMD = 42;
struct sample_t {
  int16_t temperature[2];
  uint16_t voltage;
};

...
void loop()
{

  ...  // Initiate the message with measurements
  sample_t msg;  
  msg.temperature[0] = indoors.get_temperature();
  msg.temperature[1] = outdoors.get_temperature();
  msg.voltage = AnalogPin::bandgap(1100);
  // Enable wireless transmitter and send. Wait completion and disable
  VWI::enable();
  tx.send(&msg, sizeof(msg), SAMPLE_CMD);
  tx.await();
  VWI::disable();
  ...
}


The VWI::Receiver class is also enhanced with filtering of incoming messages. The API is not changed, i.e. the recv() method has the same prototype. Incoming messages are matched against the receiver node address using a simple sub-net mask. The mask is provided as an extra parameter to the begin() method for the receiver. The logic of the address filtering is:

(incoming-message-address & receive-node-sub-net-mask) == receive-node-address

This implies that receiving nodes only listens for messages from transmitters with the same sub-net address as the receiver. Below is the setup() snippet from the CosaVWItempmonitor.ino example sketch:

// Virtual Wire Interface Receiver connected to pin D8
VirtualWireCodec codec;
VWI::Receiver rx(Board::D8, &codec);

const uint16_t SPEED = 4000;
const uint32_t ADDR = 0xc05a0000UL;
const uint32_t MASK = 0xffffff00UL;

...
void setup()
{
  ...
  // Start virtual wire interface and receiver. Use eight bit sub-net mask
  // Transmitters must have the same 24 MSB address bits as the receiver.
  VWI::begin(ADDR, SPEED);
  rx.begin(MASK);
}


In the above snippet the sub-net MASK will allow for 255 transmitting sensor reporting to the monitoring node. The first sub-net address (0) is the receiver itself. With the <24:8> bit addressing scheme in the example sketch there could be 2**24 monitor nodes if in a flat structure. Additional sub-nets are possible, if for instance <16:8:8>, the monitoring node would also report to a sub-net with yet another layer of nodes. And parts of the address could be used to sensor type tagging.

The VWI::Receiver::recv() method will return the full message with the enhanced mode header and the application payload to the caller. Further filtering and dispatch of message will require access to the enhanced VWI header. Defining a struct (msg_t) with the header and a union of the application message data types, as in the snippet below, is a convenient way to describe the incoming messages. Also it is practical to collect all messages in a separate header file to keep the code base consistent. Below is the loop() of the example monitor with an example of message filtering based on the message type (command/cmd). 

// Message received from VWI in extended mode
struct msg_t {
  VWI::header_t header;
  union {
    sample_t sample;

    // Add additional message types here
  };
};

...
void loop()
{
  // Receive a message. Sanity check the message size
  msg_t msg;
  int8_t len = rx.recv(&msg, sizeof(msg));
  if (len <= 0) return;
  // Print message header; transmitter address and sequence number
  trace << hex << msg.header.addr << ':' << msg.header.nr << ':';
  // Check message type and print contents
  if (msg.header.cmd == SAMPLE_CMD) {
  ...
  }
  else {
    trace << msg.header.cmd << PSTR(":unknown message type") << endl;
  }
}


VWI::header_t contains the transmitter node address, message type tag and sequence number. The node address is provided as the parameter to VWI::begin(), the message type tag (cmd) as the additional parameter to VWI::Transmitter::send() and the sequence number is maintained internally to the Transmitter. Below is the implementation:

class VWI {
public:
  ...
  /** Message header for enhanced Virtual Wire Interface mode */
  struct header_t {
    uint32_t addr;         /**< Transmitter node address */
    uint8_t cmd;           /**< Command or message type */
    uint8_t nr;            /**< Message sequence number */
  };
  ...

};

So far the modifications are to the original Virtual Wire Interface and have provided addressing and message types. In the following section the new VWI::Transceiver class is presented. This class adds reliable communication with message acknowledgement and auto-retransmission.


Fig.2: VWI::Transceiver Public Member Functions and Attributes

The VWI::Transceiver interface is now reduced to basically send() and recv(). The Transceiver contains both a Transmitter and Receiver instance (rx and tx attributes above). The send() method will transmit an enhanced mode message and wait for an acknowledgement. The acknowledgement is transmitted by the receiving node. If the message or acknowledgement is lost an automatic retransmission will occur. The sender node will wait a TIMEOUT period before retransmission and perform RETRANS_MAX retransmissions. The send() method will return the number of transmissions or a negative error code(-1) if the maximum number of retransmissions was exceeded.

Note that the VWI::Transmitter::send() method will return directly after placing the message into the transmission buffer. This is not the case with VWI::Transceiver::send(). This method will wait until it receives an acknowledgement with possible retransmission.

Below is a snippet of the CosaVWIclient.ino example sketch. This sketch demonstrates how to use the VWI::Transceiver and multiple message types. The first message type (sample_t/SAMPLE_CMD) contains two analog readings and is sent every second from the wireless sensor device. The second message type (stat_t/STAT_CMD) contains power supply reading and transceiver statistics. This message is sent every 15th second.

// Network configuration
const uint32_t ADDR = 0xc05a0001UL;
const uint16_t SPEED = 4000;
...
// Virtual Wire Interface Transceiver
VWI::Transceiver trx(Board::D8, Board::D9, &codec);
...
// Analog pins to sample for values to send
AnalogPin luminance(Board::A2);
AnalogPin temperature(Board::A3);
...
// Message type to send (should be in an include file for client and server)
const uint8_t SAMPLE_CMD = 1;
struct sample_t {
  uint16_t luminance;
  uint16_t temperature;
  sample_t(uint16_t lum, uint16_t temp)
  {
    luminance = lum;
    temperature = temp;
  }
};
...
const uint8_t STAT_CMD = 2;
struct stat_t {
  uint16_t voltage;
  uint16_t sent;
  uint16_t resent;
  uint16_t received;
  uint16_t failed;
  void update(int8_t nr)
  {
    sent += 1;
    if (nr <= 0)
      failed += 1;
    else if (nr > 1)
      resent += (nr - 1);
  }
};
...
// Statistics
stat_t stat;
... 
void setup()
{
  // Start watchdog for delay
  Watchdog::begin();
  RTC::begin();
  ...
  // Start virtual wire interface in extended mode; transceiver
  VWI::begin(ADDR, SPEED);
  trx.begin();
}
...
void loop()
{
  // Send message with luminance and temperature
  sample_t sample(luminance.sample(), temperature.sample());
  int8_t nr = trx.send(&sample, sizeof(sample), SAMPLE_CMD);
  stat.update(nr);
  // Send message with battery voltage and statistics every 15 messages
  if (stat.sent % 15 == 0) {
    stat.voltage = AnalogPin::bandgap(1100);
    nr = trx.send(&stat, sizeof(stat), STAT_CMD);
    stat.update(nr);
  }

  // Take a nap
  SLEEP(1);
}


A server will receive the messages from the client (above), filter, acknowledge and print the messages. Note that the receiving node must filter received messages so that multiple messages with the same sequence number are only handled once (if necessary). Below is a snippet from the CosaVWIserver.ino example sketch:

// Virtual Wire Interface Transceiver
VWI::Transceiver trx(Board::D8, Board::D9, &codec);
...
// Network configuration
const uint32_t ADDR = 0xc05a0000UL;
const uint32_t MASK = 0xffffff00UL;
const uint16_t SPEED = 4000;
..
void setup()
{
  ...
  // Start virtual wire interface transceiver
  VWI::begin(ADDR, SPEED);
  trx.begin(MASK);
}
...
// Message types
const uint8_t SAMPLE_CMD = 1;
struct sample_t {
  uint16_t luminance;
  uint16_t temperature;
};
const uint8_t STAT_CMD = 2;
struct stat_t {
  uint16_t voltage;
  uint16_t sent;
  uint16_t resent;
  uint16_t received;
  uint16_t failed;
};
...
// Extended mode message with header
struct msg_t {
  VWI::header_t header;
  union {
    sample_t sample;
    stat_t stat;
  };
};

...
void loop()
{
  // Processed sequence number (should be one per client)
  static uint8_t nr = 0xff;
  // Wait for a message. Sanity check the length
  msg_t msg;
  int8_t len = trx.recv(&msg, sizeof(msg));
  if (len <= 0) return; 
  // Check that this is not a retransmission

  // Should be one counter (nr) per connection
  if (nr == msg.header.nr) return;
  nr = msg.header.nr;
  // Print header, type message type and print contents
  trace << hex << msg.header.addr << ':' << msg.header.nr << ':';
  switch (msg.header.cmd) {
  case SAMPLE_CMD:
    ...
    break;
  case STAT_CMD:
    ...
    break;
  }


The next development of Cosa VWI is to increase performance and adapt to the event driven state-machine framework. One goal is to abstracting the interface toward wireless connection over Ethernet, WiFi, RF433, NFR24L01P, etc, so that applications are written without concern to the wireless device. RF433 is an interesting challenge as the protocol stack must be built from the ground up. 

[Update 2013-11-17]
This extension of the Virtual Wire interface in Cosa has been deprecated and replaced by an abstract Wireless interface. This interface is implemented by RF433 modules (VWI), CC1101 and NRF24L01P. The above extended VWI interface is still possible to implement on top of the new Wireless interface. This will be added in later releases of Cosa. Please see the Cosa documentation for further details. 

Monday, April 15, 2013

A Virtual Wire Digital Thermometer

In this post we will look into some details on one of the Virtual Wire Interface (VWI) example sketches; CosaVWItempsensor. The basic idea behind the example is to demonstrate how to use the VWI transmitter together with the Cosa 1-wire driver (OWI) for the DS18B20 Programmable Resolution Digital Thermometer. The sketch sends messages with the Thermometer 1-Wire identity, a sequence number, and the thermometer and power supply (battery) measurements.

Fig.1: CosaVWItempsensor (ATtiny85V/RF433TX/DS18B20)

The sketch is designed to run on an ATtiny85 with the internal 8 MHz clock and at 3.3 V. It can easily be modified to run on an Arduino (i.e. Board::D1 should be changed). First a snippet from the sketch that shows the sections that construct and sends the message:

VirtualWireCodec codec;
VWI::Transmitter tx(Board::D1, &codec);
const uint16_t SPEED = 4000;
...
void setup()
{
  ..
  // Start the Virtual Wire Interface/Transmitter
  VWI::begin(SPEED);
  tx.begin();
  ...
}
...
// Message from the device. Use one-wire identity as virtual wire identity
struct msg_t {
  uint8_t id[OWI::ROM_MAX];
  uint16_t nr;
  int16_t temperature;
  uint16_t voltage;
};
...
void loop()
{
  static uint16_t nr = 0;
  ...
  msg_t msg;
  ...
  memcpy(&msg.id, sensor->get_rom(), sizeof(msg.id));
  msg.nr = nr++;
  msg.temperature = sensor->get_temperature();
  msg.voltage = AnalogPin::bandgap(1100);

  // Enable wireless transmitter and send. Wait completion and disable
  VWI::enable();
  tx.send(&msg, sizeof(msg));
  tx.await();
  VWI::disable();
  ...
}


The message (struct msg_t) consists of an identity, a sequence number, the temperature and a battery reading. The sensor's 1-wire identity [64-bit] is used as the message identity. To reduce power the transmitter is only activated for the transmission. VWI::enable/disable are used.

The sections that handles the 1-Wire Thermometer device temperature measurements from the sensors looks like this:

// Connect to one-wire device; Assuming there are two sensors
OWI owi(Board::D2);
DS18B20 indoors(&owi);
DS18B20 outdoors(&owi);
...
void setup()
{
  ...
  // Connect to the temperature sensor
  indoors.connect(0);
  outdoors.connect(1);
  ...
}
...
void loop()
{
  ...
  static DS18B20* sensor = &indoors;
  ...
  // Make a conversion request
  sensor->convert_request();

  // Read the temperature and initiate the message
  sensor->read_scratchpad();
  memcpy(&msg.id, sensor->get_rom(), sizeof(msg.id));
  msg.nr = nr++;
  msg.temperature = sensor->get_temperature();
  msg.voltage = AnalogPin::bandgap(1100);
  ...
  sensor = (sensor == &indoors) ? &outdoors : &indoors;
  ...
}


The same design pattern as for VWI is used for the 1-Wire driver support. The statement OWI owi(Board::D2) creates an instance of the 1-Wire manager and connects it to digital pin D2. The sensors, indoors and outdoors, are instances of the DS18B20 device driver. They use the 1-Wire manager, owi. This allows several 1-Wire buses if needed.

The first step is to connect the instances to the physical devices on the 1-Wire. The device driver support several methods. The easiest is to use the DS18B20::connect() member function which will bind the indexed device, read the configuration (resolution and alarm setting) and the power supply mode. The Cosa 1-Wire driver supports parasite powering of devices. No application code changes are required to activate parasite power handling and the device driver will detect the device setting when issuing either connect() or read_power_supply(). The setup() of the devices could also contain setting of the resolution and alarm thresholds. The default resolution is 12 bits fixed-point number (4 bit fraction).

The second step is to issue a convert_request(). The sampling and conversion will be performed on the device and the value is available after 750 milli-seconds (at the default 12-bit resolution). The necessary delay is handled in the read_scratchpad() function using the Cosa Watchdog based delay function for low power handling. The read_scratchpad() function will wait for the device and read back the scratchpad structure with the temperature measurement.

The last step is to initiate the message with the temperature reading and the rest of the message fields. Please note that the loop() will toggle between the two sensors for each call. Below is the full list of member functions for the DS18B20 device driver. These correspond directly to the functions in the data sheet.


Fig.2: DS18B20 Member Functions
The sketch also contains power enable/disable of different hardware modules in the ATtiny85 to reduce power consumption.  Cosa supports battery monitoring with the AnalogPin static member function bandgap(). This function will return the power supply (Vcc) in milli-volt.

 Fig.3: CosaVWItempmonitor (RF433RX/Arduino Mini Pro)

Please see the monitor sketch CosaVWItempmonitor.ino for details on the receiver. Also note that there is no retransmission though dropped messages may be detected with the sequence numbering.

[Update 2015-08-22]
The new support library Cosa Domotica makes it really easy to develop RF433 based sensor nodes.  The library performs common operations such as battery status measurement, message sequencing, low power sleep modes, etc.

Sunday, April 7, 2013

Object-Oriented EEPROM device driver

After reading a fair number of postings on the Arduino forum about using the Arduino EEPROM class it is time to do something about this area. Some improvements are required. So far I have recommended the use of the basic support in AVR GCC, <avr/eeprom.h>, but after looking in to the code and problems using the Arduino EEPROM library I have come to the conclusion that there is a need for an abstraction of internal and external EEPROM access.

Out of the box AVR GCC supports the usage of EEPROM with the variable attribute EEMEM.

#define EEMEM __attribute__((section(".eeprom")))

Just as program memory (PROGMEM), variable/data may be declared to be in the EEPROM address space. The short form for this is the above macro EEMEM.

int x EEMEM;
int y EEMEM;

In the snippet the two variables, x and y, are declared with the attribute EEMEM. This puts them in the EEPROM address space instead of the SRAM. Assuming that the variable x and y are the first EEMEM variables they are given the addresses 0 and 2, that is &x == 0, and &y == 2 (or address of previous plus sizeof(previous)). The AVR GCC eeprom library provides functions to read and write data in the EEPROM address space. All access is done with the address of the variable.

int t = (int) eeprom_read_word(&x);
t += 1;
eeprom_write_word(&x, t);

Using EEPROM requires writing code for reading and writing data to and from the different memory address spaces. Normally the compiler does this for us for variables in SRAM (global/stack). The Arduino EEPROM library gives almost the same functionality but only byte level access.

External EEPROM may be connected to the Arduino in several ways. The more popular are devices use either the I2C/TWI or SPI bus, such as AT24CXX. The AT24C32 (8 Kbyte EEPROM) is often packaged together on modules with the I2C/TWI Real-time Clock chip DS1307.

Fig.1: Cosa EEPROM member functions

The object-oriented approach of Cosa is to construct an interface for EEPROM devices, both internal and external, and provide an application programmer's interface that allows easy usage with the base data types. The Cosa EEPROM class contains functions to read and write C/C++ base data types. The fundamental driver functions are block read and write, and a function to check if the device is ready (not busy writing data).

To achieve this Cosa uses a delegation design pattern with an EEPROM class and a EEPROM::Device class to be implemented per device type. This allows programs to simply change a variable to move from internal to external EEPROM and/or change size of EEPROM, etc. Also the EEPROM::Device only requires the implementation of the three fundamental functions, and all the data type read/write functions are reused between implementations. The EEMEM attributes may be used together with the Cosa EEPROM class. Please note that the compiler only allows a single EEPROM section/address space.

Fig. 2: AT24CXX Class Hiearchy

Devices such as the AT24C32 allows page write to improve speed. EEPROM are very slow devices when it comes to erase/write operations. The internal EEPROM may take as much as 3.3 ms per byte (approx. 50,000 clock cycles@16 MHz). For the AT24C32 EEPROM the write cycle is 10 ms per page of max 32 bytes, or byte, this does not include the I2C/TWI transfer time. 

The Cosa AT24CXX class takes advantage of the page write mode and handles all the details of reading and writing any size blocks of data. There is no hidden 32-byte max size. Applications must be aware of the delay caused by the write operation. Typical usage of the EEPROM is to store configuration data such as network addresses, sensor thresholds, etc. It is also common to use large EEPROMs for sensor data logs.

EEPROM eeprom;
const int DATA_MAX = 8;
long data[DATA_MAX] EEMEM;


void setup()
{
  ...
  for (uint8_t i = 0; i < DATA_MAX; i++) {
    eeprom.write(&data[i], (long) i);
  }
}


void loop()
{
  static int i = 0;
  long x;
  TRACE(eeprom.read(&x, &data[i]));
  trace << i << ':' << x << endl;
  x += 5;
  TRACE(eeprom.write(&data[i], x));
  i += 1;
  if (i == membersof(data)) i = 0;
  SLEEP(2);

}
Above is a snippet from the CosaEEPROM example sketch. The variable eeprom is used to access the internal EEPROM of the Arduino micro-controller. This is the default EEPROM::Device. The variable data[] is given the attribute EEMEM and the compiler provides the address to the variable (a symbol). Please note that the address of the data element is used for all EEPROM read/write functions.

The setup() function performs a simple initialization of the EEPROM variable data. Each element in the vector is given the initial value corresponding to it's index in the vector. The loop() function is performed every 2 seconds. It reads the data element using and index variable, i, and increments and writes the value back to the EEPROM. 

// The serial eeprom (sub-address 0b000) with binding to eeprom
AT24C32 at24c32(0);
EEPROM eeprom(&at24c32);


// Symbols for data stored in AT24CXX EEPROM memory address space
int x[6] EEMEM;
uint8_t y[300] EEMEM;
float z EEMEM;

void init_eeprom()
{
  int x0[membersof(x)];
  for (uint8_t i = 0; i < membersof(x0); i++) x0[i] = i;
  trace.print(x0, sizeof(x0), 16);
  TRACE(eeprom.write(x, x0, sizeof(x)));
 
  uint8_t y0[sizeof(y)];
  memset(y0, 0, sizeof(y));
  trace.print(y0, sizeof(y0), 16);
  TRACE(eeprom.write(y, y0, sizeof(y)));
 
  float z0 = 1.0;
  trace.print(&z0, sizeof(z), 16);
  TRACE(eeprom.write(&z, z0));
}

void setup()
{
  ...
  init_eeprom();
}

void loop()
{
  // Wait for 2 seconds; we don't want to burn too many write cycles
  SLEEP(2);
  ledPin.toggle();

  // Read the eeprom variables into memory
  uint8_t buffer[sizeof(y)];
  memset(buffer, 0, sizeof(buffer));
 
  TRACE(eeprom.read(buffer, &x, sizeof(x)));
  trace.print(buffer, sizeof(x), 16);

  TRACE(eeprom.read(buffer, &y, sizeof(y)));
  trace.print(buffer, sizeof(y), 16);

  float z1;
  TRACE(eeprom.read(&z1, &z));
  trace.print(&z1, sizeof(z1), 16);

  // Update the floating point number and write back
  z1 += 0.5;
  TRACE(eeprom.write(&z, z1));
 
  // Update the eeprom (y => y+1)
  for (size_t i = 0; i < sizeof(buffer); i++)
    buffer[i]++;
  TRACE(eeprom.write(&y, buffer, sizeof(buffer)));

  // Is the write completed?
  TRACE(eeprom.is_ready());
  eeprom.write_await();
  TRACE(eeprom.is_ready());
 
  // Read back and check
  TRACE(eeprom.read(buffer, &y, sizeof(y)));
  trace.print(buffer, sizeof(y), 16);
  ledPin.toggle();
}

The above CosaAT24CXX example sketch is larger and contains several EEPROM variables that are updated. Both vectors of integers of different sizes and floating point number are stored in the external EEPROM.

Compared to the previous example sketch the only difference is the binding of the eeprom variable to the AT24C32 instance. Otherwise the read and write code style is the same. By changing the instance binding the program will use the internal EEPROM or an other device.

Reading/writing data structures such as a C/C++ struct is done in the same fashion as above. Below is a snippet from the EEPROM example sketch.

EEPROM eeprom;
...
static const int NAME_MAX = 16;
struct config_t {
  int mode;
  int speed;
  char name[NAME_MAX];
};
config_t config EEMEM;

void setup()
{
  ...
  config_t init;
  init.mode = 17;
  init.speed = 9600;
  strcpy_P(init.name, PSTR(".EEPROM"));
  eeprom.write(&config, &init, sizeof(config));
}

void loop()
{
  ...
    config_t init;
    eeprom.read(&init, &config, sizeof(init));
    trace << PSTR("init(mode = ") << init.mode
          << PSTR(", speed = ")   << init.speed
          << PSTR(", name = \"")  << init.name
          << PSTR("\")\n");
  ...
}


First a "configuration" struct is defined as a data type (config_t) and a variable, config, in EEPROM address space. Second, in setup(), a local variable, init, is inititiated and written to the EEPROM. And last, in loop(), the data is read back. The snippet also shows how to write a string to the EEPROM. Note that the name field is initiated with a string in program memory.