Счётчики и таймеры
Виртуальное время
Зачем нужно знать время внутри программы?
Базовая статья на Хабре. Обратите внимание на количество «исторического наследия», оно же — легаси.
Определять относительный порядок событий. Для этого используются часы, измеряющие время от «начала времён», «эпохи» или какого-то иного фиксированного события в прошлом.
- Разрешение — не меньше, чем минимальный интервал между событиями
- Точность — достаточная, чтобы не перепутать события)
- ⇒ Скорее всего, для этого будет необходимо сгенерировать и обработать прерывание
Измерять длительность процессов. Для этого используются секундомеры (с событием по каждому интервалу) и таймеры (с событием по окончанию отсчёта, частный случай).
- Точность и разрешение зависят от требований. Как правило, требования высокие, иначе можно воспользоваться часами.
Не пропустить важное событие в будущем. Для этого нужны будильники. Процессор при этом может быть (частично) обесточен.
- Точность и разрешение соотносятся с ожиданиями от времени «пробуждения» — точнее не очень надо.
⇒ Аппаратные таймер-счётчики: устройства, периодически генерирующие сигнал (например, Кварцевый_генератор) + схемотехника, которая умеет подсчитывать их количество и / или генерировать программно обрабатываемое прерывание.
Широко используются на уровне электроники (например, в ШИМ)
Свойства:
- Точность / стабильность
- Разрешение
- В секундах или в попугаях (тиках, морганиях лампочки…)?
- Задержка при чтении
- Интервал переполнения-сброса (watchdog?) и монотонность
watchdog сброса + программная обработка = монотонные часы ∞ объёма)
- Энергонезваисимость
Устройства времени бывают:
Внешние аппаратные (например, RTC/HPET и т. п.) — специальные устройства В/В, с которыми взаимодействует процессор
- Внутренние (всякие счётчики инструкций/тактов) — встроены в процессор; часто бывают виртуальными, т. е. задаются обработкой уже имеющихся данных (например, делителем счётчика тактов или данных, снятых с аппаратного таймера)
- Программные — для формирования «правильных» значений из других источников, поддержки протоколов и т. п.
Если источников для устройств времени несколько, их надо время от времени) согласовывать.
- Выбрать «главные часы» (скорее всего это будут именно часы, но может быть и счётчик тактов, например)
- Переустанавливать и калибровать остальные (в т. ч. при переустановке и калибровке «главных»)
- Скорее всего, это дело программное, а не аппаратное, т. е. изменение каких-то множителей
Почему все непросто и что с этим делать?
- Ограничен по частоте (мегагерцами)
Имеет стабильность частоты (относительную) 10−5 — 10−12 (в последнем случае это генератор размером с автобус)
Для достижения бóльших частот используются множители — пропорционально множителю падает стабильность! Сами множители тоже вносят нестабильность
Генерировать одновременно короткий и стабильный импульс само по себе дело не простое. Однако его еще надо доставить и обработать — значит, параметры устройства времени зависят и от схемотехники / разводки / технологии и т. п.
Дополнительно Виртуальное время. Часть 2: вопросы симуляции и виртуализации, нужная для понимания «глубины неудобозримой» этого вопроса в случаях многомашинной распределенной системы с развитым уровнем привилегий.
Роль устройств времени
- «провода» — запуск и синхронизация работы устройств
- «внешние устройства и процессор» — тайм-ауты, генераторы сигналов, протоколы обмена в РВ
- «ядро и супервизор» — планировщики (в т. ч реальное время) и многозадачность
- «ОС» — запуск по расписанию, учёт потребления, энергосбережение
- «эксплуатация» — профилирование и отладка, прикладное ПО
RISC-V
Machine Timer Registers (mtime and mtimecmp)
- Это MMIO, а не CSR, потому что:
- Аппаратное внешнее устройство
- «Дорогое» и одно на все уровни/hart-ы
- Выделенное прерывание таймера:
mtime — в действительности не таймер, а часы (RTC) — в тиках; перевод в секунды — ответственность окружения
mtimecmp — «галочка», при достижении которой происходит прерывание
- Это MMIO, а не CSR, потому что:
- RARS:
cycle / cycleh — число выполненных тактов
time / timeh — время в тиках
instret / instreth — число выполненных до конца машинных инструкций
Таймер RARS
Инструмент RARS Timer Tool имитирует устройство таймера, похожее на базовый таймер mtime/mtimecmp из спецификации. Ввод-вывод с отображением в память (MMIO) линия таймерного прерывания.
Его нужно
Подключить, как внешнее устройство (см. замечание в к Д/З)
Нажать Play — он запустит часы в миллисекундах, начиная с 0.
Время хранится в виде 64-битного целого числа, и к нему можно получить доступ (с помощью инструкции lw):
time — по адресу 0xFFFF0018 для младших 32 битов
timeh — по адресу 0xFFFF001C для старших 32 битов (в документации написано 1B, но это нечётное чиcло).
За обработку прерываний по таймеру отвечают два регистра 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 битов
timpecmph — 0xFFFF0024 для старших 32 битов.
Регистры таймера time[h] и timpecmp[h] — это регистры внешнего устройства MMIO, их не надо путать с похожими CSR(time[h]), которые в RARS, к сожалению, называются так же ☹. К ещё большему сожалению, в спецификации RiscV такая неразбериха тоже встречается.
Прерывание произойдет, когда время в time* станет больше или равно значению в timecmp*. Имеет смысл записывать число, большее, чем текущее значение часов.
Таймер сработает только один раз, после чего time* окажется больше timecmp* «навсегда» — пока туда снова что-нибудь подходящее не запишут
- Более сложные таймеры могут иметь специальный управляющий регистр на MMIO, в котором можно выставить «бит сброса» — тогда при срабатывании таймера он будет обнуляться (и прерывание станет регулярным)
Как только вы записываете что-то в любой из регистров timecmp и timecmph, может случиться прерывание. Поэтому обновлять timecmp* рекомендуется так:
Однако на момент 2024-04-20 в этом месте наблюдается ошибка RARS:
- Во-первых, позорная опечатка в документации (посмотрите!)
- Во-вторых, очередная нестыковка знаковых / беззнаковых длинных / коротких целых
Если делать так, как написано, прерывания по таймеру начнут приходить непрерывно, похоже, кто-то интерпретирует это число как отрицательное.
(нажмите «Комментарии» в шапке страницы, чтобы прочитать комментарии от COKPOWEHEU)
Замечание от меня: 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 DIGLAB 0xFFFF0010
2 .eqv COUNTER 3
3 .data
4 count: .word 0
5 .text
6 li s2 DIGLAB
7 li t1 1
8 sb t1 COUNTER(s2) # Надо записать байт
9
10 la t0 handle # Адрес обработчика
11 csrw t0 utvec # CAS вектора обработки ловушек
12 csrwi uie 0x10 # Включим обработку таймерных прерываний
13 csrwi ustatus 1 # Разрешим ловушки
14
15 loop: lw a0 count # Сюда обработчик запишет текущее время
16 li a7 1 # Выведем его
17 ecall
18 li a0 ' ' # пробел
19 li a7 11
20 ecall
21 csrr a0 instret # CSR — количество выполненных инструкций
22 li a7 1
23 ecall
24 li a0 '\n' # И перевод строки
25 li a7 11
26 ecall
27 b loop # sorry, вечный цикл
28 .data
29 .align 2 # Область сохранения контекста
30 h.save: .space 4 # Пока только t1
31 .text
32 handle: csrw t0 uscratch
33 sw t1 h.save t0 # Сохраняем t1
34 lw t1 count # Счётчик
35 addi t1 t1 1
36 sw t1 count t0 # Запишем счётчик
37 lw t1 h.save # Восстановим t1
38 csrr t0 uscratch # восстановим t0
39 uret
Обратите внимание на то, что заданных 30 тактов на прерывание едва хватает на два выполнения обработчика! Если бы в нём было, скажем, в три раза больше инструкций, то ничего, кроме кода обработчика, не выполнялось бы!
Отложенные прерывания (первый заход)
Как предотвратить повторный вход в обработчик, но при этом не потерять сам факт того, что ещё одно прерывание произошло за время обработки.
Давайте наполним обработчик прерывания счётчика от Digital Lab nop-ами настолько ,чтобы он занимал больше 30 инструкций — тогда второе прерывание счётчика возникнет до выхода из ловушки.
Повторного входа не произошло (об этом позаботился соответствующий бит CSR uie), но в регистре uip (Interrupt Pending) появился 5-й бит: «было ещё одно прерывание таймера». Если мы выполним uret, содержимое uip переедет в ucause, случится прерывание на той же инструкции, все пойдёт по новой.
Мы можем попробовать обработать отложенное прерывание тем же обработчиком или просто убрать бит uip в знак того, что данное прерывание идемпотентно — то есть неважно, сколько их на определённый промежуток времени произошло, обрабатывать их можно только один раз. Сами устройства — если они достаточно сложные — могут генерировать и другие типы прерываний (например, иметь несколько регистров сравнения или режим захвата и т. п.). В этом случае придётся разбираться с их CSR-регистрами, что же в действительности там произошло.
(нажмите «Комментарии» в шапке страницы, чтобы прочитать комментарии от COKPOWEHEU)
Более длинные примеры
1 .eqv DEVTIME 0xFFFF0018
2 .eqv TIME 0
3 .eqv TIMECMP 8
4 .eqv INTERVAL 500
5
6 .macro print %ecallnum %str %reg %end
7 .data
8 prompt: .asciz %str
9 final: .asciz %end
10 .text
11 la a0 prompt
12 li a7 4
13 ecall
14 mv a0 %reg
15 li a7 %ecallnum
16 ecall
17 la a0 final
18 li a7 4
19 ecall
20 .end_macro
21 .globl main
22 .text
23 main: li s2 DEVTIME
24 li t1 INTERVAL
25 lw t0 TIME(s2)
26 add t1 t0 t1
27 sw t1 TIMECMP(s2)
28
29 la t0 handler
30 csrw t0 utvec
31 csrwi uie 0x10
32 csrwi ustatus 1
33
34 li s1 200
35 loop: csrr t1 time
36 print 34 "Time CSR:" t1 ", "
37 li a7 30
38 ecall
39 mv t1 a0
40 print 34 "Time syscall:" t1 ", "
41 lw t1 TIME(s2)
42 print 1 "DEVTIME time:" t1 ", "
43 lw t1 TIMECMP(s2)
44 print 1 "DEVTIME timer:" t1 ", "
45 lw t1 h.cnt
46 print 1 "Handler counter:" t1 "\n"
47 addi s1 s1 -1
48 bgez a1 loop
49
50 li a7 10
51 ecall
52
53 .data
54 h.cnt: .word 0
55 h.a1: .word 0
56 .text
57 handler:
58 csrw a0 uscratch
59 sw a1 h.a1 a0
60 lw a0 h.cnt
61 addi a0 a0 1
62 sw a0 h.cnt a1
63 li a0 DEVTIME
64 lw a1 TIMECMP(a0)
65 addi a1 a1 INTERVAL
66 sw a1 TIMECMP(a0)
67 lw a1 h.a1
68 csrr a0 uscratch
69 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. Исправлена в модифицированной версии (взятой отсюда).
Напоминаю, что работа с реальным (и недорогим) железом — и уж точно с прерываниями! — описана в «RISC-V на примере микроконтроллеров GD32VF103 и CH32V303»
Д/З
В этих домашних заданиях предлагается написать обработчик прерываний по таймеру, обладающий заданными свойствами. Пользовательская часть обработчика прилагается в виде footer-программы, которая приписывается в конец общего текста (так поступает EJudge), или добавляется в многофайловую сборку.
В данный момент классический RARS не умеет обрабатывать прерывания по таймеру при запуске из командной строки. Мы c EJudge используем модифицированную версию
В исправленном виде RARS входит в репозиторй Sisyphus и стабильную платформу, начиная с P11):
добавлен параметр командной строки «ti», запускающий таймер сразу при старте RARS
- исправлена ошибка со знаком в CSR
TODO
EJudge: UniHandler 'Прерывания и исключения'
Написать обработчик для прерываний по таймеру и исключений по имени handler:. Для прерываний по таймеру обработчик должен выводить шестнадцатеричное число — тип таймера (из регистра utval), а для исключений — причину исключения (из регистра ucause).
Напоминаю, что из прерывания и из исключения надо возвращаться в разные места программы
Для первого теста применяется вот такой footer: UniHandler_footer.asm
3 4 2 0
Предложенный footer по этому вводу сначала пытается вызвать несуществующий ecall — это исключение № 8, затем дожидается прерывания по таймеру (для TimerTool тип таймера — 0x10, и затем пытается прочитать данные из недоступной памяти (по адресу 123) — это исключение № 5.
3 4 2 0
0x00000008 0x00000010 0x00000005
EJudge: MultiTasking 'Многозадачность'
Написать обработчик handler: прерывания по таймеру RARS Timer Tool, который по заданному интервалу переключает управление между N «параллельными» заданиями, а по прошествии определённого времени запускает ещё одно задание — финальное. Параллельные задания оформлены как вечные циклы, финальное вызывает ecall 10. Дополнительно написать подпрограмму boot:, которая принимает пять параметров:
- Адрес таблицы заданий
- Количество заданий (N)
- Адрес финального задания
- Время окончания работы (в милисекундах)
- Интервал переключения между заданиями (в милисекундах)
Подпрограмма boot: должна настраивать обработчик, включать прерывания и запускать первое задание.
- Таблица заданий — это массив из N адресов, с которых начинается выполнение
Задания пользуются только регистрами a*
Допустимо использовать в обработчике регистры t* без сохранения
К концу программы будет приписан footer. Пример footer для первого теста
Footer содержит глобальную метку main:; остальные имена в нём начинаются на символ «_»
Количество тактов в секунду у RARS дичайше плавает, так что сравнение происходит с точностью до целой части.
1000 3
1.0261073
EJudge: PerfMeasure 'Замер производительности'
Написать функцию pref:, которая будет в бесконечном цикле запускать некоторую другую подпрограмму и подсчитывать, сколько раз в милисекунду она запускается. Параметры:
- Время в миллисекундах, отведённое на тест
- Адрес измеряемой подпрограммы
Функция perf: должна возвращать в регистре fa0 вещественное число — производительность подпрограммы. Подпрограмме не передаются параметры, и в ней не используются регистры типа s*. К первому тесту будет приписан footer.asm.
3000
2.9196