вторник, 23 августа 2022 г.

Осваиваем GigaDevice GD32

Мы делаем свою операционную систему... которая поддерживает функционал C11 и собирается с использованием GCC.
GigaDevice не помог с этим. Загрузчик и примеры GD ориентированы на Keil и JAR. Постараюсь конспективно изложить, что нужно для разработки HAL с нуля и как действовать при виде нового контроллера с несовместимым API.

1.2 Индентификация системы

Порядок индентификации: определить ядро процессора (SCB_CPUID), определить к какому семейству контроллеров относится, размер памяти и уникальный идентификатор. При прочих равных в плату можно запаять совместимые по выводам контроллеры нескольких производителей. Устройство должно выдавать отладочную информацию: микроконтроллер и размер памяти. Это необходимо для внутрисхемной диагностики и обслуживания, внутрисхемной прошивки.
Идентификация ядра CPUID_CORE_ID BITS(4,15):
	CORTEX_M0   = 0xC20, ARMv6-M (0xC)
	CORTEX_M0P  = 0xC60, ARMv6-M
	CORTEX_M1   = 0xC21, ARMv6-M
	CORTEX_M3   = 0xC23, 
	CORTEX_M4   = 0xC24,
	CORTEX_M7   = 0xC27, ARMv7-M (0xF)
	CORTEX_M23  = 0xD20, ARMv8-M baseline (0xC)
	CORTEX_M33  = 0xD21, ARMv8‑M with Main Extension (0xF)
	CORTEX_M35P = 0xD31,
	CORTEX_M55  = 0xD22,
	CORTEX_M85  = 0xD23, ARMv8.1‑M with Main Extension (0xF)
Идентификация контроллера начинается с выбора адреса регистра отладки DBG/DBGMCU. Адрес регистра отладки зависит от ядра микроконтроллера.
ID code register (DBG_ID)
DBGMCU_IDCODE    0xE004 2000
DBG_ID_GD32E10X  0xE004 2000
DBGMCU_IDCODE_G4 0xE004 2000
DBGMCU_IDCODE_F7 0xE004 2000

DBGMCU_IDCODE_L0 0x4001 5800
DBG_ID_GD32E23X  0x4001 5800
DBGMCU_IDCODE_G0 0x4001 5800

DBGMCU_IDCODE_L5 0xE004 4000
DBGMCU_IDCODE_U5 0xE004 4000
DBG_ID_GD32E50X  0xE004 4000

DBGMCU_IDCODE_H7 0x5C00 1000
Глядя на таблицу, кажется адреса архитектурно зависимы. Если ядро Cortex-M0 или Cortex-M23 -- один адрес, если CM3/CM4 - другой, CM33.. . Размер флеш памяти определяется в зависимости от производителя и архитектуры, см. Memory density information. Для контроллеров GD32 находим два регистра...
Base address: 0x1FFF F7E0
SRAM_DENSITY 	BITS(16,31)
FLASH_DENSITY	BITS( 0,15)
Уникальный идентификатор Unique device ID (96 bits)

MCU         CPUID  DEV_ID  REV_ID 	UNIQUE_ID    MEM.DENSITY
GD32F1X0	0xC23  0x410   0x1303	0x1FFF F7AC  0x1FFF F7E0
GD32F10X	0xC23  0x410			0x1FFF F7E8  0x1FFF F7E0
GD32E10X	0xC24  0x410   0x1712	0x1FFF F7E8  0x1FFF F7E0
GD32F3X0	0xC24  0x410   0x1704	0x1FFF F7AC  0x1FFF F7E0
GD32F30X	0xC24					0x1FFF F7E8  0x1FFF F7E0
GD32F4XX	0xC24					0x1FFF 7A20  0x1FFF 7A10
STM32F75X		   0x449			0x1FF0 F420  0x1FF0 F442
STM32F72X          0x452            0x1FF0 7A10  0x1FF0 7A22
GD32E23X	0xD20  0x410   0x1909	0x1FFF F7AC  0x1FFF F7E0
GD32E50X 	0xD21  0x444			0x1FFF F7E8  0x1FFF F7E0
STM32U5xx   0x	   0x482   0x2001   0x0BFA 0700  0x0BFA 07A0
GD32VF10X                   		0x1FFF F7E8  0x1FFF F7E0

