A karom, a karom! Cortex-M0 kiadásban (2. rész)

A múltkori történet ott végződött, hogy – immár sok év után, végre – sikerült a µC FLASH memóriájába adatokat tölteni, jöhet a „belevaló” elkészítése. Legalábbis valami egyszerűbb tesztkód... Kár, hogy az ilyen nem lelkesít akkor, ha nem használandó program lesz a végeredmény, csak egyszerűen teszt, de ez a környezet annyira újdonság, hogy nem maradhat ki ez a lépés.

Fordítónak a GCC van kinevezve, ebből kell egy ARM-ra fordító verzió. Jó esetben ez kb. az összes ARM magot támogatni fogja, beleértve az M0-t is, ehhez csak egy nem túl régi verzió kell.

A keresési találatok nagy része ezt a launchpad-os linket adja, ezzel már anno találkoztam! Ez egy „előfordított” toolchain, használatra kész formában letölthető Windows® illetve Linux®™ verzióban is. A találatoktól függetlenül nem érdemes ezt használni, mivel – ahogy az oldalon is szerepel – valamikortól már ide nem kerül feltöltésre új verzió, azokat az ARM-tól lehet letölteni. A jelenlegi utolsó verzió a 2017. Q4, ez (gondolom) a 2017. 4. negyedévben elérhető változat. Annyit erről a toolchainról érdemes tudni, hogy ez a Cortex-M illetve Cortex-R magokat támogatja csak, ezek mind mikrovezérlő stílusú cuccok. A Cortex-A verzióra (alkalmazásprocesszorok) ezzel nem lehet forgatni, ahhoz (valószínűleg) „teljes” ARM-GCC kell. Nekem jelenleg pont a mikrovezérlős verzió kell, kipróbáltam, működik, de aztán végül mégse ez lesz a későbbiekben használva.

A fordító „nevezéktanáról” is érdemes pár szót ejteni, ez nagyjából valahogy így szokott kinézni:

architektúra-vendor-ostípus-abitípus-programnév

Ezek jelentése:

  • architektúra – Milyen CPU architektúrához tartozik az eszköz? Esetünkben ez az arm lesz;
  • vendor – Ki a toolchain „szállítója”? Ez sokszor ki van hagyva, enélkül is elég hosszúak a nevek;
  • ostípus – Milyen „cél” operációs rendszerre fordít a toolchain? Mivel jelenleg én írnám az OS-t, :) így ez nem meghatározható, most ez a none lesz;
  • abitípus – Milyen ABI-t használ a fordító? Az ABI az Application Binary Interface rövidítése, ez itt arról szól, hogy milyen adattípus hogyan van a memóriába eltárolva, függvényhívásnál milyen módon vannak a paraméterek átadva, stb. Beágyazott rendszerfejlesztéshez az eabi, az Embedded ABI használatos (?), szerintem itt a kompatibilitás annyira nem lényeges, (ritkán fordul az elő, hogy egy µC egy programját több, különböző fajta fordítóval fordítanák,) ez viszonylag szabadon változhat (de mást az eabi-n kívül itt még nem láttam :) );
  • programnév – a toolchain külön tooljainak a neve, itt például gcc;

A fenti „nevezéktan” alapján a GCC neve arm-none-eabi-gcc lesz, az ARM-tól letölthető toolchain is ilyen neveket tartalmaz. A disztribúcióhoz tartozó csomagkezelővel is ilyet érdemes keresni, Fedora-n ez elérhető csomagból:

