logo_elektromys.eu

/ Rychlý přenos z STM32 do PC skrze FT232H |

Často v práci potřebuji posílat data ze snímačů do PC. Na většinu aplikací si vystačím s běžným USB-UART převodníkem jako je třeba FT230, který je vybavený USB 2.0 Full Speed (12Mbit/s). S tím lze ale dosahovat přenosových rychlostí nanejvýš 300kB/s. O něco lépe na tom je FT232H (respektive FT2232H a FT4232H), ten má rozhraní USB 2.0 High Speed (480Mbit/s), ale v režimu USB-UART má maximální dosažitelnou rychlost teoreticky 12Mbaud (1MB/s), v praxi o něco nižší. Bohužel se mi občas stává, že ani toto nestačí a je potřeba přenášet data z MCU ještě o něco rychleji. Tady se pak nabízí další dvě cesty, buďto USB2.0 High speed obsluhované přímo MCU a nebo Ethernet. Obě jsou ale řádově komplikovanější než předchozí řešení s USB-UART převodníky. Takže jsem hledal nějakou střední cestu, která by byla pořád dost jednoduchá a přitom by byla schopná dosahovat rychlostí v řádu jednotek MB/s. No a tu jsem našel a rád bych se s vámi o ni podělil.

USB bridge od FTDI typicky nedisponuje pouze UART rozhraním, ale nabízí i další. Konkrétně se budeme bavit o FT232H a FT2232H (H jako USB 2.0 High Speed). Kromě obligátního UARTu je totiž možné do nich zapisovat (a číst) data i třeba paralelním rozhraním a to synchronním a asynchronním. Synchronní paralelní dosahuje přenosových rychlostí skoro 40MB/s (což máme prakticky ověřeno s pomocí FPGA). Má ale jednu fundamentální nevýhodu - FTDI generuje fixní clock 60MHz a MCU, které je na sběrnici "master" se musí k tomuto clocku synchronizovat. To je bohužel tak specifický úkol, že na to skoro žádná periferie z STM32 nestačí. Jedinou periferií, která by to měla zvládat je PSSI, jenže ta zrovna obsahuje silicon bug, kvůli kterému je pro tuto úlohu nepoužitelná (BTW: ten jsem našel a zdokumentoval, právě při pokusu o komunikaci s FT232H a už je v errata). Takže zbývá asynchronní paralelní rozhraní, které má být schopno přesouvat data s rychlostí až 8MB/s. No a to v následující ukázce použijeme.

/ Asynchronní rozhraní FT232H/FT2232H |

Paralelní asynchronní rozhraní je jednoduché. Obsahuje 8 datových linek a 4 řídící signály.

Zápis jednoho bytu z MCU do FTDI vypadá následovně (MCU drží linku RD=1):

  1. FTDI nastaví TXE=0 a signalizuje tím že je ochotno přijmou 1 byte (že má v bufferu místo)
  2. MCU zapíše na datovou sběrnici byte který chce poslat (+5ns)
  3. MCU nastaví WR do nuly (v tom okamžiku FTDI čte data) a ponechá ho tam alespoň 30ns
  4. FTDI nejpozději do 14ns od sestupné hrany na WR nastaví TXE=1 a ponechá ho v té úrovni nejméně 49ns
  5. MCU vrací WR do jedničky
  6. Pokud má FTDI místo v bufferu, vrací se do bodu 1. a celý proces se může opakovat

Časový diagram zápisu byte do FTDI

A jen pro kompletnost si dovolím popsat čtení (to nás ale nebude moc zajímat):

  1. FTDI nastaví RXF=0 (signalizuje že je připraven zapsat do MCU byte dat)
  2. MCU nastaví svoje datové piny jako vstupy (pokud je tak nemá trvale) a nastaví RD=0
  3. FTDI nejpozději do 14ns nastaví datovou sběrnici na hodnotu bytu kterou chce poslat do MCU
  4. MCU si následně přečte datové linky
  5. MCU pak nejdříve za 30ns od bodu 2. vrátí RD zpět na 1
  6. FTDI pak nejpozději do 14ns uvolní datové linky (do HiZ)
  7. FTDI zároveň nejpozději do 14ns nastaví RXF=1
  8. Jakmile má FTDI k dispozici nová data vrací se do bodu 1. a může začít další přenos

