Legacy Switch Compatibility Mode — что это? (сетевая карта)

Legacy Switch Compatibility Mode — активация устаревшего режима работы, при котором ограничивается максимальная пропускная способность до 10 / 100 Мбит/с.
Из официального описания Intel: устанавливает способ установления связи со скоростью 100 Мбит/с и 10 Мбит/с. Если параметр включен, сетевое соединение будет использовать устаревший принудительный метод установки связи связи. Это может помочь обеспечить соединение надлежащей скоростью и дуплексным режимом с некоторыми устаревшими коммутаторами. Когда отключено, сетевое соединение будет использовать автосогласование для установления соединения со скоростью 100 Мбит/с и 10 Мбит/с. ПРИМЕЧАНИЕ: изменение параметра может привести к кратковременной потере связи.
Иногда из-за этой опции становится недоступной скорость до 1 гбит/с. Например такая ситуация может быть при использовании адаптера Intel(R) Ethernet Connection I217-V. Решение — отключить Legacy Switch Compatibility Mode (выставить Disabled).
Из-за этой опции у людей часто скорость режется. Видимо функцию нужно активировать, если используются устаревшие устройства, которые не могут принимать много входящего трафика, например у них может возникнуть ошибка переполнения буфера.
Название функции переводится как режим совместимости для для устаревших коммутаторов.
Опция в дополнительных параметрах сетевой карты Интел:

Надеюсь данная информация оказалась полезной. Успехов.
Старый софт, LPT и современное железо
В своей прошлой статье я не был полностью честен. Перед тем, как получить рабочее устройство, я много раз проверял как мой код работает, перезаписывая его на многоразовую флеш AT28С64. И с самого начала знал что отлаживаться придется на железе, а потому встал вопрос программатора параллельных EEPROM.
Некогда крайне востребованные, а ныне необходимые только для редких специфических задач, эти программаторы стоят неприлично дорого (на этот раз серьезно). Есть бюджетные варианты, например собрать такой программатор на основе ардуины (но не весело) или быстро изобрести решение самому (но лень писать софт).
Однако, у отца оказался программатор Omega. На самом деле это не совсем программатор, это базовый блок на основе которого, теоретически, можно собрать множество разных устройств используя разные адаптеры, но один из адаптеров (имеющихся в наличии) — это универсальный программатор Orange. Но есть одна небольшая загвоздка: у меня современные компьютеры с Windows 10 и Windows 11, а этот программатор использует LPT. И нужно было как-то из этой ситуации выходить.
Эта статья о том, как можно заставить работать на новом компьтере старый софт и старое железо, рассчитанные на связь через LPT, при этом не прибегая к изменению ни оригинальных исполняемых файлов, ни схемотехники устройства. В статье речь будет идти о программаторе Omega-Orange и поставляемого к нему софту, но все описанное актуально и для других программ с другими устройствами.

Обзор возможных решений
Некогда популярный, а ныне забытый LPT — очень удобный для программиста параллельный порт. Тем не менее, все еще можно встретить его на материнских платах в том или ином виде. Сразу рассмотрим возможные варианты подключения устройства с LPT к современному компьютеру.
Реальный LPT порт
Несмотря на довольно долгую поддержку на аппаратном уровне, последнее время LPT на материнских платах или не распаивали вовсе, или распаивали только разьем для подключения, вместо распайки полноценного DB25. Сейчас же его поддержку вырезали на аппаратном уровне, но все еще существуют актуальные материнские платы где он присутствует.
- Реальный LPT, адаптация софта не требуется.
- Далеко не у всех есть, а дальше будет и того меньше.
- Большая часть программ предполагает программирование этого порта через прямое обращение к портам ввода вывода (инструкции IN/OUT), чего винды просто так сделать не дадут. А ставить сторонний драйвер (не подписанный или подписанный слитым сертификатом) не все могут, а многие справедливо откажутся.
Шнурок USB-LPT