Nem mondom, nem lesz kevés... :) (Gyaníthatóan ez a teljes ARM palettára tud fordítani, nem csak a mikrovezérlő változatokra.) Viszont jó lenne nekem ez CentOS7 alá is, ott persze nincs belőle csomag. Akkor fordítsunk! :) Itt is „elővettem a szokásos trükköt”: Fedora alatt a forrás-RPM-et letöltve, majd azt kicsomagolva (rpm2cpio csomagnév | cpio -idmv) meg is van a forrás .tar.gz2 tömörítéssel, az esetleges peccsek, és ami a fontos, maga a .spec fájl, ez alapján generálódik a csomag. Ezt minimálisan módosítva készíthető belőle CentOS alá is .rpm. Azért vannak érdekességek... :)

  • Az arm-none-eabi-binutils-cs nagyjából simán generálható, a ppl-devel illetve cloog függőségek kiszedése után. (Csúnya hack; de lefordul nélkülük is, CentOS alatt nem találtam ilyen csomagot. A CLooG valami optimalizációs segítség lenne, hogy kell-e nekem, azt egyelőre nem tudom. :) )
  • Az arm-none-eabi-gcc-cs (a -c++ csomag ugyanebből a forrásból készül) már egy kicsit viccesebb. A fordításhoz szükség lesz az arm-none-eabi-newlib csomagra. Viszont ennek a csomagnak a fordításához meg szükség van az arm-none-eabi-gcc-re, amit épp most akarnék fordítani. :) A klasszikus 22-es csapdája. A Fedora-s csomagot a készítő úgy oldotta meg, (illetve úgy tűnik, ez a „normális” metódus, ) hogy a .spec fájlban van egy bootstrap változó. Ha ez 1, akkor az arm-none-eabi-newlib függőség nélkül lefordul egy „egyszerűsített verzió”, ezt felrakva forgatható vele a -newlib, majd az így létrejövő -newlib csomagot föltéve, a bootstrap változót 0-ra visszaállítva már lefordul a „teljes” arm-none-eabi-gcc. Ez az „teljes” változat nálam pörgött vagy 4 órát, mire elkészült... :)
  • Az arm-none-eabi-newlib fordítása az előző trükkel sima ügy; ez egy csak ARM-os kódokat tartalmazó csomag, a klasszikus „C-s” függvényeket ez valósítja meg.
  • Az arm-none-eabi-gdb a debugger, ezzel semmi bonyodalom nincs.

Tehát némi túrás, meg jó pár óra várakozás után van CentOS7-en is arm-none-eabi-gcc! Szuper! És igen, továbbra is ilyen vontatottan halad a téma. Már készen van minden is, de kódot, azt még mindig nem írtam egy sort se... Most már éppen ideje lesz! Vagyis: ideje van! (Ami most következik, az erősen jegyzet magamnak; némi próbálkozással körítve!)

A leg-egyszerűbb-tesztkód-evör legyen a következő, main.c:

int main (void) {
  while (1);
}

Még egy nyamvadt #include <stdio.h> sincs benne, egyelőre még az is luxus lenne! Talán nagyon magyarázni nem kell: egy sima végtelen-ciklus, ami nem csinál semmit. Ezt érdemes lenne lefordítani:

$ arm-none-eabi-gcc -mcpu=cortex-m0 -mthumb -nostartfiles main.c -o main.elf

A -nostartfiles paraméterrel nem kerül bele a végeredménybe a C saját inicializálása, egyelőre nem is sikerülne neki berakni, erről később. A végeredmény a main.elf fájl, ami köszönhető az -o main.elf paraméternek, (enélkül egy a.out nevű, szintén .elf típusú fájl keletkezne,) de van egy kövér warning:

A figyelmeztetésben levő elérési út egy kicsit „furcsa”; de úgy tűnik, hogy a futtatott programhoz képest relatív útvonalon keresztül éri el a másik toolt, így nem szükségszerű, hogy a toolchain benne legyen az elérési útban. Ez később még jól jöhet, lehet ugyanolyan néven több verzió is telepítve, nem kell hogy összevesszenek. Maga a figyelmeztetés:

ld: warning: cannot find entry symbol _start; defaulting to 0000000000008000

Nincs definiálva a _start cím, ezért az alapérték, 0x00008000 van használva. Ez nekünk nem jó, a µC programmemóriája nem itt kezdődik, nem ide kell pakolni a kódot. A fordítás az valahogy úgy megy, hogy a C-fordító lefordítja a kódo(ka)t, ami(k)ben a címek maximum részlegesen vannak feloldva. Ebből a linker csinálja meg a binárist, beállítva a tényleges, eddig még nem kiszámított címeket. (A fenti figyelmeztetés pontosan a linkertől jön.) Ahhoz, hogy a kívánt helyre kerüljön minden, a linkernek meg kell mondani, hogy mit hova pakoljon, erre szolgál a linker-script. Tehát kell egy ilyen is, ez is – egyelőre – a leg-egyszerűbb-linkerszkript-evör verzióban, NUC140.ld:

ENTRY(main)
SECTIONS
{
  .text : {
    *(.vectors)
    *(.text)
  }
}

Fordítás újra:

$ arm-none-eabi-gcc -mcpu=cortex-m0 -mthumb -nostartfiles -T NUC140.ld main.c -o main.elf

Itt a -T NUC140.ld paraméter adja meg a linker-script nevét. A fordítás bármilyen üzenet nélkül megtörténik, előáll a main.elf fájl, ~66900 BYTE méretben. Nem kicsi, de ennek a töredéke kell majd... No de mi is került bele? Erre is van kiváló parancs:

