Harry Broeders (met aanpassingen van Ben Kuiper)
Deze pagina is bestemd voor studenten van de Haagse Hogeschool - Academie voor Technology, Innovation & Society Delft groep EQ1.2.
Z'n 30 jaar geleden was de TV serie Knight Rider populair. In deze serie werd de hoofdrol gespeeld door een sprekende auto KITT genaamd. Deze auto was aan de voorkant voorzien van een rijtje rode LED's (lampjes) die in een bepaald patroon aan en uitgeschakeld werden. Deze rij LED's moesten een scanner voorstellen waarmee KITT de omgeving verkende.
In deze practicumopgave gaan we zelf een rijtje LED's aansturen met een bepaald patroon met behulp van de ATmega32 microcontroller. We beginnen eerst met een voorbeeldprogramma en daarna...
typedef unsigned char uint8_t;
void wait(void) {
volatile int i;
for (i = 0; i < 30000; ++i)
/*empty*/;
}
int main(void) {
void wait(void);
uint8_t c1, c2, i;
volatile uint8_t* ddrb = (uint8_t*)0x37;
volatile uint8_t* portb = (uint8_t*)0x38;
*ddrb = 0xFF;
while (1) {
c1 = 0x80;
c2 = 0x01;
for (i = 0; i < 4; i++) {
wait();
*portb = ~(c1 | c2);
c1 >>= 1;
c2 <<= 1;
}
}
return 0;
}
make
(wat verborgen zit achter de menu
optie Build, Build). De uitvoer van make verschijnt in het build window van
Atmel Studio en ziet er ongeveer als volgt uit (als je de laatste regel begrijpt is het goed ;-):
------ Build started: Project: testproject, Configuration: Debug AVR ------ Build started. Project "opdr1a.cproj" (default targets): Target "PreBuildEvent" skipped, due to false condition; ('$(PreBuildEvent)'!='') was evaluated as (''!=''). Target "CoreBuild" in file "C:\Program Files (x86)\Atmel\Atmel Studio 6.2\Vs\Compiler.targets" from project "C:\Users\Ben\Documents\Atmel Studio\6.2\testproject\testproject\opdr1a.cproj" (target "Build" depends on it): Task "RunCompilerTask" Shell Utils Path C:\Program Files (x86)\Atmel\Atmel Studio 6.2\shellUtils C:\Program Files (x86)\Atmel\Atmel Studio 6.2\shellUtils\make.exe all Building file: .././opdr1a.c Invoking: AVR/GNU C Compiler : 4.8.1 "C:\Program Files (x86)\Atmel\Atmel Toolchain\AVR8 GCC\Native\3.4.1061\avr8-gnu-toolchain\bin\avr-gcc.exe" -x c -funsigned-char -funsigned-bitfields -DDEBUG -DF_CPU=2457600UL -O0 -ffunction-sections -fdata-sections -fpack-struct -fshort-enums -g2 -Wall -mmcu=atmega32 -c -std=gnu99 -MD -MP -MF "opdr1a.d" -MT"opdr1a.d" -MT"opdr1a.o" -o "opdr1a.o" ".././opdr1a.c" Finished building: .././opdr1a.c Building target: opdr1a.elf Invoking: AVR/GNU Linker : 4.8.1 "C:\Program Files (x86)\Atmel\Atmel Toolchain\AVR8 GCC\Native\3.4.1061\avr8-gnu-toolchain\bin\avr-gcc.exe" -o opdr1a.elf opdr1a.o -Wl,-Map="opdr1a.map" -Wl,--start-group -Wl,-lm -Wl,--end-group -Wl,--gc-sections -mmcu=atmega32 Finished building target: opdr1a.elf "C:\Program Files (x86)\Atmel\Atmel Toolchain\AVR8 GCC\Native\3.4.1061\avr8-gnu-toolchain\bin\avr-objcopy.exe" -O ihex -R .eeprom -R .fuse -R .lock -R .signature -R .user_signatures "opdr1a.elf" "opdr1a.hex" "C:\Program Files (x86)\Atmel\Atmel Toolchain\AVR8 GCC\Native\3.4.1061\avr8-gnu-toolchain\bin\avr-objcopy.exe" -j .eeprom --set-section-flags=.eeprom=alloc,load --change-section-lma .eeprom=0 --no-change-warnings -O ihex "opdr1a.elf" "opdr1a.eep" || exit 0 "C:\Program Files (x86)\Atmel\Atmel Toolchain\AVR8 GCC\Native\3.4.1061\avr8-gnu-toolchain\bin\avr-objdump.exe" -h -S "opdr1a.elf" > "opdr1a.lss" "C:\Program Files (x86)\Atmel\Atmel Toolchain\AVR8 GCC\Native\3.4.1061\avr8-gnu-toolchain\bin\avr-objcopy.exe" -O srec -R .eeprom -R .fuse -R .lock -R .signature -R .user_signatures "opdr1a.elf" "opdr1a.srec" "C:\Program Files (x86)\Atmel\Atmel Toolchain\AVR8 GCC\Native\3.4.1061\avr8-gnu-toolchain\bin\avr-size.exe" "opdr1a.elf" text data bss dec hex filename 266 0 0 266 10a opdr1a.elf Done executing task "RunCompilerTask". Task "RunOutputFileVerifyTask" Program Memory Usage : 266 bytes 0,8 % Full Data Memory Usage : 0 bytes 0,0 % Full Done executing task "RunOutputFileVerifyTask". Done building target "CoreBuild" in project "opdr1a.cproj". Target "PostBuildEvent" skipped, due to false condition; ('$(PostBuildEvent)' != '') was evaluated as ('' != ''). Target "Build" in file "C:\Program Files (x86)\Atmel\Atmel Studio 6.2\Vs\Avr.common.targets" from project "C:\Users\Ben\Documents\Atmel Studio\6.2\testproject\testproject\opdr1a.cproj" (entry point): Done building target "Build" in project "opdr1a.cproj". Done building project "opdr1a.cproj". Build succeeded. ========== Build: 1 succeeded or up-to-date, 0 failed, 0 skipped ==========
De volgende paragraaf beschrijft hoe je het programma thuis kan testen en uitvoeren. Dit moet je dan ook thuis doen! Tijdens het practicum kun je hier verder gaan.
main
.
De I/O registers van de ATmega32 zijn 8 bits breed. In een C programma dat
deze I/O registers gaat lezen en/of schrijven moeten dus vaak 8 bits brede
variabelen worden gebruikt. In C89 (de eerste versie van ANSI C) is het enige
geschikte datatype het type
char
.
Omdat we 8 bits getallen zonder teken in deze variabelen willen opslaan
maken we gebruik van het datatype
unsigned
char
. In de eerste regel van het programma wordt met
behulp van een
typedef
de identifier uint8_t
gedefinieerd als een alias voor het type
unsigned
char
.
typedef unsigned char uint8_t;
Daarna wordt de functie wait
gedefinieerd.
void wait(void) {
volatile int i;
for (i = 0; i < 30000; ++i)
/*empty*/;
}
Deze functie is als tijdvertraging bedoeld. De variabele i
wordt
geïnitialiseerd op 0
en telkens met 1
verhoogd
tot de waarde 30000
is bereikt. Dit duurt ongeveer 0,25 seconde
op een ATmega32 als een klokfrequentie van 3.686 MHz gebruikt wordt. Dit
is proefondervindelijk vastgesteld. Het gebruik van het keyword
volatile
wordt hier uitgelegd.
Vervolgens wordt de hoofdfunctie main
gedefinieerd.
int main(void) {
void wait(void);
uint8_t c1, c2, i;
volatile uint8_t* ddrb = (uint8_t*)0x37;
volatile uint8_t* portb = (uint8_t*)0x38;
*ddrb = 0xFF;
while (1) {
c1 = 0x80;
c2 = 0x01;
for (i = 0; i < 4; i++) {
wait();
*portb = ~(c1 | c2);
c1 >>= 1;
c2 <<= 1;
}
}
return 0;
}
De eerste regel in main
is het prototype (de declaratie) van
de functie wait
. Dit is in dit geval niet noodzakelijk omdat
de functie wait
al gedefinieerd is (boven main
).
Als de functie wait
echter onder de functie main
wordt geplaatst dan is de declaratie van wait
wel noodzakelijk.
Vervolgens worden de variabelen c1
, c2
en
i
gedefinieerd. Dit zijn alledrie variabelen van het type
uint8_t
(een alias voor het standaardtype
unsigned
char
). Deze variabelen kunnen dus een 8 bits unsigned
waarde bevatten.
Vervolgens worden 2 pointers gedefinieerd.
volatile uint8_t* ddrb = (uint8_t*)0x37;
volatile uint8_t* portb = (uint8_t*)0x38;
De variabele ddrb
is dus een pointer naar een variabele van
het type uint8_t
. Een pointer is een variabele die staat
te wijzen naar een plaats in het geheugen (normaal gesproken een andere
variabele). In dit geval wordt de pointer geïnitialiseerd met de waarde
0x37
. Als een getal in een C programma met 0x
begint,
dan betekent dit dat de constante in het hexadecimale talstelsel is opgegeven.
De I/O registers van de ATmega32 zijn via memory adressering te bereiken.
Het ddrb register (data direction register port b) kan bereikt worden via
adres 0x37. Doordat de pointer ddrb
naar het memory adres van
het ddrb register wijst kan dit register via deze pointer worden beschreven
en gelezen. Voor de constante 0x37
staat een zogenaamde cast
operatie (uint8_t*)
dit zorgt ervoor dat de compiler begrijpt
dat het echt de bedoeling is om een uint8_t*
variabele te vullen
met een integer constante. Als de cast operatie wordt weggelaten geeft de
compiler de volgende warning:
../opdr1b.c:12: warning: initialization makes pointer from integer without a cast
Het gebruik van het keyword
volatile
wordt hier uitgelegd.
Op de volgende regel wordt de pointer ddrb
gebruikt om alle
pinnen van poort B als outputs te configureren. Dit wordt gedaan door 8 enen
(0xFF
) te schrijven in het ddrb register. De notatie
*ddrb
betekent: "waar de pointer ddrb
naar wijst"
(= geheugenlokatie 0x37 = data direction register port b).
*ddrb = 0xFF;
De rest van het programma bestaat uit een oneindige lus:
while (1) {
...
}
De voorwaarde van de while
is namelijk altijd waar
(1
). Veel programma's voor microcontrollers hebben z'n oneindige
lus omdat het programma moet blijven draaien zolang de microcontroller aan
staat.
In de lus worden de variabelen c1 en c2 geïnitialiseerd met een waarde.
c1 = 0x80;
c2 = 0x01;
c1
wordt geïnitialiseerd met de binaire waarde 10000000
en c2
met de binaire waarde 00000001. Omdat we in standaard
C constanten niet in het binaire talstelsel kunnen opgeven wordt de hexadecimale
notatie gebruikt.
Voor iemand die alles wil weten: Als je gebruik maakt van de GNU C
compiler (die gebruikt wordt door AVR Studio) dan is het wel mogelijk om
binaire constanten op te geven met de prefix 0b
. Dus
bijvoorbeeld:
c1 = 0b10000000;
De compiler geeft dan wel de volgende warning:
../opdr1b.c:17:12: warning: binary constants are a GCC extension
Het is echter beter om alleen standaard C te gebruiken voor het geval we later een andere C compiler willen gaan gebruiken.
Na het initialiseren van de variabelen c1
en c2
volgt een
for
lus die 4x wordt uitgevoerd voor i
= 0 tot 4.
for (i = 0; i < 4; i++) {
wait();
*portb = ~(c1 | c2);
c1 >>= 1;
c2 <<= 1;
}
In de
for
lus wordt eerst de functie wait
aangeroepen om even te wachten.
Daarna wordt de bitwise or bepaald van de variabelen c1
en
c2
. Deze waarde wordt geïnverteerd naar het portb register
weggeschreven. Meer informatie over bitwise bewerkingen in C kun je
hier vinden.
Vervolgens wordt de inhoud van de variabele c1
één
plaatsje naar rechts geschoven met de schuifinstructie c1 >>=
1
. De inhoud van c2
wordt één plaatsje
naar links geschoven met de schuifinstructie c2 <<= 1
.
Meer informatie over schuifbewerkingen in C kun je
hier vinden.
Voor iemand die alles wil weten: Na de
while
lus volgt nog de instructie:
return 0;
Deze instructie wordt echter nooit uitgevoerd omdat de
while
lus nooit eindigt. Als we de instructie weglaten geeft de compiler echter
de volgende warning:
../opdr1b.c:27: warning: control reaches end of non-void function
Misschien denk je nu: Waarom vertel je de compiler dan niet dat de
functie main
niets teruggeeft door het returntype
void
te gebruiken in plaats van
int
.
Volgens de C89 standaard moet main
echter een
int
returntype hebben. De compiler geeft als je
void
gebruikt de volgende warning:
../opdr1b.c:9: warning: return type of 'main' is not `int'
Als we het programma zonder warnings willen laten compileren is de
return 0
dus noodzakelijk.
In het bovenstaande programma hebben we geschreven naar de I/O registers van de ATmega32 door gebruik te maken van een pointer variabele.
volatile uint8_t* ddrb = (uint8_t*)0x37;
*ddrb = 0xFF;
Het is echter ook mogelijk om naar de I/O registers te schrijven door het gebruik van een soort "tijdelijke" pointer.
(*(volatile uint8_t*)0x37) = 0xFF;
Er wordt in dit geval een "tijdelijke" pointer aangemaakt naar geheugenlokatie
0x37
. Vervolgens wordt de waarde 0xFF
weggeschreven
naar de geheugenlokatie waar deze pointer naar wijst. Als we het aanmaken
van de tijdelijke pointer definiëren als de identifier DDRB
met behulp van een
#define
dan is het net of het ddrb register een gewone C variabele is.
#define
DDRB
(*(volatile uint8_t*)0x37)
DDRB = 0xFF;
Als we gebruik maken van deze tijdelijke pointers ziet het programma er als volgt uit:
typedef unsigned char uint8_t;
#define
DDRB(*(volatile uint8_t*)0x37)
#define
PORTB(*(volatile uint8_t*)0x38)
void wait(void) {
volatile int i;
for (i = 0; i < 30000; ++i)
/*empty*/;
}
int main(void) {
void wait(void);
uint8_t c1, c2, i;
DDRB = 0xFF;
while (1) {
c1 = 0x80;
c2 = 0x01;
for (i = 0; i < 4; i++) {
wait();
PORTB = ~(c1 | c2);
c1 >>= 1;
c2 <<= 1;
}
}
return 0;
}
Dit programma kun je hier downloaden: opdr1b.c
In de include file avr/io.h zijn onder andere
#define
's
opgenomen zodat alle registers van de gebruikte AVR microcontroller als C
variabelen gebruikt kunnen worden. In de file avr/io.h wordt gekeken naar
het ingestelde type AVR microcontroller om te bepalen welke registernamen
aan welke adressen moeten worden gekoppeld. Het is dus van groot belang om
bij het aanmaken van het project het juiste AVR type te selecteren.
In de include file stdint.h zijn onder andere de zogenaamde fixed width (vaste
breedte) integer datatypes int8_t
, uint8_t
,
int16_t
, uint16_t
, int32_t
,
uint32_t
, int64_t
en uint64_t
gedefinieerd. Het is dan niet meer nodig om deze types zelf met
typedef
te definiëren. Deze file is in 1999 in de ANSI/ISO C standaard (C99)
opgenomen.
In de include file util/delay.h zijn onder andere 2 functies gedefinieerd waarmee wachtlussen geprogrammeerd kunnen worden.
void
_delay_ms(double
ms); /* ms is maximaal 262.14 / F_CPU * 1000000 */void
_delay_us(double
us); /* us is maximaal 768 / F_CPU * 1000000 */
De maximale waarde van de parameter ms
en
us
is afhankelijk van de klokfrequentie van de
microcontroller (F_CPU
). De klokfrequentie van de ATmega32 op
het practicum is ingesteld op 3.686 MHz.
F_CPU =1000000 |
F_CPU =3686000 |
F_CPU =8000000 |
F_CPU =16000000 |
|
---|---|---|---|---|
ms max |
262.14 |
71.11 |
32.76 |
16.38 |
us max |
768.00 |
208.35 |
96.00 |
48.00 |
De delay functies zijn afhankelijk van de gebruikte klokfrequentie. Het is dus van groot belang om deze juist in te stellen in het project dat je hebt gemaakt. Dit kun je doen door allereerst Project -> Properties (Alt+F7) en vervolgens het tabblad Toolchain te selecteren. Klik vervolgens op Symbols. Je dient hier een zgn. nieuwe symbol (m.b.v. Add Item) aan te maken die bestaat uit F_CPU=3686000UL.
Voor iemand die alles wil weten: UL staat voor Unsigned Long. Je geeft hier immers een getal op dat mogelijk niet in een integer past en daarnaast is het getal altijd positief.
Er is overigens ook de mogelijkheid om in je code #define F_CPU 3686000UL
te zetten en
dit doet effectief hetzelfde. Het is wel belangrijk deze regel voor de include te zetten van
header-files die deze frequentie gebruiken (zoals util/delay.h).
Als we gebruik maken van bovenstaande include files ziet het programma er als volgt uit:
#include <avr/io.h>
#include <stdint.h>
#include <util/delay.h>
void wait(void) {
uint8_t i;
for (i = 0; i < 10; ++i)
_delay_ms(25);
}
int main(void) {
void wait(void);
uint8_t c1, c2, i;
DDRB = 0xFF;
while (1) {
c1 = 0x80;
c2 = 0x01;
for (i = 0; i < 4; i++) {
wait();
PORTB = ~(c1 | c2);
c1 >>= 1;
c2 <<= 1;
}
}
return 0;
}
Dit programma kun je hier downloaden: opdr1c.c
Als je de simulator gebruikt dan kun je ook de klokfrequencie van de simulator instellen. Vreemd genoeg neemt de simulator niet automatisch de bij de project opties ingestelde frequentie over. Je kunt deze simulator opties alleen maar instellen als de simulator gestart en op pauze staat door een break-point of door op het drukken op Break (Ctrl+F5).
Je kunt de ingestelde klokfreqentie zien in de Processor view:
Door op de Frequency te klikken kun je deze ook vervolgens veranderen. Helaas kun je maar 4 decimalen opgeven! Daardoor kun je niet exact de frequentie van 3,6864 MHz invoeren en dit zorgt ervoor dat de stopwatch niet exact meer klopt.