Časový diagram čtení byte z FTDI

/ Komunikační "jádro" na STM32 |

Pokud bychom nepotřebovali rychlost zápisu nebo čtení optimalizovat na maximální hodnoty, je možné vše provést metodou bit bang - tedy prostě přímo hýbat jednotlivými piny. Tak by ale bylo komplikované dosahovat velkých rychlostí a navíc by při vyšších rychlostech docházelo ke značnému vytížení jádra MCU. Pojďme se tedy podívat jak lze na STM32 přenos MCU-FTDI(PC) automatizovat pomocí periferií a tím dosáhnout maximální rychlosti a minimalizovat vytížení jádra. Přenosem z FTDI->MCU (říkejme čtením) se zabývat nebudu, ten mi stačí pomalý "bit bang" metodou.

Nejprve jsem si myslel, že to půjde zařídit pomocí FMC/FSMC (Flexible memory controller) periferie, která zprostředkovává efektivní a rychlou komunikaci mezi MCU a různými druhy pamětí (SRAM,SDRAM, Flash..). Bohužel žádný z pracovních módů nebyl schopen zakomponovat "flow control" (TXE vlajku signalizující připravenost FTDI číst data). Nejspíš proto, že běžné RAM paměti jsou prostě připravené zapsat data vždy. Takže jsem nakonec musel sáhnout po hrubé metodě za pomocí TIMeru a DMA. Způsob jak pomocí timeru a DMA posílat data na GPIO (jako paralelní sběrnici) je dobře zdokumentovaný v appnote. Ve zkratce funguje prostě tak, že TIMer běží, periodicky generuje DMA request a DMA na něj reaguje tím, že zapíše data z paměti do ODR registru, jednoduše řečeno na výstupní piny. Tento postup ale neobsahuje dva klíčové prvky pro náš komunikační protokol. Negeneruje WR puls a nezohledňuje stav TXE vlajky. Tyto dvě funkce jdou ale naštěstí přidat.

Přidat WR puls je vcelku přímočaré. Stačí na některý z výstupů timeru generovat PWM signál. Přesun dat pomocí DMA na GPIO a generování WR pulzu ale musí mít správné časování. Sestupnou hranu na WR musíme generovat až poté co jsou data spolehlivě zapsána na GPIO, respektive nejpozději ve stejný okamžik. Využijeme proto dva "PWM" kanály TIMeru. Na jeden kanál necháme generovat "compare" událost (interně) co nejdříve po startu/přetečení timeru a bude sloužit ke generování DMA requestů (zápisů dat na GPIO). Druhý kanál bude generovat regulérní PWM signál a bude zodpovědný za generování negativních WR pulzů (zápisů do FTDI).

Celý proces tedy bude vypadat následovně:


Časový diagram implementace vysílání v STM32

Předchozí konfigurace zapisuje data se správným časováním, ale není možné ji zastavovat TXE signálem. Na tohle se hodí "gate" funkce timeru. Signál TXE přivedeme na ETR vstup timeru a nastavíme ho tak aby byl timer "gatován" úrovní 0. Tedy aby čítal jen pokud je vstup ETR v 0. Tím získáme systém, který když je TXE=0 autonomně posílá data z paměti na GPIO a generuje WR pulzy - tedy jádro celé věci.

