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. 

Sunday, April 28, 2013

Programming ATtiny

With todays technology it is both easy and cheap to program micro controllers. The Cosa framework together with the Arduino core for ATtiny 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 (SUART) 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 and update your Arduino IDE with the Arduino ATtiny core. 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 Arduino ISP sketch is part of the Arduino example code and may be found in the Arduino IDE menu. The Arduino ATtiny core must be downloaded and install. See the MIT high-low tech tutorial page

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 Arduino ISP in the Tools 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 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.

class SUART : 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 LCD is made easy as both are Cosa IOStream::Device's.

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 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();
}

All Cosa classes are not currently available for the ATtiny. Especially support for TWI and SPI are not yet available. This implies that device drivers that use these drivers are also not available. 

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

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 challange as the protocol stack must be built from the ground up. 

Monday, April 15, 2013

A Virtual Wire Digital Thermometer

In this post we will look into 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.

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.

Tuesday, March 26, 2013

Toward Reliable Virtual Wire Interface Messaging

As described in the previous posting on the Virtual Wire Interface (VWI) the communication channels are not reliable and lack node addressing. Messages may be lost due to noise, collision, etc. The basic message passing provided by VWI or the original VirtualWire library is boardcast and the application must, if needed, realize a method of addressing and retransmission. This posting presents the Cosa VWI client-server example sketches demonstrating basic implementation techniques addressing these issues.

The first step is to design a protocol for addressing and retransmission. The simplest method is to add a client node address and message sequence number to the data messages sent from the client to the server. The server should transmit an acknowledgement message back to the client. The client will retransmit the message until an acknowledge is received (CosaVWIclient.ino). To keep things simple, data is only sent from the client to the server. The server only sends acknowledgements to one client at a time. 

There are error situation that are ignored in this simple solution. A final solution should include a maximum limit of retransmission. If this limit is exceeded the communication channel, server, should be regarded as inoperative; e.g. server is down.

The message structure for this simple protocol will be sent as the payload of the VWI message. The data message sent from client to server will contain the address of the client (32-bits), a message sequence number (8-bit), and data payload. The acknowledgement message sent back from the server to the client will contain the client address and the message sequence as received in the data message. Additional data could be piggy-backed with the acknowledgement. This is a common method of reducing messages in protocols. The above extra message fields are often hidden from the application and part of the protocol stack.

Note that messages are sent in binary form (struct) without any special serialization. The sending of a message  may be viewed as copying a memory block between processors. This works nicely in a homogeneous environment such as sending between Arduino's but would give problems between processors with different byte order/data representation. The data representation below is in a portable data width but the byte order dependents on the host architecture.

AVR is little endian (LSB first) while network order is big endian (MSB first). Many of the original VirtualWire examples use text strings as messages. Sending, for instance, an integer in textual representation (ASCII) avoids the byte order issue but may be inefficient, require longer messages, be more difficult to extend and requires more processing to translate to and from text.

// Data message type
struct msg_t {
  uint32_t id;
  uint8_t nr;
  uint16_t data[12];
};

// Acknowledge message type
struct ack_t {
  uint32_t id;
  uint8_t nr;
};


To improve the message passing between processors with different byte order the data could be put in a specific order before transmission and put back when received. This is know as network order. Typically the functions/macros hton() (host-to-network) and ntoh() (network-to-host) are used to force byte order on the data fields in the message.

// Encoder/decoder for the Virtual Wire Interface
VirtualWireCodec codec;

// Virtual Wire Interface Transmitter and Receiver
VWI::Transmitter tx(Board::D9, &codec);
VWI::Receiver rx(Board::D8, &codec);
const uint16_t SPEED = 4000;

void setup()
{
  ...
  // Start virtual wire interface, transmitter and receiver
  VWI::begin(SPEED);
  tx.begin();
  rx.begin();
}


The above snippet from CosaVWIclient.ino is the setup() section for the Virtual Wire Interface (VWI). Please note how the transmitter and receiver are declared to use the VirtualWire Codec and the Board pins D9 and D8. Also that the transmitter and receiver are separate objects.

In the loop() section below, the client constructs a message with the node address (0xC05A0001) and the message sequence number (cnt). In the example sketch the data send is two analog pin readings and a rotating data pattern.

  // Statistics; Number of messages and error count (retransmissions)
  static uint16_t cnt = 0;
  static uint16_t err = 0;

  // Message types (data and acknowledgement)
  msg_t msg;
  ack_t ack;
 

  // Initiate the message with id, sequence number and payload data
  msg.id = 0xC05A0001;
  msg.nr = cnt++;
  msg.data[0] = luminance.sample();
  msg.data[1] = temperature.sample();
  for (uint8_t i = 2; i < membersof(msg.data); i++)
    msg.data[i] = ((cnt << 8) | ((i << 4) + i)) ^ 0xa5a5;


The client will send the message and wait for an acknowledgement. A retransmission will occur if an acknowledgement is not received within the time limit, 64 ms, or the acknowledgement message was wrong.

  // Send message and receive acknowledgement
  uint8_t nr = 0;
  int8_t len;
  do {
    nr += 1;
    tx.send(&msg, sizeof(msg));
    tx.await();
    len = rx.recv(&ack, sizeof(ack), 64);
    if (len != sizeof(ack))
      DELAY(300);
  } while (len != sizeof(ack) || (ack.nr != msg.nr) || (ack.id != msg.id));


  // Check if a retransmission did occur and print statistics
  if (nr > 1) {
    err += 1;
    INFO("cnt = %ud, err = %ud, nr = %ud (%ud%%)",
     cnt, err, nr, (err * 100) / cnt);
  }


The client will collect statistics on the number of retransmission and the total number of errors. The DELAY(300) is used to reduce collision between retransmission and acknowledgements.

The server is even simpler then the client as it only receives data messages, sends acknowledgements and "processes" new messages (CosaVWIserver.ino). Below is essential section of the server loop().

  // Wait for a message
  rx.await();
  msg_t msg;
  int8_t len = rx.recv(&msg, sizeof(msg));

  // Check that the correct message size was received
  if (len != sizeof(msg)) return;
 
  // Send an acknowledgement
  ack_t ack;
  ack.id = msg.id;
  ack.nr = msg.nr;
  tx.send(&ack, sizeof(ack));
  tx.await();


The above retransmission solution must be extended with additional logic to handle multiple clients and/or channels per client. Also the protocol header (address and sequence number) may be compressed to reduce the size of the messages, and improve wireless bandwidth efficiency. There are also techniques to improve throughput and flow-control with a sliding window protocol. The above sketch is the basic framework for reliable messaging.

The two VWI example sketches may be used to benchmark the different Codec's with regard to error and retransmission rate. The bandwidth efficiency may be calculated from the statistics and the encoder parameters. The original VirtualWire Codec is 4 to 6 bits (+50%), Manchester 4 to 8 bits (+100%), 4B5B and fixed bitstuffing are 4 to 5 bits (+25%).

Typical retransmission rates are in the order of 1-5% depending on antenna, bit-rate/speed, noise and distance but also the length of the message; i.e. number of bits used to transmit the message. With even distribution of noise, collisions, etc, the probability of an error will increase with the number of bits sent.

Another factor is how well the software Phase-Locked-Loop (PLL) in the receiver code can regenerate the clock and sample the data channel correctly. Where Manchester code increases the number of bits with 100% the clock is perfectly recovered and error due to drift are almost zero.

The Next Step

The above examples are part of the prototyping for further development of the Virtual Wire Interface (VWI) in Cosa. The idea is to evolve VWI to handle both addressing and automatic retransmission as this is a very common usage pattern for wireless connections. The current proposed changes to the API are to 1) add a new network address parameter to the begin() method, 2) some new functions for retransmission control and statistics. All other details about the address matching and retransmission would be hidden.

An addressing scheme is proposed to be added to allow server and client address matching. Depending on the number of units needed in a small network the address would be 16- or 32-bits. A simple interpretation of the address may be used to distinguish between servers and clients. A servers would have a network address where the lowest byte is zero(0). A server would be allowed to have at most 255 clients. On recv() a server will match addresses with the same highest bytes in the address as the server it self while clients would match the whole address for acknowledgement frames.

VWI: [preamble, start-symbol, size, payload, checksum]
VWI+: [preamble, start-symbol, size, address, seqnr, payload, checksum]

The VWI frame contains a preamble sequence, a start symbol, frame size, payload and a 16-bit checksum. This would be extended (VWI+) to include the network address and a frame sequence number. The send() and recv() methods would be as VWI and the handling of the new frame fields would be internal.

Sunday, March 24, 2013

Object-Oriented Interrupt Handling

Interrupt Service Routines (ISR) are traditionally written as callback functions. AVR provides a set of interrupt vector callback functions that may be defined in application code. These are defined by using the macro ISR() or SIGNAL().

Cosa approaches the challange of integrating ISR functions into an object-oriented context with a number of design patterns. The first design pattern is to define an abstract class, interface, for Interrupt Handlers; Cosa/Interrupt.hh. This is used to achieve a common virtual method prototype for interrupt callback functions. The provided mapping is basically from the ISR() function to the Interrupt Handler method on_interrupt(). The major difference is that the object-oriented method will execute with the instance as a context and have access to the instance member variables. The traditional ISR() callback function often needs global variables for the context and cannot be shared among interrupt service routines.

class Interrupt {
public:
  class Handler {
  public:
    virtual void on_interrupt() {}
    virtual void on_interrupt(uint8_t arg) {}
    virtual void on_interrupt(uint16_t arg) {}

  };
};


The second design pattern is to allow ISR() functions to access the internals of the class as if they where part of the class definition. In C++ this can be achieved with a friend declaration of the function in the class. The C functions need to be declared as extern first. Below is a snippet from the Cosa class for handling External Interrupts (Cosa/Pins.hh) to give an idea of how this works. 

extern "C" void INT0_vect(void) __attribute__ ((signal));
extern "C" void INT1_vect(void) __attribute__ ((signal));


class ExternalInterruptPin :
  public InputPin,
  public Event::Handler,
  public Interrupt::Handler {
private:
  friend void INT0_vect(void);
  friend void INT1_vect(void);

  ...
  virtual void on_interrupt();
  ...

};

The ExternalInterruptPin class uses multiple inheritance and inherits from three classes; the InputPin, Event and Interrupt Handling.

 Fig.1: ExternalInterruptPin class hierarchy

The InputPin provides that basic functionality for handling the pin, the Interrupt::Handler asynchronous events and Event::Handler synchronous events.

For the standard Arduino board external interrupt pins, AVR defines two callback function in the interrupt vector. These have the same names as in the ATmega328P documentation (INT0_vect and INT1_vect). The Cosa implementation of these functions will call the Interrupt Handler on_interrupt() method.

ISR(INT0_vect)
{
  if (ExternalInterruptPin::ext[0] != 0)
    ExternalInterruptPin::ext[0]->on_interrupt();
}


The ExternalInterruptPin constructor will register the instance. The default interrupt handler performs a translation to Event::CHANGE_TYPE. This reduces the amount of processing in the interrupt service routine.

void
ExternalInterruptPin::on_interrupt()
{
  Event::push(Event::CHANGE_TYPE, this);
}


The Cosa implementation of handling of external interrupts allows two levels of extension. The normal usage of ExternalInterruptPin is to sub-class and override the virtual methods on_interrupt() and/or on_event().

An example of usage is the Cosa IR class, Cosa/IR.hh. The IR::Receiver class uses an External Interrupt Pin to read a sequence of pulses from TSOP4838 (IR Receiver Modules for Remote Control Systems).

class IR {
public:

  class Receiver : private ExternalInterruptPin, private Link {
    ...
  private:

    ...
    /**
     * @override
     * Interrupt pin handler: Measure time periods of pulses in sequence
     * from IR receiver circuit. Push an event when a full sequence has
     * been recieved; READ_COMPLETED(this, code) where the code is the
     * recieved binary code or key if a key map was provided.
     */
    virtual void on_interrupt();
    ...

};

The Event::Handler (Cosa/Event.hh) is also an abstract class to define the callback structure for events.

class Event {
public:

  ...
  class Handler {

  public:
    /**
     * Default null event handler. Should be redefined by sub-classes.
     * Called by Event::dispatch().
     * @param[in] type the event type.
     * @param[in] value the event value.
     */
    virtual void on_event(uint8_t type, uint16_t value) {}
  };
  ...

};

The above design pattern for Object-Oriented Interrupt Handling have been applied to several of the Arduino libraries that have been ported and/or rewritten in the object-oriented style of Cosa. The most important attribute is to achieve higher levels of encapsulation, performance and code quality.