Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Using the Adafruit Arduino Logger Shield on a Netduino

C9 Netduino Shield Series - Using Arduino Shields with a Netduino - Part II

Introduction

In our previous article, we examined what an Arduino shield is, how to build a simple custom shield and discussed how to quickly identify shields that are good candidates for a Netduino adaptation versus shields that may not be.

In this article, we’ll take a popular Arduino Logger Shield produced by Adafruit and we’ll interface it with a Netduino / Plus microcontroller

image

The Arduino Logger Shield is an excellent one to start with because it offers immediate benefits to a Netduino / Plus user:

  • Time-keeping
  • SD card storage
  • Two user-controllable LEDs
  • A small prototyping area
  • An onboard 3.3v voltage regulator for clean analog readings and power decoupling

In our C# data logging application, we'll interact with the time keeper, the SD card storage and its 'card detect' pin, the two LEDs as well as a temperature sensor (not included with the shield).

Before diving into the details associated with the hardware, you may want to take a look at the C# objects representing the hardware:

public static readonly string SdMountPoint = "SD";
public static OutputPort LedRed = new OutputPort(Pins.GPIO_PIN_D0, false);
public static OutputPort LedGreen = new OutputPort(Pins.GPIO_PIN_D1, false);
public static InputPort CardDetect = new InputPort(Pins.GPIO_PIN_D3, true, Port.ResistorMode.PullUp);
public static readonly Cpu.Pin ThermoCoupleChipSelect = Pins.GPIO_PIN_D2;
public static DS1307 Clock;
public static Max6675 ThermoCouple;

and their initialization:

public static void InitializePeripherals() {
    LedGreen.Write(true);
    Clock = new DS1307();
    ThermoCouple = new Max6675();
    InitializeStorage(true);
    InitializeClock(new DateTime(2012, 06, 14, 17, 00, 00));
    ThermoCouple.Initialize(ThermoCoupleChipSelect);
    TemperatureSampler = new Timer(new TimerCallback(LogTemperature), null, 250, TemperatureLoggerPeriod);
    LedGreen.Write(false);
}

The SD card, represented by the SdMountPoint string, communicates with the application over SPI. The presence of the SD card in the reader is determined through the CardDetect input pin.

The LEDs are simple outputs that we'll turn ON / OFF as the peripherals gets initialized and file I/Os take place with the SD card.

The clock communicates with the application over the I2C protocol. The clock's most important functions are accessed through the Set() and Get() methods respectively used to set the time once and to get updated time stamps afterward.

The thermocouple communicates over SPI with the application. It exposes a Read() method which caches a raw temperature sample accessed through the Celsius and Fahrenheit properties.

Note: the Netduino Plus already features a built-in microSD card reader, in which case, having another one on the shield is not really needed. Except for this hardware difference, everything else discussed within this article applies equally to the regular Netduino and the Netduino Plus.

Interfacing with the Arduino Logger shield’s hardware

Adafruit is pretty good about making usable products and generally provides Arduino libraries to use with their hardware. Indeed, the Arduino Logger Shield is well documented and comes with two C++ libraries: SD which implements a FAT file system and supporting low-level SD card I/O functions. RTCLib which wraps the I2C interface required to communicate with the DS1307 real time clock.

The SD Card Interface

Let’s deal with the SD card reader and the file system first: a quick review of SD.h reveals two C++ classes:

  • class File : public Stream {} exposing standard read, write, seek, flush file access functions.
  • class SDClass {} exposing storage management such as file and directory operations.

This is good news because the .NET Micro Framework on the Netduino already supports file streams and directory management through the use of the .NET MF System.IO assembly. This assembly comes with the .NET MF SDK port to the Netduino.

clip_image004

By the same token, interfacing with an SD card is provided by an assembly built by Secret Labs named SecretLabs.NETMF.IO which comes with the Netduino SDK.

image

SecretLabs.NETMF.IO provides two functions for 'mounting' and 'un-mounting' an SD card device and the associated FAT file system so that it can be made usable by the .NET MF through assemblies such as System.IO.