Часть 1.3. Программатор

Попробовал использовать ST-Link/V2 Programmer для прошивки GD32E10X -- работает, размер флеша определился корректно. Ревизия не известная, пишит, что найден "STM32F103 medium-density". Для GD32E23X и GD32E50X ST-Link не подходит. Прошивалку делаем из отладочной платы GD32E103R. Дополнительно изучаем тему изготовления CMSIS-DAP/DAPLink адаптера согласно рекомендациям и спецификациям ARM CMSIS-DAP.

Что-то работает не корректно. Основная проблема идентификации, что не распознаются признаки защиты памяти, OptionBytes -- выставление опций вызвает непоправимые изменения. Программатор должен знать, где располагаются, как и за что отвечают OptionBytes. Осознание проблемы приводит к некоторой модели идентификации контроллера программатором. У программатора должна быть база известных контроллеров, со своими адресами для идентификации. Идентификация начинается с CPUID, далее необходимо распознать опциональное оборудование ядра CoreSight, далее распознается DEV_ID и REV_ID. По трем параметрам (CPUID,DEV_ID,REV_ID) находим в базе микроконтроллер. Далее находим файл конфигурации периферии, например в формате CMSIS-SVD. ARM предлагает производителю контроллера cтандартный подход для встраивания поддержки в свою экосистему. Этот же подход нужен для создания инструмента программирования контроллеров.

  • Создание файлов начальной загрузки startup_{device}.s -- Reset_Handler и таблица векторов прерываний.
  • Создание файлов system_{device}.c/h -- начальная конфигурация контроллера
  • Создание файла описания аппаратуры в формате SVD и генерация на его снове заголовков, используется утилита для генерации кода. Это позволяет в какой-то мере унифицировать HAL
  • Описание и распознавание средсв отладки CoreSight встроенных в контроллер.
  • Описание конфигурации флеш-памяти: размер памяти, размер сектора, разрядность и быстродействие операций.
  • Создание и компиляция алгоритма прошивки флеш памяти, состоящего из ряда вызовов: EraseSector, ProgramPage, Verify..[CMSIS-Pack]. Скомпилированный код помещается в оперативную память и испольняется в режиме отладки. Функция программатора сводится к загрузке бутлодера и фрагментов прошиваемого кода в оперативную память и манипуляция точками запуска и останова.

Часть 1.4. Пакет описания аппаратуры DFP

Дополнительно можно изучить тему где взять подобные описания. Я нашел источник информации, помимо библиотек в пакетах DFP от производителя. Технология создания пакета описана на в стандарте CMSIS-Pack и поддерживается производителями контроллеров, см. Open-CMSIS-Pack.

К сожелению, поддержка пакетов CMSIS-Pack не является первичной. Пакеты Firmware Library обновляются раньше чем пакеты CMSIS-Pack и содержат более доставерную информацию об аппаратуре, они не полные, не сождержат описания сегментов памяти OptionBytes. Однако, это тот путь, который позволяет выполнять генерацию кода по описанию аппаратуры и может использоваться при отладке.

Структура директории

  • Device/include/{CHIPX}.h -- описание аппаратуры
  • Flash/{CHIPX}.flm -- бутлоадеры, elf формат
  • svd/{CHIPX}.svd -- описание аппаратуры в формате CMSIS-SVD

Часть 1. Нужно создать совместимый Sturtup

C учетом правил сборки проекта и рекомендаций ARM у нас должно образоваться несколько файлов с определенным именованием.
  • Device/source/gcc/gd32e10x.ld -- правила линковки. Скрипт включает размещение сегментов памяти и карту памяти. В тоже время генерация скрипта возможна по шаблону: перечислить встроенную память.
  • Device/include/gd32e10x.h -- описание периферии. Стараемся придерживаться исходников производителя. В этом файле только базовые адреса устройств.
  • Device/source/gcc/startup_gd32e10x.s -- вот этот файл линкуется первым. Он на асемблере, содержит таблицу векторов прерываний и инициализацию переменных. В нем выполняется инициализация сегментов памяти RAM data (заполнение начальными значениями) и bss (обнуление). Код производит два вызова: низкоуровневую настройку периферии (low_level_init), без которой операционка не может стартовать и после этого запускает основную функцию (main) или загружает модули ядра операционной системы. В нашем случае до функции main выполняется старт операционной системы, которая после запуска и инициализации мультизадачности, инициализации драйверов, запутит main.
  • Device/source/system_gd32e10x.c (low_level_init) -- содержит настройку частоты ядра, ресет оборудования, вызвает начальную конфигурацию периферии, вызывает настройку работы внешней памяти, если есть.
  • Кое-что еще нужно для нормальной работы программ на Си, не очевидное -- статические конструкторы и деструкторы, нужно запустить все конструкторы по списку. Мы пишем код на Си, но поддержка статический конструкторов нужна для функционала модулей ядра операционной системы. Инициализацию памяти и перемещение сегментов мы выполняем до запуска кода, в коде начальной загрузки.

Области памяти .bss, .data и таблица статических конструкторов определяеся в линкерном скрипте. В линкерном скрипте задается также размер памяти и указатель стека.

Совместимый startup это всегда один и тот же код, для Cortex-M. Отличием является таблица векторов, которая своя для каждого микроконтроллера. Метод создания - скопировать код, скопировать таблицу векторов от производителя и подправить синтакстис ассемблера, чтобы нравился GCC.

Часть 2.

Нужно создать код низкоуровневой инициализации (low_level_init). Пример нужно взять из файла startup_gd32e10x.c от производителя, но дописать и подправить придется. Тут всегда один стандартный вызов SystemInit(). SystemCoreClock -- глобальная переменная, на выходе инициализации должна содержать частоту ядра -- это требование совместимости с CMSIS-RTOS.

Часть 3.

Где-то между инициализацией периферии до запуска прикладных программ нужно настроить по списку все ноги, порты ввода-вывода. Это вероятно происходит до запуска main, до операционной системы. В нашей концепции все ноги по списку должны быть настроены до запуска приложения. Приложение может менять настройки периферии, но все выводы и порты должны быть в определенном состоянии на момент включения и на момент выключения. Нужно описать ноги согласно принятому API, в совместимом виде, чтобы один и тот же код можно было перенести между контроллерами разных произовдителей. Понятно что настройка происходит иначе, но описание ног должно быть унифицировано меду контроллерами разных производителей. Для этого надо составить файл hal/gpio.h и hal/gpio.c. Описание таблицы ног производится в заголовке board.h. Мы определяем ноги, группы ног по портам, с атрибутами: PIO_ANALOG, PIO_INPUT, PIO_OUTPUT, PIO_OPENDRAIN и т.д. Сложность в том, чтобы использовать одинаковый набор атрибутов на разных платформах, невзирая на то, что на одной нужно использовать Remap, а на другой - нет.

Часть 4.

Надо описать хотя бы один стандартный последовательный порт, который служит для отладки. На STM32 мы используем SWO для вывода отладочной информации. Тут этого нет. Но надежда умирает последней я расчитываю найти SWO даже если про него ничего не говорят. Признаком того, что SWO реализован аппаратно, является ITM (Instrumentation Trace Macrocell) и возможность настроить TRACE через DBGMCU.

Среди настроек регистра DBG_CTL находим TRACE_IOEN (включение трассировки) и TRACE_MODE=0 (Asyncronous).

Если в контроллере все же отсутсвует ITM и поддержка SWO, изучаем возможность использовать UART вместо SWO. Обращаю внимание, что аппаратура трассировки через SWO активируется со стороны программатора, программатор может не поддерживать канал отладки.

Первое что нужно при запуске нового контроллера - помыргать лампочкой, изменить логический урвоень на любой тестовой точке. Нам нужен хоть какой-то терминал, который покажет, что устройство загрузилось и работает. Это может быть последовательный порт типа UART, одноногий дебаггер -- осциллограф или логический анализатор. Теримнал и канал отладки (трассировки) - это функция операционной системы. Мы поддерживаем два режима трассировки. В одном режиме исполнение кода приостанавливается пока не выполнится вывод сообщения (debug) - блокирующий вызов, этот режим нужен для отладки драйверов и обработчиков прерываний. Второй режим -- ассинхронный (printf, puts), вывод отладочной информации не должен препятствовать работе прикладной программы, без блокировки, без задержки -- вывод сообщений выполняется в фоновом процессе.

