Ввод/Вывод: поллинг и MMIO
не забыть фанерную клавиатуру «COKPOWEHEU»
Очевидно, что управление всем, что подключено к компьютеру (включая пользователя ☺), должен взять на себя компьютер. Это означает, что
- Протокол взаимодействия с периферией должен быть формализуем
- В архитектуре ЭВМ должно быть предусмотрено
- аппаратное взаимодействие (способ подключения)
- программное взаимодействие (способ управления)
- обмен данными (в каком-то смысле — частный случай «управления», но нет)
Внешние устройства
Внешнее устройство (также периферийное) — любая аппаратура, которая обменивается данными с ЭВМ.
Задачи:
Ввод (микрофон, кнопка выключения питания и т. п.)
Вывод (колонки, монитор…)
Ввод-вывод
(консоль) Частный случай: хранение = ввод/вывод + полное (или избыточное) кодирование + аллегирование (т. е. способ сослаться, например, номера секторов на диске)
Передача = ввод/вывод + несколько ЭВМ + синхронизация (сетевая карта, …)
⇒ ВУ может быть не сложнее трёх проводов с кнопкой, а может быть целым специализированным компьютером со своим процессором, памятью, регистрами и т. п.
Способы взаимодействия с ВУ на разных архитектурах
Унифицированный: ∃ стандарт на команды и логику работы ВУ
- ВУ слишком разные, общий стандарт запредельно сложен
- Логику сложных ВУ надо уметь программировать непосредственно при работе (а что вообще в них за процессоры стоят?)
- Такие примеры были: т. н. «канальные программы» систем IBM прошлого столетия
Порты В/В: стандартизируется только способ обращения к ВУ
Все каналы связи с ВУ (т. н. порты) как-то нумеруются
Вывод — инструкция out источник номер_порта; ввод — инструкция in приёмник номер_порта
- Что означают передаваемые и принимаемые данные — зависит от самого устройства и способа его физического подключения (например, один бит = один провод)
Memory-mapped I/O (MMIO): отображение В/В на оперативную память
- Вместо портов используются обычные адреса и команды работы с памятью
- Обращение к адресу из определённого диапазона приводит к обмену данными с ВУ, а не с оперативной памятью
Соответствие конкретных адресов конкретным интерфейсам ВУ определяется как-то
- Вариант: отображается целая область памяти ВУ (например, страница видеопамяти)
MMIO-регистры бывают:
- управляющими (запись = команда ВУ или изменение его состояния)
- регистрами данных (запись/чтение данных, соответствующих команде)
- статусными (только чтение информации о состоянии ВУ)
- часто встречается т. н. «флаг готовности» — бит, который равен единице, если устройство готово что-то делать (пришли новые данные в устройство ввода, все данные устройства вывода переданы и т. п.)
Стоит заметить, что содержимое MMIO далеко не всегда однозначно соответствует состоянию внешнего устройства: например, запись в управляющий регистр может поменять значение статусного, а значение самого управляющего регистра иногда вообще недоступно или не меняется, чтение данных приводит внешнее устройство в состояние готовности и т. п.
Обычно процессор использует для доступа к MMIO контроллер памяти
На совсем примитивных устройствах может MMIO быть просто «проводами к другим банкам памяти»
- Отсюда торчат уши legacy, конечно
Аппаратные задачи MMIO
- Арбитраж (какие операции В/В делать первыми)
- Например, несколько устройств актуально одновременно обновляют свои регистры статуса
- Сопоставление MMIO-адреса устройству
- Превращение MMIO-адреса во внутреннюю операцию обмена данными с ВУ
- Анализ состояния ВУ и отображение на MMIO-ячейке
- Передача данных от/к ВУ
Барьеры памяти
(соблюдение порядка и актуальности доступа)
- По кешированию
- По аппаратному переупорядочиванию
В т. ч. в случае собственной логики ВУ (сначала записать в один регистр, затем прочесть из другого)
- …
RISC-V: инструкции типа fence. Общая идея:
Можно пометить любой заданный набор активностей (Input и Output со стороны устройства, Read и Write со стороны потока вычислений) как предшествующий (predecessor) любому заданному набору тех же активностей (successor).
Отличие механизма MMIO от CSR
- Казуальные устройства / стандартизация (даже custom-секция)
- Механизм адресации (⇒ контроллер памяти) / абстрактный механизм нумерации (⇒ сначала строго процессор, затем что угодно)
- Внешние устройства / внутренние компоненты
⇒ В спецификацию процессора RISC-V MMIO почти не входит:
Внешние таймеры mtime и mtimecmp на уровне Machine (самом верхнем), причём декларируются только названия, а адрес зависит от реализации
Межпроцессорный интерфейс к прерываниям — здесь вообще только сказано, что он должен быть (потому что прерывание одно, и память одна, а вот блоков CSR столько же, сколько и процессоров)
- … ?
MMIO и абсолютная адресация
Привязка функций компьютера к конкретным адресам оперативной памяти — не нарушение ли принципа «все адреса в программе RISC-V — относительные»? На самом деле мы чересчур упростили формулировку самого принципа, так что нет, не нарушение.
Напомним, что позиционно-независимый код позволяет загружать программу начиная с произвольного адреса в памяти. Для этого в RISC-V в командах переходов и чтения-записи вместо адресов указываются смещения. Вот смещения не должны изменяться, куда бы нашу программу ни загрузили. Это, в частности, означает, что секция данных должна располагаться на фиксированном расстоянии от секции кода. В заранее не известное место памяти, например, загружаются т. н. разделяемые библиотеки во время из компоновки с основной программой.
Однако в случае MMIO задача в каком-то смысле противоположная: в какое бы место памяти ни загрузилась программа, адреса регистров внешних устройств должны оставаться одинаковыми — они-то заранее указаны в документации к соответствующей периферии.
Забегая вперёд заметим, что механизм виртуальной памяти позволяет сопоставить произвольный блок физической памяти с заданным диапазоном адресов в адресном пространстве процесса — но если окружение вообще разрешает процессу доступ к MMIO, то виртуальный адреса, которые увидит процесс, будут, скорее всего, совпадать с теми, что в документации (иначе как он про них узнает).
Взаимодейстие на основе опроса
Поллинг (polling, опрос) — способ работы с внешними устройствами, при котором программа регулярно проверяет готовность устройства к В/В, и если готовность есть, осуществляет соответствующую операцию.
Организация поллинга из программы:
- Подготовка устройства к работе
- Цикл
- Проверка готовности устройства
- Если устройство готово
- Операция В/В
- Выход из цикла
- Если устройство не готово
- Бессмысленное ожидание
- Перевод устройства в исходное состояние
Замечание: пункт «бессмысленное ожидание» (бессмысленная трата процессорного времени!) можно было бы заменить на «выполнение полезных действий», но:
Всё происходит в процедуре обмена с ВУ, которая может и не знать о ом, какие действия полезны (это вообще может быть подпрограмма, вызываемая из многих мест)
«Полезные действия» в этом случае должны длиться строго определённое время (время ожидания готовности). Отмерить бессмысленные действия намного проще.
(срочно нужны прерывания, но они в следующей лекции)
Цифровой блок RARS
Цифровой блок «Digital Lab Sim» — это воображаемое внешнее устройство для RARS, позволяющее потренироваться в организации ввода и вывода. В учебно-тренировочных целях протокол управления цифровым блоком сделан максимально неудоб-приближенным к обычной практике разработки таких устройств.
Выглядит в работе оно так:
0xFFFF0010 |
command right seven segment display |
Биты правого цифрового индикатора (запись) |
0xFFFF0011 |
command left seven segment display |
Биты левого цифрового индикатора (запись) |
0xFFFF0012 |
command row number / enable keyboard interrupt |
Номер ряда клавиатуры для опроса (биты 0-3) |
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
- Чтобы получить ответ, надо запустить операцию сканирования, а затем считать результат
Операцию сканирования надо проводить с каждой строкой клавиатуры по отдельности!
Дело в том, что это устройство спроектировано «как в жизни». Предполагается, что в клавиатуре есть всего 8 проводов (как в матрице памяти) – 4×4 – и всё, что можно сделать — это подать напряжение на один из горизонтальных проводов и отобразить, на какой вертикальный провод он замкнут, если соответствующая клавиша нажата.
Сравнительно несложно обеспечить соответствие провода, на котором обнаружилось напряжение, отдельному биту в регистре, а также произвести простейшие операции над этими битами. Устройство могло выглядеть, например, так:
Однако более сложные логические цепочки — шифратор (для превращения провода N в двоичное число), дешифратор (обратно), сумматор и т. п. — в подобных «железках» обычно отсутствуют.
Вот и выходит, что задача превратить «сырые» данные устройства в осмысленные ложится на программу.
TODO: сфотографировать, а лучше принести на лекцию фанерную клавиатуру.
(нажмите «Комментарии» в шапке страницы, чтобы прочитать комментарии от COKPOWEHEU)
Итак, для того, чтобы просканировать, нажата ли какая-нибудь клавиша в ряду «0-1-2-3», надо:
«Подать напряжение на нулевую строку», то есть записать в 0xffff0012 число, у которого только нулевой бит равен 1 (это число 1)
Считать из регистра 0xffff0014 значение. Если клавиша в нулевом ряду не нажата, вернётся 0, если нажата, вернётся число, в котором
- установлен в 1 ровно один из первых четырёх битов, соответствующий нулевой строке (так же, как в операции сканирования)
установлен в 1 ровно один из битов 4…7, в соответствие со столбцом, в котором находится нажатая клавиша (0x10,0x20,0x40 и 0x80 для клавиш «0», «1», «2» и «3» соответственно)
например, для клавиши «2» ответ будет 0x41
- Кусок кода при этом может выглядеть так:
Для «подачи напряжения» на другие строки («4-5-6-7», «8-9-a-b» или «c-d-e-f») в 0xffff0012 надо записывать 2, 4 или 8. Из 0xffff0014 будет считываться число, у которого первые четыре бита установлены аналогично (если нажата клавиша в соответствующем ряду, иначе считывается 0)
Если записать в 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
Этот код может завесить RARS — рекомендуется передвинуть слайдер «Run speed» в положение «30 inst/sec». Обратите внимание на распространение знака в инструкции lb
Более развесистый пример от COKPOWEHEU
Последовательность внутри цикла — команды запуска считывания соответствующего ряда и аккумуляции считанных значений. Программа выполняет поллинг, но без заполнения промежутков между опросами устройства «бессмысленным ожиданием»: после каждого опроса немедленно начинается следующий. На практике такие программы начинают потреблять очень много процессорного времени, не делая почти ничего (пользователь — самое медленное на свете устройство ввода ☺). Чтобы ожидание меньше нагружало процессор, можно использовать внешний вызов sleep (32), который передаёт управление операционной системе на указанный в миллисекундах период. Может, хоть окружение в это время будет делать что-то полезное?
Графический дисплей
Графический дисплей RARS (Bitmap Display) представляет собой воображаемое внешнее устройство RARS, состоящее из единственной области памяти, целиком отображённой с помощью MMIO в адресное пространство RARS (по умолчанию — зачем-то на начало статических данных 0x10010000, но есть и другие варианты).
Запись машинного слова 0x00RRGGBB по адресу Base Address + Offset приведёт к появлению на экране точки цвета #RRGGBB в цветовом пространстве RGB. Подробнее про цветовой пространство RGB можно прочитать в Википедии, там же есть ссылка на таблицу цветов HTML.
Ширина и высота экрана задаются в точках на экране компьютера. Один пиксель Bitmap-устройства (unit) представляет собой прямоугольник из точек экрана. Если он равен одной точке (Unit Width × Unit Length — это 1×1), количество пикселей в видеопамяти устройства совпадает с количеством пикселей в соответствующей области экрана. Если размеры пикселя увеличивать (не забываем нажать кнопку «Reset»), он превратится в видимый прямоугольник, а общий объём видеопамяти пропорционально сократится.
Объём потребляемой памяти определяется так (*4 — потому что один пиксель задаётся машинным словом длиной в 4 байта):
ALL = DisplayWidth * DisplayHeight * 4 / (UnitWidth * UnitHeight)
Координаты точки со смещением Offest вычисляются так
X = (Offset / 4) % (DisplayWidth / UnitWidth)
Y = (Offset / 4) / (DisplayWidth / UnitWidth)
Обратно, смещение точки с координатами X,Y вычисляется так:
Offset = Y*DisplayWidth*4/UnitWidth+X*4
Пример: классическая программа, рисующая «звёздное небо» (точки случайного цвета по случайным координатам). В этой программе координаты не разделяются на 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
В данном примере мы постарались уйти от непривычной для RISC-V практики хранить абсолютные адреса в коде программы
Обратите внимание на то, что Bitmap Display может размещать видеопамять в различных местах адресного пространства, но отчего-то исключительно в неудобных:
По умолчанию — в области данных (0x10010000). Кучей при этом пользоваться нельзя, если размер экрана превышает 0x30000 (по умолчанию он 512*256*4 = 0x80000)
Переменные при этом надо хранить где-то ещё. Например, зарезервировать с помощью .space всю видеопамять, и располагать их после неё.
На куче (0x10040000) — система не знает о том, что эта память занята, и кучей пользоваться нельзя
Можно схитрить, и перед началом работа выделить на куче соответствующий объём данных!
В области глобальных данных (0x10000000) или gp (0x10008000) — там места ещё меньше, экран налезает на область данных
- В начале поля регистров MMIO (0xffff0000) — там там тоже памяти не хватает, а ещё мы там стек обработчика договорились держать…
Пример: случайные отрезки
Зададим константами размеры дисплея и базовый адрес
Напишем подпрограмму рисования точки по заданным координатам заданным цветом.
Подготовим макросы с прологом и эпилогом подпрограммы:
В прологе сохраняются регистры ra и s0 — s8; кроме того, регистры a0 — a4 копируются в s0 — s4. Это позволит пользоваться.
- В эпилоге сохранённые регистры восстанавливаются
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 и программу, которая бесконечно рисует отрезки случайного цвета:
Наконец, напишем программу, заполняющую дисплей отрезками случайного цвета
Программа начинается с метки main, так что при сборке надо включить «Initialize Program Counter to global 'main' if defined» в настройке RARS.
В силу природы MMIO прямая запись в видеопамять — операция долгая и ресурсоёмкая. Это видно, например, из того, с какой скоростью RARS может записывать слова в оперативную память, и с какой — последовательно заполнять точки графического дисплея. В то же время именно графические устройства требуют очень быстрой работы с очень большими объёмами данных
- Устройство снабжается собственной памятью и DMA-интерфейсом, и вместо MMIO большие объёмы данных пересылаются по DMA
- Устройство снабжается мощным процессором (GPU), весь обсчёт идёт на GPU, а на долю процессора остаётся только обработка пересылки данных по DMA, когда памяти графического устройства не хватает
Кстати: Вот что можно сделать из 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
