Ввод/Вывод: поллинг и MMIO

<!> не забыть фанерную клавиатуру «COKPOWEHEU»

Очевидно, что управление всем, что подключено к компьютеру (включая пользователя ☺), должен взять на себя компьютер. Это означает, что

Внешние устройства

Внешнее устройство (также периферийное) — любая аппаратура, которая обменивается данными с ЭВМ.

Задачи:

⇒ ВУ может быть не сложнее трёх проводов с кнопкой, а может быть целым специализированным компьютером со своим процессором, памятью, регистрами и т. п.

Способы взаимодействия с ВУ на разных архитектурах

MMIO-регистры бывают:

Стоит заметить, что содержимое MMIO далеко не всегда однозначно соответствует состоянию внешнего устройства: например, запись в управляющий регистр может поменять значение статусного, а значение самого управляющего регистра иногда вообще недоступно или не меняется, чтение данных приводит внешнее устройство в состояние готовности и т. п.

Обычно процессор использует для доступа к MMIO контроллер памяти

Аппаратные задачи MMIO

Барьеры памяти

(соблюдение порядка и актуальности доступа)

RISC-V: инструкции типа fence. Общая идея:

Отличие механизма MMIO от CSR

⇒ В спецификацию процессора RISC-V MMIO почти не входит:

MMIO и абсолютная адресация

Привязка функций компьютера к конкретным адресам оперативной памяти — не нарушение ли принципа «все адреса в программе RISC-V — относительные»? На самом деле мы чересчур упростили формулировку самого принципа, так что нет, не нарушение.

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

Однако в случае MMIO задача в каком-то смысле противоположная: в какое бы место памяти ни загрузилась программа, адреса регистров внешних устройств должны оставаться одинаковыми — они-то заранее указаны в документации к соответствующей периферии.

Забегая вперёд заметим, что механизм виртуальной памяти позволяет сопоставить произвольный блок физической памяти с заданным диапазоном адресов в адресном пространстве процесса — но если окружение вообще разрешает процессу доступ к MMIO, то виртуальный адреса, которые увидит процесс, будут, скорее всего, совпадать с теми, что в документации (иначе как он про них узнает).

Взаимодейстие на основе опроса

Поллинг (polling, опрос) — способ работы с внешними устройствами, при котором программа регулярно проверяет готовность устройства к В/В, и если готовность есть, осуществляет соответствующую операцию.

Организация поллинга из программы:

  1. Подготовка устройства к работе
  2. Цикл
    1. Проверка готовности устройства
  3. Если устройство готово
    • Операция В/В
    • Выход из цикла
    1. Если устройство не готово
      • Бессмысленное ожидание
  4. Перевод устройства в исходное состояние

Замечание: пункт «бессмысленное ожидание» (бессмысленная трата процессорного времени!) можно было бы заменить на «выполнение полезных действий», но:

(срочно нужны прерывания, но они в следующей лекции)

Цифровой блок RARS

Цифровой блок «Digital Lab Sim» — это воображаемое внешнее устройство для RARS, позволяющее потренироваться в организации ввода и вывода. В учебно-тренировочных целях протокол управления цифровым блоком сделан максимально неудоб-приближенным к обычной практике разработки таких устройств.

Выглядит в работе оно так:

LecturesCMC/ArchitectureAssembler2024/08_Input_Output/Digilab.png

0xFFFF0010

command right seven segment display

Биты правого цифрового индикатора (запись)

0xFFFF0011

command left seven segment display

Биты левого цифрового индикатора (запись)

0xFFFF0012

command row number / enable keyboard interrupt

Номер ряда клавиатуры для опроса (биты 0-3)
разрешение прерываний от клавиатуры (7 бит) (запись)

0xFFFF0013

counter interruption enable

Разрешение таймерного прерывания № 0x100 один раз в 30 инструкций (запись)

0xFFFF0014

receive row and column of the key pressed

Результат опроса: бит столбца (7-4), бит ряда (3-0), если клавиша активна (чтение)

Чтобы RARS «увидел» устройство, нужно «подключить» его нажатием кнопки «Connect to program».

Если записать байт в регистр 0xFFFF0010, на правом индикаторе загорятся красным некоторые сегменты, а некоторые станут серыми. Сегментов всего семь, восьмая — точка, так что каждый бит байта отвечает за свою лампочку. Запись 0 погасит все сегменты, запись 0xff — зажжёт. Аналогично для регистра 0xFFFF0011 и левого индикатора. Прочитать содержимое индикатора нельзя.

Например, при выполнении следующего кода:

   1     lui t6 0xffff0             # база MMIO сдвиг << 12
   2     li  t1 0xdb
   3     sb  t1 0x10(t6)
   4     li  t2 0x66
   5     sb  t2 0x11(t6)

