USART na STM32F0

STM32F051 má dvě rozhraní USART. Jedno obyčejné (USART2)) a jedno šíleně nadupané (USART1). I ten obyčejný USART má ale dost široké možnosti použití. Oba USARTy zvládají přenos rychlostmi až 6Mb/s a obsahují hardwarové řízení toku. To je totiž při těchto rychlostech nezbytné. Přenos může být obsluhován pomocí DMA, což výrazně zjednoduší vysílání a přijímání objemných dat. Má zabudovanou podporu Multiprocesorové komunikace i schopnost komunikovat "po jednom drátě". Jako bonus je možné každý kanál invertovat a využít RTS pin jako DE (Device Enable) pokud využíváme externí driver (třeba pro RS485). Občas najde využití i automatická detekce datové rychlosti. Nadupaný USART má potom podporu LIN, Smartcard, Modbus, práce v režimu spánku a kdo ví čeho ještě. Pochybuji že se kdy k těmto funkcím dostanu, ale i ty běžné postačí na slušně dlouhý tutoriál. A to je také důvod proč ho jíst po pořádných soustech.

Protože je USART vybaven řízením toku, uděláme si malou rekapitulaci jeho signálních linek.

Přirozeně není nutné využívat je všechny. Pokud plánujete pouze přijímat stačí vám pin RX, pokud chcete přijímat is řízením toku, přibude vám linka RTS atp. Vtipnou vlastností je možnost odesílat Idle nebo Break znak. Idle znak odešle před prvním vysíláním jeden rámec samých jedniček. Break znak pak po aktuální vysílání rámec plný nul. Na co to může být ? Docela zajímavá může být schopnost USARTu rozpoznávat v příchozích datech šum. Nicméně to co nás bude opravdu zajímat je přenosová rychlost. USART může realizovat přenosové rychlosti pouze jako zlomek frekvence jeho clocku. Na každý přijatý bit potřebuje 16 nebo 8 hodinových taktů. A to z toho důvodu aby dokázal rozumně rychle reagovat na asynchronní příchod Startbitu. Počet hodinových taktů na jeden bit najdete v datasheetu pod označením oversampling a můžete jej měnit. Pokud nepotřebujete velkou rychlost volte vždy 16. Znamená to přesnější určení startbitu a příjem je pak odolnější proti chybě přenosových rychlostí (přijímač a vysílač nikdy nekomunikují na přesně stejné datové rychlosti). Výpočet možných přenosových rychlostí (oversampling 16) lze provádět podle vztahu Baud=Fck/N, kde N je celé číslo větší jak 16. S oversamplingem 8 pak dle vztahu Baud=2*Fck/N. Taktujeme-li USART maximální frekvencí 48MHz, je patrné že nejvyšší možná datová rychlost je 6Mb/s.

Seznam příkladů:

USART1 Vysílání

Pro první ukázku jsem si zvolil jednoduché odesílání počtu stisknutí tlačítka. Ideální sousto. Protože chci pouze vysílat, stačí mi nakonfigurovat pin PA9 a připojit ho spolu s GND k převodníku UART->USB. Používám modulek FT234XF přilepený tavnou pistolí na zadní stranu Discovery boardu (viz foto). Příklad je ale přirozeně nezávislý na typu převodníku. Takže si klidně vystačíte s čínským CH340 nebo PL2303, případně CP2102. Jen dávejte pozor aby některý z nich nepoužíval 5V logiku. Pokud budete chtít testovat i vyšší rychlosti, určitě na to zvolte modulek vybevený handshakingem. Vraťme se ale k příkadu. Pin PA9 je potřeba nakonfigurovat jako Alternative function s funkcí AF1 (USART1). Není od věci přidělit mu maximální rychlost, protože implicitně je zvolena nejnižší a ta by při vyšších datových tocích nestačila. Konfigurace USARTu spočívá jako obvykle ve vyplnění struktury a zavolání funkce LL_USART_Init(). Funkce počítá obsah registrů podle hodnoty buadrate a podle předpokládané frekvence USARTu. Program běžící s clockem z HSE tedy potřebuje znát jeho frekvenci. Jinak řečeno musíte mít korektně nastavenou konstantu HSE_VALUE (více zde). Nesmíme přirozeně zapomenout zapnout USARTu clock. Tady udělám malou zastávku. Je to specialita tohoto čipu, že lze volit pro USART1 různé zdroje clocku. Díky tomu může USART pracovat i v režimu spánku a vzbudit čip po přijmu dat. Po startu je automaticky zvolena jako zdroj clocku sběrnice APB1, ale já jse mdo zdrojového kódu pro jistotu přidal funkci, která tento zdroj clocku zvolí. U jiných čipů se tím nebudete muset zabývat. Nastavení clocku provádím ve funkci init_clock() a jádro i sběrnice taktuji na 48MHz. Po konfiguraci už USART pouze povolím funkcí LL_USART_Enable().


