Při programování STM8 používáme knihovnu SPL. Napíšeme
GPIO_WriteHigh(LED_PORT, LED_PIN) a LED se rozsvítí. Jak to ale doopravdy funguje?
Každá periferie mikrokontroléru se ovládá
zápisem čísel do registrů.
SPL jen schovává tyto zápisy do funkcí s hezkými jmény.
Tento článek vysvětluje, co ty registry jsou, kde je v dokumentaci najít a jak s nimi pracovat přímo. Konkrétní příklad s LED a tlačítky (SPL vedle přímého zápisu) je v repositáři SFR na GitLabu.
Co je to SFR¶
SFR (Special Function Register, „registr zvláštní funkce“) je registr, který nepatří procesoru, ale periferii: portu, časovači, UARTu, převodníku. Registry procesoru (akumulátor, ukazatel zásobníku) se v C neukazují. SFR ano.
Základní myšlenka se jmenuje memory-mapped I/O: registry periferií jsou v paměťovém prostoru procesoru, na pevných adresách. Procesor s nimi pracuje jako s paměťovými buňkami. Zápis do takové buňky ale neuloží číslo, nýbrž něco udělá: rozsvítí pin, spustí časovač, odešle bajt.
Zhruba vypadá paměť STM8S208 takhle:
1 2 3 4 5 | |
Přesné rozmístění pro váš čip najdete v datasheetu v kapitole o paměťové mapě.
Pro nás je důležité: například 0x501E není nikde v RAM. Je to bajt, který patří
portu G.
Jak vypadá periferie¶
Periferie je několik registrů těsně za sebou. Port GPIO má pět registrů:
| offset | registr | význam |
|---|---|---|
| +0 | ODR |
co se vysílá na výstup |
| +1 | IDR |
co se čte ze vstupu |
| +2 | DDR |
směr (0 = vstup, 1 = výstup) |
| +3 | CR1 |
řízení 1 |
| +4 | CR2 |
řízení 2 |
Port G začíná na adrese 0x501E, takže ODR je na 0x501E, IDR na 0x501F,
DDR na 0x5020 a tak dále. Každý registr má 8 bitů a každý bit většinou
patří jednomu pinu (bit 3 pinu PG3). Jinde bity znamenají „zapni“, „příznak“,
„vyber režim“.
Jména registrů jsou zvykem napříč výrobci: CR (Control), SR (Status),
DR (Data), IER (Interrupt Enable), PSCR (Prescaler), ARR
(Auto-Reload). Když se je naučíte, poznáte je u každé periferie.
Jména bitů¶
U portu G je jméno bitu jednoduché: bit 3 registru ODR patří pinu PG3. Většina
periferií ale bity používá jinak. Každý bit (nebo skupina bitů) v registru má
vlastní jméno a význam. Ukážeme to na časovači TIM4, kterého se budeme držet
i v dalších kapitolách. Jeho registr CR1 (Control Register 1) vypadá v manuálu takto:
| bit | jméno | význam | maska |
|---|---|---|---|
| 7 | ARPE |
Auto-Reload Preload Enable | 0x80 |
| 6–4 | – | nepoužito | |
| 3 | OPM |
One Pulse Mode, čítač se po přetečení zastaví | 0x08 |
| 2 | URS |
Update Request Source | 0x04 |
| 1 | UDIS |
Update Disable, zákaz události update | 0x02 |
| 0 | CEN |
Counter Enable, zapnutí čítače | 0x01 |
Registr TIM4_CR1 je tedy jedno číslo, ale v manuálu o něm přemýšlíme po bitech: „nastav CEN“ znamená „zapiš 1 do bitu 0 registru CR1 časovače TIM4“. Jména
jsou zkratky anglických názvů a stejně jako jména registrů se opakují
u různých periferií (CEN najdete také u UARTu, SPI nebo I2C).
Hlavičkový soubor stm8s.h z SPL tato jména převádí do C. Pro každý bit je tam
maska, číslo s jedničkou na pozici bitu, pojmenované podle vzoru
PERIFERIE_REGISTR_BIT:
1 2 3 4 | |
Zápis TIM4->CR1 |= TIM4_CR1_CEN; dělá totéž co TIM4->CR1 |= 0x01;. (TIM4->CR1 je přístup k členu CR1 struktury přes ukazatel TIM4, viz kapitola
Registr v jazyce C.) Jméno masky je ale čitelnější:
čtenář hned vidí, co se děje. Číslo 0x01 je „magické číslo“, o kterém se musí
hádat, kterého registru a bitu se týká. Jména bitů tak odpovídají manuálu
a v kódu se dají rovnou vyhledat. U GPIO jsou masky pinů pojmenované zvlášť:
GPIO_PIN_3 je 0x08, tedy bit 3.
Hlavička obsahuje také hodnoty po resetu (…_RESET_VALUE), například
TIM4_CR1_RESET_VALUE.
SPL: vrstva nad registry¶
SPL (Standard Peripheral Library) nabízí funkce pro všechno. Žádná
magie v nich ale není. Podívejme se, co dělá GPIO_WriteHigh:
1 2 3 4 | |
Jeden řádek: nastav bity v registru ODR. A složitější GPIO_Init je prostě
posloupnost zápisů do DDR, CR1 a CR2 (plus ODR pro počáteční úroveň):
1 2 3 4 5 | |
Zdrojové kódy SPL jsou k dispozici (u nás ve složce ../SPLSPL/src) a stojí za to
do nich občas nahlédnout. Nejlepší učitel je zdroják funkce, kterou právě voláte.
| SPL | přímý zápis | |
|---|---|---|
| čitelnost | jména funkcí vypovídají, co se děje | musíte znát registry a bity |
| kontroly | assert_param odchytí špatný parametr |
žádné, překladač věří, co napíšete |
| velikost a rychlost | volání funkce, kontroly navíc | minimální, často jedna instrukce |
| přenositelnost | stejné API pro více čipů | závisí na čipu |
| co se naučíte | jak použít knihovnu | jak čip skutečně funguje |
Žádná varianta není „správná“. SPL je pohodlná a bezpečná. Přímé registry potřebujete, když SPL funkce chybí, nedělá přesně to, co chcete, nebo prostě potřebujete vědět, co se děje. A když čtete cizí kód nebo datasheet, rozhodně potřebujete rozumět oběma.
Kde najít informace¶
U každého čipu jsou k dispozici tři hlavní dokumenty. Každý slouží k něčemu jinému.
Datasheet (u nás STM8S207xx/STM8S208xx): konkrétní čip.
- vývody, jejich alternativní funkce, napájení, elektrické parametry,
- paměťová mapa a tabulka všech registrů s adresami a hodnotami po resetu (kapitola Memory and register map).
Reference manual (RM0016): celá rodina STM8S, popis periferií.
- pro každou periferii kapitola s popisem funkce,
- na konci kapitoly podkapitola Registers: každý registr po bitech.
Programming manual (PM0044): instrukční sada procesoru (ld, bset,
bres, …). Potřebujete ji, když čtete výstup překladače.
K tomu někdy errata: seznam chyb v křemíku a jejich obejití. Pokud je něco nelogické, tady může být odpověď.
Jak číst popis registru¶
Podívejme se, jak to vypadá, když potřebujeme vědět, jak zapnout pull-up na pinu. Postup je pokaždé stejný:
- V RM0016 najdeme kapitolu o portech (I/O ports).
- Najdeme tabulku, která říká, jak se kombinací
DDR,CR1aCR2nastavuje režim pinu. Vstup s pull-upem jeDDR = 0,CR1 = 1,CR2 = 0. - Podkapitola GPIO registers popisuje
Px_CR1po bitech. - V datasheetu ověříme, že pin existuje a že nemá nějakou zvláštní vlastnost.
Tabulka jednoho registru obvykle obsahuje:
- adresu (nebo offset od začátku periferie),
- reset hodnotu: hodnota po zapnutí. Mnohé registry jsou po resetu nula, ale ne všechny!
- bity: pozice, jméno, popis,
- přístup u každého bitu:
| zkratka | význam |
|---|---|
r |
jen čtení |
w |
jen zápis |
rw |
čtení i zápis |
rc_w0 |
čtení; zápisem 0 se bit vynuluje, zápis 1 nedělá nic (typické u příznaků) |
r s vedlejším efektem |
samotné čtení něco změní (viz níže) |
Přesné značení se může u jednotlivých registrů lišit, vždy ho najdete v legendě na začátku kapitoly o registrech.
Registr v jazyce C¶
Nikdo nechce psát *(volatile uint8_t *)0x501E = 0x01;. Kdo má SPL,
dostane překlad z manuálu do C zdarma. Zápis GPIOG->ODR totiž není žádná
magie ani zvláštní syntaxe pro mikrokontroléry. Je to obyčejný jazyk C:
GPIOGje ukazatel na strukturu, která leží na adrese registrů portu G.ODRje člen této struktury a šipka->` k němu přistupuje přes ukazatel.
Šipka je jen kratší zápis. GPIOG->ODR je to samé, jako kdybychom napsali
(*GPIOG).ODR: nejdřív se ukazatel dereferencuje (*GPIOG je celá struktura
na adrese 0x501E) a pak se vybere člen ODR. Závorky jsou nutné, protože tečka
má vyšší prioritu než hvězdička.
Struktura odpovídá tabulce registrů z manuálu (pořadí členů = offsety).
V souboru stm8s.h najdete:
1 2 3 4 5 6 7 8 9 10 | |
Zápis v C a skutečný registr v paměti si odpovídají takto:
1 2 3 4 5 6 | |
Tři věci je dobré pochopit:
1. Struktura popisuje, co v paměti je. Nic nevytváří. GPIO_TypeDef říká
„od této adresy leží pět bajtů: ODR, IDR, …“. Překladač podle pořadí členů
spočítá adresy.
2. GPIOG je ukazatel na takovou strukturu. Není to proměnná v RAM ani
struktura, kterou bychom vytvořili. Je to konstanta: číslo 0x501E přetypované
na „ukazatel na GPIO_TypeDef“. Přetypování (GPIO_TypeDef *) 0x501E říká
„na adrese 0x501E leží GPIO_TypeDef“. Ukazatel na strukturu jste už možná
používali. Zde se liší jen tím, že adresa není z malloc ani z &proměnná, ale
pevná, daná křemíkem a uvedená v datasheetu. Proto se členy píšou šipkou:
1 2 | |
Totéž platí pro všechny ostatní porty a periferie. GPIOA, GPIOB, …
jsou ukazatele na stejný typ GPIO_TypeDef, jen s jinou základní adresou. Proto
funguje jedna funkce GPIO_WriteHigh(GPIOx, …) pro všechny porty. Podobně je
TIM4 ukazatel na TIM4_TypeDef a TIM4->CR1 je registr CR1 časovače TIM4.
3. __IO je volatile. Hodnota registru se může změnit sama (tlačítko,
časovač) a každý zápis má účinek. volatile zakazuje překladači čtení
vynechat, zapamatovat si hodnotu nebo sloučit dva zápisy. Bez něj by se mohlo
stát, že třeba čekací smyčka while (!(UART1->SR & UART1_SR_RXNE)); se
zoptimalizuje na nekonečnou.
Hlavička obsahuje i masky bitů a hodnoty po resetu pro všechny periferie
(viz Jména bitů), takže se v kódu nemusí používat „magická čísla“ jako 0x01.
Struktura závisí na čipu
Některé čipy mají registry na jiných offsetech. Třeba TIM4_TypeDef má u
STM8S103 dva rezervované bajty navíc. Proto struktury v stm8s.h obsahují
#if defined(STM8S103) a proto musíme při překladu předat správný čip (-DSTM8S208).
Operátory pro práci s bity¶
Registr je 8bitové číslo. Aby se změnil jeden bit a ostatní zůstaly, používáme
bitové operátory. Šipka (->) je jen způsob, jak se k registru dostat. To, co
s ním děláme, jsou operátory jazyka C.
1 2 3 4 5 6 | |
Složené a |= b je zkratka za a = a | b, stejně &=, ^=.
Maska je číslo, které má jedničky tam, kde nás bity zajímají. Bit číslo n má masku (1u << n). Čtyři úkony, které použijete pořád:
1 2 3 4 | |
Příklad po bitech, zapnutí bitu 2:
1 2 3 | |
A vynulování:
1 2 3 | |
= proti |=¶
1 2 | |
= přepíše všech 8 bitů. |= registr přečte, změní bity a zapíše zpátky
(read-modify-write). Pro bity „na jednom pinu“ to skoro vždy chceme.
Rovnítko se hodí při inicializaci celé periferie.
Pozor na tři úskalí:
&není&&.&pracuje po bitech,&&je logické „a zároveň“ a vrací 0 nebo 1.~převede operand naint(16 bitů na STM8), takže~(uint8_t)0x04je0xFFFB. Při zápisu do 8bitového registru se horní bajt zahodí, ale čistší je(uint8_t)~MASKA.- SDCC hlásí u konstant jako
(1 << 7)předávaných douint8_twarning 158 (přetečení znaménkové konstanty). Pište(1u << 7).
Co z toho udělá překladač¶
Zápis ODR |= 0x01 vypadá jako tři operace (číst, OR, zapsat). Překladač ale ví,
že STM8 má instrukce pro práci s jedním bitem v paměti. Takže při konstantní masce:
1 2 3 4 | |
SDCC z toho udělá:
1 2 3 | |
Jedna instrukce, žádná proměnná. Když je ale maska proměnná (nebo má více bitů), výsledek je klasické čtení, OR a zápis:
1 2 3 | |
I ten je ale pořád jen pár instrukcí, mnohem menší a rychlejší než volání SPL funkce
s kontrolami. Výstup překladače si můžete prohlédnout v souboru build-*/main.asm
(naše Makefile ho tam nechává).
Další příklad: časovač TIM4¶
Princip je u každé periferie stejný. Podívejte se, co dělá nastavení časovače,
které používáme pro milis() (viz článek o časovačích).
S SPL:
1 2 3 4 | |
Totéž přímo (to, co SPL funkce uvnitř dělají):
1 2 3 4 5 | |
Všimněte si, že tři registry se zapisují jinak:
PSCRaARRse prostě přepíšou (=): jsou to celá čísla.IERaCR1jsou sbírkou jednotlivých bitů, tak se použije|=.SR1je stavový registr s příznaky typurc_w0: příznak se maže zápisem 0, zápis jedničky nedělá nic. Proto stačí zapsat~UIF. Ostatní příznaky tím nepoškodíme (zapisujeme jim jedničky) a nemusí se nic číst.
Z manuálu je ten popis vidět hned: stačí otevřít kapitolu o TIM4 v RM0016 a registr po registru ověřit.
Pár dalších zvláštností¶
Registry s vedlejším efektem při čtení. U některých registrů čtení něco
změní: čtení UART1->DR vyjme přijatý bajt a smaže příznak, čtení některých stavových
registrů smaže příznaky. Takový registr nikdy nečtěte „jen tak“, třeba v debuggeru
nebo makrem, které hodnotu čte dvakrát. Na to naše ladicí nástroje pamatují
a tyto registry samy nečtou.
16bitové hodnoty jsou dva registry. Například výsledek převodu ADC je v ADC2->DRH
a ADC2->DRL, 16bitový čítač časovače TIM2 v CNTRH a CNTRL. Pořadí čtení nebo
zápisu je v manuálu dané (typicky nejdřív horní bajt) a jeho porušení dá chybné hodnoty.
Reset hodnoty. Po resetu má většina registrů hodnotu nula, ale ne všechny. Vždycky se podívejte do sloupce reset value. A když nastavujete jen jeden bit, spoléháte se na to, že ostatní jsou už správně.
Pořadí inicializace. Periferie se většinou nejprve nastaví a teprve pak
zapíná (CEN, ADON, …). U GPIO se například doporučuje nejdřív nastavit
ODR a pak DDR, aby při přepnutí na výstup nevylétl krátký špatný impuls.
To je přesně to, co dělá GPIO_Init.
Registry v debuggeru¶
Ladicí nástroje z našeho toolchainu znají jména registrů (viz Malý kurz STM8-GDB). V gdb tedy můžete psát:
1 2 3 4 5 | |
Je to nejrychlejší způsob, jak si ověřit, že kód nastavil to, co mělo, a jak
se naučit, co která hodnota dělá. Zkuste zastavit program a prohlédnout
sfr GPIOB, zatímco držíte tlačítko.
Co dál¶
- Podívejte se na příklad SFR: LED a tlačítka
na daughterboardu, každá funkce napsaná pomocí SPL i přímo. Zkuste v něm přepínat
USE_REGISTERS. - Zkuste si nejprve zjistit z manuálu, co by udělalo zapsání do
CR2u výstupního pinu. A pak to ověřte na čipu. - Vyberte SPL funkci (třeba
ADC2_InitneboUART1_Init) a rozepište ji do jednotlivých zápisů. U každého řádku najděte registr a bit v manuálu.
Zdroje¶
- RM0016, STM8S reference manual
- Datasheet STM8S208RB
- PM0044, STM8 CPU programming manual
- Příklad SFR na GitLabu
- Daughterboard
- STM8 na elektromys.eu