Часть 5. Перенос ядра RTOS на иную архитектуру

Завязок на аппаратуру в операционной системе не так уж много, но они очень существенные. От этой зависимости можно абстрагироваться.

  1. SVC -- системное прерывание, обращение к системной функции. Кусочек кода на ассемблере.
  2. ContextSwitch -- выполняется при обращении к PendSW прерыванию. Так происходит на всех ядрах Cortex. В разных моделях различным образом используется стек при входе и выходе из прерывания. Есть модели с защитой памяти, с поддержкой FPU, с ленивыми переключениями, с поддержкой двух стеков MSP PSP, да и система команд зачастую различаются.

Таким образом, для нового контроллера надо написать две фукнции: SVC_Handler и PendSW_Handler. Еще есть функция, связанная с механизмом переключения задач, -- заполнение стека при запуске нового треда. Заполнение в частности включает EXEC_RETURN -- флаги способа возврата из обработчика прерывания. Эти флаги - псевдо-адрес возврата сообщают ядру, как восстановить стек. Количество флагов и значение меняется в разных ядрах Cortex, так что заполнение стека является аппаратно-зависимой фукнцией. В нашей реализации аппаратно зависимым от архитектуры оказался именно набор флагов EXEC_RETURN.

Ниже привожу примеры реализации обработчика системного вызова

SVC_Handler:
   mrs  r0, psp
   ldr  r1, [r0, #24]
   ldrb r1, [r1, #-2]
   ldr  pc, [r3, r1, LSL #2]

В нашей операционке SVC вызывается только из прикладных задач (Thread mode), в Handler mode вызовы SVC не используются. Код обработки SVC запросов не меняется для архитектур armv7m-e и armv8m.main, а вот ядро cortex-m23 (архитектура armv8m.base) такие инструкции со сдвигами и отрицательными смещениями не поддерживает, нужна альтернативная реализация.

Переключение c основного стека (Main Stack Pointer, MSP) на стек прикладных программ (Process Stack Pointer, PSP) происходит в процессе начальной загрузки. Мы выполняем это переключение в ассемблерном коде, чтобы исключить использование стека до момента переключения.

// переключение стека с MSP на PSP
	mov r0, sp
	msr psp, r0;
    ... настройка размера стека
	mrs r0, control;
	orr r0, #2
	msr control, r0
	sub sp, #256

Основной чертой нашей операционки должна быть лаконичность. Мы хотим минимизировать задержку на обработку данных. Если получен пакет данных в драйвере, то обработка в приложении должна произойти с минимальными и фиксированными задержками на переключение задач.

В обработчике прерывания мы принимаем решение о переключении задач. Переключение задач происходит путем вызова программного прерывания PendSW следом за обработчиком аппаратного прерывания, выставляем флаг с использованием инструкции __YIELD().

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

PendSW_Handler: // переключение задач
   push {r4-r11,lr}
   . . .// сохраняем контекст (sp) и переходим к следующей задаче
   pop  {r4-r11,pc}

В этом примере переключение стеков (контектсорв задач) очень лаконично сформулировано. Однако, не все так гладко происходит на смом деле. На самом деле, находясь в прерывании PendSW мы используем Handler Stack Pointer (MSP), а возврат происходит в Thread mode с использованием Process Stack Pointer (PSP)

PendSW_Handler: // переключение задач
   mrs   r0, psp
   stmdb r0!, {r4-r11,lr}
   . . .// сохраняем контекст (r0) и переходим к следующей задаче
   ldmia r0!, {r4-r11,lr}
   msr	 psp, r0
   bx    lr

Далее мы усложняем процесс переключения, если используется FPU, и если используется исключение безопасности. Для каждого ядра и каждого варианта оборудования, с FPU, c MPU, c Security Extension возникает необходимость дописать или переписать код переключения контекстов задач.


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

Комментариев нет:

Отправить комментарий