diff --git a/.clangd b/.clangd index 26f4dd1..4491e55 100644 --- a/.clangd +++ b/.clangd @@ -1,17 +1,23 @@ 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: + - -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 + - -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 + CompilationDatabase: ./build/Debug + Compiler: /home/yystation/EnvTools/embedded-toolchain/arm-gnu-toolchain/bin/arm-none-eabi-gcc Diagnostics: - Suppress: - - unused-includes - - drv_unknown_argument + Suppress: + - unused-includes + - drv_unknown_argument diff --git a/.eide/eide.yml b/.eide/eide.yml index 15e2d3d..2156284 100644 --- a/.eide/eide.yml +++ b/.eide/eide.yml @@ -45,6 +45,7 @@ virtualFolder: 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: [] @@ -250,7 +251,9 @@ virtualFolder: - name: Display files: - path: Device/Display/dev_display.c - - path: Device/Display/dev_display_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: @@ -336,6 +339,8 @@ targets: - /Drivers/BSP/Key/key.c - /Drivers/BSP/RingBuff/Ringbuffer.c - /Drivers/BSP/Display/scan.c + - /Device/Display/dev_display_1_260.c + - /Device/Display/dev_display_1_969.c settings: debugger: cortex-debug toolchain: GCC @@ -354,7 +359,7 @@ targets: language-cpp: c++23 one-elf-section-per-data: true one-elf-section-per-function: true - optimization: level-debug + optimization: level-3 warnings: all-warnings global: $float-abi-type: hard @@ -415,11 +420,11 @@ targets: baseAddr: "" bin: "" cpuInfo: - cpuName: "null" - vendor: "null" + cpuName: STM32F407ZG + vendor: ST otherCmds: "" proType: 1 - speed: 8000 + speed: 32000 OpenOCD: baseAddr: "0x08000000" bin: "" @@ -433,7 +438,7 @@ targets: protocol: swd speed: 10000 target: STM32F407ZG - uploader: Custom + uploader: JLink Release: cppPreprocessAttrs: defineList: diff --git a/Application/Inc/Channel/app_udp.h b/Application/Inc/Channel/app_udp.h index 633146e..9afc15c 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 21H/12H) + * - IAP 固件升级(0x5A5A5A5A) + * 端口可改,但出厂联调默认保持 10011。 */ #pragma once diff --git a/Application/Inc/LDI/app_ldi.h b/Application/Inc/LDI/app_ldi.h index 3c79d67..be1fce1 100644 --- a/Application/Inc/LDI/app_ldi.h +++ b/Application/Inc/LDI/app_ldi.h @@ -5,7 +5,7 @@ #include "app_ldi_cfg.h" #include "ring_buffer.h" -#define DEVICE_NUM (2U) // 本设备使用到的功能模块数量 +#define DEVICE_NUM (4U) // 本设备使用到的功能模块数量 #define LDI_TX_BUF_SIZE (512U) // 响应帧拼装缓冲区上限 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/app_light_sensor.h b/Application/Inc/app_light_sensor.h index 0cb01c4..e8e07d5 100644 --- a/Application/Inc/app_light_sensor.h +++ b/Application/Inc/app_light_sensor.h @@ -12,3 +12,10 @@ extern osThreadId_t g_light_sensor_task_handle; void app_light_sensor_init(void); void app_light_sensor_task(void *argument); + +/** + * @brief 设置光敏自动调光的亮度范围(Application 层包装) + * @param min 最低亮度等级 (1~7) + * @param max 最高亮度等级 (1~7) + */ +void app_light_sensor_set_range(uint8_t min, uint8_t max); diff --git a/Application/Inc/app_render.h b/Application/Inc/app_render.h index 864a4e1..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) diff --git a/Application/Inc/app_test.h b/Application/Inc/app_test.h index 012c002..42d2565 100644 --- a/Application/Inc/app_test.h +++ b/Application/Inc/app_test.h @@ -8,6 +8,8 @@ #pragma once +#include + /** @brief 运行全部测试用例 */ void app_test_run(void); @@ -22,3 +24,12 @@ 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 prepare 映射测试:通过 set_pixel 逐像素点亮,验证 prepare 像素重排是否正确 */ +void app_test_prepare_mapping(void); diff --git a/Application/Src/Channel/app_tcp_client.c b/Application/Src/Channel/app_tcp_client.c index 8f56bff..bd00ea1 100644 --- a/Application/Src/Channel/app_tcp_client.c +++ b/Application/Src/Channel/app_tcp_client.c @@ -17,7 +17,7 @@ __STATIC_INLINE void tcp_keepaliveinit(struct netconn *conn); /* ---- 连接任务属性 ---- */ const osThreadAttr_t tcp_client_conn_attr = { .name = "tcp_client_conn_task", - .stack_size = 256 * 4, + .stack_size = 256 * 8, .priority = osPriorityNormal, }; diff --git a/Application/Src/Channel/app_udp.c b/Application/Src/Channel/app_udp.c index bbe5fd4..179cf26 100644 --- a/Application/Src/Channel/app_udp.c +++ b/Application/Src/Channel/app_udp.c @@ -1,6 +1,10 @@ /** - * @file dev_udp.c - * @brief UDP 广播接收通道(监听端口 10011) + * @file app_udp.c + * @brief UDP 广播接收通道(默认端口 10011) + * + * 同端口承载两类协议(由 app_dispatch 按帧头分流): + * - 创迪发现口:LDI 21H/12H 搜索 + * - IAP 升级:0x5A5A5A5A 帧 */ #include "app_udp.h" @@ -9,7 +13,7 @@ #include "pl_net_adapt.h" /* ---- 配置 ---- */ -static uint16_t g_udp_port = 10011; /**< IAP 升级通道 */ +static uint16_t g_udp_port = 10011; /**< 创迪发现口 + IAP 共用 */ void app_udp_set_port(uint16_t port) { @@ -27,6 +31,11 @@ void app_udp_broadcast(const uint8_t *data, uint16_t len) if (conn == NULL) return; ip_set_option(conn->pcb.udp, SOF_BROADCAST); + if (netconn_bind(conn, IP_ADDR_ANY, g_udp_port) != ERR_OK) { + netconn_delete(conn); + return; + } + ip_addr_t bc_addr; IP4_ADDR(&bc_addr, 255, 255, 255, 255); @@ -103,24 +112,28 @@ void udp_task(void *argument) 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); } diff --git a/Application/Src/IAP/app_iap.c b/Application/Src/IAP/app_iap.c index 7e7f7fa..9bb638a 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。 + * 承载于 RS485 + UDP,配置存储于 Flash 0x08004000。 */ #include "app_iap.h" diff --git a/Application/Src/LDI/app_ldi.c b/Application/Src/LDI/app_ldi.c index 7d07b4c..062d76f 100644 --- a/Application/Src/LDI/app_ldi.c +++ b/Application/Src/LDI/app_ldi.c @@ -4,6 +4,7 @@ #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" @@ -48,10 +49,12 @@ 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}, }, }, }; @@ -139,6 +142,7 @@ static proto_mask_t s_ldi_mask; /* 上下文初始化必须在创建任务之前,保证 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}; @@ -283,12 +287,14 @@ const static uint8_t ldi_stx[2] = {0xFF, 0xFF}; proto_probe_sta_t ldi_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(ldi_frame_t) + sizeof(ldi_req_head_t) + 2) + /* 先等到定长帧头(STX/VER/SEQ/LEN),再按 LEN 等完整帧。 + * 创迪发现口 21H 可能仅携带 1 字节 CmdType,不能再要求完整 ldi_req_head_t。 */ + 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))) @@ -298,8 +304,23 @@ proto_probe_sta_t ldi_probe_frame(const channel_t *ch, const ring_buffer_t *buff g_ldi.rsp_seq = frame->seq; /* 保存序号用于响应回显 */ - 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]; + 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]; + /* 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; + + /* 完整帧已齐,再 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; @@ -353,6 +374,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_cmd.c b/Application/Src/LDI/app_ldi_cmd.c index 9ab3c3d..18768d3 100644 --- a/Application/Src/LDI/app_ldi_cmd.c +++ b/Application/Src/LDI/app_ldi_cmd.c @@ -9,6 +9,7 @@ #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" @@ -621,7 +622,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 +646,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 +680,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'; /* 暂填默认型号代码 */ @@ -770,17 +773,22 @@ 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]; + + // 版本号(从请求中读取) + 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 += sizeof(ldi_module_head_t) + 2 + 1 + custom_init_len; // 2: protocol_version, 1: custom_init_len dst_mod = (ldi_init_rsp_module_t *)((uint8_t *)dst_mod + sizeof(ldi_init_rsp_module_t)); } @@ -846,17 +854,26 @@ 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) @@ -902,41 +919,57 @@ 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) + * 优先单播回源(上位机常在临时端口收包),同时广播一份兼容只监听 10011 的工具。 + * 不承担业务控制;网络参数取自 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, + }; + + 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, 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)); + memcpy(rsp.ip, ip, sizeof(rsp.ip)); + uint16_t port = 10011; + 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); + /* 仅广播到 10011,避免单播回源 */ + app_udp_broadcast(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..9ddbe64 --- /dev/null +++ b/Application/Src/LDI/app_ldi_device.c @@ -0,0 +1,468 @@ +/** + * @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; +} + +/* ---- 辅助函数:判断是否为GBK双字节字符 ---- */ +static bool is_gbk_char(uint8_t high, uint8_t low) +{ + return (high >= 0x81 && high <= 0xFE) && (low >= 0x40 && low <= 0xFE && low != 0x7F); +} + +/* ---- 辅助函数:计算指定字体下文本的总像素宽度 ---- */ +static uint16_t calc_text_width(const uint8_t *text, uint16_t text_len, font_size_t font_size) +{ + uint16_t total_width = 0; + uint16_t i = 0; + + while (i < text_len) { + if (text[i] == '\n') { + // 换行符不计入宽度,但实际渲染时会换行 + i++; + continue; + } + + if (text[i] >= 0x20 && text[i] <= 0x7F) { + // ASCII字符:宽度 = 字号/2 + total_width += font_size / 2; + i++; + } else if (i + 1 < text_len && is_gbk_char(text[i], text[i + 1])) { + // GBK中文字符:宽度 = 字号 + total_width += font_size; + i += 2; + } else { + // 其他字符:跳过 + i++; + } + } + + return total_width; +} + +/* ---- 辅助函数:根据屏幕尺寸和文本选择最大合适字体 ---- */ +static font_size_t select_adaptive_font(const uint8_t *text, uint16_t text_len, + uint16_t screen_w, uint16_t screen_h) +{ + // 支持的字体大小,从大到小尝试 + const font_size_t font_sizes[] = {FONT_32, FONT_24, FONT_16}; + const uint8_t num_fonts = sizeof(font_sizes) / sizeof(font_sizes[0]); + + for (uint8_t i = 0; i < num_fonts; i++) { + font_size_t font = font_sizes[i]; + + // 检查字体高度是否超过屏幕高度 + if (font > screen_h) { + continue; + } + + // 计算文本宽度 + uint16_t text_width = calc_text_width(text, text_len, font); + + // 如果文本宽度不超过屏幕宽度,选择此字体 + if (text_width <= screen_w) { + return font; + } + } + + // 如果所有字体都不合适,使用最小字体 + return FONT_16; +} + +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'; + } + + display_fill(COLOR_BLACK); + + render_style_t style = { + .h_align = ALIGN_LEFT_UP, + .v_align = ALIGN_LEFT_UP, + .word_wrap = true, + }; + + 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; + + /* font_line > 0: 将屏幕按字号划分为若干行,文字显示在指定行; + font_line == 0: 全屏显示(自适应时选择最大合适字体) */ + if (ctrl->font_line > 0 && screen_h > 0) { + /* 按行显示:根据字体大小计算行高 */ + uint16_t line_height = (font_size == FONT_SELF_ADAPT) ? 16U : (uint16_t)font_size; + if (line_height > 0) { + uint16_t total_lines = screen_h / line_height; + if (total_lines > 0 && ctrl->font_line <= total_lines) { + render_y = (uint16_t)((ctrl->font_line - 1) * line_height); + render_h = line_height; + v_align = ALIGN_LEFT_UP; + } + } + /* 自适应字号时强制使用具体字号,否则渲染器会自适应缩放 */ + if (font_size == FONT_SELF_ADAPT) font_size = FONT_16; + } else { + /* 全屏显示:自适应模式下选择最大合适字体 */ + if (font_size == FONT_SELF_ADAPT) { + if (screen_w == 0 || screen_h == 0) { + font_size = FONT_16; + } else { + font_size = select_adaptive_font(text, text_len, screen_w, screen_h); + } + } + } + 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 = 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/app_boot.c b/Application/Src/app_boot.c index 1cd8cbc..f8101ec 100644 --- a/Application/Src/app_boot.c +++ b/Application/Src/app_boot.c @@ -7,8 +7,10 @@ #include #include +#include #include "cmsis_os.h" +#include "dev_display.h" #include "initcall.h" #include "pl_iwdg.h" #include "pl_rtc.h" @@ -23,7 +25,11 @@ #include "app_key.h" #include "app_render.h" +#define PROGRAM_CODE "9K10212482" +#define SPLASH_DURATION_MS 5000U + static void init_task(void *argument); +static void app_splash_display(void); /* ---- HalfSecTask: 500ms 喂狗 / 60s RTC 备份 / LED 翻转 ---- */ static void half_sec_task(void *argument) @@ -63,25 +69,61 @@ void app_boot(void) [[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, - }); - } + 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_32, + .font_type = FONT_HT, + .text_enc = FONT_ENC_UTF8, + }); +} + +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) @@ -91,6 +133,8 @@ static void init_task(void *argument) dev_eth_start(); sw_board_init(); /* sw_initcall 自注册:协议 + 通道任务 */ + // app_splash_display(); + /* 半秒周期任务 */ const osThreadAttr_t hst_attr = { .name = "half_sec_task", @@ -104,19 +148,25 @@ static void init_task(void *argument) app_udp_start(); app_rs485_start(); - // app_test_run(); - app_default_display(); - - printf("\nInit Task Done\n"); - + app_test_run(); + // app_default_display(); osThreadExit(); } /* ---- FreeRTOS 钩子 ---- */ void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { - (void)xTask; - (void)pcTaskName; + /* 调试时在此设断点,查看 pcTaskName 确定溢出任务 */ + volatile char *name = pcTaskName; + volatile TaskHandle_t task = xTask; + (void)name; + (void)task; + for (;;); +} + +void vApplicationMallocFailedHook(void) +{ + taskDISABLE_INTERRUPTS(); for (;;); } diff --git a/Application/Src/app_factory_test.c b/Application/Src/app_factory_test.c index a9786fb..4d74e72 100644 --- a/Application/Src/app_factory_test.c +++ b/Application/Src/app_factory_test.c @@ -1,6 +1,13 @@ /** * @file app_factory_test.c * @brief 出厂检测模式 — monitor 监听 TEST 驱动状态机 + * + * 状态流程(每次按键递进): + * IDLE ──[1]──▶ SHOW_CODE ──[2]──▶ RED ──[3]──▶ GREEN ──[4]──▶ YELLOW + * ──[5]──▶ AGING ──[6]──▶ NVIC_SystemReset() + * + * 按键测试阶段(SHOW_CODE / RED / GREEN / YELLOW):光敏自动调光生效。 + * 老化循环阶段(AGING):强制最大亮度,光敏暂停。 */ #include "app_factory_test.h" @@ -14,8 +21,10 @@ #include "app_dispatch.h" #include "app_light_sensor.h" +#include "stm32f4xx_hal.h" + #define AGING_TEXT "重庆创迪科技发展有限公司设备老化测试" -#define PROGRAM_CODE "9210209C41" +#define PROGRAM_CODE "9K10212482" static const display_color_t s_dead_pixel_colors[] = { COLOR_RED, @@ -92,10 +101,10 @@ static void factory_monitor_task(void *argument) dev_display_t *dsp = dev_display_get(); for (;;) { - /* IDLE: 等待 TEST 激活 */ + /* ===== IDLE: 等待 TEST 激活 ===== */ dev_key_wait_press(DEV_KEY_TST, osWaitForever); - /* ===== SHOW_CODE ===== */ + /* ===== 第1次按键 → SHOW_CODE: 显示固件/模组信息 ===== */ dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_BLACK); app_render(&(render_cfg_t){ .type = RENDER_TEXT, @@ -104,29 +113,38 @@ 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 = "FW:"PROGRAM_CODE"\nMD:1000000969", + .len = strlen("FW:"PROGRAM_CODE"\nMD:1000000969"), + .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, }, }); + /* ===== 第2次按键 → RED: 全屏红色 ===== + * 光敏自动调光生效(未暂停 light_sensor_task) */ + dev_key_wait_press(DEV_KEY_TST, osWaitForever); + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_RED); + dev_display_commit_frame(dsp); + + /* ===== 第3次按键 → GREEN: 全屏绿色 ===== */ + dev_key_wait_press(DEV_KEY_TST, osWaitForever); + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_GREEN); + dev_display_commit_frame(dsp); + + /* ===== 第4次按键 → YELLOW: 全屏黄色 ===== */ dev_key_wait_press(DEV_KEY_TST, osWaitForever); + dev_display_fill(dsp, 0, 0, dsp->screen_rows, dsp->screen_cols, COLOR_YELLOW); + dev_display_commit_frame(dsp); - /* ===== DEAD_PIXEL ===== */ + /* ===== 第5次按键 → AGING: 进入老化循环 ===== + * 暂停光敏,强制最大亮度 */ + dev_key_wait_press(DEV_KEY_TST, osWaitForever); 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); bool aging_exit = false; for (uint8_t type_idx = 0; !aging_exit; type_idx = (type_idx + 1) % AGING_TYPE_COUNT) { @@ -153,8 +171,8 @@ 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); + /* ===== 第6次按键 → 重启程序 ===== */ + NVIC_SystemReset(); } } diff --git a/Application/Src/app_light_sensor.c b/Application/Src/app_light_sensor.c index 3003f9d..5b11aa6 100644 --- a/Application/Src/app_light_sensor.c +++ b/Application/Src/app_light_sensor.c @@ -20,9 +20,15 @@ void app_light_sensor_task(void *argument) } } +void app_light_sensor_set_range(uint8_t min, uint8_t max) +{ + 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", diff --git a/Application/Src/app_render.c b/Application/Src/app_render.c index 316ac80..3b89bf7 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,107 +23,233 @@ #include "initcall.h" #include "crc_utils.h" #include "dev_w25qxx.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 */ }; -/* ---- 内部: bytes_per_char ---- */ -static inline uint16_t _glyph_bytes(font_key_t k) +/* + * 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+... */ + }, +}; + +/* ---- 当前激活的配置 ---- */ +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 码 ---- */ @@ -123,33 +258,130 @@ static bool _is_gbk(uint8_t high, uint8_t low) return (high >= 0x81 && high <= 0xFE) && (low >= 0x40 && low <= 0xFE && low != 0x7F); } +/* ---- 统计文本所需宽高(按现有文本内容、字号、换行符计算)---- */ +static bool _measure_text_box(const uint8_t *text_buf, uint16_t text_len, font_size_t font_size, + uint16_t *out_w, uint16_t *out_h) +{ + if (!text_buf || !text_len || !font_size || !out_w || !out_h) + return false; + + uint16_t line_w = 0; + uint16_t max_w = 0; + uint16_t line_cnt = 1; + uint16_t char_pos = 0; + + while (char_pos < text_len) { + if (text_buf[char_pos] == '\n') { + if (line_w > max_w) + max_w = line_w; + line_w = 0; + line_cnt++; + char_pos++; + continue; + } + + uint8_t glyph_w; + if (text_buf[char_pos] >= 0x20 && text_buf[char_pos] <= 0x7F) { + 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(font_size, FONT_ENC_GBK); + char_pos += 2; + } else { + char_pos++; + continue; + } + + line_w = (uint16_t)(line_w + glyph_w); + } + + if (line_w > max_w) + max_w = line_w; + + *out_w = max_w; + *out_h = (uint16_t)(line_cnt * font_size); + return true; +} + +/* ---- 自适应字号选择:从大到小尝试,选择可完整放入区域的最大字号 ---- */ +static font_size_t _select_adaptive_font(const uint8_t *text_buf, uint16_t text_len, uint16_t w, uint16_t h, + font_type_t font_type, const render_style_t *style) +{ + (void)font_type; + (void)style; + /* 从当前激活配置中读取自适应字号候选列表 */ + 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, &need_w, &need_h) && need_w <= w && need_h <= h) + return size; + } + + return FONT_16; +} + /* ---- 注册的句柄 ---- */ static dev_display_t *s_render_display; static dev_storage_t *s_render_font; static uint32_t s_persist_addr; -/* ---- 模块自注册,依赖storage和display模块)---- */ +/* ---- 内部: 读取 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 */ +} + +/* ---- 模块自注册(依赖 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; + + _dip_font_select(); } sw_app_initcall(_render_init); +/* ---- 字库芯片切换 API ---- */ + +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; +} + +font_chip_id_t app_render_chip_current(void) +{ + for (uint8_t i = 0; i < FONT_CHIP_CNT; i++) { + if (s_active_config == &s_chip_configs[i]) + return (font_chip_id_t)i; + } + return FONT_CHIP_W25Q64; +} + /* ---- 渲染分支(各功能静态内联)---- */ 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]; @@ -165,11 +397,19 @@ static inline void _render_text(const render_cfg_t *cfg) text_len = n; } + 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, cfg->w, cfg->h, + cfg->font_type, cfg->style); + } + 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) { @@ -182,10 +422,10 @@ static inline void _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++; @@ -242,7 +482,7 @@ static inline void _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) { @@ -264,17 +504,18 @@ static inline void _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(s_render_display, 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) { @@ -296,10 +537,11 @@ static inline void _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(s_render_display, 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; @@ -342,6 +584,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); } /* ================================================================ @@ -351,7 +594,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) { @@ -393,6 +636,8 @@ 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; diff --git a/Application/Src/app_test.c b/Application/Src/app_test.c index 86b875f..1f3c8c0 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_cols, /* 逐列 x */ - i / dsp->screen_cols, /* 逐行 y */ + i % dsp->screen_rows, /* 逐列 x */ + i / dsp->screen_rows, /* 逐行 y */ COLOR_GREEN); + dev_display_commit_frame(dsp); osDelay(50); dev_display_set_pixel(dsp, - i % dsp->screen_cols, - i / dsp->screen_cols, + 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,32 +81,134 @@ 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(); + dev_display_t *dsp = dev_display_get(); + if (!dsp) return; - /* 清空 hub75_buff,确保 prepare 不会覆盖(dirty=false) */ 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 线性位置 */ + 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); +} - /* 点亮当前像素(红色醒目) */ - dsp->hub75_buff[pos] = COLOR_RED; - osDelay(200); +/* ================================================================ + * prepare 映射测试 + * + * 通过 set_pixel 按屏幕坐标(左上→右下)逐像素点亮, + * 走完 set_pixel → prepare → scan 全链路。 + * 观察像素是否从模组左上角依次填充整个屏幕。 + * + * PASS 标准: + * 1. 像素从左上角 (0,0) 开始,逐列→逐行填充整个屏幕 + * 2. 每次新点亮的像素为纯红色,无偏色/混色 + * 3. 已点亮的像素不熄灭,逐个填充 + * + * 通过后可确保: + * - _1_260_prepare() 像素重排逻辑正确 + * + * 无法确保: + * - scan 输出时序、行选通、物理连线(由 app_test_led_mapping 验证) + * ================================================================ */ + +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); } - /* 通道切换停顿,方便标记 */ - osDelay(500); } - for (;;); // 暂停运行 + for (;;); } /* ================================================================ @@ -111,8 +217,11 @@ 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_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/Compiler/STM32F407XX_FLASH.ld b/Compiler/STM32F407XX_FLASH.ld index 33fbbda..f5b8690 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..77c503a 100644 --- a/Core/Inc/FreeRTOSConfig.h +++ b/Core/Inc/FreeRTOSConfig.h @@ -68,11 +68,12 @@ 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) #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/Src/main.c b/Core/Src/main.c index 9c7b53b..310ea14 100644 --- a/Core/Src/main.c +++ b/Core/Src/main.c @@ -13,8 +13,8 @@ void app_boot(void); int main(void) { - SCB->VTOR = FLASH_BASE | 0x40000; - // SCB->VTOR = FLASH_BASE | 0x00000; + // SCB->VTOR = FLASH_BASE | 0x40000; + SCB->VTOR = FLASH_BASE | 0x00000; __enable_irq(); HAL_Init(); 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/Device/Display/dev_display.c b/Device/Display/dev_display.c index 28d2441..ffdfcea 100644 --- a/Device/Display/dev_display.c +++ b/Device/Display/dev_display.c @@ -16,12 +16,24 @@ /* ---- 扫描任务事件 ---- */ 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 uint8_t *s_pending_frame; +static uint8_t s_frame_ready; /* ---- 实例注册(由派生模组的 hw_dev_initcall 调用)---- */ void dev_display_register(dev_display_t *dev) { s_active_display = dev; } dev_display_t *dev_display_get(void) { return s_active_display; } +void dev_display_commit_frame(dev_display_t *dev) +{ + if (!dev) + return; + s_pending_frame = dev->pixel_map; + s_frame_ready = 1U; +} + /* ---- TIM 周期回调(前向声明,实现在文件末尾)---- */ static void _on_tim3_period(void); static void _on_tim4_period(void); @@ -46,11 +58,20 @@ 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) */ + if (s_frame_ready) { + osKernelLock(); + s_frame_front = s_pending_frame; + s_frame_back = dev->hub75_buff; + s_frame_ready = 0U; + osKernelUnlock(); + dev->dirty = false; if (dev->ops->prepare) dev->ops->prepare(dev); @@ -84,7 +105,7 @@ void dev_display_start(void) const osThreadAttr_t attr = { .name = "scan_task", - .stack_size = 512, + .stack_size = 512 * 4, .priority = osPriorityRealtime, }; osThreadNew(scan_task, dev, &attr); diff --git a/Device/Display/dev_display_1_260.c b/Device/Display/dev_display_1_260.c new file mode 100644 index 0000000..84a1628 --- /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 = 7, + }, +}; + +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..375c86a --- /dev/null +++ b/Device/Display/dev_display_1_577.c @@ -0,0 +1,200 @@ +/** + * @file dev_display_1_577.c + * @brief 1-577 模组派生类型 — P5-960x320 户外全彩 (3x3 模块) + * + * 模组参数:64x32 单模块,2 通道/模块,6 通道总计,ABC 1/8 扫描。 + * 实现 dev_display_ops: prepare (pixel_map->hub75_buff) + scan (BSRR 查表输出) + * g_bsrr 查表放在 CCMRAM 中,避免 Flash 读取和分支。 + */ + +#include "dev_display.h" + +#include +#include "initcall.h" + +/* ================================================================ + * 1-577 模组参数 — P5-960x320 户外全彩 (3x3 模块) + * + * 单模块 64x32 像素,3 行 x 3 列共 9 模块,屏幕 192x96。 + * HUB75 接口 2 通道/模块,3 列共 6 通道。 + * ABC 3-bit 行地址,1/8 扫描。 + * ================================================================ */ +#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) + +/* ---- 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]; + +static dev_display_1_577_t g_1_577 = { + .me = { + .ops = nullptr, + .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 = 7, + }, +}; + +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[] 按 HUB75 物理移位顺序组织。 + * + * hub75_buff 布局(scan_line_pixels=384, channel_pixels=3072, 6ch): + * ch0..ch5 各含 channel_pixels 字节,每通道内按 scan line 排列。 + * + * 每条 scan line 的 384 字节内部排列(左32 + 间隔32 + 右32)× 3 列: + * [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 + * ...(后续列同理) + * + * 行映射(1/8 扫描,32 行模块): + * ch0, line L → Y_low = L, Y_high = L+8 + * ch1, line L → Y_low = L+16, Y_high = L+24 + * ch2, line L → Y_low = L+32, Y_high = L+40 + * ch3, line L → Y_low = L+48, Y_high = L+56 + * ch4, line L → Y_low = L+64, Y_high = L+72 + * ch5, line L → Y_low = L+80, Y_high = L+88 + * ================================================================ */ + +static void _1_577_prepare(dev_display_t *dev) +{ + const uint8_t *pixel_map = dev->pixel_map; + uint8_t *hub75_buff = dev->hub75_buff; + + for (uint16_t y = 0; y < _1_577_SCREEN_COLS; y++) { + const uint16_t ch = y >> 4; /* 0-5 (6 channels) */ + const uint16_t local_y = y & 0x0FU; /* 0-15 within channel */ + const uint16_t line = local_y & 0x07U; /* scan line 0-7 */ + const uint16_t row_type = (local_y < 8U) ? 1U : 0U; /* 1=low row, 0=high row */ + const uint16_t base = ch * _1_577_CHANNEL_PIXELS + + line * _1_577_SCAN_LINE_PX + + row_type * 32U; + + const uint8_t *src = pixel_map + (uint16_t)(y * _1_577_SCREEN_ROWS); + + for (uint16_t x = 0; x < 32U; x++) { + hub75_buff[base + x] = src[x]; /* left half X=0..31 */ + hub75_buff[base + 64U + x] = src[32U + x]; /* right half X=32..63 */ + } + } +} + +/* ================================================================ + * scan: 逐像素输出 — BSRR 查表 + CLK 脉冲 + * + * 6 通道,每像素输出 6 组 R/G/B。 + * hub75_buff 布局:ch0..ch5 各含 channel_pixels(3072) 字节。 + * 每条 scan line 的384字节在 ch 内连续存放。 + * ================================================================ */ + +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: 预计算 BSRR 查表 + 绑定 ops + * + * 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 (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..88f3df6 --- /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 = 7, + }, +}; + +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/IO/dev_light_sensor.c b/Device/IO/dev_light_sensor.c index ed0be4b..27268ea 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 = 7; +} + +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; } 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 7, 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->min_level) level = dev->min_level; + if (level > dev->max_level) level = dev->max_level; + return level; } diff --git a/Device/Inc/dev_display.h b/Device/Inc/dev_display.h index 3b1fef5..51ff99d 100644 --- a/Device/Inc/dev_display.h +++ b/Device/Inc/dev_display.h @@ -28,38 +28,39 @@ typedef struct dev_display dev_display_t; /* ---- 操作虚表 ---- */ typedef struct dev_display_ops { - void (*prepare)(dev_display_t *dev); /* 像素→发送缓存 (dirty 时调用) */ + void (*prepare)(dev_display_t *dev); /* 像素→发送缓存 (dirty 时调用) */ void (*scan)(dev_display_t *dev, uint8_t line); /* 输出一个扫描行 */ - void (*set_row)(uint8_t row); /* ABCD 行地址编码 */ + void (*set_row)(uint8_t row); /* ABCD 行地址编码 */ } dev_display_ops_t; /* ---- 基类 (派生类必须将其放在第一个成员位置) ---- */ struct dev_display { - const dev_display_ops_t *ops; /* 第一个成员 */ + const dev_display_ops_t *ops; /* 第一个成员 */ /* 通用参数 */ - uint8_t module_rows; /* 单模块像素行数 */ - uint8_t module_cols; /* 单模块像素列数 */ - uint8_t channels_per_module; /* 每模块通道数 */ - uint8_t modules_per_row; /* 每行模块数 */ - uint8_t modules_per_col; /* 每列模块数 */ - uint8_t scan_lines; /* 扫描行数 (静态=1, 1/4扫=4...) */ + uint8_t module_rows; /* 单模块像素行数 */ + uint8_t module_cols; /* 单模块像素列数 */ + uint8_t channels_per_module; /* 每模块通道数 */ + uint8_t modules_per_row; /* 每行模块数 */ + uint8_t modules_per_col; /* 每列模块数 */ + uint8_t scan_lines; /* 扫描行数 (静态=1, 1/4扫=4...) */ /* 派生参数 */ - uint16_t screen_rows; /* = modules_per_row * module_rows */ - uint16_t screen_cols; /* = modules_per_col * module_cols */ - uint8_t total_channels; /* = modules_per_col * channels_per_module */ - uint16_t channel_pixels; /* = module_rows * module_cols * modules_per_row / total_channels */ - uint16_t scan_line_pixels; /* = channel_pixels / scan_lines */ - uint16_t buffer_size; /* = screen_rows * screen_cols */ + uint16_t screen_rows; /* = modules_per_row * module_rows */ + uint16_t screen_cols; /* = modules_per_col * module_cols */ + uint8_t total_channels; /* = modules_per_col * channels_per_module */ + uint16_t channel_pixels; /* = module_rows * module_cols * modules_per_row / total_channels */ + uint16_t scan_line_pixels; /* = channel_pixels / scan_lines */ + uint16_t buffer_size; /* = screen_rows * screen_cols */ /* 缓冲区 (CCMRAM,派生实例静态分配) */ uint8_t *pixel_map; uint8_t *hub75_buff; /* 运行时 */ + const char *module_code; /* 模组编码字符串,由派生模组绑定 */ volatile uint8_t light_level; - volatile bool dirty; + volatile bool dirty; }; /* ---- 通用 API ---- */ @@ -70,20 +71,34 @@ 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 */ 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); + 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 +106,8 @@ void dev_display_register(dev_display_t *dev); /** @brief 获取当前活动显示实例 */ dev_display_t *dev_display_get(void); +/** @brief 将当前逻辑帧提交到扫描帧,供 scan_task 读取 */ +void dev_display_commit_frame(dev_display_t *dev); + /** @brief 设置亮度 (0=最暗/关闭, 7=最亮),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..8a8f375 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 + +/** 自动调光默认最高亮度等级(出厂约束:不允许高于 7 级) */ +#define LIGHT_SENSOR_MAX_LEVEL 7 + typedef struct { void *adc; dev_display_t *display; + uint8_t min_level; /* 自动调光最低亮度 (1~7) */ + uint8_t max_level; /* 自动调光最高亮度 (1~7) */ } 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~7, 0=不关屏) + * @param max 最高亮度等级 (1~7) + * + * min/max 会被自动钳位到 1~7,且 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/Network/dev_eth.c b/Device/Network/dev_eth.c index 3e55822..57cc7c9 100644 --- a/Device/Network/dev_eth.c +++ b/Device/Network/dev_eth.c @@ -17,12 +17,12 @@ #include "pl_dwt.h" #include "initcall.h" -/* 板级默认网络配置: 192.168.2.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.0.232/24, 网关 192.168.1.1 */ +// static const uint8_t g_default_ip[4] = {192, 168, 0, 1}; +static const uint8_t g_default_ip[4] = {192, 168, 0, 232}; 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, 0, 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..bdfea2b 100644 --- a/Device/Storage/dev_w25qxx.c +++ b/Device/Storage/dev_w25qxx.c @@ -100,6 +100,8 @@ 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}; @@ -282,7 +284,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/Makefile b/Makefile index 3d869c5..219d9e0 100644 --- a/Makefile +++ b/Makefile @@ -269,7 +269,7 @@ 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 \ @@ -293,6 +293,7 @@ SRC_APPLICATION = \ 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/AH_MQTT/ah_mqtt.c \ Application/Src/AH_MQTT/ah_mqtt_cmd.c \ diff --git a/Platform/Inc/pl_hub75.h b/Platform/Inc/pl_hub75.h index 323e9e2..e15c5dc 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,63 @@ __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) +{ + 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/Project_STD.code-workspace b/Project_STD.code-workspace index 394f3a3..616c321 100644 --- a/Project_STD.code-workspace +++ b/Project_STD.code-workspace @@ -26,7 +26,7 @@ "editor.tabSize": 4, "editor.autoIndent": "advanced" }, - "cortex-debug.variableUseNaturalFormat": false, + "cortex-debug.variableUseNaturalFormat": true, "makefile.configureOnOpen": false, "makefile.launchConfigurations": [ { 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/\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/CLAUDE.md b/doc/CLAUDE.md similarity index 98% rename from CLAUDE.md rename to doc/CLAUDE.md index 9baff7c..c4bdc97 100644 --- a/CLAUDE.md +++ b/doc/CLAUDE.md @@ -504,7 +504,9 @@ app_key key_poll_task (20ms) → 轮询 GPIO 实际电平确认释放 ### 光传感器 (`dev_light_sensor.c`) -ADC1 采样光敏电阻分压(LDR:光越暗→电阻越大→ADC 值越高),8 次均值滤波后映射为亮度等级 1~7。`dev_light_sensor_auto_adjust()` 自动更新 `display->light_level`。 +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 秒周期的自动调节任务。 diff --git "a/doc/\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/\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..d78b551 Binary files /dev/null and "b/doc/\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/\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.pdf" "b/doc/\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.pdf" new file mode 100644 index 0000000..d2877d9 Binary files /dev/null and "b/doc/\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.pdf" differ