Как записать переменную во флеш память 80c51f120
Перейти к содержимому

Как записать переменную во флеш память 80c51f120

  • автор:

Как разместить переменную по определенному адресу в Keil

Изредка возникает задача сохранить во 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

Первый путь — ручной:
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) >> 

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

Но я для примера выделю только 0 сектор (он начинается с 0x08000000 ) размером 16 кБ (0x00004000 — это 16*1024 байт в шестнадцатеричной системе).

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»))) = ;

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

Свойства проекта 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(), чтобы она и зажигала и тушила. О своих (не) успехах пишите в лс или комментариях, может быть получится ещё одна заметка.

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

Тема: запись переменной в флеш память плк

alexval2006 вне форума

По умолчаниюзапись переменной в флеш память плк

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

04.03.2012, 12:04 #2

nalnik вне форума

Пользователь Регистрация 17.12.2007 Сообщений 108

По умолчанию

Плк 150 запись и чтение.
Создается файл с именем переменной.
Постоянно перезаписывать файл нельзя— упорите флеш.

зап:
dwHandle := SysFileOpen(‘Pr_XX.t’,’

Последний раз редактировалось nalnik; 15.07.2015 в 12:22 .

04.03.2012, 12:12 #3

alexval2006 вне форума

Пользователь Регистрация 23.01.2008 Адрес Белгородская область, Валуйки Сообщений 274

По умолчанию

спасибо попробую

06.03.2012, 12:47 #4

Yegor вне форума

Пользователь Регистрация 13.10.2011 Адрес Златоуст Сообщений 1,021

По умолчанию

См. в справке RETAIN.

13.12.2012, 15:12 #5

mkhm вне форума

Пользователь Регистрация 08.12.2012 Сообщений 26

По умолчанию

ЦитатаСообщение от nalnik Посмотреть сообщение

Плк 150 запись и чтение.
Создается файл с именем переменной.
Постоянно перезаписывать файл нельзя— упорите флеш.

зап:
dwHandle := SysFileOpen(‘Pr_XX.t’,’w’);
SysFileWrite(dwHandle,ADR(Pr_XX),SIZEOF(Pr_XX));
SysFileClose(dwHandle);

Чтение:
dwHandle := SysFileOpen(‘Pr_XX.t’,’r’);
SysFileRead(dwHandle,ADR(Pr_XXf),SIZEOF(Pr_XXf)-1);
SysFileClose(dwHandle);

Работает.
Pr_XX — имя переменной
SysLibFile.lib — добавь библиотеку.

SysFileRead(dwHandle,ADR(Pr_XXf),SIZEOF(Pr_XXf)); — минус один, видимо, по ошибке

13.12.2012, 21:13 #6

nalnik вне форума

Пользователь Регистрация 17.12.2007 Сообщений 108

По умолчанию

нет это не ошибка!

13.12.2012, 21:47 #7

mkhm вне форума

Пользователь Регистрация 08.12.2012 Сообщений 26

По умолчанию

ЦитатаСообщение от nalnik Посмотреть сообщение

нет это не ошибка!

Сегодня проверял на ПЛК 100. Без -1 — работает, написал список переменных в файл. С -1 например UInt,Word работает до 255, так как из двух записанных байт восстанавливает один (из 4 -3, из 8 -7). Есть варианты когда можно проигнорировать целый байт?

19.12.2012, 23:39 #8

S.A.D. вне форума

Пользователь Регистрация 28.10.2010 Сообщений 277

По умолчанию

объясните тупому, что здесь делает переменная dwHandle? Какая-то индикация то файл открылся? Тогда зачем её пихать в функцию записи?
И еще, может кто подскажет как единоразово при включении ПЛК считать значение переменной из файла? откуда взять индикацию того что ПЛК включился после пропадания питания? Мне нужно примерно раз в 12 часов писать значение наработки двигателей во флеш и в случае пропадания питания или перезаливки проекта\сброса плк (когда RETAIN переменные слетают или могут слететь) восстанавливать это значение в программе.

Типы данных, переменные

Переменная – это ячейка в оперативной памяти микроконтроллера, которая имеет своё уникальное название (а также адрес в памяти) и хранит значение соответственно своему размеру. К переменной мы можем обратиться по её имени или адресу и получить это значение, либо изменить его. Зачем это нужно? В переменной могут храниться п ромежуточные результаты вычислений, полученные “снаружи” данные (с датчиков, Интернета, интерфейсов связи) и так далее.

Измерение информации

Прежде чем перейти к переменным и их типам, нужно вспомнить школьный курс информатики, а именно – как хранятся данные в “цифровом” мире. Любая память состоит из элементарных ячеек, которые имеют всего два состояния: 0 и 1. Эта единица информации называется бит (bit). Минимальным блоком памяти, к которому можно обратиться из программы по имени или адресу, является байт (byte), который в Arduino (и в большинстве других платформ и процессоров) состоит из 8 бит, таким образом любой тип данных будет кратен 1 байту.

Максимальное количество значений, которое можно записать в один байт, составляет 2^8 = 256. В программировании счёт всегда начинается с нуля, поэтому один байт может хранить число от 0 до 255. Более подробно о двоичном представлении информации и битовых операциях мы поговорим в отдельном уроке.

Стандартные типы переменных в Arduino по своему размеру кратны степени двойки, давайте их распишем:

  • 1 байт = 8 бит = 256
  • 2 байта = 16 бит = 65 536
  • 4 байта = 32 бита = 4 294 967 296

Типы данных

Переменные разных типов имеют разные особенности и позволяют хранить числа в разных диапазонах.

Название Альт. название Вес Диапазон Особенность
boolean bool 1 байт * 0 или 1, true или false Логический тип
char 1 байт -128… 127 (AVR), 0.. 255 (esp) Символ (код символа) из таблицы ASCII
int8_t 1 байт -128… 127 Целые числа
byte uint8_t 1 байт 0… 255 Целые числа
int ** int16_t , short 2 байта -32 768… 32 767 Целые числа. На ESP8266/ESP32 – 4 байта! См. ниже
unsigned int ** uint16_t , word 2 байта 0… 65 535 Целые числа. На ESP8266/ESP32 – 4 байта! См. ниже
long int32_t 4 байта -2 147 483 648… 2 147 483 647 Целые числа
unsigned long uint32_t 4 байта 0… 4 294 967 295 Целые числа
float 4 байта -3.4E+38… 3.4E+38 Числа с плавающей точкой, точность: 6-7 знаков
double 4/8 байт -1.7E+308.. 1.7E+308 Для AVR то же самое, что float .
  • (*) – да, bool занимает 1 байт (8 бит), так как это минимальная адресуемая ячейка памяти. Есть способы запаковать логические переменные в 1 бит, о них поговорим в другом уроке.
  • (**) – на ESP8266/ESP32 int и unsigned int занимает 4 байта, то есть является аналогами типов long и unsigned long !
  • (***) – Компилятор также поддерживает 64 битные числа. Стандартные Arduino-библиотеки с переменными этого типа не работают, поэтому можно использовать только в своём коде.

Целочисленные типы

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

  • Проще ориентироваться в максимальных значениях
  • Легче запомнить
  • Название более короткое
  • Проще изменить один тип на другой
  • Размер переменной задан жёстко и не зависит от платформы (например int на AVR это 2 байта, а на esp8266 – 4 байта)

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

  • UINT8_MAX – 255
  • INT8_MAX – 127
  • UINT16_MAX – 65 535
  • INT16_MAX – 32 767
  • UINT32_MAX – 4 294 967 295
  • INT32_MAX – 2 147 483 647
  • UINT64_MAX – 18 446 744 073 709 551 615
  • INT64_MAX – ‭9 223 372 036 854 775 807

Логический тип

bool – логический, он же булевый (придуман Джорджем Булем) тип данных, принимает значения 0 и 1 или false и true – ложь и правда. Используется для хранения состояний, например включено/выключено, а также для работы в условных конструкциях.

Также переменная типа bool принимает значение true , если присвоить ей любое отличное от нуля число.

bool a = 0; // false bool b = 1; // true bool c = 25; // true

Символьный тип

char – тип данных для хранения символов, символ указывается в одинарных кавычках: char var = ‘a’; . По факту это целочисленный тип данных, а переменная хранит номер (код) символа в таблице ASCII:

blank

Отдельный символьный тип данных нужен для удобства работы, чтобы программа могла понять разницу между числом и символом, например для вывода на дисплей (чтобы вывести именно букву A, а не число 65). Из символов можно составлять строки, об этом более подробно поговорим в уроках про символьные строки и String-строки.

Символы и числа

Несмотря на то, что в языке Си символ это по сути целое число, значения например ‘3’ и 3 не равны между собой, потому что символ ‘3’ с точки зрения программы является числом 51 . На практике иногда бывает нужно конвертировать символы чисел в соответствующие им целые числа и наоборот (при работе со строками и буферами вручную), для этого распространены следующие алгоритмы:

  • Из символа в число – взять младший ниббл (4 бита): symbol & 0xF
  • Из символа в число – вычесть символ 0: symbol — ‘0’
  • Из числа в символ – прибавить символ 0: symbol + ‘0’

Дробные числа

float (англ. float – плавающий) – тип данных для чисел с плавающей точкой, т.е. десятичных дробей. Arduino поддерживает три типа ввода чисел с плавающей точкой:

Тип записи Пример Чему равно
Десятичная дробь 20.5 20.5
Научный 2.34E5 2.34*10^5 или 234000
Инженерный 67e-12 67*10^-12 или 0.000000000067

Выше в таблице есть пометка “точность: 6-7 знаков” – это означает, что в этом типе можно хранить числа, размер которых не больше 6-7 цифр, остальные цифры будут утеряны! Причём целой части отдаётся приоритет. Вот так это выглядит в числах (в комментарии – реальное число, которое записалось в переменную):

float v; v = 123456.654321; // 123456.656250 v = 0.0123456789; // 0.0123456788 v = 0.0000123456789; // 0.0000123456788 v = 123456789; // 123456792.0

Другие особенности float чисел и работу с ними мы рассмотрим в уроках про математические операции и условия.

Объявление и инициализация

  • Объявление переменной – резервирование ячейки памяти указанного типа на имя: тип_данных имя;
  • Присваивание – задание переменной значения при помощи оператора = (равно): имя = значение;
  • Инициализация переменной – объявление и присваивание начального значения: тип_данных имя = значение;

Можно объявить и инициализировать несколько переменных через запятую:

byte myVal; int sensorRead = 10; byte val1, val2, val3 = 10;
  • Переменная должна быть объявлена до использования, буквально выше по коду. Иначе вы получите ошибку Not declared in this scope – переменная не объявлена.
  • Нельзя объявить две и более переменных с одинаковым именем в одной области определения.

Константы

Что такое константа понятно из её названия – что-то, значение чего мы можем только прочитать и не можем изменить: при попытке изменить получим ошибку компиляции. Задать константу можно двумя способами:

Как переменную, указав перед типом данных слово const: const тип_данных имя = значение; . Пример: const byte myConst = 10; . По сути это будет обычная переменная, но её значение нельзя поменять. Особенности:

  • Занимает место в оперативной памяти, но может быть оптимизирована (вырезана) компилятором, если используется просто как значение.
  • Имеет адрес в памяти, по которому к ней можно обратиться.
  • Вычисления с ней не оптимизируются и чаще всего выполняются точно так же, как с обычными переменными.
  • Компилятор выдаст ошибку, если имя константы совпадает с именем другой переменной в программе.

При помощи директивы #define, без знака равенства и точки с запятой в конце: #define имя значение . Пример: #define BTN_PIN 10 . Работает так: указанное имя буквально заменяется в тексте программы на указанное значение. Такая дефайн-константа:

  • Не занимает места в оперативной памяти, а хранится во Flash памяти как часть кода программы.
  • Не имеет адреса в оперативной памяти.
  • Вычисления с такими константами оптимизируются и выполняются быстрее, так как это просто цифры.
  • Если имя дефайн-константы совпадёт с именем другого “объекта” в программе или даже в библиотеке – работа может быть непредсказуемой: можно получить невнятную ошибку компиляции, либо программа может просто работать некорректно! Дефайн буквально заменяет текст в коде программы, это довольно опасная штука.

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

Область видимости

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

Глобальная

  • Объявляется вне функций, например просто в начале программы.
  • Доступна для чтения и записи в любом месте программы.
  • Находится в оперативной памяти на всём протяжении работы программы, то есть не теряет своё значение.
  • При объявлении имеет нулевое значение.

byte var; // глобальная переменная void setup() < var = 50; >void loop()

Локальная

  • Объявляется внутри любого блока кода, заключённого в < фигурные скобки >.
  • Доступна для чтения и записи только внутри своего блока кода (и во всех вложенных в него).
  • Находится в оперативной памяти с момента объявления и до закрывающей фигурной скобки, то есть удаляется из памяти и её значение стирается.
  • При объявлении имеет случайное значение.

Важный момент: если имя локальной переменной совпадает с одной из глобальных, то приоритет обращения отдаётся локальной переменной (в её области определения).

byte var; // глобальная переменная void setup() < byte var; // локальная переменная var = 50; // меняем локальную var >void loop() < var = 70; // меняем глобальную var >

Статические переменные

Вспомним, как работает обычная локальная переменная: при входе в свой блок кода локальная переменная создаётся заново, а при выходе – удаляется из памяти и теряет своё значение. Если локальная переменная объявлена как static – она будет сохранять своё значение на всём протяжении работы программы, но область видимости останется локальной: взаимодействовать с переменной можно будет только внутри блока кода, где она создана (и во всех вложенных в него).

void setup() < >void loop() < byte varL = 0; varL++; static byte varS = 0; varS++; // здесь varL всегда будет равна 1 // а varS - постоянно увеличиваться >

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

Преобразование типов

Иногда требуется преобразовать один тип данных в другой: например, функция принимает int , а вы хотите передать ей byte . В большинстве случаев компилятор сам разберётся и преобразует byte в int , но иногда вылетает ошибка в стиле “попытка передать byte туда, где ждут int“. В таком случае можно преобразовать тип данных, для этого достаточно указать нужный тип данных в скобках перед преобразуемой переменной (тип_данных)переменная , иногда можно встретить запись тип_данных(переменная) . Результат вернёт переменную с новым типом данных, сам же тип данной у переменной не изменится. Например:

// переменная типа byte byte val = 10; // передаём какой-то функции, которая ожидает int sendVal( (int)val );

И всё! val будет обрабатываться как int , а не как byte .

Видео

Полезные страницы

  • Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
  • Поддержать автора за работу над уроками
  • Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *