Готовим драйвера МК для проброса периферии

от автора

*Этот tutorial так же является моим очень вольным переводом статьи из блога.

В предыдущей статье рассматривался принцип, как можно пробросить периферию микроконтроллера (UART, I2C, CAN bus etc) в обычную ПК программу, так как если бы она входила в состав нашего компьютера и висела на обшей шине с памятью. В той публикации рассматривается теория и инструменты, которые позволяют это сделать. В этой части мы рассмотрим как на практике осуществляется подготовка кода драйверов к инструментизации ADIN LLVM pass и последующей сборке в отдельную динамическую библиотеку, которую вы можете использовать в своих проектах.

Для примера возьмем чип nRF51422 и nRF5 SDK от Nordicsemi. Cначала скачаем SDK для nRF51422, распакуем, посмотрим что там есть:

tree -d -L 2 nRF5_SDK_12.3.0_d7731ad/ nRF5_SDK_12.3.0_d7731ad/ ├── components │   ├── ant │   ├── ble │   ├── boards │   ├── device │   ├── drivers_ext │   ├── drivers_nrf │   ├── libraries │   ├── nfc │   ├── proprietary_rf │   ├── serialization │   ├── softdevice │   └── toolchain ├── documentation ├── examples │   ├── ant │   ├── ble_central │   ├── ble_central_and_peripheral │   ├── ble_peripheral │   ├── crypto │   ├── dfu │   ├── dtm │   ├── multiprotocol │   ├── nfc │   ├── peripheral │   └── proprietary_rf ├── external │   ├── cifra_AES128-EAX │   ├── fatfs │   ├── freertos │   ├── micro-ecc │   ├── nano-pb │   ├── nfc_adafruit_library │   ├── nrf_cc310 │   ├── protothreads │   ├── rtx │   ├── segger_rtt │   └── tiny-AES128 └── svd

Оставим только библиотеку драйверов и примеры, а вот код связанный со BLE стеком, радиочастью и драйверами сторонних производителей удалим, дабы не мешались. Остается:

tree  -L 2 nRF5_SDK_12.3.0_d7731ad/ nRF5_SDK_12.3.0_d7731ad/ ├── components │   ├── boards │   ├── device │   ├── drivers_nrf │   ├── libraries │   ├── sdk_validation.h │   └── toolchain ├── examples │   └── peripheral └── license.txt

Теперь подготовим скрипты сборки и ADIN инструментизации для этих драйверов. Создадим рядом папку NRF51422, в которой будут размещаться необходимые сборочные скрипты. Начинается все с CMakeLists.txt. Заготовку можно взять от другого МК, к примеру от STM32. Поменяем установку первых четырех переменных в скрипте CMakeLists.txt:

set(MCU_TYPE nRF51422)  set(MCU_LIB_NAME SDK)  set(MCU_MAJOR_VERSION_LIB V12.3.0)  set(MCU_MINOR_VERSION_LIB 01)

Эти переменные только для косметики, обозначают версию библиотеки и для какого чипа собирается библиотека:

set(MCU_SDK_PATH ${CMAKE_CURRENT_SOURCE_DIR}/..)

MCU_SDK_PATH — это путь к исходникам скаченный драйверов nRF5 SDK. Важно задать правильно, так как он будет участвовать в сборке

include(${REMCU_VM_PATH}/cmake/mcu_build_target.cmake)  file(INSTALL "${CMAKE_CURRENT_SOURCE_DIR}/defines_${MCU_TYPE}.h"        DESTINATION ${ALL_INCLUDE_DIR}       )  file(RENAME "${ALL_INCLUDE_DIR}/defines_${MCU_TYPE}.h"             ${ALL_INCLUDE_DIR}/device_defines.h       )

Строчки выше нужно оставить. А вот эту ниже можно убрать, так как python файла с экспортами ф-ций драйверов у нас нет:

file(INSTALL "${CMAKE_CURRENT_SOURCE_DIR}/${MCU_TYPE}_${MCU_LIB_NAME}.py"       DESTINATION ${ALL_INCLUDE_DIR}       )