На рынке есть много производителей, предлагающих такое решение. Однако я не смог найти нормальную документацию на используемые чипы.
- Дешево.
- USB есть у всех.
- Это не настоящий LPT-порт, а некая его абстракция, с которой можно взаимодействовать только через WINAPI, и то не совсем понятна функциональность. Похоже, существует исключительно для поддержки древних принтеров.
- Даже если в прошлом пункте я ошибся, и через WINAPI все же можно гибко шевелить таким виртуальным LPT — все еще необходима адаптация софта, потому что в пространстве IO он никак не будет отображен.
PCI-LPT адаптер
Активно существует и производится, как минимум китайской компанией WCH.
- Вполне себе реальный LPT-порт.
- В ноутбук, увы, PCI не воткнешь.
- Адреса ввода/вывода у такого порта будут сильно отличаться от стандартных (а в софте они, как правило, указаны жестко).
- Так же актуален вопрос с драйверами, которые откроют доступ к IO.
В итоге решение, позволяющее подключить старое устройство с портом LPT к новому железу попросту отсутствует. У некоторых производителей есть проприетарные решения, предполагающие обновленный софт, но это не портируемые решения существующие только в области промышленного оборудования (а они ОЧЕНЬ не любят обновлять железо, некоторые до сих пор используют компьютеры PDP!). Или ищи старый компьютер и ставь его рядом с новым, или изобретай свое. Конечно же, я решил изобрести свое.
Особый подход
Итак, взявшись решить проблему самостоятельно, попутно придется изобрести пару велосипедов. Для начала, надо определиться с требованиями к решению. Я составил следующие:
- Отсутствие потребности в особых драйверах.
- Отсутствие необходимости изменять оригинальную программу.
- Максимальная переносимость.
Может показаться что это слишком амбициозно для проблемы, которую еще никто почему-то не решил (или я не умею гуглить). Однако, план у меня есть.
Для начала надо разобраться с тем, что же такое LPT. Я начал свою практику когда LPT уже считался критически устаревшим, и тыкал его всего пару раз интереса ради, ограничиваясь записью в регистр 888. Но тут пришлось влезть в это дело глубже.
Что такое LPT
Это параллельный порт, претерпевший в ходе своей жизни несколько переработок. Оригинальный порт имел восемь линий данных (только вывод), пять линий статуса (только ввод) и четыре линии управления (только вывод). Еще у него было аж пять линий земли, но это не так важно.
Изначально предполагалось (обычно называется legacy или ISA), что это будет специальный порт для принтера. Собственно LPT — это Line Printer Terminal. Так как считывать с принтера нечего, то данные работали только на выход, линии статуса использовались для синхронизации и определения ошибок. Тем не менее, порт был настолько прост в программировании и удобен по своей структуре, что пользователи быстро начали создавать для него свои устройства, совершенно не похожие на принтеры. Но разработчики быстро столкнулись с нехваткой линий ввода, что делало считывание с устройств крайне неудобным.
Следующая версия (обычно называется BiDir или PS/2) была практически копией своего предшественника, но имела важное отличие: направление линий данных стало переключаемым, что позволило организовать очень удобную полудуплексную передачу данных. Однако, одна из проблем продолжающих существовать с прошлой версии: порт предполагал синхронизацию ввода/вывода, но не реализовывал ее аппаратно. А потому многие программисты игнорировали ее, полагаясь что скорость их кода сама собой будет синхронизацией, и в последствии, когда компьютеры стали быстрее, пользователи заимели много головной боли, пытаясь заставить работать свои устройства, которые теперь уже не успевали за компьютером. Нормальных решений проблеме отсутствия синхронизации не существовало, так что решали проблему чем могли, например использовали специальные программы замедляющие процессор, чтобы замедлить скорость работы IO.
Необходимость вручную считывать регистр статуса, проверять состояние отдельных бит, и в зависимости от них ждать дальше или править регистр управления была ключевой для грамотной работы LPT‑порта. Производитель решил избавить программистов от этого, и так появилась реализация LPT под названием EPP (Extended Parallel Port). Сохраняя полную совместимость с предыдущими версиями, он реализовал дополнительные регистры (адреса и данных), при записи и чтения из которых линии данных, статуса и управления переходили под контроль аппаратного обеспечения, автоматически выставляя нужную комбинацию для считывания и записи, и сами ожидали подтверждения готовности от ведомого. Это значительно упрощает работу, однако детальнее мы это рассмотрим позже.
В последствии была разработана еще одна версия LPT — ECP. Повысили скорость, добавили буферизацию, и вероятно что‑то еще. Однако, он меня совершенно не интересует на данный момент, потому что в документации к моему программатору сказано что он работает исключительно в режиме EPP.
И что с этим делать
Задачу можно разделить на два этапа:
- Заставить программу поверить, что у меня существует реальный LPT-порт, и она может с ним работать. Требуется программное решение.
- Заставить устройство поверить что программа взаимодействует с ним через LPT-порт. Требуется аппаратное решение.
Если кому‑то кажется странным, что я собираюсь заставить работать устаревшую программу — напомню, что у винды все очень хорошо с обратной совместимостью, а софт зачастую разрабатывался для Win9x/WinXP, и единственное что не дает ему нормально работать — это необходимость иметь доступ к пространству IO, где оно ожидает LPT‑порт.
Я принципиально не хочу патчить исходную программу, потому что крайне не люблю оставлять свои следы, которые могут в последствии самым неожиданным образом сказаться на работоспособности программы (я встречал программу, которая рассчитывала адрес функций, используя хеш‑сумму своего исполняемого файла). К тому же, пачинг сделает мое решение совершенно непереносимым. А значит, надо найти способ перехватывать обращения к IO не изменяя программу.
И несмотря на то, что программу изменять я не буду, никогда не лишним будет узнать что у нее внутри. По какой‑то причине разработчик выложил на сайте программу в зашифрованном виде, и ключ к архиву выдает исключительно по запросу. Мне не очень понятен этот ход, но раз уж он так решил — не буду выкладывать внутренности программы, ограничусь скриншотами и описаниями отдельных частей.

При запуске программа выдает ошибку загрузки драйвера. Что это за драйвер — можно догадаться по лежащему в папке с программой WinIo.sys. Это один множества драйверов, которые активно использовались для доступа к пространству IO в эпоху, когда подпись у драйвера была опциональной фичей. Работали они все одинаково: программа их загружала, потом отправляла запрос на доступ к портам, а драйвер ей этот доступ выдавал. В связи с особенностью устройства линейки Windows NT, права доступа к пространству IO одни на все запущенные программы, что не очень‑то и безопасно (как и загрузка стороннего драйвера). В Linux это реализовано проще и удобнее, но это другая история.
WinIo вместе с исходниками был доступен с сайта http://www.internals.com/ (а сейчас доступен через вебархив), и для программы представлял собой библиотеку с десятью функциями:
bool _stdcall InitializeWinIo(); void _stdcall ShutdownWinIo(); bool _stdcall InstallWinIoDriver(PSTR pszWinIoDriverPath, bool IsDemandLoaded); bool _stdcall RemoveWinIoDriver(); bool _stdcall GetPortVal(WORD wPortAddr, PDWORD pdwPortVal, BYTE bSize); bool _stdcall SetPortVal(WORD wPortAddr, DWORD dwPortVal, BYTE bSize); bool _stdcall GetPhysLong(PBYTE pbPhysAddr, PDWORD pdwPhysVal); bool _stdcall SetPhysLong(PBYTE pbPhysAddr, DWORD dwPhysVal); PBYTE _stdcall MapPhysToLin(PBYTE pbPhysAddr, DWORD dwPhysSize, HANDLE *pPhysicalMemoryHandle); bool _stdcall UnmapPhysicalMemory(HANDLE PhysicalMemoryHandle, PBYTE pbLinAddr);
Нас интересует InitializeWinIo , которая проверяет что драйвер запущен, и запускает его если он не запущен, и функции GetPortVal / SetPortVal , через который осуществляется доступ к портам.
Когда я увидел что WinIO предполагается в виде сторонней библиотеки — хотел порадоваться что все дело обойдется подменой dll. Однако, в данном случае используется статическая линковка.
Перехватить межмодульные вызовы несложно, и второй моей идеей было перехватывать обращения к драйверу через перехват вызова DeviceIoControl. Идея многообещающая, посмотрим что говорит документация к WinIO:
Place winio.dll, winio.vxd and winio.sys in the directory where your application’s executable file resides.
Add winio.lib to your project file by right clicking on the project name in the Visual C++ workview pane and selecting «Add Files to Project. «.
Add the #include «winio.h» statement to your source file.
Call InitializeWinIo.
Call the library’s functions to access I/O ports and physical memory.
Call ShutdownWinIo.
Тут все логично. Кидаем два файла драйвера (для NT и для Win9x) и библиотеку, инициализируем, используем функции для доступа к портам и памяти. Не знаю зачем честному человеку могло потребоваться использовать MapPhysToLin / UnmapPhysicalMemory / GetPhysLong / SetPhysLong , но прямой доступ к физической памяти затея в целом нездоровая и небезопасная (хотя и крайне веселая). Возможно, для любителей что-то рисовать на экране минуя графический драйвера винды.
Так же есть заметка относительно InitializeWinIo :
Under Windows NT/2000/XP, calling InitializeWinIo grants the application full access to the I/O address space. Following a call to this function, an application is free to use the _inp/_outp functions provided by the C run-time library to access I/O ports on the system.
И это уже куда менее веселая новость. Перехватывать исполнение инструкций не так просто, как перехватывать межмодульные вызовы. Нужно убедиться что разработчик использует вызовы к WinIO, вместо простого вызова _inp/_outp .
Впрочем, разочарование наступило когда присмотрелся к самим функциями GetPortVal / SetPortVal . Они проверяли версию системы, и если система была NT — то тоже использовали прямой вызов _inp/_outp .
bool IsWinNT()
Конечно, можно было бы перехватить и GetVersionEx и подменить значение, но это уже совершенно неспортивно. К тому же, программа вызывает GetVersionEx много раз, и подмена всех значений могла привести к неопределенным последствиям. Альтернативно — можно закладываться на адрес возврата, и относительно него определять какое значение необходимо вернуть. Но мне такая идея совершенно не понравилась.
Перехват вызовов от программы
Итак, нам нужно перехватить межмодульные вызовы чтобы программа запустилась, а затем перехватить выполнение инструкций IN/OUT чтобы эмулировать LPT.
Первая часть работы тривиальна: достаточно использовать один из множества способов перехвата межмодульных вызовов. Например, использовать библиотеку detours от самих майкрософт. Что приятно, с момента моего последнего использования этой библиотеки прошло много времени, и она успела стать опенсорсной https://github.com/microsoft/Detours.
Итак, что мы делаем:

- Создаем Dll, которая будет выполнять перехват
- Создаем лаунчер, который запустит процесс и встроит в него мою DLL
- Внутри Dll перехватываются вызовы CreateFile, подменяя хендл создаваемый для \\.\WINIO
- Так же перехватываем вызов DeviceIoControl с обращением к хендлу созданному в прошлом шаге, имитируя наличие драйвера
В целом, все просто. Не вижу смысла углубляться в детали, их можно посмотреть в исходниках. После того как я имитировал положительный ответ от драйвера, и приложение запустилось — оказалось что приложение не может обнаружить LPT (в основном потому что его и правда нет). Как оно это делает? Чтобы это понять я воспользовался API Monitor от rohitab. Изначально ожидалось что программа обращается к SetupAPI, но оказалось что оно проверяет реестр в разделе HKLM\HARDWARE\DEVICEMAP\PARALLEL PORTS, пытаясь вычитать оттуда список LPT-портов. Причем программе не так важно что именно она там найдет. Она честно пытается распарсить найденное значение, но на практике оказалось что в паре ключ-значение shit=pants успешно обнаруживается наличие LPT0. Конечно, можно было бы создать какое-то такое значение на работающей системе, но некрасиво будет оставлять такие артефакты, к тому же я уже вошел во вкус при перехвате вызовов, так что и проблему решим перехватом.
- RegOpenKeyExA ( HKEY_LOCAL_MACHINE, «HARDWARE\DEVICEMAP\PARALLEL PORTS», . )
- RegEnumValueA от предыдущего хендла
- RegQueryValueExA с запросом на возвращенное во втором шаге значение
Теперь программа запускается. На этом заканчивается специфическая для моей программы часть, все описанное далее применимо к любой программе работающей с LPT.
Перехват инструкций IN/OUT через AddVectoredExceptionHandler
Можно отлавливать системные исключения при выполнении инструкций чтения портов и обрабатывать их, это элегантно и делается довольно просто, через регистрацию своего обработчика с помощью AddVectoredExceptionHandler и написания в нем простенького декодера инструкций. Конечно же, через удаленный поток или в той же встроенной Dll. Это не так сложно, тем более что инструкций всего 12 (чтение и запись, по три (8, 16, 32) на прямое указание порта и на указание порта через DX). В теории, тут всегда должна быть запись/чтение по 8, но мало ли. К тому же, эти инструкции не обновляют флагов. Однако, есть и минусы:
The handler should not call functions that acquire synchronization objects or allocate memory, because this can cause problems. Typically, the handler will simply access the exception record and return.
Технически, это предупреждение может ничего и не значить, но на практике именно из за игнорирования таких предупреждений и возникают проблемы с переносимостью.
Перехват инструкций IN/OUT отладчиком
Альтернативный вариант — запустить процесс в режиме отладки, а затем отлавливать исключения EXCEPTION_PRIV_INSTRUCTION. Когда такое исключение получено — определить какая именно инструкция ее вызвала, и имитировать ее исполнение, изменив по необходимости значения регистров данных и исправив EIP. Чтобы запустить процесс в режиме отладки достаточно создать его с флагом DEBUG_PROCESS, а затем ловить уведомления от него через WaitForDebugEvent, обрабатывать их, и возвращать управление через ContinueDebugEvent.
while (process_alive && WaitForDebugEvent(&de, INFINITE)) < DWORD continue_type = DBG_CONTINUE; switch (de.dwDebugEventCode) < case CREATE_PROCESS_DEBUG_EVENT: < CloseHandle(de.u.CreateProcessInfo.hFile); >break; case LOAD_DLL_DEBUG_EVENT: < CloseHandle(de.u.LoadDll.hFile); >break; case EXCEPTION_DEBUG_EVENT: < switch (de.u.Exception.ExceptionRecord.ExceptionCode) < case EXCEPTION_PRIV_INSTRUCTION: < HANDLE thread = OpenThread(THREAD_ALL_ACCESS, FALSE, de.dwThreadId); if (!process_io_exception(pi.hProcess, thread, de.u.Exception.ExceptionRecord.ExceptionAddress)) < continue_type = DBG_EXCEPTION_NOT_HANDLED; >> break; default: < continue_type = DBG_EXCEPTION_NOT_HANDLED; >break; > > break; case EXIT_PROCESS_DEBUG_EVENT: < process_alive = false; >break; default: < continue_type = DBG_EXCEPTION_NOT_HANDLED; >break; > ContinueDebugEvent(de.dwProcessId, de.dwThreadId, continue_type); >
Тут вызывается функция process_io_exception, которая определяет что именно за инструкция вызвала исключение, и если это наша ожидаемая IN/OUT — обрабатывает ее. Если что‑то неожиданное — не обрабатывает.
Так как перехватываемых инструкций не так много — я написал небольшой дизассемблер. Звучит громко, хотя было реализовано лишь это:
#define FILL_INSTRUCTION_DATA(_instruction_sz, _port, _io_size, _out_direction) \ #define CHECK_OPERATION_2B(byte0, byte1, _instruction_sz, _port, _io_size, _out_direction) \ if(instr_ptr[0] == (byte0) && instr_ptr[1] == (byte1)) FILL_INSTRUCTION_DATA(_instruction_sz, _port, _io_size, _out_direction) #define CHECK_OPERATION_1B(byte0, _instruction_sz, _port, _io_size, _out_direction) \ if(instr_ptr[0] == (byte0)) FILL_INSTRUCTION_DATA(_instruction_sz, _port, _io_size, _out_direction) bool process_io_exception(HANDLE process, HANDLE thread, void* exception_address) < uint8_t instr_ptr[16]; //bytes readed from exception ptr uint8_t instruction_sz; //instruction length, bytes uint16_t port; //port number uint8_t io_size; //io data size, bits bool out_direction; //1 if OUT, 0 if IN uint32_t edx = 0; //ExceptionInfo->ContextRecord->Edx uint32_t eax = 0; //ExceptionInfo->ContextRecord->Eax CONTEXT threadContext = < .ContextFlags = WOW64_CONTEXT_INTEGER | WOW64_CONTEXT_CONTROL >; // SIZE_T readed; if (!ReadProcessMemory(process, exception_address, instr_ptr, sizeof(instr_ptr), &readed) || readed != sizeof(instr_ptr)) < return false; >if (!GetThreadContext(thread, &threadContext)) < return false; >edx = threadContext.Edx; eax = threadContext.Eax; CHECK_OPERATION_2B(0x66, 0xE5, 3, instr_ptr[2], 16, false) //IN 16 indirect else CHECK_OPERATION_2B(0x66, 0xED, 2, edx & 0xFFFF, 16, false) //IN 16 DX else CHECK_OPERATION_2B(0x66, 0xE7, 3, instr_ptr[2], 16, true) //OUT 16 indirect else CHECK_OPERATION_2B(0x66, 0xEF, 2, edx & 0xFFFF, 16, true) //OUT 16 DX else CHECK_OPERATION_1B(0xE4, 2, instr_ptr[1], 8, false) //IN 8 indirect else CHECK_OPERATION_1B(0xE5, 2, instr_ptr[1], 32, false) //IN 32 indirect else CHECK_OPERATION_1B(0xEC, 1, edx & 0xFFFF, 8, false) //IN 8 DX else CHECK_OPERATION_1B(0xED, 1, edx & 0xFFFF, 32, false) //IN 32 DX else CHECK_OPERATION_1B(0xE6, 2, instr_ptr[1], 8, true) //OUT 8 indirect else CHECK_OPERATION_1B(0xE7, 2, instr_ptr[1], 32, true) //OUT 32 indirect else CHECK_OPERATION_1B(0xEE, 1, edx & 0xFFFF, 8, true) //OUT 8 DX else CHECK_OPERATION_1B(0xEF, 1, edx & 0xFFFF, 32, true) //OUT 32 DX else < return false; >// threadContext.Eip += instruction_sz; //move EIP +n bytes threadContext.Eax = eax; if (!SetThreadContext(thread, &threadContext)) < return false; >>
Тут можно задать вопрос: а как же INS/OUTS? И уж тем более REP INS/OUTS? А никак. Добавить их поддержку можно, но не очень-то и нужно, так что пока обойдемся без них. Следующий вопрос — зачем я отлавливаю передачи размером в 16 и 32 байта, если реально обрабатываю только восьмибитные? Для полноты картины и для упрощения расширения функционала в дальнейшем.
Тем не менее, инструкции перехватываются, декомпилируются, программа считает что они работают, а это уже победа. Для начала просто игнорировались инструкции записи, а при чтении всегда возвращался ноль. Программа запустилась! «Находит» в реестре запись о порте LPT, а затем исправно начинает мучать порты IO (которые на этом этапе у меня просто логгировались в файл) и не получая от устройства ответа выдает ошибку. Заглянув в полученный файл с логом, я увидел кучу ожидаемых обращений к портам 0x37A/0x378/0x379, но помимо них так же заметил запись в порт 0x77A. О таком я услышал впервые, и погуглив, а нагуглить сейчас информацию о настолько устаревших технологиях непросто, обнаружил что этот регистр никак не упоминается в большинстве списков. Например, в списке портов реализованных в BOCHS. И это только добавило вопросов. Благо вспомнил о существовании Ralf brown interrupt list, который в своем оригинальном виде включает так же и список портов. И он говорит следующее: PORT 0778-077A — Intel 82091AA — ECP-mode PARALLEL PORT . Я плохо представляю себе тонкости работы с железом тех лет, и не уверен совместимы ли реализации ECP разных производителей, но все дальшенаписанное будет основано на документации к 82091AA. Порт 0x077A — конфигурационный порт LPT-ECP, и туда пишется значение 0x20. Значение 0x20 настраивает DATA на вход. Почему это делается именно там — не знаю, но могу предположить что это фикс для специфичной ошибки какого-то чипсета. А значит — можно просто игнорировать обращения к этому порту.
Осталось не так много — реализовать аппаратную часть.
Аппаратная часть
Что такое LPT-порт и какой он бывает уже определились. И как уже упомянул — готовые решения в виде USB-LPT шнурков не предоставляют достаточно документации, чтобы использовать их для эмуляции LPT-порта. Так что реализую свою версию.
Для реализации я выбрал микроконтроллер ATMEGA8. Почему ее? Она пятивольтовая (а LPT использует напряжения TTL, примерно 3-5 вольт), есть удобная программная реализация USB, выпускается в DIP (удобно для пайки прототипов), доступна, я умею ею пользоваться и главное — она у меня есть. Для прототипа самое то, а если по какой-то причине понадобиться еще — можно будет оперативно все это портировать на нормальный контроллер с аппаратным усб и ценой в два десятка центов, либо переразвести с ней же, но в более компактном и дешевом корпусе.
Схема получилась такой:

Тут самый минимум всего того, что может быть нужно. Я даже не стал добавлять защитных резисторов на выход LPT. Оригинальные LPT их зачастую не имели, и с удовольствием дохли от любого слегка завышенного тока, так что будем считать это дополнительным уровнем совместимости. Все необходимые для программирования пины, за исключением RESET, я использовал на разъеме, а значит для перепрошивки микроконтроллера достаточно вытащить наружу RESET, а все остальное можно взять с разъема.

Получилось сделать настолько уродливо, насколько и планировалось. В принципе, можно сделать кастомную плату, которая поместится в корпус, на край которой будет напаян LPT, а по сторонам расположены все те же детальки в микроскопических корпусах. Будет очень красиво, удобно для распайки и даже дешевле. Однако это все идеализм который требует много времени, а это всего лишь прототип.

Как известно, залог хорошего продукта — удобные средства разработки. Так что для перепрошивки контроллера я собрал вот такой адаптер под стандартный разьем AVR-ISP.


Осталось малое — написать прошивку, и для этого нужно:
- Поднять USB — делается очень просто с помощью V-USB
- Реализовать интерфейс LPT
- Заставить работать без драйверов
Первый пункт очень прост — достаточно скопировать исходники в свой проект и USB поднимется.
Для реализации LPT надо разобраться как программировали оригинальный LPT.
Версии ISA и PS/2 имели всего три регистра:
- base+0: PDATA — регистр порта данных, полудуплексная шина. У PS/2 направление определяется битом в PCON.5.
- base+1: PSTAT — регистр статуса, содержит статус LPT порта и его линий. Можно вычитать: состояние линий BUSY, ACK#, PERROR, SELECT и FAULT#.
- base+2: PCON — регистр настройки порта и управляющими линиями. У PS/2 настраивает направление PDATA, прерывания от LPT порта, линии SELECTIN#, INIT#, AUTOFD# и STROBE#.
У EPP есть еще два регистра:
- base+3: ADDSTR — регистр, при записи/чтения данных в который генерируется последовательность передачи адреса.
- base+4: DATASTR — четыре регистра с общим названием при записи/чтения в которые генерируется последовательность передачи данных.
Теоретически, EPP имеет сразу четыре регистра DATASTR, но практически определен только первый из них, а функциональность оставшихся трех зависит от реализации. Будем считать что в моей реализации их нет. А если регистры ADDSTR/DATASTR не использовать — то работать EPP будет точно как версия PS/2.
Сразу нужно заметить что некоторые линии инвертированы, а именно:
- PDATA — при чтении и записи использует прямую трансляцию, то есть 1 в регистре соответствует 1 на лини.
- PSTAT — ACK#, PERROR, SELECT, FAULT# — используют прямую трансляцию, а BUSY — обратную
- PCON — STROBE#, AUTOFD#, SELECTIN# — используют обратную трансляцию, а INIT# — прямую.
Этого достаточно чтобы реализовать LPT. Я начал с того, что реализовал только режимы Legacy и PS/2. Не смотря на документацию к программатору он может работать не только с EPP, но так же и в режиме PS/2, так что для проверки работоспособности идеи этого будет достаточно.
Для усб-устройства (которое я гордо назвал AVRUSBLPT, или кратко AVRLPT) достаточно следующих команд:
- AvrLpt_SetMode — выбор режима совместимости.
- AvrLpt_SetReg — запись в указанный регистр.
- AvrLpt_GetReg — чтение из указанного регистра. Все остальные команды, если они и будут — служебные (вроде AvrLpt_GetVersion)
Для обмена данными с AVRLPT я выбрал USB vendor control transfer. Углубляться не буду, скажу только что это наиболее простой способ реализации передачи сообщения по USB.
Следующая задача — отсутствие необходимости в драйвере. Решается элементарно, достаточно использовать драйвер WinUSB, который винда подгрузит автоматически если обнаружит на устройстве особый дескриптор под названием WCID. Это нестандартное расширение интерфейса USB от майкрософт, позволяющее винде использовать универсальный драйвер для устройства без необходимости согласовывать это с пользователем. Очень хорошо и подробно WCID описан тут: https://github.com/pbatard/libwdi/wiki/WCID‑Devices

Замечу следующее: WinUSB условно поддерживается начиная с Windows XP SP2 (хотя если у вас Windows XP — то и LPT наверняка есть), нормально поддерживается начиная с windows 8. За основу для своего устройства я использовал код отсюда: https://github.com/mariusgreuel/USBasp/
В итоге получилось устройство которое могло вести себя как LPT, подключается по USB, не требует драйвера и может работать в режимах Legacy и PS/2. Как ни странно, все заработало и даже фирменный софт смог увидеть программатор! Прогрмамматор периодически чудил, а когда не чудил то просто крайне медленно работал. Я не знаю что в этом обвинить как не проблемы с синхронизацией, вызванные использованием нестабильных задержек. Нужно было реализовать EPP.
Итак, EPP. Все довольно просто. При записи в регистр ADDSTR/DATASTR происходит следующее:
- Хост выставляет Write# в 0
- Хост выставляет данные на шину DATA
- Хост выставляет data strobe# опускается в 0 (если это DATASTR) или addr strobe# в 0 (если это ADDSTR)
- Устройство выставляет Wait# в 1
- Хост выставляет data strobe# в 1 (если это DATASTR) или addr strobe# в 1 (если это ADDSTR)
- Хост выставляет Write# в 1
- Устройство выставляет Wait# в 0
Увы, разные производители документируют эту последовательность по разному. Ниже две последовательности: из документации Intel и из документации National Semiconductor. Разница ощутимая, но я предпочел второй алгоритм.


