This is the third part of this project. The aim is to make a custom 7-segment display controlled through the I2C interface. The development will be broken down into several parts to show how I evolved the design. The display module will be controlled by an ATTiny85 and this will then drive the 7-segment displays using the 74HC595 8-bit shift register.
In this part I am going to add a simple command interface which will allow me to change the modules I2C address (via EEPROM).
If you would like to see the video, please feel free to click the link below, otherwise just read on.
Before getting started, If you want to see the how to program the ATTiny (Part 1) or how I started the writing the code for the ATTiny (Part 2) or more information on the 74HC595 then click on the links below:
In the last part, we got the basic display module working. Now I want to expand the capabilites of the display. This requires adding a simple command interface.
Most of the work is done in the recieve section. I've added two extra bytes that must be sent to the module. These extra bytes must always be sent. The first is going to be used as a 'Command', the second byte will be used as a 'Value'. With these two extra bytes we can have over 65,000 features, I think this should be enough....
The first step was to keep the existing functionality working, to do this we can define a command byte for 'normal' operation. The second byte for 'value' can just be ignored as we don't have a use for it yet.
When the data is recieved from the I2C master, the first byte is processed by a switch statment. This gives us 255 possible commands. Just allocate one for the 'Normal' operation as we currently have it working. The remaining 6-bytes of data can then be processed exactly the same as we did in the last section.
We now can define a 'Command' for setting the I2C address (See line number 108). The second byte can now be used as the 'Value' for the I2C address. We also need to add some sanity checks to make sure that the value is a valid I2C address.
The only problem is where to store it. We can not assign it to a variable since this will be lost when power is removed from the module. The solution is to write it to EEPROM. Data stored in the EEPROM does not get lost when the power is removed. The only limitation is the number of write cycles to EEPROM. Most EEPROMs have a limited number of write cycles typically about ~10,000. This will not be a problem for this application, but you would not want to store data that changes frequently in an EEPROM. Since the EEPROM we are using is in the microcontroller, once this fails you would have to replace the microcontroller.
The final task is how to read this from the EEPROM. If we go to line number 33, you can see that the first byte of the EEPROM is read on start up and assigned to 'i2c_address'. The only problem we have is what happens the first time the display module is powered if the EEPROM has not been programmed? The good news is that EEPROMS when blank have '0x00' or '0xff' in all of the memory locations. A simple sanity check allows us to use a default value if the data in the EEPROM is blank.
Note: Reading from the EEPROM does not decrease its life expectancy. The EEPROM is only limited in the number of WRITE cycles.
// Please see credits and usage for usiTwiSlave and TinyWireS in the .h files of
// those libraries.
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <EEPROM.h>
#include "TinyWireS.h"
#define I2C_DEFAULT_ADDR 0x04 // i2c default address (4, 0x04)
#define DISPLAY_NUMBER 0x00
#define SET_I2C_ADDR 0x50
uint8_t i2c_address = I2C_DEFAULT_ADDR;
// Pin 1 - Reserved for Reset
uint8_t SER = 3; // Pin 2 - SER to 74HC595
uint8_t SRCLK = 4; // Pin 3 - SRCLK to 74HC595
// Pin 4 - Gnd
// Pin 5 - SDA
uint8_t RCLK = 1; // Pin 6 - RCLK to 74HC595
// Pin 7 - SCL
// Pin 8 - Vcc
void setup()
{
pinMode(SER,OUTPUT); //Configure PB3 as output
pinMode(RCLK,OUTPUT); //Configure PB1 as output
pinMode(SRCLK,OUTPUT); //Configure PB4 as output
i2c_address = EEPROM.read(0);
if ((i2c_address== 0x00)||(i2c_address== 0xff)){
i2c_address = I2C_DEFAULT_ADDR;
}
clear_display();
TinyWireS.begin(i2c_address); // Initiazlize the I2C Slave mode
TinyWireS.onReceive(receiveEvent); // Register the onReceive() callback function
// disable the watchdog timer so that it doesn't cause power-up, code is from datasheet
// Clear WDRF in MCUSR – MCU Status Register
// MCUSR provides information on which reset source caused an MCU Reset.
MCUSR = 0x00;
// WDTCR - Watchdog Timer Control Register
WDTCR |= ( _BV(WDCE) | _BV(WDE) ); // Write logical one to WDCE and WDE (must be done before disabling)
WDTCR = 0x00; // Turn off WDT
set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Enable power down sleep mode
sleep_enable();
sei(); // Enable interrupts
}
void loop()
{
}
void clear_display(){
int numberOfShiftRegisters = 6;
digitalWrite(SER, 0); //Set SER line LOW
digitalWrite(RCLK, 0); //Set RCLK line LOW
digitalWrite(SRCLK, 0); //Set SRCLK line LOW
// Since we don't have a free pin to use to clear the data from the shift-registers, simply
// clock a '0' through all registers
for(int x = 0; x<(8*numberOfShiftRegisters);x++){ // For each settable bit in the Shift-registers
digitalWrite(SRCLK, 1); // Clock the Serial Clock line High
digitalWrite(SRCLK, 0); // Clock the Serial Clock line Low
}
digitalWrite(RCLK, 1); // Pulse the RLCK to clear any junk data on the output pins.
digitalWrite(RCLK, 0);
//Note: If we add a small delay (1ms) to enabling the OE pin, then 74HC595 will be initialized with all '0' preventing any unwanted data from being
//visible on the 7-segment displays. Even with OE and SCLR lines not being used.
}
// Gets called when the ATtiny receives an i2c write slave request
// This routine runs from the usiTwiSlave interrupt service routine (ISR)
// so interrupts are disabled while it runs.
void receiveEvent(uint8_t num_bytes)
{
uint8_t display_byte[16];
uint8_t master_bytes = num_bytes - 2;
uint8_t command = TinyWireS.receive();
uint8_t value = TinyWireS.receive();
switch (command){
case DISPLAY_NUMBER:
for (uint8_t i = 0; i < master_bytes; i++){ // Process each byte of data from the master
display_byte[i] = TinyWireS.receive();
for (uint8_t x = 0; x < 8; x++){ //Loop for each bit within data byte
digitalWrite(SER, bitRead(display_byte[i], x)); //Set the SER pin based on the bit of data
digitalWrite(SRCLK, 1); //Set SRCLK High
digitalWrite(SRCLK, 0); //Set SRCLK Low
}
}
digitalWrite(RCLK, 1); // Pulse the RLCK to clear any junk data on the output pins.
digitalWrite(RCLK, 0);
break
case SET_I2C_ADDR:
if ((value > 0) && (value < 128)){
EEPROM.write(0,value);
}
break
}
}
Upload this sketch using the Arduino ISP programmer and then place the ATTiny85 back into the development board. Try changing the I2C address. Note: After chaning the I2C address, you will need to power cycle the display module before the change takes effect.
Below is the simple sketch that I used to test the display module. To keep this working all I needed to add was the extra two bytes (line number 18 & 19) to be sent, when sending the display command.
#include <Wire.h>
#include <Arduino.h>
uint8_t i2c_address = 0x04;
void setup() {
Wire.begin();
display("654321");
}
void loop() {
}
void display (String data){
Wire.beginTransmission(i2c_address); //Start the I2C Session
Wire.write(0x00); //Command Display Number
Wire.write(0x00); //Value does not matter...
for (int x=data.length() ; x>=0 ; x--){ //Process the char array one byte at a time
char aByte=data.charAt(x);
char value = 0;
char dp = 0;
if ((x+1)//Check if we are setting the decimal place
if (data.charAt(x+1) == '.'){
dp = 0x01;
}
}
switch (aByte){ //Send the correct bit sequence for each char
case '-': // '-' sign
value = 0x04|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
case '0': // '0'
value = 0xfa|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
case '1': // '1'
value = 0x60|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
case '2': // '2'
value = 0xdc|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
case '3': // '3'
value = 0xf4|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
case '4': // '4'
value = 0x66|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
case '5': // '5'
value = 0xb6|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
case '6': // '6'
value = 0xbe|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
case '7': // '7'
value = 0xe0|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
case '8': // '8'
value = 0xff|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
case '9': // '9'
value = 0xe6|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
case 'C': // 'C'
value = 0x9a|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
case 'F': // 'F'
value = 0x8e|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
case 'o': // 'o'
value = 0x3c|dp; // Add the deciamal place if set
Wire.write(value); //Send the encoded value to the display
break
}
}
Wire.endTransmission(); //End the transmission
}