FB pixel

Button Inputs - Part 9 Microcontroller Basics (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 again! After the meaty last tutorial, I think it would be good to rest a bit and consider something relatively easy and short. Also, I haven’t provided a homework solution this time because... how can I know what your favorite song is? So I’m leaving it up to you.

Some attentive readers of these tutorials could be asking, “Why did we install two buttons and two LEDs and never use them together?” In preparation for this day! Let’s fix this omission.

Today’s task will consist of two parts:

  1. Toggle LED1 after each press of the button SW1.
  2. Toggle LED2 all the time while button SW2 is pressed with a delay of 200ms.

The program code to do this task is shown below:

#include "p10f200.inc"

__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF

i    EQU 10   ;Delay register #1

j    EQU 11   ;Delay register #2

    ORG 0x0000    

INIT

    MOVLW ~((1<<T0CS)|(1<<NOT_GPPU))

    OPTION   ;Enable GPIO2 and pull-ups

    MOVLW ~((1 << GP2)|(1 << GP1))    

    TRIS GPIO   ;set GP1 and GP2 as outputs

LOOP

    CALL CHECK_SW1   ;Check switch SW1

    CALL CHECK_SW2   ;Check switch SW2

    GOTO LOOP

CHECK_SW1   ;Beginning of the CHECK_SW1 subroutine

    BTFSC GPIO, GP3   ;Check if GP3 is 0 (SW1 is pressed)

    RETLW 0   ;If it is not 0 then return

    CALL DEBOUNCE_DELAY ;Otherwise perform the debounce delay

    BTFSC GPIO, GP3             ;And check GP3 again

    RETLW 0   ;If it is not 0 then return

    BTFSS GPIO, GP3             ;Otherwise wait while GP3 is 0

    GOTO $-1   ;Loop to wait

    CALL DEBOUNCE_DELAY ;After GP3 is set (SW1 is released)

    MOVLW (1 << GP1)            ;Toggle GP1 pin

    XORWF GPIO, F               ;using XOR operation

    RETLW 0   ;And return from the subroutine

CHECK_SW2   ;Beginning of the CHECK_SW1 subroutine

    BTFSC GPIO, GP0              ;Check if GP0 is 0 (SW2 is pressed)

    RETLW 0   ;If it is not 0 then return

    CALL DEBOUNCE_DELAY ;Otherwise perform the debounce delay

SW2_LOOP   ;Loop while SW2 is pressed

    BTFSC GPIO, GP0             ;Check if GP0 is 0

    RETLW 0   ;If it is not 0 then return

    MOVLW (1 << GP2)            ;Otherwise toggle GP2 pin

    XORWF GPIO, F               ;using XOR operation,

    CALL TOGGLE_DELAY      ;perform long delay before the next toggle

    GOTO SW2_LOOP            ;and return to the beginning of the loop

DEBOUNCE_DELAY   ;Start DEBOUNCE_DELAY subroutine here

    MOVLW D'40'   ;Load initial value for the delay    

    MOVWF i   ;Copy the value to the register i

    MOVWF j   ;Copy the value to the register j

DELAY_LOOP   ;Start delay loop

    DECFSZ i, F   ;Decrement i and check if it is not zero

    GOTO DELAY_LOOP   ;If not, then go to the DELAY_LOOP label

    DECFSZ j, F   ;Decrement j and check if it is not zero

    GOTO DELAY_LOOP   ;If not, then go to the DELAY_LOOP label

    RETLW 0   ;Else return from the subroutine

TOGGLE_DELAY   ;Start TOGGLE_DELAY subroutine here

    MOVLW D'255'               ;Load initial value for the delay    

    MOVWF i   ;Copy the value to the register i

    MOVWF j   ;Copy the value to the register j

DELAY_LOOP1   ;Start delay loop

    DECFSZ i, F   ;Decrement i and check if it is not zero

    GOTO DELAY_LOOP1       ;If not, then go to the DELAY_LOOP1 label

    DECFSZ j, F   ;Decrement j and check if it is not zero

    GOTO DELAY_LOOP1       ;If not, then go to the DELAY_LOOP1 label

    RETLW 0   ;Else return from the subroutine

    END

Doesn’t look that intimidating, huh? Let’s consider it in more detail.

In lines 1 to 7 there is nothing interesting. We define two registers “i” (line 4) and “j” (line 5) to use them in the delay subroutine. Other than that, it’s the standard initiation.

In line 8 there is something new. We already remember that ~(1<<T0SC) releases the GP2 pin from being the timer/counter input, so that it can be used as a GPIO. And now we have (1<<NOT_GPPU) as well.

GPPU stands for (General Purpose pin Pull-Up), NOT means that when this bit is 0, pull-ups are enabled, and when it is 1 the pull-ups are disabled. Unlike the majority of the microcontrollers where you can individually enable pull-up resistors for each pin, in the PIC10F200 you enable or disable pull-up resistors for GP0, GP1, and GP3 pin together. Please note that the GP2 pin doesn’t have internal pull-up.

What are pull-up resistors?

Bear with me as we take a break from the program for a moment. What are the pull-ups, or to be more specific, pull-up resistors? There are special resistors inside the microcontroller (or when not internally available, manually connected to a pin externally) that help its inputs to be more stable. If you look at figure 1 of tutorial 4, you may notice that buttons SW1 and SW2 are connected directly to the microcontroller pins GP3 and GP0 respectively. When you press any button it shorts the corresponding pin to the ground, and thus the voltage on this pin becomes 0V. But what happens if the button is not pressed? In this case, the state of the input pin is undefined. As the input driver has a very high resistance, even tiny currents (less than a microamp) can make the input change its state (for instance, if you touch the pin with your finger). And you don’t know if it is going to make the state go high or low. And, perhaps worse, even without outside interference, it’s possible that the value of the pin can “float” anywhere from 0V to 5V.

As we don’t want this to happen, we need to connect the so-called pull-up resistors. They are connected between the input pin and the positive power supply pin (VDD). Normally they are several tens of kOhms. The current flowing through this resistor from VDD to the input pin is enough to hold it in the high state until we press the button. Actually, that’s why it’s called “pull-up”, it pulls the voltage of the pin up to the power supply voltage. Pull-up resistors are more common but there are also pull-down resistor configurations that pull the pin down to ground by tying the input to ground with a several tens of kOhms resistor.

I hope now it is clear that in this application pull-up resistors are vital. You may ask “But if we enable the pull-up resistors on GP0, GP1, and GP3 pins, then LED1 that is connected to the GP1 should be lit up by it, isn’t it?”. - No, it will not. Pull-up resistors are enabled only on the input pins. As soon as you set the pin direction as output, the pull-ups are disabled for this pin. Excellent question, though!

Let’s now return to our program.

    MOVLW ~((1<<T0CS)|(1<<NOT_GPPU))

    OPTION   ;Enable GPIO2 and pull-ups

    MOVLW ~((1 << GP2)|(1 << GP1))    

    TRIS GPIO   ;set GP1 and GP2 as outputs

LOOP

    CALL CHECK_SW1   ;Check switch SW1

    CALL CHECK_SW2   ;Check switch SW2

    GOTO LOOP

With line 8, we load the value ~((1<<T0CS)|(1<<NOT_GPPU)) to the W register. This value consists of zeros at positions of T0SC and NOT_GPPU bits only while all other bits are 1.

In line 9, we load this value to the OPTION register, to which the mentioned bits belong.

In lines 10-11, we set GP1 and GP2 pins as outputs. We’ve already done this before, so let’s just keep moving.

In lines 13 and 14 we call subroutines CHECK_SW1 and CHECK_SW2 respectively. In this subroutines we check if the corresponding buttons (SW1 or SW2) is pressed and process the action according to the given task. As the PIC10F200 doesn’t support interrupts, we have to poll the buttons, thus we can’t process both buttons simultaneously.

"Polling" a button is when you frequently check the status of the button. It's an active step and is generally considered inefficient, especially in bigger, more complicated programs but it's fine for these simple routines. In more powerful microcontrollers, there's the option to use "interrupts" which can interrupt the program if it detects a change on a pin you're interested in and execute a specific action at that point. This is a huge topic and we can't do it justice without distracting from this tutorial.

CHECK_SW1   ;Beginning of the CHECK_SW1 subroutine

    BTFSC GPIO, GP3   ;Check if GP3 is 0 (SW1 is pressed)

    RETLW 0   ;If it is not 0 then return

Let’s now consider what’s going on in these subroutines.

In the CHECK_SW1 subroutine that starts on line 17, we need to check if button SW1 is pressed, wait until it is pressed and released to toggle LED1 (according to the task).

In line 18 there is the new instruction BTFSC (Bit Test in the File register, Skip if Clear). This instruction is the opposite of the BTFSS instruction which we already know. It has the same syntax but it skips the next line if the mentioned bit of the register is clear ( = 0 ) unlike the BTFSS with which the next line is skipped when the bit is set ( = 1 ). So, in line 18, we check if the bit GP3 of the GPIO register is 0. If it is, that means that the button is pressed. Hopefully, you remember that when the button is released, the corresponding bit is 1 because of the pull-up resistor, and when button is pressed, and the pin is shorted to ground and the corresponding bit becomes 0). If the button is pushed, line 19 is skipped and we move to line 20.

