Статья Системные таймеры, Часть[2] – ACPI

Содержание:

1.
Общие сведения. Legacy-таймеры PIT и RTC.
2. Шина PCI – таймер менеджера питания ACPI.
3.
Усовершенствованный таймер HPET.
4. Счётчики процессора LAPIC и TSC (time-stamp-counter).
5. Win - профилирование кода.
---------------------------------------

В первой части рассматривались общие сведения и унаследованные девайсы..
В этой части познакомимся с шиной PCI и более совершенными таймерами наших дней..



ACPI – Advanced Configuration and Power Interface
частота: 3.579545 MHz | счётчик: 24-бит


ACPI пришёл на смену APM (Advanced Power Managament) и представляет собой расширенный интерфейс для настройки и управления питанием аппаратных средств компьютера (актуальна версия 6.3 ). Интерфейс имеет встроенный таймер, по старинке называемый РМ-таймером. Частота 3.5795 MHz, что в 4-раза меньше опорной 14,318. Имеет 24-битный счётчик, продолжающий считать даже в некоторых режимах энергосбережения – это выгодно отличает его от всех остальных таймеров в системе.

Контролёр ACPI вобрал в себя три таймера – собственно PM для управления питанием, таймер GPIO (General Purpose IO) и сторожевой watchdog-таймер TCO (прослушивает систему на зависание). Последние два генерируют прерывания SMI (System Managament Interrupt), которые считаются аварийными. Получив SMI процессор немедленно переходит в самый приоритетный свой режим работы SMM, так-что от этих таймеров лучше держаться подальше – мы остановимся лишь на безобидном РМ. Он управляет схемами питания, которые можно просмотреть по powercfg /? в командной строке.

ACPI зарыт глубоко ..и сверху ещё присыпан добрым слоем щебёнки. Нужно приложить немало усилий, чтобы среди паутины шин и устройств раскопать его регистры в системном пространстве. В частности без базовых знаний архитектуры тут не обойтись, поэтому вскроем чипсет как консервную банку и заглянем к нему в душу.
-------------------


Конфигурационное пространство PCI

До недавнего времени, связь буквально всех устройств поддерживала параллельная шина PCI. 32-битной своей артерией она пересекала всю мат.плату сверху-донизу. Более того, со-временем эта шина сама превратилась в устройство, со-своим адресным пространством в ОЗУ. Этот регион памяти назвали , где собраны паспорта всех девайсов, включая выделенные им ресурсы, порты и прерывания. Из общего пула данной памяти, под характеристики каждого из устройств выделяется ровно 256 байт – сколько устройств, столько и 256-байтных блоков.

На плате имеются десятки контролёров, поэтому их сортируют по классу. Например все 32-битные девайсы вешают на одну PCI-шину, далее вставляют разделительный мост PCI-to-PCI Bridge и получают вторую шину, которую можно отдать в распоряжении менее скоростных устройств с иным протоколом обмена. Потом добавить ещё мост и открыть доступ абонентам с горячим подключением и т.д. Такое разделение одной шины превращает её в универсальную, где конкретное железо адресуется в формате: шина, устройство на шине, и функция этого устройства – в графическом представлении это выглядит примерно так:


pci-bus.png


В терминологии PCI, "функция" определяет реальный девайс типа жёсткий диск, а "устройство" – это его контролёр (может быть многофункциональным). Спецификация ограничивает общее число: шин на мат.плате =256 (0xFF), устройств на одной шине =32 (0x1F), функций у одного девайса =8.

Но прогресс не стоит на месте и довольно скоро быстродействия пар-шины PCI стало не хватать. Так в архитектуре наших дней её заменили на последовательную PCI-Express с полной переработкой чипсета – на место двойки GMCH и ICH, пришёл один PCH. Теперь имеется центральный Root-Complex, посредством которого девайсы могут общаться между-собой напрямую, ..то-есть получили соединение "Peer-to-Peer" (точка-точка), где каждое устройство считается конечным "Endpoint". Программирование таких чипсетов сводится к поиску базового адреса центрального рута, через который сможем подобраться к любому девайсу чипсета. Отметим, что прежняя схема Bus:Dev:Func тоже жива, только сброшена в подвал на растерзание Legacy-устройствам:


pcie-bus.png



Поиск ACPI-контролёра

Но вернёмся к конфигурационному пространству PCI, которое осталось прежним..
Чтобы найти ACPI-контролёр среди груды системного металлолома, сначала нужно узнать шину на которой он висит, а это только через мост. В конфигурационном пространстве данного моста будут прописаны базовые адреса всех девайсов, за которых он отвечает.

Такая цепочка создаёт определённую путаницу, поэтому советую скачать утилиту
. Этот зверь достанет инфу о любом корневом устройстве на шине PCI, причём в очень наглядном виде со-значениями всех полей выделенного ему 256-байтного блока. Вот как выглядит топология на моей экспериментальной машине:

pci_scope_0.png


Чипсет у меня бородатый ICH7, который при опытах не жалко потерять. на него даташит и маской поиска "ACPI Base" обнаруживаю, что ACPI-контролёр висит на шине LPC (Low-Pin-Count). Значит в общем кфг-пространстве нужно будет найти мост PCI-to-LPC и прочитать его 256-байтный блок регистров. На рис.выше тулза дала уже мне наводку на него в виде Bus[00]:Dev[1F]:Func[00], и остаётся только жмякнуть по ней мышью:

pci_scope_1.png


Теперь покончим с теорией шин и перейдём к софт-поиску нужного моста.
Для этого BIOS имеет специальный сервис реального режима INT-1Ah, а халёный EFI его эмулирует:

pci-bios.png


Для доступа к шине PCI выделены два системных порта – 0CF8h (конфигурация) и 0CFCh (данные), так-что можно работать с шиной и через эти порты. Однако готовый сервис PCI-BIOS избавляет нас от кропотливой возни с битами, хотя и решает не все задачи. Но в большинстве случаях указанных функций бывает достаточно, чтобы (без шума и пыли) найти и прочитать данные из конфигурационного пространства устройств.

Чтобы не привязывать софт к конкретному чипсету, обычно функцией AX=B103h ищут нужное устройство по его классу. У гугла есть дока
, где перечислены классы всех системных девайсов. Фрагмент из этой доки для мостов Bridge представлен ниже, а базу вендоров VEN-DEV можно стянуть от сюда:

pci-codes.png


Теперь у нас есть класс LPC-моста = 0х060100, и можно получить его координаты Bus:Dev:Func примерно так..

C-подобный:
       mov   eax,0xB103     ;// поиск устройства по классу
       mov   ecx,0x060100   ;// класс\подкласс\интерфейс
       xor   esi,esi        ;// искать без цикла один девайс
       int   1Ah            ;// зовём сервис PCI-BIOS !!!
       jc    @error         ;// ошибка, если флаг CF=1.
;// ---------------
;// иначе: в регистре ВХ лежат B:D:F Lpc-моста,
;// что позволит читать данные из его cfg-пространства.

Описание полей 256-байтного блока LPC-моста указывается в даташите на чипсет. Обратите внимание, что во-избежании путаницы с hex-числами A-F, Intel представляет номер шины, устройства и функции в 10-тичном, т.е. под D31:F0 нужно понимать Dev[1F]:Func[00]

LPC_reg.png


Здесь видно, что по смещению 40h лежит базовый адрес контролёра ACPI, младший бит которого является флагом достоверности и не учитывается. В данном случае – это номер порта I/O, через который инструкциями IN/OUT мы и будем вести диалог с контролёром. Таким образом, теперь у нас есть PMBASE и можно приступать к чтению/записи непосредственно регистров ACPI.

В таблице ниже приводятся номера этих регистров относительно его базового порта. Мы можем получить статус контролёра, залезть в его регистр конфигурации PM1-Control, или через регистр 08h получить доступ к ACPI-таймеру. В доке, к каждому из этих регистров следом прилагается своя таблица битов, и нам остаётся только взводить или сбрасывать их, в зависимости от поставленной задачи:

acpi.png


Здесь видно, что счётчик таймера ACPI имеет разрядность 24-бита, при этом делителя у него нет – он всегда работает на одной частоте 3.579545 MHz. Если в регистре "PM1-Enable" взведён бит нуль, то таймер может генерить прерывания системного контроля SCI (System-Control-Interrupt).



Программирование ACPI-таймера

Проделав этот тернистый путь попробуем написать программу, которая подведёт черту под вышесказанным. Это в теории всё мутно, зато на практике реализовать поиск любого контролёра достаточно просто, ..но только опять-же – если знаешь теорию. Вот пример, в котором комментов больше, чем самого кода. Алгоритм такой:

1. Проверяем поддержку сервиса PCI-BIOS – выходим, если нет.
2. Организуем поиск моста PCI-to-LPC по его классу 0х060100.
3. Получив Bus:Dev:Func моста, читаем из его кфг-пространства регистр 40h – PMBASE.
4. Теперь у нас есть базовый порт ACPI-контролёра, и можно читать с него любые регистры.
5. Выводим всю инфу на консоль.
C-подобный:
;// fasm (Real-Mode)
;// Читает регистры ACPI-контролёра
;// тестировалось на чипсете "Intel ICH7"
;// -------------
org 100h
jmp start

caption  db  13,10,' ACPI info v.01 '
         db  13,10,' ======================'
         db  13,10,' Support PCI-BIOS.: $'
totalBus db  13,10,' Total PCI-bus....: $'
find     db  13,10
         db  13,10,' Find LPC bridge... $'
lpcInfo  db  13,10,'    PCI-Bus.......: $'
device   db  13,10,'    PCI-Device....: $'
func     db  13,10,'    PCI-Function..: $'
vid      db  13,10,'    VendorID......: $'
did      db  13,10,'    DeviceID......: $'
acpiInfo db  13,10
         db  13,10,' ACPI controller'
         db  13,10,'    I/O port......: 0x$'
rcBar    db  13,10,'    Root-Complex..: 0x$'
acpiReg  db  13,10
         db  13,10,' ACPI registers'
         db  13,10,'    PM1 status....: $'   ;//регистр(00h)
pmEn     db  13,10,'    PM1 enable....: $'   ;//регистр(02h)
pmCtl    db  13,10,'    PM1 control...: $'   ;//регистр(04h)
pmTmr    db  13,10,'    Timer value...: $'   ;//регистр(08h)
cpuCtl   db  13,10,'    CPU control...: $'   ;//регистр(10h)
smiCtl   db  13,10,'    SMI ctl_enable: $'   ;//регистр(30h)
smiStat  db  13,10,'    SMI status....: $'   ;//регистр(34h)

;// Таблица для расшифровки Device-ID.
;// старший байт DevID совпадает с типом чипсетов ICH6-9.
devTable dw  0x2600,i6,0x2700,i7,0x2800,i8
         dw  0x2900,i9,0x3A00,i10,0x3B00,pch
dtLen    =   ($ - devTable)/4

i6       db  ' = ICH6 $'
i7       db  ' = ICH7 $'
i8       db  ' = ICH8 $'
i9       db  ' = ICH9 $'
i10      db  ' = ICH10. Chipset not support $'
pch      db  ' = PCH. Chipset not support $'
unkNown  db  ' = Unknown bridge $'
;//=====================================================
mError   db 'ERROR!$'
ok       db 'OK! $'
bdf      dw  0
pmBase   dd  0
rcba     dd  0
;//--------

start:
       mov   dx,caption     ;// шапка
       call  Message        ;//

;//--- Проверяем поддержку fn.PCI-BIOS
       mov   ax,0xB101      ;// fn.0xB101
       xor   edi,edi        ;//
       int   1Ah            ;// зовём сервис!
       or    ah,ah          ;//
       jz    @f             ;// ОК, если AH=0
       call  ERROR          ;// иначе: ошибка.

