Сторінка 1 з 1

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

Додано: 04 червня 2026, 20:05
Babasha
Проект не мой, но краями коснулся.

https://habr.com/ru/articles/1043440/

В ретрокомпьютерной технике зачастую возникают задачи, обратные актуальным сегодня. Если обычно мы часто сталкиваемся с проблемами, пытаясь запустить старые программы на новом оборудовании, то в ретровании проблемы проявляются куда как чаще и разнообразнее, к примеру как заставить современную периферию работать с машиной тридцати- сорокалетней давности. И если с подключением клавиатуры к старому ПК обычно сложностей немного: старые 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') then
data_out <= (others => '0');
case isa_addr(2 downto 0) is
when "000" => -- Регистр данных
if sig_DLAB = '0' then -- !DLAB check
data_out <= "0" & rx_data_reg; -- Прочитали данные UART
else
data_out <= gen_reg;
end if;
when "001" => -- Регистр разрешения прерывания
if sig_DLAB = '0' then -- !DLAB check
data_out <= "0000" & int_ena_reg;
else
data_out <= gen_reg;
end if;
when "010" => -- причина прерывания: xxxxx10x = принят символ; сбрасывается чтением приемника
if RxD_IRQ = '1' then -- Прерывание готовности принятого символа
data_out <= "00000100"; -- Сигнализация готовности принятого символа
else
if int_ena_reg(1) = '1' then -- Прерывание готовности передачи символа
data_out <= "00000010"; -- Сигнализация готовности передачи символа
else
data_out <= "00000001"; -- Нет прерываний для обработки
end if;
end if;
when "011" => -- Line control register
data_out <= sig_DLAB & gen_reg(6 downto 0);
when "100" => -- Modem control register
data_out <= "000" & mdm_ctl_reg;
when "101" => -- Line status register
data_out <= "0110000" & RxD_IRQ;
when "110" => -- Modem status register
data_out <= "00" & mdm_ctl_reg(0) & mdm_ctl_reg(1) & "0000"; -- CTS = RTS , DSR = DTR
when 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