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
0x0000 – ...     RAM (proměnné, zásobník)
0x4000 – ...     datová EEPROM
0x4800 – ...     option bytes (trvalé nastavení čipu)
0x5000 – 0x57FF  registry periferií (SFR)  ← tady se děje hardware
0x8000 – ...     Flash (program)

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
#define TIM4_CR1_CEN   ((uint8_t)0x01)   // bit 0 registru CR1 časovače TIM4
#define TIM4_CR1_UDIS  ((uint8_t)0x02)   // bit 1
#define TIM4_SR1_UIF   ((uint8_t)0x01)   // bit 0 registru SR1: příznak přetečení
#define TIM4_IER_UIE   ((uint8_t)0x01)   // bit 0 registru IER: povolení přerušení

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
void GPIO_WriteHigh(GPIO_TypeDef* GPIOx, GPIO_Pin_TypeDef PortPins)
{
  GPIOx->ODR |= (uint8_t)PortPins;
}

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
GPIOx->CR2 &= (uint8_t)(~(GPIO_Pin));
...
GPIOx->DDR |= (uint8_t)GPIO_Pin;       // výstup
GPIOx->CR1 |= (uint8_t)GPIO_Pin;       // push-pull
...

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ý:

  1. V RM0016 najdeme kapitolu o portech (I/O ports).
  2. Najdeme tabulku, která říká, jak se kombinací DDR, CR1 a CR2 nastavuje režim pinu. Vstup s pull-upem je DDR = 0, CR1 = 1, CR2 = 0.
  3. Podkapitola GPIO registers popisuje Px_CR1 po bitech.
  4. 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
typedef struct GPIO_struct {
    __IO uint8_t ODR;   // +0
    __IO uint8_t IDR;   // +1
    __IO uint8_t DDR;   // +2
    __IO uint8_t CR1;   // +3
    __IO uint8_t CR2;   // +4
} GPIO_TypeDef;

#define GPIOG_BaseAddress  0x501E
#define GPIOG ((GPIO_TypeDef *) GPIOG_BaseAddress)

Zápis v C a skutečný registr v paměti si odpovídají takto:

1
2
3
4
5
6
zápis v C       člen struktury   adresa
GPIOG->ODR      ODR (+0)         0x501E
GPIOG->IDR      IDR (+1)         0x501F
GPIOG->DDR      DDR (+2)         0x5020
GPIOG->CR1      CR1 (+3)         0x5021
GPIOG->CR2      CR2 (+4)         0x5022

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
GPIOG->ODR = 0x01;    // zkratka za  (*GPIOG).ODR = 0x01;
                      // = zapiš 0x01 na adresu 0x501E + 0 (offset členu ODR)

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
a | b     OR   (1, je-li 1 aspoň jeden)
a & b     AND  (1, jsou-li 1 oba)
a ^ b     XOR  (1, liší-li se)
~a        NOT  (každý bit se obrátí)
a << n    posun doleva o n bitů
a >> n    posun doprava o n bitů

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
REG |= MASKA;          // nastavit bit(y) na 1
REG &= (uint8_t)~MASKA;   // vynulovat bit(y)
REG ^= MASKA;          // přepnout bit(y)
if (REG & MASKA) { }   // je některý z bitů 1?

Příklad po bitech, zapnutí bitu 2:

1
2
3
REG          0101 0000
MASKA        0000 0100     (1u << 2)
REG | MASKA  0101 0100     ← bit 2 se nastavil, ostatní zůstaly

A vynulování:

1
2
3
MASKA        0000 0100
~MASKA       1111 1011
REG & ~MASKA 0101 0000     ← bit 2 se vynuloval, ostatní zůstaly

= proti |=

1
2
GPIOG->ODR = 0x01;    // LED1 svítí, všech dalších 7 LED zhasne
GPIOG->ODR |= 0x01;   // LED1 svítí, ostatní se nezmění

= 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 na int (16 bitů na STM8), takže ~(uint8_t)0x04 je 0xFFFB. 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 do uint8_t warning 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
#define ODR (*(volatile uint8_t *)0x501E)
void on0(void)  { ODR |= 0x01; }
void off0(void) { ODR &= (uint8_t)~0x01; }
void tog0(void) { ODR ^= 0x01; }

SDCC z toho udělá:

1
2
3
_on0:   bset  0x501e, #0
_off0:  bres  0x501e, #0
_tog0:  bcpl  0x501e, #0

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
ld    a, 0x501e
or    a, (0x01, sp)
ld    0x501e, a

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
TIM4_TimeBaseInit(TIM4_PRESCALER_128, 125 - 1);
TIM4_ClearFlag(TIM4_FLAG_UPDATE);
TIM4_ITConfig(TIM4_IT_UPDATE, ENABLE);
TIM4_Cmd(ENABLE);

Totéž přímo (to, co SPL funkce uvnitř dělají):

1
2
3
4
5
TIM4->PSCR = 7;                      // předdělička 2^7 = 128
TIM4->ARR  = 125 - 1;                // strop čítání
TIM4->SR1  = (uint8_t)~TIM4_SR1_UIF; // smazat příznak přetečení
TIM4->IER |= TIM4_IER_UIE;           // povolit přerušení od přetečení
TIM4->CR1 |= TIM4_CR1_CEN;           // spustit čítač

Všimněte si, že tři registry se zapisují jinak:

  • PSCR a ARR se prostě přepíšou (=): jsou to celá čísla.
  • IER a CR1 jsou sbírkou jednotlivých bitů, tak se použije |=.
  • SR1 je stavový registr s příznaky typu rc_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
(gdb) p GPIOG->ODR              # hodnota registru
(gdb) p/t GPIOB->IDR            # binárně
(gdb) sfr GPIOG                 # všechny registry portu s rozpisem bitů
(gdb) sfr TIM4.CR1              # jeden registr po bitech
(gdb) set var GPIOG->ODR = 0x55 # zapsat přímo do registru

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 CR2 u výstupního pinu. A pak to ověřte na čipu.
  • Vyberte SPL funkci (třeba ADC2_Init nebo UART1_Init) a rozepište ji do jednotlivých zápisů. U každého řádku najděte registr a bit v manuálu.

Zdroje


Související posty