ZZZ-27-程序架构

前言

本篇继 ZZZ-26-实现Modbus RTU协议,在上一篇中,所有的程序都是卸载 main.c 文件中的,包含 ADC 采集与滤波、Flash 读写、Modbus 协议解析、物理量换算等功能,但存在下面所述问题:

为什么 delay 是嵌入式的“毒药”

假设你的 CEMS 有一个功能:“反吹扫”(用高压气体清理采样探头),需要持续 30 秒。

“屎山”写法(阻塞式):

1
2
3
4
5
void Backflush_Sequence() {
    Relay_On();           // 打开电磁阀
    HAL_Delay(30000);     // 停在这里等 30 秒
    Relay_Off();          // 关闭电磁阀
}

致命问题:

  • 串口变聋子:在这 30 秒内,如果串口屏发来一个“停止”指令,或者 Modbus 上位机要读数据,单片机完全听不见,因为它正死死地卡在 delay 里。
  • 控温失效:你的 PID 控温函数无法执行。这 30 秒内,发热片可能会因为没有 PID 调节而过热烧毁。

为什么 if-else 会变成“屎山”

随着功能增加,你会发现逻辑变得极其扭曲。

“逻辑地狱”示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void loop() {
    if (is_power_on) {
        if (!is_warmed_up) {
            // 预热逻辑...
        } else {
            if (button_pressed) {
                if (mode == 1) {
                    // 执行采样...
                } else if (mode == 2) {
                    // 执行校准...
                }
            }
        }
    }
    // 如果再加一个“急停”或者“传感器故障”,这个嵌套深度会让你崩溃
}

致命问题:

  • 难以维护:改动一个地方,可能会导致原本正常的逻辑在某种特定组合下失效。
  • 无法处理“突发”事件:比如在采样过程中突然发生“漏液报警”,你需要从深深的嵌套 if 中跳出来,非常困难。

救星:状态机(FSM)架构

状态机就像是一个“自动售货机”:它永远处于某个特定的状态,并且只在接收到事件时才切换状态

FSM 代码架构(非阻塞式):

首先,定义状态(用枚举):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef enum {
    STATE_IDLE,        // 空闲
    STATE_WARMUP,      // 预热
    STATE_MEASURE,     // 测量
    STATE_BACKFLUSH,   // 反吹
    STATE_FAULT        // 故障
} SystemState_t;

SystemState_t currentState = STATE_IDLE; // 初始状态
uint32_t stateStartTime = 0;             // 用于非阻塞计时

然后,在 while(1) 循环里使用 switch-case

 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
void main_loop() {
    switch (currentState) {
        case STATE_IDLE:
            if (Start_Button_Pressed()) currentState = STATE_WARMUP;
            break;

        case STATE_WARMUP:
            Heat_Control(120.0); // 调用 PID,不会阻塞
            if (Get_Temp() >= 120.0) currentState = STATE_MEASURE;
            break;

        case STATE_BACKFLUSH:
            Relay_On();
            // --- 非阻塞计时核心 ---
            if (HAL_GetTick() - stateStartTime >= 30000) { 
                Relay_Off();
                currentState = STATE_IDLE; 
            }
            break;

        case STATE_FAULT:
            All_Stop();
            Show_Error_On_Screen();
            break;
    }
    
    // 【关键】无论在什么状态,这些函数每秒都在跑
    PID_Compute();      // 控温不停
    UART_Parse();       // 串口指令随叫随到
    Safety_Monitor();   // 安全检查一刻不松
}
  • 并发处理:因为它没有 delay,主循环跑得飞快(每秒几千次)。看起来单片机在“同时”控温、接串口、管流程。
  • 逻辑清晰:你只需要关注“当前状态”以及“跳转条件”。比如在任何状态下发现传感器坏了,直接 currentState = STATE_FAULT,系统立刻进入安全模式。
  • 易于扩展:如果你想增加一个“手动校准”模式,只需要在 enum 里加一个状态,然后在 switch 里多写一个 case,完全不影响老代码。

更改程序架构

按照“模块化”的思想,将 ZZZ-26-实现Modbus RTU协议 中完整代码章节进行改写

  • App: 存放高层逻辑(如 Modbus、校准算法)。
  • BSP: (Board Support Package) 存放硬件驱动封装(如 Flash、ADC 处理)。
  • Tools: 存放通用工具(如 CRC 计算、滤波函数)。

第一步:环境准备

在开始搬运前,先让 CubeIDE 认识你的新家:

  1. 在项目根目录下右键 -> New -> Folder,命名为 User_Library(或者你喜欢的名字)。
  2. User_Library 下创建两个文件夹:ToolsBSP
  3. 告知编译器路径:
    • 右键工程 -> Properties
    • C/C++ Build -> Settings -> MCU GCC Compiler -> Include paths
    • 点击“+”,点击 Workspace,选中 User_Library/ToolsUser_Library/BSP

![[Pasted image 20251227185813.png]]

![[Pasted image 20251227190514.png]]

这两个设置项对应着编译过程中的两个不同阶段:“寻找说明书(头文件)”和“指定加工厂(源文件)”

  • 1、为什么要在 MCU GCC Compiler -> Include Paths 中添加?
    • 作用:当你写下 #include "filter.h" 时,编译器并不知道这个文件在哪。添加 Include Paths 相当于给编译器提供了一张 “地图”
  • 为什么要在 General -> Paths and Symbols 中添加?
    • 告诉 IDE 哪些文件夹是 “源码文件夹”。只有被列入 Source Location 的文件夹,里面的 .c 文件才会被送进编译器转换成 .o 目标文件。
维度 MCU GCC Compiler -> Include Paths Paths and Symbols -> Source Location
针对对象 .h 头文件 .c 源文件
功能描述 告诉编译器去哪里找“声明” 告诉编译器去哪里找“代码实现”
递归性 不具备递归性(必须指定到最后一级) 具备递归性(指定父目录,子目录自动编译)
报错表现 No such file or directory undefined reference to …

注意事项与“潜规则”

  1. Include Paths 的非递归性:
    • 如果你有文件夹 User_Library,下面有子文件夹 AppTools
    • 你只添加 User_Library 是没用的。你必须分别添加 User_Library/AppUser_Library/Tools
  2. 优先使用相对路径:
    • 千万不要写成 D:\Desktop\Project\…
    • 应该使用 ${ProjDirPath}/Core/User_Library/… 或者使用 ../ 开头的相对路径。这样项目发给别人也能直接编译。
  3. Source Folder 的图标:
    • 检查你的文件夹图标。如果图标右上角有 蓝色小方块,说明它已在 Source Location 中;如果是纯黄色文件夹,说明它不会被编译。
  4. 修改后记得 Clean:
    • 改动这些路径设置后,IDE 的索引有时会卡住。最稳妥的操作是 Project -> Clean,然后重新 Build

转移模块 1:一阶滞后滤波 (Tools/Filter)

这是最简单的模块,因为它不依赖具体的硬件引脚,只进行数学运算。

1、创建头文件 filter.h

Tools 文件夹右键 -> New -> Header File,命名为 filter.h

1
2
3
4
5
6
7
8
9
#ifndef __FILTER_H
#define __FILTER_H

#include "main.h" // 包含 main.h 以使用 float 类型

// 函数声明:告诉别人这个函数怎么用
float Low_Pass_Filter(float input, float last_output, float alpha);

#endif

2、第二步:创建源文件 filter.c

Tools 文件夹右键 -> New -> Source File,命名为 filter.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include "filter.h"

/**
 * @brief 一阶滞后滤波算法
 * @param input: 本次采样值
 * @param last_output: 上次滤波后的输出值
 * @param alpha: 滤波系数 (0.0~1.0)
 * @return 滤波后的值
 */
float Low_Pass_Filter(float input, float last_output, float alpha) {
    return (alpha * input) + ((1.0f - alpha) * last_output);
}

转移模块 2:参数存储 (BSP/Storage)

这个模块负责把校准数据存进 Flash。它依赖 STM32 的硬件库。

1、创建头文件 storage.h

BSP 文件夹下创建 storage.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#ifndef __STORAGE_H
#define __STORAGE_H

#include "main.h"

// 将 FLASH 地址定义在这里,方便修改
#define FLASH_USER_START_ADDR   0x0800FC00 

void Save_Params_To_Flash(float zero, float slope);
void Load_Params_From_Flash(float *zero, float *slope);

#endif

2、创建源文件 storage.c

main.c 里那段长长的 Flash 读写逻辑剪切过来:

 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
#include "storage.h"

void Save_Params_To_Flash(float zero, float slope) {
    uint32_t data_to_save[2];
    data_to_save[0] = *(uint32_t*)&zero;
    data_to_save[1] = *(uint32_t*)&slope;

    HAL_FLASH_Unlock();

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

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

    if (raw_zero != 0xFFFFFFFF) {
        *zero = *(float*)&raw_zero;
        *slope = *(float*)&raw_slope;
    }
}

转移模块 3:Modbus

![[Pasted image 20251227192243.png]]

当你把代码从 main.c 分离到 modbus_slave.c 时,这两个文件就像变成了两个独立的办公室。默认情况下,他们互相看不见对方桌子上的资料(变量)和正在做的工程(函数)。这里的两行代码,就是为了打破办公室之间的墙,建立通讯。

1. 为什么必须 #include "storage.h"

逻辑:获取“使用手册(接口声明)”

Modbus_Handle 函数里,你调用了 Save_Params_To_Flash(…)

  • 编译器视角的困惑:当它编译 modbus_slave.c 时,它看到这个函数名会很懵逼:“这个函数长什么样?需要传几个参数?返回什么类型?”
  • 解决方案:#include "storage.h" 就像是把 Save_Params_To_Flash 的“说明书”复印了一份贴在了 modbus_slave.c 的墙上。
  • 结果:编译器看到说明书后说:“哦,原来这个函数接收两个 float 参数,不返回任何值,没问题,准许通过!”

2. 为什么必须使用 extern 声明变量?

逻辑:寻找“共享单车(跨文件访问)”

这是新手最容易搞混的地方。你的 zero_offset_volt(零点偏移电压)定义在 main.c 里,它是系统的核心资产

  • 错误的尝试:如果你在 modbus_slave.c 里又写了一遍 float zero_offset_volt;,这相当于在两个办公室各买了一辆单车。Modbus 修改的是它的单车,而测量模块(ADC)骑的是另一辆单车。数据不同步!
  • extern 的妙处:它告诉编译器:“听着,zero_offset_volt 这个变量真实存在于别的办公室(文件)里,我这里只是借用一下它的名字,不要在这里给它分配新的内存空间。”
  • 链接阶段的奇迹:最后链接器(Linker)干活时,会将 modbus_slave.o 里的这个借用请求,精准地指向 main.o 里那个真实的变量地址。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#ifndef __MODBUS_SLAVE_H
#define __MODBUS_SLAVE_H

#include "main.h"

// 宏定义:通讯参数
#define SLAVE_ID      0x01
#define RX_BUF_SIZE   64

// 声明外部变量:这样 main.c 就能看到并往这些寄存器里填浓度数据
extern uint16_t Modbus_Regs[10];
extern volatile uint8_t modbus_rx_flag;

// 函数声明
void Modbus_Init(UART_HandleTypeDef *huart); // 传入串口句柄(如 &huart2)
void Modbus_Handle(void);
uint16_t Modbus_CRC16(uint8_t *ptr, uint16_t len);

#endif
  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
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include "modbus_slave.h"
#include <string.h>
#include <stdio.h>
#include "storage.h"  // [FIX] 必须包含这个头文件,才能调用 Save_Params_To_Flash

// [FIX] 必须在这里用 extern 声明那些定义在 main.c 或 logic.c 里的变量
extern float zero_offset_volt;
extern float filter_volt_A;
extern float slope_k;


// 1. 变量定义:真正的内存开辟在这里
uint16_t Modbus_Regs[10];
uint8_t rx_buffer[RX_BUF_SIZE];
uint8_t tx_buffer[RX_BUF_SIZE];
volatile uint8_t  modbus_rx_flag = 0;
volatile uint16_t modbus_rx_len = 0;
UART_HandleTypeDef *p_huart; // 内部保存串口句柄

// 2. 初始化:告诉模块用哪个串口
void Modbus_Init(UART_HandleTypeDef *huart) {
    p_huart = huart;
    HAL_UARTEx_ReceiveToIdle_DMA(p_huart, rx_buffer, RX_BUF_SIZE);
}

// 3. CRC 计算(搬运你之前的代码)
uint16_t Modbus_CRC16(uint8_t *ptr, uint16_t len) {
    uint16_t crc = 0xFFFF;
    while(len--) {
        crc ^= *ptr++;
        for(int i = 0; i < 8; i++) {
            if(crc & 0x01) { crc >>= 1; crc ^= 0xA001; }
            else { crc >>= 1; }
        }
    }
    return crc;
}

// 4. 解析逻辑(搬运你的 Modbus_Handle,注意把 huart2 改为 p_huart)
void Modbus_Handle(void) {
    // 如果没有标志位,说明根本没触发串口接收
    if (modbus_rx_flag == 0) return;

    // --- 雷达探测:只要串口 2 动了,串口 1 必须说话 ---
    // 这一段放在所有 if 校验的最前面!
    printf("\r\n[Radar] New Packet! Len: %d\r\n", modbus_rx_len);
    printf("[Radar] Data: ");
    for(int i = 0; i < modbus_rx_len; i++) {
        printf("%02X ", rx_buffer[i]);
    }
    printf("\r\n");

    // --- 下面是原有的逻辑,但我们加了错误诊断 ---
    if (modbus_rx_len < 8) {
        printf("[Error] Length < 8 bytes!\r\n");
        goto reset;
    }

    if (rx_buffer[0] != SLAVE_ID) {
        printf("[Error] ID Mismatch! Recv:%02X\r\n", rx_buffer[0]);
        goto reset;
    }

    uint16_t crc_calc = Modbus_CRC16(rx_buffer, 6); // 针对 06 指令固定前 6 位
    uint16_t crc_recv = rx_buffer[6] | (rx_buffer[7] << 8);

    if (crc_calc != crc_recv) {
        printf("[Error] CRC Fail! Calc:%04X, Recv:%04X\r\n", crc_calc, crc_recv);
        goto reset; // 如果CRC校验不成功,不执行任何操作
    }

    // 真正的逻辑处理
    // --- 处理 03 读取 ---
    if (rx_buffer[1] == 0x03) {
        uint16_t start_addr = (rx_buffer[2] << 8) | rx_buffer[3];
        uint16_t reg_num = (rx_buffer[4] << 8) | rx_buffer[5];

        // 构造回复报文
        tx_buffer[0] = SLAVE_ID;
        tx_buffer[1] = 0x03;
        tx_buffer[2] = reg_num * 2; // 字节数

        for (int i = 0; i < reg_num; i++) {
            // 将数组中的 uint16 拆分为高低字节 (大端序)
            tx_buffer[3 + i * 2] = (Modbus_Regs[start_addr + i] >> 8) & 0xFF;
            tx_buffer[4 + i * 2] = Modbus_Regs[start_addr + i] & 0xFF;
        }

        uint16_t crc_send = Modbus_CRC16(tx_buffer, 3 + reg_num * 2);
        tx_buffer[3 + reg_num * 2] = crc_send & 0xFF;
        tx_buffer[4 + reg_num * 2] = (crc_send >> 8) & 0xFF;

        // 通过 USART2 发送回复
        HAL_UART_Transmit(p_huart, tx_buffer, 5 + reg_num * 2, 100);
    }
    else if (rx_buffer[1] == 0x06) {
        uint16_t addr = (rx_buffer[2] << 8) | rx_buffer[3];
        uint16_t val  = (rx_buffer[4] << 8) | rx_buffer[5];
        printf("[Match] Function 06! Addr:0x%04X, Val:0x%04X\r\n", addr, val);

        if (addr == 0x0006 && val == 0x55AA) {
             printf("[Action] Calibrating Zero...\r\n");
             zero_offset_volt = filter_volt_A;
             HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
             Save_Params_To_Flash(zero_offset_volt, slope_k);
        }
        HAL_UART_Transmit(p_huart, rx_buffer, 8, 100);
    }

reset:
    modbus_rx_flag = 0;
    modbus_rx_len = 0;
    memset(rx_buffer, 0, RX_BUF_SIZE);
    HAL_UARTEx_ReceiveToIdle_DMA(p_huart, rx_buffer, RX_BUF_SIZE);
}

// 串口接收完成回调函数 (处理 IDLE 中断)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
    if (huart->Instance == p_huart->Instance) { // 判断是不是 Modbus 用的那个串口
        modbus_rx_len = Size;
        modbus_rx_flag = 1;
    }
}

转移模块 4:Logic

这个模块将作为你系统的“大脑”,负责处理所有的传感器数据和用户的校准操作。

1. 编写头文件 logic.h

Core/Inc(或 User_Library/App)创建 logic.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#ifndef __LOGIC_H
#define __LOGIC_H

#include "main.h"

// 声明外部变量,让 main.c 打印调试信息或 Modbus 使用
extern float current_mA;
extern float calibrated_pressure;
extern float zero_offset_volt;
extern float slope_k;

// 函数声明
void Logic_Init(void);   // 逻辑初始化(加载参数等)
void Logic_Update(void); // 逻辑轮询(采样、计算、按键逻辑)

#endif

2. 编写源文件 logic.c

Core/Src(或 User_Library/App)创建 logic.c。这里要把你 while(1) 里的逻辑全部剪切过来。

 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
84
85
86
87
88
89
90
91
92
93
94
95
#include "logic.h"
#include "filter.h"
#include "storage.h"
#include "modbus_slave.h"
#include <stdio.h>

// --- 私有常量定义 ---
#define P_NOMINAL 1.6f  // 标准压力满量程 (MPa)
#define R_SAMPLE 150.0f  // 采样电阻 150 欧

// --- 变量定义 ---
float current_mA = 0;
float calibrated_pressure = 0;
float zero_offset_volt = 0.60f; // 默认零点 (4mA -> 0.6V)
float slope_k = 1.6f / (3.0f - 0.60f);       // 默认斜率
float filter_volt_A = 0.60f;


// 引用 main.c 中的硬件缓存
extern uint16_t adc_buffer[];

/**
 * @brief 逻辑模块初始化
 */
void Logic_Init(void) {
    // 从 Flash 加载之前保存的标定参数
    Load_Params_From_Flash(&zero_offset_volt, &slope_k);
}

/**
 * @brief 逻辑模块主轮询函数 (在 main 的 while 循环中调用)
 */
void Logic_Update(void) {
    // --- 1. ADC 信号采集与一阶滞后滤波 ---
    uint32_t sum_A = 0;
    for(int i = 0; i < 10; i++) {
        sum_A += adc_buffer[i * 2]; // 采样通道 0
    }
    float raw_volt = (float)sum_A / 10.0f * 3.3f / 4095.0f;
    filter_volt_A = Low_Pass_Filter(raw_volt, filter_volt_A, 0.15f);

    // --- 2. 物理量计算 ---
    current_mA = (filter_volt_A / R_SAMPLE) * 1000.0f; // 假设采样电阻 150 欧
    calibrated_pressure = (filter_volt_A - zero_offset_volt) * slope_k;

    // 负数截断保护(压力不会是负的)
    if (calibrated_pressure < 0.0f) calibrated_pressure = 0.0f;

    // --- 3. 零点校准逻辑 (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) {
            // 执行零点校准:记录当前电压为 0MPa 对应的电压
            zero_offset_volt = filter_volt_A;
            Save_Params_To_Flash(zero_offset_volt, slope_k);

            // LED 反馈:PC13 常亮 500ms 表示成功
            HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
            HAL_Delay(500);
            HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);

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

    // --- 4. 满量程校准逻辑 (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) {
            // 计算当前电压与零点电压的差值
            float v_diff = filter_volt_A - zero_offset_volt;

            // 安全检查:只有当当前电压明显高于零点电压时,才允许标定满量程
            if (v_diff > 0.1f) {
                // 计算新斜率:K = 标准压力 / 电压差
                slope_k = P_NOMINAL / v_diff;
                Save_Params_To_Flash(zero_offset_volt, slope_k);

                // LED 反馈:PC13 快闪 4 次表示成功
                for(int i = 0; i < 8; i++) {
                    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
                    HAL_Delay(100);
                }
                HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // 确保灭灯
            }
            while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET); // 等待按键松开
        }
    }

    // --- 5. 同步数据到 Modbus 寄存器仓库 ---
    Modbus_Regs[0] = (uint16_t)(current_mA * 100);      // 电流,放大100倍
    Modbus_Regs[1] = (uint16_t)(calibrated_pressure * 1000); // 压力,放大1000倍
    Modbus_Regs[2] = (uint16_t)(zero_offset_volt * 1000);    // 零点电压
    Modbus_Regs[3] = (uint16_t)(slope_k * 1000);        // 斜率K
}
  • 为什么这里的 P_NOMINAL#define 定义
    • #define 工作原理:在程序真正开始编译之前,预处理器会扫描一遍代码,把所有出现 P_NOMINAL 的地方,暴力替换1.6f
  • 为什么在 logic.c 中推荐用 #define
    • 物理常量属性:P_NOMINAL(满量程 1.6MPa)是传感器的硬件属性。在设计电路板和选型传感器时就定死了。它属于“配置参数”而非“运行变量”。
    • MISRA-C 标准习惯:在一些工业安全标准中,宏定义常用于定义这种“系统级常量”,让代码逻辑和配置参数清晰分开。

