Servo motor, indirect addressing, and electronic lock - Part 10 Microcontroller Basics (PIC10F200)
Hi there! After the last two tutorials, we are confident about dealing with buttons. Today, we’ll expand this knowledge and learn how to check the state of the several buttons at once. And, bonus! We’ll also learn how to work with a servo motor.
Today’s task is the following - Create a code lock with the three buttons and one servo motor which will rotate the locking mechanism. The code for the lock should consist of four digits from 1 to 3 (according to the button numbers). If the entered code is correct, the servo should rotate the mechanism 90 degrees clockwise. If the entered code is incorrect, nothing should happen. If the lock is already open, then pressing any button should cause it to lock by rotating the mechanism 90 degrees counterclockwise.
As you’ve probably already guessed, the schematic for this device differs from the “basic” one, so this time we’ll stick to the following schematic (figure 1).
As you can see in figure 1, there are three buttons SW1, SW2, and SW3, which are connected to pins GP3, GP0, and GP1 respectively. So far nothing new.
But there is also a connector X2 named SG90. This is the servo motor. I selected the SG90 because it’s very common and cheap (you can buy one for $1.2 - $1.5).
Before proceeding to the program, let’s first consider the servo motor connection and its operation in more detail.
The appearance of the SG90 servo is shown in figure 2. As you can see, it consists of the servo itself, and 3 changeable attachments which can be attached to the servo’s shaft and fixed in place with a screw.
The servo has 3 wires, usually they are orange, red, and brown. Their meaning is shown in figure 1 - red and brown wires for the power supply (plus 5V and ground respectively), while the orange wire is the control one.
Control of the servo motor position is performed by means of the PWM which we considered in tutorial 7 - if you didn’t grasp that as well as you should’ve, go back and check it out! This tutorial will still be here. In the SG90 datasheet, you can find the following picture (figure 3).
The position of the servo’s shaft is set by changing the pulse duration between 1 and 2 milliseconds, where 1ms corresponds to the very left position and 2ms corresponds to the very far right position. Thus, 1.5ms will put the shaft in the middle position. In figure 3, you can see that the PWM period is 50Hz but from my experience it’s the minimum period, so you can just set the stall time as 20ms regardless of the pulse duration, meaning your actual period could be anywhere from 21 milliseconds to 22 milliseconds. Also, you don’t need to send the pulses all the time, you can send them just when you want to rotate the shaft to the given position, and then just disable the PWM.
However! You should know that in this case, the servo’s shaft will be released and someone can rotate it manually relatively easily. But if you keep sending the pulses, the torque will still be applied to the shaft and you most likely will break the gear rather than move it manually. So it depends on your task and requirements as to whether you should continuously send the modulated signal or if you should just send it and then disable the PWM. In our program, we will disable the PWM after rotating the gear. This can be done because the locking mechanism will be inside the lock, so there will not be access to it from the outside.
I think that’s enough information to start working with the servo. Let’s now consider the program that implements our task.
__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF
code_value EQU b'01011110' ;code 2 3 1 1
; 01 01 11 10
; 1 1 3 2
i EQU 10 ;Delay register 1
j EQU 11 ;Delay register 2
lock_state EQU 12 ;Lock state: 3 - closed, 2 - opened
count EQU 13 ;Counter of the pressed buttons
code_reg EQU 14 ;Register for code
servo_steps EQU 15 ;Number of pulses for servo to change position
num1 EQU 16 ;First number register
OPTION ;Enable GPIO2 and pull-ups
MOVLW ~(1 << GP2)
TRIS GPIO ;Set GP2 as output
MOVWF lock_state ;Set lock state as "closed"
GOTO LOCK_CLOSE ;and close the lock
CALL INIT_REGS ;Initialize the registers values
READ_BUTTONS ;Here the "read buttons" part starts
CALL CHECK_BUTTONS ;Read the buttons state
ANDLW 3 ;Clear all the bits of the result except two LSBs
BTFSC STATUS, Z ;If result is 0 (none of buttons were pressed)
GOTO READ_BUTTONS ;then return to the READ_BUTTONS label
MOVLW D'40' ;Otherwise load initial value for the delay
CALL DELAY ;and perform the debounce delay
CALL CHECK_BUTTONS ;Then check the buttons state again
BTFSC STATUS, Z
GOTO READ_BUTTONS ;If button is still pressed
MOVWF INDF ;Then save the button code in the INDF register
CALL CHECK_BUTTONS ;and keep checking the buttons state
BTFSS STATUS, Z
GOTO $-3 ;until it becomes 0
MOVLW D'40' ;Perform the debounce delay again
BTFSS lock_state, 0 ;If the last bit of the lock_state is 0(lock is opened)
GOTO LOCK_CLOSE ;then close the lock (with any button)
INCF FSR, F ;otherwise increment the indirect address,
DECFSZ count, F ;decrement the button press counter,check if it is 0
GOTO READ_BUTTONS ;If it is not, then return to the READ_BUTTONS
CALL INIT_REGS ;otherwise initialize registers again
CHECK_CODE ;and start checking the code
MOVF code_reg, W ;Copy the code value into the W
ANDLW 3 ;and clear all the bits of W except of the two LSBs
SUBWF INDF, W ;Subtract W from the indirectly addressed register
BTFSS STATUS, Z ;If result is not 0 (code is not correct)
GOTO LOOP ;then return to the LOOP label
RRF code_reg, F ;otherwise shift the code register right
RRF code_reg, F ;two times
INCF FSR, F ;Increment the the indirect address
DECFSZ count, F ;Decrement the counter and check if it is 0
GOTO CHECK_CODE ;If it is not, then check the next code value
LOCK_OPEN ;otherwise open the lock
BCF lock_state, 0 ;Clear the LSB of the lock_state
CALL MANIPULATE_SERVO;and manipulate the servo to open the lock
GOTO LOOP ;Then return to the LOOP label
LOCK_CLOSE ;Code part to close the lock
BSF lock_state, 0 ;Set the LSB of the lock state
CALL MANIPULATE_SERVO;and manipulate the servo to open the lock
GOTO LOOP ;Then return to the LOOP label
INIT_REGS ;Initialize the registers
MOVLW num1 ;Copy the num1 register address to the
MOVWF FSR ;indirect address pointer
MOVLW 4 ;Set count as 4 wait for 4 buttons presses
MOVLW code_value ;Copy code_value
MOVWF code_reg ;into the code_reg register
RETLW 0 ;Return from the subroutine
BTFSS GPIO, GP3 ;Check if GP3 is 0 (SW1 is pressed)
RETLW 1 ;and return 1 (b'01')
BTFSS GPIO, GP0 ;Check if GP0 is 0 (SW2 is pressed)
RETLW 2 ;and return 2 (b'10')
BTFSS GPIO, GP1 ;Check if GP1 is 0 (SW3 is pressed)
RETLW 3 ;and return 3 (b'11')
RETLW 0 ;If none of the buttons are pressed then return 0
DELAY ;Start DELAY subroutine here
MOVWF i ;Copy the W value to the register i
MOVWF j ;Copy the W 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
MANIPULATE_SERVO ;Manipulate servo subroutine
MOVLW D'20' ;Copy 20 to the servo_steps register
MOVWF servo_steps ;to repeat the servo move condition 20 times
SERVO_MOVE ;Here servo move condition starts
BSF GPIO, GP2 ;Set the GP2 pin to apply voltage to the servo
MOVF lock_state, W ;Load initial value for the delay
CALL DELAY ;(2 to open the lock, 3 to close it)
BCF GPIO, GP2 ;Reset GP2 pin to remove voltage from the servo
MOVLW D'25' ;Load initial value for the delay
CALL DELAY ;(normal delay of about 20 ms)
DECFSZ servo_steps, F ;Decrease the servo steps counter, check if it is 0
GOTO SERVO_MOVE ;If not, keep moving servo
RETLW 0 ;otherwise return from the subroutine
Well, this time the program is much longer and more complicated than the previous tutorial. Though, let’s be honest, that’s the point! To learn and grow - so let’s try to understand what’s written here so we can become better at all this.
In line 4 we declare code_value as b’01011110'. This is the code pattern with which we will compare the buttons presses sequence. Let’s consider it in more detail.
As we have three buttons, let’s give them code numbers 1, 2, and 3 respectively, which in the binary system are ‘01’, ‘10’, and ‘11’ respectively.
If this is still confusing, that means you haven't checked out our Binary, Hexadecimal, and Other Base Numbers tutorial and you really, really should.
So, to encode three buttons we need just two bits. According to the task the code pattern consists of four digits, so we can fit this pattern into one byte: 4 digits x 2 bits/digit = 8 bits = 1 byte.
Thus, ‘code_value’ is this pattern - let’s ungroup it into four subgroups: ‘01 01 11 10’. In the decimal representation this will be ‘1 1 3 2’. Also it will be easier (and I’ll show why) to start comparison from the least significant bits (LSBs), which is why the current code is 2, 3, 1, 1. So, you need to press SW2, then SW3, then SW1 twice to open the lock. Now that you know how it works, you can set any other value for your particular lock.
Some attentive reader may ask “Hey, how reliable is this code? We only have 3 buttons for encoding”.
The number of combinations can be calculated with the equation:
where N - number of combinations, B - number of buttons, C - code length. So for our case
So we have just 81 different combinations which gives us fairly average reliability or difficulty in cracking it. But if you increase the code length to eight, the number of combinations will be
which is significantly tougher to guess/luck upon.
Let’s move forward. In lines 8 and 9 we declare ‘i’ and ‘j’ which are delay registers. We’ve done this before, so nothing new here.
In line 10 we declare the ‘lock_state’ register which will have one of two values - 3 if lock is closed and 2 if lock is opened. I’ll explain later why we use these values and not the typical 1 and 0.
In line 11 we declare ‘count’ register which will count the number of buttons pressed during the code entering, as well as number of checked values during the code validation.
In line 12 we declare the ‘code_reg’ register which will be used during the code validation.
The ‘servo_steps’ register declared in line 13 is used to set the number of pulses to send to the servo to let it revolve the gear.
In line 14 there is the ‘num1’ register declaration which is the button number pressed first. One can ask “Where are num2, num3, and num 4 then?” This is a good question, I’ll explain it later.
Finally, we’ve finished with the declarations, let’s now move to the code itself.
Initialization part doesn’t consist of anything new, so I’ll skip the explanation of lines 16 to 21.
In lines 22-23 we load the value 3 into the ‘lock_state’ register to indicate that the lock is closed, and after that go to the ‘LOCK_CLOSE’ label (line 24) to perform the closing of the lock. So, at the start of the program the lock is being closed if it isn’t already closed.
In this tutorial I’ll break the tradition of explaining the code line by line. It will be better to explain it by the program logic. So let’s now move to the ‘LOCK_CLOSE’ label which is located at line 70 and consider it alone with the ‘LOCK_OPEN’ part started at line 65.
In lines 71 and 66 we perform opposite actions - in the first case we set the LSB of the ‘lock_state’ register, in the second case we clear it. Why do we do this? In lines 22-23, we put the value 3 into this register. As you know (at least you should know it by now) 3 is ‘11’ in binary system and 2 is ‘10’ in the binary system. So, they differ only with the LSB: for 3 it is set, and for 2 it is clear. So instead of loading 2 or 3 into the ‘lock_state’ every time we just clear or set its LSB, saving 2 lines of code. Lines 67 and 68 are equal to lines 72 and 73. First, we call the ‘MANIPULATE_SERVO’ subroutine and then return to the ‘LOOP’ label.
Let’s now jump to line 104 and consider what’s going on in the ‘MANIPULATE_SERVO’ subroutine.
First, we load the value 20 into the ‘servo_steps’ register (lines 105-106). This value is quite enough to revolve the servo gear by 90 degrees.
In line 107, we have the ‘SERVO_MOVE’ label, indicating that here starts the code that implements servo shaft revolving. There is nothing difficult or really new here, so I’ll just briefly move through it.
In line 108, we set the GP2 bit to start the voltage pulse at the servo control input.
In line 109, we copy the content of the ‘lock_state’ register to W and then call the ‘DELAY’ subroutine in line 110.
The ‘DELAY’ subroutine is our normal routine (lines 94-102), the only difference is that the W register is loaded first, not inside the subroutine, so we can change the length of the delay from outside of the DELAY subroutine.
If you’ve been paying attention and are particularly clever, you may have guessed now why we used the values 2 and 3 for the ‘lock_state’ register. If we load value 2 into the delay registers then we get the delay value of approximately 1 ms, and if we load 3 there we get a delay of about 1.5 ms which corresponds to the very left and the central position of the servo gear. Thus ‘lock_state’ plays two roles - indicates the lock state and sets the pulse duration for the servo position control. This is another great example of the small tricks that you can do in Assembly to make the code just a touch smaller and more streamlined.
In line 111, we clear the GP2 bit to end the voltage pulse at the servo control input.
In line 112, we load the value 25 into the W register and call the DELAY again (line 113) to provide the delay between pulses of about 20 ms.
In line 114, we decrement the ‘servo_steps’ value and check if it’s 0. If not, then line 115 is implemented, and we return to the label ‘SERVO_MOVE’ (line 107), otherwise this line is skipped, and we return from the subroutine.
That’s everything for the servo manipulation. As you can see, it’s not that difficult. But in this example we used just 2 positions of the servo and the locations are pretty approximate - we don’t need a lot of precision. If you need to set the position more precisely, then you need to calculate the pulse duration more accurately like we did in tutorial 8 (where we played music).
Let’s now return to line 24 where we left our main program. We’ve done a lot since starting from this line.
In line 25 there is just ‘LOOP’ label which plays the usual role - indicates that the initialization part is over and the next code part will be looped.
In line 27 we call ‘INIT_REGS’ subroutine. And again, let’s immediately move to line 76 and consider this subroutine. As follows from its name, here we initialize some registers. In lines 77 and 78, we copy the address of the ‘num1’ register into the FSR register.
You might remember (but most likely not), I mentioned the FSR register with the INDF register in tutorial 3, saying that they are used for indirect addressing, and promising to consider them in more detail later. Well, it’s later!
It’s logical that if there is some indirect addressing then there should be some direct addressing, right? Actually, all instructions that we have used so far to deal with registers were using direct addressing. That means that we set the address of the register directly in the instruction. E.i. if we write ‘BSF GPIO, GP1’ that means that we set bit GP1 in the register ‘GPIO’, not any other one.
Indirect addressing works in a different way. It can be used with the same instructions as for the direct addressing but there is one difference. Let’s consider how to write the line ‘BSF GPIO, GP1’ using indirect addressing.
BSF INDF, GP1
It looks more complicated (and in fact, it is) but sometimes it can be beneficial. Let’s consider these 3 lines in detail.
First, we copy the address of the GPIO register into the W register. As you remember, the instruction MOVLW is used to load a literal to the W register, and in this case we use the ‘GPIO’ as the literal, which means that we copy the 0x06 value (because ‘GPIO’ register address is 0x06) into the W register. Again, now we have the address of the ‘GPIO’ register in the W, not its value.
Second, we copy the address into the FSR register. This register is the indirect data memory address pointer. Just an ordinary pointer which you may have used in high-level registers and with higher-level programming languages. As long as we loaded some address into the FSR register, you are able to manipulate the register with this address using the INDF register, which you see in the third line.
Obviously, there is no sense using indirect addressing to manipulate just one register but imagine that you want to apply the same action to a line of the registers (e,g, clear them or load some value into them). In this case, indirect addressing is very helpful. Let’s consider the short example of clearing the RAM using indirect addressing (I took it from the PIC10F200 datasheet, page 19).
As you can see, in the first two lines we load the address 0x10 (the first RAM address) into the FSR register. In the third line we clear INDF register which now will affect the register with the address 0x10. In the fourth line we increment the FSR register. This means that the next address to clear in the third line will be 0x11, and after that 0x12, and so on and so far. In the fifth line, we check if the 4th bit of the FSR register is set. This is another good trick. As we know, the PIC10F200 has 16 bytes of RAM, and 16 in binary is ‘00010000’. So we keep increasing the address until the bit 4 becomes 1 (which means that we reached the value of 16), and this means that we’re all done.
OK, I hope that’s now more or less clear and we can return to the program.
So, in lines 77 and 78, we copy the address of ‘num1’ register into the FSR register. That means that the first button code will be stored in the ‘num1’ register, then its address will be incremented, and the next button code will be stored in the next register (which is not named in our code). This is important that, after ‘num1’, there should be at least three unused registers, and that’s why I defined it as the last one.
In lines 79 and 80, we copy the value ‘4’ into the ‘count’ register indicating that we expect to have four buttons presses. Actually, this value sets the length of the code. If you want a longer (or shorter) code, this will need to change.
In lines 81 and 82, we copy the ‘code_value’ into the ‘code_reg’ register. This operation is needed for the code validation which we will consider soon.
Line 83 contains the RETLW instruction which returns us from the subroutine.
And again, let’s return to the main program for a short time (I hope you remember that we left it in line 27).
In line 28, there is the label ‘READ_BUTTONS’, so the next code part will be devoted to the reading the buttons state.
In line 29, we call the subroutine ‘CHECK_BUTTONS’, and ehhh… Let’s consider this subroutine too. So many subroutines! But the good news is that it’s the last subroutine we need to consider.
Let’s move to line 85!
In line 86, we check if bit GP3 of the GPIO register is set. Because of the pull-up resistors, if GP3 is set, this means that SW1 is released. In this case, line 87 is skipped and we move to line 88. Otherwise (if GP3 is cleared, which means that button SW1 is set) line 87 is implemented. Here, we have an unusual use of the RETLW instruction (well, it’s common enough in Assembly, but this is the first time we’ve done this). So ‘RETLW 1’ means that we return from the subroutine and load ‘1’ into the W register. In this case, ‘1’ means the code of the button SW1.
Lines 88 and 89, and 90 and 91, are equal to lines 86 and 87, we just check the GP0 (SW2) and GP1 (SW3) there and return with values 2 or 3 correspondingly.
If none of the buttons were pressed, we reach line 92 where we return with the value 0. This value is indicating that there was no button press.
So in the ‘CHECK_BUTTONS’ subroutine we check all the buttons at once and distinguish which one was pressed by checking the W register afterwards.
I want to mention that if several buttons are pressed at the same time, only one will be read, the one that is higher in the code. So buttons have a priority from higher to lower: SW1, SW2, SW3.
Now let’s return to line 29 and we don’t have any more subroutines to distract us.
So after calling the ‘CHECK_BUTTONS’ subroutine we have the pressed button code in the W register. Now we need to process and save it.
In line 30, there is a new instruction ANDLW. I think you are experienced enough at this point to understand it’s meaning - “logical AND between Literal and W”. So this instruction implements a logical AND operation between the given literal and W register and save the result into the W register. Here we use ANDLW 3. As we know 3 is ‘11’ in binary, moreover it’s the largest button number we can have, So this instruction implements two main functions here - clears all the bits of the W register except the last two bits, and checks if either of the last two bits are 1. The last check is needed to find out if any button was pressed, because as you remember, if none of them were pressed then the W contains a 0, and the result of the ANDLW 3 will be 0 too. In this case, the Zero flag of the Status register will be set, and we can use this fact to check if any button was pressed.
We do this in line 31 by means of the instruction BTFSC STATUS, Z. If this bit is cleared (Z = 0) then the result of the previous operation was not zero, and some button was pressed. In this case line 32 is skipped and we go to line 33, which performs debounce delay together with the line 34. Otherwise (if Z = 1) line 32 is implemented, and we return to the label ‘READ_BUTTONS’ again waiting while any of buttons is pressed.
Remember: If the result of a command such as ANDLW is "0" then the Z-bit in the Status register sets to "1". If the result is not "0", the Z-bit is "0" and the BTFSC command skips the next line.
The next part of the button state reading process is almost the same as in the previous tutorial. After the debounce implementation we check the buttons state again (lines 35-38). If button is still pressed (which means that there was not a random pulse) then we save the content of the W register (which contains the button number) into the INDF register (line 39).
So at the first iteration INDF will represent ‘num1’ register, at the second iteration it will represent the register after ‘num1’ and so on.
In lines 40-43 we wait while the button is pressed and do nothing. After the button is released, we perform another debounce delay (lines 44-45) to ensure the button is really released.
After that, we check if the LSB of the ‘lock_state’ is set (line 46). And we remember that when the lock is closed then the LSB is 1 and if it is open, then the LSB is 0. If it is not set (LSB = 0, lock is opened) then line 47 is not skipped, and we move to the label ‘LOCK_CLOSE’. This means that if the lock is already opened then whichever button you press, the lock will be closed without any code.
If LSB = 1 (lock is closed) then line 47 is skipped, and we move to line 48 where we increment the indirect address pointer FSR to save the new button value into the next RAM address.
In line 49 we decrement the ‘count’ register and check if it is 0. If it is not 0 then line 50 is not skipped and we return to the ‘READ_BUTTONS’ label to read the next button. If ‘count’ is 0 then it means that all four buttons were read and we can move to the code checking part.
Before checking the code we initialize the registers again by calling the ‘INIT_REGS’ subroutine in line 52, after that the FSR will point at the ‘num1’ register, and ‘count’ will contain value 4 which means that we will check four values.
In line 53, there is the ‘CHECK_CODE’ label which indicates the beginning of the code part that will validate the entered code.
In line 54 we copy the value of the ‘code_reg’ register into the W. This is needed so we don’t lose the data as we will want to manipulate it a bit.
In line 55 we perform a logical AND between W and literal 3. After implementation of this line all the bits of the W register will be cleared except the last two bits,
Then we perform a subtraction of the INDF register from the W (line 56) to check if the result is 0. If it is 0 then the first button was valid.
Let’s consider how that happens. After line 54, the W contains the value ‘01011110’ which was set in the line 4 as ‘code_value’. After line 55, the W value is ‘00000010’. If we pressed the buttons in the right order then ‘num1’ will also have the value ‘00000010’. Thus after the subtraction the result should be 0. If it is not 0 then it means that the first button was incorrect and we should skip code checking and return to the buttons reading again. This is performed in lines 57, 58. As the SUBWF instruction affects Zero flag of the Status register we check if it was set (line 57). If it was not set, then line 58 is not skipped, and we return to the ‘LOOP’ label.
Otherwise we move to line 59. Here, we find another new instruction RRF. This instruction is opposite to the familiar RLF. RRF shifts the mentioned register right through the Carry bit and saves the result back to the file register or to the W depending on the second operand.
As you can see, we call this instruction twice, that means that we want to shift the ‘code_reg’ by two bits right. Before these lines ‘code_reg’ value is ‘01 01 11 10’, and after them it becomes ‘10 01 01 11’. I specially put spaces between the number groups so you can see that the last two bits now become the first two bits, and the other part of the register is shifted right.
After that, we increase the indirect address (line 61) to compare the next saved button value.
And finally we decrement the counter value and check if it is 0 (line 62). If it is not, then we return to the label ‘CHECK_CODE” and compare the next button value. Let’s consider it one more time.
Now ‘code_reg’ value is ‘10 01 01 11’, and after line 55 the W register contains ‘00000011’. The INDF now contains the value at the address 17 (next to ‘num1’) which should be ‘00000011’ too. When we compare them, we shift the ‘code_reg’ again and will have its value as ‘11 10 01 01’, and we decrement the FSR again so the next time INDF will contain value of the register with the address 18.
In this way we compare all four values. If there is any mistake, line 58 will return us to the ‘LOOP’ label and the lock will not open. If there are no mistakes, after checking all four values line 63 will be finally skipped and we will move to line 65 where ‘LOCK_OPEN’ label is, which means that we finally reach the goal and open the lock!
Now let’s assemble the code lock according to the figure 1 and program it. If you try to press the buttons in the right sequence most likely nothing will work. The problem is the same as with the previous tutorial. You need to disconnect all the wires between the programmer and the device except VDD and GND as the programming pins affect the microcontroller inputs.
And if you disconnect these wires and nothing works yet, try to press the buttons with the shift until the lock opens. E.g. if the code is 2-3-1-1, try to press 2-3-1-1-1 several times. The code will be shifted one digit every time and thus you will finally guess it. Now, as long as you were able to open the lock, press any button once, the lock should close. Now it is set up correctly. You can enter the code 2-3-1-1 again and make sure that the lock opens.
That’s all I wanted to share with you today - it was a long one with a lot of interesting things! Let’s summarize what we’ve learnt in this tutorial. We’re familiar with two new instructions - RRF and ANDLW, so we now know 23 of 33 - just 10 left! Also, we now know how to use indirect addressing using FSR and INDF registers. Moreover, we now can control servo motors.
As homework, I’d suggest you to make the code more reliable, increasing its length from 4 to 8 digits.
Get the latest tools and tutorials, fresh from the toaster.