Получим вот такой результат ☺ :

Если с выводом в цифровые окошки всё более-менее понятно, какие биты каким сегментам соответствуют?), то ввод с клавиатуры на первый взгляд кажется совершенно эзотерическим:

Дело в том, что это устройство спроектировано «как в жизни». Предполагается, что в клавиатуре есть всего 8 проводов (как в матрице памяти) – 4×4 – и всё, что можно сделать — это подать напряжение на один из горизонтальных проводов и отобразить, на какой вертикальный провод он замкнут, если соответствующая клавиша нажата.

Сравнительно несложно обеспечить соответствие провода, на котором обнаружилось напряжение, отдельному биту в регистре, а также произвести простейшие операции над этими битами. Устройство могло выглядеть, например, так:

Однако более сложные логические цепочки — шифратор (для превращения провода N в двоичное число), дешифратор (обратно), сумматор и т. п. — в подобных «железках» обычно отсутствуют.

Вот и выходит, что задача превратить «сырые» данные устройства в осмысленные ложится на программу.

TODO: сфотографировать, а лучше принести на лекцию фанерную клавиатуру.

(нажмите «Комментарии» в шапке страницы, чтобы прочитать комментарии от COKPOWEHEU)

Итак, для того, чтобы просканировать, нажата ли какая-нибудь клавиша в ряду «0-1-2-3», надо:

  1. «Подать напряжение на нулевую строку», то есть записать в 0xffff0012 число, у которого только нулевой бит равен 1 (это число 1)

  2. Считать из регистра 0xffff0014 значение. Если клавиша в нулевом ряду не нажата, вернётся 0, если нажата, вернётся число, в котором

    • установлен в 1 ровно один из первых четырёх битов, соответствующий нулевой строке (так же, как в операции сканирования)
    • установлен в 1 ровно один из битов 4…7, в соответствие со столбцом, в котором находится нажатая клавиша (0x10,0x20,0x40 и 0x80 для клавиш «0», «1», «2» и «3» соответственно)

    • например, для клавиши «2» ответ будет 0x41

  3. Кусок кода при этом может выглядеть так:
       1         li      t0 1                   # первая строка
       2         sb      t0 0xffff0012 t1       # «подаём напряжение»
       3         lb      t0 0xffff0014          # забираем результат
    
  4. Для «подачи напряжения» на другие строки («4-5-6-7», «8-9-a-b» или «c-d-e-f») в 0xffff0012 надо записывать 2, 4 или 8. Из 0xffff0014 будет считываться число, у которого первые четыре бита установлены аналогично (если нажата клавиша в соответствующем ряду, иначе считывается 0)

  5. Если записать в 0xffff0012 число, отличное от 1,2,4 или 8, вернётся всегда 0 (более умное устройство подало бы напряжение на провод «ошибочная операция», который можно было бы прочитать в регистре статуса)

    • (нажмите «Комментарии» в шапке страницы, чтобы прочитать комментарии от COKPOWEHEU)

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

   1 .macro  scan    %result %raw %rbase
   2         li      t0 %raw                 # номер ряда
   3         sb      t0 0x12(%rbase)         # сканируем ряд
   4         lb      t0 0x14(%rbase)         # забираем результат
   5         or      %result %result t0      # добавляем биты в общий результат
   6 .end_macro
   7 .macro  bprint  %reg
   8         mv      a0 %reg                 # выведем результат как двоичное
   9         li      a7 35
  10         ecall
  11         li      a0 '\n'
  12         li      a7 11
  13         ecall
  14 .end_macro
  15 .text       
  16         lui     t6 0xffff0              # база MMIO 0xffff000000
  17         mv      t5 zero                 # счётчик
  18         mv      t4 zero                 # предыдущее значение
  19 loop:   scan    t1 1 t6                 # сканируем первый ряд
  20         scan    t1 2 t6                 # сканируем второй ряд
  21         scan    t1 4 t6                 # сканируем третий ряд
  22         scan    t1 8 t6                 # сканируем четвёртый ряд
  23         beq     t1 t4 same
  24         sb      t1 0x10(t6)             # запишем результат в биты окошка
  25         addi    t5 t5 1                 # счётчик
  26         sb      t5 0x11(t6)             # запишем его в другое окошко
  27         bprint  t1
  28         mv      t4 t1
  29 same:   li      t2 10
  30         ble     t5 t2 loop
  31         li      a7 10
  32         ecall

Более развесистый пример от COKPOWEHEU

