*Этот 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. Пример можно посмотреть здесь

Дальше нам нужно задать интервалы перехватываемых адресов, делается это в файле в 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 примерами, а так же в туториалах на сайте проекта.
