V příspěvku nehledejte nic světoborného. Jen jsem se nudil a chtěl jsem vyzkoušet zda displej s MAX7219 poběží (v rozporu s datasheetem) i s 3.3V napájením. Abych to ověřil musel jsem ho krmit nějakými daty. Komunikuje po SPI a jen tak z hecu mě napadlo, že by stálo za to vyzkoušet jak si s tím poradí USART na STM32. Jak brzy uvidíte roli SPI masteru zvládl dobře.

USART jako SPI Master na STM32


Než se pustím do stručného komentáře jak USART konfigurovat a ovládat, dovolím si malou motivaci. Proč bych měl SPI sběrnici provozovat pomocí USARTu když na to mám na čipu plnohodnotnou periferii ? Na čipech řady F0 a F3 nás k tomu asi nedonutí nic jiného než nedostatek vývodů. Na čipech řady F4 ale existuje pádnější důvod. Jejich SPI periferie totiž umí posílat jen celé bajty. Takže narozdíl od SPI na F0 nebo F3 neumí poslat například 9bitový paket. No a existují grafické LCD nebo OLED displeje s drivery jako například UC1608 nebo WS0010 (RS0010), které lze kvůli úspoře pinů ovládat 9bitovým SPI protokolem (v prvním bitu se posílá info o tom zda jde o příkaz nebo o data). A právě USART (který jak jsitě víte umí posílat 9bit zprávy) může zachránit situaci od potřeby emulovat SPI softwarově (a přijít o podporu DMA)... ale to jen tak na okraj.

Klasickou inicializaci UARTu (asynchronní komunikace) máte jistě dobře zažitou, takže se k ní není potřeba vyjadřovat. Mrkneme se tedy co umožňuje synchronní provoz a jak jej nastavit. Protože jsem konzerva (a HAL je pro mě nástroj na mučení programátorů) budu vše provádět pomocí SPL. Pin sloužící v USARTu jako clock má v datasheetech označení CK a například na mém testovacím STM32F030F4 je na pinu PA4. Ten přirozeně spolu s TX pinem (PA2) musíme nastavit jako "alternate function". Krom nich si také musíme nastavit jako výstup nějaký (nebo nějaké) CS piny. Aby to bylo pěkně v řadě zvolil jsem k tomuto účelu PA3. Ke konfiguraci clocku (tím myslím signál který poleze ven z USARTu pinem CK) slouží struktura USART_ClockInitTypeDef a má čtyři položky:

USART_CPOL - nastavuje polaritu clock signálu v neutrálním stavu (v kombinaci s následující položkou volí mód SPI)
USART_CPHA - nastavuje na kterou hranu clocku se mají data číst (v kombinaci s předchozí položkou volí mód SPI)
USART_Clock - aktivuje a deaktivuje clock signál (vybírá mezi USARTem a UARTem)
USART_LastBit- touto funkcí můžeme zakázat aby clock "tiknul" i s poledním bitem zprávy (nenapadá mě praktické uplatnění)

Funkce USART_ClockInit() pak podle této struktury konfiguruje periferii. Protože USART běžně odesílá zprávy ve formátu LSB first (tedy nejprve nejnižší bit zprávy) a SPI se provozuje většinou opačně, musíme si pořadí bitů ve zprávě otočit pomocí funkce USART_MSBFirstCmd(). Přenosová rychlost (baudrate) se nastavuje stejně jako u asynchronního režimu ve struktuře USART_InitTypeDef položkou USART_BaudRate. Já zvolil svižnější 1Mb/s. Samotné vysílání (16bit dat), realizuje funkce usartspi_send16() a je vcelku triviální. Nejprve nastaví CS pin do log.0, čímž aktivuje slave obvod. Pak zapíše do vysílacího bufferu první bajt dat, počká až se začne vysílat a uvolní se další místo v bufferu (vlajka USART_FLAG_TXE). Naloží do něj druhý bajt dat a sledováním vlajky USART_FLAG_TC (Transfer Complete) počká na konec přenosu. Pak vrátí CS do log.1 čímž deaktivuje slave (pro MAX7219 to taky znamená pokyn ke zpracování dat). Když se mrknete na oscilogram z vysílání, uvidíte, že jsou mezi jednotlivými bajty zprávy prodlevy. Ty vznikají z toho důvodu, že USART vysílá na TX linku i start a stop bity (log.0 na před začátkem bajtu a log.1 za koncem bajtu). Naštěstí k nim USART negeneruje clock, takže tyto bity se našeho SPI vysílání neúčastní a jen vytváří prodlevy.


