Pull to refresh

Изучаем RISC-V с нуля, часть 2: прерывания и стыковка с Си

Reading time17 min
Views11K


Продолжаем погружаться в строение контроллера GD32VF103CBT6. Теперь рассмотрим как он может обрабатывать прерывания работать под управлением высокоуровневого кода.
Первая часть здесь


7. Подключение UART


При выборе задачки на рекурсию я столкнулся с проблемой, что трех светодиодов немного недостаточно для отладки сложных алгоритмов. Поэтому давайте добавим полноценный отладочный интерфейс, к которому можно было бы прицепиться терминальной программой вроде screen и общаться с контроллером при помощи обычного текста. Для этого воспользуемся тем же USART0, по которому происходит прошивка.


Небольшое отступление, связанное с терминологией: USART (универсальный синхронно-асинхронный приемо-передатчик), как следует из названия, умеет работать как в синхронном, так и в асинхронном режимах. А еще в куче других, но они нам пока не интересны. На практике я ни разу не видел его работу в синхронном режиме. Поэтому наравне с USART буду использовать обозначение UART, подразумевая именно асинхронный режим.


Как и с портами, первым делом надо разрешить работу данного модуля. Смотрим в документации, какому биту RCU он соответствует и видим 14-й бит RCU_APB2EN_USART0EN. Следующая особенность GD32VF103 вслед за STM, это необходимость переключения режима работы ножки вывода с обычного GPIO на альтернативную функцию, активируемую значением GPIO_APP50 = 0b1011. Причем только на выход: входная ножка остается обычным GPIO_HIZ. Ах да, в RCU саму возможность работы альтернативных функций тоже придется включить. Делается это 0-м битом, он же RCU_APB2EN_AFEN.


А вот сама настройка UART не представляет ничего сложного: в регистре USART0_CTL0 мы просто разрешаем его работу (USART_CTL0_UEN), включаем передатчик (USART_CTL0_TEN) и приемник (USART_CTL0_REN), после чего в регистре USART0_BAUD задаем скорость обмена как делитель тактовой частоты. Если точнее, не тактовой частоты, а только частоты шины APB2, но пока мы не разбирались с тактированием, частоты всех шин у нас одинаковые и равны 8 МГц:


  la t0, USART0_BASE
    li t1, 8000000 / 9600
  sw t1, USART_BAUD_OFFSET(t0)
    li t1, USART_CTL0_UEN | USART_CTL0_REN | USART_CTL0_TEN
  sw t1, USART_CTL0_OFFSET(t0)

  la t0, USART0_BASE
    li t1, 'S'
  sb t1, USART_DATA_OFFSET(t0)

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


Ну а отправка байта осуществляется просто записью его в USART0_DATA.


Как и в предыдущих случаях, прошиваем контроллер, но перед проверкой работы запускаем на компьютере


$ screen /dev/ttyUSB0 9600

чтобы убедиться что буква 'S' принимается. Для выхода из screen, надо нажать ctrl+a, потом k, потом y.


8. Обмен строками


Обычно передача данных между устройствами не ограничивается одним байтом. А в случае UART можно даже еще больше конкретизировать: обмен идет строками. Значит, и функции обмена будем затачивать на работу со строками. Проблема тут в том, что UART штука медленная: в предыдущем примере мы выставляли всего 9600 бит в секунду, в жизни еще часто встречается 115200. По сравнению даже с 8 МГц частоты ядра, а тем более с 108 МГц это очень долго, значит нам придется подождать пока модуль передаст один байт, чтобы тут же подложить ему следующий. Для этого служит флаг USART_STAT_TBE (Transmit data buffer empty) регистра USART0_STAT.


Таким образом псевдокод на Си будет выглядеть следующим образом:


void uart_puts(char *str){
  while(str[0] != '\0'){
    while(! (USART0_STAT & USART_STAT_TBE) ){}
    USART0_DATA = str[0];
    str++;
  }
}

