ucl 1.0 # Copyright (c) 2002-2005 by Wayne C. Gramlich. # All rights reserved. # This code started out as some assembly code that was written by # Chuck McManis. It has been modified basically beyond recognition. # None-the-less, I would like to thank Chuck for his contribution # to the effort. # The Lumix(r) LCM-S01602DTR/M 2 line by 16 character LCD panel uses the # Samsung(r) S6A0069 LCD controller. The S6A0069 LCD controller has # 80 bytes of internal memory available for displaying characters. # The byte addresses for these data bytes are 0x00 through 0x4f(=79). # The controller will display the 16 characters of data starting at # 0x00 + N on the first line, and 0x20 + N on the second line, where # N is a number between 0 and 0x18(=24). # The LCD32 module provides the user with 4 lines of 16 characters # each. These are arranged as follows: # # Line Addresses Shift(N) # 0 00 - 0f 00 # 1 20 - 2f 00 # 2 10 - 1f 10 # 3 30 - 3f 10 # # In order to display lines 0 and 1, the shift amount (N) is set to 0. # In order to display lines 2 and 3, the shift amount is set to 0x10(=16). # The Samsung chip can only change the shift amount in increments of # +/- 1 per shift command. However, since the command only requires # ~40uS, 16 shifts can take place so fast that it appears to be # instantenous to the end user. library $pic16f688 library clock8mhz library bit_bang # Number of instructions required to delay a microsecond: constant microsecond = instruction_rate / 1000000 configure mclre=off, fosc=int_no_clk # Port stuff: ## Bit positions: ## Port A: #constant e_bit = 0 #constant db3_bit = 1 #constant serial_out_bit = 2 #constant serial_in_bit = 3 #constant debug_mode_bit = 4 #constant lines34_bit = 5 ## Port C: #constant db4_bit = 0 #constant db5_bit = 1 #constant db6_bit = 2 #constant db7_bit = 3 #constant rs_bit = 4 #constant rw_bit = 5 # ## Bit masks: ## Port A: #constant e_mask = 1 << e_bit #constant db3_mask = 1 << db3_bit #constant serial_out_mask = 1 << serial_out_bit #constant serial_in_mask = 1 << serial_in_bit #constant debug_mode_mask = 1 << debug_mode_bit #constant lines34_mask = 1 << lines34_bit ## Port C: #constant db4_mask = 1 << db4_bit #constant db5_mask = 1 << db5_bit #constant db6_mask = 1 << db6_bit #constant db7_mask = 1 << db7_bit #constant rs_mask = 1 << rs_bit #constant rw_mask = 1 << rw_bit constant trisc_mask = 0 #constant trisc_mask = 0x30 constant db47_mask = 0xf ## Bit bindings: ## Port A: #bind e = $porta@e_bit #bind db3 = $porta@db3_bit #bind serial_out = $porta@serial_out_bit #bind serial_in = $porta@serial_in_bit #bind debug_mode = $porta@debug_mode_bit #bind lines34 = $porta@lines34_bit ## Port C: #bind db4 = $portc@db4_bit #bind db5 = $portc@db5_bit #bind db6 = $portc@db6_bit #bind db7 = $portc@db7_bit #bind rs = $portc@rs_bit #bind rw = $portc@rw_bit #package pdip # pin 1 = power_supply # pin 2 = ra5_in, name = lines34 # pin 3 = ra4_in, name = debug_mode # pin 4 = ra3_in, name = serial_in # pin 5 = rc5_out, name = rw # pin 6 = rc4_out, name = rs # pin 7 = rc3_out, name = db7 # pin 8 = rc2_out, name = db6 # pin 9 = rc1_out, name = db5 # pin 10 = rc0_out, name = db4 # pin 11 = ra2_out, name = serial_out # pin 12 = ra1_out, name = db3 # pin 13 = ra0_out, name = e # pin 14 = ground package pdip pin 1 = power_supply pin 2 = ra5_out, name = rs pin 3 = ra4_in, name = lines34 pin 4 = ra3_in, name = debug_mode pin 5 = rc5_in, name = serial_in pin 6 = rc4_out, name = serial_out pin 7 = rc3_out, name = db7 pin 8 = rc2_out, name = db6 pin 9 = rc1_out, name = db5 pin 10 = rc0_out, name = db4 pin 11 = ra2_out, name = rw pin 12 = ra1_out, name = db3 pin 13 = ra0_out, name = e pin 14 = ground constant line_mask = 3 constant column_mask = 0xf constant cursor_mode_mask = 3 global shift byte global column byte global line byte global cursor_mode byte global font_address byte global command_previous byte global command_last byte global sent_previous byte global sent_last byte constant queue_size = 4 global data_queue[queue_size] array[byte] global control_queue[queue_size] array[byte] global processed byte global available byte # The field layout of a control mask byte is: # # Bits Name Description # 0-3 Count Execute command count - 1 times (i.e. 0 => execute once) # 4 Data Write 0 => Command Write(RS=0); 1=> Data Write(RS=1) # 5 NOP No Operation 1=>ignore command; 0=>do command constant count_mask = 0x0f # Note that RS_MASK is 0x10 #constant data_write = rs_mask constant data_write = 0x10 constant command_write = 0 constant nop_mask = 0x20 procedure main arguments_none returns_nothing local command byte local temporary byte local id_index byte local glitch byte # Switch over to 8MHz: $osccon := 0x71 column := 0 line := 0 cursor_mode := 3 shift := 0 processed := 0 available := 0 font_address := 0 glitch := 0 id_index := 0 # Initialize the LCD: call lcd_init() # Output the message: call line_put(0, '1') call line_put(1, '2') call line_put(2, '3') call line_put(3, '4') line := 0 call lcd_command(0x80) loop_forever # Get a command byte: command := byte_get() # The byte_get() procedure returns after 10-2/3 bits have # been processed. That means that the serial input bit # should be processing the stop bit. After all commands # that do not return a byte, we must call back into byte_get() # within about 1/3 of a bit time (138uS). Anything longer, # and we might miss the beginning of the start bit for the # next command byte. # # The operations to access the LCD controller take 39uS # to write a command or data and 43uS to read some data. # What this means is that for commands that do not return # any data, we only have time to perform two LCD access # commands. There is one command, clear display, that # that takes 1.53mS. In addition, some commands, like # line feed, that needs to clear the entire contents # of the next line. This takes 16+ commands. # # The solution to all of these problems is to have all of # the LCD access commands take place from the delay() # procedure. As long as everything important is performed # there and in the right sequence, we won't violate the # "do everything in 1/3 of a bit time" rule in this procedure. # # For more details on the architecture of the delay procedure, # see the delay procedure comments further below. # Zero the command queue: available := 0 processed := 0 # Dispatch on the command: switch command >> 6 case 0 # 00xx xxxx switch (command >> 3) & 7 case 1 # 0000 1xxx switch command & 7 case_maximum 7 case 0 # 0000 1000 (Back Space): if column != 0 column := column - 1 case 2 # 0000 1010 (Line Feed): line := (line + 1) & line_mask column := 0 # Force cursor to correct location: call cursor_enqueue() # Write out 16 spaces: call enqueue(data_write | (16 - 1), ' ') # There are now 2 commands in queue: case 4 # 0000 1100 (Form Feed): # Clear the display: call enqueue(command_write, 0) # The clear display command takes 1.53mS; # 12 * .138 = 1.656mS. call enqueue(nop_mask | (12 - 1), 0) line := 0 column := 0 shift := 0 case 5 # 0000 1101 (Carriage Return): column := 0 if !debug_mode line := 0 call cursor_enqueue() case 0, 4, 5, 6, 7 # 0000 0xxx or 001x xxxx: call character_put(command) case 1 # 01xx xxxx: call character_put(command) case 2 # 10xx xxxx: switch (command >> 3) & 7 case_maximum 7 case 0 # 1000 0xxx: line := command & line_mask column := 0 if command@2 # Clear the line: call cursor_enqueue() call enqueue(data_write | (16 - 1), ' ') do_nothing call cursor_enqueue() case 1 # 1000 1xxx: switch command & 7 case 0, 1, 2, 3 # 1000 10vb (Cursor Mode Set): cursor_mode := command & cursor_mode_mask call enqueue(command_write, 0xc | cursor_mode) case 4 # 1000 1100 (Cursor Mode Read): temporary := cursor_mode if debug_mode temporary@2 := 1 if lines34 temporary@3 := 1 call byte_put(temporary) case 5 # 1000 1101 (Character Read): temporary := lcd_read() call byte_put(temporary) if column = 15 line := (line + 1) & line_mask column := 0 else column := column + 1 call cursor_enqueue() case 6 # 1000 1110 (Line Read): call byte_put(line) case 7 # 1000 1111 (Column Read): call byte_put(column) case 2, 3 # 1001 xxxx (Column Set): column := command & column_mask call cursor_enqueue() case 4, 5 # 1010 xxxx (Column Set and Clear): column := column & column_mask call cursor_enqueue() call enqueue(data_write | (16 - column), ' ') call cursor_enqueue() case 6 # 1011 0xxx: switch command & 7 case_maximum 7 case 0 # 1011 0000 (Set Font Address): font_address := byte_get() & 0x1f case 1 # 1011 0001 (Read Font Address): call byte_put(font_address) case 2 # 1011 0010 (Read Font Line): # Set the font address, and access font memory: call lcd_command(0x40 | font_address) # Read the data: temporary := lcd_read() # Force back to display memory: call cursor_position() font_address := font_address + 1 # Ship the data back: call byte_put(temporary) case 3 # 11xx xxxx: switch (command >> 3) & 7 case 0, 1, 2, 3 # 110p pppp (Write Font Line): # Set font address and access font memory: call enqueue(command_write, 0x40 | font_address) # Write one line of font memory: call enqueue(data_write, command & 0x1f) # Force back to display memory: call cursor_enqueue() font_address := font_address + 1 case 7 # 1111 1xxx: switch command & 7 case 0 # 1111 1000 (Clock Decrement): #$osccal := $osccal - $osccal_lsb $osctune := $osctune - $osccal_lsb case 1 # 1111 1001 (Clock Increment): #$osccal := $osccal + $osccal_lsb $osctune := $osctune + $osccal_lsb case 2 # 1111 1010 (Clock Read): #call byte_put($osccal) call byte_put($osctune) case 3 # 1111 1011 (Clock Pulse): call byte_put(0) case 4 # 1111 1100 (ID Next): call byte_put(id_get(id_index)) id_index := id_index + 1 case 5 # 1111 1101 (ID Reset): id_index := 0 case 6 # 1111 1110 (Glitch Read): call byte_put(glitch) glitch := 0 case 7 # 1111 1111 (Glitch): if glitch != 0xff glitch := glitch + 1 procedure line_put argument new_line byte argument character byte returns_nothing line := new_line column := 0 call cursor_position() loop_exactly 16 call lcd_character_put(character) procedure character_put argument character byte returns_nothing # This procedure will output {character} to the display # and update the {line} and {position} global variables. # If the cursor is in the column 15, it is either kept # there (normal mode) or advanced to the next line in column # 0 (debug mode). This procedure enqueues a total of three # commands onto the command queue. call cursor_enqueue() call enqueue(data_write, character) if column = column_mask if !debug_mode # Advance to next line: column := 0 line := (line + 1) & line_mask # else stay put: else # Advance to next column: column := column + 1 call cursor_enqueue() # A total of 3 commands are enqueued: procedure cursor_enqueue arguments_none returns_nothing # This procedure will ensure that a command that forces the cursor # to the correct location specified by the global variables {line} # and {column}. local command byte command := 0x80 if line@0 command := command | 0x40 if line@1 command := command | 0x10 call enqueue(command_write, command | column) procedure cursor_position arguments_none returns_nothing # This prodedure will force the cursor to the location specified # by the globals variables {line} and {position}. local command byte command := 0x80 if line@0 command := command | 0x40 if line@1 command := command | 0x10 command := command | column call lcd_command(command) procedure lcd_command argument command byte returns_nothing exact_delay 52 * microsecond # This procedure will strobe {command} into the LCD controller # as two 4-bit nibbles. The procedure delays by 39uS to ensure # the command has time to be digested by the LCD controller. # Send high nibble (RW=0 & RS=0): #FIXME: Shouldn't need "& 0xf" because ">> 4" should do it!!! $portc := (command >> 4) & 0xf rw := 0 rs := 0 e := 1 e := 0 # Send low nibble (RW=0 & RS=0): $portc := command & 0xf rw := 0 rs := 0 e := 1 e := 0 # Wait for the command to be digested: # (RETURN instruction takes two cycles): #delay 39 - 2 # do_nothing procedure lcd_character_put argument character byte returns_nothing exact_delay 52 * microsecond # This procedure will output {character} to the LCD display # and move the cursor right by one. # Send high nibble (RW=0 & RS=1): #$portc := (character >> 4) | rs_mask $portc := character >> 4 rw := 0 rs := 1 e := 1 e := 0 # Send low nibble (RW=0 & RS=1): #$portc := (character & 0xf) | rs_mask $portc := character & 0xf rw := 0 rs := 1 e := 1 e := 0 # Wait 39uS for the command to finish: # (RETURN instruction takes two cycles): #delay 39 - 2 # do_nothing procedure lcd_read arguments_none returns byte # Generic routine to read a byte from the LCD controller. local result byte # First, turn DB4-7 into inputs: #$trisc := db4_mask | db5_mask | db6_mask | db7_mask $trisc := trisc_mask | db47_mask # Read high nibble: #$portc := rs_mask | rw_mask rs := 1 rw := 1 e := 1 delay 43 * microsecond do_nothing result := $portc << 4 e := 0 # Read low nibble: #$portc := rs_mask | rw_mask rs := 1 rw := 1 e := 1 result := result | ($portc & 0xf) e := 0 # Return DB4-7 to outputs: #$trisc := 0 $trisc := trisc_mask return result procedure enqueue argument control byte argument data byte returns_nothing #: This procedure will enqueue {control} and {data} onto the #, command queue. It is your responsibility to ensure that #, the there is enough space in the queue. data_queue[available] := data control_queue[available] := control available := available + 1 procedure lcd_delay argument amount byte returns_nothing # This procedure is designed to delay for 100uS times {amount}. loop_exactly amount delay (100 * microsecond) - 3 do_nothing procedure lcd_init arguments_none returns_nothing # The LCD needs to be initialized into 4 bit mode and then # we can write letters to it. Note that the LCD is ALWAYS # in write mode, no need to check to see if its busy, I just # wait and assume it isn't. # # According to the SAMSUNG data sheet, you first have to # wait 30+ mS to insure the display is "stable" after Vcc is # applied. Then the follow a specific sequence puts it into # "4 bit" mode. However, I've noticed they assume that the # LCD is already in 8 bit mode, which it won't be following # a warm restart (resetting the PIC). Further, if you ever # get "out of sync" and start sending the low nibble and then # the high nibble all heck breaks loose. So the INIT function # actually does three things: # 1) It waits 40mS for the display to initialize just # in case this was a power up event. # 2) Next it programs the display into 8 bit mode on # purpose. Even if it was in 4 bit mode already we # put the display in a "known" state. # 3) Finally, now that it knows what state the display # is in, it configures it for two line, 4 bit operation. # # This is a routine that clocks out two nibbles using the E clock and # then waits 100 uS. The first commands we send it are 0x33 hex. We send # 0x33 followed by 0x32. This has the effect of clocking out 0x3 to the # port at least 3 times, with the command bit (R/S) set. This will ALWAYS # set us to 8 bit mode eventually. # # * On power up we're already in 8 bit mode and 0x3 doesn't change that. # * In 4 bit mode the first two nibbles will put you into 8 bit mode # * Out of sync 4-bit mode, the first nibble finishes the low nybble of # the previous command, and the next two put us into 8 bit mode. # # Once we are SURE we're in 4 bit mode, we can send commands through # the 4 bit interface, to finish initialization we send: # 0x28 - Four bit mode, two line display, 5 x 8 font. # 0x08 - Turn off the display # 0x01 - Clear the display, reset the DDRAM pointer # 0x0E - Turn on the display, cursor, blink it, and don't shift. # Step 1: Wait for the LCD to initialize from Power on: loop_exactly 5 call lcd_delay(100) # Step 2: Put the LCD in a "known" mode. # "8-bit mode"; R/W = 0, RS = 0, DB7 - DB4 = 0x03: $portc := 0x03 rw := 0 rs := 0 e := 1 e := 0 call lcd_delay(1) # If in 4 bit mode this is the "low nibble": e := 1 e := 0 call lcd_delay(1) # If out of sync this is the low nybble in 4 bit mode: e := 1 e := 0 call lcd_delay(1) # Finally we can ask for 4 bit mode: $portc := 0x02 rw := 0 rs := 0 e := 1 e := 0 # Changing this delay didn't work (#2) call lcd_delay(1) # Now its in 4 bit mode and we have to send double nybbles each # time. Fortunately we can use a subroutine for this. # FUNCTION SET - 4-bit, 2 lines, 5x8 font: call lcd_command(0x28) # AGAIN : FUNCTION SET - 4-bit, 2 lines, 5x8 font: # Sending this command twice, *does* work. call lcd_command(0x28) # Turn off the display temporarily: call lcd_command(0x08) # Clear the display: call lcd_command(0x01) call lcd_delay(20) # Turn the display back on with Cursor: call lcd_command(0x0f) # Set shift mode: call lcd_command(0x06) procedure message_get argument index byte returns byte switch index & 0xf case 0 return 'L' case 1 return 'C' case 2 return 'D' case 3 return '3' case 4 return '2' case 5 return 'C' case 6 return ' ' case 7 return 'R' case 8 return 'o' case 9 return 'b' case 10 return 'o' case 11 return 'B' case 12 return 'R' case 13 return 'i' case 14 return 'X' case 15 return ' ' procedure id_get argument index byte returns byte #: This procedure returns the {index}'th byte of the id string. if index <= 44 switch index case 0 return 1 case 1 return 0 case 2 return 255 case 3 return 0 case 4 return 1 case 5, 6, 7 return 0 case 8, 9, 10, 11, 12, 13, 14, 15 return 0 case 16, 17, 18, 19, 20, 21, 22, 23 return 0 case 24 return 6 case 25 return 'L' case 26 return 'C' case 27 return 'D' case 28 return '3' case 29 return '2' case 30 return 'C' case 31 return 13 case 32 return 'M' case 33 return 'o' case 34 return 'n' case 35 return 'd' case 36 return 'o' case 37 return '-' case 38 return 't' case 39 return 'r' case 40 return 'o' case 41 return 'n' case 42 return 'i' case 43 return 'c' case 44 return 's' return 0 procedure delay arguments_none returns_nothing exact_delay delay_instructions # This procedure delays by 1/3 of a bit time. # This procedure processes the command queue commands that have # been set up by the main() procedure. If there are no more # commands in the queue, we figure out whether or not the # line switch requires any display shifting. # # The entire command queue from main() will be emptied before # the next command comes in. The reasoning for this is because # most top level commands only take two or three LCD commands. # A clear display command takes 1.53mS, a total of 12 calls to # delay() (12 * 138uS = 1.56mS). A line clear takes a few # overhead commands followed by 16 data writes. Since a command # byte requites 9-2/3 bits, 29 calls to delay will occur. Thus, # we are safe. The display shifting stuff can take place last, # since it has no time criticalities. # # What this all means is we can set up a simple queue of # instructions to be executed by the delay() procedure. # The main() procedure fills the queue and the delay() # procedure empties it. We do not have to be very sophisticated, # the queue will be empty before the next time main() # gets around to filling it. local control byte local data byte local do_it bit local count byte # Kick the dog: watch_dog_reset do_it := 0 if processed < available # We have commands in queue to execute: control := control_queue[processed] data := data_queue[processed] # Figure out whether to actually process this command: if control & nop_mask = 0 do_it := 1 # Process count: count := control & count_mask control_queue[processed] := control & 0xf0 | (count - 1) if count = 0 # Advance to next command: processed := processed + 1 else # Make sure that the proper lines are visible: if lines34 # Make sure that lines 3-4 are displayed: if shift < 16 # Lines 3-4 are not completely visible yet; shift left by 1: data := 0x18 control := command_write shift := shift + 1 do_it := 1 else # Make sure that lines 1-2 are displayed: if shift != 0 # Lines 1-2 are not completely visible yet; shift right by 1: data := 0x1c control := command_write shift := shift - 1 do_it := 1 if do_it # Note that {data_write} = {rs_mask}: # Send high nibble (RW=0): #$portc := control & rs_mask | data >> 4 $portc := data >> 4 rw := 0 rs := 0 if control & data_write != 0 rs := 1 rs := 1 e := 1 e := 0 # Send low nibble (RW=0): #$portc := control & rs_mask | data & 0xf $portc := control | (data & 0xf) rw := 0 rs := 0 if control & data_write != 0 rs := 1 e := 1 e := 0 # The delay() procedure is 138uS which is much greater than # 39uS - 43uS that commands take. We do not have to do any # further action for command delay. # We are all done: