ZZZ-25-保存数据到FLASH

本篇继 [[ZZZ-23-零点校准]]、[[ZZZ-24-满量程校准]]。想一想,好不容易通过标准物质(例如标准压力)将传感器的零点和满量程校准好,如果断电这些珍贵的校准系数丢了怎么办?这就需要单片机能够保存(并且断电不丢失)数据

STM32 的内部 FLASH 就像是电脑的硬盘,断电后数据不会丢失。我们将把校准得到的 zero_offset_voltslope_k 存入 FLASH 的最后一段空间。

1. FLASH 存储的核心规则(必读)

在操作之前,你需要记住 STM32 FLASH 的三个“脾气”:

  1. 先擦除再写入:FLASH 的位只能从 1 变成 0。擦除操作会将整个“页”(Page)恢复为全 1(即 0xFF)。
  2. 以“页”为单位:STM32F103 通常每页是 1KB2KB。即使你只改一个字,也得擦除整一页。
  3. 读写保护:写入前要“解锁”(Unlock),写完要“上锁”(Lock)。

2. 确定存储位置

我们要把数据存在 FLASH 的末尾,避免覆盖掉你写的程序代码。

  • 假设:你用的是 STM32F103C8T6(64KB FLASH)。
  • 最后一页地址:0x0800FC00(第 63 页)。

3. 实战代码:读写函数库

你可以将以下代码直接加入到 main.cUSER CODE BEGIN 0 区域。

A. 写入函数(保存参数)

 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
#define FLASH_USER_START_ADDR   0x0800FC00   // 最后一页地址

void Save_Params_To_Flash(float zero, float slope) {
    uint32_t data_to_save[2];
    
    // 使用指针强转,把 float 变成 uint32_t 存入数组
    data_to_save[0] = *(uint32_t*)&zero;
    data_to_save[1] = *(uint32_t*)&slope;

    HAL_FLASH_Unlock(); // 解锁

    // 1. 擦除最后一页
    FLASH_EraseInitTypeDef EraseInitStruct;
    uint32_t PageError = 0;
    EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
    EraseInitStruct.PageAddress = FLASH_USER_START_ADDR;
    EraseInitStruct.NbPages = 1;
    HAL_FLASHEx_Erase(&EraseInitStruct, &PageError);

    // 2. 写入数据
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_USER_START_ADDR, data_to_save[0]);
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_USER_START_ADDR + 4, data_to_save[1]);

    HAL_FLASH_Lock(); // 上锁
}

B. 读取函数(开机加载)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void Load_Params_From_Flash(float *zero, float *slope) {
    uint32_t raw_zero = *(__IO uint32_t*)(FLASH_USER_START_ADDR);
    uint32_t raw_slope = *(__IO uint32_t*)(FLASH_USER_START_ADDR + 4);

    // 如果 FLASH 是空的(0xFFFFFFFF),则不加载,维持默认值
    if (raw_zero != 0xFFFFFFFF) {
        *zero = *(float*)&raw_zero;
        *slope = *(float*)&raw_slope;
    }
}

4. 如何整合到你的系统中?

第一步:开机自动读取

main 函数的 while(1) 之前调用,确保一上电就拿到了上次校准的值。

1
2
3
4
/* USER CODE BEGIN 2 */
Load_Params_From_Flash(&zero_offset_volt, &slope_k);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, SAMPLES * CHANNELS);
/* USER CODE END 2 */

第二步:校准成功后自动保存

在你之前的按键校准逻辑里,确认校准成功后,顺手存一下:

1
2
3
4
5
6
7
// 在零点校准成功后的位置
zero_offset_volt = volt;
Save_Params_To_Flash(zero_offset_volt, slope_k); // 同步保存到 FLASH

// 在满量程校准成功后的位置
slope_k = P_NOMINAL / v_diff;
Save_Params_To_Flash(zero_offset_volt, slope_k); // 同步保存到 FLASH

关机前

关机重启后:

5. 为什么这样做很“专业”?

  1. 用户体验:在 CEMS 站房里,设备可能会因为停电或检修重启。如果没有 FLASH 存储,运维人员每次开机都得重新通标准气体校准,那是极其痛苦的。
  2. 数据安全:通过 0xFFFFFFFF 的判断,你保证了新芯片第一次运行时不会加载乱码,而是使用代码里的默认值。

完整代码

1
2
3
4
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* USER CODE BEGIN PV */
#define SAMPLES 10  					// 每个通道采样 10 次
#define CHANNELS 2 						// 共 2 个通道
uint16_t adc_buffer[SAMPLES * CHANNELS]; // 长度为 20 的数组
// --- 核心标定参数 ---
const float P_NOMINAL = 1.6f;             // 满量程标定时的标准压力值 (MPa)
const float R_SAMPLE = 150.0f;            // 采样电阻 150 欧
const float alpha = 0.15f;                // 一阶滞后滤波系数

// --- 标定状态变量 ---
float zero_offset_volt = 0.60f;           // 默认零点电压 (4mA 对应 0.6V)
float slope_k = 1.6f / (3.0f - 0.60f);    // 默认斜率
float filter_volt_A = 0.60f;              // 滤波后的电压变量
uint8_t calibration_flag = 0;             // 标定标志位
/* USER CODE END PV */
 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