#include "stm32f0xx.h"

// registry/příkazy MAX7219
#define MAX7219_NOP        0x0
#define MAX7219_DECODEMODE 0x9
#define MAX7219_INTENSITY  0xA
#define MAX7219_SCANLIMIT  0xB
#define MAX7219_SHUTDOWN   0xC
#define MAX7219_TESTDISP   0xF
#define MAX7219_DIGIT0     0x1
#define MAX7219_DIGIT1     0x2
#define MAX7219_DIGIT2     0x3
#define MAX7219_DIGIT3     0x4
#define MAX7219_DIGIT4     0x5
#define MAX7219_DIGIT5     0x6
#define MAX7219_DIGIT6     0x7
#define MAX7219_DIGIT7     0x8

#define MAX7219_BLANK 127 // dolní 4bity při zapnuté znakové sadě vyberou znak "blank", nejvyšší bit aktivuje desetinnou tečku
#define MAX7219_ON    1 // hodnota v registru MAX7219_SHUTDOWN
#define MAX7219_OFF   0 // hodnota v registru MAX7219_SHUTDOWN
#define MAX7219_DOT   1<<7  // desetinná tečka se rozsvěcí nejvyšším bitem

// makra pro ovládání Chip Select
#define CS_L GPIOA->BRR = GPIO_Pin_3;
#define CS_H GPIOA->BSRR = GPIO_Pin_3;

void init_usartspi(void);
void init_clock_48(void);
void usartspi_send16(uint16_t data);
void max7219_set(uint8_t command, uint8_t data);

uint8_t i;

int main(void){
 init_clock_48(); // taktujeme čip na 48MHz
 init_usartspi();
 // vyčistit obsah displeje (MAX7219)
 for(i=1;i<8;i++){max7219_set(i, MAX7219_BLANK);}
 max7219_set(MAX7219_DECODEMODE,0xff); // dekódovat všechny cifry (využít znakovou sadu)
 max7219_set(MAX7219_SCANLIMIT, 4); // zobrazovat jen dolní 4 cifry
 max7219_set(MAX7219_TESTDISP, 0); // test-display vypnout
 max7219_set(MAX7219_INTENSITY, 7); // poloviční intenzita
 max7219_set(MAX7219_SHUTDOWN, MAX7219_ON); // zapnout displej
 max7219_set(MAX7219_DIGIT3, 2);  // na 3. pozici zobraz cifru "2"
 max7219_set(MAX7219_DIGIT2, 5 | MAX7219_DOT); // na druhé pozici zobraz cifru "5" a desetinnou tečku
 max7219_set(MAX7219_DIGIT1, 4); // na první pozici zobraz cifru "4"
 max7219_set(MAX7219_DIGIT0, 0); // na nulté pozici zobraz cifru "0"

  while(1){

  }
}

// složí data do 16bit čísla a pošle po SPI (tato funkce by šla nahradit makrem)
void max7219_set(uint8_t command, uint8_t data){
 usartspi_send16(((uint16_t)command)<<8 | (uint16_t)data);
}

