
Здравствуйте меня зовут Дмитрий сегодня мы напишем контроллер USB шины и подключим к нему клавиатуру.
Железо
Для начала нужно определится. Мы создадим контроллер USB версии 1.1. В отличии от USB 2.0 который использует для связи дифференциальную пару. USB 1.1 для связи использует две цифровых линии работающих в противофазе. Поэтому линии данных стандарта 1.1 можно подключать напрямую к цифровым пинам.
Но есть одна тонкость, существует два стандарта USB 1.0 (дальше будем называть Low Speed) cо скоростью 1,5 Мбит/с и USB 1.1 (дальше будем называть Full Speed) со скоростью 12 Мбит/с. Чтобы хост мог понять какой из этих стандартов надо использовать, устройство на одну из линий данных подает единицу. Если Low Speed D-, а если Full Speed то D+.

Чтобы выходы не висели в воздухе. Хост должен притянуть их к земле через резистор. И мы получим вот такую схему подключения.

Кроме того линии данных должны переносить подачу на них напряжения 5В либо, короткое замыкание на землю.
Ну поскольку я использую чип Cyclone IV (а он как известно может выдать только 8мА и таким током тяжело что-то сжечь), проблема короткого замыкания на землю решается сама собой. То вот попадание 5В действительно может вывести чип из строя. Поэтому я подключил к линиям данных ещё стабилитрон на 3.3В.
Кстати в качестве подопытного я использовал плату STM32 BluePill, которая в цикле посылает символы ABC

Подробней о том как сделать клавиатуру из BluePill можно почитать в этой статье
Архитектура модуля

