Что такое dfu-util и зачем будить это лихо?

Что это такое и где мы можем это встретить?

“DFU is intended to download and upload firmware to/from devices connected over USB” - dfu-util manual page

dfu-util — это в первую очередь утилита, о чём я не очень‑то и задумывался, поĸа не начал работать с её ĸодом. Она разработана для работы с устройствами, находящимися в DFU (Device Firmware Upgrade) режиме, т. е. в режиме, позволяющим работать непосредственно с прошивĸой. Таĸими устройствами может быть всё от миĸроĸонтроллеров до смартфонов, а самыми примитивными целями работы может быть выгрузĸа и загрузĸа прошивĸи.

Бытовой пример работы с DFU на уровне пользователя

Собственно, пример из жизни, ĸоторый со мной случился буĸвально во время написания статьи.

Решил я сбросить старый айфон ĸ заводсĸому виду прошивĸи. И вот, что не сделает ни один адеĸватный человеĸ - не будет создавать во время первого запуска пароль от балды и забывать его через 2 наносекунды после ввода.

Да, именно это я и сделал. Учетная запись Apple ID еще не введена на устройстве, таĸ что через неё сбросить пароль нельзя. После целого дня подбора пароля я заблоĸировал телефон на день. Начал рысĸать в интернете, ĸаĸ сбросить пароль. Вуаля! Можно перевести смартфон в DFU режим, подĸлючить его ĸ macbook-у (либо Windows iTunes) и переставить прошивĸу на новую. Моей радости не было предела. Делов на 15 минут и смартфон готов ĸ работе.

Это пример именно ручного взаимодействия с DFU режимом. Все остальные обновления прошивки смартфона, микроволновок, лаптопов и прочего можно считать автоматическим процессом, связанным с DFU.

Небольшой забавный момент на тему первого упоминания о dfu-util в 2007-м году:

“initial (unfinished) version of new DFU utility (dfu‑programmer just sucks as something generic, device independent)” — Harald Welte в своём первом коммите.

Это ж как должна была не понравиться dfu-programmer утилита, чтобы чувак создал целый проект. Да и такой, что им будут пользоваться сотни тысяч людей и компаний следующие пару десятилетий.

Будить лихо или оно и так гуляет по миру?

dfu-util не шибко-то засыпала. Её по сей день используют компании разного уровня гигантизма от BlackMagic design (dfu-шечка присутствует в их офф. гайдах) и STM32 (dfu-util является одним из основных средством загрузки кода) до каких-нибудь стартапов.

Например, в компании делают какое-то физическое устройство, которое конечно же должно иметь функционал обновления прошивки. Перед разработчиками встанет выбор, писать собственно API для работы с устройством в DFU режиме или просто взять готовый код и по необходимости изменить его. Выбор очевиден.

В дополнение к аргументу упомяну, что одной из самых популярных библиотек для общения с USB портом является LibUsb, а dfu-util как раз её и использует.

Не так давно у меня возникла необходимость работать с DFU устройством, а dfu-util обычно контрибьютится посредством уже собранных бинарей, а не пакета или библиотеки. Отсюда и пошло желание написать прослойку для C++ & CMake, чтобы иметь возможность прямого доступа к исходному коду во время написания проекта.

Планы действий по текущей статье

  • Возможность подключения утилиты dfu-util как сабмодуля/сабдиректории С++ проекта под CMake сборкой. CMakeList-ы буду писать на достаточно базовом уровне как по причине своих навыков, так и для того, чтобы статья была более ёмкой и читабельной. Поэтому применения ExternalProject, CPM и прочих очень полезных и занимательных модулей здесь вы не увидите;

  • Конечно же иметь возможность работы как на macOS, так и на Windows. Век кроссплатформенность на дворе всё-таки;

  • Возможность сборки утилиты dfu-util посредством CMake;

  • Привести пример класса-прослойки на С++ для собственного использования инструментария dfu-util.

Повествование начнется macOS. Все основные планы будут доведены до конца именно на macOS и только под конец будет показана адаптация под Windows.

Подготовка

Посмотрел гайд от разработчиков dfu-util. Autoconf, autoheader... это что такое вообще... Ладно, ставим любимой brew-шкой

brew install make cmake pkg-config libusb autoconf autoheader

Надо бы подумать над древом проекта, но подумать надо недолго, так что возьмем стандартную структуру любого проекта.

WorDfuUtil
|--- include
|    |--- pch.hpp
|    ...
|--- src
|    |--- main.cpp
|    ...
|--- ThirdParty
|    |--- dfu-util (submodule)
|         |--- ...
|    |--- DfuUtilConfiguring.cmake
|    |--- CMakeLists.txt
|--- CMakeLists.txt

Основная часть

Топовый CMakeLists.txt мне пока что не интересен, так что пойдем сразу в ThirdParty/CMakeLists.txt. Подключил и настроил таргеты dfu-util и dfu-util_exe.

# WorDfuUtil/ThirdParty/CMakeLists.txt

cmake_minimum_required(VERSION 3.23)

# ---------- #
#   libusb   #
# ---------- #
find_path(LibusbIncludeDir
        NAMES libusb.h
        PATH_SUFFIXES "include" "libusb" "libusb-1.0")
find_library(LibusbLib
        NAMES libusb-1.0.dylib
        PATH_SUFFIXES "lib")

