На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
! Перед отправкой сообщения внимательно прочтите правила раздела!!!
1. Все статьи должны быть оформлены согласно Правил оформления статей.
2. Любые обсуждения должны происходить в специальной теме, обсуждение в любых других темах раздела запрещены.
3. Запрещается писать статьи о создании и распространении вирусов, троянов и других вредоносных программ!
4. За грамотно написанные и правильно оформленные статьи авторы награждаются DigiMoney.

Дополнительные ссылки:
Желаю творческих успехов! ;)
Модераторы: Jin X
  
    > Низкоуровневое программирование видеоадаптеров ATI (часть вторая) , x86, DOS, Hardware
      Низкоуровневое программирование видеоадаптеров ATI (часть вторая)

      Установка видеорежима с нужными параметрами через регистры

      С момента выхода первой части этой статьи мне начали периодически задавать этот вопрос о том как это делать и я вот таки решился заняться исследованием (и таки достиг некоторого успеха). Если раньше это было необходимо для того, чтобы "избавиться" от 60hz на ЭЛТ мониторах, то теперь встала проблема отсутствия поддержки нестандартных видеорежимов в VBE. Сразу оговорюсь, что у меня на руках сейчас две карточки: с rv630 и rv730. Соответственно примеры будут рабочими с большой вероятностью на rv6xx и rv7xx, частично на rv5xx. На более новых карточках работать не будет, но там все очень похоже (по факту просто регистры переместили), поэтому желающие могут портировать.

      EDID

      Для начала я расскажу некоторые общие сведения. Наверное, все замечали, что операционная система "видит" список видеорежимов, которые поддерживает монитор и не дает установить "левые". Но я так понял, что мало кто задумывался как видеодрайвер "узнает" этот список. Так вот даже у аналогового VGA есть pin'ы зарезервированные для DDC, через него передается такая штука как EDID. По ссылке есть исчерпывающее описание структуры EDID, поэтому я не буду ее здесь описывать. Она очень проста, все поля имеют абсолютное смещение, ничего вычислять не надо. По сути это просто набор полей с характеристиками монитора, там есть такие "архиважные" вещи как его название, изготовитель, габариты, но они нам не интересны. Самое важно там - это наличие списка доступных видеорежимов, а также VGA-тайминги для "родного" режима монитора. Именно их мы будем заносить в управляющие регистры CRTC. Поэтому убедительная просьба сходить по ссылке и почитать, что это такое (про GTF можно не читать). Тайминги стандартных видеорежимов можно без труда найти в гугле по примерно такому запросу "vga timings 800x600 60hz". Еще раз повторюсь, что тайминги родного пусть даже трижды стандартного режима можно (и нужно) получить из EDID. Теперь о том как получить этот самый EDID. Я потратил на это уйму времени и нашел довольно таки универсальный способ для ati-шных карточек и даже некоторых других (!). Узнал я о нем во время дизассемблирования VGA BIOS карточки. Способ заключается в использование малоизвестной фунции расширения VESA BIOS 4F15h. Читаем VESA BIOS Extensions/Display Data Channel Standard там есть исчерпывающее описание как это делать. Внимательный читатель заметил примечание, что в CX реально заносится номер монитора. В случае успеха прерывание возвращает в регистре AX код 4F (как и все сервисы VBE). От себя лишь приведу пример (Borland C++ под DOS)
      ExpandedWrap disabled
        char edid[128];
        _ES = (unsigned short) ( (unsigned long)&edid >> 16);
        _DI = (unsigned short)&edid;
        _AX = 0x4F15;
        _BL = 0x01;
        _CX = 0;
        asm int 10h;
         
        printf("result = %X", _AX);
         
        FILE *f = fopen("edid.bin", "wb");
        fwrite(edid, 1, sizeof(edid), f);
        fclose(f);

      Думаю, комментарии излишни.
      EDID можно читать непосредственно через регистры, но это очень специфично для каждой карточки: мой код от rv630 ничего не прочитал на rv730. Вот код для rv630 для жутко любопытных читает EDID первого монитора с DDC2. Все регистры описаны описаны в M76 Register Reference Guide. Но все же лучше использовать VBE: EDID можно сохранить до входа в PM32.
      ExpandedWrap disabled
        OUTREG(0x7D30, (1 << 8));
        OUTREG(0x7D64, (1 << 0) | (1 << 12) | (1 << 13) | (128 << 16));
        OUTREG(0x7D74, (0x1 << 31)  | (1 << 8) );
        OUTREG(0x7D30, INREG(0x7D30) | 1);
        while( (INREG(0x7d3c) & (0x1 << 2)) == 0);
        OUTREG(0x7D38, INREG(0x7D38) | (0x1 << 1));
        OUTREG(0x7D74, (0x1 << 31) | (0x1 << 0)  | (3 << 16) );
        BYTE edid[128];
        for(int i = 0; i < sizeof(edid); i++)
        {
            DWORD val = INREG(0x7D74);
            edid[i] = (val >> 8) & 0xFF;
        }
        OUTREG(0x7D30, 0x2);
        Delay(10);
        OUTREG(0x7D30, 0);

      Функция OUTREG - это просто запись двойного слова по соответствующему адресу в MMIO.

      Настройка CRTC для нужного видеорежима

      Дисклемейкер: непосредственная запись некорректных значений в блок регистров CRTC или PLL может поломать ваш монитор или карточку.

      Заниматься экспериментами с регистрами контроллера можно и проще прямо из под Windows. Для этого качаем библиотеку WinIo, все необходимое для использования есть в Гугле, я не буду тут останавливаться на этом. Приведу лишь функции для работы с блоком MMIO регистров.
      ExpandedWrap disabled
        DWORD INREG(DWORD Addr)
        {
            DWORD dwPortVal;
            SetPortVal(IoBase, Addr, 4);
            GetPortVal(IoBase + 4, &dwPortVal, 4);
            return dwPortVal;
        }
         
        void OUTREG(DWORD Addr, DWORD Val)
        {
                SetPortVal(IoBase, Addr, 4);
                SetPortVal(IoBase + 4, Val, 4);
        }

      IoBase можно узнать в диспетчере устройств Windows. Или хитрым способом (узнал из листинга VGA BIOS карточки):
      ExpandedWrap disabled
        GetPortVal(0x3C3, &IoBase, 1);
        IoBase <<= 8;

      Как видим, якобы стандартный VGA регистр 0x3C3 содержит на Ati'шных карточках старшую часть адреса блока расширенных IO регистров (младшая всегда равна 0). С помощью нее VGA BIOS работает c MMIO без перехода в PM32 и иных, более хитрых, способов.
      Регистры CRTC описаны в открытой части документации от AMD, а именно в M76 Register Reference Guide. Но что интересно: регистры PLL не описаны ни где, а без них невозможно сменить видеорежим. Вообще существует достаточно простой способ настойки CRTC для конкретного видеорежима: достаточно сделать дамп регистров в файл из под Windows и загрузить их уже где требуется.
      ExpandedWrap disabled
        static DWORD CrtcReg[] =
        {
            0x00404,
            0x00430,
            0x0043C,
            0x00470,
            0x06000,
            0x06004,
            0x06008,
            0x0600C,
            0x06020,
            0x06024,
            0x06028,
            0x0602C
        };
        DWORD save_crtc[sizeof(CrtcReg)/sizeof(*CrtcReg)];
        for(int i = 0; i < sizeof(CrtcReg)/sizeof(*CrtcReg); i++)
            save_crtc[i] = INREG(CrtcReg[i]);

      Загрузка соответственно:
      ExpandedWrap disabled
        for(int i = 0; i < sizeof(CrtcReg)/sizeof(*CrtcReg); i++)
            OUTREG(CrtcReg[i], save_crtc[i]);

      Примечание: здесь и далее я предполагаю, что перед выполнением переключение в нестандартный видеорежим установлен любой графический (не текстовый) видеорежим через Весу, например 800x600x32, он есть везде. Выход из VGA видеорежима потребует дополнительных строк кода (на данный момент я не занимался этим).
      Можете проверить этот код: видеорежим должен смениться на нужный, вот только в середине на экрана будет отображаться прямоугольник с данными равный по размерам предыдущиму видеорежиму. Это потому, что мы не установили viewport и surface регистры, но там все значительно проще (см. далее).
      Теперь я покажу как рассчитать значения регистров из EDID. Вообще те, кто внимательно читал содержимое моих ссылок сами без проблем смогут это написать.
      ExpandedWrap disabled
        //ищем блок описания "родного" видеорижема
        int i;
        for(i = 54; i < 126; i += 18)
            if(*(WORD*)&edid[i] != 0) //если это именно блок описания видеорежима
               break;
        int new_clock = (int)(*(WORD*)&edid[i]) * 10; //частота в кГц
        constint new_screen_width = edid[i + 2] + ((edid[i + 4] >> 4) << 8);
        const int new_screen_height = edid[i + 5] + ((edid[i + 7] >> 4) << 8);
        const int new_h_blank = edid[i + 3] + ((edid[i + 4] & 0x0F) << 8);
        const int new_v_blank = edid[i + 6] + ((edid[i + 7] & 0x0F) << 8);
        const int new_h_sync_offset = edid[i + 8] + ((edid[i + 11] >> 6) << 8);
        const int new_h_sync_pulse = edid[i + 9] + ( ((edid[i + 11] >> 4) & 0x03) << 8);
        const int new_v_sync_offset = (edid[i + 10] >> 4) + ( ((edid[i + 11] >> 2) & 0x03) << 4);
        const int new_v_sync_pulse = (edid[i + 10] & 0x0F) + ( (edid[i + 11] & 0x03) << 4);
        const int new_h_sync_polarity =  ( (edid[i + 17] & 2) != 0) ? 0 : 1;
        const int new_v_sync_polarity =  ( (edid[i + 17] & 4) != 0) ? 0 : 1;

      Ну и соответственно установка значений:
      ExpandedWrap disabled
        OUTREG(0x6000, new_h_total - 1);
        OUTREG(0x6004, new_start_h_blank | (new_end_h_blank << 16));
        OUTREG(0x6008, new_h_sync_pulse << 16);
        OUTREG(0x600C, new_h_sync_polarity );
        OUTREG(0x6020, new_v_total - 1);
        OUTREG(0x6024, new_start_v_blank | (new_end_v_blank << 16));
        OUTREG(0x6028, new_v_sync_pulse << 16);
        OUTREG(0x602C, new_v_sync_polarity);

      Но только не спешите тестировать. Еще необходимо установить новую частоту генерации пикселов.
      Настройка PLL

      Дисклемейкер: тут описаны мои способы установки значений делителей частоты. Я не претендую на их правильность. Официальных способов это сделать вы все равно не найдете в открытом доступе. По крайней мере на данный момент.

      Для того, чтобы установить частоту генерации пикселов необходимо установить 3 различных делителя частоты.
      1. reference divider (опорный делитель), он содержится в старших 10ти битах регистре 0x0404.
      2. post divider (я не знаю как он правильно переводится) содержится в регистре 0x043C.
      3. feedback divider (делитель обратной связи) содержится в старших 16ти битах регистра 0x0430, а в младших 3х его дробная часть.
      Связано все это дело вот таким соотношением:
      Цитата
      Output frequency == (reference freq * feedback divider) / (reference divider * post divider);

      где reference freq - это постоянное значение для конкретной карточки.
      Казалось бы, что подобрать три этих значения проще простого. Но на деле есть 2 проблемы:
      1. Значения могут быть только целочисленные.
      2. Эти значения могут быть только в определенных пределах для конкретной карточки.
      В исходниках открытого линуксового драйвера я нашел 2 алгоритма подбора этих значений, но после портирования они не дали нужных результатов. Есть еще способ основанный на выполнении функций внутри ATOMBIOS карточки, но для этого надо писать интерпретатор байт-кода, что очень трудоемко. Поэтому я стал наблюдать за логикой работы Виндового драйвера и заметил некоторые закономерности: в первую очередь подстраивается post divider, незначительно feedback divider, reference divider вообще почти не трогается.
      Вот исходя из этого у меня получился вот такой код:
      ExpandedWrap disabled
        const double pixel_clock = (double)( (INREG(0x6000) + 1) * (INREG(0x6020) + 1) * 60) / 1000; //вычисляем текущую частоту генерации в кГц
        const double ref_div = INREG(0x404) & 0x3FF;
        const double fb_div = INREG(0x430) >> 16;
        const double post_div = INREG(0x43C) & 0x7F;
        const double ref_clock = (pixel_clock * ref_div * post_div) / fb_div; //reference freq
        double new_div = (new_clock * ref_div) / ref_clock;
        int new_post_div = fb_div / new_div + 0.5;
        int new_fb_div = new_div * new_post_div + 0.5;
         
        OUTREG(0x430, new_fb_div << 16);
        OUTREG(0x43C, new_post_div);
        //reference divider не трогаем

      Идея основана на том, что все параметры мы можем приближенно вычислить на основе значений для текущего видеорежима и пересчитать по своей стратегии.
      Способ оказался работоспособен на обоих карточках. Но имеет существенный недостаток: output frequency тут с вычисляется погрешностью, т. е. вместо 60Hz (верт. синхронизации естественно) может получиться 61 или 59. Хотя мониторы должны работать с этим. Тот же VBE на моем старом RV350 не давал ровно 60Hz.
      Поэтому я улучшил метод:
      ExpandedWrap disabled
        const DWORD ref_div = INREG(0x404) & 0x3FF;
        const DWORD fb_div = INREG(0x430) >> 16;
        const double new_div = ((double)(new_clock * ref_div)) / (double)ref_clock;
        const DWORD tmp_post_div = ((double)fb_div) / new_div + 0.5;
         
        DWORD new_post_div, new_fb_div;
        int delta;
        unsigned min_delta = -1;
        DWORD pd, fd;
         
        if(tmp_post_div > 2)
            pd = tmp_post_div - 1;
        else
            pd = tmp_post_div;
         
        for(; pd <= (tmp_post_div + 3); pd++)
        {
            DWORD tmp_fb_div = new_div * pd + 0.5;
            if(tmp_fb_div > 1)
                 fd = tmp_fb_div - 1;
            else if(tmp_fb_div == 1)
                 fd = tmp_fb_div;
            else
                 continue;
            for(; fd <= (tmp_fb_div + 1); fd++)
            {
                 delta = new_clock - (int)(((double)(ref_clock * fd)) / (ref_div * pd) + 0.5);
                 if(delta < 0)
                      delta = -delta;
                 if((unsigned)delta < min_delta)
                 {
                      min_delta = delta;
                      new_post_div = pd;
                      new_fb_div = fd;
                 }
            }
        }
        OUTREG(0x430, new_fb_div << 16);
        OUTREG(0x43C, new_post_div);

      Этот код тоже работает на rv630 и на rv730. Идея та же, только теперь параметры подбираются в окрестности и выбирается наилучшее сочетание. Но тут используется refence clock полученный из ATOMBIOS видеокарточки. Конкретно структуры ATOMBIOS смотрите тут. Нужное значение содержится в ATOM_FIRMWARE_INFO (usReferenceClock), там только нужно проследить как к нему добраться из корневого каталога. ATOMBIOS расположен в сегменте 0xC0000 (это и есть VGA BIOS видеокарточки), все ссылки из структуры в структуры абсолютные относительно 0xC0000. Заголовок расположен соответственно 0xC0000 + OFFSET_TO_POINTER_TO_ATOM_ROM_HEADER, корневой каталог данных соответственно 0xC0000 + ATOM_ROM_HEADER.usMasterDataTableOffset и так далее. У RV6XX и у RV7XX usReferenceClock обычно равен 27000.

      Настройка Surface и ViewPort

      Тут все просто. После того, как мы настроили CRTC нужно установить ViewPort.
      По факту 2 строчки:
      ExpandedWrap disabled
        OUTREG(0x652C, ScreenHeight);
        OUTREG(0x6584, ScreenHeight| (ScreenWidth << 16));

      Насколько я понял, ViewPort определяет область на экране, которую захватывает Surface. Опять же описания регистров ViewPort в официальных источниках я не нашел.
      Упрощенно Surface можно рассматривать как поверхность, куда отображается видимая видеопамять. Все регистры описаны в RV630 Register Reference Guide, глава Display Controller Registers. Перечислю просто главные из них:
      D1GRPH_CONTROL - можно установит bpp.
      D1GRPH_PITCH - логическая длина строки. После утсановки видеорежима стандартными прерываниями она обычно равна ScreenWidth. Но вы можете округлить до 2n, чтобы с видеопамятью было удобнее работать. То же самое можно сделать через шестую функцию Весы.
      D1GRPH_X_END = ScreenWidth,
      D1GRPH_Y_END = ScreenHeight.

      Все регистры рассмотренные в этой статье предназначены для первого монитора. Для второго все аналогично, только номера регистров другие (недокументированные можно найти в исходниках линуксового драйвера), но формат тот же самый. В заключение дам ссылку на руководство программиста R128, спустя почти 15 лет AMD таки его открыла. Имеет смысл почитать для ознакомления, а для старых Radeon'ов (до RV5xx) многое из того, что там описано является актуальным. По сути большая часть первой части статьи раскрывается в этом документе.

      первая часть (поправил исходники)
      Сообщение отредактировано: shm -
      0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
      0 пользователей:


      Рейтинг@Mail.ru
      [ Script execution time: 0,0362 ]   [ 15 queries used ]   [ Generated: 19.03.24, 02:24 GMT ]