If the button is not pressed then line 19 is not skipped and we immediately leave the subroutine by means of the RETLW instruction.

Remember that RETLW is RETurn Literal to W - where the address in the top of the stack is loaded back to the program counter.

CALL DEBOUNCE_DELAY  ;Otherwise perform the debounce delay

    BTFSC GPIO, GP3     ;And check GP3 again

    RETLW 0   ;If it is not 0 then return

In line 20 there is the call of the DEBOUNCE_DELAY subroutine. Why do we need this? We live in a real world where everything is not ideal. Buttons are not an exception (especially if you bought the cheap ones on Aliexpress). So, when you press a button, it doesn’t mean that the input signal immediately changes from 1 to 0 and remains unchanged until you release the button. No, it bounces several times between 0V and 5V until it becomes stable. This happens because the button contacts are not ideal, and while you’re pressing the button they touch and release each other many times extremely quickly until the contact becomes stable.

As a microcontroller reads its input with at a frequency of about 1 MHz it can consider a single button press as multiple presses and perform the required action many times. As we don’t want this to happen, it’s better to wait until the signal becomes stable and then read the button state. This is called the “debounce delay”. After we notice the first time a signal becomes low (the first touch of the button contacts) we wait for 20-30 milliseconds, depending on the button quality, and then check the state again. If the state is still low, it means that the button was actually pressed and we can move forward. Let’s go over how this is implemented in lines 20-22.