USB модуль состоит из нескольких подмодулей, начнем по порядку:
M_DATA - модуль отвечает за получение и передачу данных. В него входят.
M_CRC16_USB - Все данные при передачи, защищены алгоритмом CRC16, этот модуль подсчитывает контрольную сумму.
M_GET_PACKET - Нельзя просто взять и получить данные по интерфейсу USB. Для начала нужно послать устройству токен IN который скажет что вы готовы получать данные. Потом получить данные и если контрольные суммы совпадут, отправить подтверждение устройству, о том что вы данные получили. За все это отвечает данный модуль.
M_RESIVE_MODULE - Этот модуль получает данные, он в свою очередь состоит из:
M_GET_DATA - Собственно именно он получает данные.
M_MEMORY_BUF_CRC16 - Это буфер данных, но в отличие от просто буферов он подсчитывает CRC16 при заполнении.
M_BUF_RETRONSLATOR - Пока мы не сравнили контрольные суммы мы не можем сказать прислали нам данные или какой-то мусор. Этот модуль ретранслирует данные из буфера если контрольные суммы совпали.
M_SEND_PACKET - Также как нельзя просто получить данные, их нельзя просто передать. Надо послать токен OUT который скажет что мы готовы передавать данные, передать данные и получить подтверждение о том что данные были получены, за это отвечает данный модуль.
M_TRANSMITE_MODULE - этот модуль передает данные, токены, пакеты начала кадра SOF и прочее, как видите хост получает от устройства только данные, а устройство получает от хоста кроме данных ещё много всяких разных пакетов.
M_CRC_5 - Модуль подсчитывающий CRC5 контрольную сумму, токены, пакеты SOF, и подтверждения защищены алгоритмом CRC5.
M_SEND_DATA - Модуль который передает пакеты с данными. То есть пакеты защищенные CRC16.
M_SEND_TOKEN - Несмотря на название, этот модуль передает кроме токенов, пакеты SOF и подтверждения передачи, то есть все что защищено CRC5 контрольной суммой.
M_SEND_END_OF_PACKET - Иногда нужно передать окончание пакета, подробней будет описано ниже.
M_DATA_TRANSFER - Модуль который непосредственно передает данные. То есть остальные модули определяют что передавать, а он передает.
M_SOF_SENDER - Модуль передающий пакеты SOF.
M_MAIN_AUTOMAT - В этом модуле содержится главный конечный автомат. Я вынес его из модуля M_USB чтобы он был не таким большим.
M_USB_INIT - Модуль инициализации USB устройства.
M_TRANZACTION - Модуль пересылающий на устройство "управляющие" транзакции, подробней ниже.
M_HID_ANALIZ - Модуль который проводит анализ HID интерфейса устройства, для понимания формата данных которые будет посылать устройство, подробней ниже.
4 MEMORY_BUF - Эти модули содержат данные модуля M_HID_ANALIZ
Инициализация
Приведу отрывок из модуля M_MAIN_AUTOMAT:
Скрытый текст
always @(posedge clk) begin if (rst) begin State_main <= S_IDLE; SOF_En <= 1'b0; FullSpeedConnect <= 1'b0; KeyBoardData_En <= 1'b0; GetPacket_En <= 1'b0; WaiteCount <= 'd0; DisconnectCount <= 'd0; end else begin case (State_main) S_IDLE: begin if (Dm & !Dp) begin FullSpeedConnect <= 1'b0; ResetTime <= 'd15_000; // 10 ms on 1.5 mHz => 15_000 thics PowerRiseTime <= 'd150_000; // 100 ms on 1.5 mHz => 150_000 thics PollWaiteCount <= 'd36_000; // 24 ms on 1.5 mHz => 36000 thics SetAddressRec = 'd2_500;// 1.6 ms on 1.5 mHz => 2_500 thics State_main <= S_POWER_RISE; end else if(Dp & !Dm) begin FullSpeedConnect <= 1'b1; ResetTime <= 'd120_000; // 10 ms on 12 mHz => 120_000 thics PowerRiseTime <= 'd1_200_000; // 100 ms on 12 mHz => 1_200_000 thics PollWaiteCount <= 'd288_000; // 24 ms on 12 mHz => 288000 thics SetAddressRec = 'd20_000;// 1.6 ms on 12 mHz => 20_000 thics State_main <= S_POWER_RISE; end else begin SOF_En <= 1'b0; FullSpeedConnect <= 1'b0; KeyBoardData_En <= 1'b0; GetPacket_En <= 1'b0; WaiteCount <= 'd0; DisconnectCount <= 'd0; end end S_POWER_RISE: begin if (WaiteCount == PowerRiseTime || SKIP_POWER_RISE) begin WaiteCount <= 'd0; State_main <= S_USB_RESET; end else begin if (!Dm && !Dp) begin State_main <= S_IDLE; WaiteCount <= 'd0; end else WaiteCount <= WaiteCount + 1'b1; end end S_USB_RESET: begin if (WaiteCount == ResetTime || SKIP_POWER_RISE) begin WaiteCount <= 'd0; State_main <= S_INIT_SOF_SENDER; end else begin WaiteCount <= WaiteCount + 1'b1; end end S_INIT_SOF_SENDER: begin if (Eof1) State_main <= S_WAITE_SOF; else SOF_En <= 1'b1; end S_WAITE_SOF: begin if (!Eof1) State_main <= S_USB_RESET_RECOWERY; end S_USB_RESET_RECOWERY: begin if (WaiteCount == ResetTime || SKIP_POWER_RISE) begin WaiteCount <= 'd0; State_main <= S_USB_INIT; end else WaiteCount <= WaiteCount + 1'b1; end S_USB_INIT: begin if (InitComplite) begin GetCollectionNumber <= 'd0; AddrBace <= 'd0; State_main <= S_REQUEST; end else if (InitFail) State_main <= S_FAIL; end S_REQUEST: begin if (GetPacketComplite && !DataValid) begin if (PacketCount + 1'b1 == CollectionPacketCount) begin PacketCount <= 'd0; WaiteCount <= 'd0; KeyBoardData_En <= 1'b0; State_main <= S_WAIT; end else begin AddrBace <= AddrBace + CollectionPacketSize; PacketCount <= PacketCount + 1'b1; State_main <= S_DELAY; end GetPacket_En <= 1'b0; end else if (GetPacketNAK) begin GetPacket_En <= 1'b0; WaiteCount <= 'd0; if (PacketCount == 'd0) begin State_main <= S_WAIT; KeyBoardData_En <= 1'b0; end end else if (GetPacketFail) begin GetPacket_En <= 1'b0; KeyBoardData_En <= 1'b0; WaiteCount <= 'd0; State_main <= S_WAIT; end else begin Addr <= DEVICE_ADDR; EndPoint <= 'h01; PacketType <= P_IN; GetPacket_En <= 1'b1; if (CollectionNum > 1) begin if (DataValid) begin if (GetDataAddr == 'd0) GetCollectionNumber <= GetData - 'd1; if (GetDataAddr == 'd2) // Skip fist 3 bytes. KeyBoardData_En <= 1'b1; end end else begin if (DataValid) begin if (GetDataAddr == 'd1) // Skip fist 2 bytes. If not ID KeyBoardData_En <= 1'b1; end end end end S_DELAY: begin if (!GetPacketComplite) State_main <= S_REQUEST; end S_WAIT: begin if (EndOfPacket) begin if (&DisconnectCount) begin State_main <= S_DISCONNECT_DELAY; DisconnectCount <= 'd0; end else DisconnectCount <= DisconnectCount + 1'b1; end else if (WaiteCount == PollWaiteCount || SKIP_POWER_RISE) begin PacketCount <= 'd0; AddrBace <= 'd0; GetCollectionNumber <= 'd0; DisconnectCount <= 'd0; State_main <= S_REQUEST; end else begin DisconnectCount <= 'd0; WaiteCount <= WaiteCount + 1'b1; end end S_DISCONNECT_DELAY: begin if (WaiteCount == PowerRiseTime || SKIP_POWER_RISE) begin WaiteCount <= 'd0; State_main <= S_IDLE; end else WaiteCount <= WaiteCount + 1'b1; end S_FAIL: begin if (EndOfPacket) begin if (&DisconnectCount) begin State_main <= S_DISCONNECT_DELAY; DisconnectCount <= 'd0; end else DisconnectCount <= DisconnectCount + 1'b1; end else DisconnectCount <= 'd0; end endcase end end
Сначала мы дожидаемся высокого уровня по одной из линий данных благодаря чему мы узнаем какую скорость поддерживает устройство. После этого мы должны дать устройству 100 миллисекунд для поднятия рабочих напряжений.
Потом идет сброс осуществляемый подачей 0 на обе шины данных в течении 10 миллисекунд. После сброса мы должны дать устройству ещё 10 миллисекунд, перед инициализацией. Но когда я делал именно так у меня некоторые клавиатуры не хотели работать. А проблема была в пакетах SOF.

