На этом уроке мы научимся сохранять данные во FLASH-память микроконтроллера.
На предыдущем уроке мы составили программу, которая переключает светодиод по нажатию кнопки. Но если сбросить питание платы – нажать кнопку RESET или переподключить разъем USB – то переключение светодиодов снова начнется со светодиода, расположенного на линии PE8.
Давайте попробуем сохранить тот светодиод, на котором мы закончили переключение светодиодов до отключения питания, и именно с него начать наш «бегущий огонь по кнопке».
Сначала откроем ST Visual Programmer и считаем содержимое памяти микроконтроллера. Подключаем отладочную плату и выбираем Read-> Active Sectors. Загрузился код программы, и мы видим, что начальная ячейка FLASH-памяти имеет шестнадцатеричный адрес 08000000. С этой ячейки начинается код программы. Каждая строчка содержит 16 ячеек.
Пролистаем вкладку вниз и увидим последний адрес FLASH-памяти, а также то, что в нижних секторах память программ чистая. Сюда вполне можно сохранить пользовательские данные.
Давайте сохраним данные в последнюю страницу памяти. Напомню, что для записи данных необходимо предварительно стереть страницу памяти, а стирание производится постранично.
Рассчитаем адрес начала последней страницы.
Размер каждой страницы памяти составляет 2 килобайта. Количество страниц – 128, поэтому до начала последней страницы – 127 страниц, что в шестнадцатеричном коде представляет собой 0x7F.
Смещение последней страницы в памяти можно определить, умножив количество страниц на размер страницы.
Смещение = 127*2048 = 260096 = 0x3F800
Для определения начального адреса последней страницы добавим это смещение к начальному адресу FLASH-памяти. Получим вот такое шестнадцатеричное значение:
0x08000000 + 0x3F800 = 0x0803F800
Давайте опишем переменную Address, теперь уже размером 32 бита и присвоим ей полученное значение.
Прежде чем изменять содержимое FLASH-памяти это действие необходимо разрешить. Для этого используется функция
Добавим ее перед бесконечным циклом.
Для того чтобы сохранить данные нужно сначала стереть страницу памяти. Открываем драйвер stm32f3xx_hal_flash_ex.c и ищем функцию, которая стирает страницу памяти. Это функция
В качестве аргументов ей передаются
- указатель на структуру FLASH_EraseInitTypeDef
- и указатель на переменную, в которую сохранится возможный некорректный адрес начала страницы, зафиксированный при стирании памяти.
Теперь разберемся со структурой. Структура FLASH_EraseInitTypeDef состоит из трех полей
- тип стирания (общее или постраничное)
- адрес первой страницы
- и количество страниц.
Для вызова функции опишем переменную типа структуры FLASH_EraseInitTypeDef и заполним ее поля. Тип стирания – постраничный, используем для этого специальную константу FLASH_TYPEERASE_PAGES.
Адрес первой страницы сохранен у нас в переменной Address.
И количество страниц указываем равным 1.
Опишем еще одну вспомогательную переменную PageError размерностью 32 бита, ссылку на которую будем передавать функции стирания в качестве второго аргумента.
Разместим вызов функции стирания после операции ветвления switch. Передаем ей адрес описанной структуры EraseInitStruct и ссылку на переменную PageError.
Для программирования данных используется функция
аргументами которой являются
- Размер слова данных, который может принимать одно из трех значений 16 бит, 32 бита или 64 бита.
- Начальный адрес, с которого производится запись
- Данные, которые необходимо записать.
Скопируем эту функцию в проект после стирания страницы памяти. В качестве первого аргумента ей передается константа FLASH_TYPEPROGRAM_WORD, что говорит о том, что мы будем записывать этой операцией 16 бит данных. В качестве второго аргумента передаем адрес начала страницы, сохраненный в переменной Address. А в качестве третьего аргумента – значение i.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration----------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
uint8_t i = 0;
uint8_t flag = 0;
uint32_t Address = 0x0803F800;
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef EraseInitStruct;
EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
EraseInitStruct.PageAddress = Address;
EraseInitStruct.NbPages = 1;
uint32_t PageError;
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if ((HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) && flag == 0)
{
flag = 1;
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11
| GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15, GPIO_PIN_RESET);
switch (i)
{
case 0: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8, GPIO_PIN_SET); break;
case 1: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_9, GPIO_PIN_SET); break;
case 2: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_10, GPIO_PIN_SET); break;
case 3: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_11, GPIO_PIN_SET); break;
case 4: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_12, GPIO_PIN_SET); break;
case 5: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_13, GPIO_PIN_SET); break;
case 6: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_14, GPIO_PIN_SET); break;
case 7: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_15, GPIO_PIN_SET); break;
}
HAL_FLASHEx_Erase(&EraseInitStruct, &PageError);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, Address, i);
i++;
i %= 8;
}
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
flag = 0;
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
Откомпилируем проект и загрузим его в отладочную плату. Запустим на выполнение и нажмем несколько раз на кнопку. Внешне ничего не изменилось, светодиоды по-прежнему переключаются при нажатии кнопки.
А теперь давайте запустим ST Visual Programmer и считаем содержимое FLASH-памяти платы. Перейдем к адресу начала последней страницы. И мы видим здесь некоторое значение, отличное от 0xFF. Это и есть сохраненное значение i. Мы просили записать в память 32 бита или 4 байта данных – и вот это значение хранится в четырех байтах. Причем обратите внимание, в каком виде хранится это значение. Мы его читаем как будто справа налево – значащие разряды находятся слева, то есть по младшим адресам. На самом деле такое хранение числовых данных является корректным. Если нам придется увеличить размер хранимых данных, то мы дополним значение нулями в сторону старших разрядов или старших адресов, которые здесь возрастают слева направо.
Осталось дело за малым. Теперь нужно извлечь из памяти значение i и именно с этого значения начать пересчет. Для этого используется вот такая хитрая строчка.
Кто не знаком с указателями, для того она может показаться непонятной. Указатель – это не что иное, как переменная, хранящая адрес. Более подробно о применении указателей можно почитать здесь.
Мы приводим переменную Address к типу указателя на 32-битное значение, то есть говорим, что значение переменной Address у нас является адресом в памяти. И нам нужно получить значение по этому адресу. Для получения значения по адресу используется символ *.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration----------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
uint8_t i = 0;
uint8_t flag = 0;
uint32_t Address = 0x0803F800;
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef EraseInitStruct;
EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
EraseInitStruct.PageAddress = Address;
EraseInitStruct.NbPages = 1;
uint32_t PageError;
i = *(uint32_t *)Address;
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if ((HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) && flag == 0)
{
flag = 1;
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11
| GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15, GPIO_PIN_RESET);
switch (i)
{
case 0: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8, GPIO_PIN_SET); break;
case 1: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_9, GPIO_PIN_SET); break;
case 2: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_10, GPIO_PIN_SET); break;
case 3: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_11, GPIO_PIN_SET); break;
case 4: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_12, GPIO_PIN_SET); break;
case 5: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_13, GPIO_PIN_SET); break;
case 6: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_14, GPIO_PIN_SET); break;
case 7: HAL_GPIO_WritePin(GPIOE, GPIO_PIN_15, GPIO_PIN_SET); break;
}
HAL_FLASHEx_Erase(&EraseInitStruct, &PageError);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, Address, i);
i++;
i %= 8;
}
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
flag = 0;
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
Добавилась строка 22.
Откомпилируем проект и загрузим в отладочную плату. После использования Visual Programmer может потребоваться переподключить плату к компьютеру.
Заканчиваем отладку и смотрим на поведение платы. При первом включении все работает как обычно.
Но теперь мы отключим питание - выдернем USB-разъем и снова включим его. Светит тот светодиод, который был активен перед отключением питания. Нажимаем на кнопку, и светодиоды переключаются по-прежнему.
На видео 9:00 как может работать светодиод после переподключения платы,
если у нас не нажата кнопка? Это результат того, что мы не подтянули кнопку к минису?
Вот ссылка на память моего МК. s018.radikal.ru/i519/1707/fb/1ee8e3071e49.png
Размерность 0 сектора — 16 кб.
16*1024= 0х0001 0000
А почему тут то памяти выделенно под нулевой сектор всего 15 кб, а не 16?
Простите за беспокойство. Это калькулятор шалит 🙂
3FFF=16383 Если теперь это число разделить на 1024 (в режиме программист) то получаем 15
А если в нормальном режиме разделмть то получаем 15.999