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

*Этот 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 микроконтроллера:

5e4e3f5b61f622c8fd6032408b45118d.png

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

© Habrahabr.ru