Подключение современной USB-мыши к ретро компьютеру с шиной ISA

от автора

В ретрокомпьютерной технике зачастую возникают задачи, обратные актуальным сегодня. Если обычно мы часто сталкиваемся с проблемами, пытаясь запустить старые программы на новом оборудовании, то в ретровании проблемы проявляются куда как чаще и разнообразнее, к примеру как заставить современную периферию работать с машиной тридцати- сорокалетней давности. И если с подключением клавиатуры к старому ПК обычно сложностей немного: старые AT клавиатуры довольно живучие, сохранилось их много и стоят они сравнительно недорого. К тому же можно подключить PS/2 клавиатуру с помощью простого пассивного переходника. То с мышью ситуация гораздо сложнее: COM портовые мыши обычно шариковые, осталось их не так много, из-за того, что в какой-то момент их стали активно заменять на оптические с разъемом PS/2. Какая-то их часть тоже может подключаться в COM порту через пассивный переходник, но таких мышей немного, да и сами PS/2 мыши уже стали раритетом. Подключить же USB мышь к какому-нибудь XT или AT вплоть до 486, да еще так, чтобы работало со старыми операционными системами штатно не получится. В этой заметке я попробую рассказать о проекте, который начинался как попытка воспроизвести существующее устройство, а вылился в самостоятельную разработку.

Пришлось как-то мне переехать, и я решил на новом месте собрать себе 486 «для души». Часть необходимого у меня была, но возник вопрос используемой периферии. К счастью, моя старая верная USB клавиатура Zalman ZM-K300M заработала через цепочку переходников USB-PS/2-AT. COM портовую мышь же искать не хотелось и я обратил внимание на тему Vogons, посвященную адаптеру PS/2 мыши для шины ISA. Устройство эмулировало микросхему последовательного порта 8250 и, получая данные от PS/2 мыши, почти синхронно передавало их на ISA шину. Благодаря этому задержки были минимальны, и мышь вела себя практически так же отзывчиво, как PS/2, заметно отличаясь в лучшую сторону от классических шариковых COM-портовых мышей.

Адаптер строился на двух ключевых компонентах: программируемой логической матрице Altera EPM3064, эмулировавшей UART 8250 и реализующий все необходимое для работы с шиной ISA. И микроконтроллере Atmega8 обрабатывавшем сигналы PS/2. Устройство поддерживало протоколы двух- и трёхкнопочных мышей Logitech и Microsoft с колесом прокрутки, а также реализовало возможность уменьшать скорость передачи данных для очень медленных процессоров, это должно было разгружать систему на системах XT и AT с ранними процессорами вроде 8086 и 286, что для моего 486, конечно же, бесполезно.

Заинтересовавшись проектом, я решил его повторить. К сожалению, на форуме были представлены лишь печатная плата и перечень элементов, прошивки отсутствовали. К несчастью, об этом я узнал лишь заказав печатные платы. Решив, что это знак свыше о необходимости освоить программирование CPLD, я принялся за исследования для написания собственной прошивки.

Логика работы устройства проста. Atmega принимает от PS/2 мыши информацию о перемещении по осям X и Y, нажатых кнопках и вращении колеса прокрутки. Затем преобразует эти данные в формат, ожидаемый драйвером последовательной мыши, и отправляет данные по SPI интерфейсу в CPLD и дальше по шине ISA они попадают в драйвер. За основу был принят код github проекта avr-mouse-ps2-to-serial, в котором была заменена часть, отвечающая за передачу данных в UART на другую, передающую данные с помощью soft SPI, а также добавлена логика инициализации при включении мыши.

Логическая матрица, со своей стороны, эмулирует присутствие микросхемы UART по определённым адресам ввода-вывода. Когда центральный процессор обращается к этим адресам (например, читает регистр данных или состояния), CPLD подставляет нужные значения. Если процессор записывает данные в управляющие регистры, CPLD делает вид, что запоминает настройки скорости и формата — хотя реальной асинхронной передачи данных через UART не происходит.

Ключевой момент здесь: эмуляция работает синхронно. Как только от мыши приходит новый пакет, контроллер передает данные в CPLD и формирует сигнал запроса прерывания. Процессор, обрабатывая это прерывание, читает данные из UART и передаёт их драйверу мыши. Задержки минимальны, так как передача по происходит по интерфейсу SPI с битовой скоростью порядка мегагерца против стандартных мышиных 1200 бод.

