Bahkan tingkat yang lebih rendah (avr-vusb)
USB pada register: titik akhir massal menggunakan contoh
USB Penyimpanan Massal pada register: titik akhir interupsi menggunakan contoh
USB HID pada register: titik akhir isochronous menggunakan contoh perangkat Audio
Kami telah menemukan perangkat lunak USB menggunakan contoh AVR, saatnya mengambil batu yang lebih berat - stm32. Subjek eksperimental kami adalah STM32F103C8T6 klasik serta perwakilan dari seri STM32L151RCT6 berdaya rendah. Seperti sebelumnya, kami tidak akan menggunakan papan debug yang dibeli dan HAL, lebih memilih sepeda.
Karena ada dua pengontrol dalam judul, ada baiknya membicarakan perbedaan utama. Pertama-tama, ini adalah resistor pull-up yang memberi tahu host usb bahwa ada sesuatu yang terjebak di dalamnya. Di L151 itu built-in dan dikendalikan oleh bit SYSCFG_PMC_USB_PU, tetapi di F103 tidak; Anda harus menyoldernya ke papan dari luar dan menghubungkannya ke VCC atau ke kaki pengontrol. Dalam kasus saya, kaki PA10 berada di bawah ketiak saya. Di mana UART1 hang ... Dan pin lain dari UART1 bentrok dengan tombol ... Aku melempar papan yang bagus, bukan begitu? Perbedaan kedua adalah jumlah memori flash: di F103 64 kB, dan di L151 sebanyak 256 kB, yang akan kita gunakan suatu saat nanti saat mempelajari titik akhir Massal. Mereka juga memiliki pengaturan pencatatan jam kerja yang sedikit berbeda, dan mereka dapat digantung pada kaki yang berbeda dengan bola lampu dengan kancing, tetapi ini sudah cukup sepele. Contoh untuk F103tersedia di repositori, jadi tidak akan sulit untuk menyesuaikan sisa eksperimen dengan L151 untuknya. Kode sumber tersedia di sini: github.com/COKPOWEHEU/usb
Prinsip umum bekerja dengan USB
Pengoperasian dengan USB pada pengontrol ini diasumsikan menggunakan modul perangkat keras. Artinya, kami memberi tahu dia apa yang harus dilakukan, dia melakukannya, dan pada akhirnya menarik pesan "Saya siap!" Karenanya, kita tidak perlu memanggil hampir semua hal dari main main (walaupun saya telah menyediakan fungsi usb_class_poll untuk berjaga-jaga). Siklus kerja normal terbatas pada satu peristiwa - pertukaran data. Sisanya - setel ulang, tidur, dan lainnya - luar biasa, acara satu kali.
Kali ini saya tidak akan membahas detail tingkat rendah dari pertukaran tersebut. Siapapun yang tertarik dapat membaca tentang vusb. Tapi izinkan saya mengingatkan Anda bahwa pertukaran data biasa bukan dengan satu byte, tetapi dengan paket, dan arah transmisi diatur oleh host. Dan dia juga menentukan nama-nama arah ini: transmisi IN berarti host menerima data (dan perangkat mentransmisikan), dan OUT berarti host mengirimkan data (dan kami menerima). Selain itu, setiap paket memiliki alamatnya sendiri - jumlah titik akhir yang ingin dikomunikasikan oleh host. Untuk saat ini, kita akan memiliki satu titik akhir 0, yang bertanggung jawab atas perangkat secara keseluruhan (singkatnya, saya juga akan menyebutnya ep0). Untuk apa sisanya, saya akan memberi tahu Anda di artikel lain. Menurut standar, ukuran ep0 benar-benar 8 byte untuk perangkat berkecepatan rendah (yang memiliki vusb yang sama) dan pilihan 8, 16, 32,64 byte untuk kecepatan penuh seperti milik kami.
Bagaimana jika datanya terlalu kecil dan tidak memenuhi buffer sepenuhnya? Semuanya sederhana di sini: selain data dalam paket, ukurannya juga ditransmisikan (ini bisa berupa bidang panjang gelombang atau kombinasi tingkat rendah dari sinyal SE0, yang menunjukkan akhir transmisi), bahkan jika kita perlu mentransfer tiga byte melalui ep0 64 byte, maka tepat tiga byte akan ditransfer ... Akibatnya, kami tidak akan menyia-nyiakan bandwidth dengan menggerakkan angka nol yang tidak perlu. Jadi jangan terlalu kecil: jika kami mampu untuk menghabiskan 64 byte, kami menghabiskan tanpa ragu-ragu. Antara lain, ini akan mengurangi beban bus, karena lebih mudah untuk mentransfer 64 byte (ditambah semua header dan tails) pada satu waktu daripada masing-masing 8 kali 8 byte (yang masing-masing, sekali lagi, header dan ekor). ).
Dan jika ada terlalu banyak data sebaliknya? Di sini lebih rumit. Data harus dibagi berdasarkan ukuran titik akhir dan ditransfer dalam beberapa bagian. Misalkan ukuran ep0 adalah 8 byte, dan host mencoba mengirimkan 20 byte. Pada interupsi pertama, byte 0-7 akan datang kepada kita, di 8-15 kedua, di 16-20 ketiga. Artinya, untuk mengumpulkan seluruh paket, Anda perlu menerima sebanyak tiga interupsi. Untuk ini, di HAL yang sama, buffer yang rumit ditemukan, yang dengannya saya mencoba mengetahuinya, tetapi setelah tingkat keempat mentransfer hal yang sama antar fungsi, saya meludah. Akibatnya, dalam implementasi saya, buffering berada di pundak programmer.
Tetapi tuan rumah setidaknya selalu mengatakan berapa banyak data yang coba ditransfer. Saat kami mentransfer data, kami perlu mengelabui status kaki tingkat rendah untuk memperjelas bahwa data sudah berakhir. Lebih tepatnya, untuk memperjelas modul usb bahwa datanya sudah habis dan Anda perlu menarik kakinya. Ini dilakukan dengan cara yang jelas - dengan menulis hanya sebagian dari buffer. Misalnya, jika kita memiliki 8 byte dalam buffer, dan kita telah menulis 4, maka jelas kita hanya memiliki 4 byte data, setelah itu modul akan mengirimkan kombinasi ajaib SE0 dan semua orang akan senang. Dan jika kita menulis 8 byte, apakah itu berarti kita hanya memiliki 8 byte, atau ini hanya sebagian dari data yang sesuai dengan buffer? Modul usb berpikir bahwa file. Oleh karena itu, jika kita ingin menghentikan transfer, maka setelah menulis buffer 8-byte, kita harus menulis buffer 0-byte berikutnya. Ini disebut ZLP, Zero Length Packet. Bagaimana tampilannya dalam kode,Aku akan memberitahumu nanti.
Organisasi memori
Menurut standar, ukuran titik akhir 0 bisa sampai 64 byte. Ukuran lainnya - sebanyak 1024 byte. Jumlah poin juga dapat berbeda dari perangkat ke perangkat. STM32L1 yang sama mendukung hingga 7 titik pada input dan 7 pada output (tidak termasuk ep0), yaitu hingga 14 kB buffer saja. Yang dalam volume sebesar itu kemungkinan besar tidak akan pernah dibutuhkan oleh siapapun. Konsumsi memori yang tidak dapat diterima! Sebagai gantinya, modul usb mengunyah sebagian dari memori kernel bersama dan menggunakannya. Area ini disebut PMA (area memori paket) dan dimulai dengan USB_PMAADDR. Dan untuk menunjukkan di mana buffer dari setiap titik akhir berada di dalamnya, larik 8 elemen masing-masing dengan struktur berikut dialokasikan di awal, dan hanya kemudian area sebenarnya untuk data:
typedef struct{
volatile uint32_t usb_tx_addr;
volatile uint32_t usb_tx_count;
volatile uint32_t usb_rx_addr;
volatile union{
uint32_t usb_rx_count;
struct{
uint32_t rx_count:10;
uint32_t rx_num_blocks:5;
uint32_t rx_blocksize:1;
};
};
}usb_epdata_t;
Di sini Anda mengatur awal buffer pengiriman, ukurannya, lalu awal buffer penerima dan ukurannya. Perhatikan pertama-tama bahwa usb_tx_count tidak mengatur ukuran buffer sebenarnya, tetapi jumlah data yang akan ditransfer. Artinya, kode kita harus menulis data ke alamat usb_tx_addr, lalu tulis ukurannya ke usb_tx_count dan baru kemudian tarik register modul usb yang datanya tertulis, transfer. Lebih memperhatikan format aneh dari ukuran buffer penerima: ini adalah struktur di mana 10 bit rx_count bertanggung jawab atas jumlah sebenarnya dari data yang dibaca, sedangkan sisanya benar-benar untuk ukuran buffer. Penting untuk mengetahui sepotong besi di mana Anda dapat menulis, dan di mana data orang lain dimulai. Format pengaturan ini juga cukup menarik: bendera rx_block_size memberi tahu dalam satuan apa ukuran diset. Jika disetel ulang ke 0,kemudian dalam kata 2-byte, maka ukuran buffer adalah 2 * rx_num_blocks, yaitu, dari 0 hingga 62. Dan jika di-set ke 1, maka dalam blok 32-byte, masing-masing, ukuran buffer ternyata menjadi 32 * rx_num_blocks dan terletak pada kisaran 32 sampai 512 (ya, tidak sampai 1024, itulah batasan pengontrol).
Untuk menempatkan buffer di area ini, kita akan menggunakan pendekatan semi-dinamis. Artinya, alokasikan memori sesuai permintaan, tetapi tidak membebaskannya (malloc / free belum cukup untuk menemukan!). Awal dari ruang yang tidak terisi akan ditunjukkan oleh variabel lastaddr, yang awalnya menunjuk ke permulaan PMA dikurangi tabel struktur yang dibahas di atas. Nah, setiap kali fungsi untuk mengkonfigurasi titik akhir berikutnya usb_ep_init () dipanggil, itu akan digeser oleh ukuran buffer yang ditentukan di sana. Dan nilai yang diinginkan akan dimasukkan ke sel tabel yang sesuai, tentu saja. Nilai variabel ini disetel ulang setelah peristiwa penyetelan ulang, diikuti oleh panggilan ke usb_class_init (), di mana poin dikonfigurasi ulang sesuai dengan tugas pengguna.
Bekerja dengan register pengirim-terima
Seperti yang baru saja dikatakan, pada penerimaan kita membaca berapa banyak data yang benar-benar diterima (kolom usb_rx_count), kemudian kita membaca datanya sendiri, lalu kita menarik modul usb agar buffernya gratis, Anda dapat menerima paket berikutnya. Untuk transmisi, sebaliknya: kita menulis data ke buffer, lalu mengatur berapa yang telah ditulis ke usb_tx_count, dan terakhir menarik modul agar buffernya penuh, kita bisa mentransfernya.
Penggaruk pertamamulai saat bekerja dengan buffer itu sendiri: buffer tidak diatur dalam 32 bit, seperti pengontrol lainnya, dan bukan dalam 8 bit, seperti yang Anda duga. Dan masing-masing 16 bit! Hasilnya, ini ditulis dan dibaca dalam 2 byte, disejajarkan dengan 4 byte. Terima kasih ST untuk melakukan penyimpangan seperti itu! Betapa membosankan hidup tanpa itu! Sekarang memcpy biasa sangat diperlukan, Anda harus memagari fungsi-fungsi khusus. Ngomong-ngomong, jika ada yang menyukai DMA, maka tampaknya dapat melakukan transformasi seperti itu sendiri, meskipun saya belum mengujinya.
Dan lalu penggaruk keduadengan menulis ke register modul. Faktanya adalah bahwa untuk konfigurasi setiap titik akhir - untuk jenisnya (kontrol, massal, dll.) Dan status - satu register USB_EPnR bertanggung jawab, yaitu, Anda tidak dapat mengubah sedikit pun di dalamnya, Anda perlu berhati-hati agar tidak merusak sisanya. Dan kedua, sudah ada empat jenis bit di register ini! Beberapa tersedia hanya untuk membaca (ini bagus), yang lain untuk membaca dan menulis (juga normal), yang lain mengabaikan catatan 0, tetapi ketika menulis 1, mereka mengubah keadaan ke kebalikan (kesenangan dimulai), dan yang keempat, di sebaliknya, abaikan record 1, tapi record 0 me-reset mereka ke 0. Katakan padaku, apa yang dipikirkan pecandu untuk membuat bit dalam satu register yang mengabaikan 0 dan mengabaikan 1 ?! Tidak, saya siap berasumsi bahwa ini dilakukan demi menjaga integritas register, ketika diakses dari kode dan perangkat keras. Tapi apa yang kamu inginkan,Apakah terlalu malas memasang inverter sehingga bit direset dengan menulis 1? Atau inverter sehingga bit lain dibalik dengan menulis 0? Akibatnya, pengaturan dua bit register terlihat seperti ini (sekali lagi terima kasih kepada ST untuk penyimpangan seperti itu):
#define ENDP_STAT_RX(num, stat) do{USB_EPx(num) = ((USB_EPx(num) & ~(USB_EP_DTOG_RX | USB_EP_DTOG_TX | USB_EPTX_STAT)) | USB_EP_CTR_RX | USB_EP_CTR_TX) ^ stat; }while(0)
Oh ya, saya hampir lupa: mereka juga tidak memiliki akses ke register dengan nomor. Artinya, makro USB_EP0R, USB_EP1R, dll. mereka punya, tetapi jika nomor itu datang dalam variabel, maka sayangnya. Saya harus menciptakan USB_EPx () saya sendiri - dan apa yang harus dilakukan.
Nah, untuk memenuhi formalitas, saya akan menunjukkan bahwa bendera kesiapan (yaitu, bahwa kita telah membaca data sebelumnya) diatur oleh topeng bit USB_EP_RX_VALID, dan untuk penulisan (yaitu, kami telah menulis datanya di penuh dan dapat ditransfer) - dengan topeng USB_EP_TX_VALID.
Memproses permintaan MASUK dan KELUAR
Terjadinya interupsi USB dapat menandakan hal-hal yang berbeda, tetapi untuk saat ini kami akan fokus pada permintaan komunikasi. Bendera untuk acara semacam itu adalah bit USB_ISTR_CTR. Jika kami melihatnya, kami dapat mengetahui titik mana yang ingin diajak berkomunikasi oleh tuan rumah. Nomor poin disembunyikan di bawah bit mask USB_ISTR_EP_ID, dan arah IN atau OUT disembunyikan di bawah bit USB_EP_CTR_TX dan USB_EP_CTR_RX.
Karena kita dapat memiliki banyak titik, dan masing-masing dengan algoritme pemrosesannya sendiri, kita akan membuat fungsi panggilan balik untuk semuanya, yang akan dipanggil pada peristiwa yang sesuai. Misalnya, host mengirim data ke endpoint3, kita membaca USB-> ISTR, ditarik keluar dari sana bahwa permintaannya adalah OUT dan nomor poinnya adalah 3. Jadi kita memanggil epfunc_out [3] (3). Nomor poin dalam tanda kurung ditransmisikan jika tiba-tiba kode pengguna ingin menggantung satu penangan di beberapa titik. Oh ya, bahkan dalam standar USB, biasanya menandai titik input IN dengan bit ke-7 yang miring. Artinya, titik akhir3 pada keluaran akan memiliki angka 0x03, dan pada masukan - 0x83. Selain itu, ini adalah poin yang berbeda, dapat digunakan secara bersamaan, tidak saling mengganggu. Nah, hampir: di stm32 mereka memiliki pengaturan tipe (massal, interupsi, ...) untuk penerimaan dan transmisi. Jadi poin IN 0x83 yang sama akan cocok dengan panggilan balik 'di epfunc_in [3] (3 | 0x80).
Prinsip yang sama berlaku untuk ep0. Satu-satunya perbedaan adalah pemrosesannya dilakukan di dalam perpustakaan, dan bukan di dalam kode pengguna. Tetapi bagaimana jika Anda perlu memproses permintaan khusus seperti beberapa HID - tidak repot-repot memilih kode perpustakaan? Untuk ini, ada panggilan balik khusus usb_class_ep0_out dan usb_class_ep0_in, yang dipanggil di tempat-tempat khusus dan memiliki format khusus, yang akan saya bicarakan lebih dekat ke bagian akhir.
Perlu disebutkan hal lain yang tidak terlalu jelas terkait dengan terjadinya interupsi pemrosesan paket. Dengan permintaan KELUAR, semuanya sederhana: datanya datang, ini dia. Tetapi interupsi IN dihasilkan bukan ketika host telah mengirim permintaan IN, tetapi ketika buffer pengiriman kosong. Artinya, pada prinsipnya, interupsi ini mirip dengan UART buffer underrun interrupt. Oleh karena itu, ketika kita ingin mentransfer sesuatu ke host, kita cukup menulis datanya ke buffer transfer, tunggu interupsi IN dan tambahkan yang tidak sesuai (jangan lupakan ZLP). Dan oke, bahkan dengan titik akhir "biasa", mereka dikendalikan oleh pemrogram, Anda dapat mengabaikannya untuk saat ini. Namun melalui ep0, pertukaran selalu terjadi. Oleh karena itu, pekerjaan dengannya harus dimasukkan ke dalam perpustakaan.
Akibatnya, awal transfer dilakukan oleh fungsi ep0_send, yang menulis alamat awal buffer dan jumlah data yang akan ditransfer ke variabel global, setelah itu, perhatikan, ia sendiri menarik peristiwa IN pawang untuk pertama kalinya. Di masa mendatang, penangan ini akan dipanggil pada acara perangkat keras, tetapi Anda masih perlu memberikan dorongan.
Nah, penangannya sendiri cukup sederhana: ia menulis bagian data berikutnya ke buffer transfer, menggeser alamat awal buffer dan mengurangi jumlah byte yang tersisa untuk transfer. Kruk terpisah dikaitkan dengan ZLP yang sama dan kebutuhan untuk menanggapi beberapa permintaan dengan paket kosong. Dalam hal ini, akhir transfer ditunjukkan oleh fakta bahwa alamat data telah menjadi NULL. Dan paket kosong - itu sama dengan konstanta ZLPP. Keduanya terjadi jika ukurannya sama dengan nol, jadi tidak ada perekaman yang sebenarnya.
Algoritme serupa harus diterapkan saat bekerja dengan titik akhir lainnya. Tapi ini adalah perhatian pengguna. Dan logika pekerjaan mereka seringkali berbeda dengan bekerja dengan ep0, jadi dalam beberapa kasus opsi ini akan lebih nyaman daripada buffering di level library.
Logika komunikasi USB
Tuan rumah menentukan fakta koneksi dengan adanya resistor pull-up antara jalur data dan catu daya. Dia mengatur ulang perangkat, memberinya alamat di bus dan mencoba untuk menentukan apa yang sebenarnya terjebak di dalamnya. Untuk melakukan ini, ia membaca deskriptor perangkat dan konfigurasi (dan, jika perlu, yang spesifik). Dia juga dapat membaca deskriptor string untuk memahami apa yang disebut perangkat itu sendiri (meskipun jika pasangan VID: PID tidak asing baginya, dia lebih suka menarik garis dari database-nya). Setelah itu, tuan rumah dapat memuat driver yang sesuai dan bekerja dengan perangkat dalam bahasa yang dimengerti. Bahasa yang dipahami mencakup permintaan dan panggilan khusus ke antarmuka dan titik akhir tertentu. Kami akan membahasnya juga, tetapi pertama-tama kami membutuhkan perangkat untuk setidaknya ditampilkan di sistem.
Memproses permintaan SETUP: DeviceDescriptor
Seseorang yang telah mengutak-atik USB setidaknya sedikit harus waspada sejak lama: COKPOWEHEU, Anda berbicara tentang permintaan MASUK dan KELUAR, tetapi SETUP juga dijabarkan dalam standar. Ya, memang demikian, tetapi ini lebih merupakan semacam permintaan KELUAR, terstruktur secara khusus dan ditujukan secara eksklusif untuk titik akhir 0. Mari kita bicarakan tentang struktur dan fitur kerjanya.
Strukturnya sendiri terlihat seperti ini:
typedef struct{
uint8_t bmRequestType;
uint8_t bRequest;
uint16_t wValue;
uint16_t wIndex;
uint16_t wLength;
}config_pack_t;
Bidang struktur ini dipertimbangkan dalam banyak sumber, tetapi saya tetap akan mengingatkan Anda.
bmRequestType adalah bit mask, yang artinya bit sebagai berikut:
7: arah transmisi. 0 - dari host ke perangkat, 1 - dari perangkat ke host. Faktanya, itu adalah tipe transmisi berikutnya, OUT atau IN.
6-5: kelas permintaan
0x00 (USB_REQ_STANDARD) - standar (kami hanya akan memprosesnya untuk saat ini)
0x20 (USB_REQ_CLASS) - khusus kelas (kami akan membahasnya di artikel berikutnya)
0x40 (USB_REQ_VENDOR) - khusus pabrikan ( Saya harap kita tidak perlu menyentuhnya)
4-0: interlocutor
0x00 (USB_REQ_DEVICE) - perangkat secara keseluruhan
0x01 (USB_REQ_INTERFACE) - antarmuka terpisah
0x02 (USB_REQ_ENDPOINT) -
bRequest endpoint - permintaan
wValue sendiri - bidang data kecil 16-bit. Dalam kasus permintaan sederhana, agar tidak mendorong transfer penuh.
wIndex adalah nomor penerima. Misalnya, antarmuka yang ingin dikomunikasikan oleh tuan rumah.
wLength - ukuran data tambahan jika 16 bit wValue tidak cukup.
Pertama-tama, saat menghubungkan perangkat, host mencoba mencari tahu apa sebenarnya yang terjebak di dalamnya. Untuk melakukan ini, ia mengirimkan permintaan dengan data berikut:
bmRequestType = 0x80 (permintaan baca) + USB_REQ_STANDARD (standar) + USB_REQ_DEVICE (ke perangkat secara keseluruhan)
bRequest = 0x06 (GET_DESCRIPTOR) - permintaan deskriptor
wValue = 0x0100 (DEVICE_DESCRIPTOR) - deskriptor perangkat secara keseluruhan
wIndex = 0 - tidak digunakan
wLength = 0 - tidak ada data tambahan
Kemudian mengirimkan permintaan IN, di mana perangkat harus meletakkan jawabannya. Seperti yang kita ingat, permintaan IN dari host dan interupsi pengontrol digabungkan secara longgar, jadi kita akan segera menulis respons ke buffer pemancar ep0. Secara teoritis, data dari ini, dan yang lainnya, deskriptor terikat ke perangkat tertentu, jadi tidak masuk akal untuk meletakkannya di inti pustaka. Permintaan terkait diteruskan ke fungsi usb_class_get_std_descr, yang mengembalikan ke kernel sebuah penunjuk ke awal data dan ukurannya. Intinya adalah bahwa beberapa deskriptor dapat berukuran variabel. Tetapi DEVICE_DESCRIPTOR bukan salah satunya. Ukuran dan strukturnya distandarisasi dan terlihat seperti ini:
uint8_t bLength; //
uint8_t bDescriptorType; // . USB_DESCR_DEVICE (0x01)
uint16_t bcdUSB; // 0x0110 usb-1.1, 0x0200 2.0.
uint8_t bDeviceClass; //
uint8_t bDeviceSubClass; //
uint8_t bDeviceProtocol; //
uint8_t bMaxPacketSize0; // ep0
uint16_t idVendor; // VID
uint16_t idProduct; // PID
uint16_t bcdDevice_Ver; // BCD-
uint8_t iManufacturer; //
uint8_t iProduct; //
uint8_t iSerialNumber; //
uint8_t bNumConfigurations; // ( 1)
Pertama-tama, perhatikan dua kolom pertama - ukuran deskriptor dan tipenya. Mereka tipikal untuk hampir semua deskriptor USB (kecuali untuk HID, mungkin). Selain itu, jika bDescriptorType adalah sebuah konstanta, maka bLength harus dihitung secara manual untuk setiap deskriptor. Pada titik tertentu, saya bosan dengan ini dan makro telah ditulis
#define ARRLEN1(ign, x...) (1+sizeof((uint8_t[]){x})), x
Ini menghitung ukuran argumen yang diteruskan ke sana dan menggantinya, bukan yang pertama. Faktanya adalah kadang-kadang deskriptor bersarang, sehingga yang satu, katakanlah, membutuhkan ukuran dalam byte pertama, yang lain dalam 3 dan 4 (angka 16-bit), dan yang ketiga dalam 6 dan 7 (sekali lagi angka 16-bit) . Makro tidak peduli dengan nilai pasti dari argumen tersebut, tetapi setidaknya angkanya harus sama. Sebenarnya, makro untuk substitusi dalam 1, 3 dan 4, serta 6 dan 7 byte juga ada, tetapi saya akan menunjukkan aplikasinya dengan contoh yang lebih khas.
Untuk saat ini, mari kita lihat bidang 16-bit seperti VID dan PID. Jelas bahwa mencampur konstanta 8-bit dan 16-bit dalam satu larik tidak akan berfungsi, ditambah endiannes ... secara umum, makro kembali menyelamatkan: USB_U16 (x).
Dalam hal pemilihan VID: PID adalah pertanyaan yang rumit. Jika Anda berencana untuk memproduksi produk yang diproduksi secara massal, masih ada baiknya membeli sepasang pribadi. Untuk penggunaan pribadi, Anda dapat mengambil milik orang lain dari perangkat serupa. Katakanlah saya memiliki pasangan dari AVR LUFA dan STM dalam contoh saya. Bagaimanapun, tuan rumah menentukan bug implementasi spesifik daripada tugas dari pasangan ini. Karena tujuan perangkat dijelaskan secara detail dalam deskriptor khusus.
Perhatian, rake!Ternyata, Windows mengikat driver ke pasangan ini, misalnya, Anda memasang perangkat HID, menunjukkan sistem dan menginstal driver. Dan kemudian kami mem-flash ulang perangkat di bawah MSD (flash drive) tanpa mengubah VID: PID, maka driver akan tetap lama dan, tentu saja, perangkat tidak akan berfungsi. Kita harus masuk ke "manajemen perangkat keras", menghapus driver dan memaksa sistem untuk mencari yang baru. Saya pikir tidak mengherankan bagi siapa pun bahwa Linux tidak memiliki masalah ini: perangkat cukup colokkan dan berfungsi.
StringDescriptor
Fitur menarik lainnya dari deskriptor USB adalah kecintaan pada string. Dalam template deskriptor, mereka dilambangkan dengan awalan i, seperti iSerialNumber
Perhatian, rake! Tidak peduli seberapa besar godaannya untuk menempelkan hanya string ke iSerialNumber, bahkan string dengan versi jujur ββseperti u''1.2.3 '' - jangan lakukan itu! Beberapa sistem operasi percaya bahwa seharusnya hanya ada digit heksadesimal, yaitu, '0' - '9', 'A' - 'Z' dan hanya itu. Anda bahkan tidak bisa titik. Mungkin, mereka entah bagaimana menghitung hash dari "nomor" ini untuk mengidentifikasinya saat menyambungkan kembali, saya tidak tahu. Tetapi saya melihat masalah seperti itu ketika menguji pada mesin virtual dengan Windows 7, dia menganggap perangkat itu rusak. Menariknya, Windows XP dan 10 tidak memperhatikan masalah tersebut.
ConfigurationDescriptor
Dari sudut pandang tuan rumah, perangkat mewakili sekumpulan antarmuka terpisah, yang masing-masing dirancang untuk menyelesaikan beberapa masalah. Deskriptor antarmuka mendeskripsikan perangkatnya dan titik akhir terkait. Ya, titik akhir tidak dijelaskan sendiri, tetapi hanya sebagai bagian dari antarmuka. Biasanya, antarmuka dengan arsitektur kompleks dikontrol oleh permintaan SETUP (yaitu, melalui ep0), di mana bidang wIndex sesuai dengan nomor antarmuka. Maksimum diizinkan untuk mengantongi titik akhir untuk interupsi. Dan dari antarmuka data, tuan rumah hanya membutuhkan deskripsi titik akhir dan pertukaran akan melewatinya.
Ada banyak antarmuka dalam satu perangkat, dan sangat berbeda. Oleh karena itu, agar tidak bingung di mana satu antarmuka berakhir dan yang lain dimulai, deskripsi menunjukkan tidak hanya ukuran "header", tetapi juga secara terpisah (biasanya 3-4 byte) ukuran penuh antarmuka. Dengan demikian, antarmuka melipat seperti boneka bersarang: di dalam wadah umum (yang menyimpan ukuran "judul", bDescriptorType dan ukuran penuh konten, termasuk judul) mungkin ada beberapa wadah yang lebih kecil, tetapi diatur dalam cara yang sama. Dan di dalam semakin banyak. Berikut adalah contoh deskriptor untuk perangkat HID primitif:
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
0, // bNumEndpoints
HIDCLASS_HID, // bInterfaceClass:
HIDSUBCLASS_NONE, // bInterfaceSubClass:
HIDPROTOCOL_NONE, // bInterfaceProtocol:
0x00, // iInterface
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_HID, //bDescriptorType
USB_U16(0x0101), //bcdHID
0, //bCountryCode
1, //bNumDescriptors
USB_DESCR_HID_REPORT, //bDescriptorType
USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength
)
)
};
Di sini level bersarangnya kecil, ditambah tidak ada satu titik akhir pun yang dijelaskan - yah, jadi saya mencoba memilih perangkat yang lebih sederhana. Beberapa kebingungan di sini dapat disebabkan oleh konstanta bLENGTH dan wTOTALLENGTH yang sama dengan delapan dan enam belas bit nol. Karena dalam kasus ini makro digunakan untuk menghitung ukuran, akan aneh jika menduplikasi pekerjaannya dan menghitung byte dengan tangan. Betapa anehnya menulis angka nol. Dan konstanta adalah hal yang terlihat, berkontribusi pada kejelasan kode.
Seperti yang Anda lihat, deskriptor ini terdiri dari "header" USB_DESCR_CONFIG (menyimpan ukuran penuh konten termasuk dirinya sendiri!), Antarmuka USB_DESCR_INTERFACE (menjelaskan detail perangkat) dan USB_DESCR_HID, yang secara umum menyatakan jenis HID kami sedang merender. Dan apa yang secara umum: struktur HID tertentu dijelaskan dalam deskripsi khusus HID_REPORT_DESCRIPTOR, yang tidak akan saya pertimbangkan di sini, hanya karena saya terlalu mengetahuinya. Jadi kami akan membatasi diri untuk menyalin-tempel dari beberapa contoh .
Mari kembali ke antarmuka. Mengingat mereka memiliki angka, logis untuk mengasumsikan bahwa terdapat banyak antarmuka dalam satu perangkat. Selain itu, mereka dapat bertanggung jawab baik untuk satu tugas umum (katakanlah, antarmuka kontrol USB-CDC dan antarmuka data), dan untuk tugas yang pada dasarnya tidak terkait. Katakanlah, tidak ada yang menghalangi kita (kecuali kurangnya pengetahuan sejauh ini) pada satu pengontrol untuk mengimplementasikan dua adapter USB-CDC ditambah USB flash drive plus, katakanlah, keyboard. Jelas, antarmuka flash drive tidak tahu tentang port COM. Namun, ada jebakan di sini, yang, saya harap, suatu saat akan kami pertimbangkan. Perlu juga dicatat bahwa satu antarmuka dapat memiliki beberapa konfigurasi alternatif (bAlternateSetting) yang berbeda, katakanlah, dalam jumlah titik akhir atau frekuensi pollingnya. Sebenarnya, itulah mengapa itu dilakukan: jika tuan rumah berpikir bahwa lebih baik menghemat bandwidth,dia dapat mengganti antarmuka ke mode alternatif apa pun yang paling dia suka.
Komunikasi dengan HID
Secara umum, perangkat HID mensimulasikan objek dunia nyata, yang tidak memiliki banyak data sebagai kumpulan parameter tertentu yang dapat diukur atau disetel (permintaan SET_REPORT / GET_REPORT) dan yang dapat memberi tahu host tentang peristiwa eksternal mendadak (INTERRUPT). Jadi, sebenarnya, perangkat ini tidak dimaksudkan untuk pertukaran data ... tetapi siapa yang menghentikannya kapan!
Kami tidak akan membahas interupsi untuk saat ini, karena interupsi memerlukan titik akhir khusus. Tapi kami akan mempertimbangkan membaca dan mengatur parameter. Dalam hal ini, hanya ada satu parameter, yang merupakan struktur dua byte, yang, menurut desain, bertanggung jawab atas dua LED, atau untuk tombol dan penghitung.
Mari kita mulai dengan yang lebih sederhana - membaca atas permintaan HIDREQ_GET_REPORT. Faktanya, ini adalah permintaan yang sama dengan DEVICE_DESCRIPTOR mana pun, hanya khusus untuk HID. Plus, permintaan ini tidak ditujukan ke perangkat secara keseluruhan, tetapi ke antarmuka. Artinya, jika kita telah mengimplementasikan beberapa perangkat HID independen dalam satu perangkat, mereka dapat dibedakan dengan bidang wIndex permintaan tersebut. Benar, ini bukan pendekatan terbaik khusus untuk HID: lebih mudah membuat deskriptor itu sendiri digabungkan. Bagaimanapun, kami jauh dari penyimpangan seperti itu, jadi kami bahkan tidak akan menganalisis apa dan di mana host mencoba mengirim: untuk permintaan apa pun ke antarmuka dan dengan bidang bRequest sama dengan HIDREQ_GET_REPORT, kami akan mengembalikan data aktual. Secara teori, pendekatan ini dimaksudkan untuk mengembalikan deskriptor (dengan semua bLength dan bDescriptorType), tetapi dalam kasus HID, pengembang memutuskan untuk menyederhanakan semuanya dan hanya bertukar data.Jadi kami mengembalikan pointer ke struktur kami dan ukurannya. Nah, sedikit logika tambahan seperti tombol pengolah dan penghitung permintaan.
Kasus yang lebih kompleks adalah permintaan tulis. Ini adalah pertama kalinya kami menemukan data tambahan dalam permintaan SETUP. Artinya, inti pustaka kita harus membaca permintaan itu sendiri terlebih dahulu, baru kemudian datanya. Dan mentransfernya ke fungsi pengguna. Dan saya ingatkan Anda bahwa kami tidak memiliki penyangga. Sebagai hasil dari beberapa sihir tingkat rendah, algoritme berikut dikembangkan. Callback akan selalu dipanggil, tetapi kami akan memberitahukannya dari byte mana data sekarang berada di titik akhir menerima buffer (offset) dan ukuran data ini (ukuran). Artinya, ketika permintaan itu sendiri diterima, nilai offset dan ukurannya adalah nol (tidak ada data). Ketika paket pertama diterima, offset masih nol dan size adalah ukuran data yang diterima. Untuk yang kedua, offset sama dengan ukuran ep0 (karena jika data harus dipecah, mereka melakukannya sesuai ukuran titik akhir), dan ukurannya akan sama dengan ukuran data yang diterima.Dll Penting! Jika data diterima, maka harus dibaca. Ini dapat dilakukan baik oleh penangan dengan memanggil usb_ep_read () dan mengembalikan 1 (mereka mengatakan "Saya pikir di sana sendiri, jangan repot-repot"), atau hanya mengembalikan 0 ("Saya tidak membutuhkan data ini") tanpa membaca - maka inti perpustakaan akan menangani pembersihan. Fungsi ini dibangun berdasarkan prinsip ini: ia memeriksa apakah data tersedia dan, jika demikian, membacanya dan menyalakan LED.
Perangkat lunak pertukaran data
Di sini saya tidak menemukan kembali roda, tetapi mengambil siap pakai Program dari sebelumnya artikel .
Kesimpulan
Faktanya, itu saja. Saya memberi tahu dasar-dasar bekerja dengan USB menggunakan modul perangkat keras di STM32, saya juga menyentuh beberapa penggaruk. Mengingat jumlah kode yang jauh lebih kecil daripada horor yang dihasilkan STMCube, akan lebih mudah untuk mengetahuinya. Faktanya, saya masih belum menemukan jawabannya di Cube noodles, ada terlalu banyak panggilan untuk hal yang sama dalam kombinasi yang berbeda. Jauh lebih baik untuk memahami opsi dari EddyEm , tempat saya memulai. Tentu, ada bukan tanpa tiang tembok, tapi setidaknya cocok untuk pemahaman. Saya juga membanggakan bahwa ukuran versi saya hampir 5 kali lebih kecil dari versi ST (~ 2,7 kB versus 14) - terlepas dari kenyataan bahwa saya belum terlibat dalam pengoptimalan dan, yang pasti, Anda masih dapat mengecilkannya.
Saya juga ingin mencatat perbedaan perilaku berbagai sistem operasi saat menghubungkan peralatan yang dipertanyakan. Linux hanya berfungsi meskipun ada kesalahan pada deskriptornya. Windows XP, 7, 10, dengan sedikit kesalahan, mereka bersumpah bahwa "perangkat rusak, saya menolak untuk bekerja dengannya." Dan XP kadang-kadang bahkan di BSOD jatuh karena marah. Oh, ya, mereka juga terus-menerus menampilkan "perangkat dapat bekerja lebih cepat", saya tidak tahu harus berbuat apa. Secara umum, tidak peduli seberapa bagus Linux untuk pengembangan, itu memaafkan terlalu banyak, perlu untuk menguji pada sistem yang kurang ramah pengguna.
Rencana lebih lanjut: pertimbangkan jenis titik akhir lainnya (sejauh ini hanya ada contoh dengan Kontrol); pertimbangkan pengontrol lain (katakanlah, saya masih memiliki at90usb162 (AVR) dan gd32vf103 (RISC_V) tergeletak di sekitar), tetapi ini adalah rencana yang sangat jauh. Akan menyenangkan juga untuk melihat lebih dekat pada masing-masing perangkat USB seperti HID yang sama, tetapi juga bukan tugas prioritas.