define 定义注意事项 1、多余的分号 2、多余的等号

1
2
3
下面两种都是错误的
#define R_SAMPLE 150.0f; // 注意末尾这个分号
#define R_SAMPLE = 150.0f // 注意这个等号

深度避坑指南:给宏加上括号。推荐写法:

1
#define R_SAMPLE (150.0f)

为什么要加括号? 假设你定义了一个电阻组合:#define R_TOTAL 100.0f + 50.0f。 如果你写 1 / R_TOTAL,替换后会变成 1 / 100.0f + 50.0f。根据数学优先级,它会先算 1/100 再加 50,这显然不是你想要的。加上括号就会变成 1 / (100.0f + 50.0f)

精简后的 main

1
2
3
4
5
6
7
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <string.h>
#include "filter.h"
#include "storage.h"
#include "logic.h"
/* USER CODE END Includes */
1
2
3
4
5
/* USER CODE BEGIN PV */
#define SAMPLES 10  					// 每个通道采样 10 次
#define CHANNELS 2 						// 共 2 个通道
uint16_t adc_buffer[SAMPLES * CHANNELS]; // 长度为 20 的数组
/* USER CODE END PV */
1
2
3
4
5
6
7
/* USER CODE BEGIN 0 */
int __io_putchar(int ch)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}
/* USER CODE END 0 */
1
2
3
4
5
6
7
8
  /* USER CODE BEGIN 2 */
    // ADC 初始化与校准
    HAL_ADCEx_Calibration_Start(&hadc1);
    // 启动 ADC DMA 搬运 (搬运 20 个数据到 adc_buffer)
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, SAMPLES * CHANNELS);
    Logic_Init();         // 初始化业务逻辑(加载 Flash)
    Modbus_Init(&huart2); // 初始化通讯
    /* USER CODE END 2 */
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
   while (1)
   {
        Logic_Update();   // 运行业务大脑
        Modbus_Handle();  // 运行通讯解析
		// --- 7. VOFA+ 调试输出 (USART1) ---
	    printf("%.2f,%.3f,%.3f,%.4f\n", current_mA, calibrated_pressure, zero_offset_volt, slope_k);
	    HAL_Delay(50); // 稍微提高刷新率

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }

非阻塞框架

在上面的 logic 函数当中,用到了 HAL_Delay(500)HAL_Delay(20),这会让 CPU 原地踏步,这期间,Modbus 无法回包、ADC 数据无法更新。

  • 另外,重构后,main.c 里的 HAL_Delay(50) 建议保留吗?
  • 不建议。应将其减小(如 1ms)或彻底移除。主循环运行越快,系统的实时响应速度和采样频率就越高。

1.核心思想:从“等 500ms”变成“记录时间戳”

想象你在煮面:

  • 阻塞式 (HAL_Delay):盯着锅看 5 分钟,什么都不干。
  • 非阻塞式 (GetTick):记下现在是 12:00,然后去扫地,每隔一会看眼表,发现 12:05 了,关火。

实现的目的:

  • 改造前(逻辑流): 按下按键 -> 死等20ms -> 检查状态 -> 存Flash -> 死等500ms闪灯 -> 结束
  • 改造后(逻辑环):
    • 主循环:每毫秒转好几圈。
    • 看按键:现在时间 - 按下时间 > 20ms 了吗?够了就处理。
    • 看灯:现在时间 - 亮灯时间 > 500ms 了吗?够了就灭灯。
    • 回 Modbus:有人叫我吗?有就立刻回。