Последовательность внутри цикла — команды запуска считывания соответствующего ряда и аккумуляции считанных значений. Программа выполняет поллинг, но без заполнения промежутков между опросами устройства «бессмысленным ожиданием»: после каждого опроса немедленно начинается следующий. На практике такие программы начинают потреблять очень много процессорного времени, не делая почти ничего (пользователь — самое медленное на свете устройство ввода ☺). Чтобы ожидание меньше нагружало процессор, можно использовать внешний вызов sleep (32), который передаёт управление операционной системе на указанный в миллисекундах период. Может, хоть окружение в это время будет делать что-то полезное?

Графический дисплей

Графический дисплей RARS (Bitmap Display) представляет собой воображаемое внешнее устройство RARS, состоящее из единственной области памяти, целиком отображённой с помощью MMIO в адресное пространство RARS (по умолчанию — зачем-то на начало статических данных 0x10010000, но есть и другие варианты).

LecturesCMC/ArchitectureAssembler2024/08_Input_Output/Bitmap.png

Запись машинного слова 0x00RRGGBB по адресу Base Address + Offset приведёт к появлению на экране точки цвета #RRGGBB в цветовом пространстве RGB. Подробнее про цветовой пространство RGB можно прочитать в Википедии, там же есть ссылка на таблицу цветов HTML.

Ширина и высота экрана задаются в точках на экране компьютера. Один пиксель Bitmap-устройства (unit) представляет собой прямоугольник из точек экрана. Если он равен одной точке (Unit Width × Unit Length — это 1×1), количество пикселей в видеопамяти устройства совпадает с количеством пикселей в соответствующей области экрана. Если размеры пикселя увеличивать (не забываем нажать кнопку «Reset»), он превратится в видимый прямоугольник, а общий объём видеопамяти пропорционально сократится.

Объём потребляемой памяти определяется так (*4 — потому что один пиксель задаётся машинным словом длиной в 4 байта):

Координаты точки со смещением Offest вычисляются так

Обратно, смещение точки с координатами X,Y вычисляется так:

Пример: классическая программа, рисующая «звёздное небо» (точки случайного цвета по случайным координатам). В этой программе координаты не разделяются на X и Y, потому что они всё равно случайные, вместо этого берётся случайное число в диапазоне 0…512*256

   1 .eqv    ALLSIZE 0x20000                 # размер экрана в ячейках
   2 .eqv    BASE    0x10040000              # MMIO экрана (на куче)
   3 .data   BASE
   4 screen:
   5 .text
   6         li      a0 ALLSIZE              # «Закажем» видеопамять в куче
   7         li      a7 9
   8         ecall
   9 again:  mv      a0 zero
  10         li      a1 ALLSIZE              # Максимальное 512*Y+X + 1
  11         li      a7 42
  12         ecall                           # Случайное 512*Y+X
  13         slli    t2 a0 2                 # Домножаем на 4
  14         mv      a0 zero
  15         li      a1 0x1000000            # Максимальный RGB-цвет + 1
  16         li      a7 42
  17         ecall  
  18         la      t0 screen
  19         add     t2 t2 t0                # Случайный цвет
  20         sw      a0 (t2)
  21         b       again

Обратите внимание на то, что Bitmap Display может размещать видеопамять в различных местах адресного пространства, но отчего-то исключительно в неудобных:

Пример: случайные отрезки

Зададим константами размеры дисплея и базовый адрес

   1 .eqv    BASE 0x10010000
   2 .eqv    WIDTH 512
   3 .eqv    HEIGHT 256

Напишем подпрограмму рисования точки по заданным координатам заданным цветом.

   1 dot:    # a0=x a1=y a2=color
   2         li      t2  WIDTH
   3         mul     a1 a1 t2
   4         add     a0 a0 a1
   5         slli    a0 a0 2
   6         li      t2 BASE
   7         add     a0 a0 t2
   8         sw      a2 (a0)
   9         ret

Подготовим макросы с прологом и эпилогом подпрограммы:

   1 .macro  subroutine
   2         addi    sp sp -36
   3         sw      ra 36(sp)
   4         sw      s7 32(sp)
   5         sw      s8 28(sp)
   6         sw      s6 24(sp)
   7         sw      s5 20(sp)
   8         sw      s4 16(sp)
   9         sw      s3 12(sp)
  10         sw      s2 8(sp)
  11         sw      s1 4(sp)
  12         sw      s0 0(sp)
  13         mv      s0 a0   
  14         mv      s1 a1
  15         mv      s2 a2
  16         mv      s3 a3
  17         mv      s4 a4
  18 .end_macro
  19 
  20 .macro  return  %reg
  21         lw      ra 36(sp)
  22         lw      s8 32(sp)
  23         lw      s7 28(sp)
  24         lw      s6 24(sp)
  25         lw      s5 20(sp)
  26         lw      s4 16(sp)
  27         lw      s3 12(sp)
  28         lw      s2 8(sp)
  29         lw      s1 4(sp)
  30         lw      s0 0(sp)
  31         addi    sp sp 36
  32         mv      a0 %reg
  33         ret
  34 .end_macro