Tím ale bohužel práce nekončí. Náš systém je teď ve stavu kdy běží neustále když je TXE=0, tedy i v okamžiku kdy nemáme co vysílat ! Je potřeba ho ještě uzpůsobit aby odeslal vždy jen definované množství dat. Bylo by nepraktické, či skoro nemožné zastavit TIMer softwarově po odeslání posledního bytu. Reakční doba programu by byla prostě tak velká, že by to nestihnul a do FTDI by doputovaly nějaké nesmysly. Musíme tedy zajistit, že se timer ve vhodnou chvíli vypne sám ! Toho lze naštěstí docílit režimem "one-pulse", v němž se timer automaticky po "update" události vypne. Update událost, ale přichází typicky s každým přetečením a my rozhodně nechceme aby se vypínal s každým odeslaným bytem, protože bychom ho pak museli "ručně" zapínat a byli bychom zase na začátku u "bit bang" ovládání. Naštěstí mají Advanced Timery (TIM1, TIM8) v STM32 funkci "repetition counter". Která umožňuje nastavit, že se update událost odehraje až po definovaném množství přetečení. Můžeme tedy nastavit, že k update události (a tedy vypnutí timeru a ukončení přenosu) dojde až po například 256 přetečení - tedy po odeslání 256 bytů dat. Případně lze zvolit i jinou menší hodnotu (což se hodí pokud prostě chceme odeslat méně dat než 256). Obecně je na STM32 maximální hodnota "repetition counter" 256, ale mladší MCU už mohou mít i 16bitovou hodnotu. Jak za chvíli uvidíte, využití toho vyššího limitu může přenosovou rychlost ještě o kousek zvětšit. Protože tak jak to máme teď sestavené, musíme po každých 256 odeslaných bytech timer znovu nastavit a nastartovat. Posíláme tedy data v 256bytových "burstech". Při přenosové rychlosti 8MB/s tedy musí CPU každých 32us spustit další přenos. Než všechno nakonfiguruje, tak mu to několik us zabere a během této doby se nevysílá. Pro maximální optimalizaci je tedy vhodné maximalizovat hodnotu "repetition counter" aby tyto prodlevy nastávaly co nejméně často. My jsme ale v naší demonstrační ukázce svázání hodnotou 256. Timer po dokončení přenosu "burstu" prostě vyvolá přerušení (od "update" události) a v IRQ rutině prostě provedeme zmíněnou "rekonfiguraci" a případně pustíme přenos dalšího burstu. A to stále dokola dokud neodešleme požadovaný objem dat.

/ Zdrojový kód pro STM32 |

Prezentovaný zdrojový kód má ještě daleko k optimální a učesané variantě - je to vlastně první funkční verze. Určitě se to dá napsat elegantněji, ale jádro (konfigurace timeru a DMA) se už změn nejspíš nedočká. Ještě se musím zamyslet nad tím zda nejde v DMA zapnout FIFO a zmenšit tak počet čtení z RAM.

#include "main.h"
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_TIM1_Init(void);

#define NUM_BUFFERS_TEST (8*1024) // počet odesílaných bufferů během testu rychlosti 8*1024*4kB=32MB
uint32_t test_bursts = NUM_BUFFERS_TEST; // počet odesílaných bufferů během testu
volatile uint8_t transfer_status=0; // indikuje aktuální stav přenosu (běží / neběží)
uint32_t datacounter; // počítadlo dat, která zbývají odeslat v jednom bufferu
uint8_t buf[4096]; // datový buffer (odesílaná data)
void init_system(void); // inicializuje systém (GPIO,TIM,DMA etc.)
void init_buf(void); // naplní vysílací buffer daty (0,1,2,3...) 
char transfer_start(void); // spustí vysílaní
void transfer_stop(void); // ukončí vysílaní (po odvysílání celého bufferu)

int main(void){
 
  HAL_Init();
  SystemClock_Config(); // jádro 180MHz (90/45MHz APB sběrnice, 180MHz timery) 
  // nastavit PB7 - jen pro účely ladění
  LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOB); 
  LL_GPIO_SetPinMode(GPIOB, LL_GPIO_PIN_7, LL_GPIO_MODE_OUTPUT);
  LL_GPIO_SetPinSpeed(GPIOB, LL_GPIO_PIN_7, LL_GPIO_SPEED_FREQ_HIGH);
  LL_GPIO_SetOutputPin(GPIOB, LL_GPIO_PIN_7);

  init_buf();
  init_system();
  // nastavit rychlost pinu PA9 na maximum (pin slouží jen pro ladící účely)
  LL_GPIO_SetPinSpeed(GPIOA, LL_GPIO_PIN_9, LL_GPIO_SPEED_FREQ_HIGH);
  // počkat než apikace v PC nenastaví RXF a nedá tím MCU vědět že běží a je připravena číst data
  while(LL_GPIO_IsInputPinSet(GPIOA, LL_GPIO_PIN_1)){ // čekej dokud RXF=1 (dokud PC nepošle "dummy" byte)
  }

  while (1)
  {
 // posílej 8*1024 4kB bufferů
   while(test_bursts){  // dokud neodešleš 8192 krát buffer
    transfer_start(); // zahaj přenos nového bufferu
    while(transfer_status){ // čekej dokud přenos neskončí
     asm("nop"); // jen místo kam můžu při ladění dát breakpoint
    }
    test_bursts--; // započítej přenesený buffer 
   }
   if(test_bursts == 0){ // pokud pošleš všechny buffery, je testovací sekvence u konce
    while(transfer_status){ // čekej dokud neskončí přenos posledního bufferu
     asm("nop"); // jen místo kam můžu při ladění dát breakpoint
    }
    test_bursts=NUM_BUFFERS_TEST; // připrav se poslat dalších 8*1024 bufferů (další test)
   }
  }
}


