惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cyberwarzone
Cyberwarzone
V
Vulnerabilities – Threatpost
T
Tenable Blog
Forbes - Security
Forbes - Security
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog
G
GRAHAM CLULEY
Know Your Adversary
Know Your Adversary
S
Securelist
C
Cybersecurity and Infrastructure Security Agency CISA
Project Zero
Project Zero
C
CXSECURITY Database RSS Feed - CXSecurity.com
V
Visual Studio Blog
WordPress大学
WordPress大学
Latest news
Latest news
K
Kaspersky official blog
T
Tailwind CSS Blog
T
Threat Research - Cisco Blogs
B
Blog RSS Feed
C
Cisco Blogs
博客园 - 聂微东
Martin Fowler
Martin Fowler
T
The Blog of Author Tim Ferriss
小众软件
小众软件
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
L
LINUX DO - 热门话题
Stack Overflow Blog
Stack Overflow Blog
罗磊的独立博客
P
Proofpoint News Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
P
Privacy International News Feed
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
C
CERT Recently Published Vulnerability Notes
Cisco Talos Blog
Cisco Talos Blog
S
SegmentFault 最新的问题
Security Latest
Security Latest
Y
Y Combinator Blog
爱范儿
爱范儿
aimingoo的专栏
aimingoo的专栏
P
Privacy & Cybersecurity Law Blog
L
LINUX DO - 最新话题
月光博客
月光博客
The GitHub Blog
The GitHub Blog
博客园 - 三生石上(FineUI控件)
S
Security Affairs
P
Proofpoint News Feed
D
DataBreaches.Net
有赞技术团队
有赞技术团队
云风的 BLOG
云风的 BLOG

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Самодельный BIOS для микшерного пульта и запуск DOS на нём
PatientZero · 2026-06-26 · via Все публикации подряд на Хабре

В 1994 году у меня появился первый компьютер: Intel i486 DX2-66 с 4 МБ ОЗУ и жёстким диском на 512 МБ. На нём были установлены IBM OS/2 и Microsoft Windows 3.11. Следующие четыре года я апгрейдил эту машину каждые несколько месяцев, добавляя больше ОЗУ (до 16 МБ), привод CD-ROM и карту SoundBlaster. Так я научился апгрейдить эту машину, устанавливать новое ПО, а потом и писать ПО на BASIC. Но я ни разу не касался процесса запуска и тонкостей MS-DOS.

В 2026 году, 32 года спустя, я узнал из скриншотов DDX3216, что в Behringer использовался настоящий процессор 386. В моём мозгу сразу же активировались какие-то нейроны и я начал размышлять о том, можно ли запускать на этом устройстве ПО или даже полнофункциональную операционную систему. Для этого мне нужно было разобраться, как запускается система x86, когда управление перехватывает DOS и что необходимо для попадания в оболочку.

Технические подробности о Behringer DDX3216

В DDX3216 используются следующие аппаратные компоненты:

  • Основной процессор: SoC AMD Elan SC300 386 (386SX с интегрированными UART, PCMCIA, GPIO и так далее)

  • Интегральные схемы 27C512 64k x 8-bit ROM (для BIOS)

  • 8 модулей HYB5117400BJ60 ОЗУ 4M x 4bit, суммарно 16 МБ DRAM

  • 1 модуль SRAM UM61256 (в качестве VideoRAM)

  • 4 модуля интегральных флэш-схем 29C040-120 для основного ПО

  • 4-битный ЖК-модуль на внутреннем ЖК-интерфейсе SC300 (с тремя контроллерами столбцов Toshiba T6A39 и одним контроллером строк T6A40)

  • Внешний UART Toshiba TLC16C552 (2 последовательных порта и 1 параллельный)

  • Разъём PCMCIA для подключения внешней CF-карты (с адаптером)

  • Бескорпусной Intel 82078 FDC (Floppy Disk Controller), подключённый к свободному 34-контактному разъёму

То есть, в конечном итоге, оборудование в связке AMD Elan SC300 довольно хорошее и должно быть совместимо с обычной системой x86. Давайте подробнее изучим систему x86.

Первые шаги в разработке собственного ПО для голого x86

Для большинства компьютеров можно скачать из Интернета готовую BIOS. Я поискал BIOS для AMD ELAN SC и обнаружил многообещающее устройство в Швейцарии: компания PC Engines разработала программы BIOS для AMD ELAN SC400 и 520, а также для некоторых других SoC-устройств. Я связался с главным разработчиком и поначалу он обнадёжил меня, что у него есть исходный код для SC300. Но спустя пару дней выяснилось, что сохранились только исходники начиная с модели SC400 и выше. Потом я попытался связаться с компанией General Software, продававшей Embedded BIOS с поддержкой SC300. Однако основанная в 1989 году General Software в 2008 году была приобретена Phoenix, поэтому мне пришлось связаться с одним из ответственных лиц в Phoenix (Германия). Он попытался найти информацию о совместимом с SC300 пакете BIOS, но через пару недель вынужден был признать, что это уже невозможно: 32 года — долгий срок.

Я был вынужден закатать рукава и приступить к чтению документации системы x86 и ведению заметок о программировании собственного BIOS для SC300. Даже у самых современных x86-совместимых CPU наподобие Intel Core i9 или AMDs Threadripper есть совместимый с 8086 процесс запуска. Сразу после выполнения сброса CPU выполняет переход к концу пространства памяти на адрес 0xFFF0 и ожидает найти там исполняемый код x86 — так называемый вектор сброса (reset vector). От этого вектора сброса мы должны выполнить переход к нужному коду, который должен исполняться следующим; он находится где-то в ROM программы BIOS.

