Choď na obsah Choď na menu
 

Vyvoj USB ovladaca 3

Vývoj USB ovládača – 3. časť

              4. 2. 2013       25 784×

V tretej časti dokončíme kód pre naše USB zariadenie. Využijeme 3 rôzne prenosové módy na komunikáciu s hostom, ktorý bude následne vedieť ovládať LED diódy, kresliť na obrazovku a čítať teplotu procesora spolu so stavom tlačidiel na vývojovej doske.

REKLAMA
 
REKLAMA
 
 

Zhrnutie funkcií zariadenia

Na znázornenie využitia bulk, interrupt a control prenosových módov bude mať zariadenie nasledovné funkcie:

  • Control Out – (prenos cez kontrolný endpoint smerom od hosta k zariadeniu) – host bude môcť nastaviť, ktoré LED diódy budú svietiť. Vývojová doska SAM3X-EK obsahuje 3 LED: modrú, oranžovú a zelenú, z toho oranžová bude ovládaná pomocou PWM (Pulse Width Modulation), vďaka čomu budeme schopní meniť intenzitu svetla.
    Ďalej bude môcť ovládať LCD – spôsobom zapnúť/vypnúť a intenzitu podsvietenia LCD.
  • Interrupt In – (prenos cez interrupt endpoint smerom od zariadenia k hostovi) – pri zmene stavu tlačidiel na vývojovej doske zariadenie vygeneruje paket a spustí prenos na tomto endpointe. SAM3X-EK obsahuje 2 mechanické tlačidlá, 5 QTouch tlačidiel a jeden QTouch slider.
  • Bulk Out – (prenos cez bulk endpoint smerom od hosta k zariadeniu) – bude slúžiť hostovi na vykresľovanie na LCD obrazovku. SAM3X-EK obsahuje LCD o rozmeroch 320x240 pixelov. Vykresľovanie bude môcť byť textové, kedy host odošle text a zariadenie tento text zobrazí na displeji alebo bitmapové, kedy host odošle bitmapu vo formáte RGB16 (R5G6B5).

Na to, aby sme všetky tieto funkcie dosiahli, musí naše zariadenie využiť hneď niekoľko komponentov z ASF (Atmel Software Framework). Tieto komponenty nebudú v kóde použité priamo ale cez sadu metód, ktorých zdrojový kód si môžete stiahnuť tu. Čitateľ nemusí poznať to, čo stojí za tým, aby bola aplikácia schopná zistiť napr. teplotu procesora. Stačí ak vie, ako vložiť túto teplotu do paketu, ktorý bude odoslaný hostovi.

  • ADC – Analog to Digital Converter – prevodník analógového signálu na digitálny. Tento komponent potrebujeme v dvoch prípadoch, a to keď budeme zisťovať úroveň napätia na vstavanom potenciometri a pri čítaní teploty zo senzora. Naša aplikácia bude k ADC pristupovať pomocou funkcií usbapp_adc_*
  • PWM – Pulse Width Modulation – použijeme na ovládanie intenzity svietivosti LED diódy. Aplikácia bude PWM využívať pomocou funkcií usbapp_pwm_*
  • HX8347-A – LCD displej, ktorý využije host na vykresľovanie textu alebo obrázkov, aplikácia ho bude využívať pomocou funkcií usbapp_lcd_*
  • QTouch – technológia kapacitných dotykových funkcií od Atmelu. Aplikácia bude stavy QTouch tlačidiel aktualizovať pomocou funkcií usbapp_qtouch_*

Ako bolo v predchádzajúcej časti tohto seriálu spomenuté, aplikácia bude bežať v cykle a každých 25 milisekúnd bude kontrolovať stav tlačidiel a aktualizovať hodnoty z ADC. Keď aplikácia zistí zmenu stavu tlačidiel, vyvolá interrupt in požiadavku, ktorá hostovi odošle aktuálny stav tlačidiel. Ostatné prenosové módy budú spravované iným spôsobom. O kontrolnom prenose sa aplikácia dozvie, keď UDI zavolá funkciu app_setup_in_received alebo app_setup_out_received. Bulk prenos nastavíme vo funkcii app_vendor_enable, ktorú UDI zavolá, keď host povolí – aktivuje naše rozhranie. Vtedy oznámime UDI, že sme pripravení prijímať dáta na endpointe určenom pre bulk out.

Kód aplikácie zariadenia

V prvom rade si musíme definovať v akých dátových štruktúrach budú naše informácie medzi hostom a zariadením prenášané a taktiež, aké hodnoty tieto štruktúry budú môcť uchovávať. V nasledujúcich riadkoch môžete vidieť definície takýchto štruktúr:

// Max. plocha bitmapy (px^2)
#define USBAPP_MAX_BITMAP_SIZE 8100

// Nastavenia pre displej
typedef enum
{
  // Displej zapnutý
  DisplayOption_DisplayOn = 0x1,
  // Podsvietenie zapnuté
  DisplayOption_BacklightOn = 0x2
} usbapp_control_display_options_t;

// Nastavenia LED diód
typedef enum
{
  LedOption_Blue = 0x1,
  LedOption_Green = 0x2,
  LedOption_Amber = 0x4
} usbapp_control_led_options_t;

// Paket pre SETUP OUT
typedef struct
{
  // Ktoré LED diódy majú svietiť
  uint8_t leds;
  // Perióda pre diódu ovládanú cez PWM
  uint8_t pwmLedDuty;
  // Nastavenia displeja
  uint8_t display;
  // Úroveň podsvietenia
  uint8_t backlight;
  // Skrolovanie obsahu displeja
  uint16_t displayScroll;
} usbapp_control_out_t;

// Typ požiadavky pre SETUP OUT
typedef enum
{
  // Paket obsahuje dáta usbapp_control_out_t
  ControlType_Out = 0x1,
  // Paket obsahuje dáta usbapp_control_in_t
  ControlType_In = 0x2
} usbapp_control_types_t;

// Paket pre SETUP IN
typedef struct
{
  // Hodnota potenciometra
  uint16_t potentiometer;
  // Digitálna hodnota senzora teploty
  uint16_t temperatureDigital;
  // Teplota
  float temperature;
} usbapp_control_in_t;

// Paket pre INTERRUPT IN
typedef struct
{
  // Ktoré tlačidlá sú stlačené
  uint8_t buttons;
  // Hodnota slider-u
  uint8_t slider;
} usbapp_interrupt_in_t;

// Nastavenia pre BULK OUT
typedef enum
{
  // Paket obsahuje bitmapu na vykreslenie
  DrawOption_Bitmap = 0x0,
  // Paket obsahuje text na vykreslenie
  DrawOption_Text = 0x80000000,
  // Vykresliť text v riadkoch
  DrawOption_TextLine = 0x40000000,
  // Vyčistiť obrazovku
  DrawOption_Clear = 0x20000000,
  // Maska pre údaj obsahujúci farbu
  DrawOption_ColorMask = 0x00FFFFFF
} usbapp_bulk_options_t;

// Hlavička pre BULK OUT
typedef struct
{
  // Pozícia X na LCD
  uint16_t posX;
  // Pozícia Y na obrazovke
  uint16_t posY;
  // Šírka bitmapy
  uint16_t width;
  // Výška bitmapy
  uint16_t height;
  // Ďalšie nastavenia (usbapp_bulk_options_t)
  uint32_t options;
} usbapp_bulk_out_t;
  • usbapp_control_out_t – Štruktúra dát odosielaná v pakete na kontrolnom endpointe v smere od hosta k zariadeniu. Obsahuje tieto informácie:
    • leds – ktoré LED majú byť rozsvietené, toto políčko obsahuje hodnoty definované v dátovom type usbapp_control_led_options_t.
    • pwmLedDuty – úroveň aktívnej doby periódy výstupu PWM pre oranžovú LED diódu. Táto hodnota je v rozpätí 0 – 100 pričom 0 znamená, že na výstupe PWM sa aktívna časť neobjaví, v prípade použitia hodnoty 50 bude 50% jednej periódy v aktívnej úrovni, v prípade 100 bude celá perióda v aktívnej úrovni.
    • display – nastavenia displeja a podsvietenia (zapnuté/vypnuté). V tomto políčku sa používajú hodnoty z dátového typu usbapp_control_display_options_t.
    • backlight – úroveň podsvietenia LCD, hodnota v rozpätí 0 – 32.
    • displayScroll – skrolovanie obsahu LCD displeja, hodnota je v rozpätí 0 – 319 (výška displeja – 1).
  • usbapp_control_in_t – Štruktúra dát odosielaná v pakete na kontrolnom endpointe v smere od zariadenia k hostovi. Obsahuje informácie z prevodníka ADC:
    • potentiometer – úroveň napätia na potenciometri v digitálnej podobe, maximálna hodnota je 4095, pri ktorej je napätie rovné 5.0V.
    • temperatureDigital – digitálna úroveň napätia na senzore teploty, maximálna hodnota je 4095, pri ktorej je napätie rovné 3.3V.
    • temperature – teplota v stupňoch Celzia.
  • usbapp_interrupt_in_t – Štruktúra dát odosielaná v pakete na interrupt endpointe v smere od zariadenia k hostovi. Obsahuje stavy tlačidiel na vývojovej doske ako aj hodnotu dotykového bežca:
    • buttons – bitová mapa stavu mechanických a QTouch tlačidiel
    • slider – posledná nameraná hodnota dotykového bežca
  • usbapp_bulk_out_t – Štruktúra hlavičky odosielaná v pakete na bulk endpointe v smere od hosta k zariadeniu. Obsahuje informácie na vykresľovanie obrázku alebo textu na LCD. Text alebo obrázok nasleduje v pakete za touto hlavičkou.
    • posX – pozícia X na obrazovke, kde sa začne vykresľovať
    • posY – pozícia Y na obrazovke, kde sa začne vykresľovať
    • width – šírka obrázka
    • height – výška obrázka
    • options – nastavenia vykresľovania, toto políčko obsahuje hodnoty z dátového typuusbapp_bulk_options_t

