ADVERTISEMENT
In this project we are going to dig the
details of a floor heating controller with wireless thermostats from LK
systems. The idea is to learn more about
basics of AVR microcontrollers
WARNING: In this case, the original device is decommissioned and using a modified firmware in a device which is in use is not recommended. The author is not liable for the potential harm from such an act.
The device uses an atmega649 as a controller
in the receive unit which controls the actuators for hot water to the floor
heating system. There are several
channels which controls the flow of hot water based up on the temperature
measurements and user set values from sensors/thermostats placed in different
rooms.
The sensors are fitted with a thermistor
for measuring the temperature and a potentiometer for user inputs (heat level
needed). Sensors use an atmega48 and the
communication between the sensors and the control unit is via 868MHZ ism band
using an FSK module.
The idea of this project is to understand
the device and write smaller modules to test and control different elements of
the system (basic level) and the possibility for writing custom firmware and
repurpose the thermostat for fun.
Some of the possibilities are:
- Wireless thermometer/weather station
- Wireless sprinkler controller & soil moisture measurement
- Wireless water pump controller based on overhead tank levels
- Wireless post box monitor & many more wireless sensor applications
- Adding Internet connectivity to semi smart old thermostats –adding ESP8266
The learning points and different steps in the project :
- Write a simple avr-gcc project to control the custom LCD on the device (using built in LCD controller on avr)
- Learn more about the interrupts and timers in an AVR
- Wireless communications, Manchester coding and Frequency Shift Keying modules
- Using the real time clock (RTC) system in an AVR with 32khz crystals & Asynchronus timer
- Learn more on power saving and sleep modes in AVR
- Controlling the Thermo electric Actuators, Battery Backups (RTC) and PWM
- Controlling a beeper on an AVR (simultaneous use of all the above functions + beep without delay/PWM)
- State machines & Button debouncing
- Adding a bootloader (TinySafeBoot) in AVR projects
Hardware Internals
In the figure below the internals of controller and the sensor (thermostat) are shown. Good quality double sided printed circuit board with perfections in every aspects. The clever choice and use of micro controller pins and quality assurance in the components are noticeable. The device did a good job in the past 9 -10 years and is currently replaced with a smarter/connected systems emerged in the recent years. The battery backups, display controller and on-board converters are excellent and i included only the controller board. There is one more excellent board with control relays are used to
manage the thermo electric actuators and pumps (and extra opto isolated ports)
Inside view of the device and both sides are shown in the same figure & flipped to make the opposite sides to coiside with each other |
The internals of the sensor/thermostat board with thermistor/user knob is shown below
Thermostat remote/sensor with atmega48, the rf module is removed |
Tools needed for the project
- A serial programmer for the avr ( you can use an arduino as isp)
- Compiler and tool chain (avr-gcc, avrdude, text editor)
- A usb to serial adapter (for debug, adding serial bootloader & get rid of isp)
Taming the LCD controller in an AVR
In this project we start with writing a simple LCD driver. Atmega649 has a built in LCD controller.
The glass type lcd (as seen in figure) needs an AC voltage to make its segment darker (active). To minimize the use of micro-controller pins the lcd uses multiplexing and around 50+ segments are present on the lcd (for digits and symbols)
To start with the project we need to get hold of the datasheet for atmega649 & atmega48. Look at the pin descriptions and make a small printout to keep aside.
The first thing to do is to identify the ICSP & Serial pins (RX, TX) and see if they are easily exposed.
Luckily in this case the manufacturer is very open and have kept all neat and clean.
In the figure below you can see the LCD pins used (22 pins) where 19 segments and 3 channels for multiplexing (19 X 3= 57 possibilities)
In the figure below you can see the LCD pins used (22 pins) where 19 segments and 3 channels for multiplexing (19 X 3= 57 possibilities)
Important pins and their usage in the controller unit |
If you are familiar with other avr projects , it uses some registers ( basically memory addresses ) to indicate different control routines. For LCD segments, there are certain memory locations in the avr where if we write 1, the corresponding lcd sement turns on and vice versa
To initialize and use the LCD controller we need to set up some control registers with proper values to indicate the (frame rate of lcd, the duty cycles, the number of segment pins used (pin mask) and common pins used and to indicate if lcd need to be enabled or not and there is an interrupt which gets fired when a new frame is drawn on the lcd)
After going through the data sheet, i have arrived at the following results (read page 228 of the manual for finer details )
After a bit of experimenting ( dont have a jtag lying around to debug and flip the registers) to identify the relation between segments and memory locations, an algorithm was developed to turn on and off the segments in response to the numbers and text ( limited to what is possible with seven segment)
lcd.c
Makefile
To initialize and use the LCD controller we need to set up some control registers with proper values to indicate the (frame rate of lcd, the duty cycles, the number of segment pins used (pin mask) and common pins used and to indicate if lcd need to be enabled or not and there is an interrupt which gets fired when a new frame is drawn on the lcd)
After going through the data sheet, i have arrived at the following results (read page 228 of the manual for finer details )
Setting up the LCD
LCDCRA – LCD Control and Status Register A
LCDEN: LCD Enable ( Writing this bit to one enables the LCD Controller)
LCDIE: LCD Interrupt Enable ( if set as 1, LCD Interrupt at the beginning of a new frame)
LCD Memory map
LCD memory map where flipping the bit turns a segment on or off. The challenge is to identify which bit and which segment |
After a bit of experimenting ( dont have a jtag lying around to debug and flip the registers) to identify the relation between segments and memory locations, an algorithm was developed to turn on and off the segments in response to the numbers and text ( limited to what is possible with seven segment)
Preceding number indicates the digit (1-4) location and letter indicate the segment as shown in bottom. Other entities are special characters and symbols built in to indicate days , channel etc |
Source code for LCD Driver
Use all four files (main.c, lcd.h, lcd.c, Makefile) in a folder and run make all && make flash
(you may have to edit Makefile to set your programmer)
main.c (test program)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | /************************************************************//** * ...written by RV * .. Free to use and leave credits * .. credits to avr rf_lib/appnotes/datasheets/avrfreaks website * .. adding new functions to be integrated back ****************************************************************/ #ifndef F_CPU #define F_CPU 1000000 #endif #include <avr/io.h> #include <util/delay.h> #include "lcd.h" void main(void) { _delay_ms(2000); LCD_Init(); // initialize the LCD LCD_WriteNum(1234); _delay_ms(1000); LCD_clear(); for(;;) //infinite loop { // TO DO MOVE TO STATE MACHINE LCD_puts("Welcome to Thermos123"); _delay_ms(1000); } } |
lcd.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | /************************************************************//** * ... written by RV * .. Free to use and leave credits * .. credits to avr appnotes/datasheets/avrfreaks website * .. adding new functions to be integrated back ****************************************************************/ #ifndef LCD_H #define LCD_H #include <avr/io.h> #include <avr/pgmspace.h> #include <util/delay.h> #include <stdint.h> #include <avr/interrupt.h> #define SetBit(number,bit) (number|=(1<<bit)) #define CLR_BIT(p,n) ((p) &= ~((1) << (n))) #define LCD_LCDREGS_START ((unsigned char*)&LCDDR00) #define FALSE 0 #define TRUE 1 #define LCDCOLON 7 //R5 #define LCDDOT 7 //R0 //SYMBOLS IN THE LCD #define LCDR3 0 //R0 #define LCDR7 6 //R1 #define LCDD6 7 //R1 #define LCDD1 2 //R2 #define LCDD2 1 //R2 #define LCDD5 0 //R2 #define LCDR2 0 //R5 #define LCDD7 7 //R6 #define LCDR6 6 //R6 #define LCDR4 2 //R7 #define LCDD3 1 //R7 #define LCDD4 0 //R7 #define LCDR1 0 //R10 #define LCDU1 1 //R10 #define LCDU2 3 //R10 #define LCDU3 6 //R10 #define LCDU4 7 //R10 #define LCDU5 2 //R11 #define LCDU6 3 //R11 #define LCDU7 5 //R11 #define LCDR5 6 //R11 #define LCDR8 7 //R11 #define LCD_SCROLLCOUNT_DEFAULT 32 #define LCD_DELAYCOUNT_DEFAULT 20 #define LCD_TEXTBUFFER_SIZE 40 #define LCD_SEGBUFFER_SIZE 19 #define LCD_DISPLAY_SIZE 4 #define LCD_FLAG_SCROLL (1 << 0) #define LCD_FLAG_SCROLL_DONE (1 << 1) #define FALSE 0 #define TRUE 1 // LCD Text + Nulls for scrolling + Null Termination static volatile char TextBuffer[LCD_TEXTBUFFER_SIZE + LCD_DISPLAY_SIZE + 1] = {}; static volatile uint8_t StrStart = 0; static volatile uint8_t StrEnd = 0; static volatile uint8_t ScrollCount = 0; static volatile uint8_t UpdateDisplay = FALSE; static volatile uint8_t ScrollFlags = 0; #define LCD_SPACE_OR_INVALID_CHAR 38 #define LCD_CONTRAST_LEVEL(level) do{ LCDCCR = (0x0F & level); }while(0) #define LCD_WAIT_FOR_SCROLL_DONE() do{ while (!(ScrollFlags & LCD_FLAG_SCROLL_DONE)) {} }while(0) static unsigned const char digitmap[] PROGMEM={ 0b01101111, // 0 "0" AAA 0b00100100, // 1 "1" F B 0b01110011, // 2 "2" F B 0b01110110, // 3 "3" GGG 0b00111100, // 4 "4" E C 0b01011110, // 5 "5" E C 0b01011111, // 6 "6" DDD 0b01100100, // 7 "7" 0b01111111, // 8 "8" 0b01111110, // 9 "9" 0b01111101, // 65 'A' 0b00011111, // 66 'b' 0b01001011, // 67 'C' 0b00110111, // 68 'd' 0b01011011, // 69 'E' 0b01011001, // 70 'F' 0b01001111, // 71 'G' 0b00111101, // 72 'H' 0b00100100, // 73 'I' 0b00100110, // 74 'J' 0b00111101, // 75 'K' Same as 'H' 0b00001011, // 76 'L' 0b00000000, // 77 'M' NO DISPLAY 0b00010101, // 78 'n' 0b01101111, // 79 'O' 0b01111001, // 80 'P' 0b01111100, // 81 'q' 0b00010001, // 82 'r' 0b01011110, // 83 'S' 0b00011011, // 84 't' 0b00101111, // 85 'U' 0b00101111, // 86 'V' Same as 'U' 0b00000000, // 87 'W' NO DISPLAY 0b00111101, // 88 'X' Same as 'H' 0b00111110, // 89 'y' 0b01110011, // 90 'Z' Same as '2' 0b00000000, // 32 ' ' BLANK 0b00010000, // 45 '-' DASH 0b00000000, // 46 '.' PERIOD }; // PROTOTYPES: void LCD_Init(void); void LCD_WriteChar(const char Byte, const char Digit); void LCD_clear(void); void LCD_test(void); void LCD_WriteNum(int number); void LCD_showcolon(unsigned char show); void LCD_showsymbol( int BYTESYMBOL,unsigned char show,int register_row); void LCD_puts(const char *Data); #endif |
lcd.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | /************************************************************//** * ... written by RV * .. Free to use and leave credits * .. credits to avr appnotes/datasheets/avrfreaks website * .. adding new functions to be integrated back ****************************************************************/ #include "lcd.h" void LCD_disable(void) { /* Wait until a new frame is started. */ while ( !(LCDCRA & (1<<LCDIF)) ) ; /* Set LCD Blanking and clear interrupt flag */ /* by writing a logical one to the flag. */ LCDCRA = (1<<LCDEN)|(1<<LCDIF)|(1<<LCDBL); /* Wait until LCD Blanking is effective. */ while ( !(LCDCRA & (1<<LCDIF)) ) ; /* Disable LCD */ LCDCRA = (0<<LCDEN); } void LCD_Init(void) { /* Use 32 kHz crystal oscillator */ /* 1/3 Bias and 1/3 duty, SEG21:SEG24 is used as port pins */ LCDCRB = (1<<LCDCS) | (1<<LCDMUX1)| (1<<LCDPM2); /* Using 16 as prescaler selection and 7 as LCD Clock Divide */ /* gives a frame rate of 49 Hz */ LCDFRR = (1<<LCDCD2) | (1<<LCDCD1); /* Set segment drive time to 125 µs and output voltage to 3.3 V*/ LCDCCR = (1<<LCDDC1) | (1<<LCDCC3) | (1<<LCDCC2) | (1<<LCDCC1); /* Enable LCD, default waveform and no interrupt enabled */ //LCD Control and Status Register A LCDCRA = (1<<LCDEN) | (1<<LCDIE); sei(); //LOW POWE Wave form & LCDIF is interrupt flag & LCDIE is enable interrupt //LCDCRA = (1<<LCDAB); } void LCD_puts(const char *Data) { uint8_t LoadB = 0; uint8_t CurrByte; do { CurrByte = *(Data++); switch (CurrByte) { case '0'...'9': TextBuffer[LoadB++] = (CurrByte-48); break; case 'A'...'Z': TextBuffer[LoadB++] = (CurrByte-55); break; case 'a'...'z': TextBuffer[LoadB++] = (CurrByte-87); break; case 0x00: break; default: TextBuffer[LoadB++] = LCD_SPACE_OR_INVALID_CHAR; } } while (CurrByte && (LoadB < LCD_TEXTBUFFER_SIZE)); ScrollFlags = ((LoadB > LCD_DISPLAY_SIZE)? LCD_FLAG_SCROLL : 0x00); for (uint8_t Nulls = 0; Nulls < 7; Nulls++) TextBuffer[LoadB++] = LCD_SPACE_OR_INVALID_CHAR; // Load in nulls to ensure that when scrolling, the display clears before wrapping TextBuffer[LoadB] = 0x00; // Null-terminate string StrStart = 0; StrEnd = LoadB; ScrollCount = LCD_SCROLLCOUNT_DEFAULT + LCD_DELAYCOUNT_DEFAULT; UpdateDisplay = TRUE; } /* ISR for lcd */ ISR(LCD_vect, ISR_NOBLOCK) { unsigned char cbyte; if (ScrollFlags & LCD_FLAG_SCROLL) { if (!(ScrollCount--)) { UpdateDisplay = TRUE; ScrollCount = LCD_SCROLLCOUNT_DEFAULT; } } if (UpdateDisplay) { for (uint8_t Character = 0; Character < LCD_DISPLAY_SIZE; Character++) { uint8_t Byte = (StrStart + Character); if (Byte >= StrEnd) Byte -= StrEnd; cbyte = pgm_read_byte(&(digitmap[(int)TextBuffer[Byte]])); LCD_WriteChar(cbyte, Character); //LCD_WriteChar(TextBuffer[Byte], Character); } if ((StrStart + LCD_DISPLAY_SIZE) == StrEnd) // Done scrolling message on LCD once ScrollFlags |= LCD_FLAG_SCROLL_DONE; if (StrStart++ == StrEnd) StrStart = 1; UpdateDisplay = FALSE; // Clear LCD management flags, LCD update is complete } } void LCD_WriteChar(const char Byte, const char Digit) { //The function takes in segment map (0bxxxxxxxx) and digit position (0-3) //Simple and direct update alternateively use the LCD interrupt and a buffer to store the changes //and write it to the register during the refresh interrupt //function takes the segmnt map and update the lcd register appropriately //nothing to do with more digits than available in the current lcd //odd digit location in register map are adhjascent, refer datasheet if (Digit>>2) //number 4 and above will b etrue after 2 shift to right return; unsigned char* BuffPtr = (unsigned char*)(LCD_LCDREGS_START + (Digit >> 1)); char MaskedSegData=0; //loop for the three groups of lcd register rows for a difgit ie 3 common planes/multiplex for (char BNib = 0; BNib < 3; BNib++) { if(BNib == 0) //pick first 3 segments MaskedSegData = (Byte & 0b00000111); if(BNib == 1) { MaskedSegData = (Byte & 0b00111000); MaskedSegData >>=3; } if(BNib == 2) { //pick last 1 segments MaskedSegData = (Byte & 0b01000000); MaskedSegData >>=5; if (Digit ==1) *BuffPtr = ((*BuffPtr & 0b11011111) | (MaskedSegData<<4)); if (Digit ==3) *BuffPtr = ((*BuffPtr & 0b11101111) | (MaskedSegData<<3)); if (Digit ==0) *BuffPtr = ((*BuffPtr & 0b11111011) | MaskedSegData<<1); if (Digit ==2) *BuffPtr = ((*BuffPtr & 0b11111101) | MaskedSegData); } //to avoid messing adjascent non numeric segments else//quick n dirty update of Bnib 3 will erase adjascent { if (Digit ==1) *BuffPtr = ((*BuffPtr & 0b10001111) | (MaskedSegData<<4)); if (Digit ==3) *BuffPtr = ((*BuffPtr & 0b11000111) | (MaskedSegData<<3)); if (Digit ==0) *BuffPtr = ((*BuffPtr & 0b11110001) | MaskedSegData<<1); if (Digit ==2) *BuffPtr = ((*BuffPtr & 0b11111000) | MaskedSegData); } BuffPtr += 5; } } void LCD_WriteNum(int number) //show a number from 0000-9999 on the lcd //add negative handling later { if ((number>9999)||(number<0)) //number 4 and above will b etrue after 2 shift to right return; //clear lcd LCD_clear(); //take the 1s place or digit0 int digit; unsigned char byte; digit=number%10; byte = pgm_read_byte(&(digitmap[digit])); LCD_WriteChar(byte, 3); //pick next digit=(number/10)%10; byte = pgm_read_byte(&(digitmap[digit])); LCD_WriteChar(byte, 2); digit=(number/100)%10; byte = pgm_read_byte(&(digitmap[digit])); LCD_WriteChar(byte, 1); digit=(number/1000)%10; byte = pgm_read_byte(&(digitmap[digit])); LCD_WriteChar(byte, 0); } void LCD_showcolon(unsigned char show) { unsigned char* BuffPtr = (unsigned char*)(LCD_LCDREGS_START); BuffPtr+=5; //location for colon (show==FALSE) ? CLR_BIT(*BuffPtr,LCDCOLON) : SetBit(*BuffPtr ,LCDCOLON); } //void rf_rx_set_io(volatile uint8_t* reg, uint8_t pin) void LCD_showsymbol( int BYTESYMBOL,unsigned char show,int register_row) { unsigned char* BuffPtr = (unsigned char*)(LCD_LCDREGS_START); BuffPtr+=register_row; //location for colon (show==FALSE) ? CLR_BIT(*BuffPtr,BYTESYMBOL) : SetBit(*BuffPtr,BYTESYMBOL); } void LCD_clear(void) { LCD_WriteChar(0b00000000, 0); LCD_WriteChar(0b00000000, 1); LCD_WriteChar(0b00000000, 2); LCD_WriteChar(0b00000000, 3); } void LCD_test(void) { //jump across the digit for (int j=0; j<4;j++) { //numbers only first 10 in the map for (int i=0; i<9;i++) { unsigned char byte = pgm_read_byte(&(digitmap[i])); LCD_WriteChar(byte, j); _delay_ms(500); LCD_showcolon(TRUE); LCD_showsymbol( LCDR3,TRUE,0); _delay_ms(500); LCD_showcolon(FALSE); } } LCD_clear(); } |
Makefile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | # Name: Makefile # Author: <insert your name here> # Copyright: <insert your copyright message here> # License: <insert your license reference here> # DEVICE ....... The AVR device you compile for # CLOCK ........ Target AVR clock rate in Hertz # OBJECTS ...... The object files created from your source files. This list is # usually the same as the list of source files with suffix ".o". # PROGRAMMER ... Options to avrdude which define the hardware you use for # uploading to the AVR and the interface where this hardware # is connected. # FUSES ........ Parameters for avrdude to flash the fuses appropriately. #DEVICE = atmega169 DEVICE = atmega649 CLOCK = 1000000 PROGRAMMER = -c avrisp -P /dev/ttyACM0 -b 19200 OBJECTS = main.o\ lcd.o TSB = tsb /dev/ttyUSB0 fw ###################################################################### ###################################################################### # Tune the lines below only if you know what you are doing: AVRDUDE = avrdude $(PROGRAMMER) -p $(DEVICE) COMPILE = avr-gcc -std=gnu99 -Wall -Os -DF_CPU=$(CLOCK) -mmcu=$(DEVICE) $(INC) # symbolic targets: all: main.hex .c.o: $(COMPILE) -c $< -o $@ .S.o: $(COMPILE) -x assembler-with-cpp -c $< -o $@ # "-x assembler-with-cpp" should not be necessary since this is the default # file type for the .S (with capital S) extension. However, upper case # characters are not always preserved on Windows. To ensure WinAVR # compatibility define the file type manually. .c.s: $(COMPILE) -S $< -o $@ flash: all $(AVRDUDE) -U flash:w:main.hex:i # if you use a bootloader, change the command below appropriately: load: all $(TSB) main.hex clean: rm -f main.hex main.elf $(OBJECTS) *~ # file targets: main.elf: $(OBJECTS) $(COMPILE) -o main.elf $(OBJECTS) main.hex: main.elf rm -f main.hex avr-objcopy -j .text -j .data -O ihex main.elf main.hex # If you have an EEPROM section, you must also create a hex file for the # EEPROM and add it to the "flash" target. # Targets for code debugging and analysis: disasm: main.elf avr-objdump -d main.elf cpp: $(COMPILE) -E main.c |
END
The more details will be updated in the subsequent posts
The more details will be updated in the subsequent posts
No comments:
Post a Comment