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

Первые впечатления от 1921вг015, отечественного RISC-V контроллера

рис.1

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

1. Корпус и плата

Рис.2 Распиновка

Корпус у контроллера большой, стоногий. Что любопытно, НИИЭТ расположили выводы портов GPIO по порядку, а не хаотично как какие-нибудь stm. Разве что между PB7 и PB8 не удержались и воткнули-таки одно из питаний. Это приятно. А вот что неприятно, так это куча абсолютно бесполезных AT_IN и WAKEUP, которым зачем-то отвели индивидуальные ноги вместо того, чтобы совместить с чем-то полезным. Да и USB могли бы убрать в альтернативную функцию, все равно, если верить еррате, он едва работает. А вот вынесение аналоговых входов понять можно: вероятно побоялись наводок на них от цифровых цепей. В конце концов, если уж ставить 16-битный АЦП, глупо портить сигнал посторонними шумами. На это же указывает наличие отдельной ножки AREF, выводов под соответствующие конденсаторы и вообще группировка всей аналоговой части в одном углу.

Еще в глаза бросается, что выводы SPI назвали нетрадиционно: вместо обычных MISO, MOSI обозвали их RX, TX. Впрочем, переключение режима ведущий — ведомый прямо на лету требуется не так уж часто, наверное это не доставит слишком больших проблем.

2. Карта памяти и стартап

В отличие от тех же 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 из репозитория.

3. Прошивка

Как уже было сказано, бутлоадер в данный контроллер не записали. И, зная любовь различных производителей делать бутлоадеры ни с чем несовместимыми, возможно, оно и к лучшему… Но вот что не сделали отдельной области памяти и отдельной ножки, конечно, недочет. Поэтому прошивка по умолчанию возможна только через 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'"

4. GPIO

Портов у нас три. Как я уже сказал, они удобно сгруппированы по трем сторонам корпуса, и ноги в них идут подряд. У каждой ноги есть хотя бы две альтернативные функции — I2C, Timer,… ну, все как обычно. А вот работа с ними уже необычна. Вместо одного — двух регистров, отвечающего за режим работы, вход — выход, наличие подтяжки и т.д., в которые можно писать нули и единицы, здесь многие регистры организованы по принципу set — clear. Например, в регистр OUTENSET можно записать единицу, это переведет соответствующую ножку в режим выхода. Но вот запись нуля не повлияет ни на что. Чтобы вернуть ее в режим входа, надо записать единицу в другой регистр, OUTENCLR. Такой подход проявляется повсюду. Вероятно, разработчики хотели сделать работу с периферией максимально атомарной, но при первом знакомстве это изрядно ломает мозг [5].

А вот с регистром выхода DATAOUT они, наоборот, недожали. Да, у него есть DATAOUTSET, DATAOUTCLR и даже DATAOUTTGL, но вот атомарной установки по маске (аналог GPIO->BSRR в stm32, где можно было в reset-половину записать маску, а в set — значение) как раз нет. Ну, по крайней мере, я не нашел.

Альтернативные функции порта задаются в "обычном" (не set-clear) регистре ALTFUNCNUM (а вот само их включение — как раз в set-clear паре ALTFUNCSET, ALTFUNCCLR). Причем сами номера альтернативных функций не прописаны нигде. Есть предположение, что они соответствуют положению вот в этой таблице

рис.3. Таблица альтернативных функций

То есть для 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;. Не то чтобы это сильно на что-то влияло, но подход интересный. Опять же, когда битовые поля состоят из нескольких битов, запись может получиться чуть более компактной.

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

А вот тут разработчики решили обойтись необходимым минимумом. В стандарте 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 указателей на функции обработчиков. И тут даже копированием из более сложного проекта, как в случае стартапа, оправдать нельзя. Ну разве ж это дело, молча зависать при любом исключении! В общем, со временем придется эту функцию переписывать.

А еще я не нашел способа сбросить флаг прерывания программно, без вызова обработчика. Но тут, возможно, просто плохо искал.

6. АЦП

(мнение Бориса, @Debian_ks)

Вчера разбирался с дельта-сигма АЦП. По факту каналы ch0 — ch6 заработали, канал ch7 молчит, даже флаг DATAUPD не выставляется. Помимо этого каналы ch0 — ch6 смещены вниз каждый от -920 до -1020 отсчетов. Кроме этого в SVD файле и K1921VG015.h есть некий регистр DIFF который не описан в руководстве пользователя.

7. Бонус, или "извините, не смог удержаться"

Ну и как же без реализации чего-нибудь красивого и бесполезного. Как-то так сложилось, что у меня одной из первых прошивок код новые контроллеры оказывается реализация трехмерной графики. Так было с stm32 [6], так было с gd32. Так же получилось и с 1921вг015. Разумеется, код [7] ни в коем случае не может служить примером для подражания, но демка [8] это демка, много от нее не требуется. Частота ядра и частота SPI 60 МГц, частота обновления 11 — 12 кадров в секунду, причем ограничена она в основном SPI, а не расчетами.

рис.4. Трехмерная графика

Заключение

Вот такой вот камень мне довелось пощупать.

Начнем с недостатков. Крайне неточный 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