SOF это признак начала кадра, эти пакеты устройство использует для синхронизации с хостом. Но для нас важно что если мы не будем их отсылать то устройство вообще не будет на нас реагировать. SOF отсылается каждую миллисекунду. Причем для Full Speed устройств SOF это пакет с номером и CRC5 контрольной суммой, а для Low Speed устройств SOF это просто окончание пакета (два такта обе линии прижаты к 0), собственно для этого и нужен модуль отправляющий окончание пакета.
Моя проблема была в том что я начинал бомбардировать устройство SOFми после ожидания 10 миллисекунд после сброса. И какие-то клавиатуры на это реагировали нормально а какие-то претворялись трупом. А правильно начинать слать SOFы сразу после сброса. Я так подробно это описываю, потому что сам застрял очень на долго с этим и не понимал почему одни клавиатуры прекрасно работают а, другие не работают вообще.
В общем если устройство все таки ожило его надо инициализировать. Честно скажу что когда писал этот модуль я всю информацию почерпнул из видео Бена Итера ссылка на его видео будет в конце. Он в видео при помощи анализатора сигналов перехватил сеанс связи компьютера с клавиатурой и разобрал его, вот собственно эти данные:
Скрытый текст

Вы скажите ничего ведь не понятно. И будете правы. Но к счастью Бен Итер расшифровал все эти данные. Итак приступим.

Слева это запросы хоста то есть нас, справа ответы устройства.
С начало у устройства нету адреса поэтому мы должны назначить устройству какой-то адрес. По умолчанию устройство имеет адрес 0, но этот адрес можно использовать только при инициализации.
Все действия по управлению устройством, мы будем осуществлять при помощи управляющих транзакций. Это те самые транзакции за которые отвечает модуль M_TRANZACTION.