void usartspi_send16(uint16_t data){
 CS_L; // aktivovat slave
    // zde by neměla nastat situace že v bufferu nebude místo, proto si dovolím to nekontrolovat
 USART_SendData(USART1, data>>8); // naložit první bajt zprávy do bufferu
 while (!USART_GetFlagStatus(USART1,USART_FLAG_TXE)){} // počkat až bude v odesílacím bufferu místo
 USART_SendData(USART1, data & 0xff); // naložit druhý bajt zprávy do bufferu
 while (!USART_GetFlagStatus(USART1,USART_FLAG_TC)){} // počkat na dokončení přenosu
 CS_H; // deaktivovat slave
}

void init_usartspi(void){
 GPIO_InitTypeDef gp;
 USART_InitTypeDef usart_is;
 USART_ClockInitTypeDef usart_clk;

 // inicializace pinů (PA2 - CK/SCK, PA4 - TX/MOSI, PA3 - CS)
 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 , ENABLE);

 gp.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_4;
 gp.GPIO_Mode = GPIO_Mode_AF;
 gp.GPIO_OType = GPIO_OType_PP;
 gp.GPIO_PuPd = GPIO_PuPd_NOPULL;
 gp.GPIO_Speed = GPIO_Speed_Level_1;
 GPIO_Init(GPIOA, &gp);
 // přidělit kontrolu nad CK/SCK a TX/MOSI USARTu
 GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_1);
 GPIO_PinAFConfig(GPIOA, GPIO_PinSource4, GPIO_AF_1);
   
 // CS (PA3) ovládáme manuálně
 gp.GPIO_Pin = GPIO_Pin_3;
 gp.GPIO_Mode = GPIO_Mode_OUT;
 GPIO_Init(GPIOA, &gp);
 CS_H; // hned deaktivovat slave

 // clock v SPI MODE 0
 usart_clk.USART_CPOL = USART_CPOL_Low; // clock neutrálně v log.0
 usart_clk.USART_CPHA = USART_CPHA_1Edge; // čtení dat na první hranu (tedy vzestupnou)
 usart_clk.USART_Clock = USART_Clock_Enable; // vypouštět clock
 usart_clk.USART_LastBit = USART_LastBit_Enable; // clock i pro poslední bit
 USART_ClockInit(USART1, &usart_clk); // inicializovat clock

 usart_is.USART_BaudRate = 1000000; // datová rychlost 1Mb/s (ze 48MHz ji lze realizovat)
 usart_is.USART_WordLength = USART_WordLength_8b; // zpráva 8 bitů
 usart_is.USART_StopBits = USART_StopBits_1; // to je celkem jedno, stopbit dělá jen zbytečnou prodlevu
 usart_is.USART_Parity = USART_Parity_No; // paritu nechceme
 usart_is.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // řízení toku nepoužíváme
 usart_is.USART_Mode = USART_Mode_Tx; // budeme jen vysílat
 USART_Init(USART1, &usart_is); // inicializovat UART
 USART_MSBFirstCmd(USART1,ENABLE); // pro SPI je běžné posílat jako první MSB (u UARTu je to běžně naopak)

 USART_Cmd(USART1, ENABLE); // spustit USART
}

void init_clock_48(void){
    // čip je zatím taktován z HSI
 RCC_PLLConfig(RCC_PLLSource_HSI,RCC_PLLMul_12);  // nastavit PLL na násobení 12x (8MHz / 2 * 12 = 48MHz)
 RCC_PLLCmd(ENABLE); // spustit PLL
 while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) != SET); // počkat na rozběh PLL
 RCC_HCLKConfig(RCC_SYSCLK_Div1); // SYSCLK z PLL nijak nedělit
 RCC_PCLKConfig(RCC_HCLK_Div1); // HCLK ze SYSCLK nijak nedělit (jedeme naplno)
 RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); // přepnout SYSCLK na PLL (jedeme na 48MHz)
 SystemCoreClockUpdate(); // updatovat globální proměnnou SystemCoreClock (nutné pro korektní výpočet baudrate)
}
 



Na oscilogramu můžete vidět komunikaci ze začátku našeho programu. Všimněte si v průběhu clocku prodlev pro start a stop bity.



Bastl na testy. Vlevo STLINK, uprostřed malý STM32F030F4 v TSSOP pouzdře, vpravo displej s MAX7219


Jak vidíte z fotografie driver si vystačil i s 3.3V napájením (červený displej má naštěstí nízká prahová napětí). Pro ty z vás, kteří MAX7219 neznají si dovolím pár slov. Jde o driver LED displejů se společnou anodou. Může řídit až 8x8 segmentů, tedy jak klasické 7-segmentové displeje tak displeje typu "dot-matrix". U nás je driver poměrně drahý (~200kč), ale v číně jsou k dostání moduly 8x7segment nebo 8x8dot matrix s tímto driverem v ceně jednoho dolaru (a zatím jsem nenarazil na žádný problémový). Drivery je možné spojovat za sebe (tzv. "daisy-chain"), a to tak, že datový výstup jednoho driveru připojíte na datový vstup druhého (stejně jako lze spojovat posuvné registry) a rozšiřovat tak displej v podstatě neomezeně. Komunikujete s ním pomocí SPI v 16bitových rámcích (8bitů příkaz + 8bitů data). Driver má v sobě zabudovanou znakovou sadu (0,1,2,3,4,5,6,7,8,9,-,E,H,L,P), kterou můžete individuálně zapínat a vypínat vybraným cifrám. Využití znakové sady vyžaduje odpovídající zapojení segmentů k displeji. Nic vám ale nebrání kontrolovat jednotlivé LEDky a znakovou sadu nepoužívat (logická volba u dot-matrix displejů). Proud do jednotlivých segmentů se nastavuje jedním externím rezistorem a programově je možné snižovat intenzitu jasu v 16ti krocích.

Co se našeho programu týče tak displej ovládáme funkcí max7219_set(). Ta složí příkaz i data do 16bitového čísla a odešle. Na začátku inicializace naplním všech 8 pozic displeje hodnotou 127 (0b111 1111). Dolní čtyři bity nesou informaci o znaku (0b1111 odpovídá prázdnému znaku - "blank"). Nejvyšším bitem lze zapnout desetinnou tečku. Tři zbývající bity nemají význam a mohou mít libovolnou hodnotu. V registru Decode mode aktivuji znakovou sadu pro všech 8 cifer (každý bit dat aktivuje/deaktivuje znakovou sadu pro odpovídající cifru). Protože chci v této modelové situaci používat jen 4 cifry (tedy půlku displeje) nastavím obsah registru Scan limit na 4. Driver pak multiplexuje jen přes dolní 4 cifry displeje. Tato volba ovlivňuje jas. Čím méně cifer driver budí, tím větší je pro každou cifru střída a tím větší je i jas displeje. Není tedy vhodné používat tuto funkci na "zhasínání" například nul před číslem a podobně. Driver je po startu "vypnutý" (v úsporném režimu), je proto potřeba ho aktivovat zápisem hodnoty 1 do registru Shutdown. Pro jistotu je dobré zapsat nulu i do registru Test display (log.1 by rozsvítila celý displej). V rámci ukázky jsem si dovolil vypsat na displej jedno číslo. Při praktickém použití si budete muset implementovat nějakou zobrazovací funkci. Například s pomocí itoa() nebo sprintf by to neměl být problém. Jediná "komplikace" je fakt, že v ASCII cifry 0 až 9 začínají na hodnotě 48, kdežto ve znakové sadě našeho displeje začínají na 0. Prosté odečtení čísla 48 od všech znaků v řetězci by mělo problém zvládnout (samozřejmě za předpokladu že v jsou v řetezci jen cifry). A to je ode mě vše

odkazy:
MAX7219 datasheet
STM32F0 Reference manual

Rozcestník na další díly seriálu naleznete na elektromys.eu