Premenné a inicializácia

Náš kód z druhého článku doplníme o niekoľko nových premenných a taktiež musíme pri spustení aplikácie inicializovať všetky moduly, ktoré sme do projektu pridali (ADC, PWM, LCD a QTouch).

// Počítadlo milisekúnd pre QTouch
static volatile uint32_t appQTouchTime = 0;
// Určuje, či aplikácia čaká na odoslanie INTERRUPT IN
static volatile bool appInterruptInWaiting = false;
// Dáta (payload) pre SETUP OUT paket
static usbapp_control_out_t appDataControlOut;
// Dáta (payload) pre SETUP IN paket
static usbapp_control_in_t appDataControlIn;
// Dáta (payload) pre INTERRUPT IN paket
static usbapp_interrupt_in_t appDataInterruptIn;
// Dáta (payload) pre BULK OUT paket
// sizeof(usbapp_bulk_out_t) + 8100 * sizeof(uint16_t)
static char appDataBulkOut[16212];

Pre každý prenosový mód budeme potrebovať zásobník dát, kde USB uloží alebo odkiaľ načíta prenášané dáta. V prípade bulk prenosu si vytvoríme zásobník o veľkosti hlavičky plus max. veľkosť obrázku v px2 * 2 (resp. sizeof(uint16_t), pretože LCD používa formát RGB16).

Premenná appQTouchTime bude obsahovať čas v milisekundách od spustenia aplikácie, pretože QTouch potrebuje túto informáciu ku svojej funkčnosti.

Pri zistení zmeny stavu tlačidiel bude procesor odosielať interrupt paket hostovi, pokiaľ sa ale ešte predchádzajúci paket nespracoval, ďalší odoslať nemôžeme. Na to nám bude slúžiť premenná appInterruptInWaiting, ktorá sa vynuluje, keď UDC dokončí prenos interrupt paketu.

Ďalším krokom je inicializácia modulov ADC, PWM, LCD a QTouch. ADC a PWM napr. potrebujú vedieť, ktoré vstupy/výstupy majú povoliť, LCD inicializuje SMC (Static Memory Controller) a následne ovládač displeja HX8347A, QTouch potrebuje vedieť, ktoré tlačidlá má povoliť a na ktorých portoch sa nachádzajú. Taktiež potrebujeme povoliť obvody, na ktorých sú pripojené LED diódy a mechanické tlačidlá.

// Povoliť obvody pre LED a mechanické tlačidlá
pmc_enable_periph_clk(ID_PIOA);
pmc_enable_periph_clk(ID_PIOB);
pmc_enable_periph_clk(ID_PIOE);

// Inicializovať displej
usbapp_lcd_initialize();

// Inicializovať ADC
usbapp_adc_initialize();

// Inicializovať PWM
usbapp_pwm_initialize();

// Inicializovať QTouch
usbapp_qtouch_initialize();

Interrupt IN

Jedinou úlohou aplikácie v hlavnom cykle bude kontrolovať stav tlačidiel a aktualizovať výstup z ADC pre potenciometer a teplomer. Tieto aktivity vykoná aplikácia každých 25 milisekúnd a pri zistení zmeny stavu tlačidiel vygeneruje požiadavku na endpoint interrupt in. Hodnoty z ADC sa iba aktualizujú v premennej appDataControlIn, hostovi budú odoslané, až keď si ich vyžiada.