Транзакции бывают 3-х видов. Мы можем отправить токен SETUP за ним данные запроса, а после ещё какие-то дополнительные данные. После чего ждать подтверждения. Такой тип транзакции в моем модуле я ни разу не использовал.
Можно после запроса получить какие-то данные с устройства, и тогда подтверждение в конце транзакции шлем мы сами, это самый распространённый тип транзакции.
Ну и третий тип когда мы шлем запрос устройству, а потом бомбардируем устройство токенами IN пока оно не сделает то что нам нужно. Адрес устанавливается именно при помощи такой транзакции.
После смены адреса мы должны дать устройству отдохнуть 1.6мс.
После этого уже при помощи нового адреса мы считываем дескриптор устройства. Для этого используем второй тип транзакции. Дескриптор устройства всегда имеет размер 18 байт. Здесь очень важен параметр MaxPacketSize. Это размер пакета который может принимать и отдавать устройство. Также мы узнаем номера строк в которых содержится информация о клавиатуре, после этого эти строки можно будет запросить у устройства. А также у устройства есть одна конфигурация которую можно применить, об этом ниже.
Потом получаем эти строки и узнаем что клавиатура произведена фирмой Dell. Все строки закодированы при помощи таблицы символов Unicode, каждый символ имеет размер 2 байта.

Запрашиваем у устройства конфигурацию (о которой мы узнали из дескриптора), и узнаем что у этого устройства есть интерфейс. Не долго думая запрашиваем интерфейс.
Из интерфейса узнаем что это так называемый HID интерфейс. И он тоже имеет свой дескриптор. А также этот интерфейс передает данные через Endpoint 1 размером 8 байт. Endpoint это такой буфер внутри устройства.
После этого нужно установить конфигурацию. Ну а поскольку конфигурация всего одна, то мы просто говорим, да мы согласны с этой конфигурацией.

