commit d275a3523b215724af450d2e359344739e11cf52 Author: 十七 <1415613786@qq.com> Date: Mon Nov 2 20:30:08 2026 +0800 feat: add STM8 mesh relay firmware diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7ae379 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# macOS 文件系统元数据 +.DS_Store + +# SDCC/Make 生成的中间文件和固件产物 +build/ diff --git a/CODEX_FIRMWARE_HANDOFF.md b/CODEX_FIRMWARE_HANDOFF.md new file mode 100644 index 0000000..4f65d4e --- /dev/null +++ b/CODEX_FIRMWARE_HANDOFF.md @@ -0,0 +1,366 @@ +# STM8S003F3P6TR 蓝牙 Mesh 继电器固件开发交接文档 + +## 1. 文档目的 + +本文件供后续 Codex 固件开发任务使用。开发人员必须以本文件和 `PROGRAM_DESIGN.md` 为当前设计基线,不得擅自修改已经确认的通信协议、故障码和安全策略。 + +当前阶段只完成了固件设计,尚未创建 STM8 工程、尚未编译、尚未烧录,也尚未进行实物或市电测试。 + +## 2. 目标功能 + +固件运行于 `STM8S003F3P6TR`,通过 `JDY-28M` 的 Mesh 指令模式实现: + +- 单路继电器吸合/断开控制; +- Mesh 短地址查询和广播发现; +- ACS712ELCTR-30A 实际电流计算; +- 吸合无电流、断开有电流故障检测; +- 故障恢复主动上报; +- 蓝牙初始化和 Mesh 组网 LED 指示; +- 电流阈值运行期配置并保存到 ROM。 + +## 3. 已确认硬件连接 + +| 功能 | MCU/网络 | 固定要求 | +|---|---|---| +| 继电器控制 | `PA1_relay` | `PA1=1` 吸合,`PA1=0` 断开 | +| ACS712 电流 ADC | `PC4` | 模拟输入,计算实际电流,不使用 ADC 原始值直接上报 | +| JDY-28M Mesh 状态 | `PC3_JDY-28M-STAT` | 高电平表示已入网,低电平表示未入网 | +| LED3 | `PD3` | 高电平点亮 | +| JDY-28M UART TX | `PD5_JDY-28M-RXD` | MCU TX 连接模块 RXD | +| JDY-28M UART RX | `PD6_JDY-28M-TXD` | MCU RX 连接模块 TXD | +| 下载调试 | `SWIM/NRST` | 用于 SWIM 下载和调试 | + +JDY-28M 运行模式必须是 `AT+ROLE2`,即 MESH 指令通信模式。 + +## 4. 上电初始化流程 + +上电后先将继电器设置为断开,然后按以下状态机执行: + +```text +初始化 GPIO、UART、定时器、ADC、看门狗 +→ PA1_relay=0 +→ 查询 AT+ROLE +→ 如果不是 ROLE2,发送 AT+ROLE2 +→ 等待 +OK +→ 发送 AT+RESET +→ 等待模块重启 +→ 再次查询 AT+ROLE +→ 发送 AT+MADDR +→ 解析 2 字节短地址并保存到运行期 RAM +→ 初始化 ACS712 零点 +→ 进入业务运行态 +``` + +要求: + +- 短地址不写 EEPROM/ROM,每次上电都从 JDY-28M 重新查询; +- `AT+ROLE2`、`AT+MADDR` 必须有超时和有限次数重试; +- 初始化失败时继电器保持断开; +- 初始化失败不能通过 Mesh 上报,因为 Mesh 通信本身不可用; +- 初始化失败由 LED3 以亮灭各 300 ms 指示; +- Mesh 未组网不是初始化故障,不因为 PC3 为低电平而强制断开继电器; +- PC3 必须连续稳定确认至少 3 秒后,才认为组网状态发生变化。 + +## 5. LED3 指示规则 + +状态优先级: + +```text +蓝牙初始化故障 > 未组网 > 已组网 +``` + +| 状态 | LED3 | +|---|---| +| 蓝牙初始化故障 | 亮 300 ms、灭 300 ms | +| 初始化完成但未组网 | 亮 1 s、灭 1 s | +| 已完成 Mesh 组网 | 常亮 | + +JDY-28M 的 STAT 依据模块手册第 7 页:MESH 模式下高电平表示入网成功,低电平表示未入网。 + +## 6. JDY-28M 外层 Mesh 帧 + +手册第 29 页给出了外层格式。 + +### 6.1 STM8/主机发送给模块 + +```text +41 54 2B AA A1 | 目标短地址 2B | A0 | 业务数据 | 0D 0A +``` + +- `41 54 2B AA A1`:固定 Mesh 指令头; +- 目标地址:2 字节; +- `FFFF`:广播; +- `A0`:固定 Mesh 数据通信标志; +- `0D 0A`:蓝牙帧结束符。 + +### 6.2 模块转发给 STM8 + +```text +F1 DD 09 00 0A | 源短地址 2B | 业务数据 | 0D 0A +``` + +- `F1 DD 09 00 0A`:固定接收帧头; +- 源短地址:2 字节; +- 业务数据:STM8 自定义业务帧; +- `0D 0A`:蓝牙帧结束符。 + +广播和单播格式相同,仅目标地址不同。整个 JDY-28M 外层数据包最大 20 字节。 + +## 7. STM8 业务帧 + +```text +0x55 0xAA | CMD | LEN | DATA | CRC16 +``` + +- `0x55 0xAA`:业务帧头; +- `CMD`:命令码; +- `LEN`:DATA 长度; +- `DATA`:命令数据; +- `CRC16`:CRC-16/MODBUS。 + +CRC-16/MODBUS 固定参数: + +- 初始值:`0xFFFF`; +- 右移实现多项式:`0xA001`; +- 输入、输出反转; +- CRC 低字节在前; +- 计算范围:从 `CMD` 到 DATA,不包含 `0x55 0xAA`; +- 不计算 JDY-28M 外层帧字段。 + +测试向量: + +```text +输入:01 01 01 +CRC:0x4498 +发送:98 44 +完整业务帧:55 AA 01 01 01 98 44 +``` + +## 8. 业务命令和应答 + +采用“请求 CMD + `0x80` = 应答 CMD”: + +| CMD | 含义 | DATA | +|---|---|---| +| `0x01` | 控制继电器 | `0x00` 断开,`0x01` 吸合 | +| `0x02` | 查询继电器状态 | 请求 DATA 为空,返回状态 | +| `0x03` | 读取电流 | 请求 DATA 为空,返回 2 字节电流 | +| `0x04` | 故障/恢复主动上报 | 1 字节故障码 | +| `0x05` | 查询/上报短地址 | 返回 2 字节短地址 | +| `0x06` | 配置电流阈值 | 2 字节阈值,低字节在前,单位 mA | +| `0x81` | 继电器控制成功应答 | 当前继电器状态 | +| `0x82` | 继电器状态应答 | `0x00/0x01` | +| `0x83` | 电流数据应答 | 2 字节实际电流 | +| `0x85` | 短地址应答 | 2 字节短地址 | +| `0x86` | 阈值配置成功应答 | 2 字节当前阈值 | +| `0x84` | 故障/恢复上报确认 | 1 字节,被确认的故障码 | +| `0xE0` | 通用错误应答 | 1 字节错误码 | + +### 8.1 控制应答 + +`CMD=0x01` 收到后,STM8 控制 PA1,并重新封装应答帧,使用 `CMD=0x81`,重新计算 CRC16,通过新的 JDY-28M 外层帧发送。不能直接原样返回主机原始 Mesh 帧。 + +控制应答只表示 STM8 已接受并发出了继电器控制动作,不表示负载电流已经正常。当前没有触点反馈,不单独实现继电器机械动作失败检测。 + +### 8.2 广播应答 + +广播地址查询和广播控制都需要应答: + +```text +delay_ms = 20 + (mesh_short_address % 16) * 20 +``` + +单播应答可以立即发送;广播应答使用短地址计算错峰时间,避免多个设备同时发送。 + +## 9. 电流采样和阈值 + +ACS712ELCTR-30A 由 5 V 供电,典型灵敏度约为 66 mV/A,零电流输出约为 VCC/2。原理图中 ACS712 的 VIOUT 经过 R3=10K、R4=10K 分压后接入 STM8 `PC4`,因此 ADC 输入电压约为 ACS712 VIOUT 的一半。 + +### 9.1 ADC 到电流的计算 + +假定 STM8 ADC 参考电压为 3.3 V、ADC 为 10 位: + +```text +V_ADC = ADC_CODE × 3.3 / 1023 +V_ACS = V_ADC × 2 +``` + +不能直接固定使用 2.5 V 零点,必须使用上电校准得到的 `ADC_ZERO`: + +```text +I_A = (ADC_CODE - ADC_ZERO) × 3.3 × 2 / (1023 × 0.066) +I_mA = (ADC_CODE - ADC_ZERO) × 3.3 × 2 × 1000 / (1023 × 0.066) +``` + +理论上约为: + +```text +I_mA ≈ (ADC_CODE - ADC_ZERO) × 97.7 +``` + +该比例只能作为初始值,正式程序应通过标准负载校准 `current_scale`,因为器件存在零点、灵敏度、温度和噪声误差。 + +### 9.2 上电零点校准 + +上电时继电器保持断开并确认负载没有电流,连续采集 200 ms 至 500 ms 的 ADC 样本并求平均: + +```text +ADC_ZERO = sum(ADC_SAMPLE[0..N-1]) / N +``` + +零点校准期间不能让负载通电,否则后续所有电流值都会产生偏移。 + +### 9.3 交流 RMS 计算 + +交流负载不能通过单次 ADC 值判断,必须先减去零点并计算 RMS: + +```text +adc_delta = ADC_CODE - ADC_ZERO +ADC_RMS = sqrt(sum(adc_delta × adc_delta) / SAMPLE_COUNT) +I_RMS_mA = ADC_RMS × current_scale +``` + +建议 ADC 采样频率为 1 kHz 至 2 kHz,RMS 窗口使用 100 ms。实时采样路径使用整数或定点运算,不使用浮点运算。 + +初始定点换算可以写成: + +```c +/* + * 根据 ADC RMS 值计算实际电流,返回单位为 mA。 + * 6600 表示 3.3 V × 1000,2 表示 ACS712 输出经过 1:1 分压, + * 1023 是 10 位 ADC 满量程,66 表示 66 mV/A 的典型灵敏度。 + */ +current_mA = adc_rms * 6600UL * 2UL / (1023UL * 66UL); +``` + +### 9.4 电流输出和阈值 + +- 上报的是计算后的实际电流,不是 ADC 原始值; +- 电流类型为 16 位无符号整数; +- 单位为 mA; +- 低字节在前; +- 采用四舍五入; +- 超过 65535 mA 视为超出 ACS712ELCTR-30A 设计范围; +- 默认电流阈值为 100 mA; +- `CMD=0x06` 可动态修改阈值; +- 阈值写入 ROM; +- 上电读取阈值失败或值无效时使用默认值 100 mA; +- 吸合无电流、断开有电流以及对应恢复都必须连续确认至少 10 秒。 + +## 10. 故障和恢复 + +| 故障码 | 含义 | +|---|---| +| `0x00` | 继电器吸合后持续无有效电流 | +| `0x01` | 继电器断开后仍持续存在电流 | +| `0x02` | ACS712 采样异常 | +| `0x04` | 电流故障恢复 | +| `0x06` | 采样异常恢复 | + +故障产生和恢复均需连续确认至少 10 秒: + +- 吸合后持续无有效电流 10 秒,上报 `0x00`; +- 断开后持续有电流 10 秒,上报 `0x01`; +- 电流故障恢复正常 10 秒,上报 `0x04`; +- 采样异常恢复正常 10 秒,上报 `0x06`。 + +故障或恢复事件需要主机确认: + +- 设备发送 `CMD=0x04` 后等待主机返回 `CMD=0x84`; +- `CMD=0x84` 的 DATA 为被确认的故障码; +- 未收到确认时,每隔 10 秒重发相同的故障或恢复上报帧; +- 收到匹配的 `0x84` 确认后,停止该事件的周期重发; +- 重复收到同一故障上报时,不重复改变继电器状态,只返回 `0x84`; +- 蓝牙初始化故障不能通过 Mesh 上报,只通过 LED3 指示。 + +蓝牙初始化异常使用 LED3 指示,不通过 Mesh 上报。 + +## 11. 错误处理 + +- CRC 错误:直接丢弃,不执行命令,不返回错误帧; +- 长度错误:返回 `CMD=0xE0`,错误码 `0x02`; +- 参数非法:返回 `CMD=0xE0`,错误码 `0x03`; +- ROM 写入失败:返回 `CMD=0xE0`,错误码 `0x05`; +- 未完成初始化:返回 `CMD=0xE0`,错误码 `0x01`; +- 看门狗复位后继电器默认断开。 + +## 12. 推荐代码分层 + +```text +drivers/ + gpio.c GPIO 配置和读写 + timer.c 1 ms 系统节拍 + uart1.c UART 中断和环形缓冲区 + adc.c PC4 ADC 采样 + eeprom.c 阈值 ROM 读写 + watchdog.c 独立看门狗 + +protocol/ + crc16_modbus.c + mesh_frame.c + business_frame.c + +services/ + bluetooth_init.c + relay_service.c + current_service.c + fault_service.c + led_service.c +``` + +所有新增函数、结构体、枚举、字段和复杂逻辑必须使用详细中文注释。主循环不得使用长时间阻塞延时,蓝牙初始化、LED 闪烁、电流确认和故障恢复都应使用状态机和 1 ms 系统节拍。 + +## 13. 分阶段实施要求 + +### 阶段一:工程和底层 + +完成 STM8 工程、GPIO、定时器、UART 环形缓冲区、ADC、ROM 和看门狗。要求能够编译通过。 + +### 阶段二:协议 + +完成 CRC-16/MODBUS、JDY-28M 外层帧解析、业务帧解析和错误处理。使用串口数据模拟测试,不接市电。 + +### 阶段三:蓝牙初始化 + +完成 `AT+ROLE`、`AT+ROLE2`、`AT+RESET`、`AT+MADDR` 状态机以及 LED3 状态指示。 + +### 阶段四:继电器业务 + +完成 `0x01` 至 `0x06` 命令、单播/广播应答和阈值 ROM 保存。 + +### 阶段五:ACS712 和故障 + +完成零点校准、RMS、电流换算、10 秒故障确认和 10 秒恢复确认。 + +未得到用户确认前,不自动跨阶段继续实现下一阶段。 + +## 14. 固定开发和烧录工具 + +- 编辑器:VS Code; +- 编译器:SDCC; +- 构建工具:Make; +- 烧录/在线调试工具:ST-LINK V2; +- 调试接口:SWIM,连接 STM8 的 SWIM/NRST/VCC/GND; +- 不以 STVD 工程作为主工程格式,主工程以可命令行执行的 Makefile 为准。 + +固件交付前至少需要完成: + +```text +make clean +make +生成 .ihx/.hex 文件 +使用 ST-LINK V2/SWIM 烧录 +``` + +## 15. 当前未授权假设 + +以下内容必须在实现前明确或以配置宏形式保留: + +- STM8 编译器和工程模板; +- PC4 对应的具体 ADC 通道编号; +- JDY-28M UART 的最终波特率; +- 外层 Mesh 20 字节限制在模块实际固件中的精确统计范围; +- 控制成功和阈值配置成功应答的具体 Mesh 发送目标地址; +- ROM 写入接口的具体芯片库实现。 + +不得把以上未确认内容硬编码成无法修改的业务逻辑。 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8a19154 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +# STM8S003F3P6TR 固件第一阶段构建文件。 +# 本阶段只验证工程结构和模块接口,硬件寄存器驱动将在后续阶段逐步接入。 + +PROJECT := mesh_relay +BUILD_DIR := build +SRC_DIR := src +INC_DIR := include + +CC := sdcc +OBJCOPY := packihx + +CFLAGS := -mstm8 --std-c99 --opt-code-size -I$(INC_DIR) +LDFLAGS := -mstm8 --out-fmt-ihx + +SOURCES := $(shell find $(SRC_DIR) -name '*.c' -print) +OBJECTS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.rel,$(SOURCES)) +IHX := $(BUILD_DIR)/$(PROJECT).ihx +HEX := $(BUILD_DIR)/$(PROJECT).hex + +.PHONY: all clean size + +all: $(HEX) + +$(HEX): $(IHX) + @mkdir -p $(dir $@) + $(OBJCOPY) $< > $@ + +$(IHX): $(OBJECTS) + @mkdir -p $(dir $@) + $(CC) $(LDFLAGS) -o $@ $(OBJECTS) + +$(BUILD_DIR)/%.rel: $(SRC_DIR)/%.c + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -c $< -o $@ + +size: $(IHX) + @ls -lh $(IHX) $(HEX) + +clean: + rm -rf $(BUILD_DIR) diff --git a/PROGRAM_DESIGN.md b/PROGRAM_DESIGN.md new file mode 100644 index 0000000..20e003e --- /dev/null +++ b/PROGRAM_DESIGN.md @@ -0,0 +1,556 @@ +# STM8S003F3P6TR 蓝牙 Mesh 继电器固件设计 + +## 1. 适用范围 + +本设计对应当前电路图中的单路交流通断器,功能包括: + +- 通过 JDY-28M 蓝牙模块接收 Mesh 控制指令; +- 控制 PA1 输出,驱动 SS8050 和继电器线圈; +- 通过 ACS712ELCTR-30A 连续采集负载电流; +- 继电器吸合后在规定时间内无电流,生成“吸合无电流”故障; +- 继电器断开后仍有电流,生成“断开有电流”故障; +- 通过蓝牙返回状态、实时电流和故障码。 + +## 2. 引脚连接确认 + +根据原理图重新核对后,ACS712 的 VIOUT 经 R3/R4/C2 滤波后连接到 STM8 的 `PC4`,用于电流 ADC 采样;`PC3_JDY-28M-STAT` 是 JDY-28M 的 STAT 状态信号,两者不是同一网络。因此,之前关于“ACS712 模拟输出与 JDY-28M 状态输出共用 PC3”的判断已撤销。 + +程序中应按以下方式配置: + +- `PC4`:模拟输入,连接 ACS712 电流检测滤波输出;禁止配置为推挽数字输出; +- `PC3`:数字输入,用于读取 JDY-28M 状态;不要把它当作 ACS712 ADC 通道; +- PC3 的有效电平已经由 JDY-28M 手册明确:MESH 模式下高电平表示入网成功,低电平表示尚未入网。 + +## 3. 推荐软件接口 + +| 功能 | 原理图网络 | 固件用途 | +|---|---|---| +| 继电器驱动 | `PA1_relay` | 高电平吸合,低电平断开 | +| 电流模拟量 | `PC4` | ADC 输入,连接 ACS712 滤波输出 | +| 蓝牙状态 | `PC3_JDY-28M-STAT` | 数字输入,读取 JDY-28M 状态 | +| 蓝牙接收 | `PD5_JDY-28M-RXD` | MCU UART TX,发送给 JDY-28M RXD | +| 蓝牙发送 | `PD6_JDY-28M-TXD` | MCU UART RX,接收 JDY-28M TXD | +| 调试下载 | `SWIM/NRST` | 仅用于 SWIM 调试和下载 | +| 本地按键 | `SW1` | 短按切换,长按恢复出厂设置 | + +STM8S003F3 的 ADC 是 10 位,输入范围为 0 至 VDDA;UART1 可用于蓝牙串口通信。根据 ST 官方 STM8S003F3 数据手册 DS7147 Rev 10 的表 6,TSSOP20/UFQFPN20 封装的 PC4 标注为 `AIN2`,因此程序选择 ADC1 通道 2;PC4 仍必须配置为模拟输入,不能作为数字推挽输出。 + +## 4. JDY-28M Mesh 使用方式 + +根据 JDY-28M V1.4 手册,本项目采用 `AT+ROLE2` 的 MESH 指令通信模式。该模式支持目标地址和 CMD 指令,通信数据带有模块定义的数据头,适合本项目的设备寻址、命令确认和故障上报。 + +JDY-28M 仍然只负责 Mesh 组网和无线转发,STM8 负责继电器、电流采样、故障判定和最终执行。不能让 JDY-28M 直接通过 OUT 引脚控制继电器,否则会绕过 STM8 的 ACS712 电流确认逻辑。 + +### 4.1 上电自动确认 Mesh 指令模式 + +STM8 上电后必须先保持继电器断开,再初始化 JDY-28M。启动流程如下: + +```text +上电 + ↓ +继电器强制断开,启动 UART 接收环形缓冲区 + ↓ +进入模块串口指令窗口,发送 AT+ROLE + ↓ +收到 +ROLE=2? ── 是 ──→ 发送 AT+MADDR 查询短地址 + │ + 否 + ↓ +发送 AT+ROLE2 + ↓ +等待 +OK,发送 AT+RESET + ↓ +等待模块重新启动,再次发送 AT+ROLE 验证 + ↓ +收到 +MADDR=<短地址> + ↓ +校验短地址并保存到运行期 RAM + ↓ +Mesh 初始化完成,允许继电器业务运行 +``` + +程序要求: + +- `AT+ROLE` 返回值必须严格等于 `2`,不能只根据 `+OK` 判断成功; +- 发送 `AT+ROLE2` 后应等待模块返回 `+OK`,再发送 `AT+RESET`,不要连续发送多条命令; +- 模块复位期间继电器必须保持断开,UART 接收缓存应清空,避免把启动回显当作业务帧; +- 复位后要重新查询并验证 `AT+ROLE`,不能假定设置命令已经持久化成功; +- 只有成功收到并解析 `AT+MADDR` 返回的有效 2 字节短地址后,才允许把 Mesh 初始化标记为完成;短地址只保存在运行期 RAM 中,每次上电重新查询,不写入 EEPROM; +- 如果连续重试仍无法确认模式或短地址,进入 `BT_INIT_FAULT`,继电器保持断开,并通过 LED/故障状态报告蓝牙初始化故障;Mesh 尚未组网本身不属于初始化故障,不强制断开继电器; +- 短地址不写入 EEPROM,设备每次上电都从 JDY-28M 重新查询。 + +JDY-28M 手册说明,MESH 指令模式相关配置需要注意模块的指令/透传状态和 PWRC 管脚条件。实际硬件上应确认 `PC5_JDY-28M-PWRC` 的上电电平;如果模块未进入指令窗口,STM8 应先按手册要求控制 PWRC,再发送 `AT+ROLE`。 + +建议的初始化状态枚举如下: + +```c +typedef enum +{ + BT_INIT_START = 0, /* 初始化开始,继电器仍保持断开 */ + BT_INIT_QUERY_ROLE, /* 查询 JDY-28M 当前工作模式 */ + BT_INIT_SET_ROLE, /* 当前模式不是 ROLE2,发送 AT+ROLE2 */ + BT_INIT_RESET_WAIT, /* 等待模块复位并重新启动 */ + BT_INIT_QUERY_ADDRESS, /* 发送 AT+MADDR 查询 Mesh 短地址 */ + BT_INIT_SAVE_ADDRESS, /* 校验地址并保存到运行期 RAM */ + BT_INIT_READY, /* 蓝牙初始化成功,允许进入业务状态 */ + BT_INIT_FAULT /* 初始化失败,继电器保持断开 */ +} BluetoothInitState; + +/* + * 蓝牙模块初始化任务。 + * 该任务必须采用超时和重试机制,不能使用长时间阻塞延时, + * 否则会影响 ACS712 采样、看门狗服务和本地按键检测。 + */ +void Bluetooth_InitTask(void) +{ + /* 只有收到明确的 +ROLE=2 和有效 +MADDR 响应后才进入 READY。 */ +} +``` + +短地址只保存在运行期 RAM 中。上电时必须重新执行 `AT+MADDR` 查询并校验,查询失败不能使用旧地址替代。 + +### 4.2 LED3 蓝牙状态指示 + +LED3 由 STM8 的 `PD3` 控制,`PC3_JDY-28M-STAT` 用于读取 JDY-28M 的 Mesh 状态。蓝牙初始化完成后,程序需要区分“模块初始化成功”和“已经完成组网”两个状态,不能仅凭 `AT+ROLE2` 成功就认为已经入网。 + +LED3 显示规则如下: + +| 条件 | LED3 状态 | 说明 | +|---|---|---| +| 蓝牙模块初始化故障 | 300 ms 亮、300 ms 灭 | 最高优先级,表示 ROLE/RESET/MADDR 初始化失败 | +| 初始化成功但尚未完成 Mesh 组网 | 1 s 亮、1 s 灭 | 表示模块正常,但当前没有完成组网 | +| 已完成 Mesh 组网 | 常亮 | 表示 PC3 已确认模块处于组网状态 | + +建议使用 1 ms 系统节拍实现 LED 状态机,不在主循环中使用阻塞延时。LED 状态优先级固定为: + +```text +BT_INIT_FAULT > BT_NOT_JOINED > BT_JOINED +``` + +JDY-28M 手册第 7 页对 STAT(第 6 脚)给出了明确规定:在 MESH 模式下,入网成功输出高电平,未入网输出低电平。因此原理图中的 `PC3_JDY-28M-STAT` 应按高有效处理: + +```c +/* JDY-28M 手册明确规定:MESH 入网成功时 STAT 输出高电平。 */ +#define JDY_MESH_JOINED_LEVEL 1 + +/* + * 更新 LED3 显示状态。 + * - 初始化故障时以 300 ms 周期闪烁; + * - 初始化成功但 PC3 未确认组网时以 1 s 周期闪烁; + * - PC3 连续稳定确认已组网时保持常亮。 + */ +void BluetoothLed_Update(uint8_t init_fault, uint8_t init_ready, + uint8_t mesh_status_level) +{ + /* 具体 GPIO 翻转由 1 ms 定时任务执行,避免阻塞业务处理。 */ +} +``` + +PC3 状态必须连续稳定确认至少 3 秒:连续 3 秒读取到高电平才切换为常亮;连续 3 秒读取到低电平才切换为 1 秒闪烁。模块的 ALED(第 17 脚)是 JDY-28M 自带的广播 LED 指示脚,不应与本项目由 STM8 PD3 控制的 LED3 混淆。 + +### 4.3 量产配置 + +每个设备在产测或配网阶段配置一次,之后发送 `AT+RESET`: + +```text +AT+ROLE2 +AT+BAUD4 +AT+MADDR<设备两字节短地址> +AT+NETID<四字节组网ID>,<十六进制参数> +AT+MCLSSA0 +AT+ZJEN1 +AT+RESET +``` + +- `AT+BAUD4` 为 9600 baud,应与 STM8 UART 保持一致; +- `AT+MADDR` 设置 Mesh 短地址,设备之间不能重复; +- `AT+NETID` 设置网络 ID,同一网络必须一致,手册说明该参数设置后不能查询,因此必须保存生产记录; +- `AT+MCLSSA0` 选择 A0 类型,适合 MESH 透传/指令通信与低功耗 IO 类设备; +- `AT+ZJEN1` 开启 Mesh 中继能力,是否关闭应根据现场网络拓扑测试决定。 + +### 4.4 MESH 指令与 STM8 应用层 + +在 `AT+ROLE2` 下,JDY-28M 使用目标短地址和 Mesh CMD 进行通信。手册第 29 页给出了 UART 发送和接收的完整示例。本项目使用其中的 Mesh 数据通信方式,把 STM8 业务帧作为 Mesh 数据区发送;不把业务帧中的 `0x55 0xAA` 误认为 JDY-28M 的无线帧头。 + +STM8 仍然需要对 UART 业务数据进行二次校验,不能把收到的任何 UART 字节直接当作继电器命令。由于当前使用的是 `AT+ROLE2` MESH 指令通信模式,目标短地址属于 JDY-28M 的发送帧字段,接收帧中还会携带来源短地址,因此不应在 STM8 业务载荷中重复添加。 + +### 4.4.1 JDY-28M Mesh 外层帧 + +手册第 29 页的示例表明,JDY-28M 串口发送给模块的外层帧为: + +```text +41 54 2B AA A1 | 目标短地址 2B | A0 | 业务数据 | 0D 0A +``` + +其中: + +- `41 54 2B AA A1`:固定的 JDY-28M MESH 指令前缀,对应 ASCII/十六进制字符串 `41542BAAA1`; +- 目标短地址:2 字节,例如 `FFFF` 表示广播,`000C` 表示发送到短地址 `000C`; +- `A0`:手册示例中的 MESH 数据通信命令字节; +- 业务数据:本项目的 STM8 业务帧,例如 `55 AA CMD LEN DATA CRC16`; +- `0D 0A`:JDY-28M 指令结束符。 + +因此,主机或上位机发送给某个继电器节点的完整数据应类似: + +```text +41 54 2B AA A1 00 0C A0 55 AA CMD LEN DATA CRC16 0D 0A +``` + +手册给出的设备接收帧格式为: + +```text +F1 DD 09 00 0A | 源短地址 2B | 业务数据 | 0D 0A +``` + +例如源地址为 `000C`、业务数据为 `1122334455` 时,接收格式为: + +```text +F1 DD 09 00 0A 00 0C 11 22 33 44 55 0D 0A +``` + +STM8 收到外层帧后,应先识别并剥离 `F1 DD 09 00 0A`、来源短地址和 `0D 0A`,再把中间的业务数据交给 `55 AA | CMD | LEN | DATA | CRC16` 解析器。手册第 29 页说明 MESH 数据长度支持最长 20 字节,因此本项目的业务帧总长度必须控制在该限制内。 + +因此,STM8 业务数据只保留以下应用层字段: + +```text +帧头 0x55 0xAA | CMD | LEN | DATA | CRC16 +``` + +字段定义: + +- `0x55 0xAA`:STM8 业务帧同步头; +- `CMD`:需要执行或返回的命令; +- `LEN`:DATA 字段的字节数; +- `DATA`:命令参数或返回数据; +- `CRC16`:从 `CMD` 开始计算到 DATA 结束的 CRC16,不包含帧头。 + +CRC16 固定采用 CRC-16/MODBUS,参数必须由主机和 STM8 完全一致地实现: + +- 多项式:标准表示为 `0x8005`,采用右移实现时使用 `0xA001`; +- 初始值:`0xFFFF`; +- 输入反转:是; +- 输出反转:是; +- CRC 字节顺序:低字节在前、高字节在后; +- 计算范围:从 `CMD` 开始,到 DATA 结束,不包含 `0x55 0xAA` 帧头; +- 不计算 JDY-28M 外层帧头、目标地址、`A0` 和 `0D 0A`。 + +固定测试向量: + +```text +输入数据:01 01 01 +CRC结果:0x4498 +发送顺序:98 44 +完整示例:55 AA 01 01 01 98 44 +``` + +### 5.5 业务命令定义 + +| CMD | 名称 | 方向 | DATA 定义 | +|---|---|---|---| +| `0x01` | 继电器控制 | 主机→设备 | `0x00` 断开;`0x01` 吸合 | +| `0x02` | 查询继电器状态 | 主机→设备/设备→主机 | 返回 `0x00` 断开;`0x01` 吸合 | +| `0x03` | 读取电流数据 | 主机→设备/设备→主机 | 返回 2 字节实际电流值,单位 mA,低字节在前 | +| `0x04` | 故障上报 | 设备→主机 | `0x00` 吸合但无电流;`0x01` 断开但有异常电流;`0x02` 采样异常;`0x04/0x06` 对应恢复 | +| `0x05` | 设备短地址上报 | 设备→主机 | 返回 2 字节 Mesh 短地址 | +| `0x06` | 配置电流阈值 | 主机→设备 | 2 字节无符号阈值,单位 mA,低字节在前 | + +### 5.5.1 应答命令分配建议 + +采用“请求命令 + `0x80` = 应答命令”的规则: + +| 请求 CMD | 应答 CMD | 应答含义 | DATA | +|---|---|---|---| +| `0x01` | `0x81` | 继电器控制成功 | `0x00` 断开,`0x01` 吸合 | +| `0x02` | `0x82` | 继电器状态返回 | `0x00` 断开,`0x01` 吸合 | +| `0x03` | `0x83` | 电流数据返回 | 2 字节实际电流值,单位 mA,低字节在前 | +| `0x04` | - | 故障/恢复主动上报 | 故障码,设备主动发送 | +| `0x05` | `0x85` | 设备短地址返回 | 2 字节短地址,低字节在前 | +| `0x06` | `0x86` | 电流阈值配置成功 | 2 字节当前阈值,单位 mA,低字节在前 | +| `0x04` | `0x84` | 故障/恢复上报确认 | 1 字节,被确认的故障码 | + +如果设备不能执行请求,使用统一错误应答 `CMD=0xE0`: + +```text +DATA[0]:错误码 +0x01 = 设备未初始化完成 +0x02 = 参数长度错误 +0x03 = 参数值非法 +0x04 = 当前状态不允许执行 +0x05 = ROM 写入失败 +0x06 = 业务命令不支持 +``` + +CRC 错误不发送 `0xE0`,直接丢弃,因为 CRC 错误时不能确认收到的命令内容。 + +广播请求同样需要应答。特别是广播 `CMD=0x05` 地址查询时,主机通过收集所有设备返回的 `0x85` 应答获得网络内的短地址列表。 + +为避免所有设备同时回复造成 Mesh 碰撞,广播地址查询应采用错峰应答: + +```text +response_delay_ms = 20 + (mesh_short_address % 16) * 20 +``` + +设备收到广播地址查询后,不立即发送,而是在计算出的时间槽发送 `CMD=0x85`,DATA 为本机 2 字节短地址。主机应在足够的收集窗口内接收多个应答,并按短地址去重。 + +广播控制指令也返回 `0x81` 控制应答;为避免多个设备同时回复,广播控制应答同样采用基于短地址的错峰发送。单播指令不需要错峰,可以在执行后立即应答。 + +建议固定返回格式: + +```text +CMD=0x02, LEN=1, DATA=[RELAY_STATE] +CMD=0x03, LEN=2, DATA=[CURRENT_L, CURRENT_H] +CMD=0x05, LEN=2, DATA=[ADDR_L, ADDR_H] +``` + +`0x04` 故障上报建议定义为: + +```text +DATA[0]:故障码,0x00=继电器已吸合但没有电流输出,0x01=继电器已断开但仍存在异常电流,0x02=ACS712采样异常,0x04=电流故障恢复,0x06=采样异常恢复。蓝牙初始化异常不通过 Mesh 上报,只通过 LED3 指示。 +``` + +`CMD=0x03` 的电流数据是 STM8 根据 ACS712 采样值、零点偏置和校准系数计算出的实际电流,不是 ADC 原始值。数据类型为 16 位无符号整数,单位为 mA,低字节在前,并采用四舍五入: + +```text +实际电流 1250 mA = 0x04E2 +DATA = 0xE2 0x04 + +实际电流 500 mA = 0x01F4 +DATA = 0xF4 0x01 +``` + +该格式的最大表示值为 65535 mA,即 65.535 A,能够覆盖 ACS712ELCTR-30A 的量程。 + +例如,继电器吸合后检测不到电流时主动发送: + +```text +55 AA 04 01 00 CRC16 +``` + +其中 `CMD=0x04`,`LEN=0x01`,故障码为 `0x00`,表示继电器已吸合但没有电流输出。实际发送时应将末尾 `CRC16` 替换为计算结果。 + +JDY-28M 先按 MESH 外层帧完成目标地址处理和来源地址携带,STM8 再校验业务帧头、命令、长度和 CRC,然后执行命令。当前协议不设置业务层 SEQ;因此主机不应在未确认应答时盲目重发继电器控制帧,后续如需要可靠重发,应另行增加事务机制。 + +只有在未来存在“模块地址”和“业务逻辑地址”不一致的需求时,才在业务载荷中额外增加逻辑设备号;此时字段名称必须使用 `logical_device_id`,不能再命名为目标短地址或源短地址,以免与 JDY-28M 地址混淆。 + +## 5. 设备进入可执行状态后的指令划分 + +只有完成以下条件后,设备才将 `device_ready` 置为 1,开始处理业务指令: + +1. JDY-28M 已确认 `AT+ROLE=2`; +2. 已成功查询并校验 Mesh 短地址; +3. 已读取并保存 Mesh 状态;组网状态由 LED3 单独指示; +4. STM8 已完成 ACS712 零点初始化; +5. 继电器输出已经处于安全的断开状态。 + +未进入 `device_ready` 前,只允许处理初始化相关命令,不能执行远程继电器控制。初始化完成后,即使 Mesh 尚未组网,也不强制断开继电器;如果收到本地或后续有效业务控制,仍可按正常逻辑执行。 + +### 5.1 指令一:询问设备短地址 + +主机发送“询问地址”业务指令后,STM8 不重新修改地址,只返回启动阶段通过 `AT+MADDR` 查询并保存的当前短地址。 + +```text +请求方式:主机发送地址查询请求 +应答命令:0x05 ADDRESS_REPORT +应答数据:设备短地址 2 bytes +``` + +如果地址没有通过校验或蓝牙初始化尚未完成,应返回地址无效/设备未就绪错误,不能返回未校验的旧地址。 + +### 5.1.1 指令六:配置电流阈值 + +主机发送 `CMD=0x06`、`LEN=2`,DATA 为低字节在前的 16 位无符号阈值,单位为 mA。STM8 校验通过后将阈值写入 ROM;后续上电时读取该值,读取失败或数据无效时使用默认值 100 mA。 + +```text +CMD=0x06, LEN=2, DATA=[THRESHOLD_L, THRESHOLD_H] +``` + +阈值配置成功后,设备重新封装应答帧,使用专用的配置应答 CMD 并重新计算 CRC16,不能直接原样返回主机的 Mesh 外层帧。 + +### 5.2 指令二:控制继电器吸合或断开 + +```text +请求命令:0x01 RELAY_CONTROL +应答方式:返回当前继电器状态帧(CMD=0x02) +``` + +处理流程: + +1. 校验 JDY-28M 外层地址、业务命令长度和 CRC; +2. 判断设备是否 `device_ready`; +3. 设置继电器软件目标状态; +4. 立即控制 `PA1_relay` 输出,使继电器吸合或断开; +5. 重新封装一帧应答数据,使用专用的控制应答 CMD,重新计算业务 CRC16,并通过新的 JDY-28M 外层地址和帧头发送; +7. 继续由 ACS712 采样和故障状态机进行后续确认。 + +控制应答不需要等待电流确认。返回成功表示继电器控制动作已经发出,不表示负载电流已经符合预期。由于目前没有触点反馈,不单独判断继电器机械控制失败;后续异常统一由 ACS712 电流检测故障上报。 + +收到新的控制指令时,应立即更新目标状态,并重新启动对应的电流确认窗口。 + +### 5.3 指令三:故障主动上报 + +故障不是主机查询后才返回,而是由 STM8 在电流确认窗口结束后主动通过 JDY-28M 上报: + +```text +上报命令:0x04 FAULT_REPORT +故障码 DATA[0]=0x00:继电器吸合,但持续无有效电流 +故障码 DATA[0]=0x01:继电器断开,但仍持续存在有效电流 +故障码 DATA[0]=0x02:ACS712 采样异常 +故障码 DATA[0]=0x04:电流故障恢复 +故障码 DATA[0]=0x06:采样异常恢复 +``` + +故障上报数据建议包含: + +```text +设备短地址 | 故障码 | 继电器目标状态 | 当前电流值 +``` + +上报规则: + +- 吸合后持续检测至少 10 s,电流始终低于 `I_ON_THRESHOLD`,主动上报 `0x00`; +- 断开后持续检测至少 10 s,电流仍持续高于 `I_OFF_THRESHOLD`,主动上报 `0x01`; +- ACS712 采样异常上报 `0x02`;蓝牙初始化异常无法通过 Mesh 上报,只通过 LED3 指示; +- 后续连续检测恢复正常至少 10 s 时,清除对应故障并主动上报恢复码 `0x04` 或 `0x06`;蓝牙初始化故障无法通过 Mesh 上报,恢复后仅结束 LED3 故障闪烁; +- 故障或恢复上报发送后,等待主机返回 `CMD=0x84` 确认; +- 未收到确认时,每隔 10 s 重发一次相同的 `CMD=0x04` 上报帧; +- 收到 `CMD=0x84` 且 DATA 与当前待确认故障码一致后,停止该事件的周期重发; +- 重复收到的故障上报不得重复改变继电器状态,只需返回 `CMD=0x84`; +- 故障上报不能改变当前继电器输出,除非产品需求明确要求保护性断开; +- `0x04` 专用于设备主动故障/恢复上报,`0x84` 专用于主机确认。 + +### 5.4 指令处理优先级 + +```text +初始化故障保护 + > 继电器控制指令 + > 故障状态机 + > 周期状态上报 +``` + +如果设备处于蓝牙初始化故障,继电器必须保持断开;如果设备已经进入业务运行状态,继电器控制应答和故障主动上报分别走同步、异步两条路径。 + +## 6. 状态模型 + +继电器控制状态不应直接等同于“负载已经工作”。建议保存以下状态: + +```c +typedef enum +{ + RELAY_OFF = 0, /* 软件要求继电器断开 */ + RELAY_ON = 1 /* 软件要求继电器吸合 */ +} RelayCommandState; + +typedef enum +{ + FAULT_NONE = 0x00, /* 无故障 */ + FAULT_ON_NO_CURRENT = 0x01, /* 要求吸合,但确认窗口内无电流 */ + FAULT_OFF_HAS_CURRENT = 0x02,/* 要求断开,但确认窗口内仍有电流 */ + FAULT_CURRENT_SENSOR = 0x03 /* 采样异常或零点未校准 */ +} FaultCode; +``` + +建议故障状态采用“锁存 + 明确清除”:故障发生后继续上报,只有收到清除故障指令、重新执行一次有效控制,或设备重启并完成自检后才清除。是否允许自动清除,应由产品需求决定。 + +## 7. ACS712 采样算法 + +ACS712 输出是以约 VCC/2 为零电流中心的模拟电压,30 A 型号的典型灵敏度约为 66 mV/A。不要用单次 ADC 值判断有无电流,应采用连续采样计算 RMS。 + +推荐参数: + +- ADC 采样频率:1 kHz 至 2 kHz; +- RMS 窗口:至少 20 ms,推荐 100 ms; +- 每个窗口先计算平均值作为零点/偏置; +- 对每个采样值计算相对零点的平方和; +- 使用整数或定点运算,避免 STM8 上使用浮点造成代码过大和执行时间不可控; +- 上电后继电器保持断开,采集 200 ms 至 500 ms 的零点偏置; +- 零点偏置只能在确认负载无电流时更新,不能在继电器吸合状态下随意跟踪。 + +计算关系为: + +```text +I_rms = sqrt(sum((ADC_i - ADC_zero)^2) / N) * Vref / (1023 * 0.066) +``` + +实际项目中应使用已知标准电流进行两点或多点校准,得到 `CURRENT_SCALE` 和 `CURRENT_OFFSET`,不要直接把典型灵敏度当成最终精度。 + +## 8. 故障判定时序 + +### 6.1 吸合无电流 + +1. 收到 ON 指令; +2. 设置软件目标状态为 ON; +3. PA1 输出吸合电平; +4. 等待继电器机械动作时间,例如 100 ms; +5. 持续检测电流,确认窗口至少 10 s; +6. 若窗口内连续满足 `I_rms >= I_ON_THRESHOLD`,判定吸合成功; +7. 窗口结束仍无有效电流,生成 `FAULT_ON_NO_CURRENT`。 + +`I_ON_THRESHOLD` 支持动态配置,默认值为 100 mA;实际产品可根据最小负载电流、ACS712 噪声和现场校准结果调整。 + +### 6.2 断开仍有电流 + +1. 收到 OFF 指令; +2. 设置软件目标状态为 OFF; +3. PA1 输出断开电平; +4. 等待触点释放时间,例如 100 ms; +5. 持续检测电流,确认窗口至少 10 s; +6. 若窗口内电流持续低于阈值,判定断开正常; +7. 若连续多个采样窗口仍满足 `I_rms >= I_OFF_THRESHOLD`,生成 `FAULT_OFF_HAS_CURRENT`。 + +`I_OFF_THRESHOLD` 应高于零点噪声。吸合和断开的电流阈值支持动态配置,默认值均为 100 mA。电流必须持续满足故障条件至少 10 s 才上报,不能根据单次采样或瞬时浪涌报错。 + +## 9. 推荐主循环结构 + +```c +void App_MainLoop(void) +{ + /* 读取 UART 环形缓冲区,将完整帧交给协议解析器。 */ + Bluetooth_ProcessRxFrames(); + + /* 由 1 ms 定时节拍触发电流采样,不在主循环中使用阻塞延时。 */ + CurrentSampler_ProcessPendingSamples(); + + /* 每完成一个 RMS 窗口,就更新当前电流和采样质量。 */ + CurrentMonitor_ProcessCompletedWindow(); + + /* 根据继电器目标状态、延时和电流结果推进故障状态机。 */ + RelayFaultMonitor_Run(); + + /* 周期性发送状态,不应阻塞继电器控制和 ADC 采样。 */ + Bluetooth_ReportPeriodicStatusIfDue(); + + /* 防止程序跑飞;喂狗动作应放在确认主循环各任务正常执行之后。 */ + IWDG_ServiceIfSystemHealthy(); +} +``` + +## 10. 建议蓝牙协议 + +正式量产使用前文定义的二进制帧: + +```text +0x55 0xAA | CMD | LEN | DATA | CRC16 +``` + +Mesh 目标地址和源地址由 JDY-28M 的 MESH 指令帧承担,不放入 STM8 业务载荷。 + +## 11. 关键异常处理 + +- 蓝牙帧 CRC 错误:丢弃并返回通信错误,不改变继电器状态; +- LEN 超出协议允许范围:丢弃整帧,不执行继电器动作; +- 上电:继电器默认断开,先完成 ADC 零点采集和自检;初始化完成后,Mesh 未组网不再强制改变继电器状态; +- ACS712 输出超出 0 至 VDDA:记录传感器故障,禁止根据异常值自动闭合; +- 采样窗口不足:不更新电流值,不进行故障判定; +- MCU 看门狗复位:重启后保持继电器断开,并上报复位原因; +- 故障状态下:可以继续发送实时电流,但不应静默覆盖原故障码。 + +## 12. 推荐实现顺序 + +1. 复核 PC4 的 ADC 通道编号、PC3 的蓝牙状态有效电平以及 PCB 走线; +2. 单独验证 PA1-继电器吸合/断开极性; +3. 在继电器断开状态下验证 ACS712 零点和噪声; +4. 用已知负载校准电流换算; +5. 验证蓝牙 UART 收发和 Mesh 重发/去重; +6. 最后启用两类故障判定,并分别测试“空载吸合”和“触点粘连/外部旁路带电”场景。 + +该文档是固件设计和测试基线,不代表已经完成编译、下载、PCB 实测或市电安全认证。 diff --git a/include/adc_current.h b/include/adc_current.h new file mode 100644 index 0000000..c4bba0d --- /dev/null +++ b/include/adc_current.h @@ -0,0 +1,26 @@ +/** + * @file adc_current.h + * @brief ACS712 电流采样服务接口。 + * + * PC4 接收经过 R3/R4 1:1 分压后的 ACS712 VIOUT。业务层只能读取经过 + * 零点校准、RMS 计算和比例换算后的实际 mA 值,不得直接把 ADC 原始值 + * 当作电流上报。 + */ +#ifndef ADC_CURRENT_H +#define ADC_CURRENT_H + +#include + +/** 初始化 PC4 ADC 和采样服务状态。 */ +void Current_Init(void); + +/** 执行一次后台采样任务。 */ +void Current_Task(void); + +/** 返回最近一次计算得到的实际电流,单位为 mA。 */ +uint16_t Current_GetMilliAmpere(void); + +/** 返回零点和 RMS 窗口是否已经准备完成。 */ +uint8_t Current_IsReady(void); + +#endif diff --git a/include/app_config.h b/include/app_config.h new file mode 100644 index 0000000..f3542af --- /dev/null +++ b/include/app_config.h @@ -0,0 +1,47 @@ +/** + * @file app_config.h + * @brief STM8 蓝牙 Mesh 继电器固件的统一配置入口。 + * + * 本文件集中保存当前已经确认的协议和业务常量,避免在多个 C 文件中 + * 重复定义相同数字。后续接入真实寄存器时,硬件相关宏也应优先放在此处 + * 或独立的 board_config.h 中,便于人工维护和更换 PCB。 + */ +#ifndef APP_CONFIG_H +#define APP_CONFIG_H + +/** 系统节拍周期,单位为毫秒。 */ +#define SYSTEM_TICK_MS 1u +/** JDY-28M 与 STM8 固定通信波特率。 */ +#define JDY28M_UART_BAUDRATE 9600ul +/** PWRC 拉低后进入 AT 指令窗口的等待时间。 */ +#define JDY28M_AT_ENTRY_WAIT_MS 10ul +/** JDY-28M RESET 低电平脉冲时间。 */ +#define JDY28M_RESET_PULSE_MS 500ul +/** 释放 RESET 后等待模块启动完成的时间。 */ +#define JDY28M_RESET_STARTUP_WAIT_MS 1500ul + +/** 默认电流阈值,单位为 mA。 */ +#define DEFAULT_CURRENT_THRESHOLD_MA 100u + +/** 电流故障产生和恢复的连续确认时间,单位为毫秒。 */ +#define CURRENT_FAULT_CONFIRM_MS 10000ul + +/** Mesh 状态连续确认时间,单位为毫秒。 */ +#define MESH_STATUS_CONFIRM_MS 3000ul + +/** 蓝牙初始化故障 LED 的亮灭周期,单位为毫秒。 */ +#define LED_INIT_FAULT_PERIOD_MS 300ul + +/** 未组网 LED 的亮灭周期,单位为毫秒。 */ +#define LED_NOT_JOINED_PERIOD_MS 1000ul + +/** ACS712 采样建议使用的 RMS 窗口,单位为毫秒。 */ +#define CURRENT_RMS_WINDOW_MS 100ul + +/** 业务协议帧头第一个字节。 */ +#define BUSINESS_FRAME_HEAD_0 0x55u + +/** 业务协议帧头第二个字节。 */ +#define BUSINESS_FRAME_HEAD_1 0xAAu + +#endif diff --git a/include/bluetooth_manager.h b/include/bluetooth_manager.h new file mode 100644 index 0000000..505b003 --- /dev/null +++ b/include/bluetooth_manager.h @@ -0,0 +1,25 @@ +/** + * @file bluetooth_manager.h + * @brief JDY-28M 上电初始化、短地址查询和 LED 状态管理接口。 + */ +#ifndef BLUETOOTH_MANAGER_H +#define BLUETOOTH_MANAGER_H + +#include + +/** 初始化蓝牙管理状态机。 */ +void BluetoothManager_Init(void); + +/** 执行一次非阻塞蓝牙初始化和 LED 状态任务。 */ +void BluetoothManager_Task(void); + +/** 返回蓝牙初始化是否成功。 */ +uint8_t BluetoothManager_IsReady(void); + +/** 返回蓝牙初始化故障状态。 */ +uint8_t BluetoothManager_IsFault(void); + +/** 返回最近一次查询到的短地址。 */ +uint16_t BluetoothManager_GetShortAddress(void); + +#endif diff --git a/include/board.h b/include/board.h new file mode 100644 index 0000000..dd4cd8b --- /dev/null +++ b/include/board.h @@ -0,0 +1,34 @@ +/** + * @file board.h + * @brief 蓝牙 Mesh 继电器硬件板级接口定义。 + * + * 本阶段先提供稳定的硬件抽象接口,不在业务层直接操作 GPIO 寄存器。 + * PA1 的有效电平已经确认:高电平吸合、低电平断开;PD3 高电平点亮 LED3。 + */ +#ifndef BOARD_H +#define BOARD_H + +#include + +/** 初始化 GPIO、时钟和板级默认安全状态。 */ +void Board_Init(void); + +/** 设置继电器目标输出状态,1 表示吸合,0 表示断开。 */ +void Board_SetRelay(uint8_t on); + +/** 读取当前继电器控制输出状态,1 表示吸合,0 表示断开。 */ +uint8_t Board_GetRelay(void); + +/** 读取 JDY-28M STAT 状态,1 表示模块报告已组网。 */ +uint8_t Board_ReadMeshJoined(void); + +/** 设置 LED3 点亮状态,1 表示点亮,0 表示熄灭。 */ +void Board_SetLed3(uint8_t on); + +/** 设置 PWRC:1 表示拉低进入 AT 窗口,0 表示恢复高电平通信。 */ +void Board_SetBluetoothAtWindow(uint8_t enabled); + +/** 设置 RESET:1 表示拉低复位,0 表示释放复位。 */ +void Board_SetBluetoothReset(uint8_t reset_active); + +#endif diff --git a/include/business_protocol.h b/include/business_protocol.h new file mode 100644 index 0000000..ebb3a0c --- /dev/null +++ b/include/business_protocol.h @@ -0,0 +1,85 @@ +/** + * @file business_protocol.h + * @brief STM8 业务帧协议接口。 + * + * 业务帧格式:0x55 0xAA | CMD | LEN | DATA | CRC16。 + * CRC 使用 CRC-16/MODBUS,计算范围为 CMD 至 DATA,低字节在前。 + */ +#ifndef BUSINESS_PROTOCOL_H +#define BUSINESS_PROTOCOL_H + +#include + +/** 业务帧最大 DATA 长度,需为 JDY-28M 外层 20 字节限制预留空间。 */ +#define BUSINESS_MAX_DATA_LENGTH 4u + +/** 业务帧头长度。 */ +#define BUSINESS_FRAME_HEADER_LENGTH 2u + +/** 业务命令:继电器控制。 */ +#define CMD_RELAY_CONTROL 0x01u + +/** 业务命令:查询继电器状态。 */ +#define CMD_RELAY_QUERY 0x02u + +/** 业务命令:查询实际电流。 */ +#define CMD_CURRENT_QUERY 0x03u + +/** 业务命令:故障/恢复上报。 */ +#define CMD_FAULT_REPORT 0x04u + +/** 业务命令:短地址查询。 */ +#define CMD_ADDRESS_QUERY 0x05u + +/** 业务命令:电流阈值配置。 */ +#define CMD_THRESHOLD_CONFIG 0x06u + +/** 应答命令基值,具体应答为请求命令加 0x80。 */ +#define CMD_RESPONSE_FLAG 0x80u + +/** 通用错误应答命令。 */ +#define CMD_ERROR_RESPONSE 0xE0u + +/** CRC 校验失败时静默丢弃,不生成业务错误应答。 */ +#define BUSINESS_PARSE_CRC_ERROR 0x01u + +/** 业务帧解析结果:没有完整帧。 */ +#define BUSINESS_PARSE_INCOMPLETE 0x00u + +/** 业务帧解析结果:收到并通过 CRC 校验。 */ +#define BUSINESS_PARSE_FRAME_READY 0x01u + +/** 业务帧解析器初始化。 */ +void BusinessProtocol_Init(void); + +/** 处理 UART 输入和待发送业务帧。 */ +void BusinessProtocol_Task(void); + +/** + * 向解析器输入一个字节。 + * + * 该接口只负责解析内层业务帧,不执行继电器动作,也不负责 JDY-28M + * 外层 Mesh 封装。外层模块应在确认 `F1 DD 09 00 0A ... 0D 0A` 后, + * 将其中的业务数据逐字节送入本接口。 + * + * @param value 输入字节。 + * @return 1 表示刚刚形成一帧合法业务帧,0 表示尚未形成完整帧。 + */ +uint8_t BusinessProtocol_InputByte(uint8_t value); + +/** 读取最近一帧合法业务帧的命令字。 */ +uint8_t BusinessProtocol_GetCommand(void); + +/** 读取最近一帧合法业务帧的数据长度。 */ +uint8_t BusinessProtocol_GetDataLength(void); + +/** 读取最近一帧合法业务帧的数据区;返回 0 表示下标越界。 */ +uint8_t BusinessProtocol_GetDataByte(uint8_t index, uint8_t *value); + +/** 执行最近一帧已经通过校验的业务命令,并向源地址返回应答。 */ +void BusinessProtocol_HandleFrame(uint16_t source_address); + +/** CRC-16/MODBUS 计算函数,输入范围不包含业务帧头。 */ +uint16_t BusinessProtocol_Crc16Modbus(const uint8_t *data, uint16_t length); + +#endif diff --git a/include/fault_manager.h b/include/fault_manager.h new file mode 100644 index 0000000..1f8bac2 --- /dev/null +++ b/include/fault_manager.h @@ -0,0 +1,29 @@ +/** + * @file fault_manager.h + * @brief 继电器状态与 ACS712 电流状态的故障判定接口。 + * + * 本模块只依据“继电器控制输出状态”和“ACS712 已换算后的实际电流值” + * 做软件诊断,不声称能够证明继电器机械触点一定闭合。因为当前硬件没有 + * 触点反馈,继电器吸合失败最终只能通过“吸合后没有有效电流”间接发现。 + */ +#ifndef FAULT_MANAGER_H +#define FAULT_MANAGER_H + +#include + +/** 初始化故障判定状态机。 */ +void FaultManager_Init(void); + +/** 周期性执行电流故障确认、恢复确认和未确认重发。 */ +void FaultManager_Task(void); + +/** 收到主机对故障/恢复上报的 ACK 后调用。 */ +void FaultManager_OnReportAck(uint8_t fault_code); + +/** 设置运行期电流阈值;单位 mA。 */ +uint8_t FaultManager_SetThreshold(uint16_t threshold_ma); + +/** 获取当前电流阈值;单位 mA。 */ +uint16_t FaultManager_GetThreshold(void); + +#endif diff --git a/include/mesh_frame.h b/include/mesh_frame.h new file mode 100644 index 0000000..f281935 --- /dev/null +++ b/include/mesh_frame.h @@ -0,0 +1,36 @@ +/** + * @file mesh_frame.h + * @brief JDY-28M Mesh 外层帧解析和发送接口。 + */ +#ifndef MESH_FRAME_H +#define MESH_FRAME_H + +#include + +/** JDY-28M 外层完整帧最大长度,包含帧头、地址、数据和 0D 0A。 */ +#define JDY_MESH_FRAME_MAX_LENGTH 20u + +/** 初始化 JDY-28M 外层帧状态机。 */ +void MeshFrame_Init(void); + +/** 从 UART 接收并解析 JDY-28M 外层帧。 */ +void MeshFrame_Task(void); + +/** 返回最近一帧合法数据的源短地址。 */ +uint16_t MeshFrame_GetLastSourceAddress(void); + +/** + * 封装并发送一帧 Mesh 业务数据。 + * + * @param target_address 目标短地址,0xFFFF 表示广播。 + * @param command 业务命令。 + * @param data 业务数据,可为 NULL(此时 length 必须为 0)。 + * @param length 业务数据长度,当前不能超过 4 字节。 + * @return 1 表示已发送,0 表示参数超出 20 字节外层限制。 + */ +uint8_t MeshFrame_SendBusiness(uint16_t target_address, + uint8_t command, + const uint8_t *data, + uint8_t length); + +#endif diff --git a/include/nonvolatile_config.h b/include/nonvolatile_config.h new file mode 100644 index 0000000..cd678a1 --- /dev/null +++ b/include/nonvolatile_config.h @@ -0,0 +1,19 @@ +/** + * @file nonvolatile_config.h + * @brief STM8 Data EEPROM 中的设备配置接口。 + */ +#ifndef NONVOLATILE_CONFIG_H +#define NONVOLATILE_CONFIG_H + +#include + +/** 初始化 EEPROM 配置访问状态。 */ +void NonvolatileConfig_Init(void); + +/** 读取保存的电流阈值;无效时返回 0。 */ +uint16_t NonvolatileConfig_ReadThreshold(void); + +/** 保存电流阈值;返回 1 表示写入并回读校验成功。 */ +uint8_t NonvolatileConfig_WriteThreshold(uint16_t threshold_ma); + +#endif diff --git a/include/stm8s003_registers.h b/include/stm8s003_registers.h new file mode 100644 index 0000000..6c44fdc --- /dev/null +++ b/include/stm8s003_registers.h @@ -0,0 +1,76 @@ +/** + * @file stm8s003_registers.h + * @brief STM8S003F3 常用外设寄存器的最小定义集。 + * + * 本工程暂不引入完整厂商库,先使用经过数据手册地址核对的最小寄存器集。 + * 后续如果切换到 ST 标准外设库,只应替换本文件和 drivers 层,不改变业务层接口。 + */ +#ifndef STM8S003_REGISTERS_H +#define STM8S003_REGISTERS_H + +#include + +/* GPIO 端口寄存器布局:ODR、IDR、DDR、CR1、CR2。 */ +#define GPIOA_ODR_ADDR 0x5000u +#define GPIOA_IDR_ADDR 0x5001u +#define GPIOA_DDR_ADDR 0x5002u +#define GPIOA_CR1_ADDR 0x5003u +#define GPIOA_CR2_ADDR 0x5004u +#define GPIOC_ODR_ADDR 0x500Au +#define GPIOC_IDR_ADDR 0x500Bu +#define GPIOC_DDR_ADDR 0x500Cu +#define GPIOC_CR1_ADDR 0x500Du +#define GPIOC_CR2_ADDR 0x500Eu +#define GPIOD_ODR_ADDR 0x500Fu +#define GPIOD_IDR_ADDR 0x5010u +#define GPIOD_DDR_ADDR 0x5011u +#define GPIOD_CR1_ADDR 0x5012u +#define GPIOD_CR2_ADDR 0x5013u + +/* UART1 寄存器地址。 */ +#define UART1_SR_ADDR 0x5230u +#define UART1_DR_ADDR 0x5231u +#define UART1_BRR1_ADDR 0x5232u +#define UART1_BRR2_ADDR 0x5233u +#define UART1_CR1_ADDR 0x5234u +#define UART1_CR2_ADDR 0x5235u +#define UART1_CR3_ADDR 0x5236u +#define UART1_CR4_ADDR 0x5237u +#define UART1_CR5_ADDR 0x5238u + +/* ADC1 寄存器地址。 */ +#define ADC1_CSR_ADDR 0x5400u +#define ADC1_CR1_ADDR 0x5401u +#define ADC1_CR2_ADDR 0x5402u +#define ADC1_CR3_ADDR 0x5403u +#define ADC1_DRH_ADDR 0x5404u +#define ADC1_DRL_ADDR 0x5405u + +/* TIM4 低功耗 8 位定时器寄存器地址。 */ +#define TIM4_CR1_ADDR 0x5340u +#define TIM4_IER_ADDR 0x5343u +#define TIM4_SR1_ADDR 0x5344u +#define TIM4_EGR_ADDR 0x5345u +#define TIM4_CNTR_ADDR 0x5346u +#define TIM4_PSCR_ADDR 0x5347u +#define TIM4_ARR_ADDR 0x5348u + +/* 独立看门狗寄存器。 */ +#define IWDG_KR_ADDR 0x50E0u +#define IWDG_PR_ADDR 0x50E1u +#define IWDG_RLR_ADDR 0x50E2u + +#define REG8(address) (*((volatile uint8_t *)(address))) + +#define PA1_MASK 0x02u +#define PC3_MASK 0x08u +#define PC4_MASK 0x10u +/** PC4 在 STM8S003F3P6 上对应 ADC1 的 AIN2 通道。 */ +#define ADC1_CHANNEL_PC4 2u +#define PD3_MASK 0x08u +#define PD5_MASK 0x20u +#define PD6_MASK 0x40u +#define PC5_MASK 0x20u +#define PC6_MASK 0x40u + +#endif diff --git a/include/system_tick.h b/include/system_tick.h new file mode 100644 index 0000000..f328597 --- /dev/null +++ b/include/system_tick.h @@ -0,0 +1,19 @@ +/** + * @file system_tick.h + * @brief 1 ms 系统节拍接口。 + */ +#ifndef SYSTEM_TICK_H +#define SYSTEM_TICK_H + +#include + +/** 初始化系统节拍定时器。 */ +void SystemTick_Init(void); + +/** 返回系统启动后经过的毫秒数。 */ +uint32_t SystemTick_NowMs(void); + +/** 在定时器中断服务程序中递增系统节拍。 */ +void SystemTick_InterruptHandler(void); + +#endif diff --git a/include/uart1.h b/include/uart1.h new file mode 100644 index 0000000..1bb346c --- /dev/null +++ b/include/uart1.h @@ -0,0 +1,28 @@ +/** + * @file uart1.h + * @brief JDY-28M 使用的 UART1 抽象接口。 + */ +#ifndef UART1_H +#define UART1_H + +#include + +/** 初始化 UART1。具体波特率和寄存器配置在底层阶段接入。 */ +void Uart1_Init(void); + +/** 轮询 UART1 硬件状态并把收到的字节放入环形缓冲区。 */ +void Uart1_Task(void); + +/** 向 UART1 发送一个字节。 */ +void Uart1_WriteByte(uint8_t value); + +/** 尝试读取一个接收字节,返回 1 表示读取成功。 */ +uint8_t Uart1_ReadByte(uint8_t *value); + +/** 返回接收环形缓冲区中尚未读取的字节数。 */ +uint8_t Uart1_Available(void); + +/** UART1 接收中断处理入口。 */ +void Uart1_InterruptHandler(void); + +#endif diff --git a/include/watchdog.h b/include/watchdog.h new file mode 100644 index 0000000..754087f --- /dev/null +++ b/include/watchdog.h @@ -0,0 +1,11 @@ +/** @file watchdog.h @brief STM8 独立看门狗接口。 */ +#ifndef WATCHDOG_H +#define WATCHDOG_H + +/** 初始化约 5 秒独立看门狗。 */ +void Watchdog_Init(void); + +/** 在主循环中刷新看门狗计数器。 */ +void Watchdog_Refresh(void); + +#endif diff --git a/src/drivers/adc_current.c b/src/drivers/adc_current.c new file mode 100644 index 0000000..b8ba644 --- /dev/null +++ b/src/drivers/adc_current.c @@ -0,0 +1,126 @@ +/** + * @file adc_current.c + * @brief ACS712 电流 ADC 采样和毫安换算实现。 + */ +#include +#include "adc_current.h" +#include "stm8s003_registers.h" + +/** 最近一次完成换算的实际电流,单位 mA。 */ +static uint16_t current_milliamperes; +/** 零点校准和当前电流数据是否已经准备完成。 */ +static uint8_t current_ready; +/** 启动校准得到的 ADC 零点码值。 */ +static uint16_t zero_code; +/** 启动校准期间用于累加 ADC 码值的 32 位累加器。 */ +static uint32_t zero_accumulator; +/** 已经完成的零点校准样本数。 */ +static uint8_t zero_count; +/** 正常运行期间用于累加偏差样本的累加器。 */ +static uint32_t signal_accumulator; +/** 当前平均窗口已经积累的偏差样本数。 */ +static uint8_t signal_count; + +/** + * STM8S003F3P6 官方引脚复用定义:PC4 = AIN2,因此 ADC1 通道选择值为 2。 + * 该对应关系来自 ST 官方 STM8S003F3 数据手册 DS7147 Rev 10 的表 6, + * PC4 项明确标注为“Analog input 2”。 + */ +#define ACS712_ADC_CHANNEL ADC1_CHANNEL_PC4 + +/** ADC 转换结束标志。 */ +#define ADC1_EOC_MASK 0x80u + +/** + * 读取一次 ADC 原始值。 + * + * ADC 采用 10 位右对齐结果,转换结束后先读 DRL 再读 DRH,避免在硬件 + * 更新期间读取到不一致的高低字节。若本次转换尚未完成,返回 0xFFFF。 + */ +static uint16_t Current_ReadAdc(void) +{ + uint16_t result; + REG8(ADC1_CSR_ADDR) = (uint8_t)(ACS712_ADC_CHANNEL & 0x0Fu); + REG8(ADC1_CR1_ADDR) |= 0x01u; + if ((REG8(ADC1_CSR_ADDR) & ADC1_EOC_MASK) == 0u) + { + return 0xFFFFu; + } + result = REG8(ADC1_DRL_ADDR); + result |= (uint16_t)REG8(ADC1_DRH_ADDR) << 8u; + return (uint16_t)(result & 0x03FFu); +} + +/** 初始化 PC4 ADC 服务状态;真实 ADC 寄存器配置在后续阶段接入。 */ +void Current_Init(void) +{ + current_milliamperes = 0u; + current_ready = 0u; + zero_code = 512u; + zero_accumulator = 0ul; + zero_count = 0u; + signal_accumulator = 0ul; + signal_count = 0u; + + /* ADC1 单次转换,右对齐,选择 PC4 对应的 ADC 通道由后续封装核对。 */ + REG8(ADC1_CR1_ADDR) = 0x01u; + REG8(ADC1_CR2_ADDR) = 0x08u; + REG8(ADC1_CR3_ADDR) = 0x00u; +} + +/** + * 执行一次后台采样任务。 + * + * ACS712 静态换算:PC4 前端为 1:1 分压,ADC 参考按 3.3V,ACS712-30A + * 灵敏度按典型 66mV/A 计算,因此理论比例约为 97.7mA/ADC count。 + * 上电前 64 个样本用于零点校准;实际产品仍建议用实测零点和校准系数 + * 替换默认值。当前接口输出的是整流后的平均电流,后续可再扩展为 AC RMS。 + */ +void Current_Task(void) +{ + uint16_t adc_code = Current_ReadAdc(); + uint16_t difference; + uint32_t milliamperes; + + if (adc_code == 0xFFFFu) + { + return; + } + if (zero_count < 64u) + { + zero_accumulator += adc_code; + ++zero_count; + if (zero_count == 64u) + { + zero_code = (uint16_t)(zero_accumulator / 64ul); + current_ready = 1u; + } + return; + } + difference = (adc_code >= zero_code) ? + (uint16_t)(adc_code - zero_code) : + (uint16_t)(zero_code - adc_code); + signal_accumulator += difference; + ++signal_count; + if (signal_count >= 16u) + { + difference = (uint16_t)(signal_accumulator / 16ul); + milliamperes = ((uint32_t)difference * 977ul + 5ul) / 10ul; + current_milliamperes = (milliamperes > 65535ul) ? 65535u : + (uint16_t)milliamperes; + signal_accumulator = 0ul; + signal_count = 0u; + } +} + +/** 返回最近一次计算的实际电流,单位为 mA。 */ +uint16_t Current_GetMilliAmpere(void) +{ + return current_milliamperes; +} + +/** 返回 RMS 和零点校准是否准备完成。 */ +uint8_t Current_IsReady(void) +{ + return current_ready; +} diff --git a/src/drivers/board.c b/src/drivers/board.c new file mode 100644 index 0000000..77655e7 --- /dev/null +++ b/src/drivers/board.c @@ -0,0 +1,99 @@ +/** + * @file board.c + * @brief 板级硬件抽象的第一阶段占位实现。 + * + * 真实 STM8 GPIO 寄存器地址和封装引脚确认后,在本文件中补充寄存器操作, + * 业务层不应因此改变接口。当前实现只用于验证工程可以被 SDCC 编译。 + */ +#include +#include "board.h" +#include "stm8s003_registers.h" + +/** 当前 PA1 继电器控制输出的逻辑状态。 */ +static uint8_t relay_state; +/** 当前 PD3 LED3 输出的逻辑状态。 */ +static uint8_t led3_state; + +/** 初始化板级资源,并将继电器置于安全断开状态。 */ +void Board_Init(void) +{ + /* PA1:推挽输出,默认低电平,确保上电继电器断开。 */ + REG8(GPIOA_DDR_ADDR) |= PA1_MASK; + REG8(GPIOA_CR1_ADDR) |= PA1_MASK; + REG8(GPIOA_CR2_ADDR) &= (uint8_t)~PA1_MASK; + REG8(GPIOA_ODR_ADDR) &= (uint8_t)~PA1_MASK; + + /* PC3:浮空输入,读取 JDY-28M STAT;PC4:模拟输入,禁止数字输出。 */ + REG8(GPIOC_DDR_ADDR) &= (uint8_t)~(PC3_MASK | PC4_MASK); + REG8(GPIOC_CR1_ADDR) &= (uint8_t)~(PC3_MASK | PC4_MASK); + REG8(GPIOC_CR2_ADDR) &= (uint8_t)~(PC3_MASK | PC4_MASK); + + /* PC5/PWRC 和 PC6/RESET 上电默认高电平,避免模块误进入 AT 或复位。 */ + REG8(GPIOC_ODR_ADDR) |= (uint8_t)(PC5_MASK | PC6_MASK); + REG8(GPIOC_DDR_ADDR) |= (uint8_t)(PC5_MASK | PC6_MASK); + REG8(GPIOC_CR1_ADDR) |= (uint8_t)(PC5_MASK | PC6_MASK); + REG8(GPIOC_CR2_ADDR) &= (uint8_t)~(PC5_MASK | PC6_MASK); + + /* PD3:推挽输出,默认熄灭 LED3。 */ + REG8(GPIOD_DDR_ADDR) |= PD3_MASK; + REG8(GPIOD_CR1_ADDR) |= PD3_MASK; + REG8(GPIOD_CR2_ADDR) &= (uint8_t)~PD3_MASK; + REG8(GPIOD_ODR_ADDR) &= (uint8_t)~PD3_MASK; + + relay_state = 0u; + led3_state = 0u; +} + +/** 设置继电器状态,后续接入 PA1 寄存器时保持高电平吸合约定。 */ +void Board_SetRelay(uint8_t on) +{ + relay_state = (on != 0u) ? 1u : 0u; + if (relay_state != 0u) + { + REG8(GPIOA_ODR_ADDR) |= PA1_MASK; + } + else + { + REG8(GPIOA_ODR_ADDR) &= (uint8_t)~PA1_MASK; + } +} + +/** 返回当前 PA1 继电器控制输出的逻辑状态。 */ +uint8_t Board_GetRelay(void) +{ + return relay_state; +} + +/** 读取 JDY-28M STAT 状态;当前阶段返回安全的未组网占位值。 */ +uint8_t Board_ReadMeshJoined(void) +{ + return ((REG8(GPIOC_IDR_ADDR) & PC3_MASK) != 0u) ? 1u : 0u; +} + +/** 设置 LED3 状态,后续接入 PD3 寄存器时保持高电平点亮约定。 */ +void Board_SetLed3(uint8_t on) +{ + led3_state = (on != 0u) ? 1u : 0u; + if (led3_state != 0u) + { + REG8(GPIOD_ODR_ADDR) |= PD3_MASK; + } + else + { + REG8(GPIOD_ODR_ADDR) &= (uint8_t)~PD3_MASK; + } +} + +/** 控制 PWRC;低电平使能 AT 指令窗口。 */ +void Board_SetBluetoothAtWindow(uint8_t enabled) +{ + if (enabled != 0u) REG8(GPIOC_ODR_ADDR) &= (uint8_t)~PC5_MASK; + else REG8(GPIOC_ODR_ADDR) |= PC5_MASK; +} + +/** 控制 RESET;低电平复位,高电平正常工作。 */ +void Board_SetBluetoothReset(uint8_t reset_active) +{ + if (reset_active != 0u) REG8(GPIOC_ODR_ADDR) &= (uint8_t)~PC6_MASK; + else REG8(GPIOC_ODR_ADDR) |= PC6_MASK; +} diff --git a/src/drivers/nonvolatile_config.c b/src/drivers/nonvolatile_config.c new file mode 100644 index 0000000..4da247f --- /dev/null +++ b/src/drivers/nonvolatile_config.c @@ -0,0 +1,96 @@ +/** + * @file nonvolatile_config.c + * @brief STM8S003 Data EEPROM 阈值保存实现。 + * + * EEPROM 布局: + * 0x4000:标志字节 0xA5; + * 0x4001:电流阈值低字节; + * 0x4002:电流阈值高字节; + * 0x4003:阈值低字节异或 0xFF; + * 0x4004:阈值高字节异或 0xFF。 + * + * 每次写入后立即回读并校验,避免掉电或 EEPROM 写入失败时把损坏配置 + * 当作有效阈值使用。实际量产前仍应依据芯片数据手册确认写周期和擦写寿命。 + */ +#include +#include "nonvolatile_config.h" +#include "stm8s003_registers.h" + +#define EEPROM_BASE_ADDR 0x4000u +#define EEPROM_MAGIC_ADDR (EEPROM_BASE_ADDR + 0u) +#define EEPROM_THRESHOLD_LOW_ADDR (EEPROM_BASE_ADDR + 1u) +#define EEPROM_THRESHOLD_HIGH_ADDR (EEPROM_BASE_ADDR + 2u) +#define EEPROM_CHECK_LOW_ADDR (EEPROM_BASE_ADDR + 3u) +#define EEPROM_CHECK_HIGH_ADDR (EEPROM_BASE_ADDR + 4u) +#define EEPROM_MAGIC 0xA5u +#define FLASH_DUKR_ADDR 0x5064u +#define FLASH_IAPSR_ADDR 0x505Fu +#define FLASH_DUL_MASK 0x08u + +/** 初始化接口;当前 EEPROM 无需额外运行时初始化。 */ +void NonvolatileConfig_Init(void) +{ +} + +/** 等待 EEPROM 解锁状态,超时后返回失败。 */ +static uint8_t NonvolatileConfig_WaitUnlocked(void) +{ + uint16_t timeout = 60000u; + while (((REG8(FLASH_IAPSR_ADDR) & FLASH_DUL_MASK) == 0u) && (timeout != 0u)) + { + --timeout; + } + return (timeout == 0u) ? 0u : 1u; +} + +/** 解锁 Data EEPROM 编程权限。 */ +static uint8_t NonvolatileConfig_Unlock(void) +{ + REG8(FLASH_DUKR_ADDR) = 0xAEu; + REG8(FLASH_DUKR_ADDR) = 0x56u; + return NonvolatileConfig_WaitUnlocked(); +} + +/** 向指定 EEPROM 地址写入一个字节。 */ +static uint8_t NonvolatileConfig_WriteByte(uint16_t address, uint8_t value) +{ + REG8(address) = value; + return (REG8(address) == value) ? 1u : 0u; +} + +/** 读取并校验 EEPROM 中的电流阈值。 */ +uint16_t NonvolatileConfig_ReadThreshold(void) +{ + uint8_t low; + uint8_t high; + if ((REG8(EEPROM_MAGIC_ADDR) != EEPROM_MAGIC) || + (REG8(EEPROM_CHECK_LOW_ADDR) != (uint8_t)(REG8(EEPROM_THRESHOLD_LOW_ADDR) ^ 0xFFu)) || + (REG8(EEPROM_CHECK_HIGH_ADDR) != (uint8_t)(REG8(EEPROM_THRESHOLD_HIGH_ADDR) ^ 0xFFu))) + { + return 0u; + } + low = REG8(EEPROM_THRESHOLD_LOW_ADDR); + high = REG8(EEPROM_THRESHOLD_HIGH_ADDR); + if ((low == 0u) && (high == 0u)) return 0u; + return (uint16_t)low | ((uint16_t)high << 8u); +} + +/** 写入阈值、校验字节和有效标志,并回读确认。 */ +uint8_t NonvolatileConfig_WriteThreshold(uint16_t threshold_ma) +{ + uint8_t low; + uint8_t high; + if (threshold_ma == 0u) return 0u; + if (NonvolatileConfig_Unlock() == 0u) return 0u; + low = (uint8_t)threshold_ma; + high = (uint8_t)(threshold_ma >> 8u); + if ((NonvolatileConfig_WriteByte(EEPROM_THRESHOLD_LOW_ADDR, low) == 0u) || + (NonvolatileConfig_WriteByte(EEPROM_THRESHOLD_HIGH_ADDR, high) == 0u) || + (NonvolatileConfig_WriteByte(EEPROM_CHECK_LOW_ADDR, (uint8_t)(low ^ 0xFFu)) == 0u) || + (NonvolatileConfig_WriteByte(EEPROM_CHECK_HIGH_ADDR, (uint8_t)(high ^ 0xFFu)) == 0u) || + (NonvolatileConfig_WriteByte(EEPROM_MAGIC_ADDR, EEPROM_MAGIC) == 0u)) + { + return 0u; + } + return (NonvolatileConfig_ReadThreshold() == threshold_ma) ? 1u : 0u; +} diff --git a/src/drivers/system_tick.c b/src/drivers/system_tick.c new file mode 100644 index 0000000..f0e1a27 --- /dev/null +++ b/src/drivers/system_tick.c @@ -0,0 +1,58 @@ +/** + * @file system_tick.c + * @brief 使用 TIM4 产生 1ms 系统节拍。 + */ +#include +#include "system_tick.h" +#include "stm8s003_registers.h" + +/** TIM4 中断递增的全局毫秒计数,溢出后按无符号差值继续工作。 */ +static volatile uint32_t system_tick_ms; + +/** + * 初始化 TIM4 产生 1ms 更新中断。 + * + * 假设系统主频为 16MHz:TIM4 预分频设置为 128 后计数频率为 125kHz, + * 自动重装值设为 124,计数 125 个周期正好得到约 1ms。若工程后续修改 + * 主时钟频率,必须同步重新计算 TIM4_PSCR 和 TIM4_ARR,否则所有 10 秒 + * 故障计时和 LED 闪烁周期都会产生比例误差。 + */ +void SystemTick_Init(void) +{ + system_tick_ms = 0ul; + REG8(TIM4_CR1_ADDR) = 0x00u; + REG8(TIM4_IER_ADDR) = 0x01u; + REG8(TIM4_PSCR_ADDR) = 0x07u; + REG8(TIM4_ARR_ADDR) = 124u; + REG8(TIM4_CNTR_ADDR) = 0u; + REG8(TIM4_EGR_ADDR) = 0x01u; + REG8(TIM4_SR1_ADDR) = 0x00u; + REG8(TIM4_CR1_ADDR) = 0x01u; +} + +/** 返回当前系统毫秒计数。 */ +uint32_t SystemTick_NowMs(void) +{ + return system_tick_ms; +} + +/** 由真实 1 ms 定时器中断调用,递增系统时间基准。 */ +void SystemTick_InterruptHandler(void) +{ + ++system_tick_ms; +} + +/** + * TIM4 更新中断服务函数。 + * + * STM8S003 的 TIM4 更新中断向量号为 23。本函数只做清除中断标志和递增 + * 软件毫秒计数,禁止在中断中执行 UART、EEPROM 或 Mesh 发送等耗时操作。 + */ +void SystemTick_TIM4_ISR(void) __interrupt(23) +{ + if ((REG8(TIM4_SR1_ADDR) & 0x01u) != 0u) + { + REG8(TIM4_SR1_ADDR) = 0x00u; + SystemTick_InterruptHandler(); + } +} diff --git a/src/drivers/uart1.c b/src/drivers/uart1.c new file mode 100644 index 0000000..5262795 --- /dev/null +++ b/src/drivers/uart1.c @@ -0,0 +1,91 @@ +/** + * @file uart1.c + * @brief UART1 第一阶段占位实现。 + */ +#include +#include "uart1.h" +#include "stm8s003_registers.h" + +/** UART1 接收环形缓冲区容量,必须使用 2 的幂便于快速回绕。 */ +#define UART1_RX_BUFFER_SIZE 64u + +/** UART1 接收环形缓存,保存尚未交给协议层处理的原始字节。 */ +static volatile uint8_t rx_buffer[UART1_RX_BUFFER_SIZE]; +/** 环形缓存写入位置,由 UART 接收任务推进。 */ +static volatile uint8_t rx_write_index; +/** 环形缓存读取位置,由上层协议任务推进。 */ +static volatile uint8_t rx_read_index; + +/** 初始化 UART1;后续接入 JDY-28M 的 9600 baud 配置。 */ +void Uart1_Init(void) +{ + /* 先关闭 UART,再配置 9600 baud@16 MHz:BRR=0x0683。 */ + REG8(UART1_CR2_ADDR) = 0x00u; + REG8(UART1_BRR2_ADDR) = 0x03u; + REG8(UART1_BRR1_ADDR) = 0x68u; + REG8(UART1_CR1_ADDR) = 0x00u; + REG8(UART1_CR3_ADDR) = 0x00u; + /* RE=1、TE=1、接收中断暂不打开,环形缓冲区接入后再开启。 */ + REG8(UART1_CR2_ADDR) = 0x0Cu; + rx_write_index = 0u; + rx_read_index = 0u; +} + +/** + * 轮询 UART1 接收状态并保存字节。 + * + * 本阶段先采用轮询方式,避免在尚未建立中断向量表前引入中断链接问题。 + * 后续如果业务帧速率提高,可将同一环形缓冲区写入逻辑迁移到 UART RX 中断。 + */ +void Uart1_Task(void) +{ + uint8_t next_index; + + while ((REG8(UART1_SR_ADDR) & 0x20u) != 0u) + { + next_index = (uint8_t)((rx_write_index + 1u) & (UART1_RX_BUFFER_SIZE - 1u)); + + /* 缓冲区满时丢弃当前字节,避免覆盖尚未处理的完整帧。 */ + if (next_index == rx_read_index) + { + (void)REG8(UART1_DR_ADDR); + continue; + } + + rx_buffer[rx_write_index] = REG8(UART1_DR_ADDR); + rx_write_index = next_index; + } +} + +/** 发送一个字节;当前阶段不连接真实 UART 寄存器。 */ +void Uart1_WriteByte(uint8_t value) +{ + while ((REG8(UART1_SR_ADDR) & 0x80u) == 0u) + { + } + REG8(UART1_DR_ADDR) = value; +} + +/** 从接收环形缓冲区读取一个字节;当前阶段始终表示无数据。 */ +uint8_t Uart1_ReadByte(uint8_t *value) +{ + if ((value == (uint8_t *)0) || (rx_read_index == rx_write_index)) + { + return 0u; + } + + *value = rx_buffer[rx_read_index]; + rx_read_index = (uint8_t)((rx_read_index + 1u) & (UART1_RX_BUFFER_SIZE - 1u)); + return 1u; +} + +/** 返回接收缓冲区当前字节数,供协议层判断是否需要继续读取。 */ +uint8_t Uart1_Available(void) +{ + return (uint8_t)((rx_write_index - rx_read_index) & (UART1_RX_BUFFER_SIZE - 1u)); +} + +/** UART1 接收中断入口占位。 */ +void Uart1_InterruptHandler(void) +{ +} diff --git a/src/drivers/watchdog.c b/src/drivers/watchdog.c new file mode 100644 index 0000000..d21e9b0 --- /dev/null +++ b/src/drivers/watchdog.c @@ -0,0 +1,24 @@ +/** + * @file watchdog.c + * @brief STM8S003 独立看门狗实现。 + * + * 看门狗只在主循环末尾刷新。任何任务死循环或长期阻塞超过约 5 秒, + * 都会触发 MCU 复位;复位后 main() 从头执行,继电器首先被置为断开。 + */ +#include "watchdog.h" +#include "stm8s003_registers.h" + +/** 启动看门狗并设置约 5 秒量级的预分频和重装值。 */ +void Watchdog_Init(void) +{ + REG8(IWDG_KR_ADDR) = 0xCCu; + REG8(IWDG_PR_ADDR) = 0x06u; + REG8(IWDG_RLR_ADDR) = 0xFFu; + REG8(IWDG_KR_ADDR) = 0xAAu; +} + +/** 刷新看门狗,防止正常运行时复位。 */ +void Watchdog_Refresh(void) +{ + REG8(IWDG_KR_ADDR) = 0xAAu; +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..e8ae1cb --- /dev/null +++ b/src/main.c @@ -0,0 +1,63 @@ +/** + * @file main.c + * @brief STM8S003F3P6TR 固件第一阶段主入口。 + * + * 当前版本用于验证工程结构和各模块接口。后续实现真实硬件驱动后, + * 主循环仍应保持非阻塞,通过 1 ms 系统节拍驱动蓝牙初始化、LED 状态、 + * ACS712 采样、继电器故障确认和业务协议处理。 + */ +#include + +#include "board.h" +#include "system_tick.h" +#include "uart1.h" +#include "adc_current.h" +#include "business_protocol.h" +#include "mesh_frame.h" +#include "fault_manager.h" +#include "nonvolatile_config.h" +#include "bluetooth_manager.h" +#include "watchdog.h" + +/** + * @brief 固件主函数。 + * + * 上电先完成板级安全初始化,继电器默认断开,然后初始化通信、采样和 + * 协议模块。主循环不使用长时间阻塞延时,保证后续可以同时处理 UART、 + * 电流采样、Mesh 状态和故障恢复计时。 + * + * @return 理论上不会返回。 + */ +int main(void) +{ + Board_Init(); + SystemTick_Init(); + __asm + rim + __endasm; + Uart1_Init(); + Current_Init(); + BusinessProtocol_Init(); + MeshFrame_Init(); + FaultManager_Init(); + BluetoothManager_Init(); + Watchdog_Init(); + + while (1) + { + Uart1_Task(); + Current_Task(); + if (BluetoothManager_IsReady() == 0u) + { + BluetoothManager_Task(); + } + else + { + MeshFrame_Task(); + BluetoothManager_Task(); + } + FaultManager_Task(); + Watchdog_Refresh(); + } + +} diff --git a/src/protocol/bluetooth_manager.c b/src/protocol/bluetooth_manager.c new file mode 100644 index 0000000..95d31f4 --- /dev/null +++ b/src/protocol/bluetooth_manager.c @@ -0,0 +1,293 @@ +/** + * @file bluetooth_manager.c + * @brief JDY-28M 上电初始化状态机。 + * + * 启动顺序: + * 1. 查询 ROLE; + * 2. 若不是 ROLE2,发送 ROLE2 和 RESET; + * 3. 再次确认 ROLE; + * 4. 查询 MADDR,保存 2 字节短地址到 RAM; + * 5. 初始化完成后由 PC3 决定 LED3 常亮或 1 秒闪烁。 + * + * 初始化过程不使用阻塞延时。UART 返回内容只识别关键字符和超时,具体 + * AT 返回格式仍应在首次联调时用串口工具记录并校准。 + */ +#include +#include "app_config.h" +#include "bluetooth_manager.h" +#include "board.h" +#include "system_tick.h" +#include "uart1.h" + +typedef enum +{ + /** 启动流程:准备查询当前 ROLE。 */ + BT_QUERY_ROLE = 0, + /** 等待 AT+ROLE 查询响应。 */ + BT_WAIT_ROLE, + /** 已确认需要切换到 ROLE2,准备发送 AT+ROLE2。 */ + BT_SET_ROLE, + /** 已发送 AT+RESET,等待模块软件复位完成。 */ + BT_WAIT_RESET, + /** 软件复位重试失败后,保持 PC6 低电平执行硬件复位。 */ + BT_HARD_RESET_LOW, + /** 已释放 PC6,等待模块硬件复位后的启动时间。 */ + BT_HARD_RESET_WAIT, + /** 已进入 ROLE2,准备查询 Mesh 短地址。 */ + BT_QUERY_ADDRESS, + /** 等待 AT+MADDR 返回完整短地址。 */ + BT_WAIT_ADDRESS, + /** ROLE2、短地址和启动检查均成功,允许进入业务通信。 */ + BT_READY, + /** 蓝牙初始化失败,只保留故障 LED 指示。 */ + BT_FAULT +} BluetoothState; + +/** AT 指令等待响应的最大时间,单位为毫秒。 */ +#define BT_TIMEOUT_MS 1000ul +/** AT+RESET 后等待模块重新启动的时间,单位为毫秒。 */ +#define BT_RESET_WAIT_MS 1500ul + +/** 蓝牙初始化状态机的当前状态。 */ +static BluetoothState bluetooth_state; +/** 当前状态开始时的系统毫秒计数,用于实现非阻塞超时。 */ +static uint32_t state_start_ms; +/** AT+MADDR 查询得到的设备短地址,仅在本次上电期间保存。 */ +static uint16_t short_address; +/** ROLE 响应中是否识别到 ROLE2 的标志,1 表示已识别。 */ +static uint8_t response_has_role2; +/** 已解析出的短地址十六进制字符数量,完整地址应为 4 位。 */ +static uint8_t response_address_digits; +/** 正在解析的短地址临时数值。 */ +static uint16_t response_address_value; +/** 软件 AT+RESET 已尝试次数,达到 3 次后切换硬件复位。 */ +static uint8_t software_reset_attempts; +/** LED3 当前闪烁节拍的起始时间。 */ +static uint32_t led_start_ms; +/** LED3 当前逻辑状态,1 表示点亮,0 表示熄灭。 */ +static uint8_t led_state; +/** 最近一次采样到的 PC3 电平,1 表示模块报告已组网。 */ +static uint8_t mesh_candidate_state; +/** 已经连续确认的 PC3 组网状态。 */ +static uint8_t mesh_confirmed_state; +/** 候选状态开始保持的时间。 */ +static uint32_t mesh_candidate_start_ms; + +/** + * 拉低 PWRC 后等待规定时间。 + * + * 该等待只发生在启动阶段的 AT 指令发送前,时间为 10ms,不会出现在 + * 正常 Mesh 数据收发路径中。系统节拍中断仍然运行,因此不会影响超时计时。 + */ +static void BluetoothManager_WaitAtWindow(void) +{ + uint32_t start = SystemTick_NowMs(); + while ((SystemTick_NowMs() - start) < JDY28M_AT_ENTRY_WAIT_MS) + { + /* 等待 PWRC 生效期间不执行其他耗时业务。 */ + } +} + +/** 发送一条 AT 命令,统一补充 CRLF,避免不同调用点格式不一致。 */ +static void BluetoothManager_SendCommand(const char *command) +{ + Board_SetBluetoothAtWindow(1u); + BluetoothManager_WaitAtWindow(); + while (*command != '\0') Uart1_WriteByte((uint8_t)*command++); + Uart1_WriteByte('\r'); + Uart1_WriteByte('\n'); +} + +/** 进入新的初始化状态并记录状态开始时间。 */ +static void BluetoothManager_Enter(BluetoothState state) +{ + bluetooth_state = state; + state_start_ms = SystemTick_NowMs(); +} + +/** 初始化蓝牙状态机和 RAM 中的短地址。 */ +void BluetoothManager_Init(void) +{ + bluetooth_state = BT_QUERY_ROLE; + state_start_ms = 0ul; + short_address = 0u; + response_has_role2 = 0u; + response_address_digits = 0u; + response_address_value = 0u; + software_reset_attempts = 0u; + led_start_ms = 0ul; + led_state = 0u; + mesh_candidate_state = 0u; + mesh_confirmed_state = 0u; + mesh_candidate_start_ms = 0ul; + Board_SetLed3(0u); +} + +/** 从 UART 返回流中提取 ROLE2 和 4 位十六进制短地址。 */ +static void BluetoothManager_ConsumeResponse(void) +{ + uint8_t value; + while (Uart1_ReadByte(&value) != 0u) + { + if ((value == '2') && (bluetooth_state == BT_WAIT_ROLE)) + { + response_has_role2 = 1u; + } + if ((bluetooth_state == BT_WAIT_ADDRESS) && (response_address_digits < 4u)) + { + uint8_t digit = 0xFFu; + if ((value >= '0') && (value <= '9')) digit = (uint8_t)(value - '0'); + else if ((value >= 'A') && (value <= 'F')) digit = (uint8_t)(value - 'A' + 10u); + else if ((value >= 'a') && (value <= 'f')) digit = (uint8_t)(value - 'a' + 10u); + if (digit != 0xFFu) + { + response_address_value = (uint16_t)((response_address_value << 4u) | digit); + ++response_address_digits; + } + } + } +} + +/** 根据初始化故障、组网状态和计时驱动 LED3。 */ +static void BluetoothManager_UpdateLed(void) +{ + uint32_t now = SystemTick_NowMs(); + uint32_t period; + uint8_t physical_state = Board_ReadMeshJoined(); + + /* PC3 瞬态变化不能直接反映到 LED。只有同一个电平连续保持至少 + * MESH_STATUS_CONFIRM_MS,才更新确认状态,避免无线组网瞬态导致 LED + * 快速抖动。 */ + if (physical_state != mesh_candidate_state) + { + mesh_candidate_state = physical_state; + mesh_candidate_start_ms = now; + } + else if ((mesh_confirmed_state != mesh_candidate_state) && + ((now - mesh_candidate_start_ms) >= MESH_STATUS_CONFIRM_MS)) + { + mesh_confirmed_state = mesh_candidate_state; + } + if (bluetooth_state == BT_FAULT) + { + period = LED_INIT_FAULT_PERIOD_MS; + } + else if (bluetooth_state != BT_READY) + { + Board_SetLed3(0u); + return; + } + else if (mesh_confirmed_state == 0u) + { + period = LED_NOT_JOINED_PERIOD_MS; + } + else + { + Board_SetLed3(1u); + return; + } + if ((now - led_start_ms) >= period) + { + led_start_ms = now; + led_state = (uint8_t)!led_state; + Board_SetLed3(led_state); + } +} + +/** 执行蓝牙初始化状态机、地址查询和 LED 状态显示。 */ +void BluetoothManager_Task(void) +{ + uint32_t now = SystemTick_NowMs(); + BluetoothManager_ConsumeResponse(); + switch (bluetooth_state) + { + case BT_QUERY_ROLE: + BluetoothManager_SendCommand("AT+ROLE"); + response_has_role2 = 0u; + BluetoothManager_Enter(BT_WAIT_ROLE); + break; + case BT_WAIT_ROLE: + if (response_has_role2 != 0u) + { + Board_SetBluetoothAtWindow(0u); + BluetoothManager_SendCommand("AT+MADDR"); + response_address_digits = 0u; + response_address_value = 0u; + BluetoothManager_Enter(BT_WAIT_ADDRESS); + } + else if ((now - state_start_ms) >= BT_TIMEOUT_MS) + { + Board_SetBluetoothAtWindow(0u); + if (software_reset_attempts >= 3u) + { + Board_SetBluetoothReset(1u); + BluetoothManager_Enter(BT_HARD_RESET_LOW); + } + else + { + BluetoothManager_SendCommand("AT+ROLE2"); + BluetoothManager_Enter(BT_SET_ROLE); + } + } + break; + case BT_SET_ROLE: + if ((now - state_start_ms) >= BT_TIMEOUT_MS) + { + Board_SetBluetoothAtWindow(0u); + BluetoothManager_SendCommand("AT+RESET"); + ++software_reset_attempts; + BluetoothManager_Enter(BT_WAIT_RESET); + } + break; + case BT_WAIT_RESET: + if ((now - state_start_ms) >= BT_RESET_WAIT_MS) + { + Board_SetBluetoothAtWindow(0u); + BluetoothManager_Enter(BT_QUERY_ROLE); + } + break; + case BT_HARD_RESET_LOW: + if ((now - state_start_ms) >= JDY28M_RESET_PULSE_MS) + { + Board_SetBluetoothReset(0u); + BluetoothManager_Enter(BT_HARD_RESET_WAIT); + } + break; + case BT_HARD_RESET_WAIT: + if ((now - state_start_ms) >= JDY28M_RESET_STARTUP_WAIT_MS) + { + software_reset_attempts = 0u; + BluetoothManager_Enter(BT_QUERY_ROLE); + } + break; + case BT_WAIT_ADDRESS: + if (response_address_digits >= 4u) + { + short_address = response_address_value; + BluetoothManager_Enter(BT_READY); + } + else if ((now - state_start_ms) >= BT_TIMEOUT_MS) + { + Board_SetBluetoothAtWindow(0u); + BluetoothManager_Enter(BT_FAULT); + } + break; + case BT_READY: + break; + case BT_FAULT: + break; + default: + BluetoothManager_Enter(BT_FAULT); + break; + } + BluetoothManager_UpdateLed(); +} + +/** 返回蓝牙初始化完成状态。 */ +uint8_t BluetoothManager_IsReady(void) { return (bluetooth_state == BT_READY) ? 1u : 0u; } + +/** 返回蓝牙初始化故障状态。 */ +uint8_t BluetoothManager_IsFault(void) { return (bluetooth_state == BT_FAULT) ? 1u : 0u; } + +/** 返回本次上电从模块查询到的短地址。 */ +uint16_t BluetoothManager_GetShortAddress(void) { return short_address; } diff --git a/src/protocol/business_protocol.c b/src/protocol/business_protocol.c new file mode 100644 index 0000000..376ef3f --- /dev/null +++ b/src/protocol/business_protocol.c @@ -0,0 +1,348 @@ +/** + * @file business_protocol.c + * @brief 业务协议第一阶段实现。 + */ +#include +#include "app_config.h" +#include "business_protocol.h" +#include "uart1.h" +#include "board.h" +#include "adc_current.h" +#include "mesh_frame.h" +#include "fault_manager.h" +#include "bluetooth_manager.h" + +/** 业务帧解析状态。 */ +typedef enum +{ + /** 等待业务帧头第一个字节 0x55。 */ + PARSER_WAIT_HEAD_0 = 0, + /** 已收到 0x55,等待帧头第二个字节 0xAA。 */ + PARSER_WAIT_HEAD_1, + /** 等待命令字节。 */ + PARSER_WAIT_CMD, + /** 等待 DATA 长度字节。 */ + PARSER_WAIT_LENGTH, + /** 按 LEN 接收 DATA 区。 */ + PARSER_WAIT_DATA, + /** 等待 CRC 低字节。 */ + PARSER_WAIT_CRC_LOW, + /** 等待 CRC 高字节并完成校验。 */ + PARSER_WAIT_CRC_HIGH +} BusinessParserState; + +/** 解析器当前状态。 */ +static BusinessParserState parser_state; +/** 当前正在接收的命令。 */ +static uint8_t parser_command; +/** 当前正在接收的数据长度。 */ +static uint8_t parser_length; +/** 当前已经接收的数据字节数。 */ +static uint8_t parser_data_index; +/** 当前正在接收的数据区。 */ +static uint8_t parser_data[BUSINESS_MAX_DATA_LENGTH]; +/** 当前正在接收的 CRC。 */ +static uint16_t parser_received_crc; +/** CRC 计算临时区,依次保存 CMD、LEN 和 DATA。 */ +static uint8_t parser_crc_input[2u + BUSINESS_MAX_DATA_LENGTH]; +/** 最近一帧合法业务帧的内容。 */ +static uint8_t last_command; +/** 最近一帧合法业务帧的 DATA 长度。 */ +static uint8_t last_length; +/** 最近一帧合法业务帧的 DATA 内容。 */ +static uint8_t last_data[BUSINESS_MAX_DATA_LENGTH]; + +/** 发送一个只有 1 字节数据的业务应答。 */ +static void BusinessProtocol_SendByteResponse(uint16_t address, + uint8_t command, + uint8_t value) +{ + (void)MeshFrame_SendBusiness(address, command, &value, 1u); +} + +/** 初始化业务协议状态机;完整解析器在第二阶段实现。 */ +void BusinessProtocol_Init(void) +{ + parser_state = PARSER_WAIT_HEAD_0; + parser_command = 0u; + parser_length = 0u; + parser_data_index = 0u; + parser_received_crc = 0u; + last_command = 0u; + last_length = 0u; +} + +/** 处理 UART 数据和待发送帧;当前阶段仅保留任务入口。 */ +void BusinessProtocol_Task(void) +{ + uint8_t value; + + while (Uart1_ReadByte(&value) != 0u) + { + /* 当前阶段仅消费 UART 字节,正式接入时由 Mesh 外层解析器调用 + * BusinessProtocol_InputByte(),避免把外层地址和帧尾误当业务数据。 */ + (void)BusinessProtocol_InputByte(value); + } +} + +/** + * 向业务帧状态机输入一个字节。 + * + * 帧格式为 `55 AA CMD LEN DATA CRC16`。CRC 计算范围为 CMD、LEN 和 + * DATA;线上 CRC 低字节在前。CRC 错误会恢复到等待帧头状态,不执行 + * 任何控制动作。 + */ +uint8_t BusinessProtocol_InputByte(uint8_t value) +{ + uint16_t expected_crc; + uint8_t index; + + switch (parser_state) + { + case PARSER_WAIT_HEAD_0: + if (value == BUSINESS_FRAME_HEAD_0) + { + parser_state = PARSER_WAIT_HEAD_1; + } + break; + + case PARSER_WAIT_HEAD_1: + parser_state = (value == BUSINESS_FRAME_HEAD_1) ? + PARSER_WAIT_CMD : PARSER_WAIT_HEAD_0; + break; + + case PARSER_WAIT_CMD: + parser_command = value; + parser_state = PARSER_WAIT_LENGTH; + break; + + case PARSER_WAIT_LENGTH: + parser_length = value; + parser_data_index = 0u; + if (parser_length > BUSINESS_MAX_DATA_LENGTH) + { + parser_state = PARSER_WAIT_HEAD_0; + } + else if (parser_length == 0u) + { + parser_state = PARSER_WAIT_CRC_LOW; + } + else + { + parser_state = PARSER_WAIT_DATA; + } + break; + + case PARSER_WAIT_DATA: + parser_data[parser_data_index++] = value; + if (parser_data_index >= parser_length) + { + parser_state = PARSER_WAIT_CRC_LOW; + } + break; + + case PARSER_WAIT_CRC_LOW: + parser_received_crc = value; + parser_state = PARSER_WAIT_CRC_HIGH; + break; + + case PARSER_WAIT_CRC_HIGH: + parser_received_crc |= (uint16_t)value << 8u; + parser_crc_input[0] = parser_command; + parser_crc_input[1] = parser_length; + for (index = 0u; index < parser_length; ++index) + { + parser_crc_input[2u + index] = parser_data[index]; + } + expected_crc = BusinessProtocol_Crc16Modbus(parser_crc_input, + (uint16_t)(2u + parser_length)); + parser_state = PARSER_WAIT_HEAD_0; + if (expected_crc != parser_received_crc) + { + return 0u; + } + + last_command = parser_command; + last_length = parser_length; + for (index = 0u; index < last_length; ++index) + { + last_data[index] = parser_data[index]; + } + return 1u; + + default: + parser_state = PARSER_WAIT_HEAD_0; + break; + } + + return 0u; +} + +/** 读取最近一帧合法业务帧的命令字。 */ +uint8_t BusinessProtocol_GetCommand(void) +{ + return last_command; +} + +/** 读取最近一帧合法业务帧的数据长度。 */ +uint8_t BusinessProtocol_GetDataLength(void) +{ + return last_length; +} + +/** 读取最近一帧合法业务帧的数据区字节。 */ +uint8_t BusinessProtocol_GetDataByte(uint8_t index, uint8_t *value) +{ + if ((value == (uint8_t *)0) || (index >= last_length)) + { + return 0u; + } + *value = last_data[index]; + return 1u; +} + +/** + * 执行当前合法业务帧。 + * + * 当前先接入三项无副作用或低风险基础命令:继电器控制、继电器状态查询 + * 和实际电流查询。控制命令只有 DATA=00/01 才会改变 PA1;其他参数只返 + * 回错误应答,不会误动作。应答目标使用接收帧中的源短地址。 + */ +void BusinessProtocol_HandleFrame(uint16_t source_address) +{ + uint8_t value; + uint8_t current_data[2u]; + uint16_t current; + uint16_t threshold; + + switch (last_command) + { + case CMD_RELAY_CONTROL: + if ((last_length != 1u) || ((last_data[0] != 0u) && (last_data[0] != 1u))) + { + value = 0x03u; + BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + break; + } + Board_SetRelay(last_data[0]); + BusinessProtocol_SendByteResponse(source_address, + CMD_RELAY_CONTROL | CMD_RESPONSE_FLAG, + Board_GetRelay()); + break; + + case CMD_RELAY_QUERY: + if (last_length != 0u) + { + value = 0x02u; + BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + break; + } + BusinessProtocol_SendByteResponse(source_address, + CMD_RELAY_QUERY | CMD_RESPONSE_FLAG, + Board_GetRelay()); + break; + + case CMD_CURRENT_QUERY: + if (last_length != 0u) + { + value = 0x02u; + BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + break; + } + current = Current_GetMilliAmpere(); + current_data[0] = (uint8_t)current; + current_data[1] = (uint8_t)(current >> 8u); + (void)MeshFrame_SendBusiness(source_address, + CMD_CURRENT_QUERY | CMD_RESPONSE_FLAG, + current_data, 2u); + break; + + case (CMD_FAULT_REPORT | CMD_RESPONSE_FLAG): + if (last_length == 1u) + { + FaultManager_OnReportAck(last_data[0]); + } + break; + + case CMD_THRESHOLD_CONFIG: + if (last_length != 2u) + { + value = 0x02u; + BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + break; + } + threshold = (uint16_t)last_data[0] | ((uint16_t)last_data[1] << 8u); + if ((threshold == 0u) || (threshold > 30000u)) + { + value = 0x03u; + BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + break; + } + if (FaultManager_SetThreshold(threshold) == 0u) + { + value = 0x05u; + BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + break; + } + (void)MeshFrame_SendBusiness(source_address, + CMD_THRESHOLD_CONFIG | CMD_RESPONSE_FLAG, + last_data, 2u); + break; + + case CMD_ADDRESS_QUERY: + if (last_length != 0u) + { + value = 0x02u; + BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + break; + } + current_data[0] = (uint8_t)(BluetoothManager_GetShortAddress() >> 8u); + current_data[1] = (uint8_t)BluetoothManager_GetShortAddress(); + (void)MeshFrame_SendBusiness(source_address, + CMD_ADDRESS_QUERY | CMD_RESPONSE_FLAG, + current_data, 2u); + break; + + default: + value = 0x06u; + BusinessProtocol_SendByteResponse(source_address, CMD_ERROR_RESPONSE, value); + break; + } +} + +/** + * 计算 CRC-16/MODBUS。 + * + * @param data 待计算数据指针,不能为 NULL。 + * @param length 数据长度。 + * @return CRC 结果;发送时低字节在前。 + */ +uint16_t BusinessProtocol_Crc16Modbus(const uint8_t *data, uint16_t length) +{ + uint16_t crc = 0xFFFFu; + uint16_t index; + uint8_t bit; + + if (data == (const uint8_t *)0) + { + return 0u; + } + + for (index = 0u; index < length; ++index) + { + crc ^= data[index]; + for (bit = 0u; bit < 8u; ++bit) + { + if ((crc & 0x0001u) != 0u) + { + crc = (uint16_t)((crc >> 1u) ^ 0xA001u); + } + else + { + crc >>= 1u; + } + } + } + + return crc; +} diff --git a/src/protocol/fault_manager.c b/src/protocol/fault_manager.c new file mode 100644 index 0000000..00d9b07 --- /dev/null +++ b/src/protocol/fault_manager.c @@ -0,0 +1,166 @@ +/** + * @file fault_manager.c + * @brief 继电器电流一致性故障检测实现。 + * + * 故障码约定: + * 0x00:继电器指令为吸合,但连续确认时间内没有有效电流; + * 0x01:继电器指令为断开,但连续确认时间内仍有有效电流; + * 0x04:电流故障恢复; + * 0x06:ACS712 采样故障恢复(采样异常检测尚待硬件量程和异常边界确认)。 + * + * 本实现使用 SystemTick_NowMs() 做非阻塞计时,不在故障判断中使用 delay, + * 这样 UART 接收、Mesh 解析和 ADC 采样仍然能够持续运行。 + */ +#include +#include "app_config.h" +#include "board.h" +#include "adc_current.h" +#include "business_protocol.h" +#include "fault_manager.h" +#include "mesh_frame.h" +#include "nonvolatile_config.h" +#include "system_tick.h" + +#define FAULT_RELAY_ON_NO_CURRENT 0x00u +#define FAULT_RELAY_OFF_HAS_CURRENT 0x01u +#define FAULT_CURRENT_RECOVERED 0x04u + +/** 故障上报采用广播,使主机能够接收设备主动上报。 */ +#define FAULT_REPORT_TARGET 0xFFFFu + +/** 当前有效电流阈值,默认 100mA,后续由 ROM 配置覆盖。 */ +static uint16_t fault_threshold_ma; +/** 当前正在连续确认的异常类型,0 表示没有异常确认。 */ +static uint8_t pending_fault_code; +/** 连续异常确认开始时间。 */ +static uint32_t pending_fault_start_ms; +/** 当前已经确认并处于活动状态的电流故障码。 */ +static uint8_t active_fault_code; +/** 上一次故障/恢复报告发送时间。 */ +static uint32_t last_report_ms; +/** 是否存在等待主机 ACK 的故障报告。 */ +static uint8_t report_waiting_ack; + +/** 初始化所有故障状态,避免上电瞬间使用未初始化的计时数据。 */ +void FaultManager_Init(void) +{ + fault_threshold_ma = NonvolatileConfig_ReadThreshold(); + if (fault_threshold_ma == 0u) + { + fault_threshold_ma = DEFAULT_CURRENT_THRESHOLD_MA; + } + pending_fault_code = 0xFFu; + pending_fault_start_ms = 0ul; + active_fault_code = 0xFFu; + last_report_ms = 0ul; + report_waiting_ack = 0u; +} + +/** 发送一字节故障码的主动上报帧。 */ +static void FaultManager_SendReport(uint8_t code) +{ + (void)MeshFrame_SendBusiness(FAULT_REPORT_TARGET, CMD_FAULT_REPORT, &code, 1u); + last_report_ms = SystemTick_NowMs(); + report_waiting_ack = 1u; +} + +/** + * 周期性处理故障状态。 + * + * 只有当电流状态连续保持异常满 10 秒,才把待确认异常升级为活动故障。 + * 一旦电流恢复并连续保持正常满 10 秒,则发送 0x04 恢复报告。报告未获 + * 得匹配的 0x84 ACK 时,严格每 10 秒重发,不重复改变继电器输出。 + */ +void FaultManager_Task(void) +{ + uint32_t now = SystemTick_NowMs(); + uint8_t relay_on = Board_GetRelay(); + uint8_t abnormal = 0u; + uint8_t expected_fault = 0xFFu; + uint16_t current_ma = Current_GetMilliAmpere(); + + if ((relay_on != 0u) && (current_ma < fault_threshold_ma)) + { + abnormal = 1u; + expected_fault = FAULT_RELAY_ON_NO_CURRENT; + } + else if ((relay_on == 0u) && (current_ma >= fault_threshold_ma)) + { + abnormal = 1u; + expected_fault = FAULT_RELAY_OFF_HAS_CURRENT; + } + + if (abnormal != 0u) + { + if (pending_fault_code != expected_fault) + { + pending_fault_code = expected_fault; + pending_fault_start_ms = now; + } + else if ((active_fault_code != expected_fault) && + ((now - pending_fault_start_ms) >= CURRENT_FAULT_CONFIRM_MS)) + { + active_fault_code = expected_fault; + FaultManager_SendReport(active_fault_code); + } + } + else if (active_fault_code != 0xFFu) + { + if (pending_fault_code != 0xFEu) + { + pending_fault_code = 0xFEu; + pending_fault_start_ms = now; + } + else if ((now - pending_fault_start_ms) >= CURRENT_FAULT_CONFIRM_MS) + { + active_fault_code = 0xFFu; + FaultManager_SendReport(FAULT_CURRENT_RECOVERED); + } + } + else + { + pending_fault_code = 0xFFu; + } + + if ((report_waiting_ack != 0u) && + ((now - last_report_ms) >= CURRENT_FAULT_CONFIRM_MS)) + { + if (active_fault_code != 0xFFu) + { + FaultManager_SendReport(active_fault_code); + } + else + { + FaultManager_SendReport(FAULT_CURRENT_RECOVERED); + } + } +} + +/** 处理匹配故障码 ACK,停止当前报告的 10 秒重发。 */ +void FaultManager_OnReportAck(uint8_t fault_code) +{ + if ((report_waiting_ack != 0u) && + ((fault_code == active_fault_code) || + ((fault_code == FAULT_CURRENT_RECOVERED) && (active_fault_code == 0xFFu)))) + { + report_waiting_ack = 0u; + } +} + +/** 设置电流阈值;参数为 0 时拒绝修改,避免关闭故障检测。 */ +uint8_t FaultManager_SetThreshold(uint16_t threshold_ma) +{ + if ((threshold_ma != 0u) && + (NonvolatileConfig_WriteThreshold(threshold_ma) != 0u)) + { + fault_threshold_ma = threshold_ma; + return 1u; + } + return 0u; +} + +/** 返回当前电流阈值。 */ +uint16_t FaultManager_GetThreshold(void) +{ + return fault_threshold_ma; +} diff --git a/src/protocol/mesh_frame.c b/src/protocol/mesh_frame.c new file mode 100644 index 0000000..ef69a0a --- /dev/null +++ b/src/protocol/mesh_frame.c @@ -0,0 +1,219 @@ +/** + * @file mesh_frame.c + * @brief JDY-28M Mesh 外层帧实现。 + * + * 发送格式:41 54 2B AA A1 | 目标地址高低字节 | A0 | 业务帧 | 0D 0A。 + * 接收格式:F1 DD 09 00 0A | 源地址高低字节 | 业务帧 | 0D 0A。 + */ +#include +#include "business_protocol.h" +#include "mesh_frame.h" +#include "uart1.h" + +#define RX_PREFIX_0 0xF1u +#define RX_PREFIX_1 0xDDu +#define RX_PREFIX_2 0x09u +#define RX_PREFIX_3 0x00u +#define RX_PREFIX_4 0x0Au +#define TX_PREFIX_0 0x41u +#define TX_PREFIX_1 0x54u +#define TX_PREFIX_2 0x2Bu +#define TX_PREFIX_3 0xAAu +#define TX_PREFIX_4 0xA1u +#define MESH_DATA_FLAG 0xA0u +#define MESH_TAIL_0 0x0Du +#define MESH_TAIL_1 0x0Au + +/** 外层解析状态。 */ +typedef enum +{ + /** 等待接收头 F1。 */ + MESH_WAIT_PREFIX_0 = 0, + /** 已收到 F1,等待 DD。 */ + MESH_WAIT_PREFIX_1, + /** 等待固定头第三字节 09。 */ + MESH_WAIT_PREFIX_2, + /** 等待固定头第四字节 00。 */ + MESH_WAIT_PREFIX_3, + /** 等待固定头第五字节 0A。 */ + MESH_WAIT_PREFIX_4, + /** 接收源地址高字节。 */ + MESH_READ_SOURCE_HIGH, + /** 接收源地址低字节。 */ + MESH_READ_SOURCE_LOW, + /** 接收内层业务帧。 */ + MESH_READ_BUSINESS, + /** 等待外层帧尾 0D。 */ + MESH_WAIT_TAIL_0, + /** 等待外层帧尾 0A。 */ + MESH_WAIT_TAIL_1 +} MeshParserState; + +/** JDY-28M 外层接收状态机当前状态。 */ +static MeshParserState mesh_state; +/** 最近一帧合法外层数据携带的源短地址。 */ +static uint16_t last_source_address; +/** 当前正在接收的业务帧缓冲区。 */ +static uint8_t business_buffer[BUSINESS_MAX_DATA_LENGTH + 6u]; +/** 当前已经接收的业务帧字节数。 */ +static uint8_t business_length; +/** 根据业务 LEN 计算出的完整业务帧长度。 */ +static uint8_t business_expected_length; + +/** 将解析器恢复到等待 F1 的状态。 */ +static void MeshFrame_ResetParser(void) +{ + mesh_state = MESH_WAIT_PREFIX_0; + business_length = 0u; + business_expected_length = 0u; +} + +/** 初始化外层帧解析器。 */ +void MeshFrame_Init(void) +{ + last_source_address = 0u; + MeshFrame_ResetParser(); +} + +/** + * 处理一个外层 UART 字节。 + * + * 业务帧长度由 LEN 字段确定,因此收齐业务帧后只接受紧随其后的 0D 0A; + * 只有内层 CRC 通过时,才把数据提交给业务协议层。 + */ +static void MeshFrame_InputByte(uint8_t value) +{ + uint8_t expected_length; + + switch (mesh_state) + { + case MESH_WAIT_PREFIX_0: + if (value == RX_PREFIX_0) mesh_state = MESH_WAIT_PREFIX_1; + break; + case MESH_WAIT_PREFIX_1: + mesh_state = (value == RX_PREFIX_1) ? MESH_WAIT_PREFIX_2 : MESH_WAIT_PREFIX_0; + break; + case MESH_WAIT_PREFIX_2: + mesh_state = (value == RX_PREFIX_2) ? MESH_WAIT_PREFIX_3 : MESH_WAIT_PREFIX_0; + break; + case MESH_WAIT_PREFIX_3: + mesh_state = (value == RX_PREFIX_3) ? MESH_WAIT_PREFIX_4 : MESH_WAIT_PREFIX_0; + break; + case MESH_WAIT_PREFIX_4: + mesh_state = (value == RX_PREFIX_4) ? MESH_READ_SOURCE_HIGH : MESH_WAIT_PREFIX_0; + break; + case MESH_READ_SOURCE_HIGH: + last_source_address = (uint16_t)value << 8u; + mesh_state = MESH_READ_SOURCE_LOW; + break; + case MESH_READ_SOURCE_LOW: + last_source_address |= value; + business_length = 0u; + mesh_state = MESH_READ_BUSINESS; + break; + case MESH_READ_BUSINESS: + if (business_length >= (uint8_t)sizeof(business_buffer)) + { + MeshFrame_ResetParser(); + break; + } + business_buffer[business_length++] = value; + if (business_length >= 4u) + { + if ((business_buffer[0] != 0x55u) || (business_buffer[1] != 0xAAu)) + { + MeshFrame_ResetParser(); + break; + } + expected_length = (uint8_t)(business_buffer[3] + 6u); + business_expected_length = expected_length; + if ((business_buffer[3] > BUSINESS_MAX_DATA_LENGTH) || + (business_length > expected_length)) + { + MeshFrame_ResetParser(); + break; + } + if (business_length == expected_length) + { + /* 先确认外层 0D 0A,再提交内层业务帧。 */ + mesh_state = MESH_WAIT_TAIL_0; + } + } + break; + case MESH_WAIT_TAIL_0: + mesh_state = (value == MESH_TAIL_0) ? MESH_WAIT_TAIL_1 : MESH_WAIT_PREFIX_0; + break; + case MESH_WAIT_TAIL_1: + if (value == MESH_TAIL_1) + { + uint8_t index; + uint8_t frame_ready = 0u; + for (index = 0u; index < business_expected_length; ++index) + { + if (BusinessProtocol_InputByte(business_buffer[index]) != 0u) + { + frame_ready = 1u; + } + } + if (frame_ready != 0u) + { + BusinessProtocol_HandleFrame(last_source_address); + } + } + MeshFrame_ResetParser(); + break; + default: + MeshFrame_ResetParser(); + break; + } +} + +/** 从 UART 接收并解析 JDY-28M 外层帧。 */ +void MeshFrame_Task(void) +{ + uint8_t value; + while (Uart1_ReadByte(&value) != 0u) + { + MeshFrame_InputByte(value); + } +} + +/** 返回最近一帧合法外层数据的源地址。 */ +uint16_t MeshFrame_GetLastSourceAddress(void) +{ + return last_source_address; +} + +/** 封装并发送一帧业务数据。 */ +uint8_t MeshFrame_SendBusiness(uint16_t target_address, + uint8_t command, + const uint8_t *data, + uint8_t length) +{ + uint8_t crc_input[2u + BUSINESS_MAX_DATA_LENGTH]; + uint16_t crc; + uint8_t index; + + if ((length > BUSINESS_MAX_DATA_LENGTH) || + ((data == (const uint8_t *)0) && (length != 0u))) + { + return 0u; + } + crc_input[0] = command; + crc_input[1] = length; + for (index = 0u; index < length; ++index) crc_input[2u + index] = data[index]; + crc = BusinessProtocol_Crc16Modbus(crc_input, (uint16_t)(2u + length)); + + Uart1_WriteByte(TX_PREFIX_0); Uart1_WriteByte(TX_PREFIX_1); + Uart1_WriteByte(TX_PREFIX_2); Uart1_WriteByte(TX_PREFIX_3); + Uart1_WriteByte(TX_PREFIX_4); + Uart1_WriteByte((uint8_t)(target_address >> 8u)); + Uart1_WriteByte((uint8_t)target_address); + Uart1_WriteByte(MESH_DATA_FLAG); + Uart1_WriteByte(0x55u); Uart1_WriteByte(0xAAu); + Uart1_WriteByte(command); Uart1_WriteByte(length); + for (index = 0u; index < length; ++index) Uart1_WriteByte(data[index]); + Uart1_WriteByte((uint8_t)crc); Uart1_WriteByte((uint8_t)(crc >> 8u)); + Uart1_WriteByte(MESH_TAIL_0); Uart1_WriteByte(MESH_TAIL_1); + return 1u; +}