while (1)
{
  if (appTicks > 25)
  {
    appTicks = 0;
    appQTouchTime += 25;

    // Získať stav tlačidiel a QTouch a spustiť konverziu ADC
    // pre potenciometer a teplomer
    if (appVendorEnabled)
    {
      // Aktualizovať stav tlačidiel a QTouch
      usbapp_qtouch_measure(appQTouchTime);
      usbapp_pushbuttons_measure();

      // Konverzia ADC pre potenciometer a senzor teploty
      usbapp_adc_convert();

      // Pokiaľ sa zmenil stav tlačidiel
      if (usbapp_qtouch_changed() || usbapp_pushbuttons_changed())
      {
        // Zaznamenať stav tlačidiel a bežca
        appDataInterruptIn.buttons =
          usbapp_qtouch_buttons() | usbapp_pushbuttons_buttons();
        appDataInterruptIn.slider = usbapp_qtouch_slider();

        // Vynulovať zmeny tlačidiel
        usbapp_qtouch_clear_changed();
        usbapp_pushbuttons_clear_changed();

        // Odoslať INTERRUPT IN
        app_run_interrupt_in();
      }

      // Aktualizovať hodnoty v premennej pre ADC
      appDataControlIn.potentiometer =
        (uint16_t)usbapp_adc_read_potentiometer();
      appDataControlIn.temperature =
        usbapp_adc_read_temperature(&(appDataControlIn.temperatureDigital));
    }
  }
}

Na spustenie prenosu na endpointe interrupt in použije aplikácia funkciu app_run_interrupt_in:

// Spustí prenos na INTERRUPT IN endpointe
void app_run_interrupt_in(void)
{
  // Pokiaľ ešte neodosielame INTERRUPT IN a zároveň je zariadenie
  // aktívne
  if (!appInterruptInWaiting && appVendorEnabled)
  {
    appInterruptInWaiting = true;

    // Spustiť prenos na INTERRUPT IN endpointe
    udi_vendor_interrupt_in_run((uint8_t *)&appDataInterruptIn,
      sizeof(usbapp_interrupt_in_t), app_callback_interrupt_in);
  }
}

// Interrupt IN odoslaný
void app_callback_interrupt_in(udd_ep_status_t status,
  iram_size_t nb_transfered, udd_ep_id_t ep)
{
  appInterruptInWaiting = false;
}

Prenos na bulk a interrupt endpointe v princípe spočíva v oznámení UDI, aby na daný endpoint pripravil zásobník, ktorý sme mu odovzdali spolu s jeho veľkosťou a funkciou (tzv. callback), ktorú UDD zavolá po dokončení prenosu (úspešného alebo nie) na danom endpointe. V prípade funkcie app_run_interrupt_in toto dosiahneme pomocou funkcie udi_vendor_interrupt_in_run, ako zásobník dát použijeme premennú appDataInterruptIn a callback funkciu app_callback_interrupt_in. UDD priradí endpointu interrupt in zásobník spolu s callback funkciou a akonáhle host odošle TOKEN IN na daný endpoint, UDD spustí prenos s našim zásobníkom a po dokončení zavolá funkciu app_callback_interrupt_in. Je to ako keď posielate e-mail a vyžadujete potvrdenie o prečítaní. Neviete, kedy si ho adresát otvorí, ale keď tak urobí, dostanete potvrdenie o prečítaní.

Možno vám vŕta hlavou, keďže už viete, že každý endpoint má svoju adresu, kde sme ju pri spustení prenosu na tomto endpointe definovali my? UDI Vendor už má svoje adresy definované, pre interrupt in je priradená 0x83. Adresa je vždy zložená z dvoch častí:

0x83
  ||-- číslo endpointu
  |--- smer endpointu, IN = 0x80, OUT = 0x00

Volaním funkcie udi_vendor_interrupt_in_run sa spustí tento kód z Atmel Software Framework:

bool udi_vendor_interrupt_in_run(uint8_t * buf, iram_size_t buf_size,
    udd_callback_trans_t callback)
{
  // #define USB_EP_DIR_IN 0x80
  // #define UDI_VENDOR_EP_INTERRUPT_IN (3 | USB_EP_DIR_IN)
  return udd_ep_run(UDI_VENDOR_EP_INTERRUPT_IN,
      false,
      buf,
      buf_size,
      callback);
}

Kontrolný prenos

Ku kontrolnému endpointu nemáme iný prístup ako pomocou volania funkcie app_setup_in_received alebo app_setup_out_received, pretože tento neslúži len našej aplikácií ale aj ďalším vrstvám USB, napr. na odosielanie deskriptorov. Funkciu app_setup_in_received resp. app_setup_out_received volá UDI, keď obdrží neštandardný paket určený rozhraniu, tým pádom je určený nám. Na rozdiel od ostatných prenosových módov, kedy aplikácia pracuje iba so zásobníkom dát, ktoré budú prenášané, pri kontrolnom prenose budeme využívať aj SETUP paket, ktorý je v kontrolnom prenose odosielaný vždy. Štruktúra SETUP paketu:

typedef struct {
  uint8_t bmRequestType;
  uint8_t bRequest;
  le16_t wValue;
  le16_t wIndex;
  le16_t wLength;
} usb_setup_req_t;

Našu aplikáciu budú zaujímať dva políčka tejto štruktúry a to wValue a wLength. Hodnota wValue bude závisieť od toho, ktorým smerom kontrolný paket posielame. Pokiaľ budeme od zariadenia požadovať informácie z prevodníka ADC, wValue bude obsahovať hodnotu ControlType_In dátového typu usbapp_control_types_t. Ak bude host odosielať kontrolný paket zariadeniu, hodnotu wValue nastaví na ControlType_Out. Je dôležité poznamenať, že tento krok je možné vynechať, pretože zariadenie smer prenosu určí z políčka bmRequestType a podľa toho zavolá príslušnú funkciu, app_setup_in_received ak má zariadenie odoslať dáta hostovi alebo app_setup_out_received, pokiaľ host odošle dáta zariadeniu. Políčko wValue využívame iba z ukážkového dôvodu, aby sme vedeli ako nastaviť SETUP paket v ovládači. Využiť by sme to mohli napr. takým spôsobom, keby sme pre wValue definovali dve hodnoty, jednu pre potenciometer a jednu pre teplomer. Keď by host požadoval údaje z ADC pomocou kontrolného prenosu, vždy by musel určiť, ktorú hodnotu mu má zariadenie poslať, potenciometer alebo teplomer. My ale v aplikácii odošleme obidva údaje spolu.
Políčko wLength obsahuje počet dát, ktoré má zariadenie odoslať alebo ktoré host zariadeniu odošle. V našom prípade to bude vždy buď veľkosť štruktúry usbapp_control_in_t alebo usbapp_control_out_t.

Ďalší dôležitý fakt, na ktorý treba poukázať je, že UDI volá funkciu app_setup_out_received predtým, než prijme ďalšie dáta na kontrolnom endpointe. V praxi to znamená, že v tejto funkcii dostaneme priestor skontrolovať, či SETUP paket obsahuje správnu hodnotu wValue a či sa veľkosť dát, ktoré budú nasledovať, rovná veľkosti štruktúry usbapp_control_out_t. V tejto funkcii musíme taktiež oznámiť UDD, ktorú funkciu (callback) má zavolať, keď prenesie aj zostávajúce dáta. Tým pádom sa bude kontrolný prenos v smere od hosta k zariadeniu odohrávať v dvoch krokoch. V prvom skontrolujeme, či je SETUP paket nastavený správne a či veľkosť dát, ktoré chce host odoslať, súhlasí s veľkosťou našej štruktúry. V druhom kroku, keď UDD dokončí prenos, z paketu extrahujeme dáta, ktoré host preniesol a podľa nich nastavíme LED a LCD.

Pri smere od zariadeniu k hostovi je to jednoduchšie. Naša funkcia iba nastaví payload na štruktúru obsahujúcu hodnoty z ADC. UDI sa postará o ďalšiu prácu za nás.

Control IN

UDI informuje aplikáciu o kontrolnom pakete pomocou app_setup_in_received. Aplikácia skontroluje, či sa veľkosť dát, ktoré host požaduje, rovná veľkosti štruktúry usbapp_control_in_t. Taktiež skontroluje, či sa hodnota v políčku wValue rovná ControlType_In (0x02). V prípade, že SETUP paket tieto dve podmienky spĺňa, aplikácia nastaví ako zdroj payload dát pre odpoveď odkaz na premennú appDataControlIn, obsahujúcu údaje z prevodníka ADC. Následne vráti true a UDI môže prenos dokončiť. Pokiaľ by bola hodnota wValue alebo wLength nesprávna, funkcia vráti false. UDI v tom prípade prenos ukončí chybou STALL (v zariadení vznikla chyba).

// Spracovať požiadavku IN z kontrolného paketu
bool app_setup_in_received(void)
{
  // Pokiaľ sme prijali SETUP IN paket určený nášmu rozhraniu
  // a zároveň je označený ako ControlType_In
  if (udd_g_ctrlreq.req.wValue == ControlType_In &&
    udd_g_ctrlreq.req.wLength == sizeof(usbapp_control_in_t))
  {
    // Nastaviť kontrolný paket tak, aby odoslal dáta
    // zo zásobníka appDataControlIn
    udd_g_ctrlreq.payload = (uint8_t *)&appDataControlIn;
    udd_g_ctrlreq.payload_size = sizeof(usbapp_control_in_t);

    return true;
  }

  return false;
}

Control OUT

UDI informuje aplikáciu o kontrolnom pakete pomocou app_setup_out_received. Aplikácia skontroluje, či sa veľkosť dát, ktoré bude host posielať, rovná veľkosti štruktúry usbapp_control_out_t. Taktiež skontroluje, či sa hodnota v políčku wValue rovná ControlType_Out (0x01). V prípade, že SETUP paket tieto dve podmienky spĺňa, aplikácia nastaví ako cieľ payload (kde sa prijaté dáta skopírujú) odkaz na premennú appDataControlOut. Aplikácia musí taktiež nastaviť callback funkciu, ktorú UDD zavolá akonáhle dokončí prenos na tomto endpointe. Naša callback funkcia potom spracuje prijaté dáta.

// Spracovať požiadavku OUT z kontrolného paketu
bool app_setup_out_received(void)
{
  // Pokiaľ sme prijali SETUP OUT paket určený nášmu rozhraniu
  // a zároveň je označený ako ControlType_Out
  if (udd_g_ctrlreq.req.wValue == ControlType_Out &&
    udd_g_ctrlreq.req.wLength == sizeof(usbapp_control_out_t))
  {
    // Nastaviť kontrolný paket tak, aby boli dáta (payload)
    // skopírované do zásobníka appDataControlOut
    udd_g_ctrlreq.payload = (uint8_t *)&appDataControlOut;
    udd_g_ctrlreq.payload_size = sizeof(usbapp_control_out_t);

    // A po dokončení prenosu bude zavolaná naša funkcia
    udd_g_ctrlreq.callback = app_callback_control_out;

    return true;
  }

  return false;
}

Po dokončení prenosu UDD zavolá našu callback funkciu app_callback_control_out. Pokiaľ počas prenosu nevznikla chyba, premenná appDataControlOut bude obsahovať dáta, ktoré host odoslal zariadeniu, čiže nastavenia LED diód a LCD. V nižšie uvedenom kóde vidno, ako aplikácia podľa nastavení z appDataControlOut nastaví LED diódy, LCD obrazovku, podsvietenie a PWM pre oranžovú LED.

// Control (setup) OUT prijatý
void app_callback_control_out(void)
{
  // Nastaviť LED diódy
  if (appDataControlOut.leds & LedOption_Blue)
    gpio_set_pin_low(LED0_GPIO);  // Rozsvietiť modrú LED
  else
    gpio_set_pin_high(LED0_GPIO); // Zhasnúť modrú LED
  if (appDataControlOut.leds & LedOption_Green)
    gpio_set_pin_low(LED1_GPIO);
  else
    gpio_set_pin_high(LED1_GPIO);

  // Nastaviť oranžovú LED (s povolením PWM)
  if (((appDataControlOut.leds & LedOption_Amber) && !usbapp_pwm_is_enabled()) ||
    (((appDataControlOut.leds & LedOption_Amber) == 0) && usbapp_pwm_is_enabled()))
  {
    // Aktivovať PWM kanál pokiaľ bol vypnutý a má sa zapnúť
    if (appDataControlOut.leds & LedOption_Amber)
      usbapp_pwm_enable_led();
    else // Vypnúť PWM kanál, pokiaľ bol zapnutý a má sa vypnúť
      usbapp_pwm_disable_led();
  }

  // Aktualizovať periódu PWM, pokiaľ sa zmenila a PWM je aktívne
  if (appDataControlOut.leds & LedOption_Amber)
  {
    if (appDataControlOut.pwmLedDuty != (uint8_t)usbapp_pwm_get_led())
      usbapp_pwm_set_led((uint32_t)appDataControlOut.pwmLedDuty);
  }

  // Vypnúť ADC vstupy, kvôli zmene podsvietenia (bug ?)
  usbapp_adc_disable_channels();

  // Nastaviť podsvietenie
  if (((appDataControlOut.display & DisplayOption_BacklightOn) && !usbapp_lcd_is_backlight_on()) ||
    (((appDataControlOut.display & DisplayOption_BacklightOn) == 0) && usbapp_lcd_is_backlight_on()))
  {
    // Zapnúť podsvietenie, pokiaľ bolo vypnuté a má sa zapnúť
    if (appDataControlOut.display & DisplayOption_BacklightOn)
      usbapp_lcd_set_backlight((uint32_t)appDataControlOut.backlight);
    else // Vypnúť podsvietenie, pokiaľ bolo zapnuté a má sa vypnúť
      usbapp_lcd_backlight_off();
  }
  else if ((appDataControlOut.display & DisplayOption_BacklightOn) &&
    appDataControlOut.backlight != usbapp_lcd_get_backlight())
  {
    // Aktualizovať úroveň podsvietenia
    usbapp_lcd_set_backlight((uint32_t)appDataControlOut.backlight);
  }

  // Znovu povoliť ADC vstupy
  usbapp_adc_enable_channels();

  // Nastaviť LCD
  if (((appDataControlOut.display & DisplayOption_DisplayOn) && !usbapp_lcd_is_on()) ||
    (((appDataControlOut.display & DisplayOption_DisplayOn) == 0) && usbapp_lcd_is_on()))
  {
    // Zapnúť displej, pokiaľ bol vypnutý a má sa zapnúť
    if (appDataControlOut.display & DisplayOption_DisplayOn)
      usbapp_lcd_on();
    else // Vypnúť displej, pokiaľ bol zapnutý a má sa vypnúť
      usbapp_lcd_off();
  }

  // Nastaviť skrolovanie displeja
  if (usbapp_lcd_is_on() && appDataControlOut.displayScroll != usbapp_lcd_get_scroll())
    usbapp_lcd_set_scroll(appDataControlOut.displayScroll);
}

Bulk OUT

Interrupt prenos sme inicializovali pri zmene stavu tlačidiel, o kontrolnom prenose nás informoval UDI, ale v prípade bulk prenosu musíme oznámiť UDI, že sme pripravený prijať dáta na endpointe bulk out. Pokiaľ by host odosielal dáta na endpoint, ktorý by sme vopred nenastavili tak, aby tieto dáta získala naša aplikácia, prenos by sa nevykonal. Preto upravíme funkciu app_vendor_enable, ktorú volá UDI keď povolí naše rozhranie, tým pádom vieme, že ovládač na strane hosta je pripravený s našim zariadením komunikovať.

// Rozhranie zariadenia bolo povolené
bool app_vendor_enable(void)
{
  appVendorEnabled = true;

  // Spustiť prenos na BULK OUT endpointe,
  // UDD nás informuje pomocou app_callback_bulk_out akonáhle
  // obdrží paket na tomto endpointe
  udi_vendor_bulk_out_run(appDataBulkOut, sizeof(appDataBulkOut),
    app_callback_bulk_out);

  return true;
}

Podobne ako vo funkcii app_run_interrupt_in, aj tu oznámime prostredníctvom UDI funkcie, že na endpointe typu bulk v smere out môže prijať dáta, ktoré má uložiť do premennej appDataBulkOut a po dokončení prenosu má zavolať funkciu app_callback_bulk_out. To, či nejaký prenos prebehol a či prebehol úspešne sa dozvieme až v tejto callback funkcii.

Po dokončení prenosu musíme spustiť ďalší prenos prostredníctvom tej istej funkcie, pretože po zavolaní callback UDD vynuluje stav endpointu. Pokiaľ by sme tak neurobili, stratíme všetky ďalšie dáta, ktoré by na tento endpoint smerovali.

// Bulk OUT prijatý
void app_callback_bulk_out(udd_ep_status_t status,
  iram_size_t nb_transfered, udd_ep_id_t ep)
{
  // Hlavička dát (payload)
  static usbapp_bulk_out_t * bulkHeader = NULL;
  // Veľkosť dát (payload)
  static iram_size_t bulkPayloadSize = 0;
  // Zásobník pre vypísanie textu
  static char * bulkTextBuffer = NULL;
  // Zásobník pre bitmapu
  static uint16_t * bulkBitmapBuffer = NULL;


  // Pokiaľ prebehol prenos úspešne a zároveň sme prijali dáta
  // minimálne o veľkosti hlavičky
  if (status == UDD_EP_TRANSFER_OK &&
    nb_transfered >= sizeof(usbapp_bulk_out_t))
  {
    // Extrahovať hlavičku
    bulkHeader = (usbapp_bulk_out_t *)appDataBulkOut;
    // Vypočítať počet prenesených dát (payload) bez hlavičky
    bulkPayloadSize = nb_transfered - sizeof(usbapp_bulk_out_t);

    // Skontrolovať, či sa koordináty nachádzajú v priestore LCD
    if (usbapp_lcd_check_coords(bulkHeader->posX, bulkHeader->posY, bulkHeader->width,
      bulkHeader->height))
    {
      // Vymazať obrazovku, pokiaľ sa v hlavičke nachádza takéto nastavenie
      if (bulkHeader->options & DrawOption_Clear)
        usbapp_lcd_clear(bulkHeader->options & DrawOption_ColorMask);

      // Vykresliť text, pokiaľ sa v hlavičke nachádza takéto nastavenie
      if (bulkHeader->options & DrawOption_Text)
      {
        // Skontrolovať, či host odoslal aj text
        if (bulkPayloadSize > 0)
        {
          // Extrahovať text
          bulkTextBuffer = (char *)(appDataBulkOut + sizeof(usbapp_bulk_out_t));

          // Ukončiť text znakom \0
          if (nb_transfered >= UDI_VENDOR_EPS_SIZE_BULK_HS)
            appDataBulkOut[UDI_VENDOR_EPS_SIZE_BULK_HS - 1] = 0;
          else
            appDataBulkOut[nb_transfered] = 0;

          // Vykresliť text ako riadok
          if (bulkHeader->options & DrawOption_TextLine)
            usbapp_lcd_write_line_col(bulkTextBuffer,
              bulkHeader->options & DrawOption_ColorMask);
          else // Alebo vykresliť text na presnú pozíciu
            usbapp_lcd_draw_text(bulkTextBuffer,
              bulkHeader->posX, bulkHeader->posY,
              bulkHeader->options & DrawOption_ColorMask);
        }
      }
      // Vykresliť bitmapu
      else
      {
        // Skontrolovať, či je bitmapa menšia než stanovená hranica
        // a zároveň, či počet prenesených dát zodpovedá rozmerom bitmapy
        if ((bulkHeader->width * bulkHeader->height) <= USBAPP_MAX_BITMAP_SIZE &&
          ((bulkHeader->width * bulkHeader->height) * 2u) == bulkPayloadSize)
        {
          // Extrahovať bitmapu
          bulkBitmapBuffer = (uint16_t *)(appDataBulkOut + sizeof(usbapp_bulk_out_t));

          // Vykresliť bitmapu
          usbapp_lcd_draw_bitmap(bulkHeader->posX, bulkHeader->posY,
            bulkHeader->width, bulkHeader->height, bulkBitmapBuffer);
        }
      }
    }
  }

  // Spustiť ďalší prenos na BULK OUT
  udi_vendor_bulk_out_run((uint8_t *)appDataBulkOut, sizeof(appDataBulkOut), app_callback_bulk_out);
}

Naše zariadenie musí z bulk out endpointu prijať minimálne hlavičku usbapp_bulk_out_t, za ktorou môžu ale nemusia nasledovať ďalšie dáta a to buď text alebo obrázok vo formáte RGB16. To, či sa bude vykresľovať obrázok alebo text sa nachádza v hlavičke v políčku options.

V prípade vykresľovania textu, bude hlavička obsahovať X a Y pozíciu (len v prípade, že budeme text vykresľovať na presne stanovenú pozíciu, zariadenie bude vedieť písať text aj do riadkov automaticky, pokiaľ sa v políčku options použije príznak DrawOption_TextLine) a ďalšie nastavenia v options, vrátane farby, ktorá sa z options získa použitím masky DrawOption_ColorMask (0x00FFFFFF).

Vykresľovanie obrázka je jednoduchšie. Jediné, čo musí obrázok spĺňať je, aby jeho celková veľkosť v px2 neprevyšovala hodnotu 8100 a aby pozícia X/Y spolu s výškou a šírkou nachádzali v priestore displeja, v opačnom prípade obrázok vykreslený nebude. Ovládač bude môcť napr. odoslať obrázok o veľkosti 90x90, obrázok 91x90 už nemôže byť odoslaný, pretože náš buffer v zariadení nie je dostatočne veľký.

Posledný riadok, ako už bolo spomenuté, znovu spustí prenos na endpointe bulk out, resp. oznámi UDI, že naša aplikácia je pripravená prijať ďalšie dáta.

Na záver

Celý zdrojový kód zariadenia si môžete stiahnuť tu. Súbory, ktoré sú pre zariadenie a pre tento článok podstatné, sú umiestnené v adresároch USBZariadenie\src a USBZariadenie\src\app.

V ďalšej časti tohto seriálu sa dostaneme k programovaniu ovládača, najskôr pomocou libusb, potom UMDF. Našim cieľom bude urobiť tieto dva ovládače kompatibilné, keďže nakoniec vytvoríme aplikáciu v jazyku C#, ktorá bude vedieť pracovať s obidvoma ovládačmi. Jednoduchým riešením je vytvoriť knižnicu, tzv. wrapper, ktorý bude mať presne stanovené funkcie. Libusb spojíme s týmto wrapperom a pre UMDF vytvoríme jednoduchú knižnicu, samozrejme všetko cez C++.

Klientska aplikácia v C# potom bude môcť volať funkcie nášho wrapperu pomocou PInvoke (Platform Invoke)., pokiaľ by sme písali klientsku aplikácia napr. v jazyku C++, použijeme priamo wrapper (prilinkovaním).