#define SPI_SEND_BIT(bit_mask, tx_data) do {  spi_sck_low();  asm volatile(“nop\n\t”);  if ((tx_data) & (bit_mask)) spi_mosi_high(); else spi_mosi_low();  asm volatile(“nop\n\t”);  spi_sck_high();  asm volatile(“nop\n\t”);  } while (0)
#define SPI_SEND_PACKET(tx_data) do { SPI_SEND_BIT(0x40, (tx_data)); SPI_SEND_BIT(0x20, (tx_data)); SPI_SEND_BIT(0x10, (tx_data)); SPI_SEND_BIT(0x08, (tx_data)); SPI_SEND_BIT(0x04, (tx_data)); SPI_SEND_BIT(0x02, (tx_data)); SPI_SEND_BIT(0x01, (tx_data)); /Завершение передачи/ spi_sck_low(); spi_mosi_low(); }while (0);

В процессе разработки, выяснилось, что полноценная эмуляция 8250 не помещается в примененную CPLD EPM3064. Всплыли некоторые ошибки в схеме оригинального устройства. К примеру, из-за того, что atmega видит только IRQ вместо внутреннего состояния готовности данных внутри CPLD, логика работы контроллера зависела от разрешения прерываний, что потенциально могло приводить к проблемам на некоторых конфигурациях. Так или иначе, в итоге на листочке в клеточку была нарисована схема адаптера и набросаны основные идеи, необходимые для написания кода.

После сравнительно недолгого процесса разработки был представлен проект ps-2-mouse-to-isa-replica, полностью совместимый с оригинальными платами и деталями. Основные сложности, как и ожидалось были связаны с CPLD. Ресурсов выбранной матрицы оказалось слишком мало для полной реализации 8250, даже для того, чтобы BIOS мог видеть плату как COM порт, пришлось дизассемблировать BIOS 486 и разобраться как происходит детектирование портов в реальном железе. К счастью, эта процедура оказалась одинаковой у AWARD и AMI BIOS. В итоге из функциональности UART пришлось оставить только самое главное: семь бит регистра данных, используемых мышью, регистр состояния и управление частью линий запроса прерывания. Всё, что не использовалось драйверами последовательных мышей и процедурами BIOS определения наличия COM порта, было отброшено, включая режим внутреннего loopback. Хотя это позволило уместить логику в ограниченный объём CPLD, но потребовало дополнительной проверки работоспособности с разными драйверами и материнскими платами. И всё равно остается некоторая вероятность, что на какой-то материнской плате с нестандартным биос COM порт может не детектироваться и ресурсы платы придется указывать вручную. Впрочем, в реальности на всем протестированном железе проблем не возникло.

В итоге, код эмуляции UART стал выглядеть следующим образом:
Чтение из 8250:

if (device_select = '1') thendata_out <= (others => '0');case isa_addr(2 downto 0) iswhen "000" => -- Регистр данныхif sig_DLAB = '0' then -- !DLAB checkdata_out <= "0" & rx_data_reg;  -- Прочитали данные UARTelsedata_out <= gen_reg;end if;when "001" => -- Регистр разрешения прерыванияif sig_DLAB = '0' then -- !DLAB checkdata_out <= "0000" & int_ena_reg;elsedata_out <= gen_reg;end if;when "010" => -- причина прерывания: xxxxx10x = принят символ; сбрасывается чтением приемникаif RxD_IRQ = '1' then             -- Прерывание готовности принятого символаdata_out <= "00000100";       -- Сигнализация готовности принятого символаelseif int_ena_reg(1) = '1' then -- Прерывание готовности передачи символаdata_out <= "00000010";   -- Сигнализация готовности передачи символаelsedata_out <= "00000001";   -- Нет прерываний для обработкиend if;end if;when "011" =>                         -- Line control registerdata_out <= sig_DLAB & gen_reg(6 downto 0);when "100" =>                         -- Modem control registerdata_out <= "000" & mdm_ctl_reg;when "101" =>                         -- Line status registerdata_out <= "0110000" & RxD_IRQ; when "110" =>                         -- Modem status registerdata_out <= "00" & mdm_ctl_reg(0) & mdm_ctl_reg(1) & "0000"; -- CTS = RTS , DSR = DTRwhen others => null;end case;end if;

Запись в 8250:

if (device_select = '1') then    case isa_addr(2 downto 0) is        when "000" => -- Регистр разрешения прерываний            if sig_DLAB = '1' then -- DLAB check                gen_reg <= isa_data;            end if;        when "001" => -- Регистр разрешения прерываний            if sig_DLAB = '0' then -- !DLAB check                int_ena_reg <= isa_data(3 downto 0);            else--            gen_reg <= isa_data; -– конфликтует с определением порта в биос             end if;        when “011” => gen_reg(6 downto 0) <= isa_data(6 downto 0);            sig_DLAB <= isa_data(7);        when “100” => mdm_ctl_reg <= isa_data(4 downto 0);        when others => null;    end case;end if;

Логика выборки устройства, разрешения прерываний и сигнала включения мыши:

    -- Комбинаторная логика    mcu_isa_res <= not isa_reset; -- Передача сигнала сброса ISA шины на MCU    mcu_DTR <= enable_mouse;    device_select <= '1' when (isa_aen = '0') and (device_rdy = '1') and                             (isa_addr(9 downto 3) = BASE_ADDR_ROM(to_integer(unsigned(base_addr_val)))) else '0';    isa_data <= data_out when (isa_reset = '0') and (device_select = '1') and (isa_ior = '0') else (others => 'Z');    enable_IRQ <= enable_mouse and mdm_ctl_reg(3);               -- OUT2 разрешает прерывания--    IRQ_state <= (RxD_IRQ and int_ena_reg(0)) or int_ena_reg(1); -- TxD_IRQ всегда выставлен    IRQ_state <= (RxD_IRQ and int_ena_reg(0));                   -- Игнорируем TxD_IRQ     -- OUT2 разрешает прерывания    IRQ4 <= '0' when (IRQ_state = '0') and (enable_IRQ = '1') and (base_addr_val(0) = '0') and                                                (use_opt_irq = '0') and (device_rdy = '1') else 'Z'; -- COM1/COM3    IRQ3 <= '0' when (IRQ_state = '0') and (enable_IRQ = '1') and (base_addr_val(0) = '1') and                                                (use_opt_irq = '0') and (device_rdy = '1') else 'Z'; -- COM2/COM4    IRQX <= '0' when (IRQ_state = '0') and (enable_IRQ = '1') and (use_opt_irq = '1') and                                                                        (device_rdy = '1') else 'Z'; -- Custom

Для понимания что PC получает от адаптера, был написан простейший аналог драйвера мыши. Чтобы не мучаться с переносимостью, написал его сразу на турбопаскале.

program SimpleMouseUART;uses  Dos, crt;const  COM1_BASE = $3F8;  COM2_BASE = $2F8;  COM3_BASE = $3E8;  COM4_BASE = $2E8;  IRQ7 = $0F;  IRQ6 = $0E;  IRQ5 = $0D;  IRQ4 = $0C;  IRQ3 = $0B;  PIC1_CMD = $20;  PIC1_IMR = $21;var  OldIntVec : pointer;  DataByte  : byte;  COM_BASE : word;  IRQ_VECTOR : word;  irq_mask : byte;function tohex(x: byte) : string;const hex: array[0..15] of char = '0123456789ABCDEF';var fdig: byte;begin fdig := x div 16; tohex := hex[fdig] + hex[x mod 16];end;function PortRead(_port:word): byte;begin  PortRead := Port[_port];end;procedure PortWrite(_port: word; value: byte);begin  Port[_port] := value;end;procedure COM1_Interrupt; interrupt;begin  if (PortRead(COM_BASE + 5) and $01) <> 0 then { LSR bit0: Data Ready }  begin    DataByte := PortRead(COM_BASE);    Write(tohex(DataByte)+' '); { For test, simply print symbol }  end;  { reset 8259A }  PortWrite(PIC1_CMD, $20);end;procedure InitCOM;begin  { set 1200 baud (for Microsoft Mouse) }  PortWrite(COM_BASE + 4, $00);      { MCR: 0 }  PortWrite(COM_BASE + 3, $80);      { LCR: DLAB=1 }  PortWrite(COM_BASE + 0, $60);      { DLL = 96 -> 115200/96 ≈ 1200 }  PortWrite(COM_BASE + 1, $00);      { DLM = 0 }  PortWrite(COM_BASE + 3, $03);      { DLAB = 0, 8 bit, no parity, 1 stop }  PortWrite(COM_BASE + 4, $0B);      { MCR: DTR + RTS + OUT2 (enable IRQ) }  PortWrite(COM_BASE + 1, $01);      { IER: enable rxd irq }  { Enable IRQ4 in PIC }  if (IRQ_VECTOR = IRQ3) then    irq_mask := $08;  if (IRQ_VECTOR = IRQ4) then    irq_mask := $10;  if (IRQ_VECTOR = IRQ5) then    irq_mask := $20;  if (IRQ_VECTOR = IRQ6) then    irq_mask := $40;  if (IRQ_VECTOR = IRQ7) then    irq_mask := $80;  PortWrite(PIC1_IMR, PortRead(PIC1_IMR) and (not irq_mask)); { enable IRQ }end;procedure DoneCOM;begin  PortWrite(COM_BASE + 1, 0);   { disable irq UART }  PortWrite(COM_BASE + 4, $00);      { MCR: DTR + RTS + OUT2 = 0 }  PortWrite(PIC1_IMR, PortRead(PIC1_IMR) or irq_mask); { disable IRQ}end;begin  IRQ_VECTOR := IRQ4;  COM_BASE := COM1_BASE;  ClrScr;  WriteLn('UART mouse demo');  DataByte := 0;  GetIntVec(IRQ_VECTOR, OldIntVec);  SetIntVec(IRQ_VECTOR, @COM1_Interrupt);  WriteLn('Listening COM port (Ctrl-Break to exit)...');  InitCOM;  repeat  until KeyPressed;  DoneCOM;  SetIntVec(IRQ_VECTOR, OldIntVec);  PortWrite(COM_BASE + 4, $00);      { MCR: 0 }  WriteLn('Done.');end.

