Знаете ли вы куда попадает ваша программа после того, как вы нажали кнопку RUN или DEBUG в IDE? Если да, то как изменить этот адрес или даже выйти за пределы постоянной памяти и прожигаться сразу в оперативную память.

Я работаю в CubeIDE и весь мой интерфейс будет именно из этой среды разработки.

Начнем с ответа на первый вопрос, если использовать стандартные настройки IDE и взять стандартный ST-LINK (не важно оригинал или свисток), то при прожигании микроконтроллера будет очищено нужное количество секторов во FLASH памяти и на их место будет записана программа, причем в самое начало FLASH памяти по адресу 0x08000000.

Тут немного остановимся. Для большинства проектов этого достаточно, программа находится во FLASH памяти, при перезагрузке микроконтроллера ни куда не исчезает и исправно запускается. Проблемы начинаются, когда приходит задача, встроить загрузчик, который будет самостоятельно принимать прошивку по одному из интерфейсов, будь то UART/USART, I2C или CAN и сохранять ее в требуемом месте FLASH памяти, а после перезагрузки должен самостоятельно подготовить и передать управление требуемой программе.

Тут уже не обойтись без небольшой настройки линкер-скрипта и кода самой программы.

В CubeIDE в корне вашего проекта вы обязательно найдете файл с расширением .ld, это и есть линкер скрипт в котором можно найти описание начальных адресов и размеров разных секторов памяти (пример STM32F429ZI):

MEMORY
{
  CCRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
  RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 192K
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 2048K
}

Тут мы видим, что наш чип имеет две области энергозависимой памяти: CCRAM и RAM размерами 64 и 192 кбайта соответственно и одну область энергонезависимой памяти - FLASH размером аж 2 Мбайта. CCRAM память начинается с адреса 0x10000000 и с учетом размера заканчивается по адресу 0x10010000, а RAM память занимает адресное пространство с 0x20000000 по 0x20030000.

На FLASH памяти остановлюсь чуть подробнее. В отличие от RAM, она не позволяет перезаписывать какие угодно биты тысячи раз в секунду. Во-первых, ресурс перезаписи достаточно быстро закончится, во вторых для того что бы изменить всего один бит с 0 на 1 во FLASH памяти, придется "затереть" целую страницу (page), размер которой (на моем опыте) может достигать 128 кбайт. Поэтому при работе с FLASH памятью нужно всегда держать в уме в каком секторе я сейчас работаю и нет ли в нем чего-то еще важного. Карту вашей FLASH памяти со всеми секторами и их размерами вы можете найти в Reference manual разделе посвященному встроенной памяти (например "Embedded flash memory interface").

Раздел FLASH в линкер скрипте как раз и определяет, куда будет загружена программа, поэтому, если хотите изменить адрес, куда будет загружаться программа и что не мало важно, для какого адреса будет собрана таблица прерываний, то нужно изменить адрес начала FLASH памяти в линкер-скрипте. Например, так:

Теперь программа будет работать из середины FLASH памяти. ВАЖНО: не забывать изменять размер области, что бы не выйти за пределы адресного пространства.

MEMORY
{
  CCRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
  RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 192K
  FLASH (rx)  : ORIGIN = 0x08100000, LENGTH = 1024K
}

Так же можно запускать программу из RAM памяти, главное не допускать пересечения областей FLASH и RAM:

MEMORY
{
  CCRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
  RAM   (xrw) : ORIGIN = 0x20025800, LENGTH = 42K
  FLASH (rx)  : ORIGIN = 0x20000000, LENGTH = 150K
}

Загружаясь в оперативную память, не забывайте, что после любого reset-a ваша программа будет стерта и придется ее прожигать заново.

НО, что бы ваш код работал из нового определенного вами места в памяти, необходимо сделать последний штрих - записать новый адрес начала программы в структуру SCB самым первым шагом в функции main.

#define START_ADDR 0x20000000

