INSTRUCTION SET
What is a Microcontroller and Assembly Language?
Think of a microcontroller as a tiny computer on a single chip. The 8051 is a popular and
foundational microcontroller used in many electronic devices. To make it do things, you need
to give it a set of instructions. This is where assembly language comes in. It's a low-level
programming language that's much closer to what the hardware can directly understand
compared to languages like Python or C++. You give the microcontroller simple, precise
commands in a specific order to perform a task. These commands are collectively known as
an instruction set.
The 8051 has a set of 255 instructions it understands. Each instruction has a unique 8-bit
code called an opcode. However, remembering these numeric codes is difficult, so we use
easy-to-remember names called mnemonics, like MOV for move or ADD for addition.
Getting Around: How the 8051 Finds Data (Addressing Modes)
Before we start programming, you need to understand how the 8051 accesses data. This is
where addressing modes come in. They are the different ways an instruction can specify the
data (operand) it needs to work with. The 8051 has five main addressing modes:
1. Immediate Addressing: The data is provided directly in the instruction itself. You'll
see this with a '#' symbol.
o Example from your lab: In MOV A, #97H, you are telling the 8051 to move
the hexadecimal value 97 directly into a special register called the
Accumulator (A).
2. Direct Addressing: The instruction contains the memory address where the data is
stored. The 8051 has an internal RAM, and you can directly access its locations.
o Example from your lab: MOV 55H, A tells the 8051 to copy the value from the
Accumulator into the memory location with the address 55H.
3. Register Addressing: This mode uses one of the eight general-purpose registers (R0
to R7) to hold the data.
o Example from your lab: In ADD A, R0, the 8051 is instructed to add the value
stored in the R0 register to the value in the Accumulator.
4. Register Indirect Addressing: Instead of the register holding the data, it holds the
address of the data. This is indicated by an '@' symbol. Only registers R0 and R1 can
be used for this.
o Example from your lab: MOV A,@R0 means "Go to the address that is
currently stored in the R0 register, get the data from that address, and move
it into the Accumulator."
5. Indexed Addressing: This is mainly used for reading data from the program memory
(ROM). It calculates the source address by adding the content of the Accumulator
with another register, either the Data Pointer (DPTR) or the Program Counter (PC).
o Example from your lab: MOVX @DPTR, A is a bit different as it deals with
external memory. A similar concept for program memory is MOVC A,
@A+DPTR. The C in MOVC stands for code memory.
The Building Blocks: 8051 Instruction Set
The 8051 instructions are divided into five main groups based on what they do.
1. Data Transfer Instructions transfer data between registers and memory.
• MOV: This is the most common instruction and is used to move data around. You've
already seen several examples like MOV A, #97H.
• MOVX: This is used to move data to or from external data memory. In your lab,
MOVX @DPTR, A moves the data from the accumulator to the external memory
location pointed to by the DPTR (Data Pointer).
• PUSH and POP: These are used to store data on a special area of memory called the
stack and retrieve it later.
2. Arithmetic Instructions for math operations.
• ADD: Adds two numbers. The result is always stored in the Accumulator (A). For
example, ADD A, #76H adds 76H to the current value in A.
• ADDC: "Add with carry." This is used for adding numbers larger than 8 bits (e.g., 16-
bit numbers). It includes the value of the carry flag (CY) from a previous addition. In
your 16-bit addition lab task, you first add the lower bytes with ADD and then the
higher bytes with ADDC A, #3BH to include any carry-over.
• SUBB: "Subtract with borrow." This subtracts one number from another and also
subtracts the value of the carry flag (which acts as a borrow flag in subtraction).
• INC: Increments (adds 1 to) a value. Example: INC R0.
• DEC: Decrements (subtracts 1 from) a value.
• MUL AB: Multiplies the values in the Accumulator (A) and the B register. The result is
a 16-bit number, with the lower byte in A and the higher byte in B.
• DIV AB: Divides the value in A by the value in B. The quotient is stored in A, and the
remainder is in B.
3. Logical Instructions perform bit-wise operations.
• ANL (AND), ORL (OR), XRL (XOR): These perform the standard logical operations on a
bit-by-bit basis. For instance, ANL A, 22H performs a logical AND between the
content of the accumulator and the data at memory address 22H.
• CPL: Complements or inverts the bits of the operand (flips 0s to 1s and 1s to 0s). For
example, CPL A.
• RL, RR, RLC, RRC: These instructions rotate the bits in the accumulator left or right,
either with or without including the carry flag.
4. Boolean or Bit-Oriented Instructions work on single bits.
This is a powerful feature of the 8051. You can manipulate individual bits in certain memory
locations and registers.
• CLR: Clears a bit (makes it 0). Example: CLR P1.7 would set the 7th bit of Port 1 to 0.
• SETB: Sets a bit (makes it 1). Example: SETB 7FH sets the most significant bit of
memory location 2FH to 1.
• CPL: Complements a single bit. Example: CPL P2.7 toggles the state of the 7th bit of
Port 2.
5. Program Branching Instructions control the flow of your program.
These instructions allow you to create loops and make decisions.
• LJMP, SJMP: Long Jump and Short Jump. These are unconditional jumps to another
part of the program.
• LCALL, ACALL: Long Call and Absolute Call. These are used to call subroutines
(reusable blocks of code).
• Conditional Jumps: These instructions check a condition (usually a flag) and jump
only if the condition is true.
o JNZ (Jump if Not Zero): DJNZ R0, LOOP is a very common looping instruction.
It decrements R0 and, if R0 is not yet zero, it jumps to the label LOOP. You
used this to add 5 to the accumulator 20 times.
o JNC (Jump if No Carry): Jumps if the carry flag is 0. You used this in your
temperature sensing task with JNC NEXT.
o CJNE (Compare and Jump if Not Equal): This compares two values and jumps
if they are different. You used this to check if the temperature was exactly
30H: CJNE A, #30H, OVER.
Keeping Track: The Flags
Many instructions, especially arithmetic ones, affect flags, which are 1-bit registers that tell
you something about the result of the last operation. These flags are stored in the Program
Status Word (PSW) register. The most important ones are:
• CY (Carry Flag): Set to 1 if there's a carry-out from the 7th bit during addition or a
borrow during subtraction. It's crucial for multi-byte arithmetic.
• AC (Auxiliary Carry Flag): Used for BCD (Binary Coded Decimal) arithmetic.
• OV (Overflow Flag): Indicates an error in signed arithmetic when the result is too
large to fit in the given bits.
• P (Parity Flag): Indicates whether the number of 1s in the result is even or odd.
EXAMPLE
The Goal
Our program will examine a list of 5 single-byte numbers stored in the 8051's internal RAM,
starting at memory address 30H. It will find the largest number in this list and store the
result in memory address 40H.
For our example, let's assume the following numbers are already in memory:
• Memory location 30H = 12H
• Memory location 31H = 56H
• Memory location 32H = 25H
• Memory location 33H = 89H
• Memory location 34H = 4FH
The program should find that 89H is the largest number and place it in memory location
40H.
The Complete Code
Here is the full assembly language program to accomplish our goal.
ORG 0000H ; Start program at memory location 0
; --- Initialization ---
MOV R0, #30H ; R0 points to the start of our list (address 30H)
MOV R2, #05H ; R2 is our loop counter, for 5 numbers
MOV A, @R0 ; Get the first number and assume it's the largest for now
INC R0 ; Point to the next number in the list
DEC R2 ; Decrement counter since we've handled the first number
; --- The Main Loop ---
LOOP:
MOV B, @R0 ; Get the next number from the list into register B
SUBB A, B ; Subtract B from A (A - B) to compare them
JNC NEXT_NUM ; If Carry Flag is 0, A was bigger or equal, so jump
MOV A, B ; If Carry Flag was 1, B was bigger, so update A with the new largest
NEXT_NUM:
INC R0 ; Point to the next number in the list
DJNZ R2, LOOP ; Decrement counter and jump back to LOOP if not zero
; --- Storing the Result ---
MOV 40H, A ; Store the final largest number (in A) into address 40H
END ; End of the program
Line-by-Line Breakdown
Let's dissect the program section by section to understand what each instruction does.
Section 1: Initialization
This part of the code sets up our registers and initial values before we start the main work.
• ORG 0000H: This is an "assembler directive," not an instruction for the 8051 itself. It
tells the assembler (the tool that converts this code into machine code) to place the
program starting at memory address 0000H in the program memory (ROM).
• MOV R0, #30H: This is a Data Transfer instruction.
o Instruction: MOV (Move).
o What it does: It copies the value 30H into the register R0.
o Addressing Mode: Immediate. We know this because of the # symbol, which
means we are using the actual value 30H, not the data at address 30H.
o Purpose: We are setting up R0 to be a pointer. It will hold the memory
address of the number we are currently looking at in our list. It starts by
pointing to the first number.
• MOV R2, #05H: Another Data Transfer instruction.
o What it does: It copies the value 05H (which is 5 in decimal) into register R2.
o Addressing Mode: Immediate (again, because of the #).
o Purpose: R2 will act as our loop counter. We have 5 numbers to check, so we
initialize it to 5.
• MOV A, @R0: A very important Data Transfer instruction.
o What it does: It copies the data from the memory location pointed to by R0
into the A register (the Accumulator). Since R0 holds 30H, this instruction
takes the value from address 30H (which is 12H in our example) and puts it
into A.
o Addressing Mode: Register Indirect. The @ symbol is the key here. It tells the
8051: "Don't take the value in R0, but go to the address stored in R0 and get
the data from there."
o Purpose: We need to start our comparison somewhere. We'll begin by
assuming the very first number in the list is the largest one.
• INC R0: An Arithmetic instruction.
o Instruction: INC (Increment).
o What it does: It adds 1 to the value in R0. R0 was 30H, so now it becomes
31H.
o Purpose: We've already dealt with the number at 30H, so we move our
pointer to the next number in the list, which is at address 31H.
• DEC R2: An Arithmetic instruction.
o Instruction: DEC (Decrement).
o What it does: It subtracts 1 from the value in R2. R2 was 5, so now it becomes
4.
o Purpose: Since we've already loaded the first number, we only have 4 more
numbers left to compare it with in our loop.
Section 2: The Main Loop
This is where the program repeatedly compares numbers to find the largest one.
• LOOP:: This is a label. It's not an instruction, but a marker for a memory address. It
gives a name to this line so we can jump back to it later.
• MOV B, @R0: Data Transfer instruction.
o What it does: It gets the next number from our list (at the address currently
in R0) and copies it into the B register. In the first pass of the loop, R0 is 31H,
so it copies 56H into B.
o Addressing Mode: Register Indirect.
o Purpose: We need to bring the next number from memory into a register so
we can compare it with our current largest number, which is in the A register.
• SUBB A, B: An Arithmetic instruction that's key for comparison.
o Instruction: SUBB (Subtract with Borrow).
o What it does: It subtracts the value in B from the value in A (A = A - B).
Crucially, this operation affects the flags, especially the Carry Flag (CY).
o The Comparison Trick:
▪ If A is greater than or equal to B (A >= B), the result will be positive or
zero, and the Carry Flag will be 0.
▪ If A is less than B (A < B), the subtraction requires a "borrow," and the
Carry Flag will be set to 1.
o Purpose: We use subtraction as a way to compare the two numbers. The
state of the Carry Flag after this instruction tells us which number was bigger.
• JNC NEXT_NUM: A Program Branching instruction.
o Instruction: JNC (Jump if No Carry).
o What it does: It checks the Carry Flag. If CY is 0, it means our original A was
the larger number, so we don't need to do anything. The program will jump
over the next instruction to the label NEXT_NUM. If CY is 1, it means B was
larger, so the program does not jump and continues to the next line.
o Purpose: This is our decision-maker. It decides whether we need to update
our "largest number" or not.
• MOV A, B: Data Transfer instruction.
o What it does: It copies the value from B into A.
o Purpose: This line only executes if the JNC instruction did not jump. This
happens when B was larger than A. We are now updating A to hold this new,
larger number.
• NEXT_NUM:: Another label to mark a location for our jump instruction.
• INC R0: Arithmetic instruction.
o What it does: Increments our pointer R0 to point to the next address in the
list (e.g., from 31H to 32H).
o Purpose: Prepare for the next iteration of the loop.
• DJNZ R2, LOOP: A very common and efficient Program Branching instruction.
o Instruction: DJNZ (Decrement and Jump if Not Zero).
o What it does: It performs two actions in one:
1. It decrements the R2 register (our counter).
2. It then checks if the new value in R2 is zero. If it's not zero, it jumps
back to the LOOP label to process the next number. If it is zero, it
means we've finished all our comparisons, and the program continues
to the line below.
o Purpose: This is the engine of our loop, ensuring we check exactly the right
number of items in our list.
Section 3: Storing the Result
The loop is finished, and the largest number is now in the Accumulator. The final step is to
save it.
• MOV 40H, A: Data Transfer instruction.
o What it does: It copies the value from the A register into the memory location
with the address 40H.
o Addressing Mode: Direct. We are directly specifying the destination memory
address (40H).
o Purpose: To store the final result in the required memory location.
• END: This is another assembler directive. It tells the assembler that this is the end of
the source code file.
10 EXAMPLE TO UNDERSTAND IT BETTER
1. Block Data Transfer
Question: Write a program to copy a block of 10 bytes of data from internal RAM
location 50H to 70H.
Solution Code:
ORG 0000H
MOV R0, #50H ; R0 is the source pointer, starting at 50H
MOV R1, #70H ; R1 is the destination pointer, starting at 70H
MOV R2, #10 ; R2 is the loop counter for 10 bytes
LOOP:
MOV A, @R0 ; Get a byte from the source location
MOV @R1, A ; Copy it to the destination location
INC R0 ; Move to the next source address
INC R1 ; Move to the next destination address
DJNZ R2, LOOP ; Decrement counter and repeat until all 10 bytes are copied
END
Logic: This program uses two pointers, R0 for the source and R1 for the destination. The
DJNZ instruction creates an efficient loop that runs exactly 10 times to move each byte of
data.
2. Sum of an Array of Numbers (16-bit result)
Question: A block of 8 numbers is stored in internal RAM starting at address 60H.
Calculate their sum. Since the sum might be larger than 255 (FFH), store the 16-bit result in
R6 (lower byte) and R7 (higher byte).
Solution Code:
ORG 0000H
MOV R0, #60H ; R0 points to the start of the number array
MOV R2, #08H ; R2 is the loop counter for 8 numbers
CLR A ; Clear Accumulator (A will hold the lower byte of the sum)
MOV R7, #00H ; R7 will hold the higher byte of the sum
LOOP:
ADD A, @R0 ; Add the current number to the accumulator
JNC NEXT ; If no carry was generated, skip the next line
INC R7 ; If there was a carry, increment the high-byte register
NEXT:
INC R0 ; Point to the next number
DJNZ R2, LOOP ; Repeat for all numbers
MOV R6, A ; Store the final lower byte of the sum in R6
END
Logic: The key here is the JNC (Jump if No Carry) instruction. After each addition, we
check the carry flag. If it's 1, it means the 8-bit accumulator overflowed, so we increment a
separate register (R7) to keep track of the higher part of the 16-bit sum.
3. Calculating Factorial
Question: Calculate the factorial of a number stored in R1 (assume the number is small,
e.g., 5). Store the result in registers R6 (lower byte) and R7 (higher byte).
Solution Code:
ORG 0000H
MOV R1, #05H ; The number for which to find the factorial (e.g., 5)
MOV A, #01H ; Initialize result to 1. A holds the lower byte.
MOV B, #00H ; B will hold the higher byte of the result.
LOOP:
PUSH B ; Save the current high-byte of the result onto the stack
MOV B, R1 ; Move the current multiplier (N) into B
MUL AB ; Multiply A by B. Result is in B (high) and A (low).
POP B ; Restore the old high-byte of the result
ADD A, B ; Add the old high-byte (now zero) to the new low-byte (no effect, just an
example)
; A more complex version would handle carries from MUL
DEC R1 ; Decrement N (e.g., 5, then 4, then 3...)
JNZ LOOP ; If N is not zero, continue multiplying
MOV R6, A ; Store the final low byte
MOV R7, B ; Store the final high byte
END
Logic: This program uses the MUL AB instruction, which multiplies A and B. The 16-bit
result is automatically placed in B (high byte) and A (low byte). The loop repeatedly
multiplies the accumulating result in A by the decreasing counter in R1. Note: This simple
version works for small factorials where the result fits in 16 bits.
4. Hexadecimal to BCD Conversion
Question: Convert an 8-bit hexadecimal number stored at 35H into a two-digit Packed
BCD (Binary Coded Decimal) number and store it in R5. For example, if 35H contains 2FH (47
decimal), R5 should become 47H.
Solution Code:
ORG 0000H
MOV A, 35H ; Get the hex number (e.g., 2FH)
MOV B, #10 ; Load 10 for division
DIV AB ; Divide A by 10. A = quotient, B = remainder.
; A = 04H (the '4'), B = 07H (the '7')
SWAP A ; Swap the nibbles of A. A becomes 40H.
ADD A, B ; Add the remainder. A = 40H + 07H = 47H.
MOV R5, A ; Store the BCD result
END
Logic: The DIV AB instruction is perfect for this. Dividing a number by 10 gives us the two
separate decimal digits. The quotient (in A) is the tens digit, and the remainder (in B) is the
units digit. SWAP A moves the tens digit into the upper 4 bits, making space to ADD the units
digit into the lower 4 bits, forming the packed BCD number.
5. Generating a Square Wave with a Delay
Question: Write a program to continuously generate a square wave on pin P1.0. Use a
simple software delay loop.
Solution Code:
ORG 0000H
AGAIN:
SETB P1.0 ; Set the port pin high (to 1)
ACALL DELAY ; Call our delay subroutine
CLR P1.0 ; Clear the port pin low (to 0)
ACALL DELAY ; Call the delay again
SJMP AGAIN ; Repeat the process indefinitely
; --- Simple Delay Subroutine ---
DELAY:
MOV R3, #255 ; Load outer loop counter
L1:
MOV R4, #255 ; Load inner loop counter
L2:
DJNZ R4, L2 ; Decrement inner counter until it's 0
DJNZ R3, L1 ; Decrement outer counter and repeat inner loop
RET ; Return from subroutine
Logic: The main loop simply turns the pin on, waits, turns it off, and waits again. The
DELAY subroutine uses a nested loop (a loop inside another loop). This creates a longer delay
than a single loop would. ACALL jumps to the subroutine, and RET brings the program
execution back to where it left off.
6. Search for a Byte in Memory
Question: A block of 16 bytes is stored at 40H. Search for the byte A5H. If it is found,
store its memory address in R1. If not found, R1 should contain 00H.
Solution Code:
ORG 0000H
MOV R0, #40H ; R0 is the pointer to our data block
MOV R2, #16 ; R2 is the loop counter
MOV R1, #00H ; R1 holds the result, initialize to 'not found'
LOOP:
MOV A, @R0 ; Get a byte from the block
CJNE A, #0A5H, NEXT ; Compare with A5H. If not equal, jump to NEXT.
MOV R1, R0 ; If they ARE equal, we found it! Store the address and stop.
SJMP FINISH ; Jump to the end
NEXT:
INC R0 ; Point to the next byte
DJNZ R2, LOOP ; Repeat until all bytes are checked
FINISH:
END
Logic: The CJNE (Compare and Jump if Not Equal) instruction is the star here. It
compares the accumulator with a value and jumps if they don't match, making it ideal for
searching. If a match is found, the program stores the current pointer (R0) and exits the
loop.
7. Count Set Bits (1s) in a Byte
Question: Count the number of bits that are set to '1' in a byte stored in R7. Store the
count in R6. For example, if R7 has B1H (which is 10110001 in binary), the count should be 5.
Solution Code:
ORG 0000H
MOV A, R7 ; Move the byte into the accumulator
MOV R6, #00H ; R6 is our '1' counter, initialize to 0
MOV R2, #08H ; We need to check all 8 bits
LOOP:
RLC A ; Rotate Accumulator Left Through Carry
; The most significant bit (D7) moves into the Carry Flag
JNC NEXT_BIT ; If the Carry Flag is 0, it wasn't a '1', so skip
INC R6 ; If Carry Flag is 1, we found a '1', so increment count
NEXT_BIT:
DJNZ R2, LOOP ; Repeat for all 8 bits
END
Logic: This program uses a clever trick with the RLC A instruction. Each time we rotate,
one bit from the number is moved into the Carry Flag. We can then simply check the Carry
Flag (JNC) to see if that bit was a 1 or a 0. We repeat this 8 times to check every bit.
8. Swap Two Nibbles in a Byte
Question: Take a byte from memory location 30H and swap its upper and lower nibbles
(4-bit parts). Store the result back in 30H. For example, if 30H contains A3H, it should
become 3AH.
Solution Code:
ORG 0000H
MOV A, 30H ; Get the byte
SWAP A ; The magic instruction that does the work
MOV 30H, A ; Store the result back
END
Logic: This one is simple because the 8051 has a dedicated instruction, SWAP A, for this
exact purpose. It exchanges the upper 4 bits and lower 4 bits of the accumulator.
9. Check if a Number is Even or Odd
Question: Check if the number in memory location 55H is even or odd. If it's even, set
pin P2.0 to 1. If it's odd, set pin P2.1 to 1.
Solution Code:
ORG 0000H
MOV A, 55H ; Get the number
JB ACC.0, IS_ODD ; Check the least significant bit (bit 0). If it's 1, jump.
IS_EVEN:
SETB P2.0 ; Number is even, set P2.0
CLR P2.1
SJMP FINISH
IS_ODD:
SETB P2.1 ; Number is odd, set P2.1
CLR P2.0
FINISH:
END
Logic: The key to checking for even or odd is the least significant bit (LSB). If the LSB is 0,
the number is even. If it's 1, the number is odd. The JB (Jump if Bit Set) instruction allows us
to directly test a single bit (ACC.0 is the LSB of the accumulator) and change the program's
flow accordingly.
10. Using the Stack to Preserve Registers
Question: Write a main program that loads values into R1 and R2. Then, call a
subroutine that uses R1 and R2 for its own purposes. The values in the main program's R1
and R2 should be unchanged after the subroutine finishes.
Solution Code:
ORG 0000H
; --- Main Program ---
MOV R1, #11H ; Load main program's value into R1
MOV R2, #22H ; Load main program's value into R2
ACALL SUBROUTINE ; Call the subroutine
; At this point, R1 should still be 11H and R2 should still be 22H
FINISH:
SJMP FINISH
; --- Subroutine ---
SUBROUTINE:
PUSH 01 ; Push the value of R1 onto the stack (address of R1 is 01)
PUSH 02 ; Push the value of R2 onto the stack (address of R2 is 02)
; Now the subroutine can use R1 and R2 for whatever it wants
MOV R1, #AAH
MOV R2, #BBH
; ... subroutine does its work ...
POP 02 ; Pop the original value of R2 from the stack back into R2
POP 01 ; Pop the original value of R1 from the stack back into R1
RET ; Return from subroutine
Logic: The stack is a "Last-In, First-Out" (LIFO) memory area. PUSH saves a register's
value onto the stack. POP retrieves the last pushed value. By pushing the registers at the
start of a subroutine and popping them in the reverse order at the end, we can use the
registers freely within the subroutine without corrupting the values used by the main
program. This is a critical concept for writing clean, modular code.