A Walk Thru of BEGIN2.ASM: Bootup Initialization

This begins a series of walk-thru's of the code in BEGIN2.ASM. This will bring the course to a close when this is complete. I will do it in more than one lesson, because I feel that it would be a very long lesson if I didn't. We will start with the code that leads up to the operating loop, starting with bootup. You will need a printed copy of BEGIN2.ASM to follow along in these next lessons, as I will leave out some of the descriptive text that is in it, to keep these files as small as possible. You will notice, I'm sure, that the name has changed from BEGIN1.ASM to BEGIN2.ASM. You can get the updated files here. This rev adds Y2K compliance to the calendar, and adds more comments to the code. The utility SETTIME.EXE changed also, for the Y2K code update. SETTIME.EXE allows you to set the system clock/calendar from your PC. Since we don't have a keyboard, this is the only way you can set the clock/calendar, so far.

When a micro boots up after power is applied, or is reset while running, there is always code executed that sets up the system for operation. It is usually only executed once each boot or reset. It's purpose is to configure any hardware registers on board the micro and configurable hardware external to the micro, initialize any interrupt vectors, and prepare volatile ram for operation.

Since there is no way to know what will be in all the writable locations, both registers and ram, after power is applied, you must purposefully set it all up yourself so that by the time you reach the operating loop, you do know exactly what is in all these locations. It is imperative that you always start out at the operating loop with everything set up exactly the same, every time. Without this consistent starting point, diagnosing problems is next to impossible, and there would be random events that would occur due to differences in these locations.

As I've stated in an earlier lesson, when power is applied, the micro always starts executing code at location 0000, at least in this micro it does. What happens after that, depends on the code. Looking at the source file, the first thing you see is a blurb about the W command. I put it there to remind me that I spent several days trouble shooting a problem relating to it.

Next you see a .command directive. The -h0 says don't include form feeds in the listing file and the -s says don't include the symbol listing in the listing file. The symbol listing shows all the labels and variables, and the locations where they are defined and referenced. It is a valuable trouble shooting aid, but it wastes paper, if you're printing out the listing file regularly. I would suggest that you remove this switch and assemble it once to see what the symbol list looks like.

Next comes a .set directive that sets a variable, dumpx, equal to 4000h. This is used for the dumpi subroutine, that dumps the contents of all the registers in the DS5000 into data memory, so that you can use the DUMPI.EXE PC utility to display this information in a neatly laid out form. I'll explain the dumpi subroutine later on.

Next comes the .org directive, that says to the assembler to start assembling the instructions that follow, at location 0000h. Next is the first actual instruction, and the label reset. This instruction jumps past the interrupt vectors to the label cont1.

Next comes the interrupt vectors, with a .org in front of each, so that they get assembled in the proper locations. The DS5000 is "hard wired" to use these unique locations, so the ljmp instructions must be placed accurately, thus the .org in front of each one.

The first vector, at location 0003h is the interrupt 0 vector, used by the 120 Hz interrupt routine. Each time a 120 Hz interrupt occurs, this ljmp is executed, which takes you to the start of the interrupt 0 service routine, starting at the label int60.

Next comes a reti instruction at location 000bh, that basically disables the timer 0 interrupt from executing any code. It is available for use by you by placing an ljmp instruction and a label, pointing to the routine you wish to execute from it, in place of the reti instruction.  The reti stands for "return from interrupt", so you can see that the only instruction that can ever be executed as a result of a timer 0 interrupt is to return from the interrupt, which ends it, with no other instructions executed. Whatever the DS5000 was doing when the interrupt occurred, it returns to doing, by this instruction.

Next comes a couple of instructions associated with the dumpi subroutine, which is initiated by flipping the BREAK switch. The first instruction, clr ea at location 0013h says to disable all interrupts. This is done so that no interrupts occur that would corrupt the contents of the DS5000 while they are being dumped to data memory by the routine dumpi. The next instruction jumps to the label dumpi, which is the start of the dumpi routine.

Next is another reti instruction at 001bh that disables the timer 1 interrupt. Since timer 1 is used to generate the RS-232 serial baud clock, there is no code to execute. This is a feature available in all 8051 micros and we use it here. The timer 1 interrupt will never be enabled, but just in case it accidentally is, this reti instruction terminates any response to it by the DS5000.