add_library(libusb INTERFACE)
target_link_libraries(libusb
        INTERFACE ${LibusbLib})
target_include_directories(libusb
        INTERFACE ${LibusbIncludeDir})

# ------------ #
#   dfu-util   #
# ------------ #
project(dfu-util
        LANGUAGES C)

set(DfuUtilRoot ${CMAKE_CURRENT_SOURCE_DIR}/dfu-util)
set(DfuUtilSourceDir ${DfuUtilRoot}/src)

add_library(dfu-util)

set(Sources 
        ${DfuUtilSourceDir}/dfu_load.c
        ${DfuUtilSourceDir}/dfu_util.c
        ${DfuUtilSourceDir}/dfuse.c
        ${DfuUtilSourceDir}/dfuse_mem.c
        ${DfuUtilSourceDir}/dfu.c
        ${DfuUtilSourceDir}/dfu_file.c
        ${DfuUtilSourceDir}/quirks.c)
target_sources(dfu-util
        PRIVATE ${Sources})
source_group("dfu-util_sources"
        FILES ${Sources})

target_include_directories(dfu-util
        PUBLIC
        ${DfuUtilRoot}/src
        ${DfuUtilRoot})

target_link_libraries(dfu-util
        PUBLIC libusb)

# ---------------- #
#   dfu-util_exe   #
# ---------------- #
add_executable(dfu-util_exe)

target_sources(dfu-util_exe
        PRIVATE
        ${Sources}
        ${DfuUtilSourceDir}/main.c)

target_include_directories(dfu-util_exe
        PUBLIC
        ${DfuUtilRoot}/src
        ${DfuUtilRoot})

target_link_libraries(dfu-util_exe
        PUBLIC libusb)

Для сбора сурсов проекта нацеленно использовал список вручную вместо file({GLOB | GLOB_RECURSE} ...), потому как в main.cpp нас ждёт сюрприз, о котором скажу позже. Вообще не понимаю, почему комьюнити так не любит эту команду даже с учетом комментария разработчиков CMake. Отключите автоматизацию, ручками управляйте обновлением CMake и будет вам счастье, НО если у вас в проекте больше сотки файлов, то да - лучше напишите свой сборщик файлов, разложить их по категориям и т.п.

Ремарка по поводу всемогущей LibUsb. Можно, конечно искать её через find_package(PkgConfig REQUIRED) + pkg_check_module(libusb REQUIRED libusb-1.0), но я не привык пользоваться pkg_config, т.к. это старенькая утилита, да и её зачастую банально нет на машинах.

Dfu-util предоставляет Makefile, но я, как разработчик современного поколения, не знаю, как с ним работать и могу только уловить суть. Постарался плюс-минус соблюсти логику линковки, собрал курсы, линканул LibUsb и с предвкушением нажал на сборку.

Первый выстрел пойман лодыжкой
Первый выстрел пойман лодыжкой

По мере изучения, откуда звуки выстрела, понял, что dfu-util-щики генерят config.h файл, который дефайнит символы в процессе конфигурации. Прикреплю часть символов для понимания, т.к. по их название уже понятно, для чего они нужны.

/**
 * ThirdParty/dfu-util/config.h.in
 */

/* config.h.in.  Generated from configure.ac by autoheader.  */

/* Define to 1 if you have the 'err' function. */
#undef HAVE_ERR

/* Define to 1 if you have the <inttypes.h> header file. */
#undef HAVE_INTTYPES_H

/* Define to 1 if you have the 'usb' library (-lusb). */
#undef HAVE_LIBUSB

/* Define to 1 if you have the 'nanosleep' function. */
#undef HAVE_NANOSLEEP

/* Define to 1 if you have the <stdint.h> header file. */
#undef HAVE_STDINT_H

/* Define to 1 if you have the <stdio.h> header file. */
#undef HAVE_STDIO_H

/* ... */

На основе этого шаблона генерируется следующий заголовочный файл:

/**
 * ThirdParty/dfu-util/config.h
 */

/* config.h.  Generated from config.h.in by configure.  */
/* config.h.in.  Generated from configure.ac by autoheader.  */

/* Define to 1 if you have the 'err' function. */
#define HAVE_ERR 1

/* Define to 1 if you have the <inttypes.h> header file. */
#define HAVE_INTTYPES_H 1

/* Define to 1 if you have the 'usb' library (-lusb). */
/* #undef HAVE_LIBUSB */

/* Define to 1 if you have the 'nanosleep' function. */
#define HAVE_NANOSLEEP 1

/* Define to 1 if you have the <stdint.h> header file. */
#define HAVE_STDINT_H 1

/* Define to 1 if you have the <stdio.h> header file. */
#define HAVE_STDIO_H 1

/* ... */

Для конфигурации и билда dfu-util нужны команды, но кто я такой, чтобы противиться любви к CMake, так что добавлю команды и выкину их в отдельный DfuUtilConfiguring.cmake файл:

# WorDfuUtil/ThirdParty/DfuUtilConfiguring.cmake

cmake_minimum_required(VERSION 3.23)

add_custom_target(dfu-util_autogen
        COMMAND ./autogen.sh
        COMMENT "Creating dfu-util configuration file..."
        WORKING_DIRECTORY ${DfuUtilRoot})