2. 实战重构:非阻塞按键消抖

重构路线图

  • 第一步:在 logic.c 顶部定义记录时间的 uint32_t 变量。
  • 第二步:改写按键逻辑,用 HAL_GetTick() - tick > 20 替换 HAL_Delay(20)
  • 第三步:引入 switch-case 状态机处理 LED 反馈。
  • 第四步:点击编译,享受那种一边闪灯校准、一边 Modbus 飞速回包的专业快感。

之前的代码:

1
2
3
4
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == 0) {
    HAL_Delay(20); // 这里死等 20ms(软件去抖的延迟)
    if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == 0) { ... }
}

重构后(利用时间戳): 我们需要在 logic.c 里定义一些静态变量来记录状态。

 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
static uint32_t key_press_time = 0; // 记录按键按下的时刻
static uint8_t key_active = 0;      // 按键是否处于待处理状态

void Logic_Update(void) {
    // 1. 读取按键物理状态
    uint8_t current_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);

    if (current_pin_state == GPIO_PIN_RESET) { // 按键按下
        if (key_press_time == 0) { 
            key_press_time = HAL_GetTick(); // 刚刚按下,记下开始时间
        } 
        else if (HAL_GetTick() - key_press_time > 20) { // 时间过了20ms
            if (key_active == 0) {
                // --- 执行校准业务 ---
                zero_offset_volt = filter_volt_A;
                Save_Params_To_Flash(zero_offset_volt, slope_k);
                
                // 触发 LED 任务(看下面)
                Start_LED_Feedback(1); 
                key_active = 1; // 标记已处理,防止长按重复触发
            }
        }
    } else {
        key_press_time = 0; // 按键松开,重置计数
        key_active = 0;
    }
}

3. 高级重构:非阻塞 LED 反馈(状态机)

这是最精彩的部分。我们希望 LED 闪烁的同时,主程序继续飞速运行。我们在 logic.c 顶部定义 LED 的状态:

 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
typedef enum {
    LED_IDLE,
    LED_ON_WAIT,
    LED_OFF_WAIT
} LED_State_t;

static LED_State_t led_step = LED_IDLE;
static uint32_t led_timer = 0;
static uint8_t flash_count = 0;

// 启动反馈的函数
void Start_LED_Feedback(uint8_t times) {
    flash_count = times;
    led_step = LED_ON_WAIT;
    led_timer = HAL_GetTick();
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 点亮
}

// 在 Logic_Update 中轮询这个状态机
void Handle_LED_Task(void) {
    switch (led_step) {
        case LED_ON_WAIT:
            if (HAL_GetTick() - led_timer > 500) { // 亮够 500ms 了
                HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // 熄灭
                led_timer = HAL_GetTick();
                led_step = LED_OFF_WAIT;
            }
            break;

        case LED_OFF_WAIT:
            if (HAL_GetTick() - led_timer > 200) { // 灭够 200ms 了
                if (--flash_count > 0) {
                    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 再亮
                    led_timer = HAL_GetTick();
                    led_step = LED_ON_WAIT;
                } else {
                    led_step = LED_IDLE; // 闪完了
                }
            }
            break;
        default: break;
    }
}

为了实现不卡顿的闪灯,我们需要三个零件:

  • 状态变量 (led_step):记录“我现在进行到哪一步了”。(备忘录)
  • 时间戳变量 (led_timer):记录“我是什么时候开始这一步的”。(闹钟)
  • 计数值 (flash_count):记录“我还要闪几次”。

我们把这个过程拆成三个状态:IDLE(闲置)、ON_WAIT(亮灯等待)、OFF_WAIT(灭灯等待)。

第一步:触发任务(按下快门)

当你的校准逻辑成功时,你调用 Start_LED_Feedback(3)。这时发生了什么?

  • 它不闪灯,它只是设置参数
    • led_step = LED_ON_WAIT; —— “备忘录:进入亮灯阶段”。
    • led_timer = HAL_GetTick(); —— “闹钟:记下现在的时间”。
    • HAL_GPIO_WritePin(… , 0); —— “动作:把灯点亮”。
  • 注意:这个函数执行完只需要几个微秒,程序立刻就退出了,去跑 Modbus 了。