Next is the vector at 0023h for the RS-232 serial interrupt, which jumps to the label rs232, the start of the RS-232 interrupt service routine. This routine handles all communication between the PC and the DS5000. Don't confuse this communication with the DS5000 loader, which also uses the same path for communication. The loader is invoked by flipping the LOAD/RUN switch to the LOAD position and doesn't use any vectors, or user code for that matter. This interrupt vector is used while the DS5000 is in the RUN mode, communicating with a PC via the serial port.

The next vector is the Power Fail interrupt. I disable this routine with another reti instruction at 002bh, because I don't use it in this design. The power fail interrupt is used to execute code that saves critical data or some other code in response to an eminent power failure. There is circuitry inside the DS5000 that senses that power is falling toward a point that the DS5000 can't operate. This interrupt is generated before that point arrives, giving the system a chance to do something.

Next comes two directives. The first, .segment .memory, says to define a new memory segment, or area, called memory. Up till now, all the instructions and labels were placed in the code segment, by default. Next comes the directive .memory which switches the assembler to this new segment, which will represent the 128 bytes of internal ram. Realize that these are directions for the assembler, not code to be executed. Next is a .org 00 directive that says to start the following .rs (reserve storage) directives at 00h, the first location in internal ram.

Next comes what looks like 32 labels, each with a .rs directive followed by a 1. Each says to reserve 1 byte in internal ram, with the name of the label. The .rs along with the label, is one of the niceties that go along with using an assembler. Once these .rs directives are read by the assembler, only the label has to be used in the code. In this way new variables can be assigned as needed without having to worry about where they actually are. You refer to them by name, not the location. The assembler keeps up with that boring task.  These first 32 directives assign names to the 4 register banks of the DS5000. This way data can be stored or retrieved by using direct addressing, using the label for each instead of the actual direct address in internal ram. These first 32 locations in internal ram must be reserved, though, so that they can be used as registers. The other option would have been to change the .org to the next location after the last of the register bank registers, or .org 32 or .org h'20. Remember the assembler defaults to decimal unless you specify otherwise.

Next comes some valuable descriptive text that describes the layout of the next 16 locations, which are the bit testable locations of the DS5000. Shown are the location address, the bit position, the direct bit address and the name of that bit, if it is used. This helps in getting the .equ directives correct, that follow. If you are viewing this with a program other than BR.COM or Q.EXE, this will all seem a meaningless jumble of characters. You need to be using one of these two programs to view this file, or have printed it on a printer that supports the IBM block graphics character set. The HP II and III printers call this font the PC-8 font. I have supplied both of these programs to you previously. I suggest you use BR.COM, because it doesn't allow you to change anything, just look at it. Q.EXE is the program I use to modify the source file.

Next come the actual equates that name all the bits in these 16 locations that are used. Equates name the bits by giving the name followed by the direct bit address. There can be a total of 128 of these bits, 16 locations of 8 bits each. They have direct bit addresses 00h through 7fh. Once these bits are named, only the names need be used in the code. This allows you to move these around, without having to change your code. Yet another of the advantages of using an assembler.

Next comes a .org h'21. This is the second location of bit testable locations. Following that is the label flag1 and a .rs directive, which gives a name to this location.

This covers the first 48 locations in internal ram. Next come the user variables we will use in the code. These will vary with each design, but the first 48 should always be used as they were designed, as register banks and bit locations. If you start running out of internal ram you could use some of the bit testable locations for user variables, but you loose their use as bit testable locations when you do that.

Next comes .org h'30, which is the first location after the bit locations. Then follow the user variables, which start at 30h, using .rs to name them and reserve locations for them. By the way, the .rs directives and the .equ directives can be placed anywhere in the .memory section. I keep them separated for the sake of clarity and readability. One of the changes that prompted the BEGIN2.ASM file is in the label years where I reserve 2 bytes instead of 1 for the year variable. This allows for a 4 digit year, instead of a 2 digit year, the Y2K problem in it's most basic terms.

Next comes some more valuable descriptive text describing the various pins of the DS5000 and the DS2250. Shown are the pin numbers, the direct bit addresses, the port number, and the name that is assigned to that pin. These are grouped by port number. Following each layout diagram, are the actual equates, naming the pins that are used. Notice that there is an extra line shown for the pins of port 3. This is because these pins are different from the other 3 ports in that there is a special function defined for each one. If that special function isn't needed, in the case of the RD line, for instance, this pin can be given a name with an equate, and used for whatever. The RD and WR pins would be used if we had an external ram chip connected to the DS5000. But since we don't, we can use them for whatever, in this case for the X-10 transmit and receive connections.

