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

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

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

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

Задачи:

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

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

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

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

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

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

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

RISC-V: инструкции типа fence

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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)

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

Digilab_2.png

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

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

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

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

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

Итак, для того, чтобы просканировать, нажата ли какая-нибудь клавиша в ряду «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 (более умное устройство подало бы напряжение на провод «ошибочная операция», который можно было бы прочитать в регистре статуса)

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

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

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

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

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

Bitmap.png

Запись машинного слова 0x00RRGGBB по адресу Base Address + Offset приведёт к появлению на экране точки цвета #RRGGB в цветовом пространстве 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

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

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

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

Поскольку стандартное для RARS место размещения локальных данных занято MMIO, совместим их с областью глобальных данных (она практически не используется в однозадачном режиме). Зададим в этой области три переменных: текущие координаты на экране («курсор») и текущий цвет графических операций. Эти координаты и цвет будут использоваться всякий раз вместо того чтобы задавать их явно, например, а качестве стартовой точки при рисовании фигур, а устанавливать мы их будем в финальную точку фигуры.

   1 .data   0x10008000
   2 X:      .half 0
   3 Y:      .half 0
   4 Color:  .word 0

Напишем подпрограмму рисования точки по заданным координатам. Точка будет рисоваться текущим цветом, а заданные координаты — сохраняться.

   1 .text
   2 dot:    # a0=x a1=y
   3         sh      a0 X t2
   4         sh      a1 Y t2
   5         li      t2  WIDTH
   6         mul     a1 a1 t2
   7         add     a0 a0 a1
   8         slli    a0 a0 2
   9         lw      a1 Color
  10         li      t2 BASE
  11         add     a0 a0 t2
  12         sw      a1 (a0)
  13         ret

Подготовим несколько макросов: push и pop; пролог subroutine и эпилог return для написания универсальных (но не слишком эффективных) подпрограмм согласно конвенции, а также «обёртку» hrandom вокруг системного вызова RARS для генерации случайного числа заданного диапазона размером в полуслово:

   1 .macro  push    %r
   2         addi    sp sp -4
   3         sw      %r (sp)
   4 .end_macro
   5 
   6 .macro  pop     %r
   7         lw      %r (sp)
   8         addi    sp sp 4
   9 .end_macro
  10 
  11 .macro  subroutine
  12         push    ra
  13         push    s11
  14         push    s1
  15         push    s2
  16         push    s3
  17         push    s4
  18         push    s5
  19         push    s6
  20         push    s7
  21         push    fp
  22         mv      fp sp
  23 .end_macro
  24 
  25 .macro  return
  26         mv      sp fp
  27         pop     fp
  28         pop     s7
  29         pop     s6
  30         pop     s5
  31         pop     s4
  32         pop     s3
  33         pop     s2
  34         pop     s1
  35         pop     s11
  36         pop     ra
  37         ret
  38 .end_macro
  39 
  40 .macro  hrandom %range %var
  41         li      a0 0
  42         li      a1 %range
  43         li      a7 42
  44         ecall
  45         sh      a0 %var a1
  46 .end_macro
  47 
  48 .macro  abs %dreg %sreg
  49         li  t6 0x1f
  50         sra t6 %sreg t6
  51         xor %dreg t6 %sreg
  52         sub %dreg %dreg t6
  53 .end_macro

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

   1         # нарисовать линию от предыдущей точки
   2         # a0=x1 a1=y1
   3 lineto: subroutine
   4         lh      s11 X         # X0
   5         lh      s1 Y          # Y0
   6         mv      s2 a0         # X1
   7         mv      s3 a1         # Y1
   8         sub     s4 s2 s11     # W
   9         abs     t0 s4
  10         sub     s5 s3 s1      # H
  11         abs     t1 s5
  12         mv      s6 t0         # количество точек N
  13         bge     t0 t1 xmax
  14         mv      s6 t1         # это N больше
  15 xmax:   mv      s7 zero       # шаг i
  16 loop:   bgt     s7 s6 done    # Нарисовали X1:Y1?
  17         # X=X0+W*i/N
  18         mul     t0 s4 s7
  19         div     t0 t0 s6
  20         add     a0 t0 s11     # новый X
  21         # Y=Y0+H*i/N
  22         mul     t2 s5 s7
  23         div     t2 t2 s6
  24         add     a1 t2 s1      # новый Y
  25         jal     dot           # поставим точку
  26         addi    s7 s7 1
  27         b       loop
  28 done:
  29         sh      s2 X t2
  30         sh      s3 Y t2
  31         return

Подпрограмма запоминает заданную конечную точку отрезка в качестве текущей. Получается нечто вроде «черепашьей графики».