add_custom_target(dfu-util_configure
        COMMAND ./configure
        DEPENDS dfu-util_autogen
        COMMENT "Configuring dfu-utils for current OS. Generating makefiles..."
        WORKING_DIRECTORY ${DfuUtilRoot})

add_custom_target(dfu-util_build
        COMMAND make
        DEPENDS dfu-util_configure
        COMMENT "Running make for dfu-utils project..."
        WORKING_DIRECTORY ${DfuUtilRoot})

Предвкушаю вопрос, зачем мне билдить это Make-ом, если я и так собираю в CMake цель dfu-util_exe. Не знаю, честно говоря. Пусть оно будет тут и не отсвечивает сильно.
Пробую выполнение configure и всё ожидаемо отрабатывает стабильно.

Лог выполнения таргета dfu-util_configure
Лог выполнения таргета dfu-util_configure

Билд dfu-util таргета снова вывел ту же самую ошибку отсутствия некоторых символов и заголовочников. Это связано с тем, что в коде почти всех исходников есть слудующие строчки:

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

При сборке нужно передать -DHAVE_CONFIG_H компилятору, чтобы в dfu*.cpp файлах произошло подключение конфигурационного заголовочника. Передаём, конечно же, публично, чтобы вышестоящая цель имела тот же дефайн.

# ThirdParty/CMakeLists.txt
# ...
add_library(dfu-util)

target_compile_definitions(dfu-util
        PUBLIC -DHAVE_CONFIG_H)
# ...

# ThirdParty/CMakeLists.txt
# ...
add_executable(dfu-util_exe)

target_compile_definitions(dfu-util_exe
        PUBLIC -DHAVE_CONFIG_H)
# ...

Запускаю снова таргет dfu-util.

Лог выполнения таргета dfu-util
Лог выполнения таргета dfu-util

Теперь можно вернуться к CMakeList-у для WorDfuUtil:

# CMakeLists.txt

cmake_minimun_required(VERSION 3.23)

project(WorDfuUtil
        LANGUAGES C)

add_subdirectory(ThirdParty)

# -------------- #
#   WorDfuUtil   #
# -------------- #
add_executable(WorDfuUtil)