В процессе разработки выяснилось, что CPLD иногда ведет себя странно и не работает с некоторыми драйверами: сигнал «залипает» и мышь подвисает. Пришлось освоить эмулятор CPLD и написать тесты. В итоге код был немного переписан без изменения логики работы и железо стало работать совершенно стабильно.

Пример теста, эмулирующего процедуру определения COM порта BIOS.

 report "=== TEST 11: BIOS detect test";    write_reg(COM1_base_addr, "010", "00111000"); -- FCR reg    read_reg(COM1_base_addr, "010", read_val);    assert read_val(7 downto 0) /= "11111111"        report "FAIL: got " & to_string(read_val)        severity error;    read_reg(COM1_base_addr, LCR_addr, read_val);    write_reg(COM1_base_addr, LCR_addr, "1" & read_val(6 downto 0));    write_reg(COM1_base_addr, RDR_addr, "01010101"); -- FCR reg    write_reg(COM1_base_addr, IER_addr, "00000000");    read_reg(COM1_base_addr, RDR_addr, read_val);    assert read_val(7 downto 0) = "01010101"        report "FAIL: got " & to_string(read_val)        severity error;

Из еще каких-то особенностей разработки стоит отметить еще одну аппаратную особенность платы: если выбрать нестандартный IRQ, один из входов ADC «повисает в воздухе» и обеспечить надежное детектирование такого варианта расположения перемычек оказалось не так уж и тривиально. Пришлось вспомнить матанализ и при определении такого положения перемычек определять среднее значение на входе и дисперсию. Для неподключенной ноги среднее значение должно заметно отличаться от нуля, при этом дисперсия сигнала не должна быть слишком высокой. Пришлось немного поэкспериментировать, печатая логи на входе ADC в eeprom, более простого способа извлечь данные из примененной атмеги придумать не удалось.

Несколько недель тестирования на реальном железе позволили исправить еще пару некритичных ошибок и подтвердили надежность созданной прошивки. Плата использовалась постоянно и проблем с ней не возникало.

К сожалению, в настоящее время PS/2 мыши тоже становятся редкостью. Современным стандартом является USB-мышь, а вот найти устройство с круглым шестиконтактным разъёмом Mini-DIN уже не так просто. Потому логичным развитием идеи стала разработка адаптера, который позволял бы подключать USB-мышь напрямую к ISA-шине.

В итоге родился следующий проект, доступный сейчас на github под названием usb-mouse-2-isa. За основу был взят тот же подход, что и у исходного: эмуляция последовательного порта, чтобы система видела её как обычную COM-мышь и работала со стандартными драйверами.

Новый адаптер устроен подобным образом: ISA часть и эмуляция части регистров последовательного порта реализована на EPM3064 почти без изменений, разве что вывод готовности приема данных приходит в чистом виде, а не как прерывание со всеми масками, зависящими от применяемого пользовательского ПО, что теоретически добавляет совместимости в сравнении с PS/2 вариантом. USB часть реализована на контроллере CH559T от китайской компании WCH, имеющий аппаратную поддержку USB-хоста. Эта часть в значительной мере была подсмотрена у проекта CH559_EasyUSBHost. Логика работы, впрочем, мало отличается от PS/2 проекта. Микроконтроллер принимает от USB-мыши HID-отчёты, содержащие информацию о перемещении по осям X и Y, нажатых кнопках и вращении колеса прокрутки. Затем он преобразует эти данные в формат, ожидаемый драйвером последовательной мыши и последовательно передаётся в CPLD.

