Allmänt
Här beskrivs helt kort de viktigaste skillnaderna mellan att skriva assembler kod i "absolut mode" jämfört med "relocatable mode".
|
Absolute mode |
Relocatable mode |
Allokering av variabler i RAM minnet. |
Måste ske "för hand".
Sker oftast med EQU direktivet vilket inte alls är någon allokering av minne, om man skall vara korrekt. EQU sätter bara ett värde på en symbol, och MPASM har inte en aning om att värdet råkar motsvara en adress i RAM minnet.
Man kan av misstag sätta samma värde på två symboler utan att få någon varning eller felmeddelande, men med ganska konstiga fel senare.
Om man flyttar koden mellan olika PIC modeller, måste man se över sina EQU eftersom olika processorer kan ha RAM minnet på olika adresser. D.v.s mer job vid flytt mellan modeller. |
Sker av MPLINK via RES direktivet. RES allokerar en minnesaddress och MPLINK ser till att ingen annan variablel i applikationen kan allokeras till samma RAM adress.
I källkoden anges aldrig en fysisk adress till RAM minnet. Adresserna finns i "Linker script", d.v.s LNK filen.
När man flyttar koden till en annan PIC modell, byter man LNK fil i projektet och därmed flyttas alla variabler till adresser som stämmer med den nya processorn. Även portningar av kod mellan PIC16 och PIC18 serierna underlättas detta sätt, eftersom hanteringen av RAM minnet sker automatiskt (mer eller mindre).
Så, använd aldrig EQU (Eller CBLOCK) för "allokering" av variabler ! |
Återanvändning av kod. |
Man måste bl.a själv kontrollare så olika delar av applikationer inte använder variabler på samma adress. Gör det svårt att skriva generella runiner som enkelt kan användas i olika projekt. |
Alla olika delar av applikationen (även de delar som har "återanvänts" använder RES och därför kommer MPLINK att allokera varabler med hänsyn till hela applikationen. Inga adresser kan allokeras till mer än en variabel.
D.v.s att samma generella rutin kan ha sina variabler på olika adresser beroende på vilken applikation den används i. Helt OK. |
Segmentering av kod. |
Hela applikationen är ett enda segment. Alla adresser måste anges "för hand". |
Man använer CODE direktivet för att segmentera koden i mindre bitar. Detta gör det enkelt för MPLINK att sprida koden runt speciella delar som man vill ha på speciella adresser (som t.ex lookup-tabeller).
Att dela koden i många mindre segment underlättar för MPLINK att fylla "hålen" i programminnet. Normalt anges bara enstaka adresser direkt i källkoden, resten får MPLINK räkna ut... |
Alla JESCAB's exempel använder rellocatable mode, och det hela blir helt naturligt om man gör "rätt" från början.
Konvertering av kod från abs till relloc mode.
Vid konvertera en gammal kod som är skriven i absolute mode finns det ett par saker som kan behöver göras :
I det nya projektet skall ett "Linker script" läggas till. Därmed kommer MPASM att ge felmeddelanden för det som inte är giltigt i relocatable mode.
Byt EQU till RES (se MPASM manulen eller programexempel för syntax).
Segmentera gärna koden lite med CODE (inte nödvändigt, men kan göra det överskådligare)
-
För ett stort projekt, fundera på att dela upp applikationen på flera ASM filer.
Övrigt
Observera att om man kodar för de större PIC modellerna (dsPIC30, dsPIC33 och PIC24) så är rellocatable mode det enda alternativet.
Exempel på användningen av RES direktivet
Eftersom många tycker det är lite svårt att följa med i exemplen i manualen till MPASM/MPLINK så kommer här några korta/enkla kodexempel som visar olika sätt att allokera utrymme i RAM (GPR) för variabler.
Ex1: Enkelt exempel med variabler i "shared memory"
(enbart PIC16 !)
Shared memory (eller "unbanked memory") är den del av GPR som är gemensam för alla banker. D.v.s att samma minne accessas oavsett hur bank-bitarna i STATUS registret råkar vara ställda. I t.ex en 16F628A är detta de sista 16 adresserna i varje bank. Detta minnesutrymme har två huvudsyften, dels för lagring av register under ett interrupt, dels för andra "snabba" variabler som man alltid vill ha tillgång till utan extra "bank-switching".
I exemplet tänker vi oss två variabler som ska allokeras till "unbanked memory", det skulle kunna se ut så här :
; Allokerar två variabler i "unbanked memory".
;
my_variables UDATA_SHR ; Starta ny data-section. Namnet "my_variables" kommer att
; finnas med i MAP filen så att man (om man vill) kan
; se vilka fysiska adresser som de hamnade på. Normalt
; är det inget som man varken vill eller behöver veta...
;
myvar1 RES 1 ; Allokerar (skapar) en variabel med namnet "myvar1".
myvar2 RES 1 ; Allokerar (skapar) en variabel med namnet "myvar2".
; De två symbolerna "myvar1" och "myvar2" används sedan
; som "vanligt" i koden där en adress till en varabel
; kan användas.
;
; Slut på variabler, här kommer koden...
;
my_code CODE ; Starta ny code-section.
;
; ...
; ...
Eftersom myvar1 och myvar2 är allokerade i "unbanked memory", så behöver man inte bekymra sig om BANKSEL innan man använder dom i koden.
Notera att PIC12F629/675 *enbart* har "unbanked memory", så på dom *måste* man använda UDATA_SHR !
Notera även att detta inte gäller (den för övrigt mycket snarlika) PIC12F683, den har sitt "unbanked memory" mer likt övriga PIC16 modeller.
Ex2: Allokering av variabler i övriga delen av minnet.
När man har mer normala variabler lägger man dom vanligtsvis i "banked memory" (fast om de ändå inte
är fler än 16, så kan ju alla vara "unbanked") m.h.a UDATA. Ett litet exempel skulle kunna se ut så här:
; Allokerar variabler i "banked memory".
;
my_variables2 UDATA ; Starta ny data-section. Namnet "my_variables2" kommer att
; finnas med i MAP filen så att man (om man vill) kan
; se vilka fysiska adresser som de hamnade på. Normalt
; är det inget som man varken vill eller behöver veta...
;
myvar21 RES 1 ; Allokerar (skapar) en variabel med namnet "myvar1".
myvar22 RES 1 ; Allokerar (skapar) en variabel med namnet "myvar2".
; De två symbolerna "myvar1" och "myvar2" används sedan
; som "vanligt" i koden där en adress till en varabel
; kan användas.
;
; Notera nu att man inte vet *var* de två variablerna
; har allokerats eller i vilken bank, så BANKSEL måste
; användas innan man använder dom!
;
; Slut på variabler, här kommer koden...
;
my_code CODE ; Starta ny code-section.
;
; Så här klan det t.ex se ut i koden
; Vi vill räkna ner myvar21 med 1 och kolla
; om resultetet blev 0.
;
;
BANKSEL myvar21 ; Se till att rätt bank är vald.
DECFSZ myvar21, f ; Minska myvar21 och "skippa" nästa instruktion om = 0.
GOTO not_zero ; Det blev *inte* 0, gå och gör något annat...
... ; Det *blev* 0, så fortsätt här...
;
; ...
; ...
Så skillnaden mellan UDATA_SHR och UDATA är i princip bara "bankningen".
Ex3: Allokeringen av temorära variabler som kan "delas" mellan olika delar av koden.
Ofta har man delar av koden (t.ex en delay rutin) där man har temorära variabler (t.ex "varv-räknarna" i en delay rutin) vars värde inte är intressant efter att man har lämnat rutinen. De adresser som dessa variabler upptar skulle ju då kunna delas av andra delar av koden för att spara RAM minne. Till detta används UDATA_OVR. Vi tittar på ett exempel igen...
; Allokerar variabler som "overlayed" från två olika subrutiner.
;
;********************************************************************************
; SUB1, som har 2 lokala temp-variabler och 2 andra variabler.
;
shr_vars UDATA_OVR ; Starta ny data-section för temp variablerna
; Vi väljer att kalla den för "shr_vars".
;
sub1_tmp1 RES 1 ; Allokera de två temp variablerna...
sub1_tmp2 RES 1
;
sub1_vars UDATA ; Starta ny data-section för de "normala" variablerna.
;
sub1_var1 RES 1 ; Allokera de två variablerna...
sub1_var2 RES 1
;
;
sub1_code CODE ; Starta code-section för SUB1.
; ...
;
;********************************************************************************
; SUB2, som har 3 lokala temp-variabler och 1 vanlig variabel.
;
shr_vars UDATA_OVR ; Starta ny data-section för temp variablerna¨
; notera att vi sätter samma namn som i SUB1
; för att dela minnesadresser!
;
sub2_tmp1 RES 1 ; Allokera de tre temp variablerna...
sub2_tmp2 RES 1
sub2_tmp3 RES 1
;
sub2_vars UDATA ; Starta ny data-section för de "normala" variablerna.
;
sub2_var1 RES 1 ; Allokera de två variablerna...
sub2_var2 RES 1
;
;
sub2_code CODE ; Starta code-section för SUB2.
; ...
; ...
Om vi nu tittar i MAP filen för att se hur detta allokerades så kan det se ut så här (endast de intressanta delarna):
...
...
Section Info
Section Type Address Location Size(Bytes)
--------- --------- --------- --------- ---------
.cinit romdata 0x000005 program 0x000004
SUB1_CODE code 0x000007 program 0x000002
SHR_VARS udata 0x000120 data 0x000003
SUB2_VARS udata 0x000123 data 0x000002
SUB1_VARS udata 0x000125 data 0x000002
...
...
Symbols - Sorted by Address
Name Address Location Storage File
--------- --------- --------- --------- ---------
SUB1_TMP1 0x000120 data static C:\DATA\proj\test\Untitled.asm
SUB2_TMP1 0x000120 data static C:\DATA\proj\test\Untitled.asm
SUB1_TMP2 0x000121 data static C:\DATA\proj\test\Untitled.asm
SUB2_TMP2 0x000121 data static C:\DATA\proj\test\Untitled.asm
SUB2_TMP3 0x000122 data static C:\DATA\proj\test\Untitled.asm
SUB2_VAR1 0x000123 data static C:\DATA\proj\test\Untitled.asm
SUB2_VAR2 0x000124 data static C:\DATA\proj\test\Untitled.asm
SUB1_VAR1 0x000125 data static C:\DATA\proj\test\Untitled.asm
SUB1_VAR2 0x000126 data static C:\DATA\proj\test\Untitled.asm
Notera hur tmp1 opch tmp2 från både SUB1 och SUB2 delar adress medan var1 och var2 är separat allokerade.
När man använder overlay variabler så måste man naturligtsvis se upp! SUB1 får ju t.ex inte göra ett CALL till SUB2!
Man kan inte heller låta SUB1 avbrytas av ett interrupt som i sin tur gör CALL till SUB2.
Men rätt använt är det ett sätt att hushålla med minnet i processorn.
Genom att man själv anger namnet på den "overlay section" som man vill använda, så kan man skapa flera stycken om man t.ex har olika grupper av subrutiner som normalt inte anropas från varandra. D.v.s en grupp av 2-3 subrutiner kan dela på en "overlay section" medan 3-4 andra subrutiner kan dela på en helt annan "overlay section".
Ex4: Allokering av variabler i "access bank" (endast PIC18 !)
"Access bank" i PIC18 motsvaras på sätt och vis av (men ska inte förväxlas med) "unbanked memory" i PIC16. Det är en del av RAM minnet som man alltid "kommer åt" oavsett hur bank-bitarna är satta. Access bank är också betydligt större än "unbanked memory" i PIC16, normalt 128 bytes (varierar dock mellan PIC18 modellerna, se respektive datablad!).
Kodexempel är lite onödigt, det ser i princip likadant ut som ovan, fast UDATA_ACS används istället.
Det finns även en specialvariant av UDATA_OVR för PIC18, den heter då ACCESS_OVR, och fungerar i princip som UDATA_OVR ovan, men att de "delade" sektionerna allokeras i just "access bank". UDATA och UDATA_OVR fungerar även till PIC18, men då allokeras utrymmet i det övriga RAM minnet och BANKSEL kan behövas (eller så kör man indexerad läsning via index registren FSR0, FSR1 och FSR2, vilket sannolikt är vanligare på PIC18).
En liten titt i en LKR fil...
Inte för att fördjupa det för långt, men vi gör i alla fall en liten koll i en LKR fil, i detta fall för PIC16F886.
Först filen så kommer kommentarerna efteråt...
// Sample linker command file for 16F886
LIBPATH .
CODEPAGE NAME=vectors START=0x0 END=0x4 PROTECTED
CODEPAGE NAME=page0 START=0x5 END=0x7FF
CODEPAGE NAME=page1 START=0x800 END=0xFFF
CODEPAGE NAME=page2 START=0x1000 END=0x17FF
CODEPAGE NAME=page3 START=0x1800 END=0x1FFF
CODEPAGE NAME=.idlocs START=0x2000 END=0x2003 PROTECTED
CODEPAGE NAME=.config START=0x2007 END=0x2008 PROTECTED
CODEPAGE NAME=eedata START=0x2100 END=0x21FF PROTECTED
DATABANK NAME=sfr0 START=0x0 END=0x1F PROTECTED
DATABANK NAME=sfr1 START=0x80 END=0x9F PROTECTED
DATABANK NAME=sfr2 START=0x100 END=0x10F PROTECTED
DATABANK NAME=sfr3 START=0x180 END=0x18F PROTECTED
DATABANK NAME=gpr0 START=0x20 END=0x6F
DATABANK NAME=gpr1 START=0xA0 END=0xEF
DATABANK NAME=gpr2 START=0x110 END=0x16F
DATABANK NAME=gpr3 START=0x190 END=0x1EF
SHAREBANK NAME=gprnobnk START=0x70 END=0x7F
SHAREBANK NAME=gprnobnk START=0xF0 END=0xFF
SHAREBANK NAME=gprnobnk START=0x170 END=0x17F
SHAREBANK NAME=gprnobnk START=0x1F0 END=0x1FF
SECTION NAME=STARTUP ROM=vectors // Reset and interrupt vectors
SECTION NAME=PROG1 ROM=page0 // ROM code space - page0
SECTION NAME=PROG2 ROM=page1 // ROM code space - page1
SECTION NAME=PROG3 ROM=page2 // ROM code space - page2
SECTION NAME=PROG4 ROM=page3 // ROM code space - page3
SECTION NAME=IDLOCS ROM=.idlocs // ID locations
SECTION NAME=DEEPROM ROM=eedata // Data EEPROM
Allt som heter xxxPAGE eller xxxBANK definierar de *fysiska* adresserna för bl.a programminne (flash) och RAM-minne (GPR).
(Dessutom anges var EEPROM minnet ligger m.m, men det hoppar vi över just nu...)
Allokeringen av minne sker i princip så här för olika direktiv:
CODE |
Letar efter ledigt minne i en CODEPAGE (som inte är "PROTECTED" !). Man kan styra sin kod till en viss "page" genom att skriva t.ex "PROG2" före CODE direktivet. Notera hur PROG2 "pekar" mot den CODEPAGE som heter "page1". Sen kan man fråga sig varför de inte heter PROG0-PROG3, men det är en helt annan sak och det har inget praktisk betydelse... :-)
Man kan själv lägga till flera SECTIONS med "ROM=" för att styra olika delar av koden till olika pages.
Eftersom det krävs lite extra "jobb" i koden när man gör CALL över page-gränserna, så kan det vara praktiskt att samla subrutiner som hör ihop (och som gör många anrop till varandra) på samma CODEPAGE. För att garantera att så sker, måste man skapa en speciell SECTION för dessa och ange vilken CODEPAGE som den nya sektionen "hör till". Låter man MPLINK allokera helt själv, så kan subrutinerna hamna på olika CODEPAGE's.
Notera att det finns en SECTION som heter STARTUP och som pekar mot "vectors" på adress "noll" d.v.s där man hamnar efter reset eller power-on. I mina exempelkoder används STARTUP för att placera de instruktioner som ska köras vid reset eller power-on på rätt ställe. |
UDATA |
Allokerar minne från ledigt minne i *någon* DATABANK. Vilken som helst som har ledigt minne (kvar). Man kan styra UDATA till en speciell bank genom att skapa en ny SECTION med ett nytt namn och med t.ex "RAM=gpr3" för att styra denna nya sektion till bank3. Sedan sätter man detta nya namn före sin UDATA och länkaren kommer då att ta hand om det hela.
Notera också att FSR minnet är definierat som DATABANK'er, men detta är PROTECTED, så länkaren kommer aldrig att försöka allokera variabler där. |
UDATA_SHR |
Allokerar minne från SHAREBANK. Notera att alla 4 är definierade i LKR filen, men de avser ju i praktiken samma fysiska minne. |
UDATA_OVR |
Samma som för UDATA, för övrigt se ovan om UDATA_SHR... |
Notera slutligen att det står "*Sample* linker command file". För större projekt är det vanligt att man gör anpassningar av LKR filen, t.ex av det slag som beskrevs ovan. Anledningen kan t.ex vara att göra placeringen av t.ex lookup-tabeller transparent för applikationen (källkoden) så att när man flyttar koden mellan olika PIC modeller så byter man bara LKR fil.