@@:    push  cx             ;// запомнить кол-во шин
       mov   dx,ok          ;// есть PCI-BIOS!
       call  Message        ;//

       mov   dx,totalBus    ;// всего шин.
       call  Message        ;//
       pop   ax             ;// снять их со-стека
       and   ax,0xff        ;// оставить только мл.байт
       inc   al             ;// +1, т.к.отсчёт с нуля
       mov   bx,10          ;// система счисления
       call  Hex2Asc        ;// Print-AX
;//-----------------------------------------------
;//--- Поиск моста "PCI-to-LPC" по его классу ----
;// Возвращает в регистр ВХ [bus:dev:func]
;// BH=Bus, BL[7:3]=Device, BL[2:0]=Function.
;//-----------------------------------------------
       mov   dx,find        ;//
       call  Message        ;//
       mov   ax,0xB103      ;// fn.0xB103
       mov   ecx,0x060100   ;// класс\подкласс\интерфейс
       xor   si,si          ;// искать без цикла 1 устройство
       int   1Ah            ;// Найти!
       jnc   @f             ;// если флаг CF=0, то Forward (вперёд)
       call  ERROR          ;// иначе: ошибка

@@:    mov   [bdf],bx       ;// запомнить выхлоп "Bus:Dev:Func"
       push  bx bx bx       ;// в стек их, для вывода на консоль.
       mov   dx,ok          ;// мессага "Найдено"!
       call  Message        ;//
;// Выводим эти данные на консоль
;//  == Bus ==
       mov   dx,lpcInfo     ;//
       call  Message        ;//
       pop   ax             ;//
       shr   ax,8           ;// оставляем только ст.часть[15:8]
       mov   bx,16          ;// система счисления
       call  Hex2Asc        ;//
;// == Device ==
       mov   dx,device      ;//
       call  Message        ;//
       pop   ax             ;//
       xor   ah,ah          ;//
       shr   ax,3           ;// оставляем биты[7:3]
       mov   bx,16          ;//
       call  Hex2Asc        ;//
;// == Function ==
       mov   dx,func        ;//
       call  Message        ;//
       pop   ax             ;//
       and   ax,111b        ;// оставляем биты[2:0]
       mov   bx,16          ;//
       call  Hex2Asc        ;//
;//-----------------------------------------
;//--- Читаем из кфг.пространства LPC-моста
;//--- его идентификаторы VEN\DEV
;//-----------------------------------------
       mov   ax,0xB10A      ;// fn= читать двордами (по 4-байт)
       mov   bx,[bdf]       ;// от куда "Bus:Dev:Func"
       xor   di,di          ;// адрес = 0 (offs.от начала)
       int   1Ah            ;//
       push  ecx ecx        ;// запомнить VEN\DEV для вывода
;//--- Читаем поле "APCI-base" (порт контролёра)
       mov   ax,0xB10A      ;// размер = 4-байта
       mov   bx,[bdf]       ;// координаты
       mov   di,0x40        ;// смещение 40h от начала LPC-cfg
       int   1Ah            ;//
       dec   ecx            ;// сбросить бит валидности адреса.
       mov   [pmBase],ecx   ;// запомнить в переменной.
;//--- Читаем поле "Root-Complex BA"
       mov   ax,0xB10A      ;//
       mov   bx,[bdf]       ;//
       mov   di,0xf0        ;// смещение F0h
       int   1Ah            ;//
       mov   [rcba],ecx     ;// запомнить RCBA.
;//----------------------------------------
;//--- Выводим нарытую инфу на консоль ----
;//----------------------------------------
;// *** Vendor-ID ***
       mov   dx,vid         ;//
       call  Message        ;//
       pop   eax            ;// снимаем со-стека считанное
       movzx eax,ax         ;// обнулить старшую часть
       mov   ebx,16         ;// система-счисления
       call  Hex2Asc        ;// Print-AX
;// *** Device-ID ***
       mov   dx,did         ;//
       call  Message        ;//
       pop   eax            ;//
       shr   eax,16         ;// обнулить младшую часть
       push  ax             ;// запомнить DevID
       mov   ebx,16         ;//
       call  Hex2Asc        ;// Print его на консоль.
