Мы делаем свою операционную систему... которая поддерживает функционал 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 на иную архитектуру
Завязок на аппаратуру в операционной системе не так уж много, но они очень
существенные. От этой зависимости можно абстрагироваться.
-
SVC -- системное прерывание, обращение к системной функции. Кусочек кода на
ассемблере.
-
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) происходит в процессе начальной
загрузки. Мы выполняем это переключение в ассемблерном коде, чтобы исключить
использование стека до момента переключения.
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}
. . .
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}
. . .
ldmia r0!, {r4-r11,lr}
msr psp, r0
bx lr
Далее мы усложняем процесс переключения, если используется FPU, и если
используется исключение безопасности. Для каждого ядра и каждого варианта
оборудования, с FPU, c MPU, c Security Extension возникает необходимость дописать или
переписать код переключения контекстов задач.
Чтобы запустить новый контроллер с новым ядром надо все по списку выполнить без ошибок,
иначе ни одна лампочка не мыргнет и контроллер не будет подавать признаков
жизни, до исполнения main() мы не дойдем. Можно отлаживать отдельно запуск
кода без операционной системы, на отладочной плате, но начальная загрузка
должна заработать сразу.