Таймер

Виртуальное время

Зачем нужно знать время внутри программы?

Базовая статья на Хабре. Обратите внимание на количество «исторического наследия», оно же — легаси.

  1. Определять относительный порядок событий. Для этого используются часы, измеряющие время от «начала времён», «эпохи» или какого-то иного фиксированного события в прошлом.

    • Разрешение — не меньше, чем минимальный интервал между событиями
    • Точность — достаточная, чтобы не перепутать события)
  2. Измерять длительность процессов. Для этого используются секундомеры (с событием по каждому интервалу) и таймеры (с событием по окончанию отсчёта, частный случай).

    • Точность и разрешение зависят от требований. Как правило, требования высокие, иначе можно воспользоваться часами.
  3. Не пропустить важное событие в будущем. Для этого нужны будильники. Процессор при этом может быть (частично) обесточен.

    • Точность и разрешение соотносятся с ожиданиями от времени «пробуждения» — точнее не очень надо.

Свойства:

Если устройств времени несколько, их надо время от времени) согласовывать.

Устройства времени бывают:

Аппаратные таймеры — ценный и ходовой ресурс.

Почему все непросто и что с этим делать?

Кварцевый резонатор

Получить одновременно короткий и стабильный импульс само по себе дело не простое. Однако его еще надо доставить и обработать — значит, параметры устройства времени зависят и от схемотехники / разводки / технологии и т. п.

Дополнительно Виртуальное время. Часть 2: вопросы симуляции и виртуализации, нужная для понимания "глубины неудобозримой" этого вопроса в случаях многмашинной распределенной системы с развитым уровнем привилегий.

Роль устройств времени

RISC-V

Таймер RARS

Инструмент RARS Timer Tool имитирует устройство таймера, похожее на базовый таймер mtime/mtimecmp из спецификации. Ввод-вывод с отображением в память (MMIO) линия таймерного прерывания.

Его нужно

Время хранится в виде 64-битного целого числа, и к нему можно получить доступ (с помощью инструкции lw):

За обработку прерываний по таймеру отвечают два регистра CSR:

Перед установкой прерывания необходимо сделать три вещи:

вот тут про CSR в старой спецификации RISC-V

Чтобы установить таймерtimecmp/tmpecmph, нужно записать туда время срабатывания виде 64-битного целого числа (с помощью инструкции sw):

Регистры таймера time и tmpecmph — это регистрв внешнего устройства MMIO, их не надо путать с спохожими CSR, которые в RARS, к сожалению, называются так же ☹.

Прерывание произойдет, когда время в time* станет больше или равно значению в timecmp*. Имеет смысл записывать число, большее, чем текущее значение часов.

   1     # a0: младшие 32 бита времени
   2     # a1: старшие 32 бита времени
   3     li t0 -1
   4     la t1 timecmp
   5     sw t0 0(t1)         # Наибольшее возможное значение timecmp
   6     sw a1 4(t1)         # Актуальное значение timecmph
   7     sw a0 0(t0)         # Актуальное значение timecmp

Замечание от нашего постоянного участника @COKPOWEHEU,:

Замечание от меня: 64-битный режим работы с регистрами снимает подобные вопросы — до тех пор, пока мы не собираемся считать что-то действительно очень быстро растущее)…

TODO проверить, что такое «64bit» в RARS, не подойдёт ли оно?

Обработчик прерывания по таймеру, если в архитектуре не предусмотрен векторный режим — это та же самая ловушка, что и обработчик исключений. Понять, что мы обрабатываем — прерывание или исключение — можно поглядев в CSR ucause:

<!> Важно отличить обработку прерывания от обработки исключения:

Плохой, негодный пример простейшего обработчика прерывания по таймеру, который не сохраняет никаких регистров, в нём используемых (что совсем нехорошо!)

   1 .eqv    TIME    0xFFFF0018
   2 .eqv    TIMECMP 0xFFFF0020
   3 .eqv    INTERVAL        500
   4 .data
   5 timer:  .word   0
   6 .text
   7         li      s2 TIMECMP      # CSR таймера
   8         li      t1 1000         # Заряжаем на секунду
   9         sw      t1 (s2)
  10 
  11         la      t0 handle       # Адрес обработчика
  12         csrw    t0 utvec        # CAS вектора обработки ловушек
  13         csrwi   uie 0x10        # Включим обработку таймерных прерываний
  14         csrwi   ustatus 1       # Разрешим ловушки
  15 
  16 loop:   lw      a0 timer        # Сюда обработчик запишет текущее время
  17         li      a7 34           # Выведем его в 16-чном виде
  18         ecall
  19         li      a0 '\n'         # И перевод строки
  20         li      a7 11
  21         ecall
  22         b       loop            # sorry, вечный цикл
  23 
  24 # Плохой, негодный обработчик — ничего не сохраняет
  25 # Но в нашем цикле регистры типа t* не испльзуются, может, пронесёт, а?
  26 handle: lw     t1 TIMECMP       # Время срабатывания таймера
  27         sw     t1 timer t0      # Запишем его в память
  28         li     t0 1000          # Увеличим на 1000 (секунда)
  29         add    t1 t1 t0
  30         sw     t1 TIMECMP t0    # Установим следующее время срабатывания
  31         uret

