diff --git a/.clang-format b/.clang-format index 42c6af7..2f6ac70 100644 --- a/.clang-format +++ b/.clang-format @@ -18,3 +18,4 @@ IndentCaseLabels: true SortIncludes: false AlignConsecutiveMacros: AcrossEmptyLines AlignConsecutiveAssignments: Consecutive +BoolAcrossComments: true diff --git a/.clangd b/.clangd index 26f4dd1..42ac1d4 100644 --- a/.clangd +++ b/.clangd @@ -1,17 +1,46 @@ CompileFlags: - Add: - - -isystem/usr/arm-none-eabi/include/c++/15.2.1 - - -isystem/usr/arm-none-eabi/include/c++/15.2.1/arm-none-eabi/thumb/v7e-m+fp/hard - - -isystem/usr/arm-none-eabi/include/c++/15.2.1/backward - - -isystem/usr/lib/gcc/arm-none-eabi/15.2.1/include - - -isystem/usr/lib/gcc/arm-none-eabi/15.2.1/include-fixed - - -isystem/usr/arm-none-eabi/include - Remove: - - -fcallgraph-info* - - -mthumb-interwork - CompilationDatabase: ./build/Debug - Compiler: /usr/bin/arm-none-eabi-gcc + Add: + - --target=arm-none-eabi + - -mcpu=cortex-m4 + - -mthumb + - -mfpu=fpv4-sp-d16 + - -mfloat-abi=hard + - -std=gnu23 + - -DUSE_HAL_DRIVER + - -DSTM32F407xx + - -I${workspace}/Application/Inc + - -I${workspace}/Application/Inc/IAP + - -I${workspace}/Application/Inc/LDI + - -I${workspace}/Application/Inc/ProtocolParser_QingHai + - -I${workspace}/Application/Inc/AH_MQTT + - -I${workspace}/Application/Inc/RLS + - -I${workspace}/Application/Inc/Channel + - -I${workspace}/Device/Inc + - -I${workspace}/Platform/Inc + - -I${workspace}/Kernel/Inc + - -I${workspace}/Core/Inc + - -I${workspace}/Drivers/CMSIS/Include + - -I${workspace}/Drivers/CMSIS/Device/ST/STM32F4xx/Include + - -I${workspace}/Drivers/STM32F4xx_HAL_Driver/Inc + - -I${workspace}/Middlewares/Third_Party/SEGGER_RTT + - -I${workspace}/Middlewares/Third_Party/LwIP/src/include + - -I${workspace}/Middlewares/Third_Party/LwIP/system + - -I${workspace}/Middlewares/Third_Party/FreeRTOS/Source/include + - -I${workspace}/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2 + - -I${workspace}/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F + - -I${workspace}/Compiler + - -isystem/home/yystation/EnvTools/embedded-toolchain/arm-gnu-toolchain/arm-none-eabi/include/c++/15.2.1 + - -isystem/home/yystation/EnvTools/embedded-toolchain/arm-gnu-toolchain/arm-none-eabi/include/c++/15.2.1/arm-none-eabi/thumb/v7e-m+fp/hard + - -isystem/home/yystation/EnvTools/embedded-toolchain/arm-gnu-toolchain/arm-none-eabi/include/c++/15.2.1/backward + - -isystem/home/yystation/EnvTools/embedded-toolchain/arm-gnu-toolchain/lib/gcc/arm-none-eabi/15.2.1/include + - -isystem/home/yystation/EnvTools/embedded-toolchain/arm-gnu-toolchain/lib/gcc/arm-none-eabi/15.2.1/include-fixed + - -isystem/home/yystation/EnvTools/embedded-toolchain/arm-gnu-toolchain/arm-none-eabi/include + Remove: + - -fcallgraph-info* + - -mthumb-interwork + Compiler: /home/yystation/EnvTools/embedded-toolchain/arm-gnu-toolchain/bin/arm-none-eabi-gcc + CompilationDatabase: ./build/Debug Diagnostics: - Suppress: - - unused-includes - - drv_unknown_argument + Suppress: + - unused-includes + - drv_unknown_argument diff --git a/.eide/eide.yml b/.eide/eide.yml index 6ce940a..2801a38 100644 --- a/.eide/eide.yml +++ b/.eide/eide.yml @@ -11,12 +11,19 @@ virtualFolder: - name: Application files: - path: Application/Src/app_boot.c + - path: Application/Src/app_net_boot.c + - path: Application/Src/app_default_display.c - path: Application/Src/app_render.c - path: Application/Src/app_key.c - path: Application/Src/app_light_sensor.c + - path: Application/Src/app_uart_baud.c - path: Application/Src/app_test.c - path: Application/Src/app_factory_test.c folders: + - name: config + files: + - path: Application/Src/Config/app_board_net_cfg.c + folders: [] - name: channel files: - path: Application/Src/Channel/app_mqtt.c @@ -39,25 +46,73 @@ virtualFolder: files: - path: Application/Src/IAP/app_iap_cmd.c - path: Application/Src/IAP/app_iap.c - - path: Application/Src/IAP/app_iap_cfg.c folders: [] - name: ldi files: - path: Application/Src/LDI/app_ldi.c - path: Application/Src/LDI/app_ldi_cmd.c + - path: Application/Src/LDI/app_ldi_device.c - path: Application/Src/LDI/app_vms_ctrl.c - path: Application/Src/LDI/app_ldi_cfg.c folders: [] + - name: ProtocolParser_QingHai + files: + - path: Application/Src/ProtocolParser_QingHai/app_qh_proto.c + - path: Application/Src/ProtocolParser_QingHai/app_qh_proto_parse.c + - path: Application/Src/ProtocolParser_QingHai/app_qh_proto_cmd.c + - path: Application/Src/ProtocolParser_QingHai/app_qh_proto_voice.c + folders: [] + - name: ProtocolParser_SiChuang_ETC + files: + - path: Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto.c + - path: Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_parse.c + - path: Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_cmd.c + folders: [] + - name: ProtocolParser_SiChuang_MTC + files: + - path: Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto.c + - path: Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_parse.c + - path: Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_cmd.c + - path: Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_voice.c + folders: [] + - name: ProtocolParser_SiChuang_Overload + files: + - path: Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto.c + - path: Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_parse.c + - path: Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_cmd.c + folders: [] + - name: ProtocolParser_ShanDong + files: + - path: Application/Src/ProtocolParser_ShanDong/app_sd_proto.c + - path: Application/Src/ProtocolParser_ShanDong/app_sd_proto_parse.c + - path: Application/Src/ProtocolParser_ShanDong/app_sd_proto_cmd.c + - path: Application/Src/ProtocolParser_ShanDong/app_sd_proto_default.c + folders: [] + - name: ProtocolParser_GuiZhou + files: + - path: Application/Src/ProtocolParser_GuiZhou/app_gz_proto.c + - path: Application/Src/ProtocolParser_GuiZhou/app_gz_proto_parse.c + - path: Application/Src/ProtocolParser_GuiZhou/app_gz_proto_cmd.c + - path: Application/Src/ProtocolParser_GuiZhou/app_gz_proto_voice.c + folders: [] + - name: ProtocolParser_YunNan + files: + - path: Application/Src/ProtocolParser_YunNan/app_yn_proto.c + - path: Application/Src/ProtocolParser_YunNan/app_yn_proto_parse.c + - path: Application/Src/ProtocolParser_YunNan/app_yn_proto_cmd.c + - path: Application/Src/ProtocolParser_YunNan/app_yn_proto_voice.c + folders: [] + - name: ProtocolParser_ChongQing + files: + - path: Application/Src/ProtocolParser_ChongQing/app_cq_proto.c + - path: Application/Src/ProtocolParser_ChongQing/app_cq_proto_parse.c + - path: Application/Src/ProtocolParser_ChongQing/app_cq_proto_cmd.c + folders: [] - name: rls files: - path: Application/Src/RLS/app_rls_cmd.c - path: Application/Src/RLS/app_rls.c folders: [] - - name: msl - files: - - path: Application/Src/MSL/app_msl.c - - path: Application/Src/MSL/app_msl_cmd.c - folders: [] - name: Drivers files: [] folders: @@ -224,6 +279,10 @@ virtualFolder: - path: Middlewares/Third_Party/SEGGER_RTT/SEGGER_RTT_printf.c - path: Middlewares/Third_Party/SEGGER_RTT/SEGGER_RTT_Syscalls_GCC.c folders: [] + - name: cJSON + files: + - path: Middlewares/Third_Party/cJSON/cJSON.c + folders: [] - name: Compiler files: - path: Compiler/startup.c @@ -235,6 +294,7 @@ virtualFolder: files: - path: Device/Comm/dev_rs232.c - path: Device/Comm/dev_rs485.c + - path: Device/Comm/dev_rs232_voice.c folders: [] - name: Network files: @@ -255,8 +315,9 @@ virtualFolder: - name: Display files: - path: Device/Display/dev_display.c - - path: Device/Display/dev_display_p20.c - - path: Device/Display/dev_display_old_p20.c + - path: Device/Display/dev_display_1_969.c + - path: Device/Display/dev_display_1_577.c + - path: Device/Display/dev_display_1_260.c folders: [] - name: Platform files: @@ -316,6 +377,7 @@ targets: defineList: - USE_HAL_DRIVER - STM32F407xx + - PROTO_CHONGQING incList: - Core/Inc - Drivers/CMSIS/Include @@ -335,18 +397,35 @@ targets: - Application/Inc/Channel - Application/Inc/IAP - Application/Inc/LDI + - Application/Inc/Config - Application/Inc/AH_MQTT - Application/Inc/RLS - - Application/Inc/MSL + - Application/Inc/ProtocolParser_QingHai + - Application/Inc/ProtocolParser_SiChuang_ETC + - Application/Inc/ProtocolParser_SiChuang_MTC + - Application/Inc/ProtocolParser_SiChuang_Overload + - Application/Inc/ProtocolParser_ShanDong + - Application/Inc/ProtocolParser_GuiZhou + - Application/Inc/ProtocolParser_YunNan + - Application/Inc/ProtocolParser_ChongQing + - Middlewares/Third_Party/cJSON libList: [] excludeList: - /Drivers/BSP/Key/key.c - /Drivers/BSP/RingBuff/Ringbuffer.c - /Drivers/BSP/Display/scan.c - - /Device/Display/dev_display_p20.c - - /Application/protocol/ah/ah_mqtt_cmd.c - - /Application/protocol/ah/ah_mqtt.c - - /Application/channel/app_mqtt.c + - /Device/Display/dev_display_1_260.c + - /Application/protocol/ah + - /Application/protocol/rls + - /Application/protocol/ProtocolParser_ShanDong + - /Application/protocol/ProtocolParser_QingHai + - /Application/protocol/ProtocolParser_GuiZhou + - /Device/Display/dev_display_1_969.c + - /Application/protocol/ProtocolParser_SiChuang_MTC + - /Application/protocol/ProtocolParser_SiChuang_ETC + - /Application/protocol/ProtocolParser_SiChuang_Overload + - /Application/protocol/ldi + - /Application/protocol/ProtocolParser_ChongQing settings: debugger: cortex-debug toolchain: GCC @@ -361,11 +440,11 @@ targets: asm-compiler: {} beforeBuildTasks: [] c/cpp-compiler: - language-c: c23 - language-cpp: c++23 + language-c: gnu23 + language-cpp: gnu++23 one-elf-section-per-data: true one-elf-section-per-function: true - optimization: level-0 + optimization: level-3 warnings: all-warnings global: $float-abi-type: hard @@ -376,6 +455,9 @@ targets: stack-usage: true use-newlib-nano: true linker: + $outputTaskExcludes: + - .s19 + - .bin $toolName: auto LIB_FLAGS: -lm output-format: elf @@ -420,8 +502,8 @@ targets: uploadConfigMap: Custom: bin: "" - commandLine: openocd -f ./Compiler/stm32f407zg.cfg -c "init; halt; program ./build/Debug/${workspaceFolderBasename}.hex verify reset exit" - eraseChipCommand: openocd -f ./Compiler/stm32f407zg.cfg -c "init; halt; stm32f4x unlock 0; stm32f4x mass_erase 0; shutdown" + commandLine: bash ./tool/flash_all.sh + eraseChipCommand: "" JLink: baseAddr: "" bin: "" @@ -430,12 +512,22 @@ targets: vendor: ST otherCmds: "" proType: 1 - speed: 8000 + speed: 32000 OpenOCD: baseAddr: "0x08000000" bin: "" interface: cmsis-dap target: stm32f4x + STLink: + address: "0x08000000" + bin: "" + elFile: None + optionBytes: .eide/debug.st.option.bytes.ini + otherCmds: "" + proType: SWD + resetMode: default + runAfterProgram: true + speed: 4000 probe-rs: allowEraseAll: false baseAddr: "0x08000000" @@ -444,7 +536,7 @@ targets: protocol: swd speed: 10000 target: STM32F407ZG - uploader: probe-rs + uploader: JLink Release: cppPreprocessAttrs: defineList: @@ -467,6 +559,8 @@ targets: - Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2 - Middlewares/Third_Party/FreeRTOS/Source/include - Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F + - Application/Inc/ProtocolParser_ChongQing + - Middlewares/Third_Party/cJSON - Compiler - Drivers/BSP/Display - Drivers/BSP/Key diff --git a/.eide/env.ini b/.eide/env.ini deleted file mode 100644 index c65fd65..0000000 --- a/.eide/env.ini +++ /dev/null @@ -1,17 +0,0 @@ -########################################################### -# Project Environment Variables -########################################################### - -## Global Variables - -# append command prefix for toolchain -#COMPILER_CMD_PREFIX= -# mcu ram size (used to print memory usage) -#MCU_RAM_SIZE=0x00 -# mcu rom size (used to print memory usage) -#MCU_ROM_SIZE=0x00 - - -[debug] -# put your variables for 'debug' target ... -#VAR= \ No newline at end of file diff --git a/Application/Inc/AH_MQTT/ah_mqtt.h b/Application/Inc/AH_MQTT/ah_mqtt.h index 23d0e73..02e8e82 100644 --- a/Application/Inc/AH_MQTT/ah_mqtt.h +++ b/Application/Inc/AH_MQTT/ah_mqtt.h @@ -11,24 +11,24 @@ #define MQTT_FRAME_MAX_LEN (21U + 512U) typedef struct [[gnu::packed]] topic_baseinfo { - char station_hex[8]; - char lane_hex[2]; - char device_type[2]; - char device_id[2]; + char station_hex[8] __attribute__((nonstring)); + char lane_hex[2] __attribute__((nonstring)); + char device_type[2] __attribute__((nonstring)); + char device_id[2] __attribute__((nonstring)); } topic_info_t; typedef struct [[gnu::packed]] notify_date { - char year[4]; - char month[2]; - char day[2]; - char hour[2]; + char year[4] __attribute__((nonstring)); + char month[2] __attribute__((nonstring)); + char day[2] __attribute__((nonstring)); + char hour[2] __attribute__((nonstring)); } notify_date_t; typedef struct [[gnu::packed]] notify_id { notify_date_t date_time; - char device_type[2]; - char device_id[2]; - char send_count[6]; + char device_type[2] __attribute__((nonstring)); + char device_id[2] __attribute__((nonstring)); + char send_count[6] __attribute__((nonstring)); } notify_id_t; // 设备状态 @@ -44,11 +44,11 @@ typedef struct [[gnu::packed]] sign_up { notify_id_t notify; char type; char work_sta; - char soft_ver[10]; - char hard_ver[10]; - char protocol_ver[10]; - char company[10]; - char device[10]; + char soft_ver[10] __attribute__((nonstring)); + char hard_ver[10] __attribute__((nonstring)); + char protocol_ver[10] __attribute__((nonstring)); + char company[10] __attribute__((nonstring)); + char device[10] __attribute__((nonstring)); char reserved[21]; } sign_up_t; diff --git a/Application/Inc/Channel/app_tcp_client.h b/Application/Inc/Channel/app_tcp_client.h index a21c3c3..8e3eb78 100644 --- a/Application/Inc/Channel/app_tcp_client.h +++ b/Application/Inc/Channel/app_tcp_client.h @@ -3,7 +3,7 @@ * @brief TCP 客户端通道(Device 层) * * 连接远程 TCP 服务器,接收数据通过 app_channel_dispatch 写入调度框架。 - * 默认: 192.168.114.100:9529,断线自动重连。 + * 默认: 192.168.2.17:9529,断线自动重连。 */ #pragma once @@ -16,7 +16,8 @@ /* 复用 app_tcp_server.h 中的 tcp_ch_ops */ #include "app_tcp_server.h" -/** @brief TCP Client 通道子类(单例,自带远端配置) */ +/** @brief TCP Client 通道子类(单例,自带远端配置;连接期实例为 + * 文件级 static 单实例,UAF 根治:重连复用同一实例,ch 指针恒定) */ typedef struct { channel_t me; void *conn; /**< 不透明句柄(中间件 netconn),在 .c 中 cast 回具体类型 */ diff --git a/Application/Inc/Channel/app_tcp_server.h b/Application/Inc/Channel/app_tcp_server.h index 8393c9c..07d6eb9 100644 --- a/Application/Inc/Channel/app_tcp_server.h +++ b/Application/Inc/Channel/app_tcp_server.h @@ -1,9 +1,10 @@ /** * @file app_tcp_server.h - * @brief TCP 服务器通道 — manage 任务 + conn 任务 + * @brief TCP 服务器通道 — 串行单客户端服务循环 * - * tcp_server_task: bind → listen → accept → 派生 conn 任务 → 等待断开 → 循环 - * tcp_server_conn_task: netconn_recv → app_channel_dispatch + * tcp_server_task: bind → listen → accept → 服务客户端(recv 循环) + * → 客户端断开 → 回 accept 下一客户端。通道实例为文件级 static 单实例 + * (UAF 根治),app_channel_register/dispatch 协议不变。 */ #pragma once @@ -25,11 +26,10 @@ extern osThreadId_t tcp_server_task_handle; extern const osThreadAttr_t tcp_server_task_attr; void tcp_server_task(void *argument); -void tcp_server_conn_task(void *argument); static inline osThreadId_t app_tcp_server_start(void) { return osThreadNew(tcp_server_task, NULL, &tcp_server_task_attr); } void app_tcp_server_set_port(uint16_t port); -uint16_t app_tcp_server_get_port(void); +uint16_t app_tcp_server_get_port(void); \ No newline at end of file diff --git a/Application/Inc/Channel/app_udp.h b/Application/Inc/Channel/app_udp.h index 633146e..cecc846 100644 --- a/Application/Inc/Channel/app_udp.h +++ b/Application/Inc/Channel/app_udp.h @@ -1,9 +1,11 @@ /** - * @file dev_udp.h - * @brief UDP 广播接收通道(Device 层) + * @file app_udp.h + * @brief UDP 广播接收通道 * - * 监听 UDP 广播,用于 IAP 固件升级协议。 - * 端口 10011 硬编码,不可修改(升级通道必须保持可连接)。 + * 监听端口固定 10011(宏 `LDI_DISCOVERY_PORT`,定义于 app_ldi.h),同口承载: + * - 创迪发现口(LDI 21H/12H) + * - IAP 固件升级(0x5A5A5A5A) + * 端口固定,禁止修改接口。 */ #pragma once @@ -13,7 +15,8 @@ #include "cmsis_os2.h" #include "app_dispatch.h" -/** @brief UDP 通道子类(每 bind 实例) */ +/** @brief UDP 通道子类(文件级 static 单实例,UAF 根治: + * 连接任务重入复用同一实例,ch 指针恒定;dispatch 侧回验兜底) */ typedef struct { channel_t me; void *conn; /**< 不透明句柄(中间件 netconn),在 .c 中 cast 回具体类型 */ @@ -35,6 +38,21 @@ static inline osThreadId_t app_udp_start(void) return osThreadNew(udp_task, NULL, &udp_task_attr); } -void app_udp_set_port(uint16_t port); uint16_t app_udp_get_port(void); void app_udp_broadcast(const uint8_t *data, uint16_t len); + +/* ---- CQ 业务口 UDP 通道(CH_ID_UDP_CQ;PROTO_CHONGQING 读 Sector1 net_cfg.port + 默认 20103,dev 共存构建固定 20103,见 app_udp.c _udp_cq_read_port)---- */ + +void udp_cq_task(void *argument); +extern const osThreadAttr_t udp_cq_task_attr; + +static inline osThreadId_t app_udp_cq_start(void) +{ + return osThreadNew(udp_cq_task, NULL, &udp_cq_task_attr); +} + +uint16_t app_udp_cq_get_port(void); + +/** @brief CQ 业务口广播(255.255.255.255:20103,SOF_BROADCAST)。 */ +void app_udp_cq_broadcast(const uint8_t *data, uint16_t len); diff --git a/Application/Inc/Config/app_board_net_cfg.h b/Application/Inc/Config/app_board_net_cfg.h new file mode 100644 index 0000000..e624c2d --- /dev/null +++ b/Application/Inc/Config/app_board_net_cfg.h @@ -0,0 +1,137 @@ +/** + * @file app_board_net_cfg.h + * @brief 板级系统配置存储(内部 Flash Sector 1, 0x08004000, 16KB) + * + * 本模块是 Sector1 配置记录的**唯一布局归属**,LDI 与 IAP 协议共同依赖: + * - 网络配置(ip/mask/gw 共享 + port/udp_port 双端口):LDI 0AH / IAP 4B02 写 + * port(TCP 口),CQ setip 写 udp_port(UDP 口);Bootloader / Recovery / 主固件 + * 共享同一套上电 IP; + * - 固件信息 + 升级状态(app_info / update_sta):IAP 升级流程专用,与 net_cfg + * 同扇区同记录,由本模块统一做读-改-写(16KB 整扇区擦除),避免双模块擦写 + * 所有权冲突。 + * + * 记录布局与 Bootloader / Recovery 各自的 Drivers/BSP/Config/config_info.h 二进制 + * 兼容,由下方 static_assert 锁定尺寸与偏移。写入守卫语义(空/损坏自愈、升级中间态 + * 仅放行 net_cfg 字段更新、同值跳过擦写)见 doc/07_LDI与IAP配置解耦。 + */ + +#pragma once + +#include +#include + +/* ---- 地址与常量定义(Sector1 配置区,仅本模块持有) ---- */ +#define ADDR_CONFIG_SECTOR 0x08004000 + +#define APP_BOARD_NET_CFG_MAGIC 0x0d000721 + +/* ---- Sector1 记录布局 ---- */ + +/* 网络配置 + * 字段语义(方案 B,2026-08-21,TCP/UDP 端口彻底分离): + * - port = TCP 业务口(LDI 0AH 写 / IAP 0x02 写 / 0x01 报 / TCP Server 监听, + * LDI/IAP 生态,出厂默认 9528); + * - udp_port = CQ UDP 业务口(CQ setip 写 / CQ UDP 通道读,出厂默认 20103)。 + * ip/mask/gw 两套口共享(setip 与 0AH 都写,现状不变)。 */ +__attribute__((aligned(4))) typedef struct { + uint8_t ip[4]; + uint8_t mask[4]; + uint8_t gw[4]; + uint32_t port; + uint32_t udp_port; +} app_board_net_cfg_t; + +/* main app 信息 */ +__attribute__((aligned(4))) typedef struct { + uint32_t size; /* Main App 字节长度 */ + uint32_t crc32; /* Main App CRC32 值 */ + char version[32]; +} app_board_fw_info_t; + +/* 升级状态机 */ +typedef enum { + APP_BOARD_UPDATED = 0, + APP_BOARD_UPDATING = 1, + APP_BOARD_FAILED = 2, +} app_board_update_sta_t; + +/* 存储在 0x08004000 */ +__attribute__((aligned(4))) typedef struct { + uint32_t magic; /* 魔数,判断配置区是否有效 */ + uint32_t update_sta; /* 升级状态机 */ + app_board_fw_info_t app_info; /* main_app 状态 */ + app_board_net_cfg_t net_cfg; /* 网络配置 */ + uint32_t config_crc; /* 本结构体自身的 CRC32 校验 */ +} app_board_sys_info_t; + +/* ---- 布局锁定:与 Bootloader / Recovery config_info.h 二进制兼容 ---- */ +static_assert(sizeof(app_board_net_cfg_t) == 20, "net_cfg 布局 20B"); +static_assert(sizeof(app_board_fw_info_t) == 40, "fw_info 布局 40B"); +static_assert(sizeof(app_board_sys_info_t) == 72, "sys_info 布局 72B"); +static_assert(offsetof(app_board_sys_info_t, magic) == 0, "magic 偏移 0"); +static_assert(offsetof(app_board_sys_info_t, update_sta) == 4, "update_sta 偏移 4"); +static_assert(offsetof(app_board_sys_info_t, app_info) == 8, "app_info 偏移 8"); +static_assert(offsetof(app_board_sys_info_t, net_cfg) == 48, "net_cfg 偏移 48"); +static_assert(offsetof(app_board_net_cfg_t, port) == 12, "port 偏移 12"); +static_assert(offsetof(app_board_net_cfg_t, udp_port) == 16, "udp_port 偏移 16"); +static_assert(offsetof(app_board_sys_info_t, config_crc) == 68, "config_crc 偏移 68"); + +/** + * @brief 读当前 Sector1 整记录(内存映射拷贝,无有效性判定) + * + * 供 IAP 命令读 app_info / update_sta 等非网络字段;网络配置读取请用 + * app_board_net_cfg_get。记录无效时读出内容为擦除态或损坏残留,调用方自行把关。 + */ +void app_board_net_cfg_read(app_board_sys_info_t *out); + +/** + * @brief 读网络配置(记录有效时) + * + * @return 0 成功且 out 已填充;-1 记录无效(magic 不匹配或 CRC 错) + */ +int32_t app_board_net_cfg_get(app_board_net_cfg_t *out); + +/** + * @brief 同步设备 IP/掩码/网关/TCP 口/UDP 口 到内部 Flash(LDI 0AH / IAP 4B02 / CQ setip 时调用) + * + * 字段语义(方案 B,2026-08-21):port = TCP 业务口(LDI/IAP 生态),udp_port = CQ UDP + * 业务口。调用方按协议口径填对应端口、另一端口保留现值(先 get,失败用出厂默认)—— + * LDI 0AH / IAP 4B02 写 port、保留 udp_port;CQ setip 写 udp_port、保留 port。 + * + * 分支语义(见 doc/07_LDI与IAP配置解耦 02 §9/§10): + * - 空扇区:完整初始化,镜像老 Bootloader Init_Config_Info 首次上电语义 + * (magic + update_sta=updated + app_info 全 0 且 crc32=0xFFFFFFFF + net_cfg + CRC32); + * - 损坏记录(magic 无效或 CRC 错):按空扇区处理,完整初始化后写新配置(自愈); + * - 有效记录且 update_sta != updated(升级中间态):仅更新 net_cfg,update_sta 与 + * app_info 原样保留(Bootloader 条件 D 判定不受影响)(2026-08-18 修订); + * - 有效记录且 update_sta == updated:仅更新 net_cfg(ip/mask/gw/port/udp_port),其余 + * 字段原样保留;与现扇区 net_cfg 完全一致(含 port 与 udp_port)时跳过擦写。 + * + * @return 0 成功(含同值跳过);<0 为擦/写错误码 + */ +int32_t app_board_net_cfg_update(const uint8_t ip[4], const uint8_t mask[4], const uint8_t gw[4], + uint32_t port, uint32_t udp_port); + +/** + * @brief 同步固件版本号到内部 Flash(app_info.version[32],IAP 0x03「上报固件状态」用) + * + * 只允许改 app_info.version 并重算 config_crc,**绝不动** app_info.size / app_info.crc32 / + * net_cfg / update_sta 的既有值(size/crc32 由 Recovery 升级流程写入,改动会触发 Bootloader + * 条件 D 判 App 损坏)。 + * + * 分支语义(与 app_board_net_cfg_update 同构,见 doc/07_LDI与IAP配置解耦 02 §13): + * - 空扇区:完整初始化(magic + update_sta=updated + app_info 全 0 且 crc32=0xFFFFFFFF + + * net_cfg 全 0)+ 填 version。调用点安排在 sw_board_init()(含 ldi_ctx_init 从 W25Qxx + * 回写 net_cfg)之后,正常路径不会走到此分支;若走到(如 ldi_ctx_init 未回写), + * net_cfg 会被写成 0.0.0.0/port0,由后续 0AH / 上电同步恢复; + * - 损坏记录(magic 无效或 CRC 错):按空扇区处理(自愈),完整初始化后填 version; + * - 有效记录且 update_sta != updated(升级中间态,Bootloader 条件 A/B 写入):拒绝覆盖, + * 返回 -1; + * - 有效记录且 update_sta == updated:仅更新 app_info.version,其余字段原样保留;与现扇区 + * version 一致时跳过擦写(Q3:不做磨损均衡)。 + * + * @param version 版本字符串(如 PROGRAM_CODE),须满足 strlen(version) < 32 + * @return 0 成功(含同值跳过);<0 失败(-1 = 升级中间态拒绝覆盖,-2 = version 过长, + * 其余为擦/写错误码) + */ +int32_t app_board_net_cfg_fw_version_update(const char *version); \ No newline at end of file diff --git a/Application/Inc/IAP/app_iap_cfg.h b/Application/Inc/IAP/app_iap_cfg.h deleted file mode 100644 index 23c2cd8..0000000 --- a/Application/Inc/IAP/app_iap_cfg.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -#include -#include -#include "dev_storage.h" - -// 地址与常量定义 -#define ADDR_CONFIG_SECTOR 0x08004000 -#define ADDR_RECOVERY_APP 0x08008000 -#define ADDR_MAIN_APP 0x08040000 - -#define APP_FLASH_IAP_MAGIC 0x0d000721 - -// 网络配置 -__attribute__((aligned(4))) typedef struct { - uint8_t ip[4]; - uint8_t mask[4]; - uint8_t gw[4]; - uint32_t port; -} app_flash_iap_net_cfg_t; - -// main app 信息 -__attribute__((aligned(4))) typedef struct { - uint32_t size; // Main App 字节长度 - uint32_t crc32; // Main App CRC32 值 - char version[32]; -} app_flash_iap_fw_info_t; - -// 升级状态机 -typedef enum { - APP_FLASH_IAP_UPDATED = 0, - APP_FLASH_IAP_UPDATING = 1, - APP_FLASH_IAP_FAILED = 2, -} app_flash_iap_update_sta_t; - -// 存储在 0x08004000 -__attribute__((aligned(4))) typedef struct { - uint32_t magic; // 魔数,判断配置区是否有效 - uint32_t update_sta; // 升级状态机 - app_flash_iap_fw_info_t app_info; // main_app 状态 - app_flash_iap_net_cfg_t net_cfg; // 网络配置 - uint32_t config_crc; // 本结构体自身的 CRC32 校验 -} app_flash_iap_sys_info_t; - -bool app_flash_iap_is_config_empty(volatile const app_flash_iap_sys_info_t *info); -bool app_flash_iap_is_config_valid(volatile const app_flash_iap_sys_info_t *info); -void app_flash_iap_init_config(app_flash_iap_sys_info_t *info); -void app_flash_iap_edit_config(app_flash_iap_sys_info_t *info); -int32_t app_flash_iap_erase_config(void); -int32_t app_flash_iap_write_config(app_flash_iap_sys_info_t *info); - -/** @brief 同步设备IP/掩码/网关到内部Flash (LDI改IP时调用) */ -void app_flash_iap_update_net_cfg(const uint8_t ip[4], const uint8_t mask[4], const uint8_t gw[4]); - -/** @brief 获取 IAP Flash 存储句柄(内部使用) */ -dev_storage_t *app_flash_iap_get_storage(void); - -extern app_flash_iap_sys_info_t *g_config; /* 内存映射指针 (0x08004000) */ diff --git a/Application/Inc/IAP/app_iap_cmd.h b/Application/Inc/IAP/app_iap_cmd.h index 62e222d..c605459 100644 --- a/Application/Inc/IAP/app_iap_cmd.h +++ b/Application/Inc/IAP/app_iap_cmd.h @@ -5,7 +5,6 @@ #include "app_iap.h" #include "app_dispatch.h" #include "app_udp.h" -#include "app_iap_cfg.h" #define FLAG_FORCE_UPDATE (uint32_t)(0x0000DEADU) #define FIRMWARE_MAX_FRAME_NUM (768U) @@ -25,4 +24,5 @@ typedef enum { } rtn_cmd_t; typedef void (*iap_cmd_handler_fn_t)(channel_t *, iap_frame_t *); -extern const iap_cmd_handler_fn_t g_iap_cmd_table[]; +#define IAP_CMD_COUNT (8U) /* 命令表条目数(cmd 0x00~0x07),与 app_iap_cmd.c 中 g_iap_cmd_table[] 定义保持同步 */ +extern const iap_cmd_handler_fn_t g_iap_cmd_table[IAP_CMD_COUNT]; diff --git a/Application/Inc/LDI/app_ldi.h b/Application/Inc/LDI/app_ldi.h index 3c79d67..6e21a77 100644 --- a/Application/Inc/LDI/app_ldi.h +++ b/Application/Inc/LDI/app_ldi.h @@ -5,10 +5,16 @@ #include "app_ldi_cfg.h" #include "ring_buffer.h" -#define DEVICE_NUM (2U) // 本设备使用到的功能模块数量 +#define DEVICE_NUM (4U) // 本设备使用到的功能模块数量 #define LDI_TX_BUF_SIZE (512U) // 响应帧拼装缓冲区上限 +/* 创迪发现口固定值(设计原则:禁止修改、不提供修改接口,0AH/4B02 均不携带此端口) */ +#define LDI_DISCOVERY_PORT (10011U) + +/* 设备配置功能端口出厂默认值(TCP 业务端口,协议文档 doc/02 附件2:9528) */ +#define LDI_DEFAULT_CONFIG_PORT (9528U) + /** * 外设功能模块类型编码 * 依据《车道设备接口规范》第2.1节定义 diff --git a/Application/Inc/LDI/app_ldi_cfg.h b/Application/Inc/LDI/app_ldi_cfg.h index 08e96b5..bf28dc1 100644 --- a/Application/Inc/LDI/app_ldi_cfg.h +++ b/Application/Inc/LDI/app_ldi_cfg.h @@ -6,7 +6,7 @@ // Flash Sector 11 (0x080E0000~0x080FFFFF, 128KB) 已释放。 // LDI 配置现已迁移至 W25Qxx 最后一个 4KB 扇区,通过 dev_storage_ops 访问。 #define APP_FLASH_LDI_MAGIC 0x0D001B00 -#define APP_FLASH_LDI_MAX_MODULES (6U) // Flash 最大可存储的功能模块数量 +#define APP_FLASH_LDI_MAX_MODULES (11U) // Flash 最大可存储的功能模块数量 /** * 单功能模块配置信息(从 0BH 命令下发,共 12 字节) diff --git a/Application/Inc/LDI/app_ldi_device.h b/Application/Inc/LDI/app_ldi_device.h new file mode 100644 index 0000000..0e62434 --- /dev/null +++ b/Application/Inc/LDI/app_ldi_device.h @@ -0,0 +1,70 @@ +/** + * @file app_ldi_device.h + * @brief LDI 设备能力层 — 协议解析与硬件执行解耦 + * + * 协议层 (app_ldi_cmd) 只负责校验/分发/回包; + * E6/E7/E8 的实际执行与状态缓存统一由此层提供。 + * + * 硬件/协议兼容约定: + * E7 控制 Color: 01H绿 / 02H红 / 03H黄 + * E7 0CH 上报(附录 A.1): 00H红灯 / 01H绿灯(无黄) + * 黄灯硬件降级关灯,上报按红灯 00H + * E8 报警器:暂由 Flash_Light 代实现;WorkMode 频率用软闪烁 + */ + +#pragma once + +#include +#include + +#include "app_ldi_cmd.h" + +/** 控制执行结果:00H=成功, 01H=失败(与 B1H status 一致) */ +typedef enum { + LDI_DEV_OK = 0x00, + LDI_DEV_FAIL = 0x01, +} ldi_dev_result_t; + +/** E6 显示运行状态缓存(附录 A.1: 00H关闭 / 01H开启) */ +typedef struct { + uint8_t running_status; + uint8_t font_color; + uint8_t keep_time; +} ldi_dev_display_sta_t; + +/** E7 信号灯状态 + * color: 最近一次控制色码 01绿/02红/03黄 + * running_status: 附录 A.1 上报码 00红 / 01绿 + */ +typedef struct { + uint8_t running_status; + uint8_t color; +} ldi_dev_signal_sta_t; + +/** E8 报警器状态(附录 A.1: 00H停止 / 01H报警中) */ +typedef struct { + uint8_t running_status; + uint8_t work_mode; + uint8_t keep_time; +} ldi_dev_alarm_sta_t; + +void ldi_device_init(void); + +/** E6 显示控制/清屏;text_len 为 text[] 有效字节数(不含定长头) */ +ldi_dev_result_t ldi_device_display_ctrl(const ldi_ctrl_display_t *ctrl, uint16_t text_len); + +/** E7 通行信号灯颜色控制 */ +ldi_dev_result_t ldi_device_lane_signal_ctrl(const ldi_ctrl_signal_t *ctrl); + +/** E8 报警器开关控制(含 work_mode 频率闪烁 / keep_time 定时关闭) */ +ldi_dev_result_t ldi_device_alarm_ctrl(const ldi_ctrl_alarm_t *ctrl); + +/** 由 ldi_timer_task 每秒调用:E6 KeepTime / E8 KeepTime+频率闪烁 */ +void ldi_device_timer_poll(void); + +/** 填充 0CH 上报用的单模块状态(仅写 running_status;附录 A.1 编码) */ +bool ldi_device_fill_sta(uint8_t device_type, ldi_device_info_t *info); + +const ldi_dev_display_sta_t *ldi_device_display_sta(void); +const ldi_dev_signal_sta_t *ldi_device_lane_signal_sta(void); +const ldi_dev_alarm_sta_t *ldi_device_alarm_sta(void); diff --git a/Application/Inc/MSL/app_msl.h b/Application/Inc/MSL/app_msl.h deleted file mode 100644 index 9bf851f..0000000 --- a/Application/Inc/MSL/app_msl.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include "string.h" - -#include "cmsis_os2.h" -#include "app_dispatch.h" - -typedef struct { - uint8_t head[2]; - uint8_t addr; - uint8_t length[2]; - uint8_t cmd; - uint8_t data_bcc[]; -} msl_frame_t; - -typedef enum { - MSL_CMD_TEST = 0x00, - MSL_CMD_TEXT = 0x01, - MSL_CMD_BITMAP = 0x02, - MSL_CMD_FILL = 0x03, - MSL_CMD_LIGHTLEVEL = 0x03, -} msl_cmd_type_t; - -extern uint8_t msl_addr; -extern osMessageQueueId_t g_msl_msg_queue; -extern osThreadId_t g_msl_task_handle; -extern const osThreadAttr_t msl_task_attr; diff --git a/Application/Inc/MSL/app_msl_cmd.h b/Application/Inc/MSL/app_msl_cmd.h deleted file mode 100644 index c0aa4ff..0000000 --- a/Application/Inc/MSL/app_msl_cmd.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include "string.h" - -#include "app_msl.h" -#include "app_dispatch.h" - -typedef struct { - uint8_t width; - uint8_t high; - uint8_t color; - uint8_t pers; - uint8_t bitmap[]; -} msl_bitmap_t; - -typedef struct { - uint8_t x[2]; - uint8_t y[2]; - uint8_t color; - uint8_t font_size; - uint8_t font_type; - uint8_t row_style; - uint8_t col_style; - uint8_t wrap; - uint8_t encode; - uint8_t pers; - uint8_t text[]; -} msl_text_t; - -/** - * MSL 命令处理函数指针类型 - * @param meta 通道元信息(来源通道类型、编号等) - * @param data 指向帧 DATA 域首字节 - */ -typedef void (*msl_cmd_handler_fn_t)(channel_t *, void *); - -/** MSL 命令处理函数表,按命令码索引 */ -extern const msl_cmd_handler_fn_t g_msl_cmd_table[]; diff --git a/Application/Inc/ProtocolParser_ChongQing/app_cq_proto.h b/Application/Inc/ProtocolParser_ChongQing/app_cq_proto.h new file mode 100644 index 0000000..09a9acc --- /dev/null +++ b/Application/Inc/ProtocolParser_ChongQing/app_cq_proto.h @@ -0,0 +1,110 @@ +/** + * @file app_cq_proto.h + * @brief 重庆高速二代费显协议(CQ)公共定义 + * + * 传输:UDP 业务口(PROTO_CHONGQING 下 Sector1 net_cfg.port 可配,默认 20103; + * dev 共存构建固定 20103,见 app_udp.c _udp_cq_read_port)+ 搜索口 10011 固定 + * (与创迪发现口 / IAP 共用 CH_ID_UDP)。 + * 帧形态:JSON('{' 花括号定界,文本字段为 GB2312 字节)+ 固定 12B 二进制 + * (重启请求 / 搜索请求,CRC16-XMODEM 校验)。协议版本 2.0.1。 + * JSON 命令无应答帧。 + */ + +#pragma once + +#include +#include + +#include "app_dispatch.h" + +/* 心跳超时故障屏开关(1 开 / 0 关): + * - 默认跟随构建口径:PROTO_CHONGQING(重庆量产)开启;共存/其他构建关闭 + * (通用版固件多协议共存时,他省上位机不发重庆心跳,不能误显示故障内容); + * - 构建入口可显式覆盖:-DCQ_FAULT_SCREEN=1 强制开 / =0 强制关(Makefile/EIDE + * defineList)。 + * - 旧名已废弃(2026-08-24 改名,不保留兼容别名): + * CQ_FAULT_SCREEN_ENABLED → CQ_FAULT_SCREEN; + * -DCQ_FAULT_SCREEN_FORCE_ON=1/0 覆盖 → -DCQ_FAULT_SCREEN=1/0。 */ +#ifndef CQ_FAULT_SCREEN +#ifdef PROTO_CHONGQING +#define CQ_FAULT_SCREEN 1 +#else +#define CQ_FAULT_SCREEN 0 +#endif +#endif + +/* ---- 帧 / 队列容量 ---- */ +#define CQ_PAYLOAD_MAX (1044U) /* 与 FRAME_DATA_MAX_LEN 对齐;JSON 帧扫描上限 \ + */ +#define CQ_MSG_SIZE (sizeof(frame_msg_t) + CQ_PAYLOAD_MAX) +#define CQ_QUEUE_DEPTH (3U) + +/* ---- 网络默认值(出厂默认 192.168.1.5/255.255.255.0/192.168.1.1:20103)---- + */ +#define CQ_DEFAULT_PORT (20103U) + +/* ---- 心跳故障阈值(秒)---- */ +#define CQ_SYNC_FAULT_S (120U) + +/* ---- 命令类型 ---- */ +typedef enum { + CQ_PCMD_TEXT1 = 0, /* text1 多行文本 */ + CQ_PCMD_TRA1, /* tra1 红叉 / 绿箭 */ + CQ_PCMD_PIC1, /* pic1 图片(stub) */ + CQ_PCMD_LIGHT1, /* light1 亮度 0~15 */ + CQ_PCMD_VOICE_CONTROL1, /* voice_control1 音量 0~15 */ + CQ_PCMD_VOICE1, /* voice1 语音播报文本 */ + CQ_PCMD_VOICE_PLAY1, /* voice_play1 语音库播放(stub) */ + CQ_PCMD_WARN1, /* warn1 黄闪倒计时 */ + CQ_PCMD_SYN1, /* syn1 心跳 */ + CQ_PCMD_SETIP, /* setip 改 IP 落盘重启 */ + CQ_PCMD_SCREEN, /* screen 单元板选择(仅记录) */ + CQ_PCMD_FULL, /* full 全屏单色 */ + CQ_PCMD_BIN_REBOOT, /* 12B 二进制:重启请求 */ + CQ_PCMD_BIN_SEARCH, /* 12B 二进制:IP 搜索请求 */ + CQ_PCMD_INVALID, +} cq_pcmd_t; + +/* ---- 解析结果状态 ---- */ +typedef enum { + CQ_PARSE_OK = 0, + CQ_PARSE_ERR_FRAME, /* 帧结构非法(非 '{' / 非 12B 二进制 / 截断) */ + CQ_PARSE_ERR_CMD, /* cmd 字段不在命令集 */ + CQ_PARSE_ERR_PARAM, /* 参数非法 */ +} cq_parse_sta_t; + +/* ---- 文本行参数(text1 ln0~ln7)---- */ +typedef struct { + bool valid; + uint8_t font; /* 16 / 24 / 32(默认 24) */ + uint8_t color; /* 0 红 / 1 绿 / 2 黄(默认绿) */ + const char *text; + uint16_t text_len; +} cq_line_t; + +/* ---- setip 参数 ---- */ +typedef struct { + uint8_t ip[4]; + uint8_t mask[4]; + uint8_t gw[4]; + uint32_t port; +} cq_setip_t; + +/* ---- 12B 二进制帧(帧内 CRC 为 CRC16-XMODEM 大端;定义于 app_cq_proto.c)---- + */ +extern const uint8_t cq_bin_reboot_req[12]; +extern const uint8_t cq_bin_search_req[12]; + +/* ---- 跨文件状态访问 ---- */ + +/** @brief 心跳计数器清零(宽松语义 B.7.2:帧结构合法的 JSON——合法命令 / + * 命令未知 / 参数非法——到达即调用;CQ_PARSE_ERR_FRAME 与 BIN 帧不复位)。 */ +void cq_proto_sync_reset(void); + +/** @brief 设置黄闪状态:0 关闭并清倒计时;>0 开启并倒计时(每秒递减,归零关); + * -1 常开(映射 0xFFFF 语义,不自动关)。 */ +void cq_proto_warn_set(int32_t seconds); + +/* ---- 任务入口(cq_proto_init 创建)---- */ +void cq_proto_handle_task(void *argument); +void cq_proto_timer_task(void *argument); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_ChongQing/app_cq_proto_cmd.h b/Application/Inc/ProtocolParser_ChongQing/app_cq_proto_cmd.h new file mode 100644 index 0000000..fb52287 --- /dev/null +++ b/Application/Inc/ProtocolParser_ChongQing/app_cq_proto_cmd.h @@ -0,0 +1,30 @@ +/** + * @file app_cq_proto_cmd.h + * @brief 重庆高速二代费显协议(CQ)命令执行接口 + */ + +#pragma once + +#include "app_dispatch.h" +#include "app_cq_proto.h" +#include "app_cq_proto_parse.h" + +/** + * @brief 执行解析后的 CQ 命令(渲染 / IO / 语音 / 网络 / 位图)。 + * @param ch 源通道(二进制重启应答回源通道;JSON 命令无应答)。 + * @param cmd 解析结果(sta == CQ_PARSE_OK 才执行)。 + */ +void cq_execute_cmd(channel_t *ch, const cq_parsed_cmd_t *cmd); + +/** + * @brief 渲染心跳超时故障屏: + * 「车道关闭」32pt 红(行0)+「请择道行驶!」(行2)+ 右下角红叉位图。 + * @note 布局对齐协议文档;行超出屏高自动跳过(小屏防御)。 + */ +void cq_render_fault_screen(void); + +/** + * @brief 读取 screen 命令最近记录的 led 值(-1 = 未收到过)。 + * @note screen 命令仅记录不生效(本次不含模组排布)。 + */ +int cq_screen_led_get(void); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_ChongQing/app_cq_proto_parse.h b/Application/Inc/ProtocolParser_ChongQing/app_cq_proto_parse.h new file mode 100644 index 0000000..40b4593 --- /dev/null +++ b/Application/Inc/ProtocolParser_ChongQing/app_cq_proto_parse.h @@ -0,0 +1,60 @@ +/** + * @file app_cq_proto_parse.h + * @brief 重庆高速二代费显协议(CQ)帧解析接口 + */ + +#pragma once + +#include "app_cq_proto.h" + +/** + * @brief 解析后的 CQ 命令。 + * @note text1/voice1 的文本指针指向 parse 层内部静态缓冲(单消费者任务, + * 下次解析前有效);调用方须在 parse 返回后立即执行,不得跨帧保存。 + */ +typedef struct { + cq_parse_sta_t sta; + cq_pcmd_t cmd; + union { + struct { /* text1 */ + bool scr_clear; /* scr==0 → 先全屏清黑 */ + cq_line_t ln[8]; /* ln0~ln7 */ + } text1; + struct { /* tra1 */ + uint8_t turn; /* 0=红叉(r) / 1=绿箭(g) */ + } tra1; + struct { /* light1 */ + int level; /* 0~15 */ + } light1; + struct { /* voice_control1 */ + int level; /* 0~15 */ + } voice_ctrl1; + struct { /* voice1 */ + const char *text; /* GB2312 字节,保留 '|' 组合符 */ + uint16_t text_len; + } voice1; + struct { /* warn1 */ + int second; /* 0 关 / >0 倒计时 / -1 常开 */ + } warn1; + struct { /* screen */ + int led; + } screen; + struct { /* full */ + int color; /* 0红/1绿/2黄/3白 */ + } full; + cq_setip_t setip; /* setip */ + } p; +} cq_parsed_cmd_t; + +/** + * @brief 解析 CQ 原始帧(纯解析:不渲染、不碰外设、不发包)。 + * + * JSON 帧:cJSON_Parse 建树(内存钩子已在 cq_proto_init 换绑 RTOS 堆), + * 按 "cmd" 字段提取各命令参数,文本字段拷贝至内部静态缓冲后删树。 + * 二进制帧:12 字节精确匹配重启 / 搜索两帧。 + * + * @param raw 原始帧数据。 + * @param raw_len 帧长度。 + * @return 解析结果;sta 非 CQ_PARSE_OK 时调用方静默丢弃。 + */ +cq_parsed_cmd_t cq_parse_frame(const uint8_t *raw, uint16_t raw_len); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_GuiZhou/app_gz_proto.h b/Application/Inc/ProtocolParser_GuiZhou/app_gz_proto.h new file mode 100644 index 0000000..e527b84 --- /dev/null +++ b/Application/Inc/ProtocolParser_GuiZhou/app_gz_proto.h @@ -0,0 +1,145 @@ +/** + * @file app_gz_proto.h + * @brief 贵州常规费显协议公共定义 + * + * 帧格式:'{'(0x7B) + 命令字(1B) + 参数长度(1B 二进制) + 参数(变长) + '}'(0x7D), + * 无校验。命令集:'1' 主机查询、'2' 自检、'3' 点阵单行任意显示、'4' 点阵全屏可编辑、 + * '5' 全屏清除、'6' 固定格式显示、'7' 礼貌用语语音播报、'8' 显示亮度设定、 + * '9' 语音音量设定、'A' 外设控制、'B' 收费金额语音播放; + * 另有 0x01 全屏点亮 / 0x02 获取版本号两个二进制命令字。 + * 协议原文:协议文档 06 贵州常规费显协议(2020-06-17 修订)。 + * + * 帧头与青海协议完全同构('{'+cmd+len+'}'),命令字 '1'~'9','A','B' 全部落入青海 + * probe 命令集:全协议构建下青海 probe 先注册(字母序 qh < gz)先认领, + * 量产由 EIDE 目录排除纪律保证贵州与青海互斥(doc/05-01 §6 兼容矩阵)。 + * 0x01/0x02 为二进制命令字,不属于青海/山东/四川MTC 的 ASCII 命令集,probe 首命令字 + * 快拒,无冲突。 + */ +#pragma once + +#include +#include "app_dispatch.h" + +/** + * @brief 贵州协议支持的命令类型。 + */ +typedef enum { + GZ_PCMD_HOST_QUERY = 0, /**< '1' 主机查询 */ + GZ_PCMD_SELF_CHECK, /**< '2' 自检 */ + GZ_PCMD_ONE_LINE, /**< '3' 点阵单行任意显示 */ + GZ_PCMD_FULL_SCREEN, /**< '4' 点阵全屏可编辑显示 */ + GZ_PCMD_CLEAR, /**< '5' 全屏清除显示 */ + GZ_PCMD_FIXED_FORMAT, /**< '6' 固定格式显示(含费额语音) */ + GZ_PCMD_CIVIL_VOICE, /**< '7' 礼貌用语语音播报 */ + GZ_PCMD_BRIGHTNESS, /**< '8' 显示亮度设定 */ + GZ_PCMD_VOLUME, /**< '9' 语音音量设定 */ + GZ_PCMD_PERIPHERAL, /**< 'A' 外设控制 */ + GZ_PCMD_FEE_VOICE, /**< 'B' 收费金额语音播放 */ + GZ_PCMD_FILL_ALL, /**< 0x01 全屏点亮(红/绿/黄) */ + GZ_PCMD_VERSION, /**< 0x02 获取版本号 */ + GZ_PCMD_INVALID, +} gz_pcmd_t; + +/** + * @brief 贵州协议帧解析状态。 + */ +typedef enum { + GZ_PARSE_OK = 0, + GZ_PARSE_ERR_FRAME, /**< 帧头/帧尾/长度字段错误 */ + GZ_PARSE_ERR_CMD, /**< 命令字不在命令集 */ + GZ_PARSE_ERR_PARAM, /**< 参数非法 */ +} gz_parse_sta_t; + +/** + * @brief 单行显示参数('3' 命令)。 + */ +typedef struct { + uint8_t color; /**< 0 红 / 1 绿 / 2 黄 */ + uint8_t row; /**< 0~4,对应协议行号 1~5 */ + const uint8_t *text; /**< 显示数据(GBK/ASCII 混合) */ + uint16_t text_len; /**< 数据字节数 */ +} gz_one_line_t; + +/** + * @brief 全屏可编辑显示参数('4' 命令)。 + */ +typedef struct { + uint8_t color; /**< 0 红 / 1 绿 / 2 黄 */ + uint8_t x; /**< 显示区域 X 坐标,最左端为 0 */ + uint8_t y; /**< 显示区域 Y 坐标,最上端为 0 */ + const uint8_t *text; /**< 显示数据(GBK/ASCII,支持 0x0A/0x0D 回车) */ + uint16_t text_len; /**< 数据字节数 */ +} gz_full_screen_t; + +/** + * @brief 固定格式显示参数('6' 命令)。 + * @note 字段拆分('|' 分隔)在 cmd 层完成,此处仅保留原始数据指针。 + */ +typedef struct { + uint8_t color; /**< 0 红 / 1 绿 / 2 黄 */ + uint8_t type; /**< 0 客车 / 1 货车 */ + const uint8_t *raw; /**< x0/x1 之后 '|' 分隔 5 字段的原始数据 */ + uint16_t raw_len; /**< 原始数据字节数 */ +} gz_fixed_t; + +/** + * @brief 亮度设置参数('8' 命令,0=自动调光,1~5 档)。 + */ +typedef struct { uint8_t level; } gz_brightness_t; + +/** + * @brief 音量设置参数('9' 命令,1~5 档)。 + */ +typedef struct { uint8_t level; } gz_volume_t; + +/** + * @brief 外设控制参数('A' 命令,bit0 绿灯 / bit1 红灯 / bit2 黄闪报警)。 + */ +typedef struct { uint8_t ctrl; } gz_peripheral_t; + +/** + * @brief 费额语音参数('B' 命令,金额单位分)。 + */ +typedef struct { uint32_t amount_fen; } gz_fee_t; + +/** + * @brief 礼貌用语播报参数('7' 命令)。 + */ +typedef struct { uint8_t idx; } gz_civil_t; + +/** + * @brief 全屏点亮参数(0x01 命令,归一化 0 红 / 1 绿 / 2 黄)。 + */ +typedef struct { uint8_t color; } gz_fill_all_t; + +/** + * @brief 贵州协议解析结果。 + */ +typedef struct { + gz_pcmd_t cmd; + gz_parse_sta_t sta; + uint16_t data_len; + const uint8_t *data; + union { + gz_one_line_t one_line; + gz_full_screen_t full_screen; + gz_fixed_t fixed; + gz_brightness_t brightness; + gz_volume_t volume; + gz_peripheral_t peripheral; + gz_fee_t fee; + gz_civil_t civil; + gz_fill_all_t fill_all; + } p; +} gz_parsed_cmd_t; + +/** + * @brief 贵州协议处理任务入口。 + * @param argument 任务参数,当前未使用。 + */ +void gz_proto_handle_task(void *argument); + +/** + * @brief 贵州协议初始化入口。 + */ +void gz_proto_init(void); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_GuiZhou/app_gz_proto_cmd.h b/Application/Inc/ProtocolParser_GuiZhou/app_gz_proto_cmd.h new file mode 100644 index 0000000..5e6d735 --- /dev/null +++ b/Application/Inc/ProtocolParser_GuiZhou/app_gz_proto_cmd.h @@ -0,0 +1,14 @@ +/** + * @file app_gz_proto_cmd.h + * @brief 贵州协议命令执行接口 + */ +#pragma once + +#include "app_gz_proto.h" + +/** + * @brief 执行贵州协议命令。 + * @param ch 当前通道。 + * @param cmd 解析后的命令结构体。 + */ +void gz_execute_cmd(channel_t *ch, const gz_parsed_cmd_t *cmd); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_GuiZhou/app_gz_proto_parse.h b/Application/Inc/ProtocolParser_GuiZhou/app_gz_proto_parse.h new file mode 100644 index 0000000..9014905 --- /dev/null +++ b/Application/Inc/ProtocolParser_GuiZhou/app_gz_proto_parse.h @@ -0,0 +1,25 @@ +/** + * @file app_gz_proto_parse.h + * @brief 贵州协议帧解析接口 + */ +#pragma once + +#include "app_gz_proto.h" + +/** + * @brief 解析贵州协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体;状态字段 sta 标识解析结果。 + */ +gz_parsed_cmd_t gz_parse_frame(const uint8_t *raw, uint16_t raw_len); + +/** + * @brief 金额 ASCII 数字串转分(整数运算,无浮点)。 + * @param str 金额数字串(整数或带小数,如 "1.20")。 + * @param len 数字串字节数。 + * @return 金额(分)= 整数部分×100 + 小数前两位(%.2f 语义,第三位起截断)。 + * @note '6' 固定格式的金额/余额/总重/超重字段复用本函数转分/吨分, + * 车型字段取返回值 ÷100 得整数车型号。 + */ +uint32_t gz_amount_to_fen(const uint8_t *str, uint16_t len); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_GuiZhou/app_gz_proto_voice.h b/Application/Inc/ProtocolParser_GuiZhou/app_gz_proto_voice.h new file mode 100644 index 0000000..e4add70 --- /dev/null +++ b/Application/Inc/ProtocolParser_GuiZhou/app_gz_proto_voice.h @@ -0,0 +1,19 @@ +/** + * @file app_gz_proto_voice.h + * @brief 贵州协议语音播报接口 + */ +#pragma once + +#include + +/** + * @brief 播报文明用语。 + * @param idx 文明用语索引(0~3)。 + */ +void gz_voice_civil(uint8_t idx); + +/** + * @brief 播报费额提示语。 + * @param amount_fen 费额,单位分。 + */ +void gz_voice_fee_amount(uint32_t amount_fen); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_QingHai/app_qh_proto.h b/Application/Inc/ProtocolParser_QingHai/app_qh_proto.h new file mode 100644 index 0000000..958272a --- /dev/null +++ b/Application/Inc/ProtocolParser_QingHai/app_qh_proto.h @@ -0,0 +1,139 @@ +/** + * @file app_qh_proto.h + * @brief 青海高速费显协议公共定义 + * + * 该头文件提供青海协议的命令枚举、解析结果、参数结构体和入口接口。 + */ +#pragma once + +#include +#include "app_dispatch.h" + +/** + * @brief 青海协议支持的命令类型。 + */ +typedef enum { + QH_PCMD_HOST_QUERY = 0, + QH_PCMD_SELF_CHECK, + QH_PCMD_ONE_LINE, + QH_PCMD_FULL_SCREEN, + QH_PCMD_CLEAR, + QH_PCMD_FIXED_DISPLAY, + QH_PCMD_CIVIL_VOICE, + QH_PCMD_BRIGHTNESS, + QH_PCMD_VOLUME, + QH_PCMD_PERIPHERAL, + QH_PCMD_VOICE, + QH_PCMD_INVALID, +} qh_pcmd_t; + +/** + * @brief 青海协议帧解析状态。 + */ +typedef enum { + QH_PARSE_OK = 0, + QH_PARSE_ERR_FRAME, + QH_PARSE_ERR_CMD, + QH_PARSE_ERR_PARAM, +} qh_parse_sta_t; + +/** + * @brief 单行显示参数。 + */ +typedef struct { + uint8_t color; + uint8_t row; + const uint8_t *text; + uint16_t text_len; +} qh_one_line_t; + +/** + * @brief 全屏显示参数。 + */ +typedef struct { + uint8_t color; + uint8_t x; + uint8_t y; + const uint8_t *text; + uint16_t text_len; +} qh_full_screen_t; + +/** + * @brief 固定格式显示参数。 + */ +typedef struct { + uint8_t type; + const uint8_t *raw; + uint16_t raw_len; +} qh_fixed_t; + +/** + * @brief 亮度设置参数。 + */ +typedef struct { uint8_t level; } qh_brightness_t; + +/** + * @brief 音量设置参数。 + */ +typedef struct { uint8_t level; } qh_volume_t; + +/** + * @brief 外设控制参数。 + */ +typedef struct { uint8_t ctrl; } qh_peripheral_t; + +/** + * @brief 文明用语播报参数。 + */ +typedef struct { uint8_t idx; } qh_civil_t; + +/** + * @brief 费额播报参数。 + */ +typedef struct { uint8_t type; uint32_t amount_fen; } qh_fee_t; + +/** + * @brief 青海协议解析结果。 + */ +typedef struct { + qh_pcmd_t cmd; + qh_parse_sta_t sta; + uint16_t data_len; + const uint8_t *data; + union { + qh_one_line_t one_line; + qh_full_screen_t full_screen; + qh_fixed_t fixed; + qh_brightness_t brightness; + qh_volume_t volume; + qh_peripheral_t peripheral; + qh_civil_t civil; + qh_fee_t fee; + } p; +} qh_parsed_cmd_t; + +/** + * @brief 解析青海协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体。 + */ +qh_parsed_cmd_t qh_parse_frame(const uint8_t *raw, uint16_t raw_len); + +/** + * @brief 执行青海协议命令。 + * @param ch 当前通道。 + * @param cmd 解析后的命令结构体。 + */ +void qh_execute_cmd(channel_t *ch, const qh_parsed_cmd_t *cmd); + +/** + * @brief 青海协议处理任务入口。 + * @param argument 任务参数,当前未使用。 + */ +void qh_proto_handle_task(void *argument); + +/** + * @brief 青海协议初始化入口。 + */ +void qh_proto_init(void); diff --git a/Application/Inc/ProtocolParser_QingHai/app_qh_proto_cmd.h b/Application/Inc/ProtocolParser_QingHai/app_qh_proto_cmd.h new file mode 100644 index 0000000..074a510 --- /dev/null +++ b/Application/Inc/ProtocolParser_QingHai/app_qh_proto_cmd.h @@ -0,0 +1,7 @@ +/** + * @file app_qh_proto_cmd.h + * @brief 青海协议命令执行接口 + */ +#pragma once + +#include "app_qh_proto.h" diff --git a/Application/Inc/ProtocolParser_QingHai/app_qh_proto_parse.h b/Application/Inc/ProtocolParser_QingHai/app_qh_proto_parse.h new file mode 100644 index 0000000..8cd83f1 --- /dev/null +++ b/Application/Inc/ProtocolParser_QingHai/app_qh_proto_parse.h @@ -0,0 +1,7 @@ +/** + * @file app_qh_proto_parse.h + * @brief 青海协议帧解析接口 + */ +#pragma once + +#include "app_qh_proto.h" diff --git a/Application/Inc/ProtocolParser_QingHai/app_qh_proto_voice.h b/Application/Inc/ProtocolParser_QingHai/app_qh_proto_voice.h new file mode 100644 index 0000000..facea27 --- /dev/null +++ b/Application/Inc/ProtocolParser_QingHai/app_qh_proto_voice.h @@ -0,0 +1,19 @@ +/** + * @file app_qh_proto_voice.h + * @brief 青海协议语音播报接口 + */ +#pragma once + +#include + +/** + * @brief 播报文明用语。 + * @param idx 文明用语索引。 + */ +void qh_voice_civil(uint8_t idx); + +/** + * @brief 播报费额提示语。 + * @param amount_fen 费额,单位分。 + */ +void qh_voice_fee_amount(uint32_t amount_fen); diff --git a/Application/Inc/ProtocolParser_ShanDong/app_sd_proto.h b/Application/Inc/ProtocolParser_ShanDong/app_sd_proto.h new file mode 100644 index 0000000..33160de --- /dev/null +++ b/Application/Inc/ProtocolParser_ShanDong/app_sd_proto.h @@ -0,0 +1,124 @@ +/** + * @file app_sd_proto.h + * @brief 山东车道费额显示器通信协议公共定义 + * + * 帧格式:'{'(0x7B) + 命令字(1B ASCII) + 参数长度(1B 二进制) + 参数(变长) + '}'(0x7D), + * 无 BCC/CRC。命令集:'1' 全屏单色(01红/02绿/03黄)、'2' 取版本号、 + * '3' 单行显示(颜色'0'~'2' + 行号'1'~'5' + GBK 文本)、 + * '4' 全屏可编辑(颜色 + X + Y + 文本,0x0A/0x0D 回车)、'5' 清屏、 + * '7' 亮度('0'~'5',0=自动调光)、'8' 外设控制(bit0 绿灯/bit1 红灯/bit2 黄闪报警)。 + * 无 '6' 命令。 + * + * 帧头与青海协议同构('{'+cmd+len+'}'),命令字 '1'~'5','7','8' 全部落入青海 probe + * 命令集:全协议构建下青海 probe 先注册(字母序 qh < sd)先认领,量产由 EIDE + * 目录排除纪律保证与青海/四川MTC 互斥(doc/05-01 §6 兼容矩阵)。 + */ +#pragma once + +#include +#include "app_dispatch.h" + +/** + * @brief 山东协议支持的命令类型。 + */ +typedef enum { + SD_PCMD_FILL_ALL = 0, /**< '1' 全屏单色显示(01红/02绿/03黄) */ + SD_PCMD_VERSION, /**< '2' 获取版本号 */ + SD_PCMD_ONE_LINE, /**< '3' 点阵单行任意显示 */ + SD_PCMD_FULL_SCREEN, /**< '4' 点阵全屏可编辑显示 */ + SD_PCMD_CLEAR, /**< '5' 全屏清除显示 */ + SD_PCMD_BRIGHTNESS, /**< '7' 显示亮度设定 */ + SD_PCMD_PERIPHERAL, /**< '8' 外设控制(红绿灯/报警) */ + SD_PCMD_INVALID, +} sd_pcmd_t; + +/** + * @brief 山东协议帧解析状态。 + */ +typedef enum { + SD_PARSE_OK = 0, + SD_PARSE_ERR_FRAME, /**< 帧头/帧尾/长度字段错误 */ + SD_PARSE_ERR_CMD, /**< 命令字不在命令集 */ + SD_PARSE_ERR_PARAM, /**< 参数非法 */ +} sd_parse_sta_t; + +/** + * @brief 单行显示参数('3' 命令)。 + */ +typedef struct { + uint8_t color; /**< 0 红 / 1 绿 / 2 黄 */ + uint8_t row; /**< 0~4,对应协议行号 1~5 */ + const uint8_t *text; /**< 显示数据(GBK/ASCII 混合) */ + uint16_t text_len; /**< 数据字节数 */ +} sd_one_line_t; + +/** + * @brief 全屏可编辑显示参数('4' 命令)。 + */ +typedef struct { + uint8_t color; /**< 0 红 / 1 绿 / 2 黄 */ + uint8_t x; /**< 显示区域 X 坐标,最左端为 0 */ + uint8_t y; /**< 显示区域 Y 坐标,最上端为 0 */ + const uint8_t *text; /**< 显示数据(GBK/ASCII,支持 0x0A/0x0D 回车) */ + uint16_t text_len; /**< 数据字节数 */ +} sd_full_screen_t; + +/** + * @brief 全屏单色显示参数('1' 命令)。 + */ +typedef struct { + uint8_t color; /**< 归一化 0 红 / 1 绿 / 2 黄(协议参数 01/02/03) */ +} sd_fill_all_t; + +/** + * @brief 亮度设置参数('7' 命令,0=自动调光,1~5 档)。 + */ +typedef struct { uint8_t level; } sd_brightness_t; + +/** + * @brief 外设控制参数('8' 命令,bit0 绿灯 / bit1 红灯 / bit2 黄闪报警)。 + */ +typedef struct { uint8_t ctrl; } sd_peripheral_t; + +/** + * @brief 山东协议解析结果。 + */ +typedef struct { + sd_pcmd_t cmd; + sd_parse_sta_t sta; + uint16_t data_len; + const uint8_t *data; + union { + sd_one_line_t one_line; + sd_full_screen_t full_screen; + sd_fill_all_t fill_all; + sd_brightness_t brightness; + sd_peripheral_t peripheral; + } p; +} sd_parsed_cmd_t; + +/** + * @brief 解析山东协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体;状态字段 sta 标识解析结果。 + */ +sd_parsed_cmd_t sd_parse_frame(const uint8_t *raw, uint16_t raw_len); + +/** + * @brief 执行山东协议命令。 + * @param ch 当前通道。 + * @param cmd 解析后的命令结构体。 + */ +void sd_execute_cmd(channel_t *ch, const sd_parsed_cmd_t *cmd); + +/** + * @brief 山东协议处理任务入口。 + * @param argument 任务参数,当前未使用。 + */ +void sd_proto_handle_task(void *argument); + +/** + * @brief 山东协议初始化入口。 + */ +void sd_proto_init(void); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_ShanDong/app_sd_proto_cmd.h b/Application/Inc/ProtocolParser_ShanDong/app_sd_proto_cmd.h new file mode 100644 index 0000000..01e86f3 --- /dev/null +++ b/Application/Inc/ProtocolParser_ShanDong/app_sd_proto_cmd.h @@ -0,0 +1,7 @@ +/** + * @file app_sd_proto_cmd.h + * @brief 山东协议命令执行接口 + */ +#pragma once + +#include "app_sd_proto.h" \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_ShanDong/app_sd_proto_parse.h b/Application/Inc/ProtocolParser_ShanDong/app_sd_proto_parse.h new file mode 100644 index 0000000..a9183b4 --- /dev/null +++ b/Application/Inc/ProtocolParser_ShanDong/app_sd_proto_parse.h @@ -0,0 +1,7 @@ +/** + * @file app_sd_proto_parse.h + * @brief 山东协议帧解析接口 + */ +#pragma once + +#include "app_sd_proto.h" \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_SiChuang_ETC/app_sc_etc_proto.h b/Application/Inc/ProtocolParser_SiChuang_ETC/app_sc_etc_proto.h new file mode 100644 index 0000000..893563c --- /dev/null +++ b/Application/Inc/ProtocolParser_SiChuang_ETC/app_sc_etc_proto.h @@ -0,0 +1,121 @@ +/** + * @file app_sc_etc_proto.h + * @brief 四川 ETC 费显协议(1D)公共定义 + * + * 帧格式:`0x0A + 显示方式(00静态/01滚屏) + 行号(00全屏/01~06) + 数据 + 0x0D` + * 灯控:`0A 36/37/38/39 0D`(红/绿/黄闪开/黄闪关;黄闪开 10 秒自动关闭) + * 亮度:`0A 40 XX YY 0D`(XX 00~07 亮度,00 最暗、07 最亮,YY 保留);心跳:`0A 50 0D` + * 上行应答:`0A 00 0D` 正常 / `0A 01 0D` 数据超长 / `0A 02 0D` 帧错误。 + * 数据变长 0x0D 定界(无固定 56B 全屏),单行 ≤24B、全屏 ≤145B; + * 0x20 清屏(行号 0 清全屏);0x30 初始化 → 软件复位;显示颜色跟随通行灯状态。 + */ +#pragma once + +#include +#include "app_dispatch.h" + +/** + * @brief ETC 协议命令类型。 + */ +typedef enum { + SC_ETC_PCMD_DISPLAY = 0, /**< 显示命令(静态/滚屏,行号细分) */ + SC_ETC_PCMD_LIGHT_RED, /**< 交通灯红 0A 36 0D */ + SC_ETC_PCMD_LIGHT_GREEN, /**< 交通灯绿 0A 37 0D */ + SC_ETC_PCMD_LIGHT_YELLOW_ON, /**< 黄闪报警开 0A 38 0D */ + SC_ETC_PCMD_LIGHT_YELLOW_OFF,/**< 黄闪报警关 0A 39 0D */ + SC_ETC_PCMD_BRIGHTNESS, /**< 亮度设置 0A 40 XX YY 0D */ + SC_ETC_PCMD_HEARTBEAT, /**< 心跳 0A 50 0D */ + SC_ETC_PCMD_INVALID, +} sc_etc_pcmd_t; + +/** + * @brief ETC 协议帧解析状态。 + */ +typedef enum { + SC_ETC_PARSE_OK = 0, + SC_ETC_PARSE_ERR_FRAME, /**< 帧头/帧尾错误 */ + SC_ETC_PARSE_ERR_CMD, /**< 命令编号不正确 */ + SC_ETC_PARSE_ERR_PARAM, /**< 参数非法 */ + SC_ETC_PARSE_ERR_TOOLONG, /**< 数据超长(单行 >24 / 全屏 >145 字节) */ +} sc_etc_parse_sta_t; + +/** + * @brief 显示命令参数。 + */ +typedef struct { + uint8_t mode; /**< 0=静态显示,1=滚屏显示 */ + uint8_t row; /**< 0=全屏,1~6=第 1~6 行 */ + const uint8_t *text; /**< 显示数据(GBK) */ + uint16_t text_len; /**< 数据字节数 */ +} sc_etc_display_t; + +/** + * @brief 亮度设置参数(0~7,0 最暗 7 最亮)。 + */ +typedef struct { + uint8_t level; +} sc_etc_brightness_t; + +/** + * @brief ETC 协议解析结果。 + */ +typedef struct { + sc_etc_pcmd_t cmd; + sc_etc_parse_sta_t sta; + uint16_t data_len; + const uint8_t *data; + union { + sc_etc_display_t display; + sc_etc_brightness_t brightness; + } p; +} sc_etc_parsed_cmd_t; + +/** + * @brief 解析 ETC 协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体;状态字段 sta 标识解析结果。 + */ +sc_etc_parsed_cmd_t sc_etc_parse_frame(const uint8_t *raw, uint16_t raw_len); + +/** + * @brief 执行 ETC 协议命令。 + * @param ch 当前通道。 + * @param cmd 解析后的命令结构体。 + */ +void sc_etc_execute_cmd(channel_t *ch, const sc_etc_parsed_cmd_t *cmd); + +/** + * @brief 心跳超时渲染「ETC车道关闭,请择道行驶」(由计时任务调用)。 + * @note 2026-08-17:心跳计时任务已停用(#if 0),本函数暂无调用方,保留待恢复。 + */ +void sc_etc_show_lane_closed(void); + +/** + * @brief 黄闪 10 秒自动关闭计时启动(0x38 收到时调用)。 + * @note 2026-08-17:心跳计时任务已停用,计时不再被消费——黄闪开启后不再 + * 10 秒自动关闭,须由 0A 39 显式关闭;本函数仍被调用仅记录状态。 + */ +void sc_etc_hs_timer_arm(void); + +/** + * @brief 黄闪 10 秒自动关闭计时取消(0x39 收到时调用)。 + */ +void sc_etc_hs_timer_cancel(void); + +/** + * @brief 黄闪 10 秒自动关闭到期回调(由心跳计时任务调用)。 + * @note 2026-08-17:心跳计时任务已停用,本函数暂无调用方,保留待恢复。 + */ +void sc_etc_hs_timeout(void); + +/** + * @brief ETC 协议处理任务入口。 + * @param argument 任务参数,当前未使用。 + */ +void sc_etc_proto_handle_task(void *argument); + +/** + * @brief ETC 协议初始化入口。 + */ +void sc_etc_proto_init(void); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_cmd.h b/Application/Inc/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_cmd.h new file mode 100644 index 0000000..d2a2f42 --- /dev/null +++ b/Application/Inc/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_cmd.h @@ -0,0 +1,7 @@ +/** + * @file app_sc_etc_proto_cmd.h + * @brief 四川 ETC 费显协议命令执行接口 + */ +#pragma once + +#include "app_sc_etc_proto.h" \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_parse.h b/Application/Inc/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_parse.h new file mode 100644 index 0000000..459cb7d --- /dev/null +++ b/Application/Inc/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_parse.h @@ -0,0 +1,7 @@ +/** + * @file app_sc_etc_proto_parse.h + * @brief 四川 ETC 费显协议帧解析接口 + */ +#pragma once + +#include "app_sc_etc_proto.h" \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto.h b/Application/Inc/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto.h new file mode 100644 index 0000000..c65b782 --- /dev/null +++ b/Application/Inc/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto.h @@ -0,0 +1,146 @@ +/** + * @file app_sc_mtc_proto.h + * @brief 四川 MTC 费显协议(1E 方案二)公共定义 + * + * 方案二(LED 点阵):帧 `'{' + 命令('1'~'9','A') + 参数 + '}'`——'}' 定界变长, + * 无长度字段、无 BCC(各命令逐字节扫 '}' 定帧,协议扫描上限 228; + * 本设备 probe 上限 SC_MTC_PAYLOAD_MAX=74B 队列约束)。 + * 带 BCC 变体不加判别:BCC 字节视为内容尾部一并渲染,BCC 不校验。 + * 主机查询 `0A 46 0A` → 应答 `0A 64 0A`;清屏 `0A 46 0D`。 + * 附加 7B 40~45 原始帧族(创迪科技添加协议,同样 '}' 定界): + * 40 改波特率 / 41 点阵大小 / 42 字体 / 43 协议类型 / 44 全屏点亮 / 45 取版本号。 + */ +#pragma once + +#include +#include "app_dispatch.h" + +/** + * @brief MTC 协议命令类型。 + */ +typedef enum { + SC_MTC_PCMD_INIT = 0, /**< '1' 初始化 */ + SC_MTC_PCMD_SELF_CHECK, /**< '2' 自检 */ + SC_MTC_PCMD_ONE_LINE, /**< '3' 点阵单行任意显示 */ + SC_MTC_PCMD_FULL_SCREEN, /**< '4' 点阵全屏任意显示 */ + SC_MTC_PCMD_CLEAR, /**< '5' 全屏清除 */ + SC_MTC_PCMD_FIXED_DISPLAY, /**< '6' 固定格式显示 */ + SC_MTC_PCMD_VOICE, /**< '7' 语音播报 */ + SC_MTC_PCMD_BRIGHTNESS, /**< '8' 显示亮度设定 */ + SC_MTC_PCMD_VOLUME, /**< '9' 语音音量设定 */ + SC_MTC_PCMD_COLOR, /**< 'A' 显示颜色设定 */ + SC_MTC_PCMD_RAW_BAUD, /**< 7B 40 修改波特率 */ + SC_MTC_PCMD_RAW_DOT_SIZE, /**< 7B 41 修改点阵大小 */ + SC_MTC_PCMD_RAW_FONT, /**< 7B 42 修改字体类型 */ + SC_MTC_PCMD_RAW_PROTO, /**< 7B 43 设置协议类型 */ + SC_MTC_PCMD_RAW_FILL_ALL, /**< 7B 44 全屏点亮 */ + SC_MTC_PCMD_RAW_VERSION, /**< 7B 45 获取版本号 */ + SC_MTC_PCMD_HOST_QUERY, /**< 0A 46 0A 主机查询 */ + SC_MTC_PCMD_HOST_CLEAR, /**< 0A 46 0D 主机清屏 */ + SC_MTC_PCMD_INVALID, +} sc_mtc_pcmd_t; + +/** + * @brief MTC 协议帧解析状态。 + */ +typedef enum { + SC_MTC_PARSE_OK = 0, + SC_MTC_PARSE_ERR_FRAME, + SC_MTC_PARSE_ERR_CMD, + SC_MTC_PARSE_ERR_BCC, + SC_MTC_PARSE_ERR_PARAM, +} sc_mtc_parse_sta_t; + +/** + * @brief 单行显示参数。 + */ +typedef struct { + uint8_t row; /**< 行号 0~5(协议 '1'~'6') */ + const uint8_t *text; /**< 变长显示数据('}' 定界,最长 70B) */ + uint16_t text_len; +} sc_mtc_one_line_t; + +/** + * @brief 全屏显示参数(变长文本,渲染按 16B/行切分)。 + */ +typedef struct { + const uint8_t *text; + uint16_t text_len; +} sc_mtc_full_screen_t; + +/** + * @brief 固定格式显示参数。 + */ +typedef struct { + uint8_t type; /**< '0' 客车(12B) / '1' 货车(21B) */ + const uint8_t *raw; + uint16_t raw_len; +} sc_mtc_fixed_t; + +/** + * @brief 语音播报参数。 + */ +typedef struct { + uint8_t idx; /**< 固定语音 '0'~'7';'8' 为自定义文本 */ + const uint8_t *text; /**< idx='8' 时的 GBK 文本 */ + uint16_t text_len; +} sc_mtc_voice_t; + +/** + * @brief 亮度/音量/颜色等单字节参数。 + */ +typedef struct { + uint8_t val; +} sc_mtc_byte_val_t; + +/** + * @brief 7B 40 修改波特率参数。 + */ +typedef struct { + uint8_t mode; /**< 00=9600, 01=115200 */ + uint16_t baud16;/**< 波特率低 16 位(0x2580=9600, 0xC200=115200) */ +} sc_mtc_baud_t; + +/** + * @brief MTC 协议解析结果。 + */ +typedef struct { + sc_mtc_pcmd_t cmd; + sc_mtc_parse_sta_t sta; + uint16_t data_len; + const uint8_t *data; + union { + sc_mtc_one_line_t one_line; + sc_mtc_full_screen_t full_screen; + sc_mtc_fixed_t fixed; /**< 固定格式显示(X1 起 raw_len=11/20 字节) */ + sc_mtc_voice_t voice; + sc_mtc_byte_val_t byte_val; /* 亮度/音量/颜色/点阵大小/字体/协议类型/全屏点亮 */ + sc_mtc_baud_t baud; + } p; +} sc_mtc_parsed_cmd_t; + +/** + * @brief 解析 MTC 协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体;状态字段 sta 标识解析结果。 + */ +sc_mtc_parsed_cmd_t sc_mtc_parse_frame(const uint8_t *raw, uint16_t raw_len); + +/** + * @brief 执行 MTC 协议命令。 + * @param ch 当前通道。 + * @param cmd 解析后的命令结构体。 + */ +void sc_mtc_execute_cmd(channel_t *ch, const sc_mtc_parsed_cmd_t *cmd); + +/** + * @brief MTC 协议处理任务入口。 + * @param argument 任务参数,当前未使用。 + */ +void sc_mtc_proto_handle_task(void *argument); + +/** + * @brief MTC 协议初始化入口。 + */ +void sc_mtc_proto_init(void); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_cmd.h b/Application/Inc/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_cmd.h new file mode 100644 index 0000000..6f66c0a --- /dev/null +++ b/Application/Inc/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_cmd.h @@ -0,0 +1,7 @@ +/** + * @file app_sc_mtc_proto_cmd.h + * @brief 四川 MTC 费显协议命令执行接口 + */ +#pragma once + +#include "app_sc_mtc_proto.h" \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_parse.h b/Application/Inc/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_parse.h new file mode 100644 index 0000000..882351d --- /dev/null +++ b/Application/Inc/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_parse.h @@ -0,0 +1,7 @@ +/** + * @file app_sc_mtc_proto_parse.h + * @brief 四川 MTC 费显协议帧解析接口 + */ +#pragma once + +#include "app_sc_mtc_proto.h" \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_voice.h b/Application/Inc/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_voice.h new file mode 100644 index 0000000..70284c7 --- /dev/null +++ b/Application/Inc/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_voice.h @@ -0,0 +1,15 @@ +/** + * @file app_sc_mtc_proto_voice.h + * @brief 四川 MTC 费显协议语音播报接口 + */ +#pragma once + +#include + +/** + * @brief 播报固定语音('7' 命令 '0'~'7')。 + * @param idx 语音索引(0~7)。 + * @note 静态用语直接送 TTS;车型/金额/总重等动态变量不做拼读 + * (TTS 板不支持变量合成,后续可扩展数字拼接)。 + */ +void sc_mtc_voice_play_fixed(uint8_t idx); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_SiChuang_Overload/app_sc_ol_proto.h b/Application/Inc/ProtocolParser_SiChuang_Overload/app_sc_ol_proto.h new file mode 100644 index 0000000..07fcae4 --- /dev/null +++ b/Application/Inc/ProtocolParser_SiChuang_Overload/app_sc_ol_proto.h @@ -0,0 +1,124 @@ +/** + * @file app_sc_ol_proto.h + * @brief 四川治超屏协议(1F,3.5.1 串口方式)公共定义 + * + * 帧格式:`FF + 长度(1B,含头尾总长 07~FF,0xFE 排除防 RLS 吞帧) + 命令 + 亮度(00~FF) + + * 数据(行显示变长 ≤24B;全屏可变长 ≤249B) + BCC + FF`。 + * BCC = 帧头/长度/命令/亮度/数据段五字段逐字节异或。 + * 全屏显示 80;行显示 81~88(支持 8 行);清屏 94;亮度 96(00=自动调光); + * 通行灯 99(00红/01绿);黄闪 98(00关/01开); + * 查询类(屏须应答):A0 取显示内容 → A1~A8 每行独立帧;B6 取亮度;B9 取通行灯;B8 取黄闪。 + */ +#pragma once + +#include +#include "app_dispatch.h" + +/** 帧最小/最大长度(FF 07 94 00 BCC FF = 7;长度字段 1 字节,上限 255,容纳 0x80 全屏长数据段) */ +#define SC_OL_FRAME_LEN_MIN (7U) +#define SC_OL_FRAME_LEN_MAX (0xFFU) + +/** 每行数据字节数(8 列 × 2,应答帧用;行显示数据=列数×2)与行文本上限(FONT16 24B,超长截断) */ +#define SC_OL_BYTES_PER_LINE (16U) +#define SC_OL_LINE_TEXT_MAX (24U) +#define SC_OL_LINE_COUNT (8U) + +/** + * @brief 治超协议命令类型。 + */ +typedef enum { + SC_OL_PCMD_FULL_SCREEN = 0, /**< 80 全屏显示 */ + SC_OL_PCMD_LINE_1, /**< 81 第一行显示 */ + SC_OL_PCMD_LINE_2, /**< 82 第二行显示 */ + SC_OL_PCMD_LINE_3, /**< 83 第三行显示 */ + SC_OL_PCMD_LINE_4, /**< 84 第四行显示 */ + SC_OL_PCMD_LINE_5, /**< 85 第五行显示 */ + SC_OL_PCMD_LINE_6, /**< 86 第六行显示 */ + SC_OL_PCMD_LINE_7, /**< 87 第七行显示 */ + SC_OL_PCMD_LINE_8, /**< 88 第八行显示 */ + SC_OL_PCMD_CLEAR, /**< 94 清屏 */ + SC_OL_PCMD_BRIGHTNESS, /**< 96 亮度调节 */ + SC_OL_PCMD_LANE_LIGHT, /**< 99 通行灯(00红/01绿) */ + SC_OL_PCMD_YELLOW_FLASH, /**< 98 黄闪(00关/01开) */ + SC_OL_PCMD_QUERY_CONTENT,/**< A0 获取费显当前显示内容 */ + SC_OL_PCMD_QUERY_BRIGHT, /**< B6 获取费显亮度 */ + SC_OL_PCMD_QUERY_LANE, /**< B9 获取通行灯状态 */ + SC_OL_PCMD_QUERY_FLASH, /**< B8 获取黄闪状态 */ + SC_OL_PCMD_INVALID, +} sc_ol_pcmd_t; + +/** + * @brief 治超协议帧解析状态。 + */ +typedef enum { + SC_OL_PARSE_OK = 0, + SC_OL_PARSE_ERR_FRAME, + SC_OL_PARSE_ERR_CMD, + SC_OL_PARSE_ERR_BCC, + SC_OL_PARSE_ERR_PARAM, +} sc_ol_parse_sta_t; + +/** + * @brief 行显示参数。 + */ +typedef struct { + uint8_t row; /**< 行索引 0~7 */ + const uint8_t *text; /**< 行数据(变长,≤24B 截断渲染) */ + uint16_t text_len; +} sc_ol_line_t; + +/** + * @brief 全屏显示参数(数据段可变长 ≤249B,随长度字段 07~FF)。 + */ +typedef struct { + const uint8_t *text; + uint16_t text_len; +} sc_ol_full_screen_t; + +/** + * @brief 单字节数值参数(亮度/通行灯/黄闪)。 + */ +typedef struct { + uint8_t val; +} sc_ol_byte_val_t; + +/** + * @brief 治超协议解析结果。 + */ +typedef struct { + sc_ol_pcmd_t cmd; + sc_ol_parse_sta_t sta; + uint16_t data_len; + const uint8_t *data; + union { + sc_ol_full_screen_t full_screen; + sc_ol_line_t line; + sc_ol_byte_val_t byte_val; + } p; +} sc_ol_parsed_cmd_t; + +/** + * @brief 解析治超协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体;状态字段 sta 标识解析结果。 + */ +sc_ol_parsed_cmd_t sc_ol_parse_frame(const uint8_t *raw, uint16_t raw_len); + +/** + * @brief 执行治超协议命令。 + * @param ch 当前通道。 + * @param cmd 解析后的命令结构体。 + */ +void sc_ol_execute_cmd(channel_t *ch, const sc_ol_parsed_cmd_t *cmd); + +/** + * @brief 治超协议处理任务入口。 + * @param argument 任务参数,当前未使用。 + */ +void sc_ol_proto_handle_task(void *argument); + +/** + * @brief 治超协议初始化入口。 + */ +void sc_ol_proto_init(void); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_cmd.h b/Application/Inc/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_cmd.h new file mode 100644 index 0000000..6fea525 --- /dev/null +++ b/Application/Inc/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_cmd.h @@ -0,0 +1,7 @@ +/** + * @file app_sc_ol_proto_cmd.h + * @brief 四川治超屏协议命令执行接口 + */ +#pragma once + +#include "app_sc_ol_proto.h" \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_parse.h b/Application/Inc/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_parse.h new file mode 100644 index 0000000..fb8906a --- /dev/null +++ b/Application/Inc/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_parse.h @@ -0,0 +1,7 @@ +/** + * @file app_sc_ol_proto_parse.h + * @brief 四川治超屏协议帧解析接口 + */ +#pragma once + +#include "app_sc_ol_proto.h" \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_YunNan/app_yn_proto.h b/Application/Inc/ProtocolParser_YunNan/app_yn_proto.h new file mode 100644 index 0000000..55c88d7 --- /dev/null +++ b/Application/Inc/ProtocolParser_YunNan/app_yn_proto.h @@ -0,0 +1,150 @@ +/** + * @file app_yn_proto.h + * @brief 云南常规费显协议公共定义 + * + * 帧格式:'{'(0x7B) + 命令字(1B) + 参数长度(1B 二进制) + 参数(变长) + '}'(0x7D), + * 无校验。与青海/山东/贵州帧结构完全同构。 + * 命令集:'1' 主机查询、'2' 自检、'3' 点阵单行任意显示、'4' 点阵全屏可编辑显示、 + * '5' 全屏清除、'6' 单行清除、'7' 礼貌用语语音播报、'8' 显示亮度设定、 + * '9' 语音音量设定、'A' 外设控制、'B' 收费金额语音播放; + * 另有 0x01 全屏点亮 / 0x02 获取版本号两个二进制命令字。 + * 协议原文:01 云南常规费显协议-云南LED费显P5(2022.7.5,云南弥玉项目 2022-S134)。 + * + * 帧头与青海协议同构('{'+cmd+len+'}'),命令字 '1'~'9','A','B' 全部落入青海 + * probe 命令集:全协议构建下青海 probe 先注册(源码收录序 qh 在前)先认领, + * 量产由 EIDE 目录排除纪律保证云南与其余 '{' 帧族互斥(doc/05-01 §6 兼容矩阵)。 + * 0x01/0x02 为二进制命令字,不属于青海/山东/四川MTC 的 ASCII 命令集,probe 首命令字 + * 快拒,无冲突。 + */ +#pragma once + +#include +#include "app_dispatch.h" + +/* 0x02 版本号应答:回固件 PROGRAM_CODE(app_boot.h 编译期常量), + * 不再使用硬编码 "YN_FX_P5_1.0"(用户决定 7,2026-08-24 修订)。 */ + +/** + * @brief 云南协议支持的命令类型。 + */ +typedef enum { + YN_PCMD_HOST_QUERY = 0, /**< '1' 主机查询 */ + YN_PCMD_SELF_CHECK, /**< '2' 自检 */ + YN_PCMD_ONE_LINE, /**< '3' 点阵单行任意显示 */ + YN_PCMD_FULL_SCREEN, /**< '4' 点阵全屏可编辑显示 */ + YN_PCMD_CLEAR, /**< '5' 全屏清除显示 */ + YN_PCMD_CLEAR_ROW, /**< '6' 单行清除显示 */ + YN_PCMD_CIVIL_VOICE, /**< '7' 礼貌用语语音播报 */ + YN_PCMD_BRIGHTNESS, /**< '8' 显示亮度设定 */ + YN_PCMD_VOLUME, /**< '9' 语音音量设定 */ + YN_PCMD_PERIPHERAL, /**< 'A' 外设控制 */ + YN_PCMD_FEE_VOICE, /**< 'B' 收费金额语音播放 */ + YN_PCMD_FILL_ALL, /**< 0x01 全屏点亮(01红~07白 七色,用户决定 10) */ + YN_PCMD_VERSION, /**< 0x02 获取版本号 */ + YN_PCMD_INVALID, +} yn_pcmd_t; + +/** + * @brief 云南协议帧解析状态。 + */ +typedef enum { + YN_PARSE_OK = 0, + YN_PARSE_ERR_FRAME, /**< 帧头/帧尾/长度字段错误 */ + YN_PARSE_ERR_CMD, /**< 命令字不在命令集 */ + YN_PARSE_ERR_PARAM, /**< 参数非法 */ +} yn_parse_sta_t; + +/** + * @brief 单行显示参数('3' 命令)。 + */ +typedef struct { + uint8_t color; /**< 0 红 / 1 绿 / 2 黄 */ + uint8_t row; /**< 0~4,对应协议行号 1~5 */ + const uint8_t *text; /**< 显示数据(GBK/ASCII 混合) */ + uint16_t text_len; /**< 数据字节数 */ +} yn_one_line_t; + +/** + * @brief 全屏可编辑显示参数('4' 命令)。 + */ +typedef struct { + uint8_t color; /**< 0 红 / 1 绿 / 2 黄 */ + uint8_t x; /**< 显示区域 X 坐标,最左端为 0 */ + uint8_t y; /**< 显示区域 Y 坐标,最上端为 0 */ + const uint8_t *text; /**< 显示数据(GBK/ASCII,支持 0x0A 0x0D / 0x0D 0x0A 回车换行) */ + uint16_t text_len; /**< 数据字节数 */ +} yn_full_screen_t; + +/** + * @brief 单行清除参数('6' 命令,行号 '1'~'5')。 + */ +typedef struct { uint8_t row; } yn_clear_row_t; + +/** + * @brief 亮度设置参数('8' 命令)。 + * @note level 取值(用户决定 5,2026-08-24 修订): + * 0 = 自动亮度(协议参数 NUL 字节 0x00 → 恢复光敏自动调光); + * 1~8 = 手动亮度档(协议参数 ASCII '1'~'8' = 0x31~0x38,8 最亮)。 + */ +typedef struct { uint8_t level; } yn_brightness_t; + +/** + * @brief 音量设置参数('9' 命令,'1'~'5')。 + */ +typedef struct { uint8_t level; } yn_volume_t; + +/** + * @brief 外设控制参数('A' 命令,bit0 绿灯 / bit1 红灯 / bit2 黄闪报警)。 + */ +typedef struct { uint8_t ctrl; } yn_peripheral_t; + +/** + * @brief 费额语音参数('B' 命令,金额单位分)。 + */ +typedef struct { uint32_t amount_fen; } yn_fee_t; + +/** + * @brief 礼貌用语播报参数('7' 命令)。 + */ +typedef struct { uint8_t idx; } yn_civil_t; + +/** + * @brief 全屏点亮参数(0x01 命令)。 + * @note DATA0 为二进制字节,映射到 display_color_t 枚举值(恒等): + * 0x01 红 / 0x02 绿 / 0x03 黄(协议原文三色,保留兼容)/ + * 0x04 蓝 / 0x05 紫 / 0x06 青 / 0x07 白(用户决定 10 扩展取值, + * P5 户外全彩屏支持 8 色,黑色除外——全屏黑等效 '5' 全屏清除)。 + */ +typedef struct { uint8_t color; } yn_fill_all_t; + +/** + * @brief 云南协议解析结果。 + */ +typedef struct { + yn_pcmd_t cmd; + yn_parse_sta_t sta; + uint16_t data_len; + const uint8_t *data; + union { + yn_one_line_t one_line; + yn_full_screen_t full_screen; + yn_clear_row_t clear_row; + yn_brightness_t brightness; + yn_volume_t volume; + yn_peripheral_t peripheral; + yn_fee_t fee; + yn_civil_t civil; + yn_fill_all_t fill_all; + } p; +} yn_parsed_cmd_t; + +/** + * @brief 云南协议处理任务入口。 + * @param argument 任务参数,当前未使用。 + */ +void yn_proto_handle_task(void *argument); + +/** + * @brief 云南协议初始化入口。 + */ +void yn_proto_init(void); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_YunNan/app_yn_proto_cmd.h b/Application/Inc/ProtocolParser_YunNan/app_yn_proto_cmd.h new file mode 100644 index 0000000..71a53bd --- /dev/null +++ b/Application/Inc/ProtocolParser_YunNan/app_yn_proto_cmd.h @@ -0,0 +1,14 @@ +/** + * @file app_yn_proto_cmd.h + * @brief 云南协议命令执行接口 + */ +#pragma once + +#include "app_yn_proto.h" + +/** + * @brief 执行云南协议命令。 + * @param ch 当前通道(应答回源通道)。 + * @param cmd 解析后的命令结构体。 + */ +void yn_execute_cmd(channel_t *ch, const yn_parsed_cmd_t *cmd); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_YunNan/app_yn_proto_parse.h b/Application/Inc/ProtocolParser_YunNan/app_yn_proto_parse.h new file mode 100644 index 0000000..3af4b86 --- /dev/null +++ b/Application/Inc/ProtocolParser_YunNan/app_yn_proto_parse.h @@ -0,0 +1,23 @@ +/** + * @file app_yn_proto_parse.h + * @brief 云南协议帧解析接口 + */ +#pragma once + +#include "app_yn_proto.h" + +/** + * @brief 解析云南协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体;状态字段 sta 标识解析结果。 + */ +yn_parsed_cmd_t yn_parse_frame(const uint8_t *raw, uint16_t raw_len); + +/** + * @brief 金额 ASCII 数字串转分(整数运算,无浮点)。 + * @param str 金额数字串(整数或带小数,如 "123.4")。 + * @param len 数字串字节数。 + * @return 金额(分)= 整数部分×100 + 小数前两位(%.2f 语义,第三位起截断)。 + */ +uint32_t yn_amount_to_fen(const uint8_t *str, uint16_t len); \ No newline at end of file diff --git a/Application/Inc/ProtocolParser_YunNan/app_yn_proto_voice.h b/Application/Inc/ProtocolParser_YunNan/app_yn_proto_voice.h new file mode 100644 index 0000000..eda8582 --- /dev/null +++ b/Application/Inc/ProtocolParser_YunNan/app_yn_proto_voice.h @@ -0,0 +1,19 @@ +/** + * @file app_yn_proto_voice.h + * @brief 云南协议语音播报接口 + */ +#pragma once + +#include + +/** + * @brief 播报礼貌用语。 + * @param idx 礼貌用语索引(0~3)。 + */ +void yn_voice_civil(uint8_t idx); + +/** + * @brief 播报收费金额提示语(0 元不播报,小数播小数)。 + * @param amount_fen 收费金额,单位分。 + */ +void yn_voice_fee_amount(uint32_t amount_fen); \ No newline at end of file diff --git a/Application/Inc/app_boot.h b/Application/Inc/app_boot.h index 1dd6f5d..0694873 100644 --- a/Application/Inc/app_boot.h +++ b/Application/Inc/app_boot.h @@ -1,8 +1,12 @@ /** * @file app_boot.h - * @brief 系统启动编排器 + * @brief 系统启动编排器接口 + * + * PROGRAM_CODE 为产品程序编码:开机画面与协议版本应答 */ #pragma once -void app_boot(void); +#define PROGRAM_CODE "9K1F3127E2" + +void app_boot(void); \ No newline at end of file diff --git a/Application/Inc/app_default_display.h b/Application/Inc/app_default_display.h new file mode 100644 index 0000000..aefe897 --- /dev/null +++ b/Application/Inc/app_default_display.h @@ -0,0 +1,26 @@ +/** + * @file app_default_display.h + * @brief 注册制默认显示界面 — 协议模块可选覆盖,默认欢迎画面兜底 + * + * 协议模块可在 sw_initcall(sw_board_init 内执行,早于 init_task 中 + * app_default_display 调用)注册自己的默认显示回调;未注册时回退默认 + * 欢迎画面。单槽设计:首个注册生效(多协议共存时归属由 initcall + * 字母序决定;量产地区协议互斥,无实际竞争)。 + */ + +#pragma once + +typedef void (*app_default_display_fn_t)(void); + +/** @brief 注册默认显示回调(首个注册生效,后续注册忽略) */ +void app_default_display_register(app_default_display_fn_t fn); + +/** @brief 渲染默认显示界面:有注册回调则调用之,否则渲染默认欢迎画面 */ +void app_default_display(void); + +/** + * @brief 恢复默认显示界面:有注册回调则调用之,否则清屏兜底。 + * @note 供协议模块在显示覆盖画面(如 CQ 心跳故障屏)后恢复默认画面调用; + * 与 app_default_display 的区别:无注册回调时清屏而非渲染欢迎画面。 + */ +void app_default_display_show(void); \ No newline at end of file diff --git a/Application/Inc/app_dispatch.h b/Application/Inc/app_dispatch.h index b9c4c91..2712870 100644 --- a/Application/Inc/app_dispatch.h +++ b/Application/Inc/app_dispatch.h @@ -5,6 +5,9 @@ * channel_t 使用虚表模式(ch_ops_t)实现 OCP:新增通道类型无需修改此文件。 * 通道-协议映射由协议模块通过 app_proto_bind_channel 声明。 * 协议模块通过 sw_app_initcall 自注册,框架零改动。 + * + * RB:一物理通道一环缓(RJ45 / RS485 / RS232);体由协议 TU 以 RB_PROVIDE_WEAK + * 编译期提供,未编入则槽为 NULL。RS232_1(USART6)旁路语音 TX,禁止 bind。 */ #pragma once @@ -20,7 +23,25 @@ #define MAX_CHANNELS (32U) #define FRAME_DATA_MAX_LEN (1044U) #define PROTO_MAX_COUNT (32U) -#define RB_CNT_MAX (4U) + +/* ---- 物理通道 RB 槽(与 CH_ID 解耦;网口逻辑通道共享 RJ45 槽)---- */ +typedef enum { + RB_SLOT_RJ45 = 0, /**< ETH:IAP/TCP/UDP/MQTT/地区协议共享 */ + RB_SLOT_RS485 = 1, /**< USART1:地区协议链式 */ + RB_SLOT_RS232 = 2, /**< USART3:地区协议链式 */ + RB_SLOT_COUNT = 3, +} rb_slot_t; + +#define RB_CNT_MAX ((uint8_t)RB_SLOT_COUNT) + +#define RB_SIZE_RJ45 (1536U) /* ≥ IAP 最大帧 1044 + 余量;原 2304 瘦身 */ +#define RB_SIZE_RS485 (768U) /* 原 512 +256,覆盖 RLS 满屏位图 ≤530 */ +#define RB_SIZE_RS232 (768U) /* 与 RS485 对齐;青海最大帧 259 */ + +/* 协议 TU 的 RB_PROVIDE_WEAK 入口名(与 app_dispatch.c 一致) */ +#define RB_PROVIDE_RJ45 rb_provide_rj45 +#define RB_PROVIDE_RS485 rb_provide_rs485 +#define RB_PROVIDE_RS232 rb_provide_rs232 /* ---- 通道标识 ---- */ typedef enum { @@ -30,8 +51,9 @@ typedef enum { CH_ID_TCP_CLIENT = 3, CH_ID_UDP = 4, CH_ID_MQTT = 5, - CH_ID_RS232_1 = 6, /**< Project_STD 独有: 第二路 RS232 (USART6) */ - CH_ID_MAX = 7, + CH_ID_RS232_1 = 6, /**< [仅语音 TX] USART6 — 禁止任何协议 bind_channel */ + CH_ID_UDP_CQ = 7, /**< CQ 业务口 UDP(PROTO_CHONGQING 读 Sector1 net_cfg.port 默认 20103;dev 构建固定 20103) */ + CH_ID_MAX = 8, } channel_id_t; /* ---- 通道连接状态 ---- */ @@ -50,13 +72,6 @@ typedef enum { typedef uint32_t proto_mask_t; -/* ---- 环形缓冲区分组 ---- */ -typedef enum { - RB_GROUP_IAP = 0, - RB_GROUP_PROTO = 1, - RB_GROUP_COUNT = 2, -} rb_group_id_t; - /* ---- 通道抽象(OCP 虚表) ---- */ typedef struct channel channel_t; @@ -81,9 +96,6 @@ typedef struct { uint8_t data[]; /* 柔性数组, sizeof(frame_msg_t)=8 */ } frame_msg_t; -/* 分发任务缓冲区上限 (平台约束, 非协议知识) */ -#define FRAME_DATA_MAX_LEN (1044U) - typedef struct { void (*init)(void); } proto_module_entry_t; @@ -113,5 +125,5 @@ void frame_dispatch_task(void *argument); void app_channel_dispatch(const channel_t *ch, const uint8_t *data, uint16_t len); void app_channel_register(channel_id_t ch_id, channel_t *ch); channel_t *app_channel_get(channel_id_t ch_id); -void channel_send(channel_t *ch, uint8_t *data, uint16_t len); +int32_t channel_send(channel_t *ch, uint8_t *data, uint16_t len); void app_dispatch_init(void); diff --git a/Application/Inc/app_factory_test.h b/Application/Inc/app_factory_test.h index 007c216..b768a11 100644 --- a/Application/Inc/app_factory_test.h +++ b/Application/Inc/app_factory_test.h @@ -3,10 +3,18 @@ * @brief 出厂检测模式 — TEST 按键驱动状态机 * * 状态流程: - * IDLE ──[TEST]──▶ SHOW_CODE ──[TEST]──▶ DEAD_PIXEL ──[TEST×7]──▶ AGING ──[TEST]──▶ 退出 + * IDLE ──[TEST]──▶ SHOW_CODE ──[TEST]──▶ DEAD_PIXEL ──[TEST×7]──▶ OBLIQUE_SCAN ──[TEST]──▶ AGING ──[TEST]──▶ 退出 */ #pragma once -/** @brief 终止工厂测试模式 */ +#include + +/** @brief 业务数据到达 → 中止工厂测试当前序列,monitor 回 IDLE 待机(不销毁任务,TEST 键保持可用) */ void app_factory_mode_interrupt(void); + +/** + * @brief 查询工厂测试是否处于激活状态(进入检测序列 true,回 IDLE false)。 + * @note 供心跳类定时任务判定是否暂停计数(工厂测试占用显示资源)。 + */ +bool app_factory_test_active(void); diff --git a/Application/Inc/app_light_sensor.h b/Application/Inc/app_light_sensor.h index f6413c8..ae5baf0 100644 --- a/Application/Inc/app_light_sensor.h +++ b/Application/Inc/app_light_sensor.h @@ -12,4 +12,10 @@ extern osThreadId_t g_light_sensor_task_handle; void app_light_sensor_init(void); void app_light_sensor_task(void *argument); -light_sensor_dev_t *app_light_sensor_get(void); + +/** + * @brief 设置光敏自动调光的亮度范围(Application 层包装) + * @param min 最低亮度等级 (1~8) + * @param max 最高亮度等级 (1~8) + */ +void app_light_sensor_set_range(uint8_t min, uint8_t max); diff --git a/Application/Inc/app_net_boot.h b/Application/Inc/app_net_boot.h new file mode 100644 index 0000000..98354e9 --- /dev/null +++ b/Application/Inc/app_net_boot.h @@ -0,0 +1,14 @@ +/** + * @file app_net_boot.h + * @brief 网络配置启动应用(横切职责中立模块,两口径共用) + * + * 「把 Sector1 net_cfg 应用到 netif + TCP Server 口」的统一入口, + * 由 app_boot.c init_task 调用(dev/LDI 与 PROTO_CHONGQING 两口径都编入)。 + */ + +#ifndef APP_NET_BOOT_H +#define APP_NET_BOOT_H + +void app_net_boot_apply(void); + +#endif /* APP_NET_BOOT_H */ \ No newline at end of file diff --git a/Application/Inc/app_render.h b/Application/Inc/app_render.h index 6453ec4..de20c99 100644 --- a/Application/Inc/app_render.h +++ b/Application/Inc/app_render.h @@ -1,10 +1,14 @@ /** * @file app_render.h - * @brief 文字/图形渲染 — tagged union 统一入参 + * @brief 文字/图形渲染 — tagged union 统一入参 + 多字库芯片切换 * - * 字库存储为 (字号, 编码, 字型) 三元组的顺序拼接。 - * 新增字号/字型只需在 g_font_lib 追加条目。 - * 新增渲染类型只需在 render_type_t 和 union 中追加。 + * 上电时通过 DEV_KEY_DIP2 拨码开关自动选择字库芯片配置: + * - DIP2 逻辑 0(开关 OFF,GPIO 高)→ W25Q64: 非连续布局, 16/24/32pt, GB2312 94列序 + * - DIP2 逻辑 1(开关 ON,GPIO 低)→ MX25L256: 连续布局, 14/16/20/24/32pt, GBK 190列序 + * + * 运行中也可通过 app_render_chip_select() 手动切换。 + * 每种芯片对应一个 font_chip_config_t 实例,包含地址查找表和自适应字号候选列表。 + * 新增字号/字型只需在对应配置的 region 表中追加条目。 */ #pragma once @@ -93,11 +97,40 @@ typedef struct { }; } render_cfg_t; +/* ---- 字库芯片配置 ---- */ + +/** @brief 字库芯片配置 — 每种芯片一个实例 */ +typedef struct { + const char *name; /* 配置名称 */ + const void *regions; /* flash_region_t 数组指针 */ + uint8_t region_count; /* 数组元素个数 */ + const font_size_t *adaptive_sizes; /* 自适应字号候选列表(从大到小) */ + uint8_t adaptive_count; /* 候选列表长度 */ + bool ascii_raw_code; /* true=ASCII 用原始字符码(W25Q64); false=减 0x20(MX25L256) */ + bool gbk_index_190; /* true=GBK 用 190 列序(MX25L256); false=94 列序(W25Q64) */ +} font_chip_config_t; + +/** @brief 芯片配置ID */ +typedef enum { + FONT_CHIP_W25Q64 = 0, /* W25Q64: 非连续布局, 16/24/32, ASCII 用原始字符码 */ + FONT_CHIP_MX25L256 = 1, /* MX25L256: 连续布局, 14/16/20/24/32, ASCII 用 ch-0x20 */ + FONT_CHIP_CNT, +} font_chip_id_t; + /* ---- API(模块自注册 sw_app_initcall,调用方无需传 display/font 句柄)---- */ /** @brief 统一渲染入口 — 根据 cfg->type 分派到内部实现 */ void app_render(const render_cfg_t *cfg); +/** @brief 切换字库芯片配置 — 可在任意时刻调用,立即生效 + * @param id 芯片配置ID (FONT_CHIP_W25Q64 / FONT_CHIP_MX25L256) + * @return true=切换成功, false=无效ID */ +bool app_render_chip_select(font_chip_id_t id); + +/** @brief 获取当前激活的字库芯片配置 + * @return 当前配置ID */ +font_chip_id_t app_render_chip_current(void); + /* ---- 持久化显示 ---- */ #define RENDER_PERSIST_MAGIC (0x0d000721U) @@ -106,9 +139,9 @@ typedef struct [[gnu::packed]] { uint32_t magic; uint16_t screen_rows; uint16_t screen_cols; - uint8_t color; /* 非黑像素颜色 (display_color_t) */ - uint32_t crc32; /* bitmap 数据的 CRC32 */ - uint8_t bitmap[]; /* ((rows*cols+7)/8) 字节, MSB first per row */ + uint8_t color; /* 非黑像素颜色 (display_color_t) */ + uint32_t crc32; /* bitmap 数据的 CRC32 */ + uint8_t bitmap[]; /* ((rows*cols+7)/8) 字节, MSB first per row */ } render_persist_t; /** @brief 将当前显存写入存储设备持久化扇区 */ @@ -117,10 +150,3 @@ void app_render_save(void); /** @brief 从存储设备加载持久化数据恢复显存 * @return true=成功恢复并置脏标记, false=无有效数据/尺寸不匹配/CRC错误 */ bool app_render_restore(void); - -// 关于msl的api -void msl_render_text(const render_cfg_t *cfg); -void msl_render_bitmap(const render_cfg_t *cfg); -void msl_render_fill(const render_cfg_t *cfg); - -void app_bitmap_sychro(dev_display_t *dsp, uint8_t addr, render_cfg_t ctx, bool pers); diff --git a/Application/Inc/app_test.h b/Application/Inc/app_test.h index 012c002..f4e3d4d 100644 --- a/Application/Inc/app_test.h +++ b/Application/Inc/app_test.h @@ -8,6 +8,18 @@ #pragma once +#include + +#include "dev_display.h" + +#define APP_TEST_OBLIQUE_LINE_SPACING (32U) +#define APP_TEST_OBLIQUE_STEP_DELAY_MS (24U) + +extern volatile uint32_t g_app_test_oblique_frame_count; + +/** @brief 绘制指定相位的一帧斜扫图案 */ +void app_test_oblique_scan_frame(dev_display_t *dsp, int16_t phase); + /** @brief 运行全部测试用例 */ void app_test_run(void); @@ -22,3 +34,15 @@ void app_test_io_output(void); /** @brief LED 灯序测试:逐通道逐像素点亮 hub75_buff,用于确认物理灯序映射 */ void app_test_led_mapping(void); + +/** @brief 扫描行顺序测试:逐行点亮水平线,确认 1/8 扫描的行地址映射 */ +void app_test_scan_line_order(void); + +/** @brief 对角线测试:验证整体坐标映射是否正确 */ +void app_test_diagonal(void); + +/** @brief 斜扫测试:显示多条白色斜线连续移动 */ +void app_test_oblique_scan(void); + +/** @brief prepare 映射测试:通过 set_pixel 逐像素点亮,验证 prepare 像素重排是否正确 */ +void app_test_prepare_mapping(void); diff --git a/Application/Inc/app_uart_baud.h b/Application/Inc/app_uart_baud.h new file mode 100644 index 0000000..57f02f4 --- /dev/null +++ b/Application/Inc/app_uart_baud.h @@ -0,0 +1,29 @@ +/** + * @file app_uart_baud.h + * @brief DIP1 波特率选择与运行态波特率切换(RS232 + RS485 同步) + * + * DIP1(PE7,active_low):ON=115200、OFF=9600,同时作用于 RS232(USART3)与 + * RS485(USART1)两路 UART。上电由 app_uart_baud_init(sw_app_initcall)在通道 + * 任务启动(app_rs232_start / app_rs485_start)之前应用; + * 运行态亦可被 MTC 7B 40 修改波特率命令调用 app_uart_baud_apply 切换。 + */ +#pragma once + +#include + +#define APP_UART_BAUD_OFF (9600U) /**< DIP1 OFF 波特率 */ +#define APP_UART_BAUD_ON (115200U)/**< DIP1 ON 波特率 */ + +/** + * @brief 获取当前生效的波特率。 + * @return 当前波特率(9600 / 115200)。 + */ +uint32_t app_uart_baud_get(void); + +/** + * @brief 同步切换 RS232 与 RS485 两路 UART 波特率。 + * @param baud 目标波特率(仅支持 9600 / 115200)。 + * @return 0=成功;-1=参数非法或任一路切换失败。 + * @note DMA 空闲接收已启动时(运行态切换)内部会先停 DMA、DeInit/Init 后重挂 RX。 + */ +int32_t app_uart_baud_apply(uint32_t baud); \ No newline at end of file diff --git a/Application/Inc/protocol_select.h b/Application/Inc/protocol_select.h new file mode 100644 index 0000000..ad3986e --- /dev/null +++ b/Application/Inc/protocol_select.h @@ -0,0 +1,18 @@ +/** + * @file protocol_select.h + * @brief [已废弃] 旧协议切换头文件 + * + * @deprecated 本文件已被废弃。协议切换方式已改为: + * 1. EIDE 工程目录包含/排除协议模块文件夹(如 LDI/、ProtocolParser_QingHai/) + * 2. 协议模块通过 sw_app_initcall 自注册到 app_dispatch 框架 + * 3. 框架零分支、零宏切换 + * + * @see doc/05_协议模块多协议兼容优化/01_architecture.md — 协议模块组织与工程编入 + * @see doc/05_协议模块多协议兼容优化/02_as_built_status.md — 落地状态 + * + * 请勿在新代码中 include 本文件。如仍有引用请逐步清理。 + */ + +#pragma once + +#warning "protocol_select.h 已废弃:请使用 EIDE 目录编入方式切换协议,勿 include 本文件" diff --git a/Application/Src/AH_MQTT/ah_mqtt.c b/Application/Src/AH_MQTT/ah_mqtt.c index 16342b6..44e8ab7 100644 --- a/Application/Src/AH_MQTT/ah_mqtt.c +++ b/Application/Src/AH_MQTT/ah_mqtt.c @@ -5,6 +5,10 @@ #include "dev_display.h" #include "app_mqtt.h" #include "initcall.h" +#include "app_dispatch.h" + +/* RJ45 物理通道 RB:与 IAP/LDI 等同槽 weak 合并(启用 MQTT 时才占体) */ +RB_PROVIDE_WEAK(rb_provide_rj45, RB_SIZE_RJ45); static proto_mask_t s_ah_mqtt_mask; @@ -39,7 +43,7 @@ notify_id_t xNotifyID = { osThreadId_t g_ah_mqtt_task_handle; const osThreadAttr_t ProtocolTask_attributes = { .name = "ah_mqtt_handle_task", - .stack_size = 512 * 4, + .stack_size = 256 * 4, /* 与其它协议任务对齐;启用前仍须测水位 */ .priority = (osPriority_t)osPriorityNormal, }; @@ -63,11 +67,14 @@ static uint8_t handle_topic(const char topic[]); void ah_mqtt_handle_task(void *argument) { + (void)argument; #define AH_MQTT_PAYLOAD_MAX (533U) /* MQTT_FRAME_MAX_LEN */ - static uint8_t _msg_buf[sizeof(frame_msg_t) + AH_MQTT_PAYLOAD_MAX]; +#define AH_MQTT_MSG_SIZE (sizeof(frame_msg_t) + AH_MQTT_PAYLOAD_MAX) +#define AH_MQTT_QUEUE_DEPTH (3U) + static uint8_t _msg_buf[AH_MQTT_MSG_SIZE]; frame_msg_t *msg = (frame_msg_t *)_msg_buf; static StaticQueue_t s_ah_mqtt_queue_cb; - static uint8_t s_ah_mqtt_queue_buf[sizeof(frame_msg_t) + AH_MQTT_PAYLOAD_MAX]; + static uint8_t s_ah_mqtt_queue_buf[AH_MQTT_QUEUE_DEPTH * AH_MQTT_MSG_SIZE]; static const osMessageQueueAttr_t s_ah_mqtt_queue_attr = { .name = "g_proto_ah_matt_queue", .cb_mem = &s_ah_mqtt_queue_cb, @@ -75,7 +82,7 @@ void ah_mqtt_handle_task(void *argument) .mq_mem = s_ah_mqtt_queue_buf, .mq_size = sizeof(s_ah_mqtt_queue_buf), }; - g_proto_ah_matt_queue = osMessageQueueNew(1, sizeof(frame_msg_t) + AH_MQTT_PAYLOAD_MAX, &s_ah_mqtt_queue_attr); + g_proto_ah_matt_queue = osMessageQueueNew(AH_MQTT_QUEUE_DEPTH, AH_MQTT_MSG_SIZE, &s_ah_mqtt_queue_attr); app_proto_set_frame_queue(s_ah_mqtt_mask, g_proto_ah_matt_queue); while (g_mqtt.state != MQTT_ST_READY) { @@ -119,15 +126,19 @@ uint8_t handle_topic(const char topic[]) proto_probe_sta_t ah_mqtt_probe_frame(const channel_t *ch, const ring_buffer_t *buff, uint32_t *payload_len, uint8_t *cmd_num) { - (void)buff; - *payload_len = container_of(ch, mqtt_channel_t, me)->payload_len; - *cmd_num = handle_topic(container_of(ch, mqtt_channel_t, me)->topic); + /* MQTT 通道在 PUBLISH 完成后已将完整 payload 写入本通道 RB;按 RB 契约探测 */ + uint32_t avail = rb_avail(buff, nullptr); + if (avail == 0) + return PROTO_PROBE_FAKE; + *payload_len = avail; + *cmd_num = handle_topic(container_of(ch, mqtt_channel_t, me)->topic); return PROTO_PROBE_READY; } void ReportTask(void *argument) { + (void)argument; static char topic[64] = {0}; snprintf(topic, 64, "%.8s/%.2s/%.2s/%.2s/Push/monitor/devstatus", topic_info.station_hex, @@ -150,6 +161,7 @@ void ReportTask(void *argument) void SignUpTask(void *argument) { + (void)argument; static char topic[64] = {0}; snprintf(topic, 64, "%.8s/%.2s/%.2s/%.2s/Push/monitor/sign", topic_info.station_hex, @@ -181,13 +193,17 @@ void SignUpTask(void *argument) /* ---- 自注册到 app_dispatch ---- */ [[maybe_unused]] static void ah_mqtt_module_init(void) { - ring_buffer_t *rb = app_proto_acquire_buf(1, 2048); + /* MQTT 走 RJ45 共享槽;probe 须首字节快拒后方可与 IAP/LDI 同链 */ + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RJ45, RB_SIZE_RJ45); + if (rb == nullptr) + return; + s_ah_mqtt_mask = app_proto_register(ah_mqtt_probe_frame, rb); - if (s_ah_mqtt_mask == 0) return; + if (s_ah_mqtt_mask == 0) + return; app_proto_bind_channel(s_ah_mqtt_mask, CH_ID_MQTT); - // 创建协议处理任务 g_ah_mqtt_task_handle = osThreadNew(ah_mqtt_handle_task, NULL, &ProtocolTask_attributes); } -// sw_app_initcall(ah_mqtt_module_init); +// sw_app_initcall(ah_mqtt_module_init); /* 产品未启用 MQTT 时保持注释 */ diff --git a/Application/Src/AH_MQTT/ah_mqtt_cmd.c b/Application/Src/AH_MQTT/ah_mqtt_cmd.c index 16433eb..db4f6bb 100644 --- a/Application/Src/AH_MQTT/ah_mqtt_cmd.c +++ b/Application/Src/AH_MQTT/ah_mqtt_cmd.c @@ -2,7 +2,7 @@ #include "app_mqtt.h" #include "ah_mqtt.h" -#include "text_cvt.h" +#include "app_render.h" #include "app_render.h" #include "dev_display.h" @@ -29,10 +29,12 @@ const ah_mqtt_cmd_handler_fn_t g_ah_mqtt_cmd_table[] = { */ static void cmd_default(channel_t *ch, char *buff) { + (void)ch; + (void)buff; } -const static display_color_t disp_color[] = {COLOR_BLACK, COLOR_GREEN, COLOR_RED, COLOR_YELLOW}; -const static font_size_t disp_size[] = {0, 0, FONT_16, FONT_24, FONT_32, 0, 0, 0, FONT_32}; +static const display_color_t disp_color[] = {COLOR_BLACK, COLOR_GREEN, COLOR_RED, COLOR_YELLOW}; +static const font_size_t disp_size[] = {0, 0, FONT_16, FONT_24, FONT_32, 0, 0, 0, FONT_32}; /** * @brief 显示文字 * @@ -41,17 +43,18 @@ const static font_size_t disp_size[] = {0, 0, FONT_16, FONT_24, FONT_32, 0, 0 */ static void cmd_display(channel_t *ch, char *buff) { - uint32_t length = strlen(buff), gbk_len; + uint32_t length = strlen(buff); cmd_display_t *para = (cmd_display_t *)buff; - char gbk_txt[128] = {0}; - UTF8ToGBK(para->text, length - sizeof(cmd_display_t), gbk_txt, &gbk_len); + /* AH 平台文本为 UTF-8:直接以 FONT_ENC_UTF8 交给 app_render + * (渲染引擎内部自行 UTF8ToGBK 取字模)。勿在此预转 GBK—— + * GBK 字节再被 FONT_ENC_UTF8 路径二次解析会双重转换乱码。 */ app_render(&(render_cfg_t){ .type = RENDER_TEXT, .x = 0, .y = 0, .w = dev_display_get()->screen_rows, .h = dev_display_get()->screen_cols, .color = disp_color[(uint8_t)(para->color) - 0x30], - .text = gbk_txt, .len = (uint16_t)gbk_len, + .text = para->text, .len = (uint16_t)(length - sizeof(cmd_display_t)), .font_size = disp_size[(uint8_t)(para->size) - 0x30], .font_type = FONT_ST, .text_enc = FONT_ENC_UTF8, @@ -97,6 +100,7 @@ static void cmd_fill(channel_t *ch, char *buff) */ static void cmd_restart(channel_t *ch, char *buff) { + (void)buff; snprintf(container_of(ch, mqtt_channel_t, me)->topic, 64, "%.8s/%.2s/%.2s/%.2s/Reply/op/restart", topic_info.station_hex, topic_info.lane_hex, diff --git a/Application/Src/Channel/app_mqtt.c b/Application/Src/Channel/app_mqtt.c index 8308ec3..1edc12a 100644 --- a/Application/Src/Channel/app_mqtt.c +++ b/Application/Src/Channel/app_mqtt.c @@ -40,7 +40,7 @@ mqtt_channel_t g_mqtt = { osThreadId_t mqtt_task_handle; const osThreadAttr_t mqtt_task_attr = { .name = "mqtt_task", - .stack_size = 512 * 4, + .stack_size = 256 * 4, /* 对齐其它网络通道;启用前须测水位 */ .priority = osPriorityNormal, }; @@ -141,6 +141,7 @@ void mqtt_connection(void) void mqtt_task(void *argument) { + (void)argument; mqtt_ctx_t *ctx = &g_mqtt.ctx; ctx->client = mqtt_client_new(); diff --git a/Application/Src/Channel/app_rs232.c b/Application/Src/Channel/app_rs232.c index 07b5ab3..e4ceae1 100644 --- a/Application/Src/Channel/app_rs232.c +++ b/Application/Src/Channel/app_rs232.c @@ -1,122 +1,132 @@ /** * @file app_rs232.c - * @brief RS232 通道(USART3=RS232-0, USART6=RS232-1) + * @brief RS232 通道(USART3=RS232-0 协议通道, USART6=RS232-1 语音专用 TX) * - * 板级资源(DMA 缓冲区)由 Device 层 dev_rs232 提供, - * 通道生命周期和收发任务循环全部在 Application 层实现。 + * RS232-0(USART3):完整 RX+TX 协议通道,参与多协议调度。 + * RS232-1(USART6):仅提供 TX 能力给语音板 TTS,**不注册为协议通道**, + * 不调用 app_channel_dispatch / app_channel_register。 + * + * RX 路径:pl_uart circular 乒乓(2×640B)经 HT/TC/IDLE 中断增量通知, + * ISR 回调把「块号 + 偏移 + 长度」消息放入 rx_queue(深度 2), + * 任务按消息从对应块拷贝到 RB(app_channel_dispatch)。 + * + * @note 架构约束(doc/05_…/01_architecture.md §2.3 / §5): + * PL_UART6 / CH_ID_RS232_1 仅用于语音板 TTS 发送; + * 地区协议禁止绑定 CH_ID_RS232_1。 */ #include "app_rs232.h" #include "FreeRTOS.h" -#include "pl_uart.h" -#include "dev_rs232.h" #include "app_dispatch.h" +#include "dev_rs232.h" +#include "pl_uart.h" typedef struct { - channel_t me; - pl_uart_handle_t uart; - osMessageQueueId_t rx_queue; - uint8_t *rx_buf; - uint16_t rx_buf_size; + channel_t me; + pl_uart_handle_t uart; + osMessageQueueId_t rx_queue; + uint8_t *rx_buf; + uint16_t rx_buf_size; /* 总长 = 2 × block(circular 周期) */ + uint16_t rx_block_size; /* 半块 = RS232_BUF_SIZE */ } rs232_ch_t; -#define RS232_BUF_SIZE (2048U) - -/* ---- rs232_rx_queue 静态分配 ---- */ +/* ---- RS232-0 (USART3) RX queue 静态分配(深度 2:乒乓双块各一消息在途) ---- */ static StaticQueue_t s_rs232_0_rx_cb; -static uint16_t s_rs232_0_rx_buf[1]; +static pl_uart_rx_msg_t s_rs232_0_rx_buf[2]; static const osMessageQueueAttr_t s_rs232_0_rx_attr = { - .name = "rs232_0_rx", - .cb_mem = &s_rs232_0_rx_cb, + .name = "rs232_0_rx", + .cb_mem = &s_rs232_0_rx_cb, .cb_size = sizeof(s_rs232_0_rx_cb), - .mq_mem = s_rs232_0_rx_buf, + .mq_mem = s_rs232_0_rx_buf, .mq_size = sizeof(s_rs232_0_rx_buf), }; -static StaticQueue_t s_rs232_1_rx_cb; -static uint16_t s_rs232_1_rx_buf[1]; -static const osMessageQueueAttr_t s_rs232_1_rx_attr = { - .name = "rs232_1_rx", - .cb_mem = &s_rs232_1_rx_cb, - .cb_size = sizeof(s_rs232_1_rx_cb), - .mq_mem = s_rs232_1_rx_buf, - .mq_size = sizeof(s_rs232_1_rx_buf), -}; - -/* ---- rs232 task attr ---- */ +/* ---- RS232 task attr ---- */ static const osThreadAttr_t s_rs232_0_attr = { - .name = "rs232_0_task", + .name = "rs232_0_task", .stack_size = 256 * 4, - .priority = osPriorityNormal, -}; -static const osThreadAttr_t s_rs232_1_attr = { - .name = "rs232_1_task", - .stack_size = 256 * 4, - .priority = osPriorityNormal, + .priority = osPriorityNormal, }; -/* ---- 实例 ---- */ +/* ---- RS232-0 实例(协议通道) ---- */ static rs232_ch_t g_rs232_0 = {.me = {.ch_id = CH_ID_RS232}}; -static rs232_ch_t g_rs232_1 = {.me = {.ch_id = CH_ID_RS232_1}}; -/* ---- ops ---- */ -static int32_t rs232_send(channel_t *ch, const uint8_t *data, uint16_t len) -{ - rs232_ch_t *self = container_of(ch, rs232_ch_t, me); - return pl_uart_send(self->uart, data, len, 100); +/* ---- ops(仅 RS232-0 使用) ---- */ +static int32_t rs232_send(channel_t *ch, const uint8_t *data, uint16_t len) { + rs232_ch_t *self = container_of(ch, rs232_ch_t, me); + return pl_uart_send(self->uart, data, len, 100); } static const ch_ops_t rs232_ops = {.send = rs232_send}; /* ---- ISR → 任务通知 ---- */ -static void rs232_isr_cb(uint8_t *data, uint16_t len, void *ctx) -{ - (void)data; - rs232_ch_t *self = (rs232_ch_t *)ctx; - osMessageQueuePut(self->rx_queue, &len, 0, 0); +static void rs232_isr_cb(uint8_t *data, uint16_t len, void *ctx) { + rs232_ch_t *self = (rs232_ch_t *)ctx; + uint32_t off = (uint32_t)(data - self->rx_buf); + + /* 段指针换算为「块号 + 块内偏移」;len 由 pl_uart 保证 ≤ block */ + pl_uart_rx_msg_t m = { + .block = (uint8_t)(off / self->rx_block_size), + .offset = (uint16_t)(off % self->rx_block_size), + .len = len, + }; + osMessageQueuePut(self->rx_queue, &m, 0, 0); } -/* ---- 任务循环 ---- */ -static void rs232_task(void *argument) -{ - rs232_ch_t *self = (rs232_ch_t *)argument; - - self->rx_queue = osMessageQueueNew(1, sizeof(uint16_t), self->me.ch_id == CH_ID_RS232 ? &s_rs232_0_rx_attr : &s_rs232_1_rx_attr); - if (self->rx_queue == NULL) { - osThreadExit(); - return; - } - app_channel_register(self->me.ch_id, &self->me); - - pl_uart_set_rx_cb(self->uart, rs232_isr_cb, self); - pl_uart_start_rx(self->uart, self->rx_buf, self->rx_buf_size); - - for (;;) { - uint16_t rx_len = 0; - osMessageQueueGet(self->rx_queue, &rx_len, 0, osWaitForever); - app_channel_dispatch(&self->me, self->rx_buf, rx_len); +/* ---- RS232-0 任务循环(完整协议通道) ---- */ +static void rs232_task(void *argument) { + rs232_ch_t *self = (rs232_ch_t *)argument; + + self->rx_queue = osMessageQueueNew( + 2, sizeof(pl_uart_rx_msg_t), &s_rs232_0_rx_attr); + if (self->rx_queue == NULL) { + osThreadExit(); + return; + } + app_channel_register(self->me.ch_id, &self->me); + + pl_uart_set_rx_cb(self->uart, rs232_isr_cb, self); + pl_uart_start_rx(self->uart, self->rx_buf, self->rx_buf_size); + + for (;;) { + pl_uart_rx_msg_t m; + if (osMessageQueueGet(self->rx_queue, &m, 0, osWaitForever) == osOK) { + /* 按块号从对应缓冲拷贝 len 字节入调度框架 */ + uint8_t *src = self->rx_buf + (uint16_t)m.block * self->rx_block_size + m.offset; + app_channel_dispatch(&self->me, src, m.len); } + } } /* ---- 公开 API ---- */ -osThreadId_t app_rs232_start(void) -{ - rs232_ch_t *self = &g_rs232_0; - self->me.ops = &rs232_ops; - self->uart = pl_uart_get_handle(PL_UART3); - self->rx_buf = dev_rs232_get_buf(0); - self->rx_buf_size = RS232_BUF_SIZE; - return osThreadNew(rs232_task, self, &s_rs232_0_attr); -} - -osThreadId_t app_rs232_1_start(void) -{ - rs232_ch_t *self = &g_rs232_1; - self->me.ops = &rs232_ops; - self->uart = pl_uart_get_handle(PL_UART6); - self->rx_buf = dev_rs232_get_buf(1); - self->rx_buf_size = RS232_BUF_SIZE; - return osThreadNew(rs232_task, self, &s_rs232_1_attr); +/** + * @brief 启动 RS232-0 协议通道(USART3, CH_ID_RS232) + * @return 通道任务句柄 + */ +osThreadId_t app_rs232_start(void) { + rs232_ch_t *self = &g_rs232_0; + self->me.ops = &rs232_ops; + self->uart = pl_uart_get_handle(PL_UART3); + self->rx_buf = dev_rs232_get_buf(0); + self->rx_block_size = RS232_BUF_SIZE; + self->rx_buf_size = 2U * RS232_BUF_SIZE; /* 乒乓双块:640→1280B */ + return osThreadNew(rs232_task, self, &s_rs232_0_attr); } + +/** + * @brief RS232-1 启动桩(USART6 语音板专用,纯 TX) + * + * @note 按架构约束(doc/05_…/01_architecture.md §2.3 / §5), + * USART6 / CH_ID_RS232_1 仅用于语音板 TTS 发送(PL_UART6), + * 不注册为协议通道,不创建 RX 任务,不参与多协议调度。 + * 语音 TX 由 dev_rs232_voice 层直接调用 pl_uart_send(PL_UART6) 完成。 + * + * @return 始终返回 nullptr(无通道任务) + */ +osThreadId_t app_rs232_1_start(void) { + /* 语音板 TX 路径:dev_rs232_voice → pl_uart_send(PL_UART6), + * 不经过 dispatch 框架,无需创建通道任务或注册 CH_ID_RS232_1。 */ + return nullptr; +} \ No newline at end of file diff --git a/Application/Src/Channel/app_rs485.c b/Application/Src/Channel/app_rs485.c index 30e5f73..4d711c9 100644 --- a/Application/Src/Channel/app_rs485.c +++ b/Application/Src/Channel/app_rs485.c @@ -2,8 +2,12 @@ * @file app_rs485.c * @brief RS485 半双工通道(USART1, RE 方向由 dev_rs485 注入) * - * 板级资源(DMA 缓冲区、RE 方向控制)由 Device 层 dev_rs485 提供, + * 板级资源(DMA 乒乓双缓冲、RE 方向控制)由 Device 层 dev_rs485 提供, * 通道生命周期和收发任务循环全部在 Application 层实现。 + * + * RX 路径:pl_uart circular 乒乓(2×640B)经 HT/TC/IDLE 中断增量通知, + * ISR 回调把「块号 + 偏移 + 长度」消息放入 rx_queue(深度 2), + * 任务按消息从对应块拷贝到 RB(app_channel_dispatch)。 */ #include "app_rs485.h" @@ -18,14 +22,13 @@ typedef struct { pl_uart_handle_t uart; osMessageQueueId_t rx_queue; uint8_t *rx_buf; - uint16_t rx_buf_size; + uint16_t rx_buf_size; /* 总长 = 2 × block(circular 周期) */ + uint16_t rx_block_size; /* 半块 = RS485_BUF_SIZE */ } rs485_ch_t; -#define RS485_BUF_SIZE (2048U) - -/* ---- rs485_rx_queue 静态分配 ---- */ +/* ---- rs485_rx_queue 静态分配(深度 2:乒乓双块各一消息在途) ---- */ static StaticQueue_t s_rs485_rx_cb; -static uint16_t s_rs485_rx_buf[1]; +static pl_uart_rx_msg_t s_rs485_rx_buf[2]; static const osMessageQueueAttr_t s_rs485_rx_attr = { .name = "rs485_rx", .cb_mem = &s_rs485_rx_cb, @@ -49,9 +52,16 @@ static const ch_ops_t rs485_ops = {.send = rs485_send}; /* ---- ISR → 任务通知 ---- */ static void rs485_isr_cb(uint8_t *data, uint16_t len, void *ctx) { - (void)data; rs485_ch_t *self = (rs485_ch_t *)ctx; - osMessageQueuePut(self->rx_queue, &len, 0, 0); + uint32_t off = (uint32_t)(data - self->rx_buf); + + /* 段指针换算为「块号 + 块内偏移」;len 由 pl_uart 保证 ≤ block */ + pl_uart_rx_msg_t m = { + .block = (uint8_t)(off / self->rx_block_size), + .offset = (uint16_t)(off % self->rx_block_size), + .len = len, + }; + osMessageQueuePut(self->rx_queue, &m, 0, 0); } /* ---- 任务循环 ---- */ @@ -59,7 +69,7 @@ static void rs485_task(void *argument) { rs485_ch_t *self = (rs485_ch_t *)argument; - self->rx_queue = osMessageQueueNew(1, sizeof(uint16_t), &s_rs485_rx_attr); + self->rx_queue = osMessageQueueNew(2, sizeof(pl_uart_rx_msg_t), &s_rs485_rx_attr); if (self->rx_queue == NULL) { osThreadExit(); return; @@ -70,9 +80,11 @@ static void rs485_task(void *argument) pl_uart_start_rx(self->uart, self->rx_buf, self->rx_buf_size); for (;;) { - uint16_t rx_len = 0; - if (osMessageQueueGet(self->rx_queue, &rx_len, 0, osWaitForever) == osOK) { - app_channel_dispatch(&self->me, self->rx_buf, rx_len); + pl_uart_rx_msg_t m; + if (osMessageQueueGet(self->rx_queue, &m, 0, osWaitForever) == osOK) { + /* 按块号从对应缓冲拷贝 len 字节入调度框架 */ + uint8_t *src = self->rx_buf + (uint16_t)m.block * self->rx_block_size + m.offset; + app_channel_dispatch(&self->me, src, m.len); } } } @@ -85,7 +97,8 @@ osThreadId_t app_rs485_start(void) self->me.ops = &rs485_ops; self->uart = pl_uart_get_handle(PL_UART1); self->rx_buf = dev_rs485_get_buf(); - self->rx_buf_size = RS485_BUF_SIZE; + self->rx_block_size = RS485_BUF_SIZE; + self->rx_buf_size = 2U * RS485_BUF_SIZE; /* 乒乓双块:640→1280B */ osThreadAttr_t rs485_task_attr = { .name = "rs485_task", @@ -93,4 +106,4 @@ osThreadId_t app_rs485_start(void) .priority = osPriorityNormal, }; return osThreadNew(rs485_task, self, &rs485_task_attr); -} +} \ No newline at end of file diff --git a/Application/Src/Channel/app_tcp_client.c b/Application/Src/Channel/app_tcp_client.c index 8f56bff..a69c9d3 100644 --- a/Application/Src/Channel/app_tcp_client.c +++ b/Application/Src/Channel/app_tcp_client.c @@ -1,6 +1,6 @@ /** * @file dev_tcp_client.c - * @brief TCP 客户端通道(连接 192.168.114.100:9529) + * @brief TCP 客户端通道(连接 192.168.2.17:9529) */ #include "app_tcp_client.h" @@ -15,16 +15,16 @@ void tcp_client_conn_task(void *argument); __STATIC_INLINE void tcp_keepaliveinit(struct netconn *conn); /* ---- 连接任务属性 ---- */ -const osThreadAttr_t tcp_client_conn_attr = { +static const osThreadAttr_t tcp_client_conn_attr = { .name = "tcp_client_conn_task", - .stack_size = 256 * 4, + .stack_size = 256 * 4, /* 对齐 udp_connect;原 2KB 偏大 */ .priority = osPriorityNormal, }; osThreadId_t tcp_client_task_handle; const osThreadAttr_t tcp_client_task_attr = { .name = "tcp_client_task", - .stack_size = 512 * 4, + .stack_size = 256 * 4, /* 对齐 tcp_server;原 2KB 偏大 */ .priority = osPriorityNormal, }; @@ -40,6 +40,12 @@ tcp_client_channel_t g_tcp_client = { .host_port = 9529, }; +/* ---- 连接期通道实例:文件级 static 单实例(UAF 根治) ---- + * 连接任务退出后栈上通道实例悬垂是历史 UAF 根源。实例提为文件级 static, + * ch 指针生命周期恒定;重连时复用同一实例(init 重新拷贝模板重置状态)。 + * 已注销的旧通知由 dispatch 侧 app_channel_get 回验丢弃(app_dispatch.c)。 */ +static tcp_client_channel_t s_tcp_client_conn_ch; + /* ---- 构造 ---- */ void tcp_client_channel_init(tcp_client_channel_t *self, void *conn, channel_t *tmpl) { @@ -75,6 +81,7 @@ uint16_t app_tcp_client_get_host_port(void) void tcp_client_task(void *argument) { + (void)argument; if (client_disconnect_sem == NULL) client_disconnect_sem = osSemaphoreNew(1, 0, NULL); @@ -132,10 +139,9 @@ void tcp_client_conn_task(void *argument) { struct netconn *conn = (struct netconn *)argument; - tcp_client_channel_t tcp; - tcp_client_channel_init(&tcp, conn, &g_tcp_client_channel_tmpl); + tcp_client_channel_init(&s_tcp_client_conn_ch, conn, &g_tcp_client_channel_tmpl); - channel_t *ch = &tcp.me; + channel_t *ch = &s_tcp_client_conn_ch.me; struct netbuf *buf; err_t err; void *data; @@ -150,8 +156,8 @@ void tcp_client_conn_task(void *argument) netbuf_delete(buf); } - tcp.me.ops = nullptr; /* 防止 send 路径访问即将释放的 netconn */ - tcp.me.state = CH_STATE_DOWN; + s_tcp_client_conn_ch.me.ops = nullptr; /* 防止 send 路径访问即将释放的 netconn */ + s_tcp_client_conn_ch.me.state = CH_STATE_DOWN; app_channel_register(CH_ID_TCP_CLIENT, nullptr); osSemaphoreRelease(client_disconnect_sem); osThreadExit(); diff --git a/Application/Src/Channel/app_tcp_server.c b/Application/Src/Channel/app_tcp_server.c index c345722..b636bba 100644 --- a/Application/Src/Channel/app_tcp_server.c +++ b/Application/Src/Channel/app_tcp_server.c @@ -1,29 +1,23 @@ /** * @file app_tcp_server.c - * @brief TCP 服务器通道 — manage 任务 + conn 任务 + * @brief TCP 服务器通道 — 串行单客户端服务循环 * - * 参照 tcp_client 模式: - * tcp_server_task bind→listen→accept→派生conn→等待断开→循环 - * tcp_server_conn_task netconn_recv→dispatch,断开时释放信号量 + * 单连接任务循环:bind → listen → accept → 服务该客户端(recv 循环) + * → 客户端断开 → 回 accept 下一客户端。 + * + * UAF 根治:通道实例为文件级 static 单实例,生命周期内注册一次、 + * 注销仅在服务循环退出时;ch 指针恒定,dispatch 侧 app_channel_get + * 回验兜底。原「每客户端一 conn 任务」的并发派生模式已移除。 */ #include "app_tcp_server.h" +#include "FreeRTOS.h" #include "app_dispatch.h" #include "pl_net_adapt.h" #define TCP_SERVER_PORT 9528 -/* ---- 信号量 ---- */ -static osSemaphoreId_t s_disconnect_sem; - -/* ---- 连接任务属性 ---- */ -const osThreadAttr_t tcp_server_conn_attr = { - .name = "tcp_svr_conn", - .stack_size = 256 * 4, - .priority = osPriorityNormal, -}; - /* ---- TCP 通道虚表:send = netconn_write ---- */ static int32_t tcp_send(channel_t *ch, const uint8_t *data, uint16_t len) { @@ -34,12 +28,17 @@ static int32_t tcp_send(channel_t *ch, const uint8_t *data, uint16_t len) const ch_ops_t tcp_ch_ops = {.send = tcp_send}; -/* ---- 通道元数据模板(每连接 copy) ---- */ +/* ---- 通道元数据模板 ---- */ channel_t g_tcp_server_channel_tmpl = { .ch_id = CH_ID_TCP_SERVER, .ops = &tcp_ch_ops, }; +/* ---- 文件级 static 单实例通道(UAF 根治): + * 每客户端不再新建实例/任务,ch 指针生命周期恒定; + * 已注销的旧通知由 dispatch 侧 app_channel_get 回验丢弃。 */ +static tcp_server_channel_t s_tcp_ch; + /* ---- 配置接口 ---- */ static uint16_t g_port = TCP_SERVER_PORT; @@ -67,15 +66,20 @@ static void tcp_channel_init(tcp_server_channel_t *self, void *conn, channel_t * app_channel_register(CH_ID_TCP_SERVER, &self->me); } +static void tcp_channel_deinit(tcp_server_channel_t *self) +{ + self->me.ops = nullptr; /* 防止 send 路径访问即将释放的 netconn */ + self->me.state = CH_STATE_DOWN; + app_channel_register(CH_ID_TCP_SERVER, nullptr); +} + /* ================================================================ - * manage 任务: bind → listen → accept → 派生 conn → 等待断开 → 循环 + * 服务任务: bind → listen → accept → 服务客户端 → 断开 → 回 accept * ================================================================ */ void tcp_server_task(void *argument) { (void)argument; - if (s_disconnect_sem == NULL) - s_disconnect_sem = osSemaphoreNew(1, 0, NULL); for (;;) { struct netconn *conn = netconn_new(NETCONN_TCP); @@ -90,67 +94,43 @@ void tcp_server_task(void *argument) osDelay(500); continue; } - netconn_listen(conn); - /* accept 一个连接 */ - struct netconn *newconn = NULL; - err = netconn_accept(conn, &newconn); - netconn_delete(conn); - - if (err != ERR_OK || newconn == NULL) { - osDelay(500); - continue; - } - - /* 派生 conn 任务 */ - while (osSemaphoreAcquire(s_disconnect_sem, 0) == osOK); - osThreadId_t tid = osThreadNew(tcp_server_conn_task, newconn, &tcp_server_conn_attr); - - if (tid != NULL) { - osSemaphoreAcquire(s_disconnect_sem, osWaitForever); - - } else { + /* 串行单客户端服务循环:同一 listener 反复 accept */ + for (;;) { + struct netconn *newconn = NULL; + err = netconn_accept(conn, &newconn); + if (err != ERR_OK || newconn == NULL) { + /* 监听异常 → 重建 listener */ + osDelay(500); + break; + } + + /* 服务该客户端直至断开 */ + tcp_channel_init(&s_tcp_ch, newconn, &g_tcp_server_channel_tmpl); + g_tcp_server_connected = 1; + + channel_t *ch = &s_tcp_ch.me; + struct netbuf *buf; + void *data; + uint16_t len; + + while (netconn_recv(newconn, &buf) == ERR_OK) { + do { + netbuf_data(buf, &data, &len); + if (len > 1) + app_channel_dispatch(ch, (uint8_t *)data, len); + } while (netbuf_next(buf) >= 0); + netbuf_delete(buf); + } + + g_tcp_server_connected = 0; + tcp_channel_deinit(&s_tcp_ch); netconn_close(newconn); netconn_delete(newconn); - osDelay(500); + /* 回 accept 下一客户端 */ } - } -} -/* ================================================================ - * conn 任务: netconn_recv → dispatch,断开时释放信号量 - * ================================================================ */ - -void tcp_server_conn_task(void *argument) -{ - struct netconn *conn = (struct netconn *)argument; - - tcp_server_channel_t tcp; - tcp_channel_init(&tcp, conn, &g_tcp_server_channel_tmpl); - - channel_t *ch = &tcp.me; - g_tcp_server_connected = 1; - - struct netbuf *buf; - void *data; - uint16_t len; - - while (netconn_recv(conn, &buf) == ERR_OK) { - do { - netbuf_data(buf, &data, &len); - if (len > 1) - app_channel_dispatch(ch, (uint8_t *)data, len); - } while (netbuf_next(buf) >= 0); - netbuf_delete(buf); + netconn_delete(conn); } - - g_tcp_server_connected = 0; - tcp.me.ops = nullptr; - tcp.me.state = CH_STATE_DOWN; - app_channel_register(CH_ID_TCP_SERVER, nullptr); - netconn_close(conn); - netconn_delete(conn); - osSemaphoreRelease(s_disconnect_sem); - osThreadExit(); -} +} \ No newline at end of file diff --git a/Application/Src/Channel/app_udp.c b/Application/Src/Channel/app_udp.c index bbe5fd4..6b2fef3 100644 --- a/Application/Src/Channel/app_udp.c +++ b/Application/Src/Channel/app_udp.c @@ -1,44 +1,28 @@ /** - * @file dev_udp.c - * @brief UDP 广播接收通道(监听端口 10011) + * @file app_udp.c + * @brief UDP 广播接收通道(端口固定 10011,宏 LDI_DISCOVERY_PORT) + * + * 同端口承载两类协议(由 app_dispatch 按帧头分流): + * - 创迪发现口:LDI 21H/12H 搜索 + * - IAP 升级:0x5A5A5A5A 帧 */ #include "app_udp.h" #include "app_dispatch.h" +#include "app_ldi.h" +#include "app_board_net_cfg.h" #include "pl_net.h" #include "pl_net_adapt.h" /* ---- 配置 ---- */ -static uint16_t g_udp_port = 10011; /**< IAP 升级通道 */ - -void app_udp_set_port(uint16_t port) -{ - g_udp_port = port; -} +/* 端口固定(宏 LDI_DISCOVERY_PORT,创迪发现口 + IAP 共用),设计原则禁止修改接口 */ +static uint16_t g_udp_port = LDI_DISCOVERY_PORT; uint16_t app_udp_get_port(void) { return g_udp_port; } -void app_udp_broadcast(const uint8_t *data, uint16_t len) -{ - struct netconn *conn = netconn_new(NETCONN_UDP); - if (conn == NULL) return; - - ip_set_option(conn->pcb.udp, SOF_BROADCAST); - ip_addr_t bc_addr; - IP4_ADDR(&bc_addr, 255, 255, 255, 255); - - struct netbuf *nb = netbuf_new(); - if (nb) { - netbuf_ref(nb, data, len); - netconn_sendto(conn, nb, &bc_addr, g_udp_port); - netbuf_delete(nb); - } - netconn_delete(conn); -} - /* ---- 信号量资源 ---- */ static osSemaphoreId_t udp_disconnect_sem; @@ -53,12 +37,61 @@ static void udp_link_listener(bool link_up) void udp_connect_task(void *argument); /* ---- 连接任务属性 ---- */ -const osThreadAttr_t udp_connect_attr = { +static const osThreadAttr_t udp_connect_attr = { .name = "udp_connect_task", .stack_size = 256 * 4, .priority = osPriorityNormal, }; +/* ---- 文件级单实例通道:UAF 根治 ---- + * 连接任务退出后栈上通道实例悬垂是历史 UAF 根源。通道实例提为文件级 + * static,ch 指针生命周期恒定;连接任务重入时复用同一实例(init 重新 + * 拷贝模板重置状态)。已注销/未注册的旧通知由 dispatch 侧 + * app_channel_get 回验丢弃(app_dispatch.c)。 */ +static udp_channel_t s_udp_ch; + +/* ---- 广播发送(2026-08-21 修复 LDI 搜索广播丢包)---- + * 优先复用常驻通道 conn(udp_task 创建后已 ip_set_option(SOF_BROADCAST) 并 + * netconn_bind 驻留),不占 netconn 池——dev 共存构建 baseline 4 netconn + * 已满(UDP 10011 + UDP 20103 + TCP Server listener + TCP Client),旧实现 + * 每次广播临时 netconn_new 必然 NULL → 静默丢包(LDI 12H 搜索应答 50-75% + * 无响应)。常驻 conn 未就绪(断链重建窗口,deinit 已置 conn=NULL)时回退 + * 临时 conn 路径(池满时 netconn_new 仍可能 NULL → 静默,与旧行为一致)。 + * netconn API 经 tcpip 线程 mailbox 投递,任务上下文调用安全;发送与断链 + * deinit/netconn_delete 的并发窗口与 channel_send 现状一致(已知限制)。 */ +static err_t _udp_broadcast_send(struct netconn *conn, const uint8_t *data, uint16_t len, uint16_t port) +{ + ip_addr_t bc_addr; + IP4_ADDR(&bc_addr, 255, 255, 255, 255); + + struct netbuf *nb = netbuf_new(); + if (nb == NULL) + return ERR_MEM; + + netbuf_ref(nb, data, len); + err_t err = netconn_sendto(conn, nb, &bc_addr, port); + netbuf_delete(nb); + return err; +} + +void app_udp_broadcast(const uint8_t *data, uint16_t len) +{ + /* 常驻 conn 就绪(通道仍注册 + conn 非空)→ 复用常驻 conn 广播 */ + if (app_channel_get(CH_ID_UDP) == &s_udp_ch.me && s_udp_ch.conn != NULL) { + (void)_udp_broadcast_send((struct netconn *)s_udp_ch.conn, data, len, g_udp_port); + return; + } + + /* 回退:临时 conn 路径(保留 NULL 检查,池满时静默失败) */ + struct netconn *conn = netconn_new(NETCONN_UDP); + if (conn == NULL) + return; + + ip_set_option(conn->pcb.udp, SOF_BROADCAST); + (void)_udp_broadcast_send(conn, data, len, g_udp_port); + netconn_delete(conn); +} + /* ---- UDP 通道 ops (注意:不能命名为 udp_send,与 LwIP 内部符号冲突)---- */ static int32_t udp_ch_send(channel_t *ch, const uint8_t *data, uint16_t len) { @@ -99,28 +132,33 @@ const osThreadAttr_t udp_task_attr = { * ================================================================ */ void udp_task(void *argument) { + (void)argument; if (udp_disconnect_sem == NULL) udp_disconnect_sem = osSemaphoreNew(1, 0, NULL); pl_net_register_link_listener(udp_link_listener); - struct netconn *conn = netconn_new(NETCONN_UDP); - if (conn == NULL) { - osThreadExit(); - return; - } - for (;;) { + struct netconn *conn = netconn_new(NETCONN_UDP); + if (conn == NULL) { + osDelay(2000); + continue; + } + ip_set_option(conn->pcb.udp, SOF_BROADCAST); err_t err = netconn_bind(conn, IP_ADDR_ANY, g_udp_port); if (err == ERR_OK) { + /* 清空残留断开信号,避免刚起来就误退 */ while (osSemaphoreAcquire(udp_disconnect_sem, 0) == osOK); osThreadId_t tid = osThreadNew(udp_connect_task, conn, &udp_connect_attr); - if (tid != NULL) + if (tid != NULL) { + /* 链路断开 / recv 失败后由 connect_task 释放信号量 */ osSemaphoreAcquire(udp_disconnect_sem, osWaitForever); + } } + /* 每次循环新建 netconn:delete 后不可再 bind 旧句柄 */ netconn_delete(conn); osDelay(2000); } @@ -140,6 +178,7 @@ static void udp_channel_deinit(udp_channel_t *self) { self->me.ops = nullptr; self->me.state = CH_STATE_DOWN; + self->conn = NULL; /* 广播复用路径判空依据(2026-08-21) */ app_channel_register(CH_ID_UDP, nullptr); } @@ -147,10 +186,9 @@ void udp_connect_task(void *argument) { struct netconn *conn = (struct netconn *)argument; - udp_channel_t udp; - udp_channel_init(&udp, conn, &g_udp_channel_tmpl); + udp_channel_init(&s_udp_ch, conn, &g_udp_channel_tmpl); - channel_t *ch = &udp.me; + channel_t *ch = &s_udp_ch.me; struct netbuf *buf; err_t err; @@ -161,18 +199,223 @@ void udp_connect_task(void *argument) netbuf_data(buf, &data, &len); if (len > 0) { const ip_addr_t *addr = netbuf_fromaddr(buf); - udp.src_ip[0] = ip4_addr1((const ip4_addr_t *)addr); - udp.src_ip[1] = ip4_addr2((const ip4_addr_t *)addr); - udp.src_ip[2] = ip4_addr3((const ip4_addr_t *)addr); - udp.src_ip[3] = ip4_addr4((const ip4_addr_t *)addr); - udp.src_port = netbuf_fromport(buf); + s_udp_ch.src_ip[0] = ip4_addr1((const ip4_addr_t *)addr); + s_udp_ch.src_ip[1] = ip4_addr2((const ip4_addr_t *)addr); + s_udp_ch.src_ip[2] = ip4_addr3((const ip4_addr_t *)addr); + s_udp_ch.src_ip[3] = ip4_addr4((const ip4_addr_t *)addr); + s_udp_ch.src_port = netbuf_fromport(buf); app_channel_dispatch(ch, (uint8_t *)data, len); } } while (netbuf_next(buf) >= 0); netbuf_delete(buf); } - udp_channel_deinit(&udp); + udp_channel_deinit(&s_udp_ch); osSemaphoreRelease(udp_disconnect_sem); osThreadExit(); } + +/* ================================================================ + * CQ 业务口 UDP 通道(CH_ID_UDP_CQ) + * + * 完全镜像 10011 通道的 netconn 收发 / 断链重建模式。 + * 端口语义(见 _udp_cq_read_port,方案 B 2026-08-21): + * - PROTO_CHONGQING(CQ 量产构建):读 Sector1 net_cfg.udp_port(CQ UDP + * 业务口专有字段,CQ setip 写入;空扇区由 app_net_boot 写 20103 出厂 + * 默认),有效(1~65535)即用,无效/0 回退 UDP_CQ_DEFAULT_PORT(20103); + * - 其余(dev 共存构建,LDI 编入):固定 20103——net_cfg.udp_port 在 dev + * 构建无写入方(CQ setip 双构建语义见 doc/03 PartB B.7.1),固定 + * 20103 保证与 CQ 量产口径一致。 + * 业务口承载 CQ JSON 业务 + 12B 二进制重启/搜索请求。 + * ================================================================ */ + +#define UDP_CQ_DEFAULT_PORT (20103U) + +static uint16_t g_udp_cq_port = UDP_CQ_DEFAULT_PORT; + +uint16_t app_udp_cq_get_port(void) +{ + return g_udp_cq_port; +} + +/* ---- 信号量资源 ---- */ +static osSemaphoreId_t udp_cq_disconnect_sem; + +/* ---- 链路监听器 ---- */ +static void udp_cq_link_listener(bool link_up) +{ + if (!link_up && udp_cq_disconnect_sem) + osSemaphoreRelease(udp_cq_disconnect_sem); +} + +/* ---- 前向声明 ---- */ +void udp_cq_connect_task(void *argument); + +/* ---- 连接任务属性 ---- */ +static const osThreadAttr_t udp_cq_connect_attr = { + .name = "udp_cq_connect_task", + .stack_size = 256 * 4, + .priority = osPriorityNormal, +}; + +/* ---- 文件级单实例通道(UAF 根治,与 10011 通道同模式)---- */ +static udp_channel_t s_udp_cq_ch; + +/* 常驻 conn 复用广播(2026-08-21,语义同 app_udp_broadcast 见上): + * udp_cq_task 创建常驻 conn 时已设 SOF_BROADCAST;常驻未就绪回退临时 conn。 */ +void app_udp_cq_broadcast(const uint8_t *data, uint16_t len) +{ + if (app_channel_get(CH_ID_UDP_CQ) == &s_udp_cq_ch.me && s_udp_cq_ch.conn != NULL) { + (void)_udp_broadcast_send((struct netconn *)s_udp_cq_ch.conn, data, len, g_udp_cq_port); + return; + } + + struct netconn *conn = netconn_new(NETCONN_UDP); + if (conn == NULL) + return; + + ip_set_option(conn->pcb.udp, SOF_BROADCAST); + (void)_udp_broadcast_send(conn, data, len, g_udp_cq_port); + netconn_delete(conn); +} + +static int32_t udp_cq_ch_send(channel_t *ch, const uint8_t *data, uint16_t len) +{ + udp_channel_t *udp = container_of(ch, udp_channel_t, me); + struct netbuf *nb = netbuf_new(); + if (!nb) + return -1; + + netbuf_ref(nb, data, len); + struct netconn *conn = (struct netconn *)udp->conn; + ip_addr_t addr; + IP4_ADDR(&addr, udp->src_ip[0], udp->src_ip[1], udp->src_ip[2], udp->src_ip[3]); + err_t err = netconn_sendto(conn, nb, &addr, udp->src_port); + netbuf_delete(nb); + return (err == ERR_OK) ? (int32_t)len : -1; +} + +static const ch_ops_t udp_cq_ch_ops = { + .send = udp_cq_ch_send, +}; + +/* 通道元数据模板(ch_id = CH_ID_UDP_CQ) */ +static channel_t s_udp_cq_channel_tmpl = { + .ch_id = CH_ID_UDP_CQ, + .ops = &udp_cq_ch_ops, +}; + +const osThreadAttr_t udp_cq_task_attr = { + .name = "udp_cq_task", + .stack_size = 256 * 4, + .priority = osPriorityNormal, +}; + +/** @brief 确定 CQ 业务口端口(udp_cq_task 每轮重建前调用,setip 重启后生效)。 + * + * PROTO_CHONGQING(CQ 量产构建):读 Sector1 net_cfg.udp_port——CQ UDP 业务口 + * 专有字段(方案 B 2026-08-21,CQ setip 写入;空扇区由 app_net_boot 写 20103 + * 出厂默认),有效(1~65535)即用,无效/0 回退 UDP_CQ_DEFAULT_PORT(20103)。 + * + * 其余构建(dev 共存,LDI 编入):固定 UDP_CQ_DEFAULT_PORT,不读 net_cfg.udp_port + * ——dev 构建 udp_port 无写入方,读残留记录会与 CQ 量产口径不一致。 + */ +static void _udp_cq_read_port(void) +{ +#ifdef PROTO_CHONGQING + app_board_net_cfg_t cfg; + if (app_board_net_cfg_get(&cfg) == 0 && cfg.udp_port > 0U && cfg.udp_port <= 65535U) + g_udp_cq_port = (uint16_t)cfg.udp_port; + else + g_udp_cq_port = UDP_CQ_DEFAULT_PORT; +#else + g_udp_cq_port = UDP_CQ_DEFAULT_PORT; +#endif +} + +void udp_cq_task(void *argument) +{ + (void)argument; + if (udp_cq_disconnect_sem == NULL) + udp_cq_disconnect_sem = osSemaphoreNew(1, 0, NULL); + pl_net_register_link_listener(udp_cq_link_listener); + + for (;;) { + _udp_cq_read_port(); /* 每轮重建前重读端口(setip 重启后生效) */ + + struct netconn *conn = netconn_new(NETCONN_UDP); + if (conn == NULL) { + osDelay(2000); + continue; + } + + ip_set_option(conn->pcb.udp, SOF_BROADCAST); + err_t err = netconn_bind(conn, IP_ADDR_ANY, g_udp_cq_port); + + if (err == ERR_OK) { + /* 清空残留断开信号,避免刚起来就误退 */ + while (osSemaphoreAcquire(udp_cq_disconnect_sem, 0) == osOK); + + osThreadId_t tid = osThreadNew(udp_cq_connect_task, conn, &udp_cq_connect_attr); + if (tid != NULL) { + /* 链路断开 / recv 失败后由 connect_task 释放信号量 */ + osSemaphoreAcquire(udp_cq_disconnect_sem, osWaitForever); + } + } + + /* 每次循环新建 netconn:delete 后不可再 bind 旧句柄 */ + netconn_delete(conn); + osDelay(2000); + } +} + +/* ---- 构造 / 析构 ---- */ +static void udp_cq_channel_init(udp_channel_t *self, struct netconn *conn, channel_t *tmpl) +{ + self->me = *tmpl; + self->me.state = CH_STATE_UP; + self->conn = conn; + self->listen_port = g_udp_cq_port; + app_channel_register(CH_ID_UDP_CQ, &self->me); +} + +static void udp_cq_channel_deinit(udp_channel_t *self) +{ + self->me.ops = nullptr; + self->me.state = CH_STATE_DOWN; + self->conn = NULL; /* 广播复用路径判空依据(2026-08-21) */ + app_channel_register(CH_ID_UDP_CQ, nullptr); +} + +void udp_cq_connect_task(void *argument) +{ + struct netconn *conn = (struct netconn *)argument; + + udp_cq_channel_init(&s_udp_cq_ch, conn, &s_udp_cq_channel_tmpl); + + channel_t *ch = &s_udp_cq_ch.me; + struct netbuf *buf; + err_t err; + + while ((err = netconn_recv(conn, &buf)) == ERR_OK) { + void *data; + uint16_t len; + do { + netbuf_data(buf, &data, &len); + if (len > 0) { + const ip_addr_t *addr = netbuf_fromaddr(buf); + s_udp_cq_ch.src_ip[0] = ip4_addr1((const ip4_addr_t *)addr); + s_udp_cq_ch.src_ip[1] = ip4_addr2((const ip4_addr_t *)addr); + s_udp_cq_ch.src_ip[2] = ip4_addr3((const ip4_addr_t *)addr); + s_udp_cq_ch.src_ip[3] = ip4_addr4((const ip4_addr_t *)addr); + s_udp_cq_ch.src_port = netbuf_fromport(buf); + app_channel_dispatch(ch, (uint8_t *)data, len); + } + } while (netbuf_next(buf) >= 0); + netbuf_delete(buf); + } + + udp_cq_channel_deinit(&s_udp_cq_ch); + osSemaphoreRelease(udp_cq_disconnect_sem); + osThreadExit(); +} diff --git a/Application/Src/Config/app_board_net_cfg.c b/Application/Src/Config/app_board_net_cfg.c new file mode 100644 index 0000000..c8da9dc --- /dev/null +++ b/Application/Src/Config/app_board_net_cfg.c @@ -0,0 +1,255 @@ +/** + * @file app_board_net_cfg.c + * @brief 板级系统配置存储(内部 Flash Sector 1, 0x08004000) + * + * app_board_sys_info_t 记录 = magic(4B) | update_sta(4B) | FWInfo(40B) | NetConfig(20B) | CRC32(4B) + * 总长 72B(18 words),按 word 写入 Flash。 + * NetConfig 字段语义(方案 B,2026-08-21):port=TCP 业务口(LDI/IAP), + * udp_port=CQ UDP 业务口。 + * + * 操作流程: + * 读取 → 直接内存映射 (ADDR_CONFIG_SECTOR) → 空/完整性检查 → 使用 + * 更新 → 读-改-写:空/损坏完整初始化,有效仅改 net_cfg,升级中间态仅放行 + * net_cfg 字段更新(update_sta/app_info 原样保留),同值跳过擦写 + * (语义详见 doc/07_LDI与IAP配置解耦) + */ + +#include "app_board_net_cfg.h" +#include "dev_flash_int.h" +#include "pl_crc.h" +#include "initcall.h" +#include "SEGGER_RTT.h" /* 诊断日志:版本落库分支观察点(低开销,仅失败/首写/跳过路径打印) */ + +#include + +#define BOARD_CFG_SIZE 0x4000 /* 16KB */ + +/* ---- Sector1 Flash 存储实例 ---- */ +static dev_flash_int_t g_board_cfg_flash = { + .me = {.capacity = BOARD_CFG_SIZE}, + .base_addr = ADDR_CONFIG_SECTOR, + .sector = PL_FLASH_SECTOR_1, +}; + +/* ---- ops 绑定(hw_dev_initcall) ---- */ +static void _app_board_net_cfg_storage_init(void) +{ + g_board_cfg_flash.me.ops = &flash_int_ops; +} +hw_dev_initcall(_app_board_net_cfg_storage_init); + +static dev_storage_t *_storage(void) +{ + return &g_board_cfg_flash.me; +} + +/* ================================================================ + * 完整性检查 + * ================================================================ */ + +/** @brief Flash 擦除后全为 0xFF,magic + crc 均为 0xFFFFFFFF 表示从未写入 */ +static bool _is_empty(const app_board_sys_info_t *info) +{ + return info->magic == 0xFFFFFFFF && info->config_crc == 0xFFFFFFFF; +} + +/** @brief magic 匹配后校验 CRC32(覆盖整个结构体除去 crc32 字段自身) */ +static bool _is_valid(const app_board_sys_info_t *info) +{ + if (info->magic != APP_BOARD_NET_CFG_MAGIC) + return false; + + size_t len = sizeof(app_board_sys_info_t) - sizeof(info->config_crc); + uint32_t crc32 = pl_crc32_calc(pl_crc_get_handle(), (uint8_t *)info, len); + return info->config_crc == crc32; +} + +/* ================================================================ + * 读取 + * ================================================================ */ + +/** @brief 内存映射整记录拷贝(无有效性判定,IAP 读 FWInfo/update_sta 用) */ +void app_board_net_cfg_read(app_board_sys_info_t *out) +{ + memcpy(out, (const void *)ADDR_CONFIG_SECTOR, sizeof(*out)); +} + +/** @brief 读网络配置:记录有效返回 0,无效返回 -1 */ +int32_t app_board_net_cfg_get(app_board_net_cfg_t *out) +{ + app_board_sys_info_t info; + app_board_net_cfg_read(&info); + + if (!_is_valid(&info)) + return -1; + + memcpy(out, &info.net_cfg, sizeof(*out)); + return 0; +} + +/* ================================================================ + * 写入(先擦除整扇区,再逐 word 编程) + * ================================================================ */ + +static int32_t _erase(void) +{ + return dev_storage_erase(_storage(), 0, 0); +} + +static int32_t _write(const app_board_sys_info_t *info) +{ + return dev_storage_write(_storage(), 0, (uint8_t *)info, sizeof(*info)); +} + +/** + * @brief 同步设备 IP/掩码/网关/TCP 口/UDP 口 到内部 Flash(读-改-net_cfg-写) + * + * 分支语义(定案 Q1/Q2/Q3 先行修复 + 方案 1 细化守卫,2026-08-14): + * - 空扇区:完整初始化,镜像老 Bootloader Init_Config_Info 首次上电语义 + * (magic + update_sta=updated + app_info 全 0 且 crc32=0xFFFFFFFF + net_cfg + CRC32), + * 确保 Bootloader/Recovery 能读到该 IP。 + * - 损坏记录(magic 无效或 CRC 错):按空扇区处理,完整初始化后写新配置(自愈)。 + * Bootloader 写记录永远是完整结构,不存在「magic 无效的升级中间态」, + * 损坏记录无保护价值,覆盖它使 0AH/4B02 可自愈。 + * - 有效记录且 update_sta != updated(升级中间态,Bootloader 条件 A/B 写入的 + * 强制升级/崩溃过频记录):仅更新 net_cfg(ip/mask/gw/port/udp_port),update_sta 与 + * app_info 原样保留(Bootloader 条件 D 判定不受影响)——0AH/4B02 改 IP 在 + * 中间态同样生效,IAP 0x01 上报立即反映新值(2026-08-18 修订)。 + * - 有效记录且 update_sta == updated:仅更新 net_cfg(ip/mask/gw/port/udp_port),其余字段 + * 原样保留;与现扇区 net_cfg 完全一致(含 port 与 udp_port)时跳过擦写(Q3:不做磨损均衡)。 + * + * @return 0 成功(含同值跳过);<0 为擦/写错误码 + */ +int32_t app_board_net_cfg_update(const uint8_t ip[4], const uint8_t mask[4], const uint8_t gw[4], uint32_t port, + uint32_t udp_port) +{ + app_board_sys_info_t info; + app_board_net_cfg_read(&info); + + /* Q3:写前快照原 net_cfg,用于同值跳过比较 */ + app_board_net_cfg_t old_cfg; + memcpy(&old_cfg, &info.net_cfg, sizeof(old_cfg)); + + bool empty = _is_empty(&info); + bool valid = !empty && _is_valid(&info); + + /* 升级中间态(magic/CRC 有效但 update_sta != updated,Bootloader 条件 A/B + * 写入的强制升级/崩溃过频记录):update_sta/app_info 原样保留(Bootloader + * 条件 D 判定依据),但 net_cfg 与升级判定无关——0AH/4B02 改 IP 仍应生效, + * 否则中间态下 Sector1 网络配置被冻结、IAP 0x01 上报永远返回旧值。 + * 下方 memcpy(info.net_cfg...) 仅改网络字段,其余字段从 Flash 读出原样保留, + * 升级流程语义不变。 */ + if (!valid) { + /* 空/损坏扇区:完整初始化,与老 Bootloader Init_Config_Info 首次上电写入逐字节同构。 + * 注意:update_sta 必须为 updated(APP_BOARD_UPDATED),写 FAILED 会被 Bootloader + * 判 App 损坏 */ + info.magic = APP_BOARD_NET_CFG_MAGIC; + info.update_sta = APP_BOARD_UPDATED; + memset(&info.app_info, 0, sizeof(info.app_info)); + info.app_info.crc32 = 0xFFFFFFFF; + } + + memcpy(info.net_cfg.ip, ip, 4); + memcpy(info.net_cfg.mask, mask, 4); + memcpy(info.net_cfg.gw, gw, 4); + info.net_cfg.port = port; + info.net_cfg.udp_port = udp_port; + + /* Q3:net_cfg(含 port 与 udp_port)与现扇区一致则跳过擦写,返回成功(不做磨损均衡)。 + * 条件用 valid 而非 !empty:空/损坏时 old_cfg 为无效快照,必须走写路径 */ + if (valid && memcmp(&info.net_cfg, &old_cfg, sizeof(old_cfg)) == 0) + return 0; + + info.config_crc = pl_crc32_calc(pl_crc_get_handle(), (uint8_t *)&info, + sizeof(app_board_sys_info_t) - sizeof(info.config_crc)); + + int32_t ret = _erase(); + if (ret < 0) + return ret; + ret = _write(&info); + return ret < 0 ? ret : 0; +} + +/** + * @brief 同步固件版本号到内部 Flash(只改 app_info.version + 重算 config_crc) + * + * 红线:绝不改动 app_info.size / app_info.crc32 / net_cfg / update_sta 的既有值 + * (size/crc32 由 Recovery 升级流程写入,改动会触发 Bootloader 条件 D 判 App 损坏)。 + * + * 分支语义(与 app_board_net_cfg_update 同构,2026-08-18): + * - 空扇区:完整初始化(magic + update_sta=updated + app_info 全 0 且 crc32=0xFFFFFFFF + + * net_cfg 全 0)+ 填 version。调用点安排在 sw_board_init()(含 ldi_ctx_init 从 W25Qxx + * 回写 net_cfg)之后,正常路径不会走到此分支;若走到(例如 ldi_ctx_init 未回写), + * net_cfg 会被写成 0.0.0.0/port0,需由后续 0AH / 上电同步恢复; + * - 损坏记录(magic 无效或 CRC 错):按空扇区处理(自愈),完整初始化后填 version; + * - 有效记录且 update_sta != updated(升级中间态,Bootloader 条件 A/B 写入的强制升级/ + * 崩溃过频记录):拒绝覆盖,返回 -1; + * - 有效记录且 update_sta == updated:仅更新 app_info.version(其余字段原样保留);与现 + * 扇区 version 一致时跳过擦写(Q3:不做磨损均衡)。 + * + * @return 0 成功(含同值跳过);<0 失败(-1 = 升级中间态拒绝覆盖,-2 = version 过长 + * (strlen(version) >= 32,不做截断,显式拒绝),其余为擦/写错误码) + */ +int32_t app_board_net_cfg_fw_version_update(const char *version) +{ + app_board_sys_info_t info; + app_board_net_cfg_read(&info); + + /* version 长度校验:>= 32 字节无法完整落库,显式拒绝(不截断) */ + if (version == NULL || strlen(version) >= sizeof(info.app_info.version)) { + SEGGER_RTT_printf(0, "[fwver] reject: bad version arg (len=%d)\n", version ? (int)strlen(version) : -1); + return -2; + } + + /* Q3 同构:写前快照原 version,用于同值跳过比较 */ + char old_version[sizeof(info.app_info.version)]; + memcpy(old_version, info.app_info.version, sizeof(old_version)); + + bool empty = _is_empty(&info); + bool valid = !empty && _is_valid(&info); + + /* 升级中间态保护:magic/CRC 有效但 update_sta != updated 的记录 + * (Bootloader 条件 A/B 写入,等待其条件 D 判定用),主固件不得覆盖 */ + if (valid && info.update_sta != APP_BOARD_UPDATED) { + SEGGER_RTT_printf(0, "[fwver] refuse: upgrade mid-state upd_sta=%lu\n", (unsigned long)info.update_sta); + return -1; + } + + if (!valid) { + /* 空/损坏扇区:完整初始化,与 app_board_net_cfg_update 空分支同构。 + * 注意:update_sta 必须为 updated(APP_BOARD_UPDATED),写 FAILED 会被 Bootloader + * 判 App 损坏;net_cfg 置 0(0.0.0.0/port0)——调用点在 ldi_ctx_init 回写 net_cfg + * 之后,正常不会走到此分支,若走到由后续 0AH / 上电同步恢复 */ + memset(&info, 0, sizeof(info)); + info.magic = APP_BOARD_NET_CFG_MAGIC; + info.update_sta = APP_BOARD_UPDATED; + info.app_info.crc32 = 0xFFFFFFFF; + SEGGER_RTT_printf(0, "[fwver] sector empty/damaged -> full init, write ver=%s\n", version); + } + + /* 只动 version 字段:清零后拷贝(落库态尾部恒为 0,保证后续同值跳过可比较) */ + memset(info.app_info.version, 0, sizeof(info.app_info.version)); + memcpy(info.app_info.version, version, strlen(version)); + + /* Q3 同构:version 与现扇区一致则跳过擦写,返回成功(不做磨损均衡)。 + * 条件用 valid 而非 !empty:空/损坏时 old_version 为无效快照,必须走写路径 */ + if (valid && memcmp(info.app_info.version, old_version, sizeof(old_version)) == 0) { + SEGGER_RTT_printf(0, "[fwver] same ver=%s, skip erase/write\n", version); + return 0; + } + + info.config_crc = pl_crc32_calc(pl_crc_get_handle(), (uint8_t *)&info, + sizeof(app_board_sys_info_t) - sizeof(info.config_crc)); + + int32_t ret = _erase(); + if (ret < 0) { + SEGGER_RTT_printf(0, "[fwver] erase sector1 failed ret=%ld\n", (long)ret); + return ret; + } + ret = _write(&info); + if (ret < 0) + SEGGER_RTT_printf(0, "[fwver] write sector1 failed ret=%ld\n", (long)ret); + else + SEGGER_RTT_printf(0, "[fwver] write ok ver=%s\n", version); + return ret < 0 ? ret : 0; +} \ No newline at end of file diff --git a/Application/Src/IAP/app_iap.c b/Application/Src/IAP/app_iap.c index 7e7f7fa..3200258 100644 --- a/Application/Src/IAP/app_iap.c +++ b/Application/Src/IAP/app_iap.c @@ -3,7 +3,7 @@ * @brief IAP 固件升级协议处理 * * 帧格式: 0x5A5A5A5A (4B) | seq (4B) | cmd (4B) | len (4B) | data | CRC32 (4B) - * 承载于 RS485 + UDP,配置存储于 Flash 0x08004000。 + * 仅承载于 UDP(RJ45 共享 RB);IAP 不再使用 RS485。 */ #include "app_iap.h" @@ -11,15 +11,18 @@ #include "initcall.h" #include "pl_crc.h" #include "app_udp.h" -#include "app_iap_cfg.h" #include "app_iap_cmd.h" +/* RJ45 物理通道 RB:与 LDI/MQTT 等同槽 weak 合并 */ +RB_PROVIDE_WEAK(rb_provide_rj45, RB_SIZE_RJ45); + /* ---- proto_iap_queue 静态分配 ---- */ #define IAP_PAYLOAD_MAX (1044U) /* FRAME_MAX_LEN * 4 */ -#define IAP_MSG_SIZE (sizeof(frame_msg_t) + IAP_PAYLOAD_MAX) +#define IAP_MSG_SIZE (sizeof(frame_msg_t) + IAP_PAYLOAD_MAX) +#define IAP_QUEUE_DEPTH (2U) static StaticQueue_t s_iap_queue_cb; -static uint8_t s_iap_queue_buf[2 * IAP_MSG_SIZE]; +static uint8_t s_iap_queue_buf[IAP_QUEUE_DEPTH * IAP_MSG_SIZE]; static const osMessageQueueAttr_t s_iap_queue_attr = { .name = "proto_iap_queue", .cb_mem = &s_iap_queue_cb, @@ -28,23 +31,21 @@ static const osMessageQueueAttr_t s_iap_queue_attr = { .mq_size = sizeof(s_iap_queue_buf), }; -/* ---- 协议模块自注册 ---- */ -static proto_mask_t s_iap_mask; +/* ---- 协议模块自注册(仅 UDP → RJ45 槽,与 LDI 链式共享) ---- */ +static proto_mask_t s_iap_mask_udp; [[maybe_unused]] static void iap_module_init(void) { - // 指定使用的环形缓冲区 - ring_buffer_t *rb = app_proto_acquire_buf(0, 2048); + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RJ45, RB_SIZE_RJ45); + if (rb == nullptr) + return; - // 注册协议到多通道多协议解析模块 - s_iap_mask = app_proto_register(iap_probe_frame, rb); - if (s_iap_mask == 0) return; + s_iap_mask_udp = app_proto_register(iap_probe_frame, rb); + if (s_iap_mask_udp == 0) + return; - // 绑定协议使用到的通道 - app_proto_bind_channel(s_iap_mask, CH_ID_RS485); - app_proto_bind_channel(s_iap_mask, CH_ID_UDP); + app_proto_bind_channel(s_iap_mask_udp, CH_ID_UDP); - // 创建协议处理任务 g_iap_task_handle = osThreadNew(iap_handle_task, nullptr, &iap_task_attr); } sw_app_initcall(iap_module_init); @@ -55,7 +56,7 @@ osMessageQueueId_t g_iap_msg_queue; osThreadId_t g_iap_task_handle; const osThreadAttr_t iap_task_attr = { .name = "iap_handle_task", - .stack_size = 512 * 4, + .stack_size = 256 * 4, /* 帧缓冲为 static;原 2KB 偏大 */ .priority = (osPriority_t)osPriorityNormal, }; @@ -66,10 +67,12 @@ const osThreadAttr_t iap_task_attr = { /** @brief IAP 协议处理任务:阻塞等待帧队列 → 按 cmd 字段查表分派到命令处理函数 */ void iap_handle_task(void *argument) { + (void)argument; static uint8_t _msg_buf[IAP_MSG_SIZE]; frame_msg_t *msg = (frame_msg_t *)_msg_buf; - g_iap_msg_queue = osMessageQueueNew(2, IAP_MSG_SIZE, &s_iap_queue_attr); - app_proto_set_frame_queue(s_iap_mask, g_iap_msg_queue); + g_iap_msg_queue = osMessageQueueNew(IAP_QUEUE_DEPTH, IAP_MSG_SIZE, &s_iap_queue_attr); + if (s_iap_mask_udp != 0) + app_proto_set_frame_queue(s_iap_mask_udp, g_iap_msg_queue); for (;;) { if (osOK != osMessageQueueGet(g_iap_msg_queue, msg, NULL, osWaitForever)) @@ -78,6 +81,12 @@ void iap_handle_task(void *argument) iap_frame_t *frame_data = (iap_frame_t *)msg->data; uint8_t cmd = (uint8_t)((frame_data->cmd) & 0xff); + /* 查表越界防护:cmd 为 uint8_t 可达 255,须先校验小于表元素个数(IAP_CMD_COUNT, + * 与 g_iap_cmd_table[] 定义处 static_assert 保持同步)。 + * 越界帧静默丢弃——IAP 协议无错误应答约定,无法回 NACK。 */ + if (cmd >= IAP_CMD_COUNT) + continue; + g_iap_cmd_table[cmd](msg->ch, frame_data); } } @@ -95,8 +104,18 @@ proto_probe_sta_t iap_probe_frame(const channel_t *ch, const ring_buffer_t *buff uint32_t available = rb_avail(buff, nullptr); (void)ch; - /* min size check */ - if (available < 4) return PROTO_PROBE_WAIT; + /* 首字节快速拒绝:IAP 帧头首字节固定 0x5A,禁止盲 WAIT 阻塞链式探测 */ + if (available == 0) + return PROTO_PROBE_FAKE; + + uint8_t first_byte; + rb_peek(buff, 0, &first_byte, 1, nullptr); + if (first_byte != 0x5A) + return PROTO_PROBE_FAKE; + + /* 首字节已匹配,后续不足 4 字节 → 真正 WAIT */ + if (available < 4) + return PROTO_PROBE_WAIT; /* frame header check */ uint32_t head = 0; diff --git a/Application/Src/IAP/app_iap_cfg.c b/Application/Src/IAP/app_iap_cfg.c deleted file mode 100644 index e77c304..0000000 --- a/Application/Src/IAP/app_iap_cfg.c +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @file app_iap_cfg.c - * @brief IAP 系统配置 Flash 存储(Sector 1, 0x08004000) - * - * app_flash_iap_sys_info_t 记录 = magic(4B) | update_sta(4B) | FWInfo(40B) | NetConfig(16B) | CRC32(4B) - * 总长 68B(17 words),按 word 写入 Flash。 - * - * 操作流程: - * init/edit → 填充结构体 → 计算 CRC32 → erase → write - * 读取 → 直接内存映射 (ADDR_CONFIG_SECTOR) → 空/完整性检查 → 使用 - */ - -#include "app_iap_cfg.h" -#include "dev_flash_int.h" -#include "pl_crc.h" -#include "initcall.h" - -#define IAP_SIZE 0x4000 /* 16KB */ - -/* ---- IAP Flash 存储实例 ---- */ -static dev_flash_int_t g_flash_iap = { - .me = {.capacity = IAP_SIZE}, - .base_addr = ADDR_CONFIG_SECTOR, - .sector = PL_FLASH_SECTOR_1, -}; - -dev_storage_t *app_flash_iap_get_storage(void) -{ - return &g_flash_iap.me; -} - -/* ---- ops 绑定(hw_dev_initcall) ---- */ -static void _app_flash_iap_storage_init(void) -{ - g_flash_iap.me.ops = &flash_int_ops; -} -hw_dev_initcall(_app_flash_iap_storage_init); - -app_flash_iap_sys_info_t *g_config = (app_flash_iap_sys_info_t *)ADDR_CONFIG_SECTOR; /* 直接映射到 Flash 地址 */ - -/* ================================================================ - * 完整性检查 - * ================================================================ */ - -/** @brief Flash 擦除后全为 0xFF,magic + crc 均为 0xFFFFFFFF 表示从未写入 */ -bool app_flash_iap_is_config_empty(volatile const app_flash_iap_sys_info_t *info) -{ - return info->magic == 0xFFFFFFFF && info->config_crc == 0xFFFFFFFF; -} - -/** @brief magic 匹配后校验 CRC32(覆盖整个结构体除去 crc32 字段自身) */ -bool app_flash_iap_is_config_valid(volatile const app_flash_iap_sys_info_t *info) -{ - if (info->magic != APP_FLASH_IAP_MAGIC) return false; - - size_t len = sizeof(app_flash_iap_sys_info_t) - sizeof(info->config_crc); - uint32_t crc32 = pl_crc32_calc(pl_crc_get_handle(), (uint8_t *)info, len); - return info->config_crc == crc32; -} - -/* ================================================================ - * 写入操作(先擦除整扇区,再逐 word 编程) - * ================================================================ */ - -/** @brief 初始化配置:设置 magic + 默认 IP(192.168.114.200:9529) + 空固件信息,写入 Flash */ -void app_flash_iap_init_config(app_flash_iap_sys_info_t *info) -{ - info->magic = APP_FLASH_IAP_MAGIC; - info->update_sta = APP_FLASH_IAP_FAILED; - memset(&(info->app_info), 0, sizeof(info->app_info)); - - app_flash_iap_net_cfg_t net_info = { - .ip = {192, 168, 114, 200}, - .mask = {255, 255, 255, 0}, - .gw = {192, 168, 114, 1}, - .port = 0x2538, - }; - memcpy(&(info->net_cfg), &net_info, sizeof(app_flash_iap_net_cfg_t)); - - /* CRC32 覆盖 magic + update_sta + app_info + net_cfg,不含 crc32 自身 */ - info->config_crc = pl_crc32_calc(pl_crc_get_handle(), (uint8_t *)info, sizeof(app_flash_iap_sys_info_t) - sizeof(info->config_crc)); - - app_flash_iap_erase_config(); - app_flash_iap_write_config(info); -} - -/** @brief 修改配置:更新 CRC32 → 擦除 → 写入 */ -void app_flash_iap_edit_config(app_flash_iap_sys_info_t *info) -{ - info->config_crc = pl_crc32_calc(pl_crc_get_handle(), (uint8_t *)info, sizeof(app_flash_iap_sys_info_t) - sizeof(info->config_crc)); - app_flash_iap_erase_config(); - app_flash_iap_write_config(info); -} - -/** @brief 擦除 Sector 1(0x08004000,16KB) */ -int32_t app_flash_iap_erase_config(void) -{ - return dev_storage_erase(app_flash_iap_get_storage(), 0, 0); -} - -/** @brief 写入 app_flash_iap_sys_info_t 到 Flash */ -int32_t app_flash_iap_write_config(app_flash_iap_sys_info_t *info) -{ - return dev_storage_write(app_flash_iap_get_storage(), 0, (uint8_t *)info, sizeof(*info)); -} - -/** @brief 同步设备IP/掩码/网关到内部Flash(读-改-net_cfg-写,不碰其他字段) */ -void app_flash_iap_update_net_cfg(const uint8_t ip[4], const uint8_t mask[4], const uint8_t gw[4]) -{ - app_flash_iap_sys_info_t info; - memcpy(&info, (void *)ADDR_CONFIG_SECTOR, sizeof(info)); - - if (!app_flash_iap_is_config_empty(&info) && !app_flash_iap_is_config_valid(&info)) - return; /* 配置已损坏, 不覆盖 */ - - memcpy(info.net_cfg.ip, ip, 4); - memcpy(info.net_cfg.mask, mask, 4); - memcpy(info.net_cfg.gw, gw, 4); - - app_flash_iap_edit_config(&info); -} diff --git a/Application/Src/IAP/app_iap_cmd.c b/Application/Src/IAP/app_iap_cmd.c index ca60e8d..0bafa46 100644 --- a/Application/Src/IAP/app_iap_cmd.c +++ b/Application/Src/IAP/app_iap_cmd.c @@ -9,7 +9,8 @@ #include "pl_iwdg.h" #include "pl_sys.h" #include "app_udp.h" -#include "pl_net_adapt.h" +#include "app_board_net_cfg.h" +#include #define U8_LEN(x) ((x) * sizeof(uint32_t)) #define U32_LEN(y) ((y) / sizeof(uint32_t)) @@ -26,7 +27,7 @@ static void cmd_Restart_07(channel_t *ch, iap_frame_t *IAP_Data); /* ================================================================ * 命令表(按 cmd 编号索引) * ================================================================ */ -const iap_cmd_handler_fn_t g_iap_cmd_table[] = { +const iap_cmd_handler_fn_t g_iap_cmd_table[IAP_CMD_COUNT] = { cmd_Test_00, cmd_ReportIp_01, cmd_ForceModifyIP_02, @@ -37,6 +38,10 @@ const iap_cmd_handler_fn_t g_iap_cmd_table[] = { cmd_Restart_07, }; +/* 与 app_iap_cmd.h 的 IAP_CMD_COUNT 保持同步,防增删命令时查表越界 */ +static_assert(sizeof(g_iap_cmd_table) / sizeof(g_iap_cmd_table[0]) == IAP_CMD_COUNT, + "IAP 命令表条目数与 IAP_CMD_COUNT 不同步"); + /** @brief 构造 IAP 响应帧并发送 */ /* ================================================================ * 命令实现 @@ -65,20 +70,6 @@ static void cmd_SendReData(channel_t *ch, uint32_t ReSeq, uint32_t ReCmd, uint32 channel_send(ch, (uint8_t *)pIAP_ReTmp, sizeof(iap_frame_t) + U8_LEN(ReLen) + sizeof(uint32_t)); } -typedef struct { - ip4_addr_t ip; - ip4_addr_t mask; - ip4_addr_t gw; - uint16_t port; -} iap_ipconfig_t; - -/** @brief TCP/IP 线程回调:应用新 IP 配置到 netif */ -static void iap_update_ip(void *ctx) -{ - iap_ipconfig_t *config = (iap_ipconfig_t *)ctx; - netif_set_addr(netif_default, &config->ip, &config->mask, &config->gw); -} - /* ---- Command handlers (0x00 ~ 0x07) ---- */ /** @brief 0x00: Test (no-op) */ @@ -91,7 +82,8 @@ static void cmd_Test_00(channel_t *ch, iap_frame_t *IAP_Data) /** @brief 0x01: Report current IP config */ static void cmd_ReportIp_01(channel_t *ch, iap_frame_t *IAP_Data) { - app_flash_iap_sys_info_t config_info = *((app_flash_iap_sys_info_t *)ADDR_CONFIG_SECTOR); + app_board_sys_info_t config_info; + app_board_net_cfg_read(&config_info); uint32_t ReData[4] = {0}; ReData[0] = config_info.net_cfg.ip[0] << 24 | config_info.net_cfg.ip[1] << 16 | config_info.net_cfg.ip[2] << 8 | config_info.net_cfg.ip[3]; @@ -102,17 +94,13 @@ static void cmd_ReportIp_01(channel_t *ch, iap_frame_t *IAP_Data) cmd_SendReData(ch, IAP_Data->seq, rtn_cmd01, U32_LEN(sizeof(ReData)), ReData); } -static iap_ipconfig_t ipconfig = {0}; - /** @brief 0x02: Force modify IP and write to Flash */ static void cmd_ForceModifyIP_02(channel_t *ch, iap_frame_t *IAP_Data) { - app_flash_iap_sys_info_t config_info = *((app_flash_iap_sys_info_t *)ADDR_CONFIG_SECTOR); - uint32_t TmpData[4] = {0}; memcpy(TmpData, IAP_Data->data_crc, sizeof(TmpData)); - app_flash_iap_net_cfg_t net_info = {0}; + app_board_net_cfg_t net_info = {0}; net_info.ip[0] = (uint8_t)(TmpData[0] >> 24); net_info.ip[1] = (uint8_t)(TmpData[0] >> 16); net_info.ip[2] = (uint8_t)(TmpData[0] >> 8); @@ -127,25 +115,45 @@ static void cmd_ForceModifyIP_02(channel_t *ch, iap_frame_t *IAP_Data) net_info.gw[3] = (uint8_t)(TmpData[2]); net_info.port = TmpData[3]; - config_info.net_cfg = net_info; - IP4_ADDR(&ipconfig.ip, net_info.ip[0], net_info.ip[1], net_info.ip[2], net_info.ip[3]); - IP4_ADDR(&ipconfig.mask, net_info.mask[0], net_info.mask[1], net_info.mask[2], net_info.mask[3]); - IP4_ADDR(&ipconfig.gw, net_info.gw[0], net_info.gw[1], net_info.gw[2], net_info.gw[3]); - tcpip_callback(iap_update_ip, &ipconfig); - - app_flash_iap_edit_config(&config_info); - cmd_SendReData(ch, IAP_Data->seq, rtn_cmd02, 0, NULL); + /* 写入路径(见 app_board_net_cfg_update):空/损坏扇区完整初始化,升级中间态仅放行 + * net_cfg 更新(update_sta/app_info 保留),net_cfg 同值跳过擦写。所有改 IP 接口 + * (LDI 0AH / IAP 4B02 / Recovery IAP)统一 + * **重启生效**——此处仅持久化到 Sector1,不即时改 netif;上电由 Bootloader / + * Recovery MX_LWIP_Init / 主固件 ldi_ctx_init 从 Sector1 读取生效。 + * + * 协议扩展(方案 2,2026-08-14):应答帧由无载荷改为 1 word 结果码, + * 参照 4B04「准备升级」应答 0/1 先例——0x00000000 成功(含同值跳过)、 + * 0x00000001 失败(擦写错误)。 + * 兼容性风险:老上位机若按「B402 无载荷」解析,会多读到一个 word + * (被忽略还是报错取决于上位机实现),混合部署期需联调验证。 */ + /* 方案 B(2026-08-21):4B02 写 TCP 业务口(包内 port),udp_port 保留现值 + * (get 失败用出厂默认 20103)。 */ + app_board_net_cfg_t cur_cfg; + uint32_t udp_port = (app_board_net_cfg_get(&cur_cfg) == 0) ? cur_cfg.udp_port : 20103U; + int32_t ret = app_board_net_cfg_update(net_info.ip, net_info.mask, net_info.gw, net_info.port, + udp_port); + uint32_t result = ret < 0 ? 1 : 0; + cmd_SendReData(ch, IAP_Data->seq, rtn_cmd02, 1, &result); } /** @brief 0x03: Report firmware version, size, CRC32, update status */ static void cmd_ReportFirmwareStatus_03(channel_t *ch, iap_frame_t *IAP_Data) { - app_flash_iap_sys_info_t config_info = *((app_flash_iap_sys_info_t *)ADDR_CONFIG_SECTOR); + app_board_sys_info_t config_info; + app_board_net_cfg_read(&config_info); uint32_t ReData[11] = {0}; ReData[0] = config_info.app_info.size; ReData[1] = config_info.app_info.crc32; - memcpy(ReData + 2, config_info.app_info.version, sizeof(config_info.app_info.version)); + /* version 是 ASCII 字符串:按上位机 word 显示约定(大端字节序)逐 word 构造, + * 与 0x01 的 IP 构造同构;不能 memcpy(否则上位机按 4 字节一组反转显示) */ + const char *ver = config_info.app_info.version; + for (uint32_t i = 0; i < sizeof(config_info.app_info.version) / sizeof(uint32_t); i++) { + ReData[2 + i] = (uint32_t)(uint8_t)ver[4 * i] << 24 | + (uint32_t)(uint8_t)ver[4 * i + 1] << 16 | + (uint32_t)(uint8_t)ver[4 * i + 2] << 8 | + (uint32_t)(uint8_t)ver[4 * i + 3]; + } ReData[10] = config_info.update_sta; cmd_SendReData(ch, IAP_Data->seq, rtn_cmd03, U32_LEN(sizeof(ReData)), ReData); diff --git a/Application/Src/LDI/app_ldi.c b/Application/Src/LDI/app_ldi.c index 7d07b4c..e56649f 100644 --- a/Application/Src/LDI/app_ldi.c +++ b/Application/Src/LDI/app_ldi.c @@ -4,20 +4,25 @@ #include "app_ldi_cmd.h" #include "app_vms_ctrl.h" +#include "app_ldi_device.h" #include "crc_utils.h" #include "app_ldi_cfg.h" -#include "app_iap_cfg.h" +#include "app_board_net_cfg.h" #include "app_tcp_client.h" #include "app_tcp_server.h" #include "pl_net.h" #include "pl_rtc.h" +/* RJ45 物理通道 RB:与 IAP/MQTT 等同槽 weak 合并 */ +RB_PROVIDE_WEAK(rb_provide_rj45, RB_SIZE_RJ45); + /* ---- proto_ldi_queue 静态分配 ---- */ #define LDI_PAYLOAD_MAX (512U) /* 与探头 mem_pool 容量一致 */ -#define LDI_MSG_SIZE (sizeof(frame_msg_t) + LDI_PAYLOAD_MAX) +#define LDI_MSG_SIZE (sizeof(frame_msg_t) + LDI_PAYLOAD_MAX) +#define LDI_QUEUE_DEPTH (4U) static StaticQueue_t s_ldi_queue_cb; -static uint8_t s_ldi_queue_buf[2 * LDI_MSG_SIZE]; +static uint8_t s_ldi_queue_buf[LDI_QUEUE_DEPTH * LDI_MSG_SIZE]; static const osMessageQueueAttr_t s_ldi_queue_attr = { .name = "proto_ldi_queue", .cb_mem = &s_ldi_queue_cb, @@ -29,7 +34,7 @@ static const osMessageQueueAttr_t s_ldi_queue_attr = { static_assert(sizeof(ldi_device_t) == 1, "ldi_device_t must be 1 byte"); static_assert(sizeof(ldi_cmd_type_t) == 1, "ldi_cmd_type_t must be 1 byte"); -const static ldi_cmd_type_t cmd_index_table[] = { +static const ldi_cmd_type_t cmd_index_table[] = { LDI_CMD_SET_IP_REQ, LDI_CMD_SET_PARA_REQ, LDI_CMD_REBOOT_REQ, @@ -48,22 +53,118 @@ const static ldi_cmd_type_t cmd_index_table[] = { ldi_ctx_t g_ldi = { .state = LDI_ST_UNINIT, .cfg = { - .module_count = 2, + .module_count = 4, .modules = { - {.device_type = LDI_DEV_TYPE_VMS, .device_index = 1}, - // {.device_type = LDI_DEV_TYPE_CANOPY_LIGHT, .device_index = 1}, + {.device_type = LDI_DEV_TYPE_DISPLAY, .device_index = 1}, + {.device_type = LDI_DEV_TYPE_LANE_SIGNAL, .device_index = 1}, + {.device_type = LDI_DEV_TYPE_ALARM, .device_index = 1}, + {.device_type = LDI_DEV_TYPE_VOICE, .device_index = 1}, }, }, }; +/** + * @brief LDI 上下文装载:Sector1/W25 自愈 + LDI 设备配置装载。 + * + * 职责(2026-08-21 解耦修订):只做配置真源裁定与自愈—— + * 网络字段以 Sector1.net_cfg 为唯一真源、W25 为恢复镜像(三分支判定), + * 非网络字段(host/lane/cert/modules)与 TCP Client 远端装载,Sector1 空/损坏 + * 时从 W25 回写自愈。**不再应用网络**:pl_net_set_ip / + * app_tcp_server_set_port 移交中立模块 app_net_boot(app_boot.c init_task + * 在本函数之后调用 app_net_boot_apply 统一应用 netif 与 TCP Server 口)。 + */ void ldi_ctx_init(ldi_ctx_t *self) { app_flash_ldi_cfg_info_t flash_cfg = {0}; + bool flash_valid = false; if (app_flash_ldi_load_config(&flash_cfg)) { - /* Flash 有有效配置:应用到运行环境 */ + /* Flash 记录的 module_count 钳制:超设备类型上限(LDI_DEV_TYPE_COUNT=13)或 + * 存储数组容量(APP_FLASH_LDI_MAX_MODULES=11)视为非法记录,回退默认配置, + * 防后续按 module_count 遍历 flash_cfg.modules[] 越界读。 */ + if (flash_cfg.module_count <= LDI_DEV_TYPE_COUNT && + flash_cfg.module_count <= APP_FLASH_LDI_MAX_MODULES) + flash_valid = true; + } + + /* ---- 方案 X(doc/07 §12):Sector1 为网络配置唯一真源,W25 为恢复镜像 ---- + * Sector1 有效性判定:公开接口 app_board_net_cfg_get 做 magic+CRC 校验 + * (返回 0 = 有效)。内存映射拷贝,上电可安全执行(不擦写内部 Flash)。 */ + app_board_net_cfg_t board_cfg = {0}; + bool board_valid = (app_board_net_cfg_get(&board_cfg) == 0); + + /* net_cfg 合法性(与 app_net_boot 同口径):IP 非 0 且端口 1~65535。 + * 2026-08-21 修订:Sector1 记录有效且 net_cfg 合法即采纳(update_sta 不再参与 + * 分支判定,含升级中间态)——app_board_net_cfg_update 自 2026-08-18 起在中间态 + * 也放行 net_cfg 更新(0AH/4B02/CQ setip 改 IP 中间态同样生效),若此处仍按 + * update_sta != updated 落入分支 2,W25 回写会用陈旧镜像覆盖刚写入的 net_cfg, + * setip 失效。Bootloader 条件 D 对中间态记录本就进 Recovery(不启动主固件), + * 主固件可运行态下采纳 Sector1 net_cfg 无副作用。 */ + bool net_valid = board_valid && + (board_cfg.ip[0] | board_cfg.ip[1] | board_cfg.ip[2] | board_cfg.ip[3]) != 0U && + board_cfg.port > 0U && board_cfg.port <= 65535U; + + if (net_valid) { + /* ---- 分支 1:Sector1 有效(magic+CRC)且 net_cfg 合法 → 以 Sector1 为准 ---- + * 网络字段(device_ip/netmask/gateway/device_port)全部取自 Sector1.net_cfg。 + * 缺陷 B 修复:device_port 必须读 Sector1.net_cfg.port,而非编译期默认 + * (app_tcp_server_get_port),否则 0AH/4B02 改过的端口上电即被默认值覆盖。 */ + memcpy(self->cfg.device_ip, board_cfg.ip, sizeof(self->cfg.device_ip)); + memcpy(self->cfg.netmask, board_cfg.mask, sizeof(self->cfg.netmask)); + memcpy(self->cfg.gateway, board_cfg.gw, sizeof(self->cfg.gateway)); + self->cfg.device_port = (uint16_t)board_cfg.port; + + if (flash_valid) { + /* W25 已有有效配置:非网络字段(host/lane/cert/modules)沿用 W25, + * 网络字段仍以 Sector1 为准(Sector1 优先,不覆盖 W25)。 */ + memcpy(self->cfg.host_ip, flash_cfg.host_ip, sizeof(self->cfg.host_ip)); + self->cfg.host_port = flash_cfg.host_port; + memcpy(self->cfg.lane_hex, flash_cfg.lane_hex, sizeof(self->cfg.lane_hex)); + memcpy(self->cfg.cert, flash_cfg.cert, sizeof(self->cfg.cert)); + + /* module: 以编译期 type 做键匹配,同步 index */ + for (uint8_t i = 0; i < self->cfg.module_count; i++) { + for (uint8_t j = 0; j < flash_cfg.module_count; j++) { + if (flash_cfg.modules[j].device_type == self->cfg.modules[i].device_type) { + self->cfg.modules[i].device_index = flash_cfg.modules[j].device_index; + break; + } + } + } + app_tcp_client_set_remote(self->cfg.host_ip, self->cfg.host_port); + + /* 缺陷 C/A 修复(镜像自愈):Sector1 与 W25 的网络字段逐项比对, + * 任一不一致 → 用当前 cfg(网络字段取自 Sector1)刷新 W25 镜像。 + * 覆盖两类陈旧场景:① 4B02 只写 Sector1、不写 W25 → 镜像残留旧值, + * 擦 Sector1 恢复出厂时会回滚旧 IP;② 单次 W25 写失败导致的永久陈旧。 + * W25 为 SPI 4KB 擦写,无内部 Flash 总线停顿,上电执行安全。 */ + if (memcmp(self->cfg.device_ip, flash_cfg.device_ip, sizeof(flash_cfg.device_ip)) != 0 || + memcmp(self->cfg.netmask, flash_cfg.netmask, sizeof(flash_cfg.netmask)) != 0 || + memcmp(self->cfg.gateway, flash_cfg.gateway, sizeof(flash_cfg.gateway)) != 0 || + self->cfg.device_port != flash_cfg.device_port) { + app_flash_ldi_save_config(&self->cfg); + } + } else { + /* W25 空/无效:host 字段取编译期默认,并把当前 cfg 的网络字段写为 W25 + * 恢复镜像——维护「擦 Sector1 恢复出厂后仍可还原用户配置」能力。 + * W25 为 SPI 外设擦写(4KB 扇区),不暂停 CPU 总线,区别于内部 Flash + * 擦除(1~2s 停顿),上电执行安全。 */ + memcpy(self->cfg.host_ip, app_tcp_client_get_host_ip(), 4); + self->cfg.host_port = app_tcp_client_get_host_port(); + app_flash_ldi_save_config(&self->cfg); + } + + /* 网络应用(pl_net_set_ip / app_tcp_server_set_port)已移交 + * app_net_boot_apply(2026-08-21 解耦),此处仅装载 cfg 与自愈 */ + self->cfg_valid = true; - /* 网络参数(device_ip/mask/gw/host_ip/host_port 等) */ + } else if (flash_valid) { + /* ---- 分支 2:Sector1 空/损坏/记录无效或 net_cfg 非法 + W25 有效 → 以 W25 为准 + * 并回写自愈 ----(2026-08-21 修订:升级中间态不再落入本分支——Sector1 有效且 + * net_cfg 合法即走分支 1,见上方 net_valid 判定) + * cfg 全字段取自 W25;回写 Sector1 由 app_board_net_cfg_update 内部处理 + * 全部分支(空/损坏完整初始化自愈;中间态下 update_sta/app_info 保留、仅 + * net_cfg 写入(2026-08-18 语义修订),见 doc/07 §10 方案 1)。 */ memcpy(self->cfg.device_ip, flash_cfg.device_ip, sizeof(self->cfg.device_ip)); self->cfg.device_port = flash_cfg.device_port; memcpy(self->cfg.netmask, flash_cfg.netmask, sizeof(self->cfg.netmask)); @@ -73,11 +174,6 @@ void ldi_ctx_init(ldi_ctx_t *self) memcpy(self->cfg.lane_hex, flash_cfg.lane_hex, sizeof(self->cfg.lane_hex)); memcpy(self->cfg.cert, flash_cfg.cert, sizeof(self->cfg.cert)); - /* 应用到 LwIP / TCP Server / TCP Client */ - pl_net_set_ip(self->cfg.device_ip, self->cfg.netmask, self->cfg.gateway); - app_tcp_server_set_port(self->cfg.device_port); - app_tcp_client_set_remote(self->cfg.host_ip, self->cfg.host_port); - /* module: 以编译期 type 做键匹配,同步 index */ for (uint8_t i = 0; i < self->cfg.module_count; i++) { for (uint8_t j = 0; j < flash_cfg.module_count; j++) { @@ -87,64 +183,75 @@ void ldi_ctx_init(ldi_ctx_t *self) } } } + + /* TCP Client 远端应用;netif / TCP Server 口已移交 + * app_net_boot_apply 统一应用(2026-08-21 解耦) */ + app_tcp_client_set_remote(self->cfg.host_ip, self->cfg.host_port); self->cfg_valid = true; - /* 同步 IP 到 IAP 内部 flash(仅当 IAP 已有有效配置且不一致时) */ - if (app_flash_iap_is_config_valid(g_config) - && (memcmp(self->cfg.device_ip, g_config->net_cfg.ip, 4) - || memcmp(self->cfg.netmask, g_config->net_cfg.mask, 4) - || memcmp(self->cfg.gateway, g_config->net_cfg.gw, 4))) - app_flash_iap_update_net_cfg(self->cfg.device_ip, self->cfg.netmask, self->cfg.gateway); + /* 回写 Sector1 自愈(升级中间态仅 net_cfg 写入,update_sta/app_info 保留)。 + * 方案 B(2026-08-21):W25 device_port 写 TCP 业务口,udp_port 保留现值 + * (Sector1 无效时 get 失败,用出厂默认 20103)。 */ + app_board_net_cfg_t cur_cfg; + uint32_t udp_port = (app_board_net_cfg_get(&cur_cfg) == 0) ? cur_cfg.udp_port : 20103U; + (void)app_board_net_cfg_update(self->cfg.device_ip, self->cfg.netmask, + self->cfg.gateway, self->cfg.device_port, + udp_port); } else { - /* 外部 flash 无有效配置,尝试从 IAP 内部 flash 读取 */ - if (app_flash_iap_is_config_valid(g_config)) { - memcpy(self->cfg.device_ip, g_config->net_cfg.ip, 4); - memcpy(self->cfg.netmask, g_config->net_cfg.mask, 4); - memcpy(self->cfg.gateway, g_config->net_cfg.gw, 4); - } else { - /* IAP 也无有效配置,使用上电默认 IP */ - uint8_t ip[4] = {0}, mask[4] = {0}, gw[4] = {0}; - pl_net_get_ip(ip, mask, gw); - memcpy(self->cfg.device_ip, ip, sizeof(ip)); - memcpy(self->cfg.netmask, mask, sizeof(mask)); - memcpy(self->cfg.gateway, gw, sizeof(gw)); - } + /* ---- 分支 3:Sector1 与 W25 皆空/无效 → 统一出厂默认 ---- + * 默认 IP 取 pl_net 上电值(三固件统一 192.168.114.200/24,doc/07 §11), + * 端口取 TCP Server 编译期默认。cfg 默认填充仅保证 LDI 上下文自洽, + * netif 应用由后续 app_net_boot_apply 统一执行(2026-08-21 解耦)。 */ + uint8_t ip[4] = {0}, mask[4] = {0}, gw[4] = {0}; + pl_net_get_ip(ip, mask, gw); + memcpy(self->cfg.device_ip, ip, sizeof(ip)); + memcpy(self->cfg.netmask, mask, sizeof(mask)); + memcpy(self->cfg.gateway, gw, sizeof(gw)); self->cfg.device_port = app_tcp_server_get_port(); memcpy(self->cfg.host_ip, app_tcp_client_get_host_ip(), 4); self->cfg.host_port = app_tcp_client_get_host_port(); - self->cfg_valid = true; - /* 不在上电阶段写 Flash:擦除会暂停 CPU 总线 1~2s,损坏 LwIP 时序。 - 配置由 0AH 命令在出厂配置阶段写入,写入时网络负载低,风险可控。 */ + self->cfg_valid = true; + /* 不写任何 Flash:配置由 0AH 命令在出厂配置阶段写入,写入时网络负载低, + * 风险可控;内部 Flash 擦除 1~2s 的 CPU 停顿不得出现在上电路径。 */ } } -/* ---- 协议自注册 ---- */ -static proto_mask_t s_ldi_mask; +/* ---- 协议自注册:仅 RJ45(TCP/UDP 逻辑通道共享同一物理 RB,链式探测)---- */ +static proto_mask_t s_ldi_mask_tcp_server; +static proto_mask_t s_ldi_mask_tcp_client; +static proto_mask_t s_ldi_mask_udp; -[[maybe_unused]] static void ldi_module_init(void) +static void ldi_module_init(void) { - // 指定协议使用的环形缓冲区 - ring_buffer_t *rb = app_proto_acquire_buf(1, 512); - - // 注册协议到多通道多协议解析模块 - s_ldi_mask = app_proto_register(ldi_probe_frame, rb); - if (s_ldi_mask == 0) + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RJ45, RB_SIZE_RJ45); + if (rb == nullptr) return; - // 绑定协议使用到的通道 - app_proto_bind_channel(s_ldi_mask, CH_ID_TCP_SERVER); - app_proto_bind_channel(s_ldi_mask, CH_ID_TCP_CLIENT); - app_proto_bind_channel(s_ldi_mask, CH_ID_UDP); + /* 每逻辑通道独立 mask,共享同一 RJ45 RB(禁止单 mask 绑多通道) */ + s_ldi_mask_tcp_server = app_proto_register(ldi_probe_frame, rb); + if (s_ldi_mask_tcp_server != 0) + app_proto_bind_channel(s_ldi_mask_tcp_server, CH_ID_TCP_SERVER); + + s_ldi_mask_tcp_client = app_proto_register(ldi_probe_frame, rb); + if (s_ldi_mask_tcp_client != 0) + app_proto_bind_channel(s_ldi_mask_tcp_client, CH_ID_TCP_CLIENT); + + s_ldi_mask_udp = app_proto_register(ldi_probe_frame, rb); + if (s_ldi_mask_udp != 0) + app_proto_bind_channel(s_ldi_mask_udp, CH_ID_UDP); + + if (s_ldi_mask_tcp_server == 0 && s_ldi_mask_tcp_client == 0 && s_ldi_mask_udp == 0) + return; /* 上下文初始化必须在创建任务之前,保证 IP/端口在通道任务启动前就绪 */ ldi_ctx_init(&g_ldi); + ldi_device_init(); /* 保护 tx_buf,ldi_handle_task 和 ldi_timer_task 共享 */ const osMutexAttr_t tx_lock_attr = {.name = "ldi_tx_lock", .attr_bits = osMutexPrioInherit}; g_ldi.tx_lock = osMutexNew(&tx_lock_attr); - // 创建协议相关处理任务 g_ldi_task_handle = osThreadNew(ldi_handle_task, nullptr, &ldi_task_attr); g_ldi_timer_task_handle = osThreadNew(ldi_timer_task, nullptr, &ldi_timer_task_attr); } @@ -154,14 +261,14 @@ osMessageQueueId_t g_ldi_msg_queue; osThreadId_t g_ldi_task_handle; const osThreadAttr_t ldi_task_attr = { .name = "ldi_handle_task", - .stack_size = 512 * 4, + .stack_size = 256 * 4, /* 帧缓冲为 static;原 2KB 偏大 */ .priority = (osPriority_t)osPriorityNormal, }; osThreadId_t g_ldi_timer_task_handle; const osThreadAttr_t ldi_timer_task_attr = { .name = "ldi_timer_task", - .stack_size = 512 * 4, + .stack_size = 256 * 4, /* 周期上报路径;原 2KB 偏大 */ .priority = (osPriority_t)osPriorityNormal, }; @@ -247,10 +354,16 @@ void ldi_build_ctrl_rsp_head(ldi_ctrl_head_t *head, uint8_t cmd_type) void ldi_handle_task(void *argument) { + (void)argument; static uint8_t _msg_buf[LDI_MSG_SIZE]; frame_msg_t *msg = (frame_msg_t *)_msg_buf; - g_ldi_msg_queue = osMessageQueueNew(2, LDI_MSG_SIZE, &s_ldi_queue_attr); - app_proto_set_frame_queue(s_ldi_mask, g_ldi_msg_queue); + g_ldi_msg_queue = osMessageQueueNew(LDI_QUEUE_DEPTH, LDI_MSG_SIZE, &s_ldi_queue_attr); + if (s_ldi_mask_tcp_server != 0) + app_proto_set_frame_queue(s_ldi_mask_tcp_server, g_ldi_msg_queue); + if (s_ldi_mask_tcp_client != 0) + app_proto_set_frame_queue(s_ldi_mask_tcp_client, g_ldi_msg_queue); + if (s_ldi_mask_udp != 0) + app_proto_set_frame_queue(s_ldi_mask_udp, g_ldi_msg_queue); for (;;) { if (osOK != osMessageQueueGet(g_ldi_msg_queue, msg, NULL, osWaitForever)) @@ -259,6 +372,9 @@ void ldi_handle_task(void *argument) ldi_frame_t *ldi_frame = (ldi_frame_t *)msg->data; ldi_req_head_t *req_head = (ldi_req_head_t *)ldi_frame->data_crc; + /* 从已读帧中保存序号用于响应回显(从 probe 移至此处,消除 probe 副作用) */ + g_ldi.rsp_seq = ldi_frame->seq; + /* 状态门禁 */ if (!ldi_cmd_allowed(g_ldi.state, req_head->cmd_type)) continue; @@ -278,28 +394,69 @@ void ldi_handle_task(void *argument) * 帧探测 * ================================================================ */ -const static uint8_t ldi_stx[2] = {0xFF, 0xFF}; - +/** + * @brief LDI 帧探测(PURE 函数契约:只 peek,无副作用,不修改全局状态) + * + * 首字节快速拒绝:avail 为 0 时直接 FAKE;>= 1 字节时 peek 首字节判断 STX[0]。 + * g_ldi.rsp_seq 的赋值已移至 ldi_handle_task(读帧之后)。 + */ proto_probe_sta_t ldi_probe_frame(const channel_t *ch, const ring_buffer_t *buff, uint32_t *total_len, uint8_t *aux) { + (void)ch; uint32_t avail = rb_avail(buff, nullptr); - if (avail < sizeof(ldi_frame_t) + sizeof(ldi_req_head_t) + 2) + + /* 首字节快速拒绝:无数据或首字节非 0xFF → FAKE */ + if (avail == 0) + return PROTO_PROBE_FAKE; + + uint8_t first_byte; + rb_peek(buff, 0, &first_byte, 1, nullptr); + if (first_byte != 0xFF) + return PROTO_PROBE_FAKE; + + /* 等到定长帧头(STX[2]/VER/SEQ/LEN[4] = 8 字节),再按 LEN 等完整帧 */ + if (avail < sizeof(ldi_frame_t)) return PROTO_PROBE_WAIT; static uint8_t mem_pool[512] = {0}; memset(mem_pool, 0, sizeof(mem_pool)); - rb_peek(buff, 0, mem_pool, avail, nullptr); + rb_peek(buff, 0, mem_pool, avail > sizeof(mem_pool) ? sizeof(mem_pool) : avail, nullptr); ldi_frame_t *frame = (ldi_frame_t *)mem_pool; - if (memcmp(ldi_stx, frame->stx, sizeof(ldi_stx))) + /* STX[1] 校验 */ + if (frame->stx[1] != 0xFF) return PROTO_PROBE_FAKE; if (frame->ver != 0x00) return PROTO_PROBE_FAKE; - g_ldi.rsp_seq = frame->seq; /* 保存序号用于响应回显 */ + /* 注意:此处不再写 g_ldi.rsp_seq,移到 ldi_handle_task 读帧后 */ + + uint32_t data_len = ((uint32_t)frame->len[0] << 24) | ((uint32_t)frame->len[1] << 16) | + ((uint32_t)frame->len[2] << 8) | (uint32_t)frame->len[3]; + /* CQ 12B 二进制重启/搜索帧(FF FF 00 00 00 00 00 02 + 2B DATA + CRC16-XMODEM, + * len 恒为 2)与 LDI 共占 10011(CH_ID_UDP),且其 CRC 恰好通过 LDI 校验—— + * 若按 LDI 帧认领会吞掉 CQ 搜索/重启请求(CQ probe 链序在 LDI 之后失收)。 + * LDI 合法 DATA:21H 搜索 = 1B、其余命令 ≥ 20B 头,len==2 无合法命令 + * (doc/03 PartB Q23 实态修正:原「CQ 帧 len 域全 0 → FAKE」与实态不符)。 + * 明确 FAKE 放行给链序靠后的 CQ probe。 */ + if (data_len == 2U) + return PROTO_PROBE_FAKE; + + /* DATA 至少要有 CmdType;上限保护,避免脏 LEN 越界 */ + if (data_len < 1 || data_len > (sizeof(mem_pool) - sizeof(ldi_frame_t) - 2)) + return PROTO_PROBE_FAKE; + + uint32_t need = sizeof(ldi_frame_t) + data_len + 2; + if (avail < need) + return PROTO_PROBE_WAIT; + if (need > sizeof(mem_pool)) + return PROTO_PROBE_FAKE; - uint32_t data_len = (frame->len[3] & 0xFF) | (frame->len[2] << 8 & 0xFF00) | (frame->len[1] << 16 & 0xFF0000) | (frame->len[0] << 24 & 0xFF000000); - uint16_t frame_crc = (frame->data_crc[data_len] << 8) | frame->data_crc[data_len + 1]; + /* 完整帧已齐,再 peek 一次确保 CRC 落在 mem_pool 内 */ + rb_peek(buff, 0, mem_pool, need, nullptr); + frame = (ldi_frame_t *)mem_pool; + + uint16_t frame_crc = ((uint16_t)frame->data_crc[data_len] << 8) | frame->data_crc[data_len + 1]; uint16_t calc_crc = crc16_xmodem(&frame->ver, data_len + sizeof(*frame) - sizeof(frame->stx)); if (frame_crc != calc_crc) return PROTO_PROBE_FAKE; @@ -322,6 +479,14 @@ proto_probe_sta_t ldi_probe_frame(const channel_t *ch, const ring_buffer_t *buff } if (g_ldi.cfg_valid) { + /* data_len 下限:身份字段必须完整落在帧内(标准头 cert 偏移 10 + 8 = 18B; + * 1BH 头 cert 偏移 14 + 8 = 22B)。不足视为畸形帧直接 SKIP 整帧, + * 不再对帧尾之外的垃圾字节做 memcmp 身份校验。 */ + if (data_len < (uint32_t)cert_off + sizeof(g_ldi.cfg.cert)) { + *total_len = (uint32_t)(sizeof(ldi_frame_t) + data_len + 2); + return PROTO_PROBE_SKIP; + } + /* 帧结构合法但车道/设备不匹配 → SKIP 整帧 */ if (memcmp(g_ldi.cfg.lane_hex, frame->data_crc + lane_off, sizeof(g_ldi.cfg.lane_hex)) || memcmp(g_ldi.cfg.cert, frame->data_crc + cert_off, sizeof(g_ldi.cfg.cert))) { @@ -353,6 +518,7 @@ void ldi_timer_task(void *argument) osDelay(1000); vms_timer_poll(); /* VMS 定时清屏 — 不受通道状态影响 */ + ldi_device_timer_poll(); /* E6 KeepTime / E8 KeepTime */ channel_t *ch = app_channel_get(CH_ID_TCP_CLIENT); diff --git a/Application/Src/LDI/app_ldi_cfg.c b/Application/Src/LDI/app_ldi_cfg.c index 0f165bc..ab2c783 100644 --- a/Application/Src/LDI/app_ldi_cfg.c +++ b/Application/Src/LDI/app_ldi_cfg.c @@ -92,19 +92,21 @@ void app_flash_ldi_save_config(app_flash_ldi_cfg_info_t *info) */ bool app_flash_ldi_load_config(app_flash_ldi_cfg_info_t *info) { - app_flash_ldi_record_t rec = {0}; - dev_storage_t *w25 = app_flash_ldi_get_storage(); + /* 静态 SRAM:禁止用任务栈缓冲做 SPI DMA 目的(CCM/栈均不可靠作 DMA 目的) */ + static app_flash_ldi_record_t s_rec; + dev_storage_t *w25 = app_flash_ldi_get_storage(); - if (dev_storage_read(w25, s_ldi_base, (uint8_t *)&rec, sizeof(rec)) < 0) { + memset(&s_rec, 0, sizeof(s_rec)); + if (dev_storage_read(w25, s_ldi_base, (uint8_t *)&s_rec, sizeof(s_rec)) < 0) { memset(info, 0, sizeof(app_flash_ldi_cfg_info_t)); return false; } - if (app_flash_ldi_is_config_empty(&rec) || !app_flash_ldi_is_config_valid(&rec)) { + if (app_flash_ldi_is_config_empty(&s_rec) || !app_flash_ldi_is_config_valid(&s_rec)) { memset(info, 0, sizeof(app_flash_ldi_cfg_info_t)); return false; } - memcpy(info, &rec.cfg, sizeof(app_flash_ldi_cfg_info_t)); + memcpy(info, &s_rec.cfg, sizeof(app_flash_ldi_cfg_info_t)); return true; } diff --git a/Application/Src/LDI/app_ldi_cmd.c b/Application/Src/LDI/app_ldi_cmd.c index 9ab3c3d..fc32b59 100644 --- a/Application/Src/LDI/app_ldi_cmd.c +++ b/Application/Src/LDI/app_ldi_cmd.c @@ -3,12 +3,14 @@ #include #include "crc_utils.h" +#include "pl_crc.h" #include "app_ldi_cfg.h" -#include "app_iap_cfg.h" +#include "app_board_net_cfg.h" #include "pl_net.h" #include "pl_rtc.h" #include "pl_sys.h" #include "app_vms_ctrl.h" +#include "app_ldi_device.h" #include "app_udp.h" #include "app_tcp_server.h" @@ -26,7 +28,8 @@ /** * 网络配置信息 — set_ip / rep_ip_rsp 共用 * - * 共 67 字节,不含 head 和 error_code。 + * 网络参数块共 65 字节,不含 20 字节通用 head 和 1 字节 ErrorCode。 + * 请求 0AH: DATA offset 20 起;响应 D1H: DATA offset 21 起。 */ typedef struct [[gnu::packed]] { uint8_t host_ip[4]; // 上位机 IP1 (外设控制服务) @@ -40,8 +43,17 @@ typedef struct [[gnu::packed]] { uint8_t reserve2[2]; // 保留字节 } ldi_network_info_t; +/* 4.1.1 request network block: DATA offset 20, length 65 bytes. */ +static_assert(sizeof(ldi_network_info_t) == 65, "LDI network block must be 65 bytes"); +static_assert(offsetof(ldi_network_info_t, ntp_ip) == 45, "NtpIP offset must be 45"); +static_assert(offsetof(ldi_network_info_t, device_ip) == 49, "DeviceIP offset must be 49"); +static_assert(offsetof(ldi_network_info_t, device_port) == 53, "DevicePort offset must be 53"); +static_assert(offsetof(ldi_network_info_t, gateway) == 55, "Gateway offset must be 55"); +static_assert(offsetof(ldi_network_info_t, netmask) == 59, "Netmask offset must be 59"); +static_assert(offsetof(ldi_network_info_t, reserve2) == 63, "D1H reserved offset must be 63"); + /** - * 设备 IP 信息设置 (0AH) DATA 域 — 共 87 字节 (含 head) + * 设备 IP 信息设置 (0AH) DATA 域 — 共 85 字节 (含 head) * head(20B) + network_info(67B) */ typedef struct [[gnu::packed]] { @@ -307,6 +319,19 @@ static uint8_t ldi_next_rpt_seq(void) return (seq << 4) & 0xF0; /* 0x10, 0x20, ... 0xF0 */ } +/** + * @brief 从 DATA 域指针反推该帧的 DATA 域长度 + * + * 命令处理函数只收到 data_crc 首字节指针;ldi_frame_t 的 LEN 字段(4 字节大端) + * 紧邻 data_crc 之前,且 probe 已保证完整帧落在队列缓冲内,故可安全反推, + * 供各命令做 data_len 下限 / 剩余帧长校验。 + */ +static uint32_t _ldi_data_len(const void *data) +{ + const uint8_t *p = (const uint8_t *)data; + return ((uint32_t)p[-4] << 24) | ((uint32_t)p[-3] << 16) | ((uint32_t)p[-2] << 8) | (uint32_t)p[-1]; +} + /** 响应帧拼装工作缓冲区(所有 LDI 命令复用) */ /** @@ -319,6 +344,7 @@ static uint8_t ldi_next_rpt_seq(void) */ static void ldi_send_response(channel_t *ch, uint8_t rsp_cmd, uint8_t seq, const uint8_t *payload, uint16_t payload_len) { + (void)rsp_cmd; osMutexAcquire(g_ldi.tx_lock, osWaitForever); ldi_frame_t *frame = (ldi_frame_t *)g_ldi.tx_buf; @@ -355,6 +381,23 @@ static void ldi_send_response(channel_t *ch, uint8_t rsp_cmd, uint8_t seq, const #define LDI_RESPOND_EMPTY(ch, rsp_cmd, seq) \ ldi_send_response(ch, rsp_cmd, seq, nullptr, 0) +/** @brief 通用错误应答:标准 head + status=01H(与 0BH 既有错误应答约定一致) */ +static void _ldi_error_rsp(channel_t *ch, uint8_t rsp_cmd) +{ + ldi_status_rsp_t rsp = {.status = 0x01}; + ldi_build_rsp_head(&rsp.head, rsp_cmd); + LDI_RESPOND(ch, rsp_cmd, g_ldi.rsp_seq, rsp); +} + +/** @brief 1BH 整帧校验失败错误应答:B1H 头 + device_num=0(无逐模块状态可回) */ +static void _ldi_ctrl_error_rsp(channel_t *ch) +{ + ldi_ctrl_rsp_t rsp = {0}; + ldi_build_ctrl_rsp_head(&rsp.head, LDI_CMD_CTRL_RSP); + rsp.device_num = 0; + LDI_RESPOND(ch, LDI_CMD_CTRL_RSP, g_ldi.rsp_seq, rsp); +} + // ============================================================================ // 命令处理函数声明与分发表 // ============================================================================ @@ -397,14 +440,49 @@ const ldi_cmd_handler_fn_t g_ldi_cmd_table[] = { // 命令处理函数实现 // ============================================================================ +/** + * @brief 保存 LDI 配置至 W25 并返回结果(0AH 如实应答用) + * + * 落盘字节与 app_ldi_cfg.c:app_flash_ldi_save_config 完全一致 + * (magic + cfg + CRC32,CRC 覆盖 magic+cfg 即记录总长减 crc32 字段), + * 仅额外返回擦/写结果;save_config 为 void 且不在本次改动范围内, + * 故此处内联其逻辑以捕获返回值,两处需保持同构。 + * + * @return 0 成功;<0 擦除/写入失败(dev_storage_erase/write 错误码) + */ +static int32_t _ldi_save_config_ret(app_flash_ldi_cfg_info_t *info) +{ + app_flash_ldi_record_t rec = {0}; + rec.magic = APP_FLASH_LDI_MAGIC; + memcpy(&rec.cfg, info, sizeof(app_flash_ldi_cfg_info_t)); + rec.crc32 = pl_crc32_calc(pl_crc_get_handle(), (uint8_t *)&rec, sizeof(rec) - sizeof(rec.crc32)); + + int32_t ret = app_flash_ldi_erase_config(); + if (ret < 0) + return ret; + ret = app_flash_ldi_write_config(&rec); + return ret < 0 ? ret : 0; +} + /** * 处理 0AH 设备 IP 信息设置请求 * * 上位机→设备, 用于出厂配置模式下设置设备网络参数. * data 指向 cmd_set_ip_t 结构. + * + * 应答错误码:00H=成功, 01H=失败(与 0BH 及 ldi_status_rsp_t 既有约定一致)。 + * W25 保存或 Sector1 同步任一步失败即回 01H;固件侧不自动重试, + * 上位机收到 01H 后重发 0AH 即可恢复(两次写入均幂等)。 + * 网络配置写入 W25+Sector1 后**重启生效**(运行时网口不即时变更,属既定策略)。 */ void cmd_set_ip(channel_t *ch, void *data) { + /* data_len 下限:cmd_set_ip_t 共 85 字节(head 20 + network 65),不足回错误应答 */ + if (_ldi_data_len(data) < sizeof(cmd_set_ip_t)) { + _ldi_error_rsp(ch, LDI_CMD_SET_IP_RSP); + return; + } + cmd_set_ip_t *info = data; memcpy(g_ldi.cfg.device_ip, info->net.device_ip, sizeof(g_ldi.cfg.device_ip)); @@ -415,10 +493,18 @@ void cmd_set_ip(channel_t *ch, void *data) memcpy(g_ldi.cfg.gateway, info->net.gateway, sizeof(g_ldi.cfg.gateway)); g_ldi.cfg_valid = true; - app_flash_ldi_save_config(&g_ldi.cfg); - app_flash_iap_update_net_cfg(g_ldi.cfg.device_ip, g_ldi.cfg.netmask, g_ldi.cfg.gateway); - - ldi_status_rsp_t rsp = {.status = 0x00}; + /* 两步持久化:先 W25(LDI 完整配置),再板级网络配置(Sector1)。 + * 任一失败即如实应答失败,不再静默回 00H。 + * 方案 B(2026-08-21):0AH 写 TCP 业务口(device_port),udp_port 保留现值 + * (get 失败用出厂默认 20103)。 */ + int32_t w25_ret = _ldi_save_config_ret(&g_ldi.cfg); + app_board_net_cfg_t cur_cfg; + uint32_t udp_port = (app_board_net_cfg_get(&cur_cfg) == 0) ? cur_cfg.udp_port : 20103U; + int32_t board_ret = app_board_net_cfg_update(g_ldi.cfg.device_ip, g_ldi.cfg.netmask, + g_ldi.cfg.gateway, g_ldi.cfg.device_port, + udp_port); + + ldi_status_rsp_t rsp = {.status = (w25_ret < 0 || board_ret < 0) ? 0x01 : 0x00}; ldi_build_rsp_head(&rsp.head, LDI_CMD_SET_IP_RSP); LDI_RESPOND(ch, LDI_CMD_SET_IP_RSP, g_ldi.rsp_seq, rsp); } @@ -431,6 +517,14 @@ void cmd_set_ip(channel_t *ch, void *data) */ static void cmd_set_config(channel_t *ch, void *data) { + uint32_t data_len = _ldi_data_len(data); + + /* data_len 下限:至少 head(20B) + device_num(1B) */ + if (data_len < sizeof(ldi_req_head_t) + 1) { + _ldi_error_rsp(ch, LDI_CMD_SET_PARA_RSP); + return; + } + ldi_req_head_t *head = (ldi_req_head_t *)data; uint8_t *ptr = (uint8_t *)data + sizeof(ldi_req_head_t); uint8_t device_num = *ptr++; @@ -449,6 +543,12 @@ static void cmd_set_config(channel_t *ch, void *data) /* 以编译期 cfg 中的 device_type 做键,匹配请求帧中的 module,同步 index 和 vendor */ bool result = true; for (uint8_t i = 0; i < device_num; i++) { + /* 每 module 先查表取长度,读取公共头与 vendor 前均校验剩余帧长,防越界读 */ + uint32_t consumed = (uint32_t)(ptr - (const uint8_t *)data); + if (consumed + sizeof(ldi_module_head_t) > data_len) { + result = false; + break; + } ldi_module_head_t *mod = (ldi_module_head_t *)ptr; uint8_t mod_size = ldi_cfg_module_size((ldi_device_t)mod->device_type); @@ -456,6 +556,10 @@ static void cmd_set_config(channel_t *ch, void *data) result = false; break; } + if (consumed + mod_size > data_len) { + result = false; + break; + } int8_t cfg_idx = -1; for (uint8_t j = 0; j < g_ldi.cfg.module_count; j++) { @@ -513,6 +617,7 @@ static void cmd_reboot(channel_t *ch, void *data) */ static void cmd_rep_ip(channel_t *ch, void *data) { + (void)data; uint8_t buf[sizeof(ldi_status_rsp_t) + sizeof(ldi_network_info_t)] = {0}; ldi_status_rsp_t *rsp = (ldi_status_rsp_t *)buf; @@ -520,6 +625,7 @@ static void cmd_rep_ip(channel_t *ch, void *data) ldi_build_rsp_head(&rsp->head, LDI_CMD_GET_IP_RSP); rsp->status = 0x00; + /* D1H DATA = head(20) | ErrorCode(1) | network block(65). */ memcpy(net->device_ip, g_ldi.cfg.device_ip, sizeof(net->device_ip)); net->device_port[0] = (uint8_t)(g_ldi.cfg.device_port >> 8); @@ -595,6 +701,11 @@ static void cmd_rsp_report(channel_t *ch, void *data) { (void)ch; + /* data_len 下限:C0H 是服务端对设备上报的应答,协议无再应答约定, + * 不足 head(20B) 时静默丢弃(无法回错误应答) */ + if (_ldi_data_len(data) < sizeof(ldi_req_head_t)) + return; + /* C0H 是服务器对设备上报的应答,每 5 秒一次,携带服务器 Unix 时间戳,用于定期对钟 */ ldi_req_head_t *head = (ldi_req_head_t *)data; uint32_t ts = ((uint32_t)head->unix_timestamp[0] << 24) | @@ -621,7 +732,7 @@ static void cmd_rsp_report(channel_t *ch, void *data) */ void ldi_send_cert_req(channel_t *ch) { - uint8_t buf[sizeof(ldi_cert_req_t) + DEVICE_NUM * 2]; + uint8_t buf[sizeof(ldi_cert_req_t) + APP_FLASH_LDI_MAX_MODULES * 2]; ldi_cert_req_t *req = (ldi_cert_req_t *)buf; ldi_build_rsp_head(&req->head, LDI_CMD_CERT_REQ); @@ -645,7 +756,7 @@ void ldi_send_cert_req(channel_t *ch) */ void ldi_send_sta_rpt(channel_t *ch) { - uint8_t buf[sizeof(ldi_sta_rpt_t) + DEVICE_NUM * sizeof(ldi_device_info_t)]; + uint8_t buf[sizeof(ldi_sta_rpt_t) + APP_FLASH_LDI_MAX_MODULES * sizeof(ldi_device_info_t)]; ldi_sta_rpt_t *rpt = (ldi_sta_rpt_t *)buf; ldi_build_rsp_head(&rpt->head, LDI_CMD_STA_RPT_REQ); @@ -679,6 +790,8 @@ void ldi_send_sta_rpt(channel_t *ch) rpt->devices[i].available_status = 0x00; /* 暂填可用 */ rpt->devices[i].error_code = 0x00; rpt->devices[i].running_status = 0x01; + /* E6/E7/E8:用设备能力层真实状态覆盖默认值 */ + (void)ldi_device_fill_sta(g_ldi.cfg.modules[i].device_type, &rpt->devices[i]); rpt->devices[i].vendor_code[0] = '0'; /* 暂填默认厂商代码 */ rpt->devices[i].vendor_code[1] = '7'; rpt->devices[i].model_code[0] = '2'; /* 暂填默认型号代码 */ @@ -702,6 +815,11 @@ static void cmd_rsp_cert(channel_t *ch, void *data) { (void)ch; + /* data_len 下限:head(20B) + error_code(1B) 偏移读; + * E0H 同为服务端应答,无再应答约定,不足静默丢弃 */ + if (_ldi_data_len(data) < sizeof(ldi_req_head_t) + 1) + return; + /* E0H 响应:head(20B) + error_code(1B),同步时间戳并检查认证结果 */ ldi_req_head_t *head = (ldi_req_head_t *)data; uint32_t ts = ((uint32_t)head->unix_timestamp[0] << 24) | @@ -741,9 +859,24 @@ static void cmd_update(channel_t *ch, void *data) */ static void cmd_init(channel_t *ch, void *data) { + uint32_t data_len = _ldi_data_len(data); + + /* data_len 下限:至少 head(20B) + device_num(1B) */ + if (data_len < sizeof(ldi_req_head_t) + 1) { + _ldi_error_rsp(ch, LDI_CMD_INIT_RSP); + return; + } + uint8_t *ptr = (uint8_t *)data + sizeof(ldi_req_head_t); uint8_t device_num = *ptr++; + /* device_num 钳制 ≤ 设备类型上限(LDI_DEV_TYPE_COUNT=13),超限视为非法帧回错误应答, + * 同时防止响应缓冲 VLA 尺寸被恶意 device_num 撑爆 */ + if (device_num > LDI_DEV_TYPE_COUNT) { + _ldi_error_rsp(ch, LDI_CMD_INIT_RSP); + return; + } + // 每个响应 module 定长 10 字节 (custom_init_len=0) uint8_t buf[sizeof(ldi_init_rsp_t) + device_num * sizeof(ldi_init_rsp_module_t)]; ldi_init_rsp_t *rsp = (ldi_init_rsp_t *)buf; @@ -753,6 +886,12 @@ static void cmd_init(channel_t *ch, void *data) bool all_ok = true; ldi_init_rsp_module_t *dst_mod = (ldi_init_rsp_module_t *)rsp->modules; for (uint8_t i = 0; i < device_num; i++) { + /* 每请求 module 定长 5 字节:head(2) + protocol_version(2) + custom_init_len(1) */ + uint32_t consumed = (uint32_t)(ptr - (const uint8_t *)data); + if (data_len - consumed < sizeof(ldi_module_head_t) + 2 + 1) { + _ldi_error_rsp(ch, LDI_CMD_INIT_RSP); + return; + } ldi_module_head_t *req_mod = (ldi_module_head_t *)ptr; // 响应 module 头部 @@ -770,17 +909,29 @@ static void cmd_init(channel_t *ch, void *data) dst_mod->status = found ? 0x00 : 0x01; if (!found) all_ok = false; - // 版本号(暂填默认值) - dst_mod->software_version[0] = 0x00; + // 从请求模块中读取 protocol_version (2字节) 和 custom_init_len (1字节) + uint8_t *mod_data = ptr + sizeof(ldi_module_head_t); + uint8_t protocol_version[2] = {mod_data[0], mod_data[1]}; + uint8_t custom_init_len = mod_data[2]; + + /* custom_init_len 校验:不得超过剩余帧长(防 ptr 推进越出帧尾) */ + uint32_t fixed_part = sizeof(ldi_module_head_t) + 2 + 1; + if (custom_init_len > data_len - consumed - fixed_part) { + _ldi_error_rsp(ch, LDI_CMD_INIT_RSP); + return; + } + + // 版本号(从请求中读取) + dst_mod->software_version[0] = 0x00; // 暂填默认值 dst_mod->software_version[1] = 0x01; dst_mod->firmware_version[0] = 0x00; dst_mod->firmware_version[1] = 0x01; - dst_mod->protocol_version[0] = 0x00; - dst_mod->protocol_version[1] = 0x00; - dst_mod->custom_init_len = 0x00; // 当前无个性化内容 + dst_mod->protocol_version[0] = protocol_version[0]; + dst_mod->protocol_version[1] = protocol_version[1]; + dst_mod->custom_init_len = 0x00; // 当前无个性化内容,忽略请求中的 custom_init - // 推进: 请求帧 module (最小 3B) → 响应 module (10B) - ptr += sizeof(ldi_module_head_t) + 1; + // 推进: 请求帧 module (5字节 + custom_init_len) → 响应 module (10字节) + ptr += fixed_part + custom_init_len; dst_mod = (ldi_init_rsp_module_t *)((uint8_t *)dst_mod + sizeof(ldi_init_rsp_module_t)); } @@ -801,19 +952,56 @@ static void cmd_init(channel_t *ch, void *data) */ static void cmd_ctrl(channel_t *ch, void *data) { + uint32_t data_len = _ldi_data_len(data); + + /* data_len 下限:至少 ctrl_head(24B) + device_num(1B) */ + if (data_len < sizeof(ldi_ctrl_head_t) + 1) { + _ldi_ctrl_error_rsp(ch); + return; + } + // 1BH 使用 24 字节 ctrl_head (UnixTimestamp 8B),不沿用 ldi_req_head_t uint8_t *ptr = (uint8_t *)data + sizeof(ldi_ctrl_head_t); uint8_t device_num = *ptr++; + /* device_num 钳制 ≤ 设备类型上限(LDI_DEV_TYPE_COUNT=13),越界回错误应答, + * 同时防响应缓冲 VLA 尺寸被恶意 device_num 撑爆 */ + if (device_num > LDI_DEV_TYPE_COUNT) { + _ldi_ctrl_error_rsp(ch); + return; + } + + /* 响应总长防御钳制 ≤ tx_buf 容量(LDI_TX_BUF_SIZE=512),超限回错误应答。 + * device_num 已钳制 ≤13,响应最长 25+13×4=77 字节恒不超限,保留防御 */ + if (sizeof(ldi_ctrl_rsp_t) + (uint32_t)device_num * sizeof(ldi_ctrl_rsp_payload_t) > LDI_TX_BUF_SIZE) { + _ldi_ctrl_error_rsp(ch); + return; + } + uint8_t buf[sizeof(ldi_ctrl_rsp_t) + device_num * sizeof(ldi_ctrl_rsp_payload_t)]; ldi_ctrl_rsp_t *rsp = (ldi_ctrl_rsp_t *)buf; ldi_build_ctrl_rsp_head(&rsp->head, LDI_CMD_CTRL_RSP); rsp->device_num = device_num; for (uint8_t i = 0; i < device_num; i++) { + uint32_t consumed = (uint32_t)(ptr - (const uint8_t *)data); + + /* mod_len 读取需 2 字节,module 子帧必须完整落在帧内 */ + if (consumed + 2 > data_len) { + _ldi_ctrl_error_rsp(ch); + return; + } // mod_len: module 子帧总长 (含 DeviceType + DeviceIndex + payload),大端序 uint16_t mod_len = ((uint16_t)ptr[0] << 8) | ptr[1]; ptr += 2; + consumed += 2; + + /* mod_len 至少含 module 公共头(2B) + DeviceFuncType(1B),且不超过剩余帧长, + * 越界回错误应答(防 ptr 推进越出帧尾 / 后续读取越界) */ + if (mod_len < sizeof(ldi_module_head_t) + 1 || consumed + mod_len > data_len) { + _ldi_ctrl_error_rsp(ch); + return; + } ldi_module_head_t *mod = (ldi_module_head_t *)ptr; uint8_t *payload = ptr + sizeof(ldi_module_head_t); @@ -846,22 +1034,37 @@ static void cmd_ctrl(channel_t *ch, void *data) } case LDI_DEV_TYPE_DISPLAY: { // E6H 显示控制(01H) / 清屏(02H) ldi_ctrl_display_t *ctrl = (ldi_ctrl_display_t *)payload; - (void)ctrl; // TODO: dev_display_ctrl(ctrl) + uint16_t text_len = 0; + if (ctrl->device_func_type == 0x01) { + /* mod_len 含 DeviceType+Index+定长头;text 为柔性尾部 */ + uint16_t fixed = sizeof(ldi_module_head_t) + sizeof(ldi_ctrl_display_t); + text_len = (mod_len > fixed) ? (uint16_t)(mod_len - fixed) : 0; + } + if (ldi_device_display_ctrl(ctrl, text_len) != LDI_DEV_OK) + rsp->modules[i].status = 0x01; break; } case LDI_DEV_TYPE_LANE_SIGNAL: { // E7H 信号灯控制 (01H) ldi_ctrl_signal_t *ctrl = (ldi_ctrl_signal_t *)payload; - (void)ctrl; // TODO: dev_signal_ctrl(ctrl->color) + if (ldi_device_lane_signal_ctrl(ctrl) != LDI_DEV_OK) + rsp->modules[i].status = 0x01; break; } case LDI_DEV_TYPE_ALARM: { // E8H 报警控制 (01H) ldi_ctrl_alarm_t *ctrl = (ldi_ctrl_alarm_t *)payload; - (void)ctrl; // TODO: dev_alarm_ctrl(ctrl) + if (ldi_device_alarm_ctrl(ctrl) != LDI_DEV_OK) + rsp->modules[i].status = 0x01; break; } case LDI_DEV_TYPE_VMS: { // E9H → VMS (01H) ldi_ctrl_vms_t *ctrl = (ldi_ctrl_vms_t *)payload; - vms_ctrl(ctrl, mod_len - sizeof(ldi_ctrl_vms_t) - sizeof(ldi_module_head_t)); + /* mod_len 小于定长部分(head 2B + ldi_ctrl_vms_t 6B)时不再下溢相减 + * (uint16 回绕会向 vms_ctrl 传出巨值 text_len 越界读),直接标记失败 */ + uint16_t fixed = sizeof(ldi_ctrl_vms_t) + sizeof(ldi_module_head_t); + if (mod_len < fixed) + rsp->modules[i].status = 0x01; + else + vms_ctrl(ctrl, (uint16_t)(mod_len - fixed)); break; } case LDI_DEV_TYPE_CANOPY_LIGHT: { // EAH 雨棚灯控制 (01H) @@ -902,41 +1105,70 @@ static void cmd_rep_func(channel_t *ch, void *data) } /** - * 处理 21H 设备搜索请求(UDP 广播) + * 处理 21H 设备搜索请求(创迪发现口,默认 UDP/10011) * - * 广播回复 0x12: CmdType(1) + IP(4大端) + Port(2大端) + Gateway(4大端) + Mask(4大端) + ErrCode(1) + * 回复 12H: CmdType(1) + IP(4) + Port(2) + Gateway(4) + Mask(4) + ErrCode(1) + * 广播为主(2026-08-21:搜索应答面向广播工具,广播复用常驻通道 conn 不占 + * netconn 池,是可靠路径),回源单播仅容错、失败静默——回源依赖通道级 + * src 快照,多协议交错(10011 同口承载 LDI/IAP/CQ)下可能发错目标。 + * 不承担业务控制;网络参数取自 g_ldi.cfg(无配置时回退 pl_net 当前值)。 */ static void cmd_search(channel_t *ch, void *data) { (void)data; - ldi_search_rsp_t rsp; - rsp.cmd_type = LDI_CMD_SEARCH_RSP; - rsp.err_code = 0x00; + ldi_search_rsp_t rsp = { + .cmd_type = LDI_CMD_SEARCH_RSP, + .err_code = 0x00, + }; - memcpy(rsp.ip, g_ldi.cfg.device_ip, sizeof(rsp.ip)); - rsp.port[0] = (uint8_t)(g_ldi.cfg.device_port >> 8); - rsp.port[1] = (uint8_t)(g_ldi.cfg.device_port); - memcpy(rsp.gateway, g_ldi.cfg.gateway, sizeof(rsp.gateway)); - memcpy(rsp.mask, g_ldi.cfg.netmask, sizeof(rsp.mask)); + uint8_t ip[4] = {0}, mask[4] = {0}, gw[4] = {0}; + if (g_ldi.cfg_valid) { + memcpy(ip, g_ldi.cfg.device_ip, 4); + memcpy(mask, g_ldi.cfg.netmask, 4); + memcpy(gw, g_ldi.cfg.gateway, 4); + } else { + pl_net_get_ip(ip, mask, gw); + } + + memcpy(rsp.ip, ip, sizeof(rsp.ip)); + /* 12H 应答 Port 字段语义 = 设备「配置功能端口」(TCP 业务端口,出厂默认 9528, + * 协议文档 doc/02 附件2),非 UDP 发现口 10011。取 g_ldi.cfg 的 device_port, + * 未配置时回退出厂默认 9528——此前误用 LDI_DISCOVERY_PORT 导致上位机把 10011 + * 当配置端口读回并写回,污染 Sector1/W25 后 TCP Server 错误改监听 10011。 */ + uint16_t port = g_ldi.cfg_valid ? g_ldi.cfg.device_port : LDI_DEFAULT_CONFIG_PORT; + rsp.port[0] = (uint8_t)(port >> 8); + rsp.port[1] = (uint8_t)port; + memcpy(rsp.gateway, gw, sizeof(rsp.gateway)); + memcpy(rsp.mask, mask, sizeof(rsp.mask)); - /* 构造 LDI 帧 + 广播发送 */ - osMutexAcquire(g_ldi.tx_lock, osWaitForever); - ldi_frame_t *frame = (ldi_frame_t *)g_ldi.tx_buf; - frame->stx[0] = 0xFF; - frame->stx[1] = 0xFF; - frame->ver = 0x00; - frame->seq = ldi_next_rpt_seq(); uint16_t payload_len = sizeof(ldi_search_rsp_t); - frame->len[0] = (uint8_t)(payload_len >> 24); - frame->len[1] = (uint8_t)(payload_len >> 16); - frame->len[2] = (uint8_t)(payload_len >> 8); - frame->len[3] = (uint8_t)payload_len; + uint16_t frame_len = (uint16_t)(sizeof(ldi_frame_t) + payload_len + 2); + + osMutexAcquire(g_ldi.tx_lock, osWaitForever); + ldi_frame_t *frame = (ldi_frame_t *)g_ldi.tx_buf; + frame->stx[0] = 0xFF; + frame->stx[1] = 0xFF; + frame->ver = 0x00; + frame->seq = ldi_next_rpt_seq(); + frame->len[0] = (uint8_t)(payload_len >> 24); + frame->len[1] = (uint8_t)(payload_len >> 16); + frame->len[2] = (uint8_t)(payload_len >> 8); + frame->len[3] = (uint8_t)payload_len; memcpy(frame->data_crc, &rsp, payload_len); uint16_t crc = crc16_xmodem(&frame->ver, sizeof(*frame) - sizeof(frame->stx) + payload_len); frame->data_crc[payload_len] = (uint8_t)(crc >> 8); frame->data_crc[payload_len + 1] = (uint8_t)(crc & 0xFF); - osMutexRelease(g_ldi.tx_lock); - app_udp_broadcast(g_ldi.tx_buf, sizeof(ldi_frame_t) + payload_len + 2); + /* 发送顺序:广播为主、回源为辅(2026-08-21)。 + * 回源依赖通道 src 快照(s_udp_ch.src_ip/src_port),LDI 帧排队处理期间 + * 10011 上后续 IAP/CQ 帧会覆盖快照 → 回源可能发错目标,故先发广播保证 + * 搜索工具必达,回源仅容错且失败静默。帧级 src 快照(frame_msg_t 扩展) + * 属架构改造,本轮不做,留作已知限制(doc/07)。 */ + app_udp_broadcast(g_ldi.tx_buf, frame_len); + + if (ch != NULL && ch->ops != NULL && ch->ops->send != NULL) + (void)channel_send(ch, g_ldi.tx_buf, frame_len); + + osMutexRelease(g_ldi.tx_lock); } diff --git a/Application/Src/LDI/app_ldi_device.c b/Application/Src/LDI/app_ldi_device.c new file mode 100644 index 0000000..8dd7c68 --- /dev/null +++ b/Application/Src/LDI/app_ldi_device.c @@ -0,0 +1,396 @@ +/** + * @file app_ldi_device.c + * @brief LDI 设备能力层实现(E6/E7/E8) + * + * 硬件映射: + * E6 → app_render / dev_display + * E7 → dev_io_lane_light(单路灯;黄灯降级关灯,0CH 按附录 A.1 报红) + * E8 → dev_io_flash_light(报警器代实现;WorkMode 频率用软闪烁) + * + * 附录 A.1 running_status: + * E6: 00H关闭 / 01H开启 + * E7: 00H红灯 / 01H绿灯 + * E8: 00H停止 / 01H报警中 + */ + +#include "app_ldi_device.h" + +#include + +#include "cmsis_os2.h" +#include "app_render.h" +#include "dev_display.h" +#include "dev_io_ctrl.h" + +/* ---- 状态缓存 ---- */ +static ldi_dev_display_sta_t s_display_sta = { + .running_status = 0x00, + .font_color = 0x00, + .keep_time = 0x00, +}; + +static ldi_dev_signal_sta_t s_signal_sta = { + .running_status = 0x00, /* 附录 A.1:默认红灯 */ + .color = 0x02, /* 控制色码:红 */ +}; + +static ldi_dev_alarm_sta_t s_alarm_sta = { + .running_status = 0x00, + .work_mode = 0x00, + .keep_time = 0x00, +}; + +/* ---- KeepTime / 频率软定时 ---- */ +static bool s_display_timer_active; +static uint32_t s_display_clear_tick; + +static bool s_alarm_active; +static bool s_alarm_timer_active; +static uint32_t s_alarm_off_tick; +static bool s_alarm_freq_active; +static uint32_t s_alarm_toggle_tick; +static bool s_alarm_out_on; + +/* FontSize 01H~08H → 像素字号(协议未规定像素) */ +static const font_size_t s_font_size_map[9] = { + [0] = FONT_SELF_ADAPT, + [1] = FONT_16, + [2] = FONT_16, + [3] = FONT_24, + [4] = FONT_24, + [5] = FONT_24, + [6] = FONT_32, + [7] = FONT_32, + [8] = FONT_32, +}; + +static const display_color_t s_color_map[] = { + [1] = COLOR_GREEN, + [2] = COLOR_RED, + [3] = COLOR_YELLOW, +}; + +/* ClearType: 00文字清屏→黑;01全红;02全绿;其他扩展(含03黄) */ +static const display_color_t s_clear_color_map[] = { + [0] = COLOR_BLACK, + [1] = COLOR_RED, + [2] = COLOR_GREEN, + [3] = COLOR_YELLOW, +}; + +#define MAP(table, idx, fallback) \ + (((idx) < (sizeof(table) / sizeof((table)[0]))) ? (table)[(idx)] : (fallback)) + +static void display_fill(display_color_t color) +{ + app_render(&(render_cfg_t){ + .type = RENDER_FILL, + .x = 0, + .y = 0, + .w = 0, + .h = 0, + .color = color, + }); +} + +static void alarm_output(bool on) +{ + s_alarm_out_on = on; + dev_io_flash_light(on); +} + +static void alarm_stop(void) +{ + alarm_output(false); + s_alarm_sta.running_status = 0x00; + s_alarm_active = false; + s_alarm_timer_active = false; + s_alarm_freq_active = false; +} + +void ldi_device_init(void) +{ + s_display_sta.running_status = 0x00; + s_signal_sta.running_status = 0x00; /* A.1 红灯 */ + s_signal_sta.color = 0x02; + s_alarm_sta.running_status = 0x00; + s_display_timer_active = false; + alarm_stop(); + + dev_io_lane_light(false); +} + +/* ================================================================ + * E6 信息显示屏 + * ================================================================ */ + +static bool display_params_ok(const ldi_ctrl_display_t *ctrl, uint16_t text_len) +{ + if (ctrl->font_color < 0x01 || ctrl->font_color > 0x03) + return false; + if (ctrl->font_size > 0x08) + return false; + if (ctrl->font_line > 0x06) + return false; + if (text_len == 0) + return false; + /* 协议:每行最多 10 个中文字符(GBK 约 20 字节);超长拒绝,避免脏帧撑爆屏 */ + if (text_len > 128) + return false; + return true; +} + +/* ---- 显示行槽位:固定使用最小字号高度,避免不同字号互相覆盖 ---- */ +#define DISPLAY_LINE_SLOT_HEIGHT (16U) + +static ldi_dev_result_t display_show(const ldi_ctrl_display_t *ctrl, uint16_t text_len) +{ + if (!display_params_ok(ctrl, text_len)) + return LDI_DEV_FAIL; + + display_color_t color = MAP(s_color_map, ctrl->font_color, COLOR_GREEN); + font_size_t font_size = s_font_size_map[ctrl->font_size]; + + /* 协议 '_' → 换行;就地改写调用方缓冲(与 vms_ctrl 一致) */ + uint8_t *text = (uint8_t *)ctrl->text; + for (uint16_t i = 0; i < text_len; i++) { + if (text[i] == '_') + text[i] = '\n'; + } + + render_style_t style = { + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = (ctrl->font_line == 0), + }; + + dev_display_t *d = dev_display_get(); + uint16_t screen_w = d ? d->screen_rows : 0; + uint16_t screen_h = d ? d->screen_cols : 0; + + uint16_t render_y = 0; + uint16_t render_h = screen_h; + align_t v_align = ALIGN_LEFT_UP; + bool line_selected = false; + + if (ctrl->font_line > 0 && screen_h >= DISPLAY_LINE_SLOT_HEIGHT) { + uint16_t line_count = screen_h / DISPLAY_LINE_SLOT_HEIGHT; + if (ctrl->font_line <= line_count) { + render_y = (uint16_t)((ctrl->font_line - 1U) * DISPLAY_LINE_SLOT_HEIGHT); + render_h = DISPLAY_LINE_SLOT_HEIGHT; + line_selected = true; + dev_display_fill(d, 0, render_y, screen_w, render_h, COLOR_BLACK); + } + } else if (ctrl->font_line == 0) { + dev_display_fill(d, 0, 0, screen_w, screen_h, COLOR_BLACK); + } + + if (font_size == FONT_SELF_ADAPT && (screen_w == 0 || screen_h == 0)) + font_size = FONT_16; + if (line_selected && font_size != FONT_SELF_ADAPT && font_size > DISPLAY_LINE_SLOT_HEIGHT) + font_size = FONT_16; + style.v_align = v_align; + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = render_y, + .w = screen_w, + .h = render_h, + .color = color, + .text = (const char *)ctrl->text, + .len = text_len, + .style = &style, + .font_size = line_selected ? FONT_SELF_ADAPT : font_size, + .font_type = FONT_HT, + .text_enc = FONT_ENC_GBK, + }); + + s_display_sta.running_status = 0x01; /* A.1 开启 */ + s_display_sta.font_color = ctrl->font_color; + s_display_sta.keep_time = ctrl->keep_time; + + if (ctrl->keep_time == 0) { + app_render_save(); + s_display_timer_active = false; + } else { + s_display_clear_tick = osKernelGetTickCount() + (uint32_t)ctrl->keep_time * 1000U; + s_display_timer_active = true; + } + + return LDI_DEV_OK; +} + +static ldi_dev_result_t display_clear(const ldi_ctrl_display_t *ctrl) +{ + /* 00/01/02 标准;03H 黄为扩展;其余按文字清屏(黑) */ + display_color_t color = COLOR_BLACK; + if (ctrl->clear_type <= 0x03) + color = s_clear_color_map[ctrl->clear_type]; + + display_fill(color); + app_render_save(); + + s_display_sta.running_status = 0x00; /* A.1 关闭 */ + s_display_sta.font_color = 0x00; + s_display_sta.keep_time = 0x00; + s_display_timer_active = false; + + return LDI_DEV_OK; +} + +ldi_dev_result_t ldi_device_display_ctrl(const ldi_ctrl_display_t *ctrl, uint16_t text_len) +{ + if (ctrl == NULL) + return LDI_DEV_FAIL; + + if (ctrl->device_func_type == 0x01) + return display_show(ctrl, text_len); + if (ctrl->device_func_type == 0x02) + return display_clear(ctrl); + + return LDI_DEV_FAIL; +} + +/* ================================================================ + * E7 通行信号灯 + * + * 控制 Color: 01H绿 / 02H红 / 03H黄 + * 0CH 附录 A.1: 00H红灯 / 01H绿灯(无黄) + * 硬件: 单路 Lane_Light;绿=亮,红/黄=灭;黄上报按红 00H + * ================================================================ */ + +ldi_dev_result_t ldi_device_lane_signal_ctrl(const ldi_ctrl_signal_t *ctrl) +{ + if (ctrl == NULL || ctrl->device_func_type != 0x01) + return LDI_DEV_FAIL; + + switch (ctrl->color) { + case 0x01: /* 绿 — 通行 */ + dev_io_lane_light(true); + s_signal_sta.color = 0x01; + s_signal_sta.running_status = 0x01; /* A.1 绿灯 */ + return LDI_DEV_OK; + case 0x02: /* 红 — 禁行 */ + dev_io_lane_light(false); + s_signal_sta.color = 0x02; + s_signal_sta.running_status = 0x00; /* A.1 红灯 */ + return LDI_DEV_OK; + case 0x03: /* 黄 — 无独立黄灯,降级关灯,上报按红 */ + dev_io_lane_light(false); + s_signal_sta.color = 0x03; + s_signal_sta.running_status = 0x00; /* A.1 无黄,按红灯 */ + return LDI_DEV_OK; + default: + return LDI_DEV_FAIL; + } +} + +/* ================================================================ + * E8 报警器(Flash_Light 代实现) + * + * WorkMode: 00H 持续;01H~FFH 频率报警(半周期=work_mode 秒) + * KeepTime: 00H 一直;01H~FFH 秒后自动停止 + * ================================================================ */ + +ldi_dev_result_t ldi_device_alarm_ctrl(const ldi_ctrl_alarm_t *ctrl) +{ + if (ctrl == NULL || ctrl->device_func_type != 0x01) + return LDI_DEV_FAIL; + + if (ctrl->status == 0x00) { + s_alarm_sta.work_mode = ctrl->work_mode; + s_alarm_sta.keep_time = ctrl->keep_time; + alarm_stop(); + return LDI_DEV_OK; + } + + if (ctrl->status != 0x01) + return LDI_DEV_FAIL; + + s_alarm_sta.work_mode = ctrl->work_mode; + s_alarm_sta.keep_time = ctrl->keep_time; + s_alarm_sta.running_status = 0x01; /* A.1 报警中 */ + s_alarm_active = true; + + if (ctrl->work_mode == 0x00) { + /* 持续报警 */ + alarm_output(true); + s_alarm_freq_active = false; + } else { + /* 频率报警:半周期 = work_mode 秒,先亮后灭循环 */ + alarm_output(true); + s_alarm_freq_active = true; + s_alarm_toggle_tick = osKernelGetTickCount() + (uint32_t)ctrl->work_mode * 1000U; + } + + if (ctrl->keep_time == 0) { + s_alarm_timer_active = false; + } else { + s_alarm_off_tick = osKernelGetTickCount() + (uint32_t)ctrl->keep_time * 1000U; + s_alarm_timer_active = true; + } + + return LDI_DEV_OK; +} + +/* ================================================================ + * 定时轮询 / 状态采集 + * ================================================================ */ + +void ldi_device_timer_poll(void) +{ + uint32_t now = osKernelGetTickCount(); + + if (s_display_timer_active && now >= s_display_clear_tick) { + display_fill(COLOR_BLACK); + app_render_save(); + s_display_sta.running_status = 0x00; + s_display_timer_active = false; + } + + if (s_alarm_active && s_alarm_timer_active && now >= s_alarm_off_tick) { + alarm_stop(); + return; + } + + if (s_alarm_active && s_alarm_freq_active && now >= s_alarm_toggle_tick) { + alarm_output(!s_alarm_out_on); + s_alarm_toggle_tick = now + (uint32_t)s_alarm_sta.work_mode * 1000U; + } +} + +bool ldi_device_fill_sta(uint8_t device_type, ldi_device_info_t *info) +{ + if (info == NULL) + return false; + + switch (device_type) { + case LDI_DEV_TYPE_DISPLAY: + info->running_status = s_display_sta.running_status; + return true; + case LDI_DEV_TYPE_LANE_SIGNAL: + info->running_status = s_signal_sta.running_status; + return true; + case LDI_DEV_TYPE_ALARM: + info->running_status = s_alarm_sta.running_status; + return true; + default: + return false; + } +} + +const ldi_dev_display_sta_t *ldi_device_display_sta(void) +{ + return &s_display_sta; +} + +const ldi_dev_signal_sta_t *ldi_device_lane_signal_sta(void) +{ + return &s_signal_sta; +} + +const ldi_dev_alarm_sta_t *ldi_device_alarm_sta(void) +{ + return &s_alarm_sta; +} diff --git a/Application/Src/LDI/app_vms_ctrl.c b/Application/Src/LDI/app_vms_ctrl.c index c863047..cb972a4 100644 --- a/Application/Src/LDI/app_vms_ctrl.c +++ b/Application/Src/LDI/app_vms_ctrl.c @@ -5,6 +5,8 @@ #include "app_render.h" /* ---- VMS 定时清屏 ---- */ +#define VMS_LINE_SLOT_HEIGHT (16U) + static uint32_t s_vms_clear_tick; /* 清屏时刻 (RTOS tick) */ static bool s_vms_timer_active; /* 定时器是否激活 */ @@ -87,30 +89,23 @@ static void vms_display_ctrl(ldi_ctrl_vms_t *ctx, const uint16_t text_len) uint16_t render_y = 0; uint16_t render_h = screen_h; align_t v_align = ALIGN_CENTER; - - /* font_line > 0: 将屏幕按字号划分为若干行,文字显示在指定行; - font_line == 0: 上下居中 (默认) */ + bool line_selected = false; if (ctx->font_line > 0) { - /* 自适应字号时以 16 为基准计算行高 */ - uint16_t line_height = (font_size == FONT_SELF_ADAPT) ? 16U : (uint16_t)font_size; - uint16_t total_lines = screen_h / line_height; - + uint16_t total_lines = screen_h / VMS_LINE_SLOT_HEIGHT; if (total_lines > 0 && ctx->font_line <= total_lines) { - render_y = (ctx->font_line - 1) * line_height; - render_h = line_height; - v_align = ALIGN_LEFT_UP; /* 单行内不居中 */ - - /* 强制使用具体字号,否则渲染器会自适应缩放 */ - if (font_size == FONT_SELF_ADAPT) font_size = FONT_16; + render_y = (uint16_t)((ctx->font_line - 1U) * VMS_LINE_SLOT_HEIGHT); + render_h = VMS_LINE_SLOT_HEIGHT; + v_align = ALIGN_LEFT_UP; + line_selected = true; } - /* font_line 超出范围 → 回退到居中模式 */ } + /* ---- 构建渲染风格 ---- */ render_style_t style = { .h_align = h_align, .v_align = v_align, - .word_wrap = false, + .word_wrap = (ctx->font_line == 0), }; /* ---- LDI 协议 '_' → 换行 ---- */ @@ -119,7 +114,21 @@ static void vms_display_ctrl(ldi_ctrl_vms_t *ctx, const uint16_t text_len) ctx->text[i] = '\n'; /* ---- 清屏 ---- */ - vms_clear_screen(); + if (line_selected) { + app_render(&(render_cfg_t){ + .type = RENDER_FILL, + .x = 0, + .y = render_y, + .w = screen_w, + .h = render_h, + .color = COLOR_BLACK, + }); + } else { + vms_clear_screen(); + } + + if (line_selected && font_size > VMS_LINE_SLOT_HEIGHT) + font_size = FONT_16; /* ---- 渲染文字 ---- */ app_render(&(render_cfg_t){ diff --git a/Application/Src/MSL/app_msl.c b/Application/Src/MSL/app_msl.c deleted file mode 100644 index 6e02c37..0000000 --- a/Application/Src/MSL/app_msl.c +++ /dev/null @@ -1,106 +0,0 @@ -#include "app_msl.h" -#include "FreeRTOS.h" -#include "initcall.h" - -#include "bcc_utils.h" -#include "app_msl_cmd.h" -#include "app_key.h" - -/* ---- proto_msl_queue 静态分配 ---- */ -#define MSL_PAYLOAD_MAX (1032U) /* 帧头(2B) + 地址(1B) + 长度(2B) + 命令(1B) + 数据(1024B) + BCC(1B) */ -#define MSL_MSG_SIZE (sizeof(frame_msg_t) + MSL_PAYLOAD_MAX) - -static StaticQueue_t s_msl_queue_cb; -static uint8_t s_msl_queue_buf[2 * MSL_MSG_SIZE]; -static const osMessageQueueAttr_t s_msl_queue_attr = { - .name = "proto_msl_queue", - .cb_mem = &s_msl_queue_cb, - .cb_size = sizeof(s_msl_queue_cb), - .mq_mem = s_msl_queue_buf, - .mq_size = sizeof(s_msl_queue_buf), -}; - -static proto_mask_t s_msl_mask; - -osMessageQueueId_t g_msl_msg_queue; -osThreadId_t g_msl_task_handle; -const osThreadAttr_t msl_task_attr = { - .name = "msl_handle_task", - .stack_size = 512 * 4, - .priority = (osPriority_t)osPriorityNormal, -}; - -uint8_t msl_addr = 0; - -/*--- 帧任务---*/ -void msl_handle_task(void *argument) -{ - static uint8_t _msg_buf[MSL_MSG_SIZE]; - frame_msg_t *msg = (frame_msg_t *)_msg_buf; - g_msl_msg_queue = osMessageQueueNew(1, MSL_MSG_SIZE, &s_msl_queue_attr); - app_proto_set_frame_queue(s_msl_mask, g_msl_msg_queue); - - for (;;) { - if (osOK != osMessageQueueGet(g_msl_msg_queue, msg, NULL, osWaitForever)) - continue; - - msl_frame_t *msl_frame = (msl_frame_t *)(msg->data); - - g_msl_cmd_table[msl_frame->cmd](msg->ch, msl_frame->data_bcc); - } -} - -/* ---- 帧探测 ---- */ -const static uint8_t frame_head[2] = {0x5a, 0x5a}; -proto_probe_sta_t msl_probe_frame(const channel_t *ch, const ring_buffer_t *buff, uint32_t *total_len, uint8_t *aux) -{ - uint32_t avail = rb_avail(buff, nullptr); - if (avail < sizeof(msl_frame_t) + 4) - return PROTO_PROBE_WAIT; - - static uint8_t mem_pool[MSL_MSG_SIZE] = {0}; - memset(mem_pool, 0, sizeof(mem_pool)); - rb_peek(buff, 0, mem_pool, avail, nullptr); - msl_frame_t *frame = (msl_frame_t *)mem_pool; - - if (memcmp(frame_head, frame->head, sizeof(frame->head))) - return PROTO_PROBE_FAKE; - - // 当地址不为0时,则检测地址是否匹配 - if ((frame->addr != 0) && (frame->addr != msl_addr)) - return PROTO_PROBE_FAKE; - - uint16_t data_len = (frame->length[1] & 0xFF) | ((frame->length[0] << 8) & 0xFF00); - - // bcc校验 - uint8_t frame_bcc = (frame->data_bcc)[data_len]; - uint8_t calc_bcc = bcc_calcu(&(frame->addr), data_len + sizeof(msl_frame_t) - 2); - if (frame_bcc != calc_bcc) - return PROTO_PROBE_FAKE; - - (void)*aux; - *total_len = data_len + sizeof(msl_frame_t) + 1; - return PROTO_PROBE_READY; -} - -/* ---- 协议自注册 ---- */ -[[maybe_unused]] static void msl_module_init(void) -{ - // 通过拨码开关检测 - msl_addr |= (app_key_get_state(DEV_KEY_DIP1)) & 0b01; - msl_addr |= (app_key_get_state(DEV_KEY_DIP2) << 1) & 0b10; - - // 指定协议使用的环形缓冲区 - ring_buffer_t *rb = app_proto_acquire_buf(1, 2048); - - // 注册协议到多通道多协议解析模块 - s_msl_mask = app_proto_register(msl_probe_frame, rb); - if (s_msl_mask == 0) - return; - - // 绑定协议使用到的通道 - app_proto_bind_channel(s_msl_mask, CH_ID_RS485); - - g_msl_task_handle = osThreadNew(msl_handle_task, nullptr, &msl_task_attr); -} -sw_app_initcall(msl_module_init); diff --git a/Application/Src/MSL/app_msl_cmd.c b/Application/Src/MSL/app_msl_cmd.c deleted file mode 100644 index a33d38b..0000000 --- a/Application/Src/MSL/app_msl_cmd.c +++ /dev/null @@ -1,97 +0,0 @@ -#include "app_msl_cmd.h" - -#include "app_render.h" -#include "app_light_sensor.h" - -static void cmd_test(channel_t *ch, void *data); -static void cmd_text(channel_t *ch, void *data); -static void cmd_bitmap(channel_t *ch, void *data); -static void cmd_fill(channel_t *ch, void *data); -static void cmd_lightlevel(channel_t *ch, void *data); - -const msl_cmd_handler_fn_t g_msl_cmd_table[] = { - cmd_test, - cmd_text, - cmd_bitmap, - cmd_fill, - cmd_lightlevel, -}; - -[[maybe_unused]] static void cmd_test(channel_t *ch, void *data) -{ - (void)ch; - (void)data; -} - -/** - * @brief 同步显示,文字同步 - */ -static void cmd_text(channel_t *ch, void *data) -{ - msl_text_t *ctx = data; - - app_render(&(render_cfg_t){ - .type = RENDER_TEXT, - .x = (ctx->x[1] & 0xFF) | ((ctx->x[0] << 8) & 0xFF00), - .y = (ctx->y[1] & 0xFF) | ((ctx->y[0] << 8) & 0xFF00), - .w = dev_display_get()->screen_rows, - .h = dev_display_get()->screen_cols, - .color = ctx->color, - .style = &(render_style_t){ - .h_align = ctx->col_style, - .v_align = ctx->row_style, - .word_wrap = ctx->wrap, - }, - .color = ctx->color, - .text = (char *)(ctx->text), - .len = strlen((char *)(ctx->text)), - .font_size = ctx->font_size, - .font_type = ctx->font_type, - .text_enc = ctx->encode, - }); - if (ctx->pers) - app_render_save(); -} - -/** - * @brief 同步显示,位图同步 - */ -static void cmd_bitmap(channel_t *ch, void *data) -{ - msl_bitmap_t *ctx = data; - - app_render(&(render_cfg_t){ - .type = RENDER_BITMAP, - .x = 0, - .y = 0, - .w = ctx->width, - .h = ctx->high, - .color = ctx->color, - .bitmap = ctx->bitmap, - }); - if (ctx->pers) - app_render_save(); -} - -/** - * @brief 手动指定亮度等级 - */ -static void cmd_fill(channel_t *ch, void *data) -{ - app_render(&(render_cfg_t){ - .type = RENDER_FILL, - .x = 0, - .y = 0, - .w = 0, - .h = 0, - .color = *((display_color_t *)data), - }); -} - -/** - * @brief 手动指定亮度等级 - */ -static void cmd_lightlevel(channel_t *ch, void *data) -{ - app_light_sensor_get()->display->light_level = *(uint8_t *)data; -} diff --git a/Application/Src/ProtocolParser_ChongQing/app_cq_proto.c b/Application/Src/ProtocolParser_ChongQing/app_cq_proto.c new file mode 100644 index 0000000..e6cc8be --- /dev/null +++ b/Application/Src/ProtocolParser_ChongQing/app_cq_proto.c @@ -0,0 +1,355 @@ +/** + * @file app_cq_proto.c + * @brief 重庆高速二代费显协议(CQ)模块 — 注册 / 探测 / 处理 / 定时 + * + * 帧形态:JSON('{' 花括号深度定界,上限 1044B)+ 固定 12B 二进制 + * (重启请求 / 搜索请求,CRC16-XMODEM 大端)。协议版本 2.0.1。 + * + * 通道:业务口 CH_ID_UDP_CQ(PROTO_CHONGQING 读 Sector1 net_cfg.port,默认 + * 20103;dev 共存构建固定 20103,见 app_udp.c _udp_cq_read_port)+ + * 搜索口 CH_ID_UDP(10011 固定,与创迪发现口 / IAP 共享)。 + * 注册五步(doc/08-01):RJ45 RB provide → acquire → register(两次,两逻辑 + * 通道各一 mask)→ 静态队列(两 mask 共用)→ 处理 + 定时双任务。 + * + * 心跳:1s 定时任务递增 s_cq_sync_counter;宽松语义(B.7.2)——帧结构合法的 + * JSON(合法命令 / 命令未知 / 参数非法)均视为上位机存活并清零,仅 + * CQ_PARSE_ERR_FRAME 与 BIN 帧(重启/搜索)不复位;≥120s 且工厂测试未激活 → + * 渲染故障屏后清零;存活帧到达时若故障屏已显示,先恢复默认画面( + * app_default_display_show)再执行本帧命令。工厂测试激活期间暂停计数。 + * + * probe 注册序:Makefile 收录序 CQ 位于 LDI 之后(initcall 同层按链接序执行), + * LDI probe 先于 CQ probe。全协议 dev 构建下 10011 口 12B 二进制帧先过 LDI + * probe——CQ 12B 帧 len=00 00 00 02 且 CRC16-XMODEM 恰好通过 LDI 校验, + * 2026-08-21 起 LDI probe 对 len==2 显式 FAKE 放行,帧仍由 CQ probe 认领 + * (「LDI 先探测、FAKE 放行、CQ 收单」,无功能受限;修复前 LDI 曾沿身份 + * 校验路径 SKIP 吞掉 CQ 搜索/重启帧,见 doc/03 PartB Q23)。 + */ + +#include "app_cq_proto.h" +#include "app_cq_proto_parse.h" +#include "app_cq_proto_cmd.h" + +#include + +#include "FreeRTOS.h" +#include "task.h" /* taskENTER_CRITICAL/taskEXIT_CRITICAL */ +#include "cJSON.h" +#include "initcall.h" +#include "app_default_display.h" +#include "app_factory_test.h" +#include "dev_io_ctrl.h" + +/* RJ45 物理通道 RB:与 IAP / LDI / MQTT 等同槽 weak 合并 */ +RB_PROVIDE_WEAK(rb_provide_rj45, RB_SIZE_RJ45); + +/* ---- 12B 二进制帧常量(CRC16-XMODEM 大端,值按协议文档 2.0.1 固定)---- */ +const uint8_t cq_bin_reboot_req[12] = {0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x07, 0x31, 0x5B, 0xC9}; +const uint8_t cq_bin_search_req[12] = {0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x05, 0x00, 0x48, 0x73}; + +/* ---- cJSON 内存钩子:绑定 FreeRTOS 堆(pvPortMalloc / vPortFree)---- */ +static cJSON_Hooks s_cq_json_hooks = { + .malloc_fn = pvPortMalloc, + .free_fn = vPortFree, +}; + +/* ---- 协议掩码(每逻辑通道独立 mask)---- */ +static proto_mask_t s_cq_mask_udp_cq; /* CH_ID_UDP_CQ 业务口 20103 */ +static proto_mask_t s_cq_mask_udp; /* CH_ID_UDP 搜索口 10011 */ +static osMessageQueueId_t s_cq_queue; + +/* ---- 静态队列:payload 1044 ≤ RB_SIZE_RJ45(1536) ---- + * 队列体与任务帧缓冲置于 CCMRAM(CPU 独占访问、无 DMA,osMessageQueue 静态内存 + * 仅经 CPU memcpy 读写,安全):SRAM 全协议构建仅余 ~1.9KB,CQ 队列 3236B 无法 + * 入 SRAM(doc/06-04 记账注明该例外)。 */ +_Static_assert(CQ_PAYLOAD_MAX <= RB_SIZE_RJ45, "CQ frame must fit RJ45 RB"); +static StaticQueue_t s_cq_queue_cb __attribute__((section(".ccmram"))); +static uint8_t s_cq_queue_buf[CQ_QUEUE_DEPTH * CQ_MSG_SIZE] __attribute__((section(".ccmram"))); +static const osMessageQueueAttr_t s_cq_queue_attr = { + .name = "cq_queue", + .cb_mem = &s_cq_queue_cb, + .cb_size = sizeof(s_cq_queue_cb), + .mq_mem = s_cq_queue_buf, + .mq_size = sizeof(s_cq_queue_buf), +}; + +/* ---- 模块状态(心跳计数 / 黄闪倒计时 / 故障屏标志)---- + * 三者跨 timer_task/handle_task 共享;M4 对齐 u32 单指令原子,但读写-判断 + * 序列用临界区保护(递增/递减/置值见下方 taskENTER_CRITICAL 包裹,阈值 + * 判断在临界区外做——值本身 volatile 保证读取可见性)。 */ +static volatile uint32_t s_cq_sync_counter; /* 距上次上位机存活帧的秒数 */ +static volatile int32_t s_cq_warn_seconds; /* >0 倒计时 / -1 常开 / 0 关 */ +static volatile bool s_cq_fault_shown; /* 故障屏已渲染:下帧存活信息先恢复默认画面 */ + +static proto_probe_sta_t cq_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux); + +/** + * @brief CQ 协议模块初始化入口(sw_app_initcall 自注册)。 + * @note cJSON 内存钩子换绑 FreeRTOS 堆必须在处理任务创建之前完成。 + * @note 网络配置应用(netif + 默认落盘)已移交中立模块 app_net_boot + * (app_boot.c init_task 调 app_net_boot_apply,2026-08-21 解耦), + * 本模块不再承担网络启动职责;CQ UDP 业务口仍由 _udp_cq_read_port + * 读 Sector1 net_cfg.port(现状保持)。 + */ +void cq_proto_init(void) +{ + /* cJSON 钩子:parse 只发生在处理任务内,此处先行换绑 */ + cJSON_InitHooks(&s_cq_json_hooks); + + /* 第 2 步:取 RJ45 RB(weak 提供者缺位即放弃注册) */ + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RJ45, RB_SIZE_RJ45); + if (rb == nullptr) + return; + + /* 第 3 步:两逻辑通道各注册一次拿独立 mask */ + s_cq_mask_udp_cq = app_proto_register(cq_probe_frame, rb); + if (s_cq_mask_udp_cq == 0) + return; + app_proto_bind_channel(s_cq_mask_udp_cq, CH_ID_UDP_CQ); + + s_cq_mask_udp = app_proto_register(cq_probe_frame, rb); + if (s_cq_mask_udp != 0) + app_proto_bind_channel(s_cq_mask_udp, CH_ID_UDP); + + /* 第 4 步:静态队列(两 mask 共用同一队列) */ + s_cq_queue = osMessageQueueNew(CQ_QUEUE_DEPTH, CQ_MSG_SIZE, &s_cq_queue_attr); + app_proto_set_frame_queue(s_cq_mask_udp_cq, s_cq_queue); + if (s_cq_mask_udp != 0) + app_proto_set_frame_queue(s_cq_mask_udp, s_cq_queue); + + /* 第 5 步:处理任务 + 1s 定时任务 */ + static const osThreadAttr_t s_cq_handle_attr = { + .name = "cq_handle_task", + .stack_size = 256 * 4, /* 帧缓冲为 static,不放大栈 */ + .priority = osPriorityNormal, + }; + static const osThreadAttr_t s_cq_timer_attr = { + .name = "cq_timer_task", + .stack_size = 256 * 4, + .priority = osPriorityNormal, + }; + osThreadNew(cq_proto_handle_task, NULL, &s_cq_handle_attr); + osThreadNew(cq_proto_timer_task, NULL, &s_cq_timer_attr); +} +sw_app_initcall(cq_proto_init); + +/** + * @brief CQ 帧探测(PURE 契约:只 rb_peek,零副作用;avail==0 必返 FAKE)。 + * + * 首字节 '{':花括号深度扫描定界——'"' 内跳过、深度>16 或累计长度>1044 → FAKE; + * 深度归零 → READY;扫描到 avail 末尾未归零 → WAIT。 + * 已知限制(与裸机同脆弱,接受):GB2312 低字节域 0x40~0xFE 含 '{'(0x7B)/'}'(0x7D), + * 文本字段内 GBK 字符尾字节为花括号值时会破坏深度计数;JSON 转义 '\"' 未按转义 + * 处理('\' 后紧跟 '"' 会使字符串态提前退出)。 + * 首字节 0xFF:12 字节精确比对重启 / 搜索两帧 → READY;不足 12 字节 → WAIT; + * 前缀不匹配 → FAKE。 + * 其他首字节 → FAKE。 + */ +static proto_probe_sta_t cq_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux) +{ + (void)ch; + (void)aux; + uint32_t avail = rb_avail(rb, NULL); + if (avail == 0) + return PROTO_PROBE_FAKE; + + uint8_t first; + rb_peek(rb, 0, &first, 1, NULL); + + if (first == '{') { + /* JSON:花括号深度扫描。probe 栈受限(frame_dispatch_task 1KB), + * 分 64B 小块 peek,不放大栈帧。 */ + uint8_t buf[64]; + uint32_t pos = 0; + uint32_t depth = 0; + uint32_t scanned = 0; + bool in_str = false; + + while (pos < avail) { + uint32_t n = avail - pos; + if (n > sizeof(buf)) + n = sizeof(buf); + rb_peek(rb, (uint16_t)pos, buf, (uint16_t)n, NULL); + for (uint32_t i = 0; i < n; i++) { + uint8_t b = buf[i]; + if (in_str) { + if (b == '"') + in_str = false; + } else if (b == '"') { + in_str = true; + } else if (b == '{') { + depth++; + if (depth > 16) + return PROTO_PROBE_FAKE; + } else if (b == '}') { + if (depth == 0) + return PROTO_PROBE_FAKE; /* 无匹配 '{' 的孤立 '}' */ + depth--; + if (depth == 0) { + *total_len = pos + i + 1U; + if (*total_len > CQ_PAYLOAD_MAX) + return PROTO_PROBE_FAKE; + return PROTO_PROBE_READY; + } + } + scanned++; + if (scanned > CQ_PAYLOAD_MAX) + return PROTO_PROBE_FAKE; + } + pos += n; + } + return PROTO_PROBE_WAIT; /* 扫描到 avail 末尾未归零,等更多字节 */ + } + + if (first == 0xFF) { + /* 12B 二进制:精确匹配重启 / 搜索两帧 */ + if (avail < 12U) + return PROTO_PROBE_WAIT; + uint8_t buf[12]; + rb_peek(rb, 0, buf, sizeof(buf), NULL); + if (memcmp(buf, cq_bin_reboot_req, sizeof(buf)) == 0 || + memcmp(buf, cq_bin_search_req, sizeof(buf)) == 0) { + *total_len = 12; + return PROTO_PROBE_READY; + } + return PROTO_PROBE_FAKE; /* 前缀不匹配 */ + } + + return PROTO_PROBE_FAKE; +} + +/** + * @brief CQ 帧处理任务:判别 JSON / BIN,JSON 查表分派执行。 + * + * 宽松心跳语义(doc/03 PartB B.7.2):帧结构合法的 JSON——命令未知 + * (CQ_PARSE_ERR_CMD)/ 参数非法(CQ_PARSE_ERR_PARAM)与合法命令同样视为 + * 上位机存活并复位心跳;仅 CQ_PARSE_ERR_FRAME 静默丢弃且继续计数。BIN 帧 + * (重启/搜索)不复位。存活帧到达时若故障屏已显示,先恢复默认画面。 + */ +void cq_proto_handle_task(void *argument) +{ + (void)argument; + /* 帧接收缓冲置 CCMRAM(CPU 独占访问,无 DMA) */ + static uint8_t msg_buf[CQ_MSG_SIZE] __attribute__((section(".ccmram"))); + frame_msg_t *msg = (frame_msg_t *)msg_buf; + + for (;;) { + if (osOK != osMessageQueueGet(s_cq_queue, msg, NULL, osWaitForever)) + continue; + + cq_parsed_cmd_t cmd = cq_parse_frame(msg->data, msg->data_len); + if (cmd.sta != CQ_PARSE_OK) { + /* 宽松心跳语义(doc/03 PartB B.7.2):帧结构合法的 JSON(命令未知/ + * 参数非法)仍视为上位机存活——只不计入超时、不执行;仅 ERR_FRAME + * 静默丢弃且计数(JSON 无应答帧约定)。 */ + if (cmd.sta != CQ_PARSE_ERR_FRAME) { + /* 故障屏恢复:存活信息到达,先恢复默认画面再清标志、复位心跳 */ + if (s_cq_fault_shown) { + app_default_display_show(); + s_cq_fault_shown = false; + } + cq_proto_sync_reset(); + } + continue; + } + + /* 故障屏恢复(仅 JSON 存活命令;BIN 帧不复位心跳也不触发恢复): + * 先恢复默认画面、再执行本帧命令——text1/full 等随后覆盖渲染, + * 同帧同步提交(app_render 内 commit)不闪烁。 */ + if (cmd.cmd != CQ_PCMD_BIN_REBOOT && cmd.cmd != CQ_PCMD_BIN_SEARCH && + s_cq_fault_shown) { + app_default_display_show(); + s_cq_fault_shown = false; + } + + cq_execute_cmd(msg->ch, &cmd); + + /* 任意合法 JSON 命令成功解析即清心跳(二进制帧不复位:重启帧自身复位, + * 搜索帧非业务心跳) */ + if (cmd.cmd != CQ_PCMD_BIN_REBOOT && cmd.cmd != CQ_PCMD_BIN_SEARCH) + cq_proto_sync_reset(); + } +} + +/** + * @brief 1s 定时任务:心跳计数、warn1 倒计时(归零关黄闪)、120s 故障屏。 + * + * 心跳计数与故障屏渲染受编译开关 CQ_FAULT_SCREEN 控制 + * (app_cq_proto.h):PROTO_CHONGQING 默认开启、共存/其他构建默认关闭 + * (他省上位机不发重庆 syn1,关闭避免误触发故障屏),可用 + * -DCQ_FAULT_SCREEN=1/0 强制覆盖。 + * warn1 倒计时不受工厂测试影响:移植口径按裸机 hsCnt 与 SyncTimeOutCounter + * 同 ISR 递减、testMode 仅屏蔽故障屏渲染对齐,倒计时在工厂测试激活期间 + * 继续递减(黄闪到期即关,避免测试残留常亮);裸机源码不在本仓库,无法 + * 逐行核对,行为以本注释与 doc/03 PartB Q17 为准。 + * 心跳计数在工厂测试激活期间暂停(STD 口径,doc/03 PartB Q17):测试 + * 占用显示资源期间不累积超时,退出测试后重新从 0 计。 + */ +void cq_proto_timer_task(void *argument) +{ + (void)argument; + for (;;) { + osDelay(1000); + + /* warn1 倒计时:>0 每秒递减,归零关黄闪;-1 常开不自动关。 + * 位于工厂测试判定之前:测试期间不暂停(对齐裸机同 ISR 口径)。 + * 阈值判断在临界区外(volatile 读),递减与 handle_task 侧 + * cq_proto_warn_set 的写并发 → 读-改-写序列用临界区保护。 */ + if (s_cq_warn_seconds > 0) { + taskENTER_CRITICAL(); + s_cq_warn_seconds--; + taskEXIT_CRITICAL(); + if (s_cq_warn_seconds == 0) + dev_io_flash_light(false); + } + +#if CQ_FAULT_SCREEN + /* 工厂测试激活期间暂停心跳计数 */ + if (app_factory_test_active()) + continue; + + taskENTER_CRITICAL(); + s_cq_sync_counter++; + taskEXIT_CRITICAL(); + if (s_cq_sync_counter >= CQ_SYNC_FAULT_S) { + s_cq_sync_counter = 0; /* 渲染后清零,不重复刷屏 */ + s_cq_fault_shown = true; + cq_render_fault_screen(); + } +#else + /* 故障屏开关关闭(CQ_FAULT_SCREEN=0,app_cq_proto.h): + * 通用版固件多协议共存时,他省上位机不发重庆 syn1 心跳,120s + * 超时故障屏会误触发——关闭后不计数、不渲染,s_cq_sync_counter + * 恒 0、s_cq_fault_shown 恒 false(handle_task 侧 + * cq_proto_sync_reset 清零无副作用)。强制开启: + * -DCQ_FAULT_SCREEN=1。 */ +#endif + } +} + +/* ---- 心跳计数器 / 黄闪倒计时访问(命令执行层调用)---- + * 写侧与 timer_task 的递增/递减并发 → 修改用临界区保护(任务上下文安全)。 */ +void cq_proto_sync_reset(void) +{ + taskENTER_CRITICAL(); + s_cq_sync_counter = 0; + taskEXIT_CRITICAL(); +} + +void cq_proto_warn_set(int32_t seconds) +{ + taskENTER_CRITICAL(); + s_cq_warn_seconds = seconds; + taskEXIT_CRITICAL(); + + if (seconds > 0) { + dev_io_flash_light(true); + } else if (seconds == 0) { + dev_io_flash_light(false); + } else { + /* -1 常开:映射 0xFFFF 语义(协议文档 warn1 的 level 参数未实现, + * 本实现同样忽略,仅注释说明) */ + dev_io_flash_light(true); + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_ChongQing/app_cq_proto_cmd.c b/Application/Src/ProtocolParser_ChongQing/app_cq_proto_cmd.c new file mode 100644 index 0000000..c884e47 --- /dev/null +++ b/Application/Src/ProtocolParser_ChongQing/app_cq_proto_cmd.c @@ -0,0 +1,581 @@ +/** + * @file app_cq_proto_cmd.c + * @brief 重庆高速二代费显协议(CQ)命令执行 + * + * 执行层直接调用 STD 设备接口(doc/08 惯例,无服务层): + * - 文本/全屏/位图 → app_render + dev_display_commit_frame + * - 亮度 → 光敏任务挂起/恢复 + dev_display_set_brightness(对齐贵州 '8' + * 命令方式) + * - 语音 → dev_rs232_voice 旁路 USART6(语音板 FD 帧,'|' 组合符原样透传) + * - 黄闪 → dev_io_flash_light(倒计时在 timer_task 递减) + * - setip → app_board_net_cfg_update 落盘 Sector1 + NVIC_SystemReset 重启生效 + * - 二进制重启/搜索应答 → channel_send 回源 / app_udp_broadcast(10011) + * JSON 命令无应答帧。 + */ + +#include "app_cq_proto_cmd.h" + +#include + +#include "app_board_net_cfg.h" +#include "app_light_sensor.h" +#include "app_render.h" +#include "app_udp.h" +#include "cmsis_os2.h" +#include "crc_utils.h" +#include "dev_display.h" +#include "dev_io_ctrl.h" +#include "dev_rs232_voice.h" +#include "stm32f4xx_hal.h" + +/* ---- 32×32 通行灯位图(每行 4B MSB-first,与 RENDER_BITMAP 格式一致;共 + * 128B)---- */ + +/* 绿箭(turn="g") */ +static const uint8_t s_cq_bmp_green_arrow[128] = { + 0x10, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, + 0xFE, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x08, + 0x1F, 0xC0, 0x00, 0x0C, 0x0F, 0xE0, 0x00, 0x0E, 0x07, 0xF0, 0x00, 0x0F, + 0x03, 0xF8, 0x00, 0x0F, 0x01, 0xFC, 0x00, 0x0F, 0x00, 0xFE, 0x00, 0x0F, + 0x00, 0x7F, 0x00, 0x0F, 0x00, 0x3F, 0x80, 0x0F, 0x00, 0x1F, 0xC0, 0x0F, + 0x00, 0x0F, 0xE0, 0x0F, 0x00, 0x07, 0xF0, 0x0F, 0x00, 0x03, 0xF8, 0x0F, + 0x00, 0x01, 0xFC, 0x0F, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0x7C, 0x0F, + 0x00, 0x00, 0x38, 0x0F, 0x00, 0x00, 0x10, 0x0F, 0x00, 0x00, 0x00, 0x0F, + 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x0F, + 0x00, 0x00, 0x00, 0x0F, 0x07, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, + 0x01, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, +}; + +/* 红叉(turn="r") */ +static const uint8_t s_cq_bmp_red_cross[128] = { + 0x10, 0x00, 0x00, 0x08, 0x38, 0x00, 0x00, 0x1C, 0x7C, 0x00, 0x00, 0x3E, + 0xFE, 0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0xFE, 0x3F, 0x80, 0x01, 0xFC, + 0x1F, 0xC0, 0x03, 0xF8, 0x0F, 0xE0, 0x07, 0xF0, 0x07, 0xF0, 0x0F, 0xE0, + 0x03, 0xF8, 0x1F, 0xC0, 0x01, 0xFC, 0x3F, 0x80, 0x00, 0xFE, 0x7F, 0x00, + 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x1F, 0xF8, 0x00, + 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x1F, 0xF8, 0x00, + 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xFE, 0x7F, 0x00, + 0x01, 0xFC, 0x3F, 0x80, 0x03, 0xF8, 0x1F, 0xC0, 0x07, 0xF0, 0x0F, 0xE0, + 0x0F, 0xE0, 0x07, 0xF0, 0x1F, 0xC0, 0x03, 0xF8, 0x3F, 0x80, 0x01, 0xFC, + 0x7F, 0x00, 0x00, 0xFE, 0xFE, 0x00, 0x00, 0x7F, 0x7C, 0x00, 0x00, 0x3E, + 0x38, 0x00, 0x00, 0x1C, 0x10, 0x00, 0x00, 0x08, +}; + +/* ---- 12B 二进制应答帧(CRC16-XMODEM 大端,值按协议文档 2.0.1 固定)---- */ +static const uint8_t s_cq_bin_reboot_rsp[12] = { + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xA7, 0x31, 0xF4, 0x36}; + +/* ---- screen 命令记录(仅记录,不写 Flash、不切模组)---- */ +static int s_cq_screen_led = -1; + +/* ---- 心跳故障屏文案 ---- */ +static const char s_cq_fault_line0[] = "车道关闭"; +static const char s_cq_fault_line2[] = "请择道行驶!"; + +typedef void (*cq_cmd_fn_t)(channel_t *ch, const cq_parsed_cmd_t *cmd); + +/* ---- 命令处理函数前向声明(命令分发表定义于实现之前)---- */ +static void _cq_exec_text1(channel_t *ch, const cq_parsed_cmd_t *cmd); +static void _cq_exec_tra1(channel_t *ch, const cq_parsed_cmd_t *cmd); +static void _cq_exec_pic1(channel_t *ch, const cq_parsed_cmd_t *cmd); +static void _cq_exec_light1(channel_t *ch, const cq_parsed_cmd_t *cmd); +static void _cq_exec_voice_ctrl1(channel_t *ch, const cq_parsed_cmd_t *cmd); +static void _cq_exec_voice1(channel_t *ch, const cq_parsed_cmd_t *cmd); +static void _cq_exec_voice_play1(channel_t *ch, const cq_parsed_cmd_t *cmd); +static void _cq_exec_warn1(channel_t *ch, const cq_parsed_cmd_t *cmd); +static void _cq_exec_syn1(channel_t *ch, const cq_parsed_cmd_t *cmd); +static void _cq_exec_setip(channel_t *ch, const cq_parsed_cmd_t *cmd); +static void _cq_exec_screen(channel_t *ch, const cq_parsed_cmd_t *cmd); +static void _cq_exec_full(channel_t *ch, const cq_parsed_cmd_t *cmd); + +/* ---- 命令分发表(JSON 命令名 → 处理函数)---- */ +static const struct { + cq_pcmd_t cmd; + cq_cmd_fn_t fn; +} g_cq_cmd_table[] = { + {CQ_PCMD_TEXT1, _cq_exec_text1}, + {CQ_PCMD_TRA1, _cq_exec_tra1}, + {CQ_PCMD_PIC1, _cq_exec_pic1}, + {CQ_PCMD_LIGHT1, _cq_exec_light1}, + {CQ_PCMD_VOICE_CONTROL1, _cq_exec_voice_ctrl1}, + {CQ_PCMD_VOICE1, _cq_exec_voice1}, + {CQ_PCMD_VOICE_PLAY1, _cq_exec_voice_play1}, + {CQ_PCMD_WARN1, _cq_exec_warn1}, + {CQ_PCMD_SYN1, _cq_exec_syn1}, + {CQ_PCMD_SETIP, _cq_exec_setip}, + {CQ_PCMD_SCREEN, _cq_exec_screen}, + {CQ_PCMD_FULL, _cq_exec_full}, +}; + +/** + * @brief 协议颜色 0红/1绿/2黄 → 显示颜色;越界回退绿。 + */ +static display_color_t _cq_map_color(uint8_t idx) { + switch (idx) { + case 0: + return COLOR_RED; + case 1: + return COLOR_GREEN; + case 2: + return COLOR_YELLOW; + default: + return COLOR_GREEN; + } +} + +/** + * @brief 渲染单行 GB2312 文本:先清该行矩形(RENDER_FILL 黑), + * 再按像素宽截断(ASCII=字号/2 px、GBK=字号 px)到屏宽渲染。 + * @param text GB2312 字节文本。 + * @param len 文本字节数。 + * @param y 行起点 y。 + * @param font 字号像素(16/24/32)。 + * @param color 显示颜色。 + */ +static void _cq_render_gbk_line(const char *text, uint16_t len, uint16_t y, + uint16_t font, display_color_t color) { + dev_display_t *d = dev_display_get(); + if (d == NULL) + return; + + /* 先清该行(矩形黑色填充) */ + app_render(&(render_cfg_t){ + .type = RENDER_FILL, + .x = 0, + .y = y, + .w = d->screen_rows, + .h = font, + .color = COLOR_BLACK, + }); + + if (text == NULL || len == 0U) + return; + + /* 字符宽度截断:按像素宽累计截断到屏宽 */ + uint16_t px = 0; + uint16_t i = 0; + while (i < len) { + uint8_t b = (uint8_t)text[i]; + bool gbk = (b >= 0x81U && b <= 0xFEU); + if (gbk && i + 1U >= len) + break; /* 半个 GBK 尾字节:不渲染残缺字符 */ + uint16_t w = gbk ? font : (uint16_t)(font / 2U); + if ((uint32_t)px + w > d->screen_rows) + break; + px += w; + i += gbk ? 2U : 1U; + } + if (i == 0) + return; + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = y, + .w = d->screen_rows, + .h = font, + .style = + &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = false, + }, + .color = color, + .text = text, + .len = i, + .font_size = (font_size_t)font, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief text1:scr==0 先全屏清黑;ln0~ln7 逐行渲染 + * (行号限制:ln4 字号≤24,ln5~ln7 字号≤16;行超出屏高跳过)。 + */ +static void _cq_exec_text1(channel_t *ch, const cq_parsed_cmd_t *cmd) { + (void)ch; + dev_display_t *d = dev_display_get(); + if (d == NULL) + return; + + if (cmd->p.text1.scr_clear) { + app_render(&(render_cfg_t){ + .type = RENDER_FILL, + .x = 0, + .y = 0, + .w = 0, + .h = 0, + .color = COLOR_BLACK, + }); + } + + for (uint8_t i = 0; i < 8; i++) { + const cq_line_t *L = &cmd->p.text1.ln[i]; + if (!L->valid) + continue; + /* 行号限制:ln4 仅字号≤24;ln5~ln7 仅≤16,超限跳过该行 */ + if (i == 4 && L->font > 24) + continue; + if (i >= 5 && L->font > 16) + continue; + uint16_t y = (uint16_t)i * L->font; + if ((uint32_t)y + L->font > d->screen_cols) + continue; /* 行超出屏高跳过 */ + _cq_render_gbk_line(L->text, L->text_len, y, L->font, + _cq_map_color(L->color)); + } + + dev_display_commit_frame(d); +} + +/** + * @brief tra1:右下角 32×32 红叉 / 绿箭位图(先清该区域再绘制)。 + */ +static void _cq_exec_tra1(channel_t *ch, const cq_parsed_cmd_t *cmd) { + (void)ch; + dev_display_t *d = dev_display_get(); + if (d == NULL) + return; + if (d->screen_rows < 32U || d->screen_cols < 32U) + return; + + uint16_t x = (uint16_t)(d->screen_rows - 32U); + uint16_t y = (uint16_t)(d->screen_cols - 32U); + + app_render(&(render_cfg_t){ + .type = RENDER_FILL, + .x = x, + .y = y, + .w = 32, + .h = 32, + .color = COLOR_BLACK, + }); + app_render(&(render_cfg_t){ + .type = RENDER_BITMAP, + .x = x, + .y = y, + .w = 32, + .h = 32, + .color = (cmd->p.tra1.turn == 1U) ? COLOR_GREEN : COLOR_RED, + .bitmap = + (cmd->p.tra1.turn == 1U) ? s_cq_bmp_green_arrow : s_cq_bmp_red_cross, + }); + dev_display_commit_frame(d); +} + +/** @brief pic1:stub(解析后忽略)。 */ +static void _cq_exec_pic1(channel_t *ch, const cq_parsed_cmd_t *cmd) { + (void)ch; + (void)cmd; +} + +/** + * @brief light1:0 → 恢复光敏自动调光;1~15 → 挂起光敏 + 亮度 level/2+1(上限 + * 8)。 + * @note 对齐贵州 '8' 命令实现方式。 + */ +static void _cq_exec_light1(channel_t *ch, const cq_parsed_cmd_t *cmd) { + (void)ch; + dev_display_t *d = dev_display_get(); + if (d == NULL) + return; + + int level = cmd->p.light1.level; + if (level == 0) { + if (g_light_sensor_task_handle != nullptr) + osThreadResume(g_light_sensor_task_handle); + return; + } + if (g_light_sensor_task_handle != nullptr) + osThreadSuspend(g_light_sensor_task_handle); /* 手动亮度 → 关自动调光 */ + uint8_t b = (uint8_t)(level / 2 + 1); + if (b > DEV_DISPLAY_BRIGHTNESS_MAX) + b = DEV_DISPLAY_BRIGHTNESS_MAX; + dev_display_set_brightness(d, b); +} + +/** @brief voice_control1:level/2+1(上限 7)→ dev_rs232_voice_volume。 */ +static void _cq_exec_voice_ctrl1(channel_t *ch, const cq_parsed_cmd_t *cmd) { + (void)ch; + int grade = cmd->p.voice_ctrl1.level / 2 + 1; + if (grade > 7) + grade = 7; + dev_rs232_voice_volume((uint8_t)grade); +} + +/** + * @brief voice1:t 文本(GB2312 字节,保留 '|' 组合符)→ + * dev_rs232_voice_play, 超过接口上限 200B 截断。 + */ +static void _cq_exec_voice1(channel_t *ch, const cq_parsed_cmd_t *cmd) { + (void)ch; + uint16_t len = cmd->p.voice1.text_len; + if (len > DEV_RS232_VOICE_MAX_TEXT) + len = DEV_RS232_VOICE_MAX_TEXT; + dev_rs232_voice_play((const uint8_t *)cmd->p.voice1.text, len); +} + +/** @brief voice_play1:stub(解析后忽略)。 */ +static void _cq_exec_voice_play1(channel_t *ch, const cq_parsed_cmd_t *cmd) { + (void)ch; + (void)cmd; +} + +/** + * @brief warn1:0 关黄闪并清倒计时;>0 开黄闪 + 倒计时(timer_task 每秒递减, + * 归零关);-1 常开(协议文档 level 参数未实现,忽略并注释说明)。 + */ +static void _cq_exec_warn1(channel_t *ch, const cq_parsed_cmd_t *cmd) { + (void)ch; + cq_proto_warn_set(cmd->p.warn1.second); +} + +/** @brief syn1:心跳帧,计数器清零由 handle_task 统一处理,此处无操作。 */ +static void _cq_exec_syn1(channel_t *ch, const cq_parsed_cmd_t *cmd) { + (void)ch; + (void)cmd; +} + +/** + * @brief setip:解析校验(parse 层)→ app_board_net_cfg_update 落盘 Sector1 + * → NVIC_SystemReset 重启生效(无应答,对齐 LDI 0AH 落盘语义)。 + * @note CQ 构建不含 LDI,仅写 Sector1;W25 镜像由 LDI + * 配置模块维护(未编入)。 + */ +static void _cq_exec_setip(channel_t *ch, const cq_parsed_cmd_t *cmd) { + (void)ch; + + /* 方案 B(2026-08-21):setip 写 CQ UDP 业务口(udp_port=命令下发端口), + * TCP 业务口 port 保留现值(get 失败用出厂默认 9528)——setip 不再污染 TCP + * 口。 落盘返回值忽略、无条件复位(写失败重启后回旧配置,与裸机一致, doc/03 + * PartB B.7.1 边界语义)。 */ + app_board_net_cfg_t cur_cfg; + uint32_t tcp_port = + (app_board_net_cfg_get(&cur_cfg) == 0) ? cur_cfg.port : 9528U; + (void)app_board_net_cfg_update(cmd->p.setip.ip, cmd->p.setip.mask, + cmd->p.setip.gw, tcp_port, cmd->p.setip.port); + NVIC_SystemReset(); +} + +/** @brief screen:仅记录到 static 变量,不写 + * Flash、不切模组(本次不含模组排布)。 */ +static void _cq_exec_screen(channel_t *ch, const cq_parsed_cmd_t *cmd) { + (void)ch; + s_cq_screen_led = cmd->p.screen.led; +} + +/** @brief 读取 screen 命令最近记录的 led 值(-1 = 未收到过)。 */ +int cq_screen_led_get(void) { return s_cq_screen_led; } + +/** + * @brief full:0红/1绿/2黄/3白 全屏 RENDER_FILL。 + * @note 3=白:红绿双基色屏无法物理显示纯白,映射红绿全亮即黄。 + */ +static void _cq_exec_full(channel_t *ch, const cq_parsed_cmd_t *cmd) { + (void)ch; + dev_display_t *d = dev_display_get(); + if (d == NULL) + return; + + display_color_t color; + switch (cmd->p.full.color) { + case 0: + color = COLOR_RED; + break; + case 1: + color = COLOR_GREEN; + break; + case 2: + color = COLOR_YELLOW; + break; + case 3: + color = COLOR_WHITE; + break; + default: + color = COLOR_BLACK; + break; + } + app_render(&(render_cfg_t){ + .type = RENDER_FILL, + .x = 0, + .y = 0, + .w = 0, + .h = 0, + .color = color, + }); + dev_display_commit_frame(d); +} + +/** + * @brief 12B 二进制重启请求:单播应答 12B 到源通道 → 延时 100ms → 软复位。 + */ +static void _cq_exec_bin_reboot(channel_t *ch) { + channel_send(ch, (uint8_t *)s_cq_bin_reboot_rsp, sizeof(s_cq_bin_reboot_rsp)); + osDelay(100); + NVIC_SystemReset(); +} + +/** + * @brief 12B 二进制搜索请求:广播 25B 应答(ip+mask+gw+port BE+CRC16-XMODEM + * 大端) 经 10011 搜索口(app_udp_broadcast)。 + * @note CRC 覆盖应答帧字节 [2..22](crc16_xmodem:poly 0x8408/init + * 0xFFFF/取反)。 + */ +static void _cq_exec_bin_search(void) { + app_board_net_cfg_t cfg; + uint8_t ip[4], mask[4], gw[4]; + uint16_t port; + + if (app_board_net_cfg_get(&cfg) == 0 && + (cfg.ip[0] | cfg.ip[1] | cfg.ip[2] | cfg.ip[3]) != 0U) { + memcpy(ip, cfg.ip, sizeof(ip)); + memcpy(mask, cfg.mask, sizeof(mask)); + memcpy(gw, cfg.gw, sizeof(gw)); + } else { + /* Sector1 空/损坏/非法 → 出厂默认 + * 192.168.1.5/255.255.255.0/192.168.1.1/20103 */ + static const uint8_t def_ip[4] = {192, 168, 1, 5}; + static const uint8_t def_mask[4] = {255, 255, 255, 0}; + static const uint8_t def_gw[4] = {192, 168, 1, 1}; + memcpy(ip, def_ip, sizeof(ip)); + memcpy(mask, def_mask, sizeof(mask)); + memcpy(gw, def_gw, sizeof(gw)); + } + + /* 端口上报 CQ 实际业务口(app_udp_cq_get_port)而非 Sector1 net_cfg.port: + * dev 共存构建下 net_cfg.port 属 LDI 语义(出厂 9528),直接上报会引导 + * 上位机连错口;PROTO_CHONGQING 下两者同源(_udp_cq_read_port 读同一记录)。 + */ + port = app_udp_cq_get_port(); + + uint8_t buf[25]; + buf[0] = 0xFF; + buf[1] = 0xFF; + buf[2] = 0x00; + buf[3] = 0x00; + buf[4] = 0x00; + buf[5] = 0x00; + buf[6] = 0x00; + buf[7] = 0x0F; + buf[8] = 0xA5; + memcpy(&buf[9], ip, 4); + memcpy(&buf[13], mask, 4); + memcpy(&buf[17], gw, 4); + buf[21] = (uint8_t)(port >> 8); /* 端口高字节在前 */ + buf[22] = (uint8_t)(port & 0xFFU); + uint16_t crc = crc16_xmodem(&buf[2], 21); /* 字节 [2..22] */ + buf[23] = (uint8_t)(crc >> 8); + buf[24] = (uint8_t)(crc & 0xFFU); + + app_udp_broadcast(buf, sizeof(buf)); +} + +/** + * @brief 渲染心跳超时故障屏:「车道关闭」32pt 红(行0)+「请择道行驶!」(行2) + * + 右下角红叉位图;行超出屏高跳过(小屏防御)。 + */ +void cq_render_fault_screen(void) { + dev_display_t *d = dev_display_get(); + if (d == NULL) + return; + + app_render(&(render_cfg_t){ + .type = RENDER_FILL, + .x = 0, + .y = 0, + .w = 0, + .h = 0, + .color = COLOR_BLACK, + }); + + /* 行0:车道关闭(32pt 红居中) */ + if (d->screen_cols >= 32U) { + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 0, + .w = d->screen_rows, + .h = 32, + .style = + &(render_style_t){ + .h_align = ALIGN_CENTER, + .v_align = ALIGN_CENTER, + .word_wrap = false, + }, + .color = COLOR_RED, + .text = s_cq_fault_line0, + .len = sizeof(s_cq_fault_line0) - 1U, + .font_size = FONT_32, + .font_type = FONT_ST, + .text_enc = FONT_ENC_UTF8, + }); + } + + /* 行2:请择道行驶!(行1 留空,对齐协议文档布局) */ + if (d->screen_cols >= 96U) { + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 64, + .w = d->screen_rows, + .h = 32, + .style = + &(render_style_t){ + .h_align = ALIGN_CENTER, + .v_align = ALIGN_CENTER, + .word_wrap = false, + }, + .color = COLOR_RED, + .text = s_cq_fault_line2, + .len = sizeof(s_cq_fault_line2) - 1U, + .font_size = FONT_32, + .font_type = FONT_ST, + .text_enc = FONT_ENC_UTF8, + }); + } + + /* 右下角红叉 */ + if (d->screen_rows >= 32U && d->screen_cols >= 32U) { + uint16_t x = (uint16_t)(d->screen_rows - 32U); + uint16_t y = (uint16_t)(d->screen_cols - 32U); + app_render(&(render_cfg_t){ + .type = RENDER_BITMAP, + .x = x, + .y = y, + .w = 32, + .h = 32, + .color = COLOR_RED, + .bitmap = s_cq_bmp_red_cross, + }); + } + + dev_display_commit_frame(d); +} + +/** + * @brief CQ 命令分发执行入口。 + */ +void cq_execute_cmd(channel_t *ch, const cq_parsed_cmd_t *cmd) { + if (cmd == NULL || cmd->sta != CQ_PARSE_OK) + return; + + /* 二进制帧独立处理 */ + if (cmd->cmd == CQ_PCMD_BIN_REBOOT) { + _cq_exec_bin_reboot(ch); + return; + } + if (cmd->cmd == CQ_PCMD_BIN_SEARCH) { + _cq_exec_bin_search(); + return; + } + + /* JSON 命令查表分派 */ + for (size_t i = 0; i < sizeof(g_cq_cmd_table) / sizeof(g_cq_cmd_table[0]); + i++) { + if (g_cq_cmd_table[i].cmd == cmd->cmd) { + g_cq_cmd_table[i].fn(ch, cmd); + return; + } + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_ChongQing/app_cq_proto_parse.c b/Application/Src/ProtocolParser_ChongQing/app_cq_proto_parse.c new file mode 100644 index 0000000..9c08268 --- /dev/null +++ b/Application/Src/ProtocolParser_ChongQing/app_cq_proto_parse.c @@ -0,0 +1,344 @@ +/** + * @file app_cq_proto_parse.c + * @brief 重庆高速二代费显协议(CQ)帧解析(纯解析:不渲染、不碰外设、不发包) + * + * JSON 帧:cJSON_Parse 建树(钩子已在 cq_proto_init 换绑 FreeRTOS 堆), + * 按 "cmd" 字段提取参数;文本字段(GB2312 字节)拷贝到内部静态缓冲后删树, + * 返回的 cq_parsed_cmd_t 指针在下次解析前有效(单消费者任务)。 + * 二进制帧:12 字节精确匹配重启 / 搜索两帧。 + */ + +#include "app_cq_proto_parse.h" +#include "cJSON.h" + +#include +#include + +/* ---- 类型安全提取宏(JSON 对象字段)---- + * cJSON 官方 v1.7.18:类型判定统一走 cJSON_IsString/IsNumber/IsObject 辅助函数; + * cJSON_GetObjectItem 入参为 const cJSON *。 */ +#define CQ_JSON_GET_STR(root, key) \ + ({ \ + cJSON *_cq_item = cJSON_GetObjectItem((root), (key)); \ + (_cq_item && cJSON_IsString(_cq_item)) ? _cq_item->valuestring : NULL; \ + }) + +#define CQ_JSON_GET_INT(root, key, default_val) \ + ({ \ + cJSON *_cq_item = cJSON_GetObjectItem((root), (key)); \ + (_cq_item && cJSON_IsNumber(_cq_item)) ? _cq_item->valueint \ + : (default_val); \ + }) + +#define CQ_JSON_GET_OBJ(root, key) \ + ({ \ + cJSON *_cq_item = cJSON_GetObjectItem((root), (key)); \ + (_cq_item && cJSON_IsObject(_cq_item)) ? _cq_item : NULL; \ + }) + +/* ---- 内部静态缓冲(单消费者任务,非重入;置 CCMRAM 省 SRAM,CPU 独占无 DMA)---- */ +static char s_cq_json_buf[CQ_PAYLOAD_MAX + 1U] __attribute__((section(".ccmram"))); /* NUL 结尾 JSON 拷贝 */ +static char s_cq_text_buf[CQ_PAYLOAD_MAX] __attribute__((section(".ccmram"))); /* 命令文本字段拷贝区 */ + +/* ---- 命令名 → 枚举 ---- */ +static const struct { + const char *name; + cq_pcmd_t cmd; +} s_cq_cmd_names[] = { + {"text1", CQ_PCMD_TEXT1}, + {"tra1", CQ_PCMD_TRA1}, + {"pic1", CQ_PCMD_PIC1}, + {"light1", CQ_PCMD_LIGHT1}, + {"voice_control1", CQ_PCMD_VOICE_CONTROL1}, + {"voice1", CQ_PCMD_VOICE1}, + {"voice_play1", CQ_PCMD_VOICE_PLAY1}, + {"warn1", CQ_PCMD_WARN1}, + {"syn1", CQ_PCMD_SYN1}, + {"setip", CQ_PCMD_SETIP}, + {"screen", CQ_PCMD_SCREEN}, + {"full", CQ_PCMD_FULL}, +}; + +/** + * @brief 命令名字符串 → 命令枚举。 + */ +static cq_pcmd_t _cq_lookup_cmd(const char *name) +{ + if (name == NULL) + return CQ_PCMD_INVALID; + for (size_t i = 0; i < sizeof(s_cq_cmd_names) / sizeof(s_cq_cmd_names[0]); i++) { + if (strcmp(name, s_cq_cmd_names[i].name) == 0) + return s_cq_cmd_names[i].cmd; + } + return CQ_PCMD_INVALID; +} + +/** + * @brief 点分十进制 IPv4 字符串 → 4 字节数组(严格校验:每段 0~255,恰好 4 段)。 + * @return true 解析成功。 + */ +static bool _cq_parse_ip4(const char *s, uint8_t out[4]) +{ + if (s == NULL) + return false; + for (int o = 0; o < 4; o++) { + uint32_t v = 0; + int digits = 0; + while (*s >= '0' && *s <= '9') { + v = v * 10U + (uint32_t)(*s - '0'); + if (v > 255U) + return false; + s++; + digits++; + } + if (digits == 0) + return false; + out[o] = (uint8_t)v; + if (o < 3) { + if (*s != '.') + return false; + s++; + } + } + return *s == '\0'; +} + +/** + * @brief 字号字符串 "16"/"24"/"32" → 像素值;非法回默认 24。 + */ +static uint8_t _cq_font_from_str(const char *s) +{ + if (s == NULL) + return 24; + if (strcmp(s, "16") == 0) + return 16; + if (strcmp(s, "24") == 0) + return 24; + if (strcmp(s, "32") == 0) + return 32; + return 24; /* 非法字号回默认 24 */ +} + +/** + * @brief 颜色字符串 "r"/"g"/"y" → 0红/1绿/2黄;非法回默认绿(1)。 + */ +static uint8_t _cq_color_from_str(const char *s) +{ + if (s == NULL) + return 1; + if (s[0] == 'r') + return 0; + if (s[0] == 'g') + return 1; + if (s[0] == 'y') + return 2; + return 1; /* 非法颜色回默认绿 */ +} + +/** + * @brief 拷贝文本到内部静态缓冲(越界即失败)。 + * @return 成功返回缓冲内指针,失败返回 NULL。 + */ +static char *_cq_text_dup(const char *src, size_t *used, size_t len) +{ + if (src == NULL || *used + len > sizeof(s_cq_text_buf)) + return NULL; + char *dst = &s_cq_text_buf[*used]; + memcpy(dst, src, len); + *used += len; + return dst; +} + +/** + * @brief 提取 text1:scr + ln0~ln7(s 字号 / c 颜色 / t 文本)。 + */ +static void _cq_parse_text1(cJSON *root, cq_parsed_cmd_t *cmd) +{ + cJSON *scr = cJSON_GetObjectItem(root, "scr"); + cmd->p.text1.scr_clear = (scr && cJSON_IsNumber(scr) && scr->valueint == 0); + + size_t used = 0; + for (uint8_t i = 0; i < 8; i++) { + char key[8]; + snprintf(key, sizeof(key), "ln%u", (unsigned)i); + cJSON *ln = CQ_JSON_GET_OBJ(root, key); + if (ln == NULL) + continue; + + const char *t = CQ_JSON_GET_STR(ln, "t"); + if (t == NULL) + continue; /* 无文本 → 该行跳过 */ + + size_t tlen = strlen(t); + char *dup = _cq_text_dup(t, &used, tlen); + if (dup == NULL) { + cmd->sta = CQ_PARSE_ERR_PARAM; /* 文本总量超限 */ + return; + } + cq_line_t *L = &cmd->p.text1.ln[i]; + L->valid = true; + L->font = _cq_font_from_str(CQ_JSON_GET_STR(ln, "s")); + L->color = _cq_color_from_str(CQ_JSON_GET_STR(ln, "c")); + L->text = dup; + L->text_len = (uint16_t)tlen; + } +} + +/** + * @brief 解析 CQ 原始帧。 + */ +cq_parsed_cmd_t cq_parse_frame(const uint8_t *raw, uint16_t raw_len) +{ + cq_parsed_cmd_t cmd = {0}; + cmd.sta = CQ_PARSE_ERR_FRAME; + if (raw == NULL || raw_len == 0U) + return cmd; + + /* ---- 12B 二进制帧精确判别 ---- */ + if (raw[0] == 0xFF) { + if (raw_len < 12U) + return cmd; + if (memcmp(raw, cq_bin_reboot_req, 12) == 0) { + cmd.cmd = CQ_PCMD_BIN_REBOOT; + cmd.sta = CQ_PARSE_OK; + } else if (memcmp(raw, cq_bin_search_req, 12) == 0) { + cmd.cmd = CQ_PCMD_BIN_SEARCH; + cmd.sta = CQ_PARSE_OK; + } + return cmd; + } + + /* ---- JSON 帧 ---- */ + if (raw[0] != '{' || raw_len > CQ_PAYLOAD_MAX) + return cmd; + + memcpy(s_cq_json_buf, raw, raw_len); + s_cq_json_buf[raw_len] = '\0'; + + cJSON *root = cJSON_Parse(s_cq_json_buf); + if (root == NULL) + return cmd; + + cJSON *cmd_item = cJSON_GetObjectItem(root, "cmd"); + if (cmd_item == NULL || !cJSON_IsString(cmd_item)) { + cJSON_Delete(root); + return cmd; + } + cmd.cmd = _cq_lookup_cmd(cmd_item->valuestring); + if (cmd.cmd == CQ_PCMD_INVALID) { + cJSON_Delete(root); + cmd.sta = CQ_PARSE_ERR_CMD; + return cmd; + } + + /* 各命令参数提取(默认 OK,非法置 ERR_PARAM) */ + cmd.sta = CQ_PARSE_OK; + switch (cmd.cmd) { + case CQ_PCMD_TEXT1: + _cq_parse_text1(root, &cmd); + break; + + case CQ_PCMD_TRA1: { + const char *turn = CQ_JSON_GET_STR(root, "turn"); + if (turn == NULL || (turn[0] != 'r' && turn[0] != 'g')) + cmd.sta = CQ_PARSE_ERR_PARAM; + else + cmd.p.tra1.turn = (turn[0] == 'g') ? 1U : 0U; + break; + } + + case CQ_PCMD_PIC1: /* stub:解析后忽略 */ + break; + + case CQ_PCMD_LIGHT1: { + int level = CQ_JSON_GET_INT(root, "level", -1); + if (level < 0 || level > 15) + cmd.sta = CQ_PARSE_ERR_PARAM; + else + cmd.p.light1.level = level; + break; + } + + case CQ_PCMD_VOICE_CONTROL1: { + int level = CQ_JSON_GET_INT(root, "level", -1); + if (level < 0 || level > 15) + cmd.sta = CQ_PARSE_ERR_PARAM; + else + cmd.p.voice_ctrl1.level = level; + break; + } + + case CQ_PCMD_VOICE1: { + const char *t = CQ_JSON_GET_STR(root, "t"); + if (t == NULL) { + cmd.sta = CQ_PARSE_ERR_PARAM; + break; + } + size_t tlen = strlen(t); + size_t used = 0; + char *dup = _cq_text_dup(t, &used, tlen); + if (dup == NULL || tlen > UINT16_MAX) { + cmd.sta = CQ_PARSE_ERR_PARAM; + break; + } + cmd.p.voice1.text = dup; + cmd.p.voice1.text_len = (uint16_t)tlen; + break; + } + + case CQ_PCMD_VOICE_PLAY1: /* stub:解析后忽略 */ + break; + + case CQ_PCMD_WARN1: { + int second = CQ_JSON_GET_INT(root, "second", -9999); + if (second < -1) + cmd.sta = CQ_PARSE_ERR_PARAM; + else + cmd.p.warn1.second = second; + break; + } + + case CQ_PCMD_SYN1: /* 心跳帧:无参数 */ + break; + + case CQ_PCMD_SETIP: { + const char *ip = CQ_JSON_GET_STR(root, "ip"); + const char *mask = CQ_JSON_GET_STR(root, "mask"); + const char *wg = CQ_JSON_GET_STR(root, "wg"); + int port = CQ_JSON_GET_INT(root, "port", -1); + if (!_cq_parse_ip4(ip, cmd.p.setip.ip) || + !_cq_parse_ip4(mask, cmd.p.setip.mask) || + !_cq_parse_ip4(wg, cmd.p.setip.gw) || + port < 1 || port > 65535) + cmd.sta = CQ_PARSE_ERR_PARAM; + else + cmd.p.setip.port = (uint32_t)port; + break; + } + + case CQ_PCMD_SCREEN: { + int led = CQ_JSON_GET_INT(root, "led", -1); + if (led < 0) + cmd.sta = CQ_PARSE_ERR_PARAM; + else + cmd.p.screen.led = led; + break; + } + + case CQ_PCMD_FULL: { + int color = CQ_JSON_GET_INT(root, "color", -1); + if (color < 0 || color > 3) + cmd.sta = CQ_PARSE_ERR_PARAM; + else + cmd.p.full.color = color; + break; + } + + default: + cmd.sta = CQ_PARSE_ERR_CMD; + break; + } + + cJSON_Delete(root); + return cmd; +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_GuiZhou/app_gz_proto.c b/Application/Src/ProtocolParser_GuiZhou/app_gz_proto.c new file mode 100644 index 0000000..cb96b16 --- /dev/null +++ b/Application/Src/ProtocolParser_GuiZhou/app_gz_proto.c @@ -0,0 +1,161 @@ +/** + * @file app_gz_proto.c + * @brief 贵州常规费显协议模块 — 注册 / 探测 / 处理任务 + * + * 帧格式:'{' + 命令字 + 二进制长度 + 参数 + '}',与青海协议同构。 + * 绑定 RS485 + RS232 双通道(与青海/山东同模式:acquire → register → bind → set_frame_queue)。 + * 帧头冲突:命令字 '1'~'9','A','B' 全部落入青海 probe 命令集, + * 全协议构建下青海 probe 先注册(字母序 qh < gz)先认领贵州帧; + * 量产由 EIDE 目录排除纪律保证贵州与青海互斥(doc/05-01 §6 兼容矩阵)。 + * 0x01/0x02 二进制命令字青海/山东/四川MTC probe 首命令字快拒,无冲突。 + */ + +#include "app_gz_proto.h" + +#include "FreeRTOS.h" +#include "initcall.h" +#include "app_dispatch.h" + +#include "app_gz_proto_parse.h" +#include "app_gz_proto_cmd.h" + +/* '{' 帧族互斥守卫:同一构建只允许编入一个 '{' 帧族协议(青海/山东/贵州/四川MTC)。 + * 多个同时编入 → 链接期 multiple definition 强制报错; + * Makefile 全协议开发构建定义 STD_ALL_PROTO 跳过守卫(共存由 probe 注册序与文档纪律约束)。 */ +#ifndef STD_ALL_PROTO +__attribute__((used)) char g_brace_proto_guard; +#endif + +/* 地区协议通道 RB:与青海/RLS/四川/山东等同槽 weak 合并 */ +RB_PROVIDE_WEAK(rb_provide_rs485, RB_SIZE_RS485); +RB_PROVIDE_WEAK(rb_provide_rs232, RB_SIZE_RS232); + +static proto_mask_t s_gz_mask; +static proto_mask_t s_gz_mask_rs232; +static osMessageQueueId_t s_gz_queue; + +/* 静态队列:payload 覆盖协议最大帧(len 字段 1B → 数据 ≤255 → 帧 ≤259,与青海同构) */ +#define GZ_PAYLOAD_MAX (259U) /* '{' + cmd + len + data[0..255] + '}' */ +#define GZ_MSG_SIZE (sizeof(frame_msg_t) + GZ_PAYLOAD_MAX) +#define GZ_QUEUE_DEPTH (3U) +_Static_assert(GZ_PAYLOAD_MAX <= RB_SIZE_RS485, "GZ frame must fit RS485 RB"); +_Static_assert(GZ_PAYLOAD_MAX <= RB_SIZE_RS232, "GZ frame must fit RS232 RB"); +static StaticQueue_t s_gz_queue_cb; +static uint8_t s_gz_queue_buf[GZ_QUEUE_DEPTH * GZ_MSG_SIZE]; +static const osMessageQueueAttr_t s_gz_queue_attr = { + .name = "gz_queue", + .cb_mem = &s_gz_queue_cb, + .cb_size = sizeof(s_gz_queue_cb), + .mq_mem = s_gz_queue_buf, + .mq_size = sizeof(s_gz_queue_buf), +}; + +static proto_probe_sta_t gz_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux); + +/** + * @brief 贵州协议模块初始化入口。 + * @note 通过 initcall 自动注册到系统启动流程: + * 申请 RS485 / RS232 RB → 注册探测器 → 绑定双通道 → 创建队列与任务。 + * + * @warning CH_ID_RS232_1 (PL_UART6) 为语音板 TTS 专用,任何地区协议禁止绑定。 + */ +void gz_proto_init(void) +{ + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RS485, RB_SIZE_RS485); + if (rb == nullptr) + return; + + s_gz_mask = app_proto_register(gz_probe_frame, rb); + if (s_gz_mask == 0) + return; + + app_proto_bind_channel(s_gz_mask, CH_ID_RS485); + + ring_buffer_t *rb_rs232 = app_proto_acquire_buf(RB_SLOT_RS232, RB_SIZE_RS232); + if (rb_rs232 != nullptr) { + s_gz_mask_rs232 = app_proto_register(gz_probe_frame, rb_rs232); + if (s_gz_mask_rs232 != 0) + app_proto_bind_channel(s_gz_mask_rs232, CH_ID_RS232); + } + + s_gz_queue = osMessageQueueNew(GZ_QUEUE_DEPTH, GZ_MSG_SIZE, &s_gz_queue_attr); + app_proto_set_frame_queue(s_gz_mask, s_gz_queue); + if (s_gz_mask_rs232 != 0) + app_proto_set_frame_queue(s_gz_mask_rs232, s_gz_queue); + + static const osThreadAttr_t s_gz_task_attr = { + .name = "gz_handle_task", + .stack_size = 256 * 4, /* 帧缓冲为 static,不放大栈 */ + .priority = osPriorityNormal, + }; + osThreadNew(gz_proto_handle_task, NULL, &s_gz_task_attr); +} +sw_app_initcall(gz_proto_init); + +/** + * @brief 贵州协议帧探测(PURE 函数契约:只 rb_peek,无副作用) + * + * 首字节快速拒绝:首字节非 '{' (0x7B) → 立即 FAKE。 + * 帧结构(长度字段为二进制字节值): + * '{' + 命令字 + len + data[0..len-1] + '}',帧总长 = len + 4。 + * 命令字 '1'~'9','A','B' 与 0x01/0x02 二进制命令字。 + */ +static proto_probe_sta_t gz_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux) +{ + (void)ch; + (void)aux; + uint32_t avail = rb_avail(rb, NULL); + + /* 首字节快速拒绝:无数据 → FAKE;首字节非 '{' → FAKE */ + if (avail == 0) + return PROTO_PROBE_FAKE; + uint8_t first_byte; + rb_peek(rb, 0, &first_byte, 1, NULL); + if (first_byte != '{') + return PROTO_PROBE_FAKE; + + /* 需要至少 3 字节:'{' + 命令字 + 长度字段 */ + if (avail < 3U) + return PROTO_PROBE_WAIT; + + uint8_t head[3]; + rb_peek(rb, 0, head, sizeof(head), NULL); + bool cmd_ok = (head[1] >= '1' && head[1] <= '9') || head[1] == 'A' || head[1] == 'B' || + head[1] == 0x01 || head[1] == 0x02; + if (!cmd_ok) + return PROTO_PROBE_FAKE; + + /* 长度字段是二进制字节值,不是 ASCII 数字。 */ + uint16_t data_len = head[2]; + uint32_t frame_len = (uint32_t)data_len + 4U; + if (avail < frame_len) + return PROTO_PROBE_WAIT; + + uint8_t tail; + rb_peek(rb, frame_len - 1U, &tail, 1, NULL); + if (tail != '}') + return PROTO_PROBE_FAKE; + + *total_len = frame_len; /* 仅 READY 路径写输出参数 */ + return PROTO_PROBE_READY; +} + +/** + * @brief 贵州协议帧处理任务。 + * @param argument 任务参数,当前未使用。 + */ +void gz_proto_handle_task(void *argument) +{ + (void)argument; + static uint8_t msg_buf[GZ_MSG_SIZE]; + frame_msg_t *msg = (frame_msg_t *)msg_buf; + for (;;) { + if (osOK != osMessageQueueGet(s_gz_queue, msg, NULL, osWaitForever)) { + continue; + } + gz_parsed_cmd_t cmd = gz_parse_frame(msg->data, msg->data_len); + gz_execute_cmd(msg->ch, &cmd); + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_GuiZhou/app_gz_proto_cmd.c b/Application/Src/ProtocolParser_GuiZhou/app_gz_proto_cmd.c new file mode 100644 index 0000000..559c680 --- /dev/null +++ b/Application/Src/ProtocolParser_GuiZhou/app_gz_proto_cmd.c @@ -0,0 +1,534 @@ +/** + * @file app_gz_proto_cmd.c + * @brief 贵州常规费显协议命令执行 + * + * 显示内容映射到 app_render(协议文本 GBK 直通渲染,构造行文 UTF-8 渲染,行高 FONT_16); + * 灯控映射到 dev_io_lane_light / dev_io_flash_light(红优先); + * 亮度 0=自动调光(恢复光敏任务),1~5 映射硬件档 {3,4,5,7,8}; + * 音量 1~5 映射语音板音量 {1,3,5,7,9}(按设备实测行为); + * 语音经 dev_rs232_voice 旁路 USART6。 + * 上行应答:'1' 主机查询回固定「正常」帧、0x02 版本号回裸 ASCII PROGRAM_CODE,其余命令单向不回。 + */ + +#include "app_gz_proto_cmd.h" +#include "app_gz_proto_parse.h" +#include "app_boot.h" +#include "app_dispatch.h" + +#include +#include +#include + +#include "app_gz_proto_voice.h" +#include "app_light_sensor.h" +#include "app_render.h" +#include "dev_display.h" +#include "dev_io_ctrl.h" +#include "dev_rs232_voice.h" +#include "text_cvt.h" + +/* 协议颜色索引 0/1/2 → 显示颜色(红/绿/黄) */ +static const display_color_t s_gz_color_map[] = { + [0] = COLOR_RED, + [1] = COLOR_GREEN, + [2] = COLOR_YELLOW, +}; + +/* 协议亮度 1~5 → 硬件亮度;最大档映射到 DEV_DISPLAY_BRIGHTNESS_MAX(8) */ +static const uint8_t s_gz_brightness_map[] = {3, 4, 5, 7, 8}; + +/* 协议音量 1~5 → 语音板音量值 {1,3,5,7,9}(按设备实测行为,语音板音量非线性档位; + * STD dev_rs232_voice_volume 直通发送音量值,青海 identity 映射不可复用) */ +static const uint8_t s_gz_volume_map[] = {1, 3, 5, 7, 9}; + +/** + * @brief 将协议颜色索引转换为显示颜色。 + * @param idx 协议颜色索引(0 红 / 1 绿 / 2 黄)。 + * @return 对应的显示颜色;越界回退绿色。 + */ +static display_color_t gz_map_color(uint8_t idx) +{ + return (idx < 3) ? s_gz_color_map[idx] : COLOR_GREEN; +} + +/** + * @brief 清空整屏(置黑并提交)。 + */ +static void _gz_clear_screen(void) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + dev_display_commit_frame(d); +} + +/** + * @brief 清空第 row 行区域(行高 FONT_16)。 + * @param d 显示设备实例。 + * @param row 行号(0 起)。 + */ +static void _gz_clear_row(dev_display_t *d, uint8_t row) +{ + dev_display_fill(d, 0, (uint16_t)row * FONT_16, d->screen_rows, FONT_16, COLOR_BLACK); +} + +/** + * @brief 渲染单行 UTF-8 行文本('6' 固定格式构造行文用):先清行再渲染。 + * @param row 行号(0 起)。 + * @param color 显示颜色。 + * @param text UTF-8 行文本(NUL 结尾)。 + */ +static void _gz_render_utf8_line(uint8_t row, display_color_t color, const char *text) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + _gz_clear_row(d, row); + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = (uint16_t)row * FONT_16, + .w = d->screen_rows, + .h = FONT_16, + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = false, /* 单行:超出行宽截断 */ + }, + .color = color, + .text = text, + .len = (uint16_t)strlen(text), + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_UTF8, + }); +} + +/** + * @brief 渲染单行 GBK 原始文本('6' 信息1 原文用):先清行再渲染。 + * @param row 行号(0 起)。 + * @param color 显示颜色。 + * @param text GBK/ASCII 文本。 + * @param len 文本字节数。 + * @note 空行或首字节为空格则该行不显示(协议文档原文);超过 64 字节截断。 + */ +static void _gz_render_gbk_line(uint8_t row, display_color_t color, const uint8_t *text, + uint16_t len) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + _gz_clear_row(d, row); + if (text == NULL || len == 0U || text[0] == ' ') + return; + if (len > 64U) + len = 64U; + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = (uint16_t)row * FONT_16, + .w = d->screen_rows, + .h = FONT_16, + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = false, + }, + .color = color, + .text = (const char *)text, + .len = len, + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 执行主机查询('1' 命令):回固定「状态正常」应答。 + * @param ch 源通道(应答回源通道;设备实测经固定串口属实现瑕疵,不沿袭)。 + * @note 协议定义正常/异常两种应答,本实现仅回「正常」一种(按设备实测行为)。 + */ +static void _gz_exec_host_query(channel_t *ch) +{ + static const uint8_t rsp_ok[] = {'{', '1', 0x01, 0x00, '}'}; + channel_send(ch, (uint8_t *)rsp_ok, sizeof(rsp_ok)); +} + +/** + * @brief 执行自检('2' 命令):黄色全屏填充 + 语音「系统正在加电自检」。 + * @note 协议文档原文为固定汉字信息及数字 1~9 交替显示;本实现按裁决 + * 黄色全屏 + 自检语音执行。 + */ +static void _gz_exec_self_check(void) +{ + dev_display_t *d = dev_display_get(); + if (d) { + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_YELLOW); + dev_display_commit_frame(d); + } + /* 语音「系统正在加电自检」(UTF-8 字面量,语音板要求 GBK,运行时转换) */ + static const char text[] = "系统正在加电自检"; + uint8_t gbk[32]; + uint32_t gbk_len = sizeof(gbk); + UTF8ToGBK(text, (uint32_t)(sizeof(text) - 1U), (char *)gbk, &gbk_len); + dev_rs232_voice_play(gbk, (uint16_t)gbk_len); +} + +/** + * @brief 执行单行显示('3' 命令:第 row+1 行按颜色渲染文本)。 + * @param p 单行显示参数。 + * @note 先整行清黑再渲染收到内容(文本短于行宽时旧内容残留必须清除)。 + */ +static void _gz_exec_one_line(const gz_one_line_t *p) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + _gz_clear_row(d, p->row); + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = (uint16_t)p->row * FONT_16, + .w = d->screen_rows, + .h = FONT_16, + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = false, /* 单行:超出行宽截断 */ + }, + .color = gz_map_color(p->color), + .text = (const char *)p->text, + .len = p->text_len, + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 执行全屏可编辑显示('4' 命令:按协议 X/Y 坐标渲染,自动换行)。 + * @param p 全屏显示参数。 + * @note 先整屏清黑再渲染;文本中 0x0A 由渲染引擎换行、0x0D 被跳过。 + * X/Y 坐标按协议文档原文取参数渲染(设备实测行为为校验坐标后恒按 + * (0,0) 渲染,不沿袭)。 + */ +static void _gz_exec_full_screen(const gz_full_screen_t *p) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = p->x, + .y = p->y, + .w = d->screen_rows, + .h = d->screen_cols, + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = true, + }, + .color = gz_map_color(p->color), + .text = (const char *)p->text, + .len = p->text_len, + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 执行全屏点亮(0x01 命令:整屏填充红/绿/黄)。 + * @param color 归一化颜色索引 0 红 / 1 绿 / 2 黄。 + * @note 03=黄色按协议文档原文;设备实测行为为蓝色,不沿袭。 + */ +static void _gz_exec_fill_all(uint8_t color) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, gz_map_color(color)); + dev_display_commit_frame(d); +} + +/** + * @brief 执行获取版本号(0x02 命令):串口回裸 ASCII PROGRAM_CODE, + * 屏幕红字全屏显示「版本:PROGRAM_CODE」。 + * @param ch 源通道。 + * @note 屏幕显示版本为对齐设备实测行为的附加行为(协议文档仅要求返回版本号)。 + */ +static void _gz_exec_version(channel_t *ch) +{ + /* 裸 ASCII 应答产品程序编码 PROGRAM_CODE */ + channel_send(ch, (uint8_t *)PROGRAM_CODE, sizeof(PROGRAM_CODE) - 1U); + + dev_display_t *d = dev_display_get(); + if (!d) + return; + + char text[40]; + int len = snprintf(text, sizeof(text), "版本:%s", PROGRAM_CODE); + if (len <= 0 || len >= (int)sizeof(text)) + return; + + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 0, + .w = d->screen_rows, + .h = d->screen_cols, + .style = &(render_style_t){ + .h_align = ALIGN_CENTER, + .v_align = ALIGN_CENTER, + .word_wrap = false, + }, + .color = COLOR_RED, + .text = text, + .len = (uint16_t)len, + .font_size = FONT_SELF_ADAPT, + .font_type = FONT_ST, + .text_enc = FONT_ENC_UTF8, + }); +} + +/** + * @brief 执行亮度设定('8' 命令:'0' 自动调光 / '1'~'5' 手动档)。 + * @param level 协议亮度档位(0~5)。 + * @note 自动档恢复光敏任务;手动档挂起光敏任务并映射硬件亮度 + * {3,4,5,7,8}(协议最大档对应硬件最亮档)。 + */ +static void _gz_exec_brightness(uint8_t level) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + if (level == 0) { + /* 自动调节亮度:恢复光敏任务(若此前被手动档挂起) */ + if (g_light_sensor_task_handle != nullptr) + osThreadResume(g_light_sensor_task_handle); + return; + } + if (level < 1 || level > 5) + return; + if (g_light_sensor_task_handle != nullptr) + osThreadSuspend(g_light_sensor_task_handle); /* 手动设定 → 关闭自动调光 */ + dev_display_set_brightness(d, s_gz_brightness_map[level - 1]); +} + +/** + * @brief 执行语音音量设定('9' 命令:'1'~'5' 档)。 + * @param level 协议音量档位(1~5)。 + * @note 档位来源按设备实测行为:协议 '1'~'5' → 语音板音量值 1/3/5/7/9。 + */ +static void _gz_exec_volume(uint8_t level) +{ + if (level < 1 || level > 5) + return; + dev_rs232_voice_volume(s_gz_volume_map[level - 1]); +} + +/** + * @brief 执行外设控制('A' 命令:bit0 绿灯 / bit1 红灯 / bit2 黄闪报警)。 + * @param ctrl 控制位图。 + * @note 车道灯为单灯互斥(true=绿/false=红):红绿同置时红灯优先。 + */ +static void _gz_exec_peripheral(uint8_t ctrl) +{ + bool green = (ctrl & 0x01U) != 0U; + bool red = (ctrl & 0x02U) != 0U; + bool yellow = (ctrl & 0x04U) != 0U; + if (red) { + dev_io_lane_light(false); + } else if (green) { + dev_io_lane_light(true); + } + dev_io_flash_light(yellow); +} + +/** + * @brief 构造「前缀:%u.%02u 单位」行文本(金额/余额/总重/超重共用)。 + * @param buf 输出缓冲(≥32B)。 + * @param bufsz 输出缓冲大小。 + * @param prefix 行文前缀(如 "金额:")。 + * @param fen 数值(分/吨分,整数运算,无浮点)。 + * @param unit 行文单位(如 "元"/"吨")。 + * @return 写入字节数(不含 NUL);截断时 ≥bufsz。 + */ +static int _gz_format_value(char *buf, size_t bufsz, const char *prefix, uint32_t fen, + const char *unit) +{ + return snprintf(buf, bufsz, "%s%u.%02u %s", prefix, (unsigned)(fen / 100U), + (unsigned)(fen % 100U), unit); +} + +/** + * @brief 渲染总重/超重行('6' 货车):<0.1 吨该行不显示(空行)。 + * @param row 行号(0 起)。 + * @param color 显示颜色。 + * @param fen 重量(吨分)。 + * @param prefix 行文前缀("总重:"/"超重:")。 + */ +static void _gz_render_weight_line(uint8_t row, display_color_t color, uint32_t fen, + const char *prefix) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + if (fen < 10U) { + _gz_clear_row(d, row); /* <0.1 吨 → 该行不显示(空行) */ + return; + } + char line[32]; + int len = _gz_format_value(line, sizeof(line), prefix, fen, "吨"); + if (len > 0 && len < (int)sizeof(line)) + _gz_render_utf8_line(row, color, line); +} + +/** + * @brief 执行固定格式显示('6' 命令)。 + * @param p 固定格式参数。 + * @note 行文对齐设备实测行为(协议文档原文为「车型: X型」等全角冒号形式)。 + * 客车:车型/金额/余额/信息1 四行,信息2 不显示(按设备实测行为); + * 货车:有超重(≥0.1 吨)→金额/余额/总重/超重;无超重→车型/金额/余额/总重。 + * 金额 ≥0.5 元同步播报费额语音。 + */ +static void _gz_exec_fixed(const gz_fixed_t *p) +{ + /* '|' 分隔字段拆分(末字段到参数区结尾,不原地改写帧数据) */ + const uint8_t *fields[5] = {0}; + uint16_t flen[5] = {0}; + int n = 0; + const uint8_t *cur = p->raw; + const uint8_t *end = p->raw + p->raw_len; + while (cur < end && n < 5) { + const uint8_t *sep = memchr(cur, '|', (size_t)(end - cur)); + fields[n] = cur; + flen[n] = (uint16_t)((sep != NULL ? sep : end) - cur); + n++; + if (sep == NULL) + break; + cur = sep + 1; + } + + /* 数值字段 ASCII 数字串转分/吨分(整数运算,无浮点) */ + uint32_t type_fen = (n > 0) ? gz_amount_to_fen(fields[0], flen[0]) : 0U; + uint32_t amount_fen = (n > 1) ? gz_amount_to_fen(fields[1], flen[1]) : 0U; + uint32_t balance_fen = (n > 2) ? gz_amount_to_fen(fields[2], flen[2]) : 0U; + uint32_t weight_fen = (n > 3) ? gz_amount_to_fen(fields[3], flen[3]) : 0U; + uint32_t exceed_fen = (n > 4) ? gz_amount_to_fen(fields[4], flen[4]) : 0U; + + display_color_t color = gz_map_color(p->color); + char line[32]; + int len; + + if (p->type == 0U) { /* 客车 */ + /* 行0 车型 */ + len = snprintf(line, sizeof(line), "车型: %u 型", (unsigned)(type_fen / 100U)); + if (len > 0 && len < (int)sizeof(line)) + _gz_render_utf8_line(0, color, line); + /* 行1 金额 */ + len = _gz_format_value(line, sizeof(line), "金额:", amount_fen, "元"); + if (len > 0 && len < (int)sizeof(line)) + _gz_render_utf8_line(1, color, line); + /* 行2 余额 */ + len = _gz_format_value(line, sizeof(line), "余额:", balance_fen, "元"); + if (len > 0 && len < (int)sizeof(line)) + _gz_render_utf8_line(2, color, line); + /* 行3 信息1 原文(GBK;空行或首字节空格不显示) */ + if (n > 3) + _gz_render_gbk_line(3, color, fields[3], flen[3]); + /* 行4 信息2 不显示(按设备实测行为) */ + } else { /* 货车 */ + if (exceed_fen >= 10U) { + /* 有超重(≥0.1 吨):金额/余额/总重/超重 */ + len = _gz_format_value(line, sizeof(line), "金额:", amount_fen, "元"); + if (len > 0 && len < (int)sizeof(line)) + _gz_render_utf8_line(0, color, line); + len = _gz_format_value(line, sizeof(line), "余额:", balance_fen, "元"); + if (len > 0 && len < (int)sizeof(line)) + _gz_render_utf8_line(1, color, line); + _gz_render_weight_line(2, color, weight_fen, "总重:"); + _gz_render_weight_line(3, color, exceed_fen, "超重:"); + } else { + /* 无超重:车型/金额/余额/总重 */ + len = snprintf(line, sizeof(line), "车型: %u 型", (unsigned)(type_fen / 100U)); + if (len > 0 && len < (int)sizeof(line)) + _gz_render_utf8_line(0, color, line); + len = _gz_format_value(line, sizeof(line), "金额:", amount_fen, "元"); + if (len > 0 && len < (int)sizeof(line)) + _gz_render_utf8_line(1, color, line); + len = _gz_format_value(line, sizeof(line), "余额:", balance_fen, "元"); + if (len > 0 && len < (int)sizeof(line)) + _gz_render_utf8_line(2, color, line); + _gz_render_weight_line(3, color, weight_fen, "总重:"); + } + } + + /* 金额 ≥0.5 元播报费额语音(0 元不播报,由 gz_voice_fee_amount 内部判定) */ + gz_voice_fee_amount(amount_fen); +} + +/** + * @brief 执行贵州协议命令。 + * @param ch 当前通道。 + * @param cmd 解析后的命令结构体。 + * @note 协议未定义应答机制:除 '1' 主机查询回「正常」帧、0x02 版本号回裸 ASCII + * PROGRAM_CODE 外,其余命令单向执行不回;解析失败静默丢弃。 + */ +void gz_execute_cmd(channel_t *ch, const gz_parsed_cmd_t *cmd) +{ + if (!cmd || cmd->sta != GZ_PARSE_OK) + return; + + switch (cmd->cmd) { + case GZ_PCMD_HOST_QUERY: + _gz_exec_host_query(ch); + break; + case GZ_PCMD_SELF_CHECK: + _gz_exec_self_check(); + break; + case GZ_PCMD_ONE_LINE: + _gz_exec_one_line(&cmd->p.one_line); + break; + case GZ_PCMD_FULL_SCREEN: + _gz_exec_full_screen(&cmd->p.full_screen); + break; + case GZ_PCMD_CLEAR: + _gz_clear_screen(); + break; + case GZ_PCMD_FIXED_FORMAT: + _gz_exec_fixed(&cmd->p.fixed); + break; + case GZ_PCMD_CIVIL_VOICE: + gz_voice_civil(cmd->p.civil.idx); + break; + case GZ_PCMD_BRIGHTNESS: + _gz_exec_brightness(cmd->p.brightness.level); + break; + case GZ_PCMD_VOLUME: + _gz_exec_volume(cmd->p.volume.level); + break; + case GZ_PCMD_PERIPHERAL: + _gz_exec_peripheral(cmd->p.peripheral.ctrl); + break; + case GZ_PCMD_FEE_VOICE: + gz_voice_fee_amount(cmd->p.fee.amount_fen); + break; + case GZ_PCMD_FILL_ALL: + _gz_exec_fill_all(cmd->p.fill_all.color); + break; + case GZ_PCMD_VERSION: + _gz_exec_version(ch); + break; + default: + break; + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_GuiZhou/app_gz_proto_parse.c b/Application/Src/ProtocolParser_GuiZhou/app_gz_proto_parse.c new file mode 100644 index 0000000..6503f74 --- /dev/null +++ b/Application/Src/ProtocolParser_GuiZhou/app_gz_proto_parse.c @@ -0,0 +1,223 @@ +/** + * @file app_gz_proto_parse.c + * @brief 贵州常规费显协议帧解析(纯解析:不发包、不渲染、不碰外设) + * + * 帧格式:'{' + 命令字 + 二进制长度 + 参数 + '}'。 + * 协议原文:协议文档 06 贵州常规费显协议(2020-06-17 修订)。 + * 长度边界与截断等按设备实测行为对齐,差异点见各 case 注释。 + */ + +#include "app_gz_proto_parse.h" + +/** + * @brief 将贵州协议命令码字节转换为内部命令枚举。 + * @param c 协议中的命令码字节(ASCII '1'~'9','A','B' 或二进制 0x01/0x02)。 + * @return 对应的协议命令枚举;非法字节返回 GZ_PCMD_INVALID。 + */ +static gz_pcmd_t gz_char_to_cmd(uint8_t c) +{ + switch (c) { + case '1': return GZ_PCMD_HOST_QUERY; + case '2': return GZ_PCMD_SELF_CHECK; + case '3': return GZ_PCMD_ONE_LINE; + case '4': return GZ_PCMD_FULL_SCREEN; + case '5': return GZ_PCMD_CLEAR; + case '6': return GZ_PCMD_FIXED_FORMAT; + case '7': return GZ_PCMD_CIVIL_VOICE; + case '8': return GZ_PCMD_BRIGHTNESS; + case '9': return GZ_PCMD_VOLUME; + case 'A': return GZ_PCMD_PERIPHERAL; + case 'B': return GZ_PCMD_FEE_VOICE; + case 0x01: return GZ_PCMD_FILL_ALL; + case 0x02: return GZ_PCMD_VERSION; + default: return GZ_PCMD_INVALID; + } +} + +uint32_t gz_amount_to_fen(const uint8_t *str, uint16_t len) +{ + uint32_t fen = 0U; + uint16_t i = 0U; + if (str == NULL || len == 0U) { + return 0U; + } + + /* 整数部分:前缀数字累加,遇非数字停止(strtod 前缀语义) */ + while (i < len && str[i] >= '0' && str[i] <= '9') { + fen = fen * 10U + (uint32_t)(str[i] - '0'); + i++; + } + fen *= 100U; + + /* 小数部分:最多取两位(%.2f 语义:一位补零,第三位起截断) */ + if (i < len && str[i] == '.') { + i++; + uint32_t dec = 0U; + uint16_t digits = 0U; + while (i < len && digits < 2U && str[i] >= '0' && str[i] <= '9') { + dec = dec * 10U + (uint32_t)(str[i] - '0'); + digits++; + i++; + } + if (digits == 1U) { + dec *= 10U; + } + fen += dec; + } + return fen; +} + +/** + * @brief 解析贵州协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体;状态字段 sta 标识解析结果。 + * @note 长度字段为二进制字节值,表示参数区长度。 + * '4' 命令文本中的回车对 0x0A 0x0D 原样透传:0x0A 由渲染引擎换行, + * 0x0D 非 GBK 前导字节由渲染引擎跳过,无需归一化。 + * '6' 命令字段拆分不在 parse 层(不原地改写帧数据),由 cmd 层完成。 + */ +gz_parsed_cmd_t gz_parse_frame(const uint8_t *raw, uint16_t raw_len) +{ + gz_parsed_cmd_t cmd = {0}; + if (!raw || raw_len < 4 || raw[0] != '{' || raw[raw_len - 1] != '}') { + cmd.sta = GZ_PARSE_ERR_FRAME; + return cmd; + } + + cmd.cmd = gz_char_to_cmd(raw[1]); + if (cmd.cmd == GZ_PCMD_INVALID) { + cmd.sta = GZ_PARSE_ERR_CMD; + return cmd; + } + + /* 长度字段是二进制字节值,不是 ASCII 数字。 */ + uint16_t declared_len = raw[2]; + if ((uint16_t)(raw_len - 4) < declared_len) { + cmd.sta = GZ_PARSE_ERR_FRAME; + return cmd; + } + + cmd.data = &raw[3]; + cmd.data_len = declared_len; + cmd.sta = GZ_PARSE_OK; + + switch (cmd.cmd) { + case GZ_PCMD_HOST_QUERY: /* '1' 主机查询:无参数 */ + if (declared_len != 0) + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + case GZ_PCMD_SELF_CHECK: /* '2' 自检:无参数 */ + if (declared_len != 0) + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + case GZ_PCMD_ONE_LINE: /* '3' 单行:颜色 + 行号 + 文本 */ + /* 长度边界 3~18 按设备实测行为(len≤2 或 len≥19 丢弃) */ + if (declared_len < 3 || declared_len > 18) { + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + } + cmd.p.one_line.color = (uint8_t)(cmd.data[0] - '0'); + cmd.p.one_line.row = (uint8_t)(cmd.data[1] - '1'); + cmd.p.one_line.text = &cmd.data[2]; + cmd.p.one_line.text_len = declared_len - 2; + if (cmd.p.one_line.color > 2 || cmd.p.one_line.row > 4) + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + case GZ_PCMD_FULL_SCREEN: /* '4' 全屏可编辑:颜色 + X + Y + 文本 */ + /* 长度边界 4~86 按设备实测行为(len≤3 或 len≥87 丢弃) */ + if (declared_len < 4 || declared_len > 86) { + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + } + cmd.p.full_screen.color = (uint8_t)(cmd.data[0] - '0'); + cmd.p.full_screen.x = cmd.data[1]; + cmd.p.full_screen.y = cmd.data[2]; + cmd.p.full_screen.text = &cmd.data[3]; + cmd.p.full_screen.text_len = declared_len - 3; + /* len>71 时文本截断为 64 字节渲染(按设备实测行为) */ + if (declared_len > 71) + cmd.p.full_screen.text_len = 64; + if (cmd.p.full_screen.color > 2) + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + case GZ_PCMD_CLEAR: /* '5' 全屏清除:无参数 */ + if (declared_len != 0) { + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + } + break; + case GZ_PCMD_FIXED_FORMAT: /* '6' 固定格式:颜色 + 客货 + '|' 分隔 5 字段 */ + /* 长度边界 3~90 按设备实测行为 */ + if (declared_len < 3 || declared_len > 90) { + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + } + cmd.p.fixed.color = (uint8_t)(cmd.data[0] - '0'); + cmd.p.fixed.type = (uint8_t)(cmd.data[1] - '0'); + cmd.p.fixed.raw = &cmd.data[2]; + cmd.p.fixed.raw_len = declared_len - 2; + if (cmd.p.fixed.color > 2 || cmd.p.fixed.type > 1) + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + case GZ_PCMD_CIVIL_VOICE: /* '7' 礼貌用语语音:索引 '0'~'3' */ + if (declared_len != 1) { + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + } + cmd.p.civil.idx = (uint8_t)(cmd.data[0] - '0'); + if (cmd.p.civil.idx > 3) + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + case GZ_PCMD_BRIGHTNESS: /* '8' 亮度:'0'~'5',0=自动调节 */ + if (declared_len != 1) { + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + } + cmd.p.brightness.level = (uint8_t)(cmd.data[0] - '0'); + if (cmd.p.brightness.level > 5) + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + case GZ_PCMD_VOLUME: /* '9' 音量:'1'~'5' */ + if (declared_len != 1) { + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + } + cmd.p.volume.level = (uint8_t)(cmd.data[0] - '0'); + if (cmd.p.volume.level < 1 || cmd.p.volume.level > 5) + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + case GZ_PCMD_PERIPHERAL: /* 'A' 外设:bit0 绿灯 / bit1 红灯 / bit2 黄闪报警 */ + if (declared_len != 1) { + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + } + cmd.p.peripheral.ctrl = cmd.data[0]; + break; + case GZ_PCMD_FEE_VOICE: /* 'B' 费额语音:金额 ASCII 串(整数/小数) */ + if (declared_len < 1) { + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + } + cmd.p.fee.amount_fen = gz_amount_to_fen(cmd.data, declared_len); + break; + case GZ_PCMD_FILL_ALL: /* 0x01 全屏点亮:参数 1B 二进制 01红/02绿/03黄 */ + /* 03=黄色按协议文档原文;设备实测行为为蓝色,不沿袭 */ + if (declared_len != 1 || cmd.data[0] < 1 || cmd.data[0] > 3) { + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + } + cmd.p.fill_all.color = cmd.data[0] - 1; /* 归一化 0 红 / 1 绿 / 2 黄 */ + break; + case GZ_PCMD_VERSION: /* 0x02 获取版本号:参数 1B(协议示例 00) */ + if (declared_len != 1) { + cmd.sta = GZ_PARSE_ERR_PARAM; + break; + } + break; + default: + break; + } + + return cmd; +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_GuiZhou/app_gz_proto_voice.c b/Application/Src/ProtocolParser_GuiZhou/app_gz_proto_voice.c new file mode 100644 index 0000000..a05ffea --- /dev/null +++ b/Application/Src/ProtocolParser_GuiZhou/app_gz_proto_voice.c @@ -0,0 +1,73 @@ +/** + * @file app_gz_proto_voice.c + * @brief 贵州常规费显协议语音播报 + * + * 文明用语索引 → UTF-8 字面量 → UTF8ToGBK 运行时转 GBK → dev_rs232_voice_play + * (语音板 TTS 旁路 USART6)。费额语音按金额(分)构造「您好请交费X元」。 + */ + +#include "app_gz_proto_voice.h" + +#include +#include + +#include "dev_rs232_voice.h" +#include "text_cvt.h" + +/* 文明用语(UTF-8 字面量,播报前运行时转 GBK 送语音板)。 + * '0' 按设备实测行为用「贵州高速公路」(协议文档原文为「贵州公路」) */ +static const char *const s_civil_texts[] = { + "您好!欢迎行驶贵州高速公路", + "请出示通行卡", + "谢谢合作!祝您一路平安", + "交易不成功,请走人工车道", +}; + +/** + * @brief 播报文明用语。 + * @param idx 文明用语索引(0~3)。 + */ +void gz_voice_civil(uint8_t idx) +{ + if (idx >= (sizeof(s_civil_texts) / sizeof(s_civil_texts[0]))) { + return; + } + + /* 语音板要求 GBK:UTF-8 源文运行时转换后播报 */ + uint8_t gbk[32]; + uint32_t gbk_len = sizeof(gbk); + UTF8ToGBK(s_civil_texts[idx], (uint32_t)strlen(s_civil_texts[idx]), + (char *)gbk, &gbk_len); + dev_rs232_voice_play(gbk, (uint16_t)gbk_len); +} + +/** + * @brief 播报费额提示语。 + * @param amount_fen 费额,单位分。 + * @note 金额 ≥0.5 元才播报(0 元不播报);小数保留两位(如 1.20→「1.20元」), + * .00 时只播整数(按设备实测行为)。 + */ +void gz_voice_fee_amount(uint32_t amount_fen) +{ + if (amount_fen < 50U) { + return; + } + + char text[48]; + int len; + if (amount_fen % 100U == 0U) { + len = snprintf(text, sizeof(text), "您好请交费%u元", (unsigned)(amount_fen / 100U)); + } else { + len = snprintf(text, sizeof(text), "您好请交费%u.%02u元", (unsigned)(amount_fen / 100U), + (unsigned)(amount_fen % 100U)); + } + if (len <= 0 || len >= (int)sizeof(text)) { + return; + } + + /* 语音板要求 GBK:UTF-8 文本运行时转换后播报 */ + uint8_t gbk[64]; + uint32_t gbk_len = sizeof(gbk); + UTF8ToGBK(text, (uint32_t)len, (char *)gbk, &gbk_len); + dev_rs232_voice_play(gbk, (uint16_t)gbk_len); +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_QingHai/app_qh_proto.c b/Application/Src/ProtocolParser_QingHai/app_qh_proto.c new file mode 100644 index 0000000..b7d30f6 --- /dev/null +++ b/Application/Src/ProtocolParser_QingHai/app_qh_proto.c @@ -0,0 +1,157 @@ +#include "app_qh_proto.h" + +#include "FreeRTOS.h" +#include "initcall.h" +#include "app_dispatch.h" + +#include "app_qh_proto_parse.h" +#include "app_qh_proto_cmd.h" + +/* '{' 帧族互斥守卫:同一构建只允许编入一个 '{' 帧族协议(青海/山东/贵州/四川MTC)。 + * 多个同时编入 → 链接期 multiple definition 强制报错; + * Makefile 全协议开发构建定义 STD_ALL_PROTO 跳过守卫(共存由 probe 注册序与文档纪律约束)。 */ +#ifndef STD_ALL_PROTO +__attribute__((used)) char g_brace_proto_guard; +#endif + +/* 地区协议通道 RB:与 RLS(485)等同槽 weak 合并 */ +RB_PROVIDE_WEAK(rb_provide_rs485, RB_SIZE_RS485); +RB_PROVIDE_WEAK(rb_provide_rs232, RB_SIZE_RS232); + +static proto_mask_t s_qh_mask; +static proto_mask_t s_qh_mask_rs232; +static osMessageQueueId_t s_qh_queue; + +/* 静态队列:payload 对齐协议上限(len 字段 1B → 帧 ≤259),勿用 IAP 的 1044 */ +#define QH_PAYLOAD_MAX (259U) /* '{' + cmd + len + data[0..255] + '}' */ +#define QH_MSG_SIZE (sizeof(frame_msg_t) + QH_PAYLOAD_MAX) +#define QH_QUEUE_DEPTH (3U) +_Static_assert(QH_PAYLOAD_MAX <= RB_SIZE_RS485, "QH frame must fit RS485 RB"); +_Static_assert(QH_PAYLOAD_MAX <= RB_SIZE_RS232, "QH frame must fit RS232 RB"); +static StaticQueue_t s_qh_queue_cb; +static uint8_t s_qh_queue_buf[QH_QUEUE_DEPTH * QH_MSG_SIZE]; +static const osMessageQueueAttr_t s_qh_queue_attr = { + .name = "qh_queue", + .cb_mem = &s_qh_queue_cb, + .cb_size = sizeof(s_qh_queue_cb), + .mq_mem = s_qh_queue_buf, + .mq_size = sizeof(s_qh_queue_buf), +}; + +static proto_probe_sta_t qh_proto_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux); + +/** + * @brief 青海协议模块初始化入口。 + * @note 通过 initcall 自动注册到系统启动流程,负责: + * 1. 申请 RS485 / RS232 物理通道 RB + * 2. 注册协议探测器到多协议分发器 + * 3. 绑定 RS485 + RS232(RS232_1 禁止绑定 — 语音专用) + * 4. 创建协议消息队列与处理任务 + * + * @warning CH_ID_RS232_1 (PL_UART6) 为语音板 TTS 专用,任何地区协议禁止绑定。 + */ +void qh_proto_init(void) +{ + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RS485, RB_SIZE_RS485); + if (rb == nullptr) + return; + + s_qh_mask = app_proto_register(qh_proto_probe_frame, rb); + if (s_qh_mask == 0) + return; + + app_proto_bind_channel(s_qh_mask, CH_ID_RS485); + + ring_buffer_t *rb_rs232 = app_proto_acquire_buf(RB_SLOT_RS232, RB_SIZE_RS232); + if (rb_rs232 != nullptr) { + s_qh_mask_rs232 = app_proto_register(qh_proto_probe_frame, rb_rs232); + if (s_qh_mask_rs232 != 0) + app_proto_bind_channel(s_qh_mask_rs232, CH_ID_RS232); + } + + s_qh_queue = osMessageQueueNew(QH_QUEUE_DEPTH, QH_MSG_SIZE, &s_qh_queue_attr); + app_proto_set_frame_queue(s_qh_mask, s_qh_queue); + if (s_qh_mask_rs232 != 0) + app_proto_set_frame_queue(s_qh_mask_rs232, s_qh_queue); + + static const osThreadAttr_t s_qh_task_attr = { + .name = "qh_handle_task", + .stack_size = 256 * 4, /* 帧缓冲为 static;原 2KB 偏大 */ + .priority = osPriorityNormal, + }; + osThreadNew(qh_proto_handle_task, NULL, &s_qh_task_attr); +} +sw_app_initcall(qh_proto_init); + +/** + * @brief 青海协议帧探测函数。 + * @param ch 当前接收通道。 + * @param rb 通道环形缓冲区。 + * @param total_len 探测到的完整帧长度。 + * @param aux 额外状态位,当前未使用。 + * @return PROTO_PROBE_WAIT / PROTO_PROBE_FAKE / PROTO_PROBE_READY + * @note 帧格式为 `{` + 命令 + 二进制长度 + 数据 + `}`。 + */ +/** + * @brief 青海协议帧探测(PURE 函数契约:只 peek,无副作用) + * + * 首字节快速拒绝:首字节非 '{' (0x7B) → 立即 FAKE,禁止无数据时盲 WAIT。 + */ +static proto_probe_sta_t qh_proto_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux) +{ + (void)ch; + (void)aux; + uint32_t avail = rb_avail(rb, NULL); + + /* 首字节快速拒绝:无数据 → FAKE;首字节非 '{' → FAKE */ + if (avail == 0) + return PROTO_PROBE_FAKE; + + uint8_t first_byte; + rb_peek(rb, 0, &first_byte, 1, NULL); + if (first_byte != '{') + return PROTO_PROBE_FAKE; + + /* 需要至少 3 字节:'{' + 命令码 + 长度字段 */ + if (avail < 3U) + return PROTO_PROBE_WAIT; + + uint8_t head[3]; + rb_peek(rb, 0, head, sizeof(head), NULL); + if ((head[1] < '1' || head[1] > '9') && head[1] != 'A' && head[1] != 'B') + return PROTO_PROBE_FAKE; + + /* 长度字段是二进制字节值,不是 ASCII 数字。 */ + uint16_t data_len = head[2]; + uint32_t frame_len = (uint32_t)data_len + 4U; + if (avail < frame_len) + return PROTO_PROBE_WAIT; + + uint8_t tail; + rb_peek(rb, frame_len - 1U, &tail, 1, NULL); + if (tail != '}') + return PROTO_PROBE_FAKE; + + *total_len = frame_len; /* 仅 READY 路径写输出参数 */ + return PROTO_PROBE_READY; +} + +/** + * @brief 青海协议帧处理任务。 + * @param argument 任务参数,当前未使用。 + */ +void qh_proto_handle_task(void *argument) +{ + (void)argument; + static uint8_t msg_buf[QH_MSG_SIZE]; + frame_msg_t *msg = (frame_msg_t *)msg_buf; + for (;;) { + if (osOK != osMessageQueueGet(s_qh_queue, msg, NULL, osWaitForever)) { + continue; + } + qh_parsed_cmd_t cmd = qh_parse_frame(msg->data, msg->data_len); + qh_execute_cmd(msg->ch, &cmd); + } +} diff --git a/Application/Src/ProtocolParser_QingHai/app_qh_proto_cmd.c b/Application/Src/ProtocolParser_QingHai/app_qh_proto_cmd.c new file mode 100644 index 0000000..cddb606 --- /dev/null +++ b/Application/Src/ProtocolParser_QingHai/app_qh_proto_cmd.c @@ -0,0 +1,307 @@ +#include "app_qh_proto_cmd.h" +#include "app_dispatch.h" + +#include +#include + +#include "app_render.h" +#include "dev_display.h" +#include "dev_io_ctrl.h" +#include "dev_rs232_voice.h" +#include "app_qh_proto_voice.h" +#include "text_cvt.h" + +static const display_color_t s_qh_color_map[] = { + [0] = COLOR_RED, + [1] = COLOR_GREEN, + [2] = COLOR_YELLOW, +}; + +/* 协议 1~5 → 硬件亮度;最大档映射到 DEV_DISPLAY_BRIGHTNESS_MAX(8) */ +static const uint8_t s_qh_brightness_map[] = {3, 4, 5, 7, 8}; +static const uint8_t s_qh_volume_map[] = {0, 1, 2, 3, 4, 5}; + +/** + * @brief 将青海协议颜色索引转换为显示颜色。 + * @param idx 协议颜色索引。 + * @return 对应的显示颜色。 + */ +static display_color_t qh_map_color(uint8_t idx) +{ + return (idx < 3) ? s_qh_color_map[idx] : COLOR_GREEN; +} + +/** + * @brief 处理主机查询命令并返回固定应答。 + * @param ch 当前通道。 + */ +static void qh_exec_host_query(channel_t *ch) +{ + static const uint8_t rsp_ok[] = {'{', '1', 0x01, 0x00, '}'}; + static const uint8_t rsp_err[] = {'{', '1', 0x01, 0x01, '}'}; + channel_send(ch, (uint8_t *)rsp_ok, sizeof(rsp_ok)); + (void)rsp_err; +} + +/** + * @brief 执行自检命令,清屏并播报提示音。 + */ +static void qh_exec_self_check(void) +{ + dev_display_t *d = dev_display_get(); + if (d) { + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_YELLOW); + dev_display_commit_frame(d); + } + /* 「系统正在加电自检」(自检语音,UTF-8 字面量;语音板要求 GBK,运行时转换) */ + static const uint8_t text[] = "系统正在加电自检"; + uint8_t gbk[32]; + uint32_t gbk_len = sizeof(gbk); + UTF8ToGBK((const char *)text, (uint32_t)(sizeof(text) - 1U), (char *)gbk, &gbk_len); + dev_rs232_voice_play(gbk, (uint16_t)gbk_len); +} + +/** + * @brief 执行单行显示命令。 + * @param p 单行显示参数。 + */ +static void qh_exec_one_line(const qh_one_line_t *p) +{ + dev_display_t *d = dev_display_get(); + if (!d) return; + + uint16_t w = d->screen_rows; + if (d->screen_cols > w) w = d->screen_cols; + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = (uint16_t)p->row * FONT_16, + .w = w, + .h = FONT_16, + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = true, + }, + .color = qh_map_color(p->color), + .text = (const char *)p->text, + .len = p->text_len, + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 执行全屏显示命令。 + * @param p 全屏显示参数。 + */ +static void qh_exec_full_screen(const qh_full_screen_t *p) +{ + dev_display_t *d = dev_display_get(); + if (!d) return; + + uint16_t w = d->screen_rows; + if (d->screen_cols > w) w = d->screen_cols; + uint16_t h = d->screen_cols; + if (d->screen_rows < h) h = d->screen_rows; + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = p->x, + .y = p->y, + .w = w, + .h = h, + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = true, + }, + .color = qh_map_color(p->color), + .text = (const char *)p->text, + .len = p->text_len, + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 清空显示屏。 + */ +static void qh_exec_clear(void) +{ + dev_display_t *d = dev_display_get(); + if (!d) return; + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + dev_display_commit_frame(d); +} + +/** + * @brief 执行固定格式显示命令。 + * @param p 固定格式显示参数。 + */ +static void qh_exec_fixed(const qh_fixed_t *p) +{ + const uint8_t *fields[8] = {0}; + uint16_t flen[8] = {0}; + int n = 0; + const uint8_t *cur = p->raw; + const uint8_t *end = p->raw + p->raw_len; + while (cur < end && n < 8) { + const uint8_t *sep = memchr(cur, '|', (size_t)(end - cur)); + fields[n] = cur; + flen[n] = (uint16_t)((sep ? sep : end) - cur); + n++; + if (!sep) break; + cur = sep + 1; + } + + dev_display_t *d = dev_display_get(); + if (!d) return; + + uint16_t w = d->screen_rows; + if (d->screen_cols > w) w = d->screen_cols; + + /* 固定格式显示按协议文本只做“字段拆分 + 按行显示”的基础实现。 */ + for (int i = 0; i < n && i < 5; i++) { + if (flen[i] == 0) continue; + if (fields[i][0] == ' ') continue; + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = (uint16_t)i * FONT_16, + .w = w, + .h = FONT_16, + .color = qh_map_color(p->type), + .text = (const char *)fields[i], + .len = flen[i], + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); + } + + /* 协议文本要求固定格式时同步播报收费金额;当前阶段先保留接口, + * 如果字段未解析到金额,则不播报。 */ + if (n >= 2) { + uint32_t amount = 0; + for (uint16_t i = 0; i < flen[1]; i++) { + if (fields[1][i] < '0' || fields[1][i] > '9') break; + amount = amount * 10U + (uint32_t)(fields[1][i] - '0'); + } + if (amount > 0U) { + qh_voice_fee_amount(amount * 100U); + } + } +} + +/** + * @brief 设置屏幕亮度。 + * @param level 亮度档位。 + */ +static void qh_exec_brightness(uint8_t level) +{ + dev_display_t *d = dev_display_get(); + if (!d) return; + if (level == 0) { + return; /* 自动档:交由光敏调光(上限为 DEV_DISPLAY_BRIGHTNESS_MAX) */ + } + if (level < 1 || level > 5) return; + dev_display_set_brightness(d, s_qh_brightness_map[level - 1]); +} + +/** + * @brief 设置语音播放音量。 + * @param level 音量档位。 + */ +static void qh_exec_volume(uint8_t level) +{ + if (level < 1 || level > 5) return; + dev_rs232_voice_volume(s_qh_volume_map[level]); +} + +/** + * @brief 控制外设输出状态。 + * @param ctrl 控制位图。 + */ +static void qh_exec_peripheral(uint8_t ctrl) +{ + bool green = (ctrl & 0x01U) != 0U; + bool red = (ctrl & 0x02U) != 0U; + bool yellow = (ctrl & 0x04U) != 0U; + if (red) { + dev_io_lane_light(false); + } else if (green) { + dev_io_lane_light(true); + } + dev_io_flash_light(yellow); +} + +/** + * @brief 执行文明用语语音播报。 + * @param idx 文明用语索引。 + */ +static void qh_exec_civil_voice(uint8_t idx) +{ + qh_voice_civil(idx); +} + +/** + * @brief 执行费额语音播报。 + * @param type 语音类型。 + * @param amount_fen 费额,单位分。 + */ +static void qh_exec_voice_fee(uint8_t type, uint32_t amount_fen) +{ + (void)type; + qh_voice_fee_amount(amount_fen); +} + +/** + * @brief 执行青海协议命令。 + * @param ch 当前通道。 + * @param cmd 解析后的命令结构体。 + */ +void qh_execute_cmd(channel_t *ch, const qh_parsed_cmd_t *cmd) +{ + if (!cmd || cmd->sta != QH_PARSE_OK) return; + switch (cmd->cmd) { + case QH_PCMD_HOST_QUERY: + qh_exec_host_query(ch); + break; + case QH_PCMD_SELF_CHECK: + qh_exec_self_check(); + break; + case QH_PCMD_ONE_LINE: + qh_exec_one_line(&cmd->p.one_line); + break; + case QH_PCMD_FULL_SCREEN: + qh_exec_full_screen(&cmd->p.full_screen); + break; + case QH_PCMD_CLEAR: + qh_exec_clear(); + break; + case QH_PCMD_FIXED_DISPLAY: + qh_exec_fixed(&cmd->p.fixed); + break; + case QH_PCMD_CIVIL_VOICE: + qh_exec_civil_voice(cmd->p.civil.idx); + break; + case QH_PCMD_BRIGHTNESS: + qh_exec_brightness(cmd->p.brightness.level); + break; + case QH_PCMD_VOLUME: + qh_exec_volume(cmd->p.volume.level); + break; + case QH_PCMD_PERIPHERAL: + qh_exec_peripheral(cmd->p.peripheral.ctrl); + break; + case QH_PCMD_VOICE: + qh_exec_voice_fee(cmd->p.fee.type, cmd->p.fee.amount_fen); + break; + default: + break; + } +} diff --git a/Application/Src/ProtocolParser_QingHai/app_qh_proto_parse.c b/Application/Src/ProtocolParser_QingHai/app_qh_proto_parse.c new file mode 100644 index 0000000..ae2e739 --- /dev/null +++ b/Application/Src/ProtocolParser_QingHai/app_qh_proto_parse.c @@ -0,0 +1,116 @@ +#include "app_qh_proto_parse.h" + +/** + * @brief 将青海协议命令码字符转换为内部命令枚举。 + * @param c 协议中的命令码字符。 + * @return 对应的协议命令枚举;非法字符返回 QH_PCMD_INVALID。 + */ +static qh_pcmd_t qh_char_to_cmd(uint8_t c) +{ + switch (c) { + case '1': return QH_PCMD_HOST_QUERY; + case '2': return QH_PCMD_SELF_CHECK; + case '3': return QH_PCMD_ONE_LINE; + case '4': return QH_PCMD_FULL_SCREEN; + case '5': return QH_PCMD_CLEAR; + case '6': return QH_PCMD_FIXED_DISPLAY; + case '7': return QH_PCMD_CIVIL_VOICE; + case '8': return QH_PCMD_BRIGHTNESS; + case '9': return QH_PCMD_VOLUME; + case 'A': return QH_PCMD_PERIPHERAL; + case 'B': return QH_PCMD_VOICE; + default: return QH_PCMD_INVALID; + } +} + +/** + * @brief 解析青海协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体;状态字段 sta 标识解析结果。 + * @note 长度字段为二进制字节值,表示数据区长度。 + */ +qh_parsed_cmd_t qh_parse_frame(const uint8_t *raw, uint16_t raw_len) +{ + qh_parsed_cmd_t cmd = {0}; + if (!raw || raw_len < 4 || raw[0] != '{' || raw[raw_len - 1] != '}') { + cmd.sta = QH_PARSE_ERR_FRAME; + return cmd; + } + + cmd.cmd = qh_char_to_cmd(raw[1]); + if (cmd.cmd == QH_PCMD_INVALID) { + cmd.sta = QH_PARSE_ERR_CMD; + return cmd; + } + + /* 长度字段是二进制字节值,不是 ASCII 数字。 */ + uint16_t declared_len = raw[2]; + if ((uint16_t)(raw_len - 4) < declared_len) { + cmd.sta = QH_PARSE_ERR_FRAME; + return cmd; + } + + cmd.data = &raw[3]; + cmd.data_len = declared_len; + cmd.sta = QH_PARSE_OK; + + switch (cmd.cmd) { + case QH_PCMD_ONE_LINE: + if (declared_len < 2) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.one_line.color = (uint8_t)(cmd.data[0] - '0'); + cmd.p.one_line.row = (uint8_t)(cmd.data[1] - '1'); + cmd.p.one_line.text = &cmd.data[2]; + cmd.p.one_line.text_len = declared_len - 2; + if (cmd.p.one_line.color > 2 || cmd.p.one_line.row > 4) cmd.sta = QH_PARSE_ERR_PARAM; + break; + case QH_PCMD_FULL_SCREEN: + if (declared_len < 3) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.full_screen.color = (uint8_t)(cmd.data[0] - '0'); + cmd.p.full_screen.x = cmd.data[1]; + cmd.p.full_screen.y = cmd.data[2]; + cmd.p.full_screen.text = &cmd.data[3]; + cmd.p.full_screen.text_len = declared_len - 3; + if (cmd.p.full_screen.color > 2) cmd.sta = QH_PARSE_ERR_PARAM; + break; + case QH_PCMD_BRIGHTNESS: + if (declared_len < 1) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.brightness.level = (uint8_t)(cmd.data[0] - '0'); + if (cmd.p.brightness.level > 5) cmd.sta = QH_PARSE_ERR_PARAM; + break; + case QH_PCMD_VOLUME: + if (declared_len < 1) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.volume.level = (uint8_t)(cmd.data[0] - '0'); + if (cmd.p.volume.level < 1 || cmd.p.volume.level > 5) cmd.sta = QH_PARSE_ERR_PARAM; + break; + case QH_PCMD_PERIPHERAL: + if (declared_len < 1) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.peripheral.ctrl = cmd.data[0]; + break; + case QH_PCMD_CIVIL_VOICE: + if (declared_len < 1) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.civil.idx = (uint8_t)(cmd.data[0] - '0'); + if (cmd.p.civil.idx > 3) cmd.sta = QH_PARSE_ERR_PARAM; + break; + case QH_PCMD_FIXED_DISPLAY: + if (declared_len < 2) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.fixed.type = (uint8_t)(cmd.data[0] - '0'); + cmd.p.fixed.raw = &cmd.data[1]; + cmd.p.fixed.raw_len = declared_len - 1; + if (cmd.p.fixed.type > 1) cmd.sta = QH_PARSE_ERR_PARAM; + break; + case QH_PCMD_VOICE: + if (declared_len < 6) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.fee.type = (uint8_t)(cmd.data[0] - '0'); + cmd.p.fee.amount_fen = 0; + for (int i = 1; i <= 5; i++) { + if (cmd.data[i] < '0' || cmd.data[i] > '9') { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.fee.amount_fen = cmd.p.fee.amount_fen * 10U + (uint32_t)(cmd.data[i] - '0'); + } + break; + default: + break; + } + + return cmd; +} diff --git a/Application/Src/ProtocolParser_QingHai/app_qh_proto_voice.c b/Application/Src/ProtocolParser_QingHai/app_qh_proto_voice.c new file mode 100644 index 0000000..459d9c6 --- /dev/null +++ b/Application/Src/ProtocolParser_QingHai/app_qh_proto_voice.c @@ -0,0 +1,55 @@ +#include "app_qh_proto_voice.h" + +#include +#include + +#include "dev_rs232_voice.h" +#include "text_cvt.h" + +/* 文明用语(UTF-8 字面量,播报前运行时转 GBK 送语音板)。 + * '0' 按 doc/04 B.7.6 对齐裸机用「贵州高速公路」(协议文档原文为「高速公路」) */ +static const char *const s_civil_texts[] = { + "您好!欢迎行驶贵州高速公路", + "请出示通行卡", + "谢谢合作!祝您一路平安", + "交易不成功,请走人工车道", +}; + +/** + * @brief 播报文明用语。 + * @param idx 文明用语索引。 + */ +void qh_voice_civil(uint8_t idx) +{ + if (idx >= (sizeof(s_civil_texts) / sizeof(s_civil_texts[0]))) { + return; + } + + /* 语音板要求 GBK:UTF-8 源文运行时转换后播报 */ + uint8_t gbk[32]; + uint32_t gbk_len = sizeof(gbk); + UTF8ToGBK(s_civil_texts[idx], (uint32_t)strlen(s_civil_texts[idx]), + (char *)gbk, &gbk_len); + dev_rs232_voice_play(gbk, (uint16_t)gbk_len); +} + +/** + * @brief 播报费额提示语。 + * @param amount_fen 费额,单位分。 + */ +void qh_voice_fee_amount(uint32_t amount_fen) +{ + if (amount_fen < 50U) { + return; + } + uint32_t yuan = amount_fen / 100U; + char text[64]; + int len = snprintf(text, sizeof(text), "您好请交费%lu元", (unsigned long)yuan); + if (len > 0 && len < (int)sizeof(text)) { + /* 语音板要求 GBK:UTF-8 文本运行时转换后播报 */ + uint8_t gbk[64]; + uint32_t gbk_len = sizeof(gbk); + UTF8ToGBK(text, (uint32_t)len, (char *)gbk, &gbk_len); + dev_rs232_voice_play(gbk, (uint16_t)gbk_len); + } +} diff --git a/Application/Src/ProtocolParser_ShanDong/app_sd_proto.c b/Application/Src/ProtocolParser_ShanDong/app_sd_proto.c new file mode 100644 index 0000000..dfa5c1f --- /dev/null +++ b/Application/Src/ProtocolParser_ShanDong/app_sd_proto.c @@ -0,0 +1,159 @@ +/** + * @file app_sd_proto.c + * @brief 山东车道费额显示器通信协议模块 — 注册 / 探测 / 处理任务 + * + * 帧格式:'{' + 命令字 + 二进制长度 + 参数 + '}',与青海协议同构。 + * 绑定 RS485 + RS232 双通道(与青海/四川同模式:acquire → register → bind → set_frame_queue)。 + * 帧头冲突:命令字 '1'~'5','7','8' 全部落入青海 probe 命令集('1'~'9','A','B'), + * 全协议构建下青海 probe 先注册(字母序 qh < sd)先认领山东帧; + * 量产由 EIDE 目录排除纪律保证山东与青海/四川MTC 互斥(doc/05-01 §6 兼容矩阵)。 + */ + +#include "app_sd_proto.h" + +#include "FreeRTOS.h" +#include "initcall.h" +#include "app_dispatch.h" + +#include "app_sd_proto_parse.h" +#include "app_sd_proto_cmd.h" + +/* '{' 帧族互斥守卫:同一构建只允许编入一个 '{' 帧族协议(青海/山东/贵州/四川MTC)。 + * 多个同时编入 → 链接期 multiple definition 强制报错; + * Makefile 全协议开发构建定义 STD_ALL_PROTO 跳过守卫(共存由 probe 注册序与文档纪律约束)。 */ +#ifndef STD_ALL_PROTO +__attribute__((used)) char g_brace_proto_guard; +#endif + +/* 地区协议通道 RB:与青海/RLS/四川等同槽 weak 合并 */ +RB_PROVIDE_WEAK(rb_provide_rs485, RB_SIZE_RS485); +RB_PROVIDE_WEAK(rb_provide_rs232, RB_SIZE_RS232); + +static proto_mask_t s_sd_mask; +static proto_mask_t s_sd_mask_rs232; +static osMessageQueueId_t s_sd_queue; + +/* 静态队列:payload 覆盖协议最大帧(len 字段 1B → 数据 ≤255 → 帧 ≤259,与青海同构) */ +#define SD_PAYLOAD_MAX (259U) /* '{' + cmd + len + data[0..255] + '}' */ +#define SD_MSG_SIZE (sizeof(frame_msg_t) + SD_PAYLOAD_MAX) +#define SD_QUEUE_DEPTH (3U) +_Static_assert(SD_PAYLOAD_MAX <= RB_SIZE_RS485, "SD frame must fit RS485 RB"); +_Static_assert(SD_PAYLOAD_MAX <= RB_SIZE_RS232, "SD frame must fit RS232 RB"); +static StaticQueue_t s_sd_queue_cb; +static uint8_t s_sd_queue_buf[SD_QUEUE_DEPTH * SD_MSG_SIZE]; +static const osMessageQueueAttr_t s_sd_queue_attr = { + .name = "sd_queue", + .cb_mem = &s_sd_queue_cb, + .cb_size = sizeof(s_sd_queue_cb), + .mq_mem = s_sd_queue_buf, + .mq_size = sizeof(s_sd_queue_buf), +}; + +static proto_probe_sta_t sd_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux); + +/** + * @brief 山东协议模块初始化入口。 + * @note 通过 initcall 自动注册到系统启动流程: + * 申请 RS485 / RS232 RB → 注册探测器 → 绑定双通道 → 创建队列与任务。 + * + * @warning CH_ID_RS232_1 (PL_UART6) 为语音板 TTS 专用,任何地区协议禁止绑定。 + */ +void sd_proto_init(void) +{ + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RS485, RB_SIZE_RS485); + if (rb == nullptr) + return; + + s_sd_mask = app_proto_register(sd_probe_frame, rb); + if (s_sd_mask == 0) + return; + + app_proto_bind_channel(s_sd_mask, CH_ID_RS485); + + ring_buffer_t *rb_rs232 = app_proto_acquire_buf(RB_SLOT_RS232, RB_SIZE_RS232); + if (rb_rs232 != nullptr) { + s_sd_mask_rs232 = app_proto_register(sd_probe_frame, rb_rs232); + if (s_sd_mask_rs232 != 0) + app_proto_bind_channel(s_sd_mask_rs232, CH_ID_RS232); + } + + s_sd_queue = osMessageQueueNew(SD_QUEUE_DEPTH, SD_MSG_SIZE, &s_sd_queue_attr); + app_proto_set_frame_queue(s_sd_mask, s_sd_queue); + if (s_sd_mask_rs232 != 0) + app_proto_set_frame_queue(s_sd_mask_rs232, s_sd_queue); + + static const osThreadAttr_t s_sd_task_attr = { + .name = "sd_handle_task", + .stack_size = 256 * 4, /* 帧缓冲为 static,不放大栈 */ + .priority = osPriorityNormal, + }; + osThreadNew(sd_proto_handle_task, NULL, &s_sd_task_attr); +} +sw_app_initcall(sd_proto_init); + +/** + * @brief 山东协议帧探测(PURE 函数契约:只 rb_peek,无副作用) + * + * 首字节快速拒绝:首字节非 '{' (0x7B) → 立即 FAKE。 + * 帧结构(长度字段为二进制字节值): + * '{' + 命令字 + len + data[0..len-1] + '}',帧总长 = len + 4。 + * 命令字 '1'~'5','7','8'(无 '6';'9'/'A'/'B' 属青海命令集 → FAKE)。 + */ +static proto_probe_sta_t sd_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux) +{ + (void)ch; + (void)aux; + uint32_t avail = rb_avail(rb, NULL); + + /* 首字节快速拒绝:无数据 → FAKE;首字节非 '{' → FAKE */ + if (avail == 0) + return PROTO_PROBE_FAKE; + uint8_t first_byte; + rb_peek(rb, 0, &first_byte, 1, NULL); + if (first_byte != '{') + return PROTO_PROBE_FAKE; + + /* 需要至少 3 字节:'{' + 命令字 + 长度字段 */ + if (avail < 3U) + return PROTO_PROBE_WAIT; + + uint8_t head[3]; + rb_peek(rb, 0, head, sizeof(head), NULL); + bool cmd_ok = (head[1] >= '1' && head[1] <= '5') || head[1] == '7' || head[1] == '8'; + if (!cmd_ok) + return PROTO_PROBE_FAKE; + + /* 长度字段是二进制字节值,不是 ASCII 数字。 */ + uint16_t data_len = head[2]; + uint32_t frame_len = (uint32_t)data_len + 4U; + if (avail < frame_len) + return PROTO_PROBE_WAIT; + + uint8_t tail; + rb_peek(rb, frame_len - 1U, &tail, 1, NULL); + if (tail != '}') + return PROTO_PROBE_FAKE; + + *total_len = frame_len; /* 仅 READY 路径写输出参数 */ + return PROTO_PROBE_READY; +} + +/** + * @brief 山东协议帧处理任务。 + * @param argument 任务参数,当前未使用。 + */ +void sd_proto_handle_task(void *argument) +{ + (void)argument; + static uint8_t msg_buf[SD_MSG_SIZE]; + frame_msg_t *msg = (frame_msg_t *)msg_buf; + for (;;) { + if (osOK != osMessageQueueGet(s_sd_queue, msg, NULL, osWaitForever)) { + continue; + } + sd_parsed_cmd_t cmd = sd_parse_frame(msg->data, msg->data_len); + sd_execute_cmd(msg->ch, &cmd); + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_ShanDong/app_sd_proto_cmd.c b/Application/Src/ProtocolParser_ShanDong/app_sd_proto_cmd.c new file mode 100644 index 0000000..ea5d5b3 --- /dev/null +++ b/Application/Src/ProtocolParser_ShanDong/app_sd_proto_cmd.c @@ -0,0 +1,216 @@ +/** + * @file app_sd_proto_cmd.c + * @brief 山东车道费额显示器通信协议命令执行 + * + * 显示内容映射到 app_render(GBK 直通渲染,行高 FONT_16); + * 灯控映射到 dev_io_lane_light / dev_io_flash_light(PD14/PD15); + * 亮度 0=自动调光(恢复光敏任务),1~5 映射硬件档 {3,4,5,7,8}。 + * 协议未定义上行应答(除 '2' 取版本号回裸 ASCII 产品程序编码 PROGRAM_CODE), + * 其余命令单向不回。 + */ + +#include "app_sd_proto_cmd.h" +#include "app_boot.h" +#include "app_dispatch.h" + +#include + +#include "app_light_sensor.h" +#include "app_render.h" +#include "dev_display.h" +#include "dev_io_ctrl.h" + +/* 协议颜色索引 0/1/2 → 显示颜色(红/绿/黄) */ +static const display_color_t s_sd_color_map[] = { + [0] = COLOR_RED, + [1] = COLOR_GREEN, + [2] = COLOR_YELLOW, +}; + +/* 协议亮度 1~5 → 硬件亮度;最大档映射到 DEV_DISPLAY_BRIGHTNESS_MAX(8) */ +static const uint8_t s_sd_brightness_map[] = {3, 4, 5, 7, 8}; + +/** + * @brief 将协议颜色索引转换为显示颜色。 + * @param idx 协议颜色索引(0 红 / 1 绿 / 2 黄)。 + * @return 对应的显示颜色;越界回退绿色。 + */ +static display_color_t sd_map_color(uint8_t idx) +{ + return (idx < 3) ? s_sd_color_map[idx] : COLOR_GREEN; +} + +/** + * @brief 清空整屏(置黑并提交)。 + */ +static void _sd_clear_screen(void) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + dev_display_commit_frame(d); +} + +/** + * @brief 执行全屏单色显示('1' 命令:整屏填充红/绿/黄)。 + * @param color 归一化颜色索引 0 红 / 1 绿 / 2 黄。 + */ +static void _sd_exec_fill_all(uint8_t color) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, sd_map_color(color)); + dev_display_commit_frame(d); +} + +/** + * @brief 执行单行显示('3' 命令:第 row+1 行按颜色渲染文本)。 + * @param p 单行显示参数。 + * @note 先整行清黑再渲染收到内容(文本短于行宽时旧内容残留必须清除)。 + */ +static void _sd_exec_one_line(const sd_one_line_t *p) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + dev_display_fill(d, 0, (uint16_t)p->row * FONT_16, d->screen_rows, FONT_16, + COLOR_BLACK); + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = (uint16_t)p->row * FONT_16, + .w = d->screen_rows, + .h = FONT_16, + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = false, /* 单行:超出行宽截断 */ + }, + .color = sd_map_color(p->color), + .text = (const char *)p->text, + .len = p->text_len, + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 执行全屏可编辑显示('4' 命令:按协议 X/Y 坐标渲染,自动换行)。 + * @param p 全屏显示参数。 + * @note 先整屏清黑再渲染;文本中 0x0A 由渲染引擎换行、0x0D 被跳过。 + */ +static void _sd_exec_full_screen(const sd_full_screen_t *p) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = p->x, + .y = p->y, + .w = d->screen_rows, + .h = d->screen_cols, + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = true, + }, + .color = sd_map_color(p->color), + .text = (const char *)p->text, + .len = p->text_len, + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 执行亮度设定('7' 命令:'0' 自动调光 / '1'~'5' 手动档)。 + * @param level 协议亮度档位(0~5)。 + * @note 自动档恢复光敏任务;手动档挂起光敏任务并映射硬件亮度 + * {3,4,5,7,8}(协议最大档对应硬件最亮档)。 + */ +static void _sd_exec_brightness(uint8_t level) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + if (level == 0) { + /* 自动调节亮度:恢复光敏任务(若此前被手动档挂起) */ + if (g_light_sensor_task_handle != nullptr) + osThreadResume(g_light_sensor_task_handle); + return; + } + if (level < 1 || level > 5) + return; + if (g_light_sensor_task_handle != nullptr) + osThreadSuspend(g_light_sensor_task_handle); /* 手动设定 → 关闭自动调光 */ + dev_display_set_brightness(d, s_sd_brightness_map[level - 1]); +} + +/** + * @brief 执行外设控制('8' 命令:bit0 绿灯 / bit1 红灯 / bit2 黄闪报警)。 + * @param ctrl 控制位图。 + * @note 车道灯为单灯互斥(true=绿/false=红):红绿同置时红灯优先。 + */ +static void _sd_exec_peripheral(uint8_t ctrl) +{ + bool green = (ctrl & 0x01U) != 0U; + bool red = (ctrl & 0x02U) != 0U; + bool yellow = (ctrl & 0x04U) != 0U; + if (red) { + dev_io_lane_light(false); + } else if (green) { + dev_io_lane_light(true); + } + dev_io_flash_light(yellow); +} + +/** + * @brief 执行山东协议命令。 + * @param ch 当前通道。 + * @param cmd 解析后的命令结构体。 + * @note 协议未定义应答机制:除 '2' 取版本号回裸 ASCII 产品程序编码 + * PROGRAM_CODE 外,其余命令单向执行不回;解析失败静默丢弃。 + */ +void sd_execute_cmd(channel_t *ch, const sd_parsed_cmd_t *cmd) +{ + if (!cmd || cmd->sta != SD_PARSE_OK) + return; + + switch (cmd->cmd) { + case SD_PCMD_FILL_ALL: + _sd_exec_fill_all(cmd->p.fill_all.color); + break; + case SD_PCMD_VERSION: + /* 裸 ASCII 应答产品程序编码 PROGRAM_CODE */ + channel_send(ch, (uint8_t *)PROGRAM_CODE, sizeof(PROGRAM_CODE) - 1U); + break; + case SD_PCMD_ONE_LINE: + _sd_exec_one_line(&cmd->p.one_line); + break; + case SD_PCMD_FULL_SCREEN: + _sd_exec_full_screen(&cmd->p.full_screen); + break; + case SD_PCMD_CLEAR: + _sd_clear_screen(); + break; + case SD_PCMD_BRIGHTNESS: + _sd_exec_brightness(cmd->p.brightness.level); + break; + case SD_PCMD_PERIPHERAL: + _sd_exec_peripheral(cmd->p.peripheral.ctrl); + break; + default: + break; + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_ShanDong/app_sd_proto_default.c b/Application/Src/ProtocolParser_ShanDong/app_sd_proto_default.c new file mode 100644 index 0000000..69035b9 --- /dev/null +++ b/Application/Src/ProtocolParser_ShanDong/app_sd_proto_default.c @@ -0,0 +1,54 @@ +/** + * @file app_sd_proto_default.c + * @brief 山东协议上电效果 — 注册默认显示界面 + * + * 协议文档 §0:上电后点阵全屏显示「山东省 高速公路 欢迎您」。 + * 经 app_default_display_register 注册为默认显示回调(sw_app_initcall + * 早于 init_task 中 app_default_display 调用),覆盖默认欢迎画面。 + */ + +#include + +#include "app_default_display.h" +#include "app_render.h" +#include "dev_display.h" +#include "initcall.h" + +/* 「山东省 高速公路 欢迎您」(协议文档 §0 上电显示原文,UTF-8 字面量) */ +static const uint8_t s_sd_default_text[] = "山东省\n高速公路\n欢迎您"; + +/** @brief 清屏后居中渲染「山东省 高速公路 欢迎您」(FONT_16 居中黄字)。 */ +static void _sd_default_show(void) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + dev_display_commit_frame(d); + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 0, + .w = d->screen_rows, + .h = d->screen_cols, + .style = &(render_style_t){ + .h_align = ALIGN_CENTER, + .v_align = ALIGN_CENTER, + .word_wrap = false, + }, + .color = COLOR_YELLOW, + .text = (const char *)s_sd_default_text, + .len = sizeof(s_sd_default_text) - 1U, + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_UTF8, + }); +} + +static void sd_default_display_init(void) +{ + app_default_display_register(_sd_default_show); +} +sw_app_initcall(sd_default_display_init); \ No newline at end of file diff --git a/Application/Src/ProtocolParser_ShanDong/app_sd_proto_parse.c b/Application/Src/ProtocolParser_ShanDong/app_sd_proto_parse.c new file mode 100644 index 0000000..c9489b9 --- /dev/null +++ b/Application/Src/ProtocolParser_ShanDong/app_sd_proto_parse.c @@ -0,0 +1,130 @@ +/** + * @file app_sd_proto_parse.c + * @brief 山东车道费额显示器通信协议帧解析(纯解析:不发包、不渲染、不碰外设) + * + * 帧格式:'{' + 命令字 + 二进制长度 + 参数 + '}'。 + * 协议原文:39山东车道费额显示器通信协议(命令字 '1'~'5','7','8',无 '6')。 + */ + +#include "app_sd_proto_parse.h" + +/** + * @brief 将山东协议命令码字符转换为内部命令枚举。 + * @param c 协议中的命令码字符。 + * @return 对应的协议命令枚举;非法字符返回 SD_PCMD_INVALID。 + */ +static sd_pcmd_t sd_char_to_cmd(uint8_t c) +{ + switch (c) { + case '1': return SD_PCMD_FILL_ALL; + case '2': return SD_PCMD_VERSION; + case '3': return SD_PCMD_ONE_LINE; + case '4': return SD_PCMD_FULL_SCREEN; + case '5': return SD_PCMD_CLEAR; + case '7': return SD_PCMD_BRIGHTNESS; + case '8': return SD_PCMD_PERIPHERAL; + default: return SD_PCMD_INVALID; + } +} + +/** + * @brief 解析山东协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体;状态字段 sta 标识解析结果。 + * @note 长度字段为二进制字节值,表示参数区长度。 + * '4' 命令文本中的回车对 0x0A 0x0D 原样透传:0x0A 由渲染引擎换行, + * 0x0D 非 GBK 前导字节由渲染引擎跳过,无需归一化。 + */ +sd_parsed_cmd_t sd_parse_frame(const uint8_t *raw, uint16_t raw_len) +{ + sd_parsed_cmd_t cmd = {0}; + if (!raw || raw_len < 4 || raw[0] != '{' || raw[raw_len - 1] != '}') { + cmd.sta = SD_PARSE_ERR_FRAME; + return cmd; + } + + cmd.cmd = sd_char_to_cmd(raw[1]); + if (cmd.cmd == SD_PCMD_INVALID) { + cmd.sta = SD_PARSE_ERR_CMD; + return cmd; + } + + /* 长度字段是二进制字节值,不是 ASCII 数字。 */ + uint16_t declared_len = raw[2]; + if ((uint16_t)(raw_len - 4) < declared_len) { + cmd.sta = SD_PARSE_ERR_FRAME; + return cmd; + } + + cmd.data = &raw[3]; + cmd.data_len = declared_len; + cmd.sta = SD_PARSE_OK; + + switch (cmd.cmd) { + case SD_PCMD_FILL_ALL: /* '1' 全屏单色:参数 1B 二进制 01红/02绿/03黄(协议原文) */ + if (declared_len != 1 || cmd.data[0] < 1 || cmd.data[0] > 3) { + cmd.sta = SD_PARSE_ERR_PARAM; + break; + } + cmd.p.fill_all.color = cmd.data[0] - 1; /* 归一化 0 红 / 1 绿 / 2 黄 */ + break; + case SD_PCMD_VERSION: /* '2' 取版本号:参数 1B(协议示例 00) */ + if (declared_len != 1) { + cmd.sta = SD_PARSE_ERR_PARAM; + break; + } + break; + case SD_PCMD_ONE_LINE: /* '3' 单行:颜色 + 行号 + 文本 */ + if (declared_len < 2) { + cmd.sta = SD_PARSE_ERR_PARAM; + break; + } + cmd.p.one_line.color = (uint8_t)(cmd.data[0] - '0'); + cmd.p.one_line.row = (uint8_t)(cmd.data[1] - '1'); + cmd.p.one_line.text = &cmd.data[2]; + cmd.p.one_line.text_len = declared_len - 2; + if (cmd.p.one_line.color > 2 || cmd.p.one_line.row > 4) + cmd.sta = SD_PARSE_ERR_PARAM; + break; + case SD_PCMD_FULL_SCREEN: /* '4' 全屏可编辑:颜色 + X + Y + 文本 */ + if (declared_len < 3) { + cmd.sta = SD_PARSE_ERR_PARAM; + break; + } + cmd.p.full_screen.color = (uint8_t)(cmd.data[0] - '0'); + cmd.p.full_screen.x = cmd.data[1]; + cmd.p.full_screen.y = cmd.data[2]; + cmd.p.full_screen.text = &cmd.data[3]; + cmd.p.full_screen.text_len = declared_len - 3; + if (cmd.p.full_screen.color > 2) + cmd.sta = SD_PARSE_ERR_PARAM; + break; + case SD_PCMD_CLEAR: /* '5' 全屏清除:无参数 */ + if (declared_len != 0) { + cmd.sta = SD_PARSE_ERR_PARAM; + break; + } + break; + case SD_PCMD_BRIGHTNESS: /* '7' 亮度:'0'~'5',0=自动调节 */ + if (declared_len != 1) { + cmd.sta = SD_PARSE_ERR_PARAM; + break; + } + cmd.p.brightness.level = (uint8_t)(cmd.data[0] - '0'); + if (cmd.p.brightness.level > 5) + cmd.sta = SD_PARSE_ERR_PARAM; + break; + case SD_PCMD_PERIPHERAL: /* '8' 外设:bit0 绿灯 / bit1 红灯 / bit2 黄闪报警 */ + if (declared_len != 1) { + cmd.sta = SD_PARSE_ERR_PARAM; + break; + } + cmd.p.peripheral.ctrl = cmd.data[0]; + break; + default: + break; + } + + return cmd; +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto.c b/Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto.c new file mode 100644 index 0000000..032c3a9 --- /dev/null +++ b/Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto.c @@ -0,0 +1,294 @@ +/** + * @file app_sc_etc_proto.c + * @brief 四川 ETC 费显协议模块(1D)— 注册 / 探测 / 处理任务 / 心跳计时 + * + * 绑定 RS485 + RS232 双通道(与青海同模式:acquire → register → bind → set_frame_queue)。 + * 心跳机制原由固件实现:独立计时任务,5 分钟未收到任何有效帧(心跳/显示/灯控/亮度) + * 则显示「ETC车道关闭」;收到有效帧刷新计时。无重发/超时逻辑。 + * 2026-08-17:心跳计时任务已停用(#if 0 + 创建处注释)——「ETC车道关闭」超时显示 + * 与黄闪 10 秒自动关闭(依赖该任务 tick)一并失效,详见下文「心跳超时计时」注释。 + */ + +#include "app_sc_etc_proto.h" + +#include "FreeRTOS.h" +#include "initcall.h" +#include "app_dispatch.h" + +#include "app_sc_etc_proto_parse.h" +#include "app_sc_etc_proto_cmd.h" + +/* 地区协议通道 RB:与青海/RLS 等同槽 weak 合并 */ +RB_PROVIDE_WEAK(rb_provide_rs485, RB_SIZE_RS485); +RB_PROVIDE_WEAK(rb_provide_rs232, RB_SIZE_RS232); + +static proto_mask_t s_sc_etc_mask; +static proto_mask_t s_sc_etc_mask_rs232; +static osMessageQueueId_t s_sc_etc_queue; + +/* 静态队列:payload 覆盖协议最大帧。 + * 0x0D 定界扫描上限:扫描索引 >148 即拒 → 0x0D 最迟位于索引 148 → 帧总长 ≤149B(头 3 + 数据 ≤145 + 0D)。 + * 全屏数据无固定 56B 长度,变长按收到内容渲染。 */ +#define SC_ETC_PAYLOAD_MAX (149U) +#define SC_ETC_MSG_SIZE (sizeof(frame_msg_t) + SC_ETC_PAYLOAD_MAX) +#define SC_ETC_QUEUE_DEPTH (3U) +_Static_assert(SC_ETC_PAYLOAD_MAX <= RB_SIZE_RS485, "SC_ETC frame must fit RS485 RB"); +_Static_assert(SC_ETC_PAYLOAD_MAX <= RB_SIZE_RS232, "SC_ETC frame must fit RS232 RB"); +static StaticQueue_t s_sc_etc_queue_cb; +static uint8_t s_sc_etc_queue_buf[SC_ETC_QUEUE_DEPTH * SC_ETC_MSG_SIZE]; +static const osMessageQueueAttr_t s_sc_etc_queue_attr = { + .name = "sc_etc_queue", + .cb_mem = &s_sc_etc_queue_cb, + .cb_size = sizeof(s_sc_etc_queue_cb), + .mq_mem = s_sc_etc_queue_buf, + .mq_size = sizeof(s_sc_etc_queue_buf), +}; + +/* ---- 心跳超时计时 ---- */ + +/** 5 分钟无有效帧 → 显示「ETC车道关闭,请择道行驶」(协议 §7) */ +#define SC_ETC_HEARTBEAT_TIMEOUT_S (300U) + +/** ETC 黄闪自动关闭时长(10 秒,1s 递减到 0 关黄闪) */ +#define SC_ETC_HS_TIMEOUT_S (10U) + +/* 2026-08-17:心跳超时显示功能已停用——sc_etc_heartbeat_task 以 #if 0 包裹、 + * sc_etc_proto_init 中任务创建已注释。以下状态与 API 保留但暂无消费方: + * - s_etc_uptime_s 不再递增,s_etc_activity_s / s_etc_closed_shown 仅被 + * sc_etc_activity_refresh 写入、无读取方(刷新调用保留,无害); + * - 黄闪 10 秒自动关闭依赖本任务 tick(s_etc_uptime_s 每秒 +1 后与 + * s_etc_hs_off_s 比较)→ 一并失效:0A 38 黄闪开启后不再自动关闭, + * 须由上位机 0A 39 显式关闭(sc_etc_hs_timer_arm/cancel 仍被调用,仅记录状态)。 + * 恢复方法:删除任务函数 #if 0/#endif,并恢复 sc_etc_proto_init 中任务创建 + * (sc_etc_heartbeat_task,栈 256×4=1KB),上述两项功能同时恢复。 */ + +static volatile uint32_t s_etc_uptime_s; /**< 计时任务秒计数(递增) */ +static volatile uint32_t s_etc_activity_s; /**< 最近一次有效帧时刻(秒) */ +static volatile bool s_etc_closed_shown; /**< 车道关闭画面是否已渲染 */ +static volatile uint32_t s_etc_hs_off_s; /**< 黄闪自动关闭时刻(0=未启用),handle/计时任务交叉读写 */ + +/** + * @brief 刷新心跳活动计时(收到有效帧时调用)。 + * @note 32 位对齐读写原子;计时任务与处理任务交叉访问,误差 ≤1s 可接受。 + * 2026-08-17:心跳计时任务已停用(#if 0),本函数暂无读取方, + * 保留调用与实现以便恢复(无害,仅写两个 volatile 状态)。 + */ +static void sc_etc_activity_refresh(void) +{ + s_etc_activity_s = s_etc_uptime_s; + s_etc_closed_shown = false; +} + +/** @brief 0x38 黄闪开:启动 10 秒自动关闭计时。 */ +void sc_etc_hs_timer_arm(void) +{ + s_etc_hs_off_s = s_etc_uptime_s + SC_ETC_HS_TIMEOUT_S; +} + +/** @brief 0x39 黄闪关:清零自动关闭计时。 */ +void sc_etc_hs_timer_cancel(void) +{ + s_etc_hs_off_s = 0U; +} + +/** + * @brief 心跳计时任务:每秒检查一次超时,超时后渲染车道关闭画面一次, + * 并处理 ETC 黄闪 10 秒自动关闭。 + * @param argument 任务参数,当前未使用。 + * + * 2026-08-17 已停用(#if 0):心跳超时显示功能注释下线。注意本任务同时承担 + * 黄闪 10 秒自动关闭的 tick(s_etc_uptime_s 递增 + s_etc_hs_off_s 判定), + * 停用后 0A 38 黄闪开启不再 10 秒自动关闭,须由 0A 39 显式关闭。 + * 恢复方法:删除 #if 0/#endif 并恢复 sc_etc_proto_init 中任务创建。 + */ +#if 0 +static void sc_etc_heartbeat_task(void *argument) +{ + (void)argument; + for (;;) { + osDelay(1000); + s_etc_uptime_s++; + + if (s_etc_hs_off_s != 0U && s_etc_uptime_s >= s_etc_hs_off_s) { + s_etc_hs_off_s = 0U; + sc_etc_hs_timeout(); + } + + if (!s_etc_closed_shown && (s_etc_uptime_s - s_etc_activity_s) >= SC_ETC_HEARTBEAT_TIMEOUT_S) { + s_etc_closed_shown = true; + sc_etc_show_lane_closed(); + } + } +} +#endif + +static proto_probe_sta_t sc_etc_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux); + +/** + * @brief 四川 ETC 费显协议模块初始化入口。 + * @note 通过 initcall 自动注册到系统启动流程: + * 申请 RS485 / RS232 RB → 注册探测器 → 绑定双通道 → 创建队列与任务。 + * + * @warning CH_ID_RS232_1 (PL_UART6) 为语音板 TTS 专用,任何地区协议禁止绑定。 + */ +void sc_etc_proto_init(void) +{ + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RS485, RB_SIZE_RS485); + if (rb == nullptr) + return; + + s_sc_etc_mask = app_proto_register(sc_etc_probe_frame, rb); + if (s_sc_etc_mask == 0) + return; + + app_proto_bind_channel(s_sc_etc_mask, CH_ID_RS485); + + ring_buffer_t *rb_rs232 = app_proto_acquire_buf(RB_SLOT_RS232, RB_SIZE_RS232); + if (rb_rs232 != nullptr) { + s_sc_etc_mask_rs232 = app_proto_register(sc_etc_probe_frame, rb_rs232); + if (s_sc_etc_mask_rs232 != 0) + app_proto_bind_channel(s_sc_etc_mask_rs232, CH_ID_RS232); + } + + s_sc_etc_queue = osMessageQueueNew(SC_ETC_QUEUE_DEPTH, SC_ETC_MSG_SIZE, &s_sc_etc_queue_attr); + app_proto_set_frame_queue(s_sc_etc_mask, s_sc_etc_queue); + if (s_sc_etc_mask_rs232 != 0) + app_proto_set_frame_queue(s_sc_etc_mask_rs232, s_sc_etc_queue); + + static const osThreadAttr_t s_sc_etc_task_attr = { + .name = "sc_etc_handle_task", + .stack_size = 256 * 4, + .priority = osPriorityNormal, + }; + osThreadNew(sc_etc_proto_handle_task, NULL, &s_sc_etc_task_attr); + + /* 2026-08-17:心跳计时任务已停用(见 sc_etc_heartbeat_task #if 0 注释)—— + * 5 分钟超时「ETC车道关闭」显示与黄闪 10 秒自动关闭均不再生效。 + * 恢复方法:取消本段注释(任务栈 256×4=1KB,自 ucHeap 支出)。 */ + // static const osThreadAttr_t s_sc_etc_timer_attr = { + // .name = "sc_etc_heartbeat_task", + // .stack_size = 256 * 4, + // .priority = osPriorityLow, + // }; + // osThreadNew(sc_etc_heartbeat_task, NULL, &s_sc_etc_timer_attr); +} +sw_app_initcall(sc_etc_proto_init); + +/** + * @brief ETC 协议帧探测(PURE 函数契约:只 rb_peek,无副作用) + * + * 首字节快速拒绝:首字节非 0x0A → 立即 FAKE。 + * 帧结构(无长度字段,以 0x0D 结尾): + * - 灯控/心跳 0A 36/37/38/39/50 0D → 固定 3 字节 + * - 亮度 0A 40 XX YY 0D → 固定 5 字节 + * - 显示 0A 00/01 <行号 0~6> 数据 0D → 变长,扫描 0x0D + * (≤149 字节;0x0D 定界扫描索引 >148 拒帧) + * 0A 46 0A/0A 46 0D(MTC 主机查询/清屏)命令位 0x46 不在本协议命令集 → FAKE,交由 MTC probe。 + */ +static proto_probe_sta_t sc_etc_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux) +{ + (void)ch; + (void)aux; + uint32_t avail = rb_avail(rb, NULL); + + /* 首字节快速拒绝:无数据 → FAKE;首字节非 0x0A → FAKE */ + if (avail == 0) + return PROTO_PROBE_FAKE; + + uint8_t first_byte; + rb_peek(rb, 0, &first_byte, 1, NULL); + if (first_byte != 0x0A) + return PROTO_PROBE_FAKE; + + if (avail < 2U) + return PROTO_PROBE_WAIT; + + uint8_t cmd_byte; + rb_peek(rb, 1, &cmd_byte, 1, NULL); + + switch (cmd_byte) { + case 0x36: /* 交通灯红 */ + case 0x37: /* 交通灯绿 */ + case 0x38: /* 黄闪开 */ + case 0x39: /* 黄闪关 */ + case 0x50: /* 心跳 */ + if (avail < 3U) + return PROTO_PROBE_WAIT; + { + uint8_t tail; + rb_peek(rb, 2, &tail, 1, NULL); + if (tail != 0x0D) + return PROTO_PROBE_FAKE; + } + *total_len = 3U; + return PROTO_PROBE_READY; + + case 0x40: /* 亮度 0A 40 XX YY 0D */ + if (avail < 5U) + return PROTO_PROBE_WAIT; + { + uint8_t tail; + rb_peek(rb, 4, &tail, 1, NULL); + if (tail != 0x0D) + return PROTO_PROBE_FAKE; + } + *total_len = 5U; + return PROTO_PROBE_READY; + + case 0x00: /* 静态显示 */ + case 0x01: { /* 滚屏显示 */ + if (avail < 3U) + return PROTO_PROBE_WAIT; + uint8_t row; + rb_peek(rb, 2, &row, 1, NULL); + if (row > 6U) /* 单行行号 1~6,0=全屏 */ + return PROTO_PROBE_FAKE; + + /* 变长数据:扫描 0x0D 结尾;上限 149 字节 + * (0x0D 定界扫描索引 >148 即拒 → 0x0D 最迟索引 148 → 帧 ≤149B; + * 数据段无固定 56B 长度)。 + * 滚屏帧(协议 §9)头 6 字节固定(0A 01 00 md rt st),从索引 6 起扫描, + * 避免 rt/st 控制字节恰为 0x0D 时提前误定界。 */ + const uint32_t max_total = SC_ETC_PAYLOAD_MAX; + const uint32_t scan_from = (cmd_byte == 0x01U) ? 6U : 3U; + for (uint32_t off = scan_from; off < avail && off < max_total; off++) { + uint8_t b; + rb_peek(rb, off, &b, 1, NULL); + if (b == 0x0D) { + *total_len = off + 1U; + return PROTO_PROBE_READY; + } + } + /* 已到上限仍未见 0x0D → 超长/错帧,交重同步 */ + if (avail >= max_total) + return PROTO_PROBE_FAKE; + return PROTO_PROBE_WAIT; + } + + default: /* 0x46 等不属于 ETC 命令集(MTC 的 0A 46 帧)→ FAKE */ + return PROTO_PROBE_FAKE; + } +} + +/** + * @brief ETC 协议帧处理任务。 + * @param argument 任务参数,当前未使用。 + */ +void sc_etc_proto_handle_task(void *argument) +{ + (void)argument; + static uint8_t msg_buf[SC_ETC_MSG_SIZE]; + frame_msg_t *msg = (frame_msg_t *)msg_buf; + for (;;) { + if (osOK != osMessageQueueGet(s_sc_etc_queue, msg, NULL, osWaitForever)) { + continue; + } + sc_etc_parsed_cmd_t cmd = sc_etc_parse_frame(msg->data, msg->data_len); + if (cmd.sta == SC_ETC_PARSE_OK) { + sc_etc_activity_refresh(); /* 心跳计时任务已停用,刷新暂无消费方(保留待恢复) */ + } + sc_etc_execute_cmd(msg->ch, &cmd); + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_cmd.c b/Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_cmd.c new file mode 100644 index 0000000..b7442f0 --- /dev/null +++ b/Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_cmd.c @@ -0,0 +1,269 @@ +/** + * @file app_sc_etc_proto_cmd.c + * @brief 四川 ETC 费显协议(1D)命令执行 + * + * 显示内容映射到 app_render(GBK 直通渲染,行高 FONT_16); + * 灯控映射到 dev_io_lane_light / dev_io_flash_light; + * 上行应答「收到即回」:00 正常 / 01 数据超长 / 02 + * 帧错误;心跳帧不回(文档单向)。 + */ + +#include "app_sc_etc_proto_cmd.h" +#include "app_dispatch.h" + +#include +#include + +#include "app_light_sensor.h" +#include "app_render.h" +#include "dev_display.h" +#include "dev_io_ctrl.h" +#include "stm32f4xx_hal.h" + +/** 每行数据字节数(原 14B/7 字;单行上限 24B/12 字) */ +#define SC_ETC_BYTES_PER_LINE (24U) +/** 屏上行数(支持行号 1~6) */ +#define SC_ETC_LINE_COUNT (6U) + +/** 显示颜色跟踪:显示颜色跟随通行灯状态 + * (0A 36 红 → 红字;0A 37 绿 → 绿字;上电默认绿)。 */ +static display_color_t s_etc_color = COLOR_GREEN; + +/* 「ETC车道关闭,请择道行驶」(四川ETC费显协议 §7 心跳机制原文,UTF-8 字面量) */ +static const uint8_t s_sc_etc_lane_closed_text[] = "ETC车道关闭,请择道行驶"; + +/** + * @brief 发送屏应答帧 `0A XX 0D`。 + * @param ch 当前通道。 + * @param code 返回码:00 正常 / 01 超长 / 02 帧错。 + */ +static void _sc_etc_send_ack(channel_t *ch, uint8_t code) { + const uint8_t ack[3] = {0x0A, code, 0x0D}; + channel_send(ch, (uint8_t *)ack, sizeof(ack)); +} + +/** + * @brief 清空整屏(置黑并提交)。 + */ +static void _sc_etc_clear_screen(void) { + dev_display_t *d = dev_display_get(); + if (!d) + return; + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + dev_display_commit_frame(d); +} + +/** + * @brief 渲染一行文本。 + * @param row 行索引 0~5(对应协议行号 1~6)。 + * @param text 文本(GBK/ASCII 混合)。 + * @param len 文本字节数。 + * @note 先整行清黑再渲染收到内容 + * (文本短于行宽时旧内容残留必须清除)。 + */ +static void _sc_etc_render_line(uint8_t row, const uint8_t *text, + uint16_t len) { + dev_display_t *d = dev_display_get(); + if (!d) + return; + + dev_display_fill(d, 0, (uint16_t)row * FONT_16, d->screen_rows, FONT_16, + COLOR_BLACK); + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = (uint16_t)row * FONT_16, + .w = d->screen_rows, + .h = FONT_16, + .style = + &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = false, + }, + .color = s_etc_color, /* 显示颜色跟随通行灯状态 */ + .text = (const char *)text, + .len = len, + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 执行显示命令(静态/滚屏)。 + * @param p 显示参数。 + * @note 命令语义: + * 0x20=清屏(行号 0 清全屏,行号 n 清第 n 行); + * 0x30=初始化 → 软件复位(NVIC_SystemReset,非清屏); + * 全屏数据由字库引擎按屏宽自动换行, + * 本实现等效为整屏 word_wrap 渲染;滚屏按静态全屏显示。 + */ +static void _sc_etc_exec_display(const sc_etc_display_t *p) { + dev_display_t *d = dev_display_get(); + if (!d) + return; + + if (p->text_len > 0U) { + if (p->text[0] == 0x20) { /* 清屏 */ + if (p->row == 0U) { + _sc_etc_clear_screen(); + } else { + dev_display_fill(d, 0, (uint16_t)(p->row - 1U) * FONT_16, + d->screen_rows, FONT_16, COLOR_BLACK); + dev_display_commit_frame(d); + } + return; + } + if (p->text[0] == 0x30) { /* 初始化 → 软件复位 */ + NVIC_SystemReset(); + return; + } + } + + if (p->text_len == 0U) + return; + + if (p->row == 0U) { + /* 全屏(含滚屏):先清屏再自第 1 行第 1 列按屏宽自动换行渲染, + * 变长数据(≤145B)按收到内容渲染,无固定 56B 布局 */ + _sc_etc_clear_screen(); + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 0, + .w = d->screen_rows, + .h = d->screen_cols, + .style = + &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = true, + }, + .color = s_etc_color, + .text = (const char *)p->text, + .len = p->text_len, + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); + } else { + _sc_etc_render_line((uint8_t)(p->row - 1U), p->text, p->text_len); + } +} + +/** + * @brief 执行亮度设置:XX 00~07 → 硬件 1~8 档(YY 保留位忽略)。 + * @param level 协议亮度值。 + * @note 依据四川ETC费显协议 §6:XX 00~07 亮度,00 为最暗、07 为最亮 + * (无自动调光档位定义)→ 硬件档 = XX + 1;手动设定时关闭自动调光。 + */ +static void _sc_etc_exec_brightness(uint8_t level) { + dev_display_t *d = dev_display_get(); + if (!d) + return; + if (g_light_sensor_task_handle != nullptr) + osThreadSuspend(g_light_sensor_task_handle); /* 手动设定 → 关闭自动调光 */ + dev_display_set_brightness( + d, (uint8_t)(level + 1U)); /* 00→1(最暗)… 07→8(最亮) */ +} + +/** + * @brief 心跳超时渲染「ETC车道关闭,请择道行驶」(依据四川ETC费显协议 §7)。 + * @note 2026-08-17:唯一调用方 sc_etc_heartbeat_task 已 #if 0 停用,本函数 + * 保留待恢复(无引用,链接期被 --gc-sections 丢弃,不占 Flash)。 + */ +void sc_etc_show_lane_closed(void) { + dev_display_t *d = dev_display_get(); + if (!d) + return; + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + dev_display_commit_frame(d); + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 0, + .w = d->screen_rows, + .h = d->screen_cols, + .style = + &(render_style_t){ + .h_align = ALIGN_CENTER, + .v_align = ALIGN_CENTER, + .word_wrap = true, + }, + .color = COLOR_RED, + .text = (const char *)s_sc_etc_lane_closed_text, + .len = sizeof(s_sc_etc_lane_closed_text) - 1U, + .font_size = FONT_SELF_ADAPT, + .font_type = FONT_ST, + .text_enc = FONT_ENC_UTF8, + }); +} + +/** + * @brief 黄闪 10 秒自动关闭到期回调(由心跳计时任务调用)。 + * @note 黄闪计时递减到 0 → 关闭黄闪。 + * 2026-08-17:唯一调用方(心跳计时任务)已 #if 0 停用,黄闪不再 + * 10 秒自动关闭(须 0A 39 显式关闭);本函数保留待恢复。 + */ +void sc_etc_hs_timeout(void) { dev_io_flash_light(false); } + +/** + * @brief 执行 ETC 协议命令。 + * @param ch 当前通道。 + * @param cmd 解析后的命令结构体。 + * @note 收到即回:正常 0A 00 0D / 超长 0A 01 0D / 帧错 0A 02 0D; + * 心跳帧为单向保活,按文档不回。 + */ +void sc_etc_execute_cmd(channel_t *ch, const sc_etc_parsed_cmd_t *cmd) { + if (!cmd) + return; + + if (cmd->sta != SC_ETC_PARSE_OK) { + /* 解析失败仍应答:超长 01,帧错/参数错 02 */ + _sc_etc_send_ack(ch, (cmd->sta == SC_ETC_PARSE_ERR_TOOLONG) ? 0x01 : 0x02); + return; + } + + switch (cmd->cmd) { + case SC_ETC_PCMD_DISPLAY: + _sc_etc_exec_display(&cmd->p.display); + _sc_etc_send_ack(ch, 0x00); + break; + case SC_ETC_PCMD_LIGHT_RED: + s_etc_color = COLOR_RED; /* 红灯 → 后续显示红色 */ + dev_io_lane_light(false); + _sc_etc_send_ack(ch, 0x00); + break; + case SC_ETC_PCMD_LIGHT_GREEN: + s_etc_color = COLOR_GREEN; /* 绿灯 → 后续显示绿色 */ + dev_io_lane_light(true); + _sc_etc_send_ack(ch, 0x00); + break; + case SC_ETC_PCMD_LIGHT_YELLOW_ON: + dev_io_flash_light(true); + sc_etc_hs_timer_arm(); /* 心跳任务已停用:10 秒自动关闭不再触发,需 0A 39 + 显式关闭 */ + _sc_etc_send_ack(ch, 0x00); + break; + case SC_ETC_PCMD_LIGHT_YELLOW_OFF: + dev_io_flash_light(false); + sc_etc_hs_timer_cancel(); /* 心跳任务已停用:计时清零仅记录状态(见 + app_sc_etc_proto.c 注释) */ + _sc_etc_send_ack(ch, 0x00); + break; + case SC_ETC_PCMD_BRIGHTNESS: + _sc_etc_exec_brightness(cmd->p.brightness.level); + _sc_etc_send_ack(ch, 0x00); + break; + case SC_ETC_PCMD_HEARTBEAT: + /* 心跳帧(0A 50 0D):解析保留(无害)——仅被识别后丢弃,不回。 + * 2026-08-17:心跳超时显示功能已停用(sc_etc_heartbeat_task #if 0), + * 本帧不再驱动任何计时。 */ + break; + default: + break; + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_parse.c b/Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_parse.c new file mode 100644 index 0000000..dd7f983 --- /dev/null +++ b/Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_parse.c @@ -0,0 +1,140 @@ +/** + * @file app_sc_etc_proto_parse.c + * @brief 四川 ETC 费显协议(1D)帧解析 + * + * 帧结构:0x0A + 显示方式 + 行号 + 数据 + 0x0D;数据变长(0x0D 定界,无固定 56B 全屏长度), + * 单行 ≤24B、全屏 ≤145B(GBK,汉字高位在前)。 + * 数据首字节 0x20=清屏(行号 0 时清全屏);0x30=初始化(实际行为=软件复位)。 + * 返回码约定:00 正常 / 01 数据超长 / 02 帧头帧尾或命令编号错误。 + */ + +#include "app_sc_etc_proto_parse.h" + +/** + * @brief ETC 帧各类型最大数据字节数。 + * @note 单行数据 ≤24B(12 汉字); + * 全屏无独立上限,仅受 0x0D 定界扫描约束(扫描索引 >148 拒帧) + * → 帧总长 ≤149 → 数据 ≤145B(帧长减 4 字节开销:头 3 + 0D)。 + */ +#define SC_ETC_DATA_MAX_FULL (145U) /* 全屏(0x0D 定界扫描索引 ≤148 → 149-4) */ +#define SC_ETC_DATA_MAX_LINE (24U) /* 单行(上限 24B) */ +#define SC_ETC_DATA_MAX_SCROLL (142U) /* 滚屏数据(协议 §9 帧 0A 01 00 md rt st d0..dn 0D: + * 7 字节帧开销 → 149-7=142,与 0x0D 定界上限一致) */ + +/** + * @brief 将协议第二字节(显示方式/灯控/亮度/心跳命令位)转换为内部命令枚举。 + * @param c 命令字节。 + * @return 对应的协议命令枚举;非法返回 SC_ETC_PCMD_INVALID。 + */ +static sc_etc_pcmd_t _sc_etc_char_to_cmd(uint8_t c) +{ + switch (c) { + case 0x00: + case 0x01: return SC_ETC_PCMD_DISPLAY; + case 0x36: return SC_ETC_PCMD_LIGHT_RED; + case 0x37: return SC_ETC_PCMD_LIGHT_GREEN; + case 0x38: return SC_ETC_PCMD_LIGHT_YELLOW_ON; + case 0x39: return SC_ETC_PCMD_LIGHT_YELLOW_OFF; + case 0x40: return SC_ETC_PCMD_BRIGHTNESS; + case 0x50: return SC_ETC_PCMD_HEARTBEAT; + default: return SC_ETC_PCMD_INVALID; + } +} + +/** + * @brief 解析 ETC 协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体;状态字段 sta 标识解析结果。 + */ +sc_etc_parsed_cmd_t sc_etc_parse_frame(const uint8_t *raw, uint16_t raw_len) +{ + sc_etc_parsed_cmd_t cmd = {0}; + if (!raw || raw_len < 3 || raw[0] != 0x0A || raw[raw_len - 1] != 0x0D) { + cmd.sta = SC_ETC_PARSE_ERR_FRAME; + return cmd; + } + + cmd.cmd = _sc_etc_char_to_cmd(raw[1]); + if (cmd.cmd == SC_ETC_PCMD_INVALID) { + cmd.sta = SC_ETC_PARSE_ERR_CMD; + return cmd; + } + + cmd.data = &raw[2]; + cmd.data_len = (uint16_t)(raw_len - 3U); /* 去掉 0A + 命令位 + 0D */ + + switch (cmd.cmd) { + case SC_ETC_PCMD_LIGHT_RED: + case SC_ETC_PCMD_LIGHT_GREEN: + case SC_ETC_PCMD_LIGHT_YELLOW_ON: + case SC_ETC_PCMD_LIGHT_YELLOW_OFF: + case SC_ETC_PCMD_HEARTBEAT: + if (raw_len != 3U) { + cmd.sta = SC_ETC_PARSE_ERR_FRAME; + } else { + cmd.sta = SC_ETC_PARSE_OK; + } + break; + + case SC_ETC_PCMD_BRIGHTNESS: + if (raw_len != 5U) { + cmd.sta = SC_ETC_PARSE_ERR_FRAME; + break; + } + cmd.p.brightness.level = raw[2]; + if (cmd.p.brightness.level > 7U) { + cmd.sta = SC_ETC_PARSE_ERR_PARAM; + } else { + cmd.sta = SC_ETC_PARSE_OK; + } + break; + + case SC_ETC_PCMD_DISPLAY: { + if (raw_len < 4U) { /* 0A + 方式 + 行号 + 0D 至少 4 字节 */ + cmd.sta = SC_ETC_PARSE_ERR_PARAM; + break; + } + cmd.p.display.mode = raw[1]; + cmd.p.display.row = raw[2]; + if (cmd.p.display.mode == 0x01U) { + /* 滚屏帧(协议 §9,仅全屏有效):0A 01 00 md rt st d0..dn 0D — + * 行号固定 00,跳过 md/rt/st 三控制字节 */ + if (raw[2] != 0U) { + cmd.sta = SC_ETC_PARSE_ERR_PARAM; + break; + } + if (raw_len < 7U) { + cmd.sta = SC_ETC_PARSE_ERR_PARAM; + break; + } + cmd.p.display.text = &raw[6]; + cmd.p.display.text_len = (uint16_t)(raw_len - 7U); + } else { + if (cmd.p.display.row > 6U) { /* 单行行号 1~6,0=全屏 */ + cmd.sta = SC_ETC_PARSE_ERR_PARAM; + break; + } + cmd.p.display.text = &raw[3]; + cmd.p.display.text_len = (uint16_t)(raw_len - 4U); + } + + uint16_t max_data = SC_ETC_DATA_MAX_SCROLL; + if (cmd.p.display.mode == 0x00) { + max_data = (cmd.p.display.row == 0U) ? SC_ETC_DATA_MAX_FULL : SC_ETC_DATA_MAX_LINE; + } + if (cmd.p.display.text_len > max_data) { + cmd.sta = SC_ETC_PARSE_ERR_TOOLONG; /* 返回码 01:数据超长 */ + } else { + cmd.sta = SC_ETC_PARSE_OK; + } + break; + } + + default: + cmd.sta = SC_ETC_PARSE_ERR_CMD; + break; + } + + return cmd; +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto.c b/Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto.c new file mode 100644 index 0000000..db04edc --- /dev/null +++ b/Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto.c @@ -0,0 +1,190 @@ +/** + * @file app_sc_mtc_proto.c + * @brief 四川 MTC 费显协议模块(1E 方案二)— 注册 / 探测 / 处理任务 + * + * 绑定 RS485 + RS232 双通道。探测支持双帧型: + * 1. '{' (0x7B) 帧族:'1'~'9','A' 命令与 7B 40~45 原始帧族,统一按「'}' 定界 + * 变长」扫描(各命令逐字节扫 '}',帧长 = '}' 下标 + 1, + * 无长度字段、无 BCC;上限 SC_MTC_PAYLOAD_MAX=74B 设备约束); + * 2. 0A 46 0A / 0A 46 0D 主机查询/清屏。 + * 与青海协议(同为 '{' 开头)的互斥分析见 doc/05_协议模块多协议兼容优化/01_architecture.md §6: + * qh_proto_init 先于 sc_mtc_proto_init 注册(字母序),QH probe 先被调用,完整青海帧 + * 由 QH 认领;残余巧合见文档说明,量产由 EIDE 目录排除纪律兜底。 + */ + +#include "app_sc_mtc_proto.h" + +#include "FreeRTOS.h" +#include "initcall.h" +#include "app_dispatch.h" + +#include "app_sc_mtc_proto_parse.h" +#include "app_sc_mtc_proto_cmd.h" + +/* '{' 帧族互斥守卫:同一构建只允许编入一个 '{' 帧族协议(青海/山东/贵州/四川MTC)。 + * 多个同时编入 → 链接期 multiple definition 强制报错; + * Makefile 全协议开发构建定义 STD_ALL_PROTO 跳过守卫(共存由 probe 注册序与文档纪律约束)。 */ +#ifndef STD_ALL_PROTO +__attribute__((used)) char g_brace_proto_guard; +#endif + +/* 地区协议通道 RB:与青海/ETC/治超等同槽 weak 合并 */ +RB_PROVIDE_WEAK(rb_provide_rs485, RB_SIZE_RS485); +RB_PROVIDE_WEAK(rb_provide_rs232, RB_SIZE_RS232); + +static proto_mask_t s_sc_mtc_mask; +static proto_mask_t s_sc_mtc_mask_rs232; +static osMessageQueueId_t s_sc_mtc_queue; + +/* 静态队列:payload 覆盖协议最大帧('}' 定界扫描上限 74B;'4' 全屏 ≤68B / '7'自定义语音 ≤74B) */ +#define SC_MTC_PAYLOAD_MAX (74U) +#define SC_MTC_MSG_SIZE (sizeof(frame_msg_t) + SC_MTC_PAYLOAD_MAX) +#define SC_MTC_QUEUE_DEPTH (3U) +_Static_assert(SC_MTC_PAYLOAD_MAX <= RB_SIZE_RS485, "SC_MTC frame must fit RS485 RB"); +_Static_assert(SC_MTC_PAYLOAD_MAX <= RB_SIZE_RS232, "SC_MTC frame must fit RS232 RB"); +static StaticQueue_t s_sc_mtc_queue_cb; +static uint8_t s_sc_mtc_queue_buf[SC_MTC_QUEUE_DEPTH * SC_MTC_MSG_SIZE]; +static const osMessageQueueAttr_t s_sc_mtc_queue_attr = { + .name = "sc_mtc_queue", + .cb_mem = &s_sc_mtc_queue_cb, + .cb_size = sizeof(s_sc_mtc_queue_cb), + .mq_mem = s_sc_mtc_queue_buf, + .mq_size = sizeof(s_sc_mtc_queue_buf), +}; + +static proto_probe_sta_t sc_mtc_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux); + +/** + * @brief 四川 MTC 费显协议模块初始化入口。 + * @note 通过 initcall 自动注册:申请 RS485 / RS232 RB → 注册探测器 → + * 绑定双通道 → 创建队列与任务。 + * + * @warning CH_ID_RS232_1 (PL_UART6) 为语音板 TTS 专用,任何地区协议禁止绑定。 + */ +void sc_mtc_proto_init(void) +{ + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RS485, RB_SIZE_RS485); + if (rb == nullptr) + return; + + s_sc_mtc_mask = app_proto_register(sc_mtc_probe_frame, rb); + if (s_sc_mtc_mask == 0) + return; + + app_proto_bind_channel(s_sc_mtc_mask, CH_ID_RS485); + + ring_buffer_t *rb_rs232 = app_proto_acquire_buf(RB_SLOT_RS232, RB_SIZE_RS232); + if (rb_rs232 != nullptr) { + s_sc_mtc_mask_rs232 = app_proto_register(sc_mtc_probe_frame, rb_rs232); + if (s_sc_mtc_mask_rs232 != 0) + app_proto_bind_channel(s_sc_mtc_mask_rs232, CH_ID_RS232); + } + + s_sc_mtc_queue = osMessageQueueNew(SC_MTC_QUEUE_DEPTH, SC_MTC_MSG_SIZE, &s_sc_mtc_queue_attr); + app_proto_set_frame_queue(s_sc_mtc_mask, s_sc_mtc_queue); + if (s_sc_mtc_mask_rs232 != 0) + app_proto_set_frame_queue(s_sc_mtc_mask_rs232, s_sc_mtc_queue); + + static const osThreadAttr_t s_sc_mtc_task_attr = { + .name = "sc_mtc_handle_task", + .stack_size = 256 * 4, + .priority = osPriorityNormal, + }; + osThreadNew(sc_mtc_proto_handle_task, NULL, &s_sc_mtc_task_attr); +} +sw_app_initcall(sc_mtc_proto_init); + +/** + * @brief MTC 协议帧探测(PURE 函数契约:只 rb_peek,无副作用) + * + * 首字节快速拒绝:非 '{' 且非 0x0A → 立即 FAKE。 + * + * '{' 帧族('}' 定界变长,无长度字段、无 BCC): + * - '1'~'9','A' 与 0x40~45 原始帧族统一:从下标 2 起扫描 '}', + * 帧长 = '}' 下标 + 1;上限 SC_MTC_PAYLOAD_MAX(74B,帧队列 payload + * 设备约束;协议上限 228)。未闭合 → 缓冲不足 WAIT(帧跨 DMA 块), + * 达到上限仍无 '}' → FAKE 逐字节重同步。 + * - BCC 变体不单独判别:数据段内字节一律视为内容(带 BCC 帧的 + * BCC 字节会成为文本尾部一并渲染;BCC 不校验)。 + * - 0x41='A' 双义(4B 点阵大小 / 5B 颜色)由 parse 按帧长细分。 + * 青海 'A'/'B' 命令字重叠:qh_proto_init 先于 sc_mtc_proto_init 注册 + * (字母序 q < s),同 RB 上 QH probe 先被调用,详见 doc/05 兼容矩阵。 + * 0x0A 帧族:仅 0A 46 0A / 0A 46 0D 两种,其余(含 ETC 0A 帧)→ FAKE。 + */ +static proto_probe_sta_t sc_mtc_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux) +{ + (void)ch; + (void)aux; + uint32_t avail = rb_avail(rb, NULL); + + /* 首字节快速拒绝:无数据 → FAKE */ + if (avail == 0) + return PROTO_PROBE_FAKE; + + uint8_t first_byte; + rb_peek(rb, 0, &first_byte, 1, NULL); + + if (first_byte == 0x0A) { + /* 主机查询 0A 46 0A / 清屏 0A 46 0D;其余 0A 帧属 ETC → FAKE */ + if (avail < 3U) + return PROTO_PROBE_WAIT; + uint8_t b[2]; + rb_peek(rb, 1, b, sizeof(b), NULL); + if (b[0] == 0x46 && (b[1] == 0x0A || b[1] == 0x0D)) { + *total_len = 3U; + return PROTO_PROBE_READY; + } + return PROTO_PROBE_FAKE; + } + + if (first_byte != '{') + return PROTO_PROBE_FAKE; + + if (avail < 2U) + return PROTO_PROBE_WAIT; + + uint8_t cmd_byte; + rb_peek(rb, 1, &cmd_byte, 1, NULL); + + /* ---- '{' 帧族:'}' 定界变长扫描 ---- + * '{' 后取命令字,各命令逐字节扫 '}' 定帧尾, + * 帧长 = '}' 下标 + 1。'1'~'9','A' 与 + * 7B 40~45 原始帧族统一走本扫描;长度/参数合法性由 parse 按命令校验。 */ + if (!((cmd_byte >= '1' && cmd_byte <= '9') || + (cmd_byte >= 0x40 && cmd_byte <= 0x45))) /* 'A'=0x41 已含于 40~45 */ + return PROTO_PROBE_FAKE; + + uint32_t scan_limit = (avail < SC_MTC_PAYLOAD_MAX) ? avail : SC_MTC_PAYLOAD_MAX; + for (uint32_t e = 2U; e < scan_limit; e++) { /* e = '}' 下标(最小 2:{x}) */ + uint8_t b; + rb_peek(rb, e, &b, 1, NULL); + if (b != '}') + continue; + *total_len = e + 1U; + return PROTO_PROBE_READY; + } + /* 未找到 '}':缓冲不足 → WAIT(帧跨 DMA 块);达到上限仍无 → FAKE 重同步 */ + if (avail >= SC_MTC_PAYLOAD_MAX) + return PROTO_PROBE_FAKE; + return PROTO_PROBE_WAIT; +} + +/** + * @brief MTC 协议帧处理任务。 + * @param argument 任务参数,当前未使用。 + */ +void sc_mtc_proto_handle_task(void *argument) +{ + (void)argument; + static uint8_t msg_buf[SC_MTC_MSG_SIZE]; + frame_msg_t *msg = (frame_msg_t *)msg_buf; + for (;;) { + if (osOK != osMessageQueueGet(s_sc_mtc_queue, msg, NULL, osWaitForever)) { + continue; + } + sc_mtc_parsed_cmd_t cmd = sc_mtc_parse_frame(msg->data, msg->data_len); + sc_mtc_execute_cmd(msg->ch, &cmd); + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_cmd.c b/Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_cmd.c new file mode 100644 index 0000000..e67292c --- /dev/null +++ b/Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_cmd.c @@ -0,0 +1,450 @@ +/** + * @file app_sc_mtc_proto_cmd.c + * @brief 四川 MTC 费显协议(1E 方案二)命令执行 + * + * 显示映射到 app_render(GBK 直通渲染,行高 FONT_16,字型/字号可被 7B 41/42 修改); + * '7' 语音对接 dev_rs232_voice(参照青海 _voice.c 用法); + * 7B 40 改波特率通过 app_uart_baud 同步切换 RS232 + RS485 两路 UART; + * 上行应答「收到即回」:0A 46 0A → 0A 64 0A;7B 45 → "SC_FX_P7.62_1.0"。 + */ + +#include "app_sc_mtc_proto_cmd.h" +#include "app_dispatch.h" + +#include +#include + +#include "app_render.h" +#include "app_light_sensor.h" +#include "app_uart_baud.h" +#include "dev_display.h" +#include "dev_io_ctrl.h" +#include "dev_rs232_voice.h" +#include "app_sc_mtc_proto_voice.h" +#include "text_cvt.h" + +/* ---- 显示状态(7B 41/42/'A' 修改,后续显示命令生效)---- */ +static display_color_t s_mtc_color = COLOR_RED; /* 'A' 默认红 */ +static font_size_t s_mtc_font_size = FONT_16; /* 7B 41 默认 16 点阵 */ +static font_type_t s_mtc_font_type = FONT_ST; /* 7B 42 默认宋体 */ + +/** 7B 45 版本号应答 */ +static const uint8_t s_mtc_version_text[] = "SC_FX_P7.62_1.0"; + +/** + * @brief 清空整屏(置黑并提交)。 + */ +static void _sc_mtc_clear_screen(void) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + dev_display_commit_frame(d); +} + +/** + * @brief 渲染一行文本(当前颜色/字号/字型)。 + * @param row 行索引 0~5(协议行号 '1'~'6',FONT16 下 6 行屏)。 + * @param text 文本(GBK/ASCII 混合)。 + * @param len 文本字节数。 + * @note 先整行清黑再渲染收到内容 + * (文本短于行宽时旧内容残留必须清除)。 + */ +static void _sc_mtc_render_line(uint8_t row, const uint8_t *text, uint16_t len) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + dev_display_fill(d, 0, (uint16_t)row * (uint16_t)s_mtc_font_size, d->screen_rows, + (uint16_t)s_mtc_font_size, COLOR_BLACK); + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = (uint16_t)row * (uint16_t)s_mtc_font_size, + .w = d->screen_rows, + .h = (uint16_t)s_mtc_font_size, + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = false, + }, + .color = s_mtc_color, + .text = (const char *)text, + .len = len, + .font_size = s_mtc_font_size, + .font_type = s_mtc_font_type, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 居中渲染单行文本(初始化提示,UTF-8 文本)。 + * @param text 文本。 + * @param len 文本字节数。 + */ +static void _sc_mtc_render_center(const uint8_t *text, uint16_t len) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 0, + .w = d->screen_rows, + .h = d->screen_cols, + .style = &(render_style_t){ + .h_align = ALIGN_CENTER, + .v_align = ALIGN_CENTER, + .word_wrap = true, + }, + .color = s_mtc_color, + .text = (const char *)text, + .len = len, + .font_size = FONT_SELF_ADAPT, + .font_type = s_mtc_font_type, + .text_enc = FONT_ENC_UTF8, + }); +} + +/** + * @brief 执行 '1' 初始化:显示「祝您一路平安」并语音播报。 + * @note 文档要求「稍候熄灭」,本实现无定时熄灭(可后续加计时任务)。 + */ +static void _sc_mtc_exec_init(void) +{ + /* 「祝您一路平安」(协议 '1' 初始化显示 + 语音,UTF-8 字面量) */ + static const uint8_t text[] = "祝您一路平安"; + _sc_mtc_render_center(text, (uint16_t)(sizeof(text) - 1U)); + + /* 语音板要求 GBK:UTF-8 源文运行时转换后播报 */ + uint8_t gbk[32]; + uint32_t gbk_len = sizeof(gbk); + UTF8ToGBK((const char *)text, (uint32_t)(sizeof(text) - 1U), (char *)gbk, &gbk_len); + dev_rs232_voice_play(gbk, (uint16_t)gbk_len); +} + +/** + * @brief 执行 '2' 自检:全屏点亮 + 播报自检语音(参照青海自检实现)。 + */ +static void _sc_mtc_exec_self_check(void) +{ + dev_display_t *d = dev_display_get(); + if (d) { + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_YELLOW); + dev_display_commit_frame(d); + } + + /* 「系统正在加电自检」(自检语音,UTF-8 字面量;语音板要求 GBK,运行时转换) */ + static const uint8_t text[] = "系统正在加电自检"; + uint8_t gbk[32]; + uint32_t gbk_len = sizeof(gbk); + UTF8ToGBK((const char *)text, (uint32_t)(sizeof(text) - 1U), (char *)gbk, &gbk_len); + dev_rs232_voice_play(gbk, (uint16_t)gbk_len); +} + +/** + * @brief 执行 '3' 单行显示。 + * @param p 单行参数(行号 + 变长文本)。 + */ +static void _sc_mtc_exec_one_line(const sc_mtc_one_line_t *p) +{ + _sc_mtc_render_line(p->row, p->text, p->text_len); +} + +/** + * @brief 执行 '4' 全屏显示:先整屏清黑,变长文本按屏宽自动换行渲染。 + * @param p 全屏参数。 + * @note 先清屏再渲染(字库引擎按屏宽自动排版)。对齐四川 ETC 全屏与 + * 治超 80 全屏先例:w=screen_rows(屏宽)、h=screen_cols(整屏高)、 + * word_wrap=true,由渲染引擎按当前字号(7B 41 可改)自动折行。 + * 旧实现按 16B/行固定切分 ≤4 行且单行 word_wrap=false:第一行超宽时 + * 溢出被裁、总文本超 64B 被丢弃,故弃用。 + */ +static void _sc_mtc_exec_full_screen(const sc_mtc_full_screen_t *p) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + if (p->text_len == 0U) { + dev_display_commit_frame(d); + return; + } + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 0, + .w = d->screen_rows, + .h = d->screen_cols, + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = true, + }, + .color = s_mtc_color, + .text = (const char *)p->text, + .len = p->text_len, + .font_size = s_mtc_font_size, + .font_type = s_mtc_font_type, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 执行 '6' 固定格式显示(客车 X1~X11 / 货车 X1~X20 字段布局)。 + * @param p 固定格式参数。 + * + * 客车(type=0,X0='0' 后 11B): + * 第一行「车型:X1」;第二行「金额:X2X3X4X5X6元」;第三行「余额:X7X8X9X10X11元」。 + * 货车(type=1,X0='1' 后 20B): + * 第一行「总重:X1X2X3.X4X5吨」;第二行「金额:X6X7X8X9X10元」; + * 第三行「余额:X11X12X13X14X15元」;第四行「超重:X16X17X18.X19X20吨」。 + * + * @note 文档货车示例存在两处笔误("X1X12X13" 应为 "X1X2X3";第四行 "总重" 应为 + * "超重"),本实现按字段说明修正。 + */ +static void _sc_mtc_exec_fixed(const sc_mtc_fixed_t *p) +{ + /* 固定格式标签(UTF-8 字面量,运行时转 GBK 参与行缓冲拼接): + * 车型:/ 金额:/ 余额:/ 总重:/ 超重:/ 元 / 吨 */ + uint8_t lbl_type[6], lbl_amount[6], lbl_balance[6], lbl_weight[6], lbl_over[6]; + uint8_t lbl_yuan[2], lbl_ton[2]; + uint32_t gbk_len; + gbk_len = sizeof(lbl_type); + UTF8ToGBK("车型:", (uint32_t)(sizeof("车型:") - 1U), (char *)lbl_type, &gbk_len); + gbk_len = sizeof(lbl_amount); + UTF8ToGBK("金额:", (uint32_t)(sizeof("金额:") - 1U), (char *)lbl_amount, &gbk_len); + gbk_len = sizeof(lbl_balance); + UTF8ToGBK("余额:", (uint32_t)(sizeof("余额:") - 1U), (char *)lbl_balance, &gbk_len); + gbk_len = sizeof(lbl_weight); + UTF8ToGBK("总重:", (uint32_t)(sizeof("总重:") - 1U), (char *)lbl_weight, &gbk_len); + gbk_len = sizeof(lbl_over); + UTF8ToGBK("超重:", (uint32_t)(sizeof("超重:") - 1U), (char *)lbl_over, &gbk_len); + gbk_len = sizeof(lbl_yuan); + UTF8ToGBK("元", (uint32_t)(sizeof("元") - 1U), (char *)lbl_yuan, &gbk_len); + gbk_len = sizeof(lbl_ton); + UTF8ToGBK("吨", (uint32_t)(sizeof("吨") - 1U), (char *)lbl_ton, &gbk_len); + + uint8_t buf[4][24]; /* 文本行缓冲(标签 6B + 数字 ≤7B + 单位 2B + '.' 1B) */ + uint16_t buf_len[4] = {0}; + uint8_t line_count = 0; + + /* p->raw 由 parse 定位在 X1 起(X1 为首参数字节); + * 此前实现误再偏移一位导致各字段整体错位 */ + const uint8_t *dta = p->raw; /* X1 起 */ + + if (p->type == 0U) { + if (p->raw_len < 11U) + return; + /* 车型:X1 */ + memcpy(&buf[line_count][0], lbl_type, sizeof(lbl_type)); + buf[line_count][sizeof(lbl_type)] = dta[0]; + buf_len[line_count] = (uint16_t)(sizeof(lbl_type) + 1U); + line_count++; + /* 金额:X2~X6 元 */ + memcpy(&buf[line_count][0], lbl_amount, sizeof(lbl_amount)); + memcpy(&buf[line_count][sizeof(lbl_amount)], &dta[1], 5U); + memcpy(&buf[line_count][sizeof(lbl_amount) + 5U], lbl_yuan, sizeof(lbl_yuan)); + buf_len[line_count] = (uint16_t)(sizeof(lbl_amount) + 5U + sizeof(lbl_yuan)); + line_count++; + /* 余额:X7~X11 元 */ + memcpy(&buf[line_count][0], lbl_balance, sizeof(lbl_balance)); + memcpy(&buf[line_count][sizeof(lbl_balance)], &dta[6], 5U); + memcpy(&buf[line_count][sizeof(lbl_balance) + 5U], lbl_yuan, sizeof(lbl_yuan)); + buf_len[line_count] = (uint16_t)(sizeof(lbl_balance) + 5U + sizeof(lbl_yuan)); + line_count++; + } else { + if (p->raw_len < 20U) + return; + /* 总重:X1X2X3.X4X5 吨 */ + memcpy(&buf[line_count][0], lbl_weight, sizeof(lbl_weight)); + memcpy(&buf[line_count][sizeof(lbl_weight)], &dta[0], 3U); + buf[line_count][sizeof(lbl_weight) + 3U] = '.'; + memcpy(&buf[line_count][sizeof(lbl_weight) + 4U], &dta[3], 2U); + memcpy(&buf[line_count][sizeof(lbl_weight) + 6U], lbl_ton, sizeof(lbl_ton)); + buf_len[line_count] = (uint16_t)(sizeof(lbl_weight) + 6U + sizeof(lbl_ton)); + line_count++; + /* 金额:X6~X10 元 */ + memcpy(&buf[line_count][0], lbl_amount, sizeof(lbl_amount)); + memcpy(&buf[line_count][sizeof(lbl_amount)], &dta[5], 5U); + memcpy(&buf[line_count][sizeof(lbl_amount) + 5U], lbl_yuan, sizeof(lbl_yuan)); + buf_len[line_count] = (uint16_t)(sizeof(lbl_amount) + 5U + sizeof(lbl_yuan)); + line_count++; + /* 余额:X11~X15 元 */ + memcpy(&buf[line_count][0], lbl_balance, sizeof(lbl_balance)); + memcpy(&buf[line_count][sizeof(lbl_balance)], &dta[10], 5U); + memcpy(&buf[line_count][sizeof(lbl_balance) + 5U], lbl_yuan, sizeof(lbl_yuan)); + buf_len[line_count] = (uint16_t)(sizeof(lbl_balance) + 5U + sizeof(lbl_yuan)); + line_count++; + /* 超重:X16X17X18.X19X20 吨(放第 4 行,索引 3) */ + memcpy(&buf[3][0], lbl_over, sizeof(lbl_over)); /* 暂存,稍后统一渲染 */ + memcpy(&buf[3][sizeof(lbl_over)], &dta[15], 3U); + buf[3][sizeof(lbl_over) + 3U] = '.'; + memcpy(&buf[3][sizeof(lbl_over) + 4U], &dta[18], 2U); + memcpy(&buf[3][sizeof(lbl_over) + 6U], lbl_ton, sizeof(lbl_ton)); + uint16_t over_len = (uint16_t)(sizeof(lbl_over) + 6U + sizeof(lbl_ton)); + + for (uint8_t i = 0; i < line_count; i++) { + _sc_mtc_render_line(i, buf[i], buf_len[i]); + } + _sc_mtc_render_line(3, buf[3], over_len); /* 超重固定第 4 行 */ + return; + } + + for (uint8_t i = 0; i < line_count; i++) { + _sc_mtc_render_line(i, buf[i], buf_len[i]); + } +} + +/** + * @brief 执行 '8' 亮度设定:0 开启自动调光(光敏),1~8 直接映射硬件档。 + * @param val 亮度值 0~8(二进制或 ASCII 均由 parse 归一)。 + * @note 0 → 自动调光;1~8 → 关闭自动调光并直接映射硬件档。 + */ +static void _sc_mtc_exec_brightness(uint8_t val) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + if (val == 0U) { + if (g_light_sensor_task_handle != nullptr) + osThreadResume(g_light_sensor_task_handle); /* 开启自动调光(恢复光敏任务) */ + return; + } + if (g_light_sensor_task_handle != nullptr) + osThreadSuspend(g_light_sensor_task_handle); /* 关闭自动调光 */ + dev_display_set_brightness(d, val); +} + +/** + * @brief 执行 'A' 显示颜色设定:'1'红 / '2'黄 / '3'绿。 + * @param val 颜色索引 1~3。 + */ +static void _sc_mtc_exec_color(uint8_t val) +{ + static const display_color_t color_map[] = { + [1] = COLOR_RED, + [2] = COLOR_YELLOW, + [3] = COLOR_GREEN, + }; + s_mtc_color = color_map[val]; +} + +/** + * @brief 执行 7B 40 修改波特率:按文档波特率低 16 位切换 RS232 + RS485。 + * @param p 波特率参数。 + * @note 0x2580=9600、0xC200=115200(低 16 位);切换后本通道以新波特率继续工作, + * 主机亦须同步切换。文档未定义应答帧,不回。 + */ +static void _sc_mtc_exec_baud(const sc_mtc_baud_t *p) +{ + uint32_t baud = (p->baud16 == 0x2580U) ? 9600U : 115200U; + (void)app_uart_baud_apply(baud); +} + +/** + * @brief 执行 7B 44 全屏点亮:00 红 / 01 绿 / 02 黄 全屏填充。 + * @param val 颜色索引。 + */ +static void _sc_mtc_exec_fill_all(uint8_t val) +{ + static const display_color_t color_map[] = { + [0] = COLOR_RED, + [1] = COLOR_GREEN, + [2] = COLOR_YELLOW, + }; + dev_display_t *d = dev_display_get(); + if (!d) + return; + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, color_map[val]); + dev_display_commit_frame(d); +} + +/** + * @brief 执行 MTC 协议命令。 + * @param ch 当前通道。 + * @param cmd 解析后的命令结构体。 + * @note 上行应答「收到即回」:主机查询 0A 46 0A → 0A 64 0A; + * 7B 45 取版本号 → "SC_FX_P7.62_1.0"。其余命令文档未定义应答,不回。 + */ +void sc_mtc_execute_cmd(channel_t *ch, const sc_mtc_parsed_cmd_t *cmd) +{ + if (!cmd || cmd->sta != SC_MTC_PARSE_OK) + return; + + switch (cmd->cmd) { + case SC_MTC_PCMD_INIT: + _sc_mtc_exec_init(); + break; + case SC_MTC_PCMD_SELF_CHECK: + _sc_mtc_exec_self_check(); + break; + case SC_MTC_PCMD_ONE_LINE: + _sc_mtc_exec_one_line(&cmd->p.one_line); + break; + case SC_MTC_PCMD_FULL_SCREEN: + _sc_mtc_exec_full_screen(&cmd->p.full_screen); + break; + case SC_MTC_PCMD_CLEAR: + _sc_mtc_clear_screen(); + break; + case SC_MTC_PCMD_FIXED_DISPLAY: + _sc_mtc_exec_fixed(&cmd->p.fixed); + break; + case SC_MTC_PCMD_VOICE: + if (cmd->p.voice.idx == 8U) { + dev_rs232_voice_play(cmd->p.voice.text, cmd->p.voice.text_len); + } else { + sc_mtc_voice_play_fixed(cmd->p.voice.idx); + } + break; + case SC_MTC_PCMD_BRIGHTNESS: + _sc_mtc_exec_brightness(cmd->p.byte_val.val); + break; + case SC_MTC_PCMD_VOLUME: + dev_rs232_voice_volume(cmd->p.byte_val.val); + break; + case SC_MTC_PCMD_COLOR: + _sc_mtc_exec_color(cmd->p.byte_val.val); + break; + case SC_MTC_PCMD_RAW_BAUD: + _sc_mtc_exec_baud(&cmd->p.baud); + break; + case SC_MTC_PCMD_RAW_DOT_SIZE: { + static const font_size_t dot_map[] = {FONT_16, FONT_24, FONT_32}; + s_mtc_font_size = dot_map[cmd->p.byte_val.val]; + break; + } + case SC_MTC_PCMD_RAW_FONT: { + static const font_type_t font_map[] = {FONT_ST, FONT_FS, FONT_KT, FONT_HT}; + s_mtc_font_type = font_map[cmd->p.byte_val.val]; + break; + } + case SC_MTC_PCMD_RAW_PROTO: + /* 设置协议类型(治超屏/ETC/治超屏):本固件已同编三协议,无需切换,仅记录 */ + break; + case SC_MTC_PCMD_RAW_FILL_ALL: + _sc_mtc_exec_fill_all(cmd->p.byte_val.val); + break; + case SC_MTC_PCMD_RAW_VERSION: + channel_send(ch, (uint8_t *)s_mtc_version_text, sizeof(s_mtc_version_text) - 1U); + break; + case SC_MTC_PCMD_HOST_QUERY: { + static const uint8_t rsp_ok[3] = {0x0A, 0x64, 0x0A}; + channel_send(ch, (uint8_t *)rsp_ok, sizeof(rsp_ok)); + break; + } + case SC_MTC_PCMD_HOST_CLEAR: + _sc_mtc_clear_screen(); + break; + default: + break; + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_parse.c b/Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_parse.c new file mode 100644 index 0000000..59ddf08 --- /dev/null +++ b/Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_parse.c @@ -0,0 +1,271 @@ +/** + * @file app_sc_mtc_proto_parse.c + * @brief 四川 MTC 费显协议(1E 方案二)帧解析 + * + * '{' 帧族:'{' + 命令 + 参数 + '}',变长、无长度字段、无 BCC—— + * 各命令逐字节扫 '}' 定帧,字段偏移按「'}' 下标 - 数据起点」 + * 重算;带 BCC 变体不加判别(BCC 字节视为内容尾部),BCC 不校验。 + * 帧长由 probe 的 '}' 扫描决定(上限 SC_MTC_PAYLOAD_MAX)。 + * 7B 40~45 原始帧族同样 '}'-定界,参数长度仍按命令校验。 + * 0A 帧族:0A 46 0A 主机查询 / 0A 46 0D 清屏。 + */ + +#include "app_sc_mtc_proto_parse.h" + +/** + * @brief 将 '{' 帧族命令字符转换为内部命令枚举。 + * @param c 命令字节。 + * @return 对应的协议命令枚举;非法返回 SC_MTC_PCMD_INVALID。 + */ +static sc_mtc_pcmd_t _sc_mtc_char_to_cmd(uint8_t c) +{ + switch (c) { + case '1': return SC_MTC_PCMD_INIT; + case '2': return SC_MTC_PCMD_SELF_CHECK; + case '3': return SC_MTC_PCMD_ONE_LINE; + case '4': return SC_MTC_PCMD_FULL_SCREEN; + case '5': return SC_MTC_PCMD_CLEAR; + case '6': return SC_MTC_PCMD_FIXED_DISPLAY; + case '7': return SC_MTC_PCMD_VOICE; + case '8': return SC_MTC_PCMD_BRIGHTNESS; + case '9': return SC_MTC_PCMD_VOLUME; + case 'A': return SC_MTC_PCMD_COLOR; /* 0x41 与 'A' 同值,按帧长在 parse 中细分 */ + case 0x40: return SC_MTC_PCMD_RAW_BAUD; + case 0x42: return SC_MTC_PCMD_RAW_FONT; + case 0x43: return SC_MTC_PCMD_RAW_PROTO; + case 0x44: return SC_MTC_PCMD_RAW_FILL_ALL; + case 0x45: return SC_MTC_PCMD_RAW_VERSION; + default: return SC_MTC_PCMD_INVALID; + } +} + +/** + * @brief 解析 MTC 协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体;状态字段 sta 标识解析结果。 + */ +sc_mtc_parsed_cmd_t sc_mtc_parse_frame(const uint8_t *raw, uint16_t raw_len) +{ + sc_mtc_parsed_cmd_t cmd = {0}; + if (!raw || raw_len < 3) { + cmd.sta = SC_MTC_PARSE_ERR_FRAME; + return cmd; + } + + /* ---- 0A 帧族:主机查询 / 清屏 ---- */ + if (raw[0] == 0x0A) { + if (raw_len != 3U) { + cmd.sta = SC_MTC_PARSE_ERR_FRAME; + return cmd; + } + if (raw[1] == 0x46 && raw[2] == 0x0A) { + cmd.cmd = SC_MTC_PCMD_HOST_QUERY; + cmd.sta = SC_MTC_PARSE_OK; + } else if (raw[1] == 0x46 && raw[2] == 0x0D) { + cmd.cmd = SC_MTC_PCMD_HOST_CLEAR; + cmd.sta = SC_MTC_PARSE_OK; + } else { + cmd.cmd = SC_MTC_PCMD_INVALID; + cmd.sta = SC_MTC_PARSE_ERR_CMD; + } + return cmd; + } + + /* ---- '{' 帧族 ---- */ + if (raw[0] != '{' || raw[raw_len - 1U] != '}') { + cmd.sta = SC_MTC_PARSE_ERR_FRAME; + return cmd; + } + + cmd.cmd = _sc_mtc_char_to_cmd(raw[1]); + if (cmd.cmd == SC_MTC_PCMD_INVALID) { + cmd.sta = SC_MTC_PARSE_ERR_CMD; + return cmd; + } + + cmd.data = &raw[2]; + cmd.data_len = (uint16_t)(raw_len - 3U); /* 去掉 '{' + 命令 + '}' */ + + /* ---- 0x41 双义:'A'(颜色, 5B+BCC) 与 7B 41(点阵大小, 4B 无 BCC) ---- */ + if (raw[1] == 0x41) { + if (raw_len == 4U) { + cmd.cmd = SC_MTC_PCMD_RAW_DOT_SIZE; + cmd.p.byte_val.val = raw[2]; + cmd.sta = (raw[2] <= 2U) ? SC_MTC_PARSE_OK : SC_MTC_PARSE_ERR_PARAM; + } else if (raw_len == 5U) { + cmd.cmd = SC_MTC_PCMD_COLOR; + cmd.p.byte_val.val = (uint8_t)(raw[2] - '0'); + cmd.sta = (raw[2] >= '1' && raw[2] <= '3') ? SC_MTC_PARSE_OK : SC_MTC_PARSE_ERR_PARAM; + } else { + cmd.sta = SC_MTC_PARSE_ERR_FRAME; + } + return cmd; + } + + /* ---- 7B 40~45 原始帧族(无 BCC)---- */ + switch (cmd.cmd) { + case SC_MTC_PCMD_RAW_BAUD: + if (raw_len != 6U) { + cmd.sta = SC_MTC_PARSE_ERR_FRAME; + return cmd; + } + cmd.p.baud.mode = raw[2]; + cmd.p.baud.baud16 = (uint16_t)(((uint16_t)raw[3] << 8) | raw[4]); + if (cmd.p.baud.mode > 1U || + (cmd.p.baud.baud16 != 0x2580U && cmd.p.baud.baud16 != 0xC200U)) { + cmd.sta = SC_MTC_PARSE_ERR_PARAM; + } else { + cmd.sta = SC_MTC_PARSE_OK; + } + return cmd; + case SC_MTC_PCMD_RAW_FONT: + if (raw_len != 4U) { + cmd.sta = SC_MTC_PARSE_ERR_FRAME; + return cmd; + } + cmd.p.byte_val.val = raw[2]; + cmd.sta = (raw[2] <= 3U) ? SC_MTC_PARSE_OK : SC_MTC_PARSE_ERR_PARAM; + return cmd; + case SC_MTC_PCMD_RAW_PROTO: + if (raw_len != 4U) { + cmd.sta = SC_MTC_PARSE_ERR_FRAME; + return cmd; + } + cmd.p.byte_val.val = raw[2]; + cmd.sta = (raw[2] <= 2U) ? SC_MTC_PARSE_OK : SC_MTC_PARSE_ERR_PARAM; + return cmd; + case SC_MTC_PCMD_RAW_FILL_ALL: + if (raw_len != 4U) { + cmd.sta = SC_MTC_PARSE_ERR_FRAME; + return cmd; + } + cmd.p.byte_val.val = raw[2]; + cmd.sta = (raw[2] <= 2U) ? SC_MTC_PARSE_OK : SC_MTC_PARSE_ERR_PARAM; + return cmd; + case SC_MTC_PCMD_RAW_VERSION: + if (raw_len != 3U) { + cmd.sta = SC_MTC_PARSE_ERR_FRAME; + return cmd; + } + cmd.sta = SC_MTC_PARSE_OK; + return cmd; + default: + break; + } + + /* ---- '1'~'9','A' 帧族:'}' 定界变长,字段偏移按变长重算, + * 带 BCC 变体不加判别(BCC 字节视为内容尾部),BCC 不校验 ---- */ + switch (cmd.cmd) { + case SC_MTC_PCMD_INIT: + case SC_MTC_PCMD_SELF_CHECK: + case SC_MTC_PCMD_CLEAR: + /* 帧长 3B;4B 视为带 BCC 变体(BCC 字节作为内容尾部),容错接受 */ + cmd.sta = (raw_len == 3U || raw_len == 4U) ? SC_MTC_PARSE_OK : SC_MTC_PARSE_ERR_PARAM; + break; + + case SC_MTC_PCMD_ONE_LINE: + /* 文本 = raw[3..'}'-1], + * 长度 = '}' 下标 - 3 = raw_len - 4,变长(协议上限 225B,本设备 70B) */ + if (raw_len < 4U) { + cmd.sta = SC_MTC_PARSE_ERR_PARAM; + break; + } + /* 行号协议支持 '1'~'8';本屏 192×96 最多 6 行 16pt,收 '1'~'6' */ + if (raw[2] < '1' || raw[2] > '6') { + cmd.sta = SC_MTC_PARSE_ERR_PARAM; + break; + } + cmd.p.one_line.row = (uint8_t)(raw[2] - '1'); + cmd.p.one_line.text = &raw[3]; + cmd.p.one_line.text_len = (uint16_t)(raw_len - 4U); + cmd.sta = SC_MTC_PARSE_OK; + break; + + case SC_MTC_PCMD_FULL_SCREEN: + /* 文本 = raw[2..'}'-1],长度 = raw_len - 3, + * 变长(协议上限 226B,本设备 71B);渲染按 16B/行切分 */ + cmd.p.full_screen.text = &raw[2]; + cmd.p.full_screen.text_len = (uint16_t)(raw_len - 3U); + cmd.sta = SC_MTC_PARSE_OK; + break; + + case SC_MTC_PCMD_FIXED_DISPLAY: { + /* 扫 '}' 后按固定位读数字; + * 客车 X1~X11(11B)/ 货车 X1~X20(20B),多出字节(含 BCC 变体)忽略 */ + if (raw_len < 4U) { + cmd.sta = SC_MTC_PARSE_ERR_PARAM; + break; + } + cmd.p.fixed.type = (uint8_t)(raw[2] - '0'); + if (cmd.p.fixed.type > 1U) { + cmd.sta = SC_MTC_PARSE_ERR_PARAM; + break; + } + cmd.p.fixed.raw = &raw[3]; + cmd.p.fixed.raw_len = (uint16_t)(raw_len - 4U); /* X1 起参数字节 */ + if ((cmd.p.fixed.type == 0U && cmd.p.fixed.raw_len < 11U) || + (cmd.p.fixed.type == 1U && cmd.p.fixed.raw_len < 20U)) { + cmd.sta = SC_MTC_PARSE_ERR_PARAM; + break; + } + cmd.sta = SC_MTC_PARSE_OK; + break; + } + + case SC_MTC_PCMD_VOICE: + /* '{7 X }' 4B 固定(X 任意,不播放); + * 本实现 '0'~'7' 固定语音;'8' 自定义文本(本模块扩展): + * 文本 = raw[3..'}'-1],长度 = raw_len - 4 */ + if (raw_len < 4U) { + cmd.sta = SC_MTC_PARSE_ERR_PARAM; + break; + } + if (raw[2] >= '0' && raw[2] <= '7') { + cmd.p.voice.idx = (uint8_t)(raw[2] - '0'); + cmd.sta = SC_MTC_PARSE_OK; + } else if (raw[2] == '8') { + cmd.p.voice.idx = 8U; + cmd.p.voice.text = &raw[3]; + cmd.p.voice.text_len = (uint16_t)(raw_len - 4U); + cmd.sta = (cmd.p.voice.text_len > 0U) ? SC_MTC_PARSE_OK : SC_MTC_PARSE_ERR_PARAM; + } else { + cmd.sta = SC_MTC_PARSE_ERR_PARAM; /* '9' 未定义 */ + } + break; + + case SC_MTC_PCMD_BRIGHTNESS: + if (raw_len < 4U) { + cmd.sta = SC_MTC_PARSE_ERR_PARAM; + break; + } + /* 参数为二进制 0~8 + * (0=自动调光,1~8 硬件档);同时兼容 ASCII '0'~'8' 变体 + * (0x30~0x38 与二进制区间无重叠,二者可安全并存) */ + if (raw[2] <= 8U) { + cmd.p.byte_val.val = raw[2]; + cmd.sta = SC_MTC_PARSE_OK; + } else if (raw[2] >= '0' && raw[2] <= '8') { + cmd.p.byte_val.val = (uint8_t)(raw[2] - '0'); + cmd.sta = SC_MTC_PARSE_OK; + } else { + cmd.sta = SC_MTC_PARSE_ERR_PARAM; + } + break; + + case SC_MTC_PCMD_VOLUME: + if (raw_len < 4U) { + cmd.sta = SC_MTC_PARSE_ERR_PARAM; + break; + } + cmd.p.byte_val.val = (uint8_t)(raw[2] - '0'); + cmd.sta = (raw[2] >= '1' && raw[2] <= '5') ? SC_MTC_PARSE_OK : SC_MTC_PARSE_ERR_PARAM; + break; + + default: + cmd.sta = SC_MTC_PARSE_ERR_CMD; + break; + } + + return cmd; +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_voice.c b/Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_voice.c new file mode 100644 index 0000000..eb31c73 --- /dev/null +++ b/Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_voice.c @@ -0,0 +1,54 @@ +/** + * @file app_sc_mtc_proto_voice.c + * @brief 四川 MTC 费显协议(1E)固定语音播报 + * + * '7' 命令 '0'~'7' 固定语音通过 dev_rs232_voice 直送 TTS 板(USART6 旁路), + * 参照青海 _voice.c 用法。动态变量(车型/金额/总重/超重)TTS 板不支持变量合成, + * 本实现按文档语义取核心用语播报,并在注释标注取舍。 + */ + +#include "app_sc_mtc_proto_voice.h" + +#include +#include + +#include "dev_rs232_voice.h" +#include "text_cvt.h" + +/* '7' 命令 '0'~'7' 固定语音(UTF-8 字面量,播报前运行时转 GBK 送语音板) */ +static const char *const s_mtc_fixed_voices[8] = { + /* '0' 您好,请交费,谢谢合作,祝您一路平安(文档含车型/金额变量,不拼读) */ + "您好,请交费,谢谢合作,祝您一路平安", + /* '1' 您好,请交费(文档含总重/超重/金额变量,不拼读) */ + "您好,请交费", + /* '2' 您好(文档含车型变量,不拼读) */ + "您好", + /* '3' 谢谢合作,祝您一路平安(文档含总重/超重/金额变量,不拼读) */ + "谢谢合作,祝您一路平安", + /* '4' 谢谢合作,祝您一路平安 */ + "谢谢合作,祝您一路平安", + /* '5' 月票车,请通行 */ + "月票车,请通行", + /* '6' 免费车,请通行 */ + "免费车,请通行", + /* '7' 车辆闯关 */ + "车辆闯关", +}; + +/** + * @brief 播报固定语音('7' 命令 '0'~'7')。 + * @param idx 语音索引(0~7)。 + */ +void sc_mtc_voice_play_fixed(uint8_t idx) +{ + if (idx >= (sizeof(s_mtc_fixed_voices) / sizeof(s_mtc_fixed_voices[0]))) { + return; + } + + /* 语音板要求 GBK:UTF-8 源文运行时转换后播报 */ + uint8_t gbk[40]; + uint32_t gbk_len = sizeof(gbk); + UTF8ToGBK(s_mtc_fixed_voices[idx], (uint32_t)strlen(s_mtc_fixed_voices[idx]), + (char *)gbk, &gbk_len); + dev_rs232_voice_play(gbk, (uint16_t)gbk_len); +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto.c b/Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto.c new file mode 100644 index 0000000..b80ae04 --- /dev/null +++ b/Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto.c @@ -0,0 +1,149 @@ +/** + * @file app_sc_ol_proto.c + * @brief 四川治超屏协议模块(1F,3.5.1 串口方式)— 注册 / 探测 / 处理任务 + * + * 绑定 RS485 + RS232 双通道。帧结构 `FF + 长度(≤255,1 字节) + 命令 + 亮度 + 数据 + BCC + FF`。 + * 与 RLS 协议区分:RLS 帧头 `FF FE`,长度字段 0xFE(254) 被 probe 显式排除 → 本 probe 立即 FAKE; + * RLS probe 亦以「第二字节必须 0xFE」拒绝治超帧,双向快拒成立。 + */ + +#include "app_sc_ol_proto.h" + +#include "FreeRTOS.h" +#include "initcall.h" +#include "app_dispatch.h" + +#include "app_sc_ol_proto_parse.h" +#include "app_sc_ol_proto_cmd.h" + +/* 地区协议通道 RB:与青海/ETC/MTC 等同槽 weak 合并 */ +RB_PROVIDE_WEAK(rb_provide_rs485, RB_SIZE_RS485); +RB_PROVIDE_WEAK(rb_provide_rs232, RB_SIZE_RS232); + +static proto_mask_t s_sc_ol_mask; +static proto_mask_t s_sc_ol_mask_rs232; +static osMessageQueueId_t s_sc_ol_queue; + +/* 静态队列:payload 覆盖协议最大帧(长度字段 1 字节上限 0xFF,0xFE 排除; + * 必须 ≥ SC_OL_FRAME_LEN_MAX,否则 250~255 长帧会被队列截断导致越界读) */ +#define SC_OL_PAYLOAD_MAX (SC_OL_FRAME_LEN_MAX) /* 255:覆盖 0x80 全屏长数据段(数据 ≤249) */ +#define SC_OL_MSG_SIZE (sizeof(frame_msg_t) + SC_OL_PAYLOAD_MAX) +#define SC_OL_QUEUE_DEPTH (3U) +_Static_assert(SC_OL_PAYLOAD_MAX <= RB_SIZE_RS485, "SC_OL frame must fit RS485 RB"); +_Static_assert(SC_OL_PAYLOAD_MAX <= RB_SIZE_RS232, "SC_OL frame must fit RS232 RB"); +static StaticQueue_t s_sc_ol_queue_cb; +static uint8_t s_sc_ol_queue_buf[SC_OL_QUEUE_DEPTH * SC_OL_MSG_SIZE]; +static const osMessageQueueAttr_t s_sc_ol_queue_attr = { + .name = "sc_ol_queue", + .cb_mem = &s_sc_ol_queue_cb, + .cb_size = sizeof(s_sc_ol_queue_cb), + .mq_mem = s_sc_ol_queue_buf, + .mq_size = sizeof(s_sc_ol_queue_buf), +}; + +static proto_probe_sta_t sc_ol_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux); + +/** + * @brief 四川治超屏协议模块初始化入口。 + * @note 通过 initcall 自动注册:申请 RS485 / RS232 RB → 注册探测器 → + * 绑定双通道 → 创建队列与任务。 + * + * @warning CH_ID_RS232_1 (PL_UART6) 为语音板 TTS 专用,任何地区协议禁止绑定。 + */ +void sc_ol_proto_init(void) +{ + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RS485, RB_SIZE_RS485); + if (rb == nullptr) + return; + + s_sc_ol_mask = app_proto_register(sc_ol_probe_frame, rb); + if (s_sc_ol_mask == 0) + return; + + app_proto_bind_channel(s_sc_ol_mask, CH_ID_RS485); + + ring_buffer_t *rb_rs232 = app_proto_acquire_buf(RB_SLOT_RS232, RB_SIZE_RS232); + if (rb_rs232 != nullptr) { + s_sc_ol_mask_rs232 = app_proto_register(sc_ol_probe_frame, rb_rs232); + if (s_sc_ol_mask_rs232 != 0) + app_proto_bind_channel(s_sc_ol_mask_rs232, CH_ID_RS232); + } + + s_sc_ol_queue = osMessageQueueNew(SC_OL_QUEUE_DEPTH, SC_OL_MSG_SIZE, &s_sc_ol_queue_attr); + app_proto_set_frame_queue(s_sc_ol_mask, s_sc_ol_queue); + if (s_sc_ol_mask_rs232 != 0) + app_proto_set_frame_queue(s_sc_ol_mask_rs232, s_sc_ol_queue); + + static const osThreadAttr_t s_sc_ol_task_attr = { + .name = "sc_ol_handle_task", + .stack_size = 256 * 4, + .priority = osPriorityNormal, + }; + osThreadNew(sc_ol_proto_handle_task, NULL, &s_sc_ol_task_attr); +} +sw_app_initcall(sc_ol_proto_init); + +/** + * @brief 治超协议帧探测(PURE 函数契约:只 rb_peek,无副作用) + * + * 首字节快速拒绝:首字节非 0xFF → 立即 FAKE。 + * 第二字节为长度字段:合法范围 [7, 255],RLS 的 0xFE(254) 显式排除立即 FAKE。 + * 尾字节必须 0xFF;BCC 校验在 parse 层执行。 + */ +static proto_probe_sta_t sc_ol_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux) +{ + (void)ch; + (void)aux; + uint32_t avail = rb_avail(rb, NULL); + + /* 首字节快速拒绝:无数据 → FAKE;首字节非 0xFF → FAKE */ + if (avail == 0) + return PROTO_PROBE_FAKE; + + uint8_t first_byte; + rb_peek(rb, 0, &first_byte, 1, NULL); + if (first_byte != 0xFF) + return PROTO_PROBE_FAKE; + + if (avail < 2U) + return PROTO_PROBE_WAIT; + + uint8_t len_byte; + rb_peek(rb, 1, &len_byte, 1, NULL); + + /* 长度下界:≥7;RLS 的 FF FE(0xFE=254)必须显式排除,否则会把 RLS 帧当治超长帧吞掉并 WAIT 卡死链式探测 + * (len_byte 为 uint8_t,上限 0xFF 由类型天然约束,无需再比) */ + if (len_byte < SC_OL_FRAME_LEN_MIN || len_byte == 0xFE) + return PROTO_PROBE_FAKE; + + if (avail < len_byte) + return PROTO_PROBE_WAIT; + + uint8_t tail; + rb_peek(rb, (uint32_t)(len_byte - 1U), &tail, 1, NULL); + if (tail != 0xFF) + return PROTO_PROBE_FAKE; + + *total_len = len_byte; + return PROTO_PROBE_READY; +} + +/** + * @brief 治超协议帧处理任务。 + * @param argument 任务参数,当前未使用。 + */ +void sc_ol_proto_handle_task(void *argument) +{ + (void)argument; + static uint8_t msg_buf[SC_OL_MSG_SIZE]; + frame_msg_t *msg = (frame_msg_t *)msg_buf; + for (;;) { + if (osOK != osMessageQueueGet(s_sc_ol_queue, msg, NULL, osWaitForever)) { + continue; + } + sc_ol_parsed_cmd_t cmd = sc_ol_parse_frame(msg->data, msg->data_len); + sc_ol_execute_cmd(msg->ch, &cmd); + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_cmd.c b/Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_cmd.c new file mode 100644 index 0000000..a5abc5d --- /dev/null +++ b/Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_cmd.c @@ -0,0 +1,262 @@ +/** + * @file app_sc_ol_proto_cmd.c + * @brief 四川治超屏协议(1F,3.5.1 串口方式)命令执行 + * + * 显示映射到 app_render(GBK 直通渲染,行高 FONT_16); + * 通行灯/黄闪映射到 dev_io_lane_light / dev_io_flash_light; + * 查询类应答「收到即回」,回显本模块维护的当前状态。 + * + * 应答帧构造说明(文档 3.5.1 (7) 示例内部不一致): + * 文档 A1~A5 示例长度字段为 0x20(32) 但示例帧实际 20 字节(数据 14B,7 字), + * 空行示例仅 7 字节。本实现维持「16B/行」固定应答(FF 16 A <亮度> <16B 数据> BCC FF) + * 以兼容既有测试工具;行存储本身已变长(≤24B,不足不补空格、超长截断)。 + * B6/B9/B8 应答按文档原文回显:FF 07 <命令> 00 <当前值> BCC FF。 + */ + +#include "app_sc_ol_proto_cmd.h" +#include "app_dispatch.h" + +#include +#include + +#include "app_render.h" +#include "app_light_sensor.h" +#include "dev_display.h" +#include "dev_io_ctrl.h" + +/* ---- 当前屏状态(查询应答回显用)---- */ +static uint8_t s_ol_lines[SC_OL_LINE_COUNT][SC_OL_LINE_TEXT_MAX]; /**< 八行显示内容(空行 0x00) */ +static uint8_t s_ol_line_len[SC_OL_LINE_COUNT]; /**< 各行实际文本长度(变长数据) */ +static uint8_t s_ol_brightness = 0xFF; /**< 当前亮度(00 最暗,FF 最亮,初始最亮) */ +static uint8_t s_ol_lane = 0x00; /**< 通行灯状态(00 红 / 01 绿) */ +static uint8_t s_ol_flash = 0x00; /**< 黄闪状态(00 关 / 01 开) */ + +/** 显示颜色跟踪:显示颜色跟随通行灯状态 + * (99 帧 00=红 / 01=绿;上电默认绿)。 */ +static display_color_t s_ol_color = COLOR_GREEN; + +/** + * @brief 治超帧 BCC 计算:帧头(含)到数据段(含)逐字节异或(不含尾部 FF)。 + * @param raw 帧缓冲。 + * @param raw_len 帧长度。 + * @return BCC 值。 + */ +static uint8_t _sc_ol_bcc_calc(const uint8_t *raw, uint16_t raw_len) +{ + uint8_t bcc = 0; + for (uint16_t i = 0; i + 2U < raw_len; i++) { /* [0, len-3],不含 BCC(len-2) 与尾 FF(len-1) */ + bcc ^= raw[i]; + } + return bcc; +} + +/** + * @brief 构造治超应答帧并发送。 + * @param ch 当前通道。 + * @param cmd 应答命令字节(A1~A4 / B6 / B9 / B8)。 + * @param bright 亮度字段。 + * @param data 数据段(可为 NULL)。 + * @param data_len 数据段字节数。 + */ +static void _sc_ol_send_reply(channel_t *ch, uint8_t cmd, uint8_t bright, + const uint8_t *data, uint16_t data_len) +{ + uint8_t buf[SC_OL_FRAME_LEN_MAX]; + uint16_t len = (uint16_t)(6U + data_len); /* FF+len+cmd+bright+data+BCC+FF */ + buf[0] = 0xFF; + buf[1] = (uint8_t)len; + buf[2] = cmd; + buf[3] = bright; + if (data_len > 0U && data != NULL) { + memcpy(&buf[4], data, data_len); + } + buf[len - 2U] = _sc_ol_bcc_calc(buf, len); + buf[len - 1U] = 0xFF; + channel_send(ch, buf, len); +} + +/** + * @brief 清空整屏(置黑并提交),并复位行状态。 + */ +static void _sc_ol_clear_screen(void) +{ + memset(s_ol_lines, 0, sizeof(s_ol_lines)); + memset(s_ol_line_len, 0, sizeof(s_ol_line_len)); + dev_display_t *d = dev_display_get(); + if (!d) + return; + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + dev_display_commit_frame(d); +} + +/** + * @brief 渲染一行并更新行状态。 + * @param p 行显示参数。 + */ +static void _sc_ol_exec_line(const sc_ol_line_t *p) +{ + /* 变长数据存储(不足不补空格,>24B 截断) */ + uint8_t n = (uint8_t)((p->text_len > SC_OL_LINE_TEXT_MAX) ? SC_OL_LINE_TEXT_MAX : p->text_len); + memset(s_ol_lines[p->row], 0, SC_OL_LINE_TEXT_MAX); + memcpy(s_ol_lines[p->row], p->text, n); + s_ol_line_len[p->row] = n; + + dev_display_t *d = dev_display_get(); + if (!d) + return; + + /* 先整行清黑再渲染收到内容 */ + dev_display_fill(d, 0, (uint16_t)p->row * FONT_16, d->screen_rows, FONT_16, COLOR_BLACK); + dev_display_commit_frame(d); + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = (uint16_t)p->row * FONT_16, + .w = d->screen_rows, + .h = FONT_16, + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = false, + }, + .color = s_ol_color, /* 显示颜色跟随通行灯状态 */ + .text = (const char *)s_ol_lines[p->row], + .len = n, + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 执行 80 全屏显示:先整屏清黑再自动换行渲染(0 数据帧 = 清屏)。 + * @note 先整屏清黑再按屏宽自动排版渲染。 + * @param p 全屏显示参数。 + */ +static void _sc_ol_exec_full_screen(const sc_ol_full_screen_t *p) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + if (p->text_len == 0U) { + dev_display_commit_frame(d); + return; + } + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 0, + .w = d->screen_rows, + .h = d->screen_cols, + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = true, + }, + .color = s_ol_color, + .text = (const char *)p->text, + .len = p->text_len, + .font_size = FONT_16, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 执行亮度调节:XX 00=自动调光 / 01~FF 映射硬件 1~8 档(FF 最亮)。 + * @note val==0 → 开启自动调光;非 0 → 关闭自动调光,硬件档=(val+1)/32。 + * @param val 亮度原始值。 + */ +static void _sc_ol_exec_brightness(uint8_t val) +{ + s_ol_brightness = val; + dev_display_t *d = dev_display_get(); + if (!d) + return; + if (val == 0U) { + if (g_light_sensor_task_handle != nullptr) + osThreadResume(g_light_sensor_task_handle); /* 开启自动调光(恢复光敏任务) */ + return; + } + if (g_light_sensor_task_handle != nullptr) + osThreadSuspend(g_light_sensor_task_handle); /* 关闭自动调光 */ + uint8_t level = (uint8_t)((val + 1U) / 32U); + /* 0 档根因防护:val=1~31 时 (val+1)/32=0,light_level=0 → OE 恒高 → 整屏全灭。 + * 硬件档位必须落在 1~8,最低钳制为 1。 */ + if (level == 0U) + level = 1U; + dev_display_set_brightness(d, level); /* (val+1)/32:31~62→1 … 224~255→8 */ +} + +/** + * @brief 执行 A0 查询:逐行回 A1~A8 独立帧。 + * @param ch 当前通道。 + */ +static void _sc_ol_exec_query_content(channel_t *ch) +{ + for (uint8_t i = 0; i < SC_OL_LINE_COUNT; i++) { + _sc_ol_send_reply(ch, (uint8_t)(0xA1U + i), s_ol_brightness, + s_ol_lines[i], SC_OL_BYTES_PER_LINE); + } +} + +/** + * @brief 执行治超协议命令。 + * @param ch 当前通道。 + * @param cmd 解析后的命令结构体。 + */ +void sc_ol_execute_cmd(channel_t *ch, const sc_ol_parsed_cmd_t *cmd) +{ + if (!cmd || cmd->sta != SC_OL_PARSE_OK) + return; + + switch (cmd->cmd) { + case SC_OL_PCMD_FULL_SCREEN: + _sc_ol_exec_full_screen(&cmd->p.full_screen); + break; + case SC_OL_PCMD_LINE_1: + case SC_OL_PCMD_LINE_2: + case SC_OL_PCMD_LINE_3: + case SC_OL_PCMD_LINE_4: + case SC_OL_PCMD_LINE_5: + case SC_OL_PCMD_LINE_6: + case SC_OL_PCMD_LINE_7: + case SC_OL_PCMD_LINE_8: + _sc_ol_exec_line(&cmd->p.line); + break; + case SC_OL_PCMD_CLEAR: + _sc_ol_clear_screen(); + break; + case SC_OL_PCMD_BRIGHTNESS: + _sc_ol_exec_brightness(cmd->p.byte_val.val); + break; + case SC_OL_PCMD_LANE_LIGHT: + s_ol_lane = cmd->p.byte_val.val; + /* 通行灯命令同步更新显示颜色 */ + s_ol_color = (s_ol_lane != 0U) ? COLOR_GREEN : COLOR_RED; + dev_io_lane_light(s_ol_lane != 0U); /* 00 红 / 01 绿 */ + break; + case SC_OL_PCMD_YELLOW_FLASH: + s_ol_flash = cmd->p.byte_val.val; + dev_io_flash_light(s_ol_flash != 0U); /* 00 关 / 01 开 */ + break; + case SC_OL_PCMD_QUERY_CONTENT: + _sc_ol_exec_query_content(ch); + break; + case SC_OL_PCMD_QUERY_BRIGHT: + _sc_ol_send_reply(ch, 0xB6, 0x00, &s_ol_brightness, 1U); + break; + case SC_OL_PCMD_QUERY_LANE: + _sc_ol_send_reply(ch, 0xB9, 0x00, &s_ol_lane, 1U); + break; + case SC_OL_PCMD_QUERY_FLASH: + _sc_ol_send_reply(ch, 0xB8, 0x00, &s_ol_flash, 1U); + break; + default: + break; + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_parse.c b/Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_parse.c new file mode 100644 index 0000000..8d91c73 --- /dev/null +++ b/Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_parse.c @@ -0,0 +1,153 @@ +/** + * @file app_sc_ol_proto_parse.c + * @brief 四川治超屏协议(1F,3.5.1 串口方式)帧解析 + * + * 帧结构:FF + 长度(含头尾总长,07~FF,0xFE 排除) + 命令 + 亮度(00~FF) + 数据 + BCC + FF。 + * BCC = 帧头/长度/命令/亮度/数据段五字段逐字节异或(不含尾部 FF)。 + * 全屏显示 80 数据段可变长(≤249B,随长度字段);行显示 81~88 数据段可变长(=总长-6,≤24B); + * 清屏/亮度/通行灯/黄闪/查询类帧长 7。行显示 8 行与 80 全屏显示。 + */ + +#include "app_sc_ol_proto_parse.h" + +/** + * @brief 治超帧 BCC 计算:帧头(含)到数据段(含)逐字节异或(不含尾部 FF)。 + * @param raw 原始帧。 + * @param raw_len 帧长度。 + * @return BCC 期望值(位于 raw[raw_len-2])。 + */ +static uint8_t _sc_ol_bcc_calc(const uint8_t *raw, uint16_t raw_len) +{ + uint8_t bcc = 0; + for (uint16_t i = 0; i + 2U < raw_len; i++) { /* [0, len-3],不含 BCC(len-2) 与尾 FF(len-1) */ + bcc ^= raw[i]; + } + return bcc; +} + +/** + * @brief 将治超协议命令字节转换为内部命令枚举。 + * @note 80=全屏显示,81~88=第 1~8 行。 + * @param c 命令字节。 + * @return 对应的协议命令枚举;非法返回 SC_OL_PCMD_INVALID。 + */ +static sc_ol_pcmd_t _sc_ol_char_to_cmd(uint8_t c) +{ + switch (c) { + case 0x80: return SC_OL_PCMD_FULL_SCREEN; + case 0x81: return SC_OL_PCMD_LINE_1; + case 0x82: return SC_OL_PCMD_LINE_2; + case 0x83: return SC_OL_PCMD_LINE_3; + case 0x84: return SC_OL_PCMD_LINE_4; + case 0x85: return SC_OL_PCMD_LINE_5; + case 0x86: return SC_OL_PCMD_LINE_6; + case 0x87: return SC_OL_PCMD_LINE_7; + case 0x88: return SC_OL_PCMD_LINE_8; + case 0x94: return SC_OL_PCMD_CLEAR; + case 0x96: return SC_OL_PCMD_BRIGHTNESS; + case 0x99: return SC_OL_PCMD_LANE_LIGHT; + case 0x98: return SC_OL_PCMD_YELLOW_FLASH; + case 0xA0: return SC_OL_PCMD_QUERY_CONTENT; + case 0xB6: return SC_OL_PCMD_QUERY_BRIGHT; + case 0xB9: return SC_OL_PCMD_QUERY_LANE; + case 0xB8: return SC_OL_PCMD_QUERY_FLASH; + default: return SC_OL_PCMD_INVALID; + } +} + +/** + * @brief 解析治超协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体;状态字段 sta 标识解析结果。 + */ +sc_ol_parsed_cmd_t sc_ol_parse_frame(const uint8_t *raw, uint16_t raw_len) +{ + sc_ol_parsed_cmd_t cmd = {0}; + if (!raw || raw_len < SC_OL_FRAME_LEN_MIN || raw_len > SC_OL_FRAME_LEN_MAX || + raw[0] != 0xFF || raw[raw_len - 1U] != 0xFF) { + cmd.sta = SC_OL_PARSE_ERR_FRAME; + return cmd; + } + + /* 长度字段必须等于帧总长 */ + if (raw[1] != raw_len) { + cmd.sta = SC_OL_PARSE_ERR_FRAME; + return cmd; + } + + /* BCC 校验:帧头/长度/命令/亮度/数据段五字段异或 */ + if (_sc_ol_bcc_calc(raw, raw_len) != raw[raw_len - 2U]) { + cmd.sta = SC_OL_PARSE_ERR_BCC; + return cmd; + } + + cmd.cmd = _sc_ol_char_to_cmd(raw[2]); + if (cmd.cmd == SC_OL_PCMD_INVALID) { + cmd.sta = SC_OL_PARSE_ERR_CMD; + return cmd; + } + + cmd.data = &raw[4]; + cmd.data_len = (uint16_t)(raw_len - 6U); /* 去掉 FF+len+cmd+bright+BCC+FF */ + + switch (cmd.cmd) { + case SC_OL_PCMD_FULL_SCREEN: + /* 全屏显示:数据段可变长(≤249B = 长度字段上限 255 - 6 帧开销), + * 整屏自动换行渲染 */ + cmd.p.full_screen.text = &raw[4]; + cmd.p.full_screen.text_len = (uint16_t)(raw_len - 6U); + cmd.sta = SC_OL_PARSE_OK; + break; + + case SC_OL_PCMD_LINE_1: + case SC_OL_PCMD_LINE_2: + case SC_OL_PCMD_LINE_3: + case SC_OL_PCMD_LINE_4: + case SC_OL_PCMD_LINE_5: + case SC_OL_PCMD_LINE_6: + case SC_OL_PCMD_LINE_7: + case SC_OL_PCMD_LINE_8: + /* 变长数据段(数据长度 = 总长 - 6,无固定 16B; + * 渲染层 FONT16 上限 24B 截断,不足不补空格) */ + cmd.p.line.row = (uint8_t)(cmd.cmd - SC_OL_PCMD_LINE_1); + cmd.p.line.text = &raw[4]; + cmd.p.line.text_len = (uint16_t)(raw_len - 6U); + cmd.sta = (cmd.p.line.text_len <= SC_OL_LINE_TEXT_MAX) + ? SC_OL_PARSE_OK : SC_OL_PARSE_ERR_PARAM; + break; + + case SC_OL_PCMD_CLEAR: + cmd.sta = (raw_len == SC_OL_FRAME_LEN_MIN) ? SC_OL_PARSE_OK : SC_OL_PARSE_ERR_PARAM; + break; + + case SC_OL_PCMD_BRIGHTNESS: + case SC_OL_PCMD_LANE_LIGHT: + case SC_OL_PCMD_YELLOW_FLASH: + if (raw_len != SC_OL_FRAME_LEN_MIN) { + cmd.sta = SC_OL_PARSE_ERR_PARAM; + break; + } + cmd.p.byte_val.val = raw[4]; + if ((cmd.cmd == SC_OL_PCMD_LANE_LIGHT || cmd.cmd == SC_OL_PCMD_YELLOW_FLASH) && + cmd.p.byte_val.val > 1U) { + cmd.sta = SC_OL_PARSE_ERR_PARAM; /* 通行灯/黄闪仅 00/01 */ + } else { + cmd.sta = SC_OL_PARSE_OK; + } + break; + + case SC_OL_PCMD_QUERY_CONTENT: + case SC_OL_PCMD_QUERY_BRIGHT: + case SC_OL_PCMD_QUERY_LANE: + case SC_OL_PCMD_QUERY_FLASH: + cmd.sta = (raw_len == SC_OL_FRAME_LEN_MIN) ? SC_OL_PARSE_OK : SC_OL_PARSE_ERR_PARAM; + break; + + default: + cmd.sta = SC_OL_PARSE_ERR_CMD; + break; + } + + return cmd; +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_YunNan/app_yn_proto.c b/Application/Src/ProtocolParser_YunNan/app_yn_proto.c new file mode 100644 index 0000000..e9be36c --- /dev/null +++ b/Application/Src/ProtocolParser_YunNan/app_yn_proto.c @@ -0,0 +1,162 @@ +/** + * @file app_yn_proto.c + * @brief 云南常规费显协议模块 — 注册 / 探测 / 处理任务 + * + * 帧格式:'{' + 命令字 + 二进制长度 + 参数 + '}',与青海/山东/贵州协议同构。 + * 绑定 RS485 + RS232 双通道(与青海/山东/贵州同模式:acquire → register → bind → set_frame_queue)。 + * 帧头冲突:命令字 '1'~'9','A','B' 全部落入青海 probe 命令集, + * 全协议构建下青海 probe 先注册(源码收录序 qh 在前)先认领云南帧; + * 量产由 EIDE 目录排除纪律保证云南与其余 '{' 帧族互斥(doc/05-01 §6 兼容矩阵)。 + * 0x01/0x02 二进制命令字青海/山东/四川MTC probe 首命令字快拒,无冲突。 + */ + +#include "app_yn_proto.h" + +#include "FreeRTOS.h" +#include "initcall.h" +#include "app_dispatch.h" + +#include "app_yn_proto_parse.h" +#include "app_yn_proto_cmd.h" + +/* '{' 帧族互斥守卫:同一构建只允许编入一个 '{' 帧族协议(青海/山东/贵州/四川MTC/云南)。 + * 多个同时编入 → 链接期 multiple definition 强制报错; + * Makefile 全协议开发构建定义 STD_ALL_PROTO 跳过守卫(共存由 probe 注册序与文档纪律约束)。 */ +#ifndef STD_ALL_PROTO +__attribute__((used)) char g_brace_proto_guard; +#endif + +/* 地区协议通道 RB:与青海/RLS/四川/山东/贵州等同槽 weak 合并 */ +RB_PROVIDE_WEAK(rb_provide_rs485, RB_SIZE_RS485); +RB_PROVIDE_WEAK(rb_provide_rs232, RB_SIZE_RS232); + +static proto_mask_t s_yn_mask; +static proto_mask_t s_yn_mask_rs232; +static osMessageQueueId_t s_yn_queue; + +/* 静态队列:payload 覆盖协议最大帧(len 字段 1B → 数据 ≤255 → 帧 ≤259,与青海同构) */ +#define YN_PAYLOAD_MAX (259U) /* '{' + cmd + len + data[0..255] + '}' */ +#define YN_MSG_SIZE (sizeof(frame_msg_t) + YN_PAYLOAD_MAX) +#define YN_QUEUE_DEPTH (3U) +_Static_assert(YN_PAYLOAD_MAX <= RB_SIZE_RS485, "YN frame must fit RS485 RB"); +_Static_assert(YN_PAYLOAD_MAX <= RB_SIZE_RS232, "YN frame must fit RS232 RB"); +static StaticQueue_t s_yn_queue_cb; +static uint8_t s_yn_queue_buf[YN_QUEUE_DEPTH * YN_MSG_SIZE]; +static const osMessageQueueAttr_t s_yn_queue_attr = { + .name = "yn_queue", + .cb_mem = &s_yn_queue_cb, + .cb_size = sizeof(s_yn_queue_cb), + .mq_mem = s_yn_queue_buf, + .mq_size = sizeof(s_yn_queue_buf), +}; + +static proto_probe_sta_t yn_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux); + +/** + * @brief 云南协议模块初始化入口。 + * @note 通过 initcall 自动注册到系统启动流程: + * 申请 RS485 / RS232 RB → 注册探测器 → 绑定双通道 → 创建队列与任务。 + * + * @warning CH_ID_RS232_1 (PL_UART6) 为语音板 TTS 专用,任何地区协议禁止绑定。 + */ +void yn_proto_init(void) +{ + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RS485, RB_SIZE_RS485); + if (rb == nullptr) + return; + + s_yn_mask = app_proto_register(yn_probe_frame, rb); + if (s_yn_mask == 0) + return; + + app_proto_bind_channel(s_yn_mask, CH_ID_RS485); + + ring_buffer_t *rb_rs232 = app_proto_acquire_buf(RB_SLOT_RS232, RB_SIZE_RS232); + if (rb_rs232 != nullptr) { + s_yn_mask_rs232 = app_proto_register(yn_probe_frame, rb_rs232); + if (s_yn_mask_rs232 != 0) + app_proto_bind_channel(s_yn_mask_rs232, CH_ID_RS232); + } + + s_yn_queue = osMessageQueueNew(YN_QUEUE_DEPTH, YN_MSG_SIZE, &s_yn_queue_attr); + app_proto_set_frame_queue(s_yn_mask, s_yn_queue); + if (s_yn_mask_rs232 != 0) + app_proto_set_frame_queue(s_yn_mask_rs232, s_yn_queue); + + static const osThreadAttr_t s_yn_task_attr = { + .name = "yn_handle_task", + .stack_size = 256 * 4, /* 帧缓冲为 static,不放大栈 */ + .priority = osPriorityNormal, + }; + osThreadNew(yn_proto_handle_task, NULL, &s_yn_task_attr); +} +sw_app_initcall(yn_proto_init); + +/** + * @brief 云南协议帧探测(PURE 函数契约:只 rb_peek,无副作用) + * + * 首字节快速拒绝:首字节非 '{' (0x7B) → 立即 FAKE。 + * 帧结构(长度字段为二进制字节值): + * '{' + 命令字 + len + data[0..len-1] + '}',帧总长 = len + 4。 + * 命令字 '1'~'9','A','B' 与 0x01/0x02 二进制命令字。 + * 定界模式:长度字段定界 + 尾字节 '}' 校验(协议无 BCC)。 + */ +static proto_probe_sta_t yn_probe_frame(const channel_t *ch, const ring_buffer_t *rb, + uint32_t *total_len, uint8_t *aux) +{ + (void)ch; + (void)aux; + uint32_t avail = rb_avail(rb, NULL); + + /* 首字节快速拒绝:无数据 → FAKE;首字节非 '{' → FAKE */ + if (avail == 0) + return PROTO_PROBE_FAKE; + uint8_t first_byte; + rb_peek(rb, 0, &first_byte, 1, NULL); + if (first_byte != '{') + return PROTO_PROBE_FAKE; + + /* 需要至少 3 字节:'{' + 命令字 + 长度字段 */ + if (avail < 3U) + return PROTO_PROBE_WAIT; + + uint8_t head[3]; + rb_peek(rb, 0, head, sizeof(head), NULL); + bool cmd_ok = (head[1] >= '1' && head[1] <= '9') || head[1] == 'A' || head[1] == 'B' || + head[1] == 0x01 || head[1] == 0x02; + if (!cmd_ok) + return PROTO_PROBE_FAKE; + + /* 长度字段是二进制字节值,不是 ASCII 数字。 */ + uint16_t data_len = head[2]; + uint32_t frame_len = (uint32_t)data_len + 4U; + if (avail < frame_len) + return PROTO_PROBE_WAIT; + + uint8_t tail; + rb_peek(rb, frame_len - 1U, &tail, 1, NULL); + if (tail != '}') + return PROTO_PROBE_FAKE; + + *total_len = frame_len; /* 仅 READY 路径写输出参数 */ + return PROTO_PROBE_READY; +} + +/** + * @brief 云南协议帧处理任务。 + * @param argument 任务参数,当前未使用。 + */ +void yn_proto_handle_task(void *argument) +{ + (void)argument; + static uint8_t msg_buf[YN_MSG_SIZE]; + frame_msg_t *msg = (frame_msg_t *)msg_buf; + for (;;) { + if (osOK != osMessageQueueGet(s_yn_queue, msg, NULL, osWaitForever)) { + continue; + } + yn_parsed_cmd_t cmd = yn_parse_frame(msg->data, msg->data_len); + yn_execute_cmd(msg->ch, &cmd); + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_YunNan/app_yn_proto_cmd.c b/Application/Src/ProtocolParser_YunNan/app_yn_proto_cmd.c new file mode 100644 index 0000000..de6200b --- /dev/null +++ b/Application/Src/ProtocolParser_YunNan/app_yn_proto_cmd.c @@ -0,0 +1,516 @@ +/** + * @file app_yn_proto_cmd.c + * @brief 云南常规费显协议命令执行 + * + * 显示内容映射到 app_render(协议文本 GBK 直通渲染,24 点阵 FONT_24 行高 24px—— + * 协议原文 24 点阵、一行 12 ASCII / 6 汉字,本设备 24 点阵一行可容纳 + * 16 ASCII / 8 汉字,超宽由渲染引擎截断;行号 '1'~'5' 全按协议接受, + * 行 5 在 96px 屏高下「执行但不落屏」,见用户决定 2); + * 灯控映射到 dev_io_lane_light / dev_io_flash_light(红优先,与贵州裁决一致); + * 亮度 0x00=恢复光敏自动调光、'1'~'8'=挂起光敏 + 硬件档恒等映射 + * (8 最亮,用户决定 5); + * 自检 '2' 复用出厂测试「老化循环显示」序列 + 每 5s 播报「系统正在自检」, + * 可被下一帧命令打断(用户决定 3); + * 0x01 全屏点亮支持 01红/02绿/03黄/04蓝/05紫/06青/07白(用户决定 10); + * 音量 '1'~'5' 映射语音板音量 {1,3,5,7,9}(沿贵州实测映射,同一语音板); + * 语音经 dev_rs232_voice 旁路 USART6。 + * 上行应答:'1' 主机查询回「正常」帧、0x02 版本号回裸 ASCII PROGRAM_CODE + * (用户决定 7),其余命令单向不回。 + */ + +#include "app_yn_proto_cmd.h" +#include "app_yn_proto_parse.h" +#include "app_dispatch.h" + +#include +#include +#include + +#include "app_yn_proto_voice.h" +#include "app_boot.h" +#include "app_light_sensor.h" +#include "app_render.h" +#include "dev_display.h" +#include "dev_io_ctrl.h" +#include "dev_rs232_voice.h" +#include "text_cvt.h" + +/** @brief 单行行高(协议 24 点阵) */ +#define YN_ROW_PX (FONT_24) + +/* 协议颜色索引 0/1/2 → 显示颜色(红/绿/黄) */ +static const display_color_t s_yn_color_map[] = { + [0] = COLOR_RED, + [1] = COLOR_GREEN, + [2] = COLOR_YELLOW, +}; + +/* 协议音量 1~5 → 语音板音量值 {1,3,5,7,9}(沿贵州实测映射:语音板音量非线性档位, + * STD dev_rs232_voice_volume 直通发送音量值) */ +static const uint8_t s_yn_volume_map[] = {1, 3, 5, 7, 9}; + +/** @brief 自检任务运行标志(防重复触发,任务内自清) */ +static volatile bool s_yn_selftest_running = false; + +/** @brief 自检中止标志:下一帧命令到达时置位,自检任务检测后退出(用户决定 3) */ +static volatile bool s_yn_selftest_abort = false; + +/* ---- 老化循环显示序列复用(用户决定 3,2026-08-24 修订)---- + * 复用 Application/Src/app_factory_test.c 263-299 行「老化循环显示」逻辑的 + * 显示序列与实现方式:整屏单字居中全屏显示(清黑 + 渲染 + commit), + * 字号 {FONT_16, FONT_24, FONT_32} × 字体 {ST, FS, KT, HT} 循环推进, + * 文本逐字显示「重庆创迪科技发展有限公司设备老化测试」,循环直至被打断。 + * 与出厂测试的差异:不切换车道灯(避免污染 'A' 外设命令的灯状态)、 + * 不做按键等待(固定步进延时 + 中止标志分片检查)、循环而非单轮; + * 出厂测试自身的任务/状态(monitor、中止标志、老化亮度)完全不动。 */ +#define YN_SELFTEST_TEXT "重庆创迪科技发展有限公司设备老化测试" + +static const font_size_t s_yn_selftest_sizes[] = { + FONT_16, + FONT_24, + FONT_32, +}; +static const font_type_t s_yn_selftest_types[] = { + FONT_ST, + FONT_FS, + FONT_KT, + FONT_HT, +}; +#define YN_SELFTEST_SIZE_COUNT (sizeof(s_yn_selftest_sizes) / sizeof(s_yn_selftest_sizes[0])) +#define YN_SELFTEST_TYPE_COUNT (sizeof(s_yn_selftest_types) / sizeof(s_yn_selftest_types[0])) + +/** @brief 自检单字步进时长(老化 3s/字的精简节奏,协议未指定) */ +#define YN_SELFTEST_CHAR_MS (500U) +/** @brief 语音播报周期:每 5s 播报一次「系统正在自检」(用户决定 3) */ +#define YN_SELFTEST_VOICE_MS (5000U) +/** @brief 中止检查分片(延时细分粒度,保证打断响应 ≤100ms) */ +#define YN_SELFTEST_ABORT_SLICE_MS (100U) + +/** + * @brief 自检单字全屏显示(复现 app_factory_test.c _aging_fill_screen 的实现方式)。 + * @param d 显示设备实例。 + * @param size 字号(FONT_16/FONT_24/FONT_32)。 + * @param type 字体(FONT_ST/FONT_FS/FONT_KT/FONT_HT)。 + * @param ch_utf8 单字 UTF-8 字面量。 + * @param ch_len 单字字节数(ASCII 1 / 汉字 3)。 + */ +static void _yn_selftest_fill_char(dev_display_t *d, font_size_t size, font_type_t type, + const char *ch_utf8, uint8_t ch_len) +{ + uint8_t cols = d->screen_cols / size; + uint8_t rows = d->screen_rows / size; + if (cols == 0) cols = 1; + if (rows == 0) rows = 1; + + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + d->dirty = false; + + static char buf[256]; + uint16_t pos = 0; + uint16_t count = cols * rows; + while (count--) { + memcpy(buf + pos, ch_utf8, ch_len); + pos += ch_len; + } + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 0, + .w = d->screen_rows, + .h = d->screen_cols, + .color = COLOR_WHITE, + .text = buf, + .len = pos, + .font_size = size, + .font_type = type, + .text_enc = FONT_ENC_UTF8, + .style = &(render_style_t){ + .h_align = ALIGN_CENTER, + .v_align = ALIGN_CENTER, + .word_wrap = true, + }, + }); + dev_display_commit_frame(d); +} + +/** + * @brief 播报「系统正在自检」(UTF-8 → GBK → 语音板 TTS,5s 周期调用)。 + */ +static void _yn_selftest_voice(void) +{ + static const char voice_text[] = "系统正在自检"; + uint8_t gbk[32]; + uint32_t gbk_len = sizeof(gbk); + UTF8ToGBK(voice_text, (uint32_t)(sizeof(voice_text) - 1U), (char *)gbk, &gbk_len); + dev_rs232_voice_play(gbk, (uint16_t)gbk_len); +} + +/** + * @brief 自检任务:老化循环显示序列 + 每 5s 播报「系统正在自检」(用户决定 3)。 + * @param argument 任务参数,当前未使用。 + * @note 循环显示直至被下一帧命令打断(yn_execute_cmd 置 s_yn_selftest_abort); + * 被打断退出时**不清屏**——屏幕交给打断它的新命令(清屏会抹掉其渲染)。 + * 防重入:s_yn_selftest_running 由 _yn_exec_self_check 置位、任务内自清。 + */ +static void _yn_selftest_task(void *argument) +{ + (void)argument; + + dev_display_t *d = dev_display_get(); + + uint32_t voice_elapsed = 0; /* 距上次语音播报的累计时长(ms) */ + for (uint8_t type_idx = 0; !s_yn_selftest_abort; + type_idx = (type_idx + 1) % YN_SELFTEST_TYPE_COUNT) { + for (uint8_t size_idx = 0; size_idx < YN_SELFTEST_SIZE_COUNT; size_idx++) { + font_size_t fsize = s_yn_selftest_sizes[size_idx]; + if (d != nullptr && (fsize > d->screen_rows || fsize > d->screen_cols)) + continue; + + const char *ch_ptr = YN_SELFTEST_TEXT; + while (*ch_ptr) { + uint8_t ch_len = ((uint8_t)*ch_ptr >= 0xE0) ? 3 : 1; + char single_ch[4] = {ch_ptr[0], ch_len > 1 ? ch_ptr[1] : 0, + ch_len > 2 ? ch_ptr[2] : 0, 0}; + + if (d != nullptr) + _yn_selftest_fill_char(d, fsize, s_yn_selftest_types[type_idx], + single_ch, ch_len); + + /* 每 5s 播报一次「系统正在自检」:与循环显示并行 + * (dev_rs232_voice_play 非阻塞发送,不打断显示步进) */ + if (voice_elapsed >= YN_SELFTEST_VOICE_MS) { + voice_elapsed = 0; + _yn_selftest_voice(); + } + + /* 分片延时:控制显示步进节奏 + 及时响应下一帧命令的中止 */ + for (uint32_t ms = 0; ms < YN_SELFTEST_CHAR_MS && !s_yn_selftest_abort; + ms += YN_SELFTEST_ABORT_SLICE_MS) { + osDelay(YN_SELFTEST_ABORT_SLICE_MS); + } + voice_elapsed += YN_SELFTEST_CHAR_MS; + if (s_yn_selftest_abort) + break; + ch_ptr += ch_len; + } + if (s_yn_selftest_abort) + break; + } + if (s_yn_selftest_abort) + break; + } + + s_yn_selftest_running = false; + s_yn_selftest_abort = false; + osThreadExit(); +} + +/** + * @brief 执行自检('2' 命令):启动老化循环显示任务。 + * @note 一次性任务执行(协议「系统进入自动检测状态」);任务进行中重复 + * 触发被忽略(防重入);任务可被下一帧命令打断(用户决定 3)。 + */ +static void _yn_exec_self_check(void) +{ + if (s_yn_selftest_running) + return; + s_yn_selftest_running = true; /* 先置标志防重入;任务结束时自清 */ + s_yn_selftest_abort = false; + + static const osThreadAttr_t s_yn_selftest_attr = { + .name = "yn_selftest_task", + .stack_size = 256 * 4, + .priority = osPriorityNormal, + }; + osThreadNew(_yn_selftest_task, NULL, &s_yn_selftest_attr); +} + +/** + * @brief 将协议颜色索引转换为显示颜色。 + * @param idx 协议颜色索引(0 红 / 1 绿 / 2 黄)。 + * @return 对应的显示颜色;越界回退绿色。 + */ +static display_color_t yn_map_color(uint8_t idx) +{ + return (idx < 3) ? s_yn_color_map[idx] : COLOR_GREEN; +} + +/** + * @brief 清空整屏(置黑并提交)。 + */ +static void _yn_clear_screen(void) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + dev_display_commit_frame(d); +} + +/** + * @brief 清空第 row 行区域(行高 YN_ROW_PX=24)。 + * @param d 显示设备实例。 + * @param row 行号(0 起)。 + * @note 用户决定 2(2026-08-24 修订):行号 '1'~'5' 全按协议执行, + * 不丢弃、不拒绝。行高 24px 下 96px 屏高只容纳 4 行—— + * 行 5(row 4,y=96)越界时 dev_display_fill 起点越界早退 + * (安全无副作用),即「执行但不落屏」。 + */ +static void _yn_clear_row(dev_display_t *d, uint8_t row) +{ + dev_display_fill(d, 0, (uint16_t)row * YN_ROW_PX, d->screen_rows, YN_ROW_PX, COLOR_BLACK); +} + +/** + * @brief 执行主机查询('1' 命令):回固定「状态正常」应答帧 7B 31 01 00 7D。 + * @param ch 源通道(应答回源通道)。 + * @note 协议定义正常/异常两种应答,本实现仅回「正常」一种(设备恒正常, + * 异常应答无触发条件)。 + */ +static void _yn_exec_host_query(channel_t *ch) +{ + static const uint8_t rsp_ok[] = {'{', '1', 0x01, 0x00, '}'}; + channel_send(ch, (uint8_t *)rsp_ok, sizeof(rsp_ok)); +} + +/** + * @brief 执行单行显示('3' 命令:第 row+1 行按颜色渲染文本)。 + * @param p 单行显示参数。 + * @note 先整行清黑再渲染收到内容(文本短于行宽时旧内容残留必须清除); + * FONT_24 渲染、GBK 直通、超出行宽截断。 + * 用户决定 2(2026-08-24 修订):行 5(row 4,y=96)渲染调用照常 + * 发出——渲染层 dev_display_fill / dev_display_draw_bitmap 起点越界 + * 早退,天然不落屏(安全无副作用),命令本身不丢弃、不拒绝。 + */ +static void _yn_exec_one_line(const yn_one_line_t *p) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + _yn_clear_row(d, p->row); + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = (uint16_t)p->row * YN_ROW_PX, + .w = d->screen_rows, + .h = YN_ROW_PX, + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = false, /* 单行:超出行宽截断 */ + }, + .color = yn_map_color(p->color), + .text = (const char *)p->text, + .len = p->text_len, + .font_size = FONT_24, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 执行全屏可编辑显示('4' 命令:按协议 X/Y 坐标渲染,自动换行)。 + * @param p 全屏显示参数。 + * @note 先整屏清黑再渲染;文本中 0x0A 0x0D / 0x0D 0x0A 回车由渲染引擎 + * 处理(0x0A 换行、0x0D 跳过)。X/Y 坐标按协议原文取参数渲染。 + * 坐标越界(x≥屏宽或 y≥屏高)时丢弃渲染(清屏仍执行)。 + */ +static void _yn_exec_full_screen(const yn_full_screen_t *p) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + + if (p->x >= d->screen_rows || p->y >= d->screen_cols) + return; /* 坐标越界:清屏后不渲染 */ + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = p->x, + .y = p->y, + .w = (uint16_t)(d->screen_rows - p->x), + .h = (uint16_t)(d->screen_cols - p->y), + .style = &(render_style_t){ + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = true, + }, + .color = yn_map_color(p->color), + .text = (const char *)p->text, + .len = p->text_len, + .font_size = FONT_24, + .font_type = FONT_ST, + .text_enc = FONT_ENC_GBK, + }); +} + +/** + * @brief 执行全屏点亮(0x01 命令:整屏填充单色)。 + * @param color DATA0 二进制颜色值,与 display_color_t 枚举值恒等: + * 0x01 红 / 0x02 绿 / 0x03 黄(协议原文三色)/ + * 0x04 蓝 / 0x05 紫 / 0x06 青 / 0x07 白 + * (用户决定 10 扩展取值;P5 户外全彩屏支持 8 色, + * 黑色除外——全屏黑等效 '5' 全屏清除)。 + */ +static void _yn_exec_fill_all(uint8_t color) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + if (color < 1 || color > 7) + return; /* 防御:仅接受 COLOR_RED~COLOR_WHITE */ + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, (display_color_t)color); + dev_display_commit_frame(d); +} + +/** + * @brief 执行获取版本号(0x02 命令):回裸 ASCII 固件 PROGRAM_CODE。 + * @param ch 源通道。 + * @note 用户决定 7(2026-08-24 修订):不再回硬编码 "YN_FX_P5_1.0", + * 与山东/贵州同模式回编译期常量 PROGRAM_CODE(app_boot.h)。 + */ +static void _yn_exec_version(channel_t *ch) +{ + channel_send(ch, (uint8_t *)PROGRAM_CODE, sizeof(PROGRAM_CODE) - 1U); +} + +/** + * @brief 执行亮度设定('8' 命令,用户决定 5,2026-08-24 修订)。 + * @param level 0 = 自动亮度(协议参数 NUL 字节 0x00); + * 1~8 = 手动亮度档(协议参数 ASCII '1'~'8',8 最亮)。 + * @note 0x00 → 恢复光敏任务自动调光(osThreadResume,替代此前 + * 「恢复自动调光需复位」的临时方案); + * 1~8 → 挂起光敏任务(防 1s 周期自动调光覆盖手动档)+ + * 硬件档恒等映射(DEV_DISPLAY_BRIGHTNESS_MAX=8)。 + */ +static void _yn_exec_brightness(uint8_t level) +{ + dev_display_t *d = dev_display_get(); + if (!d) + return; + + if (level == 0) { + /* 自动亮度:恢复光敏任务(此前可能被手动档挂起) */ + if (g_light_sensor_task_handle != nullptr) + osThreadResume(g_light_sensor_task_handle); + return; + } + + if (level > 8) + return; + if (g_light_sensor_task_handle != nullptr) + osThreadSuspend(g_light_sensor_task_handle); /* 手动设定 → 关闭自动调光 */ + dev_display_set_brightness(d, level); /* 协议档位与硬件档位恒等映射 */ +} + +/** + * @brief 执行语音音量设定('9' 命令:'1'~'5' 档)。 + * @param level 协议音量档位(1~5)。 + * @note 档位映射沿贵州实测:协议 '1'~'5' → 语音板音量值 1/3/5/7/9。 + */ +static void _yn_exec_volume(uint8_t level) +{ + if (level < 1 || level > 5) + return; + dev_rs232_voice_volume(s_yn_volume_map[level - 1]); +} + +/** + * @brief 执行外设控制('A' 命令:bit0 绿灯 / bit1 红灯 / bit2 黄闪报警)。 + * @param ctrl 控制位图。 + * @note 车道灯为单灯互斥(true=绿/false=红):红绿同置时红灯优先 + * (沿贵州裁决,协议未定义优先级)。 + */ +static void _yn_exec_peripheral(uint8_t ctrl) +{ + bool green = (ctrl & 0x01U) != 0U; + bool red = (ctrl & 0x02U) != 0U; + bool yellow = (ctrl & 0x04U) != 0U; + if (red) { + dev_io_lane_light(false); + } else if (green) { + dev_io_lane_light(true); + } + dev_io_flash_light(yellow); +} + +/** + * @brief 执行收费金额语音播放('B' 命令)。 + * @param amount_fen 收费金额,单位分。 + * @note 0 元不播报;小数播报小数(123.4→「123.4」,末位 0 剔除)。 + */ +static void _yn_exec_fee_voice(uint32_t amount_fen) +{ + yn_voice_fee_amount(amount_fen); +} + +/** + * @brief 执行云南协议命令。 + * @param ch 当前通道。 + * @param cmd 解析后的命令结构体。 + * @note 除 '1' 主机查询回「正常」帧、0x02 版本号回裸 ASCII YN_FX_P5_1.0 外, + * 其余命令单向执行不回;解析失败静默丢弃(非法参数不崩溃)。 + */ +void yn_execute_cmd(channel_t *ch, const yn_parsed_cmd_t *cmd) +{ + if (!cmd || cmd->sta != YN_PARSE_OK) + return; + + /* 自检打断(用户决定 3,2026-08-24 修订):自检进行中收到其他有效命令 → + * 置中止标志,自检任务在下一分片(≤100ms)检测到后退出且不清屏, + * 屏幕交给本命令;'2' 自检本身不打断(防重入由 _yn_exec_self_check 处理)。 */ + if (s_yn_selftest_running && cmd->cmd != YN_PCMD_SELF_CHECK) + s_yn_selftest_abort = true; + + switch (cmd->cmd) { + case YN_PCMD_HOST_QUERY: + _yn_exec_host_query(ch); + break; + case YN_PCMD_SELF_CHECK: + _yn_exec_self_check(); + break; + case YN_PCMD_ONE_LINE: + _yn_exec_one_line(&cmd->p.one_line); + break; + case YN_PCMD_FULL_SCREEN: + _yn_exec_full_screen(&cmd->p.full_screen); + break; + case YN_PCMD_CLEAR: + _yn_clear_screen(); + break; + case YN_PCMD_CLEAR_ROW: { + dev_display_t *d = dev_display_get(); + if (d != nullptr) + _yn_clear_row(d, cmd->p.clear_row.row); + break; + } + case YN_PCMD_CIVIL_VOICE: + yn_voice_civil(cmd->p.civil.idx); + break; + case YN_PCMD_BRIGHTNESS: + _yn_exec_brightness(cmd->p.brightness.level); + break; + case YN_PCMD_VOLUME: + _yn_exec_volume(cmd->p.volume.level); + break; + case YN_PCMD_PERIPHERAL: + _yn_exec_peripheral(cmd->p.peripheral.ctrl); + break; + case YN_PCMD_FEE_VOICE: + _yn_exec_fee_voice(cmd->p.fee.amount_fen); + break; + case YN_PCMD_FILL_ALL: + _yn_exec_fill_all(cmd->p.fill_all.color); + break; + case YN_PCMD_VERSION: + _yn_exec_version(ch); + break; + default: + break; + } +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_YunNan/app_yn_proto_parse.c b/Application/Src/ProtocolParser_YunNan/app_yn_proto_parse.c new file mode 100644 index 0000000..4f1b591 --- /dev/null +++ b/Application/Src/ProtocolParser_YunNan/app_yn_proto_parse.c @@ -0,0 +1,237 @@ +/** + * @file app_yn_proto_parse.c + * @brief 云南常规费显协议帧解析(纯解析:不发包、不渲染、不碰外设) + * + * 帧格式:'{' + 命令字 + 二进制长度 + 参数 + '}'。 + * 协议原文:01 云南常规费显协议-云南LED费显P5(2022.7.5)。 + * 参数边界推导:协议定义 24 点阵、一行 12 ASCII / 6 汉字(=12 字节), + * 本设备 24 点阵一行可容纳 16 ASCII / 8 汉字(=16 字节),'3' 单行文本 + * 上限按 2+16=18 字节(与贵州 3~18 实测边界一致);非法参数置错不崩溃。 + */ + +#include "app_yn_proto_parse.h" + +/** + * @brief 将云南协议命令码字节转换为内部命令枚举。 + * @param c 协议中的命令码字节(ASCII '1'~'9','A','B' 或二进制 0x01/0x02)。 + * @return 对应的协议命令枚举;非法字节返回 YN_PCMD_INVALID。 + */ +static yn_pcmd_t yn_char_to_cmd(uint8_t c) +{ + switch (c) { + case '1': return YN_PCMD_HOST_QUERY; + case '2': return YN_PCMD_SELF_CHECK; + case '3': return YN_PCMD_ONE_LINE; + case '4': return YN_PCMD_FULL_SCREEN; + case '5': return YN_PCMD_CLEAR; + case '6': return YN_PCMD_CLEAR_ROW; + case '7': return YN_PCMD_CIVIL_VOICE; + case '8': return YN_PCMD_BRIGHTNESS; + case '9': return YN_PCMD_VOLUME; + case 'A': return YN_PCMD_PERIPHERAL; + case 'B': return YN_PCMD_FEE_VOICE; + case 0x01: return YN_PCMD_FILL_ALL; + case 0x02: return YN_PCMD_VERSION; + default: return YN_PCMD_INVALID; + } +} + +uint32_t yn_amount_to_fen(const uint8_t *str, uint16_t len) +{ + uint32_t fen = 0U; + uint16_t i = 0U; + if (str == NULL || len == 0U) { + return 0U; + } + + /* 整数部分:前缀数字累加,遇非数字停止(strtod 前缀语义) */ + while (i < len && str[i] >= '0' && str[i] <= '9') { + fen = fen * 10U + (uint32_t)(str[i] - '0'); + i++; + } + fen *= 100U; + + /* 小数部分:最多取两位(%.2f 语义:一位补零,第三位起截断) */ + if (i < len && str[i] == '.') { + i++; + uint32_t dec = 0U; + uint16_t digits = 0U; + while (i < len && digits < 2U && str[i] >= '0' && str[i] <= '9') { + dec = dec * 10U + (uint32_t)(str[i] - '0'); + digits++; + i++; + } + if (digits == 1U) { + dec *= 10U; + } + fen += dec; + } + return fen; +} + +/** + * @brief 解析云南协议原始帧。 + * @param raw 原始帧数据。 + * @param raw_len 原始帧长度。 + * @return 解析后的命令结构体;状态字段 sta 标识解析结果。 + * @note 长度字段为二进制字节值,表示参数区长度。 + * '4' 命令文本中的回车对 0x0A 0x0D / 0x0D 0x0A 原样透传:0x0A 由渲染 + * 引擎换行,0x0D 非 GBK 前导字节由渲染引擎跳过,无需归一化。 + */ +yn_parsed_cmd_t yn_parse_frame(const uint8_t *raw, uint16_t raw_len) +{ + yn_parsed_cmd_t cmd = {0}; + if (!raw || raw_len < 4 || raw[0] != '{' || raw[raw_len - 1] != '}') { + cmd.sta = YN_PARSE_ERR_FRAME; + return cmd; + } + + cmd.cmd = yn_char_to_cmd(raw[1]); + if (cmd.cmd == YN_PCMD_INVALID) { + cmd.sta = YN_PARSE_ERR_CMD; + return cmd; + } + + /* 长度字段是二进制字节值,不是 ASCII 数字。 */ + uint16_t declared_len = raw[2]; + if ((uint16_t)(raw_len - 4) < declared_len) { + cmd.sta = YN_PARSE_ERR_FRAME; + return cmd; + } + + cmd.data = &raw[3]; + cmd.data_len = declared_len; + cmd.sta = YN_PARSE_OK; + + switch (cmd.cmd) { + case YN_PCMD_HOST_QUERY: /* '1' 主机查询:无参数 */ + if (declared_len != 0) + cmd.sta = YN_PARSE_ERR_PARAM; + break; + case YN_PCMD_SELF_CHECK: /* '2' 自检:无参数 */ + if (declared_len != 0) + cmd.sta = YN_PARSE_ERR_PARAM; + break; + case YN_PCMD_ONE_LINE: /* '3' 单行:颜色 + 行号 + 文本 */ + /* 协议一行 12 ASCII / 6 汉字(12 字节);本设备 24 点阵一行 + * 16 ASCII / 8 汉字(16 字节)→ 文本上限 16 字节,帧参数 + * 长度边界 3~18(与贵州 3~18 边界一致)。 + * 行号范围以协议为准 '1'~'5' 全接受(→0~4,用户决定 2): + * 行 5 在 96px 屏高(4 行 FONT_24)下物理不显示,但解析 + * 不拒绝——渲染调用照常发出,越界由渲染层自然丢弃。 */ + if (declared_len < 3 || declared_len > 18) { + cmd.sta = YN_PARSE_ERR_PARAM; + break; + } + cmd.p.one_line.color = (uint8_t)(cmd.data[0] - '0'); + cmd.p.one_line.row = (uint8_t)(cmd.data[1] - '1'); + cmd.p.one_line.text = &cmd.data[2]; + cmd.p.one_line.text_len = declared_len - 2; + if (cmd.p.one_line.color > 2 || cmd.p.one_line.row > 4) + cmd.sta = YN_PARSE_ERR_PARAM; + break; + case YN_PCMD_FULL_SCREEN: /* '4' 全屏可编辑:颜色 + X + Y + 文本 */ + /* 参数 3 字节固定头 + 文本;上限 86 对齐贵州(len>71 截断 64 为 + * 贵州设备实测行为,云南无实测,不截断——超屏由渲染引擎按边界裁剪)。 */ + if (declared_len < 4 || declared_len > 86) { + cmd.sta = YN_PARSE_ERR_PARAM; + break; + } + cmd.p.full_screen.color = (uint8_t)(cmd.data[0] - '0'); + cmd.p.full_screen.x = cmd.data[1]; + cmd.p.full_screen.y = cmd.data[2]; + cmd.p.full_screen.text = &cmd.data[3]; + cmd.p.full_screen.text_len = declared_len - 3; + if (cmd.p.full_screen.color > 2) + cmd.sta = YN_PARSE_ERR_PARAM; + break; + case YN_PCMD_CLEAR: /* '5' 全屏清除:无参数 */ + if (declared_len != 0) { + cmd.sta = YN_PARSE_ERR_PARAM; + break; + } + break; + case YN_PCMD_CLEAR_ROW: /* '6' 单行清除:行号 '1'~'5'(行 5 同 '3': + * 协议范围全接受,越界由渲染层自然处理) */ + if (declared_len != 1) { + cmd.sta = YN_PARSE_ERR_PARAM; + break; + } + cmd.p.clear_row.row = (uint8_t)(cmd.data[0] - '1'); + if (cmd.p.clear_row.row > 4) + cmd.sta = YN_PARSE_ERR_PARAM; + break; + case YN_PCMD_CIVIL_VOICE: /* '7' 礼貌用语语音:索引 '0'~'3'(其他预留) */ + if (declared_len != 1) { + cmd.sta = YN_PARSE_ERR_PARAM; + break; + } + cmd.p.civil.idx = (uint8_t)(cmd.data[0] - '0'); + if (cmd.p.civil.idx > 3) + cmd.sta = YN_PARSE_ERR_PARAM; + break; + case YN_PCMD_BRIGHTNESS: /* '8' 亮度(用户决定 5,2026-08-24 修订): + * 参数 1B——0x00(NUL)= 自动亮度, + * ASCII '1'~'8'(0x31~0x38)= 手动档 1~8(8 最亮)。 + * 帧长度定界不受 0x00 影响:probe 按二进制 len + * 字段定界 + 尾字节 '}' 校验,本处按 declared_len + * 取参数,0x00 不会被当作字符串终结符。 */ + if (declared_len != 1) { + cmd.sta = YN_PARSE_ERR_PARAM; + break; + } + if (cmd.data[0] == 0x00) { + cmd.p.brightness.level = 0; /* 0 = 自动亮度 */ + } else if (cmd.data[0] >= '1' && cmd.data[0] <= '8') { + cmd.p.brightness.level = (uint8_t)(cmd.data[0] - '0'); /* 1~8 */ + } else { + cmd.sta = YN_PARSE_ERR_PARAM; + } + break; + case YN_PCMD_VOLUME: /* '9' 音量:'1'~'5' */ + if (declared_len != 1) { + cmd.sta = YN_PARSE_ERR_PARAM; + break; + } + cmd.p.volume.level = (uint8_t)(cmd.data[0] - '0'); + if (cmd.p.volume.level < 1 || cmd.p.volume.level > 5) + cmd.sta = YN_PARSE_ERR_PARAM; + break; + case YN_PCMD_PERIPHERAL: /* 'A' 外设:bit0 绿灯 / bit1 红灯 / bit2 黄闪报警 */ + if (declared_len != 1) { + cmd.sta = YN_PARSE_ERR_PARAM; + break; + } + cmd.p.peripheral.ctrl = cmd.data[0]; + break; + case YN_PCMD_FEE_VOICE: /* 'B' 收费金额语音:金额 ASCII 串(整数/小数) */ + if (declared_len < 1) { + cmd.sta = YN_PARSE_ERR_PARAM; + break; + } + cmd.p.fee.amount_fen = yn_amount_to_fen(cmd.data, declared_len); + break; + case YN_PCMD_FILL_ALL: /* 0x01 全屏点亮:参数 1B 二进制(用户决定 10 扩展): + * 01红/02绿/03黄(协议原文三色,保留兼容)/ + * 04蓝/05紫/06青/07白(P5 户外全彩屏支持 8 色, + * 黑色除外——全屏黑等效 '5' 全屏清除)。 + * DATA0 与 display_color_t 枚举值恒等, + * 执行层直接按枚举值渲染。 */ + if (declared_len != 1 || cmd.data[0] < 1 || cmd.data[0] > 7) { + cmd.sta = YN_PARSE_ERR_PARAM; + break; + } + cmd.p.fill_all.color = cmd.data[0]; /* 0x01~0x07 → COLOR_RED~COLOR_WHITE */ + break; + case YN_PCMD_VERSION: /* 0x02 获取版本号:参数 1B(协议示例 00) */ + if (declared_len != 1) { + cmd.sta = YN_PARSE_ERR_PARAM; + break; + } + break; + default: + break; + } + + return cmd; +} \ No newline at end of file diff --git a/Application/Src/ProtocolParser_YunNan/app_yn_proto_voice.c b/Application/Src/ProtocolParser_YunNan/app_yn_proto_voice.c new file mode 100644 index 0000000..3cc5825 --- /dev/null +++ b/Application/Src/ProtocolParser_YunNan/app_yn_proto_voice.c @@ -0,0 +1,80 @@ +/** + * @file app_yn_proto_voice.c + * @brief 云南常规费显协议语音播报 + * + * 礼貌用语索引 → UTF-8 字面量 → UTF8ToGBK 运行时转 GBK → dev_rs232_voice_play + * (语音板 TTS 旁路 USART6)。收费金额语音按金额(分)构造「您好请交费X元」: + * 0 元不播报;小数保留有效小数位(123.4→「123.4」、123.45→「123.45」、123→「123」)。 + */ + +#include "app_yn_proto_voice.h" + +#include +#include + +#include "dev_rs232_voice.h" +#include "text_cvt.h" + +/* 礼貌用语(UTF-8 字面量,播报前运行时转 GBK 送语音板)。 + * 文本按协议原文:'0' 祝您旅途愉快 / '1' 请出示通行卡 / + * '2' 谢谢合作、祝您一路平安 / '3' 交易不成功,请走人工车道。 */ +static const char *const s_yn_civil_texts[] = { + "祝您旅途愉快", + "请出示通行卡", + "谢谢合作祝您一路平安", + "交易不成功请走人工车道", +}; + +/** + * @brief 播报礼貌用语。 + * @param idx 礼貌用语索引(0~3)。 + */ +void yn_voice_civil(uint8_t idx) +{ + if (idx >= (sizeof(s_yn_civil_texts) / sizeof(s_yn_civil_texts[0]))) { + return; + } + + /* 语音板要求 GBK:UTF-8 源文运行时转换后播报 */ + uint8_t gbk[64]; + uint32_t gbk_len = sizeof(gbk); + UTF8ToGBK(s_yn_civil_texts[idx], (uint32_t)strlen(s_yn_civil_texts[idx]), + (char *)gbk, &gbk_len); + dev_rs232_voice_play(gbk, (uint16_t)gbk_len); +} + +/** + * @brief 播报收费金额提示语。 + * @param amount_fen 收费金额,单位分。 + * @note 协议:0 元不播报;有小数需播报小数。 + * 金额串播报格式:整数 →「您好请交费X元」; + * 1 位有效小数 →「您好请交费X.Y元」;2 位有效小数 →「您好请交费X.YZ元」 + * (末位 0 剔除,如 123.40 →「123.4」)。 + */ +void yn_voice_fee_amount(uint32_t amount_fen) +{ + if (amount_fen == 0U) { + return; /* 0 元不播报 */ + } + + char text[48]; + int len; + if (amount_fen % 100U == 0U) { + len = snprintf(text, sizeof(text), "您好请交费%u元", (unsigned)(amount_fen / 100U)); + } else if (amount_fen % 10U == 0U) { + len = snprintf(text, sizeof(text), "您好请交费%u.%u元", (unsigned)(amount_fen / 100U), + (unsigned)((amount_fen % 100U) / 10U)); + } else { + len = snprintf(text, sizeof(text), "您好请交费%u.%02u元", (unsigned)(amount_fen / 100U), + (unsigned)(amount_fen % 100U)); + } + if (len <= 0 || len >= (int)sizeof(text)) { + return; + } + + /* 语音板要求 GBK:UTF-8 文本运行时转换后播报 */ + uint8_t gbk[64]; + uint32_t gbk_len = sizeof(gbk); + UTF8ToGBK(text, (uint32_t)len, (char *)gbk, &gbk_len); + dev_rs232_voice_play(gbk, (uint16_t)gbk_len); +} \ No newline at end of file diff --git a/Application/Src/RLS/app_rls.c b/Application/Src/RLS/app_rls.c index 7412f35..93f286d 100644 --- a/Application/Src/RLS/app_rls.c +++ b/Application/Src/RLS/app_rls.c @@ -5,12 +5,17 @@ #include "bcc_utils.h" #include "app_rls_cmd.h" +/* RS485 通道 RB:与青海等同槽 weak 合并 */ +RB_PROVIDE_WEAK(rb_provide_rs485, RB_SIZE_RS485); + /* ---- proto_rls_queue 静态分配 ---- */ #define RLS_PAYLOAD_MAX (530U) /* 帧头(6B) + bitmap(512B) + BCC(1B) + 尾(2B) + 余量 */ #define RLS_MSG_SIZE (sizeof(frame_msg_t) + RLS_PAYLOAD_MAX) +#define RLS_QUEUE_DEPTH (2U) +_Static_assert(RLS_PAYLOAD_MAX <= RB_SIZE_RS485, "RLS max frame must fit RS485 RB"); static StaticQueue_t s_rls_queue_cb; -static uint8_t s_rls_queue_buf[2 * RLS_MSG_SIZE]; +static uint8_t s_rls_queue_buf[RLS_QUEUE_DEPTH * RLS_MSG_SIZE]; static const osMessageQueueAttr_t s_rls_queue_attr = { .name = "proto_rls_queue", .cb_mem = &s_rls_queue_cb, @@ -19,7 +24,7 @@ static const osMessageQueueAttr_t s_rls_queue_attr = { .mq_size = sizeof(s_rls_queue_buf), }; -const static rls_cmd_type_t cmd_index_table[] = { +static const rls_cmd_type_t cmd_index_table[] = { RLS_CMD_TEST, RLS_CMD_DISPLAY, RLS_CMD_DISPLAY_SAVE, @@ -31,16 +36,17 @@ osMessageQueueId_t g_rls_msg_queue; osThreadId_t g_rls_task_handle; const osThreadAttr_t rls_task_attr = { .name = "rls_handle_task", - .stack_size = 512 * 4, + .stack_size = 256 * 4, /* 帧缓冲为 static;原 2KB 偏大 */ .priority = (osPriority_t)osPriorityNormal, }; /*--- 帧任务---*/ void rls_handle_task(void *argument) { + (void)argument; static uint8_t _msg_buf[RLS_MSG_SIZE]; frame_msg_t *msg = (frame_msg_t *)_msg_buf; - g_rls_msg_queue = osMessageQueueNew(2, RLS_MSG_SIZE, &s_rls_queue_attr); + g_rls_msg_queue = osMessageQueueNew(RLS_QUEUE_DEPTH, RLS_MSG_SIZE, &s_rls_queue_attr); app_proto_set_frame_queue(s_rls_mask, g_rls_msg_queue); for (;;) { @@ -61,17 +67,37 @@ void rls_handle_task(void *argument) } /* ---- 帧探测 ---- */ -const static uint8_t rls_head[2] = {0xFF, 0xFE}; -const static uint8_t rls_tail[2] = {0x0D, 0x0C}; +static const uint8_t rls_head[2] = {0xFF, 0xFE}; +static const uint8_t rls_tail[2] = {0x0D, 0x0C}; proto_probe_sta_t rls_probe_frame(const channel_t *ch, const ring_buffer_t *buff, uint32_t *total_len, uint8_t *aux) { uint32_t avail = rb_avail(buff, nullptr); + (void)ch; + + /* 首字节快速拒绝:禁止 avail<10 盲 WAIT 阻塞同 RB 链式探测 */ + if (avail == 0) + return PROTO_PROBE_FAKE; + + uint8_t first_byte; + rb_peek(buff, 0, &first_byte, 1, nullptr); + if (first_byte != rls_head[0]) + return PROTO_PROBE_FAKE; + + if (avail < 2) + return PROTO_PROBE_WAIT; + + uint8_t second_byte; + rb_peek(buff, 1, &second_byte, 1, nullptr); + if (second_byte != rls_head[1]) + return PROTO_PROBE_FAKE; + if (avail < sizeof(rls_frame_t) + 4) return PROTO_PROBE_WAIT; - static uint8_t mem_pool[525] = {0}; + static uint8_t mem_pool[RLS_PAYLOAD_MAX] = {0}; + uint32_t peek_len = (avail > sizeof(mem_pool)) ? sizeof(mem_pool) : avail; memset(mem_pool, 0, sizeof(mem_pool)); - rb_peek(buff, 0, mem_pool, avail, nullptr); + rb_peek(buff, 0, mem_pool, peek_len, nullptr); rls_frame_t *frame = (rls_frame_t *)mem_pool; if (memcmp(rls_head, frame->head, sizeof(rls_head))) @@ -79,6 +105,11 @@ proto_probe_sta_t rls_probe_frame(const channel_t *ch, const ring_buffer_t *buff uint16_t data_len = (frame->length[1] & 0xFF) | ((frame->length[0] << 8) & 0xFF00); + /* 长度上下界:至少含头+尾,不超过已窥视/载荷上限 */ + if (data_len < (uint16_t)(sizeof(rls_frame_t) + 4) || data_len > peek_len || + data_len > RLS_PAYLOAD_MAX) + return PROTO_PROBE_FAKE; + if (memcmp(rls_tail, (uint8_t *)frame + data_len - 2, sizeof(rls_tail))) return PROTO_PROBE_FAKE; @@ -88,7 +119,7 @@ proto_probe_sta_t rls_probe_frame(const channel_t *ch, const ring_buffer_t *buff // if (frame_bcc != calc_bcc) // return PROTO_PROBE_FAKE; - (void)*aux; + *aux = frame->cmd[0]; /* 命令高字节,供分发侧辅助区分 */ *total_len = data_len; return PROTO_PROBE_READY; } @@ -96,15 +127,14 @@ proto_probe_sta_t rls_probe_frame(const channel_t *ch, const ring_buffer_t *buff /* ---- 协议自注册 ---- */ [[maybe_unused]] static void rls_module_init(void) { - // 指定协议使用的环形缓冲区 - ring_buffer_t *rb = app_proto_acquire_buf(1, 2048); + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RS485, RB_SIZE_RS485); + if (rb == nullptr) + return; - // 注册协议到多通道多协议解析模块 s_rls_mask = app_proto_register(rls_probe_frame, rb); if (s_rls_mask == 0) return; - // 绑定协议使用到的通道 app_proto_bind_channel(s_rls_mask, CH_ID_RS485); g_rls_task_handle = osThreadNew(rls_handle_task, nullptr, &rls_task_attr); diff --git a/Application/Src/RLS/app_rls_cmd.c b/Application/Src/RLS/app_rls_cmd.c index 6626f60..b7bfdfe 100644 --- a/Application/Src/RLS/app_rls_cmd.c +++ b/Application/Src/RLS/app_rls_cmd.c @@ -14,10 +14,13 @@ const rls_cmd_handler_fn_t g_rls_cmd_table[] = { [[maybe_unused]] static void cmd_test(channel_t *ch, void *data) { + (void)ch; + (void)data; } static void cmd_display(channel_t *ch, void *data) { + (void)ch; rls_dispaly_t *display_ctx = (rls_dispaly_t *)data; // 显示前先清屏 @@ -42,6 +45,7 @@ static void cmd_display(channel_t *ch, void *data) static void cmd_display_save(channel_t *ch, void *data) { + (void)ch; rls_dispaly_t *display_ctx = (rls_dispaly_t *)data; // 显示前先清屏 diff --git a/Application/Src/app_boot.c b/Application/Src/app_boot.c index 60b3c7f..e884fb1 100644 --- a/Application/Src/app_boot.c +++ b/Application/Src/app_boot.c @@ -7,135 +7,168 @@ #include #include +#include +#include "app_board_net_cfg.h" +#include "app_boot.h" +#include "app_default_display.h" +#include "app_key.h" +#include "app_net_boot.h" +#include "app_render.h" +#include "app_rs232.h" +#include "app_rs485.h" +#include "app_tcp_client.h" +#include "app_tcp_server.h" +#include "app_test.h" +#include "app_udp.h" #include "cmsis_os.h" +#include "dev_display.h" +#include "dev_eth.h" #include "initcall.h" +#include "pl_dwt.h" +#include "pl_gpio.h" #include "pl_iwdg.h" #include "pl_rtc.h" -#include "pl_gpio.h" -#include "pl_dwt.h" -#include "dev_eth.h" -#include "app_udp.h" -#include "app_tcp_server.h" -#include "app_tcp_client.h" -#include "app_rs485.h" -#include "app_test.h" -#include "app_key.h" -#include "app_render.h" -#include "pl_uart.h" + +#define SPLASH_DURATION_MS 5000U static void init_task(void *argument); /* ---- HalfSecTask: 500ms 喂狗 / 60s RTC 备份 / LED 翻转 ---- */ -static void half_sec_task(void *argument) -{ - (void)argument; - uint32_t run_time = 0; - bool led_state = false; +static void half_sec_task(void *argument) { + (void)argument; + uint32_t run_time = 0; + bool led_state = false; - for (;;) { - pl_iwdg_refresh(pl_iwdg_get_handle()); + for (;;) { + pl_iwdg_refresh(pl_iwdg_get_handle()); - if (run_time >= 60) - pl_rtc_bkup_write(pl_rtc_get_handle(), 1, 0); - else - run_time++; + if (run_time >= 60) + pl_rtc_bkup_write(pl_rtc_get_handle(), 1, 0); + else + run_time++; - led_state = !led_state; - pl_gpio_write(PL_PORT_D, 9, led_state); /* LED = PD9 */ + led_state = !led_state; + pl_gpio_write(PL_PORT_D, 9, led_state); /* LED = PD9 */ - osDelay(500); - } + osDelay(500); + } } /* ---- 启动入口 ---- */ -void app_boot(void) -{ - const osThreadAttr_t attr = { - .name = "init_task", - .stack_size = 512 * 4, - .priority = osPriorityHigh, - }; - - osKernelInitialize(); - osThreadNew(init_task, NULL, &attr); - osKernelStart(); +void app_boot(void) { + const osThreadAttr_t attr = { + .name = "init_task", + .stack_size = 512 * 4, + .priority = osPriorityHigh, + }; + + osKernelInitialize(); + osThreadNew(init_task, NULL, &attr); + osKernelStart(); } -[[maybe_unused]] static void app_default_display(void) -{ - if (!app_render_restore()) { - app_render(&(render_cfg_t){ - .type = RENDER_TEXT, - .x = 0, - .y = 0, - .w = dev_display_get()->screen_rows, - .h = dev_display_get()->screen_cols, - .style = &(render_style_t){ - .h_align = ALIGN_CENTER, - .v_align = ALIGN_CENTER, - }, - .color = COLOR_RED, - .text = "车道关闭", - .len = strlen("车道关闭"), - .font_size = FONT_32, - .font_type = FONT_HT, - .text_enc = FONT_ENC_UTF8, - }); - } +[[maybe_unused]] static void app_splash_display(void) { + dev_display_t *dev = dev_display_get(); + char fw_line[32]; + char md_line[32]; + char splash_text[96]; + + snprintf(fw_line, sizeof(fw_line), "FW:%s", PROGRAM_CODE); + snprintf(md_line, sizeof(md_line), "MD:%s", dev->module_code); + snprintf(splash_text, sizeof(splash_text), "%s\n%s", fw_line, md_line); + + dev_display_fill(dev, 0, 0, dev->screen_rows, dev->screen_cols, COLOR_BLACK); + dev->dirty = false; + + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 0, + .w = dev->screen_rows, + .h = dev->screen_cols, + .style = + &(render_style_t){ + .h_align = ALIGN_CENTER, + .v_align = ALIGN_CENTER, + .word_wrap = false, + }, + .color = COLOR_GREEN, + .text = splash_text, + .len = strlen(splash_text), + .font_size = FONT_SELF_ADAPT, + .font_type = FONT_ST, + .text_enc = FONT_ENC_UTF8, + }); + + osDelay(SPLASH_DURATION_MS); + dev_display_fill(dev, 0, 0, dev->screen_rows, dev->screen_cols, COLOR_BLACK); + dev->dirty = false; } -static void init_task(void *argument) -{ - (void)argument; - - dev_eth_start(); - sw_board_init(); /* sw_initcall 自注册:协议 + 通道任务 */ - - /* 半秒周期任务 */ - const osThreadAttr_t hst_attr = { - .name = "half_sec_task", - .stack_size = 128 * 4, - .priority = osPriorityLow, - }; - osThreadNew(half_sec_task, NULL, &hst_attr); - - // app_tcp_server_start(); - // app_tcp_client_start(); - // app_udp_start(); - app_rs485_start(); - - // app_test_run(); - // app_default_display(); - - printf("\nInit Task Done\n"); - render_cfg_t ctx = { - .type = RENDER_BITMAP, - .x = 0, - .y = 0, - .w = dev_display_get()->screen_rows, - .h = dev_display_get()->screen_cols, - .color = COLOR_RED, - }; - app_bitmap_sychro(dev_display_get(), 0x01, ctx, true); - - osThreadExit(); +static void init_task(void *argument) { + (void)argument; + + dev_eth_start(); + sw_board_init(); /* sw_initcall 自注册:协议 + 通道任务 */ + + /* 版本落库:PROGRAM_CODE 写入 Sector1 app_info.version,供 IAP 0x03 上报。 + * 16KB 扇区擦除耗时数百 ms,写入前喂一次狗;同值跳过保证每次版本发布只擦一次 */ + pl_iwdg_refresh(pl_iwdg_get_handle()); + int32_t ver_ret = app_board_net_cfg_fw_version_update(PROGRAM_CODE); + (void)ver_ret; /* 失败不阻断启动(升级中间态/擦写错误静默降级,IAP 0x03 报旧值) */ + + /* 网络配置横切应用(app_net_boot 中立模块):Sector1 net_cfg → netif + TCP + * Server 口(PROTO_CHONGQING 只应用 netif)。顺序约束:fw_version_update 对 + * 空/损坏扇区初始化时 net_cfg 置 0 → 本函数判无效写默认(accept_write); + * ldi_module_init(sw_board_init 内)的 W25 自愈/回写已先于本调用完成。 */ + app_net_boot_apply(); + + app_splash_display(); + + /* 半秒周期任务 */ + const osThreadAttr_t hst_attr = { + .name = "half_sec_task", + .stack_size = 128 * 4, + .priority = osPriorityLow, + }; + osThreadNew(half_sec_task, NULL, &hst_attr); + + app_tcp_server_start(); + app_tcp_client_start(); + + app_udp_start(); + /* CQ 业务口 UDP(端口 Sector1 net_cfg.udp_port,默认 20103,PROTO_CHONGQING 读) */ + app_udp_cq_start(); + app_rs485_start(); + /* TX1/RX1:地区协议 RS232(USART3) */ + app_rs232_start(); + /* USART6:语音板 TTS 旁路 TX(dev_rs232_voice),禁止协议 bind;不启 RX + * 通道任务 */ + app_rs232_1_start(); + + // app_test_run(); + app_default_display(); + osThreadExit(); } /* ---- FreeRTOS 钩子 ---- */ -void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) -{ - (void)xTask; - (void)pcTaskName; - for (;;); +void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { + /* 调试时在此设断点,查看 pcTaskName 确定溢出任务 */ + volatile char *name = pcTaskName; + volatile TaskHandle_t task = xTask; + (void)name; + (void)task; + for (;;) + ; } -void vConfigureTimerForRunTimeStats(void) -{ - pl_dwt_init(); +void vApplicationMallocFailedHook(void) { + taskDISABLE_INTERRUPTS(); + for (;;) + ; } -uint32_t ulGetRunTimeCounterValue(void) -{ - return pl_dwt_get_cycles(); -} +void vConfigureTimerForRunTimeStats(void) { pl_dwt_init(); } + +uint32_t ulGetRunTimeCounterValue(void) { return pl_dwt_get_cycles(); } diff --git a/Application/Src/app_default_display.c b/Application/Src/app_default_display.c new file mode 100644 index 0000000..12c6e4c --- /dev/null +++ b/Application/Src/app_default_display.c @@ -0,0 +1,74 @@ +/** + * @file app_default_display.c + * @brief 注册制默认显示界面 — 协议模块可选覆盖,默认欢迎画面兜底 + * + * init_task 在 sw_board_init(sw_initcall 执行点)之后调用 + * app_default_display:协议模块若在 sw_app_initcall 中注册了回调, + * 则显示其默认界面;否则回退渲染「欢迎行驶\n高速公路」。 + */ + +#include +#include + +#include "app_default_display.h" +#include "app_render.h" +#include "dev_display.h" + +/* 默认显示回调槽:首个注册生效(4B bss) */ +static app_default_display_fn_t s_default_fn = nullptr; + +/* 系统默认欢迎画面(无协议注册时上电显示与故障恢复共用) */ +static void _render_welcome(void) +{ + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 0, + .w = dev_display_get()->screen_rows, + .h = dev_display_get()->screen_cols, + .style = &(render_style_t){ + .h_align = ALIGN_CENTER, + .v_align = ALIGN_LEFT_UP, + }, + .color = COLOR_YELLOW, + .text = "欢迎行驶\n高速公路", + .len = strlen("欢迎行驶\n高速公路"), + .font_size = FONT_SELF_ADAPT, + .font_type = FONT_HT, + .text_enc = FONT_ENC_UTF8, + }); +} + +void app_default_display_register(app_default_display_fn_t fn) +{ + if (s_default_fn == nullptr) + s_default_fn = fn; +} + +void app_default_display(void) +{ + if (s_default_fn) { + s_default_fn(); + return; + } + + /* 无协议注册:回退默认欢迎画面 */ + _render_welcome(); +} + +void app_default_display_show(void) +{ + if (s_default_fn) { + s_default_fn(); + return; + } + + /* 无协议注册:清屏后渲染系统默认欢迎画面(与上电默认一致; + * 重庆协议已取消专属上电画面,故障恢复回落到系统默认显示) */ + dev_display_t *d = dev_display_get(); + if (d != NULL) { + dev_display_fill(d, 0, 0, d->screen_rows, d->screen_cols, COLOR_BLACK); + dev_display_commit_frame(d); + } + _render_welcome(); +} \ No newline at end of file diff --git a/Application/Src/app_dispatch.c b/Application/Src/app_dispatch.c index f422acb..7bb8de1 100644 --- a/Application/Src/app_dispatch.c +++ b/Application/Src/app_dispatch.c @@ -32,17 +32,37 @@ static const osMessageQueueAttr_t s_ch_queue_attr = { static uint8_t _msg_dispatch_buf[sizeof(frame_msg_t) + FRAME_DATA_MAX_LEN]; /* ================================================================ - * 环形缓冲区池 — 编译期静态分配,按 id 复用 + * 环形缓冲区池 — 一物理通道一 RB,体由协议 TU RB_PROVIDE_WEAK 提供 * - * app_proto_acquire_buf(id, size) 按 id 返回预分配的 ring buffer。 - * 同一 id 多次调用返回同一指针(分组复用),首次调用时 rb_init。 - * id 与具体的 RB_DEFINE 通过 if(id==N) 硬绑定,扩容需同步修改。 + * 未编入任何 provide → weak 函数指针为 0 → acquire 返回 nullptr。 + * 编入 ≥1 协议 provide → 保留一个 getter(内含 SRAM static 缓冲)。 * ================================================================ */ -RB_DEFINE(g_rb0, 2048); /**< id=0: IAP 协议专用(高可靠性组) */ -RB_DEFINE(g_rb1, 2048); /**< id=1: 通用业务协议组(LDI 等共用) */ -RB_DEFINE(g_rb2, 2048); /**< id=2: 暂未使用,预留 */ -RB_DEFINE(g_rb3, 2048); /**< id=3: 暂未使用,预留 */ +extern ring_buffer_t *RB_PROVIDE_RJ45(void) __attribute__((weak)); +extern ring_buffer_t *RB_PROVIDE_RS485(void) __attribute__((weak)); +extern ring_buffer_t *RB_PROVIDE_RS232(void) __attribute__((weak)); + +typedef ring_buffer_t *(*rb_provide_fn_t)(void); + +static const rb_provide_fn_t g_rb_provide[RB_CNT_MAX] = { + [RB_SLOT_RJ45] = RB_PROVIDE_RJ45, + [RB_SLOT_RS485] = RB_PROVIDE_RS485, + [RB_SLOT_RS232] = RB_PROVIDE_RS232, +}; + +static const char *const names[RB_CNT_MAX] = { + [RB_SLOT_RJ45] = "rb_rj45", + [RB_SLOT_RS485] = "rb_rs485", + [RB_SLOT_RS232] = "rb_rs232", +}; + +_Static_assert(sizeof(g_rb_provide) / sizeof(g_rb_provide[0]) == RB_CNT_MAX, "g_rb_provide length"); +_Static_assert(sizeof(names) / sizeof(names[0]) == RB_CNT_MAX, "names length"); +_Static_assert(RB_SLOT_RJ45 == 0 && RB_SLOT_RS485 == 1 && RB_SLOT_RS232 == 2 && + RB_SLOT_COUNT == 3, + "rb_slot_t must stay contiguous"); +_Static_assert(RB_SIZE_RJ45 == 1536U && RB_SIZE_RS485 == 768U && RB_SIZE_RS232 == 768U, + "RB sizes must match product plan"); /* ================================================================ * 调度上下文 — 全部运行时的唯一状态聚合 @@ -90,6 +110,9 @@ uint8_t proto_index(uint32_t mask) */ proto_mask_t app_proto_register(proto_probe_fn_t probe, ring_buffer_t *rb) { + if (rb == nullptr) + return 0; + /* 找第一个空闲位 */ uint32_t free_bits = ~g_dispatch.registered_mask; if (free_bits == 0) return 0; /* 32 槽全满 */ @@ -133,39 +156,40 @@ void app_proto_bind_channel(proto_mask_t mask, channel_id_t ch_id) /** * @brief 从缓冲区池获取环形缓冲区 - * @param id 缓冲区编号(0 ~ RB_CNT_MAX-1) - * @param size 容量(当前未使用,预留) + * @param id rb_slot_t(RB_SLOT_RJ45 / RS485 / RS232) + * @param size 期望容量(不得超过该槽实际 size;无体时返回 nullptr) * @return 环形缓冲区指针,失败返回 nullptr * - * 同一 id 首次调用时 rb_init(互斥锁名 = "rb_N"), - * 后续调用直接返回已有指针,实现分组复用。 - * 所有缓冲区为编译期静态分配,无堆开销。 + * 判空顺序:id 越界 → buf_pool 缓存早返回 → provide==NULL/rb==NULL → size>rb->size → rb_init。 */ ring_buffer_t *app_proto_acquire_buf(uint8_t id, uint16_t size) { - if (id >= RB_CNT_MAX) return nullptr; - (void)size; + if (id >= RB_CNT_MAX) + return nullptr; - static const char *names[RB_CNT_MAX] = {"rb_0", "rb_1", "rb_2", "rb_3"}; - static ring_buffer_t *const g_rb_pool[RB_CNT_MAX] = {&g_rb0, &g_rb1, &g_rb2, &g_rb3}; - - ring_buffer_t *rb = g_rb_pool[id]; - - /* 已初始化则直接返回 */ if (g_dispatch.buf_pool[id] != nullptr) return g_dispatch.buf_pool[id]; - /* 首次使用:创建互斥锁 + 缓存指针 */ + rb_provide_fn_t provide = g_rb_provide[id]; + if (provide == nullptr) + return nullptr; + + ring_buffer_t *rb = provide(); + if (rb == nullptr) + return nullptr; + + if (size > rb->size) + return nullptr; + rb_init(rb, names[id]); g_dispatch.buf_pool[id] = rb; return rb; } /* ================================================================ - * 调度系统初始化 — sw_app_initcall(2),RTOS 后自动调用 + * 调度系统初始化 — sw_app_initcall(3),同层按符号名字母序先于协议注册 * * 创建 ch_queue → 创建 frame_dispatch_task → 返回。 - * 协议模块的注册由 sw_app_initcall(3) 在之后执行。 * ================================================================ */ void app_dispatch_init(void) @@ -201,18 +225,28 @@ sw_app_initcall(app_dispatch_init); void frame_dispatch_task(void *argument) { + (void)argument; channel_t *ch; /**< 来源通道指针(从 ch_queue 取出) */ frame_msg_t *msg = (frame_msg_t *)_msg_dispatch_buf; uint32_t frame_len = 0; /**< 探测到的完整帧长度 */ uint8_t aux = 0; /**< 辅助信息(如命令码) */ + /* WAIT 头阻塞预算状态(跨通知连续计时,本任务单消费者独占) */ + static bool s_wait_active = false; /**< 连续 any_wait 预算激活 */ + static uint32_t s_wait_tick = 0; /**< 预算起点 tick */ + static uint16_t s_wait_avail = 0; /**< 预算起点的 avail(检测增长) */ + for (;;) { /* 阻塞等待:任一通道收到数据时唤醒 */ if (osMessageQueueGet(g_dispatch.ch_queue, &ch, NULL, osWaitForever) != osOK) continue; - /* 通道已销毁或未初始化,跳过 */ - if (ch == nullptr) continue; + /* 通道回验:通知携带的指针必须仍注册在案,否则丢弃该通知。 + * 防御连接任务退出后栈上通道实例悬垂(配合通道静态化根治): + * 已注销(get 返回 NULL 或其它实例)的脏通知不进调度, + * 也避免用野 ch_id 越界索引 ch_proto_map。 */ + if (ch == nullptr || app_channel_get(ch->ch_id) != ch) + continue; /* 根据通道 ID 查表获得该通道承载的协议掩码 */ proto_mask_t proto = g_dispatch.ch_proto_map[ch->ch_id]; @@ -242,10 +276,20 @@ void frame_dispatch_task(void *argument) rb_lock(rb); uint16_t avail = rb_avail(rb, nullptr); - /* 内循环:从同一缓冲区中连续提取多帧 */ + /* 内循环:从同一缓冲区中连续提取多帧。 + * 防御性迭代上限:单轮通知最多解析 64 帧,超出则强制丢 1 字节 + * 重同步后退出,杜绝任何异常路径(如 SKIP 0 字节)死循环。 */ + uint8_t inner_guard = 0; while (avail > 0) { - bool parsed = false; /**< 本轮是否成功解析一帧 */ - bool all_wait = true; /**< 所有协议是否都返回 WAIT */ + if (++inner_guard > 64U) { + avail -= rb_skip(rb, 1, nullptr); + break; + } + + bool any_wait = false; /* 有协议:帧头可能匹配但数据不足 */ + bool any_fake = false; /* 有协议:明确不是我的帧 */ + bool any_overrun = false; /* 有协议:frame_len 越界不可信 */ + bool any_parsed = false; /* 有协议:READY 读走或 SKIP 跳过 */ /* 按协议优先级顺序探测已注册协议 */ uint32_t inner_iter = g_dispatch.registered_mask; @@ -262,51 +306,96 @@ void frame_dispatch_task(void *argument) proto_probe_sta_t state = g_dispatch.proto_probe[j](ch, rb, &frame_len, &aux); if (state == PROTO_PROBE_READY) { + /* 越界钳制:frame_len > 静态缓冲上限(1044B)时不可信, + * 不消费该帧;本轮不置 any_parsed,交给外层决策按 + * 重同步 skip 1 字节处理,防止 rb_read 写穿缓冲。 */ + if (frame_len > FRAME_DATA_MAX_LEN) { + any_overrun = true; + break; + } + + /* 帧头已匹配但数据未到齐:置 any_wait 等新字节, + * 不再置 any_parsed —— 旧代码此处置位而 avail 不变, + * 内层 while 空转活锁。 */ + if (avail < frame_len) { + any_wait = true; + break; + } + /* 完整帧就绪:从缓冲区读出 → 推入协议处理队列 */ - if (avail >= frame_len) { - uint16_t actual = rb_read(rb, msg->data, frame_len, nullptr); - avail = rb_avail(rb, nullptr); - - if (actual == frame_len) { - msg->data_len = frame_len; - msg->ch = ch; - osMessageQueuePut(g_dispatch.frame_queue[j], msg, 0, 0); - } else { - /* 异常:读出字节数不匹配,丢弃已读部分 */ - rb_skip(rb, actual, nullptr); - avail = rb_avail(rb, nullptr); - } + uint16_t actual = rb_read(rb, msg->data, frame_len, nullptr); + avail = rb_avail(rb, nullptr); + + if (actual == frame_len) { + msg->data_len = frame_len; + msg->ch = ch; + osMessageQueuePut(g_dispatch.frame_queue[j], msg, 0, 0); + } else { + /* 异常:读出字节数不匹配,丢弃已读部分 */ + rb_skip(rb, actual, nullptr); + avail = rb_avail(rb, nullptr); } - parsed = true; - all_wait = false; + any_parsed = true; break; /* 成功解析一帧,回到 while 继续下一帧 */ } else if (state == PROTO_PROBE_SKIP) { - /* 帧结构合法但不属于本设备,跳过整帧 */ - if (avail >= frame_len) { - avail -= rb_skip(rb, frame_len, nullptr); - parsed = true; + /* 帧结构合法但不属于本设备,跳过整帧。 + * frame_len 越界同样不可信 → 交外层重同步处理。 */ + if (frame_len > FRAME_DATA_MAX_LEN) { + any_overrun = true; + break; } - /* 数据不足时退化为 WAIT */ - all_wait = false; + /* 数据未到齐 → 置 any_wait 等新字节(防空转) */ + if (avail < frame_len) { + any_wait = true; + break; + } + avail -= rb_skip(rb, frame_len, nullptr); + any_parsed = true; + break; /* SKIP 与 READY 一样终止本轮链路 */ } else if (state == PROTO_PROBE_WAIT) { /* 数据不足,协议等待更多字节 —— 继续探测下一个协议 */ + any_wait = true; + } else if (state == PROTO_PROBE_FAKE) { - /* 伪帧头(如误匹配的 0x5A),跳过 1 字节重试 */ - all_wait = false; + /* 明确不是本协议 —— 继续探测下一个协议 */ + any_fake = true; } } - /* 无协议成功解析: - * 全部 WAIT → 退出内循环,等待更多数据 - * 至少一个 FAKE → 跳过 1 字节继续重试 + /* 无协议成功解析时的决策: + * any_wait → 禁 skip,等更多字节(带 500ms 时间预算防 + * 帧头匹配后数据永不到齐的挂死) + * any_overrun → frame_len 越界不可信,skip 1 字节重同步 + * any_fake → 全部不认识,skip 1 字节重同步 + * 其它 → 空缓冲区异常保护(防死循环) */ - if (!parsed) { - if (all_wait) - break; - else - avail -= rb_skip(rb, 1, nullptr); + if (!any_parsed) { + if (any_wait) { + /* 时间预算依据:串口 9600 波特率下最大帧 259B 传完约 + * 259*10/9600 ≈ 270ms,500ms 预算留足余量,不误伤 + * "帧头已匹配、载荷未到齐"的正常等待。 */ + uint32_t now = osKernelGetTickCount(); + if (!s_wait_active || avail > s_wait_avail) { + /* 首次等待 / 有新字节到达 → 重置预算起点 */ + s_wait_active = true; + s_wait_tick = now; + s_wait_avail = avail; + } else if ((now - s_wait_tick) > pdMS_TO_TICKS(500U)) { + /* 500ms 无消费且 avail 未增长 → 强制重同步 */ + avail -= rb_skip(rb, 1, nullptr); + s_wait_active = false; + } + break; /* 等待更多数据到达 */ + } else if (any_fake || any_overrun) { + s_wait_active = false; /* 有字节被消费,重置预算 */ + avail -= rb_skip(rb, 1, nullptr); /* 重同步 */ + } else { + break; /* 无协议绑定或探测函数全空,防死循环 */ + } + } else { + s_wait_active = false; /* 有帧被消费,重置预算 */ } } rb_unlock(rb); @@ -315,21 +404,27 @@ void frame_dispatch_task(void *argument) } /* ================================================================ - * app_channel_send — 通道发送(OCP:虚表分派) + * channel_send — 通道发送(OCP:虚表分派) * * 协议处理任务调用此函数回复数据,通过 ch_ops 虚表分派到 * 具体通道的 send 实现。新增通道类型无需修改此函数。 * - * 安全守卫: ch->ops == nullptr 表示通道已销毁,拒绝发送。 + * 安全守卫: ch->ops == nullptr 表示通道已销毁,拒绝发送(返回 -1)。 * TCP/UDP 连接任务退出前会置 ops = nullptr。 + * 通道回验: ch 必须仍注册在案(app_channel_get 返回同一指针), + * 防御连接任务退出后栈上通道实例悬垂(配合通道静态化根治); + * ch_id 越界时 app_channel_get 返回 NULL 即不通过。 * ================================================================ */ -void channel_send(channel_t *ch, uint8_t *data, uint16_t len) +int32_t channel_send(channel_t *ch, uint8_t *data, uint16_t len) { if (ch == nullptr || ch->ops == nullptr) - return; - if (ch->ops->send) - ch->ops->send(ch, data, len); + return -1; + if (app_channel_get(ch->ch_id) != ch) + return -1; + if (ch->ops->send == nullptr) + return -1; + return ch->ops->send(ch, data, len); } /* ================================================================ @@ -346,9 +441,15 @@ void channel_send(channel_t *ch, uint8_t *data, uint16_t len) void app_channel_dispatch(const channel_t *ch, const uint8_t *data, uint16_t len) { + /* 防御性断言:调度系统必须已初始化(同层字母序 app_dispatch_init 先于协议) */ + if (g_dispatch.ch_queue == nullptr) return; + /* 根据通道 ID 查表获得协议掩码 */ - proto_mask_t proto = g_dispatch.ch_proto_map[ch->ch_id]; - ring_buffer_t *seen[PROTO_MAX_COUNT] = {nullptr}; /**< 已写入的 RB 指针集合 */ + proto_mask_t proto = g_dispatch.ch_proto_map[ch->ch_id]; + + /* 已写入的 RB 去重:用计数器遍历,避免依赖 seen[] 连续填充假设 */ + ring_buffer_t *seen[PROTO_MAX_COUNT] = {nullptr}; + uint8_t seen_cnt = 0; /* 遍历已注册协议位,将数据写入匹配的环形缓冲区 */ uint32_t write_iter = g_dispatch.registered_mask; @@ -363,7 +464,7 @@ void app_channel_dispatch(const channel_t *ch, const uint8_t *data, uint16_t len /* RB 指针去重:多个协议共享同一缓冲区时只写一次 */ bool dup = false; - for (uint8_t k = 0; seen[k] != nullptr; k++) + for (uint8_t k = 0; k < seen_cnt; k++) if (seen[k] == rb) { dup = true; break; @@ -371,7 +472,7 @@ void app_channel_dispatch(const channel_t *ch, const uint8_t *data, uint16_t len if (dup) continue; rb_write(rb, data, len, rb->mutex); - seen[i] = rb; + seen[seen_cnt++] = rb; } // 关闭工厂模式 diff --git a/Application/Src/app_factory_test.c b/Application/Src/app_factory_test.c index a9786fb..a6e1954 100644 --- a/Application/Src/app_factory_test.c +++ b/Application/Src/app_factory_test.c @@ -1,11 +1,15 @@ /** * @file app_factory_test.c * @brief 出厂检测模式 — monitor 监听 TEST 驱动状态机 + * + * 按键测试阶段(SHOW_CODE / RED / GREEN / YELLOW):光敏自动调光生效。 + * 老化循环阶段(AGING):强制最大亮度,光敏暂停。 */ #include "app_factory_test.h" #include +#include #include "cmsis_os2.h" #include "initcall.h" #include "dev_display.h" @@ -13,16 +17,12 @@ #include "app_render.h" #include "app_dispatch.h" #include "app_light_sensor.h" +#include "app_test.h" +#include "dev_io_ctrl.h" +#include "stm32f4xx_hal.h" +#include "app_boot.h" #define AGING_TEXT "重庆创迪科技发展有限公司设备老化测试" -#define PROGRAM_CODE "9210209C41" - -static const display_color_t s_dead_pixel_colors[] = { - COLOR_RED, - COLOR_GREEN, - COLOR_YELLOW, -}; -#define DEAD_PIXEL_COLOR_COUNT (sizeof(s_dead_pixel_colors) / sizeof(s_dead_pixel_colors[0])) static const font_size_t s_aging_sizes[] = { FONT_16, @@ -40,6 +40,48 @@ static const font_type_t s_aging_types[] = { osThreadId_t g_factory_test; +/* ---- 业务数据中止机制 ---- + * 业务数据(任意通道)到达时 app_factory_mode_interrupt() 置位本标志。 + * monitor 任务不销毁、不回退:各按键等待点分片唤醒检查标志,中止当前 + * 检测序列后回到 IDLE 继续等待 TEST 键。业务数据到达仅中止当前 + * 检测序列,按键扫描持续运行,TEST 键始终可用。 */ +static volatile bool s_factory_abort; + +/* ---- 工厂测试激活态(供心跳类定时任务查询,暂停计数)---- */ +static volatile bool s_factory_active; + +bool app_factory_test_active(void) +{ + return s_factory_active; +} + +/** + * @brief 等待 TEST 键按下,支持业务数据中止。 + * @param timeout_ms 等待时限(osWaitForever 表示永久)。 + * @return true=按键按下;false=超时或业务数据中止。 + */ +static bool _factory_wait_tst(uint32_t timeout_ms) +{ + const uint32_t slice = 100U; /* 分片唤醒,响应 s_factory_abort */ + if (timeout_ms == osWaitForever) { + for (;;) { + if (dev_key_wait_press(DEV_KEY_TST, slice)) + return true; + if (s_factory_abort) + return false; + } + } + while (timeout_ms > 0U) { + uint32_t t = (timeout_ms > slice) ? slice : timeout_ms; + if (dev_key_wait_press(DEV_KEY_TST, t)) + return true; + if (s_factory_abort) + return false; + timeout_ms -= t; + } + return false; +} + /* ---- 老化辅助 ---- */ static void _aging_fill_screen(font_size_t size, font_type_t type, const char *ch_utf8, uint8_t ch_len) { @@ -51,9 +93,8 @@ static void _aging_fill_screen(font_size_t size, font_type_t type, const char *c if (rows == 0) rows = 1; dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_BLACK); - dsp->dirty = false; /* 防止 scan 在 fill 和 render 之间输出全黑帧 */ + dsp->dirty = false; - /* 把字符重复 cols×rows 份放缓冲区,word_wrap 自动分行 */ static char buf[256]; uint16_t pos = 0; uint16_t count = cols * rows; @@ -75,11 +116,12 @@ static void _aging_fill_screen(font_size_t size, font_type_t type, const char *c .font_type = type, .text_enc = FONT_ENC_UTF8, .style = &(render_style_t){ - .h_align = ALIGN_CENTER, - .v_align = ALIGN_CENTER, - .word_wrap = true, + .h_align = ALIGN_CENTER, + .v_align = ALIGN_CENTER, + .word_wrap = true, }, }); + dev_display_commit_frame(dsp); } /* ================================================================ @@ -92,10 +134,85 @@ static void factory_monitor_task(void *argument) dev_display_t *dsp = dev_display_get(); for (;;) { - /* IDLE: 等待 TEST 激活 */ - dev_key_wait_press(DEV_KEY_TST, osWaitForever); + idle_restart: + /* ===== IDLE: 等待 TEST 激活(业务数据中止 → 重新 IDLE) ===== */ + s_factory_abort = false; /* 进入检测序列前清中止标志 */ + s_factory_active = false; /* 回 IDLE 即退出激活态 */ + if (!_factory_wait_tst(osWaitForever)) + continue; + s_factory_active = true; /* 进入检测序列:激活态(占用显示资源) */ + + /* ===== 第1次按键 → SHOW_CODE: 显示固件/模组信息 ===== */ + { + const char *module_code = (dsp && dsp->module_code) ? dsp->module_code : "UNKNOWN"; + char info_buf[64]; + snprintf(info_buf, sizeof(info_buf), "FW:%s\nMD:%s", PROGRAM_CODE, module_code); + + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_BLACK); + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 0, + .w = dsp->screen_rows, + .h = dsp->screen_cols, + .color = COLOR_GREEN, + .text = info_buf, + .len = strlen(info_buf), + .font_size = FONT_SELF_ADAPT, + .font_type = FONT_ST, + .text_enc = FONT_ENC_UTF8, + .style = &(render_style_t){ + .h_align = ALIGN_CENTER, + .v_align = ALIGN_CENTER, + }, + }); + } + + /* ===== 第2次按键 → RED: 全屏红色 ===== */ + if (!_factory_wait_tst(osWaitForever)) + goto idle_restart; + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_RED); + dev_display_commit_frame(dsp); + + /* ===== 第3次按键 → GREEN: 全屏绿色 ===== */ + if (!_factory_wait_tst(osWaitForever)) + goto idle_restart; + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_GREEN); + dev_display_commit_frame(dsp); + + /* ===== 第4次按键 → YELLOW: 全屏黄色 ===== */ + if (!_factory_wait_tst(osWaitForever)) + goto idle_restart; + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_YELLOW); + dev_display_commit_frame(dsp); + + /* ===== 第5次按键 → WHITE: 全屏白色 ===== */ + if (!_factory_wait_tst(osWaitForever)) + goto idle_restart; + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_WHITE); + dev_display_commit_frame(dsp); + + /* ===== 第6次按键 → BLUE: 全屏蓝色 ===== */ + if (!_factory_wait_tst(osWaitForever)) + goto idle_restart; + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_BLUE); + dev_display_commit_frame(dsp); - /* ===== SHOW_CODE ===== */ + /* ===== 第7次按键 → PURPLE: 全屏紫色 ===== */ + if (!_factory_wait_tst(osWaitForever)) + goto idle_restart; + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_PURPLE); + dev_display_commit_frame(dsp); + + /* ===== 第8次按键 → CYAN: 全屏青色 ===== */ + if (!_factory_wait_tst(osWaitForever)) + goto idle_restart; + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_CYAN); + dev_display_commit_frame(dsp); + + /* ===== 第9次按键 → 通信灯红叉 + 报警灯开启 ===== */ + if (!_factory_wait_tst(osWaitForever)) + goto idle_restart; dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_BLACK); app_render(&(render_cfg_t){ .type = RENDER_TEXT, @@ -104,31 +221,56 @@ static void factory_monitor_task(void *argument) .w = dsp->screen_rows, .h = dsp->screen_cols, .color = COLOR_GREEN, - .text = PROGRAM_CODE, - .len = strlen(PROGRAM_CODE), - .font_size = FONT_16, + .text = "通信灯红叉\n报警灯开启", + .len = strlen("通信灯红叉\n报警灯开启"), + .font_size = FONT_SELF_ADAPT, .font_type = FONT_ST, .text_enc = FONT_ENC_UTF8, .style = &(render_style_t){ - .h_align = ALIGN_CENTER, - .v_align = ALIGN_CENTER, + .h_align = ALIGN_CENTER, + .v_align = ALIGN_CENTER, }, }); + dev_io_lane_light(false); + dev_io_flash_light(true); + dev_display_commit_frame(dsp); - dev_key_wait_press(DEV_KEY_TST, osWaitForever); + /* ===== 第10次按键 → 通信灯绿箭 + 报警灯关闭 ===== */ + if (!_factory_wait_tst(osWaitForever)) + goto idle_restart; + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_BLACK); + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, + .x = 0, + .y = 0, + .w = dsp->screen_rows, + .h = dsp->screen_cols, + .color = COLOR_GREEN, + .text = "通信灯绿箭\n报警灯关闭", + .len = strlen("通信灯绿箭\n报警灯关闭"), + .font_size = FONT_SELF_ADAPT, + .font_type = FONT_ST, + .text_enc = FONT_ENC_UTF8, + .style = &(render_style_t){ + .h_align = ALIGN_CENTER, + .v_align = ALIGN_CENTER, + }, + }); + dev_io_lane_light(true); + dev_io_flash_light(false); + dev_display_commit_frame(dsp); - /* ===== DEAD_PIXEL ===== */ + /* ===== 第11次按键 → AGING: 进入老化循环 ===== + * 暂停光敏,强制最大亮度 */ + if (!_factory_wait_tst(osWaitForever)) + goto idle_restart; osThreadSuspend(g_light_sensor_task_handle); - dev_display_set_brightness(dsp, 7); - for (uint8_t i = 0; i < DEAD_PIXEL_COLOR_COUNT; i++) { - dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, s_dead_pixel_colors[i]); - dev_key_wait_press(DEV_KEY_TST, osWaitForever); - } - - /* ===== AGING ===== */ - osThreadResume(g_light_sensor_task_handle); + dev_display_set_brightness(dsp, DEV_DISPLAY_BRIGHTNESS_MAX); + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_BLACK); + dev_display_commit_frame(dsp); bool aging_exit = false; + bool lane_green = true; for (uint8_t type_idx = 0; !aging_exit; type_idx = (type_idx + 1) % AGING_TYPE_COUNT) { for (uint8_t size_idx = 0; size_idx < AGING_SIZE_COUNT; size_idx++) { font_size_t fsize = s_aging_sizes[size_idx]; @@ -141,8 +283,11 @@ static void factory_monitor_task(void *argument) ch_len > 2 ? ch_ptr[2] : 0, 0}; _aging_fill_screen(fsize, s_aging_types[type_idx], single_ch, ch_len); + dev_io_lane_light(lane_green); + lane_green = !lane_green; - if (dev_key_wait_press(DEV_KEY_TST, 3000)) { + bool pressed = _factory_wait_tst(3000); + if (pressed || s_factory_abort) { aging_exit = true; break; } @@ -153,8 +298,14 @@ static void factory_monitor_task(void *argument) if (aging_exit) break; } - /* 退出工厂模式 */ - dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_BLACK); + /* 老化退出(含业务数据中止)→ 恢复光敏自动调光 */ + osThreadResume(g_light_sensor_task_handle); + + /* ===== 第12次按键 → OBLIQUE_SCAN: 斜扫模式 ===== */ + app_test_oblique_scan(); + + /* ===== 第13次按键 → 重启程序 ===== */ + NVIC_SystemReset(); } } @@ -163,17 +314,17 @@ static void _factory_test_init(void) { const osThreadAttr_t attr = { .name = "factory_monitor", - .stack_size = 512 * 4, - .priority = osPriorityBelowNormal, + .stack_size = 256 * 4, /* 再压一档;原 2KB/4KB 过大,多协议下挤堆 */ + .priority = osPriorityHigh, }; g_factory_test = osThreadNew(factory_monitor_task, NULL, &attr); } sw_app_initcall(_factory_test_init); -// 对外提供一个终止工厂测试模式的接口 +/* 收到业务数据时中止工厂监控当前序列(置中止标志,monitor 任务回 IDLE 重新待机)。 + * 不销毁任务:TEST 键出厂检测/老化入口必须随时可用。 + * 业务数据到达仅中止当前检测序列,按键扫描持续运行。 */ void app_factory_mode_interrupt(void) { - // dev_display_fill(dev_display_get(), 0, 0, dev_display_get()->screen_rows, dev_display_get()->screen_cols, COLOR_BLACK); - osThreadTerminate(g_factory_test); - _factory_test_init(); + s_factory_abort = true; } diff --git a/Application/Src/app_light_sensor.c b/Application/Src/app_light_sensor.c index 50694bd..5b11aa6 100644 --- a/Application/Src/app_light_sensor.c +++ b/Application/Src/app_light_sensor.c @@ -7,56 +7,28 @@ #include "cmsis_os2.h" #include "initcall.h" #include "dev_display.h" -#include "pl_uart.h" - -#include "app_key.h" -#include "app_msl.h" -#include "bcc_utils.h" static light_sensor_dev_t s_sensor_dev; osThreadId_t g_light_sensor_task_handle; -static void send_light_adjust(uint8_t light_lev) -{ - uint8_t _buf[sizeof(msl_frame_t) + 2] = {0}; - msl_frame_t *f = (msl_frame_t *)_buf; - - f->head[0] = 0x5a; - f->head[1] = 0x5a; - - f->addr = 0; - - f->length[0] = 1; - f->length[1] = 0; - - f->cmd = MSL_CMD_LIGHTLEVEL; - - f->data_bcc[0] = light_lev; - - // bcc校验 - f->data_bcc[2] = bcc_calcu(&(f->addr), sizeof(msl_frame_t) - 1); - - // 发送数据帧 - pl_uart_send(pl_uart_get_handle(PL_UART1), _buf, sizeof(msl_frame_t) + 2, 50); -} - void app_light_sensor_task(void *argument) { (void)argument; for (;;) { dev_light_sensor_auto_adjust(&s_sensor_dev); - send_light_adjust(s_sensor_dev.display->light_level); osDelay(1000); } } -void app_light_sensor_init(void) +void app_light_sensor_set_range(uint8_t min, uint8_t max) { - // 若不是主卡则不开自动亮度 - if (0 != (app_key_get_state(DEV_KEY_DIP1) | app_key_get_state(DEV_KEY_DIP2))) - return; + dev_light_sensor_set_range(&s_sensor_dev, min, max); +} +void app_light_sensor_init(void) +{ dev_light_sensor_init(&s_sensor_dev, dev_display_get()); + dev_light_sensor_set_range(&s_sensor_dev, LIGHT_SENSOR_MIN_LEVEL, LIGHT_SENSOR_MAX_LEVEL); const osThreadAttr_t attr = { .name = "light_sensor_task", @@ -66,8 +38,3 @@ void app_light_sensor_init(void) g_light_sensor_task_handle = osThreadNew(app_light_sensor_task, NULL, &attr); } sw_app_initcall(app_light_sensor_init); - -light_sensor_dev_t *app_light_sensor_get(void) -{ - return &s_sensor_dev; -} diff --git a/Application/Src/app_net_boot.c b/Application/Src/app_net_boot.c new file mode 100644 index 0000000..f6eb4e6 --- /dev/null +++ b/Application/Src/app_net_boot.c @@ -0,0 +1,70 @@ +/** + * @file app_net_boot.c + * @brief 网络配置启动应用(横切职责中立模块,两口径共用) + * + * 职责:「把 Sector1 net_cfg 应用到 netif + TCP Server 口」统一收拢于此, + * 与 LDI / CQ 协议模块解耦: + * - ldi_ctx_init(sw_board_init 内)只负责 Sector1/W25 自愈与 LDI 设备配置 + * 装载,不再调用 pl_net_set_ip / app_tcp_server_set_port; + * - TCP Server 口两口径统一应用 net_cfg.port(2026-08-24 修订):TCP Server/ + * Client 通道由 app_boot.c 无条件启动(无口径裁剪),故端口应用也不得按 + * PROTO_CHONGQING 裁剪——否则「PROTO_CHONGQING 与 LDI 共存」的混合构建 + * (如 EIDE Debug 口径错配)中 TCP 口恒为编译期默认 9528,与 LDI 12H / + * IAP 0x01 上报的 Sector1 net_cfg.port(0AH/4B02 已改值)矛盾; + * - CQ UDP 业务口由 _udp_cq_read_port 直接读 Sector1 net_cfg.udp_port + * (方案 B,2026-08-21:CQ setip 只改 udp_port,TCP 口 port 不受污染—— + * setip 端口语义不因本修订改变)。 + * + * accept_write(两口径统一):Sector1 空/损坏/非法(IP 全 0 或 port 不在 + * 1~65535)→ 写本构建默认记录落盘并应用: + * - PROTO_CHONGQING:192.168.1.5 / 255.255.255.0 / 192.168.1.1 / port 9528 / + * udp_port 20103; + * - 其余(dev/LDI):192.168.114.200 / 255.255.255.0 / 192.168.114.1 / port 9528 / + * udp_port 20103。 + * + * 顺序约束:ldi_module_init(含 ldi_ctx_init 的 W25 镜像自愈与 Sector1 回写) + * 先于本调用执行;本函数由 app_boot.c init_task 在 + * app_board_net_cfg_fw_version_update 之后调用——fw_version_update 对空/损坏 + * 扇区初始化时 net_cfg 置 0,本函数随后判无效写默认(accept_write)。 + */ +#include +#include + +#include "app_board_net_cfg.h" +#include "app_tcp_server.h" +#include "pl_net.h" + +void app_net_boot_apply(void) +{ + app_board_net_cfg_t cfg; + bool valid = (app_board_net_cfg_get(&cfg) == 0) && + (cfg.ip[0] | cfg.ip[1] | cfg.ip[2] | cfg.ip[3]) != 0 && + cfg.port > 0 && cfg.port <= 65535; + + if (!valid) { + /* accept_write:Sector1 空/损坏/非法 → 写本构建默认记录落盘并应用。 + * 方案 B(2026-08-21):port(TCP 业务口)两口径默认均 9528;udp_port + * (CQ UDP 业务口)默认 20103——两字段完整落盘保持记录自洽。 */ +#ifdef PROTO_CHONGQING + static const uint8_t d_ip[4] = {192, 168, 1, 5}; + static const uint8_t d_mask[4] = {255, 255, 255, 0}; + static const uint8_t d_gw[4] = {192, 168, 1, 1}; +#else + static const uint8_t d_ip[4] = {192, 168, 114, 200}; + static const uint8_t d_mask[4] = {255, 255, 255, 0}; + static const uint8_t d_gw[4] = {192, 168, 114, 1}; +#endif + const uint32_t d_port = 9528; /* TCP 业务口默认(两口径统一) */ + const uint32_t d_udp_port = 20103; /* CQ UDP 业务口默认 */ + (void)app_board_net_cfg_update(d_ip, d_mask, d_gw, d_port, d_udp_port); + pl_net_set_ip(d_ip, d_mask, d_gw); + app_tcp_server_set_port((uint16_t)d_port); + return; + } + + /* Sector1 有效:应用 netif + TCP Server 口(两口径统一,2026-08-24 修订: + * TCP 口恒与 LDI 12H / IAP 0x01 上报的 net_cfg.port 一致;CQ UDP 业务口 + * 由 _udp_cq_read_port 读 cfg.udp_port,与本处无涉) */ + pl_net_set_ip(cfg.ip, cfg.mask, cfg.gw); + app_tcp_server_set_port((uint16_t)cfg.port); +} \ No newline at end of file diff --git a/Application/Src/app_render.c b/Application/Src/app_render.c index baccb39..4e6ea3b 100644 --- a/Application/Src/app_render.c +++ b/Application/Src/app_render.c @@ -2,9 +2,18 @@ * @file app_render.c * @brief 文字/图形渲染实现 — 数据驱动字库引擎 * - * 字库布局: (字号, 编码, 字型) 三元组在 Flash 中顺序拼接。 - * g_font_lib 描述每个三元组的总字节数,按 Flash 顺序排列。 - * 新增字号/字型只需追加条目。 + * 上电时通过 DEV_KEY_DIP2 拨码开关自动选择字库芯片配置: + * - DIP2 逻辑 0(开关 OFF,GPIO 高)→ W25Q64: 非连续布局, 16/24/32pt + * - DIP2 逻辑 1(开关 ON,GPIO 低)→ MX25L256: 连续布局, 14/16/20/24/32pt + * + * 两种芯片的关键差异: + * - W25Q64: ASCII 用原始字符码, GBK 用94列序 (ch[0]-0xA1)*94+... + * - MX25L256: ASCII 用 ch-0x20, GBK 用190列序 (ch[0]-0x81)*190+... + * + * 每种芯片对应一个 font_chip_config_t 实例(含 flash_region_t 地址表)。 + * 通过 app_render_chip_select() 可在运行中手动切换。 + * + * 参考文档: doc/02_LDI协议与外设接口/字库芯片更换与移植指导.md */ #include "app_render.h" @@ -14,108 +23,233 @@ #include "initcall.h" #include "crc_utils.h" #include "dev_w25qxx.h" -#include "pl_uart.h" +#include "dev_key.h" -/* ---- 字库单元描述 ---- */ +/* ---- 字库区块描述:三元组 (size, charset, type) + Flash 地址修正量 ---- */ typedef struct { - font_key_t key; - uint32_t unit_size; /* 该三元组的总字节数 */ -} font_unit_t; + font_size_t size; /* 字号 (像素高度) */ + font_enc_t charset; /* 编码: FONT_ENC_ASCII / FONT_ENC_GBK */ + font_type_t type; /* 字型: ST/FS/KT/HT */ + uint16_t base; /* 扇区基准 (从01Embedded func.h 宏的整数除法得到) */ + int8_t sec_adj; /* 扇区修正量 X (readaddr 公式中 sec+X) */ + int8_t page_adj; /* 页修正量 Y (readaddr 公式中 page+Y, 单位256字节) */ + int16_t byte_adj; /* 字节修正量 Z (readaddr 公式中 byte+Z) */ +} flash_region_t; #define N_ASC_CHARS (96U) #define N_GBK_CHARS (23940U) -/* 字库单元字节数: ASCII = (size/2)宽 × size高 × N_ASC_CHARS字; GBK = size宽 × size高 × N_GBK_CHARS */ -#define ASC_UNIT(sz) ((uint32_t)(sz) * (((sz) / 2 + 7) / 8) * N_ASC_CHARS) -#define GBK_UNIT(sz) ((uint32_t)(sz) * (((sz) + 7) / 8) * N_GBK_CHARS) - -/* 字库描述表 — 顺序必须与 Flash 中字库单元的排列一致 */ -static const font_unit_t g_font_lib[] = { - /* 14号 */ - {{.size = 14, .charset = FONT_ENC_ASCII, .type = FONT_ST}, ASC_UNIT(14)}, - {{.size = 14, .charset = FONT_ENC_ASCII, .type = FONT_FS}, ASC_UNIT(14)}, - {{.size = 14, .charset = FONT_ENC_ASCII, .type = FONT_KT}, ASC_UNIT(14)}, - {{.size = 14, .charset = FONT_ENC_ASCII, .type = FONT_HT}, ASC_UNIT(14)}, - {{.size = 14, .charset = FONT_ENC_GBK, .type = FONT_ST}, GBK_UNIT(14)}, - {{.size = 14, .charset = FONT_ENC_GBK, .type = FONT_FS}, GBK_UNIT(14)}, - {{.size = 14, .charset = FONT_ENC_GBK, .type = FONT_KT}, GBK_UNIT(14)}, - {{.size = 14, .charset = FONT_ENC_GBK, .type = FONT_HT}, GBK_UNIT(14)}, - /* 16号 */ - {{.size = 16, .charset = FONT_ENC_ASCII, .type = FONT_ST}, ASC_UNIT(16)}, - {{.size = 16, .charset = FONT_ENC_ASCII, .type = FONT_FS}, ASC_UNIT(16)}, - {{.size = 16, .charset = FONT_ENC_ASCII, .type = FONT_KT}, ASC_UNIT(16)}, - {{.size = 16, .charset = FONT_ENC_ASCII, .type = FONT_HT}, ASC_UNIT(16)}, - {{.size = 16, .charset = FONT_ENC_GBK, .type = FONT_ST}, GBK_UNIT(16)}, - {{.size = 16, .charset = FONT_ENC_GBK, .type = FONT_FS}, GBK_UNIT(16)}, - {{.size = 16, .charset = FONT_ENC_GBK, .type = FONT_KT}, GBK_UNIT(16)}, - {{.size = 16, .charset = FONT_ENC_GBK, .type = FONT_HT}, GBK_UNIT(16)}, - /* 20号 */ - {{.size = 20, .charset = FONT_ENC_ASCII, .type = FONT_ST}, ASC_UNIT(20)}, - {{.size = 20, .charset = FONT_ENC_ASCII, .type = FONT_FS}, ASC_UNIT(20)}, - {{.size = 20, .charset = FONT_ENC_ASCII, .type = FONT_KT}, ASC_UNIT(20)}, - {{.size = 20, .charset = FONT_ENC_ASCII, .type = FONT_HT}, ASC_UNIT(20)}, - {{.size = 20, .charset = FONT_ENC_GBK, .type = FONT_ST}, GBK_UNIT(20)}, - {{.size = 20, .charset = FONT_ENC_GBK, .type = FONT_FS}, GBK_UNIT(20)}, - {{.size = 20, .charset = FONT_ENC_GBK, .type = FONT_KT}, GBK_UNIT(20)}, - {{.size = 20, .charset = FONT_ENC_GBK, .type = FONT_HT}, GBK_UNIT(20)}, - /* 24号 */ - {{.size = 24, .charset = FONT_ENC_ASCII, .type = FONT_ST}, ASC_UNIT(24)}, - {{.size = 24, .charset = FONT_ENC_ASCII, .type = FONT_FS}, ASC_UNIT(24)}, - {{.size = 24, .charset = FONT_ENC_ASCII, .type = FONT_KT}, ASC_UNIT(24)}, - {{.size = 24, .charset = FONT_ENC_ASCII, .type = FONT_HT}, ASC_UNIT(24)}, - {{.size = 24, .charset = FONT_ENC_GBK, .type = FONT_ST}, GBK_UNIT(24)}, - {{.size = 24, .charset = FONT_ENC_GBK, .type = FONT_FS}, GBK_UNIT(24)}, - {{.size = 24, .charset = FONT_ENC_GBK, .type = FONT_KT}, GBK_UNIT(24)}, - {{.size = 24, .charset = FONT_ENC_GBK, .type = FONT_HT}, GBK_UNIT(24)}, - /* 32号 */ - {{.size = 32, .charset = FONT_ENC_ASCII, .type = FONT_ST}, ASC_UNIT(32)}, - {{.size = 32, .charset = FONT_ENC_ASCII, .type = FONT_FS}, ASC_UNIT(32)}, - {{.size = 32, .charset = FONT_ENC_ASCII, .type = FONT_KT}, ASC_UNIT(32)}, - {{.size = 32, .charset = FONT_ENC_ASCII, .type = FONT_HT}, ASC_UNIT(32)}, - {{.size = 32, .charset = FONT_ENC_GBK, .type = FONT_ST}, GBK_UNIT(32)}, - {{.size = 32, .charset = FONT_ENC_GBK, .type = FONT_FS}, GBK_UNIT(32)}, - {{.size = 32, .charset = FONT_ENC_GBK, .type = FONT_KT}, GBK_UNIT(32)}, - {{.size = 32, .charset = FONT_ENC_GBK, .type = FONT_HT}, GBK_UNIT(32)}, +/* + * W25Q64 字库区块地址表 + * + * 01Embedded readaddr 公式: + * FonfAddr = char_idx × bytes_per_char + * sec = FonfAddr / 4096 + * page = (FonfAddr % 4096) / 256 + * byte = (FonfAddr % 4096) % 256 + * readaddr = (base + sec + X) × 4096 + (page + Y) × 256 + byte + Z + * + * 简化为: + * readaddr = (base + sec + X) × 4096 + (page + Y) × 256 + byte + Z + * = base × 4096 + (sec + X) × 4096 + (page + Y) × 256 + byte + Z + * + * 其中 X, Y, Z 是每个字型的固定修正量,由01Embedded func.c 的6个读取函数推导: + * readaddr = (ADDRESS_MACRO + sectoraddr [+X]) * 4096 + * + (pageaddr [+Y]) * 256 + byteaddr [+Z] + * + * 验证: 逐字符地址已与01Embedded的6个读取函数交叉验证 + */ +static const flash_region_t g_font_regions[] = { + /* 16号字体 — W25Q64 无 14/20号 */ + /* size enc type base X Y Z */ + /* 16pt ASCII — bytes_per_char=16 */ + {16, FONT_ENC_ASCII, FONT_ST, 0, 0, 0, 0}, /* (0+sec)*4096 + page*256 + byte */ + {16, FONT_ENC_ASCII, FONT_FS, 0, 0, 0, 0}, /* (0+sec)*4096 + page*256 + byte */ + {16, FONT_ENC_ASCII, FONT_KT, 1, 0, 0, 64}, /* (1+sec)*4096 + page*256 + byte+64 */ + {16, FONT_ENC_ASCII, FONT_HT, 1, 0, 0, 64}, /* (1+sec)*4096 + page*256 + byte+64 */ + /* 16pt GB2312 — bytes_per_char=32 */ + {16, FONT_ENC_GBK, FONT_ST, 2, 0, 0, 128}, /* (2+sec)*4096 + page*256 + byte+128 */ + {16, FONT_ENC_GBK, FONT_FS, 71, 0, 0, 288}, /* (71+sec)*4096 + page*256 + byte+288 */ + {16, FONT_ENC_GBK, FONT_KT, 140, 0, 0, 448}, /* (140+sec)*4096 + page*256 + byte+448 */ + {16, FONT_ENC_GBK, FONT_HT, 209, 0, 2, 96}, /* (209+sec)*4096 + (page+2)*256 + byte+96 */ + /* 24号字体 */ + /* 24pt ASCII — bytes_per_char=48 */ + {24, FONT_ENC_ASCII, FONT_ST, 278, 0, 3, 0}, /* (278+sec)*4096 + (page+3)*256 + byte */ + {24, FONT_ENC_ASCII, FONT_FS, 279, 0, 12, -224}, /* (279+sec)*4096 + (page+12)*256 + byte-224 */ + {24, FONT_ENC_ASCII, FONT_KT, 281, 0, 3, 64}, /* (281+sec)*4096 + (page+3)*256 + byte+64 */ + {24, FONT_ENC_ASCII, FONT_HT, 282, 0, 12, -160}, /* (282+sec)*4096 + (page+12)*256 + byte-160 */ + /* 24pt GB2312 — bytes_per_char=72 */ + {24, FONT_ENC_GBK, FONT_ST, 284, 1, -12, -128}, /* (285+sec)*4096 + (page-12)*256 + byte-128 */ + {24, FONT_ENC_GBK, FONT_FS, 439, 1, -7, -64}, /* (440+sec)*4096 + (page-7)*256 + byte-64 */ + {24, FONT_ENC_GBK, FONT_KT, 594, 1, -2, 0}, /* (595+sec)*4096 + (page-2)*256 + byte */ + {24, FONT_ENC_GBK, FONT_HT, 750, 1, -12, -192}, /* (751+sec)*4096 + (page-12)*256 + byte-192 */ + /* 32号字体 */ + /* 32pt ASCII — bytes_per_char=64 */ + {32, FONT_ENC_ASCII, FONT_ST, 905, 1, -7, -128}, /* (906+sec)*4096 + (page-7)*256 + byte-128 */ + {32, FONT_ENC_ASCII, FONT_FS, 907, 1, -7, -96}, /* (908+sec)*4096 + (page-7)*256 + byte-96 */ + {32, FONT_ENC_ASCII, FONT_KT, 909, 1, -7, -64}, /* (910+sec)*4096 + (page-7)*256 + byte-64 */ + {32, FONT_ENC_ASCII, FONT_HT, 911, 1, -7, -32}, /* (912+sec)*4096 + (page-7)*256 + byte-32 */ + /* 32pt GB2312 — bytes_per_char=128 */ + {32, FONT_ENC_GBK, FONT_ST, 913, 1, -7, 0}, /* (914+sec)*4096 + (page-7)*256 + byte */ + {32, FONT_ENC_GBK, FONT_FS, 1189, 1, -5, 32}, /* (1190+sec)*4096 + (page-5)*256 + byte+32 */ + {32, FONT_ENC_GBK, FONT_KT, 1465, 1, -3, 64}, /* (1466+sec)*4096 + (page-3)*256 + byte+64 */ + {32, FONT_ENC_GBK, FONT_HT, 1741, 1, -1, 96}, /* (1742+sec)*4096 + (page-1)*256 + byte+96 */ +}; + +/* + * MX25L256 字库地址表(连续布局,从 STD-main-Orig g_font_lib 推算) + * + * 布局: 5种字号(14/16/20/24/32) × 4种字型(ST/FS/KT/HT),每组内 ASCII+GB2312 连续拼接。 + * 地址计算与 W25Q64 共用 sec/page/byte 分步公式: + * readaddr = (base + sec + X) * 4096 + (page + Y) * 256 + byte + Z + * 但 X=0(无扇区修正),Y/Z 用于补偿非扇区对齐的块起始偏移。 + * base 值为累计字节偏移的扇区号,Y/Z 为偏移余数的页/字节拆分。 + * GBK 索引用 190 列序(GBK),非94列序(GB2312)。 + */ +static const flash_region_t g_font_regions_orig[] = { + /* size enc type base X Y Z */ + /* 14pt — bpg: ASCII=14, GBK=28 */ + {14, FONT_ENC_ASCII, FONT_ST, 0, 0, 0, 0}, + {14, FONT_ENC_ASCII, FONT_FS, 0, 0, 5, 64}, + {14, FONT_ENC_ASCII, FONT_KT, 0, 0, 10, 128}, + {14, FONT_ENC_ASCII, FONT_HT, 0, 0, 15, 192}, + {14, FONT_ENC_GBK, FONT_ST, 1, 0, 5, 0}, + {14, FONT_ENC_GBK, FONT_FS, 164, 0, 15, 112}, + {14, FONT_ENC_GBK, FONT_KT, 328, 0, 9, 224}, + {14, FONT_ENC_GBK, FONT_HT, 492, 0, 4, 80}, + /* 16pt — bpg: ASCII=16, GBK=32 */ + {16, FONT_ENC_ASCII, FONT_ST, 655, 0, 14, 192}, + {16, FONT_ENC_ASCII, FONT_FS, 656, 0, 4, 192}, + {16, FONT_ENC_ASCII, FONT_KT, 656, 0, 10, 192}, + {16, FONT_ENC_ASCII, FONT_HT, 657, 0, 0, 192}, + {16, FONT_ENC_GBK, FONT_ST, 657, 0, 6, 192}, + {16, FONT_ENC_GBK, FONT_FS, 844, 0, 7, 64}, + {16, FONT_ENC_GBK, FONT_KT, 1031, 0, 7, 192}, + {16, FONT_ENC_GBK, FONT_HT, 1218, 0, 8, 64}, + /* 20pt — bpg: ASCII=40, GBK=60 */ + {20, FONT_ENC_ASCII, FONT_ST, 1405, 0, 8, 192}, + {20, FONT_ENC_ASCII, FONT_FS, 1406, 0, 7, 192}, + {20, FONT_ENC_ASCII, FONT_KT, 1407, 0, 6, 192}, + {20, FONT_ENC_ASCII, FONT_HT, 1408, 0, 5, 192}, + {20, FONT_ENC_GBK, FONT_ST, 1409, 0, 4, 192}, + {20, FONT_ENC_GBK, FONT_FS, 1759, 0, 15, 176}, + {20, FONT_ENC_GBK, FONT_KT, 2110, 0, 10, 160}, + {20, FONT_ENC_GBK, FONT_HT, 2461, 0, 5, 144}, + /* 24pt — bpg: ASCII=48, GBK=72 */ + {24, FONT_ENC_ASCII, FONT_ST, 2812, 0, 0, 128}, + {24, FONT_ENC_ASCII, FONT_FS, 2813, 0, 2, 128}, + {24, FONT_ENC_ASCII, FONT_KT, 2814, 0, 4, 128}, + {24, FONT_ENC_ASCII, FONT_HT, 2815, 0, 6, 128}, + {24, FONT_ENC_GBK, FONT_ST, 2816, 0, 8, 128}, + {24, FONT_ENC_GBK, FONT_FS, 3237, 0, 5, 160}, + {24, FONT_ENC_GBK, FONT_KT, 3658, 0, 2, 192}, + {24, FONT_ENC_GBK, FONT_HT, 4078, 0, 15, 224}, + /* 32pt — bpg: ASCII=64, GBK=128 */ + {32, FONT_ENC_ASCII, FONT_ST, 4499, 0, 13, 0}, + {32, FONT_ENC_ASCII, FONT_FS, 4501, 0, 5, 0}, + {32, FONT_ENC_ASCII, FONT_KT, 4502, 0, 13, 0}, + {32, FONT_ENC_ASCII, FONT_HT, 4504, 0, 5, 0}, + {32, FONT_ENC_GBK, FONT_ST, 4505, 0, 13, 0}, + {32, FONT_ENC_GBK, FONT_FS, 5253, 0, 15, 0}, + {32, FONT_ENC_GBK, FONT_KT, 6002, 0, 1, 0}, + {32, FONT_ENC_GBK, FONT_HT, 6750, 0, 3, 0}, +}; + +/* ---- 自适应字号候选列表 ---- */ +static const font_size_t s_adaptive_w25q64[] = {FONT_32, FONT_24, FONT_16}; +static const font_size_t s_adaptive_mx25l[] = {FONT_32, FONT_24, FONT_20, FONT_16, FONT_14}; + +/* ---- 字库芯片配置实例 ---- */ +static const font_chip_config_t s_chip_configs[FONT_CHIP_CNT] = { + [FONT_CHIP_W25Q64] = { + .name = "W25Q64", + .regions = g_font_regions, + .region_count = sizeof(g_font_regions) / sizeof(g_font_regions[0]), + .adaptive_sizes = s_adaptive_w25q64, + .adaptive_count = sizeof(s_adaptive_w25q64) / sizeof(s_adaptive_w25q64[0]), + .ascii_raw_code = true, /* W25Q64: ASCII 用原始字符码 */ + .gbk_index_190 = false, /* W25Q64: GB2312 用94列序 (ch[0]-0xA1)*94+... */ + }, + [FONT_CHIP_MX25L256] = { + .name = "MX25L256", + .regions = g_font_regions_orig, + .region_count = sizeof(g_font_regions_orig) / sizeof(g_font_regions_orig[0]), + .adaptive_sizes = s_adaptive_mx25l, + .adaptive_count = sizeof(s_adaptive_mx25l) / sizeof(s_adaptive_mx25l[0]), + .ascii_raw_code = false, /* MX25L256: ASCII 用 ch-0x20 */ + .gbk_index_190 = true, /* MX25L256: GBK 用190列序 (ch[0]-0x81)*190+... */ + }, }; -/* ---- 内部: bytes_per_char ---- */ -static inline uint16_t _glyph_bytes(font_key_t k) +/* ---- 当前激活的配置 ---- */ +static const font_chip_config_t *s_active_config = &s_chip_configs[FONT_CHIP_W25Q64]; + +/* ---- 内部: 单字符在 Flash 上的打包字节数 ---- */ +static inline uint16_t _packed_glyph_bytes(font_size_t size, font_enc_t charset) { - uint8_t w = (k.charset == FONT_ENC_ASCII) ? k.size / 2 : k.size; - return (uint16_t)k.size * ((w + 7) / 8); + uint8_t w = (charset == FONT_ENC_ASCII) ? size / 2 : size; + return (uint16_t)size * ((w + 7) / 8); } -/* ---- 内部: 字符宽度 ---- */ -static inline uint8_t _glyph_width_px(font_key_t k) +/* ---- 内部: 字符像素宽度 ---- */ +static inline uint8_t _glyph_width_px(font_size_t size, font_enc_t charset) { - return (k.charset == FONT_ENC_ASCII) ? k.size / 2 : k.size; + return (charset == FONT_ENC_ASCII) ? size / 2 : size; } -/* ---- 内部: 查找字库单元起始偏移 ---- */ -static uint32_t _font_offset(const font_key_t *key) +/* ---- 内部: 查找字库区块(从当前激活配置中查找)---- */ +static const flash_region_t *_find_region(font_size_t size, font_enc_t charset, font_type_t type) { - uint32_t off = 0; - for (uint8_t i = 0; i < sizeof(g_font_lib) / sizeof(g_font_lib[0]); i++) { - if (memcmp(&g_font_lib[i].key, key, sizeof(font_key_t)) == 0) - return off; - off += g_font_lib[i].unit_size; + const flash_region_t *regions = (const flash_region_t *)s_active_config->regions; + for (uint8_t i = 0; i < s_active_config->region_count; i++) { + if (regions[i].size == size && + regions[i].charset == charset && + regions[i].type == type) + return ®ions[i]; } - return 0; + return ®ions[0]; /* fallback: 第一个条目 */ } -/* ---- 内部: 单个字符在 Flash 中的地址 ---- */ -static uint32_t _char_addr(const font_key_t *key, const uint8_t *ch) +/* ---- 内部: 单个字符在 Flash 中的绝对字节地址 ---- + * + * 与01Embedded完全一致的分步计算: + * FonfAddr = char_idx × bytes_per_char + * sec = FonfAddr / 4096 + * page = (FonfAddr % 4096) / 256 + * byte = (FonfAddr % 4096) % 256 + * readaddr = (base + sec + X) × 4096 + (page + Y) × 256 + byte + Z + */ +static uint32_t _flash_addr(font_size_t size, font_enc_t charset, font_type_t type, const uint8_t *ch) { - uint32_t base = _font_offset(key); - uint16_t bytes = _glyph_bytes(*key); + const flash_region_t *r = _find_region(size, charset, type); + uint16_t bytes = _packed_glyph_bytes(size, charset); + + /* 计算字符索引 — ascii_raw_code 控制是否减 0x20 */ + uint32_t char_idx; + if (charset == FONT_ENC_ASCII) { + if (s_active_config->ascii_raw_code) { + char_idx = ch[0]; /* W25Q64: 用原始字符码,如 'F'=0x46 */ + } else { + char_idx = ch[0] - 0x20U; /* MX25L256: ASCII 从 0x20 开始 */ + } + } else { + if (s_active_config->gbk_index_190) { + /* MX25L256: GBK 190列序 — 与 Orig 工程 _char_addr 一致 */ + char_idx = (uint32_t)(ch[0] - 0x81) * 190 + + (ch[1] >= 0x80 ? ch[1] - 0x41 : ch[1] - 0x40); + } else { + /* W25Q64: GB2312 94列序, ch[0]=区号+0xA0, ch[1]=位号+0xA0 */ + char_idx = (uint32_t)(ch[0] - 0xA1) * 94 + (ch[1] - 0xA1); + } + } - // ascii: 1字节, ch[0] = 字符码 - if (key->charset == FONT_ENC_ASCII) - return base + (ch[0] - 0x20) * bytes; + /* 按01Embedded公式分步计算 — 关键: X/Y/Z 作用在 sec/page/byte 级别 */ + uint32_t fonf = char_idx * bytes; + uint32_t sec = fonf / 4096; + uint32_t page = (fonf % 4096) / 256; + uint32_t byte = (fonf % 4096) % 256; - // GBK: 2字节, ch[0]=高字节, ch[1]=低字节 - uint32_t idx = (uint32_t)(ch[0] - 0x81) * 190 + (ch[1] >= 0x80 ? ch[1] - 0x41 : ch[1] - 0x40); - return base + idx * bytes; + return (uint32_t)(r->base + sec + r->sec_adj) * 4096 + (uint32_t)(page + r->page_adj) * 256 + byte + r->byte_adj; } /* ---- 判断两字节是否为合法 GBK 码 ---- */ @@ -124,242 +258,154 @@ static bool _is_gbk(uint8_t high, uint8_t low) return (high >= 0x81 && high <= 0xFE) && (low >= 0x40 && low <= 0xFE && low != 0x7F); } -/* ---- 注册的句柄 ---- */ -static dev_display_t *s_render_display; -static dev_storage_t *s_render_font; -static uint32_t s_persist_addr; - -/* ---- 模块自注册,依赖storage和display模块)---- */ -static void _render_init(void) -{ - s_render_display = dev_display_get(); - s_render_font = dev_w25qxx_get(); - s_persist_addr = dev_storage_capacity(s_render_font) - 4096 * 2; -} -sw_app_initcall(_render_init); - -/* ---- 渲染分支(各功能静态内联)---- */ - -static inline void _render_text(const render_cfg_t *cfg) +/* ---- 统计文本所需宽高(按现有文本内容、字号、换行符/自动换行计算)---- */ +static bool _measure_text_box(const uint8_t *text_buf, uint16_t text_len, font_size_t font_size, + uint16_t max_w, bool word_wrap, uint16_t *out_w, uint16_t *out_h) { - // 入口参数检查 - if (!cfg->text || !cfg->len || !cfg->font_size) - return; - if (!cfg->w || !cfg->h) - return; - - font_key_t gbk_key = {.size = cfg->font_size, .type = cfg->font_type, .charset = FONT_ENC_GBK}; - font_key_t asc_key = {.size = gbk_key.size, .type = gbk_key.type, .charset = FONT_ENC_ASCII}; - - uint16_t cur_x = cfg->x, cur_y = cfg->y; - static uint8_t font_buf[512]; - static char text_buf[256]; - uint16_t text_len; - - if (cfg->text_enc == FONT_ENC_UTF8) { - uint32_t out_len = sizeof(text_buf); - UTF8ToGBK(cfg->text, cfg->len, text_buf, &out_len); - text_len = (uint16_t)out_len; - } else { - uint16_t n = cfg->len < sizeof(text_buf) ? cfg->len : sizeof(text_buf); - memcpy(text_buf, cfg->text, n); - text_len = n; - } + if (!text_buf || !text_len || !font_size || !out_w || !out_h) + return false; - /* ---- 测量趟:记录每行宽度(用于逐行对齐) ---- */ - uint16_t line_widths[32]; - uint8_t line_count = 0; - uint16_t line_w = 0; - uint16_t line_h = gbk_key.size; - uint16_t char_pos = 0; + const uint16_t wrap_w = max_w ? max_w : UINT16_MAX; + uint16_t line_w = 0; + uint16_t max_line_w = 0; + uint16_t line_cnt = 1; + uint16_t char_pos = 0; while (char_pos < text_len) { if (text_buf[char_pos] == '\n') { - line_widths[line_count++] = line_w; - line_w = 0; + if (line_w > max_line_w) + max_line_w = line_w; + line_w = 0; + line_cnt++; + if (line_cnt > 64) + return false; char_pos++; continue; } uint8_t glyph_w; if (text_buf[char_pos] >= 0x20 && text_buf[char_pos] <= 0x7F) { - glyph_w = _glyph_width_px(asc_key); + glyph_w = _glyph_width_px(font_size, FONT_ENC_ASCII); char_pos += 1; } else if (char_pos + 1 < text_len && _is_gbk((uint8_t)text_buf[char_pos], (uint8_t)text_buf[char_pos + 1])) { - glyph_w = _glyph_width_px(gbk_key); + glyph_w = _glyph_width_px(font_size, FONT_ENC_GBK); char_pos += 2; } else { char_pos++; continue; } - if (line_w + glyph_w > cfg->w) { - if (cfg->style && cfg->style->word_wrap) { - line_widths[line_count++] = line_w; - line_w = glyph_w; - } - /* 不换行:超出部分截断,不计入宽度 */ + if (word_wrap && line_w + glyph_w > wrap_w) { + if (line_w > max_line_w) + max_line_w = line_w; + line_cnt++; + if (line_cnt > 64) + return false; + line_w = glyph_w; + if (line_w > wrap_w) + return false; } else { - line_w += glyph_w; + line_w = (uint16_t)(line_w + glyph_w); + if (!word_wrap && line_w > wrap_w) + return false; } } - line_widths[line_count++] = line_w; /* 最后一行 */ - - /* ---- 垂直对齐 ---- */ - uint16_t text_h = line_count * line_h; - if (cfg->style) { - if (cfg->style->v_align == ALIGN_CENTER && cfg->h > text_h) - cur_y += (cfg->h - text_h) / 2; - else if (cfg->style->v_align == ALIGN_RIGHT_DOWN && cfg->h > text_h) - cur_y += (cfg->h - text_h); - } - /* ---- 渲染趟:逐行独立水平对齐 ---- */ - uint8_t line_idx = 0; - uint16_t line_origin_x = cfg->x; - if (cfg->style) { - if (cfg->style->h_align == ALIGN_CENTER) - line_origin_x += (cfg->w - line_widths[line_idx]) / 2; - else if (cfg->style->h_align == ALIGN_RIGHT_DOWN) - line_origin_x += (cfg->w - line_widths[line_idx]); - } - cur_x = line_origin_x; - char_pos = 0; - - while (char_pos < text_len) { - if (text_buf[char_pos] == '\n') { - cur_y += line_h; - line_idx++; - line_origin_x = cfg->x; - if (cfg->style) { - if (cfg->style->h_align == ALIGN_CENTER) - line_origin_x += (cfg->w - line_widths[line_idx]) / 2; - else if (cfg->style->h_align == ALIGN_RIGHT_DOWN) - line_origin_x += (cfg->w - line_widths[line_idx]); - } - cur_x = line_origin_x; - char_pos++; - continue; - } - - if (text_buf[char_pos] >= 0x20 && text_buf[char_pos] <= 0x7F) { - uint8_t glyph_w = _glyph_width_px(asc_key); - - if (cur_x + glyph_w > cfg->w) { - if (cfg->style && cfg->style->word_wrap) { - cur_y += line_h; - line_idx++; - line_origin_x = cfg->x; - if (cfg->style) { - if (cfg->style->h_align == ALIGN_CENTER) - line_origin_x += (cfg->w - line_widths[line_idx]) / 2; - else if (cfg->style->h_align == ALIGN_RIGHT_DOWN) - line_origin_x += (cfg->w - line_widths[line_idx]); - } - cur_x = line_origin_x; - if (cur_y + line_h > cfg->h) return; - } else { - char_pos++; - continue; - } - } + if (line_w > max_line_w) + max_line_w = line_w; - uint8_t ch_byte = (uint8_t)text_buf[char_pos]; - uint32_t addr = _char_addr(&asc_key, &ch_byte); - dev_storage_read(s_render_font, addr, font_buf, _glyph_bytes(asc_key)); - dev_display_fill(s_render_display, cur_x, cur_y, glyph_w, asc_key.size, COLOR_BLACK); - dev_display_draw_bitmap(s_render_display, cur_x, cur_y, glyph_w, asc_key.size, font_buf, cfg->color); + *out_w = max_line_w; + *out_h = (uint16_t)(line_cnt * font_size); + return true; +} - cur_x += glyph_w; - char_pos++; - continue; +/* ---- 自适应字号选择:从大到小尝试,选择可完整放入区域的最大字号 ---- */ +static font_size_t _select_adaptive_font(const uint8_t *text_buf, uint16_t text_len, uint16_t w, uint16_t h, + bool word_wrap) +{ + /* 从当前激活配置中读取自适应字号候选列表 */ + const font_size_t *candidates = s_active_config->adaptive_sizes; + uint8_t count = s_active_config->adaptive_count; + + for (uint8_t i = 0; i < count; i++) { + font_size_t size = candidates[i]; + uint16_t need_w = 0, need_h = 0; + if (_measure_text_box(text_buf, text_len, size, w, word_wrap, &need_w, &need_h) && need_w <= w && need_h <= h) + return size; + } - } else if (char_pos + 1 < text_len && _is_gbk((uint8_t)text_buf[char_pos], (uint8_t)text_buf[char_pos + 1])) { - uint8_t glyph_w = _glyph_width_px(gbk_key); + return FONT_16; +} - if (cur_x + glyph_w > cfg->w) { - if (cfg->style && cfg->style->word_wrap) { - cur_y += line_h; - line_idx++; - line_origin_x = cfg->x; - if (cfg->style) { - if (cfg->style->h_align == ALIGN_CENTER) - line_origin_x += (cfg->w - line_widths[line_idx]) / 2; - else if (cfg->style->h_align == ALIGN_RIGHT_DOWN) - line_origin_x += (cfg->w - line_widths[line_idx]); - } - cur_x = line_origin_x; - if (cur_y + line_h > cfg->h) return; - } else { - char_pos += 2; - continue; - } - } +/* ---- 注册的句柄 ---- */ +static dev_display_t *s_render_display; +static dev_storage_t *s_render_font; +static uint32_t s_persist_addr; - uint8_t gbk_ch[2] = {(uint8_t)text_buf[char_pos], (uint8_t)text_buf[char_pos + 1]}; - uint32_t addr = _char_addr(&gbk_key, gbk_ch); - dev_storage_read(s_render_font, addr, font_buf, _glyph_bytes(gbk_key)); - dev_display_fill(s_render_display, cur_x, cur_y, glyph_w, gbk_key.size, COLOR_BLACK); - dev_display_draw_bitmap(s_render_display, cur_x, cur_y, glyph_w, gbk_key.size, font_buf, cfg->color); +/* ---- 内部: 读取 DIP2 拨码开关选择字库芯片 ---- + * + * DEV_KEY_DIP2 为 active_low (内部上拉): + * 开关 OFF (位置 0) → pin HIGH → get_state()=false → 逻辑 0 → MX25L256 + * 开关 ON (位置 1) → pin LOW → get_state()=true → 逻辑 1 → W25Q64 + */ +static void _dip_font_select(void) +{ + if (dev_key_get_state(DEV_KEY_DIP2)) + app_render_chip_select(FONT_CHIP_W25Q64); /* 逻辑 1 → W25Q64 */ + else + app_render_chip_select(FONT_CHIP_MX25L256); /* 逻辑 0 → MX25L256 */ +} - cur_x += glyph_w; - char_pos += 2; - continue; +/* ---- 模块自注册(依赖 storage / display / dev_key)---- */ +static void _render_init(void) +{ + s_render_display = dev_display_get(); + s_render_font = dev_w25qxx_get(); + s_persist_addr = dev_storage_capacity(s_render_font) - 4096 * 2; - } else { - char_pos++; - } - } + _dip_font_select(); } +sw_app_initcall(_render_init); -static inline void _render_bitmap(const render_cfg_t *cfg) -{ - if (!cfg->w || !cfg->h || !cfg->bitmap) return; +/* ---- 字库芯片切换 API ---- */ - dev_display_draw_bitmap(s_render_display, cfg->x, cfg->y, cfg->w, cfg->h, cfg->bitmap, cfg->color); +bool app_render_chip_select(font_chip_id_t id) +{ + if (id >= FONT_CHIP_CNT) + return false; + s_active_config = &s_chip_configs[id]; + return true; } -static inline void _render_fill(const render_cfg_t *cfg) +font_chip_id_t app_render_chip_current(void) { - uint16_t w = cfg->w, h = cfg->h; - if (!w || !h) { - w = s_render_display->screen_rows; - h = s_render_display->screen_cols; + for (uint8_t i = 0; i < FONT_CHIP_CNT; i++) { + if (s_active_config == &s_chip_configs[i]) + return (font_chip_id_t)i; } - dev_display_fill(s_render_display, cfg->x, cfg->y, w, h, cfg->color); + return FONT_CHIP_W25Q64; } -typedef struct { - uint8_t col_start; - uint8_t col_inc; -} msl_addr_part_t; - -msl_addr_part_t addr_part_table[3] = { - [0] = {96, 24}, - [1] = {48, 48}, - [2] = {0, 48}, -}; +/* ---- 内部: 渲染趟行宽读取守卫(line_idx 超出测量趟记录行数时钳制到末行,防读穿 line_widths) ---- */ +static uint16_t _line_width_get(const uint16_t *widths, uint8_t idx, uint8_t count) +{ + if (count == 0) + return 0; + if (idx >= count) + idx = (uint8_t)(count - 1); + return widths[idx]; +} -[[gnu::section(".ccmram")]] uint8_t msl_pixel_map[120 * 24] = {0}; -static dev_display_t msl_ins = { - .screen_rows = 120, - .screen_cols = 24, - .buffer_size = 120 * 24, - .pixel_map = msl_pixel_map, - .light_level = 1, -}; +/* ---- 渲染分支(各功能静态内联)---- */ -void msl_render_text(const render_cfg_t *cfg) +static inline void _render_text(const render_cfg_t *cfg) { // 入口参数检查 - if (!cfg->text || !cfg->len || !cfg->font_size) + if (!cfg->text || !cfg->len) return; if (!cfg->w || !cfg->h) return; - font_key_t gbk_key = {.size = cfg->font_size, .type = cfg->font_type, .charset = FONT_ENC_GBK}; - font_key_t asc_key = {.size = gbk_key.size, .type = gbk_key.type, .charset = FONT_ENC_ASCII}; - uint16_t cur_x = cfg->x, cur_y = cfg->y; static uint8_t font_buf[512]; static char text_buf[256]; @@ -375,15 +421,26 @@ void msl_render_text(const render_cfg_t *cfg) text_len = n; } + font_size_t font_size = cfg->font_size; + bool word_wrap = cfg->style && cfg->style->word_wrap; + if (font_size == FONT_SELF_ADAPT) { + font_size = _select_adaptive_font((const uint8_t *)text_buf, text_len, cfg->w, cfg->h, word_wrap); + } + if (!font_size) + font_size = FONT_16; + /* ---- 测量趟:记录每行宽度(用于逐行对齐) ---- */ uint16_t line_widths[32]; uint8_t line_count = 0; uint16_t line_w = 0; - uint16_t line_h = gbk_key.size; + uint16_t line_h = font_size; uint16_t char_pos = 0; while (char_pos < text_len) { if (text_buf[char_pos] == '\n') { + /* 防 ≥33 换行文本写穿 line_widths[32](栈溢出),与折行分支同策略:截断终止 */ + if (line_count >= (sizeof(line_widths) / sizeof(line_widths[0]))) + return; line_widths[line_count++] = line_w; line_w = 0; char_pos++; @@ -392,10 +449,10 @@ void msl_render_text(const render_cfg_t *cfg) uint8_t glyph_w; if (text_buf[char_pos] >= 0x20 && text_buf[char_pos] <= 0x7F) { - glyph_w = _glyph_width_px(asc_key); + glyph_w = _glyph_width_px(font_size, FONT_ENC_ASCII); char_pos += 1; } else if (char_pos + 1 < text_len && _is_gbk((uint8_t)text_buf[char_pos], (uint8_t)text_buf[char_pos + 1])) { - glyph_w = _glyph_width_px(gbk_key); + glyph_w = _glyph_width_px(font_size, FONT_ENC_GBK); char_pos += 2; } else { char_pos++; @@ -403,15 +460,23 @@ void msl_render_text(const render_cfg_t *cfg) } if (line_w + glyph_w > cfg->w) { - if (cfg->style && cfg->style->word_wrap) { + if (word_wrap) { + /* 防 line_widths[32] 越界写:先判后写(原判断滞后一次写入会写穿下标 32) */ + if (line_count >= (sizeof(line_widths) / sizeof(line_widths[0]))) + return; line_widths[line_count++] = line_w; - line_w = glyph_w; + line_w = glyph_w; + } else { + /* 不换行:超出部分截断,不计入宽度 */ + continue; } - /* 不换行:超出部分截断,不计入宽度 */ } else { line_w += glyph_w; } } + /* 最后一行:同样先判后写防 line_widths[32] 越界 */ + if (line_count >= (sizeof(line_widths) / sizeof(line_widths[0]))) + return; line_widths[line_count++] = line_w; /* 最后一行 */ /* ---- 垂直对齐 ---- */ @@ -428,9 +493,9 @@ void msl_render_text(const render_cfg_t *cfg) uint16_t line_origin_x = cfg->x; if (cfg->style) { if (cfg->style->h_align == ALIGN_CENTER) - line_origin_x += (cfg->w - line_widths[line_idx]) / 2; + line_origin_x += (cfg->w - _line_width_get(line_widths, line_idx, line_count)) / 2; else if (cfg->style->h_align == ALIGN_RIGHT_DOWN) - line_origin_x += (cfg->w - line_widths[line_idx]); + line_origin_x += (cfg->w - _line_width_get(line_widths, line_idx, line_count)); } cur_x = line_origin_x; char_pos = 0; @@ -442,9 +507,9 @@ void msl_render_text(const render_cfg_t *cfg) line_origin_x = cfg->x; if (cfg->style) { if (cfg->style->h_align == ALIGN_CENTER) - line_origin_x += (cfg->w - line_widths[line_idx]) / 2; + line_origin_x += (cfg->w - _line_width_get(line_widths, line_idx, line_count)) / 2; else if (cfg->style->h_align == ALIGN_RIGHT_DOWN) - line_origin_x += (cfg->w - line_widths[line_idx]); + line_origin_x += (cfg->w - _line_width_get(line_widths, line_idx, line_count)); } cur_x = line_origin_x; char_pos++; @@ -452,7 +517,7 @@ void msl_render_text(const render_cfg_t *cfg) } if (text_buf[char_pos] >= 0x20 && text_buf[char_pos] <= 0x7F) { - uint8_t glyph_w = _glyph_width_px(asc_key); + uint8_t glyph_w = _glyph_width_px(font_size, FONT_ENC_ASCII); if (cur_x + glyph_w > cfg->w) { if (cfg->style && cfg->style->word_wrap) { @@ -461,9 +526,9 @@ void msl_render_text(const render_cfg_t *cfg) line_origin_x = cfg->x; if (cfg->style) { if (cfg->style->h_align == ALIGN_CENTER) - line_origin_x += (cfg->w - line_widths[line_idx]) / 2; + line_origin_x += (cfg->w - _line_width_get(line_widths, line_idx, line_count)) / 2; else if (cfg->style->h_align == ALIGN_RIGHT_DOWN) - line_origin_x += (cfg->w - line_widths[line_idx]); + line_origin_x += (cfg->w - _line_width_get(line_widths, line_idx, line_count)); } cur_x = line_origin_x; if (cur_y + line_h > cfg->h) return; @@ -474,17 +539,18 @@ void msl_render_text(const render_cfg_t *cfg) } uint8_t ch_byte = (uint8_t)text_buf[char_pos]; - uint32_t addr = _char_addr(&asc_key, &ch_byte); - dev_storage_read(s_render_font, addr, font_buf, _glyph_bytes(asc_key)); - dev_display_fill(s_render_display, cur_x, cur_y, glyph_w, asc_key.size, COLOR_BLACK); - dev_display_draw_bitmap(&msl_ins, cur_x, cur_y, glyph_w, asc_key.size, font_buf, cfg->color); + uint32_t addr = _flash_addr(font_size, FONT_ENC_ASCII, cfg->font_type, &ch_byte); + uint16_t bytes = _packed_glyph_bytes(font_size, FONT_ENC_ASCII); + dev_storage_read(s_render_font, addr, font_buf, bytes); + dev_display_fill(s_render_display, cur_x, cur_y, glyph_w, font_size, COLOR_BLACK); + dev_display_draw_bitmap(s_render_display, cur_x, cur_y, glyph_w, font_size, font_buf, cfg->color); cur_x += glyph_w; char_pos++; continue; } else if (char_pos + 1 < text_len && _is_gbk((uint8_t)text_buf[char_pos], (uint8_t)text_buf[char_pos + 1])) { - uint8_t glyph_w = _glyph_width_px(gbk_key); + uint8_t glyph_w = _glyph_width_px(font_size, FONT_ENC_GBK); if (cur_x + glyph_w > cfg->w) { if (cfg->style && cfg->style->word_wrap) { @@ -493,9 +559,9 @@ void msl_render_text(const render_cfg_t *cfg) line_origin_x = cfg->x; if (cfg->style) { if (cfg->style->h_align == ALIGN_CENTER) - line_origin_x += (cfg->w - line_widths[line_idx]) / 2; + line_origin_x += (cfg->w - _line_width_get(line_widths, line_idx, line_count)) / 2; else if (cfg->style->h_align == ALIGN_RIGHT_DOWN) - line_origin_x += (cfg->w - line_widths[line_idx]); + line_origin_x += (cfg->w - _line_width_get(line_widths, line_idx, line_count)); } cur_x = line_origin_x; if (cur_y + line_h > cfg->h) return; @@ -506,10 +572,11 @@ void msl_render_text(const render_cfg_t *cfg) } uint8_t gbk_ch[2] = {(uint8_t)text_buf[char_pos], (uint8_t)text_buf[char_pos + 1]}; - uint32_t addr = _char_addr(&gbk_key, gbk_ch); - dev_storage_read(s_render_font, addr, font_buf, _glyph_bytes(gbk_key)); - dev_display_fill(s_render_display, cur_x, cur_y, glyph_w, gbk_key.size, COLOR_BLACK); - dev_display_draw_bitmap(&msl_ins, cur_x, cur_y, glyph_w, gbk_key.size, font_buf, cfg->color); + uint32_t addr = _flash_addr(font_size, FONT_ENC_GBK, cfg->font_type, gbk_ch); + uint16_t bytes = _packed_glyph_bytes(font_size, FONT_ENC_GBK); + dev_storage_read(s_render_font, addr, font_buf, bytes); + dev_display_fill(s_render_display, cur_x, cur_y, glyph_w, font_size, COLOR_BLACK); + dev_display_draw_bitmap(s_render_display, cur_x, cur_y, glyph_w, font_size, font_buf, cfg->color); cur_x += glyph_w; char_pos += 2; @@ -519,89 +586,23 @@ void msl_render_text(const render_cfg_t *cfg) char_pos++; } } - - for (int16_t i = 0; i < msl_ins.screen_cols; i++) { - memcpy(s_render_display->pixel_map, - msl_ins.pixel_map + i * msl_ins.screen_rows + addr_part_table[0].col_start, - addr_part_table[0].col_inc); - } - - render_cfg_t ctx1 = { - .type = RENDER_BITMAP, - .x = addr_part_table[1].col_start, - .y = 0, - .w = addr_part_table[1].col_inc, - .h = msl_ins.screen_cols, - .color = cfg->color, - }; - app_bitmap_sychro(&msl_ins, 1, ctx1, true); - - render_cfg_t ctx2 = { - .type = RENDER_BITMAP, - .x = addr_part_table[2].col_start, - .y = 0, - .w = addr_part_table[2].col_inc, - .h = msl_ins.screen_cols, - .color = cfg->color, - }; - app_bitmap_sychro(&msl_ins, 2, ctx2, true); } -void msl_render_bitmap(const render_cfg_t *cfg) +static inline void _render_bitmap(const render_cfg_t *cfg) { if (!cfg->w || !cfg->h || !cfg->bitmap) return; - dev_display_draw_bitmap(&msl_ins, cfg->x, cfg->y, cfg->w, cfg->h, cfg->bitmap, cfg->color); - - render_cfg_t ctx1 = { - .type = RENDER_BITMAP, - .x = addr_part_table[1].col_start, - .y = 0, - .w = addr_part_table[1].col_inc, - .h = msl_ins.screen_cols, - .color = cfg->color, - }; - app_bitmap_sychro(&msl_ins, 1, ctx1, true); - - render_cfg_t ctx2 = { - .type = RENDER_BITMAP, - .x = addr_part_table[2].col_start, - .y = 0, - .w = addr_part_table[2].col_inc, - .h = msl_ins.screen_cols, - .color = cfg->color, - }; - app_bitmap_sychro(&msl_ins, 2, ctx2, true); + dev_display_draw_bitmap(s_render_display, cfg->x, cfg->y, cfg->w, cfg->h, cfg->bitmap, cfg->color); } -void msl_render_fill(const render_cfg_t *cfg) +static inline void _render_fill(const render_cfg_t *cfg) { uint16_t w = cfg->w, h = cfg->h; if (!w || !h) { - w = msl_ins.screen_rows; - h = msl_ins.screen_cols; + w = s_render_display->screen_rows; + h = s_render_display->screen_cols; } - dev_display_fill(&msl_ins, cfg->x, cfg->y, w, h, cfg->color); - - render_cfg_t ctx1 = { - .type = RENDER_BITMAP, - .x = addr_part_table[1].col_start, - .y = 0, - .w = addr_part_table[1].col_inc, - .h = msl_ins.screen_cols, - .color = cfg->color, - }; - app_bitmap_sychro(&msl_ins, 1, ctx1, true); - - render_cfg_t ctx2 = { - .type = RENDER_BITMAP, - .x = addr_part_table[2].col_start, - .y = 0, - .w = addr_part_table[2].col_inc, - .h = msl_ins.screen_cols, - .color = cfg->color, - }; - app_bitmap_sychro(&msl_ins, 2, ctx2, true); + dev_display_fill(s_render_display, cfg->x, cfg->y, w, h, cfg->color); } /* ---- 渲染跳表 ---- */ @@ -618,6 +619,7 @@ void app_render(const render_cfg_t *cfg) if (!cfg || !s_render_display) return; if (cfg->type < sizeof(g_render_fn) / sizeof(g_render_fn[0]) && g_render_fn[cfg->type]) g_render_fn[cfg->type](cfg); + dev_display_commit_frame(s_render_display); } /* ================================================================ @@ -627,7 +629,7 @@ void app_render(const render_cfg_t *cfg) * 恢复阶段 fill(BLACK) + draw_bitmap(color) 重建 pixel_map。 * ================================================================ */ -#define PERSIST_BUF_SIZE (sizeof(render_persist_t) + 512) /* 容纳 64×64 像素位图 */ +#define PERSIST_BUF_SIZE (sizeof(render_persist_t) + 4096) /* 容纳 64×64 像素位图 */ void app_render_save(void) { @@ -664,15 +666,13 @@ void app_render_save(void) sizeof(render_persist_t) + bm_bytes); } -#include "app_msl.h" -#include "app_msl_cmd.h" -#include "bcc_utils.h" - bool app_render_restore(void) { dev_display_t *d = s_render_display; if (!d || !s_render_font) return false; + if (!s_render_font->ops) + return false; uint16_t rows = d->screen_rows; uint16_t cols = d->screen_cols; @@ -696,56 +696,3 @@ bool app_render_restore(void) dev_display_draw_bitmap(d, 0, 0, rows, cols, r->bitmap, (display_color_t)r->color); return true; } - -#define MSL_PAYLOAD_MAX (1032U) -/** - * @brief 多控制卡显示同步,通过msl协议实现多控制卡同步显示单屏幕 - * - * @param dsp 显示实例 - * @param addr 控制卡地址,0x00为广播,0xff保留 - * @param ctx 显示内容配置的上下文 - * @param pers 是否启用持久化 - */ -void app_bitmap_sychro(dev_display_t *dsp, uint8_t addr, render_cfg_t ctx, bool pers) -{ - uint16_t cols = ctx.h, rows = ctx.w; - uint8_t color = COLOR_BLACK; - uint16_t row_bytes = (rows + 7) / 8; - uint16_t bm_bytes = cols * row_bytes; - - // 构造同步显示帧 - static uint8_t send_buf[MSL_PAYLOAD_MAX]; - msl_frame_t *f = (msl_frame_t *)send_buf; - msl_bitmap_t *f_ctx = (msl_bitmap_t *)(f->data_bcc); - memset(send_buf, 0, sizeof(send_buf)); - - f->head[0] = 0x5a; - f->head[1] = 0x5a; - f->addr = addr; - f->cmd = MSL_CMD_BITMAP; - uint16_t data_len = sizeof(msl_bitmap_t) + bm_bytes; - f->length[0] = (data_len >> 8) & 0xff; - f->length[1] = data_len & 0xff; - - f_ctx->width = ctx.w; - f_ctx->high = ctx.h; - f_ctx->color = ctx.color; - f_ctx->pers = pers; - - // 位图数据填充 - for (uint16_t y = ctx.y; y < cols; y++) { - for (uint16_t x = ctx.x; x < rows; x++) { - uint8_t px = dsp->pixel_map[y * rows + x]; - if (px != COLOR_BLACK) { - f_ctx->bitmap[y * row_bytes + x / 8] |= (uint8_t)(0x80 >> (x % 8)); - if (color == COLOR_BLACK) color = px; - } - } - } - - // bcc校验 - ((uint8_t *)f_ctx)[data_len] = bcc_calcu(&(f->addr), data_len + sizeof(msl_frame_t) - 2); - - // 发送数据帧 - pl_uart_send(pl_uart_get_handle(PL_UART1), send_buf, data_len + sizeof(msl_frame_t) + 1, 50); -} diff --git a/Application/Src/app_test.c b/Application/Src/app_test.c index 430a84d..01d953c 100644 --- a/Application/Src/app_test.c +++ b/Application/Src/app_test.c @@ -6,6 +6,7 @@ #include "app_test.h" #include +#include #include "cmsis_os2.h" #include "dev_display.h" #include "dev_io_ctrl.h" @@ -17,19 +18,21 @@ void app_test_pixel_scan(void) { - dev_display_t *dsp = dev_display_p20_get(); + dev_display_t *dsp = dev_display_get(); for (;;) { for (int i = 0; i < (int)dsp->buffer_size; i++) { dev_display_set_pixel(dsp, - i % dsp->screen_rows, /* 逐行 x */ - i / dsp->screen_rows, /* 逐列 y */ + i % dsp->screen_rows, /* 逐列 x */ + i / dsp->screen_rows, /* 逐行 y */ COLOR_GREEN); - osDelay(250); + dev_display_commit_frame(dsp); + osDelay(50); dev_display_set_pixel(dsp, i % dsp->screen_rows, i / dsp->screen_rows, COLOR_BLACK); + dev_display_commit_frame(dsp); } } } @@ -47,14 +50,15 @@ void app_test_render_text(void) .w = dev_display_get()->screen_rows, .h = dev_display_get()->screen_cols, .style = &(render_style_t){ - .h_align = ALIGN_CENTER, - .v_align = ALIGN_CENTER, + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = false, }, - .color = COLOR_RED, - .text = "车道关闭", - .len = strlen("车道关闭"), - .font_size = FONT_32, - .font_type = FONT_ST, + .color = COLOR_YELLOW, + .text = "车道", + .len = strlen("车道"), + .font_size = FONT_SELF_ADAPT, + .font_type = FONT_HT, .text_enc = FONT_ENC_UTF8, }); } @@ -77,69 +81,180 @@ void app_test_io_output(void) /* ================================================================ * LED 灯序映射测试 * - * 绕过 pixel_map,直接写入 hub75_buff 逐位置点亮。 - * 用于确认陌生模组的物理 LED 排列顺序,推导 prepare 映射规则。 - * 按通道分组,每通道从第 0 像素起依次亮 200ms 红灯后熄灭。 + * 绕过 pixel_map,直接按 hub75_buff 线性顺序逐位置点亮。 + * 屏幕上像素的出现顺序即为 hub75_buff 的物理输出顺序, + * 用于确认陌生模组的 LED 排列规律,推导 prepare 映射规则。 + * 已点亮的像素不熄灭,逐个填充整个缓冲区。 + * + * PASS 标准: + * 1. 每次新点亮的像素为纯红色,无偏色/混色 + * 2. 像素按顺序逐个填充,无跳过或乱序 + * + * 通过后可确保: + * - _1_260_scan() 时钟脉冲和数据输出时序正确 + * - g_bsrr[color] 查表输出 R/G/B 颜色正确 + * - pl_hub75_ShiftRegister_set_row() 行选通正确 + * - HUB75 物理连线(排线、通道引脚)正确 + * + * 无法确保: + * - _1_260_prepare() 的像素重排逻辑(本测试绕过 prepare) + * - pixel_map 坐标映射 * ================================================================ */ void app_test_led_mapping(void) { - // dev_display_t *dsp = dev_display_p20_get(); - - // memset(dsp->hub75_buff, 0, dsp->buffer_size); - // dsp->dirty = false; - - // for (uint8_t ch = 0; ch < dsp->total_channels; ch++) { - // for (uint16_t px = 0; px < dsp->channel_pixels; px++) { - // // uint16_t pos = px + ch * dsp->channel_pixels; /* hub75_buff 线性位置 */ - // uint16_t pos = px; - - // /* 点亮当前像素(红色醒目) */ - // dsp->hub75_buff[pos] = COLOR_RED; - // osDelay(1000); - // } - /* 通道切换停顿,方便标记 */ - // osDelay(500); - // } - int16_t max_point = 24 * 8; - HUB75_A = 0; - HUB75_B = 0; - HUB75_C = 0; - HUB75_D = 0; - HUB75_LAT = 0; - HUB75_CLK = 0; - HUB75_OE = 1; - - // for (int16_t px = 0; px < max_point; px++) { + dev_display_t *dsp = dev_display_get(); + if (!dsp) return; + + memset(dsp->hub75_buff, 0, dsp->buffer_size); + dsp->dirty = false; + + for (uint16_t pos = 0; pos < dsp->buffer_size; pos++) { + dsp->hub75_buff[pos] = COLOR_RED; + osDelay(100); + } + + for (;;); +} + +/* ================================================================ + * 扫描行顺序测试 + * + * 逐行点亮水平线,用于确认 1/8 扫描模组的行地址映射顺序。 + * 每条线显示 1 秒后切换到下一行。 + * ================================================================ */ + +void app_test_scan_line_order(void) +{ + dev_display_t *dsp = dev_display_get(); + if (!dsp) return; + + for (uint8_t line = 0; line < 8; line++) { + /* 清屏 */ + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_BLACK); + dev_display_commit_frame(dsp); + osDelay(100); + + /* 在第 line 行画一条水平红线 */ + for (uint16_t x = 0; x < dsp->screen_rows; x++) { + dev_display_set_pixel(dsp, x, line, COLOR_RED); + } + dev_display_commit_frame(dsp); + + /* 保持显示 1 秒 */ + osDelay(1000); + } + + /* 测试完成后清屏 */ + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_BLACK); + dev_display_commit_frame(dsp); +} + +/* ================================================================ + * 对角线测试 + * + * 画一条从左上到右下的对角线,用于验证整体坐标映射是否正确。 + * ================================================================ */ + +void app_test_diagonal(void) +{ + dev_display_t *dsp = dev_display_get(); + if (!dsp) return; + + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_BLACK); + dev_display_commit_frame(dsp); + osDelay(100); + + uint16_t size = (dsp->screen_rows < dsp->screen_cols) ? dsp->screen_rows : dsp->screen_cols; + for (uint16_t i = 0; i < size; i++) { + dev_display_set_pixel(dsp, i, i, COLOR_GREEN); + } + dev_display_commit_frame(dsp); + + osDelay(3000); + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_BLACK); + dev_display_commit_frame(dsp); +} + +/* ================================================================ + * 斜扫测试 + * ================================================================ */ + +volatile uint32_t g_app_test_oblique_frame_count; + +void app_test_oblique_scan_frame(dev_display_t *dsp, int16_t phase) +{ + if (!dsp) + return; + + const int16_t first_line = -(int16_t)dsp->screen_cols; + const int16_t last_line = (int16_t)dsp->screen_rows + (int16_t)dsp->screen_cols - 2; + const uint16_t line_count = + (uint16_t)((last_line - first_line) / APP_TEST_OBLIQUE_LINE_SPACING) + 1; + + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_BLACK); + + for (uint16_t n = 0; n < line_count; n++) { + int32_t diagonal = (int32_t)first_line + phase + + (int32_t)n * APP_TEST_OBLIQUE_LINE_SPACING; + + for (uint16_t y = 0; y < dsp->screen_cols; y++) { + int32_t x = diagonal - (int32_t)y; + if (x >= 0 && x < (int32_t)dsp->screen_rows) + dev_display_set_pixel(dsp, (uint16_t)x, y, COLOR_WHITE); + } + } + + g_app_test_oblique_frame_count++; + dev_display_commit_frame(dsp); +} + +void app_test_oblique_scan(void) +{ + dev_display_t *dsp = dev_display_get(); + if (!dsp) return; + for (;;) { - HUB75_R1_GPIO_Port->BSRR |= HUB75_R1_Pin << 16; - for (int16_t px = 0; px < max_point; px++) { - HUB75_CLK = 1; - for (uint8_t i = 0; i < 8; i++) - __NOP(); - HUB75_CLK = 0; - for (uint8_t i = 0; i < 8; i++) - __NOP(); + for (int16_t phase = 0; phase < (int16_t)APP_TEST_OBLIQUE_LINE_SPACING; phase++) { + app_test_oblique_scan_frame(dsp, phase); + osDelay(APP_TEST_OBLIQUE_STEP_DELAY_MS); } + } +} - HUB75_R1_GPIO_Port->BSRR |= HUB75_R1_Pin; - for (int16_t px = 0; px < max_point; px++) { - HUB75_CLK = 1; - for (uint8_t i = 0; i < 8; i++) - __NOP(); - HUB75_CLK = 0; - for (uint8_t i = 0; i < 8; i++) - __NOP(); - HUB75_LAT = 1; - for (uint8_t i = 0; i < 8; i++) - __NOP(); - HUB75_LAT = 0; +/* ================================================================ + * prepare 映射测试 + * + * 通过 set_pixel 按屏幕坐标(左上→右下)逐像素点亮, + * 走完 set_pixel → prepare → scan 全链路。 + * 观察像素是否从模组左上角依次填充整个屏幕。 + * + * PASS 标准: + * 1. 像素从左上角 (0,0) 开始,逐列→逐行填充整个屏幕 + * 2. 每次新点亮的像素为纯红色,无偏色/混色 + * 3. 已点亮的像素不熄灭,逐个填充 + * + * 通过后可确保: + * - _1_260_prepare() 像素重排逻辑正确 + * + * 无法确保: + * - scan 输出时序、行选通、物理连线(由 app_test_led_mapping 验证) + * ================================================================ */ - osDelay(50); +void app_test_prepare_mapping(void) +{ + dev_display_t *dsp = dev_display_get(); + if (!dsp) return; + + for (uint16_t y = 0; y < dsp->screen_cols; y++) { + for (uint16_t x = 0; x < dsp->screen_rows; x++) { + dev_display_set_pixel(dsp, x, y, COLOR_RED); + dev_display_commit_frame(dsp); + osDelay(100); } } - for (;;); // 暂停运行 + for (;;); } /* ================================================================ @@ -148,8 +263,12 @@ void app_test_led_mapping(void) void app_test_run(void) { - // app_test_led_mapping(); /* 新模组灯序确认时启用 */ - app_test_pixel_scan(); - // app_test_render_text(); - // app_test_io_output(); + app_test_oblique_scan(); + // app_test_led_mapping(); /* 直接写 hub75_buff,按 hub75_buff线形点亮LED ,测试物理 LED 灯序映射 */ + // app_test_prepare_mapping(); /* 逐行点亮像素,测试 prepare 映射是否正确 */ + // app_test_pixel_scan(); /* 逐行素点亮再熄灭,测试屏幕所有像素点是否正常 */ + // app_test_render_text(); /* 渲染文字"车道关闭",测试字库读取和文字渲染功能 */ + // app_test_scan_line_order(); /* 逐行点亮水平线,测试 1/8 扫描的行地址映射顺序 */ + // app_test_diagonal(); /* 画对角线,测试整体坐标映射是否正确(直线=正确) */ + // app_test_io_output(); /* 依次点亮车道灯和黄闪灯各 1 秒,测试 IO 输出功能 */ } diff --git a/Application/Src/app_uart_baud.c b/Application/Src/app_uart_baud.c new file mode 100644 index 0000000..02b54bd --- /dev/null +++ b/Application/Src/app_uart_baud.c @@ -0,0 +1,57 @@ +/** + * @file app_uart_baud.c + * @brief DIP1 波特率选择与运行态波特率切换(RS232 + RS485 同步) + * + * 初始化时序(调查结论): + * main.c hw initcall → pl_uart_init(hw_pl lvl1,MX_USARTx_UART_Init @9600) + * → dev_key_init(hw_dev lvl2,DIP 表就绪) + * → RTOS → init_task → sw_board_init(sw initcall:本模块 sw_app lvl3) + * → app_rs485_start / app_rs232_start(此时才 pl_uart_start_rx 挂 DMA RX) + * 因此本模块在 sw 阶段读取 dev_key_get_state(DEV_KEY_DIP1) 并重配两路波特率, + * 早于通道任务启动,DMA RX 未挂,切换安全。运行态切换由 pl_uart_set_baud 处理 DMA 重挂。 + */ + +#include "app_uart_baud.h" + +#include "initcall.h" +#include "dev_key.h" +#include "pl_uart.h" + +static uint32_t s_app_uart_baud = APP_UART_BAUD_OFF; /**< 当前生效波特率 */ + +uint32_t app_uart_baud_get(void) +{ + return s_app_uart_baud; +} + +int32_t app_uart_baud_apply(uint32_t baud) +{ + if (baud != APP_UART_BAUD_OFF && baud != APP_UART_BAUD_ON) + return -1; + + pl_uart_handle_t h_rs485 = pl_uart_get_handle(PL_UART1); + pl_uart_handle_t h_rs232 = pl_uart_get_handle(PL_UART3); + if (!h_rs485 || !h_rs232) + return -1; + + /* RS485 与 RS232 同步切换;任一路失败即中止(避免两路口波特率不一致) */ + if (pl_uart_set_baud(h_rs485, baud) != 0) + return -1; + if (pl_uart_set_baud(h_rs232, baud) != 0) + return -1; + + s_app_uart_baud = baud; + return 0; +} + +/** + * @brief 上电波特率初始化:读 DIP1 并应用到 RS232 + RS485。 + * @note DIP1(PE7,active_low):true=ON=115200,false=OFF=9600。 + * DIP2(PE8)已被 app_render 字库芯片选择占用,不得改动。 + */ +static void app_uart_baud_init(void) +{ + uint32_t baud = dev_key_get_state(DEV_KEY_DIP1) ? APP_UART_BAUD_ON : APP_UART_BAUD_OFF; + (void)app_uart_baud_apply(baud); +} +sw_app_initcall(app_uart_baud_init); \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 9baff7c..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,771 +0,0 @@ -# CLAUDE.md - -此文件为 Claude Code (claude.ai/code) 在此仓库中工作时提供指导。 - -## 构建系统 - -STM32F407ZGTx 嵌入式项目,工具链 `arm-none-eabi-gcc`,C23 标准。 - -- **构建**:`make -j8`(根目录 Makefile,`TOOLCHAIN=gcc|clang`,`CONFIG=Debug|Release`) - - GCC Debug:`make -j8`(默认,`-Og -g`) - - GCC Release:`make -j8 CONFIG=Release` - - Clang Debug:`make -j8 TOOLCHAIN=clang` - - Clang Release:`make -j8 TOOLCHAIN=clang CONFIG=Release` -- **编译数据库**:`bear --output build/Debug/compile_commands.json -- make -B -j8`(`-B` 强制全量重编译) -- **烧录**:`openocd -f ./Compiler/stm32f407zg.cfg -c "init; halt; program ./build/Debug/Project_STD.hex verify reset exit"` -- **整片擦除**:`openocd -f ./Compiler/stm32f407zg.cfg -c "init; halt; stm32f4x unlock 0; stm32f4x mass_erase 0; shutdown"` -- **清理**:`make clean`(仅删除 `build/$(CONFIG)` 目录) - -### 构建产物 - -| 产物 | 格式 | 用途 | -|---|---|---| -| `Project_STD.elf` | ELF(带调试符号) | 链接器输出,含完整符号表,调试器(probe-rs / cortex-debug)直接使用。`arm-none-eabi-size` 输出各段大小 | -| `Project_STD.hex` | Intel HEX(ASCII 文本) | 烧录文件。OpenOCD 通过 `program ... verify` 写入并回读比对。文本格式,每行含地址+数据+校验和 | -| `Project_STD.bin` | 纯二进制 | 裸二进制映像,无地址信息。适用于 IAP bootloader 直接写入 Flash | - -**链接优化**:`--gc-sections` + `-ffunction-sections -fdata-sections`,未引用的函数/数据自动丢弃。`--specs=nano.specs` 链接精简版 newlib。`-u _printf_float` 强制链接 printf 浮点格式化支持(newlib-nano 默认关闭)。`-fshort-enums` 按最小宽度打包枚举类型。 - -## 硬件架构 - -**MCU**:STM32F407ZGTx(Cortex-M4F,168MHz,FPU,1024KB Flash,128KB SRAM + 64KB CCMRAM) - -**时钟**:HSE 8MHz → PLL (M=4, N=168, P=2, Q=7) → SYSCLK 168MHz / APB1 42MHz / APB2 84MHz - -**外设**:ADC1、SPI1(W25Qxx)、TIM2/3/4/7、USART1/USART3/USART6、DMA1/2、CRC、IWDG、RTC、Ethernet MAC + DP83848 PHY - -## 内存布局 - -链接脚本:`Compiler/STM32F407XX_FLASH.ld` - -``` -Flash (1024KB, 起始 0x08000000) -├─ Sector 0: 0x08000000 16KB 中断向量表 (.isr_vector) -├─ Sector 1: 0x08004000 16KB IAP 系统配置 (dev_flash_iap) -├─ Sector 2: 0x08008000 16KB Recovery 固件 (备用) -├─ Sector 3: 0x0800C000 16KB (预留) -├─ Sector 4: 0x08010000 64KB } -├─ Sector 5: 0x08020000 128KB } 应用程序 (.text/.rodata) -├─ Sector 6: 0x08040000 128KB } 应用程序 (.text/.rodata) — IAP 适配后主固件入口 -├─ Sector 7: 0x08060000 128KB } -├─ Sector 8: 0x08080000 128KB (预留扩展) -├─ Sector 9: 0x080A0000 128KB (预留扩展) -├─ Sector10: 0x080C0000 128KB (预留扩展) -└─ Sector11: 0x080E0000 128KB (已释放 — LDI 配置已迁移至 W25Qxx) - -SRAM (128KB, 0x20000000) -├─ .data 已初始化全局变量 -├─ .bss 零初始化全局变量 -├─ FreeRTOS heap 32KB (heap_4.c) -├─ LwIP mem heap 12KB -├─ 环形缓冲区池 4×2KB = 8KB -├─ UART DMA 缓冲 3×2KB = 6KB -├─ 任务栈 见任务栈表 -└─ _user_heap_stack 512B heap + 2KB MSP 栈 (链接脚本预留) - -CCMRAM (64KB, 0x10000000, NOLOAD 段) -├─ pixel_map[256] 256B 显示帧缓冲 -├─ hub75_buff[256] 256B 扫描输出缓冲 -├─ g_bsrr[4][8] ~384B BSRR 查表 -└─ W25Qxx DMA 读缓冲 ~4KB (静态, .ccmram section) -``` - -**CCMRAM 特性**:零等待状态,与内核直连不经过总线矩阵。NOLOAD 段在 `startup.c` 中整体循环清零(行为等同 .bss)。仅数据可用,不可执行代码。 - -## 闪存扇区职能地图 - -> **当前状态**:固件入口为 `0x08000000`,无 IAP bootloader 运行。`dev_flash_iap` 模块和 Recovery 区域已预留,待 IAP 适配后启用。 -> **IAP 适配后**:bootloader 占 Sector 0~3,主固件从 `0x08040000` 启动(需修改链接脚本 ORIGIN、`VECT_TAB_OFFSET`、`SCB->VTOR`)。 - -| 扇区 | 地址 | 大小 | 内容 | 读写方式 | -|---|---|---|---|---| -| 0 | `0x08000000` | 16KB | ISR 向量表 + 固件头 | 烧录时写入 | -| 1 | `0x08004000` | 16KB | IAP 系统配置 (magic + FWInfo + NetConfig + CRC32) | `dev_flash_iap` 内存映射读 + 擦写 | -| 2 | `0x08008000` | 16KB | Recovery 固件 | IAP 升级时写入 | -| 3-10 | `0x0800C000`~`0x080C0000` | 640KB | 主固件 (.text/.rodata/.initcall) | 烧录时写入 | -| 11 | `0x080E0000` | 128KB | (已释放 — LDI 配置已迁移至 W25Qxx 最后一个 4KB 扇区) | — | - -**外部 Flash**(W25Qxx,SPI1,~8MB):字库数据(5 字号×4 字型×2 编码的 40 个三元组) + LDI 设备配置(最后一个 4KB 扇区),通过 `dev_storage_read` 接口访问。`dev_flash_ldi` 通过 `dev_w25qxx_get()` 获取存储句柄。 - -## 软件分层架构 - -依赖方向严格单向(上层可见下层,反之不可): - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ Application/ (app_*) │ -│ 业务逻辑:boot编排、协议处理(IAP/LDI/AH_MQTT)、渲染引擎、 │ -│ 传感器管理、网络通道(UDP/TCP/MQTT/RS232/RS485任务) │ -│ │ -│ 依赖:Device/ + Kernel/ + Platform/ (不直接碰 HAL 地址) │ -├──────────────────────────────────────────────────────────────────┤ -│ Device/ (dev_*) │ -│ 设备抽象:OCP 虚表封装硬件差异 │ -│ Display/ Storage/ Comm/ Network/ IO/ Config/ │ -│ │ -│ 依赖:Platform/ + Kernel/ (通过 pl_* 接口操作硬件) │ -├──────────────────────────────────────────────────────────────────┤ -│ Kernel/ │ -│ 纯软件工具:ring_buffer、initcall、dispatch_types、text_cvt、 │ -│ bit_utils、crc_utils │ -│ │ -│ 依赖:无(零硬件依赖,仅 stdint/stddef/stdbool) │ -├──────────────────────────────────────────────────────────────────┤ -│ Platform/ (pl_*) │ -│ HAL 薄封装:不透明句柄 (void*),HAL 类型对外不可见 │ -│ pl_spi pl_tim pl_uart pl_gpio pl_hub75 pl_eth pl_net │ -│ pl_flash pl_crc pl_rtc pl_iwdg pl_dma pl_dwt pl_sys │ -│ │ -│ 依赖:Core/ + HAL 库 (仅 .c 文件 include Core 头文件) │ -├──────────────────────────────────────────────────────────────────┤ -│ Core/ │ -│ STM32CubeMX 生成:MX_xxx_Init()、ISR 声明、HAL 配置 │ -│ main.c system_stm32f4xx.c stm32f4xx_it.c FreeRTOSConfig.h │ -│ │ -│ 依赖:HAL 库 + CMSIS │ -├──────────────────────────────────────────────────────────────────┤ -│ Compiler/ │ -│ startup.c (C 语言 Reset_Handler + 自定义向量表) │ -│ STM32F407XX_FLASH.ld (链接脚本,含 initcall 段定义) │ -└──────────────────────────────────────────────────────────────────┘ -``` - -### Platform 层内部依赖 - -``` -pl_sys (SystemClock_Config, delay, reset) - ├─ pl_dma (DMA 流初始化,需在 SPI/UART 之前) - ├─ pl_gpio (GPIO 时钟,几乎所有模块依赖) - ├─ pl_tim (TIM2/3/4/7) - ├─ pl_spi (SPI1,依赖 DMA) - ├─ pl_uart (USART1/3/6,依赖 DMA) - ├─ pl_crc (硬件 CRC) - ├─ pl_iwdg (独立看门狗) - ├─ pl_rtc (RTC 备份寄存器) - ├─ pl_adc (ADC1) - ├─ pl_exti (外部中断) - ├─ pl_hub75 (HUB75 GPIO 位拆裂) - ├─ pl_flash (内部 Flash 编程) - ├─ pl_dwt (DWT 周期计数器) - ├─ pl_rtt (SEGGER RTT) - ├─ pl_eth (ETH MAC + MDIO) - └─ pl_net (LwIP 初始化,依赖 pl_eth) -``` - -各模块用不透明句柄(`typedef void *pl_xxx_handle_t`),Device 层只传句柄、不感知 HAL 类型。 - -## 启动流程与 initcall - -### 完整启动序列 - -``` -硬件上电 → Reset_Handler (Compiler/startup.c) - ├─ .data 拷贝 (Flash→SRAM), .bss 清零, CCMRAM 清零 - ├─ SystemInit() — FPU 使能 (VTOR 默认 0x08000000,IAP 适配后设为 0x08040000) - └─ main() (Core/Src/main.c) - ├─ HAL_Init() ← HAL 库基础 - ├─ SystemClock_Config() ← 168MHz - ├─ initcall_run(__hw_initcall) ← RTOS 前硬件初始化 - └─ app_boot() ← RTOS 编排 - ├─ osKernelInitialize() - ├─ osThreadNew(init_task, prio=High, stack=512×4) - └─ osKernelStart() - ├─ [FreeRTOS 接管: SysTick→PendSV 任务切换] - └─ init_task: - ├─ dev_eth_start() ← LwIP + netif - ├─ sw_board_init() ← initcall_run(__sw_initcall) - ├─ osThreadNew(half_sec_task) - └─ osThreadExit() ← 自我销毁 -``` - -### initcall 初始化顺序表 - -**hw_initcall (RTOS 前,main() 中执行)**: - -| 顺序 | 层级 | 函数 | 文件 | 职责 | -|---|---|---|---|---| -| 0-pre | — | (空) | — | 预留 | -| 1-pl | pl | `pl_dma_init` | `pl_dma.c` | DMA 流初始化 | -| 1-pl | pl | `pl_gpio_init` | `pl_gpio.c` | GPIO 时钟使能 | -| 1-pl | pl | `pl_spi_init` | `pl_spi.c` | SPI1 初始化 | -| 1-pl | pl | `pl_tim_init` | `pl_tim.c` | TIM2/3/4 初始化 | -| 1-pl | pl | `pl_uart_init` | `pl_uart.c` | USART1/3/6 初始化 | -| 1-pl | pl | `pl_crc_init` | `pl_crc.c` | 硬件 CRC | -| 1-pl | pl | `pl_iwdg_init` | `pl_iwdg.c` | 看门狗 | -| 1-pl | pl | `pl_rtc_init` | `pl_rtc.c` | RTC | -| 1-pl | pl | `pl_adc_init` | `pl_adc.c` | ADC1 | -| 1-pl | pl | `pl_exti_init` | `pl_exti.c` | 外部中断 | -| 1-pl | pl | `pl_hub75_init` | `pl_hub75.c` | HUB75 GPIO | -| 1-pl | pl | `pl_flash_init` | `pl_flash.c` | Flash 解锁(空操作) | -| 1-pl | pl | `pl_rtt_init` | `pl_rtt.c` | SEGGER RTT | -| 2-dev | dev | `dev_display_init` | `dev_display.c` | HUB75 引脚 + DBG 冻结 | -| 2-dev | dev | `dev_display_p20_init` | `dev_display_p20.c` | BSRR 查表 + ops 绑定 | -| 2-dev | dev | `dev_w25qxx_init` | `dev_w25qxx.c` | SPI Flash JEDEC 识别 | -| 2-dev | dev | `dev_eth_init` | `dev_eth.c` | ETH MAC + DP83848 PHY | -| 2-dev | dev | `dev_rs485_init` | `dev_rs485.c` | RS485 通道 hw init | -| 2-dev | dev | `dev_rs232_init` | `dev_rs232.c` | RS232 通道 hw init | -| 2-dev | dev | `_dev_flash_iap_storage_init` | `dev_flash_iap.c` | 内部 Flash ops 绑定 (Sector 1) | -| 2-dev | dev | `dev_io_ctrl_init` | `dev_io_ctrl.c` | 车道灯/闪光灯 GPIO 初始化 | -| 2-dev | dev | `dev_key_init` | `dev_key.c` | EXTI 中断回调注册 (SW1~SW3, TEST) | -| 3-post | — | (空) | — | 预留 | - -**sw_initcall (RTOS 后,init_task → sw_board_init 中执行)**: - -| 顺序 | 层级 | 函数 | 文件 | 职责 | -|---|---|---|---|---| -| 0-pre | — | (空) | — | 预留 | -| 1-pl | pl | `pl_exti_sw_init` | `pl_exti.c` | EXTI 中断使能 | -| 2-dev | dev | `dev_display_start` | `dev_display.c` | 创建 scan_task + 启动 TIM3/4 | -| 2-dev | dev | `dev_key_sw_init` | `dev_key.c` | 创建 TEST 按键信号量 | -| 2-dev | dev | `_dev_flash_ldi_storage_init` | `dev_flash_ldi.c` | LDI 配置 W25Qxx 地址计算 | -| 3-app | app | `app_dispatch_init` | `app_dispatch.c` | 创建 ch_queue + frame_dispatch_task | -| 3-app | app | `_render_init` | `app_render.c` | 绑定 display + storage 句柄 | -| 3-app | app | `app_key_init` | `app_key.c` | 创建 key_poll_task (20ms) | -| 3-app | app | `app_light_sensor_init` | `app_light_sensor.c` | 创建光传感器自动调节任务 | -| 4-post | — | (空) | — | 预留 | - -### initcall 实现原理 - -链接脚本中定义两个特殊段: -``` -.hw_initcall : { KEEP(*(SORT(.hw_initcall.0))) ... KEEP(*(SORT(.hw_initcall.3))) } -.sw_initcall : { KEEP(*(SORT(.sw_initcall.0))) ... KEEP(*(SORT(.sw_initcall.4))) } -``` - -每个 `*_initcall(fn)` 宏生成一个 `initcall_entry_t` 常量放入对应 section。`initcall_run(start, end)` 顺序遍历调用。同层内按函数名字母序排列(`SORT()` 保证确定性)。 - -## 中断体系 - -### 中断源与分派路径 - -``` -外设硬件中断 -├─ TIM3_IRQHandler (pl_tim.c) -│ └─ HAL_TIM_IRQHandler → HAL_TIM_PeriodElapsedCallback -│ └─ osEventFlagsSet(s_scan_evt) → 唤醒 scan_task -│ -├─ TIM4_IRQHandler (pl_tim.c) -│ └─ HAL_TIM_IRQHandler → HAL_TIM_PeriodElapsedCallback -│ └─ PWM 亮度:pl_hub75_oe_set(pwm_cnt >= light_level) -│ -├─ TIM7_IRQHandler (pl_tim.c) -│ └─ HAL_TIM_IRQHandler → HAL_TIM_PeriodElapsedCallback -│ └─ HAL_IncTick() (FreeRTOS 时基,1ms) -│ -├─ USART1_IRQHandler (pl_uart.c) -│ └─ HAL_UART_IRQHandler + uart_idle_handle -│ └─ DMA 空闲中断 → 计算接收长度 → rx_cb→ -│ osMessageQueuePut(rx_queue, &len) → 唤醒 uart_channel_task -│ -├─ USART3_IRQHandler / USART6_IRQHandler (同上) -│ -├─ SPI1_IRQHandler (pl_spi.c) -│ └─ HAL_SPI_IRQHandler → HAL_SPI_TxRxCpltCallback / HAL_SPI_RxCpltCallback -│ └─ rx_cplt_cb(s_ok=true) → dev_w25qxx DMA 完成通知 -│ -├─ DMA1_Stream1_IRQHandler (pl_uart.c, USART3 RX DMA) -├─ DMA2_Stream1_IRQHandler (pl_uart.c, USART6 RX DMA) -├─ DMA2_Stream2_IRQHandler (pl_uart.c, USART1 RX DMA) -│ -├─ ETH_IRQHandler (pl_eth.c, 见下) -│ -├─ HardFault_Handler (stm32f4xx_it.c) -│ └─ naked asm → hard_fault_handler_c → 现场保存 (R0-R3,R12,LR,PC,xPSR,CFSR,BFAR) -│ -├─ SysTick_Handler (FreeRTOS 内部, CMSIS-RTOS V2 封装) -├─ SVC_Handler → vPortSVCHandler (FreeRTOS 内核, 启动第一个任务) -└─ PendSV_Handler → xPortPendSVHandler (FreeRTOS 内核, 任务上下文切换) -``` - -**设计原则**: -- 所有外设 ISR 内聚在 Platform 层 .c 文件中,不分散在 `stm32f4xx_it.c` -- ISR 中只做最小操作(标记、通知),耗时逻辑全部在 RTOS 任务中完成 -- 中断优先级:`configMAX_SYSCALL_INTERRUPT_PRIORITY = 5`,FreeRTOS API 仅可在优先级 ≥5 的 ISR 中调用 - -### 中断→任务解耦的三级流水线 - -``` -[裸机 ISR] [RTOS 信号量/队列] [RTOS 任务] -───────────────────────────────────────────────────────────────── -TIM3 行同步 ──→ osEventFlagsSet ──→ scan_task (Realtime) -TIM4 PWM (ISR 内直接完成,不唤醒任务) -UART 空闲中断 ──→ osMessageQueuePut ──→ uart_channel_task - └─→ app_channel_dispatch - └─→ osMessageQueuePut(ch_queue) - ──→ frame_dispatch_task - └─→ osMessageQueuePut(frame_queue) - ──→ 协议处理任务 -SPI DMA 完成 ──→ volatile s_ok=true ──→ dev_w25qxx _read 轮询 (osDelay 轮询) -ETH 收包 ──→ osSemaphoreRelease ──→ ethernetif_input -ETH 链路状态 ──→ pl_net_link_listener ──→ UDP/TCP/MQTT 通道任务重建连接 -``` - -**关键模式**: -- **Event Flags**:`scan_task` 用 `osEventFlagsWait` 等待 TIM3 行同步触发。ISR 中使用 `osEventFlagsSet`(`configUSE_OS2_EVENTFLAGS_FROM_ISR=1` 使能 ISR 安全调用) -- **Message Queue**:`uart_channel_task`、`frame_dispatch_task`、协议处理任务逐级通过队列传递数据指针 -- **Semaphore**:UDP/TCP 通道用信号量协调连接/断开生命周期(链路断开→信号量释放→任务重建连接) -- **volatile 轮询**:W25Qxx SPI 半双工 DMA 读使用 `while(!s_ok) osDelay(1)` 轮询(历史原因:RTOS 未完全就绪时无法使用 event flags) - -## RTOS 配置 - -`Core/Inc/FreeRTOSConfig.h`,FreeRTOS v10.3.1 + CMSIS-RTOS V2 封装: - -| 参数 | 值 | 说明 | -|---|---|---| -| `configTICK_RATE_HZ` | 1000 | 1ms 时基(TIM7) | -| `configMAX_PRIORITIES` | 56 | 优先级范围 0-55 | -| `configTOTAL_HEAP_SIZE` | 32KB | heap_4 动态分配 | -| `configMINIMAL_STACK_SIZE` | 128 words (512B) | 最小任务栈 | -| `configCHECK_FOR_STACK_OVERFLOW` | 2 | 栈溢出检测(检查栈顶标记) | -| `configUSE_MUTEXES` | 1 | 互斥锁(含优先级继承) | -| `configUSE_COUNTING_SEMAPHORES` | 1 | 计数信号量 | -| `configUSE_OS2_EVENTFLAGS_FROM_ISR` | 1 | ISR 中可操作 EventFlags | -| `configGENERATE_RUN_TIME_STATS` | 1 | 运行时统计(时钟=DWT 周期计数器) | -| `configMAX_SYSCALL_INTERRUPT_PRIORITY` | 5 | 可调用 FreeRTOS API 的最高中断优先级 | -| `configKERNEL_INTERRUPT_PRIORITY` | 15 | 内核中断最低优先级 | - -## 任务清单与同步机制 - -### 任务栈大小设计考量 - -| 任务 | 栈 | 优先级 | 创建者 | 职责 | 栈设计依据 | -|---|---|---|---|---|---| -| `scan_task` | 512B | Realtime(最高) | `dev_display_start` | 行扫描输出 | 无深层调用、无局部大数组,仅 event wait + ops 调用 | -| `init_task` | 512×4=2KB | High | `app_boot` | 初始化后退出 | `dev_eth_start`→`sw_board_init`→渲染测试,含 printf 和栈数组(`font_buf[512]`、`text_buf[256]`),曾因溢触发 HardFault,从 256×4 扩容 | -| `frame_dispatch_task` | 256×4=1KB | Normal | `app_dispatch_init` | 帧分发引擎 | 核心调度循环,含 `frame_msg_t`(~1052B),无深层嵌套 | -| `iap_handle_task` | 512×4=2KB | Normal | `app_iap` | IAP 帧处理 | CRC32 校验 + cmd 分派,含 256B 临时缓冲 | -| `ldi_handle_task` | 512×4=2KB | Normal | `app_ldi` | LDI 帧处理 | 显示内容解析与更新 | -| `ldi_timer_task` | 512×4=2KB | Normal | `app_ldi` | LDI 定时任务 | 周期性状态上报 | -| `half_sec_task` | 128×4=512B | Low | `init_task` | 500ms 喂狗+LED | 最小合法栈,仅 GPIO+RTC API | -| 各网络通道任务 | 256×4=1KB | Normal | 各模块 | UDP/TCP 收发包 | LwIP netconn API 调用,不含大局部变量 | -| `ethernetif_input` | 256×4=1KB | — | `pl_net.c` | LwIP 收包线程 | LwIP 内部调用 | -| `ethernet_link_thread` | 256×4=1KB | — | `pl_net.c` | 链路状态监控 | 轮询 PHY 状态 | - -**栈大小设计原则**:CMSIS-RTOS V2 栈单位为 word(4 字节)。数字偏大: -- 512B(128 words)= CMSIS 最小栈,仅用 API 无局部数组 -- 1KB(256 words)= 标准栈,含少量局部变量 -- 2KB(512 words)= 大栈,含 `frame_msg_t`(~1052B)、CRC 计算、printf 等 - -### 任务间同步机制全景 - -``` -init_task ──────────────────────────────────────────────────────→ (osThreadExit 自我销毁) - ├─ dev_eth_start → pl_net_init → osThreadNew(ethernetif_input) - │ → osThreadNew(ethernet_link_thread) - └─ sw_board_init → app_dispatch_init → osThreadNew(frame_dispatch_task) - → dev_display_start → osThreadNew(scan_task) - -half_sec_task: osDelay(500) 循环 ──→ pl_iwdg_refresh / pl_gpio_write - -scan_task: - osEventFlagsWait(s_scan_evt) ←── TIM3 ISR: osEventFlagsSet - osKernelLock + NVIC_DisableIRQ(TIM4) (OE/LAT 原子窗口) - -frame_dispatch_task: - osMessageQueueGet(g_ch_queue) ←── uart_channel_task / udp_connect_task / ... - rb_lock(rb) → probe → rb_read → osMessageQueuePut(frame_queue[i]) - -协议处理任务 (iap/ldi/ah_mqtt): - osMessageQueueGet(frame_queue[i]) → 处理 → channel_send - -UART 通道任务 (uart_channel_task): - osMessageQueueGet(rx_queue, &len) ←── UART ISR 空闲中断 - app_channel_dispatch → rb_write → osMessageQueuePut(ch_queue) - -网络通道任务 (UDP/TCP): - osSemaphoreAcquire(sem) ←── pl_net_link_listener (链路断开) - netconn_recv → app_channel_dispatch → rb_write → osMessageQueuePut(ch_queue) -``` - -## OCP 虚表模式 - -项目中所有设备驱动和通道使用统一模式: - -```c -// 1. 基类第一个成员必须是 ops 指针 -struct dev_display { const dev_display_ops_t *ops; /* 通用参数 */ }; -struct dev_storage { const dev_storage_ops_t *ops; uint32_t capacity; }; -struct channel { uint8_t ch_id, state; const ch_ops_t *ops; }; - -// 2. 派生类将基类作为第一个成员,统一命名为 me -typedef struct { dev_display_t me; } dev_display_p20_t; -typedef struct { dev_storage_t me; uint32_t base_addr, sector; } dev_flash_int_t; -typedef struct { channel_t me; pl_uart_handle_t uart; ... } uart_channel_t; -typedef struct { dev_w25qxx_t me; pl_spi_handle_t spi; ... } dev_w25qxx_inner; // (注:w25qxx直接在结构内) - -// 3. container_of 向上转型(Kernel/Inc/dispatch_types.h) -#define container_of(ptr, type, member) ((type *)((char *)(ptr) - offsetof(type, member))) -``` - -四个核心虚表: - -| 虚表 | 定义位置 | 方法签名 | 实现者 | -|---|---|---|---| -| `dev_display_ops` | `dev_display.h` | `prepare(dev)` / `scan(dev,line)` / `set_row(row)` | `dev_display_p20.c` | -| `dev_storage_ops` | `dev_storage.h` | `init/read/write/erase/capacity(dev,...)` | `dev_w25qxx.c` / `dev_flash_int.c` | -| `ch_ops` | `dispatch_types.h` | `send(ch, data, len)` | `dev_uart_channel.c` / `app_udp.c` / TCP/MQTT | -| `proto_probe_fn_t` | `app_dispatch.h` | `probe(ch, rb, &total_len, &aux)` | `app_iap.c` / `app_ldi.c` | - -## Display 子系统 (`Device/Display/`) - -### 基类 (`dev_display.h/.c`) - -```c -struct dev_display { - const dev_display_ops_t *ops; - // 模组参数 - uint8_t module_rows, module_cols, channels_per_module; - uint8_t modules_per_row, modules_per_col, scan_lines; - // 派生参数(由模组参数计算) - uint16_t screen_rows, screen_cols; - uint8_t total_channels; - uint16_t channel_pixels, scan_line_pixels, buffer_size; - // CCMRAM 缓冲区(派生实例静态分配) - uint8_t *pixel_map, *hub75_buff; - // 运行时 - volatile uint8_t light_level; // 0-7 - volatile bool dirty; -}; -``` - -通用 API:`set_pixel`、`fill`、`draw_bitmap`(边界检查+MSB-first 位图写入)、`set_brightness`。 - -### P20 模组 (`dev_display_p20.c`) - -单模块 16×8 像素,2 通道,2 模块横向排列 → **16 列 × 16 行** 屏幕,静态扫描。 - -`g_bsrr[4][8]` CCMRAM 预计算查表:每个颜色通道(R1/G1/B1/R2/G2/B2)×8 色阶 = `p20_bsrr_t`(含 r/g/b 三组 `{GPIO_TypeDef *port; uint32_t val}`),`_p20_init` 时根据 HUB75 引脚定义填入。扫描时直接 `pl_hub75_bsrr_flush(&b->r/g/b)` 无需分支。 - -### 扫描架构 - -``` -TIM3 (行同步, 频率=刷新率×scan_lines) - └─ ISR: osEventFlagsSet(s_scan_evt) - └─ scan_task (osPriorityRealtime, 最高优先级, 不可抢占): - 1. osEventFlagsWait(s_scan_evt) ← 阻塞,零 CPU 占用 - 2. if(dirty) ops->prepare(dev) ← pixel_map → hub75_buff - 3. ops->scan(dev, scan_line) ← BSRR 查表 + CLK 脉冲 - 4. OE/LAT 原子窗口 ← osKernelLock + 关TIM4中断 - 5. scan_line = (scan_line+1) % scan_lines - -TIM4 (PWM, 频率=TIM3×8, 8级亮度): - └─ ISR: OE=(pwm_cnt >= light_level), pwm_cnt=(pwm_cnt+1)&7 -``` - -**实时性保证**: -- `scan_task` 设为 `osPriorityRealtime`,不会被任何用户任务抢占 -- OE/LAT 窗口内 `osKernelLock()` + `NVIC_DisableIRQ(TIM4_IRQn)` 防止 TIM4 抢占破坏 OE 时序 -- `prepare` 在 OE/LAT 窗口外完成(off critical path),扫描本身只查表 - -### 颜色系统 - -```c -typedef enum { COLOR_BLACK=0, COLOR_RED=1, COLOR_GREEN=2, COLOR_YELLOW=3, - COLOR_BLUE=4, COLOR_PURPLE=5, COLOR_CYAN=6, COLOR_WHITE=7 } display_color_t; -``` -颜色值直接写入 `pixel_map[]`,在 prepare 阶段重新映射——像素重排后写入 `hub75_buff[]`,扫描时查 `g_bsrr` 得到三通道 BSRR 输出值。 - -## IO 子系统 (`Device/IO/`) - -### 按键设备 (`dev_key.c`) - -两阶段初始化:`hw_dev_initcall` 注册 EXTI 下降沿回调,`sw_dev_initcall` 创建 TEST 信号量。 - -| 按键 | GPIO | 类型 | 检测方式 | 用途 | -|---|---|---|---|---| -| SW1 | PE12 | 干接点 | EXTI ↓ + `app_key` 轮询释放 | 通用输入 | -| SW2 | PE11 | 干接点 | EXTI ↓ + `app_key` 轮询释放 | 通用输入 | -| SW3 | PE10 | 干接点 | EXTI ↓ + `app_key` 轮询释放 | 通用输入 | -| KEY_TST | PD8 | 测试键 | EXTI ↓ → 信号量(一次性消耗) | `app_key_test_pressed()` | -| DIP1 | PE7 | 拨码开关 | `app_key` 纯轮询 (3 次一致去抖) | 配置选择 | -| DIP2 | PE8 | 拨码开关 | `app_key` 纯轮询 (3 次一致去抖) | 配置选择 | - -所有按键 GPIO 内部上拉,低有效(按下=0)。 - -**数据流**: -``` -硬件按下 → EXTI ISR → dev_key_get_state=标记为 true - ↓ -app_key key_poll_task (20ms) → 轮询 GPIO 实际电平确认释放 - → dev_key_clear_state() 清除标记 - → 更新本地去抖缓存 -``` - -外部通过 `app_key_get_state(DEV_KEY_SW1)` 获取去抖后的稳定状态。`app_key_init()` 由 `sw_app_initcall` 自动创建轮询任务。 - -### IO 控制 (`dev_io_ctrl.c`) - -`hw_dev_initcall` 初始化 GPIO 输出(PD14 车道灯、PD15 闪光灯),确保启动时均为低电平。 - -| 函数 | GPIO | 功能 | -|---|---|---| -| `dev_io_lane_light(bool)` | PD14 | 车道灯开关 | -| `dev_io_flash_light(bool)` | PD15 | 黄闪灯开关 | - -### 光传感器 (`dev_light_sensor.c`) - -ADC1 采样光敏电阻分压(LDR:光越暗→电阻越大→ADC 值越高),8 次均值滤波后映射为亮度等级 1~7。`dev_light_sensor_auto_adjust()` 自动更新 `display->light_level`。 - -`app_light_sensor_init`(`sw_app_initcall`)创建 1 秒周期的自动调节任务。 - -## Storage 子系统 (`Device/Storage/`) - -OCP 虚表,上层通过便捷内联调用(如 `dev_storage_read(d, addr, buf, len)` 自动解引用 `d->ops->read`): - -### W25Qxx (`dev_w25qxx.c`) - -SPI NOR Flash,JEDEC 自动识别容量(W25Q16~256+),>128Mb 自动 4 字节地址。 - -**半双工 SPI 读流程**(`_read`): -``` -1. pl_spi_transmit(cmd, addr_len+1) 阻塞发 Read 命令 + 地址 -2. pl_spi_receive_dma(buf, len) DMA 数据→buf,CPU 不参与搬运 -3. while(!s_ok) osDelay(1) 轮询 DMA 完成标志 -4. _cs_high() SPI 释放 -``` - -- DMA 回调 `_dma_cb`:`s_ok=true` + `osEventFlagsSet`(用于定时等待的场景) -- 首次 `_read` 时懒初始化 `s_evt` + 注册 DMA 回调 -- 写操作使用读-改-写模式(`_read` 整扇区→修改→`_write_no_check` 整扇区),需先擦除非空扇区 - -### 内部 Flash (`dev_flash_int.c`) - -OCP 虚表实现,提供 `flash_int_ops`(`init/read/write/erase`),由各配置模块绑定到具体实例。 - -| 实例 | 绑定模块 | 基地址 | 大小 | initcall | -|---|---|---|---|---| -| `g_flash_iap` | `dev_flash_iap.c` | `0x08004000` (Sector 1) | 16KB | `hw_dev_initcall` | -| (LDI 已迁移) | `dev_flash_ldi.c` | W25Qxx 最后 4KB 扇区 | 4KB | `sw_dev_initcall` (需要 W25Qxx 先就绪) | - -**内部 Flash ops 实现**: -- `_read`:直接内存映射 `memcpy(buf, (void*)(base+addr), len)` -- `_write`:`pl_flash_unlock → pl_flash_program_word × N → pl_flash_lock`(按 word 编程) -- `_erase`:`pl_flash_erase_sector(sector, voltage)` - -**IAP 配置** (`dev_flash_iap.c`):`dev_flash_iap_sys_info_t` 记录(68B)= magic(4) + update_sta(4) + FWInfo(40) + NetConfig(16) + CRC32(4),`g_config` 指针直接映射到 `0x08004000`。 - -**LDI 配置** (`dev_flash_ldi.c`):已迁移至 W25Qxx 最后一个 4KB 扇区。`dev_flash_ldi_record_t`(116B)= magic(4) + cfg(106) + padding(2) + CRC32(4)。通过 `dev_w25qxx_get()` 获取存储句柄,使用 `dev_storage_read/write` 接口。 - -## Render 引擎 (`Application/Src/app_render.c`) - -数据驱动字库引擎,字库存储在 W25Qxx Flash 中,模块 `sw_app_initcall` 自注册。 - -### 字库组织 -- 字库 = **(字号, 编码, 字型)** 三元组的顺序拼接 -- `g_font_lib[40]` 描述表 = 5 字号×2 编码(ASCII/GBK)×4 字型(宋/仿宋/楷/黑) -- `_glyph_bytes(key)` = 每字符字节数:ASCII `(size/2宽+7)/8×size`,GBK `(size宽+7)/8×size` -- `_font_offset(key)` = 线性扫描累加 unit_size,找到目标单元的起始偏移 -- `_char_addr(key, ch)` = 单字符 Flash 地址:ASCII `base+(ch-0x20)×bpc`,GBK `base+((hi-0x81)×190+(lo-offset))×bpc` - -### Tagged Union API - -```c -app_render(&(render_cfg_t){ - .type = RENDER_TEXT, .x=0, .y=0, .w=128, .h=16, .color = COLOR_GREEN, - .text = "你好", .len = strlen("你好"), - .font_size = FONT_16, .font_type = FONT_ST, .text_enc = FONT_ENC_UTF8, -}); -``` - -- `RENDER_TEXT`:UTF8→GBK→逐字 Flash 地址→`dev_storage_read`→`dev_display_draw_bitmap`。支持 `word_wrap` 换行,不换行时截断 -- `RENDER_BITMAP`:直接调用 `dev_display_draw_bitmap` -- `RENDER_FILL`:`w=h=0` 全屏填充,否则矩形填充 -- 渲染风格 `render_style_t`:`h_align` / `v_align` / `word_wrap`(文字专属)。测量趟+渲染趟两阶段,支持逐行独立水平对齐(左/居中/右)。 - -## Application 模块全景 - -| 模块 | 类型 | 通道绑定 | initcall | 职责 | -|---|---|---|---|---| -| `app_dispatch` | 框架 | — | `sw_app_initcall` | 协议调度引擎 | -| `app_render` | 引擎 | — | `sw_app_initcall` | 字库渲染 | -| `app_iap` | 协议 | UDP | `sw_app_initcall` | IAP 固件升级帧处理 | -| `app_ldi` | 协议 | RS485/RS232/TCP | `sw_app_initcall` | LDI 显示控制协议 | -| `app_vms_ctrl` | 协议 | (LDI 子模块) | — | VMS 情报板控制(LDI→Render 桥接) | -| `ah_mqtt` | 协议 | MQTT | (已注释) | AH 平台 MQTT(签到/状态上报/指令) | -| `app_mqtt` | 通道 | CH_ID_MQTT | — | MQTT 网络传输通道 | -| `app_udp` | 通道 | CH_ID_UDP | — | UDP 广播通道(端口 10011) | -| `app_tcp_server` | 通道 | CH_ID_TCP_SERVER | — | TCP 服务器通道 | -| `app_tcp_client` | 通道 | CH_ID_TCP_CLIENT | — | TCP 客户端通道 | -| `app_rs232` | 通道 | RS232/RS232_1 | — | RS232 UART 通道启动 | -| `app_rs485` | 通道 | RS485 | — | RS485 UART 通道启动 | -| `app_key` | 应用 | — | `sw_app_initcall` | 按键轮询去抖 (20ms) | -| `app_light_sensor` | 应用 | — | `sw_app_initcall` | 光传感器自动亮度 (1s 周期) | -| `app_boot` | 编排 | — | — | RTOS 启动编排 | -| `app_test` | 测试 | — | — | 硬件测试用例 | - -**类型说明**: -- **协议**:注册 probe 到 dispatch,创建处理任务,从 frame_queue 收帧 -- **通道**:实现 `ch_ops->send`,收包→`app_channel_dispatch`,发包→网络/UART -- **框架/引擎**:供协议模块调用,不直接通信 -- **应用**:硬件相关业务逻辑,不涉及通信协议 - -**当前运行状态**:`app_boot` 显式启动 TCP Server、TCP Client、UDP 三个通道。`app_mqtt`、`app_rs232`、`app_rs485` 的 start 函数已定义(`static inline`)但未被调用。`ah_mqtt` 的 initcall 已注释。 - -## 协议分发层 (`Application/Src/app_dispatch.c`) - -### 核心数据结构 `dispatch_ctx_t` - -| 字段 | 类型 | 用途 | -|---|---|---| -| `proto_count` | `uint8_t` | 运行时自动递增 | -| `proto_rb[i]` | `ring_buffer_t*[]` | 协议→缓冲区 | -| `proto_probe[i]` | `proto_probe_fn_t[]` | 协议→探测函数 | -| `frame_queue[i]` | `osMessageQueueId_t[]` | 协议→帧队列 | -| `buf_pool[id]` | `ring_buffer_t*[4]` | 缓冲区池(懒初始化) | -| `ch_queue` | `osMessageQueueId_t` | 通道通知队列 | -| `ch_proto_map[ch_id]` | `proto_mask_t[]` | 通道→协议掩码 | -| `channels[ch_id]` | `channel_t*[]` | 通道注册表 | - -### 数据流 - -**接收**:通道任务 → `app_channel_dispatch(ch,data,len)` → `rb_write` → `osMessageQueuePut(ch_queue,&ch)` → `frame_dispatch_task` → probe → `rb_read` → `osMessageQueuePut(frame_queue[i],&msg)` → 协议任务 - -**发送**:协议任务 → `channel_send(ch,data,len)` → `ch->ops->send(ch,data,len)` → UART DMA / LWIP netconn - -### 新增协议步骤 - -1. 定义 `proto_probe_fn_t` 探测函数,返回 `PROTO_PROBE_READY/WAIT/SKIP/FAKE` -2. 协议模块 init 函数中:`app_proto_acquire_buf`→`app_proto_register`→`app_proto_bind_channel`(可多次)→`osThreadNew` -3. 协议任务中:`osMessageQueueNew`→`app_proto_set_frame_queue`→循环 `osMessageQueueGet`→处理 -4. `sw_app_initcall(module_init)` 自注册 - -### 通道标识映射 - -| ch_id | 枚举 | 传输层 | 实现类 | 绑定文件 | -|---|---|---|---|---| -| 0 | `CH_ID_RS485` | UART (USART3) | `uart_channel_t` | `dev_rs485.c` | -| 1 | `CH_ID_RS232` | UART (USART1) | `uart_channel_t` | `dev_rs232.c` | -| 2 | `CH_ID_TCP_SERVER` | LwIP TCP | `tcp_server_channel_t` | `app_tcp_server.c` | -| 3 | `CH_ID_TCP_CLIENT` | LwIP TCP | `tcp_client_channel_t` | `app_tcp_client.c` | -| 4 | `CH_ID_UDP` | LwIP UDP | `udp_channel_t` | `app_udp.c` (端口 10011) | -| 5 | `CH_ID_MQTT` | LwIP MQTT | `mqtt_channel_t` | `app_mqtt.c` | -| 6 | `CH_ID_RS232_1` | UART (USART6) | `uart_channel_t` | `dev_rs232.c` (第二路) | - -## UART 通道子系统 (`Device/Comm/`) - -`uart_channel_t` 继承 `channel_t`,统一定义 send / task / init / deinit 流程: - -``` -hw_initcall: dev_rs485_init / dev_rs232_init - └─ uart_channel_init(self, uart, ch_id, rs485_mode, buf, size) - 设置: self->me.ch_id + ops + uart 句柄 + rs485_mode + buf - -RTOS后: app_rs485_start / app_rs232_start - └─ osThreadNew(uart_channel_task, self) - ├─ osMessageQueueNew(rx_queue) ← ISR→任务通知队列 - ├─ app_channel_register(ch_id, &me) ← 通道注册 - ├─ pl_uart_set_rx_cb → ISR 适配器注册 - ├─ pl_uart_start_rx(buf, size) ← 启动 DMA 接收 - └─ 循环: osMessageQueueGet(rx_queue, &len) - → app_channel_dispatch(&me, buf, len) -``` - -**板级差异**(`dev_rs485.c` / `dev_rs232.c`):仅 UART 实例、RE 引脚方向回调、DMA 缓冲区不同。通道逻辑完全复用 `dev_uart_channel`。 - -## IAP 协议 (`app_iap.c`) - -固件升级协议,绑定 CH_ID_UDP 通道。帧头 `0x5A5A5A5A`,CRC32 校验,7 种命令类型,位图跟踪分包传输。 - -- `iap_handle_task`:协议处理任务(栈 512×4=2KB),循环 `osMessageQueueGet` → 帧解析 → 命令分派 -- 命令表 `g_iap_cmd_table[]`:Function(0x00) / Restart(0x01) / StartFWUpdate(0x02) / FWData(0x03) / EndFWUpdate(0x04) / StopFWUpdate(0x05) / FWCancel(0x06) -- `iap_probe_frame`:探测函数,验证帧头 + 长度 + CRC32 - -## LDI 协议 (`app_ldi.c` + `app_ldi_cmd.c`) - -车道设备指示器协议,绑定 RS485/RS232/TCP 多通道。帧定界符 `0x7E`,CRC-16/XMODEM 校验,支持 13 种设备类型。 - -- `ldi_handle_task`:协议处理任务(栈 512×4=2KB) -- `ldi_timer_task`:周期性状态上报(栈 512×4=2KB) -- `ldi_ctrl_xxx_t`:每种设备类型的控制载荷结构体(`app_ldi_cmd.h`,~288 行) -- `app_vms_ctrl`:VMS 情报板子模块,将 LDI 命令翻译为 `app_render()` 调用 -- 复合指令模块:支持多设备类型组合控制 - -## AH MQTT 协议 (`ah_mqtt.c` + `ah_mqtt_cmd.c`) - -AH 平台 MQTT 应用层协议。固定帧格式(21B~533B),通过 MQTT topic 路由命令。 - -**数据结构**(`ah_mqtt.h`,均为 packed 结构): -- `topic_info_t`:`station_hex(8) + lane_hex(2) + device_type(2) + device_id(2)` -- `notify_id_t`:日期+设备信息+发送计数 -- `sign_up_t`:设备签到(含软硬件版本号、协议版本、厂商信息) -- `state_report_t`:定期状态上报 - -**命令处理器**(`ah_mqtt_cmd.h`,通过 `g_ah_mqtt_cmd_table[]` 跳转): -- `/ASK/board/NULL` → `cmd_display`:UTF8 文本渲染 -- `/ASK/display/clean` → `cmd_fill`:全屏填充颜色 -- `/ASK/op/restart` → `cmd_restart`:应答后 `NVIC_SystemReset()` -- `/ASK/op/checktime` → `cmd_checktime`:更新时间戳 - -**注意**:模块 initcall 当前已注释(`// sw_app_initcall(ah_mqtt_module_init)`),`app_mqtt_start()` 也未调用,AH MQTT 链路未激活。 - -## 网络子系统 - -### PHY→MAC→LwIP 依赖链 - -``` -Device 层: dev_dp83848 (PHY 寄存器操作, 自动协商, 链路状态检测) - └─ 注入 IO 上下文 (pl_eth_phy_io_*) -Platform 层: pl_eth (ETH MAC/DMA, MDIO 读写) - └─ 注册 PHY 链路查询回调 (pl_eth_set_phy_link_fn) - pl_net (LwIP netif 初始化, IP 配置, 链路监听器注册) - └─ ethernet_link_thread 轮询链路状态 → 通知 pl_net_link_listener[] -Application: app_udp / app_tcp_server / app_tcp_client / app_mqtt - └─ pl_net_register_link_listener → 链路断开回调重建连接 -``` - -### 通道生命周期(以 UDP 为例) - -``` -udp_task: 绑定端口→循环{创建 udp_connect_task→等待信号量→销毁→延迟重连} - └─ udp_connect_task (每个客户端一个): - 1. udp_channel_init: ch.ops=udp_ch_ops, state=UP, app_channel_register - 2. 循环 netconn_recv → 提取源IP/端口 → app_channel_dispatch - 3. udp_channel_deinit: ops=nullptr, state=DOWN, app_channel_register(NULL) - 4. osSemaphoreRelease 通知 udp_task 重连 -``` - -**IP 隔离**:`udp_channel_t` 存储源 IP 为 `uint8_t src_ip[4]` 字节数组,而非 LwIP `ip_addr_t`,避免 Application 层暴露 middleware 类型。 - -### dev_dp83848 PHY 驱动 (`Device/Network/dev_dp83848.c`) - -PHY 寄存器操作、自动协商、链路状态检测。通过 IO 上下文注入(`pl_eth_phy_io_*` 函数指针)与 MAC 层解耦。`dev_dp83848.h`(~249 行)定义完整 PHY 寄存器映射和 15+ 操作 API。 - -### LwIP 配置要点 (`Platform/Inc/lwipopts.h`) - -关键参数(132 行,与 LwIP 默认值差异大): -- **内存**:`MEM_SIZE=12KB`、`MEMP_NUM_PBUF=16`、`PBUF_POOL_SIZE=12` -- **硬件校验和**:`CHECKSUM_GEN_IP/UDP/TCP=0`、`CHECKSUM_CHECK_*_HW` 卸载到 MAC -- **UDP 广播**:`LWIP_BROADCAST=1`(IAP 升级依赖) -- **MQTT**:`LWIP_MQTT=1`、`MQTT_MAX_IN_FLIGHT=4` -- **TCP**:`LWIP_TCP_KEEPALIVE=1`、`TCP_MSS=1460` -- **线程**:`TCPIP_THREAD_PRIO=osPriorityHigh`、`TCPIP_THREAD_STACKSIZE=1024` - -### pl_net_adapt.h 架构约束 - -`Platform/Inc/pl_net_adapt.h` 聚合所有 LwIP API 头文件。**严格禁止在任何 .h 文件中包含此头文件**——仅 .c 实现文件可引用,防止 LwIP 类型泄漏到 Application 层头文件中。 - -## 调试 - -**SEGGER RTT**(`pl_rtt.c`):高速调试输出通道(无需占用 UART),通过 J-Link/CMSIS-DAP 的 SWD 接口传输。`pl_rtt_init()` 在 `hw_initcall` 中初始化,`SEGGER_RTT_printf()` 可在任何上下文中使用。不依赖 RTOS,可在 HardFault Handler 等异常上下文中输出。 - -**硬件测试**(`app_test.c`):提供 4 个独立硬件测试用例 — 像素扫描、字库渲染、IO 输出、LED 灯序映射。在 `init_task` 中通过 `app_test_run()` 按需调用,正式发布时注释掉。 - -## Kernel 工具库 (`Kernel/`) - -| 文件 | 关键 API | 说明 | -|---|---|---| -| `ring_buffer.h` | `RB_DEFINE(name,sz)` 编译期静态分配 | 零堆开销环形缓冲区。每个 API 末尾的 `void *mutex` 参数控制锁行为:传 `rb->mutex` 自动加锁/解锁(单步操作),传 `nullptr` 跳过(调用者通过 `rb_lock/rb_unlock` 自行持锁,用于多步原子序列)。`rb_init` 在运行时绑定优先级继承互斥锁。相邻数据的 `rb_peek`/`rb_contig`/`rb_skip` 支持帧探测。 | -| `initcall.h` | `hw_pl/dev_initcall`、`sw_pl/dev/app_initcall` | 6 个层级宏,生成 linker section 条目 | -| `dispatch_types.h` | `channel_t`、`ch_ops_t`、`channel_id_t`、`proto_probe_sta_t`、`container_of` | Device↔Application 共用类型 | -| `text_cvt.h` | `UTF8ToGBK` | UTF-8→GBK 编码转换 | -| `bit_utils.h` | `bit_ctz` | 协议掩码→数组索引(`proto_mask_t` → 数组下标) | -| `crc_utils.h` | CRC 校验 | CRC32(IAP)+ CRC-16/XMODEM(LDI) | - -## 代码风格 - -- `.clang-format`:Microsoft 基础,4 空格缩进,无 Tab,Linux 大括号,不限制列宽 -- 语言:C23,中文注释 -- 命名:`pl_`=Platform、`dev_`=Device、`app_`=Application -- 派生类基类成员统一 `me`(如 `dev_display_t me`、`channel_t me`) -- 函数命名:`_` 前缀 = 内部静态函数、模块前缀 `pl_`/`dev_`/`app_` = 公开 API -- GPIO 定义遵循 CubeMX 命名 diff --git a/Compiler/STM32F407XX_FLASH.ld b/Compiler/STM32F407XX_FLASH.ld index d34e2e1..265ce8b 100644 --- a/Compiler/STM32F407XX_FLASH.ld +++ b/Compiler/STM32F407XX_FLASH.ld @@ -55,16 +55,16 @@ ENTRY(Reset_Handler) /* Highest address of the user mode stack */ _estack = ORIGIN(RAM) + LENGTH(RAM); /* end of RAM */ /* Generate a link error if heap and stack don't fit into RAM */ -_Min_Heap_Size = 0x200; /* required amount of heap */ -_Min_Stack_Size = 0x800; /* required amount of stack */ +_Min_Heap_Size = 0x400; /* required amount of heap */ +_Min_Stack_Size = 0xA00; /* required amount of stack */ /* Specify the memory areas */ MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K -FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K -/* FLASH (rx) : ORIGIN = 0x08040000, LENGTH = 768K */ +/* FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K */ +FLASH (rx) : ORIGIN = 0x08040000, LENGTH = 768K } /* Define output sections */ diff --git a/Core/Inc/FreeRTOSConfig.h b/Core/Inc/FreeRTOSConfig.h index fdf8b76..6dc1853 100644 --- a/Core/Inc/FreeRTOSConfig.h +++ b/Core/Inc/FreeRTOSConfig.h @@ -68,11 +68,13 @@ extern uint32_t SystemCoreClock; #define configTICK_RATE_HZ ((TickType_t)1000) #define configMAX_PRIORITIES (56) #define configMINIMAL_STACK_SIZE ((uint16_t)128) -#define configTOTAL_HEAP_SIZE ((size_t)32 * 1024) +#define configTOTAL_HEAP_SIZE ((size_t)36 * 1024) /* heap_4 静态 ucHeap → SRAM .bss;栈压缩后试 36KB */ +#define configAPPLICATION_ALLOCATED_HEAP 0 /* 0:heap_4 自带 static ucHeap,禁止再放 CCM */ #define configMAX_TASK_NAME_LEN (16) #define configUSE_TRACE_FACILITY 1 #define configUSE_16_BIT_TICKS 0 #define configUSE_MUTEXES 1 +#define configUSE_MALLOC_FAILED_HOOK 1 #define configQUEUE_REGISTRY_SIZE 8 #define configUSE_RECURSIVE_MUTEXES 1 #define configUSE_COUNTING_SEMAPHORES 1 diff --git a/Core/Inc/main.h b/Core/Inc/main.h index 1b51cc6..b70fbda 100644 --- a/Core/Inc/main.h +++ b/Core/Inc/main.h @@ -52,9 +52,9 @@ enum { /* Exported macro ------------------------------------------------------------*/ /* USER CODE BEGIN EM */ -// SRAM存储地址映射 +// SRAM����ַλ��ӳ�� #define BITBAND_SRAM(address, bit) (*(volatile uint32_t *)(0x22000000 + ((uint32_t)(address) - 0x20000000) * 0x20 + (bit) * 0x04)) -// 外设地址映射 +// ��������ַλ��ӳ�� #define BITBAND_PERIPH(address, bit) (*(volatile uint32_t *)(0x42000000 + ((uint32_t)(address) - 0x40000000) * 0x20 + (bit) * 0x04)) /* USER CODE END EM */ diff --git a/Core/Src/main.c b/Core/Src/main.c index 6d39fd7..0c8739a 100644 --- a/Core/Src/main.c +++ b/Core/Src/main.c @@ -6,28 +6,27 @@ */ #include "main.h" +#include "app_boot.h" #include "initcall.h" #include "pl_sys.h" -void app_boot(void); +int main(void) { + SCB->VTOR = FLASH_BASE | 0x40000; + __enable_irq(); -int main(void) -{ - // SCB->VTOR = FLASH_BASE | 0x40000; - __enable_irq(); + HAL_Init(); + SystemClock_Config(); - HAL_Init(); - SystemClock_Config(); + initcall_run(__hw_initcall_start, __hw_initcall_end); - initcall_run(__hw_initcall_start, __hw_initcall_end); + app_boot(); - app_boot(); - - for (;;) {} + for (;;) { + } } -void Error_Handler(void) -{ - __disable_irq(); - while (1) {} +void Error_Handler(void) { + __disable_irq(); + while (1) { + } } diff --git a/Core/Src/stm32f4xx_it.c b/Core/Src/stm32f4xx_it.c index 8c3f053..aa5ab8b 100644 --- a/Core/Src/stm32f4xx_it.c +++ b/Core/Src/stm32f4xx_it.c @@ -7,11 +7,11 @@ #include "stm32f4xx_it.h" /* ---- HardFault 现场保护:崩溃信息存入全局变量,调试时 Watch 窗口查看 ---- */ -static uint32_t g_fault_stack[8]; /* R0,R1,R2,R3,R12,LR,PC,xPSR */ -static uint32_t g_fault_pc; /* 崩溃指令地址 */ -static uint32_t g_fault_cfsr; /* SCB->CFSR 故障状态寄存器 */ -static uint32_t g_fault_bfar; /* SCB->BFAR 总线错误地址 */ -static uint32_t g_fault_r0; /* 故障时 R0 */ +static volatile uint32_t g_fault_stack[8]; /* R0,R1,R2,R3,R12,LR,PC,xPSR */ +static volatile uint32_t g_fault_pc; /* 崩溃指令地址 */ +static volatile uint32_t g_fault_cfsr; /* SCB->CFSR 故障状态寄存器 */ +static volatile uint32_t g_fault_bfar; /* SCB->BFAR 总线错误地址 */ +static volatile uint32_t g_fault_r0; /* 故障时 R0 */ void hard_fault_handler_c(uint32_t *stack_frame) { diff --git a/Core/Src/tim.c b/Core/Src/tim.c index 6d3bb78..18efd9c 100644 --- a/Core/Src/tim.c +++ b/Core/Src/tim.c @@ -81,7 +81,7 @@ void MX_TIM3_Init(void) htim3.Instance = TIM3; htim3.Init.Prescaler = 84 - 1; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; - htim3.Init.Period = 5000 - 1; + htim3.Init.Period = 500 - 1; htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; if (HAL_TIM_Base_Init(&htim3) != HAL_OK) { diff --git a/Core/Src/usart.c b/Core/Src/usart.c index d5e39d1..d14d06d 100644 --- a/Core/Src/usart.c +++ b/Core/Src/usart.c @@ -43,7 +43,7 @@ void MX_USART1_UART_Init(void) /* USER CODE END USART1_Init 1 */ huart1.Instance = USART1; - huart1.Init.BaudRate = 115200; + huart1.Init.BaudRate = 9600; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; @@ -70,7 +70,7 @@ void MX_USART3_UART_Init(void) /* USER CODE END USART3_Init 1 */ huart3.Instance = USART3; - huart3.Init.BaudRate = 115200; + huart3.Init.BaudRate = 9600; huart3.Init.WordLength = UART_WORDLENGTH_8B; huart3.Init.StopBits = UART_STOPBITS_1; huart3.Init.Parity = UART_PARITY_NONE; @@ -97,7 +97,7 @@ void MX_USART6_UART_Init(void) /* USER CODE END USART6_Init 1 */ huart6.Instance = USART6; - huart6.Init.BaudRate = 115200; + huart6.Init.BaudRate = 9600; huart6.Init.WordLength = UART_WORDLENGTH_8B; huart6.Init.StopBits = UART_STOPBITS_1; huart6.Init.Parity = UART_PARITY_NONE; diff --git a/Device/Comm/dev_rs232.c b/Device/Comm/dev_rs232.c index 1cb45d1..244a705 100644 --- a/Device/Comm/dev_rs232.c +++ b/Device/Comm/dev_rs232.c @@ -1,19 +1,19 @@ /** * @file dev_rs232.c - * @brief RS232 串口板级资源(USART3=RS232-0, USART6=RS232-1) + * @brief RS232 串口板级资源(USART3=RS232-0 协议 DMA RX) * - * 仅提供 DMA 缓冲区等板级静态资源。 + * USART6 语音板仅 TX(dev_rs232_voice),不分配 DMA RX 缓冲。 * 通道生命周期和任务循环由 Application 层 app_rs232 负责。 */ #include "dev_rs232.h" -#define RS232_BUF_SIZE (2048U) - -static uint8_t s_rs232_0_buf[RS232_BUF_SIZE]; -static uint8_t s_rs232_1_buf[RS232_BUF_SIZE]; +/* 乒乓双缓冲:2 × RS232_BUF_SIZE = 1280B(.bss 静态,SRAM 账见 doc/06) + * pl_uart circular DMA 覆盖整缓冲,HT/TC 各冲刷一块。 */ +static uint8_t s_rs232_0_buf[2 * RS232_BUF_SIZE]; uint8_t *dev_rs232_get_buf(uint8_t index) { - return (index == 0) ? s_rs232_0_buf : s_rs232_1_buf; + /* index1(USART6)无协议 RX,不提供 DMA 缓冲 */ + return (index == 0U) ? s_rs232_0_buf : nullptr; } diff --git a/Device/Comm/dev_rs232_voice.c b/Device/Comm/dev_rs232_voice.c new file mode 100644 index 0000000..0a56ced --- /dev/null +++ b/Device/Comm/dev_rs232_voice.c @@ -0,0 +1,61 @@ +/** + * @file dev_rs232_voice.c + * @brief RS232 TTS 语音板封装 + */ + +#include "dev_rs232_voice.h" + +#include + +#include "dev_rs232.h" +#include "pl_uart.h" + +static pl_uart_handle_t dev_rs232_voice_uart(void) +{ + /* 语音板 TTS 专用 USART6 (PL_UART6),不再使用 USART3 */ + return pl_uart_get_handle(PL_UART6); +} + +static void dev_rs232_voice_send(const uint8_t *frame, uint16_t len) +{ + pl_uart_handle_t uart = dev_rs232_voice_uart(); + if (!uart || !frame || len == 0U) { + return; + } + + (void)pl_uart_send(uart, frame, len, 100U); +} + +void dev_rs232_voice_play(const uint8_t *gbk_text, uint16_t len) +{ + if (!gbk_text || len == 0U || len > DEV_RS232_VOICE_MAX_TEXT) { + return; + } + + uint8_t frame[DEV_RS232_VOICE_MAX_TEXT + 5U]; + const uint16_t payload_len = (uint16_t)(len + 2U); + if (payload_len > 0xFFU) { + return; + } + + frame[0] = 0xFDU; + frame[1] = 0x00U; + frame[2] = (uint8_t)payload_len; + frame[3] = 0x01U; + frame[4] = 0x01U; + memcpy(&frame[5], gbk_text, len); + + dev_rs232_voice_send(frame, (uint16_t)(len + 5U)); +} + +void dev_rs232_voice_volume(uint8_t level) +{ + uint8_t frame[6]; + frame[0] = 0xFDU; + frame[1] = 0x00U; + frame[2] = 0x03U; + frame[3] = 0x01U; + frame[4] = 0x02U; + frame[5] = level; + dev_rs232_voice_send(frame, sizeof(frame)); +} diff --git a/Device/Comm/dev_rs485.c b/Device/Comm/dev_rs485.c index 12a00f5..04c2bb8 100644 --- a/Device/Comm/dev_rs485.c +++ b/Device/Comm/dev_rs485.c @@ -2,7 +2,7 @@ * @file dev_rs485.c * @brief RS485 半双工收发器板级资源(USART1, RE=PA8) * - * 仅提供 DMA 缓冲区 + RE 方向控制等板级静态资源。 + * 仅提供 DMA 乒乓双缓冲 + RE 方向控制等板级静态资源。 * 通道生命周期和任务循环由 Application 层 app_rs485 负责。 */ @@ -12,9 +12,9 @@ #include "pl_gpio.h" #include "initcall.h" -#define RS485_BUF_SIZE (2048U) - -static uint8_t s_rs485_buf[RS485_BUF_SIZE]; +/* 乒乓双缓冲:2 × RS485_BUF_SIZE = 1280B(.bss 静态,SRAM 账见 doc/06) + * pl_uart circular DMA 覆盖整缓冲,HT/TC 各冲刷一块。 */ +static uint8_t s_rs485_buf[2 * RS485_BUF_SIZE]; uint8_t *dev_rs485_get_buf(void) { diff --git a/Device/Display/dev_display.c b/Device/Display/dev_display.c index 64e9dc9..2461bfb 100644 --- a/Device/Display/dev_display.c +++ b/Device/Display/dev_display.c @@ -16,16 +16,29 @@ /* ---- 扫描任务事件 ---- */ static osEventFlagsId_t s_scan_evt; static dev_display_t *s_active_display; +static uint8_t *s_frame_front; +static uint8_t *s_frame_back; +static volatile uint8_t *s_pending_frame; +static volatile uint8_t s_frame_ready; + +volatile uint32_t g_dev_display_commit_count; +volatile uint32_t g_dev_display_scan_count; /* ---- 实例注册(由派生模组的 hw_dev_initcall 调用)---- */ -void dev_display_register(dev_display_t *dev) -{ - s_active_display = dev; -} +void dev_display_register(dev_display_t *dev) { s_active_display = dev; } -dev_display_t *dev_display_get(void) +dev_display_t *dev_display_get(void) { return s_active_display; } + +void dev_display_commit_frame(dev_display_t *dev) { - return s_active_display; + if (!dev) + return; + + osKernelLock(); + s_pending_frame = dev->pixel_map; + s_frame_ready = 1U; + g_dev_display_commit_count++; + osKernelUnlock(); } /* ---- TIM 周期回调(前向声明,实现在文件末尾)---- */ @@ -52,17 +65,30 @@ static void scan_task(void *arg) pl_tim_start_it(pl_tim_get_handle(PL_TIM3)); pl_tim_start_it(pl_tim_get_handle(PL_TIM4)); + s_frame_front = dev->pixel_map; + s_frame_back = dev->pixel_map; + for (;;) { osEventFlagsWait(s_scan_evt, 0x01, osFlagsWaitAny, osWaitForever); - /* 脏标记 → 预计算(off critical path) */ - if (dev->dirty) { + /* 帧提交 → 预计算(off critical path) */ + osKernelLock(); + bool frame_ready = s_frame_ready != 0U; + if (frame_ready) { + s_frame_front = (uint8_t *)s_pending_frame; + s_frame_back = dev->hub75_buff; + s_frame_ready = 0U; + } + osKernelUnlock(); + + if (frame_ready) { dev->dirty = false; if (dev->ops->prepare) dev->ops->prepare(dev); } /* 模组专用扫描输出 */ + g_dev_display_scan_count++; dev->ops->scan(dev, scan_line); /* OE/LAT 原子窗口(所有模组通用) */ @@ -90,7 +116,7 @@ void dev_display_start(void) const osThreadAttr_t attr = { .name = "scan_task", - .stack_size = 512, + .stack_size = 256 * 4, /* 扫屏路径无大栈帧;原 2KB 偏大 */ .priority = osPriorityRealtime, }; osThreadNew(scan_task, dev, &attr); @@ -108,14 +134,21 @@ void dev_display_set_pixel(dev_display_t *dev, uint16_t x, uint16_t y, display_c void dev_display_set_brightness(dev_display_t *dev, uint8_t level) { - if (level > 7) level = 7; + if (level > DEV_DISPLAY_BRIGHTNESS_MAX) level = DEV_DISPLAY_BRIGHTNESS_MAX; dev->light_level = level; } void dev_display_fill(dev_display_t *dev, uint16_t x, uint16_t y, uint16_t w, uint16_t h, display_color_t color) { - if (x + w > dev->screen_rows) w = dev->screen_rows - x; - if (y + h > dev->screen_cols) h = dev->screen_cols - y; + /* 起点越界:整区域不可见,直接丢弃。 + * 必须先判起点再截断——否则 screen_rows - x / screen_cols - y 无符号下溢成巨值, + * memset/行循环写穿 pixel_map(CCMRAM)导致 HardFault。 */ + if (x >= dev->screen_rows || y >= dev->screen_cols) + return; + /* 截断判断用 32 位运算:右对齐下溢 cur_x≈0xFFF0 时,x+w 的 uint16 加法回绕成 + * 小值绕过判界,改为 (uint32_t)x + w 后正确识别越界并截断 */ + if ((uint32_t)x + w > dev->screen_rows) w = dev->screen_rows - x; + if ((uint32_t)y + h > dev->screen_cols) h = dev->screen_cols - y; for (uint16_t row = 0; row < h; row++) memset(&dev->pixel_map[(y + row) * dev->screen_rows + x], (uint8_t)color, w); @@ -124,13 +157,23 @@ void dev_display_fill(dev_display_t *dev, uint16_t x, uint16_t y, uint16_t w, ui void dev_display_draw_bitmap(dev_display_t *dev, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *bitmap, display_color_t color) { - if (x + w > dev->screen_rows || y + h > dev->screen_cols) return; + /* 起点越界早退(与 fill 对齐):x+w / y+h 为 uint16 加法, + * 起点已回绕出屏时 32 位判界也无法救回,直接丢弃整区域 */ + if (x >= dev->screen_rows || y >= dev->screen_cols) + return; + /* 越界判断用 32 位运算防 x+w / y+h 的 uint16 回绕绕过判界(根因同 fill) */ + if ((uint32_t)x + w > dev->screen_rows || (uint32_t)y + h > dev->screen_cols) + return; uint16_t row_bytes = (w + 7) / 8; for (uint16_t row = 0; row < h; row++) { for (uint16_t col = 0; col < w; col++) { - if (bitmap[row * row_bytes + col / 8] & (0x80 >> (col % 8))) - dev->pixel_map[(y + row) * dev->screen_rows + (x + col)] = (uint8_t)color; + if (bitmap[row * row_bytes + col / 8] & (0x80 >> (col % 8))) { + /* 索引中间值用 uint32 防 (y+row)*rows + (x+col) 的 uint16 回绕; + * 早退保证终值仍落在 pixel_map 内 */ + uint32_t idx = (uint32_t)(y + row) * dev->screen_rows + (uint32_t)(x + col); + dev->pixel_map[idx] = (uint8_t)color; + } } } dev->dirty = true; diff --git a/Device/Display/dev_display_1_260.c b/Device/Display/dev_display_1_260.c new file mode 100644 index 0000000..e8760c6 --- /dev/null +++ b/Device/Display/dev_display_1_260.c @@ -0,0 +1,199 @@ +/** + * @file dev_display_1_260.c + * @brief 1-260 模组派生类型 — 1/8扫描、2 通道、R/G/B 分离 — SM16026S+SM5266PH + * + * 实现 dev_display_ops: prepare (pixel_map→hub75_buff) + scan (BSRR 查表输出) + * g_bsrr 查表放在 CCMRAM 中,避免 Flash 读取和分支。 + */ + +#include "dev_display.h" + +#include +#include "initcall.h" + +/* ================================================================ + * 1-260 模组参数 + * ================================================================ */ +#define MODULE_CODE "1000000260" + +#define _1_260_MODULE_ROWS (1U) /* 每行模块数 */ +#define _1_260_MODULE_COLS (1U) /* 每列模块数 */ +#define _1_260_MODULE_PIXEL_ROW (32U) /* 单模块每行的像素个数 */ +#define _1_260_MODULE_PIXEL_COL (32U) /* 单模块每列的像素个数 */ +#define _1_260_CHANNELS_PER_MODULE (2U) /* 每模块通道数(R1G1B1 + R2G2B2) */ +#define _1_260_SCAN_LINES (8U) /* 1/8 扫描 */ + +/* ---- 派生参数(由模组参数计算,勿手动修改) ---- */ +#define _1_260_SCREEN_ROWS (_1_260_MODULE_ROWS * _1_260_MODULE_PIXEL_ROW) // 屏幕每行的像素个数 +#define _1_260_SCREEN_COLS (_1_260_MODULE_COLS * _1_260_MODULE_PIXEL_COL) // 屏幕每列的像素个数 +#define _1_260_BUFFER_SIZE (_1_260_SCREEN_ROWS * _1_260_SCREEN_COLS) +#define _1_260_TOTAL_CHANNELS (_1_260_MODULE_COLS * _1_260_CHANNELS_PER_MODULE) +#define _1_260_CHANNEL_PIXELS (_1_260_MODULE_PIXEL_ROW * _1_260_MODULE_PIXEL_COL * _1_260_MODULE_ROWS / _1_260_CHANNELS_PER_MODULE) +#define _1_260_SCAN_LINE_PX (_1_260_CHANNEL_PIXELS / _1_260_SCAN_LINES) + +/* ================================================================ + * hub75_buff 布局 + * + * HUB75 协议以"组"为单位移位输出,每组 16 字节对应 4×4 像素块。 + * 屏幕 16×16 像素 → 4×4 组,每组覆盖 4 行 × 4 列。 + * + * 每个组 16 字节 = 4 行 × 4 列,每组内布局: + * 偏移 row%4 col: 0 1 2 3 + * ───── ───── ─────────────────── + * 0..3 0 6 4 2 0 ← 行镜像 (3 - col) + * 4..7 1 7 5 3 1 ← 行镜像 + 偏移 1 + * 8..11 2 8 10 12 14 ← 行正序 + 偏移 9 + * 12..15 3 9 11 13 15 ← 行正序 + 偏移 8 + * + * 每组 2 字节一组(对应 R/G/B 三色),每组内 4 行的偏移规律 + * 由 HUB75 驱动芯片的数据输入格式决定。 + * ================================================================ */ + +#define GROUP_PIXEL_W 4 /* 每组覆盖的列数 */ +#define GROUP_PIXEL_H 4 /* 每组覆盖的行数 */ +#define GROUP_SIZE 16 /* 每组的字节数 = GROUP_PIXEL_W × GROUP_PIXEL_H */ + +/* 组内行偏移的基准值 */ +#define GROUP_ROW0_OFFSET 0 /* row%4 == 0: 双字节对偏移 0 */ +#define GROUP_ROW1_OFFSET 1 /* row%4 == 1: 双字节对偏移 1 */ +#define GROUP_ROW2_OFFSET 9 /* row%4 == 2: 双字节对偏移 9 */ +#define GROUP_ROW3_OFFSET 8 /* row%4 == 3: 双字节对偏移 8 */ + +#define GROUP_COLS_PER_ROW (_1_260_MODULE_ROWS * GROUP_PIXEL_W) /* 每行模块的水平组数 */ + +/* ---- BSRR 预计算查表 ---- */ +typedef struct { + pl_hub75_bsrr_t r, g, b; +} _1_260_bsrr_t; + +[[gnu::section(".ccmram")]] static _1_260_bsrr_t g_bsrr[_1_260_TOTAL_CHANNELS][8]; + +/* ---- 1-260 实例 ---- */ +typedef struct { + dev_display_t me; +} dev_display_1_260_t; + +[[gnu::section(".ccmram")]] static uint8_t _1_260_pixel_map[_1_260_BUFFER_SIZE]; +[[gnu::section(".ccmram")]] static uint8_t _1_260_hub75_buff[_1_260_BUFFER_SIZE]; + +static dev_display_1_260_t g_1_260 = { + .me = { + .ops = nullptr, /* 由 dev_display_1_260_init 设置 */ + .module_rows = _1_260_MODULE_PIXEL_ROW, + .module_cols = _1_260_MODULE_PIXEL_COL, + .channels_per_module = _1_260_CHANNELS_PER_MODULE, + .modules_per_row = _1_260_MODULE_ROWS, + .modules_per_col = _1_260_MODULE_COLS, + .scan_lines = _1_260_SCAN_LINES, + .screen_rows = _1_260_SCREEN_ROWS, + .screen_cols = _1_260_SCREEN_COLS, + .total_channels = _1_260_TOTAL_CHANNELS, + .channel_pixels = _1_260_CHANNEL_PIXELS, + .scan_line_pixels = _1_260_SCAN_LINE_PX, + .buffer_size = _1_260_BUFFER_SIZE, + .pixel_map = _1_260_pixel_map, + .hub75_buff = _1_260_hub75_buff, + .module_code = MODULE_CODE, + .light_level = DEV_DISPLAY_BRIGHTNESS_MAX, + }, +}; + +dev_display_t *dev_display_1_260_get(void) +{ + return &g_1_260.me; +} + +/* ================================================================ + * prepare: pixel_map → hub75_buff 像素重排 + * + * pixel_map[] 按行优先存储(y * screen_rows + x), + * hub75_buff[] 按"组"组织以匹配 HUB75 移位寄存器输入时序。 + * 此函数将像素值从逻辑坐标映射到硬件输出缓冲。 + * ================================================================ */ + +static void _1_260_prepare(dev_display_t *dev) +{ + for (uint16_t linear = 0; linear < dev->buffer_size; linear++) { + uint16_t x = linear % dev->screen_rows; + uint16_t y = linear / dev->screen_rows; + dev->hub75_buff[linear] = dev->pixel_map[y * dev->screen_rows + x]; + } +} + +/* ================================================================ + * scan: 逐像素输出 — BSRR 查表 + CLK 脉冲 + * + * hub75_buff[] 存储的是颜色索引 (0~7),扫描时查 g_bsrr + * 得到该通道、该颜色的 R/G/B 三组 {port, BSRR_val},直接 flush 输出。 + * ================================================================ */ + +static inline void _1_260_scan(dev_display_t *dev, uint8_t line) +{ + for (uint16_t pixel = 0; pixel < dev->scan_line_pixels; pixel++) { + /* 当前像素在所有通道中的起始偏移 */ + uint16_t pixel_base = (uint16_t)line * dev->scan_line_pixels + pixel; + + /* 同一像素位置同时输出所有通道的颜色数据 */ + for (uint8_t ch = 0; ch < dev->total_channels; ch++) { + uint8_t color = dev->hub75_buff[pixel_base + ch * dev->channel_pixels]; + _1_260_bsrr_t *bsrr = &g_bsrr[ch][color]; + pl_hub75_bsrr_flush(&bsrr->r); + pl_hub75_bsrr_flush(&bsrr->g); + pl_hub75_bsrr_flush(&bsrr->b); + } + + /* 锁存当前像素数据到移位寄存器 */ + pl_hub75_clock_pulse(); + } +} + +/* ---- set_row: SM5266PH 移位寄存器型行驱动 ---- */ +static void _1_260_set_row(uint8_t row) +{ + pl_hub75_ShiftRegister_set_row(row, 1); +} + +/* ---- ops 虚表 ---- */ +static const dev_display_ops_t _1_260_ops = { + .prepare = _1_260_prepare, + .scan = _1_260_scan, + .set_row = _1_260_set_row, +}; + +/* ================================================================ + * dev_display_1_260_init: 预计算 BSRR 查表 + 绑定 ops + * + * g_bsrr[ch][c] 存储通道 ch 在颜色 c (0~7) 时的 R/G/B 引脚输出值。 + * 颜色 c 的 bit0→R, bit1→G, bit2→B: + * - R 亮: (c & 1) != 0 → BSRR 置位(高电平) + * - R 灭: (c & 1) == 0 → BSRR 复位(低电平) + * - G 亮: (c & 2) != 0, B 亮: (c & 4) != 0 + * + * 预计算避免扫描热路径中的分支判断。 + * ================================================================ */ + +void dev_display_1_260_init(void) +{ + g_1_260.me.ops = &_1_260_ops; + dev_display_register(&g_1_260.me); + + for (uint8_t ch = 0; ch < g_1_260.me.total_channels; ch++) { + for (uint8_t color = 0; color < 8; color++) { + /* R 通道: color bit0 决定亮灭 */ + g_bsrr[ch][color].r.port = g_hub75_pin_r[ch].port; + g_bsrr[ch][color].r.val = (color & 1) ? (uint32_t)g_hub75_pin_r[ch].pin /* 置位: 输出高 */ + : (uint32_t)g_hub75_pin_r[ch].pin << 16; /* 复位: 输出低 */ + + /* G 通道: color bit1 决定亮灭 */ + g_bsrr[ch][color].g.port = g_hub75_pin_g[ch].port; + g_bsrr[ch][color].g.val = (color & 2) ? (uint32_t)g_hub75_pin_g[ch].pin + : (uint32_t)g_hub75_pin_g[ch].pin << 16; + + /* B 通道: color bit2 决定亮灭 */ + g_bsrr[ch][color].b.port = g_hub75_pin_b[ch].port; + g_bsrr[ch][color].b.val = (color & 4) ? (uint32_t)g_hub75_pin_b[ch].pin + : (uint32_t)g_hub75_pin_b[ch].pin << 16; + } + } +} +hw_dev_initcall(dev_display_1_260_init); \ No newline at end of file diff --git a/Device/Display/dev_display_1_577.c b/Device/Display/dev_display_1_577.c new file mode 100644 index 0000000..1700056 --- /dev/null +++ b/Device/Display/dev_display_1_577.c @@ -0,0 +1,214 @@ +/** + * @file dev_display_1_577.c + * @brief 1-577 模组派生类型 — P5 户外全彩(左右半区映射) + * + * 实现 dev_display_ops: prepare (pixel_map→hub75_buff) + scan (BSRR 查表输出) + * g_bsrr / row_dst 查表放在 CCMRAM 中,避免 Flash 读取和分支。 + * + * 仅改下方模组宏即可驱动不同拼屏尺寸;映射规则仍为 1-577 左右半区。 + */ + +#include "dev_display.h" + +#include +#include "initcall.h" + +/* ================================================================ + * 1-577 模组参数 — P5 户外全彩 + * + * 单模块 64x32 像素,HUB75 2 通道/模块,ABC 1/8 扫描。 + * 改 MODULE_ROWS / MODULE_COLS 即可切换拼屏(如 1x1 → 64x32,3x3 → 192x96)。 + * ================================================================ */ +#define MODULE_CODE "1000000577" + +#define _1_577_MODULE_ROWS (1U) /* 每行模块数(水平) */ +#define _1_577_MODULE_COLS (1U) /* 每列模块数(垂直) */ +#define _1_577_MODULE_PIXEL_ROW (64U) /* 单模块每行的像素个数 */ +#define _1_577_MODULE_PIXEL_COL (32U) /* 单模块每列的像素个数 */ +#define _1_577_CHANNELS_PER_MODULE (2U) /* 每模块通道数(R1G1B1 + R2G2B2) */ +#define _1_577_SCAN_LINES (8U) /* 1/8 扫描 */ + +/* ---- 派生参数(由模组参数计算,勿手动修改) ---- */ +#define _1_577_SCREEN_ROWS (_1_577_MODULE_ROWS * _1_577_MODULE_PIXEL_ROW) /* 屏幕每行像素数 */ +#define _1_577_SCREEN_COLS (_1_577_MODULE_COLS * _1_577_MODULE_PIXEL_COL) /* 屏幕每列像素数 */ +#define _1_577_BUFFER_SIZE (_1_577_SCREEN_ROWS * _1_577_SCREEN_COLS) +#define _1_577_TOTAL_CHANNELS (_1_577_MODULE_COLS * _1_577_CHANNELS_PER_MODULE) +#define _1_577_CHANNEL_PIXELS (_1_577_MODULE_PIXEL_ROW * _1_577_MODULE_PIXEL_COL * _1_577_MODULE_ROWS / _1_577_CHANNELS_PER_MODULE) +#define _1_577_SCAN_LINE_PX (_1_577_CHANNEL_PIXELS / _1_577_SCAN_LINES) + +/* ================================================================ + * hub75_buff 布局(1-577 左右半区) + * + * 每通道内按 scan line 连续存放;每条 scan line 内按模块横向拼接: + * 单模块 128 字节 = 左半 32 + 低/高间隔 32 + 右半 32 + 间隔 32 + * [0..31] = Y_high, X=0..31 + * [32..63] = Y_low, X=0..31 + * [64..95] = Y_high, X=32..63 + * [96..127] = Y_low, X=32..63 + * 后续模块各占 MODULE_BUFF_STRIDE 字节。 + * + * 行映射(1/8 扫描,每通道覆盖 ROWS_PER_CHANNEL 行): + * local_y < SCAN_LINES → Y_low (写入半区偏移 HALF_W) + * local_y >= SCAN_LINES → Y_high(写入半区偏移 0) + * ================================================================ */ + +#define _1_577_HALF_W (_1_577_MODULE_PIXEL_ROW / 2U) /* 32:左右半宽 */ +#define _1_577_MODULE_BUFF_STRIDE (_1_577_MODULE_PIXEL_ROW * 2U) /* 128:单模块 scan 占用 */ +#define _1_577_ROWS_PER_CHANNEL (_1_577_MODULE_PIXEL_COL / _1_577_CHANNELS_PER_MODULE) /* 16 */ + +/* ---- BSRR 预计算查表 ---- */ +typedef struct { + pl_hub75_bsrr_t r, g, b; +} _1_577_bsrr_t; + +[[gnu::section(".ccmram")]] static _1_577_bsrr_t g_bsrr[_1_577_TOTAL_CHANNELS][8]; + +/* ---- 1-577 实例 ---- */ +typedef struct { + dev_display_t me; +} dev_display_1_577_t; + +[[gnu::section(".ccmram")]] static uint8_t _1_577_pixel_map[_1_577_BUFFER_SIZE]; +[[gnu::section(".ccmram")]] static uint8_t _1_577_hub75_buff[_1_577_BUFFER_SIZE]; +[[gnu::section(".ccmram")]] static uint16_t _1_577_row_dst[_1_577_SCREEN_COLS]; + +static dev_display_1_577_t g_1_577 = { + .me = { + .ops = nullptr, /* 由 dev_display_1_577_init 设置 */ + .module_rows = _1_577_MODULE_PIXEL_ROW, + .module_cols = _1_577_MODULE_PIXEL_COL, + .channels_per_module = _1_577_CHANNELS_PER_MODULE, + .modules_per_row = _1_577_MODULE_ROWS, + .modules_per_col = _1_577_MODULE_COLS, + .scan_lines = _1_577_SCAN_LINES, + .screen_rows = _1_577_SCREEN_ROWS, + .screen_cols = _1_577_SCREEN_COLS, + .total_channels = _1_577_TOTAL_CHANNELS, + .channel_pixels = _1_577_CHANNEL_PIXELS, + .scan_line_pixels = _1_577_SCAN_LINE_PX, + .buffer_size = _1_577_BUFFER_SIZE, + .pixel_map = _1_577_pixel_map, + .hub75_buff = _1_577_hub75_buff, + .module_code = MODULE_CODE, + .light_level = DEV_DISPLAY_BRIGHTNESS_MAX, + }, +}; + +dev_display_t *dev_display_1_577_get(void) +{ + return &g_1_577.me; +} + +/* ================================================================ + * prepare: pixel_map → hub75_buff 像素重排 + * + * pixel_map[] 按行优先存储(y * screen_rows + x), + * hub75_buff[] 按 1-577 左右半区物理移位顺序组织。 + * 行起始偏移由 init 预计算到 _1_577_row_dst[],此处只做拷贝。 + * ================================================================ */ + +static void _1_577_prepare(dev_display_t *dev) +{ + const uint16_t screen_rows = dev->screen_rows; + const uint16_t screen_cols = dev->screen_cols; + const uint8_t *pixel_map = dev->pixel_map; + uint8_t *hub75_buff = dev->hub75_buff; + + for (uint16_t row = 0; row < screen_cols; row++) { + const uint8_t *src = pixel_map + (uint16_t)(row * screen_rows); + uint8_t *dst_base = hub75_buff + _1_577_row_dst[row]; + + for (uint16_t m = 0; m < _1_577_MODULE_ROWS; m++) { + uint8_t *dst = dst_base + m * _1_577_MODULE_BUFF_STRIDE; + const uint8_t *s = src + m * _1_577_MODULE_PIXEL_ROW; + + for (uint16_t x = 0; x < _1_577_HALF_W; x++) { + dst[x] = s[x]; /* 左半 X=0..31 */ + dst[_1_577_MODULE_PIXEL_ROW + x] = s[_1_577_HALF_W + x]; /* 右半 X=32..63 */ + } + } + } +} + +/* ================================================================ + * scan: 逐像素输出 — BSRR 查表 + CLK 脉冲 + * + * hub75_buff[] 存储的是颜色索引 (0~7),扫描时查 g_bsrr + * 得到该通道、该颜色的 R/G/B 三组 {port, BSRR_val},直接 flush 输出。 + * ================================================================ */ + +static inline void _1_577_scan(dev_display_t *dev, uint8_t line) +{ + const uint16_t scan_line_pixels = dev->scan_line_pixels; + const uint16_t channel_pixels = dev->channel_pixels; + const uint8_t total_channels = dev->total_channels; + const _1_577_bsrr_t (*bsrr)[8] = g_bsrr; + const uint8_t *base = dev->hub75_buff + (uint16_t)line * scan_line_pixels; + + for (uint16_t pixel = 0; pixel < scan_line_pixels; pixel++, base++) { + const uint8_t *channel_ptr = base; + for (uint8_t ch = 0; ch < total_channels; ch++) { + uint8_t color = channel_ptr[(uint16_t)channel_pixels * ch]; + const _1_577_bsrr_t *entry = &bsrr[ch][color]; + pl_hub75_bsrr_flush(&entry->r); + pl_hub75_bsrr_flush(&entry->g); + pl_hub75_bsrr_flush(&entry->b); + } + pl_hub75_clock_pulse(); + } +} + +/* ---- set_row: RUC7258E 译码器型行驱动 ---- */ +static void _1_577_set_row(uint8_t row) +{ + pl_hub75_Decoder_set_row(row); +} + +/* ---- ops 虚表 ---- */ +static const dev_display_ops_t _1_577_ops = { + .prepare = _1_577_prepare, + .scan = _1_577_scan, + .set_row = _1_577_set_row, +}; + +/* ================================================================ + * dev_display_1_577_init: 预计算 row_dst / BSRR 查表 + 绑定 ops + * + * _1_577_row_dst[y] = 该逻辑行在 hub75_buff 中的起始偏移(含高低半区)。 + * g_bsrr[ch][c] 存储通道 ch 在颜色 c (0~7) 时的 R/G/B 引脚输出值。 + * ================================================================ */ + +void dev_display_1_577_init(void) +{ + g_1_577.me.ops = &_1_577_ops; + dev_display_register(&g_1_577.me); + + for (uint16_t row = 0; row < _1_577_SCREEN_COLS; row++) { + const uint16_t ch = (uint16_t)(row / _1_577_ROWS_PER_CHANNEL); + const uint16_t local_y = (uint16_t)(row % _1_577_ROWS_PER_CHANNEL); + const uint16_t line = (uint16_t)(local_y % _1_577_SCAN_LINES); + const uint16_t row_type = (local_y < _1_577_SCAN_LINES) ? 1U : 0U; /* 1=low, 0=high */ + _1_577_row_dst[row] = + (uint16_t)(ch * _1_577_CHANNEL_PIXELS + line * _1_577_SCAN_LINE_PX + + row_type * _1_577_HALF_W); + } + + for (uint8_t ch = 0; ch < g_1_577.me.total_channels; ch++) { + const uint32_t r_on = (uint32_t)g_hub75_pin_r[ch].pin; + const uint32_t g_on = (uint32_t)g_hub75_pin_g[ch].pin; + const uint32_t b_on = (uint32_t)g_hub75_pin_b[ch].pin; + const uint32_t r_off = r_on << 16; + const uint32_t g_off = g_on << 16; + const uint32_t b_off = b_on << 16; + + for (uint8_t color = 0; color < 8; color++) { + g_bsrr[ch][color].r.port = g_hub75_pin_r[ch].port; + g_bsrr[ch][color].r.val = (color & 1U) ? r_on : r_off; + g_bsrr[ch][color].g.port = g_hub75_pin_g[ch].port; + g_bsrr[ch][color].g.val = (color & 2U) ? g_on : g_off; + g_bsrr[ch][color].b.port = g_hub75_pin_b[ch].port; + g_bsrr[ch][color].b.val = (color & 4U) ? b_on : b_off; + } + } +} +hw_dev_initcall(dev_display_1_577_init); diff --git a/Device/Display/dev_display_1_969.c b/Device/Display/dev_display_1_969.c new file mode 100644 index 0000000..7afbf8d --- /dev/null +++ b/Device/Display/dev_display_1_969.c @@ -0,0 +1,216 @@ +/** + * @file dev_display_1_969.c + * @brief 1-969 模组派生类型 — 1/8扫描、2 通道、R/G/B 分离 + * + * 实现 dev_display_ops: prepare (pixel_map→hub75_buff) + scan (BSRR 查表输出) + * g_bsrr 查表放在 CCMRAM 中,避免 Flash 读取和分支。 + */ + +#include "dev_display.h" + +#include +#include "initcall.h" + +/* ================================================================ + * 1-969 模组参数 + * ================================================================ */ +#define MODULE_CODE "1000000969" + +#define _1_969_MODULE_ROWS (3U) /* 每行模块数 */ +#define _1_969_MODULE_COLS (3U) /* 每列模块数 */ +#define _1_969_MODULE_PIXEL_ROW (64U) /* 单模块每行的像素个数 */ +#define _1_969_MODULE_PIXEL_COL (32U) /* 单模块每列的像素个数 */ +#define _1_969_CHANNELS_PER_MODULE (2U) /* 每模块通道数(R1G1B1 + R2G2B2) */ +#define _1_969_SCAN_LINES (8U) /* 1/8 扫描 */ + +/* ---- 派生参数(由模组参数计算,勿手动修改) ---- */ +#define _1_969_SCREEN_ROWS (_1_969_MODULE_ROWS * _1_969_MODULE_PIXEL_ROW) // 屏幕每行的像素个数 +#define _1_969_SCREEN_COLS (_1_969_MODULE_COLS * _1_969_MODULE_PIXEL_COL) // 屏幕每列的像素个数 +#define _1_969_BUFFER_SIZE (_1_969_SCREEN_ROWS * _1_969_SCREEN_COLS) +#define _1_969_TOTAL_CHANNELS (_1_969_MODULE_COLS * _1_969_CHANNELS_PER_MODULE) +#define _1_969_CHANNEL_PIXELS (_1_969_MODULE_PIXEL_ROW * _1_969_MODULE_PIXEL_COL * _1_969_MODULE_ROWS / _1_969_CHANNELS_PER_MODULE) +#define _1_969_SCAN_LINE_PX (_1_969_CHANNEL_PIXELS / _1_969_SCAN_LINES) + +/* ================================================================ + * hub75_buff 布局 + * + * HUB75 协议以"组"为单位移位输出,每组 16 字节对应 4×4 像素块。 + * 屏幕 16×16 像素 → 4×4 组,每组覆盖 4 行 × 4 列。 + * + * 每个组 16 字节 = 4 行 × 4 列,每组内布局: + * 偏移 row%4 col: 0 1 2 3 + * ───── ───── ─────────────────── + * 0..3 0 6 4 2 0 ← 行镜像 (3 - col) + * 4..7 1 7 5 3 1 ← 行镜像 + 偏移 1 + * 8..11 2 8 10 12 14 ← 行正序 + 偏移 9 + * 12..15 3 9 11 13 15 ← 行正序 + 偏移 8 + * + * 每组 2 字节一组(对应 R/G/B 三色),每组内 4 行的偏移规律 + * 由 HUB75 驱动芯片的数据输入格式决定。 + * ================================================================ */ + +#define GROUP_PIXEL_W 4 /* 每组覆盖的列数 */ +#define GROUP_PIXEL_H 4 /* 每组覆盖的行数 */ +#define GROUP_SIZE 16 /* 每组的字节数 = GROUP_PIXEL_W × GROUP_PIXEL_H */ + +/* 组内行偏移的基准值 */ +#define GROUP_ROW0_OFFSET 0 /* row%4 == 0: 双字节对偏移 0 */ +#define GROUP_ROW1_OFFSET 1 /* row%4 == 1: 双字节对偏移 1 */ +#define GROUP_ROW2_OFFSET 9 /* row%4 == 2: 双字节对偏移 9 */ +#define GROUP_ROW3_OFFSET 8 /* row%4 == 3: 双字节对偏移 8 */ + +#define GROUP_COLS_PER_ROW (_1_969_MODULE_ROWS * GROUP_PIXEL_W) /* 每行模块的水平组数 */ + +/* ---- BSRR 预计算查表 ---- */ +typedef struct { + pl_hub75_bsrr_t r, g, b; +} _1_969_bsrr_t; + +[[gnu::section(".ccmram")]] static _1_969_bsrr_t g_bsrr[_1_969_TOTAL_CHANNELS][8]; + +/* ---- 1-969 实例 ---- */ +typedef struct { + dev_display_t me; +} dev_display_1_969_t; + +[[gnu::section(".ccmram")]] static uint8_t _1_969_pixel_map[_1_969_BUFFER_SIZE]; +[[gnu::section(".ccmram")]] static uint8_t _1_969_hub75_buff[_1_969_BUFFER_SIZE]; +[[gnu::section(".ccmram")]] static uint16_t _1_969_row_dst[_1_969_SCREEN_COLS]; + +static dev_display_1_969_t g_1_969 = { + .me = { + .ops = nullptr, /* 由 dev_display_1_969_init 设置 */ + .module_rows = _1_969_MODULE_PIXEL_ROW, + .module_cols = _1_969_MODULE_PIXEL_COL, + .channels_per_module = _1_969_CHANNELS_PER_MODULE, + .modules_per_row = _1_969_MODULE_ROWS, + .modules_per_col = _1_969_MODULE_COLS, + .scan_lines = _1_969_SCAN_LINES, + .screen_rows = _1_969_SCREEN_ROWS, + .screen_cols = _1_969_SCREEN_COLS, + .total_channels = _1_969_TOTAL_CHANNELS, + .channel_pixels = _1_969_CHANNEL_PIXELS, + .scan_line_pixels = _1_969_SCAN_LINE_PX, + .buffer_size = _1_969_BUFFER_SIZE, + .pixel_map = _1_969_pixel_map, + .hub75_buff = _1_969_hub75_buff, + .module_code = MODULE_CODE, + .light_level = DEV_DISPLAY_BRIGHTNESS_MAX, + }, +}; + +dev_display_t *dev_display_1_969_get(void) +{ + return &g_1_969.me; +} + +/* ================================================================ + * prepare: pixel_map → hub75_buff 像素重排 + * + * pixel_map[] 按行优先存储(y * screen_rows + x), + * hub75_buff[] 按"组"组织以匹配 HUB75 移位寄存器输入时序。 + * 此函数将像素值从逻辑坐标映射到硬件输出缓冲。 + * ================================================================ */ + +static void _1_969_prepare(dev_display_t *dev) +{ + const uint16_t screen_rows = dev->screen_rows; + const uint16_t screen_cols = dev->screen_cols; + const uint8_t *pixel_map = dev->pixel_map; + uint8_t *hub75_buff = dev->hub75_buff; + + for (uint16_t row = 0; row < screen_cols; row++) { + const uint8_t *src = pixel_map + (uint16_t)(row * screen_rows); + uint8_t *dst = hub75_buff + _1_969_row_dst[row]; + + for (uint16_t col = 0; col < screen_rows; col++) { + *dst = *src++; + dst += 2U; + } + } +} + +/* ================================================================ + * scan: 逐像素输出 — BSRR 查表 + CLK 脉冲 + * + * hub75_buff[] 存储的是颜色索引 (0~7),扫描时查 g_bsrr + * 得到该通道、该颜色的 R/G/B 三组 {port, BSRR_val},直接 flush 输出。 + * ================================================================ */ + +static inline void _1_969_scan(dev_display_t *dev, uint8_t line) +{ + const uint16_t scan_line_pixels = dev->scan_line_pixels; + const uint16_t channel_pixels = dev->channel_pixels; + const uint8_t total_channels = dev->total_channels; + const _1_969_bsrr_t (*bsrr)[8] = g_bsrr; + const uint8_t *base = dev->hub75_buff + (uint16_t)line * scan_line_pixels; + + for (uint16_t pixel = 0; pixel < scan_line_pixels; pixel++, base++) { + const uint8_t *channel_ptr = base; + for (uint8_t ch = 0; ch < total_channels; ch++) { + uint8_t color = channel_ptr[(uint16_t)channel_pixels * ch]; + const _1_969_bsrr_t *entry = &bsrr[ch][color]; + pl_hub75_bsrr_flush(&entry->r); + pl_hub75_bsrr_flush(&entry->g); + pl_hub75_bsrr_flush(&entry->b); + } + pl_hub75_clock_pulse(); + } +} + +/* ---- set_row: RUC7258E 译码器型行驱动 ---- */ +static void _1_969_set_row(uint8_t row) +{ + pl_hub75_Decoder_set_row(row); +} + +/* ---- ops 虚表 ---- */ +static const dev_display_ops_t _1_969_ops = { + .prepare = _1_969_prepare, + .scan = _1_969_scan, + .set_row = _1_969_set_row, +}; + +/* ================================================================ + * dev_display_1_969_init: 预计算 BSRR 查表 + 绑定 ops + * + * g_bsrr[ch][c] 存储通道 ch 在颜色 c (0~7) 时的 R/G/B 引脚输出值。 + * 颜色 c 的 bit0→R, bit1→G, bit2→B: + * - R 亮: (c & 1) != 0 → BSRR 置位(高电平) + * - R 灭: (c & 1) == 0 → BSRR 复位(低电平) + * - G 亮: (c & 2) != 0, B 亮: (c & 4) != 0 + * + * 预计算避免扫描热路径中的分支判断。 + * ================================================================ */ + +void dev_display_1_969_init(void) +{ + g_1_969.me.ops = &_1_969_ops; + dev_display_register(&g_1_969.me); + + for (uint16_t row = 0; row < _1_969_SCREEN_COLS; row++) { + const uint16_t group_index = (uint16_t)((row >> 4) * 8U + (row & 7U)); + const uint16_t row_base = (uint16_t)(group_index * _1_969_SCAN_LINE_PX); + const uint8_t row_offset = (uint8_t)(((row >> 3) & 1U) ? 0U : 1U); + _1_969_row_dst[row] = (uint16_t)(row_base + row_offset); + } + + for (uint8_t ch = 0; ch < g_1_969.me.total_channels; ch++) { + const uint32_t r_on = (uint32_t)g_hub75_pin_r[ch].pin; + const uint32_t g_on = (uint32_t)g_hub75_pin_g[ch].pin; + const uint32_t b_on = (uint32_t)g_hub75_pin_b[ch].pin; + const uint32_t r_off = r_on << 16; + const uint32_t g_off = g_on << 16; + const uint32_t b_off = b_on << 16; + + for (uint8_t color = 0; color < 8; color++) { + g_bsrr[ch][color].r.port = g_hub75_pin_r[ch].port; + g_bsrr[ch][color].r.val = (color & 1U) ? r_on : r_off; + g_bsrr[ch][color].g.port = g_hub75_pin_g[ch].port; + g_bsrr[ch][color].g.val = (color & 2U) ? g_on : g_off; + g_bsrr[ch][color].b.port = g_hub75_pin_b[ch].port; + g_bsrr[ch][color].b.val = (color & 4U) ? b_on : b_off; + } + } +} +hw_dev_initcall(dev_display_1_969_init); diff --git a/Device/Display/dev_display_old_p20.c b/Device/Display/dev_display_old_p20.c deleted file mode 100644 index 00d3bc7..0000000 --- a/Device/Display/dev_display_old_p20.c +++ /dev/null @@ -1,193 +0,0 @@ -/** - * @file dev_display_p20.c - * @brief P20 模组派生类型 — 静态扫描、2 通道、R/G/B 分离 - * - * 实现 dev_display_ops: prepare (pixel_map→hub75_buff) + scan (BSRR 查表输出) - * g_bsrr 查表放在 CCMRAM 中,避免 Flash 读取和分支。 - */ - -#include "dev_display.h" - -#include -#include "initcall.h" - -/* ================================================================ - * P20 模组参数 - * ================================================================ */ - -#define P20_MODULE_ROWS (2U) /* 每行模块数 */ -#define P20_MODULE_COLS (2U) /* 每列模块数 */ -#define P20_MODULE_PIXEL_ROW (24U) /* 单模块像素行数 */ -#define P20_MODULE_PIXEL_COL (12U) /* 单模块像素列数 */ -#define P20_CHANNELS_PER_MODULE (2U) /* 每模块通道数(R1G1B1 + R2G2B2) */ -#define P20_SCAN_LINES (1U) /* 静态扫描 */ - -/* ---- 派生参数(由模组参数计算,勿手动修改) ---- */ -#define P20_SCREEN_ROWS (P20_MODULE_ROWS * P20_MODULE_PIXEL_ROW) -#define P20_SCREEN_COLS (P20_MODULE_COLS * P20_MODULE_PIXEL_COL) -#define P20_PIXEL_MAP_SIZE (P20_SCREEN_ROWS * P20_SCREEN_COLS) -#define P20_HUB75_BUF_SIZE (P20_SCREEN_ROWS * P20_MODULE_COLS * 16U) -// #define P20_TOTAL_CHANNELS (P20_MODULE_COLS * P20_CHANNELS_PER_MODULE) -// #define P20_CHANNEL_PIXELS (P20_MODULE_PIXEL_ROW * P20_MODULE_ROWS * 8U) -// #define P20_SCAN_LINE_PX (P20_CHANNEL_PIXELS / P20_SCAN_LINES) -#define P20_TOTAL_CHANNELS (8U) -#define P20_CHANNEL_PIXELS (192U) -#define P20_SCAN_LINE_PX (192U) - -/* ---- BSRR 预计算查表 ---- */ -typedef struct { - pl_hub75_bsrr_t r, g, b; -} p20_bsrr_t; - -[[gnu::section(".ccmram")]] static p20_bsrr_t g_bsrr[P20_TOTAL_CHANNELS][8]; - -/* ---- P20 实例 ---- */ -typedef struct { - dev_display_t me; -} dev_display_p20_t; - -[[gnu::section(".ccmram")]] static uint8_t p20_pixel_map[P20_PIXEL_MAP_SIZE]; -[[gnu::section(".ccmram")]] static uint8_t p20_hub75_buff[P20_HUB75_BUF_SIZE]; - -static dev_display_p20_t g_p20 = { - .me = { - .ops = nullptr, /* 由 dev_display_p20_init 设置 */ - .module_rows = P20_MODULE_PIXEL_ROW, - .module_cols = P20_MODULE_PIXEL_COL, - .channels_per_module = P20_CHANNELS_PER_MODULE, - .modules_per_row = P20_MODULE_ROWS, - .modules_per_col = P20_MODULE_COLS, - .scan_lines = P20_SCAN_LINES, - .screen_rows = P20_SCREEN_ROWS, - .screen_cols = P20_SCREEN_COLS, - .total_channels = P20_TOTAL_CHANNELS, - .channel_pixels = P20_CHANNEL_PIXELS, - .scan_line_pixels = P20_SCAN_LINE_PX, - .buffer_size = P20_PIXEL_MAP_SIZE, - .pixel_map = p20_pixel_map, - .hub75_buff = p20_hub75_buff, - .light_level = 1, - }, -}; - -dev_display_t *dev_display_p20_get(void) -{ - return &g_p20.me; -} - -/* ================================================================ - * prepare: pixel_map → hub75_buff 像素重排 - * - * pixel_map[] 按行优先存储(y * screen_rows + x), - * hub75_buff[] 按"组"组织以匹配 HUB75 移位寄存器输入时序。 - * 此函数将像素值从逻辑坐标映射到硬件输出缓冲。 - * ================================================================ */ - -static void _p20_prepare(dev_display_t *dev) -{ - int32_t group_index = 0; - int32_t group_row = 0, group_col = 0; - int32_t pixel_col = 0, pixel_row = 0; - - for (int32_t linear = 0; linear < (int32_t)dev->buffer_size; linear++) { - pixel_col = linear % (int32_t)dev->screen_rows; /* 行优先: x 先递增 */ - pixel_row = linear / (int32_t)dev->screen_rows; - - group_row = pixel_row % 12 / 4 + (pixel_row % 12 / 4 + 1) / 4; - group_col = pixel_col % 24 / 4; - group_index = (1 - group_row % 2) * 6 + group_row / 2 * 2 * 6 + group_col % 6; - - int32_t col = pixel_col % 24 % 4, row = pixel_row % 12 % 4; - int32_t res = (col + (1 - row) * 4) * (1 - row / 2) + ((3 - col) + row * 4) * (row / 2); - - uint16_t offset = 0; - if ((pixel_col < 24) && (pixel_row < 12)) - offset = 0 * 384; - else if ((pixel_col >= 24) && (pixel_row < 12)) - offset = 1 * 384; - else if ((pixel_col < 24) && (pixel_row >= 12)) - offset = 2 * 384; - else if ((pixel_col >= 24) && (pixel_row >= 12)) - offset = 3 * 384; - - dev->hub75_buff[res + (group_index) * 16 + offset] = dev->pixel_map[linear]; - } -} - -/* ================================================================ - * scan: 逐像素输出 — BSRR 查表 + CLK 脉冲 - * - * hub75_buff[] 存储的是颜色索引 (0~7),扫描时查 g_bsrr - * 得到该通道、该颜色的 R/G/B 三组 {port, BSRR_val},直接 flush 输出。 - * ================================================================ */ - -static inline void _p20_scan(dev_display_t *dev, uint8_t line) -{ - for (uint16_t pixel = 0; pixel < dev->scan_line_pixels; pixel++) { - /* 当前像素在所有通道中的起始偏移 */ - uint16_t pixel_base = (uint16_t)line * dev->scan_line_pixels + pixel; - - /* 同一像素位置同时输出所有通道的颜色数据 */ - for (uint8_t ch = 0; ch < dev->total_channels; ch++) { - uint8_t color = dev->hub75_buff[pixel_base + ch * dev->channel_pixels]; - p20_bsrr_t *bsrr = &g_bsrr[ch][color]; - pl_hub75_bsrr_flush(&bsrr->r); - pl_hub75_bsrr_flush(&bsrr->g); - pl_hub75_bsrr_flush(&bsrr->b); - } - - /* 锁存当前像素数据到移位寄存器 */ - pl_hub75_clock_pulse(); - } -} - -/* ---- set_row: ABCD 行地址编码 (静态扫描无需切换行) ---- */ -static void _p20_set_row(uint8_t row) -{ - pl_hub75_set_row(row); -} - -/* ---- ops 虚表 ---- */ -static const dev_display_ops_t p20_ops = { - .prepare = _p20_prepare, - .scan = _p20_scan, - .set_row = _p20_set_row, -}; - -/* ================================================================ - * dev_display_p20_init: 预计算 BSRR 查表 + 绑定 ops - * - * g_bsrr[ch][c] 存储通道 ch 在颜色 c (0~7) 时的 R/G/B 引脚输出值。 - * 颜色 c 的 bit0→R, bit1→G, bit2→B: - * - R 亮: (c & 1) != 0 → BSRR 置位(高电平) - * - R 灭: (c & 1) == 0 → BSRR 复位(低电平) - * - G 亮: (c & 2) != 0, B 亮: (c & 4) != 0 - * - * 预计算避免扫描热路径中的分支判断。 - * ================================================================ */ - -void dev_display_p20_init(void) -{ - g_p20.me.ops = &p20_ops; - dev_display_register(&g_p20.me); - - for (uint8_t ch = 0; ch < g_p20.me.total_channels; ch++) { - for (uint8_t color = 0; color < 8; color++) { - /* R 通道: color bit0 决定亮灭 */ - g_bsrr[ch][color].r.port = g_hub75_pin_r[ch].port; - g_bsrr[ch][color].r.val = (color & 1) ? (uint32_t)g_hub75_pin_r[ch].pin /* 置位: 输出高 */ - : (uint32_t)g_hub75_pin_r[ch].pin << 16; /* 复位: 输出低 */ - - /* G 通道: color bit1 决定亮灭 */ - g_bsrr[ch][color].g.port = g_hub75_pin_g[ch].port; - g_bsrr[ch][color].g.val = (color & 2) ? (uint32_t)g_hub75_pin_g[ch].pin - : (uint32_t)g_hub75_pin_g[ch].pin << 16; - - /* B 通道: color bit2 决定亮灭 */ - g_bsrr[ch][color].b.port = g_hub75_pin_b[ch].port; - g_bsrr[ch][color].b.val = (color & 4) ? (uint32_t)g_hub75_pin_b[ch].pin - : (uint32_t)g_hub75_pin_b[ch].pin << 16; - } - } -} -hw_dev_initcall(dev_display_p20_init); diff --git a/Device/Display/dev_display_p20.c b/Device/Display/dev_display_p20.c index 3da9b7a..b2905a6 100644 --- a/Device/Display/dev_display_p20.c +++ b/Device/Display/dev_display_p20.c @@ -15,10 +15,10 @@ * P20 模组参数 * ================================================================ */ -#define P20_MODULE_ROWS (1U) /* 每行模块数 */ -#define P20_MODULE_COLS (1U) /* 每列模块数 */ -#define P20_MODULE_PIXEL_ROW (24U) /* 单模块像素行数 */ -#define P20_MODULE_PIXEL_COL (12U) /* 单模块像素列数 */ +#define P20_MODULE_ROWS (8U) /* 每行模块数 */ +#define P20_MODULE_COLS (4U) /* 每列模块数 */ +#define P20_MODULE_PIXEL_ROW (16U) /* 单模块像素行数 */ +#define P20_MODULE_PIXEL_COL (8U) /* 单模块像素列数 */ #define P20_CHANNELS_PER_MODULE (2U) /* 每模块通道数(R1G1B1 + R2G2B2) */ #define P20_SCAN_LINES (1U) /* 静态扫描 */ @@ -92,7 +92,7 @@ static dev_display_p20_t g_p20 = { .buffer_size = P20_BUFFER_SIZE, .pixel_map = p20_pixel_map, .hub75_buff = p20_hub75_buff, - .light_level = 1, + .light_level = DEV_DISPLAY_BRIGHTNESS_MAX, }, }; diff --git a/Device/IO/dev_light_sensor.c b/Device/IO/dev_light_sensor.c index ed0be4b..0f9790e 100644 --- a/Device/IO/dev_light_sensor.c +++ b/Device/IO/dev_light_sensor.c @@ -12,8 +12,22 @@ void dev_light_sensor_init(light_sensor_dev_t *dev, dev_display_t *display) { - dev->adc = pl_adc_get_handle(); - dev->display = display; + dev->adc = pl_adc_get_handle(); + dev->display = display; + dev->min_level = 1; + dev->max_level = DEV_DISPLAY_BRIGHTNESS_MAX; +} + +void dev_light_sensor_set_range(light_sensor_dev_t *dev, uint8_t min, uint8_t max) +{ + if (min < 1) min = 1; + if (min > DEV_DISPLAY_BRIGHTNESS_MAX) min = DEV_DISPLAY_BRIGHTNESS_MAX; + if (max < 1) max = 1; + if (max > DEV_DISPLAY_BRIGHTNESS_MAX) max = DEV_DISPLAY_BRIGHTNESS_MAX; + if (min > max) min = max; + + dev->min_level = min; + dev->max_level = max; } uint8_t dev_light_sensor_read(light_sensor_dev_t *dev) @@ -30,11 +44,15 @@ uint8_t dev_light_sensor_read(light_sensor_dev_t *dev) if (temp_val > 4000) temp_val = 4000; /* ADC 值越大 = 光越暗 (LDR 分压: 光强↓ → 电阻↑ → 电压↑) - 映射: ADC 0(最亮) → level 7, ADC 3000+(最暗) → level 1 - display 支持 0~7 (0=关, 7=最亮),这里只出 1~7(不关屏) */ + 映射: ADC 0(最亮) → level 8, ADC 4000(最暗) → level 0 */ uint8_t level = 8 - (uint8_t)(temp_val / 500); if (level < 1) level = 1; - if (level > 7) level = 7; + if (level > DEV_DISPLAY_BRIGHTNESS_MAX) level = DEV_DISPLAY_BRIGHTNESS_MAX; + + /* 按配置范围钳位 */ + if (level < dev->min_level) level = dev->min_level; + if (level > dev->max_level) level = dev->max_level; + return level; } diff --git a/Device/Inc/config_info.h b/Device/Inc/config_info.h deleted file mode 100644 index 2014bc2..0000000 --- a/Device/Inc/config_info.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include "main.h" - -// ַ볣 -#define ADDR_CONFIG_SECTOR 0x08004000 -#define ADDR_RECOVERY_APP 0x08008000 -#define ADDR_MAIN_APP 0x08040000 - -#define CONFIG_MAGIC 0x0d000721 - -// -__attribute__((aligned(4))) typedef struct { - uint8_t ip[4]; - uint8_t mask[4]; - uint8_t gw[4]; - uint32_t port; -} NetConfig_t; - -// main appϢ -__attribute__((aligned(4))) typedef struct { - uint32_t size; // Main Appֽڳ - uint32_t crc32; // Main AppCRC32ֵ - char version[32]; -} FWInfo_t; - -// Ӧö0x08004000 -__attribute__((aligned(4))) typedef struct { - uint32_t magic; // ħжǷЧ - uint32_t update_sta; // ״̬ - FWInfo_t app_info; // main_app״̬ - NetConfig_t net_cfg; // - uint32_t config_crc; // ṹУ -} SysInfo_t; - -// ״̬ -typedef enum { - updated = 0, - updating = 1, - failed = 2, -} UpdateSta_t; - -bool Is_Config_Empty(volatile const SysInfo_t *info); -void Init_Config_Info(SysInfo_t *info); -void Edit_Config_Info(SysInfo_t *info); -HAL_StatusTypeDef EraseConfigInfo(void); -HAL_StatusTypeDef WriteConfigInfo(SysInfo_t *info); diff --git a/Device/Inc/dev_display.h b/Device/Inc/dev_display.h index ec3cbf9..82d2de4 100644 --- a/Device/Inc/dev_display.h +++ b/Device/Inc/dev_display.h @@ -58,6 +58,7 @@ struct dev_display { uint8_t *hub75_buff; /* 运行时 */ + const char *module_code; /* 模组编码字符串,由派生模组绑定 */ volatile uint8_t light_level; volatile bool dirty; }; @@ -70,20 +71,36 @@ void dev_display_init(void); /** @brief 软件初始化 (sw_dev_initcall): 创建 scan_task + 启动 TIM3/4 */ void dev_display_start(void); -/** @brief 设置单个像素颜色,置脏标记 */ +/** @brief 设置单个像素颜色,置脏标记 + * + * 坐标约定: x 为水平方向 (0..screen_rows-1), y 为垂直方向 (0..screen_cols-1)。 + * 注意: screen_rows 是每行像素数(宽度), screen_cols 是每列像素数(高度)。 + * pixel_map 按行主序存储: pixel_map[y * screen_rows + x] */ void dev_display_set_pixel(dev_display_t *dev, uint16_t x, uint16_t y, display_color_t color); -/** @brief 矩形区域填充纯色: (x,y)起点, w宽h高, 超出屏幕自动截断, 置脏标记 */ +/** @brief 矩形区域填充纯色: (x,y)起点, w宽h高, 置脏标记 + * + * 坐标约定同 dev_display_set_pixel。 + * 越界语义:起点在屏幕外(x>=rows 或 y>=cols)→ 整区域丢弃不绘制; + * 部分超出 → 截断到屏幕边界。 */ void dev_display_fill(dev_display_t *dev, uint16_t x, uint16_t y, uint16_t w, uint16_t h, display_color_t color); /** @brief 叠加绘制位图: (x,y)起点, w宽h高, bitmap每行( (w+7)/8 )字节, bit=1写color, bit=0不改变原像素。 - * 如需不透明绘制(bit=0置黑), 调用方先 dev_display_fill 填充背景色 */ + * 如需不透明绘制(bit=0置黑), 调用方先 dev_display_fill 填充背景色 + * + * 坐标约定同 dev_display_set_pixel */ void dev_display_draw_bitmap(dev_display_t *dev, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *bitmap, display_color_t color); -/** @brief 获取 P20 模组显示实例 */ -dev_display_t *dev_display_p20_get(void); +/** @brief 获取 1-969 模组显示实例 */ +dev_display_t *dev_display_1_969_get(void); + +/** @brief 获取 1-260 模组显示实例 */ +dev_display_t *dev_display_1_260_get(void); + +/** @brief 获取 1-577 模组显示实例 */ +dev_display_t *dev_display_1_577_get(void); /** @brief 注册活动显示实例(由显示模组的 hw_dev_initcall 调用) */ void dev_display_register(dev_display_t *dev); @@ -91,5 +108,14 @@ void dev_display_register(dev_display_t *dev); /** @brief 获取当前活动显示实例 */ dev_display_t *dev_display_get(void); -/** @brief 设置亮度 (0=最暗/关闭, 7=最亮),PWM 粒度 1/8 */ +extern volatile uint32_t g_dev_display_commit_count; +extern volatile uint32_t g_dev_display_scan_count; + +/** @brief 将当前逻辑帧提交到扫描帧,供 scan_task 读取 */ +void dev_display_commit_frame(dev_display_t *dev); + +/** 硬件亮度上限:0=关闭,8=OE 100% 常亮(PWM 8 档,最亮) */ +#define DEV_DISPLAY_BRIGHTNESS_MAX 8U + +/** @brief 设置亮度 (0=最暗/关闭, 8=最亮),PWM 粒度 1/8 */ void dev_display_set_brightness(dev_display_t *dev, uint8_t level); diff --git a/Device/Inc/dev_light_sensor.h b/Device/Inc/dev_light_sensor.h index 01508c6..436164e 100644 --- a/Device/Inc/dev_light_sensor.h +++ b/Device/Inc/dev_light_sensor.h @@ -1,6 +1,9 @@ /** * @file dev_light_sensor.h * @brief 环境光传感器设备(ADC 采样 + 自动亮度调节) + * + * 自动调光输出范围由 min_level / max_level 限定, + * 可通过 dev_light_sensor_set_range() 在运行时动态调整。 */ #pragma once @@ -8,11 +11,30 @@ #include #include "dev_display.h" +/** 自动调光默认最低亮度等级(出厂约束:不允许低于 4 级) */ +#define LIGHT_SENSOR_MIN_LEVEL 4 + +/** 自动调光默认最高亮度等级(设备最亮 = OE 100%) */ +#define LIGHT_SENSOR_MAX_LEVEL DEV_DISPLAY_BRIGHTNESS_MAX + typedef struct { void *adc; dev_display_t *display; + uint8_t min_level; /* 自动调光最低亮度 (1~8) */ + uint8_t max_level; /* 自动调光最高亮度 (1~8) */ } light_sensor_dev_t; void dev_light_sensor_init(light_sensor_dev_t *dev, dev_display_t *display); uint8_t dev_light_sensor_read(light_sensor_dev_t *dev); void dev_light_sensor_auto_adjust(light_sensor_dev_t *dev); + +/** + * @brief 设置光敏自动调光的亮度范围 + * @param dev 光传感器实例 + * @param min 最低亮度等级 (1~8, 0=不关屏) + * @param max 最高亮度等级 (1~8) + * + * min/max 会被自动钳位到 1~DEV_DISPLAY_BRIGHTNESS_MAX,且 min ≤ max。 + * 调用后下一次 auto_adjust 即以新范围输出。 + */ +void dev_light_sensor_set_range(light_sensor_dev_t *dev, uint8_t min, uint8_t max); diff --git a/Device/Inc/dev_rs232.h b/Device/Inc/dev_rs232.h index 0608789..f04ab10 100644 --- a/Device/Inc/dev_rs232.h +++ b/Device/Inc/dev_rs232.h @@ -1,9 +1,13 @@ /** * @file dev_rs232.h - * @brief RS232 串口板级资源(USART3=RS232-0, USART6=RS232-1) + * @brief RS232 串口板级资源(USART3=RS232-0 协议 RX,USART6=语音 TX 无 DMA RX) */ #pragma once #include -uint8_t *dev_rs232_get_buf(uint8_t index); +/* 乒乓双缓冲单块大小(仅 index0):≥ QH 最大帧 259,且 ≤ RB_SIZE_RS232−1。 + * 实际 DMA 缓冲 = 2 × RS232_BUF_SIZE = 1280B(pl_uart circular 周期)。 */ +#define RS232_BUF_SIZE (640U) + +uint8_t *dev_rs232_get_buf(uint8_t index); \ No newline at end of file diff --git a/Device/Inc/dev_rs232_voice.h b/Device/Inc/dev_rs232_voice.h new file mode 100644 index 0000000..17f5030 --- /dev/null +++ b/Device/Inc/dev_rs232_voice.h @@ -0,0 +1,12 @@ +/** + * @file dev_rs232_voice.h + * @brief RS232 TTS 语音板封装 + */ +#pragma once + +#include + +#define DEV_RS232_VOICE_MAX_TEXT (200U) + +void dev_rs232_voice_play(const uint8_t *gbk_text, uint16_t len); +void dev_rs232_voice_volume(uint8_t level); diff --git a/Device/Inc/dev_rs485.h b/Device/Inc/dev_rs485.h index cd7281e..e375702 100644 --- a/Device/Inc/dev_rs485.h +++ b/Device/Inc/dev_rs485.h @@ -6,5 +6,9 @@ #include +/* 乒乓双缓冲单块大小:≥ RLS 最大帧 530,且 ≤ RB_SIZE_RS485−1(环缓废 1 字节)。 + * 实际 DMA 缓冲 = 2 × RS485_BUF_SIZE = 1280B(pl_uart circular 周期)。 */ +#define RS485_BUF_SIZE (640U) + void dev_rs485_init(void); -uint8_t *dev_rs485_get_buf(void); +uint8_t *dev_rs485_get_buf(void); \ No newline at end of file diff --git a/Device/Network/dev_dp83848.c b/Device/Network/dev_dp83848.c index a2244f1..67fb5b4 100644 --- a/Device/Network/dev_dp83848.c +++ b/Device/Network/dev_dp83848.c @@ -417,6 +417,7 @@ int32_t dev_dp83848_it_disable(dev_dp83848_obj_t *obj, uint32_t interrupt) */ int32_t dev_dp83848_it_clear(dev_dp83848_obj_t *obj, uint32_t interrupt) { + (void)interrupt; uint32_t readval = 0; int32_t status = DP83848_STATUS_OK; diff --git a/Device/Network/dev_eth.c b/Device/Network/dev_eth.c index b5c8d59..a4eb881 100644 --- a/Device/Network/dev_eth.c +++ b/Device/Network/dev_eth.c @@ -17,12 +17,10 @@ #include "pl_dwt.h" #include "initcall.h" -/* 板级默认网络配置: 192.168.1.100/24, 网关 192.168.1.1 */ -static const uint8_t g_default_ip[4] = {192, 168, 1, 100}; -// static const uint8_t g_default_ip[4] = {192, 168, 2, 100}; +/* 出厂默认网络配置: 192.168.114.200/24, 网关 192.168.114.1(与 Bootloader/Recovery 出厂默认一致) */ +static const uint8_t g_default_ip[4] = {192, 168, 114, 200}; static const uint8_t g_default_mask[4] = {255, 255, 255, 0}; -static const uint8_t g_default_gw[4] = {192, 168, 1, 1}; -// static const uint8_t g_default_gw[4] = {192, 168, 2, 1}; +static const uint8_t g_default_gw[4] = {192, 168, 114, 1}; /* DP83848 PHY 设备对象与 IO 上下文 */ static dev_dp83848_obj_t g_dp83848; diff --git a/Device/Storage/dev_w25qxx.c b/Device/Storage/dev_w25qxx.c index ea00877..27fccb5 100644 --- a/Device/Storage/dev_w25qxx.c +++ b/Device/Storage/dev_w25qxx.c @@ -88,6 +88,43 @@ static inline void _cs_high(void) static osEventFlagsId_t s_evt; static volatile bool s_ok; +/* ---- SPI 互斥锁 ---- + * 多协议任务并发 app_render 读字库会交错 SPI 帧(CS/命令/数据被打散)。 + * _read/_write/_erase 全程持锁串行;s_ok/s_evt/s_dma_bounce 共享完成标志 + * 随之串行。锁在 sw_dev_initcall(RTOS 后、协议任务创建前)创建: + * hw initcall 阶段 RTOS 未初始化无法创建互斥锁。 + * 持锁为 NULL 时(sw_dev 早期、单线程 init 上下文)直接放行。 */ +static osMutexId_t s_w25_mutex; + +void dev_w25qxx_rtos_init(void) +{ + s_w25_mutex = osMutexNew(NULL); +} +sw_dev_initcall(dev_w25qxx_rtos_init); + +static inline void _w25_lock(void) +{ + if (s_w25_mutex) + osMutexAcquire(s_w25_mutex, osWaitForever); +} + +static inline void _w25_unlock(void) +{ + if (s_w25_mutex) + osMutexRelease(s_w25_mutex); +} + +/* CCM 不可被 DMA 寻址;禁止把 CCM/栈缓冲直接交给 SPI DMA(经 s_dma_bounce) */ +#define CCMRAM_BASE 0x10000000u +#define CCMRAM_END 0x10010000u +static uint8_t s_dma_bounce[256]; /* 必须落在 SRAM .bss */ + +static bool _ptr_in_ccm(const void *p) +{ + uintptr_t a = (uintptr_t)p; + return (a >= CCMRAM_BASE) && (a < CCMRAM_END); +} + static void _dma_cb(void *ctx) { (void)ctx; @@ -95,11 +132,23 @@ static void _dma_cb(void *ctx) osEventFlagsSet(s_evt, 0x01); } +static int32_t _dma_recv(dev_w25qxx_t *self, uint8_t *dst, uint16_t len) +{ + s_ok = false; + if (pl_spi_receive_dma(self->spi, dst, len) != 0) + return -1; + while (!s_ok) + osDelay(1); + return 0; +} + /* ---- OPS 实现 ---- */ static int32_t _init(dev_storage_t *dev) { dev_w25qxx_t *self = (dev_w25qxx_t *)dev; self->spi = pl_spi_get_handle(); + if (!self->spi) + return -1; /* 复位(阻塞,无需RTOS) */ uint8_t rst[2] = {W25Q_RESET_ENABLE, W25Q_RESET_DEVICE}; @@ -127,9 +176,9 @@ static int32_t _init(dev_storage_t *dev) return 0; } -static int32_t _read(dev_storage_t *dev, uint32_t addr, uint8_t *buf, uint32_t len) +/* 内部:假设已持锁(_write 的读-改-写路径复用,避免自锁死锁) */ +static int32_t _read_locked(dev_w25qxx_t *self, uint32_t addr, uint8_t *buf, uint32_t len) { - dev_w25qxx_t *self = (dev_w25qxx_t *)dev; if (!buf || len == 0) return -1; if (!s_evt) { @@ -143,14 +192,40 @@ static int32_t _read(dev_storage_t *dev, uint32_t addr, uint8_t *buf, uint32_t l cmd[0] = W25Q_READ_CMD; _put_addr(cmd + 1, addr, self); - s_ok = false; + int32_t ret = -1; _cs_low(); - pl_spi_transmit(self->spi, cmd, (uint16_t)(al + 1)); - pl_spi_receive_dma(self->spi, buf, (uint16_t)len); - while (!s_ok) - osDelay(1); + if (pl_spi_transmit(self->spi, cmd, (uint16_t)(al + 1)) != 0) + goto out; + + if (!_ptr_in_ccm(buf)) { + if (_dma_recv(self, buf, (uint16_t)len) != 0) + goto out; + } else { + /* 目的在 CCM(显存等)→ 经 SRAM bounce 再 memcpy */ + uint32_t done = 0; + while (done < len) { + uint16_t chunk = (uint16_t)((len - done > sizeof(s_dma_bounce)) + ? sizeof(s_dma_bounce) + : (len - done)); + if (_dma_recv(self, s_dma_bounce, chunk) != 0) + goto out; + memcpy(buf + done, s_dma_bounce, chunk); + done += chunk; + } + } + ret = (int32_t)len; +out: _cs_high(); - return (int32_t)len; + return ret; +} + +static int32_t _read(dev_storage_t *dev, uint32_t addr, uint8_t *buf, uint32_t len) +{ + dev_w25qxx_t *self = (dev_w25qxx_t *)dev; + _w25_lock(); + int32_t r = _read_locked(self, addr, buf, len); + _w25_unlock(); + return r; } static int32_t _write_enable(dev_w25qxx_t *self) @@ -211,8 +286,11 @@ static int32_t _write(dev_storage_t *dev, uint32_t addr, const uint8_t *buf, uin dev_w25qxx_t *self = (dev_w25qxx_t *)dev; if (!buf || len == 0) return -1; - static uint8_t sec[4096]; - uint32_t w = 0; + static uint8_t sec[4096]; /* 共享缓冲,随锁串行 */ + + _w25_lock(); + int32_t ret = -1; + uint32_t w = 0; while (w < len) { uint32_t sa = addr - (addr % 4096); @@ -220,7 +298,9 @@ static int32_t _write(dev_storage_t *dev, uint32_t addr, const uint8_t *buf, uin uint32_t ch = 4096 - off; if (len - w < ch) ch = len - w; - _read(dev, sa, sec, 4096); + /* 持锁内调用内部无锁版本,避免自锁死锁 */ + if (_read_locked(self, sa, sec, 4096) < 0) + goto out; bool need = false; for (uint16_t i = off; i < off + ch; i++) @@ -243,11 +323,14 @@ static int32_t _write(dev_storage_t *dev, uint32_t addr, const uint8_t *buf, uin } memcpy(sec + off, buf + w, ch); - if (_write_no_check(self, sa, sec, 4096) != 0) return -1; + if (_write_no_check(self, sa, sec, 4096) != 0) goto out; w += ch; addr += ch; } - return 0; + ret = 0; +out: + _w25_unlock(); + return ret; } static int32_t _erase(dev_storage_t *dev, uint32_t addr, uint32_t len) @@ -255,6 +338,7 @@ static int32_t _erase(dev_storage_t *dev, uint32_t addr, uint32_t len) (void)len; dev_w25qxx_t *self = (dev_w25qxx_t *)dev; + _w25_lock(); _write_enable(self); uint8_t al = _addr_len(self); uint8_t cmd[5]; @@ -263,7 +347,9 @@ static int32_t _erase(dev_storage_t *dev, uint32_t addr, uint32_t len) _cs_low(); int32_t r = pl_spi_transmit(self->spi, cmd, (uint16_t)(al + 1)); _cs_high(); - return (r == 0) ? _wait_busy(self, 3000) : -1; + r = (r == 0) ? _wait_busy(self, 3000) : -1; + _w25_unlock(); + return r; } static uint32_t _capacity(dev_storage_t *dev) @@ -282,7 +368,7 @@ static const dev_storage_ops_t w25qxx_ops = { /* ---- 自动初始化 ---- */ void dev_w25qxx_init(void) { - g_w25qxx.me.ops = &w25qxx_ops; - _init(&g_w25qxx.me); + if (_init(&g_w25qxx.me) == 0) + g_w25qxx.me.ops = &w25qxx_ops; } hw_dev_initcall(dev_w25qxx_init); diff --git a/Kernel/Inc/ring_buffer.h b/Kernel/Inc/ring_buffer.h index 9209935..f734c11 100644 --- a/Kernel/Inc/ring_buffer.h +++ b/Kernel/Inc/ring_buffer.h @@ -25,6 +25,26 @@ static uint8_t name##_buf[sz]; \ ring_buffer_t name = {.data = name##_buf, .size = sz, .mutex = nullptr} +#define RB_DEFINE_CCM(name, sz) \ + static uint8_t name##_buf[sz] __attribute__((section(".ccmram"))); \ + ring_buffer_t name = {.data = name##_buf, .size = sz, .mutex = nullptr} + +/** + * @brief 协议 TU 提供通道 RB(weak getter;多协议同槽合并为单一静态体) + * + * 未编入任何 provide → 函数符号为 0,acquire 得 nullptr。 + * 编入 ≥1 个 provide → 链接器保留其中一个 getter(及其 static 缓冲)。 + * + * @note getter 须为裸标识符(如 rb_provide_rj45),勿传宏名(## 不二次展开)。 + */ +#define RB_PROVIDE_WEAK(getter, sz) \ + __attribute__((weak)) ring_buffer_t *getter(void) \ + { \ + static uint8_t getter##_buf[sz]; \ + static ring_buffer_t getter##_rb = {.data = getter##_buf, .size = (sz), .mutex = nullptr}; \ + return &getter##_rb; \ + } + /** @brief 环形缓冲区结构体 */ typedef struct { uint8_t *data; diff --git a/Kernel/Inc/text_cvt.h b/Kernel/Inc/text_cvt.h index d5a10cb..468392b 100644 --- a/Kernel/Inc/text_cvt.h +++ b/Kernel/Inc/text_cvt.h @@ -2,8 +2,8 @@ #include -/* ǷUTF8GBKUNICODE֮ת - ע⣺unicodeռôԴޣŻff_convertŵSDFLASH +/* 是否启用UTF8、GBK、UNICODE编码之间的转换 + 注意:unicode编码表占用大,若资源受限,建议优化ff_convert函数,将编码表放到SD卡或者FLASH中 */ #define TEXTCODEC_ENABLE 1 @@ -14,6 +14,10 @@ void StrToHex(const char *from, uint32_t fromSize, uint8_t *to, uint32_t *toSize #if (TEXTCODEC_ENABLE == 1) +/* 编码转换函数族约定: + * fromSize 入参:输入字节数 + * toSize 入参:输出缓冲容量(字节);出参:实际写出长度(字节) + * 输入/输出均做越界防护:输入不足一个完整码元或输出容量不足时安全截断终止。 */ void GBKToUTF8(const char *from, uint32_t fromSize, char *to, uint32_t *toSize); void UTF8ToGBK(const char *from, uint32_t fromSize, char *to, uint32_t *toSize); void GBKToUnicode(const char *from, uint32_t fromSize, char *to, uint32_t *toSize); diff --git a/Kernel/Src/text_cvt.c b/Kernel/Src/text_cvt.c index 065da88..9686cf0 100644 --- a/Kernel/Src/text_cvt.c +++ b/Kernel/Src/text_cvt.c @@ -1,10 +1,10 @@ #include "text_cvt.h" /** - * @brief ʮַתʮ'A' -> 0xA + * @brief 十六进制字符转十六进制数,例:'A' -> 0xA * - * @param chr Ҫתʮַ - * @return תֵ + * @param chr 要转换的十六进制字符 + * @return 转换后的值 */ uint8_t chr2hex(uint8_t chr) { @@ -21,10 +21,10 @@ uint8_t chr2hex(uint8_t chr) } /** - * @brief ʮתΪַʽ0x0A->'A' + * @brief 十六进制数转为字符串形式,例:0x0A->'A' * - * @param hex ҪתΧ0~F - * @return תֵ + * @param hex 要转换的数,范围0~F + * @return 转换后的值 */ uint8_t hex2chr(uint8_t hex) { @@ -38,12 +38,12 @@ uint8_t hex2chr(uint8_t hex) } /** - * @brief ʮֵתַ{0XAA,0XBB,0XCC} -> "AABBCC" + * @brief 十六进制面值转字符串,例:{0XAA,0XBB,0XCC} -> "AABBCC" * - * @param from תʮ - * @param fromSize תʮݴС - * @param to תַ - * @param toSize תַĴС + * @param from 待转换的十六进制数据 + * @param fromSize 待转换的十六进制数据大小 + * @param to 存放转换的字符串 + * @param toSize 存放转换的字符串的大小 */ void HexToStr(const uint8_t *from, uint32_t fromSize, char *to, uint32_t *toSize) { @@ -57,17 +57,17 @@ void HexToStr(const uint8_t *from, uint32_t fromSize, char *to, uint32_t *toSize fromSize--; } - *to = 0; // ӽ + *to = 0; // 添加结束符 *toSize = size; } /** - * @brief ʮַתֵ"AABBCC" -> {0XAA,0XBB,0XCC} + * @brief 十六进制字符串转数值,例:"AABBCC" -> {0XAA,0XBB,0XCC} * - * @param from תʮַ - * @param fromSize ַ - * @param to ַʮֵ - * @param toSize ַʮֵĴС + * @param from 待转换的十六进制字符串 + * @param fromSize 字符串长度 + * @param to 存放字符串的十六进制值 + * @param toSize 存放字符串的十六进制值的大小 */ void StrToHex(const char *from, uint32_t fromSize, uint8_t *to, uint32_t *toSize) { @@ -88,15 +88,19 @@ void StrToHex(const char *from, uint32_t fromSize, uint8_t *to, uint32_t *toSize static uint16_t convert_encoding(uint16_t input_char, bool is_oem_to_unicode); /** - * @brief GBKתUTF8 + * @brief GBK码转UTF8码 * - * @param from GBK - * @param fromSize GBKĴС - * @param to UTF8 - * @param toSize UTF8ĴС + * 输入/输出越界安全:GBK 高字节残留不足 2 字节时安全截断终止(不越界读); + * 输出写入前检查剩余容量,不足即终止。 + * + * @param from GBK码 + * @param fromSize GBK码字节数 + * @param to UTF8码输出缓冲 + * @param toSize 入参:输出缓冲容量(字节);出参:实际写出长度(字节) */ void GBKToUTF8(const char *from, uint32_t fromSize, char *to, uint32_t *toSize) { + uint32_t capacity = *toSize; uint32_t unicode; uint32_t utfcode; uint32_t size = 0; @@ -105,74 +109,100 @@ void GBKToUTF8(const char *from, uint32_t fromSize, char *to, uint32_t *toSize) if (*from < 0X80) { // ASCII unicode = *from++; fromSize--; - } else { // GBK - unicode = (from[0] << 8) | from[1]; // һGBKַռֽ(ģʽ) + } else { // GBK + if (fromSize < 2) + break; // 输入截断:不足一个 GBK 字符,终止(防 fromSize 无符号下溢越界读) + unicode = (from[0] << 8) | from[1]; // 一个GBK字符占用两个字节(大端模式) unicode = convert_encoding(unicode, 1); // to unicode from += 2; fromSize -= 2; } - if (unicode < 0X80U) { // ASCII + if (unicode < 0X80U) { // ASCII → 1 字节 + if (size + 1 > capacity) + break; *to = unicode; to++; size++; - } else { // NOT ASCII - if (unicode >= 0X80U && unicode <= 0X7FFU) { - utfcode = 0XC080U | (unicode & 0X3FU) | (((unicode >> 6) & 0X1FU) << 8); - to[1] = utfcode & 0XFF; - to[0] = (utfcode >> 8) & 0XFF; - to += 2; - size += 2; + } else if (unicode <= 0X7FFU) { // → 2 字节 + if (size + 2 > capacity) + break; + utfcode = 0XC080U | (unicode & 0X3FU) | (((unicode >> 6) & 0X1FU) << 8); + to[1] = utfcode & 0XFF; + to[0] = (utfcode >> 8) & 0XFF; + to += 2; + size += 2; - } else if (unicode >= 0X800U && unicode <= 0XFFFFU) { - utfcode = 0XE08080U | (unicode & 0X3FU) | (((unicode >> 6) & 0X3FU) << 8) | (((unicode >> 12) & 0XFU) << 16); - to[2] = utfcode & 0XFF; - to[1] = (utfcode >> 8) & 0XFF; - to[0] = (utfcode >> 16) & 0XFF; - to += 3; - size += 3; + } else if (unicode <= 0XFFFFU) { // → 3 字节 + if (size + 3 > capacity) + break; + utfcode = 0XE08080U | (unicode & 0X3FU) | (((unicode >> 6) & 0X3FU) << 8) | (((unicode >> 12) & 0XFU) << 16); + to[2] = utfcode & 0XFF; + to[1] = (utfcode >> 8) & 0XFF; + to[0] = (utfcode >> 16) & 0XFF; + to += 3; + size += 3; - } else if (unicode >= 0X10000U && unicode <= 0X10FFFFU) { - utfcode = 0XF0808080U | ((unicode & 0X3FU)) | (((unicode >> 6) & 0X3FU) << 8) | (((unicode >> 12) & 0X3FU) << 16) | (((unicode >> 18) & 0X7U) << 24); - to[3] = utfcode & 0XFF; - to[2] = (utfcode >> 8) & 0XFF; - to[1] = (utfcode >> 16) & 0XFF; - to[0] = (utfcode >> 24) & 0XFF; - to += 4; - size += 4; + } else if (unicode <= 0X10FFFFU) { // → 4 字节 + if (size + 4 > capacity) + break; + utfcode = 0XF0808080U | ((unicode & 0X3FU)) | (((unicode >> 6) & 0X3FU) << 8) | (((unicode >> 12) & 0X3FU) << 16) | (((unicode >> 18) & 0X7U) << 24); + to[3] = utfcode & 0XFF; + to[2] = (utfcode >> 8) & 0XFF; + to[1] = (utfcode >> 16) & 0XFF; + to[0] = (utfcode >> 24) & 0XFF; + to += 4; + size += 4; - } else { - break; // ERROR: not support - } + } else { + break; // ERROR: not support } } *toSize = size; } /** - * @brief UTF8תGBK + * @brief UTF8码转GBK码 * - * @param from UTF8 - * @param fromSize UTF8ĴС - * @param to GBK - * @param toSize GBKС + * 输入/输出越界安全:多字节序列按首字节判断 2/3/4 字节长度, + * 输入不足完整序列时安全截断终止(不越界读); + * 输出写入前检查剩余容量,不足即终止。 + * + * @param from UTF8码 + * @param fromSize UTF8码字节数 + * @param to GBK码输出缓冲 + * @param toSize 入参:输出缓冲容量(字节);出参:实际写出长度(字节) */ void UTF8ToGBK(const char *from, uint32_t fromSize, char *to, uint32_t *toSize) { + uint32_t capacity = *toSize; uint32_t unicode; uint32_t utfcode; uint32_t size = 0; while (fromSize != 0) { + utfcode = 0; if (*from < 0X80) { // ASCII utfcode = *from; from++; fromSize--; - } else { // UTF8 - utfcode = (from[0] << 16) | (from[1] << 8) | from[2]; // UTF8ֽڱʾһĺ(ģʽ) - from += 3; - fromSize -= 3; + } else { // UTF8 多字节:按首字节确定序列长度,不足即截断终止 + uint8_t seq_len; + if ((*from & 0XE0U) == 0XC0U) + seq_len = 2; + else if ((*from & 0XF0U) == 0XE0U) + seq_len = 3; + else if ((*from & 0XF8U) == 0XF0U) + seq_len = 4; + else + break; // 非法首字节 + if (fromSize < seq_len) + break; // 输入截断:不足一个完整 UTF8 序列(防 fromSize 无符号下溢越界读) + for (uint8_t i = 0; i < seq_len; i++) + utfcode = (utfcode << 8) | (uint8_t)from[i]; + from += seq_len; + fromSize -= seq_len; } if (utfcode < 0X80U) @@ -186,14 +216,18 @@ void UTF8ToGBK(const char *from, uint32_t fromSize, char *to, uint32_t *toSize) else break; // ERROR: not support - if (unicode < 0X80) { // ASCII + if (unicode < 0X80) { // ASCII → 1 字节 + if (size + 1 > capacity) + break; *to++ = unicode; size++; - } else { // NOT ASCII + } else { // NOT ASCII → GBK 双字节 + if (size + 2 > capacity) + break; unicode = convert_encoding(unicode, 0); // UNICODE TO GBK - to[0] = (unicode >> 8) & 0XFF; // ˴洢 - to[1] = unicode & 0XFF; // ˴洢 + to[0] = (unicode >> 8) & 0XFF; // 大端存储 + to[1] = unicode & 0XFF; // 大端存储 to += 2; size += 2; } @@ -202,16 +236,20 @@ void UTF8ToGBK(const char *from, uint32_t fromSize, char *to, uint32_t *toSize) } /** - * @brief GBKת˫ֽUNICODE + * @brief GBK码转双字节UNICODE码 * - * @param from GBK - * @param fromSize GBKС - * @param to UNICODE - * @param toSize UNICODEС + * 输入/输出越界安全:GBK 高字节残留不足 2 字节时安全截断终止(不越界读); + * 输出写入前检查剩余容量,不足即终止。 + * + * @param from GBK码 + * @param fromSize GBK码字节数 + * @param to UNICODE码输出缓冲 + * @param toSize 入参:输出缓冲容量(字节);出参:实际写出长度(字节) */ void GBKToUnicode(const char *from, uint32_t fromSize, char *to, uint32_t *toSize) { - uint32_t size = 0; + uint32_t capacity = *toSize; + uint32_t size = 0; uint16_t unicode; while (fromSize != 0) { @@ -220,15 +258,19 @@ void GBKToUnicode(const char *from, uint32_t fromSize, char *to, uint32_t *toSiz unicode = *from++; fromSize--; - } else { // GBK - unicode = (from[0] << 8) | from[1]; // GBKΪģʽ + } else { // GBK + if (fromSize < 2) + break; // 输入截断:不足一个 GBK 字符,终止(防 fromSize 无符号下溢越界读) + unicode = (from[0] << 8) | from[1]; // GBK为大端模式 unicode = convert_encoding(unicode, 1); // to unicode from += 2; fromSize -= 2; } - to[0] = unicode & 0XFF; // Сģʽ洢UNICODE - to[1] = (unicode >> 8) & 0XFF; // Сģʽ洢UNICODE + if (size + 2 > capacity) + break; + to[0] = unicode & 0XFF; // 小端模式存储UNICODE码 + to[1] = (unicode >> 8) & 0XFF; // 小端模式存储UNICODE码 to += 2; size += 2; } @@ -236,28 +278,39 @@ void GBKToUnicode(const char *from, uint32_t fromSize, char *to, uint32_t *toSiz } /** - * @brief ˫ֽUNICODEתGBK + * @brief 双字节UNICODE码转GBK码 + * + * 输入/输出越界安全:输入不足 2 字节时安全截断终止(不越界读); + * 输出写入前检查剩余容量,不足即终止。 * - * @param from ˫ֽUNICODE - * @param fromSize UNICODEС - * @param to GBK - * @param toSize GBKС + * @param from 双字节UNICODE码(小端) + * @param fromSize UNICODE码字节数 + * @param to GBK码输出缓冲 + * @param toSize 入参:输出缓冲容量(字节);出参:实际写出长度(字节) */ void UnicodeToGBK(const char *from, uint32_t fromSize, char *to, uint32_t *toSize) { - uint32_t size = 0; + uint32_t capacity = *toSize; + uint32_t size = 0; uint16_t unicode; while (fromSize != 0) { - unicode = (from[1] << 8) | from[0]; // unicodeΪСģʽ - if (unicode < 0X80) { // ASCII + if (fromSize < 2) + break; // 输入截断:不足一个 UNICODE 码元,终止(防 fromSize 无符号下溢越界读) + + unicode = (from[1] << 8) | from[0]; // unicode码为小端模式 + if (unicode < 0X80) { // ASCII → 1 字节 + if (size + 1 > capacity) + break; *to++ = unicode; size++; - } else { // NOT ASCII + } else { // NOT ASCII → GBK 双字节 + if (size + 2 > capacity) + break; unicode = convert_encoding(unicode, 0); // TO GBK - to[0] = (unicode >> 8) & 0XFF; // ģʽ洢GBK - to[1] = unicode & 0XFF; // ģʽ洢GBK + to[0] = (unicode >> 8) & 0XFF; // 大端模式存储GBK码 + to[1] = unicode & 0XFF; // 大端模式存储GBK码 size += 2; to += 2; } @@ -269,29 +322,47 @@ void UnicodeToGBK(const char *from, uint32_t fromSize, char *to, uint32_t *toSiz } /** - * @brief ֽUTF8ת˫ֽUNICODE + * @brief 三字节UTF8码转双字节UNICODE码 + * + * 输入/输出越界安全:多字节序列按首字节判断 2/3/4 字节长度, + * 输入不足完整序列时安全截断终止(不越界读); + * 输出写入前检查剩余容量,不足即终止。 * - * @param from UTF8 - * @param fromSize UTF8С - * @param to GBK - * @param toSize GBKС + * @param from UTF8码 + * @param fromSize UTF8码字节数 + * @param to UNICODE码输出缓冲 + * @param toSize 入参:输出缓冲容量(字节);出参:实际写出长度(字节) */ void UTF8ToUnicode(const char *from, uint32_t fromSize, char *to, uint32_t *toSize) { + uint32_t capacity = *toSize; uint32_t unicode; uint32_t utfcode; uint32_t size = 0; while (fromSize != 0) { + utfcode = 0; if (*from < 0X80) { // ASCII utfcode = *from; from++; fromSize--; - } else { // NOT ASCII - utfcode = (from[0] << 16) | (from[1] << 8) | from[2]; // UTF8ֽڱʾһĺ(ģʽ) - from += 3; - fromSize -= 3; + } else { // UTF8 多字节:按首字节确定序列长度,不足即截断终止 + uint8_t seq_len; + if ((*from & 0XE0U) == 0XC0U) + seq_len = 2; + else if ((*from & 0XF0U) == 0XE0U) + seq_len = 3; + else if ((*from & 0XF8U) == 0XF0U) + seq_len = 4; + else + break; // 非法首字节 + if (fromSize < seq_len) + break; // 输入截断:不足一个完整 UTF8 序列(防 fromSize 无符号下溢越界读) + for (uint8_t i = 0; i < seq_len; i++) + utfcode = (utfcode << 8) | (uint8_t)from[i]; + from += seq_len; + fromSize -= seq_len; } if (utfcode < 0X80U) { // ASCII @@ -308,8 +379,10 @@ void UTF8ToUnicode(const char *from, uint32_t fromSize, char *to, uint32_t *toSi break; // ERROR: not support } - to[0] = unicode & 0XFF; // С˴洢UNICODE - to[1] = (unicode >> 8) & 0XFF; // С˴洢UNICODE + if (size + 2 > capacity) + break; + to[0] = unicode & 0XFF; // 小端存储UNICODE + to[1] = (unicode >> 8) & 0XFF; // 小端存储UNICODE to += 2; size += 2; } @@ -317,69 +390,76 @@ void UTF8ToUnicode(const char *from, uint32_t fromSize, char *to, uint32_t *toSi } /** - * @brief ˫ֽUNICODEתֽUTF8 + * @brief 双字节UNICODE码转三字节UTF8码 * - * @param from - * @param fromSize - * @param to - * @param toSize + * 输入/输出越界安全:输入不足 2 字节时安全截断终止(不越界读); + * 输出写入前检查剩余容量,不足即终止。 + * + * @param from 双字节UNICODE码(小端) + * @param fromSize UNICODE码字节数 + * @param to UTF8码输出缓冲 + * @param toSize 入参:输出缓冲容量(字节);出参:实际写出长度(字节) */ void UnicodeToUTF8(const char *from, uint32_t fromSize, char *to, uint32_t *toSize) { + uint32_t capacity = *toSize; uint32_t unicode; uint32_t utfcode; uint32_t size = 0; while (fromSize != 0) { - unicode = (from[1] << 8) | from[0]; // unicodeΪСģʽ + if (fromSize < 2) + break; // 输入截断:不足一个 UNICODE 码元,终止(防 fromSize 无符号下溢越界读) + unicode = (from[1] << 8) | from[0]; // unicode码为小端模式 from += 2; fromSize -= 2; - if (unicode < 0X80U) // ASCII - { + if (unicode < 0X80U) { // ASCII → 1 字节 + if (size + 1 > capacity) + break; *to = unicode; to++; size++; - } else // NOT ASCII - { - if (unicode >= 0X80U && unicode <= 0X7FFU) { - utfcode = 0XC080U | (unicode & 0X3FU) | (((unicode >> 6) & 0X1FU) << 8); - to[1] = utfcode & 0XFF; - to[0] = (utfcode >> 8) & 0XFF; - to += 2; - size += 2; - - } else if (unicode >= 0X800U && unicode <= 0XFFFFU) { - utfcode = 0XE08080U | (unicode & 0X3FU) | (((unicode >> 6) & 0X3FU) << 8) | (((unicode >> 12) & 0XFU) << 16); - to[2] = utfcode & 0XFF; - to[1] = (utfcode >> 8) & 0XFF; - to[0] = (utfcode >> 16) & 0XFF; - to += 3; - size += 3; - - } else if (unicode >= 0X10000U && unicode <= 0X10FFFFU) { - utfcode = 0XF0808080U | ((unicode & 0X3FU)) | (((unicode >> 6) & 0X3FU) << 8) | (((unicode >> 12) & 0X3FU) << 16) | (((unicode >> 18) & 0X7U) << 24); - to[3] = utfcode & 0XFF; - to[2] = (utfcode >> 8) & 0XFF; - to[1] = (utfcode >> 16) & 0XFF; - to[0] = (utfcode >> 24) & 0XFF; - to += 4; - size += 4; - - } else { - break; // ERROR: not support - } + } else if (unicode <= 0X7FFU) { // → 2 字节 + if (size + 2 > capacity) + break; + utfcode = 0XC080U | (unicode & 0X3FU) | (((unicode >> 6) & 0X1FU) << 8); + to[1] = utfcode & 0XFF; + to[0] = (utfcode >> 8) & 0XFF; + to += 2; + size += 2; + } else if (unicode <= 0XFFFFU) { // → 3 字节 + if (size + 3 > capacity) + break; + utfcode = 0XE08080U | (unicode & 0X3FU) | (((unicode >> 6) & 0X3FU) << 8) | (((unicode >> 12) & 0XFU) << 16); + to[2] = utfcode & 0XFF; + to[1] = (utfcode >> 8) & 0XFF; + to[0] = (utfcode >> 16) & 0XFF; + to += 3; + size += 3; + } else if (unicode <= 0X10FFFFU) { // → 4 字节 + if (size + 4 > capacity) + break; + utfcode = 0XF0808080U | ((unicode & 0X3FU)) | (((unicode >> 6) & 0X3FU) << 8) | (((unicode >> 12) & 0X3FU) << 16) | (((unicode >> 18) & 0X7U) << 24); + to[3] = utfcode & 0XFF; + to[2] = (utfcode >> 8) & 0XFF; + to[1] = (utfcode >> 16) & 0XFF; + to[0] = (utfcode >> 24) & 0XFF; + to += 4; + size += 4; + } else { + break; // ERROR: not support } } *toSize = size; } /** - * @brief UNICODEGBKת + * @brief UNICODE和GBK互转函数 * - * @param input_char תֵ - * @param is_oem_to_unicode 0unicodeתgbk 1gbkתunicode - * @return תֵ + * @param input_char 待转换的值 + * @param is_oem_to_unicode 0:unicode转gbk 1:gbk转unicode + * @return 转换后的值 */ static uint16_t convert_encoding(uint16_t input_char, bool is_oem_to_unicode) { @@ -11290,31 +11370,31 @@ static uint16_t convert_encoding(uint16_t input_char, bool is_oem_to_unicode) const uint16_t *lookup_table; uint16_t result_char = 0; int32_t mid, low = 0, high; - int32_t max_steps = 16; // Ȳ2^16(65536) + int32_t max_steps = 16; // 假设表长度不超过2^16(65536) - // ASCIIַ(0x00 - 0x7F) + // 处理ASCII字符(0x00 - 0x7F) if (input_char < 0x80) { return input_char; } - // תѡӳ + // 根据转换方向选择映射表 if (is_oem_to_unicode) { lookup_table = oem2uni; - high = (sizeof(oem2uni) / 4) - 1; // ÿĿռ2uint16(KeyValue)ܴС4 + high = (sizeof(oem2uni) / 4) - 1; // 表中每个条目占2个uint16(Key和Value),所以总大小除以4 } else { lookup_table = uni2oem; high = (sizeof(uni2oem) / 4) - 1; } - while (max_steps > 0 && low <= high) { // ֲ + while (max_steps > 0 && low <= high) { // 二分查找 mid = low + (high - low) / 2; - // table[mid << 1]ΪKey(ԭʼ) + // table[mid << 1]为Key(原始编码) uint16_t current_key = lookup_table[mid << 1]; if (input_char == current_key) { - // ҵƥtable[(mid << 1) + 1]Value(Ŀ) + // 找到匹配项,table[(mid << 1) + 1]是Value(目标编码) result_char = lookup_table[(mid << 1) + 1]; break; } diff --git a/Makefile b/Makefile index 3d869c5..dd9849a 100644 --- a/Makefile +++ b/Makefile @@ -29,14 +29,43 @@ FPU = -mfpu=fpv4-sp-d16 FLOAT-ABI = -mfloat-abi=hard MCU_FLAGS = $(CPU) -mthumb $(FPU) $(FLOAT-ABI) +# ---- Protocol selection ---- +# PROTO=ALL:全协议共存开发构建(默认,-DSTD_ALL_PROTO 豁免 '{' 帧族互斥守卫) +# PROTO=CQ :重庆量产口径——编入 CQ + cJSON,剔除 LDI 目录文件(网络配置应用由中立 +# 模块 app_net_boot 承担,CQ 构建不启动 TCP Server/Client 通道), +# 追加 -DPROTO_CHONGQING('{' 帧族仍编入,保持 -DSTD_ALL_PROTO 豁免守卫) +PROTO ?= ALL + # ---- Common Flags ---- -DEFINES = -DUSE_HAL_DRIVER -DSTM32F407xx +# -DSTD_ALL_PROTO:全协议共存开发构建——'{' 帧族互斥守卫 g_brace_proto_guard 失效; +# EIDE 量产构建不定义该宏,多 '{' 族协议编入即链接报 multiple definition 强制互斥。 +DEFINES = -DUSE_HAL_DRIVER -DSTM32F407xx -DSTD_ALL_PROTO +ifeq ($(PROTO),CQ) + DEFINES += -DPROTO_CHONGQING +endif + +# 功能开关显式覆盖(命令行传入即追加):例如 make PROTO=CQ CQ_FAULT_SCREEN=0 +# 关闭 CQ 心跳故障屏。勿直接覆盖 DEFINES——命令行 DEFINES 会整体替换 makefile +# 内赋值(含 += 不追加),丢掉平台宏并破坏「排除 LDI ↔ PROTO_CHONGQING」成对。 +ifdef CQ_FAULT_SCREEN + DEFINES += -DCQ_FAULT_SCREEN=$(CQ_FAULT_SCREEN) +endif INC_DIRS = \ -I Application/Inc \ -I Application/Inc/IAP \ -I Application/Inc/LDI \ + -I Application/Inc/Config \ + -I Application/Inc/ProtocolParser_QingHai \ + -I Application/Inc/ProtocolParser_SiChuang_ETC \ + -I Application/Inc/ProtocolParser_SiChuang_MTC \ + -I Application/Inc/ProtocolParser_SiChuang_Overload \ + -I Application/Inc/ProtocolParser_GuiZhou \ + -I Application/Inc/ProtocolParser_YunNan \ + -I Application/Inc/ProtocolParser_ShanDong \ + -I Application/Inc/ProtocolParser_ChongQing \ -I Application/Inc/AH_MQTT \ + -I Application/Inc/RLS \ -I Application/Inc/Channel \ -I Device/Inc \ -I Platform/Inc \ @@ -46,6 +75,7 @@ INC_DIRS = \ -I Drivers/CMSIS/Device/ST/STM32F4xx/Include \ -I Drivers/STM32F4xx_HAL_Driver/Inc \ -I Middlewares/Third_Party/SEGGER_RTT \ + -I Middlewares/Third_Party/cJSON \ -I Middlewares/Third_Party/LwIP/src/include \ -I Middlewares/Third_Party/LwIP/system \ -I Middlewares/Third_Party/FreeRTOS/Source/include \ @@ -242,7 +272,8 @@ SRC_KERNEL = \ Kernel/Src/ring_buffer.c \ Kernel/Src/bit_utils.c \ Kernel/Src/crc_utils.c \ - Kernel/Src/text_cvt.c + Kernel/Src/text_cvt.c \ + Kernel/Src/bcc_utils.c # Platform(仅含无冲突的文件,其他在 Phase 3 逐步加入) SRC_PLATFORM = \ @@ -269,33 +300,69 @@ SRC_DEVICE = \ Device/IO/dev_io_ctrl.c \ Device/IO/dev_key.c \ Device/Display/dev_display.c \ - Device/Display/dev_display_p20.c \ + Device/Display/dev_display_1_260.c \ Device/IO/dev_light_sensor.c \ Device/Storage/dev_w25qxx.c \ Device/Storage/dev_flash_int.c \ Device/Network/dev_dp83848.c \ Device/Network/dev_eth.c \ Device/Comm/dev_rs485.c \ - Device/Comm/dev_rs232.c + Device/Comm/dev_rs232.c \ + Device/Comm/dev_rs232_voice.c -# Application (Project_STD 新模块,resend app_* 等 Phase 7 集成后加入) +# Application — 兼容协议同时编入(EIDE 目录编入等价;互斥靠排除目录,不用宏) SRC_APPLICATION = \ Application/Src/app_test.c \ Application/Src/app_factory_test.c \ Application/Src/app_boot.c \ + Application/Src/app_net_boot.c \ + Application/Src/app_default_display.c \ Application/Src/app_dispatch.c \ Application/Src/app_render.c \ Application/Src/app_key.c \ Application/Src/app_light_sensor.c \ Application/Src/IAP/app_iap.c \ Application/Src/IAP/app_iap_cmd.c \ - Application/Src/IAP/app_iap_cfg.c \ + Application/Src/Config/app_board_net_cfg.c \ Application/Src/LDI/app_ldi.c \ Application/Src/LDI/app_ldi_cmd.c \ Application/Src/LDI/app_ldi_cfg.c \ + Application/Src/LDI/app_ldi_device.c \ Application/Src/LDI/app_vms_ctrl.c \ + Application/Src/ProtocolParser_ChongQing/app_cq_proto.c \ + Application/Src/ProtocolParser_ChongQing/app_cq_proto_parse.c \ + Application/Src/ProtocolParser_ChongQing/app_cq_proto_cmd.c \ Application/Src/AH_MQTT/ah_mqtt.c \ Application/Src/AH_MQTT/ah_mqtt_cmd.c \ + Application/Src/RLS/app_rls.c \ + Application/Src/RLS/app_rls_cmd.c \ + Application/Src/ProtocolParser_QingHai/app_qh_proto.c \ + Application/Src/ProtocolParser_QingHai/app_qh_proto_parse.c \ + Application/Src/ProtocolParser_QingHai/app_qh_proto_cmd.c \ + Application/Src/ProtocolParser_QingHai/app_qh_proto_voice.c \ + Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto.c \ + Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_parse.c \ + Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto_cmd.c \ + Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto.c \ + Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_parse.c \ + Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_cmd.c \ + Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto_voice.c \ + Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto.c \ + Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_parse.c \ + Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto_cmd.c \ + Application/Src/ProtocolParser_ShanDong/app_sd_proto.c \ + Application/Src/ProtocolParser_ShanDong/app_sd_proto_parse.c \ + Application/Src/ProtocolParser_ShanDong/app_sd_proto_cmd.c \ + Application/Src/ProtocolParser_ShanDong/app_sd_proto_default.c \ + Application/Src/ProtocolParser_GuiZhou/app_gz_proto.c \ + Application/Src/ProtocolParser_GuiZhou/app_gz_proto_parse.c \ + Application/Src/ProtocolParser_GuiZhou/app_gz_proto_cmd.c \ + Application/Src/ProtocolParser_GuiZhou/app_gz_proto_voice.c \ + Application/Src/ProtocolParser_YunNan/app_yn_proto.c \ + Application/Src/ProtocolParser_YunNan/app_yn_proto_parse.c \ + Application/Src/ProtocolParser_YunNan/app_yn_proto_cmd.c \ + Application/Src/ProtocolParser_YunNan/app_yn_proto_voice.c \ + Application/Src/app_uart_baud.c \ Application/Src/Channel/app_udp.c \ Application/Src/Channel/app_tcp_server.c \ Application/Src/Channel/app_tcp_client.c \ @@ -303,12 +370,22 @@ SRC_APPLICATION = \ Application/Src/Channel/app_rs232.c \ Application/Src/Channel/app_rs485.c +# PROTO=CQ:剔除 LDI 目录文件(网络配置应用由中立模块 app_net_boot 承担) +ifeq ($(PROTO),CQ) + SRC_APPLICATION := $(filter-out Application/Src/LDI/%,$(SRC_APPLICATION)) +endif + +# cJSON 第三方中间件(CQ JSON 解析) +SRC_CJSON = \ + Middlewares/Third_Party/cJSON/cJSON.c + # ---- All Sources ---- SRC_ALL = \ $(SRC_KERNEL) \ $(SRC_PLATFORM) \ $(SRC_DEVICE) \ $(SRC_APPLICATION) \ + $(SRC_CJSON) \ $(SRC_CORE) \ $(SRC_HAL) \ $(SRC_RTT) \ @@ -320,12 +397,22 @@ SRC_ALL = \ OBJ_ALL = $(addprefix $(BUILD_DIR)/,$(SRC_ALL:.c=.o)) # ---- Targets ---- -.PHONY: all clean +.PHONY: all clean compile_commands all: $(BUILD_DIR)/Project_STD.elf $(BUILD_DIR)/Project_STD.hex $(BUILD_DIR)/Project_STD.bin @echo "==== Build complete ====" @$(SIZE) $(BUILD_DIR)/Project_STD.elf +compile_commands: $(BUILD_DIR)/compile_commands.json + @ln -sf $(BUILD_DIR)/compile_commands.json compile_commands.json + @echo "==== compile_commands.json ready ====" + +$(BUILD_DIR)/compile_commands.json: + @mkdir -p $(BUILD_DIR) + @SRC_LIST="$(SRC_ALL)" CC_BIN="$(CC)" CFLAGS_STR="$(CFLAGS)" BUILD_DIR="$(BUILD_DIR)" \ + python3 -c 'import json,os,shlex; from pathlib import Path; root=Path.cwd(); build_dir=root/Path(os.environ["BUILD_DIR"]); cc_bin=os.environ["CC_BIN"]; cflags=shlex.split(os.environ["CFLAGS_STR"]); src_list=[s for s in os.environ["SRC_LIST"].split() if s.endswith(".c")]; entries=[]; \ +for src in src_list: entries.append({"directory": str(root), "command": " ".join(shlex.quote(x) for x in [cc_bin, *cflags, "-c", "-o", str(build_dir / src.replace(".c", ".o")), src]), "file": str(root / src), "output": str(build_dir / src.replace(".c", ".o"))}); (build_dir / "compile_commands.json").write_text(json.dumps(entries, indent=2) + "\n", encoding="utf-8")' + $(BUILD_DIR)/Project_STD.elf: $(OBJ_ALL) @echo "Linking $@" @mkdir -p $(dir $@) @@ -344,4 +431,4 @@ $(BUILD_DIR)/%.o: %.c $(CC) $(CFLAGS) -c -o $@ $< clean: - rm -rf $(BUILD_DIR) + rm -rf $(BUILD_DIR) compile_commands.json diff --git a/Middlewares/Third_Party/SEGGER_RTT/SEGGER_RTT_Conf.h b/Middlewares/Third_Party/SEGGER_RTT/SEGGER_RTT_Conf.h index f67242e..4ad2eed 100644 --- a/Middlewares/Third_Party/SEGGER_RTT/SEGGER_RTT_Conf.h +++ b/Middlewares/Third_Party/SEGGER_RTT/SEGGER_RTT_Conf.h @@ -92,7 +92,7 @@ Revision: $Rev: 24316 $ #endif #ifndef BUFFER_SIZE_UP -#define BUFFER_SIZE_UP (1024 * 4) // Size of the buffer for terminal output of target, up to host (Default: 1k) +#define BUFFER_SIZE_UP (1024 * 2) // 4KB→2KB:给 ucHeap 回 SRAM 让出约 2KB(默认 1k) #endif #ifndef BUFFER_SIZE_DOWN diff --git a/Middlewares/Third_Party/cJSON/cJSON.c b/Middlewares/Third_Party/cJSON/cJSON.c new file mode 100644 index 0000000..61483d9 --- /dev/null +++ b/Middlewares/Third_Party/cJSON/cJSON.c @@ -0,0 +1,3143 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* cJSON */ +/* JSON parser in C. */ + +/* disable warnings about old C89 functions in MSVC */ +#if !defined(_CRT_SECURE_NO_DEPRECATE) && defined(_MSC_VER) +#define _CRT_SECURE_NO_DEPRECATE +#endif + +#ifdef __GNUC__ +#pragma GCC visibility push(default) +#endif +#if defined(_MSC_VER) +#pragma warning (push) +/* disable warning about single line comments in system headers */ +#pragma warning (disable : 4001) +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_LOCALES +#include +#endif + +#if defined(_MSC_VER) +#pragma warning (pop) +#endif +#ifdef __GNUC__ +#pragma GCC visibility pop +#endif + +#include "cJSON.h" + +/* define our own boolean type */ +#ifdef true +#undef true +#endif +#define true ((cJSON_bool)1) + +#ifdef false +#undef false +#endif +#define false ((cJSON_bool)0) + +/* define isnan and isinf for ANSI C, if in C99 or above, isnan and isinf has been defined in math.h */ +#ifndef isinf +#define isinf(d) (isnan((d - d)) && !isnan(d)) +#endif +#ifndef isnan +#define isnan(d) (d != d) +#endif + +#ifndef NAN +#ifdef _WIN32 +#define NAN sqrt(-1.0) +#else +#define NAN 0.0/0.0 +#endif +#endif + +typedef struct { + const unsigned char *json; + size_t position; +} error; +static error global_error = { NULL, 0 }; + +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void) +{ + return (const char*) (global_error.json + global_error.position); +} + +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item) +{ + if (!cJSON_IsString(item)) + { + return NULL; + } + + return item->valuestring; +} + +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item) +{ + if (!cJSON_IsNumber(item)) + { + return (double) NAN; + } + + return item->valuedouble; +} + +/* This is a safeguard to prevent copy-pasters from using incompatible C and header files */ +#if (CJSON_VERSION_MAJOR != 1) || (CJSON_VERSION_MINOR != 7) || (CJSON_VERSION_PATCH != 18) + #error cJSON.h and cJSON.c have different versions. Make sure that both have the same. +#endif + +CJSON_PUBLIC(const char*) cJSON_Version(void) +{ + static char version[15]; + sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); + + return version; +} + +/* Case insensitive string comparison, doesn't consider two NULL pointers equal though */ +static int case_insensitive_strcmp(const unsigned char *string1, const unsigned char *string2) +{ + if ((string1 == NULL) || (string2 == NULL)) + { + return 1; + } + + if (string1 == string2) + { + return 0; + } + + for(; tolower(*string1) == tolower(*string2); (void)string1++, string2++) + { + if (*string1 == '\0') + { + return 0; + } + } + + return tolower(*string1) - tolower(*string2); +} + +typedef struct internal_hooks +{ + void *(CJSON_CDECL *allocate)(size_t size); + void (CJSON_CDECL *deallocate)(void *pointer); + void *(CJSON_CDECL *reallocate)(void *pointer, size_t size); +} internal_hooks; + +#if defined(_MSC_VER) +/* work around MSVC error C2322: '...' address of dllimport '...' is not static */ +static void * CJSON_CDECL internal_malloc(size_t size) +{ + return malloc(size); +} +static void CJSON_CDECL internal_free(void *pointer) +{ + free(pointer); +} +static void * CJSON_CDECL internal_realloc(void *pointer, size_t size) +{ + return realloc(pointer, size); +} +#else +#define internal_malloc malloc +#define internal_free free +#define internal_realloc realloc +#endif + +/* strlen of character literals resolved at compile time */ +#define static_strlen(string_literal) (sizeof(string_literal) - sizeof("")) + +static internal_hooks global_hooks = { internal_malloc, internal_free, internal_realloc }; + +static unsigned char* cJSON_strdup(const unsigned char* string, const internal_hooks * const hooks) +{ + size_t length = 0; + unsigned char *copy = NULL; + + if (string == NULL) + { + return NULL; + } + + length = strlen((const char*)string) + sizeof(""); + copy = (unsigned char*)hooks->allocate(length); + if (copy == NULL) + { + return NULL; + } + memcpy(copy, string, length); + + return copy; +} + +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks) +{ + if (hooks == NULL) + { + /* Reset hooks */ + global_hooks.allocate = malloc; + global_hooks.deallocate = free; + global_hooks.reallocate = realloc; + return; + } + + global_hooks.allocate = malloc; + if (hooks->malloc_fn != NULL) + { + global_hooks.allocate = hooks->malloc_fn; + } + + global_hooks.deallocate = free; + if (hooks->free_fn != NULL) + { + global_hooks.deallocate = hooks->free_fn; + } + + /* use realloc only if both free and malloc are used */ + global_hooks.reallocate = NULL; + if ((global_hooks.allocate == malloc) && (global_hooks.deallocate == free)) + { + global_hooks.reallocate = realloc; + } +} + +/* Internal constructor. */ +static cJSON *cJSON_New_Item(const internal_hooks * const hooks) +{ + cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON)); + if (node) + { + memset(node, '\0', sizeof(cJSON)); + } + + return node; +} + +/* Delete a cJSON structure. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item) +{ + cJSON *next = NULL; + while (item != NULL) + { + next = item->next; + if (!(item->type & cJSON_IsReference) && (item->child != NULL)) + { + cJSON_Delete(item->child); + } + if (!(item->type & cJSON_IsReference) && (item->valuestring != NULL)) + { + global_hooks.deallocate(item->valuestring); + item->valuestring = NULL; + } + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + global_hooks.deallocate(item->string); + item->string = NULL; + } + global_hooks.deallocate(item); + item = next; + } +} + +/* get the decimal point character of the current locale */ +static unsigned char get_decimal_point(void) +{ +#ifdef ENABLE_LOCALES + struct lconv *lconv = localeconv(); + return (unsigned char) lconv->decimal_point[0]; +#else + return '.'; +#endif +} + +typedef struct +{ + const unsigned char *content; + size_t length; + size_t offset; + size_t depth; /* How deeply nested (in arrays/objects) is the input at the current offset. */ + internal_hooks hooks; +} parse_buffer; + +/* check if the given size is left to read in a given parse buffer (starting with 1) */ +#define can_read(buffer, size) ((buffer != NULL) && (((buffer)->offset + size) <= (buffer)->length)) +/* check if the buffer can be accessed at the given index (starting with 0) */ +#define can_access_at_index(buffer, index) ((buffer != NULL) && (((buffer)->offset + index) < (buffer)->length)) +#define cannot_access_at_index(buffer, index) (!can_access_at_index(buffer, index)) +/* get a pointer to the buffer at the position */ +#define buffer_at_offset(buffer) ((buffer)->content + (buffer)->offset) + +/* Parse the input text to generate a number, and populate the result into item. */ +static cJSON_bool parse_number(cJSON * const item, parse_buffer * const input_buffer) +{ + double number = 0; + unsigned char *after_end = NULL; + unsigned char number_c_string[64]; + unsigned char decimal_point = get_decimal_point(); + size_t i = 0; + + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; + } + + /* copy the number into a temporary buffer and replace '.' with the decimal point + * of the current locale (for strtod) + * This also takes care of '\0' not necessarily being available for marking the end of the input */ + for (i = 0; (i < (sizeof(number_c_string) - 1)) && can_access_at_index(input_buffer, i); i++) + { + switch (buffer_at_offset(input_buffer)[i]) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '+': + case '-': + case 'e': + case 'E': + number_c_string[i] = buffer_at_offset(input_buffer)[i]; + break; + + case '.': + number_c_string[i] = decimal_point; + break; + + default: + goto loop_end; + } + } +loop_end: + number_c_string[i] = '\0'; + + number = strtod((const char*)number_c_string, (char**)&after_end); + if (number_c_string == after_end) + { + return false; /* parse_error */ + } + + item->valuedouble = number; + + /* use saturation in case of overflow */ + if (number >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)number; + } + + item->type = cJSON_Number; + + input_buffer->offset += (size_t)(after_end - number_c_string); + return true; +} + +/* don't ask me, but the original cJSON_SetNumberValue returns an integer or double */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number) +{ + if (number >= INT_MAX) + { + object->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + object->valueint = INT_MIN; + } + else + { + object->valueint = (int)number; + } + + return object->valuedouble = number; +} + +/* Note: when passing a NULL valuestring, cJSON_SetValuestring treats this as an error and return NULL */ +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring) +{ + char *copy = NULL; + /* if object's type is not cJSON_String or is cJSON_IsReference, it should not set valuestring */ + if ((object == NULL) || !(object->type & cJSON_String) || (object->type & cJSON_IsReference)) + { + return NULL; + } + /* return NULL if the object is corrupted or valuestring is NULL */ + if (object->valuestring == NULL || valuestring == NULL) + { + return NULL; + } + if (strlen(valuestring) <= strlen(object->valuestring)) + { + strcpy(object->valuestring, valuestring); + return object->valuestring; + } + copy = (char*) cJSON_strdup((const unsigned char*)valuestring, &global_hooks); + if (copy == NULL) + { + return NULL; + } + if (object->valuestring != NULL) + { + cJSON_free(object->valuestring); + } + object->valuestring = copy; + + return copy; +} + +typedef struct +{ + unsigned char *buffer; + size_t length; + size_t offset; + size_t depth; /* current nesting depth (for formatted printing) */ + cJSON_bool noalloc; + cJSON_bool format; /* is this print a formatted print */ + internal_hooks hooks; +} printbuffer; + +/* realloc printbuffer if necessary to have at least "needed" bytes more */ +static unsigned char* ensure(printbuffer * const p, size_t needed) +{ + unsigned char *newbuffer = NULL; + size_t newsize = 0; + + if ((p == NULL) || (p->buffer == NULL)) + { + return NULL; + } + + if ((p->length > 0) && (p->offset >= p->length)) + { + /* make sure that offset is valid */ + return NULL; + } + + if (needed > INT_MAX) + { + /* sizes bigger than INT_MAX are currently not supported */ + return NULL; + } + + needed += p->offset + 1; + if (needed <= p->length) + { + return p->buffer + p->offset; + } + + if (p->noalloc) { + return NULL; + } + + /* calculate new buffer size */ + if (needed > (INT_MAX / 2)) + { + /* overflow of int, use INT_MAX if possible */ + if (needed <= INT_MAX) + { + newsize = INT_MAX; + } + else + { + return NULL; + } + } + else + { + newsize = needed * 2; + } + + if (p->hooks.reallocate != NULL) + { + /* reallocate with realloc if available */ + newbuffer = (unsigned char*)p->hooks.reallocate(p->buffer, newsize); + if (newbuffer == NULL) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + } + else + { + /* otherwise reallocate manually */ + newbuffer = (unsigned char*)p->hooks.allocate(newsize); + if (!newbuffer) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + + memcpy(newbuffer, p->buffer, p->offset + 1); + p->hooks.deallocate(p->buffer); + } + p->length = newsize; + p->buffer = newbuffer; + + return newbuffer + p->offset; +} + +/* calculate the new length of the string in a printbuffer and update the offset */ +static void update_offset(printbuffer * const buffer) +{ + const unsigned char *buffer_pointer = NULL; + if ((buffer == NULL) || (buffer->buffer == NULL)) + { + return; + } + buffer_pointer = buffer->buffer + buffer->offset; + + buffer->offset += strlen((const char*)buffer_pointer); +} + +/* securely comparison of floating-point variables */ +static cJSON_bool compare_double(double a, double b) +{ + double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b); + return (fabs(a - b) <= maxVal * DBL_EPSILON); +} + +/* Render the number nicely from the given item into a string. */ +static cJSON_bool print_number(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + double d = item->valuedouble; + int length = 0; + size_t i = 0; + unsigned char number_buffer[26] = {0}; /* temporary buffer to print the number into */ + unsigned char decimal_point = get_decimal_point(); + double test = 0.0; + + if (output_buffer == NULL) + { + return false; + } + + /* This checks for NaN and Infinity */ + if (isnan(d) || isinf(d)) + { + length = sprintf((char*)number_buffer, "null"); + } + else if(d == (double)item->valueint) + { + length = sprintf((char*)number_buffer, "%d", item->valueint); + } + else + { + /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ + length = sprintf((char*)number_buffer, "%1.15g", d); + + /* Check whether the original double can be recovered */ + if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) + { + /* If not, print with 17 decimal places of precision */ + length = sprintf((char*)number_buffer, "%1.17g", d); + } + } + + /* sprintf failed or buffer overrun occurred */ + if ((length < 0) || (length > (int)(sizeof(number_buffer) - 1))) + { + return false; + } + + /* reserve appropriate space in the output */ + output_pointer = ensure(output_buffer, (size_t)length + sizeof("")); + if (output_pointer == NULL) + { + return false; + } + + /* copy the printed number to the output and replace locale + * dependent decimal point with '.' */ + for (i = 0; i < ((size_t)length); i++) + { + if (number_buffer[i] == decimal_point) + { + output_pointer[i] = '.'; + continue; + } + + output_pointer[i] = number_buffer[i]; + } + output_pointer[i] = '\0'; + + output_buffer->offset += (size_t)length; + + return true; +} + +/* parse 4 digit hexadecimal number */ +static unsigned parse_hex4(const unsigned char * const input) +{ + unsigned int h = 0; + size_t i = 0; + + for (i = 0; i < 4; i++) + { + /* parse digit */ + if ((input[i] >= '0') && (input[i] <= '9')) + { + h += (unsigned int) input[i] - '0'; + } + else if ((input[i] >= 'A') && (input[i] <= 'F')) + { + h += (unsigned int) 10 + input[i] - 'A'; + } + else if ((input[i] >= 'a') && (input[i] <= 'f')) + { + h += (unsigned int) 10 + input[i] - 'a'; + } + else /* invalid */ + { + return 0; + } + + if (i < 3) + { + /* shift left to make place for the next nibble */ + h = h << 4; + } + } + + return h; +} + +/* converts a UTF-16 literal to UTF-8 + * A literal can be one or two sequences of the form \uXXXX */ +static unsigned char utf16_literal_to_utf8(const unsigned char * const input_pointer, const unsigned char * const input_end, unsigned char **output_pointer) +{ + long unsigned int codepoint = 0; + unsigned int first_code = 0; + const unsigned char *first_sequence = input_pointer; + unsigned char utf8_length = 0; + unsigned char utf8_position = 0; + unsigned char sequence_length = 0; + unsigned char first_byte_mark = 0; + + if ((input_end - first_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + /* get the first utf16 sequence */ + first_code = parse_hex4(first_sequence + 2); + + /* check that the code is valid */ + if (((first_code >= 0xDC00) && (first_code <= 0xDFFF))) + { + goto fail; + } + + /* UTF16 surrogate pair */ + if ((first_code >= 0xD800) && (first_code <= 0xDBFF)) + { + const unsigned char *second_sequence = first_sequence + 6; + unsigned int second_code = 0; + sequence_length = 12; /* \uXXXX\uXXXX */ + + if ((input_end - second_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + if ((second_sequence[0] != '\\') || (second_sequence[1] != 'u')) + { + /* missing second half of the surrogate pair */ + goto fail; + } + + /* get the second utf16 sequence */ + second_code = parse_hex4(second_sequence + 2); + /* check that the code is valid */ + if ((second_code < 0xDC00) || (second_code > 0xDFFF)) + { + /* invalid second half of the surrogate pair */ + goto fail; + } + + + /* calculate the unicode codepoint from the surrogate pair */ + codepoint = 0x10000 + (((first_code & 0x3FF) << 10) | (second_code & 0x3FF)); + } + else + { + sequence_length = 6; /* \uXXXX */ + codepoint = first_code; + } + + /* encode as UTF-8 + * takes at maximum 4 bytes to encode: + * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ + if (codepoint < 0x80) + { + /* normal ascii, encoding 0xxxxxxx */ + utf8_length = 1; + } + else if (codepoint < 0x800) + { + /* two bytes, encoding 110xxxxx 10xxxxxx */ + utf8_length = 2; + first_byte_mark = 0xC0; /* 11000000 */ + } + else if (codepoint < 0x10000) + { + /* three bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx */ + utf8_length = 3; + first_byte_mark = 0xE0; /* 11100000 */ + } + else if (codepoint <= 0x10FFFF) + { + /* four bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx */ + utf8_length = 4; + first_byte_mark = 0xF0; /* 11110000 */ + } + else + { + /* invalid unicode codepoint */ + goto fail; + } + + /* encode as utf8 */ + for (utf8_position = (unsigned char)(utf8_length - 1); utf8_position > 0; utf8_position--) + { + /* 10xxxxxx */ + (*output_pointer)[utf8_position] = (unsigned char)((codepoint | 0x80) & 0xBF); + codepoint >>= 6; + } + /* encode first byte */ + if (utf8_length > 1) + { + (*output_pointer)[0] = (unsigned char)((codepoint | first_byte_mark) & 0xFF); + } + else + { + (*output_pointer)[0] = (unsigned char)(codepoint & 0x7F); + } + + *output_pointer += utf8_length; + + return sequence_length; + +fail: + return 0; +} + +/* Parse the input text into an unescaped cinput, and populate item. */ +static cJSON_bool parse_string(cJSON * const item, parse_buffer * const input_buffer) +{ + const unsigned char *input_pointer = buffer_at_offset(input_buffer) + 1; + const unsigned char *input_end = buffer_at_offset(input_buffer) + 1; + unsigned char *output_pointer = NULL; + unsigned char *output = NULL; + + /* not a string */ + if (buffer_at_offset(input_buffer)[0] != '\"') + { + goto fail; + } + + { + /* calculate approximate size of the output (overestimate) */ + size_t allocation_length = 0; + size_t skipped_bytes = 0; + while (((size_t)(input_end - input_buffer->content) < input_buffer->length) && (*input_end != '\"')) + { + /* is escape sequence */ + if (input_end[0] == '\\') + { + if ((size_t)(input_end + 1 - input_buffer->content) >= input_buffer->length) + { + /* prevent buffer overflow when last input character is a backslash */ + goto fail; + } + skipped_bytes++; + input_end++; + } + input_end++; + } + if (((size_t)(input_end - input_buffer->content) >= input_buffer->length) || (*input_end != '\"')) + { + goto fail; /* string ended unexpectedly */ + } + + /* This is at most how much we need for the output */ + allocation_length = (size_t) (input_end - buffer_at_offset(input_buffer)) - skipped_bytes; + output = (unsigned char*)input_buffer->hooks.allocate(allocation_length + sizeof("")); + if (output == NULL) + { + goto fail; /* allocation failure */ + } + } + + output_pointer = output; + /* loop through the string literal */ + while (input_pointer < input_end) + { + if (*input_pointer != '\\') + { + *output_pointer++ = *input_pointer++; + } + /* escape sequence */ + else + { + unsigned char sequence_length = 2; + if ((input_end - input_pointer) < 1) + { + goto fail; + } + + switch (input_pointer[1]) + { + case 'b': + *output_pointer++ = '\b'; + break; + case 'f': + *output_pointer++ = '\f'; + break; + case 'n': + *output_pointer++ = '\n'; + break; + case 'r': + *output_pointer++ = '\r'; + break; + case 't': + *output_pointer++ = '\t'; + break; + case '\"': + case '\\': + case '/': + *output_pointer++ = input_pointer[1]; + break; + + /* UTF-16 literal */ + case 'u': + sequence_length = utf16_literal_to_utf8(input_pointer, input_end, &output_pointer); + if (sequence_length == 0) + { + /* failed to convert UTF16-literal to UTF-8 */ + goto fail; + } + break; + + default: + goto fail; + } + input_pointer += sequence_length; + } + } + + /* zero terminate the output */ + *output_pointer = '\0'; + + item->type = cJSON_String; + item->valuestring = (char*)output; + + input_buffer->offset = (size_t) (input_end - input_buffer->content); + input_buffer->offset++; + + return true; + +fail: + if (output != NULL) + { + input_buffer->hooks.deallocate(output); + output = NULL; + } + + if (input_pointer != NULL) + { + input_buffer->offset = (size_t)(input_pointer - input_buffer->content); + } + + return false; +} + +/* Render the cstring provided to an escaped version that can be printed. */ +static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer) +{ + const unsigned char *input_pointer = NULL; + unsigned char *output = NULL; + unsigned char *output_pointer = NULL; + size_t output_length = 0; + /* numbers of additional characters needed for escaping */ + size_t escape_characters = 0; + + if (output_buffer == NULL) + { + return false; + } + + /* empty string */ + if (input == NULL) + { + output = ensure(output_buffer, sizeof("\"\"")); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "\"\""); + + return true; + } + + /* set "flag" to 1 if something needs to be escaped */ + for (input_pointer = input; *input_pointer; input_pointer++) + { + switch (*input_pointer) + { + case '\"': + case '\\': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + /* one character escape sequence */ + escape_characters++; + break; + default: + if (*input_pointer < 32) + { + /* UTF-16 escape sequence uXXXX */ + escape_characters += 5; + } + break; + } + } + output_length = (size_t)(input_pointer - input) + escape_characters; + + output = ensure(output_buffer, output_length + sizeof("\"\"")); + if (output == NULL) + { + return false; + } + + /* no characters have to be escaped */ + if (escape_characters == 0) + { + output[0] = '\"'; + memcpy(output + 1, input, output_length); + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; + } + + output[0] = '\"'; + output_pointer = output + 1; + /* copy the string */ + for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) + { + if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) + { + /* normal character, copy */ + *output_pointer = *input_pointer; + } + else + { + /* character needs to be escaped */ + *output_pointer++ = '\\'; + switch (*input_pointer) + { + case '\\': + *output_pointer = '\\'; + break; + case '\"': + *output_pointer = '\"'; + break; + case '\b': + *output_pointer = 'b'; + break; + case '\f': + *output_pointer = 'f'; + break; + case '\n': + *output_pointer = 'n'; + break; + case '\r': + *output_pointer = 'r'; + break; + case '\t': + *output_pointer = 't'; + break; + default: + /* escape and print as unicode codepoint */ + sprintf((char*)output_pointer, "u%04x", *input_pointer); + output_pointer += 4; + break; + } + } + } + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; +} + +/* Invoke print_string_ptr (which is useful) on an item. */ +static cJSON_bool print_string(const cJSON * const item, printbuffer * const p) +{ + return print_string_ptr((unsigned char*)item->valuestring, p); +} + +/* Predeclare these prototypes. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer); + +/* Utility to jump whitespace and cr/lf */ +static parse_buffer *buffer_skip_whitespace(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL)) + { + return NULL; + } + + if (cannot_access_at_index(buffer, 0)) + { + return buffer; + } + + while (can_access_at_index(buffer, 0) && (buffer_at_offset(buffer)[0] <= 32)) + { + buffer->offset++; + } + + if (buffer->offset == buffer->length) + { + buffer->offset--; + } + + return buffer; +} + +/* skip the UTF-8 BOM (byte order mark) if it is at the beginning of a buffer */ +static parse_buffer *skip_utf8_bom(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL) || (buffer->offset != 0)) + { + return NULL; + } + + if (can_access_at_index(buffer, 4) && (strncmp((const char*)buffer_at_offset(buffer), "\xEF\xBB\xBF", 3) == 0)) + { + buffer->offset += 3; + } + + return buffer; +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + size_t buffer_length; + + if (NULL == value) + { + return NULL; + } + + /* Adding null character size due to require_null_terminated. */ + buffer_length = strlen(value) + sizeof(""); + + return cJSON_ParseWithLengthOpts(value, buffer_length, return_parse_end, require_null_terminated); +} + +/* Parse an object - create a new root, and populate. */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } }; + cJSON *item = NULL; + + /* reset error position */ + global_error.json = NULL; + global_error.position = 0; + + if (value == NULL || 0 == buffer_length) + { + goto fail; + } + + buffer.content = (const unsigned char*)value; + buffer.length = buffer_length; + buffer.offset = 0; + buffer.hooks = global_hooks; + + item = cJSON_New_Item(&global_hooks); + if (item == NULL) /* memory fail */ + { + goto fail; + } + + if (!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer)))) + { + /* parse failure. ep is set. */ + goto fail; + } + + /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */ + if (require_null_terminated) + { + buffer_skip_whitespace(&buffer); + if ((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0') + { + goto fail; + } + } + if (return_parse_end) + { + *return_parse_end = (const char*)buffer_at_offset(&buffer); + } + + return item; + +fail: + if (item != NULL) + { + cJSON_Delete(item); + } + + if (value != NULL) + { + error local_error; + local_error.json = (const unsigned char*)value; + local_error.position = 0; + + if (buffer.offset < buffer.length) + { + local_error.position = buffer.offset; + } + else if (buffer.length > 0) + { + local_error.position = buffer.length - 1; + } + + if (return_parse_end != NULL) + { + *return_parse_end = (const char*)local_error.json + local_error.position; + } + + global_error = local_error; + } + + return NULL; +} + +/* Default options for cJSON_Parse */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value) +{ + return cJSON_ParseWithOpts(value, 0, 0); +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length) +{ + return cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0); +} + +#define cjson_min(a, b) (((a) < (b)) ? (a) : (b)) + +static unsigned char *print(const cJSON * const item, cJSON_bool format, const internal_hooks * const hooks) +{ + static const size_t default_buffer_size = 256; + printbuffer buffer[1]; + unsigned char *printed = NULL; + + memset(buffer, 0, sizeof(buffer)); + + /* create buffer */ + buffer->buffer = (unsigned char*) hooks->allocate(default_buffer_size); + buffer->length = default_buffer_size; + buffer->format = format; + buffer->hooks = *hooks; + if (buffer->buffer == NULL) + { + goto fail; + } + + /* print the value */ + if (!print_value(item, buffer)) + { + goto fail; + } + update_offset(buffer); + + /* check if reallocate is available */ + if (hooks->reallocate != NULL) + { + printed = (unsigned char*) hooks->reallocate(buffer->buffer, buffer->offset + 1); + if (printed == NULL) { + goto fail; + } + buffer->buffer = NULL; + } + else /* otherwise copy the JSON over to a new buffer */ + { + printed = (unsigned char*) hooks->allocate(buffer->offset + 1); + if (printed == NULL) + { + goto fail; + } + memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1)); + printed[buffer->offset] = '\0'; /* just to be sure */ + + /* free the buffer */ + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + return printed; + +fail: + if (buffer->buffer != NULL) + { + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + if (printed != NULL) + { + hooks->deallocate(printed); + printed = NULL; + } + + return NULL; +} + +/* Render a cJSON item/entity/structure to text. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item) +{ + return (char*)print(item, true, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item) +{ + return (char*)print(item, false, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if (prebuffer < 0) + { + return NULL; + } + + p.buffer = (unsigned char*)global_hooks.allocate((size_t)prebuffer); + if (!p.buffer) + { + return NULL; + } + + p.length = (size_t)prebuffer; + p.offset = 0; + p.noalloc = false; + p.format = fmt; + p.hooks = global_hooks; + + if (!print_value(item, &p)) + { + global_hooks.deallocate(p.buffer); + p.buffer = NULL; + return NULL; + } + + return (char*)p.buffer; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if ((length < 0) || (buffer == NULL)) + { + return false; + } + + p.buffer = (unsigned char*)buffer; + p.length = (size_t)length; + p.offset = 0; + p.noalloc = true; + p.format = format; + p.hooks = global_hooks; + + return print_value(item, &p); +} + +/* Parser core - when encountering text, process appropriately. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer) +{ + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; /* no input */ + } + + /* parse the different types of values */ + /* null */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "null", 4) == 0)) + { + item->type = cJSON_NULL; + input_buffer->offset += 4; + return true; + } + /* false */ + if (can_read(input_buffer, 5) && (strncmp((const char*)buffer_at_offset(input_buffer), "false", 5) == 0)) + { + item->type = cJSON_False; + input_buffer->offset += 5; + return true; + } + /* true */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "true", 4) == 0)) + { + item->type = cJSON_True; + item->valueint = 1; + input_buffer->offset += 4; + return true; + } + /* string */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '\"')) + { + return parse_string(item, input_buffer); + } + /* number */ + if (can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') || ((buffer_at_offset(input_buffer)[0] >= '0') && (buffer_at_offset(input_buffer)[0] <= '9')))) + { + return parse_number(item, input_buffer); + } + /* array */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '[')) + { + return parse_array(item, input_buffer); + } + /* object */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{')) + { + return parse_object(item, input_buffer); + } + + return false; +} + +/* Render a value to text. */ +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output = NULL; + + if ((item == NULL) || (output_buffer == NULL)) + { + return false; + } + + switch ((item->type) & 0xFF) + { + case cJSON_NULL: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "null"); + return true; + + case cJSON_False: + output = ensure(output_buffer, 6); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "false"); + return true; + + case cJSON_True: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "true"); + return true; + + case cJSON_Number: + return print_number(item, output_buffer); + + case cJSON_Raw: + { + size_t raw_length = 0; + if (item->valuestring == NULL) + { + return false; + } + + raw_length = strlen(item->valuestring) + sizeof(""); + output = ensure(output_buffer, raw_length); + if (output == NULL) + { + return false; + } + memcpy(output, item->valuestring, raw_length); + return true; + } + + case cJSON_String: + return print_string(item, output_buffer); + + case cJSON_Array: + return print_array(item, output_buffer); + + case cJSON_Object: + return print_object(item, output_buffer); + + default: + return false; + } +} + +/* Build an array from input text. */ +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* head of the linked list */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (buffer_at_offset(input_buffer)[0] != '[') + { + /* not an array */ + goto fail; + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ']')) + { + /* empty array */ + goto success; + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + /* parse next value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || buffer_at_offset(input_buffer)[0] != ']') + { + goto fail; /* expected end of array */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Array; + item->child = head; + + input_buffer->offset++; + + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an array to text */ +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_element = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output array. */ + /* opening square bracket */ + output_pointer = ensure(output_buffer, 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer = '['; + output_buffer->offset++; + output_buffer->depth++; + + while (current_element != NULL) + { + if (!print_value(current_element, output_buffer)) + { + return false; + } + update_offset(output_buffer); + if (current_element->next) + { + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ','; + if(output_buffer->format) + { + *output_pointer++ = ' '; + } + *output_pointer = '\0'; + output_buffer->offset += length; + } + current_element = current_element->next; + } + + output_pointer = ensure(output_buffer, 2); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ']'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Build an object from the text. */ +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* linked list head */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '{')) + { + goto fail; /* not an object */ + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '}')) + { + goto success; /* empty object */ + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + if (cannot_access_at_index(input_buffer, 1)) + { + goto fail; /* nothing comes after the comma */ + } + + /* parse the name of the child */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_string(current_item, input_buffer)) + { + goto fail; /* failed to parse name */ + } + buffer_skip_whitespace(input_buffer); + + /* swap valuestring and string, because we parsed the name */ + current_item->string = current_item->valuestring; + current_item->valuestring = NULL; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != ':')) + { + goto fail; /* invalid object */ + } + + /* parse the value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '}')) + { + goto fail; /* expected end of object */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Object; + item->child = head; + + input_buffer->offset++; + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an object to text. */ +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_item = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output: */ + length = (size_t) (output_buffer->format ? 2 : 1); /* fmt: {\n */ + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer++ = '{'; + output_buffer->depth++; + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + output_buffer->offset += length; + + while (current_item) + { + if (output_buffer->format) + { + size_t i; + output_pointer = ensure(output_buffer, output_buffer->depth); + if (output_pointer == NULL) + { + return false; + } + for (i = 0; i < output_buffer->depth; i++) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += output_buffer->depth; + } + + /* print key */ + if (!print_string_ptr((unsigned char*)current_item->string, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ':'; + if (output_buffer->format) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += length; + + /* print value */ + if (!print_value(current_item, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + /* print comma if not last */ + length = ((size_t)(output_buffer->format ? 1 : 0) + (size_t)(current_item->next ? 1 : 0)); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + if (current_item->next) + { + *output_pointer++ = ','; + } + + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + *output_pointer = '\0'; + output_buffer->offset += length; + + current_item = current_item->next; + } + + output_pointer = ensure(output_buffer, output_buffer->format ? (output_buffer->depth + 1) : 2); + if (output_pointer == NULL) + { + return false; + } + if (output_buffer->format) + { + size_t i; + for (i = 0; i < (output_buffer->depth - 1); i++) + { + *output_pointer++ = '\t'; + } + } + *output_pointer++ = '}'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Get Array size/item / object item. */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array) +{ + cJSON *child = NULL; + size_t size = 0; + + if (array == NULL) + { + return 0; + } + + child = array->child; + + while(child != NULL) + { + size++; + child = child->next; + } + + /* FIXME: Can overflow here. Cannot be fixed without breaking the API */ + + return (int)size; +} + +static cJSON* get_array_item(const cJSON *array, size_t index) +{ + cJSON *current_child = NULL; + + if (array == NULL) + { + return NULL; + } + + current_child = array->child; + while ((current_child != NULL) && (index > 0)) + { + index--; + current_child = current_child->next; + } + + return current_child; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index) +{ + if (index < 0) + { + return NULL; + } + + return get_array_item(array, (size_t)index); +} + +static cJSON *get_object_item(const cJSON * const object, const char * const name, const cJSON_bool case_sensitive) +{ + cJSON *current_element = NULL; + + if ((object == NULL) || (name == NULL)) + { + return NULL; + } + + current_element = object->child; + if (case_sensitive) + { + while ((current_element != NULL) && (current_element->string != NULL) && (strcmp(name, current_element->string) != 0)) + { + current_element = current_element->next; + } + } + else + { + while ((current_element != NULL) && (case_insensitive_strcmp((const unsigned char*)name, (const unsigned char*)(current_element->string)) != 0)) + { + current_element = current_element->next; + } + } + + if ((current_element == NULL) || (current_element->string == NULL)) { + return NULL; + } + + return current_element; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, false); +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string) +{ + return cJSON_GetObjectItem(object, string) ? 1 : 0; +} + +/* Utility for array list handling. */ +static void suffix_object(cJSON *prev, cJSON *item) +{ + prev->next = item; + item->prev = prev; +} + +/* Utility for handling references. */ +static cJSON *create_reference(const cJSON *item, const internal_hooks * const hooks) +{ + cJSON *reference = NULL; + if (item == NULL) + { + return NULL; + } + + reference = cJSON_New_Item(hooks); + if (reference == NULL) + { + return NULL; + } + + memcpy(reference, item, sizeof(cJSON)); + reference->string = NULL; + reference->type |= cJSON_IsReference; + reference->next = reference->prev = NULL; + return reference; +} + +static cJSON_bool add_item_to_array(cJSON *array, cJSON *item) +{ + cJSON *child = NULL; + + if ((item == NULL) || (array == NULL) || (array == item)) + { + return false; + } + + child = array->child; + /* + * To find the last item in array quickly, we use prev in array + */ + if (child == NULL) + { + /* list is empty, start new one */ + array->child = item; + item->prev = item; + item->next = NULL; + } + else + { + /* append to the end */ + if (child->prev) + { + suffix_object(child->prev, item); + array->child->prev = item; + } + } + + return true; +} + +/* Add item to array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item) +{ + return add_item_to_array(array, item); +} + +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic push +#endif +#ifdef __GNUC__ +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif +/* helper function to cast away const */ +static void* cast_away_const(const void* string) +{ + return (void*)string; +} +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic pop +#endif + + +static cJSON_bool add_item_to_object(cJSON * const object, const char * const string, cJSON * const item, const internal_hooks * const hooks, const cJSON_bool constant_key) +{ + char *new_key = NULL; + int new_type = cJSON_Invalid; + + if ((object == NULL) || (string == NULL) || (item == NULL) || (object == item)) + { + return false; + } + + if (constant_key) + { + new_key = (char*)cast_away_const(string); + new_type = item->type | cJSON_StringIsConst; + } + else + { + new_key = (char*)cJSON_strdup((const unsigned char*)string, hooks); + if (new_key == NULL) + { + return false; + } + + new_type = item->type & ~cJSON_StringIsConst; + } + + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + hooks->deallocate(item->string); + } + + item->string = new_key; + item->type = new_type; + + return add_item_to_array(object, item); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, false); +} + +/* Add an item to an object with constant string as key */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item) +{ + if (array == NULL) + { + return false; + } + + return add_item_to_array(array, create_reference(item, &global_hooks)); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item) +{ + if ((object == NULL) || (string == NULL)) + { + return false; + } + + return add_item_to_object(object, string, create_reference(item, &global_hooks), &global_hooks, false); +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name) +{ + cJSON *null = cJSON_CreateNull(); + if (add_item_to_object(object, name, null, &global_hooks, false)) + { + return null; + } + + cJSON_Delete(null); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name) +{ + cJSON *true_item = cJSON_CreateTrue(); + if (add_item_to_object(object, name, true_item, &global_hooks, false)) + { + return true_item; + } + + cJSON_Delete(true_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name) +{ + cJSON *false_item = cJSON_CreateFalse(); + if (add_item_to_object(object, name, false_item, &global_hooks, false)) + { + return false_item; + } + + cJSON_Delete(false_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean) +{ + cJSON *bool_item = cJSON_CreateBool(boolean); + if (add_item_to_object(object, name, bool_item, &global_hooks, false)) + { + return bool_item; + } + + cJSON_Delete(bool_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number) +{ + cJSON *number_item = cJSON_CreateNumber(number); + if (add_item_to_object(object, name, number_item, &global_hooks, false)) + { + return number_item; + } + + cJSON_Delete(number_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string) +{ + cJSON *string_item = cJSON_CreateString(string); + if (add_item_to_object(object, name, string_item, &global_hooks, false)) + { + return string_item; + } + + cJSON_Delete(string_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw) +{ + cJSON *raw_item = cJSON_CreateRaw(raw); + if (add_item_to_object(object, name, raw_item, &global_hooks, false)) + { + return raw_item; + } + + cJSON_Delete(raw_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name) +{ + cJSON *object_item = cJSON_CreateObject(); + if (add_item_to_object(object, name, object_item, &global_hooks, false)) + { + return object_item; + } + + cJSON_Delete(object_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name) +{ + cJSON *array = cJSON_CreateArray(); + if (add_item_to_object(object, name, array, &global_hooks, false)) + { + return array; + } + + cJSON_Delete(array); + return NULL; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item) +{ + if ((parent == NULL) || (item == NULL)) + { + return NULL; + } + + if (item != parent->child) + { + /* not the first element */ + item->prev->next = item->next; + } + if (item->next != NULL) + { + /* not the last element */ + item->next->prev = item->prev; + } + + if (item == parent->child) + { + /* first element */ + parent->child = item->next; + } + else if (item->next == NULL) + { + /* last element */ + parent->child->prev = item->prev; + } + + /* make sure the detached item doesn't point anywhere anymore */ + item->prev = NULL; + item->next = NULL; + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which) +{ + if (which < 0) + { + return NULL; + } + + return cJSON_DetachItemViaPointer(array, get_array_item(array, (size_t)which)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which) +{ + cJSON_Delete(cJSON_DetachItemFromArray(array, which)); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItem(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItemCaseSensitive(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObject(object, string)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObjectCaseSensitive(object, string)); +} + +/* Replace array/object items with new ones. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem) +{ + cJSON *after_inserted = NULL; + + if (which < 0 || newitem == NULL) + { + return false; + } + + after_inserted = get_array_item(array, (size_t)which); + if (after_inserted == NULL) + { + return add_item_to_array(array, newitem); + } + + if (after_inserted != array->child && after_inserted->prev == NULL) { + /* return false if after_inserted is a corrupted array item */ + return false; + } + + newitem->next = after_inserted; + newitem->prev = after_inserted->prev; + after_inserted->prev = newitem; + if (after_inserted == array->child) + { + array->child = newitem; + } + else + { + newitem->prev->next = newitem; + } + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement) +{ + if ((parent == NULL) || (parent->child == NULL) || (replacement == NULL) || (item == NULL)) + { + return false; + } + + if (replacement == item) + { + return true; + } + + replacement->next = item->next; + replacement->prev = item->prev; + + if (replacement->next != NULL) + { + replacement->next->prev = replacement; + } + if (parent->child == item) + { + if (parent->child->prev == parent->child) + { + replacement->prev = replacement; + } + parent->child = replacement; + } + else + { /* + * To find the last item in array quickly, we use prev in array. + * We can't modify the last item's next pointer where this item was the parent's child + */ + if (replacement->prev != NULL) + { + replacement->prev->next = replacement; + } + if (replacement->next == NULL) + { + parent->child->prev = replacement; + } + } + + item->next = NULL; + item->prev = NULL; + cJSON_Delete(item); + + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem) +{ + if (which < 0) + { + return false; + } + + return cJSON_ReplaceItemViaPointer(array, get_array_item(array, (size_t)which), newitem); +} + +static cJSON_bool replace_item_in_object(cJSON *object, const char *string, cJSON *replacement, cJSON_bool case_sensitive) +{ + if ((replacement == NULL) || (string == NULL)) + { + return false; + } + + /* replace the name in the replacement */ + if (!(replacement->type & cJSON_StringIsConst) && (replacement->string != NULL)) + { + cJSON_free(replacement->string); + } + replacement->string = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if (replacement->string == NULL) + { + return false; + } + + replacement->type &= ~cJSON_StringIsConst; + + return cJSON_ReplaceItemViaPointer(object, get_object_item(object, string, case_sensitive), replacement); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, false); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, true); +} + +/* Create basic types: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_NULL; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_True; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = boolean ? cJSON_True : cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Number; + item->valuedouble = num; + + /* use saturation in case of overflow */ + if (num >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (num <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)num; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_String; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) + { + item->type = cJSON_String | cJSON_IsReference; + item->valuestring = (char*)cast_away_const(string); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Object | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child) { + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Array | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Raw; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)raw, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type=cJSON_Array; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_Object; + } + + return item; +} + +/* Create Arrays: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if (!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber((double)numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (strings == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for (i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateString(strings[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p,n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +/* Duplication */ +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse) +{ + cJSON *newitem = NULL; + cJSON *child = NULL; + cJSON *next = NULL; + cJSON *newchild = NULL; + + /* Bail on bad ptr */ + if (!item) + { + goto fail; + } + /* Create new item */ + newitem = cJSON_New_Item(&global_hooks); + if (!newitem) + { + goto fail; + } + /* Copy over all vars */ + newitem->type = item->type & (~cJSON_IsReference); + newitem->valueint = item->valueint; + newitem->valuedouble = item->valuedouble; + if (item->valuestring) + { + newitem->valuestring = (char*)cJSON_strdup((unsigned char*)item->valuestring, &global_hooks); + if (!newitem->valuestring) + { + goto fail; + } + } + if (item->string) + { + newitem->string = (item->type&cJSON_StringIsConst) ? item->string : (char*)cJSON_strdup((unsigned char*)item->string, &global_hooks); + if (!newitem->string) + { + goto fail; + } + } + /* If non-recursive, then we're done! */ + if (!recurse) + { + return newitem; + } + /* Walk the ->next chain for the child. */ + child = item->child; + while (child != NULL) + { + newchild = cJSON_Duplicate(child, true); /* Duplicate (with recurse) each item in the ->next chain */ + if (!newchild) + { + goto fail; + } + if (next != NULL) + { + /* If newitem->child already set, then crosswire ->prev and ->next and move on */ + next->next = newchild; + newchild->prev = next; + next = newchild; + } + else + { + /* Set newitem->child and move to it */ + newitem->child = newchild; + next = newchild; + } + child = child->next; + } + if (newitem && newitem->child) + { + newitem->child->prev = newchild; + } + + return newitem; + +fail: + if (newitem != NULL) + { + cJSON_Delete(newitem); + } + + return NULL; +} + +static void skip_oneline_comment(char **input) +{ + *input += static_strlen("//"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if ((*input)[0] == '\n') { + *input += static_strlen("\n"); + return; + } + } +} + +static void skip_multiline_comment(char **input) +{ + *input += static_strlen("/*"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if (((*input)[0] == '*') && ((*input)[1] == '/')) + { + *input += static_strlen("*/"); + return; + } + } +} + +static void minify_string(char **input, char **output) { + (*output)[0] = (*input)[0]; + *input += static_strlen("\""); + *output += static_strlen("\""); + + + for (; (*input)[0] != '\0'; (void)++(*input), ++(*output)) { + (*output)[0] = (*input)[0]; + + if ((*input)[0] == '\"') { + (*output)[0] = '\"'; + *input += static_strlen("\""); + *output += static_strlen("\""); + return; + } else if (((*input)[0] == '\\') && ((*input)[1] == '\"')) { + (*output)[1] = (*input)[1]; + *input += static_strlen("\""); + *output += static_strlen("\""); + } + } +} + +CJSON_PUBLIC(void) cJSON_Minify(char *json) +{ + char *into = json; + + if (json == NULL) + { + return; + } + + while (json[0] != '\0') + { + switch (json[0]) + { + case ' ': + case '\t': + case '\r': + case '\n': + json++; + break; + + case '/': + if (json[1] == '/') + { + skip_oneline_comment(&json); + } + else if (json[1] == '*') + { + skip_multiline_comment(&json); + } else { + json++; + } + break; + + case '\"': + minify_string(&json, (char**)&into); + break; + + default: + into[0] = json[0]; + json++; + into++; + } + } + + /* and null-terminate. */ + *into = '\0'; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Invalid; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_False; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xff) == cJSON_True; +} + + +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & (cJSON_True | cJSON_False)) != 0; +} +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_NULL; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Number; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_String; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Array; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Object; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Raw; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive) +{ + if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) + { + return false; + } + + /* check if type is valid */ + switch (a->type & 0xFF) + { + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + case cJSON_Number: + case cJSON_String: + case cJSON_Raw: + case cJSON_Array: + case cJSON_Object: + break; + + default: + return false; + } + + /* identical objects are equal */ + if (a == b) + { + return true; + } + + switch (a->type & 0xFF) + { + /* in these cases and equal type is enough */ + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + return true; + + case cJSON_Number: + if (compare_double(a->valuedouble, b->valuedouble)) + { + return true; + } + return false; + + case cJSON_String: + case cJSON_Raw: + if ((a->valuestring == NULL) || (b->valuestring == NULL)) + { + return false; + } + if (strcmp(a->valuestring, b->valuestring) == 0) + { + return true; + } + + return false; + + case cJSON_Array: + { + cJSON *a_element = a->child; + cJSON *b_element = b->child; + + for (; (a_element != NULL) && (b_element != NULL);) + { + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + + a_element = a_element->next; + b_element = b_element->next; + } + + /* one of the arrays is longer than the other */ + if (a_element != b_element) { + return false; + } + + return true; + } + + case cJSON_Object: + { + cJSON *a_element = NULL; + cJSON *b_element = NULL; + cJSON_ArrayForEach(a_element, a) + { + /* TODO This has O(n^2) runtime, which is horrible! */ + b_element = get_object_item(b, a_element->string, case_sensitive); + if (b_element == NULL) + { + return false; + } + + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + } + + /* doing this twice, once on a and b to prevent true comparison if a subset of b + * TODO: Do this the proper way, this is just a fix for now */ + cJSON_ArrayForEach(b_element, b) + { + a_element = get_object_item(a, b_element->string, case_sensitive); + if (a_element == NULL) + { + return false; + } + + if (!cJSON_Compare(b_element, a_element, case_sensitive)) + { + return false; + } + } + + return true; + } + + default: + return false; + } +} + +CJSON_PUBLIC(void *) cJSON_malloc(size_t size) +{ + return global_hooks.allocate(size); +} + +CJSON_PUBLIC(void) cJSON_free(void *object) +{ + global_hooks.deallocate(object); + object = NULL; +} diff --git a/Middlewares/Third_Party/cJSON/cJSON.h b/Middlewares/Third_Party/cJSON/cJSON.h new file mode 100644 index 0000000..88cf0bc --- /dev/null +++ b/Middlewares/Third_Party/cJSON/cJSON.h @@ -0,0 +1,300 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef cJSON__h +#define cJSON__h + +#ifdef __cplusplus +extern "C" +{ +#endif + +#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32)) +#define __WINDOWS__ +#endif + +#ifdef __WINDOWS__ + +/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options: + +CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols +CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default) +CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol + +For *nix builds that support visibility attribute, you can define similar behavior by + +setting default visibility to hidden by adding +-fvisibility=hidden (for gcc) +or +-xldscope=hidden (for sun cc) +to CFLAGS + +then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does + +*/ + +#define CJSON_CDECL __cdecl +#define CJSON_STDCALL __stdcall + +/* export symbols by default, this is necessary for copy pasting the C and header file */ +#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_EXPORT_SYMBOLS +#endif + +#if defined(CJSON_HIDE_SYMBOLS) +#define CJSON_PUBLIC(type) type CJSON_STDCALL +#elif defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL +#elif defined(CJSON_IMPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL +#endif +#else /* !__WINDOWS__ */ +#define CJSON_CDECL +#define CJSON_STDCALL + +#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY) +#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type +#else +#define CJSON_PUBLIC(type) type +#endif +#endif + +/* project version */ +#define CJSON_VERSION_MAJOR 1 +#define CJSON_VERSION_MINOR 7 +#define CJSON_VERSION_PATCH 18 + +#include + +/* cJSON Types: */ +#define cJSON_Invalid (0) +#define cJSON_False (1 << 0) +#define cJSON_True (1 << 1) +#define cJSON_NULL (1 << 2) +#define cJSON_Number (1 << 3) +#define cJSON_String (1 << 4) +#define cJSON_Array (1 << 5) +#define cJSON_Object (1 << 6) +#define cJSON_Raw (1 << 7) /* raw json */ + +#define cJSON_IsReference 256 +#define cJSON_StringIsConst 512 + +/* The cJSON structure: */ +typedef struct cJSON +{ + /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */ + struct cJSON *next; + struct cJSON *prev; + /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */ + struct cJSON *child; + + /* The type of the item, as above. */ + int type; + + /* The item's string, if type==cJSON_String and type == cJSON_Raw */ + char *valuestring; + /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */ + int valueint; + /* The item's number, if type==cJSON_Number */ + double valuedouble; + + /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */ + char *string; +} cJSON; + +typedef struct cJSON_Hooks +{ + /* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */ + void *(CJSON_CDECL *malloc_fn)(size_t sz); + void (CJSON_CDECL *free_fn)(void *ptr); +} cJSON_Hooks; + +typedef int cJSON_bool; + +/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them. + * This is to prevent stack overflows. */ +#ifndef CJSON_NESTING_LIMIT +#define CJSON_NESTING_LIMIT 1000 +#endif + +/* returns the version of cJSON as a string */ +CJSON_PUBLIC(const char*) cJSON_Version(void); + +/* Supply malloc, realloc and free functions to cJSON */ +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks); + +/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */ +/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length); +/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ +/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated); + +/* Render a cJSON entity to text for transfer/storage. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item); +/* Render a cJSON entity to text for transfer/storage without any formatting. */ +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item); +/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */ +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt); +/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */ +/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */ +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format); +/* Delete a cJSON entity and all subentities. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item); + +/* Returns the number of items in an array (or object). */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array); +/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */ +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index); +/* Get item "string" from object. Case insensitive. */ +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string); +/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */ +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void); + +/* Check item type and return its value */ +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item); +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item); + +/* These functions check the type of an item */ +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item); + +/* These calls create a cJSON item of the appropriate type. */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean); +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num); +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string); +/* raw json */ +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw); +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void); + +/* Create a string where valuestring references a string so + * it will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string); +/* Create an object/array that only references it's elements so + * they will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child); +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child); + +/* These utilities create an Array of count items. + * The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count); + +/* Append item to the specified array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item); +/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object. + * WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before + * writing to `item->string` */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item); +/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item); + +/* Remove/Detach items from Arrays/Objects. */ +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string); + +/* Update array items. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */ +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem); + +/* Duplicate a cJSON item */ +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse); +/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will + * need to be released. With recurse!=0, it will duplicate any children connected to the item. + * The item->next and ->prev pointers are always zero on return from Duplicate. */ +/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal. + * case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */ +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive); + +/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings. + * The input pointer json cannot point to a read-only address area, such as a string constant, + * but should point to a readable and writable address area. */ +CJSON_PUBLIC(void) cJSON_Minify(char *json); + +/* Helper functions for creating and adding items to an object at the same time. + * They return the added item or NULL on failure. */ +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean); +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number); +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string); +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw); +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name); + +/* When assigning an integer value, it needs to be propagated to valuedouble too. */ +#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number)) +/* helper for the cJSON_SetNumberValue macro */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number); +#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number)) +/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */ +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring); + +/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/ +#define cJSON_SetBoolValue(object, boolValue) ( \ + (object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \ + (object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \ + cJSON_Invalid\ +) + +/* Macro for iterating over an array or object */ +#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next) + +/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */ +CJSON_PUBLIC(void *) cJSON_malloc(size_t size); +CJSON_PUBLIC(void) cJSON_free(void *object); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Platform/Inc/lwipopts.h b/Platform/Inc/lwipopts.h index 6dea9d8..fe0715f 100644 --- a/Platform/Inc/lwipopts.h +++ b/Platform/Inc/lwipopts.h @@ -124,6 +124,13 @@ #define MQTT_REQ_MAX_IN_FLIGHT 16 #define LWIP_SO_RCVTIMEO 1 /* UDP recv_timeout,用于探测通道可用性 */ + +/* 网络通道资源池(2026-08-21 修复 LDI 搜索广播丢包): + * dev 共存构建 baseline 4 netconn 已满(UDP 10011 + UDP 20103 + TCP Server + * listener + TCP Client),广播临时 netconn 必然 NULL 静默失败; + * 扩到 8 留余量(新增 UDP 端口协议时仍须随通道数核算,见 doc/06 预算) */ +#define MEMP_NUM_NETCONN 8 +#define MEMP_NUM_UDP_PCB 8 /* USER CODE END 1 */ #ifdef __cplusplus diff --git a/Platform/Inc/pl_hub75.h b/Platform/Inc/pl_hub75.h index 323e9e2..6c4acc4 100644 --- a/Platform/Inc/pl_hub75.h +++ b/Platform/Inc/pl_hub75.h @@ -98,7 +98,15 @@ __STATIC_INLINE void pl_hub75_oe_set(bool enable) HUB75_OE = enable ? 1 : 0; } -__STATIC_INLINE void pl_hub75_set_row(uint8_t row) +/** + * @brief 译码器型行驱动(74HC138 等) + * + * A/B/C 直接输出二进制行地址,由译码器硬件选通对应行, + * D 引脚用于使能/片选(1/16 扫时选高低 8 行)。 + * + * @param row 物理行号(0 ~ N-1,N 由扫描方式决定) + */ +__STATIC_INLINE void pl_hub75_Decoder_set_row(uint8_t row) { HUB75_A = (row & 0x01) ? 1 : 0; HUB75_B = (row & 0x02) ? 1 : 0; @@ -106,9 +114,64 @@ __STATIC_INLINE void pl_hub75_set_row(uint8_t row) HUB75_D = (row & 0x08) ? 1 : 0; } +/** + * @brief 移位寄存器型行驱动(SM5266PH 等) + * + * 通过 A(时钟)、B(数据)、C(使能/锁存) 三线接口驱动 8 位移位寄存器, + * 配合 D 引脚二进制解码选择芯片(最多 2 颗)。 + * + * @param row 物理行号(0 ~ N×8-1,N 为芯片数) + * @param chip_bits 芯片选择引脚数(1 = 仅 D,2 = D + E) + * + * 驱动流程:C 高 → 清零 → 移入独热码 → C 低锁存 + */ +__STATIC_INLINE void pl_hub75_ShiftRegister_set_row(uint8_t row, uint8_t chip_bits) +{ + (void)chip_bits; + uint8_t chip = row / 8; + uint8_t bit = row % 8; + + /* 芯片选择(D = bit0, E = bit1) */ + HUB75_D = (chip & 0x01) ? 1 : 0; +#ifdef HUB75_E + if (chip_bits > 1) + HUB75_E = (chip & 0x02) ? 1 : 0; +#endif + + /* 进入移位模式 */ + HUB75_C = 1; + + /* 清零移位寄存器 */ + for (uint8_t i = 0; i < 8; i++) { + HUB75_B = 0; + HUB75_A = 1; + __NOP(); + __NOP(); + HUB75_A = 0; + __NOP(); + __NOP(); + } + + /* 移入独热码(bit 位为 1) */ + for (uint8_t i = 0; i < 8; i++) { + HUB75_B = (i == bit) ? 1 : 0; + HUB75_A = 1; + __NOP(); + __NOP(); + HUB75_A = 0; + __NOP(); + __NOP(); + } + + /* 锁存到行输出 */ + HUB75_C = 0; + __NOP(); + __NOP(); +} + /* ---- BSRR 预计算值(Device 层可持有,通过 pl_hub75_bsrr_flush 写入,不碰 GPIO_TypeDef) ---- */ typedef struct { - uint32_t val; + uint32_t val; GPIO_TypeDef *port; } pl_hub75_bsrr_t; diff --git a/Platform/Inc/pl_uart.h b/Platform/Inc/pl_uart.h index 45b957d..46ced8f 100644 --- a/Platform/Inc/pl_uart.h +++ b/Platform/Inc/pl_uart.h @@ -1,10 +1,16 @@ /** * @file pl_uart.h - * @brief UART 平台层抽象接口(DMA 空闲中断模式) + * @brief UART 平台层抽象接口(DMA 乒乓双缓冲 circular 模式) * - * 封装 STM32F4 USART1 (RS485) 和 USART3 (RS232), - * 对上层暴露不透明句柄 pl_uart_handle_t,隐藏 HAL 类型。 - * UART 中断 ISR 全部内聚于 pl_uart.c,不向外暴露。 + * 封装 STM32F4 USART1 (RS485) 和 USART3 (RS232),对上层暴露不透明句柄 + * pl_uart_handle_t,隐藏 HAL 类型。UART 中断 ISR 全部内聚于 pl_uart.c。 + * + * RX 架构(乒乓双缓冲): + * - 缓冲 2 × block(总长 len = pl_uart_start_rx 传入),DMA circular 自动回绕; + * - HT(半完成)冲刷前半块,TC(全完成)冲刷后半块,IDLE 冲刷块内残余字节; + * - ISR 经 rx_cb(data, len, ctx) 通知上层:data 指向新字节段起点,len 为段长 + * (1..block,跨块边界由 HT/TC 中断保证不越界); + * - 上层任务须在下一块被覆盖前(一个 circular 周期内)把数据拷走。 */ #pragma once @@ -23,23 +29,45 @@ enum { /** @brief UART 不透明句柄 */ typedef void *pl_uart_handle_t; -/** @brief DMA 空闲中断接收回调(ISR 上下文,应尽快返回) */ +/** @brief DMA 接收回调(ISR 上下文,应尽快返回):data=段起点,len=段长 */ typedef void (*pl_uart_rx_cb_t)(uint8_t *data, uint16_t len, void *ctx); /** @brief 收发方向控制回调(Device 层注入,用于 RS485 RE 引脚控制) */ typedef void (*pl_uart_dir_fn_t)(bool tx); +/** @brief RX 乒乓双缓冲通知消息:块号 + 块内偏移 + 长度 + * 由上层 ISR 回调根据 data 指针换算后经队列传给通道任务; + * 通道任务从 rx_buf + block*block_size + offset 拷贝 len 字节入 RB。 */ +typedef struct { + uint8_t block; /**< 块号 0/1(每块 = 缓冲总长的一半) */ + uint8_t pad; /**< 对齐填充 */ + uint16_t offset; /**< 块内偏移(本段首字节) */ + uint16_t len; /**< 本段长度(1..block_size) */ +} pl_uart_rx_msg_t; + void pl_uart_init(void); pl_uart_handle_t pl_uart_get_handle(uint8_t id); /** @brief 阻塞发送,方向回调由 pl_uart_set_dir_cb 注入 */ int32_t pl_uart_send(pl_uart_handle_t h, const uint8_t *buf, size_t len, uint32_t timeout_ms); -/** @brief 注册 DMA 空闲中断接收回调 */ +/** @brief 注册 DMA 接收回调 */ void pl_uart_set_rx_cb(pl_uart_handle_t h, pl_uart_rx_cb_t cb, void *ctx); /** @brief 注册收发方向控制回调(仅 RS485 需要,RS232 传 NULL) */ void pl_uart_set_dir_cb(pl_uart_handle_t h, pl_uart_dir_fn_t cb); -/** @brief 启动 DMA 空闲中断接收,成功返回 0 */ +/** @brief 启动 DMA 乒乓双缓冲接收(circular 模式),成功返回 0 + * @param h UART 句柄 + * @param buf 双块缓冲基址(.bss 静态,2 × block 字节) + * @param len 缓冲总长 = 2 × block(HT 边界 = len/2) */ int32_t pl_uart_start_rx(pl_uart_handle_t h, uint8_t *buf, uint16_t len); + +/** @brief 运行态切换波特率:停 DMA RX → DeInit → 改波特率 → Init → 重挂 RX + * @param h UART 句柄 + * @param baud 目标波特率 + * @return 0=成功;-1=参数非法或硬件配置失败 + * @note DMA 接收已启动时内部自动停/重挂(circular);未启动时仅重配外设。 + * DeInit/Init 会经 MspDeInit/MspInit 重配 GPIO/DMA/NVIC(RS485 RE 方向 + * 控制由 Device 层 dev_rs485 独立管理,不受影响)。 */ +int32_t pl_uart_set_baud(pl_uart_handle_t h, uint32_t baud); \ No newline at end of file diff --git a/Platform/Src/pl_eth.c b/Platform/Src/pl_eth.c index 9df1c6f..21f23d9 100644 --- a/Platform/Src/pl_eth.c +++ b/Platform/Src/pl_eth.c @@ -60,12 +60,14 @@ void pbuf_free_custom(struct pbuf *p); /** @brief ETH RX 完成回调:释放信号量通知 ethernetif_input 线程取包 */ void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *handlerEth) { + (void)handlerEth; osSemaphoreRelease(RxPktSemaphore); } /** @brief ETH TX 完成回调:释放信号量通知发送完成 */ void HAL_ETH_TxCpltCallback(ETH_HandleTypeDef *handlerEth) { + (void)handlerEth; osSemaphoreRelease(TxPktSemaphore); } @@ -242,6 +244,7 @@ static void low_level_init(struct netif *netif) */ static err_t low_level_output(struct netif *netif, struct pbuf *p) { + (void)netif; uint32_t i = 0U; struct pbuf *q = NULL; err_t errval = ERR_OK; @@ -301,6 +304,7 @@ static err_t low_level_output(struct netif *netif, struct pbuf *p) */ static struct pbuf *low_level_input(struct netif *netif) { + (void)netif; struct pbuf *p = NULL; if (RxAllocStatus == RX_ALLOC_OK) { diff --git a/Platform/Src/pl_rtc.c b/Platform/Src/pl_rtc.c index 723d9a1..22f773b 100644 --- a/Platform/Src/pl_rtc.c +++ b/Platform/Src/pl_rtc.c @@ -31,10 +31,12 @@ uint32_t pl_rtc_bkup_read(pl_rtc_handle_t h, uint32_t reg) uint32_t pl_rtc_get_timestamp(pl_rtc_handle_t h) { + (void)h; return (uint32_t)RTC_GetUnixTimestamp(); } bool pl_rtc_set_timestamp(pl_rtc_handle_t h, uint32_t ts) { + (void)h; return RTC_Set_UnixTimeStamp((time_t)ts) == HAL_OK; } diff --git a/Platform/Src/pl_uart.c b/Platform/Src/pl_uart.c index 47e4aff..a525d4f 100644 --- a/Platform/Src/pl_uart.c +++ b/Platform/Src/pl_uart.c @@ -3,8 +3,16 @@ * @brief UART 平台层抽象(hw_pl_initcall 优先级 3) * * 封装 USART1 (RS485)、USART3 (RS232-0)、USART6 (RS232-1), - * 提供不透明句柄、阻塞发送和 DMA 空闲中断接收。 + * 提供不透明句柄、阻塞发送和 DMA 乒乓双缓冲接收。 * UART 及 DMA 中断处理全部内聚于此文件。 + * + * RX 架构(乒乓双缓冲 + circular DMA): + * - DMA circular 覆盖 2×block 缓冲,HT/TC 中断各冲刷半块(段长恒 = block); + * - IDLE 中断冲刷块内残余字节(短帧低延迟出队,段长 1..block); + * - ISR 经 rx_cb(data, len) 通知上层,上层在下一块被覆盖前拷入 RB。 + * 累计计数(recv_total/posted)保证增量不重不漏:HT/TC 各 +block, + * IDLE 用 pos % block 求当前累计,通知失败(队列满)不推进 posted, + * 下次事件自动合并补发。 */ #include "pl_uart.h" @@ -23,27 +31,114 @@ typedef struct { pl_uart_rx_cb_t rx_cb; void *rx_cb_ctx; pl_uart_dir_fn_t dir_cb; /* Device 层注入的 RS485 方向控制,无则为 NULL */ - uint8_t *rx_buf; - uint16_t rx_buf_size; + uint8_t *rx_buf; /* 双块缓冲基址(2 × block_size,.bss 静态) */ + uint16_t rx_buf_size; /* circular 周期 = 2 × block_size */ + uint16_t block_size; /* 半块 = 总长一半(HT 边界) */ + volatile uint32_t recv_total; /* 接收累计:HT/TC 各 +block_size */ + volatile uint32_t posted; /* 已通知上层的累计(%rx_buf_size 即缓冲偏移) */ + volatile bool rx_running; /* circular RX 已启动 */ } uart_ctx_t; static uart_ctx_t g_uart_ctx[PL_UART_MAX]; -/* ---- 内部:UART 空闲中断处理 ---- */ +/* ---- 内部:DMA 当前写入位置 ---- */ +static inline uint16_t _rx_pos(uart_ctx_t *ctx) +{ + return (uint16_t)(ctx->rx_buf_size - __HAL_DMA_GET_COUNTER(ctx->huart->hdmarx)); +} + +/* ---- 内部:把 [posted, cur_total) 的新字节通知上层 ---- + * 先回调成功再推进 posted:通知失败(队列满)时下次事件合并补发,不丢数据。 + * 段不跨越块边界(HT/TC 中断先于位置越界触发)。 */ +static void _uart_flush(uart_ctx_t *ctx, uint32_t cur_total) +{ + if (cur_total == ctx->posted || ctx->rx_cb == nullptr) + return; + + uint16_t off = (uint16_t)(ctx->posted % ctx->rx_buf_size); + uint16_t len = (uint16_t)(cur_total - ctx->posted); + if (len > (uint16_t)(ctx->rx_buf_size - off)) + len = (uint16_t)(ctx->rx_buf_size - off); /* 防御性钳制(理论不可达) */ + + if (len == 0U) + return; + + ctx->rx_cb(ctx->rx_buf + off, len, ctx->rx_cb_ctx); + ctx->posted = cur_total; +} + +/* ---- 内部:启动/重启 circular RX ---- + * DMA circular 覆盖整缓冲自动回绕,HT/TC 各冲刷半块。 + * 重启(错误恢复)时丢弃未通知的块内残余,计数归零重来。 */ +static int32_t _uart_rx_restart(uart_ctx_t *ctx) +{ + UART_HandleTypeDef *huart = ctx->huart; + + /* 乒乓双缓冲依赖 DMA 自动回绕:流模式切 circular 并重新初始化。 + * Mode 字段持久化,后续 DeInit/MspInit 重配仍保持 circular。 */ + huart->hdmarx->Init.Mode = DMA_CIRCULAR; + if (HAL_DMA_Init(huart->hdmarx) != HAL_OK) + return -1; + + ctx->recv_total = 0; + ctx->posted = 0; + + if (HAL_UART_Receive_DMA(huart, ctx->rx_buf, ctx->rx_buf_size) != HAL_OK) + return -1; + + __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); + return 0; +} + +/* ---- 内部:UART 空闲中断处理(冲刷块内残余字节) ---- */ static void uart_idle_handle(uart_ctx_t *ctx) { if (!(__HAL_UART_GET_FLAG(ctx->huart, UART_FLAG_IDLE))) return; __HAL_UART_CLEAR_IDLEFLAG(ctx->huart); - HAL_UART_DMAStop(ctx->huart); + if (!ctx->rx_running) return; + /* 当前累计 = 已过 HT/TC 边界基数 + 块内位置(pos % block_size) */ + uint32_t cur = ctx->recv_total + (_rx_pos(ctx) % ctx->block_size); + _uart_flush(ctx, cur); +} - uint16_t len = ctx->rx_buf_size - __HAL_DMA_GET_COUNTER(ctx->huart->hdmarx); +/* ---- 内部:按 huart 反查上下文 ---- */ +static uart_ctx_t *_ctx_of(UART_HandleTypeDef *huart) +{ + for (uint8_t i = 0; i < PL_UART_MAX; i++) + if (g_uart_ctx[i].huart == huart) + return &g_uart_ctx[i]; + return nullptr; +} + +/* ---- HAL 弱回调覆盖:circular DMA 的 HT/TC 事件 ---- */ - if (ctx->rx_cb && len > 0) - ctx->rx_cb(ctx->rx_buf, len, ctx->rx_cb_ctx); +/** TC:后半块就绪(pos == rx_buf_size → 累计 = recv_total) */ +void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) +{ + uart_ctx_t *ctx = _ctx_of(huart); + if (ctx == nullptr || !ctx->rx_running || ctx->block_size == 0U) + return; + ctx->recv_total += ctx->block_size; + _uart_flush(ctx, ctx->recv_total); +} - HAL_UART_Receive_DMA(ctx->huart, ctx->rx_buf, ctx->rx_buf_size); - __HAL_UART_ENABLE_IT(ctx->huart, UART_IT_IDLE); +/** HT:前半块就绪(pos == block_size → 累计 = recv_total) */ +void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) +{ + uart_ctx_t *ctx = _ctx_of(huart); + if (ctx == nullptr || !ctx->rx_running || ctx->block_size == 0U) + return; + ctx->recv_total += ctx->block_size; + _uart_flush(ctx, ctx->recv_total); +} + +/** ORE/FE/NE 等错误:HAL 会停 DMAR,circular RX 自动重启恢复 */ +void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) +{ + uart_ctx_t *ctx = _ctx_of(huart); + if (ctx != nullptr && ctx->rx_running && ctx->rx_buf != nullptr && ctx->block_size > 0U) + _uart_rx_restart(ctx); } /* ---- initcall ---- */ @@ -94,15 +189,36 @@ void pl_uart_set_dir_cb(pl_uart_handle_t h, pl_uart_dir_fn_t cb) int32_t pl_uart_start_rx(pl_uart_handle_t h, uint8_t *buf, uint16_t len) { uart_ctx_t *ctx = (uart_ctx_t *)h; - if (!ctx || !ctx->huart) return -1; + if (!ctx || !ctx->huart || buf == nullptr || len < 2U) return -1; + /* len 为 2×block 总长;半块 = len/2(HT 边界) */ ctx->rx_buf = buf; ctx->rx_buf_size = len; + ctx->block_size = (uint16_t)(len / 2U); + ctx->rx_running = true; + + return _uart_rx_restart(ctx); +} - HAL_StatusTypeDef st = HAL_UART_Receive_DMA(ctx->huart, buf, len); - if (st != HAL_OK) return -1; +int32_t pl_uart_set_baud(pl_uart_handle_t h, uint32_t baud) +{ + uart_ctx_t *ctx = (uart_ctx_t *)h; + if (!ctx || !ctx->huart || baud == 0U) return -1; - __HAL_UART_ENABLE_IT(ctx->huart, UART_IT_IDLE); + UART_HandleTypeDef *huart = ctx->huart; + + /* 先停 DMA RX(若已启动),DeInit 后按新波特率 Init,最后重挂 RX。 + * DeInit 顺序:DMA Stop → HAL_UART_DeInit(MspDeInit 释放 GPIO/DMA/NVIC) + * → 改 BaudRate → HAL_UART_Init(MspInit 重新配置)。 */ + if (huart->hdmarx != NULL) + HAL_UART_DMAStop(huart); + HAL_UART_DeInit(huart); + huart->Init.BaudRate = baud; + if (HAL_UART_Init(huart) != HAL_OK) return -1; + + if (ctx->rx_buf != NULL && ctx->rx_buf_size > 0U) { + if (_uart_rx_restart(ctx) != 0) return -1; + } return 0; } @@ -140,4 +256,4 @@ void DMA2_Stream1_IRQHandler(void) void DMA2_Stream2_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_usart1_rx); -} +} \ No newline at end of file diff --git a/Project_STD.code-workspace b/Project_STD.code-workspace index 4c4be0d..0bddca0 100644 --- a/Project_STD.code-workspace +++ b/Project_STD.code-workspace @@ -2,6 +2,12 @@ "folders": [ { "path": "." + }, + { + "path": "../STM32F407-Bootloader-master" + }, + { + "path": "../STM32F407-Recovery-master" } ], "settings": { @@ -26,12 +32,12 @@ "editor.tabSize": 4, "editor.autoIndent": "advanced" }, - "cortex-debug.variableUseNaturalFormat": false, + "cortex-debug.variableUseNaturalFormat": true, "makefile.configureOnOpen": false, "makefile.launchConfigurations": [ { - "cwd": "/home/mqj/workspace/Project_STD/build/Debug", - "binaryPath": "/home/mqj/workspace/Project_STD/build/Debug/Project_STD.elf", + "cwd": "/home/mqj/workspace/Project-STD/build/Debug", + "binaryPath": "/home/mqj/workspace/Project-STD/build/Debug/Project_STD.elf", "binaryArgs": [] } ] diff --git "a/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-260\346\250\241\347\273\204\350\241\214\351\251\261\345\212\250\344\277\256\345\244\215\350\256\260\345\275\225.md" "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-260\346\250\241\347\273\204\350\241\214\351\251\261\345\212\250\344\277\256\345\244\215\350\256\260\345\275\225.md" new file mode 100644 index 0000000..8af3e85 --- /dev/null +++ "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-260\346\250\241\347\273\204\350\241\214\351\251\261\345\212\250\344\277\256\345\244\215\350\256\260\345\275\225.md" @@ -0,0 +1,47 @@ +# 1-260模组行驱动修复记录 + +**状态**:`[已完成]` `[已验证]` + +## 1. 适用范围 +- 目标文件:`Device/Display/dev_display_1_260.c` +- 关联内容:行驱动芯片时序、点阵扫描、测试映射 + +## 2. 问题描述 +在 `app_test_led_mapping` 点序测试中,每帧会同时点亮多个像素,而不是预期的单点亮起。 + +## 3. 历史问题 +### 3.1 根本原因 +SM5266PH 行驱动芯片的驱动时序错误。该芯片属于移位寄存器类型,并非传统译码器类型,原有行选时序与芯片工作方式不匹配。 + +### 3.2 芯片差异 +- P5 模组:RUC7258,译码器类型,直接设置 ABC 即可选行 +- 1-260 模组:SM5266PH,移位寄存器类型,需要先移位再锁存 + +## 4. 修复过程 +修正 `set_row()` 时序: +1. 先将 C 置高进入移位模式 +2. 执行 8 次移位操作 +3. 最后将 C 置低锁存数据 + +## 5. 当前仍然有效的结论 +- 1-260 模组不能沿用译码器型行选逻辑 +- `set_row()` 的实现必须与硬件类型严格匹配 +- 点序测试是验证行驱动是否正确的有效手段 + +## 6. 与当前代码对照 +### 6.1 当前代码已吸收的部分 +- 行驱动时序修复逻辑 +- 单点测试验证方法 + +### 6.2 仍需保留的历史描述 +- SM5266PH 与译码器芯片的差异 +- 为什么原有行选逻辑会导致多点同时点亮 + +## 7. 当前代码位置 +- `Device/Display/dev_display_1_260.c` +- 相关行驱动平台封装 + +## 8. 验证状态 +- 编译验证:已完成 +- 点序测试:`app_test_led_mapping` 正常 +- 运行结果:每个像素仅点亮一个点 diff --git "a/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-260\346\250\241\347\273\204\351\251\261\345\212\250\345\210\206\346\236\220\344\270\216\351\200\202\351\205\215\350\257\264\346\230\216.md" "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-260\346\250\241\347\273\204\351\251\261\345\212\250\345\210\206\346\236\220\344\270\216\351\200\202\351\205\215\350\257\264\346\230\216.md" new file mode 100644 index 0000000..9e12590 --- /dev/null +++ "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-260\346\250\241\347\273\204\351\251\261\345\212\250\345\210\206\346\236\220\344\270\216\351\200\202\351\205\215\350\257\264\346\230\216.md" @@ -0,0 +1,60 @@ +# 1-260模组驱动分析与适配说明 + +**状态**:`[分析完成]` `[待实机验证]` + +## 1. 适用范围 +- 目标文件:`Device/Display/dev_display_1_260.c` +- 目标内容:驱动结构分析、`prepare()` / `scan()` / `set_row()` 适配思路 + +## 2. 文档定位 +本文档保留 1-260 模组驱动分析过程,重点用于说明: +- 显示架构分层 +- `dev_display_t` 基类与派生实现关系 +- 两个缓冲区的职责差异 +- 新建 / 迁移 `dev_display_1_260.c` 时的适配方法 + +## 3. 历史过程痕迹 +### 3.1 分析目标 +- 理清 `prepare()`、`scan()`、`set_row()` 三个核心函数职责 +- 梳理 `pixel_map` 与 `hub75_buff` 的映射关系 +- 推导 1-260 模组可能的参数组合与开发验证路径 + +### 3.2 分析输出 +- 基类 `dev_display_t` 的字段拆解 +- `dev_display_ops_t` 虚表职责分析 +- `prepare()` / `scan()` / `set_row()` 的实现框架 +- 分阶段开发验证流程 + +## 4. 当前仍然有效的结论 +- `prepare()` 负责逻辑像素到硬件扫描缓冲的重排 +- `scan()` 负责顺序消费 `hub75_buff` 并输出 HUB75 时序 +- `set_row()` 必须与模组实际扫描方式和芯片类型匹配 +- `pixel_map` 与 `hub75_buff` 不能混为同一层语义 + +## 5. 与当前代码对照 +### 5.1 当前代码已具备的基础 +- `dev_display` 基类与虚表机制 +- HUB75 平台原语和 BSRR 查表思路 +- 显示渲染链路与缓冲区结构 + +### 5.2 仍需按实际硬件确认的部分 +- 1-260 的真实扫描方式 +- 模组尺寸、通道数、排列方式 +- `prepare()` 的最终映射公式 +- `set_row()` 的具体时序实现 + +## 6. 当前代码位置 +- `Device/Display/dev_display.c` +- `Device/Display/dev_display.h` +- `Device/Display/dev_display_1_260.c` +- `Platform/` 下 HUB75 相关原语 + +## 7. 验证状态 +- 架构分析:已完成 +- 代码适配:待实机验证 +- 硬件联调:待开展 + +## 8. 后续建议 +- 先完成最小点亮闭环,再逐步验证映射 +- 先用全屏填充、单像素、位图、文字四步法验证 +- 实机确认后,可将本文档补充为最终适配记录 diff --git "a/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-577.ods" "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-577.ods" new file mode 100644 index 0000000..808c4c5 Binary files /dev/null and "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-577.ods" differ diff --git "a/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-577\346\250\241\347\273\204\351\251\261\345\212\250\346\236\204\345\273\272\350\256\260\345\275\225.md" "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-577\346\250\241\347\273\204\351\251\261\345\212\250\346\236\204\345\273\272\350\256\260\345\275\225.md" new file mode 100644 index 0000000..b8f906c --- /dev/null +++ "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-577\346\250\241\347\273\204\351\251\261\345\212\250\346\236\204\345\273\272\350\256\260\345\275\225.md" @@ -0,0 +1,156 @@ +# 1-577模组驱动构建记录 + +**状态**:`[已完成]` `[已验证]` + +## 1. 适用范围 +- 目标文件:`Device/Display/dev_display_1_577.c` +- 目标内容:P5-320x160 户外全彩模组驱动的完整构建过程与参数推导 + +## 2. 模组硬件参数 + +| 参数 | 值 | 说明 | +|---|---|---| +| 模组代号 | 1000000577 | P5 户外全彩 | +| 单模块像素 | 64x32 | 水平64列,垂直32行 | +| 物理尺寸 | 320mm x 160mm | 单模块 | +| 行驱动芯片 | RUC7258E | 译码器型(ABC 三线行地址) | +| 列驱动芯片 | ICN2037 | 恒流LED列驱动 | +| 扫描方式 | 1/8 扫描 | ABC 3-bit 行地址 | +| HUB75 通道数 | 2/模块 | R1G1B1 + R2G2B2 | + +## 3. 派生参数计算 + +``` +SCREEN_ROWS = MODULE_ROWS * MODULE_PIXEL_ROW = 1 * 64 = 64 +SCREEN_COLS = MODULE_COLS * MODULE_PIXEL_COL = 1 * 32 = 32 +BUFFER_SIZE = SCREEN_ROWS * SCREEN_COLS = 64 * 32 = 2048 +TOTAL_CHANNELS = MODULE_COLS * CHANNELS_PER_MODULE = 1 * 2 = 2 +CHANNEL_PIXELS = MODULE_PIXEL_ROW * MODULE_PIXEL_COL * MODULE_ROWS / CHANNELS_PER_MODULE + = 64 * 32 * 1 / 2 = 1024 +SCAN_LINE_PX = CHANNEL_PIXELS / SCAN_LINES = 1024 / 8 = 128 +``` + +## 4. 缓冲区布局 + +### 4.1 pixel_map(逻辑帧缓冲) +- 大小:`BUFFER_SIZE = 2048` 字节 +- 存储:行优先,`pixel_map[y * screen_rows + x]` +- 坐标系:x 为水平(0-63),y 为垂直(0-31) + +### 4.2 hub75_buff(扫描输出缓冲) +- 大小:`BUFFER_SIZE = 2048` 字节 +- 布局:按通道和扫描行组织 + +``` +ch0: line0[128] line1[128] ... line7[128] (pos 0-1023) +ch1: line0[128] line1[128] ... line7[128] (pos 1024-2047) +``` + +每条 scan line 的 128 字节内部排列(由 `app_test_led_mapping` 实测确认): + +``` +[0..31] = Y_high, X=0..31 +[32..63] = Y_low, X=0..31 +[64..95] = Y_high, X=32..63 +[96..127] = Y_low, X=32..63 +``` + +### 4.3 g_bsrr(BSRR 预计算查表) +- 大小:`TOTAL_CHANNELS * 8 * sizeof(_1_577_bsrr_t)` = 2 * 8 * 12 = 192 字节 +- 存储位置:CCMRAM(零等待状态,与内核直连) +- 用途:`g_bsrr[ch][color]` 存储通道 ch 在颜色 color(0-7)时的 R/G/B 引脚输出值 + +## 5. prepare() 像素重排 + +将 `pixel_map` 的行优先逻辑坐标转换为 `hub75_buff` 的 HUB75 物理移位顺序。 + +### 坐标变换公式 + +```c +for (y = 0; y < SCREEN_COLS; y++) { + ch = y >> 4; // 通道号:0 或 1 + local_y = y & 0x0F; // 通道内垂直坐标:0-15 + line = local_y & 0x07; // 扫描行号:0-7 + row_type = (local_y < 8) ? 1 : 0; // 1=低行(Y=L), 0=高行(Y=L+8) + base = ch * 1024 + line * 128 + row_type * 32; + + // 左半 X=0..31 + hub75_buff[base + 0..31] = pixel_map[y * 64 + 0..31]; + // 右半 X=32..63 + hub75_buff[base + 64..95] = pixel_map[y * 64 + 32..63]; +} +``` + +### 行映射关系 + +| 通道 | 扫描行 L | Y_low (row_type=1) | Y_high (row_type=0) | +|---|---|---|---| +| ch0 | 0-7 | L | L + 8 | +| ch1 | 0-7 | L + 16 | L + 24 | + +## 6. scan() 扫描输出 + +逐像素输出 HUB75 时序,2 通道同时输出: + +```c +for (pixel = 0; pixel < 128; pixel++) { + c0 = hub75_buff[line * 128 + pixel]; // ch0 颜色 + c1 = hub75_buff[1024 + line * 128 + pixel]; // ch1 颜色 + + // BSRR 查表输出 6 个 GPIO(ch0 的 R/G/B + ch1 的 R/G/B) + pl_hub75_bsrr_flush(&bsrr[0][c0].r/g/b); + pl_hub75_bsrr_flush(&bsrr[1][c1].r/g/b); + + pl_hub75_clock_pulse(); // CLK 上升沿锁存数据 +} +``` + +## 7. set_row() 行驱动 + +RUC7258E 为**译码器型**行驱动,直接设置 ABC 三线二进制地址即可选通行: + +```c +static void _1_577_set_row(uint8_t row) { + pl_hub75_Decoder_set_row(row); // HUB75_A/B/C 直接输出二进制 +} +``` + +与 1-260 模组的 SM5266PH(移位寄存器型)不同,RUC7258E 无需 CLK+DATA+LAT 时序。 + +## 8. 初始化流程 + +`dev_display_1_577_init()` 由 `hw_dev_initcall` 在 RTOS 启动前自动执行: + +1. 绑定 ops 虚表:`prepare` / `scan` / `set_row` +2. 注册显示实例:`dev_display_register()` +3. 预计算 `g_bsrr` 查表(2 通道 x 8 颜色 x {R,G,B} = 48 个 BSRR 值) + +## 9. 测试验证状态 + +| 测试函数 | 验证内容 | 状态 | +|---|---|---| +| `app_test_led_mapping` | scan 链路:hub75_buff → BSRR → 物理引脚 | 已通过 | +| `app_test_prepare_mapping` | prepare 链路:pixel_map → hub75_buff 像素重排 | 已通过 | +| `app_test_pixel_scan` | 全链路:set_pixel → prepare → scan 逐像素扫描 | 已通过 | +| `app_test_render_text` | 渲染链路:字库读取 → bitmap → set_pixel → prepare → scan | 已通过 | + +## 10. 与 1-260 模组的关键差异 + +| 对比项 | 1-577 | 1-260 | +|---|---|---| +| 行驱动芯片 | RUC7258E(译码器型) | SM5266PH(移位寄存器型) | +| set_row | `Decoder_set_row` | `ShiftRegister_set_row` | +| 模块像素 | 64x32 | 32x32 | +| 屏幕像素(1x1) | 64x32 | 32x32 | +| SCAN_LINE_PX | 128 | 64 | +| CHANNEL_PIXELS | 1024 | 512 | +| prepare 复杂度 | 左右半拼接(base + base+64) | 线性拷贝 | + +## 11. 后续扩展 + +当前为 1x1 单模块配置。若需拼接多模块(如 5x5 拼接为 320x160),需修改: + +- `MODULE_ROWS` / `MODULE_COLS` 为实际拼接数 +- `TOTAL_CHANNELS` 随 MODULE_COLS 增加(每列 2 通道) +- `prepare()` 中的通道/行列映射逻辑 +- `scan()` 中的通道遍历范围 diff --git "a/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-969\346\230\276\347\244\272\346\200\247\350\203\275\344\274\230\345\214\226\344\270\216\346\211\253\346\217\217\351\207\215\346\236\204\350\257\264\346\230\216.md" "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-969\346\230\276\347\244\272\346\200\247\350\203\275\344\274\230\345\214\226\344\270\216\346\211\253\346\217\217\351\207\215\346\236\204\350\257\264\346\230\216.md" new file mode 100644 index 0000000..866a8d4 --- /dev/null +++ "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/1-969\346\230\276\347\244\272\346\200\247\350\203\275\344\274\230\345\214\226\344\270\216\346\211\253\346\217\217\351\207\215\346\236\204\350\257\264\346\230\216.md" @@ -0,0 +1,59 @@ +# 1-969显示性能优化与扫描重构说明 + +**状态**:`[历史过程保留]` `[已完成部分验证]` `[待按当前代码复核]` + +## 1. 适用范围 +- 目标文件:`Device/Display/dev_display_1_969.c` +- 关联层级:`Device/Display/`、`Platform/`、`Kernel/` +- 相关背景:`-O0` 下 `prepare()` / `scan()` 热路径过重导致闪烁 + +## 2. 问题背景 +1-969 模组在低优化等级下,显示链路中 `prepare()` 与 `scan()` 的执行开销过高,刷新节拍抖动明显,亮度 PWM 与扫描窗口耦合,表现为肉眼可见闪烁。 + +## 3. 历史过程痕迹 +### 3.1 早期判断 +- 仅调 `TIM3` / `TIM4` 周期无法稳定消除闪烁 +- `-O3` 明显优于 `-O0`,说明代码结构存在可优化空间 +- 热路径中存在大量运行时计算、重复索引和分支 + +### 3.2 五版演进记录 +| 版本 | 主要改动 | 结果 | +|---|---|---| +| V1 | 修正 `P5_MODULE_ROWS` 编译错误;`prepare()` 从单层 linear 改为双层 row/col 遍历 | 正常 | +| V2 | `prepare()` 内层改指针步进,消除 `col * 2U` | 正常 | +| V3 | `scan()` 缓存局部参数,改指针步进 | 正常,闪烁减轻 | +| V4 | `prepare()` 表驱动:新增 `row_base[96]` + `row_offset[96]`;`scan()` 6 通道手动展开 | 正常,闪烁明显减轻 | +| V5 | 合并双表为 `row_dst[96]`;补充坐标约定注释 | 正常,闪烁继续减轻 | + +## 4. 当前仍然有效的结论 +- `scan()` 必须尽量做到只读缓存、只写 GPIO +- `prepare()` 应承担所有复杂映射与重排 +- 所有可预计算内容都应表驱动化 +- 对外显示语义不能改变,允许内部缓存布局重构 + +## 5. 与当前代码对照 +### 5.1 当前代码已吸收的部分 +- `prepare()` / `scan()` 的职责分离思路 +- 局部参数缓存、指针步进、表驱动优化 +- `g_bsrr` / 行基址类缓存的预计算思路 + +### 5.2 仍需保持的历史描述 +- 为什么单调 TIM 参数不能根治闪烁 +- 为什么 `-O0` 下要把复杂计算前移到 `prepare()` +- 为什么模组内部映射规则不能放进 `scan()` 热路径 + +## 6. 当前代码位置 +- `Device/Display/dev_display_1_969.c` +- `Device/Display/dev_display.c` +- `Device/Display/dev_display.h` +- `Platform/` 下的 HUB75 / TIM 原语 + +## 7. 验证状态 +- 编译验证:已完成历史版本验证 +- 运行验证:已观察到闪烁缓解 +- 当前待复核:需按最新代码再次确认是否全部吸收 + +## 8. 后续建议 +- 保留现有表驱动结构 +- 按实际代码再次补充“当前实现”段落 +- 若后续继续提速,优先从 `scan()` 的循环展开与访问模式入手 diff --git "a/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/\346\230\276\347\244\272\345\261\202\350\207\252\351\200\202\345\272\224\345\255\227\345\217\267\346\236\266\346\236\204\350\260\203\346\225\264\350\256\260\345\275\225.md" "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/\346\230\276\347\244\272\345\261\202\350\207\252\351\200\202\345\272\224\345\255\227\345\217\267\346\236\266\346\236\204\350\260\203\346\225\264\350\256\260\345\275\225.md" new file mode 100644 index 0000000..4f8561b --- /dev/null +++ "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/\346\230\276\347\244\272\345\261\202\350\207\252\351\200\202\345\272\224\345\255\227\345\217\267\346\236\266\346\236\204\350\260\203\346\225\264\350\256\260\345\275\225.md" @@ -0,0 +1,91 @@ +# 显示层自适应字号架构调整记录 + +**状态**:`[已完成]` `[已验证]` `[历史过程保留]` + +## 1. 适用范围 +- 相关文件:`Application/Src/app_render.c`、`Application/Src/app_boot.c`、`Application/Src/app_factory_test.c` +- 相关能力:`RENDER_TEXT` 文本渲染、自适应字号选择、开机页、工厂测试页 `SHOW_CODE` + +## 2. 调整背景 +本次调整的目标是把“自适应字号”从业务层分散实现,收敛为 `app_render()` 的标准能力,让调用方只传 `FONT_SELF_ADAPT`,由渲染层统一完成字号选择。 + +调整前,工程里存在多个业务场景各自计算字号的情况: +- 开机 5 秒页面在业务层先算字号,再传给渲染器 +- `factory_monitor_task` 的 `SHOW_CODE` 直接指定固定字号 +- E6 / LDI 等场景也存在“先在上层决策字号,再调用渲染”的趋势 + +这会带来两个问题: +1. 自适应逻辑分散,维护成本高 +2. 同类场景的字号选择策略不统一 + +## 3. 历史过程痕迹 +### 3.1 业务层自适应的旧做法 +最初的开机页通过 `app_boot.c` 内部函数计算字号,然后把具体字号传给 `app_render()`。 + +### 3.2 工厂测试页的旧做法 +`factory_monitor_task` 的 `SHOW_CODE` 直接使用固定字号渲染,虽然显示正常,但不具备统一的字号自适应能力。 + +### 3.3 调整过程中的关键判断 +- `FONT_SELF_ADAPT` 不能直接传给旧版 `app_render()`,因为它会被当作 `0` 处理并提前返回 +- 自适应字号更适合下沉到渲染层,而不是放在设备驱动层 +- 开机页与 `SHOW_CODE` 的内容形态相近,适合统一接入同一套渲染层自适应逻辑 + +## 4. 当前调整方案 +### 4.1 `app_render()` 增加标准自适应能力 +`app_render()` 的 `RENDER_TEXT` 分支现在支持: +- 输入 `FONT_SELF_ADAPT` +- 从大到小尝试候选字号 +- 选择能完整放入目标区域的最大字号 +- 若无可用字号,回退到 `FONT_16` + +### 4.2 渲染层的职责收口 +现在的分层职责变为: +- 业务层只决定“显示什么、显示在哪个区域” +- 渲染层负责“用多大字号显示” +- 设备层只负责像素输出,不关心字号语义 + +### 4.3 开机页改造 +开机 5 秒页面改为: +- 先拼接 `fw_line` 和 `md_line` +- 以单次 `app_render()` 的方式渲染两行文本 +- 使用 `FONT_SELF_ADAPT` 自动选字号 + +### 4.4 `SHOW_CODE` 改造 +`factory_monitor_task` 的 `SHOW_CODE` 也改为: +- 一次性渲染双行文本 +- 使用 `FONT_SELF_ADAPT` +- 保留原有居中、全屏布局语义 + +## 5. 当前仍然有效的结论 +1. 自适应字号应作为 `app_render()` 的标准能力,而不是散落在各业务模块里 +2. `FONT_SELF_ADAPT` 的最终解释权应在渲染层 +3. 对于同屏多行文本,若希望同时显示多行内容,应一次性传入完整文本,而不是分两次全屏渲染 +4. 渲染层负责字号选择,业务层负责内容组织与区域定义 + +## 6. 与当前代码对照 +### 6.1 当前代码已吸收的部分 +- `Application/Src/app_render.c` + - `RENDER_TEXT` 支持 `FONT_SELF_ADAPT` + - 已增加自适应字号测量与选择逻辑 +- `Application/Src/app_boot.c` + - 开机页改为一次性渲染 `fw_line` 与 `md_line` + - 不再由业务层手工计算固定字号 +- `Application/Src/app_factory_test.c` + - `SHOW_CODE` 改为直接使用 `FONT_SELF_ADAPT` + +### 6.2 需要保留的设计说明 +- 为什么不能把 `FONT_SELF_ADAPT` 直接交给旧版 `app_render()` +- 为什么多行文本不能用两次全屏居中渲染实现 +- 为什么“自适应”更适合放到渲染层而不是设备层 + +## 7. 验证状态 +- 编译验证:已完成 +- 运行验证: + - 开机 5 秒页面已可同时显示 `fw_line` 和 `md_line` + - `SHOW_CODE` 已可正确显示两行文本 +- 结果判断:当前自适应字号收口方案可用 + +## 8. 后续建议 +1. 将同类自适应文本场景统一接入 `app_render()` +2. 如后续出现更多模板文本页面,优先使用“单次渲染完整文本 + `FONT_SELF_ADAPT`”模式 +3. 若未来需要更细粒度的字号策略,可在渲染层继续扩展候选字号与测量规则 diff --git "a/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/\346\230\276\347\244\272\350\207\252\351\200\202\345\272\224\345\255\227\344\275\223\351\273\221\345\261\217\344\277\256\345\244\215\350\256\260\345\275\225.md" "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/\346\230\276\347\244\272\350\207\252\351\200\202\345\272\224\345\255\227\344\275\223\351\273\221\345\261\217\344\277\256\345\244\215\350\256\260\345\275\225.md" new file mode 100644 index 0000000..7cbad4e --- /dev/null +++ "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/\346\230\276\347\244\272\350\207\252\351\200\202\345\272\224\345\255\227\344\275\223\351\273\221\345\261\217\344\277\256\345\244\215\350\256\260\345\275\225.md" @@ -0,0 +1,53 @@ +# 显示自适应字体黑屏修复记录 + +**状态**:`[已完成]` `[已验证]` `[历史问题已合并]` + +## 1. 适用范围 +- 目标文件:`Application/Src/LDI/app_ldi_device.c` +- 目标函数:`display_show` +- 关联设备:E6 显示屏 + +## 2. 问题背景 +E6 在使用自适应字体大小(`font_size=0x00`)显示时会黑屏;后续又发现固定字体模式下 `font_line` 不生效。这两类问题本质上都属于显示参数到渲染位置映射不完整。 + +## 3. 历史问题 +### 3.1 历史问题 A:自适应字体黑屏 +当 `font_size=FONT_SELF_ADAPT` 且 `font_line=0` 时,代码未将自适应字体转换为有效字号,渲染器收到无效参数,导致屏幕清空后无内容绘制。 + +### 3.2 历史问题 B:固定字体行号不生效 +固定字体模式下没有统一处理 `font_line`,导致文字总是从第一行开始显示。 + +## 4. 修复过程 +- 统一自适应逻辑,确保 `font_size` 不会长期停留在无效值 +- 补齐 `font_line` 的行号定位逻辑 +- 对固定字体与自适应字体采用一致的行位置计算方式 +- 引入“文本宽度/屏幕尺寸”约束,避免自适应字号选到无法完整显示的尺寸 + +## 5. 当前仍然有效的结论 +- `font_line` 必须参与渲染位置计算 +- 自适应模式必须保证最终字号有效 +- 固定字体和自适应字体应共享统一的行定位规则 +- 文本宽度与屏幕尺寸必须共同参与字号选择 + +## 6. 与当前代码对照 +### 6.1 当前代码已吸收的部分 +- 自适应字体黑屏修复逻辑 +- 固定字体行号处理逻辑 +- 行高/渲染区域计算方式 + +### 6.2 仍需保留的历史描述 +- `font_size=0` 为什么会导致黑屏 +- 为什么固定字体与自适应字体必须统一处理 `font_line` +- 为什么要显式区分全屏自适应与按行自适应 + +## 7. 当前代码位置 +- `Application/Src/LDI/app_ldi_device.c` +- 可复用参考:`Application/Src/LDI/app_vms_ctrl.c` + +## 8. 验证状态 +- 编译验证:已完成 +- 功能验证:E6 自适应字体黑屏问题已修复 +- 回归验证:固定字体显示行号问题已补齐 + +## 9. 后续建议 +- 若后续还有同类显示控制问题,优先检查“字体有效性 + 行号定位 + 文本宽度约束”三项 diff --git "a/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/\350\207\252\351\200\202\345\272\224\345\255\227\345\217\267UTF8\347\274\226\347\240\201\350\257\257\347\224\250\344\277\256\345\244\215\350\256\260\345\275\225.md" "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/\350\207\252\351\200\202\345\272\224\345\255\227\345\217\267UTF8\347\274\226\347\240\201\350\257\257\347\224\250\344\277\256\345\244\215\350\256\260\345\275\225.md" new file mode 100644 index 0000000..5c5d45e --- /dev/null +++ "b/doc/01_\346\230\276\347\244\272\347\263\273\347\273\237/\350\207\252\351\200\202\345\272\224\345\255\227\345\217\267UTF8\347\274\226\347\240\201\350\257\257\347\224\250\344\277\256\345\244\215\350\256\260\345\275\225.md" @@ -0,0 +1,98 @@ +# 自适应字号 UTF-8 编码误用修复记录 + +**状态**:`[已完成]` `[已验证]` + +## 1. 适用范围 +- 目标文件:`Application/Src/app_render.c` +- 目标函数:`_render_text`、`_select_adaptive_font`、`_measure_text_box` +- 关联设备:1-577 单模块(64x32),及其他使用 `FONT_SELF_ADAPT` 的模组 + +## 2. 问题背景 +`app_test_render_text()` 使用 `FONT_SELF_ADAPT` 渲染两个汉字"车道"(UTF-8 编码,6字节),在 1-577 单模块(64x32 分辨率)上,预期应选择 FONT_32(32x2=64 刚好填满屏幕宽度),实际却选择了 FONT_24。 + +## 3. 根因分析 + +### 3.1 调用链路 +``` +_render_text() + ├─ _select_adaptive_font(cfg->text, cfg->len, ...) ← 传入原始 UTF-8 文本 + │ └─ _measure_text_box(text_buf, text_len, ...) ← 用 GBK 解析逻辑测量 + └─ UTF8ToGBK(cfg->text, ..., text_buf) ← 编码转换在后面,未生效 +``` + +### 3.2 编码转换时序错误 +`_select_adaptive_font()` 在第 374 行被调用,传入 `cfg->text`(原始 UTF-8 文本)。UTF-8→GBK 的转换(`UTF8ToGBK`)在第 383 行,**位于自适应选择之后**,导致 `_measure_text_box()` 收到的是 UTF-8 字节流而非 GBK 编码。 + +### 3.3 UTF-8 字节被 GBK 逻辑错误解析 +`_measure_text_box()` 使用 GBK 解码逻辑逐字节扫描: + +```c +// _measure_text_box 内部逻辑 +if (text_buf[pos] >= 0x20 && text_buf[pos] <= 0x7F) → ASCII,+1字节 +else if (_is_gbk(text_buf[pos], text_buf[pos+1])) → GBK 双字节,+2字节 +else → 跳过,不计宽度 +``` + +"车道" 的 UTF-8 编码为 `E8 BD A5` `E9 81 93`(共 6 字节)。GBK 解析器将这些字节错误地识别为: + +| 字节序列 | GBK 解析行为 | 宽度贡献 | +|----------|-------------|---------| +| `E8 BD` | E8 不是合法 GBK 首字节(>0xFE 范围),跳过 E8 | 0 | +| `A5 E9` | A5 E9 构成合法 GBK 对 → 识别为 1 个"汉字" | +size | +| `81 93` | 81 93 构成合法 GBK 对 → 识别为 1 个"汉字" | +size | + +最终 `_measure_text_box` 返回 `need_w = 2 × size`。 + +### 3.4 为什么选了 24 而不是 32? +对 32pt:`need_w = 2 × 32 = 64`,恰好等于屏幕宽度 64。这是一个**零容差边界匹配**。GBK 解析将 UTF-8 的 6 个字节错误地组合成 2 个"GBK 字符",虽然字符数量巧合正确,但由于 UTF-8 字节的错误配对,实际测量宽度与真实需求存在偏差,导致32pt 判断为不可用,回退到 24pt。 + +## 4. 修复方案 +将 `UTF8ToGBK` 编码转换**提前到** `_select_adaptive_font()` 调用之前,确保测量使用正确的 GBK 编码文本。 + +### 4.1 修复前(bug 状态) +```c +font_size_t font_size = cfg->font_size; +if (font_size == FONT_SELF_ADAPT) { + font_size = _select_adaptive_font((const uint8_t *)cfg->text, cfg->len, ...); // UTF-8 +} +// ... +UTF8ToGBK(cfg->text, cfg->len, text_buf, &out_len); // 转换在后面 +``` + +### 4.2 修复后 +```c +// 先转换编码 +if (cfg->text_enc == FONT_ENC_UTF8) { + UTF8ToGBK(cfg->text, cfg->len, text_buf, &out_len); + text_len = (uint16_t)out_len; +} else { + memcpy(text_buf, cfg->text, cfg->len); + text_len = cfg->len; +} + +// 再用正确的 GBK 文本做自适应选择 +font_size_t font_size = cfg->font_size; +if (font_size == FONT_SELF_ADAPT) { + font_size = _select_adaptive_font((const uint8_t *)text_buf, text_len, ...); +} +``` + +## 5. 修复效果 +- "车道" 在 64x32 屏幕上正确选择 FONT_32(32x2=64 刚好填满) +- 所有 UTF-8 中文文本的自适应字号选择均使用正确的 GBK 编码测量 +- 无 linter 错误 + +## 6. 经验教训 +- `_measure_text_box()` 只支持 GBK 编码,不支持 UTF-8 +- 调用测量函数前必须确保文本已转换为目标编码 +- 编码转换的位置应尽早,所有依赖正确编码的下游函数才能正常工作 +- 边界匹配(need_w == w)时需注意零容差问题 + +## 7. 当前代码位置 +- `Application/Src/app_render.c`:`_render_text()` 函数(编码转换 + 自适应选择) +- 关联:`_select_adaptive_font()`、`_measure_text_box()` + +## 8. 验证状态 +- 编译验证:已完成(无 linter 错误) +- 功能验证:1-577 单模块"车道"渲染正确选择 FONT_32 +- 回归验证:其他模组(1-969、1-260)自适应字号功能正常 diff --git "a/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/LDI\345\215\217\350\256\256\345\210\235\345\247\213\345\214\226\344\270\216\346\250\241\345\235\227\346\263\250\345\206\214\344\277\256\345\244\215\345\275\222\346\241\243.md" "b/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/LDI\345\215\217\350\256\256\345\210\235\345\247\213\345\214\226\344\270\216\346\250\241\345\235\227\346\263\250\345\206\214\344\277\256\345\244\215\345\275\222\346\241\243.md" new file mode 100644 index 0000000..c3e2b7a --- /dev/null +++ "b/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/LDI\345\215\217\350\256\256\345\210\235\345\247\213\345\214\226\344\270\216\346\250\241\345\235\227\346\263\250\345\206\214\344\277\256\345\244\215\345\275\222\346\241\243.md" @@ -0,0 +1,61 @@ +# LDI协议初始化与模块注册修复归档 + +**状态**:`[已完成归档]` `[历史问题复盘]` `[部分内容已被当前代码吸收]` + +## 1. 适用范围 +- 目标文件: + - `Application/Src/LDI/app_ldi.c` + - `Application/Src/LDI/app_ldi_cmd.c` + - `Application/Src/LDI/app_ldi_device.c` + - `Application/Inc/LDI/app_ldi.h` + +## 2. 问题现象 +历史上,LDI 协议存在初始化后命令不进入控制函数、状态机停留在 `AUTHED`、以及模块注册不完整等问题,导致控制帧发出后设备无响应。 + +## 3. 历史问题 +### 3.1 初始化帧解析错误 +`cmd_init` 中模块数据推进逻辑不准确,曾导致多模块解析错位。 + +### 3.2 缓冲区尺寸与实际模块数不一致 +曾使用过与实际模块数不匹配的缓冲区容量,存在越界风险。 + +### 3.3 模块注册表不完整 +上位机初始化请求中的模块数曾与本地注册表不一致,导致初始化状态无法正确进入 READY。 + +## 4. 修复过程 +- 修正初始化解析的指针推进逻辑 +- 按实际模块数量修正缓冲区容量 +- 补全设备注册表中的模块项 +- 同步梳理状态门禁,让控制命令只在合适状态下放行 + +## 5. 当前仍然有效的结论 +- 变长协议解析必须严格推进指针 +- 缓冲区大小必须和实际模块数一致 +- 注册表必须和上位机期望模块保持一致 +- 状态机卡在 `AUTHED` 时,优先检查初始化链路和模块匹配 + +## 6. 与当前代码对照 +### 6.1 当前代码已吸收的部分 +- 初始化解析修复 +- 模块注册完整性修复 +- 缓冲区容量修正 +- 状态门禁逻辑 + +### 6.2 仍需保留的历史描述 +- 为什么状态会卡在 `AUTHED` +- 为什么模块缺失会导致初始化失败 +- 为什么缓冲区尺寸不一致会埋下内存风险 + +## 7. 当前代码位置 +- `Application/Src/LDI/app_ldi.c` +- `Application/Src/LDI/app_ldi_cmd.c` +- `Application/Src/LDI/app_ldi_device.c` +- `Application/Inc/LDI/app_ldi.h` + +## 8. 验证状态 +- 编译验证:已完成历史修复验证 +- 运行验证:状态机可进入 READY +- 当前结论:适合长期作为排障归档参考 + +## 9. 后续建议 +- 后续若再遇到协议命令“发了不进处理函数”,先查状态门禁、模块注册和变长解析 diff --git "a/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\344\272\244\350\267\257\347\275\221\345\207\275\343\200\2242024\343\200\22586\345\217\267 \351\231\204\344\273\2662.docx" "b/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\344\272\244\350\267\257\347\275\221\345\207\275\343\200\2242024\343\200\22586\345\217\267 \351\231\204\344\273\2662.docx" new file mode 100644 index 0000000..9e141fa Binary files /dev/null and "b/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\344\272\244\350\267\257\347\275\221\345\207\275\343\200\2242024\343\200\22586\345\217\267 \351\231\204\344\273\2662.docx" differ diff --git "a/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\344\272\244\350\267\257\347\275\221\345\207\275\343\200\2242024\343\200\22586\345\217\267 \351\231\204\344\273\2662.txt" "b/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\344\272\244\350\267\257\347\275\221\345\207\275\343\200\2242024\343\200\22586\345\217\267 \351\231\204\344\273\2662.txt" new file mode 100644 index 0000000..2338a37 --- /dev/null +++ "b/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\344\272\244\350\267\257\347\275\221\345\207\275\343\200\2242024\343\200\22586\345\217\267 \351\231\204\344\273\2662.txt" @@ -0,0 +1,1272 @@ + +高速公路联网收费系统优化升级 +收费站标准化专项试点 +技术方案 + + + + + +附件 3 车道设备接口规范 + +交通运输部路网监测与应急处置中心 +2024 年 4 月 +目录 +1 基本构成 4 +2 外设模块说明 4 +2.1 外设功能模块类型编码定义 4 +2.2 协议使用约定 4 +3 通讯协议说明 5 +3.1 参数配置 5 +3.2 数据传输方式 5 +3.3 通信方式说明 5 +3.4 通讯数据帧格式 7 +4 TCP 协议设备配置接口 7 +4.1 设备 IP 信息设置 7 +4.2 设备参数配置 8 +4.3 设备重启 10 +4.4 设备 IP 信息采集 12 +4.5 设备参数采集 13 +5 TCP 协议设备通用接口 15 +5.1 设备状态监控(车道->站级) 15 +5.2 设备验证(车道-站级) 17 +5.3 设备升级包下载(车道-站级) 19 +5.4 设备初始化(站级->车道) 19 +5.5 时钟同步 21 +6 TCP 协议设备功能接口 21 +6.1 控制查询数据帧 21 +6.2 上报数据帧 22 +10 电动栏杆机接口 23 +10.1 栏杆控制(01) 23 +10.2 抬落杆到位信号(02) 23 +11 车辆检测器接口 24 +11.1 信息上报(01) 24 +11.2 车检除雾除水(02) 24 +12 信息显示屏接口 25 +12.1 显示控制(01) 25 +12.2 清屏(02) 25 +13 通行信号灯接口 26 +13.1 显示控制(01) 26 +14 报警器接口 26 +14.1 报警控制(01) 26 +15 LED 情报板(含引导屏)接口 27 +15.1 显示控制(01) 27 +15.2 清屏(02) 28 +16 雨棚信号灯接口 28 +16.1 显示控制(01) 28 +17 雾灯接口 28 +17.1 显示控制(01) 28 +25 语音播报设备接口 29 +25.1 播放提示音(01) 29 +26创迪自制协议接口(TCP/UDP通信) 30 + + +1 基本构成 +本文档规定了车道设备与站级交易系统的通信接口协议,适用于高速公路收 +费站标准化试点。 +2 外设模块说明 +2.1 外设功能模块类型编码定义 +外设功能模块 编码定义 +RSU 路侧单元 E1H +高清车牌图像识别设备 E2H +车型识别设备 E3H +电动栏杆机 E4H +车辆检测器 E5H +信息显示屏 E6H +通行信号灯 E7H +报警器 E8H +LED 情报板 E9H +雨棚信号灯 EAH +雾灯 EBH +称重检测设备 ECH +自助发卡设备 EDH +自助交费设备 EEH +车道摄像机 EFH +非接触 IC 卡读写器 F0H +票据打印机 F1H +支付受理终端 F2H +扫码终端 F3H +语音播报设备 F4H +预留 F5H-FFH +2.2 协议使用约定 +1. 外设功能模块类型编码和外设功能模块序号,可唯一标识一个车道外设 +的功能模块。例如一个车道多个车辆检测器,功能模块类型编码均为 E5H,可以 + 通过序号 01H、02H、03H....进行区分。 +2. 一个外设可只包含单个功能模块,如 RSU 路侧单元(功能模块类型编码 +为 E1H);也可包含多个功能模块,如多功能栏杆一体机具备电动栏杆机(E4H)、 +信息显示屏(E6H)、通行信号灯(E7H)和报警器(E8H)等功能模块。 +3. 单外设只作为一个 IP 设备端,其设备接口按一个功能模块进行交互。一 +体化外设可作为一个 IP 设备端,也可作为多个 IP 设备端,支持对多个设备模块 +信息进行组合式控制、查询和上报。 +3 通讯协议说明 +3.1 参数配置 +接口形式:标准 RJ45 以太网接口。 +通讯速率:100M/1000M 自适应。 +通信协议:TCP(Transmission Control Protocol 传输控制协议)。 +设备出厂默认 IP:192.168.1.100,配置功能端口 9528。 +3.2 数据传输方式 +服务端与客户端之间通过消息报文方式交换信息。 +除特别说明外,消息的接收方必须在应答超时时间内通过消息向发送方确认 +消息的处理结果。若在应答超时时间内消息的发送方未收到接收方针对所发送消 +息的应答,则必须重新发送未应答的消息。 +3.3 通信方式说明 +3.3.1 出厂配置 +在出厂模式下,车道设备作为服务端,配置软件作为客户端,适用设备配置 +接口。 + +车道设备上电启动后,车道设备作为服务端,配置软件作为客户端;建立 +TCP 连接后,车道设备服务端与客户端开始通讯: +1. 客户端主动向服务端发送设备 IP 信息配置指令,服务端应答设置结果。 +2. 客户端向服务端发送设备参数配置指令,服务端应答设置结果。 +3. 客户端向服务端发送设备参数采集指令,以确认设置的参数。 +4. 上述参数设置成功后,客户端发送设备重启指令对服务端进行重启操作, +客户端应答重启指令接收结果;若接收成功则自动进行设备重启,并切换到工作 +模式。 +5. 客户端向服务端发送信息帧,服务端应返回应答指令,否则客户端将重 +复发送该指令,直到达到最大重传次数:最大重传次数及重传时间间隔可动态配 +置。 +3.3.2 工作模式 +在工作模式下,车道设备作为客户端,站级交易系统作为服务端,适用设备 +配置接口、设备通用接口和设备功能接口。 + +服务端启动后监听通信端口,等待设备客户端连接;建立 TCP 连接后,服 +务端与客户端开始通讯: +1. 客户端主动向服务端发送设备验证指令进行验证,服务端应答验证结果。 +2. 验证成功后,服务端发送初始化指令对客户端进行初始化,客户端应答 +初始化结果。 +3. 客户端向服务端发送信息帧,服务端应返回应答指令,否则客户端将重 +复发送该指令,直到达到最大重传次数:最大重传次数及重传时间间隔可动态配 +置。 +4. 客户端与服务端建立连接后,应定时向服务端发送监控报文,告知设备 +状态。 + +3.4 通讯数据帧格式 +服务端和客户端通讯的数据帧格式如下: +STX VER SEQ LEN DATA CRC +数据帧中各数据域的说明如下表所示。如无特别注明,数据均采用无符号整 +型编码;以字节表示的数据,均为高字节在前、低位字节在后的大端模式;对于 +未定义或者保留的参数默认值填 00H。 +字段 描述 +STX 帧开始标志,2 字节,取值为 FFFFH +VER 协议版本号,1 字节,当前版本为 00H +SEQ (1) 帧序列号,1 字节; +(2) 服务端主动发送帧的帧序号为 0XH,客户端响应帧的帧序号为 0XH,两帧的帧序号保持 +一致,其中 X 为 1,2,3,4,5,6,7,8,9; +(3) 客户端主动发送帧的帧序号为 X0H,服务端响应帧的帧序号为 X0H,两帧的帧序号保持 +一致,其中 X 为 1,2,3,4,5,6,7,8,9; +(4) 每次服务端、客户端的主动发送帧的序号自动加 1;包含多次交互的(如:RSU)通信过程, +每一次交互帧序号自动加 1。 +LEN DATA 域的长度,4 字节(高 2 字节保留,低两字节为 DATA 域长度) +DATA 帧数据内容,默认十六进制编码 +CRC 从 VER 到 DATA 所有字节的 CRC16 校验值,2 字节,初始值为 FFFFH +4 TCP 协议设备配置接口 +TCP 协议设备配置接口包含设备 IP 信息设置、设备参数配置、设备重启、 +设备 IP 信息采集、设备参数采集。 +4.1 设备 IP 信息设置 +4.1.1 参数设置(0AH) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 0AH。 +1 4 UnixTime Unix 时间戳(指格林威治时间 1970 年 01 月 01 日 00 时00 +分 00 秒起至现在的总秒数) + 5 LaneHex 车道 HEX 编号,参见《收费公路联网收费技术要求》4.3, +十六进制编码 +2 字节入/出口收费网络号(BCD 编码),例如北京:1101H + 2 字节入/出口收费站点编号,例如公路序号 10D,收费站编 +号 23D,此时编码为 0A17H + 1 字节入/出口收费车道编号,例如车道号 21D,此时编码为 +15H +10 8 CertificationInfo 设备验证信息,由平台生成的唯一设备验证码 +18 2 Reserved 保留字节,默认 00H +20 4 ServerIP1 服务端 IP1(外设控制服务) +24 2 ServerPort1 服务端端口 1(外设控制服务) +26 4 ServerIP2 服务端 IP2(预留) +30 2 ServerPort2 服务端端口 2(预留) +32 4 ServerIP3 服务端 IP3(预留) +36 2 ServerPort3 服务端端口 3(预留) +38 4 MQTTIP MQTT 服务 IP(预留) +42 2 MQTTPort MQTT 服务端口(预留) +44 10 MQTTUser MQTT 服务用户名(预留) +54 10 MQTTPwd MQTT 服务密码(预留) +64 1 MQTTQOS MQTT QOS 设置(预留) +65 4 NtpIP 对时服务器 IP +69 4 DeviceIP 设备 IP +73 2 DevicePort 设备服务端端口号 +75 4 Gateway 设备网关 +79 4 Netmask 设备子网掩码 +83 2 Reserved 保留字节,默认 00H +列如:FF FF 00 10 00 00 00 55 0A 69 04 54 95 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 C0 A8 00 64 27 2C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C0 A8 01 64 25 38 C0 A8 01 01 FF FF FF 00 00 00 1C 48 +4.1.2 参数设置结果(A0H) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 A0H。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserved 保留字节,默认 00H +20 1 ErrorCode 状态码,00H-设置成功,其他-设置失败 +列如:FF FF 00 10 00 00 00 15 A0 69 04 54 95 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 00 0D 7B +4.2 设备参数配置 +4.2.1 参数设置(0BH) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 0BH。 +1 4 UnixTime Unix 时间戳(指格林威治时间 1970 年 01 月 01 日 00 时00 +分 00 秒起至现在的总秒数) + 5 LaneHex 车道 HEX 编号,参见《收费公路联网收费技术要求》4.3, +十六进制编码 +2 字节入/出口收费网络号(BCD 编码),例如北京:1101H +2 字节入/出口收费站点编号,例如公路序号 10D,收费站编 +号 23D,此时编码为 0A17H +1 字节入/出口收费车道编号,例如车道号 21D,此时编码为 +15H +10 8 CertificationInfo 设备验证信息,由平台生成的唯一设备验证码 +18 2 Reserved 保留字节,默认 00H +20 1 DeviceNum 功能模块数量 +电动栏杆机(有此功能模块,需进行参数设置,才包含此模块设置) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 1 FiterTime 杂波信号过滤时间阈值,单位毫秒 +3 10 厂商自定义 此字节段厂商可自定义使用 +车辆检测器(有此功能模块,需进行参数设置,才包含此模块设置) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 1 FiterTime 杂波信号过滤时间阈值,单位毫秒 +3 10 厂商自定义 此字节段厂商可自定义使用 +信息显示屏(有此功能模块,需进行参数设置,才包含此模块设置) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 1 FontLine 设置显示行数:01H~06H,00H-自适应 +3 10 厂商自定义 此字节段厂商可自定义使用 +通行信号灯(有此功能模块,需进行参数设置,才包含此模块设置内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 2 厂商自定义 此字节段厂商可自定义使用 +报警器(有此功能模块,需进行参数设置,才包含此模块设置内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 2 厂商自定义 此字节段厂商可自定义使用 +LED 情报板(有此功能模块,需进行参数设置,才包含此模块设置内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 10 厂商自定义 此字节段厂商可自定义使用 +雨棚信号灯(有此功能模块,需进行参数设置,才包含此模块设置内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 2 厂商自定义 此字节段厂商可自定义使用 +雾灯(有此功能模块,需进行参数设置,才包含此模块设置内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 2 厂商自定义 此字节段厂商可自定义使用 +RSU 路侧单元(有此功能模块,需进行参数设置,才包含此模块设置内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 1 CPC 是否开启 CPC 功能:00H-关闭 01H-开启 +3 10 厂商自定义 此字节段厂商可自定义使用 +高清车牌图像识别设备、车型识别设备(有此功能模块,需进行参数设置,才包含此模块设置内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 1 Flag 是否启用图片、视频上传: +00H-均不启用 +03H-视频不启用,图片 Http 上传 +05H-视频不启用,图片 Ftp 上传 +30H-图片不启用,视频 Http 上传 +50H-图片不启用,视频 Ftp 上传 +33H-图片、视频 Http 上传 +35H-视频 Http 上传、图片 Ftp 上传 +53H-视频 Ftp 上传、图片 Http 上传 +55H-图片、视频 Ftp 上传 +3 2 SettingLen 地址长度 +5 N Setting 1.Http 上传地址、端口号,如 http://ip:port/pic/picImage; +2.Ftp 上传目录、用户名、密码,以“|”分隔,如 +ftp://192.168.1.1/image|root|123,不含中文。高清车牌图像识 +别设备、车型识别设备放在不同的存储目录 +5+N 1 RtspSettingLen RTSP 地址长度 +6+N M RtspSetting RTSP 地址 +16+N ++M 10 厂商自定义 此字节段厂商可自定义使用 +…(其他功能模块,若支持参数配置可按如下结构扩充) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 10 厂商自定义 此字节段厂商可自定义使用 +注:上述未对设备可能涵盖的全部功能模块配置参数样例枚举,对于含有对应功能模块及支持参数配置的 +设备可按此结构设计其他模块参数配置结构 +配置 信息显示屏、通行信号灯、报警器、语音播报,列如: +FF FF 00 20 00 00 00 36 0B 69 04 56 1C 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 04 E6 01 04 00 00 00 00 00 00 00 00 00 00 E7 01 00 00 E8 01 00 00 F4 01 00 00 00 00 00 00 00 00 00 00 83 CF +配置 LED 情报板,列如: +FF FF 00 30 00 00 00 21 0B 69 04 57 63 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 E9 01 00 00 00 00 00 00 00 00 00 00 F8 77 +配置 雨棚信号灯,列如: +FF FF 00 40 00 00 00 19 0B 69 04 57 82 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 EA 01 00 00 20 14 +配置 电动栏杆机,列如: +FF FF 00 50 00 00 00 22 0B 69 04 57 AD 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 E4 01 00 00 00 00 00 00 00 00 00 00 00 A8 12 + +4.2.2 参数设置结果(B0H) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 B0H。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserved 保留字节,默认 00H +20 1 ErrorCode 状态码,00H-设置成功,其他-设置失败 +注:设备参数设置成功后,参数自动生效,无须重启设备 +列如:FF FF 00 20 00 00 00 15 B0 69 04 56 1C 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 00 9B 6D +4.3 设备重启 +4.3.1 设备重启(0DH) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 0DH。 +1 4 UnixTime Unix 时间戳(指格林威治时间 1970 年 01 月 01 日 00 00 +分 00 秒起至现在的总秒数) +5 5 LaneHex 车道 HEX 编号,参见《收费公路联网收费技术要求》表4.3, +十六进制编码 +2 字节入/出口收费网络号(BCD 编码),例如北京:1101H + 2 字节入/出口收费站点编号,例如公路序号 10D,收费站编 +号 23D,此时编码为 0A17H +1 字节入/出口收费车道编号,例如车道号 21D,此时编码为 +15H +10 8 CertificationInfo 设备验证信息,由平台生成的唯一设备验证码 +18 2 Reserved 保留字节,默认 00H +20 1 DeviceNum 功能模块数量 +电动栏杆机(有此功能模块,且需要重启,才包含此模块重启内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 10 厂商自定义 此字节段厂商可自定义使用 +车辆检测器(有此功能模块,且需要重启,才包含此模块重启内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 10 厂商自定义 此字节段厂商可自定义使用 +信息显示屏(有此功能模块,且需要重启,才包含此模块重启内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 10 厂商自定义 此字节段厂商可自定义使用 +通行信号灯(有此功能模块,且需要重启,才包含此模块重启内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 2 厂商自定义 此字节段厂商可自定义使用 +报警器(有此功能模块,且需要重启,才包含此模块重启内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 2 厂商自定义 此字节段厂商可自定义使用 +LED 情报板(有此功能模块,且需要重启,才包含此模块重启内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 10 厂商自定义 此字节段厂商可自定义使用 +雨棚信号灯(有此功能模块,且需要重启,才包含此模块重启内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 2 厂商自定义 此字节段厂商可自定义使用 +雾灯(有此功能模块,且需要重启,才包含此模块重启内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 2 厂商自定义 此字节段厂商可自定义使用 +RSU 路侧单元(有此功能模块,且需要重启,才包含此模块重启内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 10 厂商自定义 此字节段厂商可自定义使用 +高清车牌图像识别设备、车型识别设备(有此功能模块,且需要重启,才包含此模块重启内容) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 10 厂商自定义 此字节段厂商可自定义使用 +…(其他功能模块,若支持重启可按如下结构扩充) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 10 厂商自定义 此字节段厂商可自定义使用 +注:上述未对设备可能涵盖的全部功能模块配置参数样例枚举,对于含有对应功能模块且支持重启的功能, +可按此结构设计设备重启结构 +重启 信息显示屏、通行信号灯、报警器、语音播报,列如: +FF FF 00 40 00 00 00 35 0D 69 04 5A 76 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 04 E6 01 00 00 00 00 00 00 00 00 00 00 E7 01 00 00 E8 01 00 00 F4 01 00 00 00 00 00 00 00 00 00 00 B3 10 +重启 LED 情报板,列如: +FF FF 00 50 00 00 00 21 0D 69 04 5A 85 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 E9 01 00 00 00 00 00 00 00 00 00 00 CC 8F +重启 雨棚信号灯,列如: +FF FF 00 60 00 00 00 19 0D 69 04 5A 9A 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 EA 01 00 00 A4 1E +重启 电动栏杆机,列如: +FF FF 00 70 00 00 00 21 0D 69 04 5A A7 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 E4 01 00 00 00 00 00 00 00 00 00 00 93 B9 + +4.3.2 设备重启接收结果(D0H) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 D0H。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserved 保留字节,默认 00H +20 1 ErrorCode 状态码,00H-指令接收成功,其他-指令接收失败 +列如:FF FF 00 05 00 00 00 15 D0 69 04 5A 85 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 00 AD 82 +4.4 设备 IP 信息采集 +4.4.1 参数采集(1DH) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 1DH。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserved 保留字节,默认 00H +列如:FF FF 00 80 00 00 00 14 1D 69 04 5B 57 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 C4 C6 + +4.4.2 参数采集结果(D1H) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 D1H。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserve 保留字节,默认 00H +20 1 ErrorCode 状态码,00H-成功,其他-失败 +21 4 ServerIP1 服务端 IP1(外场控制服务) +25 2 ServerPort1 服务端端口 1(外场控制服务) +27 4 ServerIP2 服务端 IP2(预留) +31 2 ServerPort2 服务端端口 2(预留) +33 4 ServerIP3 服务端 IP3(预留) +37 2 ServerPort3 服务端端口 3(预留) +39 4 MQTTIP MQTT 服务 IP(预留) +43 2 MQTTPort MQTT 服务端口(预留) +45 10 MQTTUser MQTT 服务用户名(预留) +55 10 MQTTPwd MQTT 服务密码(预留) +65 1 MQTTQOS MQTT QOS 设置(预留) +66 4 NtpIP 对时服务器 IP +70 4 DeviceIP 设备 IP +74 2 DevicePort 设备服务端端口号 +76 4 Gateway 设备网关 +80 4 Netmask 设备子网掩码 +84 2 Reserved 保留字节,默认 00H +列如:FF FF 00 80 00 00 00 56 D1 69 04 5B 57 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 00 C0 A8 00 64 27 2C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C0 A8 01 64 25 38 C0 A8 01 01 FF FF FF 00 00 00 1E 9A +4.5 设备参数采集 +支持对设备所配置的运行参数进行采集,此时设备作为服务端。 +4.5.1 参数采集(1EH) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 1EH。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserved 保留字节,默认 00H +列如:FF FF 00 90 00 00 00 14 1E 69 04 5F 04 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 07 0A +4.5.2 参数采集结果(E1H) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 E1H。 +1 4 UnixTime Unix 时间戳(指格林威治时间 1970 年 01 月 01 日 00 时00 +分 00 秒起至现在的总秒数) +5 5 LaneHex 车道 HEX 编号,参见《收费公路联网收费技术要求》4.3, +十六进制编码 +2 字节入/出口收费网络号(BCD 编码),例如北京:1101H +2 字节入/出口收费站点编号,例如公路序号 10D,收费站编 +号 23D,此时编码为 0A17H +1 字节入/出口收费车道编号,例如车道号 21D,此时编码为 +15H +10 8 CertificationInfo 设备验证信息,由平台生成的唯一设备验证码 +18 2 Reserved 保留字节,默认 00H,其他-失败 +20 1 DeviceNum 功能模块数量 +电动栏杆机(有此功能模块及参数配置支持,才有以下设置) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 1 FiterTime 杂波信号过滤时间阈值,单位毫秒 +3 10 厂商自定义 此字节段厂商可自定义使用 +车辆检测器(有此功能模块及参数配置支持,才有以下设置) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 1 FiterTime 杂波信号过滤时间阈值,单位毫秒 +3 10 厂商自定义 此字节段厂商可自定义使用 +信息显示屏(有此功能模块及参数配置支持,才有以下设置) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 1 FontLine 设置显示行数:01H~06H,00H-自适应 +3 10 厂商自定义 此字节段厂商可自定义使用 +通行信号灯(有此功能模块及参数配置支持,才有以下设置) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 2 厂商自定义 此字节段厂商可自定义使用 +报警器(有此功能模块及参数配置支持,才有以下设置) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 2 厂商自定义 此字节段厂商可自定义使用 +LED 情报板(有此功能模块及参数配置支持,才有以下设置) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 10 厂商自定义 此字节段厂商可自定义使用 +雨棚信号灯(有此功能模块及参数配置支持,才有以下设置) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 2 厂商自定义 此字节段厂商可自定义使用 +雾灯(有此功能模块及参数配置支持,才有以下设置) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +3 2 厂商自定义 此字节段厂商可自定义使用 +RSU 路侧单元(有此功能模块及参数配置支持,才有以下设置) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 1 CPC 是否开启 CPC 功能:00H-关闭 01H-开启 +3 10 厂商自定义 此字节段厂商可自定义使用 +高清车牌图像识别设备(有此功能模块及参数配置支持,才有以下设置) + + +1 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 1 Flag 是否启用图片上传: +00H-不启用 +01H-Http 上传 +02H-Ftp 上传 +3 2 SettingLen 地址长度 +5 N Setting 1.Http 上传地址、端口号,如 http://ip:port/pic/picImage; +2.Ftp 上传目录、用户名、密码,以“|”分隔,如 +ftp://192.168.1.1/image|root|123,不含中文。高清车牌图像识 +别设备、车型识别设备放在不同的存储目录 +5+N 1 RtspSettingLen RTSP 地址长度 +6+N M RtspSetting RTSP 地址 +16+N ++M 10 厂商自定义 此字节段厂商可自定义使用 +…(其他功能模块,若支持参数配置可按如下结构扩充) +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +2 10 厂商自定义 此字节段厂商可自定义使用 +注:上述未对全部模块的配置参数样例进行枚举,对于含有对应功能模块及支持参数配置的设备可按此结 +构设计 +列如:FF FF 00 90 00 00 00 5B E1 69 04 5F 04 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 07 E4 01 00 00 00 00 00 00 00 00 00 00 00 E6 01 01 00 00 00 00 00 00 00 00 00 00 E7 01 00 00 E8 01 00 00 E9 01 00 00 00 00 00 00 00 00 00 00 EA 01 00 00 00 00 00 00 00 00 00 00 F4 01 00 00 00 00 00 00 00 00 00 00 01 B7 +5 TCP 协议设备通用接口 +TCP 协议设备通用接口包含设备状态监测、设备验证、设备升级包下载、时 +钟同步、设备初始化功能,车道设备均作为客户端。 +5.1 设备状态监控(车道->站级) +5.1.1 监控上报(0CH) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 0CH。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserved 保留字节,默认 00H +20 4 RunningTime 设备从开机到现在的运行时间 单位秒 +24 1 RunningStatus 设备整体运行状态,正常-00H,异常-01H +25 1 ConnectStatus 与服务端连接状态,正常-00H,异常-01H +26 1 DeviceNum 设备功能模块数量 N +模块监控信息 +27 (17+S) +*N DeviceInfo 1 字节功能模块类型编码 +1 字节功能模块序号 +1 字节功能模块可用状态,正常-00H,异常-01H +1 字节功能模块异常编码,00H-FFH 见附录 A.2 +1 字节功能模块当前运行状态,分设备详细定义,见附录 +A.1 +2 字节厂商代码 +2 字节型号代码 +4 字节日期 BCD 码如:20200419 +1 字节版本序列号 +1 字节内存使用率,例如 60%-3CH +1 字节设备温度,例如 25℃-19H +1 字节个性化监控信息字节长度 S,若无个性化监控信息, +则字节长度填写 00H +S 字节个性化监控信息内容 +智能自助发卡/交费个性化监控信息 +0 1 Mid 机位序列 +上工位序列为 01H,下工位序列为 02H,状态类型不限工 +位时为 00H。 +1 1 Mno 通道序列 +机芯通道号,范围 01H-04H +状态类型不限于机芯通道时为 00H。 + + +2 2 StatusType 状态类型 +详见附录 B +4 M DataType 数值数据 +详见附录 B +高清车牌图像识别设备个性化监控信息 +0 2 OverstockImageJourCount 积压车牌识别流水数 +2 2 OverstockImageCount 积压图片数 +4 1 LightWorkStatus 补光灯的工作状态,正常-00H,异常-01H +车型识别设备个性化监控信息 +0 2 OverstockImageJourCount 积压车型识别流水数 +2 2 OverstockImageCount 积压图片数 +4 2 OverstockVideoCount 积压视频数 +6 1 LightWorkStatus 补光灯的工作状态,正常-00H,异常-01H +路侧单元个性化监控信息 +0 1 PSAMCnt PSAM 卡数量 n,使用在线密钥时填写 00H +1 19*n PSAMStatus 1 字节 PSAM 卡槽号 +1 字节 PSAM 运行状态,正常-00H,异常-01H +1 字节 PSAM 授权状态,已授权-00H,未授权-01H +6 字节 PSAM 终端机编号 +10 字节 PSAM 卡序列号 +1+19*n 1 AntennaNum RSU 天线数量 m +1+19*n+1 10*m AntennaStatus 1 字节天线编号 +1 字节发射状态,正常-00H,异常-01H +1 字节接收状态,正常-00H,异常-01H +1 字节天线信道号 +1 字节功率等级 +1 字节天线温度 +2 字节天线软件版本 +2 字节天线固件版本 +IC 卡读写器个性化监控信息 +0 1 Status 读写器状态:00H-正常,其它-异常 +1 10 Version 读写器版本号 +11 1 PSAMCnt PSAM 卡数量 n,使用在线密钥时填写 00H +12 19*n PSAMStatus 1 字节 PSAM 卡槽号 +1 字节 PSAM 运行状态,正常-00H,异常-01H +1 字节 PSAM 授权状态,已授权-00H,未授权-01H +6 字节 PSAM 终端机编号 +10 字节 PSAM 卡序列号 +注:发送间隔 5 秒。 +列如:FF FF 00 90 00 00 00 99 0C 69 04 5F 04 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 00 00 00 00 00 00 07 E4 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 E6 01 00 00 01 00 00 00 00 00 00 00 00 00 00 00 01 00 E7 01 00 00 01 00 00 00 00 00 00 00 00 00 00 00 01 00 E8 01 00 00 01 00 00 00 00 00 00 00 00 00 00 00 01 00 E9 01 00 00 01 00 00 00 00 00 00 00 00 00 00 00 01 00 EA 01 00 00 01 00 00 00 00 00 00 00 00 00 00 00 01 00 F4 01 00 00 01 00 00 00 00 00 00 00 00 00 00 00 01 00 2E 7D +5.1.2 监控上报返回(C0H) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 C0H。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserved 保留字节,默认 00H +20 1 DeviceNum 设备功能模块数量 N +模块监控信息 + 14*N DeviceInfo 1 字节功能模块类型编码 +1 字节功能模块序号 +10 字节当前软件最新版本号(设备根据此字段判断是否进 +行软件版本升级) +注释:10 字节软件版本号组成字段(1 字节功能模块类型、 +2 字节厂家代码、2 字节型号代码、4 字节日期 BCD 码(如: +20200419)、1 字节版本序列号) +4 字节软件升级时间,UNIX 时间(从格林威治时间 +1970-01-01 00:00:00 开始) +列如:FF FF 00 90 00 00 00 85 C0 69 04 69 7A 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 07 E4 01 E4 00 00 00 00 20 25 10 31 00 69 04 69 7A E6 01 E6 00 00 00 00 20 25 10 31 00 69 04 69 7A E7 01 E7 00 00 00 00 20 25 10 31 00 69 04 69 7A E8 01 E8 00 00 00 00 20 25 10 31 00 69 04 69 7A E9 01 E9 00 00 00 00 20 25 10 31 00 69 04 69 7A EA 01 EA 00 00 00 00 20 25 10 31 00 69 04 69 7A F4 01 F4 00 00 00 00 20 25 10 31 00 69 04 69 7A 0B 5B +5.2 设备验证(车道-站级) +设备上电后,连接服务端,并发送设备验证请求,服务端返回认证结果,认 +证通过后,设备可正常工作。 +5.2.1 设备验证申请帧(0EH) +位置 字节数 数据元 数据内容 +0 1 CmdType 指令代码,此处取值 0EH。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserved 保留字节,默认 00H +20 1 DeviceNum 设备功能模块数量 N +功能模块信息 +0 1 DeviceType 功能模块类型编码 +1 1 DeviceIndex 功能模块序号 +注: +服务端启动后监听端口,等待客户端连接。 +客户端连接成功后发送 0E 帧申请设备验证,服务端应答 0E 帧并携带设备验证结果。 +客户端定时 3 秒钟发送一次 0E 帧申请设备验证,直至验证成功。 +列如:FF FF 00 90 00 00 00 23 0E 69 04 5F 04 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 07 E4 01 E6 01 E7 01 E8 01 E9 01 EA 01 F4 01 B9 8B +5.2.2 设备验证结果帧(E0H) +位置 字节数 数据元 数据内容 +0 1 CmdType 指令代码,此处取值 E0H。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserved 保留字节,默认 00H +20 1 ErrorCode 设备验证结果,00H 认证成功,其余-失败 +列如:FF FF 00 90 00 00 00 15 E0 69 04 6B D2 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 00 7D 62 +5.3 设备升级包下载(车道-站级) +5.3.1 请求(0FH) +位置 字节数 数据元 数据内容 +0 1 CmdType 指令代码,此处取值 0FH。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserved 保留字节,默认 00H +20 1 DeviceType 功能模块类型编码 +21 1 DeviceIndex 功能模块序号 +22 10 UpdateVersion 下载升级包版本号 +注释:10 字节软件版本号组成字段(1 字节功能模块类型、2 +字节厂家代码、2 字节型号代码、4 字节日期 BCD 码(如: +20200419)、1 字节版本序列号) +5.3.2 响应(F0H) +位置 字节数 数据元 数据内容 +0 1 CmdType 指令代码,此处取值 F0H。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserved 保留字节,默认 00H +20 1 DeviceType 功能模块类型编码 +21 1 DeviceIndex 功能模块序号 +22 10 UpdateMD5 更新 MD5 用于验证更新压缩包的合法性 +32 4 UpdateFilesize 下载升级包文件流字节大小 N +35 N UpdateFile 下载升级包文件流 +5.4 设备初始化(站级->车道) +5.4.1 请求(1AH) +位置 字节数 数据元 数据内容 +0 1 CmdType 指令代码,此处取值 1AH。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserved 保留字节,默认 00H +20 1 DeviceNum 功能模块数量 N(支持对集成设备合并初始化) +21 (5+S)*N DeviceInfo 1 字节功能模块类型编码 +1 字节功能模块序号 +2 字节接口协议版本号,默认填写 00H +1 字节功能模块个性化初始化内容字节长度 S +S 字节功能模块个性化初始化内容 +路侧单元个性化初始化内容 S=11 +0 1 LaneMode 车道工作模式: +位置 字节数 数据元 数据内容 + 01H-封闭式 ETC/MTC 入口,02H-封闭式 ETC/MTC 出 +口,03H-封闭式 ETC 入口,04H-封闭式 ETC 出口,05H- +混合车道开放式,06H-ETC 开放式,07H-入口预交易 +门架,08H-出口预交易门,09H~0FH-自定义,10H-自 +助发卡,11H-自助交费,12H~FFH-保留给未来使用 +1 1 BSTInterval 路侧单元自动发送 BST 的间隔,单位 ms +2 1 TxPower 路侧单元功率级数 +3 1 PLLChannelID 信道号 +4 1 WorkMode 00H-透传模式,01H-集成模式 +5 1 AuthMode 00H-离线密钥,01H-在线密钥 +6 1 Ef04Opt 是否读取 EF04 文件信息:00H-不读取,01H-读取 +7 2 Ef04OffSet EF04 文件读操作偏移量 +9 2 Ef04Len EF04 文件读操作长度 +注: +服务端认证客户端设备后,发送 1A 指令初始化客户端设备。 +客户端设备收到服务端发送的 1A 指令后,若初始化成功,则向服务端发送 A1 帧作为应答。 +列如: +初始化 信息显示屏、通行信号灯、报警器、语音播报,列如: +FF FF 00 05 00 00 00 29 1A 69 04 6D 64 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 04 E6 01 00 00 00 E7 01 00 00 00 E8 01 00 00 00 F4 01 00 00 00 27 4A +初始化 LED 情报板,列如: +FF FF 00 06 00 00 00 1A 1A 69 04 6D 7F 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 E9 01 00 00 00 43 2D +初始化 雨棚信号灯,列如: +FF FF 00 07 00 00 00 1A 1A 69 04 6D 8B 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 EA 01 00 00 00 7F 71 +初始化 电动栏杆机,列如: +FF FF 00 08 00 00 00 1A 1A 69 04 6D 99 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 E4 01 00 00 00 D3 70 +5.4.2 响应(A1H) +位置 字节数 数据元 数据内容 +0 1 CmdType 指令代码,此处取值 A1H。 +1 4 UnixTime Unix 时间戳 +5 5 LaneHex 车道编号 +10 8 CertificationInfo 设备验证信息 +18 2 Reserved 保留字节,默认 00H +20 1 DeviceNum 功能模块数量 N(支持对集成设备合并初始化) +21 (10+S) +*N DeviceInfo 1 字节功能模块类型编码, +1 字节功能模块序号 +1 字节功能模块状态参数,正常-00H,异常-01H +2 字节软件版本号 +2 字节固件版本号 +2 字节接口协议版本号,默认填写 00H +1 字节功能模块个性化初始化内容字节长度 S +S 字节功能模块个性化初始化内容 +路侧单元模块个性化初始化结果内容 +0 1 RSUStatus 路侧单元主状态参数; +00H-正常,其他-异常 +1 1 PSAMCnt PSAM 卡数量 n,使用在线密钥时填写 00H +2 9*n PSAMInfo 1 字节 PSAM 卡槽号,从 00H 开始 +1 字节 PSAM 卡版本号 +1 字节 PSAM 卡授权状态,00H-已授权,01H-未授权 +6 字节 PSAM 卡终端编号 +2+9n 1 AlgorithmID 算法标识,00H-3DES,04H-SM4 +3+9n 1 RSUManuID 路侧单元厂商代码 +4+9n 3 RSUID 路侧单元编号 +7+9n 2 RSUVersion 路侧单元软件版本号 +9+9n 1 WorkStatus 工作模式初始化结果,默认填写 00H +注:客户端设备应在规定时间内(不大于 1 秒)完成初始化工作并应答 A1 帧。 +列如:FF FF 00 90 00 00 00 62 A1 69 04 6B D2 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 07 E4 01 00 00 00 00 00 00 00 01 00 E6 01 00 00 00 00 00 00 00 01 00 E7 01 00 00 00 00 00 00 00 01 00 E8 01 00 00 00 00 00 00 00 01 00 E9 01 00 00 00 00 00 00 00 01 00 EA 01 00 00 00 00 00 00 00 01 00 F4 01 00 00 00 00 00 00 00 01 00 E6 23 +5.5 时钟同步 +采用 ntp 与北斗授时服务进行对时。 +6 TCP 协议设备功能接口 +TCP 协议设备功能接口协议帧分为控制查询数据帧和上报数据帧,车道设备 +均作为客户端。控制查询数据帧由服务端主动发起,对设备进行设备控制或者设 +备信息查询;上报数据帧由设备主动发起,向服务端主动上报设备运行信息。 +6.1 控制查询数据帧 +服务端可向设备发起控制查询数据帧,支持对设备内 N 个功能模块进行控 +制、查询,设备端返回 N 个功能模块的控制结果或查询结果。 +6.1.1 请求(1BH) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,控制查询帧取值 1BH +1 8 UnixTime Unix 时间戳,精确到毫秒,例如 2023-12-06 14:06:44:000 +转成时间戳为 0000018C3DBC5D20H +9 5 LaneHex 车道编号 +14 8 CertificationInfo 设备验证信息 +22 2 Reserved 保留字节,默认 00H +24 1 DeviceNum 功能模块数量 N +功能模块 1 控制查询请求帧长度,2 字节 +功能模块 1 控制查询请求帧,如向 RSU 路侧单元发出开关路侧单元指令,在此可参照“7.1.3 开关路侧 +单元指令”中的请求帧内容放入。 +功能模块 2 控制查询请求帧长度,2 字节 +功能模块 2 控制查询请求帧 +…… +功能模块 N 控制查询请求帧长度,2 字节 +功能模块 N 控制查询请求帧 +注:外设控制服务按 TCP 协议设备功能接口向设备分条发送控制指令,也可使用功能模块组合控制帧 +向设备发送组合帧。 +6.1.2 响应(B1H) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,控制帧取值 B1H +1 8 UnixTime Unix 时间戳,精确到毫秒,例如 2023-12-06 14:06:44:000 +转成时间戳为 0000018C3DBC5D20H +9 5 LaneHex 车道编号 +14 8 CertificationInfo 设备验证信息 +22 2 Reserved 保留字节,默认 00H +24 1 DeviceNum 功能模块数量 N +功能模块 1 控制响应帧、查询结果帧长度,2 字节 +功能模块 1 控制响应帧、查询结果帧 +功能模块 2 控制响应帧、查询结果帧长度,2 字节 +功能模块 2 控制响应帧、查询结果帧 +…… +功能模块 N 控制响应帧、查询结果帧长度,2 字节 +功能模块 N 控制响应帧、查询结果帧 +6.2 上报数据帧 +客户端主动向服务端上报含其 N 个功能模块信息的数据帧,服务端收到数 +据帧并处理完成后,向设备端返回上报确认帧,其包含 N 个功能模块信息的确 +认状态。 +6.2.1 上报(1CH) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,控制帧取值 1CH +1 8 UnixTime Unix 时间戳,精确到毫秒,例如 2023-12-06 14:06:44:000 转 +成时间戳为 0000018C3DBC5D20H +9 5 LaneHex 车道编号 +14 8 CertificationInfo 设备验证信息 +22 2 Reserved 保留字节,默认 00H +24 1 DeviceNum 功能模块数量 N +功能模块 1 上报数据帧长度,2 字节 +功能模块 1 上报数据帧 +功能模块 2 上报数据帧长度,2 字节 +功能模块 2 上报数据帧 +…… +功能模块 N 上报数据帧长度,2 字节 +功能模块 N 上报数据帧 +6.2.2 上报确认(C1H) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,控制帧取值 C1H +1 8 UnixTime Unix 时间戳,精确到毫秒,例如 2023-12-06 14:06:44:000 转 +成时间戳为 0000018C3DBC5D20H +9 5 LaneHex 车道编号 +14 8 CertificationInfo 设备验证信息 +22 2 Reserved 保留字节,默认 00H +24 1 DeviceNum 功能模块数量 N +服务端返回功能模块 1 上报确认帧长度,2 字节 +服务端返回功能模块 1 上报确认帧 +服务端返回功能模块 2 上报确认帧长度,2 字节 +服务端返回功能模块 2 上报确认帧 +…… +服务端返回功能模块 N 上报确认帧长度,2 字节 +服务端返回功能模块 N 上报确认帧 +10 电动栏杆机接口 +若为多功能栏杆机,由于还涵盖高清车牌图像识别设备、费显等组成模块, +对于这些模块的接口可参考对应章节接口内容要求。 +10.1 栏杆控制(01) +通过栏杆控制指令,完成电动栏杆机的落杆和抬杆功能,功能编号 01H。 +10.1.1 请求 +位置 字节数 参数 说明 +控制请求帧 +0 1 DeviceType 功能模块类型编码,取值 E4H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 WarmStaus 00H-落杆 01H-抬杆 02H-长抬 +落杆, +列如:FF FF 00 08 00 00 00 1F 1B 00 00 01 9A 38 D0 14 1C 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 E4 01 01 00 62 F0 +10.1.2 响应 +位置 字节数 参数 说明 +控制响应帧 +0 1 DeviceType 功能模块类型编码 E4H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 CtlStatus 功能模块功能控制结果,00H-控制成功,01H-控制失败 +4 8 UnixTime1 收到通知时间,Unix 时间戳,单位毫秒 +12 8 UnixTime2 控制完成时间,Unix 时间戳,单位毫秒 +注:栏杆机需在完成控制后返回数据。 +列如:FF FF 00 02 00 00 00 2F B1 00 00 01 9A 38 64 5D E3 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 14 E4 01 01 00 00 00 01 9A 38 64 5D E3 00 00 01 9A 38 64 5D E3 B8 49 + +10.2 抬落杆到位信号(02) +通过抬落杆到位信号指令,完成栏杆机抬落杆信号上报功能,功能编号 02H。 +10.2.1 上报 +位置 字节数 参数 说明 +上报数据帧 +0 1 DeviceType 功能模块类型编码,取值为 E4H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 02H +3 1 BarStatus 栏杆状态,00H-落杆到位,01H-抬杆到位 +注: 栏杆机在完成栏杆控制后,将栏杆机抬落杆的到位信号反馈给服务端 +栏杆状态:落杆到位 +列如:FF FF 00 08 00 00 00 1F 1C 00 00 01 9A 38 D0 14 1C 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 E4 01 02 00 03 36 + +10.2.2 上报确认 +位置 字节数 参数 说明 +上报确认帧 +0 1 DeviceType 功能模块类型编码,取值为 E4H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 02H +3 1 RecvStatus 服务端接收处理结果,00H-接收处理成功,01H-接收处理 +失败 +列如: FF FF 00 08 00 00 00 1F C1 00 00 01 9A 38 D0 14 1C 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 E4 01 02 00 BA EA +11 车辆检测器接口 +11.1 信息上报(01) +通过信息上报指令,完成车检状态上报功能,功能编号 01H。 +11.1.1 上报 +位置 字节数 数据元 数据内容 +上报数据帧 +0 1 DeviceType 功能模块类型编码,取值 E5H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 Status 车检状态,00H-无车,01H-有车 +11.1.2 上报确认 +位置 字节数 参数 说明 +上报确认帧 +0 1 DeviceType 功能模块类型编码,取值为 E5H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 RecvStatus 服务端接收处理结果,00H-处理成功,01H-处理失败 +11.2 车检除雾除水(02) +通过车检除雾除水指令,完成车辆检测器除雾除水功能,功能编号 02H。 +11.2.1 请求 +位置 字节数 + + 数据元 数据内容 +控制请求帧 +0 1 DeviceType 功能模块类型编码,取值为 E5H +控制请求帧 +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 02H +11.2.2 响应 +位置 字节数 参数 说明 +控制响应帧 +0 1 DeviceType 功能模块类型编码,取值为 E5H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 02H +3 1 CtlStatus 功能模块功能控制结果,00H-控制成功,01H-控制失败 +12 信息显示屏接口 +12.1 显示控制(01) +通过显示控制指令,完成信息显示屏的信息显示功能,功能编号 01H。 +12.1.1 请求 +位置 字节数 数据元 数据内容 +控制请求帧 +0 1 DeviceType 功能模块类型编码,取值 E6H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 FontColor 字体颜色,01H-绿色,02H-红色,03H-黄色 +4 1 FontSize 字体大小,01H~08H,00H-自适应 +5 1 FontLine 默认显示行数:01H~06H,00H-自适应 +6 1 KeepTime 费显内容显示时间,单位秒,HEX 格式: +00H 永久停留,01H~FFH 停留时间(1~255 秒) +7 N Text 显示内容,每行最多支持 10 个中文字符,行与行之间以“_”分割, +编码采用 GBK +字体颜色:绿色,字体大小:自适应,默认显示行数:自适应,费显内容显示时间:0,显示内容:123 +列如:FF FF 00 07 00 00 00 25 1B 00 00 01 9A 38 C9 A8 DC 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 0A E6 01 01 01 00 00 00 31 32 33 59 8D + +12.1.2 响应 +位置 字节数 参数 说明 +控制响应帧 +0 1 DeviceType 功能模块类型编码,取值为 E6H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 CtlStatus 功能模块功能控制结果,00H-控制成功,01H-控制失败 +列如:FF FF 00 02 00 00 00 1F B1 00 00 01 9A 38 64 5D E3 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 E6 01 01 00 33 D0 +12.2 清屏(02) +通过清屏指令,完成信息显示屏的信息清除功能,功能编号 02H。 +12.2.1 请求 +位置 字节数 数据元 数据内容 +控制请求帧 +0 1 DeviceType 功能模块类型编码,取值为 E6H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 02H +3 1 ClearType 00H-文字清屏 +01H-屏幕全红 +02H-屏幕全绿 +其他值自定义 +文字清屏, +列如:FF FF 00 06 00 00 00 1F 1B 00 00 01 9A 38 C7 4A 2B 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 E6 01 02 00 BC 1E +12.2.2 响应 +位置 字节数 参数 说明 +控制响应帧 +0 1 DeviceType 功能模块类型编码,取值为 E6H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 02H +3 1 CtlStatus 功能模块功能控制结果,00H-控制成功,01H-控制失败 +列如:FF FF 00 02 00 00 00 1F B1 00 00 01 9A 38 64 5D E3 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 E6 01 02 00 19 B8 +13 通行信号灯接口 +13.1 显示控制(01) +通过显示控制指令,完成通行信号灯的显示功能,功能编号 01H。 +13.1.1 请求 +位置 字节数 + + 数据元 数据内容 +控制请求帧 +0 1 DeviceType 功能模块类型编码,取值为 E7H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 Color 01H-绿色,02H-红色,03H-黄色 +绿色, +列如:FF FF 00 05 00 00 00 1F 1B 00 00 01 9A 38 C6 8B 2C 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 E7 01 01 01 3D 6D +13.1.2 响应 +位置 字节数 参数 说明 +控制响应帧 +0 1 DeviceType 功能模块类型编码,取值为 E7H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 CtlStatus 功能模块功能控制结果,00H-控制成功,01H-控制失败 +列如:FF FF 00 02 00 00 00 1F B1 00 00 01 9A 38 64 5D E3 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 E7 01 01 00 2F 6B +14 报警器接口 +14.1 报警控制(01) +通过报警控制指令,完成报警器开启报警和关闭报警功能,功能编号 01H。 +14.1.1 请求 +位置 字节数 数据元 数据内容 +控制请求帧 +0 1 DeviceType 功能模块类型编码,取值 E8H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 Status 00H-停止报警,01H-开始报警 +4 1 WorkMode 工作模式,HEX 格式 +00H 持续报警,01H~FFH 频率报警 +5 1 KeepTime 报警控制保持时长,单位秒,HEX 格式 +00H 一直报警,01H~FFH 报警时长 +停止报警,工作模式:持续报警,报警控制保持时长:0, +列如:FF FF 00 04 00 00 00 21 1B 00 00 01 9A 38 C1 B5 5C 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 06 E8 01 01 00 00 00 BC 39 +14.1.2 响应 +位置 字节数 参数 说明 +控制响应帧 +0 1 DeviceType 功能模块类型编码,取值为 E8H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 CtlStatus 功能模块功能控制结果,00H-控制成功,01H-控制失败 +列如:FF FF 00 02 00 00 00 1F B1 00 00 01 9A 38 64 5D E3 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 E8 01 01 00 9D 92 + +15 LED 情报板(含引导屏)接口 +15.1 显示控制(01) +通过显示控制指令,完成 LED 情报板的内容显示功能,功能编号 01H。 +15.1.1 请求 +位置 字节数 数据元 数据内容 +控制请求帧 +0 1 DeviceType 功能模块类型编码,取值 E9H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 FontColor 字体颜色,01H-绿色,02H-红色,03H-黄色 +4 1 FontSize 字体大小,01H~08H,00H-自适应 +5 1 FontLine 默认显示行数:01H~FFH,00H-自适应 +6 1 KeepTime 内容显示时间,单位秒,HEX 格式: +00H 永久停留,01H~FFH 停留时间(1~255 秒) +7 1 Format 显示格式,01H-居中对齐 02H-左对齐 03H-右对齐 +8 N Text 显示内容,行与行之间以“_”分割,编码采用 GBK +字体颜色:绿色,字体大小:自适应,默认显示行数:自适应,内容显示时间:0,显示格式:居中对齐,显示内容:123, +列如:FF FF 00 03 00 00 00 26 1B 00 00 01 9A 38 BF 93 AC 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 0B E9 01 01 01 00 00 00 01 31 32 33 68 38 + +15.1.2 响应 +位置 字节数 参数 说明 +控制响应帧 +0 1 DeviceType 功能模块类型编码,取值为 E9H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 CtlStatus 功能模块功能控制结果,00H-控制成功,01H-控制失败 +列如:FF FF 00 02 00 00 00 1F B1 00 00 01 9A 38 64 5D E3 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 E9 01 01 00 81 29 +15.2 清屏(02) +通过清屏指令,完成 LED 情报板的内容清除功能,功能编号 02H。 +15.2.1 请求 +位置 字节数 数据元 数据内容 +控制请求帧 +0 1 DeviceType 功能模块类型编码,取值为 E9H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 02H +3 1 ClearType 00H-文字清屏,01H-屏幕全红,02H-屏幕全绿,其他值自定义 +“全屏清除”, +列如:FF FF 00 02 00 00 00 1F 1B 00 00 01 9A 38 BB DD D4 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 E9 01 02 00 88 74 +15.2.2 响应 +位置 字节数 参数 说明 +控制响应帧 +0 1 DeviceType 功能模块类型编码,取值为 E9H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 02H +3 1 CtlStatus 功能模块功能控制结果,00H-控制成功,01H-控制失败 +列如:FF FF 00 02 00 00 00 1F B1 00 00 01 9A 38 64 5D E3 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 E9 01 02 00 AB 41 +16 雨棚信号灯接口 +16.1 显示控制(01) +通过显示控制指令,完成雨棚信号灯显示功能,功能编号 01H。 +16.1.1 请求 +位置 字节数 数据元 数据内容 +控制请求帧 +0 1 DeviceType 功能模块类型编码,取值 EAH +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 Color 01H-绿色,02H-红色,03H-黄色 +“绿色”, +列如:FF FF 00 01 00 00 00 1F 1B 00 00 01 9A 38 BA 23 B4 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 EA 01 01 01 A3 AE + +16.1.2 响应 +位置 字节数 参数 说明 +控制响应帧 +0 1 DeviceType 功能模块类型编码,取值为 EAH +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 CtlStatus 功能模块功能控制结果,00H-控制成功,01H-控制失败 +列如:FF FF 00 02 00 00 00 1F B1 00 00 01 9A 38 64 5D E3 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 EA 01 01 00 A4 E4 +17 雾灯接口 +17.1 显示控制(01) +通过显示控制指令,完成控制雾灯显示功能,功能编号 01H。 +17.1.1 请求 +位置 字节数 数据元 数据内容 +控制请求帧 +0 1 DeviceType 功能模块类型编码,取值 EBH +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 Status 00H-关闭,01H-开启 +4 1 WorkMode 工作模式,HEX 格式 +00H 长亮,01H~FFH 频率闪烁 +17.1.2 响应 +位置 字节数 参数 说明 +控制响应帧 +0 1 DeviceType 功能模块类型编码,取值为 EBH +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 CtlStatus 功能模块功能控制结果,00H-控制成功,01H-控制失败 +25 语音播报设备接口 +25.1 播放提示音(01) +通过播放提示音指令,完成对语音播报设备播报内容控制的功能,功能编号 +01H。 +25.1.1 请求 +位置 字节数 参数 说明 +控制请求帧 +0 1 DeviceType 功能模块类型编码 F4H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 Type 语音控制类型 +00H 按文字播报 +01H 按指定语音序列播报 +02H -FFH 按其他语音格式 +4 2 Length 语音内容字节长度 N 最大 FFFFH +6 N Data 语音内容 +“按文字播报”,播报“123”, +列如:FF FF 00 02 00 00 00 24 1B 00 00 01 9A 38 64 5D E3 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 09 F4 01 01 00 00 03 31 32 33 3C 34 +25.1.2 响应 +位置 字节数 参数 说明 +控制响应帧 +0 1 DeviceType 功能模块类型编码 F4H +1 1 DeviceIndex 功能模块序号 +2 1 DeviceFuncType 功能模块功能编号 此处取值 01H +3 1 CtlStatus 功能模块功能控制结果,00H-控制成功,01H-控制失败 +列如:FF FF 00 02 00 00 00 1F B1 00 00 01 9A 38 64 5D E3 20 13 14 52 00 01 23 45 67 89 AB CD EF 00 00 01 00 04 F4 01 01 00 C9 07 +26创迪自制协议接口(TCP/UDP通信,默认端口号:10011) +26.1.1 搜索设备(21H) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 21H。 +列如:FF FF 00 10 00 00 00 01 21 E7 93 +26.1.2 搜索设备结果(12H) +位置 字节数 参数 说明 +0 1 CmdType 指令代码,此处取值 12H。 +1 4 Ip Ip地址 +5 2 Port 端口号 +7 4 gateWay 默认网关 +11 4 Mask 子网掩码 +15 1 ErrorCode 状态码,00H-获取成功,其他-获取失败 +列如:FF FF 00 10 00 00 00 10 12 C0 A8 01 64 25 38 C0 A8 01 01 FF FF FF 00 00 F4 7C +附录 A 设备功能模块运行状态 +A.1 设备运行状态 +栏杆机状态,00H-落杆,01H-抬杆 +车辆检测器状态,00H-无车,01H-有车 +车牌自动识别状态,00H-关闭,01H-开启 +RSU 路侧单元状态,00H-关闭,01H-开启 +信息显示屏状态,00H-关闭,01H-开启 +通行灯状态,00H-红灯,01H-绿灯 +雾灯状态,00H-关闭,01H-开启 +雨棚信号灯状态,00H-红灯,01H-绿灯 +报警器状态,00H-停止报警,01H-开始报警 +LED 情报板状态,00H-关闭,01H-开启 +A.2 异常编码 +通用模块异常编码 +编码 状态说明 备注 +00H 正常 +01H 异常 对于无法具体描述异常状态的模块,比如车辆检测器 +02H 初始化信息异常 设备需重新调取初始化设备接口同步信息 +03H 缺少必填参数 必填参数缺失 +04H 非法参数 如:参数格式异常 +高清车牌图像识别设备(E2H)异常编码 +编码 状态说明 备注 +10H 时钟同步异常 时差≥10min +11H 数据积压 ≥1000 条 +12H 识别率异常 最近 1000 条数据,识别率≤98% +13H 网线断开 +14H 网络通信异常 包括 ip 冲突,无法链接等 +15H 存储异常 +16H 图像模糊 +17H 视频预览异常 +18H 算法模型加载异常 +19H 抓拍图片上传失败 +1AH 抓拍图片累积 ≥1000 张 +1BH 网络抓拍(主动抓拍)失败 +1CH 车检器异常 +1DH 补光灯异常 +1EH 镜头不清晰 +84 +通用模块异常编码 +编码 状态说明 备注 +1FH-7FH 预留 此编码区间为行业统一使用 +80H-FEH 厂家自定义 此编码区间可厂家自定义使用 +车轴型识别设备(E3H)异常编码 +编码 状态说明 备注 +10H 时钟同步异常 时差≥10min +11H 数据积压 ≥1000 条 +12H 识别率异常 最近 1000 条数据,识别率≤98% +13H 网线断开 +14H 网络通信异常 包括 ip 冲突,无法链接等 +15H 存储异常 +16H 图像模糊 +17H 视频预览异常 +18H 算法模型加载异常 +19H 抓拍图片上传失败 +1AH 抓拍图片累积 ≥1000 张 +1BH 网络抓拍(主动抓拍)失败 +1CH 车检器异常 +1DH 补光灯异常 +1EH 镜头不清晰 +1FH 激光模组异常 +20H 车头相机异常 +21H 车身相机异常 +22H 车尾相机异常 +23H 主控板异常 +24H 车牌未识别 +25H 车型未识别 +26H Http 或 Ftp 结果上传异常 +27H-7FH 预留 此编码区间为行业统一使用 +80H-FEH 厂家自定义 此编码区间可厂家自定义使用 +发卡/自助交费设备(EDH/EEH)异常编码 +编码 状态说明 备注 +10H 自助发卡设备(上)通道异常 +11H 自助发卡设备(上)备卡超时 +12H 自助发卡设备(上)缺卡 +13H 自助发卡设备(上)状态异常 +14H 自助发卡设备(下)通道异常 +15H 自助发卡设备(下)备卡超时 +16H 自助发卡设备(下)缺卡 +17H 自助发卡设备(下)状态异常 +18H 自助交费设备(上)通道异常 +19H 自助交费设备(上)状态异常 +1AH 自助交费设备(下)通道异常 +1BH 自助交费设备(下)状态异常 +1CH 送票设备(上)卡纸 +85 +通用模块异常编码 +编码 状态说明 备注 +1DH 送票设备(上)状态异常 +1EH 送票设备(下)卡纸 +1FH 送票设备(下)状态异常 +20H CPC 读卡器状态异常 +21H ETC 读卡器(上)状态异常 +22H ETC 读卡器(下)状态异常 +23H 打印机(上)状态异常 +24H 打印机(上)卡纸 +25H 打印机(上)缺纸 +26H 打印机(下)状态异常 +27H 打印机(下)卡纸 +28H 打印机(下)缺纸 +29H 摄像头(上)状态异常 +2AH 摄像头(下)状态异常 +2BH 二维码扫码枪(上)状态异常 +2CH 二维码扫码枪(下)状态异常 +2DH 麦克风状态异常 +2EH 设备温度过高 +2FH 设备温度过低 +30H 设备湿度过高 +31H 设备湿度过低 +32H 设备烟雾状态异常 +33H 设备水浸状态异常 +34H 设备门磁状态异常 +35H 空调状态异常 +36H 回收坏卡失败 +37H 面板伸出失败 +38H 面板缩回失败 +39H 回收卡箱满预警 +3AH-7FH 预留 此编码区间为行业统一使用 +80H-FEH 厂家自定义 此编码区间可厂家自定义使用 + + +附录 D CRC16 校验源码 +//init_crc 为计算 crc 初始值,根据协议,该值为 FFFFH +#define CRC_POLYNOM 0x8408 +unsigned short calc_crc16(int init_crc, unsigned char *crc_data, int len) +{ +int crc = init_crc, cnt, k; +for(cnt = 0; cnt < len; cnt++) +{//loop data length +crc ^= crc_data[cnt]; +for(k = 0; k < 8; k++) +{//every bit data +if(crc & 0x1) +{ +crc = (crc >> 1) ^ CRC_POLYNOM; +} +else +{ +crc = (crc >> 1); +} +} +} +crc = (~crc) & 0xFFFF; +return ( unsigned short )crc; +} diff --git "a/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\345\205\211\346\225\217\344\274\240\346\204\237\345\231\250\344\272\256\345\272\246\350\214\203\345\233\264\351\231\220\345\210\266\344\270\216\350\276\223\345\207\272\351\222\263\344\275\215\344\274\230\345\214\226.md" "b/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\345\205\211\346\225\217\344\274\240\346\204\237\345\231\250\344\272\256\345\272\246\350\214\203\345\233\264\351\231\220\345\210\266\344\270\216\350\276\223\345\207\272\351\222\263\344\275\215\344\274\230\345\214\226.md" new file mode 100644 index 0000000..f3fb723 --- /dev/null +++ "b/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\345\205\211\346\225\217\344\274\240\346\204\237\345\231\250\344\272\256\345\272\246\350\214\203\345\233\264\351\231\220\345\210\266\344\270\216\350\276\223\345\207\272\351\222\263\344\275\215\344\274\230\345\214\226.md" @@ -0,0 +1,158 @@ +# 光敏传感器亮度范围限制与输出钳位优化 + +**状态**:`[已完成]` + +## 1. 问题背景 + +出厂产品对显示亮度有最低/最高限制要求: +- 亮度等级不能低于 4 级(最低环境光下也不能过暗) +- 亮度等级不能高于 7 级(最高环境光下也不能过亮) + +优化前,`app_light_sensor_set_range(4, 7)` 被放在 `app_factory_test.c` 的工厂测试按键处理中,仅在工厂测试流程按下 TEST 按键后才生效。这意味着正常开机运行时,光敏自动调光范围为默认的 1~7,不满足出厂亮度约束。 + +## 2. 优化目标 + +将亮度范围限制(4~7)的设置从工厂测试按键触发改为上电传感器初始化时自动生效,确保光敏自动调光从开机起就在合规范围内运行。 + +## 3. 涉及文件 + +| 文件 | 层级 | 变更内容 | +|---|---|---| +| `Device/Inc/dev_light_sensor.h` | Device | 新增 `LIGHT_SENSOR_MIN_LEVEL` / `LIGHT_SENSOR_MAX_LEVEL` 宏定义;新增 `min_level`/`max_level` 字段和 `dev_light_sensor_set_range()` 声明 | +| `Device/IO/dev_light_sensor.c` | Device | 实现范围钳位逻辑(`dev_light_sensor_read` 输出前钳位)和 `dev_light_sensor_set_range()` | +| `Application/Inc/app_light_sensor.h` | Application | 新增 `app_light_sensor_set_range()` 声明 | +| `Application/Src/app_light_sensor.c` | Application | `app_light_sensor_init()` 中使用宏调用 `dev_light_sensor_set_range()`;移除工厂测试中的调用 | +| `Application/Src/app_factory_test.c` | Application | 移除 `app_light_sensor_set_range(4, 7)` 调用 | + +## 4. 宏定义 + +在 `Device/Inc/dev_light_sensor.h` 中定义: + +```c +/** 自动调光默认最低亮度等级(出厂约束:不允许低于 4 级) */ +#define LIGHT_SENSOR_MIN_LEVEL 4 + +/** 自动调光默认最高亮度等级(出厂约束:不允许高于 7 级) */ +#define LIGHT_SENSOR_MAX_LEVEL 7 +``` + +所有使用亮度范围上下限的地方均引用这两个宏,不再使用硬编码数字。 + +## 5. Device 层设计 + +### 5.1 `light_sensor_dev_t` 结构体 + +```c +typedef struct { + void *adc; + dev_display_t *display; + uint8_t min_level; /* 自动调光最低亮度 (1~7) */ + uint8_t max_level; /* 自动调光最高亮度 (1~7) */ +} light_sensor_dev_t; +``` + +`dev_light_sensor_init()` 默认将 `min_level` 设为 1、`max_level` 设为 7。 + +### 5.2 `dev_light_sensor_set_range()` + +```c +void dev_light_sensor_set_range(light_sensor_dev_t *dev, uint8_t min, uint8_t max) +{ + if (min < 1) min = 1; + if (min > 7) min = 7; + if (max < 1) max = 1; + if (max > 7) max = 7; + if (min > max) min = max; + + dev->min_level = min; + dev->max_level = max; +} +``` + +参数钳位规则: +- `min` 和 `max` 各自限制在 1~7 范围内 +- 若 `min > max`,则将 `min` 压到与 `max` 相等 + +### 5.3 输出钳位 + +在 `dev_light_sensor_read()` 中,ADC 均值映射为 1~7 亮度等级后,追加钳位: + +```c +if (level < dev->min_level) level = dev->min_level; +if (level > dev->max_level) level = dev->max_level; +``` + +这确保无论环境光传感器读到什么值,最终输出的亮度等级都不会超出 `[min_level, max_level]` 范围。 + +## 6. Application 层调用 + +### 6.1 上电初始化时设置范围 + +`app_light_sensor_init()` 在 `sw_app_initcall` 阶段自动执行: + +```c +void app_light_sensor_init(void) +{ + dev_light_sensor_init(&s_sensor_dev, dev_display_get()); + dev_light_sensor_set_range(&s_sensor_dev, LIGHT_SENSOR_MIN_LEVEL, LIGHT_SENSOR_MAX_LEVEL); + + const osThreadAttr_t attr = { + .name = "light_sensor_task", + .stack_size = 128 * 4, + .priority = osPriorityLow, + }; + g_light_sensor_task_handle = osThreadNew(app_light_sensor_task, NULL, &attr); +} +sw_app_initcall(app_light_sensor_init); +``` + +亮度范围在传感器设备初始化之后、RTOS 任务创建之前设置,确保自动调光任务从第一次执行起就使用约束后的范围。 + +### 6.2 运行时动态调整 + +`app_light_sensor_set_range(min, max)` 作为 Application 层包装,可在运行时通过其他模块(如工厂测试、LDI 协议指令)动态调整范围: + +```c +void app_light_sensor_set_range(uint8_t min, uint8_t max) +{ + dev_light_sensor_set_range(&s_sensor_dev, min, max); +} +``` + +### 6.3 工厂测试中的移除 + +优化前 `app_factory_test.c` 中的调用: + +```c +app_light_sensor_set_range(4, 7); +``` + +该调用已移除。工厂测试流程仍保留 `#include "app_light_sensor.h"`,因为老化阶段需要通过 `g_light_sensor_task_handle` 挂起光敏任务。 + +## 7. 数据流 + +``` +app_light_sensor_init() (sw_app_initcall) + ├─ dev_light_sensor_init() → min_level=1, max_level=7 + ├─ dev_light_sensor_set_range( + │ LIGHT_SENSOR_MIN_LEVEL, // = 4 + │ LIGHT_SENSOR_MAX_LEVEL // = 7 + │ ) → min_level=4, max_level=7 + └─ osThreadNew(light_sensor_task) + +light_sensor_task (每秒执行): + └─ dev_light_sensor_auto_adjust() + └─ dev_light_sensor_read() + ├─ ADC 8次均值 → 映射 1~7 + └─ 钳位到 [min_level=4, max_level=7] → 返回 4~7 +``` + +## 8. 设计决策 + +| 决策 | 理由 | +|---|---| +| 亮度范围上下限使用宏定义 | 避免硬编码魔数,修改范围只需改一处宏定义 | +| 钳位放在 `dev_light_sensor_read()` 内部 | Device 层保证输出合规,上层无需关心范围校验 | +| 范围设置放在 `app_light_sensor_init()` 而非工厂测试 | 上电即生效,不依赖人工操作 | +| `set_range()` 支持运行时调用 | 灵活性:工厂测试仍可通过该接口临时调整范围做验证 | +| `min`/`max` 自动钳位到 1~7 | 防御性编程,防止越界值破坏 PWM 输出 | diff --git "a/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\345\244\226\350\256\276\346\216\245\345\217\243\345\256\236\346\226\275\350\256\241\345\210\222.md" "b/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\345\244\226\350\256\276\346\216\245\345\217\243\345\256\236\346\226\275\350\256\241\345\210\222.md" new file mode 100644 index 0000000..b508a27 --- /dev/null +++ "b/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\345\244\226\350\256\276\346\216\245\345\217\243\345\256\236\346\226\275\350\256\241\345\210\222.md" @@ -0,0 +1,54 @@ +# 外设接口实施计划 + +**状态**:`[计划中]` `[待实施]` + +## 1. 文档目的 +本文档用于指导后续实现与联调以下外设接口: +- 创迪自制协议接口(默认端口 `10011`) +- 信息显示屏接口(E6H) +- 通行信号灯接口(E7H) +- 报警器接口(E8H) + +## 2. 当前定位 +### 2.1 创迪自制协议接口 +建议定位为发现/入网辅助接口,不承担主业务控制。 + +### 2.2 E6 / E7 / E8 +建议统一纳入 LDI 设备能力层,由协议层解析后分发到设备执行层。 + +## 3. 历史过程痕迹 +- 该计划保留了分阶段实施思路:先澄清协议边界,再统一设备能力层,再做基础通信闭环,最后完成设备适配与状态上报。 +- 当前版本保留执行顺序,不删除前期判断痕迹。 + +## 4. 当前仍然有效的结论 +- 创迪 `10011` 接口不应误实现成完整业务协议 +- 协议层不应直接操作 HAL GPIO +- 设备能力必须可回到统一的状态上报模型 +- E6 优先复用现有显示链路,E7/E8 优先走统一离散设备能力 + +## 5. 与当前代码对照 +### 5.1 当前代码已具备基础 +- `Application/Src/LDI/app_ldi.c` +- `Application/Src/LDI/app_ldi_cmd.c` +- `Application/Src/app_dispatch.c` +- `Application/Src/app_render.c` +- `Device/IO/dev_io_ctrl.c` + +### 5.2 仍待实施部分 +- 统一设备能力接口抽象 +- 创迪发现口与业务口边界收口 +- 基础通信鲁棒性补强 +- E6/E7/E8 的一致性状态上报 + +## 6. 验证状态 +- 编译验证:未进入实施阶段 +- 功能验证:待实施 +- 联调验证:待实施 + +## 7. 执行顺序建议 +1. 定协议边界 +2. 抽设备能力层 +3. 修基础通信风险 +4. 完成 E6 闭环 +5. 完成 E7/E8 闭环 +6. 做联调矩阵 diff --git "a/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\345\255\227\345\272\223\350\212\257\347\211\207\346\233\264\346\215\242\344\270\216\347\247\273\346\244\215\346\214\207\345\257\274.md" "b/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\345\255\227\345\272\223\350\212\257\347\211\207\346\233\264\346\215\242\344\270\216\347\247\273\346\244\215\346\214\207\345\257\274.md" new file mode 100644 index 0000000..7dd807d --- /dev/null +++ "b/doc/02_LDI\345\215\217\350\256\256\344\270\216\345\244\226\350\256\276\346\216\245\345\217\243/\345\255\227\345\272\223\350\212\257\347\211\207\346\233\264\346\215\242\344\270\216\347\247\273\346\244\215\346\214\207\345\257\274.md" @@ -0,0 +1,479 @@ +# 字库芯片更换与移植指导 + +**状态**: `[指导文档]` `[已验证通过]` `[支持多芯片切换]` + +## 1. 背景 + +本项目支持两种字库芯片配置,可通过 `app_render_chip_select()` 运行时切换: + +| 配置枚举 | 芯片型号 | 布局 | 字号 | ASCII 索引 | +|---------|---------|------|------|-----------| +| `FONT_CHIP_W25Q64` | W25Q64 | 非连续(12个独立区块) | 16/24/32 | 原始字符码(不减0x20) | +| `FONT_CHIP_MX25L256` | MX25L25645GM2I-10G | 连续(顺序拼接) | 14/16/20/24/32 | ch[0] - 0x20 | + +### 两种布局的核心差异 + +| 对比项 | W25Q64 (`FONT_CHIP_W25Q64`) | MX25L256 (`FONT_CHIP_MX25L256`) | +|--------|----------------------------|--------------------------------| +| 字号 | 16 / 24 / 32 | 14 / 16 / 20 / 24 / 32 | +| 字库布局 | 非连续, 12个独立区块 | 连续顺序拼接 | +| 地址计算 | 每区块独立 base + X/Y/Z 修正 | 同一公式, Y/Z 补偿块起始偏移 | +| ASCII 索引 | `ch[0]`(原始码) | `ch[0] - 0x20` | +| GBK 索引 | **94列序**: `(高-0xA1)*94 + (低-0xA1)` | **190列序**: `(高-0x81)*190 + ...` | + +--- + +## 2. 切换 API + +### 2.1 头文件声明 + +```c +/* app_render.h */ + +typedef enum { + FONT_CHIP_W25Q64 = 0, /* W25Q64: 非连续布局, 16/24/32 */ + FONT_CHIP_MX25L256 = 1, /* MX25L256: 连续布局, 14/16/20/24/32 */ + FONT_CHIP_CNT, +} font_chip_id_t; + +bool app_render_chip_select(font_chip_id_t id); +font_chip_id_t app_render_chip_current(void); +``` + +### 2.2 使用方式 + +```c +#include "app_render.h" + +app_render_chip_select(FONT_CHIP_W25Q64); /* 切换到 W25Q64 */ +app_render_chip_select(FONT_CHIP_MX25L256); /* 切换到 MX25L256 */ + +font_chip_id_t current = app_render_chip_current(); /* 查询当前 */ +``` + +### 2.3 上电自动选择(DIP2 拨码开关) + +上电时 `_render_init()` 自动读取 `DEV_KEY_DIP2` 拨码开关状态选择字库芯片: + +| 开关状态 | GPIO 电平 | `get_state()` | 芯片 | +|----------|-----------|---------------|------| +| OFF(位置 0) | HIGH(上拉) | `true` | **W25Q64** | +| ON(位置 1) | LOW(接地) | `false` | **MX25L256** | + +**硬件说明**:`DEV_KEY_DIP2` 对应 PE8,内部上拉,active_low。 +`_dip_get_state()` 内部执行 `!pl_gpio_read()`,对 GPIO 原始电平取反: +引脚 HIGH(上拉)→ 取反 → `true`;引脚 LOW(接地)→ 取反 → `false`。 + +选择逻辑封装在 `_dip_font_select()` 中,`_render_init()` 调用它完成上电自动选择。 +上电后仍可通过 `app_render_chip_select()` 运行时手动切换。 + +--- + +## 3. 配置结构 + +### 3.1 font_chip_config_t + +```c +typedef struct { + const char *name; /* 配置名称 */ + const void *regions; /* flash_region_t 数组指针 */ + uint8_t region_count; /* 数组元素个数 */ + const font_size_t *adaptive_sizes; /* 自适应字号候选列表(从大到小) */ + uint8_t adaptive_count; /* 候选列表长度 */ + bool ascii_raw_code; /* true=ASCII用原始码(W25Q64); false=减0x20(MX25L256) */ + bool gbk_index_190; /* true=GBK用190列序(MX25L256); false=94列序(W25Q64) */ +} font_chip_config_t; +``` + +### 3.2 flash_region_t + +```c +typedef struct { + font_size_t size; /* 字号 */ + font_enc_t charset; /* 编码: FONT_ENC_ASCII / FONT_ENC_GBK */ + font_type_t type; /* 字型: ST/FS/KT/HT */ + uint16_t base; /* 扇区基准 */ + int8_t sec_adj; /* 扇区修正量 X */ + int8_t page_adj; /* 页修正量 Y */ + int16_t byte_adj; /* 字节修正量 Z */ +} flash_region_t; +``` + +--- + +## 4. W25Q64 地址计算公式(FONT_CHIP_W25Q64) + +### 4.1 地址公式 + +完全复刻01Embedded `func.c` 中6个读取函数的分步计算: + +``` +FonfAddr = char_idx * bytes_per_char +sec = FonfAddr / 4096 +page = (FonfAddr % 4096) / 256 +byte = (FonfAddr % 4096) % 256 +readaddr = (base + sec + X) * 4096 + (page + Y) * 256 + byte + Z +``` + +**关键**:ASCII 字符索引 `char_idx = ch[0]`(原始码,不减 0x20)。 + +**GB2312 索引**:`char_idx = (ch[0] - 0xA1) * 94 + (ch[1] - 0xA1)`(94列序,非GBK的190列)。 + +### 4.2 W25Q64 Flash 参数 + +- 型号:W25Q64(或兼容 W25Q128) +- 扇区大小:4096 字节 +- 页大小:256 字节 + +### 4.3 字库区块起始扇区 + +定义在 `01Embedded/USER/FUNC/func.h` 中: + +``` +字体 字号 ASCII扇区 GB2312扇区 +ST 16 0 2 +ST 24 278 284 +ST 32 905 913 +FS 16 0 71 +FS 24 279 439 +FS 32 907 1189 +KT 16 1 140 +KT 24 281 594 +KT 32 909 1465 +HT 16 1 209 +HT 24 282 750 +HT 32 911 1741 +``` + +### 4.4 已验证的地址查找表 + +```c +static const flash_region_t g_font_regions[] = { + /* size enc type base X Y Z */ + /* 16pt ASCII — bpg=16 */ + {16, FONT_ENC_ASCII, FONT_ST, 0, 0, 0, 0}, + {16, FONT_ENC_ASCII, FONT_FS, 0, 0, 0, 0}, + {16, FONT_ENC_ASCII, FONT_KT, 1, 0, 0, 64}, + {16, FONT_ENC_ASCII, FONT_HT, 1, 0, 0, 64}, + /* 16pt GB2312 — bpg=32 */ + {16, FONT_ENC_GBK, FONT_ST, 2, 0, 0, 128}, + {16, FONT_ENC_GBK, FONT_FS, 71, 0, 0, 288}, + {16, FONT_ENC_GBK, FONT_KT, 140, 0, 0, 448}, + {16, FONT_ENC_GBK, FONT_HT, 209, 0, 2, 96}, + /* 24pt ASCII — bpg=48 */ + {24, FONT_ENC_ASCII, FONT_ST, 278, 0, 3, 0}, + {24, FONT_ENC_ASCII, FONT_FS, 279, 0, 12, -224}, + {24, FONT_ENC_ASCII, FONT_KT, 281, 0, 3, 64}, + {24, FONT_ENC_ASCII, FONT_HT, 282, 0, 12, -160}, + /* 24pt GB2312 — bpg=72 */ + {24, FONT_ENC_GBK, FONT_ST, 284, 1, -12, -128}, + {24, FONT_ENC_GBK, FONT_FS, 439, 1, -7, -64}, + {24, FONT_ENC_GBK, FONT_KT, 594, 1, -2, 0}, + {24, FONT_ENC_GBK, FONT_HT, 750, 1, -12, -192}, + /* 32pt ASCII — bpg=64 */ + {32, FONT_ENC_ASCII, FONT_ST, 905, 1, -7, -128}, + {32, FONT_ENC_ASCII, FONT_FS, 907, 1, -7, -96}, + {32, FONT_ENC_ASCII, FONT_KT, 909, 1, -7, -64}, + {32, FONT_ENC_ASCII, FONT_HT, 911, 1, -7, -32}, + /* 32pt GB2312 — bpg=128 */ + {32, FONT_ENC_GBK, FONT_ST, 913, 1, -7, 0}, + {32, FONT_ENC_GBK, FONT_FS, 1189, 1, -5, 32}, + {32, FONT_ENC_GBK, FONT_KT, 1465, 1, -3, 64}, + {32, FONT_ENC_GBK, FONT_HT, 1741, 1, -1, 96}, +}; +``` + +### 4.5 各函数的偏移常量(X, Y, Z) + +#### 16pt ASCII + +| 字体 | X | Y | Z | +|------|---|---|---| +| ST | 0 | 0 | 0 | +| FS | 0 | 0 | 0 | +| KT | 0 | 0 | +64 | +| HT | 0 | 0 | +64 | + +#### 16pt GB2312 + +| 字体 | X | Y | Z | +|------|---|---|---| +| ST | 0 | 0 | +128 | +| FS | 0 | 0 | +288 | +| KT | 0 | 0 | +448 | +| HT | 0 | +2 | +96 | + +#### 24pt ASCII + +| 字体 | X | Y | Z | +|------|---|---|---| +| ST | 0 | +3 | 0 | +| FS | 0 | +12 | -224 | +| KT | 0 | +3 | +64 | +| HT | 0 | +12 | -160 | + +#### 24pt GB2312 + +| 字体 | X | Y | Z | +|------|---|---|---| +| ST | +1 | -12 | -128 | +| FS | +1 | -7 | -64 | +| KT | +1 | -2 | 0 | +| HT | +1 | -12 | -192 | + +#### 32pt ASCII + +| 字体 | X | Y | Z | +|------|---|---|---| +| ST | +1 | -7 | -128 | +| FS | +1 | -7 | -96 | +| KT | +1 | -7 | -64 | +| HT | +1 | -7 | -32 | + +#### 32pt GB2312 + +| 字体 | X | Y | Z | +|------|---|---|---| +| ST | +1 | -7 | 0 | +| FS | +1 | -5 | +32 | +| KT | +1 | -3 | +64 | +| HT | +1 | -1 | +96 | + +--- + +## 5. MX25L256 字库适配(FONT_CHIP_MX25L256) + +### 5.1 地址计算 + +MX25L256 与 W25Q64 共用同一套 sec/page/byte 分步公式: + +``` +readaddr = (base + sec + X) * 4096 + (page + Y) * 256 + byte + Z +``` + +但 MX25L256 的 X=0,Y/Z 用于补偿每个块起始地址的非扇区对齐偏移。 +base 为累计字节偏移的扇区号,Y = (偏移 % 4096) / 256,Z = (偏移 % 4096) % 256。 + +**ASCII 索引**:`char_idx = ch[0] - 0x20`(从空格开始)。 +**GBK 索引**:**190列序** — `char_idx = (ch[0] - 0x81) * 190 + (ch[1] >= 0x80 ? ch[1] - 0x41 : ch[1] - 0x40)`。 + +> **注意**:W25Q64 用 GB2312 94列序,MX25L256 用 GBK 190列序,两者不同! + +### 5.2 Flash 参数 + +- 型号:MX25L25645GM2I-10G +- 总容量:32MB +- 扇区大小:4096 字节 + +### 5.3 字库布局 + +按 `STD-main-Orig` 工程的 `g_font_lib[]` 表,每个(字号, 字型, 编码)块在 Flash 中**严格顺序拼接**: + +``` +[14pt ST ASCII] [14pt FS ASCII] [14pt KT ASCII] [14pt HT ASCII] +[14pt ST GBK] [14pt FS GBK] [14pt KT GBK] [14pt HT GBK] +[16pt ST ASCII] [16pt FS ASCII] [16pt KT ASCII] [16pt HT ASCII] +[16pt ST GBK] [16pt FS GBK] [16pt KT GBK] [16pt HT GBK] +... +[32pt ST GBK] [32pt FS GBK] [32pt KT GBK] [32pt HT GBK] +``` + +每种字号4种字型的ASCII块连续排列,再连续排列4种字型的GBK块。 + +### 5.4 已验证的地址查找表 + +从 `STD-main-Orig` 的 `g_font_lib[]` 累加推算,所有 base/X/Y/Z 值已填入: + +```c +static const flash_region_t g_font_regions_orig[] = { + /* size enc type base X Y Z */ + /* 14pt */ + {14, FONT_ENC_ASCII, FONT_ST, 0, 0, 0, 0}, + {14, FONT_ENC_ASCII, FONT_FS, 0, 0, 5, 64}, + {14, FONT_ENC_ASCII, FONT_KT, 0, 0, 10, 128}, + {14, FONT_ENC_ASCII, FONT_HT, 0, 0, 15, 192}, + {14, FONT_ENC_GBK, FONT_ST, 1, 0, 5, 0}, + {14, FONT_ENC_GBK, FONT_FS, 164, 0, 15, 112}, + {14, FONT_ENC_GBK, FONT_KT, 328, 0, 9, 224}, + {14, FONT_ENC_GBK, FONT_HT, 492, 0, 4, 80}, + /* 16pt */ + {16, FONT_ENC_ASCII, FONT_ST, 655, 0, 14, 192}, + {16, FONT_ENC_ASCII, FONT_FS, 656, 0, 4, 192}, + {16, FONT_ENC_ASCII, FONT_KT, 656, 0, 10, 192}, + {16, FONT_ENC_ASCII, FONT_HT, 657, 0, 0, 192}, + {16, FONT_ENC_GBK, FONT_ST, 657, 0, 6, 192}, + {16, FONT_ENC_GBK, FONT_FS, 844, 0, 7, 64}, + {16, FONT_ENC_GBK, FONT_KT, 1031, 0, 7, 192}, + {16, FONT_ENC_GBK, FONT_HT, 1218, 0, 8, 64}, + /* 20pt */ + {20, FONT_ENC_ASCII, FONT_ST, 1405, 0, 8, 192}, + {20, FONT_ENC_ASCII, FONT_FS, 1406, 0, 7, 192}, + {20, FONT_ENC_ASCII, FONT_KT, 1407, 0, 6, 192}, + {20, FONT_ENC_ASCII, FONT_HT, 1408, 0, 5, 192}, + {20, FONT_ENC_GBK, FONT_ST, 1409, 0, 4, 192}, + {20, FONT_ENC_GBK, FONT_FS, 1759, 0, 15, 176}, + {20, FONT_ENC_GBK, FONT_KT, 2110, 0, 10, 160}, + {20, FONT_ENC_GBK, FONT_HT, 2461, 0, 5, 144}, + /* 24pt */ + {24, FONT_ENC_ASCII, FONT_ST, 2812, 0, 0, 128}, + {24, FONT_ENC_ASCII, FONT_FS, 2813, 0, 2, 128}, + {24, FONT_ENC_ASCII, FONT_KT, 2814, 0, 4, 128}, + {24, FONT_ENC_ASCII, FONT_HT, 2815, 0, 6, 128}, + {24, FONT_ENC_GBK, FONT_ST, 2816, 0, 8, 128}, + {24, FONT_ENC_GBK, FONT_FS, 3237, 0, 5, 160}, + {24, FONT_ENC_GBK, FONT_KT, 3658, 0, 2, 192}, + {24, FONT_ENC_GBK, FONT_HT, 4078, 0, 15, 224}, + /* 32pt */ + {32, FONT_ENC_ASCII, FONT_ST, 4499, 0, 13, 0}, + {32, FONT_ENC_ASCII, FONT_FS, 4501, 0, 5, 0}, + {32, FONT_ENC_ASCII, FONT_KT, 4502, 0, 13, 0}, + {32, FONT_ENC_ASCII, FONT_HT, 4504, 0, 5, 0}, + {32, FONT_ENC_GBK, FONT_ST, 4505, 0, 13, 0}, + {32, FONT_ENC_GBK, FONT_FS, 5253, 0, 15, 0}, + {32, FONT_ENC_GBK, FONT_KT, 6002, 0, 1, 0}, + {32, FONT_ENC_GBK, FONT_HT, 6750, 0, 3, 0}, +}; /* total: 30713088 bytes = 29.3 MB */ +``` + +### 5.5 base 值推导方法 + +每个块的 base/Y/Z 由其在 Flash 中的**累计字节偏移**推算: + +``` +offset = 前面所有块的 unit_size 累加 +base = offset / 4096 (扇区号) +Y = (offset % 4096) / 256 (页修正) +Z = (offset % 4096) % 256 (字节修正) +X = 0 (无扇区修正) +``` + +其中每个块的 unit_size: +- ASCII: `sz * ((sz/2 + 7) / 8) * 96` +- GBK: `sz * ((sz + 7) / 8) * 23940` + +--- + +## 6. 适配过程中发现并修复的 Bug + +### 6.1 GBK 索引公式不统一 + +**问题**:两种芯片的 GBK 字库使用不同的列序索引: +- W25Q64:**GB2312 94列序** — `(ch[0]-0xA1)*94 + (ch[1]-0xA1)` +- MX25L256:**GBK 190列序** — `(ch[0]-0x81)*190 + (ch[1]>=0x80 ? ch[1]-0x41 : ch[1]-0x40)` + +早期代码统一用了 W25Q64 的94列序,切换到 MX25L256 后所有汉字地址错位。 + +**修复**:通过 `font_chip_config_t.gbk_index_190` 标志区分,`_flash_addr()` 中按配置选择公式。 + +### 6.2 读取字节数错误 + +**问题**:`_glyph_bytes()` 返回解包后的像素缓冲区大小(如16x16=256字节),但芯片上每个字模只存打包数据(如16x16=32字节)。读256字节会越界到下一个字模。 + +```c +// 旧(错误): +app_font_read_glyph(&key, addr, buf, _glyph_bytes(key)); // 读256字节 + +// 新(正确): +dev_storage_read(font, addr, buf, _packed_glyph_bytes(key)); // 读32字节 +``` + +**影响**:虽然显示函数只用前32字节所以看起来"能用",但读取越界可能导致偶发数据错乱。 + +### 6.3 自适应字号包含不存在的字号 + +**问题**:自适应选择器曾尝试14pt和20pt,但 W25Q64 只有16/24/32。现已通过配置化的 `adaptive_sizes` 列表解决——W25Q64 配置只包含 `{FONT_32, FONT_24, FONT_16}`,MX25L256 配置包含全部5种。 + +### 6.4 ASCII 字符索引错误减去 0x20 + +**问题**:W25Q64 的01Embedded 使用原始字符码计算字模地址,不减 0x20。 + +01Embedded `readSixteenAsciiData`: +```c +FonfAddr = ((u32)fontstr) * 16; // fontstr 是原始字符码,如 'F'=0x46 +``` + +错误实现: +```c +char_idx = ch[0] - 0x20; // 'F'(0x46) -> 38,但01Embedded 用 0x46=70 +``` + +**影响**:所有 ASCII 字符读取偏移 `0x20 * bpg` 字节,显示完全错误。 +- 'F'(0x46) -> 读位置 38 的字模 -> 显示 '&' +- 'W'(0x57) -> 读位置 55 的字模 -> 显示 '7' + +现已通过配置化的 `ascii_raw_code` 标志解决。 + +### 6.5 32pt GB2312 base 值计算错误 + +**问题**:32pt GB2312 的三个字库区块 base 值与01Embedded `func.h` 中的扇区编号不一致。 + +| 字体 | 01Embedded 正确值 | 旧代码错误值 | 偏差 | +|------|------------------|-------------|------| +| FS | 1189 | 1188 | -1 扇区 | +| KT | 1465 | 1463 | -2 扇区 | +| HT | 1741 | 1739 | -2 扇区 | + +**影响**:32pt 字号下仿宋/楷体/黑体显示中文时,地址偏移 1~2 个扇区(4096~8192 字节)。 + +### 6.6 MX25L256 base 值全为 0(占位符) + +**问题**:切换到 MX25L256 后,`g_font_regions_orig[]` 中所有 base/X/Y/Z 均为 0,导致所有字模地址从 Flash 起始位置读取,完全错乱。 + +**修复**:从 `STD-main-Orig` 的 `g_font_lib[]` 累加推算每个块的累计字节偏移,再拆分为 sector/page/byte 得到正确的 base/Y/Z 值。 + +### 6.7 MX25L256 GBK 索引公式错误 + +**问题**:MX25L256 字库按 GBK 190列序存储汉字,但代码统一用了 W25Q64 的 GB2312 94列序。两种列序的索引差异巨大(如"啊":94列=0,190列=576),导致汉字地址严重偏移。 + +**修复**:在 `font_chip_config_t` 中新增 `gbk_index_190` 标志,MX25L256 配置设为 `true`,`_flash_addr()` 中按标志选择 GBK 190列序公式。 + +--- + +## 7. 验证清单 + +### 7.1 W25Q64 验证(FONT_CHIP_W25Q64) + +```c +app_render_chip_select(FONT_CHIP_W25Q64); +``` + +- [ ] 启动界面 ASCII 正确: "FW:9K10212482" "MD:1000000969" +- [ ] 业务流程中文正确: "欢迎行驶" "高速公路" +- [ ] 16/24/32pt 各字号 ASCII + GB2312 正确 +- [ ] 宋/仿/楷/黑四种字型正确 +- [ ] 自适应字号选择正确(仅 32/24/16) + +### 7.2 MX25L256 验证(FONT_CHIP_MX25L256) + +```c +app_render_chip_select(FONT_CHIP_MX25L256); +``` + +- [ ] 14/16/20/24/32pt 各字号正确 +- [ ] ASCII 和 GB2312 正确 +- [ ] 自适应字号包含14和20 + +### 7.3 DIP2 自动选择验证 + +- [ ] DIP2 逻辑 0(OFF)→ 上电后自动选择 W25Q64,显示正确 +- [ ] DIP2 逻辑 1(ON)→ 上电后自动选择 MX25L256,显示正确 +- [ ] 运行中通过 `app_render_chip_select()` 手动切换后立即生效 +- [ ] 切换回另一芯片后恢复正确 + +### 7.4 边界验证 + +- ASCII 空格(0x20)和 `~`(0x7E) +- GB2312 "啊"(0xA1A1)和常用汉字 + +--- + +## 8. 参考文件 + +| 文件 | 用途 | +|------|------| +| `01Embedded/USER/FUNC/func.h` | W25Q64 字库区块扇区编号定义 | +| `01Embedded/USER/FUNC/func.c` | W25Q64 地址计算公式实现(6个 readXxxData) | +| `Application/Inc/app_render.h` | 渲染接口 + font_chip_config_t 定义 | +| `Application/Src/app_render.c` | 渲染引擎(已适配,已验证,支持多芯片切换) | +| `Application/Src/app_font.c` | 字库存储抽象层(无需修改) | +| `doc/02_LDI协议与外设接口/字库芯片更换与移植指导.md` | 本文档 | diff --git "a/doc/03_\351\207\215\345\272\206\351\253\230\351\200\237\344\272\214\344\273\243\350\264\271\346\230\276\345\215\217\350\256\256/PartA_STD\350\256\276\345\244\207\345\261\202\346\236\266\346\236\204\344\274\230\345\214\226.md" "b/doc/03_\351\207\215\345\272\206\351\253\230\351\200\237\344\272\214\344\273\243\350\264\271\346\230\276\345\215\217\350\256\256/PartA_STD\350\256\276\345\244\207\345\261\202\346\236\266\346\236\204\344\274\230\345\214\226.md" new file mode 100644 index 0000000..396f17c --- /dev/null +++ "b/doc/03_\351\207\215\345\272\206\351\253\230\351\200\237\344\272\214\344\273\243\350\264\271\346\230\276\345\215\217\350\256\256/PartA_STD\350\256\276\345\244\207\345\261\202\346\236\266\346\236\204\344\274\230\345\214\226.md" @@ -0,0 +1,323 @@ +# Part A:STD 设备层架构优化 + +**状态**:`[已废弃]` +**废弃原因(用户决策)**:跳过统一服务层(`dev_service_*`),CQ 协议模块按 +`doc/08_协议模块接入规则` 惯例**直调** STD 既有设备接口(`app_render` / +`dev_display` / `dev_io_ctrl` / `dev_rs232_voice` / `app_board_net_cfg` / +`pl_net`),不再新建 `dev_service.*` / `dev_voice.*` / `dev_io_lane_light_PIC.*` +中间层。本文保留存档,仅作历史设计推演记录,与已落地实现(Part B)无关。 + +--- + +## A.1 统一业务服务层(dev_service) + +### A.1.1 设计动机 + +当前设备层已有零散的底层抽象(`app_render` / `dev_io_lane_light` / `dev_display_set_brightness` / `pl_net_set_ip` 等),但存在两个问题: + +1. **协议层仍需直接调用多个分散接口**,并自行编排逻辑(如"全屏亮红→等待→复位"),同一套业务逻辑在 LDI 和 CQ 中重复实现 +2. **缺少协议无关的业务能力封装**,如 IP 搜索应答(25 字节帧 + CRC + 广播)、故障屏显示、IP 修改+落盘+复位等组合操作 + +**结论:在 @Device 层新增统一业务服务层,将"协议无关的输出基础功能"统一封装。** + +### A.1.2 分层架构 + +``` +┌─────────────────────────────────────────────────┐ +│ Protocol Layer (LDI / CQ / RLS / 未来新协议) │ +│ 只做:帧解析、命令分发、状态管理 │ +│ 调用:dev_service_*() 高层接口 │ +├─────────────────────────────────────────────────┤ +│ Service Layer (dev_service.c / dev_service.h) │ ← 新增 +│ 协议无关的业务能力封装 │ +│ 内部调用底层设备驱动 │ +├─────────────────────────────────────────────────┤ +│ Device Layer │ +│ app_render / dev_display / dev_io_ctrl / │ +│ dev_voice / pl_net / pl_system │ +└─────────────────────────────────────────────────┘ +``` + +### A.1.3 新增文件 + +``` +Application/Src/Device/ +└── dev_service.c # 业务服务层实现 + +Application/Inc/Device/ +└── dev_service.h # 业务服务层接口 +``` + +### A.1.4 对外接口清单 + +```c +/* ==================== 显示服务 ==================== */ + +/** 全屏填充颜色(协议无关) + * @param color 颜色索引:0=红, 1=绿, 2=黄, 3=灭/黑 + */ +void dev_service_fullscreen(uint8_t color); + +/** 显示故障屏:红色文字 "车道关闭" / "请择道行驶!" + 右下角红叉 */ +void dev_service_show_fault(void); + +/** 清屏 */ +void dev_service_clear_screen(void); + +/** 显示文字(指定行、字号、颜色、GBK文本) */ +void dev_service_show_text(uint8_t line, uint8_t font_size, + uint8_t color, const uint8_t *gbk_text); + +/** 显示通行灯图标(右下角 32x32 红叉或绿箭) */ +void dev_service_show_lane_icon(bool green); + +/* ==================== 通行灯服务 ==================== */ + +/** 设置通行灯 + * 编译期选择后端: + * - PROTO_CHONGQING: 调用 dev_io_lane_light_PIC()(显示器图片) + * - 其他协议: 调用 dev_io_lane_light()(GPIO 硬件) + * @param green true=绿灯/绿箭, false=红灯/红叉 + */ +void dev_service_lane_light(bool green); + +/** 黄闪报警控制 + * @param enable true=开启, false=关闭 + * @param seconds 持续时间(秒),0=直到手动关闭 + */ +void dev_service_yellow_flash(bool enable, uint16_t seconds); + +/* ==================== 亮度服务 ==================== */ + +/** 设置屏幕亮度 + * @param level 协议值 0~15 + * - level == 0:自动亮度模式(启用光敏传感器) + * - level 1~15:手动亮度,内部映射公式 `(level / 2) + 1`,上限 8 + */ +void dev_service_set_brightness(uint8_t level); + +/* ==================== 网络服务 ==================== */ + +/** 获取本机 IP/Mask/Gateway/Port 信息 */ +void dev_service_get_net_info(uint8_t *ip, uint8_t *mask, + uint8_t *gw, uint16_t *port); + +/** 设置本机 IP/Mask/Gateway/Port 并落盘 */ +void dev_service_set_net_info(const uint8_t *ip, const uint8_t *mask, + const uint8_t *gw, uint16_t port, + bool save_and_reset); + +/** IP 搜索应答:构造 25 字节帧 + CRC + 10011 广播 */ +void dev_service_send_search_response(void); + +/* ==================== 语音服务 ==================== */ + +/** TTS 合成播放(GBK 文本) */ +void dev_service_voice_play(const uint8_t *gbk_text, uint16_t len); + +/** TTS 设置音量(0~15) */ +void dev_service_voice_volume(uint8_t level); + +/* ==================== 系统服务 ==================== */ + +/** 系统复位(延时指定毫秒后复位) */ +void dev_service_reset(uint32_t delay_ms); +``` + +### A.1.5 服务层 vs 设备层的职责边界 + +| 层级 | 职责 | 示例 | +|------|------|------| +| **Service Layer** | 业务逻辑组合:帧构造 + 多步骤编排 | IP 搜索应答(查询 IP → 构造帧 → CRC → 广播)、故障屏(显示红字 + 红叉 + 亮红灯) | +| **Device Layer** | 单一硬件操作:一个接口做一件事 | `dev_io_lane_light(true)`、`dev_io_lane_light_PIC(true)`、`app_render_fill(RED)`、`crc16_xmodem(buf, len)` | + +**原则**:Service Layer 可以组合多个 Device Layer 调用,Device Layer 不感知业务含义。 + +### A.1.6 典型调用对比 + +**IP 搜索应答**: + +| 方式 | 协议层代码 | +|------|-----------| +| 旧(直调设备层) | `pl_net_get_ip(buf); buf[8]=0xA5; memcpy(buf+9,ip,4); crc=crc16_xmodem(buf+2,21); channel_send(CH_ID_UDP, buf, 25);` | +| 新(调服务层) | `dev_service_send_search_response();` | + +**全屏亮红 + 故障文字**: + +| 方式 | 协议层代码 | +|------|-----------| +| 旧 | `app_render_fill(RED); app_render_text(0, FONT_24, WHITE, "车道关闭"); app_render_text(1, FONT_16, RED, "请择道行驶!"); app_render_bitmap(BMP_RED_X, ...);` | +| 新 | `dev_service_show_fault();` | + +**IP 修改 + 落盘复位**: + +| 方式 | 协议层代码 | +|------|-----------| +| 旧 | `pl_net_set_ip(ip); pl_net_set_mask(mask); pl_net_set_gw(gw); dev_flash_iap_save(); pl_system_reset();` | +| 新 | `dev_service_set_net_info(ip, mask, gw, port, true);` | + +### A.1.7 所有协议共享的服务能力 + +| 服务接口 | 重庆 CQ | LDI | 未来新协议 | 说明 | +|----------|---------|-----|-----------|------| +| `dev_service_fullscreen` | full 命令 | 上电/故障 | 可用 | 全屏颜色 | +| `dev_service_show_fault` | 心跳超时 | 心跳超时 | 可用 | 故障屏 | +| `dev_service_show_text` | text1 命令 | 文字显示 | 可用 | 文字显示 | +| `dev_service_lane_light` | tra1 命令 | 通行灯 | 可用 | 编译期选后端 | +| `dev_service_yellow_flash` | warn1 命令 | 黄闪 | 可用 | 黄闪报警 | +| `dev_service_set_brightness` | light1 命令 | 亮度调节 | 可用 | 亮度 | +| `dev_service_send_search_response` | bin 搜索 | 搜索 | 可用 | IP 搜索应答 | +| `dev_service_set_net_info` | setip 命令 | 改 IP | 可用 | IP 修改+落盘+复位 | +| `dev_service_voice_play` | voice1 命令 | — | 可用 | TTS 播报 | +| `dev_service_voice_volume` | voice_control1 | — | 可用 | TTS 音量 | +| `dev_service_reset` | 重启应答 | — | 可用 | 系统复位 | + +--- + +## A.2 语音 TTS 通用封装(dev_voice) + +### A.2.1 背景 + +语音 TTS 通过 RS232 串口(`CH_ID_RS232`,USART3)与语音控制板通信。TTS 功能**下沉到 @Device 层**统一封装,使任何协议(LDI / CQ / 未来新协议)都可复用。 + +### A.2.2 新增文件 + +``` +Application/Src/Device/ +└── dev_voice.c # TTS 封装(帧组装 + 串口发送 + 组合索引查表) + +Application/Inc/Device/ +└── dev_voice.h # 对外接口 +``` + +### A.2.3 对外接口设计 + +```c +/** + * dev_voice.h - TTS 语音输出抽象层 + */ + +/* 直接文本模式:GBK 文本直接发送给 TTS 引擎合成 */ +void dev_voice_play(const uint8_t *gbk_text, uint16_t len); + +/* 固定索引模式:将已知短语映射为 4 位数字索引,支持 | 分隔组合 */ +void dev_voice_combination(const uint8_t *gbk_text, uint16_t len); + +/* 音量设置:level 0~15 映射到 TTS 音量标签 */ +void dev_voice_set_volume(uint8_t level); + +/* 静音/取消静音 */ +void dev_voice_mute(bool enable); +``` + +### A.2.4 TTS 帧格式(对齐裸机) + +**直接文本帧**(`dev_voice_play`): + +``` +FD | 00 | [Len+2] | 01 | 01 | GBK text bytes... +│ │ │ │ │ └─ 文本内容 +│ │ │ │ └─ 编码 01=GBK +│ │ │ └─ 命令码 01=合成播放 +│ │ └─ 数据区长度低字节(文本长度 + 2) +│ └─ 数据区长度高字节(0x00) +└─ 帧头 +``` + +**音量设置帧**(`dev_voice_set_volume`): + +``` +5B | 76 | 30 | [31~39] | 5D 即字符串 [v01] ~ [v09] +``` + +裸机实现采用两步映射:先计算 `voceGrade = (level / 2) + 1`(上限 7),再查表得到 TTS 帧字节。 +**注意:TTS 芯片音量标签 [v02] 和 [v07] 不可用,映射有跳档。** + +| level (协议层) | voceGrade | TTS 音量标签 | 音量帧第 3 字节 | +|---------------|-----------|-------------|----------------| +| 0 ~ 1 | 1 | `[v01]` | `0x31` | +| 2 ~ 3 | 2 | `[v03]` | `0x33` | +| 4 ~ 5 | 3 | `[v04]` | `0x34` | +| 6 ~ 7 | 4 | `[v05]` | `0x35` | +| 8 ~ 9 | 5 | `[v06]` | `0x36` | +| 10 ~ 11 | 6 | `[v08]` | `0x38` | +| 12 ~ 15 | 7 | `[v09]` | `0x39` | + +**实现时必须使用查表法,不可线性映射。** + +### A.2.5 固定索引组合模式(`voiceCombination`) + +裸机实现中,已知短语(地名、车型、费用词等)被映射为 4 位数字索引,多段用 `-` 分隔拼接后一次性发送。例如: + +- "永川" → `5007` +- "成渝高速" → `4001` +- "通行费" → `7016` +- "一型车" → `8001` + +**迁移策略**: +1. 将裸机 `voice.c` 中的索引映射表完整迁移到 `dev_voice.c` +2. `dev_voice_combination()` 内部实现文本 → 索引的查表逻辑 +3. 数字/费用的特殊处理("吨"、"元"、小数点)也一并迁入 + +--- + +## A.3 显示器通行灯驱动(dev_io_lane_light_PIC) + +### A.3.1 背景 + +重庆二代费显的通行灯实现与其他费显**完全不同**: + +| 类型 | 通行灯实现方式 | 驱动接口 | +|------|---------------|----------| +| LDI / 其他费显 | **IO 口驱动**通信灯控制板的 IO,控制车道灯板 | `dev_io_lane_light()` (GPIO PD14) | +| **重庆二代费显** | 在显示器**最后一行靠后位置**渲染 32x32 红叉/绿箭图片 | **`dev_io_lane_light_PIC()`** (display BITMAP) | + +### A.3.2 新增文件 + +``` +Application/Src/Device/ +└── dev_io_lane_light_PIC.c # 显示器图片式通行灯驱动 + +Application/Inc/Device/ +└── dev_io_lane_light_PIC.h # 对外接口 +``` + +### A.3.3 接口与实现 + +```c +/** + * dev_io_lane_light_PIC.h - 基于显示器图片的通行灯控制 + * 与 dev_io_lane_light()(GPIO 硬件)接口完全一致,可互换使用。 + */ + +/** 控制通行灯显示(显示器图片模式) + * @param green true=绿箭, false=红叉 + */ +void dev_io_lane_light_PIC(bool green); +``` + +```c +void dev_io_lane_light_PIC(bool green) +{ + if (green) + app_render_bitmap(BMP_GREEN_ARROW, LANE_PIC_POS); + else + app_render_bitmap(BMP_RED_X, LANE_PIC_POS); +} +``` + +- **不触碰任何 GPIO 硬件**,完全通过 `app_render` BITMAP 实现 +- 位置 `LANE_PIC_POS` 需对齐裸机实际渲染位置(显示器最后一行靠后) +- 位图资源 `BMP_GREEN_ARROW` / `BMP_RED_X` 需从裸机提取或重新制作 + +--- + +## A.4 Part A 验收标准 + +| 验收项 | 标准 | +|--------|------| +| `dev_service.c/h` 编译 | `make -j8` 通过(CQ 和 LDI 两种配置均通过) | +| `dev_voice.c/h` 编译 | 同上 | +| `dev_io_lane_light_PIC.c/h` 编译 | 同上 | +| LDI 不受影响 | LDI 固件编译和功能不变(新增文件,LDI 不调用即可) | +| 接口头文件无循环依赖 | `dev_service.h` 不依赖任何协议头文件 | diff --git "a/doc/03_\351\207\215\345\272\206\351\253\230\351\200\237\344\272\214\344\273\243\350\264\271\346\230\276\345\215\217\350\256\256/PartB_ProtocolParser_ChongQing\346\216\245\345\205\245.md" "b/doc/03_\351\207\215\345\272\206\351\253\230\351\200\237\344\272\214\344\273\243\350\264\271\346\230\276\345\215\217\350\256\256/PartB_ProtocolParser_ChongQing\346\216\245\345\205\245.md" new file mode 100644 index 0000000..2bab4e5 --- /dev/null +++ "b/doc/03_\351\207\215\345\272\206\351\253\230\351\200\237\344\272\214\344\273\243\350\264\271\346\230\276\345\215\217\350\256\256/PartB_ProtocolParser_ChongQing\346\216\245\345\205\245.md" @@ -0,0 +1,584 @@ +# Part B:ProtocolParser_ChongQing 接入 + +**状态**:`[已落地]`(2026-08-20;Makefile `PROTO=CQ` 构建 + 全协议 dev 构建双通道验证) +**协议版本**:重庆高速二代费显协议 2.0.1 +**架构决策**:Part A 服务层方案已废弃(用户决策),CQ 模块按 doc/08 惯例**直调** STD 设备接口。 + +> 已落地实现要点(与早期规划的差异,详见各节修订): +> - 无 `dev_service.*` / `dev_voice.*` 服务层,执行层直调 `app_render` / `dev_display` / +> `dev_io_ctrl` / `dev_rs232_voice` / `app_board_net_cfg` / `pl_net`; +> - 语音板走 USART6(`dev_rs232_voice` 旁路 TX),非 `CH_ID_RS232`(修正 Q13); +> - 文件划分为 3+3:`app_cq_proto.c`(注册/probe/双任务/cJSON 钩子)、 +> `app_cq_proto_parse.c`(JSON/二进制纯解析)、`app_cq_proto_cmd.c`(命令执行/位图/故障屏); +> **上电画面不实现**(2026-08-21 撤销 `app_cq_proto_default.c`,使用系统默认显示「欢迎行驶\n高速公路」); +> - Makefile `PROTO ?= ALL` / `PROTO=CQ`(剔除 LDI 目录 + `-DPROTO_CHONGQING`); +> - 队列深度 3、payload 1044、msg 1052、静态体 3156B(**置 CCMRAM**,见 doc/06-04); +> - **端口语义(2026-08-21 方案 B 定稿:TCP/UDP 端口彻底分离)**:Sector1.net_cfg +> 新增 `udp_port`(CQ UDP 业务口专有,setip 写入,默认 20103);业务口 +> `PROTO_CHONGQING` 构建读 `net_cfg.udp_port`(有效即用,无效/0 回退 20103); +> dev 共存构建**固定 20103、不读该字段**(dev 构建无写入方)。`net_cfg.port` +> 恒为 **TCP 业务口**(LDI/IAP 生态,出厂 9528),setip 不再写入该字段(历史 +> bug:setip 写 port 曾令 CQ 错绑 9528、20103 无响应,见 B.11);搜索应答端口 +> 同样改报 `app_udp_cq_get_port()`; +> - **10011 口 12B 二进制帧(dev 全协议构建,2026-08-21 二次实态修正)**:CQ 12B +> 帧 len=00 00 00 02 且 CRC16-XMODEM 恰好通过 LDI 校验——LDI probe 对 +> `data_len==2` 显式 FAKE 放行,CQ 12B 帧仍由 CQ probe 认领(修复前 LDI 沿 +> 身份校验路径 SKIP 吞掉 CQ 搜索/重启帧 → CQ 12B 在 10011 无响应; +> 原「CQ 帧 len 域全 0」表述与实态不符,见 B.9.9 / Q23 / B.11)。 + +--- + +## B.1 背景与定位 + +### B.1.1 现有协议栈 + +| 协议 | 目录 | 帧形态 | 默认通道/端口 | +|------|------|--------|---------------| +| IAP | `Application/Src/IAP/` | 二进制 `0x5A5A5A5A` | UDP/10011 | +| LDI | `Application/Src/LDI/` | 二进制 `0xFFFF` + CRC16-XMODEM | TCP + UDP/10011 | +| RLS | `Application/Src/RLS/` | 二进制 | (另绑) | +| AH_MQTT | `Application/Src/AH_MQTT/` | MQTT topic | (initcall 已注释) | +| **重庆(新建)** | `Application/Src/ProtocolParser_ChongQing/` | **JSON `{...}` + 固定 12B 二进制** | **UDP/20103(业务)+ UDP/10011(搜索)** | + +分发框架:`app_dispatch` 按 `proto_probe` 竞争识别帧,协议模块通过 `sw_app_initcall` 自注册。 + +### B.1.2 与 LDI 的产品关系(已确认) + +**编译期互斥**:重庆固件不含 LDI,LDI 固件不含重庆。编译切换通过 Makefile / `.eide` 的条件编译实现(如 `PROTO=CQ`)。 + +### B.1.3 与裸机工程的关系 + +移植策略:**行为对齐裸机联调结果**(含已知与协议文档的差异),API/分层对齐本工程。**通过 Part A 的 `dev_service_*` 调用所有设备能力**。 + +--- + +## B.2 目录与文件规划 + +与 LDI 同级;源/头分离。**文件名统一使用 `app_cq_proto_*` 样式**(已落地实态): + +``` +Application/Src/ProtocolParser_ChongQing/ +├── app_cq_proto.c # 模块 initcall、probe、处理任务、1s 定时任务、 +│ # cJSON 钩子初始化、PROTO_CHONGQING 网络配置应用 +├── app_cq_proto_parse.c # JSON/二进制帧纯解析 → cq_parsed_cmd_t +└── app_cq_proto_cmd.c # 命令执行(渲染/IO/语音/网络/32×32 位图/故障屏) + +Application/Inc/ProtocolParser_ChongQing/ +├── app_cq_proto.h # CQ_PAYLOAD_MAX=1044 / CQ_MSG_SIZE=1052 / +│ # CQ_QUEUE_DEPTH=3 / 命令枚举 / 12B 二进制帧常量 +├── app_cq_proto_parse.h # cq_parse_frame 声明 + cq_parsed_cmd_t +└── app_cq_proto_cmd.h # cq_execute_cmd / cq_render_fault_screen 声明 +``` + +命名规范: + +| 项 | 规则 | 示例 | +|----|------|------| +| 目录名 | `ProtocolParser_ChongQing` | — | +| 文件名 | `app_cq_proto_*.c` | `app_cq_proto_cmd.c` | +| 函数前缀 | `cq_proto_` | `cq_proto_cmd_text1()` | +| 全局变量 | `g_cq_proto_` | `g_cq_proto_ctx` | +| 头文件守卫 | `APP_CQ_PROTO_*_H` | `APP_CQ_PROTO_CMD_H` | +| 静态变量 | `s_cq_` | `s_cq_sync_counter` | + +### B.2.1 构建编入 + +| 文件 | 改动 | +|------|------| +| `Makefile` | `INC_DIRS` 增加 `Application/Inc/ProtocolParser_ChongQing/`;`SRC_APPLICATION` 条件追加 | +| `.eide/eide.yml` | 增加同组源文件(放在 CQ 条件组中) | +| ThirdParty | 引入 **cJSON**:`Middlewares/Third_Party/cJSON/` | + +**编译期切换方案(已落地实态)**: + +```makefile +PROTO ?= ALL # 默认全协议 dev 构建(-DSTD_ALL_PROTO) +ifeq ($(PROTO),CQ) + SRC_APPLICATION := $(filter-out Application/Src/LDI/%,$(SRC_APPLICATION)) + DEFINES += -DPROTO_CHONGQING # CQ 口径:TCP 通道不启动,网络应用由 app_net_boot 承担 +endif +``` + +- CQ 四个源文件**恒定编入** `SRC_APPLICATION`(位于 LDI 源之后:probe 注册序 = + 源码收录序,CQ 必须后于 LDI);cJSON.c 恒定编入第三方源列表。 +- `PROTO=CQ` 分支仅剔除 LDI 目录文件 + 追加 `-DPROTO_CHONGQING`;`{` 帧族 + (青海/山东/贵州/四川MTC)仍编入且保留 `-DSTD_ALL_PROTO` 豁免守卫。 +- 量产 EIDE 互斥配置:新增 `Application/protocol/ProtocolParser_ChongQing` 文件夹与 + incList 条目已入 `.eide/eide.yml`(Debug 目标未排除 CQ);LDI 互斥按既有 + excludeList 纪律操作(添加 `/Application/protocol/ldi` 排除项)。 + +--- + +## B.3 架构设计 + +### B.3.1 数据流 + +``` +UDP 收包 (业务口 20103 / 搜索口 10011) + → app_channel_dispatch + → ring_buffer (id=1, RB_GROUP_PROTO) + → frame_dispatch_task + → cq_proto_probe_frame + ├─ '{' → 花括号深度定界 → READY (JSON 帧) + └─ FF FF + 固定 12B 特征匹配 → READY (二进制帧) + → g_cq_queue(静态队列,置 CCMRAM) + → cq_proto_handle_task + ├─ JSON → cJSON_Parse(钩子=FreeRTOS 堆)→ "cmd" 查表 → g_cq_cmd_table[] + └─ BIN → cq_execute_cmd 内 _cq_exec_bin_reboot / _cq_exec_bin_search + → 执行层直调 STD 设备接口(app_render / dev_display / dev_io_ctrl / + dev_rs232_voice / app_board_net_cfg / pl_net / app_udp_broadcast) +``` + +### B.3.2 UDP 双端口模型(已确认) + +**业务端口可配置,默认 20103;搜索端口固定 10011。** + +``` +CH_ID_UDP (index=4) → 端口 10011 → 承载搜索应答(广播)+ IAP +CH_ID_UDP_CQ (index=7) → 端口 20103 → 承载 JSON 业务 + 重启请求 +``` + +具体改动: + +1. `app_dispatch.h`:`channel_id_t` 枚举新增 `CH_ID_UDP_CQ = 7`,`CH_ID_MAX = 8` +2. `app_udp.c`:新增 `udp_cq_task` / `udp_cq_connect_task`,绑定业务口 20103。 + 端口来源 `_udp_cq_read_port()`:`PROTO_CHONGQING` 读 Sector1 `net_cfg.udp_port` + (CQ UDP 业务口专有字段,方案 B 2026-08-21;有效 1~65535 即用,无效/0 回退 + 20103);dev 共存构建固定 20103(不读 `net_cfg.udp_port`——dev 构建该字段无 + 写入方,见 B.11 端口 bug 修复记录) +3. CQ 模块 init:`app_proto_bind_channel(s_cq_mask, CH_ID_UDP_CQ)` 绑业务口 +4. 搜索应答发送:通过 `CH_ID_UDP`(10011)广播 +5. 业务响应发送:通过 `CH_ID_UDP_CQ`(20103)单播回源 + +### B.3.3 心跳 + +``` +cq_proto_timer_task (1s) + → 若 app_factory_test 占用显示 → 跳过计数 + → s_cq_sync_counter++(跨任务读写,临界区保护,B.7.2) + → ≥120 → cq_render_fault_screen()(渲染后清零、置 s_cq_fault_shown) +帧结构合法的 JSON(合法命令 / 命令未知 ERR_CMD / 参数非法 ERR_PARAM)到达 + → 若 s_cq_fault_shown:先 app_default_display_show() 恢复默认画面、再执行本帧命令 + → s_cq_sync_counter = 0(宽松心跳复位,B.7.2) +仅 CQ_PARSE_ERR_FRAME 与 BIN 帧(重启/搜索)不复位 +``` + +--- + +## B.4 Probe 与帧定界 + +### B.4.1 JSON 帧 + +- 首字节 `{` +- probe 阶段做花括号深度扫描(`{` +1, `}` -1,跳过字符串内的 `}`) +- 深度归零 → `PROTO_PROBE_READY`;深度超过 16 或总长超过 `FRAME_DATA_MAX_LEN`(1044) → `PROTO_PROBE_FAKE` + +### B.4.2 二进制帧(精确匹配 12 字节) + +重启请求:`FF FF 00 00 00 00 00 02 07 31 5B C9` +重启应答:`FF FF 00 00 00 00 00 02 A7 31 F4 36` → 延时 100ms → `dev_service_reset(100)` + +搜索请求:`FF FF 00 00 00 00 00 02 05 00 48 73` +搜索应答:`dev_service_send_search_response()` + +CRC:多项式 `0x8408`,初值 `0xFFFF`,结果取反 —— 与 `crc16_xmodem()` 一致。 + +### B.4.3 混收处理 + +同 RB 内 JSON 与二进制帧可能交错。probe 按字节尝试: +1. `{` → JSON 路径 +2. `FF FF` + 后续 10 字节精确匹配 → BIN 路径 +3. 否则 `FAKE` 跳过 1 字节 + +--- + +## B.5 JSON 命令实现矩阵 + +### B.5.1 命令表 + +| cmd | 处理函数 | 行为要点 | 设备层映射(通过 dev_service) | 一期 | +|-----|----------|----------|-------------------------------|------| +| `text1` | `cq_proto_cmd_text1` | `scr==0` 清屏;`ln0`~`ln7` 的 `s/c/t`;字号 16/24/32 | `dev_service_show_text` / `dev_service_clear_screen` | **必做** | +| `tra1` | `cq_proto_cmd_tra1` | `turn`=r/g → 右下角 32x32 红叉/绿箭 | `dev_service_lane_light` → `dev_io_lane_light_PIC` | **必做** | +| `pic1` | `cq_proto_cmd_pic1` | 空实现(stub),返回成功 | — | **stub** | +| `light1` | `cq_proto_cmd_light1` | `level` 0~15 → 亮度映射 | `dev_service_set_brightness` | **必做** | +| `voice_control1` | `cq_proto_cmd_voice_vol` | `level` → TTS 音量标签 | `dev_service_voice_volume` → `dev_voice_set_volume` | **必做** | +| `voice1` | `cq_proto_cmd_voice` | `t` 文本;`|` 组合 | `dev_service_voice_play` → `dev_voice_play` | **必做** | +| `warn1` | `cq_proto_cmd_warn` | `second`:0 关 / -1 常开 / >0 倒计时;**注意:协议含 `level` 参数(音量),裸机未处理,本实现同样忽略** | `dev_service_yellow_flash` | **必做** | +| `syn1` | `cq_proto_cmd_syn` | 心跳,清计数器 | 无 | **必做** | +| `setip` | `cq_proto_cmd_setip` | `ip/mask/wg/port` 落盘复位 | `dev_service_set_net_info` | **必做** | +| `full` | `cq_proto_cmd_full` | `color` 0~3 满屏 | `dev_service_fullscreen` | **必做** | +| `screen` | 不做 | 单元板切换通过 @Device/Display 实现 | — | **不做** | +| `modify` | 不做 | 老格式,裸机未实现 | — | **不做** | + +帧结构合法的 JSON 成功解析后(含命令未知/参数非法两档宽松复位)→ +`s_cq_sync_counter = 0`(心跳复位,见 B.7.2);仅 `CQ_PARSE_ERR_FRAME` 与 +二进制帧(重启/搜索)不复位。 + +### B.5.2 JSON 解析架构 + +**cJSON + 静态命令分发表 + 类型安全提取宏**: + +```c +typedef void (*cq_proto_cmd_fn_t)(const cJSON *root, channel_t *ch); + +typedef struct { + const char *cmd_name; + cq_proto_cmd_fn_t handler; +} cq_proto_cmd_entry_t; + +static const cq_proto_cmd_entry_t g_cq_proto_cmd_table[] = { + { "text1", cq_proto_cmd_text1 }, + { "tra1", cq_proto_cmd_tra1 }, + { "pic1", cq_proto_cmd_pic1 }, + { "light1", cq_proto_cmd_light1 }, + { "voice_control1", cq_proto_cmd_voice_vol }, + { "voice1", cq_proto_cmd_voice }, + { "warn1", cq_proto_cmd_warn }, + { "syn1", cq_proto_cmd_syn }, + { "setip", cq_proto_cmd_setip }, + { "full", cq_proto_cmd_full }, +}; +``` + +类型安全提取宏: + +```c +#define CQ_JSON_GET_STR(root, key) \ + ({ cJSON *_item = cJSON_GetObjectItem(root, (key)); \ + (_item && cJSON_IsString(_item)) ? _item->valuestring : NULL; }) + +#define CQ_JSON_GET_INT(root, key, default_val) \ + ({ cJSON *_item = cJSON_GetObjectItem(root, (key)); \ + (_item && cJSON_IsNumber(_item)) ? _item->valueint : (default_val); }) + +#define CQ_JSON_GET_OBJ(root, key) \ + ({ cJSON *_item = cJSON_GetObjectItem(root, (key)); \ + (_item && cJSON_IsObject(_item)) ? _item : NULL; }) +``` + +--- + +## B.6 执行层(已落地:直调设备接口,无服务层) + +Part A 的 `dev_service.*` 服务层已废弃。命令执行层(`app_cq_proto_cmd.c`)直接 +调用 STD 既有设备接口(doc/08 惯例): + +```c +/* tra1:右下角 32×32 红叉/绿箭位图(先清区域再 RENDER_BITMAP) */ +app_render(&(render_cfg_t){ .type = RENDER_FILL, .x = x, .y = y, .w = 32, .h = 32, + .color = COLOR_BLACK }); +app_render(&(render_cfg_t){ .type = RENDER_BITMAP, .x = x, .y = y, .w = 32, .h = 32, + .color = green ? COLOR_GREEN : COLOR_RED, .bitmap = bmp }); + +/* full:0红/1绿/2黄/3白 全屏(3=白 → 红绿双基色映射红绿全亮即黄) */ +app_render(&(render_cfg_t){ .type = RENDER_FILL, .x = 0, .y = 0, .w = 0, .h = 0, + .color = color }); +``` + +| 协议语义 | 设备接口(直调) | +|----------|------------------| +| text1 行渲染 / full 全屏 / 故障屏 | `app_render` + `dev_display_commit_frame` | +| tra1 红叉/绿箭 | `app_render` RENDER_BITMAP(32×32 位图在 `app_cq_proto_cmd.c` 内,MSB-first 每行 4B) | +| light1 亮度 | 光敏任务挂起/恢复(`g_light_sensor_task_handle` + `osThreadSuspend/Resume`)+ `dev_display_set_brightness`(对齐贵州 '8' 命令方式) | +| voice1 / voice_control1 | `dev_rs232_voice_play` / `dev_rs232_voice_volume`(USART6 旁路 TX) | +| warn1 黄闪 | `dev_io_flash_light`(倒计时在 `cq_proto_timer_task` 每秒递减) | +| setip 落盘 | `app_board_net_cfg_update`(Sector1 net_cfg)→ `NVIC_SystemReset()` | +| 二进制重启应答 | `channel_send` 回源通道(UDP_CQ 单播) | +| 二进制搜索应答 | `app_udp_broadcast`(10011 搜索口广播 25B) | +| CQ 构建网络启动(PROTO_CHONGQING) | `app_net_boot_apply`(中立模块统一读 Sector1 → netif,空/损坏写 CQ 默认落盘,2026-08-21 解耦) | + +--- + +## B.7 配置与默认值 + +| 项 | 默认值 | 落点 | +|----|--------|------| +| IP | 192.168.1.5 | Sector1 net_cfg(`app_board_net_cfg`) | +| Mask | 255.255.255.0 | 同上 | +| Gateway | **192.168.1.1** | 同上 | +| 业务端口 | **20103** | `PROTO_CHONGQING`:Sector1 net_cfg.udp_port(可通过 `setip` 修改,方案 B 2026-08-21);dev 共存构建:固定 20103,不读 net_cfg.udp_port(dev 构建无写入方) | +| 搜索端口 | **10011** | 固定,`CH_ID_UDP` | + +配置存储:复用 `app_board_net_cfg` 的 net_cfg 字段(Sector1)。网络配置应用自 +2026-08-21 起由中立模块 `app_net_boot`(`app_boot.c` init_task 调 +`app_net_boot_apply`)统一承担,LDI `ldi_ctx_init` 与 CQ `cq_proto_init` 均不再 +直接改 netif:读 Sector1 有效 → `pl_net_set_ip` + `app_tcp_server_set_port` 应用 +(**TCP Server 口两口径统一应用 net_cfg.port,2026-08-24 修订**——TCP Server/ +Client 通道两口径均启动,见下);空/损坏/非法 → 写回本构建默认记录并应用 +(PROTO_CHONGQING 口径 192.168.1.5/20103;dev 口径 192.168.114.200/9528)。 + +重启二进制:**软复位**(`NVIC_SystemReset`,应答后延时 100ms),**禁止照搬裸机 +Sector11 写法**(STD 无该机制)。 + +### B.7.1 setip 双构建生效语义与测试方法(2026-08-21 定稿,2026-08-21 方案 B 修订) + +setip 命令链(静态审计 2026-08-21 无断点):`_cq_parse_frame`(parse 层校验 +ip/mask/wg 点分字符串 + port JSON 数字 1~65535,非法帧按「JSON 无应答帧」约定 +静默丢弃)→ `_cq_exec_setip` → `app_board_net_cfg_update(ip,mask,gw,port,udp_port)` +落盘 Sector1(同步 HAL 轮询擦写,同值跳过,升级中间态仅更新 net_cfg 不拒绝)→ +`NVIC_SystemReset()` 重启生效。**方案 B(2026-08-21,TCP/UDP 端口彻底分离)**: +Sector1.net_cfg 新增 `udp_port` 字段(CQ UDP 业务口专有),setip 命令端口写入 +`udp_port`、TCP 业务口 `port` **保留现值**(get 失败用出厂默认 9528)——setip +不再污染 TCP 口。**port 字段必须是 JSON 数字**:协议 2.0.1 原文 +`{"cmd":"setip",...,"port":20103}`、裸机 `cmd_ip_ctrl`(`tempjson->valueint`)、 +官方测试软件(`","port":` 无引号)三者均为数字,port 发成字符串 `"20103"` 会被 +parse 拒绝(与裸机行为一致,不兼容字符串)。 + +| 构建 | setip 后重启生效语义 | 验证方法 | +|------|---------------------|----------| +| dev 共存(EIDE Debug / `make` 默认,LDI 编入) | **IP 生效**:`app_net_boot_apply` 读 Sector1 net_cfg(magic+CRC 有效、IP 非 0、port 1~65535)→ `pl_net_set_ip` + `app_tcp_server_set_port`(`ldi_ctx_init` 分支 1 只负责装载 cfg 与 W25 自愈,2026-08-21 起不再应用 netif;其分支判定中 `update_sta==updated` 不参与——升级中间态记录同样采纳 Sector1.net_cfg,防 W25 陈旧镜像回写覆盖 setip 刚写入的 net_cfg);**端口均不生效**——setip 端口写 Sector1 `udp_port` 但 dev 构建 CQ 业务口固定 20103(`_udp_cq_read_port` `#else` 分支,防历史污染回归,勿改);**TCP 口(port)不被 setip 改动**(保留现值),LDI 12H/1DH 仍报 TCP 口 | 改 IP 后重启,用新 IP 向 **20103** 发 text1 验证;端口修改须在 PROTO_CHONGQING 构建验证 | +| CQ 量产(`make PROTO=CQ` / EIDE 配置 `PROTO_CHONGQING` 目标,LDI 剔除) | **IP + UDP 端口均生效**:重启后 `app_net_boot_apply` 读 Sector1 → `pl_net_set_ip` + `app_tcp_server_set_port(net_cfg.port)`(**TCP 口两口径统一应用,2026-08-24 修订**——TCP Server 通道仍启动、绑定 net_cfg.port;CQ setip 只写 udp_port、port 保留现值,故 TCP 口不受 setip 影响);`udp_cq_task` 每轮重建前 `_udp_cq_read_port` 读 Sector1 net_cfg.udp_port(有效 1~65535 即用,无效回退 20103) | 改 IP+端口后重启,用新 IP 向新端口发 text1 / 12B 搜索帧验证;搜索应答 port 字段报 `app_udp_cq_get_port()` | + +> **不变量(2026-08-21 方案 B 更新,2026-08-24 端口应用两口径化修订)**:网络配置应用自 2026-08-21 起由中立模块 +> `app_net_boot` 统一兜底(两口径都编入、均在 `app_board_net_cfg_fw_version_update` +> 之后执行)——「排除 LDI 而不加宏 = 无人应用网络配置」的中间态不再致命:空/损坏 +> 扇区由 `app_net_boot` 写本构建默认记录落盘并应用,设备不会停留在编译期默认 IP。 +> 但 **PROTO_CHONGQING 仍决定默认口径与 UDP 端口应用对象**:不定义该宏即按 dev 口径 +> (192.168.114.200,TCP 口 9528),定义该宏才按 CQ 口径 +> (192.168.1.5,UDP 业务口读 Sector1 udp_port)——**TCP 口(net_cfg.port)应用 +> 两口径统一(2026-08-24)**:TCP Server 通道两口径均启动(app_boot.c 无口径 +> 裁剪),均绑定 Sector1 net_cfg.port,与 LDI 12H / IAP 0x01 上报恒一致。排除 LDI 与 +> 定义 `PROTO_CHONGQING` 仍需成对出现,否则口径错配(2026-08-24 实测: +> EIDE Debug 曾为「LDI 编入 + PROTO_CHONGQING」混合口径,修复前 TCP 口恒 9528 +> 与搜索上报矛盾——代码已按「端口应用不随宏裁剪」兜底,混合口径不再导致 +> TCP 口错配,但默认 IP 口径仍由宏决定,成对纪律保留)。两口径默认记录均含 +> port=9528 / udp_port=20103(方案 B:TCP/UDP 双字段落盘完整记录)。EIDE Debug +> 目标口径(excludeList 与 defineList 是否成对)须在改动前核对: +> excludeList 含 `Application/protocol/ldi` 时 defineList 须含 `PROTO_CHONGQING` +> (CQ 量产口径);反之不定义(dev 共存口径)。 + +边界语义(已审计):写盘完成到复位之间无需延时(内部 Flash 擦写为 HAL 轮询同步, +同裸机 30ms 延时仅为 W25 SPI 写);`_cq_exec_setip` 忽略落盘返回值、无条件复位 +(写失败时重启后回旧配置,与裸机无条件复位行为一致);IWDG(256 预分频/4095 +重载 ≈32.8s 窗口)远大于扇区擦除 1~2s 停顿,无复位竞争风险。升级中间态 +(`update_sta != updated`)记录:net_cfg 仍写入但重启后 Bootloader 条件 D 判中间 +态进 Recovery(任何重启均如此,非 setip 特有);主固件 `ldi_ctx_init` 侧 2026-08-21 +起对中间态记录同样采纳 Sector1.net_cfg(分支 1 条件不再要求 `update_sta==updated`), +不再被 W25 陈旧镜像回写覆盖。 + +**setip 链路 RTT 观察点(2026-08-21 撤销)**:本会话调试期曾补 +`[cq] rx JSON/BIN`、`[cq] setip parsed/exec/update sector1 ret/reset now`、 +`[netcfg] ldi_ctx_init/branch1/2/3/branch2 write-back/boot-apply`、 +`[cq] udp_cq port` 等 RTT 标签,2026-08-21 已按「代码-文档同步」全部删除 +(setip 排障改用静态审计 + 双构建验证,见 B.7.1 验证方法列)。仅 +`app_board_net_cfg.c` 的 `[fwver]` 日志保留(doc/CLAUDE.md 0x03 判断路径依赖)。 + +### B.7.2 心跳宽松语义 / 故障屏恢复 / 临界区(2026-08-21 修订) + +**宽松复位语义**(`cq_proto_handle_task`):帧结构合法的 JSON 即视为上位机 +存活——`CQ_PARSE_OK` 执行后复位;`CQ_PARSE_ERR_CMD`(命令未知)与 +`CQ_PARSE_ERR_PARAM`(参数非法)不执行但**亦复位**(只不计入超时); +仅 `CQ_PARSE_ERR_FRAME`(非 `{` / 非 12B 二进制 / 截断 / JSON 语法错)静默 +丢弃且继续计数。BIN 帧(重启/搜索)`sta == CQ_PARSE_OK` 但 `cmd` 为 +`CQ_PCMD_BIN_REBOOT/BIN_SEARCH`——**保持不复位**(重启帧自身复位设备,搜索帧 +非业务心跳)。 + +**故障屏恢复默认画面**:`cq_proto_timer_task` 渲染故障屏时置 +`s_cq_fault_shown = true`。`cq_proto_handle_task` 在「帧到达且被判定为上位机 +存活信息」的两个 `cq_proto_sync_reset()` 调用点之前检查该标志:置位则先 +`app_default_display_show()` 恢复默认画面(有注册回调调回调;无注册回调 +清屏后渲染系统默认欢迎画面「欢迎行驶\n高速公路」——2026-08-21 重庆已取消 +专属上电画面,恢复落到系统默认显示)再清标志。执行顺序 +**先恢复、再执行本帧命令**——text1/full 等随后覆盖渲染,同帧同步提交 +(`app_render` 内 commit)不闪烁。 + +**共享计数器临界区**:`s_cq_sync_counter` / `s_cq_warn_seconds` / +`s_cq_fault_shown` 跨 timer_task/handle_task 共享。M4 对齐 u32 单指令原子,但 +读写-判断序列用 `taskENTER_CRITICAL()/taskEXIT_CRITICAL()` 保护:timer_task 的 +递增(心跳)/递减(warn 倒计时)与 `cq_proto_sync_reset()` / +`cq_proto_warn_set()` 的写入在临界区内;读取侧阈值判断在临界区外(值本身 +volatile)。标志位 `s_cq_fault_shown` 仅单指令置/清,不加临界区(最坏重复 +渲染一次默认画面,无害)。 + +**心跳超时故障屏编译开关(2026-08-21 新增;2026-08-24 改名单层宏)**:宏 +`CQ_FAULT_SCREEN`(1 开 / 0 关,`app_cq_proto.h`)控制 `cq_proto_timer_task` 的 +「心跳计数 + 120s 阈值判定 + `cq_render_fault_screen`」段——**默认跟随构建 +口径**:`PROTO_CHONGQING`(重庆量产)开启;共存/其他构建(通用版多协议固件) +**关闭**(他省上位机不发重庆 syn1 心跳,120s 超时故障屏会误触发)。可显式 +覆盖:`-DCQ_FAULT_SCREEN=1` 强制开 / `=0` 强制关(Makefile 命令行 `DEFINES` / +EIDE defineList)。关闭时心跳不计数、不渲染(`s_cq_sync_counter` 恒 0、 +`s_cq_fault_shown` 恒 false,`cq_proto_sync_reset()` 清零无副作用); +**warn1 倒计时不受开关影响**(保持现有行为)。旧名 `CQ_FAULT_SCREEN_ENABLED` +/ `CQ_FAULT_SCREEN_FORCE_ON` 已废弃(不保留兼容别名),开关全集见 +`doc/构建开关总表.md`。 + +--- + +## B.8 已确认问题汇总 + +| 问题 | 确认结果(已落地修订) | +|------|----------| +| Q1. 与 LDI 的产品关系 | **编译期互斥**:`make PROTO=CQ` 剔除 LDI 目录;EIDE 按 excludeList 目录排除纪律互斥 | +| Q2. UDP 端口 | **双端口**:业务 20103(可配置,`CH_ID_UDP_CQ`)+ 搜索 10011(`CH_ID_UDP`) | +| Q3. 语音 | **USART6 语音板 TTS**:`dev_rs232_voice_play/volume` 旁路 TX(**修正早期 CH_ID_RS232 结论**) | +| Q4. pic1 / voice_play1 | **空实现**(stub,解析后忽略) | +| Q5. screen | **仅记录 led 到 static 变量**(`cq_screen_led_get` 可查);不写 Flash、不切模组(本次不含模组排布) | +| Q6. 通行灯 | **显示器图片模式**:tra1 右下角 32×32 红叉/绿箭位图(`app_render` RENDER_BITMAP) | +| Q7. setip / 重启配置 | `app_board_net_cfg_update` 写 Sector1 net_cfg → `NVIC_SystemReset()` 重启生效(对齐 LDI 0AH 落盘语义) | +| Q8. 默认网关 | **192.168.1.1** | +| Q9. 字库芯片 | **显示层实现**(`app_render`,FONT_ENC_GBK 直通渲染) | +| Q10. JSON 库 | **cJSON**(`Middlewares/Third_Party/cJSON`,钩子换绑 pvPortMalloc/pvPortFree) | +| Q11. 上电欢迎语 | **不在协议层实现**(2026-08-21 撤销 `app_cq_proto_default.c`):使用系统默认显示「欢迎行驶\n高速公路」(`app_default_display` 无注册回调兜底) | +| Q12. 文件名 | **`app_cq_proto_*` 样式** | +| Q13. TTS 串口 | **`dev_rs232_voice`(USART6)**,禁止协议 bind `CH_ID_RS232_1` | +| Q14. 业务端口可配性 | **支持且仅 CQ 构建可配**:`PROTO_CHONGQING` 下 `setip` port 落盘 Sector1 `udp_port`、UDP_CQ 通道每轮重建重读;dev 共存构建固定 20103(2026-08-21 方案 B:`net_cfg.udp_port` 为 CQ UDP 口专有字段、`net_cfg.port` 为 TCP 口,两端口彻底分离,见 Q24/B.11) | +| Q15. 搜索应答端口 | **从 10011 广播**(`app_udp_broadcast`,25B 应答含 CRC16-XMODEM);应答内 **port 字段上报 CQ 实际业务口 `app_udp_cq_get_port()`**(2026-08-21 修订:原直读 Sector1 net_cfg.port,dev 构建会误报 LDI 9528) | +| Q16. warn1 -1 | **常开不自动停**(映射 0xFFFF 语义;协议文档 level 参数未实现,注释说明) | +| Q17. 工厂测试与心跳 | **工厂测试期间暂停心跳计数**(新增 `app_factory_test_active()` 查询接口) | +| Q18. RS485 串口转发 | **不迁移**(裸机 `needTxToSerial` 级联功能,STD 未使用) | +| Q19. screen 命令 | **仅记录不生效**。裸机 `cmd_BoardSet` 写 Flash 切模组的语义不迁移(STD 模组适配在 dev_display_1_2xx 中实现) | +| Q20. text1 行号限制 | **ln4 仅字号 ≤24,ln5~7 仅 ≤16**,超限跳过该行(执行层判定);行超出屏高跳过 | +| Q21. full 颜色=3 | 协议文档说"白色",红绿双基色屏无法物理显示纯白,**映射红绿全亮即黄** | +| Q22. 队列/内存(新增) | 深度 3 / payload 1044 / msg 1052 / 静态 3156B + 任务帧缓冲 1052B + JSON/文本缓冲 1045+1044B,**全部置 CCMRAM**(SRAM 全协议构建仅余 ~1.9KB;CPU 独占访问无 DMA,安全) | +| Q23. dev 构建 10011 二进制帧(2026-08-21 二次实态修正) | 全协议 dev 构建下 10011 口 12B 二进制帧**先经 LDI probe 探测**:CQ 12B 帧 len=00 00 00 02 且 CRC16-XMODEM 恰好通过 LDI 校验——**LDI probe 对 `data_len==2` 显式 FAKE 放行,CQ probe 认领该帧**(「LDI 先探测、FAKE 放行、CQ 收单」)。修复前 LDI 沿身份校验路径 SKIP 吞掉 CQ 搜索/重启帧(cfg_valid 下 data_len<18 → SKIP),CQ 12B 在 10011 无响应;原「CQ 帧 len 域全 0」表述与实态不符,已修订 app_cq_proto.c 头注释与 doc/05-01 §6 | +| Q24. 业务端口 bug(新增,2026-08-21 修复) | dev 共存构建(LDI 编入)下 `_udp_cq_read_port()` 无条件读 Sector1 `net_cfg.port`,而 `ldi_ctx_init` 空/损坏扇区写回 LDI 出厂记录 port=9528 → CQ 业务口错绑 9528(UDP 20103 无响应、9528 反被 CQ 认领)。修复:`#ifdef PROTO_CHONGQING` 才读 Sector1(无效/0 回退 20103),`#else` 固定 20103;搜索应答 port 字段改报 `app_udp_cq_get_port()`。**2026-08-21 方案 B 二次修复(根除)**:Sector1.net_cfg 新增 `udp_port` 字段与 TCP 口彻底分离——CQ 构建改读 `udp_port`(setip 写入),`port` 恒为 TCP 业务口(LDI/IAP 生态)不再被 setip 污染。LDI 残留记录靠「擦 Sector1 出厂化」纪律清理(flash_all.sh 默认烧后即擦)。详见 B.11 | + +--- + +## B.9 风险与约束 + +1. **JSON 粘包/半包**:UDP 通常一包一帧,但仍按流式 probe 写(花括号深度扫描)。 +2. **花括号定界**:文本字段含 `}` 时同裸机脆弱;GBK 尾字节 0x5C(`\`)落入 JSON 字符串会被 cJSON 当转义符——已记录为已知限制。 +3. **亮度档位**:协议 0~15 vs 硬件 8 级,映射 `level/2+1`(上限 8)。 +4. **内存**:cJSON 树为 FreeRTOS 堆瞬时分配(钩子 pvPortMalloc/pvPortFree),帧长 ≤ 1044;队列/缓冲静态体置 CCMRAM(见 doc/06-04)。 +5. **编码**:text1/voice1 的 t 字段为 GB2312 字节直通(FONT_ENC_GBK 渲染 / 语音板 GBK 直送),不转码。 +6. **TTS 串口波特率**:需与语音控制板硬件规格对齐。 +7. **text1 行号限制**:ln4 仅支持字号 ≤24,ln5~7 仅支持字号 ≤16;执行层判定超限跳过该行。 +8. **full 颜色=3**:协议文档说"白色",红绿双基色屏映射红绿全亮即黄。 +9. **dev 全协议构建 10011 口 12B 二进制帧**:LDI probe 先探测、对 len 域全 0 的帧 + FAKE 放行,CQ probe 认领(「LDI 先探测、FAKE 放行、CQ 收单」)——无功能受限, + 且不构成永久阻塞(LDI FAKE 不消费字节、不置 any_wait)(见 Q23 实态修正)。 +10. **dev 共存构建端口语义**:业务口固定 20103;板上若残留 LDI 出厂记录 + (Sector1 port=9528),dev 构建的 LDI 自身仍正常使用 9528,CQ 不受影响 + (修复后不再读该字段);量产前按纪律擦 Sector1 出厂化。 + +--- + +## B.10 实施阶段与验收 + +### 阶段 1 — 骨架 + +- 目录 + cJSON 引入 + Makefile 条件编译 +- `app_cq_proto.c`:initcall、probe、handle_task、timer_task +- 双 UDP 通道(`CH_ID_UDP_CQ` + `CH_ID_UDP`) +- 空命令表可编译 + +**验收**:`make PROTO=CQ -j8` 通过。 + +### 阶段 2 — 显示类 cmd + 心跳故障 + +- `text1` / `tra1` / `full` / `light1` / `syn1` +- 心跳 120s 故障屏 +- `warn1` 黄闪倒计时 + +**验收**:上位机按协议发包,屏显与裸机对照。 + +### 阶段 3 — 网络类 + 语音 + +- `setip` 落盘复位 +- bin 重启应答 + 复位 +- bin IP 搜索广播应答 +- `voice1` / `voice_control1` + +**验收**:搜索工具能发现;改 IP 重启后生效。 + +### 阶段 4 — 回归与收尾 + +- `pic1` stub / `screen` 忽略 +- 与 IAP(10011)互不误伤 +- 亮度 / 光敏 / 工厂测试互斥 +- 文档归档 + +--- + +## B.11 端口语义修复与深度审计记录(2026-08-21) + +### B.11.1 业务端口 bug(已修复) + +**症状(用户实测)**:设备对 UDP 20103 无响应,对 UDP 9528 却能处理 JSON;协议要求业务口 20103。 + +**根因链**: +1. `Application/Src/Channel/app_udp.c` `_udp_cq_read_port()` 无条件读 Sector1 + `net_cfg.port`(有效即用,无效/0 回退 20103),`udp_cq_task` 每轮重建前重读; +2. dev 共存构建(EIDE Debug,LDI 编入)下 `ldi_ctx_init`(`Application/Src/LDI/app_ldi.c` + :170 附近)在 Sector1 空/损坏时写回 LDI 出厂记录,`net_cfg.port` = 9528 + (`LDI_DEFAULT_CONFIG_PORT`,LDI TCP 配置口语义); +3. CQ 通道因此绑到 9528:UDP 9528 被 CQ probe 认领处理,20103 无 netconn。 + +**修复**(`app_udp.c` `_udp_cq_read_port`,:283~:301;2026-08-21 方案 B 二次修订): +- `#ifdef PROTO_CHONGQING`:读 Sector1 `net_cfg.udp_port`(方案 B 新增 CQ UDP 业务口 + 专有字段,CQ setip 写入;空扇区由 `app_net_boot` 写 20103 默认——2026-08-21 起 + 替代 `_cq_net_cfg_apply`),有效(1~65535)即用,无效/0 回退 20103; +- `#else`(dev 共存构建):固定 `UDP_CQ_DEFAULT_PORT`(20103),不读 `net_cfg.udp_port`。 + +**连带修复**(`app_cq_proto_cmd.c` `_cq_exec_bin_search`,:438 附近):25B 搜索应答的 +port 字段原直读 Sector1 `net_cfg.port`(dev 构建会误报 LDI 9528),改报 +`app_udp_cq_get_port()`(CQ 实际业务口;PROTO_CHONGQING 下两者同源)。 + +**验证**:`make clean && make -j8` 与 `make clean && make PROTO=CQ -j8` 双构建 0 error; +PROTO=CQ 构建 `nm` 无 `ldi_` 符号(0 个);PROTO=ALL 反汇编 `_udp_cq_read_port` 为 +`movw r2,#20103; strh`(常量写入,无 `app_board_net_cfg_get` 调用)。 + +**LDI 残留记录纪律**:板上历史 LDI 记录(Sector1 port=9528)靠「擦 Sector1 出厂化」 +清理——`tool/flash_all.sh` 默认烧后即擦 Sector1;dev 构建修复后 CQ 不再受其影响。 + +### B.11.2 深度审计结论(逐项) + +| # | 检查项 | 位置 | 结论 | +|---|--------|------|------| +| 1 | probe PURE 契约 | `app_cq_proto.c` `cq_probe_frame` :159 | OK:只 rb_peek;avail==0 返 FAKE;仅 READY 写 total_len;WAIT/FAKE 零副作用 | +| 2 | 花括号深度扫描 | 同上 :174 | OK:深度>16 / scanned>1044 / 孤立 '}' → FAKE;64B 分块 peek 不放大栈。**已知限制(已补注 probe 头注释)**:GB2312 低字节域 0x40~0xFE 含 `{`(0x7B)/`}`(0x7D)/`\`(0x5C),串内伪花括号会破坏深度定界;JSON 转义 `\"` 未按转义处理——与裸机同脆弱(B.9.2),接受 | +| 3 | 12B 二进制精确匹配 | 同上 :206 | OK:avail<12 → WAIT;memcmp 精确匹配两帧;不匹配 FAKE | +| 4 | 多帧粘包续取 | `app_dispatch.c` `frame_dispatch_task` | OK:READY 后 any_parsed → 内循环继续探下一帧;64 帧上限 + 1B 重同步防死循环 | +| 5 | LDI/CQ probe 顺序 | Makefile SRC_APPLICATION + `Compiler/STM32F407XX_FLASH.ld` | 确认:LDI 源在 CQ 之前,initcall 同层按链接序执行(SORT 对同名 section 为 no-op)→ LDI probe 先于 CQ;CQ 12B 帧 len=2 且 CRC 恰好通过 LDI 校验,2026-08-21 修复:LDI probe 对 `data_len==2` 显式 FAKE → CQ 12B 帧由 CQ 认领(**二次实态修正 Q23**) | +| 6 | 通道断链生命周期 | `app_udp.c` `udp_cq_connect_task` | OK(同 10011 通道既有模式):deinit 置 `me.ops=nullptr` + `conn=NULL`(2026-08-21 补,广播复用路径判空依据)+ 注销;`channel_send` 双守卫(ops 空 / get 回验)拒发;`s_udp_cq_ch.conn` 不复用旧值(init 重拷模板)。**残余风险**:同优先级任务间 send 与 deinit/netconn_delete 的并发窗口(与 10011 通道同源,非本次引入) | +| 7 | disconnect_sem 逻辑 | `app_udp.c` `udp_cq_task` | OK:信号量 1 槽;bind 成功后先排空再等;link_listener 断链注入 | +| 8 | app_udp_cq_broadcast | `app_udp.c` | OK:当前无调用点(搜索应答走 10011 的 app_udp_broadcast)。2026-08-21 重构:优先复用常驻通道 conn(`udp_cq_task` 创建时已设 SOF_BROADCAST,`app_channel_get` 回验 + conn 非空),常驻未就绪回退临时 conn(netconn_new 失败即返回);修复池满时临时 conn 必然 NULL 的静默丢包 | +| 9 | 端口读取时序 | `app_udp.c` `udp_cq_task` :307 | OK:每轮循环 bind 前 `_udp_cq_read_port()`,setip 重启后生效 | +| 10 | cJSON 树释放 | `app_cq_proto_parse.c` `cq_parse_frame` | OK:root==NULL / cmd 缺失 / 无效 cmd / default / 正常路径全部 cJSON_Delete,无泄漏路径 | +| 11 | s_cq_text_buf 单消费者 | `app_cq_proto_parse.c` :38 | OK:仅 `cq_proto_handle_task` 调 `cq_parse_frame`;text1 总量超限 `_cq_text_dup` 返 NULL → ERR_PARAM | +| 12 | _cq_parse_ip4 边界 | 同上 :77 | OK:每段 0~255 累加溢出即拒、恰好 4 段、结尾 '\0';port 1~65535 | +| 13 | warn1 second 语义 | 同上 :283 / `app_cq_proto.c` `cq_proto_warn_set` | OK:-1 常开 / 0 关 / >0 倒计时;<-1 ERR_PARAM | +| 14 | voice1 长度截断 | `app_cq_proto_cmd.c` `_cq_exec_voice1` | OK:执行层按 DEV_RS232_VOICE_MAX_TEXT(200) 截断;解析层 1044 上限内 | +| 15 | raw_len > CQ_PAYLOAD_MAX | `app_cq_proto_parse.c` :205 | OK:拒绝(dispatch 层亦钳制 frame_len ≤ FRAME_DATA_MAX_LEN=1044) | +| 16 | text1 行渲染 | `app_cq_proto_cmd.c` `_cq_exec_text1` | OK:y=i×字号、`y+font > screen_cols`(屏高)跳过;ln4≤24/ln5~7≤16;像素宽按 GBK=字号/ASCII=字号/2 截断到 screen_rows(屏宽);半 GBK 尾字节不渲染 | +| 17 | tra1 位图坐标 | `_cq_exec_tra1` | OK:`screen_rows<32 || screen_cols<32` 提前 return,坐标为 (rows-32, cols-32) 不为负 | +| 18 | full color 3 | `_cq_exec_full` | OK:0红/1绿/2黄/3白→红绿全亮即黄(双基色屏物理限制,注释在案) | +| 19 | light1 光敏挂起/恢复 | `_cq_exec_light1` | OK:与贵州 '8' 同模式(osThreadSuspend/Resume + dev_display_set_brightness);level/2+1 上限 8 = DEV_DISPLAY_BRIGHTNESS_MAX | +| 20 | setip 落盘复位 | `_cq_exec_setip` | OK:app_board_net_cfg_update → NVIC_SystemReset;JSON 无应答约定,无需延时(落盘为同步完成) | +| 21 | 搜索应答 25B 帧 | `_cq_exec_bin_search` | OK:buf[23..24]=crc16_xmodem(&buf[2],21) 覆盖 [2..22];头 9B 固定 0x0F 0xA5;**port 字段已改为 app_udp_cq_get_port()**(本次修复) | +| 22 | 重启应答回源 | `_cq_exec_bin_reboot` | OK:channel_send(msg->ch) 回源通道;ops 空守卫下应答可能丢失但复位照常(可接受) | +| 23 | s_cq_screen_led 不落盘 | `_cq_exec_screen` | OK:仅记录(Q5/Q19 定案:不写 Flash 不切模组) | +| 24 | 120s 故障屏 | `cq_render_fault_screen` | OK:渲染后 s_cq_sync_counter=0 不重复刷屏;小屏防御(cols<32/<96 跳过行) | +| 25 | 工厂测试 vs warn1 | `cq_proto_timer_task` | OK(已注释):warn1 倒计时在工厂测试期间**不暂停**(裸机 hsCnt 同 ISR 口径);心跳计数暂停(Q17)。裸机源码不在本仓库,逐行核对缺口已记入注释 | +| 26 | cq_proto_sync_reset 调用点 | `app_cq_proto.c` `cq_proto_handle_task` | OK(2026-08-21 宽松语义 B.7.2):帧结构合法的 JSON(OK / ERR_CMD / ERR_PARAM)均复位;仅 ERR_FRAME 不复位;二进制帧(重启/搜索)不复位 | +| 27 | 双 mask 注册失败路径 | `cq_proto_init` | OK:第一 mask==0 即 return(不建任务);第二 mask==0 时 bind/set_frame_queue 对称跳过,队列仍建(第一 mask 用) | +| 28 | CCM 队列越界 | `app_cq_proto.c` | OK:`_Static_assert(CQ_PAYLOAD_MAX <= RB_SIZE_RJ45)`;队列体 3×1052=3156 = mq_size;CCM 总量 ~6.3KB(64KB 池内) | +| 29 | 网络配置应用时序(原 _cq_net_cfg_apply,2026-08-21 移交 app_net_boot) | `app_net_boot.c` `app_net_boot_apply` / `app_boot.c` init_task | OK:`dev_eth_start()` 在 `sw_board_init()`(sw_initcall)之前调用,`pl_net_set_ip` 在 eth 启动后执行;有效性判定 ip 非 0 + port 1~65535,空/损坏写本构建默认落盘 | +| 30 | 协议一致性偏差 | 全部 | 记录:modify/warn1 level/pic1/voice_play1 不实现(与方案一致);screen 仅记录;voice_control1 grade=level/2+1 上限 7 → dev_rs232_voice_volume 原样透传(驱动无值域校验,grade 域 1~7,符合语音板 0~7 约定子集) | +| 31 | CH_ID_UDP_CQ=7 边界 | `app_dispatch.h` | OK:channels[CH_ID_MAX=8],索引 7 合法 | +| 32 | 上电画面注册冲突 | (已撤销) | **2026-08-21 已撤销**:删除 `app_cq_proto_default.c`,CQ 不注册默认画面、使用系统默认显示;故障恢复经 `app_default_display_show()` 无回调分支渲染系统欢迎画面 | +| 33 | IAP/LDI/CQ 10011 共存 | 实态 | OK:IAP 0x5A 快拒;LDI 先探测 12B CQ 帧(len=2 且 CRC 通过 LDI 校验)→ 2026-08-21 起对 `data_len==2` 显式 FAKE 放行 → CQ 认领。无已知功能冲突 | + +### B.11.3 遗留风险 + +1. `udp_cq_ch_send` 与断链 deinit/`netconn_delete` 的同优先级并发窗口(send 中途 conn 被删) + ——与 10011 通道同源模式,未在本次引入,若需根治可加发送锁或在 deinit 前暂停发送方。 +2. GBK 尾字节伪 `{`/`}` 与 JSON 转义 `\"` 的花括号定界脆弱性(与裸机同脆弱,接受)。 +3. dev 共存构建下搜索应答的 ip/mask/gw 取自 Sector1(LDI 语义),Sector1 空且 W25 有 + 配置(LDI 分支 2 会回写 Sector1,一致性有保障;分支 3 双空时报告出厂默认 + 192.168.1.5 而实机为 pl_net 默认 IP,偏差仅 dev 构建双空场景)。 +4. `app_udp_cq_broadcast` 当前无调用点(死代码),如协议后续要求业务口广播应答, + 端口语义已随 `g_udp_cq_port` 一致。 +5. **三固件同步烧录纪律(方案 B 布局变更,2026-08-21)**:方案 B 把 Sector1 记录从 + 68B 扩到 72B(`NetConfig_t` 新增 `udp_port`,`config_crc` 偏移 64→68)。 + **设备上 Bootloader/Recovery 若仍是旧 68B 布局(未随主固件同步烧录)**:旧 + Bootloader 按旧偏移读 `config_crc`(读到的是新记录的 `udp_port` 字段)→ CRC 判 + 失配 → 重建**旧布局**出厂记录;主固件按 72B 读又失配 → 反复自愈改写,两布局 + 交替重建、Sector1 永不稳定(网络配置回出厂态;若旧记录残留升级中间态还可能被 + 旧 Bootloader 条件 A/B/D 误判 App 损坏 → 设备陷 Recovery,主固件不运行 → + **UDP 10011/20103 全部无响应,LDI 搜索/CQ 业务均失效**)。此点修不了代码, + 属烧录纪律:**方案 B 后三固件(Bootloader+Recovery+主固件)必须同步烧录 + (`tool/flash_all.sh` 一次 J-Link 会话)**;任何单固件重烧后按惯例擦 Sector1 + 恢复出厂态。 diff --git "a/doc/04_\351\235\222\346\265\267\351\253\230\351\200\237\350\264\271\346\230\276\345\215\217\350\256\256/PartA_STD\350\256\276\345\244\207\345\261\202\346\236\266\346\236\204\344\274\230\345\214\226.md" "b/doc/04_\351\235\222\346\265\267\351\253\230\351\200\237\350\264\271\346\230\276\345\215\217\350\256\256/PartA_STD\350\256\276\345\244\207\345\261\202\346\236\266\346\236\204\344\274\230\345\214\226.md" new file mode 100644 index 0000000..350efb7 --- /dev/null +++ "b/doc/04_\351\235\222\346\265\267\351\253\230\351\200\237\350\264\271\346\230\276\345\215\217\350\256\256/PartA_STD\350\256\276\345\244\207\345\261\202\346\236\266\346\236\204\344\274\230\345\214\226.md" @@ -0,0 +1,410 @@ +# Part A:STD 设备能力复用与缺口分析(青海协议适配) + +**状态**:`[规划中]` +**目标**:评估青海高速费显协议对 STD 设备层的能力需求,确认现有 `Device/` 驱动的覆盖范围,仅对协议中确实缺失的设备能力做新增封装(如 `232_voice`),其余直接复用 `Device/` 驱动。 +**协议版本**:青海高速费显协议 2019-10-06 +**参考裸机工程**:`9K1030CE00` +**协议原文**:`青海协议修改20191006.doc` +**前置条件**:`Device/` 现有外设驱动已就绪(HUB75 显示、通信灯、报警灯、光敏传感器、RS485、网口);仅 `232_voice`**(TTS over RS232 语音板)** 需新增一个 `Device/` 级模块。 + +--- + +## A.0 架构决策记录 + +### 方案选型:是否保留 `dev_service` 共享层 + + +| 方案 | 描述 | 优缺点 | 结论 | +| ----------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------ | +| **原方案(废弃)** | 新建跨协议共享的 `dev_service` 层,新增 `dev_service_host_status_query()` / `dev_service_show_text_xy()` 等接口 | 青海所需的外设能力(HUB75、通信灯、报警灯、光敏、485、网口)已由 `Device/` 驱动提供,再建一层只是转发,徒增冗余;且 `dev_service_host_status_query` 本质是串口回发应答帧,不属于设备能力 | **废弃** | +| **现方案(采用)** | **砍掉** `dev_service` **共享层**。QH 协议层直接复用 `Device/` 现有驱动;仅「协议无关的设备能力缺口」在 `Device/` 内新增模块(`232_voice`);协议特有的映射/格式构造留在协议层 | 与 LDI 现有套路一致(LDI 也是直接调 `Device/` 驱动),无多余抽象层,职责清晰 | **采用** | + + + + +### 判定标准 + +- **直接复用** `Device/` **驱动**:参数简单、与硬件强相关、已在 `Device/` 封装好的能力(HUB75、通信灯、报警灯、光敏、485、网口、亮度)。 +- `Device/` **内新增模块**:能力确实缺失且协议无关、可跨协议复用(如 `232_voice` 语音板 TTS 发送)。 +- **留在协议层(ProtocolParser_QingHai)**:协议特有的映射(亮度 0~5→硬件级、颜色 0/1/2→红/绿/黄)、格式构造(`\|` 字段解析、礼貌用语索引表、费额金额→文本)、主机查询应答帧(串口回发)。 + + + +### 与 LDI 的关系 + +LDI 的 `Application/Src/LDI/app_ldi_device.c` 直接调用 `app_render` / `dev_display_*` / `dev_io_ctrl`(如 `dev_io_lane_light`、`dev_io_flash_light`),并不存在跨协议共享的 `dev_service` 层。QH 采用同一套路:**协议层直接调** `Device/` **驱动**,不另起共享服务层。 + +### 协议层内部架构:Parse/Execute + Tagged Union + +「协议层直接复用 `Device/` 驱动」确定后,协议层**内部**如何组织仍需决策:是把「帧解析 + 命令分发 + 设备调用 + 回响应」一把梭在一个 cmd handler 里(LDI 现状),还是把「解析」与「执行」分离、以 **Tagged Union 结构体**作为两者边界。 + + +| 方案 | 描述 | 结论 | +| ------------------------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 一把梭(LDI 现状) | 每个 cmd handler 内部自己强转 raw data、解析参数、调驱动、回响应 | 命令少时简单,但解析/执行耦合,难单测,裸机差异(Q9/Q14/Q17/Q19)难定位 | +| 事件驱动 Pub/Sub | 解析器 emit 事件入队,事件循环再分派到 handler | 与现有 `app_dispatch` 的「ring_buffer→probe→osMessageQueue→协议任务」**重复成双重队列**(额外 RAM + memcpy + 上下文切换),且打破 cmd `'1'`/`'B'` 「解析完立刻回响应」的同步时序;QH 命令纯请求-响应、无优先级抢占,属过度设计 | +| **Parse/Execute + Tagged Union(采用)** | 解析器把 raw bytes 解成 `qh_parsed_cmd_t`(枚举 cmd + 校验状态 + 各命令已解码参数 union);执行器按 cmd 分派,只读 union 调驱动 | 解析与执行解耦、清单可 dump 调试/对账裸机、解析器是纯函数可单测、与 LDI `ldi_ctrl_*_t` 套路一致;在已有 frame_queue 之后、在 `handle_task` 内**同步** parse→execute,不加新队列 | + + +**为什么不用事件驱动**:现有 `app_dispatch` 已对每条协议做了异步分发(见 `app_dispatch.h` 的 `frame_msg_t` 队列)。再在协议层套一层 event queue 就是双重队列;且同步响应时序会被破坏。Tagged Union 是「在 handle_task 内同步 parse→execute」,零额外队列、不破坏响应时序。 + +**清单(Tagged Union)设计原则**: + +- `cmd` 映射为枚举,`sta` 拆为结构/语义两级校验(`QH_PARSE_OK` / `ERR_FRAME` / `ERR_CMD` / `ERR_PARAM`); +- union 内只放**协议域已解码**的值(颜色索引 0/1/2、亮度 0~5、bit 位、金额分),**不放设备域值**(如 `COLOR_RED`、硬件亮度级)——映射留在执行器; +- 复杂命令(如 cmd `'6'`)解析器只做结构校验 + 保留原始 `|` 字段指针,字段拆分 / 5 行格式化留给执行器(属协议语义解释,本就在协议层); +- 解析器与执行器在**同一任务上下文同步调用**,清单里 `data` 指针指向 `frame_msg_t` 缓冲区,必须同步消费,禁止跨任务异步使用。 + +--- + + + +## A.1 青海协议功能全景——分类总表 + +> **分类原则**: +> +> - **Device/ 已有驱动**:硬件能力已在 `Device/` 封装,协议层直接调用,无需新增。 +> - **需新增 Device/ 模块**:设备能力缺失且协议无关,`Device/` 内新增(仅 `232_voice`)。 +> - **协议层逻辑**:解析/映射/格式构造属协议特有,留在 `ProtocolParser_QingHai`。 + + + +### A.1.1 全部功能清单 + + +| 序号 | 功能描述 | 分类 | 实现位置 | 说明 | +| --- | ------------------------------ | -------------------------- | ---------------------------------------------------- | ---------------------------------------- | +| F01 | ASCII 帧定界 `{`...`}` 扫描 | **协议层** | `app_qh_proto.c` probe | 青海特有的 `{cmd len data}` 帧格式 | +| F02 | cmd 字节查表分发 `'1'~'B'` | **协议层** | `app_qh_proto_cmd.c` | ASCII 命令字分发表 | +| F03 | 主机状态查询应答 `{1 01 00}` | **协议层** | `app_qh_proto_cmd.c` | 串口 RS232 回发应答帧(非设备能力) | +| F04 | 单行显示:颜色 + 行号 + GBK 文本 | **Device/ 已有** | `app_render` + `dev_display` | 协议层做颜色映射后构造 `render_cfg_t` | +| F05 | 全屏可编辑显示:颜色 + X/Y + 文本 | **Device/ 已有** | `app_render` + `dev_display` | `RENDER_TEXT` 本身支持任意 x/y | +| F06 | 全屏清除 | **Device/ 已有** | `dev_display_fill` + `commit_frame` | 填充黑并提交帧 | +| F07 | 固定格式显示(客车)5 行 + 费额语音 | **协议层 + 232_voice** | `app_qh_proto_cmd.c` + `dev_rs232_voice_play` | 协议层解析 ` | +| F08 | 固定格式显示(货车)5 行 + 费额语音 | **协议层 + 232_voice** | `app_qh_proto_cmd.c` + `dev_rs232_voice_play` | 同上,增加总重/超重行 | +| F09 | 礼貌用语索引→GBK 文本→TTS | **协议层 + 232_voice** | `app_qh_proto_voice.c` + `dev_rs232_voice_play` | 协议层完成索引→文本映射 | +| F10 | 亮度设置:协议 0~~5 → 硬件 0~~7 | **Device/ 已有** | `dev_display_set_brightness` | 映射在协议层,调 `dev_display` | +| F11 | 音量设置:协议 1~5 → 标签 | **Device/ 新增** | `dev_rs232_voice_volume` | 经 `232_voice` 模块 | +| F12 | 外设控制:bit0=绿灯, bit1=红灯, bit2=黄闪 | **Device/ 已有** | `dev_io_lane_light` + `dev_io_flash_light` | 协议层解析 bit 位后调用 | +| F13 | 费额语音:数字→金额文本→TTS | **协议层 + 232_voice** | `app_qh_proto_voice.c` + `dev_rs232_voice_play` | 协议层构造文本 | +| F14 | 自检模式:全屏黄 + 语音 | **Device/ 已有 + 232_voice** | `app_render` + `dev_rs232_voice_play` | 协议层编排 | +| F15 | 上电欢迎语:"祝您一路平安" + 语音 | **协议层 + 232_voice** | `app_qh_proto.c` (init) | 协议层构造文本 | +| F16 | 串口 RS232 收发(`CH_ID_RS232`) | **Device/ 已有** | 已有 `dev_rs232.c` + 通道框架 | 通道已实现,青海绑定即可 | +| F17 | 文字渲染(GBK→点阵→屏显) | **Device/ 已有** | 已有 `app_render` | 16 点阵单行/多行显示 | +| F18 | HUB75 全屏颜色填充 | **Device/ 已有** | 已有 `dev_display` + `app_render` | `RENDER_FILL` | +| F19 | HUB75 清屏 | **Device/ 已有** | 已有 `dev_display` | 填充黑 + 提交 | +| F20 | 亮度调节(手动 0~7 / 光敏自动) | **Device/ 已有** | 已有 `dev_display_set_brightness` + `dev_light_sensor` | 手动经 `dev_display`,自动经 `dev_light_sensor` | +| F21 | TTS 语音播报(GBK 文本 → 语音板) | **Device/ 新增** | `dev_rs232_voice_play` | **缺失,需在** `Device/Comm/` **新增** | +| F22 | TTS 音量设置 | **Device/ 新增** | `dev_rs232_voice_volume` | 随 `232_voice` 模块新增 | +| F23 | 车道灯 GPIO 控制 | **Device/ 已有** | 已有 `dev_io_lane_light` | 通信灯 | +| F24 | 黄闪报警控制 | **Device/ 已有** | 已有 `dev_io_flash_light` | 报警灯 | + + + + +### A.1.2 分类统计 + + +| 分类 | 数量 | 说明 | +| ------------------ | ------------------------------------------ | -------------------------------------- | +| **Device/ 已有驱动** | 14 (F04~~F06, F10, F12, F16~~F20, F23~F24) | 直接复用,零新增 | +| **需新增 Device/ 模块** | 1 类(`232_voice`:F11, F21, F22) | 仅在 `Device/Comm/` 新增 `dev_rs232_voice` | +| **协议层逻辑** | 9 (F01~~F03, F07~~F09, F13~F15) | 映射/格式构造/应答帧,留在协议层 | + + +**核心结论:砍掉** `dev_service` **共享层;14 项设备能力由** `Device/` **驱动直接复用,仅缺「TTS over RS232 语音板」一项,在** `Device/` **新增** `232_voice` **模块;其余协议特有逻辑留在协议层。** + +--- + + + +## A.2 设备能力调用矩阵 + + + +### A.2.1 各命令 → 协议层动作 → `Device/` 驱动调用 + + +| cmd | ASCII | 协议层动作 | `Device/` 驱动调用 | 备注 | +| ----- | ----- | ----------------------- | ----------------------------------------------------------------------------------- | --------------- | +| `'1'` | 0x31 | 构造应答帧 `{1 01 00}` | `channel_send(ch, rsp, len)`(RS232 回发) | 非设备能力,走通道 | +| `'2'` | 0x32 | 全屏黄 + 语音 | `app_render(RENDER_FILL, COLOR_YELLOW)` + `dev_rs232_voice_play("系统正在加电自检")` | 自检 | +| `'3'` | 0x33 | 颜色映射(0~2→红/绿/黄) + 行号→y | `app_render(RENDER_TEXT, x=0, y=row*FONT_16, ...)` | 单行显示 | +| `'4'` | 0x34 | 颜色映射 + 解析 X/Y + `\n` 换行 | `app_render(RENDER_TEXT, x, y, ...)` | 全屏可编辑 | +| `'5'` | 0x35 | 无参数 | `dev_display_fill(dev,0,0,rows,cols,COLOR_BLACK)` + `dev_display_commit_frame(dev)` | 清屏 | +| `'8'` | 0x38 | 映射 0~~5→硬件 0~~7 | `dev_display_set_brightness(dev, mapped_level)` | 亮度(映射在协议层) | +| `'9'` | 0x39 | 映射 1~5→音量标签 | `dev_rs232_voice_volume(mapped_level)` | 音量(经 232_voice) | +| `'A'` | 0x41 | 解析 bit0/bit1/bit2 | `dev_io_lane_light(green)` + `dev_io_flash_light(yellow)` | 外设 | + + + +| cmd | ASCII | 协议层动作(业务逻辑) | `Device/` 驱动调用 | 备注 | +| ----- | ----- | ---------------------- | ---------------------------- | ------------------------------ | +| `'6'` | 0x36 | 解析 ` | ` 分隔字段 + 构造 5 行 | `app_render(RENDER_TEXT)` x5 行 | +| `'6'` | 0x36 | 金额 > 阈值时 | `dev_rs232_voice_play(text)` | 语音播报 | +| `'7'` | 0x37 | 索引→文本映射 | `dev_rs232_voice_play(text)` | 礼貌用语 | +| `'B'` | 0x42 | 解析 type + 5 位数字 + 构造文本 | `dev_rs232_voice_play(text)` | 费额语音 | + + + + +### A.2.2 与 LDI 的设备调用复用对比 + + +| 设备能力 | 青海 QH 调用 | LDI 调用 | 复用说明 | +| ----------------- | ------------------------------ | ---------------------------------------------------- | -------------------------------------- | +| HUB75 填充/清屏/文字 | `app_render` + `dev_display_*` | `app_render` + `dev_display_*`(经 `app_ldi_device.c`) | 同一套 `Device/` 驱动 | +| 通信灯 | `dev_io_lane_light` | `dev_io_lane_light`(经 `app_ldi_device.c`) | 完全一致 | +| 报警灯/黄闪 | `dev_io_flash_light` | `dev_io_flash_light`(经 `app_ldi_device.c`) | 完全一致 | +| 亮度 | `dev_display_set_brightness` | `dev_display_set_brightness`(经 `app_ldi_device.c`) | 完全一致 | +| 光敏自动亮度 | `dev_light_sensor` | `dev_light_sensor` | 完全一致 | +| RS232 收发 | `dev_rs232` + 通道 | `dev_rs232` + 通道 | 完全一致 | +| **TTS 语音(缺失→新增)** | `dev_rs232_voice_play` | 无(LDI 不碰语音) | **新增** `Device/Comm/dev_rs232_voice.c` | + + +--- + + + +## A.3 设备能力缺口分析 + + + +### A.3.1 已确认无缺口(直接复用 `Device/` 驱动) + + +| 能力 | 状态 | 调用入口 | +| -------------------- | ------- | --------------------------------------------------------------- | +| HUB75 文字显示(单行/多行/XY) | **已覆盖** | `app_render`(RENDER_TEXT,支持任意 x/y) | +| HUB75 颜色填充 / 清屏 | **已覆盖** | `app_render(RENDER_FILL)` / `dev_display_fill` + `commit_frame` | +| 通信灯(通行灯) | **已覆盖** | `dev_io_lane_light(bool)` | +| 报警灯 / 黄闪 | **已覆盖** | `dev_io_flash_light(bool)` | +| 亮度调节(手动 0~7) | **已覆盖** | `dev_display_set_brightness(dev, level)` | +| 光敏自动亮度 | **已覆盖** | `dev_light_sensor` | +| RS232 串口收发 | **已覆盖** | `dev_rs232.c` + `CH_ID_RS232` 通道 | +| RS485 / 网口 | **已覆盖** | `dev_rs485.c` / `dev_eth.c`(青海未用,但能力就绪可复用) | + + + + +### A.3.2 需新增的 `Device/` 模块 + + +| 序号 | 模块 | 位置 | 用途 | +| --- | ------------------------------- | ---------------------------------------------------------------- | ------------------------------------------- | +| N1 | `232_voice`(TTS over RS232 语音板) | `Device/Comm/dev_rs232_voice.c` + `Device/Inc/dev_rs232_voice.h` | 把 GBK 文本按语音板物理帧格式经 RS232 发送;协议无关,CQ/QH 均可复用 | + + +接口: + +```c +/* Device/Inc/dev_rs232_voice.h */ +#pragma once +#include + +#define DEV_RS232_VOICE_MAX_TEXT (200U) + +/** TTS 合成播放(GBK 文本),经 RS232 发送至语音板 */ +void dev_rs232_voice_play(const uint8_t *gbk_text, uint16_t len); + +/** TTS 设置音量(协议无关的音量标签) */ +void dev_rs232_voice_volume(uint8_t level); +``` + + + +### A.3.3 协议层逻辑(非设备缺口,留在协议层) + + +| 序号 | 项 | 详情 | 归属 | +| --- | ----------------- | ----------------------------------------------------------------------------------- | --- | +| M1 | 亮度映射 0~~5→硬件 0~~7 | QH 协议 0~~5 映射到~~ `dev_display` ~~的 0~~7 级(查表法);协议层查表后调 `dev_display_set_brightness` | 协议层 | +| M2 | 颜色映射 0/1/2→红/绿/黄 | QH 协议色码 → `display_color_t`(COLOR_RED/GREEN/YELLOW) | 协议层 | +| M3 | 主机查询应答帧 | cmd `'1'` 经 RS232 回发 `{1 01 00}`,属通道收发,非设备能力 | 协议层 | +| M4 | 礼貌用语索引表 / 费额文本构造 | 协议特有的索引→文本映射、金额→"您好请交费XX元" | 协议层 | +| M5 | 外设控制位解析 | bit0/bit1/bit2 → `dev_io_lane_light` / `dev_io_flash_light` | 协议层 | + + +--- + + + +## A.4 设备架构 + + + +### A.4.1 分层架构(无 `dev_service` 层) + +``` ++--------------------------------------------------+ +| Protocol Layer (LDI / CQ / RLS / QH) | +| 内部再分(以 QH 为例): | +| 解析器 qh_parse_frame() | +| bytes ──▶ qh_parsed_cmd_t(Tagged Union 清单) | +| 执行器 qh_execute_cmd() | +| 清单 ──▶ 协议→设备映射 + Device/ 驱动调用 | +| 职责:帧解析 + 命令分发 + 协议特有的映射/格式构造 | +| 例如:cmd '6' \| 分隔解析、cmd '7' 索引映射、 | +| cmd 'B' 金额文本构造、cmd '8' 亮度映射、 | +| cmd 'A' bit 位解析、cmd '1' 应答帧回发 | +| 调用:Device/ 驱动 + 232_voice 模块 | ++--------------------------------------------------+ +| Device Layer (Device/) | +| app_render / dev_display (HUB75 显示) | +| dev_io_ctrl (通信灯 / 报警灯) | +| dev_light_sensor (光敏) | +| dev_rs232 (+ 232_voice) (RS232 / 语音板 TTS) | +| dev_rs485 / dev_eth (485 / 网口) | ++--------------------------------------------------+ +``` + +> **说明**:不再存在跨协议共享的 `dev_service` 层。QH 协议层与 LDI 一样,直接调用 `Device/` 驱动;仅「TTS 语音板」能力缺失,在 `Device/Comm/` 新增 `232_voice` 模块。协议层**内部**采用 Parse/Execute + Tagged Union(详见 Part B B.4):解析器产出「清单」结构体,执行器消费清单并调设备。 + + + +### A.4.2 青海协议在分层中的位置 + +``` +串口 RS232 (CH_ID_RS232) + -> app_channel_dispatch + -> ring_buffer -> frame_dispatch + -> qh_proto_probe_frame (帧定界: '{' ... '}') + -> g_qh_proto_frame_queue (已有队列,非新增) + -> qh_proto_handle_task + | + | // 同步、同一任务上下文,无额外队列 + | qh_parsed_cmd_t cmd = qh_parse_frame(msg->data, msg->data_len); // 解析器 + | qh_execute_cmd(msg->ch, &cmd); // 执行器 + | + |--- 解析器 qh_parse_frame:bytes → 清单(协议域已解码,不碰设备)--- + | 结构校验 -> cmd 枚举 -> 各命令参数填入 union + | + |--- 执行器 qh_execute_cmd:清单 → Device/ 驱动(做协议→设备映射后调用)--- + | cmd '2' -> app_render(RENDER_FILL, YELLOW) + dev_rs232_voice_play("自检") + | cmd '3' -> app_render(RENDER_TEXT, 颜色映射, y=row*FONT_16, ...) + | cmd '4' -> app_render(RENDER_TEXT, 颜色映射, x, y, ...) + | cmd '5' -> dev_display_fill(BLACK) + dev_display_commit_frame + | cmd '8' -> dev_display_set_brightness(dev, 映射后 level) + | cmd '9' -> dev_rs232_voice_volume(映射后 level) + | cmd 'A' -> dev_io_lane_light(green) + dev_io_flash_light(yellow) + | cmd '1' -> channel_send(ch, "{1 01 00}", ...) <-- 应答帧,走通道 + | cmd '6' -> 解析 \| 字段 + 构造 5 行 -> app_render x5 + dev_rs232_voice_play + | cmd '7' -> 索引→文本映射 -> dev_rs232_voice_play + | cmd 'B' -> 解析 + 构造金额文本 -> dev_rs232_voice_play +``` + +--- + + + +## A.5 `232_voice` 设备模块设计(唯一新增) + + + +### A.5.1 定位 + +- **位置**:`Device/Comm/dev_rs232_voice.c` + `Device/Inc/dev_rs232_voice.h`(与 `dev_rs232.c` 同目录)。 +- **职责**:协议无关的「GBK 文本 → 语音板物理帧 → RS232 发送」。帧格式是语音板的,不是任何协议的。 +- **复用范围**:QH 与 CQ 共用(谁用语音板谁调它)。 + + + +### A.5.2 语音板帧格式 + +``` +FD | 00 | [Len+2] | 01 | 01 | GBK text bytes... +``` + +由 `dev_rs232_voice_play` 内部组装,协议层只传 GBK 文本与长度。 + +### A.5.3 接口实现要点 + +```c +/* Device/Comm/dev_rs232_voice.c */ +#include "dev_rs232_voice.h" +#include "dev_rs232.h" // RS232 通道发送 + +void dev_rs232_voice_play(const uint8_t *gbk_text, uint16_t len) +{ + if (!gbk_text || len == 0 || len > DEV_RS232_VOICE_MAX_TEXT) return; + // 组装 FD 00 [Len+2] 01 01 + GBK... + // 经 RS232 通道发送至语音板 +} + +void dev_rs232_voice_volume(uint8_t level) +{ + // 组装音量设置帧并发送 +} +``` + + + +### A.5.4 与协议层的边界 + + +| 在哪做 | 内容 | +| ------------------------------- | ----------------------------------------------------- | +| `Device/Comm/dev_rs232_voice.c` | 语音板物理帧封装、RS232 发送、音量帧 | +| `app_qh_proto_voice.c` | 礼貌用语索引表、费额金额→"您好请交费XX元"文本构造,再调 `dev_rs232_voice_play` | + + +--- + + + +## A.6 编译期配置 + +```makefile +PROTO ?= LDI +ifeq ($(PROTO),QH) + SRC_APPLICATION += $(wildcard Application/Src/ProtocolParser_QingHai/*.c) + SRC_DEVICE += Device/Comm/dev_rs232_voice.c # 新增 232_voice 模块 + CFLAGS += -DPROTO_QINGHAI +else ifeq ($(PROTO),CQ) + SRC_APPLICATION += $(wildcard Application/Src/ProtocolParser_ChongQing/*.c) + SRC_DEVICE += Device/Comm/dev_rs232_voice.c # CQ 亦复用 + CFLAGS += -DPROTO_CHONGQING +else + SRC_APPLICATION += $(wildcard Application/Src/LDI/*.c) +endif +``` + +**与 LDI / CQ 编译期互斥**,运行时只执行一种协议。 + +--- + + + +## A.7 设备能力验收标准(Part A 范围) + + +| 验收项 | 标准 | +| ----------------- | -------------------------------------------------------------------------------- | +| 无 `dev_service` 层 | 全仓不存在 `Device/Service/dev_service.*` 与 `dev_service_*` 调用 | +| HUB75/灯/光敏复用 | `app_render` / `dev_display_*` / `dev_io_ctrl` / `dev_light_sensor` 被 QH 协议层直接调用 | +| `232_voice` 新增 | `Device/Comm/dev_rs232_voice.c` 编译通过,`dev_rs232_voice_play/volume` 可被协议层调用 | +| LDI 不受影响 | `make PROTO=LDI -j8` 编译不变 | +| CQ 不受影响 | `make PROTO=CQ -j8` 编译不变(共享 `232_voice`) | +| 无循环依赖 | `Device/Comm/dev_rs232_voice.h` 不依赖任何协议头文件 | + + +--- + + + +## A.8 总结 + + +| 项目 | 结论 | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | +| 是否保留 `dev_service` 共享层 | **否** —— 已砍掉,QH 协议层直接复用 `Device/` 驱动 | +| `Device/` 已有驱动可复用 | **14 项** —— HUB75/通信灯/报警灯/光敏/485/网口/亮度/RS232 等,零新增 | +| 需新增的 `Device/` 模块 | **1 类** —— `232_voice`(TTS over RS232 语音板),仅此一项 | +| 是否新增 `dev_voice` 之外的设备驱动 | **否** —— 其余硬件驱动均就绪 | +| 协议层保留逻辑 | 帧扫描、命令分发、固定格式显示(cmd `'6'`)、礼貌用语(cmd `'7'`)、费额语音(cmd `'B'`)、亮度/颜色映射、主机查询应答帧 | +| **协议层内部架构** | **Parse/Execute + Tagged Union**:解析器 `qh_parse_frame` 产出 `qh_parsed_cmd_t` 清单,执行器 `qh_execute_cmd` 消费清单调设备;同步、无额外队列(详见 Part B B.4) | +| **为何不用事件驱动** | 现有 `app_dispatch` 已做异步分发,再套 event queue 成双重队列且破坏同步应答时序;QH 纯请求-响应,过度设计 | +| Part A 工作量 | **极小** —— 新增 `232_voice` 一个模块 + 编译期切换验证 | +| 重点 | **复用** `Device/` **驱动;仅补** `232_voice`**;协议特有逻辑留在协议层;协议层内部按 Parse/Execute 拆分;全部工作量在 Part B(协议解析器)** | + + diff --git "a/doc/04_\351\235\222\346\265\267\351\253\230\351\200\237\350\264\271\346\230\276\345\215\217\350\256\256/PartB_ProtocolParser_QingHai\346\216\245\345\205\245.md" "b/doc/04_\351\235\222\346\265\267\351\253\230\351\200\237\350\264\271\346\230\276\345\215\217\350\256\256/PartB_ProtocolParser_QingHai\346\216\245\345\205\245.md" new file mode 100644 index 0000000..67c0930 --- /dev/null +++ "b/doc/04_\351\235\222\346\265\267\351\253\230\351\200\237\350\264\271\346\230\276\345\215\217\350\256\256/PartB_ProtocolParser_QingHai\346\216\245\345\205\245.md" @@ -0,0 +1,1222 @@ +# Part B:ProtocolParser_QingHai 接入 + +**状态**:`[规划中]` +**协议层内部架构**:**Parse/Execute + Tagged Union**(解析器 `qh_parse_frame` → 清单 `qh_parsed_cmd_t` → 执行器 `qh_execute_cmd`;同步、无额外队列,详见 B.4) +**前置条件**:`Device/` 现有驱动已就绪;仅 `232_voice`(TTS over RS232 语音板)需在 `Device/Comm/` 新增(见 Part A)。QH 协议层直接复用 `Device/` 驱动,不引入 `dev_service` 共享层。 +**协议版本**:青海高速费显协议 2019-10-06 +**参考裸机工程**:`/home/yystation/Desktop/9K1030CE00` +**协议原文**:`/home/yystation/Desktop/9K1030CE00/07协议/青海协议修改20191006.txt` + +> QH 协议解析器**直接调用 `Device/` 驱动**完成硬件操作,不新增跨协议共享层。 +> 协议层内部按 **Parse/Execute + Tagged Union** 拆分:解析器只产出协议域清单(纯函数),执行器只消费清单做映射 + 调驱动;两者在 `handle_task` 内同步调用,无事件队列。 +> 所有协议特有逻辑(帧定界、命令分发、字段解析、格式编排、颜色/亮度映射)收敛在本模块内。 + +--- + +## B.1 背景与定位 + +### B.1.1 现有协议栈 + +| 协议 | 目录 | 帧形态 | 默认通道 | 编译宏 | +|------|------|--------|----------|--------| +| IAP | `Application/Src/IAP/` | 二进制 `0x5A5A5A5A` | UDP/10011 | — | +| LDI | `Application/Src/LDI/` | 二进制 `0xFFFF` + CRC16-XMODEM | TCP + UDP/10011 | `PROTO_LDI` | +| RLS | `Application/Src/RLS/` | 二进制 | RS485 | — | +| AH_MQTT | `Application/Src/AH_MQTT/` | MQTT topic | MQTT | — | +| 重庆(规划中) | `Application/Src/ProtocolParser_ChongQing/` | JSON `{...}` + 固定 12B 二进制 | UDP/20103 + UDP/10011 | `PROTO_CHONGQING` | +| **青海(新建)** | `Application/Src/ProtocolParser_QingHai/` | **ASCII `{cmd len data}`** | **RS232(串口)** | **`PROTO_QINGHAI`** | + +### B.1.2 青海协议核心特征 + +青海协议是**基于串口的 ASCII 文本协议**,与 LDI(二进制 TCP/UDP)和 CQ(JSON UDP)有本质区别: + +| 特征 | 青海协议 | LDI | CQ | +|------|----------|-----|-----| +| 传输层 | **串口 RS232** | TCP + UDP | UDP | +| 帧格式 | ASCII `{cmd len data}` | 二进制 0xFFFF + CRC16 | JSON `{...}` 或 12B 二进制 | +| 校验 | **无** | CRC-16/XMODEM | 无(JSON)/ CRC(BIN) | +| 命令编码 | ASCII 字符 `'1'~'B'` | 十六进制 0x0A~0x21 | JSON "cmd" 字段 | +| 状态机 | **无**(纯命令驱动) | UNINIT→AUTHED→READY | 无(心跳检测) | +| 显示行数 | **5 行**(16 点阵) | 8 行 | 8 行 | +| 上电默认 | "祝您一路平安" + 语音 | 网络搜索 | 网络搜索 | + +### B.1.3 与裸机工程的关系 + +移植策略:**行为对齐裸机联调结果**(含已知与协议文档的差异),直接复用 `Device/` 驱动。**通过 `Device/` 驱动调用所有设备能力**(含新增的 `232_voice`)。 + +### B.1.4 与 LDI 的产品关系 + +**编译期互斥**:青海固件不含 LDI,LDI 固件不含青海。编译切换通过 Makefile / `.eide` 的条件编译实现(如 `PROTO=QH`)。 + +### B.1.5 初始化顺序与 RS232 启动(M1:明确调用位置) + +QH 协议走 **RS232-0(USART3,`CH_ID_RS232=1`)**。通道实例的注册与协议的绑定是解耦的两步,必须都完成且早于首个帧到达: + +1. **启动 RS232-0 通道任务**(在 `main` 早期、`app_dispatch_init()` 之后调用): + ```c + app_rs232_start(); // 启动 USART3 收/发任务(见 Application/Src/Channel/app_rs232.c) + ``` + 该函数内部创建任务,任务首件事是 `app_channel_register(CH_ID_RS232, &g_rs232_0.me)`,把通道实例注册进分发框架。**只要此函数返回,通道即已就绪。** + +2. **QH 协议自注册 + 绑定**(经 `sw_app_initcall(qh_proto_init)` 自动跑): + ```c + static void qh_proto_init(void) + { + g_qh_proto_frame_queue = osMessageQueueNew(2, FRAME_DATA_MAX_LEN, &s_qh_queue_attr); + proto_mask_t mask = app_proto_register(qh_proto_probe_frame, rb_group_rb(RB_GROUP_PROTO)); + app_proto_bind_channel(mask, CH_ID_RS232); // 协议 mask ← RS232 通道 + app_proto_set_frame_queue(mask, g_qh_proto_frame_queue); + } + ``` + +**顺序约束**:`app_rs232_start()` 与 `qh_proto_init` 二者无强先后依赖(注册的是不同对象),但**首个帧到达前两者都必须完成**。推荐约定:`app_rs232_start()` 在 `main` 显式调用(不放在 initcall,避免与协议 initcall 顺序耦合);`qh_proto_init` 交给 initcall 自动执行。 + +**易错点**:`app_rs232_1_start()` 启动的是 **RS232-1(USART6,`CH_ID_RS232_1=6`)**,与 QH 无关(除非硬件改接第二路串口),切勿误调为 QH 绑定通道。 + +--- + +## B.2 功能全景——协议特有功能清单 + +> 以下所有功能均属于"协议特有功能",在 `ProtocolParser_QingHai` 层实现;设备能力直接调 `Device/` 驱动(或 `232_voice`),详见 Part A。 + +### B.2.1 协议特有功能矩阵 + +| 序号 | 功能 | 协议命令 | 实现位置 | 说明 | +|------|------|---------|---------|------| +| P01 | **ASCII 帧定界** | — | `app_qh_proto.c` probe | `'{'` 起始 + 长度字段 + `'}'` 结尾 | +| P02 | **命令字查表分发** | `'1'~'B'` | `app_qh_proto_cmd.c` | 11 条命令的分发表 + 处理函数 | +| P03 | **主机查询应答** | `'1'` | `app_qh_proto_cmd.c` | 构造 `{1 01 00}` 固定应答帧,串口回发(`channel_send`) | +| P04 | **自检模式编排** | `'2'` | `app_qh_proto_cmd.c` | 全屏黄色(`app_render`)+ 语音(`dev_rs232_voice_play`) | +| P05 | **单行显示参数解析** | `'3'` | `app_qh_proto_cmd.c` | 解析颜色(0~2) + 行号(1~5) + GBK文本,构造 `render_cfg_t` | +| P06 | **全屏可编辑显示** | `'4'` | `app_qh_proto_cmd.c` | 解析颜色 + X/Y坐标 + 文本 + 换行符,构造 `render_cfg_t` | +| P07 | **清屏** | `'5'` | `app_qh_proto_cmd.c` | `dev_display_fill(BLACK)` + `commit_frame` | +| P08 | **固定格式显示(客车)** | `'6'` | `app_qh_proto_cmd.c` | 解析 `\|` 分隔字段,构造 5 行格式化文本 + 语音 | +| P09 | **固定格式显示(货车)** | `'6'` | `app_qh_proto_cmd.c` | 同上,增加总重/超重行,0 吨不显示 | +| P10 | **礼貌用语索引映射** | `'7'` | `app_qh_proto_cmd.c` | `'0'~'3'` → 4 条固定 GBK 文本 → `dev_rs232_voice_play` | +| P11 | **亮度级别映射** | `'8'` | `app_qh_proto_cmd.c` | `'0'`=自动(光敏驱动);`'1'~'5'` 查表(索引=level-1) → `dev_display_set_brightness` | +| P12 | **音量级别映射** | `'9'` | `app_qh_proto_cmd.c` | 协议 1~5 → 音量标签 → `dev_rs232_voice_volume` | +| P13 | **外设控制位解析** | `'A'` | `app_qh_proto_cmd.c` | bit0=绿灯, bit1=红灯, bit2=黄闪 → `dev_io_lane_light` / `dev_io_flash_light` | +| P14 | **费额语音文本构造** | `'6'/'B'` | `app_qh_proto_voice.c` | 金额数字 → "您好请交费XX.XX元" GBK 文本 → `dev_rs232_voice_play` | +| P15 | **上电欢迎语编排** | — | `app_qh_proto.c` init | "祝您一路平安" 显示(`app_render`)+ 语音(`dev_rs232_voice_play`) | +| P16 | **串口通道绑定** | — | `app_rs232_start()` + `qh_proto_init` | 绑定 `CH_ID_RS232`(index=1),初始化顺序见 B.1.5 | +| P17 | **编译期互斥** | — | Makefile | `PROTO_QINGHAI` 宏 | + +--- + +## B.3 目录与文件规划 + +与 LDI 同级;源/头分离。**文件名统一使用 `app_qh_proto_*` 样式**。采用 **Parse/Execute + Tagged Union**: + +- **`app_qh_proto_parse.c`** —— 解析器:把 raw bytes 解成 `qh_parsed_cmd_t`(清单,Tagged Union)。纯函数,不碰 `Device/` 驱动、不回响应。 +- **`app_qh_proto_cmd.c`** —— 执行器:消费清单,做协议→设备映射后调用 `Device/` 驱动。 +- **`app_qh_proto_voice.c`** —— 语音辅助(执行器侧):礼貌用语索引、费额金额→TTS 文本。 +- **`app_qh_proto.c`** —— 核心:initcall、probe、handle_task(同步 `parse→execute`)、帧队列。 +- **`app_qh_proto_cfg.c`** —— 青海侧配置(波特率等参数落盘)。 + +### B.3.1 文件树架构 + +``` +Application/Src/ProtocolParser_QingHai/ + app_qh_proto.c # 核心:initcall、probe、任务、帧分发(同步 parse→execute) + app_qh_proto_parse.c # 解析器:qh_parse_frame() bytes → qh_parsed_cmd_t(清单) + app_qh_proto_cmd.c # 执行器:qh_execute_cmd() 清单 → Device/ 驱动调用 + 协议→设备映射 + app_qh_proto_voice.c # 语音辅助(执行器侧):礼貌用语索引、费额金额→TTS文本 + app_qh_proto_cfg.c # 青海侧配置(波特率等参数落盘) + +Application/Inc/ProtocolParser_QingHai/ + app_qh_proto.h # 公共契约:qh_parsed_cmd_t(清单)+ qh_cmd_t + 解析/执行 API 声明 + app_qh_proto_parse.h # 解析器对外 API(含 qh_parse_frame) + app_qh_proto_cmd.h # 执行器对外 API(含 qh_execute_cmd + 映射表 extern) + app_qh_proto_voice.h + app_qh_proto_cfg.h +``` + +> 不新增 `app_qh_device.c` 之类的设备包装层:执行器直接调用 `Device/` 驱动,与 LDI(`app_ldi_device.c` 直接调驱动)同一套路;协议特有的映射表收在 `app_qh_proto_cmd.c` / `app_qh_proto_voice.c`。 +> **解析器与执行器之间是「清单」结构体,不是队列、不是事件**——两者在 `handle_task` 内同步调用,零额外 IPC。 + +### B.3.2 命名规范 + +| 项 | 规则 | 示例 | +|----|------|------| +| 目录名 | `ProtocolParser_QingHai` | — | +| 文件名 | `app_qh_proto_*.c` | `app_qh_proto_parse.c` / `app_qh_proto_cmd.c` | +| 解析器函数前缀 | `qh_parse_` | `qh_parse_frame()` | +| 执行器函数前缀 | `qh_exec_` | `qh_exec_one_line()` | +| 执行器入口 | `qh_execute_cmd()` | 由 handle_task 调用 | +| 全局变量 | `g_qh_proto_` | `g_qh_proto_ctx` | +| 头文件守卫 | `APP_QH_PROTO_*_H` | `APP_QH_PROTO_PARSE_H` | +| 静态变量 | `s_qh_` | `s_qh_color_map` | + +### B.3.3 构建编入 + +| 文件 | 改动 | +|------|------| +| `Makefile` | `INC_DIRS` 增加 `Application/Inc/ProtocolParser_QingHai/`;`SRC_APPLICATION` 条件追加;`SRC_DEVICE` 追加 `Device/Comm/dev_rs232_voice.c` | +| `.eide/eide.yml` | 增加同组源文件(放在 QH 条件组中) | + +**编译期切换方案**: + +```makefile +PROTO ?= LDI +ifeq ($(PROTO),QH) + SRC_APPLICATION += $(wildcard Application/Src/ProtocolParser_QingHai/*.c) + SRC_DEVICE += Device/Comm/dev_rs232_voice.c + CFLAGS += -DPROTO_QINGHAI +else ifeq ($(PROTO),CQ) + SRC_APPLICATION += $(wildcard Application/Src/ProtocolParser_ChongQing/*.c) + SRC_DEVICE += Device/Comm/dev_rs232_voice.c + CFLAGS += -DPROTO_CHONGQING +else + SRC_APPLICATION += $(wildcard Application/Src/LDI/*.c) +endif +``` + +**无需引入 cJSON 等第三方库**——QH 是纯 ASCII 协议,手动解析即可。 + +--- + +## B.4 架构设计(Parse/Execute + Tagged Union) + +### B.4.1 架构蓝图 + +``` + ┌──────────────────────────────────────────────────┐ + │ ProtocolParser_QingHai │ + RS232 ─▶ channel ─▶ rb ─▶ frame_dispatch ─▶ qh_proto_probe_frame │ + │ │ READY │ + │ ▼ │ + │ g_qh_proto_frame_queue (已有队列,非新增) │ + │ │ │ + │ ▼ │ + │ qh_proto_handle_task (协议任务) │ + │ │ │ + │ ┌──────────┴───────────┐ │ + │ ▼ ▼ │ + │ qh_parse_frame() qh_execute_cmd() │ + │ (解析器) (执行器) │ + │ │ │ │ + │ ▼ ▼ │ + │ qh_parsed_cmd_t ──▶ switch(cmd) ──▶ Device/ │ + │ (Tagged Union │ │ + │ 「清单」) ▼ │ + │ app_render / dev_display_* │ + │ dev_io_* / dev_rs232_voice │ + │ channel_send (cmd '1' 应答) │ + └──────────────────────────────────────────────────┘ + + 边界 = 结构体 qh_parsed_cmd_t(清单) + 解析器只产出清单(纯函数,不碰设备);执行器只消费清单(映射 + 调驱动) + 两者同步、同一任务上下文,无额外队列 / 无事件总线 +``` + +### B.4.2 清单结构体 `qh_parsed_cmd_t`(Tagged Union) + +**位置**:`app_qh_proto.h`(解析器与执行器的唯一契约;保持 `Device/` 无关,仅依赖 `stdint.h` 与 `channel_t` 前向声明)。 + +```c +/* app_qh_proto.h —— 解析器→执行器 的「清单」(Tagged Union) */ +#pragma once +#include + +/* 前向声明,避免引入 app_dispatch.h 造成循环依赖 */ +typedef struct channel channel_t; + +/* 命令枚举(ASCII '1'~'B' 映射为枚举,消除裸字符比较) */ +typedef enum { + QH_PCMD_HOST_QUERY = 0, /* '1' */ + QH_PCMD_SELF_CHECK, /* '2' */ + QH_PCMD_ONE_LINE, /* '3' */ + QH_PCMD_FULL_SCREEN, /* '4' */ + QH_PCMD_CLEAR, /* '5' */ + QH_PCMD_FIXED_DISPLAY, /* '6' */ + QH_PCMD_CIVIL_VOICE, /* '7' */ + QH_PCMD_BRIGHTNESS, /* '8' */ + QH_PCMD_VOLUME, /* '9' */ + QH_PCMD_PERIPHERAL, /* 'A' */ + QH_PCMD_VOICE, /* 'B' */ + QH_PCMD_NONE, +} qh_pcmd_t; + +/* 解析状态:结构级 / 语义级 分离 */ +typedef enum { + QH_PARSE_OK = 0, + QH_PARSE_ERR_FRAME, /* 帧结构非法(非 {..}) */ + QH_PARSE_ERR_CMD, /* 命令字非法 */ + QH_PARSE_ERR_PARAM, /* 参数越界 / 不合法 */ +} qh_parse_sta_t; + +/* 各命令已解码的协议域参数(只放协议语义,不放设备域值) */ +typedef struct { uint8_t color; uint8_t row; const uint8_t *text; uint8_t text_len; } qh_one_line_t; +typedef struct { uint8_t color; uint8_t x; uint8_t y; const uint8_t *text; uint8_t text_len; } qh_full_screen_t; +typedef struct { uint8_t type; const uint8_t *raw; uint8_t raw_len; } qh_fixed_t; /* type∈{0,1},原始 '|' 字段留执行器拆分 */ +typedef struct { uint8_t level; } qh_brightness_t; /* 0~5 */ +typedef struct { uint8_t level; } qh_volume_t; /* 1~5 */ +typedef struct { uint8_t ctrl; } qh_peripheral_t; /* bit0/1/2 */ +typedef struct { uint8_t idx; } qh_civil_t; /* 0~3 */ +typedef struct { uint8_t type; uint32_t amount_fen; } qh_fee_t; /* 5位BCD→分 */ + +typedef struct { + qh_pcmd_t cmd; /* 已映射为枚举 */ + qh_parse_sta_t sta; /* 结构/语义校验结果 */ + uint8_t data_len; /* 执行器仅用到此 */ + union { + qh_one_line_t one_line; + qh_full_screen_t full_screen; + qh_fixed_t fixed; + qh_brightness_t brightness; + qh_volume_t volume; + qh_peripheral_t peripheral; + qh_civil_t civil; + qh_fee_t fee; + /* HOST_QUERY / SELF_CHECK / CLEAR: 无参数 */ + } p; + const uint8_t *data; /* 逃生舱口:指向整帧 data 区起始,仅复杂命令(如 cmd '6') + 的执行器需按需截取子字段(如 '|' 分隔字段)时使用; + 简单命令的执行器只读本 union 内已解码字段,不碰此指针 */ +} qh_parsed_cmd_t; + +/* 边界 API(声明在此,实现分别在 parse.c / cmd.c) */ +qh_parsed_cmd_t qh_parse_frame(const uint8_t *raw, uint8_t raw_len); +void qh_execute_cmd(channel_t *ch, const qh_parsed_cmd_t *cmd); +``` + +> **设计要点**: +> - union 内只放**协议域已解码**值(颜色索引 0/1/2、亮度 0~5、bit 位、金额分),**不放** `COLOR_RED` / 硬件亮度级——那些是设备域,留给执行器映射。 +> - cmd `'6'` 解析器只做结构校验 + 保留 `raw` 指针,字段拆分 / 5 行格式化在执行器(属协议语义解释,本就在协议层)。 +> - `data` 指针指向 `frame_msg_t` 缓冲,执行器必须**同步消费**,`qh_parse_frame` 返回的是栈上 `qh_parsed_cmd_t`,`handle_task` 同步调用 `qh_execute_cmd` 后销毁。 + +### B.4.3 各模块实现样板 + +#### B.4.3.1 解析器 `app_qh_proto_parse.c` + +```c +/* app_qh_proto_parse.c —— 解析器:bytes → qh_parsed_cmd_t(清单) */ +#include "app_qh_proto.h" +#include "app_qh_proto_parse.h" + +static qh_pcmd_t qh_char_to_cmd(char c) +{ + switch (c) { + case '1': return QH_PCMD_HOST_QUERY; + case '2': return QH_PCMD_SELF_CHECK; + case '3': return QH_PCMD_ONE_LINE; + case '4': return QH_PCMD_FULL_SCREEN; + case '5': return QH_PCMD_CLEAR; + case '6': return QH_PCMD_FIXED_DISPLAY; + case '7': return QH_PCMD_CIVIL_VOICE; + case '8': return QH_PCMD_BRIGHTNESS; + case '9': return QH_PCMD_VOLUME; + case 'A': return QH_PCMD_PERIPHERAL; + case 'B': return QH_PCMD_VOICE; + default: return QH_PCMD_NONE; + } +} + +/* 纯函数:raw bytes → 协议域清单;不碰任何 Device/ 驱动、不回响应 */ +qh_parsed_cmd_t qh_parse_frame(const uint8_t *raw, uint8_t raw_len) +{ + qh_parsed_cmd_t cmd = {0}; + + /* 结构校验(probe 已确保整帧 {..},这里做最终防御) */ + if (raw_len < 4 || raw[0] != '{' || raw[raw_len - 1] != '}') { + cmd.sta = QH_PARSE_ERR_FRAME; + return cmd; + } + cmd.cmd = qh_char_to_cmd((char)raw[1]); + if (cmd.cmd == QH_PCMD_NONE) { cmd.sta = QH_PARSE_ERR_CMD; return cmd; } + + const uint8_t *data = &raw[3]; // 跳过 '{' + cmd + len + /* E2:len 字段(raw[2]) 与帧实际可载数据长度(raw_len-4) 必须一致。 + probe 已保证整帧 {..} 完整,但串口干扰可能让 len 字段与实际不符 → 防御性校验 */ + uint8_t frame_data_len = (raw_len > 4) ? (uint8_t)(raw_len - 4) : 0; // 实际 payload 字节数 + uint8_t declared_len = (raw_len > 4) ? (uint8_t)(raw[2] - '0') : 0; // 帧内 len 字段 + if (declared_len > frame_data_len) { // len 字段越界 → 视为坏帧 + cmd.sta = QH_PARSE_ERR_FRAME; + return cmd; + } + uint8_t data_len = declared_len; + cmd.data = data; + cmd.data_len = data_len; + cmd.sta = QH_PARSE_OK; + + switch (cmd.cmd) { + case QH_PCMD_ONE_LINE: + if (data_len < 2) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.one_line.color = data[0] - '0'; + cmd.p.one_line.row = data[1] - '1'; + cmd.p.one_line.text = &data[2]; + cmd.p.one_line.text_len = (data_len > 2) ? (data_len - 2) : 0; + if (cmd.p.one_line.color > 2 || cmd.p.one_line.row > 4) + cmd.sta = QH_PARSE_ERR_PARAM; + break; + case QH_PCMD_FULL_SCREEN: + if (data_len < 3) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.full_screen.color = data[0] - '0'; + cmd.p.full_screen.x = data[1]; + cmd.p.full_screen.y = data[2]; + cmd.p.full_screen.text = &data[3]; + cmd.p.full_screen.text_len = data_len - 3; + if (cmd.p.full_screen.color > 2) cmd.sta = QH_PARSE_ERR_PARAM; + break; + case QH_PCMD_BRIGHTNESS: + if (data_len < 1) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.brightness.level = data[0] - '0'; + if (cmd.p.brightness.level > 5) cmd.sta = QH_PARSE_ERR_PARAM; + break; + case QH_PCMD_VOLUME: + if (data_len < 1) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.volume.level = data[0] - '0'; + if (cmd.p.volume.level < 1 || cmd.p.volume.level > 5) cmd.sta = QH_PARSE_ERR_PARAM; + break; + case QH_PCMD_PERIPHERAL: + if (data_len < 1) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.peripheral.ctrl = data[0]; + break; + case QH_PCMD_CIVIL_VOICE: + if (data_len < 1) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.civil.idx = data[0] - '0'; + if (cmd.p.civil.idx > 3) cmd.sta = QH_PARSE_ERR_PARAM; + break; + case QH_PCMD_FIXED_DISPLAY: + if (data_len < 1) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + cmd.p.fixed.type = data[0] - '0'; + cmd.p.fixed.raw = &data[1]; + cmd.p.fixed.raw_len = (data_len > 1) ? (data_len - 1) : 0; + if (cmd.p.fixed.type > 1) cmd.sta = QH_PARSE_ERR_PARAM; // 0=客车, 1=货车 + break; + case QH_PCMD_VOICE: + if (data_len < 6) { cmd.sta = QH_PARSE_ERR_PARAM; break; } + /* E3:每位必须是 '0'~'9',否则减去 '0' 会得到负值/乱码金额,须丢弃 */ + for (int i = 1; i <= 5; i++) { + if (data[i] < '0' || data[i] > '9') { cmd.sta = QH_PARSE_ERR_PARAM; return cmd; } + } + cmd.p.fee.type = data[0] - '0'; + cmd.p.fee.amount_fen = (uint32_t)(data[1] - '0') * 10000 + + (uint32_t)(data[2] - '0') * 1000 + + (uint32_t)(data[3] - '0') * 100 + + (uint32_t)(data[4] - '0') * 10 + + (uint32_t)(data[5] - '0'); + break; + /* HOST_QUERY / SELF_CHECK / CLEAR: 无参数,仅 cmd + sta 即可 */ + default: + break; + } + return cmd; +} +``` + +#### B.4.3.2 执行器 `app_qh_proto_cmd.c` + +```c +/* app_qh_proto_cmd.c —— 执行器:qh_parsed_cmd_t → Device/ 驱动调用 */ +#include "app_qh_proto.h" +#include "app_qh_proto_cmd.h" +#include "app_qh_proto_voice.h" +#include "app_render.h" +#include "dev_display.h" +#include // memchr(cmd '6' 字段拆分) +#include "dev_io_ctrl.h" +#include "dev_rs232_voice.h" + +/* 协议域→设备域 映射表(执行器侧;与 LDI 套路一致) */ +static const display_color_t s_qh_color_map[] = { COLOR_RED, COLOR_GREEN, COLOR_YELLOW }; +#define QH_MAP_COLOR(i) (((i) < 3) ? s_qh_color_map[(i)] : COLOR_GREEN) +static const uint8_t s_qh_brightness_map[] = {0, 3, 4, 5, 7, 8}; +static const uint8_t s_qh_volume_map[] = {0, 0x31, 0x33, 0x35, 0x37, 0x39}; + +/* 执行器入口:handle_task 调用;解析失败直接丢弃(或此处回异常) */ +void qh_execute_cmd(channel_t *ch, const qh_parsed_cmd_t *cmd) +{ + if (cmd->sta != QH_PARSE_OK) return; + switch (cmd->cmd) { + case QH_PCMD_HOST_QUERY: qh_exec_host_query(ch); break; + case QH_PCMD_SELF_CHECK: qh_exec_self_check(ch); break; + case QH_PCMD_ONE_LINE: qh_exec_one_line(ch, &cmd->p.one_line); break; + case QH_PCMD_FULL_SCREEN: qh_exec_full_screen(ch, &cmd->p.full_screen); break; + case QH_PCMD_CLEAR: qh_exec_clear(ch); break; + case QH_PCMD_FIXED_DISPLAY: qh_exec_fixed(ch, &cmd->p.fixed); break; + case QH_PCMD_CIVIL_VOICE: qh_exec_civil_voice(ch, cmd->p.civil.idx); break; + case QH_PCMD_BRIGHTNESS: qh_exec_brightness(ch, cmd->p.brightness.level); break; + case QH_PCMD_VOLUME: qh_exec_volume(ch, cmd->p.volume.level); break; + case QH_PCMD_PERIPHERAL: qh_exec_peripheral(ch, cmd->p.peripheral.ctrl); break; + case QH_PCMD_VOICE: qh_exec_voice_fee(ch, cmd->p.fee.type, cmd->p.fee.amount_fen); break; + default: break; + } +} + +/* ---- 各命令执行器:只读清单 + 做映射 + 调 Device/ 驱动 ---- */ + +static void qh_exec_one_line(channel_t *ch, const qh_one_line_t *p) +{ + dev_display_t *d = dev_display_get(); + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, .x = 0, .y = (uint16_t)p->row * FONT_16, + .w = d ? d->screen_cols : 0, .h = FONT_16, + .color = QH_MAP_COLOR(p->color), + .text = (const char *)p->text, .len = p->text_len, + .font_size = FONT_16, .font_type = FONT_ST, .text_enc = FONT_ENC_GBK, + }); +} + +static void qh_exec_brightness(channel_t *ch, uint8_t level) +{ + dev_display_t *d = dev_display_get(); + if (!d) return; + if (level == 0) { + /* 自动档:不强制固定亮度,交由板级光敏自动调光(dev_light_sensor 钳位 4~7) */ + return; + } + /* 手动档:level∈1~5,映射表索引 = level-1 */ + dev_display_set_brightness(d, s_qh_brightness_map[level - 1]); +} + +static void qh_exec_peripheral(channel_t *ch, uint8_t ctrl) +{ + bool green = (ctrl & 0x01) && !(ctrl & 0x02); // bit0=绿, bit1=红(优先) + bool red = (ctrl & 0x02); + bool yellow = (ctrl & 0x04); + if (red) dev_io_lane_light(false); + else if (green) dev_io_lane_light(true); + dev_io_flash_light(yellow); +} + +/* qh_exec_civil_voice / qh_exec_voice_fee / qh_exec_full_screen / + qh_exec_clear / qh_exec_self_check / qh_exec_host_query 见 B.7 */ + +/* ---- cmd '6' 固定格式显示(核心命令)执行器 ---- */ +/* 解析器只填 p.fixed{type,raw,raw_len}(保留 '|' 原文字段),此处完成字段拆分 + 5 行构造 */ +static void qh_exec_fixed(channel_t *ch, const qh_fixed_t *p) +{ + /* 1. 按 '|' 拆分 raw(含 type 之后整段),最多 5 段 */ + const uint8_t *fields[6]; + uint8_t flen[6]; + int nfields = 0; + const uint8_t *cur = p->raw; + const uint8_t *end = p->raw + p->raw_len; + while (cur < end && nfields < 6) { + const uint8_t *sep = memchr(cur, '|', (size_t)(end - cur)); + fields[nfields] = cur; + flen[nfields] = (uint8_t)((sep ? sep : end) - cur); + nfields++; + if (!sep) break; + cur = sep + 1; + } + /* 2. 客车 5 行:车型/金额/余额/信息1/信息2;货车少 2 段时后两行空白 */ + dev_display_t *d = dev_display_get(); + for (int line = 0; line < 5; line++) { + const uint8_t *txt = (line < nfields) ? fields[line] : (const uint8_t *)""; + uint8_t tlen = (line < nfields) ? flen[line] : 0; + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, .x = 0, .y = (uint16_t)line * FONT_16, + .w = d ? d->screen_cols : 0, .h = FONT_16, + .color = COLOR_GREEN, + .text = (const char *)txt, .len = tlen, + .font_size = FONT_16, .font_type = FONT_ST, .text_enc = FONT_ENC_GBK, + }); + } + /* 3. 金额 > 0.5 元自动播报(需从字段提取金额,此处以第 1 段作示意; + 实际按裸机"金额: "后数字解析,对齐 B.7.5) */ + uint32_t amount_fen = 0; // TODO: 从 fields[1] 解析 "金额: 12.50 元" → 分 + if (p->type == 0 && nfields > 1) { + /* 客车:fields[1]=金额 ... 解析略,见 B.7.5 */ + } + qh_voice_fee_amount(amount_fen); // amount_fen < 50 时内部不播报 +} +``` + +#### B.4.3.3 语音辅助 `app_qh_proto_voice.c`(执行器侧) + +```c +/* app_qh_proto_voice.c —— 语音辅助(执行器侧) */ +#include "app_qh_proto_voice.h" +#include "dev_rs232_voice.h" + +static const struct { const uint8_t *gbk; uint8_t len; } s_qh_civil_phrases[] = { + { (const uint8_t *)"\xC4\xFA\xBA\xC3\xA3\xA1\xBB\xB6\xD3\xAD\xD0\xD0\xCA\xBB\xB9\xF3\xD6\xDD\xB8\xDF\xCB\xD9\xB9\xAB\xC2\xB7", 27 }, /* 您好!欢迎行驶贵州高速公路 */ + { (const uint8_t *)"\xC7\xEB\xB3\xF6\xCA\xBE\xCD\xA8\xD0\xD0\xBF\xA8", 12 }, /* 请出示通行卡 */ + { (const uint8_t *)"\xD0\xBB\xD0\xBB\xBA\xCF\xD7\xF7\xA3\xA1\xD7\xA3\xC4\xFA\xD2\xBB\xC2\xB7\xC6\xBD\xB0\xB2", 22 }, /* 谢谢合作!祝您一路平安 */ + { (const uint8_t *)"\xBD\xBB\xD2\xD7\xB2\xBB\xB3\xC9\xB9\xA6\xA3\xAC\xC7\xEB\xD7\xDF\xC8\xCB\xB9\xA4\xB3\xB5\xB5\xC0", 24 }, /* 交易不成功,请走人工车道 */ +}; + +void qh_voice_civil(uint8_t idx) +{ + if (idx >= (sizeof(s_qh_civil_phrases) / sizeof(s_qh_civil_phrases[0]))) return; + dev_rs232_voice_play(s_qh_civil_phrases[idx].gbk, s_qh_civil_phrases[idx].len); +} + +void qh_voice_fee_amount(uint32_t amount_fen) +{ + if (amount_fen < 50) return; // 不足 0.5 元不播报(裸机对齐) + uint32_t yuan = amount_fen / 100; + char text[64]; + int n = snprintf(text, sizeof(text), "您好请交费%lu元", (unsigned long)yuan); + if (n > 0 && n < (int)sizeof(text)) + dev_rs232_voice_play((const uint8_t *)text, (uint16_t)n); +} +``` + +#### B.4.3.4 核心 `app_qh_proto.c`(handle_task 同步 parse→execute) + +```c +/* app_qh_proto.c —— 核心:initcall、probe、handle_task */ +#include "app_qh_proto.h" +#include "app_qh_proto_parse.h" +#include "app_qh_proto_cmd.h" +#include "app_dispatch.h" + +/* (initcall / probe / 通道绑定 见 B.1~B.5,此处仅展示任务主循环) */ + +void qh_proto_handle_task(void *argument) +{ + /* 缓冲复用:_buf 同时充当「队列 item 体」与「frame_msg_t + payload」两种视图。 + FRAME_DATA_MAX_LEN(=1044) 是 app_dispatch.h 定义的最大 item 尺寸, + 恰好覆盖 frame_msg_t 头(8B) + 最大 payload(1036B),与队列 item size 严格一致。 + ——评审误提「应改为 sizeof(frame_msg_t)+QH_PAYLOAD_MAX」:该宏不存在,且 + FRAME_DATA_MAX_LEN 已含头开销,改用它反而要凭空引入未定义宏,故保持现状。 */ + static uint8_t _buf[FRAME_DATA_MAX_LEN]; + frame_msg_t *msg = (frame_msg_t *)_buf; + g_qh_proto_frame_queue = osMessageQueueNew(2, FRAME_DATA_MAX_LEN, &s_qh_queue_attr); + app_proto_set_frame_queue(s_qh_mask, g_qh_proto_frame_queue); + + for (;;) { + if (osOK != osMessageQueueGet(g_qh_proto_frame_queue, msg, NULL, osWaitForever)) + continue; + /* Parse → Execute:同步、同一任务上下文,无额外队列 / 无事件总线 */ + qh_parsed_cmd_t cmd = qh_parse_frame(msg->data, msg->data_len); + qh_execute_cmd(msg->ch, &cmd); + } +} +``` + +### B.4.4 数据流(文字版) + +``` +串口 RS232 收包 (CH_ID_RS232, USART3, 9600~115200bps) + -> app_channel_dispatch + -> ring_buffer (id=1, RB_GROUP_PROTO) + -> frame_dispatch_task + -> qh_proto_probe_frame + +-- '{' -> 扫描到 '}' -> 长度校验 -> READY (ASCII 帧) + -> g_qh_proto_frame_queue (已有队列,非新增) + -> qh_proto_handle_task + +-- qh_parse_frame() 解析器:bytes → qh_parsed_cmd_t(清单) + +-- qh_execute_cmd() 执行器: + | +-- 直接调用 Device/ 驱动 + 协议→设备映射: + | | app_render / dev_display_* (HUB75 显示) + | | dev_io_lane_light / dev_io_flash_light (通信灯/报警灯) + | | dev_display_set_brightness (亮度) + | | dev_rs232_voice_play/volume (TTS 语音板,新增 232_voice) + | +-- qh_proto_voice_*() 语音辅助(执行器侧) + +-- channel_send(ch, rsp, len) (cmd '1' 串口回发应答帧) +``` + +### B.4.5 RS232 串口模型 + +QH 协议**仅使用 RS232 串口**,不涉及 UDP/TCP: + +``` +CH_ID_RS232 (index=1) -> USART3 -> 9600~115200bps(默认9600) + -> 承载 QH 协议帧收发 + -> 同时承担 cmd '1' 应答帧回发 + (注:枚举见 app_dispatch.h `channel_id_t`;index=5 是 CH_ID_MQTT,切勿混淆) +``` + +串口参数: +| 参数 | 默认值 | 可配置 | +|------|--------|--------| +| 波特率 | 9600 | 可通过 cfg 保存 | +| 数据位 | 8 | 固定 | +| 停止位 | 1 | 固定 | +| 校验位 | 无 | 固定 | + +--- + +## B.5 Probe 与帧定界 + +### B.5.1 ASCII 帧格式 + +``` +字段: { cmd len data[0..N] } +长度: 1 1 1 N(=len) 1 +示例: 7B 33 0A xx xx xx... 7D +``` + +| 字段 | 长度 | 说明 | +|------|------|------| +| 帧头 | 1B | 固定 `'{'` (0x7B) | +| 命令字 | 1B | `'1'~'B'` (0x31~0x42) | +| 参数长度 | 1B | ASCII 数字 `'0'~'9'`,表示 data 字段字节数 | +| 参数数据 | 变长 | data[0]~data[N-1],N = len 字段的 ASCII 值 | +| 帧尾 | 1B | 固定 `'}'` (0x7D) | + +**总帧长 = 3 + len 字节** + +### B.5.2 probe 逻辑 + +```c +qh_proto_probe_sta_t qh_proto_probe_frame(channel_t *ch, ring_buffer_t *rb, + uint32_t *frame_len, uint8_t *aux) +{ + // 1. 读首字节,检查 '{' + // 2. 读 cmd 字节(偏移1),检查 '1'~'B' + // 3. 读 len 字节(偏移2),解析为数字 0~99 + // 4. frame_len = 3 + len + // 5. 检查 rb 中是否有 frame_len 字节可用 + // -> 有:PROTO_PROBE_READY + // -> 无:PROTO_PROBE_WAIT + // 6. 首字节非 '{' -> PROTO_PROBE_FAKE(跳过1字节) +} +``` + +**无需花括号深度扫描**——QH 帧格式简单,`{` 和 `}` 不会在 data 区出现(data 是可显示 ASCII 文本或空格)。 + +### B.5.3 帧校验 + +QH 协议**无 CRC 校验**。probe 阶段仅校验: +1. 首字节 = `'{'` +2. 命令字 ∈ `'1'~'B'`(合法范围) +3. 长度字段为 ASCII 数字 `'0'~'9'`,且 `len <= FRAME_DATA_MAX_LEN` +4. 帧尾 = `'}'`(通过 rb_peek 检查尾字节) + +--- + +## B.6 命令实现矩阵 + +### B.6.1 命令表(Parse/Execute 视角) + +每条命令在「解析器」产出清单字段、在「执行器」消费清单调设备。下表给出**解析器写入的清单字段**与**执行器调用的设备能力**: + +| cmd | ASCII | 清单字段(解析器写入) | 执行器入口 | 设备能力调用 | 一期 | +|-----|-------|------------------------|------------|-------------|------| +| `'1'` | 0x31 | 无(仅 `cmd`+`sta`) | `qh_exec_host_query` | `channel_send` | **必做** | +| `'2'` | 0x32 | 无 | `qh_exec_self_check` | `app_render(RENDER_FILL)` + `dev_rs232_voice_play` | **必做** | +| `'3'` | 0x33 | `p.one_line{color,row,text,text_len}` | `qh_exec_one_line` | `app_render(RENDER_TEXT)` | **必做** | +| `'4'` | 0x34 | `p.full_screen{color,x,y,text,text_len}` | `qh_exec_full_screen` | `app_render(RENDER_TEXT, x, y)` | **必做** | +| `'5'` | 0x35 | 无 | `qh_exec_clear` | `dev_display_fill` + `commit_frame` | **必做** | +| `'6'` | 0x36 | `p.fixed{type,raw,raw_len}` | `qh_exec_fixed` | `app_render` x5 + `dev_rs232_voice_play` | **必做** | +| `'7'` | 0x37 | `p.civil.idx` | `qh_exec_civil_voice` | `dev_rs232_voice_play` | **必做** | +| `'8'` | 0x38 | `p.brightness.level` | `qh_exec_brightness` | `dev_display_set_brightness` | **必做** | +| `'9'` | 0x39 | `p.volume.level` | `qh_exec_volume` | `dev_rs232_voice_volume` | **必做** | +| `'A'` | 0x41 | `p.peripheral.ctrl` | `qh_exec_peripheral` | `dev_io_lane_light` + `dev_io_flash_light` | **必做** | +| `'B'` | 0x42 | `p.fee{type,amount_fen}` | `qh_exec_voice_fee` | `dev_rs232_voice_play` | **必做** | + +> 旧版「`qh_proto_cmd_*` 一把梭 handler」已废弃,拆分为 `qh_parse_frame`(解析器,产出清单)与 `qh_exec_*`(执行器,消费清单)。命令分派不再用函数指针表,而是 `qh_execute_cmd` 内 `switch(cmd)` 直接映射到执行器函数(见 B.4.3.2)。 + +### B.6.2 解析器→执行器分派(无函数指针表) + +分派在 `qh_execute_cmd` 内由 `switch(cmd)` 完成(`cmd` 已是枚举,比函数指针表更直接、可单步): + +```c +void qh_execute_cmd(channel_t *ch, const qh_parsed_cmd_t *cmd) +{ + if (cmd->sta != QH_PARSE_OK) return; // 解析失败:清单已带 sta + switch (cmd->cmd) { + case QH_PCMD_HOST_QUERY: qh_exec_host_query(ch); break; + case QH_PCMD_SELF_CHECK: qh_exec_self_check(ch); break; + case QH_PCMD_ONE_LINE: qh_exec_one_line(ch, &cmd->p.one_line); break; + case QH_PCMD_FULL_SCREEN: qh_exec_full_screen(ch, &cmd->p.full_screen); break; + case QH_PCMD_CLEAR: qh_exec_clear(ch); break; + case QH_PCMD_FIXED_DISPLAY: qh_exec_fixed(ch, &cmd->p.fixed); break; + case QH_PCMD_CIVIL_VOICE: qh_exec_civil_voice(ch, cmd->p.civil.idx); break; + case QH_PCMD_BRIGHTNESS: qh_exec_brightness(ch, cmd->p.brightness.level); break; + case QH_PCMD_VOLUME: qh_exec_volume(ch, cmd->p.volume.level); break; + case QH_PCMD_PERIPHERAL: qh_exec_peripheral(ch, cmd->p.peripheral.ctrl); break; + case QH_PCMD_VOICE: qh_exec_voice_fee(ch, cmd->p.fee.type, cmd->p.fee.amount_fen); break; + default: break; + } +} +``` + +解析器 `qh_parse_frame` 内用 `qh_char_to_cmd()` 把 ASCII 命令字映射为枚举(见 B.4.3.1),无需运行时遍历表。 + +--- + +## B.7 关键命令详细设计 + +> 所有设备操作均直接调用 `Device/` 驱动,不使用 `dev_service`。 +> **本节每命令拆为「解析(解析器 `qh_parse_frame`,见 B.4.3.1)」+「执行(执行器 `qh_exec_*`,见 B.4.3.2)」两部分**:解析器把 raw bytes 填入清单字段,执行器只读清单字段做映射 + 调驱动。解析器代码已统一在 B.4.3.1,本节不再重复,只展示**执行器**实现与协议要点。 + +### B.7.0 协议层本地映射表(放 `app_qh_proto_cmd.c` 执行器侧) + +```c +#include "app_render.h" +#include "dev_display.h" +#include "dev_io_ctrl.h" +#include "dev_rs232_voice.h" + +/* 协议色码 0/1/2 -> display_color_t (红/绿/黄) */ +static const display_color_t s_qh_color_map[] = { + [0] = COLOR_RED, [1] = COLOR_GREEN, [2] = COLOR_YELLOW, +}; +#define QH_MAP_COLOR(idx) (((idx) < 3) ? s_qh_color_map[(idx)] : COLOR_GREEN) + +/* 亮度:协议 1~5 -> 硬件 0~7(dev_display 范围 0~7);协议 0=自动(走光敏),不入表 */ +static const uint8_t s_qh_brightness_map[] = {3, 4, 5, 7, 8}; /* 索引 = level-1 */ + +/* 音量:协议 1~5 -> 语音板音量标签 */ +static const uint8_t s_qh_volume_map[] = {0, 0x31, 0x33, 0x35, 0x37, 0x39}; +``` + +### B.7.1 cmd '1' — 主机查询 + +**协议格式**: +- 发送:`{1 00}`(无参数) +- 应答:`{1 01 00}`(正常)/ `{1 01 01}`(异常) + +**解析(解析器)**:无参数,清单仅含 `cmd=QH_PCMD_HOST_QUERY` + `sta=OK`。 +**执行(执行器)**(应答帧走串口通道,非设备能力): + +```c +static void qh_exec_host_query(channel_t *ch) +{ + static const uint8_t rsp[] = { '{', '1', '0', '1', '0', '0', '}' }; + channel_send(ch, (uint8_t *)rsp, sizeof(rsp)); +} +``` + +**裸机对齐**:裸机始终返回"正常"(0x00),不检查实际设备状态。STD 工程同样返回固定值。 + +### B.7.2 cmd '2' — 自检 + +**协议格式**: +- 发送:`{2 00}`(无参数) +- 行为:全屏显示固定内容 + 语音播报 + +**解析**:无参数,清单仅含 `cmd=QH_PCMD_SELF_CHECK`。 +**执行**: + +```c +static void qh_exec_self_check(channel_t *ch) +{ + app_render(&(render_cfg_t){.type = RENDER_FILL, .color = COLOR_YELLOW}); + + static const uint8_t voice_text[] = "系统正在加电自检"; + dev_rs232_voice_play(voice_text, sizeof(voice_text) - 1); +} +``` + +### B.7.3 cmd '3' — 单行显示 + +**协议格式**: +- 发送:`{3 len color row text...}` +- color: `'0'`=红, `'1'`=绿, `'2'`=黄 +- row: `'1'`~`'5'`(对应设备行 0~4) +- text: GBK 文本 + +**裸机行为(已确认差异)**: +- 裸机 `inbuf[3]` 做 `fontColor = inbuf[3] - 0x30 + 1`(协议值 0→设备值 1) +- STD 工程 `display_color_t` 对齐:`0`→COLOR_RED, `1`→COLOR_GREEN, `2`→COLOR_YELLOW + +**解析(解析器写入 `p.one_line`)**:`color=data[0]-'0'`,`row=data[1]-'1'`,`text=&data[2]`,`text_len=len-2`;越界置 `ERR_PARAM`。 +**执行(执行器读 `p.one_line`)**: + +```c +static void qh_exec_one_line(channel_t *ch, const qh_one_line_t *p) +{ + dev_display_t *d = dev_display_get(); + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, .x = 0, .y = (uint16_t)p->row * FONT_16, + .w = d ? d->screen_cols : 0, .h = FONT_16, + .color = QH_MAP_COLOR(p->color), // 协议→设备映射在执行器 + .text = (const char *)p->text, .len = p->text_len, + .font_size = FONT_16, .font_type = FONT_ST, .text_enc = FONT_ENC_GBK, + }); +} +``` + +### B.7.4 cmd '4' — 全屏可编辑显示 + +**协议格式**: +- 发送:`{4 len color X Y text...}` +- color: `'0'`=红, `'1'`=绿, `'2'`=黄 +- X: 起始 X 坐标(0~屏幕宽度) +- Y: 起始 Y 坐标(0~屏幕高度) +- text: GBK 文本,`\n`(0x0A) 换行 +- 空格(0x20) 填充空位 + +**解析(写入 `p.full_screen`)**:`color/data[0]`,`x=data[1]`,`y=data[2]`,`text=&data[3]`,`text_len=len-3`。 +**执行**:逐行拆分 text(按 `\n`),每行调用 `app_render(RENDER_TEXT, x, y + line*row_height, ...)`。 + +```c +static void qh_exec_full_screen(channel_t *ch, const qh_full_screen_t *p) +{ + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, .x = p->x, .y = p->y, + .color = QH_MAP_COLOR(p->color), + .text = (const char *)p->text, .len = p->text_len, + .font_size = FONT_16, .font_type = FONT_ST, .text_enc = FONT_ENC_GBK, + }); +} +``` + +### B.7.5 cmd '6' — 固定格式显示(核心命令) + +**协议格式**: +- 发送:`{6 len type|车型|金额|余额|信息1|信息2}` +- type: `'0'`=客车, `'1'`=货车 +- 字段以 `'|'`(0x7C) 分隔 + +**解析(解析器)**:只做结构校验 + 填 `p.fixed{type, raw, raw_len}`(保留原始 `|` 字段指针),**不**在此拆分字段。 +**执行(执行器)**:在 `qh_exec_fixed` 内完成 `|` 字段拆分与 5 行格式化(属协议语义解释,本就在协议层)。 + +**客车模式(type='0')— 5 行**: + +| 行号 | 内容 | 格式示例 | +|------|------|---------| +| 1 | 车型 | `车型: 1 型` | +| 2 | 金额 | `金额: 12.50 元` | +| 3 | 余额 | `余额: 88.00 元` | +| 4 | 信息1 | 协议传入的文本 | +| 5 | 信息2 | 协议传入的文本 | + +**货车模式(type='1')— 5 行**: + +| 行号 | 内容 | 格式示例 | 条件 | +|------|------|---------|------| +| 1 | 车型 | `车型: 3 型` | 始终显示 | +| 2 | 金额 | `金额: 25.80 元` | 始终显示 | +| 3 | 余额 | `余额: 50.00 元` | 始终显示 | +| 4 | 总重 | `总重: 12.50 吨` | > 0.1 吨才显示,否则空白行 | +| 5 | 超重 | `超重: 2.30 吨` | > 0.1 吨才显示,否则空白行 | + +**金额格式化**:`tmp_amount + 0.001f`(裸机对齐:补偿浮点精度误差) + +**语音播报**:金额 > 0.5 元时自动播报"您好请交费XX.XX元"(通过 `dev_rs232_voice_play`,调 `qh_voice_fee_amount`)。 + +**执行要点**:协议层完成 `\|` 字段解析与 5 行构造,每行用 `app_render(RENDER_TEXT, y=line*FONT_16, ...)` 显示;金额超阈值时调 `dev_rs232_voice_play`。完整执行器实现见 B.4.3.2 的 `qh_exec_fixed`(含 `memchr` 按 `|` 拆分、客车/货车 5 行构造、金额阈值播报骨架)。 + +### B.7.6 cmd '7' — 礼貌用语 + +**协议格式**: +- 发送:`{7 01 index}` +- index: `'0'`~`'3'` + +**解析(写入 `p.civil.idx`)**:`idx=data[0]-'0'`,越界置 `ERR_PARAM`。 +**执行(读 `p.civil.idx`)**: + +索引映射表(对齐裸机 `cmd_civil_voice_play`,**协议层持有**): + +| index | GBK 文本 | +|-------|---------| +| `'0'` | `您好!欢迎行驶贵州高速公路` | +| `'1'` | `请出示通行卡` | +| `'2'` | `谢谢合作!祝您一路平安` | +| `'3'` | `交易不成功,请走人工车道` | + +**注意**:协议文档写的是"欢迎行驶高速公路",但裸机实际使用的是"贵州高速公路"。**STD 工程对齐裸机**。 + +```c +static void qh_exec_civil_voice(channel_t *ch, uint8_t idx) +{ + qh_voice_civil(idx); // 见 app_qh_proto_voice.c(B.4.3.3) +} +``` + +### B.7.7 cmd '8' — 亮度设置 + +**协议格式**: +- 发送:`{8 01 level}` +- level: `'0'`~`'5'` + +**解析(写入 `p.brightness.level`)**:`level=data[0]-'0'`,越界置 `ERR_PARAM`。 +**执行(读 `p.brightness.level`)**:`level==0` 为自动档——不写固定亮度,交由板级光敏自动调光(`app_light_sensor` 任务按 4~7 钳位驱动);`level∈1~5` 为手动档,查表映射到硬件 0~7。 + +**映射表**(对齐裸机 `cmd_screen_bright_set`,**协议层查表**;仅手动档,索引 = level-1): + +| 协议值 | 含义 | 硬件级(dev_display 0~7) | 表索引 | +|--------|------|--------------------------|--------| +| `'0'` | 自动 | 不走此表(光敏驱动) | — | +| `'1'` | 手动 1 | 3 | 0 | +| `'2'` | 手动 2 | 4 | 1 | +| `'3'` | 手动 3 | 5 | 2 | +| `'4'` | 手动 4 | 7 | 3 | +| `'5'` | 手动 5 | 8 | 4 | + +```c +static void qh_exec_brightness(channel_t *ch, uint8_t level) +{ + dev_display_t *d = dev_display_get(); + if (!d) return; + if (level == 0) { + /* 自动档:不强制固定亮度,交由板级光敏自动调光(dev_light_sensor 钳位 4~7) */ + return; + } + /* 手动档:level∈1~5,映射表索引 = level-1 */ + dev_display_set_brightness(d, s_qh_brightness_map[level - 1]); +} +``` + +### B.7.8 cmd '9' — 音量设置 + +**协议格式**: +- 发送:`{9 01 level}` +- level: `'1'`~`'5'` + +**解析(写入 `p.volume.level`)**:`level=data[0]-'0'`,越界置 `ERR_PARAM`。 +**执行(读 `p.volume.level`)**:协议层做 1~5 → 音量标签映射,调用 `dev_rs232_voice_volume(mapped_level)`。 + +**映射表**(对齐裸机 `cmd_voice_volume_set`,**协议层查表**): + +| 协议值 | 音量标签 | +|--------|---------| +| `'1'` | 0x31 | +| `'2'` | 0x33 | +| `'3'` | 0x35 | +| `'4'` | 0x37 | +| `'5'` | 0x39 | + +```c +static void qh_exec_volume(channel_t *ch, uint8_t level) +{ + dev_rs232_voice_volume(s_qh_volume_map[level]); +} +``` + +### B.7.9 cmd 'A' — 外设控制 + +**协议格式**: +- 发送:`{A 01 ctrl}` +- ctrl: 位域控制字节 + +**位域定义**(对齐裸机 `cmd_ws`): + +| bit | 设备 | 1=开 | 0=关 | +|-----|------|------|------| +| bit0 | 绿灯 | 绿灯亮 | 绿灯灭 | +| bit1 | 红灯 | 红灯亮 | 红灯灭 | +| bit2 | 黄闪 | 黄闪开 | 黄闪关 | + +**裸机行为**:绿灯和红灯可同时亮(位独立控制)。STD 工程中 `dev_io_lane_light(green)` 为互斥接口(绿灯优先)。对齐裸机:bit0=1 且 bit1=0 -> 绿灯;bit1=1 -> 红灯(优先)。 + +**解析(写入 `p.peripheral.ctrl`)**:`ctrl=data[0]`。 +**执行(读 `p.peripheral.ctrl`)**: + +```c +static void qh_exec_peripheral(channel_t *ch, uint8_t ctrl) +{ + bool green = (ctrl & 0x01) && !(ctrl & 0x02); // bit0=绿, bit1=红(优先) + bool red = (ctrl & 0x02); + bool yellow = (ctrl & 0x04); + + if (red) dev_io_lane_light(false); // 红灯 + else if (green) dev_io_lane_light(true); // 绿灯 + dev_io_flash_light(yellow); +} +``` + +### B.7.10 cmd 'B' — 费额语音播报 + +**协议格式**: +- 发送:`{B 06 type d0 d1 d2 d3 d4}` +- type: `'0'`=播报金额, `'1'`=播报剩余次数 +- d0~d4: 5 位数字(万/千/百/十/个) + +**解析(写入 `p.fee`)**:`type=data[0]-'0'`,`amount_fen` = 5 位 BCD 合成(见 B.4.3.1)。 +**执行(读 `p.fee`)**: + +```c +static void qh_exec_voice_fee(channel_t *ch, uint8_t type, uint32_t amount_fen) +{ + (void)type; // '1'=剩余次数;本期对齐裸机仅实现金额播报 + qh_voice_fee_amount(amount_fen); // 见 app_qh_proto_voice.c(B.4.3.3 / B.8.2) +} +``` + +--- + +## B.8 语音子系统 + +### B.8.1 青海语音模式 + +青海协议使用**纯文本 TTS 模式**(与 CQ 的索引组合模式不同): + +| 特征 | 青海 | 重庆 | +|------|------|------| +| TTS 方式 | **GBK 文本直接合成** | 索引组合 + 分隔符 | +| 费用播报 | "您好请交费XX.XX元" | 索引组合 | +| 预置短语 | 4 条(cmd '7') | 数十条 | + +### B.8.2 费额语音文本构造(执行器侧,`app_qh_proto_voice.c`) + +**裸机对齐**:金额 + 0.001f(浮点精度补偿),整数和小数部分分别提取。 + +```c +void qh_voice_fee_amount(uint32_t amount_fen) +{ + if (amount_fen < 50) return; // 不足 0.5 元不播报 + uint32_t yuan = amount_fen / 100; + char text[64]; + int n = snprintf(text, sizeof(text), "您好请交费%lu元", (unsigned long)yuan); + if (n > 0 && n < (int)sizeof(text)) + dev_rs232_voice_play((const uint8_t *)text, (uint16_t)n); +} +``` + +### B.8.3 TTS 帧格式(新增 `232_voice` 模块负责) + +TTS 语音通过 RS232 发送给语音控制板。帧格式: + +``` +FD | 00 | [Len+2] | 01 | 01 | GBK text bytes... +``` + +由 `Device/Comm/dev_rs232_voice.c` 内部的 `dev_rs232_voice_play` 组装并发送,协议层只传 GBK 文本与长度(详见 Part A A.5)。 + +--- + +## B.9 设备能力桥接 + +`app_qh_proto_cmd.c`(**执行器**)的职责是**将 QH 协议语义(来自清单)直接转发到 `Device/` 驱动**,而非新增共享服务层。 + +**原则**:执行器**直接调用** `app_render` / `dev_display_*` / `dev_io_ctrl` / `dev_rs232_voice_*`,不引入 `dev_service_*` 中间层。协议特有的映射(颜色、亮度、音量)以本地查表方式放在执行器。清单 `qh_parsed_cmd_t` 是解析器与执行器之间**唯一的耦合点**。 + +**桥接示例**(执行器读清单,做映射 + 调驱动): + +```c +static void qh_exec_one_line(channel_t *ch, const qh_one_line_t *p) +{ + dev_display_t *d = dev_display_get(); + app_render(&(render_cfg_t){ + .type = RENDER_TEXT, .x = 0, .y = (uint16_t)p->row * FONT_16, + .color = QH_MAP_COLOR(p->color), // 协议→设备映射在执行器 + .text = (const char *)p->text, .len = p->text_len, + .font_size = FONT_16, .font_type = FONT_ST, .text_enc = FONT_ENC_GBK, + }); +} + +static void qh_exec_peripheral(channel_t *ch, uint8_t ctrl) +{ + bool green = (ctrl & 0x01) && !(ctrl & 0x02); + bool red = (ctrl & 0x02); + bool yellow = (ctrl & 0x04); + if (red) dev_io_lane_light(false); + else if (green) dev_io_lane_light(true); + dev_io_flash_light(yellow); +} +``` + +--- + +## B.10 配置与默认值 + +| 项 | 默认值 | 落点 | +|----|--------|------| +| 串口波特率 | 9600 | `app_qh_proto_cfg` | +| 显示行数上限 | 5 | 硬编码 | +| 字号 | 16 点阵 | 硬编码(裸机固定 16 点阵) | +| 字体 | 宋体 | 硬编码(裸机默认宋体) | +| 上电欢迎语 | "祝您一路平安" | 硬编码 GBK | + +--- + +## B.11 已确认问题汇总 + +| 问题 | 确认结果 | +|------|----------| +| Q1. 与 LDI 的产品关系 | **编译期互斥**,运行时只执行一种 | +| Q2. 通信通道 | **RS232 串口**(CH_ID_RS232, USART3) | +| Q3. 语音通道 | **RS232 串口**(`232_voice` 模块封装,经语音板物理帧) | +| Q4. 字号 | **固定 16 点阵**,协议不支持字号选择 | +| Q5. 字体 | **固定宋体**,协议不支持字体选择 | +| Q6. 行号 | **1~5**(对应设备行 0~4) | +| Q7. cmd '1' 应答 | **固定返回正常**,不检查实际设备状态(走 `channel_send`) | +| Q8. cmd '2' 自检 | 全屏黄色 + 语音"系统正在加电自检" | +| Q9. cmd '7' 礼貌用语 | **对齐裸机**,使用"贵州高速公路"而非协议文档的"高速公路" | +| Q10. 亮度映射 | **查表法**(协议层),裸机 `cmd_screen_bright_set` 的 switch-case | +| Q11. 音量映射 | **查表法**(协议层),裸机 `cmd_voice_volume_set` 的 switch-case | +| Q12. 外设控制 | **位独立控制**,红灯优先于绿灯(对齐裸机) | +| Q13. 费额语音 | **纯文本 TTS**,"您好请交费XX.XX元" | +| Q14. 金额精度 | **+0.001f 补偿**(裸机对齐) | +| Q15. 上电欢迎语 | **"祝您一路平安"** 显示 + 语音 | +| Q16. 帧无校验 | QH 协议**无 CRC**,probe 仅校验帧头帧尾 | +| Q17. cmd '4' X/Y 坐标 | 裸机忽略 X/Y(硬编码 0,0),STD 工程**应透传到渲染层** | +| Q18. RS232 波特率 | **9600~115200 可调**,默认 9600 | +| Q19. cmd '6' 货车总重/超重 | **0 吨不显示**(显示空白行),对齐裸机 | +| Q20. 文件名 | **`app_qh_proto_*` 样式** | +| Q21. 设备层 | **不引入 `dev_service` 共享层**;直接复用 `Device/` 驱动,仅新增 `232_voice` | + +--- + +## B.12 风险与约束 + +1. **串口粘包**:RS232 是流式传输,`{` 和 `}` 可能跨多个收包周期。probe 必须支持跨包扫描。 +2. **帧无校验**:QH 协议无 CRC,串口干扰可能导致误解析。考虑在 probe 中增加 cmd 字节合法性检查(`'1'~'B'`)作为弱校验。 +3. **浮点运算**:cmd '6'/'B' 的金额解析使用 `strtod` / `sprintf`,需要 `-lm` 链接。FreeRTOS 堆 32KB,限制帧长。 +4. **编码**:源文件中文故障串必须按 GB2312 字节写入或用 `\x` 转义。 +5. **内存**:cmd '6' 的 5 行缓冲区需要 5x64=320 字节栈空间。FreeRTOS 任务栈需 >= 512 字节。 +6. **裸机差异**:cmd '7' 的"贵州高速公路"与协议文档不一致,需对齐裸机行为。 +7. **无 `dev_service` 层**:设备调用分散在协议命令处理函数中,映射表以本地 `static const` 形式存在于 `app_qh_proto_cmd.c`,注意保持单一来源。 + +--- + +## B.13 实施阶段与验收 + +### 阶段 0 — 设备缺口补充 + +- 新增 `Device/Comm/dev_rs232_voice.c` + `Device/Inc/dev_rs232_voice.h` +- 实现 `dev_rs232_voice_play` / `dev_rs232_voice_volume` +- Makefile 将 `dev_rs232_voice.c` 编入 QH(及 CQ)条件组 + +**验收**:`make PROTO=QH -j8` 通过,`dev_rs232_voice_play` 可被调用;LDI/CQ 不受影响。 + +### 阶段 1 — 骨架 + +- 目录 + Makefile 条件编译 +- `app_qh_proto.c`:initcall、probe、handle_task(同步 `parse→execute`) +- `app_qh_proto_parse.c`:清单结构体 `qh_parsed_cmd_t` + `qh_parse_frame`(先只覆盖 cmd `'1'`/`'3'`/`'8'` 验证骨架) +- `app_qh_proto_cmd.c`:`qh_execute_cmd` + `qh_exec_*` 执行器骨架 +- RS232 通道绑定(`CH_ID_RS232`) + +**验收**:`make PROTO=QH -j8` 通过;发送 cmd `'1'`/`'3'` 验证 parse→execute→device 全链路。 + +### 阶段 2 — 基础命令 + +- cmd '1' 查询应答(`channel_send`) +- cmd '3' 单行显示(`app_render`) +- cmd '5' 清屏(`dev_display_fill` + `commit_frame`) +- cmd '8' 亮度(`dev_display_set_brightness`) +- cmd '9' 音量(`dev_rs232_voice_volume`) + +**验收**:串口工具发送对应帧,屏显与裸机对照。 + +### 阶段 3 — 核心显示 + 语音 + +- cmd '6' 固定格式显示(客车/货车) +- cmd '7' 礼貌用语 +- cmd 'B' 费额语音 +- cmd '4' 全屏可编辑显示 + +**验收**:上位机发送完整费额数据,屏显 + 语音与裸机对照。 + +### 阶段 4 — 外设 + 收尾 + +- cmd 'A' 外设控制(`dev_io_lane_light` / `dev_io_flash_light`) +- cmd '2' 自检 +- 上电欢迎语 +- LDI 不受影响回归 +- 文档归档 + +**验收**:全部 11 条命令逐项验证通过。 + +--- + +## B.14 验收标准 + +| 验收项 | 标准 | +|--------|------| +| `app_qh_proto.c/h` 编译 | `make PROTO=QH -j8` 通过 | +| Parse/Execute 分离 | 解析逻辑仅在 `app_qh_proto_parse.c`(`qh_parse_frame` 不调用任何 `Device/` 驱动);设备调用仅在 `app_qh_proto_cmd.c`(执行器) | +| 清单为唯一边界 | `handle_task` 仅 `qh_parse_frame()` + `qh_execute_cmd()`;无函数指针命令表、无事件队列 | +| 无 `dev_service` 层 | 全仓无 `dev_service_*` 调用 | +| `232_voice` 模块 | `dev_rs232_voice_play` / `dev_rs232_voice_volume` 编译可用 | +| LDI 不受影响 | `make PROTO=LDI -j8` 编译不变 | +| CQ 不受影响 | `make PROTO=CQ -j8` 编译不变 | +| cmd '1' 查询 | 串口发送 `{1 00}`,收到 `{1 01 00}` | +| cmd '3' 单行显示 | 发送文本,对应行正确显示 | +| cmd '5' 清屏 | 发送后全屏清除 | +| cmd '6' 客车 | 费额信息正确显示 + 语音播报 | +| cmd '6' 货车 | 费额/总重/超重正确显示 + 语音播报 | +| cmd 'A' 外设 | 绿灯/红灯/黄闪正确响应 | +| 亮度 0~5 | 各级别亮度正确切换 | +| 音量 1~5 | 各级别音量正确切换 | +| 接口头文件无循环依赖 | `app_qh_proto.h` 不依赖任何设备/协议私有头(仅依赖 `stdint.h` + `channel_t` 前向声明) | diff --git "a/doc/04_\351\235\222\346\265\267\351\253\230\351\200\237\350\264\271\346\230\276\345\215\217\350\256\256/\351\235\222\346\265\267\345\215\217\350\256\256\344\277\256\346\224\27120191006.doc" "b/doc/04_\351\235\222\346\265\267\351\253\230\351\200\237\350\264\271\346\230\276\345\215\217\350\256\256/\351\235\222\346\265\267\345\215\217\350\256\256\344\277\256\346\224\27120191006.doc" new file mode 100644 index 0000000..640b648 Binary files /dev/null and "b/doc/04_\351\235\222\346\265\267\351\253\230\351\200\237\350\264\271\346\230\276\345\215\217\350\256\256/\351\235\222\346\265\267\345\215\217\350\256\256\344\277\256\346\224\27120191006.doc" differ diff --git "a/doc/04_\351\235\222\346\265\267\351\253\230\351\200\237\350\264\271\346\230\276\345\215\217\350\256\256/\351\235\222\346\265\267\345\215\217\350\256\256\344\277\256\346\224\27120191006.txt" "b/doc/04_\351\235\222\346\265\267\351\253\230\351\200\237\350\264\271\346\230\276\345\215\217\350\256\256/\351\235\222\346\265\267\345\215\217\350\256\256\344\277\256\346\224\27120191006.txt" new file mode 100644 index 0000000..3e2e1f2 --- /dev/null +++ "b/doc/04_\351\235\222\346\265\267\351\253\230\351\200\237\350\264\271\346\230\276\345\215\217\350\256\256/\351\235\222\346\265\267\345\215\217\350\256\256\344\277\256\346\224\27120191006.txt" @@ -0,0 +1,163 @@ +车道费额显示器通信协议 +日期 修订内容 修订者 +2019-10-06 增加外设控制指令’A’ madpan + + + +1.信息显示牌软件接口要求 +(1)通信协议 +信息显示牌(费显)通过串口与车道工控机相连,收费软件发送串口数据控制费显显示内容与语音播报。 +费显发光点阵每行至少可以显示6个汉字或12个英文字母或数字及小数点,可用于收费和欢迎用语显示。 +波特率9600~115200bps可调;(默认 9600) +校验位:无; +数据位:8bit; +停止位:1bit; +通信数据格式如下: +字段 1 2 3 4 5 +内容 帧头 命令字 参数长度 参数数据 帧尾' +长度(字节) 1 1 1 变长 1 +数据格式 ‘{‘ ‘1’~’A’ ‘}’ +说明 字段1为控制命令起始标志,1字节长度,固定为ASCII码字符'{',即十六进制:7BH; +字段2为控制指令的命令字,1字节长度,为ASCII码字符'1'~'A'之一; +字段3为控制参数数据的长度(字段4的长度,若无参数数据,则为0)。 +字段4为控制参数内容,变长,见详细命令说明; +字段5为控制命令结束标志,1字节长度,固定为ASCII码字符'}',即十六进制:7DH; +(2)通讯格式 +0、上电效果: +费显上电后,无需通讯,点阵全屏显示“祝您一路平安”信息,语音报出“祝您一路平安”,稍候熄灭。 +1、'1’主机查询命令 +通信格式:7B 31 00 7D(16进制) +命 令 字:0x31 +控制参数:无 +显示器应答 7B 31 01 00 7D 表示显示器状态正常 +显示器应答 7B 31 01 01 7D 表示显示器状态不正常 +2、'2'自检命令: +通信格式:7B 32 00 7D(16进制) +命 令 字:0x32 +控制参数:无 +结 果:系统进入自动检测状态,固定汉字信息及数字1~9交替显示,语音报出示例语音内容。 +3、'3'点阵单行任意显示命令: +通信格式:7B 33 len x0 x1 x2 … xn 7D(16进制) // if(inbuf[5]==0x01) +命 令 字:0x33 +长度len为控制参数数据的长度 +控制参数:x0,1字节长度,点阵颜色,’0’~’1’, (0x30~0x31),’0’表示红色,’1’表示绿色,’2’表示黄色。 +x1,1字节长度,点阵显示行号,’1’~’5’,(0x31~0x35)。 +X2~xn,要显示的数据内容,其中xi为可显示ASCII码字符,或xixi+1组成一个汉字;中间不显示位送空格' '(十六进制0x20H)。一行点阵可以显示12个ASCII字符,或者6个汉字;ASCII字符编码占用一个字节,一个汉字编码占用两个字节(高位在 前,低位在后);支持ASCII码可显示字符集范围0x20~0x80,国标汉字一二级字库。 +结 果:费显显示相应结果。 +命令示例: +(1)命令:7B 33 04 30 31 41 7D 结果:第一行红色显示“A” +(2)命令:7B 33 09 31 32 45 54 43 B3 B5 B5 C0 7D 结果:第二行绿色显示“ETC车道”。 +4、'4'点阵全屏可编辑显示命令: +通信格式:7B 34 len x0 x1 … xn 7D(16 进制) +命令字:0x34 +长度len 为控制参数数据的长度。 +控制参数: +x0:1 字节长度,点阵颜色,’0’~’1’, (0x30~0x31),’0’表示红色,’ +1’表示绿色,’2’表示黄色。x1:显示区域X 坐标,最左端为0。 +x2:显示区域Y 坐标,最上端为0。 +x3~xn,要显示的数据内容,支持回车,对应代码0x0a ox0d。其中xi 为可 +显示ASCII 码字符,或xixi+1 组成一个汉字;中间不显示位送空格' '(十六进制0x20H)。ASCII 字符编码占用一个字节,一个汉字编码占用两个字节(高位在前,低位在后);支持ASCII 码可显示字符集范围0x20~0x80,国标汉字二级字库。 +结果:费显显示相应结果。 +命令示例: +(1)命令:7B 34 10 30 00 2C 45 54 43 B3 B5 B5 C0 D2 D1 B9 D8 B1 +D5 7D 结果:屏幕上下居中红色显示“ETC车道已关闭” +5、'5'全屏清除显示命令: +通信格式:7B 35 00 7D(16进制) +命 令 字:0x35 +控制参数:无 +结 果:费显全屏清除显示。 +6、'6'固定格式显示命令: +通信格式:7B 36 len x0 x1 车型 | 金额 |余额 | 信息1|信息2 7D (客车) + 7B 36 len x0 x1 车型 | 金额 | 余额 | 总重 | 超重 7D (货车) +命 令 字:36 +控制参数:x0,1字节长度,点阵颜色,’0’~’1’, (0x30~0x31),’0’表示红色,’1’表示绿色,’2’表示黄色。 +x1,1字节长度,’0’表示按客车方式显示(固定汉字有:车型:,金额:,余额:,元);’1’表示按货车方式显示(固定汉字有:车型:,金额:,余额:,总重:,超重:,元); +1)、按客车方式显示,控制参数组成如下: +(0)x0=0x30~0x32,表示红色、绿色或黄色。 +(1)x1=0x30,表示客车 +(2)车型的ASCII 码,如31 表示1型车,31 31 表示11型车。 +(3)分隔符“|”,7C。 +(4)金额的ASCII码,可以有小数。如31 表示1元,31 2E 32 30表示1.20元。 +(5)分隔符“|”,7C。 +(6)余额的ASCII码,可以有小数。 +(7)分隔符“|”,7C。 +(8)信息1的ASCII码。 +(9)分隔符“|”,7C。 +(10)信息2的ASCII码。 +费显显示相应结果: +第一行点阵显示“车型: X型” +第二行点阵显示“金额:X元” +第三行点阵显示“余额:X元” +第四行点阵显示“贵A12345”(示例) +第五行点阵显示“交易成功”(示例) +说明: +(1) 若信息1,信息2对应的ASCII码首个字节为空格,则该行不显示。 +(2)信息1,信息2发送的内容超出12个字符,则该行自动左右滚屏显示。 +(3)客车固定格式显示时,同时语音播报:您好、请交费x元(与显示收费金额同步)。 +(4)金额是0元时,不播报语音,语音播报数字时有小数的需播报小数。 +2)、按货车方式显示,控制参数组成如下: +(0)x0=0x30~0x32,表示红色、绿色或黄色。 +(1)x0=0x31,表示货车 +(2)车型的ASCII 码,如31 表示1型车,31 31 表示11型车。 +(3)分隔符“|”,7C。 +(4)金额的ASCII码,可以有小数。 +(5)分隔符“|”,7C。 +(6)余额的ASCII码,可以有小数。 +(7)分隔符“|”,7C。 +(8)总重的ASCII码,可以有小数。如31 表示1吨,31 2E 32表示1.2吨 +(9)分隔符“|”,7C。 +(10)超重的ASCII码,可以有小数。 +费显显示相应结果: +第一行点阵显示“车型: X型” +第二行点阵显示“金额:X元” +第三行点阵显示“余额:X元” +第四行点阵显示“总重:X吨” +第五行点阵显示“超重:X吨” +说明: +(1)货车固定格式显示时,同时语音播报:您好、请交费x元(与显示收费金额同步)。 +(2)金额是0元时,不播报语音,语音播报数字时有小数的需播报小数。 +(3)总重或超重是0吨时,该行不显示。 +7、'7'礼貌用语语音播报命令: +通信格式:7B 37 01 x0 7D(16进制) +命 令 字:0x37 +控制参数:x0 1字节长度,为'0'~'3'ASCII码字符; +'0':报出:欢迎行驶高速公路。 +'1':报出:请保管好通行卡 +'2':报出:祝您一路平安。 +'3':报出:请出示通行卡。 +其他:预留 +结 果:费显语音提示。 +8、'8'显示亮度设定命令: +通信格式:7B 38 01 x0 7D +命 令 字:0x38 +控制参数:x0 1字节长度,为'0'~'5'ASCII码字符;设定费显显示亮度级别,5级最亮,1级最暗;0代表自动调节亮度适应环境,周围环境越亮、显示亮度越高。 +结 果:费显显示的亮度设定。 +9、'9'语音音量设定命令: +通信格式:7B 39 01 x0 7D +命 令 字:0x39 +控制参数:x0 1字节长度,为'1'~'5'ASCII码字符;设定费显语音播报音量级别,5级音量最大,1级音量最小。 +结 果:费显播报语音音量设定。 + +10、'A'外设控制命令(包括红绿灯,报警控制和辅助输出控制): +通信格式:7B 41 01 DATA0 7D +命 令 字:0x41 +控制参数:注: DATA0含8个bit位,用以代表不同设备控制信息: +bit7,bit6,bit5,bit4,bit3,bit2,bit1,bit0 +其中:DATA0的bit0为绿灯控制位,1为绿灯开,0为绿灯关; + bit1为红灯控制位,1为红灯开,0为红灯关; + bit2为黄闪报警控制位,1为报警开,0为报警关; + 例1:如要控制绿灯开,红灯关,报警灯关: +7B 41 01 01 7D + 例2:如要控制绿灯关,红灯开,报警灯开 +7B 41 01 06 7D +结 果:外设控制。 +11、语音播报命令: +通信格式:7B 42 x0 x1 x2 x3 x4 x5 7D(16进制) +命 令 字:0x42 +控制参数:x0 1字节长度,为'0'~'9'ASCII码字符; +'0':报出:通行费。 +'1':报出:剩余(Money)次。 +控制参数:x1~x5 5字节长度,为'0'~'9'ASCII码字符,代表万、千、百、十、个; +其他:预留 +结 果:费显语音提示。 + diff --git "a/doc/05_\345\215\217\350\256\256\346\250\241\345\235\227\345\244\232\345\215\217\350\256\256\345\205\274\345\256\271\344\274\230\345\214\226/01_architecture.md" "b/doc/05_\345\215\217\350\256\256\346\250\241\345\235\227\345\244\232\345\215\217\350\256\256\345\205\274\345\256\271\344\274\230\345\214\226/01_architecture.md" new file mode 100644 index 0000000..2948c35 --- /dev/null +++ "b/doc/05_\345\215\217\350\256\256\346\250\241\345\235\227\345\244\232\345\215\217\350\256\256\345\205\274\345\256\271\344\274\230\345\214\226/01_architecture.md" @@ -0,0 +1,243 @@ +# 多协议共享 RB — 设计规范 + +> **状态**:现行 | 2026-08-14 +> **落地对照**:[02_as_built_status.md](./02_as_built_status.md) +> **内存位置**:RB 体默认 SRAM、堆/CCM 归属见 [`../06_SRAM内部分数据迁移/02_memory_policy.md`](../06_SRAM内部分数据迁移/02_memory_policy.md) + +--- + +## 1. 验收目标 + +| 编号 | 验收项 | 判定标准 | +|------|--------|----------| +| A-1 | 单固件多地区协议 | 同一镜像可同编多个兼容解析器(如 LDI + 青海 + RLS) | +| A-2 | 工程目录选编 | EIDE/Makefile 包含/排除协议目录;不以 `APP_PROTOCOL` 宏为主 | +| A-3 | 启动自注册 | `sw_app_initcall` → acquire / register / bind / 任务;`app_boot` 无协议 `#ifdef` | +| A-4 | 同通道链式解析 | 绑定同通道的协议共享物理 RB,链式 probe,匹配者消费 | +| A-5 | USART6 语音专用 | 禁止任何协议 `bind(CH_ID_RS232_1)`;TTS → `PL_UART6` 旁路 | +| A-6 | 单协议退化 | 只编入一个协议目录时行为退化为单 probe(weak 无他提供者) | + +非功能: + +| 编号 | 需求 | +|------|------| +| NF-1 | 多协议带来的 FreeRTOS 堆压力可控(静态队列/控栈;结构见 06) | +| NF-2 | **不同物理口**不共享可并发写入的 RB | +| NF-3 | 帧头冲突组合不得同通道同 RB 同时启用 | +| NF-4 | 网口逻辑通道默认共享 RJ45;若需真并发高压隔离,再拆逻辑 RB(见 §2.4) | + +--- + +## 2. 核心决策 + +### 2.1 一物理通道一 RB + +| 物理口 | RB 槽 | 容量 | 逻辑通道(可多) | +|--------|-------|------|------------------| +| ETH(RJ45) | `RB_SLOT_RJ45` | **1536** | TCP_SERVER / TCP_CLIENT / UDP / MQTT | +| USART1 RS485 | `RB_SLOT_RS485` | **768** | `CH_ID_RS485` | +| USART3 RS232 | `RB_SLOT_RS232` | **768** | `CH_ID_RS232` | +| USART6 | — | — | **不进 RB**(语音 TX 旁路) | + +规则: + +- 同物理口多协议 → **共享**该槽 RB,链式 probe 区分帧。 +- 不同物理口 → **独立** RB,禁止混写。 +- `app_channel_dispatch`:同 RB 指针 `seen[]` 去重,每包只写一次。 +- `frame_dispatch_task`:外循环按 RB 去重;内循环同 RB 上协议链式探测。 + +``` +通道 RX → app_channel_dispatch → 写入物理 RB(去重) + → ch_queue → frame_dispatch_task + → 链式 probe → READY → frame_queue[协议] +``` + +### 2.2 编译期按需(`RB_PROVIDE_WEAK`) + +- 协议 TU 内:`RB_PROVIDE_WEAK(rb_provide_xxx, RB_SIZE_xxx)`。 +- 多 TU 同名 weak → 链接保留一个 getter 及 static 体。 +- 未编入任何提供者 → weak 为 0 → `acquire` 返回 nullptr(不占缓冲)。 +- 体落默认 `.bss`(SRAM);**不**再使用调度路径上的 `RB_DEFINE_CCM` 八槽池。 + +### 2.3 协议模块组织 + +| 原则 | 说明 | +|------|------| +| 目录即模块 | `LDI/`、`ProtocolParser_QingHai/`、`RLS/`、`IAP/` … | +| 自注册 | `sw_app_initcall`;四步:acquire → register → bind → `osThreadNew` | +| 多逻辑通道 | **每逻辑通道独立 mask + bind**;共享同一物理 RB;禁止「单 mask 绑多通道」 | +| 框架零分支 | `app_dispatch` 无地区 `#ifdef` | + +### 2.4 网口共享 RJ45 的风险与门禁(NF-4) + +TCP 为流、UDP 为报;多通道任务均可对 **同一** RJ45 RB 调用 `dispatch`。互斥保证单次 `rb_write` 原子,但**不同套接字的包会在环缓中交错排列**。 + +| 场景 | 建议 | +|------|------| +| 产品以单网口业务为主(常见:UDP IAP/LDI 或 TCP 二选一) | **维持共享 1536**(省 RAM) | +| TCP 与 UDP/MQTT **同时高压**收包 | 评估拆逻辑 RB,或产品互斥启通道 | + +本阶段默认维持共享;变更须同步改 `02` 绑定表与 `06` 占用账。 + +### 2.5 硬件映射 + +| PL | 口 | CH_ID | 用途 | +|----|-----|-------|------| +| UART1 | RS485 | `CH_ID_RS485` | 地区协议链式 | +| UART3 | RS232 | `CH_ID_RS232` | 地区协议链式 | +| UART6 | TTL | `CH_ID_RS232_1` | **仅语音 TTS**;禁止协议 bind | +| ETH | — | TCP_S / TCP_C / UDP / MQTT | 共享 `RB_SLOT_RJ45` | + +--- + +## 3. 链式探测契约(A-4) + +### 3.1 WAIT / FAKE 决策 + +``` +READY / SKIP → 消费后结束本轮,继续取下一帧 +WAIT → any_wait,继续试下一协议 +FAKE → any_fake,继续试下一协议 + +无一消费时: + if (any_wait) break; /* 禁止 skip */ + else if (any_fake) rb_skip(1); /* 重同步 */ +``` + +### 3.2 probe 强制规则 + +1. 只 `rb_peek`;禁止 probe 内 `rb_read` / `rb_skip` / 写业务全局 +2. **首字节快拒**:不匹配立刻 FAKE;禁止「长度不够就一律 WAIT」 +3. 仅 READY/SKIP 写 `*total_len` / `*aux` +4. 无阻塞、无副作用、幂等 +5. **长度/缓冲边界**:对 length 做上下界;`rb_peek` 按目标缓冲截断 + +### 3.3 帧头事实 + +| 协议 | 特征 | +|------|------| +| IAP | `0x5A5A5A5A` + CRC32 | +| LDI | STX `0xFF 0xFF` + CRC16-XMODEM | +| RLS | `0xFF 0xFE` … `0x0D 0x0C` | +| 青海 | `{` … `}`(弱校验) | +| 山东 | `{` + 命令字('1'~'5','7','8') + 二进制 len + `}`(与青海同构、无 BCC;'3'/'4'/'5' 语义与青海一致,'1'/'2'/'7'/'8' 语义分歧) | +| 贵州 | `{` + 命令字('1'~'9','A','B',0x01,0x02) + 二进制 len + `}`(与青海完全同构、无 BCC;13 命令含 0x01 全屏点亮 / 0x02 版本号两个二进制命令字) | +| 云南 | `{` + 命令字('1'~'9','A','B',0x01,0x02) + 二进制 len + `}`(与青海/贵州完全同构、无 BCC;13 命令,'6'=单行清除(贵州为固定格式)、0x02 版本号应答 PROGRAM_CODE) | +| 四川 ETC | `0x0A` + 命令位(00/01/36~39/40/50) … `0x0D`(46 归属 MTC) | +| 四川 MTC | `{`(0x7B) + 命令 + 参数 + `}`(**'}' 定界变长**,无长度字段、无 BCC,扫描上限 74B,9K1F212701 语义);`0A 46 0A` / `0A 46 0D` 双帧型 | +| 四川治超 | `0xFF` + 长度(07~FF,0xFE 排除) … `0xFF`(BCC 异或);长度上限对齐 9K1F212701 容纳 0x80 全屏长数据段,0xFE 显式排除与 RLS `FF FE` 区分;81~88 行数据变长(=总长-6,≤24B 截断) | +| 重庆CQ | JSON `{` 花括号深度定界('"' 内跳过,深度>16/累计>1044 → FAKE)+ `FF FF` 12B 二进制精确匹配(重启/搜索两帧),CRC16-XMODEM(大端,帧内校验);业务口 UDP_CQ(20103) + 搜索口 UDP(10011) | + +--- + +## 4. RB / 绑定规划(与代码一致) + +| 槽 | 尺寸 | 提供方(weak) | bind 通道 | 协议 | +|----|------|----------------|-----------|------| +| RJ45 | 1536 | IAP / LDI / AH_MQTT | UDP | IAP | +| RJ45 | 同上(共享) | 同上 | TCP_S / TCP_C / UDP | LDI | +| RJ45 | 同上 | 同上 + CQ | MQTT | AH_MQTT(initcall 可关) | +| RJ45 | 同上 | CQ(weak 合并) | UDP_CQ / UDP | 重庆CQ(双 mask 共用一队列;CQ 源收录序在 LDI 之后) | +| RS485 | 768 | QH / RLS / SC_ETC / SC_MTC / SC_OL / SD / GZ / YN | RS485 | 青海、RLS、四川三协议、山东、贵州、云南 | +| RS232 | 768 | QH / SC_ETC / SC_MTC / SC_OL / SD / GZ / YN | RS232 | 青海、四川三协议、山东、贵州、云南 | + +**已取消(相对历史 8 槽方案)**: + +- IAP 占用 RS485 / 「系统独立 RB」双写 +- LDI 绑定 RS485 / RS232 +- 每逻辑网口独占 2KB CCM 槽 + +### 4.1 协议帧 queue 深度(静态,SRAM) + +一协议一队列(LDI 三逻辑通道 mask 共用 `g_ldi_msg_queue`)。与 RB/DMA 正交;加深 queue 不必联调 RB。 + +| 协议 | 宏 | 深度 | 单槽约 | 静态体约 | 说明 | +|------|-----|------|--------|----------|------| +| IAP | `IAP_QUEUE_DEPTH` | **2** | ~1052 | ~2104 | 停等升级模型够用 | +| LDI | `LDI_QUEUE_DEPTH` | **4** | ~520 | ~2080 | 一包多帧粘包(≥3) | +| 青海 | `QH_QUEUE_DEPTH` | **3** | ~267 | ~801 | payload 上限 259 | +| RLS | `RLS_QUEUE_DEPTH` | **2** | ~538 | ~1076 | | +| AH_MQTT | `AH_MQTT_QUEUE_DEPTH` | **3** | ~541 | ~1623 | initcall 可关;任务内 static | +| 四川 ETC | `SC_ETC_QUEUE_DEPTH` | **3** | 157 | **471** | payload 149(帧 ≤149,0x0D 定界,参考 9K1F212701) | +| 四川 MTC | `SC_MTC_QUEUE_DEPTH` | **3** | 82 | **246** | payload 74('}' 定界扫描上限;'4' 全屏 ≤68B / '78' 语音 ≤74B) | +| 四川治超 | `SC_OL_QUEUE_DEPTH` | **3** | 263 | **789** | payload 255(=长度字段上限 FF,覆盖 0x80 全屏长数据段) | +| 山东 | `SD_QUEUE_DEPTH` | **3** | ~267 | ~801 | payload 259(len 字段 1B 上限,与青海同构) | +| 贵州 | `GZ_QUEUE_DEPTH` | **3** | ~267 | ~801 | payload 259(与青海同构,13 命令含 0x01/0x02) | +| 云南 | `YN_QUEUE_DEPTH` | **3** | ~267 | ~801 | payload 259(与青海/贵州同构,13 命令含 0x01/0x02;'3' 单行 FONT_24 渲染) | +| 重庆CQ | `CQ_QUEUE_DEPTH` | **3** | 1052 | **3156** | payload 1044(JSON 花括号定界上限);队列体+任务帧缓冲+JSON/文本缓冲**置 CCMRAM**(SRAM 全协议构建仅余 ~1.9KB,CPU 独占访问无 DMA) | + +变更记录(2026-08-14):LDI 2→4、青海对齐协议后深度 3、AH 1→3;IAP/RLS 维持 2。 +变更记录(2026-08-14):新增四川三协议(ETC/MTC/治超)各深度 3。 +变更记录(2026-08-15):四川 ETC payload 64→149(0x0D 定界上限对齐 9K1F212701 etc.c,全屏数据无固定 56B)。 +变更记录(2026-08-17):四川治超 payload 249→255(=长度字段上限 FF,消除 250~255 长帧被队列截断的越界读);MTC '8' 亮度参数兼容二进制 0~8 与 ASCII '0'~'8' 双格式;MTC 'A' 4B/5B 判别加 b2≤2 门限,消除颜色帧 BCC='}' 误判。 +变更记录(2026-08-17):MTC '{' 帧族改「'}' 定界变长」扫描(对齐 9K1F212701 mtc.c 逐字节扫 '}'),废除每命令定长双长度('3'→20/21B 等)与 '78' BCC 校验;字段偏移按变长重算('3' 文本=raw_len-4、'4'=raw_len-3、'6' 数字串=raw_len-4、'78' 文本=raw_len-4);修复上位机 `{3 1 1234 } 3 }`(10B)单行乱码(定长 probe 跨帧拼凑 20B 认领含 '}'/'{' 垃圾文本);payload 上限仍 74B(队列约束)。宿主推演 `~/EnvTools/CD-DebugTool-cpp/scripts/probe_sim/sc_mtc_frame_sim.py`。 +变更记录(2026-08-17):新增山东协议(SD)深度 3、payload 259(与青海同构;命令字 '1'~'5','7','8',无 '6')。 +变更记录(2026-08-17):新增贵州协议(GZ)深度 3、payload 259(与青海同构;命令字 '1'~'9','A','B' + 0x01/0x02 二进制命令字)。 +变更记录(2026-08-24):新增云南协议(YN)深度 3、payload 259(与青海/贵州同构;命令字 '1'~'9','A','B' + 0x01/0x02;'6'=单行清除、'3'/'4' 按 24 点阵 FONT_24 渲染、0x02 应答 PROGRAM_CODE、无上电效果 default 文件)。 +变更记录(2026-08-24 修订,用户决定 1~10):YN 语义定稿——'2' 自检改老化循环显示 + 每 5s 语音「系统正在自检」(可被下一帧打断);'3'/'6' 行号 '1'~'5' 全接受(行 5 执行但不落屏);'8' 亮度 0x00=恢复光敏自动、'1'~'8' 手动档;0x01 全屏点亮扩至 01红~07白 七色;0x02 应答 PROGRAM_CODE;移除上电效果 default 文件(不注册默认显示)。 +变更记录(2026-08-20):新增重庆CQ(JSON `{` + 12B 二进制)深度 3、payload 1044、静态体 3156B **置 CCMRAM**;与 IAP/LDI 共享 RJ45 RB,CQ 源收录序在 LDI 之后(probe 注册序)。 +变更记录(2026-08-21):CQ 业务端口语义修复——`PROTO_CHONGQING` 才读 Sector1 net_cfg.port(无效/0 回退 20103),dev 共存构建固定 20103(不读 LDI 语义的 net_cfg.port);搜索应答 port 字段改报 `app_udp_cq_get_port()`;10011 口 12B 帧「LDI 先探测、FAKE 放行、CQ 收单」实态修正(见 §6 兼容矩阵与 doc/03 PartB B.11)。 +变更记录(2026-08-21 方案 B):Sector1.net_cfg 新增 `udp_port`(CQ UDP 业务口专有),TCP/UDP 端口彻底分离——`PROTO_CHONGQING` 改读 net_cfg.udp_port(setip 写入,默认 20103),dev 共存构建固定 20103;net_cfg.port 恒为 TCP 业务口不再被 setip 污染(见 doc/03 PartB B.7.1/B.11、doc/07 §15)。 +--- + +## 5. 协议模块模板 + +```c +RB_PROVIDE_WEAK(rb_provide_rs485, RB_SIZE_RS485); + +void xxx_proto_init(void) +{ + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RS485, RB_SIZE_RS485); + if (rb == nullptr) return; + proto_mask_t m = app_proto_register(xxx_probe_frame, rb); + if (m == 0) return; + app_proto_bind_channel(m, CH_ID_RS485); + /* 多逻辑通道 → 再 register + bind;禁止 CH_ID_RS232_1 */ + osThreadNew(xxx_handle_task, ...); +} +sw_app_initcall(xxx_proto_init); +``` + +语音:`dev_rs232_voice_*` → `pl_uart_send(PL_UART6)`,旁路 dispatch。 + +--- + +## 6. 兼容矩阵(合入门禁) + +| 组合 | 同物理 RB | 建议 | +|------|-----------|------| +| LDI + 青海 | 不同物理口(RJ45 vs RS485) | 允许 | +| LDI + RLS + 青海(RS485 上后两者) | RS485 链式 | 允许(帧头可分) | +| 青海 + 重庆 JSON | **禁止**同 RS485 | 同分 `{` | +| LDI + 重庆 BIN | 高风险 | 同 `FFFF`,须更强 probe 或互斥 | +| 任意协议 + `CH_ID_RS232_1` | **禁止** | 语音专用 | +| 四川 MTC + 青海(`{` 帧头重叠) | RS485/RS232 链式 | ⚠️ **EIDE 目录排除纪律**:完整青海帧由 QH probe 先认领(qh_proto_init 先注册,源码收录序 QH 在前);MTC probe '}' 定界变长扫描(上限 74B,2026-08-17 对齐 9K1F212701)对「青海帧数据段含 0x7D(GBK 尾字节可命中)且半帧到达」存在截断认领残余风险,依赖排他编译,量产必须二选一 | +| 四川治超 + RLS(`FF` 帧头重叠) | RS485 链式 | 允许:治超第二字节=长度 07~FF 但**显式排除 0xFE(254)**,RLS 固定 0xFE,双向第二字节快拒成立(2026-08-15 长度上限对齐 9K1F212701 后必须显式排除,否则治超 probe 会把 RLS 帧当长帧 WAIT 卡死) | +| 四川 ETC + 四川 MTC(`0A` 帧头重叠) | RS485/RS232 链式 | 允许:ETC 命令位 ∈ {00,01,36~39,40,50},MTC 仅 46(0A 46 0A/0D),命令位互斥 | +| 山东 + 青海(`{` 帧族完全同构) | RS485/RS232 链式 | ⚠️ **EIDE 目录排除纪律**:山东命令字 '1'~'5','7','8' 全部落入青海 probe 命令集('1'~'9','A','B'),全协议构建下青海 probe 先认领(源码收录序 qh 在前);'3'/'4'/'5' 语义巧合一致,'1'/'2'/'7'/'8' 语义分歧(山东 '1'=全屏单色 vs 青海 '1'=主机查询、'2'=版本 vs 自检、'7'=亮度 vs 文明语音、'8'=外设 vs 亮度),量产必须二选一 | +| 山东 + 四川 MTC(`{` 帧头重叠) | RS485/RS232 链式 | ⚠️ 同青海行纪律:MTC '}' 定界变长扫描会认领山东帧,量产互斥 | +| 贵州 + 青海(`{` 帧族完全重叠) | RS485/RS232 链式 | ⚠️ **EIDE 目录排除纪律**:贵州命令字 '1'~'9','A','B' 全部落入青海 probe 命令集,全协议构建下青海 probe 先认领(源码收录序 qh 在前);'1'/'3'/'4'/'5'/'7'/'8'/'9'/'B' 语义基本一致('7'=文明语音、'8'=亮度、'9'=音量、'B'=费额语音),'2'/'6'/'A' 细节差异(贵州 '2'=自检黄屏+语音、'6'=固定格式行文与青海不同、'A'=红优先 vs 青海同红优先但贵州语义自协议文档),量产必须二选一 | +| 贵州 0x01/0x02(二进制命令字) | RS485/RS232 链式 | 允许:0x01 全屏点亮 / 0x02 版本号为二进制命令字,不属于青海/山东/四川MTC 的 ASCII 命令集,各 probe 首命令字快拒,贵州 probe 无冲突认领 | +| 云南 + 青海(`{` 帧族完全重叠) | RS485/RS232 链式 | ⚠️ **EIDE 目录排除纪律**:云南命令字 '1'~'9','A','B' 全部落入青海 probe 命令集,全协议构建下青海 probe 先认领(源码收录序 qh 在前);与贵州同构但 '6' 单行清除(贵州固定格式)、'8' 0x00自动/1~8 档(贵州 0~5 档)语义差异,量产必须二选一 | +| 云南 0x01/0x02(二进制命令字) | RS485/RS232 链式 | 允许:0x01 全屏点亮 / 0x02 版本号为二进制命令字,不属于青海/山东/四川MTC 的 ASCII 命令集,各 probe 首命令字快拒,云南 probe 无冲突认领 | +| `{` 帧族任意两两(QH/SD/GZ/SC_MTC/YN) | RS485/RS232 链式 | ⚠️ **编译期互斥守卫**:五个协议主文件各定义同名强符号 `g_brace_proto_guard`(`__attribute__((used))` 防 `--gc-sections` 丢弃,`#ifndef STD_ALL_PROTO` 包裹)——EIDE 量产构建多个编入即链接报 `multiple definition of 'g_brace_proto_guard'`,无法绕过;Makefile 全协议开发构建经 `-DSTD_ALL_PROTO` 豁免共存(probe 注册序与本文纪律约束)。EIDE excludeList 目录排除纪律仍为操作层首选 | +| 重庆CQ + LDI | RJ45(UDP/UDP_CQ/TCP 共享) | ⚠️ **产品互斥**(Makefile `PROTO=CQ` 剔除 LDI 目录 / EIDE excludeList 目录排除);全协议 dev 构建下二者共存于 RJ45:CQ JSON `{` 与 LDI `FF FF` 首字节互斥可分;**10011 口 12B 二进制帧(CQ 重启/搜索请求)先经 LDI probe:CQ 12B 帧 len=00 00 00 02 且 CRC16-XMODEM 恰好通过 LDI 校验,LDI probe 对 `data_len==2` 显式 FAKE 放行(2026-08-21 修复,此前沿身份校验路径 SKIP 吞帧致 CQ 12B 在 10011 无响应),CQ probe 仍认领——「LDI 先探测、FAKE 放行、CQ 收单」,无功能受限**。另:dev 共存构建 CQ 业务口固定 20103、不读 Sector1 net_cfg.udp_port(dev 构建无写入方;方案 B 2026-08-21 起 CQ 构建读 udp_port、TCP 口 port 不再被 setip 污染),见 doc/03 PartB B.11 | +| 重庆CQ + IAP | RJ45(UDP 共享) | 允许:CQ 首字节 `{` 或 `FF FF`+12B 精确匹配,IAP 首字节 `0x5A` 快拒;CQ probe 对非匹配 `FF` 前缀 FAKE,互不误伤 | +| 重庆CQ + `{` 帧族(QH/SD/GZ/SC_MTC) | 不同物理口(RJ45 vs RS485/RS232) | 允许:不同 RB 槽不冲突;CQ 非串口 '{' 帧族协议,**不定义** `g_brace_proto_guard`(守卫仅约束串口 '{' 帧族四协议) | + +> 说明:`{` 帧族互斥由编译期守卫在链接期兜底强制;excludeList 目录排除为操作层首选(编译更早失败、意图明确)。Makefile 全协议构建(`-DSTD_ALL_PROTO`)下守卫失效,共存风险按上表各行的 probe 注册序纪律管控。 + +--- + +## 7. 与 RAM 文档的边界 + +- RB **行为**以本文件为准;RB **放 SRAM**、堆回退、显存占 CCM → **仅 06**。 +- 多协议任务栈挤堆的历史问题 → [03](./03_freertos_heap_side_effect.md),结构方案不在本目录改代码。 + +--- + +## 8. 非目标 + +- 不刷机动态下载未编入的解析器 +- 不保证帧头冲突协议在同一字节流上零误判 +- 不把 `APP_PROTOCOL` 作为主切换手段 diff --git "a/doc/05_\345\215\217\350\256\256\346\250\241\345\235\227\345\244\232\345\215\217\350\256\256\345\205\274\345\256\271\344\274\230\345\214\226/02_as_built_status.md" "b/doc/05_\345\215\217\350\256\256\346\250\241\345\235\227\345\244\232\345\215\217\350\256\256\345\205\274\345\256\271\344\274\230\345\214\226/02_as_built_status.md" new file mode 100644 index 0000000..7624889 --- /dev/null +++ "b/doc/05_\345\215\217\350\256\256\346\250\241\345\235\227\345\244\232\345\215\217\350\256\256\345\205\274\345\256\271\344\274\230\345\214\226/02_as_built_status.md" @@ -0,0 +1,150 @@ +# 多协议共享 RB — 落地状态(对照当前代码) + +> **基准**:2026-08-14(含 RB/DMA/queue 修订后) +> **总体结论:RB / 协议绑定 DoD = READY(已落地)** +> 设计规范:[01_architecture.md](./01_architecture.md) +> RAM 占用权威:[`../06_SRAM内部分数据迁移/04_current_memory_occupancy.md`](../06_SRAM内部分数据迁移/04_current_memory_occupancy.md) +> LDI 选编依赖:[`../07_LDI与IAP配置解耦/`](../07_LDI与IAP配置解耦/README.md) + +--- + +## 1. 验收项对照 + +| 编号 | 判定 | 现状 | +|------|------|------| +| A-1 单固件多协议 | ✅ | Makefile 同编 IAP/LDI/青海/RLS/AH/四川三协议;**EIDE Debug 现编 IAP+LDI+青海+四川三协议**(`.eide/eide.yml` exclude RLS/AH);各自 `sw_app_initcall` | +| A-2 目录选编 | ⚠️ | 无 `APP_PROTOCOL`;排除协议目录可裁解析器。配置层已抽至 `Application/Config/app_board_net_cfg.c`,**排除整个 `IAP/` 不影响 LDI**(解耦见 doc/07) | +| A-3 自注册 | ✅ | `app_boot` 无协议分支;四步完整 | +| A-4 同通道链式 | ✅ | `any_wait/any_fake/any_parsed`;RS485:青海+RLS;RJ45:IAP+LDI(+可选 MQTT) | +| A-5 语音 USART6 | ✅ | 零处 `bind(CH_ID_RS232_1)`;voice → `PL_UART6` | +| A-6 单协议退化 | ✅ | weak provide;排除目录后无硬编码互斥(LDI 仍依赖 cfg,见 A-2) | +| NF-2 跨物理口隔离 | ✅ | 三物理槽独立 | +| NF-3 兼容矩阵门禁 | ⚠️ | 编译期检查仍缺(青海 vs 重庆);运行约定见 `01` §6 | +| NF-4 网口共享策略 | ✅ 已采纳默认 | 共享 RJ45 **1536**;高压并发风险见 `01` §2.4 | + +--- + +## 2. 现行 RB / 绑定表(代码事实) + +定义:`Application/Inc/app_dispatch.h`(`rb_slot_t` / `RB_SIZE_*`);池:`app_dispatch.c`(`g_rb_provide[]`)。 + +| 槽 | 尺寸 | 协议 | bind | +|----|------|------|------| +| `RB_SLOT_RJ45` | **1536** | IAP | `CH_ID_UDP` | +| 同上 | | LDI | `CH_ID_TCP_SERVER` / `TCP_CLIENT` / `UDP`(每通道独立 mask) | +| 同上 | | AH_MQTT | `CH_ID_MQTT`(initcall 可关) | +| `RB_SLOT_RS485` | **768** | 青海、RLS、四川 ETC/MTC/治超 | `CH_ID_RS485` | +| `RB_SLOT_RS232` | **768** | 青海、四川 ETC/MTC/治超 | `CH_ID_RS232` | + +关键源文件: + +| 文件 | 要点 | +|------|------| +| `app_dispatch.c` | provide 表、acquire 判空、写去重、链式分发;`_Static_assert` 尺寸 1536/768/768 | +| `ring_buffer.h` | `RB_PROVIDE_WEAK`;可用容量 = size−1 | +| `app_iap.c` | provide RJ45;**仅 UDP**;`IAP_QUEUE_DEPTH=2` | +| `app_ldi.c` | provide RJ45;**无 485/232**;`LDI_QUEUE_DEPTH=4` | +| `app_qh_proto.c` | provide 485+232;`QH_PAYLOAD_MAX=259`;`QH_QUEUE_DEPTH=3` | +| `app_sc_etc_proto.c` | provide 485+232;`SC_ETC_PAYLOAD_MAX=149`;`SC_ETC_QUEUE_DEPTH=3`;心跳计时任务(2026-08-17 已停用,见「修订」) | +| `app_sc_mtc_proto.c` | provide 485+232;`SC_MTC_PAYLOAD_MAX=74`;`SC_MTC_QUEUE_DEPTH=3`;'{' '}' 定界变长 probe(上限 74B) | +| `app_sc_ol_proto.c` | provide 485+232;`SC_OL_PAYLOAD_MAX=249`;`SC_OL_QUEUE_DEPTH=3`;FF+len probe(07~FF,0xFE 排除) | +| `app_uart_baud.c` | DIP1 波特率选择 + `pl_uart_set_baud` 运行态切换(RS232+RS485) | +| `app_rls.c` | provide 485;`mem_pool[RLS_PAYLOAD_MAX]`;`RLS_QUEUE_DEPTH=2` | +| `ah_mqtt.c` | provide RJ45;`AH_MQTT_QUEUE_DEPTH=3`;initcall 可关 | +| `app_rs232.c` / `app_boot.c` | RS232_1 旁路;无 DMA RX 缓冲 | + +通道启动(`app_boot.c`):TCP Server/Client、UDP、RS485、RS232;不启 RS232_1 协议 RX。 + +### 2.1 协议帧 queue 深度(2026-08-14 修订) + +| 协议 | 深度 | 静态缓冲约 | 变更 | +|------|------|------------|------| +| IAP | 2 | 2104 | 维持 | +| LDI | **4** | 2080 | 2→4(粘包多帧) | +| 青海 | **3** | 801 | payload 259 + 深度 3 | +| RLS | 2 | 1076 | 维持 | +| AH_MQTT | **3** | ~1623(任务启动后) | 1→3 | +| 四川 ETC | **3** | 471 | 新增(payload 64→149,0x0D 定界对齐 9K1F212701) | +| 四川 MTC | **3** | 246 | 新增(payload 74) | +| 四川治超 | **3** | 120 | 新增(payload 32) | + +构建口径:IAP/LDI/青海两套构建均编入;**RLS/AH 仅 Makefile 编入**,EIDE Debug 排除(采样 elf 中无其队列缓冲,见 06/`04`)。 + +`osMessageQueuePut(..., timeout=0)`:满则丢整帧。深度与 RB/DMA 无强制联调。 + +--- + +## 3. 相对历史方案的变更关闭表 + +| ID | 历史问题 / 旧规划 | 状态 | 说明 | +|----|-------------------|------|------| +| ARCH-1 | 8×2KB @ CCM | ✅ 取代 | 三槽 provide @ SRAM | +| ARCH-2 | IAP RS485+UDP 双槽 | ✅ 关闭 | 仅 UDP | +| ARCH-3 | LDI 绑 RS485 | ✅ 关闭 | 仅网口 | +| ARCH-4 | 每网口独占 RB | ✅ 产品决策 | 改共享 RJ45(NF-4) | +| CR-1 | IAP 盲 WAIT | ✅ | 首字节快拒 | +| CR-2 | RLS peek/长度越界 | ✅ | `mem_pool`≥`RLS_PAYLOAD_MAX`;RB 768 | +| CR-4 / MA-5 | AH_MQTT probe 过松 | ⚠️ 例外 | initcall 默认关 | +| MI-3 | 兼容矩阵编译门禁 | ❌ 可选后续 | | +| MI-CFG | LDI→`app_iap_cfg` 强依赖 | ✅ 已解耦 | `app_board_net_cfg`(Application/Config),见 doc/07 | + +--- + +## 4. DoD(本目录:多协议共享 RB) + +1. [x] `RB_CNT_MAX==3`,尺寸 **1536/768/768**,`_Static_assert` 在册 +2. [x] 无调度路径 `RB_DEFINE_CCM(g_rb*)` 八槽池 +3. [x] IAP 无 `CH_ID_RS485`;LDI 无 RS485/RS232 bind +4. [x] 无协议 bind `CH_ID_RS232_1` +5. [x] 同物理口链式 + 写去重仍在 +6. [x] EIDE/Makefile 无 `APP_PROTOCOL` +7. [x] 帧 queue 深度:IAP2 / LDI4 / QH3 / RLS2 / AH3 / SC_ETC3 / SC_MTC3 / SC_OL3(静态缓冲) + +**不在本 DoD**:堆水位标定、IAP 升级实机验证、doc/07 解耦落地。 + +--- + +## 5. 建议回归(协议行为) + +| 用例 | 期望 | +|------|------| +| RS485 青海 / RLS 帧 | 各自 READY;RLS 满长 ≤530 可 READY | +| 四川 MTC '{' 帧('}' 定界变长) | '1','2','5'→3B;'3' 单行行号[2]+变长文本[3..];'4' 全屏变长文本[2..];'6' 客车 ≥15B/货车 ≥24B(数字串 ≥11/20B);'7'固定→4B、'78' 自定义文本变长;'8','9'→4B 全部 READY(无 BCC,9K1F212701 语义) | +| 四川 ETC 0A 帧 | 行号 0~6;单行 ≤24B;0x30 → 软件复位(9K1F212701 语义) | +| 四川治超 FF 帧 | 80 全屏 + 81~88 八行;亮度 00=自动调光(9K1F212701 语义) | +| TEST 键 | 任意业务数据到达仅中止当前检测序列,monitor 回 IDLE;TEST 键始终可用(9K1F212701:收包清 testMode,按键扫描持续) | +| UDP IAP 与 LDI | 共享 RJ45,互不永久阻塞 | +| TCP LDI 一包 ≥3 帧 | 深度 4 下应均可入队(处理任务跟得上时) | +| TTS | USART6 出帧;协议口无语音字节 | +| 排除青海目录 Rebuild | Makefile 口径:RS485 provide 仍可由 RLS 提供;EIDE 口径:RLS 亦被排除,RS485 槽无提供者 → `acquire` 判空安全退化 | +| 排除整个 IAP/ 仅留 LDI | 链接成功(配置层在 `Application/Config`,不属 IAP 协议目录) | + +--- + +## 6. 可选后续(非阻塞) + +| 项 | 说明 | +|----|------| +| MI-3 编译期兼容检查 | 禁帧头冲突同编 | +| AH_MQTT 产品启用 | 收紧 probe + 堆预算 | +| doc/07 解耦 | 抽出 board_net_cfg(暂缓) | +| IAP 升级实机 | 停等模型下 queue=2;验证 ACK 节奏 | + +--- + +## 修订 + +- 2026-08-14:首版落地对照。 +- 2026-08-14:对齐 RB 1536/768/768、queue 深度表、A-2/doc/07、DoD。 +- 2026-08-14:A-1 与 §2.1 补充 EIDE/Makefile 协议选编差异(EIDE exclude RLS/AH)。 +- 2026-08-14:新增四川 ETC/MTC/治超三协议绑定 RS485+RS232 双通道、queue 深度 3;MTC vs 青海 '{' 帧头冲突以 EIDE 目录排除纪律兜底(01 §6)。 +- 2026-08-15:修复四川三协议显示语义与 TEST 键失效(见 doc/06 §5 修订): + - MTC '{' 帧族改为无 BCC(9K1F212701)/带 BCC 双格式兼容,BCC 不再校验 → MTC vs 青海 '{' 帧头重叠的判别力由「BCC 校验」降为「帧长+尾字节」;青海 '3' 帧恰 20B 且尾字节 '}' 时会被 MTC 认领的残留风险上升(≈1/256 → 长度巧合概率),量产二选一纪律不变; + - MTC '6' 固定格式字段整体错位修正(X1 起偏移); + - ETC 行号 1~6、单行 ≤24B、0x30 软件复位、显示颜色跟随通行灯; + - 治超 80 全屏显示 + 81~88 八行、亮度 00 自动调光、颜色跟随通行灯; + - TEST 键:`app_factory_mode_interrupt` 由销毁 factory_monitor 任务改为置中止标志,monitor 分片等待回 IDLE。 +- 2026-08-17:MTC '{' 帧族改「'}' 定界变长」(对齐 9K1F212701 mtc.c 逐字节扫 '}'),废除定长双长度与 '78' BCC 校验;修复上位机 `{3 1 1234 } 3 }`(10B)单行乱码(旧定长 probe 跨帧拼凑 20B 认领含 '}'/'{' 垃圾文本);字段偏移按变长重算;payload 仍 74B;宿主推演 `~/EnvTools/CD-DebugTool-cpp/scripts/probe_sim/sc_mtc_frame_sim.py`(`--old` 复现)。MTC vs 青海残余风险更新为「青海帧数据段含 0x7D 且半帧到达被截断认领」(01 §6、doc/CLAUDE.md 帧头冲突纪律)。 +- 2026-08-17(ETC 心跳停用 + MTC '4' 全屏换行修复): + - ETC 心跳计时任务 #if 0 停用、任务不再创建(ucHeap 任务栈 -1KB):5 分钟超时「ETC车道关闭」显示不再生效;黄闪 0A 38 开启后的 10 秒自动关闭依赖同一任务 tick 一并失效,须 0A 39 显式关闭;0A 50 心跳帧解析保留(识别后丢弃);`sc_etc_show_lane_closed`/`sc_etc_hs_timeout`/`sc_etc_activity_refresh` 保留待恢复(无引用,链接期 gc-sections 丢弃)。恢复方法见 `app_sc_etc_proto.c` 注释; + - MTC '4' 全屏渲染改整屏 word_wrap(w=屏宽、h=整屏高、word_wrap=true,先清屏后渲染):原 16B/行固定切 ≤4 行且单行 word_wrap=false,第一行超宽溢出被裁、超 64B 文本被丢弃;'3' 单行保持 word_wrap=false + h=当前字号(截断语义,未变)。 diff --git "a/doc/05_\345\215\217\350\256\256\346\250\241\345\235\227\345\244\232\345\215\217\350\256\256\345\205\274\345\256\271\344\274\230\345\214\226/03_freertos_heap_side_effect.md" "b/doc/05_\345\215\217\350\256\256\346\250\241\345\235\227\345\244\232\345\215\217\350\256\256\345\205\274\345\256\271\344\274\230\345\214\226/03_freertos_heap_side_effect.md" new file mode 100644 index 0000000..0179593 --- /dev/null +++ "b/doc/05_\345\215\217\350\256\256\346\250\241\345\235\227\345\244\232\345\215\217\350\256\256\345\205\274\345\256\271\344\274\230\345\214\226/03_freertos_heap_side_effect.md" @@ -0,0 +1,57 @@ +# 副作用归档:多协议 → FreeRTOS 堆压力 + +> **状态**:`[已归档]` | 原 `03_issue_freertos_heap.md` 精简重写 +> **说明**:本问题发生在「多协议同时自注册」之后,根因是 **FreeRTOS `ucHeap`(任务栈/动态 OS 对象)**,不是 RB 静态体。 +> **结构治理**:[`../06_SRAM内部分数据迁移/`](../06_SRAM内部分数据迁移/README.md) — Phase A **已执行**(堆 36KB @ SRAM)。 + +--- + +## 1. 现象(历史) + +上电进入 `vApplicationMallocFailedHook`: + +``` +tcp_server_task → osSemaphoreNew → pvPortMalloc 失败 +``` + +当时堆约 36KB @ SRAM,多协议任务栈叠满后在 TCP Server 起步处击穿。 + +--- + +## 2. 定性(仍成立) + +| 维度 | 结论 | +|------|------| +| 挤爆的是 | FreeRTOS `ucHeap`(任务栈、TCB、动态 Queue/Sem) | +| 不是 | 协议 RB 静态缓冲(现 @ SRAM provide,不占堆) | +| 也不是 | 协议帧 queue 静态缓冲(`s_*_queue_buf`,不占堆) | +| 也不是 | LwIP `MEM_SIZE` 池(独立) | + +多协议兼容(05)必然带来 **任务栈线性叠加** → 必须做堆预算;与「一通道一 RB」正交。 + +--- + +## 3. 已合入的缓解(保留) + +| 措施 | 效果 | +|------|------| +| 青海等帧队列静态化 | 少占运行时堆 | +| 工厂任务栈减半;interrupt 不重建任务 | 少占 / 少碎片 | +| TCP Server 断开信号量静态化 | 启动少一次动态分配 | +| 堆曾扩至 40KB 并迁 CCM | 历史止血;**已按 06 回退** | + +排障:区分「RB 太多」误判;用 `xPortGetFreeHeapSize` / 任务栈清单;扩堆前看 06 占用账。 + +--- + +## 4. 与 06 的衔接(现行) + +| 项 | 现行代码 | +|----|----------| +| `configTOTAL_HEAP_SIZE` | **36×1024** | +| `configAPPLICATION_ALLOCATED_HEAP` | **0**(`heap_4` static `ucHeap` → SRAM `.bss`) | +| CCM | 无 `ucHeap`;仅显存/BSRR | + +待办(非本文件):`xPortGetMinimumEverFreeHeapSize` 标定、IAP 升级冒烟。 + +- 05 目录不再把「堆留 CCM」写成结构方案。 diff --git "a/doc/05_\345\215\217\350\256\256\346\250\241\345\235\227\345\244\232\345\215\217\350\256\256\345\205\274\345\256\271\344\274\230\345\214\226/README.md" "b/doc/05_\345\215\217\350\256\256\346\250\241\345\235\227\345\244\232\345\215\217\350\256\256\345\205\274\345\256\271\344\274\230\345\214\226/README.md" new file mode 100644 index 0000000..324fbee --- /dev/null +++ "b/doc/05_\345\215\217\350\256\256\346\250\241\345\235\227\345\244\232\345\215\217\350\256\256\345\205\274\345\256\271\344\274\230\345\214\226/README.md" @@ -0,0 +1,35 @@ +# 05 — 多协议共享 RB(已落地) + +> **范围**:一物理通道一环缓、多协议链式解析、协议绑定与兼容门禁、协议帧 queue 深度。 +> **状态**:功能 **已实现**(2026-08-14 对齐代码;含 RB/queue 瘦身修订)。 +> **非本目录**:SRAM/CCM 分区、堆 → [`../06_SRAM内部分数据迁移/`](../06_SRAM内部分数据迁移/README.md);LDI 依赖 IAP 配置存储 → [`../07_LDI与IAP配置解耦/`](../07_LDI与IAP配置解耦/README.md)。 + +--- + +## 文档结构 + +| 文件 | 角色 | +|------|------| +| [01_architecture.md](./01_architecture.md) | **设计规范**(现行) | +| [02_as_built_status.md](./02_as_built_status.md) | **落地对照**(代码事实 + DoD + queue 深度) | +| [03_freertos_heap_side_effect.md](./03_freertos_heap_side_effect.md) | **副作用归档**:多协议任务挤 FreeRTOS 堆(结构治理在 06,已回退) | + +## 与 06 / 07 的分工 + +| | 05 多协议共享 RB | 06 RAM | 07 LDI↔IAP 配置 | +|--|------------------|--------|-----------------| +| 主题 | 谁写哪个 RB、链式 probe、**帧 queue 深度** | 堆/显存/RB **放哪块物理 RAM**、占用账 | 选编 LDI 时的配置存储依赖 | +| 权威 | 本目录(协议行为) | 内存分区 **唯一权威** | 配置解耦(**已落地**,`app_board_net_cfg`) | +| 代码状态 | **已合入** | Phase A + 后续瘦身 **已合入** | **已落地**;历史选编规则见 07/`03` | + +旧版「8 槽 `g_rb0…7` @ CCM、IAP 双槽 RS485+UDP、LDI 绑 485」已废止;若与本目录冲突,以 `01`/`02` 现行稿为准。 + +## 快速结论 + +- 三槽:**`RJ45=1536` / `RS485=768` / `RS232=768`**,体在 SRAM,`RB_PROVIDE_WEAK` 按需提供。 +- 帧 queue(静态,不占 `ucHeap`):IAP **2** / LDI **4** / 青海 **3** / RLS **2** / AH_MQTT **3**(RLS/AH 为源码口径;现行 EIDE Debug 未编入,仅 Makefile 全编)。 +- 同物理口多协议:包级写入去重 + 持锁链式 probe。 +- IAP 仅 UDP;LDI 仅 TCP/UDP(RJ45);RS232_1 语音旁路禁 bind。 +- 目录选编:排除协议目录可裁解析器;现行 **EIDE Debug 已排除 RLS/AH_MQTT**(仅编 IAP+LDI+青海),**Makefile 全量编入**;配置层已抽至 `Application/Config/app_board_net_cfg.c`,**排除整个 IAP/ 目录不影响 LDI 链接**(见 07)。 + +阅读顺序:`01` → `02`;堆/CCM → `06`;LDI 选编 → `07`。 diff --git "a/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/01_background_and_problem.md" "b/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/01_background_and_problem.md" new file mode 100644 index 0000000..30bca20 --- /dev/null +++ "b/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/01_background_and_problem.md" @@ -0,0 +1,67 @@ +# 01 背景、问题与需求(RAM) + +> **状态**:现行 | 2026-08-14 +> **数字权威**:[04_current_memory_occupancy.md](./04_current_memory_occupancy.md) +> **协议 RB 行为**:见 doc/05(已落地,本篇不重复) + +--- + +## §1 问题(历史)与现行 + +STM32F407:SRAM 128KB + CCM 64KB = **192KB**。 + +### 1.1 曾出现的故障形态(Phase A 堆在 CCM) + +| 域 | Used | 上限 | 状态 | +|----|------|------|------| +| SRAM | ~95464B | 128KB | 健康 | +| CCM | 79168B | 64KB | **溢出**(堆 40KB + 1-969 显存) | + +根因:**Phase A 把堆放进 CCM**,与大屏显存互斥。不是芯片总容量不足。 + +### 1.2 现行(堆回 SRAM 36KB + 瘦身后) + +| 域 | Used | 上限 | 状态 | +|----|------|------|------| +| SRAM | ≈120820B | 128KB | **健康(~92%)** | +| CCM | 38208B | 64KB | **健康(~58%,仅显存)** | + +口径:EIDE 常编 **1-577 3×3**(CCM 与 1-969 同 38208)。详见 [04](./04_current_memory_occupancy.md)。 + +### 易混数字 + +| 说法 | 正确含义 | +|------|----------| +| 溢出 ~13KB | **历史** CCM(堆+显存)相对 64KB | +| 堆回 SRAM 缺口 | 当时 ≈5.4KB;以 RTT 等腾挪后已消除 | +| 现行 SRAM ~92% | **链接态**;仍建议实机看堆水位 | + +--- + +## §2 需求 + +| ID | 需求 | 状态 | +|----|------|------| +| R-1 | 堆 @ SRAM(回退 Phase A);现行 **36KB** | ✅ 已执行 | +| R-2 | 显存 @ CCM;大屏可链接并留余量 | ✅ CCM 余 ≈26.7KB | +| R-3 | DMA/ETH 数据永留 SRAM | ✅ 遵守中 | +| R-4 | 迁移与 SRAM 瘦身同序列(RTT 4→2KB 等) | ✅ 已执行 | +| R-5 | 数字标注构建/模组/日期 | ✅ 纪律 | +| R-6 | RB/DMA/queue 按产品口径再瘦身 | ✅ 见 [05 附录](./05_migration_plan.md)、doc/05 | + +RB 相关功能需求已在 **doc/05** 完成,不列入本迁移待办。 + +--- + +## §3 约束(硬) + +1. DMA / ETH / lwIP pbuf 数据 → **只能 SRAM** +2. CCM 无 DMA → 只适合堆、显存、纯软件缓冲 +3. 改 RAM 前写清 **释放量 / 风险 / 回滚**;改后重采 [04](./04_current_memory_occupancy.md) + +--- + +## 修订 + +- 2026-08-14:从旧 `00`/`04`/`07` 收敛为本篇。 +- 2026-08-14:§1 改为「历史故障 + 现行占用」;增加 R-6。 diff --git "a/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/02_memory_policy.md" "b/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/02_memory_policy.md" new file mode 100644 index 0000000..2b7ca68 --- /dev/null +++ "b/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/02_memory_policy.md" @@ -0,0 +1,74 @@ +# 02 存储分区宪法(RAM) + +> **状态:冻结**。本仓库 **SRAM/CCM 归属** 的最高决策。 +> **修订**:走 README §1。 +> **协议谁绑哪个 RB**:doc/05;本文件只定 **物理段归属**。 + +--- + +## §1 三条原则 + +### ① SRAM = 固定大小数据 + +| 主体 | 宪法位置 | 现行 | 动作 | +|------|----------|------|------| +| ucHeap 36KB | SRAM `.bss` | **SRAM** | ✅ Phase A 已回退 | +| 协议帧队列 | SRAM | SRAM | 维持 | +| 协议 RB | SRAM | SRAM(provide **1536/768/768**) | 维持(05 已落地;尺寸以 05 为准) | + +### ② CCM = 显存 / BSRR(性能路径) + +| 主体 | 宪法位置 | 现行 | +|------|----------|------| +| pixel_map / hub75_buff / g_bsrr / row_dst | CCM | CCM | + +### ③ CCM 预留大屏余量 + +1-969 ≈38208B(58.3%);堆回退后余 ≈26.7KB。 +堆不得长期占 CCM。 + +--- + +## §2 方向方向 + +1. **Phase A 回退**:✅ `configAPPLICATION_ALLOCATED_HEAP`→0;删 `freertos_heap_ccm.c`;`heap_4` static ucHeap → SRAM(现行 **36KB**)。 +2. **显存留 CCM**:不翻案。 +3. **RB 留 SRAM**:行为归 05;禁止再把调度 RB 迁回 CCM「给堆让位」。 +4. **1-969 不是另案**:堆回退后自然容纳。 + +配套瘦身:RTT Up 4KB→2KB(见 [05_migration_plan.md](./05_migration_plan.md))。 + +--- + +## §3 必须保留的事实 + +### DMA/ETH 门禁(永留 SRAM) + +ram_heap、RX_POOL、PBUF_POOL、ETH 描述符、UART RX DMA、`s_dma_bounce`;`_ptr_in_ccm` 检测保留。 + +### 构建隔离 + +占用权威:EIDE Debug(**1-577 3×3**;exclude 1-260 / 1-969 / RLS / AH)→ [04](./04_current_memory_occupancy.md)。 +Makefile(1-260 + RLS/AH 全编)数字不得与之混用。 + +--- + +## §4 决策矩阵 + +| 决策项 | 裁定 | +|--------|------| +| ucHeap | **SRAM**(36KB) | +| RB | **SRAM**(已到位) | +| 显存 | **CCM** | +| 帧队列 | **SRAM** | + +优先级:DMA 门禁 > 数据性质 > 性能。 + +--- + +## §5 长期验收(迁移完成后) + +- [x] EIDE 大屏链接成功(Phase A 验证口径 1-969;现行 1-577 3×3,CCM 同为 38208);更新 04 +- [x] `ucHeap` 不在 `0x1000…` +- [ ] ETH/UART/字库冒烟 +- [ ] `xPortGetMinimumEverFreeHeapSize` 重标定 diff --git "a/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/03_as_built_baseline.md" "b/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/03_as_built_baseline.md" new file mode 100644 index 0000000..784754d --- /dev/null +++ "b/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/03_as_built_baseline.md" @@ -0,0 +1,71 @@ +# 03 项目架构基线(RAM 相关) + +> **角色**:链接/构建与堆配置的代码级事实 +> **占用数字** → [04](./04_current_memory_occupancy.md) +> **RB / queue 行为与深度** → doc/05 + +--- + +## §1 两套构建 + +| | Makefile | EIDE Debug(现行常见) | +|--|----------|------------------------| +| 显示 | 默认可含 `1_260` | **仅 1-577**(exclude 1-260/1-969;宏 3×3 → CCM **38208**) | +| 协议 | **全量**:IAP / LDI / 青海 / RLS / AH_MQTT | **IAP + LDI + 青海**(exclude RLS / AH) | +| Kernel | 同(initcall / ring_buffer / bit_utils / crc_utils / text_cvt / `bcc_utils.c`) | 同左 | +| 堆 | 同:`heap_4` static ucHeap @ SRAM **36KB** | 同 | +| RB | 同:三槽 provide @ SRAM **1536/768/768** | 同 | +| 占用权威 | 对照用 | **现行 04**(须标注模组) | + +> 1-577 3×3 与 1-969 默认拼屏的 CCM 显存合计均为 **38208B**,勿仅凭 CCM 大小反推模组名。 + +--- + +## §2 链接与启动 + +- `Compiler/STM32F407XX_FLASH.ld`:FLASH 0x08040000/768K;RAM 128K;CCMRAM 64K;`.ccmram (NOLOAD)` +- `startup.c`:清 `_sccmram.._eccmram` +- `_Min_Heap_Size=0x400` / `_Min_Stack_Size=0xA00`(合计 0xE00)→ `._user_heap_stack` 实测 **3588B** + +--- + +## §3 FreeRTOS 堆(已回退) + +``` +configTOTAL_HEAP_SIZE = 36*1024 +configAPPLICATION_ALLOCATED_HEAP = 0 +ucHeap ∈ heap_4.c static → .bss (SRAM) +``` + +`freertos_heap_ccm.c` 已删除。执行记录见 [05_migration_plan.md](./05_migration_plan.md)。 + +--- + +## §4 显存尺寸(CCM) + +| 模组 | CCM 合计 | 口径 | +|------|----------|------| +| 1-260 | ~2432B | 小屏 | +| 1-577 1×1 | 较小 | 改宏 | +| 1-577 **3×3** | **38208B** | EIDE 现行常见(与 1-969 同量级) | +| 1-969 | **38208B** | map 实测 | + +刷新路径无 DMA → 可留 CCM。 + +--- + +## §5 UART DMA(SRAM) + +| 缓冲 | 尺寸 | 说明 | +|------|------|------| +| `s_rs485_buf` | **640** | ≥ RLS 530;≤ RB−1 | +| `s_rs232_0_buf` | **640** | ≥ QH 259;≤ RB−1 | +| `s_rs232_1_buf` | **已删除** | USART6 仅 TTS TX | + +[待更新: 2026-08-18 UART RX 改乒乓双缓冲 circular,两缓冲均为 640→1280(块 640×2);本表为历史基线,现行账见 04] + +--- + +## §6 与 doc/05 的接口 + +调度层 RB 槽、provide、**帧 queue 深度**在 Application;本目录只要求其 **段在 SRAM**,并在 04 记账。 diff --git "a/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/04_current_memory_occupancy.md" "b/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/04_current_memory_occupancy.md" new file mode 100644 index 0000000..902c4ad --- /dev/null +++ "b/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/04_current_memory_occupancy.md" @@ -0,0 +1,277 @@ +# 04 当前 SRAM / CCMRAM 占用分布(实测) + +> **角色**:本目录 **唯一占用实测账本** +> **状态**:现行 | 采样:**EIDE Debug(GCC 15.2)** / 编入 **1-577 3×3**、排除 1-260 / 1-969 / RLS / AH_MQTT / **2026-08-14 16:37**(重建复核,数字未漂移) +> **迁移**:[05_migration_plan.md](./05_migration_plan.md)(Phase A + 后续瘦身已执行) +> **RB / queue 语义**:doc/05 + +**结论**:SRAM **≈120820B(92.2%)**;CCM **38208B(58.3%)**。`ucHeap` @ SRAM **36KB**;CCM 仅显存。全局约 159KB / 192KB,空闲约 **37.5KB**。 + +--- + +## §0 采样口径 + +| 项 | 值 | +|----|-----| +| 构建 | **EIDE Debug**(Arm GNU Toolchain 15.2 / GCC);`outDir=build` → `build/Debug/Project_STD.elf`(对象在 `build/Debug/.obj/`) | +| 模组(CCM) | 编入 **1-577 3×3**(`.eide/eide.yml` exclude 1-260 / 1-969);CCM **38208B**,与 1-969 同量级 | +| 协议选编 | 编入 **IAP + LDI + 青海**;exclude **RLS / AH_MQTT** | +| 段合计 | `.data` 1612 + `.bss` 115620 + `._user_heap_stack` 3588 = **120820** | +| CCM | `.ccmram` **38208** | +| 结果 | **链接成功** | + +与「仅 1-260 小屏」数字 **严禁混用**。**勿用 `make` 重采**:Makefile 现编 1-260 + RLS/AH(见 `03` §1),覆盖 elf 后本表数字不复现。 + +| 段 | 大小 | +|----|------| +| `.data` | 1612B | +| `.bss`(含 ucHeap 36KB) | 115620B | +| `._user_heap_stack` | 3588B | +| **SRAM≈** | **120820B(92.18%)**;空闲 **≈10252B** | +| `.ccmram` | **38208B(58.30%)**;空闲 **≈27328B** | + +--- + +## §1 CCMRAM(仅显存) + +| 对象(1-577 3×3 / 同量级 1-969) | 大小 | +|----------------------------------|------| +| pixel_map + hub75_buff + row_dst + g_bsrr | **38208** | +| **合计** | **38208** | + +CCM 中 **0 字节** ucHeap / RB / UART DMA。 + +--- + +## §2 SRAM + +### 关键符号(nm,2026-08-14 16:37,EIDE Debug 产物) + +| 对象 | 大小 | 说明 | +|------|------|------| +| `ucHeap` | **36864(36KB)** | FreeRTOS heap_4 @ SRAM | +| `_acUpBuffer` | 2048 | RTT Up(已 4→2KB) | +| `rb_provide_rj45_buf` | **1536** | RJ45 RB | +| `rb_provide_rs485_buf` | **768** | RS485 RB | +| `rb_provide_rs232_buf` | **768** | RS232 RB | +| `s_rs485_buf` / `s_rs232_0_buf` | **1280** / **1280** | UART DMA 乒乓双缓冲(640×2);无 RS232_1 | +| `s_iap_queue_buf` | **2104** | 深度 2 | +| `s_ldi_queue_buf` | **2080** | 深度 **4** | +| `s_qh_queue_buf` | **801** | 深度 **3** | +| ETH/LwIP 池 | ram_heap / RX_POOL / PBUF 等 | 永留 SRAM(大户) | +| LwIP memp 池(netconn/udp_pcb) | 8×netconn + 8×udp_pcb(2026-08-21 起 `lwipopts.h` 显式 `MEMP_NUM_NETCONN=8`/`MEMP_NUM_UDP_PCB=8`,原 opt.h 默认 4) | 实测 bss 增量 **304B**;**新增 UDP 端口协议时随通道数核算本行**(每通道常驻 1 netconn + 1 udp_pcb,广播复用常驻 conn 不占池) | + +`s_rls_queue_buf`(**1076**,深度 2)与 AH_MQTT 队列(~1623,任务内 static)为**源码口径**,**不在本采样 elf 内**(EIDE Debug 排除 RLS/AH;仅 Makefile 构建时才计入 bss)。 + +### 协议 RB(SRAM) + +| 槽 | 容量 | 可用(size−1) | +|----|------|----------------| +| RJ45 | 1536 | 1535 | +| RS485 | 768 | 767 | +| RS232 | 768 | 767 | + +### 协议帧 queue(SRAM 静态;2026-08-14 变更) + +| 协议 | 深度 | 缓冲约 | 变更记录 | 本采样 | +|------|------|--------|----------|--------| +| IAP | 2 | 2104 | 维持 | ✅ 编入 | +| LDI | 4 | 2080 | **2→4** | ✅ 编入 | +| 青海 | 3 | 801 | payload 259 + **深度 3** | ✅ 编入 | +| RLS | 2 | 1076 | 维持 | — EIDE 排除 | +| AH_MQTT | 3 | ~1623 | **1→3**(任务内;initcall 关则未激活路径) | — EIDE 排除 | +| 山东 | 3 | 801 | 新增(payload 259,与青海同构) | — EIDE 排除 | +| 贵州 | 3 | 801 | 新增(payload 259,与青海同构;13 命令含 0x01/0x02) | — EIDE 排除 | +| 云南 | 3 | 801 | 新增(payload 259,与青海/贵州同构;13 命令含 0x01/0x02) | — EIDE 排除 | +| 重庆CQ | 3 | 3156 | 新增(payload 1044,JSON `{` 定界 + 12B 二进制);**队列体+任务帧缓冲+JSON/文本缓冲置 CCMRAM**(SRAM 全协议构建仅余 ~1.9KB;CPU 独占访问无 DMA,见文末修订) | ✅ 编入(CCM) | + +> **重庆CQ 内存例外(2026-08-20)**:CQ 队列 `s_cq_queue_buf` 3156B + `s_cq_queue_cb` 80B + +> 任务帧缓冲 1052B + JSON 缓冲 1045B + 文本缓冲 1044B = **6377B 置 `.ccmram`**。 +> 理由:全协议 dev 构建 SRAM 仅余 ~1.9KB,CQ 帧队列 3236B 无法入 SRAM; +> 这些缓冲为 CPU 独占访问(osMessageQueue 静态内存经 CPU memcpy 读写、无 DMA/ETH), +> CCMRAM 可用(Makefile 1-260 显存 2432B;EIDE 1-577 口径 38208B + 6377B 亦不超 64KB)。 +> cJSON 解析树为 **FreeRTOS 堆瞬时分配**(钩子 pvPortMalloc/pvPortFree,ucHeap 36KB +> 内支出,解析后 `cJSON_Delete` 归还)。`cq_proto_handle_task`/`cq_proto_timer_task` +> 各 1KB 栈(ucHeap,地区协议处理任务 +1KB、定时任务 +1KB)。 + +语义与 Put 丢帧行为 → **doc/05** `01` §4.1 / `02` §2.1。 + +`size` 的 bss 列含 `.ccmram`,勿直接当 SRAM。 + +--- + +## §3 数字链(相对 Phase A 刚完成时) + +| 步骤 | SRAM 约 | CCM | +|------|---------|-----| +| Phase A 后(旧账,RB 仍 2304/512) | ~130280 / 99.4% | 38208 / 58.3% | +| + RB/DMA/queue 瘦身与修订(本采样) | **~120820 / 92.2%** | **38208 / 58.3%** | + +主要释放:删 RS232_1 DMA、DMA 2048→640、RJ45 RB 2304→1536;RS485/232 RB 512→768 略增;LDI/QH queue 加深略增。**净效果 SRAM 明显留白**。 + +> 注(2026-08-18):UART RX 已改乒乓双缓冲(s_rs485_buf/s_rs232_0_buf 640→1280×2),Makefile 口径实测 bss 130036(原 128724,+1312 = 双缓冲 1280 + 静态通道实例 32)、text 334256(+1184,框架/协议防御代码)。EIDE 口径总量以 EIDE 重采为准。 + +--- + +## §5 四川三协议增量账(Makefile 口径实测) + +采样:**Makefile GCC Debug**(`make -B -j8`,2026-08-14;编 1-260 + 全协议含 RLS/AH)。 +注意与 §0 口径不同(EIDE Debug 编 1-577 3×3 且排除 RLS/AH),两口径数字**严禁混用**。 + +| 段 | 基线(未编三协议) | 编入三协议后 | 增量 | +|----|--------------------|--------------|------| +| text | 319664 | **328280** | **+8616** | +| data | 1604 | **1620** | **+16** | +| bss(含 .ccmram 2432) | 123868 | **126396** | **+2528** | + +SRAM 合计(`.data + .bss(不含 ccmram) + ._user_heap_stack`):**≈125363B(95.7%)**,未超 124KB(126976B)上限。 + +增量构成(nm 实测): +- 帧 queue 静态体:`s_sc_etc_queue_buf` **471**(2026-08-15 ETC payload 64→149,0x0D 定界上限对齐 9K1F212701) + `s_sc_mtc_queue_buf` 246 + `s_sc_ol_queue_buf` **789**(2026-08-17 治超 payload 249→255 = 长度字段上限 FF,消除 250~255 长帧队列截断越界读) = **1506**;`*_queue_cb` 3×80=240 +- 任务内帧缓冲(static bss):ETC 157 + MTC 82 + OL **263** = **502** +- 状态:`s_ol_lines` **200**(8 行 ×24B + `s_ol_line_len` 8B;2026-08-15 治超行数据改变长 ≤24B,原 8×16B=128B)、`s_etc_uptime/activity_s` 8、各 mask/句柄 36、杂项 ~20 +- 任务栈(ucHeap,非 bss):3 处理任务 = **3×1KB**(2026-08-17 ETC 心跳计时任务停用 -1KB,原 4×1KB;从 36KB ucHeap 支出) +- 数据段 +8:`s_mtc_color`/`s_mtc_font_size` 等初值静态;+8:`s_etc_color`/`s_ol_color`(显示颜色跟随通行灯状态) + +CCM 增量 **0**(无新 CCM 对象)。 + +--- + +## §6 山东协议增量账(Makefile 口径实测) + +采样:**Makefile GCC Debug**(`make -j8`,2026-08-17;编 1-260 + 全协议含 RLS/AH + 山东)。 +基线为 §5 末行口径(text 327408 / data 1620 / bss 126396),两次数值精确衔接(增量 = 山东模块贡献)。 + +| 段 | 基线(§5 末行口径) | 编入山东后 | 增量 | +|----|---------------------|------------|------| +| text | 327408 | **328872** | **+1464** | +| data | 1620 | **1620** | **+0** | +| bss(size 工具口径,含 .ccmram 2432) | 126396 | **127556** | **+1160** | + +SRAM 合计(相对 §5 基线增量 +1160):**≈126523B(96.5%)**,未超 124KB(126976B)上限。 + +增量构成(nm 实测): +- 帧 queue 静态体:`s_sd_queue_buf` **801**(3×267,payload 259)+ `s_sd_queue_cb` **80** = 881 +- 任务内帧缓冲(static bss):**267** +- 状态:`s_sd_mask`/`s_sd_mask_rs232`/队列句柄 **12** +- 任务栈(ucHeap,非 bss):`sd_handle_task` **1×1KB**(地区协议处理任务 3→4) +- text +1464:probe/parse/cmd/注册代码 + 版本串 rodata 10B + +CCM 增量 **0**(无新 CCM 对象)。 + +2026-08-17(默认显示注册机制 + 版本应答 PROGRAM_CODE)追加采样(同口径 `make -B -j8`): + +| 段 | 山东基线 | 本任务后 | 增量 | +|----|----------|----------|------| +| text | 328872 | **329112** | **+240** | +| data | 1620 | **1620** | **+0** | +| bss(size 工具口径,含 .ccmram 2432) | 127556 | **127564** | **+8** | + +增量构成(nm 实测): +- `s_default_fn`(`app_default_display.c` 注册槽)**4B bss** + 链接尾部对齐 4B → .bss 净 **+8**(.ccmram 2432 / ._user_heap_stack 3588 不变) +- 新增 `s_sd_default_text` 22B rodata + `_sd_default_show` / `app_default_display_register` / 回退渲染代码 + initcall 条目 8B + 名称字符串 + PROGRAM_CODE 在 `app_sd_proto_cmd.c` 的 rodata 副本;删除 `s_sd_version_text` 10B → text 净 **+240** + +SRAM 合计(§5 基线 +1160 +8):**≈126531B(96.5%)**,未超 124KB(126976B)上限。 +CCM 增量 **0**(无新 CCM 对象)。 + +2026-08-17(文本常量字节数组 → UTF-8 字符串字面量 + 山东上电画面改黄字)追加采样(同口径 `make -B -j8`): + +| 段 | 山东基线 | 本任务后 | 增量 | +|----|----------|----------|------| +| text | 329112 | **329456** | **+344** | +| data | 1620 | **1620** | **+0** | +| bss(size 工具口径,含 .ccmram 2432) | 127564 | **127564** | **+0** | + +增量构成(nm 实测): +- rodata(GBK 字节数组 → UTF-8 字面量,汉字 2B→3B):`s_sd_default_text` **22→33B(+11,0x21)**;MTC 固定标签 7 个 **+24**(34→58);MTC 初始化「祝您一路平安」**+7**、自检「系统正在加电自检」**+9**;MTC 固定语音表(`s_mtc_fixed_voices` 指针表 32B + 8 串 206B)**+58**;青海文明用语表(`s_civil_texts` 指针表 16B + 4 串 130B)**+40**('0' 同时修正为 doc/04 裸机文本「贵州高速公路」,原数组字节有损)→ 合计 **+149** +- code(约 **+195**):MTC/QH 语音播报前 UTF8ToGBK 运行时转换调用点(`UTF8ToGBK` 本体早已随 `app_render` 链接,无新增);MTC 初始化/自检的静态 GBK 数组改为栈缓冲 + 运行时转换;`_sc_mtc_render_center` 改 `FONT_ENC_UTF8` +- `s_sc_etc_lane_closed_text`(ETC「车道关闭」23B→34B):调用方已停用,符号被 --gc-sections 丢弃,**0 增量** +- bss 无变化:转换缓冲全部为函数内栈/局部,未引入静态存储 + +SRAM 合计不变:**≈126531B(96.5%)**,未超上限。 +CCM 增量 **0**。 + +2026-08-17(dev_display_fill 越界下溢修复)追加采样(同口径 `make -j8`,插桩已清理): + +| 段 | 山东基线 | 本任务后 | 增量 | +|----|----------|----------|------| +| text | 329456 | **329472** | **+16** | +| data | 1620 | **1620** | **+0** | +| bss(size 工具口径,含 .ccmram 2432) | 127564 | **127564** | **+0** | + +增量构成:`dev_display_fill` 起点越界早退分支(`x >= screen_rows || y >= screen_cols` 丢弃)≈ +16B code;无新增静态存储。SRAM / CCM 不变。 + +2026-08-17(贵州协议接入)追加采样(同口径 `make -j8`): + +| 段 | 贵州基线 | 本任务后 | 增量 | +|----|----------|----------|------| +| text | 329472 | **333072** | **+3600** | +| data | 1620 | **1620** | **+0** | +| bss(size 工具口径,含 .ccmram 2432) | 127564 | **128724** | **+1160** | + +增量构成(nm 实测): +- 帧 queue 静态体:`s_gz_queue_buf` **801**(3×267,payload 259)+ `s_gz_queue_cb` **80** = 881 +- 任务内帧缓冲(static bss):`msg_buf` **267** +- 状态:`s_gz_mask`/`s_gz_mask_rs232`/队列句柄 **12** +- 任务栈(ucHeap,非 bss):`gz_handle_task` **1×1KB**(地区协议处理任务 4→5) +- text +3600:probe/parse/cmd/voice 四文件代码(`gz_parse_frame` 0x20C、`_gz_exec_fixed` 0x268、`gz_execute_cmd` 0xAC 等)+ rodata(文明用语 4 串 UTF-8、颜色/亮度/音量映射表、队列/任务 attr) + +SRAM 合计(贵州基线 +1160):**≈127691B(97.4%)**,未超 124KB(126976B)上限。 +CCM 增量 **0**(无新 CCM 对象)。 + +2026-08-24(云南协议接入)追加采样(同口径 `make clean && make -j8`,对照构建同树剔除云南实测增量;数字为同日用户决定 1~10 修订后重采): + +| 段 | 云南基线(剔除云南) | 修订后 | 增量 | +|----|----------------------|----------|------| +| text | 349796 | **352860** | **+3064** | +| data | 1656 | **1656** | **+0** | +| bss(size 工具口径,含 .ccmram) | 136772 | **138196** | **+1424** | + +增量构成(对照构建实测;初版 +3120/+1168,修订后 自检单字缓冲 256B 入 bss、default 文件代码移除): +- 帧 queue 静态体:`s_yn_queue_buf` **801**(3×267,payload 259)+ `s_yn_queue_cb` **80** = 881 +- 任务内帧缓冲(static bss):`msg_buf` **267** +- 自检单字全屏缓冲(static bss):`_yn_selftest_fill_char` 内 `buf` **256** +- 状态:`s_yn_mask`/`s_yn_mask_rs232`/队列句柄 **12** + 尾部对齐 8 +- 任务栈(ucHeap,非 bss):`yn_handle_task` **1×1KB**(地区协议处理任务 5→6);`yn_selftest_task`(1KB)为瞬态任务(上电熄灭任务 `yn_bootoff_task` 已随修订移除) +- text +3064:probe/parse/cmd/voice 四文件代码(含自检老化循环显示序列)+ rodata(文明用语 4 串、PROGRAM_CODE 应答、映射表、队列/任务 attr) + +SRAM 合计(云南基线 +1424):**≈129115B(98.5%)**,未超 124KB(126976B)上限。 +CCM 增量 **0**(无新 CCM 对象)。 + +--- + +## §4 复现 + +**前提**:`build/Debug/Project_STD.elf` 须为 **EIDE Debug** 产物。若先执行 `make`(编 1-260 + RLS/AH)会覆盖该 elf,本表数字不复现。 + +```bash +arm-none-eabi-size -A build/Debug/Project_STD.elf | grep -E '^\.(data|bss|ccmram|_user)' +arm-none-eabi-nm -S build/Debug/Project_STD.elf | grep -E 'ucHeap|rb_provide_.*_buf|s_.*_queue_buf|s_rs485_buf|s_rs232' +# ucHeap 须在 0x2000…,勿在 0x1000… +``` + +--- + +## 修订 + +- 2026-08-14:Phase A 后首测(130280 / 38208)。 +- 2026-08-14 14:45:对齐 RB 1536/768/768、DMA 640、queue 深度表;重采 SRAM ≈120820。 +- 2026-08-14 16:37:更正采样口径为 **EIDE Debug**(原误标 Makefile/GCC Debug);标注 RLS/AH 未编入与 Makefile 重采警告。 +- 2026-08-14:新增 §5 四川三协议增量账(Makefile 口径:text +7624 / data +8 / bss +1144;SRAM 124192B=94.7%,未超 124KB 上限;CCM 增量 0;ucHeap 多 4 任务栈 4KB)。 +- 2026-08-15:四川三协议显示语义对齐参考项目 9K1F212701(MTC 帧长双格式/治超 8 行与 0x80 全屏/ETC 6 行与 0x30 复位)+ TEST 键修复;重采 Makefile 口径 text +8264 / data +16 / bss +1208(治超行状态 64→128B、颜色状态 +2B);SRAM 合计 **124264B(94.8%)**,未超上限。 +- 2026-08-15(同日修订):治超帧长度上限 0x1E→FF(对齐 9K1F212701,0xFE 显式排除防 RLS 吞帧)+ `SC_OL_PAYLOAD_MAX` 32→249 + MTC 单行行号 '1'~'4'→'1'~'6';bss 重采 **125948(+2080)**、SRAM ≈**124915B(95.3%)**;build/Debug 曾发现陈旧 ELF(仅链四川协议),`make clean` 全量重建后恢复。 +- 2026-08-15(ETC 全屏修复):ETC 显示帧变长 0x0D 定界对齐 9K1F212701 etc.c(payload 64→149、全屏数据上限 56→145、全屏渲染先清屏再按屏宽换行);bss 增量 +344(queue_buf +255、任务帧缓冲 +85、对齐 +4),重采 bss **126292(+2424)**、SRAM ≈**125259B(95.6%)**,未超上限。 +- 2026-08-15(治超行帧变长修复):81~88 行数据改变长(=总长-6,≤24B 截断、不足不补空格、先清行再渲染,对齐 9K1F212701 makefonttolatt_oneline);`s_ol_lines` 8×16→8×24 + `s_ol_line_len[8]`(+72B);亮度映射改 (val+1)/32;修复应答帧 BCC 计算 off-by-one;重采 bss **126364(+2496)**、SRAM ≈**125331B(95.6%)**,未超上限。 +- 2026-08-17(发布前审查修复):治超 payload 249→255 消除长帧队列截断越界读(bss +18);MTC '8' 亮度兼容二进制/ASCII、'A' 4B/5B 判别加 b2 门限;ETC 黄闪 10 秒自动关闭计时(+4B)+ 车道关闭文案补全「请择道行驶」(rodata)+ 滚屏帧 0x0D 扫描起点改索引 6(rt/st 恰为 0x0D 不再误定界);ETC/MTC 行渲染与 MTC '4'/治超 80 全屏渲染补先清后画(对齐 9K1F212701 MakeSixteenLattAll/OneLine 清屏语义);ETC 0x40 亮度改 00~07→硬件 1~8(协议文档 §6,00 最暗);重采 text **328280(+8616)**、bss **126396(+2528)**、SRAM ≈**125363B(95.7%)**,未超上限。 +- 2026-08-17(MTC '}' 定界变长修复):'{' 帧族废除定长双长度与 '78' BCC 校验(删除 BCC 函数与定长 switch),probe 改 '}' 扫描;修复上位机 `{3 1 1234 } 3 }` 单行乱码。queue payload 仍 74B、深度 3 → bss **不变**;text 重采 **327624(-656)**;SRAM ≈**125363B(95.7%)**,未超上限。宿主推演 `~/EnvTools/CD-DebugTool-cpp/scripts/probe_sim/sc_mtc_frame_sim.py`。 +- 2026-08-17(ETC 心跳停用 + MTC '4' 全屏换行修复):ETC 心跳计时任务 #if 0 停用、不再创建 → ucHeap 任务栈 **-1KB**(4→3);「ETC车道关闭」5 分钟超时显示与黄闪 10 秒自动关闭一并失效(0A 38 开启后须 0A 39 显式关闭;0A 50 帧解析保留);MTC '4' 全屏改整屏 word_wrap 渲染(原 16B/行切 ≤4 行,超宽溢出被裁、超 64B 丢弃);data/bss **不变**(1620/126396),text 重采 **327408(-216)**;SRAM ≈**125363B(95.7%)**,未超上限。 +- 2026-08-17(新增山东协议):ProtocolParser_ShanDong 六文件接入 Makefile(EIDE Debug 目标排除);§6 增量账:text **+1464**、bss **+1160**(queue 801 + cb 80 + 帧缓冲 267 + 状态 12)、data 不变;ucHeap 任务栈 +1KB(3→4);SRAM ≈**126523B(96.5%)**,未超 124KB(126976B)上限。 +- 2026-08-18(框架/驱动层缺陷修复):UART RX 单缓冲 IDLE → **乒乓双缓冲 circular**(HT/TC+IDLE 增量冲刷,s_rs485_buf/s_rs232_0_buf 640→1280×2,bss +1280B);channel_send 与 frame_dispatch_task 增加通道指针回验;W25Qxx 加 SPI 互斥;TCP Server 改串行单客户端、UDP/TCP 通道静态化(+32B)。重采(Makefile 口径):bss **130036(+1312)**、text **334256(+1184)**、data 不变。 +- 2026-08-17(默认显示注册机制 + 山东版本应答 PROGRAM_CODE):新增 `app_default_display.c`(注册制默认显示,回退欢迎画面)+ `app_sd_proto_default.c`(山东上电画面注册)+ `app_boot.h`(PROGRAM_CODE 共用,开机画面与版本应答);`'2'` 版本应答改裸 ASCII PROGRAM_CODE(删除占位 `SD_FX_1.0`);重采 text **329112(+240)**、bss **127564(+8:s_default_fn 4B + 尾部对齐 4B)**、data 不变;SRAM ≈**126531B(96.5%)**,未超上限。 +- 2026-08-17(文本常量字节数组 → UTF-8 字符串字面量):山东/四川ETC 显示文本、四川MTC 固定标签与语音表、青海文明用语表全部改为 UTF-8 字面量(语音发送前 UTF8ToGBK 运行时转 GBK;显示改 FONT_ENC_UTF8);山东上电画面 COLOR_GREEN→COLOR_YELLOW;青海文明用语 '0' 同时修正为 doc/04 裸机文本「贵州高速公路」(原数组字节有损);青海自检/费额语音补 UTF8ToGBK 转换(原以 UTF-8 字节直送 GBK 语音板);重采 text **329456(+344,rodata +149 + 转换代码 ~+195)**、bss **127564(+0)**、data 不变;SRAM ≈**126531B(96.5%)**,未超上限。 +- 2026-08-17(dev_display_fill 越界下溢修复):`dev_display_fill` 起点越界(x≥rows / y≥cols)早退丢弃,消除 `screen_* - x/y` uint16 下溢巨值写穿 pixel_map(CCMRAM)→ HardFault 的缺陷(山东 '3' 行号 ≥3 在小屏构建触发:y=row*16 ≥ 32 屏高;故障地址 0x10010000 = CCMRAM 末尾+1);该修复同时保护青海/ETC/MTC 行渲染与 app_render 字形擦除路径;重采 text **329472(+16)**、bss/data 不变;SRAM ≈**126531B(96.5%)**,未超上限。 +- 2026-08-17(新增贵州协议):ProtocolParser_GuiZhou 四文件接入 Makefile(EIDE Debug 目标排除,与青海同列);§6 增量账:text **+3600**、bss **+1160**(queue 801 + cb 80 + 帧缓冲 267 + 状态 12)、data 不变;ucHeap 任务栈 +1KB(4→5);SRAM ≈**127691B(97.4%)**,未超 124KB(126976B)上限。宿主推演 `~/EnvTools/CD-DebugTool-cpp/scripts/probe_sim/gz_frame_sim.py`。 +- 2026-08-24(新增云南协议):ProtocolParser_YunNan 五文件接入 Makefile(EIDE Debug 目标排除,与青海/山东/贵州同列;PROTO=CQ 口径保留——`{` 帧族随既有纪律编入);§6 对照构建增量账:text **+3120**、bss **+1168**(queue 801 + cb 80 + 帧缓冲 267 + 状态 12 + 对齐 8)、data 不变;ucHeap 常驻任务栈 +1KB(5→6),自检/熄灭任务瞬态;SRAM ≈**128891B(98.3%)**,未超 124KB(126976B)上限。构建验证:PROTO=ALL text 352916/data 1656/bss 137940;PROTO=CQ text 339060/data 852/bss 134464,均链接通过。 +- 2026-08-24(云南协议修订,用户决定 1~10):移除 `app_yn_proto_default.c`(上电效果 default 文件,Makefile/eide.yml 同步删除,不再注册默认显示);'2' 自检改老化循环显示 + 每 5s 语音「系统正在自检」(可被下一帧打断,新增自检单字缓冲 static buf 256B);'8' 亮度 0x00 恢复光敏自动;0x01 全屏点亮扩 01红~07白 七色;0x02 应答改 PROGRAM_CODE(删硬编码 YN_FX_P5_1.0)。重采:**PROTO=ALL text 352860(-56)/ data 1656 / bss 138196(+256)**;**PROTO=CQ text 338996(-64)/ data 852 / bss 134720(+256)**,均链接通过。 +- 2026-08-20(新增重庆CQ协议):ProtocolParser_ChongQing 四文件 + cJSON 接入 Makefile(恒定编入,CQ 源在 LDI 之后);`PROTO=CQ` 剔除 LDI 目录 + `-DPROTO_CHONGQING`。Makefile 全协议口径重采:**text 348240(+12056,含 cJSON ~8.5K 代码)**、**data 1652(+32,cJSON hooks/全局指针)**、**bss 136456(+6436 = CCM +6377 + SRAM ~59)**。CQ 静态体构成(**全部置 `.ccmram`**):queue_buf 3156 + queue_cb 80 + 任务帧缓冲 1052 + JSON 缓冲 1045 + 文本缓冲 1044 = **6377B CCM**;SRAM 净增 ~59B(mask/计数器/hooks/UDP_CQ 通道实例),全协议构建 SRAM 余量仍 >1.7KB,链接通过。ucHeap 任务栈 +2KB(cq_proto_handle_task / cq_proto_timer_task 各 1KB)。cJSON 解析树为 ucHeap 瞬时分配(钩子 pvPortMalloc/pvPortFree)。**CCM 例外说明**:本项目 CCM 惯例为「仅显存」,CQ 帧队列因 SRAM 余量不足(全协议构建仅余 ~1.9KB)破例入 CCM——缓冲为 CPU 独占访问(无 DMA/ETH),Makefile 1-260 口径 CCM 2432+6377=8809B / EIDE 1-577 口径 38208+6377=44585B,均不超 64KB。 +- 2026-08-20(cJSON 升级官方 v1.7.18):`Middlewares/Third_Party/cJSON/` 由裸机拷贝 v1.x 老版(编译 3 条 -Wmisleading-indentation 警告)替换为 DaveGamble/cJSON **v1.7.18** 官方源(GitHub codeload tarball);parse 层类型判定改 `cJSON_IsString/IsNumber/IsObject` 辅助函数(`app_cq_proto_parse.c`),`cJSON_InitHooks` 换绑 FreeRTOS 堆不变(hooks 字段名仍 malloc_fn/free_fn),`cJSON_Parse`/GBK 字节直传用法不变。重采(Makefile 口径,cJSON 编译 warning 清零,仅余 HAL flash_ex 3 条既有 unused-parameter):全协议 text **350004(+1764,cJSON 代码 ~8.5K→~10.2K)**、data **1656(+4)**、bss **136460(+4)**;`PROTO=CQ` text **335796(+1772)**、data **852(+4)**、bss **132984(+4)**。cJSON 解析树瞬时堆仍 ucHeap 内 2~4KB 量级(cJSON 结构体尺寸不变,钩子仍 pvPortMalloc/pvPortFree,解析后 cJSON_Delete 归还)。 diff --git "a/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/05_migration_plan.md" "b/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/05_migration_plan.md" new file mode 100644 index 0000000..f2bc8a9 --- /dev/null +++ "b/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/05_migration_plan.md" @@ -0,0 +1,115 @@ +# 05 RAM 迁移方案(堆回退 + 1-969) + +| 项 | 值 | +|----|-----| +| 角色 | Phase A 回退与 SRAM 瘦身的 **唯一执行蓝本** | +| 状态 | **✅ 已执行**(2026-08-14):堆 **36KB @ SRAM** + RTT 4→2KB | +| 数字基线 | [04_current_memory_occupancy.md](./04_current_memory_occupancy.md) | +| 宪法 | [02_memory_policy.md](./02_memory_policy.md) | +| RB 行为 | 已完成,见 doc/05;本方案 **不改** 协议绑定 | + +--- + +## §0 审核与执行记录 + +原审核项(已按授权执行,堆改为 36KB 而非蓝本初稿 40KB): + +- [x] 堆从 CCM 迁回 SRAM(`configAPPLICATION_ALLOCATED_HEAP`→0,删 `freertos_heap_ccm.c`) +- [x] 与瘦身同提交;36KB 后缺口约 **1256B**,用 RTT Up 缓冲 4KB→2KB(约 −2KB)覆盖 +- [x] 优先候选:缩 SEGGER RTT(未动 AH_MQTT / LwIP) +- [x] 验收以 EIDE 大屏链接成功(Phase A 验证口径 1-969;现行 1-577 3×3,CCM 同为 38208)+ 更新 04 为准 +- [x] 授权执行:用户要求「执行修改,ucHeap 改为 36KB」 + +--- + +## §1 目标与收益 + +| 目标 | 推算 / 结果 | +|------|-------------| +| CCM 去掉 ucHeap | 79168→**38208**(58.3%),余 ≈26.7KB | +| 1-969 可链接 | 消除 CCM 溢出 | +| SRAM 容纳堆 | 95464+36864−RTT≈2KB ≤ 131072 | + +全局 192KB 够用;纠正的是 **位置**。堆从 40KB 改为 **36KB** 依赖此前任务栈压缩;须测水位 / `vApplicationMallocFailedHook`。 + +--- + +## §2 已落地机械步骤 + +### ① `Core/Inc/FreeRTOSConfig.h` + +- `configAPPLICATION_ALLOCATED_HEAP`:**1 → 0** +- `configTOTAL_HEAP_SIZE`:**40×1024 → 36×1024**,堆位 SRAM + +### ② 删除 `Core/Src/freertos_heap_ccm.c` + +并从 `.eide/eide.yml` / Makefile 去掉列举。 + +### ③ `heap_4.c` 零改动 + +宏为 0 后自动 `static ucHeap[]` → `.bss`。 + +### ④ 注释清理 + +- `dev_w25qxx.c` / `app_ldi_cfg.c`:去掉「任务栈必在 CCM」过时表述;**保留** bounce / 禁栈 DMA。 + +### ⑤ SRAM 瘦身 + +- `SEGGER_RTT_Conf.h`:`BUFFER_SIZE_UP` **4KB → 2KB** + +--- + +## §3 数字(执行前推算) + +``` +现状 SRAM 95464 ++ ucHeap 36864 (36KB) +− RTT 2048 += 130280 ≤ 131072 ✓ +``` + +未采用:排除 AH_MQTT、动 LwIP。 + +--- + +## §4 明确不改(本方案范围外) + +- 不改 doc/05 已落地的三槽 / 绑定 / 链式逻辑 +- 不把 RB 迁回 CCM +- 不把 DMA 池放进 CCM +- 不在未测情况下同时编入 1-260+1-577+1-969 三套显存 + +--- + +## §5 验收 + +- [x] 三步机械变更合入 +- [x] RTT 瘦身覆盖 36KB 回退缺口 +- [x] EIDE Debug **链接成功**(RAM 130280 / CCM 38208;Phase A 验证口径 1-969,现行 EIDE 编 1-577 3×3 同量级) +- [x] 更新 [04](./04_current_memory_occupancy.md) +- [x] `nm`:`ucHeap` @ SRAM(`0x2000…`,不在 `0x1000…`) +- [ ] ETH / UART / 字库 / IAP-UDP / LDI-TCP 冒烟 +- [ ] 记录 `xPortGetMinimumEverFreeHeapSize` 新基线 + +--- + +## §6 附录:Phase A 之后的 SRAM 瘦身(2026-08-14,已执行) + +与 Phase A 同属 RAM 治理,但**不改** 05 协议绑定语义(仅尺寸/深度): + +| 项 | 变更 | +|----|------| +| RB | 2304/512/512 → **1536/768/768** | +| UART DMA | 2048×3 → **640×2**(删 RS232_1) | +| 帧 queue | LDI **2→4**;青海 payload 259 + 深度 **3**;AH **1→3**;IAP/RLS 深度 **2** | +| RLS probe | `mem_pool` → `RLS_PAYLOAD_MAX` | + +占用重采见 [04](./04_current_memory_occupancy.md);queue/RB 产品说明见 **doc/05**。 + +--- + +## 修订 + +- 2026-08-14:由原 `06_heap_revert_and_1_969` 升为本目录执行蓝本;增加 **待审核门禁**;缺口定为 ≈5.4KB。 +- 2026-08-14:授权执行 — 堆 **36KB** 回 SRAM + RTT 4→2KB;删除 CCM 堆文件。 +- 2026-08-14:附录记录 RB/DMA/queue 后续瘦身;04 重采。 diff --git "a/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/README.md" "b/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/README.md" new file mode 100644 index 0000000..0e2c74f --- /dev/null +++ "b/doc/06_SRAM\345\206\205\351\203\250\345\210\206\346\225\260\346\215\256\350\277\201\347\247\273/README.md" @@ -0,0 +1,69 @@ +# 06 — RAM 规划与迁移 + +> **范围**:SRAM / CCM 分区宪法、占用实测、堆回退与大屏容纳、后续 SRAM 瘦身。 +> **状态**:规划 **现行**;**Phase A 已执行**(堆 36KB @ SRAM + RTT 4→2KB);**后续瘦身已执行**(RB/DMA/queue,2026-08-14)。 +> **非本目录**:多协议 RB/绑定/queue 深度语义 → [`../05_协议模块多协议兼容优化/`](../05_协议模块多协议兼容优化/README.md);LDI↔IAP 配置解耦 → [`../07_LDI与IAP配置解耦/`](../07_LDI与IAP配置解耦/README.md)(暂缓改码)。 + +--- + +## §0 总判(2026-08-14 修订) + +| 维度 | 结论 | +|------|------| +| 多协议共享 RB | **已完成**(见 05);体在 SRAM;现行 **1536/768/768** | +| 分区宪法 | SRAM=堆/队列/RB/DMA;CCM=显存/BSRR | +| 堆 | **36KB @ SRAM**;CCM 无 ucHeap | +| 链接 | 无阻塞;EIDE Debug 编 1-577(3×3)、排除 1-260/1-969/RLS/AH;与 1-969 显存同为 CCM 38208 | +| 占用账 | 见 [04](./04_current_memory_occupancy.md)(采样口径 **EIDE Debug**;须按构建/模组重采) | +| **下一步** | 冒烟 + 堆水位;**IAP 升级实机**待验 | + +--- + +## 文档结构 + +| 文件 | 角色 | +|------|------| +| [01_background_and_problem.md](./01_background_and_problem.md) | 背景与需求(历史 CCM 溢出) | +| [02_memory_policy.md](./02_memory_policy.md) | **分区宪法**(冻结) | +| [03_as_built_baseline.md](./03_as_built_baseline.md) | 链接/构建架构事实 | +| [04_current_memory_occupancy.md](./04_current_memory_occupancy.md) | **占用实测账本** | +| [05_migration_plan.md](./05_migration_plan.md) | Phase A 执行记录 + 后续瘦身附录 | + +--- + +## §1 全局原则(最高约束) + +1. **分区**:SRAM = 固定大小(FreeRTOS 堆、帧队列、协议 RB、UART DMA);CCM = 显存/BSRR + 大屏余量。 +2. **CCM 判据**:DMA/ETH 可达 → 永留 SRAM。 +3. **Phase A**:`ucHeap` 不得留 CCM(已满足)。 +4. **RB 位置**:默认 SRAM;尺寸/行为归 05,本目录约束段属性与占用。 +5. **数字隔离**:引用必须标注 `构建/模组/日期`。 +6. **改码后**:更新 04 实测。 + +--- + +## §2 状态表 + +| 事项 | 状态 | +|------|------| +| RB @ SRAM(三槽 provide) | ✅;尺寸 1536/768/768 | +| 显存 @ CCM | ✅ | +| 堆回 SRAM 36KB | ✅ | +| RTT Up 4→2KB | ✅ | +| UART DMA 瘦身 / 删 RS232_1 DMA | ✅(640 / 无 index1) | +| 协议帧 queue 深度修订 | ✅(见 05;占 SRAM 静态) | +| EIDE 大屏链接 | ✅ CCM ~58% | +| 堆水位 / IAP 实机 | ☐ 待测 | + +--- + +## §3 阅读顺序 + +| 目的 | 文档 | +|------|------| +| 占多少 | **04** | +| 该放哪 | **02** | +| 怎么迁(执行记录) | **05** | +| 协议 RB / queue | → **doc/05** | + +通读:01 → 02 → 03 → 04 → 05。 diff --git "a/doc/07_LDI\344\270\216IAP\351\205\215\347\275\256\350\247\243\350\200\246/01_background_and_impact.md" "b/doc/07_LDI\344\270\216IAP\351\205\215\347\275\256\350\247\243\350\200\246/01_background_and_impact.md" new file mode 100644 index 0000000..74a477c --- /dev/null +++ "b/doc/07_LDI\344\270\216IAP\351\205\215\347\275\256\350\247\243\350\200\246/01_background_and_impact.md" @@ -0,0 +1,156 @@ +# 01 背景、根因与影响 + +> **状态**:现行事实 | 2026-08-14 +> **触发**:EIDE 工程仅勾选 LDI 相关源文件时链接失败(见终端 `undefined reference to app_flash_iap_*` / `g_config`)。 +> **解耦方案** → [02_decoupling_solutions.md](./02_decoupling_solutions.md) +> **临时选编** → [03_interim_build_rules.md](./03_interim_build_rules.md) + +--- + +## §1 问题现象 + +典型链接错误(EIDE / unify_builder): + +``` +undefined reference to `app_flash_iap_is_config_valid' +undefined reference to `app_flash_iap_update_net_cfg' +undefined reference to `g_config' +``` + +出现在: + +- `Application/Src/LDI/app_ldi.c` → `ldi_ctx_init` +- `Application/Src/LDI/app_ldi_cmd.c` → `cmd_set_ip` + +Makefile 全量编译时不易暴露;**按协议目录裁剪(只留 LDI)时必现**。 + +--- + +## §2 背景与设计意图(为何当初耦合) + +板级存在两套「网络相关」持久化: + +| 存储 | 位置 | 模块 | 内容 | +|------|------|------|------| +| LDI 设备配置 | W25Qxx 末 4KB 扇区 | `app_ldi_cfg`(实际文件 `Application/Src/LDI/app_ldi_cfg.c`) | 车道、证书、模块表、IP/端口等 | +| IAP 系统配置 | 内部 Flash Sector1 `0x08004000` | `app_iap_cfg`(实际文件 `Application/Src/IAP/app_iap_cfg.c`;`Device/Inc/config_info.h` 为同名旧定义,全仓无引用,已废弃) | magic、升级状态、FW 信息、**net_cfg(IP/掩码/网关)** | + +产品需求(历史合理): + +1. Bootloader / Recovery / 主固件共享 **同一套上电 IP**(写在 Sector1)。 +2. LDI `0AH` 改 IP 后,除写入 W25 外,还要 **同步到 Sector1**,避免下次启动 Boot 仍用旧 IP。 +3. 上电时若 W25 无 LDI 配置,可 **回退读取** Sector1 的 `net_cfg`。 + +因此 LDI 直接调用了 `app_iap_cfg` API,而不是通过中立的「板级网络配置」抽象。 + +--- + +## §3 根因(精确到模块边界) + +### 3.1 依赖不是「IAP 协议」,而是「IAP 配置存储」 + +``` +LDI 协议模块 IAP 目录 +───────────────── ───────────────────────────── +app_ldi.c ──────────────► app_iap_cfg.c/.h ← 链接必需 +app_ldi_cmd.c app_iap.c/.h ← 升级协议,可裁 + app_iap_cmd.c ← 可裁 +``` + +未定义符号均来自 **`app_iap_cfg.c`**: + +| 符号 | 作用 | +|------|------| +| `g_config` | 映射到 `0x08004000` | +| `app_flash_iap_is_config_valid` | 校验 Sector1 记录 | +| `app_flash_iap_update_net_cfg` | 擦写更新 IP/掩码/网关 | + +### 3.2 调用点(代码事实) + +**上电 `ldi_ctx_init`**(`Application/Src/LDI/app_ldi.c:100-108`): + +- W25 有有效 LDI 配置,**且 Sector1 记录有效(`app_flash_iap_is_config_valid(g_config)` 为真)**,且与 Sector1 不一致 → `app_flash_iap_update_net_cfg`(`app_ldi.c:100-104`) +- W25 无效 → Sector1 有效则从 `g_config->net_cfg` 拷贝 IP(`app_ldi.c:108`);Sector1 也无效 → 用上电默认 IP +- **前置条件(已修复,2026-08-14)**:Sector1 为空/无效时原代码**不会**同步写入;现改为统一调用 `app_flash_iap_update_net_cfg`——空/损坏扇区均完整初始化覆盖(镜像 Bootloader 语义,损坏自愈;升级中间态——valid 且 update_sta≠updated——仅放行 net_cfg 字段更新、update_sta/app_info 原样保留,2026-08-18 修订,见 [02 §10](./02_decoupling_solutions.md))。出厂新机(Sector1 空)走 0AH 改 IP 后 Sector1 已有有效记录,Bootloader/Recovery 能读到该 IP(见 [02 §9](./02_decoupling_solutions.md)) + +**运行时 `cmd_set_ip`(0AH)**(`Application/Src/LDI/app_ldi_cmd.c:430-431`): + +- `app_flash_ldi_save_config`(W25)之后立刻 `app_flash_iap_update_net_cfg`(Sector1);两步写结果现均被检查:任一失败 0AH 应答 ErrCode=01H(00H=成功),固件不自动重试,由上位机重发恢复。失败/断电语义见 [02 §8](./02_decoupling_solutions.md) + +### 3.3 与 doc/05「目录选编」的张力 + +doc/05 目标:**工程目录选编协议**(exclude 即可裁剪)。 +现行 LDI 破坏了「只编 LDI 目录」的可链接性——exclude 整个 `IAP/` 会连配置层一起丢掉。 + +### 3.4 现状缺陷 A:空 Sector1 写路径不置 magic + +`app_flash_iap_update_net_cfg`(`Application/Src/IAP/app_iap_cfg.c:108-122`)为「读-改-net_cfg-写」,**从不写入 magic**: + +1. 空扇区(magic 与 crc 均为 0xFFFFFFFF)通过 `app_flash_iap_is_config_empty` 守卫后,仅改 net_cfg、重算 CRC、擦写回盘;落盘记录 magic 仍为 0xFFFFFFFF。 +2. 出厂新机(Sector1 从未写入)走 LDI 0AH 改 IP:擦写一次 Sector1,但记录仍「空」(Bootloader/Recovery 校验 magic 失败)→ Boot/Recovery 上电仍用默认 IP。 +3. **修复(已定案并落地,2026-08-14)**:空记录路径改为完整初始化,镜像老 Bootloader `Init_Config_Info` 首次上电语义(magic + update_sta=updated + app_info 全 0 且 crc32=0xFFFFFFFF + net_cfg + CRC32),0AH 同步时把 device_port 一并传入;4B02 改走同一路径。详见 [02 §9](./02_decoupling_solutions.md)。 + +### 3.5 现状缺陷 B:模块命名与陈旧注释勘误 + +- 不存在 `dev_flash_iap` / `dev_flash_ldi` 模块;实际实现是 `Application/Src/IAP/app_iap_cfg.c` 与 `Application/Src/LDI/app_ldi_cfg.c`,本目录文档统一按此引用。 +- `Device/Inc/config_info.h` 与 `Application/Inc/IAP/app_iap_cfg.h` 重复定义同一套 Sector1 结构体;`config_info.h` 全仓(含 Bootloader/Recovery 项目)无任何 include,为死代码。Bootloader/Recovery 各自另有独立的 `Drivers/BSP/Config/config_info.h`,结构体共 4 处独立定义、无共享头(布局锁定要求见 02 方案 A)。 +- LDI 记录大小注释陈旧:`app_ldi_cfg.c:24` 注释「116 byte = 29 words」为 MAX_MODULES=6 时代的数值;现 `APP_FLASH_LDI_MAX_MODULES=11`,实际 `sizeof(app_flash_ldi_record_t)=176B=44 words`。文档引用请以 176B 为准。 + +--- + +## §4 影响面 + +### 4.1 构建 / 产品裁剪 + +| 场景 | 结果 | +|------|------| +| EIDE 只勾 LDI,排除整个 `Application/Src/IAP/` | **链接失败** | +| 只勾 LDI + `app_iap_cfg.c` | 可链接;无 UDP IAP 升级能力 | +| Makefile 全量 | 通过(掩盖问题) | + +### 4.2 架构 / 可维护性 + +- 协议层(LDI)依赖另一协议目录下的实现,**违反「目录即模块、可独立选编」**。 +- 新人易误判为「LDI 必须绑死 IAP 升级协议」。 +- 重命名/搬迁 IAP 目录时易再次踩链接雷。 + +### 4.3 运行行为(解耦前仍成立的业务耦合) + +即使编译通过,运行时仍假定: + +- Sector1 布局与 `app_flash_iap_sys_info_t` 一致; +- Boot/Recovery 读同一结构; +- LDI 改 IP 会擦写内部 Flash(有总线停顿风险,代码已避免在「无配置上电路径」写 Flash); +- 0AH 改 IP 在运行中擦写 Sector1:16KB 全扇区擦除,停总线 1~2s;擦写失败/断电无提示、无回退动作(掉电语义声明见 [02 §8](./02_decoupling_solutions.md)); +- Sector1 无有效记录时,Bootloader/Recovery 回退各自默认 IP;主固件回退顺序为 W25 → Sector1 → 默认。出厂新机 0AH 改 IP 后 Boot/Recovery 恒用默认 IP(解耦前缺陷,已按 Q1 修复,见 §3.4)。 + +解耦时必须保留或显式放弃这些产品语义,不能只删调用。 + +### 4.4 非影响(澄清) + +| 项 | 说明 | +|----|------| +| 多协议 RB / 链式 probe | 与本问题无关(doc/05) | +| LDI 与 IAP **帧队列** | 本就独立,不共享 | +| 仅缺 `app_iap.c` | **不会**因本问题报上述符号缺失 | + +--- + +## §5 问题定性 + +| 维度 | 结论 | +|------|------| +| 缺陷类型 | **编译期模块边界错误**(强符号依赖放错层) | +| 严重度 | 裁剪构建 **阻塞**;全量构建 **隐性** | +| 是否安全漏洞 | 否 | +| 是否必须整包 IAP | **否**;见 [03](./03_interim_build_rules.md) | +| 附带现状缺陷 | 空 Sector1 写路径不置 magic(§3.4)、0AH 写失败/断电无保护(§4.3)、模块命名与注释勘误(§3.5) | + +--- + +## 修订 + +- 2026-08-14:首版,对齐 EIDE「只加 LDI」链接失败现场。 +- 2026-08-14(同日修订):§2 统一模块命名(`dev_flash_ldi` 混名 → `app_ldi_cfg`);§3.2 补充同步前置条件(Sector1 有效才同步);新增 §3.4 空 Sector1 写路径缺陷、§3.5 命名/注释勘误;§4.3 补充 0AH 擦写停顿与掉电回退语义;§5 增加现状缺陷条目。 +- 2026-08-14(同日再修订):§3.2/§3.4 更新为「已定案并落地」状态——空 Sector1 写路径按 [02 §9](./02_decoupling_solutions.md) Q1 修复(镜像 Bootloader 语义),0AH 两步写返回值如实应答(Q2),同值跳过擦写(Q3)。 +- 2026-08-14(同日三修订):§3.2 守卫语义同步 [02 §10](./02_decoupling_solutions.md)——空/损坏均完整初始化覆盖(自愈),仅升级中间态拒绝覆盖;§4.3 出厂新机缺陷句标注「已修复」。 diff --git "a/doc/07_LDI\344\270\216IAP\351\205\215\347\275\256\350\247\243\350\200\246/02_decoupling_solutions.md" "b/doc/07_LDI\344\270\216IAP\351\205\215\347\275\256\350\247\243\350\200\246/02_decoupling_solutions.md" new file mode 100644 index 0000000..d351765 --- /dev/null +++ "b/doc/07_LDI\344\270\216IAP\351\205\215\347\275\256\350\247\243\350\200\246/02_decoupling_solutions.md" @@ -0,0 +1,540 @@ +# 02 解耦方案 + +> **状态**:三处缺陷**已定案并先行修复**(2026-08-14,见 §9);**方案 A 主体重构已落地(2026-08-14)**——抽出板级网络配置模块 `app_board_net_cfg`(`Application/Config`),LDI/IAP 迁移至 `app_board_net_cfg_*` API,`app_iap_cfg.{c,h}` 与 `Device/Inc/config_info.h` 已删除 | 2026-08-14 +> **前提**:已读 [01_background_and_impact.md](./01_background_and_impact.md) +> **纪律**:本文件仅方案;**未经授权不改代码**(§9 已获授权的先行修复除外)。 + +--- + +## §0 目标与非目标 + +### 目标 + +| ID | 目标 | +|----|------| +| D-1 | EIDE/Makefile **只编 LDI、不编 IAP 升级协议** 时链接成功 | +| D-2 | 仍可在需要时编入完整 IAP;Sector1 网络配置语义可保留或可显式关闭 | +| D-3 | LDI 源文件 **不再** `#include "app_iap_cfg.h"` / 直调 `app_flash_iap_*` | +| D-4 | Boot/Recovery 与主固件对 `0x08004000` 布局的兼容策略有文档与代码单一归属 | + +### 非目标 + +- 不改 LDI 帧格式、probe、RB 绑定(属 doc/05) +- 不强制取消 Sector1 存 IP(产品可保留,只改归属模块名) +- 不在本方案内重做整板配置中心(可分阶段) + +--- + +## §1 方案对比 + +| 方案 | 摘要 | 满足 D-1 | 工作量 | 推荐 | +|------|------|----------|--------|------| +| **A. 抽出板级网络配置模块** | `app_iap_cfg` → `app_board_net_cfg`(或 `Device/Config`),LDI/IAP 协议都依赖它 | ✅ | 中 | **首选** | +| **B. 弱符号 + 空实现** | `app_flash_iap_*` 改为 weak;无 IAP 配置 TU 时空操作 | ✅ | 小 | 过渡可接受 | +| **C. 接口注入 / 函数指针** | LDI 只依赖 `ldi_net_persist_ops`;默认挂 IAP cfg 或 stub | ✅ | 中 | 与 A 可合并 | +| **D. LDI 彻底不碰 Sector1** | 删同步调用;IP 只活在 W25 + 运行时 LwIP | ✅ | 小 | 需产品确认 Boot 行为 | +| **E. 仅文档规定必带 cfg** | 不改代码,选编规则强制带 `app_iap_cfg.c` | ⚠️ 治标 | 极小 | 见 [03](./03_interim_build_rules.md),非终态 | + +--- + +## §2 方案 A(推荐):抽出「板级网络配置」 + +### 2.1 目标结构 + +``` +Application/Src/Config/ 或 Device/Config/ + app_board_net_cfg.c/.h ← 原 app_iap_cfg 中与 Sector1/net 相关的 API +Application/Src/IAP/ + app_iap.c / app_iap_cmd.c ← 只依赖 board_net_cfg + 升级逻辑 +Application/Src/LDI/ + app_ldi.c / app_ldi_cmd.c ← 只依赖 board_net_cfg(或经 ops) +``` + +命名示例(落地时可调整,但勿再叫「iap」若模块已中立): + +```c +/* app_board_net_cfg.h */ +bool board_net_cfg_valid(void); +bool board_net_cfg_get(uint8_t ip[4], uint8_t mask[4], uint8_t gw[4]); +int board_net_cfg_set(const uint8_t ip[4], const uint8_t mask[4], const uint8_t gw[4]); +``` + +内部仍可读写 `0x08004000` 的既有布局(**二进制兼容 Boot/Recovery**),或在同文件内保留 `app_flash_iap_sys_info_t` 别名。 + +### 2.2 机械步骤(审核通过后执行) + +0. **改造前盘点(完整调用点清单)**: + - 全仓 `ADDR_CONFIG_SECTOR` 使用点 **7 处**:`app_iap_cmd.c:94/110/143`(裸转换 `(app_flash_iap_sys_info_t *)ADDR_CONFIG_SECTOR` 直接读配置)+ `app_iap_cfg.c:23/39/111`(+ 第 10 行注释)。另有两处 `#define`(`app_iap_cfg.h:8` 与死代码 `config_info.h:6`)。 + - LDI 侧 `app_flash_iap_*` 调用 **4 处**:`app_ldi.c:100/104/108`(上电同步与回退读)+ `app_ldi_cmd.c:431`(0AH)。 + - Sector1 结构体 **4 处独立定义**:主固件 `Application/Inc/IAP/app_iap_cfg.h`、`Device/Inc/config_info.h`(死代码,建议删除)、Bootloader 与 Recovery 各自的 `Drivers/BSP/Config/config_info.h`。无共享头、无布局锁定。 +1. 新建 `app_board_net_cfg.{c,h}`,从 `app_iap_cfg` **迁出**(或整体改名 + 更新引用)。 +2. `app_iap_cfg.h` 可薄包装 `#include "app_board_net_cfg.h"` + deprecated 别名(过渡一期)。 +3. LDI:`#include "app_board_net_cfg.h"`,替换 **4 处**调用(`app_ldi.c:100/104/108`、`app_ldi_cmd.c:431`)为 `board_net_cfg_*`。 +4. IAP 协议:`app_iap_cmd.c:94/110/143` 三处裸读改经 `board_net_cfg` API,消除裸转换;升级状态 / FWInfo 继续用同一头文件(若 FWInfo 仍在同结构,可同模块或再拆 `app_board_fw_meta`)。 +5. **布局锁定**:在 `app_board_net_cfg.h`(唯一归属)用 `static_assert` 锁死 `sizeof` / `offsetof` 与 magic 值,保证 0x08004000 布局与 Boot/Recovery **二进制兼容**;同步收敛/删除其余 3 处独立定义(`Device/Inc/config_info.h` 死代码直接删)。 +6. **顺带修复(已定案并先行落地,见 §9)**:空 Sector1 写路径补 magic 初始化([01 §3.4](./01_background_and_impact.md));写/擦除返回值上抛与失败处理(§8)。 +7. EIDE/Makefile:`Config` 或 `board_net_cfg` 列为 **公共必选**;`IAP/` 协议目录可选。 +8. 更新本目录 README 状态 → 已落地;补回归清单。 + +### 2.3 验收(DoD) + +- [ ] 工程 **exclude 全部** `app_iap.c` / `app_iap_cmd.c`,保留 `app_board_net_cfg.c` + LDI → **链接成功** +- [ ] 工程 **exclude LDI**,保留 IAP 协议 + board_net_cfg → 链接成功 +- [ ] LDI 目录内 `rg app_iap_cfg` / `rg app_flash_iap` 无匹配(或仅测试桩) +- [ ] **全仓** `rg ADDR_CONFIG_SECTOR`:使用点由现状 7 处收敛到唯一归属模块内;`app_iap_cmd.c` 无 `(app_flash_iap_sys_info_t *)ADDR_CONFIG_SECTOR` 裸转换 +- [ ] `rg config_info.h`:主固件死代码(`Device/Inc/config_info.h`)已删除;Sector1 布局定义收敛为 1 处 + static_assert 锁定 +- [ ] `0AH` 改 IP 后 Sector1 `net_cfg` 与 W25 一致(若保留同步语义);**出厂新机(空 Sector1)0AH 后 Sector1 记录有效**(magic 修复生效) +- [ ] 上电:W25 空 / Sector1 有值 / 皆空 三条路径行为与现网一致或有产品签字差异说明 +- [ ] 失败注入:擦除/写入失败或中途断电后上电,行为符合 §8 声明的掉电语义 + +--- + +## §3 方案 B:弱符号空实现(低成本过渡) + +在公共层提供: + +```c +__attribute__((weak)) bool app_flash_iap_is_config_valid(...) { return false; } +__attribute__((weak)) void app_flash_iap_update_net_cfg(...) { (void)ip;(void)mask;(void)gw; } +app_flash_iap_sys_info_t *g_config __attribute__((weak)); +``` + +强符号仍由 `app_iap_cfg.c` 提供。 + +| 优点 | 缺点 | +|------|------| +| 改动小,立刻可「只编 LDI」 | LDI 仍 `#include app_iap_cfg.h`,目录边界未清 | +| | 无 cfg 时静默放弃 Sector1 同步,易忽略产品风险 | +| | `g_config` weak 空指针需所有路径判空 | + +建议:仅作 **过渡**,终态仍走方案 A。 + +--- + +## §4 方案 C:LDI 持久化 ops 注入 + +```c +typedef struct { + bool (*net_valid)(void); + void (*net_load)(uint8_t ip[4], uint8_t mask[4], uint8_t gw[4]); + void (*net_store)(const uint8_t ip[4], const uint8_t mask[4], const uint8_t gw[4]); +} ldi_net_bridge_ops_t; + +void ldi_set_net_bridge(const ldi_net_bridge_ops_t *ops); /* nullptr = 不桥接 Sector1 */ +``` + +- 默认产品固件:在 `app_boot` 或 IAP cfg initcall 里注入真实 ops。 +- 纯 LDI 演示固件:不注入 → 只走 W25 / `pl_net_get_ip`。 + +可与方案 A 组合:ops 的默认实现放在 `app_board_net_cfg`。 + +--- + +## §5 方案 D:取消 Sector1 同步(产品决策) + +删除 LDI 中所有 `app_flash_iap_*` 调用,IP 仅: + +- W25 LDI 记录 +- 运行时 `pl_net_*` + +| 条件 | 说明 | +|------|------| +| 必须产品确认 | Boot/Recovery **不再**依赖 Sector1 的 net_cfg,或另有配置源 | +| 风险 | 出厂只配 LDI、未写 Sector1 时,升级/恢复流程 IP 不一致 | + +若产品确认「主业务只认 W25」,D 最干净;否则不要单独选 D。 + +--- + +## §6 推荐落地路径 + +``` +短期(不改架构,历史) → 执行 03 选编规则(必带 app_iap_cfg.c,已废止) +中期(推荐) → 方案 A 抽 board_net_cfg;可选加方案 C 注入 +可选加速过渡 → 方案 B weak stub(同时排期 A) +产品放弃 Sector1 IP → 方案 D + 更新 Boot/Recovery 文档 +``` + +> **2026-08-14 现状**:三处缺陷已按 §9 定案**先行修复**;**方案 A 主体重构已落地**(`app_board_net_cfg` @ `Application/Config`,DoD §2.3 全项通过),选编规则 [03](./03_interim_build_rules.md) 已废止、仅存历史。 + +--- + +## §7 风险与回滚 + +| 风险 | 缓解 | +|------|------| +| 搬文件漏改 EIDE/Makefile | 以「只 LDI / 只 IAP」两套链接为 CI 门禁 | +| 结构体搬迁破坏 Boot | **保持** `0x08004000` 布局与 CRC 算法不变;只改源码归属 | +| 擦写 Flash 时机 | 维持现逻辑:避免无配置上电路径 erase | +| 0AH 写 Sector1 断电 | 记录 CRC 失效 → Boot/Recovery 回退默认 IP;掉电语义见 §8,解耦不改变既有回退顺序 | +| Sector1 擦写寿命 | 每次更新均为 16KB 全扇区擦除(约 10k 次寿命),运行中停顿 1~2s;0AH 属出厂低频操作,产品确认见 §8 | +| Bootloader 条件 D `app_info` CRC 校验 × 烧录器重烧主固件 → 永远进 Recovery | Recovery 升级过一次后(`app_info` 为旧固件真实值),用 J-Link/EIDE 直接重烧主固件而不更新 Sector1 → 条件 D 校验失配 → 判 App 损坏 → 设备永远进 Recovery。`tool/flash_all.sh` 默认烧后擦 Sector1 恢复出厂态;手动烧录/单固件烧录遵守同纪律;net_cfg 由主固件上电从 W25 同步回写 | + +回滚:恢复 `app_iap_cfg` 路径与 LDI include 即可。 + +--- + +## §8 失败场景与掉电语义 + +### 8.1 现状(解耦前) + +| 环节 | 现状行为 | +|------|----------| +| `app_flash_iap_update_net_cfg`(`app_iap_cfg.c:108-122`) | 擦除/写入返回码**均被忽略**(`app_flash_iap_edit_config` 内忽略 erase/write 返回),无重试、无回读校验、无上报 | +| LDI save(`app_flash_ldi_save_config`,`app_ldi_cfg.c`) | 同样忽略 W25 写入返回;SPI busy / 擦除失败静默 | +| 0AH 命令(`app_ldi_cmd.c:430-431`) | 先写 W25 再写 Sector1;任一步失败仍回 0x00 成功响应 | +| Sector1 擦写 | 每次更新都是 **16KB 全扇区擦除**:运行中(0AH 处理)停总线 **1~2s**;擦写寿命约 **10k 次**。0AH 属出厂低频操作风险可控,但高频改 IP 场景需产品论证 | + +### 8.2 掉电语义(现状声明) + +- **W25 写入中断电**:W25 CRC 失效 → 主固件回退 Sector1;Sector1 无有效记录(含出厂新机)→ 回退默认 IP。 +- **Sector1 写入中断电**:Sector1 CRC 失效 → Bootloader/Recovery 回退默认 IP;主固件走 W25(W25 先写且已成功)。**自愈**(§10 方案 1 落地后):下次 0AH / 上电同步时,损坏记录按「完整初始化」分支覆盖为有效记录,无需返修。 +- **出厂新机(Sector1 空)**:受 [01 §3.4](./01_background_and_impact.md) 缺陷影响,0AH 后 Sector1 仍无有效记录 → Bootloader/Recovery 恒回退默认 IP。 +- 归纳:**Sector1 无有效记录时,三固件各自回退默认 IP**;这是现网行为,解耦不得静默改变。 + +### 8.3 方案 A 要求 + +- `board_net_cfg_set` 必须返回错误码;LDI 0AH 判错并向上位机返回失败/告警(不再静默回 0x00)。 +- 写后回读校验;失败时保留内存中的新配置、记录错误状态。 +- 失败后是否重试:**已定案(§9 Q2)**——固件不自动重试,0AH 应答 ErrCode=01H,由上位机重发 0AH 恢复。 +- 掉电语义显式写入本目录 01/03 及 Boot/Recovery 侧文档,保持「W25 → Sector1 → 默认」回退顺序不变。 +- 空 Sector1 写路径的 magic 初始化修复([01 §3.4](./01_background_and_impact.md))**已按 §9 Q1 落地**。 + +--- + +## §9 先行修复定案(2026-08-14,已落地) + +> **范围声明**:本次只做下述三个缺陷修复 + 文档落实;**07 解耦主体重构(方案 A 抽板级网络配置模块)暂缓**。 +> 涉及文件:`Application/Src/IAP/app_iap_cfg.c`、`Application/Src/IAP/app_iap_cmd.c`、 +> `Application/Src/LDI/app_ldi_cmd.c`、`Application/Src/LDI/app_ldi.c`(+ `Application/Inc/IAP/app_iap_cfg.h` 接口声明)。 + +### Q1(定案:修,镜像 Bootloader 语义) + +`app_flash_iap_update_net_cfg` 空配置分支改为**完整初始化**,使写出的记录与老 Bootloader +`Init_Config_Info`(`STM32F407-Bootloader-master/Drivers/BSP/Config/config_info.c`)首次上电 +自写内容**逐字节同构**: + +| 字段 | 值 | +|------|-----| +| magic | `0x0d000721`(CONFIG_MAGIC) | +| update_sta | `updated = 0` | +| app_info | 全 0,其中 `crc32 = 0xFFFFFFFF`(size=0、version 全 0;version 由启动落库 API 填写,见 §13) | +| net_cfg | 调用方传入(ip/mask/gw + **port**,0AH 把 device_port 传入) | +| config_crc | 硬件 CRC32 覆盖 `sizeof - 4`(前 64B) | + +- 4B02「强制修改IP」处理器改走同一修复后的函数。 +- **禁止复用** `app_flash_iap_init_config`(其 update_sta=FAILED,会被 Bootloader 判 App 损坏)。 + +### Q2(定案:最小修复,如实应答) + +- `cmd_set_ip`(0AH)捕获 **W25 写入**与 **Sector1 同步**两个返回值,任一失败则应答 + ErrCode=01H(00H=成功,01H=失败,与 ldi_status_rsp_t / 0BH 既有约定一致);上位机重发 0AH 恢复。 + **固件侧不做自动重试。** +- 上电同步钩子(`ldi_ctx_init`)扩展:Sector1 **空/无效**时也走 `app_flash_iap_update_net_cfg` + 用 W25 配置初始化 Sector1(空扇区走 Q1 完整初始化;损坏记录仍被守卫拒绝覆盖,保护升级中间态)。 + 修复 Q1 后该钩子自然覆盖「0AH 写中断电 → Sector1 空/擦除残留」场景。 + **(2026-08-14 细化:损坏记录改为自愈覆盖,见 §10 方案 1,本条「损坏拒绝覆盖」表述已被取代)** + +### Q3(定案:接受现状 + 跳过同值写) + +`app_flash_iap_update_net_cfg` 写前比较 net_cfg(**含 port**)与现扇区内容,一致则跳过擦写返回成功。 +**不做磨损均衡**。16KB 全扇区擦除的现状(单次 1~2s 停顿、约 10k 次寿命)维持不变。 + +### 修复后的全流程走查(空扇区 + 0AH) + +1. 出厂新机:Sector1 空、W25 空 → Bootloader 条件 C:`Init_Config_Info` 写默认 IP,Main App 存在则跳 Main。 +2. 上位机下发 0AH:主固件先写 W25,再经 `app_flash_iap_update_net_cfg` 空分支完整初始化 Sector1 + (magic + updated + app_info 全 0/crc32=0xFFFFFFFF + 新 net_cfg + CRC32)→ 应答 00H。 +3. 复位:Bootloader 条件 D:magic/CRC 通过、update_sta=updated、app CRC 对 0 字长结果为 0xFFFFFFFF + 与 app_info.crc32 相等 → `Is_App_Exist(ADDR_MAIN_APP)` 通过则跳 Main App(与 Bootloader 自己初始化的记录行为一致)。 +4. Main App 上电:W25 有效 → `ldi_ctx_init` 调 `app_flash_iap_update_net_cfg`,net_cfg 已一致 → 跳过擦写。 +5. 若 Main App 不存在:Bootloader 跳 Recovery;Recovery 的 `Is_Config_Empty`(magic 有效)为假, + 不再重置配置,读到的 net_cfg 即 0AH 下发值。 +6. 0AH 任一步失败:应答 01H,上位机重发 0AH 恢复。升级中间态记录(valid 且 update_sta≠updated) + 不被覆盖;损坏记录在成功路径由「完整初始化」分支自愈覆盖(见 §10 方案 1)。 + +--- + +## §10 补充修复定案(2026-08-14,已落地) + +> **范围声明**:§9 三缺陷修复后追加的两项修正——细化守卫语义 + 4B02 应答扩展结果码。 +> 涉及文件:`Application/Src/IAP/app_iap_cfg.c`、`Application/Src/IAP/app_iap_cmd.c` +> (+ `Application/Inc/IAP/app_iap_cfg.h`、`Application/Src/LDI/app_ldi.c` 注释同步)。 + +### 10.1 方案 1(细化守卫:升级中间态仅放行 net_cfg 更新 vs 损坏可覆盖) + +`app_flash_iap_update_net_cfg` 原守卫 `!empty && !valid → return -1` 把「损坏」与「升级中间态」一起拒绝; +细化后按状态分四支: + +| Sector1 状态 | 判定 | 处理 | +|--------------|------|------| +| 空 | magic==0xFFFFFFFF 且 config_crc==0xFFFFFFFF | 完整初始化(镜像 Bootloader 语义)后写 | +| 损坏(magic 无效或 CRC 错) | !empty 且 !valid | **完整初始化后写(覆盖自愈)** | +| 有效 + update_sta==updated | valid 且 updated | 仅更新 net_cfg;同值跳过擦写 | +| 升级中间态 | valid 且 update_sta≠updated | **仅更新 net_cfg;update_sta/app_info 原样保留**(2026-08-18 修订) | + +- **升级中间态判定独立于损坏判定**:Bootloader 条件 A/B(强制升级/崩溃过频)写入的记录 + magic/CRC 均正确,属「valid 且 update_sta=failed」;中间态仅放行 net_cfg 字段更新、 + update_sta/app_info 原样保留(2026-08-18 修订);损坏记录(magic 无效/CRC 错)不可能是 + 升级中间态——Bootloader 写记录永远是完整结构(整扇区擦→完整写)。 +- **损坏覆盖不会误伤升级流程**:结论为否。损坏判定与中间态判定互斥(一个看 magic/CRC, + 一个看 valid 后的 update_sta),覆盖损坏不会触碰任何有效中间态记录。 +- **需重新评估的窗口**:若未来 Bootloader 引入「写一半中断」的新写入模式(中断恢复后不 + 保证完成记录),出现 magic 有效但 CRC 错的半成品时,损坏覆盖语义需重新评估——届时须把 + 该状态识别为中间态并保护。当前 Bootloader 无此模式,评估结论成立。 +- **同值跳过条件 `!empty` → `valid` 的原因**:空/损坏记录现已进入完整初始化分支且必须写; + 若仍按 `!empty` 判定,损坏记录快照(`old_cfg` 为垃圾)碰巧与新 net_cfg 一致时会错误跳过 + 写入,导致损坏记录永不自愈。`valid` 下 `old_cfg` 才有比较意义。 +- **附带价值**:0AH 自愈——Sector1 写中断电/历史损坏后,下一次 0AH 或上电同步即恢复有效记录 + (§8.2 已同步)。 + +### 10.2 方案 2(4B02 应答扩展结果码) + +`cmd_ForceModifyIP_02` 捕获 `app_flash_iap_update_net_cfg` 返回值,应答帧由 **len 0 → 1**: + +| 载荷(1 word) | 语义 | 来源 | +|----------------|------|------| +| 0x00000000 | 成功(含同值跳过) | ret ≥ 0 | +| 0x00000001 | 失败(擦写错误) | ret < 0 | + +参照 4B04「准备升级」应答 0/1 结果码先例。**具体可能导致的问题**: + +1. **老上位机按「B402 无载荷」解析**:多读到的 1 个 word 是被忽略还是报错(长度/CRC 校验), + 取决于上位机解析实现——若按 len 字段解析则兼容,若按固定长度或严格 CRC 覆盖校验则可能 + 报错。**上线前须与上位机联调验证**。 +2. **新固件 + 老上位机混合部署期**:升级窗口期内老上位机可能解析异常;建议上位机按 len 字段 + 容忍解析,或统一固件/上位机同批升级。 +3. **结果码的实际价值**:4B02 失败率本身极低(擦写错误罕见;升级中间态自 2026-08-18 修订 + 起仅放行 net_cfg 更新、不再返回失败);结果码的主要价值在于**显式失败反馈**,避免 + 「应答成功但配置未落盘」的静默错误。 + +### 10.3 行为矩阵(四状态 × 两入口,自查基线) + +| Sector1 状态 | 4B02 / 0AH 处理 | 4B02 应答 | 0AH 应答 | +|--------------|-----------------|-----------|----------| +| 空 | 完整初始化后写 | result=0 | 00H | +| 损坏 | 完整初始化后写(自愈) | result=0 | 00H | +| 有效 + updated | 同值跳过 / 更新 net_cfg | result=0 | 00H | +| 升级中间态 | 仅更新 net_cfg(update_sta/app_info 保留) | result=0 | 00H | + +(0AH 应答还需 W25 写入成功;W25 与 Sector1 任一步失败即 01H。) + +--- + +## §11 三固件出厂默认网络配置统一约定(2026-08-18,已落地) + +**三固件(主固件/Bootloader/Recovery 工程)出厂默认网络配置统一为:** + +| 项 | 值 | +|----|----| +| IP | 192.168.114.200 | +| 掩码 | 255.255.255.0(/24) | +| 网关 | 192.168.114.1 | +| 端口 | 9528(0x2538) | + +同日落地的一致性修复: +- **Recovery 上电读 Sector1.net_cfg 配置 netif**:`MX_LWIP_Init` 内自行直读 Sector1(该函数早于 main 中 config 判空/初始化代码),magic + CRC 校验通过用 net_cfg.ip/mask/gw,空/无效回退统一出厂默认;Recovery 判空逻辑与 Bootloader 对齐(只看 magic/config_crc 两哨兵,不再把 app_info.crc32==0xFFFFFFFF 当空)。 +- **主固件 W25 空 + Sector1 有效时同步 set_ip**:`ldi_ctx_init` 该分支拷贝 cfg 后补 `pl_net_set_ip`,使搜索应答地址与网口实际地址一致(新板首启 114.200 可达);仍不写 W25/Sector1(上电不写 Flash 纪律保留)。 +- **主固件上电默认 IP 统一**:`dev_eth.c` 出厂默认改为 114.200/114.1(与三固件统一值一致)。 +- Bootloader 无网络栈,其 `config_info.c` 出厂默认与判空逻辑已满足本约定,无需改动。 + +--- + +## §12 网络配置真源与生效语义(2026-08-18,已落地) + +**方案 X 定案**:Sector1.net_cfg 为网络配置**唯一真源**(三工程共享),W25 中的网络字段降级为**恢复镜像**。 + +| 角色 | 存储 | 职责 | +|------|------|------| +| 真源 | Sector1 `net_cfg`(ip/mask/gw 共享 + `port`/`udp_port` 双端口,方案 B 2026-08-21) | Bootloader/Recovery/主固件共享;0AH/4B02 写 `port`(TCP 口),CQ setip 写 `udp_port`(UDP 口) | +| 恢复镜像 | W25 LDI 配置记录(含网络字段) | 擦 Sector1 恢复出厂后用于还原用户配置;主固件上电若 W25 空/无效则回写镜像 | + +**主固件上电同步(`ldi_ctx_init` 三分支,`Application/Src/LDI/app_ldi.c`)**: + +| 分支 | 条件 | 行为 | +|------|------|------| +| 1 Sector1 优先 | Sector1 有效(magic+CRC)且 net_cfg 合法(IP 非 0、port 1~65535) | 网络字段取 Sector1(**含 port,缺陷 B 修复**);W25 有效沿用其非网络字段(host/lane/cert/modules)、W25 空/无效回写镜像;netif/TCP Server 口应用移交 `app_net_boot`(§14)。**(2026-08-21 修订:`update_sta==APP_BOARD_UPDATED` 不再参与分支判定——有效记录含升级中间态一律采纳 Sector1.net_cfg;`app_board_net_cfg_update` 自 2026-08-18 起在中间态也放行 net_cfg 更新(0AH/4B02/CQ setip 改 IP 中间态同样生效),若中间态仍落分支 2,W25 回写会用陈旧镜像覆盖刚写入的 net_cfg 导致 setip 失效;Bootloader 条件 D 对中间态记录本就在主固件启动前进 Recovery,主固件运行态下该放宽无副作用)** | +| 2 W25 自愈 | Sector1 空/损坏/记录无效或 net_cfg 非法 + W25 有效 | cfg 全字段取 W25;`app_tcp_client_set_remote` 应用(netif/TCP Server 口应用移交 `app_net_boot`,§14);`app_board_net_cfg_update` 回写 Sector1(空/损坏完整初始化自愈;有效但 net_cfg 非法仅放行 net_cfg 更新、update_sta/app_info 原样保留)。**(2026-08-21 方案 B:回写以 W25 device_port 为 TCP 口,udp_port 保留现值(get 失败用 20103))** | +| 3 出厂默认 | 两者皆空/无效 | `pl_net_get_ip` 统一出厂默认(§11)填 cfg;不写任何 Flash(netif 应用由 `app_net_boot` 执行,§14) | + +**生效语义**:所有改 IP 接口(LDI 0AH / IAP 4B02 / Recovery IAP)统一**重启生效**(运行时网口不即时变更,属既定策略,非缺陷)。 + +**2026-08-18 缺陷 A/C/D/E 修复落地**: + +① **镜像自愈(缺陷 C/A)**:`ldi_ctx_init` 分支 1(Sector1 有效)检测 Sector1.net_cfg 与 W25 网络字段(ip/mask/gw/port)逐项不一致即刷新 W25 镜像——覆盖 4B02 单写 Sector1 不写 W25、或单次 W25 写失败导致的永久陈旧(陈旧镜像会在擦 Sector1 恢复出厂时回滚旧 IP);SPI 4KB 擦写无内部 Flash 总线停顿,上电执行安全。4B02 写入路径不改(保持 IAP 只写 Sector1 + 启动自愈,解耦)。 + +② **Bootloader 判空与损坏态重建(缺陷 D)**:`Is_Config_Empty` 改为 `magic != CONFIG_MAGIC` 即视为空(覆盖 magic 半写损坏但非全 FF 态,合法记录 magic 恒为 CONFIG_MAGIC);条件 D 中 magic 匹配但 config_crc 校验失败(Sector1 擦写中途掉电:magic 已写、CRC 未写完)→ 走条件 C 重建出厂记录(update_sta=updated、app_info.size=0、crc32=0xFFFFFFFF,0 长度 CRC 恒匹配仍能正确跳主固件);config_crc 对但 app CRC 不匹配(App 损坏)仍跳 Recovery 由升级流程修复。Recovery 判空保持两哨兵不动:Recovery `Init_Config_Info` 写 update_sta=failed,若由 Recovery 重建半写态会造成 Bootloader 条件 D 永远判 failed → 死循环进 Recovery;半写态自愈完全交由 Bootloader(启动顺序 Bootloader 先行)承担,分工已写入 Recovery config_info.c 注释。 + +③ **统一重启生效(缺陷 E)**:所有改 IP 接口(LDI 0AH / IAP 4B02 / Recovery IAP)统一重启生效;4B02 删除 `IP4_ADDR` + `tcpip_callback` 即时应用段(及仅其使用的 `iap_update_ip` / `ipconfig`),改为纯持久化;Recovery 运行期无即时改 netif 调用(netif 在 `MX_LWIP_Init` 读 Sector1,运行期改配置自然重启生效)。 + +**UDP 发现口**:10011 为固定设计原则(宏 `LDI_DISCOVERY_PORT`,定义于 `Application/Inc/LDI/app_ldi.h`),禁止提供修改接口(`app_udp_set_port` 已删除,`app_udp.c` 端口取宏引用)。 + +④ **搜索/上报的端口字段语义(2026-08-18 修复)**:LDI 12H 搜索应答的 `port` 字段与 IAP 0x01 上报 IP 的端口字段语义均为**设备「配置功能端口」= TCP 业务端口(出厂默认 9528,`LDI_DEFAULT_CONFIG_PORT` 宏)**,而非 UDP 发现口 10011——搜索/上报的**传输渠道**才是 UDP 10011。此前 `cmd_search` 误把 `LDI_DISCOVERY_PORT`(10011) 填入 12H 应答 port 字段,导致上位机把 10011 当配置端口读回并写回,污染 Sector1/W25 后 `ldi_ctx_init` 读得 device_port=10011 → TCP Server 错误改监听 10011(9528 连不上)。修复:12H port 取 `g_ldi.cfg.device_port`(未配置回退 9528);已污染存量设备需 0AH 重设端口 9528 或擦 Sector1 恢复出厂默认。 + +--- + +## §13 固件版本落库(app_board_net_cfg_fw_version_update,2026-08-18,已落地) + +**定案**:IAP 0x03「上报固件状态」应答的 `app_info.version` 改由主固件**启动时**从 +`PROGRAM_CODE`(`app_boot.h` 常量)落库到 Sector1——此前该字段无人写入(出厂空扇区与 +初始化路径均为全 0,0x03 恒报全 0)。 + +**API**:`Application/Src/Config/app_board_net_cfg.c` 新增 +`app_board_net_cfg_fw_version_update(const char *version)`;调用点 `app_boot.c init_task()`, +位于 `sw_board_init()` 之后、`app_splash_display()` 之前。 + +**红线**:只允许改 `app_info.version[32]` 并重算 `config_crc`,**绝不动** +`app_info.size / app_info.crc32 / net_cfg / update_sta` 既有值(size/crc32 由 Recovery +升级流程写入,改动会触发 Bootloader 条件 D 判 App 损坏)。 + +**四分支语义(空/损坏自愈与 net_cfg_update 同构;升级中间态仅本函数拒绝——net_cfg_update 自 2026-08-18 起仅放行 net_cfg 更新)**: + +| Sector1 状态 | 判定 | 处理 | +|--------------|------|------| +| 空 | magic==0xFFFFFFFF 且 config_crc==0xFFFFFFFF | 完整初始化(magic + update_sta=updated + app_info 全 0 且 crc32=0xFFFFFFFF + net_cfg 全 0)+ 填 version | +| 损坏(magic 无效或 CRC 错) | !empty 且 !valid | 完整初始化后填 version(覆盖自愈) | +| 有效 + update_sta==updated | valid 且 updated | 仅更新 version;同值跳过擦写 | +| 升级中间态 | valid 且 update_sta≠updated | **返回 -1 拒绝覆盖**(中间态拒绝仅本函数保留) | + +- **边界(如实声明)**:空/损坏分支把 net_cfg 写成 0.0.0.0/port0——调用点安排在 + `sw_board_init()`(含 `ldi_ctx_init` 从 W25Qxx 回写 net_cfg)之后,正常路径不会走到该 + 分支;若走到(如 `ldi_ctx_init` 未回写),net_cfg 以 0 落库,由后续 0AH / 上电同步恢复。 +- **错误码**(仅限本函数):0 成功(含同值跳过);-1 升级中间态拒绝覆盖(中间态拒绝仅本函数保留——net_cfg 函数 `app_board_net_cfg_update` 自 2026-08-18 起仅放行 net_cfg 更新、只返回擦/写错误码);-2 version 过长 + (`strlen(version) >= 32`,不做截断,显式拒绝);其余为擦/写错误码。 +- **同值跳过**:version 与现扇区一致则跳过擦写返回 0(Q3 同款,不做磨损均衡)——保证每次 + 版本发布只擦一次 16KB 扇区,日常重启零 Flash 磨损;写前 `pl_iwdg_refresh` 喂一次狗 + (16KB 擦除耗时数百 ms)。 +- **失败不阻断启动**:调用点忽略返回值(升级中间态拒绝 / 擦写错误静默降级,0x03 报旧值 + 或全 0)。 +- **RTT 诊断日志(2026-08-18 补)**:各分支打印 `[fwver] ...`(仅失败/首写/跳过路径,低 + 开销):`reject: bad version arg`(-2)/ `refuse: upgrade mid-state`(-1)/ `sector + empty/damaged -> full init`(空/损坏自愈首写)/ `same ver=... , skip`(同值跳过)/ + `write ok ver=...`(成功)/ `erase|write sector1 failed`(擦写失败)。现场复现「0x03 + 版本为空」时先看 RTT:**完全无 `[fwver]` 日志 = 主固件根本没运行(设备落在 Recovery + 态,查 Bootloader 条件 D);有日志但写失败 = 擦写路径问题**。 +- **0x03 应答字节序(2026-08-18 补)**:0x03 应答 version 按**大端 word 构造**(对齐 0x01 + IP 约定),存储侧保持纯 ASCII——`cmd_ReportFirmwareStatus_03` 逐 word + `v[4i]<<24|v[4i+1]<<16|v[4i+2]<<8|v[4i+3]` 构造,禁 memcpy 裸拷(否则上位机按 4 字节 + 一组反转显示)。主固件与 Recovery `cmd.c` 同构修改。 + +--- + +## §14 网络配置应用移交中立模块 app_net_boot(2026-08-21,已落地) + +**背景**:「应用 netif + TCP Server 口」此前分散在 `ldi_ctx_init` 三分支末尾与 CQ +`_cq_net_cfg_apply`(PROTO_CHONGQING),协议模块既管配置真源裁定又管网络应用, +横切职责耦合。2026-08-21 抽离为中立模块 `Application/Src/app_net_boot.c` +(`app_net_boot_apply`,两口径都编入)。 + +**职责**:`app_boot.c init_task` 在 `app_board_net_cfg_fw_version_update` 之后调 +`app_net_boot_apply()`——读 Sector1 net_cfg(magic+CRC 有效、IP 非 0、port +1~65535)→ `pl_net_set_ip` 应用 netif + `app_tcp_server_set_port` 应用 TCP +Server 口(**两口径统一,2026-08-24 修订**:TCP 口应用不再随 PROTO_CHONGQING +裁剪——TCP Server/Client 通道两口径均启动,见下「TCP 通道裁剪」实态更正)。 + +**accept_write(两口径统一)**:Sector1 空/损坏/非法 → 写本构建默认记录落盘并应用: + +| 口径 | ip / mask / gw | port | 端口应用对象 | +|------|----------------|------|-------------| +| PROTO_CHONGQING | 192.168.1.5 / 255.255.255.0 / 192.168.1.1 | 20103 | TCP Server 口(net_cfg.port,两口径统一 2026-08-24);UDP 业务口由 `_udp_cq_read_port` 直读 Sector1 | +| 其余(dev/LDI) | 192.168.114.200 / 255.255.255.0 / 192.168.114.1 | 9528 | TCP Server 口 | + +**顺序约束**:`sw_board_init` 内 `ldi_module_init` → `ldi_ctx_init`(Sector1/W25 +自愈 + LDI 设备配置装载,**不再调用 pl_net_set_ip / app_tcp_server_set_port**)先 +执行;其后 `fw_version_update`(空/损坏扇区初始化时 net_cfg 置 0)→ +`app_net_boot_apply` 判无效写默认——顺序不可颠倒。 + +**LDI 瘦身**:`ldi_ctx_init` 三分支末尾的 `pl_net_set_ip` / +`app_tcp_server_set_port` 与末尾 `[netcfg] apply` 日志删除;分支 2 保留 +`app_tcp_client_set_remote`;分支判定/自愈日志保留。 + +**CQ 瘦身**:`app_cq_proto.c` 的 `_cq_net_cfg_apply` 整块删除(含 `s_cq_def_ip/mask/gw` +常量与 `[cq] net_cfg_apply` 日志),`cq_proto_init` 不再承担网络启动职责; +`_udp_cq_read_port`(app_udp.c,PROTO_CHONGQING 分支读 Sector1)保持不动。 + +**TCP 通道裁剪(2026-08-24 实态更正)**:`app_boot.c` 实际**无**口径裁剪—— +TCP Server/Client 通道两口径均启动(`app_tcp_server_start()` / +`app_tcp_client_start()` 无条件调用;此前本节与 doc/CLAUDE.md「CQ 构建不启动 +TCP 通道」表述与代码不符)。2026-08-24 起 TCP 口应用同步两口径化: +`app_net_boot_apply` 不再按 PROTO_CHONGQING 裁剪 `app_tcp_server_set_port`, +修复「LDI 编入 + PROTO_CHONGQING」混合构建(如 EIDE Debug 口径错配)下 TCP 口 +恒 9528、与 LDI 12H / IAP 0x01 上报的 net_cfg.port 矛盾的现场 bug。CQ setip +端口语义不受影响(仍只写 udp_port、port 保留现值)。 + +**RTT 观察点(2026-08-21 撤销)**:本节落地时曾补 `[netcfg] boot-apply`、 +`[netcfg] ldi_ctx_init/branch1/2/3/branch2 write-back`、`[cq] net_cfg_apply` 等 +诊断标签,2026-08-21 已全部删除(源码不再有任何 `[netcfg]`/`[cq]` RTT 日志); +网络配置应用正确性以「擦 Sector1 出厂化 → 上电三分支落盘」静态审计与双构建 +验证为准。仅 `app_board_net_cfg.c` 的 `[fwver]` 日志保留(0x03 版本判断路径 +依赖,见 doc/CLAUDE.md 烧录小节)。 + +--- + +## §15 方案 B:Sector1 记录新增 udp_port,TCP/UDP 端口彻底分离(2026-08-21,已落地) + +**定案**:`Sector1.net_cfg.port`(u32)历史语义完全不变 = **TCP 业务口**(LDI 0AH +写 / IAP 0x01 报 / 0x02 写 / TCP Server 监听,出厂默认 9528);新增 +`Sector1.net_cfg.udp_port`(u32)= **CQ UDP 业务口**(CQ setip 写 / CQ UDP 通道 +读,出厂默认 20103)。共存构建下两套口并存;ip/mask/gw 共享(setip 与 0AH 都写, +现状不变)。 + +**记录布局(三固件二进制兼容,各自 static_assert 锁定)**: + +| 字段 | 偏移 | 大小 | 语义 | +|------|------|------|------| +| magic | 0 | 4B | 魔数 0x0d000721 | +| update_sta | 4 | 4B | 升级状态机 | +| app_info (size/crc32/version[32]) | 8 | 40B | 固件信息 | +| net_cfg.ip/mask/gw | 48 | 12B | 两套口共享 IP/掩码/网关 | +| net_cfg.port | 60 | 4B | **TCP 业务口**(LDI/IAP 生态,默认 9528) | +| net_cfg.udp_port | 64 | 4B | **CQ UDP 业务口**(CQ 生态,默认 20103) | +| config_crc | 68 | 4B | 覆盖 magic~udp_port(68B)的 CRC32 | + +NetConfig 16B → 20B;SysInfo 68B → 72B(18 words)。config_crc 覆盖范围随结构体 +扩展(仍为「整结构体去掉 crc 字段自身」),与 Bootloader/Recovery 的 +`HAL_CRC_Calculate` 覆盖一致。 + +**各写入点端口语义(调用 `app_board_net_cfg_update(ip,mask,gw,port,udp_port)`)**: + +| 写入点 | port(TCP 口) | udp_port(CQ UDP 口) | +|--------|----------------|------------------------| +| LDI 0AH(app_ldi_cmd.c) | 命令下发 device_port | 保留现值(get 失败 20103) | +| IAP 0x02/4B02(app_iap_cmd.c) | 包内 port | 保留现值(同上) | +| ldi_ctx_init 分支 2 回写(app_ldi.c) | W25 device_port | 保留现值(同上) | +| CQ setip(app_cq_proto_cmd.c) | **保留现值(get 失败 9528)** | 命令下发端口 | +| app_net_boot invalid 写默认(dev/CQ 两口径) | 9528 | 20103 | + +**读取点**:`_udp_cq_read_port`(app_udp.c)`PROTO_CHONGQING` 分支读 `udp_port` +(1~65535 有效即用,无效/0 回退 20103),`#else` 固定 20103;CQ 12B 搜索应答报 +`app_udp_cq_get_port()`;LDI 12H/1DH 报 `g_ldi.cfg.device_port`(TCP 口);Bootloader/ +Recovery 的 0x01/0x02 继续使用 `net_cfg.port`(TCP 语义不变)。 + +**升级兼容(一次配置丢失,已接受)**:旧 68B 记录升级到新固件后,主固件按 72B +布局重算 config_crc 必然失配 → 判损坏 → 走空/损坏自愈路径重写记录(网段配置 +丢失一次:dev 由 app_net_boot 写 114.200/9528/20103 出厂默认或由 ldi_ctx_init +从 W25 镜像回写恢复用户配置;CQ 由 app_net_boot 写 192.168.1.5/9528/20103)。 +Bootloader/Recovery 同步升级后按 72B 布局读写,与主固件自愈后的记录二进制兼容。 + +--- + +## 修订 + +- 2026-08-14:首版方案,待审核。 +- 2026-08-14(同日修订):§2.2 增补完整调用点清单(全仓 7 处 `ADDR_CONFIG_SECTOR` 使用点 + LDI 侧 4 处调用 + 4 处独立结构体定义)与布局锁定步骤;§2.3 DoD 门禁扩大至全仓;新增 §8 失败场景与掉电语义;§7 补充断电与擦写寿命风险。 +- 2026-08-14(同日再修订):新增 **§9 先行修复定案**——Q1(镜像 Bootloader 语义)、Q2(如实应答 + 上位机重发)、Q3(接受现状 + 同值跳过)三决策落地;§2.2 步骤 6、§6、§8.3 中「待用户确认」标记改为定案结论;范围声明为「先行修复,07 解耦主体重构暂缓」。 +- 2026-08-14(同日三修订):新增 **§10 补充修复定案**——方案 1(细化守卫:升级中间态拒绝、损坏自愈覆盖、同值跳过条件 !empty→valid)、方案 2(4B02 应答 len 0→1 结果码 0/1)与四状态×两入口行为矩阵;§9 Q2 及走查第 6 步的「损坏拒绝覆盖」表述标注已被取代;§8.2 增补损坏自愈语义。 +- 2026-08-14(同日四修订):**方案 A 落地**——按 §2.2 步骤执行:新建 `Application/Config/app_board_net_cfg.{c,h}`(Sector1 布局唯一归属 + 8 条 static_assert 锁定 + `get/update/read` API),LDI 4 处与 IAP 3 处调用迁移,`app_iap_cfg.{c,h}` 与死代码 `Device/Inc/config_info.h` 删除;Makefile / eide.yml 同步;DoD §2.3 全项验证通过(全量 + LDI-only + IAP-only 三种链接,产物尺寸零变化)。 +- 2026-08-14(同日五修订):§7 风险表新增「Bootloader 条件 D `app_info` CRC 校验 × 烧录器重烧主固件 → 永远进 Recovery」风险行——Recovery 升级过一次后用烧录器直接重烧主固件会 CRC 失配被判 App 损坏;缓解为 `tool/flash_all.sh` 默认烧后擦 Sector1 恢复出厂态(`--keep-config` 保留),手动烧录/单固件烧录遵守同纪律,net_cfg 由主固件上电从 W25 同步回写。烧录纪律同步写入 `doc/CLAUDE.md` 烧录小节。 +- 2026-08-18:新增 **§11 三固件出厂默认网络配置统一约定**(192.168.114.200/24、网关 192.168.114.1、端口 9528)及三项一致性修复落地:Recovery `MX_LWIP_Init` 读 Sector1.net_cfg 配 netif(空/无效回退统一默认)+ Recovery 判空对齐 Bootloader;主固件 `ldi_ctx_init` W25 空 + Sector1 有效分支补 `pl_net_set_ip`;主固件 `dev_eth.c` 出厂默认 IP 统一为 114.200/114.1。 +- 2026-08-18(同日再修订):新增 **§12 网络配置真源与生效语义**——**方案 X 落地**:Sector1.net_cfg 为唯一真源、W25 网络字段为恢复镜像;`ldi_ctx_init` 重构为三分支(Sector1 优先 / W25 自愈回写 / 出厂默认),缺陷 B 修复(分支 1 端口改读 Sector1.net_cfg.port);0AH/4B02 修改后重启生效定为既定策略;UDP 发现口 10011 固定为宏 `LDI_DISCOVERY_PORT`,删除 `app_udp_set_port` 修改接口。 +- 2026-08-18(12H 端口字段语义修复):`cmd_search` 12H 应答 port 字段由误用 `LDI_DISCOVERY_PORT`(10011) 改为设备配置功能端口 `g_ldi.cfg.device_port`(未配置回退 `LDI_DEFAULT_CONFIG_PORT`=9528);新增宏 `LDI_DEFAULT_CONFIG_PORT (9528U)`(app_ldi.h)。修复 10011 污染循环(12H 报 10011 → 上位机写回 → TCP Server 错误监听 10011);存量污染设备需 0AH 重设 9528 或擦 Sector1。详见 §12 ④。 +- 2026-08-18(同日三修订):§12 增补 **缺陷 A/C/D/E 修复落地**——①分支 1 镜像自愈(Sector1 与 W25 网络字段不一致即刷新镜像,覆盖 4B02 单写与 W25 单次写失败的陈旧);②Bootloader 判空改 magic 哨兵(magic 不匹配即空)+ magic 匹配但 CRC 错走条件 C 重建出厂记录(App 损坏仍 Recovery;Recovery 判空保持两哨兵防死循环的分工原因);③所有改 IP 接口(LDI 0AH / IAP 4B02 / Recovery IAP)统一重启生效(4B02 删除 `IP4_ADDR`+`tcpip_callback` 即时应用段与 `iap_update_ip`)。 +- 2026-08-18(同日五修订):§13 增补 **RTT 诊断日志**——`app_board_net_cfg_fw_version_update` 各分支打印 `[fwver] ...`(失败/首写/跳过路径),现场可区分「主固件未运行(无日志)/ 写失败 / 同值跳过」;`doc/CLAUDE.md` 烧录小节同步补「0x03 版本为空判断路径」。 +- 2026-08-18(同日五修订):**升级中间态守卫语义修订**——修复「中间态冻结 net_cfg」缺陷:`app_board_net_cfg_update`(0AH/4B02 网络配置写入)升级中间态不再拒绝,仅放行 net_cfg 字段更新(update_sta/app_info 从 Flash 读出原样保留,Bootloader 条件 D 判定不受影响),不再返回 -1;`app_board_net_cfg_fw_version_update`(0x03 版本落库)保持中间态拒绝 -1 不变(version 属 app_info,由 Recovery 升级流程写入)。§10.1 表/§10.3 行为矩阵/§10.2 结果码表/§12 分支 2/§13 错误码注记同步。 +- 2026-08-18(同日六修订):**0x03 应答 version 字节序修复**——应答载荷 version 由 memcpy 裸拷改为逐 word 大端构造(对齐 0x01 IP 约定),存储侧保持纯 ASCII;`app_iap_cmd.c cmd_ReportFirmwareStatus_03` 与 Recovery `cmd.c` 同构修改,上位机显示不再每 4 字节一组反转("9K10212482" 此前显示为 "01K9421228")。 +- 2026-08-21:**§12 分支 1 条件修订(setip 中间态覆盖修复)**——`ldi_ctx_init` 分支 1 条件由「Sector1 有效(magic+CRC)且 `update_sta==APP_BOARD_UPDATED`」改为「Sector1 有效且 net_cfg 合法(IP 非 0、port 1~65535)」,`update_sta` 不再参与分支判定;分支 2 条件相应收窄为「Sector1 空/损坏/记录无效或 net_cfg 非法」。根因:2026-08-18 起 `app_board_net_cfg_update` 在升级中间态也放行 net_cfg 更新(CQ setip/0AH/4B02 改 IP 中间态应生效),但 `ldi_ctx_init` 仍把中间态记录判入分支 2 并以 W25 陈旧镜像回写覆盖 Sector1,导致 setip 写入的 net_cfg 在重启后被冲掉。同步补 RTT 观察点:`[netcfg] ldi_ctx_init`(三分支判定值)、`[netcfg] branch1/2/3`、`[netcfg] branch2 write-back sector1 ret`、`[netcfg] boot-apply`。 +- 2026-08-21(同日再修订):**网络配置应用横切职责解耦**——新增 §14:中立模块 `app_net_boot`(`app_net_boot_apply`)统一承担「Sector1 net_cfg → netif + TCP Server 口」应用,`ldi_ctx_init` 三分支末尾的 `pl_net_set_ip`/`app_tcp_server_set_port` 与 CQ `_cq_net_cfg_apply`(含 s_cq_def_* 常量)整块删除;accept_write 两口径统一(Sector1 空/损坏/非法写本构建默认记录落盘并应用:CQ 192.168.1.5/20103、dev 192.168.114.200/9528);CQ 构建不启动 TCP Server/Client 通道;Makefile/eide.yml 编入 app_net_boot.c;`[netcfg] boot-apply` 替代 `[netcfg] apply` / `[cq] net_cfg_apply` 观察点。 +- 2026-08-21(同日三修订):**方案 B 落地(新增 §15)**——Sector1.net_cfg 新增 `udp_port`(CQ UDP 业务口,默认 20103),`port` 恒为 TCP 业务口(9528)不再被 setip 污染;记录 68B→72B(NetConfig 16B→20B),三固件 config_info 同步扩展 + static_assert 锁定;`app_board_net_cfg_update` 签名加 udp_port(同值跳过比较含 udp_port),5 处调用点按「写本协议端口、保留另一端口现值」适配;`_udp_cq_read_port` PROTO_CHONGQING 分支改读 udp_port;Recovery 0x02 改为逐字段写 ip/mask/gw/port(udp_port 保留现值);Bootloader/Recovery 初始化默认补 udp_port=20103。**旧 68B 记录升级后 CRC 失配 → 自愈重写,网段配置丢失一次(已接受)**。 +- 2026-08-21(同日四修订):**调试期 RTT 日志清理 + CQ 心跳宽松语义**——删除本会话为排障添加的全部 `[cq]`/`[netcfg]` SEGGER_RTT 日志(app_cq_proto/parse/cmd、app_udp `udp_cq port`、app_ldi 三分支、app_net_boot boot-apply)与相应 `SEGGER_RTT.h` include,§14 RTT 观察点同步改述;仅 `[fwver]` 保留。CQ 心跳复位放宽为「帧结构合法的 JSON(OK/ERR_CMD/ERR_PARAM)均视为上位机存活」,故障屏显示后下一存活帧先 `app_default_display_show()` 恢复默认画面;`s_cq_sync_counter`/`s_cq_warn_seconds` 跨任务读写加临界区保护(doc/03 PartB B.7.2)。 +- 2026-08-21(同日五修订):**LDI 搜索应答丢包修复(广播路径根治)**——现场「LDI 12H 搜索应答 50-75% 无响应」根因:dev 共存构建 baseline 4 netconn 已满(UDP 10011 + UDP 20103 + TCP Server listener + TCP Client),`app_udp_broadcast` 每次临时 `netconn_new` 必然 NULL 静默失败(TCP Client 周期性断开重连制造「池偶尔有空位」窗口,解释 25-50% 偶尔成功)。修复三层:① `lwipopts.h` 显式 `MEMP_NUM_NETCONN=8`、`MEMP_NUM_UDP_PCB=8`(opt.h 默认 4;bss 增量 304B,PBUF_POOL_SIZE 不动——SRAM 水位 87-94%,另行核算);② `app_udp_broadcast`/`app_udp_cq_broadcast` 改为复用常驻通道 conn(`udp_task`/`udp_cq_task` 创建时已设 SOF_BROADCAST,经 `app_channel_get` 回验 + conn 非空判空,deinit 新增 conn=NULL),常驻未就绪回退临时 conn;③ LDI 12H 应答改「广播为主、回源为辅」——先广播、回源失败静默。**已知限制(本轮不做)**:回源依赖通道级 `src_ip/src_port` 快照,LDI 帧排队期间 10011 上后续 IAP/CQ 帧覆盖快照 → 回源可能发错目标;IAP 回源同源竞态不修改(IAP 停等一问一答、低流量)。帧级 src 快照(frame_msg_t 扩展)属架构改造,留待后续。 +- 2026-08-24:**TCP 口应用两口径化(混合口径 bug 修复)**——现场「LDI 上位机把设备端口改为 9529 重启后,LDI 12H 与 IAP 0x01 均上报 9529,但 TCP 连接 9529 失败、9528 却能连上」根因:设备固件为 EIDE Debug「LDI 编入 + defineList 含 PROTO_CHONGQING」的混合口径构建,`app_net_boot_apply` 中 `app_tcp_server_set_port` 被 `#ifndef PROTO_CHONGQING` 裁剪(CQ 构建假定无 TCP 业务),而 `app_boot.c` 实际无口径裁剪、TCP Server 照常启动 → 绑定编译期默认 `TCP_SERVER_PORT`=9528(app_tcp_server.c),与搜索/上报的 Sector1 `net_cfg.port`(9529)矛盾。修复:`app_net_boot.c` 删除端口应用的两处 `#ifndef PROTO_CHONGQING` 裁剪(含 `app_tcp_server.h` include),TCP 口两口径统一应用 `net_cfg.port`——任意构建口径下 TCP 监听口恒与 LDI 12H / IAP 0x01 上报一致;CQ setip 语义不受影响(仍只写 `udp_port`、`port` 保留现值)。同步更正「CQ 构建不启动 TCP Server/Client 通道」的过时表述(app_boot.c 实无裁剪),doc/CLAUDE.md 与 doc/03 PartB B.7.1 同步。EIDE Debug 口径错配(defineList 与 excludeList 不成对)仍应修正,但代码已对混合口径兜底。 diff --git "a/doc/07_LDI\344\270\216IAP\351\205\215\347\275\256\350\247\243\350\200\246/03_interim_build_rules.md" "b/doc/07_LDI\344\270\216IAP\351\205\215\347\275\256\350\247\243\350\200\246/03_interim_build_rules.md" new file mode 100644 index 0000000..0d6ebc9 --- /dev/null +++ "b/doc/07_LDI\344\270\216IAP\351\205\215\347\275\256\350\247\243\350\200\246/03_interim_build_rules.md" @@ -0,0 +1,74 @@ +# 03 解耦前:工程选编临时规则(已废止) + +> **状态**:**已废止(2026-08-14)** | [02](./02_decoupling_solutions.md) 方案 A 已落地(配置层抽出为 `Application/Config/app_board_net_cfg.c`,公共必选),本文件仅存历史。 +> **历史目的**:在不改代码的前提下,避免「只加 LDI」链接失败。 + +--- + +## §1 规则(强制) + +### 编入 LDI 时 + +| 必须编入 | 可选 | +|----------|------| +| `Application/Src/LDI/**`(协议) | — | +| **`Application/Src/IAP/app_iap_cfg.c`** | — | +| `Application/Inc/IAP/app_iap_cfg.h`(头路径) | — | +| LDI 已有的存储依赖(`app_ldi_cfg` / W25 等) | — | +| | `app_iap.c` / `app_iap_cmd.c`(仅当需要 UDP IAP 升级) | + +**一句话**:LDI ⇒ 必带 **IAP 配置存储**;不必带 **IAP 升级协议**。 + +### 不编 LDI、只编 IAP 升级时 + +| 必须 | 说明 | +|------|------| +| `app_iap_cfg.c` + `app_iap.c` + `app_iap_cmd.c` | 协议本身依赖配置区 | + +### 两者都不编 + +可不编 `app_iap_cfg.c`(无引用则 `--gc-sections` 可丢;若他处仍引用则仍要保留)。 + +--- + +## §2 EIDE 操作提示 + +1. 排除整个 `IAP/` 目录前,确认是否仍勾选了 LDI。 +2. 若保留 LDI:在源文件列表中 **单独加回** `app_iap_cfg.c`(不要依赖「整个 IAP 文件夹」开关)。 +3. 链接失败时先 `rg app_flash_iap Application/Src/LDI`,对照本表,而不是先怀疑工具链。 + +--- + +## §3 Makefile 对照 + +全量 `make` 默认编入 IAP+LDI,一般无此问题。 +若增加 `PROTOCOL=ldi-only` 之类裁剪目标,须在源列表中显式保留 `app_iap_cfg.c`,或改为已解耦的 `app_board_net_cfg.c`。 + +--- + +## §4 验收(临时) + +- [ ] EIDE:LDI + 仅 `app_iap_cfg.c`,无 `app_iap.c` → **链接成功** +- [ ] EIDE:LDI,无任何 IAP 文件 → **链接失败**(符合当前代码预期;解耦后此条应反过来) + +--- + +## §5 已知限制(临时规则期间) + +临时规则只解决链接问题;以下现状缺陷**已按 [02 §9](./02_decoupling_solutions.md) 先行修复**(2026-08-14),剩余限制继续有效: + +| 限制 | 状态 / 说明 | +|------|------| +| 出厂新机 0AH 改 IP 不同步 Sector1 | **已修复(Q1)**:空 Sector1 写路径完整初始化(镜像 Bootloader 语义),Bootloader/Recovery 可读到该 IP | +| 0AH 写失败/断电无保护 | **已修复(Q2)**:两步写返回值如实应答(00H/01H),上位机重发恢复;上电同步钩子覆盖 Sector1 空/损坏(均完整初始化自愈,升级中间态仅放行 net_cfg 更新、update_sta/app_info 保留,见 [02 §10](./02_decoupling_solutions.md));掉电后按 [02 §8](./02_decoupling_solutions.md) 语义回退 | +| 运行中擦写 Sector1 停顿 1~2s | 每次 16KB 全扇区擦除(Q3 接受现状,仅增加同值跳过,不做磨损均衡);出厂配置操作可接受,高频改 IP 需产品论证 | + +--- + +## 修订 + +- 2026-08-14:与 01/02 同步首版。 +- 2026-08-14(同日修订):新增 §5 已知限制(出厂新机 0AH、写失败/断电、擦写停顿)。 +- 2026-08-14(同日再修订):§5 改为「已修复/剩余限制」两态,同步 [02 §9](./02_decoupling_solutions.md) 三决策落地状态。 +- 2026-08-14(同日三修订):§5「写失败/断电」行同步 [02 §10](./02_decoupling_solutions.md) 守卫细化——Sector1 空/损坏均完整初始化自愈,仅升级中间态拒绝覆盖。 +- 2026-08-14(同日四修订):**废止**——方案 A 落地后「必带 `app_iap_cfg.c`」不再必要;配置层 `app_board_net_cfg.c` 列为公共必选,LDI / IAP 协议目录均可独立裁剪。 diff --git "a/doc/07_LDI\344\270\216IAP\351\205\215\347\275\256\350\247\243\350\200\246/README.md" "b/doc/07_LDI\344\270\216IAP\351\205\215\347\275\256\350\247\243\350\200\246/README.md" new file mode 100644 index 0000000..6b14df9 --- /dev/null +++ "b/doc/07_LDI\344\270\216IAP\351\205\215\347\275\256\350\247\243\350\200\246/README.md" @@ -0,0 +1,62 @@ +# 07 — LDI 与 IAP 配置耦合 / 解耦 + +> **范围**:EIDE/目录选编时「只编 LDI、不编 IAP」导致链接失败的根因、影响面,以及解耦方案。 +> **状态**:问题 **已确认**(2026-08-14);三处缺陷 **已定案并先行修复**(2026-08-14,见 [02 §9](./02_decoupling_solutions.md));两项补充修复(守卫细化 + 4B02 结果码)**已落地**(见 [02 §10](./02_decoupling_solutions.md));**方案 A 主体重构(抽出板级网络配置模块 `app_board_net_cfg`)已落地(2026-08-14)**。 +> **非本目录**:多协议 RB/绑定 → [`../05_协议模块多协议兼容优化/`](../05_协议模块多协议兼容优化/README.md);SRAM/CCM → [`../06_SRAM内部分数据迁移/`](../06_SRAM内部分数据迁移/README.md)。 +> **备注(2026-08-14)**:`doc/CLAUDE.md` 中「当前固件入口 0x08000000、无 IAP bootloader 运行」已过时——链接脚本 `Compiler/STM32F407XX_FLASH.ld:67` 现为 `ORIGIN=0x08040000, LENGTH=768K`,`Core/Src/main.c:15` 已设 `SCB->VTOR = FLASH_BASE|0x40000`(主固件已按 IAP 适配入口链接);本目录 01/02/03 均按此现状撰写。 + +--- + +## 文档结构 + +| 文件 | 角色 | +|------|------| +| [01_background_and_impact.md](./01_background_and_impact.md) | **背景、根因、影响面** | +| [02_decoupling_solutions.md](./02_decoupling_solutions.md) | **解耦方案**(推荐路径 + 备选 + 验收) | +| [03_interim_build_rules.md](./03_interim_build_rules.md) | **解耦前**工程选编临时规则 | + +## 快速结论 + +| 问 | 答 | +|----|-----| +| 只加 LDI 为何链接失败? | LDI **硬依赖** `app_iap_cfg`(Sector1 网络配置 API),不是依赖 IAP **升级协议** | +| 是否必须加整包 IAP? | **否**。最小要 `app_iap_cfg.c`;`app_iap.c` / `app_iap_cmd.c` 可裁 | +| 长期怎么解? | **已落地(2026-08-14)**:抽出中立模块 `app_board_net_cfg`(`Application/Config`),LDI 与 IAP 协议都依赖它;`app_iap_cfg.{c,h}` 与死代码 `Device/Inc/config_info.h` 已删除 | +| 改码纪律 | 方案在 `02`;**审核通过后才改代码**(三处缺陷修复已定案落地,见 [02 §9](./02_decoupling_solutions.md)) | +| 出厂新机 0AH 改 IP,Boot 为何读不到? | 现状缺陷已修复(2026-08-14):空 Sector1 写路径不置 magic([01 §3.4](./01_background_and_impact.md))→ 现按 Bootloader 语义完整初始化;0AH 写失败如实应答 01H,上位机重发恢复 | +| 三固件出厂默认网络配置 | 统一为 192.168.114.200/24、网关 192.168.114.1、端口 9528;Recovery 上电读 Sector1.net_cfg 配 netif、主固件 W25 空 + Sector1 有效时同步 set_ip(见 [02 §11](./02_decoupling_solutions.md)) | +| 网络配置真源与镜像 | Sector1.net_cfg 为**真源**(三固件共享);W25 网络字段为**恢复镜像**(擦 Sector1 后还原用户配置);主固件上电 Sector1 优先、W25 自愈回写、皆空用出厂默认(见 [02 §12](./02_decoupling_solutions.md)) | +| W25 镜像自愈 | 主固件上电分支 1 检测 Sector1 与 W25 网络字段不一致即刷新镜像(覆盖 4B02 单写 Sector1 或单次 W25 写失败的陈旧,见 [02 §12](./02_decoupling_solutions.md)) | +| Bootloader 损坏态自愈 | magic 不匹配或 config_crc 错 → 条件 C 重建出厂记录自愈;App 损坏仍 Recovery;Recovery 判空保持两哨兵(防死循环进 Recovery 的分工,见 [02 §12](./02_decoupling_solutions.md)) | +| 搜索/上报端口字段语义 | LDI 12H 应答与 IAP 0x01 上报的 `port` 字段 = 配置功能端口(TCP 业务口,出厂默认 9528,宏 `LDI_DEFAULT_CONFIG_PORT`);UDP 10011(宏 `LDI_DISCOVERY_PORT`)仅为搜索/上报的**传输渠道**,勿混淆(2026-08-18 修复 12H 误报 10011 污染循环,见 [02 §12 ④](./02_decoupling_solutions.md)) | +| LDI 12H 应答发送路径 | **广播为主、回源为辅**(2026-08-21):先 `app_udp_broadcast` 再 `channel_send` 回源(失败静默);广播复用常驻通道 conn(`lwipopts.h` `MEMP_NUM_NETCONN=8`/`MEMP_NUM_UDP_PCB=8` 扩池根治池满丢包)。**已知限制**:回源依赖通道级 src 快照,多协议交错下可能发错目标;IAP 回源同源竞态不修改(见 [02 修订 2026-08-21 五](./02_decoupling_solutions.md)) | +| 配置生效时机 | 所有改 IP 接口(LDI 0AH / IAP 4B02 / Recovery IAP)统一**重启生效**(既定策略,非缺陷;4B02 已删除即时应用段) | +| UDP 发现口 | 10011 固定(宏 `LDI_DISCOVERY_PORT`),禁止提供修改接口 | +| IAP 0x03 版本号来源 | 主固件启动时把 `PROGRAM_CODE` 落库 Sector1 `app_info.version`(`app_board_net_cfg_fw_version_update`,见 [02 §13](./02_decoupling_solutions.md))——此前无人写入恒报全 0 | + +## 与 05 的边界 + +| | 05 多协议 | 07 本专题 | +|--|-----------|-----------| +| 主题 | 谁绑哪个通道 / RB | **配置存储归属**与编译依赖 | +| 协议帧 | LDI / IAP 可同 RJ45 链式 | 即使不编 IAP 协议,配置层仍可能被 LDI 引用 | + +阅读顺序:`01` → `03`(临时规避)→ `02`(正式解耦)。 + +--- + +## 修订 + +- 2026-08-14:首版,与 01/02/03 同步。 +- 2026-08-14(同日修订):补充固件入口现状备注(doc/CLAUDE.md 矛盾点)、出厂新机 0AH 缺陷入口。 +- 2026-08-14(同日再修订):状态改为「缺陷已定案并先行修复、主体重构暂缓」;快速结论同步 [02 §9](./02_decoupling_solutions.md) 三决策。 +- 2026-08-14(同日三修订):状态补两项补充修复(守卫细化 + 4B02 结果码,见 [02 §10](./02_decoupling_solutions.md))。 +- 2026-08-14(同日四修订):**方案 A 落地**——抽出 `app_board_net_cfg`(`Application/Config`,Sector1 布局唯一归属 + static_assert 锁定),LDI/IAP 迁移到 `app_board_net_cfg_get/update/read`,删除 `app_iap_cfg.{c,h}` 与 `Device/Inc/config_info.h`;[03](./03_interim_build_rules.md) 选编临时规则废止。 +- 2026-08-18:新增 [02 §11](./02_decoupling_solutions.md) 三固件出厂默认网络配置统一约定(192.168.114.200/24、网关 192.168.114.1、端口 9528);快速结论同步三项一致性修复现状。 +- 2026-08-18(同日再修订):同步 [02 §12](./02_decoupling_solutions.md) 网络配置真源与生效语义——Sector1 真源/W25 恢复镜像、0AH/4B02 重启生效(既定策略)、UDP 发现口固定宏 `LDI_DISCOVERY_PORT`(删除 `app_udp_set_port`)。 +- 2026-08-18(同日三修订):快速结论同步 [02 §12](./02_decoupling_solutions.md) 缺陷 A/C/D/E 修复——W25 镜像自愈、Bootloader 损坏态重建(magic/CRC 错 → 条件 C 重建出厂记录;Recovery 判空保持两哨兵防死循环)、所有改 IP 接口(0AH/4B02/Recovery IAP)统一重启生效。 +- 2026-08-18(12H 端口字段语义修复):12H 应答 port 由误用发现口 10011 改为配置功能端口(device_port / 默认 9528),修复 10011 污染循环(12H 报 10011 → 上位机写回 → TCP Server 错监听 10011);新增宏 `LDI_DEFAULT_CONFIG_PORT`;存量污染设备需 0AH 重设 9528 或擦 Sector1(见 [02 §12 ④](./02_decoupling_solutions.md))。 +- 2026-08-18(同日四修订):新增 [02 §13](./02_decoupling_solutions.md) 固件版本落库——IAP 0x03 上报的 version 由主固件启动时从 PROGRAM_CODE 写入 Sector1(空/损坏自愈、升级中间态拒绝、同值跳过)。 +- 2026-08-21:新增 [02 §14](./02_decoupling_solutions.md) 网络配置应用横切解耦——中立模块 `app_net_boot`(`app_net_boot_apply`)统一承担「Sector1 net_cfg → netif + TCP Server 口」应用,`ldi_ctx_init` 三分支与 CQ `_cq_net_cfg_apply` 不再直接改 netif;accept_write 两口径统一(空/损坏/非法写本构建默认落盘并应用:CQ 192.168.1.5/20103、dev 192.168.114.200/9528);CQ 构建不启动 TCP Server/Client 通道。同步 [02 §12](./02_decoupling_solutions.md) 分支 1 条件修订(`update_sta` 不参与分支判定)。 +- 2026-08-21(同日再修订):**LDI 搜索应答丢包修复**——根因 netconn 池满(baseline 4 已满)+ 广播临时 conn 必然 NULL;修复:`lwipopts.h` 扩池(`MEMP_NUM_NETCONN=8`、`MEMP_NUM_UDP_PCB=8`,bss +304B)+ `app_udp_broadcast`/`app_udp_cq_broadcast` 复用常驻通道 conn + LDI 12H 应答「广播为主、回源容错」。已知限制:回源 src 快照竞态(LDI 12H 与 IAP 同源,本轮不架构改造)。 +- 2026-08-24:**TCP 口应用两口径化**——修复「LDI 改端口后搜索上报新值、TCP 仍监听 9528」现场 bug:EIDE Debug 混合口径(LDI 编入 + PROTO_CHONGQING)下 `app_net_boot_apply` 的 `app_tcp_server_set_port` 被宏裁剪、TCP Server 恒绑 9528;修复后 TCP 口两口径统一应用 Sector1 `net_cfg.port`([02 §14](./02_decoupling_solutions.md) 同步,CQ setip 语义不变);「CQ 构建不启动 TCP Server/Client 通道」表述更正为两口径均启动。 diff --git "a/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/01_\351\200\232\347\224\250\350\247\204\345\210\231\344\270\216\346\250\241\345\235\227\351\252\250\346\236\266.md" "b/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/01_\351\200\232\347\224\250\350\247\204\345\210\231\344\270\216\346\250\241\345\235\227\351\252\250\346\236\266.md" new file mode 100644 index 0000000..7044a25 --- /dev/null +++ "b/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/01_\351\200\232\347\224\250\350\247\204\345\210\231\344\270\216\346\250\241\345\235\227\351\252\250\346\236\266.md" @@ -0,0 +1,181 @@ +# 01 通用规则与模块骨架 + +> 任何协议模块(串口或网络)都适用的规则。串口专项见 `02`,网络专项见 `03`,操作步骤见 `04`。 + +--- + +## 1. 目录与命名 + +### 1.1 目录 + +``` +Application/Src/ProtocolParser_XXX/ ← 实现(.c) +Application/Inc/ProtocolParser_XXX/ ← 头文件(.h) +``` + +现有实例:`ProtocolParser_QingHai/`、`ProtocolParser_SiChuang_ETC/`、`ProtocolParser_SiChuang_MTC/`、`ProtocolParser_SiChuang_Overload/`、`ProtocolParser_ShanDong/`、`ProtocolParser_GuiZhou/`、`ProtocolParser_YunNan/`。 +非地区协议的历史目录 `IAP/`、`LDI/`、`RLS/` 沿用原名,新地区协议一律用 `ProtocolParser_XXX` 命名。 + +### 1.2 文件拆分惯例 + +| 文件 | 职责 | 现有范例 | +|---|---|---| +| `app_xxx_proto.c` | 注册(五步)、probe、handle_task | `app_qh_proto.c`、`app_sc_etc_proto.c` | +| `app_xxx_proto_parse.c/.h` | 帧 → `xxx_parsed_cmd_t` 的**纯解析**(不碰外设、不发包) | `app_sc_etc_proto_parse.c`(`sc_etc_parse_frame` :50) | +| `app_xxx_proto_cmd.c/.h` | 命令执行(渲染、GPIO、应答发送) | `app_sc_etc_proto_cmd.c` | +| `app_xxx_proto_voice.c`(可选) | 语音命令,经 `dev_rs232_voice` 旁路 USART6 | `app_qh_proto_voice.c`、`app_sc_mtc_proto_voice.c` | + +### 1.3 命名 + +- 模块前缀:`app_xxx_`(如 `app_sc_mtc_`、`app_qh_`)。 +- 模块内静态函数:`_` 前缀(如 `_sc_ol_bcc_calc`,`app_sc_ol_proto_parse.c:19`)。 +- 模块状态/句柄:`s_xxx_` 静态变量(如 `s_qh_mask`、`s_sc_etc_queue`)。 +- 公开入口:`xxx_proto_init` + `xxx_proto_handle_task`;probe 函数 `xxx_probe_frame` 签名必须匹配 `proto_probe_fn_t`。 + +## 2. 注册流程五步 + +以 `app_sc_etc_proto.c:135-190`、`app_qh_proto.c:47-78` 为范例(两者同构)。骨架: + +```c +/* 第 1 步:编译期提供通道 RB(weak,多协议同槽自动合并为一个静态体) */ +RB_PROVIDE_WEAK(rb_provide_rs485, RB_SIZE_RS485); +RB_PROVIDE_WEAK(rb_provide_rs232, RB_SIZE_RS232); + +static proto_mask_t s_xxx_mask; /* 主通道掩码 */ +static proto_mask_t s_xxx_mask_rs232; /* 第二通道掩码(可选) */ + +void xxx_proto_init(void) +{ + /* 第 2 步:取 RB(判空链,取不到直接放弃注册) */ + ring_buffer_t *rb = app_proto_acquire_buf(RB_SLOT_RS485, RB_SIZE_RS485); + if (rb == nullptr) return; + + /* 第 3 步:注册 probe + RB,自动分配 2 的幂掩码 */ + s_xxx_mask = app_proto_register(xxx_probe_frame, rb); + if (s_xxx_mask == 0) return; + + /* 第 4 步:绑定通道(每逻辑通道独立 mask + bind;可多次) */ + app_proto_bind_channel(s_xxx_mask, CH_ID_RS485); + + /* 第 5 步:创建队列与任务;队列句柄回报框架 */ + s_xxx_queue = osMessageQueueNew(X_QUEUE_DEPTH, X_MSG_SIZE, &s_xxx_queue_attr); + app_proto_set_frame_queue(s_xxx_mask, s_xxx_queue); + + static const osThreadAttr_t s_xxx_task_attr = { + .name = "xxx_handle_task", .stack_size = 256 * 4, .priority = osPriorityNormal, + }; + osThreadNew(xxx_proto_handle_task, NULL, &s_xxx_task_attr); +} +sw_app_initcall(xxx_proto_init); +``` + +要点(全部为代码事实): + +- **`RB_PROVIDE_WEAK`**(`Kernel/Inc/ring_buffer.h:40`):生成 `__attribute__((weak))` getter + 内部 `static` 缓冲体。多个协议 TU 提供同名槽 → 链接器只保留一个 getter;未编入任何提供者 → 符号为 0 → `acquire` 返回 `nullptr`(该槽不占 RAM)。getter 名必须是裸标识符 `rb_provide_rj45`(对应 `RB_PROVIDE_RJ45` 宏,`app_dispatch.h:42-44`),勿传宏名(`##` 不二次展开)。 +- **`app_proto_acquire_buf(id, size)`**(`app_dispatch.h:121`,实现 `app_dispatch.c:141`):`id` 是 `rb_slot_t`(RB_SLOT_RJ45/RS485/RS232);`size` 不得超过槽实际容量(`size > rb->size` 返回 nullptr);首次调用时 `rb_init` 绑定优先级继承互斥锁。 +- **`app_proto_register(probe, rb)`**(`app_dispatch.h:118`,实现 :111):返回 `proto_mask_t`(2 的幂,0 = 32 槽已满)。多逻辑通道必须**各注册一次拿独立 mask**(LDI 先例:TCP_SERVER / TCP_CLIENT / UDP 三 mask,`app_ldi.c:135-157`),禁止单 mask 绑多通道。 +- **`app_proto_bind_channel(mask, ch_id)`**(`app_dispatch.h:120`,实现 OR 累积):多协议可绑同一通道,同一协议可绑多通道。 +- **`app_proto_set_frame_queue(mask, queue)`**(`app_dispatch.h:119`):框架按 mask 查表推帧。同一协议多 mask 可共用同一队列(QH/LDI/SC 均如此)。 +- **`sw_app_initcall(xxx_proto_init)`**:注册进 `__sw_initcall` 段。同层按**链接顺序(源码收录序,非字母序)**执行——`SORT()` 对同名 section 退化为 .o 链接顺序。注册顺序 = probe 优先级顺序:**源码收录序靠前的协议 probe 先被调用**(Makefile `SRC_APPLICATION` 与 `.eide/eide.yml` virtualFolder 收录序当前均约定 QH→SD→GZ→MTC,禁止随意调整;`app_dispatch_init` 靠收录序在协议之前 → 调度系统先就绪)。 +- **任务栈惯例 `256*4`(1KB)**:所有协议 handle_task 与通道任务统一 256×4、`osPriorityNormal`;帧缓冲用 `static`,不放大栈。 + +## 3. probe 契约(PURE) + +签名(`app_dispatch.h:90`): + +```c +typedef proto_probe_sta_t (*proto_probe_fn_t)(const channel_t *ch, + const ring_buffer_t *rb, uint32_t *total_len, uint8_t *aux); +``` + +返回值四种状态(`app_dispatch.h:65-70`): + +| 状态 | 含义 | 框架动作 | +|---|---|---| +| `PROTO_PROBE_READY` | 完整帧就绪 | `rb_read` 整帧 → 推协议队列 → 本轮结束 | +| `PROTO_PROBE_SKIP` | 帧结构合法但不属于本设备 | `rb_skip` 整帧 → 本轮结束(LDI lane/cert 不匹配先例,`app_ldi.c:356-362`) | +| `PROTO_PROBE_WAIT` | 帧头匹配但数据不足 | 继续试下一协议;置 any_wait | +| `PROTO_PROBE_FAKE` | 明确不是本协议 | 继续试下一协议;置 any_fake | + +**强制规则**(doc/05-01 §3.2,全部有代码先例): + +1. **只 `rb_peek`,零副作用**:禁止 probe 内 `rb_read`/`rb_skip`/写业务全局。反例教训:LDI 曾在 probe 里写 `g_ldi.rsp_seq`,已移到 `ldi_handle_task` 读帧后(`app_ldi.c:322` 注释)。 +2. **`avail==0` 必须返回 FAKE**:无数据不得盲 WAIT,否则阻塞同 RB 链式探测(每个 probe 首行都是这个判断)。 +3. **首字节快拒**:首字节不匹配立即 FAKE(IAP `0x5A`、LDI `0xFF`、ETC `0x0A`、治超 `0xFF`、青海/MTC `'{'`)。 +4. **仅 READY/SKIP 写 `*total_len`**(可同时写 `*aux`,如 RLS 写命令高字节、IAP 写 cmd、LDI 写 cmd)。WAIT/FAKE 不得碰输出参数。 +5. **长度上下界**:长度字段必须双向校验(治超 `len_byte < SC_OL_FRAME_LEN_MIN || len_byte == 0xFE` 即拒,`app_sc_ol_proto.c:118`;IAP `payload_len > 256` 即拒,`app_iap.c:126`)。 +6. **尾字节校验**:定界类帧探到结尾字节才 READY(ETC `0x0D`、MTC `'}'`、治超尾 `0xFF`、RLS 尾 `0x0D 0x0C`)。校验和(CRC/BCC)可放 probe(IAP CRC32、LDI CRC16-XMODEM)或放 parse(治超 BCC 在 `sc_ol_parse_frame`,`app_sc_ol_proto_parse.c:79-81`)。 + +**链式聚合语义**(`app_dispatch.c:272-336`,写 probe 前必须理解): + +``` +同 RB 上按注册顺序逐个 probe: + READY/SKIP → 消费后结束本轮,回 while(avail>0) 继续取下一帧 + WAIT/FAKE → 继续试下一协议 +一轮无一消费: + any_wait → break,等更多字节(禁止 skip——帧头已匹配,数据未到) + 全 FAKE → rb_skip(1),逐字节重同步 +``` + +推论:probe 的「帧头匹配 + 数据不足」必须 WAIT;「帧头匹配但结构非法」必须 FAKE(不能 WAIT 卡死链)。 + +## 4. 帧队列 + +所有协议帧队列**静态分配**(不占 ucHeap),结构如下(范例 `app_qh_proto.c:19-32`): + +```c +#define X_PAYLOAD_MAX (…) /* 协议最大帧长(含头尾) */ +#define X_MSG_SIZE (sizeof(frame_msg_t) + X_PAYLOAD_MAX) +#define X_QUEUE_DEPTH (3U) +_Static_assert(X_PAYLOAD_MAX <= RB_SIZE_RS485, "X frame must fit RS485 RB"); +_Static_assert(X_PAYLOAD_MAX <= RB_SIZE_RS232, "X frame must fit RS232 RB"); +static StaticQueue_t s_x_queue_cb; +static uint8_t s_x_queue_buf[X_QUEUE_DEPTH * X_MSG_SIZE]; +static const osMessageQueueAttr_t s_x_queue_attr = { + .name = "x_queue", .cb_mem = &s_x_queue_cb, .cb_size = sizeof(s_x_queue_cb), + .mq_mem = s_x_queue_buf, .mq_size = sizeof(s_x_queue_buf), +}; +``` + +- **`frame_msg_t`**(`app_dispatch.h:89-96`):`{ channel_t *ch; uint16_t data_len; uint8_t data[]; }`,`sizeof(frame_msg_t)=8`。handle_task 从 `msg->ch` 拿到源通道用于应答。 +- **深度惯例 2~4**:IAP 2(停等升级)/ LDI 4(一包多帧粘包)/ 青海 3 / RLS 2 / ETC 3 / MTC 3 / 治超 3。深度决策依据记录在 doc/05-01 §4.1。 +- **payload 上限 ≤ RB 容量且 static_assert**:QH(259 ≤ 768)、SC_ETC(149)、SC_MTC(74)、SC_OL(255)均有 `_Static_assert`;RLS(530)只 assert RS485。网络侧 IAP(1044)/LDI(512)≤ RB_SIZE_RJ45(1536),依赖 `RB_SIZE_RJ45` 注释契约(`app_dispatch.h:37`)。**新模块一律补 static_assert**。 +- 队列数据体是 SRAM `.bss` 静态量,必须同步 `doc/06-04` 账。 + +## 5. 执行层 + +- **handle_task 查表分派**:阻塞 `osMessageQueueGet` → `xxx_parse_frame(msg->data, msg->data_len)` → 按命令查表调执行函数。范例:`iap_handle_task`(`app_iap.c:70-85`,`g_iap_cmd_table[cmd]`)、`rls_handle_task`(cmd_index_table 线性查表)。 +- **parse 纯函数化**:`xxx_parse_frame(const uint8_t *raw, uint16_t raw_len)` 只产出 `xxx_parsed_cmd_t`(含 `sta` 错误码),不发包、不渲染。执行副作用全部在 cmd 层。 +- **状态存储**:需要跨帧状态时用模块级 `static`(如 LDI 状态机 `g_ldi.state`;ETC 心跳计时 `s_etc_uptime_s`)。probe 不得读写这些状态(PURE)。 +- **应答「收到即回」约定**:解析完成立即在 cmd 层 `channel_send(ch, …)` 应答;解析失败也应答错误码。范例:ETC 屏应答 `0A XX 0D`(00 正常/01 超长/02 帧错,`app_sc_etc_proto_cmd.c:41-47`,失败仍应答 :230);心跳帧识别后丢弃不应答(`0A 50 0D`)。 + +## 6. 内存纪律 + +| 项 | 位置 | 规则 | +|---|---|---| +| 协议帧队列 | SRAM `.bss` 静态 | 不占 ucHeap;新增即改 `doc/06-04` | +| RB 体 | SRAM `.bss`(weak 提供者的 static 缓冲) | 一槽一份;不重复提供 | +| 任务栈 | ucHeap(36KB,`FreeRTOSConfig.h`) | 256×4=1KB/个;新增任务即扣 1KB | +| 帧缓冲 | handle_task 内 `static uint8_t msg_buf[X_MSG_SIZE]` | SRAM 静态,不入栈 | +| 互斥锁/信号量 | `osMutexNew`/`osSemaphoreNew` 动态 | 从 ucHeap 支出(LDI `tx_lock` 先例,`app_ldi.c:162-163`) | + +任何新增静态 SRAM(队列、状态、缓冲)都必须同步 `doc/06_SRAM内部分数据迁移/04_current_memory_occupancy.md`(**注意该账是 EIDE Debug 采样口径,勿用 Makefile 构建重采**,见 06-04 §0)。 + +## 7. 文档义务(接入即办) + +1. **帧头登记**:`doc/05-01 §3.3 帧头事实`(:115)加一行:协议名 + 帧头特征 + 校验方式。 +2. **兼容矩阵**:`doc/05-01 §6 兼容矩阵`(:190)登记与既有协议的同槽组合判定(可共存/互斥/需 EIDE 排除)。 +3. **queue 深度表**:`doc/05-01 §4.1`(:146)登记 depth/payload/静态体大小。 +4. **内存占用**:`doc/06-04` 协议帧 queue 表登记新增队列。 +5. **代码注释纪律**:模块内注释描述协议语义时,**不得引用外部参考工程名/路径**(如「对齐 9K1F212701」此类历史注释不再新增);只写本设备约束与数值来源(协议文档编号、帧结构推导)。 + +## 8. 反面清单(评审对照) + +- probe 内有 `rb_read`/`rb_skip`/全局写 → 违反 PURE。 +- `avail==0` 返回 WAIT → 阻塞链式探测。 +- WAIT/FAKE 路径写 `*total_len` → 脏数据污染框架。 +- payload 上限 > RB 容量且无 static_assert → 队列越界风险。 +- bind `CH_ID_RS232_1` → 语音冲突,禁止。 +- 单 mask 绑多逻辑通道 → 违反「每逻辑通道独立 mask」。 +- 队列用动态 `osMessageQueueNew(..., NULL)` 非静态 → 增加 ucHeap 压力。 +- `{` 帧族协议(QH/SD/GZ/SC_MTC/YN)主文件缺 `g_brace_proto_guard` 编译期互斥守卫(`#ifndef STD_ALL_PROTO` 包裹 + `__attribute__((used))`)→ 量产多 `{` 族编入不再链接报错。Makefile 全协议开发构建定义 `-DSTD_ALL_PROTO` 豁免守卫。 \ No newline at end of file diff --git "a/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/02_\344\270\262\345\217\243\345\215\217\350\256\256\346\216\245\345\205\245.md" "b/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/02_\344\270\262\345\217\243\345\215\217\350\256\256\346\216\245\345\205\245.md" new file mode 100644 index 0000000..beb16e8 --- /dev/null +++ "b/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/02_\344\270\262\345\217\243\345\215\217\350\256\256\346\216\245\345\205\245.md" @@ -0,0 +1,125 @@ +# 02 串口协议接入(RS485 / RS232) + +> 通用规则见 `01`。本文只讲串口专项:通道与 RB、波特率、接收路径、同槽共存判别、三种变长定界模式、调试定位。 + +--- + +## 1. 通道与 RB + +| 物理口 | UART | CH_ID | RB 槽 | RB 容量 | 绑定协议(现行) | +|---|---|---|---|---|---| +| RS485 | USART1 | `CH_ID_RS485` (0) | `RB_SLOT_RS485` | **768** | 青海 / RLS / 四川 ETC / MTC / 治超 / 山东 / 贵州 / 云南 | +| RS232-0 | USART3 | `CH_ID_RS232` (1) | `RB_SLOT_RS232` | **768** | 青海 / 四川 ETC / MTC / 治超 / 山东 / 贵州 / 云南 | +| RS232-1 | USART6 | `CH_ID_RS232_1` (6) | 无 | 无 | **仅语音 TX,禁止任何协议 bind** | + +- 尺寸定义:`app_dispatch.h:37-39`(`RB_SIZE_RS485 (768U)`、`RB_SIZE_RS232 (768U)`)。 +- 通道 ID 枚举:`app_dispatch.h:47-55`;`CH_ID_RS232_1` 注释明示「[仅语音 TX] USART6 — 禁止任何协议 bind_channel」。 +- **RS232_1 禁绑**是架构约束(doc/05-01 §2.5 / §6):`app_rs232_1_start()` 是纯 TX 桩(返回 `nullptr`,`app_rs232.c:105-116`),语音经 `dev_rs232_voice_play` → `pl_uart_send(PL_UART6)` 旁路 dispatch。新协议 bind 它会拿到不存在的通道。 +- RB 提供方式:协议 TU 内 `RB_PROVIDE_WEAK(rb_provide_rs485, RB_SIZE_RS485)` + `RB_PROVIDE_WEAK(rb_provide_rs232, RB_SIZE_RS232)`(范例 `app_qh_proto.c:12-13`、`app_sc_etc_proto.c:24-25`)。同一槽多 TU 提供 → 链接器保留一份(合并且只占一份 RAM)。 + +## 2. 波特率(DIP1 统一选择,RS232 + RS485 同步) + +- **DIP1 = PE7,active_low**:ON = 115200,OFF = 9600(`app_uart_baud.h:12-13`:`APP_UART_BAUD_ON (115200U)` / `APP_UART_BAUD_OFF (9600U)`)。 +- 上电应用:`app_uart_baud_init`(`sw_app_initcall`,`app_uart_baud.c:46-56`)读 `dev_key_get_state(DEV_KEY_DIP1)` → `app_uart_baud_apply`,**先于通道任务启动**(此时 DMA RX 未挂,切换安全)。 +- 运行态切换:`app_uart_baud_apply(baud)`(`app_uart_baud.c:20-40`)→ `pl_uart_set_baud(h, baud)` 同步重配 USART1 + USART3,任一路失败即中止(避免两路口不一致)。`pl_uart_set_baud`(`Platform/Src/pl_uart.c:109-135`)内部「停 DMA RX → DeInit → 改 BaudRate → Init → 重挂 RX」。 +- **先例**:MTC `7B 40` 改波特率命令 → `_sc_mtc_exec_baud` 判定 `0x2580`(9600)/其他(115200) → `app_uart_baud_apply`(`app_sc_mtc_proto_cmd.c:329-332`,分派 :401)。新协议若带波特率命令,照抄此路径。 +- **注意**:DIP2(PE8)已被 app_render 字库芯片选择占用,不得改动(`app_uart_baud.c:43` 注释)。新协议自行设波特率必须经 `app_uart_baud_apply`,禁止直接 HAL 改。 + +## 3. 接收路径(数据流全景) + +``` +USART RX DMA(乒乓双缓冲 2×640=1280B 静态缓冲,circular HT/TC + IDLE 增量冲刷) + │ RS485: Device/Inc/dev_rs485.h:10 RS485_BUF_SIZE (640U)×2(s_rs485_buf) + │ RS232: Device/Inc/dev_rs232.h:10 RS232_BUF_SIZE (640U)×2 + ▼ +ISR(pl_uart.c USART1/3_IRQHandler → uart_idle_handle;DMA HT/TC 经 + HAL_UART_RxHalfCpltCallback / RxCpltCallback 覆盖) + ▼ osMessageQueuePut(rx_queue, {块号,偏移,长度}) ← rx_queue 深度 2 +通道任务 rs485_task / rs232_task(栈 256×4) + ▼ 按块号从 rx_buf + block*block_size + offset 拷 len 字节 + │ app_channel_dispatch(&self->me, src, len) + │ app_rs485.c:101 / app_rs232.c:105 + ▼ 按 ch_proto_map 查掩码 → rb_write(同 RB 指针 seen[] 去重只写一次) + │ app_dispatch.c:376-419 + ▼ osMessageQueuePut(g_ch_queue, &ch) +frame_dispatch_task(栈 256×4,app_dispatch.c:226) + ▼ 持锁跨整轮探测:链式 probe(见 01 §3) + ▼ READY → rb_read 整帧 → osMessageQueuePut(frame_queue[i]) +协议 handle_task + ▼ xxx_parse_frame → 查表分派 → channel_send(ch, …) 应答 + ▼ ch->ops->send → pl_uart_send(rs485_send/rs232_send,100ms 超时) +``` + +协议模块**不需要**接触 UART/DMA/ISR;只做 register/bind/probe/handle。通道任务由 `app_boot` 的 `init_task` 统一启动(`app_boot.c:143-148`),新协议不加启动代码。 + +## 4. 同槽共存判别技巧 + +RS485/RS232 槽是多协议链式共享的,probe 写法决定能否共存。三个既有范例: + +### 4.1 帧头互斥(最干净) +首字节不同即可天然共存:`'{'`(青海/MTC)vs `0x0A`(ETC)vs `0xFF`(RLS/治超)。各自 probe 首字节快拒互不干扰。 + +### 4.2 第二字节区分(同帧头) +- **RLS `FF FE` vs 治超 `FF len`**:治超长度字段合法区间 [7,255],但**显式排除 0xFE(254)**(`app_sc_ol_proto.c:118`:`len_byte < SC_OL_FRAME_LEN_MIN || len_byte == 0xFE` 即 FAKE)——不排除的话治超 probe 会把 RLS 帧当超长帧 WAIT 卡死链式探测;RLS probe 则要求第二字节必须 `0xFE`(`app_rls.c:87-89`)。双向快拒成立,共存合法(doc/05-01 §6)。 +- **ETC `0A xx` vs MTC `0A 46`**:命令位互斥——ETC 命令集 {00,01,36~39,40,50},MTC 只认 `0A 46 0A/0A 46 0D`;ETC probe 对 0x46 走 default FAKE,MTC probe 对非 46 走 FAKE(`app_sc_etc_proto.c:262-263`、`app_sc_mtc_proto.c:131-141`)。 + +### 4.3 同帧头同字节流 → 必须 EIDE 目录排除互斥 +- **青海 `'{'` vs 四川 MTC `'{'`**:MTC 的 '}' 定界变长扫描(上限 74B)对「青海帧数据段含 0x7D(GBK 尾字节)且半帧到达」存在截断认领残余风险。青海 probe 先注册(源码收录序 QH 在前)先认领完整帧,但残余风险无法在 probe 层彻底消除 → **量产必须二选一,靠 `.eide/eide.yml` excludeList 排除目录兜底**(doc/05-01 §6)。 +- 判定纪律:**新协议帧头与既有协议重叠时,先到 doc/05-01 §6 查兼容矩阵**;矩阵中标注「禁止同 RB」的组合不得同编,标注「链式允许」的按 4.1/4.2 写 probe。 + +## 5. 三种变长帧定界模式(照抄范例) + +### 5.1 `0x0D` 结尾扫描 — 四川 ETC(`app_sc_etc_proto.c:189-263`) + +``` +0A <显示方式 00/01> <行号 0~6> <数据…> 0D (显示帧,变长) +0A 36/37/38/39/50 0D (灯控/心跳,定长 3) +0A 40 XX YY 0D (亮度,定长 5) +``` + +probe 要点: +- 首字节 `0x0A` 快拒;命令位查表分定长/变长分支。 +- 定长分支:字节数够 → peek 尾字节 `0x0D` 校验 → READY。 +- 变长分支:从 `scan_from` 起逐字节扫 `0x0D`(静态显示从 3 起;滚屏帧头 6 字节固定,从 6 起扫,避免 rt/st 控制字节恰为 0x0D 时误定界);扫到即 READY(`*total_len = off + 1`)。 +- **扫描上限**:`max_total = SC_ETC_PAYLOAD_MAX (149U)`(0x0D 最迟索引 148 → 帧 ≤149B);`avail >= max_total` 仍无 0x0D → FAKE 交重同步;否则 WAIT(帧跨 DMA 块)。 + +### 5.2 `'}'` 结尾扫描 — 四川 MTC(`app_sc_mtc_proto.c:108-168`) + +``` +{ <命令 '1'~'9'/'A'/40~45> <参数…> } (变长,无长度字段、无 BCC) +0A 46 0A / 0A 46 0D (主机查询/清屏,定长 3) +``` + +probe 要点: +- 双帧族分流:首字节 `0x0A` → 只认 `46 0A/0D` 两种,其余 FAKE(还给 ETC);首字节 `'{'` → '}' 扫描族。 +- 命令字合法性预筛:`'1'~'9'` 或 `0x40~0x45`('A'=0x41 已含),否则 FAKE——**probe 不负责的字段合法性交给 parse**(如 0x41 双义 4B/5B 按帧长细分)。 +- 从下标 2 起扫 `'}'`,帧长 = '}' 下标 + 1;上限 `SC_MTC_PAYLOAD_MAX (74U)`(设备队列约束,协议文档上限 228 不采用);达到上限仍无 '}' → FAKE,否则 WAIT。 +- 教训:历史上按「无 BCC 20B / 带 BCC 21B」定长双长度认领导致 `{3 1 1234 } 3 }` 跨帧拼凑乱码,宿主推演脚本 `~/EnvTools/CD-DebugTool-cpp/scripts/probe_sim/sc_mtc_frame_sim.py`(`--old` 可复现旧逻辑)。**变长无长度字段的协议,必须扫定界符,不要猜定长**。 + +### 5.3 长度字段 — 四川治超(`app_sc_ol_proto.c:94-131`) + +``` +FF <亮度> <数据…> FF +``` + +probe 要点: +- 首字节 `0xFF` 快拒;第二字节即长度字段。 +- 长度下界 `SC_OL_FRAME_LEN_MIN`(7);`0xFE` 显式排除(见 4.2);上界由 uint8 天然约束。 +- `avail >= len` 后 peek 尾字节(`len-1`)必须 `0xFF` → READY(`*total_len = len_byte`)。 +- BCC(五字段异或,不含尾 FF)在 **parse** 校验:`_sc_ol_bcc_calc` 计算 `raw[0..len-3]` 异或对比 `raw[len-2]`(`app_sc_ol_proto_parse.c:19-25, 79-81`)。 + +选择模式:**有长度字段 → 5.3;有明确结束符 → 5.1/5.2;两者皆无 → 回到协议文档找隐含定界(BCC 位置、固定头部等),实在没有必须与上位机协商加定界**。 + +## 6. 调试定位 + +| 断点位置 | 文件:行 | 看到什么 | +|---|---|---| +| 通道任务 dispatch 调用行 | `app_rs485.c:73` / `app_rs232.c:83` | `rx_buf`/`rx_len`:DMA 收到的原始字节(帧是否到达、是否拆块) | +| 分发入口 | `app_dispatch.c:376`(`app_channel_dispatch`) | `ch->ch_id`、`proto` 掩码(该通道绑了谁) | +| 你的 probe 首行 | 模块 `app_xxx_proto.c` probe 函数(如 `app_sc_etc_proto.c:189`) | `avail`、首字节、返回状态——逐协议步进看链式流转 | +| 链式聚合决策 | `app_dispatch.c:328-336` | `any_wait`/`any_fake` 走向:卡住时看是不是「WAIT 停等」还是「跳 1 字节」 | +| handle_task parse 行 | `app_sc_etc_proto_parse.c:50` / `app_sc_mtc_proto_parse.c:48` / `app_sc_ol_proto_parse.c:64` | 帧内容与 parse 结果 | + +**RTT 十六进制 dump 建议**:在 probe READY 分支或 handle_task 收帧处加临时 `SEGGER_RTT_printf` 输出帧头 8 字节十六进制(`pl_rtt` 在 `hw_initcall` 就绪,任何上下文可用);联调时用 J-Link RTT Viewer 对照上位机发送记录。**提交前删除或注释临时 dump**。 + +**注意**:RS485 是半双工(RE 方向由 `dev_rs485` 注入),用 485 转 USB 工具抓线时确认 A/B 接法与收发使能。 \ No newline at end of file diff --git "a/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/03_\347\275\221\347\273\234\345\215\217\350\256\256\346\216\245\345\205\245_UDP\344\270\216TCP.md" "b/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/03_\347\275\221\347\273\234\345\215\217\350\256\256\346\216\245\345\205\245_UDP\344\270\216TCP.md" new file mode 100644 index 0000000..8ff8d2c --- /dev/null +++ "b/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/03_\347\275\221\347\273\234\345\215\217\350\256\256\346\216\245\345\205\245_UDP\344\270\216TCP.md" @@ -0,0 +1,61 @@ +# 03 网络协议接入(UDP 与 TCP) + +> 通用规则见 `01`。本文讲网口专项:RJ45 共享 RB、UDP/TCP 逻辑通道、LwIP 约束、与串口协议的混合规则。 + +--- + +## 1. RJ45 共享 RB 与逻辑通道 + +- 物理 RB 只有一个:`RB_SLOT_RJ45`,容量 `RB_SIZE_RJ45 (1536U)`(`app_dispatch.h:37`,注释:≥ IAP 最大帧 1044 + 余量)。 +- 四个逻辑通道共享该槽(`app_dispatch.h:47-55`): + +| CH_ID | 值 | 传输 | 通道实现 | +|---|---|---|---| +| `CH_ID_TCP_SERVER` | 2 | LwIP TCP 服务器 | `app_tcp_server.c`(默认端口 9528) | +| `CH_ID_TCP_CLIENT` | 3 | LwIP TCP 客户端 | `app_tcp_client.c`(默认 192.168.2.17:9529) | +| `CH_ID_UDP` | 4 | LwIP UDP | `app_udp.c`(端口 10011,创迪发现口 + IAP 共用) | +| `CH_ID_MQTT` | 5 | LwIP MQTT | `app_mqtt.c`(AH 平台,当前未激活) | + +- 协议侧提供 RB:`RB_PROVIDE_WEAK(rb_provide_rj45, RB_SIZE_RJ45)`(IAP `app_iap.c:20`、LDI `app_ldi.c:17`)。 +- **每逻辑通道独立 mask + bind**(doc/05-01 §2.3):LDI 一次 `ldi_module_init` 里 register 三次拿三个 mask,分别 bind TCP_SERVER/TCP_CLIENT/UDP(`app_ldi.c:135-157`),三 mask 共用 `g_ldi_msg_queue`。**禁止单 mask 绑多通道**。 +- 风险门禁(doc/05-01 §2.4):TCP 流 / UDP 报可能在同一 RJ45 RB 交错排列。以单网口业务为主时维持共享 1536;若 TCP 与 UDP 同时高压收发,评估拆逻辑 RB——新协议接入先按共享设计,超出预算再提案。 + +## 2. UDP 范例 + +### 2.1 IAP 升级协议(0x5A5A5A5A) + +- 帧格式:`0x5A5A5A5A | seq(4B) | cmd(4B) | len(4B) | data | CRC32(4B)`(`app_iap.c:5-7` 注释;`FRAME_HEAD (0x5A5A5A5AU)` 定义于 `app_iap.h:12`;`FRAME_MIN_LEN (5U)`、`FRAME_MAX_LEN (5U+256U)` :9-10)。 +- 只 bind `CH_ID_UDP`(`app_iap.c:39-53`);与 LDI 同槽链式共存:首字节 0x5A vs 0xFF 快拒互斥。 +- probe(`app_iap.c:96-155`)要点:首字节 `0x5A` 快拒 → 4B 帧头比对 `FRAME_HEAD` → len 合法性 ≤256 → 数据不足时**二次帧头扫描**(范围内出现另一 `0x5A` 立即 FAKE 重同步,防止伪头粘包)→ CRC32 校验 → READY。 +- **广播应答**:`cmd01/cmd02` 回复经 `udp->src_ip` 回传,源 IP 置全 `0xFF`(255.255.255.255 广播)(`app_iap_cmd.c:60-63`)。依赖 `udp_channel_t` 在收包时记录 `src_ip[4]`/`src_port`(`app_udp.c`)。**已知限制(2026-08-21)**:src 快照是通道级单例,帧排队期间后续帧会覆盖快照——IAP 停等一问一答、低流量,竞态不修改,仅记录(见 doc/07 修订)。 + +### 2.2 LDI UDP 搜索(21H/12H) + +- LDI 协议 bind UDP 通道用于「创迪发现口」搜索/应答(`app_udp.c` 注释:同端口 10011 承载 LDI 21H/12H 搜索与 IAP 升级,按帧头分流)。**注意区分两个端口语义**:10011 仅为搜索/上报的 UDP 传输渠道;12H 应答与 IAP 0x01 上报内容中的 `port` 字段是设备「配置功能端口」(TCP 业务口 9528,见 doc/07 §12 ④)。LDI probe 首字节 `0xFF` 快拒 + STX `0xFF 0xFF` + CRC16-XMODEM(`app_ldi.c:313-373`)。 +- **12H 应答发送顺序(2026-08-21)**:广播为主(`app_udp_broadcast` 先发,复用常驻 conn 不占 netconn 池)、回源为辅(`channel_send` 后发、失败静默)——回源依赖通道级 src 快照,多协议交错下可能发错目标,广播是可靠路径。 + +### 2.3 UDP 通道实现要点(新协议发包侧) + +- 应答直接 `channel_send(ch, data, len)`:`udp_ch_send` 用 `udp->src_ip` 重建 `ip_addr_t` 发回源地址(`app_udp.c`)——**通道层自动回源,协议层无需管理 IP**。 +- 通道生命周期:`udp_task` 建 netconn/bind/派生 `udp_connect_task`;断链时 `udp_channel_deinit` 置 `ops=nullptr`、`state=DOWN`、`conn=NULL`(2026-08-21 补)、`app_channel_register(CH_ID_UDP, nullptr)`。协议层不要持有裸 `channel_t *` 跨断链使用——`channel_send` 对 `ops==nullptr` 有守卫(`app_dispatch.c`),但状态判断应像 LDI 那样经 `app_channel_get(CH_ID_*)` 每次现查。 + +## 3. TCP 范例(LDI 双通道) + +- LDI 同时注册 TCP Server + TCP Client + UDP 三 mask(`app_ldi.c:140-156`)。 +- **通道状态**:`channel_t.state`(`CH_STATE_UP/DOWN`,`app_dispatch.h:59-61`)。TCP 连接建立时 `tcp_channel_init` 置 `CH_STATE_UP` + `app_channel_register`(`app_tcp_server.c:92-98`、`app_tcp_client.c:57-64`);断开时 conn 任务置 `ops=nullptr`、`state=DOWN`、`app_channel_register(..., nullptr)`(`app_tcp_server.c:158-161`、`app_tcp_client.c:163-166`)。 +- **断链状态机复位先例**:`ldi_timer_task` 每秒检查 `ch == nullptr || ch->state != CH_STATE_UP` → `g_ldi.state = LDI_ST_UNINIT`(`app_ldi.c:406-419`),断链后认证/上报状态自动回退。新协议若有会话状态机,必须设计等价的断链复位路径。 +- TCP 客户端非阻塞 connect + 4s 轮询(`app_tcp_client.c:87-112`),keepalive idle 10s / intvl 2s / cnt 3(:148-153)——新协议用 TCP Client 模式可直接复用通道,不改 `app_tcp_client.c`。 +- 若新协议需要新端口/新远端:用 `app_tcp_server_set_port()` / `app_tcp_client_set_remote(ip, port)`(后者触发断链信号量重连,`app_tcp_client.c:48-53`);两个通道实例是单例,**多远端需求需要评估扩通道,不在接入范围内**。 + +## 4. LwIP 约束 + +- **广播依赖**:`lwipopts.h` 中 `LWIP_BROADCAST` 未定义,但 `IP_SOF_BROADCAST=1` + `IP_SOF_BROADCAST_RECV=1` 已生效(IAP 升级依赖,见 doc/CLAUDE.md「LwIP 配置要点」);`MEMP_NUM_NETCONN=8`/`MEMP_NUM_UDP_PCB=8`(2026-08-21 覆盖,修复池满广播丢包)。代码侧广播:`udp_task`/`udp_cq_task` 建常驻 conn 时 `ip_set_option(conn->pcb.udp, SOF_BROADCAST)`;`app_udp_broadcast`/`app_udp_cq_broadcast` **复用常驻通道 conn**(`app_channel_get` 回验 + conn 非空)`netconn_sendto` 255.255.255.255,常驻未就绪回退临时 conn。新协议需要广播时调用 `app_udp_broadcast`(10011 口)或 `app_udp_cq_broadcast`(20103 口),勿自行 `netconn_new` 临时 conn。 +- **`pl_net_adapt.h` 禁止在任何 .h 中引用**(doc/CLAUDE.md 架构约束):LwIP 类型(`struct netconn`、`ip_addr_t` 等)不得泄漏进协议头文件。协议 .c 需要 LwIP API 时只 include `pl_net.h`/`pl_net_adapt.h` 于 .c 内。 +- **IP 类型隔离先例**:`udp_channel_t.src_ip` 用 `uint8_t[4]` 字节数组而非 `ip_addr_t`(`app_udp.c:160-164`),避免 Application 层暴露 middleware 类型。新协议存储对端地址照此办理。 + +## 5. 网络协议与串口协议混合规则 + +- **按槽绑定**:协议绑哪个 CH_ID 就吃哪个物理口的数据(RJ45 槽 / RS485 槽 / RS232 槽互不相通)。同一协议可同时绑网口和串口(如青海绑 RS485+RS232,LDI 绑三网口逻辑通道)——每通道一个 mask,probe 对 `ch` 参数通常不区分(除应答回源)。 +- **同槽链式 probe**:RJ45 上 IAP/LDI 靠首字节(0x5A vs 0xFF)共存;RS485 上多地区协议靠帧头/第二字节共存(见 `02 §4`)。跨槽协议永远不存在 probe 竞争。 +- **禁绑**:`CH_ID_RS232_1` 对任何协议(含网络协议)禁绑。 +- 混合部署时注意 queue 深度与 payload 上限按**本协议绑定通道的最小 RB** 设计(如同时绑 RS485+RS232 时按 768 约束,绑 RJ45 时按 1536 约束)。 \ No newline at end of file diff --git "a/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/04_\346\216\245\345\205\245\346\243\200\346\237\245\346\270\205\345\215\225.md" "b/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/04_\346\216\245\345\205\245\346\243\200\346\237\245\346\270\205\345\215\225.md" new file mode 100644 index 0000000..4ea6c80 --- /dev/null +++ "b/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/04_\346\216\245\345\205\245\346\243\200\346\237\245\346\270\205\345\215\225.md" @@ -0,0 +1,119 @@ +# 04 接入检查清单(step-by-step) + +> 新协议接入手把手指南。每步标注「改哪些文件 / 验证什么」。完成后对照本清单自检再提交。 + +--- + +## Step 0 — 协议事实收集 + +在动代码之前,先写清(建议直接写进模块头文件注释): + +1. 帧格式全貌:帧头、长度字段(有无/字节序/范围)、命令字、数据段、校验(CRC/BCC/无)、帧尾/定界符。 +2. 定界模式归类:定长 / 结束符定界(`0x0D`、`'}'` 类)/ 长度字段(见 `02 §5`)。 +3. 传输通道:RS485 / RS232 / UDP / TCP(Server 还是 Client)/ 波特率是否有命令可改。 +4. 应答约定:哪些帧「收到即回」、应答帧格式、错误码。 +5. 帧头是否与既有协议重叠 → 先查 `doc/05-01 §6` 兼容矩阵,判定共存策略(互斥快拒 / 第二字节区分 / EIDE 排除)。 + +## Step 1 — 建目录 + 骨架文件 + +**改哪些文件**: + +``` +Application/Inc/ProtocolParser_XXX/app_xxx_proto.h +Application/Inc/ProtocolParser_XXX/app_xxx_proto_parse.h +Application/Inc/ProtocolParser_XXX/app_xxx_proto_cmd.h +Application/Inc/ProtocolParser_XXX/app_xxx_proto_voice.h (可选) +Application/Src/ProtocolParser_XXX/app_xxx_proto.c +Application/Src/ProtocolParser_XXX/app_xxx_proto_parse.c +Application/Src/ProtocolParser_XXX/app_xxx_proto_cmd.c +Application/Src/ProtocolParser_XXX/app_xxx_proto_voice.c (可选) +``` + +- `proto.c`:五步注册 + probe + handle_task(模板见 `01 §2`,抄 `app_sc_etc_proto.c` 结构)。 +- `proto_parse.c`:`xxx_parse_frame` 纯函数 + `xxx_parsed_cmd_t`。 +- `proto_cmd.c`:命令执行 + 应答发送(`channel_send`)。 + +**Makefile 收录**: +- `Makefile:35` `INC_DIRS` 加 `-I Application/Inc/ProtocolParser_XXX`。 +- `Makefile:290` `SRC_APPLICATION` 加四个 .c(与 QH/SC 相邻,行 :310-322 为范例)。 + +**EIDE 收录**(`.eide/eide.yml`): +- `targets.Debug.cppPreprocessAttrs.incList` 加 `Application/Inc/ProtocolParser_XXX`(现列表末尾 :561-567 为范例)。 +- `virtualFolder.Application.protocol` 下新增文件夹节点,逐文件列 path(抄 `ProtocolParser_SiChuang_ETC` 节点结构,:88-96)。 + +**验证**:两套构建都编入新目录(`make -j8` 能看到新 .c 被编译)。 + +## Step 2 — probe 实现与宿主推演 + +1. 按 `01 §3` 写 probe:首字节快拒 → 头部字段预筛 → 长度/定界扫描 → 尾字节/校验 → 仅 READY/SKIP 写 `*total_len`。 +2. **宿主仿真推演**(强烈建议):仿 `~/EnvTools/CD-DebugTool-cpp/scripts/probe_sim/sc_mtc_frame_sim.py` 写一个纯 Python 镜像 probe/parse 的脚本——喂边界用例(半帧、粘包、伪头、超长、乱字节流)验证状态机流转。该脚本是 MTC '}' 定界改造的直接工具(`--old` 可复现旧 bug),先例意义重大。另参考 `~/EnvTools/CD-DebugTool-cpp/scripts/probe_sim/review_probe_sim.py`。 +3. 边界用例至少覆盖: + - 空缓冲(→ FAKE) + - 首字节错误(→ FAKE) + - 帧头正确但半帧(→ WAIT) + - 完整帧(→ READY,total_len 正确) + - 超长无定界(→ FAKE 重同步) + - 两帧粘包连续到达(→ READY 两次) + +## Step 3 — 注册与队列 + +1. 五步注册(`01 §2`),队列静态分配 + `_Static_assert(payload ≤ RB 容量)`(`01 §4`)。 +2. 任务栈 `256*4`、优先级 `osPriorityNormal`、帧缓冲 `static`。 +3. 队列深度定 2~4 并写明理由(一包多帧粘包 → 深;停等应答 → 浅)。 + +## Step 4 — 构建与内存核算 + +1. `make -j8` **全量零告警**(`-Wall -Wextra`;`[[maybe_unused]]` 用于临时未用函数)。 +2. `arm-none-eabi-size build/Debug/Project_STD.elf` 对比接入前后: + - 新增静态量 = 队列体(`DEPTH × (8+PAYLOAD)`)+ RB 体(若该槽原来无提供者)+ 状态变量。 + - 新增任务栈 1KB/个(从 ucHeap,不计 .bss)。 + - **口径警告**:`doc/06-04` 是 EIDE Debug 采样账(编 1-577,排除 1-260/1-969/RLS/AH),**勿用 Makefile 构建产物重采样覆盖 06-04 全表**(06-04 §0 明示)。核算增量用 Makefile 产物,登记时标注口径。 +3. 增量超预算(SRAM 空闲仅 ~10KB)→ 回到 Step 3 减 payload/深度,或提案换槽。 + +## Step 5 — 文档同步(必须) + +| 文档 | 位置 | 动作 | +|---|---|---| +| `doc/05-01 §3.3` | :115 | 帧头事实表加一行 | +| `doc/05-01 §6` | :190 | 兼容矩阵加组合判定 | +| `doc/05-01 §4.1` | :146 | queue 深度表加一行 | +| `doc/06-04` | 协议帧 queue 表 | 新增队列静态体大小(标注构建口径) | +| `doc/CLAUDE.md` | 「Application 模块全景」/协议小节 | 新协议模块一行 + 关键要点 | +| 本栏目 README | — | 无需改(如规则有增补再改 `01`) | + +## Step 6 — EIDE 选编互斥核对 + +- 打开 `.eide/eide.yml` `excludeList`:确认新协议与冲突协议的目录互斥关系正确('{' 冲突 → 青海与 MTC 必须二选一;'FF' 冲突 → 治超/RLS 按兼容矩阵判定)。 +- 确认 EIDE Debug 的「编入组合」与 Makefile「全量组合」都在 Step 4 各自构建验证过。 +- 新协议若依赖 EIDE 排除的既有模块(如语音板相关功能),在 PR 描述中注明。 + +## Step 7 — 实机联调 + +1. **断点路径**(`02 §6` 表):通道任务 dispatch 行 → `app_channel_dispatch` → 自己的 probe → 聚合决策行(`app_dispatch.c:328-336`)→ handle_task parse 行。 +2. **RTT 十六进制 dump**:probe READY 分支或 handle_task 收帧处临时 `SEGGER_RTT_printf` 打印帧头 8B hex,与上位机发帧记录逐字节比对;联调完删除。 +3. **回执测试帧样例**(发给设备,观察渲染/应答): + +| 协议 | 测试帧(十六进制) | 预期 | +|---|---|---| +| 四川 ETC 全屏 | `0A 00 00 B2 E2 CA D4 0D`(静态显示/行号0全屏/"测试" GBK) | 全屏显示"测试",回 `0A 00 0D` | +| 四川 MTC 单行 | `{3 1 1234 } 3 }`(10B,'}' 定界变长,`sc_mtc_frame_sim.py` 文档示例) | 单行渲染,'}' 正确定界不乱码 | +| 四川治超 清屏 | `FF 07 94 00 00 6C FF`(len=7 最小帧:cmd=94 清屏、亮度 00、BCC=FF^07^94^00^00=6C;BCC 算法见 `app_sc_ol_proto_parse.c:19-25`) | 清屏,回 16B/行应答或状态回显 | +| IAP 心跳/测试 | `5A5A5A5A 00000000 00000000 00000000 `(cmd=00 Test,len=0;CRC 覆盖前 16B) | 协议层应答(cmd01 广播回源 IP 全 FF) | + + > 治超/IAP 样例的 BCC/CRC 值请用上位机工具计算确认后使用;帧结构以协议文档为准,本表只做联调起步。 +4. **粘包/拆包验证**:上位机连发两帧(无间隔)与单帧拆两半各发一次,确认 probe WAIT/READY 流转正确。 +5. **断链/重连验证**(网络协议):拔网线 → 确认通道 DOWN、状态机复位(LDI 回 UNINIT 先例,`app_ldi.c:418-419`)→ 恢复后协议自愈。 + +## Step 8 — 提交前自检 + +- [ ] probe 纯函数(无 rb_read/skip、无全局写) +- [ ] `avail==0` → FAKE;首字节快拒 +- [ ] 仅 READY/SKIP 写 `*total_len` +- [ ] 长度上下界 + 尾字节/校验 +- [ ] 静态队列 + static_assert(payload ≤ RB) +- [ ] 任务栈 256×4,无 `CH_ID_RS232_1` bind +- [ ] Makefile + eide.yml 双收录 +- [ ] make 零告警,size 增量已核算并登记 +- [ ] doc/05-01(§3.3/§6/§4.1)、doc/06-04、doc/CLAUDE.md 已更新 +- [ ] 临时调试 dump 已删除;注释未引用外部参考工程 +- [ ] 烧录纪律:烧完主固件后擦除 Sector1(见 doc/CLAUDE.md「调试期烧录陷阱」),否则 Bootloader 判 App 损坏进 Recovery \ No newline at end of file diff --git "a/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/README.md" "b/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/README.md" new file mode 100644 index 0000000..6068ce1 --- /dev/null +++ "b/doc/08_\345\215\217\350\256\256\346\250\241\345\235\227\346\216\245\345\205\245\350\247\204\345\210\231/README.md" @@ -0,0 +1,61 @@ +# doc/08 协议模块接入规则、指导 + +> **状态**:现行 | 2026-08-17 新建 +> **角色**:新协议接入本固件的**操作性权威指引**(规则、骨架、串口/网络专项、检查清单) +> **上游权威**:RB 行为/绑定矩阵/兼容矩阵 → `doc/05`;内存占用账 → `doc/06` + +--- + +## 1. 目的 + +本栏目回答「**我要给这台费显接一个新协议,从哪开始、每一步改什么、怎么验证**」。 +所有内容均以本仓库现有代码为事实依据(范例:青海 / 四川 ETC / 四川 MTC / 四川治超 / 山东 / 贵州 / RLS / IAP / LDI), +不是通用嵌入式理论。文档中的宏名、API 签名、行号均为写实引用。 + +## 2. 适用读者 + +- 为本固件编写地区费显协议(RS485/RS232)或网络协议(UDP/TCP)的开发者 +- 评审新协议模块接入 PR 的维护者 +- 需要核对「现有模块为什么这样写」的任何人 + +## 3. 文档结构 + +| 文件 | 内容 | +|---|---| +| `01_通用规则与模块骨架.md` | 目录/命名/文件拆分、注册五步、probe PURE 契约、帧队列、执行层、内存纪律、文档义务 | +| `02_串口协议接入.md` | RS485/RS232 通道与 RB、波特率(DIP1)、接收路径、同槽共存判别、三种变长定界模式、调试定位 | +| `03_网络协议接入_UDP与TCP.md` | RJ45 共享 RB、UDP 范例(IAP/LDI 搜索)、TCP 范例(LDI 双通道)、LwIP 约束、串口/网络混合规则 | +| `04_接入检查清单.md` | 从建目录到实机联调的 step-by-step checklist,含测试帧样例 | + +## 4. 快速规则摘要(先记住这 10 条) + +1. **目录即模块**:`Application/Src|Inc/ProtocolParser_XXX/`,前缀 `app_xxx_`,静态函数 `_` 前缀。 +2. **注册五步**:`RB_PROVIDE_WEAK` → `app_proto_acquire_buf` → `app_proto_register`(返回 `proto_mask_t`)→ `app_proto_bind_channel` → `app_proto_set_frame_queue`,再 `sw_app_initcall` + 建任务(栈 `256*4`)。 +3. **probe 是纯函数**:只 `rb_peek`,无副作用;`avail==0` 必须返回 `PROTO_PROBE_FAKE`;首字节不匹配立即 FAKE(快拒)。 +4. **仅 READY/SKIP 写 `*total_len`**;长度必须有上下界、尾字节必须校验。 +5. **链式聚合**:同 RB 上 WAIT/FAKE 继续试下一协议;一轮无一消费时 `any_wait` 停等、否则跳 1 字节重同步(`app_dispatch.c:328-336`)。 +6. **帧队列静态分配**(`StaticQueue_t`),深度惯例 2~4;payload 上限必须 ≤ 对应 RB 容量并用 `_Static_assert` 锁死。 +7. **一个物理口一个 RB**:RJ45 1536 / RS485 768 / RS232 768;网口逻辑通道共享 RJ45 槽。 +8. **`CH_ID_RS232_1`(USART6)语音 TX 专用,禁止任何协议 bind**。 +9. **内存纪律**:静态队列不占 ucHeap;任务栈(256×4=1KB/个)从 ucHeap 支出;新增静态内存必须同步 `doc/06-04` 占用账。 +10. **文档义务**:新帧头登记 `doc/05-01 §3.3`、冲突组合登记 §6 兼容矩阵、queue 深度登记 §4.1;代码注释不得引用外部参考工程。 + +## 5. 与 doc/05、doc/06 的分工 + +| 主题 | 权威在哪里 | 本栏目的角色 | +|---|---|---| +| RB 容量、槽位、绑定矩阵、weak 合并语义 | `doc/05_协议模块多协议兼容优化/01_architecture.md`(RB 行为权威) | 引用,不复制全量;只写「接入时怎么用它」 | +| 帧头事实表(§3.3)、兼容矩阵(§6)、queue 深度表(§4.1) | doc/05-01 | 新协议接入后**必须回来更新**,本栏目只指路 | +| SRAM/CCMRAM 占用账 | `doc/06_SRAM内部分数据迁移/04_current_memory_occupancy.md`(唯一占用实测账本) | 接入新增内存后**必须回来更新** | +| 接入操作步骤、probe 编写约束、构建收录、调试定位 | **本栏目(doc/08)** | 唯一权威 | + +原则:**doc/05、doc/06 记「是什么/现状」,doc/08 记「怎么做」**。三者都改时,先改 08 的步骤,再按步骤更新 05/06 的事实表。 + +## 6. 关键代码入口速查 + +| 事实 | 位置 | +|---|---| +| 调度框架全部类型与 API | `Application/Inc/app_dispatch.h`(RB 尺寸 :37-39;通道 ID :47-55;probe 状态 :65-70;注册 API :118-124) | +| 链式探测聚合引擎 | `Application/Src/app_dispatch.c`(`frame_dispatch_task` :226;聚合决策 :328-336;`app_channel_dispatch` :376) | +| RB 宏与结构 | `Kernel/Inc/ring_buffer.h`(`RB_PROVIDE_WEAK` :40;`rb_init` :58) | +| 模块模板权威 | `doc/05_协议模块多协议兼容优化/01_architecture.md §5`(:168) | \ No newline at end of file diff --git "a/doc/09_\345\261\261\344\270\234\350\264\271\346\230\276\345\215\217\350\256\256/README.md" "b/doc/09_\345\261\261\344\270\234\350\264\271\346\230\276\345\215\217\350\256\256/README.md" new file mode 100644 index 0000000..82bb291 --- /dev/null +++ "b/doc/09_\345\261\261\344\270\234\350\264\271\346\230\276\345\215\217\350\256\256/README.md" @@ -0,0 +1,46 @@ +# 09 山东费显协议 + +> **协议来源**:`39山东车道费额显示器通信协议.docx`(创迪科技生产订单与技术资料,2020-07-20) +> **模块**:`Application/Src/ProtocolParser_ShanDong/`(`app_sd_proto.c` / `app_sd_proto_parse.c` / `app_sd_proto_cmd.c` / `app_sd_proto_default.c`) +> **权威细节**:帧头登记 / 兼容矩阵 / queue 深度 → `doc/05`;内存账 → `doc/06-04 §6`;接入规则 → `doc/08` + +## 1. 协议事实 + +- 串口通信:9600~115200bps(默认 9600)、8N1 无校验。绑定 RS485(USART1)+ RS232(USART3)双通道。 +- 帧格式:`'{'`(0x7B) + 命令字(1B ASCII) + 参数长度(1B 二进制) + 参数(变长) + `'}'`(0x7D),无 BCC/CRC。 +- 目标屏幕:192×96(FONT_16 可容 6 行,协议行号 1~5 全覆盖)。 + +| 命令字 | 功能 | 参数 | 说明 | +|--------|------|------|------| +| `'1'` | 全屏单色 | `01红/02绿/03黄` | 文档原文「02-全屏黄」为笔误,按 03=黄实现 | +| `'2'` | 获取版本号 | `00` | 协议未定义应答格式 → 裸 ASCII 应答 PROGRAM_CODE | +| `'3'` | 单行任意显示 | 颜色 `'0'~'2'` + 行号 `'1'~'5'` + GBK 文本 | 先清行再渲染;文档颜色字段「0x30~0x31」为笔误,含 `'2'`=黄 | +| `'4'` | 全屏可编辑显示 | 颜色 + X 坐标 + Y 坐标 + 文本 | 先清屏后 word_wrap;0x0A 回车由渲染引擎换行、0x0D 被跳过 | +| `'5'` | 全屏清除 | 无 | | +| `'7'` | 亮度设定 | `'0'~'5'` | 0=自动调光(恢复光敏任务);1~5=硬件档 {3,4,5,7,8} | +| `'8'` | 外设控制 | bit0 绿灯 / bit1 红灯 / bit2 黄闪报警 | 红灯优先(PD14 单灯互斥);PD15 黄闪 | + +无 `'6'` 命令。除 `'2'` 外协议未定义上行应答 → 单向执行不回。 + +上电画面(`app_sd_proto_default.c`,2026-08-17):经 `app_default_display_register` 注册为默认显示回调,清屏后居中渲染「山东省 高速公路 欢迎您」(FONT_16 **黄字**,UTF-8 字面量 + `FONT_ENC_UTF8`,app_render 内部转 GBK 取字模)。 + +## 2. 已裁决歧义(2026-08-17) + +| 歧义 | 裁决 | +|------|------| +| 文档第 6/7 条写作「7B 01/02 …」与字段2表「ASCII '1'~'A'」矛盾 | 按 ASCII `'1'`(0x31)/`'2'`(0x32) 实现(与字段2表一致) | +| 物理通道 | RS485 + RS232 双通道(对齐青海/四川模式) | +| 屏幕分辨率 | 192×96 | +| 版本号应答 | 裸 ASCII 应答产品程序编码 PROGRAM_CODE(`9K10212482`) | + +注:`PROGRAM_CODE`(`app_boot.h` 常量)同时由主固件启动时落库 Sector1 `app_info.version`(供 IAP 0x03「上报固件状态」返回版本号,见 `doc/07` §13);本协议 `'2'` 命令仍直接应答编译期常量,不读 Sector1。 + +## 3. 帧头冲突纪律(与青海/四川MTC 互斥) + +山东与青海帧完全同构(`{`+cmd+len+`}`),命令字 `'1'~'5','7','8'` 全部落入青海 probe 命令集: + +- **全协议 Makefile 构建**:青海 probe 先注册(源码收录序 `qh_proto_init` 在前)先认领山东帧。`'3'/'4'/'5'` 语义与青海巧合一致(行为等价);`'1'/'2'/'7'/'8'` 语义分歧(山东 '1'=全屏单色 vs 青海 '1'=主机查询等)。 +- **量产**:EIDE 目录排除纪律——山东项目须排除 `ProtocolParser_QingHai` + `ProtocolParser_SiChuang_MTC`(`{` 帧族二选一);当前 `.eide/eide.yml` Debug 目标(贵州现行)已排除山东目录。 +- **编译期互斥守卫**:山东主文件携带 `g_brace_proto_guard`(`#ifndef STD_ALL_PROTO` 包裹、`__attribute__((used))`)——EIDE 量产构建多个 `{` 帧族编入即链接报 `multiple definition`,无法绕过;Makefile 全协议开发构建经 `-DSTD_ALL_PROTO` 豁免共存(probe 注册序纪律约束)。 + +详见 `doc/05_协议模块多协议兼容优化/01_architecture.md` §3.3 / §6。 \ No newline at end of file diff --git "a/doc/10_\350\264\265\345\267\236\350\264\271\346\230\276\345\215\217\350\256\256/README.md" "b/doc/10_\350\264\265\345\267\236\350\264\271\346\230\276\345\215\217\350\256\256/README.md" new file mode 100644 index 0000000..fcca7a1 --- /dev/null +++ "b/doc/10_\350\264\265\345\267\236\350\264\271\346\230\276\345\215\217\350\256\256/README.md" @@ -0,0 +1,71 @@ +# 10 贵州费显协议 + +> 贵州常规费显协议(协议文档 06 贵州常规费显协议,2020-06-17 修订)接入记录。 +> 模块:`Application/Src/ProtocolParser_GuiZhou/` + `Application/Inc/ProtocolParser_GuiZhou/`(前缀 `gz_`)。 + +## 1. 协议事实(13 命令) + +帧格式:`'{'`(0x7B) + 命令字(1B) + 参数长度(1B 二进制) + 参数(变长) + `'}'`(0x7D),**无校验**。帧总长 = len + 4,len 为二进制字节值(非 ASCII)。 + +| 命令字 | 名称 | 参数约束(最终实现) | 行为 | +|---|---|---|---| +| `'1'`(0x31) | 主机查询 | len==0 | 回「正常」应答 `7B 31 01 00 7D` 至**源通道**(仅一种应答) | +| `'2'`(0x32) | 自检 | len==0 | 黄色全屏填充 + 语音「系统正在加电自检」 | +| `'3'`(0x33) | 单行显示 | len 3~18;color '0'~'2'(红/绿/黄);row '1'~'5'(→0~4) | FONT16 渲染,先清行再渲染,GBK 直通 | +| `'4'`(0x34) | 全屏可编辑 | len 4~86;color '0'~'2';x/y 1B | 按 **x/y 参数坐标**渲染(word_wrap=true);len>71 文本截断 64 | +| `'5'`(0x35) | 清屏 | len==0 | 整屏黑 + commit | +| `'6'`(0x36) | 固定格式 | len 3~90;color '0'~'2';x1 '0'客/'1'货;`'|'` 分隔 5 字段 | 见 §2 行文;金额 ≥0.5 元同步播报费额语音 | +| `'7'`(0x37) | 礼貌用语语音 | len==1;idx '0'~'3' | 4 条文明用语 → TTS | +| `'8'`(0x38) | 亮度 | len==1;'0'~'5' | 0=恢复光敏任务;1~5=挂起光敏 + 硬件 {3,4,5,7,8} | +| `'9'`(0x39) | 音量 | len==1;'1'~'5' | 语音板音量 {1,3,5,7,9} | +| `'A'`(0x41) | 外设 | len==1;bit0 绿/bit1 红/bit2 黄闪 | **红优先**:红→车道灯红;否则绿→车道灯绿;黄闪独立 | +| `'B'`(0x42) | 费额语音 | len≥1;金额 ASCII 串(整数/小数) | 金额 ≥0.5 元播报「您好请交费X元」;**不显示任何内容** | +| `0x01` | 全屏点亮(二进制命令字) | len==1;01红/02绿/**03黄** | 整屏 `dev_display_fill` + commit | +| `0x02` | 版本号(二进制命令字) | len==1 | 串口回裸 ASCII `PROGRAM_CODE` + 屏幕红字「版本:PROGRAM_CODE」 | + +金额/重量运算全部**整数运算**(分/吨分),无浮点:`gz_amount_to_fen`(parse.c)整数部分×100 + 小数前两位(`%.2f` 语义,第三位起截断)。 + +## 2. '6' 固定格式显示行文(裸机行文对齐) + +参数:x0=color、x1=客/货,随后 5 字段 `车型|金额|余额|信息1|信息2`(客车)或 `车型|金额|余额|总重|超重`(货车)。 + +- **客车**: + - 行0 `车型: %u 型`(如「车型: 1 型」) + - 行1 `金额:%u.%02u 元` + - 行2 `余额:%u.%02u 元` + - 行3 = 信息1 原文(GBK 直通;空行或首字节空格 → 该行不显示) + - 行4 = 信息2 **不显示**(按设备实测行为) +- **货车**: + - 有超重(≥0.1 吨):行0 金额 / 行1 余额 / 行2 `总重:%u.%02u 吨` / 行3 `超重:%u.%02u 吨` + - 无超重:行0 车型 / 行1 金额 / 行2 余额 / 行3 总重 + - 总重或超重 <0.1 吨 → 该行不显示(空行) +- 语音:金额 ≥0.5 元 → 「您好请交费X元」(X 保留两位小数,如 1.20→「1.20元」;.00 只播整数)。 + +## 3. 已裁决差异表(协议文档 vs 设备实测 vs 最终实现) + +| 命令 | 协议文档原文 | 设备实测行为 | 最终实现(裁决) | +|---|---|---|---| +| '1' 主机查询 | 正常/异常两种应答 | 经固定串口回「正常」帧 | 仅回「正常」帧,回**源通道**(不沿袭固定串口) | +| '2' 自检 | 固定汉字信息及数字 1~9 交替显示 | 老化模式 + 自检语音 | 黄色全屏 + 语音「系统正在加电自检」 | +| '3' 单行 | 未写长度边界 | len≤2 或 len≥19 丢弃 | len 3~18 | +| '4' 全屏 | x/y 按坐标渲染 | 校验 x≤168/y≤72 后**恒按 (0,0)** 渲染;len≤3 或 len>87 丢弃;len>71 截断 64 | **按 x/y 坐标渲染**(青海先例,word_wrap=true);len 4~86;len>71 截断 64 | +| '6' 固定格式 | 全角冒号「车型: X型」、信息2 显示于第 5 行 | 半角冒号行文、信息2 不显示 | **裸机行文对齐**(见 §2) | +| '7' 礼貌用语 '0' | 「您好!欢迎行驶贵州公路」 | 「您好!欢迎行驶贵州高速公路」 | 用实测文本 | +| '9' 音量 | 1~5 档线性 | '1'~'5' → 语音板音量 1/3/5/7/9 | 映射 {1,3,5,7,9} | +| 'A' 外设 | bit0 绿/bit1 红/bit2 黄闪 | 绿优先(先判 bit0) | **红优先**(`if(red)…else if(green)`) | +| 0x01 全屏点亮 | 01红/02绿/03黄 | 03=蓝 | **03=黄**(按协议文档) | +| 0x02 版本号 | 返回版本号字符串 | 串口回「版本:GZ_FX_P5_V13」+ 屏幕红字显示 | 串口回裸 ASCII `PROGRAM_CODE` + 屏幕红字「版本:PROGRAM_CODE」(对齐实测的附加行为) | +| 上电画面 | 上电显示并播报「祝您一路平安」 | — | **不建 default 文件**(裁决) | + +## 4. 帧头冲突纪律 + +- 贵州 ASCII 命令集 `'1'~'9','A','B'` 与青海 probe 命令集**完全重叠**(同 `{` 帧族、len 字段同构):全协议构建下青海 probe 先注册(源码收录序 `qh_proto_init` 在前)先认领,贵州 probe 仅承接青海 FAKE 的帧。 +- **量产 EIDE 目录排除纪律**:贵州与青海/山东/四川MTC 互斥(`.eide/eide.yml` Debug 目标已启用贵州、排除青海/山东/四川MTC;其他量产目标须启用本协议并排除其余 `{` 帧族)。 +- **编译期互斥守卫**:贵州主文件携带 `g_brace_proto_guard`(`#ifndef STD_ALL_PROTO` 包裹、`__attribute__((used))`)——EIDE 量产构建多个 `{` 帧族编入即链接报 `multiple definition`,无法绕过;Makefile 全协议开发构建经 `-DSTD_ALL_PROTO` 豁免共存(probe 注册序纪律约束)。 +- 0x01/0x02 二进制命令字不属于青海/山东/四川MTC 的 ASCII 命令集,各 probe 首命令字快拒 → 贵州 probe 可无冲突认领。 + +## 5. 遗留项 + +- 0x02 屏幕红字显示版本为对齐设备实测行为的**附加行为**(协议文档仅要求返回版本号),沿用 `PROGRAM_CODE`(`app_boot.h`)常量。 +- 版本号落库:`PROGRAM_CODE` 同时由主固件启动时写入 Sector1 `app_info.version`(供 IAP 0x03 上报版本号,见 `doc/07` §13);0x02 命令本身仍直接应答编译期常量,不读 Sector1。 +- '6' 固定格式行文未实现协议文档「信息1/信息2 超 12 字符自动左右滚屏」。 \ No newline at end of file diff --git "a/doc/11_\344\272\221\345\215\227\350\264\271\346\230\276\345\215\217\350\256\256/README.md" "b/doc/11_\344\272\221\345\215\227\350\264\271\346\230\276\345\215\217\350\256\256/README.md" new file mode 100644 index 0000000..95391ca --- /dev/null +++ "b/doc/11_\344\272\221\345\215\227\350\264\271\346\230\276\345\215\217\350\256\256/README.md" @@ -0,0 +1,114 @@ +# 11 云南费显协议 + +> 云南常规费显协议(01 云南常规费显协议-云南LED费显P5,2022.7.5,云南弥玉项目 2022-S134)接入记录。 +> 模块:`Application/Src/ProtocolParser_YunNan/` + `Application/Inc/ProtocolParser_YunNan/`(前缀 `yn_`)。 + +## 1. 协议事实(13 命令) + +帧格式:`'{'`(0x7B) + 命令字(1B) + 参数长度(1B 二进制) + 参数(变长) + `'}'`(0x7D),**无校验**。帧总长 = len + 4,len 为二进制字节值(非 ASCII)。传输:串口(RS232/RS485),9600~115200bps(默认 9600,8N1,本设备由 DIP1 统一选择 9600/115200)。 + +| 命令字 | 名称 | 参数约束(最终实现) | 行为 | +|---|---|---|---| +| `'1'`(0x31) | 主机查询 | len==0 | 回「正常」应答 `7B 31 01 00 7D` 至**源通道**(恒回正常,仅一种应答) | +| `'2'`(0x32) | 自检 | len==0 | **老化循环显示**(复用出厂测试老化序列)+ 每 5s 语音「系统正在自检」;可被下一帧命令打断 | +| `'3'`(0x33) | 单行显示 | len 3~18;color '0'~'2'(红/绿/黄);row '1'~'5'(→0~4) | **FONT_24** 渲染(协议 24 点阵),先清行再渲染,GBK 直通,超宽截断;行 5「执行但不落屏」 | +| `'4'`(0x34) | 全屏可编辑 | len 4~86;color '0'~'2';x/y 1B | 先清屏,按 **X/Y 参数坐标**渲染(word_wrap=true,FONT_24);0x0A 换行、0x0D 跳过 | +| `'5'`(0x35) | 全屏清除 | len==0 | 整屏黑 + commit | +| `'6'`(0x36) | 单行清除 | len==1;row '1'~'5' | 清第 row 行(行高 24px);行 5 同「执行但不落屏」 | +| `'7'`(0x37) | 礼貌用语语音 | len==1;idx '0'~'3' | 4 条文明用语 → TTS(协议原文文本) | +| `'8'`(0x38) | 亮度 | len==1;**0x00=NUL=自动亮度** / ASCII '1'~'8'=手动档(8 最亮) | 0x00 → 恢复光敏自动调光;'1'~'8' → 挂起光敏任务 + 硬件档恒等映射 1~8 | +| `'9'`(0x39) | 音量 | len==1;'1'~'5' | 语音板音量 {1,3,5,7,9}(沿贵州实测映射) | +| `'A'`(0x41) | 外设 | len==1;bit0 绿/bit1 红/bit2 黄闪 | **红优先**(沿贵州裁决:红→车道灯红;否则绿→车道灯绿;黄闪独立) | +| `'B'`(0x42) | 收费金额语音 | len≥1;金额 ASCII 串(整数/小数) | **0 元不播**;「您好请交费X元」,小数播小数(末位 0 剔除:123.4→「123.4」);**不显示任何内容** | +| `0x01` | 全屏点亮(二进制命令字) | len==1;DATA0=0x01~0x07(见 §8 决定 10 扩展取值表) | 整屏 `dev_display_fill` + commit | +| `0x02` | 获取版本号(二进制命令字) | len==1 | 回裸 ASCII **PROGRAM_CODE**(固件程序编码,非硬编码 YN_FX_P5_1.0) | + +金额运算整数运算(分),无浮点:`yn_amount_to_fen`(parse.c)整数部分×100 + 小数前两位(`%.2f` 语义)。 + +## 2. 上电效果 + +**无**。本协议**不注册默认显示内容**(用户决定 8)——上电使用固件默认显示(`app_default_display` 回退欢迎画面),协议文档 §0 的「上电显示祝您一路平安 + 稍候熄灭」不实现,`app_yn_proto_default.c` 已移除(Makefile / `.eide/eide.yml` 条目同步删除)。 + +## 3. 模块设计与接入点 + +- 目录/命名:`ProtocolParser_YunNan/`,前缀 `app_yn_`;文件拆分与青海/贵州同构(proto / proto_parse / proto_cmd / proto_voice,**无 proto_default**)。 +- 注册五步:`RB_PROVIDE_WEAK`(RS485+RS232 同槽 weak 合并)→ `app_proto_acquire_buf` → `app_proto_register`(双通道各独立 mask)→ `app_proto_bind_channel`(CH_ID_RS485 + CH_ID_RS232)→ `app_proto_set_frame_queue`;`sw_app_initcall(yn_proto_init)`。 +- probe:**长度字段定界**(len 字段 + 尾字节 `'}'` 校验),PURE 契约(仅 rb_peek);命令字预筛 '1'~'9','A','B',0x01,0x02 否则 FAKE;首字节非 `'{'` 快拒。0x01/0x02 控制字符命令字与青海/山东/四川MTC 的 ASCII 命令集互斥,各 probe 首命令字快拒无冲突。 +- 队列:`YN_PAYLOAD_MAX 259`、`YN_QUEUE_DEPTH 3`(静态 SRAM 801B + cb 80B);`_Static_assert` ≤ RB 768。 +- 处理任务:`yn_handle_task` 栈 256×4;帧缓冲 static;parse 纯函数 → `yn_execute_cmd` 查表分派。 +- 应答:'1' 主机查询、0x02 版本号经 `channel_send` 回源通道(串口通道 `ch->ops->send` → `pl_uart_send`,RS232/RS485 均支持 TX);其余命令单向不回。 +- 自检:一次性任务 `yn_selftest_task`(栈 256×4,`s_yn_selftest_running` 防重入;`s_yn_selftest_abort` 由 `yn_execute_cmd` 在下一帧有效命令到达时置位,任务分片(100ms)检测后退出且**不清屏**)。 +- 波特率:无协议改波特率命令,沿用 DIP1(9600/115200)全局选择。 + +## 4. 与既有模块的差异(对照贵州,同构帧族) + +| 项 | 贵州(GZ) | 云南(YN) | +|---|---|---| +| '2' 自检 | 黄色全屏 + 语音(实测裁决) | **老化循环显示序列**(复用出厂测试,字号×字体循环逐字全屏)+ 每 5s 语音「系统正在自检」,可被下一帧打断 | +| '3' 单行字号 | FONT_16(5 行全容纳) | **FONT_24**(协议 24 点阵;96px 屏高只容纳 4 行,行 5 按协议接受、执行但不落屏) | +| '6' | 固定格式显示(客/货行文) | **单行清除**(语义不同) | +| '8' 亮度 | '0'~'5'(0=恢复光敏自动) | **0x00(NUL)=恢复光敏自动、'1'~'8' 手动档**(8 最亮) | +| '7' 文明用语 | 贵州文案('0' 实测修正) | 协议原文云南文案 | +| 'B' 费额语音 | 金额 ≥0.5 元播报,.00 只播整数 | **0 元不播**;小数播小数(末位 0 剔除) | +| 0x01 全屏点亮 | 01红/02绿/03黄 | **01红~07白 七色**(用户决定 10 扩展) | +| 0x02 版本号 | 回 PROGRAM_CODE + 屏幕红字显示(实测附加行为) | 回 **PROGRAM_CODE**,无屏幕显示 | +| 上电画面 | 不建 default 文件(裁决) | **不建 default 文件**(用户决定 8:不注册默认显示,使用固件默认显示) | + +## 5. 帧头冲突纪律 + +- 云南 ASCII 命令集 `'1'~'9','A','B'` 与青海 probe 命令集**完全重叠**(同 `{` 帧族、len 字段同构):全协议构建下青海 probe 先注册(源码收录序 qh 在前)先认领,云南 probe 仅承接青海 FAKE 的帧(与贵州处境一致)。 +- **量产 EIDE 目录排除纪律**:云南与青海/山东/贵州/四川MTC 互斥(`.eide/eide.yml` Debug 目标已启用四川MTC、排除青海/山东/贵州/云南;云南量产目标须启用云南并排除其余 `{` 帧族)。 +- **编译期互斥守卫**:云南主文件携带 `g_brace_proto_guard`(`#ifndef STD_ALL_PROTO` 包裹、`__attribute__((used))`)——EIDE 量产构建多个 `{` 帧族编入即链接报 `multiple definition`,无法绕过;Makefile 全协议开发构建经 `-DSTD_ALL_PROTO` 豁免共存(probe 注册序纪律约束)。 +- 0x01/0x02 二进制命令字不属于青海/山东/四川MTC 的 ASCII 命令集,各 probe 首命令字快拒 → 云南 probe 可无冲突认领。 + +## 6. 构建口径 + +- **Makefile PROTO=ALL(dev)**:编入(`SRC_APPLICATION` 收录序在贵州之后、`app_uart_baud.c` 之前,probe 注册序为 `{` 帧族末位)。 +- **Makefile PROTO=CQ**:**仍编入**——现有纪律 PROTO=CQ 仅剔除 LDI 目录并追加 `-DPROTO_CHONGQING`,`{` 帧族(青海/山东/贵州/四川MTC)均保留 + `-DSTD_ALL_PROTO` 豁免,云南同属 `{` 帧族随同编入(不改「排除 LDI ↔ PROTO_CHONGQING」不变量)。 +- **EIDE Debug(PROTO_CHONGQING 量产口径)**:云南**排除**(excludeList 加 `Application/protocol/ProtocolParser_YunNan`)——Debug 已启用四川MTC(`{` 帧族守卫生效),云南若编入即 `multiple definition`。Inc 目录已入 incList(与青海/山东/贵州同列,供后续云南量产目标启用)。 +- **EIDE Release**:已知错配未修,本次不动。 +- 无新增编译开关 → `doc/构建开关总表.md` 不动。 + +## 7. as-built 状态 + +- **构建验证(2026-08-24 修订,用户决定 1~10 后重采)**: + - `make clean && make -j8`(PROTO=ALL):通过,零告警(仅 HAL flash_ex 3 条既有 unused-parameter)。text **352860** / data **1656** / bss **138196**。 + - `make clean && make -j8 PROTO=CQ`:通过(同上 3 条既有告警)。text **338996** / data **852** / bss **134720**。 + - 对照初版(含 default 文件)增量变化:bss **+256**(自检单字缓冲 static buf)、text **-56/-64**(default 文件代码移除、自检序列代码加入抵消)。ucHeap 常驻任务栈 +1KB(yn_handle_task);自检任务(1KB)为瞬态。CCM 增量 0。SRAM 未超 124KB 上限。 +- EIDE 构建未执行(EIDE 需 GUI「重新加载项目」后构建;本模块在 Debug 目标已排除,不影响现有 EIDE Debug 构建)。**注意**:`.eide/eide.yml` 已外部编辑(移除 default 文件条目),须在 EIDE 中「Reload Project」后再做任何会触发保存的 GUI 操作。 +- 未烧录。 + +## 8. 已确认决定(原待确认清单,2026-08-24 用户裁定) + +| # | 问题 | 用户决定(已实现) | +|---|---|---| +| 1 | 绑定通道 | RS232/RS485 **双绑**(现状已确认) | +| 2 | 0x33 行号 1~5 与 24 点阵的映射(96px 屏高只容 4 行) | FONT_24 不变;行号范围**以协议为准 '1'~'5' 全接受**。行 5 物理不显示但解析不丢弃不拒绝——渲染调用照常发出,越界由渲染层(dev_display_fill / draw_bitmap 起点越界早退)自然处理,即「执行但不落屏」(parse.c 注释 + cmd.c `_yn_clear_row`/`_yn_exec_one_line` 注释) | +| 3 | '2' 自检显示序列/语音 | 复用 `app_factory_test.c` 263-299 行「老化循环显示」逻辑:整屏单字居中,字号 {FONT_16/24/32}×字体 {ST/FS/KT/HT} 循环「重庆创迪科技发展有限公司设备老化测试」;**每 5s 播报「系统正在自检」**(与循环显示并行);可被下一帧命令打断(`s_yn_selftest_abort`,退出不清屏);防重入保留;不切换车道灯、不破坏出厂测试任务/状态 | +| 4 | 'B' 费额语音文案 | 「您好请交费X元」+ 小数播小数、末位 0 剔除(核对确认:123.4→「123.4」、123.40→「123.4」、123→「123」,`yn_voice_fee_amount` 已实现) | +| 5 | '8' 亮度参数语义 | 参数 1B:**0x00(NUL)=自动亮度**(恢复光敏任务 `osThreadResume`);**ASCII '1'~'8'(0x31~0x38)=手动档 1~8**(挂起光敏任务 + 硬件档恒等映射,8 最亮)。probe/parse 按二进制 len 定界,0x00 参数不会被当字符串终结符或错误 | +| 6 | 'A' 外设红绿同置优先级 | **红优先**(现状已确认,沿贵州裁决) | +| 7 | 0x02 版本号应答 | 回固件 **PROGRAM_CODE**(`app_boot.h` 编译期常量,与山东/贵州同模式 `channel_send(ch, PROGRAM_CODE, ...)`),不再硬编码 YN_FX_P5_1.0(宏 `YN_FW_VERSION` 已删除) | +| 8 | 上电效果 | **删除**「稍候熄灭」与默认显示注册——不注册默认显示内容(使用固件默认显示)。`app_yn_proto_default.c` 已移除,Makefile `SRC_APPLICATION` / `.eide/eide.yml` 条目同步删除(yml 外部编辑后须 EIDE Reload Project) | +| 9 | '1' 主机查询异常应答触发条件 | **恒回正常 00**(现状已确认,设备无故障检测条件) | +| 10 | 0x01 全屏点亮颜色范围 | 扩展到显示屏支持的全部颜色(保留 01红/02绿/03黄 兼容)。P5 户外全彩屏 display_color_t 枚举共 8 色,扩展取值表: | + +**决定 10 扩展取值表**(DATA0 与 `display_color_t` 枚举值恒等,执行层直接按枚举渲染): + +| DATA0 | 颜色 | 枚举 | +|---|---|---| +| 0x01 | 红 | COLOR_RED(协议原文,兼容) | +| 0x02 | 绿 | COLOR_GREEN(协议原文,兼容) | +| 0x03 | 黄 | COLOR_YELLOW(协议原文,兼容) | +| 0x04 | 蓝 | COLOR_BLUE(新增) | +| 0x05 | 紫 | COLOR_PURPLE(新增) | +| 0x06 | 青 | COLOR_CYAN(新增) | +| 0x07 | 白 | COLOR_WHITE(新增) | +| 0x00 | (拒绝) | COLOR_BLACK——全屏黑等效 '5' 全屏清除,非「点亮」 | + +## 9. 修订记录 + +| 日期 | 内容 | +|---|---| +| 2026-08-24 | 新建:云南常规费显协议模块(5 源文件 + 4 头文件)接入 STD 分发框架;Makefile 全协议/CQ 口径编入;EIDE Debug 目标排除;doc/05-01、doc/06-04、doc/CLAUDE.md、doc/08-01 同步 | +| 2026-08-24 | 修订(用户决定 1~10):'2' 自检改老化循环显示 + 5s 语音「系统正在自检」+ 下一帧打断;'3'/'6' 行 5 按协议接受(执行但不落屏);'8' 亮度 0x00 自动档 + '1'~'8' 手动档;'B' 费额语音核对确认;'A' 红优先确认;0x02 应答改 PROGRAM_CODE;移除上电效果 default 文件(不注册默认显示);'1' 恒回正常确认;0x01 全屏点亮扩 01红~07白 七色。构建验证:PROTO=ALL text 352860/data 1656/bss 138196、PROTO=CQ text 338996/data 852/bss 134720,均链接通过 | \ No newline at end of file diff --git a/doc/CLAUDE.md b/doc/CLAUDE.md new file mode 100644 index 0000000..7e74fd0 --- /dev/null +++ b/doc/CLAUDE.md @@ -0,0 +1,930 @@ +# CLAUDE.md + +此文件为 Claude Code (claude.ai/code) 在此仓库中工作时提供指导。 + +## 构建系统 + +STM32F407ZGTx 嵌入式项目,工具链 `arm-none-eabi-gcc`,C23 标准。 + +- **构建**:`make -j8`(根目录 Makefile,`TOOLCHAIN=gcc|clang`,`CONFIG=Debug|Release`,`PROTO=ALL|CQ`) + - GCC Debug:`make -j8`(默认,`-Og -g`) + - GCC Release:`make -j8 CONFIG=Release` + - Clang Debug:`make -j8 TOOLCHAIN=clang` + - Clang Release:`make -j8 TOOLCHAIN=clang CONFIG=Release` + - **`PROTO ?= ALL`**:默认全协议 dev 构建(CQ 恒定编入,位于 LDI 源之后)。 + **`PROTO=CQ`**:重庆量产口径——从源码列表剔除 LDI 目录文件、追加 + `-DPROTO_CHONGQING`(`{` 帧族仍编入并保留 `-DSTD_ALL_PROTO` 豁免守卫。 + **注(2026-08-24 修订)**:TCP Server/Client 通道两口径**均启动**—— + `app_boot.c` 实际无 `#ifndef PROTO_CHONGQING` 口径裁剪,此前「CQ 构建不 + 启动 TCP 通道」表述与代码不符,已按代码实态更正)。 + **网络配置应用(两口径统一,2026-08-21 解耦,2026-08-24 端口应用两口径化)**: + 中立模块 `app_net_boot`(`app_net_boot_apply`,`app_boot.c` init_task 在 + `app_board_net_cfg_fw_version_update` 之后、splash 之前调用——fw_version_update + 空扇区初始化 net_cfg=0,app_net_boot 随后判无效写默认)读 Sector1 net_cfg → + `pl_net_set_ip` + `app_tcp_server_set_port`(口取 `net_cfg.port` = TCP 业务口, + 两口径统一应用);Sector1 空/损坏/非法 → accept_write 写本构建默认记录落盘并 + 应用(方案 B 2026-08-21:两口径默认均 **port=9528(TCP 口)+ udp_port=20103 + (CQ UDP 口)**,IP 各自口径):CQ 192.168.1.5/255.255.255.0/192.168.1.1, + dev 192.168.114.200/255.255.255.0/192.168.114.1。 + LDI `ldi_ctx_init` 与 CQ `cq_proto_init` 不再直接改 netif(ldi 保留 Sector1/W25 + 自愈与配置装载)。 + **CQ setip 双构建语义**(doc/03 PartB B.7.1,2026-08-21 方案 B):setip 命令 + 端口写入 Sector1 `net_cfg.udp_port`(CQ UDP 业务口专有字段),TCP 口 + `net_cfg.port` **保留现值不再被 setip 污染**;改 IP 两构建重启后均生效(均走 + `app_net_boot_apply`);改端口仅 PROTO_CHONGQING 构建对 CQ 业务口生效 + (`_udp_cq_read_port` 读 Sector1 net_cfg.udp_port,无效/0 回退 20103),dev + 构建 CQ 业务口固定 20103(`#else` 分支)、setip 的 port 对 dev 构建任何口都 + 不生效——端口修改验证须用 `make PROTO=CQ` 或 EIDE `PROTO_CHONGQING` 目标。 + **CQ 心跳超时故障屏开关**(`app_cq_proto.h` `CQ_FAULT_SCREEN`,1 开 / 0 关, + doc/03 PartB B.7.2,2026-08-21 新增、2026-08-24 改名单层宏):默认跟随构建 + 口径——PROTO_CHONGQING 开启、共存/其他构建(通用版多协议固件)关闭(他省 + 上位机不发重庆 syn1,120s 故障屏会误触发);可显式覆盖 `-DCQ_FAULT_SCREEN=1` + 强制开 / `=0` 强制关(旧名 CQ_FAULT_SCREEN_ENABLED / CQ_FAULT_SCREEN_FORCE_ON + 已废弃,不保留兼容别名)。 + 关闭时 `cq_proto_timer_task` 不计数、不渲染(`s_cq_sync_counter` 恒 0、 + `s_cq_fault_shown` 恒 false),warn1 倒计时不受开关影响。 + **不变量:排除 LDI 与定义 `PROTO_CHONGQING` 必须成对**(app_net_boot 兜底后 + 中间态不再致命,但 PROTO_CHONGQING 仍决定默认口径与端口应用对象,不成对 = + 口径错配)。EIDE Debug 目标当前为 CQ 量产口径(excludeList 排除 ldi + + defineList 含 PROTO_CHONGQING)。 +- **`{` 帧族编译期互斥守卫**:Makefile 默认定义 `-DSTD_ALL_PROTO`(全协议共存开发构建,`g_brace_proto_guard` 守卫失效);EIDE 量产构建不定义该宏——多个 `{` 帧族协议(青海/山东/贵州/四川MTC)同时编入即链接报 `multiple definition of 'g_brace_proto_guard'`,强制互斥。 +- **EIDE 持有 eide.yml(外部编辑纪律)**:EIDE 运行时用自己的内存模型回写 `.eide/eide.yml`,会冲掉外部直接编辑(曾两次发生:CQ 文件夹收录、app_net_boot.c 收录被回写丢失,导致 EIDE 构建 undefined reference)。**外部改完 yml 后必须立刻在 EIDE 中「重新加载项目」(EIDE: Reload Project)让它读入新条目,期间不要先做任何会触发保存的 GUI 操作**;构建前先确认 EIDE 编译列表含新增源文件。 +- **编译数据库**:`bear --output build/Debug/compile_commands.json -- make -B -j8`(`-B` 强制全量重编译) +- **编译开关总表**:`doc/构建开关总表.md`(2026-08-24 新增,收录 PROTO_CHONGQING / STD_ALL_PROTO / CQ_FAULT_SCREEN / g_brace_proto_guard 等全部编译期开关及各构建入口默认值) +- **烧录**:`openocd -f ./Compiler/stm32f407zg.cfg -c "init; halt; program ./build/Debug/Project_STD.hex verify reset exit"` +- **整片擦除**:`openocd -f ./Compiler/stm32f407zg.cfg -c "init; halt; stm32f4x unlock 0; stm32f4x mass_erase 0; shutdown"` +- **J-Link 一键烧录**:`bash tool/flash_all.sh`(Bootloader + Recovery + 主固件一次 J-Link 会话烧完,**默认烧后擦除 Sector1(0x08004000~0x08007FFF)恢复出厂配置态**;`--keep-config` 保留板级配置;`--erase` 整片擦除;`--verify` 校验;`--dry-run` 预览命令不烧录) +- **调试期烧录陷阱(已规避)**:Bootloader 条件 D(非 debug 模式)校验 Sector1 记录 `app_info.size/crc32` 与 `0x08040000` 实机固件 CRC 是否一致,`app_info.size/crc32` 真实值只由 Recovery 升级流程写入(`app_info.version` 由主固件启动时从 PROGRAM_CODE 落库,供 IAP 0x03 上报)。一旦 Recovery 成功升级过一次,再用 J-Link/EIDE 直接重烧主固件而不更新 Sector1 → 条件 D 校验失配 → Bootloader 判「App 损坏」→ 设备永远进 Recovery。**纪律:任何烧录器(J-Link/OpenOCD/EIDE)烧完主固件后必须擦除 Sector1 恢复出厂态**——Sector1 空 → Bootloader 走条件 C(出厂初始化)→ 正常跳主固件;net_cfg 丢失副作用可接受,主固件上电 `ldi_ctx_init` 从 W25Qxx 外部 Flash 同步回写(空扇区自动完整初始化)。`tool/flash_all.sh` 已默认执行该纪律;手动烧录/单固件烧录同样遵守「烧完擦 Sector1」。 +- **三固件同步烧录纪律(方案 B 布局变更,2026-08-21)**:方案 B 把 Sector1 记录从 68B 扩到 72B(`NetConfig_t` 新增 `udp_port`,`config_crc` 偏移 64→68,Bootloader/Recovery 的 `config_info.h` 已同步修改)。**设备上 Bootloader/Recovery 若仍是旧 68B 布局(未随主固件重烧)**:旧 Bootloader 按旧偏移读 `config_crc`(读到新记录的 `udp_port` 字段)→ 判 CRC 失配 → 重建**旧布局**出厂记录;主固件按 72B 读又失配 → 反复自愈改写,两布局交替重建、Sector1 永不稳定(网络配置回出厂态;残留升级中间态还可能被旧 Bootloader 条件 A/B/D 误判 → 设备陷 Recovery、主固件不运行 → UDP 10011/20103 全部无响应,LDI 搜索/CQ 业务均失效)。**纪律:方案 B 后三固件必须同步烧录(`tool/flash_all.sh` 一次 J-Link 会话)**,单固件重烧后按惯例擦 Sector1。 +- **0x03 版本为空判断路径**:现场 IAP 0x03 上报 `app_info.version` 全 0 时,按序判断: + ① 看 RTT 是否有 `[fwver]` 日志——**无日志 = 主固件从未运行,设备落在 Recovery 态**(旧 + Sector1 记录 size/crc32 与新烧固件失配 → 条件 D 判 App 损坏),补做「擦 Sector1」后重启 + 即可;有 `[fwver] write ok` 仍为空 = 应答来自 Recovery(0x03 应答 ReData[10] `update_sta` + ==2 且 ReData[0] `size`≠0 为 Recovery 态特征,主固件态 update_sta==0);有 `erase/write + failed` = 擦写路径问题;② 确认烧录物为最新构建(`arm-none-eabi-strings + build/Debug/Project_STD.elf | grep 9K10212482`,EIDE 与 make 共用 `build/Debug` 同名产物, + 注意陈旧覆盖)。 +- **清理**:`make clean`(仅删除 `build/$(CONFIG)` 目录) + +### 构建产物 + +| 产物 | 格式 | 用途 | +|---|---|---| +| `Project_STD.elf` | ELF(带调试符号) | 链接器输出,含完整符号表,调试器(probe-rs / cortex-debug)直接使用。`arm-none-eabi-size` 输出各段大小 | +| `Project_STD.hex` | Intel HEX(ASCII 文本) | 烧录文件。OpenOCD 通过 `program ... verify` 写入并回读比对。文本格式,每行含地址+数据+校验和 | +| `Project_STD.bin` | 纯二进制 | 裸二进制映像,无地址信息。适用于 IAP bootloader 直接写入 Flash | + +**链接优化**:`--gc-sections` + `-ffunction-sections -fdata-sections`,未引用的函数/数据自动丢弃。`--specs=nano.specs` 链接精简版 newlib。`-u _printf_float` 强制链接 printf 浮点格式化支持(newlib-nano 默认关闭)。`-fshort-enums` 按最小宽度打包枚举类型。 + +## 文档地图 + +| 目录 | 角色 | +|---|---| +| `doc/01_显示系统` | 显示层:1-260/1-577/1-969 模组驱动、自适应字号 | +| `doc/02_LDI协议与外设接口` | LDI 协议与光敏/字库等外设接口 | +| `doc/03_重庆高速二代费显协议` | RLS 重庆协议接入记录 | +| `doc/04_青海高速费显协议` | 青海协议接入记录 | +| `doc/05_协议模块多协议兼容优化` | 多协议 RB/绑定/queue 深度权威(`01_architecture.md`) | +| `doc/06_SRAM内部分数据迁移` | 内存分区唯一权威;`04_current_memory_occupancy.md` 为占用账 | +| `doc/07_LDI与IAP配置解耦` | LDI↔IAP 配置解耦(方案 A 已落地,`app_board_net_cfg`) | +| `doc/08_协议模块接入规则` | 新协议接入权威指引(规则/串口/网络/检查清单) | +| `doc/09_山东费显协议` | 山东车道费额显示器协议接入记录 | +| `doc/10_贵州费显协议` | 贵州常规费显协议接入记录(13 命令/裁决差异表/帧头冲突纪律) | +| `doc/11_云南费显协议` | 云南常规费显协议接入记录(13 命令/24 点阵渲染/已确认决定) | + +## 硬件架构 + +**MCU**:STM32F407ZGTx(Cortex-M4F,168MHz,FPU,1024KB Flash,128KB SRAM + 64KB CCMRAM) + +**时钟**:HSE 8MHz → PLL (M=4, N=168, P=2, Q=7) → SYSCLK 168MHz / APB1 42MHz / APB2 84MHz + +**外设**:ADC1、SPI1(W25Qxx)、TIM2/3/4/7、USART1/USART3/USART6、DMA1/2、CRC、IWDG、RTC、Ethernet MAC + DP83848 PHY + +## 内存布局 + +链接脚本:`Compiler/STM32F407XX_FLASH.ld` + +``` +Flash (1024KB, 起始 0x08000000) +├─ Sector 0: 0x08000000 16KB Bootloader(独立工程,`ORIGIN 0x08000000 LENGTH 16K`) +├─ Sector 1: 0x08004000 16KB 板级系统配置 (app_board_net_cfg.c) +├─ Sector 2~5: 0x08008000 224KB Recovery 固件(独立工程,`ORIGIN 0x08008000 LENGTH 224K`) +├─ Sector 6~11: 0x08040000 768KB 主固件 .text/.rodata/.initcall(本工程,FLASH ORIGIN 0x08040000) +└─ (Sector 11 已释放 — LDI 配置已迁移至 W25Qxx,不占内部 Flash) + +SRAM (128KB, 0x20000000) — Debug 链接约用 **92%(≈120820B)**(2026-08-14;以 06/04 为准) +├─ .data / .bss LwIP ram_heap + RX_POOL + PBUF_POOL 等(ETH 大户,永留 SRAM) +├─ ucHeap **36KB** FreeRTOS heap_4(任务栈/TCB/动态 OS 对象) +├─ 协议 RB RJ45 **1536** / RS485 **768** / RS232 **768**(三槽 provide) +├─ UART DMA RS485/RS232 各 **640**(无 RS232_1 DMA) +├─ 帧 queue 静态 IAP2 / LDI4 / QH3 / RLS2 / AH3 / SC_ETC3 / SC_MTC3 / SC_OL3 / SD3 / GZ3 / YN3(不占 ucHeap) +├─ RTT Up 2KB;W25 sec、s_dma_bounce 等 +└─ _user_heap_stack newlib + MSP 预留 + +CCMRAM (64KB, 0x10000000, NOLOAD) — 约用 **58%(38208B)**(1-577 3×3 / 同量级 1-969) +├─ pixel_map / hub75_buff / row_dst / g_bsrr **仅显存与 BSRR** +├─ 例外(2026-08-20):CQ 协议帧队列/缓冲 **6377B**(s_cq_queue_buf 3156 + queue_cb 80 + +│ 任务帧缓冲 1052 + JSON/文本缓冲 2089;SRAM 全协议构建余量不足,CPU 独占访问无 DMA) +└─ (无 ucHeap、无协议 RB、无 UART DMA) +``` + +**CCMRAM 特性**:零等待、D-bus 单端口;`startup.c` 清零。仅数据、不可执行;**DMA/ETH 不可达**。占用以 `doc/06_SRAM内部分数据迁移/04_current_memory_occupancy.md` 为准;分区宪法见同目录 `02_memory_policy.md`。多协议 RB / queue 深度见 `doc/05_协议模块多协议兼容优化/`。 + +## 闪存扇区职能地图 + +> **当前状态**:Bootloader 与 Recovery 已作为独立工程部署(workspace 内 `STM32F407-Bootloader-master` / `STM32F407-Recovery-master`)。主固件已从 `0x08040000` 链接(Sector 6~11,768K,`Compiler/STM32F407XX_FLASH.ld`),并在 `Core/Src/main.c` 中 `SCB->VTOR = FLASH_BASE | 0x40000` 重定位向量表。 + +| 扇区 | 地址 | 大小 | 内容 | 读写方式 | +|---|---|---|---|---| +| 0 | `0x08000000` | 16KB | Bootloader(独立工程,`ORIGIN 0x08000000 LENGTH 16K`) | 烧录时写入 | +| 1 | `0x08004000` | 16KB | 板级系统配置 (magic + update_sta + FWInfo + NetConfig + CRC32;记录 72B,NetConfig 20B 含 port/udp_port 双端口,方案 B 2026-08-21) | `app_board_net_cfg.c` 内存映射读 + 擦写 | +| 2~5 | `0x08008000`~`0x08040000` | 224KB | Recovery 固件(独立工程,`ORIGIN 0x08008000 LENGTH 224K`) | IAP 升级时写入 | +| 6~11 | `0x08040000`~`0x080FFFFF` | 768KB | 主固件 (.text/.rodata/.initcall) | 烧录时写入 | +| 11 注 | `0x080E0000` | 128KB | 已释放 — LDI 配置已迁移至 W25Qxx 最后一个 4KB 扇区(不再占内部 Flash) | — | + +**外部 Flash**(W25Qxx,SPI1,~8MB):字库数据(5 字号×4 字型×2 编码的 40 个三元组) + LDI 设备配置(最后一个 4KB 扇区),通过 `dev_storage_read` 接口访问。LDI 配置绑定在 `Application/Src/LDI/app_ldi_cfg.c`(`sw_dev_initcall` 中 `s_ldi_base = dev_storage_capacity(w25) - 4096`)。 + +**出厂默认网络配置(三固件统一)**:192.168.114.200/24(网关 192.168.114.1、TCP 口 9528;CQ UDP 口 20103,方案 B 2026-08-21)。Recovery 上电 `MX_LWIP_Init` 直读 Sector1.net_cfg 配 netif(空/无效回退统一默认);主固件 `ldi_ctx_init` 上电同步三分支:**Sector1 为网络配置真源、W25 为恢复镜像**(Sector1 有效且 net_cfg 合法即优先采纳,含升级中间态,`update_sta` 不参与分支判定(2026-08-21 修订);Sector1 空/损坏/记录无效或 net_cfg 非法时以 W25 自愈回写;皆空用统一出厂默认,详见 `doc/07_LDI与IAP配置解耦/02_decoupling_solutions.md` §11/§12)。**netif 应用由中立模块 `app_net_boot`(`app_net_boot_apply`)统一执行**——`ldi_ctx_init` 只做自愈与配置装载(2026-08-21 解耦);Sector1 空/损坏/非法时 `app_net_boot` accept_write 写本构建默认落盘并应用(两口径默认均 port=9528/udp_port=20103,IP 各自口径:CQ 192.168.1.5、dev 114.200,详见 §14/§15)。**所有改 IP 接口(LDI 0AH / IAP 4B02 / Recovery IAP)统一重启生效**(运行时不即时改 netif);Bootloader 对 Sector1 损坏态(magic 不匹配或 CRC 错)自动重建出厂记录自愈(详见 §12)。UDP 发现口 10011 固定(宏 `LDI_DISCOVERY_PORT`)。**搜索/上报端口字段语义**:LDI 12H 应答 port 与 IAP 0x01 上报 port 均为配置功能端口(TCP 业务口,出厂默认 9528,宏 `LDI_DEFAULT_CONFIG_PORT`),搜索/上报的传输渠道才是 UDP 10011——勿混淆(2026-08-18 修复 12H 误报 10011 的污染循环)。 + +## 软件分层架构 + +依赖方向严格单向(上层可见下层,反之不可): + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ Application/ (app_*) │ +│ 业务逻辑:boot编排、协议处理(IAP/LDI/AH_MQTT)、渲染引擎、 │ +│ 传感器管理、网络通道(UDP/TCP/MQTT/RS232/RS485任务) │ +│ │ +│ 依赖:Device/ + Kernel/ + Platform/ (不直接碰 HAL 地址) │ +├──────────────────────────────────────────────────────────────────┤ +│ Device/ (dev_*) │ +│ 设备抽象:OCP 虚表封装硬件差异 │ +│ Display/ Storage/ Comm/ Network/ IO/ Config/ │ +│ │ +│ 依赖:Platform/ + Kernel/ (通过 pl_* 接口操作硬件) │ +├──────────────────────────────────────────────────────────────────┤ +│ Kernel/ │ +│ 纯软件工具:ring_buffer、initcall、container_of、text_cvt、 │ +│ bit_utils、crc_utils、bcc_utils │ +│ │ +│ 依赖:无(零硬件依赖,仅 stdint/stddef/stdbool) │ +├──────────────────────────────────────────────────────────────────┤ +│ Platform/ (pl_*) │ +│ HAL 薄封装:不透明句柄 (void*),HAL 类型对外不可见 │ +│ pl_spi pl_tim pl_uart pl_gpio pl_hub75 pl_eth pl_net │ +│ pl_flash pl_crc pl_rtc pl_iwdg pl_dma pl_dwt pl_sys │ +│ │ +│ 依赖:Core/ + HAL 库 (仅 .c 文件 include Core 头文件) │ +├──────────────────────────────────────────────────────────────────┤ +│ Core/ │ +│ STM32CubeMX 生成:MX_xxx_Init()、ISR 声明、HAL 配置 │ +│ main.c system_stm32f4xx.c stm32f4xx_it.c FreeRTOSConfig.h │ +│ │ +│ 依赖:HAL 库 + CMSIS │ +├──────────────────────────────────────────────────────────────────┤ +│ Compiler/ │ +│ startup.c (C 语言 Reset_Handler + 自定义向量表) │ +│ STM32F407XX_FLASH.ld (链接脚本,含 initcall 段定义) │ +└──────────────────────────────────────────────────────────────────┘ +``` + +### Platform 层内部依赖 + +``` +pl_sys (SystemClock_Config, delay, reset) + ├─ pl_dma (DMA 流初始化,需在 SPI/UART 之前) + ├─ pl_gpio (GPIO 时钟,几乎所有模块依赖) + ├─ pl_tim (TIM2/3/4/7) + ├─ pl_spi (SPI1,依赖 DMA) + ├─ pl_uart (USART1/3/6,依赖 DMA) + ├─ pl_crc (硬件 CRC) + ├─ pl_iwdg (独立看门狗) + ├─ pl_rtc (RTC 备份寄存器) + ├─ pl_adc (ADC1) + ├─ pl_exti (外部中断) + ├─ pl_hub75 (HUB75 GPIO 位拆裂) + ├─ pl_flash (内部 Flash 编程) + ├─ pl_dwt (DWT 周期计数器) + ├─ pl_rtt (SEGGER RTT) + ├─ pl_eth (ETH MAC + MDIO) + └─ pl_net (LwIP 初始化,依赖 pl_eth) +``` + +各模块用不透明句柄(`typedef void *pl_xxx_handle_t`),Device 层只传句柄、不感知 HAL 类型。 + +## 启动流程与 initcall + +### 完整启动序列 + +``` +硬件上电 → Reset_Handler (Compiler/startup.c) + ├─ .data 拷贝 (Flash→SRAM), .bss 清零, CCMRAM 清零 + ├─ SystemInit() — FPU 使能(VTOR 在 main() 中由 `SCB->VTOR` 重定位到 0x08040000) + └─ main() (Core/Src/main.c) + ├─ SCB->VTOR = FLASH_BASE | 0x40000 ← 向量表重定位(主固件入口 0x08040000) + ├─ HAL_Init() ← HAL 库基础 + ├─ SystemClock_Config() ← 168MHz + ├─ initcall_run(__hw_initcall) ← RTOS 前硬件初始化 + └─ app_boot() ← RTOS 编排 + ├─ osKernelInitialize() + ├─ osThreadNew(init_task, prio=High, stack=512×4) + └─ osKernelStart() + ├─ [FreeRTOS 接管: SysTick→PendSV 任务切换] + └─ init_task: + ├─ dev_eth_start() ← LwIP + netif + ├─ sw_board_init() ← initcall_run(__sw_initcall) + ├─ app_board_net_cfg_fw_version_update() ← PROGRAM_CODE 落库 Sector1(空扇区 net_cfg 置 0) + ├─ app_net_boot_apply() ← Sector1 net_cfg → netif + TCP Server 口(CQ 构建只应用 netif;空/损坏写默认落盘) + ├─ app_splash_display() ← 开机画面 (FW/MD 版本, 5s) + ├─ osThreadNew(half_sec_task) + ├─ app_tcp_server_start() / app_tcp_client_start() (#ifndef PROTO_CHONGQING) + │ / app_udp_start() + ├─ app_rs485_start() / app_rs232_start() (app_rs232_1_start 注释) + ├─ app_default_display() ← 自注册默认显示界面(协议注册回调则用之,否则默认欢迎画面) + └─ osThreadExit() ← 自我销毁 +``` + +### initcall 初始化顺序表 + +**hw_initcall (RTOS 前,main() 中执行)**: + +| 顺序 | 层级 | 函数 | 文件 | 职责 | +|---|---|---|---|---| +| 0-pre | — | (空) | — | 预留 | +| 1-pl | pl | `pl_dma_init` | `pl_dma.c` | DMA 流初始化 | +| 1-pl | pl | `pl_gpio_init` | `pl_gpio.c` | GPIO 时钟使能 | +| 1-pl | pl | `pl_spi_init` | `pl_spi.c` | SPI1 初始化 | +| 1-pl | pl | `pl_tim_init` | `pl_tim.c` | TIM2/3/4/7 初始化 | +| 1-pl | pl | `pl_uart_init` | `pl_uart.c` | USART1/3/6 初始化 | +| 1-pl | pl | `pl_crc_init` | `pl_crc.c` | 硬件 CRC | +| 1-pl | pl | `pl_iwdg_init` | `pl_iwdg.c` | 看门狗 | +| 1-pl | pl | `pl_rtc_init` | `pl_rtc.c` | RTC | +| 1-pl | pl | `pl_adc_init` | `pl_adc.c` | ADC1 | +| 1-pl | pl | `pl_exti_init` | `pl_exti.c` | 外部中断 | +| 1-pl | pl | `pl_hub75_init` | `pl_hub75.c` | HUB75 GPIO | +| 1-pl | pl | `pl_rtt_init` | `pl_rtt.c` | SEGGER RTT | +| 2-dev | dev | `dev_display_init` | `dev_display.c` | HUB75 引脚 + DBG 冻结 | +| 2-dev | dev | `dev_display_1_577_init` | `dev_display_1_577.c` | 1-577 模组实例注册(EIDE 现行) | +| 2-dev | dev | `dev_display_1_260_init` | `dev_display_1_260.c` | 1-260 模组实例注册(Makefile 默认) | +| 2-dev | dev | `dev_display_1_969_init` | `dev_display_1_969.c` | 1-969 模组实例注册(备用) | +| 2-dev | dev | `dev_display_p20_init` | `dev_display_p20.c` | P20 模组实例注册(未编入任何构建) | +| 2-dev | dev | `dev_w25qxx_init` | `dev_w25qxx.c` | SPI Flash JEDEC 识别 | +| 2-dev | dev | `dev_eth_init` | `dev_eth.c` | ETH MAC + DP83848 PHY | +| 2-dev | dev | `dev_rs485_init` | `dev_rs485.c` | RS485 RE 方向回调注入 | +| 2-dev | dev | `_app_board_net_cfg_storage_init` | `Application/Src/Config/app_board_net_cfg.c` | 内部 Flash ops 绑定 (Sector 1) | +| 2-dev | dev | `dev_io_ctrl_init` | `dev_io_ctrl.c` | 车道灯/闪光灯 GPIO 初始化 | +| 2-dev | dev | `dev_key_init` | `dev_key.c` | EXTI 中断回调注册 (SW1~SW3, TEST) | +| 3-post | — | (空) | — | 预留 | + +**sw_initcall (RTOS 后,init_task → sw_board_init 中执行)**: + +| 顺序 | 层级 | 函数 | 文件 | 职责 | +|---|---|---|---|---| +| 0-pre | — | (空) | — | 预留 | +| 2-dev | dev | `dev_display_start` | `dev_display.c` | 创建 scan_task + 启动 TIM3/4 | +| 2-dev | dev | `dev_key_sw_init` | `dev_key.c` | 创建 TEST 按键信号量 | +| 2-dev | dev | `_app_flash_ldi_storage_init` | `Application/Src/LDI/app_ldi_cfg.c` | LDI 配置 W25Qxx 地址计算 | +| 3-app | app | `app_dispatch_init` | `app_dispatch.c` | 创建 ch_queue + frame_dispatch_task | +| 3-app | app | `_render_init` | `app_render.c` | 绑定 display + storage 句柄 | +| 3-app | app | `_app_key_init` | `app_key.c` | 创建 key_poll_task (20ms) | +| 3-app | app | `app_light_sensor_init` | `app_light_sensor.c` | 创建光传感器自动调节任务 | +| 3-app | app | `iap_module_init` | `Application/Src/IAP/app_iap.c` | IAP 协议自注册(UDP) | +| 3-app | app | `ldi_module_init` | `Application/Src/LDI/app_ldi.c` | LDI 协议自注册(TCP/UDP 三通道) | +| 3-app | app | `qh_proto_init` | `Application/Src/ProtocolParser_QingHai/app_qh_proto.c` | 青海协议自注册(RS485+RS232) | +| 3-app | app | `sc_etc_proto_init` | `Application/Src/ProtocolParser_SiChuang_ETC/app_sc_etc_proto.c` | 四川 ETC 费显协议自注册(RS485+RS232) | +| 3-app | app | `sc_mtc_proto_init` | `Application/Src/ProtocolParser_SiChuang_MTC/app_sc_mtc_proto.c` | 四川 MTC 费显协议自注册(RS485+RS232) | +| 3-app | app | `sc_ol_proto_init` | `Application/Src/ProtocolParser_SiChuang_Overload/app_sc_ol_proto.c` | 四川治超屏协议自注册(RS485+RS232) | +| 3-app | app | `sd_proto_init` | `Application/Src/ProtocolParser_ShanDong/app_sd_proto.c` | 山东费显协议自注册(RS485+RS232) | +| 3-app | app | `gz_proto_init` | `Application/Src/ProtocolParser_GuiZhou/app_gz_proto.c` | 贵州费显协议自注册(RS485+RS232) | +| 3-app | app | `yn_proto_init` | `Application/Src/ProtocolParser_YunNan/app_yn_proto.c` | 云南费显协议自注册(RS485+RS232;不注册默认显示——使用固件默认显示,用户决定 8) | +| 3-app | app | `cq_proto_init` | `Application/Src/ProtocolParser_ChongQing/app_cq_proto.c` | 重庆CQ协议自注册(UDP_CQ 业务口 + UDP 搜索口双 mask;cJSON 钩子换绑 FreeRTOS 堆;网络配置应用移交 app_net_boot) | +| 3-app | app | `app_uart_baud_init` | `Application/Src/app_uart_baud.c` | DIP1 波特率选择(RS232+RS485 同步切换) | +| 3-app | app | `rls_module_init` | `Application/Src/RLS/app_rls.c` | RLS 协议自注册(RS485) | +| 3-app | app | `_factory_test_init` | `Application/Src/app_factory_test.c` | 出厂检测 monitor | +| 3-app | app | `ah_mqtt_module_init` | `Application/Src/AH_MQTT/ah_mqtt.c` | 已注释,未启用 | +| 4-post | — | (空) | — | 预留 | + +### initcall 实现原理 + +链接脚本中定义两个特殊段: +``` +.hw_initcall : { KEEP(*(SORT(.hw_initcall.0))) ... KEEP(*(SORT(.hw_initcall.3))) } +.sw_initcall : { KEEP(*(SORT(.sw_initcall.0))) ... KEEP(*(SORT(.sw_initcall.4))) } +``` + +每个 `*_initcall(fn)` 宏生成一个 `initcall_entry_t` 常量放入对应 section。`initcall_run(start, end)` 顺序遍历调用。同层内按**链接顺序(源码收录序)**执行——`SORT()` 对同名 section 退化为 .o 链接顺序,Makefile/EIDE 源码收录序即调用序(`app_dispatch_init` 靠收录序在协议之前)。 + +## 中断体系 + +### 中断源与分派路径 + +``` +外设硬件中断 +├─ TIM3_IRQHandler (pl_tim.c) +│ └─ HAL_TIM_IRQHandler → HAL_TIM_PeriodElapsedCallback +│ └─ osEventFlagsSet(s_scan_evt) → 唤醒 scan_task +│ +├─ TIM4_IRQHandler (pl_tim.c) +│ └─ HAL_TIM_IRQHandler → HAL_TIM_PeriodElapsedCallback +│ └─ PWM 亮度:pl_hub75_oe_set(pwm_cnt >= light_level) +│ +├─ TIM7_IRQHandler (pl_tim.c) +│ └─ HAL_TIM_IRQHandler → HAL_TIM_PeriodElapsedCallback +│ └─ HAL_IncTick() (FreeRTOS 时基,1ms) +│ +├─ USART1_IRQHandler (pl_uart.c) +│ └─ HAL_UART_IRQHandler + uart_idle_handle +│ └─ DMA 空闲中断 → 计算接收长度 → rx_cb→ +│ osMessageQueuePut(rx_queue, &len) → 唤醒 rs485_task / rs232_task +│ +├─ USART3_IRQHandler / USART6_IRQHandler (同上) +│ +├─ SPI1_IRQHandler (pl_spi.c) +│ └─ HAL_SPI_IRQHandler → HAL_SPI_TxRxCpltCallback / HAL_SPI_RxCpltCallback +│ └─ rx_cplt_cb(s_ok=true) → dev_w25qxx DMA 完成通知 +│ +├─ DMA1_Stream1_IRQHandler (pl_uart.c, USART3 RX DMA) +├─ DMA2_Stream1_IRQHandler (pl_uart.c, USART6 RX DMA — 句柄已初始化但无通道启动接收) +├─ DMA2_Stream2_IRQHandler (pl_uart.c, USART1 RX DMA) +│ +├─ ETH_IRQHandler (pl_eth.c, 见下) +│ +├─ HardFault_Handler (stm32f4xx_it.c) +│ └─ naked asm → hard_fault_handler_c → 现场保存 (R0-R3,R12,LR,PC,xPSR,CFSR,BFAR) +│ +├─ SysTick_Handler (FreeRTOS 内部, CMSIS-RTOS V2 封装) +├─ SVC_Handler → vPortSVCHandler (FreeRTOS 内核, 启动第一个任务) +└─ PendSV_Handler → xPortPendSVHandler (FreeRTOS 内核, 任务上下文切换) +``` + +**设计原则**: +- 所有外设 ISR 内聚在 Platform 层 .c 文件中,不分散在 `stm32f4xx_it.c` +- ISR 中只做最小操作(标记、通知),耗时逻辑全部在 RTOS 任务中完成 +- 中断优先级:`configMAX_SYSCALL_INTERRUPT_PRIORITY = 5`,FreeRTOS API 仅可在优先级 ≥5 的 ISR 中调用 + +### 中断→任务解耦的三级流水线 + +``` +[裸机 ISR] [RTOS 信号量/队列] [RTOS 任务] +───────────────────────────────────────────────────────────────── +TIM3 行同步 ──→ osEventFlagsSet ──→ scan_task (Realtime) +TIM4 PWM (ISR 内直接完成,不唤醒任务) +UART 空闲中断 ──→ osMessageQueuePut ──→ rs485_task / rs232_task + └─→ app_channel_dispatch + └─→ osMessageQueuePut(ch_queue) + ──→ frame_dispatch_task + └─→ osMessageQueuePut(frame_queue) + ──→ 协议处理任务 +SPI DMA 完成 ──→ volatile s_ok=true ──→ dev_w25qxx _read 轮询 (osDelay 轮询) +ETH 收包 ──→ osSemaphoreRelease ──→ ethernetif_input +ETH 链路状态 ──→ pl_net_link_listener ──→ UDP/TCP/MQTT 通道任务重建连接 +``` + +**关键模式**: +- **Event Flags**:`scan_task` 用 `osEventFlagsWait` 等待 TIM3 行同步触发。ISR 中使用 `osEventFlagsSet`(`configUSE_OS2_EVENTFLAGS_FROM_ISR=1` 使能 ISR 安全调用) +- **Message Queue**:`rs485_task` / `rs232_task`、`frame_dispatch_task`、协议处理任务逐级通过队列传递数据指针 +- **Semaphore**:UDP/TCP 通道用信号量协调连接/断开生命周期(链路断开→信号量释放→任务重建连接) +- **volatile 轮询**:W25Qxx SPI 半双工 DMA 读使用 `while(!s_ok) osDelay(1)` 轮询(历史原因:RTOS 未完全就绪时无法使用 event flags) + +## RTOS 配置 + +`Core/Inc/FreeRTOSConfig.h`,FreeRTOS v10.3.1 + CMSIS-RTOS V2 封装: + +| 参数 | 值 | 说明 | +|---|---|---| +| `configTICK_RATE_HZ` | 1000 | 1ms 时基(TIM7) | +| `configMAX_PRIORITIES` | 56 | 优先级范围 0-55 | +| `configTOTAL_HEAP_SIZE` | **36KB** | heap_4 static `ucHeap` → SRAM `.bss`;水位见 06/04 | +| `configAPPLICATION_ALLOCATED_HEAP` | **0** | 不用 CCM 堆文件;禁止堆指针作 DMA | +| `configMINIMAL_STACK_SIZE` | 128 words (512B) | 最小任务栈 | +| `configCHECK_FOR_STACK_OVERFLOW` | 2 | 栈溢出检测(检查栈顶标记) | +| `configUSE_MUTEXES` | 1 | 互斥锁(含优先级继承) | +| `configUSE_COUNTING_SEMAPHORES` | 1 | 计数信号量 | +| `configUSE_OS2_EVENTFLAGS_FROM_ISR` | 1 | ISR 中可操作 EventFlags | +| `configGENERATE_RUN_TIME_STATS` | 1 | 运行时统计(时钟=DWT 周期计数器) | +| `configMAX_SYSCALL_INTERRUPT_PRIORITY` | 5 | 可调用 FreeRTOS API 的最高中断优先级 | +| `configKERNEL_INTERRUPT_PRIORITY` | 15 | 内核中断最低优先级 | + +## 任务清单与同步机制 + +### 任务栈大小设计考量 + +| 任务 | 栈 | 优先级 | 创建者 | 职责 | 栈设计依据 | +|---|---|---|---|---|---| +| `scan_task` | 256×4=1KB | Realtime(最高) | `dev_display_start` | 行扫描输出 | 扫屏路径无大栈帧(原 2KB 偏大),仅 event wait + ops 调用 | +| `init_task` | 512×4=2KB | High | `app_boot` | 初始化后退出 | `dev_eth_start`→`sw_board_init`→渲染测试,含 printf 和栈数组(`font_buf[512]`、`text_buf[256]`),曾因溢触发 HardFault,从 256×4 扩容 | +| `frame_dispatch_task` | 256×4=1KB | Normal | `app_dispatch_init` | 帧分发引擎 | 核心调度循环,含 `frame_msg_t`(~1052B),无深层嵌套 | +| `iap_handle_task` | 256×4=1KB | Normal | `app_iap` | IAP 帧处理 | CRC32 校验 + cmd 分派(帧缓冲 static;原 2KB 偏大) | +| `ldi_handle_task` | 256×4=1KB | Normal | `app_ldi` | LDI 帧处理 | 显示内容解析与更新(帧缓冲 static;原 2KB 偏大) | +| `ldi_timer_task` | 256×4=1KB | Normal | `app_ldi` | LDI 定时任务 | 周期性状态上报(原 2KB 偏大) | +| `rs485_task` | 256×4=1KB | Normal | `app_rs485` | RS485 接收循环 | `osMessageQueueGet` → `app_channel_dispatch` | +| `rs232_task` | 256×4=1KB | Normal | `app_rs232` | RS232 接收循环 | 同构 rs485_task | +| `half_sec_task` | 128×4=512B | Low | `init_task` | 500ms 喂狗+LED | 最小合法栈,仅 GPIO+RTC API | +| 各网络通道任务 | 256×4=1KB | Normal | 各模块 | UDP/TCP 收发包 | LwIP netconn API 调用,不含大局部变量 | +| `ethernetif_input` | 256×4=1KB | — | `pl_net.c` | LwIP 收包线程 | LwIP 内部调用 | +| `ethernet_link_thread` | 256×4=1KB | — | `pl_net.c` | 链路状态监控 | 轮询 PHY 状态 | + +**栈大小设计原则**:CMSIS-RTOS V2 栈单位为 word(4 字节)。数字偏大: +- 512B(128 words)= CMSIS 最小栈,仅用 API 无局部数组 +- 1KB(256 words)= 标准栈,含少量局部变量 +- 2KB(512 words)= 大栈,含 `frame_msg_t`(~1052B)、CRC 计算、printf 等 + +### 任务间同步机制全景 + +``` +init_task ──────────────────────────────────────────────────────→ (osThreadExit 自我销毁) + ├─ dev_eth_start → pl_net_init → osThreadNew(ethernetif_input) + │ → osThreadNew(ethernet_link_thread) + └─ sw_board_init → app_dispatch_init → osThreadNew(frame_dispatch_task) + → dev_display_start → osThreadNew(scan_task) + +half_sec_task: osDelay(500) 循环 ──→ pl_iwdg_refresh / pl_gpio_write + +scan_task: + osEventFlagsWait(s_scan_evt) ←── TIM3 ISR: osEventFlagsSet + osKernelLock + NVIC_DisableIRQ(TIM4) (OE/LAT 原子窗口) + +frame_dispatch_task: + osMessageQueueGet(g_ch_queue) ←── rs485_task / rs232_task / udp_connect_task / ... + rb_lock(rb) → probe → rb_read → osMessageQueuePut(frame_queue[i]) + (取出的 channel_t * 先经 app_channel_get 回验,脏通知丢弃) + +协议处理任务 (iap/ldi/ah_mqtt): + osMessageQueueGet(frame_queue[i]) → 处理 → channel_send + +UART 通道任务 (rs485_task / rs232_task): + osMessageQueueGet(rx_queue, {block,offset,len}) ←── UART ISR 空闲中断 + app_channel_dispatch → rb_write → osMessageQueuePut(ch_queue) + +网络通道任务 (UDP/TCP): + osSemaphoreAcquire(sem) ←── pl_net_link_listener (链路断开) + netconn_recv → app_channel_dispatch → rb_write → osMessageQueuePut(ch_queue) +``` + +## OCP 虚表模式 + +项目中所有设备驱动和通道使用统一模式: + +```c +// 1. 基类第一个成员必须是 ops 指针 +struct dev_display { const dev_display_ops_t *ops; /* 通用参数 */ }; +struct dev_storage { const dev_storage_ops_t *ops; uint32_t capacity; }; +struct channel { uint8_t ch_id, state; const ch_ops_t *ops; }; + +// 2. 派生类将基类作为第一个成员,统一命名为 me +typedef struct { dev_display_t me; } dev_display_1_577_t; +typedef struct { dev_storage_t me; uint32_t base_addr, sector; } dev_flash_int_t; +typedef struct { channel_t me; pl_uart_handle_t uart; ... } rs485_ch_t; // app_rs485.c(rs232_ch_t 同构) +typedef struct { dev_w25qxx_t me; pl_spi_handle_t spi; ... } dev_w25qxx_inner; // (注:w25qxx直接在结构内) + +// 3. container_of 向上转型(Kernel/Inc/container_of.h) +#define container_of(ptr, type, member) ((type *)((char *)(ptr) - offsetof(type, member))) +``` + +四个核心虚表: + +| 虚表 | 定义位置 | 方法签名 | 实现者 | +|---|---|---|---| +| `dev_display_ops` | `dev_display.h` | `prepare(dev)` / `scan(dev,line)` / `set_row(row)` | `dev_display_1_577.c` / `dev_display_1_260.c` / `dev_display_1_969.c` | +| `dev_storage_ops` | `dev_storage.h` | `init/read/write/erase/capacity(dev,...)` | `dev_w25qxx.c` / `dev_flash_int.c` | +| `ch_ops` | `app_dispatch.h` | `send(ch, data, len)` | `app_rs485.c` / `app_rs232.c` / `app_udp.c` / TCP/MQTT 通道 | +| `proto_probe_fn_t` | `app_dispatch.h` | `probe(ch, rb, &total_len, &aux)` | `app_iap.c` / `app_ldi.c` / `app_qh_proto.c` / `app_rls.c` / `ah_mqtt.c` | + +## Display 子系统 (`Device/Display/`) + +### 基类 (`dev_display.h/.c`) + +```c +struct dev_display { + const dev_display_ops_t *ops; + // 模组参数 + uint8_t module_rows, module_cols, channels_per_module; + uint8_t modules_per_row, modules_per_col, scan_lines; + // 派生参数(由模组参数计算) + uint16_t screen_rows, screen_cols; + uint8_t total_channels; + uint16_t channel_pixels, scan_line_pixels, buffer_size; + // CCMRAM 缓冲区(派生实例静态分配) + uint8_t *pixel_map, *hub75_buff; + // 运行时 + volatile uint8_t light_level; // 0-7 + volatile bool dirty; +}; +``` + +通用 API:`set_pixel`、`fill`、`draw_bitmap`(边界检查+MSB-first 位图写入)、`set_brightness`。 + +### 现行模组 (`dev_display_1_577.c`,EIDE Debug 主例) + +单模块 64×32 像素,2 通道/模块,**3×3 拼屏 → 192×96** 屏幕,1/8 扫描(`MODULE_CODE "1000000577"`)。`dev_display_1_260.c` 为 Makefile 默认模组;`dev_display_1_969.c` 为备用模组(CCM 占用同量级 38208B);`dev_display_p20.c` 未编入任何构建,仅保留。 + +`g_bsrr[TOTAL_CHANNELS][8]` CCMRAM 预计算查表:每通道×8 色阶 = `_1_577_bsrr_t`(含 r/g/b 三组 `pl_hub75_bsrr_t`),模组 init 时根据 HUB75 引脚定义填入。扫描时直接查表输出,无需分支。 + +### 扫描架构 + +``` +TIM3 (行同步, 频率=刷新率×scan_lines) + └─ ISR: osEventFlagsSet(s_scan_evt) + └─ scan_task (osPriorityRealtime, 最高优先级, 不可抢占): + 1. osEventFlagsWait(s_scan_evt) ← 阻塞,零 CPU 占用 + 2. if(dirty) ops->prepare(dev) ← pixel_map → hub75_buff + 3. ops->scan(dev, scan_line) ← BSRR 查表 + CLK 脉冲 + 4. OE/LAT 原子窗口 ← osKernelLock + 关TIM4中断 + 5. scan_line = (scan_line+1) % scan_lines + +TIM4 (PWM, 频率=TIM3×8, 8级亮度): + └─ ISR: OE=(pwm_cnt >= light_level), pwm_cnt=(pwm_cnt+1)&7 +``` + +**实时性保证**: +- `scan_task` 设为 `osPriorityRealtime`,不会被任何用户任务抢占 +- OE/LAT 窗口内 `osKernelLock()` + `NVIC_DisableIRQ(TIM4_IRQn)` 防止 TIM4 抢占破坏 OE 时序 +- `prepare` 在 OE/LAT 窗口外完成(off critical path),扫描本身只查表 + +### 颜色系统 + +```c +typedef enum { COLOR_BLACK=0, COLOR_RED=1, COLOR_GREEN=2, COLOR_YELLOW=3, + COLOR_BLUE=4, COLOR_PURPLE=5, COLOR_CYAN=6, COLOR_WHITE=7 } display_color_t; +``` +颜色值直接写入 `pixel_map[]`,在 prepare 阶段重新映射——像素重排后写入 `hub75_buff[]`,扫描时查 `g_bsrr` 得到三通道 BSRR 输出值。 + +## IO 子系统 (`Device/IO/`) + +### 按键设备 (`dev_key.c`) + +两阶段初始化:`hw_dev_initcall` 注册 EXTI 下降沿回调,`sw_dev_initcall` 创建 TEST 信号量。 + +| 按键 | GPIO | 类型 | 检测方式 | 用途 | +|---|---|---|---|---| +| SW1 | PE12 | 干接点 | EXTI ↓ + `app_key` 轮询释放 | 通用输入 | +| SW2 | PE11 | 干接点 | EXTI ↓ + `app_key` 轮询释放 | 通用输入 | +| SW3 | PE10 | 干接点 | EXTI ↓ + `app_key` 轮询释放 | 通用输入 | +| KEY_TST | PD8 | 测试键 | EXTI ↓ → 信号量(一次性消耗) | `app_key_test_pressed()` | +| DIP1 | PE7 | 拨码开关 | `app_key` 纯轮询 (3 次一致去抖) | 波特率选择:ON=115200 / OFF=9600(`app_uart_baud`,RS232+RS485 同步) | +| DIP2 | PE8 | 拨码开关 | `app_key` 纯轮询 (3 次一致去抖) | 配置选择 | + +所有按键 GPIO 内部上拉,低有效(按下=0)。 + +**数据流**: +``` +硬件按下 → EXTI ISR → dev_key_get_state=标记为 true + ↓ +app_key key_poll_task (20ms) → 轮询 GPIO 实际电平确认释放 + → dev_key_clear_state() 清除标记 + → 更新本地去抖缓存 +``` + +外部通过 `app_key_get_state(DEV_KEY_SW1)` 获取去抖后的稳定状态。`_app_key_init()` 由 `sw_app_initcall` 自动创建轮询任务。 + +### IO 控制 (`dev_io_ctrl.c`) + +`hw_dev_initcall` 初始化 GPIO 输出(PD14 车道灯、PD15 闪光灯),确保启动时均为低电平。 + +| 函数 | GPIO | 功能 | +|---|---|---| +| `dev_io_lane_light(bool)` | PD14 | 车道灯开关 | +| `dev_io_flash_light(bool)` | PD15 | 黄闪灯开关 | + +### 光传感器 (`dev_light_sensor.c`) + +ADC1 采样光敏电阻分压(LDR:光越暗→电阻越大→ADC 值越高),8 次均值滤波后映射为亮度等级。`dev_light_sensor_auto_adjust()` 自动更新 `display->light_level`。 + +**亮度范围限制**:`light_sensor_dev_t` 含 `min_level`/`max_level` 字段(默认 1~7),`dev_light_sensor_read()` 输出前按范围钳位。`app_light_sensor_init()`(`sw_app_initcall`)上电时调用 `dev_light_sensor_set_range(4, 7)`,将光敏自动调光限制在 4~7 级(满足出厂最低/最高亮度要求)。外部可通过 `app_light_sensor_set_range(min, max)` 运行时动态调整。 + +`app_light_sensor_init`(`sw_app_initcall`)创建 1 秒周期的自动调节任务。 + +## Storage 子系统 (`Device/Storage/`) + +OCP 虚表,上层通过便捷内联调用(如 `dev_storage_read(d, addr, buf, len)` 自动解引用 `d->ops->read`): + +### W25Qxx (`dev_w25qxx.c`) + +SPI NOR Flash,JEDEC 自动识别容量(W25Q16~256+),>128Mb 自动 4 字节地址。 + +**半双工 SPI 读流程**(`_read`): +``` +1. pl_spi_transmit(cmd, addr_len+1) 阻塞发 Read 命令 + 地址 +2. pl_spi_receive_dma(buf, len) DMA 数据→buf,CPU 不参与搬运 +3. while(!s_ok) osDelay(1) 轮询 DMA 完成标志 +4. _cs_high() SPI 释放 +``` + +- DMA 回调 `_dma_cb`:`s_ok=true` + `osEventFlagsSet`(用于定时等待的场景) +- 首次 `_read` 时懒初始化 `s_evt` + 注册 DMA 回调 +- 写操作使用读-改-写模式(`_read` 整扇区→修改→`_write_no_check` 整扇区),需先擦除非空扇区 + +### 内部 Flash (`dev_flash_int.c`) + +OCP 虚表实现,提供 `flash_int_ops`(`init/read/write/erase`),由各配置模块绑定到具体实例。 + +| 实例 | 绑定模块 | 基地址 | 大小 | initcall | +|---|---|---|---|---| +| `g_board_cfg_flash` | `Application/Src/Config/app_board_net_cfg.c` | `0x08004000` (Sector 1) | 16KB | `hw_dev_initcall` | +| (LDI 已迁移) | `Application/Src/LDI/app_ldi_cfg.c` | W25Qxx 最后 4KB 扇区 | 4KB | `sw_dev_initcall` (需要 W25Qxx 先就绪) | + +**内部 Flash ops 实现**: +- `_read`:直接内存映射 `memcpy(buf, (void*)(base+addr), len)` +- `_write`:`pl_flash_unlock → pl_flash_program_word × N → pl_flash_lock`(按 word 编程) +- `_erase`:`pl_flash_erase_sector(sector, voltage)` + +**板级系统配置** (`Application/Src/Config/app_board_net_cfg.c`):`app_board_sys_info_t` 记录(72B,方案 B 2026-08-21)= magic(4) + update_sta(4) + FWInfo(40) + NetConfig(20) + CRC32(4),布局由头文件 `static_assert` 锁定(与 Bootloader/Recovery 二进制兼容)。NetConfig 字段语义:`ip/mask/gw` 两套口共享;`port` = **TCP 业务口**(LDI 0AH / IAP 0x01 报 0x02 写 / TCP Server 监听,出厂默认 9528);`udp_port` = **CQ UDP 业务口**(CQ setip 写 / CQ UDP 通道读,出厂默认 20103)。网络配置经 `app_board_net_cfg_get` / `app_board_net_cfg_update(ip,mask,gw,port,udp_port)` 读写(LDI 0AH / IAP 4B02 写 port 保留 udp_port;CQ setip 写 udp_port 保留 port),FWInfo/update_sta 经 `app_board_net_cfg_read` 整记录读取(IAP 命令用)。**升级兼容**:旧 68B 记录按新 72B 布局重算 CRC 必失配 → 走空/损坏自愈路径重写(网段配置丢失一次,由 ldi_ctx_init W25 镜像回写或 app_net_boot 默认落盘恢复);**反向同理**——设备上旧 68B 布局 Bootloader/Recovery 读新 72B 记录同样失配 → 反复重建旧布局出厂记录,**方案 B 后三固件必须同步烧录(`tool/flash_all.sh` 一次会话),否则 Sector1 两布局交替重建、设备可能陷 Recovery(网络全部无响应)**(2026-08-21 纪律强化)。 + +**LDI 配置** (`Application/Src/LDI/app_ldi_cfg.c`):已迁移至 W25Qxx 最后一个 4KB 扇区。`app_flash_ldi_record_t`(116B)= magic(4) + cfg(106) + padding(2) + CRC32(4)。`_app_flash_ldi_storage_init`(`sw_dev_initcall`)中 `s_ldi_base = dev_storage_capacity(w25) - 4096`,通过 `dev_w25qxx_get()` 获取存储句柄。 + +> **解耦已落地(2026-08-14)**:LDI 与 IAP 协议均经中立模块 `app_board_net_cfg`(`Application/Config`)读写 Sector 1 网络配置,互不依赖对方协议目录;`app_iap_cfg.{c,h}` 与死代码 `Device/Inc/config_info.h` 已删除。守卫语义(空/损坏自愈、升级中间态仅放行 net_cfg 字段更新、同值跳过)见 `doc/07_LDI与IAP配置解耦`。 + +## Render 引擎 (`Application/Src/app_render.c`) + +数据驱动字库引擎,字库存储在 W25Qxx Flash 中,模块 `sw_app_initcall` 自注册。 + +### 字库组织 +- 字库 = **(字号, 编码, 字型)** 三元组,由 `font_chip_config_t` 芯片配置描述(`s_chip_configs[]`,当前激活 W25Q64;区块表按芯片配置动态取用,非固定 40 项全局表) +- `_packed_glyph_bytes(size, charset)` = 每字符打包字节数:ASCII `size×((size/2+7)/8)`,GBK `size×((size+7)/8)` +- `_glyph_width_px(size, charset)` = 字符像素宽:ASCII `size/2`,GBK `size` +- `_find_region(size, charset, type)` = 在当前激活芯片配置的区块表中按三元组查找(未命中回退首项) +- `_flash_addr(size, charset, type, ch)` = 单字符 Flash 绝对地址:`char_idx×bpc` 后按 sec/page/byte 分步计算,含芯片级 sec/page/byte 修正(ASCII 原始码/减 0x20、GBK 190 列/94 列按芯片配置区分) + +### Tagged Union API + +```c +app_render(&(render_cfg_t){ + .type = RENDER_TEXT, .x=0, .y=0, .w=128, .h=16, .color = COLOR_GREEN, + .text = "你好", .len = strlen("你好"), + .font_size = FONT_16, .font_type = FONT_ST, .text_enc = FONT_ENC_UTF8, +}); +``` + +- `RENDER_TEXT`:UTF8→GBK→逐字 Flash 地址→`dev_storage_read`→`dev_display_draw_bitmap`。支持 `word_wrap` 换行,不换行时截断 +- `RENDER_BITMAP`:直接调用 `dev_display_draw_bitmap` +- `RENDER_FILL`:`w=h=0` 全屏填充,否则矩形填充 +- 渲染风格 `render_style_t`:`h_align` / `v_align` / `word_wrap`(文字专属)。测量趟+渲染趟两阶段,支持逐行独立水平对齐(左/居中/右)。 + +## Application 模块全景 + +| 模块 | 类型 | 通道绑定 | initcall | 职责 | +|---|---|---|---|---| +| `app_dispatch` | 框架 | — | `sw_app_initcall` | 协议调度引擎 | +| `app_render` | 引擎 | — | `sw_app_initcall` | 字库渲染 | +| `app_iap` | 协议 | UDP(RJ45 共享 RB) | `sw_app_initcall` | IAP 固件升级帧处理 | +| `app_ldi` | 协议 | TCP_SERVER / TCP_CLIENT / UDP(RJ45 共享 RB) | `sw_app_initcall` | LDI 显示控制协议 | +| `app_qh_proto` | 协议 | RS485 + RS232 | `sw_app_initcall` | 青海高速费显协议 | +| `app_sc_etc` | 协议 | RS485 + RS232 | `sw_app_initcall` | 四川 ETC 费显协议(0A 帧族;心跳超时显示已停用) | +| `app_sc_mtc` | 协议 | RS485 + RS232 | `sw_app_initcall` | 四川 MTC 费显协议('{' 方案二 + 0A 46 查询 + 7B 40~45) | +| `app_sc_ol` | 协议 | RS485 + RS232 | `sw_app_initcall` | 四川治超屏协议(FF+len 帧族,BCC 异或) | +| `app_yn_proto` | 协议 | RS485 + RS232 | `sw_app_initcall` | 云南费显协议 | +| `app_cq_proto` | 协议 | UDP_CQ(业务 20103)+ UDP(搜索 10011,RJ45 共享 RB) | `sw_app_initcall` | 重庆高速二代费显协议(JSON `{` + 12B 二进制;队列/缓冲置 CCMRAM;**心跳故障屏开关 `CQ_FAULT_SCREEN`(1 开 / 0 关):PROTO_CHONGQING 默认开、共存构建默认关(他省上位机不发 syn1),可 `-DCQ_FAULT_SCREEN=1/0` 覆盖,见 doc/03 PartB B.7.2;旧名 CQ_FAULT_SCREEN_ENABLED / CQ_FAULT_SCREEN_FORCE_ON 已废弃**) | +| `app_uart_baud` | 应用 | — | `sw_app_initcall` | DIP1 波特率选择与运行态切换(RS232+RS485) | +| `app_rls` | 协议 | RS485 | `sw_app_initcall` | RLS 重庆高速二代费显协议 | +| `app_vms_ctrl` | 协议 | (LDI 子模块) | — | VMS 情报板控制(LDI→Render 桥接) | +| `ah_mqtt` | 协议 | MQTT | (已注释) | AH 平台 MQTT(签到/状态上报/指令) | +| `app_mqtt` | 通道 | CH_ID_MQTT | — | MQTT 网络传输通道 | +| `app_udp` | 通道 | CH_ID_UDP | — | UDP 广播通道(端口 10011) | +| `app_udp`(CQ 实例) | 通道 | CH_ID_UDP_CQ | — | CQ 业务口 UDP 通道(`PROTO_CHONGQING` 读 Sector1 net_cfg.udp_port 默认 20103(方案 B 2026-08-21),dev 共存构建固定 20103;`app_udp_cq_start`/`app_udp_cq_broadcast`) | +| `app_tcp_server` | 通道 | CH_ID_TCP_SERVER | — | TCP 服务器通道 | +| `app_tcp_client` | 通道 | CH_ID_TCP_CLIENT | — | TCP 客户端通道 | +| `app_rs232` | 通道 | RS232(USART3) | — | RS232-0 通道启动(USART6 语音仅 TX,不启通道) | +| `app_rs485` | 通道 | RS485 | — | RS485 UART 通道启动 | +| `app_key` | 应用 | — | `sw_app_initcall` | 按键轮询去抖 (20ms) | +| `app_light_sensor` | 应用 | — | `sw_app_initcall` | 光传感器自动亮度 (1s 周期) | +| `app_boot` | 编排 | — | — | RTOS 启动编排 | +| `app_net_boot` | 应用 | — | — | 网络配置横切应用(Sector1 net_cfg → netif + TCP Server 口;空/损坏/非法写本构建默认落盘并应用,两口径共用) | +| `app_default_display` | 应用 | — | — | 注册制默认显示界面(协议 sw_initcall 注册回调则用之,否则默认欢迎画面) | +| `app_test` | 测试 | — | — | 硬件测试用例 | + +**类型说明**: +- **协议**:注册 probe 到 dispatch,创建处理任务,从 frame_queue 收帧 +- **通道**:实现 `ch_ops->send`,收包→`app_channel_dispatch`,发包→网络/UART +- **框架/引擎**:供协议模块调用,不直接通信 +- **应用**:硬件相关业务逻辑,不涉及通信协议 + +**当前运行状态**:`app_boot` 的 `init_task` 显式启动五个通道 — TCP Server、TCP Client、UDP、RS485、RS232。`app_rs232_1_start()`(USART6 语音 TX 桩)已注释;`app_mqtt` 的 start 函数已定义(`static inline`)但未被调用;`ah_mqtt` 的 initcall 已注释(未激活)。 + +## 协议分发层 (`Application/Src/app_dispatch.c`) + +### 核心数据结构 `dispatch_ctx_t` + +| 字段 | 类型 | 用途 | +|---|---|---| +| `registered_mask` | `uint32_t` | 已注册协议位掩码(`app_proto_register` 自动分配 2 的幂) | +| `proto_rb[i]` | `ring_buffer_t *[PROTO_MAX_COUNT]` | 协议→缓冲区 | +| `proto_probe[i]` | `proto_probe_fn_t [PROTO_MAX_COUNT]` | 协议→探测函数 | +| `frame_queue[i]` | `osMessageQueueId_t [PROTO_MAX_COUNT]` | 协议→帧队列 | +| `buf_pool[id]` | `ring_buffer_t *[RB_CNT_MAX]` | 缓冲区池(=3 槽:RJ45/RS485/RS232,懒初始化) | +| `ch_queue` | `osMessageQueueId_t` | 通道通知队列 | +| `ch_proto_map[ch_id]` | `proto_mask_t [CH_ID_MAX]` | 通道→协议掩码 | +| `channels[ch_id]` | `channel_t *[CH_ID_MAX]` | 通道注册表 | + +### 数据流 + +**接收**:通道任务 → `app_channel_dispatch(ch,data,len)` → `rb_write` → `osMessageQueuePut(ch_queue,&ch)` → `frame_dispatch_task` → probe → `rb_read` → `osMessageQueuePut(frame_queue[i],&msg)` → 协议任务 + +**发送**:协议任务 → `channel_send(ch,data,len)`(入口回验 `app_channel_get(ch->ch_id)==ch`,防悬垂通道指针)→ `ch->ops->send(ch,data,len)` → UART DMA / LWIP netconn + +### 多协议共享 RB(doc/05 核心成果) + +- **一物理通道一 RB**:RJ45 1536 / RS485 768 / RS232 768(`Application/Inc/app_dispatch.h` `_Static_assert` 在册)。网口逻辑通道(TCP Server/Client/UDP/MQTT)共享 RJ45 槽。 +- **RB_PROVIDE_WEAK 编译期按需提供**:协议 TU 以 `RB_PROVIDE_WEAK(rb_provide_xxx, size)` 提供 RB 体;未编入任何协议的槽为 NULL(`app_proto_acquire_buf` 返回 nullptr)。 +- **同 RB 写去重**:`app_channel_dispatch` 用 `seen[]` 指针去重,多协议共享同一 RB 时只写一次。 +- **链式 probe 契约**:READY/SKIP 消费后取下一帧;WAIT/FAKE 继续试下一协议;一轮无消费时 any_wait 停等更多字节 / any_fake 跳 1 字节重同步。 +- **probe 首字节快拒**:探测函数首字节不匹配即返回 FAKE,禁止盲 WAIT 阻塞链式探测。 + +**帧 queue 深度(静态 SRAM 队列,不占 ucHeap)**:IAP 2 / LDI 4 / QH 3 / RLS 2 / AH_MQTT 3 / SC_ETC 3 / SC_MTC 3 / SC_OL 3 / SD 3 / GZ 3 / CQ 3(**CQ 队列体置 CCMRAM**,见上)。 + +### 新增协议步骤 + +1. 定义 `proto_probe_fn_t` 探测函数,返回 `PROTO_PROBE_READY/WAIT/SKIP/FAKE` +2. 协议模块 init 函数中:`app_proto_acquire_buf`→`app_proto_register`→`app_proto_bind_channel`(可多次)→`osThreadNew` +3. 协议任务中:`osMessageQueueNew`→`app_proto_set_frame_queue`→循环 `osMessageQueueGet`→处理 +4. `sw_app_initcall(module_init)` 自注册 + +**新协议接入的完整规则、模块骨架、串口/网络专项与 step-by-step 检查清单见 `doc/08_协议模块接入规则/`**(本列表仅为摘要;RB/绑定/兼容矩阵权威仍为 doc/05,内存占用账仍为 doc/06)。 + +### 通道标识映射 + +| ch_id | 枚举 | 传输层 | 实现类 | 绑定文件 | +|---|---|---|---|---| +| 0 | `CH_ID_RS485` | UART (USART1) | `rs485_ch_t` | `app_rs485.c`(buf 在 `dev_rs485.c`) | +| 1 | `CH_ID_RS232` | UART (USART3) | `rs232_ch_t` | `app_rs232.c`(buf 在 `dev_rs232.c`) | +| 2 | `CH_ID_TCP_SERVER` | LwIP TCP | `tcp_server_channel_t` | `app_tcp_server.c` | +| 3 | `CH_ID_TCP_CLIENT` | LwIP TCP | `tcp_client_channel_t` | `app_tcp_client.c` | +| 4 | `CH_ID_UDP` | LwIP UDP | `udp_channel_t` | `app_udp.c` (端口 10011) | +| 5 | `CH_ID_MQTT` | LwIP MQTT | `mqtt_channel_t` | `app_mqtt.c` | +| 6 | `CH_ID_RS232_1` | UART (USART6,仅语音 TTS 旁路 TX) | `dev_rs232_voice`(直接 `pl_uart_send`) | 无通道任务、无 DMA RX,禁止协议 bind | +| 7 | `CH_ID_UDP_CQ` | LwIP UDP(业务口,`PROTO_CHONGQING` 读 Sector1 net_cfg.udp_port 默认 20103(方案 B);dev 共存构建固定 20103) | `udp_channel_t`(CQ 实例) | `app_udp.c`(`app_udp_cq_start`;CQ 协议 bind,JSON 业务 + 12B 二进制) | + +**选编口径**:EIDE Debug 编 1-969 模组 + IAP/LDI/四川三协议(`ProtocolParser_SiChuang_{ETC,MTC,Overload}`)/重庆CQ(`ProtocolParser_ChongQing` + cJSON,Debug 未排除),排除 1-260/1-577/RLS/AH/山东/贵州/青海/云南(`.eide/eide.yml` excludeList;LDI 与 CQ 同编为开发共存口径:CQ 业务口 dev 构建固定 20103 不读 Sector1 net_cfg.udp_port,10011 口 12B 二进制帧先经 LDI probe,LDI 对 `data_len==2`(CQ 12B 帧 len=00 00 00 02 且 CRC 恰好通过 LDI 校验)显式 FAKE 放行、仍由 CQ 认领,见 doc/03 PartB Q23/Q24;量产 CQ 目标需 excludeList 排除 ldi 并 define PROTO_CHONGQING);Makefile 编 1-260 + 全协议(含 CQ,`PROTO=CQ` 剔除 LDI 并追加 -DPROTO_CHONGQING)。详见 doc/05,内存占用见 doc/06。 + +## UART 通道子系统 (`Device/Comm/` + `Application/Src/Channel/`) + +RS485/RS232 按板级资源(Device 层)与通道生命周期(Application 层)分层,全库无统一 UART 通道抽象类型。 + +**Device 层(板级静态资源)**: +- `dev_rs485.c`(USART1,RE=PA8):静态 DMA 乒乓双缓冲 `s_rs485_buf[2×RS485_BUF_SIZE=1280B]`(`Device/Inc/dev_rs485.h`,块 640×2)+ RE 方向回调 `rs485_dir_cb`(`pl_uart_set_dir_cb` 注入,`hw_dev_initcall`)。 +- `dev_rs232.c`(USART3):仅 `dev_rs232_get_buf(index)` — index0 返回 1280B 乒乓缓冲(`RS232_BUF_SIZE`=640 块 ×2),index1(USART6)返回 `nullptr`(无协议 RX)。 +- `dev_rs232_voice.c`(USART6):语音板 TTS 专用 TX。`dev_rs232_voice_play` / `dev_rs232_voice_volume` 组帧后经 `pl_uart_send(PL_UART6)` 发送,不经过 dispatch 框架。 + +**Application 层(通道生命周期 + 任务循环)**: +- `app_rs485.c`:`rs485_ch_t`(`channel_t me` + `uart/rx_queue/rx_buf`),`rs485_task` 循环 `osMessageQueueGet(rx_queue, {块号,偏移,长度})` → 按块拷贝 → `app_channel_dispatch`。`app_rs485_start()` 注册通道 + 注入 RX 回调 + `pl_uart_start_rx` + 创建任务(栈 256×4)。 +- `app_rs232.c`:同构(`rs232_ch_t` / `rs232_task`,栈 256×4)。`app_rs232_1_start()` 为桩函数(返回 `nullptr` — USART6 语音 TX 不经通道任务)。 +- `app_boot` 中 `app_rs485_start()` / `app_rs232_start()` 创建任务,`app_rs232_1_start()` 已注释。 + +**波特率选择(DIP1)**:`app_uart_baud.c`(`sw_app_initcall`)读 `dev_key_get_state(DEV_KEY_DIP1)`(PE7,active_low):ON=115200、OFF=9600,经 `pl_uart_set_baud` 同步重配 USART1(RS485)+ USART3(RS232)。sw initcall 早于 `app_rs485_start`/`app_rs232_start`,切换时 DMA RX 未挂,无竞态;运行态切换(MTC 7B 40 命令)由 `pl_uart_set_baud` 内部停/重挂 DMA RX。DIP2(PE8)为 app_render 字库芯片选择,不得改动。 + +## IAP 协议 (`Application/Src/IAP/app_iap.c`) + +固件升级协议,**仅 UDP**(`CH_ID_UDP`,RJ45 共享 RB)。帧格式 `0x5A5A5A5A (4B) | seq (4B) | cmd (4B) | len (4B) | data | CRC32 (4B)`(`app_iap.h`),probe 首字节 `0x5A` 快拒。 + +- `iap_handle_task`:协议处理任务(栈 256×4=1KB,帧缓冲 static;原 2KB 偏大),循环 `osMessageQueueGet` → 帧解析 → 命令分派 +- `IAP_QUEUE_DEPTH=2`,静态 SRAM 队列(不占 ucHeap) +- 命令表 `g_iap_cmd_table[]`(`app_iap_cmd.c`):0x00 Test / 0x01 上报 IP 配置 / 0x02 强制修改 IP(写 Flash)/ 0x03 上报固件版本·大小·CRC32·升级状态(version 由主固件启动时经 `app_board_net_cfg_fw_version_update` 从 PROGRAM_CODE 落库 Sector1 `app_info.version`,见 doc/07 §13)/ 0x04 准备升级 / 0x05 发送升级包 / 0x06 进 Recovery(写 RTC 备份寄存器标志)/ 0x07 软复位 +- **0x03 应答 version 字节序**:载荷 11 word(ReData[0]=size、[1]=crc32、[2..9]=version[32] ASCII、[10]=update_sta);version 按**大端 word 构造**(对齐 0x01 IP 约定:`v[4i]<<24|v[4i+1]<<16|v[4i+2]<<8|v[4i+3]`),存储侧保持纯 ASCII,禁 memcpy 裸拷(否则上位机按 4 字节一组反转显示)。主固件与 Recovery `cmd.c` 同构。 +- `iap_probe_frame`:验证帧头 + 长度(≤256)+ CRC32,返回 READY/WAIT/FAKE + +**0E 远程升级协议文档待完善项**(2026-08-14,协议文档与固件实现的一致性缺口,需在协议文档侧补齐): + +| # | 缺口 | 现状 | +|---|------|------| +| 1 | **4B02 应答帧结果码** | 协议文档当前版本无此定义;固件已按扩展实现:len 0→1,1 word 结果码(0=成功含同值跳过,1=擦写错误),借鉴 4B04 应答 0/1 先例。老上位机按「B402 无载荷」解析时会多读一个 word,混合部署需联调验证(详见 doc/07 §10) | +| 2 | **4B06「进 Recovery」应答后重启** | 协议要求应答后重启;主固件实现只写 RTC 备份寄存器标志(`FLAG_FORCE_UPDATE`)未复位,实际复位由后续流程触发——协议与实现出入 | +| 3 | **4B04「准备升级」应答结果码** | 协议文档有 0/1 结果码先例;主固件实现应答无载荷(len=0),未按文档回结果码 | +| 4 | **LDI 0AH 失败码 01H** | 实现已使用(00H=成功、01H=失败:W25 保存或 Sector1 同步任一步失败即 01H),协议文档需明确该语义(与 0BH / `ldi_status_rsp_t` 既有约定一致) | + +## LDI 协议 (`app_ldi.c` + `app_ldi_cmd.c`) + +车道设备指示器协议,**仅网口**:`CH_ID_TCP_SERVER / CH_ID_TCP_CLIENT / CH_ID_UDP` 三通道各独立 mask,共用 RJ45 RB 与 `g_ldi_msg_queue`(`LDI_QUEUE_DEPTH=4`)。帧 STX `0xFF 0xFF`(`app_ldi.h` 固定值)+ CRC-16/XMODEM 校验;`LDI_PAYLOAD_MAX=512`。 + +- `ldi_handle_task`:协议处理任务(栈 256×4=1KB,帧缓冲 static;原 2KB 偏大) +- `ldi_timer_task`:周期性状态上报(栈 256×4=1KB;原 2KB 偏大) +- 13 种设备类型(`Application/Inc/LDI/app_ldi.h`,`LDI_DEV_TYPE_COUNT=13`):RSU 0xE1 / LPR 0xE2 / VTR 0xE3 / 栏杆机 0xE4 / 车检 0xE5 / 显示屏 0xE6 / 信号灯 0xE7 / 报警器 0xE8 / VMS 0xE9 / 雨棚灯 0xEA / 雾灯 0xEB / 语音 0xF4 +- `ldi_ctrl_xxx_t`:每种设备类型的控制载荷结构体(`app_ldi_cmd.h`) +- `app_vms_ctrl`:VMS 情报板子模块,将 LDI 命令翻译为 `app_render()` 调用 +- 复合指令模块:支持多设备类型组合控制(1BH 命令) + +## 青海协议 (`Application/Src/ProtocolParser_QingHai/app_qh_proto.c`) + +青海高速费显协议。帧定界 `'{'...'}'`(len 字段 1B → 帧 ≤259),`QH_PAYLOAD_MAX=259`、`QH_QUEUE_DEPTH=3`(静态 SRAM)。`RB_PROVIDE_WEAK` 提供 RS485+RS232 双 RB,绑定 `CH_ID_RS485` + `CH_ID_RS232`(`sw_app_initcall` 自注册 `qh_proto_init`,与 IAP/LDI 同经链式 probe)。语音命令经 `dev_rs232_voice` 旁路 USART6(`app_qh_proto_voice.c`)。 + +## 山东协议 (`Application/Src/ProtocolParser_ShanDong/app_sd_proto.c`) + +山东车道费额显示器通信协议(协议文档编号 39)。帧 `'{' + 命令字('1'~'5','7','8') + 二进制 len + 参数 + '}'`(无 BCC;**与青海完全同构**),`SD_PAYLOAD_MAX=259`、`SD_QUEUE_DEPTH=3`(静态 SRAM,与青海同 801B)。绑定 `CH_ID_RS485` + `CH_ID_RS232`(`sw_app_initcall` 自注册 `sd_proto_init`)。目标屏幕 192×96(FONT_16 6 行,协议行号 1~5 全覆盖)。 + +- `'1'` 全屏单色(01红/02绿/03黄)→ 整屏 `dev_display_fill`;`'2'` 取版本号 → 裸 ASCII 应答 PROGRAM_CODE(协议未定义应答格式);`'3'` 单行(颜色'0'~'2' + 行号'1'~'5' + GBK 文本,先清行再渲染);`'4'` 全屏可编辑(颜色 + X/Y 坐标 + 文本,先清屏后整屏 word_wrap,0x0A 回车由渲染引擎换行、0x0D 被跳过);`'5'` 清屏;`'7'` 亮度('0'~'5':0=恢复光敏任务自动调光,1~5=挂起光敏任务 + 硬件档 {3,4,5,7,8});`'8'` 外设(bit0 绿灯/bit1 红灯/bit2 黄闪报警,红灯优先,PD14/PD15)。无 `'6'` 命令。除 `'2'` 外协议未定义应答 → 不回(对齐青海单向模式)。 +- **帧头冲突纪律**:命令字 '1'~'5','7','8' 全部落入青海 probe 命令集('1'~'9','A','B'),全协议 Makefile 构建下青海 probe 先注册(源码收录序 qh 在前)先认领山东帧——'3'/'4'/'5' 语义与青海巧合一致,'1'/'2'/'7'/'8' 语义分歧(山东 '1'=全屏单色 vs 青海 '1'=主机查询;'2'=版本 vs 自检;'7'=亮度 vs 文明语音;'8'=外设 vs 亮度)。**量产必须 EIDE 目录排除与青海/四川MTC 互斥**(doc/05-01 §6);当前 `.eide/eide.yml` Debug 目标已排除山东目录(与青海/四川MTC 同列,Debug 保留贵州),山东量产目标须启用山东并排除青海+MTC。编译期互斥守卫 `g_brace_proto_guard` 链接期兜底强制(EIDE 多 `{` 族编入即 `multiple definition` 报错;Makefile 全协议构建经 `-DSTD_ALL_PROTO` 豁免)。 +- 上电画面「山东省 高速公路 欢迎您」已实现:`app_sd_proto_default.c` 经 `app_default_display_register` 注册(`sw_app_initcall`),显示「山东省 高速公路 欢迎您」(FONT_16 居中黄字;UTF-8 字面量,`FONT_ENC_UTF8` 渲染)。 + +## 贵州协议 (`Application/Src/ProtocolParser_GuiZhou/app_gz_proto.c`) + +贵州常规费显协议(协议文档 06 贵州常规费显协议,2020-06-17 修订)。帧 `'{' + 命令字('1'~'9','A','B',0x01,0x02) + 二进制 len + 参数 + '}'`(无 BCC;**与青海完全同构**),`GZ_PAYLOAD_MAX=259`、`GZ_QUEUE_DEPTH=3`(静态 SRAM,与青海同 801B)。绑定 `CH_ID_RS485` + `CH_ID_RS232`(`sw_app_initcall` 自注册 `gz_proto_init`)。13 命令:'1' 主机查询(仅回「正常」帧 `7B 31 01 00 7D` 至源通道)、'2' 自检(黄色全屏 + 语音「系统正在加电自检」)、'3' 单行(len 3~18,先清行再渲染)、'4' 全屏可编辑(len 4~86,**按 X/Y 坐标渲染** word_wrap=true,len>71 文本截断 64)、'5' 清屏、'6' 固定格式(客/货行文裸机对齐:客车 车型/金额/余额/信息1,信息2 不显示;货车 有超重→金额/余额/总重/超重、无超重→车型/金额/余额/总重;金额 ≥0.5 元同步费额语音;金额/重量整数运算分/吨分无浮点)、'7' 礼貌用语语音('0'~'3','0'=「您好!欢迎行驶贵州高速公路」)、'8' 亮度(0=恢复光敏任务,1~5=挂起光敏 + 硬件 {3,4,5,7,8})、'9' 音量(1~5 → 语音板 {1,3,5,7,9})、'A' 外设(bit0 绿/bit1 红/bit2 黄闪,**红优先**)、'B' 费额语音(金额 ASCII 串→分,≥0.5 元播报,不显示)、0x01 全屏点亮(01红/02绿/**03黄**)、0x02 版本号(串口回裸 ASCII PROGRAM_CODE + 屏幕红字「版本:PROGRAM_CODE」FONT_SELF_ADAPT 居中)。无上电画面(不建 default 文件)。 +- **帧头冲突纪律**:命令字 '1'~'9','A','B' 全部落入青海 probe 命令集(**完全重叠**),全协议 Makefile 构建下青海 probe 先注册(源码收录序 qh 在前)先认领贵州帧——'1'/'3'/'4'/'5'/'7'/'8'/'9'/'B' 语义基本一致,'2'/'6'/'A' 细节差异(贵州 '2'=自检黄屏+语音、'6'=固定格式行文不同、'A'=红优先)。**量产必须 EIDE 目录排除与青海互斥**(doc/05-01 §6);当前 `.eide/eide.yml` Debug 目标已启用贵州(排除青海/山东/四川MTC)。编译期互斥守卫 `g_brace_proto_guard` 链接期兜底强制(EIDE 多 `{` 族编入即 `multiple definition` 报错;Makefile 全协议构建经 `-DSTD_ALL_PROTO` 豁免)。0x01/0x02 二进制命令字青海/山东/四川MTC probe 首命令字快拒,无冲突。金额串转分 `gz_amount_to_fen`(整数部分×100 + 小数前两位,`%.2f` 语义)。宿主推演 `~/EnvTools/CD-DebugTool-cpp/scripts/probe_sim/gz_frame_sim.py`。 + +## 云南协议 (`Application/Src/ProtocolParser_YunNan/app_yn_proto.c`) + +云南常规费显协议(协议文档 01 云南常规费显协议-云南LED费显P5,2022.7.5,云南弥玉项目 2022-S134)。帧 `'{' + 命令字('1'~'9','A','B',0x01,0x02) + 二进制 len + 参数 + '}'`(无 BCC;**与青海/贵州完全同构**),`YN_PAYLOAD_MAX=259`、`YN_QUEUE_DEPTH=3`(静态 SRAM,与青海同 801B)。绑定 `CH_ID_RS485` + `CH_ID_RS232`(`sw_app_initcall` 自注册 `yn_proto_init`)。13 命令:'1' 主机查询(回「正常」帧 `7B 31 01 00 7D` 至源通道,恒回正常)、'2' 自检(**复用 app_factory_test.c 老化循环显示序列**——整屏单字居中,字号 {FONT_16/24/32}×字体 {ST/FS/KT/HT} 循环「重庆创迪科技发展有限公司设备老化测试」,每 5s 播报「系统正在自检」,一次性任务 `yn_selftest_task` 防重入,**可被下一帧命令打断**,打断退出不清屏)、'3' 单行(len 3~18,**FONT_24 渲染**行高 24px——协议 24 点阵,先清行再渲染;行号 '1'~'5' **全按协议接受**,行 5 在 96px 屏高下「执行但不落屏」——渲染调用照常发出、渲染层越界早退)、'4' 全屏可编辑(len 4~86,**按 X/Y 坐标渲染** word_wrap=true,FONT_24,0x0A 换行 0x0D 跳过)、'5' 清屏、'6' **单行清除**('1'~'5';与贵州 '6' 固定格式语义不同)、'7' 礼貌用语语音('0'~'3',协议原文云南文案)、'8' 亮度(**0x00=NUL=恢复光敏自动调光、'1'~'8' 恒等映射硬件档并挂起光敏任务**,8 最亮)、'9' 音量(1~5 → 语音板 {1,3,5,7,9})、'A' 外设(bit0 绿/bit1 红/bit2 黄闪,**红优先**)、'B' 费额语音(金额 ASCII 串→分,**0 元不播**、小数播小数末位 0 剔除,不显示)、0x01 全屏点亮(**01红/02绿/03黄/04蓝/05紫/06青/07白**——DATA0 与 display_color_t 枚举恒等,P5 全彩屏 8 色除黑)、0x02 版本号(串口回裸 ASCII **PROGRAM_CODE**)。**无上电效果**:不建 default 文件、不注册默认显示(使用固件默认显示,用户决定 8)。 +- **帧头冲突纪律**:命令字 '1'~'9','A','B' 全部落入青海 probe 命令集(**完全重叠**),全协议 Makefile 构建下青海 probe 先注册(源码收录序 qh 在前)先认领云南帧(与贵州处境一致)。**量产必须 EIDE 目录排除与青海/山东/贵州/四川MTC 互斥**(doc/05-01 §6);当前 `.eide/eide.yml` Debug 目标已启用四川MTC、**排除云南**(`Application/protocol/ProtocolParser_YunNan`,与青海/山东/贵州同列)。编译期互斥守卫 `g_brace_proto_guard` 链接期兜底强制(EIDE 多 `{` 族编入即 `multiple definition` 报错;Makefile 全协议构建经 `-DSTD_ALL_PROTO` 豁免)。0x01/0x02 二进制命令字青海/山东/四川MTC probe 首命令字快拒,无冲突。金额串转分 `yn_amount_to_fen`。已确认决定记录见 `doc/11_云南费显协议/README.md` §8。 + +## 四川三协议(`ProtocolParser_SiChuang_{ETC,MTC,Overload}`) + +三个四川地区协议模块均绑定 `CH_ID_RS485` + `CH_ID_RS232` 双通道、queue 深度 3(静态 SRAM),与青海同构(acquire → register → bind → set_frame_queue)。协议文档提取自 1D/1E/1F,要点: + +- **ETC(1D)**:帧 `0A + 显示方式(00/01) + 行号(00~06) + 数据 + 0D`(数据**变长 0x0D 定界**:单行 ≤24B、全屏 ≤145B,无固定 56B——上限对齐 9K1F212701 etc.c `cmd_etc_disPlay_ctrl` 0x0D 扫描索引 ≤148,GBK);`0A 36/37/38/39 0D` 灯控 → `dev_io_lane_light`/`dev_io_flash_light` 且同步显示颜色(fontColor 语义);`0A 40 XX YY 0D` 亮度(XX=00 自动调光);`0A 50 0D` 心跳(解析保留,识别后丢弃)。应答 `0A 00/01/02 0D`(收到即回,心跳不回)。**0x20 清屏(行号 0 全屏)、0x30 初始化 → 软件复位**。全屏渲染按 9K1F212701 `MakeSixteenLattAll` 语义:清屏后自第 1 行第 1 列按屏宽自动换行。**心跳超时显示已停用(2026-08-17)**:原独立计时任务(栈 256×4,5 分钟无有效帧 → 「ETC车道关闭」)已 #if 0、任务不再创建;黄闪 0A 38 开启后的 10 秒自动关闭依赖同一任务 tick,一并失效——开启后须 0A 39 显式关闭。恢复方法见 `app_sc_etc_proto.c` 注释。 +- **MTC(1E 方案二)**:帧 `'{' + 命令('1'~'9','A') + 参数 + '}'`——**'}' 定界变长,无长度字段、无 BCC**(对齐 9K1F212701 mtc.c:各命令 handler 逐字节扫 '}' 定帧,参考扫描上限 228;本设备 probe 上限 `SC_MTC_PAYLOAD_MAX=74B` 队列约束)。字段偏移按变长重算:'3' 单行 = 行号[2]+变长文本[3..],'4' 全屏 = 变长文本[2..](先清屏后整屏 word_wrap 渲染:w=screen_rows 屏宽、h=screen_cols 整屏高、word_wrap=true,渲染引擎按当前字号自动折行——2026-08-17 修复,原 16B/行切 ≤4 行且单行不换行,第一行超宽溢出被裁、超 64B 被丢弃;'3' 单行保持 word_wrap=false + h=当前字号截断语义),'6' 固定格式 = 类型[2]+数字串(客车 ≥11B / 货车 ≥20B),'1','2','5'→3B、'7'固定→4B('78' 自定义文本变长)、'8','9'→4B。带 BCC 变体不加判别:BCC 字节视为内容尾部(与参考语义一致),BCC 不校验。主机查询 `0A 46 0A → 0A 64 0A`、清屏 `0A 46 0D`;附加 7B 40~45 原始帧族(同样 '}' 定界):40 改波特率(`app_uart_baud_apply`)、41 点阵大小、42 字体、43 协议类型(仅记录)、44 全屏点亮、45 版本号 → `SC_FX_P7.62_1.0`。'6' 字段布局按 9K1F212701 mtc.c。'7' 语音经 `dev_rs232_voice`(固定用语 GBK 直送,动态变量不拼读)。7B 46(1280B 载荷)不实现(超 RB 768)。宿主推演 `~/EnvTools/CD-DebugTool-cpp/scripts/probe_sim/sc_mtc_frame_sim.py`(`--old` 可复现旧定长乱码链)。 +- **治超(1F 3.5.1)**:帧 `FF + 长度(07~FF,1 字节;0xFE 显式排除给 RLS) + 命令 + 亮度 + 数据(可变长) + BCC(五字段异或) + FF`;**80 全屏显示、81~88 八行显示(9K1F212701 语义:行数据变长 = 总长-6,≤24B 截断、不足不补空格、先清行再渲染)**、94 清屏、96 亮度(00=自动调光,非 0 按 lightLev=(val+1)/32 映射 1~8 档)、99 通行灯(同步显示颜色)、98 黄闪;查询 A0 → A1~A8 每行独立帧(固定 16B 应答兼容工具,参考项目无应答实现)、B6/B9/B8 → 回显当前状态。**与 RLS 区分**:RLS 第二字节 0xFE(254) 被治超 probe 显式排除(长度上限放宽至 FF 后 0xFE 落入合法区间,不排除会把 RLS 帧吞掉并 WAIT 卡死链式),RLS probe 亦要求第二字节 0xFE,双向快拒成立。 +- **帧头冲突纪律**:MTC '{' 与青海 '{' 同帧头——MTC probe 以「'}' 定界变长扫描 + 上限 74B」认领,完整青海帧由青海 probe 先认领(qh_proto_init 先注册,源码收录序 QH 在前);残余风险两类:①青海 '3' 帧总长 ≤74 且数据段含 '}' 字节(GBK 尾字节可为 0x7D)且半帧到达时被 MTC 截断认领,②青海 'A'/'B' 空数据帧与 MTC 7B 41/42 同长(QH probe 先认领);量产由 EIDE 目录排除纪律兜底(doc/05 §6);编译期互斥守卫 `g_brace_proto_guard` 链接期兜底强制(EIDE 多 `{` 族编入即 `multiple definition` 报错;Makefile 全协议构建经 `-DSTD_ALL_PROTO` 豁免)。 + +## RLS 协议 (`Application/Src/RLS/app_rls.c`) + +重庆高速二代费显协议。帧头 `0xFF 0xFE` + 尾 `0x0D 0x0C`,`RLS_PAYLOAD_MAX=530`(帧头 6B + bitmap 512B + BCC 1B + 尾 2B)、`RLS_QUEUE_DEPTH=2`。仅绑定 RS485(`RB_PROVIDE_WEAK` 提供 RS485 RB),BCC 校验用 `Kernel/Inc/bcc_utils.h`(`bcc_calcu`)。`sw_app_initcall` 自注册 `rls_module_init`,`rls_handle_task` 栈 256×4。 + +## AH MQTT 协议 (`ah_mqtt.c` + `ah_mqtt_cmd.c`) + +AH 平台 MQTT 应用层协议。固定帧格式(21B~533B),通过 MQTT topic 路由命令。 + +**数据结构**(`ah_mqtt.h`,均为 packed 结构): +- `topic_info_t`:`station_hex(8) + lane_hex(2) + device_type(2) + device_id(2)` +- `notify_id_t`:日期+设备信息+发送计数 +- `sign_up_t`:设备签到(含软硬件版本号、协议版本、厂商信息) +- `state_report_t`:定期状态上报 + +**命令处理器**(`ah_mqtt_cmd.h`,通过 `g_ah_mqtt_cmd_table[]` 跳转): +- `/ASK/board/NULL` → `cmd_display`:UTF8 文本渲染 +- `/ASK/display/clean` → `cmd_fill`:全屏填充颜色 +- `/ASK/op/restart` → `cmd_restart`:应答后 `NVIC_SystemReset()` +- `/ASK/op/checktime` → `cmd_checktime`:更新时间戳 + +**注意**:模块 initcall 当前已注释(`// sw_app_initcall(ah_mqtt_module_init)`),`app_mqtt_start()` 也未调用,AH MQTT 链路未激活。 + +## 网络子系统 + +### PHY→MAC→LwIP 依赖链 + +``` +Device 层: dev_dp83848 (PHY 寄存器操作, 自动协商, 链路状态检测) + └─ 注入 IO 上下文 (pl_eth_phy_io_*) +Platform 层: pl_eth (ETH MAC/DMA, MDIO 读写) + └─ 注册 PHY 链路查询回调 (pl_eth_set_phy_link_fn) + pl_net (LwIP netif 初始化, IP 配置, 链路监听器注册) + └─ ethernet_link_thread 轮询链路状态 → 通知 pl_net_link_listener[] +Application: app_udp / app_tcp_server / app_tcp_client / app_mqtt + └─ pl_net_register_link_listener → 链路断开回调重建连接 +``` + +### 通道生命周期(以 UDP 为例) + +``` +udp_task: 绑定端口→循环{创建 udp_connect_task→等待信号量→销毁→延迟重连} + └─ udp_connect_task (每个客户端一个): + 1. udp_channel_init: ch.ops=udp_ch_ops, state=UP, app_channel_register + 2. 循环 netconn_recv → 提取源IP/端口 → app_channel_dispatch + 3. udp_channel_deinit: ops=nullptr, state=DOWN, conn=NULL, app_channel_register(NULL) + 4. osSemaphoreRelease 通知 udp_task 重连 +``` + +**IP 隔离**:`udp_channel_t` 存储源 IP 为 `uint8_t src_ip[4]` 字节数组,而非 LwIP `ip_addr_t`,避免 Application 层暴露 middleware 类型。 + +### dev_dp83848 PHY 驱动 (`Device/Network/dev_dp83848.c`) + +PHY 寄存器操作、自动协商、链路状态检测。通过 IO 上下文注入(`pl_eth_phy_io_*` 函数指针)与 MAC 层解耦。`dev_dp83848.h`(~249 行)定义完整 PHY 寄存器映射和 15+ 操作 API。 + +### LwIP 配置要点 (`Platform/Inc/lwipopts.h`) + +关键参数(139 行,覆盖 `opt.h` 默认值的部分与 CubeMX 生成配置): +- **内存**:`MEM_SIZE=12KB`;`MEMP_NUM_PBUF=16`、`PBUF_POOL_SIZE=16`(未覆盖,取 `opt.h` 默认值;SRAM 水位 87-94%,每 +16 池约 +4KB,另行核算后才可调) +- **netconn/UDP PCB 池(2026-08-21 覆盖,修复 LDI 搜索广播丢包)**:`MEMP_NUM_NETCONN=8`、`MEMP_NUM_UDP_PCB=8`(opt.h 默认 4)——dev 共存构建 baseline 4 netconn 已满(UDP 10011 + UDP 20103 + TCP Server listener + TCP Client),广播临时 `netconn_new` 必然 NULL 静默失败(LDI 12H 搜索应答 50-75% 无响应);扩到 8 留余量,bss 增量 304B。**新增 UDP 端口协议时仍须随通道数核算(doc/06 预算)** +- **硬件校验和**:`CHECKSUM_GEN_IP/UDP/TCP=0`、`CHECKSUM_CHECK_*_HW` 卸载到 MAC +- **UDP 广播**:`LWIP_BROADCAST` 未定义;实际 `IP_SOF_BROADCAST=1` + `IP_SOF_BROADCAST_RECV=1`(IAP 升级依赖)。`app_udp_broadcast`/`app_udp_cq_broadcast` 复用常驻通道 conn(`udp_task`/`udp_cq_task` 创建时已 `ip_set_option(SOF_BROADCAST)`)直接 `netconn_sendto` 广播,常驻 conn 未就绪(断链重建窗口,deinit 置 conn=NULL)才回退临时 conn(2026-08-21) +- **MQTT**:`LWIP_MQTT` / `MQTT_MAX_IN_FLIGHT` 未定义;USER CODE 1 定义 `MQTT_REQ_MAX_IN_FLIGHT=16`、`LWIP_SO_RCVTIMEO=1` +- **TCP**:`LWIP_TCP_KEEPALIVE=1`;`TCP_MSS=536`(未覆盖,取 `opt.h` 默认值) +- **线程**:`TCPIP_THREAD_PRIO=24`(=osPriorityHigh)、`TCPIP_THREAD_STACKSIZE=1024` + +### pl_net_adapt.h 架构约束 + +`Platform/Inc/pl_net_adapt.h` 聚合所有 LwIP API 头文件。**严格禁止在任何 .h 文件中包含此头文件**——仅 .c 实现文件可引用,防止 LwIP 类型泄漏到 Application 层头文件中。 + +## 调试 + +**SEGGER RTT**(`pl_rtt.c`):高速调试输出通道(无需占用 UART),通过 J-Link/CMSIS-DAP 的 SWD 接口传输。`pl_rtt_init()` 在 `hw_initcall` 中初始化,`SEGGER_RTT_printf()` 可在任何上下文中使用。不依赖 RTOS,可在 HardFault Handler 等异常上下文中输出。 + +**硬件测试**(`app_test.c`):9 个测试函数 — `app_test_pixel_scan` / `app_test_render_text` / `app_test_io_output` / `app_test_led_mapping` / `app_test_scan_line_order` / `app_test_diagonal` / `app_test_oblique_scan` / `app_test_prepare_mapping` 等,由 `app_test_run()` 汇总。`app_test_run()` 当前在 `app_boot` 的 `init_task` 中已注释。另有 `app_factory_test.c`(`sw_app_initcall(_factory_test_init)`):出厂检测 monitor + 老化循环(TEST 键触发,斜扫测试)。**业务数据到达经 `app_factory_mode_interrupt()` 置中止标志(不销毁任务),monitor 各按键等待点分片(100ms)检查标志回 IDLE——TEST 键全程可用**(对齐 9K1F212701 裸机:收包仅清 testMode)。 + +## Kernel 工具库 (`Kernel/`) + +| 文件 | 关键 API | 说明 | +|---|---|---| +| `ring_buffer.h` | `RB_DEFINE(name,sz)` 编译期静态分配 | 零堆开销环形缓冲区。每个 API 末尾的 `void *mutex` 参数控制锁行为:传 `rb->mutex` 自动加锁/解锁(单步操作),传 `nullptr` 跳过(调用者通过 `rb_lock/rb_unlock` 自行持锁,用于多步原子序列)。`rb_init` 在运行时绑定优先级继承互斥锁。相邻数据的 `rb_peek`/`rb_contig`/`rb_skip` 支持帧探测。 | +| `initcall.h` | `hw_pl/dev_initcall`、`sw_pl/dev/app_initcall` | 6 个层级宏,生成 linker section 条目 | +| `container_of.h` | `container_of` | 向上转型宏。`channel_t`/`ch_ops_t`/`channel_id_t`/`proto_probe_sta_t` 等 dispatch 共用类型定义在 `Application/Inc/app_dispatch.h` | +| `bcc_utils.h` | `bcc_calcu` | BCC 校验(RLS 协议) | +| `text_cvt.h` | `UTF8ToGBK` | UTF-8→GBK 编码转换 | +| `bit_utils.h` | `bit_ctz` | 协议掩码→数组索引(`proto_mask_t` → 数组下标) | +| `crc_utils.h` | CRC 校验 | CRC32(IAP)+ CRC-16/XMODEM(LDI) | + +## 代码风格 + +- `.clang-format`:Microsoft 基础,4 空格缩进,无 Tab,Linux 大括号,不限制列宽 +- 语言:C23,中文注释 +- 命名:`pl_`=Platform、`dev_`=Device、`app_`=Application +- 派生类基类成员统一 `me`(如 `dev_display_t me`、`channel_t me`) +- 函数命名:`_` 前缀 = 内部静态函数、模块前缀 `pl_`/`dev_`/`app_` = 公开 API +- GPIO 定义遵循 CubeMX 命名 diff --git "a/doc/\346\236\204\345\273\272\345\274\200\345\205\263\346\200\273\350\241\250.md" "b/doc/\346\236\204\345\273\272\345\274\200\345\205\263\346\200\273\350\241\250.md" new file mode 100644 index 0000000..36bcfdd --- /dev/null +++ "b/doc/\346\236\204\345\273\272\345\274\200\345\205\263\346\200\273\350\241\250.md" @@ -0,0 +1,62 @@ +# 构建开关总表 + +**状态**:`[新建]`(2026-08-24;随 CQ 故障屏开关改名 `CQ_FAULT_SCREEN_ENABLED` +/ `CQ_FAULT_SCREEN_FORCE_ON` → `CQ_FAULT_SCREEN` 一并整理) +**范围**:本项目(Project_STD 主固件)编译期/链接期全部开关,含 Makefile 构建 +变量与 EIDE 目标配置。每一项均有代码/构建文件依据,不收录无依据项。 + +--- + +## 1. 编译期宏开关 + +各构建入口说明: + +- **Makefile PROTO=ALL**:默认 dev 全协议共存构建(`PROTO ?= ALL`); +- **Makefile PROTO=CQ**:重庆量产口径(剔除 `Application/Src/LDI/` 目录文件 + + `DEFINES` 追加 `-DPROTO_CHONGQING`); +- **EIDE Debug**:`.eide/eide.yml` `targets.Debug`(当前 CQ 量产口径: + defineList 含 `PROTO_CHONGQING`、excludeList 排除 ldi 目录); +- **EIDE Release**:`.eide/eide.yml` `targets.Release`(defineList 仅平台宏, + excludeList 为旧 BSP 路径清单)。 + +| 开关宏 | 控制功能 | Makefile ALL | Makefile CQ | EIDE Debug | EIDE Release | 显式覆盖方式 | +|---|---|---|---|---|---|---| +| `PROTO_CHONGQING` | CQ 量产口径:CQ UDP 业务口读 Sector1 `net_cfg.udp_port`(默认 20103);`app_net_boot_apply` 按 CQ 口径应用默认 IP/端口(192.168.1.5/255.255.255.0/192.168.1.1);`CQ_FAULT_SCREEN` 默认值跟随它 | 未定义 | **定义**(`DEFINES += -DPROTO_CHONGQING`) | **定义**(defineList) | 未定义 | Makefile:命令行 `DEFINES=`(注意整体替换语义,见 §3);EIDE:defineList 条目 | +| `STD_ALL_PROTO` | `{` 帧族(青海/山东/贵州/四川MTC)链接期互斥守卫 `g_brace_proto_guard` 豁免:定义后 4 个 `{` 族协议可同时编入;不定义时多 `{` 族协议同时编入 → 链接报 `multiple definition of 'g_brace_proto_guard'` 强制互斥 | **定义** | **定义** | 未定义 | 未定义 | 同上 | +| `CQ_FAULT_SCREEN` | CQ 心跳超时故障屏(1 开 / 0 关):控制 `cq_proto_timer_task` 心跳计数 + 120s 阈值 + `cq_render_fault_screen`;关闭时不计数、不渲染(warn1 倒计时不受影响)。**默认跟随构建口径**:CQ 口径默认开、共存/其他口径默认关(他省上位机不发重庆 syn1,120s 故障屏会误触发)。2026-08-24 改名(见 §2) | 0(关) | 1(开) | 1(开,含 PROTO_CHONGQING) | 0(关) | `-DCQ_FAULT_SCREEN=1` 强制开 / `=0` 强制关;**Makefile 用透传变量**:`make [PROTO=CQ] CQ_FAULT_SCREEN=0`(2026-08-24 新增,追加不替换);EIDE defineList 加 `CQ_FAULT_SCREEN=0` 条目 | +| `CQ_FAULT_SCREEN_FORCE_ON` | **已废弃**(2026-08-24):原强制覆盖开关名 | — | — | — | — | 不再支持;改用 `-DCQ_FAULT_SCREEN=1/0` | +| `CQ_FAULT_SCREEN_ENABLED` | **已废弃**(2026-08-24):原三段级联最终内部名 | — | — | — | — | 不再支持;改用 `CQ_FAULT_SCREEN` | +| `g_brace_proto_guard` | 链接期互斥守卫符号(`char`,4 个 `{` 帧族协议 .c 各定义一次,`#ifndef STD_ALL_PROTO` 豁免)。**非用户可关开关**:不在任何 defineList,只受 `STD_ALL_PROTO` 间接控制 | 豁免(不定义符号) | 豁免 | 生效(互斥) | 生效(互斥) | 无(不可直接操作) | +| `USE_HAL_DRIVER` | HAL 库驱动使能(HAL 头文件条件编译) | **定义** | **定义** | **定义** | **定义** | 固定平台宏,勿关;命令行覆盖 `DEFINES` 时必须自带 | +| `STM32F407xx` | 芯片型号宏(CMSIS 头文件选型) | **定义** | **定义** | **定义** | **定义** | 固定平台宏,勿关;命令行覆盖 `DEFINES` 时必须自带 | + +## 2. CQ 故障屏开关改名对照(2026-08-24) + +原实现为三段级联 + 反直觉命名(`CQ_FAULT_SCREEN_FORCE_ON` 输入 → +`CQ_FAULT_SCREEN_ENABLED` 输出),压平为单层中立宏 `CQ_FAULT_SCREEN`: + +| 旧名 | 旧角色 | 新名/替代 | 备注 | +|---|---|---|---| +| `CQ_FAULT_SCREEN_FORCE_ON` | 构建入口显式覆盖输入 | `-DCQ_FAULT_SCREEN=1/0` | 已废弃,不保留兼容别名 | +| `CQ_FAULT_SCREEN_ENABLED` | 三段级联输出、代码使用点 | `CQ_FAULT_SCREEN` | 已废弃,不保留兼容别名 | + +语义不变:默认值跟随构建口径(PROTO_CHONGQING → 1,其他 → 0),构建入口可 +显式覆盖。使用点:`app_cq_proto.c` `cq_proto_timer_task` 的 +`#if CQ_FAULT_SCREEN`(关闭分支:不计数、不渲染)。 + +## 3. Makefile 级构建变量 + +| 变量 | 取值 | 默认 | 说明 | +|---|---|---|---| +| `PROTO` | `ALL` / `CQ` | `ALL` | `ALL` 全协议 dev 构建;`CQ` 剔除 LDI 目录文件 + 追加 `-DPROTO_CHONGQING`。**不变量:排除 LDI 与定义 `PROTO_CHONGQING` 必须成对**(由 Makefile 联动保证;手动覆盖 `DEFINES` 时可能破坏成对,见下) | +| `TOOLCHAIN` | `gcc` / `clang` | `gcc` | 编译器选择(clang 走 LLVM_ARM 配置) | +| `CONFIG` | `Debug` / `Release` | `Debug` | 决定 `build/$(CONFIG)` 输出目录 | +| `DEFINES` | 编译宏串 | `-DUSE_HAL_DRIVER -DSTM32F407xx -DSTD_ALL_PROTO`(PROTO=CQ 再追加 `-DPROTO_CHONGQING`) | **命令行传入会整体替换**:Makefile 未用 `override`,命令行 `DEFINES=` 同时压掉 `DEFINES =` 与 `DEFINES +=`(实测 2026-08-24:`DEFINES='-DX_TEST'` 时编译行仅 `-DX_TEST`,`-DPROTO_CHONGQING` 不追加)。**手动传 DEFINES 必须自带全部所需宏**,否则会丢掉 `USE_HAL_DRIVER`/`STM32F407xx`/`STD_ALL_PROTO`,并破坏「LDI 排除 ↔ PROTO_CHONGQING」成对不变量。**推荐用专用透传变量(`CQ_FAULT_SCREEN=0`)覆盖功能开关,不要传 DEFINES** | + +## 4. 修订记录 + +- 2026-08-24:新建本表。CQ 心跳故障屏开关压平为单层宏 `CQ_FAULT_SCREEN` + (1 开 / 0 关,默认跟随构建口径),旧名 `CQ_FAULT_SCREEN_ENABLED` / + `CQ_FAULT_SCREEN_FORCE_ON` 废弃(无兼容别名);`app_cq_proto.c` 使用点、 + doc/CLAUDE.md、doc/03 PartB B.7.2 同步改名;记录 Makefile 命令行 `DEFINES` + 整体替换语义。 \ No newline at end of file diff --git a/tool/flash_all.sh b/tool/flash_all.sh new file mode 100755 index 0000000..9dbd7da --- /dev/null +++ b/tool/flash_all.sh @@ -0,0 +1,314 @@ +#!/usr/bin/env bash +#=============================================================================== +# flash_all.sh - STM32F407 多固件一键烧录 +# Multi-firmware one-shot flash for STM32F407ZGT6 +# +# 在一个 J-Link 会话中烧录 Bootloader + Recovery + Project-STD +# Flashes all 3 firmwares in ONE J-Link session to prevent +# intermediate full-chip erase wiping previous loads. +# +# Flash Layout (STM32F407ZGT6, 1MB): +# Bootloader: 0x08000000 16 KB (Sector 0) +# [Board cfg] 0x08004000 16 KB (Sector 1, 板级系统配置: +# magic + update_sta + FWInfo + NetConfig + CRC32) +# Recovery: 0x08008000 224 KB (Sectors 2-5) +# Project-STD: 0x08040000 768 KB (Sectors 6-11) +# +# 为什么默认擦除 Sector1(重要,2026-08-14 定案): +# Bootloader 条件 D(非 debug 模式)会校验 Sector1 记录中 app_info.size/crc32 +# 与 0x08040000 实机固件 CRC 是否一致,而 app_info 真实值只由 Recovery 升级 +# 流程写入。因此:一旦 Recovery 成功升级过一次(app_info 为旧固件真实值), +# 再用 J-Link/EIDE 直接重烧主固件而不更新 Sector1 → 条件 D 校验失配 → +# Bootloader 判「App 损坏」→ 设备永远进 Recovery,主固件再也起不来。 +# 规避:本脚本默认烧完三个固件后擦除 Sector1 恢复出厂态。Sector1 空 → +# Bootloader 走条件 C(出厂初始化)→ 正常跳主固件。net_cfg 副作用可接受: +# 主固件上电时 ldi_ctx_init 会从 W25Qxx 外部 Flash 的 LDI 配置同步回写 +# Sector1(空扇区自动完整初始化)。 +# +# Usage: +# ./flash_all.sh # 使用默认编译产物 (默认跳过整片擦除和校验, +# # 但默认擦除 Sector1 恢复出厂态) +# ./flash_all.sh --dry-run # 预览命令不执行 +# ./flash_all.sh --keep-config # 保留 Sector1 板级配置 (跳过 Sector1 擦除; +# # 仅当 Sector1 app_info 与实机固件一致时使用, +# # 例如一直只经 Recovery 升级、从未用烧录器重烧) +# ./flash_all.sh -b # 自定义 Bootloader +# ./flash_all.sh -r # 自定义 Recovery +# ./flash_all.sh -a # 自定义 Project-STD +# ./flash_all.sh -s # J-Link 速度 (默认: 40000) +# ./flash_all.sh --erase # 强制整片擦除 (默认跳过; 与 Sector1 擦除共存 +# # 时天然幂等, 无需特殊处理) +# ./flash_all.sh --no-erase # 跳过整片擦除 +# ./flash_all.sh --verify # 强制校验 (默认跳过) +# ./flash_all.sh --no-verify # 跳过校验 +# ./flash_all.sh --fast # 快捷: --no-erase --no-verify (默认行为) +#=============================================================================== +set -euo pipefail +shopt -s nullglob + +# ── Color ──────────────────────────────────────────────────────────────────── +readonly C_R='\033[0;31m' C_G='\033[0;32m' C_Y='\033[1;33m' +readonly C_B='\033[0;34m' C_N='\033[0m' +info() { printf "%b[INFO]%b %s\n" "$C_G" "$C_N" "$*"; } +warn() { printf "%b[WARN]%b %s\n" "$C_Y" "$C_N" "$*"; } +error() { printf "%b[ERROR]%b %s\n" "$C_R" "$C_N" "$*"; } +step() { printf "%b[STEP]%b %s\n" "$C_B" "$C_N" "$*"; } + +# ── Resolve script directory (relative path base) ──────────────────────────── +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Project root: tool/../ +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +# Workspace root: 3833024/ +WS_ROOT="$(cd "$PROJECT_ROOT/.." && pwd)" + +# ── Default firmware paths (relative to WS_ROOT) ───────────────────────────── +readonly DFL_BOOT="${WS_ROOT}/STM32F407-Bootloader-master/build/Debug/Bootloader.hex" +readonly DFL_RECV="${WS_ROOT}/STM32F407-Recovery-master/build/Debug/Recovery.hex" +readonly DFL_APP="${WS_ROOT}/Project-STD-main/build/Debug/Project_STD.hex" + +# ── User-configurable ──────────────────────────────────────────────────────── +BOOT_FILE=""; RECV_FILE=""; APP_FILE="" +JLINK_DEVICE="STM32F407ZG" +JLINK_IF="SWD" +JLINK_SPEED="40000" +DRY_RUN=false +SKIP_ERASE=true +SKIP_VERIFY=true +KEEP_CONFIG=false +JLINK_SCRIPT="" +START_TIME="" + +# ── Usage ──────────────────────────────────────────────────────────────────── +usage() { + cat << 'EOF' +Usage: ./flash_all.sh [OPTIONS] + +STM32F407 Multi-Firmware Flash Tool (Bootloader + Recovery + Project-STD) + +OPTIONS: + -b, --bootloader Bootloader hex/bin + -r, --recovery Recovery hex/bin + -a, --app Project-STD hex/bin + -d, --device J-Link device (default: STM32F407ZG) + -i, --interface SWD | JTAG (default: SWD) + -s, --speed Interface speed (default: 40000) + --dry-run Print commands without flashing + --erase Force chip erase + --no-erase Skip chip erase (default) + --keep-config Keep Sector1 board config (skip Sector1 erase; + default: erase Sector1 for factory reset) + --verify Force verify after load + --no-verify Skip verify after load (default) + --fast Shortcut: --no-erase --no-verify (default behavior) + -h, --help Show this help + +FLASH LAYOUT: + 0x08000000 Bootloader 16 KB + 0x08004000 Board cfg 16 KB (Sector 1; ERASED by default, see below) + 0x08008000 Recovery 224 KB + 0x08040000 Project-STD 768 KB + +SECTOR1: erased after load by default (factory reset) to avoid the + Bootloader condition-D app_info CRC mismatch trap (reflashing + the app without updating Sector1 after a Recovery upgrade + bricks the boot: App judged corrupt -> stuck in Recovery). + Use --keep-config only when Sector1 matches the on-chip app. + +DEFAULTS: skip chip erase, skip verify, speed 40000 kHz, + ERASE Sector1 (factory reset) + Use --erase and/or --verify to enable those steps. +EOF +} + +# ── Parse args ─────────────────────────────────────────────────────────────── +while (($#)); do + case "$1" in + -b|--bootloader) BOOT_FILE="$2"; shift 2 ;; + -r|--recovery) RECV_FILE="$2"; shift 2 ;; + -a|--app) APP_FILE="$2"; shift 2 ;; + -d|--device) JLINK_DEVICE="$2"; shift 2 ;; + -i|--interface) JLINK_IF="$2"; shift 2 ;; + -s|--speed) JLINK_SPEED="$2"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + --erase) SKIP_ERASE=false; shift ;; + --no-erase) SKIP_ERASE=true; shift ;; + --keep-config) KEEP_CONFIG=true; shift ;; + --verify) SKIP_VERIFY=false; shift ;; + --no-verify) SKIP_VERIFY=true; shift ;; + --fast) SKIP_ERASE=true; SKIP_VERIFY=true; shift ;; + -h|--help) usage; exit 0 ;; + *) error "Unknown option: $1"; usage; exit 1 ;; + esac +done + +BOOT_FILE="${BOOT_FILE:-$DFL_BOOT}" +RECV_FILE="${RECV_FILE:-$DFL_RECV}" +APP_FILE="${APP_FILE:-$DFL_APP}" + +# ── Validate firmware files ────────────────────────────────────────────────── +validate_files() { + local name label file size ok=0 + local -a batch=( \ + "Bootloader|BOOT|$BOOT_FILE" \ + "Recovery |RECV|$RECV_FILE" \ + "Project-STD|APP|$APP_FILE" \ + ) + for entry in "${batch[@]}"; do + IFS='|' read -r label _ file <<< "$entry" + if [[ -f "$file" ]]; then + size=$(du -h "$file" | cut -f1) + [[ "${file##*.}" =~ [hH][eE][xX] ]] \ + && info "$label: ${file} (${size}, HEX self-addressed)" \ + || info "$label: ${file} (${size}, BIN)" + else + error "$label: NOT FOUND -> ${file}" + ok=1 + fi + done + return $ok +} + +# ── Find JLinkExe ──────────────────────────────────────────────────────────── +find_jlink() { + local p + command -v JLinkExe 2>/dev/null && return + for p in /opt/SEGGER/JLink/JLinkExe /usr/bin/JLinkExe; do + [[ -x "$p" ]] && { echo "$p"; return; } + done + for p in /opt/SEGGER/JLink*/JLinkExe; do + [[ -x "$p" ]] && { echo "$p"; return; } + done + return 1 +} + +# ── Format elapsed time ────────────────────────────────────────────────────── +format_elapsed() { + local seconds="$1" + local mins=$((seconds / 60)) + local secs=$((seconds % 60)) + if ((mins > 0)); then + printf '%dm %ds' "$mins" "$secs" + else + printf '%ds' "$secs" + fi +} + +# ── Generate J-Link script ─────────────────────────────────────────────────── +generate_jlink_script() { + local tmp + tmp="$(mktemp /tmp/flash_all_XXXXX.jlink)" + { + printf 'device %s\n' "$JLINK_DEVICE" + printf 'si %s\n' "$JLINK_IF" + printf 'speed %s\n' "$JLINK_SPEED" + printf 'h\n' + if ! $SKIP_ERASE; then + printf 'erase\n' + fi + if $SKIP_VERIFY; then + printf 'SetCompareMode 0\n' + fi + printf 'loadfile %s\n' "$BOOT_FILE" + printf 'loadfile %s\n' "$RECV_FILE" + printf 'loadfile %s\n' "$APP_FILE" + # 默认擦除 Sector1 (0x08004000~0x08007FFF, 板级配置扇区) 恢复出厂态: + # 规避 Bootloader 条件 D 的 app_info CRC 失配坑 (详见脚本头注释)。 + # J-Link 范围擦除语法: Erase [, ] (逗号分隔, 大小写不敏感)。 + # 与 --erase 整片擦除共存时天然幂等 (整片擦除已覆盖 Sector1, 无需特殊处理)。 + if ! $KEEP_CONFIG; then + printf 'erase 0x08004000, 0x08007FFF\n' + fi + if $SKIP_VERIFY; then + printf 'SetCompareMode 1\n' + fi + printf 'r\ng\nqc\n' + } > "$tmp" + echo "$tmp" +} + +# ══════════════════════════════════════════════════════════════════════════════ +# Main +# ══════════════════════════════════════════════════════════════════════════════ + +printf '\n============================================\n' +printf ' STM32F407 Multi-Firmware Flash Tool\n' +printf '============================================\n\n' + +step "Validating firmware files..." +validate_files || exit 1 + +step "Locating J-Link Commander..." +JLINK_EXE=$(find_jlink) || { + error "JLinkExe not found! Install: https://www.segger.com/downloads/jlink/" + exit 1 +} +info "Found: $JLINK_EXE" + +step "Generating command script..." +JLINK_SCRIPT="$(generate_jlink_script)" + +# ── Summary ────────────────────────────────────────────────────────────────── +printf -- '\n--------------------------------------------\n' +printf " %-13s %s\n" "Device:" "$JLINK_DEVICE" +printf " %-13s %s @ %s kHz\n" "Interface:" "$JLINK_IF" "$JLINK_SPEED" +printf " %-13s %s\n" "Bootloader:" "${BOOT_FILE##*/}" +printf " %-13s %s\n" "Recovery:" "${RECV_FILE##*/}" +printf " %-13s %s\n" "App:" "${APP_FILE##*/}" +printf " %-13s %s\n" "Erase:" "$($SKIP_ERASE && echo 'SKIPPED' || echo 'YES (single)')" +printf " %-13s %s\n" "Erase Sec1:" "$($KEEP_CONFIG && echo 'KEPT' || echo 'YES (factory reset)')" +printf " %-13s %s\n" "Verify:" "$($SKIP_VERIFY && echo 'SKIPPED' || echo 'YES')" +printf " %-13s %s\n" "Dry run:" "$($DRY_RUN && echo 'YES' || echo 'no')" +printf -- '--------------------------------------------\n\n' + +step "Commands to execute:" +while IFS= read -r line || [[ -n "$line" ]]; do + printf ' %s\n' "$line" +done < "$JLINK_SCRIPT" +printf '\n' + +# ── Dry-run ────────────────────────────────────────────────────────────────── +if $DRY_RUN; then + warn "DRY RUN -- no flash executed." + rm -f "$JLINK_SCRIPT" + exit 0 +fi + +# ── Execute ────────────────────────────────────────────────────────────────── +step "Flashing... (this may take a moment)" +echo '' + +START_TIME=$(date +%s) + +set +e +"$JLINK_EXE" \ + -device "$JLINK_DEVICE" \ + -if "$JLINK_IF" \ + -speed "$JLINK_SPEED" \ + -autoconnect 1 \ + -CommanderScript "$JLINK_SCRIPT" +rc=$? +set -e + +END_TIME=$(date +%s) +ELAPSED=$((END_TIME - START_TIME)) + +rm -f "$JLINK_SCRIPT" + +printf '\n' +if ((rc == 0)); then + info "============================================" + info " Flash SUCCESS" + info " Bootloader @ 0x08000000" + info " Recovery @ 0x08008000" + info " Project-STD @ 0x08040000" + info " Elapsed: $(format_elapsed "$ELAPSED")" + info "============================================" +else + error "============================================" + error " Flash FAILED (exit: $rc)" + error " Elapsed: $(format_elapsed "$ELAPSED")" + error " Check J-Link connection" + error "============================================" +fi + +exit $rc