- PVSM.RU - https://www.pvsm.ru -

Недавно мне в руки противоестественными путями попал интересный представитель RISC-V контроллеров производства НИИЭТ. Упакован он в пластиковый lqfp100 корпус, в котором скрывается ядро на 50 МГц, мегабайт флеш-памяти и 256 кБ оперативки. Разумеется, в наличии и стандартная периферия вроде UART-ов, SPI и USB. А вот из необычного — сигма-дельта АЦП на 16 бит. Ну и всякая неинтересная периферия вроде аппаратных модулей шифрования. Сразу оговорюсь, что тыкаю палочкой я его меньше двух недель, поэтому здесь описаны именно первые впечатления.

Корпус у контроллера большой, стоногий. Что любопытно, НИИЭТ расположили выводы портов GPIO по порядку, а не хаотично как какие-нибудь stm. Разве что между PB7 и PB8 не удержались и воткнули-таки одно из питаний. Это приятно. А вот что неприятно, так это куча абсолютно бесполезных AT_IN и WAKEUP, которым зачем-то отвели индивидуальные ноги вместо того, чтобы совместить с чем-то полезным. Да и USB могли бы убрать в альтернативную функцию, все равно, если верить еррате, он едва работает. А вот вынесение аналоговых входов понять можно: вероятно побоялись наводок на них от цифровых цепей. В конце концов, если уж ставить 16-битный АЦП, глупо портить сигнал посторонними шумами. На это же указывает наличие отдельной ножки AREF, выводов под соответствующие конденсаторы и вообще группировка всей аналоговой части в одном углу.
Еще в глаза бросается, что выводы SPI назвали нетрадиционно: вместо обычных MISO, MOSI обозвали их RX, TX. Впрочем, переключение режима ведущий — ведомый прямо на лету требуется не так уж часто, наверное это не доставит слишком больших проблем.
В отличие от тех же stm32 (и контроллеров, которые создавались под впечатлением от них — gd32, ch32), флеш-память начинается не с 0x0800'0000, а с 0x8000'0000, ОЗУ с 0x4000'0000 (второе ОЗУ — с 0x1000'0000), а периферия с 0x2000'0000. А вот бутлоадера не завезли, поэтому его придется изобретать вручную.
Стартует камень на встроенном RC-генераторе на 1 МГц, но программно можно переключиться на внешний кварц или ФАПЧ до 50 или 60 МГц (в документации встречаются и то, и другое). И тут, внимание, грабли: частота встроенного RC-генератора довольно сильно отличается от заявленной. В моем экземпляре она оказалась примерно 947 кГц — более чем на 5% ниже обещанного. Это настолько большая погрешность, что не позволяет обеспечить работу UART без ручной подстройки.
Стартап, расположенный в официальном репозитории [1], был, похоже, взят от существенно более сложного ядра. Там неоднократно упоминаются Hart-ы, вызывается прямая проверка поддерживается ли FPU, предприняты специальные ухищрения для получения позиционно-независимых адресов. В относительно несложном микроконтроллере все это ни к чему, поэтому я его переписал [2]. Из интереса — на Си. Получилось чуть компактнее, но менее универсально. И, честно говоря, учитывая количество ассемблерных вставок, смысла от Си в стартапе для RISC-V немного. Отдельно отмечу вот этот участок стартапа от НИИЭТ (чуть сократил):
.macro load_addrword_abs reg, sym
.option push
.option norelax
lui reg, %hi(sym)
addi reg, reg, %lo(sym)
.option pop
.endm
...
load_addrword_abs gp, __global_pointer$
Здесь в регистр gp записывается абсолютное значение переменной __global_pointer$, объявленной в ld-скрипте. Обычное присвоение выглядело бы как la gp, __global_pointer$, развернулось бы в auipc + addi и записало бы в регистр значение, вычисленное относительно pc. Отличие в том, что "фирменный" стартап может быть прошит не только в 0x8000'0000, но и в любое другое место без перекомпиляции. Вот только пользу из этого извлечь можно только когда и весь остальной код написан в том же стиле. Под контроллеры же обычно позиционно-независимый код не пишут, и все наши обращения к переменным из Си будут развернуты в тот же la, что сводит смысл от страдания со стартапом на нет. Поэтому в своей версии я оставил la.
Ах да, еще "фирменный" стартап требует наличия функций memset и memcpy. Они, конечно, есть в libgcc, но его подключение в мейкфайле тоже может отказаться далеко не таким тривиальным, как хотелось бы. Разумеется, я говорю про обычный risc-v gcc из репозитория.
Как уже было сказано, бутлоадер в данный контроллер не записали. И, зная любовь различных производителей делать бутлоадеры ни с чем несовместимыми, возможно, оно и к лучшему… Но вот что не сделали отдельной области памяти и отдельной ножки, конечно, недочет. Поэтому прошивка по умолчанию возможна только через JTAG при помощи пропатченного openocd [3]. Ну хоть не стали изобретать свои проприетарные протоколы, как, скажем, WCH, камни которого шьются только их же wch-link-ом. В данном же случае можно обойтись обычным программатором на микросхеме ft2232. У меня, правда, нашелся только ft4232, но по сути это тоже самое. На всякий случай выложу приблизительное заклинание прошивки (хотя особой нужды в этом и нет. Спасибо НИИЭТам, свои примеры они не скрывают: niiet_riscv_sdk/tools/openocd/openocd-snippets/README.md [4] ):
../xpack-openocd-k1921vk-0.12.0-k1921vk/bin/openocd -f ft4232.cfg -f k1921vg015.cfg -c 'init' -c 'reset halt' -c 'program firmware.bin 0x80000000 verify' -c 'reset run' -c 'exit'
Внимание, грабли: прошивка по JTAG не может полноценно поресетить контроллер. В регистрах сохраняются старые значения, и это здорово мешает. Лично я пока что использую костыль с ручным ресетом кнопкой. Возможно, есть какая-то команда и для openocd чтобы он все-таки ресетил контроллер по-нормальному.
Также, я думаю, здесь самое место рассказать об одной интересной ножке, SERVEN. Если ее подтянуть к питанию во время старта, контроллер перейдет в сервисный режим, в котором его можно только стереть. Сначала я подумал, что это просто еще она "лишняя" ножка, которой разработчики не нашли применения (как те же AT_IN). Но нет. Иногда, записывая некоторые значения в регистры, контроллер можно превратить в кирпич. Причем настолько качественно, что JTAG к нему подключиться не может. В этом случае ножку SERVEN можно замкнуть на питание (после чего JTAG все-таки подключается, но полноценно все равно не взаимодействует) и подать команду сервисного стирания. Это важно: не обычного, а именно сервисного. В общем, себе я на всякий случай написал отдельную команду для makefile:
unbrick:
echo "Connect SERVEN to VCC, reset controller and press Enter"
read
ssh $(virt) "cd /home/user/prog/vg015/rem ; "
"../xpack-openocd-k1921vk-0.12.0-k1921vk/bin/openocd -f ft4232.cfg -f k1921vg015.cfg -c 'init' -c 'reset halt' -c 'mww 0x3000F104 0x00000100' -c 'mdw 0x3000F104' -c 'reset run' -c 'exit'"
Портов у нас три. Как я уже сказал, они удобно сгруппированы по трем сторонам корпуса, и ноги в них идут подряд. У каждой ноги есть хотя бы две альтернативные функции — I2C, Timer,… ну, все как обычно. А вот работа с ними уже необычна. Вместо одного — двух регистров, отвечающего за режим работы, вход — выход, наличие подтяжки и т.д., в которые можно писать нули и единицы, здесь многие регистры организованы по принципу set — clear. Например, в регистр OUTENSET можно записать единицу, это переведет соответствующую ножку в режим выхода. Но вот запись нуля не повлияет ни на что. Чтобы вернуть ее в режим входа, надо записать единицу в другой регистр, OUTENCLR. Такой подход проявляется повсюду. Вероятно, разработчики хотели сделать работу с периферией максимально атомарной, но при первом знакомстве это изрядно ломает .
А вот с регистром выхода DATAOUT они, наоборот, недожали. Да, у него есть DATAOUTSET, DATAOUTCLR и даже DATAOUTTGL, но вот атомарной установки по маске (аналог GPIO->BSRR в stm32, где можно было в reset-половину записать маску, а в set — значение) как раз нет. Ну, по крайней мере, я не нашел.
Альтернативные функции порта задаются в "обычном" (не set-clear) регистре ALTFUNCNUM (а вот само их включение — как раз в set-clear паре ALTFUNCSET, ALTFUNCCLR). Причем сами номера альтернативных функций не прописаны нигде. Есть предположение, что они соответствуют положению вот в этой таблице