In line 20 as it was mentioned before we invoke the debounce delay, after which, in line 21, we check the button state again with the same BTFSC instruction. If the state is not 0 (which means that, for some reason, the button was released during these 30 ms) then line 22 is implemented where the RETLW instruction returns us from the subroutine. If the state is still 0, then line 22 is skipped and we move to line 23.

BTFSS GPIO, GP3     ;Otherwise wait while GP3 is 0

    GOTO $-1    ;Loop to wait

    CALL DEBOUNCE_DELAY  ;After GP3 is set (SW1 is released)

Lines 23 and 24 perform the loop that waits while we keep the button pressed. In line 23, we use the BTFSS instruction.

BTFSS is "Bit Test in the File register, Skip if Set" so it checks the bit of the register and skips the next line if the bit is 1.

In line 24 there is the instruction GOTO $-1. I mentioned in the previous tutorials that “$” stands for the current PC value. And thus “$-1” stands for the previous code line. So, if bit GP3 is not set, then line 24 is invoked, and we return to line 23 again. This happens until the value of the GP3 bit becomes 1, which means that the button has been released.

But we call the debounce delay in line 25 because when the button is released, the contact bouncing still happens.

MOVLW (1 << GP1)    ;Toggle GP1 pin

    XORWF GPIO, F   ;using XOR operation

    RETLW 0   ;And return from the subroutine