$ arm-none-eabi-objdump -d main.elf

Ez „diszasszemblálja” az imént elkészült main.elf-et, a végeredmény valami ilyesmi:

main.elf:     file format elf32-littlearm

Disassembly of section .text:

00000000 <main>:
   0:    b580            push    {r7, lr}
   2:    af00            add     r7, sp, #0
   4:    e7fe            b.n     4 <main+0x4>

Ez egy függvény-előkészítés, illetve egy „magára ugró” ugró-utasítás, azaz egy végtelen-ciklus. Ez így még jó is! :) Az .elf fájl még továbbra se programozható be a mikrovezérlőbe, abból a „bináris kódot” még ki kell másolni, de a fentiekben még van egy – nem éppen elhanyagolható – hiányosság. A Cortex-M0 CPU mag „úgy indul”, hogy a memória legelején, 0x00000000 címtől kezdődően kell neki egy „vektor-tábla”, aminek az első eleme a beállítandó SP (veremmutató), a második meg az indulási cím, jelenleg az lenne a main() függvény kezdőcíme. Tehát minimum ez a kettő 32 bites érték szükséges a kód elejére, 0x00000000 címtől kezdődően. Ezt viszonylag egyszerű belerakni, a main.c fájl végére a következő kerül:

int *myvectors[2] __attribute__ ((section(".vectors"))) = {
  (unsigned int *) 0x20004000,     // SP
  (unsigned int *) main            // Main
};

Az, hogy ez a két unsigned int hova is kerüljön, azt az __attribute__ ((section(".vectors"))) paraméter mondja meg. A .vectors az ismerős a fenti linker-script-ből, azzal „kezdődik” a kiosztás, nem véletlenül... Az így kiegészített main.c fordítása utáni „diszasszemblálás” végeredménye:

main.elf:     file format elf32-littlearm

Disassembly of section .text:

00000000 <myvectors>:
   0:     00 40 00 20 09 00 00 00              .@. ....

00000008 <main>:
   8:    b580            push    {r7, lr}
   a:    af00            add     r7, sp, #0
   c:    e7fe            b.n     c <main+0x4>

Ez már kezd úgy kinézni, amit bele is lehetne tölteni a µC-be. A „mi Cortex-M0 magunk” Little-Endian, emiatt az alsó helyiértékkel kezdődnek a számok. (A „régebbi” ARM magok, mint amilyen az ARM7TDMI, Bi-Endian-ok, szoftverből átkapcsolhatók Little-Endian / Big-Endian módra. A Cortex-M0 is lehet mindkét fajta, de az, hogy milyen lesz, a csip implementálásakor dől el, szoftverből ezek nem állíthatóak.) Az első 4 BYTE 00 40 00 20, ami a 0x20004000 memóriacímet jelöli, ez lesz a veremmutató, eddig sima ügy. A következő 4 BYTE 09 00 00 00 viszont érdekes: ez a 0x00000009 memóriacímet jelöli. A kód viszont 0x00000008-on kezdődik... A válasz (nagyjából) megtalálható a Cortex-M0 Generic User Guide-ban. A Cortex-M a thumb utasításkészletet támogatja csak, ezek 16 (vagy esetenként 32) bites op-kódokból állnak, amik mindig páros címen kezdődnek. A cím B0 bitje az EPSR „T” bitjébe másolódik be, amikor a (bármelyik) vektort beolvassa a CPU, ez pedig a „Thumb” állapotot jelöli. Mivel nincs „natív ARM” utasításkészlet támogatás, így ennek a bitnek mindig 1-nek kell lennie, különben egy Hard-Fault lesz a jutalom. Azaz a vektorokban itt a B0=1, tehát jó az úgy.

Az eddigi elkészült tesztkód már „futóképes” lenne, de – mivel az volt a cél – nem csinál semmit. Nálam a (bármilyen) µC-es Hello World példaprogram az szokott lenni, hogy beállítok kimenetre egy portbitet, meg bemenetre egy másikat. Majd a bemenet állapotától függően 1-be illetve 0-ba rakom a kimenetet. A kimeneti portlábra jön egy LED, a bemenetre meg egy kapcsoló, ha a tesztkód fut, akkor a kapcsoló állapotától függ, hogy a LED világít-e vagy sem. (A „többség” általában LED-et villogtat, de ahhoz várakozni kell, ahhoz meg illik tudni a CPU sebességét, szóval szerintem bonyolultabb mint az én verzióm.) Ehhez már minimális regiszterismeret kell, de erre jó az adatlap. A gyártók ezeket a konfigurációs regisztereket – természetesen (?) – elnevezik, sőt, szoktak adni C-s header fájlokat, amik tartalmazzák ezeknek a definícióit. Itt is van ilyen, tele is van struktúradefinícióval, de azt most későbbre halasztom, egyelőre maradjunk az #include-nélküliségnél... :) A tesztkód most valami ilyesmi lesz, újabb main.c:

#define GPIOA_PMD  ((volatile unsigned int *) 0x50004000)
#define GPIOA_DOUT ((volatile unsigned int *) 0x50004008)
#define GPIOA_PIN  ((volatile unsigned int *) 0x50004010)

int main (void) {
  *GPIOA_PMD = 0x00000001;    // PORTA B15..1 Input, B0 Output
  while (1) {
    if ((*GPIOA_PIN & 0x00000002) != 0) {
      *GPIOA_DOUT &= 0xfffffffe;
    } else {
      *GPIOA_DOUT |= 0x00000001;
    }
  }
}

int *myvectors[2] __attribute__ ((section(".vectors"))) = {
  (unsigned int *) 0x20004000,     // SP
  (unsigned int *) main            // Main
};

Szerencsére a GPIO felélesztése nem egy komoly feladat, ehhez még nem kell órajeleket kapcsolgatni. A tokban 16 bites „szélességű” GPIO blokkok vannak, de a regisztereik 32 bitesek. A példakód három regisztert használ, a PMD a Port Mode, ezzel lehet az adatirányt beállítani. Minden portbithez két konfigurációs bit tartozik, ezek kombinációi:

  • %00 – Az adott vonal bemenet
  • %01 – Az adott vonal sima push-pull kimenet
  • %10 – Az adott vonal Open-collector-os kimenet
  • %11 – Az adott vonal „Quasi-Bidirectional” vonal lesz

Ezen utolsó üzemmód egy olyan kimenet, aminél ha alacsonyra kapcsolják a vezérlő bitet, akkor alacsonyat hajt a vonalra; ez eddig egy open-collector-os viselkedés. Viszont ha magasra kapcsolják a vonalat, akkor két órajelciklusig „alacsony impedanciával” magasat hajt a láb, majd utána kikapcsolódik ez a hajtás, a vonalon a magas szintet egy felhúzó ellenállás biztosítja a továbbiakban. Ilyenkor a külső eszköz lehúzhatja nyugodtan a vonalat alacsonyra, az ekkor simán bemenetként viselkedik. Tehát ez úgy kétirányú üzemmód, hogy közben nem kell az adatirányt kapcsolgatni.

A csip fejlesztőinek itt azért beszólnék egy kicsit: a lábakon van felhúzó ellenállás, de közvetlenül nem kapcsolható, a „Quasi-Bidirectional” módban aktív csak, ha simán bemenetként használnám, akkor lehet „kerülgetni”. Ráadásul az összes port RESET esetén ebben az üzemmódban indul el, amit azért nem érzek túl szerencsésnek...

A másik két regiszter már egyértelmű, a PIN a Port in, ezen lehet a lábak állapotát visszaolvasni, a DOUT pedig a Data out, ezen keresztül lehet a kimeneteket írni. A fenti tesztkód beállítja a PORTA 0. bitjét kimenetre, ez fogja hajtani a LED-et, az 1-est (a többi vonallal együtt) meg bemenetre kapcsolja. A végtelen-ciklus a PORTA 1. bitjének az állapotától függően törli vagy beállítja a 0. bitet, ez lesz most a tesztkód. A fenti linker-script-tel lefordítva, majd „diszasszemblálva”:

00000000 <myvectors>:
   0:     00 40 00 20 09 00 00 00              .@. ....

00000008 <main>:
   8:    b580      push    {r7, lr}
   a:    af00      add     r7, sp, #0
   c:    4b0a      ldr     r3, [pc, #40]    ; (38 <main+0x30>)
   e:    220d      movs    r2, #13
  10:    601a      str     r2, [r3, #0]
  12:    4b0a      ldr     r3, [pc, #40]    ; (3c <main+0x34>)
  14:    681b      ldr     r3, [r3, #0]
  16:    2202      movs    r2, #2
  18:    4013      ands    r3, r2
  1a:    d006      beq.n   2a <main+0x22>
  1c:    4b08      ldr     r3, [pc, #32]    ; (40 <main+0x38>)
  1e:    681a      ldr     r2, [r3, #0]
  20:    4b07      ldr     r3, [pc, #28]    ; (40 <main+0x38>)
  22:    2101      movs    r1, #1
  24:    438a      bics    r2, r1
  26:    601a      str     r2, [r3, #0]
  28:    e7f3      b.n     12 <main+0xa>
  2a:    4b05      ldr     r3, [pc, #20]    ; (40 <main+0x38>)
  2c:    681a      ldr     r2, [r3, #0]
  2e:    4b04      ldr     r3, [pc, #16]    ; (40 <main+0x38>)
  30:    2101      movs    r1, #1
  32:    430a      orrs    r2, r1
  34:    601a      str     r2, [r3, #0]
  36:    e7ec      b.n     12 <main+0xa>
  38:    50004000  .word	0x50004000
  3c:    50004010  .word	0x50004010
  40:    50004008  .word	0x50004008

Az látszik, hogy nem éppen optimális a kód (semmiféle optimalizáció nincs bekapcsolva egyelőre), de ez akár már működhet is! Jöjjön a bináris előállítása:

$ arm-none-eabi-objcopy -O binary main.elf main.bin

A parancs kimásolja a bináris tartalmat egy külön fájlba (main.bin), ami – meglepetés! – 68 BYTE hosszú lesz. Ez azért már baráti, így, ahogy van, be is lehet sütni. Ehhez a múltkor tárgyalt OpenOCD kell, a hozzá tartozó openocd.cfg fájl valahogy így néz ki:

source [find interface/ftdi/luminary-icdi.cfg]
transport select swd
set CHIPNAME NUC140VE3AN
source [find target/numicro.cfg]
adapter_khz 500

init
reset run
halt

targets
numicro chip_erase
flash write_image erase main.bin 0x0
reset run
shutdown

A végeredmény:

Ez – látszólag – tökéletes! :) Ehhez már csak egy apróság kell: egy LED meg némi ellenállás a megfelelő pontokra csatlakoztatva:

A LED anódja egy ellenálláson keresztül a tápra van kötve, a katód meg a PORTA0 vonalra. A kapcsolót most egy jumper helyettesíti, a két érintkező közül az egyik a GND-re kötött, a másik a PORTA1 vonalra jön, de ezt egy másik ellenállás felhúzza a tápra. (Ha a jumper fel van rakva, az alacsonyra kapcsolja a PORTA1 vonalat, egyébként meg az ellenállás magasra húzza.) A tesztkódnak annyit kellene csinálni, hogy ha a PORTA1 vonal magas (nincs jumper), akkor a PORTA0 alacsony (LED bekapcsolva). Fordított esetben minden fordul. És... Csinálja is! Működik a mini Hello World! :) De ha már van valami, ami csinál is valamit, esetleg lehetne egy kicsit debuggolni, még ha itt konkrétan az is történik, aminek kell. Ehhez szükség lesz egy másik OpenOCD konfigurációs fájlra, legyen mondjuk debug.cnf:

source [find interface/ftdi/luminary-icdi.cfg]
transport select swd
set CHIPNAME NUC140VE3AN
source [find target/numicro.cfg]
adapter_khz 500

init
reset run
halt

Ez egy openocd -f debug.cnf paranccsal indítható, szépen le is áll tőle a target:

target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x00000014 msp: 0x20003ff8

A debughoz a GDB, a GNU Project Debugger kell, annak is az ARM-ra fordított verziója. (Ebből készült csomag még az elején, ez is a toolchain része.)

$ arm-none-eabi-gdb

Az arm-none-eabi-gdb parancsot elindítva a GDB parancsértelmezője kerül elő, ebbe lehet manuálisan a parancsokat beleirkálni, klasszikus MONITOR fíling. :) A GDB „csak” az UI-ért felel, a debug funkciókat a GDB-Server valósítja meg. Ebben az esetben a GDB-Server az OpenOCD maga, ehhez kapcsolódik a GDB a

(gdb) target remote localhost:3333

parancs segítségével. Mivel az OOCD konfigurációs fájlban már meg lett állítva a target, ezért a tesztkód LED-kapcsolgatása se megy jelenleg. (Nyilván...) Egy sima

(gdb) continue

paranccsal tovább lehet indítani, egy Ctrl+C billentyűkombináció viszont leállítja, mint egy rendes programot... :) A regiszterek aktuális állapotát az

(gdb) info registers

paranccsal lehet lekérdezni, de van egy jó nagy rakás parancs, amivel lehet bűvészkedni. Ez valószínűleg megérne egy komplett blogbejegyzést... Amennyiben a GDB úgy van indítva, hogy

$ arm-none-eabi-gdb -tui main.elf

akkor a debugger a címekhez ismerni fogja a szimbólumokat, jóformán a C-s forráskódot lehet figyelni közvetlenül. A -tui paraméterrel egy „több-ablakos” karakteres felület lesz a jutalom, ami alapból nem túl információ-gazdag. (Valamerre láttam ehhez egy konfigurációs fájlt, amivel egy kicsit fel lehet turbózni a látottakat, majd ez is megér egy vizsgálatot.) A nexti vagy röviden ni paranccsal lehet utasításokat végrehajtani egyesével, betöltött .elf mellett kiírja, hogy éppen melyik utasítás hajtódik végre, de ez így tényleg nagyon „nyögvenyelős”. Lehet, hogy érdemes lesz ehhez valami GUI-s frontendet keríteni. (Nem, az Eclipse-t nem akarom! :) ) Kilépni a quit paranccsal lehet egy megerősítés után, az OpenOCD-t meg egy Ctrl+C megállítja. (Viszont ha haltolva volt a µC, akkor az úgy is marad!)

De vissza a „kódolásra”. A fenti példában a CPU vektorai a C-s forrásba vannak beillesztve, de meg lehet ezt csinálni úgy is, hogy egy saját, assembly startup kód illeszti be ezt a kód elejére. Ennek jelentősége talán csak akkor van, ha az ember inkább asm-ben kódolna, a C-t meg mondjuk meghagyja az inicializációs feladatokra. A C-s példakód egy asm rutin-indítást csinál, ez se sokkal bonyolultabb, mint a leg-egyszerűbb-tesztkód-evör, main.c:

void AsmFunction(void);

int main (void) {
  AsmFunction();
}

Ehhez tartozik a „startup”, amiben ott vannak a vektorok a hívott függvénnyel, de ez is minimalista verzió, ez se csinál semmit, startup.s:

.section  .vectors

Vectors:
    .word 0x20004000
    .word main

.section .text

.global AsmFunction

AsmFunction:
        nop
        nop
        nop
        b       AsmFunction

.end

A linker-script marad az eddigi. A fordítás:

$ arm-none-eabi-gcc -mcpu=cortex-m0 -mthumb -nostartfiles -T NUC140.ld main.c startup.s -o main.elf

A végeredmény:

main.elf:     file format elf32-littlearm

Disassembly of section .text:

00000000 <Vectors>:
   0:    20004000    .word   0x20004000
   4:    00000009    .word   0x00000009

00000008 <main>:
   8:    b580        push    {r7, lr}
   a:    af00        add     r7, sp, #0
   c:    f000 f804   bl      18 <AsmFunction>
  10:    2300        movs    r3, #0
  12:    0018        movs    r0, r3
  14:    46bd        mov     sp, r7
  16:    bd80        pop     {r7, pc}

00000018 <AsmFunction>:
  18:    46c0        nop             ; (mov r8, r8)
  1a:    46c0        nop             ; (mov r8, r8)
  1c:    46c0        nop             ; (mov r8, r8)
  1e:    e7fb        b.n     18 <AsmFunction>

Ez a tesztkód ismételten nem érdemli meg, hogy letöltsem a µC-be, de úgy néz ki, ahogyan elvárható, a main() függvény meghívja az asm rutint, ami jelenleg nem is tér vissza. (Ha meg mégis visszatérne, a main() is befejeződik, amiből jó eséllyel egy fagyás lesz. :) )

Egy dologgal kapcsolatban még van mit megvizsgálni, erről egyelőre nem találtam információt. A Cortex-M0 magnál nem konfigurálható a vektortábla kezdőcíme, az mindig 0x00000000. Viszont a NuMicro-sorozat tartalmaz bootloader-képességet, amihez van egy külön 4 KBYTE-os FLASH terület LDROM néven, 0x00100000..0x00100FFF tartományban. Ahhoz, hogy innen induljon RESET esetén a végrehajtás, a Config0 regiszterben (ez szintén EEPROM, programozásnál beállítandó) be kell programozni a CBS (Chip Boot Selection) bitet. De ebben az esetben vajon mi történik? Ilyenkor szimplán elindul a végrehajtás 0x00100000-tól? Vagy átkerül a vektortábla kezdőcíme ide, tehát ugyanúgy kellenek a vektorok az elejére? Én az utóbbira tippelek, de egy próbát mindenképpen megér. Így lesz LDROM FLASH, illetve konfigurációs terület programozási próba is, amik eddig elmaradtak... A sima LED-kapcsolgatós teszt tökéletes ide is, a linker-script-et kell csak módosítani a megfelelő eredmény érdekében. Ha nincs külön megadva, alapesetben 0x00000000 címen kezdődik minden, de itt most más kell, NUC140.ld az LDROM-hoz:

ENTRY(main)

SECTIONS
{
  . = 0x00100000;
  .text : {
    *(.vectors)
    *(.text)
  }
}

Ezzel lefordítva a LED-kapcsolgatós tesztet, a végeredmény az LDROM címére kerül:

main.elf:     file format elf32-littlearm

Disassembly of section .text:

00100000 <myvectors>:
  100000:    00 40 00 20 09 00 10 00              .@. ....

00100008 <main>:
  100008:    b580         push    {r7, lr}
  10000a:    af00         add     r7, sp, #0
  10000c:    4b0a         ldr     r3, [pc, #40]      ; (100038 <main+0x30>)
  10000e:    220d         movs    r2, #13

...és a többi. A cím az jó, be kell sütni a FLASH LDROM területére, illetve a megfelelő Config0 bitet is be kell pirítani, ehhez szintén az OOCD segítsége kell:

source [find interface/ftdi/luminary-icdi.cfg]
transport select swd
set CHIPNAME NUC140VE3AN
source [find target/numicro.cfg]
adapter_khz 500

init
reset run
halt

targets
numicro chip_erase
flash write_image erase main.bin 0x100000
numicro write_isp 0x300000 0xffffff7f
reset run
shutdown

Az LDROM kezdőcíme 0x00100000, oda égeti be az elkészült kódot. A numicro write_isp 0x300000 0xffffff7f sor a Config0 regisztert programozza be, ennek a 7-es bitje ha 0, akkor bútol az LDROM-ból a µC. A végeredmény:

A program természetesen nem fut. A fenti képről az derül ki, hogy ha a CBS bit be van programozva, akkor az LDROM tartalma 0x00000000 címre lapozódik, nem mennek sehova a vektorok! :) A numicro read_isp paranccsal ebben az esetben is „minden a helyéről olvasható”, de a sima memóriaolvasás az LDROM-ot a memória elején látja, a saját címéről nem is hajlandó olvasni. Ha a fenti OOCD-s konfigurációval az eredeti, 0x00000000 címre fordított kód van beprogramozva, akkor – természetesen – minden szépen működik. A kezdőcím eltolásos próbának annyi értelme azért volt, hogy ennek a működése is kiderült, máshova még jó lesz ez!

A numicro chip_erase parancs törli az egész FLASH tartalmat, beleértve a konfigurációs biteket is! Jó esetben ezzel meg lehet szabadulni egy beprogramozott Lock-bittől is, de ezt még nem mertem kipróbálni. :-D Az OpenOCD-ben közben találtam egy furcsaságot, amit egyelőre nem is sikerült megoldani. A fenti példákban a

reset run
halt

sorok állítják le a mikrovezérlőt, ezután működik a többi parancs. Viszont ez csak akkor jó, ha éppen van (és fut is) valami a FLASH-ben. Ha üres a tok, akkor folyamatosan valami hibára fut, ekkor ez a leállítás hatástalan. Ilyenkor a

reset halt

sor lesz megfelelő a fenti két sor helyett, de ezt vagy érdemes lenne „rendbe szedni”, vagy valahogy automatizálni kellene. Az utóbbi feladat valószínűleg nem lehetetlen, mivel az OpenOCD konfigurációs fájljai Tcl szkriptek, de a „rendbe szedés” inkább célravezetőbb. (Vagy lesz rá valami workaround; külön erase, utána meg mindig üres tokkal indul a programozás?) A csip RESET-elése még tesztelendő, mert most nem tudom, hogyan megy. Van ugye az nSRST vonal, de az SWD DAP-on keresztül is újraindítható a tok, hogy jelenleg melyik aktív, az OOCD konfiguráció függő.

Kezd ez a rész is elnyúlni, pedig rengeteg dolog van még vissza. Azok majd a következő rész(ek)re marad(nak). De! A jelenlegi kísérletezések végére egy erős figyelmeztetés ide kívánkozik... Az eddigi tesztkódok nem alkalmasak arra, hogy egy C-s projekt alapjai legyenek! Nagyon sok minden hiányzik, leginkább a linker-script oldalán, ugyanis nincsenek definiálva memória-részek. A veremmutató be van állítva, mivel a CPU RESET esetén felszedi a megfelelő helyről. Emiatt a függvényhívások működnek, illetve a függvényeken belüli (lokális) változók is használhatók, mert azokat a (beállított) verembe pakolja a fordított kód. Viszont a globális változóknak nincs memória kijelölve! A fordító – ennek hiányában – (gondolom) a programkód utáni részre helyezné el őket, de az ROM, nem írható. Aztán a C-s „startup” is hiányzik, emiatt nem történik meg a memóriatörlés sem, illetve az előre beállított változók rendberakása is hiányzik. Tehát a legjobb esetben is csak az asm példa használható, az eddigiek csak a toolchain próbálgatásai!

Link most nem lesz túl sok, az majd a következő részben. Ami hátra van: végleges(ebb) linker-script, amivel használható az eredeti startup-kód is, illetve legyen normális C-s használhatóság. Aztán az eredeti header(ek) használata a regiszterdefiníciókkal, sokat egyszerűsíti a perifériák kezelését. Meg a gyári programozási példák is jók lennének, legalább mintának. Esetleg CMSIS? (Bár erről az „erőforrások elpocsékolása” előbb jut az eszembe, mint az hogy „újrafelhasználható kód”, de mindegy.) Plusz egy komolyabb demó-projekt, amiben lesz legalább egy soros-port kezelés órajelállításokkal, engedélyezésekkel, miegyébbel. Izgalmas lesz, pláne, hogy csak fejben van meg. :)

A jelenlegi tesztekből azért csináltam egy csomagot, de a fenti figyelmeztetés értelmében csak saját felelősségre! A „gyökér”-ben van egy tchn.sh nevű fájl, ebben vannak megadva a használt toolchain-programok nevei. Ezekből egyszerű bármilyen verziót használni, belenézve a fájlba egyből látszik, hogy mit kell átírni benne ehhez. A tesztek könyvtárában van néhány „számnevű” szkript. Ebből az 1 egy tisztítást csinál, a 2 végzi a fordítást (használva az előbb említett tchn.sh-t), a 3 „diszasszemblál” illetve elkészíti a beégetni valót. Az esetleges 4 az OpenOCD-t indítja a tok programozásával, az 5 meg ugyanezt teszi a debughoz szükséges módon. A többi meg értelemszerű. Jelenleg három teszt van, a testcode1 az első, LED-kapcsolgatós példaprogram, a testcode2 az asm próba, a testcode3 pedig szintén a LED-kapcsolgatás, de ez a boot-ROM-os (LDROM) verzió.

Folyt.köv.

Linkek:

balagesz

---

2018.02.20.
2018.09.23. Elírás jav.
2019.02.17. Köv. rész link
2019.12.21. D8.
2024.08.25. Kép.jav. + elírások

Hozzászólások

Komoly írás, még nem tudtam végigolvasni. De megerősít abban, hogy csak akkor mozdulok el majd a 8 bites AVR-ekről, ha muszáj lesz...

"A csip fejlesztőinek itt azért beszólnék egy kicsit: a lábakon van felhúzó ellenállás, de közvetlenül nem kapcsolható, a „Quasi-Bidirectional” módban aktív csak, ha simán bemenetként használnám, akkor lehet „kerülgetni”. Ráadásul az összes port RESET esetén ebben az üzemmódban indul el, amit azért nem érzek túl szerencsésnek..."

Nem egyértelmű számomra - és nem néztem utána az adatlapon :-) - hogy melyik a default üzemmód? És miért szólsz be a fejlesztőknek?

A default üzemmód a Quasi-Bidirectional, RESET-kor abban indul a tok. Az adatregiszter persze 0xFFFF, tehát minden "kvázi-kimenet" magas, vagyis a belső felhúzó ellenállások miatt magas, ha az tud lenni. A szemöldökráncolás emiatt van; minden normális µC esetén "lebeg" az összes I/O láb RESET-kor, itt meg felhúzza őket egy ellenállás magasra. Ha kimenetnek használsz egy lábat, erre oda kell figyelni! Az azért a "mentségükre" szól, hogy ez a felhúzó ellenállás 100K nagyságrendű, de ezt jóval elegánsabban is meg lehetett volna oldani, persze IMHO.

(És igen, a 8 bites AVR nekem is az egyik kedvenc. :) )

Off: script logfile /bin/sh, majd a logfile tartalmát code-tagbe olvasztod, és így nem kell képekkel vacakolnod :)