Наконец, напишем программу, заполняющую дисплей отрезками случайного цвета (для того, чтобы случайный цвет в пространстве RGB оказался достаточно ярким, пришлось написать специальную подпрограмму):

   1         # Достаточно яркий случайный цвет
   2 randomcolor:
   3         li      t0 0
   4 rcnext: li      a0 0           # Цикл по B, G, R
   5         li      a1 0x10
   6         li      a7 42
   7         ecall
   8         slli    a0 a0 4        # каждый цвет поярче
   9         la      t2 Color
  10         add     t2 t2 t0
  11         sb      a0 (t2)
  12         addi    t0 t0 1
  13         li      t2 3
  14         blt     t0 t2 rcnext
  15         ret
  16 .data
  17 nx:     .half   0
  18 ny:     .half   0
  19 
  20 .text
  21 .globl  main
  22 main:
  23         hrandom WIDTH X
  24         hrandom HEIGHT Y
  25 
  26 forever:
  27         jal     randomcolor
  28 
  29         hrandom WIDTH nx
  30         hrandom HEIGHT ny
  31 
  32         mv      a1 a0
  33         lh      a0 nx
  34         jal     lineto
  35         b       forever

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

Bitmap_L.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)

  1. EJudge: FullRainbow 'Рисуем флажок'

    Написать программу, которая раскрашивыает RARS Bitmap Display вертикальными полосками равной ширины (возможно, +1 пиксель) в заданные цвета. Цвета вводятся в виде десятичных чисел построчно, конец ввода — 0 (не считается цветом); всего чисел N ⩽ 100.

    • Параметры Bitmap Display: 2×2; 512×256
    • Формула для начала полоски № k: width = 256 * k / N (арифметика целочисленная)

    FullRainbow.png

    Input:

    1249394
    7864115
    5601041
    43775
    6706551
    0
    Output:

    См. картинку выше
  2. EJudge: TheCircle 'Рисуем кружок'

    Написать программу, которая рисует на RARS Bitmap Display множество точек, расстояние от которых до данной с координатами X, Y не превышает заданного R, т. е. круг. Программа вводит пять неотрицательных целых чисел: X, Y, R (все < 214), цвет точек в круге и цвет точек вне круга.

    • Не гарантируется, что круг целиком помещается в видеопамяти или вообще находится в ней

    • Параметры Bitmap Display: 2×2; 512×256

    TheCircle.png

    Input:

    100
    60
    50
    15641122
    5575048
    Output:

    См. картинку выше
  3. EJudge: DrawSquares 'Рисуем квадраты'

    Написать программу, которая рисует на RARS Bitmap Display серию квадратов, стороны которых параллельны линиям x=y и x=-y. Программа вводит неотрицательное число — цвет фона, которым она закрашивает экран. Далее вводится «полудиагональ» 0 < R ⩽ 50 квадрата. Если она нулевая — это конец ввода. Если R ненулевое, вводятся целые координаты X, Y центра квадрата, и цвет, после чего снова водится и проверяется «полудиагональ». Всего квадратов ⩽ 128.

    • Не гарантируется, что квадрат целиком помещается в видеопамяти или вообще находится в ней. Рисовать надо только то, что попадает на экран.
    • Параметры Bitmap Display: 2×2; 512×256
    • «Полудиагональ» — это расстояние от центра до вершины квадрата. Пример двух квадратов с «полудиагональю» 1 и 2 (если пиксели не квадратные, выглядят как ромбы):
      • ................

      • ..........#.....

      • ..#......###....

      • .###....#####...

      • ..#......###....

      • ..........#.....

      • ................

    • Картинка для первого теста:

      WithDiamonds.png

    Input:

    3364164
    48
    178
    40
    15619583
    50
    120
    70
    11163135
    25
    37
    48
    11206485
    40
    200
    95
    16755285
    33
    65
    55
    1136127
    0
    Output:

    См. картинку
  4. EJudge: LabNumbers 'Выводим числа'

    Написать подпрограмму outnum, которая принимает в регистре fa0 вещественное число F и выводит его в цифровой индикатор RARS DigitalLab. Используется стандартный протокол округления. Форма цифр стандартная.

    • Если F ⩾ 100 или F ⩽ -10, в индикаторе высвечивается EE

    • Если 100 < F ⩽ 10, число округляется до ближайшего целого

    • Если 10 > F > 0, число округляется до первого знака после запятой, и высвечивается в индикаторе с точкой посередине

    • Если -10 < F < 0, число округляется до ближайшего целого и высвечивается с минусом в начале

    Склейка нескольких картинок из теста:

    • Numbers.png

    К решению будет приписан вот такой footer, содержащий глобальную метку main:LabNumbers_f.asm

    Input:

    34.75
    5.2
    0.01
    -7
    -11
    0
    Output:

    См. картинку

LecturesCMC/ArchitectureAssembler2024/08_Input_Output (последним исправлял пользователь FrBrGeorge 2024-07-26 11:19:05)