// inicializuj buffer testovacími daty (0,1,2,3...)
void init_buf(void){
 uint32_t i;
 for(i=0;i<sizeof(buf)/sizeof(buf[0]);i++){
  buf[i]=i;
 }
}

/*
 * Externí signál (TXE vlajka z FTDI) gatuje TIM1 (negativní hodnota povoluje čítání)
 * TIM1 běží v one-pulse režimu s Repetition counterem na 256 (nebo méně pokud je to potřeba)
 * TIM1 generuje interně z CH2 DMA requesty (brzo po začátku periody, proto CCR2=1)
 * DMA transportuje paralelní data z paměti na GPIO
 * TIM1 generuje na výstup CH1 PWM signál. Sestupná hrana signalizuje zápis dat pro FTDI (WR), Čas sestupné hrany je volen tak aby DMA stihl poslat data na GPIO a ještě uplynul setup time
 * Pokud externí signál přejde do H (FTDI na TXE signalizuje že nechce data), timer se pozastaví (je gatovaný), jakmile přejde TXE zpět do L, pokračuje v odesílání
 * Jakmile Timer dokončí předem daný počet period (Repetiton counter), zastaví se a vyvolá přerušení
 * V rutině přerušení program zjistí kolik je ještě potřeba poslat dat a podle toho nastaví Repetition counter a spustí další přenos (enable timer)
 *
 * Timer tedy vždy pošle předem daný a volitelný počet dat (period, DMA requestů), 
 * Přenos tedy probíhá v "burstech" daných RTR hodnotou timeru, typicky se volí RTR co největší pokud je dat hodně. RTR je ale u starších čipů omezeno na 256, u novějších je to víc.
 * 
 * zapojení:
 * STM32   FTDI
 * PC0..7  ->   D0..7
 * PA12  <-  C1 (!TXE flag)
 * PA8  ->  C3 (!WR cmd)
 *
 * PA1  <-  C0 (!RXF flag) - set to input with pullup
 * PA0  ->  C2 (!RD cmd) - set to low
 *
 * PA9 - Pomocný výstup signalizují kdy došlo ke generování DMA requestu (v praxi se nepoužívá)
 * PB9 - Pomocný výstup signalizuje kolik času tráví aplikace v IRQ rutině (a nevysílá)
 */
 
void init_system(void){
LL_TIM_InitTypeDef TIM_InitStruct = {0};
LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0};
LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
LL_DMA_InitTypeDef dma;

// inicializace pinů
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_TIM1);
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA2 | LL_AHB1_GRP1_PERIPH_GPIOD | LL_AHB1_GRP1_PERIPH_GPIOA | LL_AHB1_GRP1_PERIPH_GPIOC);
LL_RCC_SetTIMPrescaler(LL_RCC_TIM_PRESCALER_FOUR_TIMES);

// ošetření RD a RXF pinů používaných pro příjem dat (RD musíme držet v 1, RXF je vstup)
LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_0); // RD=1
GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// RXF je vstup (potřebujeme si přečíst RXF vlajku abychom věděli že je PC aplikace připravená)
GPIO_InitStruct.Pin = LL_GPIO_PIN_1;
GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
LL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// PC0..7 ------> datová sběrnice (výstupy, do FTDI pouze zapisujeme)
GPIO_InitStruct.Pin = LL_GPIO_PIN_0|LL_GPIO_PIN_1|LL_GPIO_PIN_2|LL_GPIO_PIN_3|LL_GPIO_PIN_4|LL_GPIO_PIN_5|LL_GPIO_PIN_6|LL_GPIO_PIN_7;
GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
LL_GPIO_Init(GPIOC, &GPIO_InitStruct);