Все бы хорошо и понятно, но остаются вопросы о таймингах и таймаутах. Совершенно не ясно какие задержки должны быть реализованы, через какое время считать что данные не переданы, как сигнализировать об ошибке и как обрабатывать ситуацию если в начале передачи Wait# изначально высокий. В интернете есть упоминания таймаутов в 5, 10 или 15 мкс, возьму за основу 10. Так же вызывает вопрос предварительная инициализация линий порта. Судя по всему, это зависит от реализации. Так же кое‑где упоминается что младший бит PSTAT может быть флагом таймаута, но в документации на 82 091 этого нет. Пришлось откопать документацию на PC87 338 (другая реализация SuperIO) и посмотреть там. Там он описан так: действует только при EPP, в нормальном режиме 0, если произошел таймаут — устанавливается в 1, и сбрасывается при чтении. Так и реализую.
После реализации EPP глюки пропали, а скорость работы значительно выросла, хотя и осталось далекой от ожидаемой.
Первая мысль — буферизировать операции вывода, но на практике большая часть обращений к портам это поллинг регистра PSTAT, а операции записи идущие друг за другом последовательно — явление крайне редкое.
Ускоряем работу перехвата IN/OUT
Тест показал что обработка одного исключения, не считая обращения к AVRLPT занимает примерно 101 мкс, а вместе с обращением — уже около 381 мкс. То есть если исключить из этого время обработки прерываний — то каждое обращение к порту будет занимать около 280 мкс. Все еще много, но уже лучше. А при условии что такие обращения происходят тысячами — выигрыш во времени (на треть быстрее!) уже заметный.
Как я говорил, патчить исполняемый файл очень сильно не хочется — это не только лишает решение портируемости (на потенциальные новые версии этой же программы) и универсальности (на другие программы), но и чрезвычайно скучно.
Другое дело — патчить сразу в памяти. Отлавливаем обращение к IO, проверяем размер, создаем процессу‑жертве новый клок памяти, куда копируем нужный код и по адресу где произошло исключение вставляем заплатку, вызывающую мой патч. Так как вызов по 32-х битному адресу занимает 5 байт, а инструкция чтения/записи не более 3 — придется еще и часть инструкций переносить.
Финальный штрих, патчинг программы прямо в памяти. План дествий такой:
- В Dll, которая подгружается в процесс, добавляем функции работы с AVRLPT
- После подгрузки этой Dll получаем адреса искомых функций
- После запуска процесса ждем исключений UNPRIVILEDGED INSTRUCTION
- По адресу исключения определяем точный тип инструкции IN/OUT, вырезаем ее
- Дизассемблером длин определяем длину следующей инструкции, повторяем пока освободившегося места не хватит для LONG JMP
- Все вырезанные инструкции копируем в буфер
- Добавляем в этот же буфер код обработки IO
- Добавляем в этот же буфер адреса функций работы с AVRLPT
- Добавляем в этот же буфер LONG JMP обратно на место исключения + длина LONG JMP
- Буфер внедряем в адресное пространство обрабатываемого процесса
- На месте с вырезанными инструкциями добавляем LONG JMP на ранее подготовленный буфер, при необходимости дополняем инструкциями NOP
Дизассемблер длин был использован этот: https://github.com/greenbender/lend, но немного доработан для предотвращения выхода за пределы буфера.
В конце концов нашелся плюс от того что EIP нельзя использовать как адресный регистр. Однако, если вызов был в конце одной функции, сразу за которой начинается другая, то такой патч испортит вызов следующей функции. Будем надеяться что такое происходит не слишком часто, потому что однозначного способа предотвратить это я не вижу. Аналогичная проблема возникнет если выше будет осуществляться условный переход на адреса следующие прямо за вызовом IN/OUT. Теоретически, можно реализовать алгоритм поиска свободного места и адаптивным патчингом, но это огромный пласт работы который я делать не хочу. Остается лишь надеяться что применимо к программе программатора этот метод будет работать не создавая ошибок, но всегда можно откатиться на версию осуществляющую перехват без патчинга.
Может показаться что логичнее было бы не использовать два LONG JMP и уникальный кусок кода для каждого вызова, а ограничится CALL и универсальными функциями для ввода и для вывода. Однако, мы копируем себе следующую за IN/OUT инструкцию, и это вполне может оказаться инструкция работы со стеком, так что стоит оставить стек в том виде, в котором он ожидается.
Это решение замедлит обработку первого исключения по каждому из адресов, но в последствии IO будет работать быстрее. Можно, конечно, представить себе синтетическую ситуацию, где обрабатываемый процесс постоянно создает новые адресные пространства с вызовами IO, и тогда получится что мы генерируем код для генерированного кода, что приведет к неконтролируемому нарастанию потребления памяти в целевом процессе.
Инструкцию LONG JMP можно осуществить только со сменой страницы, и мне не хочется вникать в вопрос всегда ли винда использует для кода одну и ту же страницу, так что эту инструкцию отбрасываем. Зато в те же шесть байт помещается PUSH DWORD+RET, что по сути тот же LONG JMP, но без смены страницы. Его и использовал.
После реализации метода с патчингом в памяти скорость обмена данными увеличилась и программу теперь использовать довольно комфортно. Однако скорость выросла не так сильно как хотелось бы, и зависания интерфейса остались, так как разработчик использует один поток на все операции. В моем случае скорость программы ограничивается искусственными (и сильно завышенными) задержками, которые используются при обращении к микросхеме памяти.
Патчинг в памяти, как бы он ни был хорош в моем случае, может сломать программу в прочих случаях. А потому я на всякий случай оставил возможность собрать инжектор в режиме обработки при перехвате — это определяется флагом препроцессора MEMORY_PATCH_MODE .
Вывод: адаптировать таким образом старые приложения под новые реалии можно, но скорость обмена данными страдает. И это может быть фатально в тех случаях, когда разработчик не реализовал синхронизацию (а это, увы, случается, хотя и не мой случай). Приборы, рассчитывающие на скорость обмена данных, вроде логических анализаторов втыкаемых в LPT работать не будут.
Вывод
Используя описанный метод можно подключить устройство рассчитанное на работу с LPT-портом используя LPT-USB адаптер, а мой софт сможет перехватить «сырые» обращения к LPT и перенаправить их через адаптер. Скорость работы немного пострадает, но это не должно быть критично в большинстве случаев. Да, большинству устаревших устройств можно дать вторую жизнь. А если очень хочется — то можно даже снова что-то разработать под LPT.
Послесловие
- VID/PID у моего USBLPT используются нелегально, запрос на их выделение отправлю в ближайшем будущем.
- Мое решение может потребовать незначительной доработки (ECP, INS/OUTS, REP INS/OUTS) для использования с другими программами, использующими LPT. Я мог бы заняться и этим, но для решения моей задачи это излишне.
- В статье, на схеме и в прошивке может наблюдаться некоторая неразбериха в названиях выводов LPT‑порта, связанная с тем, что разные версии имеют разные названия, и некоторые названия хоть и не официальные — но прижившиеся. В разных случаях я использую разные названия, но на схеме устройства есть табличка проясняющая этот момент.
- Если обнаружится что какой‑то из многочисленных USB‑LPT адаптеров позволяет реализовать все то, что я реализовал через AVRLPT — один из велосипедов можно будет исключить, оставив лишь самое важное — пехеват и перенаправление.
Элементы сетевого взаимодействия в Unity
Встроенные в Юнити способы работы с сетями поддерживают всё, описанное на предыдущей странице. Создание серверов и подключение клиентов, обмен данными между подключенными клиентами, определение какой игрок управляет какими объектами, доступ через различные конфигурации сети — всё это поддерживается сразу после установки Юнити. На этой странице мы рассмотрим реализацию в Юнити этих сетевых задач.
Создание сервера
Перед тем как вы сможете начать играть в сетевую игру, вам необходимо определить другие компьютеры, с которым вы будете обмениваться данными. Чтобы это сделать, необходимо создать сервер. Это может быть как машина, на которой запущена игра, так и отдельная выделенная машина, не принимающая участия в игре. Чтобы создать сервер, просто вызовите Network.InitializeServer() в скрипте. Если вы хотите подсоединться к уже существующему серверу как клиент, вызывайте Network.Connect().
В общем, вам может быть очень полезно ознакомиться со всем классом Network class.
Связь с использованием компонентов Network View
Network View (просмотр сети) это компонент, который отправляет данные через сеть. Компонент Network View даёт вашим объектам GameObject возможность отправлять данные, используя удаленный вызов процедур RPC или синхронизацию состояний State Synchronization. Способ, которым вы используете Network View будет определять, как будут работать сетевые взаимодействия вашей игры. Network View имеют несколько вариантов, но все они необычайно важны для ваших сетевых игр.
Для большей информации об использовании Network View изучите Network View Guide page и Component Reference page.
Удаленные вызовы процедур (Remote Procedure CAlls, RPC)
Удаленные вызовы процедур (Remote Procedure Calls, RPC) это функции, объявленные в скриптах, прикрепленных к GameObject, который содержит NetworkView. Network View должен указывать на скрипт, содержащий RPC функцию. После этого, RPC функция может быть вызвана из любого скрипта в этом GAmeObject.
Для большей информации об использовании RPC в Юнити, изучите RPC Details page.
Синхронизация состояний (State Synchronization)
Синхронизация состояний это постоянный обмен данными между всеми клиентами игры. Таким способом позиция игрока может быть синхронизирована со всем клиентами, так что будет казаться, что он управляется локально, когда данные в действительности доставляются через сеть. Для синхронизации состояний внутри объекта GameObject вам просто надо добавить компонент NetworkView на этот объет и объяснить ему, за чем наблюдать. Наблюдаемые данные после этого синхронизируются со всеми клиентами в игре.
Для большей информации об использовании синхронизации состояний в Юнити, изучите State Synchronization page.
Network.Instantiate()
Network.Instantiate() позволяет вам создавать экземпляры префабов на всех клиентах естественно и просто. По сути, это вызов функции Instantiate() , но он выполняет создание экземпляров на всех клиентах.
Внутренне Network.Instantiate это простой вызов RPC, который выполняется на всех клиентах (также локально). Он распределяет NetworkViewID и назначает его созданной копии префаба, что гарантирует его правильную синхронизацию среди всех клиентов.
Для большей информации изучите страницу Network Instantiate.
NetworkLevelLoad()
Работа с обменом данными, состоянием клиентов игроков и загрузкой уровней может быть слишком большой. На странице Network Level Load вы найдёте полезный пример для решения этой задачи.
Master Server
Master Server (Управляющий сервер) помогает вам подбирать игры. При запуске сервера, вы подключаетесь к master server, и он предоставляет вам список всех активных серверов.
Master Server это место встречи для серверов и клиентов, где афишируются серверы и совместимые клиенты подключаются к запущенным играм. Это снимает необходимость заботиться об IP адресах для всех сторон. Это даже может помочь пользователям хостить игры без необходимости возиться с их маршрутизаторами, что требовалось бы при обычных обстоятельствах. Это может помочь клиентам пройти через брандмауэр сервера и добраться до частных IP адресов, обычно недоступных из публичного интернета. Это делается с помощью facilitator, который способствует установлению соединения.
Для большей информации изучите Master Server page.
Минимизация сетевого трафика
Важно использовать минимальный объём сетевого трафика, достаточный для корректной работы игры. В вашем распоряжении различные способы передачи данных, различные методы, определяющие что или когда пересылать и прочие ухищрения.
Для получения советов по уменьшению сетевого трафика, изучите Minimizing Bandwith page.
Отладка сетевых игр
Юнити поставляется с несколькими вспомогательными инструментами, которые помогут вам отладить вашу сетевую игру.
- Network Manager (Менеджер сетей) может быть использован для документирования всего входящего и исходящего сетевого трафика.
- Используя окна инспектора и иерархии вы можете отслеживать создание объектов, проверять сетевые идентификаторы и т.д.
- Вы можете запустить Юнити дважды на одной машине и открыть разные проекты в каждой программе. Для Windows это может быть сделано простым запуском другого экземпляра Юнити и открытием проекта из мастера проектов. Для Mac OS X, несколько экземпляров Юнити могут быть открыты из терминала и аргумент -projectPath может быть определен как: /Applications/Unity/Unity.app/Contents/MacOS/Unity -projectPath “/Users/MyUser/MyProjectFolder/” /Applications/Unity/Unity.app/Contents/MacOS/Unity -projectPath “/Users/MyUser/MyOtherProjectFolder/”
Убедитесь, что проигрыватель работает в фоновом режиме во время отладки сети потому что, если, например, два экземпляра запущены одновременно, один из них не будет активным. Это разорвет цикл сетевого взаимодействия и может вызвать непредсказуемые последствия. Вы можете включить этот параметр в Edit->Project Settings->Player в редакторе или при помощи Application.runInBackground
Legacy Game Compatibility Mode от MSI позволяет запускать любые игры на ПК с ЦП Intel Alder Lake
Стало известно, что процессоры Intel Alder Lake часто «конфликтуют» с играми, которые защищены системой от Denuvo. В «проблемном списке» 51 проект (например, Madden 22, Total War: Warhammer, FIFA 20, Ghost Recon Wildlands, Just Cause 4, Shadow of the Tomb Raider).

