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