Счётчик инструкций в Digital Lab

В RARS есть ещё одно устройство, которое умеет вызывать внутреннее таймерное прерывание — это уже знакомый нам Digital Lab.

Если записать ненулевой байт в MMIO-регистр 0xFFFF0013, это устройcтво будет вызывать таймерное прерывание каждые 30 выполненных инструкций RARS. Не знаю, существуют ли в реальном мире такие возможности / необходимость тоже под вопросом, но это пример другого «таймера» (который в действительности счётчик).

Не забываем, что для работы Digital Lab надо «подключить» к RARS-у.

   1 .eqv    COUNTER 0xFFFF0013
   2 .data
   3 count:  .word   0
   4 .text
   5         li      s2 COUNTER      # CSR счётчика
   6         li      t1 1
   7         sb      t1 (s2)         # Надо записать байт
   8 
   9         la      t0 handle       # Адрес обработчика
  10         csrw    t0 utvec        # CAS вектора обработки ловушек
  11         csrwi   uie 0x10        # Включим обработку таймерных прерываний
  12         csrwi   ustatus 1       # Разрешим ловушки
  13 
  14 loop:   lw      a0 count        # Сюда обработчик запишет текущее время
  15         li      a7 34           # Выведем его в 16-чном виде
  16         ecall
  17         li      a0 '\n'         # И перевод строки
  18         li      a7 11
  19         ecall
  20         b       loop            # sorry, вечный цикл
  21 .data
  22         .align  2               # Область сохранения контекста
  23 h.save: .space  4               # Пока только t1
  24 .text
  25 handle: csrw    t0 uscratch
  26         la      t0 h.save
  27         sw      t1 (t0)         # Сохраняем t0
  28         lw     t1 count         # Счётчик
  29         li     t0 1             # Увеличим на 1
  30         add    t1 t1 t0
  31         sw     t1 count t0      # Запишем счётчик
  32         la      t0 h.save
  33         lw      t1 (t0)         # Восстановим t1
  34         csrr    t0 uscratch     # восстановим t0
  35         uret

Обратите внимание на то, что заданных 30 тактов на прерывание едва хватает на два выполнения обработчика! Если бы в нём было, скажем, в три раза больше инструкций, то ничего, кроме кода обработчика, не выполнялось бы!

Отложенные прерывания (первый заход)

Как предотвратить повторный вход в обработчик, но при этом не потерять сам факт того, что ещё одно прерывание произошло за время обработки.

Давайте наполним обработчик прерывания счётчика от Digital Lab nop-ами настолько ,чтобы он занимал больше 30 инструкций — тогда второе прерывание счётчика возникнет до выхода из ловушки.

Повторного входа не произошло (об это позаботился соответствующий бит CSR uie), но в регистре uip (Interrupt Pending) появился 5-й бит: «было ещё одно прерывание таймера». Если мы выполним uret, содержимое uip переедет в ucause, случится прерывание на той же инструкции, все пойдёт по новой.

Мы можем попробовать обработать отложенное прерывание тем же обработчиком или просто убрать бит uip в знак того, что данное прерывание идемпотентно — то есть неважно, сколько их на определённый промежуток времени произошло, обрабатывать их можно только один раз.

Более длинные примеры

Пример (с использованием самописной библиотеки макросов из прошлых Д/З)

   1 .include        "utils.inc"
   2 .eqv    TIME    0xFFFF0018
   3 .eqv    TIMECMP 0xFFFF0020
   4 .eqv    INTERVAL        500
   5 .globl  main
   6 .text
   7 main:   li      s3 TIME
   8         li      s2 TIMECMP
   9         li      t1 INTERVAL
  10         sw      t1 (s2)
  11 
  12         la      t0 h.proc
  13         csrw    t0 utvec
  14         csrwi   uie 0x10
  15         csrwi   ustatus 1
  16 
  17         li      s1 200
  18 loop:   csrr    t1 time
  19         print.x "Time CSR:" t1 ','
  20         syscall 30
  21         mv      t1 a0
  22         print.x " Time syscall:" t1 ','
  23         lw      t1 (s3)
  24         print.w " MMIO time:" t1 ','
  25         lw      t1 (s2)
  26         print.w " MMIO timer:" t1 ','
  27         lw      t1 h.time
  28         print.w " Handler counter:" t1
  29         addi    s1 s1 -1
  30         bgez    a1 loop
  31 
  32         syscall 10
  33 .data
  34 h.time: .word   0
  35 .text
  36 h.proc: hprocedure
  37         lw      a0 h.time
  38         addi    a0 a0 1
  39         sw      a0 h.time a1
  40         li      a0 TIMECMP
  41         lw      a1 (a0)
  42         li      a2 INTERVAL
  43         add     a1 a1 a2
  44         sw      a1 (a0)
  45         hreturn

Макросы в этом примере:

Программа из rars-master/examples

   1 .data
   2 loopStr:.asciz "Loop\n"
   3 hello:  .asciz "Hello\n"
   4 newLine:.asciz "\n"
   5 Time:   .word 0xFFFF0018
   6 cmp:    .word 0xFFFF0020
   7 .text
   8 main:
   9         # Set time to trigger interrupt to be 5 seconds
  10         lw  a0, cmp
  11         li  a1, 5000
  12         sw  a1, 0(a0)
  13 
  14         # Set the handler address and enable interrupts
  15         la      t0, handle
  16         csrrs   zero, 5, t0
  17         csrrsi  zero, 4, 0x10
  18         csrrsi  zero, 0, 0x1
  19 
  20 
  21 loop:
  22         # Output current time in loop
  23         li      a7, 1
  24         lw      a0 Time
  25         lw      a0, 0(a0)
  26         ecall
  27         li      a7, 4
  28         la      a0, newLine
  29         ecall
  30         j       loop
  31 
  32 
  33 handle:
  34         # Save some space for temporaries
  35         addi    sp, sp, -20
  36         sw      t0, 16(sp)
  37         sw      t1, 12(sp)
  38         sw      t2, 8(sp)
  39         sw      a0, 4(sp)
  40         sw      a7, 0(sp)
  41 
  42         # Print out hello
  43         li      a7, 4
  44         la      a0, hello
  45         ecall
  46 
  47         # Set cmp to time + 5000
  48         lw a0 Time
  49         lw t2 0(a0)
  50         li t1 5000
  51         add t1 t2 t1
  52         lw t0 cmp
  53         sw t1 0(t0)
  54 
  55         # Reload the saved registers and return
  56         lw      t0, 16(sp)
  57         lw      t1, 12(sp)
  58         lw      t2, 8(sp)
  59         lw      a0, 4(sp)
  60         lw      a7, 0(sp)
  61         addi    sp, sp, 20
  62         uret
  63 
  64 done:
  65         li      a7, 10
  66         ecall

Д/З

В этих домашних заданиях предлагается написать обработчик прерываний по таймеру, обладающий заданными свойствами. Пользовательская часть обработчика прилагается в виде footer-программы, которая приписывается в конец общего текста (так поступает EJudge). или добавляется в многофайловую сборку.

В данный момент классический RARS не умеет обрабатывать прерывания по таймеру при запуске из командной строки. Желающие могут потестировать модифицированную версию, в которой добавлен параметр командной строки «ti», запускающий таймер сразу при старте RARS. Эта же версия будет на EJudge, когда там появятся тесты.

  1. EJudge: TwoTasks 'Двухзадачность'

    Написать обработчик прерываний по таймеру handler:, который переключает контекст выполнения между двумя задачами, а после истечения тайм-аута — выполняет третью. Написать также подпрограмму init:, которой передаётся 6 параметров (через регистры a*):

    1. Адрес первой задачи
    2. Продолжительность такта (квант работы) первой задачи
    3. Адрес второй задачи
    4. Продолжительность такта (квант работы) второй задачи
    5. Адрес завершающей задачи
    6. Время, по истечении которого надо запустить завершающую задачу

    Требования и допущения:

    • Задачи используют только регистры типа a* и не используют стек, для простоты сохранения контекста

    • Алгоритм работы обработчика: переключить контекст на другую задачу (включая uepc), выставить таймер равным кванту работы этой задачи; когда придёт время завершения, таймер не выставлять, а вернуться сразу в завершающую задачу

    • Первая и вторая «задачи» в данном задании — это бесконечно работающие фрагменты кода, начинающиеся с определённого адреса; третья — фрагмент кода, вызывающий по окончании работы ecall 10.

    • footer, применяющийся в первом тесте

    /!\ Значения очень сильно отличаются от запуска к запуску и сильно зависят от производительности компьютера (привет Java?), так что если одно число результата примерно в два раза больше другого, то и ладно.

    • Вводится три числа:
      1. квант работы первой задачи в миллисекундах,
      2. квант работы первой второй в миллисекундах,
      3. общее время работы в миллисекундах
    Input:

    29
    58
    3000
    Output:

    551645
    1114830
  2. EJudge: UniHandler 'Прерывания и исключения'

    Написать обработчик для прерываний по таймеру и исключений. Для прерываний по таймеру обработчик должен выводить тип таймера (из регистра utval), а для исключений — причину исключения (из регистра ucause).

    • Напоминаю, что из прерывания и из исключения надо возвращаться в разные места программы

    • TODO footer пока не готов

    Input:

    TODO
    Output:

    TODO

TODO

LecturesCMC/ArchitectureAssembler2022/08_Timers (последним исправлял пользователь FrBrGeorge 2022-04-09 23:06:19)