OpenHarmony 小鸿 AI 开发实战 02:从板级源码读懂 WS63、CI1302、ST7789 与外部 Flash
第 01 篇已经确认当前小鸿本体走的是 xiaohong 这条 WS63/OpenHarmony 主线。继续开发之前,还要把板级源码中的引脚、总线和器件职责理清,否则很容易把一个 GPIO 当成普通按键、把两类 Flash 混成同一存储空间,或者在 LCD 与外部 Flash 共享 SPI 时同时拉低两个片选。
本文标题刻意使用“从板级源码读懂”,因为当前证据主要来自 board_config.h、board_config.c、BUILD.gn 和各驱动文件,而不是一份完整公开的整机原理图。能够从源码确认的内容会给出真实代码;需要原理图、示波器或实机测量才能确认的电气细节,不会根据芯片名称自行补齐。

先界定本文能证明到哪一层
当前 vendor/atomgit/xiaohong/config.json 把产品定义为 OpenHarmony mini,内核为 LiteOS-M;vendor/atomgit/xiaohong/xiaohong/BUILD.gn 在 xiaohong_ws63_v1 板型下收录 board_config.c、key_config.c 和 w25q128.c,还收录 ST7789/LVGL 与 CI1302 音频文件。由此可以确认这些模块属于当前固件的构建链。
源码还能确认 GPIO 编号、复用模式、串口参数、SPI 频率、LCD 分辨率、外部 Flash 地址空间和初始化顺序。但仅凭这些代码不能证明每根走线的电气参数、板上实际贴片批次或所有器件都已经在当前候选固件上完成回归。本文的结论层级是“源码确认”,已知的实机事实会单独注明。
WS63 是板级控制和联网的中心
从当前文件关系看,WS63 承担四类工作:通过 GPIO/ADC/PWM 处理按键、电池和背光;通过 SPI1 驱动 ST7789 与外部 W25Q128;通过 UART2 与 CI1302 交换控制和音频帧;在 LiteOS-M 任务与消息队列中运行显示、网络、Agent 和音频逻辑。
board_hw_init() 是板级入口。下面代码摘自 vendor/atomgit/xiaohong/xiaohong/src/boards/xiaohong_ws63_v1/board_config.c,函数按照 GPIO、按键、SPI1 和 UART 的顺序初始化。代码中没有凭文章需要增加不存在的初始化步骤。
errcode_t board_hw_init(void)
{
errcode_t ret = ERRCODE_SUCC;
static uint8_t hw_inited = 0;
if (hw_inited) {
printf("[BoardCfg] board_hw_init: hw_inited[1]\r\n");
return ERRCODE_SUCC;
}
ret |= gpio_init();
ret |= key_init();
ret |= spi1_init();
ret |= uart_init();
if (ret == ERRCODE_SUCC) {
hw_inited = 1;
printf("[BoardCfg] board_hw_init: hw_inited[0->1]\r\n");
}
return ret;
}
这个入口还说明一个排障原则:LCD、外部 Flash 和 CI1302 都依赖板级总线初始化。上层 LVGL、字库下载或语音状态机报错时,应先确认底层 board_hw_init() 是否成功,而不是直接把问题归因于页面或服务器。

引脚表来自 board_config.h,不来自连续 GPIO 猜测
当前 vendor/atomgit/xiaohong/xiaohong/src/boards/board_config.h 集中定义 WS63 V1 的板级资源。下面是真实宏定义的必要摘录。按键是 P13、P12 和 P11;电池采样是 GPIO10/ADC3;LCD 控制脚、SPI1 和 UART2 也在同一文件中定义。
#define BAT_LEVEL_PIN (10) // GPIO10:ADC[3]
#define BAT_LEVEL_ADC_CHN (3)
#define KEY_VOLUP_PIN (13)
#define KEY_VOLDW_PIN (12)
#define KEY_WAKEUP_PIN (11)
#define PWR_ON_RESERVED_PIN (14)
#define FLASH_CS_PIN (0)
#define LCD_RST_PIN (9)
#define LCD_DC_PIN (4)
#define LCD_CS_PIN (5)
#define LCD_BLK_PIN (2)
#define SPI_IDX (1)
#define SPI_WORKING_FREQUENCY (40)
#define SPI_CLK_PIN (6)
#define SPI_MOSI_PIN (1)
#define SPI_MISO_PIN (3)
#define UART2_RXD_PIN (7)
#define UART2_TXD_PIN (8)
#define UART_BAUDRATE (921600)
这些数字的价值不是让读者背诵,而是建立“一个引脚只有一个所有者”的板级规则。修改按键、探针、中断或电源控制前,先查这个表和对应初始化代码,再查原理图;不能看到 P11~P14 连续就把四个脚一起扫描。