// PA12   ------> TIM1_ETR (TXE signal z FTDI)
// PA8   ------> TIM1_CH1 (WR signal do FTDI)
// PA9   ------> TIM1_CH2 (Ladící výstup pro sledování okamžiků DMA requestů)
GPIO_InitStruct.Pin = LL_GPIO_PIN_8 | LL_GPIO_PIN_9 | LL_GPIO_PIN_12;
GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
GPIO_InitStruct.Alternate = LL_GPIO_AF_1;
LL_GPIO_Init(GPIOA, &GPIO_InitStruct);

GPIO_InitStruct.Pin = LL_GPIO_PIN_12;
GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
LL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// konfigurace DMA 
dma.PeriphOrM2MSrcAddress  = (uint32_t)&(GPIOC->ODR); // zapisovat přímo na GPIOC
dma.MemoryOrM2MDstAddress  = (uint32_t)buf; // adresu bufferu nastavujeme až později ve funkci "transfer_start()"
dma.Direction     = LL_DMA_DIRECTION_MEMORY_TO_PERIPH;
dma.Mode       = LL_DMA_MODE_NORMAL; // nevyužíváme "circular" režim
dma.PeriphOrM2MSrcIncMode  = LL_DMA_PERIPH_NOINCREMENT;
dma.MemoryOrM2MDstIncMode  = LL_DMA_MEMORY_INCREMENT;
dma.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_BYTE;
dma.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_BYTE;
dma.NbData     = sizeof(buf)/sizeof(buf[0]); // počet přenášených data nastavíme později ve funkci "transfer_start()"
dma.Channel    = LL_DMA_CHANNEL_6;
dma.Priority      = LL_DMA_PRIORITY_LOW; // v praxi se tady asi nastaví vyšší priorita
dma.FIFOMode      = LL_DMA_FIFOMODE_DISABLE; // vylepšit - použití FIFO by mohlo odlehčit čtení z RAM
dma.FIFOThreshold    = LL_DMA_FIFOTHRESHOLD_1_4;
dma.MemBurst      = LL_DMA_MBURST_SINGLE;
dma.PeriphBurst   = LL_DMA_PBURST_SINGLE;
LL_DMA_Init(DMA2, LL_DMA_STREAM_2, &dma);

// TIM1 base
TIM_InitStruct.Prescaler = 0; // tick timeru  1/180M = 5.55ns
TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
TIM_InitStruct.Autoreload = 15; // perioda 16*5.55 = 88ns
TIM_InitStruct.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1;
TIM_InitStruct.RepetitionCounter = 16; // Přepíšeme později podle toho kolik dat v burstu budeme chtít poslat
LL_TIM_Init(TIM1, &TIM_InitStruct);
LL_TIM_SetOnePulseMode(TIM1, LL_TIM_ONEPULSEMODE_SINGLE); // one pulse mode - proběhne celkem "repetition counter" period

// Vstup, kterým TIM1 gatujeme - TXE signal z FTDI
LL_TIM_SetTriggerInput(TIM1, LL_TIM_TS_ETRF);
LL_TIM_SetSlaveMode(TIM1, LL_TIM_SLAVEMODE_GATED); // gate mód - čítej jen když je TXE aktivní...
LL_TIM_ConfigETR(TIM1, LL_TIM_ETR_POLARITY_INVERTED, LL_TIM_ETR_PRESCALER_DIV1, LL_TIM_ETR_FILTER_FDIV1); // ...konrkrétně když je TXE=0

// PWM výstup na pin WR
LL_TIM_OC_StructInit(&TIM_OC_InitStruct);
TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1; // PWM mod, Vzestupná hrana se startem periody, sestupná při "compare" události
TIM_OC_InitStruct.OCState = LL_TIM_OCSTATE_ENABLE;
TIM_OC_InitStruct.OCNState = LL_TIM_OCSTATE_DISABLE;
TIM_OC_InitStruct.CompareValue = 9; // vynuluje WR=0 56ns po začátku periody (negativní pulz potrvá 88-55 = 33ns)
TIM_OC_InitStruct.OCPolarity = LL_TIM_OCPOLARITY_HIGH;
LL_TIM_OC_Init(TIM1, LL_TIM_CHANNEL_CH1, &TIM_OC_InitStruct);

