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.
1 : // Please see credits and usage for usiTwiSlave and TinyWireS in the .h files of
2 : // those libraries.
3 :
4 : #include <avr/sleep.h>
5 : #include <avr/wdt.h>
6 : #include <EEPROM.h>
7 : #include "TinyWireS.h"
8 :
9 : #define I2C_DEFAULT_ADDR 0x04 // i2c default address (4, 0x04)
10 :
11 : #define DISPLAY_NUMBER 0x00
12 : #define SET_I2C_ADDR 0x50
13 :
14 :
15 : uint8_t i2c_address = I2C_DEFAULT_ADDR;
16 :
17 : // Pin 1 - Reserved for Reset
18 : uint8_t SER = 3; // Pin 2 - SER to 74HC595
19 : uint8_t SRCLK = 4; // Pin 3 - SRCLK to 74HC595
20 : // Pin 4 - Gnd
21 : // Pin 5 - SDA
22 : uint8_t RCLK = 1; // Pin 6 - RCLK to 74HC595
23 : // Pin 7 - SCL
24 : // Pin 8 - Vcc
25 :
26 :
27 : void setup()
28 : {
29 : pinMode(SER,OUTPUT); //Configure PB3 as output
30 : pinMode(RCLK,OUTPUT); //Configure PB1 as output
31 : pinMode(SRCLK,OUTPUT); //Configure PB4 as output
32 :
33 : i2c_address = EEPROM.read(0);
34 : if ((i2c_address== 0x00)||(i2c_address== 0xff)){
35 : i2c_address = I2C_DEFAULT_ADDR;
36 : }
37 :
38 : clear_display();
39 :
40 : TinyWireS.begin(i2c_address); // Initiazlize the I2C Slave mode
41 : TinyWireS.onReceive(receiveEvent); // Register the onReceive() callback function
42 :
43 : // disable the watchdog timer so that it doesn't cause power-up, code is from datasheet
44 : // Clear WDRF in MCUSR – MCU Status Register
45 : // MCUSR provides information on which reset source caused an MCU Reset.
46 : MCUSR = 0x00;
47 : // WDTCR - Watchdog Timer Control Register
48 : WDTCR |= ( _BV(WDCE) | _BV(WDE) ); // Write logical one to WDCE and WDE (must be done before disabling)
49 : WDTCR = 0x00; // Turn off WDT
50 :
51 :
52 : set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Enable power down sleep mode
53 : sleep_enable();
54 : sei(); // Enable interrupts
55 : }
56 :
57 : void loop()
58 : {
59 : }
60 :
61 :
62 : void clear_display(){
63 : int numberOfShiftRegisters = 6;
64 :
65 : digitalWrite(SER, 0); //Set SER line LOW
66 : digitalWrite(RCLK, 0); //Set RCLK line LOW
67 : digitalWrite(SRCLK, 0); //Set SRCLK line LOW
68 :
69 : // Since we don't have a free pin to use to clear the data from the shift-registers, simply
70 : // clock a '0' through all registers
71 : for(int x = 0; x<(8*numberOfShiftRegisters);x++){ // For each settable bit in the Shift-registers
72 : digitalWrite(SRCLK, 1); // Clock the Serial Clock line High
73 : digitalWrite(SRCLK, 0); // Clock the Serial Clock line Low
74 : }
75 : digitalWrite(RCLK, 1); // Pulse the RLCK to clear any junk data on the output pins.
76 : digitalWrite(RCLK, 0);
77 :
78 : //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
79 : //visible on the 7-segment displays. Even with OE and SCLR lines not being used.
80 : }
81 :
82 :
83 : // Gets called when the ATtiny receives an i2c write slave request
84 : // This routine runs from the usiTwiSlave interrupt service routine (ISR)
85 : // so interrupts are disabled while it runs.
86 : void receiveEvent(uint8_t num_bytes)
87 : {
88 : uint8_t display_byte[16];
89 : uint8_t master_bytes = num_bytes - 2;
90 :
91 : uint8_t command = TinyWireS.receive();
92 : uint8_t value = TinyWireS.receive();
93 :
94 : switch (command){
95 : case DISPLAY_NUMBER:
96 : for (uint8_t i = 0; i < master_bytes; i++){ // Process each byte of data from the master
97 : display_byte[i] = TinyWireS.receive();
98 : for (uint8_t x = 0; x < 8; x++){ //Loop for each bit within data byte
99 : digitalWrite(SER, bitRead(display_byte[i], x)); //Set the SER pin based on the bit of data
100 : digitalWrite(SRCLK, 1); //Set SRCLK High
101 : digitalWrite(SRCLK, 0); //Set SRCLK Low
102 : }
103 : }
104 : digitalWrite(RCLK, 1); // Pulse the RLCK to clear any junk data on the output pins.
105 : digitalWrite(RCLK, 0);
106 : break
107 :
108 : case SET_I2C_ADDR:
109 : if ((value > 0) && (value < 128)){
110 : EEPROM.write(0,value);
111 : }
112 : break
113 : }
114 :
115 : }
116 :
117 :
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.
1 : #include <Wire.h>
2 : #include <Arduino.h>
3 :
4 : uint8_t i2c_address = 0x04;
5 :
6 : void setup() {
7 : Wire.begin();
8 : display("654321");
9 : }
10 :
11 : void loop() {
12 : }
13 :
14 : void display (String data){
15 :
16 : Wire.beginTransmission(i2c_address); //Start the I2C Session
17 :
18 : Wire.write(0x00); //Command Display Number
19 : Wire.write(0x00); //Value does not matter...
20 :
21 : for (int x=data.length() ; x>=0 ; x--){ //Process the char array one byte at a time
22 : char aByte=data.charAt(x);
23 : char value = 0;
24 : char dp = 0;
25 :
26 : if ((x+1)//Check if we are setting the decimal place
27 : if (data.charAt(x+1) == '.'){
28 : dp = 0x01;
29 : }
30 : }
31 :
32 : switch (aByte){ //Send the correct bit sequence for each char
33 : case '-': // '-' sign
34 : value = 0x04|dp; // Add the deciamal place if set
35 : Wire.write(value); //Send the encoded value to the display
36 : break
37 :
38 : case '0': // '0'
39 : value = 0xfa|dp; // Add the deciamal place if set
40 : Wire.write(value); //Send the encoded value to the display
41 : break
42 :
43 : case '1': // '1'
44 : value = 0x60|dp; // Add the deciamal place if set
45 : Wire.write(value); //Send the encoded value to the display
46 : break
47 :
48 : case '2': // '2'
49 : value = 0xdc|dp; // Add the deciamal place if set
50 : Wire.write(value); //Send the encoded value to the display
51 : break
52 :
53 : case '3': // '3'
54 : value = 0xf4|dp; // Add the deciamal place if set
55 : Wire.write(value); //Send the encoded value to the display
56 : break
57 :
58 : case '4': // '4'
59 : value = 0x66|dp; // Add the deciamal place if set
60 : Wire.write(value); //Send the encoded value to the display
61 : break
62 :
63 : case '5': // '5'
64 : value = 0xb6|dp; // Add the deciamal place if set
65 : Wire.write(value); //Send the encoded value to the display
66 : break
67 :
68 : case '6': // '6'
69 : value = 0xbe|dp; // Add the deciamal place if set
70 : Wire.write(value); //Send the encoded value to the display
71 : break
72 :
73 : case '7': // '7'
74 : value = 0xe0|dp; // Add the deciamal place if set
75 : Wire.write(value); //Send the encoded value to the display
76 : break
77 :
78 : case '8': // '8'
79 : value = 0xff|dp; // Add the deciamal place if set
80 : Wire.write(value); //Send the encoded value to the display
81 : break
82 :
83 : case '9': // '9'
84 : value = 0xe6|dp; // Add the deciamal place if set
85 : Wire.write(value); //Send the encoded value to the display
86 : break
87 :
88 : case 'C': // 'C'
89 : value = 0x9a|dp; // Add the deciamal place if set
90 : Wire.write(value); //Send the encoded value to the display
91 : break
92 :
93 : case 'F': // 'F'
94 : value = 0x8e|dp; // Add the deciamal place if set
95 : Wire.write(value); //Send the encoded value to the display
96 : break
97 :
98 : case 'o': // 'o'
99 : value = 0x3c|dp; // Add the deciamal place if set
100 : Wire.write(value); //Send the encoded value to the display
101 : break
102 : }
103 : }
104 : Wire.endTransmission(); //End the transmission
105 : }
106 :