Montáž FT234XF na discovery board (připojen plný UART s řízením toku)

Tlačítko, kterým zahajujeme vysílání je připojené na PA0 a ve stisknutém stavu je na něm log.1. Abych ošetřil zákmity tlačítka využvám přerušení od Systick (více o něm zde), v němž 100x za vteřinu skenuji jeho stav. Jakmile je stisknuto inkrementuji počítadlo pocet_stisku a skrze proměnnou posli dávám vědět hlavní smyčce aby zahájila odeslání zprávy. V hlavní smyčce se pomocí funkce snprintf() nejprve připraví řetězec a poté se předá funkci usart1_puts(). Ta jej prochází a odesílá jednotlivé znaky tak dlouho než narazí na ukončovací znak řetězce. Odesílání je prováděno jednoduchým "pollingem". Funkce čeká dokud je vlajka TXE (Transmit data register empty) nulová. Ta značí že je vysílací buffer plný a není kam vložit další data. Jakmile se s odesláím předchozích dat buffer uvolní, vlajka se vynuluje a funkce do bufferu uloží další znak. No a to je vlastně všecho :)

Zdrojový kód ke stažení

// 4A - USART1 jednoduché vysílání
#include "stm32f0xx.h"
#include "stm32f0xx_ll_bus.h"
#include "stm32f0xx_ll_gpio.h"
#include "stm32f0xx_ll_rcc.h"
#include "stm32f0xx_ll_usart.h"
#include "stm32f0xx_ll_utils.h" // kvůli konfiguraci clocku
#include "stdio.h" // kvůli fci snprintf()

void init_clock(void);
void init_usart1(void);
void usart1_putchar(uint8_t data);
void usart1_puts(char *Buffer);

LL_RCC_ClocksTypeDef clocks;
volatile uint8_t posli=0;
volatile uint16_t pocet_stisku=0;
char text[31];

int main(void){
init_clock(); // 48MHz z HSE
init_usart1();
SysTick_Config(480000); // přerušení 100x za vteřinu

while (1){
 if(posli){ // jakmile máme co odesílat
  posli=0; // už jsme to poslali
  snprintf(text,30,"Stisknuto %u krat\n\r",pocet_stisku); // připraví řetězec
  usart1_puts(text);// odešle řetězec
  }
 }
}

// Rutina přerušení Systick - pravidelná kontrola stavu tlačítka
void SysTick_Handler(void){
static uint8_t button_status=1;
 if(!LL_GPIO_IsInputPinSet(GPIOA,LL_GPIO_PIN_0) && button_status==0){
  button_status=1; // tlačítko stisknuto
  pocet_stisku++; // inkrementuj počítadlo stisků
  posli=1; // dej vědět hlavní smyčce ať pošle zprávu
  }
 if(LL_GPIO_IsInputPinSet(GPIOA,LL_GPIO_PIN_0)){
  button_status=0; // tlačítko uvolněno
 }
}

