; *************************************************************************** ; File: I2C-LCD-v03 ; Date: May 16, 2011 ; ; Target: ATmega168 ; Assembler: Atmel AvrAssembler2 (AVR Studio 4) ; Author: Donald Weiman ; ; Hardware: 'Boarduino' or equivalent, MCP23017 IO Expander, LCD Module ; ; Summary: 8-bit data interface, busy flag not implemented ; Simple write string routine. ; ; History: v01 - I2C version of LCD-8b-10w-vxx ; v02 - comments cleaned up ; v02a - comments updated ; v02b - change some rcalls/rjmps etc ; v03 - rework SLA_W and SLA_R implementation for multiple I2C devices ; ; ****************************** Program Notes ****************************** ; ; This is an LCD implementation using the Spikenzielabs I2C LCD ; Interface v3 which is basically a Microchip MCP23017 on a PC board. ; ; Since all eight LCD data lines are implemented on the pc board the ; LCD program code uses 8-bit techniques. ; ; The busy flag is not used but since the RW line is implemented on ; the pc board the program will have to deal with it. ; ; This program does not preserve any information concerning the five ; unused bits of Port B (GPB3 - GPB7). ; ; All time delays are longer than those specified in most datasheets in ; order to accomodate slower than normal LCD modules. This requirement ; is well documented but almost always ignored. The information is in ; a note at the bottom of the right hand (Execution Time) column of the ; instruction set. ; ; *************************************************************************** ; ; ----------- ; | ATmega168 | ; | | ---------- ; | | | MCP23017 | ; | | | | ; | PC5|---------------->|SCL | ---------- ; | PC4|<--------------->|SDA | | LCD | ; | | | | | | ; | | | GPA7|-------------->|D7 | ; | | | GPA6|-------------->|D6 | ; | | |_____ GPA5|-------------->|D5 | ; | | 1 -->|RESET GPA4|-------------->|D4 | ; | | | GPA3|-------------->|D3 | ; | | 0 -->|A2 GPA2|-------------->|D2 | ; | | 0 -->|A1 GPA1|-------------->|D1 | ; | | 0 -->|A0 GPA0|-------------->|D0 | ; | | | | | | ; | | | GPB0|-------------->|E | ; | | | GPB1|-------------->|RW | ; | | | GPB2|-------------->|RS | ; | | | | | | ; | | ---------- ---------- ; | | ; | PC3|--> LED (I2C error) ; | | ; ----------- ; *************************************************************************** /* .nolist .include "m168def.inc" .list */ .equ fclk = 16000000 ; system clock frequency .equ fscl = 100000 ; I2C clock frequency (SCL) .equ twi_ps = 1 ; TWI prescaler ; register usage .def temp = R16 ; temporary storage .def counter = R17 ; counter .def status = R18 ; TWI status code ; This is the connection for the LED (not required) .equ R_led_port = PORTC ; 'I2C Error' .equ R_led_num = PORTC3 .equ R_led_ddr = DDRC ; LCD module information .equ lcd_LineOne = 0x00 ; start of line 1 .equ lcd_LineTwo = 0x40 ; start of line 2 ;.equ lcd_LineThree = 0x14 ; start of line 3 ;.equ lcd_lineFour = 0x54 ; start of line 4 ; LCD instructions .equ lcd_FunctionReset = 0b00110000 ; reset the LCD .equ lcd_FunctionSet8bit = 0b00111000 ; 8-bit data, 2-line display, 5 x 7 font .equ lcd_DisplayOff = 0b00001000 ; turn display off .equ lcd_Clear = 0b00000001 ; replace all characters with ASCII 'space' .equ lcd_EntryMode = 0b00000110 ; shift cursor from left to right on read/write .equ lcd_Home = 0b00000010 ; return cursor to first position on first line .equ lcd_DisplayOn = 0b00001100 ; display on, cursor off, don't blink character ; The following TWI Status Codes are found in Table 21-2. "Status codes for Master Transmitter Mode" (Atmel p. 225) .equ I2C_START = 0x08 ; A START condition has been transmitted .equ MT_SLA_ACK = 0x18 ; SLA+W has been transmitted; ACK has been received .equ MT_DATA_ACK = 0x28 ; Data byte has been transmitted; ACK has been received ; The following TWI Status Codes are found in Table 21-3. "Status codes for Master Receiver Mode" (Atmel p. 228) .equ R_START = 0x10 ; A REPEATED START condition has been transmitted .equ MR_SLA_ACK = 0x40 ; SLA+R has been transmitted; ACK has been received .equ MR_DATA_ACK = 0x50 ; Data byte has been received; ACK has been transmitted .equ MR_DATA_NACK = 0x58 ; Data byte has been received; NACK has been transmitted ; MCP23017 information ; The Slave Address information for the MCP23017 is in section 1.4 of the Microchip documentation ; the 7-bit address is 0100xxx and in this application the address pins (xxx) are all tied low .equ IO_I2C_ADDR = 0b0100000 ; MCP23017 7-bit slave address ; Default MCP23017 register addresses (IOCON.BANK = 0) .equ IODIRA = 0x00 ; I/O Direction Registers .equ IODIRB = 0x01 .equ IOCONA = 0x0A ; I/O Expander Configuration Register .equ IOCONB = 0x0B ; either address will work, there is only one register .equ GPIOA = 0x12 ; General Purpose I/O Port Registers .equ GPIOB = 0x13 ; *************************************************************************** .org 0x0000 jmp start ; jump over Program ID etc. ;******************************* Program ID ********************************* .org INT_VECTORS_SIZE program_author: .db "Donald Weiman",0 program_version: .db "I2C-LCD-v03",0 program_date: .db "May 16, 2011",0,0 ; ****************************** Main Program Code ************************** start: ; initialize the stack pointer to the highest RAM address ldi temp,low(RAMEND) out SPL,temp ldi temp,high(RAMEND) out SPH,temp ; set up the data direction for the LED sbi R_led_ddr, R_led_num ; LED - output ; turn the LED off cbi R_led_port, R_led_num ; output low ; flash the error LED once for 1/4 second sbi R_led_port, R_led_num ; turn the led on ldi YL, low(250) ; keep the led on for 1/4 second (250 mS) ldi YH, high(250) call delayYx1mS cbi R_led_port, R_led_num ; turn the led off ; .............................................................................. ; initialize the I2C subsystem to operate at 100 KHz ; set up the TWI clock rate ; TWSR TWI Status Register ; 0 0 0 0 0 0 0 0 ; | | | | | | | | ; | | | | | | | |____ TWPS0 ..... prescaler = 1 ; | | | | | | |______ TWPS1 ; | | | | | |________ ..... not implemented ; | | | | |__________ TWS3 ..... status codes (read only - vary depending on TWI mode) ; | | | |____________ TWS4 ; | | |______________ TWS5 ; | |________________ TWS6 ; |__________________ TWS7 ldi temp, 0b00000000 ; set prescaler to x1 sts TWSR, temp ldi temp, ((fclk/fscl)-16)/(2*twi_ps) ; set TWI baud rate sts TWBR, temp ; .............................................................................. ; initialize the IO expander - this program uses the IO Expander in it's default ; configuration except for the I/O direction ; set I/O direction call TWI_start ; I2C START condition ldi temp, IO_I2C_ADDR ; get I2C Slave Address call TWI_send_SLA_W ; send I2C Slave Address with 'Write' bit appended (Device Opcode) ldi temp, IODIRA ; data direction register for PORTA (Register Address) call TWI_MT_DATA ldi temp, 0b00000000 ; all PORTA bits set for output call TWI_MT_DATA ; Microchip uses '0' for output, the opposite of Atmel ldi temp, 0b11111000 ; PORTB bits 0, 1, 2 set for output call TWI_MT_DATA ; Do not have to set address first due to auto-increment call TWI_send_stop ; I2C STOP condition ; .............................................................................. ; initialize the LCD controller as determined by the equates (LCD instructions) call lcd_init_I2C ; initialize the LCD display for an 8-bit interface ; display the first line of information ldi ZH, high(program_author) ; point to the information that is to be displayed ldi ZL, low(program_author) lsl ZL ; shift the pointer one bit left for the lpm instruction rol ZH ldi temp, lcd_LineOne ; point to where the information should be displayed call lcd_write_string_I2C ; display the second line of information ldi ZH, high(program_version) ; point to the information that is to be displayed ldi ZL, low(program_version) lsl ZL ; shift the pointer one bit left for the lpm instruction rol ZH ldi temp, lcd_LineTwo ; point to where the information should be displayed call lcd_write_string_I2C ; endless loop here: rjmp here ; ****************************** End of Main Program Code ******************* ; ============================== I2C LCD Subroutines ====================== lcd_init_I2C: ; This subroutine initializes the LCD module for an 8-bit data interface. ; Enter with the equates (LCD instructions) set up for the desired operation. ; Power-up delay ldi temp, 80 ; initial 40 mSec delay call delayTx1mS ; Reset the LCD controller. ldi temp, lcd_FunctionReset ; first reset instruction call lcd_write_instruction_I2C ldi temp, 9 ; data sheet calls for a delay of at least 4.1 mS call delayTx1mS ldi temp, lcd_FunctionReset ; second reset instruction call lcd_write_instruction_I2C ldi temp, 200 ; data sheet calls for a delay of at least 100 uS call delayTx1uS ldi temp, lcd_FunctionReset ; third reset instruction call lcd_write_instruction_I2C ldi temp, 80 ; data sheet calls for a delay of at least 40 uS call delayTx1uS ; Function Set instruction ldi temp, lcd_FunctionSet8bit ; set mode, lines, and font call lcd_write_instruction_I2C ldi temp, 80 ; data sheet calls for a delay of at least 40 uS call delayTx1uS ; The next three instructions are specified in the data sheet as part of the initialization routine, so it is ; a good idea (but probably not necessary) to do them just as specified and then redo them later if our ; application requires a different configuration. ; Display On/Off Control instruction ldi temp, lcd_DisplayOff ; turn display OFF call lcd_write_instruction_I2C ldi temp, 80 ; data sheet calls for a delay of at least 40 uS call delayTx1uS ; Clear Display instruction ldi temp, lcd_Clear ; clear display RAM call lcd_write_instruction_I2C ldi temp, 4 ; data sheet calls for a delay of at least 1.64 mS call delayTx1mS ; Entry Mode Set instruction ldi temp, lcd_EntryMode ; set destired shift characteristics call lcd_write_instruction_I2C ldi temp, 80 ; data sheet calls for a delay of at least 40 uS call delayTx1uS ; This is the end of the LCD controller initilization as specified in the data sheet, but the display ; has been left in the OFF condition. This is a good time to turn the display back ON. ; Display On/Off Control instruction ldi temp, lcd_DisplayOn ; turn the display ON call lcd_write_instruction_I2C ldi temp, 80 ; data sheet calls for a delay of at least 40 uS call delayTx1uS ret ; --------------------------------------------------------------------------- lcd_write_string_I2C: ; This subroutine displays a string of characters on the LCD module in the 8-bit mode ; Enter with: ZH and ZL pointing to the start of the string ; The string must end with a nul (0) ; The desired DDRAM address at which to display the information in (temp) ; set up the initial DDRAM address ori temp, 0b10000000 ; convert the plain address to an address instruction rcall lcd_write_instruction_I2C ; set up the first DDRAM address ldi temp, 80 ; data sheet calls for a delay of at least 40 uS call delayTx1uS ; write the string of characters lcd_write_string_I2C_01: lpm temp, Z+ ; get a character cpi temp, 0 ; check for end of string breq lcd_write_string_I2C_02 ; done ; arrive here if this is a valid character rcall lcd_write_character_I2C ; display the character ldi temp, 80 ; data sheet calls for a delay of at least 40 uS call delayTx1uS rjmp lcd_write_string_I2C_01 ; not done, send another character ; arrive here when all characters in the message have been sent to the LCD module lcd_write_string_I2C_02: ret ; --------------------------------------------------------------------------- lcd_write_character_I2C: ; This subroutine sends 8-bits of data (usually an ASCII character code) to the ; LCD module in the 8-bit mode. ; Enter with the desired data in (temp). ; set up RS push temp ; save data ldi temp, 0b00000100 ; RS high, RW low, E low call IOEXP_Write_PortB ; put data on LCD data lines pop temp ; retrieve data call IOEXP_Write_PortA ; pulse Enable ldi temp, 0b00000101 ; RS high, RW low, E high call IOEXP_Write_PortB call delay1uS ldi temp, 0b00000100 ; RS high, RW low, E low call IOEXP_Write_PortB ret ; --------------------------------------------------------------------------- lcd_write_instruction_I2C: ; This subroutine sends 8-bits of instruction data to the LCD module in the 8-bit mode. ; Enter with the desired instruction in (temp) ; set up RS push temp ; save instruction data ldi temp, 0b00000000 ; RS low, RW low, E low call IOEXP_Write_PortB ; put instruction data on LCD data lines pop temp ; retrieve data call IOEXP_Write_PortA ; pulse Enable ldi temp, 0b00000001 ; RS low, RW low, E high call IOEXP_Write_PortB call delay1uS ldi temp, 0b00000000 ; RS low, RW low, E low call IOEXP_Write_PortB ret ; ============================== End of I2C LCD Subroutines ================= ; ****************************** Time Delay Subroutines ************************ delayYx1mS: ; this subroutine provides a delay of (YH:YL) x 1 mS ; the 16-bit register provides for a delay of up to 65.535 Seconds ; enter with: (YH:YL) = delay data call delay1mS ; delay for 1 mS sbiw YH:YL, 1 ; update the the delay counter brne delayYx1mS ; counter is not zero ; arrive here when delay counter is zero (total delay period is finished) ret ; --------------------------------------------------------------------------- delayTx1mS: ; this subroutine provides a delay of (temp) x 1 mS ; the 8-bit register provides for a delay of up to 255 mS ; enter with: (temp) = delay data call delay1mS ; delay for 1 mS dec temp ; update the delay counter brne delayTx1mS ; counter is not zero ; arrive here when delay counter is zero (total delay period is finished) ret ; --------------------------------------------------------------------------- delay1mS: ; this subroutine provides a delay of 1 mS ; it chews up fclk/1000 clock cycles (including the 'call') push YL ; [2] preserve registers push YH ; [2] ldi YL, low (((fclk/1000)-18)/4) ; [1] delay counter ldi YH, high(((fclk/1000)-18)/4) ; [1] delay1mS_01: sbiw YH:YL, 1 ; [2] update the the delay counter brne delay1mS_01 ; [2] delay counter is not zero ; arrive here when delay counter is zero pop YH ; [2] restore registers pop YL ; [2] ret ; [4] ; --------------------------------------------------------------------------- delayTx1uS: ; this subroutine provides a delay of (temp) x 1 uS with a 16 MHz clock frequency ; the 8-bit register provides for a delay of up to 255 uS ; enter with: (temp) = delay data call delay1uS ; delay for 1 uS dec temp ; decrement the delay counter brne delayTx1uS ; counter is not zero ; arrive here when delay counter is zero (total delay period is finished) ret ; --------------------------------------------------------------------------- delay1uS: ; this subroutine provides a delay of 1 uS with a 16 MHz clock frequency ; it chews up 16 clock cycles (including the 'call') push temp ; [2] these instructions do nothing except consume clock cycles pop temp ; [2] push temp ; [2] pop temp ; [2] ret ; [4] ; ****************************** End of Time Delay Subroutines ***************** ; ****************************** Generic TWI Subroutines *********************** TWI_repeated_start: ; alternate entry point to 'TWI_start' routine ldi status, R_START ; desired status code rjmp TWI_start_01 TWI_start: ; send START condition ldi status, I2C_START ; desired status code TWI_start_01: ; TWCR ; 1 0 1 0 0 1 0 0 ; | | | | | | | | ; | | | | | | | |____ TWIE ; | | | | | | |______ reserved ; | | | | | |________ TWEN ..... enable TWI operation and activate the TWI interface ; | | | | |__________ TWWC ; | | | |____________ TWSTO ; | | |______________ TWSTA .... generate Start condition ; | |________________ TWEA ; |__________________ TWINT .... clear Flag (by setting this bit) to start the data transfer ldi temp, 0b10100100 ; start data transfer sts TWCR, temp ; wait for TWINT and verify status call TWI_verify_status ; arrive here with the T Flag in SREG indicating the TWI status brtc TWI_start_02 ; status code is correct ; arrive here if the status code is incorrect rjmp TWI_error ; display error messages and end the program ; arrive here if the status code is correct TWI_start_02: ret ; --------------------------------------------------------------------------- TWI_send_SLA_W: ; alternate entry point to 'TWI_transfer_data' ; arrive here with 7-bit slave address in (temp) ; send I2C Slave Address with 'Write' bit appended ldi status, MT_SLA_ACK ; desired status code lsl temp ; shift slave address to make room for R/W bit andi temp, 0b11111110 ; clear R/W bit to create 'write' control byte sts TWDR, temp rjmp TWI_transfer_data TWI_send_SLA_R: ; alternate entry point to 'TWI_transfer_data' ; arrive here with 7-bit slave address in (temp) ; send I2C Slave Address with 'Read' bit appended ldi status, MR_SLA_ACK ; desired status code lsl temp ; shift slave address to make room for R/W bit ori temp, 0b00000001 ; set R/W bit to create 'read' control byte sts TWDR, temp rjmp TWI_transfer_data TWI_MT_DATA: ; alternate entry point to 'TWI_transfer_data' ; send I2C data byte ; arrive here with desired data in (temp) ldi status, MT_DATA_ACK ; desired status code sts TWDR, temp TWI_transfer_data: ; main entry point to 'TWI_transfer_data' ; send a data byte to a slave receiver and verify status ; (or) get a data byte from a slave receiver and send a NACK (used for last byte) ldi temp, 0b10000100 ; start data transfer sts TWCR, temp ; wait for TWINT and verify status call TWI_verify_status ; arrive here with the T Flag in SREG indicating the TWI status brtc TWI_transfer_data_01 ; status code is correct ; arrive here if the status code is incorrect rjmp TWI_error ; display error messages and end the program ; arrive here if the status code is correct TWI_transfer_data_01: ret ; --------------------------------------------------------------------------- TWI_MR_DATA: ; get a data byte from a slave receiver and send an ACK (used for all except last byte) ldi status, MR_DATA_ACK ; desired status code ; TWCR ; 1 1 0 0 0 1 0 0 ; | | | | | | | | ; | | | | | | | |____ TWIE ; | | | | | | |______ reserved ; | | | | | |________ TWEN ..... enable TWI operation and activate the TWI interface ; | | | | |__________ TWWC ; | | | |____________ TWSTO ; | | |______________ TWSTA ; | |________________ TWEA ..... enable Acknowledge pulse ; |__________________ TWINT .... clear Flag (by setting this bit) to start the data transfer ldi temp, 0b11000100 ; start data transfer sts TWCR, temp ; wait for TWINT and verify status call TWI_verify_status ; arrive here with the T Flag in SREG indicating the TWI status brtc TWI_MR_DATA_01 ; status code is correct ; arrive here if the status code is incorrect rjmp TWI_error ; display error messages and end the program ; arrive here if the status code is correct TWI_MR_DATA_01: ret ; --------------------------------------------------------------------------- TWI_send_stop: ; Send Stop Condition ; TWCR ; 1 0 0 1 0 1 0 0 ; | | | | | | | | ; | | | | | | | |____ TWIE ; | | | | | | |______ reserved ; | | | | | |________ TWEN ..... enable TWI operation and activate the TWI interface ; | | | | |__________ TWWC ; | | | |____________ TWSTO .... generate Stop condition ; | | |______________ TWSTA ; | |________________ TWEA ; |__________________ TWINT .... clear Flag (by setting this bit) to start the data transfer ldi temp, 0b10010100 sts TWCR, temp ; implement tBUF, 'BUS free time between a STOP and START condition' ldi temp, 10 ; must be at least 4.7uS for Standard Mode (100KHz) I2C call delayTx1uS ; NOTE: There is no status code for having sent a successful Stop Condition. ret ; --------------------------------------------------------------------------- ; Read a series of bytes from TWI device ; Enter with: number of bytes to read in (counter) ; destination of the first byte in (ZH) and (ZL) TWI_read_data: dec counter ; last byte will be dealt with differently TWI_read_data_01: ; get all except the last byte of data and send ACK call TWI_MR_DATA ; get a data byte and send ACK ; arrive here if status code was correct ; get the data from the TWDR and store it lds temp, TWDR ; get the data st Z+, temp ; store the data ; update counter, repeat until done dec counter brne TWI_read_data_01 ; not done - get another byte ; arrive here when all bytes except the last have been read ; get last byte and send NACK ldi status, MR_DATA_NACK ; desired status code rcall TWI_transfer_data ; get a data byte and send NACK ; arrive here if status code was correct lds temp, TWDR ; get the data st Z+, temp ; store the data ret ; --------------------------------------------------------------------------- TWI_verify_status: ; the T Flag in SREG is used to indicate a TWI status ; return with T Flag = 0 if status code correct, T Flag = 1 if status code incorrect clt ; assume the status code is correct ; wait for TWINT Flag set. This indicates that data has been completed verify_status_01: lds temp, TWCR ; get a copy of TWI Control Register sbrs temp, TWINT ; check TWINT flag bit rjmp verify_status_01 ; not ready ; arrive here when when the transmission of the data is completed ; check value of TWI Status Register after masking prescaler bits lds temp, TWSR ; get a copy of TWI Status Register andi temp, 0b11111000 ; mask off prescaler and unused bits cp temp, status ; check for correct status code breq verify_status_02 ; ok (can probably rework to use the carry flag here) ; arrive here if the correct condition code was not received set ; the status code is incorrect verify_status_02: ret ; --------------------------------------------------------- TWI_error: ; Arrive here with the desired status code in (status) and the actual status code in (TWISR) ; We have arrived here via a jump out of a subroutine pop temp ; balance the stack ; turn the error LED on sbi R_led_port, R_led_num ; turn the led on ; could return here with T Flag still set so main program can jump to endless loop - but that gets complicated ; end here error_loop: rjmp error_loop ; ****************************** End of Generic TWI Subroutines **************** ; ****************************** I/O Expander Subroutines ********************** IOEXP_Write_PortA: ; write the byte in (temp) to Port A of the I/O expander push temp ; save data call TWI_start ; I2C START condition ldi temp, IO_I2C_ADDR ; get I2C Slave Address call TWI_send_SLA_W ; send I2C Slave Address with 'Write' bit appended (Device Opcode) ldi temp, GPIOA ; I/O PORTA address (Register Address) call TWI_MT_DATA pop temp ; retrieve data call TWI_MT_DATA ; write data call TWI_send_stop ; I2C STOP condition ret ; --------------------------------------------------------- IOEXP_Write_PortB: ; write the byte in (temp) to Port B of the I/O expander push temp ; save data call TWI_start ; I2C START condition ldi temp, IO_I2C_ADDR ; get I2C Slave Address call TWI_send_SLA_W ; send I2C Slave Address with 'Write' bit appended (Device Opcode) ldi temp, GPIOB ; I/O PORTB address (Register Address) call TWI_MT_DATA pop temp ; retrieve data call TWI_MT_DATA ; write data call TWI_send_stop ; I2C STOP condition ret ; ****************************** End of I/O Expander Subroutines ***************