USART Extended features

V datasheetu k STM32F0 se v kapitole "USART Extended features" můžete dočíst, že USART obsahuje podpůrné funkce pro LIN, IrDA, Smartcard a Modbus protokol. Některé z těchto funkcí jsou docela slušně užitečné i v "běžném provozu". Jde zejména o funkci "Character Recognition" a "Timeout". Character recognition (rozpoznání znaku) vlastně slouží jako filtr příchozích dat. Pokud se data shodují s vámi zvolenou hodnotou, může USART vyvolat přerušení. Funkce "Timeout" měří dobu nečinnosti na datové lince a jakmile tato doba přesáhne vámi stanovenou hodnotu nastaví příslušnou vlajku a může volat přerušení. Tyto zdánlivě podřadné "featury" mohou být výborným pomocníkem ve spolupráci s DMA.

Seznam příkladů:

USART příjem s DMA + character recognition

Příjem s pomocí DMA je výhodný pokud přijímáte zprávy o předem dané délce. Stačí DMA vysvětlit kolik znaků zpráva obsahuje a jakmile celá dorazí dozvíte se to od něj třeba pomocí přerušení. Ne vždy ale víte jak bude zpráva dlouhá. Například při "ASCII" komunikaci je konec zprávy typicky označován nějakým speciálním znakem (např CR nebo LF). DMA ale znaky nečte a nemá tedy šanci konec zprávy rozpoznat. Zdálo-by se, že je DMA pro tento typ zpráv nepoužitelné a že vám nezbývá nic jiného než přijímat data pomocí přerušení a rovnou je na ukončovací znak testovat (jako například v této ukázce). Naštěstí některé řady STM32 (F0 nebo L4) mají výše zmíněnou funkci "character match", která umí vyvolat přerušení po příchodu vybraného znaku. Lze tedy přijímat pomocí DMA a zároveň se dozvědět o tom, že zpráva dorazila celá aniž bychom museli každý znak individuálně kontrolovat. Pojďte se podívat jak na to na STM32F0.

Základní konfiguraci USARTu ani GPIO nebudu komentovat, to by byla ztráta času. Ještě před spuštěním USARTu musíte nastavit ukončovací znak, ten se u STM32F0 zapisuje do horních 8bitů registru USART1->CR2. v SPL k tomuto účelu žádnou funkci nenajdete, takže si musíte poradit "ručně". Dále je potřeba povolit USARTu DMA requesty od přijímače a povolit přerušení od funkce "character match". Nastavení DMA je vcelku přímočaré, v naší ukázce je cílem pole rx_buff[] a zdrojem je USART1->RDR. Počet přijímaných dat v DMA musí odpovídat velikosti pole do nějž přijímáme aby nedošlo k přepisu paměti. V případě, zprávy delší než pole, se DMA po přijetí limitního počtu znaků prostě zastaví a pokud si to nastavíte může vyvolat přerušení a říct vám, že máte průser. Volbu DMA kanálu pak provedeme podle tabulky 29 v datasheetu (já používám DMA channel 3, ale po remapování lze použít i channel 5). Nakonec už stačí jen DMA spustit a zařízení je připraveno k příjmu. Drobný komentář si zaslouží ještě rutina přerušení. Po příchodu ukončovacího znaku (tedy i celé zprávy) je potřeba z DMA kanálu zjistit počet přijatých znaků a resetovat ho. Resetem DMA kanálu myslím v tomto případě znovunastavení maximálního počtu přijímaných znaků. Což lze provést jen pokud je DMA kanál vypnutý (kanál si ve vypnutém stavu ponechává veškeré nastavení). Pokud si to situace vyžaduje můžete ještě pro jistotu připravit kopii zprávy a uvolnit si tak pole rx_buff[] k dalšímu příjmu (což není nutnost, pokud jste si jistí, že zprávu zpracujete dřív než přijde další znak). Nakonec jen poslední znak zprávy (CR nebo LF) přepíšete znakem '0' a tím se z ní stane plnohodnotný řetězec se kterým je možné rozumně nakládat. Celý zdrojový kód můžete stáhnout zde zdrojak_dmawCM.c

#define MAX_LEN 64 // maximální délka přijímaného řetězce

volatile uint8_t rx_buff[MAX_LEN]; // do tohoto pole se přijímá řetězec
volatile uint8_t prikaz[MAX_LEN]; // druhý buffer na řetězec
volatile uint16_t prijatych_znaku=0;
volatile uint8_t nova_zprava=0;

int main(void){
 SystemCoreClockUpdate();
 USART1_Configuration();
 init_aux_gpio(); // PA4 jako výstup (pro měření rychlosti reakce)
 DMA_Cmd(DMA1_Channel3, ENABLE); // spusť příjem skrze DMA

 while(1){
  if(nova_zprava){ // pokud přišel nový řetězec
   prikaz[prijatych_znaku-1]=0; // na konec řetězce připsat znak '0'
   // zpracujeme příchozí řetězec...
   if(strncmp(prikaz,"ahoj",prijatych_znaku)==0){
    USART_puts("nazdar\n\r"); // ... a slušně odpovíme
   }
   else{
    USART_puts("nerozumim\n\r"); // ... a slušně odpovíme
   }
   nova_zprava=0; // zprávu jsme zpracovali
  }
 }
}


// převezmeme přijatý řetezec a připravíme se pro příjem dalšího
void USART1_IRQHandler(void){
 GPIOA->BSRR = GPIO_Pin_4; // jen k měření rychlosti reakce
 // máme sice jen jeden zdroj pro toto přerušení,
 // ale aby to bylo obecně tak si ho raději zjistím
 if(USART_GetITStatus(USART1,USART_IT_CM)){
  USART_ClearITPendingBit(USART1,USART_IT_CM); // už jej obsluhujeme
  // zjistíme si kolik jsme přijali znaků (včetně ukončovacího)
  prijatych_znaku=MAX_LEN-DMA_GetCurrDataCounter(DMA1_Channel3);
  // a zkopírujeme si je aby se uvolnil rx_buffer pro další příjem
  memcpy(prikaz,rx_buff,prijatych_znaku);
  // v několika krocích postupně resetujeme DMA kanál
  DMA_Cmd(DMA1_Channel3, DISABLE);
  // nezapomeneme znovu nastavit limit přijímaných znaků
  DMA_SetCurrDataCounter(DMA1_Channel3,MAX_LEN);
  // povolíme DMA
  DMA_Cmd(DMA1_Channel3, ENABLE);
  // od teď už může USART přijímat další řetězec
  nova_zprava=1; // dáme vědět hlavní smyčce že máme data
 }
 GPIOA->BRR = GPIO_Pin_4; // jen k měření rychlosti reakce
}

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

// konfigurace pinů Rx (PA15),Tx (PA2)
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
gp.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_15;
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); // AF USART1
GPIO_PinAFConfig(GPIOA, GPIO_PinSource15, GPIO_AF_1);

// USART1, 8b+1stop, 115200b/s, No FlowControl
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_Rx | USART_Mode_Tx;
USART_Init(USART1, &usart_is);

// Volba ukončovacího znaku (CR => '\r')
USART1->CR2 |= (USART_CR2_ADD & (((uint32_t)('\r'))<<24));
// povolit DMA pro příjem
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
USART_Cmd(USART1, ENABLE); // spustit USART

// povolit přerušení při příjmu ukončovacího znaku
USART_ITConfig(USART1,USART_IT_CM,ENABLE);
// pro jistotu smazat vlajku (co kdyby byla nastavená "z minula")
USART_ClearFlag(USART1,USART_FLAG_CM);

DMA_DeInit(DMA1_Channel3); // pro jistotu (nevím co bylo s DMA dříve)
// init DMA channel3 (kanál s requestem USART1 Rx)
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
dma.DMA_BufferSize = MAX_LEN; // maximální délka přijímaného řetězce
dma.DMA_DIR = DMA_DIR_PeripheralSRC; // směr periferie->paměť
dma.DMA_M2M = DMA_M2M_Disable; // režim M2M nechceme
dma.DMA_MemoryBaseAddr = (uint32_t)(rx_buff); // sem ukládej přenesená data
dma.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // ...jsou 8bit
dma.DMA_MemoryInc = DMA_MemoryInc_Enable; // adresu v paměti inkrementuj
dma.DMA_Mode = DMA_Mode_Normal; // nepoužíváme "kruhový" režim
dma.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->RDR); // odtud data přenášej
dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // ...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_Channel3, &dma); // aplikovat nastavení na channel3

