前言
本篇继 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 认识你的新家:
- 在项目根目录下右键 -> New -> Folder,命名为
User_Library(或者你喜欢的名字)。
- 在
User_Library 下创建两个文件夹:Tools 和 BSP。
- 告知编译器路径:
- 右键工程 -> Properties。
- C/C++ Build -> Settings -> MCU GCC Compiler -> Include paths。
- 点击“+”,点击 Workspace,选中
User_Library/Tools 和 User_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 … |
注意事项与“潜规则”
- Include Paths 的非递归性:
- 如果你有文件夹
User_Library,下面有子文件夹 App 和 Tools。
- 你只添加
User_Library 是没用的。你必须分别添加 User_Library/App 和 User_Library/Tools。
- 优先使用相对路径:
- 千万不要写成
D:\Desktop\Project\…。
- 应该使用
${ProjDirPath}/Core/User_Library/… 或者使用 ../ 开头的相对路径。这样项目发给别人也能直接编译。
- Source Folder 的图标:
- 检查你的文件夹图标。如果图标右上角有 蓝色小方块,说明它已在
Source Location 中;如果是纯黄色文件夹,说明它不会被编译。
- 修改后记得 Clean:
- 改动这些路径设置后,IDE 的索引有时会卡住。最稳妥的操作是 Project -> Clean,然后重新 Build。
这是最简单的模块,因为它不依赖具体的硬件引脚,只进行数学运算。
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 了!
- 动作:
HAL_GPIO_WritePin(… , 1); —— “把灯关了”。
led_timer = HAL_GetTick(); —— “重新设闹钟,记下关灯的这一刻”。
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);
} // 函数在这里结束
|