// Kanál generuje DMA requesty (+ pro kontrolu generuje pulzy na PA9)
TIM_OC_InitStruct.CompareValue = 1; // vytvoř DMA request 5ns po začátku periody (tedy co nejdříve to jde) -> a pošli data na port/sběrnici
LL_TIM_OC_Init(TIM1, LL_TIM_CHANNEL_CH2, &TIM_OC_InitStruct);

// advanced timer potřebuje povolit výstupy globálně
LL_TIM_EnableAllOutputs(TIM1);

// povolit přerušení od "update" události (tedy po každém "burstu" a zastavení timeru)
WRITE_REG(TIM1->SR, ~(TIM_SR_CC1IF | TIM_SR_CC2IF | TIM_SR_UIF)); 
NVIC_SetPriority(TIM1_UP_TIM10_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),0, 0));
NVIC_EnableIRQ(TIM1_UP_TIM10_IRQn);
LL_TIM_EnableIT_UPDATE(TIM1); 
}

// Rutina přerušení obstarává přípravu a spuštění přenosu dalšího burstu dat (v rámci bufferu)
void TIM1_UP_TIM10_IRQHandler(void){
 LL_TIM_ClearFlag_UPDATE(TIM1);
 LL_GPIO_SetOutputPin(GPIOB, LL_GPIO_PIN_7); // pro monitorování na osciloskopu
 // připravit nový burst
 // Pokud zbývá odelsat více jak 256 bytů, pošleme plný burst (256)
 if(datacounter > 256){
  LL_TIM_SetRepetitionCounter(TIM1, 255); // nastavit burst na 256 (asi není nutné, protože to tak bude nastavené od startu, ale pro jistotu)
  LL_TIM_EnableCounter(TIM1); // poslat další burst
  datacounter = datacounter - 256; // odpočítat byty které jsme právě začali odesílat
 } 
 else if(datacounter == 0){ // pokud už není co posílat, je celý buffer odeslaný a máme hotovo
  transfer_stop();
 }
 else{ // pokud zbývá poslat méně jak 256 dat (poslední část bufferu)
  LL_TIM_SetRepetitionCounter(TIM1, datacounter); // nastavíme repetition counter na zbývající počet bytů
  LL_TIM_EnableCounter(TIM1); // poslat další (poslední) burst
  datacounter=0; // poslali jsme poslední burst,
 }
 LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_7); // pro monitorování na osciloskopu
}


// zahájit přenos bufferu (funkci lze snadno upravit že buffer i jeho velikost budou v argumentech)
char transfer_start(void){
 uint16_t transfer_size=4096; // pro testy je veliksot bufferu fixní, ale obecně se může měnit dle potřeby
 datacounter = transfer_size; // zaznamenat si kolik bytů je potřeba poslat

 // zkontrolovat jestli je buffer větší jak burst
 if(datacounter > 256){ // první burst bude "plný" (256)
  LL_TIM_SetRepetitionCounter(TIM1, 255);
  datacounter = datacounter - 256; // započítat byty jako odeslané
 }else if(datacounter>0){ // burst je jen částečný (a bude tedy i poslední) protože buffer je malý
  LL_TIM_SetRepetitionCounter(TIM1, datacounter);
  datacounter = 0; // poslední... už nebude co posílat
 }else{ // chceme poslat 0 bytů ?! asi ne...
  return 1;
 }

 // DMA je v tomto okamžiku vypnuté, takže mu nastavíme počet bytů a adresu bufferu
 LL_DMA_SetMemoryAddress(DMA2, LL_DMA_STREAM_2, (uint32_t)buf); // nastavit adresu bufferu
 LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_2, transfer_size); // počet bytů které chceme odeslat

 // pro jistotu ještě vynuluji čítač a updatuji jeho registry
 LL_TIM_SetCounter(TIM1, 0);
 LL_TIM_DisableIT_UPDATE(TIM1);
 LL_TIM_GenerateEvent_UPDATE(TIM1);
 WRITE_REG(TIM1->SR, ~(TIM_SR_CC1IF | TIM_SR_CC2IF | TIM_SR_UIF));
 LL_TIM_EnableIT_UPDATE(TIM1);

 // signalizuji že probíhá přenos
 transfer_status=1;

 // spustím přenos
 LL_TIM_EnableDMAReq_CC2(TIM1);
 LL_DMA_ClearFlag_HT2(DMA2); // aby bylo možné DMA zapnout, musí být všechny vlajky smazané
 LL_DMA_ClearFlag_TC2(DMA2); // mažu jen HT a TC, protože ostatní se mi nemají nastavovat
 LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_2); // povoluji DMA
 LL_TIM_EnableCounter(TIM1); // povoluji timer - od teď přenos běží
 return 0;
}

// ukončit přenos (po tom co sám doběhne)
void transfer_stop(void){
 LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_2);
 LL_TIM_DisableDMAReq_CC2(TIM1);
 LL_TIM_DisableCounter(TIM1);
 transfer_status=0;
}

/ Ověřování |

Abych mohl ověřit, že to celé funguje, napsal jsem si jednoduchý program na PC, který přijímá data z MCU a zapisuje je do souboru tak dlouho než mu klávesou potvrdím, že jsou všechna a u toho měří přenosovou rychlost. A pak ještě provede kontrolu zapsaných dat (posíláme vzestupnou řadu čísel, takže se snadno kontroluje). Použitý zdrojový kód, využívající D2XX knihovny je níže. Zajímavé je, že ke komunikaci vůbec nemusíte použít D2XX knihovny, protože v tomto režimu se umí FTDI stále tvářit jako virtuální COM port, takže můžete zapisovat přímo na VCP, nebo klidně použít běžný terminálový program (což jsem zkoušel a fakt to jde, ale brzdí to rychlost). Já jsem ale chtěl maximalizovat prostupnost takže jsem využil D2XX.


Fotografie z ověřovací sestavy

Během ověřování jsem narazil na problém s chováním FTDI během volání funkce FT_Open. Když totiž počítač "otevírá" komunikaci s FTDI stávalo se mi, že FTDI přijalo od MCU nějaká data, ale ty pak nebylo možné v PC přečíst - prostě došlo k jejich ztrátě. Tento problém lze ale naštěstí relativně snadno vyřešit tak, že program na PC po "otevření" prostě pošle jednoduchý příkaz do MCU. V mém testovacím příkladě MCU ani příkaz nečte a prostě jen tupě čeká na RXF vlajku (která indikuje že FTDI má data k odeslání - a ty má jen pokud je program na PC připraven a odeslal je). V reálné praxi by jistě bylo vhodné aby MCU tato data přečetlo. Už jen proto aby došlo ke smazání RXF vlajky. Ale jak už jsem uváděl, čtení bit-bang metodou je vcelku triviální.

Výsledky dají shrnout do jediné hodnoty. Při přenosu 32MB dosahovala rychlost přes 7.5MB/s (teoretický limit je 8MB).

Zdrojový kód aplikace v PC:

#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <stdint.h>
#include "ftd2xx.h"

#define BUFFER_SIZE (64*1024) // čtecí buffer 64kB

HANDLE hFile;
FT_HANDLE ftHandle;
FT_STATUS ftStatus;
char buffer[BUFFER_SIZE]; // přijatá data
char txbuf[2]; // vysílací buffer (jen pro dummy data)
DWORD bytesRead, bytesWritten;
unsigned long int totalBytes=0; // celkový počet přijatých bytů (pro kontrolu)

LARGE_INTEGER freq; // proměnné pro měření času
LARGE_INTEGER t_start;
LARGE_INTEGER t_end;
char mereni_casu=0;

