*Этот 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 примерами, а так же в туториалах на сайте проекта.