Прерывания по таймеру

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

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

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

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

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

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

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

⇒ Аппаратные таймер-счётчики: устройства, периодически генерирующие сигнал (например, Кварцевый_генератор) + схемотехника, которая умеет подсчитывать их количество и / или генерировать программно обрабатываемое прерывание.

  • Широко используются на уровне электроники (например, в ШИМ)

Свойства:

  • Точность / стабильность
  • Разрешение
  • В секундах или в попугаях (тиках, морганиях лампочки…)?
  • Задержка при чтении
  • Интервал переполнения-сброса (watchdog?) и монотонность
    • watchdog сброса + программная обработка = монотонные часы ∞ объёма)
  • Энергонезваисимость

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

  • Выбрать «главные часы» (скорее всего это будут именно часы, но может быть и счётчик тактов, например)
  • Переустанавливать и калибровать остальные (в т. ч. при переустановке и калибровке «главных»)
    • Скорее всего, это дело программное, а не аппаратное, т. е. изменение каких-то множителей

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

  • Внешние аппаратные (например, RTC/HPET и т. п.) — специальные устройства В/В, с которыми взаимодействует процессор

  • Внутренние (всякие счётчики инструкций/тактов) — встроены в процессор; часто бывают виртуальными, т. е. задаются обработкой уже имеющихся данных
  • Программные — для формирования «правильных» значений, протоколов и т. п.

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

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

  • Ограничен по частоте (мегагерцами)
  • Имеет стабильность частоты (относительную) 10−5 — 10−12 (в последнем случае это генератор размером с автобус)

  • Другого пока нет. Для достижения бОльших частот используются множители — пропорционально множителю падает стабильность! Сами множители тоже вносят нестабильность

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

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

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

  • «провода» — запуск и синхронизация работы устройств
  • «внешние устройства и процессор» — тайм-ауты, генераторы сигналов, протоколы обмена в РВ
  • «ядро и супервизор» — планировщики (в т. ч реальное время) и многозадачность
  • «ОС» — запуск по расписанию, учёт потребления, энергосбережение
  • «Эксплуатация» — профилирование и отладка, прикладное ПО

RISC-V

  • Machine Timer Registers (mtime and mtimecmp)
    • Volume II: RISC-V Privileged Architectures V20211203 44
    • Это MMIO, а не CSR, потому что:
      • Аппаратное внешнее устройство
      • «Дорогое» и одно на все уровни/hart-ы
    • Выделенные прерывания таймера:
      • mtime — в действительности не таймер, а часы (RTC) — в тиках; перевод в секунды — ответственность окружения

      • mtimecmp — «галочка», при достижении которой происходит прерывание

Таймер RARS

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

  • LecturesCMC/ArchitectureAssembler2022/08_Timers/Timer_Tool.png

Его нужно

  • Подключить, как внешнее устройство (см. замечание в к Д/З)

  • Нажать Play — он запустит часы в миллисекундах, начиная с 0.

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

  • time — по адресу 0xFFFF0018 для младших 32 битов

  • timeh — по адресу 0xFFFF001B для старших 32 битов.

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

  • uie, в котором отмечается, какие именно типы прерываний уходят в нашу ловушку: от устройств, от таймеров или программные (нам нужны таймерные, UYIP, т. е. 0x10)

  • ustatus, в котором, как и в случае обработки исключений, надо выставить младший бит (UIE — разрешение обработки ловушек) в 1

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

  • Адрес вашего обработчика ловушек (прерываний и исключений) должен храниться в CSR utvec.

  • Четвертый бит CSR uie должен быть установлен в 1.

    • csrsi        uie 0x10

  • Нулевой бит CSR ustatus должен быть установлен в 1

    • csrsi        ustatus 1

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

  • timecmp — по адресу 0xFFFF0020 для младших 32 битов

  • timpecmph0xFFFF0024 для старших 32 битов.

Регистры таймера time[h] и timpecmp[h] — это регистры внешнего устройства MMIO, их не надо путать с похожими CSR(time[h]), которые в RARS, к сожалению, называются так же ☹. К ещё большему сожалению, в спецификации RiscV такая неразбериха тоже встречается.

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

  • Таймер сработает только один раз, после чего time* окажется больше timecmp* «навсегда» — пока туда снова что-нибудь подходящее не запишут

    • Более сложные таймеры могут иметь специальный управляющий регистр на MMIO, в котором можно выставить «бит сброса» — тогда при срабатывании таймера он будет обнуляться (и прерывание станет регулярным)
  • Как только вы записываете что-то в любой из регистров timecmp и timecmph, может случиться прерывание. Поэтому обновлять 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(t1)         # Актуальное значение timecmp

Однако на момент 2024-04-20 в этом месте наблюдается ошибка RARS:

  • Во-первых, позорная опечатка в документации (посмотрите!)
  • Во-вторых, очередная нестыковка знаковых / беззнаковых длинных / коротких целых

Если делать так, как написано, прерывания по таймеру начнут приходить непрерывно, похоже, кто-то интерпретирует это число как отрицательное. X-(

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

  • Вот это интересный момент. Поскольку таймер работает асинхронно и разрядность времени больше, чем размер регистра, старшая и младшая половины могут быть рассинхронизированы. Скажем, начали читать время 0x00000000:FFFFFFFF, а между чтениями произошел клок, получим либо 0x00000001:FFFFFFFF, либо 0x00000000:00000000. Одно из аппаратных решений - теневые регистры. При доступе к старшей части, младшая "защелкивается". Не наоборот, потому старшая часть и так не изменится при неизменной младшей

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

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

  • в случае, когда ловушка сработала по исключению, знаковый бит ucause нулевой,

  • а если по прерыванию — единичный (0x80000000).
  • Прерывание от Timer Tool также выставляет 2-й бит в ucause — получается 0x80000004

  • Чтобы отличить это таймерное прерывание от других, в SCR utval Timer Tool выставляет значение 0x10

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

  • После обработки прерывания мы должны вернуться к исполнению прерванной инструкции (с помощью uret)

  • после обработки исключения мы должны перейти к исполнению инструкции, следующей за прерванной (прибавить 4 к CSR uepc и только после этого выполнить uret)

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

   1 .eqv    TIME    0xFFFF0018
   2 .eqv    TIMECMP 0xFFFF0020
   3 .eqv    INTERVAL        500
   4 .data
   5 timer:  .word   0
   6 .text
   7         li      s2 TIMECMP      # MMIO таймера
   8         li      t1 1000         # Заряжаем на секунду
   9         sw      t1 (s2)
  10 
  11         la      t0 handle       # Адрес обработчика
  12         csrw    t0 utvec        # CSR вектора обработки ловушек
  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      # MMIO счётчика
   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 1            # Выведем его
  16         ecall
  17         li      a0 ' '          # пробел
  18         li      a7 11
  19         ecall
  20         csrr    a0 instret      # CSR — количество выполненных инструкций
  21         li      a7 1
  22         ecall
  23         li      a0 '\n'         # И перевод строки
  24         li      a7 11
  25         ecall
  26         b       loop            # sorry, вечный цикл
  27 .data
  28         .align  2               # Область сохранения контекста
  29 h.save: .space  4               # Пока только t1
  30 .text
  31 handle: csrw    t0 uscratch
  32         sw      t1 h.save t0    # Сохраняем t1
  33         lw      t1 count        # Счётчик
  34         addi    t1 t1 1
  35         sw      t1 count t0     # Запишем счётчик
  36         lw      t1 h.save       # Восстановим t1
  37         csrr    t0 uscratch     # восстановим t0
  38         uret

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

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

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

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

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

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

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

   1 .eqv    TIME    0xFFFF0018
   2 .eqv    TIMECMP 0xFFFF0020
   3 .eqv    INTERVAL        500
   4 
   5 .macro  print   %ecallnum %str %reg %end
   6 .data
   7 prompt: .asciz  %str
   8 final:  .asciz  %end
   9 .text
  10         la      a0 prompt
  11         li      a7 4
  12         ecall
  13         mv      a0 %reg
  14         li      a7 %ecallnum
  15         ecall
  16         la      a0 final
  17         li      a7 4
  18         ecall
  19 .end_macro
  20 .globl  main
  21 .text
  22 main:   li      s3 TIME
  23         li      s2 TIMECMP
  24         li      t1 INTERVAL
  25         sw      t1 (s2)
  26 
  27         la      t0 handler
  28         csrw    t0 utvec
  29         csrwi   uie 0x10
  30         csrwi   ustatus 1
  31 
  32         li      s1 200
  33 loop:   csrr    t1 time
  34         print   34 "Time CSR:" t1 ", "
  35         li      a7 30
  36         ecall
  37         mv      t1 a0
  38         print   34 "Time syscall:" t1 ", "
  39         lw      t1 (s3)
  40         print   1 "MMIO time:" t1 ", "
  41         lw      t1 (s2)
  42         print   1 "MMIO timer:" t1 ", "
  43         lw      t1 h.cnt
  44         print   1 "Handler counter:" t1 "\n"
  45         addi    s1 s1 -1
  46         bgez    a1 loop
  47 
  48         li      a7 10
  49         ecall
  50 
  51 .data
  52 h.cnt:  .word   0
  53 h.a1:   .word   0
  54 .text
  55 handler:
  56         csrw    a0 uscratch
  57         sw      a1 h.a1 a0
  58         lw      a0 h.cnt
  59         addi    a0 a0 1
  60         sw      a0 h.cnt a1
  61         li      a0 TIMECMP
  62         lw      a1 (a0)
  63         addi    a1 a1 INTERVAL
  64         sw      a1 (a0)
  65         lw      a1 h.a1
  66         csrr    a0 uscratch
  67         uret

Программа из 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
  • <!> Найдите ошибку в этой программе!

    • Подсказка: в программе молчаливо предполагается, что некоторые значения инициализированы нулём
    • Подсказка 2: программа явно рассчитана на то, что некоторые значения она инициализирует ровно один раз

На 2024-04-11 в RARS есть ошибка: если читать «длинный» CSR — например, csrr t0 time — то в структуру данных Java, отвечающую за регистр t0 приезжает 64-разрядное значение — в данном случае time / timeh. Оно заведомо больше любого 32-разрядного числа. Лечится любой операцией над этим регистром, например mv t0 t0. Исправлена в модифицированной версии (взятой отсюда).

LecturesCMC/ArchitectureAssemblerProject/18_Timers (последним исправлял пользователь FrBrGeorge 2024-07-26 11:58:37)