;// Определяем тип моста по его DevID
       pop   bx             ;//
       and   bx,0xFF00      ;// очистить мл.байт
       mov   esi,devTable   ;// указатель на таблицу
       mov   ecx,dtLen      ;//   ..её длина
@@:    lodsd                ;// берём в EAX сразу 4-байта из SI
       cmp   ax,bx          ;// ищем совпадения младшего слова
       je    @f             ;//   ..(в старшем лежит адр.мессаги)
       loop  @b             ;// повторить, если не равно
       mov   dx,unkNown     ;// нет совпадений.
       call  Message        ;//
       jmp   @acpi          ;//
                            ;// Нашли DevID в своей базе!
@@:    shr   eax,16         ;// сместить адрес текста в АХ
       mov   dx,ax          ;// вывести его на консоль.
       call  Message        ;//

;// *** ACPI Base Address ***
@acpi: mov   dx,acpiInfo    ;//
       call  Message        ;//
       mov   eax,[pmBase]   ;//
       mov   ebx,16         ;//
       call  Hex2Asc        ;// Print-ЕAX
;// *** Root Complex Base Addr ***
       mov   dx,rcBar       ;//
       call  Message        ;//
       mov   eax,[rcba]     ;//
       mov   ebx,16         ;//
       call  Hex2Asc        ;// Print-ЕAX
;//-----------------------------------------------------------
;//--- В переменной "PMBASE" лежит номер порта ACPI-контролёра
;//--- читаем его регистры на консоль ------------------------
;//-----------------------------------------------------------
       mov   dx,acpiReg     ;//
       call  Message        ;//
       mov   edx,00h        ;//  ..регистр в нём "PM1_Status"
       call  ReadAcpiPort   ;//
       mov   ecx,16         ;// кол-во бит для вывода
       call  Bin2Asc        ;// Print-AX в бинарном виде!

       mov   dx,pmEn        ;//
       call  Message        ;//
       mov   edx,02h        ;//  ..регистр в нём "PM1_Status"
       call  ReadAcpiPort   ;//
       mov   ecx,16         ;//
       call  Bin2Asc        ;//

       mov   dx,pmCtl       ;//
       call  Message        ;//
       mov   edx,04h        ;//  ..регистр в нём "PM1_Status"
       call  ReadAcpiPort   ;//
       mov   ecx,32         ;// кол-во бит для вывода
       call  Bin2Asc        ;// Print-EAX в бинарном!

       mov   dx,pmTmr       ;//
       call  Message        ;//
       mov   edx,08h        ;//  ..регистр в нём "PM1_Status"
       call  ReadAcpiPort   ;//
       mov   ebx,10         ;// система-счисления для вывода
       call  Hex2Asc        ;// Print-EAX в 10-тичном!

       mov   dx,cpuCtl      ;//
       call  Message        ;//
       mov   edx,10h        ;//  ..регистр в нём "PM1_Status"
       call  ReadAcpiPort   ;//
       mov   ecx,32         ;//
       call  Bin2Asc        ;//

       mov   dx,smiCtl      ;//
       call  Message        ;//
       mov   edx,30h        ;//  ..регистр в нём "PM1_Status"
       call  ReadAcpiPort   ;//
       mov   ecx,32         ;//
       call  Bin2Asc        ;//

       mov   dx,smiStat     ;//
       call  Message        ;//
       mov   edx,34h        ;//  ..регистр в нём "PM1_Status"
       call  ReadAcpiPort   ;//
       mov   ecx,32         ;//
       call  Bin2Asc        ;//

@exit:                      ;//
       xor   ax,ax          ;//
       int   16h            ;//
       int   20h            ;//

;//===============================
;// === ДОП.ФУНКЦИИ ==============
ReadAcpiPort:        ;//<------ на входе: EDX = регистр.
       add   edx,[pmBase]   ;// добавить базовый порт.
       in    eax,dx         ;// считать данные в ЕАХ