На ассемблере он выглядит аналогично, единственное что займет немножко больше места, у меня вышло 14 строк.


Таким же способом пишется функция чтения строки, только флаг будем проверять USART_STAT_RBNE (Read data buffer not empty), а конец ввода определять по одному из символов '\r' или '\n' либо по переполнению буфера.


В качестве примера работы с UART можно ввести с терминала строку, преобразовать все строчные буквы с прописные, после чего отправить обратно.


9. Прерывания


Проблема только что описанного способа работы с периферией в том, что на время передачи или приема блокируется вообще все. Причем если на передачу мы можем хотя бы предсказать, какое время это займет, то на прием не можем даже этого. Мало ли, вдруг пользователь уснул за клавиатурой?


Простейший способ это исправить — переписать функции, чтобы они при неготовности интерфейса возвращали управление основной программе, а не крутились в цикле. Такой подход называется опросом (polling) и вполне себе используется для медленных и низкоприоритетных устройств. То есть тех, которые могут потерпеть пока до них дойдет очередь опроса. Реализация такого подхода для UART сложности не представляет, поэтому я ее приводить не буду, а вместо этого рассмотрю более сложный способ — использование прерываний.


Этот способ заключается в том, что при наступлении определенного события, от ошибки деления на ноль до получения здоровенного пакета по USB, генерируется сигнал прерывания. И если это прерывание разрешено локально, разрешено глобально, разрешено на данной периферии и не перекрыто более приоритетным прерыванием (да-да, выполнены должны быть все эти условия), то контроллер спешно бросает выполнение основного кода и переходит к выполнению обработчика прерываний. К чему это может привести?


Самый очевидный плюс — скорость реакции на внешнее событие, которая может составлять единицы тактов. Но рядом со вкусным сыром прячутся грабли: если прерываний слишком много, у контроллера просто не останется ресурсов на выполнение основного кода. Впрочем, такая ситуация в любом случае не должна происходить в грамотно спроектированной системе.
Особенность RISC-V в отличие от многих других контроллеров: поскольку ядро знать не знает что такое стек, оно не может аппаратно сохранить туда контекст выполнения, то есть адрес возврата (нам же нужно знать откуда продолжить работу после завершения обработки прерывания), используемые регистры и все остальное. Поэтому программисту приходится самому думать, куда это все сохранить и при этом не попортить данные основной программы. По большому счету варианта три: либо использовать общий стек, либо раздельный, либо специальные регистры. Специальных регистров архитектурой не предусмотрено, вводить еще одну область памяти и способы работы с ней сложно, поэтому будем пользоваться общим стеком. Нет, в более сложных системах с разделением прав доступа, с планировщиком задач и вообще операционной системой, выделение специального ядерного стека встречается часто. Ну правда, если кривая пользовательская программа убьет себе стек и данные, это плохо, но только для нее. Но если она убьет стек и данные не только себе, но и ядру, пострадают все.
Еще раз напоминаю, что при использовании общего стека надо следить за его целостностью, то есть чтобы запись и чтение никогда не шли ниже sp.


Переходим собственно к прерываниям, а также ловушкам и другим исключительным ситуациям. В контроллере GD32VF103 за них отвечает модуль eclic (Enhanced Core Local Interrupt Controller). Он позволяет гибко настроить способ обработки прерываний, приоритеты и многое другое. Мы начнем, как всегда, с простого варианта, но не упустим шанс все себе усложнить. Собственно исключительные ситуации делятся на три типа: немаскируемые (NMI), ловушки (traps) и прерывания (interrupts). Немаскируемые исключения это слишком страшная штука, трогать мы их не будем. Ловушки срабатывают либо если они были заранее поставлены в нужном месте (точка останова breakpoint или специальные инструкции ecall и ebreak), либо если ядро попыталось выполнить невыполнимое и допустить недопустимое. Скажем, прочитать или записать по невыровненному адресу (не кратному размеру переменной) или выполнить незнакомую инструкцию. Ну а прерывания это события от внешних устройств вроде того же UART`а.


Настройка контроллера прерываний eclic осуществляется при помощи кучи специальных регистров. Причем они настолько специальные, что даже не отображаются на память. Доступ к ним возможен при помощи специальных команд вроде scrr (чтение) или scrw (запись). Первым из таких регистров, необходимых для настройки, является mtvec. Старшие 26 битов его отвечают за хранение адреса обработчика прерывания, а младшие 6 — за режим работы: волшебное число 3 включает eclic, а любое другое — не включает, то есть заставляет использовать предыдущую версию обработчика прерываний clic (для совместимости ее оставили что ли?):


  la t0, trap_entry
    andi t0, t0, ~(64-1) #выравнивание адреса должно быть минимум на 64 байта
    ori t0, t0, CSR_MTVEC_ECLIC
  csrw CSR_MTVEC, t0

Зануление младших битов означает, что при размещении обработчика исключений в памяти надо удостовериться, что в шести младших битах его реального адреса также находятся нули, то есть он выровнен по 64-битной сетке. Это делается директивой ассемблера .align 6:


.align 6
trap_entry:
  push t0
  push t1
  push a0

  la t0, GPIOB_OCTL
  lh t1, 0(t0)
    xori t1, t1, (1<<GLED)
  sh t1, 0(t0)

  la t0, USART0_BASE
    la t1, USART_CTL0_UEN | USART_CTL0_REN | USART_CTL0_TEN
  sw t1, USART_CTL0_OFFSET(t0)
    la t1, 'I'
  sw t1, USART_DATA_OFFSET(t0)

  la a0, 100000
  call sleep

  pop a0
  pop t1
  pop t0
mret

Пока мы хотим работать только с прерываниями от UART`а, можно не заморачиваться с разбором что же произошло на самом деле. Считаем, что в обработчик мы могли попасть только из-за него. Прерывание, которое проще всего проверить — по опустошению буфера передатчика. За его работу в модуле UART отвечает бит USART_CTL0_TBEIE, а как видно из кода, в прерывании мы его снимаем. То есть при входе в прерывание оно тут же запрещает само себя чтобы не уйти в бесконечный цикл. Ну и отправляет символ 'I' чтобы изобразить бурную деятельность. И зеленым светодиодиком мигнет с той же целью. В реальной жизни, естественно, программисты вписывают туда что-то более осмысленное.


Но если мы просто выставим бит USART_CTL0_TBEIE в регистре USART0_CTL0, мы выполним только одно из условий срабатывания прерывания. Помимо этого надо разрешить это же прерывание внутри контроллера eclic и разрешить обработку прерываний глобально. Это делается следующим кодом:


  # Локальное разрешение прерывания USART0 (eclic_int_ie[i] = 1)
  la t0, (ECLIC_ADDR_BASE + ECLIC_INT_IE_OFFSET + USART0_IRQn*4)
    la t1, 1
  sb t1, 0(t0)

  #глобальное разрешение прерываний
  csrrs zero, CSR_MSTATUS, MSTATUS_MIE

Рассмотрим что здесь происходит. По адресам ECLIC_ADDR_BASE + ECLIC_INT_IP_OFFSET находится массив четверок регистров, отвечающих за каждый номер прерывания. В виде псевдокода на Си это можно представить так:


struct{
  uint8_t clicintip; //interrupt pending
  uint8_t clicintie; //interrupt enable
  uint8_t clicintattr; //attributes
  uint8_t clicintctl; //level and priority
}eclic_interrupt[ECLIC_NUM_INTERRUPTS];

  • clicintip — флаг наличия прерывания. Он взводится и сбрасывается автоматически при возникновении соответствующего события. Иногда его можно сбросить и программно.
  • clicintie — флаг разрешения прерывания. Пока он сброшен, состояние clicintip игнорируется. Пока что нас будет интересовать только этот регистр.
  • clicintattr — настройка режима прерывания. Тут можно установить чтобы флаг clicintip выставлялся по уровню или по фронту (изменению сигнала 0->1 или 1->0) а также включить векторный режим. Пока не трогаем.
  • clicintctl — настройка уровней и приоритетов. Тоже не трогаем.

Обратите внимание, что каждый из регистров 8-битный, то есть записывать в него нужно инструкцией sb. Ну то, что в коде это написано как-то сложно связано с тем, как это описано в документации. Это я так понял, что данные регистры прекрасно ложатся на структуру, но у разработчиков, похоже, другое мнение. В любом случае, подобная настройка происходит только во время инициализации, так что неоптимальный подход тут вполне простителен.


За прерывание USART0_IRQ отвечает 56-й элемент массива, точнее его поле clicintie, которое нужно выставить в 1.


Остается только добавить обработчик кнопки, чтобы по нажатию выставлялся флаг USART_CTL0_TBEIE, который разрешит прерывание. И, естественно, сам обработчик прерывания, который этот флаг сбросит.


UPD: В комментариях подняли интересную тему: вот мы рассматриваем подход с общим стеком. Но что делать тем, кто хочет разделить привилегии? То есть основной код выполняется в пользовательском режиме, а исключения — в ядерном, мы ведь не имеем права лезть ни в стек (вдруг юзер его испортил?), ни в регистры. Для этого в нашем контроллере (не уверен что это специфика RISC-V в целом) предусмотрен спецрегистр mscratchcsw. Он служит для обмена значениями между указателем ядерного стека и пользовательского:


csrrw sp, mscratchcsw, sp
  # что-то делаем
csrrw sp, mscratchcsw, sp

10. Обработка ловушек


Поскольку прерывания провоцируются периферией, а не конкретными инструкциями, возврат из них должен осуществляться ровно в то же место, где им довелось возникнуть. Другое дело ловушки. Они срабатывают обычно не по асинхронным внешним событиям, а по вполне конкретным командам вроде упоминавшегося ранее ecall. И если в конце обработчика ловушки вернуться на ту же инструкцию, ловушка тут же сработает снова. Поэтому сначала адрес возврата нужно увеличить на длину команды, что в нашем случае непросто, ведь команды бывают как 16-битные, так и 32-битные. Но пока не будем заострять на этом внимание, все равно ловушками пользоваться практически не придется. Пока что считаем любую команду 32-битной. Это значит что надо считать значение из местного аналога регистра ra, который тут называется mepc, увеличить на 4 и записать обратно.


Но ведь этот же обработчик служит и для прерываний, где увеличивать регистр не надо. Значит придется сначала определить, что послужило причиной нашего попадания сюда. Для этого служит регистр mcause, у которого есть 31-й бит, ровно за это отвечающий. Если он равен 1, то мы в прерывании, если 0 — в ловушке. Не менее эффективно он рассказывает и подробности исключения. Биты 0-11 хранят код ловушки (если 31-й бит равен 0) либо прерывания (если 1).
Кодов ловушек не слишком много:


0 — instruction address misaligned, ошибка выравнивания инструкции
1 — instruction access fault, ошибка доступа к инструкции
2 — illegal instruction, незнакомая инструкция
3 — breakpoint, инструкция ebreak
4 — load address misaligned, ошибка выравнивания памяти
5 — load address fault, ошибка доступа к памяти
6 — store/AMO misaligned, ошибка выравнивания памяти на запись
7 — store/AMO access fault, ошибка доступа к памяти на запись
8 — enviroment call from U-mode, инструкция ecall, вызванная из пользовательского кода
9 — ?
10 — ?
11 — Enviroment call from M-mode, инструкция ecall, вызванная из ядерного кода

Из них проще всего проверить коды 2, 3 и 11.


Для выполнения неверной инструкции (код 2) достаточно всего лишь встроить кусок данных между инструкциями. Например, константу 0xFFFF'FFFF (в коде примера закомментирована).
Команда ebreak (код 3) не так проста как кажется. Я в начале сказал что размер инструкции полагаю 32-битным. Так вот, команда ebreak занимает всего 2 байта. Можно обрабатывать ее отдельно, но я предпочту просто ей не пользоваться.


Инструкция ecall в нашем случае генерирует 11-ю ловушку, а не 8-ю как можно было ожидать. Дело в том, что мы не настраивали разграничение доступа, так что фактически весь наш код считается ядерным. То есть нам можно все и отовсюду.


Для проверки напишем обработчик кнопки чтобы по нажатии программа выполняла либо несуществующую инструкцию (ту самую 0xFFFF'FFFF), либо нормальную ecall. А в обработчике ловушки будем мигать красным светодиодом и правильно двигать адрес возврата.


С прерываниями все аналогично. Помните, как мы настраивали биты разрешения прерываний в eclic? Этот же номер нам приедет в младших битах регистра mcause. Достаточно обрезать его по маске и сравнить с интересующим нас номером. В результате мы почти корректно обрабатываем прерывание от UART`а и никак не реагируем на остальные. "Почти" потому что обрабатываем только опустошение буфера передачи, а всего событий на этом векторе висит немного больше.
В общем, вот код обработчика ловушек:


Скрытый текст
.align 6
trap_entry:
  push t0
  push t1
  push a0

  csrr a0, CSR_MCAUSE
  la t1, (1<<31)
  and t1, a0, t1 #t1 - interrupt / trap
    beqz t1, trap_exception
 #interrupt

  la t0, GPIOB_OCTL
  lh t1, 0(t0)
    xori t1, t1, (1<<GLED)
  sh t1, 0(t0)

  la t0, 0xFFF
  and a0, a0, t0
  la t0, USART0_IRQn
    bne t0, a0, trap_end

  la t0, USART0_BASE
    la t1, USART_CTL0_UEN | USART_CTL0_REN | USART_CTL0_TEN
  sw t1, USART_CTL0_OFFSET(t0)
    la t1, 'I'
  sw t1, USART_DATA_OFFSET(t0)

trap_end:
  la a0, 100000
  call sleep

  pop a0
  pop t1
  pop t0
mret
trap_exception:
  la t0, GPIOB_OCTL
  lh t1, 0(t0)
    xori t1, t1, (1<<RLED)
  sh t1, 0(t0)

  csrr t0, CSR_MEPC
  addi t0, t0, 4
  csrw CSR_MEPC, t0
j trap_end

11. Разделение ловушек и прерываний


В предыдущем примере мы анализировали один бит регистра чтобы разделить две принципиально разные ситуации: сбой алгоритма и внешнее событие. К счастью, контроллер прерываний умеет делать это за нас. Для этого воспользуемся регистром mtvt2: его 30 старших битов хранят адрес обработчика прерываний, 1-й бит не отвечает ни за что, а младший бит является переключателем между раздельными обработчиками (mtvt2 + mtvec) при равенстве 1, либо совмещенном при равенстве нулю. Как и в предыдущем случае, использование для адреса обработчика только старших битов намекает нам на выравнивание по 4-байтной сетке. Так что инициализируем:


  la t0, irq_entry
  csrw CSR_MTVT2, t0 #выравнивание минимум на 4 байта
  csrs CSR_MTVT2, 1

и пишем обработчик. Как и раньше, номер прерывания хранится в младших битах регистра mcause:


Скрытый текст
align 2
irq_entry:
  push t0
  push t1
  push a0

  csrr a0, CSR_MCAUSE
  la t0, 0xFFF
  and a0, a0, t0
  la t0, USART0_IRQn
    bne t0, a0, irq_end

  la t0, USART0_BASE
    la t1, USART_CTL0_UEN | USART_CTL0_REN | USART_CTL0_TEN
  sw t1, USART_CTL0_OFFSET(t0)
    la t1, 'I'
  sw t1, USART_DATA_OFFSET(t0)

  la t0, GPIOB_OCTL
  lh t1, 0(t0)
    xori t1, t1, (1<<YLED)
  sh t1, 0(t0)

  la a0, 100000
  call sleep

irq_end:
  pop a0
  pop t1
  pop t0
mret

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


12. Векторный режим работы прерываний


И вот наконец пришел момент окончательно облениться и заставить контроллер самостоятельно выбирать обработчик в зависимости от произошедшего события. Для этого используется такая структура как таблица векторов прерываний. По не вполне очевидным причинам ее адрес должен быть выровнен на 64, 128, 256, 512, 1024, 2048, 4096, 8192 или 16384 байта в зависимости от размера таблицы. Вероятнее всего, это сделано для упрощения арифметики: номер прерывания просто-напросто побитово складывается (OR) с адресом начала таблицы. У нашего контроллера 86 прерываний, по 4 байта на каждое, то есть 344 байта. Ближайшая степень двойки, в которую они влезают — 512, этому соответствует выравнивание .align 9. В любом случае таблицу прерываний обычно располагают в самом начале кода, то есть по нулевому адресу, так что проблем не возникает. Но на всякий случай пропишем выравнивание явно. Приводить таблицу прерываний полностью я здесь не буду: она очень длинная. Кому надо, посмотрит в документации или примере кода. Отмечу только, что первые 4 байта в ней разработчики предусмотрительно зарезервировали для размещения инструкции прыжка в начало исполняемого кода:


.text
.section .init
...
.align 9
vector_base:
  j _start
  .align    2
  .word     0
  .word     0
  .word     eclic_msip_handler
...
  .word     RTC_IRQHandler
...
  .word     SPI1_IRQHandler
  .word     USART0_IRQHandler
...
.align 2
.text
.global _start
_start:
  la sp, _stack_end
...

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


USART0_IRQHandler:
  push t0
  push a0

  la t0, USART0_BASE
    la a0, USART_CTL0_UEN | USART_CTL0_REN | USART_CTL0_TEN
  sw a0, USART_CTL0_OFFSET(t0)
    la a0, 'U'
  sw a0, USART_DATA_OFFSET(t0)

  la t0, GPIOB_OCTL
  lh a0, 0(t0)
    xori a0, a0, (1<<GLED)
  sh a0, 0(t0)

  la a0, 100000
  call sleep

  pop a0
  pop t0
mret

Адрес таблицы нужно положить в регистр mtvt:


  la t0, vector_base
  csrw CSR_MTVT, t0

И не забываем настроить поле clicintattr уже знакомого нам массива. Оно состоит из двух частей: биты 1 и 2 отвечают за фронты прерывания:


0b00, 0b01 — по уровню, то есть флаг clicintip выставляется постоянно когда на проводке события высокий уровень
0b10 — по фронту, то есть флаг выставляется при переходе проводка из состояния 0 в 1
0b11 — по спаду, то есть при переходе из 1 в 0.
Насколько я понял, это актуально только для внешних прерываний EXTI, когда проводок события непосредственно связан с ножкой контроллера. Для внутренней периферии это безразлично.


Ну а бит 0 отвечает за векторный или не-векторный режим работы. Когда он равен 0 (по умолчанию) прерывание работает в не-векторном режиме, а когда 1 — в векторном.


# Использование векторизованного режима обработки (eclic_int_attr[i] = 1)
  la t0, (ECLIC_ADDR_BASE+ECLIC_INT_ATTR_OFFSET+USART0_IRQn*4)
    la t1, 1
  sw t1, 0(t0)

В общем-то вот и все, отдельное прерывание для UART работает.


Для полного фен-шуя можно еще прописать обработку немаскируемых прерываний, хотя я понятия не имею как их проверять.


  la t0, nmi_entry
  csrs CSR_MNVEC, t0
  li t0, (1<<9)
  csrs CSR_MMISC_CTL, t0

13. Переходим на Си


Смею надеяться, с основами ассемблера и архитектуры контроллера мы познакомились. Теперь можно перейти к языкам высокого уровня. Первым делом разберемся с иерархией. При программировании на Си основной код пишется именно на нем, а ассемблер используется лишь для служебных целей вроде инициализации памяти или прерываний, ну плюс низкоуровневые функции или вставки. Следовательно, наш ассемблерный код переименовывается в startup.S, а на его место создаем main.c. В этот Си`шный файл переносим инициализацию портов, UART'а и все остального, оставляем только копирование секций памяти, стек и инициализацию контроллера прерываний. Также я предлагаю переработать работу с отдельными линиями ввода-вывода. На ассемблере это было сделать сложно, но Си и его препроцессоре несколько удобнее. На них я написал файл pinmacro.h чтобы можно было работать с выводами например так:


#define RLED B, 5, 1, GPIO_PP50
#define SBTN B, 0, 0, GPIO_HIZ
...
GPIO_config( RLED );
GPIO_config( SBTN );
GPO_ON( RLED );
if( GPI_ON( SBTN ) )GPIO_OFF( RLED);

В конце ассемблерного файла нужно вызвать main как обычную функцию. Можно даже передать ей argc и argv. Но после нее добавим бесконечный цикл ну случай если кто-то по глупости сделает return.


  li a0, 0
  li a1, 0
  call main

INF_LOOP:
.weak UnhandledInterruptHandler
UnhandledInterruptHandler:
  j INF_LOOP

Ничего сложного в этом примере делать не будем, просто снова помигаем светодиодами. Даже прерывания пока что удалим, оставив вместо них заглушку:


.weak IRQHandler
IRQHandler:
.weak NMIHandler
NMIHandler:
.weak TrapHandler
TrapHandler:
j UnhandledInterruptHandler

Как видно, любое внешнее событие приводит к бесконечному зависанию. Хорошо бы добавить сюда еще и индикацию ошибки, но в общем случае мы не знаем какая периферия подключена к контроллеру и как через нее вывести код ошибки. Поэтому предоставим возможность обработки высокоуровневому коду. Пусть программист объявит обработчик прерываний UnhandledInterruptHandler и сам разбирается что же пошло не так.


Директивы .weak, с которыми вы могли познакомиться и раньше, если анализировали ассемблерный код таблицы векторов прерываний означают, что если данный символ (адрес, константа, ...) объявлен где-то еще, то использовать надо именно то, другое объявление, а если ничего подобного в коде нет, то здешнее, "слабое".


Если присмотреться к выхлопу дизассемблера, станет понятно, что наш код инициализации может оказаться после Си'шного. Это не проблема, поскольку и туда и обратно мы перемещаемся прыжками по адресам, но для красоты выделим специальную подсекцию .start, которая будет располагаться после таблицы прерываний, но перед основным кодом.


Также если в ассемблерном коде мы сами прописывали адреса всех регистров и битов, то в Си'шном коде принято использовать готовые наработки производителя. Я не уверен что нашел правильные, но пока что особых ошибок там не видел. Пусть лежат рядом с нашим скриптом линковщика в каталоге lib.


14. Прерывания на Си


Поскольку мы уже знаем какие регистры задействовать, можно было все это написать самостоятельно. Но, как я уже говорил раньше, в Си так не принято. Поэтому воспользуемся готовым кодом, который лежит в lib/Firmware/RISCV/drivers/n200_func.c. Он отвечает за настройку контроллера прерываний и предоставляет функции eclic_set_vmode (переклюить в векторный режим) и eclic_enable_interrupt (разрешить данное прерывание). А вот глобального разрешения и запрета прерываний там почему-то нет. Ладно, пишем вручную:


#define eclic_global_interrupt_enable() set_csr(mstatus, MSTATUS_MIE)
#define eclic_global_interrupt_disable() clear_csr(mstatus, MSTATUS_MIE)

Для самого файла n200_func.c имеет смысл описать персональное правило в makefile чтобы не копировать его в src. Если немного его поизучать, можно найти функцию eclic_init, отвечающую за инициализацию настроек всех прерываний (но не их адресов!). Не то чтобы вызывать ее было обязательно, но ведь и лишним не будет. Довольно неприятным побочным эффектом оказалось то, что этот файл требует наличия глобальной переменной SystemCoreClock. Ничего не поделаешь, придется ее объявить.


Как мы видели раньше, внутреннее устройство обработчиков прерываний отличается от обычных функций: приходится сохранять вообще все регистры (в том числе t0-t6), а возврат происходит инструкцией mret вместо обычной ret. Чтобы подсказать Си'шному компилятору что вот эта функция является именно прерыванием, используется специальный атрибут, например так:


__attribute__((interrupt)) void USART0_IRQHandler(void)

Прерываний у нас много, а писать этот атрибут каждый раз лень, поэтому вынесем в файл lib/interrupt_util.h все прототипы обработчиков прерываний с этим атрибутом. Туда же унесем код разрешения и запрета прерываний, про который говорили раньше.


Если посмотреть в дизассемблерный код еще раз, можно увидеть, что main ведет себя не как главный цикл программы, а как обычная функция — сохраняет регистры на стеке, при выходе восстанавливает. Поскольку нам это не нужно, объявим и ее прототип перед обработчиками прерываний, только с другим атрибутом:


__attribute__((naked)) int main();

В качестве примера работы прерываний на Си отлично подойдет наш предыдущий код, по кнопке включающий прерывание, а в обработчике посылающий байт и отключающий себя.


Вот и все, что я хотел себе рассказать о первых шагах в освоении данного контроллера.


Заключение


RISC-V довольно интересная архитектура, изучать которую после тесного общения с AVR и поверхностного с ARM было необычно. Некоторые технические решения показались странными, но не лишенными внутренней логики, как, например, отказ от стека или от статусных регистров. Красиво решена работа с прерываниями: хочешь используй один общий обработчик, а хочешь — каждому устройству свой. Таблицу можно положить не только в начало основного кода или бутлоадер, а вообще куда угодно, лишь бы влезла.


Решение с макросами вроде USART_DATA( USART0 ) или вскользь упомянутых в начале DMA гораздо удобнее, чем структуры в stm32. Нумерация периферии с нуля мне тоже понравилась больше.


Хорошо бы сравнить ассемблер RISC-V с ассемблером ARM, но с последним я почти не знаком, так что много сказать не смогу. Разве что RISC-V несколько проще для понимания: там почти нет инструкций-комбайнов (если не считать li, которая, согласно документации, разворачивается в "Myriad sequences"). С академической точки зрения это прекрасно, но для реальной работы может обернуться небольшим замедлением кода.


Рекомендую ли я эту архитектуру вообще и GD32VF103 в частности к практическому применению? И да и нет. Основной аргумент против это малая распространенность подобных контроллеров. Те же GigaDevice производят по сути только клон stm32f103, то есть довольно слабого контроллера с высоким потреблением. На мой взгляд, лучше бы скопировали stm32l151.


Ну а из плюсов — опять же приятный ассемблер и получение общего представления об устройстве контроллеров, которое пригодится и в работе с контроллерами вообще, и особенно с теми же stm32f103. Опять же существуют компьютерные процессоры, основанные на RISC-V и еще неизвестно за кем будущее — x86, ARM, RISC-V или еще какой-то архитектурой.


Список источников


https://habr.com/ru/post/516006/
https://www.youtube.com/watch?v=M0dEugoU8PM&list=PL6kSdcHYB3x4okfkIMYgVzmo3ll6a9dPZ
https://www.youtube.com/watch?v=LyQcTmNcSpY&list=PL6kSdcHYB3x6KOBxEP1YZAzR8hkMQoEva
https://doc.nucleisys.com/nuclei_spec/isa/eclic.html
http://www.gd32mcu.com/en/download/0?kw=GD32VF1

Tags:
Hubs:
Total votes 16: ↑16 and ↓0+16
Comments12

Articles