Jestliže jste se rozhodli zainvestovat do nějakého profi debuggeru (Atmel-ICE nebo AVR Dragon) nebo využíváte služeb mEDBG na Xplained deskách, bude vás čtení této kapitoly asi velice těšit.
Nejspíš se mnou budete souhlasit, že debugování je věčná bitva mezi programátorem a chybičkami zakuklenými v programu. Chyby se dle Murphyho zákonů schovávají a udeří většinou v tu nejnevhodnější chvíli. Jsou v tom úplnými mistry a používají celou paletu triků jak vás zmást. Čím častěji se s nimi budete setkávat, tím pevněji budete věřit, že v každém programu je vždy nějaká chyba. Program, který beží není bezchybný. Nějaká zákeřná chyba v něm čeká aby se projevila později, pokud možno až potom co zapomenete jak program pracuje. V této věčné bitvě, ale nebudete bezbraní. Zvu vás na prohlídku arzenálu, které vám Atmel studio a debuggery nabízí.
Základní prostředek který velice dobře poslouží k hledání chyb v algoritmech. Krokujete si prográmek instrukci za instrukcí a sledujete obsah proměnných. Zdlouhavější procedury jako třeba smyčky, můžete v pohodě přeskočit takže vás nebudou zdržovat. Ideálně se doplňuje s možnostmi Watch a Autos. Jde o natolik intuitivní prostředek, že si nezaslouží příliš dlouhý komentář. Proto ve stručnosti:
Funkce slouží k přehledné kontrole nad obsahem proměnných. Najetím kurzorem nad proměnnou se zobrazí její aktuální hodnota. Pokud pak kliknete na ikonku "špendlíku", můžete si malé okno připnout kamkoli do prostoru zdrojového kódu (viz obrázek W1). Máte pak přehled o stavu proměnné kdykoli program zastavíte. Přirozeně můžete její hodnotu i měnit. V okně Watch 1 můžete vidět, že sledovat nemusíte jen proměnnou, ale i libovolný výraz (jehož hodnotu vypočte vždy Atmel studio). Proměnnou můžete do okna Watch 1 přidat několika způsoby. Buď ji najdete ve zdrojovém kódu, kliknete na ni pravým tlačítkem a zvolíte Add to Watch. Nebo v okně Watch 1 kliknete na prázdný řádek a ručně do něj vypíšete název proměnné nebo výraz. Mazat sledované výrazy pak můžete kliknutím pravého tlačítka a zvolením Delete watch. O formátování výrazů se můžete více dozvědět zde. Jak vidno, můžete volit z mnoha formátů zápisu. Dávejte si ale pozor, výrazy mohou měnit stav programu. Například zápisem y=x, dojde k přepsání proměnné y v cílové aplikaci !
Breakpointy můžete umisťovat na libovolné instrukce v programu a slouží k tomu aby se program zastavil v okamžiku kdy má vykonat instrukci na níž se breakpoint nachází. Typicky ho využijete v situaci, kdy potřebujete ručně odkrokovat nějakou rutinu nacházející se uprostřed programu. Umístíte na ni breakpoint, spustíte program, počkáte než dojde na inkriminované místo a pak odtamtud můžete s přehledem krokovat a sledovat dění. Velice ho oceníte v situacích, kdy program reaguje na vnější nepředvídatelnou událost. Dejme tomu na externí přerušení. Stačí do rutiny přerušení umístit breakpoint a jakmile je zavolána, program se vám zastaví a můžete vesele kontrolovat jeho chod opět krokováním. U složitějších aplikací se bez breakpointů v podstatě neobejdete. Nastíním několik typických případů kdy vám může výrazně zjednodušit práci. Dejme tomu, že mikrokontrolér komunikuje s PC a na základě jeho pokynů vykonává nějakou akci (třeba pohybuje motorem). Čas od času se vám stane, že se motor pohne do polohy do které by se dostávat neměl. A vy nevíte zda je to tím že PC odeslalo falešný pokyn, nebo zda je chyba ve vašem programu. Nastavíte proto breakpoint na kus kódu, který může s motorem pohnout do polohy kde ho nechcete a spustíte ho. Jakmile kritický okamžik nastane, program se vám zastaví a vy si můžete prohlédnout obsah proměnných a typicky i zprávu od PC a vyhodnotit, zda je na vině PC nebo váš program. A případně i proč to program udělal. A takových situací je hodně, jejich řešení bývá na dlouhé hodiny a breakpointy vám mohou výrazně ušetřit čas. Na obrázku B1 vidíte ukázku použití. Program se má zastavit v rutině přerušení na instrukci x++ (označené červeným kolečkem vlevo). Poté co se tam zastavil jsem udělal jeden krok a vy vidíte aktuální pozici programu označenou žlutou šipkou vlevo. V okně Watch 1 můžete vidět že proměnná x má hodnotu 1 (tedy byla poprvé inkrementována). V okně Breakpoints pak máte seznam všech breakpointů. Zde je můžete jim můžete měnit parametry, případně je povolovat nebo zakazovat.
Mnohdy nebudete chtít program zastavit pokaždé když projde příslušnou instrukcí. Budete chtít aby se na ní zastavil jen pokud je splněna ještě nějaká další podmínka. V takovém případě můžete využít podmíněný breakpoint. V okně Breakpoints klikněte pravým tlačítkem na vámi vybraný breakpoint a zvolte <>Stettings. Objeví se před vámi okno jako na obrázku B2. V něm můžete specifikovat podmínku, já zvolil že se y má rovnat třem. Spustíte-li takový program, zastaví se vám v okamžiku kdy je y=3 a program došel k instrukci x++. Nejspíš sami uznáte, že má takový nástroj značný potenciál. Mezi podmínkami je i položka Hit count. Tou můžete specifikovat kolikrát musí program breakpointem projít aby se zastavil. A to není zdaleka všechno, položce Hit point můžete namísto konkrétní hodnoty nastavit "násobek". Jinak řečeno můžete říct že se má program zastavit když počet průchodů breakpointem dosáhne nějakého násobku třeba 13ti. Dále namísto podmínky, můžete spustit "akci". Akcí se rozumí výpis do okna Output a hezkou ukázku můžete vidět na obrázku B3. Breakpoint jsem nastavil tak že s každým jeho průchodem by se do okna OUTPUT měla vypsat hodnota proměnné x ve tvaru "The value of x is {x}". Výraz "$Function" zařadí před výpis informaci o tom, ve které funkci se program v tom okamžiku nacházel. Tady konkrétně vidíte, že šlo o vektor přerušení s číslem 14. To by mělo být přerušení od přetečení čítače 0. Proměnnou, kterou chcete vypsat dávejte do složených závorek. Pro další možnosti využijte jsem nápovědy v okně (modré "i" v kroužku).
Data breakpoint vám umožňuje sledovat nějakou proměnnou v paměti. Dejme tomu, že máte globální proměnnou, jejíž hodnotu mění více nezávislých procesů. Pokud by jste chtěli zastavit program před jakýmkoli zápisem do této proměnné, nebyl by pro vás klasický breakpoint zrovna nejvhodnější nástroj. Museli by jste totiž breakpointy rozmístit na všechny instrukce, které k proměnné přistupují a to vůbec nemusí být snadné (o časové náročnosti vůbec nemluvím). Proto máte k dispozici Data breakpoint. Ten nastavujete na nějakou pozici v paměti, takže musíte vědět kde se vaše proměnná v paměti nachází. To můžete snadno zjistit z okna Watch. Pak zvolíte Acces mode v němž rozhodujete zda se má program zastavit při zápisu do proměnné, při čtení nebo při libovolné z těchto dvou akcí. A pokud chcete, můžete ho vybavit další podmínkou řešící hodnotu proměnné. V příkladu na obrázku B4 můžete vidět data breakpoint, který má zastavit program jakmile se do proměnné y (s adresou 0x2000) zapíše hodnota větší nebo rovna 9. Všimněte si, že se program zastavil na další instrukci hned po zápisu do y.
Stejně jako běžný breakpoint i data breakpoint může mít podmínky nebo vykonávat akce. Pokud v okně Data Breakpoint kliknete na vybraný breakpoint pravým tlačítkem, můžete specifikovat další podmínky. V mém případě (obrázek B5) třeba Hit count. Program by se měl zastavit při třetím okamžiku kdy je v proměnné x hodnota 7. Což se soudě dle výsledků patrných v okně Watch stalo. Případně můžete po kliknutím pravého tlačítka zvolit položku When hit a specifikovat akci, kterou má studio udělat. Například do okna Output vypsat nějakou informaci.
V okně Autos slouží k efektivnímu sledování proměnných během krokování (typicky nějakého algoritmu). Studio samo přidává do okna proměnné a registry které se účastnily minulé a následující instrukce. Díky tomu nemusíte složitě ručně přidávat všechny proměnné, které se nějak vyskytují v algoritmu. V okně Locals zase můžete sledovat stav lokálních proměnných (například v těle funkce).
Live Watch umožňuje sledovat aktuální stav proměnné i za běhu programu. Kliknutí pravým tlačítkem a volbou Show history lze sledovat i historii s informacemi o tom kdy byla její hodnota přečtena a s jakým výsledkem (obrázek L1). Případně kliknutím na ikonku diskety je možné ukládat výsledek sledování do souboru. Dalším parametrům už bohužel nerozumím. Nemám ale představu do jaké míry je tato metoda invazivní. Tedy jak moc může live sledování narušit chod programu. To by mohl být problém u časově kritických aplikací.
Některé čipy řady Atmega, vybavené JTAG rozhraním mají speciální OCDR registr, který slouží k předávání uživatelských data do debuggeru. Aplikace zapíše do OCDR registru jeden byte a debugger jeho obsah pravidelně kontroluje. Pokud tam nalezne data, přepošle je do Atmel studia. To by je mělo v okně Output zobrazit. Pro zobrazení je potřeba v roletkovém menu zvolit IDR Messages. Jak často debugger testuje obsah OCDR registru netuším, někde jsem četl že každých 50ms, jinde každých 100ms. Přirozeně nemá význam zapisovat do toho registru častěji. Váš program může sledováním nejvyššího bitu OCDR registru zjistit zda byl debugerem přečten a podle toho se zařídit. Bližší informace jak na to najdete v datasheetu. Spolu s hodnotou OCDR vám Studio zobrazí i čas, kdy zprávu debugger vzvedl. Sběr data probíhá stále, tedy ať už program běží nebo ho krokujete. Vzhledem k tomu, že jsem jej objevil před nedávnem, nemůžu vám říct zda a kde tento prostředek prakticky použijete. Dávejte si ale pozor na jednu nepěknou vlastnost studia. Když jsem neměl okno Output dost "roztažené", nevešlo se mi tam roletkové menu v němž si IDR zprávy vybíráte. Když se tam nevejde, Atmel ho nezobrazí vůbec a pak máte dojem, že tam prostě nic není. Tato banální záhada mě stála skoro hodinu laborování :D
Veledůležité je okno I/O. Tam můžete sledovat a měnit stav všech speciálních funkčních registrů. Díky tomu máte kompletní přehled o dění ve všech periferiích. Všechny změny od posledního zastavení programu se v tomto okně zobrazují červeně. Díky tomu nemusíte držet v paměti stavy jednotlivých bitů a stačí vám sledovat, které zčervenaly a víte co se změnilo. Nezapomeňte, že tímto nástrojem můžete ovládat všechny registry, tedy i ty zodpovědné za ovládání portů. Což neznamená nic jiného než že pouhým kliknutím myší a nastavením nebo vynulováním registru PORTx můžete nastavit logickou hodnotu na výstupu. A ať už k němu máte připojenu LED, motor nebo relé, máte ho díky tomu pod plnou kontrolou. Za zmínku možná stojí i okno Immediate, které slouží jako chytrá kalkulačka. Můžete zde zapisovat výrazy, které chcete ihned vyhodnotit. Do jaké míry ho ale využijete netuším.
Práce s výše uvedenými nástroji má bohužel i svá omezení. To nejokatější vyplývá z optimalizace programu. Překladač může často vyhodnotit, že vaše proměnná nemá v programu moc smysl a v rámci šetření pamětí ji prostě vyřadí. Přirozeně tak aby chod programu zůstal stejný. V takovém případě logicky nemůžete pomocí funkcí Watch, Autos, Locals nebo Actions sledovat její obsah. Pokud si i přes to proměnnou přidáte třeba do okna Watch, dozvíte se od Atmel studia, že její obsah není možné zobrazit, nebo vám někdy i řekne, že proměnná byla při optimalizaci odstraněna. Podobná omezení vyplývají i pro krokování a breakpointy. Pokud překladač v rámci optimalizace některou část kódu upravil, nemůžete tuto oblast krokovat ani na tyto instrukce umísťovat breakpointy. Protože ty instrukce prostě v programu nejsou. Pokud i přes to umístíte na takovou instrukci breakpointy, bude jeho symbol vyplněn bílou barvou a přibude u něj ikonka žlutého trojúhelníku.
Breakpointy pak podléhají dalším omezením. Například čipy s rozhraním DebugWIRE nemají vůbec žádné breakpointy. Tedy ani datové a podmíněné. Breakpoint se u nich realizuje pomocí instrukce BREAK, kterou debugger vloží na odpovídající místo v programu. To ale znamená, že pro vložení této instrukce je potřeba přepsat část paměti programu. To je relativně zdlouhavá operace a nesmíte se proto divit, že to Atmel studiu (respektive Debuggeru) trvá (zvláště pokud čip taktujete nízkou frekvencí). Pokud to tedy situace umožňuje využívejte namísto breakpointu příkaz "run to cursor". Když potom k breakpointu přidáte nějakou akci, jako třeba výpis obsahu proměnné, musíte si uvědomit, že je to invazivní metoda. Debugger počká až se program zastaví na breakpointu, nechá ho nějakou dobu pozastavený a mezi tím vyčte obsah proměnné. Chod programu se tedy mění.
Podmíněné breakpointy jsou invazivní metoda. Pokud program na breakpoint narazí, zastaví svůj chod, Debugger vyhodnotí jestli je splněna podmínka a nechá program pokračovat nebo jeho chod zastaví (klidně na 50ms). Na breakpoint se tedy dívejte tak že se na něm program vždy zastaví ! Atmel studio ale po provedení akce případně zkontrolování podmínky program zase pustí aby běžel dál. Totéž platí i pro podmíněný data breakpoint. A stejně tak i Live Watch je invazivní metoda, která může radikálně narušit chod programu
Omezení jsou kladena také na množství breakpointů. Některé málo vybavené čipy jak už víme nemají žádné, vybavenější čipy jako třeba Atmega32, mají 2-4 breakpointy podle typu. Některé Atxmegy mají neomezené množství breakpointů. Je proto dobré si u konkrétního čipu prohlédnout v datasheetu kapitolu o možnostech debugu.
Další zajímavé omezení potkáte při debugování rozhraním PDI (čipy Atxmega). I když máte program pozastavený, mohou na pozadí běžet například čítače (pokud to povolíte). Případně můžete využívat výstupu CLKOUT k taktování nějakého vnějšího obvodu. Pak vás jistě bude zajímat, že takt procesoru (tedy i pro čítače i pro výstup CLKOUT) se během pozastavení programu mění a to na hodnotu kterou si můžete navolit v Projetct->Properties->Tools. Například u ATMEL-ICE můžete volit frekvenci v rozsahu 32kHz-7.5MHz. Změna clocku je tedy další věc, která by vás mohla překvapit.
Mějte tedy vždy na paměti že proces debugování může rapidně narušovat plynulost chodu programu. A vždy zvažte co to může způsobit. Například v časově nenáročné aplikaci, kde program vyčítá data ze senzoru, zpracovává je a posílá do PC, vás zdržení v řádu desítek až stovek milisekund nemusí moc trápit. Pokud ale program například v rámci přerušení musí rychle zareagovat na vnější událost můžou jeho reakci invazivní metody debugu výrazně zpomalit.
Může se vám stát, že někdy budete stavět zařízení třeba s vícero mikrokontroléry. Případně, a to je pravděpodobnější, s mikrokontrolérem a hradlovým polem nebo CPLD. Pokud budou mít všechna zařízení rozhraní JTAG, můžete za určitých okolností programovat obě dvě skrze jedno rozhraní (jeden konektor). Vysvětlím (a předvedu) to na dvou mikrokontrolérech. Dejme tomu že máte desku kde pracují dva Atmely (řekněme Atmega16). Oba vybavené rozhraním JTAG. Je pravděpodobné, že budete chtít oba Atmely nejen programovat, ale i nějak debugovat. Vybavíte tedy svou desku dvěma JTAG konektory a během ladění programu budete střídavě přepojovat debugger podle toho, který z čipu zrovna potřebujete debugovat. A přesně tomuto se můžete vyhnout. Zařízení na JTAG lze řetězit za sebe. Schéma spojení můžete vidět na obrázku CH1.
Signály TCK a TMS jsou společné všem čipům, signál TDI vedoucí z debuggeru se připojí na vývod TDI prvního čipu. Vývod TDO prvního čipu se připojí na TDI druhého čipu. A tak stále dokola až u posledního čipu se vývod TDO připojí do debuggeru na pin označený TDO. Já přidal na všechny linky (TCK,TMS,TDI,TDO) vedoucí k debuggeru ještě pull-up rezistory o hodnotě 10k, protože bez nich mi spojení padalo. S takovou konfigurací můžete debuggovat i programovat libovolný čip řetězce. Je ale nutné Atmel studiu sdělit základní parametry řetězce. V okně Project -> Properties -> Tool vyberete programátor (Atmel-ICe nebo Dragon), rozhraní JTAG a v sekci JTAG Daisy chain settings zvolíte manual. Pak musíte Atmel studiu sdělit kde v celém řetězci se vaše zařízení nachází. Studio po vás bude chtít vědět kolik zařízení se nachází před tím vaším a kolik se nachází za ním. Máte-li tedy například tři zařízení a chcete programovat to prostřední, tak se před vaším zařízením nachází jedno jiné zařízení a za ním také jedno. Pokud budete chtít ze tří programovat ten poslední, pak se před ním nachází dva a za ním žádné. Já mám zařízení jen dvě (Atmegy16). A schválně, kdo uhodne z následujícího obrázku (CH2), které z nich chci programovat...
... uhodli jste. Je to první zařízení v řetězci. Počet zařízení před ním je 0 a počet zařízení za ním je 1. Každé zařízení má ještě několik tzv Instruction bits. To se může pro různá zařízení lišit, takže vám můžu říct jen to že čipy rodiny AVR mají 4. Pokud to dobře chápu, musíte počet Instruction bits před nebo za vaším zařízením sečíst. Raději řeknu opět na příkladu, máte-li řetězec složený ze tří AVR čipů a chcete-li ovládat poslední z nich, musíte do kolonky Instruction bits before zapsat hodnotu 8. Protože se před vaším zařízením nachází dvě AVR, každé se 4mi Instruction bits. Pokud budete mít v řetězci nějaké jiné zařízení (například CPLD), musíte se z jeho datasheetu dočíst kolik IR bitů má a počítat s nimi. Jen pro ilustraci jsem testovací zapojení se dvěma Atmely v řetězci vyfotil a můžete ho vidět na obrázku CH3.
Debugování skrze delay funkce je problematické a většinou je musíte "přeskočit" buď pomocí breakpointu nebo pomocí Run to Cursor. Ale jistě víte, že delay funkce se používají opravdu výjimečně jen na nějaké rychlé testování. Otravné chování při debugu vás bude o to víc motivovat pracovat bez nich. Také vás musím upozornit, že tak jak všechno na počítači i debug se občas zasekává. Někdy pomůže vytáhnout programátor z USB, jindy restart studia :D