То есть для PA4 альтернативной функцией 1 будет UART2_RX, функцией 2 будет TMR1_CCIA, а функцией 3 — QSPI_CLK. Как минимум, для UART это предположение подтверждается, но как на самом деле, пока не знаю.
Также из таблицы видно, что альтернативные функции назначаются не для периферии, а для самих портов. То есть можно настроить, например, UART0_RX на PA0, а на UART0_TX не PA1, а PB7. Более того, можно настроить и PA1, и PB7 на UART0_TX, выход UART пойдет на обе ножки. Что будет, если настроить две ножки на RX, я проверять не рискнул.
Что еще интересно в регистрах 1921вг015, так это то, что в заголовочном файле разработчики прописали не только битовые маски, но и битовые поля:
/*-- DATAOUTSET: Data output set bits register ---------------------------------------------------------------*/
typedef struct {
uint32_t PIN0 :1; /*!< Data output set bit 0 */
uint32_t PIN1 :1; /*!< Data output set bit 1 */
uint32_t PIN2 :1; /*!< Data output set bit 2 */
uint32_t PIN3 :1; /*!< Data output set bit 3 */
uint32_t PIN4 :1; /*!< Data output set bit 4 */
uint32_t PIN5 :1; /*!< Data output set bit 5 */
uint32_t PIN6 :1; /*!< Data output set bit 6 */
uint32_t PIN7 :1; /*!< Data output set bit 7 */
uint32_t PIN8 :1; /*!< Data output set bit 8 */
uint32_t PIN9 :1; /*!< Data output set bit 9 */
uint32_t PIN10 :1; /*!< Data output set bit 10 */
uint32_t PIN11 :1; /*!< Data output set bit 11 */
uint32_t PIN12 :1; /*!< Data output set bit 12 */
uint32_t PIN13 :1; /*!< Data output set bit 13 */
uint32_t PIN14 :1; /*!< Data output set bit 14 */
uint32_t PIN15 :1; /*!< Data output set bit 15 */
} _GPIO_DATAOUTSET_bits;
Благодаря этому выставить PA3 в лог.1 можно не только наложением маски GPIOA->DATAOUTSET = (1<<3);, но и обращением к соответствующему битовому полю GPIOA->DATAOUTSET_bit.PIN3 = 1;. Не то чтобы это сильно на что-то влияло, но подход интересный. Опять же, когда битовые поля состоят из нескольких битов, запись может получиться чуть более компактной.
А вот тут разработчики решили обойтись необходимым минимумом. В стандарте RISC-V описана единственная точка входа в обработчик, вот и нам хватит. Никаких таблиц векторов прерываний, никаких таблиц адресов. Даже разделения на прерывания и исключения — и то нет. При любом событии ядро прыгает по адресу mtvec, а дальше уже пусть программисты разбираются. Вот только программисты немного схалтурили:
void PLIC_MachHandler(void) {
uint32_t isr_num = PLIC_ClaimIrq(Plic_Mach_Target);
if(mach_plic_handler[isr_num] != NULL_IRQ) {
mach_plic_handler[isr_num]();
PLIC_ClaimComplete(Plic_Mach_Target, isr_num);
}
}
...
void trap_handler (void)
{
uint32_t mcause_val = read_csr(mcause);
if((mcause_val & MCAUSE_INTERRUPT_FLAG) == 0) {
// handle exception
switch (mcause_val & MCAUSE_EXCEPT_MASK)
{
case MCAUSE_EXCEPT_INSTRADDRMISALGN:
break;
case MCAUSE_EXCEPT_INSTRACCSFAULT:
break;
case MCAUSE_EXCEPT_INSTRILLEGAL:
break;
case MCAUSE_EXCEPT_BREAKPNT:
break;
case MCAUSE_EXCEPT_LOADADDRMISALGN :
break;
case MCAUSE_EXCEPT_LOADACCSFAULT:
break;
case MCAUSE_EXCEPT_STAMOADDRMISALGN:
break;
case MCAUSE_EXCEPT_STAMOACCSFAULT:
break;
case MCAUSE_EXCEPT_ECALLFRM_M_MODE:
break;
default: // MCAUSE_EXCEPT UNKNOWN
break;
}
while(1) {}; //TRAP
} else {
// handle interrupt
PLIC_MachHandler();
}
}
Если присмотреться, сначала идет проверка mcause чтобы выяснить прерывание произошло или исключение. И если второе — программа просто-напросто зависает. Даже без возможности добавить юзерский обработчик. Ну а для прерываний они просто разместили в оперативной памяти таблицу mach_plic_handler указателей на функции обработчиков. И тут даже копированием из более сложного проекта, как в случае стартапа, оправдать нельзя. Ну разве ж это дело, молча зависать при любом исключении! В общем, со временем придется эту функцию переписывать.
А еще я не нашел способа сбросить флаг прерывания программно, без вызова обработчика. Но тут, возможно, просто плохо искал.
(мнение Бориса, @Debian_ks)
Вчера разбирался с дельта-сигма АЦП. По факту каналы ch0 — ch6 заработали, канал ch7 молчит, даже флаг DATAUPD не выставляется. Помимо этого каналы ch0 — ch6 смещены вниз каждый от -920 до -1020 отсчетов. Кроме этого в SVD файле и K1921VG015.h есть некий регистр DIFF который не описан в руководстве пользователя.
Ну и как же без реализации чего-нибудь красивого и бесполезного. Как-то так сложилось, что у меня одной из первых прошивок код новые контроллеры оказывается реализация трехмерной графики. Так было с stm32 [6], так было с gd32. Так же получилось и с 1921вг015. Разумеется, код [7] ни в коем случае не может служить примером для подражания, но демка [8] это демка, много от нее не требуется. Частота ядра и частота SPI 60 МГц, частота обновления 11 — 12 кадров в секунду, причем ограничена она в основном SPI, а не расчетами.

