Institutionen för data- och informationsteknik 2011-10-11 LEU240 Mikrodatorsystem Vi har tidigare i olika sammanhang sett att det är önskvärt att kunna använda ett högnivåspråk som C för att skriva program för vår mikrokontroller och därmed undvika den mer otympliga assemblerprogrammeringen. Låt oss se hur vi stegvis kan gå över från assemblerkod till C- kod. Vi skall studera ett mycket enkelt exempel som samtidigt innebär hantering av externa portar som ligger minnesmappade till fasta adresser och därför i C måste adresseras med hjälp av pekare till dessa adresser. Genomgången kommer inte att behandla hantering av interrupt i C. Vi kommer att återkomma till detta senare. Antag att vi vill använda labsystemet MC12 för att läsa inporten PORTB (adress $0600) på experimentkortet ML4 och skriva resultatet av läsningen till utporten PORTA (adress $0400) på samma kort. Vi vill göra läsningen och skrivningen kontinuerligt, dvs gång efter gång. I assembler skulle vi skriva Kod 1 ORG $1000 start: LDAA $0600 STAA $0400 BRA start Kod 1 och assemblera den via Eterm. ORG-direktivet anger den minnesadress där programstarten skall placeras och resten av programmet hamnar då på efterföljande adresser. Adressen $1000 är den lägsta adressen i MC12:s RAM-minne och är därför lämplig som startadress för programmet. Låt oss nu flytta över denna kod till utvecklingsmiljön XCC. I denna miljö kommer vi att länka in en startup-fil (_startup.s12) före vår programkod. Denna startup-fil är skriven i assembler och sätter upp processorns grundkonfiguration och initierar en enkel seriekommunikation så att vi kan låta en applikation skriva meddelanden till XCC:s terminalfönster. Vi återkommer till denna fil i annat sammanhang. Vi måste dock redan här känna till att denna fil efter initieringen avslutas med hopp till labeln _main och det måste då här innebära ett hopp till vår programkod. Denna label måste då ligga först i vår programkod så att programkoden CHALMERS TEKNISKA HÖGSKOLA Institutionen för data- och informationsteknik Sida 1 Avdelningen för datorteknik Besöksadress: Rännvägen 6 412 96 Göteborg
anropas efter initieringen. Vi återkommer lite senare till varför labeln inleds med en understrykning (_). Samtidigt kommer länkarskriptet (default.lsc) som styr hur koden skall länkas ihop och var den skall placeras i minnet att ta hand om minnesallokeringen. Vi återkommer även till denna fil. Startup-filen kommer av länkaren att placeras med start på adress $1000 i minnet och vår kod kommer att placeras på nästföljande adress efter startupkoden. Vilka adresser vårt program placeras på är nu ointressant eftersom länkaren kommer att sköta om hoppet från startup-kod till vårt programavsnitt. Då detta nu sköts av länkarskriptet så skall vi i vår programkod Kod 1 ta bort ORG-direktivet samtidigt som vi ersätter start-labeln med labeln _main. Vi får Kod 2 define _main _main: LDAA $0600 STAA $0400 BRA _main Kod 2 segment bss Som synes har vi även infört ett define-direktiv. Detta för att labeln _main skall vara synlig i resten av projektet. Segmentdeklarationen för att skapa ett bss-segment för oinitierade variabler kan synas omotiverad men vi måste införa denna deklaration då länkaren förutsätter att det finns ett bss-segment. Detta sätts i normala fall upp av C-kompilatorn men här körs inte C-kompilatorn utan bara assemblatorn, då vi bara har en assemblerfil, och bss-segmentet kommer inte att sättas upp om vi inte anger det explicit. Låt oss nu börja att steg för steg överföra koden till C. Vi börjar med att införa ett huvudprogram, en main()-rutin. Då denna rutin kompileras så sker detta genom att C-koden först översätts till assembler och rutinens namn, här main, kommer att ersättas av en label med samma namn men föregången av en understrykning (_) för att särskilja den från lablar som kommer från assemblerkod som vi själva har skrivit. Det betyder att här kommer den till assembler översatta main-rutinen att inledas av labeln _main precis som i Kod 2 och den kommer då att anropas av startup-rutinen. Vi startar kodöversättningen med att införa huvudprogrammet och behålla vår assemblerkod och helt enkelt skriva vårt program som inline-assembler. Vi får Kod 3 asm(" LDAA 0x0600"); asm(" STAA 0x0400"); asm(" BRA _main"); Kod 3 Lägg märke till de dubbla understrykningarna i kommandot asm. Lägg också märke till att de assembleruttryck som saknar label måste inledas med ett mellanslag (eller ett tabsteg). Dessa uttryck måste precis som i vanlig assemblerkod vara intabbade för att inte tolkas som sida 2
lablar. Observera också att vi har utnyttjat rutinens namn som återhoppsadress i loopen samt att vi skriver hexadecimala tal via förstavelsen 0x och inte via $. Det senare gör vi för att förstavelsen 0x fungerar både i assembler och C medan förstavelsen $ bara fungerar i assembler. För att göra koden lite mer läsbar så kan vi börja med att deklarera portadresserna som konstanter med begripliga namn, Kod 4 #define ML4_PORTB 0x0600 #define ML4_PORTA 0x0400 Lägg märke till att assemblerdirektiven ML4_PORTB EQU 0x0600 ML4_PORTA EQU 0x0400 Inte fungerar i C, däremot fungerar #define-direktiven både i assembler och C varför det är lämpligt att alltid använda dess. asm(" LDAA %a",ml4_portb); asm(" STAA %a",ml4_porta); asm(" BRA _main"); Kod 4 Lägg märke till att konstanterna inte kan skrivas in direkt i inline-assemblerkoden utan måste läggas till som ett argument. Detta beror på att koden i inline-uttrycket kommer att överförs till assemblatorn som en textsträng och namnen skulle om de skrivits in direkt i assemblerkommandona då bli delar av textsträngen och inte tolkats som våra definierade konstanter, dvs som adresser. Nästa steg i förenklingen kan vara att samla alla deklarationer rörande HC12 i en h-fil som vi inkluderar. h-filen skulle här då ha ett innehåll enligt Kod 5 #define ML4_PORTB 0x0600 #define ML4_PORTA 0x0400 Kod 5 Låt oss kalla filen HC12.h. Vår programkod blir nu då Kod 6 sida 3
asm(" LDAA %a",ml4_portb); asm(" STAA %a",ml4_porta); asm(" BRA _main"); Kod 6 Delen..\ i include-kommandot betyder att filen inte ligger i den aktuella projektkatalogen utan ett steg uppåt i katalogstrukturen. Vi gör detta för för att filen enkelt skall kunna användas av alla projekt i aktuellt workspace. Vi går vidare och gör den första övergången till C genom att skriva evighetsloopen som en while-loop i C, Kod 7 asm(" LDAA %a",ml4_portb); asm(" STAA %a",ml4_porta); Kod 7 Förfarandet är välkänt från C och kräver väl knappast någon kommentar. Låt oss nu lämna inline-assembler och i stället skriva assemblerkoden som en subrutin i assembler, en rutin som vi anropar från main-funktionen. Vi behöver alltså dels en C-fil med huvudprogrammet enligt Kod 8 asm(" JSR start"); Kod 8 och dessutom en assemblerfil med subrutinen i Kod 9 sida 4
USE "..\HC12.h" define start entry start start: LDAA ML4_PORTB STAA ML4_PORTA Kod 9 exit start define-direktivet behövs för att labeln start skall vara synlig globalt. entry- och exit-direktiven används av debuggern och inte nödvändiga för själva programmet. Observera att samma h-fil med definitioner kan användas i både C och assembler, detta tack vara att vi använder #define-direktiv som är tillåtna i både C och assembler och inte assembler-skrivsättet med EQU som inte fungerar i C. Lägg märke till att i assembler inkluderas h-filen via USE-direktiv och inte via #includedirektiv. Lägg också märke till att vi har återinfört labeln start som anropslabel för vårt subrutinanrop. Låt oss gå ytterligare ett steg genom att skriva subrutinanropet med C-syntax, Kod 10 start(); Kod 10 Som vi minns från tidigare så kommer funktionsnamnet start på den anropade funktionen att översättas till en label som inleds med en understrykning (_) då koden kompileras till assembler, dvs den blir _start vilket gör att vi måste ändra labelnamnet i subrutinen genom att inleda labeln med understrykningen, Kod 11 sida 5
USE "..\HC12.h" define _start entry _start _start: LDAA ML4_PORTB STAA ML4_PORTA exit _start Kod 11 Vi går nu över helt till C genom att införa pekare för att adressera de fysiska adresserna till våra portar. Samtidigt tar vi bort subrutinen då vårt huvudprogram nu ändå blir bara en rad långt. Vi bygger alltså vidare på Kod 7. Vi får Kod 12 *((unsigned char *)(0x0400))= =*((unsigned char *)(0x0600)); Kod 12 Raden med pekarna är medvetet delad i två delar för att öka läsbarheten. Även här kan vi naturligtvis använda våra konstantdeklarationer för adresserna som används i pekarna, Kod 13 *((unsigned char *)(ML4_PORTA))= =*((unsigned char *)(ML4_PORTB)); Kod 13 Vi ser att dessa pekarkommandon är ganska komplicerade och långa. Då vi använder dem så är risken mycket stor att vi skriver fel och blandar ihop alla asterisker och parenteser. Låt oss därför deklarera makron som kan ersätta våra pekare. De portar vi hanterar har 8 bitar så vår pekare skall vara av typen char. Då vi i de flesta fall skall hantera ett bitmönster och inte ett sida 6
8 bitars tal med tecken så är datatypen unsigned char lämplig eftersom vi på detta sätt undviker att talet (bitmönstret) teckenkompletteras på ett felaktigt sätt. Vi får Kod 14 #define REG8(x) *((unsigned char *)(x)) REG8(ML4_PORTA)=REG8(ML4_PORTB); Kod 14 Även här är det lämpligt att samla våra makrodeklarationer i en h-fil som kan användas av alla projekt, dvs vi lägger h-filen en katalognivå upp. Eftersom det också kan förekomma att vi adresserar 16 och eventuellt 32 bitars objekt så kan vi här också deklarera REG16 och REG32 med hjälp av datatyperna unsigned short respektive unsigned long. Det är i alla lägen lämpligare att använda short och long i stället för int då short alltid är 16 bitar och long alltid är 32 bitar medan int kan vara 16 eller 32 bitar beroende på utvecklingsmiljö. Vi får Kod 15 #include../hc12.h #include../reg_macro.h REG8(ML4_PORTA)=REG8(ML4_PORTB) Kod 15 Där h-filen reg_macro.h då har innehållet enligt Kod 16 #define REG8(x) *((unsigned char *)(x)) #define REG16(x) *((unsigned short *)(x)) #define REG32(x) *((unsigned long *)(x)) Kod 16 I och med detta avslutar vi övergången från assembler till C. sida 7