From 30540d884d483e33efa152397a6b506c911ed302 Mon Sep 17 00:00:00 2001 From: hulk Date: Sat, 10 Oct 2026 10:04:48 +0800 Subject: [PATCH] Heater PID closed-loop on PB12 (HEAT_EN) with safety protections --- AGENTS.md | 17 +++-- CMakeLists.txt | 1 + Inc/board_config.h | 15 ++++ Inc/heat.h | 47 +++++++++++++ Src/command.c | 49 +++++-------- Src/heat.c | 171 +++++++++++++++++++++++++++++++++++++++++++++ Src/main.c | 5 +- protocols.md | 11 +-- 8 files changed, 276 insertions(+), 40 deletions(-) create mode 100644 Inc/heat.h create mode 100644 Src/heat.c diff --git a/AGENTS.md b/AGENTS.md index 087beb1..51bb7c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,8 +69,15 @@ - **命令协议 v1.0 定稿**(权威定义见根目录 `protocols.md`,帧 hex 全部脚本生成): - M1 查温度 / M2 查上报状态 / M2S 上报间隔+开关(M2S0 停)/ M3 加热开关 (M3S1/M3S0/M3 查询)/ M4 目标温度(M4S/M4 查询)/ M888 / M999 / M9999 - - M3/M4 为**预留**:仅维护软件状态(ack+可回读),真实加热硬件执行体待接入 - (`command.c` 的 `heater_apply()` 桩);GUI 可先行全量联调协议 +- **加热驱动 + PID 闭环实装**(`heat.h/heat.c`,PB12=HEAT_EN): + - 执行器:慢 PWM(载波 1s、占空 10% 步进 → 脉宽恒 >=100ms、开关频率恒 1Hz, + 满足 2N7002K→VBE2412 PMOS 驱动电路 <=10Hz 约束) + - PID:500ms 周期(NTC 采样 100ms),位置式 + 积分限幅 + 输出量化 10% 档; + 参数 Kp=8/Ki=0.05/Kd=0(初值,宏在 board_config.h,待整定) + - 安全:上电默认关;NTC 故障(OPEN/SHRT)强制关断;过温 150℃ 滞回保护; + 未使能关断;使能切换/故障时复位 PID 积分 + - 实测 30℃ 闭环(NTC 贴 PTC 鳍片):30s 爬升、超调 1.4℃(7%)、60s 收敛中 + - M3/M4 已从软件桩切到 heat 模块(协议帧不变) - 协议要点:全幂等(ack 丢可重发)、响应超时建议 300ms×3 次、温度一律 ASCII - **专用 GUI 上位机 v0.1.0 完成**(`GUI/`,参照 DLPilot 分层:协议模块纯标准库 + PySide6-Fluent 界面 + 自检 + PyInstaller): @@ -107,8 +114,10 @@ - [x] **NTC 测温上板验证**:NTC 支路修复后读数正常(23.6~25.9℃ 稳定)。 - [ ] **B 值确认**:温度计对照实测定 `NTC_B_VALUE`(3950/3435 二选一)。 - [ ] **适配器更换**:换可靠的 USB-RS485 适配器(当前自动方向切换间歇丢响应)。 -- [ ] **加热驱动**:加热功率器件驱动与控制(MOS/PWM/继电器方案待定), - 接入 `command.c` 的 `heater_apply()` 桩;随后控温闭环(PID)。 +- [x] **加热驱动 + PID 闭环**(PB12 慢 PWM 1s/10% 步进 + PID 500ms 周期, + 安全保护齐全;实测 30℃ 闭环通过:30s 爬升、超调 1.4℃、60s 收敛中)。 +- [ ] **PID 参数整定**:初值 Kp=8/Ki=0.05/Kd=0(board_config.h 宏),按真实 + 工况温度(80~120℃)与负载重新整定,抑制 ~7% 超调。 - [x] **专用 GUI 上位机 v0.1.0**:`GUI/` 已完成并通过 COM5 真机全链路验证 (协议按 `protocols.md`;遗留见其 README"已知事项":应用图标、加热硬件 接入后的闭环联调、固件 M3/M4 硬件执行体实现)。 diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a2072d..a6a7599 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,7 @@ set(TARGET_SRC Src/ntc.c Src/command.c Src/pwr_hold.c + Src/heat.c # TODO: 待从 GD32E23x 工程移植后启用 # Src/i2c.c diff --git a/Inc/board_config.h b/Inc/board_config.h index 7f1d6a1..6743e77 100644 --- a/Inc/board_config.h +++ b/Inc/board_config.h @@ -143,6 +143,21 @@ void usart1_irq_handler(void); #define PWR_HOLD_PORT GPIOA #define PWR_HOLD_PIN GPIO_PIN_11 +/* >>>>>>>>>>>>>>>>>>>>[HEATER OUTPUT(加热驱动 PB12=HEAT_EN)]<<<<<<<<<<<<<<<<<<<< */ +/* PB12:高 = 加热开,低 = 关(2N7002K 电平转换 → VBE2412 PMOS → 24V 加热回路)。 + 开关频率限制:慢 PWM 周期 1s、占空比 10% 步进——脉宽恒 >=100ms,开关频率恒 1Hz。 */ +#define HEATER_EN_RCU RCU_GPIOB +#define HEATER_EN_PORT GPIOB +#define HEATER_EN_PIN GPIO_PIN_12 +#define HEATER_PWM_PERIOD_MS 1000U /* PWM 载波周期(1s,恒 1Hz 开关频率) */ +#define HEATER_PWM_STEP_PCT 10U /* 占空比步进(每档脉宽 = 100ms) */ +#define HEATER_PID_PERIOD_MS 500U /* PID 计算周期 */ +#define HEATER_PID_KP 8.0f /* PID 参数:初值,需上板实测整定 */ +#define HEATER_PID_KI 0.05f +#define HEATER_PID_KD 0.0f /* NTC 噪声对微分放大,先禁用 */ +#define HEATER_OVERTEMP_C 150.0f /* 过温强制关断阈值(滞回保护) */ +#define HEATER_OVERTEMP_HYST 5.0f /* 过温恢复滞回 */ + /******************************************************************************/ #define DEBUG_UART USART1 diff --git a/Inc/heat.h b/Inc/heat.h new file mode 100644 index 0000000..4d0c88d --- /dev/null +++ b/Inc/heat.h @@ -0,0 +1,47 @@ +#ifndef HEAT_H +#define HEAT_H + +#include "gd32f4xx.h" +#include "board_config.h" +#include + +/** + * @brief 初始化加热输出(PB12 推挽输出低 = 加热关)。 + */ +void heat_init(void); + +/** + * @brief 加热控制任务(在主循环中周期调用,主循环周期 10ms)。 + * @details 内部包含:慢 PWM 载波(1s 周期、10% 步进,脉宽恒 >=100ms)、 + * PID 计算(500ms 周期,输出量化到占空档位)、安全保护 + * (传感器故障强制关断 / 过温滞回保护 / 未使能强制关断)。 + */ +void heat_task(void); + +/** + * @brief 使能/关闭加热(对应 M3S1/M3S0)。 + * @note 关闭时立即输出低并复位 PID 积分。 + */ +void heat_set_enable(bool on); + +/** + * @brief 查询加热使能状态。 + */ +bool heat_get_enable(void); + +/** + * @brief 设定目标温度(摄氏度,对应 M4S)。 + */ +void heat_set_target(float temp_c); + +/** + * @brief 查询目标温度(摄氏度)。 + */ +float heat_get_target(void); + +/** + * @brief 查询当前量化占空比档位(0~10,对应 0~100%,10% 步进)。 + */ +uint8_t heat_get_duty(void); + +#endif // HEAT_H diff --git a/Src/command.c b/Src/command.c index e1fd9d5..bffdc08 100644 --- a/Src/command.c +++ b/Src/command.c @@ -16,6 +16,7 @@ #include "uart.h" #include "systick.h" #include "ntc.h" +#include "heat.h" #include "board_config.h" #include #include @@ -68,22 +69,10 @@ static uint16_t s_temp_report_period_ms = 500; /**< 上报间隔(ms),100~6 static uint16_t s_temp_report_counter = 0; /**< 主循环 10ms 圈计数 */ /** @} */ -/** @name M3/M4 加热控制状态(预留:当前仅软件状态,硬件执行体待接入) +/** @name M3/M4 加热控制(heat 模块:PB12 慢 PWM + PID 闭环) * @{ */ -static bool s_heater_on = false; /**< 加热开关状态 */ -static float s_heater_target = 0.0f; /**< 目标温度(摄氏度,M4S 设定) */ /** @} */ -/** - * @brief 加热输出执行体(按开关状态与目标温度驱动加热器件)。 - * @note TODO: 加热驱动(MOS/PWM/继电器)接入后在此实现真实输出; - * 当前 M3/M4 仅维护软件状态(GUI 可先行联调协议)。 - */ -static void heater_apply(void) -{ - /* 占位:接入加热驱动后替换为真实硬件输出 */ -} - void system_software_reset(void) { // 确保所有待处理的内存访问完成 @@ -354,10 +343,10 @@ static bool __attribute__((unused)) find_parameter_value(const uint8_t *cmd, uin * - M1: 单次查询温度值(payload 为 ASCII 摄氏度,如 "23.8";OPEN/SHRT=故障) * - M2: 查询连续上报状态(payload: "ON " 或 "OFF") * - M2S: 设置连续上报间隔(100~60000)并开启;M2S0 停止 - * - M3: 查询加热状态(payload: "ON" / "OFF") - * - M3S1/M3S0: 开启/关闭加热(预留,当前仅软件状态) + * - M3: 查询加热使能(payload: "ON" / "OFF") + * - M3S1/M3S0: 开启/关闭加热(PB12 慢 PWM,PID 按目标温度闭环) * - M4: 查询目标温度(payload: ASCII 摄氏度) - * - M4S: 设定目标温度(整数摄氏度,0~200;预留,当前仅软件状态) + * - M4S: 设定目标温度(整数摄氏度,0~200) * - M888: 软件复位 * - M999: 输出固件版本号(v<板卡码>.<主>.<次>.<修订>) * - M9999: 进入 OTA 模式(复位进 Bootloader) @@ -466,11 +455,11 @@ void handle_command(const uint8_t *frame, uint8_t len) { return; /* ========================================== - * M3 加热开关(预留,当前仅软件状态) - * M3 : 查询加热状态(payload: "ON" / "OFF") - * M3S1 : 开启加热 - * M3S0 : 关闭加热 - * 注意:上电默认关闭;看门狗/异常处理策略待控温闭环设计时补充。 + * M3 加热开关(PB12 慢 PWM 驱动 + PID 闭环) + * M3 : 查询加热使能(payload: "ON" / "OFF") + * M3S1 : 开启加热(使能后由 PID 按目标温度闭环输出) + * M3S0 : 关闭加热(立即断输出并复位 PID) + * 安全:上电默认关闭;NTC 故障/过温时强制关断。 * ========================================== */ case 3u: { @@ -478,23 +467,22 @@ void handle_command(const uint8_t *frame, uint8_t len) { if (find_parameter_value(&cmd[cmd_index], (uint8_t)(cmd_len - cmd_index), 'S', &onoff)) { if (onoff == 0U || onoff == 1U) { - s_heater_on = (onoff == 1U); - heater_apply(); + heat_set_enable(onoff == 1U); send_response(RESP_TYPE_OK, s_report_status_ok, sizeof(s_report_status_ok)); } else { send_response(RESP_TYPE_PARAM_ERR, s_report_status_err, sizeof(s_report_status_err)); } } else { send_response(RESP_TYPE_OK, - s_heater_on ? (const uint8_t *)"ON" : (const uint8_t *)"OFF", - s_heater_on ? 2U : 3U); + heat_get_enable() ? (const uint8_t *)"ON" : (const uint8_t *)"OFF", + heat_get_enable() ? 2U : 3U); } - COMMAND_DEBUG("M3 heater %s", s_heater_on ? "ON" : "OFF"); + COMMAND_DEBUG("M3 heater %s", heat_get_enable() ? "ON" : "OFF"); } return; /* ========================================== - * M4 温度设定(预留,当前仅软件状态) + * M4 温度设定(PID 目标值) * M4 : 查询目标温度(payload: ASCII 摄氏度,如 "120.0") * M4S : 设定目标温度(整数摄氏度,HEATER_TEMP_MIN~MAX) * ========================================== */ @@ -504,18 +492,17 @@ void handle_command(const uint8_t *frame, uint8_t len) { if (find_parameter_value(&cmd[cmd_index], (uint8_t)(cmd_len - cmd_index), 'S', &target)) { if (target >= HEATER_TEMP_MIN && target <= HEATER_TEMP_MAX) { - s_heater_target = (float)target; - heater_apply(); + heat_set_target((float)target); send_response(RESP_TYPE_OK, s_report_status_ok, sizeof(s_report_status_ok)); } else { send_response(RESP_TYPE_PARAM_ERR, s_report_status_err, sizeof(s_report_status_err)); } } else { char target_str[8]; - uint8_t n = (uint8_t)sprintf(target_str, "%.1f", s_heater_target); + uint8_t n = (uint8_t)sprintf(target_str, "%.1f", heat_get_target()); send_response(RESP_TYPE_OK, (const uint8_t *)target_str, n); } - COMMAND_DEBUG("M4 target: %.1f", s_heater_target); + COMMAND_DEBUG("M4 target: %.1f", heat_get_target()); } return; diff --git a/Src/heat.c b/Src/heat.c new file mode 100644 index 0000000..9efbec5 --- /dev/null +++ b/Src/heat.c @@ -0,0 +1,171 @@ +/** + * @file heat.c + * @brief 加热控制:慢 PWM 驱动(PB12)+ PID 控温闭环 + 安全保护。 + * @details 硬件:PB12=HEAT_EN,高 = 加热开(2N7002K 电平转换 → VBE2412 PMOS → 24V)。 + * 开关频率约束(<=10Hz)由"1s 载波周期 + 10% 占空步进"保证——每档脉宽 + * 恒为 100ms 的整数倍。PID 以 500ms 周期计算(NTC 后台采样 100ms), + * 输出量化到 0~10 档占空。 + * @note 安全保护:上电默认关闭;NTC 传感器故障(OPEN/SHRT)强制关断; + * 过温(>= HEATER_OVERTEMP_C)滞回保护;未使能强制关断。 + */ + +#include "heat.h" +#include "ntc.h" +#include "gd32f4xx_rcu.h" +#include "gd32f4xx_gpio.h" +#include "board_config.h" + +/******************************************************************************/ +/* 内部状态 */ + +static bool s_enable = false; /* 加热使能(M3) */ +static float s_target = 0.0f; /* 目标温度(M4) */ +static uint8_t s_duty10 = 0; /* 量化占空档位 0~10(x10%) */ +static bool s_overtemp = false; /* 过温锁存(滞回恢复) */ +static float s_integral = 0.0f; /* PID 积分项 */ +static float s_prev_err = 0.0f; /* PID 上次误差 */ +static uint16_t s_pwm_phase_ms = 0; /* PWM 载波相位 0..PERIOD-1 */ +static uint16_t s_pid_divider = 0; /* PID 周期分频(10ms 圈) */ + +/******************************************************************************/ +/* 底层输出 */ + +static void heat_out(bool on); + +static void heat_out(bool on) { + if (on) { + gpio_bit_set(HEATER_EN_PORT, HEATER_EN_PIN); + } else { + gpio_bit_reset(HEATER_EN_PORT, HEATER_EN_PIN); + } +} + +void heat_init(void) { + /* PB12 推挽输出低:上电默认加热关 */ + rcu_periph_clock_enable(HEATER_EN_RCU); + gpio_mode_set(HEATER_EN_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, HEATER_EN_PIN); + gpio_output_options_set(HEATER_EN_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, HEATER_EN_PIN); + heat_out(false); +} + +/******************************************************************************/ +/* PID */ + +/** + * @brief 复位 PID 积分与历史误差(使能切换/传感器故障时调用,防积分残留冲击)。 + */ +static void pid_reset(void) { + s_integral = 0.0f; + s_prev_err = 0.0f; +} + +/** + * @brief PID 计算(500ms 周期):误差 -> 占空档位 s_duty10(0~10)。 + */ +static void pid_compute(void) { + float t = ntc_get_temperature(); + + /* 传感器故障(开路/短路):安全第一,强制关断并复位 PID */ + if (t <= -273.0f) { + s_duty10 = 0; + pid_reset(); + return; + } + + /* 未使能:强制关断 */ + if (!s_enable) { + s_duty10 = 0; + pid_reset(); + return; + } + + /* 过温滞回保护:达到阈值锁存关断,降回阈值-滞回量后恢复 PID */ + if (t >= HEATER_OVERTEMP_C) { + s_overtemp = true; + } else if (t <= (HEATER_OVERTEMP_C - HEATER_OVERTEMP_HYST)) { + s_overtemp = false; + } + if (s_overtemp) { + s_duty10 = 0; + pid_reset(); + return; + } + + /* 位置式 PID */ + float err = s_target - t; + s_integral += err; + { + /* 积分限幅(抗饱和):积分项贡献不超过满量程 */ + float imax = 100.0f / HEATER_PID_KI; + if (s_integral > imax) s_integral = imax; + if (s_integral < -imax) s_integral = -imax; + } + float deriv = err - s_prev_err; + s_prev_err = err; + + float u = HEATER_PID_KP * err + HEATER_PID_KI * s_integral + HEATER_PID_KD * deriv; + if (u < 0.0f) u = 0.0f; /* 降温靠自然散热,输出不为负 */ + if (u > 100.0f) u = 100.0f; + + /* 输出量化到 10% 档位(四舍五入) */ + s_duty10 = (uint8_t)((u + (float)HEATER_PWM_STEP_PCT / 2.0f) / (float)HEATER_PWM_STEP_PCT); +} + +/******************************************************************************/ +/* 公共接口 */ + +void heat_task(void) { + uint8_t prev_duty = s_duty10; + + /* PWM 载波相位推进(10ms tick) */ + s_pwm_phase_ms += 10U; + if (s_pwm_phase_ms >= HEATER_PWM_PERIOD_MS) { + s_pwm_phase_ms = 0U; + } + + /* PID 周期分频 */ + s_pid_divider += 10U; + if (s_pid_divider >= HEATER_PID_PERIOD_MS) { + s_pid_divider = 0U; + pid_compute(); + if (s_duty10 != prev_duty) { + s_pwm_phase_ms = 0U; /* 占空变更:从新周期起点开始,响应即时 */ + } + } + + /* 按占空档位输出:0 档常关、10 档常开、其余按相位斩波 */ + uint16_t on_ms = (uint16_t)s_duty10 * (HEATER_PWM_PERIOD_MS / 10U); + if (s_duty10 == 0U) { + heat_out(false); + } else if (s_duty10 >= 10U) { + heat_out(true); + } else { + heat_out(s_pwm_phase_ms < on_ms); + } +} + +void heat_set_enable(bool on) { + if (s_enable == on) return; /* 幂等 */ + s_enable = on; + if (!on) { + s_duty10 = 0; + pid_reset(); + heat_out(false); + } +} + +bool heat_get_enable(void) { + return s_enable; +} + +void heat_set_target(float temp_c) { + s_target = temp_c; +} + +float heat_get_target(void) { + return s_target; +} + +uint8_t heat_get_duty(void) { + return s_duty10; +} diff --git a/Src/main.c b/Src/main.c index 7ab53a3..d9596f4 100644 --- a/Src/main.c +++ b/Src/main.c @@ -13,6 +13,7 @@ #include "rs485.h" #include "command.h" #include "ntc.h" +#include "heat.h" #include "led.h" #include "pwr_hold.h" #include @@ -38,6 +39,7 @@ int main(void) rs485_init(); ntc_init(); + heat_init(); #if DEBUG_MODE == CFG_ENABLE printf("FilmForge FW %s, built %s\r\n", PROJECT_VERSION, BUILD_DATE); @@ -52,8 +54,9 @@ int main(void) /* RS485 数据口命令处理(D5 帧协议,具体业务命令在 command.c 扩展) */ command_process(); - /* NTC 后台采样(100ms 一次)+ M2 连续上报任务 */ + /* NTC 后台采样(100ms 一次)+ 加热 PID/PWM 任务 + M2 连续上报任务 */ ntc_sample_task(); + heat_task(); command_report_task(); /* debug 串口周期打印 NTC 温度(约 1s 一次,读后台采样全局值) */ diff --git a/protocols.md b/protocols.md index 605dd8a..52c8b65 100644 --- a/protocols.md +++ b/protocols.md @@ -61,13 +61,16 @@ GUI 曲线解析:匹配 `B5 F0`,取 payload 按 ASCII float 解析;payload - 后台每 100ms 采样一次(16 次采样修剪均值滤波),M1/M2/上报读同一全局最新值 - 单次测量耗时约 10~20ms,查询响应延迟 < 主循环周期(10ms 级) -## 加热控制(M3/M4,预留) +## 加热控制(M3/M4) -- **当前固件仅维护软件状态**(M3S1/M4S 设定会 ack 并可查询回读), - 真实加热硬件驱动尚未接入;GUI 可先行完成全部协议联调 +- PB12 = HEAT_EN:高 = 加热开(2N7002K 电平转换 → VBE2412 PMOS → 24V 回路) +- **PID 闭环**:使能后固件按目标温度自动控温;执行器为慢 PWM + (载波 1s、占空比 10% 步进,开关频率恒 1Hz,满足驱动电路 <=10Hz 约束) +- 安全保护:上电默认关闭;NTC 传感器故障(OPEN/SHRT)强制关断; + 过温 >=150℃ 滞回保护(降至 145℃ 以下恢复) +- M3 查询的是**使能状态**;当前占空比暂无查询命令(可从温度曲线间接观察) - 目标温度范围 0~200℃(整数),越界回 `FD` - 上电默认:加热 OFF,目标 0.0℃ -- 后续接入硬件执行体后协议不变 ## 通信注意事项