void usart1_puts(char *Buffer){
while(*Buffer){ // než narazíš na konec řetězce (znak /0)
 while(!LL_USART_IsActiveFlag_TXE(USART1)){}; // čekej než bude volno v Tx Bufferu
  LL_USART_TransmitData8(USART1,*Buffer++); // předej znak k odeslání
 }
}

void init_usart1(void){
LL_USART_InitTypeDef usart;
LL_GPIO_InitTypeDef gp;
// konfigurace pinů - PA9 jako TX
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA); // clock pro GPIOA
LL_GPIO_StructInit(&gp);
gp.Pin = LL_GPIO_PIN_9;
gp.Mode = LL_GPIO_MODE_ALTERNATE;
gp.Speed = LL_GPIO_SPEED_HIGH;
gp.Alternate = LL_GPIO_AF_1;
gp.Pull = LL_GPIO_PULL_NO;
LL_GPIO_Init(GPIOA,&gp);

// konfigurace USART
LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_USART1); // clock pro USART1 z APB1
// USART1 si může vybírat z vícero zdrojů clocku (aby mohl běžet v režimu spánku)
LL_RCC_SetUSARTClockSource(LL_RCC_USART1_CLKSOURCE_PCLK1); // clock z APB1
// konfigurace USARTu
usart.BaudRate = 9600; // 9600 b/s
usart.DataWidth = LL_USART_DATAWIDTH_8B;
usart.HardwareFlowControl = LL_USART_HWCONTROL_NONE;
usart.OverSampling = LL_USART_OVERSAMPLING_16; // pokud není nouze o rychlost raději 16x
usart.Parity = LL_USART_PARITY_NONE;
usart.StopBits = LL_USART_STOPBITS_1;
usart.TransferDirection = LL_USART_DIRECTION_TX; // pouze vysíláme
LL_USART_Init(USART1,&usart); // můžete hlídat návratovou hodnotu ...
LL_USART_Enable(USART1);
}

// 48MHz z externího 8MHz signálu
void init_clock(void){
LL_UTILS_PLLInitTypeDef pll;
LL_UTILS_ClkInitTypeDef clk;
pll.Prediv = LL_RCC_PREDIV_DIV_2; // 8MHz / 2 = 4MHz
pll.PLLMul = LL_RCC_PLL_MUL_12;  // 4MHz * 12 = 48MHz
clk.AHBCLKDivider = LL_RCC_SYSCLK_DIV_1; // APB i AHB bez předděličky
clk.APB1CLKDivider = LL_RCC_APB1_DIV_1;
// voláme konfiguraci clocku
LL_PLL_ConfigSystemClock_HSE(8000000,LL_UTILS_HSEBYPASS_ON,&pll,&clk);
// aktualizuj proměnnou SystemCoreClock
SystemCoreClockUpdate();
}


Ukázka odesílání s rychlostí 3Mb/s

USART1 Příjem řetězců s přerušením

Slíbil jsem vám, že si budeme v tutoriálu ukusovat větší sousta a s touto ukázkou jsme si ukousli jedno zvlášť výživné. Příjem je totiž citelně obtížnější než odesílání. Když odesíláte, máte spoustu času si vše připravit a nemusíte si dělat starosti s tím kdy data odesíláte. Příjem je naopak závod s časem. Příjem znaku je nanedálá událost na kterou musíte včas zareagovat a vyřídit ji rychle - dřív než dorazí další znak. Abychom to dokázali využijeme přerušení. Náš program bude mít za úkol přijmout příkaz (jehož konec rozpozná podle ukončovacího znaku \r nebo \n - stisk klávesy Enter v terminálu) a zareagovat na něj. Formát příkazu bude "c=12345" + stisk Enter. Hrubé rysy programu budou následující. Nakonfigurujeme USART1 tak aby vysílal i přijímal a aby s přijatým znakem vyvolal přerušení. V rutině přerušení budeme ukládat příchozí zprávu do dočasného pole až do okamžiku ky přijde ukončovací znak. Pak zprávu překopírujeme do jiného pole (abychom si tak uvolnili dočasné pole k přijmu dalšího příkazu) a dáme zbytku programu vědět, že je tu nový příkaz. Hlavní program tento nový příkaz zpracuje, pokusí se z něj vytáhnout užitečná data a odpoví nám.

