Tuesday, July 2, 2013

Object-Oriented LCD management

One for the latest developments in Cosa is the object-oriented interface for LCD devices. The goal has been to create a common interface for the Cosa LCD device drivers (PCD8544, ST7565 and HD44780) and allow LCD devices to act as an IOStream::Device and finally adapt to different forms of ports (e.g. parallel and serial communication). With a common interface applications may change output device more easily. Below is an UML Inheritance Diagram for IOStream::Device with all Cosa classes that currently implement the interface and are inter-changeable.

Fig.1: IOStream class hierarchy
The Cosa LCD class is basically a name space for the LCD::Device abstract class which holds the common interface for LCDs. This interface is an extension of the IOStream::Device interface with some LCD specific methods. By providing an implementation of IOStream::putchar() an LCD::Device can directly be used for text output with number conversion, string and formated output both function (e.g. print) and operator<< notation.

Fig.2: IOStream::Device interface
Above are the member functions in the IOStream::Device interface. The "abstract" implementation acts as a null device which accepts any input and always returns end-of-file (EOF(-1)). By overriding putchar() an output stream device reuses all other output methods, i.e. puts(), puts_P() and write(). The default implementation of these use putchar().

Number conversion (binary to textual representation) is performed by the IOStream class which delegates the actual output to the attached IOStream::Device. The IOStream and the null device code is reused by all devices.

The LCD::Device class extends the IOStream::Device class with additional methods typical for LCD. Most of these are pure abstract and must be defined by the device driver.

Fig.3: LCD::Device Interface
The abstract LCD::Device captures some display properties, cursor, tabulator and text mode handling. If an application only uses the abstract LCD interface the code may be more easily adapted for different LCD devices. Also as the LCD device acts as an IOStream::Device the text output may be redirected to any other device that implements the interface. See figure 1 for the current classes that implement the interface.

There are basically two major types of LCD devices; pixel and character based. Some pixel based devices restrict drawing of pixels to a vertical byte, 8 pixels at a time, but allow horizontal cursor handling on pixel level. Examples of this type of LCD devices are PCD8544 and ST7565. HD44780 aka 1602/1604/2004 are typical character based devices where the characters codes are written to the device and not pixels.

Fig. 4: HD44780 Device Driver collaboration diagram
Another issue when dealing with LCD devices is the method of communication. Most devices use serial communication such as I2C or SPI to reduce the number of pins. The alternative is parallel communication. This is the default communication method for the HD44780 LCD device. It allows 8-bit or 4-bit command/data parallel communication. An I2C IO expander may be used to adapt HD44780 to serial communication over the I2C bus. To handle adaption of the device driver an abstract IO port is defined.

Fig. 5: HD44780::IO class hierarchy
The HD44780 device driver can now be adapted to any of these communication modules by simply changing the IO port device. The Cosa LCD class hierarchy is constructed for Standard Arduino, Mega, Mighty and possible to be scaled down to ATtiny8X. The footprint both SRAM and PROGMEM of the LCD device driver becomes especially important for micro controllers with limited program memory.

The HD44780 device driver requires 2.5 Kbyte program memory for parallel port, 3.5 Kbyte with I2C expander. The PCD8544 device driver with font (System7X5) requires 3.5 Kbyte. And last, ST7565, 4 Kbyte including font. All devices drivers include IOStream::Device, LCD::Device and default virtual methods.

Fig. 6: Benchmarking the LCD device drivers
The above benchmark configurations are (left to right, top to bottom):
  1. Arduino Mega, 20x4 character, HD44780 with MJKDZ I2C IO expander.
  2. Arduino Nano, 128x64 pixel, ST7565, software bit serial, OutputPin::write().
  3. Arduino Nano, 84x48 pixel, PCD8544, software bit serial.
  4. Arduino Uno, 16x2 character, HD44780, 4-bit direct port access, D4..D7.
The example sketch CosaLCDspeed performs basic benchmarking of the different device drivers. The benchmarks are:
  1. Clear the display, lcd.display_clear().
  2. Fill the display with characters, lcd.putchar(), WIDTH * HEIGHT number of characters per iteration and lcd.set_cursor() per line.
  3. Write a string, lcd.puts(), 2x16 characters per iteration, and lcd.set_cursor() per iteration.
  4. Write a string from program memory, lcd.puts_P(), as previous.
  5. Write an integer in decimal format, lcd.set_cursor() and number print per iteration (trace << num).
  6. Write an integer in binary format, as previous (trace << bin << num).
The below measurements are in operations per second:

Tab. 1: Benchmark operations per second
Please note that the benchmark putchar() is adjusted with the number of characters written (80, 160, 84, resp 32). The New LiquidCrystal Library reports 299 fps and Cosa LCD HD44780 driver 564 fps (putchar() benchmark 4-bit) which is 1.9X faster and 6.5X faster than the original LiquidCrystal Library.

The Cosa I2C IO expander handler is further optimized to achieve higher performance by reducing the transmission overhead. Writing a command or data byte to the I2C IO expander is transmitted as a sequence of five bytes instead of four X two byte transmissions (address and port data, total eight bytes). This reduces the transmission time with nearly 40%. This method can be applied further to reduce string output (see puts() and compare with puts_P() in tab. 1). The I2C@100khz improvement is over 1.7X faster compared to that reported by New LiquidCrystal Library benchmarks (31 fps). With I2C@400khz the improvement is over 4.6X faster.

Fig.7: ATtiny84 acting as an I2C LCD slave device. Running 4-bit parallel LCD device driver
Running the benchmark on an ATtiny85/84 with the internal 8 Mhz clock shows also great improvements; 4.9X for 4-bit parallel mode and 1.26X for I2C mode. Using an ATtiny as a Virtual LCD slave device on I2C gives 2.3X compared using an I2C IO expander. The ATtiny acts as an I2C slave device and uses the 4-bit parallel mode.

With the new USI based TWI master device driver support even an ATtiny85 can be used with an LCD. 

Fig.8: ATtiny85 connected to LCD with I2C IO expander
For the pixel based displays (PCD8544 and ST7565) the actual number of bytes transmitted is 6X per character.

For further details see the Cosa on-line documentation and the LCD example sketches

Monday, May 6, 2013

More RF433 Wireless: Home Automation

The RF433 transmitter and receiver modules can be used with Home Automation equipment. This blog post presents the Cosa support for sending and receiving wireless commands for NEXA/HomeEasy equipment. With sensor modules and supporting drivers it is possible to build advanced Home Automation applications that both receive and send commands to monitor and control lighting, alarms, door-bells, heaters, fans, etc. The Cosa NEXA/HomeEasy may be used with ATtiny.

Fig.1: CosaNEXAreceiver on ATtiny85 with RF433 Receiver and NEXA Remote

The Cosa NEXA::Receiver allows Arduino sketches to listen for wireless commands. These may be transmitted by a standard remote as in the figure(1) above or from another Arduino running the NEXA::Transmitter as in the figure(2) below. The receiver supports message decoding and address matching hiding much of the protocol complexity. Below is an example sketch (CosaNEXAreceiver.ino). The command data type (NEXA::code_t) simplifies the access of the command members. It also supports address matching.  

OutputPin led(Board::LED);
NEXA::Receiver receiver(Board::EXT0);
NEXA::code_t device = 0;
...
void setup()
{
  ...
  // Use polling version to receive the remote button for device
  receiver.recv(device);
  trace << PSTR("learning: ") << device << endl;
  ...
  // Enable the interrupt driven version
  receiver.enable();
}
...
void loop()
{
  // Wait for the next event
  Event event;
  Event::queue.await(&event);
  ...
  // Get the received command code and check if it is for this device
  NEXA::code_t code = receiver.get_code();
  if (code == device) {
    trace << PSTR("matched: ") << code << endl;
    led << code.onoff;
  }
}


The NEXA::Receiver uses an external interrupt pin, Board::EXT0. Both a polling (busy-wait) and interrupt driven version are provided. In the sketch above the polled version, receiver.revc(), is used to receive the first command code. This code is used as the device address for matching command codes received from the interrupt driven version in the loop() function.

Fig.2: Arduino Nano with RF433 Transmitter and NEXA Receiver

An event is pushed by the interrupt handler when a command code has been received and decoded. The loop() simply waits for the next event, retrieves the received command code, get_code(), and matches it with the operator==. If the unit address matches the received code is printed to the trace stream and the LED is turned on according to the onoff member of the received command code. The Cosa NEXA class also contains a command code transmitter, NEXA::Transmitter. The current version of send() is delay-based. The below example sketch (CosaNEXAsender.ino) is the wireless version of the Blink sketch.

OutputPin led(Board::LED);
NEXA::Transmitter transmitter(Board::D9, 0xc05a01L);
...
void setup()
{
  ...
  // First code will be used by receiver as address (learning mode)
  transmitter.send(0, 1);
}
...
void loop()
{
  // Blink the transmitter and receiver leds
  led.on();
  transmitter.send(0, 1);
  SLEEP(1);
  transmitter.send(0, 0);
  led.off();
  SLEEP(5);
}