In lines 26 and 27, only after all that we can perform the required action - toggle LED1. This is made in a similar way to the previous tutorial by XORing the GPIO register with the W register preloaded with the 1 only in the GP1 bit.

In line 28, we can consider the action completed and return from the subroutine with the RETLW instruction (line 28).

Now, for the CHECK_SW2 subroutine. It is similar to the CHECK_SW1 but has some differences because this time, we need not only to wait while the button is pressed, but also to do some things during this period.

CHECK_SW2   ;Beginning of the CHECK_SW1 subroutine

BTFSC GPIO, GP0      ;Check if GP0 is 0 (SW2 is pressed)

    RETLW 0   ;If it is not 0 then return

    CALL DEBOUNCE_DELAY  ;Otherwise perform the debounce delay

SW2_LOOP       ;Loop while SW2 is pressed

    BTFSC GPIO, GP0     ;Check if GP0 is 0

    RETLW 0   ;If it is not 0 then return

    MOVLW (1 << GP2)     ;Otherwise toggle GP2 pin

    XORWF GPIO, F   ;using XOR operation,

So, lines 31-33 are the same as lines 18-20 except that now we check the GP0 pin instead of GP3.

In line 34, we add the label SW2_LOOP, here we start the loop in which we wait until button SW2 is released.

Lines 35-36 are basically identical to lines 21-22 with their intent. We check the button state after the debounce delay and if the button is still pressed, we move on.

In lines 37-38, we toggle the LED2 the same way we toggled LED1 in lines 26-27.

CALL TOGGLE_DELAY      ;perform long delay before the next toggle

    GOTO SW2_LOOP          ;and return to the beginning of the loop

After that, we call the TOGGLE_DELAY in line 39. Toggle delay is the same delay but longer than DEBOUNCE_DELAY. Actually, I could use the same code as in the previous tutorial - just load the “i” and “j” values outside the DELAY subroutine and then decrease them inside the DELAY subroutine. But if you calculate the total number of times we call DEBOUNCE_DELAY and TOGGLE_DELAY in the program, it’s 4 times. Loading the “i” and “j’ values takes 3 lines (basically 3 words of flash memory). So we need 6 extra lines (because we load them twice in DEBOUNCE_DELAY and TOGGLE_DELAY subroutines). Whereas the delay loop only takes 5 lines (47-51 and 58-62), so we save one word of memory, yay! This is an excellent example of the small things you can do to optimize code in assembly, versus writing in C, C++, or Java and having a compiler automatically generate the machine code. Professional level compilers can do a pretty awesome job, admittedly, and if you’re not that good at assembly, you may even be less efficient. But if you have a good understanding of how assembly works and a little creativity, you can do some amazing things.

Returning to the program, after invocation of the TOGGLE_DELAY we return to line 34 by the GOTO instruction in line 40. Then we check if the button is still pressed and toggle the LED2 again if it is. Thus, we keep toggling the LED with the specified delay until we release the button.

DEBOUNCE_DELAY       ;Start DEBOUNCE_DELAY subroutine here

MOVLW D'40'   ;Load initial value for the delay    

    MOVWF i   ;Copy the value to the register i

    MOVWF j   ;Copy the value to the register j

DELAY_LOOP       ;Start delay loop

    DECFSZ i, F   ;Decrement i and check if it is not zero

    GOTO DELAY_LOOP   ;If not, then go to the DELAY_LOOP label

    DECFSZ j, F   ;Decrement j and check if it is not zero

    GOTO DELAY_LOOP   ;If not, then go to the DELAY_LOOP label

    RETLW 0   ;Else return from the subroutine

As I mentioned before, in lines 42-51 there is the DEBOUNCE_DELAY subroutine, and in lines 53-62 there is the TOGGLE_DELAY subroutine. We know all about the delays so I won’t consider them in more detail.

TOGGLE_DELAY       ;Start TOGGLE_DELAY subroutine here

