Hlavní navigace

Kompilátor Forthu pro PIC řady 12 a 16

15. 12. 2010 12:00 (aktualizováno) Josef Pavlík

S programovacím jazykem FORTH jistě nemusím čtenáře roota seznamovat. Kdo častěji pracuje s chipy PIC firmy Microchip, brzy zjistí, že existuje několik kompilátorů Forthu pro tyto procesory, ovšem všechny jenom pro řady 18 a víc. Pro PIC řady 10, 12 a 16 nic. Rozhodl jsem se tedy tuto mezeru zaplnit vlastními silami. Řekněme si to upřímně, PIC není zrovna ideální processor pro tento typ jazyka, ale chtěl jsem to aspoň zkusit.

Řadu PIC10 můžeme zavrhnout hned. Tato řada se vyznačuje dvěma základními rysy: extrémě nízký počet nožiček a hlavně extrémě málo paměti. Jak ram, tak flash. Takže tady se nemá smysl o nic pokoušet. Ovšem s PIC12F a PIC16F už je situace jiná, tady už je víc prostoru. S processory řady 18 a vyššími jsem se zatím nezabýval. Jednak pro ně už kompilátory Forthu existují a druhak mají lehce odlišnou sadu instrukcí a nemám s nimi tolik zkušeností. Takže v současné verzi jsou podporovány jenom řady 12F a 16F.

Jako programovací jazyk jsem zvolil (jak jinak) Forth, konkrétně GForth. Výstupem kompilátoru je program v jazyce symbolických instrukcí, který je po průchodu jednoduchým optimalizátorem (v perlu) přeložen pomocí gpasm z balíku gputils. Kompilátor PicoForth tedy pro svůj běh vyžaduje GForth, gputils a perl. Všechny tyto programy jsou multiplatformní, takže PicoForth je použitelný i na alternativních operačních systémech, nejenom na Linuxu. Bylo by potřeba pouze přepsat script pro kompilaci z bash do bat, případně nainstalovat bash na windows.

Projekt je ve stádiu alpha, ovšem je už použitelný. Jeden reálný program jsem s ním už napsal a funguje. Výstupem překladu je hex file připravený na vypálení do chipu.

Program je možno stáhnout ze sourceForge.


Teď zhruba jak kompilátor funguje:

Hned na začátku je potřeba vyjasnit několik omezení, které nám PIC klade – jednak je to Harwardská architektura, takže program je ve Flash, data jsou v RAM. Program nemůže běžet v RAM, Flash se nedá programově modifikovat. Druhé omezení je tragicky krátký stack – jenom 8 levelů, navíc nepřístupný pro program. Je přístupný pouze přes instrukce call a return.

Takže oba stacky – datový a return stack, jsou implementovány softwarově. Rostou proti sobě, datový zdola nahoru, return stack shora dolů. Když se potkají, je to malér :-). 16ti bitové hodnoty jsou uloženy ve stylu Intel, nižší byte na nižší adrese. Prostor pro oba stacky je alokován staticky. Ideální by bylo naalokovat veškerou volnou paměť, ale to zatím kompilátor nedokáže. Snad časem.

Způsoby volání slov (funkcí) jsou v zásadě 3. Slovo může být definováno jako inline = v tomto případě je vkládáno přímo do kódu programu. Tento způsob je nejrychlejší, ovšem zabírá nejvíc programové paměti. Vyplatí se hlavně pro krátká slova a nebo naopak pro slova která se volají malý počet krát.

Druhý způsob volání je pomocí instrukce call. V programové paměti zabírá pouhý jeden až 3 wordy (záleží na počtu programových stránek, jestli je třeba plnit i registr PCLATH), ale problém je v délce hardwarového stacku, který má pouhých 8 úrovní.

Třetí způsob volání slov je přes softwarovou implementaci návratového stacku. Tento způsob zabírá řádově 8 wordů v programové paměti, ovšem je časově dost náročný. Je to ovšem jediný způsob jak dosáhnout vyšší úrovně vnoření slov.

Je tady ještě jeden způsob, jak by se dal přeložit program – v definici slova by byly pouze adresy volaných slov, program by byl vlastně přeložen do něčeho jako bytecode a ten by se potom interpretoval. Takto fungují klasické interpretry forthu. Nevýhoda je v tom, že tímto způsobem nemohou být přímo volána elementární slova (definovaná v assembleru) a i programové konstrukce (if, else, while atd.) se musí řešit jinak. Takže tento způsob překladu jsem zatím neimplementoval. Možná nekdy v budoucnosti.

Kompilátor si registruje, jakým způsobem je definováno které slovo, takže když je slovo zavoláno, vygeneruje korektní call nebo vyvolání makra. Konstanty jsou kompilovány jako literály.

Pro proměnné a speciální registry jsou automaticky generována optimalizovaná slova pro práci s touto proměnnou. Dejme tomu, že nadefinuji proměnnou foo.

var16 foo

automaticky mám k dispozici tato slova:

  • foo – uloží na stack 16ti bitovou adresu proměnné (adresu OR $c000, podle toho se pozná že je to adresa do RAM)
  • foo! – store – uloží 16ti bitovou hodnotu ze stacku do proměnné = optimalizovaná sekvence foo !
  • foo@ – fetch – vybere 16ti bitovou hodnotu z proměnné do stacku
  • foo-c! – char store – vezme ze stacku 1 byte a vloží do proměnné
  • foo-c@ – char fetch
  • foo-inc, foo-dec, foo+! – inc, dec a přičtení 16ti bitové hodnoty
  • foo-or , foo-and, foo-xor – vysvětlují se samy

podobně je možno definovat bity.

intcon 7 defbit gie \ definice bitu GIE, což je 7. bit registru INTCON

automaticky se vygenerují tato slova:

  • gie – uloží na stack bitovou masku, v tomto případě $80
  • gie@ – uloží true nebo false na stack, podle stavu bitu GIE
  • gie! – nastaví bit podle hodnoty na stacku
  • gie-set, gie-reset
  • gie-if, gie-0-if – podmínka podle bitu
  • gie-while, gie-0-while
  • gie-until, gie-0-until

Tato slova jsou definována interně v kompilátoru, do výsledného file jsou vložena už jako volání generického makra s parametry. Původně jsem každé slovo generoval do programu jako makro, které se mohlo a taky nemuselo nikde v programu použít. Ale v tomto případě měl průměrný program přeložený do assembleru délku řádově 25000 řádků! Takže v poslední verzi jsem tuto ideu opustil a pamatuji si tyto parametry v kompilátoru. Rychlost překladu se zrychlila o přibližně 30%, délka programu v assebleru se zmenšila řádově o 95% a výsledný kód samozřejmě zůstal stejný.

Při překladu se registruje stromová struktura programu a kompilátor je schopen zjistit která slova jsou v programu použita a která ne. Každé slovo v definici je uzavřeno mezi #ifdef slovo_REQ a #endif, kompilátor vygeneruje seznam použitých slov a ten se includne před definice. Assembler potom nepřekládá slova která nejsou použita.

Po překladu z forthu do assembleru přijde na řadu optimalizace. Optimalizují se pouze operace se stackem. Jestliže v programu použiji konstrukci typu literal16 >byte, zoptimalizuje se na literal8. Konstrukce literal16 var8!, která se interně přeloží jako literal16 >byte var8-c!, se zoptimalizuje na literal8 var8!. A konečně konstrukce var8@ jinavar8! se zoptimalizuje na var8-c@ jinavar8-c!.

Teď přijde na řadu assembler, přeloží všechna slova která jsou v programu potřeba. Následuje linker a pokud nedošlo k žádné chybě, máme k dispozici hex file.

Kdo to dočetl až sem, má u mě pivo :-)

Jestli si chce někdo pohrát, stáhněte ze sourceForge, hrajte si a dejte vědět jak to šlo. Nebo hledej přes google ‚picoforth‘.

Jesti má někdo zájem spolupracovat na tomto projektu, je tady ještě spousta práce. Viz file TODO.

Sdílet