GPIO14 是 PWR_ON 红线,不是第四个按键
当前源码把 GPIO14 命名为 PWR_ON_RESERVED_PIN,注释明确说明它用于电源保持或显示供电相关链路,不能按普通按键初始化。文件还增加了编译期检查,只要 P11、P12、P13 中任一按键宏被改成 14,构建就直接报错。
#if (KEY_VOLUP_PIN == PWR_ON_RESERVED_PIN) || \
(KEY_VOLDW_PIN == PWR_ON_RESERVED_PIN) || \
(KEY_WAKEUP_PIN == PWR_ON_RESERVED_PIN)
#error "GPIO14 is PWR_ON and must not be initialized by key_config"
#endif
key_config.c 的轮询范围也固定为 11~13,并为三个按键分别构造状态。历史故障包曾把中键改到 GPIO14,又扫描 P11~P14;该版本虽然能由 BurnTool 完成写入,设备却出现黑屏。回刷已知良好包后显示恢复,说明硬件并未因此被直接判定损坏,也把排查范围收回到应用镜像与 GPIO 初始化。当前源码已经恢复 P11、排除 P14,但本轮尚未重新构建、烧录并完成三键与显示联合回归,因此不能把“源码已改”写成“修复已实机闭环”。完整因果复盘留到第 05 篇,本篇只把 P14 作为引脚所有权红线。
ST7789 与 W25Q128 共享 SPI1
板级宏为 SPI1 定义 P6 时钟、P1 MOSI、P3 MISO,工作频率为 40 MHz。ST7789 使用 P5 作为 LCD 片选,W25Q128 使用 P0 作为 Flash 片选。两者共享总线,所以每次访问前必须保证另一个设备处于未选中状态。

这不是从“两个驱动都叫 SPI”推断出来的。board_config.c 中的 spi_cs_pin_race() 明确检查两个片选:选择 LCD 之前要求 Flash CS 为高,选择 Flash 之前要求 LCD CS 为高。下面是当前函数的真实代码。
errcode_t spi_cs_pin_race(uint8_t pin, uint8_t level)
{
if (level) {
return uapi_gpio_set_val(pin, level);
} else {
if ((pin == LCD_CS_PIN) &&
(GPIO_LEVEL_HIGH == uapi_gpio_get_val(FLASH_CS_PIN))) {
return uapi_gpio_set_val(pin, level);
}
if ((pin == FLASH_CS_PIN) &&
(GPIO_LEVEL_HIGH == uapi_gpio_get_val(LCD_CS_PIN))) {
return uapi_gpio_set_val(pin, level);
}
}
return ERRCODE_FAIL;
}
LCD 写入路径通过 lcd_set_cs_pin() 间接调用该互斥函数,Flash 路径则通过 flash_set_cs_pin() 调用。两边都有 50 次、每次 1 ms 的等待上限。文章不能把共享总线写成“完全没有竞争”;准确说法是当前代码用独立片选和软件检查避免同时选中,真实高负载稳定性仍需在显示刷新与字库读写同时发生时验证。
ST7789 的 240×240 与背光 PWM 是两条控制链
lcd_st7789_driver.h 将面板尺寸定义为 240×240,像素写入经 disp_driver.c 设置 DC、片选并调用 uapi_spi_master_write()。屏幕内容能写入,不代表背光一定点亮;背光由 GPIO2 的 PWM 端口单独控制。
disp_driver.h 当前定义默认亮度 40、非零可见下限 12、默认频率 1230 Hz。disp_set_backlight() 会对亮度和频率做边界处理,再关闭并重新打开 PWM 通道。由此可以把黑屏排查拆成三段:PWR_ON/供电保持、ST7789 初始化与像素写入、GPIO2/PWM 背光。仅凭“SPI 写入没有报错”不能证明后两段都正常。
CI1302 通过 UART2 承担语音前端
板级代码确认 WS63 使用 UART2 的 P8 TX、P7 RX,与 CI1302 通信参数为 921600、8 数据位、1 停止位、无校验。board_config.c 的 uart_init() 把这组宏传给 uapi_uart_init();CI1302 的监听和播放任务也由 BUILD.gn 收录。

协议含义不能只根据注释猜测,还要看当前解析分支。ci1302_data_process.c 把 0x0102 转成唤醒事件,把 0x0105 的负载作为 Opus 上行,把 0x0106 转成本段 VAD 结束;0x020A 会触发发送下一块音频,0x020D 在下行缓存未空时复用同一路径。当前 ci1302_tts_downlink.h 明确说明 0x020B 发送的是 WS63 解码后的 16 kHz PCM,而不是把服务器 Opus 裸流直接交给 CI1302。
本文只建立板级边界,不展开完整帧头、校验、环形缓冲和末帧顺序;这些内容留到语音上行和 TTS 下行文章。这样可以避免一篇硬件前端文章被协议细节淹没。
外部 W25Q128 是 16 MiB,不等于 4 MiB 固件备份
w25q128.h 将 W25X_P0_SIZE 定义为 0x01000000U,即 16 MiB;页大小为 256 字节,扇区大小为 4096 字节。littlefs_config.h 把 LittleFS 的外部 Flash 范围绑定到这个地址和容量,littlefs_adapt.c 则通过 W25QXX_Read、W25QXX_Prog 和扇区擦除完成块设备操作。
#define W25X_P0_ADDR (0x00000000U)
#define W25X_P0_SIZE (0x01000000U)
#define W25X_PAGE_SIZE (256U)
#define W25X_SECTOR_SIZE (4096U)
此前通过 BurnTool 验证的 4 MiB 原始备份采用 GD25Q32/0x400000 容量口径,属于 WS63 固件备份链。它与 SPI1 上用于 LittleFS 和 font.bin 的 16 MiB W25Q128 不是同一用途,也不能在文章中合并成“设备只有 4 MiB Flash”或“BurnTool 导出了整个 W25Q128”。

电池采样代码已接通,但不能直接当精确电量计
GPIO10 被定义为 ADC3,gpio_init() 调用 uapi_adc_init(),settings.c 的 get_voltage_level() 通过 adc_port_read(BAT_LEVEL_ADC_CHN, &voltage) 读取数值,再映射成界面使用的等级;lvgl_task.c 最终选择不同电池图标显示。
这条源码链能够证明“ADC 读数进入状态栏”,但当前映射注释和整数换算并不是完整的电池电压标定报告。没有万用表对照、分压电阻参数、充放电曲线和温度条件时,不应把状态栏等级写成高精度剩余电量百分比。文章使用“电池等级/状态显示”而不是“精确 SOC”。
当前代码已经确认什么,还缺什么
已确认的源码事实包括:当前产品是 OpenHarmony mini/LiteOS-M 的 WS63 工程;三个按键位于 P13、P12、P11;P14 是 PWR_ON 保留脚;ST7789 为 240×240;LCD 与 W25Q128 共享 SPI1、40 MHz、独立片选;背光使用 GPIO2/PWM;CI1302 使用 UART2、921600、8N1;外部 W25Q128 的代码容量为 16 MiB;GPIO10/ADC3 的读数进入电池状态显示。
本文没有用源码替代整机原理图,也没有在本轮重新测量 SPI 波形、UART 波特率误差、背光 PWM、电池标定或外部 Flash 实际 JEDEC ID。当前源码中的关键文件已经保存 SHA-256,图片由这些定义确定性生成;源码发生变化时,校验器会要求重新核对文章。下一篇应在建立可回滚源码快照后,记录一次 OpenHarmony WS63 的干净全量构建、输出目录、包大小和 SHA-256。
更多推荐


所有评论(0)