Přerušení, pokud jste se s ním ještě na STM32 nesetkali, zprostředkovává NVIC (Nested Vector Interrupt Controller), který je součástí jádra. Funkce k jeho ovládání najdete v knihovně CMSIS v souboru core_cm0.h. KAždé přerušení lze povolit nebo zakázat a je možné mu nastavit prioritu. Priorita se nastavuje číslem 0 až 3 a čím je toto číslo nižší tím je priorita vyšší. Pokud běží rutina přerušení s nízkou prioritou může ji přerušení s vyšší prioritou přerušit (nehezká formulace). Seznam všech možných přerušení najdete v hlavičkovém souboru (v našem případě stm32f051x8.h). My budeme využívat přerušení USART1_IRQn. Všechny rutiny přerušení mají název odvozený od názvu přerušení, takže naše rutina se musí jmenovat USART1_IRQHandler(). USART může vyvolat přerušení jako reakci na mnoh různých událostí. Například na přijatý znak, na prázdný odesílací buffer, na ukončení přenosu, na různé chyby přenosu atd. Všechny tyto události skončí v jedné jediné rutině přerušení a je na vašem programu aby dokázal rozpoznat která z událostí přerušení vyvolala. Naše situace ale bude vcelku jednoduchá, my USARTu povolíme jako zdroj přerušení pouze událost RXNE (Receive buffer not empty - přišel znak). Ze slušnosti si ale v rutině přerušení ověříme jestli ji vyvolala opravdu tato událost. Nesmíme také zapomenout dát USARTu nějak vědět že jsme na jeho přerušení zareagovali, jinak by nadále trvalo a náš program by rutinu přerušení vykonával stále dokola. Žádost o přerušení se startuje nastavením tvz. vlajky (v našem případě RXNE) a způsob jejího vynulování je různý, podle toho o jakou vlajku jde. V případě RXNE stačí abychom přečetli přijatá data a dojde k automatickému smazání. V našem případě je tedy nezbytně nutné data přečíst ! Pokud budete využívat přerušení i od jiných událostí, musíte si v datasheetu zjistit jak jejich vlajky smazat.

Pro jistotu zrekapituluji to důležité. USART v reakci na různé události nastavuje vlajky. Některé vlajky mohou volat přerušení a lze individuálně povolit které to jsou. Rutina přerušení je jen jedna, takže je v ní potřeba zjistit jaká událost ji vyvolala (podle stavu vlajek). Než rutina přerušení skončí je potřeba odstranit podnět který ji vyvolal (vynulovat příslušnou vlajku) - což se dělá rozličnými způsoby. Tento postup bude shodný u přerušení od většiny dalších periferií.

Do teď jsme se zabývali přerušením, tedy schopností programu reagovat na nenadálou událost. V úvodu jsme ale diskutovali ještě jedno úskalí, nutnost zareagovat rychle, dřív než přijde další znak. Jak je na tom tedy naše funkce s reakčním časem ? Shrneme si to v přehledné tabulce níže.Při příjmu jediného znaku se v rutině přerušení nic moc neděje a program se v ní dlouho nezdržuje a je během 3us připraven přijímat další znak. Zajímavé to začne být v situaci kdy přijde poslední znak zprávy, v takovém případě musí program v rutině přerušení zkopírovat přijatou zprávu. To mu v případě průměrně dlouhé zprávy trvá přibližně 6.4us. Zpracování řetězce o maximální délce (30 znaků), tedy nejhorší možná situace trvá skro 16us. To znamená že pokud by znaky přicházely s časovým odstupem menším jak 16us, hrozilo by riziko že některý z nich nestihneme přijmout. V našem konkrétním případě ale nic takového nehrozí. Při datové rychlosti 9600b/s trvá přenos jednoho znaku 1/9600*10=1.04ms. I při datové rychlosti 115200b/s, kde přenos jednoho znaku trvá 86us, má funkce ještě rezervy.