В следующий раз напишу как можно проэкпортировать ф-циий из SDK в питон и красиво вызывать их с помощью ctypes. Пример можно посмотреть здесь

Пример использования инструментированных драйверов в Python использую ctypes

Пример использования инструментированных драйверов в Python использую ctypes

Дальше нам нужно задать интервалы перехватываемых адресов, делается это в файле в conf.cpp. Инструментируемые ADIN ф-ции будут перехватывать только адреса из этих интервалов. Шаблон опять же можно взять от STM32. Интервалы адресов задаются через ф-ции:

add_to_mem_interval — как можно догадаться устанавливает интервал для RAM, его можно оставить без изменений, он совпадает для STM32 и nRF51. Для периферии используется
add_to_adin_interval — устанавливает интервалы для периферии и там интервалы будут отличаться. Взглянем на схему адресов nRF51 из datasheet микроконтроллера:

Для простоты зададим интервалы только для AHB peripherals и APB peripherals, так как там находится основная периферия не связанная с RF(ADC, таймеры, GPIO и др.)

add_to_mem_interval(0x20000000, 0x20000000 + 8*1024); //SRAM  8k add_to_adin_interval(0x40000000,  0x40008000); //APB peripherals add_to_adin_interval(0x50000000,  0x50060000); //AHB peripherals add_to_adin_interval(0xF0000FE0,  0xF0000FE8 + 4); //PAN 26 "System: Manual setup is required to enable the use of peripherals"

Последний интервал нужен для ф-ции SystemInit. Там происходит считывание по этим адресам. Более подробно стоит почитать в коментариях к коду. Ф-цию get_RAM_addr_for_test можно оставить без изменений.

uint32_t get_RAM_addr_for_test(){     return 0x20000000; }

Она отдает адрес, по которому будет проводиться тесты с памятью. Адрес должен быть в пределах RAM микроконтроллера. Это нужно для диагностики отладчика, иногда они работают не совсем корректно и что бы в этом убедиться можно вызвать ф-цию remcu_debuggerTest после установки соединения с отладчиком (т.е. после успешного вызова remcu_connect2OpenOCD / remcu_connect2GDB)

/**  * @brief remcu_debuggerTest  * Performs test of debugger and debug server while mcu is connected.  * @return If no error occurs, the function returns NULL  * else the function returns error message (char array)  * Don't free the pointer after use!  * Note:  Invoke the function after establishing a connection with the debugger  * through the successful utilization of either the  * remcu_connect2OpenOCD or remcu_connect2GDB functions.  */ REMCULIB_DLL_API const char* remcu_debuggerTest();

Дальше надо сформировать файл defines_${MCU_TYPE}.h в нашем случае он будет называться defines_nRF51422.h. В нем нужно прописать Си макросы, которые использовались при сборке. Сделано что бы не потерять эти макросы при последующем использовании в собранной библиотеки. К примеру, в заголовочных файлах SDK, которые мы потом будем использовать у себя в проектах, может быть такой код:

#ifdef OPTION1 void foo(); #endif

Что бы не потерять ф-цию foo, надо не потерять макрос OPTION1
Надо найти макросы с которыми собирается nRF5 SDK под нужный нам чип. Проще всего это посмотреть в примерах. Взглянем на Makefile для примера ADC. Там можно найти необходимые макросы:

CFLAGS += -DNRF51 CFLAGS += -DNRF51422 CFLAGS += -DBOARD_PCA10028 CFLAGS += -DBSP_DEFINES_ONLY

Заполняем их в defines_nRF51422.h

#define REMCU_LIB  #define NRF51  #define NRF51422  #define BOARD_PCA10028  #define BSP_DEFINES_ONLY

Из того же Makefile возьмем список компилируемых файлов и путей до заголовочных файлов:

# Source files common to all targets SRC_FILES += \   $(SDK_ROOT)/components/libraries/log/src/nrf_log_backend_serial.c \   $(SDK_ROOT)/components/libraries/log/src/nrf_log_frontend.c \   $(SDK_ROOT)/components/libraries/util/app_error.c \   $(SDK_ROOT)/components/libraries/util/app_error_weak.c \   $(SDK_ROOT)/components/libraries/util/app_util_platform.c \   $(SDK_ROOT)/components/libraries/util/nrf_assert.c \   $(SDK_ROOT)/components/libraries/util/sdk_errors.c \   $(SDK_ROOT)/components/boards/boards.c \   $(SDK_ROOT)/components/drivers_nrf/hal/nrf_adc.c \   $(SDK_ROOT)/components/drivers_nrf/adc/nrf_drv_adc.c \   $(SDK_ROOT)/components/drivers_nrf/common/nrf_drv_common.c \   $(SDK_ROOT)/components/drivers_nrf/uart/nrf_drv_uart.c \   $(PROJ_DIR)/main.c \   $(SDK_ROOT)/external/segger_rtt/RTT_Syscalls_GCC.c \   $(SDK_ROOT)/external/segger_rtt/SEGGER_RTT.c \   $(SDK_ROOT)/external/segger_rtt/SEGGER_RTT_printf.c \   $(SDK_ROOT)/components/toolchain/gcc/gcc_startup_nrf51.S \   $(SDK_ROOT)/components/toolchain/system_nrf51.c \  # Include folders common to all targets INC_FOLDERS += \   $(SDK_ROOT)/components \   $(SDK_ROOT)/components/libraries/util \   $(SDK_ROOT)/components/toolchain/gcc \   $(SDK_ROOT)/components/drivers_nrf/uart \   ../config \   $(SDK_ROOT)/components/drivers_nrf/common \   $(SDK_ROOT)/components/drivers_nrf/adc \   $(PROJ_DIR) \   $(SDK_ROOT)/external/segger_rtt \   $(SDK_ROOT)/components/libraries/bsp \   $(SDK_ROOT)/components/drivers_nrf/nrf_soc_nosd \   $(SDK_ROOT)/components/toolchain \   $(SDK_ROOT)/components/device \   $(SDK_ROOT)/components/libraries/log \   $(SDK_ROOT)/components/boards \   $(SDK_ROOT)/components/drivers_nrf/delay \   $(SDK_ROOT)/components/toolchain/cmsis/include \   $(SDK_ROOT)/components/drivers_nrf/hal \   $(SDK_ROOT)/components/libraries/log/src \

Уберем исходники связанные с отладкой, логированием, самописными printf и файлы ассемблера. Так же уберем пути до заголовочных файлов, которые требовались для сборки выкинутого кода и получаем такой список:

SRC_FILES +=  \   $(SDK_ROOT)/components/boards/boards.c \   $(SDK_ROOT)/components/drivers_nrf/hal/nrf_adc.c \   $(SDK_ROOT)/components/drivers_nrf/adc/nrf_drv_adc.c \   $(SDK_ROOT)/components/drivers_nrf/common/nrf_drv_common.c \   $(SDK_ROOT)/components/toolchain/system_nrf51.c \  INC_FOLDERS =  \   $(SDK_ROOT)/components/libraries/util \   ../config \   $(SDK_ROOT)/components/drivers_nrf/common \   $(SDK_ROOT)/components/drivers_nrf/adc \   $(SDK_ROOT)/components/drivers_nrf/nrf_soc_nosd \   $(SDK_ROOT)/components/toolchain \   $(SDK_ROOT)/components/device \   $(SDK_ROOT)/components/libraries/log \   $(SDK_ROOT)/components/boards \   $(SDK_ROOT)/components/toolchain/cmsis/include \   $(SDK_ROOT)/components/drivers_nrf/hal \   $(SDK_ROOT)/components/libraries/log/src \

Теперь нужно сделать Makefile для ADIN инструментезации кода nRF5 SDK, шаблон опять же можно взять от STM32:

