In part 1 we discussed some of the parts of the Attiny13A and got to a point where we would be ready to loading a program in to the microcontroller. (LINK TO PART 1)
In this part we will look at how to create a program.
If you are not really interested in how we got the binary code, I would suggest to jump to part 3 where we will actually load this code in to the Attiny13A. (ADD LINK TO PART X)
The scope of this code, will simply be the often used blink example, where we are tuning an LED on and off.
If we programmed this in the Arduino IDE, it could look something like this:
‘digitalWrite set’s the pin high, so pin 3 goes high, then delay for x miliseconds and then another ‘digitalWrite’ to set the pin low again.
So a few line of code would make this possible, now if we are to do the same in binary, we need to break this down and find the appropriate instructions on the microcontroller.
I have decided that I will first create the logic using assembler, as this is so close to the binary form we need for the eventual load and much easier to read (at least relative to binary code). The assembler instructions available for the AVR – can all be found here: http://www.atmel.com/images/Atmel-0856-AVR-Instruction-Set-Manual.pdf.
We will first look at how to program some of the more physical aspects of the microcontroller, like values for pin’s and using interrupts. After that we will look at the logic aspects of our program.
Here I will go through the following 4 type of tasks:
- Set a pin as output
- Set value on selected pin to ‘1’ or ‘0’
- Make the program wait
- Creating a loop where we keep running the program
Setting a pin as output
Setting the pin as output is controlled by the Data Direction Register, in this case for Port B (DDRB). In order for us to control this, we need to:
- Find the address of the Data Direction Register for Port B
- Find what bit in this register that controls if the pin is input or output
In the datasheet we find the following information for register DDRB:
|Register Address||Name||Further details|
The address os the DDRB register is 0x17 and we can find further details on page 56 – please see below:
Port B Data Direction Register
We can see what bits control each of the pins, and we can also see what the initial value of the bits are.
So if we want to make pin 3 output, then we need set this to ‘1’, and since we are writing an entire byte, the byte we need to write here is: ‘0000 1000’.
So our task for the program is:
Write ‘0000 1000’ to DDRB
The assembler instruction for this is: OUT DDRB, register X (register with value we want to load)
We only need to set this once, as we need this to be enabled for output for the entire time.
Set the value of the pin high ‘1’ and low ‘0’
The is controlled from the Port x Data Register, as we are working with Port B, it is the PORTB register – the address is 0x18 (you can check this in the datasheet – LINK TO DATASHEET) and we can find further details on PORTB on page 56 again.
From the datasheet we see that bit 4 is controlling PRTB4, so to set this high ‘1’, we need to write the byte ‘0001 0000’ to this address, when we want the LED to light up, and ‘0000 0000’ when we want to turn the LED off.
So here the programming task is:
First write ‘0001 0000’ to PORTB
Write ‘0000 0000’ to PORTB
The assembler instruction for this is: OUT PORTB, (register with – 0b00010000) for high, and
OUT PORTB, (register with – 0b00000000) for low.
In between these two commands, we have the wait statement.
Let the program wait
If we look at the possible instructions available for us in the Attiny13A, there is no direct wait x seconds or cycles instruction.
In order to perform a wait for a microcontroller there is basically 2 options;
Make the microcontroller perform a number of instructions that really do nothing but wasting time. Since we know how many clock cycles an instruction takes and we know how many clock cycles the microcontroler runs at, we can make some calculations of how many instructions would we need to run in order to ‘waste’ 1 second
The other option is to have something external that would indicate that a certain amount of time has passed – interrupts.
After reading up on both approaches I decided to try to use the interrupt approach. So we need to look at a few things on interrupts first.
Interrupts are external to the CPU and can be external to the microcontroller or we can use internal interrupts. For our purpose we will use the timer within the microcontroller to create the interrupt.
The timer in our microcontroller is just a counter, that counts (it will count 1 for every clock cycle), and as we are working with an 8-bit, we can only count from 0 – 255, before starting from zero again.
If we are running the microcontroller at 1 MHz (that is 1 million instruction, or counts, per second), then this will only takes us 256 nano seconds, to count from 0 to 255 – we would need to do this 1,000,000 / 256 = 3906 times to give us approxamately one 1 second.
To make this easier we are going to use an option for the AVR microcontroller’s to make the timer run slower than the CPU – this feature is called prescaling. So while the microcontroller still runs at 1 MHz, we can force the timer to run at a fraction of this speed.
If we look at the datasheet for the ATtiny13A (table 11-9) we see the following prescaling options:
|Bit 2||Bit 1||Bit 0||Description|
|0||1||0||clock IO / 8|
|0||1||1||clock IO / 64|
|1||0||0||clock IO / 256|
|1||0||1||clock IO / 1024|
From the above we see that we have 4 options for making the timer run slower – the 4 options are:
1/8 of the clock speed → 8 clock cycles will make the timer count 1 time
1/64 of the clock speed → 64 clock cycles will make the timer count 1 time
1/256 of the clock speed → 256 clock cycles will make the timer count 1 time
1/1024 of the clock speed → 1024 clock cycles will make the timer count 1 time
If we use the 1/1024 option, that would mean that the counter only count 1 up, for every 1024 clock cycles.
So using this math:
1 Mhz processor = 1 million clock cycles per second
Timer only count 1 for every 1024 clock cycles
For the timer to count from 0 to 0 again (255 + 1 count) that would now take:
256 counts * 1024 clock cycles = 262,144 clock cycles
262,144 clock cyles approxamately ¼ of a second
From the above that gives us that we would use up approximately ¼ of a second to count from 0 – 255. If we do this 4 times, that would give us close to the 1 second delay we are looking for.
The register where we configure this is TCCR0B at address 0x33:
Bit 3 to 7 are not relevant for our purpose.
The assembler code: LDI, TCCR0B, 0b0000 0101
That gives us the next task – how do we keep track of how many times the counter have counted from 0 to 255?
Overflow flag and interrupts
When the timer hit’s the 255, it will throw an overflow flag – so if we count how many times we get this overthrow flag, we know that after 4 times, we have 1 seconds. One option would be to always poll for this overflow flag, but the other option, the one used here, us to have the overflow flag create an interrupt request – and this is when our program will add one to how many times we have seen the overflow flag.
When the CPU receives our interrupt request, the running program will finish it’s current instruction and then perform a jump to the address where the code associated with the interrupt exists.
So our task is to create the configuration for the interrupt triggered by the timer. This has 2 steps:
Make the timer send an interrupt request
Let the microcontroller know what to do when the interrupt happens
Enabling the timer to send the overflow flag is done by setting bit 1 (‘0000 0010’) in the TIMSKO (Timer/Counter Interrupt Mask Register), see in the datasheet in section 11.9.6.
The assembler code for this: LDI TIMSKO, 0b00000010.
Know when the timer overflows (that is at the point where the counter goes from 255 to 0), the AVR will look in the table of interrupt vectors (table 9-1) if any address is present – if yes jump to this address.
So we need to place a jump to our code that reacts to the overflow in address 0x0003 (binary 0000 0011′) – IN THE PROGRAM CODE. One interesting feature is that if we let the overflow flag trigger this interrupt, it will automatically be reset. As a result we do NOT need to write any code that reset this overflow indicator.
Please note, that here we are asked to set a value in the programming address space, not an register. That creates the necessity to avoid writing any of our program code in this specific address.
The assembler code: LDI 0b00000011, xxxx.
In summary we have so far:
Found the register where we need to set the pin we plan to use (pin 3) as an output pin.
Found the register where we can control the value of this pin (high/low)
Found the register where we can make the timer run at a slower speed than the CPU
Found the register where we can enable the timer
Found the address where we can insert the address of the program to run when an interrupt happens
Now it is time to look at some of the logic we want to create in the program.
Since we know that in order for us to measure 1 second, we need to track that the overflow flag from the timer has done this 4 times, we need logic to keep track of the number of interrupts.
The program will have to do something similar to this:
set a variable to 4
subtract one for each interrupt
if variable is 0, then change the output pin to other state (if pin is ‘1’ then change to ‘0’ and visa versa)
If not, then go back to sleep
Then go back to step 1
Set a variable
Setting a variable in assembler can be done with the LDI command, and we will use register 16 (R16) to hold this variable:
The assembler code: LDI R16, 4 /* we load 4 in to register R16
Subtracting from variable
The command DEC wil subract 1 from the value given.
The assembler code: DEC R16 /* subtract 1 from current value in register R16
If then test
The assembler code: CPI R16, 1 /* Compare if R16 = 1
The assembler code: BRLT (address) /* If R16 lower than 1, then jump to address
For this we use branching instruction, BRLT, Branch Lower Than. This test is done again the result after the execution of the CPI instruction.
We need to supply this command with the actual address to jump to, and here we can not use the logical names that would otherwise be available in assembler.
For our program we will have the make the address calculation manually.
The main program is going to be simple;
The assembler code: SLEEP /* This will make the CPU stop processing and wait for the interrupt.
The assembler code: JMP address /* This will be the instruction we return to after the subroutine called by the interrupt.
Using the SLEEP command will also save power, but is not the main concern here of course.
Putting all of this together in to one program
We have 3 main parts:
Set selected registers with needed values
Rutine called for every interrupt that controls the light
Set selected registers with needed values
ldi r16, 8 ; Setting bit 3 high
ldi r17, 20 ; load 4 for count down to register R17
ldi r18, 0 ; used for setting pin 3 low
ldi r19, 8 ; used for setting pin 3 high
out DDRB, r16 ; loading R16 to PORTB – set pin 3 as output
ldi r16, 0 ; set register R16 to 0
out PORTB, r16 ; set to pin 3 low
ldi r16, 5 ; set R16 to 000 0101
out TCCR0B, r16 ; load R16 to timer, prescaling = 1024
ldi r16, 2
out TIMSK0, r16 ; Enable timer overflow interrupt
sei ; enable interrupts glbally
In our case the only thing we want the main program to do is sleep
when not interrupted by the timer.
sleep ; makes the microcontroller sleep
rjmp main ; jumps to main
SLEEP /* Will put the CPU in the microcontroller to sleep and wait for interrupt
Rutine called for interrupt
This where we control if the LED should be switched on or off as well as performing this switch.
count: ; When interrupt, call this rutine
dec r17 ; subtract 1 one Register 17 (starts at 4)
cpi r17, 1 ; Test if Register 17 is equal to 1
brlt change ; If Register 17 lower than 1, from test above jump to change
reti ; if not, then just return to where the interrupt was called from
; First test what current value for output is
in r16, PORTB ; move current port status to R16
cpi r16, 4 ; test if pin 3 high
breq chglow ; if above test tru, then jump to change to low
out PORTB, r19 ; change to high
ldi r17, 12 ; set counter again, this time for 3 seconds
I created the above code in Atmel Studio, and tested it in the simulater. The next step will be to convert the above program to binary code. This we will do in part 3.