int main(void)
{
    /* Пока не будет передан новый адрес таблицы векторов, 
       другие функции не должны вызываться */
    __disable_irq();
    SCB->VTOR = START_ADDR;
    __enable_irq();

    /* Остальная программа начинается тут */
  
    while(1)
    {
    
    }
}

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

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


  1. odobryabov
    24.07.2024 11:09
    +6

    А можно немного покрасивей абстрагироваться. В LD дописать:

    MEMORY
    {
      CCRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
      RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 192K
      FLASH (rx)  : ORIGIN = 0x08100000, LENGTH = 1024K
    }
    
    LD_FLASH_ORIGIN = ORIGIN(FLASH);

    И далее в main:

    extern char	LD_FLASH_ORIGIN;
    #define START_ADDR ((uint32_t)&LD_FLASH_ORIGIN)
    
    int main(void)
    {
        /* Пока не будет передан новый адрес таблицы векторов, 
           другие функции не должны вызываться */
        __disable_irq();
        SCB->VTOR = START_ADDR;
        __enable_irq();
    
        /* Остальная программа начинается тут */
      
        while(1)
        {
        
        }
    }


    1. VaTiKaNeTs Автор
      24.07.2024 11:09

      Да, вы правы, можно и так сделать.


  1. mlnw
    24.07.2024 11:09
    +2

    Загружаясь в оперативную память, не забывайте, что после любого reset-a ваша программа будет стерта и придется ее прожигать заново.

    Прожигать в оперативную память?


    1. Albert2009Zi
      24.07.2024 11:09
      +3

      Burn motherf#cker burn (c) :)


  1. Serge78rus
    24.07.2024 11:09
    +1

    Загружаясь в оперативную память, не забывайте, что после любого reset-a
    ваша программа будет стерта и придется ее прожигать заново.

    Применительно к ОЗУ термин "прожигать" звучит несколько зловеще...

    Упс... Меня опередили. Но значит не только меня это "торкнуло"


    1. aabzel
      24.07.2024 11:09

      Применительно к ОЗУ термин "прожигать" звучит несколько зловеще...


      Обычно программист микроконтроллеров работает в отделах, которые по советских называются: "отдел разработки радио электронной аппаратуры" (РЭА).

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

      О программировании они знать ничего не хотят из принципа, они из тех кто просто ненавидит программировать еще с института.

      Начальник-схемотехник даже программирование называет "программизмом", прошивание называет "прожиганием", а самих программистов-микроконтроллеров "софтописцами".


      1. Serge78rus
        24.07.2024 11:09

        ... прошивание называет "прожиганием" ...

        Вообще то "прошивание" еще более древний термин, чем "прожигание"

        "Прошивали" ПЗУ, когда оно выполнялось на ферритовых сердечниках, причем "прошивали" в буквальном смысле.

        "Прожигать" ПЗУ стали, когда появились микросхемы однократно программируемого ПЗУ с пережигаемыми перемычками, типа советской серии К556.


  1. back_side
    24.07.2024 11:09
    +5

    Правильнее выполнить перенос таблицы векторов прерываний в обработчике прерывания Reset, так как main не первая выполняемая функция.


    1. VaTiKaNeTs Автор
      24.07.2024 11:09

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


      1. juramehanik
        24.07.2024 11:09

        вы уверены? если вы генерируете кубом проекты то там всегда есть стартап файл ассемблерный где это все делается в функции Reset_Handler

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


  1. MaxPower432
    24.07.2024 11:09

    Даже после выполненных действий при загрузке открывается страница с ошибкой "Break at address "0x8000f40" with no debug information available, or outside of program code.", а в дизассемблере видно попытки чтения данных с адреса 0x08000000, откуда оно и берет переход по адресу 0x08000f40. Но программа загружается и работает.


    1. VaTiKaNeTs Автор
      24.07.2024 11:09

      Перед тем как загрузить программу по адресу 0x08000F40, попробуйте полностью очистить флешку.


      1. MaxPower432
        24.07.2024 11:09

        Вы не поняли. У меня загрузчик начинается с 0x08000000, а основная программа с 0x08010000. Так вот когда загружаю основную программу, она зачем то лезет в область загрузчика за адресами, берет оттуда не правильные адреса и пытается их выполнить, естественно при этом выплевывая ошибку. Такое ощущение что недостаточно указать адреса в файле .Id, надо IDE объяснить, что старт программы осуществлять тоже с других адресов.


        1. back_side
          24.07.2024 11:09
          +1

          Если у вас по адресу 0х08000000 уже есть программа она ничего не знает, о приложении которое лежит в более старших адресах и вы сами должны перейти в bootloader на стартовый адрес основной программы. Примерно так будет выглядеть код перехода на основную программу в bootloader.

          #define MAIN_PROGRAM_START_ADDRESS        (uint32_t)0x0800A000
          
          int main () {
            __disable_irq();
            NVIC_SetVectorTable(NVIC_VectTab_FLASH, MAIN_PROGRAM_START_ADDRESS);
            jumpAddress = *(__IO uint32_t*) (MAIN_PROGRAM_START_ADDRESS + 4);
            Jump_To_Application = (pFunction) jumpAddress;
            __set_MSP(*(__IO uint32_t*) MAIN_PROGRAM_START_ADDRESS);
            Jump_To_Application();
          } 
          


          1. MaxPower432
            24.07.2024 11:09

            Так и происходит, если прошивать МК готовыми бинарниками: сначала запускается бутлодер потом он передает управление основной программе. С этим проблем нет, все работает. Проблема начинается когда я пытаюсь в IDE отладить основную программу. При заливки программы в режиме Debug выходит ошибка, о которой я писал выше. Но программа заливается и стартует, правда со своими приколами. При попытке сделать reset, она опять берет не правильные адреса и крашится.


            1. back_side
              24.07.2024 11:09

              Ну в целом если вы вызываете Reset вы и должны попадать сначала в загрузчик, попробуйте его запускать в режиме отладки с залитым основным приложением и посмотрите откуда идет обращение.


              1. MaxPower432
                24.07.2024 11:09

                Так я хочу отладить основную программу, а не бутлодер.И по моей логике, если я запустил отладку основного приложения и указал в .Id что оно у меня начинается с адреса 0x08010000, то после ресета через IDE я должен попасть на дрес 0x08010000, а не на 0x08000000. И при старте отладки я должен попасть на адрес со смещением, а не на дефолтный.


                1. back_side
                  24.07.2024 11:09

                  Я не уверен, что так можно сделать за счет этих настроек. В CubeIde не знаю, можно или нет, но в keil можно было в качестве костыля прикрепить *.ini файл c PC = 0x0800A000, и тогда отладка начиналась с нужного адреса.


                  1. wowa
                    24.07.2024 11:09

                    Предедущая System Workbench умела запускать програму на дебаг с правильного адреса. STM32 Cube IDE действительно без настройки неумеет. пишет что ненайден маин и открывает лис асемблера. Пока ненужно было - не разобрался как завести.


                1. RepoMan
                  24.07.2024 11:09

                  # Reconfigure vector table offset register to match the application location
                  set *0xe000ed08 = 0x8010000
                  
                  # Get the application stack pointer (First entry in the application vector table)
                  set $sp = *(unsigned int*)0x8010000
                  
                  # Get the application entry point (Second entry in the application vector table)
                  set $pc = *(unsigned int*)0x8010004

                  Наверное, имелось в виду это? Прописать в Startup Scripts


                1. sami777
                  24.07.2024 11:09

                  Сдается мне, то что вы указываете а линкере и то, с чего должна начинаться отладка с самим линкером слабо связаны. Ведь в линкере вы указываете то, с какого адреса будет заливаться флэш. А отладка у вас начинается с стандартного адреса начала флэш (0x08000000). В том же кейле, по-моему, есть возможность явно это указать.


          1. MikeSmith
            24.07.2024 11:09

            Осторожнее с таким кодом. Как было сказано в комментариях к этой статье, __set_MSP() - это зло. Если вдруг переменная Jump_To_Application хранится в стеке, то после set_MSP она превратится в тыкву. И это действительно так, я натыкался на эту проблему. Шаманство с отключением оптимизаций кода помогает, но это некрасиво и ненадёжно. Проверенное решение (GCC) такое:

            uint32_t topOfMainStack = *(uint32_t*)MAIN_PROGRAM_START_ADDRESS;
            __ASM volatile("msr msp, %0 \n bx %1" :: "r" (topOfMainStack), "r" (JumpAddress));

            Компилятор с любыми флагами оптимизациями предварительно загрузит значение в регистр %1, затем загрузит указатель стека, и затем перейдёт по адресу из регистра %1. Ассемблерная вставка не оптимизируется, т.е. в любом случае сначала выполнится команда msr, затем bx.
            Например, O0:

            ...
            801ed9c:	68fb      	ldr	r3, [r7, #12]
            801ed9e:	693a      	ldr	r2, [r7, #16]
            801eda0:	f383 8808 	msr	MSP, r3
            801eda4:	4710      	bx	r2

            Или Os+LTO:

            ...
            80004b8:	6823      	ldr	r3, [r4, #0]
            80004ba:	f383 8808 	msr	MSP, r3
            80004be:	4728      	bx	r5

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


        1. yappari
          24.07.2024 11:09

          Это не программа "лезет" в область загрузчика. Прочитайте как просходит старт мк на базе Arm CortexM. Если хотите, чтобы стартовало с 0x08010000, прошейте это значение в 0x08000000 (точнее не само значение 0x08010000, а то, что лежит по этому адресу. И по-хорошему надо скопировать и следующие 4 байта).

          Хотя если и это не помогает, возможно, что это такие кренделя самого КубИда.


        1. VaTiKaNeTs Автор
          24.07.2024 11:09

          Какой МК вы используете?


          1. MaxPower432
            24.07.2024 11:09

            F429zit6


  1. aabzel
    24.07.2024 11:09

    Прошивка из RAM памяти исполняется быстрее чем из Flash?


    1. VT100
      24.07.2024 11:09
      +2

      Конечно. Без ожидания готовности данных и т.п.


      1. mlnw
        24.07.2024 11:09
        +2

        Зависит от кода. В STM есть и prefetch, и кэширование, т.ч. выигрыш может быть и околонулевым.


    1. VaTiKaNeTs Автор
      24.07.2024 11:09

      Могу сказать точно, что "прожигание" в RAM происходит сильно быстрее, чем во FLASH, особенно заметно с большими прошивками