SDK_ROOT := $(MCU_SDK_PATH)  C_SRC +=  \   $(SDK_ROOT)/components/boards/boards.c \   $(SDK_ROOT)/components/drivers_nrf/hal/nrf_adc.c \   $(SDK_ROOT)/components/drivers_nrf/adc/nrf_drv_adc.c \   $(SDK_ROOT)/components/drivers_nrf/common/nrf_drv_common.c \   $(SDK_ROOT)/components/toolchain/system_nrf51.c \  INC_PATH =  \   $(SDK_ROOT)/components/libraries/util \   $(SDK_ROOT)/examples/peripheral/adc/pca10028/blank/config \   $(SDK_ROOT)/components/drivers_nrf/common \   $(SDK_ROOT)/components/drivers_nrf/adc \   $(SDK_ROOT)/components/drivers_nrf/nrf_soc_nosd \   $(SDK_ROOT)/components/toolchain \   $(SDK_ROOT)/components/device \   $(SDK_ROOT)/components/libraries/log \   $(SDK_ROOT)/components/boards \   $(SDK_ROOT)/components/toolchain/cmsis/include \   $(SDK_ROOT)/components/drivers_nrf/hal \   $(SDK_ROOT)/components/libraries/log/src \  # TOOLCHAIN OPTIONS #-------------------------------------------------------------------------------  DEFS += -DNRF51 -DNRF51422 -DBOARD_PCA10028 -DBSP_DEFINES_ONLY  include $(TARGET_MK)

C_SRC и INC_PATH именно так должны называться переменные с исходниками и путями до заголовочных файлов. Если файлы имеют расширение .cpp то переменная будет CPP_SRC . Эти переменные подхватывает встраиваемый скрипт include $(TARGET_MK). Посмотреть его можно здесь.

В переменную DEFS записываем уже знакомые нам макросы.

В наш Makefile передается переменная MCU_SDK_PATH, которую мы определили выше в CMakeLists.txt, эта переменная позволит нам не писать полные пути до файлов:

SDK_ROOT := $(MCU_SDK_PATH)

Давайте попробуем собрать пока то что имеем, для простоты буду использовать Ubuntu как основную систему и подготовленный Docker образ :

docker pull sermkd/remcu_builder

Если у вас Windows OS, то среду сборки придется подготавливать как здесь. Для MacOS тоже будет непростая подготовка окружения. Поэтому очень рекомендую использовать сборку с помощью GitHub Action, сэкономите уйма времени и сил!

Исходники nRF5 SDK на этом этапе подготовил в общем репозитории с уже инструментированными драйверами от других производителей, скачаем:

git clone --recurse-submodules https://github.com/remotemcu/remcu-chip-sdks.git cd remcu-chip-sdks git checkout 8ee6eb05ed1e584f20f108caf19b08802de23458 

Запускаем docker. В докере мы можем собирать только под Linux и есть кроскомпиляция для Raspberry.

docker run -it --name remcu-build-docker -v $PWD/remcu-chip-sdks:/remcu-chip-sdks -w /remcu-chip-sdks remcu_builder

В докере, идем в папку с NRF51422 и пытаемся собрать. В опции -DCMAKE_TOOLCHAIN_FILE указывается путь до toolchain файла под нужную нам платформу, полный список toolchain файлов здесь.

cd /remcu-mcu-sdks/nordicsemi/nRF5_SDK_12.3.0_d7731ad/NRF51422 mkdir build cd build cmake .. -DCMAKE_TOOLCHAIN_FILE=/remcu-mcu-sdks/REMCU/platform/linux_x64.cmake make

И получим следующую ошибку

/nordicsemi/nRF5_SDK_12.3.0_d7731ad/NRF51422/../components/libraries/util/app_error.h:128:8: error: unknown type name '__INLINE' static __INLINE void app_error_log(uint32_t id, uint32_t pc, uint32_t info)

type name '__INLINE’ определен в заголовочном файле compiler_abstraction.h , который подключается в nrf.h. Nordic любезно уберегает нас от подключения нужных файлов, когда мы компилируем под PC host. Что бы не ломать исходники, исправим это с помощью своего макроса REMCU_LIB, который используется при сборке динамической библиотеки с помощью сборочных скриптов REMCU. Патч