/* USER CODE BEGIN 0 */
int __io_putchar(int ch)
{

    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

// 写入函数(保存参数)
#define FLASH_USER_START_ADDR   0x0800FC00   // 最后一页地址
void Save_Params_To_Flash(float zero, float slope) {
    uint32_t data_to_save[2];

    // 使用指针强转,把 float 变成 uint32_t 存入数组
    data_to_save[0] = *(uint32_t*)&zero;
    data_to_save[1] = *(uint32_t*)&slope;

    HAL_FLASH_Unlock(); // 解锁

    // 1. 擦除最后一页
    FLASH_EraseInitTypeDef EraseInitStruct;
    uint32_t PageError = 0;
    EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
    EraseInitStruct.PageAddress = FLASH_USER_START_ADDR;
    EraseInitStruct.NbPages = 1;
    HAL_FLASHEx_Erase(&EraseInitStruct, &PageError);

    // 2. 写入数据
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_USER_START_ADDR, data_to_save[0]);
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_USER_START_ADDR + 4, data_to_save[1]);

    HAL_FLASH_Lock(); // 上锁
}

// 读取函数(开机加载)
void Load_Params_From_Flash(float *zero, float *slope) {
    uint32_t raw_zero = *(__IO uint32_t*)(FLASH_USER_START_ADDR);
    uint32_t raw_slope = *(__IO uint32_t*)(FLASH_USER_START_ADDR + 4);

    // 如果 FLASH 是空的(0xFFFFFFFF),则不加载,维持默认值
    if (raw_zero != 0xFFFFFFFF) {
        *zero = *(float*)&raw_zero;
        *slope = *(float*)&raw_slope;
    }
}

/* USER CODE END 0 */
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  /* USER CODE BEGIN 2 */
  // 为了精准,建议先进行 ADC 校准(F1系列特有�?
  HAL_ADCEx_Calibration_Start(&hadc1);

  // 核心命令:启�? ADC + DMA
  // 参数:ADC句柄,存储的目标地址,要搬运的数据个�?
  HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, SAMPLES * CHANNELS);

  // 开机加载校准数据
  Load_Params_From_Flash(&zero_offset_volt, &slope_k);
  HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, SAMPLES * CHANNELS);

  /* USER CODE END 2 */
 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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
  while (1)
  {
      // --- 1. 信号采集与双重滤波 ---
      uint32_t sum_A = 0;
      for(int i = 0; i < SAMPLES; i++) {
          sum_A += adc_buffer[i * CHANNELS];
      }

      // 原始电压计算 (基于 3.3V 理论值,若需更准可换回 VREFINT 公式)
      float raw_volt_A = (float)sum_A / SAMPLES * 3.3f / 4095.0f;

      // 一阶滞后滤波
      filter_volt_A = alpha * raw_volt_A + (1.0f - alpha) * filter_volt_A;
      float volt = filter_volt_A;

      // --- 2. 零点校准逻辑 (PB0) ---
      if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET)
      {
          HAL_Delay(20); // 初步去抖
          if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET)
          {
              zero_offset_volt = volt; // 记录当前电压为新零点

              // 亮灯反馈
              HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
              HAL_Delay(500);
              HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);

              // 在零点校准成功后的位置
              zero_offset_volt = volt;
              Save_Params_To_Flash(zero_offset_volt, slope_k); // 同步保存到 FLASH

              while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET); // 等待松开
          }
      }

      // --- 3. 满量程校准逻辑 (PB1) ---
      if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET)
      {
          HAL_Delay(20);
          if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET)
          {
              // 核心逻辑:斜率 k = 标准压力 / (当前满量程电压 - 零点电压)
              float v_diff = volt - zero_offset_volt;

              if (v_diff > 0.1f) // 安全检查:确保满位电压确实大于零点电压
              {
                  slope_k = P_NOMINAL / v_diff;

                  // 在满量程校准成功后的位置
                  slope_k = P_NOMINAL / v_diff;
                  Save_Params_To_Flash(zero_offset_volt, slope_k); // 同步保存到 FLASH

                  // 成功后快闪两次灯
                  for(int i=0; i<4; i++) {
                      HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
                      HAL_Delay(100);
                  }
              }
              while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET);
          }
      }

      // --- 4. 最终工业参数换算 ---
      // 计算电流 (mA)
      float current_mA = (volt / R_SAMPLE) * 1000.0f;

      // 计算校准后的压力
      float calibrated_pressure = (volt - zero_offset_volt) * slope_k;

      // 负数截断保护
      if (calibrated_pressure < 0.0f) calibrated_pressure = 0.0f;

      // --- 5. VOFA+ 数据输出 ---
      // 通道说明:1.电流(mA), 2.校准后压力(MPa), 3.零点偏置电压(V), 4.当前斜率k
      printf("%.2f,%.3f,%.3f,%.4f\n",
              current_mA,
              calibrated_pressure,
              zero_offset_volt,
              slope_k);

      HAL_Delay(100); // 10Hz 刷新率,适合观察
  }
Licensed under CC BY-NC-SA 4.0