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

Долги за прошлую лекцию

Имитация исключений

С одной стороны, на все случаи жизни исключения предусмотреть нельзя.

С другой стороны, это очень эффективный механизм обращения к ОС почти (или совсем) без дополнительных действий, в отличие от syscall. Системный обработчик по адресу 0x80000180 вызывается немедленно одной инструкцией, атомарно с операцией сравнения.

teq $t1,$t2

Trap if equal

Trap if $t1 is equal to $t2

teqi $t1,-100

Trap if equal to immediate

Trap if $t1 is equal to sign-extended 16 bit immediate

tge $t1,$t2

Trap if greater or equal

Trap if $t1 is greater than or equal to $t2

tgei $t1,-100

Trap if greater than or equal to immediate

Trap if $t1 greater than or equal to sign-extended 16 bit immediate

tgeiu $t1,-100

Trap if greater or equal to immediate unsigned

Trap if $t1 greater than or equal to sign-extended 16 bit immediate, unsigned comparison

tgeu $t1,$t2

Trap if greater or equal unsigned

Trap if $t1 is greater than or equal to $t2 using unsigned comparision

tlt $t1,$t2

Trap if less than

Trap if $t1 less than $t2

tlti $t1,-100

Trap if less than immediate

Trap if $t1 less than sign-extended 16-bit immediate

tltiu $t1,-100

Trap if less than immediate unsigned

Trap if $t1 less than sign-extended 16-bit immediate, unsigned comparison

tltu $t1,$t2

Trap if less than unsigned

Trap if $t1 less than $t2, unsigned comparison

tne $t1,$t2

Trap if not equal

Trap if $t1 is not equal to $t2

tnei $t1,-100

Trap if not equal to immediate

Trap if $t1 is not equal to sign-extended 16 bit immediate

Таким образом гарантируется, что за время, прошедшее между сравнением и обращением к системе, никакие данные не изменятся (потому что этого времени нет :) ).

Назначение инструкций типа trap:

Содержимое поля code игнорируется системой и может быть использовано для кодированиия информации для системного ПО. Для получения этой информации программа должна загрузить слово команды из памяти (адрес хранится в регистре EPC сопроцессора 0). В ассемблере Mars эта возможность отсутствует, обидно.

Свойства макроассемблера MARS

Замечания авторов MARS относительно их макроассемблера:

В больших многофайловых проектах принято все макросы складывать в отдельный файл и включать их в код программы с помощью директивы .include файл_с_макросами . Подпрограммы при этом складываются в другой файл (возможно. не один), т. н. «библиотеку», и подключаются посредством многофайловой сборки. На предыдущем примере: Файл с программой prog.asm:

   1 .include        "macro.inc"
   2 .globl  main
   3 .text
   4 main:
   5         input   "First input" $t0
   6         input   "Second input" $t1
   7         print   "First result" $t0
   8         print   "Second result" $t1
   9         exit

Файл с подпрограммами lib.asm:

   1 .globl  _print _input
   2 
   3 .text
   4 _input: # $a0message / $v0input value
   5         li      $v0 4
   6         syscall
   7         li      $v0 5
   8         syscall
   9         jr      $ra
  10 
  11 _print: # $a0message, $a1number
  12         li      $v0 4
  13         syscall
  14         move    $a0 $a1
  15         li      $v0 1
  16         syscall
  17         li      $a0 10
  18         li      $v0 11
  19         syscall
  20         jr      $ra

Не забываем метки всех подпрограмм, которые понадобятся в других файлах, объявлять как .globl Файл с макросами macro.inc (имя файла не заканчивается на .asm в знак того, что его не нужно транслировать отдельно):

   1 .macro  input   %msg %reg
   2 .data
   3 msg:    .ascii  %msg
   4         .asciiz ": "
   5 .text
   6         la      $a0 msg
   7         jal     _input
   8         move    %reg $v0
   9 .end_macro
  10 
  11 .macro  print   %msg %reg
  12 .data
  13 msg:    .ascii  %msg
  14         .asciiz ": "
  15 .text
  16         la      $a0 msg
  17         move    $a1 %reg
  18         jal     _print
  19 .end_macro
  20 
  21 .macro  exit
  22         li      $v0 10
  23         syscall
  24 .end_macro

Чего нет в Mars

Макроассемблер Mars вполне достаточен для учебных целей, но не реализует много из того, что есть в промышленных средствах программирования на ассемблере

Ввод-вывод

Базовая лекция

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

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

Задачи:

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

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

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

Работа процессора с MMIO:

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

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

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

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

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

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

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

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

DigitalLab.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

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

0xFFFF0014

receive row and column of the key pressed

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

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

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

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

   1     lui   $t8 0xffff              # база MMIO
   2     li    $t1 0xdb
   3     sb    $t1 0x10($t8)
   4     li    $t2 0x66
   5     sb    $t2 0x11($t8)

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

snap-0414-103238.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          # «подаём напряжение»
       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         lui     $t8 0xffff              # база MMIO
   2         move    $t7 $zero               # счётчик
   3         move    $t6 $zero               # предыдущее значение
   4 loop:
   5         move    $t1 $zero               # общий результат сканирования
   6         li      $t0 1                   # первый ряд
   7         sb      $t0 0x12($t8)           # сканируем
   8         lb      $t0 0x14($t8)           # забираем результат
   9         or      $t1 $t1 $t0             # добавляем биты в общий результат
  10         li      $t0 2                   # второй ряд
  11         sb      $t0 0x12($t8)
  12         lb      $t0 0x14($t8)
  13         or      $t1 $t1 $t0
  14         li      $t0 4                   # третий ряд
  15         sb      $t0 0x12($t8)
  16         lb      $t0 0x14($t8)
  17         or      $t1 $t1 $t0
  18         li      $t0 8                   # четвёртый ряд
  19         sb      $t0 0x12($t8)
  20         lb      $t0 0x14($t8)
  21         or      $t1 $t1 $t0
  22         beq     $t1 $t6 same
  23         sb      $t1 0x10($t8)           # запишем результат в биты окошка
  24         move    $a0 $t1                 # выведем результат как двоичное
  25         li      $v0 35
  26         syscall
  27         li      $a0 10
  28         li      $v0 11
  29         syscall
  30         addi    $t7 $t7 1               # счётчик
  31         sb      $t7 0x11($t8)           # запишем его в другое окошко
  32         move    $t6 $t1
  33 same:   ble     $t7 20 loop
  34 
  35         li      $v0 10
  36         syscall

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

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

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

BitmapDisplay.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    0x10010000              # MMIO экрана
   3 .text
   4 again:  move    $a0 $zero
   5         li      $a1 ALLSIZE             # Максимальное 512*Y+X + 1
   6         li      $v0 42
   7         syscall                         # Случайное 512*Y+X
   8         sll     $t2 $a0 2               # Домножаем на 4
   9         move    $a0 $zero
  10         li      $a1 0x1000000           # Максимальный RGB-цвет + 1
  11         li      $v0 42
  12         syscall                         # Случайный цвет
  13         sw      $a0 BASE($t2)
  14         j       again

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

Дисплей можно «привязать» к нескольким различным местам «настоящей» памяти (т. е. сделать видеопамять доступной посредством MMIO).

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

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

Поскольку стандартное для MIPS место размещения локальных данных занято 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
   4         sh      $a1 Y
   5         mul     $a1 $a1 WIDTH
   6         add     $a0 $a0 $a1
   7         sll     $a0 $a0 2
   8         lw      $a1 Color
   9         sw      $a1 BASE($a0)
  10         jr      $ra

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

   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    $s0
  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         move    $fp $sp
  23 .end_macro
  24 
  25 .macro  return
  26         move    $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     $s0
  36         pop     $ra
  37         jr      $ra
  38 .end_macro
  39 
  40 .macro  hrandom %range %var
  41         li      $a0 0
  42         li      $a1 %range
  43         li      $v0 42
  44         syscall
  45         sh      $a0 %var
  46 .end_macro

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

   1         # нарисовать линию от предыдущей точки
   2         # $a0=x1 $a1=y1
   3 lineto: subroutine
   4         lh      $s0 X           # X0
   5         lh      $s1 Y           # Y0
   6         move    $s2 $a0         # X1
   7         move    $s3 $a1         # Y1
   8         sub     $s4 $s2 $s0     # W
   9         abs     $t0 $s4
  10         sub     $s5 $s3 $s1     # H
  11         abs     $t1 $s5
  12         move    $s6 $t0         # количество точек N
  13         bge     $t0 $t1 xmax
  14         move    $s6 $t1         # это N больше
  15 xmax:   move    $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 $s6
  20         mflo    $t0
  21         add     $a0 $t0 $s0     # новый X
  22         # Y=Y0+H*i/N
  23         mul     $t2 $s5 $s7
  24         div     $t2 $s6
  25         mflo    $t2
  26         add     $a1 $t2 $s1     # новый Y
  27         jal     dot             # поставим точку
  28         addi    $s7 $s7 1
  29         j       loop
  30 done:
  31         sh      $s2 X
  32         sh      $s3 Y
  33         return

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

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

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

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

Lines.png

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

Д/З

LecturesCMC/ArchitectureAssembler2019/08_TrapsAndMMIO (последним исправлял пользователь FrBrGeorge 2019-12-10 20:02:38)