Arbetsbok för MD407 2017 Roger Johansson och Göteborgs Mikrovaror (GMV) e-post: info@gbgmv.se Internet: http://www.gbgmv.se Förord Denna arbetsbok är avsedd i första hand som självstudiematerial. Arbetsboken ger en introduktion till enkortsdatorn MD407 uppbyggd kring den ARM-baserade ST32F407 och laborationskort avsedda för MD407. Arbetsboken omfattar teori och övningsuppgifter fördelade på åtta kapitel: I) Det inledande kapitlet behandlar programutveckling i assemblerspråk. Syftet är att ge en introduktion till den första av de använda programutvecklingsmiljöerna, ETERM8. II) Det andra kapitlet behandlar mikroprocessorns instruktionsuppsättning. Speciellt belyses hur assemblerprogrammering kan utföras på ett strukturerat sätt där man utgår från konstruktioner i programspråket C, dvs. kodgenerering. Avsnittet introducerar en rad viktiga begrepp och förutsätter grundläggande kunskaper i exempelvis C eller Java. III) I kapitel 3 introducerar vi ett generellt verktyg för test och avlusning, GDB (GNU-debugger). Vi ger några enkla exempel på hur du använder GDB som alternativ till ETERM8. Vi behandlar också CodeLite och GCC. IV) I detta kapitel fortsätter vi med att detaljstudera parallell IO (generella IO-portar) hos ARM-processorn. V) Kapitlet behandlar synkronisering med yttre enheter, vi studerar en lättanvänd krets för tidmätning, gemensam för olika ARM-processorer. Vi konstruerar programpaket med basala drivrutiner för två olika LCD-displayer VI) VII) VIII) Undantagshantering och avbrott med Cortex-M4. I kapitlet studeras olika typer av seriekommunikation. Speciellt behandlar vi uppbyggnaden hos en generell seriekommunikationskrets (USART) som kan användas för såväl synkron som asynkron seriekommunikation I detta kapitel behandlar vi olika programbibliotek, speciellt så kallade kompilatorbibliotek för såväl heltalsaritmetik som flyttalsaritmetik och standard C-biblioteket, men även hur man skapar sina egna bibliotek. Göteborg i oktober 2017, Roger Johansson 1
Arbetsbok för MD407 - PROV Innehåll: Avsnitt 1 Programutveckling i assemblerspråk Avsnitt 2 Strukturerad assemblerprogrammering Avsnitt 3 Introduktion till Codelite, GCC och debugger GDB Avsnitt 4 General Purpose IO Avsnitt 5 Synkronisering, alfanumerisk och grafisk display Avsnitt 6 Undantag, avbrott Avsnitt 7 Seriekommunikation Avsnitt 8 Programbibliotek Appendix A MD407 minnesdisposition B THUMB 16 instruktionsuppsättning C Minnesdisposition och länkarscript D Motorola S-format E ASCII (UTF-8) representation F Exceptionvektorer 2
Inledning Programutveckling i assemblerspråk Kapitel 1 Programutveckling i assemblerspråk Denna arbetsbok innehåller en lång rad övningar som bland annat syftar till att du ska lära dig behärska utveckling av maskinorienterade program i assemblerspråk och programspråket C. I boken behandlas laborationsdatorn MD407 som är uppbyggd kring microcontrollern ST32F407, med en ARM Cortex M4 processor. Arbetsboken har organiserats i avsnitt som du arbetar dig igenom i tur och ordning. Du får lära dig att hantera verktyg för utveckling och test av datorprogram. Detta första avsnitt har ägnats helt åt en introduktion till hur du använder utvecklingssystemet ETERM8. Vi behandlar programutvecklingsprocessen, dvs implementering och test av programvara. ETERM8 och programutvecklingen ETERM8 är avsett för programutveckling i assemblerspråk och har anpassats för undervisningsändamål. ETERM8 omfattar funktioner för: Textredigering, källtexten skrivs/redigeras med hjälp av en Editor, färgad syntax används för att hjälpa dig upptäcka enklare stavningsfel. Assemblering, källtexten översätts till en laddfil som innehåller programmets maskinkod och data. Test, laddfilen överförs till den inbyggda simulatorn eller till laborationsdatorn MD407 via ETERM8:s terminalfunktion. Programutveckling i assembler skiljer sig inte särskilt mycket från programutveckling i något högnivåspråk. Programmet skrivs i form av källtext, dvs. en textfil som innehåller instruktioner och direktiv till assemblatorn. Då programmet, eller en lämplig del av det, är färdigt måste det översättas till maskinkod innan programmet kan testas i en måldator eller simulator. Översättningen av programmet sker i flera steg med hjälp av olika verktyg. Följande figur illustrerar processen. Översättning av assemblerprogram till exekverbar kod 3
Arbetsbok för MD407 - PROV Det första steget kallas assemblering (ung. sätta samman ) och utförs av assemblatorn. Assemblatorn översätter programmets källtext till objektkod, till skillnad från källtextens textformat är objektkoden i binärformat. En, eller eventuellt flera objektfiler, kombineras samman av länkaren till en exekverbar fil med elf-format (executable linkable format). Man kan också välja att låta länkaren skapa bibliotek med program som senare kan kombineras på nytt med ytterligare objektkod. För att skapa en exekverbar fil krävs också ett så kallat länkarskript, en textfil där man bland annat samlat information om var maskinkoden ska placeras i måldatorns minne, var i minnet utrymme reserverats för variabler etc. För att kunna överföra en exekverbar fil till laborationsdatorn måste den konverteras till en så kallad laddfil, med ett textbaserat format (s-records) detta görs med ett konverteringsprogram. I laddfilen finns programmet representerat på en form som kan överföras till laborationsdatorn och där tolkas som instruktioner och data. Då programmet, i form av laddfil, överförts till laborationsdatorn MD407 kan det exekveras (utföras) och man kan då kontrollera programmets funktion men man kan också använda ETERM8:s simulator för test av program. Till simulatorn kan programmet överföras på såväl S-format som elf-format. Laborationsdator MD407 Om du inte redan gjort det så starta nu ETERM8. Tänk också fortsättningsvis på att bilderna som visas i denna bok kommer att avvika mer eller mindre beroende på vilken plattform (Windows, MacOS eller Linux) du använder. På samma sätt kan utseendet avvika beroende på vilket tema, i operativsystemet, man använder. 4
Programutveckling i assemblerspråk Anm. För ditt arbetsbibliotek bör du undvika sökvägar med svenska tecken eller "mellanslag" eftersom vissa programverktyg som ETERM använder inte hanterar dessa korrekt under Windows. ETERM8 har initialt tre olika arbetsytor med olika flikar: Navigator - fliken Directories används för att ange ett arbetsbibliotek, under fliken Files visas alla filer i arbetsbiblioteket. Messages - i fliken Output visas meddelanden från assemblatorn, fliken Console rymmer ett terminalfönster som används för att kommunicera med laborationsdatorn MD407 via någon USB-port. Den tredje arbetsytan som initialt visar en startsida, används som redigeringsfönster för textfiler. Ytterligare flikar skapas av ETERM8:s debugger, vi återkommer med detaljer efterhand som dessa introduceras. Börja nu med att skapa ett arbetsbibliotek (här D:\mop\lab1) välj själv lämplig plats och namn. Använd Navigator-Directories för att välja arbetsbiblioteket, välj (vänsterklicka) på arbetsbibliotekets namn, högerklicka därefter, en popup med namnet Set as working directory visas, klicka på denna. Växla därefter till fliken Files. Under Files sorteras filerna i arbetsbiblioteket i fyra olika grupper: Assembler source (ändelse.asm), dina källtexter Loadfile (ändelse.s19), laddfil med binär kod/data som överförs till laborationsdator eller simulator Listfile (ändelse.lst) adressinformation tillsammans med assemblerkod, kan ibland vara användbart vid test och felsökning. Filer som inte har någon speciell betydelse för ETERM8 sorteras in under Other files. 5
Arbetsbok för MD407 - PROV Skapa ett assemblerprogram Du skapar en ny källtextfil genom att välja File New från menyn. Därefter skriver du in namnet på den fil du vill skapa, skriv nu mom1.asm och klicka på Save. Om du inte anger något filnamnstillägg (eller anger ett annorlunda filnamnstillägg) lägger ETERM8 automatiskt till.asm. Nu skapas ett nytt fönster: ETERM s textredigeringsverktyg skapar ett nytt fönster där du kan redigera din källtext. Uppgift 1.1 Skriv nu in källtext enligt följande: Slut uppgift 1.1 Observera hur texteditorn färglägger din text ("färgad syntax"). Tecknet @ används för att ange att all påföljande text är kommentarer. Kommentarer ignoreras av assemblatorn, tecknet kan förekomma var som helst på en textrad och kommentarer färgas grå. Ett giltigt symbolnamn färgas grönt. Med "giltigt symbolnamn" menas alla kombinationer av teckensträngar där de ingående tecknen är tillåtna för en symbol. Detta innebär självfallet inte att symbolen är korrekt definierad eller refererad. Giltiga instruktioner, namn på ARM-register och assemblerdirektiv färgas blå Konstanter och operatorer färgas röda. Teckenkombinationen '0x' (noll-x) anger att påföljande talvärde ska tolkas på hexadecimal form. Notera speciellt hur instruktionen: LDT R1,=0x40020C00 färgas grön, dvs. tolkas som en symbol. Detta beror på att vi (avsiktligt) stavat instruktionen fel, rätt instruktion ska här vara LDR. Låt felet vara kvar, vi ska strax rätta till det. För att spara filen använder du nu File Save. Tänk på att korrekt färgad syntax inte nödvändigtvis innebär att ditt assemblerprogram är korrekt, det är snarare till för att göra dig uppmärksam på enklare stavfel, syntaxfel etc. Därför kan det hända att du får felmeddelanden även om du stavat såväl instruktioner som operander riktigt. 6
Programutveckling i assemblerspråk Assemblering Du assemblerar källtexten genom att: 1. Växla till fliken Files i Navigator fönstret. 2. Öppna Assembler source 3. Välj (vänsterklicka) på filnamnet mom1.asm högerklicka därefter, en popup knapp visas... 4. välj Assemble File alternativt: Från Menyn väljer du Tools Assemble, ett dialogfönster låter dig nu välja den fil du vill assemblera. Utskrifter från assemblatorn visas i sektionen Messages under fliken Output. Uppgift 1.2 1. Assemblera nu filen mom1.asm Assemblatorn kommer att klaga på den felstavade instruktionen. Efter filnamnet, med fullständig sökväg (som kan se annorlunda ut i ditt exempel) skrivs radnummer, därefter typ av fel. Utskriften "Error: bad instruction 'ldt R1,=0x40020C00'" berättar vad vi redan misstänkte, dvs. att instruktionens namn är felstavat. Felutskrift från assemblator 2. Dubbelklicka nu (vänster knapp) på felutskriften. Markören i marginalen pekar ut raden i källtextfilen som genererat felet. 3. Rätta felet (ändra den felaktiga instruktionen till LDR) och assemblera på nytt. Meddelandet No Errors ska nu visas i Output-fliken. Slut uppgift 1.2 7
Arbetsbok för MD407 - PROV Låt oss uppehålla oss vid det program vi skrivit och assemblerat. Detta innehåller såväl kommentarer som instruktioner och assemblerdirektiv. Vi har också definierat flera olika symboler. Programmet beskrivs av följande flödesplaner som steg för steg visar hur "specifikationen" längst till vänster översätts till ett (nästan) komplett assemblerprogram, längst till höger. @ alla tecken efter @ betraktas som kommentarer @ mom1.asm @ start: LDR R0,=0x55555555 sätt upp port D som utport LDR R1,=0x40020C00 STR R0,[R1] LDR R1,=0x40020C14 @ utport skapar en pekare till port D i R1 LDR R2,=0x40021010 @ inport skapar en pekare till port E i R2 main: LDR R0,[R2] laddar 32 bitar från port E i R0 STR R0,[R1] skriver innehållet i R0 till port E B main absolut programflödesändring (B=branch) Låt oss nu titta närmre på några av de instruktioner vi använt i vårt inledande programexempel. Ladda en konstant Eftersom alla LOAD-instruktioner är kodade med 16 bitar finns det ingen instruktion där en full 32-bitars adress kan kodas in i instruktionen. I stället tillhandahålls ett PC-relativt adresseringssätt. För att förenkla kodningen tillhandahåller assemblatorn så kallade pseudo-instruktioner som skapar rätt maskinkod. För att ladda en 32-bitars konstant i ett register har vi: ADR Address to register Adderar en konstant till värdet i PC och skriver resultatet till ett generellt register. Syntax: ADD Rd,PC,#<const> @ Generisk form ADR Rd,<label> @ Pseudo instruktion LDR Rd,=<label> @ Alternativ (GNU as) Observera att bara de två sista accepteras av assemblatorn. Rd Destination, generellt register R0-R7. <label> Adressen till en instruktion eller data. Assemblern bestämmer ett värde för offseten så att denna är rättad till WORD-adresser <const> Positiv konstant 0-1020. Endast multiplar av 4 är tillåtna, kodas därför med 8 bitar. Exempel Följande instruktioner: start: ADR R1,label LDR R2,=label label: utför exakt samma sak, dvs. placerar adressen label i register R1 och R2. Man kan också ange absoluta adresser som numeriska värden: LDR R1,=0x40020C00 placerar värdet 0x40020C00 i register R1. 8
Programutveckling i assemblerspråk Ladda en variabel från konstant adress LDR Load literal En basadress bestäms genom att en konstant adderas till innehållet i PC. Därefter kopieras ett WORD, från denna adress, till destinationsregistret Syntax: LDR Rt,<label> LDR Rt,[PC,imm] Rt Destination, generellt register R0-R7. <label> Symbolisk adress till data som kopieras till Rt.. imm Positiv konstant 0-1020. Endast multiplar av 4 är tillåtna, kodas därför med 8 bitar. Avståndet mellan referensen (LDR) och definitionen (label:) får alltså vara maximalt 1020 bytes, och symbolen label måste finnas på en adress som är jämnt delbar med 4, så kallat rättningsvillkor ( alignment ) eftersom instruktionen kodas på följande sätt: LDR Rd,<label> 0 1 0 0 1 Rd PC-rel offset Registret Rd kan vara R0-R7 och offseten kodas med 8 bitar. Genom att placera två implicita nollor efter offseten utökas det maximala avståndet mellan referensen men kräver samtidigt att label placerats på en fysisk adress i minnet som är jämnt delbar med 4, så kallat "word aligned". Exempel Assemblerdirektivet.WORD används för att generera en 32-bitars konstant i minnet. För att försäkra oss om att rättningsvillkoret är uppfyllt kan vi placera direktivet.align omedelbart framför symboldefinitionen label..align label:.word 0x11223344 Assemblatorn kommer då att garantera att alignmentvillkoret är uppfyllt genom att eventuellt fylla ut med enstaka bytes så att label hamnar på en tillåten adress. Instruktionen: LDR R0,label kan nu användas för att ladda konstanten 0x11223344 till register R0. Ladda en variabel från en evaluerad adress Då man refererar innehållet på en adress upprepade gånger är det lämpligt att först placera adressen i ett register. Därefter kan man enkelt komma åt innehållet med registerindirekt adresseringssätt. LDR Load immediate En basadress bestäms genom att en konstant offset adderas till innehållet i ett basregister. Därefter kopieras ett WORD, från denna adress, till destinationsregistret. Om konstanten är 0 kan den utelämnas. Syntax: LDR Rt,[Rn,+/-#imm5] LDR Rt,[Rn] Rt Destination, generellt register R0-R7. Rn Basregister för adressberäkningen. +/- Anger positiv eller negativ offset. Om tecken utelämnas tolkas detta som positiv offset. imm5 Positiv konstant 0-124. Endast multiplar av 4 är tillåtna, kodas därför med 5 bitar. 9
Arbetsbok för MD407 - PROV Exempel Den första instruktionen laddar adressen till en IO-port till register R2. LDR R2,=0x40021010 Nästa instruktion laddar 32 bitar från denna adress: LDR R0,[R2] Man kan läsa påföljande adresser (med 4 bytes intervall) genom att lägga till en offset, exempelvis laddar nu följande instruktion 32 bitar från adress 0x40021014: LDR R0,[R2,#4] Skriva till minnet Data skrivs till minnet med Store-instruktionen, STR. En skrivning måste alltid ske via ett register. Den vanligaste varianten är: STR Store immediate En basadress bestäms genom att en konstant offset adderas till innehållet i ett basregister. Därefter kopieras ett WORD, från källregistret till denna adress. Om konstanten är 0 kan den utelämnas. Syntax: STR Rt,[Rn,+/-#imm5] STR Rt,[Rn] Rt Källa, generellt register R0-R7. Rn Basregister för adressberäkningen. +/- Anger positiv eller negativ offset. Om tecken utelämnas tolkas detta som positiv offset. imm5 Positiv konstant 0-124. Endast multiplar av 4 är tillåtna, kodas därför med 5 bitar. Exempel Instruktionsföljden: LDR R1,=0x40021010 STR R0,[R1] skriver innehållet i register R0 till adress 0x40021010 Simulatorfunktioner Med ETERM:s debugger och MD407-simulator kan du simulera instruktionsutförandet i laborationsdatorn MD407. Du kan också övervaka och manuellt ändra minnesinnehåll. Processorns användarregister Simulatorns registersektion visar registeruppsättningen hos MD407. Eftersom vi återkommer till flyttalsenheten senare i denna arbetsbok är det nu i första hand de två första kolumnerna du har användning av. Du kan tills vidare bortse från flyttalsregistren S0-S21. Du kan ändra registrens innehåll manuellt genom att klicka på fönstret för det register du vill ändra och skriva in ett nytt värde, värdet anges på hexadecimal form. Då du ändrat ett registerinnehåll måste du också trycka Enter, för att ändringarna ska registreras i simulatorn. 10
Programutveckling i assemblerspråk Registeruppsättning Namn Beskrivning R0-R12 Generella register används för aritmetiska, logiska, skift-operationer etc. SP Aktiv stackpekares värde (MSP eller PSP), skillnader mellan stackpekarna behandlas i kapitel 6, till dess kan du utgå från att SP=MSP LR Länk-register, här sparas återhoppsadressen vid subrutinanrop PC Programräknare PSR Processor Status Register innehåller olika statusbitar från heltalsoperationer i de generella registren. FPSR Floating Point Status Register innehåller olika statusbitar från flyttalsoperationer, behandlas i kapitel 7 MSP Master Stack Pointer PSP Process Stack Pointer PRIMASK Speciella register, behandlas i kapitel 6 FAULTMASK BASEPRI CONTROL S0-S31 Generella flyttalsregister, behandlas i kapitel 8 Simulering av programexekvering Med debuggerns hjälp kan du få en första inblick i hur assemblerinstruktioner fungerar. Du kan utföra ett assemblerprogram instruktionsvis och i lugn och ro studera effekterna. Glöm inte att spara dina källtextfiler, många moment utgår från att återanvända kod. Under detta moment kommer vi att introducera användningen av debuggern. Först visar vi hur du kan ladda program till simulatorn och utföra programmet instruktionsvis. Du kommer också att se hur man kan koppla kringenheter till simulatorn. Starta Vi ska nu prova programmet mom1.asm. Simulatorn kan startas på olika sätt, 1. I fliken Navigator Files, välj gruppen Loadfile(.s19) 2. Högerklicka på mom1.s19, en popup med två alternativ visas: Open file - om du vill granska laddfilen med en editor Debug in simulator - för att starta simulatorn med denna laddfil alternativt: Välj från menyn Tools Start debugger en dialog låter dig nu välja laddfil. 1. Välj från menyn Tools Start debugger 2. Välj i dialogrutan: mom1.s19 Debuggern har två flikar: 11
Arbetsbok för MD407 - PROV CPU/disassembly, MD407-simulatorns olika ARM-register, en bild av det disassemblerade minnesinnehållet dvs. programmet och debuggerns kontrollfunktioner. Memory: MD407-simulatorns minne Eftersom programmet utför in- och ut- matning måste vi dock göra vissa förberedelser innan vi provar programmet. Till simulatorn finns också en fristående del som kallas IO-simulator (Input/Outputsimulator). Dess uppgift är att simulera olika omgivningar till laborationsdatorn, dvs. de enheter som inmatning sker från och utmatning sker till. Eftersom IO-simulatorn innehåller olika typer av kringenheter och dessa kan kopplas till olika anslutningar hos MD407 och vi måste göra dessa inställningar manuellt. Välj nu Setup IO-devices: Följande dialogruta öppnas: Till vänster, IO-ports, visas de anslutningar (portar) hos MD407 som kan användas i simulatorn. I mitten, IO-devices, visas de kringenheter som kan anslutas. Till höger visas en bild av den valda kringenheten. För att prova vårt första program, mom1, kopplar vi nu en 8 bitars strömställare (8 bit dipswitch) till den minst signifikanta byten av port E (GPIOE pin 0-7). Märk porten och kringenheten: Klicka därefter på Connect. Strömställarmodulen öppnas. Den är försedd med 8 st. enpoliga knappar som kan ställas i läge on eller off genom att man klickar på dem. Initialt står knapparna i läge off, vilket innebär att motsvarande logiknivå är 0. Genom att klicka en gång ändras läget till on, logiknivån är då 1, klicka en gång till på knappen för att få den att återgå till off, osv. I fönstret skrivs också den aktuella anslutningen ut, i detta fall PE0-7. Fortsätt nu med att ansluta en diodramp (8 segment Bargraph) till den minst signifikanta byten hos port D, GPIOD pin 0-7. 12
Programutveckling i assemblerspråk Diodrampsmodulen öppnas, den innehåller 8 st. dioder som kopplats till porten. Klicka slutligen OK för att behålla anslutningarna. Test av program Då vi nu anslutit enheter för såväl in- som utmatning börjar vi bli redo att testa vårt program. Följande knappar används för att kontrollera programexekveringen: Step instruction: Utför en maskininstruktion, kallas också trace, exakt en maskininstruktion utförs. Next instruction: Utför nästa instruktion, skillnaden mot Step instruction är att en tillfällig brytpunkt placeras på nästa adress i det sekventiella programflödet. Detta innebär exempelvis att hela subrutiner kan utföras utan att man behöver stega igenom varje instruktion i subrutinen. Step out: Utför instruktioner till en programflödesändring Step 5 ins/sec: Stega automatiskt, 5 instruktioner per sekund Run, stop at breakpoint: Utför programmet snabbt, respektera brytpunkter Run, ignore breakpoints: Utför programmet snabbt, ignorera brytpunkter Observera nu hur debuggern har märkt den instruktion som står i tur att utföras med en gul bakgrund. Klicka på Step instruction för att utföra denna instruktion. Instruktionen utförs, nästa instruktion märks med gul bakgrund. Notera effekten av instruktionen, register R0 har fått nytt värde. Fortsätt stega tills instruktionen på adress 2000000A märks med gul bakgrund. Notera hur värdet i processorns register R1 och R2 ändrats i registersektionen. Adressen till GPIOD:s indataregister finns nu i R2, adressen till GPIOE:'s utdataregister finns i R1. 13
Arbetsbok för MD407 - PROV Ställ nu in värdet 1 på strömställarmodulen genom att klicka på den minst signifikanta biten. Utför nu även nästa instruktion för att läsa ett värde från inporten: LDR R0,[R2,#0] Observera hur register R0 laddas med strömställarmodulens värde Fortsätt nu och utför nästa instruktion: STR R0,[R1,#0] Värdet i register R0 skrivs till ljusdiodrampen. Då du testat ett programs funktion genom att utföra det instruktionsvis kan du också utföra det med något Run-kommandot. Här finns det tre olika varianter, det första alternativet tillåter dig följa programexekveringen i detalj eftersom debuggern uppdaterar vyn mellan varje instruktion. Det båda andra alternativen används för att utföra programmet så snabbt som möjligt, endast programräknaren i registerfönstret uppdateras, det första alternativet, med en gul böjd pil, kontrollerar om det finns brytpunkter i programmet, det andra alternativet ignorerar brytpunkter. Brytpunkter En brytpunkt är ett ställe där programexekveringen avbryts. Brytpunkter är bara meningsfyllda då du använder Run. Du hanterar brytpunktstabellen via debuggerns verktygslist. Brytpunkten kan vara permanent och läggs då in i en brytpunktstabell. Varje gång programmet ska utföra instruktionen på denna adress kommer simulatorn att avbryta exekveringen. Brytpunkten kan också vara tillfällig, programmet exekveras tills denna adress uppträder nästa gång. Brytpunkten tas därefter automatiskt bort av simulatorn. Slutligen finns två speciella brytpunktsfunktioner: Break on IO, då denna aktiverats stoppas programexekveringen då programmet gör någon in- eller utmatning, respektive Break on IRQ, vilken gör att programexekveringen stoppas vid upptakten av avbrottshantering. Brytpunktstabellen I brytpunkstabellen kan du lägga in upp till 20 samtidiga brytpunkter i ditt program. För att bestämma vilka adresser som kan vara lämpliga för brytpunkter kan du granska exempelvis listfilen från ditt program. Brytpunkter kan sättas ut på olika sätt: identifiera instruktionen i simulatorn och högerklicka på dess rad i programfönstret: 14
Programutveckling i assemblerspråk Go to.. sätter en tillfällig brytpunkt och startar programexekveringen SetBreakpoint sätter en permanent brytpunkt här. I programfönstret märks raderna enligt följande: En aktiv brytpunkt illustreras av röd bakgrund. En inaktiv brytpunkt illustreras av blå bakgrund. Den gula bakgrunden anger vilken instruktion som kommer att utföras näst. Om den instruktion som står i tur att utföras har en brytpunkt visas denna med gul text mot svart bakgrund. Du kan också öppna dialogen för brytpunktstabellen och hantera brytpunkter härifrån: För att lägga till en ny brytpunkt skriver du in adressen i fönstret längst ned till vänster och klickar på Add, exempelvis: För att ta bort en brytpunkt klickar du först på dess nummer i brytpunktstabellen och därefter Remove. Du kan tillfälligt inaktivera en brytpunkt genom att märka av enabled-boxen. 15
Arbetsbok för MD407 - PROV Simulatorns hantering av minnet Adressrymden hos MD407 används på följande sätt. 0x0800 0000-0x080F FFFF 1 MB Flash 0x2000 0000-0x2001 BFFF 112 kb SRAM 0x4000 0000-0x5FFF FFFF 512 MB Periferikretsar- 0xE000 0000-0xFFFF FFFF 512 MB ARM systemregister Adresser utanför dessa block används ej. Under fliken Memory kan minnesinnehållet studeras och i vissa fall ändras. Minnesinnehåll visas i block om 128 bytes. Du kan ändra visningsintervallet med hjälp av rullningslisten till höger. Med Lower limit och Upper limit kan du avgränsa det minnesutrymme du vill studera. Du kan också ange en startadress för blocket som ska visas (Display base). Eftar att ha skrivit in en ny adress måste du trycka ned Entertangenten för att ändringarna ska få effekt. Du kan också välja om du vill att minnet ska visas som word (32 bitar), halfword (16 bitar) eller byte (8 bitar). Read Write Memory (RWM) Följande figur visar adressutrymmet 2000 0000 16-2000 007F 16, detta minne är av SRAMtyp, dvs. läs och skrivbart minne. Det skrivbara minnet kan ändras genom att skriva in ett nytt (hexadecimalt) värde. Ändringsbara minnesinnehåll har en vit bakgrund. Minnet kan också visas med 16-bitars format (halfword) eller 8 bitar (byte). Read Only Memory (FLASH) En annan typ är de minnesareor som är reserverade som ROM-typ (läsbart minne). För denna kategorin minne har fönstren för minnesinnehåll grå bakgrund. Innehållet kan då inte ändras. 16
Programutveckling i assemblerspråk I ett verkligt system kan sådana minnen normalt sett inte skrivas, (en speciell programmeringsrutin krävs) men i simulatorn vill vi i bland kunna initiera detta minne. Av denna anledning finns knappen Allow changes in RO-memory. Då den är aktiv behandlas ROM som skrivbart minne av simulatorn. Periferikretsar och systemregister Adressrymder med periferikretsar och systemregister visas med röd färg mot vit bakgrund. Observera att bara en mycket liten del av dessa adressrymder används. De funktioner som är implementerade i simulatorn beskrivs efter hand. Icke använt minne Den sista kategorin i adressrymden är helt enkelt odefinierade block. I simulatorn illustreras dessa med frågetecken mot grå bakgrund. 17
Arbetsbok för MD407 - PROV 18
Strukturerad assemblerprogrammering Kapitel 2 Strukturerad assemblerprogrammering Inledning En processors instruktioner kan indelas i grupper på olika sätt. I detta avsnitt har vi gjort en indelning av ARM-v6-m instruktioner som närmast associeras till hur man kan använda dom vid översättning av ett högnivåspråk till assemblerspråk. I avsnittet gör vi följande gruppindelning: Tilldelningar instruktioner för tilldelningar och typkonverteringar. Uttrycksevaluering instruktioner för aritmetik- och logik men även jämförelser och testoperationer. Programflödeskontroll instruktioner för villkorliga/ovillkorliga flödesändringar och anrop av subrutiner. Övriga instruktioner för speciella ändamål. Men vi börjar med att beskriva några användbara assemblerdirektiv. Då det gäller assemblerspråk är det viktigt att komma ihåg en textsträng är beroende av sitt sammanhang. En rad i ett assemblerprogram har följande struktur: [symbol:] Mnemonic/Assemblerdirektiv [Operand/parameter] @ kommentar där symbol kan, men måste inte förekomma Symbolen utgör en så kallad etikett (label) och identifierar en position med program eller data. Anledningen till att man definierar en symbolisk position på detta sättet är att man vill referera till positionen från något annat ställe. Symbolen ska bara förekomma i symbol- eller operand- fälten, I symbolfältet ska dessa alltid avslutas med : (kolon) Mnemonic, ett ord som om det förekommer i instruktionsfältet tolkas som en assemblerinstruktion ur processorns instruktionsuppsättning. Mot varje sådan mnemonic svarar som regel EN maskininstruktion. Assemblerdirektiv, ett direktiv till assemblatorn. Direktiv och mnemonics är inte reserverade ord i vanlig bemärkelse utan kan till exempel också användas som symbolnamn, det bör dock undvikas. Reservation av minne för variabler Statisk allokering av minnesutrymme kan göras med.space direktivet..space n{,val} reserverar n konsekutiva bytes vars initialvärden är val. Om val utelämnas sätts initialvärdet till 0. Exempel.SPACE 4 @ reservera 4 bytes, med initialt innehåll 0..SPACE 2,0xFF @ reservera två bytes med initialt innehåll 0xFF 19
Arbetsbok för MD407 - PROV Direktivet kan föregås av en etikett som ger en symbolisk adress till det reserverade utrymmet. Detta ger oss en direkt koppling mellan deklaration av permanenta variabler i högnivåspråk och assemblerspråk. För ARM Cortex-M gäller exempelvis följande storlekar för de enkla grundläggande datatyperna i programspråket C: datatyp benämning assembler antal bytes char byte 1 short halfword 2 long word 4 int word 4 Exempel: deklaration av globala variabler Följande C-deklarationer: char c; short s; long l; int i; översätts till assemblerspråk enligt följande: c:.space 1.ALIGN s:.space 2.ALIGN l:.space 4 i:.space 4 Observera hur.align-direktiv sätts in för att uppfylla rättningsvillkor för efterföljande deklarationer. För att reservera och initiera minne med ett godtyckligt värde används för formaten byte, halfword och word följande assemblerdirektiv:.byte <8-bitars värde>{,<8-bitars värde>...}.hword <16-bitars värde>{,<16-bitars värde>...}.word <32-bitars värde>{,<32-bitars värde>...} Även här kan direktiven föregås av en symbol som identifierar positionen i minnet. Exempel: deklarationer av initierade globala variabler De globala C-deklarationerna: char c = 'A'; short s = 1000; int i = 100000; resulterar i följande assemblerdirektiv: c:.byte 'A'.ALIGN s:.hword 1000.ALIGN i:.word 100000 Kodning av tilldelningar ARM är en Load/Store-arkitektur som utformats för att, framför allt, arbeta med 32-bitars data (WORD). Alla beräkningar i ALU:n görs med 32-bitar, de generella registren är 32 bitar stora. Instruktioner för att läsa från minnet (load) eller skriva till minnet (store) kan dock referera såväl WORD (32 bitar), som HALFWORD (16 bitar) och BYTE (8 bitar). Följande figur visar hur ett 32-bitars ord kan delas upp i bytes och halfwords: 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 bit +3 +2 +1 +0 byte +2 +0 halfword +0 word 20
Strukturerad assemblerprogrammering Som en restriktion hos ARM-arkitekturen gäller som tidigare sagts att WORD måste placeras på en adress jämnt delbar med 4 och dessutom gäller att HALFWORD måste börja på en jämn adress. För operationer på olika datatyper finns det bland andra följande load- och store- instruktioner: LDR Load Register Läs 32 bitar från minnet LDRH Load Register Halfword Läs 16 bitar från minnet LDRB Load Register Byte Läs 8 bitar från minnet STR Store Register Skriv 32 bitar till minnet STRH Store Register Halfword Skriv 16 bitar till minnet STRB Store Register Byte Skriv 8 bitar till minnet Exempel: Om vi har deklarationerna: char ca,cb; short sa,sb; int ia,ib; kan tilldelningar kodas på följande sätt: ca = cb; LDR R1,=cb @ adress (&cb) till R1 LDR R2,=ca @ adress (&ca) till R2 LDRB R0,[R1] @ (cb) till R0, bit 8-31 nollställs STRB R0,[R2] @ R0 till (ca) sa = sb; LDR R1,=sb @ adress (&sb) till R1 LDR R2,=sa @ adress (&sa) till R2 LDRH R0,[R1] @ (sb) till R0, bit 16-31 nollställs STRH R0,[R2] @ R0 till (sa) ia = ib; LDR R1,=ib @ adress (&ib) till R1 LDR R2,=ia @ adress (&ia) till R2 LDR R0,[R1] @ (ib) till R0 STR R0,[R2] @ R0 till (ia) alternativt: LDR R1,=ia @ adress (&ia) till R1 LDR R0,ib @ (ib) till R0 STR R0,[R1] @ R0 till (ia) man noterar följande: För vänsterledet måste vi alltid ta adressen i ett register eftersom store-instruktioner bara använder register indirekt (immediate) adresseringssätt. För högerledet kan vi spara en instruktion om det är en 32-bitars datatyp som ska läsas. Annars måste vi även här först ta adressen i ett register. Skrivsättet, där vi skriver &ca för att beteckna den symboliska adressen till ca. Vi skriver ca för att beteckna innehållet på adressen, dvs. ca:s värde. Konstanttilldelningar Då det gäller konstanter blir det mindre skillnader i kodgenereringen beroende på datatyp. För 8 bitar kan man använda en MOV-instruktion vilket ger en enkel 16-bitars kodning. För större konstanter använder vi som tidigare en PC-relativ instruktion där vi låter assemblatorn arrangera kod och data. I praktiken kan man använda samma syntax oberoende av datatyp eftsersom assemblatorn alltid kodar instruktionen på det mest fördelaktigaste sättet. 21
Arbetsbok för MD407 - PROV Uppgift 2.1 Om vi har deklarationerna: char c; short s; int i; kan tilldelningarna c = 'A'; s = 1000; i = 100000; kodas i assemblerkod: LDR R1,=c LDRB R0,= A STRB R0,[R1] LDR R1,=s LDRH R0,=1000 STRH R0,[R1] LDR R1,=i LDR R0,=100000 STR R0,[R1].ALIGN i:.space 4 s:.space 2 c:.space 1 Redigera en fil assignconst.asm med instruktionssekvensen. Assemblera och öppna därefter listfilen, assignconst.lst. Studera listfilen, komplettera följande tabell: LDR R1,=c 20000000: 4d06 ;(2000001c <c+0x2>) LDRB R0,='A' 20000002: 2041 ; 0x41 STRB R0,[R1] 20000004: 7028 LDR R1,=s 20000006: 4d06 ; (20000020 <c+0x6>) LDRH R0,=1000 20000008: 4806 ; (20000024 <c+0xa>) STRH R0,[R1] 2000000a: 8028 LDR R1,=i 2000000c: 4d06 ; (20000028 <c+0xe>) LDR R0,=100000 2000000e: 4807 ; (2000002c <c+0x12>) STR R0,[R1] 20000010: 6028 20000012: 46c0 ; (mov r8, r8) 20000014 <i>: 20000014:.word 0x00000000 20000018 <s>: 2000001a <c>: 2000001a:.short 0x0000 2000001c: 2000001a.word 0x2000001a 20000020: 20000018.word 0x20000018 20000024: 000003e8.word 0x000003e8 20000028: 20000014.word 0x20000014 2000002c: 000186a0.word 0x000186a0 Utför sedan instruktionssekvensen i simulatorn och kontrollera, i minnet, att tilldelningarna utförts korrekt. För in värdena i följande tabell 20000014 <i>: 20000018 <s>: 2000001a <c>: Slut uppgift 2.1 22
Hos MD407 har de 16 bitarna i GPIOD respektive GPIOE organiserats i 8-bitars portar, se figurerna. Här använder vi hela port E som inport och hela port D som utport. Eftersom portarna i utgångsläget båda är programmerade som inportar måste vi programmera om port D till utport och i tidigare exempel görs detta med sekvensen: LDR R0,=0x55555555 LDR R1,=0x40020C00 STR R0,[R1] Portarnas funktion är är som sagt programmerbara till flera olika funktioner men vi återkommer till dessa i avsnitt 4. Strukturerad assemblerprogrammering Uppgift 2.2 Skapa följande assemblerprogram och spara som mom2.asm @ @ mom2.asm @ start: @ initiera port D som utport LDR R0,=0x55555555 LDR R1,=0x40020C00 STR R0,[R1] @ adressen till port D:s ut-dataregister till R1 LDR R1,=0x40020C14 @ adressen till port E:s in-dataregister till R2 LDR R2,=0x40021010 main: LDRB R0,[R2] STRB R0,[R1] B main Assemblera och rätta eventuella fel. Anslut två strömställarmoduler, till GPIOE och två diodramper, till GPIOD (se även figur). Starta programmet (Run) Ställ in några olika värden hos båda inportarna och observera ljusdiodramperna. Endast bitar 0-7 ska nu kunna påverkas. Avbryt exekveringen ändra i mainslingan till LDRH respektive STRH, assemblera, ladda på nytt. Starta programmet, nu ska även bitarna 8-15 hos ljusdiodramperna påverkas. Slut uppgift 2.2 Typkonverteringar Det är fullt tillåtet att utföra tilldelningar mellan olika datatyper. Då tilldelningen sker från en större till en mindre typ stryks helt enkelt de bitar som inte får plats, ofta, men inte nödvändigtvis, med fel resultat som följd. 23
Arbetsbok för MD407 - PROV Uppgift 2.3 Undersök hur typkonvertering från större till mindre datatyp kodas. Utgå från följande deklarationer: short s; char c; Tilldelningen: c = s; kodas: LDR R1,=s LDR R2,=c LDRH R0,[R1] STRB R0,[R2] Skapa ett assemblerprogram och spara som mom3.asm. Assemblera, rätta eventuella fel och ladda till simulator, Använd listfilen och lokalisera adresser för variablerna, undersök minnesinnehållet med simulatorn, fyll i dessa: Adress Variabel Värde <s>: <c>: Placera initialvärdet 0x3E0 i minnespositionen för variabeln s. Utför instruktionssekvensen, vilket värde har c efter sekvensen? 0x Utgå nu i stället från följande deklarationer: int i; short s; Koda nu tilldelningen: s = i; Assemblera, rätta eventuella fel och ladda till simulator, Placera initialvärdet 0x3E000 i minnespositionen för variabeln i. Utför instruktionssekvensen, observera värdet för s efter sekvensen. 0x Slut uppgift 2.3 Då tilldelningen sker från en mindre till en större datatyp måste man ta speciell hänsyn till om det gäller typer utan tecken (unsigned) eller med tecken (signed). Därför finns det olika Load-instruktioner för såväl BYTE som HALFWORD. LDRB Load byte immediate En basadress bestäms genom att en konstant adderas till innehållet i ett basregister. Därefter kopieras en BYTE från denna adress, utvidgas med nollor till ett WORD och placeras i destinationsregistret. Syntax: LDRB Rt,[Rn,+/-#imm] Rt Destination, (R0-R7). Rn Basregister för adressberäkningen, (R0-R7). +/- Anger positiv eller negativ offset. Om tecken utelämnas tolkas detta som positiv offset. imm Positiv konstant 0-31. LDRB Load byte register En basadress bestäms genom att innehållet i ett indexregister adderas till innehållet i basregistret. Därefter kopieras en BYTE från denna adress, utvidgas med nollor till ett WORD och placeras i destinationsregistret. 24
Syntax: LDRB Rt,[Rn,Rm] Rt Destination, (R0-R7). Rn Basregister för adressberäkningen, (R0-R7). Rm Indexregister för adressberäkningen, (R0-R7). LDRSB Load signed byte register Strukturerad assemblerprogrammering En basadress bestäms genom att innehållet i ett indexregister adderas till innehållet i basregistret. Därefter kopieras en BYTE från denna adress, teckenutvidgas till ett WORD och placeras i destinationsregistret. Syntax: LDRSB Rt,[Rn,Rm] Rt Destination, (R0-R7). Rn Basregister för adressberäkningen, (R0-R7). Rm Indexregister för adressberäkningen, (R0-R7). På motsvarande sätt finns det olika instruktioner för HALFWORD, LDRH, dvs datatyp unsigned short int respektive LDRSH för (signed) short int. Uppgift 2.4 Undersök hur typkonvertering från mindre till större datatyp kodas. Utgå från följande deklarationer: short s; char c; Tilldelningen: s = c; kodas: LDR R1,=c LDR R2,=s LDR R3,=0 LDRSB R0,[R1,R3] STRH R0,[R2] Skapa ett assemblerprogram och spara som mom4.asm. Assemblera, rätta eventuella fel och ladda till simulator, Använd listfilen och lokalisera adresser för variablerna, undersök minnesinnehållet med simulatorn, fyll i dessa, placera initialvärdet 0x80 i minnespositionen för variabeln c. Adress Variabel Värde <s>: <c>: 0x80 Utför instruktionssekvensen, vilket värde har s efter sekvensen? Utgå nu från följande deklarationer: unsigned short s; unsigned char c; Koda nu tilldelningen: s = c; Assemblera, rätta eventuella fel och ladda till simulator, Placera på nytt initialvärdet 0x80 i minnespositionen för variabeln c. Utför instruktionssekvensen, vilket värde har s efter sekvensen? Slut uppgift 2.4 25
Arbetsbok för MD407 - PROV Instruktioner för aritmetikoperationer I instruktionsuppsättningen finns instruktioner för 32-bitars addition, subtraktion och multiplikation. ADD Addition, finns i flera former, för generell uttrycksevaluering är följande form lämplig: ADD Add Innehållen i två register adderas. Resultatet placeras i destinationsregistret. Flaggor uppdateras. Syntax: ADD Rd,Rn,Rm Rn Källregister 1 (R0-R7). Rm Källregister 2 (R0-R7). Rd Destinationsregister (R0-R7). Flaggor: N Z C V kopia av bit 31 hos resultatet Ettställs om resultatet blev 0, nollställs annars Carry från additionen Tvåkomplementspill ADC som ADD ovan men Carry-flaggan ingår som minnessiffra i operationen. SUB som ADD men subtraction. SBC som SUB men Carry-flaggan ingår som lånesiffra i operationen. MUL multiplication 32 32 bitar 32 bitar, dvs. endast de minst signifikanta 32 bitarna av resultatet sparas. NEG (RSB) Reverse subtract from 0, eller Negate som den också kallas. Används för att negera, dvs. bilda 2-komplementet av operanden. Det är rättframt att generera kod för operationerna. Exempel: 32-bitars operander Med deklarationerna int a,b,c; kodas operationen a = b + c; LDR R3,=a @ adress (&a) till R3 LDR R1,b @ (b) till R1 LDR R2,c @ (c) till R2 ADD R0,R1,R2 @ R1+R2 till R0 STR R0,[R3] @ R0 till (a) Vi får ett likartat mönster för subtraktion och multiplikation. För datatyper short och char kompliceras saken något av att vi inte har tillgång till samma adresseringssätt för att ladda 8-bitars eller 16-bitars data. Dessutom behöver vi hålla reda på om dessa är signed eller unsigned. Instruktionssekvensen blir därför något längre, vilket illustreras av följande exempel. Exempel: 16 bitars operander utan tecken Med deklaration: unsigned short a,b,c; kodas operationen a = b + c; LDR R1,=b @ adress (&b) till R1 LDRH R1,[R1] @ (b) till R1, bit 16-31 i R1 nollställs LDR R2,=c @ adress (&c) till R2 LDRH R2,[R2] @ (c) till R2, bit 16-31 i R2 nollställs ADD R0,R1,R2 @ R1+R2, dvs. (unsigned)b+(unsigned)c, till R0 LDR R3,=a @ adress (&a) till R3 STRH R0,[R3] @ R0, dvs. (unsigned short)(b+c), till a 26
Strukturerad assemblerprogrammering Instruktionsuppsättningen innehåller också speciella instruktioner avsedda att användas vid typkonvertering mellan datatyper med olika storlekar. Uppgift 2.5 Slå upp följande instruktioner i instruktionslistan och red ut vad de gör, komplettera tabellen med en kort beskrivning. Instruktion SXTH Rd,Rm SXTB Rd,Rm UXTH Rd,Rm UXTB Rd,Rm Beskriv med ord Slut uppgift 2.5 Exempel: 8-bitars operander med tecken Om vi har deklarationerna: int u; unsigned char sb,sc; och vill evaluera uttrycket: u = (signed int) (sb - sc); tvingas vi använda en instruktion för den explicita typkonverteringen enligt följande: LDR R3,=sb @ adress (&sb) till R3 LDR R2,=sc @ adress (&sc) till R2 LDRB R3,[R3] @ (sb) till R3, bit 8-31 i R3 nollställs LDRB R2,[R2] @ (sc) till R2, bit 8-31 i R2 nollställs SUB R3,R3,R2 @ R3-R2,dvs. sb-sc till R3 SXTB R3,R3 @ teckenutvidga 8 till 32 bitar LDR R2,=u @ adress (&u) till R2 STR R3,[R2] @ R3, dvs. (signed int)(sb-sc), till u Uppgift 2.6 Skapa följande assemblerprogram och spara som fil mom5.asm @ mom5.asm start: @ initiera port D som utport LDR R6,=0x55555555 LDR R5,=0x40020C00 STR R6,[R5] @ adressen till port D:s ut-dataregister till R5 LDR R5,=0x40020C14 @ adressen till port E:s in-dataregister till R6 LDR R6,=0x40021010 main: LDRB R0,[R6] @ läs PE0-E7 LDRB R1,[R6,#1] @ läs PE8-E15 ADD R0,R0,R1 STRH R0,[R5] B main Assemblera, rätta eventuella fel och ladda till simulator. För att kunna variera indata kopplar vi en 8 bitars strömställare (8 bit dipswitch) till den minst signifikanta byten av port E (GPIOE pin 0-7) och ytterligare en till den mest signifikanta byten, GPIOE pin 8-15. 27
Arbetsbok för MD407 - PROV Det är här lämpligt att använda en visningsenhet för hexadecimala siffror. Anslut därför en Double hexadecimal display till den minst signifikanta byten hos port D, GPIOD pin 0-7 och en till GPIOD pin 8-15. Låt de inställda värdena på omkopplarna representera 8-bitars tal utan tecken, dvs. unsigned char. Programmet utför addition av dessa och skriver resultatet till de dubbla displayerna. Prova med att ställa in olika värden på omkopplarna. Vilket är det största värde man kan få på displayen för bitar D8-D15? Vi tolkar nu i stället värdena som läses från omkopplarna som tal med tecken. Modifiera programmet för detta och kontrollera funktionen. Prova med att ställa in olika värden på omkopplarna. Vilka är de enda värden man kan få på displayen för bitar D8-D15? Slut uppgift 2.6 Instruktioner för logikoperationer Operationerna är avsedda enbart för att manipulara bitar och utförs på alla 32 bitar i ett register. Gruppen logikinstruktioner består av: Operation C-operator Assemblerinstruktion AND, bitvis logiskt OCH & AND OR, bitvis logiskt ELLER ORR XOR, bitvis logiskt exklusivt ELLER ^ EOR Bitclear, nollställ enskilda bitar BIC Move NOT, bilda 1-komplement ~ MVN Exempel Med deklarationerna int a,b,c; kodas operationen a = b & c; LDR R1,=a @ adress (&a) till R1 LDR R2,b @ (b) till R2 LDR R3,c @ (c) till R3 AND R0,R2,R3 @ (b)&(c) till R0 STR R0,[R1] @ R0 till (a) Uppgift 2.7 Låt A representera port E0-E7, låt B representera port E8-E15. Låt slutligen C representera port D0-D7. Använd tilldelningen C = A op B; C = ~B; 28 för AND, ORR, EOR och BIC. för MVN för att undersöka instruktionerna. Skapa följande assemblerprogram och spara filen. Assemblera, rätta eventuella fel och ladda till simulator. @ mom6.asm start: @ initiera port D som utport LDR R6,=0x55555555 LDR R5,=0x40020C00 STR R6,[R5] @ adressen till port D:s ut-dataregister till R5 LDR R5,=0x40020C14 @ adressen till port E:s in-dataregister till R6 LDR R6,=0x40021010 main: LDRB R0,[R6] @ läs PE0-E7 LDRB R1,[R6,#1] @ läs PE8-E15 op R0,R0,R1 STRB R0,[R5] B main
Strukturerad assemblerprogrammering För att kunna variera indata kopplar vi en 8 bitars strömställare (8 bit dipswitch) till den minst signifikanta byten av port E (GPIOE pin 0-7) och ytterligare en till den mest signifikanta byten, GPIOE pin 8-15. slutligen en Bargraph till GPIOD pin 0-7. Ställ in följande olika värden på omkopplaren utför programmet med Run, prova också med att utföra programmet med Run Fast. Under simulatorns programexekvering ändrar du inställningarna på omkopplaren, observera ändringarna hos ljusdiodrampen, fyll i tabellerna: Inställda värden (binärt) Resultat C A B EOR AND ORR 1111 0000 1010 0101 1010 1010 1010 1010 1100 0011 0000 0000 Inställda värden (binärt) Resultat C A B BIC MVN 1111 0000 1010 0101 1010 1010 1010 1010 1100 0011 0000 0000 Kom i håg att operationerna utförs med 32 bitar även om vi bara illustrerar med de 8 minst signifikanta bitarna här. Slut uppgift 2.7 Skift instruktioner Skiftoperationer används för att "flytta" bitar ett eller flera steg. ARM stödjer skiftoperationer med tre olika instruktioner: logiskt skift (LS) aritmetiskt skift (AS) rotation med carry (RO) Logiskt och aritmetiskt skift används speciellt under aritmetiska operationer. Vänsterskift innebär som bekant att operanden multipliceras med 2, medan högerskift innebär att operanden divideras med 2. Operation C-operator Assemblerinstruktion Skift vänster, unsigned och signed << LSL Skift höger, unsigned >> LSR Skift höger, signed >> ASR LSL logical shift left Operationen skrivs allmänt: X = Y << Z med betydelsen att Y skiftas Z steg till vänster och resultatet placeras i X. Vare sig Y eller Z påverkas av operationen. Det finns två varianter: med den första, konstant skift, används formen LSL Rd,Rs,#shift 29
Arbetsbok för MD407 - PROV Källoperanden i register Rs skiftas ett konstant antal steg shift och resultatet placeras i register Rd. Med den andra varianten, registerskift, skiftas operanden i Rd det antal steg som anges i register Rs. LSL Rd,Rs Exempel: Deklarationerna unsigned int a,b; är givna. Koda följande tilldelningssatser: a = a << 5; a = a << b; Lösning: @ a = a << 5; LDR R1,=a @ adress (&a) till R1 LDR R0,[R1] @ (a) till R0 LSL R0,#5 @ (a<<5) till R0 STR R0,[R1] @ R0, dvs. (unsigned)(a<<5), till (a) @ a = a << b; LDR R1,=a @ adress (&a) till R1 LDR R0,[R1] @ (a) till R0 LDR R2,b @ (b) till R2 LSL R0,R2 @ (a<<b) till R0 STR R0,[R1] @ R0, dvs. (unsigned)(a<<b), till (a) LSR logical shift right Operandens minst signifikanta bit kopieras till C. En nolla skiftas in i den mest signifikanta positionen. Operationen skrivs allmänt: Z = X >> Y med betydelsen att X skiftas Y steg till höger och resultatet placeras i Z. Vare sig X eller Y påverkas av operationen. Det finns två varianter även för denna instruktion: LSR Rd,Rs,#shift @ konstantskift LSR Rd,Rs @ registerskift ASR arithmetic shift right Då tal med inbyggt tecken ska skiftas används aritmetiska skift. Här är det bara högerskiftet som blir annorlunda eftersom teckenbiten kopieras vid skiftet. Precis som tidigare kan två olika former användas: ASR Rd,Rs,#<shift> ASR Rd,Rs Exempel: Deklarationerna unsigned int ua; int ia; är givna. Koda följande tilldelningssatser: ua = ua >> 5; ia = ia >> 5; Lösning: @ ua = ua >> 5; LDR R1,=ua @ adress (&ua) till R1 LDR R0,[R1] @ (ua) till R0 LSR R0,#5 @ (ua>>5) till R0 STR R0,[R1] @ R0, dvs. (unsigned)(a>>5), till (ua) 30
Strukturerad assemblerprogrammering @ ia = ia >> 5; LDR R1,=ia @ adress (&ia) till R1 LDR R0,[R1] @ (ia) till R0 ASR R0,#5 @ (ia>>5) till R0 STR R0,[R1] @ R0, dvs. (int)(ia>>5), till (ia) Uppgift 2.8 Skapa följande assemblerprogram och spara som fil mom7.asm @ @ mom7.asm @ start: @ initiera port D och E som utportar LDR R6,=0x55555555 LDR R5,=0x40020C00 STR R6,[R5] LDR R5,=0x40021000 STR R6,[R5] @ adressen till port D:s ut-dataregister till R5 LDR R5,=0x40020C14 @ adressen till port E:s ut-dataregister till R6 LDR R6,=0x40021014 main: @ R0 = R0 << R1 @ Skriv registervärden före skift till Bargraph LSR R2,R0,#16 STRH R2,[R5] STRH R0,[R6] @ Utför skift LSL R0,R1 @ Skriv registervärden efter skift till Bargraph LSR R2,R0,#16 STRH R2,[R5] STRH R0,[R6] B main Assemblera, rätta eventuella fel, ladda till simulator. Koppla 4 st Bargraph till respektive portar D15-D8, D7-D0, E15-E8 och E7-E0. Programslingan ska utföras instruktionsvis. Före första instruktionen i main-slingan lägger du manuellt in värden i R0 (värde som skiftas) och i R1 (antal skift). Prova några olika värden, observera diodrampen. Undersök instruktionerna LSR och ASR på samma sätt. Slut uppgift 2.8 (Carry) Rotation Utöver skiftinstruktionerna finns också rotation med carry som inte har någon direkt motsvarighet i form av C-operator. En inskiftad bit hämtas alltid från Carry därefter hamnar den utskiftade biten i Carry. C-flaggan kan därför ses som en mellanlagring av biten som skiftas mellan två operationer. ROR rotate right För denna instruktion finns dock bara registerskift, dvs: ROR Rd,Rs 31
Arbetsbok för MD407 - PROV Programflödeskontroll Med "programflödesändring" menar man att det gängse arbetssättet "läs från nästa adress och tolka innehållet som instruktion", avbryts, och programflödet ändras. Någon speciell instruktion som utför programflödesändringen används då. Ovillkorlig programflödesändring Vi har tidigare sett exempel på användning av: B <adress> Instruktionens operand bestämmer den adress som anger var nästa instruktion ska hämtas. Typiskt implementerar instruktionen så kallade goto-satser i C men den kommer också till användning vid flera andra typer av programkonstruktioner. Exempel: Följande C-konstruktion:... goto mylabel;... mylabel:... kodas i assemblerspråk:... B mylabel... mylabel:... Exempel: En oavbruten iteration, s.k. "oändlig slinga" kodas i C: while(1) {... } och kodas i assemblerspråk: while_label:... B while_label Subrutiner I C organiseras alla program i olika funktioner eller som vi vanligtvis, då det gäller assemblerspråk, kallar det subrutiner. En subrutin karakteriseras av att den har ett inträde (entry) och ett utträde (exit). Inträdet anges oftast genom att man placerar en symbol i symbolfältet. Utträdet, dvs åter från subrutin utgörs av en maskininstruktion som placerar returadressen i PC. Ett subrutinanrop utförs enligt... BL sub... BL-instruktionen (Branch and Link) utför två operationer: adressen till nästa instruktion (återhoppsadressen) sparas i LR programräknaren initieras med adressen till subrutinen 32
Strukturerad assemblerprogrammering Instruktionen BX register (branch extended) överför värdet från något register till PC. Om återhoppsadressen finns i LR kan därför subrutinen utformas som: sub:...... BX LR Observera att detta bara tillåter subrutinanrop i en nivå, om vi vill göra ytterligare subrutinanrop från rutinen sub, måste därför återhoppsadressen för sub, som ju finns i LR, sparas innan ytterligare subrutinarop sker. Man kan lämpligen använda stacken för detta och då göra det omedelbart i sub. För att återgå från rutinen kan man då, som sista instruktion återställa det som finns på stacktoppen, dvs. återhoppsadressen, direkt till PC. sub: PUSH {LR}...... POP {PC} Stackpekaren, register SP, måste alltså ha initierats med en lämplig adress innan PUSH eller POP används. Observera att simulatorn ger register SP initialvärdet 2001C000 16. Stacken kan också användas för att tillfälligt spara godtyckliga registerinnehåll. Detta är många gånger användbart eftersom registren är få och samtidigt behövs för många olika saker. Uppgift 2.9 Använd följande programsekvens för att studera stackhantering. Observera speciellt stackpekarens värde samt innehållet på toppen av stacken. Ställ minnesvisningen enligt följande så du kan avläsa ändringarna på stacken: Utför programmet instruktionsvis och komplettera följande tabell med värden hos register och minne. @ stack.asm PC R1 LR SP mem(sp) start: BL sub 20000000 B start 20000004 sub: PUSH {LR} 20000006 MOV R0,#0x10 20000008 PUSH {R0} 2000000A BL sub2 2000000C POP {R0} 20000010 POP {PC} 20000012 sub2: PUSH {R7} 20000014 MOV R7,SP 20000016 LDR R1,[R7,#4] 20000018 POP {R7} 2000001A BX LR 2000001C Slut uppgift 2.9 33
Arbetsbok för MD407 - PROV Observera att den minst signifikanta biten hos adressen som sparas i LR vid BL-instruktionen är 1 trots att det naturligtvis är en felaktig adress. Biten används i själva verket inte vid adressbildningen men utgör en statusbit för exekveringstillståndet. Då adressen återförs till PC, exempelvis via instruktionen BX {LR}, kommer den minst signifikanta biten i adressen att kopieras till T-biten i EPSR: 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 N Z C V Q APSR ISR_NUMBER IPSR ICI/IT T ICI/IT EPSR Biten talar alltså i själva verket om att processorn ska exekvera i THUMB-tillstånd efter BXinstruktionen. Detta innebär att, för de ARM-processorer som implementerar båda instruktionsuppsättningarna (THUMB och ARM), kan man blanda binärkod från dessa och växla processorn mellan dessa. Observera dock att processorn i MD407 endast implementerar THUMB-instruktioner och ett försök att växla till ARM-tillstånd därför orsakar exekveringsfel (vi återkommer till detta i kapitel 6). För vår del måste alltså alltid den minst signifikanta biten i en adress som placeras i PC vara 1. Villkorlig programflödesändring Instruktionstypen Branch ( gren ) används också för att ange så kallade villkorliga programflödesändringar, dvs. beroende på hur någon test har utfallit så utförs antingen den ena grenen eller den andra. Vi ska nu titta närmare på hur detta är tänkt att användas. Test- eller jämförelseinstruktion Evaluering av villkor Villkorlig BRANCHinstruktion Uppfyllt villkor? NEJ JA Villkorlig programsekvens Villkorsevaluering Villkorsevaluering kan göras explicit med speciella jämförelseinstruktioner. För jämförelse av två operander kan någon av följande instruktioner (compare) användas: Mnemonic CMP Rn,Rm CMN Rn,Rm CMP Rn,#Imm Operation Rn-Rm Rn+Rm Rn-Imm Instruktionerna påverkar flaggorna N,Z,C och V i processorns statusregister. Flaggtesterna utgår alltid från att opearnderna i registren är 32 bitar. Observera också att instruktionen CMN (compare negative) utför jämförelseoperationen Rn- (-Rm). Villkorstest 14 olika villkor kan anges i form av instruktioner som kan ändra programflödet. Assemblersyntaxen är: Bcc <symbol> 34
Strukturerad assemblerprogrammering Där cc (condition codes) står för något flaggvillkor givet i tabellen nedan och <symbol> är någon lägesangivelse i programmet. Flaggorna N,Z,V och C som används för att bilda de olika villkoren finns samlade i PSR-registret i debuggerns registersektion. PSR består i själva verket av flera olika fysiska register där samtliga bitar samlats i PSR: PSR kombinerar: 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 N Z C V Q APSR ISR_NUMBER IPSR ICI/IT T ICI/IT EPSR APSR - Application Program Status Register IPSR - Interrupt Program Status Register EPSR - Execution Program Status Register Registren kan läsas, APSR även skrivas, endast med speciella instruktioner. För tillfället räcker det med att identifiera flaggorna N,Z,C,V i APSR. Flaggorna: N: Sätts som b 31 i resultat av operation, dvs. teckenbit. Z: Sätts till 1 om resultat av operation är noll, sätts till 0 annars. C: Spillindikator vid aritmetik med tal utan tecken. Observera speciellt att C-flaggan representerar borrow efter subtraktion i jämförelseoperationer. V: Spillindikator vid aritmetik med tal med tecken. Q: Sätts av speciella instruktioner, huvudsakligen avsedda för digital signalbehandling, vi behandlar inte dessa närmre i detta material. De 14 olika instruktionerna för villkorligt programflöde är: Mnemonic Funktion Villkor Enkla flaggtest BCS/BHS Hopp om carry C=1 BCC/BLO Hopp om ICKE carry C=0 BEQ Hopp om zero Z=1 BNE Hopp om ICKE zero Z=0 BMI Hopp om negative N=1 BPL Hopp om ICKE negative N=0 BVS Hopp om overflow V=1 BVC Hopp om ICKE overflow V=0 Jämförelse av tal utan tecken BHI Villkor: Rn>Rm C! Z = 1 BHS/BCS Villkor: Rn Rm C=1 BLO/BCC Villkor: Rn < Rm C=0 BLS Villkor: Rn Rm! C + Z = 1 Jämförelse av tal med tecken BGT Villkor: Rn > Rm Z + ( N V ) = 0 BGE Villkor: Rn Rm N V = 0 BLT Villkor: Rn < Rm N V = 1 BLE Villkor: Rn Rm Z + ( N V ) = 1 Samma tabell kan uttryckas med C-operatorer och får då följande utseende: C-operator Betydelse Datatyp Instruktion == Lika signed/unsigned BEQ!= Skild från signed/unsigned BNE < Mindre än signed BLT unsigned BCC <= Mindre än eller lika signed BLE unsigned BLS > Större än signed BGT unsigned BHI >= Större än eller lika signed BGE unsigned BCS 35
Arbetsbok för MD407 - PROV Uppgift 2.10 I denna uppgift implementeras en enkel jämförelsefunktion. Använd IO-simulatorerna DIL-switch och Bargraph. Programmet ska läsa av värden från två DIL-switchar, göra en jämförelseoperation och presentera resultatet på diodrampen. Följande koppling ska användas: Programmet ska kontrollera om värdet hos Inport 1 är större än värdet hos Inport 2 och jämförelsen ska göras först för tal utan tecken, därefter modifieras programmet så att jämförelsen görs för tal med tecken. Programmet ska utformas efter följande flödesplan. Observera att portarna läses med teckenutvidgande load-byte-operationer. Flödesplanen till höger beskriver programmets funktion. En ofullständig implementering av flödesplanens program följer. Komplettera programmet. @ compare.asm start: LDR R0,=0x55555555 @ initiera port D som utport LDR STR LDR LDR R1,=0x40020C00 R0,[R1] R5,=0x40020C14 @ adressen till port D:s ut-dataregister till R5 R6,=0x40021010 @ adressen till port E:s in-dataregister till R6 main: @ läs operand 1 till R0 @ läs operand 2 till R1 @ op1 - op2 -> APSR main_1 MOV R0,#0 B main_2 main_1: MOV R0,#0xFF main_2: STRB R0,[R5] B main 36
Strukturerad assemblerprogrammering Kontrollera nu programmets funktion genom att undersöka resultatet för följande inställda värden. Markera med ett 'x' i de rader där jämförelsen utfaller med resultatet sant. Inport 1 Inport 2 Utan tecken Binärt Dec Binärt Dec 00000001 1 00000000 0 00000000 0 00000001 1 00000000 0 11111111-1 11111111-1 00000000 0 Med tecken Slut uppgift 2.10 Kodgenerering för programflödeskontroll if-satsen utgör den enklaste flödeskontrollkonstruktionen: if ( villkor ) { satser; } dvs. om villkor är sant, utförs satser, annars inte. Kodsekvensen blir: B<CondTrue> If_Do B If_End If_Do: satser If_End: Exempel: int a,b,c;... if ( a > b ) c = a; kodas: LDR R0,a @ R0 = a LDR R1,b @ R1 = b CMP R0,R1 @ (a-b)->apsr BGT If_Do B If_End If_Do: LDR R1,=c @ R1 = &c STR R0,[R1] @ c = R0 If_End:... if-else satsen utvidgar med ett alternativt val, då villkoret är falskt. if ( villkor ) { satser1; }else{ satser2; } Kodsekvensen blir därför: B<CondTrue> If_Do If_Else: satser2 B If_End If_Do: satser1 If_End:... 37
Arbetsbok för MD407 - PROV Exempel: unsigned int a,b,c; if ( a > b ) c = a; else c = b; kodas: LDR R0,a @ R0 = a LDR R1,b @ R1 = b LDR R2,=c @ R2 = &c CMP R0,R1 @ (a-b)->apsr BHI If_Do If_Else: STR R1,[R2] @ c = R1 B If_End If_Do: STR R0,[R2] @ c = R0 If_End:... while- satsen while( villkor ) { satser } skapar en iterativ slinga som upprepas så länge villkoret är sant. I en sådan slinga används också instruktioner för ovillkorlig flödesändring. C-satsen continue medför att programexekveringen upptas direkt före villkorstesten och motsvaras av assemblerkoden: B While_Continue medan break medför att iterationen avslutas och programexekveringen fortsätter omedelbart efter slingan, dvs. assemblerkoden: B While_Break Vid kodning av while -iteration används oftast det komplementära villkoret (CondFalse) eftersom man då kan spara en instruktion. Kodsekvensen för whileslingan blir då: While_Continue: B<CondFalse> While_Break While_Do: satser B While_Continue While_Break:... Exempel unsigned int a,b,c; while ( a >= 20 ) {... } kodas (observera val av komplementärt villkor) While_Continue: LDR R0,a CMP R0,#20 BHI While_Break While_Do:... B While_Continue While_Break: 38
Strukturerad assemblerprogrammering Exempel: Genom att sätta ett villkor som alltid är sant kan man skapa en oupphörlig programslinga. while( 1 ) {... } kodas: While_Continue: While_Do:... B While_Continue While_Break: Men man kan också komplettera med något villkor för att avbryta programslingan med användning av break, till exempel: int flag; while( 1 ) {.... if( flag ) break;.... } kodas: While_Continue: While_Do:.... LDR R0,flag CMP R0,#0 BNE While_Break.... B While_Continue While_Break: do-while satsen är en variant, inte alls lika vanlig som while, men likafullt lämplig för vissa speciella programkonstruktioner. Här utförs alltid programslingan minst en gång innan villkorstest utförs. do{ satser } while( villkor ); Kodsekvensen för do-while-slingan blir: Do_While: satser Do_Continue:... B<CondTrue> Do_While Do_Break: for-satsen används ofta för att skapa bundna programslingor. for( uttryck1; uttryck2; uttryck3 ) { satser } Konstruktionen är egentligen ett förenklat skrivsätt för att skapa en komplett slinga med en initiering (uttryck1), evaluering av fortsättningsvillkor (uttryck2). Efter satser kan man slutligen placera ytterligare ett uttryck som då alltså utförs en gång för varje iteration av programslingan. 39
Arbetsbok för MD407 - PROV Exempel: int i; for (i = 0; i < 100; i = i+1 ) { satser (här kan även break och continue användas) } kan kodas: @ uttryck1 LDR R1,=i LDR R0,=0 STR R0,[R1] @ i = 0 For_Continue: @ uttryck2 LDR R0,i CMP R0,#100 @ (i-100)->apsr BGE For_Break @ komplementärt villkor For_Do: satser @uttryck3 LDR R1,=i LDR R0,[R1] ADD R0,R0,#1 STR R0,[R1] @ i = i+1 B For_Continue Funktionsparametrar och returvärden Kompilatorkonventioner För att underlätta för kompilatortillverkare och göra det möjligt att skapa portabel assemblerkod har ARM specificerat konventioner för hur register används, hur parametrar och returvärden överförs mellan C-funktioner. Detta kallas Arm Procedure Call Standard, (APCS). Standarden är omfattande men speciellt intressant är följande sammanfattning av hur registren bör användas. Register Användning R15 (PC) Programräknare R14 (LR) Länkregister R13 (SP) Stackpekare R12 (IP) R11 Dessa register är avsedda för variabler och som temporära register. R10 Om dom används måste dom sparas och återställas av R9 den anropade (callee) funktionen R8 R7 Speciellt använder GCC R7 som pekare till aktiveringspost (stack frame) R6 Också dessa register är avsedda för variabler och temporärbruk R5 Om dom används måste dom sparas och återställas av R4 den anropade (callee) funktionen R3 parameter 4 / temporärregister Dessa register sparas normalt sett R2 parameter 3 / temporärregister inte över funktionsanrop men om, R1 parameter 2 / resultat 2 /temporärregister så är det den anropande (caller) R0 parameter 1 / resultat 1/ temporärregister funktionens uppgift 40
Strukturerad assemblerprogrammering Returvärden För funktioner som returnerar datatyp char, short eller int används register R0 för returvärdet. Exempel Funktionen: char rcval( void ) { return 0xFF; } kodas: rcval: MOV BX R0,#0xFF LR Exempel Funktionen: int rival( void ) { return 0xFF; } kodas: rival: MOV BX R0,#0xFF LR För funktioner som returnerar datatyp long long (64 bitar) används dessutom R1 för de mest signifikanta 32 bitarna. Exempel Funktionen: long long rval( void ) { return 0x1234567800000000; } kodas: rval: MOV LDR BX R0,#0 R1,=0x12345678 LR Parametrar Upp till 4 st parametrar med datatyp char, short eller int, skickas i register R0-R3. Registertilldelningen sker från vänster till höger i parameterlistan, informellt uttryckt: f( R0,R1,R2,R3); Exempel: En funktion max kodas i C enligt följande: int max( int a, int b) { if( a >= b ) return a; else return b; } Visa hur funktionen kan kodas i assemblerspråk med tillämpning av APCS. @ int max( int a, int b) @ a=r0, b=r1 max: @{ @ if( a >= b ) @ return a; CMP R0,R1 BGE max_1 @ else @ return b; MOV R0,R1 max_1: @} BX LR Vi har sett exempel på hur parametrar skickas som värdeanrop (call by value). För parametrar och returvärden vars storlek överstiger de tillgängliga registren som exempelvis fält och sammansatta typer, används i stället referensanrop (call by reference), där man skickar en pekare till data som ska överföras. Detta återkommer vi till senare. 41
Arbetsbok för MD407 - PROV Registerspill Av prestandaskäl vill man alltså använda register i så stor utsträckning som möjligt men det finns naturligtvis alltid en gräns för detta. Då fler parametrar än fyra krävs, används precis som tidigare registren R0-R3 för de fyra första parametrarna, därefter används stacken som komplement. Kodningen av funktioner kan också vara sådan att något av registren R0-R3 måste återanvändas. I sådana fall skapar man helt enkelt utrymme på stacken, där man sparar parametrarna, detta kallas då registerspill. Vi behandlar dock inte detta här. Lokala variabler Lokala variabler har en begränsad synlighet inom det block där de deklareras. Det innebär att det oftast är onödigt slöseri att tilldela dom ett statiskt minnesutrymme. Betydligt bättre är att i första hand försöka använda register. Exempel: Variabeldeklarationerna c och i är lokala för funktionen f, eftersom de placerats inom funktionsblocket (klamrar). De är synliga från den punkt de deklareras och framåt till blockets slut. void f( void ) { char c; int i; { /* inre block */ int j; /* här syns c,i och j */ } /* här syns c och i */... } Om antalet lokala variabler är få kan man med fördel reservera register för dessa. Oavsett datatyp (char, short eller int) åtgår ett register per variabel. Exempel Vi har en funktion int bitnum_mask( int bitno ) som tar en parameter och returnerar en bitmask baserat på parameterns värde. Om parametern är 0 ska bit 0 sättas till 1 Om parametern är 1 ska bit 1 sättas till 1, osv. Funktionen har implementerats enligt följande: int bitnum_mask( int bitno) { int mask = 1; if( bitno > 31) return 0; while( bitno ) { mask = mask << 1; bitno = bitno - 1; } return mask; } Vi vill översätta funktionen till assemblerspråk: Vi har: R0=bitno och gör registerallokeringen: R1=mask R3=temporärregister bitnum_mask: @int bitnum_mask( int bitno) @ { @ int mask = 1; MOV R1, #1 @ if( bitno > 31) return 0; MOV R3,#0 CMP R0,#31 BGT.L2 @ while( bitno ).L1: CMP R0,#0 BEQ.L3 @ { @ mask = mask << 1; LSL R1,R1,#1 @ bitno = bitno - 1; SUB R0,R0,#1 B.L1 @ }.L2: MOV R1,#0 @ return mask;.l3: MOV R0,R1 @} BX LR 42
Strukturerad assemblerprogrammering Även register R4,R5,R6 och R7 kan komma till användning för lokala variabler, tänk dock på att de ska sparas och återställas av den anropade funktionen. Exempel: Gör en lämplig registerallokering för funktionen: void f (int a, int b, int c ) { int d: int e; } Vi ser att R0,R1 och R2 redan är upptagna av parametrar, R3 upplåter vi för uttrycksevaluering i funktionen och gör registerallokeringen: d=r4, e=r5. Vi får då följande kodning: @void f (int a, int b, int c ) f: @{ @ int d: @ int e; PUSH {R4,R5,LR} POP {R4,R5,PC} @} Kodgenerering för fält I C kan man skapa fält av en tidigare definierad datatyp. Syntaxen för en deklaration av ett fält är: typ identifierare [ antal element ]; Exempel: fält med datatyper char, short och int: Globala C-deklarationer: char ca[10]; short int sa[10]; int ia[10]; resulterar i följande assemblerdirektiv: ca:.space 10 sa:.space 20 ia:.space 40 medan C-deklarationerna: char cia[]={'b','c','d','e'}; short int sia[]={1001,1002,1003,1004}; int iia[]={100001,100002,100003,100004}; resulterar i: cia:.byte 'B','C','D','E'.ALIGN sia:.hword 1001,1002,1003,1004.ALIGN iia:.word 100001,100002,100003,100004 Fältets element lagras efter varandra i minnet, för ett fält av char:s har vi: char charvec[n]; sizeof (charvec) = N bytes Figuren till höger illustrerar hur N element lagras i ett minne med 32-bitars organisation. Observera hur elementen indexeras från 0 och uppåt. Adressen till element i ( i<n) i fältet bestäms därför av: charvec + i; Vid referens av ett element i ett fält tar man alltså först adressen till fältets första element (basadressen), dvs. &identifierare[0]; därefter bestämmer man index för det element man vill referera och bestämmer förskjutningen. Beroende på om detta är ett konstant index eller ett evaluerat index, väljer man sedan adresseringssätt. 43
Arbetsbok för MD407 - PROV Exempel Vi har en konstant k, i intervallet 0..N-1 och deklarationen: unsigned char charvec[n]; Evaluera följande uttryck i register R0. charvec[k]; Detta kan göras på flera olika sätt: LDR R0,=charvec @ R0 = &charvec[0] ADD R0,R0,#k @ R0 = &charvec[0] + k LDRB R0,[R0] @ R0 = charvec[k] men vi kan också utnyttja en konstant offset i den sista LDRB och utesluta additionen: LDR R0,=charvec @ R0 = &charvec[0] LDRB R0,[R0,#k] @ R0 = charvec[k] Exempel Vi har deklarationerna: int i; unsigned char charvec[n]; Evaluera följande uttryck i register R0. charvec[i]; Som vanligt kan vi välja mellan olika sätt: LDR R0,=charvec @ R0 = &charvec[0] LDR R1,i @ R1 = i ADD R0,R0,R1 @ R0 = &charvec[0] + i LDRB R0,[R0] @ R0 = charvec[i] men vi kan också utnyttja en annan form av LDRB som tillåter offset i register: LDR R0,=charvec @ R0 = &charvec[0] LDR R1,i @ R1 = i LDRB R0,[R0,R1] @ R0 = charvec[i] Sju-sifferindikator Sju-sifferindikatorn är en vanlig presentationsenhet i digitala system. Med en sådan kan man på ett enkelt och billigt sätt presentera tecken som kan hänföras till de välbekanta siffrorna 0-9. Namnet sju-segment kommer av att det faktiskt går att representera dessa siffror, om än något kantigt, med endast sju olika streck, kallas också segment. Man införde också ett åttonde segment vars uppgift är att tända en decimalpunkt. Under detta moment ska ett assemblerprogram konstrueras som utför översättning och utmatning av de decimala siffrorna 0 t.o.m 9, samt de hexadecimala siffrorna A-F, till motsvarande representationer på sju-sifferindikatorn. Funktionsbeskrivning Översättningen från en siffra (0-9, A-F) till motsvarande sju-segmentskod beror naturligtvis helt och hållet på vilken typ av sju-sifferindikator man använder. I detta fall ska du först studera följande exempel på översättningen av den decimala siffran 2 till dess motsvarande sju-sifferkod. Vår 7-Segment display fungerar enligt följande: Varje bit, i det dataord (8 bitar) som skrivs till utporten, motsvarar ett segment på sjusifferindikatorn. En etta tänder ett segment, en nolla släcker segmentet. 44
Strukturerad assemblerprogrammering Exempel: För att representera siffran 2 måste vi tända de segment som (tillsammans) ger det mest 2-lika utseendet, i detta fallet segmenten 0, 1,3,4 och 6. En etta tänder ett segment, en nolla släcker segmentet. I exemplet använder vi därför 5B 16 detta bitmönster formar siffran två på sifferindikatorn. Observera speciellt att det krävs 7 bitar för en sjusifferrepresentation, medan en hexadecimal siffra representeras med 4 bitar. Vi måste därför översätta varje hexadecimal siffra 0..F till dess 7-segmentkod för att kunna visa denna på indikatorn. Uppgift 2.11 Följande tabell illustrerar förhållandet mellan binära koder och sjusegmentskod. Studera speciellt föregående exempel och komplettera i tabellen med med de saknade sjusegmentskoderna. Siffra Sju-segmentskod Binär kod Binär form Hexadecimal form 0 0000 0011 1111 3F 1 0001 2 0010 0101 1011 5B 3 0011 4 0100 5 0101 6 0110 7 0111 8 1000 9 1001 A 1010 B 1011 C 1100 D 1101 E 1110 F 1111 Ge en C-deklaration SegCodes av ett initierat fält som rymmer samtliga segmentskoder: översätt deklarationen till assemblerkod: 45
Arbetsbok för MD407 - PROV Fortsätt nu med att implementera ett program binto7seg, se flödesdiagram, som kontinuerligt läser av värden från en DIL-switch, kontrollerar att värdet finns i ett tillåtet intervall, dvs. 0-15, och presenterar resultatet på en 7-Segment display. Redigera en källtext, binto7seg.asm med programmet och spara denna. Assemblera och rätta eventuella fel. Anslut en strömställarmodul DIL-switch, till GPIOE (0-7) och en 7-Segment display, till GPIOD (0-7). Kontrollera funktionen, avhjälp eventuella fel. Slut uppgift 2.11 Om elementets storlek är större än 1, måste indexet justeras vid adressberäkningen. short shortvec[n]; sizeof(shortvec)= N*2 bytes I figuren till höger visas hur varje element av typ short upptar 2 bytes i minnet. Adressen till element i ( i<n) i fältet bestäms nu av: shortvec + i*2; Exempel Vi har en konstant k, i intervallet 0..N-1 och deklarationen: short shortvec[n]; Evaluera följande uttryck i register R0. shortvec[k]; En rättfram kodning blir då: LDR R0,=shortvec @ R0 = &shortvec[0] LDRH R0,[R0,#2*k] @ R0 = (unsigned) shortvec [k] SXTH R0,R0 @ R0 = (signed) shortvec [k] Då ett variabelt index ingår i uttrycket måste även detta multipliceras med 2, men det är det samma som ett logiskt vänsterskift. Exempel Vi har deklarationerna: int i; short shortvec[n]; Evaluera följande uttryck i register R0. shortvec[i]; En rättfram kodning blir då: LDR R0,=shortvec @ R0 = &shortvec[0] LDR R1,i @ R1 = i LSL R1,R1,#1 @ R1 = 2*i LDRSH R0,[R0,R1] @ R0 = shortvec[i] 46
Strukturerad assemblerprogrammering int intvec[n]; sizeof (intvec) = N*4 bytes Eftersom typen int representeras med 4 bytes ges adressen till element i ( i<n) i fältet bestäms nu av: intvec + i*4; Exempel Vi har en konstant k, i intervallet 0..N-1 och deklarationen: int intvec[n]; Evaluera följande uttryck i register R0. intvec [k]; En rättfram kodning blir då: LDR R0,=intvec @ R0 = &intvec[0] LDR R0,[R0,#4*k] @ R0 = intvec[k] Exempel Vi har deklarationerna: int i; int intvec [N]; Evaluera följande uttryck i register R0. intvec [i]; En rättfram kodning blir då: LDR R0,=intvec @ R0 = &intvec[0] LDR R1,i @ R1 = i LSL R1,R1,#2 @ R1 = i*4 LDR R0,[R0,R1] @ R0 = intvec[i] Uppgift 2.12 Vi har C-deklarationen: unsiged char ca[10]; Betrakta följande sekvens: for (int i = 0; i < sizeof( ca ); i = i+1 ) ca[i] = i; ca[5] = 0xFF; Sekvensen kan kodas i assembler enligt följande, ofullständiga kodsekvens. Komplettera assemblerkoden med det saknade operandfältet. Följande registerallokering ska användas: @ Registerallokering: @ R0 = i @ R1 = basadress ca @ R2 = temporärregister 47
Arbetsbok för MD407 - PROV start: MOV @ i = 0 LDR @ R1 = &ca[0] for: CMP @ i < sizeof(ca)? BGE STRB @ ca[i] = i ADD @ i = i+1 B forend: MOV MOV STRB @ ca[5] = 0xFF; ca:.space Skapa en källtextfil fillcharvec.asm, redigera och assemblera filen, rätta eventuella fel. Öppna listfilen, fillcharvec.lst, och kontrollera att basadressen för fältet ca är 0x20000014. Ladda programmet till simulatorn. Växla till minnesfönstret, ställ Display size på 8 bit och kontrollera att, hela fältet, dvs. ca[0] t.o.m ca[9] är noll. Växla till programfönstret, exekvera nu hela programslingan och kontrollera därefter att fältet nu fått nya värden enligt följande: Om ditt resultat inte stämmer med ovanstående bör du gå tillbaks, kontrollera hur du kodat operanderna, rätta fel och göra om proceduren. Slut uppgift 2.12 48
Fält som parameter till funktion Strukturerad assemblerprogrammering Då ett helt fält ska göras tillgängligt i en funktion skickas fältets basadress i ett register (referensanrop) snarare än hela fältet (värdeanrop). Exempel: Vi har följande deklarationer: int intvec[8]; short shortvec[] = {1,2,3,4,-1,-2,-3,-4}; En funktion, copyvec, som kopierar ett fält (känd längd) med short till ett fält med int kan deklareras: void copyvec( int dest[], short src[], int size); Observera hakparenteserna, som anger att detta är ett referensanrop, dvs. basadresser för fält. Funktionsanropet utförs i detta fall enligt: copyvec( intvec, shortvec, sizeof (shortvec)); Uppgift 2.13 Funktionen copyvec, ska kodas i assemblerspråk. C-definitionen av funktionen är: void copyvec( int dest[], short src[], int size) { int i; for( i = 0; i < size; i = i+1) { dest[i] = src[i]; } } Kompilatorkonventioner för registeranvändning ska följas. @ Registerallokering: @ R0 = parameter, basadress till 'dest' @ R1 = parameter, basadress till 'src' @ R2 = parameter, antal element i fälten 'size' @ R3 = lokal variabel 'i' @ R4 = temporrärregister för data @ R5 = temporrärregister för indexberäkning copyvec: PUSH @ MOV @ i = 0 copyvec1: CMP @ i < size? BGE LSL @ R5 = i*2 LDRSH @ R4 = src[i] SXTH @ teckenutvidga LSL @ R5 = i*4 STR @ dest[i] = R4 ADD @ i = i+1; B @ copyvec2: POP BX LR 49
Arbetsbok för MD407 - PROV Utforma ditt testprogram på följande sätt: start: LDR R0,=intvec LDR R1,=shortvec MOV R2,#8 BL copyvec B start copyvec:....align @ Nästa direktiv är bara en utfyllnad för att data ska hamna på adress 0x20000030. @ Det blir då lättare att kontrollera resultatet av funktionen.space 8 intvec:.space 32 shortvec:.hword 1,2,3,4,-1,-2,-3,-4 Implementera din lösning i en fil copyvec.asm. Assemblera och rätta eventuella syntaxfel. Öppna listfilen, copyvec.lst, och kontrollera att basadressen för fältet intvec är 0x20000030. Ladda programmet till simulatorn. Växla till minnesfönstret, ställ Display size på 16 bit och observera det initierade fältet shortvec, dvs. shortvec[0] t.o.m shortvec[7] med basadress 0x20000050. Växla till minnesfönstret, ställ Display size på 32 bit för visning av typen int. Kontrollera att, hela fältet intvec, med basadress 0x20000030, dvs. intvec[0] t.o.m intvec[7] är noll. Växla till programfönstret, exekvera nu hela programslingan och kontrollera därefter att fältet nu fått nya värden enligt följande: Om ditt resultat inte stämmer med ovanstående bör du gå tillbaks, kontrollera hur du kodat operanderna, rätta fel och göra om proceduren. Slut uppgift 2.13 50
Strukturerad assemblerprogrammering Adressberäkning för flerdimensionella fält Fält kan ha godtyckligt antal dimensioner där varje ny dimension anges med en hakparentes. Den generella formen för att deklarera ett flerdimensionellt fält är: typ id[dim1][dim2]...[dimn]; där dimx anger storleken av respektive dimension. Fältets totala storlek får man då man multiplicerar samman samtliga dimensioners storlekar. Adressberäkningen till ett enskilt element med index i1, i2... in i fältet blir därför: basadress + i1*(dim2+...+dimn) + i2(...+dimn) +...+ in; Exempel: Vi har deklarationerna: char mc[n][m]; int i1,i2; Adressen till ett element (i1,i2) blir då: mc + (i1*m)+i2 Exempel: Vi har deklarationerna: short ms[n][m]; int i1,i2; Adressen till ett element (i1,i2) blir då: ms + ((i1*m)+i2)*2 eftersom varje element är 2 bytes. Exempel: Vi har deklarationerna: int mi[n][m]; int i1,i2; Adressen till ett element (i1,i2) blir då: mi + ((i1*m)+i2)*4 eftersom varje element är 4 bytes. 51
Arbetsbok för MD407 - PROV Exempel: Vi har de globala deklarationerna: int i,j; int mi[4][5]; Visa en kodsekvens som evaluerar uttrycket mi[i][j] till register R0. Lösning: Adressen dvs. &mi[i][j] blir: mi + ((i*5)+j)*4 Kodsekvensen kan då skrivas: LDR R0,i @ R0 = i MOV R1,#6 @ R1 = 5 MUL R0,R1 @ R0 = i*5 LDR R1,j @ R1 = j ADD R0,R0,R1 @ R0 = (i*5)+j LSL R0,R0,#2 @ R0 = ((i*5)+j)*4 LDR R1,=mi @ R1 = &mi[0] LDR R0,[R1,R0] @ R0 = mi[i][j] I exemplet såg vi hur en multiplikationsinstruktion används för att bestämma förskjutningen till elementet i matrisen. Eftersom denna operation alltid är en multiplikation med en konstant kan man korta ned exekveringstiden väsentligt genom att ersätta MUL-instruktionen med en skift/add kombination, dvs. xk = x(2 y +k), där y,k väljs så att 0 k < (K-2 y ) Exempel: Skriv multiplikationen i*5 som en skift/add-kombination och visa en kodsekvens som evaluerar kombinationen värdet i register R0. Lösning: i*5 = i*(2 2 +1) = (i*2 2 + i*1) Kodsekvensen blir: LDR R0,i @ R0 = i LSL R1,R0,#2 @ R1 = 4*i ADD R0,R1,R0 @ R0 = 4*i + i Uppgift 2.14 Vi har följande globala deklarationer: int i; int j; char m3[3][3]={ {1,2,3},{4,5,6},{7,8,9}}; char ch; I denna uppgift ska du visa hur följande tilldelningssats kodas: ch = m3[i][j]; Antag i=2 och j=1, Vilket värde ska ch ha efter tilldelningssatsen: Utgå från kommentarerna oh visa en kodsekvens som utför tilldelningen: 52
Strukturerad assemblerprogrammering.align i:.space j:.space m3:.byte ch:.space @ R0 = i @ R1 = i*2 @ R0 = i*(2+1) @ R1 = j @ R0 = (i*3)+j @ R1 = &m3[0] @ R0 = m3[i][j] @ R1 = &ch @ ch = R0 Redigera en källtextfil, readmatrix.asm, assemblera och rätta eventuella syntaxfel. Öppna listfilen readmatrix.lst, lokalisera adresser för variablerna i och j och ch. variabel i j ch adress Använd minnesfönstret för att ge i och j initiala värden. Utför programmet instruktionsvis, kontrollera att ch får rätt värde. Slut uppgift 2.14 Sammansatta typer (struct och union) Flera komponenter "medlemmar", eventuellt med olika datatyper, kan grupperas tillsammans för att bilda en ny användardefinierad datatyp med hjälp av en structkonstruktion. Det finns olika sätt att deklarera structer, man kan välja att samtidigt ge structen ett unikt typnamn, men detta kan också utelämnas. 53
Arbetsbok för MD407 - PROV Exempel: Deklaration av struct utan typ struct { int x; int y; int z; }coord; skapar en instans av den sammansatta variabeln coord, de ingående komponenterna refereras med punktnotation: coord.x coord.y Av figuren till höger framgår hur coord lagras i minnet. De ingående medlemmarna läggs normalt ut i samma ordning som de deklareras i structen och man kan därför enkelt bestämma förskjutning till de individuella medlemmarna. Genom att skapa en lista av identifierare kan vi skapa flera olika instanser av structen i deklarationen. Exempel: Otypad struct med 3 instanser struct { int x; int y; int z; }coord1, coord2, coord3; Ofta vill man hålla i sär typdeklarationen för att enkelt skapa nya nya instanser bara genom att ange typen. Structen kan då förses med ett namn. Exempel: Namngiven struct Genom att namnge en struct vid deklarationen har vi skapat en typ med benämningen struct structnamn. Vi kan sedan använda structens namn vid deklaration av variabler med denna typ. struct coord{ int x; int y; int z; }; struct coord coord1, coord2, coord3; Med denna metod ingår alltid nyckelordet struct i typnamnet. Man kan i stället välja att göra en typdefinition: typedef struct coord{ int x; int y; int z; } COORD; Deklarationen COORD coord1; är nu samma sak som deklarationen struct cord1; Stlutligen kan man välja att utelämna structens namn i typdefinitionen, och enbart använda det definierade typnamnet: typedef struct{ int x; int y; int z; } COORD; 54
Exempel: struct-tilldelning Hela structen kan tilldelas med en enda sats. Man måste som vanligt se till att tilldelningen är typriktig. Vi har deklarationerna: typedef struct coord{ int x; int y; int z; } COORD; COORD start,end; En kodsekvens för tilldelningssatsen: start = (COORD) { 1,2,3 }; Strukturerad assemblerprogrammering En kodsekvens för tilldelningssatsen: end = start; kan utformas som: LDR R0,=start MOV R1,#1 STR R1,[R0] MOV R1,#2 STR R1,[R0,#4] MOV R1,#3 STR R1,[R0,#8] kan utformas som: LDR R0,=start LDR R1,=end LDR R2,[R0] STR R2,[R1] LDR R2,[R0,#4] STR R2,[R1,#4] LDR R2,[R0,#8] STR R2,[R1,#8] Eftersom structen kan bestå av element med olika storlek måste man tänka på eventuella rättningsvillkor, betrakta följande exempel: Exempel: Vi vill kapsla in en variabel av typen int och en annan variabel av typen char i en struct x. Det finns två olika sätt att göra detta, det första sättet: struct x{ int a; char b; }; Låt oss anta att structens basadress är korrekt rättad, vi har då att sizeof (struct x) är 5, med minneslagring enligt figuren till höger. Anta nu att vi i stället vill deklarera structen på det andra sättet: struct x{ char b; int a; }; Om vi inte tillämpar rättningsvillkor i structen ser vi att minneslagringen blir som i figuren till höger. Där ser vi också att variabeln a bryter en ordgräns eftersom den nu är lagrad på udda adress. Det blir nu mycket komplicerat att ladda en int x.a till ett register dvs. LDR Rd,[Rs,#k] eftersom k måste vara jämnt delbar med 4 och basadressen Rs är korrekt rättad enligt förutsättningarna. Lösningen blir att tillämpa lämpligt rättningsvillkor på varje variabel i structen så att den senare deklarationen i stället resulterar i minneslagring enligt figuren till höger. Detta innebär samtidigt att structens storlek ökar till 8. 55
Arbetsbok för MD407 - PROV För att bestämma adressen till structens medlemmar bestämmer man först förskjutningarna relativt structens första element. Det första elementet har ingen förskjutning i förhållande till struktens basadress i minnet. Genom att succesivt addera storlekarna av de ingående medlemmarna bestäms förskjutningen till varje individuell medlem. Exempel: Vi har deklarationen: struct { int a; char b; short c; COORD d; } st; Visa kodsekvenser som evaluerar följande uttryck i register R0. a) st.c b) st.d.y Lösning: Vi kan teckna uttryck för de ingående förskjutningarna (offset), observera att eventuella rättningsvillkor respekteras:.align 2 offset_a = 0 offset_b = offset_a + sizeof(a) = 0+4=4 offset_c = offset_b + sizeof(b) + align(1) = 4+1+1=6 offset_d_x= offset_c + sizeof(c) + align(2) = 6+2=8 offset_d_y= offset_d_x+ sizeof(d.x) + align(2) =8+4=12 offset_d_z= offset_d_x+ sizeof(d.x) + align(2) =12+4=16 Förskjutningarna till respektive element visas också i figuren till höger, som är ett alternativt (snabbare) sätt att beskriva datastrukturen. Kodsekvenserna blir då: a) LDR R0,=st LDRH R0,[R0,#6] @ R0= &st + 6 (offset_c) b) LDR R0,=st LDR R0,[R0,#12] @ R0= &st + 12 (offset_d_y) 56
Strukturerad assemblerprogrammering Pekare Vi har hittills behandlat datatyper avsedda för att hantera i första hand alfanumeriska tecken (char) och heltal (short och int). Vi har sett hur man upplåter en plats (allokerar minnesutrymme) för variabler med sådana typer. Utvecklingssystemet har då tagit hand om var i minnet dessa variabler hamnar, vi refererar bara till variablerna med deras symboliska namn (identifierare). Detta är tillräckligt i många fall men vi har också situationer där vi, som programmerare, explicit vill hantera minnesutrymmen och behöver då något instrument för detta: en pekare är en datatyp för hantering av minnesadresser. Exempel: Primärminnet kan ses som ett enda stort fält med variabler av typen char. En pekare kan användas för att lagra adressen till ett specifikt element i fältet. Det tillgängliga adressutrymmets storlek är givet av programräknaren PC, dvs 32 bitar. Pekartypens storlek är därför också 32 bitar. Vi säger också att pekaren refererar fältets element. Alla pekartyper är lika stora, dvs. 32 bitar, men för typkontroll finns det också en pekartyp för varje typ i C. Asterix (*) används vid deklaration ev en pekartyp: grundtyp * identifierare; Exempel: char *cp; /* 'cp' är en pekare till char */ short *sp; /* 'sp' är en pekare till short */ int *ip; /* 'ip' är en pekare till int */ Tilldelningar Tilldelningar mellan olika pekare följer samma mönster som vi tidigare sett exempel på, minns dock att storleken alltid är 32 bitar. Exempel: Med de globala deklarationerna: char *cp1, *cp2; kodas tilldelningen: cp1 = cp2; @ cp1 = cp2; LDR R0,cp2 LDR R1,=cp1 STR R0,[R1] Tilldelningar mellan olika pekartyper är tillåten men kan resultera i en varning om grundtyperna är olika. Exempel char *cp; short *sp;... cp = sp; sp = cp; är fullt tillåtet. 57
Arbetsbok för MD407 - PROV Adressoperator och indirektion(dereferens) Med adressen till en pekare menar vi minnesadressen till pekarens plats i minnet. Minnesadressens innehåll, dvs. pekaren innehåller i sin tur en minnesadress till en minnesposition. För att ange en variabels minnesadress, oavsett om detta är en grundtyp eller en pekartyp, användes operatorn &. Exempel: Vi har de globala deklarationerna: int i, *ip; Tilldelningen: ip = &i; kodas då LDR R0,=i LDR R1,=ip STR R0,[R1] Det omvända, då man vill ha minnesinnehållet från en adress kallas indirektion, eller dereferensiera. Operatorn för detta är återigen en asterix (*) som placeras före pekarvariabeln vars innehåll man avser. Exempel: Vi har de globala deklarationerna: int i,j, *ip; Efter tilldelningen ip = &i; har i och *ip samma värde. Tilldelningen j = i; kodas som bekant: LDR R0,i LDR R1,=j STR R0,[R1] medan tilldelningen j = *ip; kodas: LDR R0,ip LDR R0,[R0] LDR R1,=j STR R0,[R1] 58
Exempel Vi har deklarationerna: int i, *ip; Med tilldelningen: ip = &i; har vi placerat i:s adress i variabeln ip. Antag nu att i och ip hamnat i minnet enligt figuren. Här gäller då att: &ip == 0x20000004, ip == 0x20000020 == &i det gäller samtidigt att: i == *ip ==? Vi ser också att operationen i = (int) value; är det samma som *ip = (int) value; 59
Appendix F: Vektortabell Konstantkonvertering För att kunna läsa från, eller skriva till, en fast minnesadress, exempelvis en portadress i datorns minne använder man pekare. Pekaren måste då först tilldelas portadressen, eftersom portadressen är en konstant (med default typ int) måste man explicit typkonvertera denna till en pekartyp. Exempel: portadressering Vi vill kunna läsa/skriva ut-data registret hos GPIO D0-D7, dvs. adress 0x40020C14. Vi deklarerar en pekare: char *p; Tilldelningen: p = 0x40020C14; /* FEL */ är inte tillåten eftersom kompilatorn kontrollerar typerna i vänstra respektive högra ledet. Till vänster har vi en pekartyp och till höger har vi en heltalstyp. För att göra tilldelningen typriktig gör vi i stället en typkonvertering: p = (char *) 0x40020C14; Om vi har deklarationerna: char *p, c; innebär tilldelningarna: p = (char *) 0x40020C14; c = *p; att portens värde tilldelas variabeln c. Men man måste inte använda en pekarvariabel i detta fall, här fungerar det lika bra med: c = * ((char *) 0x40020C14); Då det gäller portar måste man också vara uppmärksam på att C-kompilatorns optimering kan ställa till det genom att flytta kodfragment för att skapa effektivare maskinkod. Då vi använder portar, vilkas värde kan ändras helt utanför programflödets kontroll, kan detta i vissa fall resultera att rent felaktig kod genereras. För att undvika sådan kodoptimering används nyckelordet volatile ("flyktig") i samband med variabeldeklarationen. Exempel: deklaration av porttyp Då vi refererar en port på en konstant adress anger vi att denna minnesadress kan ändras på ett sätt som inte kan förutses av kompilatorn. Vi anger detta med volatile i typkonverteringen: p = (volatile char *) 0x40020C14; Pekararitmetik Aritmetik med pekare skiljer sig en del från aritmetik med heltalstyper. Exempelvis är inte operationerna addition, subtraktion, division eller multiplikation tillåtna mellan två pekare. Det är däremot tillåtet att ta skillnaden mellan två pekare eller att addera/subtrahera en konstant. Då det gäller inc- och dec- operatorerna (++ och --) gäller för en pekare p att p++; pekar på nästa element i grundtypen, dvs. om vi har: char *cp; short *sp; int *ip; så gäller att: cp++; respektive ++cp; medför cp = cp+1; sp++; respektive ++sp; medför sp = sp+2; ip++; respektive ++ip; medför ip = ip+4; och på motsvarande sätt för p--; 60
Exempel Funktionen fillmem (char *address, int num, char pattern )kan användas för att initiera minneinnehåll med ett känt mönster. Funktionen skriver pattern till num konsekutiva adresser, med start på address, i minnet. I C kan den kodas: void fillmem( char *start, unsigned int size, char fill) { while( size ) { *start++ = fill; size--; } } Översätt funktionen till assemblerkod: fillmem: @ void fillmem( char *start, unsigned int size, char fill) @ {.L1: @ while( size ) CMP R1,#0 BEQ.L2 @ { @ *start++ = fill; STRB R2,[R0] ADD R0,R0,#1 @ size--; SUB B.L1 @ } @ }.L2: BX R1,R1,#1 LR Uppgift 2.15 I denna uppgift ska du kontrollera funktionen av subrutinen fillmem från föregående exempel. Du ska därför skapa enklast tänkbara omgivning för din test, det innebär väsentligen att ladda parametrar och utföra anropet, därefter upprepas programflödet vilket ger dig möjlighet att, manuellt ändra parametrar (registervärden) före anropet och på så sätt utföra upprepade test av subrutinen. @ @ fillmem.asm @ start: @ ladda parametrar LDR R0,mptr @ address MOV R1,#32 @ size MOV R2,#0xAA @ fill BL fillmem B start fillmem:... @ Parameter 1, startadress för minnesarean.align mptr:.word 0x20008000 Vi upplåter en minnesarea med start på adress 0x20008000, vi vet att detta inte ger någon konflikt i MD407:s minneskarta. Redigera en källtext, fillmem.asm med programmet och spara denna. 61
Assemblera och rätta eventuella fel. Appendix F: Vektortabell Ladda programmet till simulatorn, växla till minnesfliken, sätt visningens basadress till minnesareans start: Stega fram till subrutinaropet Utför hela subrutinen genom att klicka på Next Instruction i verktygslisten. Växla till minnesfliken, kontrollera minnesinnehållet. Växla tillbaks till programfliken Stega nu på nytt fram till subrutinanropet, ändra parametrar size och fill, dvs. R1 och R2 och utför hela subrutinen på nytt. Växla till minnesfliken, kontrollera minnesinnehållet. Slut uppgift 2.15 62