Изредка возникает задача сохранить во flash памяти контрольную сумму, картинку, строчку текста, настройку. Иногда возникает задача сохранить не просто в ОЗУ, а в определенной области, чтобы для этой области например включить/выключить DCACHE. Или например иметь функцию, исполняемую из ОЗУ чтобы можно было присылать по UART и сразу исполнять новый код функции.

Рассмотрим задачу на примерах. В качестве испытуемого будет народный stm32f401ret6 со следующей адресацией flash памяти (страница 51 даташита):

#define ADDR_FLASH_SECTOR_0 ((uint32_t)0x08000000) //Sector 0, 16 Kbytes
#define ADDR_FLASH_SECTOR_1 ((uint32_t)0x08004000) //Sector 1, 16 Kbytes
#define ADDR_FLASH_SECTOR_2 ((uint32_t)0x08008000) //Sector 2, 16 Kbytes
#define ADDR_FLASH_SECTOR_3 ((uint32_t)0x0800C000) //Sector 3, 16 Kbytes
#define ADDR_FLASH_SECTOR_4 ((uint32_t)0x08010000) //Sector 4, 64 Kbytes
#define ADDR_FLASH_SECTOR_5 ((uint32_t)0x08020000) //Sector 5, 128 Kbytes


В Keil это можно сделать разными путями. Нулевой путь - через GUI с помощью редактирования опций проекта - весьма ограниченный, не будем рассматривать.

Первый путь - ручной:
uint32_t keyFlash __attribute__((at(0x08004000))) = 0xAABBCCDD;
Здесь мы записали 32-битное число 0xAABBCCDD по адресу 0x08004000. Этот путь имеет следующий недостаток. Пусть мы точно разместили число в flash по адресу 0x08004000, рядом могут располагаться код программы, значения для констант. Если мы захотим перезаписать число по адресу, придётся стирать весь сектор, потому что во flash писать можно только посекторно. Я даже не знаю что будет, если выполняемые в данном секторе flash памяти инструкции попытаться стереть, но это очевидно плохая идея. Так что если предполагается возможность изменения данных во flash в процессе работы, под эти данные следует выделить отдельный сектор (сектора). И это нас приводит к второму пути.

Второй путь заключается в использовании скеттер (scatter) файла.Теорию можно прочесть здесь. Также желательно понимать что такое объектный файл. Совсем в двух словах, после работы препроцессор->компилятор получается множество
файлов *.o
где звездочка значит любое имя. Например, из main.c получается файл main.o

Рассмотрим пример scatter файла.

В дефолтный scatter файла, который генерирует сам Keil.
; *************************************************************
;Scatter-Loading Description File generated by uVision stm32f401ret6
; *************************************************************

LR_IROM1 0x08000000 0x00080000  {    ; load region size_region
  ER_IROM1 0x08000000 0x00080000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
  }
  RW_IRAM1 0x20000000 0x00018000  {  ; RW data
   .ANY (+RW +ZI)
  }
}

Были добавлены две правки. Первое, исходный файл выделял под программу всю флеш:

ER_IROM1 0x08000000 0x00080000 { ; load address = execution address

Но я для примера выделю только 0 сектор размером 16 кБ.

ER_IROM1 0x08000000 0x00004000{ ; Sector 0: 16kB

где 0x08000000 - начальный адрес сектора, 0x00004000 - количество байт в секторе. По аналогии можно выделить два сектора, и не обязательно 0 и 1, а например 4 и 5 или при некоторых ухищрения даже например 0 и 5. (Но в stm32f4 таблица векторов прерываний должна располагаться в 0 секторе, так что в моём случае 0 сектор точно придётся выделить)

Вторая правка, в дефолтный scatter файл был добавлен execution region, который я решил назвать MYREGION. В этом регионе есть секция mysection, которая ищется во всех объектных файлах. Если линкер найдёт синтаксическую единицу (читай переменную) из этой секции, то эта синтаксическая единица попадёт в регион MYREGION.
LR_IROM1 0x08000000 0x08001000 { ; load region size_region
ER_IROM1 0x08000000
0x00004000{ ; Sector 0: 16kB
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}

RW_IRAM1 0x20000000 0x00000900 { ; RW data
.ANY (+RW +ZI)
}

MYREGION 0x0800C000 FIXED {
*.o (mysection)
}
}

А теперь собственно в файле main.c заведем нашу синтаксическую единицу для хранения в регионе:
const uint16_t ADC_Buf[7] __attribute__((section("mysection"))) = {0xDDDD, 0xDDDD, 0xDDDD, 0xDDDD, 0xDDDD, 0xDDDD, 0xDDDD};

И тут нас ждёт ловушка. Язык высокого уровня мы любим за оптимизацию. Включаем -O3 и наша неиспользуемая в программе константа исчезает. Тут случай, когда про toolchain можно сказать "слишком умный". Чтобы объяснить, что в секции mysection у нас всё только нужное, зайдём на вкладку Linker в Misc controls и пропишем
--keep=''main.o(mysection)''

Свойства проекта Keil
Свойства проекта Keil

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


MYREGION 0x0800C000 FIXED {
*.o (mysection)
}

пишите
MYREGION 0x0800C000 FIXED {
main.o (mysection)
}

А ещё можно например все переменные из файла пусть main.c заставить жить в вашей секции вот так


MYREGION 0x0800C000 FIXED {
main.o(+RW +ZI)
}

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

Исходный код.

Кому оказалась интересна тема scatter файлов, предлагаю упражнение. Наверняка вы писали программу моргания светодиодом. Я перелагаю вам написать одну программу, которая работает верно: зажигает и тушит светодиод, А вторая программа будет зажигать и зажигать светодиод:

void blink(){
  while(1){
    on();
    delay();
    on();
    delay();
  }
}

После этого я предлагаю вам посмотреть содержимое памяти и глазами найти отличие. Далее blink() с помощью scatter файла поместите в ОЗУ, пусть оттуда выполняется. Затем перед вызовом функции blink() в main() попробуйте написать код, который починит blink(), чтобы она и зажигала и тушила. О своих (не) успехах пишите в лс или комментариях, может быть получится ещё одна заметка.

Также может быть полезно
https://radiohlam.ru/stm32_1/

Дополнительные материалы:
Как не инициализировать переменные в кейл?
Про слово FIXED

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


  1. IgorPie
    28.08.2022 04:07
    -4

    Только для keil господ. Gcc нищеброды будут по прежнему пользоваться #define


    1. AKudinov
      28.08.2022 06:40
      +17

      Нее, "Gcc-нищеброды" задействуют linker scripts.


  1. catBasilio
    28.08.2022 11:44
    +4

    Блин, целую статью написали о том как переменную записать. Куда катится хабр?

    volatile int *some_peirph = (uintptr_t)0x1234566;
    

    всегда пользовался таким. И оно работает всюду. так как использует только стандартные (от слова standard) конструкции.


    1. hedger
      28.08.2022 12:32
      +5

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


      1. catBasilio
        28.08.2022 13:04

        Все бы ничего, но как подход gcc автор приводит код

        #define ADDR_FLASH_SECTOR_0 ((uint32_t)0x08000000)
        

        А это именно то, что я с Volatile написал

        Если бы было

        #define FLASH_SRCTOR_0 (*(Volatile int *)0x123456)
        

        То код бы был равнозначный


        1. VelocidadAbsurda
          28.08.2022 19:46
          +5

          Ваш код с volatile указателем решает другую задачу - пишет значение по адресу во время выполнения, что отлично подходит для записи в регистры периферии, но не во flash. Тогда как пример автора размещает значение по адресу на этапе компоновки, оно оказывается там ещё в прошивочном файле и зашивается программатором (для чего? к примеру, это может быть маркер наличия "основной" части прошивки для бутлоадера)

          Упоминаний GCC в статье вообще нет, а данные #define - просто константы адресов секторов. Кода, использующего их, в статье также нет.


          1. catBasilio
            28.08.2022 19:50
            -1

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


          1. catBasilio
            29.08.2022 08:21

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

            uint32_t keyFlash __attribute__((at(0x08004000))) ;
            ...
            {
            KeyFlash =42;
            }
            

            Кейл будет какой-то инструментированный код делать чтобы во флеше перезаписать данные?


            1. zloe_morkoffko
              29.08.2022 15:10

              Не будет


            1. VelocidadAbsurda
              29.08.2022 16:34

              Обратите внимание: в коде автора нет отдельного присваивания позже, у него — сразу в объявлении статическая инициализация, из неё никакого кода не генерируется вообще, просто в выходном elf появится секция с соответствующими адресом и содержимым, которая прошьётся во flash.
              Ваш же код с отдельным присваиванием даст тот же результат, что и запись по указателю — сгенерируется код для записи значения по адресу уже во время исполнения (и точно так же не сработает).
              То, что у автора там отсутствует const, хоть и не очень красиво, но результат в данном случае не изменит. Разница будет в типе секции (RW, а не RO) и в отсутствии дальнейших проверок компилятором (этой переменной дальше можно будет безрезультатно присваивать новые значения и компилятор не будет ругаться).


    1. DungeonLords Автор
      28.08.2022 12:50
      +1

      @catBasilio

      main.c(14): warning:  #144-D: a value of type "uintptr_t" cannot be used to initialize an entity of type "volatile int *"
      main.c(14): warning: #144-D: a value of type "uintptr_t" cannot be used to initialize an entity of type "volatile int *"


      1. catBasilio
        28.08.2022 12:55

        А давайте я целую статью напишу о том как прикастить целое число к указателю?

        Я без компилятора строку писал. По памяти. Я думал, что общая идея понятна. А оказывается вот оно как.


        1. nixtonixto
          28.08.2022 13:13

          А теперь перенесите эту переменную, например, на последнюю страницу Flash-памяти контроллера, чтобы при обновлении прошивки она не затиралась (в загрузчике программно защищаем эту последнюю страницу от записи или стирания).


          1. catBasilio
            28.08.2022 14:56
            -1

            я не понимаю проблемы. Адрес можно написать любой. хоть на последнюю страницу, хоть на первую, хоть на срединную.
            и при обновлении прошивки ничего затираться не будет так как это просто адрес. Если нужно указать, что память RO - то используйте const. Точно также const будет работать и для всяких экстеншинов arm cc.
            volatile int * const some_perf - если адрес не нужно менять. и нужно чтоб он фиксированный был.
            const volatile int *some_perf - ecли это память RO и нужно только читать
            const volatile int *const some_perif - если все сразу.


        1. DungeonLords Автор
          28.08.2022 13:21

          @catBasilio

          уж будьте добры - напишите и скомпилируйте. Вообще не понимаю что вы предлагаете. Так что-ли?

          volatile uint32_t p = (uint32_t)0x0800C000;

          int main(void){
          *p= 0xAAAAAAAA;
          }


          1. catBasilio
            28.08.2022 15:23
            -1

            volatile uint32_t p = (uint32_t)0x0800C000;
            int main(void){
            *p= 0xAAAAAAAA;
            }

            вот я непонимаю, вы сейчас серьезно, или пытаетесь меня троллить?
            тот код, который вы написали выше в комменте - это даже не указатель.

            То что на скриншоте - уже правильный код, и он должен работать.
            Почему он не работает - это отдельный вопрос. Навскидку могу предположить:

            • Keil криво трэкает изменение содержимое памяти

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

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

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


            1. DungeonLords Автор
              28.08.2022 15:31
              +1

              "Почему он не работает - это отдельный вопрос."

              Нет, это не отдельный вопрос, а именно тема статьи. Прочтите пожалуйста ещё раз статью, пошагово повторите описанные в ней действия на реальном железе (хотя возможно Proteus тоже подойдёт), также можно попробовать взять железо в аренду. Вам предстоит разобраться и открыть для себя много нового. Можете также поробоватьть сделать упражнение. Спасибо за участие в обсуждении.


              1. catBasilio
                28.08.2022 15:49
                -5

                у меня нет железа, у меня нет kail. и я не хочу тратить время на выяснение, почему у ВАС оно не работает.

                Нет, это не отдельный вопрос, а именно тема статьи

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

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


                1. thevlad
                  28.08.2022 21:39
                  +2

                  Так очевидно почему он не работает, потому что адрес указывает на read-only flash(ROM), который хотя и отображается в адресное пространство изменить нельзя. Работало бы если это был адрес в RAM.


                  1. catBasilio
                    29.08.2022 08:05
                    -1

                    Тогда почему эксепшин не кидается?


                    1. thevlad
                      29.08.2022 10:38
                      +1

                      Наверняка потому, что это простенький микроконтроллер, без MMU и защиты памяти.


    1. fiego
      29.08.2022 18:16

      Этот код - не переменная по адресу, это указатель на адрес. Для переменной подошёл бы С++ синтакс:
      volatile uintmax_t &some_periph = *(uintptr_t*)0x1234566