file(GLOB Sources src/*.cpp)
target_sources(WorDfuUtil
        PRIVATE ${Sources})

target_compile_features(WorDfuUtil
        PRIVATE cxx_std_17)

target_include_directories(WorDfuUtil
        PUBLIC include)

target_link_libraries(WorDfuUtil 
        PRIVATE dfu-util)

Специально сделал пока только исполняемый тип сборки, чтобы тестировать код. После проверки работоспособности исполняемый тип заменится на библиотечный.

Не забываю про pch файл. Удобно иметь это конструкцию, чтобы не писать в каждом файле. Да и вообще у меня эти dfu*.h заголовочники использоваться будут повсеместно, так что кину их в прекомпиляцию.

/**
 * include/pch.hpp
 */

#pragma once

#include "libusb.h"

extern "C" {

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "portable.h"
#include "dfu.h"
#include "dfu_file.h"
#include "dfu_load.h"
#include "dfu_util.h"
#include "dfuse.h"
}

#include "DfuUtilVariables.hpp"

Такой еще момент. Если у нас нет ThirdParty/dfu-util/config.h при первой инициализации проекта, то CMake падает из-за того, что config.h указан в pch файле. Добавлю еще условие, чтобы основные таргеты были недоступны, пока не прошла конфигурация dfu-util файлов.

# ThirdParty/DfuUtilConfiguring.cmake
# ...
add_custom_target(dfu-util_build
        COMMAND make
        DEPENDS dfu-util_configure
        COMMENT "Running make for dfu-utils project..."
        WORKING_DIRECTORY ${DfuUtilRoot})

if(NOT EXIST ${DfuUtilRoot}/config.h)
    message(WARNING "Cannot find dfu-util's config file.  Configure dfu-util first.
    Run build dfu-util_configure target.")
    return()
else ()
    set(HaveConfig ON PARENT_SCOPE)
endif()
# ...

# CMakeLists.txt
# ...
add_subdirectory(ThirdParty)

if(NOT ${HaveConfig})
    return()
endif()
# ...

# CMakeLists.txt
# ...
target_compile_features(WorDfuUtil
        PRIVATE cxx_std_17)

target_precompile_headers(WorDfuUtil
        PRIVATE include/pch.h)
# ...

Создал main.cpp файл для таргета WorDfuUtil, в котором набросал вызовы пары функций для тестов библиотек.

/**
 * src/main.cpp
 */

#include <cstdio>

#include "libusb.h"

#include "pch.hpp"

int main() {
    libusb_context *ctx;
    const int ec = libusb_init(&ctx);
    if (ec < 0) {
        std::printf("LibUsb if ducked up.\n");
        return -1;
    }
    probe_devices(ctx);
    if (dfu_root == nullptr) {
        std::printf("dfu-util is ducked up.\n");
        return -2;
    }
    return 0;
}

На что получил вот такой вывод:

Пуля от второго выстрела уже летит в коленную чашечку
Пуля от второго выстрела уже летит в коленную чашечку

Неопределенные символы... Пошел посмотреть, что это такое.

Коленная чашечка раздроблена вторым выстрелом
Коленная чашечка раздроблена вторым выстрелом

Разработчики без сарказма прекрасного инструмента даже и не планировали, чтобы утилита была библиотекой, и просто захардкодили глобальные переменные, которые декларированы в ThirdParty/dfu-util/src/main.cpp. Мне этот файл не нужен, поэтому в голове созрел чудесный и быстродействующий план. Я на всякий случай прикреплю фото своего лица в этот момент.

Моё лицо, когда увидел во всех сурcах extern
Моё лицо, когда увидел во всех сурcах extern

Невелика потеря. Действую примитивно и действенно - как баран, увидевший хлипкую доску забора.

Создаю файлы ThirdParty/DfuUtilVariables.hpp и ThirdParty/DfuUtilVariables.cpp с очевидным содержанием. Не забываю закинуть cpp-шник в CMake таргет dfu-util.

/**
 * ThirdParty/DfuUtilVariables.hpp
 */

#pragma once

extern int verbose;
extern struct dfu_if *dfu_root;
extern char *match_path;
extern int match_vendor;
extern int match_product;
extern int match_vendor_dfu;
extern int match_product_dfu;
extern int match_config_index;
extern int match_iface_index;
extern int match_iface_alt_index;
extern int match_devnum;
extern const char *match_iface_alt_name;
extern const char *match_serial;
extern const char *match_serial_dfu;

/**
 * ThirdParty/DfuUtilVariables.cpp
 */

#include "DfuUtilVariables.hpp"

#include <cstddef>

int verbose = 0;
struct dfu_if *dfu_root = nullptr;
char *match_path = nullptr;
int match_vendor = -1;
int match_product = -1;
int match_vendor_dfu = -1;
int match_product_dfu = -1;
int match_config_index = -1;
int match_iface_index = -1;
int match_iface_alt_index = -1;
int match_devnum = -1;
const char *match_iface_alt_name = nullptr;
const char *match_serial = nullptr;
const char *match_serial_dfu = nullptr;

Заветные фразы получены. Нравится.

[12/12] Linking CXX executable WorDfuUtil

Build finished

dfu-util is ducked up.

Process finished with exit code 156
(interrupted by signal 28:SIGWINCH)

Вылет по 156-му коду мне не интересен, т.к. это скорее всего инициализированная LibUsb ругается, что её не закрыли. Можно считать, что подключение dfu-util успешно завершено.

Пример более высокоуровневого API

Надо бы проверить существующую прослойку на предмет юзабельности. Напишу какой-нибудь объект с примитивным функционалом. Пусть это будет максимально простая загрузка прошивки с устройства. Думается, написание будет создавать некий дискомфорт по следующим причинам:

  1. У меня есть глобальные переменные из ThirdParty/DfuUtilVariables.hpp, которые очень невкусно выглядят. Хорошо бы их потом обернуть в какой‑нибудь «контейнер», но пока это не выглядит как необходимость;

  2. Есть буквально «инструкция» из файла ThirdParty/dfu-util/src/main.cpp, откуда можно брать куски готового кода, но в нём не все так очевидно, как хотелось бы.
    Например, если заполнить глобальную переменную вручную, а не вызовом dfuse_parse_options(const char*), то она всё равно будет игнорироваться в методе dfuload_do_upload(...), т.к. не будет поднят флаг int dfuse_address_present. Можно, конечно, его тоже поднять вручную, но это не единственный подобный момент. Для полноценной работы кода нужно взять прочные штаны и написать полноценную обёртку с устранением всех прелестей, но это выходит за пределы публикации и скорее всего будет выполнено позже и выложено на гите.

Для более короткого представления кода я нацелено пойду на следующие упрощения:

  • Отсутствие JavaDoc в коде. Я, честно говоря, не знаю, стоит ли писать JavaDoc к коду в статье или нет. Приведу пример без доки, но на гит выложу с докой. Поправьте, пожалуйста, данное решение в комментариях при необходимости, и я внесу правки в статью, добавив JavaDoc;

  • Вместо создания класса для проверки и управления состоянием USB устройством я просто объявлю функцию в анонимном пространстве имён;

  • Отсутствие постоянных проверок на статус USB устройства;

  • Буквально ничего не буду делать, чтобы исправить факт существования глобальных переменных из ThirdParty/DfuUtilVariables.hpp.

Погнали.

/**
 * include/FirmwareStatus.hpp
 */

#pragma once

#include <cstdint>

namespace WorDfuUtil {

    enum class FrameworkStatus
            : std::uint8_t {
        Deinited = 0b0,
        Inited = 0b1,
        DeviceOpened = 0b10,
        DfuDeviceFound = 0b100,
        InterfaceClaimed = 0b1000,
        AltInterfaceSelected = 0b10000
    };
}
/**
 * include/Firmware.hpp
 */

#pragma once

#include <string>

namespace WorDfuUtil {

    class Firmware final {
    public:
        [[nodiscard]]
        static bool Upload(const std::string& filePath, int transferLimit, int transferSize = -1) noexcept;
    };
}
/**
 * src/Firmware.cpp
 */

#include "pch.hpp"

#include "Firmware.hpp"
#include "FirmwareStatus.hpp"

#include <cstdio>
#include <filesystem>
#include <fcntl.h>
#include <sstream>

using namespace WorDfuUtil;

namespace {

    libusb_context *ctx;

    std::uint8_t frameworkStatus = static_cast<std::uint8_t>(FrameworkStatus::Deinited);

    [[nodiscard]]
    bool isDfuDeviceFound() noexcept {
        std::printf("Cannot find DFU device.\n");
        return ::dfu_root != nullptr;
    }

    [[nodiscard]]
    bool checkDfuStatus() {
        if (!::isDfuDeviceFound()) {
            return false;
        }
        dfu_status dfuStatus {};
        const int ec = dfu_get_status(::dfu_root, &dfuStatus);
        if (ec < 0) {
            std::printf("Cannot check device status: %s\n", libusb_error_name(ec));
            return false;
        }
        if (dfuStatus.bStatus != 0 || dfuStatus.bState != DFU_STATE_dfuIDLE) {
            std::printf("Bad DFU device status: %s\n", dfu_status_to_string(dfuStatus.bStatus));
            std::printf("Bad DFU device state: %s\n", dfu_state_to_string(dfuStatus.bState));
            return false;
        }
        return true;
    }

    void deinitFrameworks() noexcept {
        if (::frameworkStatus == static_cast<std::uint8_t>(FrameworkStatus::Deinited)) {
            return;
        }

        if (::frameworkStatus & static_cast<std::uint8_t>(FrameworkStatus::DeviceOpened)) {
            libusb_close(::dfu_root->dev_handle);
        }

        if (::frameworkStatus & static_cast<std::uint8_t>(FrameworkStatus::DfuDeviceFound)) {
            disconnect_devices();
        }

        if (::frameworkStatus & static_cast<std::uint8_t>(FrameworkStatus::Inited)) {
            libusb_exit(::ctx);
        }
    }

    bool prepareFrameworks() {
        int ec = libusb_init(&::ctx);
        if (ec < 0) {
            std::printf("LibUsb initialization error - %s.\n", libusb_error_name(ec));
            return false;
        }
        ::frameworkStatus |= static_cast<std::uint8_t>(FrameworkStatus::Inited);

        probe_devices(::ctx);
        if (!::isDfuDeviceFound()) {
            return false;
        }
        ::frameworkStatus |= static_cast<std::uint8_t>(FrameworkStatus::DfuDeviceFound);

        ec = libusb_open(::dfu_root->dev, &::dfu_root->dev_handle);
        if (ec < 0) {
            std::printf("Cannot open device - %s.\n", libusb_error_name(ec));
            return false;
        }
        ::frameworkStatus |= static_cast<std::uint8_t>(FrameworkStatus::DeviceOpened);

        return ::checkDfuStatus();
    }
}

bool Firmware::Upload(const std::string &filePath, int transferLimit, int transferSize) noexcept {
    if (!::prepareFrameworks()) {
        ::deinitFrameworks();
        return false;
    }

    if (std::filesystem::exists(filePath)) {
        std::ignore = std::filesystem::remove(filePath);
    }

    const int fileDescriptor = open(filePath.c_str(), O_WRONLY | O_BINARY | O_CREAT | O_EXCL | O_TRUNC, 0666);
    if (fileDescriptor < 0) {
        std::printf("Cannot create and open file %s for writing.\n", filePath.c_str());
        ::deinitFrameworks();
        return false;
    }

    std::stringstream dfuUtilOptions;
    dfuUtilOptions << "-s 0x08000000";
    dfuUtilOptions << ":" << transferLimit;

    if (transferSize < 0) {
        transferSize = libusb_le16_to_cpu(::dfu_root->func_dfu.wTransferSize);
        if (transferSize == 0) {
            std::printf("Device return zero transfer size. Specify it manually.\n");
            close(fileDescriptor);
        }
        if (transferSize < ::dfu_root->bMaxPacketSize0) {
            transferSize = ::dfu_root->bMaxPacketSize0;
        }
    }

    const int ec = dfuse_do_upload(::dfu_root,
                                   transferSize,
                                   fileDescriptor,
                                   dfuUtilOptions.str().c_str());
    close(fileDescriptor);
    if (ec < 0) {
        std::printf("Error in firmware uploading.\n");
    }

    ::deinitFrameworks();
    return true;
}

Нужно. Побороть. Желание. Написать. Нормально...
Статья. Должна быть. Короткой...
Ладно, после написания статья просто на гите внесу все правки и перепишу код, заполнив все комментарии и разделив всё на объекты.

Ну и протестим это счастье:

/**
 * src/main.cpp
 */

#include "Firmware.hpp"

using namespace WorDfuUtil;

int main() {
    std::string filePath = "firmware.bin";
    int transferLimit = 150'000;

    const bool uploadRes = Firmware::upload(filePath, transferLimit);
    if(!uploadRes) {
        return -50;
    }
    return 0;
}
Вывод программы чтения прошивки с DFU устройства
Вывод программы чтения прошивки с DFU устройства

В целом, можно было бы заканчивать и предоставить финальный вид кода, но ещё остался Windows, который обещает быть очень весёлым.

Продолжаем. Windows

Сразу же началось веселье из-за того, что autoconf и autoheader нет готовых под Windows. Идём на msys2, скачиваем этот замечательный инструмент и качаем из его среды все инструменты. На билд инструкциях от dfu-util разработчиков есть небольшие подсказки для сборки инструментам. Обычно я ставлю пакет mingw-w64-x86_64-toolchain, но сейчас можно и точечно поставить только необходимое:

$ pacman -S mingw-w64-x86_64-gcc
$ pacman -S mingw-w64-x86_64-autotools 
$ pacman -S mingw-w64-x86_64-liusb

Не забываю добавить в переменную пути окружение msys2:

C:/msys64/mingw64
C:/msys64/mingw64/bin

Возвращаюсь к проекту и пытаюсь запустить конфигурацию dfu-util по билд-инструции. Винда - она такая, лучше сначала проверить работоспособность ручками:

$ bash autogen.sh
$ bash configure USB_CFLAGS="-IC:/msys64/mingw64/include/libusb" \
                 USB_LIBS="-L C:/msys64/mingw64/lib -lusb-1.0"

Когда я первый раз настраивал dfu-util, возникал целый вагон ошибок. То он не может найти libusb.h, то он не может найти определение lusb-1.0 библиотеки. Подозреваю, я просто неверно передавал параметры в configure.

Между делом заметил, что в логе от configure есть вот такой вывод, которого не было на macOS:

Лог вывода configure
Лог вывода configure

Эта строчка носит фиктивный характер. Дефайн HAVE_LIBUSB не участвует в источниках dfu-util, так что я закрыл на это глаза. В силу бесполезности исправления данной проверки на LibUsb я даже помещу способы решения в скрывающийся блок.

Способы исправления проверки LibUsb

Если же хочется исправить лог вывода, то есть 2 пути:

Путь первый. Путь непредсказуемый.
Переопределить файл для pkg-config libusb-1.0.pc. Он обычно поставляется, если ставить LibUsb через пакетный менеджер а-ля vcpkg или homebrew.

# libusb-1.0.pc

prefix=/opt/homebrew/Cellar/libusb/1.0.27
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include

Name: libusb-1.0
Description: C API for USB device access from Linux, Mac OS X, Windows, OpenBSD/NetBSD and Solaris userspace
Version: 1.0.27
Libs: -L${libdir} -lusb-1.0
Libs.private:  -lobjc -Wl,-framework,IOKit -Wl,-framework,CoreFoundation -Wl,-framework,Security
Cflags: -I${includedir}/libusb-1.0

Заменяем 9-ую строку

# libusb-1.0.pc
# ...
Version: 1.0.27
Libs: -L${libdir} -lusb
# ...

Максимально не рекомендую этого делать, т.к. другие библиотеки могут искать -lusb-1.0 и не найдут. Я с подобным не сталкивался, но лучше не играться с файлами поставляемыми через пакетный менеджер.

Зачем я привел заведомо опасный пример? Теперь вы знаете, что из себя представляют файлы для pkg-config и какую информацию они в себе несут.

Путь второй. Путь рекомендуемый.
Этот способ самый безболезненный. Открываем файл ThirdParty/dfu-util/configure.ac

# ThirdParty/configure.ac
# ...
Checks for libraries.
On FreeBSD the libusb-1.0 is called libusb and resides in system location
AC_CHECK_LIB([usb], [libusb_init],, [native_libusb=no],)
AS_IF([test x$native_libusb = xno], [
PKG_CHECK_MODULES([USB], [libusb-1.0 >= 1.0.0],,
AC_MSG_ERROR([*** Required libusb-1.0 >= 1.0.0 not installed ***]))
])
# ...

И заменяем исправляем 18-ую строку

# ThirdParty/configure.ac
# ...
AC_CHECK_LIB([usb-1.0], [libusb_init],, [native_libusb=no],)
# ...

При этом будут некоторые изменения генерации.
Во множестве строчках файла configure все упоминания lusb заменятся на lusb-1.0.
В выходном заголовочном файле config.h дефайн HAVE_LIBUSB заменится на HAVE_LIBUSB_1_0. Этот символ больше нигде не используется... Не знаю, зачем они его генерят.

После повторного запуска конфигурации лог проверки LibUsb уже будет успешным.

Лог вывода configure после исправления
Лог вывода configure после исправления

Переношу эти команды в CMake:

# ThirdPart/DfuUtilConfiguring.cmake

if (APPLE)
    set(AutogenCommand ./autogen.sh)
    set(ConfigureCommand ./configure --libdir=${DfuRoot}/lib --includedir=${DfuRoot}/include)
elseif (WIN32)
    set(AutogenCommand bash autogen.sh)
    set(ConfigureCommand bash configure USB_CFLAGS="-I${LibusbIncludeDir}" USB_LIBS="-L ${LibusbLibDir} -lusb-1.0")
endif ()

add_custom_target(dfu-util_autogen
        COMMAND ${AutogenCommand}
        COMMENT "Creating dfu-util configuration file..."
        WORKING_DIRECTORY ${DfuUtilRoot})

add_custom_target(dfu-util_configure
        COMMAND ${ConfigureCommand}
        DEPENDS dfu-util_autogen
        COMMENT "Configuring dfu-utils for current OS. Generating make files..."
        WORKING_DIRECTORY ${DfuUtilRoot})

add_custom_target(dfu-util_build
        COMMAND make
        DEPENDS dfu-util_configure
        COMMENT "Running make for dfu-utils project..."
        WORKING_DIRECTORY ${DfuUtilRoot})

На комбинации MinGW + Ninja всё ожидаемо работает корректно и без проблем

Результат сборки на MinGW + Ninja
Результат сборки на MinGW + Ninja

Но я хочу адаптировать и на MSVC, так что ныряем в проблемы и продолжаем.

Результат сборки на MSVC + Ninja
Результат сборки на MSVC + Ninja

unistd.h - это файл для POSIX системы, которого естественно нет на Windows. Указывать путь к заголовочникам MinGW безсполезно, т.к. MSVC с ума сойдет, если их увидит.

То, что мне никак не хотелось делать - это менять config.h файл. Изначально планировалось оставить исходные файлы dfu-util как они есть, но все-таки придется немножко их изменить. После генерации с помощью config.h.in формируется config.in, которые в свою очередь декларирует, что в системе есть unistd.h заголовочник. Подкорректирую этот момент с помощью CMake, чтобы генерация была незаметной для пользователя и не требовала ручного форматирования. Инструкция записи дополнительного кофига должна срабатывать автоматически после конфигурации, так что выносим её в отдельный файл и вызываем как POST_BUILD после таргета dfu-util_configure .

# ThirdParty/WriteExtraConfig.cmake

cmake_minimum_required(VERSION 3.23)

set(ConfigComment
        "
/**
 * Wor:
 */")

file(READ dfu-util/config.h Config)
string(FIND "${Config}" "${ConfigComment}" ConfigCommentIndex REVERSE)

if (${ConfigCommentIndex} EQUAL -1)
    file(READ extraConfig.in ExtraConfig)
    string(APPEND Config "${ConfigComment}\n${ExtraConfig}")
    file(WRITE dfu-util/config.h "${Config}")
endif ()
# ThirdParty/DfuUtilConfiguring.cmake
# ...
add_custom_target(dfu-util_configure
        COMMAND ${ConfigureCommand}
        DEPENDS dfu-util_autogen
        COMMENT "Configuring dfu-utils for current OS. Generating make files..."
        WORKING_DIRECTORY ${DfuUtilRoot})

include(WriteExtraConfig.cmake)

add_custom_command(TARGET dfu-util_configure
        POST_BUILD
        COMMENT "Adding extra config to config.h file..."
        COMMAND ${CMAKE_COMMAND}
        -P ${CMAKE_CURRENT_SOURCE_DIR}/WriteExtraConfig.cmake
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
# ...
/**
 * ThirdPart/extraConfig.in
 */

#if WIN32

#undef HAVE_UNISTD_H
#undef HAVE_NANOSLEEP

#include <basetsd.h>
#include <sys/types.h>

#ifndef SSIZE_MAX
#define SSIZE_MAX INTPTR_MAX
#endif

typedef SSIZE_T ssize_t;

#endif
Возможные проблемы с разыменованием переменных в CMake

Изначально код выглядел вот так.

# ThirdParty/WriteExtraConfig.cmake

cmake_minimum_required(VERSION 3.23)

set(ConfigComment
        "
/**
 * Wor:
 */")

file(READ dfu-util/config.h Config)
string(FIND "${Config}" "${ConfigComment}" ConfigCommentIndex REVERSE)

if (${ConfigCommentIndex} EQUAL -1)
    file(READ extraConfig.in ExtraConfig)
    string(APPEND Config ${ConfigComment} "\n" ${ExtraConfig})
    string(REPLACE "%" "\;" Config "${Config}")
    file(WRITE dfu-util/config.h ${Config})
endif ()
/**
 * ThirdPart/extraConfig.in
 */

/* ... */

typedef SSIZE_T ssize_t%

/* ... */

Можете взять паузу и попробовать найти ошибку в нём и причину существование строки 15 в файле ThirdParty/WriteExtraConfig.cmake и строки 13 файла extraConfig.in.
* Секции Variable References и Lists из офф. мануала CMake подскажут решение.

Дело достаточно банально. Сначала я испытал проблемы с парсингом символа ';' в CMake и посчитал, что это и справедливо. Точка с запятой является знаком разделения элементов массива, а строка - это такой же массив. Приняв это как данное, я просто заменил в файле extraConfig.in символ ';' на '%' и меняю обратно их в CMake.

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

Приведу небольшой пример:

include(CMakePrintHelpers)
set(Var1 1;2;3;4;5)
cmake_print_variables(Var1)    -> Var1="1;2;3;4;5" 
message(WARNING ${Var1})       -> 12345
message(WARNING "${Var1}")     -> 1;2;3;4;5
set(Var2 "1;2;3;4;5")
cmake_print_variables(Var2)    -> Var2="1;2;3;4;5"
message(WARNING ${Var2})       -> 12345
message(WARNING "${Var2}")     -> 1;2;3;4;5
set(StrVar1 "Hel;lo")
cmake_print_variables(StrVar1) -> StrVar1="Hel;lo"
message(WARNING ${StrVar1})    -> Hello
message(WARNING "${StrVar1}")  -> Hel;lo
set(StrVar2 "Hel" "lo")
cmake_print_variables(StrVar1) -> StrVar2="Hel;lo"
message(WARNING ${StrVar2})    -> Hello
message(WARNING "${StrVar2}")  -> Hel;lo

Данный фрагмент кода и ссылки на секции из документации CMake опишут происходящее намного лаконичнее, чем я.

Пробую исполнение таргета WorDfuUtil:

Лог билда WorDfuUtil
Лог билда WorDfuUtil

Возвращаем WorDfuUtil-у тип библиотеки add_library(WorDfuUtil) и проверяем на компиляцию статического и динамического типов.

Лог WorDfuUtil как статической библиотеки
Лог WorDfuUtil как статической библиотеки
Лог WorDfuUtil как динамической библиотеки
Лог WorDfuUtil как динамической библиотеки

Заключение

Какие можно подвести итоги? Для себя я выделял несколько целей написания и публикации статьи:

  1. Когда я столкнулся на рабочих задачах с dfu-util, то основным тормозом был я было как раз подключение инструмента. В принципе, подключение чего-то на языке Си без готового CMake-а не является чем-то необычным, но dfu-util привнесла новые краски в этот процесс. Поэтому первостепенной целью статьи было облегчить жизнь остальным участникам комьюнити. В целом, эту статью можно даже считать псевдо-гайдом по подключению неподключаемого кода Си и написанию CMakeList-ов.

  2. Во время написание статьи приходится разбираться в моментах, на которые обычно закрываешь глаза и пропускаешь. Можно назвать это насильственным обучением. Пришлось смотреть и тестить некоторые штуки в CMake и придумывать какие-то пути, чтобы не использовать костыли, которые я привык допускать для скорости выполнения задач.
    * Привет моему лиду, который бомбит на дублирование кода, goto и остальные снасти упрощения жизни.

Код вы можете посмотреть по ссылке, которая ведёт на версию библиотеки, соответствующую статье. Позже будут добавляться новые коммиты как от меня, так и, надеюсь, от других пользователей. Там же можете либо открывать issue на код, либо предлагать собственные варианта развития библиотеки. Я постараюсь периодически вносить обновления и писать новые классы.

dfu-util — прекрасная утилита. Она может показаться достаточно кривой, если смотреть в исходный код, но определенно стоит внимания и внесению вклада со стороны комьюнити.

P.S. Не знаю, как это делается на хабре, так что скажу здесь. Если у вас есть примеры библиотек или утилит, которые будет интересно адаптировать под подключение к CMake, я постараюсь поработать и над ними.

Комментарии (5)


  1. Playa
    18.05.2024 07:38
    +1

    Если у вас есть примеры библиотек или утилит, которые будет интересно адаптировать под подключение к CMake, я постараюсь поработать и над ними.

    А может не надо? В мире уже достаточно CMake-говнокода и то, что описано в статье, только увеличило его количество.


    1. WorHyako Автор
      18.05.2024 07:38
      +1

      :D

      Если бы вы подчеркнули хотя бы несколько моментов, где CMake написан криво, я бы был благодарен и с радостью исправлю.

      Чур, не предлагать алиасы, объекты и генераторы выражений, т.к. это поднимет порог чтения статьи, чего мне не хочется делать (:


  1. Mcublog
    18.05.2024 07:38

    Классная статья, спасибо)

    Может быть в другой покажите кунг фу по симейку?

    Я пользуюсь базовыми вещами в нем, было бы интересно посмотреть, где используются его навороты. В официальной доке на удивление практически нет примеров, только пространные рассказы о возможности)

    Ну и по теме загрузчиков неплохой проект openblt.

    https://github.com/feaser/openblt/

    Он уже на симейк, поэтому переделывать ничего не нужно☺️☺️


    1. WorHyako Автор
      18.05.2024 07:38
      +1

      Спасибо, Mcublog
      Действительно в сети в быстром доступе достаточно мало примеров кода на CMake. Из-за этого в сети, как отметил @Playa, много примеров некорректного и антипаттерного кода. Если бы спрашивали моё мнение, то я бы порекомендовал книгу "Professional CMake: A Practical Guide" авторства Craig Scott .

      На Habr я встречал статью, которая посвящена вольному пересказу книги. Какое-то время я раздумывал о том, чтобы сделать аналогичную публикацию по книге "Professional CMake: A Practical Guide", но это обесценит вклад Крейга Скотта, т.к. его книга не в открытом доступе. Могу порекомендовать Вам создать репозиторий, идти по чаптерам книги и экспериментировать с CMake. Эта система сборки очень и очень стоит внимания.

      Про проект Feaser/OpenBit, который Вы упомянули, я не слышал из-за того, что моя сфера деятельности лишь косвенно связана с embedded сферой. Посмотрел его CMake код и увидел простоту и лаконичность. Не хватает CMake 3.21+, применения модулей, исправления дублирования кода, который есть в каждом проекте, объединения всех проектов под единый CMakeLists.txt, чтобы можно было заниматься всеми проектами с единым пространством кэша, и прочее.

      Спасибо за Ваш пример. Возможно когда-нибудь я открою PR в его репозитория для усовершенствования CMake части.

      Сила IT в его комьюнити! Да здравствует коллективная работа над open sources проектами!


      1. Mcublog
        18.05.2024 07:38

        Большое спасибо за такое подробное мнение. И за наводку на книгу, добавлю её в закладки.

        Также было интересно прочитать вашу оценку сборки openblt, довольно плотно одно время с ним работал и остались приятные воспоминания.

        Сила IT в его комьюнити! Да здравствует коллективная работа над open sources проектами!

        Ура!!)