Вот моя первая попытка реализации валидного вектора сброса x86:

reset_vector:
    nop                         // no-operation
    cli                         // отключение прерываний
    jmp start                   // переход к началу текущего сегмента

    // Заполнение до конца и добавление даты
    .zero (0x10 - (. - reset_vector) - 8)
    .ascii "06/04/26"           // MM/DD/YY

Этот код отключает аппаратные прерывания, а затем переходит к другому коду в функции start. Исполняя эту команду перехода, CPU выходит из состояния запуска и входит в так называемый «реальный режим» (real mode), то есть исходный 16-битный режим 8086. Код вектора сброса помещается скриптом компоновщика в позицию 0xFFF0 готового ROM. Как можно увидеть из списка выше, в DDX3216 установлен чип 64k x 8bit ROM, поэтому код и данные можно хранить где-то в интервале от 0x0000 и 0xFFFF, а вектор сброса должен быть размещён по адресу 0xFFF0 для совместимости со спецификациями x86. Вот код скрипта компоновщика, сообщающий GCC, куда помещать код в готовом двоичном файле:

OUTPUT_FORMAT("elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(reset_vector)

MEMORY {
    ROM (rx)  : ORIGIN = 0x0000, LENGTH = 64K
}

SECTIONS {
    .text : {
        __text_start = .;
        KEEP(*(.text))
        *(.text.*)
        . = ALIGN(2);
        __text_end = .;
    } > ROM

    .reset 0xFFF0 : {
        KEEP(*(.reset))
    } > ROM
}

Скомпилированный двоичный файл выглядит так:

0000ffa0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000ffb0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000ffc0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000ffd0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000ffe0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000fff0 90 fa e9 0b 00 00 00 00 30 36 2f 30 34 2f 32 36 .úé.....06/04/26

Здесь мы видим по адресу 0xFFF0 команду 0x90, то есть nop (No Operation), команду 0xFA, то есть cli, отключающую все прерывания, за которой следует 0xE9, то есть команда перехода. 0x0B — это ближний адрес для команды перехода. «Ближний» означает, что этот адрес находится внутри текущего сегмента. Сегменты — это особая структура в системе x86: так как у нас есть только 16 бит, мы можем адресовать только 65535 байт. Для адресации большего количества адресов x86 использует сегменты по 64 килобайт. Проблема заключается в том, что для обеспечения обратной совместимости указатели адресов сегментов накладываются друг на друга на 16 байт. Это позволяет адресовать 0xFFFF сегментов каждые 16 байт, получив таким образом адресное пространство от 0x00000 до 0xFFFF0, то есть 1 МБ. Физический адрес вычисляется по следующему уравнению:

Physical Address = (SEGMENT << 4) + OFFSET

// максимальный физический адрес
Physical Address = (0xFFFF << 4) + 0x000F = 0xFFFFF = 1048575 = 1 MB

Так я наконец-то понял, почему DOS и игры для неё имели проблемы с основной памятью, которая должна была находиться в диапазоне 1 МБ. На самом деле, ограничения были ещё более серьёзными: можно было использовать в качестве основной памяти только первые 640 КБ, более высокие адреса зарезервированы под видеопамять, ROM расширений и саму BIOS. Из-за этого обычная система x86 в реальном режиме имела такую структуру памяти:

+--------------------------------------------------+ 0x100000 (1 МБ)
|                                                  |
|   SYSTEM BIOS                                    | ROM 64 КБ
|                                                  |
+--------------------------------------------------+ 0xF0000  (960 КБ)
|                                                  |
|   Option ROM или свободная верхняя память        | 160 КБ
|                                                  |
+--------------------------------------------------+ 0xC8000  (800 КБ)
|   Video BIOS                                     | ROM 32 КБ
+--------------------------------------------------+ 0xC0000  (768 KB)
|   Video RAM                                      | ОЗУ 128 КБ
+==================================================+ 0xA0000  (640 КБ)
|                                                  |
|                                                  |
|                                                  |
|                                                  |
|   ОСНОВНАЯ ПАМЯТЬ (ОЗУ)                          |
|   Свободное место для DOS,                       | около 605 КБ
|   программ, драйверов и т.д.                     | 
|                                                  |
|                                                  |
|                                                  |
+--------------------------------------------------+ 0x07E00
|   Загрузочный сектор (загружаемый                |
|   с загрузочного устройства)                     | 512 байт
+--------------------------------------------------+ 0x07C00
|   Свободная память DOS / ядро DOS                | ~29 КБ
+--------------------------------------------------+ 0x00500
|   BDA (BIOS Data Area)                           | 256 байт
+--------------------------------------------------+ 0x00400
|   IVT (таблицы векторов прерываний)              | 1 КБ
+--------------------------------------------------+ 0x00000

То есть вместо 640 КБ у нас есть только 605 КБ свободной ОЗУ для нашего кода, потому что часть памяти занимают IVT, BDA и загрузочный сектор. От 0x0500 до 0x7C00 у нас есть немного места для ядра, а от 0xC8000 до 0xF0000 есть 160 КБ верхней памяти, которую можно использовать в случае отсутствия Option ROM.

Поначалу я не понимал, как тестировать мой скомпилированный код. Да, чаще всего применяют EEPROM-программатор для записи образа BIOS на интегральную схему EEPROM, но из-за этого цикл разработки будет медленным. Я поискал решение получше и нашёл проекты PicoROM и OneROM. В обоих проектах используются контроллеры RaspberryPi Pico, в которых достаточно памяти для эмуляции интегральной схемы ROM. Я заказал оба устройства из США и Великобритании. Сначала я загрузил оригинальный ROM DDX3216, чтобы проверить беспроблемный запуск консоли аудиомикширования.

Оригинальное ПО загрузилось и я удостоверился в работоспособности эмулятора ROM. Чтобы проверить работу моего кода, я хотел запустить внешний UART. Как показано на изображении, у DDX3216 есть 9-контактный разъём RS232:

Проблема в том, что компания Behringer для этого разъёма использовала не внутренний UART SC300, а внешнюю интегральную схему Toshiba TLC16C552. Внешний UART подключён к адресным линиям с SA0 по SA2 (обозначены синим кругом) и с линиями данных с D0 по D7. TLC16C552 состоит из двух последовательных портов и одного параллельного порта, а каждую функцию можно включить при помощи одного из трёх сигналов выбора чипа от CS0# до CS2# (зелёный круг). Эти сигналы выбора чипа подключены к каким-то логическим интегральным схемам и соединены с SA3, SA4, SA12, SA13, SA14 и SA15:

Изучив спецификации подключённых адресных линий, можно понять, что CS0# активируется, когда используется адрес ввода-вывода с 0x1000 по 0x1007, CS1# активируется, когда используется адрес ввода-вывода с 0x1008 по 0x100F, CS#2 — когда используются адреса ввода-вывода с 0x1010 по 0x1017. Прежде, чем мы сможем задействовать 9-контактный разъём UART, нам нужно гарантировать включение логической интегральной схемы IC110 для передачи сигналов TxD. В качестве этой схемы используется 74HCT125; для включения её вывода необходимо активировать сигнал RS232#. RS232# соединён с SLIN# схемы TLC16C552, соединённым с параллельным выводом. То есть сначала нам нужно запрограммировать параллельный порт, активировать SLIN#, а затем инициализировать последовательный вывод для отладки:

; подача тактового сигнала на внешний UART
out 0x0022, 0xBA          ; задание адреса конфигурации
out 0x0023, 0b00001000    ; задание данных конфигурации

; программирование внешнего UART через операции записи ввода-вывода
; сначала интерфейс параллельного порта по базовому адресу 0x1010
out 0x1012, 0x00001000    ; включение SLIN# = RS232# через параллельный порт 

; теперь интерфейс последовательного порта по базовому адресу 0x1000
out 0x1003, 0x80          ; включение доступа к регистрам-защёлкам делителя
out 0x1000, 0x5D          ; установка делителя скорости передачи (LSB)
out 0x1001, 0x00          ; установка делителя скорости передачи (MSB)
out 0x1003, 0x03          ; сброс бита DLAB и задание режима 8N1
out 0x1001, 0x00          ; отключение всех прерываний
out 0x1002, 0x00          ; отключение FIFO
out 0x1004, 0x03          ; задание DTR и RTS

Этого должно быть достаточно для включения внешнего UART и передачи сигналов TxD через 9-контактный разъём на обратной стороне устройства. Из Programmers Reference Manual я узнал, что для переключения SoC в рабочее состояние самые важные регистры нужно устанавливать после сброса запуска, поэтому запрограммировал ещё примерно двадцать регистров конфигурации ELAN SC300, чтобы SoC работала на частоте 33 МГц.

Для вывода отладочного сообщения через терминал я отправлял символы на последовательный приёмопередатчик:

out 0x1000, 'A'            ; вывод символа через RS232

Потратив пару часов на чтение руководства и спецификаций других интегральных устройств, я смог загрузить свой образ ROM и успешно получил на подключённом компьютере символы на скорости 9600 бод:

Запуск ЖК-дисплея и проблема с сегментами

Спустя какое-то время мне удалось инициализировать стек и перейти к моей основной функции на C. После этого было довольно просто инициализировать другие компоненты, например, дисплей, подключённый напрямую к 4-битному интерфейсу ЖК-дисплея AMD Elan SC300. Этот интерфейс реализует совместимую с CGA/HGA видеокарту. SC300 использует сегмент памяти 0xB800 для хранения символов при работе с текстовым режимом. В этом режиме SC300 ожидает по два байта на каждый отображаемый символ: первый байт — это отображаемый символ, а второй содержит байт атрибута. Этот байт атрибута содержит контрастность и цвет символа. Вроде здесь никаких проблем, но... мы не можем выполнять запись напрямую в сегмент 0xB800. BIOS работает в сегменте 0xF000. Вместе со смещением мы можем обеспечивать доступ ко всему 16-битному диапазону ROM, то есть ко всем 64 КБ. Но при попытке записи в сегмент 0xB800 доступ к памяти завершается сбоем.

Система x86 использует множество регистров для доступа к данным или коду. В реальном режиме есть CS (CodeSegment) и DS (DataSegment) для доступа к коду и переменным в определённом сегменте. Пока оба сегмента привязаны к 0xF000. Но есть и другие регистры, например, ES. Можно записать в него нужный целевой сегмент и скопировать данные. Чтобы реализовать это, я использую встроенный ассемблерный код: «значение» записывается в ES, «смещением» становится регистр BX, после чего «значение» записывается по нужному адресу:

static inline void writeFarByte(uint16_t segment, uint16_t offset, uint8_t value) {
	__asm__ __volatile__(
        "pushw %%es\n\t"
        "movw  %w0, %%es\n\t"
        "movb  %b2, %%es:(%%bx)\n\t"
        "popw  %%es\n\t"
        :
        : "r"(segment), "b"(offset), "q"(value)
        : "memory"
    );
}

Мы можем и считывать переменные:

static inline uint8_t readFarByte(uint16_t segment, uint16_t offset) {
    uint8_t value;

    __asm__ __volatile__(
        "pushw %%es\n\t"
        "movw  %w1, %%es\n\t"
        "movb  %%es:(%%bx), %b0\n\t"
        "popw  %%es\n\t"
        : "=q"(value)
        : "r"(segment), "b"(offset)
        : "memory"
    );

    return value;
}

Благодаря двум этим функциям с ассемблерным кодом у меня должно получиться записывать символы на дисплей. Но оказалось, что пока нет. SC300 имеет поддержку нескольких шрифтов ЖК-дисплея, но у него нет ROM со шрифтами. Поэтому мне пришлось побайтово реализовать полную таблицу ASCII, потому что SC300 требует символов размером 8×8 пикселей. Итого получается 8 байт на один символ. Суммарно мой готовый файл заголовка шрифтов занимает примерно 22 КБ, но, к счастью, с этой тупой задачей смог справиться ИИ. Google Gemini сгенерировал красивый шрифт для моего BIOS. На уровне отдельных символов пришлось устранить некоторые проблемы с пикселями, но в целом шрифт был вполне рабочим с самого начала. Вот пример частей шрифта:

static const uint8_t bios_font_8x8[256][8] = {
    // ...
    ['a']  = {0x00, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0x76, 0x00},
    ['b']  = {0xE0, 0x60, 0x7C, 0x66, 0x66, 0x66, 0xDC, 0x00},
    ['c']  = {0x00, 0x00, 0x78, 0xCC, 0xC0, 0xCC, 0x78, 0x00},
    ['d']  = {0x1C, 0x0C, 0x7C, 0xCC, 0xCC, 0xCC, 0x76, 0x00},
    ['e']  = {0x00, 0x00, 0x78, 0xCC, 0xFC, 0xC0, 0x78, 0x00},
    ['f']  = {0x38, 0x6C, 0x60, 0xF0, 0x60, 0x60, 0xF0, 0x00},
    ['g']  = {0x00, 0x00, 0x76, 0xCC, 0xCC, 0x7C, 0x0C, 0xF8},
    ['h']  = {0xE0, 0x60, 0x6C, 0x76, 0x66, 0x66, 0xE6, 0x00},
    ['i']  = {0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3C, 0x00},
    ['j']  = {0x06, 0x00, 0x16, 0x06, 0x06, 0x06, 0x66, 0x3C},
    ['k']  = {0xE0, 0x60, 0x6C, 0x78, 0x70, 0x78, 0x6C, 0xC6},
    ['l']  = {0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00},
    ['m']  = {0x00, 0x00, 0xEC, 0xFE, 0xD6, 0xD6, 0xC6, 0x00},
    // ....
}

Реализация полнофункционального BIOS x86 для SC300

Я потратил ещё немного времени на инициализацию других компонентов SC300, например, таблицы векторов прерываний (IVT), BIOS Data Area (BDA), внутреннего контроллера клавиатуры, таймера и интерфейса карты CF. Первые два компонента — это просто область в основной памяти. Взглянув на структуру памяти, вы сможете найти обе эти части внизу:

+--------------------------------------------------+ 0x07E00
|   Загрузочный сектор (загружаемый                |
|   с загрузочного устройства)                     | 512 байт
+--------------------------------------------------+ 0x07C00
|   Свободная память DOS / ядро DOS                | ~29 КБ
+--------------------------------------------------+ 0x00500
|   BDA (BIOS Data Area)                           | 256 байт
+--------------------------------------------------+ 0x00400
|   IVT (таблицы векторов прерываний)              | 1 КБ
+--------------------------------------------------+ 0x00000

Область IVT считывается процессором x86, чтобы получить указатель соответствующей функции обработки прерывания при создании запроса прерывания оборудованием или ПО. То есть это просто список 16-битных указателей, за которым следует ещё одно 16-битное значение, содержащее адрес сегмента, на который указывает 16-битный указатель. Если вкратце, мы храним до 256 32-битных (примерно) адресов для каждого прерывания.

BDA можно сравнить с пространством конфигурации. Здесь BIOS хранит такие данные, как базовые адреса COM-портов, LPT-портов, общий размер основной памяти, положения курсоров, видеорежимы, такты таймеров, данные клавиатуры и так далее. DOS использует часть 16-битных значений для взаимодействия с BIOS наряду с функциями прерываний.

Из Programmers Reference Manual я узнал, что интегральная схема внутреннего таймера типа 8254 применяется в качестве эталонной интегральной схемы таймера x86. Интегральная схема таймера имеет тактовую частоту 1,1892 МГц, в отличие от более распространённого стандарта AT 1,19318 МГц. Для достижения стандартного прерывания по таймеру в 18,207 Гц мне пришлось сконфигурировать счётчик таймера на 0xFF23, получив в результате счётчик на 18,20715 Гц, что достаточно близко.

В SC300 используется стандартный контроллер клавиатуры XT, получающий 8-битные данные от внешней клавиатуры XT, поэтому мне пришлось создать адаптер, преобразующий данные моей клавиатуры AT/PS2 в XT.

Ещё одним сложным аспектом стал интерфейс карты CF, на реализацию которого мне понадобилось много времени. У SC300 есть два нативных разъёма для карт PCMCIA. То есть теоретически, к нему можно подключить две карты CF или другие карты PCMCIA, но со внешним разъёмом карты соединён только порт A. К счастью, на eBay всё ещё можно найти кучу разных адаптеров PCMCIA, поэтому я приобрёл PCMCIA-2-CF-Card-Adapter. Карты CF — это, по сути, накопители IDE со стандартным интерфейсом команд ATA, а PCMCIA напрямую соединяется с картой CF без дополнительной электроники. Всё это здорово, но размер моей карты CF больше 500 МБ. Из истории с сегментами я уже понял что в реальном режиме можно адресовать только до 1 МБ. Как же мне считывать данные с карты CF без этих ограничений? Оказалось, что у SC300 есть мощный блок распределения памяти, а то и два. В SC300 они называются MMS (MemoryMappingSystem) A и B.

MMSA способен распределять 8 страниц по 64 КБ по адресам от 0xD0000 до 0xEFFFF, а MMSB — только 4 страницы по 64 КБ от 0xA0000 до 0xAFFFF. На изображении ниже показано потенциальное распределение различных частей системы. DRAM, PCMCIA CardA или B, ROM BIOS или ROM DOS можно распределить в эти области по 64 КБ. ROM DOS означает одну из четырёх интегральных флэш-схем, перечисленных в первой таблице. Сюда помещается исходная прошивка, поэтому я пока не буду использовать эти интегральные схемы. А вот MMS в таком распределении можно использовать для проверки памяти модулей DRAM верхних 15 МБ.

Впрочем, здесь нужно признаться, что чтение и запись с карты CF можно выполнять без MMS, поскольку команды ATA поддерживают LBA-доступ или, как минимум, доступ с использованием CHS, позволяющий адресовать цилиндры, головки и секторы по 512 байт на сектор. То есть суммарно мы сможем получать доступ примерно к 8 ГБ:

C * H * S * 512 байт = x

1024 * 255 * 63 * 512 байт = 8422686720 байт = 8032,50 МБ

Однако проблема в том, что моя карта CF пока не запускается в режиме TrueIDE и доступ к ней нельзя получать через команды ATA. Это связано с аппаратной конфигурацией разъёма карты CF, где важный контакт изначально установлен в режим PCMCIA-памяти. То есть нам нужно инициализировать карту CF, при помощи MMS получить доступ к так называемой CIS (Card Information Structure), а затем переключить карту CF из режима PCMCIA-памяти в режим TrueIDE:

// Записываем в REGA# LOW для доступа к памяти атрибутов
outb(REGA_BASE, 0x01);

// Задаём COR, чтобы переключиться из режима распределения памяти в режим распределения ввода-вывода
// Мы используем режим опроса (0x1F0...0x1F7 / 0x3F6...0x3F7)
// b7=SoftReset, b6=Interrupt-Mode, b5=CardReset, b4..0=ConfigIndex
writeFarByte(PCMCIA_BASE, 0x0200, 0b00000010);

// Записываем в REGA# HIGH для доступа к общей памяти
outb(REGA_BASE, 0x00);

// Выполняем сброс карты CF через регистр IDE
outb(IDE_DEV_CTRL, 0x06); // Программный сброс карты CF через регистр IDE
for (volatile uint16_t i = 0; i < 10000; i++);
outb(IDE_DEV_CTRL, 0x02); // SRST=0, nIEN=1 (IRQ остаётся отключенным)
for (volatile uint16_t i = 0; i < 10000; i++);

// Выбор привода 0 (master)
outb(IDE_DRIVE_HEAD, 0xA0);
for (volatile uint32_t i = 0; i < 10000; i++);

Теперь мы можем использовать обычные команды ATA для адресов ввода-вывода с 0x1F0 по 0x1F7 или с 0x3F6 по 0x3F7. Ниже приведён пример моей функции чтения всего сектора (512 байт) в ОЗУ. Для простоты я не использую CHS-адресацию, а преобразую CHS в LBA и вызываю эту функцию чтения. Да, это не самый быстрый способ, но мне просто хотелось получать удовольствие, а не максимально оптимизировать свой код:

uint8_t ide_read_sector(uint32_t lba, uint16_t dest_seg, uint16_t offset) {
    // Ожидаем готовности привода
    if (!ide_wait_ready()) {
        return 0xAA; // ERROR: Drive not ready
    }

    // Задаём адрес LBA и отправляем команду
    outb(IDE_SECT_COUNT,  1);  // 0x1F2
    outb(IDE_LBA_LOW,     (uint8_t)( lba        & 0xFF));  // 0x1F3
    outb(IDE_LBA_MID,     (uint8_t)((lba >>  8) & 0xFF));  // 0x1F4
    outb(IDE_LBA_HIGH,    (uint8_t)((lba >> 16) & 0xFF));  // 0x1F5
    outb(IDE_DRIVE_HEAD,  0xE0 | ((lba >> 24) & 0x0F));    // 0x1F6
    outb(IDE_COMMAND,     IDE_CMD_READ);  // 0x1F7

    // Ждём готовности данных (DRQ установлен)
    if (!ide_wait_drq()) {
        return 0xBB;  // ERROR: DRQ timeout
    }

    // Считанные 512 байт данных (один полный сектор)
    // доставляются по адресу ввода-вывода 0x1F0 (регистр IDE_DATA)
    for (uint16_t i = 0; i < 512; i++) {
        uint8_t data = inb(IDE_DATA);
        writeFarByte(dest_seg, offset + i, data);
    }

    return 0x00; // ошибок нет
}

После создания этой функции у нас есть большинство функций для взаимодействия с основными компонентами. Наконец-то при одной из моих попыток запустить машину я увидел такой экран:

Для этого мне понадобилось примерно две с половиной недели, но не благодаря исключительно чтению технической документации системы x86 — частично мне помогал ИИ. Наверно, это подходящий момент для того, чтобы рассказать, как я пользуюсь ИИ: я никогда не использовал ИИ-агентов для управления своим кодом, мне кажется небезопасным терять контроль над ним. В своём университете я пользуюсь системой Google Gemini и ChatAI (чаще всего работаю с Claude Sonnet 4.6). ИИ не пишет и не модифицирует мой код, я задаю ему конкретные вопросы. Например, я загрузил в ChatAI Programmers Reference Manuel системы AMD Elan SC300 и попросил помочь с инициализацией моих конкретных чипов DRAM. Я сверил установленные регистры с руководством, но с 95% регистров было всё в порядке. Затем я реализовал этот проверенный код в своём проекте. Благодаря такой методике даже сложные задачи можно решить достаточно быстро.

Функции прерываний и попытка запуска MS-DOS 6.22

Моя конечная цель заключалась в запуске DOS, но прежде предстоял большой объём работы. DOS взаимодействует с BIOS крайне специфичным образом: частично DOS взаимодействует с BIOS Data Area, но чаще всего работа с BIOS выполняется при помощи прерываний. Вот список самых важных прерываний:

INT 0x08: прерывание таймера
INT 0x09: прерывание внешней клавиатуры
INT 0x0C: прерывание внешнего UART
INT 0x10: прерывание видео
INT 0x11: список оборудования
INT 0x12: размер памяти
INT 0x13: прерывание диска
INT 0x14: прерывание UART
INT 0x15: многофункциональное прерывание
INT 0x16: прерывание клавиатуры
INT 0x17: прерывание параллельного порта
INT 0x19: прерывание запуска
INT 0x1A: прерывание RTC

Если DOS хочет отобразить данные на ЖК-дисплее, то не вызывает конкретно функции ЖК-модуля, а просто записывает в регистр AX значение 0x0Ecc; cc содержит отображаемый символ. BIOS нужно взаимодействовать с интерфейсом ЖК-модуля, но DOS просто общается с прерыванием 0x10. Аналогично и для данных с диска: если DOS нужно считать определённый сектор с диска, она вызывает прерывание 0x13 с какими-то данными в регистрах AX, BX и так далее. Для чтения в AX должно быть записано 0x02ss, где ss — это количество считываемых секторов. Регистр BX содержит смещение с целевым сегментом, а регистр ES — целевой сегмент. CX и DX содержат конкретный цилиндр, головку и сектор диска. Мой BIOS преобразует этот CHS-адрес в 48-битный LBA-адрес и считывает конкретный сектор.

Для реализации некоторых из функций прерываний потребовалось довольно много времени, но в конечном итоге всё было готово к попытке запуска MS-DOS 6.22: я знал, что DOS довольно капризна, когда дело касается загрузочных секторов и данных в первом секторе, поэтому запустил свой старый компьютер 486 с загрузочным диском DOS 6.22. При помощи fdisk я разбил карту CF на разделы и выполнил format c:, чтобы отформатировать её в FAT16. Затем я перенёс программные файлы на карту CF. При этом в самое начало карты CF скопировался IO.SYS, а также MSDOS.SYS и COMMAND.COM.

Перед запуском DOS требуется выполнить ещё несколько операций:

Этап 1:
BIOS загружает Master Boot Record (MBR) выбранного диска в ОЗУ по адресу 0x7C00 (суммарно 512 байт)

Этап 2:
MBR считывает таблицу разделов, выбирает активный раздел и загружает первый сектор в ОЗУ по адресу 0x7C00
В случае MS-DOS файл IO.SYS должен быть первым(!) файлом в корневой папке

Этап 3:
Первые три сектора IO.SYS загружает в ОЗУ и система переходит к IO.SYS

Этап 4:
IO.SYS через INT12h запрашивает основную память и загружает другие секторы в сегмент 0x0000:0x0500,
а позже последний сегмент в конец основной памяти

Этап 5:
IO.SYS загружает MSDOS.SYS (60-80 секторов)

Этап 6:
MSDOS.SYS загружает COMMAND.COM и запускает оболочку

Мне нужно было скопировать загрузочный сектор (MBR) в сегмент 0x7C00, который предназначен для загрузочного сектора, а затем выполнить дальний переход к этому адресу:

void boot_dos() {
    uint8_t status = 0xFF;
    uint8_t retries = 3;
    
    // чтение загрузочного сектора
    while (retries-- > 0) {
        if (ide_read_bootsector() == 0x00) {
            status = 0; // успешно
            break;
        }
        // ошибка чтения -> повтор
    }

    if (status != 0) {
        // ERROR: read после трёх повторных попыток
        return;
    }

    // проверка сигнатуры загрузки в конце MBR
    uint16_t signature = readFarWord(BASE_SEG, 0x7C00 + 510);
    if (signature != 0xAA55) {
        // ERROR: нет валидного магического слова
        return;
    }
    
    // похоже, загрузочный сектор в порядке -> переход к загрузочному сектору
    launch_bootsector();
}

Функция launch_bootsector() написана на языке ассемблера; она просто сбрасывает сегментные регистры, задаёт начальный стек прямо под загрузочным сектором, выбирает загрузочный диск при помощи регистра DX и выполняет дальний переход к началу загрузочного сектора:

launch_bootsector:
    cli                         ; отключение прерываний
    
    ; сброс всех сегментов на 0x0000
    xor ax, ax                  ; присвоение ax значения 0x0000
    mov ds, ax
    mov es, ax
    
    ; задание указателя стека для инициализации DOS
    ; прямо под загрузочным сектором по адресу 0x0000:0x7C00
    mov ss, ax                  ; присвоение сегменту стека значения 0x0000
    mov sp, 0x7C00  

    ; запись загрузочного диска в DL
    mov dl, 0x80

    sti                         ; включение прерываний

    ; дальний переход к загрузочному сектору
    jmp 0x0000:0x7C00

После компиляции и загрузки всего этого я подключил карту CF и запустил систему... но ничего не произошло. Приступив к отладке вызовов прерываний через интерфейс UART, я выяснил, что в некоторых моих функциях прерываний имелись проблемы с ошибочными кодами функций и ответами DOS. Я исправил эти прерывания, а потом запустился снова. ЖК-дисплей по-прежнему не отображал никакой информации DOS, но вызывалось гораздо больше INT 0x13. Впрочем, система внезапно снова вылетела.

Я вывел через интерфейс UART указатель стека и обнаружил, что стек опустился очень низко, хотя я запустил его прямо под 0x7C00. Причина этого заключается в том, что стек растёт сверху вниз, но когда указатель стека становится слишком низким, это означает наличие проблем в стеке. Очевидно, DOS или изменяла порядок стека, или использовала огромный объём стека (оказалось, что для получения больше свободной ОЗУ DOS перемещает стек в нижнюю часть памяти). Я потратил несколько часов на переупорядочивание своей модели памяти и выделил отдельный стек BIOS в начале основной памяти исключительно под вызовы прерываний BIOS. Это позволило решить проблему и запуск DOS продвинулся гораздо дальше. Как видно на следующем изображении, экран показывает сообщение «Starting MS-DOS…», за которым следует ещё несколько вызовов прерываний:

После текста «Starting MS-DOS…» вызывается прерывание 0x15, за которым следует INT 0x1A (таймер реального времени). Точки обозначают вызовы INT 0x13 (чтение с диска), но система зависает после последнего вызова INT 0x15 с AX = 0x4101, не обозначающего обычный вызов функции. То есть DOS, похоже, сдаётся после того, как пытается прочитать какие-то секторы. Я много дней потратил на решение этой проблемы, но в конечном итоге поднял руки вверх… Я был так близок к оболочке DOS. Судя по изученному исходному коду MS-DOS 4.0, файлы IO.SYS и MSDOS.SYS загружались успешно, поскольку уже вызывалось прерывание RTC. То есть система зависала где-то между MSDOS.SYS и COMMAND.COM. Даже после исследования опубликованного Microsoft исходного кода MS-DOS 4.0 мне так и не удалось найти виновника, поэтому я временно отказался от запуска MS-DOS 6.22.

Успешный запуск FreeDOS v1.4

MS-DOS 6.22 не запускалась, поэтому я скачал самую свежую версию FreeDOS, а именно 1.4. Затем я запустил QEMU и создал небольшое виртуальное окружение для установки FreeDOS:

rem создание нового виртуального образа для FreeDOS
qemu-img create -f raw freedos.img 100M

rem Запуск системы с FreeDOS LiveCD
rem Виртуальный диск с 203 цилиндрами, 16 головками и 63 секторами
qemu-system-i386 -machine isapc -cpu 486 -m 8 -device isa-vga,vgamem_mb=1 -rtc base=localtime -drive file=freedos.img,format=raw,if=none,id=d1 -device ide-hd,drive=d1,cyls=203,heads=16,secs=63 -cdrom FD14LIVE.iso -boot d

rem Альтернатива:
rem Запуск системы с загрузочным гибким диском MS-DOS 6.22
rem Виртуальный диск с 203 цилиндрами, 16 головками и 63 секторами
qemu-system-i386 -machine isapc -cpu 486 -m 8 -device isa-vga,vgamem_mb=1 -rtc base=localtime -drive file=freedos.img,format=raw,if=none,id=d1 -device ide-hd,drive=d1,cyls=203,heads=16,secs=63 -fda dos622.img -boot a

После процесса установки я при помощи Rufus посекторно скопировал виртуальный образ на реальную карту CF. Затем подключил её к консоли аудиомикшера Behringer DDX3216 и запустил систему:

Ура, хотя бы FreeDOS справилась с запуском. Я потратил три недели на попытки создания самодельной BIOS, которая будет достаточно совместимой с реальным ПО x86, чтобы запустить реальную операционную систему. Мне ещё осталось реализовать немного кода для прерывания 0x16 (доступ к клавиатуре) и другие мелочи, но в целом система вполне работает.

Другое внутреннее оборудование и следующие шаги

Многие компоненты DDX3216 основаны строго на логических интегральных схемах. Например, все светодиоды управляются простым сдвиговым регистром, подключённым к интерфейсу ввода-вывода SC300. Сначала сигналы управления подаются на IC5A и IC6A; IC5A управляет сигналами VULTCH, LSSELR, UCSELR и SPTESR, а IC6A управляет сигналами VUSELW, LSSELW, UCSELW, LSLTCH, FLSET1, FLSET0. Адресные биты SA12..15 SC300 используются на шине ввода-вывода, образуя адресное пространство от 0x1000 до 0xF000. 0x3000 позволяет VUSELW выполнять запись, а VULTCH выполнять чтение шины ввода-вывода. То есть светодиоды 1 и 9 аудиоканала 1-4 управляются битом 0 адреса 0x3000, светодиоды 8 и 16 канала 1-4 — битом 7 адреса 0x3000. К девятому биту предыдущего сдвигового регистра подключено ещё четыре сдвиговых регистра, чтобы биты сдвигались и через различные логические интегральные схемы.

Кроме того, для ограничения максимального тока контактов VCC или GND драйверов светодиодов чётные и нечётные светодиоды подключены к GND и VCC попеременно:

То есть нам нужно передавать 0 и 1 в зависимости от выбранного в сдвиговом регистре светодиода. Показанный ниже код устанавливает для всех светодиодов индикаторов уровня всех каналов сигнал HIGH:

bool even = false;
for (uint8_t i = 0; i < (8 * 5); i++) {
    if (even) {
        outb(0x3000, 0b11111111);
    }else{
        outb(0x3000, 0b00000000);
    }
    inb(0x3000);

    even = !even;
}

Этот код работает благодаря тому. что у нас есть пять конкатенированных 8-битных сдвиговых регистров с попеременными светодиодами на каждом выводе. Каждый бит показанного выше байта связан с конкретным светодиодом. Вот полный список светодиодов индикаторов уровней:

// DL30..37
outb(0x3000, 0b00000000); inb(0x3000); // led 1, Left   led 2, Left     led 3, Left
outb(0x3000, 0b11111111); inb(0x3000); // led 9, Left   led 10, Left    led 11, Left
outb(0x3000, 0b00000000); inb(0x3000); // led 1, Right  led 2, Right    led 3, Right
outb(0x3000, 0b11111111); inb(0x3000); // led 9, Right  led 10, Right   led 11, Right
outb(0x3000, 0b00000000); inb(0x3000); // LED segment
outb(0x3000, 0b00000000); inb(0x3000); // LED segment
outb(0x3000, 0b00000000); inb(0x3000); // LED segment
outb(0x3000, 0b00000000); inb(0x3000); // свободно

// DL20..27
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 13   led 2, ch 13    led 3, ch 13
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 13   led 10, ch 13   led 11, ch 13
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 14   led 2, ch 14    led 3, ch 14
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 14   led 10, ch 14   led 11, ch 14
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 15   led 2, ch 15    led 3, ch 15
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 15   led 10, ch 15   led 11, ch 15
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 16   led 2, ch 16    led 3, ch 16
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 16   led 10, ch 16   led 11, ch 16

// DL10..17
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 9    led 2, ch 9     led 3, ch 9
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 9    led 10, ch 9    led 11, ch 9
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 10   led 2, ch 10    led 3, ch 10
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 10   led 10, ch 10   led 11, ch 10
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 11   led 2, ch 11    led 3, ch 11
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 11   led 10, ch 11   led 11, ch 11
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 12   led 2, ch 12    led 3, ch 12
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 12   led 10, ch 12   led 11, ch 12

// DL00..07
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 5    led 2, ch 5     led 3, ch 5
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 5    led 10, ch 5    led 11, ch 5
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 6    led 2, ch 6     led 3, ch 6
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 6    led 10, ch 6    led 11, ch 6
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 7    led 2, ch 7     led 3, ch 7
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 7    led 10, ch 7    led 11, ch 7
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 8    led 2, ch 8     led 3, ch 8
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 8    led 10, ch 8    led 11, ch 8

// DL0..7
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 1    led 2, ch 1     led 3, ch 1
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 1    led 10, ch 1    led 11, ch 1
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 2    led 2, ch 2     led 3, ch 2
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 2    led 10, ch 2    led 11, ch 2
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 3    led 2, ch 3     led 3, ch 3
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 3    led 10, ch 3    led 11, ch 3
outb(0x3000, 0b00000000); inb(0x3000); // led 1, ch 4    led 2, ch 4     led 3, ch 4
outb(0x3000, 0b11111111); inb(0x3000); // led 9, ch 4    led 10, ch 4    led 11, ch 4

Итак, в дальнейшем можно было бы написать функцию, изменяющую состояние светодиодов в зависимости от части ОЗУ, содержащей состояние светодиода, аналогично буферу пикселей. Также можно запрограммировать ползунки и вращающиеся ручки.

Так как в DDX3216 есть множество микроконтроллеров PIC16 с проприетарными прошивками, нам, вероятно, не удастся заставить работать все детали. Набравшись опыта в работе с Behringer X32 и DSP 21379, я думал о том, чтобы попробовать управлять DSP AnalogDevices SHARC, но четыре DSP SHARC подключены к проприетарному логическому устройству, похожему на FPGA, только программируемому во включённом состоянии. Без конкретной информации об этом устройстве подключиться к DSP будет довольно сложно.

Поэтому я думаю продолжить эксперименты с FreeDOS, подключить клавиатурный переходник AT-XT и, возможно, реализовать графический видеорежим для тестирования на этом устройстве Windows 2.0 или 3.0, но это уже тогда, когда у меня будет много времени.

Полный исходный код можно найти на GitHub: https://github.com/xn--nding-jua/DDX3216. В нём я подготовил несколько самодельных программ для загрузочного сектора, способных переключаться в защищённый режим, а также использовать плоские 32-битные указатели, что сильно упрощает адресацию всей памяти. Но с этими загрузочными секторами вам не удастся запустить DOS, потому что она требует реального режима CPU x86.