А вот тут уже начинаются намного более сложные вещи. Мы запрашиваем дескриптор HID интерфейса. В нем описывается за что отвечает каждый из 8 байт содержащихся в Endpoint 1. Вообще анализ подобных вещей производят в драйверах устройства, написанных на языках программирования вроде C++. Эти языки имеют средства структурирования данных, а Verilog таких средств не имеет. Поэтому я старался как мог но честно скажу модуль M_HID_ANALIZ это наверно самый слабый модуль в проекте. Итак что мы там видим.
Скрытый текст
DeviceHIDKeyboard[0] = 'h05; DeviceHIDKeyboard[1] = 'h01; //Usage page 1 (Generic desktop) DeviceHIDKeyboard[2] = 'h09; DeviceHIDKeyboard[3] = 'h06; //Usage (Keyboard) DeviceHIDKeyboard[4] = 'hA1; DeviceHIDKeyboard[5] = 'h01; //Collection(Application) //Fist byte MODIFIER DeviceHIDKeyboard[6] = 'h05; DeviceHIDKeyboard[7] = 'h07; //Usage page 7 (Keyboard/KeyPad) DeviceHIDKeyboard[8] = 'h19; DeviceHIDKeyboard[9] = 'hE0; //Local usage minimum E0 DeviceHIDKeyboard[10] = 'h29; DeviceHIDKeyboard[11] = 'hE7; //Local usage maximum E7 DeviceHIDKeyboard[12] = 'h15; DeviceHIDKeyboard[13] = 'h00; //Logical minimum: 0 DeviceHIDKeyboard[14] = 'h25; DeviceHIDKeyboard[15] = 'h01; //Logical maximum: 1 DeviceHIDKeyboard[16] = 'h75; DeviceHIDKeyboard[17] = 'h01; //Report size 1 bit DeviceHIDKeyboard[18] = 'h95; DeviceHIDKeyboard[19] = 'h08; //Report count: 8 reports DeviceHIDKeyboard[20] = 'h81; DeviceHIDKeyboard[21] = 'h02; //Input (variable) //Second byte RESERVED DeviceHIDKeyboard[22] = 'h95; DeviceHIDKeyboard[23] = 'h01; //Report count: 1 reports DeviceHIDKeyboard[24] = 'h75; DeviceHIDKeyboard[25] = 'h08; //Report size 8 bit DeviceHIDKeyboard[26] = 'h81; DeviceHIDKeyboard[27] = 'h01; //Input (constant) //Led states 3 bit DeviceHIDKeyboard[28] = 'h95; DeviceHIDKeyboard[29] = 'h03; //Report count: 3 reports DeviceHIDKeyboard[30] = 'h75; DeviceHIDKeyboard[31] = 'h01; //Report size 1 bit DeviceHIDKeyboard[32] = 'h05; DeviceHIDKeyboard[33] = 'h08; //Usage page 8 DeviceHIDKeyboard[34] = 'h19; DeviceHIDKeyboard[35] = 'h01; //Local usage minimum 1 DeviceHIDKeyboard[36] = 'h29; DeviceHIDKeyboard[37] = 'h03; //Local usage maximum 3 DeviceHIDKeyboard[38] = 'h91; DeviceHIDKeyboard[39] = 'h02; //Output (variable) //Padding 5 bit for 8 DeviceHIDKeyboard[40] = 'h95; DeviceHIDKeyboard[41] = 'h01; //Report count: 1 reports DeviceHIDKeyboard[42] = 'h75; DeviceHIDKeyboard[43] = 'h05; //Report size 5 bit DeviceHIDKeyboard[44] = 'h91; DeviceHIDKeyboard[45] = 'h01; //Output (Constant) //Six key codes bytes DeviceHIDKeyboard[46] = 'h95; DeviceHIDKeyboard[47] = 'h06; //Report count: 6 reports DeviceHIDKeyboard[48] = 'h75; DeviceHIDKeyboard[49] = 'h08; //Report size 8 bit DeviceHIDKeyboard[50] = 'h15; DeviceHIDKeyboard[51] = 'h00; //Logical minimum: 0 DeviceHIDKeyboard[52] = 'h26; DeviceHIDKeyboard[53] = 'hFF; DeviceHIDKeyboard[54] = 'h00; //Logical maximum: 00FF DeviceHIDKeyboard[55] = 'h05; DeviceHIDKeyboard[56] = 'h07; //Usage page 7 (Keyboard/Keypad) DeviceHIDKeyboard[57] = 'h19; DeviceHIDKeyboard[58] = 'h00; //Local usage minimum: 00 DeviceHIDKeyboard[59] = 'h2A; DeviceHIDKeyboard[60] = 'hFF; DeviceHIDKeyboard[61] = 'h00; //Local usage maximum: 00FF DeviceHIDKeyboard[62] = 'h81; DeviceHIDKeyboard[63] = 'h00; //Input (array) DeviceHIDKeyboard[64] = 'hC0; //End collection
Первый байт это 8 бит отвечающих за клавиши модификации то есть Alt Ctr Shift. Клавиши изменяющие поведение других клавиш. Следующий байт зарезервирован имеет тип Constant.
Следующие 3 бита отвечают за светодиоды клавиатуры NumLock и т.д. Поле имеет значение Output.
Следующие 5 бит выравнивание.
Ну и шесть полей по 8 бит это коды нажатых клавиш. То есть одновременно можно нажать только 6 клавиш. То что нас и интересует.

Ну а после анализа интерфейса. Мы устанавливаем время опроса. Соглашаемся с HID протоколом и выключаем светодиоды на клавиатуре.
Все теперь из Endpoint 1 можно получать данные о нажатых кнопках с интервалом 24ms.
CRC
Пара слов про CRC. Когда начал этот проект и увидел что для проверки используется контрольная сумма CRC16 я обрадовался ведь я уже делал подобный модуль в контроллере SD карт. Но как оказалось в USB используется совсем не тот CRC16 что в интерфейсе SD карт. И таблица для CRC16 создается совсем другим образом. Поэтому вот программа создающая таблицу для CRC16 модуля:
CRC16
#include <windows.h> #include <iostream> #include <stdint.h> #include <fstream> #include <filesystem> #include <bitset> //x^16 + x^15 + x^2 + 1 /* const unsigned short Crc16Table[256] = { 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 }; */ void CRC_table_Generate(uint16_t* CRCTable) { const uint16_t LengthCRC = 16; const uint16_t polynomial = 0xA001; // This is invert 0x8005 for (int i = 0; i < 256; i++) { CRCTable[i] = i; for (int j = 0; j < 8; j++) { if (CRCTable[i] & 0x1) CRCTable[i] = (CRCTable[i] >> 1) ^ polynomial; else CRCTable[i] = CRCTable[i] >> 1; } } } uint16_t Crc16( const uint8_t * data, size_t length ) { uint16_t crc = 0xffff; uint16_t crc_tmp = 0; uint32_t index = 0; unsigned short Crc16Table[256]; CRC_table_Generate(Crc16Table); for (int i = 0; i < length; i++) { crc_tmp = crc >> 8; crc = crc & 0xFF; index = crc ^ data[i]; crc = Crc16Table[index] ^ crc_tmp; } return crc ^ 0xFFFF; } int main() { const int Arr_len = 9; unsigned char message [Arr_len] = {0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39}; unsigned short Crc16Table[256]; std::cout<<"CRC = "<< std::hex << Crc16(message, Arr_len) << std::endl; //Create Table.txt std::ofstream fout("mem_block.txt"); CRC_table_Generate(Crc16Table); for (int i = 0; i < 256; i++) { fout << "mem[" << i << "] = 16'b" << std::bitset<16>(Crc16Table[i]) << ";" << std::endl; } return 0; }
Ну и как я уже говорил некоторые пакеты защищены контрольной суммой CRC5. Таблицу для неё я не нашел, поэтому модуль вычисляет эту сумму в лоб, без таблицы.
CRC5
#include <windows.h> #include <iostream> #include <stdint.h> #include <fstream> #include <filesystem> #include <bitset> //poly x^5 + x^2 + 1 unsigned char crc5usb(unsigned short input) { unsigned char crc = 0x1f; unsigned char b; for (int i = 0; i < 11; ++i) { b = (input ^ crc) & 0b00000001; input = input >> 1; if (b) { crc = (crc >> 1) ^ 0x14; // 10100 - revers 0x05 or x^5 + x^2 + 1 poly } else { crc = (crc >> 1); } } return crc ^ 0x1f; } int main() { uint8_t endp = 0x1; uint8_t addr = 0x18; std::cout<<"CRC = "<< std::bitset<5>(crc5usb((endp << 7) | addr)) << std::endl; return 0; }
Вывод на экран

USB модуль это хорошо, но если данные от него некуда выводить, то какой в нем смысл? Поэтому я дописал модули которые выводят данные от него на экран.

M_USB - Наш модуль
M_VIDEO_ADAPTER - Модуль для выв��да изображения через VGA
M_SCREEN - Модуль экрана который состоит из
M_SCREEN_CLEANER - Очищает экран при сбросе.
M_TEXT_CONTROLLER - Этот модуль получает данные от M_USB декодирует их при помощи M_UNICODE_TABLE или M_HID_CODE_TABLE и записывает их в StringRAM.
Тут нужно пояснить. Дело в том что через шину USB идет много служебной информации. Поэтому в M_USB модуле я создал две переменные. GetName и KeyBoardData_En. Если активирована первая это значит что через USB шину передается строка информации и декодировать данные надо при помощи Unicode таблицы. Если активирована KeyBoardData_En то это значит через шину передаются опкоды кнопок клавиатуры и декодировать данные надо при помощи M_HID_CODE_TABLE. А если просто активирована переменная GetDataValid то передаются какие-то служебные данные которые не нужно выводить на экран.
M_UNICODE_TABLE - Таблица Unicode кодов. Латинские буквы и цифры в Unicde совпадают с ascii. Просто откидываем ноль и делаем вид что это ascii.
M_HID_CODE_TABLE - Таблица опкодов клавиш клавиатуры.
M_INICIALIZATION_FAIL - Строка которая выводится на экран если инициализация USB устройства закончилась неудачно.
M_STRING_BUFER_FILLER - Модуль который получает номер строки от M_VIDEO_ADAPTER и заполняет буфер ScreenRAM пикселями.
M_FONT_ROM - Буфер содержащий шрифт размером 8x10. Шрифт в формате bmp можно найти в репозитории, а в модуль я его загружал при помощи этой программы.
Скрытый текст
#include <windows.h> #include <iostream> #include <locale.h> #include <wchar.h> #include <fstream> #include <vector> #include <bitset> #include <filesystem> BITMAPFILEHEADER FileHeader; BITMAPINFOHEADER InfoHeader; std::vector<std::vector<unsigned char>> Font; unsigned char buf; void DrawPixel(unsigned char Pix); int main() { std::ifstream fin("Font16.bmp", std::ios::binary); std::ofstream fout("Font16.txt"); if(fin.fail()) { std::cout << "Can't open bmp file " << std::endl; return 0; } fin.read ((char*)&FileHeader, sizeof(BITMAPFILEHEADER)); if(FileHeader.bfType != 0x4d42) { std::cout << "Wrong file type " << std::endl; return 0; } fin.read ((char*)&InfoHeader, sizeof(BITMAPINFOHEADER)); if(InfoHeader.biSize != 0x28) { std::cout << "Wrong header size " << std::endl; return 0; } if(InfoHeader.biBitCount != 0x1) { std::cout << "Wrong Bit depth " << std::endl; return 0; } std::cout << "Width = " << InfoHeader.biWidth << std::endl; std::cout << "Hidth = " << InfoHeader.biHeight << std::endl; std::cout << "bfOffBits = " << (int)FileHeader.bfOffBits << std::endl; fin.seekg(FileHeader.bfOffBits, std::ios::beg); int FontCount = InfoHeader.biWidth/8; int alignment = 4 - (FontCount % 4); //Bitmap data alignmented by 4 Font.resize(10); std::cout << "FontCount = " << FontCount << std::endl; std::cout << "alignment = " << alignment << std::endl; for (int i = 9; i > -1; i--) { Font[i].resize(FontCount); for (int j = 0; j < FontCount; j++) { fin.read((char*)&buf, sizeof(char)); buf ^= 0b11111111; std::bitset<8> Bits; for (int k = 0; k < 8; k++) { if (buf & (1 << k)) { Bits.set(7 - k, true); } } Font[i][j] = (unsigned char)Bits.to_ulong(); } fin.seekg(alignment, std::ios::cur); } int count = 0; for(int j = 0; j < FontCount; j++) { for (int i = 0; i < 10; i++) { if (i == 0) { if (j == 0) fout << "Font_mem[" << count++ << "] = 8'b" << std::bitset<8>(Font[i][j]) << "; // N = " << j << " L = Space" << std::endl; else if (j > 0 && j <= 10) fout << "Font_mem[" << count++ << "] = 8'b" << std::bitset<8>(Font[i][j]) << "; // N = " << j << " L = " << j - 1 << std::endl; else if (j > 10 && j <= 36) fout << "Font_mem[" << count++ << "] = 8'b" << std::bitset<8>(Font[i][j]) << "; // N = " << j << " L = " << (char)(j + 54) << std::endl; else fout << "Font_mem[" << count++ << "] = 8'b" << std::bitset<8>(Font[i][j]) << "; // " << std::endl; } else fout << "Font_mem[" << count++ << "] = 8'b" << std::bitset<8>(Font[i][j]) << ";" << std::endl; for (int k = 0; k < 8; k++) { if (Font[i][j] & (1 << k)) DrawPixel(0b111111); else DrawPixel(0b000000); } std::cout << std::endl; } fout << std::endl; } fin.close(); fout.close(); return 0; } void DrawPixel(unsigned char Pix) { switch (Pix) { case 0b000000: std::cout << " " ; break; case 0b111111: std::cout << (char)219; break; } }
Шрифт нужно класть в папку с программой.
Также если вам понравилась эта статья то возможно вам понравятся и другие статьи на эту тему:
Создание полнофункционального (не-SPI) контроллера SD карт на FPGA
Написание i2c контроллера для FPGA и подключение камер ov7670 и ov2640
Видео Бена Итера по USB:
