В продолжении своего предыдущего поста, хочу рассказать о своем переходе на WCH и RISC-V. Поскольку QingKeV4 существует аж в 5-и версиях, то SDK ко всему этому великолепию содержит рудименты и нестыковки. Возможно, кому-то мой опыт сэкономит время и нервы. Начнем с прерываний.
Итак, в семействах V2XX/V3XX применяются ядра QingKeV4С/F с PFIC. Основных отличий от NVIC в большинстве популярных STM-подобных контроллеров на нашем рынке, два:
В PFIC всего 3 бита приоритетов прерываний (кроме QingKeV4A, где их 4, но семейств CH32V это не касается).
В PFIC есть аппаратный 3-х уровневый (QingKeV4F) или 2-х уровневый (QingKeV4С) стек
Есть еще отличия, но поскольку на уровне SDK я не встречал примеров применения этих PFIC специфических механизмов, сказать мне тут нечего.
Приоритеты прерываний.
Кол-во вытесняющих приоритетов в системе - это nesting depth. Биты приоритета образуют поле PriorityGroup или Priority configuration. В ядре зарезервировано 8 бит под приоритет каждого прерывания, но в текущих реализациях используются старшие 3 бита регистра. Как и в NVIC, биты делятся на биты приоритета группы (Рreempted bits) и биты приоритета внутри группы (Subpriority bits). И как в NVIC, чем меньше числовое значение приоритета, тем он выше. PriorityGroup прерывания располагается в [7:5] битах регистра приоритета прерывания.
Изменять соотношения бит в PriorityGroup можно только в старших CH32V3xx (QingKeV4F). Nesting depth может быть 0,2,4 или 8. Т.е. кол-во preempted bits от 0 до 3. subpriority bits соответственно будут все оставшиеся, для nesting depth = 8 субриоритетов нет.
На CH32V2xx (QingKeV4С) nesting depth всегда 2. ( preempted bits - 1 шт. ; subpriority bits -2 шт.) По крайне мерее каких-то отсылок, что nesting depth можно сделать 0, я не нашел.
С точки зрения API, имеем функцию NVIC_SetPriority(IRQn_Type IRQn, uint8_t priority), (core_riscv.h). Она просто пишет маску priority в соответствующий регистр. Пользоваться в реальной жизни не очень удобно, надо сдвигать маску на 5 влево и думать про nesting depth.
Проще написать свой API и забыть. Но если решили воспользоваться SDK, то может быть следующие:
Поскольку SDК общий и на RISC-V и на ARM контроллеры, в нем есть сh32vxxx_misс.h., С привычной NVIC_Init(NVIC_InitTypeDef *NVIC_InitStruct), где инициализация идет через формат структуры и можно однозначно прописывать приоритет и субприоритет. И есть функция конфигурации битов NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup), однако она физический ничего не делает, а просто меняет значения внутренней переменной NVIC_PriorityGroup. По значению этой переменной NVIC_Init формирует битовое поле приоритета. И вот тут лезут нестыковки.
CH32V3XX. В файле ch32v3xx_misс, который вылезет из MountRiver при генерации проекта, NVIC_PriorityGroup = INTSYSCR_INEST_EN_4Level. Но физический nesting depth задаётся в стартапе. И для примеров работы с периферией - проблем нет. Вот строчка из стартапа
/* Enable interrupt nesting and hardware stack */
li t0, 0x0b
csrw 0x804, t0
0x804 это адрес регистра INTSYSCR .А вот та же самая строчка, если сгенерить из МountRiver проект c FreeRTOS.
/* Enable nested and hardware stack */
li t0, 0x1f
csrw 0x804, t0
И получается что API сконфигурирован на nesting depth = 4, а на самом деле nesting depth = 8. Соответственно прерывания могут работать слегка не так, как вы ожидайте:)
СH32V2XX. . Тут в текущем SDK V2.2 косяк уже исправлен, по крайне мерее при генерации проекта из под MountRiverIDE. Но в начале 2024 года, ch32v2xx_misc был от QingKeV4A, с его 4 бита приоритета и nesting depth по умолчанию 0. Поэтому при использовании NVIC_Init приоритет игнорировался, а также обрезался младший бит субприоритета. Пока лучше проверять файл ch32v2xx_misc. Может быть в каких-то конфигурациях проекта или примерах он вылезет.
HPE (hardware Prologue/Epilogue)
Это аппаратный механизм однотактового сохранения контекста (16 регистров процессора) при входе в обработчик прерывания и восстановления при выходе. Контекст сохраняется в аппаратном стеке. Т.е. вместо 32-х команд - 2 команды. Для общего случая естественно. Свой обработчик на asm мы тут не рассматриваем. Для CH32V3xx, если включен FPU, его регистры f нужно сохранять в обычный стек в любом случае.
Кстати, либо я что то не понимаю, либо все проекты из MountRiver для CH32V3xx по умолчанию не используют при компиляции FPU. В стартапе, для CH32V3xx FPU всегда включается, а в настройках TragetProcessor проекта по умолчанию - нет...
Обработчик прерывания, для которого нужно использовать HPE, объявляется с атрибутом: __attribute__((interrupt("WCH-Interrupt-fast"))).
Для интересу посмотрел как выглядит в листингах обработчик с HPE и без него. Действительно при использовании HPE сохраняется только контекст FPU. Но вот как из под Си сказать компилятору, что это делать не надо, я не нашел.
HardFault при переполнении HPE.
Очень неприятная штука, возникает если бездумно объявлять прерывания как __attribute__((interrupt("WCH-Interrupt-fast"))). Как это сделал я:). Возможна она только на серии CH32V3xx, которая поддерживают 3-х уровнённый HPE буфер. Возникает, когда nesting depth 4 или 8, и при этом в системе 4 и более прерываний с HPE имеют разный приоритет, Т.е. у нас 3 прерывания заполнили буфер HPE. И тут прилетает 4-е, с самым высоким приоритетом. В ситуации, когда переполнение HPE разрешено ( а оно разрешено в стартапе по дефолту), 4-е прерывание сохранит контекст в программном стеке, но после завершения выполнения, контекст будет загружен из аппаратного стека. Как это происходит, я не где информации не нашел, но в hard fault система падает стабильно:)
Можно либо отключить переполнение в стартапе (тогда, даже если у HPE прерывания будет высокий приоритет, он не будет обрабатываться), либо программно отслеживать переполнения HPE в обработчике. Но самое простое, делать как рекомендует WCH. Объявлять HPE прерывания с самыми низкими приоритетами. Тогда аппаратный стек не сможет переполниться, поскольку низкоприоритетные прерывания никого ни куда не вытеснят. .Получиться вот так:
. С СH32V2 такой проблемы нет, там nesting depth =2 и HPE стек 2-х уровневый.
WCH порт FreeRTOS.
Порт вполне себе рабочий, но есть нюансы. Прерывание SysTick объявлено c HPE. Приоритет самый низкий - 7. Это важно, если решили использовать HPE. В системе уже одно такое прерывание есть. С таким же приоритетом SoftwateIRQ. Но оно обычное, без HPE.
Хотя в PFIC есть маскирование прерываний, порт FreeRTOS его не поддерживает. Соответственно configLIBRARY_LOWEST_INTERRUPT_PRIORITY в порте нет. Т.е. в отличии от портов ARM, тут нет прерываний, в которых нельзя использовать API FreeRTOS. Как собственно и прерываний, которые могут вклиниваться к критическую секцию операционной системы. portDISABLE_INTERRUPTS() глобально запрещает все прерывания, без всякого маскирования. Видимо порт делался под младшие семейства, и под CH32V3 переехал без исправлений.
В остальном со стороны Си все как на ARM. Самое стременное было переполнение HPE. Отсутствие опыта наложилось на кривое назначение приоритетов и скрытый HPE в SysTick. Но реально, что там было понял только сейчас. Тогда я выключил HPE в своих прерываниях и пошел яростно жмакать клавиши дальше. Ибо путь самурая. Безальтернативно заложен в проект CH32V307. Прототип на столе, аванс потрачен, пути назад нет.
Комментарии (2)
COKPOWEHEU
22.11.2024 12:13Кстати, либо я что то не понимаю, либо все проекты из MountRiver для CH32V3xx по умолчанию не используют при компиляции FPU
Речь идет о примерах от WCH? Так-то никто не запрещает указать архитектуру
-march=rv32imafc_zicsr
и пользоваться FPU в свое удовольствие.__attribute__((interrupt("WCH-Interrupt-fast")))
Здесь стоило уточнить, что это нестандартное расширение от wch, обычный gcc его не поддерживает. Есть несколько костылей вроде вот такого:
void TIM3_IRQHandler(void) __attribute__((naked)); //Это чтобы не компилятор не сохранял контекста и не делал обычного возврата void TIM3_IRQHandler( void ) //Это чтобы сохранить регистры s0 и s1, если они используются в вызываемом коде и чтобы сделать подходящий возврат mret вместо ret { __asm volatile ("call TIM3_IRQHandler_Real; mret"); //Вызов naked-кода для обработки и возврат из прерывания } void TIM3_IRQHandler_Real( void ) { /* Interrupt handler that works with upstream GCC */ }
Но тоже не идеально. Скажем, с FPU это вообще не работает.
Контроллер прерываний PFIC
Стоило указать еще одну забавную особенность местного контроллера прерываний. Он может работать аж в трех режимах: общий обработчик на все прерывания, таблица адресов и таблица переходов. В первом варианте программист вручную должен проверять
mcause
чтобы узнать какое именно прерывание возникло. Во втором используется таблица, заполненная адресами обработчиков. И в третьем - таблица, заполненная командами переходов.Зачем это сделано - без понятия.
nikolz