Introduction

( 1 )
Next (Table Of Contents)

8051 Simulator

by John Kopplin

Copyright © 2002-05

Welcome to 8051.EXE, a Windows program that simulates Intel's venerable 8051 microprocessor (uP), which is employed in more devices than any other microprocessor. I created this simulator to teach programming concepts to beginners.

This 8051 simulator is the second section of my curriculum as I start my students with a programmable RPN calculator that offers a more gentle introduction to programming. I use the RPN calculator to demonstrate fundamental programming concepts such as program memory, data memory, looping, single-stepping, labels, subroutines, etc. Then we move on to this 8051 environment to learn about microprocessors and assembly language. I believe that it is best to have this background before you tackle a modern high-level language such as C, C++, or Java. Otherwise, many of the features of the high-level language (principally pointers, dynamic memory allocation, and casting) will be "explained" to you with hand-waving that I consider too superficial. Once you complete this section of the curriculum you will be ready to move on to CPPIDE which offers an integrated development environment for writing Windows programs in the C++ language.

The help page that you are now viewing is merely the first of an extensive set of help pages that together represent a self-study guide that should allow you to learn to program on your own, at your own pace. To use these help pages as a self-study guide simply read them in the order they are listed on the Table Of Contents page. There is a hyperlink at the top and bottom of each page labeled "Next" which will take you to the next page in the order I intended. You will also see a page number in the upper right hand corner of each page to help you keep track of your progress. Of course I have also provided a keyword index and lots of other hyperlinks to help you navigate directly to particular topics. You can leave this help page window open while you interact with the 8051 simulator (and you probably want to make it bigger than its default size).

You have probably seen a Help system like this one already. It is built upon Microsoft's standard HTML Help technology. In the left-hand pane you can observe 4 tabs labeled Contents, Index, Search, and Favorites. The Contents tab simulates a book's table of contents while the Index tab simulates a book's index. The Search tab offers the feature that books would kill for. The Favorites tab is where you can record bookmarks on your favorite pages. These bookmarks make it easy to return to these spots later. One good use for a bookmark is to record the spot where you break from your studies so you can resume there later.

The home page for all of my software is www.computersciencelab.com For questions, you can reach me at support@computersciencelab.com. Enjoy!

Next (Table Of Contents)


( 2 )
Previous Table Of Contents Next

Table Of Contents

Introduction

Table of Contents (this page)

History of Microprocessors

The Birth of the Microprocessor

History of Intel's x86 uP Family

History of Intel's 8051 uP Family

Programming a Microprocessor

A Little Electronics

Machine Language

Assembly Language

Addressing Modes

Binary and Hexadecimal Numbers

Programming the 8051

8051 Architecture

Data Memory

Special Function Registers

8051 Addressing Modes

Generic Instruction Forms

Compose Mode of the Simulator

The IncrementForever.txt Example Program

Execute Mode of the Simulator

The IncrementForever2.txt Example Program

The Train1.txt Example Program

The Diamond.txt Example Program

The ContinuousDiamonds.txt Example Program

The SpacedDiamonds.txt Example Program

The WalkingTriangle.txt Example Program

The Train2.txt Example Program

The Alphabet.txt Example Program

The Factorial.txt Example Program

The FillMemory.txt Example Program

The FillMemory2.txt Example Program

The FillMemory3.txt Example Program

The YourName.txt Example Program

Subroutines

The YourName2.txt Example Program

The YourName3.txt Example Program

Interrupts

The SerialPort.txt Example Program

The SerialPort2.txt Example Program

The SerialPort3.txt Example Program

The MazeSolver.txt Example Program

Bit Addressable Bits

The MazeSolver2.txt Example Program

Standard Operations

Signed and Unsigned Numbers

The PeakDetect.txt Example Program

Internal Timers

The TimerTest.txt Example Program

The InterruptTest.txt Example Program

The Full 8051 Instruction Set

The Example Programs

The Windows Simulator

Simulator Options

About the Simulator


Birth of the Microprocessor

( 3 )
Previous Table Of Contents Next

The Birth of the Microprocessor

A microprocessor (uP) is a computer that is fabricated on an integrated circuit. An integrated circuit (I.C.) is a sliver of silicon about the size of the fingernail on your pinkie onto which have been fabricated millions of transistors. A transistor is an electronic component like a resistor or capacitor except that it can be employed as an electronically controlled switch.

An integrated circuit or computer chip [photo courtesy of IBM]

The very first digital computers were constructed using relays or vacuum tubes which also act as electronically controlled switches. Tens of thousands of these relays were required for even the most primitive computers and each of these relays was a few inches in size and required a handful of wire connections. Each of these wires had to be hand soldered (connected) and hence the first computers were extremely expensive to build and as large as rooms.

ILLIAC II at the Univ. of Illinois at Champaign-Urbana, the model for Hal in the movie "2001: A Space Odyssey"

Close-up of the hand-wiring necessary in a mainframe computer [photo courtesy "The Computer Museum"]

The primary advantage of an integrated circuit is not that the transistors (switches) are miniscule (that's the secondary advantage), but rather that millions of transistors can be created and interconnected in a mass-production process. All the elements on the integrated circuit are fabricated simultaneously via a small number (maybe 12) of optical masks that define the geometry of each layer. This speeds up the process of fabricating the computer -- and hence reduces its cost -- just as Gutenberg's printing press sped up the fabrication of books and thereby made them affordable to all.

Computers had been around for about 20 years before the first microprocessor was developed (by Intel). The "micro" in the name microprocessor refers to the physical size. The invention of the computer on a chip meant that a name was now needed for the original type of computer, and they are now called mainframes. Large mainframe computers are still built today (principally by IBM) but even they now employ microprocessors internally. The fact that a mainframe computer costs a million dollars while a microprocessor costs $1 explains why microprocessors outsell mainframes by a ratio of at least a million to one.

An IBM mainframe from the 1960s [photo courtesy of IBM]

Previous Table Of Contents Next


History of the x86

( 4 )
Previous Table Of Contents Next

History of Intel's x86 uP Family

Intel didn't invent the electronic computer. But they were the first to succeed in cramming an entire computer on a single chip (an integrated circuit). Intel Corporation was started in 1968 and initially produced only semiconductor memory. Intel invented both the DRAM and the EPROM, two memory technologies that are still going strong today. In 1969 they were approached by Busicom Corp., a Japanese manufacturer of high performance calculators. These were typewriter sized calculators (the first shirt-pocket sized scientific calculator was the Hewlett-Packard HP-35 introduced in 1972). Busicom wanted Intel to produce 12 custom calculator chips: one chip which would run the keyboard, another chip dedicated to the display, another for the printer, etc. But integrated circuits were (and are) expensive to design and this approach would have required Busicom to bear the full expense of creating 12 new chips.

A typical Busicom desktop calculator

But a new Intel employee (Ted Hoff) convinced Busicom to instead fund development of a general purpose computer chip which, like all computers, could be reprogrammed for many different tasks. Computers are adapted to each new purpose by writing a program, which is a sequence of instructions stored in memory (now there's a clever tie-in: remember that memory was Intel's forte). The re-programmability of a general purpose computer meant that it was truly a "one size fits all" device.

Busicom became convinced that this was a superior approach. And Intel became convinced that they could sell the same device to others who would adapt it to their needs merely by changing its program. So Intel refunded Busicom's $60,000 in order that Intel would have the right to sell the chip on the open market. Thus became the Intel 4004, the first microprocessor (uP). Intel started selling these chips in 1971 for $200 each. One of them still functions aboard Pioneer 10, which is now the man-made object farthest from the earth. Curiously, Busicom went bankrupt and never ended up using the ground-breaking microprocessor. But by then Intel was far enough along to know they had a winner and finished the chip on their own.

A packaged Intel microprocessor [photo courtesy Intel]

The Intel 4004 was a 4 bit machine meaning that the unit of work was 4 binary digits. Because 4 binary digits can only express 16 different values, many program steps were required to accomplish even the most basic operations. [We will study binary numbers in a later section.] The 4004 consisted of 2300 transistors and was clocked at 108 KHz (i.e., 108,000 times per second). Compare this to the 42 million transistors and the 2 GHz clock rate (i.e., 2,000,000,000 times per second) used in a Pentium 4.

Intel followed the 4004 with the 8008 and 8080 which both employed an 8 bit data word size. 8 bits can express 256 possible values so each operation was quite a bit more powerful than the operations of a 4 bit uP. The 8080 was employed in the MITS Altair computer, which -- in 1975 -- was the world's first personal computer (PC). It was personal all right: you had to build it yourself from a kit of parts that arrived in the mail.

The Altair kit computer

The Altair kit computer convinced a Harvard freshman by the name of Bill Gates to drop out of college so he could concentrate all his time writing programs for the Altair. This early experience put Bill Gates in the right place at the right time once IBM decided to standardize on the Intel uPs (microprocessors) for their line of PCs (personal computers) in 1981. The Intel Pentium 4 used in today's PCs is still compatible with the Intel 8088 used in IBM's first PC.

The first IBM PC

The family of uPs employed in PCs is known as Intel's "x86" family. The major generations of this family are known as the 8086, 80186, 80286, 80386, 80486, Pentium, Pentium Pro, Pentium II, Pentium III, and Pentium 4.


History of the 8051

( 5 )
Previous Table Of Contents Next

History of Intel's 8051 uP Family

The family of uPs (microprocessors) employed in PCs (personal computers) is known as Intel's x86 family. But Intel had another family of uPs intended for embedded applications. An embedded application refers to a situation where a computer is used in a product whose outward appearance gives little or no indication that it contains a computer. For example, a traffic stoplight has no attached keyboard or monitor, yet employs a microprocessor to count off the seconds for its cycle of lights. A car's anti-lock braking system requires a computer to constantly monitor all four wheels for signs of slippage. But this computer has no floppy drive, no hard drive, and no mouse.

In an embedded application, a computer powers up ready to perform a single job and then continues to perform that one job until it is powered down. Hence there is no need for a user interface (UI) that would allow a human being to direct its activities. To this day, Intel's 8051 family of microprocessors has been shipped in more embedded applications than any other uP.

If you round off the fractions, embedded systems consume 100% of the worldwide production of microprocessors. That is, all the microprocessors intended for use in desktop computers (the largest group of which are Intel's x86 family) account for less than 1/2 % of microprocessor shipments by numbers (but they account for 15% of microprocessor shipments by dollars).

Where do all these microprocessors go? In your home you can find a uP in your cell phone, microwave oven, washer, dryer, dishwasher, coffee maker, refrigerator, VCR, television, DVD player, video-game console, stereo receiver, CD Audio player, portable Discman, in each of your remote controls, your garage-door opener, automatic sprinkler timer, fax machine, PDA, answering machine, pager, etc. Furthermore, the average 2000 model year automobile incorporated 15 uPs. The 2000 BMW 7 Series led the way with 65 microprocessors. Where are all those car microprocessors? Well, there is 1 microprocessor behind each of the headlights in all recent Lexus, Mercedes, and BMW cars. As many as 8 microprocessors might be involved with the car's air bags.

So why did I choose to teach the 8051? It is employed in about half of the devices containing an embedded uP. Hence, it is by far the most widespread uP architecture. Even though the 8051 was first introduced in 1980, yearly sales are over $1 billion per year. Since its 1980 introduction, the 8051 and its derivatives have shipped over three billion units world-wide and the production rate is still growing, which is very unusual since sales of an integrated circuit usually start to fall off after a couple of years.

And these sales and longevity records were achieved with essentially no changes to the core architecture. Intel's x86 family, by comparison, has undergone three major architecture enhancements and six major redesigns during these same years.

It is a curious fact of the semiconductor industry that Intel no longer bothers to manufacture this device (they can make more money on other products). But plenty of other semiconductor companies still manufacture versions of this chip.

The 8051 offers 64K (i.e., 64 * 1024) bytes of program memory address space and another 64K bytes of data memory address space. The address space is the range of addresses that a computer can accommodate. The 8051 employs a 16 bit value for each program memory address and since 16 bits can describe 64K different values, the program address space is 64K bytes wide. Similarly, the 8051 employs a 16 bit value for each data memory address. Each of these 64K program memory and data memory locations is a byte wide (that is, holds 1 byte). This makes the 8051 a byte oriented machine (in contrast, the members of Intel's x86 family all gobble 4 bytes at a time).

The address space sets an upper limit on the amount of memory, since there is no point in providing storage that can't be addressed. But each application is free to use less memory to save cost. The most common flavor of 8051 chip (and the flavor that this Windows simulator assumes) offers only 4K bytes of program memory and 256 bytes of data memory. This is still sufficient for applications such as stoplights, electronic signboards, bathroom scales, etc. The contents of the 8051's program memory can only be read, not written to. Data memory can be both read and written under program control.


( 6 )
Previous Table Of Contents Next

A Little Electronics

Like any other computer, a microprocessor executes a stored program. While the program is being executed, its only manifestation inside the computer is as a pattern of voltage levels. Only two voltage levels are employed: a high voltage approximately equal to the power supply voltage and a low voltage approximately equal to the ground potential.

Ground potential is the potential of earth; that is, soil. Ground potential can be fabricated simply by sticking a metal pipe in the ground as you do to make a lightning rod. Modern houses (those having electrical outlets with 3 rather than 2 prongs) are grounded (or "earthed", as they say in Britain).

You might guess that the high voltage used for a computer would be something life threatening in order to explain the incredible number crunching capability of these machines. But actually, the high voltage level used in a Pentium 4 chip is less than 3 volts, and hence is less than the voltage produced by 2 flashlight batteries.

So when a computer is being put through its paces, you can visualize that the work is happening because a flashlight battery keeps flipping its polarity. That such a small voltage is sufficient is rather surprising. When your phone rings the wire coming out the back of it cycles over a range of 50 volts. And the reason your television or computer monitor takes a few seconds to produce a visible image is because it must first generate a potential of a few tens of thousands of volts (safety hint: don't open a television or a computer monitor).

Only two voltage levels are employed because an on/off switch can quickly produce two voltages: when the switch is closed the voltage goes to one level and when the switch is open it goes to the other (I am being ambiguous because some computers are designed such that a closed switch generates a high voltage while in other computers a closed switch generates a low voltage). A modern integrated circuit can hold millions of transistors and each transistor behaves like a very fast switch, modulating a voltage between two levels. The fact that computers are based upon devices having only two states explains why binary numbers are the best representation for a computer program. The digits (bits) of a binary number can have only two values: 0 or 1.


( 7 )
Previous Table Of Contents Next

Machine Language

While a program is being executed, its only manifestation inside the computer is as a stored pattern of voltage levels. Since voltage levels (at least those of the size produced by flashlight batteries) are not directly perceivable by humans, we need an alternate representation for computer programs. This alternate representation needs only 2 symbols to correspond to a high and a low voltage which in turn correspond to an open and a closed switch. Machine language is the name given to a computer program written using a binary alphabet having only the two letters 0 and 1 which correspond to the low and high voltage respectively. Hence a computer program in machine language consisting of four operations might look like:

    01101110
    10111100
    00000100
    11001111

This is not very illuminating, to say the least. After inspecting this program we can hazard a guess that it is intended for a computer whose natural word size is 8 bits (like the 8051). A bit is a quantity that can have only one of two possible states: either a 0 or a 1. This is similar to the digits humans use to write numbers. Digits can have the ten possible values 0 through 9 and are used to write decimal (i.e., base 10) numbers. Bits are used to write binary (i.e. base 2) numbers (we will study binary numbers in a later section).

Modern Pentium microprocessors are all 32 bit machines and hence a program consisting of 4 operations expressed in machine language for a Pentium chip might look like:

    10010001110110011010111000111011
    00010101111010010001010111011111
    00101000000110000011101001011100
    11110001001101111111100001010100

Yes, there was a time when human beings assembled computer programs by working with this type of notation. Thankfully those days are past.

There was an obvious need for a "higher level" (i.e., more abstract) computer programming language that looked more like a natural (i.e., human) language. This need was filled by assembly language which is still in common use today.


( 8 )
Previous Table Of Contents Next

Assembly Language

An assembler is a computer program that functions as a text processor to convert an assembly language program to the form (machine language) that can be executed by a particular computer. Since each computer has a unique architecture, you must employ an assembler appropriate to the computer you are targeting.

Assembly language programs consist of three kinds of statements:

  • assembly language instructions specific to the particular microprocessor,
  • assembler directives which are commands to the assembler rather than to the microprocessor,
  • comment (and blank) lines

The first of these three types of assembly language statements, the assembly language instructions, are translated by the assembler into the equivalent machine language instructions. The programmer produces a source file holding his assembly language program and the assembler then produce the listing file that shows the corresponding machine language instructions. We will see an example of a source file and a listing file below.

Assembly language statements consist of four ordered fields:

[label:] mnemonic [operand list] [;comment]

Square brackets are used to show optional fields. Hence the label field does not need to be present but if it is present it must be the first field and must terminate with a colon. Similarly, the comment field is not required but if it is present then it must start with a semicolon. The only required field is the instruction mnemonic which is so called because it serves to remind the programmer of the intended machine operation. Examples of the mnemonics used for the 8051 are MOV for "move operation" and ADDC for "add with carry". A complete list of all 8051 mnemonics is provided.

Many instructions require operands and these are placed -- separated by commas -- in the operand list. An operand can be a constant (such as "7") or a symbol (such as "A" standing for the accumulator) or a combination of constants and symbols. Constants can be expressed in decimal (the default) or in hexadecimal, signaled by a "0x" prefix (e.g., 0xF70A). Note that "0x" is "zero X" not "oh X". We will study binary and hexadecimal numbers in an upcoming section.

Assembly language allows free-form meaning that blanks (space characters) can be used freely to separate the four fields and the comma separated operands within the operand list. This is done to improve readability. Most modern programming languages are free-form but some very old computer languages (you may have heard of FORTRAN) require that the fields start in pre-assigned columns. Each assembly language statement must reside on a separate line of the source file and each such source line should be no more than 80 characters long.

Assembler directives are commands to the assembler itself and hence do not result in machine language statements. An example of an assembler directive is the END statement which simply causes the assembler to quit processing the source file and ignore anything below the END statement.

Comment lines are ignored by the assembler. They exist to allow the programmer to embed notes in his program to explain the purpose of its various sections.

You will get comfortable with the four fields of an assembly language statement as you study the example programs that are distributed with this simulator. I will let you know when it's time to dive into the first example program.

An example of a short assembly language program is shown below:

-------- Start of the Assembly Language Program ------------

; Here is a short Assembly Language program for an 8051 uP.
; Anything after a semicolon (such as this text) is a
; comment and is ignored by the assembler.  Totally blank lines
; are also allowed as shown below.

; To this point you have only seen comment and blank lines.
; Below are two assembly language instructions, the first of
; which starts with a label and both of which have comment
; fields:

Start:  MOV   A,#7  ; place the number 7 into the accumulator
        INC   A     ; increment the number in the accumulator

----------- End of Assembly Language Program ----------------

Given the above program (called the source file), an 8051 assembler will produce the following listing file:

--------------- Start of the Listing File -------------------

LOC  OBJ    LINE SOURCE
               1 ; Here is a short Assembly Language program for an 8051 uP.
               2 ; Anything after a semicolon (such as this text) is a
               3 ; comment and is ignored by the assembler. Totally blank lines
               4 ; are also allowed as shown below.
               5
               6 ; To this point you have only seen comment and blank lines.
               7 ; Below are two assembly language instructions, the first of
               8 ; which starts with a label and both of which have comment
               9 ; fields:
              10
0000 7407     11 Start:  MOV A,#7 ; place the number 7 into the accumulator
0002 04       12         INC A    ; increment the number in the accumulator
              13

SYMBOL TABLE

symbol            value         defining filename and line #
----------------  ------  -----------------------------------
           Start  0x0000  D:\johnk\projects\8051\junk.txt  11

# of assembly errors   = 0
# of assembly warnings = 0

----------------  End of the Listing File -------------------

Remember: the programmer creates the source file and a computer program called the assembler creates the listing file from the source file.

Most of the listing file is just for the benefit of the programmer and very little of it shows the machine language that will be loaded into the microprocessor.

At the bottom of the listing file we can see that our 2 statement program generated 0 errors and 0 warnings, meaning that it was a good program and we could execute it on an 8051 uP. This Windows simulator offers two main operating modes: a Compose Mode where you can construct your program and get it to assemble without errors and an Execute Mode where you can run your program on a simulation of an 8051 microprocessor. You can't enter the Execute Mode until your source file assembles without errors. While you remain in Compose Mode you can select to view either your source file or the listing file.

Just above the error summary in the listing file we can see the symbol table which lists all of the symbols that were defined in the program. This particular program defines only one symbol: "Start". You get to choose these names, which are called symbols or labels. You don't get to choose the names for the mnemonics. The mnemonics seen in this example program are MOV and INC.

The portion of the listing file that corresponds to the machine language to be executed by the 8051 uP resides in the column labeled OBJ (for object code). The entries in this column are hex numbers even though the 0x prefix is not shown (this was done to save space). We see that the machine language for this two statement program consists of three bytes, shown below in both binary and hex (the 0x prefix indicates a hex number):

  01110100  =  0x74
  00000111  =  0x07
  00000100  =  0x04

Surprised to see that a two statement 8051 program has resulted in three words of machine language? This occurs because the "MOV A,#7" assembly language statement requires 2 words of machine language: the first word (0x74) signals the MOV operation and the second word (0x07) holds the number to be moved into the accumulator. These two bytes, 0x74 and 0x07, are shown on the same line of the listing file because they both originate from the "MOV A,#7" instruction.

The column of the listing file labeled LOC shows the program counter (PC). The way that any microprocessor executes a program is it employs a counter (the PC) that marches through the entries in its program memory. Generally, this counter is initialized to 0 when power is first applied to the microprocessor. The microprocessor then continues to repeat the following sequence of steps for as long as it remains powered up:

  • Fetch an opcode from that location of program memory pointed to by the PC. An opcode (operation code) is a coded representation of an activity. We have already seen that the opcode for the 8051's MOV operation is 0x74.
  • Perform the operation requested by the latest opcode.
  • Increment the program counter to point to the next instruction in program memory.
  • Repeat.

You will be able to watch the program counter increment while this Windows simulator runs the 8051 programs that you write. In fact, you will be able to see all of the 8051's internal resources such as its program memory, data memory, accumulator, timers, etc.

Each computer programming language has lots of rules regarding the syntax of the statements that you can write. Below are the most important rules for assembly language.

Labels can be any desired length but can only contain lower and upper-case alphabet letters and the ten number digits (0-9) and the underscore punctuation symbol ( _ ). Even though labels can include the ten number digits, they cannot begin with a number digit. Hence they must start with a letter or an underscore.

The reason for this rule is that the assembler believes that anything that starts with a number digit is a number. This is the reason the prefix signaling a hexadecimal number is 0x (zero X) rather than Ox (oh X) or merely x. Note that you do see hex numbers without the 0x prefix in the LOC and OBJ columns of a listing file.

A label on a line by itself is a valid statement: its purpose is to associate that label with that location in the program. Labels are case sensitive which means that the two labels "Start" and "START" are unique and can both exist within the same program.

The instruction mnemonic field and operand field are NOT case sensitive. Hence, the instructions "ADD A,#7" and "add a,#7" are identical.

This assembler has predefined labels for all the special function registers such as IE and TCON and for all the bit addressable bits such as C and OV.

The dollar sign '$' is used as a special symbol to denote the current value of the program counter. You will generally see it used in statements such as the following which purposefully accomplishes an infinite loop:

        SJMP  $

In English, this last statement reads as "Short jump to yourself". This is an infinite loop because it offers no chance for the program to ever go anywhere else.

For another example of the use of the dollar sign symbol, assume that data memory address 5 holds the value 3. Then the following statement will cause the 8051 to hang up on this statement for 3 execution time periods.

        DJNZ 5,$

In English, this last statement reads as "Decrement the contents of data memory address 5 and jump to yourself if this content is not yet zero". So after this DJNZ statement is executed the first time, the contents of data memory address 5 will be decremented to 2. The same DJNZ statement will be executed again with the consequence that dma ("data memory address") 5 gets decremented to 1. The same DJNZ statement executes one more time resulting in dma 5 going to 0. Since dma 5 now holds the value 0, this DJNZ statement will not jump to itself after this third execution. Instead the program will continue with the next statement. This type of code is used to create short time delays.

An assembler directive is a command to the assembler and hence doesn't produce an operation that will be executed by the microprocessor. Standard assembler directives are:

ORG | "origin", presets the program counter to a desired value
EQU | "equate", defines a symbol that "stands in" for a constant value
INCLUDE | "include", incorporates another file into the current file
DB | "define byte", places an arbitrary byte into the program
DW | "define word", places an arbitrary word (two bytes) into the program
END | "end", halts assembly

These are the only assembler directives supported by this Windows 8051 simulator. These assembler directives can be seen in action in the example programs distributed with this simulator.

Just like the mnemonics, assembler directives are not case sensitive and can be written as either "EQU" or "equ".


( 9 )
Previous Table Of Contents Next

Addressing Modes

The architecture of each microprocessor provides the programmer a small number of alternatives called addressing modes. The RPN calculator that we previously studied offers the programmer the choice of only two addressing modes: direct and indirect addressing. In the RPN calculator an instruction that employed direct addressing to recall a value from memory location 5 looked like "RCL 5" while an instruction that employed indirect addressing looked like "RCL i". The 8051 microprocessor offers four choices of addressing modes. The more modern Intel Pentium III microprocessor offers nine addressing modes.

You choose an addressing mode for every operation when you program in a low level language such as assembly. The compiler (a replacement for the assembler) selects an addressing mode to accomplish each of the operations you request when you program in a high level language such as C, C++, or Java. As an example, let's consider the situation where we need to increment the value held in data memory address 5. This type of situation calls for use of direct addressing since we know the specific data memory location we want to affect. In 8051 assembly language we could write:

  INC 5  ; increment the contents of data memory addr 5

In a high-level language the programmer refers to all data memory locations by symbolic names of his own choosing. As an example, assume that data memory address 5 was associated with the symbol "Sum". In this case, to increment Sum in a high-level language the programmer would write:

    Sum = Sum + 1

meaning "the data memory location indicated by Sum should be populated with 1 larger than the current value of this same data memory location". Note that the choice of the addressing mode is left up to the compiler. Without taking special effort, the programmer would not even know that the compiler had chosen to use data memory address 5 to hold Sum, nor would the programmer know which addressing mode the compiler had selected to use to increment Sum.

Note also that this last programming statement makes no sense as a math equation since there are no values for the variable Sum that allow the quantity Sum to equal the quantity (Sum+1). Programming languages use the = symbol to designate assignment. A statement with an equals sign assigns a new value to the variable on the left hand side of the equals sign. You can think of the equals sign as being the "gets" operation, as in, "Sum gets the value of 1 larger than its prior value". It might have been more clear if high-level languages had standardized on the following notation for assignment statements:

    Sum <- Sum + 1

The four addressing modes offered by the 8051 are:

  • Direct addressing where a data address is embedded in the instruction. For example, the command "MOV A,5" causes the 8051's accumulator to be loaded with the contents of data memory address 5.
  • Indirect addressing where a register holds the address of the intended data memory location. For example, the command "MOV A,@R0" causes the accumulator to be loaded with the contents of the data memory location addressed by the value currently held in Register #0. We can't tell what data memory address this would happen to be without knowing the current value held in Register #0.
  • Register addressing where a register holds the desired value (in indirect addressing the register holds the address of the desired value). For example, the command "MOV A,R1" causes the accumulator to be loaded with the value in Register #1. Most microprocessors offer a small number of registers which you are free to use to hold results. The 8051 is peculiar in that its registers are synonymous with certain data memory locations (usually, a microprocessor's registers are distinct from its data memory). In the 8051's default configuration (the configuration it powers up in) Register #0 is synonymous with data memory address 0, Register #1 is synonymous with data memory address 1, etc. through Register #7.
  • Immediate addressing where a data value (not an address) is embedded in the instruction. For example, the command "MOV A,#5 causes the accumulator to be loaded with the value 5. Note that this value did not come from data memory or from a register: it came from program memory since the value 5 came from the instruction itself.

Any 8051 instruction that employs immediate addressing must include the # symbol while any instruction that employs indirect addressing must include the @ symbol.

As a beginning programmer, start with direct and immediate addressing. When you need to use a single statement inside a loop to touch many data memory locations you will suddenly realize why indirect addressing was invented. The example 8051 programs that are distributed with this simulator avoid using register addressing because of the confusion that arises from the fact that the registers are synonymous with certain data memory locations.


Binary and Hex Numbers

( 10 )
Previous Table Of Contents Next

Binary and Hexadecimal Numbers

We are so used to decimal numbers that we can easily forget the rules regarding their construction. The decimal number 137 indicates the quantity that has 1 hundred, 3 tens, and 7 ones. This is because the columns of a decimal number are powers of 10; i.e., 1, 10, 100, 1000, etc. The same symbols can be used in any column and the set of legal symbols for the columns of a decimal number is {0, 1, 2, ... 9}. Note that the number 10 is not legal within a column (a SINGLE column) of a decimal number.

A binary number is a number expressed in a notation where the column values are all powers of 2. Hence the columns have the values 1, 2, 4, 8, 16, 32, ... Furthermore, the set of legal symbols for use in the columns is {0, 1}. Note that the number 2 is not legal within a column of a binary number. With this background you ought to be able to decode the following binary numbers:

   00000101 = 4 + 1 = 5
   00100011 = 32 + 2 + 1 = 35

The largest possible value that an 8 bit binary number can have is:

   11111111 = 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255

So why would anyone want to write numbers in binary notation? If the values in question really are numbers, meaning that they will be added, subtracted, multiplied, or divided; then decimal is probably the best notation. But not all values really are numbers: some are just codes; i.e., patterns that stand in for something else. For example, we need some way to command the 8051 to add. The 8051 will perform addition if the appropriate switches are set as necessary for addition (remember that computers think by opening and closing switches at very high speed). So the natural notation for the ADD operation is whatever pattern of switches accomplishes addition. Perhaps this pattern of switches is 00100110. Is this a number? No, it is merely a code that represents addition. Binary numbers work great for this code because each switch must either be open or closed and hence can be represented by a single bit.

For a better example (the problem with the previous argument is that way more than 8 switches are involved in addition), consider the electronic signboard that is used for many of the example programs distributed with this simulator. To create images on this signboard we merely need to specify which light bulbs should be illuminated and which should be dark. For example, one column of an image may require the following pattern of lights:

        ---
       | * |  LSB (least significant bit)
        ---
       | * | <- this light bulb is lit
        ---
       |   | <- this light bulb is dark
        ---
       | * |
        ---
       |   |
        ---
       |   |
        ---
       | * |
        ---
       |   |  MSB (most significant bit)
        ---

The only way that the 8051 can command actions on the signboard is by writing bytes (8 bit patterns) to its I/O (input/output) pins. The signboard is wired to the 8051 in such a way that the LSBit (least significant bit) of the byte output to the P0 register by the 8051 corresponds to the light bulb at the top of the column of light bulbs. So to get the lights lit as shown above the 8051 needs to output the bit pattern 01001011. Is this a number? Not really, it is a bit pattern.

But it is ungainly to describe columns of the signboard using binary numbers because they are so verbose. For example, 255 certainly is more concise than 11111111. This is where hexadecimal (hex) numbers come in.

A hexadecimal number is a number expressed in a notation where the column values are all powers of 16. Hence the columns have the values 1, 16, 256, 4096, ... Furthermore, the set of legal symbols for use in the columns is {0, 1, 2, ... 15}. Note that the number 16 is not legal within a column of a hexadecimal number. But what may be surprising is that the values 10, 11, 12, 13, 14, and 15 ARE legal within a SINGLE column of a hexadecimal number. We need a one-column-wide symbol to represent these 6 troublesome values and so we choose the following mapping:

   A represents 10
   B represents 11
   C represents 12
   D represents 13
   E represents 14
   F represents 15

With this background you ought to be able to decode the following hexadecimal numbers. Note that we always use the prefix 0x to identify hexadecimal numbers because otherwise they could (if they don't include A, B, C, D, E, or F) be confused with decimal numbers.

   0x23 = ( 2 * 16 ) + ( 3 * 1 ) = 35
   0xAF = ( 10 * 16 ) + ( 15 * 1 ) = 175

Remember that column of the signboard that we wanted to illuminate according to the binary number 01001011? This bit pattern can be more easily described as 0x4B. There is a trick for quickly converting from an 8 bit binary number to a two hex digit hexadecimal number. First, group the 8 bit binary number into two groups of 4 bits. So 01001011 becomes 0100 1011. Then look up each of the two 4 bit patterns in the following table to get the corresponding hex digits.

  for 0000 use 0x0
  for 0001 use 0x1
  for 0010 use 0x2
  for 0011 use 0x3
  for 0100 use 0x4
  for 0101 use 0x5
  for 0110 use 0x6
  for 0111 use 0x7
  for 1000 use 0x8
  for 1001 use 0x9
  for 1010 use 0xA
  for 1011 use 0xB
  for 1100 use 0xC
  for 1101 use 0xD
  for 1110 use 0xE
  for 1111 use 0xF

Do you see how 01001011 becomes 0x4B? It makes no difference whether you write this as 0x4B or as 0X4B or as 0x4b or as 0X4b. After programming for about a week you will have this table memorized.

I designed my 8051 simulator to use the 0x prefix to indicate hex numbers because that is the notation used in the higher-level languages like C, C++, and Java. Most assembly language tools will accept this notation, but an even more common assembly language standard is to use an H suffix to indicate a hex number. That is, 0x4B would be written 4BH. You will probably see this notation if you research the 8051 in other literature. My 8051 simulator will accept this alternate notation but I don't emphasize it because this notation is not employed in C, C++, or Java.

Finally, I should point out that every Windows computer comes with a calculator that can convert to and from hex numbers. You can find this calculator at:

  Start/Programs/Accessories/Calculator

At first this calculator may appear like a simple 4 function calculator. But it will change its appearance when you set this calculator into its scientific mode by selecting View/Scientific from the calculator's menu. After doing this the calculator will look like:

By default, the calculator accepts and expresses numbers in decimal (base 10) notation. As practice, enter the number 4095 and then click on the "Hex" radio button (found on the calculator's face) to see this number expressed in hex. The display will then change from 4095 to FFF. To employ this hex number in an assembly language program you must include the 0x prefix. You could write 0xFFF but it is more standard to write 0x0FFF since the presence of the 4 hex digits constantly reminds us that the 8051's architecture employs 16 bit (4 hex digit) addresses. Every company that manufacturers an 8051 chip can decide how much program memory they want to include so the actual maximum program memory address can vary but will never be larger than 0xFFFF = 65535. The most common varieties of 8051 chips (and this Windows simulator) only support 4096 bytes of on-chip program memory and hence the largest legal program memory address is 4095 = 0x0FFF.


( 11 )
Previous Table Of Contents Next

8051 Architecture

In this section I am going to summarize the main characteristics of an 8051 microprocessor. Don't worry if some of these topics seem confusing. I am merely introducing them now. They will each be more fully explored in the example programs that I distribute with this simulator.

The 8051 offers a 64 Kbyte (that is, 64 * 1024) program memory address space. However on-chip program memory was limited to 4 Kbytes. Any additional program memory had to be provided by a separate chip. This Windows 8051 simulator restricts program memory to 4 Kbytes (don't worry, that's plenty). Program memory can only be read, not written to. Furthermore, program memory is non-volatile, meaning that it survives the loss of power. Every time the 8051 is powered up there is something useful in its program memory (if there wasn't, the 8051 would be in an incurable coma).

The 8051 also offers a 64 Kbyte data memory address space. On-chip data memory (that is, the data memory available without adding additional chips) was originally limited to 128 bytes. Later members of the 8051 family increased the on-chip data memory to 256 bytes. This Windows 8051 simulator restricts data memory to 256 bytes. Data memory can be both read and written. At power-up, data memory is empty. This doesn't mean it holds all zeros. It means you don't know what it holds and you must initialize it however you want.

Besides the data memory, the 8051 also offers twenty-one special function registers (SFR). These registers are like data memory locations in that you can read and write them. But rather than being places where you can park arbitrary numbers, these registers control the on-chip components of the 8051 such as its timers, its interrupts, its serial port, etc.

The 8051 offers 32 individually addressable input/output (I/0) lines. This means that the 8051's package provides a place where you can solder 32 separate wires. "Input/output" means that your program can both read from and write to these 32 wires (also known as "pins" or "leads"). In the example programs distributed with this 8051 simulator these 32 I/O pins are used to control things such as an electronic signboard, a mechanized mouse exploring a maze for a piece of cheese, and an LED bar graph.

One of the 8051's features that didn't particularly catch on was its bit addressability. This means that the 8051 offers many instructions that act upon individual bits within certain of the bytes in its data memory and special function registers. Other uPs (microprocessors) can still accomplish this same activity but require a short sequence of instructions to do it.

The 8051 offers two 16-bit timers. These two timers can be separately configured into 4 possible operating modes, such as a mode where they increment based upon the 8051's internal clock or a mode where they increment when an external pin on the 8051's package is pulsed (in this later mode the timer is usually called a counter). We will study the timers using the TimerTest.txt example program.

The 8051 offers a full duplex UART (universal, asynchronous receiver and transmitter) which means it can communicate with other devices via the RS-232 standard. This is the same standard employed for the serial ports on a PC (the ones that have largely been replaced by the USB ports).

The 8051 has the ability to respond to five different interrupts which can be arbitrarily assigned to one of two possible priorities. An interrupt means that an external event (such as a pulse on a wire attached to the 8051's package) can alter the normal flow of the 8051's program. We will study interrupts using the SerialPort.txt example program.

All these features (timers, serial port, interrupts, etc.) of the 8051 are illustrated in the example programs distributed with this simulator. I will let you know when it's time to dive into the first example program.


( 12 )
Previous Table Of Contents Next

Data Memory

The 8051's data memory space is divided into three blocks which are generally referred to as the Lower 128, the Upper 128, and SFR space (SFR = "special function register"). The Lower 128 cause the least trouble, as they are guaranteed to be available on any flavor of 8051 chip. The Upper 128 were added later after the 8051 was already designed, and hence they had to be shoehorned in. The only place they could go was on top of the SFR space, as shown below:

This figure attempts to explain that the Upper 128 bytes of data memory share the same addresses as the special function registers. If you want to access the Upper 128 bytes of data memory you must do so via indirect addressing. If you want to access the SFRs you must do so via direct addressing. This is what is called a hack meaning an ugly solution to a sticky problem.

The Lower 128 bytes of RAM (i.e., data memory) can be accessed using either direct or indirect addressing. These Lower 128 bytes are further subdivided as shown below:

Note that the first 8 bytes of data memory are synonymous with the 8 registers. Program instructions call out these registers as R0 through R7. This figure shows the 8 registers in their default location but they can move. Two bits (RS1 and RS0) in the Program Status Word (PSW, one of the SFR registers) select which register bank is in use. When the 8051 first powers up (and until you write to the PSW register to change the RS1 and RS0 bits) bank 0 will be active meaning that R0 is synonymous with data memory address 0, R1 is synonymous with data memory address 1, etc. I feel this is unnecessarily confusing to the beginning programmer and hence I have avoided employing the registers in the example programs I distribute with this simulator. But I can't avoid using R0 and R1 since these provide the only means to accomplish indirect addressing. Therefore be forewarned:

Do not employ data memory addresses 0 or 1 since these locations are identical with R0 and R1 which you must use to accomplish indirect addressing. As long as you don't employ any of the other registers (R2 through R7) then you can freely employ the remaining data memory locations. But do not change the RS1 and RS0 bits in the PSW register or else R0 and R1 (and all the other registers) will move to a different location in data memory.

But there's another complication. The 8051 employs part of its on-chip RAM (data memory) for its return address stack, which is involved with both subroutines and interrupts. We will study subroutines and interrupts later, but for now just know that if your program employs subroutines or interrupts then you will have to worry about a collision between the stack and the data memory locations that your program employs. In its default configuration, the stack will start at data memory address 8 and then grow into larger data memory addresses. But the programmer can move it to a different place in data memory if he so desires.

Starting at data memory address 0x20 and continuing through 0x2F is a special block of data memory that is bit-addressable. That is, the individual 8 bits of each of these 16 bytes are individually addressable, meaning that you can flip one of these bits without perturbing its neighbors. This idea didn't catch on with later microprocessors. Bit addressability is further described in the MazeSolver2.txt example program that I distribute with this 8051 simulator.

Remember: all of the bytes in the Lower 128 can be accessed by either direct or indirect addressing. The Upper 128 bytes of RAM can only be accessed by indirect addressing.


( 13 )
Previous Table Of Contents Next

Special Function Registers

If you use a direct address of 128 or larger you will be addressing the Special Function Register (SFR) space. These SFR registers are like data memory locations in that you can read and write them. But rather than being places where you can park arbitrary numbers, these registers control the on-chip components of the 8051 such as its timers, its interrupts, its serial port, its input/output pins, etc. Not all of the addresses in the SFR address space are functional, as seen in the following diagram:

You don't need to memorize the addresses of the SFR registers since you refer to them by their label (e.g., TCON) in your assembly language programs.

Sixteen addresses in SFR space are both byte and bit-addressable. The bit-addressable SFRs are those whose hex address ends in 0 or 8. Hence the TCON register, having a hex address of 0x88, is bit-addressable but the TMOD register, having a hex address of 0x89, is not.

To see why Intel wanted to offer bit-addressability, let's look at the contents of a typical SFR register: the IE or "interrupt enable" register:

The five least significant bits in this register control whether the five possible interrupt sources are enabled to produce an interrupt. The most significant bit globally disables all interrupts. When you want to change, say, the serial port interrupt enable bit ES you need to be careful to not disrupt the other bits in the same register. On most microprocessors this would require you to employ a read/modify/write cycle. That is, you first read the register, thus acquiring the current state of all 8 bits. Via code statements you then set or clear the one bit you are interested in. Then, as a third step, you write the new byte back to the register. What takes 3 steps in most uPs takes only a single step on the 8051 thanks to the ES bit being bit addressable. That means that there are a few instructions (such as SETB and CLR) which can -- in a single step -- change this one bit of the register. This feature has pretty much been dropped from more modern microprocessors since the 3 step procedure is still plenty fast.

The SFR register that you need to become familiar with first is the accumulator, which is usually labeled as ACC or A. The 8051 can add, subtract, multiply, and divide; but only if you first load the accumulator with one of the operands. Following the math operation the result is always placed into the accumulator. The following three 8051 instructions show how to add the contents of data memory address 7 to the contents of data memory address 8 and place the result back into data memory address 7:

  MOV  A,7   ; load ACC with contents of RAM[7]
  ADD  A,8   ; add contents of RAM[8] to ACC
  MOV  7,A   ; load RAM[7] with contents of ACC

The Program Status Word (PSW) is the SFR register that contains the status bits that reflect the current state of the 8051. These bits are the Carry bit C, the Auxiliary Carry AC (useful for BCD operations), the two register bank select bits RS0 and RS1 (I warned you about these), the Overflow flag OV, the Parity bit P, and one user-definable status flag F0. This F0 bit is not employed within the 8051 and Intel's engineers give it to you for any purpose you wish. Back in the 1970's when the 8051 was designed, it was a big deal to offer even 1 more bit of memory here and there. But today, the computer sitting in front of you probably has 800 million bits of memory in it!

You don't need to remember the locations of the bits within the PSW register or any other SFR register as your assembly language programs can refer to them symbolically. That is, the instruction which clears the carry bit in the PSW register is:

    CLR  C

The Carry bit also serves as the Borrow bit during subtraction and serves as the destination for many of the 8051 instructions that operate on single bits. The Carry bit is the only one of the bits in the PSW register that we will pay much attention to in the example programs that are distributed with this simulator.

Every microprocessor has a register similar to the 8051's PSW register that holds status bits describing its state. These status bits are called flags or condition codes.

The B register is used in only two instructions: the multiply instruction MUL and the divide instruction DIV. When you are not multiplying or dividing you are free to use the B register as a temporary storage location.

A few 8051 instructions (such as MOVC A,@A+DPTR) refer to the DPTR (data pointer). This is a 16 bit register that can be used to point anywhere within either program memory address space or data memory address space. The 16 bit DPTR consists of the two halves DPL and DPH (data pointer low and data pointer high), which are both 8 bits wide. The DPTR is further discussed in the Train2.txt example program distributed with this 8051 simulator.


( 14 )
Previous Table Of Contents Next

8051 Addressing Modes

The 8051 offers the following addressing modes:

Direct Addressing

In direct addressing, the operand is specified by an 8-bit address field in the instruction. An example of direct addressing is:

  MOV  A,3  ; move contents of RAM[3] to the ACC

Indirect Addressing

In indirect addressing, the instruction specifies a register which contains the address of the operand. This register can be either R0 or R1 at the programmer's discretion. An example of indirect addressing is:

  MOV  A,@R0  ; use the contents of the R0 register as a
              ; data memory address and load the ACC with
              ; the contents of that data memory location

The R0 and R1 registers are both 8 bits wide and hence can hold any on-chip data memory address. But the 8051 also offers a DPTR (data pointer) register which is 16 bits wide and can be used to point to any program memory address.

Note that you must always include the @ symbol before an indirect address and only R0 and R1 can be employed for indirect addressing.

Register Addressing

In register addressing, the 8051 instruction specifies one of the 8 available registers R0 through R7 as the source of an operand. An example of register addressing is:

  MOV  A,R3   ; load the ACC with the contents of R3

Register addressing can get confusing because the registers are not distinct from data memory. In the 8051's default configuration (the configuration it powers up in) register #0 (R0) is synonymous with data memory location 0, R1 is synonymous with data memory location 1, etc. through R7. For this reason the example programs distributed with this simulator avoid using register addressing.

Immediate Addressing

Immediate addressing is a bit of a misnomer since it is not really an address at all. Rather, when an instruction uses immediate addressing the operand is located in the instruction. An example of immediate addressing is:

  MOV  A,#5   ; load the ACC with the value 5

Note that you must always include the # symbol before an immediate address.


( 15 )
Previous Table Of Contents Next

Generic Instruction Forms

In order to aid the beginning programmer the Compose mode of this Windows simulator offers a right-hand pane that lists all of the generic forms of the 8051's instructions. For example, under the section entitled "Arithmetic Instructions" you see the following:

  ADD A,Rn

The Rn in this notation is shorthand for a member of the set {R0, R1, R2, R3, R4, R5, R6, R7}.

Using the list given in the right-hand pane we can learn that the ADD, ADDC, and SUBB instructions can employ any of the following four canonical forms for their operand lists:

   A,Rn   A,addr8   A,@Ri   A,#data8

where Rn is one of the members of the set {R0, R1, R2, ... R7}, addr8 is one of the members of the set {0, 1, 2, ... 255}, Ri is one of the members of the set {R0, R1}, and data8 is one of the members of the set {0, 1, 2, ... 255}. The notation addr8 refers to an 8 bit wide address. The notation data8 refers to an 8 bit wide piece of data not used as an address. So ADD A,R3 is a legal 8051 instruction (that adds the contents of register #3 to the accumulator) since it is a specific form of the generic ADD A,Rn instruction.

The ANL, ORL, and XLR instructions can employ any of the following six canonical forms for their operand lists:

   A,Rn   A,addr8   A,@Ri   A,#data8   addr8,A   addr8,#data8

That is, ANL 5,#7 is a legal instruction (that ANDs data memory address 5 with the value 7). Note that ANL 5,#7 is an instruction that employs both a direct address (the 5) and an immediate address (the 7).

The MOV instruction can employ any of the following 16 canonical forms for its operand list:

   A,Rn       A,addr8    A,@Ri         A,#data8
   addr8,A    addr8,Rn   addr8,addr8   addr8,@Ri   addr8,#data
   Rn,A       Rn,addr8   Rn,#data8
   @Ri,A      @Ri,addr8  @Ri,#data8
   DPTR,#data16

where data16 is any 16 bit number. Hence MOV R3,5 is a legal instruction (that loads register #3 with the contents of data memory address 5). Note that MOV R3,5 contains both a register address (R3) and a direct address (5). After examining the above list you should understand why the assembler will give you an error if you try to use the instruction MOV R3,@R0.

The complete list of all of the 8051's instructions can be found here.


Compose Mode

( 16 )
Previous Table Of Contents Next

Compose Mode of the Simulator

My 8051 simulator offers two basic modes: a Compose Mode you use to write your 8051 programs and an Execute Mode that you use to run your 8051 programs. When you first start the 8051 simulator you are in the Compose Mode. Whenever you are in Compose mode the program's window is divided into two panes: on the left is a text editor pane and on the right is a list of all of the generic forms of all the 8051's instructions. These two panes are shown below:

The two panes of "Compose Mode"

The text editor pane on the left works just like the Notepad application that Microsoft distributes with all version of their Windows operating system. This pane allows you to type text, edit that text, and preserve it in disk drive files. These concepts should be familiar to you from having used Notepad or another word processor.

All of the 8051 programs that you will write and run in this simulator are stored in plain text word processor files. "Plain text" means the file holds nothing but ASCII characters. That is, it contains no hidden content that accomplishes fancy formatting such as changes to the font typeface, the font size, the text color, etc. It also means that the filename always ends with a .TXT suffix. You can create plain text files using any word processor. They are the only type of file that Notepad can deal with. The Windows operating system considers that Notepad owns all such files. This means that if you double-click on a .TXT file being displayed by Windows Explorer, the Windows operating system will call up Notepad and use it to display the file to you.

You can use Notepad to create your 8051 programs or you can write them in the left-hand pane anytime that this simulator is in its Compose mode. If you want to create a plain text file using some other word processor you must ask the word processor to save the file as "plain text" (the word processor will usually warn you that that choice will result in the elimination of all the formatting in the document).

I won't bother describing the menu choices and toolbar buttons that are associated with editing and saving text as you should be familiar with these from having used Notepad. But there are a few toolbar controls that deserve mention. You can search for text using either the "Edit/Find..." choice in the menu or via the Find tool on the toolbar. To search for a text string using the Find tool on the toolbar simply type the text string into the EDIT control on the toolbar and then hit the ENTER key on the keyboard. You can move to the next occurrence of the search string by again hitting the ENTER key or by hitting F3. You can search backwards by holding down the SHIFT key while you type ENTER or F3.

The Find tool on the toolbar is connected to the Find dialog that opens when you select Edit/Find from the menu. That is, if you specify a case sensitive search on the Find dialog, then until you change that selection the Find tool on the toolbar will also perform case sensitive searches. This can be very confusing! But I felt I had to design my 8051 simulator this way because this is how Microsoft's Visual C++ compiler works and many of you will be advancing on to that tool.

Now let's learn about the final three buttons (pushbuttons) on the toolbar.

A good way to identify the buttons on the toolbar is to allow the mouse cursor to rest above any button for a second or two until a tooltip appears with an explanation for that button. The tooltip for the Assemble button says, "Assembles the source code into an executable program". The icon on the Assemble button has a picture ( [image]) that's supposed to look like paper being shredded. That is, the assembly process converts your source file into something else: machine code instructions for the 8051. This doesn't affect your source code in any way. But it does generate a listing file that shows the results of the assembly process. [Blame Microsoft if you don't like the pictures used for these toolbar buttons. I am using their standard pictures in order to prepare you for their development tool: the Visual C++ compiler.] Remember: you create the source file and the assembler creates the listing file based upon your source file.

Let's perform our first assemble. In the left-hand pane go ahead and type 1 line of arbitrary text. Type whatever tickles your fancy as we don't want it to constitute a legal 8051 program. Then click on the Assemble button on the toolbar. You will notice that the left-hand pane is reconfigured. No longer does it show you your source file, instead it shows the listing file that the assembler created from your source file. You know you are looking at the listing file because of two clues:

  • observe that the Listing File button on the toolbar ( [image]) is depressed
  • observe that the status bar (at the very bottom of the window) begins with the text "Lst filename:"

So what's in the listing file? As you can see, everything in your source file is brought into the listing file. Each line of your source file is simply shifted to the right to make room for three new columns:

  • a column labeled LOC holding program counter values,
  • a column labeled OBJ holding the machine code (also known as object code),
  • a column labeled LINE holding consecutive line numbers

The LOC column shows the program memory address where each instruction is placed. The OBJ column holds the machine language code which the 8051 microprocessor understands. These two columns are one of the few places where you will see hexadecimal numbers without their usual 0x prefix.

The listing file that we just created will contain an error message. These messages help you perfect your program. At the very bottom of the listing file is a summary of the number of errors and the number of warnings that your program generated.

Anytime you perform an assemble, the left-hand pane of the simulator switches over to showing you the listing file. You can switch back and forth between your source file and your listing file by clicking on the Listing file button on the toolbar ( [image]). Your source file can be edited at any time it is visible. But you will notice that you are prevented from typing on your listing file. This is because the listing file is maintained in a read-only state. It is declared "read-only" because it is a derived file. It is created by the assembler from your source program.

The last of the big 3 toolbar buttons in Compose Mode is the Execute button ( [image]) which causes you to leave Compose mode and enter Execute mode. This button's tooltip says "Enters the debugger" and its picture is a large exclamation point. You will notice that the Execute button is presently grayed out which means that it is not functional. We cannot enter the Execute mode of the 8051 simulator until we accomplish an error free assembly. That is, you can't run your 8051 program until it has no errors. You can run a program that generates warnings as long as it does not also generate errors.


IncrementForever.txt

( 17 )
Previous Table Of Contents Next

The IncrementForever.txt Example Program

I bet you're tired of reading and want to get started programming. Well here we go! This is the first of the example programs that I distribute with my simulator to illustrate assembly language programming of the 8051 microprocessor. Our first 8051 program is only 3 lines long (not counting the comment lines) and it's almost impossible to make a program any simpler than that.

The purpose of this initial program is to get you started assembling and executing 8051 assembly language programs. So start up the simulator and then load the IncrementForever.txt file into the simulator. You do this by selecting the File/Open choice from the menu and then navigating (or browsing) to the folder holding IncrementForever.txt. Once you have opened the IncrementForever.txt file you will see its contents displayed in the left-hand pane of the simulator. If you wanted to, you could make changes in the IncrementForever.txt program at this point.

The first (non-comment) line of the IncrementForever.txt program is:

Start:  MOV  A,#0  ; the program starts here

This statement happens to include all 4 of the fields that an assembly language statement can include. It starts with a label. The label in this case is Start. You can choose your own labels within the following rules:

  • Labels can include lower and upper-case alphabet letters, the 10 number digits (0-9), and the underscore ( _ ) symbol; but they cannot start with a number digit.
  • Labels can be any reasonable length but they must end with a colon. The colon, although required, is not part of the label. Furthermore, you cannot have blanks (space characters) between the end of the label and the colon.
  • Labels are case sensitive so START is a different label than Start.

This particular Start label serves no purpose in this program. This means you can take it out (just the label, not the whole line) and the program will still assemble and run the same as before. I included it because it is a style that will be useful in later programs.

After the label this statement has a mnemonic. The particular mnemonic is MOV which represents the "move byte" operation. You do not get to choose mnemonics: they were selected by Intel back in the 1970's when the 8051 was designed.

The MOV mnemonic requires an operand field that specifies the bytes that are to be moved. This statement's operand field is "A,#0". This operand field includes two operands: the symbol for the accumulator and the symbol for the value 0. Whenever an operand field holds more than one operand the operands must be separated by commas. You are free to include blanks (space characters) between the operands to improve the appearance of your code.

Finally, this statement has a comment field. The semicolon character (;) announces to the assembler that it should ignore the remainder of the line. Hence there are no rules regarding what you can put after a semicolon. Comment fields are used to include explanations of the code on that line.

The first statement is an example of the generic 8051 instruction MOV A,#data8. You can see a list of the generic forms of all the 8051 instructions in the right-hand pane while the simulator is in its Compose mode. Go ahead and find this generic MOV A,#data8 instruction in the right-hand pane of the simulator (you will have to scroll down a bit). Then read the comments at the very bottom of this right-hand pane.

In the comments at the bottom of the right-hand pane you will read that the data8 portion of MOV A,#data8 generic instruction is a place-holder for any 8 bit wide data value that you care to use. An unsigned 8 bit value can range from 0 to 255 (you have read about binary numbers haven't you?). The statement MOV A,#0 chooses to use the value 0 for this 8 bit wide data value. The # symbol indicates that this instruction uses immediate addressing which is one of the four types of addressing offered by the 8051. If you leave off the # character the program will still assemble because the statement will still be legal but it won't accomplish what we want (we'll try this later so don't do it now). The MOV A,#0 statement causes the 8051's accumulator to be loaded with the value 0. We will be able to observe this happening by single-stepping this statement (more on this later).

The next statement is "Loop: ADD A,#1". This statement also starts with a label and unlike the Start label, the Loop label is critical. It is critical because the final statement of this program, "LJMP Loop" refers to this label. You create labels in order to serve as the destination of branch (jump) statements such as the LJMP statement. In this program the second statement creates a label which the third statement employs.

The statement ADD A,#1 is a specific form of the generic ADD A,#data8 instruction. Locate this generic instruction in the right-hand pane. Note that it too uses immediate addressing. The ADD A,#1 instruction adds the value 1 to the current contents of the accumulator and places the result in the accumulator.

Every 8051 instruction has its own help page. For a complete description of the ADD command look here. These instruction-specific help pages may be a bit hard to read at first as they discuss many issues that we have not yet introduced. But after you get the hang of programming the 8051 these instruction-specific help pages will be the help pages you access the most.

The final statement of this program is LJMP Loop. This is a specific form of the generic LJMP addr16 instruction. LJMP is the mnemonic for the "long jump" operation. A long jump is a jump that can reach anywhere within the 8051's program memory (yes, there is also a short jump, SJMP, that can't reach as far). In this case, the LJMP causes the program to branch back to the statement that has "Loop" in its label field.

In fact this program has what's called an infinite loop because it will run forever, cycling between the second and third statements. We will be able to watch this behavior once we enter the simulator's Execute mode.

But before we can run any 8051 program we must assemble it. This is the step where our assembly language program is converted into machine language which the 8051 microprocessor understands.

To assemble the program you need to click on the Assemble button on the toolbar ( [image]). The icon on this button has a picture that's supposed to look like paper being shredded. That is, our source text is being processed into something else: instructions for the 8051. Remember that you can identify the buttons on the toolbar by allowing the mouse cursor to rest above a button for a second or two until a tooltip appears with an explanation for that button.

Each time you assemble you create a listing file. The listing file shows the combination of your code and other items added by the assembler. Assemble the IncrementForever.txt program now (that is, click on the Assemble button on the toolbar now).

Anytime you perform an assemble, the left-hand pane of the simulator switches over to showing you the listing file. Assuming you have not made any changes to the IncrementForever.txt file, then you should observe at the bottom of the listing file:

# of assembly errors   = 0
# of assembly warnings = 0

If there was something wrong with your source file then you would see errors or warnings or both. Only error free programs can be executed (that is, run).

Let's go ahead and introduce an error so you can see this. Before we can make a change to our source file we must switch back to viewing our source file. So click on the Listing file button on the toolbar ( [image]) so that it is no longer depressed. Then remove the label "Start" from the first statement (remove the colon also). Then click on the Assemble button on the toolbar. You will again be switched to the listing file and will observe that we have not yet introduced an error (I told you the Start label was not critical).

Switch back to the source file again and this time purposefully misspell the label on the second statement by changing this second statement to:

Lupe:  ADD  A,#1

When you now assemble this new program you will observe at the bottom of the listing file:

# of assembly errors   = 1 (hit F4 to view first error)
# of assembly warnings = 0

Furthermore, you will see that the listing file includes a new line in red. This is an error message and it appears just after the line that caused the error.

0000 7400      8           MOV   A,#0   ; the program starts here
0002 2401      9 Lupe:     ADD   A,#1
              10           LJMP  Loop   ; we could also use SJMP here
   *** ERROR on line 10: illegal operand

It is easy to spot this error in the current listing file since the file is so short. But in a longer program you will appreciate knowing that you merely need to press the F4 key to be taken to the location in the listing file of the first error.

Now I know what you are thinking. You changed the second line and yet you are getting an error message regarding the third line. But this is correct. You are free to create whatever labels you want. Placing the label Lupe on the second line does not cause the second line to be faulty in any way. It is the third statement that has the error because this statement attempts to branch (jump) to a non-existent label. That is what is illegal about its operand.

At this point (or any time when an assemble has produced errors) you are prevented from leaving the Compose mode of the simulator and entering the Execute mode of the simulator. You can't execute a defective 8051 program. You can see that the simulator refuses to allow you to run your program because the Execute button on the toolbar ( [image]) is grayed out. This button holds a picture of an exclamation point and its tooltip says "Enters the debugger". Only in the debugger can you execute your programs.

Now repair IncrementForever.txt or else reload the original copy that I provided you (note that you can't load a .TXT file while you are viewing the listing file). When you again assemble and get 0 errors you will see that the Execute button on the toolbar is now enabled. Click on it to leave the Compose mode of the simulator and enter the Execute mode.


Execute Mode

( 18 )
Previous Table Of Contents Next

Execute Mode of the Simulator

You leave the simulator's Compose Mode and enter the simulator's Execute Mode by clicking on the Execute button on the toolbar ( [image]). This button is labeled with a picture of an exclamation point and its tooltip says "Enters the Debugger". The debugger is where you execute your 8051 programs.

You will observe that the 8051 simulator presents an entirely new form in its Execute mode. The right-hand pane that used to show all the generic instruction forms is gone. In its place are two new panes: a middle pane that shows the 8051's special function registers and a right-hand pane that shows the 8051's data memory. And there is a bottom pane that seems empty. Actually, this bottom pane is an electronic signboard. We will soon be writing 8051 programs that can scroll messages across this signboard. The IncrementForever.txt program does not employ this pane, called the output window.

The four panes of "Execute Mode"

While in the Execute mode the left-hand pane can only display the listing file (and it will continue to be read-only). To modify your program you must return to the Compose mode by clicking on the "Exit from the debugger" button on the toolbar ( [image]). This button has a picture of a piece of paper with a large red X written on it. Remember that you can identify the buttons on the toolbar by allowing the mouse cursor to rest above any button for a second or two until a tooltip appears with an explanation for that button. Go ahead and practice going back and forth between the Compose and Execute modes a few times.

While in the Execute mode you have new toolbar buttons.

We won't need all of these toolbar buttons right away. We have already been introduced to the "Exit from the debugger" button. The next toolbar button to learn is the "Continue the program" button ( [image]). This button has a picture of a piece of paper with a downward arrow next to it. Click on this button and your program will continue running starting from wherever it last left off. At the moment we have never previously executed the IncrementForever.txt program so it will "continue" from the start. Go ahead and continue the IncrementForever.txt program now.

While an 8051 program is running you will see activity in the various panes of the simulator. There is a highlight bar in the left-hand pane that constantly shows you the next instruction about to be executed. You can observe that your program is endlessly switching between the second and third of its three statements. In the middle pane you will see that the registers labeled PC, ACC, and PSW are constantly changing. In this middle pane another highlight bar identifies the most recently changed register.

The PC register is the program counter. The 8051 executes its program by constantly performing the program step identified by the PC and then incrementing the PC. If your eyes are real good (or your computer real old) you will see that the PC is constantly switching between the values 0x0002 and 0x0004. You can find these same values in the far left-hand column of the listing file shown in the left-hand pane. This column is labeled LOC for "location".

It is the PC (program counter) that selects each instruction for execution. It is because the PC keeps taking on the values 0x0002 and 0x0004 that the 8051 keeps executing the "ADD A,#1" and "LJMP Loop" statements. The values 0x0002 and 0x0004 are program memory addresses. You can usually think of the 8051's program memory being populated with the assembly language statements that you wrote. That is, you can say that program memory address 0x0002 holds the ADD instruction and program memory address 0x0004 holds the LJMP instruction.

But program memory actually holds bytes not mnemonics like ADD and LJMP. The actual contents of program memory are the bytes shown in the column labeled OBJ in the listing file. These bytes are called opcodes because they are the codes that stand for operations. The code 0x74 stands for the MOV operation, the code 0x24 stands for the ADD operation, and the code 0x02 stands for the LJMP operation. The code for the MOV operation actually requires 2 bytes and these are the bytes 0x74 and 0x00. They are shown together on the 7th line of the listing file. Note that the LOC and OBJ columns of the listing file always hold hex numbers and yet don't use a leading 0x prefix (I skipped the prefix here to save space).

Program and Data Memory

Looking again at the middle pane showing the registers, you will observe that the PC register requires 4 hex digits while all the other registers require only 2 hex digits. This is because the PC (which must hold program memory addresses) is 16 bits wide while all the other registers are 8 bits wide. Remember, the 8051 is a byte oriented machine.

In this middle pane we can observe that the 8051's accumulator is constantly incrementing. This is a consequence of the fact that our program is constantly adding 1 to the accumulator's present value. Since the accumulator is only 8 bits wide once it reaches the binary value 11111111 = 0xFF the next time we add 1 it will "roll over" back to 0.

The IncrementForever.txt program has been doing its thing for a while now so let's halt the 8051. Click on the Stop button on the toolbar ( [image]). The 8051 comes to a stop wherever it happens to be within its program. Now let's have it continue executing its program but only by a single instruction at a time. We do this via the "Step Into" button on the toolbar ( [image]).

Each time you click on the Step Into button the 8051 will execute a single instruction. So the 8051 will continue doing what it was doing before (endlessly incrementing the accumulator) but now will do this activity as paced by you. Click on the Step Into button a few times and watch the highlighting in the left-hand pane and the changes to the PC and ACC registers in the middle-pane.

Let's learn about two more toolbar buttons before we move on to the next example program. You have probably grown tired of watching the second and third statements of your three statement program. But did you ever observe the first statement being executed? It has only executed once so far since it is at the very start of the program but not a part of the infinite loop. To see it executed again we must restart the program from the top. This is the purpose of the Restart button on the toolbar ( [image]). Incidentally, these toolbar button pictures were selected to prepare you for Microsoft's Visual C++ compiler.

Click on the Restart button (you will have to click on the Stop button first if your program is presently running) and observe that the highlight in the left-hand pane returns to the very first statement. This highlight indicates the next statement to be executed. If you now click on the Step Into button this first statement will be executed.

It is easy to lose or damage the highlighting in the left-hand pane. This happens if you click and drag with the mouse in the left-hand pane to highlight something else (perhaps because you want to cut and paste this text). And even if you don't disturb the highlighting you can cause it to scroll out of sight using the scroll bars of the left-hand pane. To recover from these events you can click on the "Show the next statement to be executed" button on the toolbar ( [image]). The "Show next statement" button does not execute any statements: it merely redraws the highlight that shows you the statement corresponding to the current value of the PC (program counter). This is always the next statement that will be executed.

Can you change the PC without executing statements? You bet. Right-click on the PC register in the middle pane. This will present a dialog box that allows you to modify that (or any other) register.

The Modify Register dialog

Try this and change the PC to any of the values 0, 2, or 4 (you can enter these values in decimal or hex with the 0x prefix). These are the only legal values because these are the only program memory addresses that our IncrementForever.txt program has populated. Note that as you change the PC the highlighting in the left-hand pane will change since the PC indicates the program memory address of the next instruction to be executed.

In a similar manner (i.e., by right-clicking in the middle pane) you can change the current value of the accumulator. Try it. If you right-click on top of the letters ACC, the Modify Register dialog box will be pre-initialized to allow you to change the accumulator.

Now let's do something bad. Right-click on the PC and change it to something other than 0, 2, or 4. Nothing goes wrong initially. But when you then click on the Step Into button ( [image]) the 8051 will attempt to execute a malformed instruction and this will cause the simulator to present an error message such as:

Program execution reached an undefined instruction at PC = 0x0003

This message alerts you to the fact that this program memory address does not hold a legitimate instruction. The IncrementForever.txt program populates program memory with only three instructions and these begin at program memory addresses 0, 2, and 4. Setting the PC to any other value will cause an error.

An actual 8051 chip, by the way, cannot detect this failure and will blindly execute whatever garbage happens to be at the bad program memory address. I have added this feature to my simulator to help reduce the aggravation level of beginning programmers who often write programs that manage to "go south", "lose their mind", "leap into hyperspace", etc.

Finally, I promised earlier that we would modify the second statement from:

Loop:   ADD A,#1

to:

Loop:   ADD A,1

Remember that you have to return to Compose mode to make changes in your source file. Make the change, re-assemble, return to Execute mode, and then Continue ( [image]) your program. Anytime you first enter Execute mode the PC will be set to 0 and hence "Continue" will cause your program to run from the top.

We have changed the second statement by a single character. Whereas before this statement said "add to the accumulator the number 1" now without the # symbol this statement says "add to the accumulator the contents of data memory address #1".

The modified IncrementForever.txt program still runs in an infinite loop, endlessly cycling between the second and third statements. But the middle pane shows that the accumulator never changes from the value 0. You can probably guess this is because data memory address 1 holds the value 0. So let's put something in this RAM location. An 8051 program can populate any RAM location or we can do it manually. In this case we will have to populate data memory address 1 manually.

Remember that the far right-hand pane displays the contents of data memory. To save space in this pane the hex values are displayed without an 0x prefix. The layout of this pane is shown below:

To change a particular RAM location right-click on that value in the far right-hand pane. Your right-click will cause a context menu to appear. The top choice in this context menu is "Modify Value". Select this choice from the context menu by left-clicking on it. A dialog will then open that allows you to change that (or any other) RAM location.

The Modify Memory dialog

Using this dialog, place the value 2 into data memory address 1. Then continue running the modified IncrementForever.txt program (it is possible to leave the program running while you manually modify data memory or the special function registers). You will now observe that the accumulator is incrementing twice as fast as it did during the original IncrementForever.txt program. This is because each iteration of the infinite loop now adds the value 2 (taken from data memory address 1) to the accumulator.


IncrementForever2.txt

( 19 )
Previous Table Of Contents Next

The IncrementForever2.txt Example Program

Hopefully you have taken the time to experiment with the example program held in the IncrementForever.txt file. Because now we are going to add to it.

Use the File/Open choice from the menu (or else the corresponding button on the toolbar) to load the contents of the IncrementForever2.txt file. Remember that you must be in Compose mode and viewing the source file in order to load a different program.

The non-comment portions of the new IncrementForever2.txt program are shown below:

Start:   MOV   A,#0
Loop:    ADD   A,#1
         JNC   Loop
         INC   B      ; increment the B register
         SJMP  Loop   ; LJMP would also work here

This program introduces three new 8051 instructions. The JNC Loop instruction that you see on the third line is a specific version of the generic JNC rel8 instruction. You can read the JNC mnemonic as "Jump if No Carry" or, in better English, "Jump if the Carry flag is not set". This JNC instruction is an example of a conditional jump instruction. A conditional jump instruction branches to a new spot in your program ONLY if its condition is true.

The Carry flag is one of the bits in the PSW (program status register), which is one of the special function (SFR) registers. You can find an illustration of the PSW register here.

The INC B instruction that you see on the fourth line is a specific version of the generic INC addr8 instruction. You can read the INC mnemonic as "Increment the operand".

The SJMP Loop instruction you see on the fifth line is a specific version of the generic SJMP rel8 instruction. You can read the SJMP mnemonic as "Short jump". This SJMP instruction is an example of an unconditional jump instruction. An unconditional jump is made, well, what else, unconditionally.

The difference between the SJMP and the LJMP (long jump) instructions is the distance that you can jump in your program. The LJMP instruction allows you to jump anywhere. The SJMP instruction can only jump forward by 127 bytes or backward by 128 bytes (the asymmetry is a natural property of two's complement numbers which we will study later). Why did Intel bother to invent the SJMP instruction when they already had the LJMP instruction? To save a single byte in program memory (actually, one byte everytime you replace a LJMP with a SJMP). The SJMP's opcode can be made smaller than the LJMP's opcode since the displacement that must be included in the opcode is smaller.

Some of you will be curious about these opcodes. The opcode for each instruction is listed on the instruction-specific help pages next to the entry labeled "Encoding". If you are curious you can look at the encodings for the SJMP and LJMP instructions and see that the former requires only 2 bytes while the latter requires 3 byes. Incidentally, it is these Encodings that become the contents of program memory and hence the contents of the OBJ column in the listing file. Find the opcode for the SJMP instruction in the listing file for the IncrementForever2.txt program. The instruction-specific help page shows these encodings in binary while the listing file shows them in hex.

Getting back to the IncrementForever2.txt program, this program continually adds 1 to the accumulator and hence the accumulator will eventually roll over from its maximum value of (in binary) 11111111 = 0xFF = 255 back to its minimum value of 0. The Carry (C) flag in the PSW (program status word) special function register is set at the moment this roll over happens. This is all a consequence of how Intel designed the ADD instruction. The IncrementForever2.txt program detects the moment when the accumulator rolls over by using the JNC instruction to watch the Carry flag. Only when the accumulator rolls over does the IncrementForever2.txt program increment the B register.

Let's watch the program run. Assemble the IncrementForever2.txt program and then enter the Execute mode and run the program. Remember that you run a program by using the Continue button on the toolbar ( [image]). You will observe that the accumulator is changing rapidly and the B register is also changing, but not nearly as rapidly.

The sequence of steps that this program performs is shown below:

statement executed  ACC value  carry flag
------------------  ---------  ----------
Start: MOV   A,#0      0x00        0
Loop:  ADD   A,#1      0x01        0
       JNC   Loop      0x01        0
Loop:  ADD   A,#1      0x02        0
       JNC   Loop      0x02        0
Loop:  ADD   A,#1      0x03        0
       JNC   Loop      0x03        0
 ...
Loop:  ADD   A,#1      0xFE        0
       JNC   Loop      0xFE        0
Loop:  ADD   A,#1      0xFF        0
       JNC   Loop      0xFF        0
Loop:  ADD   A,#1      0x00        1  <-- rollover occurs, carry flag gets set
       JNC   Loop      0x00        1
       INC   B         0x00        1
       SJMP  Loop      0x00        1
Loop:  ADD   A,#1      0x01        0  <-- carry flag gets cleared
       JNC   Loop      0x01        0
Loop:  ADD   A,#1      0x02        0
       JNC   Loop      0x02        0
 ...

Note that the INC B statement has a long wait to get executed. It cannot execute until the JNC Loop statement quits looping back to the Loop label. And the JNC Loop statement won't quit looping until the Carry flag is set. This does not happen until we have added the number 1 to the accumulator 256 times. Then finally the B register can be incremented for the first time. The next addition of 1 to the accumulator does not cause an overflow. Instead it causes the accumulator to go from the value 0 to 1 and hence the Carry flag is cleared at this time. The reason that Intel connected the Carry flag to the rollover events caused by the ADD (and other) instructions is so that the Carry flag could be used to accomplish 16 bit arithmetic on an 8 bit machine.

On the instruction-specific help pages you can learn which instructions set or clear which of the flags, which are the bits C, OV, and AC in the PSW register. If you look at the instruction-specific help page for the ADD instruction you will see that the ADD instruction affects the C (carry) flag (as well as the AC and OV flags) while the instruction-specific help page for the INC instruction shows that the INC instruction does not affect any of the flags in the PSW.

The IncrementForever2.txt program continues to run and eventually the accumulator rolls over again, at which time the program will increment the B register to the value 2. Let the program run long enough and the B register itself will rollover from the value 255 back to 0.

Don't leave this example program until you understand why the B register is incrementing so much slower than the accumulator. Would a picture help?


Train1.txt

( 20 )
Previous Table Of Contents Next

The Train1.txt Example Program

I bet you're dying to see something on the output window of the simulator. This output window is the bottom pane of the simulator when it is in its Execute Mode. In the default configuration of the simulator this output window holds an electronic signboard.

The Train1.txt example program will march an image of a train's locomotive across this signboard. Now marching an image is easier than you might think because the signboard has a built in scrolling mode. When using the signboard's scrolling mode, every time you re-define the right-most column of lights in the signboard the other columns will all shift left. Hence the image scrolls to the left. The left-most column of lights simply disappears. Of course it is the pattern of lights that shifts, not the light bulbs themselves.

But how is the signboard connected to the 8051 microprocessor? The reason that microprocessors sell like hotcakes is that both their software (their program) and their hardware (their electronic circuitry) can be adapted to almost any use. You customize the software by populating the microprocessor's program memory with a program designed for a particular application. And you customize the hardware by connecting the microprocessor's input/output bus to whatever electronics are appropriate. In order to connect a microprocessor to an electronic signboard we simply wire (solder) the microprocessor's output pins (wires) to the signboard's input pins.

So how does an 8051 program manage to cause particular bit patterns to emerge on the 8051's output pins? This is the job of the P0, P1, P2, and P3 special function registers. Each of these four byte-wide registers holds eight bits which totals to 32 bits which exactly matches the 32 input/output pins that emerge from the 8051's package. The circuit designer can choose where to connect these 32 wires. Some of them can be used to sense (i.e., for input) and some of them can be used to control (i.e., for output).

I have chosen to connect the 8051's P0 register to the electronic signboard's scrolling mode. More properly I should say that I have chosen to connect the output pins controlled by the P0 register to the signboard's scrolling mode. Anytime your 8051 program writes to the P0 register you will be defining a new right-most column of lights and you will be causing all the prior lights to scroll left.

Let's watch this happen. Load the Train1.txt example program, assemble it, and then enter the debugger. Execute the Train1.txt program by "continuing" it. You should observe a locomotive marching into view from the right.

The Train1.txt program is not very interesting. It consists of nothing but repetitions of the generic MOV addr8,#data8 instruction. This instruction uses immediate addressing to copy a particular value to a location. The location we write to is the P0 register, which is one of the special function registers. The magic is in the selection of the bytes written to P0. We have already explained how hex numbers provide the best representation for the columns of lights so re-read the section on hexadecimal numbers if you can't explain how each byte written to P0 causes the corresponding pattern of lights in the right-most column of the signboard.

At the end of the Train1.txt program you will observe an SJMP instruction which is used to cause an infinite loop. This statement is the answer to the question, "What should the 8051 do once it has done everything we want it to do?". In this case we choose to keep it busy jumping to a particular instruction over and over again. This may seem silly but there really is no other alternative. Let's go ahead and remove the SJMP instruction to demonstrate the importance of this statement. You must return to Compose mode to edit your program. You can either delete this entire line of the source file or you can simply place a semicolon (;) in front of the statement in order to turn it into a comment line. Then re-assemble and re-enter the debugger.

This time single step the program by repeatedly clicking the Step Into button on the toolbar ( [image]). As each byte is written to the P0 register you will observe another column of the train's image scrolling onto the signboard. But once you single step beyond the end of the program you will get the error message:

  Program Execution reached an
  undefined instruction at PC = 0x0045.

The last statement in your modified Train1.txt program is located at program memory address 0x0042 and requires 3 bytes of program memory (you can observe these facts from the LOC and OBJ columns of the listing file). Hence your program has never placed anything into program memory address 0x0045 or beyond. What should the 8051 microprocessor do when it reaches this section of its program?

A real 8051 uP will blindly execute whatever garbage it finds in this un-initialized section of program memory. This is not likely to be a pretty event. The 8051 will continue acting screwy as it bangs around doing who knows what. This is an example of a program crash or failure. Everyone has seen a Windows program crash or hang or blue screen and this is one of the ways these failures occur.

If your program attempts to execute code from an un-initialized section of program memory, my simulator will intervene and prevent the 8051 uP from executing garbage opcodes. I added this feature to aid the beginning programmer who will already have enough mystery swirling about his head without having to wonder what he is seeing when the 8051 starts doing things he never asked for in the first place.


Diamond.txt

( 21 )
Previous Table Of Contents Next

The Diamond.txt Example Program

This Diamond.txt program is the first of the skeletal example programs that I have supplied. Or rather, not supplied since by "skeletal" I mean that you must flesh out this program.

What you will find in each of the skeletal programs is a skeleton of code that will ease your workload. The skeleton that I have provided for the Diamond.txt program is a few of the lines from the Train1.txt program.

My solutions for each of the skeleton programs are found in a folder named "Solutions" but don't look there until you have given each of these exercises an honest amount of labor. Otherwise, you won't end up learning to program, you'll only end up learning to read programs.

The goal of the Diamond.txt program is to scroll a diamond shape across the electronic signboard. Hence the program can have the same basic structure as the Train1.txt. But instead of a locomotive we desire to see the following image:

You will need to employ the hex numbers you see in this figure in MOV addr8,#data8 instructions that you add to the Diamond.txt skeleton. The explanation of the correspondence between these hex numbers and the pattern of lights on a column of the signboard can be found here. Get your program to assemble and then run it. We will be adding more features to it next.


ContinuousDiamonds.txt

( 22 )
Previous Table Of Contents Next

The ContinuousDiamonds.txt Example Program

The ContinuousDiamonds.txt program is the second of the skeletal example programs. You know the drill: you have to finish the program given only a skeleton of the necessary code. The skeleton that I have provided for the ContinuousDiamonds.txt program is my solution for the Diamond.txt example program.

The goal of the ContinuousDiamonds.txt program is to scroll a never-ending sequence of diamond shapes across the electronic signboard. You should be able to finish this program by adding only 1 instruction.


SpacedDiamonds.txt

( 23 )
Previous Table Of Contents Next

The SpacedDiamonds.txt Example Program

The SpacedDiamonds.txt program is another of the skeletal example programs. You know the drill: you have to finish the program given only a skeleton of the necessary code. The skeleton that I have provided for the SpacedDiamonds.txt program is my solution for the ContinuousDiamonds.txt example program.

The goal of the SpacedDiamonds.txt program is to scroll a never-ending sequence of non-touching diamond shapes across the electronic signboard. You should be able to finish this program by adding only 1 instruction (though the image will look nicer if you duplicate this new instruction a few times).


WalkingTriangle.txt

( 24 )
Previous Table Of Contents Next

The WalkingTriangle.txt Example Program

This example program demonstrates that the electronic signboard has a random-access mode as well as the serial or scrolling mode we have used previously. The scrolling mode is a serial access mode because you must construct (that is, present) your image in left-to-right column order. There is no way to create a new column on the signboard except at the far right-hand side. And every column on the signboard must have at one time been the right-most column.

In contrast, the random-access mode gives your 8051 programs immediate access to any column of the signboard. If you are getting the hang of programming then you have probably guessed that you must specify the column that you want to affect and then specify the new contents for this column. Each of the columns therefore requires a column address. These addresses are assigned from the left starting with column #0. The signboard has 64 columns so the right-most column has column address #63.

Your 8051 program must output a column address prior to outputting the image data for that column. You write the column address to the P2 register and then write the image data to the P1 register. The write to the P2 register does not make any visible change in the signboard, it merely prepares the signboard for the upcoming write to the P1 register. The write to the P1 register modifies the lights that are lit in the addressed column and makes no changes to any other columns of the signboard. Remember that the engineers at Intel who designed the 8051 provided the P0, P1, P2, and P3 registers so you could connect an 8051 to arbitrary circuitry. It was my decision to connect the P1 and P2 registers to a random-access mode of a signboard. The following figure illustrates the circuit connections between the 8051's general purpose input/output pins (controlled by the P0, P1, P2, and P3 registers) and the scrolling signboard:

Load the WalkingTriangle.txt example program, assemble it, and execute it. You will observe that a triangle shape appears in the middle portion of the signboard and then walks left. I use the signboard's random-access mode to create the triangle and then I use the signboard's scrolling mode to walk the triangle left.

Unlike our previous programs, I don't want to scroll the triangle forever. I want it to scroll 40 columns to the left and then stop. I can accomplish this by employing the signboard's serial access mode and writing a blank column 40 times. So my program needs a loop that will repeat (iterate) 40 times and then exit.

This is a common programming requirement and the purpose of the DJNZ instruction. The WalkingTriangle.txt program employs a specific form of the generic DJNZ addr8,rel8 instruction. The DJNZ mnemonic can be read as "Decrement and Jump if Not Zero". The basic idea is that the DJNZ instruction operates upon a chosen data memory location or SFR register and keeps decrementing this location until its contents become zero. Until the data memory location reaches zero the DJNZ instruction will "take the jump" (i.e., perform the conditional branch). Then once the data memory location reaches zero the DJNZ instruction will not take the jump and hence execution will continue with whatever instruction follows the DJNZ.

I chose to use data memory address 0 for the loop counter (that is, the data memory location operated upon by the DJNZ instruction). The relevant DJNZ statement could then look like:

        DJNZ  0,Loop

A year from now if you looked at this program you might wonder, "Why does this program decrement and test data memory location 0?" The intention of this statement would be much clearer if we could specify an arbitrary label for data memory address 0. This label could then proclaim our use for this RAM location. This is the purpose of the EQU assembler directive.

LoopCounter   EQU   0

Thanks to this EQU ("equate") statement we can now employ the label LoopCounter wherever we previously employed the data memory address 0. So the DJNZ statement can be rewritten as:

        DJNZ  LoopCounter,Loop

When you read this statement a year from now your brain will instantly comprehend that this statement is controlling how many times we iterate inside a loop. The EQU statement exists merely for the convenience of the programmer. It does not change the program in any way.

Incidentally, one of the most famous programming bugs is the off by one bug. This bug occurs in situations where you want a loop to repeat 40 times but it ends up repeating 39 or 41 times due to an error in your program. In the WalkingTriangle.txt program we initialize the LoopCounter data memory location to hold 40 but does that mean that our loop will repeat 40 times? To learn the answer let's first simplify the code to the following:

      MOV   LoopCounter,#1
Loop  MOV   P0,#0
      DJNZ  LoopCounter,Loop

How many times will we write to the P0 register? One or two? Our LoopCounter data memory location is initialized to 1 and ultimately reaches the value 0. So we might write to the P0 register while LoopCounter = 1 and again while LoopCounter = 0.

The answer is that we write to P0 only once. The DJNZ instruction first decrements the operand and then tests it (DJNZ stands for "decrement and jump if then not equal to zero"). So the code above would execute as shown below:

      MOV   LoopCounter,#1    ; LoopCounter now equals 1
Loop  MOV   P0,#0             ; we write to P0
      DJNZ  Loopcounter,Loop  ; LoopCounter is decremented and
                              ; hence equals 0.  LoopCounter
                              ; is then tested and since it
                              ; does equal 0 the DJNZ instruction
                              ; does NOT branch back to Loop.

So when using the DJNZ instruction to control a loop, you want to initialize the loop counter with the number of iterations you desire. In other programming situations you will find you must initialize the loop counter with 1 more or 1 less than the desired number of loop iterations to avoid the "off by one" error.

This is also a good program to experiment with the "Run to Cursor" and "Breakpoint" buttons on the toolbar (use the tooltips to find these toolbar buttons or else refer to the illustration here).

To see the operation of the Run To Cursor button ( [image]) first halt your program and then restart it. After clicking on the Restart button on the toolbar ( [image]) the PC will be reset to 0 and the blue highlight bar in the left-hand pane will show that the first statement of the program is about to be executed. In situations like this we have thus far only used the Continue ( [image]) and Step Into buttons ( [image]). To prepare to use the Run To Cursor button we must first place the cursor (more properly called the caret) in the left-hand pane on a line where we want execution to halt. Choose any one of the lines of your listing file and left click on that line to place a caret somewhere in the line. Then click on the Run to Cursor button ( [image]). You will observe that your 8051 program runs until it reaches the statement you marked with the cursor. This is an easy way to get your program to a particular spot that you want to study (once at the spot you usually want to single step).

The Breakpoint button provides a function similar to that of the Run to Cursor button. After you click on the Breakpoint button ( [image]) a dialog will open wherein you specify a program memory address where you want execution to halt. As usual, you can specify the program memory address in decimal or in hex using a 0x prefix. It will usually be easiest to use hex since you can select a value directly from the LOC column of the listing file. The program memory address 0xFFFF is reserved to mean "disable the breakpoint" (yes, this is a bit of a hack). Go ahead and set a breakpoint on some program memory address used in your program and then restart and continue your program. You will observe that execution will halt when it reaches the breakpoint.

There is an even quicker way to set a breakpoint. Simply right-click on any of the executable statements shown in the left-hand pane (an executable statement is a statement that populates program memory with an opcode) and a breakpoint will be set at that address. Try it. The breakpoint line will be colored red to remind you that execution will stop when it reaches this statement. To cancel the breakpoint simply right-click on any non-executable statement such as a comment or blank line. Another way to cancel (remove) the breakpoint is to click on the "Clear Breakpoint" button ( [image]) found on the toolbar.

I want to point out one more thing before we leave the WalkingTriangle.txt example program. By now you should have clicked on the Restart button on the toolbar ( [image]) a few times. Have you noticed how this Restart button not only resets the 8051 microprocessor to its default, power-on condition but also resets the signboard to its default, power-on condition? This means that whenever you restart your 8051 program it will always have a clean (empty) signboard to begin drawing on.


Train2.txt

( 25 )
Previous Table Of Contents Next

The Train2.txt Example Program

Until now our sample programs have consisted solely of executable statements. Everything that these programs have placed into program memory has been a command that the 8051 can execute. You might be surprised to learn that we often have to put non-executable data inside program memory. The natural home for non-executable data (things like the data that describes the train image) is data memory, not program memory. However the data memory on any microprocessor is volatile, meaning it is wiped out after every loss of power. This also means data memory is empty after power is re-applied. Only the program memory is non-volatile, meaning it survives the loss of power.

So if we want our 8051 microprocessor to power up already having available a train image, then the data that describes this train image must be located in program memory. Go back and look at the listing file produced by assembling the Train1.txt program. You can see that the data bytes that describe the train image are part of the program memory: they are located in the OBJ column intermingled with the opcodes that signify the MOV commands that we use to write to the P0 register.

This intermingling causes the Train1.txt example program to be poorly organized. This program would be improved if we separated the logic that marches an image across the signboard from the data that describes the particular image. With this improved organization, the new program would be able to march different images across the signboard.

As a first step toward this goal we will segregate the data that describes the train image. We employ the DB ("define byte") assembler directive to introduce these data bytes into program memory. The operand of a DB statement can be any value that fits within a byte. The assembler places the operand value into program memory at the current location. Open the TrainData.txt file (that is, load it into the simulator) and you will see that it contains nothing but the DB statements that hold the signboard column data necessary to create a train image.

Next, load the Train2.txt file and you will see that it holds no train image data. Instead you find the INCLUDE assembler directive used to bring the statements from TrainData.txt into Train2.txt. The INCLUDE directive causes the assembler to import the contents of a specified .TXT file.

Go ahead and assemble the Train2.txt program. Then inspect the listing file produced by the assembler. You will see that the TrainData.txt and Train2.txt files have been merged by the assembler. The listing file will show that program memory has been populated as summarized below:

 LOC     OBJ            SOURCE
 0000   020043          LJMP Start
 0003   80              DB 0x80
 0004   50387A          DB 0x50,0x38,0x7A
    ...
 0043   900003  Start:  MOV  DPTR,#StartOfImageData
    ...

The 0x02 that this listing file shows is at program memory address 0x0000 is the opcode for the LJMP operation. The remaining two bytes on this line of the listing file, 0x0043, are the address of the Start label (you can observe this address in the LOC column). The next byte in program memory, 0x80, is not an executable byte (an opcode) but rather is train image column data.

When you power up an 8051 it will always begin executing its program starting with the contents of program memory address 0. Hence you must have an executable statement at this address. We meet this requirement by having a LJMP instruction at program memory address 0. But we want to have a chunk of non-executable bytes (representing data for a train image for a signboard) in subsequent program memory locations. We accomplish this via the DB assembler directive. These bytes will not be executed because the 8051's program counter (PC) will be initialized to 0 and will then change to 0x0043 due to the LJMP.

A very common type of bug -- in fact, the one usually responsible for blue screens -- occurs when data is interpreted as code. For example, suppose that due to a bad LJMP the 8051's program counter got set to 0x0003. Whatever is at this program memory address the 8051 will be forced to interpret as an opcode. In our example, the 8051 will read 0x80 as the opcode (the code for its next operation). It turns out that 0x80 is the opcode for a SJMP which is not at all what we intended (the 0x80 is the first column of train image data). But the 0x80 -- interpreted as an opcode -- forces the 8051 to jump somewhere else in program memory. The 8051 will continue to act screwy as it misinterprets whatever it finds at that new program memory location.

Hopefully you now understand that the Train2.txt example program segregates all of the train image data into a single area of program memory and then branches around this section of program memory. This will make it easy to replace this image with a different image (which is what we will do next).

Now let's dig into the logic of the program. The program must output a bunch of bytes to the signboard. These bytes are initially located in consecutive program memory locations, starting at some address. The listing file shows us that the image data bytes start at program memory address 0x0003. We need a way to write these bytes to the signboard. In the Train1.txt example program we employed the MOV instruction to write bytes to the signboard. The various forms of the MOV instruction allow us to copy bytes taken from a directly addressed data memory location, a register, an indirectly addressed data memory location, or immediate data residing in the instruction. None of these choices are appropriate for the Train2.txt example program. We need a way of accessing an arbitrary byte at some specified program memory address.

This is the purpose of the MOVC instruction. You can read the MOVC mnemonic as "Move code byte". The phrase "code byte" indicates a byte coming from program memory. There are two generic forms of the MOVC instruction and the one appropriate for our present needs is MOVC A,@A+DPTR. The @ symbol in this instruction reminds you that it employs indirect addressing. The MOVC A,@A+DPTR instruction will copy a single byte from program memory and place it into the accumulator. The program memory address is formed by adding the values in the data pointer register (DPTR) and the accumulator (A). Unlike all the other 8051 registers, the DPTR register is 16 bits wide and hence can point anywhere within program memory (that is, it can hold any legal program memory address).

An illustration will show how we employ the DPTR and the accumulator to point to any location within program memory.

We know that the column data for the train image begins at program memory address 0x0003. So the Train2.txt program initializes the DPTR register to point at this address. This is accomplished by the statement:

    MOV  DPTR,#StartOfImageData

The # symbol in this instruction indicates that it employs immediate addressing: StartOfImageData is a label that you can observe in the TrainData.txt file. This label is assigned the value 0x0003 when the assembler processes the TrainData.txt file.

We leave the DPTR pointing at this same program memory address throughout the Train2.txt program. You will be able to observe this by watching the DPL and DPH registers (the two bytes that make up the 16 bit DPTR register) in the middle pane of the simulator.

Our Train2.txt program uses the accumulator to hold an offset BEYOND where the DPTR points. That is, we initialize the ACC to the value 0 and then use the MOVC command to access the byte at program memory address A+DPTR = 0 + 0x0003 = 0x0003 which is the location of the first column of train image data. We then increment the ACC and again use the MOVC command to access the byte at program memory address A+DPTR = 1 + 0x0003 = 0x0004 which holds the second column of the train image. As we continue to increment the ACC we eventually access each column of the train image held in program memory. All these bytes are sent to the signboard by writing them to P0.

One thing we must watch out for is that the MOVC A,@A+DPTR instruction causes the byte from program memory to be placed into the accumulator. This means that every time we use this instruction the offset beyond the DPTR that we placed into accumulator prior to the MOVC A,@A+DPTR instruction is wiped out. Hence we must re-initialize the accumulator before we again employ the MOVC A,@A+DPTR instruction. Look at Train2.txt and observe how we accomplish this need by storing the offset in the data memory location we have labeled (via an EQU assembler directive) LoopCounter. This name, LoopCounter, indicates that this data memory location also controls how many times we iterate through the loop that contains the MOVC instruction. So we are getting double duty from this one data memory location. The LoopCounter variable performs two jobs: counting the number of loop iterations and also functioning as the offset beyond where the DPTR points.

If you would like to practice your 8051 programming, you can rewrite Train2.txt to leave the accumulator holding the value 0 and instead advance the DPTR register so as to access each of the columns of train image data held in program memory. The command that you will need is INC DPTR. This illustrates that there are usually many ways to write a program to accomplish a specific goal.

The final thing to learn from the Train2.txt example program is the CJNE instruction. The CJNE mnemonic can be read as "Compare and Jump if Not Equal". This is another example of a conditional jump instruction like DJNZ, JNC, etc. Conditional jumps are used to create loops and loops are the reason a 10 statement computer program can generate an infinite (yes, infinite) amount of data. The power of loops was first observed by Ada Byron (the daughter of the famous poet Lord Byron) who was the very first programmer. She wrote programs for a steam powered computer that a friend, Charles Babbage, was attempting to build in the 1800's.

We use the CJNE instruction to control how many times we iterate around the loop that holds the MOVC instruction. How many times do we want to iterate? We want to execute the MOVC instruction enough times to copy every column of the train image to the signboard. Hence the number of times we should iterate is equal to the number of columns (bytes) of train image data. This number is hard coded as 64 in the CJNE statement (take a look).

The generic form of the particular CJNE instruction that we use is CJNE A,#data8,rel8. The specific form of CJNE instruction that we use is CJNE A,#64,Loop. The CJNE instruction compares its first two operands and branches if they are not equal. So our CJNE statement is comparing the contents of the accumulator to the number 64. There are 64 columns in the train image and hence 64 bytes in program memory that we wish to access via the MOVC instruction. So until the accumulator reaches the value 64 we continue branching back to the label "Loop" for another execution of the MOVC instruction. Remember that the value of the accumulator is different every time we execute the MOVC A,@A+DPTR instruction.

Use single-stepping to observe this program work. Watch the value in the accumulator (the accumulator's current value is always displayed in the middle-pane when the simulator is in its Execute mode).


Alphabet.txt

( 26 )
Previous Table Of Contents Next

The Alphabet.txt Example Program

I have argued that the Train2.txt example program has superior architecture to the Train1.txt program because it separates the program logic from the train image data. And you have observed that we have at least segregated the train image data into one area of program memory via the DB ("define byte") assembler directive.

But we haven't seen the real payoff of this approach yet. So here it is. The Alphabet.txt example program differs from the Train2.txt program in only a single statement. What was:

    INCLUDE "TrainData.txt"

in Train2.txt becomes:

    INCLUDE "FontData.txt"

in Alphabet.txt. Go ahead and view the contents of FontData.txt. You will see that it holds nothing but DB statements, just like TrainData.txt did. But these bytes now represent the columns of the letters of the alphabet rather than columns of a train image.

Assemble and execute the Alphabet.txt program. You should observe the letters A, B, C, D, E, F, G, and H scroll across the signboard. What happened to the rest of the alphabet? Well, we haven't totally managed to separate the image data from the program logic. Remember the "CJNE A,#64,Loop" statement that controls how many times we iterate through the loop containing the MOVC statement? Because of this statement we copy 64 bytes from program memory and write them to the P0 register (which is connected to the scrolling mode of the electronic signboard). Each letter of our alphabet is described by 8 columns so after outputting 64 consecutive bytes we will have scrolled 8 letters across the signboard.

As a quickie programming assignment, fix the Alphabet.txt example program so that it scrolls the entire alphabet across the signboard. You should only have to change 1 statement.


Factorial.txt

( 27 )
Previous Table Of Contents Next

The Factorial.txt Example Program

The factorial operation is defined as:

   n! = n * (n-1) * (n-2) * ... * 2 * 1

Hence:

  5! = 5 * 4 * 3 * 2 * 1 = 120
  6! = 6 * 5 * 4 * 3 * 2 * 1 = 720

Since the accumulator in the 8051 is only 8 bits wide and since the largest unsigned 8 bit number is 255, the largest factorial that the 8051's accumulator can hold is 5! This is not very impressive and explains why the 8051 is not very often sold to factorial enthusiasts. But this example program will suffice to introduce multiplication on the 8051 and serves as a nice counterpart to the factorial program we studied on the RPN calculator.

The 8051 has the capability of multiplying an unsigned 8 bit operand by another unsigned 8 bit operand. The crucial instruction is the MUL AB instruction. You place one of the operands into the accumulator and the other into the B register (one of the special function registers) before issuing the MUL AB instruction. When an 8 bit number is multiplied by an 8 bit number a 16 bit result ensues. The 8051 places the most significant byte (the upper 8 bits) of this 16 bit result into the B register and the least significant byte (the lower 8 bits) into the accumulator. Hence, if you know that the product of your multiplication will be less than 255, then you need only deal with the lower 8 bits placed into the accumulator. This is the approach taken in the Factorial.txt example program.

Load the program, assemble it, and then watch it execute by single-stepping it. Immediately before the first occurrence of the MUL AB instruction, you should observe that the ACC holds 5 and the B register holds 4. Hence we are computing (5*4). The result (20 = 0x14) is placed back into the accumulator. Prior to the next occurrence of the MUL AB instruction you should observe that the ACC holds 0x14 = 20 and the B register holds 3. Hence we are computing ((5*4)*3). A DJNZ instruction causes us to continue iterating through the MUL AB instruction until the data memory location that we have labeled (via the EQU assembler directive) LoopControl reaches the value 0. Then the program is done and the accumulator holds the final value 0x78 = 120 = 5!

What do I want you to take away from this program? Well, 8 bits is pretty insufficient for performing general math. The microprocessors employed in modern PCs (personal computers) are all 32 bits wide and the largest unsigned value that a 32 bit register can hold is 4,294,967,295. We can certainly do a lot of math without needing to exceed the value of 4 billion. The other take away from this program is the mechanics of the MUL AB instruction since you will need to employ this instruction in one of the upcoming skeleton programs.

The only other new 8051 instruction introduced by the Factorial.txt example program is the DEC instruction which is a counterpart to the INC instruction we have already seen.


FillMemory.txt

( 28 )
Previous Table Of Contents Next

The FillMemory.txt Example Program

Until now our example programs have not made much use of the 8051's data memory. About the only need we have had for data memory is as a place to hold a loop index that is employed for a conditional branch (a.k.a. conditional jump) that controls how many times we iterate through a loop.

So let's go hog wild and write a program that touches every single data memory location. Suppose we wanted to populate all of data memory with the value 7. One way to do this would be something like:

    MOV  0,#7
    MOV  1,#7
    MOV  2,#7
    ...

If we used 256 of these instructions then we could populate every one of the 256 data memory locations with the value 7. But this would be a lousy program. It would take a lot of typing and require 256*3 bytes of program memory since every one of these MOV instructions requires 3 bytes.

A better solution is to employ a single MOV statement inside a loop. After the loop has iterated 256 times we will have written to every data memory location. We can't use direct addressing inside this loop because there is no way to change the address between each iteration of the loop (if we tried this anyway we would end up writing to a single data memory location 256 times and not writing to the other 255 data memory locations at all). This is the reason indirect addressing was created. This FillMemory.txt example program will allow us to watch indirect addressing do its thing.

The word indirect in "indirect addressing" indicates that the operand does not hold the address, it holds the address of the address. That is, you must make an extra stop while tracking down the address. You have your choice of using either R0 or R1 to hold an indirect address. As an example, if the R0 register holds the value 5 then the following instruction:

   MOV  A,@R0

loads the accumulator with the contents of data memory address 5. We go to the R0 register, from it we acquire the value 5, which leads us to data memory location 5 where we find the value that we load into the accumulator. This is obviously more work than direct addressing and so you wouldn't use it if you didn't have to. But inside a loop you often have to. The key portion of the loop in FillMemory.txt is:

Loop:  MOV  @R0,#7    ; write the value 7 to that data
                      ;  memory location pointed to by R0
       INC  R0        ; increment the destination pointer
       INC  A         ; increment the loop counter
       CJNE A,#0,Loop ; keep looping until the ACC rolls over

The instruction that writes to each location in data memory is a specific form of the generic "MOV @Ri,#data8" instruction. The '@' symbol indicates that this instruction uses indirect addressing which is one of the four types of addressing offered by the 8051. The programmer has the choice of employing either the R0 or R1 register to hold the indirect address (this flexibility is indicated by the Ri notation in the generic instruction).

During every iteration of this loop the R0 register will hold a different address since the loop also contains an "INC R0" statement. So every time we visit the R0 register to acquire an address to modify, we find that it holds a different address. After doing this 256 times we will have touched all 256 data memory locations.

Well, almost. There is the complication that the R0 register is synonymous with data memory location 0 (at least while the 8051 remains in its default configuration). Since we need to employ R0 inside the loop then we are not going to be able to populate data memory address 0 from inside the loop. So we really only iterate through this loop 255 times. And then outside of the loop we execute a final "clean up" instruction which initializes data memory address 0 with the value 7.

You will notice that in the comments for the short section of the FillMemory.txt example program that I excerpted above, I used the phrase, "write the value 7 to that data memory location pointed to by R0." Since R0 is being used to hold addresses, it is customary to call it a pointer. R0 is used to identify or point at a particular data memory location. In the earlier Train2.txt example program we employed the DPTR register as a pointer. The difference is that in FillMemory.txt R0 points into data memory while in Train2.txt the DPTR pointed into program memory. This concept of pointers is very prominent in high-level languages like C, C++, and Java.

Hopefully I have convinced you that indirect addressing is made for these situations where one instruction must touch different locations at different times. But even if there wasn't that reason to employ indirect addressing, we have to also remember the 8051 quirk where indirect addressing is the ONLY way you can reach the upper 128 bytes of data memory.

Load the FillMemory.txt program, assemble it ( [image]), get into Execute mode ( [image]), and then watch it execute by single-stepping ( [image]). Then restart the program ( [image]) and continue it ( [image]) to watch the process at full speed.

The FillMemory.txt program is a good opportunity for me to introduce another of the buttons found on the toolbar while the simulator is in Execute mode. Try using the "Step Into N" toolbar button ( [image]). You will observe that it is identical to twenty-five clicks of the Step Into button. Hence it is useful for taking larger steps when you are trying to get to a particular point in your program. And you can customize this N value to something other than 25. Simply select View/Options from the simulator menu and look for the StepIntoN setting.


FillMemory2.txt

( 29 )
Previous Table Of Contents Next

The FillMemory2.txt Example Program

The FillMemory2.txt program is another of the skeletal example programs. You know the drill: you have to finish the program given only a skeleton of the necessary code. The skeleton that I have provided for the FillMemory2.txt program is the FillMemory.txt program we just finished studying.

So what's wrong with the FillMemory.txt program? Well, watch it run and compare the values seen in the accumulator and the R0 register. You can observe the ACC in the middle-pane and the R0 register in the far right-hand pane since the R0 register is synonymous with data memory location 0 (at least while the 8051 remains in its default configuration).

You should observe that the accumulator and R0 register always hold the same value. This indicates that we don't need them both. So your assignment for this FillMemory2.txt program is to eliminate either the ACC or R0.

Let's start by considering how the FillMemory.txt program employs these two registers. FillMemory.txt employs R0 as a pointer into data memory. The key statement is:

     Loop:  MOV  @R0,#7   ; write the value 7 to that data
                          ; memory location pointed to by R0

FillMemory.txt employs the ACC as the loop control agent responsible for determining how many times we loop (iterate). The key statement is:

    CJNE  A,#0,Loop  ; keep looping until the ACC rolls over

So we have to modify one of these two statements. To do this you will have to study the list of all the available 8051 instructions to see whether:

  • the MOV statement can be modified to employ the ACC to hold an indirect address (thus allowing us to eliminate use of R0)
  • or whether the CJNE statement can be modified to employ the R0 register for the comparison (thus allowing us to eliminate use of the ACC).

Only one of these two possibilities is supported by the 8051's instruction set. This is a very common activity for a programmer: he knows what he wants to accomplish and he must arrange the statements allowed by his programming language to get the job done. You can find the list of all legal 8051 instructions either in the right-hand pane of the simulator while it is in its Compose mode or else on the instruction set help page.

Modify the FillMemory2.txt skeleton and then assemble and execute it. You will know you have succeeded when your program populates all of data memory with the value 7.

The original FillMemory.txt program was contrived to be inefficient so that you could write a better FillMemory2.txt program. However even FillMemory2.txt is not the best way to populate all of data memory with a constant value. Consider instead the following program:

AddrPointer  EQU   0

        MOV   AddrPointer,#255
Loop:   MOV   @R0,#7      ; reg 0 (R0) is address 0
        DJNZ  AddrPointer,Loop

        MOV   R0,#7       ; now patch that one data memory location
                          ;   we couldn't touch inside the loop
Done:   SJMP  Done

The loop seen in the program above consists of only 2 statements which together consume 36 microprocessor clock periods per iteration. The loop in my solution for the FillMemory2.txt program consists of 3 statements that together consume 48 clock periods per iteration. Hence the program above will complete in 25 % less time than the FillMemory2.txt program.


FillMemory3.txt

( 30 )
Previous Table Of Contents Next

The FillMemory3.txt Example Program

The FillMemory3.txt program is another of the skeletal example programs. You know the drill: you have to finish the program given only a skeleton of the necessary code. The skeleton that I have provided for the FillMemory3.txt program is my solution for the FillMemory2.txt program we just finished studying.

The previous FillMemory.txt and FillMemory2.txt programs populated all of data memory with the constant value 7. This time we want to populate all of data memory with an incrementing pattern. That is, we want data memory location 0 to get the value 0, data memory location 1 to get the value 1, etc.

It turns out you can accomplish this goal by changing only a single statement in the skeleton program. This is because an incrementing value just happens to be already available. You just have to use it as the source of the value written to data memory.

Modify the FillMemory3.txt skeleton and then assemble and execute it. You will know you have succeeded when your program populates all of data memory with an incrementing pattern.


YourName.txt

( 31 )
Previous Table Of Contents Next

The YourName.txt Example Program

This example program marches the name "JOHN" across the signboard using a beginner's algorithm. I call it a beginner's algorithm because you will notice a lot of repetition. In our next version of this program we will improve this code so don't bother modifying it now to scroll your own name.

This program is similar to the Alphabet.txt example program in that it INCLUDES the FontData.txt file and thus places a large chunk of DB ("define byte") statements near the start of program memory. These bytes, though in program memory, are not executable opcodes. Instead they are data: they are the signboard columns for all the letters in the alphabet.

Just like the Train2.txt and Alphabet.txt example programs, this YourName.txt program initializes the DPTR ("data pointer") to point at the beginning of the image data in program memory and then uses the accumulator to hold an offset beyond where the DPTR points. By varying the accumulator's value we can then use the "MOVC A,@A+DPTR" instruction to access any one of these image bytes. Once we have acquired the image byte we write it to the P0 register in order that it becomes the new column on the signboard.

So the only thing new in the YourName.txt program is that we aren't displaying the full alphabet but rather individual letters within it. This means we initialize the accumulator to point (when combined with the DPTR) at some letter and then issue 8 MOVC instructions (incrementing the ACC each time) to acquire the image data for one particular letter. Then we move the accumulator somewhere else and acquire another letter. We do this four times in order to scroll out the name "JOHN".

Load, assemble, and execute this program. You can "continue" the program to see the name appear in all its glory but then restart the program and single-step to watch how the code works.

Pay attention to the repetition in the code. Observe that the code after the Char1Loop label is conceptually identical to the code after the Char2Loop label which is conceptually identical to the code after the Char3Loop label, etc. Hence this program can be improved by rewriting it to employ subroutines which we will do next. Observe that the CJNE statements that you find within Char1Loop, Char2Loop, Char3Loop, and Char4Loop all cause 8 iterations of their respective loop. This is how we acquire the 8 consecutive columns of each letter of the alphabet.


( 32 )
Previous Table Of Contents Next

Subroutines

Subroutines are one of the great inventions of the software (i.e., programming) industry. They were invented back in the 1800's by a woman who was writing programs for a steam-powered computer that a friend was trying -- unsuccessfully -- to build. Subroutines not only improve the productivity of the programmer but also improve the quality of the code produced. The basic idea springs from the fact that all programs have many sections of identical or nearly identical code. Obviously, no human wants to keep typing in the same text over and over so it would be better to type it in once and use it multiple times.

The way you use the code in a subroutine is you jump to it via a call statement and then return from it via a return statement. For example, the YourName2.txt program that is distributed with this 8051 simulator employs a subroutine named "Draw1Char" to replace multiple occurrences of this same code in the more primitive YourName.txt example program.

To draw the four letters 'J', 'O', 'H', and 'N' on the electronic signboard, the YourName2.txt program makes four calls into the Draw1Char subroutine as shown below:

    MOV   A, #9*8   ; letter 'J' has the alphabet index 9
                    ; and hence the first column of font data
                    ; for the letter 'J' is 9*8=72 bytes beyond
                    ; the start of the font data (which is where DPTR
                    ; points) in program memory.
    LCALL Draw1Char
    MOV   A, #14*8  ; letter 'O' has the alphabet index 14
                    ; and hence its column data starts 14*8=112
                    ; bytes beyond where the DPTR points
    LCALL Draw1Char
    MOV   A, #7*8   ; letter 'H' has the alphabet index 7 and
                    ; and hence its column data starts 7*8=56
                    ; bytes beyond where the DPTR points
    LCALL Draw1Char
    MOV   A, #13*8  ; letter 'N' has the alphabet index 13
                    ; and hence its column data starts 13*8=104
                    ; bytes beyond where the DPTR points
    LCALL Draw1Char

You can see the four LCALL statements (the L refers to "long"). There is only one RET statement and it is located at the end of the Draw1Char subroutine which is not shown here. The RET statement is kind of magical: it manages to do different things at different times. That is, it returns program execution to four different places following the four LCALL statements seen above.

How does it do this? The secret is a memory structure known as the stack. The stack is merely a section of the on-chip data RAM which is accessed via the stack pointer (SP) SFR. At power-up the SP is automatically initialized to the value 7. Upon each call statement the program memory return address is pushed onto the stack. This return address is the program count (PC value) of the next instruction to be executed once the subroutine completes. Since all program memory addresses are 16 bits wide but the stack can only hold bytes, two separate bytes must be pushed onto the stack in order to preserve the 16 bit return address. The SP is incremented and the least significant byte (LSByte) of the return address is written to data memory at the location where the SP currently points. Then the SP is incremented again and the MSByte of the return address is pushed onto the stack. Once the return address has been safely set aside, the program counter is set to the start of the subroutine and execution continues there. All this activity is in response to the LCALL statement. Don't worry, the simulator will allow you to observe all this activity.

The last instruction in any subroutine must always be a RET. This instruction causes two bytes to be popped from the stack and re-assembled into the new value for the program counter. In this manner execution resumes just after the call statement that entered the subroutine.

To summarize, when any LCALL statement is executed the program counter value for the next statement after the LCALL is pushed onto the stack. Then the program counter is initialized to the beginning of the subroutine. When the RET statement is encountered at the end of the subroutine, the program counter is re-initialized with whatever address (the "return address") happens to be on the top of the stack. This may sound unnecessarily complicated but the use of the stack allows one subroutine to call into another subroutine. While the 8051 is executing inside such a nested subroutine the stack will hold two return addresses (4 bytes).

You won't get comfortable with this process until you have watched it happen while single stepping through code. So be sure to do this while you are studying the YourName2.txt or YourName3.txt example programs that are distributed with this 8051 simulator.

You are free to employ the stack to hold other variables as well and you don't have to be inside a subroutine to do this. The PUSH instruction first increments the SP and then copies a specified byte into data memory wherever the SP currently points. The POP instruction reads a value from data memory at the location where the SP currently points and then decrements the SP. Thus the pair of instructions, PUSH and POP, return the stack to its former condition.

These PUSH and POP instructions insist that you use direct addressing to identify the byte being saved or restored. But the 8051's hardware employs indirect addressing to reach the data memory locations pointed to by the SP. This means that as the stack grows it can go into the Upper 128 bytes of data RAM but not into SFR space. Since there is only 256 bytes of data memory, the stack cannot possibly grow larger than 256 bytes. If your program attempts to push more than 256 bytes onto the stack you will cause another famous bug: the stack overflow.

Now how do subroutines improve the quality of code? Imagine that instead of a single subroutine your program had the same code sprinkled about at multiple places. And then one day you realized this code had a bug. In this situation, fixing that bug may be easier than finding all the places where that faulty section of code resided. When you employ subroutines you know you have squashed the bug when you have repaired the subroutine; that is, you only have to go to one place in your program and make the fix there.

Subroutines have another benefit which was particularly important back in the early days of microprocessors when memory was always in short supply. If you replace a 20 byte section of code that had existed in 5 places in your program with a subroutine that is called 5 times you will save 64 bytes of program memory. You might have guessed that you would save 4 * 20 = 80 bytes but you save slightly less because you have to add 5 LCALL statements and 1 RET statement that you didn't have previously.

Incidentally, what assembly languages call a subroutine, high-level languages such as C, C++, or Java call a function or method.

Besides the use of a subroutine, there was something else new in the YourName2.txt example program. This other new feature might have escaped your attention because it just looked so natural. Back in the prior YourName.txt example program when we needed to load the accumulator with an offset beyond the start of the font data we used the instruction:

    MOV  A,#72     ; The letter 'J' has alphabet index 9 (indices always
                   ;   start from 0) and hence the first column of font
                   ;   data for the letter 'J' is 9*8=72 bytes beyond
                   ;   the start of the font data (which is where DPTR
                   ;   points) in Program Memory.

But in the new YourName2.txt example program this same line looks like:

    MOV  A, #9*8   ; letter 'J' has the alphabet index 9
                   ; and hence the first column of font data
                   ; for the letter 'J' is 9*8=72 bytes beyond
                   ; the start of the font data (which is where DPTR
                   ; points) in program memory.

This new feature is known as assembly-time math (when this feature appears in a high-level language such as C or C++ it is known as compile-time math). The multiplication of 9 by 8 is performed when the program is assembled. Hence the assembler performs this multiplication, not the 8051 microprocessor chip. The only way to get an 8051 microprocessor to perform a multiplication is via the MUL AB instruction. That type of multiplication occurs whenever the program is executed. In contrast, an assembly-time multiplication occurs once and only the result of the multiplication is embedded into the program. In other words, the two statements shown above produce exactly the same machine code.

So assembly-time math is simply a convenience to the programmer, hopefully preventing him from needing to reach for a hand calculator. The 8051 assembler allows you to specify an arbitrarily complex expression at any location where you must supply a constant value. These expressions are written using conventional algebraic notation such as (2+0x3)*4, etc. One thing to keep in mind though is that integer math is performed. Hence 11 / 4 = 2 not 2.75 because fractional results are truncated rather than rounded.


YourName2.txt

( 33 )
Previous Table Of Contents Next

The YourName2.txt Example Program

The YourName2.txt example program improves upon the earlier YourName.txt program by replacing repeated code with a subroutine. You call into a subroutine using the LCALL instruction and you return from the subroutine back to where you were originally executing using the RET instruction.

Load, assemble, and execute this program. Single-step through it watching the calls to the Draw1Char subroutine. Observe the changes in the stack pointer (the SP register visible in the middle-pane) and the contents of the stack. The stack is merely the section of data memory that starts at data memory address 8 and continues into larger addresses, as dictated by the SP.

When you write programs that have subroutines you necessarily employ the stack and this means there is a chance for conflict between the data memory locations your program employs and the ones employed for the stack. One thing in your favor with regard to this type of contention is that you can relocate the stack to a different portion of data memory. This is easily accomplished merely by writing a new value to the stack pointer. Since the Upper 128 bytes of data memory are more difficult to employ anyway (since they can only be accessed using indirect addressing), many 8051 programmers choose to locate the stack there. This is accomplished with the following statement:

     MOV  SP,#128

If you choose to do this, you must relocate the stack before the first call that your program makes to a subroutine.

After you have thoroughly studied this program you might want to modify it to scroll your own name. You will need to add or subtract calls to the Draw1Char subroutine if your name doesn't have 4 letters.

Since this is our first subroutine, we can now learn about the remaining toolbar buttons. The "Step Over" button ( [image]) functions identically to the "Step Into" button unless the current instruction is an LCALL (or ACALL). What does it mean to single-step a call statement? Clicking the Step Into button when the PC (program counter) is positioned on an LCALL statement will cause the 8051 simulator to execute the call and then halt on the first statement of the subroutine. This is called "single stepping into". Alternatively, clicking on the Step Over button when the PC is positioned on an LCALL statement will cause the 8051 simulator to execute the entire subroutine and then halt on the statement following the LCALL. This is called "single stepping over" and is also a form of single-stepping since we have executed a single statement, the LCALL statement. On any other statement the Step Over button will cause the same action as the Step Into button.

The "Step Out" button ( [image]) should be used only when the PC is positioned within a subroutine. Clicking on the Step Out button causes the simulator to continue executing however many instructions are necessary to come to the next RET statement. So after clicking on the Step Out button the 8051 will be halted just after the return from the subroutine.

Another feature of this Windows program that is particularly valuable for programs that employ subroutines is its tracing capability. By default, tracing is disabled. You can enable tracing by selecting the View/Options choice from the menu and then adding a checkmark to the Tracing checkbox. When tracing is activated, execution of your 8051 program will result in the creation of a log file documenting each and every instruction that was performed. This log file is a standard .TXT file that can be viewed with any word processor. The log file will be named TRACELOG.TXT and will be located in whatever folder your 8051.EXE program is in.

Go ahead and enable tracing and then restart and continue the YourName2.txt program. As soon as the name is fully visible on the signboard click on the Stop button on the toolbar ( [image]). You want to stop your program quickly because the:

Loop:  SJMP Loop

statement at the end of YourName2.txt will quickly cause thousands of lines to be added to the TRACELOG.TXT file and it will grow huge. Then take a look at this file (use Windows Explorer to find it and then double-click on it to coax Notepad to display it to you). I am assuming you learned how to read a trace file thanks to the RPN calculator portion of my curriculum.

Every time you restart your 8051 program the old TRACELOG.TXT file will be copied into TRACELOG_OLDER.TXT and a new TRACELOG.TXT file will be started (this also happens if your program runs for so long that TRACELOG.TXT grows excessively large). You can delete these files any time you want.

Occasionally while trying to read the TRACELOG.TXT file you may get the error message:

  The document TRACELOG.TXT is in use by another
  application and cannot be accessed.

All you need to do to avoid this error message is to either disable tracing (that is, again visit the View/Options dialog box) or else to return to the Compose Mode of the simulator before trying to view the trace file.


YourName3.txt

( 34 )
Previous Table Of Contents Next

The YourName3.txt Example Program

The YourName3.txt program is another of the skeletal example programs. You know the drill: you have to finish the program given only a skeleton of the necessary code. We want to modify the Draw1Char subroutine so that this subroutine is easier to call. In the YourName2.txt example program it was the programmer's responsibility to multiply an alphabet index (such as 0 for the letter A) by 8 to get a column index which was then passed to the Draw1Char subroutine. In the new YourName3.txt program we want to make use of the 8051's ability to multiply and thus allow the Draw1Char subroutine to be called with a straight alphabet index. That is, the first job of the new Draw1Char subroutine will be to multiply the value passed in the accumulator by 8 and thus, given an alphabet index, generate the corresponding column index (which is used as an offset beyond where the DPTR points).

The skeleton that I have provided you is complete except it still has the version of the Draw1Char subroutine used in the YourName2.txt program. You need to add a few new instructions at the start of this subroutine. Of course one of these instructions is MUL AB.

Once you have finished YourName3.txt we are going to take it to the next level by making it completely general purpose, able to scroll any message typed by an operator in real-time. The key to this technique will be the use of interrupts.


( 35 )
Previous Table Of Contents Next

Interrupts

The 8051 can respond to five different interrupts. An interrupt is an event that causes the 8051 program to deviate from its normal path in order to handle an unexpected event. For example, it would not be a good use of the microprocessor in the computer in front of you to program it to constantly interrogate the keyboard to see if you have pressed any keys. Instead, the microprocessor in your PC is busy doing other things, knowing that should you press a key on the keyboard an interrupt will be generated. In response to the interrupt, the microprocessor will set aside its current activity, branch to special code that deals with the interrupt, and then return to its original activity.

The five interrupt events that the 8051 can respond to are:

  • an overflow in the internal timer #0
  • an overflow in the internal timer #1
  • an electrical pulse applied to the 8051's "external interrupt #0" pin
  • an electrical pulse applied to the 8051's "external interrupt #1" pin
  • receipt or transmission of a byte via the serial port

When the 8051 first powers up, all of its interrupts are disabled. Each 8051 program can decide which of the five possible interrupts it wishes to enable. This is accomplished by writing to the IE ("Interrupt Enable") register, which is one of the many special function registers (SFRs):

Note that the IE register also offers a EA bit which is a "global interrupt enable". The EA bit must be set or else all five interrupts are disabled, regardless of the other bits in the IE register.

In the 8051, each of the five possible interrupts has been assigned a fixed location in program memory. Should an interrupt occur, the 8051 will immediately jump to that special program memory location where it commences execution of what is called the interrupt service routine (ISR). The ISR is whatever code you have written to deal with that particular interrupt.

Since there are 5 possible interrupts, there must be 5 reserved program memory addresses. These reserved program memory addresses are known as interrupt service vectors and "to take an interrupt" is sometimes called "vectoring to an interrupt". The 5 interrupt service vectors are:

  0x0003  is where the external interrupt #0 code must begin
  0x000B  is where the timer #0 interrupt code must begin
  0x0013  is where the external interrupt #1 code must begin
  0x001B  is where the timer #1 interrupt code must begin
  0x0023  is where the serial port interrupt code must begin

If any of these interrupts are never going to be enabled then their special program memory location can be employed for other purposes.

You will note that Intel left 8 bytes between these interrupt service vectors. If an interrupt service routine is short enough it can reside entirely within the 8-byte section of program memory reserved for it. But more typically a branch (a.k.a. jump) statement will be located at the interrupt service vector. The branch allows the rest of the interrupt service routine to reside anywhere in program memory and hence be of unlimited length. You can observe this arrangement in the SerialPort.txt example program distributed with this simulator (we will study this program next).

Upon powering up (or being released from reset) the 8051 always begins executing its program starting with the contents of program memory address 0. Hence the very first statement in an 8051 program will often have to be a branch (LJMP) statement to avoid the interrupt service vectors which start at program memory address 3.

Each interrupt source can also be individually programmed to one of two priority levels by setting or clearing a bit in the SFR named IP ("Interrupt Priority").

A low-priority interrupt can be interrupted by a high-priority interrupt, but not by another low-priority interrupt. A high-priority interrupt can’t be interrupted by any other interrupt source. If two interrupt requests of different priority levels are received simultaneously, the request of higher priority is serviced (accepted) first. If interrupt requests of the same priority level are received simultaneously, an internal polling sequence determines which request is first serviced.

We've talked about the IE SFR register that we must program to enable or disable each of the five possible interrupts. But where do the interrupts actually come from? Two of the five interrupts come from the 8051's two internal timers. Each time one of these timers overflows (that is, wraps around) a timer interrupt request is generated. It is the TF1 and TF0 bits within the TCON ("Timer Control") register that signify these interrupt requests:

As soon as your 8051 program accepts one of these interrupt requests (that is, branches into the interrupt service routine) the corresponding interrupt request is removed.

An interrupt request is just that. An actual interrupt (that is, the resetting of the program counter to the special interrupt service vector, 0x000B for Timer #0's interrupt) does not happen unless the particular interrupt is also enabled.

Another two of the 8051's five possible interrupts are caused when an electrical pulse is applied to a particular pin on the 8051 package. This Windows simulator does not offer a place to solder a wire but the View/Options choice on its menu bar allows you to specify the frequency of the simulated pulses that you want applied to these pins. Every time an "external interrupt #0" pulse is received the IE0 bit in the TCON register goes high (goes to 1) to signal an interrupt request. Similarly, every time an "external interrupt #1" pulse is received the IE1 bit in the TCON register goes high to signal an interrupt request. Whenever your 8051 program accepts one of these interrupt requests that request bit is cleared.

The final interrupt source is the 8051's serial port. This Windows simulator fakes receipt of a byte (character) over this serial port each time you press one of the alphabet keys on your computer's keyboard. This interrupt request is signaled by the RI bit in the SCON ("Serial Control") register going high. Unlike the other interrupt requests, the hardware does NOT automatically remove the serial port interrupt request. That is, the 8051 circuitry does not automatically clear the RI bit in the SCON register when this interrupt is accepted so your program must do that.

The 8051's stack is intimately involved with interrupts. The stack is merely a section of the on-chip data RAM which is accessed via the stack pointer (SP) SFR. At power-up the SP is automatically initialized to the value 7. Upon each interrupt the program memory return address is pushed onto the stack. This return address is merely the program count of the next instruction to be executed once the interrupt service routine completes. Since all program memory addresses are 16 bits wide, two separate bytes must be pushed onto the stack in order to preserve the return address. The SP is incremented and the least significant byte (LSByte) of the return address is written to data memory at the location where the SP currently points. Then the SP is incremented again and the MSByte of the return address is pushed onto the stack. This happens automatically before the 8051's hardware initializes the program counter (PC) to the program memory address (vector) associated with whatever interrupt was just accepted.

The last instruction in your interrupt service routine must always be a RETI. You can read the RETI mnemonic as "Return from Interrupt". The RETI instruction causes two bytes to be popped from the stack and re-assembled into the new value for the program counter. In this manner execution resumes wherever it had left off in order to take the interrupt.

The branch to the interrupt service routine is almost identical to a call to a subroutine. The main difference is that you don't provide the LCALL statement (it happens automatically when the interrupt occurs) and you use a RETI rather than a RET statement at the end of an interrupt service routine.

You won't get comfortable with this process until you have watched it happen while single stepping through code. And the SerialPort.txt example program that is distributed with this 8051 simulator gives you the perfect opportunity (we will study this program next).

Only the Program Counter is automatically pushed onto the stack, not the PSW or any other register. Most modern microprocessors automatically preserve additional registers on the stack when interrupts occur. But in the 8051 it is up to the programmer to push anything else that he wants preserved onto the stack. In the SerialPort2.txt example program we will see what happens when you don't preserve registers before executing your interrupt service routine.


SerialPort.txt

( 36 )
Previous Table Of Contents Next

The SerialPort.txt Example Program

This example program monitors the computer's keyboard and as each alphabetic key is depressed, paints the corresponding letter on the electronic signboard. Obviously, the 8051 program does not know when the operator will type on the keyboard or what letter he will type. But the 8051 does not sit around worrying about that. Instead it enables a serial port interrupt and then proceeds with other work.

When an interrupt occurs the normal progression of the 8051's program counter (PC) is interrupted. The current PC value is written to the stack so that we will have a mechanism to return to the current location in our program. And then the PC is set to the interrupt service vector for whatever interrupt has occurred. In the case of the serial port interrupt this interrupt service vector is 0x0023 and hence we must place our 8051 code that responds to keyboard keypresses at this location in program memory.

Everything that I have described happens automatically, thanks to the hardware built into the 8051. Your job as programmer is to write to the IE (interrupt enable) register to enable whatever interrupts you wish to receive and then to prepare interrupt service routines to respond to these interrupts. Each interrupt service routine must end with the RETI instruction which causes the return address to be popped back off the stack and used to restore the program counter to whatever value it had before the interrupt.

I said that the 8051 program could enable an interrupt and then proceed with other work. In the case of the SerialPort.txt example program this other work is the not very productive endless loop shown below:

    Loop:  SJMP  Loop

This looks a lot like the infinite loops we have seen in the past but this one should really be called semi-infinite because there is a way out: each occurrence of the serial port interrupt (or any other interrupt that we choose to enable) will get us out of this loop. And when the interrupt service routine is complete we will be returned right back into this loop. The basic structure of most of the 8051 programs used in industry can be described as "sitting in a loop waiting for an interrupt".

A more modern microprocessor would offer a mechanism whereby the chip could go to sleep while awaiting an interrupt. This is important for reducing battery consumption in hand-held devices.

I haven't explained how you can manage to place your interrupt service routines at the necessary location in program memory. Until now we have just strung program statements together and where they land in program memory depends only upon the code that preceded them. Hence we could employ some padding instructions (such as the do-nothing NOP instruction) to manage to align our interrupt service routines on the necessary interrupt service vectors (addresses).

But there is an easier way and it is provided by the ORG assembler directive. The ORG statement takes a single argument and causes the program counter to be set to this new value. I am talking here about the program counter used to assemble the assembly language program rather than the program counter that is active while the program is executing (some people refer to this assembly-time program counter as the location counter).

Let's consider an example program that consists solely of:

     MOV A,#1
     ORG 0x0023
     ADD A,#1

The assembler always begins populating program memory at program memory address 0. So the "MOV A,#1" instruction will be placed into program memory at addresses 0 and 1 (this instruction takes 2 bytes as you can observe from the listing file). Without the ORG statement the "ADD A,#1" instruction would be placed in the next available locations, which are program memory addresses 2 and 3 (this instruction also takes 2 bytes). But due to the ORG statement the "ADD A,#1" instruction will be placed at program memory addresses 0x0023 and 0x0024. This would be appropriate if the "ADD A,#1" instruction was the start of an interrupt service routine for the serial port interrupt.

Go ahead and load the SerialPort.txt example program into the simulator and assemble it. It has lots of comments so read them to better understand the program.

The SerialPort.txt is written in a defensive programming style. That is, it tries to protect itself from bad things that might happen. As an example, the program expects that it will never get the timer #0 interrupt or the timer #1 interrupt or the external interrupt #0 or the external interrupt #1 since it never enables these interrupts. But what if these interrupts happen anyway? This won't happen by accident but it can happen when someone else takes your program and modifies it without first understanding it. In response to one of these unanticipated interrupts the 8051 will vector to one of the reserved program memory locations and start to execute garbage. The 8051's behavior in this situation will be bizarre and will cause you a lot of embarrassment since the first and only time this will ever happen is the day your boss just happens to be watching over your shoulder.

To safeguard against this embarrassment, this SerialPort.txt program goes ahead and provides interrupt service routines for all of the possible interrupt sources. The interrupt service routines for the unexpected interrupts are infinite loops that will attempt to corral the runaway 8051. Many an engineer in industry has set a breakpoint on a supposedly unused interrupt service vector only to discover that his program was taking this interrupt in some bizarre situation he hadn't anticipated. You can try this technique in this Windows simulator since it supports breakpoints (read about them in the documentation for the WalkingTriangle.txt example program).

Now execute the SerialPort.txt program and then begin typing arbitrary alphabetic characters on your PC's keyboard. You should see the letters you type scroll across the signboard. Lower and upper case letters are treated identically and punctuation characters are ignored (I designed it this way because we don't have the signboard font data for the punctuation characters).

Obviously this program is managing to escape from the:

Loop:  SJMP Loop

statement that would otherwise keep the 8051 locked in a coma, unresponsive to external events. If you would like to single-step through the serial port interrupt's interrupt service routine then place a breakpoint on the 0x0023 interrupt service vector.

I still haven't discussed how the 8051 can acquire the character that was typed on the keyboard and hence caused the serial port interrupt. An actual 8051 package includes a few pins (leads) where you would solder the wires of a serial port connection to another device. Then when that other device issued a character across the serial port the 8051 would be interrupted and would find the newly received character in its SBUF special function register. So I wrote my Windows simulator to deposit the keypress character into the simulated SBUF register which is where your 8051 program can find it. There is (of course) one more complication: you must set the REN (read enable) bit in the SCON (serial port control) register before the serial port will receive anything. I decided to include this complication in my Windows simulator so that programs you write for it would be ready to run on real 8051 chips.

The final thing I want to point out is that the serial port's interrupt service routine (which is very similar to a subroutine) calls a subroutine. This means that while we are inside the Draw1Char subroutine the stack will hold 2 return addresses (4 bytes). On the top of the stack will be the subroutine return address (it's on top because it will be popped first) and below that the stack holds the interrupt service routine's return address. Watch the SP (stack pointer) and the data memory locations constituting the stack while this program runs.

Interrupts are mankind's best invention for how to minimize the workload on a machine while keeping it ready to respond to external events. Incidentally, the other alternative, writing a program that keeps checking over and over if a keypress has occurred, is called polling.


SerialPort2.txt

( 37 )
Previous Table Of Contents Next

The SerialPort2.txt Example Program

This example program adds the complication that the accumulator is doing other work while the 8051 waits for each serial port interrupt. The work we have it doing is scrolling a simple image (a diagonal line) across the signboard. Unfortunately, we will see that the call to the interrupt service routine damages the accumulator and hence damages the signboard image.

Go ahead and load, assemble, and execute this program. Before you type on your computer's keyboard you will observe a sequence of diagonal lines. We use the RL A instruction to create these diagonal lines. You can read the RL mnemonic as "Rotate the accumulator Left". As an example, if the accumulator initially holds:

  1 1 1 0 0 0 1 0

then after the RL A instruction it will hold:

  1 1 0 0 0 1 0 1

Note how the accumulator's most significant bit is not lost but instead is rotated all the way around to become the new least significant bit.

The SerialPort2.txt program generates a diagonal line by initializing the accumulator with the value 1 and then endlessly rotating this value. Hence the accumulator will see the following repeating sequence of values:

  0 0 0 0 0 0 0 1
  0 0 0 0 0 0 1 0
  0 0 0 0 0 1 0 0
  0 0 0 0 1 0 0 0
  0 0 0 1 0 0 0 0
  0 0 1 0 0 0 0 0
  0 1 0 0 0 0 0 0
  1 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 1
  0 0 0 0 0 0 1 0
 ...

You would not believe how many times some lucky engineer in Hollywood has been asked to write this program so that the lights on the deck of the Starship Enterprise would appear to be doing something.

Getting back to the SerialPort2.txt program, as soon as you press any key on your computer's keyboard this sequence of diagonal lights disappears and does not restart. The occurrence of the interrupt has broken our program. We can say that the background process (i.e., the rare activity) has interfered with our foreground process (i.e., the usual activity).

This problem occurs because both processes (sections of code) want to employ the accumulator. The 3 line section of code that is the foreground process:

Loop:   RL    A    ; loop while waiting for an interrupt
        MOV   P0,A
        SJMP  Loop

believes that the accumulator will forever hold one of the eight patterns we just listed. But this condition is violated when the background process (the serial port's interrupt service routine) initializes the accumulator with something else. Specifically, the serial port interrupt service routine has the following statement:

   MOV   A,SBUF  ; read alphabet index from serial buffer SFR

The solution is actually pretty simple, but I will wait to discuss it with the SerialPort3.txt program.


SerialPort3.txt

( 38 )
Previous Table Of Contents Next

The SerialPort3.txt Example Program

This example program solves the problem we observed in the prior SerialPort2.txt example program. The problem was that the foreground process (drawing a never-ending sequence of diagonal lines) was getting ruined the first time the background process (the serial port interrupt service routine) occurred.

Go ahead and load this program, assemble it, and execute it. You will observe the sequence of diagonal lines on the signboard. Now type a letter on your computer's keyboard. You will observe that the letter you typed gets scrolled across the signboard and the diagonal lines restart. Success!

Before I get into how I accomplished this (which is pretty easy) I want to explain how interrupts are asynchronous to the rest of an 8051 program. "Asynchronous" means that they occur unpredictably and at their own schedule. The foreground process will be interrupted at any point within the 3 statements of its loop. The first time you press a keyboard key the timing may be such that the 8051 is interrupted here:

Loop:  RL  A
       MOV   P0,A
       SJMP  Loop
Loop:  RL  A
  <your keypress occurs so we execute the serial
   port interrupt service routine here>
       MOV   P0,A
       SJMP  Loop

while the next time you press a keyboard key the timing may be such that the 8051 is interrupted here:

Loop:  RL  A
       MOV   P0,A
       SJMP  Loop
Loop:  RL  A
       MOV   P0,A
  <your keypress occurs so we execute the serial
   port interrupt service routine here>
       SJMP  Loop

You can observe this variability by looking closely at the diagonal lines on the signboard. Each time you type a letter the diagonal lines are broken at a different location.

I have already explained how the problem with the SerialPort2.txt program is that two different sections of code believe they have sole use of the accumulator. We need to rewrite the code so that these two sections of code will share the accumulator. The foreground process doesn't need to change. The right place to put the fix is in the background process (the interrupt service routine).

If you review the documentation on interrupts you will find the sentences:

Only the Program Counter is automatically pushed onto the stack, not the PSW or any other register. Most modern microprocessors automatically preserve additional registers on the stack when interrupts occur. But in the 8051 it is up to the programmer to push anything else that he wants preserved onto the stack.

We need to preserve the prior value in the accumulator before we use the accumulator inside the serial port interrupt service routine. We accomplish this by pushing the accumulator's prior value onto the stack. Find the following statement near the start of the serial port interrupt service routine in the SerialPort3.txt program:

  PUSH  ACC ; protect the prior value in the
            ; ACC since the interrupt service
            ; routine needs to use it

That one statement is half the fix. The other half of the fix is the following statement at the end of the serial port interrupt service routine:

  POP  ACC ; restore the prior value of the ACC

Thanks to this pair of PUSH and POP instructions the foreground process will never know that anybody else has messed around with the accumulator. As far as the foreground process is aware the accumulator never holds anything but the 8 values:

  0 0 0 0 0 0 0 1
  0 0 0 0 0 0 1 0
  0 0 0 0 0 1 0 0
  0 0 0 0 1 0 0 0
  0 0 0 1 0 0 0 0
  0 0 1 0 0 0 0 0
  0 1 0 0 0 0 0 0
  1 0 0 0 0 0 0 0

This use of the stack to preserve the accumulator's prior value means that while we are inside the Draw1Char subroutine the stack will be populated by 5 bytes. At the very top of the stack will be the 2 bytes which are the subroutine return address for the RET statement at the end of the Draw1Char subroutine. Below that on the stack will be a 1 byte value which will be returned to the accumulator. And below that will be the 2 bytes which are the interrupt service routine's return address.

Confused? Then let's take this slower. Place a breakpoint on program memory address 0x0023 in order to get a chance to start single stepping through the serial port interrupt service routine. Then restart the 8051 program and then press a letter on the keyboard. The breakpoint will halt the program. Observe that the SP (stack pointer) has already advanced from its default value of 0x07 to 0x09 by the time the breakpoint fires. This is because 2 bytes (the interrupt return address) have already been pushed onto the stack. You can verify this by observing that data memory locations 8 and 9 hold either:

  • the values 0x0B and 0x01 which when put back together form the PC return address of 0x010B,
  • the values 0x0C and 0x01 which when put back together form the PC return address of 0x010C,
  • the values 0x0E and 0x01 which when put back together form the PC return address of 0x010E,

The reason I had to list 3 possibilities here is that the serial port interrupt can occur in between any of the 3 statements in the foreground loop.

Now click the Step Into button once. This will bring you to the "PUSH ACC" statement. Observe that the SP = 0x09 prior to this instruction. Now single-step this instruction. Observe that the SP advances to 0x0A and data memory location 0x0A = 10 is populated with whatever the accumulator held (there are 8 possible values).

Now click the Step Into button once more. This will bring you to the "LCALL Draw1Char" statement. Then Step Into this subroutine. Observe that following the LCALL statement the SP has advanced to 0x0C = 12 since two more bytes have been pushed onto the stack. Data memory locations 0x0B = 11 and 0x0C = 12 now hold the values 0x1F and 0x01 since these values can be put back together to form the program counter value of 0x011F which is the address of the statement after the LCALL statement (look at the LOC column of the listing file to see this).

The stack now holds 5 bytes and that is as full as it ever gets while this program runs. Continue to single step the program and watch as the 8 columns of the letter you typed are written by the Draw1Char subroutine to the signboard. The RET at the end of Draw1Char then causes the SP to decrement back to 0x0A since the 2 bytes of the subroutine return address have been popped from the stack. It is these 2 bytes that control how the PC is re-initialized by the RET instruction. [If you want to screw up the 8051, right-click on the data memory location 0x0B or 0x0C and change its value prior to this RET. This will cause the RET statement to cause the 8051 to jump into hyperspace which is another common programming bug.] Note also that the 2 bytes of the return address are not removed from data memory: you can still see them at data memory addresses 0x0B and 0x0C. Nonetheless they are dead since a subsequent PUSH would overwrite them. Continue single stepping through the POP and the RETI statements and you will notice that the SP has been returned to its default value of 0x07. In other words, the 8051 is back in its default configuration, ready for the next interrupt.

Once you get comfortable with this type of activity you will be eligible for the title of "intermediate programmer".

You probably wonder why we used the stack to preserve the accumulator's prior value. That is, why couldn't we just write the accumulator's prior value to data memory location 37, for instance? This technique works but ONLY in the very simplest situations. It will not work when subroutines and interrupt service routines can nest (that is, call each other). If multiple subroutines (or even multiple calls to the same subroutine) all tried to employ data memory location 23 then we would again have contention. The stack is the simplest structure that has enough flexibility to handle ALL these situations. Since the top of the stack keeps moving there is always an available place to put the newest thing you are trying to preserve. The only downside to the stack is that you must ensure that it never runs out of room.


MazeSolver.txt

( 39 )
Previous Table Of Contents Next

The MazeSolver.txt Example Program

Okay, you've been working pretty hard. It's time for some fun. How about we create a robotic mouse and program him to wander about in a maze looking for cheese? Turns out this 8051 simulator supports this type of programming environment. All you have to do is to visit the "Options" dialog (you know, select View/Options from the menu), find the "Output Window" setting, and then request the Maze. This has the consequence that the pins (wires) on the 8051 microprocessor that are controlled by the P0, P1, P2, and P3 registers are reconfigured from their signboard settings:

to new settings appropriate for a robotic maze:

The 8051 microprocessor hasn't changed at all. But we are now connecting different circuitry to the 8051's general purpose input/output pins. We even get to decide which pins we will write (control) and which pins we will read (sense). This ability to employ the same microprocessor in many different circuits just by changing how you connect up the wires explains why millions of microprocessors are sold every year.

The next time you are in the simulator's Execute mode (perhaps you already are) you will observe that the bottom pane no longer holds an electronic signboard and instead now holds a maze. A mouse is visible in the upper left-hand corner and a piece of cheese is somewhere in the lower right quadrant. Every time you click on the Restart button on the toolbar ( [image]) your 8051 program will be reset to the top (meaning the program counter will be reset to 0), the mouse will be returned to the upper left-hand corner, and the cheese will be randomly placed somewhere in the lower right quadrant.

You are ready to begin the hunt. By writing to the P0 register you will be able to command the mouse to move. At any moment, there are potentially 4 directions the mouse can move in a maze: left, right, forward, or backward.

  • To move left write the value 0x00 to P0.
  • To move forward write the value 0x01 to P0.
  • To move right write the value 0x02 to P0.
  • To move backward write the value 0x03 to P0.
  • Writing any other value to P0 causes no motion.

Note that because we reconfigured the simulator, the P0 register (or more properly the input/output pins on the 8051's package controlled by the P0 register) is no longer connected to the signboard but is now connected to a motorized mouse.

Any of these 4 directions might be blocked by a wall. If you command the mouse to crash into a wall you will hear a beep and the mouse will stay where he is. You can learn which walls currently surround the mouse by reading from the P1 register. The bits within the byte read from P1 have the following meaning:

 MSBit  7     6     5     4     3      2      1      0  LSBit
     cheese  not   not   not   rear   right  front  left
             used  used  used  wall   wall   wall   wall

As an example, if you read the value 0x05 (00000101) from P1 then at the mouse's current position there is no cheese and there is a wall on both the left and right but no wall to the front or rear.

Note that wall sensing (bytes read from P1) and motion commands (bytes written to P0) employ the point of view of the mouse. This point of view (or "reference frame") keeps changing as the mouse explores the maze. For example, each time you command the mouse to move left, the mouse will both move to the square on his left and rotate 90 degrees counter-clockwise. Hence what used to be to his left is now to his front. The icon of the mouse rotates to remind you that the reference frame keeps changing.

We now need to learn how to interrogate the individual bits within the byte we read from P1. This is a good application of the 8051's bit-addressable bits which we will study next. But in the meantime we will employ standard Boolean logic to interrogate these bits. While only some microprocessors have bit-addressable bits, all microprocessors can perform Boolean logic.

Boolean logic was named in honor of George Boole, an Englishman who in the mid 1800s simplified and standardized logic by expressing it algebraically, using equations. The three basic Boolean operations are quite easy to understand since all humans use them on a daily basis to reason. These three basic Boolean operations are the AND, OR, and NOT operations (the NOT operation is also called the COMPLEMENT operation). The AND and OR operations require 2 operands which are both single bits. The NOT operation requires a single operand which is a single bit.

The truth tables below show the AND, OR, and NOT operations. From these truth tables you should understand why these operations were given the names they have.

  inputs  AND       inputs   OR      input   NOT
        -------           -------          ------
   00  |   0         00  |   0        0   |   1
   01  |   0         01  |   1        1   |   0
   10  |   0         10  |   1
   11  |   1         11  |   1

In English, the AND of bit1 and bit2 is a 1 only if both bit1 and bit2 are one. The OR of bit1 and bit2 is a 1 if either bit1 or bit2 is a one. The COMPLEMENT of bit1 is 1 if bit1 is 0, and 0 otherwise.

George Boole didn't think in terms of bits, of course, but he employed these same rules for reasoning about statements. Each statement could only be true or false, and these two possibilities match up perfectly with bits, which have only two possible states: 0 or 1. Here are some examples of how George Boole would have used his Boolean operations to reason about microprocessors:

  • All microprocessors provide a way to store the steps of an algorithm (program memory) AND a way to store the operands affected by the algorithm (data memory).
  • A jump is either conditional OR it is unconditional.
  • The LJMP statement is NOT a conditional jump, hence it is an unconditional jump.

Getting back to our maze, the mouse should move to a new position if he is NOT already on the cheese. So we need our 8051 program to perform a conditional jump based upon the most significant bit of the value read from P1. One way to accomplish this is shown below:

     MOV  A,P1          ; read the current status
     ANL  A,#0x80       ; check for presence of cheese
     CJNE A,#0x80,Move  ; if no cheese then move
Eat: SJMP Eat           ; don't move, we're on the cheese
Move:                   ; we need to go somewhere else

This code employs the ANL instruction which performs the logical AND function on the 8 bits of its operand and the 8 bits of the accumulator and places the result back in the accumulator. Let's imagine that the mouse had already landed on the cheese and the value read from the P1 register was 0x8C (10001100) meaning "there's cheese and a wall to the right and rear". The command "MOV A,P1" then places the byte 0x8C into the accumulator. The ANL statement ANDs the accumulator with the byte 0x80. This is performed in binary below. Note how every bit is ANDed with its corresponding bit in the other operand.

     10001100    ; 0x8C, the value read from P1
     10000000    ; 0x80 the "mask" byte
 AND --------
     10000000    ; the result

The value 0x80 used in the command "ANL A,#0x80" is called a mask byte because it is used to "mask out" all but the bits we are interested in. In this case we want to learn about the presence of cheese and hence we are only interested in the most significant bit. You can observe that there is only 1 bit asserted (equal to 1) in the byte 0x80. Thanks to the properties of the AND operation, if we AND 0x80 with any other byte then the result will definitely have 7 zeroes since 0 ANDed with either a 0 or a 1 will always produce a 0 (look again at the AND truth table if you don't understand this).

This is illustrated below where I have used an 'x' to represent those bits that don't matter (that is, the outcome is the same regardless if these bits are a 0 or a 1).

Either we read        0xxxxxxx  or we read 1xxxxxxx from P1.
When we then AND with 10000000             10000000
we get                00000000      or     10000000

7 of the bits read from P1 don't matter (and hence are labeled 'x') because we AND these bits with 0 which is guaranteed to clear these bits. So when we AND the byte read from P1 with the mask byte 0x80 there are only two possible outcomes: the value 0x80 if we are on cheese and the value 0x00 if we are not. In the illustration above we read 0x8C from P1 and the AND produced the value 0x80 indicating that we are on cheese.

This is progress, since we can now conclude that the mouse is on the cheese if and only if the value read from P1 when ANDed with 0x80 equals 0x80. How can we decide whether the accumulator holds 0x80? This is the job for the CJNE instruction that compares its first two operands and then takes the conditional jump only if these first two operands are not equal. So scroll back up and look at that snippet of 8051 code and observe that once the mouse lands on the cheese he will forever stay on the cheese because the CJNE command will not take the jump to the "Move" label and instead the 8051 will fall into the infinite loop:

Eat:  SJMP Eat

When the CJNE statement does branch to the "Move" label we enter the portion of the program that decides whether to move left, right, forward, or backward. Look at the program and see how the same ANL/CJNE combination is used to check in turn for the presence of the front, left, right, and rear walls. As soon as the mouse realizes that one of these walls is missing he moves that direction. That is, he will when you provide the missing statements that write to the P0 register and actually command the mouse to move. That's right! This is another of the skeletal programs that you must complete. Once you have finished the MazeSolver.txt example program assemble it and then run it. You probably won't be impressed with the mouse's hunting skills but we will take care of that in the next example program.

Remember that you must visit the Options dialog (i.e. select View/Options from the menu) and request the maze Output Window before executing the MazeSolver.txt example program.


( 40 )
Previous Table Of Contents Next

Bit Addressable Bits

The designers of the 8051 wanted their chip to excel at bit twiddling which is the manipulation of individual bits. The reason for this is that the 8051 would be sold to engineers needing to command switches which in turn command motors and lights, etc. One bit can control one switch since they both have exactly two possible states.

So the Intel engineers gave the 8051 special provisions to deal with individual bits. The width of each bit address was set at 8 bits and since 8 bits can represent 256 different values there are 256 bit-addressable bits.

We have already discussed program memory addresses and data memory addresses and now we have bit addresses. This gets confusing because bit addresses point to objects within data memory and within the special function registers (SFRs). The Intel engineers decided to allot 128 of the 256 possible bit addresses to data memory and the other 128 bit addresses to the SFRs. 128 bit addressable bits fit within 16 bytes since 16 * 8 = 128.

The Intel engineers selected the data memory address range from 0x20 through 0x2F (which is 16 bytes) as being the location which would hold the bit addressable bits that were located in data memory. This bit-addressable section of data memory is identified in the illustration here. The least significant bit (LSBit) of the byte at data memory address 0x20 has the bit address of 0. The next least significant bit of this same byte has the bit address of 1. The least significant bit of data memory address 0x21 has the bit address 8. And the most significant bit of data memory address 0x2F has the bit address 127.

You don't need to remember these bit addresses as most assemblers will accept either of the two notations used below. The following code snippet sets and then clears the very first bit addressable bit, which is the least significant bit of data memory address 0x20:

    SETB  0
    CLR   0x20.0

I prefer the second notation since it reminds me that this bit-addressable bit is located within a particular byte of data memory (this notation is also a great lead-in to the "structs" used in the C and C++ languages).

Sixteen of the registers in SFR space are also bit-addressable. The bit-addressable SFRs are those whose hex address ends in 0 or 8. So the TCON register, having a hex address of 0x88, is bit-addressable but the TMOD register, having a hex address of 0x89, is not. The least significant bit of the P0 register has the bit address 128. The next least significant bit of the P0 register has the bit address 129. You don't need to remember these bit addresses as most assemblers will accept either of the two notations used below. The following code snippet sets and then clears the least significant bit of the P0 SFR register:

    SETB  128
    CLR   P0.0

Again, I prefer the second notation since it makes clear where the bit-addressable bit is located.

So let's take a look at an example program that employs the bit-addressable bits. This is the purpose of the MazeSolver2.txt example program which we will study next.


MazeSolver2.txt

( 41 )
Previous Table Of Contents Next

The MazeSolver2.txt Example Program

This example program is intended to illustrate usage of the bit-addressable bits. It is a rewrite of my solution for the MazeSolver.txt skeleton program.

Go ahead and load, assemble, and execute the MazeSolver2.txt program. Remember that you must request the maze Output Window before running the program by selecting View/Options from the menu.

What you notice with this program is that the mouse makes some initial progress through the maze but then becomes trapped in a certain section of the maze. This is a failure of the algorithm that we are using to navigate through the maze. The algorithm embedded in the MazeSolver2.txt example program is:

  • If you are on the cheese, then stay there.
  • Otherwise, if you can move forward, then move forward.
  • Otherwise, if you can move left, then move left.
  • Otherwise, if you can move right, then move right.
  • Otherwise, if you can move backwards, then move backwards.
  • Otherwise, complain about being placed in a defective maze.

Study the section of the maze where the mouse gets trapped and convince yourself that this algorithm does account for the mouse's behavior in the bad section of the maze. Programming gives you lots of opportunities to improve your reasoning skills!

Since the MazeSolver2.txt example program is defective, I have labeled it a skeleton program. Your assignment is to modify this program such that the mouse makes it to the cheese. The necessary modification is almost trivial, and it is surprising that such a subtle change in the algorithm would have such major benefits. As usual, no peeking at the solution I have provided until your keyboard is wet with sweat that has dripped off your fevered brow. I'll give you one hint: no new statements are required. Instead you only need to rearrange the order that the various possibilities are checked.


( 42 )
Previous Table Of Contents Next

Standard Operations

In this section I present some of the idioms of programming. These are the situations that occur again and again no matter what language you are programming in. I will also begin to expose you to C, C++, and Java so you can see these same idioms in a high-level language.

Loop Control (Iterating)

The DJNZ instruction ("Decrement and Jump if Not Zero") is intended to control a loop. It can be used with either direct or register addressing. To use DJNZ with direct addressing select a particular data memory location that will hold an integer that will decrement during each iteration of the loop. For example, we might select:

LoopControl   EQU   16

To execute a loop N times, initialize this data memory location with N and terminate the loop with a DJNZ instruction that branches back to the beginning of the loop, as shown below for N = 10.

      MOV LoopControl,#10
Loop:
      <put some code here>
      DJNZ LoopControl, Loop

We first saw this type of code construct in the WalkingTriangle.txt example program.

If we were programming in the C, C++, or Java languages we would write this same sliver of code as:

     for ( i=10, i > 0; i = i-1 )
     {
         <put some code here>
     }

meaning "initialize the variable i with the value 10 then repeatedly execute the code between the braces, decrementing the value of i once per iteration and stop when i is no longer greater than 0".

I am going to continue showing you the high-level language version of each of the standard operations we discuss in this section in order that you can begin to understand the difference between a low-level language like assembly (where the programmer must dictate every minute detail of how the steps are to be performed) and a high-level language (where the compiler chooses many of the implementation details).

The CJNE (Compare and Jump if Not Equal) instruction can also be used for loop control. This instruction takes 3 operands, the first 2 of which are bytes that get compared. The jump (by an amount controlled by the third operand) is taken only if the first two operands are not equal. CJNE also offers a number of addressing modes, and just to be different we will employ register addressing in this example. Furthermore, we will iterate through a loop 10 times using a loop control agent (R1) which starts at 10 and proceeds upward to 20:

      MOV  R1,#10     ; initialize the loop counter
Loop:
      <put some code here>
      INC  R1         ; increment the loop counter
      CJNE R1,#20,Loop

We first saw this type of code construct in the Train2.txt example program.

Relational Comparisons

All high-level languages allow you to compare variables and then execute code only if the comparison produces a certain result. For example, in the C, C++, or Java language we could write:

    if ( Sum > 5 )
    {
        print( "The value of Sum is greater than 5." );
    }

The print() statement causes its argument (which is basically a sentence) to be printed on the standard output device. Hence this program would announce that "The value of Sum is greater than 5" if the value of Sum was greater than 5.

To perform this same function in 8051 assembly language you use the CJNE instruction. For example:

Sum     EQU    16 ; choose a data memory location to hold the Sum

        CJNE  Sum,#5,NotEqual ; compare Sum to the number 5
Equal:            ; If we get here then Sum = 5
        <put some code here>
        SJMP  Ahead
NotEqual:         ; If we get here then Sum != 5.
                  ; There are now 2 possibilities:
                  ; Sum < 5 or Sum > 5.  We resolve
                  ; this question using the JC instruction.
        JC    LessThan
GreaterThan:      ; If we get here then Sum > 5
        <put some code here>
        SJMP  Ahead
LessThan:         ; If we get here then Sum < 5
        <put some code here>
Ahead:

The above code depends upon the fact that while comparing its first two operands for equality, the CJNE instruction also sets the Carry (C) bit if the unsigned integer value of its first operand is less than the unsigned integer value of its second operand and CJNE clears the Carry bit otherwise. So we can use the JC instruction to interrogate the Carry bit following the CJNE instruction.

There are 3 locations in the above code snippet labeled:

     <put some code here>

These 3 locations allow us to detect the 3 possibilities: Sum = 5, Sum < 5, and Sum > 5. In a high-level language we could write:

    if ( Sum > 5 )
    {
        <put some code here>
    }
    else if ( Sum < 5 )
    {
        <put some code here>
    }
    else  ; Sum must equal 5
    {
        <put some code here>
    }

A low-level language usually requires more statements and more typing to accomplish a given goal. You might then wonder why anyone would choose to program in a low-level language. If you are aiming to produce the smallest and fastest program possible then you have to use assembly language. It gives you total control over the microprocessor and anything that is done in a wasteful or non-optimal manner will be your fault. When you use a high-level language you are asking a computer program (called the compiler) to look at your statements and flesh them out with all the necessary details. Since computer programs are not yet as smart as humans, the compiler will get the job done but will end up doing some things in a non-optimal manner. Hence the resultant program will typically be a little bigger and run a little slower than it really needs to.

The usual rule-of-thumb is that a compiler-generated program is maybe 10 % larger and 10 % slower than a hand-crafted assembly language program (these numbers apply to C and C++, Java programs run much slower). Because modern computers are so fast anyway, you generally won't mind the slight performance penalty that accompanies the use of a high-level language. And you can certainly finish your program faster if you employ a high-level language. Therefore most programming is done in high-level languages.

Some schools teach programming to beginners by starting right in on high-level languages. However I believe that many of the things you see in high-level languages will be mysterious and hence irksome if you haven't been first exposed to low-level programming. The architecture of the machine is only exposed to the low-level programmer. The high-level programmer doesn't know whether there is or is not an accumulator in the machine and whether it is 8 or 16 or 32 bits wide. He doesn't know how much program memory or how much data memory are available. He doesn't know which addressing modes the machine supports.

In particular, the high-level programmer doesn't know how long each statement that he writes will take to execute, since he doesn't know which assembly language statements the compiler will select to accomplish his high-level statement (if he wants to find out he can inspect the listing file produced by the compiler). When you program in assembly you lay out the exact sequence of operations you want the machine to perform. The documentation for the microprocessor lists exactly how many clock cycles are required to complete each assembly language operation. By adding up these timings you can know the precise execution speed of your program. When you write a program to control an elevator you can be sloppy with time and use a statement such as "activate the pulley and then wait for the car to arrive at the next floor". But when you write a program to control the firing of the spark plugs in an internal combustion motor you must know that your code can keep up with the sequence of events as they happen in real time.

The Switch Statement

Another common idiom seen in all high-level languages is the "switch" statement. Consider the following code (which is legal in C, C++, and Java):

    switch (i)
    {
       case 0:
           print("i=0");
           break;
       case 1:
           print("i=1");
           break;
       case 2:
           print("i=2");
           break;
       default:
           print("i is not 0, 1, or 2");
    }

The 8051's JMP @A+DPTR instruction provides this type of functionality. The JMP @A+DPTR instruction causes the 8051's program counter to jump to a new value given by the sum of the accumulator's current value and the data pointer's current value. The DPTR happens to be 16 bits wide and the ACC is 8 bits wide but the 8051's hardware can still sum them together. Prior to the JMP @A+DPTR instruction you initialize the DPTR to point to the beginning of a sequence of SJMP statements and then you initialize the ACC with a value indicating which of the short jumps you wish to take. For example:

Index    EQU     16

       MOV  DPTR,#JumpTable
       MOV  A,Index   ; the value of Index determines
                      ;   which SJMP we will take
       RL   A         ; ACC now holds 2 * Index
       JMP  @A+DPTR   ; branch to 1 of the SJMP statements
                      ;   below based on the Index
JumpTable:
       SJMP Case0
       SJMP Case1
       SJMP Case2

Case0: <put some code here>
       SJMP Ahead
Case1: <put some code here>
       SJMP Ahead
Case2: <put some code here>
       SJMP Ahead

The RL A instruction converts the index value (which in this case must be 0, 1 or 2) to an even number in the range 0 through 4. This is necessary because each of the SJMP entries in our jump table is 2 bytes long. Using a 1 bit left shift to accomplish a multiply by 2 is a clever programming trick. Note that the assembly language version of this switch idiom will mess up if the variable Index has any value other than 0, 1, or 2. Programming is all in the details!

Multiplication

The MUL AB instruction multiplies the unsigned 8 bit data in the Accumulator by the unsigned 8 bit data in the B register. When an 8 bit value is multiplied by another 8 bit value, a 16 bit product results. As the 8051 is a byte oriented machine, this presents a small problem. The solution is that the most significant byte (MSByte) of the 16 bit result is placed into the B register and the LSByte of the result is placed into the Accumulator. If you happen to know that the result will always be less than 256 then your code only needs to deal with the portion of the product placed into the accumulator.

Control of Individual I/O Pins

Imaging that you have the need to cause one of the bit-addressable special function register bits to match the current state of one of the bit-addressable data memory bits. This need is met with the following code:

       MOV C,0x20.0
       MOV P0.0,C

The above code employs the Carry bit as a temporary way station for the 1 bit value being copied to the I/O pin.

Exclusive-OR

The 8051's instruction set offers AND and OR operations that can operate upon single bits (see ANL and ORL). But the 8051 does not offer an XOR (exclusive-OR) operation that can act on single bits. If you are not familiar with the exclusive-OR (XOR) operation, it is summarized in the following truth table:

    Bit1   Bit2        XOR
   ----------------|---------
     0      0      |    0
     0      1      |    1
     1      0      |    1
     1      1      |    0

Note from this truth table that if Bit2 = 0, then (Bit1 XOR Bit2) equals Bit1. And if Bit2 = 1, then (Bit1 XOR Bit2) equals the complement (inverse) of Bit1. These facts will be employed in the following 8051 code that accomplishes the XOR of two bit-addressable bits:

    MOV  C,0x20.0      ; Bit1
    JNB  0x20.1,Ahead  ; Bit2
    CPL  C
Ahead:        ; when we get here C holds (Bit1 XOR Bit2)

Note how Bit1 is moved to the Carry bit. If Bit2 = 0, then Bit2 XORed with any bit is just the original value of that other bit. So in this case C now contains the correct result. That is, (Bit1 XOR Bit2) = Bit1 if Bit2 = 0. On the other hand, if Bit2 = 1, then we need to complement the other bit in order to compute (Bit1 XOR Bit2). This is accomplished by the CPL instruction.


( 43 )
Previous Table Of Contents Next

Signed and Unsigned Numbers

Our next example program involves audio samples which are signed numbers, meaning that both positive and negative values are allowed. Quiet is represented by the audio reading of 0. Vibrations are represented by audio readings which oscillate to either side of 0, just as the loudspeaker cone displays excursions on either side of its rest position.

Previously, we have always said that 8 bits could represent the 256 values from 0 to 255. We were obviously assuming that these 8 bits would be interpreted as representing an unsigned value. But it is also common to need to represent signed values such as -3 and +3. Audio samples will be represented by signed bytes, which are still 8 bits wide but give up the larger positive values in order to now include negative values.

One of the oldest ways for representing negative numbers is called sign-magnitude notation. With this approach the most significant bit is treated differently than all the other bits. All the other bits continue to have a column value that is a factor of 2 but the most significant bit is now understood to simply be a flag: if this bit is 1 then the number is negative, otherwise the number is positive. A sign-magnitude example is given below:

   00000011 = 0 0000011 = + 3
   10000011 = 1 0000011 = - 3  in sign-magnitude representation

Sign-magnitude has some nice properties, such as the fact that we can easily negate any value simply by flipping the most significant bit. But it has the drawback that we can't perform arithmetic, as shown below where we try to add +3 and -3 and are disappointed to not get 0:

   00000011  = + 3
   10000011  = - 3
  ---------    ---
   10000110  = - 6

Another standard way of representing negative values is called one's complement. To produce the one's complement of any positive number you simply flip every bit. This is illustrated below:

   00000011 = +3
   11111100 = -3   in one's complement representation

One's complement has the unusual property that there are two ways to write 0: +0 and -0.

   00000000 = +0
   11111111 = -0   in one's complement representation

But at least we can add +3 and -3 and get 0 (try it, you'll see that you actually get -0 but that's still correct).

The most common way of representing negative values is called two's complement. To produce the two's complement of any positive number you flip every bit and then add 1. This is illustrated below:

    00000011 = +3
    11111101 = -3   in two's complement

Two's complement has only a single way to write 0 but has the peculiar property of being able to represent one more negative value than positive value. This is seen below where I show all of the possible 4 bit wide two's complement numbers:

    0111 = +7
    0110 = +6
    0101 = +5
    0100 = +4
    0011 = +3
    0010 = +2
    0001 = +1
    0000 = 0
    1111 = -1
    1110 = -2
    1101 = -3
    1100 = -4
    1011 = -5
    1010 = -6
    1001 = -7
    1000 = -8

8 bit wide two's complement numbers can range from -128 to +127. It is this asymmetry that causes the 8051's SJMP instruction to be able to jump forward by up to 127 bytes and backward by up to 128 bytes.

Note that regardless of whether we employ sign-magnitude, one's complement, or two's complement representation we can detect that a number is negative by inspecting its most significant bit.

In the upcoming PeakDetect.txt example program we will want to compute the peak amplitude seen in the sequence of samples coming from the left and right audio channels. This "peak amplitude" is the largest excursion from quiet (which is a reading of 0), and is an unsigned (that is, always positive) value. As an example, consider the following sequence of left channel samples:

  LeftSample   excursion from quiet   new LeftPeak
  ----------   --------------------   ------------
      0                 0                  0
      4                 4                  4
      3                 3                  4
      7                 7                  7
      0                 0                  7
     -5                 5                  7
     -8                 8                  8

We see that the "excursion from quiet" is just the absolute value of the sample. Some microprocessors have an absolute value operation built in but the 8051 does not. We can compute the absolute value by negating the sample when we detect that the sample is negative. To "negate" a signed number means to take its two's complement. The 8051 does not have an instruction that computes the two's complement. However the 8051 does have the CPL instruction which computes the one's complement. You get the one's complement by complementing each individual bit. You get the two's complement from a one's complement by adding 1. We can accomplish these two steps using the following 8051 instructions:

   CPL  A
   INC  A

In the table below we apply this two step sequence to every possible negative value:

  original number    ones complement    twos complement
  ----------------   ---------------    ---------------
    -1 = 11111111    00000000 = 0       00000001 = 1
    -2 = 11111110    00000001 = 1       00000010 = 2
    -3 = 11111101    00000010 = 2       00000011 = 3
     ...
  -127 = 10000001    01111110 = 126     01111111 = 127
  -128 = 10000000    01111111 = 127     10000000 = -128

We can see that the two step sequence:

   CPL  A
   INC  A

correctly negates all negative numbers between -1 and -127. This two step sequence fails only on -128 but this is because it is impossible to express +128 as a signed 8 bit number.

We have a couple of choices for dealing with the troublesome reading of -128:

Choice #1

We could just ignore the possibility of a reading of -128, since this reading indicates that the audio waveform is clipping which sounds so lousy that no commercially prepared .WAV file would have this defect.

Choice #2

We could employ the one's complement instead of the two's complement in order to negate the negative samples. This would mean that our peak detector would be in error on the negative samples by 1 part in 256 which is an error less than .5% and hence not very significant.

Choice #3

We could employ special program logic to detect a reading of -128 and convert it into an "almost right" excursion from quiet value of +127.

I will show the 8051 code for all 3 choices shortly. But first, consider the problem of detecting a negative sample (number) in the first place. If a signed byte is negative then its MSBit (most significant bit) will be a 1. The 8051 allows us to test the MSBit of the accumulator using the JB and JNB instructions. In the code below, we employ the JNB instruction so that when the accumulator holds a positive number we can "jump around" the code that negates the value in the accumulator. To specify that the JNB instruction should branch based on the MSBit of the accumulator we use the notation "ACC.7", referring to the 7th bit from the right. Remember that the notation "ACC.7" depends upon the accumulator being bit-addressable.

I now show the 8051 code that accomplishes Choice #1 which was to just ignore the possibility of a reading (sample) of -128:

; We need two data memory locations to hold the left
; and right "excursions from quiet" so we arbitrarily
; choose the following data memory addresses:

LeftSample   EQU   18        ; excursion from quiet
RightSample  EQU   19

        MOV   A,LeftSample
        JNB   ACC.7,ItsPositive   ; test the MSBit
ItsNegative:                 ; so we need to negate the value
        CPL   A              ; form the "one's complement"...
        INC   A              ; ...and then add 1 to form "two's comp"
        MOV   LeftSample,A
ItsPositive:                 ; at this point LeftSample will hold the
                             ;   "excursion from quiet" which is an
                             ;   unsigned (i.e., positive) number.

Show below is the 8051 code that accomplishes Choice #2 which is to employ the one's complement to form the absolute value of any negative number. With this choice, a reading of -128 becomes +127 (which is as good as we can do) and a reading of -127 becomes +126 (which is close enough):

       MOV   A,LeftSample
       JNB   ACC.7,ItsPositive   ; test the MSBit
ItsNegative:              ; so we need to negate the value
       CPL   A            ; form the "one's complement"
       MOV   LeftSample,A
ItsPositive:              ; at this point LeftSample will hold the
                          ;   "excursion from quiet" which is an
                          ;   unsigned (i.e., positive) number.

Shown below is the 8051 code that accomplishes Choice #3, which includes special logic to detect and deal with a reading of -128:

       MOV   A,LeftSample
       JNB   ACC.7,ItsPositive   ; test the MSBit
ItsNegative:                 ; so we need to compute the "two's comp"
       CJNE  A,#0x80,NormalNegativeNumber
ItsMinus128:
       MOV   LeftSample,#127 ; our approximation to +128
       SJMP  ItsPositive
NormalNegativeNumber:
       CPL   A             ; form the "one's complement"...
       INC   A             ; ...and then add 1 to form "two's comp"
       MOV   LeftSample,A
ItsPositive:               ; at this point LeftSample will hold the
                           ;   "excursion from quiet" which is an
                           ;   unsigned (i.e., positive) number.

As you can see, Choice #3 doubles the amount of code that we must write and, worse, doubles the amount of code that must execute upon receipt of each audio sample.

I decided to use Choice #1 in the PeakDetect.txt example program that we are now ready to study.


PeakDetect.txt

( 44 )
Previous Table Of Contents Next

The PeakDetect.txt Example Program

Now that you have mastered the maze we can move on to the example program that perhaps most closely resembles the 8051 programs used in industry. This is because this program is entirely interrupt driven. Based upon interrupts that signal the passage of time, the PeakDetect.txt program will sample an arbitrary song and display its volume peaks on an LED bar graph. Any song may be employed as long as it resides in an uncompressed .WAV file on your computer. I have provided a few sample .WAV files but you can download others from the Internet or else rip them from your favorite music CDs.

Before running the PeakDetect.txt example program you must visit the Options dialog and request the "Peak Detector" output window which is shown below:

The figure below illustrates how the 8051's general purpose registers are employed when the simulator is configured for the peak detector:

In the PeakDetect.txt example program the 8051 will enable 2 interrupts: the external interrupt #0 (EXT0) and the external interrupt #1 (EXT1). These two interrupts are generated by electrical signals arriving on wires connected to the 8051's package. By offering these 2 wires the Intel engineers provided a mechanism whereby an 8051 program could be influenced by external events. The only requirement was that some sort of external circuitry could generate an electrical pulse in response to these events.

When you select the "Peak Detect" output window this simulator connects the external interrupt #1 to a signal that indicates that a new audio sample is available. Hence the 8051 is alerted to the arrival of each audio sample. In response to this event the 8051 program must read the latest left-channel audio sample from P0 (one of the special function registers) and read the latest right-channel audio sample from P1. It does not matter whether you read from P0 before P1 or vice versa. As the 8051 continues to observe audio samples it remembers the largest sample (peak) that it has seen for both the left and right audio channels. By "peak" I mean the largest unsigned excursion from quiet observed in the audio waveform. This unsigned excursion can be computed as the absolute value of the samples, as described in the prior section.

Another consequence of selecting the "Peak Detect" output window is that the simulator connects the external interrupt #0 to a signal that indicates that it is time to update the LED bar graph. This external interrupt #0 occurs much less often than the external interrupt #1 in order that the LED bar graph can indicate the max audio sample observed during thousands of samples. Specifically, the external interrupt #0 interrupt occurs 8 times per second. In contrast, the rate of the external interrupt #1 interrupt will be thousands or tens of thousands of times per second. I can't list an exact number because this rate depends upon the sampling frequency inherent in the particular .WAV file you select to play. But the point is, after observing about a thousand audio samples the 8051 program should update the LED bar graph with the value of the largest sample seen in the latest batch of audio samples.

Each time the external interrupt #0 interrupt occurs the 8051 writes the detected left-channel peak reading to port P2 and the detected right-channel peak reading to port P3. The 8051 MUST write to P2 before P3 because I designed things such that it is the act of writing to P3 that causes the most recent values written to both P2 and P3 to be transferred to the LED bar graph display. This way, both the left and right channels of the bar graph are updated simultaneously even though the 8051 microprocessor obviously can't write to P2 and P3 simultaneously.

Each time you write to port P2 and P3 to update the LED bar graph you then want to re-zero the memory locations where you remember the largest left and right channel samples you have observed during a batch of samples. This way you are guaranteed to find a new peak reading within the next batch of a thousand samples. The arrival of these new samples are indicated by a whole bunch of external interrupt #1 interrupts which occur before the next external interrupt #0 interrupt, at which time you again update the LED bar graph. The 8051 microprocessor is repeating the same sequence of commands over and over again. This is a typical assignment for a microprocessor in an embedded device. The microprocessor that controls the air bags in your car is constantly executing a loop of instructions that repeatedly ask, "Have I just hit something?"

Hopefully the following illustration will help make this clear. Each of the external interrupt #1 events is represented by a small red square. Each of the external interrupt #0 events is represented by a large, hollow green rectangle seen at the bottom of the graph. The 8051 program looks at about one thousand consecutive audio samples in between each time it writes a new value to the LED bar graph. While this illustration shows only a single (mono) audio channel, the 8051 program performs the same activities for both a left and right audio channel and then updates a two-channel LED bar graph. The illustration should make clear that either a positive or negative excursion from quiet can qualify as the peak reading observed during an interval.

A complication is that a .WAV file can employ a number of different formats. If you select a .WAV file that contains 16 bit samples, then only the MSByte of each sample will be made available to the 8051 (since the P0 and P1 registers are only 1 byte wide). And if you select a .WAV file holding a mono (1 channel) recording, then the P0 and P1 registers will be populated with identical readings. My simulator takes care of all these details in order to simplify your 8051 program.

The left and right channel samples presented to the 8051 are both signed bytes, meaning that they have a range from -128 to +127. This is the first time in these example programs that we have had to deal with signed values so make sure you have already read about two's complement numbers. I described how the two instruction sequence:

   CPL A
   INC A

is used to negate any audio sample that is negative and thereby obtain the unsigned "excursion from quiet".

Now that we have the unsigned excursion from quiet, we need to detect the largest such reading. This will be our first use of the 8051's SUBB instruction. You can read the SUBB mnemonic as "Subtract with Borrow". The Carry bit in the PSW register serves as both the carry flag and the borrow flag. Whereas the 8051 instruction set offers both an ADD and an ADDC instruction, it lacks a SUB instruction that does not involve the Carry bit. In the PeakDetect.txt example program when we employ the SUBB instruction we do not want to include the Carry flag and hence we must explicitly zero out the Carry bit prior to the SUBB instruction.

Upon each EXT1 interrupt the PeakDetect.txt program reads from P0 and P1 and then places the absolute value of these latest audio samples into data memory locations that it has labeled (using the EQU assembler directive) LeftSample and RightSample. The PeakDetect.txt program must then determine if these are the largest readings observed since the last time it zeroed its peak detectors (it zeros its peak detectors after every time it writes to the LED bar graph). We use the SUBB instruction to decide if these latest readings are larger than the current champs held in two data memory locations that are labeled LeftPeak and RightPeak. The relevant section of 8051 code is shown below (left channel only):

    LeftPeak     EQU   16   ; use data memory addr 16 to hold the
                            ;   left channel peak
    RightPeak    EQU   17   ; use dma 17 to hold the right peak

    MOV   A,LeftSample  ; load ACC with left-channel
                        ;   excursion from 0
    CLR   C             ; necessary prior to SUBB since
                        ;   there is no SUB instr
    SUBB  A,LeftPeak    ; compare new reading to prior peak,
                        ;   the SUBB instr populates the ACC with
                        ;   [(the prior value in the ACC) - P0]
    JC    Ahead1        ; SUBB sets the carry/borrow flag C if a
                        ;   borrow is needed and clears C otherwise
NewLeftPeak:
    MOV   LeftPeak,LeftSample   ; remember new left-channel peak
Ahead1:

The JC instruction is used to check the Carry flag following the SUBB operation and thereby determine if the new reading is the new champion. If so, it is copied into the LeftPeak data memory location.

The 8051 code that executes upon every EXT0 interrupt is remarkably simple. This is the moment when the 8051 must update the LED bar graph and then to reset the detected left and right peak readings.

Ext0ISR:
    MOV   P2,LeftPeak   ; write the left-channel peak reading to
                        ;   the left-channel peak meter
    MOV   P3,RightPeak  ; write to the right-channel peak meter

; Now reset the two peak detectors.
    MOV   LeftPeak,#0   ; zero out the left peak reading
    MOV   RightPeak,#0  ; zero out the right peak reading

    RETI                ; return from the interrupt

One thing that I like about this example program is that the 8051 is not required to remember how many audio samples it has seen. Instead it just keeps observing the readings and remembering the peak until eventually it gets the EXT0 interrupt whereupon it writes to the LED bar graph and then resets the detected peak reading to 0.

Remember that you must visit the "Options" dialog (i.e. select View/Options from the menu) and request the Peak Detector "Output Window" before executing the PeakDetect.txt example program. You will then observe both a set of tape deck like pushbuttons and an LED bar graph in the Output Window. You start playing the default .WAV file by clicking on the "Play" pushbutton. You can load a different .WAV file by right-clicking on the tape deck pushbuttons and then selecting "Load .WAV file" from the context menu. I have provided four .WAV files holding fragments of the following rock and roll songs so you will have some source material to get started with:

  • "Bridge of Sighs" by Robin Trower which can be found on the album "Bridge Of Sighs", copyrighted by Chrysalis Records.
  • "Hawkeye" by The Alan Parsons Project which can be found on the album "The Instrumental Works", copyrighted by Arista Records.
  • "Fortunate Son" by Creedence Clearwater Revival which can be found on the album "Chronicle", copyrighted by Fantasy Records.
  • "It's Only Money" by Argent which can be found on the album "The Argent Anthology", copyrighted by CBS Records.
Alternatively, you can use my SoundLab software to rip (i.e., create) a .WAV file from any audio CD you own.

You can play the .WAV file even if your 8051 program is not running. Only when you start running the PeakDetect.txt program will you observe the LED bar graph come alive, because that's what the 8051 program is accomplishing. The LED bar graph will continually display the peak amplitude detected within the most recent 1/8 of a second (since the EXT0 interrupt event happens 8 times a second).

WARNING! This is a very demanding example program since the simulation of the 8051 microprocessor must keep up with the music being played from the .WAV file. In the default configuration of this simulator there is basically no chance the 8051 will keep up and you will either hear distortion in the music (probably gaps) or else observe (even with a perfect 8051 program) the LED bar graph incorrectly indicating the musical peaks. It turns out that the highlighting done in the three upper panes of the Execute Mode is the main drag on the simulation speed so we must disable this feature while running this example program. To do this go to the View/Options dialog and add a checkmark to the "Simulation Speed" checkbox. With this change you will probably observe correct results if your computer is no more than, say, 5 years old.

Shown below is another type of plot that documents the instantaneous sound volume throughout a song. I created these plots using the Cool Edit 2000 sound editor offered by Syntrillium Corporation. These four plots show the left and right channel data for the four rock and roll song fragments that I provide for you. Compare the readings produced by the 8051 peak detector with these other plots to discover the identity of these four songs. Note also how little difference there usually is between the left and right channels of a stereo recording.

A Puzzle: Name these 4 songs

Previous Table Of Contents Next


Internal Timers

( 45 )
Previous Table Of Contents Next

The 8051's Internal Timers

The 8051 microprocessor includes two internal timers (an "external timer" would be one you purchase from another manufacturer and hence does not reside in the 8051 package). These two internal timers are known as "Timer #0" and "Timer #1". They both have identical capabilities and you can individually configure them.

These timers are both 16 bits wide which allows them to count from 0 to 65535 before they roll over back to 0. Because the 8051 is a byte oriented machine, these 16 bits must be read and written as two bytes. The current count of Timer #0 is given by the two bytes in the TL0 (least significant) and TH0 (most significant) special function registers (SFRs). The current count of Timer #1 is similarly given by the TL1 and TH1 SFR registers. You can read from and write to these registers at any time, even while the timers are running.

The timers do not start counting as soon as power is applied to the 8051. Instead, it is up to your program to configure the timers and then command them to start counting. This is done via the TCON ("timer control") and TMOD ("timer mode") SFR registers which are discussed below.

An actual 8051 chip offers 4 operating modes for its internal timers. However this Windows simulator only supports 2 of these 4 modes:

  • 16 bit mode where THx and TLx (where x = 0 or 1) are concatenated together to give Timer #x a 16 bit range. Once the 16 bit timer reaches the count of 65535 it next wraps around back to 0.
  • 8 bit auto-reload mode where the timer is only 8 bits wide and hence can only count up to 255 before it rolls over. The 8 bits that are used for the timer are those from TLx. The 8 bits in THx are used as the re-load value. That is, once TLx reaches the "all ones" count the next clock pulse will cause TLx to be loaded with whatever value THx currently has. This mode allows you to specify an arbitrary period for the 8 bit timer's overflow events.

You specify the operating mode for each of the two timers via the TMOD ("timer mode") SFR register illustrated below:

Regardless of which mode the timer is operating in, every time it overflows it generates an interrupt. Timer #0's interrupt request is signaled by the TF0 bit in the TCON ("timer control") register while Timer #1's interrupt request is signaled by the TF1 bit as shown below:

These TF1 and TF0 bits represent an interrupt request. An actual interrupt (that is, the resetting of the program counter to the special interrupt service vector, 0x000B for Timer #0's interrupt) does not happen unless this particular interrupt is also enabled. The 5 possible interrupts to an 8051 can be individually enabled or disabled by the bits in the IE SFR register as discussed in the section on interrupts.

So what clocks these timers? On an actual 8051 you have the option of clocking the timer based upon an signal wired to a particular pin on the microprocessor's package (a timer is known as an event counter when it is employed in this manner). This Windows simulator does not offer any places you can solder wires. So this Windows simulator can only offer the other option which is that the timer will be clocked every 12 cycles of the 8051's oscillator. Most 8051 instructions take 12 oscillator cycles to complete so you can approximately say that the timer increments once per instruction. The detailed description of each command such as MOV lists how many oscillator cycles ("clocks") are required to complete that command.

Even if you write to the TCON register to enable the timer, you will not see the TLx and THx registers moving until you also request that this Windows simulator support the timer. This is done via the View/Options choice on the simulator's menu. I didn't want the simulator to have to provide these timers if your program was not using them because each time you activate one of the two internal timers your 8051 program runs about 20% slower on this simulator.


TimerTest.txt

( 46 )
Previous Table Of Contents Next

The TimerTest.txt Example Program

This example program employs the electronic signboard so if your Output Window is configured for something else then you will have to switch it back.

The TimerTest.txt example program enables interrupts from both of the internal timers and then goes into a semi-infinite loop, awaiting those interrupts. While the 8051 waits for the interrupts that it has enabled, it scrolls diagonal lines across the signboard. Each time the 8051 receives the TMR0 interrupt it scrolls the letter A onto the signboard. Each time the 8051 receives the TMR1 interrupt it scrolls the letter B onto the signboard.

Note that you must not only write to the TMOD and TCON special function registers to activate the internal timers, but you must also request that the simulator support them by going to the View/Options menu choice and adding checkmarks in the "Internal Timers" checkboxes. By default, the simulator does not support the internal timers because doing so slows the 8051 program by about 20 % per enabled timer. While you have the Options dialog open you may also want to maximize the speed of the simulator by adding a checkmark to the "Simulation Speed" option. This way, your 8051 program will run as close as it can to real time.

Both timers are configured for their "8 bit auto-reload" mode but I have set timer #1 to have a shorter period than timer #0 and hence you will observe more B's than A's (in a complicated pattern) mixed among the diagonal lines. Otherwise, there is not much new in this program since it employs the same Draw1Char subroutine that we wrote for the YourName3.txt example program and it protects the accumulator's prior value at the start of the interrupt service routine as we discussed for the SerialPort3.txt example program.


InterruptTest.txt

( 47 )
Previous Table Of Contents Next

The InterruptTest.txt Example Program

This is the last of the example programs that I have prepared for distribution with my 8051 simulator. It employs the electronic signboard so if your Output Window is configured for something else then you will have to switch it back.

This InterruptTest.txt example program enables interrupts from both of the external interrupt sources (EXT0 and EXT1) and then goes into a semi-infinite loop, awaiting those interrupts. While the 8051 waits for the interrupts that it has enabled, it scrolls diagonal lines across the signboard. Each time the 8051 receives the EXT0 interrupt it scrolls the letter A onto the signboard. Each time the 8051 receives the EXT1 interrupt it scrolls the letter B onto the signboard. This example program is basically identical to the TimerTest.txt example program except the functions provided by the two internal timers are now being provided by the two external interrupts.

Before running this program you must go to the View/Options dialog and specify the frequency of these interrupts. I would recommend that you request 1 pulse (interrupt) per second for the external interrupt #0 and 2 pulses per second for the external interrupt #1. Remember to shut off support for the internal timers if you previously ran the TimerTest.txt program. If you want to see the 8051 run at close to real time add a checkmark to the "Simulation Speed" checkbox found on the Options dialog. Since we request a higher frequency of external interrupt #1 events, you will observe a preponderance of the letter B on the signboard.


( 48 )
Previous Table Of Contents Next

8051 Instruction Set

The following list shows all of the 8051's instructions. You can click on each mnemonic to go to a page that shows the details for that instruction. The language on those pages may seem a little stiff: it was taken from Intel's original "data sheet" which was prepared by engineers for engineers. You have been warned!

ACALL | Absolute Call
ADD | Add to Accumulator (without carry)
ADDC | Add to Accumulator with Carry
AJMP | Absolute Jump
ANL | AND
ANL C | AND with Carry bit
CJNE | Compare and Jump if Not Equal
CLR A | Clear Accumulator
CLR bit | Clear Bit
CPL A | Complement Accumulator
CPL bit | Complement Bit
DA A | Decimal Adjust Accumulator for Addition
DEC | Decrement
DIV AB | Divide Accumulator by B register
DJNZ | Decrement and Jump if Not Zero
INC | Increment
INC DPTR | Increment Data Pointer
JB | Jump if Bit is set
JBC | Jump if Bit is set and Clear
JC | Jump if Carry is set
JMP | Jump Indirect
JNB | Jump if Not Bit
JNC | Jump if Not Carry
JNZ | Jump if Accumulator is Not Zero
JZ | Jump if Accumulator is Zero
LCALL | Long Call
LJMP | Long Jump
MOV | Move Byte
MOV bit | Move Bit
MOV DPTR | Move Data Pointer
MOVC | Move Code Byte
MOVX | Move External Byte
MUL AB | Multiply Accumulator and B register
NOP | No Operation
ORL | OR
ORL C | OR with Carry bit
POP | Pop from Stack
PUSH | Push onto Stack
RET | Return from Subroutine
RETI | Return from Interrupt
RL A | Rotate Accumulator Left
RLC A | Rotate Accumulator Left thru Carry
RR A | Rotate Accumulator Right
RRC A | Rotate Accumulator Right thru Carry
SETB | Set Bit
SJMP | Short Jump
SUBB | Subtract with Borrow
SWAP A | Swap Nibbles within the Accumulator
XCH | Exchange Accumulator with Byte
XCHD | Exchange Digit
XRL | Exclusive OR (XOR)


( 49 )
Previous Table Of Contents Next

The Example Programs

To help you learn to program the 8051 microprocessor I have prepared a number of example programs. Each program is very short and has comment statements that explain its purpose and how it accomplishes that goal. I would definitely recommend that you study every one of these programs. I have carefully designed them to introduce new topics in a logical order and at a pace that hopefully won't rip your head from your shoulders. The example programs are integrated into these help pages. That is, read these help pages in order from the beginning and you will be told the appropriate time to explore each of the example programs.

Some of the programs I call "skeletons". These are the programs you will have to finish in order to develop your programming skills. I have provided the skeleton to save you typing and get you started on the right foot. But there is always something missing or just plain wrong in the programs I have labeled as skeletons. You can employ the usual "cut and paste" procedure to bring in code from any place you desire. However this will probably not be necessary as the number of changes that you must make to the skeleton programs is very small. I'm not kidding: initially, you will be asked to make 1 character changes and only later will you have to bear the burden of providing an entire new line of code! So don't fret. You can do this. Don't just skip past these exercises or you won't learn the material. Think for a while and try some wild guesses if the solution isn't immediately obvious. Watch your program run in the debugger. Your observations on what the program is and what it is not doing will give you ideas on what to try next. You can't hurt your computer or this simulator. Only when you are exhausted do I give you permission to look at my solutions to the skeleton exercises which you can find in the "Solutions" folder.

IncrementForever.txt

This program does nothing but endlessly increment the accumulator. But it is a good introduction to 8051 assembly language programming because it forces us to learn about labels, mnemonics, operands, single-stepping, addressing modes, registers, and all of the technical mumbo-jumbo that we must master to be a programming guru. It introduces the "MOV A,#data8", "ADD A,#data8", and "LJMP addr16" generic instructions and the accumulator, which is the most important special function register.

IncrementForever 2.txt

This program builds on the previous IncrementForever.txt program by introducing the "INC addr8", "JNC rel8", and "SJMP rel8" generic instructions and the B register.

Train1.txt

This program introduces the scrolling mode of the electronic signboard and creates an image of a train locomotive on the signboard. This program introduces the "MOV addr8,#data8" generic instruction and the P0 register.

Diamond.txt

This is our first skeletal program, meaning that you must finish it! The program's structure can be identical to the Train1.txt example program but the image must be changed to a diamond. This is a good time to practice your binary and hex numbers.

ContinuousDiamonds.txt

This is another skeletal program that you must finish. Your goal is to create not one but a never-ending sequence of diamonds scrolling across the signboard. You can write this program by adding 1 new statement to the Diamonds.txt program you just finished. This new statement should accomplish an infinite loop so that the diamonds keep coming and coming.

SpacedDiamonds.txt

This is another skeletal program that you must finish. Your goal is to space the scrolling diamonds apart on the signboard.

WalkingTriangle.txt

This program demonstrates that the electronic signboard has a random access mode in addition to the scrolling mode we have already seen. This program introduces the "DJNZ addr8,rel8" generic instruction, the EQU assembler directive, and the P1 and P2 special function registers.

Train2.txt

This program again marches the train across the signboard but has a much more sophisticated architecture than our earlier Train1.txt program. In Train2.txt we manage to separate the logic of scrolling an image from the data that describes a particular image. This program introduces the "MOV DPTR,#addr16", "MOVC A,@A+DPTR", and "CJNE A,#data8,rel8" generic instructions and the DB and INCLUDE assembler directives.

Alphabet.txt

This is another skeletal program which you must finish. Your goal is to scroll the entire alphabet across the signboard instead of a locomotive. Luckily you can write this program by making a single change to the previous Train2.txt example program. Once you see how easy this is you'll appreciate the importance of keeping your logic separate from your data.

Factorial.txt

This program computes 5 factorial and shows why an 8 bit accumulator will never qualify as being "Super Sized" at Wendys. This program introduces the "DEC addr8", "MOV addr8,addr8", and "MUL AB" generic instructions.

FillMemory.txt

This program initializes all of data memory with a constant value. It introduces indirect addressing since without indirect addressing it would be impossible for a single statement inside a loop to keep touching different data memory locations during each iteration of the loop. This program introduces the "INC Rn", "MOV Rn,#data8", and "MOV @Ri,#data8" generic instructions and the R0 register.

FillMemory2.txt

This is another skeletal program which you must finish. Your assignment is to eliminate the redundant use of the accumulator and the R0 register seen in the prior FillMemory.txt example program. My solution to this exercise introduces the "CJNE Rn,#data8,rel8" generic instruction.

FillMemory3.txt

This is another skeletal program which you must finish. Your goal is to populate all of data memory with an incrementing pattern. This can be accomplished with a trivial change to the prior FillMemory2.txt example program. My solution to this exercise introduces the "MOV @Ri,addr8" generic instruction.

YourName.txt

This 8051 program scrolls a name across the electronic signboard. But it looks like it was written by a beginner because it has multiple occurrences of the same code. It was this situation that led to the development of the subroutine.

YourName2.txt

This program introduces the concept of the subroutine and shows how it improves the prior YourName.txt example program. This program introduces the "LCALL addr16" and "RET" generic instructions and the stack. It also introduces the concept of assembly-time math which is a programmer convenience feature.

YourName3.txt

This is another skeletal program which you must finish. Your assignment is employ the 8051's ability to multiply to simplify the code in YourName2.txt which calls the Draw1Char subroutine.

SerialPort.txt

This program paints on the signboard whatever characters the operator types on the computer's keyboard. This means that the signboard message can be dynamic (in all previous programs the message was fixed when the 8051 program was assembled). More importantly, this program introduces the concept of interrupts and shows how they can simplify a program. This program introduces the "RETI" instruction, the ORG assembler directive, and the SCON, SBUF, and IE special function registers.

SerialPort2.txt

This program demonstrates the main complication of interrupts: the program counter is the ONLY register that is automatically preserved at the start of the interrupt and then restored at the conclusion of the interrupt. This program demonstrates how the accumulator is damaged because it is used in both the foreground process and in the interrupt service routine. This program introduces the "RL A" instruction.

SerialPort3.txt

This program solves the damage observed in the accumulator in the prior example program. The solution is to employ the stack to preserve the prior value in the accumulator before starting the interrupt service routine. This program introduces the "PUSH addr8" and "POP addr8" generic instructions.

MazeSolver.txt

This is the first example program to employ the maze instead of the electronic signboard for the simulator's "output window" (you control this aspect of the simulator via the View/Options dialog). It attempts to navigate a maze to help a mouse find the hidden cheese. It's a skeletal program because you must supply a few statements. This program introduces the "ANL A,#data8" generic instruction.

MazeSolver2.txt

This program contains the same algorithm as the prior program but employs the 8051's bit-addressable bits to simplify the code. Nonetheless, it is another skeletal program because the algorithm doesn't manage to get the mouse to the cheese. You'll be surprised how much a trivial re-arrangement of the algorithm improves the mouse's diet. This program introduces the "JNB bit,rel8" generic instruction.

PeakDetect.txt

This is the first example program to employ the peak detector output window which you must manually select via the View/Options dialog. The peak detector output window contains both a set of tape-deck like pushbuttons and a dual-channel LED bar graph. The output window has the intelligence to play any .WAV file while making the audio samples available to the 8051. The 8051 program watches the audio samples and displays the musical peaks on the LED bar graph. This program introduces the "SUBB A,addr8" generic instruction.

WARNING! This is a very demanding sample program since the simulation of the 8051 microprocessor must keep up with the music being played from the .WAV file. In the default configuration of this simulator there is basically no chance the 8051 will keep up and you will either hear distortion in the music (probably gaps) or else observe (even with a perfect 8051 program) the LED bar graph incorrectly indicating the musical peaks. It turns out that the highlighting done in the three upper panes of the Execute Mode is the main drag on the simulation speed so we must disable this feature while running this example program. To do this go to the View/Options dialog and add a checkmark to the "Simulation Speed" checkbox. With this change you will probably observe correct results if your computer is no more than, say, 5 years old.

TimerTest.txt

This program demonstrates the 8051's two internal timers. Besides writing to the TMOD and TCON registers to enable these timers, you must also request that the simulator support them via the View/Options dialog. This program employs the signboard output window so if you were last using a different output window then you'll need to reconfigure the simulator (again via the View/Options dialog). This program introduces the TMOD, TH0, TL0, TH1, and TL1 special function registers.

InterruptTest.txt

This program demonstrates the 8051's two external interrupts. Before running this program you must go to the View/Options dialog and specify the frequency of these interrupts. I would recommend that you request 1 pulse (interrupt) per second for the external interrupt #0 and 2 pulses per second for the external interrupt #1. Remember to shut off support for the internal timers if you previously ran the TimerTest.txt program. If you want to see the 8051 run at close to real time add a checkmark to the "Simulation Speed" checkbox.

This example program is basically identical to the TimerTest.txt example program except the functions provided by the two internal timers are now being provided by the two external interrupts. This program employs the signboard output window so if you were last using a different output window then you'll need to reconfigure the simulator (again via the View/Options dialog).

ExternalMemory.txt

I don't discuss this example program anywhere else within this documentation. That's probably because it doesn't really accomplish anything, other than to demonstrate that the 8051 simulator also includes 65536 bytes of external memory. This is in addition to the 256 bytes of internal (on-chip) memory. Why 65536 bytes? Because the 8051 can only generate a 16 bit address and 16 bits can form no more than 65536 different addresses. There are only 2 instructions that allow you to write to this external memory space and 2 other instructions that allow you to read from it. For those of you that are interested in some additional memory, I'll leave you to read the comments in this program.


( 50 )
Previous Table Of Contents Next

Simulator Options

This section describes the user-configurable options seen on the "Options" dialog that appears when you select "View/Options" from the program's menu bar. I will walk you through the purpose of each option in column order as seen on this dialog.

The first option offers a checkbox that will cause the assembler to store the listing file on your hard drive every time you assemble your 8051 source program. This checkbox is not checked by default because you don't need to save the listing file since it can always be recreated from your source file. But if you do save it then it will be saved in a plain text ASCII file with the same filename as your source file but using the filename suffix ".LST". It will be located in whatever folder your source file resides in. Note that your source file is also a plain text ASCII file. Such files can be created by any word processor as long as you ask the word processor to save the file as "plain text" (plain meaning that the file holds no fancy formatting instructions).

The next option is another checkbox that will cause the assembler to create an Intel hex file every time you assemble your program and get no errors (listing files for programs that cause assembly errors are still useful since they document the errors but an Intel hex file for a program that has errors is worthless). These Intel hex files are again plain text ASCII files and you can use Notepad or some other word processor to look at them. They have no value to this simulator. I added the feature merely so I could transfer a finished program into an actual 8051 chip.

The next option is another checkbox that will cause your program to create a log file (another plain text ASCII file) that documents each and every instruction executed as your program runs. This is a standard debugging technique (like single-stepping). The log file is not created by default because it slows down the simulator.

The final option in the first column is another checkbox that turns off the highlighting of the current instruction and the most recently changed data memory locations and registers. This highlighting consumes a remarkable amount of horsepower and without it your 8051 program will run dramatically faster. Be aware that you MUST increase the simulation speed using this checkbox before you try the PeakDetect.txt sample program that I distribute with this simulator.

The first option in the second column consists of two EDIT controls that allow you to specify the frequency of interrupt pulses that you want applied to the 8051's "external interrupt #0" and "external interrupt #1" pins. These pulses can be created with very little overhead so if you need to create a repeating interrupt then use this feature in preference to the 8051's internal timers since then your program will run faster.

The next option in the second column allows you to specify the number of program steps you want to perform every time you click on the "Step Into N" toolbar button. By default, this N value is set to 25 instructions.

The final option in the second column offers two checkboxes that control whether this simulator bothers to simulate the 8051's internal timers. Each time you activate one of these timers your 8051 program will run about 20% slower because of the extra work to continually advance the internal timer. Note that adding a checkmark in these checkboxes is necessary but not sufficient for observing the TL0, TH0, TL1, and TH1 special function registers incrementing. Your 8051 program must also write the appropriate configuration data to the TCON and TMOD special function registers. For an example see the TimerTest.txt sample program distributed with this simulator.

The final option is everybody's favorite. It configures the "Output Window" which is the simulation of the circuitry that is connected to the 8051 microprocessor. Your choices are:

  • a scrolling electronic signboard. The behavior of this device is fully described in the documentation for the Train1.txt and WalkingTriangle.txt sample programs that are distributed with this simulator.
  • a maze where a mouse searches for cheese. The behavior of this device (a mouse robot basically) is described in the documentation for the MazeSolver.txt sample program that is distributed with this simulator.
  • a tape deck with pushbutton controls and an LED bar graph peak meter. The behavior of this device is described in the documentation for the PeakDetect.txt sample program that is distributed with this simulator.


( 51 )
Previous Table Of Contents

About The Simulator

This 8051 simulator is a Windows program that was written in C++ using Microsoft's WIN32 API. The program (16,000 C++ statements), these help pages (54,000 words), and the example 8051 programs were written over a period of a few months in 2002 by yours truly, John Kopplin, in Boise, Idaho. This 8051 simulator is compatible with Windows 95, Windows 98, Windows ME, Windows NT 4, Windows 2000, and Windows XP. It should run acceptably on any PC purchased within about the last 6 years. Newer computers will of course run faster than older computers but this program is perfectly useable even on a Celeron or Pentium I computer. The only example program that might require a modern computer is the PeakDetect.txt program since in this program the simulated 8051 program must keep up with a .WAV file being played through the computer's speakers.

Although this program runs under Windows 95 it does not run under a stock installation of Windows 95 because it requires Microsoft's "Rich Edit" text control (this is the file RICHED20.DLL). All of the other operating systems that I have listed as being acceptable already include the RICHED20.DLL file but Windows 95 did not. Fortunately, RICHED20.DLL is compatible with Windows 95 and may already be on a given Windows 95 computer if some other application has already installed it. You can use the Windows Search capability (i.e., "Start/Find/Files or Folders...") to determine whether your computer has a RICHED20.DLL file. If it doesn't you can obtain this file by downloading from Microsoft's web site their "Windows Installer". The Windows Installer is a new technology for installing program on Windows computers. We don't need to do that but it just so happens that installation of the Windows Installer installs the RICHED20.DLL file onto your computer. I am sure this sounds round about but that's what dealing with a monopoly is like: Microsoft won't allow you to get this 1 file all by itself even though that's all you need. The file that you want to locate and download from Microsoft's web site in order to install the "Windows Installer" is INSTMSIA.EXE if you are running Windows 95/98/ME and INSTMSIW.EXE if you are running Windows NT/2000. Simply run this installation file and RICHED20.DLL will be added to your computer.

Even the newer operating systems can benefit from installing the Windows Installer because then they will get the latest and greatest version of RICHED20.DLL. This file has had a history that I'm sure Microsoft can't be proud of. The latest and greatest version of RICHED20.DLL at the time I wrote this was 5.30.11.2410. This version works fine for me. I have written my 8051 simulator to work around some of the bugs in the older versions as best it can.

If you obtained this Windows simulator from one of my CD-ROMs then there is already a copy of INSTMSIA.EXE and INSTMSIW.EXE on that CD-ROM so you don't even have to go visit the hall of darkness.

I wrote this Windows simulator to teach programming to beginners, not to be the do-all, end-all of 8051 simulators. It is actually the second component in my curriculum: I start my students off on a programmable RPN Calculator that is a more gentle introduction to programming than this 8051 microprocessor.

In designing my program I chose visibility over speed. This is why the default configuration of the program keeps moving a highlight bar to identify the next instruction to be executed and the most recently changed data memory locations and special function registers. You can turn off this highlighting via the View/Options choice on the menu bar and you will then see a significant speed increase. In fact, you MUST increase the simulation speed in this manner before you try the PeakDetect.txt example program that I distribute with this simulator.

My 8051 assembler is what is called an absolute rather than a relocating assembler. This means that you can partition your source code into multiple files but you must assemble all of these files anytime you change even one of them. A relocating assembler allows you to only re-assemble those files you have actually changed which can save you time if you are working on a huge project.

To partition your work into multiple source files you must design one of your source files as the primary file that you "File/Open" with this simulator. That primary file must then call out all the other necessary files using the INCLUDE assembler directive. You can see this organization used in the Train2.txt example program distributed with this simulator.

My assembler provides you the option of creating an Intel hex file, which is the standard mechanism used to transfer a finished program into an actual 8051 chip.

My assembler supports the 6 assembler directives: EQU, INCLUDE, END, ORG, DB, and DW (you can read about them here). I don't support the BIT assembler directive which allows you to assign a label (much as you do using the EQU assembler directive) to a bit address. That is, I don't support:

Overflow    BIT  PSW.OV

I don't need to support the BIT assembler directive because my assembler has pre-defined labels for every bit of every special function register. Consequently, instead of something like "SETB PSW.OV" you just want to use "SETB OV".

In the section on the internal timers I have already mentioned that my simulator only supports 2 of the 8051's 4 modes for its internal timers. I don't think you will miss the other 2 modes as one of them ("Mode 0") was included by Intel only so their new 8051 would be backwards compatible with a prior 8048 chip and the other mode ("Mode 3") is only useful generating the baud rate for the serial port, which this simulator doesn't support anyway.

The reason that so many microprocessors have been sold is that all microprocessors offer a collection of pins that you can wire to arbitrary circuitry. That is, the microprocessor doesn't need to know anything about motors or lights or pumps: that specialization is provided by the program. For the purposes of this Windows simulator I had to select some real-world circuits to connect to my 8051. I choose an electronic signboard, a maze, and a tape deck peak detector based upon an LED bar graph. These were chosen so that the example programs would light lights and make sounds and thus help keep you interested.

I had to make choices about how to best connect my simulated 8051 uP to my simulated external circuitry. One choice that I should mention concerns the feature where the operator can (via the View/Options dialog) select a frequency of pulses to be sent to the external interrupt #0 and external interrupt #1 pins. If you choose this option this interrupt source will always be cleared when the branch to the interrupt service vector is taken. In a real 8051 this automatic clearing of the interrupt source occurs only if you have requested an edge sensitive interrupt. With a level sensitive interrupt your program must clear the interrupt source before leaving the interrupt service routine. The choice between edge sensitive and level sensitive interrupts is made via bits in the TCON register.

I have also mentioned how running the internal timers slows down the simulation by about 20% per timer. This is because the simulation must laboriously clock these timers in lock-step with the running program. If you just need a source of periodic interrupts, use the external interrupts instead of the internal timers. Unlike the internal timers, the external interrupts occur in real time (the time of a wall clock) and don't occur at all while you are single-stepping. This means you will have to "continue" your 8051 program (not just single-step) before you will get an external interrupt.

I have chosen to depart from the behavior of a true 8051 microprocessor with regard to uninitialized program memory locations and bad jumps into the middle of multi-byte opcodes. A true 8051 uP cannot detect these problems and blindly continues attempting to decode as opcodes bytes that are not opcodes. This can produce very perplexing symptoms which drive beginning programmers nuts. My simulator will detect these types of bad jumps, halt the 8051, and present an error message so you know your program has been naughty.

The only negative consequence to this feature is that the MOVC instruction does not act exactly as it does on a real 8051. For example, if you take the Train2.txt sample program that is distributed with this simulator and purposefully use the MOVC instruction to read past the end of the train image data residing in program memory then we will see the train scroll by followed by garbage. But this garbage will be different than the garbage produced by a real 8051 in this same situation (the train will be identical).


ACALL

ACALL addr11

Function: | Absolute Call within the same 2 Kbyte block
Description: | ACALL unconditionally calls a subroutine located at the indicated address. The instruction increments the PC twice to obtain the address of the following instruction, then pushes the 16-bit result onto the stack (low-order byte first) and increments the Stack Pointer twice. The destination address is obtained by successively concatenating the five high-order bits of the incremented PC, opcode bits 7-5, and the second byte of the instruction. The subroutine called must therefore start within the same 2K block of the program memory as the first byte of the instruction following the ACALL. No flags are affected.
Example: | Initially the SP (stack pointer) equals 0x07. The label "SUBRTN" is at program memory location 0x0345. After executing the instruction: ACALL SUBRTN at location 0x0123, SP will contain 0x09, internal RAM locations 0x08 and 0x09 will contain 0x25 and 0x01 respectively, and the PC will contain 0x0345.
Bytes: | 2
Clocks: | 24
Encoding: | a10 a9 a8 1 0 0 0 1 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (PC) <- (PC) + 2 (SP) <- (SP) + 1 ((SP)) <- (PC7-0) (SP) <- (SP) + 1 ((SP)) <- (PC15-8) (PC10-0) <- addr11
Flags: | None


ADD

Function: | Add to Accumulator (without carry)
Description: | ADD adds the indicated byte variable to the Accumulator, leaving the result in the Accumulator. The carry and auxiliary-carry flags are set, respectively, if there is a carry-out from bit 7 or bit 3, and cleared otherwise. When adding unsigned integers, the carry flag indicates an overflow occured. OV is set if there is a carry-out of bit 6 but not out of bit 7, or a carry-out of bit 7 but not bit 6; otherwise OV is cleared. When adding signed integers, OV indicates the sum of two positive operands produced a negative result, or two negative operands produced a positive result. Four source operand addressing modes are allowed : register, direct, register-indirect, or immediate.
Example: | The Accumulator holds 0xC3 (11000011B) and register 0 (R0) holds 0xAA (10101010B). The instruction: ADD A, R0 will leave 0x6D (01101101B) in the Accumulator with the AC flag cleared and both the Carry flag and OV flags set to 1.

ADD A,Rn
Function: | Add register byte to Accumulator
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 1 0 1 r r r
Operation: | (A) <- (A) +(Rn) (PC) <- (PC) + 1
Flags: | C AC OV

ADD A,addr8
Function: | Add directly addressed byte to Accumulator
Bytes: | 2
Clocks: | 12
Encoding: | 0 0 1 0 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (A) <- (A) + (addr8) (PC) <- (PC) + 2
Flags: | C AC OV

ADD A,@Ri
Function: | Add indirectly addressed RAM byte to Accumulator
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 1 0 0 1 1 i
Operation: | (A) <- (A) + ((Ri)) (PC) <- (PC) + 1
Flags: | C AC OV

ADD A,#data8
Function: | Add immediate data to Accumulator
Bytes: | 2
Clocks: | 12
Encoding: | 0 0 1 0 0 1 0 0 d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (A) <- (A) + data8 (PC) <- (PC) + 2
Flags: | C AC OV


ADDC

Function: | Add to Accumulator with Carry
Description: | ADDC simultaneously adds the indicated byte variable, the carry flag and the Accumulator contents, leaving the result in the Accumulator. The carry and auxiliary-carry bit flags are set, respectively, if there is a carry-out from bit 7 or bit 3, and cleared otherwise. When adding unsigned integers, the carry flag indicates an overflow occured. OV is set if there is a carry-out of bit 6 but not out of bit 7, or a carry-out of bit 7 but not out of bit 6; otherwise OV is cleared. When adding signed intergers, OV indicates the sum of two positive operands produced a negative result or the sum of two negative operands produced a positive result. Four source operand addressing mode are allowed; register, direct, register-indirect, or immediate.
Example: | The Accumulator holds 0xC3 (11000011B) and register 0 (R0) holds 0xAA (10101010B) with the carry flag set. The instruction: ADDC A, R0 will leave 0x6E (01101110B) in the Accumulator with AC cleared and both the Carry flag and OV flags set to 1.

ADDC A,Rn
Function: | Add register to Accumulator along with Carry
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 1 1 1 r r r
Operation: | (A) <- (A) + (C) + (Rn) (PC) <- (PC) + 1
Flags: | C AC OV

ADDC A,addr8
Function: | Add directly addressed byte to Accumulator along with Carry
Bytes: | 2
Clocks: | 12
Encoding: | 0 0 1 1 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (A) <- (A) + (C) + (addr8) (PC) <- (PC) + 2
Flags: | C AC OV

ADDC A,@Ri
Function: | Add indirectly addressed RAM byte to Accumulator along with the Carry
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 1 1 0 1 1 i
Operation: | (A) <- (A) + (C) + ((Ri)) (PC) <- (PC) + 1
Flags: | C AC OV

ADDC A,#data8
Function: | Add immediate data to Accumulator along with Carry
Bytes: | 2
Clocks: | 12
Encoding: | 0 0 1 1 0 1 0 0 d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (A) <- (A) + (C) + data8 (PC) <- (PC) + 2
Flags: | C AC OV


AJMP

AJMP addr11

Function: | Absolute jump to address within the same 2 Kbyte block
Description: | AJMP transfers program execution to the indicated address, which is formed at run-time by concatenating the high-order five bits of the program counter (PC) -- after incrementing the PC twice --, opcode bits 7-5, and the second byte of the instruction. The destination must therefore be within the same 2 Kbyte block of program memory as the first byte of the instruction following the AJMP.
Example: | The label "JMPADR" is at program memory location 0x0123. The instruction: AJMP JMPADR is at location 0x0345 and will load the PC with 0x0123.
Bytes: | 2
Clocks: | 24
Encoding: | a10 a9 a8 0 0 0 0 1 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (PC) <- (PC) + 2 (PC10-0) <- addr11
Flags: | None


ANL

Function: | Logical-AND for byte variables
Description: | ANL performs the bitwise logical-AND operation between the indicated variables and stores the result in the destination variable. No flags are affected. The two operands allow six addressing mode combinations. When the destination is the Accumulator, the source can use register, direct, register-indirect, or immediate addressing; when the destination is a direct address, the source can be the Accumulator or immediate data. Note: When this instruction is used to modify an output port, the value used as the original port data will be read from the output data latch, not the input pins.
Example: | If the Accumulator holds 0xC3 (11000011B) and register 0 (R0) holds 0x55 (01010101B) then the instruction: ANL A, R0 will leave 0x41 (01000001B) in the Accumulator. When the destination is a directly addressed byte, this instruction will clear combinations of bits in any RAM location or hardware register. The mask byte determining the pattern of bits to be cleared would either be a constant contained in the instruction or a value computed in the Accumulator at run-time. The instruction: ANL P1, #0x73 ;0x73 = 01110011B will clear bits 7, 3, and 2 of output port 1 (P1).

ANL A,Rn
Function: | Logical AND register byte to Accumulator
Bytes: | 1
Clocks: | 12
Encoding: | 0 1 0 1 1 r r r
Operation: | (A) <- (A) & (Rn) where & represents the AND operation (PC) <- (PC) + 1
Flags: | None

ANL A,addr8
Function: | Logical AND directly addressed byte to Accumulator
Bytes: | 2
Clocks: | 12
Encoding: | 0 1 0 1 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (A) <- (A) & (addr8) where & represents the AND operation (PC) <- (PC) + 2
Flags: | None

ANL A,@Ri
Function: | Logical AND indirectly addressed byte to Accumulator
Bytes: | 1
Clocks: | 12
Encoding: | 0 1 0 1 0 1 1 i
Operation: | (A) <- (A) & ((Ri)) where & represents the AND operation (PC) <- (PC) + 1
Flags: | None

ANL A,#data8
Function: | Logical AND immediate data to Accumulator
Bytes: | 2
Clocks: | 12
Encoding: | 0 1 0 1 0 1 0 0 d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (A) <- (A) & data8 where & represents the AND operation (PC) <- (PC) + 2
Flags: | None

ANL addr8,A
Function: | Logical AND Accumulator to directly addressed byte
Bytes: | 2
Clocks: | 12
Encoding: | 0 1 0 1 0 0 1 0 d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (addr8) <- (addr8) & A where & represents the AND operation (PC) <- (PC) + 2
Flags: | None

ANL addr8,#data8
Function: | Logical AND immediate data to directly addressed byte
Bytes: | 3
Clocks: | 24
Encoding: | 0 1 0 1 0 0 1 1 a7 a6 a5 a4 a3 a2 a1 a0 d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (addr8) <- (addr8) & data8 where & represents the AND operation (PC) <- (PC) + 3
Flags: | None


ANL BIT

ANL C

Function: | Logical-AND for bit variables
Description: | If the Boolean value of the source bit is a logical 0 then clear the carry flag; otherwise leave the carry flag in its current state. A slash ("/") preceding the operand in the assembly language indicates that the logical complement of the addressed bit is to be used as the source value (in choosing to use the complement you are not affecting the source bit itself). No other flags are affected. Only direct addressing is allowed for the source operand.
Example: | Set the carry flag if, and only if, P1.0 = 1, ACC.7 = 1, and OV = 0 MOV C, P1.0 ; LOAD CARRY WITH INPUT PIN STATE ANL C, ACC.7 ; AND CARRY WITH ACC BIT 7 ANL C,/OV ; AND WITH INVERSE OF THE OVERFLOW FLAG

ANL C,bit
Function: | Logical AND bit variable to carry
Bytes: | 2
Clocks: | 24
Encoding: | 1 0 0 0 0 0 1 0 b7 b6 b5 b4 b3 b2 b1 b0
Operation: | (C) <- (C) & (bit) where & represents the AND operation (PC) <- (PC) + 2
Flags: | C

ANL C,/bit
Function: | Logical AND bit variable to carry
Bytes: | 2
Clocks: | 24
Encoding: | 1 0 1 1 0 0 0 0 b7 b6 b5 b4 b3 b2 b1 b0
Operation: | (C) <- (C) & ~(bit) where ~ represents the complement operation (PC) <- (PC) + 2
Flags: | C


CJNE

Function: | Compare and Jump if Not Equal
Description: | CJNE compares the magnitudes of the first two operands, and branches if their values are not equal. The branch destination is computed by adding the signed relative-displacement in the last instruction byte to the PC, after incrementing the PC to the start of the next instruction. The carry flag is set if the unsigned integer value of the first operand is less than the unsigned integer value of the second operand; otherwise, the carry is cleared. Neither operand is affected. The first two operands allow four addressing mode combinations: the Accumulator may be compared with any directly addressed byte or immediate data, and any indirectly addressed RAM location or working register can be compared with an immediate constant.
Example: | The CJNE instruction is used to test variables for equality and inequality. Consider the following code: CJNE R7, #0x60, NotEqual ... ... ; if we get here then R7 = 0x60 SJMP Ahead NotEqual: JC LessThan GreaterThan: ; if we get here then R7 > 0x60 ... ... SJMP Ahead LessThan: ; if we get here then R7 < 0x60 ... ... Ahead: If register 7 (R7) contains 0x56 then the first instruction in this sequence sets the carry flag and branches to the instruction at label "NotEqual". By next testing the carry flag, we determine whether R7 is greater or less than 0x60. As another example, if the Accumulator holds 0x34 and the data being presented to Port 1 (P1) is also 0x34, then the instruction: Wait: CJNE A, P1, Wait clears the carry flag and continues with the next instruction in sequence, since the Accumulator does equal the data read from P1. If some other value was being input on P1, the program will loop at this point until the P1 data changes to 0x34.

CJNE A,addr8,rel8
Function: | Compare directly addressed byte to Accumulator, Jump if Not Equal
Bytes: | 3
Clocks: | 24
Encoding: | 1 0 1 1 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0 r7 r6 r5 r4 r3 r2 r1 r0
Operation: | (PC) <- (PC) + 3 IF (A) != (addr8) THEN (PC) <- (PC) + rel8 IF (A) < (addr8) THEN (C) <- 1 ELSE (C) <- 0
Flags: | C

CJNE A,#data8,rel8
Function: | Compare immediate data to Accumulator, Jump if Not Equal
Bytes: | 3
Clocks: | 24
Encoding: | 1 0 1 1 0 1 0 0 d7 d6 d5 d4 d3 d2 d1 d0 r7 r6 r5 r4 r3 r2 r1 r0
Operation: | (PC) <- (PC) + 3 IF (A) != data8 THEN (PC) <- (PC) + rel8 IF (A) < data8 THEN (C) <- 1 ELSE (C) <- 0
Flags: | C

CJNE Rn,#data8,rel8
Function: | Compare immediate data to register, Jump if Not Equal
Bytes: | 3
Clocks: | 24
Encoding: | 1 0 1 1 1 r r r d7 d6 d5 d4 d3 d2 d1 d0 r7 r6 r5 r4 r3 r2 r1 r0
Operation: | (PC) <- (PC) + 3 IF (Rn) != data8 THEN (PC) <- (PC) + rel8 IF (Rn) < data8 THEN (C) <- 1 ELSE (C) <- 0
Flags: | C

CJNE @Ri,#data8,rel8
Function: | Compare immediate data to register, Jump if Not Equal
Bytes: | 3
Clocks: | 24
Encoding: | 1 0 1 1 0 1 1 i d7 d6 d5 d4 d3 d2 d1 d0 r7 r6 r5 r4 r3 r2 r1 r0
Operation: | (PC) <- (PC) + 3 IF ((Ri)) != data8 THEN (PC) <- (PC) + rel8 IF ((Ri)) < data8 THEN (C) <- 1 ELSE (C) <- 0
Flags: | C


CLR A

Function: | Clears the Accumulator
Description: | The Accumulator is cleared (all bits are reset to zero). No flags are affected.
Example: | The Accumulator contains 0x5C (01011100B). The instruction: CLR A Will leave the Accumulator set to 0x00 (00000000B).
Bytes: | 1
Clocks: | 12
Encoding: | 1 1 1 0 0 1 0 0
Operation: | (A) <- 0 (PC) <- (PC) + 1
Flags: | None


CLR bit

Function: | Clear Bit
Description: | The indicated bit is cleared (reset to zero). No other flags are affected. CLR can operate on the carry flag or any directly addressable bit.
Example: | Port 1 has previously been written with 0x5D (01011101B). The instruction: CLR P1.2 will leave the port set to 0x59 (01011001B).

CLR C
Function: | Clears the carry
Bytes: | 1
Clocks: | 12
Encoding: | 1 1 0 0 0 0 1 1
Operation: | (C) <- 0 (PC) <- (PC) + 1
Flags: | C

CLR bit
Function: | Clears bit
Bytes: | 2
Clocks: | 12
Encoding: | 1 1 0 0 0 0 1 0 b7 b6 b5 b4 b3 b2 b1 b0
Operation: | (bit) <- 0 (PC) <- (PC) + 2
Flags: |


CPL A

Function: | Complement Accumulator
Description: | Each bit of the Accumulator is logically complemented (replaced with its complement). Bits which previously contained a one are changed to a zero and vice-versa. No flags are affected.
Example: | The accumulator contains 0x5C (01011100B). The instruction: CPL A will leave the Accumulator set to 0x0A3 (10100011B).
Bytes: | 1
Clocks: | 12
Encoding: | 1 1 1 1 0 1 0 0
Operation: | (A) <- ~(A) where ~ represents the complement operation (PC) <- (PC) + 1
Flags: | None


CPL bit

Function: | Complement Bit
Description: | The specified bit variable is complemented. A bit which had been a one is changed to zero and vice-versa. No other flags are affected. CLR can operate on the carry or any directly addressable bit. Note: When this instruction is used to modify an output pin, the value used as the original data will be read from the output data latch, not the input pin.
Example: | Port 1 has previously been written with 0x5D (01011101B). The instruction sequence: CPL P1.1 CPL P1.2 will leave the port set to 0x5B (01011011B).

CPL C
Function: | Complements the carry
Bytes: | 1
Clocks: | 12
Encoding: | 1 0 1 1 0 0 1 1
Operation: | (C) <- ~(C) where ~ represents the complement operation (PC) <- (PC) + 1
Flags: | C

CPL bit
Function: | Complements selected bit
Bytes: | 2
Clocks: | 12
Encoding: | 1 0 1 1 0 0 1 0 b7 b6 b5 b4 b3 b2 b1 b0
Operation: | (bit) <- ~(bit) where ~ represents the complement operation (PC) <- (PC) + 2
Flags: |


DA A

Function: Decimal-adjust Accumulator for Addition
Description: The DA A instruction is for BCD (binary coded decimal) arithmetic. When performing BCD arithmetic, ADD and ADDC instructions should always be followed by a DA A operation, to ensure that the result is also in BCD. Note that DA A will not convert a binary number to BCD. The DA A operation produces a meaningful result only as the second step in the addition of two BCD bytes.

DA A adjusts the eight-bit value in the Accumulator resulting from the earlier addition of two variables, each in packed-BCD format, producing two four-bit digits. Any ADD or ADDC instruction may have been used to perform this prior addition. If Accumulator bits 3-0 are greater than nine (i.e., xxxx1010B through xxxx1111B), or if the AC flag is 1, six is added to the Accumulator producing the proper BCD digit in the low-order nibble. This prior addition will have set the carry flag if a carry-out of the low-order four-bit field propagated through all high-order bits, but it will not have cleared the carry flag otherwise. If the carry flag is now set, or if the four high-order bits now exceed nine (1010xxxxB through 1111xxxxB), these high-order bits are incremented by six, producing the proper BCD digit in the high-order nibble. Again, this will set the carry flag if there is a carry-out of the high-order bits, but will not clear the carry. The carry flag thus indicates if the sum of the original two BCD variables is greater than 100, allowing multiple precision decimal addition. OV is not affected.

All of this occurs during the one instruction cycle. Essentially, this instruction performs the decimal conversion by adding 0x00, 0x06, 0x60, or 0x66 to the Accumulator, depending on the initial Accumulator and PSW conditions.

Note : DA A cannot simply convert a hexadecimal number in the Accumulator to BCD notation, nor does DA A apply to decimal subtraction.

If the accumulator contains a binary number which is known to be less than 100, that number can be converted to BCD by the following code:

    MOV  B,#10
    DIV  AB
    SWAP A
    ADD  A,B

Dividing the number by 10 leaves the tens digit in the low nibble of the accumulator, and the ones digit in the B register. The SWAP and ADD instructions move the tens digit to the high nibble of the accumulator, and the ones digit to the low nibble.

Example: The Accumulator holds the value 0x56 (01010110B) representing the packed BCD digits of the decimal number 56. Register 3 (R3) contains the value 0x67 (01100111B) representing the packed BCD digits of the decimal number 67. The carry flag is set. The instruction sequence:
ADDC A, R3
DA   A
will first perform a standard two's-complement binary addition, resulting in the value 0xBE (10111110) in the Accumulator. The carry and auxiliary carry flags will be cleared. The Decimal Adjust instruction will then alter the Accumulator to the value 0x24 (00100100B) indicating the packed BCD digits of the decimal number 24, the low-order two digits of the decimal sum of 56,67, and the carry-in. The carry flag will set by the Decimal Adjust instruction, indicating that a decimal overflow occured. The true sum of 56,67, and 1 is 124.
BCD variables can be incremented or decremented by adding 0x01 or 0x99. If the Accumulator initially holds 0x30 (representing the digits of 30 decimal), then the instruction sequence:
ADD A, #0x99
DA  A
will leave the carry set and 0x29 in the Accumulator, since 30 + 99 = 129. The low-order byte of the sum can be interpreted to mean 30 -1 = 29.
Bytes: 1
Clocks: 12
Encoding: 1 1 0 1 0 1 0 0
Operation: IF [[(A3-0 ) > 9] | [(AC) = 1]]
THEN (A3-0 ) <- (A3-0 ) + 6
    AND
IF [[(A7-4 ) > 9] | [(C) = 1]]
THEN (A7-4 ) <- (A7-4 ) + 6
(PC) <- (PC) + 1
Flags: C


DEC

Function: | Decrement
Description: | The indicated variable is decremented by one. An original value of 0x00 will underflow to 0xFF. No flags are affected. Three operand addressing modes are allowed: register (with a special variant to decrement the accumulator), direct, or register-indirect. Note: When this instruction is used to modify an output port, the value used as the original port data will be read from the output data latch, not the input pins.
Example: | Register 0 (R0) contains 0x7F (01111111B). Internal RAM locations 0x7E and 0x7F contain 0x00 and 0x40, respectively. The instruction sequence: DEC @R0 DEC R0 DEC @R0 will leave register 0 set to 0x7E and internal RAM locations 0x7E and 0x7F to 0xFF and 0x3F.

DEC A
Function: | Decrement Accumulator by one
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 0 1 0 1 0 0
Operation: | (A) <- (A) - 1 (PC) <- (PC) + 1
Flags: | None

DEC Rn
Function: | Decrement register by one
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 0 1 1 r r r
Operation: | (Rn) <- (Rn) - 1 (PC) <- (PC) + 1
Flags: | None

DEC addr8
Function: | Decrement directly addressed byte by one
Bytes: | 2
Clocks: | 12
Encoding: | 0 0 0 1 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (addr8) <- (addr8) - 1 (PC) <- (PC) + 2
Flags: | None

DEC @Ri
Function: | Decrement indirectly addressed RAM byte by one
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 0 1 0 1 1 i
Operation: | ((Rii)) - 1 (PC) <- (PC) + 1
Flags: | None


DIV

DIV AB

Function: | Divide
Description: | DIV AB divides the unsigned eight-bit integer in the Accumulator by the unsigned eight-bit integer in register B. The Accumulator receives the integer part of the quotient while register B receives the integer remainder. The carry and OV flags will be cleared. Exception : If B had originally contained 0; the values returned in the Accumulator and B-register will be undefined and the overflow flag will be set. The carry flag is cleared in any case.
Example: | The Accumulator contains 251 (0xFB or 11111011B) and B contains 18 (0x12 or 00010010B). The instruction: DIV AB will leave 13 in the Accumulator (0x0D or 00001101B) and the value 17 (0x11 or 00010001B) in B, since 251 = (13 × 18) + 17. The Carry and OV flags will both be cleared.
Bytes: | 1
Clocks: | 48
Encoding: | 1 0 0 0 0 1 0 0
Operation: | (A) <- (A/B)15-8 (B) <- (A/B)7-0 (PC) <- (PC) + 1
Flags: | OV, C


DJNZ

Function: | Decrement and Jump if Not Zero
Description: | DJNZ decrements the indicated location by 1, and branches to the address indicated by the second operand if the resulting value is not zero. An original value of 0x00 will underflow to 0xFF. No flags are affected. The branch destination is computed by adding the signed relative-displacement value in the last instruction byte to the PC, after incrementing the PC to the first byte of the following instruction. The location decremented may be a register or directly addressed byte. Note: When this instruction is used to modify an output port, the value used as the original port data will be read from the output data latch, not the input pins.
Example: | Internal RAM locations 0x40, 0x50, and 0x60 contain the values 0x01, 0x70, and 0x15, respectively. The instruction sequence: DJNZ 0x40, Label1 DJNZ 0x50, Label2 DJNZ 0x60, Label3 will cause a jump to the instruction at label "Label2" with the values 0x00, 0x6F, and 0x15 in the three RAM locations. The first jump was not taken because the result was zero. This instruction provides a simple way of executing a program loop a given number of times, or for adding a moderate time delay (from 2 to 512 machine cycles) with a single instruction. The instruction sequence: MOV R2, #8 Toggle: CPL P1.7 DJNZ R2, Toggle will toggle P1.7 eight times, causing four output pulses to appear at bit 7 of output Port 1 (P1). Each pulse will last three machine cycles; two for DJNZ and one to alter the pin.

DJNZ Rn,rel8
Function: | Decrement register, Jump if Not Zero
Bytes: | 2
Clocks: | 24
Encoding: | 1 1 0 1 1 r r r r7 r6 r5 r4:r3 r2 r1 r0
Operation: | (PC) <- (PC) + 2 (Rn) <- (Rn) - 1 IF (Rn) != 0 THEN (PC) <- (PC) + rel8
Flags: | None

DJNZ addr8,rel8
Function: | Decrement directly addressed byte, Jump if Not Zero
Bytes: | 3
Clocks: | 24
Encoding: | 1 1 0 1 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0 r7 r6 r5 r4 r3 r2 r1 r0
Operation: | (PC) <- (PC) + 3 (addr8) <- (addr8) - 1 IF (addr8) != 0 THEN (PC) <- (PC) + rel8
Flags: | None


INC

Function: | Increment
Description: | INC increments the indicated variable by 1. An original value of 0xFF will overflow to 0x00. No flags are affected. Three addressing modes are allowed: register (with a special variant to increment the accumulator), direct, or register-indirect. Note: When this instruction is used to modify an output port, the value used as the original port data will be read from the output data latch, not the input pins.
Example: | Register 0 (R0) contains 0x7E (011111110B). Internal RAM locations 0x7E and 0x7F contain 0xFF and 0x40, respectively. The instruction sequence: INC @R0 INC R0 INC @R0 will leave register 0 set to 0x7F and internal RAM locations 0x7E and 0x7F holding 0x00 and 0x41 respectively.

INC A
Function: | Increment Accumulator by one
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 0 0 0 1 0 0
Operation: | (A) <- (A) + 1 (PC) <- (PC) + 1
Flags: | None

INC Rn
Function: | Increment register by one
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 0 0 1 r r r
Operation: | (Rn) <- (Rn) + 1 (PC) <- (PC) + 1
Flags: | None

INC addr8
Function: | Increment directly addressed byte by one
Bytes: | 2
Clocks: | 12
Encoding: | 0 0 0 0 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (addr8) <- (addr8) + 1 (PC) <- (PC) + 2
Flags: | None

INC @Ri
Function: | Increment indirectly addressed byte by one
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 0 0 0 1 1 i
Operation: | ((Ri)) <- ((Ri)) + 1 (PC) <- (PC) + 1
Flags: | None


INC DPTR

Function: | Increment Data Pointer
Description: | Increment the 16-bit data pointer by 1. A 16-bit increment (modulo 216) is performed. An overflow of the low-order byte of the data pointer (DPL) from 0xFF to 0x00 will increment the high-order byte (DPH). No flags are affected. This is the only 16-bit register which can be incremented.
Example: | Registers DPH and DPL contain 0x12 and 0xFE, respectively. The instruction sequence: INC DPTR INC DPTR INC DPTR will change DPH and DPL to 0x13 and 0x01.
Bytes: | 1
Clocks: | 24
Encoding: | 1 0 1 0 0 0 1 1
Operation: | (DPTR) <- (DPTR) + 1 (PC) <- (PC) + 1
Flags: | None


JB

JB bit,rel8

Function: | Jump if Bit is Set
Description: | If the indicated bit is a one, jump to the address indicated; otherwise proceed with the next instruction. The branch destination is computed by adding the signed relative-displacement in the third instruction byte to the PC, after incrementing the PC to the first byte of the next instruction. The bit tested is not modified. No flags are affected. Note: When this instruction is used to test an output pin, the value used as the original data will be read from the output data latch, not the input pin.
Example: | The data present at input port 1 (P1) is 0xCA (11001010B). The Accumulator holds 0x56 (01010110B). The instruction sequence: JB P1.2, Label1 JB ACC.2, Label2 will cause program execution to branch to the instruction at label "Label2".
Bytes: | 3
Clocks: | 24
Encoding: | 0 0 1 0 0 0 0 0 b7 b6 b5 b4 b3 b2 b1 b0 r7 r6 r5 r4 r3 r2 r1 r0
Operation: | (PC) <- (PC) + 3 IF (bit) = 1 THEN (PC) <- (PC) + rel8
Flags: | None


JBC

JBC bit,rel8

Function: | Jump if Bit is set and Clear the bit
Description: | If the indicated bit is a one, branch to the address indicated; otherwise proceed with the next instruction. The bit will not be cleared if it is already a zero. The branch destination is computed by adding the signed relative-displacement in the third instruction byte to the PC, after incrementing the PC to the first byte of the next instruction. No flags are affected. Note: When this instruction is used to test an output pin, the value used as the original data will be read from the output data latch, not the input pin.
Example: | The Accumulator holds 0x56 (01010110B). The instruction sequence: JBC ACC.3, Label1 JBC ACC.2, Label2 will cause program execution to continue at the instruction identified by the label "Label2", with the Accumulator modified to 0x52 (01010010B).
Bytes: | 3
Clocks: | 24
Encoding: | 0 0 0 1 0 0 0 0 b7 b6 b5 b4 b3 b2 b1 b0 r7 r6 r5 r4 r3 r2 r1 r0
Operation: | (PC) <- (PC) + 3 IF (bit) = 1 THEN (bit) <- 0 (PC) <- (PC) + rel8
Flags: | None


JC

JC rel8

Function: | Jump if Carry is Set
Description: | If the carry flag is set, branch to the address indicated; otherwise proceed with the next instruction. The branch destination is computed by adding the signed relative-displacement in the second instruction byte to the PC, after incrementing the PC twice. No flags are affected.
Example: | The carry flag is cleared. The instruction sequence: JC Label1 CPL C JC Label2 will set the carry and cause program execution to continue at the instruction identified by the label "Label2".
Bytes: | 2
Clocks: | 24
Encoding: | 0 1 0 0 0 0 0 0 r7 r6 r5 r4 r3 r2 r1 r0
Operation: | (PC) <- (PC) + 2 IF (C) = 1 THEN (PC) <- (PC) + rel8
Flags: | None


JMP

JMP @A+DPTR

Function: | Jump indirect
Description: | Add the eight-bit unsigned contents of the Accumulator with the sixteen-bit data pointer, and load the resulting sum to the program counter. This will be the address for subsequent instruction fetches. Sixteen-bit addition is performed (modulo 216): a carry-out from the low-order eight bits propagates through the higher-order bits. Neither the Accumulator nor the Data Pointer is altered. No flags are affected.
Example: | An even number from 0 to 6 is in the Accumulator. The following sequence of instructions will branch to one of four AJMP instructions in a jump table starting at the label "JmpTable": MOV DPTR,#JmpTable JMP @A+DPTR JmpTable: AJMP Label0 AJMP Label1 AJMP Label2 AJMP Label3 If the Accumulator equals 0x04 when starting this sequence, execution will jump to label "Label2". Remembers that AJMP is a two-byte instruction, so the jump instructions start at every other address.
Bytes: | 1
Clocks: | 24
Encoding: | 0 1 1 1 0 0 1 1
Operation: | (PC) <- (A) + (DPTR)
Flags: | None


JNB

JNB bit,rel8

Function: | Jump if Bit is Not set
Description: | If the indicated bit is a zero, branch to the indicated address; otherwise proceed with the next instruction. The branch destination is computed by adding the signed relative-displacement in the third instruction byte to the PC, after incrementing the PC to the first byte of the next instruction. The bit tested is not modified. No flags are affected.
Example: | The data present at input port 1 (P1) is 0xCA (11001010B). The Accumulator holds 0x56 (01010110B). The instruction sequence: JNB P1.3, Label1 JNB ACC.3, Label2 will cause program execution to continue at the instruction at label "Label2".
Bytes: | 3
Clocks: | 24
Encoding: | 0 0 1 1 0 0 0 0 b7 b6 b5 b4 b3 b2 b1 b0 r7 r6 r5 r4 r3 r2 r1 r0
Operation: | (PC) <- (PC) + 3 IF (bit) = 0 THEN (PC) <- (PC) + rel8
Flags: | None


JNC

JNC rel8

Function: | Jump if Carry flag is Not set
Description: | If the carry flag is a zero, branch to the address indicated; otherwise proceed with the next instruction. The branch destination is computed by adding the signed relative-displacement in the second instruction byte to the PC, after incrementing the PC twice to point to the next instruction. The carry flag is not modified.
Example: | The carry flag is set. The instruction sequence: JNC Label1 CPL C JNC Label2 will clear the carry and cause program execution to continue at the instruction identified by the label "Label2".
Bytes: | 2
Clocks: | 24
Encoding: | 0 1 0 1 0 0 0 0 r7 r6 r5 r4 r3 r2 r1 r0
Operation: | (PC) <- (PC) + 2 IF (C) = 0 THEN (PC) <- (PC) + rel8
Flags: | None


JNZ

JNZ rel8

Function: | Jump if the Accumulator is Not Zero
Description: | If any bit of the Accumulator is a one, branch to the indicated address; otherwise proceed with the next instruction. The branch destination is computed by adding the signed relative-displacement in the second instruction byte to the PC, after incrementing the PC twice. The Accumulator is not modified. No flags are affected.
Example: | The Accumulator originally holds 0x00. The instruction sequence: JNZ Label1 INC A JNZ Label2 will set the Accumulator to 0x01 and cause program execution to continue at the instruction identified by label "Label2".
Bytes: | 2
Clocks: | 24
Encoding: | 0 1 1 1 0 0 0 0 r7 r6 r5 r4 r3 r2 r1 r0
Operation: | (PC) <- (PC) + 2 IF (A) != 0 THEN (PC) <- (PC) + rel8
Flags: | None


JZ

JZ rel8

Function: | Jump if the Accumulator is Zero
Description: | If all bits of the Accumulator are zero, branch to the address indicated; otherwise proceed with the next instruction. The branch destination is computed by adding the signed relative-displacement in the second instruction byte to the PC, after incrementing the PC twice. The Accumulator is not modified. No flags are affected.
Example: | The Accumulator originally contains 0x01. The instruction sequence: JZ Label1 DEC A JZ Label2 will change the Accumulator to 0x00 and cause program execution to continue at the instruction identified by the label "Label2".
Bytes: | 2
Clocks: | 24
Encoding: | 0 1 1 0 0 0 0 0 r7 r6 r5 r4 r3 r2 r1 r0
Operation: | (PC) <- (PC) + 2 IF (A) = 0 THEN (PC) <- (PC) + rel8
Flags: | None


LCALL

LCALL addr16

Function: | Long Call
Description: | LCALL calls a subroutine located at the indicated address. The instruction adds three to the program counter to generate the address of the next instruction and then pushes the 16-bit result onto the stack (low byte first), incrementing the Stack Pointer (SP) by two. The high-order and low-order bytes of the PC are then loaded, respectively, with the second and third bytes of the LCALL instruction. Program execution continues with the instruction at this address. The subroutine may therefore begin anywhere in the full 64K-byte program memory address space. No flags are affected.
Example: | Initially the Stack Pointer equals 0x07. The label "Subrtn" is assigned to program memory location 0x1234. After executing the instruction: LCALL Subrtn at location 0x0123, the Stack Pointer will contain 0x09, internal RAM locations 0x08 and 0x09 will contain 0x26 and 0x01 respectively, and the PC will contain 0x1235.
Bytes: | 3
Clocks: | 24
Encoding: | 0 0 0 1 0 0 1 0 a15 a14 a13 a12 a11 a10 a9 a8 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (PC) <- (PC) + 3 (SP) <- (SP) + 1 ((SP)) <- (PC7-0) (SP) <- (SP) + 1 ((SP)) <- (PC15-8) (PC) <- addr16
Flags: | None


LJMP

LJMP addr16

Function: | Long Jump
Description: | LJMP causes an unconditional branch to the indicated address, by loading the high-order and low-order bytes of the PC (respectively) with the second and third instruction bytes. The destination may therefore be anywhere in the full 64 Kbyte program memory address space. No flags are affected.
Example: | The label "JmpAddr" is assigned to the instruction at program memory location 0x1234. The instruction: LJMP JmpAddr at location 0x0123 will load the program counter with 0x1234.
Bytes: | 3
Clocks: | 24
Encoding: | 0 0 0 0 0 0 1 0 a15 a14 a13 a12 a11 a10 a9 a8 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (PC) <- addr16
Flags: | None


MOV

Function: Move byte variable
Description: The byte variable indicated by the second operand is copied into the location specified by the first operand. The source byte is not affected. No other register or flag is affected.

This is by far the most flexible operation as fifteen combinations of source and destination addressing modes are allowed:
MOV A,Rn      MOV A,addr8      MOV A,@Ri      MOV A,#data8
MOV Rn,A      MOV Rn,addr8     MOV Rn,#data8   MOV addr8,A
MOV addr8,Rn  MOV addr8,addr8  MOV addr8,@Ri  MOV addr8,#data8
MOV @Ri,A     MOV @Ri,direct   MOV @Ri,#data8

Note: Mov A,ACC is not a valid instruction.

Example: Internal RAM location 0x30 holds 0x40. The value of RAM location 0x40 is 0x10. The data present at input port 1 (P1) is 0xCA (11001010B). The instruction sequence:
MOV R0, #0x30  ; R0 <- 0x30
MOV A, @R0    ; A <- 0x40
MOV R1, A     ; R1 <- 0x40
MOV B, @R1    ; B <- 0x10
MOV @R1, P1   ; RAM[0x40] <- OxCA
MOV P2, P1    ; P2 <- 0xCA
leaves the value 0x30 in register 0 (R0), 0x40 in both the Accumulator and register 1 (R1), 0x10 in register B, and 0xCA (11001010B) both in RAM location 0x40 and output on port 2 (P2).

MOV A,Rn
Function: | Move register to Accumulator
Bytes: | 1
Clocks: | 12
Encoding: | 1 1 1 0 1 r r r
Operation: | (A) <- (Rn) (PC) <- (PC) + 1
Flags: |

MOV A,addr8
Function: | Move directly addressed byte to Accumulator
Bytes: | 2
Clocks: | 12
Encoding: | 1 1 1 0 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (A) <- (addr8) (PC) <- (PC) + 2
Flags: |

MOV A,@Ri
Function: | Move indirectly addressed byte to Accumulator
Bytes: | 1
Clocks: | 12
Encoding: | 1 1 1 0 0 1 1 i
Operation: | (A) <- ((Ri)) (PC) <- (PC) + 1
Flags: |

MOV A,#data8
Function: | Move immediate data to Accumulator
Bytes: | 2
Clocks: | 12
Encoding: | 0 1 1 1 0 1 0 0 d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (A) <- data8 (PC) <- (PC) + 2
Flags: |

MOV Rn,A
Function: | Move Accumulator to register
Bytes: | 1
Clocks: | 12
Encoding: | 1 1 1 1 1 r r r
Operation: | (Rn) <- (A) (PC) <- (PC) + 1
Flags: |

MOV Rn,addr8
Function: | Move directly addressed byte to register
Bytes: | 2
Clocks: | 24
Encoding: | 1 0 1 0 1 r r r a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (Rn) <- (addr8) (PC) <- (PC) + 2
Flags: |

MOV Rn,#data8
Function: | Move immediate data to register
Bytes: | 2
Clocks: | 12
Encoding: | 0 1 1 1 1 r r r d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (Rn) <- data8 (PC) <- (PC) + 2
Flags: |

MOV addr8,A
Function: | Move Accumulator to directly addressed byte
Bytes: | 2
Clocks: | 12
Encoding: | 1 1 1 1 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (addr8) <- (A) (PC) <- (PC) + 2
Flags: |

MOV addr8,Rn
Function: | Move register to directly addressed byte
Bytes: | 2
Clocks: | 24
Encoding: | 1 0 0 0 1 r r r a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (addr8) <- (Rn) (PC) <- (PC) + 2
Flags: |

MOV addr8,addr8
Function: | Move directly addressed byte to another directly addressed byte Note that the order of the operands is dest,src.
Bytes: | 3
Clocks: | 24
Encoding: | 1 0 0 0 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0(Src) a7 a6 a5 a4 a3 a2 a1 a0(Dst)
Operation: | (addr8) <- (addr8) (PC) <- (PC) + 3
Flags: |

MOV addr8,@Ri
Function: | Move indirectly addressed RAM byte to directly addressed byte
Bytes: | 2
Clocks: | 24
Encoding: | 1 0 0 0 0 1 1 i a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (addr8) <- ((Ri)) (PC) <- (PC) + 2
Flags: |

MOV addr8,#data8
Function: | Move immediate data to directly addressed byte
Bytes: | 3
Clocks: | 24
Encoding: | 0 1 1 1 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0 d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (addr8) <- data8 (PC) <- (PC) + 3
Flags: |

MOV @Ri,A
Function: | Move Accumulator to indirectly addressed RAM byte
Bytes: | 1
Clocks: | 12
Encoding: | 1 1 1 1 0 1 1 i
Operation: | ((Ri)) <- (A) (PC) <- (PC) + 1
Flags: |

MOV @Ri,addr8
Function: | Move directly addressed byte to indirectly addressed RAM byte
Bytes: | 2
Clocks: | 24
Encoding: | 1 0 1 0 0 1 1 i a7 a6 a5 a4 a3 a2 a1 a0
Operation: | ((Ri)) <- (addr8) (PC) <- (PC) + 2
Flags: |

MOV @Ri,#data8
Function: | Move immediate data to indirectly addressed RAM byte
Bytes: | 2
Clocks: | 12
Encoding: | 0 1 1 1 0 1 1 i d7 d6 d5 d4 d3 d2 d1 d0
Operation: | ((Ri)) <- data8 (PC) <- (PC) + 2
Flags: |


MOV bit

Function: | Move bit data
Description: | The Boolean variable indicated by the second operand is copied into the location specified by the first operand. One of the operands must be the carry flag; the other may be any directly addressable bit. No other register or flag is affected.
Example: | The carry flag is originally set. The data present at input Port 3 (P3) is 0xC5 (11000101B). The data previously written to output Port 1 is 0x35 (00110101B). The instruction sequence: MOV P1.3, C MOV C, P3.3 MOV P1.2, C will leave the carry cleared and change Port 1 to 0x39 (00111001B).

MOV C,bit
Function: | Move bit data to Carry
Bytes: | 2
Clocks: | 12
Encoding: | 1 0 1 0 0 0 1 0 b7 b6 b5 b4 b3 b2 b1 b0
Operation: | (C) <- (bit) (PC) <- (PC) + 2
Flags: | C

MOV bit,C
Function: | Move Carry to bit data
Bytes: | 2
Clocks: | 24
Encoding: | 1 0 0 1 0 0 1 0 b7 b6 b5 b4 b3 b2 b1 b0
Operation: | (bit) <- (C) (PC) <- (PC) + 2
Flags: |


MOVC

Function: | Move Code byte
Description: | The MOVC instructions load the Accumulator with a code byte, or constant from program memory. The address of the byte fetched is the sum of the original unsigned eight-bit Accumulator contents and the contents of a sixteen-bit base register, which may be either the Data Pointer or the PC. In the latter case, the PC is incremented to the address of the following instruction before being added with the Accumulator; otherwise the base register is not altered. Sixteen-bit addition is performed so a carry-out from the low-order eight bits may propagate through higher-order bits. No flags are affected.
Example: | A value between 0 and 3 is in the Accumulator. The following instructions will translate the value in the Accumulator to one of four values defined by the DB (define byte) directive. Subrtn: INC A MOVC A, @A + PC RET DB 0x66 DB 0x77 DB 0x88 DB 0x99 If the subroutine is called with the Accumulator equal to 0x01, it will return with 0x77 in the Accumulator. The INC A before the MOVC instruction is needed to "get around" the RET instruction above the table. If several bytes of code separated the MOVC from the table, the corresponding number would need to be added to the Accumulator instead.

MOVC A,@A+DPTR
Function: | Move Code byte at (ACC + DPTR) to the Accumulator
Bytes: | 1
Clocks: | 24
Encoding: | 1 0 0 1 0 0 1 1
Operation: | (A) <- ((A) + (DPTR)) (PC) <- (PC) + 1
Flags: | None

MOVC A,@A+PC
Function: | Move Code byte at (ACC + PC) to the Accumulator
Bytes: | 1
Clocks: | 24
Encoding: | 1 0 0 0 0 0 1 1
Operation: | (PC) <- (PC) + 1 (A) <- ((A) + (PC))
Flags: | None


MOV DPTR

MOV DPTR,#data16

Function: | Load the Data Pointer (the combination of DPH and DPL) with a 16 bit constant.
Description: | The Data Pointer is loaded with the indicated 16-bit constant. The 16-bit constant is held in the second and third bytes of the instruction. The second byte holds the high-order byte loaded into DPH while the third byte holds the low-order byte loaded into DPL. No flags are affected. This is the only instruction which moves 16-bits of data at once.
Example: | The instruction: MOV DPTR, 0x1234 will load the value 0x1234 into the Data Pointer: DPH will hold 0x12 and DPL will hold 0x34.
Bytes: | 3
Clocks: | 24
Encoding: | 1 0 0 1 0 0 0 0 d15 d14 d13 d12 d11 d10 d9 d8 d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (DPH) <- data1615-8 (DPL) <- data167-0 (PC) <- (PC) + 3
Flags: | None


MOVX

Function: Move External
Description: The MOVX instruction transfers data between the Accumulator and a byte of external data memory, hence the "X" appended to MOV. There are two types of MOVX instruction, differing in whether they provide an eight-bit or sixteen-bit indirect address to the external data RAM.

In the first type, the contents of R0 or R1 in the current register bank provide an eight-bit address multiplexed with data on P0. Eight bits are sufficient for external I/O expansion decoding or for a relatively small RAM array. For somewhat larger arrays, port pins can be used to output higher-order address bits. These pins would be controlled by an output instruction preceding the MOVX.

In the second type of MOVX instruction, the Data Pointer generates a sixteen-bit address. P2 outputs the high-order eight address bits (the contents of DPH) while P0 multiplexes the low-order eight bits (DPL) with data. The P2 Special Function Register (SFR) retains its previous contents while the P2 output buffers are emitting the contents of DPH. This form is faster and more efficient when accessing very large data arrays (up to 64 Kbytes), since no additional instructions are needed to set up the output ports.

It is possible in some situation to mix the two MOVX types. A large RAM array with its high-order address lines driven by P2 can be addressed via the Data Pointer, or with code to output high-order address bits to P2 followed by a MOVX instruction using R0 or R1.

Example: An external 256 byte RAM using multiplexed address/data lines is connected to the 80C51 Port 0. Port 3 provides control lines for the external RAM. Ports 1 and 2 are used for normal I/O. Registers 0 and 1 contain 0x12 and 0x34. Location 0x34 of the external RAM holds the value 0x56. The instruction sequence:
MOVX A, @R1
MOVX @R0, A
copies the value 0x56 into both the Accumulator and external RAM location 0x12.

MOVX A,@Ri
Function: | Move external indirectly addressed byte to Accumulator
Bytes: | 1
Clocks: | 24
Encoding: | 1 1 1 0 0 0 1 i
Operation: | (A) <- ((Ri)) (PC) <- (PC) + 1
Flags: | None

MOVX A,@DPTR
Function: | Move external byte at DPTR to Accumulator
Bytes: | 1
Clocks: | 24
Encoding: | 1 1 1 0 0 0 0 0
Operation: | (A) <- ((DPTR)) (PC) <- (PC) + 1
Flags: | None

MOVX @Ri,A
Function: | Move Accumulator to external indirectly addressed byte
Bytes: | 1
Clocks: | 24
Encoding: | 1 1 1 1 0 0 1 i
Operation: | ((Ri)) <- (A) (PC) <- (PC) + 1
Flags: | None

MOVX @DPTR,A
Function: | Move Accumulator to external byte at DPTR
Bytes: | 1
Clocks: | 24
Encoding: | 1 1 1 1 0 0 0 0
Operation: | ((DPTR)) <- (A) (PC) <- (PC) + 1
Flags: | None


MUL

MUL AB

Function: | Multiply
Description: | MUL AB multiplies the unsigned eight-bit integers in the Accumulator and register B. The low-order byte of the sixteen-bit product is left in the Accumulator, and the high-order byte in B. If the product is greater than 255 (OxFF) the overflow flag is set; otherwise it is cleared. The carry flag is always cleared.
Example: | Originally the Accumulator holds the value 80 (0x50). Register B holds the value 160 (OxAO). The instruction: MUL AB will give the product 12,800 (0x3200), so B is changed to 0x32 (00110010B) and the Accumulator is cleared. The overflow flag is set, carry is cleared.
Bytes: | 1
Clocks: | 48
Encoding: | 1 0 1 0 0 1 0 0
Operation: | (A) <- (AB)7-0 (B) <- (AB)15-8 (PC) <- (PC) + 1
Flags: | OV C


NOP

Function: | No Operation
Description: | Execution continues at the following instruction. Other than the PC, no registers or flags are effected.
Example: | It is desired to produce a low-going output pulse on bit 7 of Port 2 (P2.7) lasting exactly 5 cycles. A simple SETB/CLR sequence would generate a one-cycle pulse, so four additional cycles must be inserted. This may be done (assuming no interrupts are enabled) with the instruction sequence: CLR P2.7 NOP NOP NOP NOP SETB P2.7
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 0 0 0 0 0 0
Operation: | (PC) <- (PC) + 1
Flags: | None


ORL

Function: | Logical-OR for byte variables
Description: | ORL performs the bitwise logical-OR operation between the indicated variables, storing the results in the destination byte. No flags are affected. The two operands allow six addressing mode combinations. When the destination is the Accumulator, the source can use register, direct, register-indirect, or immediate addressing; when the destination is a direct address, the source can be the Accumulator or immediate data. Note: When this instruction is used to modify an output port, the value used as the original port data will be read from the output data latch, not the input pins.
Example: | If the Accumulator holds 0xC3 (11000011B) and Register 0 (R0) holds 0x55 (01010101B) then the instruction: ORL A, R0 will leave the Accumulator holding the value 0xD7 (11010111B). When the destination is a directly addressed byte, the instruction can set combinations of bits in any RAM location or hardware register. The pattern of bits to be set is determined by a mask byte, which may be either a constant data value in the instruction or a variable computed in the Accumulator at run-time. The instruction: ORL P1, #0x32 ; 00110010B will set bits 5, 4, and 1 of output Port 1 (P1).

ORL A,Rn
Function: | Logical OR register byte to Accumulator
Bytes: | 1
Clocks: | 12
Encoding: | 0 1 0 0 1 r r r
Operation: | (A) <- (A) | (Rn) where | represents the OR operation (PC) <- (PC) + 1
Flags: | None

ORL A,addr8
Function: | Logical OR directly addressed byte to Accumulator
Bytes: | 2
Clocks: | 12
Encoding: | 0 1 0 0 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (A) <- (A) | (addr8) where | represents the OR operation (PC) <- (PC) + 2
Flags: | None

ORL A,@Ri
Function: | Logical OR indirectly addressed byte to Accumulator
Bytes: | 1
Clocks: | 12
Encoding: | 0 1 0 0 0 1 1 i
Operation: | (A) <- (A) | ((Ri)) where | represents the OR operation (PC) <- (PC) + 1
Flags: | None

ORL A,#data8
Function: | Logical OR immediate data to Accumulator
Bytes: | 2
Clocks: | 12
Encoding: | 0 1 0 0 0 1 0 0 d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (A) <- (A) | data8 where | represents the OR operation (PC) <- (PC) + 2
Flags: | None

ORL addr8,A
Function: | Logical OR Accumulator to directly addressed byte
Bytes: | 2
Clocks: | 12
Encoding: | 0 1 0 0 0 0 1 0 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (addr8) <- (addr8) | (A) where | represents the OR operation (PC) <- (PC) + 2
Flags: | None

ORL addr8,#data8
Function: | Logical OR immediate data to directly addressed byte
Bytes: | 3
Clocks: | 24
Encoding: | 0 1 0 0 0 0 1 1 a7 a6 a5 a4 a3 a2 a1 a0 d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (addr8) <- (addr8) | data8 where | represents the OR operation (PC) <- (PC) + 3
Flags: | None


ORL BIT

ORL C

Function: | Logical OR for bit variables
Description: | Set the carry flag if the Boolean value is a logical 1; leave the carry in its current state otherwise. A slash ("/") preceding the operand in the assembly language indicates that the logical complement of the addressed bit is to be used as the source value (this does not modify the source bit). No other flags are affected.
Example: | Set the carry flag if and only if P1.0 = 1, ACC.7 = 1, or OV = 0. MOV C, P1.0 ; LOAD CARRY WITH INPUT PIN P1.0 ORL C, ACC.7 ; OR CARRY WITH THE ACC BIT 7 ORL C,/OV ; OR CARRY WITH THE INVERSE OF OV

ORL C,bit
Function: | Logical OR bit variable to Carry
Bytes: | 2
Clocks: | 24
Encoding: | 0 1 1 1 0 0 1 0 b7 b6 b5 b4 b3 b2 b1 b0
Operation: | (C) <- (C) | (bit) where | represents the OR operation (PC) <- (PC) + 2
Flags: | C

ORL C,/bit
Function: | Logical OR complement of bit variable to Carry
Bytes: | 2
Clocks: | 24
Encoding: | 1 0 1 0 0 0 0 0 b7 b6 b5 b4 b3 b2 b1 b0
Operation: | (C) <- (C) | ~(bit) where ~ represents the complement operation (PC) <- (PC) + 2
Flags: | C


POP

POP addr8

Function: | Pop from the stack
Description: | The content of the internal RAM location addressed by the Stack Pointer (SP) is read, and the Stack Pointer is decremented by one. The value read is then transferred to the directly addressed byte indicated. No flags are affected.
Example: | The Stack Pointer originally contains the value 0x32, and internal RAM locations 0x30 through 0x32 contain the values 0x20, 0x23, and 0x01, respectively. The instruction sequence: POP DPH POP DPL will leave the Stack Pointer equal to the value 0x30 and the Data Pointer set to 0x0123. At this point the instruction, POP SP will leave the Stack Pointer set to 0x20. Note that in this special case the Stack Pointer was decremented to 0x2F before being loaded with the value popped (0x20).
Bytes: | 2
Clocks: | 24
Encoding: | 1 1 0 1 0 0 0 0 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (addr8) <- ((SP)) (SP) <- (SP) -1 (PC) <- (PC) + 2
Flags: | None


PUSH

PUSH addr8

Function: | Push onto the stack
Description: | First, the Stack Pointer is incremented by one. Then the content of the indicated variable is copied into the internal RAM location addressed by the Stack Pointer. Otherwise no flags are affected.
Example: | On entering an interrupt routine the Stack Pointer contains 0x09 and the Data Pointer (DPTR) holds the value 0x0123. The instruction sequence: PUSH DPL PUSH DPH will leave the Stack Pointer set to 0x0B and store 0x23 and 0x01 in internal RAM locations 0x0A and 0x0B, respectively.
Bytes: | 2
Clocks: | 24
Encoding: | 1 1 0 0 0 0 0 0 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (SP) <- (SP) + 1 ((SP)) <- (addr8) (PC) <- (PC) + 2
Flags: | None


RET

Function: | Return from subroutine
Description: | RET pops the high and low-order bytes of the PC successively from the stack, decrementing the Stack Pointer by two. Program execution continues at the resulting address, generally the instruction immediately following an ACALL or LCALL. No flags are affected.
Example: | The Stack Pointer originally contains the value 0x0B and internal RAM locations 0x0A and 0x0B contain the values 0x23 and 0x01, respectively. The instruction: RET will leave the Stack Pointer equal to the value 0x09. Program execution will continue at location 0x0123.
Bytes: | 1
Clocks: | 24
Encoding: | 0 0 1 0 0 0 1 0
Operation: | (PC15-8) <- ((SP)) (SP) <- (SP) - 1 (PC7-0) <- ((SP)) (SP) <- (SP) - 1
Flags: | None


RETI

Function: | Return from interrupt
Description: | RETI pops the high and low-order bytes of the PC successively from the stack, and restores the interrupt logic to accept additional interrupts at the same priority level as the one just processed. Program execution continues at the resulting address, which is generally the instruction immediately after the point at which the interrupt request was detected. The Program Status Word (PSW) register is NOT automatically restored to its pre-interrupt status. The Stack Pointer is left decremented by two. No other registers are affected. If a lower or-same-level interrupt had been pending when the RETI instruction is executed, the RETI instruction will be executed before the pending interrupt is processed.
Example: | The Stack Pointer originally contains the value 0x0B. An interrupt is detected during the instruction ending at location 0x0122. Internal RAM locations 0x0A and 0x0B contain the values 0x23 and 0x01, respectively. The instruction: RETI will leave the Stack Pointer equal to 0x09 and return program execution to location 0x0123.
Bytes: | 1
Clocks: | 24
Encoding: | 0 0 1 1 0 0 1 0
Operation: | (PC15-8) <- ((SP)) (SP) <- (SP) - 1 (PC7-0) <- ((SP)) (SP) <- (SP) - 1
Flags: | None


RL

RL A

Function: | Rotate Accumulator Left
Description: | The eight bits in the Accumulator are rotated one bit to the left. The most significant bit, bit 7, is rotated into the bit 0 position. No flags are affected.
Example: | The Accumulator holds the value 0xC5 (11000101B). The instruction: RL A leaves the Accumulator holding the value 0x8B (10001011B) with the Carry flag unaffected.
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 1 0 0 0 1 1
Operation: | (An + 1) <- (An) for n = {0-6} (A0) <- (A7) (PC) <- (PC) + 1
Flags: | None


RLC

RLC A

Function: | Rotate Accumulator Left through the Carry flag
Description: | The eight bits in the Accumulator and the carry flag are together rotated one bit to the left. The most significant bit, bit 7, moves into the carry flag and the original state of the carry flag moves into the bit 0 position. No other flags are affected.
Example: | The Accumulator holds the value 0xC5 (11000101B), and the carry is zero. The instruction: RCL A leaves the Accumulator holding the value 0x8A (10001010B) with the carry set.
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 1 1 0 0 1 1
Operation: | (An + 1) <- (An) for n = {0-6} (A0) <- (C) (C) <- (A7) (PC) <- (PC) + 1
Flags: | C


RR

RR A

Function: | Rotate Accumulator Right
Description: | The eight bits in the Accumulator are rotated one bit to the right. The least significant bit, bit 0, is rotated into the bit 7 position. No flags are affected.
Example: | The Accumulator holds the value 0xC5 (11000101B). The instruction: RR A leaves the Accumulator holding the value 0xE2 (11100010B) with the carry unaffected.
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 0 0 0 0 1 1
Operation: | (An) <- (An + 1) for n = {0-6} (A7) <- (A0) (PC) <- (PC) + 1
Flags: | None


RRC

RRC A

Function: | Rotate Accumulator Right through the Carry flag
Description: | The eight bits in the Accumulator and the carry flag are together rotated one bit to the right. The least significant bit, bit 0, moves into the carry flag and the original value of the carry flag moves into the bit 7 position. No other flags are affected.
Example: | The Accumulator holds the value 0xC5 (11000101B), and the carry is zero. The instruction: RRC A leaves the Accumulator holding the value 0x62 (01100010B) with the carry set.
Bytes: | 1
Clocks: | 12
Encoding: | 0 0 0 1 0 0 1 1
Operation: | (An) <- (An + 1) for n = {0-6} (A7) <- (C) (C) <- (A0) (PC) <- (PC) + 1
Flags: | C


SETB

SETB bit

Function: | Set Bit
Description: | SETB sets the indicated bit to one. SETB can operate on the carry flag or any directly addressable bit. No other flags are affected.
Example: | The carry flag is cleared. Output Port 1 (P1) has been written with the value 0x34 (00110100B). The instructions: SETB C SETB P1.0 will leave the carry flag set to 1 and change the data output on Port 1 to 0x35 (00110101B).

SETB C
Function: | Set the Carry
Bytes: | 1
Clocks: | 12
Encoding: | 1 1 0 1 0 0 1 1
Operation: | (C) <- 1 (PC) <- (PC) + 1
Flags: | C

SETB bit
Function: | Set bit variable
Bytes: | 2
Clocks: | 12
Encoding: | 1 1 0 1 0 0 1 0 b7 b6 b5 b4 b3 b2 b1 b0
Operation: | (bit) <- 1 (PC) <- (PC) + 2
Flags: |


SJMP

SJMP rel8

Function: | Short Jump
Description: | Program control branches unconditionally to the address indicated. The branch destination is computed by adding the signed displacement in the second instruction byte to the PC, after incrementing the PC twice. Therefore, the range of destinations allowed is from 128 bytes preceding this instruction to 127 bytes following it.
Example: | The label "RelAddr" is assigned to an instruction at program memory location 0x0123. Furthermore, assume that the following instruction is located at location 0x0100: SJMP RelAddr After this instruction is executed, the PC will contain the value 0x0123. In this scenario, the instruction following the SJMP will be at 0x0102. Therefore, the displacement byte of the instruction will be the relative offset (0x0123 - 0x0102) = 0x21. Put another way, an SJMP with a displacement of 0xFE (-2) would be a one-instruction infinite loop.
Bytes: | 2
Clocks: | 24
Encoding: | 1 0 0 0 0 0 0 0 r7 r6 r5 r4 r3 r2 r1 r0
Operation: | (PC) <- (PC) + 2 (PC) <- (PC) + rel8
Flags: | None


SUBB

Function: | Subtract with Borrow
Description: | SUBB subtracts the indicated variable and the carry flag together from the Accumulator, leaving the result in the Accumulator (the carry flag also functions as the borrow flag). SUBB sets the carry (borrow) flag if a borrow is needed for bit 7, and clears C otherwise. If C was set before executing a SUBB instruction, this indicates that a borrow was needed for the previous step in a multiple precision substraction, so the carry is subtracted from the Accumulator along with the source operand. AC is set if a borrow is needed for bit 3, and cleared otherwise. OV is set if a borrow is needed into bit 6 but not into bit 7, or into bit 7 but not bit 6. When subtracting signed integers OV indicates that a negative number was produced when a negative value was subtracted from a positive value, or that a positive number was produced when a positive number was subtracted from a negative number. The source operand allows four addressing modes: register, direct, register-indirect, or immediate.
Example: | The Accumulator holds 0xC9 (11001001B), register 2 (R2) holds 0x54 (01010100B), and the carry flag is set. The instruction: SUBB A, R2 will leave the value 0x74 (01110100B) in the accumulator, with the carry flag and AC cleared but OV set. Notice that 0xC9 minus 0x54 is 0x75. The difference between this and the above result is due to the carry (borrow) flag being set before the operation. If the state of the carry is not known before starting a single or multiple-precision substraction, it should be explicity cleared by a CLR C instruction.

SUBB A,Rn
Function: | Subtract register from Accumulator along with the borrow
Bytes: | 1
Clocks: | 12
Encoding: | 1 0 0 1 1 r r r
Operation: | (A) <- (A) - (C) - (Rn) (PC) <- (PC) + 1
Flags: | C AC OV

SUBB A,addr8
Function: | Subtract a directly addressed byte from the Accumulator along with the borrow
Bytes: | 2
Clocks: | 12
Encoding: | 1 0 0 1 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (A) <- (A) - (C) - (addr8) (PC) <- (PC) + 2
Flags: | C AC OV

SUBB A,@Ri
Function: | Subtract an indirectly addressed RAM byte from the Accumulator along with the borrow
Bytes: | 1
Clocks: | 12
Encoding: | 1 0 0 1 0 1 1 i
Operation: | (A) <- (A) - (C) - ((Ri)) (PC) <- (PC) + 1
Flags: | C AC OV

SUBB A,#data8
Function: | Subtract immediate data from the Accumulator along with the borrow
Bytes: | 2
Clocks: | 12
Encoding: | 1 0 0 1 0 1 0 0 d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (A) < (A) - (C) - data8 (PC) <- (PC) + 2
Flags: | C AC OV


SWAP

SWAP A

Function: | Swap nibbles within the Accumulator
Description: | SWAP A interchanges the low and high-order nibbles (four-bit fields) of the Accumulator (bits 3-0 and bits 7-4). This operation can also be thought of as a four-bit rotate instruction. No flags are affected.
Example: | The Accumulator holds the value 0xC5 (11000101B). The instruction: SWAP A leave the Accumulator holding the value 0x5C (01011100B).
Bytes: | 1
Clocks: | 12
Encoding: | 1 1 0 0 0 1 0 0
Operation: | (A3-0) «» (A7-4) (PC) <- (PC) + 1
Flags: | None


XCH

Function: | Exchange Accumulator with byte variable
Description: | XCH loads the Accumulator with the contents of the indicated variable, at the same time writing the original Accumulator contents to the indicated variable. The source/destination operand can use register, direct or register-indirect addressing.
Example: | Assume Register 0 (R0) contains 0x20 (00100000B), the Accumulator holds 0x3F (00111111B), and internal RAM location 0x20 holds 0x75 (01110101B). The instruction: XCH A, @R0 will leave RAM location 0x20 holding the value 0x3F (00111111B) and will leave 0x75 (01110101B) in the Accumulator.

XCH A,Rn
Function: | Exchange Accumulator with register
Bytes: | 1
Clocks: | 12
Encoding: | 1 1 0 0 1 r r r
Operation: | (A) «» (Rn) (PC) <- (PC) + 1
Flags: | None

XCH A,addr8
Function: | Exchange Accumulator with directly addressed byte
Bytes: | 2
Clocks: | 12
Encoding: | 1 1 0 0 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (A) «» (addr8) (PC) <- (PC) + 2
Flags: | None

XCH A,@Ri
Function: | Exchange Accumulator with indirectly addressed RAM byte
Bytes: | 1
Clocks: | 12
Encoding: | 1 1 0 0 0 1 1 i
Operation: | (A) «» ((Ri)) (PC) <- (PC) + 1
Flags: | None


XCHD

XCHD A,@Ri

Function: | Exchange Digit
Description: | XCHD exchanges the low-order nibble of the Accumulator (bits 3-0) with the low-order nibble of that internal RAM location indirectly addressed by the specified register. The high order nibbles (bits 7-4) of each operand are not affected. No flags are affected. This operation is useful when working with BCD digits.
Example: | Assume register 0 (R0) contains 0x20, the Accumulator holds 0x36 (00110110B), and internal RAM location 0x20 holds 0x75 (01110101B). The instruction: XHCD A,@R0 will leave RAM location 0x20 holding the value 0x76 (01110110B) and will leave 0x35 (00110101B) in the Accumulator.
Bytes: | 1
Clocks: | 12
Encoding: | 1 1 0 1 0 1 1 i
Operation: | (A3-0) «» ((Ri3-0)) (PC) <- (PC) + 1
Flags: |


XRL

Function: | Logical Exclusive OR for byte variables
Description: | XRL performs the bitwise logical Exclusive OR operation between the indicated variables, storing the result in the destination variable. No flags are affected. The two operands allow six addressing mode combinations. When the destination is the Accumulator, the source can use register, direct, register-indirect, or immediate addressing; when the destination is a direct address, the source can be the Accumulator or immediate data. Note: When this instruction is used to modify an output port, the value used as the original port data will be read from the output data latch, not the input pins.
Example: | If the Accumulator holds 0xC3 (11000011B) and register 0 (R0) holds 0xAA (10101010B) then the instruction: XRL A, R0 will leave the Accumulator holding 0x69 (01101001B). When the destination is a directly addressed byte, this instruction can complement combinations of bits in any RAM location or hardware register. The mask byte determining the pattern of bits to be complemented would either be a constant contained in the instruction or a value computed in the Accumulator at run-time. The instruction: XRL P1, #0x31 ;00110001B will complement bits 5, 4, and 0 of output port 1 (P1).

XRL A,Rn
Function: | Logical Exclusive OR register byte to Accumulator
Bytes: | 1
Clocks: | 12
Encoding: | 0 1 1 0 1 r r r
Operation: | (A) <- (A) XOR (Rn) (PC) <- (PC) + 1
Flags: | None

XRL A,addr8
Function: | Logical Exclusive OR directly addressed byte to Accumulator
Bytes: | 2
Clocks: | 12
Encoding: | 0 1 1 0 0 1 0 1 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (A) <- (A) XOR (addr8) (PC) <- (PC) + 2
Flags: | None

XRL A,@Ri
Function: | Logical Exclusive OR indirectly addressed RAM byte to Accumulator
Bytes: | 1
Clocks: | 12
Encoding: | 0 1 1 0 0 1 1 i
Operation: | (A) <- (A) XOR ((Ri)) (PC) <- (PC) + 1
Flags: | None

XRL A,#data8
Function: | Logical Exclusive OR immediate data to Accumulator
Bytes: | 2
Clocks: | 12
Encoding: | 0 1 1 0 0 1 0 0 d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (A) <- (A) XOR data8 (PC) <- (PC) + 2
Flags: | None

XRL addr8,A
Function: | Logical Exclusive OR Accumulator to directly addressed byte
Bytes: | 2
Clocks: | 12
Encoding: | 0 1 1 0 0 0 1 0 a7 a6 a5 a4 a3 a2 a1 a0
Operation: | (addr8) <- (addr8) XOR A (PC) <- (PC) + 2
Flags: | None

ANL addr8,#data8
Function: | Logical Exclusive OR immediate data to directly addressed byte
Bytes: | 3
Clocks: | 24
Encoding: | 0 1 1 0 0 0 1 1 a7 a6 a5 a4 a3 a2 a1 a0 d7 d6 d5 d4 d3 d2 d1 d0
Operation: | (addr8) <- (addr8) XOR data8 (PC) <- (PC) + 3
Flags: | None