This ends the definition of the internal ram, the bit locations and the port pins. Notice that following the last .rs directive there is a label called stack. This will be the first location of the stack, which will consume the rest of the internal ram locations. The stack is used to temporarily store return addresses for subroutines and interrupt routines and sometimes data. When a call is made to a subroutine, the address following the call instruction is "pushed" onto the stack, taking up 2 bytes of the stack. When the subroutine ends, the address is "popped" off of the stack, returning these 2 locations, to be used again. The stack pointer, called the SP, keeps up with all this pushing and popping. All you need to do is make sure you've left enough room for the stack to "grow". How much room you actually need is determined, partly, by how "deep" you nest subroutines.

Nesting refers to calling one subroutine, that calls another, that calls another. You can see that the "deeper" you go, the more locations you will need for the stack. As you finish each subroutine, these locations will be released back to the stack. The other thing that grows the stack is how many registers you push onto the stack at the start of a subroutine. You almost always have to push two more bytes onto the stack, which are the acc and the flags. So each subroutine will probably use 4 locations in the stack, minimum. If the subroutine uses the dptr, the data pointer, which many do, that is another 2 bytes to be pushed.

The register banks come into play here, to limit the number of things that need to be pushed onto the stack. There are 4 banks of registers, each with 8 separate registers, making a total of 32 (the first 32 locations of internal ram). By using these, where you can, the number of pushes, besides the acc and flags, can be minimized. The acc and the flags nearly always need to be pushed at the start of a subroutine, since so many instructions use the acc and affect the flags. When you return from the subroutine, or interrupt routine, you want the flags, and possibly the acc, to be what they were before the call or interrupt happened. This allows you to use the acc and flags in the subroutine, but return with them intact, like you never used them at all. You'll see this more as we go through the subroutines and interrupt routines later.

The register banks can be switched with a single instruction. I use this feature in the interrupt routines. The other reason that you need to push the flags is that it contains, among other things, the register bank that was in use before you switched them in the routine. So when you pop the flags back, just before you return from the interrupt, or subroutine, the previous bank will be restored and things will move along, without having to push 8 registers, saving stack space.

All the equates and reserves defined above, will be explained further as we get into the code that follows.

Next you will see the .code directive, that tells the assembler that we are now switching back to the code segment, and all that follows will be assembled, starting at 0030h, into the program memory. Next is the label rxltb. This is the start of a table that is used by the RS-232 interrupt routine, to tell this routine how many bytes to expect from the PC, for various commands, one of which is actually used, the SETTIME command, which sets the clock/calendar. Listed here are .db directives that have a value, that is the length of the command, in bytes. Also, in brackets, for your benefit, is the command byte, in this case an 80h. This will be explained further in the RS-232 interrupt service routine description.

Next comes another .org followed by the label cont1, the label that was in the very first instruction we assembled in the beginning of this LONG dissertation. This is where we jumped to and where we will continue with the initialization code. This jump took us past the last interrupt vectors and the table above, and is the actual start of the initialization code.

First the watch dog timer (WDT) is disabled with a timed access to the pcon (Power Control) register. This is done by first writing an aah and then a 55h to the ta (Timed Access) register (c7h) and then writing a 00h to pcon. This also sets SMOD=0 and disables the power fail interrupt. The WDT is a hardware device built into the DS5000 that can be used to monitor the operation of the DS5000 software. It consists of a timer that must be reset on a regular basis (approx. every 132 ms at 11.0592 MHz) by software, to keep it from resetting the DS5000. This keeps the DS5000 from getting lost, although this shouldn't happen if your software is sound. It is explained in detail in the Soft Microcontroller Data book from Dallas Semiconductor, starting on page 67. I don't use it in this design, but it should be included in any critical design.

Next we disable all interrupts. This is in case we accidentally got here. A reset or power-on will do the same thing. There is some valuable descriptive text about the ie (Interrupt Enable) register here. We load it with a 00h, which disables all interrupts

Next we initialize the txx and rxx pins, which are X-10 transmit and receive pins of the DS5000, respectively. These are set to a 1. For the txx pin, this is now sending a 0 to the TW523, which does nothing. Remember the TW523 signals are inverted so a 1 is really a 0, which puts silence on the power line. By setting the rxx pin to a 1, this allows this pin to be an input. In the DS5000 there is no way to say how a pin will be used, whether input or output. Inside the DS5000 is a very weak pull-up, that can be overcome quite easily by an outside source. With the pin set to a 1, the receive output of the TW523 can drive this line low, or leave it high. If there had been a 0 written to it, and the outside source tried to drive it high, damage could result to the DS5000.

Next comes the initialization of the other port lines. P0 is the tristate data bus that is connected to the display and the A/D chip. Sometimes it is an output and other times it is an input. Writing all 1's sets it up to be an input. P1 has the various pins used by the display and the A/D for controlling who has the tristate bus at any one time. Writing a 10h initializes all these lines to their proper state. Some need to be 1's and some need to be 0's. More on that later.

Next comes a routine that clears all the internal data memory. This is done to always start with a known state of the internal ram. This routine is 6 instructions long. The first sets the starting address for the clear, contained in r0, to location 02h, or the third location. Were are using the first two locations as part of the routine, so they can't be cleared with the routine. R1 holds the count of how many locations we are going to clear. This is set to h'fe or 126d, which is two less than the total of 128. The next instruction puts a zero in the acc, which will be used to write zeros in all the other locations. Next we write the contents of the acc (zero) to the address contained in r0 (initially a 02h, the third location). Notice that there is a label on this line, clrm2. Next r0 is incremented, then r1 is decremented, and a jump made to clrm2 if r1 is not zero. When r1 is zero the program falls through the djnz instruction.

Next the stack pointer is set to the address of the label stack -1. The -1 is due to the mechanics of the stack pointer. When you push something onto the stack, the first thing that happens is that the stack pointer is incremented before the push is done. So, in this case, the stack pointer will now be pointing to the address of the label stack and the byte will be stored at that location. When you pop something off of the stack, the byte is read from the location pointed to by the stack pointer, and then the stack pointer is decremented. The main thing about the stack pointer and pushing and popping is that there is enough room in the stack and that there are the same number of pushes and pops in your code (or calls or interrupts and returns). One of the hardest problems to troubleshoot is a mismatch in the number of pushes and pops, or the order you did the pushes verses the order you did the pops.

Next the clock/calendar is set to a time/date of 23:59:00, Feb 28,1998. This is an arbitrary time/date that could be anything. The thing to notice is that there are 8 bytes that make up the clock/calendar. These are secnd (not shown here, but cleared by the above routine), minut, hours, daymo (day of the month), month, and two bytes for years. There is also a daywk (day of the week) that is also zeroed by the above routine. This means that the calendar is set to Sun, Feb 28, 1998, since Sun is a day of the week of 0, and Sat is day 6. SETTIME.EXE sets all 8 bytes from your PC. From that point on, the calendar is perpetual as long as power is applied, to include leap years.

Next all 16K of the data memory is set to ffh. This routine is 8 instructions that use the dptr as the data pointer, r2 and r3 together as the byte counter, and the acc holds the ffh to be written to each location. The dptr is set to 4000h, which is the first location of data memory and the byte counter is set to 4000h with r2 being the most significant byte and r3 the least significant byte. Then acc is loaded with ffh and then the first byte is written with the next instruction. The dptr is then incremented and the byte counter is decremented, and when both r2 and r3 are zero, the program falls through the last djnz instruction, otherwise a jump is made to the label nextx, and the next location is written with ffh.

Next the display buffer is filled with spaces. The display buffer is 128 bytes long, 4 lines of 32 characters, even though only 20 of the 32 characters per line actually get displayed. This is done to simplify the software that updates the display during the 120 Hz interrupt routine. First the dptr is loaded with 4100h, the start of the display buffer. R2, the byte counter is loaded with 80h (128d). The rest is much like the other two previous fill routines.

Next the display buffer has a comma for the date, and two colons for the time written in the right places. This way, when the date/time is written to the display, these three characters don't have to be sent each time.

Next the 8 location A/D buffer, that starts at loc 4180h in data memory, is zeroed out.

Next flag1 is set to 3fh. This causes the operating loop to update the display buffer with the date/time, the first time it runs. After that, the display buffer is updated with the seconds, each time they change, minutes each time they change, and so forth.

Next more of the special function registers within the DS5000 are setup.

First tmod (Timer/Counter Mode Register) is set to 22h which sets both hardware counter/timers to be 8 bit auto reload timers (Mode 2). Remember timer 1 is used as the baud rate clock for the serial port. Timer 0 isn't used yet in this design.

Next tcon (Timer Control/External Interrupt Control Register) is set to 05h which clears the timer overflow interrupts, stops the timers and sets int 0 and int 1 to be negative edge triggered inputs. Negative edge means that the interrupt is generated by a high to low transition on the interrupt input pins (int 0 and int 1) of the DS5000.