第二步:轮询检查(Logic_Update)

你的 main 循环跑得极快(一秒钟转几万圈)。每一圈都会跑进 Handle_LED_Task()

如果现在是亮灯后的第 10 毫秒:

  • 程序进入 case LED_ON_WAIT
  • 检查:HAL_GetTick() - led_timer > 500?(现在的时间减去亮灯的时间,够 500ms 了吗?)
  • 结果:不够。程序直接跳出 switch。CPU 去干别的事了(比如处理 ADC 采样)。

如果现在是亮灯后的第 501 毫秒:

  • 程序再次进入 case LED_ON_WAIT
  • 检查:够 500ms 了!
  • 动作
    1. HAL_GPIO_WritePin(… , 1); —— “把灯关了”。
    2. led_timer = HAL_GetTick(); —— “重新设闹钟,记下关灯的这一刻”。
    3. led_step = LED_OFF_WAIT; —— “备忘录:改成灭灯等待阶段”。

4. 最终的 Logic_Update 样子

你会发现 HAL_Delay 彻底消失了,取而代之的是不断地“看表”。

 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
/**
 * @brief 逻辑模块主轮询函数
 */
void Logic_Update(void) {
    // --- 1. ADC 信号采集与一阶滞后滤波 ---
    uint32_t sum_A = 0;
    for(int i = 0; i < 10; i++) {
        sum_A += adc_buffer[i * 2]; 
    }
    float raw_volt = (float)sum_A / 10.0f * 3.3f / 4095.0f;
    filter_volt_A = Low_Pass_Filter(raw_volt, filter_volt_A, 0.15f);

    // --- 2. 物理量计算 ---
    current_mA = (filter_volt_A / R_SAMPLE) * 1000.0f; 
    calibrated_pressure = (filter_volt_A - zero_offset_volt) * slope_k;

    if (calibrated_pressure < 0.0f) calibrated_pressure = 0.0f;

    // --- 3. 零点校准逻辑 (PB0) ---
    uint8_t pin0 = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);
    if (pin0 == GPIO_PIN_RESET) {
        if (key0_tick == 0) key0_tick = HAL_GetTick();
        else if (HAL_GetTick() - key0_tick > 20) {
            if (key0_active == 0) {
                zero_offset_volt = filter_volt_A;
                Save_Params_To_Flash(zero_offset_volt, slope_k);
                Start_LED_Feedback(1); 
                key0_active = 1;       
            }
        }
    } else {
        key0_tick = 0;
        key0_active = 0;
    }

    // --- 4. 满量程校准逻辑 (PB1) ---
    uint8_t pin1 = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1);
    if (pin1 == GPIO_PIN_RESET) {
        if (key1_tick == 0) key1_tick = HAL_GetTick();
        else if (HAL_GetTick() - key1_tick > 20) {
            if (key1_active == 0) {
                float v_diff = filter_volt_A - zero_offset_volt;
                if (v_diff > 0.1f) {
                    slope_k = P_NOMINAL / v_diff;
                    Save_Params_To_Flash(zero_offset_volt, slope_k);
                    Start_LED_Feedback(4); 
                } else {
                    Start_LED_Feedback(1);
                }
                key1_active = 1; 
            }
        }
    } else {
        key1_tick = 0;
        key1_active = 0;
    }

    // 【新增重要步骤】驱动 LED 状态机运行
    // 只有调用了它,Start_LED_Feedback 下达的闪烁命令才会被执行
    Handle_LED_Task();

    // --- 5. 同步数据到 Modbus 寄存器仓库 ---
    // 这些代码现在回到了 Logic_Update 的怀抱中
    Modbus_Regs[0] = (uint16_t)(current_mA * 100);      
    Modbus_Regs[1] = (uint16_t)(calibrated_pressure * 1000); 
    Modbus_Regs[2] = (uint16_t)(zero_offset_volt * 1000);    
    Modbus_Regs[3] = (uint16_t)(slope_k * 1000);        
} // 函数在这里结束
Licensed under CC BY-NC-SA 4.0