Выделение битовых полей данных из пакета HID данных мыши:

map = &HIDdevice[hiddevice].mouse_map;report = RxBuffer;if (map->report_id != 0) {if (report[0] != map->report_id) {DEBUG_OUT("Wrong report ID: expected %d, got %d\n", map->report_id, report[0]);return;}report++;}if (len - (map->report_id?1:0) < (map->report_length_bits + 7) >> 3) {DEBUG_OUT("Report too short: got %d bytes, expected at least %d\n",len - (map->report_id?1:0), (map->report_length_bits + 7) >> 3);return;}if (map->buttons_bit_size > 0) {*buttons = (uint32_t)extract_field(report, map->buttons_bit_offset,map->buttons_bit_size, 0);}if (map->x_bit_size > 0) {*dx = extract_field(report, map->x_bit_offset,map->x_bit_size, 1);}if (map->y_bit_size > 0) {*dy = extract_field(report, map->y_bit_offset,map->y_bit_size, 1);}if (map->wheel_bit_size > 0) {*dwheel = extract_field(report, map->wheel_bit_offset,map->wheel_bit_size, 1);}

Основное отличие в CPLD части: контроллеру передается информация о готовности приема новых 7 бит состояния мыши напрямую:

    -- Комбинаторная логика    int_rx_irq <= RxD_IRQ; -- Передача сигнала внутреннего состояния Rx IRQ    mcu_DTR <= enable_mouse;    device_select <= '1' when (isa_aen = '0') and (device_rdy = '1') and                             (isa_addr(9 downto 3) = BASE_ADDR_ROM(to_integer(unsigned(base_addr_val)))) else '0';    isa_data <= data_out when (isa_reset = '0') and (device_select = '1') and (isa_ior = '0') else (others => 'Z');    enable_IRQ <= enable_mouse and mdm_ctl_reg(3);               -- OUT2 разрешает прерывания--    IRQ_state <= (RxD_IRQ and int_ena_reg(0)) or int_ena_reg(1); -- TxD_IRQ всегда выставлен    IRQ_state <= (RxD_IRQ and int_ena_reg(0));                   -- Игнорируем TxD_IRQ     -- OUT2 разрешает прерывания    IRQ4 <= '0' when (IRQ_state = '0') and (enable_IRQ = '1') and (base_addr_val(0) = '0') and                                                (use_opt_irq = '0') and (device_rdy = '1') else 'Z'; -- COM1/COM3    IRQ3 <= '0' when (IRQ_state = '0') and (enable_IRQ = '1') and (base_addr_val(0) = '1') and                                                (use_opt_irq = '0') and (device_rdy = '1') else 'Z'; -- COM2/COM4    IRQX <= '0' when (IRQ_state = '0') and (enable_IRQ = '1') and (use_opt_irq = '1') and                                                                        (device_rdy = '1') else 'Z'; -- Custom

Заключение

Адаптер проверен на самых разных системах: от xi8088 с частотой 4.77 мегагерц до Pentium III 1200. Работает с операционными системами MS-DOS с разными драйверами, Windows 3.11, Windows 95, 98, NT 4.0 и 2000 а также с Kolibri OS. Он не нагружает процессор задачами по обработке USB, вся эта работа ложится на встроенный микроконтроллер.

Исходные коды проектов открыты и доступны под лицензией GPL-3.0. В репозитории usb-mouse-2-isa можно найти прошивки для микроконтроллеров на C, исходники для CPLD на VHDL и тесты, а также схемы и печатные платы. Для сборки потребуется среда разработки Quartus версии 13 или старше для синтеза логики и утилита WCHISPTool для прошивки микроконтроллера.

Для тех, кто захочет повторить устройство, представлены все исходники, готовые прошивки, схемы, печатные платы и список компонентов. К сожалению, использованная CPLD уже не производится, но на Aliexpress их предостаточно и по не слишком высокой цене. Можно использовать альтернативу в виде выпускаемой поныне Atmel ATF1504AS, но при этом скомпилированная прошивка, вероятно не подойдёт, нужно будет перекомпилировать. Возможно, проект вдохновит кого-то на новые проекты в области ретроПК, ведь тема эта неисчерпаема и до сих пор интересна многим энтузиастам.

Упомянутые репозитории:

https://github.com/Yftul/ps-2-mouse-to-isa-replica

https://github.com/Yftul/usb-mouse-2-isa

ссылка на оригинал статьи https://habr.com/ru/articles/1043440/