Electric Imp Developer Wiki
This example is for the Hannah development board. It will introduce you to the I2C peripheral and the SX1509 IO expander, and provide some colorful eye candy too.
The IO Expander, one of Hannah's on-board peripherals, offers 16 GPIO lines with optional LED drive while requiring only two or three imp pins. Two of the pins are for the I2C port (clock and data) while the optional pin is for an interrupt input (which is not used by this example). The LED drive feature of the SX1509 is very useful - the chip can provide PWM intensity control with 8-bit resolution, timed blinking and automatic fading between levels. Hannah features an RGB LED connected to IO pins 7, 6 and 5 of the Expander. By programming the Expander appropriately we can recreate any color with 24-bit resolution.
Our example code will blink random colors.
// Color Blink code example for Hannah
The first section of code comprises a convenient class for driving the SX1509 IO expander. This functionality will be used by many applications and can be conveniently copied and pasted as required. We'll look at each method here in some detail, but for most purposes it's not necessary to be familiar with what's happening within the class to access the higher level features of the board. Some of the methods are not used by this example, but will be useful later.
// IO Expander Class for SX1509 class IoExpander { i2cPort = null; i2cAddress = null;
The Expander is connected to the imp by I2C, a bus which can connect multiple peripherals using only two wires. Each peripheral has a unique address which is set by the manufacturer (sometimes with a options set by the hardware designer to allow for multiple devices of the same type to be connected to the same bus).
The constructor takes the I2C port ID (I2C_12 or I2C_89) and I2C hardware address, so the class can be easily applied to other hardware designs. In our case the SX1509BULTRT Datasheet and Hannah Schematic show us that the Expander's address is 0x3E, which we'll pass later when we construct the class. Note that the address is shifted before we save it in our member data. I2C addresses are 7 bits in size, while bit 0 is used to indicate a read (0) or write (1) operation, therefore we'll shift the address left by one bit.
constructor(port, address) { if(port == I2C_12) { // Configure I2C bus on pins 1 & 2 hardware.configure(I2C_12); i2cPort = hardware.i2c12; } else if(port == I2C_89) { // Configure I2C bus on pins 8 & 9 hardware.configure(I2C_89); i2cPort = hardware.i2c89; } else { server.log("Invalid I2C port specified."); } i2cAddress = address << 1; }
Now for the member functions.
The read method calls i2c.read to fetch a single byte from the specified register. Although the Expander has a single I2C address, it has many internal registers which are addressed separately. An I2C read operation comprises writing the hardware address and register offset before reading back the response - the i2c API class handles all of this for us.
Should the operation fail for any reason (typically an incorrect address) we'll return -1, otherwise we'll return the data.
// Read a byte function read(register) { local data = i2cPort.read(i2cAddress, format("%c", register), 1); if(data == null) { server.log("I2C Read Failure"); return -1; } return data[0]; }
The write method calls i2c.write to send a single byte to the specified register. Any failure would be silent.
// Write a byte function write(register, data) { i2cPort.write(i2cAddress, format("%c%c", register, data)); }
We could use the read and write methods to access the full functionality of the IO Expander, however for convenience we'll define some additional methods to help with common operations.
The writeBit method sets or clears a single bit in the specified register.
// Write a bit to a register function writeBit(register, bitn, level) { local value = read(register); value = (level == 0)?(value & ~(1<<bitn)):(value | (1<<bitn)); write(register, value); }
The writeMasked function writes a bit pattern masked with the specified register.
// Write a masked bit pattern function writeMasked(register, data, mask) { local value = read(register); value = (value & ~mask) | (data & mask); write(register, value); }
The setPin method sets or clears a specified GPIO pin (the Expander has 16 GPIOs, identified as 0 through 15).
// Set a GPIO level function setPin(gpio, level) { writeBit(gpio>=8?0x10:0x11, gpio&7, level?1:0); }
The setDir method configures the specified GPIO as either an input (0) or an output (1).
// Set a GPIO direction function setDir(gpio, output) { writeBit(gpio>=8?0x0e:0x0f, gpio&7, output?0:1); }
The setPullUp method enables or disables an internal pull up resistor for the specified GPIO.
// Set a GPIO internal pull up function setPullUp(gpio, enable) { writeBit(gpio>=8?0x06:0x07, gpio&7, enable); }
The setIrqMask method configures whether the specified GPIO will trigger an interrupt. The Expander interrupt pin is connected to Hannah on pin 1 (which can be used to wake up the imp from deep sleep).
// Set GPIO interrupt mask function setIrqMask(gpio, enable) { writeBit(gpio>=8?0x12:0x13, gpio&7, enable); }
The setIrqEdges method configures which edges trigger an interrupt for the specified GPIO. Any combination of rising and falling edges may be specified (1 = will interrupt).
// Set GPIO interrupt edges function setIrqEdges(gpio, rising, falling) { local addr = 0x17 - (gpio>>2); local mask = 0x03 << ((gpio&3)<<1); local data = (2*falling + rising) << ((gpio&3)<<1); writeMasked(addr, data, mask); }
The clearIrq method will clear an interrupt on the specified GPIO.
// Clear an interrupt function clearIrq(gpio) { writeBit(gpio>=8?0x18:0x19, gpio&7, 1); }
The getPin method will get the state of the specified GPIO.
// Get a GPIO input pin level function getPin(gpio) { return (read(gpio>=8?0x10:0x11)&(1<<(gpio&7)))?1:0; } }
We'll derive from IoExpander to create a class to represent an RGB LED. This is really three separate LEDs conveniently packaged together.
The constructor will accept and store the pin assignments for each LED (remember these are IO pins on the Expander, not imp pins) and configure the registers to set LED drive mode.
// RGB LED Class class RgbLed extends IoExpander { // IO Pin assignments pinR = null; pinG = null; pinB = null; constructor(port, address, r, g, b) { base.constructor(port, address); // Save pin assignments pinR = r; pinG = g; pinB = b; // Disable pin input buffers writeBit(pinR>7?0x00:0x01, pinR>7?(pinR-7):pinR, 1); writeBit(pinG>7?0x00:0x01, pinG>7?(pinG-7):pinG, 1); writeBit(pinB>7?0x00:0x01, pinB>7?(pinB-7):pinB, 1); // Set pins as outputs writeBit(pinR>7?0x0E:0x0F, pinR>7?(pinR-7):pinR, 0); writeBit(pinG>7?0x0E:0x0F, pinG>7?(pinG-7):pinG, 0); writeBit(pinB>7?0x0E:0x0F, pinB>7?(pinB-7):pinB, 0); // Set pins open drain writeBit(pinR>7?0x0A:0x0B, pinR>7?(pinR-7):pinR, 1); writeBit(pinG>7?0x0A:0x0B, pinG>7?(pinG-7):pinG, 1); writeBit(pinB>7?0x0A:0x0B, pinB>7?(pinB-7):pinB, 1); // Enable LED drive writeBit(pinR>7?0x20:0x21, pinR>7?(pinR-7):pinR, 1); writeBit(pinG>7?0x20:0x21, pinG>7?(pinG-7):pinG, 1); writeBit(pinB>7?0x20:0x21, pinB>7?(pinB-7):pinB, 1); // Set to use internal 2MHz clock, linear fading write(0x1e, 0x50); write(0x1f, 0x10); // Initialise as inactive setLevels(0, 0, 0); setPin(pinR, 0); setPin(pinG, 0); setPin(pinB, 0); }
The setLed method enables or disables each color segment, or makes no change given a null parameter. This will toggle the segment between off and the current brightness level.
// Set LED enabled state function setLed(r, g, b) { if(r != null) writeBit(pinR>7?0x20:0x21, pinR&7, r); if(g != null) writeBit(pinG>7?0x20:0x21, pinG&7, g); if(b != null) writeBit(pinB>7?0x20:0x21, pinB&7, b); }
The setLevels method sets the intensity for each color segment or makes no change given a null parameter. Intensity control is achieved by varying the duty ratio of a PWM signal. The eye cannot see the flicker as the switching happens very quickly (using a 2MHz clock in this case).
// Set red, green and blue intensity levels function setLevels(r, g, b) { if(r != null) write(pinR<4?0x2A+pinR*3:0x36+(pinR-4)*5, r); if(g != null) write(pinG<4?0x2A+pinG*3:0x36+(pinG-4)*5, g); if(b != null) write(pinB<4?0x2A+pinB*3:0x36+(pinB-4)*5, b); } }
Finally we can implement our example functionality.
We'll construct an RGB LED using the correct parameters for the Hannah board.
// Construct an LED led <- RgbLed(I2C_89, 0x3E, 7, 5, 6);
We'll change color every 500ms, using a wakeup timer to call change.
// Change to a random color every 500ms
function change()
{
// Schedule the next change
imp.wakeup(0.5, change);
// Select a color at random
local r = math.rand()%256;
local g = math.rand()%256;
local b = math.rand()%256;
// Set the LED color
led.setLevels(r, g, b);
}
We register with the server - no inputs or outputs required this time.
// Register with the server imp.configure("Color Blink", [], []);
Finally we can set the color changer running.
// Enable the LED outputs and start color changing led.setLed(1, 1, 1); change(); // End of code.
For the full code listing click here.