Здравствуйте меня зовут Дмитрий сегодня мы напишем контроллер 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+.

Схема USB порта STM32 BluePill
Схема USB порта STM32 BluePill

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

Кроме того линии данных должны переносить подачу на них напряжения 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;

	}
	
}

Шрифт нужно класть в папку с программой.

Репозиторий на GitHub.

Также если вам понравилась эта статья то возможно вам понравятся и другие статьи на эту тему:

Видео Бена Итера по USB:

How does a USB keyboard work?

How does USB device discovery work?