#ifndef REMCU_LIB     #define NO_REMCU_LIB #endif //REMCU_LIB  #if defined(_WIN32) && defined(NO_REMCU_LIB)     /* Do not include nrf specific files when building for PC host */ #elif defined(__unix) && defined(NO_REMCU_LIB)     /* Do not include nrf specific files when building for PC host */ #elif defined(__APPLE__) && defined(NO_REMCU_LIB)     /* Do not include nrf specific files when building for PC host */ #else      /* Device selection for device includes. */     #if defined (NRF51)         #include "nrf51.h"         #include "nrf51_bitfields.h"         #include "nrf51_deprecated.h"     #elif defined (NRF52840_XXAA)         #include "nrf52840.h"         #include "nrf52840_bitfields.h"         #include "nrf51_to_nrf52840.h"         #include "nrf52_to_nrf52840.h"     #elif defined (NRF52832_XXAA)         #include "nrf52.h"         #include "nrf52_bitfields.h"         #include "nrf51_to_nrf52.h"         #include "nrf52_name_change.h"     #else         #error "Device must be defined. See nrf.h."     #endif /* NRF51, NRF52832_XXAA, NRF52840_XXAA */      #include "compiler_abstraction.h"  #endif /* _WIN32 || __unix || __APPLE__ */

Попробуем снова собрать и в этот раз успешно, динамическая библиотека собрана(libremcu.so). Теперь для библиотеки надо проэкспортировать заголовочные файлы nRF5 SDK, которые мы будем подключать в своем проекте вместе с собранной библиотекой. Берем нам уже знакомый список путей:

  $(SDK_ROOT)/components/libraries/util \   $(SDK_ROOT)/examples/peripheral/adc/pca10028/blank/config \   $(SDK_ROOT)/components/drivers_nrf/common \   $(SDK_ROOT)/components/drivers_nrf/adc \   $(SDK_ROOT)/components/drivers_nrf/nrf_soc_nosd \   $(SDK_ROOT)/components/toolchain \   $(SDK_ROOT)/components/device \   $(SDK_ROOT)/components/libraries/log \   $(SDK_ROOT)/components/boards \   $(SDK_ROOT)/components/toolchain/cmsis/include \   $(SDK_ROOT)/components/drivers_nrf/hal \   $(SDK_ROOT)/components/libraries/log/src \

И используем его в CMakeLists.txt

file(INSTALL        "${MCU_SDK_PATH}/components/libraries/util/"       "${MCU_SDK_PATH}/components/drivers_nrf/common/"       "${MCU_SDK_PATH}/components/drivers_nrf/adc/"       "${MCU_SDK_PATH}/components/drivers_nrf/nrf_soc_nosd/"       "${MCU_SDK_PATH}/components/toolchain/"       "${MCU_SDK_PATH}/components/device/"       "${MCU_SDK_PATH}/components/libraries/log/"       "${MCU_SDK_PATH}/components/boards/"       "${MCU_SDK_PATH}/components/toolchain/cmsis/include/"       "${MCU_SDK_PATH}/components/drivers_nrf/hal/"       "${MCU_SDK_PATH}/components/libraries/log/src/"       "${MCU_SDK_PATH}/examples/peripheral/adc/pca10028/blank/config/"       DESTINATION ${ALL_INCLUDE_DIR}       FILES_MATCHING PATTERN "*.h"       )

Все заголовочные(*.h) файлы из этих путей будут в папке remcu_include Так как мы собирали динамическую библиотеку, есть вероятность, что мы собрали не все что нужно для последующей линковки выполняемого файла. Давайте это проверим с помощью теста. А заодно для нас это будет пример использования периферии ADC. Добавим поддиректорию в наш CMakeLists.txt

add_subdirectory(test)

Создадим поддиректорию test, разместим там еще один CMakeLists.txt для сборки теста и сам тест(main.c). Код я взял из примера для ADC. Убрал от туда лишние заголовочные файлы, не относящиеся к периферии, заменил логирование на printf , а так же убрал ассемблерные вызовы. Заменил получение данных в прерывании на polling(опрос). Прерывания доступны только на процессорном ядре МК, поэтому их использовать не получится.

