USB pada register: interupsi titik akhir menggunakan HID sebagai contoh





Tingkat yang lebih rendah (avr-vusb)

USB pada register: STM32L1 / STM32F1

USB pada register: titik akhir massal pada contoh



USB Penyimpanan Massal pada register: titik akhir isochronous pada contoh perangkat Audio Kami



terus menangani USB pada pengontrol STM32L151. Seperti di bagian sebelumnya, tidak akan ada yang bergantung pada platform di sini, tetapi bergantung pada USB. Lebih tepatnya, kami akan mempertimbangkan jenis titik akhir ketiga - interupsi. Dan kami akan melakukan ini menggunakan contoh perangkat komposit "keyboard + tablet" ( tautan ke sumber ).

Untuk berjaga-jaga, saya peringatkan Anda: artikel ini (seperti orang lain) lebih merupakan sinopsis dari apa yang saya pahami saat memahami topik ini. Banyak hal yang tetap "ajaib" dan saya akan berterima kasih jika ada spesialis yang bisa menjelaskannya.



Pertama-tama, izinkan saya mengingatkan Anda bahwa protokol HID (Perangkat Antarmuka Manusia) tidak dimaksudkan untuk bertukar data dalam jumlah besar. Seluruh pertukaran didasarkan pada dua konsep: peristiwa dan negara bagian . Peristiwa adalah pesan satu kali yang terjadi sebagai tanggapan terhadap dampak eksternal atau internal. Misalnya, pengguna menekan tombol atau menggerakkan mouse. Atau pada satu keyboard saya menonaktifkan NumLock, setelah itu host dipaksa dan yang kedua mengirim perintah yang sesuai untuk memperbaikinya, juga mengirim sinyal penekanan tombol NumLock dan mengaktifkannya kembaliditampilkan di indikator. Titik interupsi digunakan untuk menandai peristiwa. Suatu negara adalah sejenis karakteristik yang tidak berubah begitu saja. Nah, katakanlah suhunya. Atau sesuaikan level volume. Artinya, sesuatu di mana host mengontrol perilaku perangkat. Kebutuhan untuk ini jarang muncul, oleh karena itu interaksinya adalah yang paling primitif - melalui ep0.



Dengan demikian, tujuan titik interupsi sama dengan interupsi dalam pengontrol - untuk melaporkan kejadian langka dengan cepat. Tetapi USB adalah hal yang berpusat pada host, jadi perangkat tidak memiliki hak untuk memulai transfer sendiri. Untuk menyiasati ini, pengembang USB datang dengan penopang: host secara berkala mengirim permintaan untuk membaca semua titik interupsi. Frekuensi permintaan dikonfigurasi oleh parameter terakhir di EndpointDescriptor (ini adalah bagian dari ConfigurationDescriptor). Kita telah melihat field bInterval di chapter sebelumnya, tapi nilainya diabaikan. Sekarang dia akhirnya menemukan kegunaannya. Nilainya memiliki ukuran 1 byte dan disetel dalam milidetik, jadi kami akan disurvei dengan interval dari 1 ms hingga 2,55 detik. Untuk perangkat kecepatan rendah, interval minimum adalah 10ms. Kehadiran kruk dengan titik interupsi berarti polling untuk kamibahwa bahkan tanpa pertukaran, mereka akan membuang-buang bandwidth bus.



Kesimpulan logisnya: titik interupsi hanya untuk transaksi IN. Secara khusus, mereka digunakan untuk mengirimkan peristiwa dari keyboard atau mouse, untuk memberi tahu tentang perubahan dalam jalur layanan port COM, untuk menyinkronkan aliran audio dan sejenisnya. Tetapi untuk semua ini, Anda harus menambahkan jenis poin lainnya. Oleh karena itu, agar tidak mempersulit contoh, kami akan membatasi diri pada penerapan perangkat HID. Sebenarnya, kami sudah membuat perangkat seperti itu di bagian pertama, tetapi ada poin tambahan yang tidak digunakan sama sekali, dan struktur protokol HID tidak dipertimbangkan.



ConfigurationDescriptor



static const uint8_t USB_ConfigDescriptor[] = {
  ARRLEN34(
  ARRLEN1(
    bLENGTH, // bLength: Configuration Descriptor size
    USB_DESCR_CONFIG,    //bDescriptorType: Configuration
    wTOTALLENGTH, //wTotalLength
    1, // bNumInterfaces
    1, // bConfigurationValue: Configuration value
    0, // iConfiguration: Index of string descriptor describing the configuration
    0x80, // bmAttributes: bus powered
    0x32, // MaxPower 100 mA
  )
  ARRLEN1(
    bLENGTH, //bLength
    USB_DESCR_INTERFACE, //bDescriptorType
    0, //bInterfaceNumber
    0, // bAlternateSetting
    2, // bNumEndpoints
    HIDCLASS_HID, // bInterfaceClass: 
    HIDSUBCLASS_BOOT, // bInterfaceSubClass: 
    HIDPROTOCOL_KEYBOARD, // bInterfaceProtocol: 
    0x00, // iInterface
  )
  ARRLEN1(
    bLENGTH, //bLength
    USB_DESCR_HID, //bDescriptorType
    USB_U16(0x0110), //bcdHID
    0, //bCountryCode
    1, //bNumDescriptors
    USB_DESCR_HID_REPORT, //bDescriptorType
    USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength
  )
  ARRLEN1(
    bLENGTH, //bLength
    USB_DESCR_ENDPOINT, //bDescriptorType
    INTR_NUM, //bEdnpointAddress
    USB_ENDP_INTR, //bmAttributes
    USB_U16( INTR_SIZE ), //MaxPacketSize
    10, //bInterval
  )
  ARRLEN1(
    bLENGTH, //bLength
    USB_DESCR_ENDPOINT, //bDescriptorType
    INTR_NUM | 0x80, //bEdnpointAddress
    USB_ENDP_INTR, //bmAttributes
    USB_U16( INTR_SIZE ), //MaxPacketSize
    10, //bInterval
  )
  )
};
      
      





Pembaca yang penuh perhatian mungkin segera memperhatikan deskripsi dari titik akhir. Dengan yang kedua semuanya beres - IN point (karena penambahan dengan 0x80) adalah tipe interupsi, ukuran dan interval ditentukan. Tetapi yang pertama tampaknya dinyatakan sebagai KELUAR, tetapi pada saat yang sama menyela, yang bertentangan dengan apa yang dikatakan sebelumnya. Dan akal sehat juga: tuan rumah tidak memerlukan kruk untuk mentransfer apa pun ke perangkat kapan saja. Tetapi dengan cara ini, penggaruk lain dilewati: tipe titik akhir di STM32 diatur bukan untuk satu titik, tetapi hanya untuk pasangan IN / OUT, jadi tidak akan berfungsi untuk mengatur titik 0x81 ke jenis interupsi, tetapi ke 0x01st kontrol. Namun, ini bukan masalah bagi tuan rumah, itu mungkin akan mengirim data yang sama dalam titik massal juga ... yang, bagaimanapun, saya tidak akan memeriksanya.



Deskriptor HID



Struktur deskriptor HID paling mirip dengan file konfigurasi "name = value", tetapi tidak seperti itu, "name" adalah konstanta numerik dari daftar khusus USB, dan "value" juga merupakan konstanta atau variabel dari ukuran 0 sampai dengan 3 byte.



Penting:untuk beberapa "nama", panjang "nilai" ditentukan dalam 2 bit paling signifikan dari bidang "nama". Misalnya, ambil LOGICAL_MINIMUM (nilai minimum yang dapat diambil variabel ini dalam mode normal). Kode untuk konstanta ini adalah 0x14. Karenanya, jika tidak ada "nilai" (tampaknya ini tidak terjadi, tetapi saya tidak akan membantah - untuk beberapa alasan kasus ini dimasukkan), maka deskriptor akan berisi satu nomor 0x14. Jika "nilai" adalah 1 (satu byte), maka 0x15, 0x01 akan ditulis. Untuk nilai dua byte 0x1234, 0x16, 0x34, 0x12 akan ditulis - nilai ditulis dari rendah ke tinggi. Nah, sebelum heap, angka 0x123456 akan menjadi 0x17, 0x56, 0x34, 0x12.



Secara alami, saya terlalu malas untuk menghafal semua konstanta numerik ini, jadi kami akan menggunakan makro. Sayangnya, saya tidak pernah menemukan cara untuk membuat mereka mengetahui ukuran nilai yang diteruskan itu sendiri dan mengembangkannya menjadi 1, 2, 3, atau 4 byte. Oleh karena itu, saya harus membuat kruk: makro tanpa sufiks bertanggung jawab atas nilai 8-bit yang paling umum, dengan sufiks 16 untuk nilai 16-bit, dan dengan 24 untuk nilai 24-bit. Makro juga telah ditulis untuk nilai "komposit" seperti rentang LOGICAL_MINMAX24 (min, maks), yang diperluas menjadi 4, 6, atau 8 byte.



Seperti file konfigurasi, ada "bagian" yang disebut halaman (use_page) yang mengelompokkan perangkat dengan tujuan. Misalnya, ada halaman dengan periferal dasar seperti keyboard, mouse, dan hanya tombol, ada joystick dan gamepad (saya dengan tulus merekomendasikan untuk melihat yang mana! Ada juga untuk tank, dan untuk pesawat luar angkasa, dan untuk kapal selam dan untuk hal lain ), bahkan ada pajangan ... Benar, di mana mencari perangkat lunak yang dapat bekerja dengan semua ini, saya tidak tahu.



Di dalam setiap halaman, perangkat tertentu dipilih. Misalnya, untuk mouse, ini adalah penunjuk dan tombol, dan untuk tablet - stylus atau jari pengguna (apa?!). Mereka juga menentukan bagian komponen perangkat. Jadi, bagian dari pointer adalah koordinat X dan Y. Beberapa karakteristik dapat dikelompokkan menjadi "kumpulan", tetapi mengapa saya tidak begitu mengerti mengapa hal ini dilakukan. Dalam dokumentasi, bidang terkadang ditandai dengan beberapa huruf tentang tujuan bidang tersebut dan bagaimana cara menggunakannya:



CA Koleksi (aplikasi) Informasi layanan, tidak terkait dengan variabel apa pun
CL Koleksi (logis) - / -
CP Koleksi (fisik) - / -
Dv Nilai Dinamis nilai masukan atau keluaran (variabel)
MC Kontrol sesaat bendera status (1-bendera dikokang, 0-dihapus)
OSC Kontrol satu tembakan acara satu kali. Hanya transisi 0-> 1 yang diproses




Ada yang lain, tentu saja, tetapi tidak digunakan dalam contoh saya. Jika, misalnya, bidang X ditandai sebagai DV, maka itu dianggap sebagai variabel dengan panjang bukan nol dan akan disertakan dalam struktur laporan. Kolom MC atau OSC juga disertakan dalam laporan, tetapi ukurannya 1 bit.



Satu laporan (paket data yang dikirim atau diterima oleh perangkat) berisi nilai dari semua variabel yang dijelaskan di dalamnya. Deskripsi tombol hanya berbicara tentang satu bit yang ditempati, tetapi untuk koordinat relatif (seberapa banyak mouse bergerak, misalnya), setidaknya diperlukan satu byte, dan untuk koordinat absolut (seperti untuk layar sentuh), setidaknya 2 byte dibutuhkan. Ditambah lagi, banyak kontrol memiliki keterbatasan fisiknya sendiri. Misalnya, ADC dari layar sentuh yang sama dapat memiliki resolusi hanya 10 bit, yaitu, memberikan nilai dari 0 hingga 1023, yang harus diskalakan oleh host ke resolusi layar penuh. Oleh karena itu, selain tujuan setiap bidang, deskriptor juga menentukan rentang nilai yang diizinkan (LOGICAL_MINMAX), ditambah terkadang rentang nilai fisik (dalam milimeter di sana, atau dalam derajat) dan presentasi dalam laporan wajib.Representasi diatur oleh dua angka: ukuran satu variabel (dalam bit) dan jumlahnya. Misalnya, koordinat sentuhan layar sentuh di perangkat yang kami buat diatur sebagai berikut:



USAGE( USAGE_X ), // 0x09, 0x30,
USAGE( USAGE_Y ), // 0x09, 0x31,
LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00,   0x26, 0x10, 0x27,
REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,
INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,
      
      





Di sini Anda dapat melihat bahwa dua variabel dideklarasikan, bervariasi dalam rentang 0 hingga 10000 dan menempati dua bagian 16 bit dalam laporan.



Kolom terakhir mengatakan bahwa variabel di atas akan dibaca oleh host (IN) dan menjelaskan caranya. Saya tidak akan menjelaskan benderanya secara detail, saya hanya akan membahas beberapa. Bendera HID_ABS menunjukkan bahwa nilainya mutlak, yaitu, tidak ada riwayat yang mempengaruhinya. Nilai alternatif HID_REL menunjukkan bahwa nilai tersebut merupakan offset dari yang sebelumnya. Bendera HID_VAR mengatakan bahwa setiap bidang bertanggung jawab atas variabelnya sendiri. Nilai alternatif HID_ARR mengatakan bahwa bukan status semua tombol dari daftar yang akan dikirim, tetapi hanya nomor yang aktif. Bendera ini hanya berlaku untuk bidang bit tunggal. Alih-alih mengirimkan 101/102 status semua tombol keyboard, Anda dapat membatasi diri Anda sendiri hingga beberapa byte dengan daftar tombol yang ditekan. Kemudian parameter pertama REPORT_FMT akan bertanggung jawab atas ukuran nomor, dan yang kedua untuk nomor tersebut.



Karena ukuran semua variabel diatur dalam bit, logis untuk bertanya: bagaimana dengan tombol, karena jumlahnya mungkin bukan kelipatan 8, dan ini akan menyebabkan kesulitan penyelarasan saat membaca dan menulis. Dimungkinkan untuk mengalokasikan satu byte ke setiap tombol, tetapi kemudian volume laporan akan meningkat pesat, yang tidak menyenangkan untuk program berkecepatan tinggi seperti interupsi. Sebaliknya, tombol mencoba ditempatkan lebih dekat satu sama lain, dan ruang yang tersisa diisi dengan bit dengan bendera HID_CONST.



Sekarang kita dapat, jika tidak menulis deskriptor dari awal, maka setidaknya mencoba membacanya, yaitu menentukan bit mana yang sesuai dengan bidang ini atau itu. Cukup menghitung INPUT_HID dan REPORT_FMT yang sesuai. Ingatlah bahwa saya datang dengan makro semacam itu, tidak ada orang lain yang menggunakannya. Di deskriptor orang lain, Anda harus mencari input, report_size, report_count, atau bahkan konstanta numerik.



Sekarang Anda dapat membawa seluruh deskriptor:



static const uint8_t USB_HIDDescriptor[] = {
  //keyboard
  USAGE_PAGE( USAGEPAGE_GENERIC ),//0x05, 0x01,
  USAGE( USAGE_KEYBOARD ), // 0x09, 0x06,
  COLLECTION( COLL_APPLICATION, // 0xA1, 0x01,
    REPORT_ID( 1 ), // 0x85, 0x01,
    USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,
    USAGE_MINMAX(224, 231), //0x19, 0xE0, 0x29, 0xE7,    
    LOGICAL_MINMAX(0, 1), //0x15, 0x00, 0x25, 0x01,
    REPORT_FMT(1, 8), //0x75, 0x01, 0x95, 0x08     
    INPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x81, 0x02,
     //reserved
    REPORT_FMT(8, 1), // 0x75, 0x08, 0x95, 0x01,
    INPUT_HID(HID_CONST), // 0x81, 0x01,
              
    REPORT_FMT(1, 5),  // 0x75, 0x01, 0x95, 0x05,
    USAGE_PAGE( USAGEPAGE_LEDS ), // 0x05, 0x08,
    USAGE_MINMAX(1, 5), //0x19, 0x01, 0x29, 0x05,  
    OUTPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x91, 0x02,
    //  1 
    REPORT_FMT(3, 1), // 0x75, 0x03, 0x95, 0x01,
    OUTPUT_HID( HID_CONST ), // 0x91, 0x01,
    REPORT_FMT(8, 6),  // 0x75, 0x08, 0x95, 0x06,
    LOGICAL_MINMAX(0, 101), // 0x15, 0x00, 0x25, 0x65,         
    USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,
    USAGE_MINMAX(0, 101), // 0x19, 0x00, 0x29, 0x65,
    INPUT_HID( HID_DATA | HID_ARR ), // 0x81, 0x00,           
  )
  //touchscreen
  USAGE_PAGE( USAGEPAGE_DIGITIZER ), // 0x05, 0x0D,
  USAGE( USAGE_PEN ), // 0x09, 0x02,
  COLLECTION( COLL_APPLICATION, // 0xA1, 0x0x01,
    REPORT_ID( 2 ), //0x85, 0x02,
    USAGE( USAGE_FINGER ), // 0x09, 0x22,
    COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,
      USAGE( USAGE_TOUCH ), // 0x09, 0x42,
      USAGE( USAGE_IN_RANGE ), // 0x09, 0x32,
      LOGICAL_MINMAX( 0, 1), // 0x15, 0x00, 0x25, 0x01,
      REPORT_FMT( 1, 2 ), // 0x75, 0x01, 0x95, 0x02,
      INPUT_HID( HID_VAR | HID_DATA | HID_ABS ), // 0x91, 0x02,
      REPORT_FMT( 1, 6 ), // 0x75, 0x01, 0x95, 0x06,
      INPUT_HID( HID_CONST ), // 0x81, 0x01,
                
      USAGE_PAGE( USAGEPAGE_GENERIC ), //0x05, 0x01,
      USAGE( USAGE_POINTER ), // 0x09, 0x01,
      COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,         
        USAGE( USAGE_X ), // 0x09, 0x30,
        USAGE( USAGE_Y ), // 0x09, 0x31,
        LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00, 0x26, 0x10, 0x27,
        REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,
        INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,
      )
    )
  )
};
      
      





Selain bidang yang telah dibahas sebelumnya, ada juga bidang yang menarik seperti REPORT_ID. Karena, seperti yang jelas dari komentar, perangkat kami adalah komposit, host perlu menentukan data siapa yang diterimanya. Untuk ini, bidang ini diperlukan.



Dan satu bidang lagi yang ingin saya perhatikan adalah OUTPUT_HID. Sesuai dengan namanya, tidak bertanggung jawab untuk menerima laporan (IN), tetapi untuk mengirimkan (OUT). Itu terletak di bagian keyboard dan menjelaskan indikator CapsLock, NumLock, ScrollLock serta dua yang eksotis - Tulis (tanda untuk memasukkan beberapa karakter yang tidak memiliki tombol sendiri seperti Γ‘, Β΅ atau) dan Kana (memasukkan hieroglif) . Sebenarnya, demi bidang ini, kami memulai titik KELUAR. Dalam penangannya, kami akan memeriksa apakah indikator CapsLock dan NumLock perlu dinyalakan: hanya ada dua dioda di papan dan disambungkan.



Ada bidang ketiga yang terkait dengan pertukaran data - FEATURE_HID, kami menggunakannya di contoh pertama. Jika INPUT dan OUTPUT dimaksudkan untuk mengirimkan peristiwa, maka FITUR adalah status yang dapat dibaca atau ditulis. Benar, ini dilakukan tidak melalui titik akhir khusus, tetapi melalui ep0 biasa melalui permintaan yang sesuai.



Jika Anda melihat lebih dekat deskriptor, Anda dapat memulihkan struktur laporan. Lebih tepatnya, dua laporan:



struct{
  uint8_t report_id; //1
  union{
    uint8_t modifiers;
    struct{
      uint8_t lctrl:1; //left control
      uint8_t lshift:1;//left shift
      uint8_t lalt:1;  //left alt
      uint8_t lgui:1;  //left gui.   hyper,   winkey
      uint8_t rctrl:1; //right control
      uint8_t rshift:1;//right shift
      uint8_t ralt:1;  //right alt
      uint8_t rgui:1;  //right gui
    };
  };
  uint8_t reserved; //        
  uint8_t keys[6]; //   
}__attribute__((packed)) report_kbd;

struct{
  uint8_t report_id; //2
  union{
    uint8_t buttons;
    struct{
      uint8_t touch:1;   //  
      uint8_t inrange:1; //   
      uint8_t reserved:6;//  1 
    };
  };
  uint16_t x;
  uint16_t y;
}__attribute__((packed)) report_tablet;
      
      





Selain itu, kami akan mengirimkannya dengan menekan tombol di papan. karena kami menulis hanya contoh implementasi, dan bukan perangkat lengkap, kami akan melakukannya dengan cara yang biadab - dengan mengirimkan dua laporan, yang pertama "menekan" tombol, dan yang kedua - "melepaskan". Selain itu, dengan penundaan "bodoh" yang sangat besar di antara pesan-pesan tersebut. Jika Anda tidak mengirim laporan dengan tombol "dilepas", sistem akan menganggap bahwa tombol tersebut masih ditekan dan akan mengulanginya. Secara alami, tidak ada pertanyaan tentang efisiensi, keamanan juga, tetapi itu akan dilakukan untuk pengujian. Oh ya, di mana tanpa penggaruk lain ! Ukuran struktur harus sesuai dengan apa yang dijelaskan dalam deskriptor, jika tidak, Windows akan berpura-pura tidak mengerti apa yang mereka inginkan darinya. Seperti biasa, Linux mengabaikan kesalahan tersebut dan bekerja seolah-olah tidak terjadi apa-apa.



Selama pengujian, saya menemukan efek samping yang lucu: di Windows7, saat Anda mengklik "layar sentuh", jendela tulisan tangan akan muncul. Saya tidak tahu tentang fitur ini.



Jika Anda memiliki perangkat yang sudah jadi



... dan saya ingin melihatnya dari dalam. Pertama-tama, tentu saja, kami melihat, Anda bahkan dapat dari pengguna biasa, ConfigurationDescriptor:



lsusb -v -d <VID:PID>
      
      





Untuk deskriptor HID, saya tidak menemukan (dan tidak mencari) cara yang lebih baik daripada dari root:



cat /sys/kernel/debug/hid/<address>/rdes
      
      





Demi kelengkapan, ada baiknya menambahkan di sini cara melihat hal-hal serupa di sistem operasi lain. Tapi saya tidak memiliki pengetahuan yang relevan, mungkin mereka akan memberi tahu Anda di komentar. Diinginkan, tentu saja, tanpa menginstal perangkat lunak pihak ketiga.



Kesimpulan



Faktanya, ini semua yang saya gali di HID. Rencana minimum - untuk mempelajari cara membaca deskriptor yang sudah jadi, mengemulasi beberapa perangkat secara bersamaan dan mengimplementasikan input tablet - selesai. Nah, filosofi titik interupsi dipertimbangkan pada saat yang sama.



Seperti waktu yang buruk, saya meninggalkan sedikit dokumentasi di repositori jika desainer USB-IF memutuskan untuk merusak situs lagi.



All Articles