The First Assembly Program - Part 5 Simple Microcontroller (PIC10F200)
Published
Note: Microchip recently made some changes and their newer IDE versions use the XC8 compiler for Assembly, whereas all sample code here is created for the MPASM compiler. However, the sample code has been ported to XC8 by user tinyelect on our Discord channel (to whom we are extremely grateful!) and the XC8 version of the code is at the bottom of this tutorial for your reference. To see the changes needed to switch from MPASM to XC8, please check out the process that he shared.
Hi there! This is an auspicious moment. In this microcontroller tutorial, we start applying what we’ve learned and make things happen. This is our first tutorial devoted to writing code using Assembly language. Not just pure theory anymore, I’m going to give a description of the task or device, give the code that performs it and the code’s explanation. At first, I’ll have to explain a lot of stuff but the further in we go and the more samples we do, the less explanations you’ll need and the more confident you’ll be. Let's jump into some PIC microcontroller programming and see what happens!
I’ll organize this article as follows: First, I’ll give you what we need to do. Second, I’ll write the entire code that accomplishes the given task. Third, I’ll explain the code line by line as needed. So let’s go!
Today’s goal will be very easy – to light up LED1. Yeah, just light it up, not even blink. Assembly language is different from any other programming language, so be warned that some things may look very weird at first glance. Once you’ve prepared yourself mentally, then have a look at the Assembly program that does the given task:
#include "p10f200.inc"
; CONFIG
__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF
ORG 0x0000
INIT ; We are initializing the microcontroller over the next three lines.
MOVLW ~(1 << GP1) ;these two lines set GP1 as an output
TRIS GPIO
BSF GPIO, GP1 ;this line is where we set GP1 output high to light the LED
LOOP
GOTO LOOP ; loop forever
END ; Needed to end the program.
This code should be written into the main.asm file that we created in the previous tutorial and then saved. I recommend typing this code in manually as it’ll force you to look at it more closely but copy and pasting won’t be the end of the world either. But let’s discuss what this code is doing before we execute.
Assembly Language Overview
Before we go line by line, let’s go over some basic formatting and organization with the Assembly language.
In general, each line of code will contain one of the following elements:
- Labels
- Mnemonics
- Opcodes and Operands
- Comments
Let’s consider each element briefly.
Labels
Labels simplify navigation in the program, they’re, in a way, the signposts of the program. Or perhaps GPS coordinates. You could use them in every line but you would be lost quickly if you did so due to information overload. So it’s better to put them only where needed - usually where they are used to perform branches or GOTO commands.
Labels always start in column 1, i.e. at the beginning of the line. So everything written in column 1 is considered a label other than comments.
Labels are case sensitive by default unlike mnemonics and operands which are case insensitive.
Labels must begin with a letter of the alphabet or an underscore, the max length is 32 characters.
Labels can be followed by a colon (:) or a whitespace.
In the code above there are two labels – INIT (for initiation/initiate) at line 6 and LOOP at line 10.
Mnemonics
Mnemonics are Assembly instructions written as understandable text. These can also be called opcodes, operational codes, and more intuitively, are also called instructions. The instructions are code that tell the microcontroller to do something specific. They are given in the data sheet of the microcontroller.
Mnemonics must not start in column 1, and must be separated from a label by a colon or whitespace. Mnemonics are case insensitive by default.
Opcodes and Operands
Operands are the data, or parameters, used by mnemonics/opcodes/instructions. Some instructions may have no operands, or they could have 1 or 2 operands. They must be separated from mnemonics with whitespace, and from each other by a comma (,). In the code above, MOVLW ~(1 << GP1) has “MOVLW” as the opcode and “~(1 << GP1)” as the operand.
Comments
Comments are, as expected, comments. You can write anything you want; this text will be ignored by the compiler and is used to help remind you (and explain to others) what the code is doing. Comments can start anywhere with the semicolon (;) and lasts until the end of the line. So everything behind a semicolon is considered a comment. In most Integrated Development Environments, comments are highlighted with green.
Code Review
Now that we know what the four main elements of Assembly are, let’s review the code. The first line is a directive - very similar to other, higher level languages.
Line 1 contains the Assembly directive #include.
#include "p10f200.inc"
Assembly directives are instructions in the code that tell the assembler how to assemble the file. They are not included in the final file that will be loaded into the PIC microcontroller. They just help us to make the program more readable and allows you to use pre-defined libraries, making our lives easier. We will consider them as found in the text.
The assembler is what takes our low-level code and turns it into machine code.
So, the #include directive. If you are familiar with the C language you may exclaim “Hey! I know this one, it allows me to include another file in the current one” And you would be right. This directive works the same as in C. Moreover, the Assembly language supports conditional directives, e.g. #if, #ifdef, #define and others. So if you know how those work, feel free to use them.
With this particular directive, we include the “pic10f200.inc” file which is located in the folder with the Assembler. This file contains some definitions that will be helpful for us and specific to our microcontroller. I’ll name them when we meet them in the code.
Line 2 contains just a comment which tells us that the following line will set the processor configuration bits.
#include "p10f200.inc"
; CONFIG
Line 3 as line 2 mentioned, this line sets the configuration bits with another Assembly directive __CONFIG followed by the bit values.
#include "p10f200.inc"
; CONFIG
__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF
These configuration bits are used to set the global microcontroller operations. If you are familiar with AVR, these bits are like fuse bits. They can be loaded only while programming the chip and can’t be changed by the user code.
- _WDT_OFF disables the watchdog timer. This timer resets the microcontroller if the program hangs but we won’t need it.
- _CP_OFF disables code protection. I’m pretty sure intellectual property for lighting up an LED is open source, so we won’t worry about protecting our code.
- _MCLRE_OFF disables the reset function on pin GP3/MCLR/VPP and allows this pin to act as a typical input pin, GP3. For the current program it doesn’t matter but in the future this bit might be vital.
All three of these values are defined in the pic10f200.inc file, so that the program knows what the otherwise gibberish characters such as _CP_OFF actually means. Once we set these bits they will be loaded into the microcontroller during programming automatically.
Note that the bits are separated with an ampersand (&) not a comma. An ampersand in this case means a logical AND, like in C language.
Remember that a logical "AND" returns a "1" only if all of the input bits at a specific location are a "1".
I should note here that Assembly language can recognize a lot of the mathematical and logical operations from the C language. So if you are not familiar with them, please spend some time to learn them. Another important thing to understand is that these mathematical operations are performed by the Assembler and only the result is included into the microcontroller. I’ll repeat this again when we run into this again (spoiler: very soon).
Line 4 contains another Assembly directive, this time “ORG”, which sets the program origin.
#include "p10f200.inc"
; CONFIG
__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF
ORG 0x0000
As you can see, the argument of this directive is 0x0000 which means that the following code line will be started at address 0x0000 of the program memory. And this address is loaded into the PC (program counter) after reset.
Remember from Tutorial 3 that the program counter is how the microcontroller knows which command to execute next.
Line 5, as mentioned, contains just a label INIT. This label has no important role as there are no links to it in the code, it just informs us that here the initialization part of the code is started (like setup() function in the Arduino).
#include "p10f200.inc"
; CONFIG
__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF
ORG 0x0000
INIT ; We are initializing the microcontroller over the next three lines.
The lines from 1 to 5 will be the same (or with minimal differences) in future programs, so you can just copy/paste this later.
Line 6 is the start of the real Assembly program with the first Assembly instruction MOVLW.
#include "p10f200.inc"
; CONFIG
__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF
ORG 0x0000
INIT ; We are initializing the microcontroller over the next three lines.
MOVLW ~(1 << GP1) ;these two lines set GP1 as an output
This mnemonic roughly stands for “MOVe Literal to W”. This instruction allows loading the “literal” into the W register. Literal is a formal way to refer to any byte of information (value from 0 to 255) represented in any convenient form. By default all numbers in the Assembly are represented as hexadecimal, so if you write MOVLW 14, that means in fact MOVLW 0x14, not decimal 14.
If you don't understand that 14h is the same as 20d, you really should go check out our decimal to binary and hexadecimal tutorial.
So care should be taken not to make a mistake. If you want to specify a decimal number you should write D’14’. I highly recommend you get comfortable with hexadecimal if you’re not already - it will help some commands become considerably more intuitive.
I didn’t talk about the W register when explaining the PIC10F200 structure but I’ll correct that now. W is a special register, called the “Working register” which doesn’t have a specific address you can access but it helps perform certain arithmetical, logical, or move operations. Some Assembly instructions can only be with the W register, while other instructions can only be applied to file registers.
As I mentioned earlier, the MOVLW instructions has one operand which is the literal we want to load into the W register. In line 6, it is written as ~(1 << GP1). This is another arithmetical expression solved by the Assembler, not the microcontroller - I told you this would be soon! But, if you’re not familiar with this notation, this could be extremely confusing.
Let’s look at this more in-depth. If you remember, our goal is to light up LED1, and to do this, we need to set GP1 as an output. Checking our datasheet, you can see that we need to clear the corresponding bit of the TRISGPIO register to 0, leaving all other bits as 1.
As discussed in tutorial three, TRISGPIO is where we state if a GPIO, or General Purpose Input Output, is going to be an input or output. TRIS is a mnemonic for "TRIState" - referring to the three states a GPIO pin can take (high voltage/low voltage/high-impedance).
In the pic10f200.inc file, there is a mnemonic of sorts for each GPIO pin so that you don’t have to memorize the address or bit location for everything. In this case, GP1 acts as the “address” or location of the appropriate TRIS bit, and for GP1, the value happens to be “1”.
The expression ~(1 << GP1) has three parts. It means the value of “1” left shifted to the location of the GP1 bit, which, again, happens to be “1” in this case. “<<” is the left shift command. “1” is the number that is to be shifted. From the include file, “GP1” is the location of the TRIS bit in TRISGPIO, or 1.
The operation “~” performs a bitwise inversion of the value. It basically takes all the “0”s and makes them “1”s and all the “1”s and makes them “0”s. This will make more sense as we look at it step by step.
I will represent the value in binary for better understanding.
Bits | ||||||||
Value | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
1 << GP1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
~(1 <<GP1) | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 |
As you can see now, we got the value that we needed, it has only one 0 at bit position 1 while all other bits are 1s.
Again, while this may seem convoluted at first glance, as you get more comfortable and used to it, you’ll find it is significantly better in other ways. For example, you could write the same thing in the following ways:
- MOVLW 0xFD
- MOVLW D’253’
- MOVLW B’11111101’
All these lines are identical to the microcontroller itself. But these forms are not informative (why 253? What does it mean?) while the first form tells us directly what we are doing and why. Again, it might look complex at first but you will get used to it and it’s quite common for all microcontroller development.
We’ve spent a lot of time on Line 6 but have learned quite a bit! Let’s move on.
Line 7 contains another TRIS related instruction.
#include "p10f200.inc"
; CONFIG
__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF
ORG 0x0000
INIT ; We are initializing the microcontroller over the next three lines.
MOVLW ~(1 << GP1) ;these two lines set GP1 as an output
TRIS GPIO
This is the special instruction; its only application is to copy the contents of the W register into the TRISGPIO register. Also, its only operand is the GPIO register.
GPIO register address is also defined in the pic10f200.inc file, and it happens to be 6. You can confirm this with the data memory table from tutorial 3.
So the TRIS instruction has a form that can’t be changed, otherwise it will not make any sense. So, just TRIS GPIO. Of course, if you don’t load the value you want into the W register first, this will copy whatever random values happen to be in the W register at the moment.
After implementation of this line the GP1 pin will be switched to the output direction, but to actually turn on the LED we need to set the corresponding bit of the GPIO register to 1. This takes us to line 8.
Line 8 has another instruction - BSF. I believe this mnemonic stands for “Bit Set F” which, is most useful if you remember that “setting” a bit is changing it to a “1”.
#include "p10f200.inc"
; CONFIG
__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF
ORG 0x0000
INIT ; We are initializing the microcontroller over the next three lines.
MOVLW ~(1 << GP1) ;these two lines set GP1 as an output
TRIS GPIO
BSF GPIO, GP1 ;this line is where we set GP1 output high to light the LED
Fortunately, it’s a very simple instruction with only two operands. The first operand is an address and the second operand is bit within that address. BSF will set the specific bit defined at that address as a “1”. This instruction is only applicable to the file registers, and can’t work with the W register.
So BSF GPIO, GP1 sets the bit GP1 of the register GPIO as 1. After implementation of this line, LED1 will finally light up.
If we wanted, we could stop the program there but it’s good practice to loop the program instead of making the microcontroller restart code execution from the very beginning every time. So we’ll add another two lines before we end the program.
Line 9 contains just a label LOOP. We already know what labels are, so nothing interesting here.
#include "p10f200.inc"
; CONFIG
__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF
ORG 0x0000
INIT ; We are initializing the microcontroller over the next three lines.
MOVLW ~(1 << GP1) ;these two lines set GP1 as an output
TRIS GPIO
BSF GPIO, GP1 ;this line is where we set GP1 output high to light the LED
LOOP
Line 10 is more interesting. It contains the GOTO instruction. People who already are familiar with high-level programming might be shocked at this moment – “What? GOTO? This is terrible - it should never be used!” Calm down, please. In high level languages, GOTO is the operator and truly should be strictly avoided. But here, we’re using assembly, and it’s a unique situation.
#include "p10f200.inc"
; CONFIG
__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF
ORG 0x0000
INIT ; We are initializing the microcontroller over the next three lines.
MOVLW ~(1 << GP1) ;these two lines set GP1 as an output
TRIS GPIO
BSF GPIO, GP1 ;this line is where we set GP1 output high to light the LED
LOOP
GOTO LOOP ; loop forever
GOTO is an unconditional branch instruction. It allows us to load the PC with the address where we want to continue the execution of our program. But it would be difficult and inconvenient to load the physical address because we don’t actually know it, and it can change if we modify the program. Fortunately, we don’t need to do this; we can use the label as the operand of this instruction. Thus, code execution will continue from the line at the specified label (or below it, as in our case). So GOTO LOOP instruction will move the PC to the line at the LOOP label which is… GOTO LOOP. So the program execution will loop forever in this instruction until we reset the microcontroller.
Line 11 contains the Assembly directive END. This directive is mandatory, and it specifies the end of the program. Everything else after this directive is ignored. Absence of this directive will cause an error.
#include "p10f200.inc"
; CONFIG
__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF
ORG 0x0000
INIT ; We are initializing the microcontroller over the next three lines.
MOVLW ~(1 << GP1) ;these two lines set GP1 as an output
TRIS GPIO
BSF GPIO, GP1 ;this line is where we set GP1 output high to light the LED
LOOP
GOTO LOOP ; loop forever
END ; Needed to end the program.
And that’s it’s about the program itself! Now, we can compile it and load it into the microcontroller.
Again, I’ll show how to do it in MPLAB 8.76 and in MPLAB X.
MPLAB 8.76
In MPLAB 8.76 you need to execute main menu command Project -> Make or use the toolbar button Make
If everything is correct in your program then the IDE will show something like this:
Then you need to connect your programmer/debugger to the attached circuit and load the program into the microcontroller with the command Programmer -> Program or with the toolbar button Program
If everything is connected properly, you will see the following notification:
Also you will see that the LED1 is now lit up which proves that we’ve done everything correctly! You did it!
MPLAB X
In MPLAB X, you need to execute main menu command Production -> Build Main Project or use the toolbar button Build Main Project
If everything is correct in your program, then the IDE will show something like this:
Then you need to connect your programmer/debugger to the attached circuit and load the program into the microcontroller with the toolbar button Run Main Project
If everything is connected properly, you will see the following notification:
Also, you will see that the LED1 is now lit up which proves that we’ve done everything correctly! We have just begun where we can do some really cool stuff!
Now, at the end of the tutorial, I’ll provide some interesting information. The produced code size for our program is just 4 words! If you are familiar with Arduino programming, try to write the LED light up program and see how much code it will produce (another spoiler: 710 bytes!).
OK, we’re finally done. The first and very simple program turned out to have a lot of theory and new knowledge required. But it’s a good start. We learned an important Assembly directive. Also, we now know four important Assembly instructions: MOVLW, TRIS, BSF, and GOTO.
As a review, see if you can repeat these steps and get them to light up! In the next tutorial, we’re going to make this LED blink and learn a couple more important instructions.
Check Yourself
20 Questions
Tutorial FAQs
Assembly language looks a little complicated. Can I used C or Python to program a microcontroller?
It just looks complicated, but actually it’s easier to configure the peripherals of a microcontroller on a register level when you use assembly language. In cases where the memory is limited or faster execution of the instructions is needed, you really need to go back to assembly language. While it is possible to use C, it will be less efficient. Python needs an interpreter, so this would be even less efficient with memory. In the long run, learning some assembly language basics will go a long ways.
I'm getting the following error:
"The target circuit may require more power than the debug tool can provide. An external power supply might be necessary. Connection Failed."
The PICKit 3 can only source 30mA, which we shouldn't be even close to that. Is it possible that you used the wrong value of resistor with the LED and it's trying to pull too much current? If you want, you can actually remove all of the excess circuitry and only leave the connection between the microcontroller and the programmer. If it's only the microcontroller and the debugger and you're still getting that error, then either the PIC is bad or something odd is going on with the settings. If that works, then you can put back everything piece by piece and see where you're getting the excess current draw.
Get the latest tools and tutorials, fresh from the toaster.