Снова из папки build запускаем cmake и make, все собирается без ошибок.

/remcu-mcu-sdks/nordicsemi/nRF5_SDK_12.3.0_d7731ad/NRF51422/build# rm -rf * /remcu-mcu-sdks/nordicsemi/nRF5_SDK_12.3.0_d7731ad/NRF51422/build# cmake .. -DCMAKE_TOOLCHAIN_FILE=/build/AddressInterceptorLib/platform/linux_x64.cmake && make /remcu-mcu-sdks/nordicsemi/nRF5_SDK_12.3.0_d7731ad/NRF51422/build# ls CMakeCache.txt  CMakeFiles  IrTest  Makefile  README.txt  REMCU_LICENSE.txt  build_remcu_object  cmake_install.cmake  libremcu.so  nRF51422-SDK-V12.3.0-01  remcu_include  test

Давайте рассмотрим main.c файл примера. Он в самом начале парсит аргументы передающиеся через командную строку(адрес севера и порт) и в зависимости от порта, подключается к OpenOCD(порт 6666) или GDB серверу, дальше чип сбрасывается в режим ожидания, проверяется успешно ли подключились к серверу. И обязательно надо не забыть вызвать ф-цию

SystemInit();

Она всегда вызывается на этапе инициализации векторов прерывания из asmbler кода. Здесь нам надо ее вызвать явно. Дальше идет просто код работы с периферией.

Можно запустить тест-пример. Подключитесь к чипу и запустите OpenOCD сервер(лучше версию 0.10.0-11-20190118-1134 или v0.12.0-1)

openocd -f interface/stlink.cfg -f target/nrf51.cfg

и сам тест:

LD_LIBRARY_PATH=$PWD test/test_build_nrf5_adc localhost 6666

6666 — это порт OpenOCD сервера, вместо этого можно использовать GDB сервер он будет висеть на порту 3333

LD_LIBRARY_PATH=$PWD test/test_build_nrf5_adc localhost 3333

LD_LIBRARY_PATH нужен что бы бинарник нашего примера смог подхватить библиотеку скомпилированных драйверов, иначе положить к системным либам.

Если у вас Windows OS

Будет сложнее настройка среды сборки, более подробно тут. Очень важно поставить именно версию VS2017 и clang 8.0.0, а так же использовать сборочную систему ninja или make. Но лучше использовать сборку с помощью GitHub Action, это будет намного легче.

Помимо патча выше, который мы делали для сборки в Unix системе. Для Windows OS еще придется немного пропатчить SDK nRF5, что бы сборка под Windows была успешной. Для этой системы в компиляторе не установлен макрос

__GNUC__

Из-за чего рушится вся сборка. Добавим его в Makefile и defines_nRF51422.h Все будет собираться но ничего не будет работать, так как мы еще должны проэкспортировать ф-ции из SDK, что бы они могли вызываться из программ, которые используют нашу динамическую библиотеку.

Делается это обычно с помощью директивы __declspec( dllexport ) Но что бы не писать эту директиву возле каждого объявления ф-ции, можно использовать специфическую #pragma у clang

#pragma clang attribute push (__declspec(dllexport), apply_to = function)  // Functions to be exported void exportedFunction1(); void exportedFunction2();  #pragma clang attribute pop

И все что между этими #pragma будет экспортировано. Что бы не перепутать местами #pragma и использовать их только под Windows, я их завернул в отдельные заголовочные файлы:

#include "remcu_exports_symbol_enter.h"  // Functions to be exported void exportedFunction1(); void exportedFunction2();  #include "remcu_exports_symbol_exit.h"

После сборки у нас есть динамическая библиотека:

remcu.dll remcu.lib — для Windows
libremcu.so -для Linux
libremcu.myLib — для Macos

А так же все необходимые заголовочные файлы в папке remcu_include.

Примеры использования можно посмотреть в репозитории c примерами, а так же в туториалах на сайте проекта.


ссылка на оригинал статьи https://habr.com/ru/articles/768688/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *