# ############################################################################# # # Copyright (c) 2002-2003 by Wayne C. Gramlich and Bill Benson # All rights reserved. # # Permission to use, copy, modify, distribute, and sell this software # for any purpose is hereby granted without fee provided that the above # copyright notice and this permission are retained. The author makes # no representations about the suitability of this software for any purpose. # It is provided "as is" without express or implied warranty. # # This is the code that implements the CompassDT1 RoboBrick. Basically # it just waits for commands that come in at 2400 baud and responds # to them. See # # http://gramlich.net/projects/robobricks/compassdt1/index.html # # for more details. # # ############################################################################# processor pic16c505 cp=off wdte=on mclre=off fosc=intrc_no_clock # Define processor constants: constant clock_rate 4000000 constant clocks_per_instruction 4 constant instruction_rate clock_rate / clocks_per_instruction # Define serial communication control constants: constant baud_rate 2400 constant instructions_per_bit instruction_rate / baud_rate constant delays_per_bit 3 constant instructions_per_delay instructions_per_bit / delays_per_bit constant extra_instructions_per_bit 9 constant extra_instructions_per_delay extra_instructions_per_bit / delays_per_bit constant delay_instructions instructions_per_delay - extra_instructions_per_delay # Register definitions: # Status register: register status 3 bind c status@0 bind z status@2 # OSCCAL register: register osccal 5 constant osccal_lsb 4 constant i2c_delay 3 # Define bit offsets: constant d1_bit 5 constant d2_bit 4 constant d3_bit 3 constant d4_bit 2 constant d5_bit 1 constant d6_bit 0 constant scl_bit 0 constant sda_bit 1 constant freq_bit 2 constant serial_in_bit 3 constant serial_out_bit 4 constant trigger_bit 5 # Define pin assignments and directions: port portb b bits_and_byte read_write_manual port portc c bits_and_byte read_write_static pin d1 portc d1_bit write_only pin d2 portc d2_bit write_only pin d3 portc d3_bit write_only pin d4 portc d4_bit write_only pin d5 portc d5_bit write_only pin d6 portc d6_bit write_only pin scl portb scl_bit read_write_manual pin sda portb sda_bit read_write_manual pin trigger portb trigger_bit write_only pin freq portb freq_bit write_only pin serial_out portb serial_out_bit write_only pin serial_in portb serial_in_bit read_only constant mask 0x1f string_constants { id = 1, 0, 13, 1, 0, 0, 0, 0, 0r'16', 11, 0s'CompassDT1C', 15, 0s'Gramlich&Benson' } # Some globals: byte global interrupt_enable bit global interrupt_pending bit global receiving bit bank 1 global temporary byte global glitch byte global index byte # Note that the 12-bit PIC's only have a 2-level deep stack. # The code starts in the main procedure (located at the end of this code) # The next level of procedure call is either get_byte or send_byte. # Lastly, the lowest level of procedure call is delay. It all fits, # but just barely. bank 0 procedure i2c_start { arguments_none returns_nothing # Invariant: SCL=OC & SDA=OC nop i2c_delay direction sda write nop i2c_delay direction scl write nop i2c_delay # Invariant: SCL=0V & SDA=0V } procedure i2c_restart { arguments_none returns_nothing nop i2c_delay direction sda read nop i2c_delay direction scl read nop i2c_delay direction sda write nop i2c_delay direction scl write } procedure i2c_stop { arguments_none returns_nothing # Invariant: SCL=0V & SDA=0V nop i2c_delay direction scl read nop i2c_delay direction sda read nop i2c_delay # Invariant: SCL=OC & SDA=OC } procedure i2c_get_byte { arguments_none returns byte # This procedure will wait for a byte to be received from # serial_in_bit. It calls the delay procedure for all delays. variable count byte variable char byte # Invariant: SCL=0V & SDA=0V direction sda read count_down count 8 { char := char << 1 nop i2c_delay direction scl read nop i2c_delay if (sda) { char@0 := 1 } direction scl write nop i2c_delay } # Clock in ACK bit: direction sda write nop i2c_delay direction scl read nop i2c_delay direction scl write nop i2c_delay # Invariant: SCL=0V & SDA=0V return char } procedure i2c_send_byte { argument data byte returns_nothing # Send {data} to compass using I2c: variable count byte # Invariant: SCL=0V & SDA=0V # Send 8-bits of data: count_down count 8 { if (data@7) { direction sda read } else { direction sda write } data := data << 1 nop i2c_delay direction scl write nop i2c_delay direction sda read nop i2c_delay } # Clock in ACK bit: direction sda read nop i2c_delay direction scl read count := 255 while (sda && count != 0) { count := count - 1 } direction scl write count := 255 while (!sda && count != 0) { count := count - 1 } # Grab the SDA back again: direction sda write #Invariant: SCL=0V & SDA=0V } procedure get_byte { arguments_none returns byte # This procedure will wait for a byte to be received from # serial_in_bit. It calls the delay procedure for all delays. variable count byte variable char byte # Why does the delay procedure wait for a third of bit? Well, it # has to do with the loop immediately below. If we catch the # start bit at the beginning of a 1/3 bit time, we will be # sampling data at approximately 1/3 of the way into each bit. # Conversely, if we catch the start near the end of a 1/3 bit # bit time, we will be sampling data at approximately 2/3 of the # way into each bit. So, what this means is that our bit sample # times will be somewhere between 1/3 and 2/3 of bit (i.e. in # the middle of the bit. # It would be nice to tweak the code to shorter delay times # (1/4 bit, 1/5 bit, etc.) but then it gets too hard to get # the bookeeping done in the delay routine. A 12-bit PIC # running at 4MHz (=1MIPS), only has 138 instructions available # for the delay routine when at 1/3 of bit. # Wait for a start bit: receiving := 1 while (serial_in) { call delay() } # Clear interrupt: # 1 cycle: serial_out := 1 # Skip over start bit: call delay() call delay() call delay() # Sample in the middle third of each data bit; # 1 cycle: char := 0 # 2 cycles to set up loop: # 1+1+2 = 4 nop extra_instructions_per_bit - 4 count_down count 8 { call delay() # 2 cycles: char := char >> 1 # 2 cycles: if (serial_in) { char@7 := 1 } call delay() call delay() # 3 cycles at end of loop: # 2+2+3 = 7 nop extra_instructions_per_bit - 7 } # Skip over 2/3's of stop bit: call delay() call delay() return char } origin 0x200 bank 1 procedure send_byte { argument char byte returns_nothing # Send {char} to {tx}: variable count byte # {receiving} will be 1 if the last get/put routine was a get. # Before we start transmitting a response back, we want to ensure # that there has been enough time to turn the line around. # We delay the first 1/3 of a bit to pad out the 9-2/3 bits from # for get_byte to 10 bits. We delay another 1/3 of a bit just # for good measure. Technically, the second call to delay() # is not really needed. if (receiving) { receiving := 0 call delay() call delay() } # Send the start bit: # 1 cycle: serial_out := 0 call delay() call delay() call delay() # 2 cycles to set up loop: # 1+2 = 3 nop extra_instructions_per_bit - 3 # Send the data: count_down count 8 { # 4 cycles: serial_out := char@0 # 2 cycles: char := char >> 1 call delay() call delay() call delay() # 3 cycles at end of loop: # 4+2+3 = 9 = no NOP's needed: } # Send the stop bit: nop 1 # 1 cycle serial_out := 1 call delay() call delay() call delay() # 2 cycles for call/return # 2 cycles for argument # 1+2+2 = 5 nop extra_instructions_per_bit - 5 } procedure delay { arguments_none returns_nothing uniform_delay delay_instructions # This procedure delays 1/3 of a bit. # Kick the dog: watch_dog_reset } # The main procedure is loaded with switch statements. On the 12-bit # PIC's, switch statements have to live in the first 256 bytes of # each code bank. For this reason, we shove main into code bank 1. # If we, try to put main in code bank 0, it pushes the first bytes # of several routines out of the first 256 bytes, which is also a # no-no of the 12-bit PIC's. procedure main { arguments_none returns_nothing variable command byte variable temp byte # Initalize all of the globals: interrupt_enable := 0 interrupt_pending := 0 # Initialize remaining registers: glitch := 0 index := 0 # Set up the SCL bus: direction sda read direction scl read sda := 0 scl := 0 trigger := 0 loop_forever { nop 40 trigger := 0 sda := 0 scl := 0 direction scl read direction sda read nop 60 trigger := 1 sda := 0 scl := 0 direction scl write direction sda write } # Process commands: loop_forever { # Wait for command: #command := get_byte() command := 3 # Dispatch on command: switch (command >> 6) { case 0 { # (Command = 00xx xxxx): switch ((command >> 3) & 7) { case 0 { # (Command = 0000 0xxx): switch (command & 7) { case 0 { # (Command = 0000 0000): call send_byte(0xa4) call i2c_start() call i2c_send_byte(0xc0) call i2c_send_byte(1) call i2c_restart() call i2c_send_byte(0xc1) temporary := i2c_get_byte() call i2c_stop() call send_byte(temporary) portc := temporary } case 1 { # (Command = 0000 0001): call send_byte(0xa5) call i2c_start() call i2c_send_byte(0xc0) call i2c_send_byte(0) call i2c_restart() call i2c_send_byte(0xc1) temporary := i2c_get_byte() call i2c_stop() call send_byte(temporary) } case 2 { # (Command = 0001 0xxx): call send_byte(2) call send_byte(3) } case 3 { loop_forever { trigger := 1 nop 5 call i2c_start() call i2c_send_byte(0xc0) nop 50 call i2c_send_byte(0) call i2c_stop() nop 50 call i2c_start() call i2c_send_byte(0xc1) nop 50 temporary := i2c_get_byte() call i2c_stop() portc := temporary trigger := 0 call delay() } } case 4, 5, 6, 7 { # (Command == (0000 01xx): } } } case 1 { # (Command = 0000 1xxx): } case 2 { # (Command = 0001 0xxx): } case 3 { # (Command = 0001 1xxx): } case 4, 5, 6, 7 { # (Command = 001x xxxx): } } } case 1 { # (Command = 01xx xxxx): # Do nothing: } case 2 { # (Command = 10xx xxxx): # Do nothing: } case 3 { # (Command = 11xx xxxx): switch ((command >> 3) & 7) { case 0, 1, 2, 3, 4 { # (Command = 110x xxxx or 1110 0xxx): # Do nothing: } case 5 { # (Command = 1110 1xxx): if (command = 0xef) { # Read Interrupt Bits (Command = 1110 1111): temporary := 0 if (interrupt_pending) { temporary@0 := 1 } if (interrupt_enable) { temporary@1 := 1 } call send_byte(temporary) } } case 6 { # (Command = 1111 0xxx): # Switching between register banks generates bulky code; # Keep code generation in bank 0 by assigning command to # temporary: temporary := command switch (temporary & 7) { case 0, 1, 2, 3 { # Set Interrupt Bits (Command = 1111 00ep): interrupt_enable := temporary@1 interrupt_pending := temporary@0 } case 4, 5 { # Set Interrupt Pending (Command = 1111 010p): interrupt_pending := temporary@0 } case 6, 7 { # Set Interrupt Enable (Command = 1111 011e): interrupt_enable := temporary@0 } } } case 7 { switch (command & 7) { case 0 { # Clock Decrement (Command = 1111 1000): osccal := osccal - osccal_lsb } case 1 { # Clock Increment (Command = 1111 1001): osccal := osccal + osccal_lsb } case 2 { # Clock Read (Command = 1111 1010): call send_byte(osccal) } case 3 { # Clock Pulse (Command = 1111 1011): call send_byte(0) } case 4 { # ID Next (Command = 1111 1100): call send_byte(id[index]) index := index + 1 if (index >= id.size) { index := 0 } } case 5 { # ID Reset (Command = 1111 1101): index := 0 } case 6 { # Glitch Read (Command = 1111 1110): call send_byte(glitch) glitch := 0 } case 7 { # Glitch (Command = 1111 1111): if (glitch != 0xff) { glitch := glitch + 1 } } } } } } } } }