Сохранение данных

Сохранение данных

На этом уроке мы научимся сохранять данные во 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-памяти это действие необходимо разрешить. Для этого используется функция

 
HAL_StatusTypeDef HAL_FLASH_Unlock(void)


Добавим ее перед бесконечным циклом.
Для того чтобы сохранить данные нужно сначала стереть страницу памяти. Открываем драйвер stm32f3xx_hal_flash_ex.c и ищем функцию, которая стирает страницу памяти. Это функция

 
HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit, uint32_t *PageError)

В качестве аргументов ей передаются

  • указатель на структуру FLASH_EraseInitTypeDef
  • и указатель на переменную, в которую сохранится возможный некорректный адрес начала страницы, зафиксированный при стирании памяти.

Теперь разберемся со структурой. Структура FLASH_EraseInitTypeDef состоит из трех полей

  • тип стирания (общее или постраничное)
  • адрес первой страницы
  • и количество страниц.

Для вызова функции опишем переменную типа структуры FLASH_EraseInitTypeDef и заполним ее поля. Тип стирания – постраничный, используем для этого специальную константу FLASH_TYPEERASE_PAGES.

Адрес первой страницы сохранен у нас в переменной Address.
И количество страниц указываем равным 1.
Опишем еще одну вспомогательную переменную PageError размерностью 32 бита, ссылку на которую будем передавать функции стирания в качестве второго аргумента.

Разместим вызов функции стирания после операции ветвления switch. Передаем ей адрес описанной структуры EraseInitStruct и ссылку на переменную PageError.

Для программирования данных используется функция

 
HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data)

аргументами которой являются

  • Размер слова данных, который может принимать одно из трех значений 16 бит, 32 бита или 64 бита.
  • Начальный адрес, с которого производится запись
  • Данные, которые необходимо записать.

Скопируем эту функцию в проект после стирания страницы памяти. В качестве первого аргумента ей передается константа FLASH_TYPEPROGRAM_WORD, что говорит о том, что мы будем записывать этой операцией 16 бит данных. В качестве второго аргумента передаем адрес начала страницы, сохраненный в переменной Address. А в качестве третьего аргумента – значение i.

1
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
int main(void)
{
  /* 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 байта данных – и вот это значение хранится в четырех байтах. Причем обратите внимание, в каком виде хранится это значение. Мы его читаем как будто справа налево – значащие разряды находятся слева, то есть по младшим адресам. На самом деле такое хранение числовых данных является корректным. Если нам придется увеличить размер хранимых данных, то мы дополним значение нулями в сторону старших разрядов или старших адресов, которые здесь возрастают слева направо.
Записанные данные во Flash-памяти

Осталось дело за малым. Теперь нужно извлечь из памяти значение i и именно с этого значения начать пересчет. Для этого используется вот такая хитрая строчка.
 
i = *(uint32_t *)Address;

Кто не знаком с указателями, для того она может показаться непонятной. Указатель – это не что иное, как переменная, хранящая адрес. Более подробно о применении указателей можно почитать здесь.
Мы приводим переменную Address к типу указателя на 32-битное значение, то есть говорим, что значение переменной Address у нас является адресом в памяти. И нам нужно получить значение по этому адресу. Для получения значения по адресу используется символ *.

1
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
int main(void)
{
  /* 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-разъем и снова включим его. Светит тот светодиод, который был активен перед отключением питания. Нажимаем на кнопку, и светодиоды переключаются по-прежнему.

Прокрутить вверх