The NEXA::Transmitter constructor requires an output pin and the house address (26-bit). The unit number is given as a parameter to the send() method. There is also a broadcast() method to send a group-message.

  union code_t {
    int32_t as_long;
    struct {
      uint8_t device:4;       /** device number, group:unit<2,2> */
      uint8_t onoff:1;        /** device mode, off(0), on(1) */
      uint8_t group:1;        /** group command */
      uint32_t house:26;      /** house code number */
    };
    code_t(int32_t value = 0L);
    code_t(int32_t h, uint8_t g, uint8_t d, uint8_t f);
    bool operator==(const NEXA::code_t &other) const;
    friend IOStream& operator<<(IOStream& outs, code_t code);
  };


The last example sketch in this post integrates the Virtual Wire Interface (VWI) and NEXA Transmitter to form a wireless Thermostat. The sketch receives humidity and temperature readings from the wireless DHT11 sketch (CosaTinyDHT11.ino) running on a ATtiny85. When temperature drops too low a relay/heater is turned on and when the humidity is too high a relay/fan is turned on and a command is send to a NEXA receiver (vent). When the readings drop below a lower threshold the heater/fan/vent are turned off again.

Fig.3: CosaTinyDHT11 running on an ATtiny85 and receiver

Two support classes, Relay/RemoteRelay, are defined to make the setup() and loop() easier to read. Below is the setup() of CosaVWIthermostat.ino. The setup() starts the Virtual Wire Interface (VWI) and receiver, and sends a NEXA command to the vent.

Relay heater(Board::D2);
Relay fan(Board::D3);
RemoteRelay vent(Board::D4, 0xc05a01L, 0);
...
void setup()
{
  ...
  // Start virtual wire interface and receiver
  VWI::begin(SPEED);
  rx.begin();
  ...
  // Close the remote vent
  vent.close();
}

...
// Message type to receive
struct msg_t {
  uint16_t nr;
  int16_t humidity;
  int16_t temperature;
};
...
void loop()
{
  // Wait for a message
  msg_t msg;
  int8_t len = rx.recv(&msg, sizeof(msg));
  if (len != sizeof(msg)) return;
  int16_t humidity = msg.humidity;
  int16_t temperature = msg.temperature;
  ...
  // Check if heater should be turned on @ 22 C and off @ 26 C
  static const uint8_t TEMP_MIN = 22;
  static const uint8_t TEMP_MAX = 26;
  if (heater.is_on()) {
    if (temperature > TEMP_MAX) heater.off();
  }
  else if (temperature < TEMP_MIN) heater.on();
  ...
  // Check if fan/vent should be turned on @ 70 %RH and off @ 50 %RH.
  static const uint8_t RH_MIN = 50;
  static const uint8_t RH_MAX = 70;
  if (fan.is_on()) {
    if (humidity < RH_MIN) {
      fan.off();
      vent.close();
    }
  }
  else if (humidity > RH_MAX) {
    vent.open();
    fan.on();
  }
}


Please see the Cosa examples folder for the full listing of the example sketches.

[Update 2013-06-02]

Since the first publishing of this post the NEXA device driver has been improved to make it even easier to handle Remote Control device actions. A NEXA::Receiver::Device class has been added. Below is an example of usage:

class LED : public NEXA::Receiver::Device {
private:
  OutputPin m_pin;
public:
  LED(Board::DigitalPin pin) : NEXA::Receiver::Device(0L), m_pin(pin) {}
  virtual void on_event(uint8_t type, uint16_t value) { m_pin << value; }
};
...
NEXA::Receiver receiver(Board::EXT0);
LED device(Board::LED);
...
void setup()
{
  ...
  // Use polling version to receive the remote button to attach
  NEXA::code_t cmd;
  receiver.recv(cmd);
  device.set_key(cmd);
  receiver.attach(&device);
  ...
  // Enable the interrupt driven version of the receiver
  receiver.enable();
}


The NEXA::Receiver::Device class is used to create a LED output pin that is controlled by the NEXA Remote. The incoming command is filtered by the receiver and the matching device will receive event and the value (on/off/dim level). In the example above the event value is written to the output pin (m_pin). To bind the device to the receiver we need to 1) set the key (address) of the device, 2) attach the device to the receiver. That is all. Once the interrupt handler for the receiver is enabled all incoming commands are compared to the attached devices and events passed on a match. The loop() function looks like this:

void loop()
{
  // Wait for the next event and dispatch
  Event event;
  Event::queue.await(&event);
  event.dispatch();
}


This is the standard event loop for Cosa. Typically in an application there would be several devices and a special "learning" mode where the devices would be bound to buttons on the Remote. The keys would be stored in EEPROM and read on startup. The LED (NEXA::Receiver::Device) class can actually be used for any control that would be the toggling of an output pin and not just LEDs.

The NEXA classes may be used with both ATtinyX4 and ATtinyX5.

[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 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.