В ретрокомпьютерной технике зачастую возникают задачи, обратные актуальным сегодня. Если обычно мы часто сталкиваемся с проблемами, пытаясь запустить старые программы на новом оборудовании, то в ретровании проблемы проявляются куда как чаще и разнообразнее, к примеру как заставить современную периферию работать с машиной тридцати- сорокалетней давности. И если с подключением клавиатуры к старому ПК обычно сложностей немного: старые 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://habr.com/ru/articles/1043440/