MOVLW D'255'       ;Load initial value for the delay    

    MOVWF i   ;Copy the value to the register i

    MOVWF j   ;Copy the value to the register j

DELAY_LOOP1       ;Start delay loop

    DECFSZ i, F   ;Decrement i and check if it is not zero

    GOTO DELAY_LOOP1     ;If not, then go to the DELAY_LOOP1 label

    DECFSZ j, F   ;Decrement j and check if it is not zero

    GOTO DELAY_LOOP1     ;If not, then go to the DELAY_LOOP1 label

    RETLW 0   ;Else return from the subroutine

    END

That’s it for this program. Memorize, or keep as a reference, the sequence used for processing the buttons, it will be the same in the next our programs. Making code modular and reusing it is not cheating - it’s recommended! Not forgetting how it works is also recommended.

Now you can copy the program to the MPLAB editor, assemble it and load it into the microcontroller. Then press the SW1 button and release it. You should see that the LED1 toggles its state. When you press it the next time the LED1 will return to the initial state. Please pay attention that the time you hold the SW1 button doesn’t matter - the LED1 will toggle just one time per click.

Now, try to press the SW2 button briefly, you will see the same behavior as with the SW1 - LED2 will be toggled. But when you press SW2 and hold it, the LED2 will change its state several times per second (5 times to be specific, as according to the task the delay is 200 millisecond).

Warning! You might face the issue that nothing works after you load the program, or just LED2 blinks. Don’t worry, it’s not an error. This might happen because of the programmer. After finishing the programming it holds GP0 low and GP3 high for some reason (at least with my PICKit 3). That’s why the program hangs. To fix this issue, just disconnect the wires that come from the programmer to GP0 and GP3 of the microcontroller, and everything will work fine. But don’t forget to return them back when you want to program your microcontroller again. (And certainly don’t mix them up! Your microcontroller might be damaged.)


And that’s all. Today’s dry statistic is even smaller than the last tutorial. We learned just one new instruction - BTFSC, and now we know 20 of 33. The program size is just 43 words of the flash memory, or nearly 17% of our program space.

As homework, I’d recommend the following task. Count the number of presses of the button SW1 and indicate it in the binary code with the LED1 and LED2 according to the table below:



Don’t check my solution until after you’ve done it yourself. My solution also consists of several new instructions which we will use later.

#include "p10f200.inc"

__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF

i   EQU   10   ;Delay register 1

j   EQU   11   ;Delay register 2

count    EQU 12   ;Number of button presses

    ORG 0x0000    

INIT

    MOVLW ~((1<<T0CS)|(1<<NOT_GPPU))

    OPTION   ;Enable GPIO2 and pull-ups

    MOVLW ~((1 << GP2)|(1 << GP1))    

    TRIS GPIO   ;set GP1 and GP2 as outputs

LOOP

    BTFSC GPIO, GP3   ;Check if GP3 is 0 (SW1 is pressed)

    GOTO LOOP   ;If it is not 0 then return to the LOOP

    CALL DEBOUNCE_DELAY ;Otherwise perform the debounce delay

    BTFSC GPIO, GP3   ;And check GP3 again

    GOTO LOOP   ;If it is not 0 then return to the LOOP

    BTFSS GPIO, GP3   ;Otherwise wait while GP3 is 0

    GOTO $-1   ;Loop to wait

    CALL DEBOUNCE_DELAY ;After GP3 is set (SW1 is released)

    INCF count, F               ;increment the count register

    MOVLW 0x03   ;Load the 0x03 value to the W register

    ANDWF count, F               ;Perform AND operation with the count register

    RLF count, W               ;Rotate the count register left

    MOVWF GPIO   ;and save the value into the GPIO register

    GOTO LOOP

DEBOUNCE_DELAY   ;Start DEBOUNCE_DELAY subroutine here

    MOVLW D'40'   ;Load initial value for the delay    

    MOVWF i   ;Copy the value to the register i

    MOVWF j   ;Copy the value to the register j