Next scon (Serial Control Register) is set to 50h. This sets the serial port to mode 1, which is 10 bits asynchronous (1 start bit, 8 data bits, and 1 stop bit) and uses timer 1 overflow as the baud rate clock. This also enables the receiver and clears both ti (transmit buffer empty) and ri (receive buffer full) interrupts, in case they might be set.

Next is some valuable descriptive text and a table that describes what to load into TH1 to get different baud rates for the serial port. Part of the table is for SMOD=0 and SMOD=1. Since we set SMOD=0 previously in the program, we are interested in the part of the table with SMOD=0. I use a 11.0592 MHz clock in this design, so that is the column we want to look at. A value of f4h gives us a baud rate of 2400. So I set TH1 and TL1 both equal to f4h. We will be operating the serial port at 2400 baud, while in the RUN mode. This is fast enough for a reasonable speed in transfers between the PC and the DS5000 and leaves plenty of time between serial interrupts to do lots of stuff, including reading the receive serial buffer before it overruns (the next character comes in before you get around to reading the previous one). I always start at 2400 baud and get everything working. You can then speed up the serial port, if you want, to see how fast you can operate without problems. It will probably operate at 9600 baud in most designs without problems. But it depends on how much stuff you are trying to do with your design.

Next the ip (Interrupt Priority) register is loaded with a 00h, which makes all interrupts the same priority, low. This means that while an interrupt is being serviced, no other interrupts will be honored. If you want one particular interrupt to be able to interrupt another interrupt service routine (ISR), you can set that interrupt to have a high priority, by setting that bit to 1. The rule is that no high priority interrupt can interrupt another high priority ISR and no low priority interrupt can interrupt another low priority ISR. But a high priority interrupt can interrupt a low priority ISR. If a high and a low happen at the same time, the high will be honored first. Generally a high priority interrupt will be one that has to be serviced within a short time of it's occurrence.

In this design, a problem could arise if you are using X-10 control, which has a very strict timing requirement, and at the same time you are communicating with the PC, with lots of data. The problem would be if you are transmitting or receiving X-10 data and receiving serial data from the PC. At 2400 baud, a continuous stream of data would cause an interrupt every 4.16 ms. The 120Hz interrupt is occurring every 8.33 ms. If a serial character came in just before the 120 Hz interrupt happened, the 120 Hz would have to wait for the serial ISR to finish, before it was honored. If the serial ISR lasted 1 ms, which is quite a while, you would have missed the 50 usec window, and if you are transmitting X-10, the data just got garbled, and that transmission would be ignored by all X-10 devices.

Since the 120 Hz ISR only lasts about 2 ms, if you experience many missed X-10 messages, you might try setting the 120 Hz interrupt to a high priority, which shouldn't bother the serial communications as long as you're not trying to run at too high a speed. You could also add to that, lowering the speed of the serial port to 1200 baud. You will have to do some timing calculations of your software to determine the best course. But beware of high speed serial data, coupled with concurrent X-10 communications!

Next ie (Interrupt Enable Register) is loaded with 00h, which disables all interrupts, in case they weren't already. Either a power on or reset will disable all interrupts, among other things.

Then timer 1, the baud rate clock is started by setting the bit tr1 in tcon.

Next both external interrupts are enabled by setting bits ex0 and ex1 in ie. This doesn't really enable anything yet, since the ea bit hasn't been set yet.

Now the first subroutine is called, that initializes the display. (See the table that describes the HD44780 instructions). This sub (inlcd) goes through a sequence of steps, timed out, that are required to initialize the HD44780 display driver chip on-board the display. It uses three other subs waitp, strob, and wclcd. We will look at these later, when I cover the subs.

Next the number 103 is placed in the display buffer. This is just something I put there to tell me the rev level of the software. Since you can't look inside the DS5000 and see what's there, this is the next best thing. You can use whatever number, or none, that you wish.

Next we enable the serial interrupt, es, though we haven't enabled ea yet, so nothing is really enabled.

Finally ea is set, which does enable the two external interrupts and the serial interrupt, all at once. Since we set the bits in flag1, the display will shortly show the data/time, the number 103, and lots more.

We have just finished the initialization process!!!!! Next we will look at the operating loop. Hope everyone enjoyed this, ha! ha!

My home page is http://www.hkrmicrop.com/personal/index.html .

On to lesson 18.

Table of Contents