retn
Hex2Asc:             ;//<------ Перевод чисел в символы
       xor   ecx,ecx        ;// описание смотри,
isDiv: xor   edx,edx        ;//   ... в первой программе.
       div   ebx            ;//
       push  edx            ;//
       inc   ecx            ;//
       or    eax,eax        ;//
       jnz   isDiv          ;//
isOut: pop   eax            ;//
       cmp   al,9           ;//
       jle   noHex          ;//
       add   al,7           ;//
noHex: add   al,30h         ;//
       int   29h            ;//
       loop  isOut          ;//
retn
Bin2Asc:             ;//<------ Вывод в BIN
       mov   ebx,eax        ;// взять число в EBX
       or    ebx,ebx        ;// проверить его на нуль
       jne   @f             ;// продолжить, если больше -----+
       mov   al,'0'         ;// иначе: вывести 0             |
       int   29h            ;//  ..и возвратить управление.  |
       retn                 ;//                              |
@@:    cmp   cx,32          ;// проверить аргумент на 32 <---+
       je    @f             ;// если равно..
       shl   ebx,16         ;// иначе: число влево на 16-бит
@@:    mov   al,'0'         ;// в дефолте выводить нуль
       shl   ebx,1          ;// выталкиваем по 1-биту влево..
       jnc   okey           ;// если CF=0
       inc   al             ;// иначе: +1
okey:  int   29h            ;// вывести бит на консоль
       loop  @b             ;// промотать цикл СХ-раз
retn
Message:             ;//<------ Вывод строк
       mov   ah,9           ;//
       int   21h            ;//
retn
ERROR: pop   ax      ;//<------ снять адрес возврата
       mov   dx,mError      ;// мессага
       call  Message        ;//
       jmp   @exit          ;// на выход!

readAcpi.png


Программу запускал в реальном режиме под чистым досом. Поскольку клавиша "Prn-Screen" в нём не работает, скрины делал внешней утилитой "snarf.exe", которую прикрепил в скрепке. Там-же есть и софтина для создания загрузочных юсби-брелков.

Здесь я специально вывел регистры в бинарном виде, чтобы показать рычаги управления контролёром ACPI на программном уровне. Каждый бит его регистров вкл/выкл какую-то опцию, и среди этих опций есть весьма не безобидные. Например через регистр "CPU-Control" можно вогнать процессор в режим троттлинга, после чего чипсет начнёт посылать ему холостые такты считая, что он загружен работой по самые помидоры – это приведёт к жутким тормозам. Все "прелести" менеджера питания РМ можно найти в даташите на чипсет, и описывать их тут не имеет смысла.

У этого таймера имеются все шансы стать эталоном времени в системе. При частоте 3.579545 MHz его разрешение: 1/3579545 =279 ns, а это вполне приличная единица. Однако имеется одна проблема, из-за которой система относится к нему насторожено – это обвальное падение производительности при высокой загруженности шины PCI. В этом случае РМ-таймер не успевает во-время отправлять свои тики по шине, и хотя они при этом не пропадают, счётчик на приёмной стороне обновляется рывками, в промежутке которых может произойти что-то важное. По этой причине самым надёжным таймером считается тот, который физически расположен ближе к процессору, например LAPIC или TSC.

Что касается интерфейса ACPI в целом, так это огромный механизм с кучей настроек, разобраться в которых не каждому по зубам. В хардвейрных его таблицах BIOS можно найти много интересного, в том числе и OEM-ключ активации Win7-10. В общем тема эта не для части одной статьи, а для двупудового тома. Встретимся в следующей части, где ждёт обзор таймера HPET.
 

Вложения

  • SOFT.ZIP
    525,8 КБ · Просмотры: 594
Последнее редактирование:

Vitaliy_Simpson

One Level
18.04.2020
2
2
BIT
0
Очень круто, спасибо огромное!

Но выражение "в общем" пишется исключительно раздельно и с одной буквой 'о'.
Извините меня, не удержался.
Снова благодарю за статью. Безумно интересно!
 
  • Нравится
Реакции: Сергей Попов
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!