Вот такой вот камень мне довелось пощупать.
Начнем с недостатков. Крайне неточный HSI. Отсутствие таблицы прерываний. Отсутствие бутлоадера. Поддерживается прошивка обычными JTAG-программаторами (это-то достоинство), но все же нужен пропатченный openocd. Целых семь ног отвели под какую-то ерунду — AT_IN, WKUP, плюс еще две под USB. Сами-то функции, может, и могут где пригодиться, но их стоило совместить с GPIO. Слабый USB (даже без учета ерраты, всего 4 конечные точки). Суровая необходимость в ножке SERVEN (иначе говоря, возможность окирпичить контроллер просто записью неверного значения в регистр).
Достоинства. Необычность: что распределение памяти, что подход к регистрам не похож на то, что я видел в stm32; довольно интересно его изучать. Большой объем памяти: 1 МБ флеша и 256+64 кБ ОЗУ. Выводы портов расположены по порядку, а не абы как. Поддерживается прошивка обычными JTAG-программаторами. Мощные аналоговые модули (АЦП, компараторы — теоретически; в реальности я их не проверял).
Возможно, достоинства, но лично мне применить их некуда. Аппаратные блоки подсчета CRC-сумм, хеширования, криптографии (AES-128, AES-256, "Кузнечик", "Магма"), CAN.
В целом, камень весьма интересный. Косяков, конечно, много, но будем надеяться, что НИИЭТ в будущих разработках их исправит. Да и те, что есть, обойти почти всегда возможно. Полагаю, что применение ему в каких-то устройствах найдется.
Автор: COKPOWEHEU
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/risc-v/411034
Ссылки в тексте:
[1] официальном репозитории: https://gitflic.ru/project/niiet/niiet_riscv_sdk
[2] переписал: https://github.com/COKPOWEHEU/1921vg015/blob/main/lib/Source/startup.c
[3] пропатченного openocd: https://github.com/DCVostok/openocd-k1921vk/releases/tag/v0.12.0-k1921vk
[4] niiet_riscv_sdk/tools/openocd/openocd-snippets/README.md: https://gitflic.ru/project/niiet/niiet_riscv_sdk/blob?file=tools%2Fopenocd%2Fopenocd-snippets%2FREADME.md&branch=master&mode=markdown
[5] мозг: http://www.braintools.ru
[6] stm32: https://habr.com/ru/articles/496046/
[7] код: https://github.com/COKPOWEHEU/1921vg015/tree/main/ili9341_3D
[8] демка: https://github.com/COKPOWEHEU/1921vg015/blob/main/ili9341_3D/res.mp4
[9] Источник: https://habr.com/ru/articles/883220/?utm_source=habrahabr&utm_medium=rss&utm_campaign=883220
Нажмите здесь для печати.