It's important to note that the SecretLabs.NETMF.IO assembly must not be deployed with an application targeting the Netduino Plus: on boot, the .NET Micro Framework implementation specific to the Netduino Plus automatically detects and mounts the SD card if one is present in its microSD card reader. This functionality is redundant with the MountSD / Unmount functions provided by the SecretLabs.NETMF.IO assembly which is only needed on Netduino SKUs without a built-in SD card reader.

How does the .NET MF interact with the SD card through the shield?

At this point, it's a good time to review the Arduino Logger Shield's pin-out and the shield's schematics:

image

As we know from our previous article, pins D10-D13 map to the SPI interface and pins A4-A5 map to the I2C interface of the Netduino. On the shield's schematics, the SPI interface leads us to the SD & MMC section of the diagram, connected through a 74HC125N logic-level shifter chip indicated as IC3A-D.

The role of the logic-level shifter is to ensure that logic voltages supplied to the SD card do not exceed 3.3v, even if they come from a microcontroller using 5v logic levels, such as the Arduino. When using an SD card with a Netduino, a level-shifter is not required since all logic levels run at 3.3v on the AT91SAM7x chip but it doesn't interfere with any I/O operations either when the voltage is already 3.3v.

image

The SD card reader in itself is just a passive connector, giving access to the controller built into the SD card. It also provides a mechanical means (i.e. switches) of detecting the presence of a card in the reader (see JP14 pin 1) as well as detecting if the card is write-protected (see JP14 pin 2). We'll make use of the card detection pin in the sample temperature logging application later on.

For background on how SD cards work, the following application note "Secure Digital Card Interface for the MSP430" is excellent and much easier to digest than the extensive 'simplified' SD card protocol specifications provided on the SD Card Association site. The following table taken from the "Secure Digital Card Interface for the MSP430" shows the pin out of an SD card and the corresponding SPI connections:

clip_image012

clip_image014

An SD standard-compliant card can support 3 distinct access modes, each one providing different performance characteristics:

  • SD 1-bit protocol: synchronous serial protocol with one data line, one clock line and one line for commands. The full SD card protocol command set is supported in 1-bit mode.
  • SD 4-bit protocol: this mode is nearly identical to the SD 1-bit mode, except that the data is multiplexed over 4 data lines, yielding up to 4x the performance of SD 1-bit mode. The full SD card protocol command set is supported in 4-bit mode.
  • SPI mode: provide a standard SPI bus interface (/SS, MOSI, MISO, SCK). In SPI mode, the SD card only supports a subset of the full SD card protocol but it is sufficient for implementing a fully functional storage mechanism with a file system.

As you might have guessed, the .NET Micro Framework on the Netduino makes use of the SD card in SPI mode (see \DeviceCode\Drivers\BlockStorage\SD\SD_BL_driver.cpp). The block-oriented SD card I/Os are abstracted thanks to the FAT file system provided by the System.IO assembly (see \DeviceCode\Drivers\FS\FAT\FAT_FileHandle.cpp and FAT_LogicDisk.cpp).

The role of the SecretLabs.NETMF.IO assembly on the Netduino (or its built-in equivalent on the Netduino Plus) is to initialize the SD card in SPI mode during the 'mounting' process by sending the proper set of commands as defined in the SD Card protocol.

In the C# code of the AdafruitNetduinoLogger sample application, which we will review as a whole later on in the code walkthrough section, the following function takes care of the SD card initialization:

public static void InitializeStorage(bool mount) {
    try {
        if (mount == true) {
            StorageDevice.MountSD(SdMountPoint, SPI.SPI_module.SPI1, Pins.GPIO_PIN_D10);
        } else {
            StorageDevice.Unmount(SdMountPoint);
        }
    } catch (Exception e) {
        LogLine("InitializeStorage: " + e.Message);
        SignalCriticalError();
    }
}

Once mounted, the file system is accessed through System.IO calls such as this:

using (var tempLogFile = new StreamWriter(filename, true)) {
    tempLogFile.WriteLine(latestRecord);
    tempLogFile.Flush();
}

Using the StreamWriter class in this context made sense for writing strings as used in the sample application:

clip_image016

However, there are many other file I/O classes available in System.IO that may be better suited depending on the scenario.

The DS1307 real time clock

Our next step is to examine the interface with the DS1307 real time clock (RTC). We'll start by extracting the most important parts of the DS1307 datasheet and reviewing how it's wired up on the shield's schematics.

DS1307 features
  • Real-Time Clock (RTC) Counts Seconds, Minutes, Hours, Date of the Month, Month, Day of the week, and Year with Leap-Year Compensation Valid Up to 2100
  • 56-Byte, Battery-Backed, General-Purpose RAM with Unlimited Writes
  • I2C Serial Interface
  • Programmable Square-Wave Output Signal
  • Automatic Power-Fail Detect and Switch Circuitry
  • Consumes Less than 500nA in Battery-Backup Mode with Oscillator Running

Note: If you need to measure the time something takes in milliseconds, a time granularity that the DS1307 clock does not provide, you can use the Utility functions provided by the .NET Micro Framework like this:

var tickStart = Utility.GetMachineTime().Ticks;

// <...code to be timed...>

var elapsedMs = (int)((Utility.GetMachineTime().Ticks - tickStart) / TimeSpan.TicksPerMillisecond);

This timing method relies on the CPU's internal tick counter and is not 100% accurate due to the overhead of the .NET MF itself but may be sufficient in most scenarios. In addition, the internal tick counter rolls over every so often, something that should be taken into account in production code.

DS1307 register map

Accessing the clock's features comes down reading and writing to and from a set of registers as described on page 8 of the datasheet.

clip_image018

Page 9 of the DS1307 datasheet provides more details about the square wave generation function of the clock, which we will not be using here. The generated square wave signal is available on the shield through connector JP14 on pin 3 as you can see on the schematics below and can be used to provide a slow but reliable external clock signal to another device such as a microcontroller.

clip_image020

clip_image022

DS1307 I2C bus address

The final piece of the puzzle needed before we can use the DS1307 is the device's address on the I2C data bus and its maximum speed (specified at 100 KHz on page 10 of the datasheet). The device address is revealed on page 12 as being 1101000 binary (0x68) along with the two operations modes (Slave Receiver and Slave Transmitter) of the clock. The 8th bit of the address is used by the protocol to indicate whether a 'read' or a 'write' operation is requested.

Note: I2C devices sometime make use of 10-bit addresses. If you aren't familiar with the I2C data bus, you should read the section of the datasheet starting on page 10 which provides a good foundation for understanding how I2C generally works.

It can be summarized as follows:

  • I2C is a 2-wire serial protocol with one bidirectional data line referred to as SDA and one clock line, referred to as SCL.
  • The I2C bus is an open-drain bus (i.e. devices pull the bus low to create a '0' and let go of the bus to create a '1'). To achieve this, I2C requires a pull-up resistor on the SCL and SDA lines between 1.8K ohms and 10K ohms. I2C devices do not need to provide pull-ups themselves if the bus already has them.
  • The I2C master (i.e. the Netduino microcontroller) always provides the clock signal, generally between 100 KHz (or lower) for standard speed devices or 400 KHz for high-speed devices. There's also a 'Fast Mode Plus' allowing for speeds up to 1MHz on devices supporting it. There can be more than one master on the bus even though this is uncommon.
  • An I2C device can have a 7-bit or 10-bit address, allowing for multiple I2C devices to be used on the same bus.
  • I2C read and write operations are transactions initiated by the I2C master targeting a specific device by address. Some I2C slave devices can notify their master that they need to communicate using a bus interrupt.
  • A transaction is framed by 'start' and 'stop signals, with each byte transferred requiring an acknowledgement signal.

image

image

image

At this point, we have all the pieces needed to communicate with the RTC using I2C transactions.

Using the I2C protocol with the .NET Micro Framework

On the Arduino, the library used with the shield to communicate with the DS1307 is a C++ library called RTClib. The header of the library declares a DateTime class, similar in functionality to the standard .NET Micro Framework DateTime class provided by System in the mscorlib assembly. We'll use the standard .NET MF data type to work with the clock instead.