// konfigurace NVIC pro přerušení od USART1
nvic.NVIC_IRQChannel = USART1_IRQn;
nvic.NVIC_IRQChannelCmd = ENABLE;
nvic.NVIC_IRQChannelPriority = 2;
NVIC_Init(&nvic);
}

Nezapomeňte, že software potřebuje po přijetí posledního znaku zprávu zpracovat. Pokud během této doby přijde další znak, ztratíte ho. Přirozeně tato situace jde řešit, ale to už je mimo rámec tohoto příkladu. Jen pro zajímavost jsem změřil jak dlouho toto zpracování trvá (dobu trvání signalizuji pinem PA4). Rutinu přerušení jsem pro tyto účely napsal dvakrát. Jedna verze využívá pouze funkce z knihovny SPL a druhá, optimalizovaná, verze ručně přistupuje do registrů. S použitím neoptimalizované verze je "mrtvá doba" pro 5 respektive 24 přijímaných znaků rovna 5.8 respektive 6.2us. Optimalizované verzi pak zpracování trvá 3us (5zn.) respektive 3.4us (24zn.). Problém s těmito časy by nastal při datových rychlostech 1Mb a vyšších, kde trvá příjem jednoho znaku 10us nebo méně. Pro úplnost dodám, že překlad probíhal s optimalizací -O1

USART příjem s DMA + timeout

V případě binárního přenosu dat, není možné rozpoznávat konec zprávy pomocí "ukončovacího znaku", neboť tělo zprávy může obsahovat libovolné hodnoty a mohlo by se stát, že některá z nich bude rovna "ukončovacímu znaku". Chcete-li tedy přijímat binární zprávy s neznámou délkou, musíte opět najít mechanismus jak konec zprávy poznat. V mnoha případech jsou zprávy "kompaktní", tedy mezi znaky nejsou zbytečně dlouhé mezery. A právě v takových případech můžete funkci "timeout" využít k rozpoznání konce zprávy. Stačí vhodně zvolit jak dlouho musí být datová linka v nečinnosti (jak dlouho nesmí přijít žádná data). Takový způsob přenosu klade jisté nároky na vysílač, ten musí být schopen udržet zprávu dostatečně kompaktní (nemůže si tedy během vysílání udělat "pauzu") a musí mezi jednotlivými zprávami udržovat vhodné "rozestupy" (jinak řečeno nesmí poslat dvě zprávy těsně po sobě). Nicméně tyto podmínky nejsou nijak drastické a neměl by být problém je dodržet. No a nám nezbývá než si takový mechanismus příjmu zpráv vyzkoušet.

Zdrojový kód je z větší části shodný s předchozím příkladem. Změnou je povolení RTO (Receiver timeout), které provedeme opět "ručně" protože k němu není v SPL žádná funkce. Drobné komplikace může způsobovat vlajka RTOF (která značí že "timeout" nastal) neboť není jasné jak se zachová po spuštění USARTu. Udělal jsem proto malý experiment a ověřil, že vlajka se po startu USARTu nenastaví (a to bez ohledu na to zda jako první spustím USART nebo povolím RTO). Vraťme se ale ke konfiguraci. Hodnotu "timeout" nastavujete v dolních třech bajtech registru USART1->RTOR a jednotkou je jeden baud. Čas tedy můžete vypočítat přibližně podle vztahu RTOR=T*baudrate. V mém příkladě jsem volil čas 260us, což při baudrate 115200b/s odpovídá hodnotě přibližně 30. Poslední odlišností od předchozího příkladu je konfigurace přerušení, kde namísto CM (Character Match) používáme RTO. Zdrojový kód celé ukázky si můžete stáhnout.

#define MAX_LEN 64 // maximální délka přijímaného řetězce

volatile uint8_t rx_buff[MAX_LEN]; // do tohoto pole se přijímá řetězec
volatile uint8_t prikaz[MAX_LEN]; // druhý buffer na řetězec
volatile uint16_t prijatych_znaku=0;
volatile uint8_t nova_zprava=0;