Главная / Новости Hardware / Legacy Game Compatibility Mode от MSI позволяет запускать любые игры на ПК с ЦП Intel Alder Lake
Стало известно, что процессоры Intel Alder Lake часто «конфликтуют» с играми, которые защищены системой от Denuvo. В «проблемном списке» 51 проект (например, Madden 22, Total War: Warhammer, FIFA 20, Ghost Recon Wildlands, Just Cause 4, Shadow of the Tomb Raider).
Дело в том, что ПО DRM (Digital Rights Management) воспринимает гибридную архитектуру ЦП Alder Lake в качестве двух независимых ПК, состоящих из Performance ядер (P-Core) и Efficiency ядер (E-Core). К слову, у этих ядер и впрямь разная архитектура (Golden Cove и Gracemont).

Компания MSI нашла решение. Оно называется «Legacy Game Compatibility mode». Функция включается в BIOS. Пока профильные обновления (бета-версия) доступны для следующих платформ:
- MSI MEG Z690 ACE BETA BIOS (107)
- MSI MEG Z690 Unify БЕТА BIOS (112)
- MSI MPG Z690 CARBON BETA BIOS (114)
- MSI MPG Z690 EDGE DDR4 БЕТА BIOS (112)
- MSI MAG Z690 Tomahawk БЕТА BIOS (H11)
- MSI MAG Z690 Torpedo DDR4 BETA BIOS (A05)
- MSI MAG Z690 Torpedo БЕТА BIOS (A11)
- MSI PRO Z690-A DDR4 BETA BIOS (113)