Introduction
In this demo we will focus on how we generate input and output for the hcs12, and on how we use subroutines. We will also learn a little about the stack. Understanding these concepts is essential to developing programs for embedded systems.
We will create a program that uses the push-button keys labeled SW2 - SW3 on the Dragon12 board as input (these are the yellow pushbutton keys on the lower left of the board). For the output, each key will light a specific LED for a specific amount of time. The program uses a subroutine called "DELAY" which determines the amount of time each LED is on.
Key Concepts
Before doing the demo, read the points below and make sure you understand them.
* The program below runs in an infinite loop . We start the main program and it will run forever, unless we turn the power off or unless we manually force an exit via the DBug12 "abort" button. Programs in embedded systems are typically written like this - they are always on, running in an infinite loop, doing something over and over again.
* The parameters for the delay loop subroutine are passed from the main program to the subroutine via Register D. There are many ways to pass a parameter to a subroutine, and this is the most commonly used method. It works OK as long as there aren't too many parameters to pass - we can't pass more parameters than there are registers with this technique. Note that we use Register D (16 bits) since the smaller 8 bit registers A and B don't have enough bits to hold the relatively large values we pass to the subroutine.
* We use a technique called polling to determine the switch states. In polling, our CPU is constantly checking the condition of a switch to see if it is open or closed. This is not a problem for simple programs like this, but for more involved cases, it is a waste of CPU time. It is much better to respond to the keys only when they are pushed - in the meantime, the CPU can be doing something else. The technique used for this is to use "interrupts", a topic that will be covered in a later lab.
* We generate a delay using a brute force (and inefficient) routine called a "software delay". For the whole of the delay time, the microcontroller is doing nothing but running in a long loop, and it can't do anything else while it's in this timing loop. It is more accurate and much more efficient to use a hardware timer, which runs in the background and doesn't tie up the CPU. The timer uses an interrupt to notify the CPU when the time has elapsed, and at this point the CPU will then focus its attention on what follows the delay. Using hardware timers and interrupts are explored in a later demo.
* The total time spent in the software DELAY subroutine below is determined by counting the number of clock cycles spent in the routine, and then multiplying this by the clock time per cycle. For instance, in the "startx" loop, each "dex" takes 1 cycle and the "bne" takes 3 cycles if the branch is taken (you can determine the number of clock cycles per instruction from the HCS12 programmers reference book). So, there are four clock cycles per startx loop at ~ 20 nsec per cycle (1/48MHz for the CPU only) to give a cycle time of about 80 nsec per startx loop. If we run 12,000 ($2EE0) startx loops, this will take about 12,000 x 80 nsec ~ 1 msec. The number in the X register, loaded in at the "starty" label, determines the number of startx loops we run. So if we load 12000 ($2EE0) into X, then the starty loop cycle time will be about 1 msec (12000 x 80 nsec). If we then run the starty loop 1000 ($3E8) times, then the starty loop will take 1000 x 1msec = 1 sec to complete. Put another way, we can say that each inner startx loop runs 12000 times at 80 nsec per loop for a total of 1 msec, while the outer starty loop runs 1000 times at 1msec a loop for a total of 1 sec. Note that we have ignored the delay time from the instructions outside of the startx and starty loops. These instructions, such as the 'ldx' and 'dey' are only executed a few times total, and only add a small bit of time to the total time. If very accurate timings were needed, we would have to consider these as well as other factors (such as the overhead time in jumping to the subroutine).
* We use Port H, at address $0260, for input and Port B, at address $0001, for output. These ports have eight pins, each of which can be configured to be either an input or an output port by writing to the Data Direction Register (DDR) for the port. For instance, the Port H DDR is at address $0262, and to set all the pins at Port H to be inputs, we write $00 to address $0262. We might do the write with a command such as "movb #$00,$0262". If we wanted to set them all to outputs, we would write $FF to address $0262. Once we have set the I/O direction on a port via the DDR, we may then write or read to that port as appropriate. For instance, if all the pins on Port B are set as outputs, then to set bit 3 (of 0-7) high and the others low, we would write "$08" ($08 = %0000 1000 in binary) to address $0001. We might do this using "movb #$08, $0001". When we want to read an input port, we check to see what logical values are on the pins of the port. As an example, we would use "ldaa $0001" to read the values on Port B (assuming its set as an input) and store them in Register A. If pin 2 of Port B is low, and the others high, then after the read register A would contain the value %1111 1011 = $FB.
> Note that when we write constants, such as "#$08" to an address or register, we are using immediate addressing, so we prefix the value with a number sign "#". Also note in that binary numbers are prefixed with a percent sign "%", and hex with a dollar sign "$" (decimal numbers don't use any prefixing).
* In the program below, also note the use of WHITE SPACE . "White space" is something which puts space at the beginning of a line, and is either a space character(s) or a tab character(s) or any mix of these. The incorrect use of whitespace is one of the most common problems encountered by beginning Code Warrior programmers. Note that some directives such as the EQU label have no white space. Labels such as "Entry:" also have no white space. Others such as ORG must have white space. Machine instructions such as "mov #ledson, portb" must also have white space. A machine instruction in Code Warrior will not work if there is no whitespace before it.
Part 1 - Create Project and Add Source Code
1. Create a new project using the ee3376 stationery, and name the project "iosub" (which will go into a folder called "iosub") where "iosub" is an acronym for "input output subroutines".
2. Click on the Target Settings icon description and choose the Linker Panel. Change the absolute file name to "iosub_hcs12.abs". Click "OK" to accept the Target Settings.
3. Open the source file named "labx_hcs12.asm". In the IDE menu, choose "File > Save As" and then name the file "iosub_hcs12.asm".
4. Copy/paste the source code below into "iosub_hcs12.asm" and Save it. Don't forget to add appropriate comments (Name, Date, etc.)
;****************************************************************
;*
;* UTEP EE3376
;* iosub_hcs12.asm
;* 20 June 2005
;*
;* Each pushbutton activates a Port B LED for a certain time:
;* sw2 lights LED2 (port B, bit 2) for 1 second
;* sw3 lights LED3 (port B, bit 3) for 5 seconds
;*
;* In the hardware, want Port H Dip Switches all set to "Off" (= logical 1)
;* Port H pusbuttons reversed from expected bit order:
;* Port H pushbutton SW2: when pushed Port H bit 3 (of 0-7) is logical 0
;* Port H pushbutton SW3: when pushed Port H bit 2 (of 0-7) is logical 0
;
;* Code Warrior based, Runs on the HCS12, assumes 24 MHz CPU clock
;* Subroutine "DELAY" parameters passed via Register D
;* Need Register D since parameters are > 8 bits
;*
;* Has Port J code for the Rev. E Dragon12 boards
;*
;****************************************************************
; export symbols
XDEF Entry ; export 'Entry' symbol
ABSENTRY Entry ; for absolute assembly: mark this as application entry point
PSEUDO_ROM EQU $1000 ; absolute address to place code/constant data
RAM EQU $2000 ; absolute address to place variables
STACK EQU $3C00 ; top of stack
portb_ddr EQU $0003 ; Port B Data Direction Register
portb EQU $0001
porth_ddr EQU $0262 ; Port H Data Direction Register
porth EQU $0260
input EQU $00 ; write to ddr to make port an input
output EQU $FF ; write to ddr to make port an output
ledsoff EQU $00 ; set all LED bits to off
ledson EQU $FF ; set all LED bits to on (lights LEDs)
sw2on EQU $08 ; sw2 of Port H tied to bit 3 of Port H
sw3on EQU $04 ; sw3 of Port H tied to bit 2 of Port H
led2on EQU $04 ; bit 2 for LED2
led3on EQU $08 ; bit 3 for LED3
msec EQU $2EE0 ; 12000 (=$2EE0) loops = 1 msec in startx loop
sec EQU $3E8 ; 1000 steps * 1msec/step = 1 sec delay
five_sec EQU $1388 ; 5000 steps = 5 sec delay
;-----------------------------------------------------
; variable/data section
ORG RAM
; Insert your data definitions here.
;------------------------------------------------------
; code section
ORG PSEUDO_ROM ;set PC to $1000
; Insert your code following the label "Entry"
Entry:
; do some initialization
lds #STACK ; initialize stack pointer
; the following two lines for Port J are needed for Rev.E boards
; it won't hurt to leave them with the Rev C and Rev D boards
movb #$FF,$026A ; make Port J output via DDR
movb #$00, $0268 ; make Port J low to enable Port B LEDs
movb #input, porth_ddr ; set Port H to input
movb #output, portb_ddr ; set Port B to output
;flash all LEDs for one second
movb #ledson, portb
ldd #sec
jsr DELAY
; our main program infinite loop
loop: movb #ledsoff, portb ; turn LEDs off
ldd #$0 ; initialize Port D
sw2: brset porth, #sw2on, sw3 ; if bit 3 Port H high, branch to sw3
movb #led2on, portb ; LED2 will light if Port H bit 3 low
ldd #sec
jsr DELAY ; delay time = (Reg D) * msec
sw3: brset porth, #sw3on, loop ; if bit 2 Port H high, branch to loop
movb #led3on, portb ; LED3 will light if Port H bit 2 low
ldd #five_sec
jsr DELAY ; delay time = (Reg D) * msec
bra loop
swi ; end the program
;------------------------------------------------------
; Subroutine DELAY - Delay = (D) * msec
; where Register D contains number of x (startx) loops
; and there is a 1 msec delay per x loop
; Add push/pull instructions to protect D, X and Y
DELAY:
pshd
pshx
pshy
tfr D,Y ; (D) -> (Y)
starty: ldx #msec ; start y loop and set x loop to 1 msec
startx: dex ; start x loop - 4 cycles per startx loop if branch
bne startx
dey
bne starty
puly
pulx
puld
rts
;****************************************************************
5. The project should be set up correctly now. Double check that the "iosub_hcs12.asm" and "burner.bbl" files are "touched" OK - if they're not, manually mark them by clicking in the touch column.
6. We're now ready to build the project and generate our executable. Click on the Make Icon description to build the program. If all goes well, it will build without error, and you will have a file named "iosub_hcs12.abs.s1" in your "iosub" project folder. This is the executable we will load in the Dragon12 and run. If there were build errors, carefully read the error messages and make corrections as necessary, and then rebuild the project. You can always call the TA over for assistance.
Part 2 - Download and Run on HCS12
1. Check that the DIP switches of SW1 (8 position red DIP switch) are in the OFF (switch open) position, pushed up toward the LCD.
2. Connect to the Dragon12 using Hyperterminal, and make sure that D-Bug12 is running OK.
4. Once your program loads in OK, type in "g 1000" at the prompt. This runs your program. The LEDs should flash on for about a half a second and then turn off. Pushing the sw2 button should light LED2 (of 0-7) for 1 second, and pushing sw3 should light LED3 for 5 seconds. Pushing the other switches sw4 and sw5 should have no visible effect. This program will run until you hit the "abort" button. To restart, just type in "g 1000" again. You can use the single-step "t" command to check short sections of code; set the PC to the address you want before executing the "t" command. You can use the "asm 1000" command to view the final form of your source and machine code.