int main(void){
  SystemCoreClockUpdate();
 USART1_Configuration();
 init_aux_gpio(); // PA4 jako výstup (pro měření rychlosti reakce)
 DMA_Cmd(DMA1_Channel3, ENABLE); // spusť příjem skrze DMA

 while(1){
  if(nova_zprava){ // pokud přišel nový řetězec
   prikaz[prijatych_znaku]=0; // na konec řetězce připsat znak '0'
   // zpracujeme příchozí řetězec...
   if(strncmp(prikaz,"ahoj",prijatych_znaku)==0){
    USART_puts("nazdar\n\r"); // ... a slušně odpovíme
   }
   else{
    USART_puts("nerozumim\n\r"); // ... a slušně odpovíme
   }
   nova_zprava=0; // zprávu jsme zpracovali
  }
 }
}

// převezmeme přijatý řetezec a připravíme se pro příjem dalšího
void USART1_IRQHandler(void){
 GPIOA->BSRR = GPIO_Pin_4; // jen k měření rychlosti reakce
 // máme sice jen jeden zdroj pro toto přerušení,
 // ale aby to bylo obecně tak si ho raději zjistím
 if(USART_GetITStatus(USART1,USART_IT_RTO)){
  USART_ClearITPendingBit(USART1,USART_IT_RTO); // už jej obsluhujeme
  // zjistíme si kolik jsme přijali znaků (včetně ukončovacího)
  prijatych_znaku=MAX_LEN-DMA_GetCurrDataCounter(DMA1_Channel3);
  // a zkopírujeme si je aby se uvolnil rx_buffer pro další příjem
  memcpy(prikaz,rx_buff,prijatych_znaku);
  // v několika krocích postupně resetujeme DMA kanál
  DMA_Cmd(DMA1_Channel3, DISABLE);
  // nezapomeneme znovu nastavit limit přijímaných znaků
  DMA_SetCurrDataCounter(DMA1_Channel3,MAX_LEN);
  // povolíme DMA
  DMA_Cmd(DMA1_Channel3, ENABLE);
  // od teď už může USART přijímat další řetězec
  nova_zprava=1; // dáme vědět hlavní smyčce že máme data
 }
 GPIOA->BRR = GPIO_Pin_4; // jen k měření rychlosti reakce
}

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

// konfigurace pinů Rx (PA15),Tx (PA2)
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
gp.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_15;
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); // AF USART1
GPIO_PinAFConfig(GPIOA, GPIO_PinSource15, GPIO_AF_1);

// USART1, 8b+1stop, 115200b/s, No FlowControl
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_Rx | USART_Mode_Tx;
USART_Init(USART1, &usart_is);


USART1->CR2 |= USART_CR2_RTOEN; // povolit "timeout" funkci
USART1->RTOR = 30; // timeout 260us (30/baudrate)
// povolit DMA pro příjem
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
USART_Cmd(USART1, ENABLE); // spustit USART

// povolit přerušení od "timeout"
USART_ITConfig(USART1,USART_IT_RTO,ENABLE);

DMA_DeInit(DMA1_Channel3); // pro jistotu (nevím co bylo s DMA dříve)
// init DMA channel3 (kanál s requestem USART1 Rx)
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
dma.DMA_BufferSize = MAX_LEN; // maximální délka přijímaného řetězce
dma.DMA_DIR = DMA_DIR_PeripheralSRC; // směr periferie->paměť
dma.DMA_M2M = DMA_M2M_Disable; // režim M2M nechceme
dma.DMA_MemoryBaseAddr = (uint32_t)(rx_buff); // sem ukládej přenesená data
dma.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // ...jsou 8bit
dma.DMA_MemoryInc = DMA_MemoryInc_Enable; // adresu v paměti inkrementuj
dma.DMA_Mode = DMA_Mode_Normal; // nepoužíváme "kruhový" režim
dma.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->RDR); // odtud data přenášej
dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // ...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_Channel3, &dma); // aplikovat nastavení na channel3

// konfigurace NVIC pro přerušení od USART1
nvic.NVIC_IRQChannel = USART1_IRQn;
nvic.NVIC_IRQChannelCmd = ENABLE;
nvic.NVIC_IRQChannelPriority = 2;
NVIC_Init(&nvic);
}


Oscilogram ilustruje, že USART opravdu čeká něco málo přes 260us než vyvolá "timeout" přerušení

Home
V1.02 23.7.2017
By Michal Dudka (m.dudka@seznam.cz)