The next declared class is RTC_DS1307 which implements the driver for the DS1307 chip using the Wire library to wrap the I2C protocol. The .NET Micro Framework also supports the I2C protocol through to the Microsoft.SPOT.Hardware assembly. Here again, we'll use the .NET MF implementation of I2C in order to communicate with the clock. However, the I2C transaction patterns implemented by the C++ driver can still provide a useful guide for writing a C# driver for the DS1307 when you don't know where to begin just based on the datasheet.

For instance, the following functions taken from RTClib.cpp shows the call sequence used with the Wiring API to address the date and time registers of the clock:

int i = 0; //The new wire library needs to take an int when you are sending for the zero register

void RTC_DS1307::adjust(const DateTime& dt) {
    Wire.beginTransmission(DS1307_ADDRESS);
    Wire.write(i);
    Wire.write(bin2bcd(dt.second()));
    Wire.write(bin2bcd(dt.minute()));
    Wire.write(bin2bcd(dt.hour()));
    Wire.write(bin2bcd(0));
    Wire.write(bin2bcd(dt.day()));
    Wire.write(bin2bcd(dt.month()));
    Wire.write(bin2bcd(dt.year() - 2000));
    Wire.write(i);
    Wire.endTransmission();
}

DateTime RTC_DS1307::now() {
    Wire.beginTransmission(DS1307_ADDRESS);
    Wire.write(i);
    Wire.endTransmission();
    Wire.requestFrom(DS1307_ADDRESS, 7);
    uint8_t ss = bcd2bin(Wire.read() & 0x7F);
    uint8_t mm = bcd2bin(Wire.read());
    uint8_t hh = bcd2bin(Wire.read());
    Wire.read();
    uint8_t d = bcd2bin(Wire.read());
    uint8_t m = bcd2bin(Wire.read());
    uint16_t y = bcd2bin(Wire.read()) + 2000;
    return DateTime (y, m, d, hh, mm, ss);
}

The final class is RTC_Millis, a utility class converting time data into milliseconds, effectively providing the functionality of the DateTime.Millisecond property on the .NET MF.

Having assessed that the functionality of RTClib only handles date and time registers and knowing the role of the other clock registers, we can proceed with implementing a complete DS1307 C# driver, supporting the square wave and RAM functions, using the native I2C protocol support of the .NET Micro Framework.

The driver starts by defining key constants matching the clock registers according to the datasheet:

[Flags]
// Defines the frequency of the signal on the SQW interrupt pin on the clock when enabled
public enum SQWFreq { SQW_1Hz, SQW_4kHz, SQW_8kHz, SQW_32kHz, SQW_OFF };

[Flags]
// Defines the logic level on the SQW pin when the frequency is disabled
public enum SQWDisabledOutputControl { Zero, One };

// Real time clock I2C address
public const int DS1307_I2C_ADDRESS = 0x68;

// Start / End addresses of the date/time registers
public const byte DS1307_RTC_START_ADDRESS = 0x00;
public const byte DS1307_RTC_END_ADDRESS = 0x06;

// Start / End addresses of the user RAM registers
public const byte DS1307_RAM_START_ADDRESS = 0x08;
public const byte DS1307_RAM_END_ADDRESS = 0x3f;

// Square wave frequency generator register address
public const byte DS1307_SQUARE_WAVE_CTRL_REGISTER_ADDRESS = 0x07;

// Start / End addresses of the user RAM registers
public const byte DS1307_RAM_START_ADDRESS = 0x08;
public const byte DS1307_RAM_END_ADDRESS = 0x3f;

// Total size of the user RAM block
public const byte DS1307_RAM_SIZE = 56;

Next the driver defines an I2C device object representing the clock:

// Instance of the I2C clock
protected I2CDevice Clock;

In the class constructor, the I2C clock device is initialized, specifying its address and speed in KHz:

public DS1307(int timeoutMs = 30, int clockRateKHz = 50) {
    TimeOutMs = timeoutMs;
    ClockRateKHz = clockRateKHz;
    Clock = new I2CDevice(new I2CDevice.Configuration(DS1307_I2C_ADDRESS, ClockRateKHz));
}

The driver retrieves the date and time from the clock through a Get function returning a DateTime object.

public DateTime Get() {
    byte[] clockData = new byte [7];

    // Read time registers (7 bytes from DS1307_RTC_START_ADDRESS)
    var transaction = new I2CDevice.I2CTransaction[] {
    I2CDevice.CreateWriteTransaction(new byte[] {DS1307_RTC_START_ADDRESS}),
        I2CDevice.CreateReadTransaction(clockData)
    };

    if (Clock.Execute(transaction, TimeOutMs) == 0) {
        throw new Exception("I2C transaction failed");
    }

    return new DateTime(
        BcdToDec(clockData[6]) + 2000, // year
        BcdToDec(clockData[5]), // month
        BcdToDec(clockData[4]), // day
        BcdToDec(clockData[2] & 0x3f), // hours over 24 hours
        BcdToDec(clockData[1]), // minutes
        BcdToDec(clockData[0] & 0x7f) // seconds
    );
}

Let's break it down:

  • A 7-byte array is allocated which will receive the raw date and time data registers, starting at address DS1307_RTC_START_ADDRESS (0x00) and ending at DS1307_RTC_END_ADDRESS (0x06).
  • An I2C transaction object is allocated, comprising two parameters:
    • A 'write' transaction object telling the DS1307 device which register address to start reading data from. In this case, this is DS1307_RTC_START_ADDRESS (0x00), the very first time-keeping register.
    • A 'read' transaction object specifying where the clock's time-keeping data registers will be stored, implicitly defining the total number of bytes to be read and acknowledged.
  • Clock.Execute is the function calling into the .NET MF I2C interface to run the prepared transactions. The second parameter specifies a time out value expressed in milliseconds before the transaction fails, resulting in a generic exception being thrown.
  • When the transactions succeed, a DateTime object is instantiated with the 7 time-keeping registers returned by the 'read' transaction. Each register is converted from Binary Coded Decimal form to decimal form using a custom utility function:

protected int BcdToDec(int val) {
    return ((val / 16 * 10) + (val % 16));
}

Conversely, the driver provides a Set function to update the clock's time-keeping registers. Because the driver doesn't expect a response from the DS1307 in this scenario, the I2C transaction is write-only. The fields of the DateTime parameter corresponding to the time -keeping registers are converted from decimal form to BCD form and stuffed in a 7-byte array before executing the transaction.

public void Set(DateTime dt) {
    var transaction = new I2CDevice.I2CWriteTransaction[] {
        I2CDevice.CreateWriteTransaction(new byte[] {
            DS1307_RTC_START_ADDRESS,
            DecToBcd(dt.Second),
            DecToBcd(dt.Minute),
            DecToBcd(dt.Hour),
            DecToBcd((int)dt.DayOfWeek),
            DecToBcd(dt.Day),
            DecToBcd(dt.Month),
        DecToBcd(dt.Year - 2000)} )
    };

    if (Clock.Execute(transaction, TimeOutMs) == 0) {
        throw new Exception("I2C write transaction failed");
    }
}

The rest of the functions provided by the C# driver implement the other DS1307 features, such as

  • SetSquareWave
  • Halt
  • SetRAM
  • GetRAM
  • The [] operator used to access a specific clock register
  • WriteRegister

In all case, these functions are wrappers around the 'read' and 'write' I2C transaction model, involving the appropriate DS1307 registers as defined in the datasheet. 

Using the Adafruit Arduino Logger Shield as a temperature logger

image

To illustrate the points discussed so far, we'll use the Adafruit Arduino Logger shield with a Netduino and a MAX6675 thermocouple amplifier for the purpose of recording ambient temperature samples at ten second intervals.

Each record includes a date, a time and the temperature expressed in Celsius and Fahrenheit. The records are written to daily files in CSV format for easy export to a spreadsheet, making the application easily adaptable for acquiring data from different sensors:

Date

Time

Celsius

Fahrenheit

6/14/2012

15:35:00:05

18.75

65.75

6/14/2012

15:35:10:05

18

64.4

6/14/2012

15:35:20:05

18.5

65.29

6/14/2012

15:35:30:05

18

64.4

6/14/2012

15:35:40:05

18

64.4

6/14/2012

15:35:50:05

18.75

65.75

 

Device Connections

Instead of permanently soldering the temperature sensor to the prototyping area of the shield, female / female jumper wires were used to make connections between the shield's own pin headers as well as the thermocouple's male pin headers.

LoggerShieldBoard

The following table enumerates these connections:

Shield Pin

Destination Pin

3v (Power header)

Max6675 VCC

GND (Power or Digital I/O header)

Max6675 GND

D13 (Digital I/O header, SPI CLK)

Max6675 CLK (SPI CLK)

D12 (Digital I/O header, SPI MISO)

Max6675 DO (SPI MISO)

D2 (Digital I/O header, used as SPI /SS)

Max6675 CS (SPI /SS)

L1 (LEDS header)

D1 (Digital I/O header)

L2 (LEDS header)

D0 (Digital I/O header)

CD (SD card detect)

D3 (Digital I/O header)

 
Reading temperature using an Adafruit Max6675 Thermocouple amplifier breakout board

The Max6675 thermocouple amplifier chip on the breakout board is a read-only SPI device. When the CS pin (SPI /SS) of the device is asserted with a 1ms delay before reading, the chip returns a 12-bit value on its DO pin (SPI MISO) corresponding to the temperature measured by a K-type Thermocouple wire. The resulting C# driver for the Max6675 is short:

using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;

namespace Maxim.Temperature{
    public class Max6675 : IDisposable {
        protected SPI Spi;

        public void Initialize(Cpu.Pin chipSelect) {
            Spi = new SPI(
            new SPI.Configuration(
            chipSelect, false, 1, 0, false, true, 2000, SPI.SPI_module.SPI1)
            );
        }

        public double Celsius {
            get { return RawSensorValue * 0.25; }
        }

        public double Farenheit {
            get { return ((Celsius * 9.0) / 5.0) + 32; }
        }

        protected UInt16 RawSensorValue;
        protected byte[] ReadBuffer = new byte[2];
        protected byte[] WriteBuffer = new byte[2];

        public void Read() {
            RawSensorValue = 0;
            Spi.WriteRead(WriteBuffer, ReadBuffer);
            RawSensorValue |= ReadBuffer[0];
            RawSensorValue <<= 8;
            RawSensorValue |= ReadBuffer[1];

            if ((RawSensorValue & 0x4) == 1) {
                throw new ApplicationException("No thermocouple attached.");
            }

            RawSensorValue >>= 3;
        }

        public void Dispose() {
            Spi.Dispose();
        }

        ~Max6675() {
            Dispose();
        }
    }
}

Temperature logger application walkthrough

Let's review the key parts of the temperature logging application code and how it interacts with the devices connected to the shield.

public static readonly string SdMountPoint = "SD";

Defines an arbitrary string used to refer to the SD card when using StorageDevice.MountSD and StorageDevice.Unmount functions.

public static readonly int TemperatureLoggerPeriod = 10 * 1000; // milliseconds

Defines the interval between temperature samples.

public static OutputPort LedRed = new OutputPort(Pins.GPIO_PIN_D0, false);

Defines an output connected to pin D0 controlling the state of the red LED on the shield.

public static OutputPort LedGreen = new OutputPort(Pins.GPIO_PIN_D1, false);

Defines an output connected to pin D1 controlling the state of the green LED on the shield.

public static InputPort CardDetect = new InputPort(
    Pins.GPIO_PIN_D3,
    true,
    Port.ResistorMode.PullUp);

Defines an input connected to pin D3 used to determine if an SD card is inserted in the SD socket.

public static ManualResetEvent ResetPeripherals = new ManualResetEvent(false);

Defines a manual reset event object that will be used in the main application loop to determine when to re-initialize the shield's peripherals.

public static readonly Cpu.Pin ThermoCoupleChipSelect = Pins.GPIO_PIN_D2;

Defines D2 as the SPI chip select pin connected to the Max6675 Thermocouple board.

public static Timer TemperatureSampler;

Defines an instance of a timer object which will drive temperature sampling.

public static DS1307 Clock;

Defines an instance of the DS1307 real time clock driver.

public static Max6675 ThermoCouple;

Defines an instance of the Max6675 thermocouple driver.

public static ArrayList Buffer = new ArrayList();

Defines an array list instance which will be used as a temporary buffer when the SD card is removed from its socket.

The application's main loop is only concerned about the state of the peripherals:

  • It initializes the devices connected to the shield
  • It waits indefinitely for a signal indicating that a hardware error occurred
  • It disposes of the current device instances and starts over

public static void Main() {
    while (true) {
        InitializePeripherals();
        ResetPeripherals.WaitOne();
        ResetPeripherals.Reset();
        DeInitializePeripherals();
    }
}

InitializePeripherals indicates that it is working by controlling the green LED on the shield. Its role is focused on object creation and initialization.

public static void InitializePeripherals() {
    LedGreen.Write(true);
    Clock = new DS1307();
    ThermoCouple = new Max6675();
    InitializeStorage(true);
    InitializeClock(new DateTime(2012, 06, 14, 17, 00, 00));
    ThermoCouple.Initialize(ThermoCoupleChipSelect);
    TemperatureSampler = new Timer(
    new TimerCallback(LogTemperature),
    null,
    250,
    TemperatureLoggerPeriod);
    LedGreen.Write(false);
}

If the initialization of a peripheral fails, the shield will quickly blink its LEDs, indefinitely:

public static void SignalCriticalError() {
    while (true) {
        LedRed.Write(true);
        LedGreen.Write(true);
        Thread.Sleep(100);
        LedRed.Write(false);
        LedGreen.Write(false);
        Thread.Sleep(100);
    }
}

The clock initialization function only sets the clock date and time when it is unable to find a file named 'clockSet.txt' on the SD card, ensuring that the initialization of the DS1307 only happens once in the InitializePeripherals function or until the file is deleted.

public static void InitializeClock(DateTime dateTime) {
    var clockSetIndicator = SdMountPoint + @"\clockSet.txt";

    try {
        if (File.Exists(clockSetIndicator) == false) {
            Clock.Set(dateTime);
            Clock.Halt(false);
            File.Create(clockSetIndicator);
        }
    } catch (Exception e) {
        LogLine("InitializeClock: " + e.Message);
        SignalCriticalError();
    }
}

The LogTemperature function is the callback invoked by the Timer object every 10 seconds. The function indicates that it is working by turning the red LED on the shield ON and OFF.

public static void LogTemperature(object obj) {
    LedRed.Write(true);
}

The function reads the current time from the clock with Clock.Get() and takes a temperature sample with ThermoCouple.Read().

var tickStart = Utility.GetMachineTime().Ticks;
var now = Clock.Get();
ThermoCouple.Read();
var elapsedMs = (int)((Utility.GetMachineTime().Ticks - tickStart) / TimeSpan.TicksPerMillisecond);

Then, it concatenates a string containing the date, time and temperature expressed in Celsius and Fahrenheit, with each field separated by commas.

var date = AddZeroPrefix(now.Year) + "/" + AddZeroPrefix(now.Month) + "/" + AddZeroPrefix(now.Day);
var time = AddZeroPrefix(now.Hour) + ":" + AddZeroPrefix(now.Minute) + ":" + AddZeroPrefix(now.Second) + ":" + AddZeroPrefix(elapsedMs);
var celsius = Shorten(ThermoCouple.Celsius.ToString());
var farenheit = Shorten(ThermoCouple.Farenheit.ToString());
var latestRecord = date + "," + time + "," + celsius + "," + farenheit;

To make the data more manageable, daily temperature files are created as needed, each one starting with the column headers expected for parsing the values in CSV format.

var filename = SdMountPoint + BuildTemperatureLogFilename(now);
if (File.Exists(filename) == false) {
    using (var tempLogFile = new StreamWriter(filename, true)) {
        tempLogFile.WriteLine("date,time,celsius,fahrenheit");
    }
}

The temperature sampling application lets the user remove the SD card from its socket so that the CSV files can be moved over to a PC for processing without losing data in the meantime. In order to do this, the application checks the state of the 'Card Detect' pin before attempting file system I/Os.

When the SD card is not present, the latest temperature record is preserved in the array list buffer until the SD card is put back in its socket. The array list data is then flushed to storage.

if (CardDetect.Read() == false) {
    using (var tempLogFile = new StreamWriter(filename, true)) {
        if (Buffer.Count != 0) {
            foreach (var bufferedLine in Buffer) {
                tempLogFile.WriteLine(bufferedLine);
            }
            Buffer.Clear();
        }
        tempLogFile.WriteLine(latestRecord);
        tempLogFile.Flush();
    }
} else {
    LogLine("No card in reader. Buffering record.");
    Buffer.Add(latestRecord);
}

The temperature logging function expects to run out of memory if the array list buffer grows too large, in which case, all the records get purged. Other memory management strategies could be used to mitigate data loss in this case. However, this depends entirely on the requirements of the data logging application and is out of scope for this discussion.

catch (OutOfMemoryException e) {
    LogLine("Memory full. Clearing buffer.");
    Buffer.Clear();
}

The temperature logging function also handles file system exceptions caused by the removal of the SD card and reacts by signaling the ResetPeripherals event. In turn, this lets the application's main loop know that the peripherals, and most specifically the SD card, need to be recycled and initialized again in order to recover from the error.

catch (IOException e) {
    LogLine("IO error. Resetting peripherals.");
    Buffer.Add(latestRecord);
    ResetPeripherals.Set();
}

Conclusion

In this article, we took a shield designed for the Arduino and learned how to critically review the Arduino code libraries supporting it, drawing parallels with features offered by the .NET Micro Framework. This process allowed us to identify areas in the Arduino code which were not necessary to port over to C# such as SD card and file system handlers. It also allowed us to see the similarities in the way the Arduino and the Netduino handle I2C communications.

Most importantly, we also learned the importance of reviewing a device's schematics and component datasheets to ensure that important features have not been omitted and potentially incorrectly implemented when considering using an unknown library: in the case of RTClib, we saw that the implementation was limited to the basic date and time functions of the DS1307, leaving out other useful features such as the clock's built-in RAM and the square wave generation functions.

In our next article, we'll take on a much more complex shield and we will learn how to analyze Arduino libraries in depth before porting them from C/C++ to C#.

Bio

Fabien is the Chief Hacker and co-founder of Nwazet, a start-up company located in Redmond WA, specializing in Open Source software and embedded hardware design. Fabien's passion for technology started 30 years ago, creating video games for fun and for profit. He went on working on mainframes, industrial manufacturing systems, mobile and web applications. Before Nwazet, Fabien worked at MSFT for eight years in Windows Core Security, Windows Core Networking and Xbox. During downtime, Fabien enjoys shooting zombies and watching sci-fi.

Tags:

Follow the Discussion

  • I found this to be an extremely helpful article.  I've recently been puttering around with interfacing Arduino shields and sensors to various 3.3V devices, and this clarified quite a few things for me.

    A couple of things I'd love to see covered in future articles are

    - interfacing 5V analog sensors, especially in shields where there isn't much room for inserting voltage dividers

    - options for interfacing the Netduino to sensors that require "bit banging" interfaces, like the DHT11/22 temperature sensor. 

     

    Thanks,

    Dan.

  • FreemanFreeman

    I totally enjoyed this post very much altough i am from the managed code world (.net for windows), i am surprised that i understood how things work :), looking forward on any similar posts.

  • Thanks a lot for this very complete post. One little question about the pull-up resistor, already have been searching around for more info on pull-up resistor. But for the I2C bus, does this bus have such a high capacitance that pull-up > 10k will limit the speed of the bus?

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.