Отрезок будем рисовать наиболее простым алгоритмом: циклом по количеству точек в нём + масштабированием. Функцию, определяющую количество точек (по вертикали или по горизонтали — смотря что больше) напишем отдельно. Подпрограмма рисования отрезка принимает пять параметров — координаты начальной точки отрезка, цвет и координаты конечной точки отрезка; для красоты завернём в макрос её вызов:

   1 length: fcvt.s.w        f0 a0
   2         fabs.s          f0 f0
   3         fcvt.s.w        f1 a1
   4         fabs.s          f1 f1
   5         fmax.s          f0 f0 f1
   6         fcvt.w.s        a0 f0
   7         ret
   8 
   9         # a0=x0 a1=y0 a2=color a3=x1 a4=y1      
  10 _line:  subroutine
  11         sub     s5 s3 s0                # Ширина прямоугольника с отрезком W
  12         sub     s6 s4 s1                # Высота прямоугольника H
  13         mv      a0 s5
  14         mv      a1 s6
  15         jal     length
  16         mv      s7 a0                   # Количество точек N
  17         mv      s8 zero                 # Счётчик точек
  18 linel:  mul     a0 s5 s8                # W * i
  19         div     a0 a0 s7                # W / N * i
  20         add     a0 a0 s0                # x0 + W / N * i
  21         mul     a1 s6 s8                # H * i
  22         div     a1 a1 s7                # H / N * i
  23         add     a1 a1 s1                # y0 + H / N * i
  24         jal     dot
  25         addi    s8 s8 1                 # следующая точка
  26         ble     s8 s7 linel 
  27         return  t0
  28 
  29 .macro  line    %x0 %y0 %x1 %y1 %col
  30         mv      a0 %x0
  31         mv      a1 %y0
  32         mv      a2 %col
  33         mv      a3 %x1
  34         mv      a4 %y1
  35         jal     _line
  36 .end_macro

Первый параметр системного вызова RARS № 42 — т. н. «номер случайной последовательности», достаточно, чтобы он был равен 0. Напишем макрос-обёртку вокруг ecall 42 и программу, которая бесконечно рисует отрезки случайного цвета:

   1 .macro  random  %reg %max
   2         mv      a0 zero
   3         li      a1 %max
   4         li      a7 42
   5         ecall
   6         mv      %reg a0
   7 .end_macro
   8 
   9 .globl  main
  10 main:   random  s0 WIDTH
  11         random  s1 HEIGHT
  12 loop:   random  s3 WIDTH
  13         random  s4 HEIGHT
  14         random  s2 0xffffff
  15         line    s0 s1 s3 s4 s2
  16         mv      s0 s3
  17         mv      s1 s4
  18         j       loop

Наконец, напишем программу, заполняющую дисплей отрезками случайного цвета

Программа начинается с метки main, так что при сборке надо включить «Initialize Program Counter to global 'main' if defined» в настройке RARS.

ColorLines.png

В силу природы MMIO прямая запись в видеопамять — операция долгая и ресурсоёмкая. Это видно, например, из того, с какой скоростью RARS может записывать слова в оперативную память, и с какой — последовательно заполнять точки графического дисплея. В то же время именно графические устройства требуют очень быстрой работы с очень большими объёмами данных

Кстати: Вот что можно сделать из RARS Bitmap!

Д/З

Задачи типа «рисуем» предназначены для использования с RARS Bitmap Display. На EJude после окончания работы программы RARS будет сам выводить дамп видеопамяти и этот дамп и будет сравниваться с эталонным. В некоторых случаях сравнение может быть нечётким (для компенсации погрешностей вычисления). Для скорости вычислений все домашние задания выполняются со следующими настройками Bitmap Display (2×2; 512×256):

Отличие от стандартных настроек: размер пикселя увеличен вчетверо (при этом размер видеопамяти уменьшается вчетверо). Того же самого эффекта можно добиться, поделив все настройки пополам: 1×1; 256×128. Адрес MMIO — 0x10010000 (секция данныx).

Для дампа видеопамяти я (и EJudge) использую ключи rars, для конвертации дампа в png написал скрипт на питоне HexText.py.

$ rars ae1 se2 sm me nc dump 0x10010000-0x10210000 HexText дамп.hex программа.asm < входные_данные.text
$ python3 HexText.py дамп.hex   # получается файл дамп.hex.png

В задачах по адресу 0x10010000 располагается только видеопамять (это частично входит в проверку). Все переменные просьба размещать в области глобальных данных (.data 0x10000000) или на куче (.data 0x10040000)

TODO

LecturesCMC/ArchitectureAssembler2026/08_Input_Output (последним исправлял пользователь FrBrGeorge 2026-04-06 17:23:23)