DELAY_LOOP   ;Start delay loop

    DECFSZ i, F   ;Decrement i and check if it is not zero

    GOTO DELAY_LOOP   ;If not, then go to the DELAY_LOOP label

    DECFSZ j, F   ;Decrement j and check if it is not zero

    GOTO DELAY_LOOP   ;If not, then go to the DELAY_LOOP label

    RETLW 0   ;Else return from the subroutine

    END

I just wanted to show you that using some tricks you can make the solution very compact. The listed program uses just 26 words of Flash memory but totally implements the given task.

Let’s now consider how I managed this.

Apparently, we need a register where we can count the number of the SW1 button pressings. So, let’s define it as “count” in line 6.

The initialization part is absolutely the same so let’s skip it and move to line 14. I eliminated the CHECK_SW1 subroutine and put its code into the main loop. This allows us to save two words - the CALL and RETLW instructions.

Lines 14 to 21 are copied from the CHECK_SW1 subroutine and are the same except for lines 15 and 18, where I replaced the RETLW 0 instruction with the GOTO LOOP which does the same thing in the current case.

In line 22, instead of toggling the LED after releasing the button, we increment the “count” variable with the INCF instruction which you are already familiar with.

Lines 23-26 do the actual magic, which I will try to explain.

In line 23, we load the value 0x03 to the W register. Why do we need this? 0x03 is 00000011b in binary. As we want to indicate just the last two bits of the “count” value with the LED1 and LED2, we might want to set the other bits of this register to zero. So, we prepare this value in the W register.

In line 24, we find a new instruction ANDWF (AND the W and F registers). According to its description, it performs the logical operation AND on the W register and the given file register and saves the result to either W or F register according to operand 2. In our case, we load the value back to the “count” register. By implementing this line we set all the bits of the “count” register to 0 except the last two bits - they remain unchanged. Let’s see how it works:

So you see that the “count” value after the AND operation changes in loop as 1, 2, 3, 0, 1, 2… which serves us well as we just need the last two bits (highlighted).

So we could use this value to load into the GPIO register and directly control LED1 and LED2, but they are connected to the GP1 and GP2 pins respectively which correspond to bit 1 and 2 of the GPIO register, not bits 0 and 1 that we have here.

So we need to shift the “count” value one bit to the left to have the value that is ready to be loaded into the GPIO register.

PIC10F200 has the special instruction for this: RLF (Rotate Left File register) which we can see in the line 25. This instruction rotates the given file register one position to the left through the C (Carry) bit of the STATUS register. Sounds a bit complicated but let me explain using the image from the PIC10F200 datasheet (figure 1)

Figure 1 - RLF Instruction Explanation
Figure 1 - RLF Instruction Explanation

So after implementing this instruction, the bit #0 value is moved to bit #1, bit #1 value is moved to bit #2 and so on. But the bit #7 is the last, so there is nowhere to move it, and thus it is copied to the C bit of the STATUS register, and after that, the C bit is copied to bit #0. So, basically the value of bit #7 is moved to the bit #0. Thus, the register value is rotated in a big circle, and nothing is lost.

The result of the shift can be stored back to the File register or moved to the W register according to the operand 2. In our case, we want to store the result in the W register to save the “count” value unshifted. After implementation of this instruction we will have the following:

Now we have the changing bits at the right places (see highlighted). So we can just copy the W register to the GPIO register. And we already know how to do this, using the MOVWF instruction which we see in line 26.

So that’s it! You see that using the hardware connection and understanding how everything is made, we can decrease the code size several times. I also want to note that if LED1 and LED2 were connected to GP0 and GP1, respectively, we wouldn’t even need the AND and shift operation as it would work correctly without them. This brings us to the idea that understanding how these things work can help us to design the best schematic diagram that implements the current task with minimal memory usage. The line between developing the schematic and developing the firmware is thin indeed!

PS. Now we know 2 more instructions - ANDWF and RLF, so 22 of 33.

Make Bread with our CircuitBread Toaster!

Get the latest tools and tutorials, fresh from the toaster.

What are you looking for?