Как перестать писать прошивки для микроконтроллеров и начать жить. Часть I


    Здравствуйте, меня зовут Евгений, и мне надоело писать прошивки для микроконтроллеров. Как это это случилось и что с этим делать, давайте разберемся.


    После того как поработаешь в большом программировании С++, Java, Python, и т. Д. Возвращаться к маленьким и пузатым микроконтроллерам совсем не хочется. К их скудным инструментам и библиотекам. Но делать иногда нечего, задачи real-time и автономности, не оставляют выбора. Но есть некоторые типы задач, которые просто выбешивает в этой области решать.


    К примеру тестирование оборудования, ничего более скучного и занудного занятия в embedded программировании, вряд ли можно придумать. Вообщем как и удобных инструментов для этого. Пишешь… Прошиваешь… моргаешь… светодиодиком (иногда логи по UART). Все ручками, без специализированных инструментов для тестирования.


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


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


    Для таких экспериментов больше подходит что-то типа REPL, дабы можно было просто и безболезненно делать вот такие, хотя бы банальные, вещи:


    \


    Как к этому прийти, посвящен этот цикл статей.


    И в этот раз мне попался проект где потребовалось тестировать довольно сложное устройство, с уймой всяких датчиков и прочих, неизвестных мне ранее, микросхем, которые использовали множество периферии МК и кучу разных интерфейсов. Особое веселье было, то что к плате у меня не было исходных кодов прошивки, так что все тесты пришлось бы писать с нуля, без использования наработок из исходного кода.


    Проект обещал хорошего тамаду и конкурсы интересные на месяца два так ( а скорее всего и больше).


    Ладно, мы же тут не плакаться собрались. Надо либо снова окунуться в дебри С и бесконечных прошивок, либо отказаться либо что-то придумать, что бы облегчить себе это занятие. В конце концов лень да любопытство — двигатель прогресса.


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


    http://openocd.org/doc/html/General-Commands.html
    15.4 Memory access commands
    mdw, mdh, mdb — позволяют считывать значению по физическому адресу на микроконтроллере
    mww, mwh, mwb — позволяют записывать по физическому адресу на микроконтроллере

    Интересно…. А регистры периферии читать и писать с их помощью можно?.. оказывается можно, да к тому же эти команды можно выполнять удаленно через TCL сервер, который запускается при старте openOCD.


    Вот пример моргания светодиодиком для stm32f103C8T6


    // Step 1: Enable the clock to PORT B
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
    
    // Step 2: Change PB0's mode to 0x3 (output) and cfg to 0x0 (push-pull)
    GPIOC->CRH = GPIO_CRH_MODE13_0 | GPIO_CRH_MODE13_1;
    
    // Step 3: Set PB0 high
    GPIOC->BSRR = GPIO_BSRR_BS13;
    
    // Step 4: Reset PB0 low
    GPIOC->BSRR = GPIO_BSRR_BR13;

    и аналогичный ему последовательность команд openOCD


    mww 0x40021018 0x10
    mww 0x40011004 0x300000
    mww 0x40011010 0x2000
    mww 0x40011010 0x20000000

    А теперь, если задуматься о вечном и рассмотреть прошивки для МК… то основное предназначение этих программ это запись в регистры чипа; прошивка, которая будет просто что-то делать и работать только с процессорным ядром, не имеет никакого практического применения!


    Примечание

    Хотя конечно можно и крипту считать(=


    Многие вспомнят, еще про работу с прерываниями. Но они не всегда требуются, и в моем случае можно обойтись и без них.


    И так, жизнь налаживается. В исходниках openOCD можно даже найти, интересный пример использования данного интерфейса.


    Очень хорошая заготовочка на питоне.


    Вполне можно конвертировать адреса регистров из заголовочных файлов, и начать писать на кошерном скриптовом языке. Уже можно готовить шампанское, но мне показалось этого мало, ведь хочется вместо возни с регистрами использовать Standard Peripherals Library или новый HAL для работы с периферией.


    Портировать библиотеки на питон … в каком-нибудь страшном сне этим займемся. Значит надо как использовать эти библиотеки в С или … С++. А в плюсах же можно переопределить почти все операторы … для своих классов.


    А базовые адреса в заголовочных файлах, подменить на объекты своих классов.


    К примеру в файле stm32f10x.h


    #define PERIPH_BB_BASE        ((uint32_t)0x42000000) /*!< Peripheral base address in the bit-band region */

    Заменить на


    class InterceptAddr;
    InterceptAddr addr;
    #define PERIPH_BB_BASE        (addr) /*!< Peripheral base address in the bit-band region */

    Но игры с указателями в библиотеке, рубят на корню эту идею...


    Вот к примеру файл stm32f10x_i2c.c :


    FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG)
    {
    __IO uint32_t i2creg = 0, i2cxbase = 0;
        ….
      /* Get the I2Cx peripheral base address */
      i2cxbase = (uint32_t)I2Cx;
    ….

    Значит надо как-то по другому перехватывать обращения к адресам. Как это делать наверно стоит посмотреть у Valgrind, не зря у него есть memchecker. Уж он то точно должен знать как перехватывать обращения по адресам.


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


    Int * p = ...
    *p = 0x123;

    Перехватить адрес есть возможность, а вот перехватить записываемые данные уже не получалось. Только название внутреннего регистра в котором это значение лежит, но до которого не добраться из memcheck.


    На самом деле Valgrind удивил меня, внутри используется древний монстр libVEX, о котором я вообще не нашел никакой информации в интернете. Хорошо что немного документации удалось найти в заголовочных файлах.


    Потом были другие инструменты DBI.


    Frida, Dynamic RIO, еще какой-то, и наконец попался Pintool.


    У PinTool оказалась неплохая документация и примеры. Хотя мне их все равно не хватило, и с некоторыми вещами пришлось делать эксперименты. Инструмент оказался очень мощный, единственно огорчает закрытый код и ограничение только платформой intel (хотя в дальнейшем это можно будет обойти)


    Итак, нам нужно перехватывать запись и чтение по определенным адресам. Посмотрим какие инструкции отвечают за это https://godbolt.org/z/nJS9ci.


    Для х64 это будет MOV для обоих операций.


    А для х86 это будет MOV для записи и MOVZ для чтения.


    Примечание: лучше всего не включать оптимизацию, иначе могут повылазить другие инструкции.


    Пишем перехватчики для этих инструкций
      INS_AddInstrumentFunction(EmulateLoad, 0);
      INS_AddInstrumentFunction(EmulateStore, 0);
    
    .....
    
    static VOID EmulateLoad(INS ins, VOID *v) {
      // Find the instructions that move a value from memory to a register
      if ((INS_Opcode(ins) == XED_ICLASS_MOV ||
           INS_Opcode(ins) == XED_ICLASS_MOVZX) &&
          INS_IsMemoryRead(ins) && INS_OperandIsReg(ins, 0) &&
          INS_OperandIsMemory(ins, 1)) {
        INS_InsertCall(ins, IPOINT_BEFORE, AFUNPTR(loadAddr2Reg),
                       IARG_MEMORYREAD_EA, IARG_MEMORYREAD_SIZE, IARG_RETURN_REGS,
                       INS_OperandReg(ins, 0), IARG_END);
    
        // Delete the instruction
        INS_Delete(ins);
      }
    }
    
    static VOID EmulateStore(INS ins, VOID *v) {
      if (INS_Opcode(ins) == XED_ICLASS_MOV && INS_IsMemoryWrite(ins) &&
          INS_OperandIsMemory(ins, 0)) {
        if (INS_hasKnownMemorySize(ins)) {
          if (INS_OperandIsReg(ins, 1)) {
            INS_InsertCall(ins, IPOINT_BEFORE, AFUNPTR(multiMemAccessStore),
                           IARG_MULTI_MEMORYACCESS_EA, IARG_REG_VALUE,
                           INS_OperandReg(ins, 1), IARG_END);
          } else if (INS_OperandIsImmediate(ins, 1)) {
            INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)multiMemAccessStore,
                           IARG_MULTI_MEMORYACCESS_EA, IARG_UINT64,
                           INS_OperandImmediate(ins, 1), IARG_END);
          }
        } else {
          if (INS_OperandIsReg(ins, 1)) {
            INS_InsertCall(ins, IPOINT_BEFORE, AFUNPTR(storeReg2Addr),
                           IARG_MEMORYWRITE_EA, IARG_REG_VALUE,
                           INS_OperandReg(ins, 1), IARG_MEMORYWRITE_SIZE, IARG_END);
          } else if (INS_OperandIsImmediate(ins, 1)) {
            INS_InsertCall(ins, IPOINT_BEFORE, AFUNPTR(storeReg2Addr),
                           IARG_MEMORYWRITE_EA, IARG_UINT64,
                           INS_OperandImmediate(ins, 1), IARG_UINT32,
                           IARG_MEMORYWRITE_SIZE, IARG_END);
          }
        }
      }
    }

    В случае чтения из адреса мы вызываем ф-цию loadAddr2Reg и удаляем оригинальную инструкцию. Исходя из этого loadAddr2Reg нам должна возвращать необходимое значение.


    С записью все сложнее… аргументы могут быть разных типов и к тому же передаваться по разному, поэтому приходится перед командой вызывать разные ф-ции. На 32-битной платформе multiMemAccessStore, а на 64 будет вызываться storeReg2Addr. Причем здесь инструкцию из конвеера не удаляем. Удалить проблем её нет, но вот сымитировать её действие в некоторых случаях не получается. Программа почему-то иногда валится в sigfault. Для нас это не критично, пусть себе пишет, главное что есть возможность перехвата аргументов.


    Дальше надо посмотреть, а какие адреса нам надо перехватывать, посмотрим на Memory Map для нашего чипа stm32f103C8T6:



    Нас интересуют адреса с SRAM и PERIPH_BASE, т.е с 0x20000000 по 0x20000000 + 128*1024 и с 0x40000000 по 0x40030000. Отлично, вернее не совсем, как помним инструкцию записи мы удалить не смогли. Поэтому запись по этим адресам будет вываливаться в sigfault. К тому же есть неиллюзорная вероятность того что на эти адреса будет приходится данные нашей программы, не у этого чипа так у другого. Поэтому однозначно надо их куда-то отремапить. Допустим на какой нибудь массив.


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


    В нашей программе, в заголовчниках вместо


    #define SRAM_BASE             ((uint32_t)0x20000000) /*!< SRAM base address in the alias region */
    #define PERIPH_BASE           ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */

    Делаем


     #define SRAM_BASE             ((AddrType)pAddrSRAM)
     #define PERIPH_BASE           ((AddrType)pAddrPERIPH)

    и где pAddrSRAM и pAddrPERIPH указатели на заранее выделенные массивы.


    Теперь нашему PinTool клиенту надо как-то передать как мы отремапили необходимые адреса.
    Самое простое что мне показалось, как сделать это перехват ф-ции, которая возвращает структуру массив с такого формата:


    typedef struct
    {
     addr_t start_addr; //адрес массива куда ремапятся нужные адреса
     addr_t end_addr;   //размер этого массива
     addr_t reference_addr; // отремапленные адрес
    } memoryTranslate;

    К примеру для нашего чипа это будет так заполняться


        map->start_addr = (addr_t)pAddrSRAM;
        map->end_addr = 96*1024;
        map->reference_addr = (addr_t)0x20000000U;

    Перехватить ф-цию и взять из нее требуемые значения не составляет большого труда:


    Перехватить ф-цию и взять из нее требуемые значения не составляет большого труда:
    IMG_AddInstrumentFunction(ImageReplace, 0);
    ....
    
    static memoryTranslate *replaceMemoryMapFun(CONTEXT *context,
                                                AFUNPTR orgFuncptr,
                                                sizeMemoryTranslate_t *size) {
      PIN_CallApplicationFunction(context, PIN_ThreadId(), CALLINGSTD_DEFAULT,
                                  orgFuncptr, NULL, PIN_PARG(memoryTranslate *),
                                  &addrMap, PIN_PARG(sizeMemoryTranslate_t *), size,
                                  PIN_PARG_END());
    
      sizeMap = *size;
    
      return addrMap;
    }
    
    static VOID ImageReplace(IMG img, VOID *v) {
      RTN freeRtn = RTN_FindByName(img, NAME_MEMORY_MAP_FUNCTION);
      if (RTN_Valid(freeRtn)) {
        PROTO proto_free =
            PROTO_Allocate(PIN_PARG(memoryTranslate *), CALLINGSTD_DEFAULT,
                           NAME_MEMORY_MAP_FUNCTION,
                           PIN_PARG(sizeMemoryTranslate_t *), PIN_PARG_END());
    
        RTN_ReplaceSignature(freeRtn, AFUNPTR(replaceMemoryMapFun), IARG_PROTOTYPE,
                             proto_free, IARG_CONTEXT, IARG_ORIG_FUNCPTR,
                             IARG_FUNCARG_ENTRYPOINT_VALUE, 0, IARG_END);
      }
    }

    И нашу перехватываемую ф-цию сделать такого вида:
    memoryTranslate * getMemoryMap(sizeMemoryTranslate_t * size){
    ...
    return memoryMap;
    }

    Что же самая нетривиальная работа сделана, осталось сделать клиента к OpenOCD, в PinTool клиенте мне не хотелось его реализовать, поэтому я делал отдельным приложением, с которым наш PinTool клиент общается через named fifo.


    Таким образом схема интерфейсов и коммуникаций получается такая:



    А упрощенный workflow работы на примере перехвата адреса 0х123:



    Давайте разберемся по порядку что же здесь происходит:


    1. запускается PinTool клиент, делает инициализацию наших перехватчиков, запускает программу
    2. Программа запускается, ей нужно отремапить адреса регистров на какой-нить массив, вызывается ф-ция getMemoryMap, которую перехватывает наш PinTool. Для примера один из регистров отрепамился на адрес 0х123, его будем отслеживать
    3. PinTool клиент сохраняет значения отремапленных адресов
    4. Передает управление обратно нашей программе
    5. Дальше где-то происходит запись по нашему отслеживаемому адресу 0x123. Ф-ция storeReg2Addr отслеживает это
    6. И передает запрос на запись в OpenOCD клиент
    7. Client возвращает ответ, тот парсится. Если все нормально, то возращается управление программе
    8. Дальше где-то в программе происходит чтение по отслеживаемому адресу 0x123.
    9. loadAddr2Reg отслеживает это и посылает запрос OpenOCD клиенту.
    10. OpenOCD клиент обрабатывает его и возвращает ответ
    11. Если все нормально, но в программу возвращается значение из регистра МК
    12. Программа продолжается.

    На этом пока все, полные исходники и примеры будут в следующих частях.

    Поделиться публикацией

    Похожие публикации

    Комментарии 18
      0
      Ох какая классная наркомания! Круто!

      По-моему, более «традиционный» подход к такому (который исповедуют всякие микропитоны и эспруино) — это интерпретатор на самом проце, с написанием кода в терминале. Не берусь судить, какой подход проще или лучше.
        +1
        Можно и так, но тут считай сравнивать теплое с мягким.
        Потому как предназначения разные. Виртуальные машины / интерпретаторы на МК, могут исполнять скрипты автономно и вполне могут использоваться в продакшене. Но там мы упираемся в ограничение ресурсов МК, необходимость портирования вирутальной машины / интерпретатора, а так же еще написание библиотек переферии.
        В моем подходе программы/скрипты не могут исполняться автономно, но нам доступны все ресурсы нашего хостового ПК, все виды библиотек и все виды языков программирования. При этом при использовании С/С++ можно использовать стандартные библиотеки от производителя для работы с переферией.
          +1
          … а также в надёжность, производительность, объём и соответствие этих библиотек/интерпретаторов условиям задачи (лицензия, железо)
        0
        А есть инструменты, в которых можно сразу такой workflow рисовать и сразу в C компилировать?
          0
          Вот именно такой нет, для чего-нибудь попроще есть xod.io
          +7
          Абсолютно противоположное впечатление: «После того как поработаешь в большом программировании С++, Java, Python, и т. Д с микроконтроллерами, возвращаться к маленьким и пузатым микроконтроллерам к большому программированию С++, Java, Python совсем не хочется. К их скудным монстроузным инструментам и библиотекам. Но делать иногда нечего, задачи real-time и автономности UI, хранения и обработки данных, не оставляют выбора.»
            +4
            Подозреваю, что просто усталость от того, чем чаще занимаешься.
            Я лично подустал от невозможности использовать динамическую память, от медленных компиляторов, от кривой поддержки хотя бы С++11, от безумных аппаратных косяков в процах и ошибок монтажа…

            Все мы страдаем от несовершенства мира в своем углу.
              0
              Скорее от недостатка понимания и/или отсутствия мотивации (неинтересно). А за статью спасибо, интересно, и, главное, применимо
            +1
            Форт. мне кажется, для микроконтроллеров с достаточным объемом ОЗУ (от 8Кб, скажем) и достаточным объемом FLASH (от 64Кб, скажем) форт будет хорошим решением для того, что бы сделать хоть прототип, хоть готовое приложение ))
            А еще можно делать кровавый патчинг через терминал, тоже очень мило ))

            Давеча попробовал mecrisp.sourceforge.net и остался доволен результатами. Как раз надо было сопрячь железку со странным протоколом с МК. Получилось хорошо и быстро.
            Прототип сделал. Теперь думаю, что может его не перекладывать на Си, а продолжать писать на форте.

            В этой, конкретной, реализации очень удобно разрабатывать тем, что можно слова создавать в ОЗУ, а потом пересоздавать их во FLASH. А если есть возможность подключить SD, то набросав редактор можно полностью вести процесс разработки на МК.
              0
              Очень бы хотелось развернутую статью об этом опыте ( mecrisp )
              +1

              А почему просто не воспользоваться возможностями которые идут из коробки?
              С кросс-компилятором идет gdb, openocd умеет работать как gdb сервер,
              подключаемся gdb к openocd и вот у нас repl, gdb умеет и скрипты запускать,
              и интерпретироваться подмножество языка С, типа *(int *)0x4 = 5;,
              и можно даже вызывать отдельные функции из загруженного кода в память МК.
              Ну и скрипты на python он поддерживает.

                0
                Хорошее замечание. Тоже об этом думал. Но давайе по порядку…
                У GDB нет REPL, это скорее консоль для исполнения уже встроенных команд и откомпилированных команд программы. Может интерпретровать простые констуркции С но и только.
                Проблематично вызывать ф-ции из программы, а допустим иницилизировать структуры и классы и передавать их в аргументы это будет жуткая боль.
                Скрипты и расширения на python отчасти спасают, но всеравно читаемость и масштабируемость будет хромать.
                Во второй части будет наглядный пример, как все довольно просто работать будет. В этой статье мы всего лишь готовим инструмент для этого.
                +1
                BSDL? Не, не слышал!
                  0
                  Я тоже не слышал. Может раскроете свою мысль?
                    0
                    Boundary Scan Description Language — с помощью такого файла-описания чипа и JTAG Boundary Scan (который как раз был в первую очередь придуман с целью упрощения тестирования плат с большим количеством взаимосвязанных чипов, а не для отладки/прошивки чипов, как многие думают), можно дрыгнуть любой ногой любого чипа, «сидящего на JTAG-chain, считать состояние любого пина и т.д., и все это без знания регистров и внутреннего устройства чипа (т.к. JTAG Boundary Scan-функционал реализован независимо от остальных частей кристалла, включая CPU). Минусом технологии является низкая скорость, т.к. данные последовательно загоняются с первый чип, потом во второй, третий и т.д., и потом возвращаются обратно в JTAG-железку, через которую идет управление.
                      0
                      Могу ошибаться, но еще наверно стоит к минусам отнести:
                      то что у микросхемы должен быть обязательно JTAG интерфейс — на той тестируемой плате это не прокатывало, так как там была единственная stm32 и у нее был выведен SWD только
                      стоимость такого решения — было бы интересно узнать у людей кто в теме
                      Ну и BSDL решает только вопрос тестирования соединений микросхем ( на сколько я понял )
                      Допустим более сложные тесты уже не провернуть — к примеру проверить работу трансформатора на определенной частоте, корректность работы микросхем (на случай их брака)
                  +5
                  По-моему автор просто перестал ценить ту изюминку и детерминированность встраиваемых систем. Это целый мир. Бывает. Со временем само проходит.
                    –1
                    Все относительно. Сейчас могут набежать, те кто топит за функциональное программирование, кто за хайповые golang и Котлин и закидать вас монадами, коурутинами и прочими плюшками.
                    Под каждую задачу свой инструмент. И что-то громоздкое городить на бедном мк, не всегда рационально.
                    P.s. не холивара ради.

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

                  Самое читаемое