habrahabr

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

  • среда, 19 февраля 2025 г. в 00:00:09
https://habr.com/ru/articles/883220/

рис.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 без ручной подстройки.


Стартап, расположенный в официальном репозитории, был, похоже, взят от существенно более сложного ядра. Там неоднократно упоминаются Hart-ы, вызывается прямая проверка поддерживается ли FPU, предприняты специальные ухищрения для получения позиционно-независимых адресов. В относительно несложном микроконтроллере все это ни к чему, поэтому я его переписал. Из интереса — на Си. Получилось чуть компактнее, но менее универсально. И, честно говоря, учитывая количество ассемблерных вставок, смысла от Си в стартапе для 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. Ну хоть не стали изобретать свои проприетарные протоколы, как, скажем, WCH, камни которого шьются только их же wch-link-ом. В данном же случае можно обойтись обычным программатором на микросхеме ft2232. У меня, правда, нашелся только ft4232, но по сути это тоже самое. На всякий случай выложу приблизительное заклинание прошивки (хотя особой нужды в этом и нет. Спасибо НИИЭТам, свои примеры они не скрывают: niiet_riscv_sdk/tools/openocd/openocd-snippets/README.md ):


../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. Такой подход проявляется повсюду. Вероятно, разработчики хотели сделать работу с периферией максимально атомарной, но при первом знакомстве это изрядно ломает мозг.


А вот с регистром выхода 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, так было с gd32. Так же получилось и с 1921вг015. Разумеется, код ни в коем случае не может служить примером для подражания, но демка это демка, много от нее не требуется. Частота ядра и частота 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.


В целом, камень весьма интересный. Косяков, конечно, много, но будем надеяться, что НИИЭТ в будущих разработках их исправит. Да и те, что есть, обойти почти всегда возможно. Полагаю, что применение ему в каких-то устройствах найдется.