int main(void){
 // otevřu soubor kam budu zapisovat přijatá data
 hFile = CreateFileA("output.bin",GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
 if (hFile == INVALID_HANDLE_VALUE) {printf("Nelze otevrit vystupni soubor\n"); return 1;}
 // pro měření času
 QueryPerformanceFrequency(&freq);

 // otevřít FTDI (takhle jak je to napsané musí být v PC jen jedno FTDI, jinak je potřeba použít vhodnější způsob otevření který identifikuje konkrétní čip)
 ftStatus = FT_Open(0, &ftHandle);
 if (ftStatus != FT_OK){printf("Err\n");return 1;}else{printf("OK.\n");}
 // nastavit timeouty pro zápis
 ftStatus = FT_SetTimeouts(ftHandle,20,20);
 if(ftStatus != FT_OK){printf("timeout A status not ok %lu\n", ftStatus);}
 // nastavit buffery a latency timer v USB
 FT_SetUSBParameters(ftHandle, 65536, 65536);
 FT_SetLatencyTimer(ftHandle, 1);

 // promazat všechna data v bufferech
 ftStatus = FT_Purge(ftHandle, FT_PURGE_RX | FT_PURGE_TX);
 printf("FT_Purge %u\n",ftStatus);
 if(ftStatus != FT_OK){printf("Error FT_PURGE");return 1;}

 // poslat dummy data do FTDI, to pak nastaví RXF=0 a MCU se dozví že program běží a může poslat testovací balík dat
 if(FT_Write(ftHandle, txbuf, 1, &bytesWritten) != FT_OK){printf("error FT_Write\n");return 1;}
 else{printf("Written %lu\n",bytesWritten);}

 while(1){
  // zkouším číst data z FTDI
  ftStatus = FT_Read(ftHandle, buffer, BUFFER_SIZE, &bytesRead);
  if (ftStatus != FT_OK){printf("FT_Read Error\n");break;}
  // pokud jsem něco přečetl, je čas zapnout stopky (měříme čas přenosu)
  if(!mereni_casu && bytesRead>0){mereni_casu=1;QueryPerformanceCounter(&t_start);}
  // pokud jsem přečetl 0 bytů a byly spuštěné stopky, je přenos ukončen a zastavím stopky
  if(mereni_casu==1 && bytesRead==0){QueryPerformanceCounter(&t_end);mereni_casu=2;}
  // započítat přijaté byty
  totalBytes += bytesRead;
  // zapsat přečtený buffer do souboru
  if (bytesRead > 0) {
   if (!WriteFile(hFile, buffer, bytesRead, &bytesWritten, NULL)) {printf("Chyba pri zapisu do souboru\n");break;}
  }
  // zkontrolovat stisk klávesy a případně zastavit program
  if (_kbhit()) {_getch();break;}
 }

 // czavřít FTDI a soubor
 FT_Close(ftHandle);
 CloseHandle(hFile);
 // vytisknout "report"
 printf("Total: %lu\n",totalBytes);
 double elapsed_s =(double)(t_end.QuadPart - t_start.QuadPart) / freq.QuadPart;
 double speed = ((double)totalBytes/(1024*1024))/elapsed_s;
 printf("Elapsed ms: %6.2fms\n",elapsed_s*1000.0);
 printf("Speed: %3.3fMB/s\n",speed);


 // ------------------------- kontrola dat -----------------------------------
 // otevřít soubor s přijatými daty a zkontrolovat jestli je první byte 0 a každý další o 1 větší
 FILE *file = fopen("output.bin", "rb");
 if (file == NULL)
 {
  perror("Error file opening\n");
  return 1;
 }

 unsigned char prev_byte;
 unsigned char curr_byte;
 long long total_bytes = 0;
 int mismatch_count = 0;

 if (fread(&prev_byte, 1, 1, file) != 1)
 {
  printf("File is empty\n");
  fclose(file);
  return 1;
 }


 if (prev_byte != 0)
 {
  printf("error at address 0 (value %u)\n", prev_byte);
  mismatch_count++;
 }

 total_bytes = 1;
 long long address = 1;

 while (fread(&curr_byte, 1, 1, file) == 1)
 {
  unsigned char expected = (unsigned char)(prev_byte + 1);

  if (curr_byte != expected)
  {
   printf("Nesrovnalost na adrese %lld: ocekavano %u, nalezeno %u\n",
       address, expected, curr_byte);
   mismatch_count++;
  }

  prev_byte = curr_byte;
  total_bytes++;
  address++;
 }

 fclose(file);
 // vypsat report
 printf("\n===== REPORT =====\n");
 printf("Number of bytes: %lld\n", total_bytes);
 printf("number of mismatch: %d\n", mismatch_count);

 if (mismatch_count == 0)
 {
  printf("Check succesful.\n");
 }
 else
 {
  printf("Check unsuccesful.\n");
 }
 return 0;
}

| Poznámky /

| Zdrojové kódy /

Znaková sada UTF-8 (pro korektní zobrazení diakritiky stáhněte a otevírejte z PC)

| Odkazy /

Home
V1.0 12.2.2026
By Michal Dudka (m.dudka@seznam.cz)