UdálostRakční čas rutiny přerušeníReakční čas funkce sscanf()
příjem jednoho znaku 3 us -
dokončení příjmu zprávy "c=12345" 6.4 us 86.2us
dokončení příjmu zprávy "aaaaa...30x" 15.8 us 9.2us

Zdrojový kód ke stažení

// 4B - USART1 příjem řetězců s přerušením
#include "stm32f0xx.h"
#include "stm32f0xx_ll_bus.h"
#include "stm32f0xx_ll_gpio.h"
#include "stm32f0xx_ll_rcc.h"
#include "stm32f0xx_ll_usart.h"
#include "stm32f0xx_ll_utils.h" // konfigurace clocku
#include "stdio.h" // kvůli fci snprintf()
#include "string.h"

#define MAX_STRLEN 32 // maximální počet znaků v přijímaném řetězci

void init_clock(void);
void init_usart1(void);
void usart1_putchar(uint8_t data);
void usart1_puts(char *Buffer);

LL_RCC_ClocksTypeDef clocks;
char command[MAX_STRLEN]; // přijatý řetězec (zpráva)
char answer[MAX_STRLEN]; // řetězec pro zpracování odpovědi
uint8_t num_commands=0; // informuje o příchodu nové zprávy
uint32_t test=0; // něco zajímavého ve zprávě :)
uint8_t scanned=0; // pomocná proměnná

int main(void){
init_clock(); // 48MHz z HSE
init_usart1();

while (1){
 // pokud přišla nová zpráva
 if(num_commands){
  scanned=sscanf(command,"c = %lu",&test); // pokus se ji přečíst
  if(scanned==0){usart1_puts("Error\n\r");} // pokud to nešlo
  else{
   // pokud je v pořádku, připrav odpověď...
   snprintf(answer,MAX_STRLEN,"c = %lu, 2*c = %lu \n\r",test,test*2);
   usart1_puts(answer); // ...a pošli ji
   }
  num_commands=0; // dokončili jsme zpracování zprávy
  }
 }
}

void USART1_IRQHandler(void){
static char received_string[MAX_STRLEN]; // zde se bude ukládat přijímaná zpráva
static uint16_t cnt = 0; // počítadlo přijatých znaků
char t;
// pokud je zdrojem přerušení přijatý znak
if(LL_USART_IsActiveFlag_RXNE(USART1)){
 t = LL_USART_ReceiveData8(USART1); // přečti přijatý znak (a maže vlajku RXNE - zdroj přerušení)
 // pokud příchozí znak není konec řádku a ještě máme místo v přijímací paměti
 if((t!='\r') && (t!='\n') && (cnt < (MAX_STRLEN-1))){
  received_string[cnt] = t; // ulož příchozí znak do dočasného pole
  cnt++; // inkrementuj počítadlo znaků
  }
 else if(cnt>0){ // je to konec příchozí zprávy (pokud není prázdná)
  strncpy (command,received_string, cnt); // zkopíruj příkaz do pole command
  command[cnt]= '\0'; // přiřaď na konec řetězce ukončovací znak
  cnt = 0; // vynuluj počítadlo znaků
  num_commands++; // oznam hlavní smyčce že má zprávu ke zpracování
  }
 }
}

void usart1_puts(char *Buffer){
while(*Buffer){ // než narazíš na konec řetězce (znak /0)
 while(!LL_USART_IsActiveFlag_TXE(USART1)){}; // čekej než bude volno v Tx Bufferu
  LL_USART_TransmitData8(USART1,*Buffer++); // předej znak k odeslání
 }
}

void init_usart1(void){
LL_USART_InitTypeDef usart;
LL_GPIO_InitTypeDef gp;
// PA9 jako TX, PA10 jako RX
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA); // clock pro GPIOA
LL_GPIO_StructInit(&gp);
gp.Pin = LL_GPIO_PIN_9 | LL_GPIO_PIN_10;
gp.Mode = LL_GPIO_MODE_ALTERNATE;
gp.Speed = LL_GPIO_SPEED_HIGH;
gp.Alternate = LL_GPIO_AF_1;
gp.Pull = LL_GPIO_PULL_NO;
LL_GPIO_Init(GPIOA,&gp);

// konfigurace USART
LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_USART1); // clock pro USART1 z APB1
// USART1 si může vybírat z vícero zdrojů clocku (aby mohl běžet v režimu spánku)
LL_RCC_SetUSARTClockSource(LL_RCC_USART1_CLKSOURCE_PCLK1); // clock z APB1
// konfigurace USARTu
usart.BaudRate = 9600; // 9600 bd/s
usart.DataWidth = LL_USART_DATAWIDTH_8B;
usart.HardwareFlowControl = LL_USART_HWCONTROL_NONE;
usart.OverSampling = LL_USART_OVERSAMPLING_16; // pokud není nouze o rychlost raději 16x
usart.Parity = LL_USART_PARITY_NONE;
usart.StopBits = LL_USART_STOPBITS_1;
usart.TransferDirection = LL_USART_DIRECTION_TX_RX; // vysíláme i přijímáme
LL_USART_Init(USART1,&usart); // můžete hlídat návratovou hodnotu ...
LL_USART_Enable(USART1);

// konfigurace přerušení
NVIC_SetPriority(USART1_IRQn,2); // nízká priorita (vysoké číslo)
NVIC_EnableIRQ(USART1_IRQn); // povolit přerušení od USART1
// v USART1 povolit přerušení od RXNE (Receive buffer not empty)
LL_USART_EnableIT_RXNE(USART1);
}

// 48MHz z externího 8MHz signálu
void init_clock(void){
LL_UTILS_PLLInitTypeDef pll;
LL_UTILS_ClkInitTypeDef clk;
pll.Prediv = LL_RCC_PREDIV_DIV_2; // 8MHz / 2 = 4MHz
pll.PLLMul = LL_RCC_PLL_MUL_12;  // 4MHz * 12 = 48MHz
clk.AHBCLKDivider = LL_RCC_SYSCLK_DIV_1; // APB i AHB bez předděličky
clk.APB1CLKDivider = LL_RCC_APB1_DIV_1;
// voláme konfiguraci clocku
LL_PLL_ConfigSystemClock_HSE(8000000,LL_UTILS_HSEBYPASS_ON,&pll,&clk);
// aktualizuj proměnnou SystemCoreClock
SystemCoreClockUpdate();
}

USART1 vysílání pomocí DMA

Pokud jste se ještě neseznámili s DMA na STM32, doporučil bych vám odskočit si na tutoriál o DMA a prostudovat jej před tím než se pustíte do následujícího příkladu. Použití DMA při vysílání přináší nesporné výhody. První z nich je schopnost dosáhnout datové rychlosti až 6Mb/s, což nejspíš příliš často neoceníte. Tou druhou výhodou, je schopnost DMA zařídit celé vysílání za vás. A právě to si předvedeme. Celý proces bude vlastně jednoduchý. Vyčleníme si v RAM pole v němž budeme skladovat data k odeslání (v našem případě řetězec). USART nastavíme jako vysílač a povolíme mu aby generoval requesty pro DMA. USART umí generovat dva requesty. Request RXNE (Receive buffer not empty) a TXE (Transmitt buffer empty). My budeme využívat TXE, protože ten se generuje v okamžiku kdy je USART hladový (má prostor převzít další byte dat). DMA nastavíme tak aby s každým requestem přeneslo jeden byte z našeho pole (řetězce) do USART1->TDR registru. Jinak řečeno aby udělalo přesně to co děláme ručně pokud odesíláme "blokujicí" metodou jako v předchozích příkladech.

Před odesláním musíme DMA specifikovat kolik dat má přenést. V našem případě to bude odpovídat počtu znaků v řetězci. Pak už jen DMA spustíme. Hned po startu bude na DMA čekat request od hladového USARTu, který se s prázdným bufferem dožaduje svých dat. Takže DMA okamžitě přenese jede znak zprávy a nakrmený USART se dá ihned do vysílání. To povede k tomu že se buffer USARTu vyprázdní, vyvolá request a DMA přesune další znak. Takto celý proces poběží do té doby než DMA přesune přednastavený počet znaků. Potom se DMA zastaví (s výjimkou situace kdy používáte "circular" mód). Během toho se budou postupně nastavovat dvě vlajky - DMAx_FLAG_HTx (Half Transfer) a DMAx_FLAG_TCx (Transfer Complete). Jejich názvy napovídají co signalizují, HT indikuje, že byla přenesena polovina dat a TC značí že byl dokončen celý přenos. Za zmínku ještě stojí vlajka DMAx_FLAG_TEx (Transfer Error), která signalizuje že se něco pokazilo, ale o tom až jindy. Od zmínených událostí (HT,TC,TE) může DMA vyvolat přerušení a my toho využijeme. Necháme volat přerušení od Transfer Complete a skrze proměnnou uart_dma_transfer dáme hlavní smyčce na vědomí, že je přenos dokončen a že může zahájit další vysílání. Přirozeně můžete vlajku hlídat i "pollingem". Tím ale jádro připravíte o možnost dělat během přenosu něco jiného a celé použití DMA ztratí smysl. Tuto jinou činnost bude v našem příkladě představovat instrukce nudim_se++ .

Závěrem jen krátce připomenu, že pro náš přenos používáme DMA1 (F031 jich stejně víc nemá) a kanál 2. Volba kanálu je vázaná periferií od které chcete přijímat requesty, což je ale problematika se kterou jste se jistě už seznámili v tutoriálu o DMA. Více se dozvíte z komentářů ve zdrojovém kódu. Upozorňuji vás, že příklad je psán s pomocí SPL (Standard Peripheral Library) namísto LL_API, jež jsem použil v předchozích příkladech. SPL mám mnohem raději a budu v něm připravovat asi všechny další příklady. Naštěstí není principiálně příliš odlišné od toho co jste viděli v LL_API v rámci předchozích příkladů. Očekávám tedy, že vás přechod nebude tolik bolet :D

#include "stm32f0xx.h"
#include "stdio.h" // kvůli snprintf()

void USART1_Configuration(void); // nastaví GPIO + USART + DMA

#define MAX_LEN 64 // maximální délka odesílaného řetězce

uint8_t uart_buff[MAX_LEN]; // prostor pro odesílaná data
uint16_t uart_buff_len; // délka odesílaných dat
volatile uint8_t uart_dma_transfer=0; // informace o probíhajícím přenosu
uint32_t nudim_se=0; // počítáme jak moc se program při přenosu nudí :D

int main(void){
 SystemCoreClockUpdate();
 USART1_Configuration(); // nastaví GPIO + USART + DMA

 while(1){
  // počkáme až přenos skončí
  nudim_se++; // jádro se nudí
  if(uart_dma_transfer==0){
   // připravíme řetězec k odeslání a zjistíme si jeho délku
   uart_buff_len=snprintf(uart_buff,MAX_LEN-1,"x=%u\n\r",nudim_se);
   // nastavíme délku přenosu
   DMA_SetCurrDataCounter(DMA1_Channel2,uart_buff_len);
   // a spustíme to (request od USART_TXE už čeká)
   DMA_Cmd(DMA1_Channel2, ENABLE);
   uart_dma_transfer=1; // přenos probíhá
   nudim_se=0; // jádro se začíná nudit znovu
  }
 }
}

void DMA1_Channel2_3_IRQHandler(void){
 // máme sice jen jeden zdroj pro toto přerušení, ale ze slušnosti zjišťujeme zda je to on
 if(DMA_GetFlagStatus(DMA1_FLAG_TC2)){
  DMA_ClearFlag(DMA1_FLAG_TC2); // mažeme vlajku Transfer Complete
  DMA_Cmd(DMA1_Channel2, DISABLE); // vypínáme DMA - aby ho bylo možné rekonfigurovat
  uart_dma_transfer=0; // oznámíme zbytku programu, že přenos je dokončen
 }
}

void USART1_Configuration(void){
USART_InitTypeDef usart_is;
GPIO_InitTypeDef gp;
DMA_InitTypeDef dma;
NVIC_InitTypeDef nvic;

// init GPIO
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
gp.GPIO_Pin = GPIO_Pin_2; //PA2 Tx
gp.GPIO_Mode = GPIO_Mode_AF;
gp.GPIO_OType = GPIO_OType_PP;
gp.GPIO_PuPd = GPIO_PuPd_NOPULL;
gp.GPIO_Speed = GPIO_Speed_Level_3;
GPIO_Init(GPIOA, &gp);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_1); // AF1 => USART1

// init USART
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 , ENABLE);
usart_is.USART_BaudRate = 115200;
usart_is.USART_WordLength = USART_WordLength_8b;
usart_is.USART_StopBits = USART_StopBits_1;
usart_is.USART_Parity = USART_Parity_No ;
usart_is.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
usart_is.USART_Mode = USART_Mode_Tx; // pouze vysíláme
USART_Init(USART1, &usart_is);

// povolíme TXE události volat DMA request
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
// spustíme USART
USART_Cmd(USART1, ENABLE);

// init DMA
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
dma.DMA_BufferSize = MAX_LEN; // před odesláním přepíšeme jinou hodnotou
dma.DMA_DIR = DMA_DIR_PeripheralDST; // cíl přenosu - periferie
dma.DMA_M2M = DMA_M2M_Disable; // režim M2M nechceme
dma.DMA_MemoryBaseAddr = (uint32_t)(uart_buff); // adresa pole s daty k odeslání
dma.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // data jsou 8bit
dma.DMA_MemoryInc = DMA_MemoryInc_Enable; // zdrojovou adresu inkrementovat
dma.DMA_Mode = DMA_Mode_Normal; // neppoužíváme "kruhový" režim
dma.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->TDR); // cíl přenosu
dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // data jsou 8bit
dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // vše posílat na jednu adresu
dma.DMA_Priority = DMA_Priority_Low; // priorita nízká (nízký baudrate, času je dost)
DMA_Init(DMA1_Channel2, &dma); // aplikovat nastavení na channel2

if(DMA_GetFlagStatus(DMA1_FLAG_TC2)){	DMA_ClearFlag(DMA1_FLAG_TC2);} // kdyby náhodou
DMA_ITConfig(DMA1_Channel2,DMA_IT_TC,ENABLE);  // povolit přerušení od dokončení přenosu

// konfigurace přerušení
nvic.NVIC_IRQChannel = DMA1_Channel2_3_IRQn;
nvic.NVIC_IRQChannelCmd = ENABLE;
nvic.NVIC_IRQChannelPriority = 2;
NVIC_Init(&nvic);
}


Procesor se evidentně dosti nudil a během vysílání napočítal do 3000

Home
V1.03 23.7.2017 (edit. 02/2024)
By Michal Dudka (m.dudka@seznam.cz)