第2章:平台与构建——SiFli SDK + SCons

换平台第一关:用一套新工具链,把代码变成能烧进芯片的固件

🔨

本章导师:鲁班

核心方法论:工欲善其事,必先利其器

「上章我们看清了要搬什么,这章就来打造能搬的『车』。你不必迷恋工具本身,但必须认得工具的门道:SCons 是怎么把几千个源文件串成一条线的、board 参数背后那块板子的脾气、menuconfig 里每个开关对应芯片里的哪块硬件。工具认得清,动手才不慌。」

2.1 构建工具链:SCons 与 SiFli SDK

先认主角。SCons 是什么?一句话说人话:一个用 Python 写的构建系统(构建系统是"把源代码编译成可执行程序"的流水线总管,传统上由 Make 或 CMake 扮演)。它用 SConstruct / SConscript 两个文件描述"怎么编",而这两个文件本身就是 Python 脚本——所以它比 Make 的语法灵活得多,还能直接调用 Python 生态里的工具。它跟编译器(比如 GCC)是两回事:编译器负责把 .c 变成 .o,SCons 负责决定"哪些文件要编、按什么顺序、编完放哪"。上季我们用 PlatformIO 时,这些工作被藏起来了;这季 SCons 把它们全摊在你面前。

第二个主角是 SiFli SDK。前面说过,SDK 是芯片厂商提供的"软件开发工具包"。思澈的这份 SDK 特别之处在于:它是基于 RT-Thread 定制的一套固件开发框架(RT-Thread 是一个国产开源实时操作系统,类似 FreeRTOS,但自带文件系统、消息队列等组件),官方一句话总结:"使用它可以快速开发运行于思澈科技芯片平台的应用程序"。它之于 SF32,就像 ESP-IDF 之于 ESP32、HAL 库之于 STM32——芯片寄存器、外设驱动、启动代码、量产工具全在里面,应用工程师主要写自己的业务逻辑,需要外设时调 SDK 的 API。本仓库用的是 v2.5.0 版本(见 SDK 根目录 version.txt)。

在敲任何构建命令之前,有一个绕不开的步骤:设置环境变量 SIFLI_SDK。SCons 脚本要靠它找到 SDK 装在哪。Windows 上在 SDK 根目录运行 set_env.bat(PowerShell 用户用 export.ps1),Linux/macOS 上 source ./export.sh。如果你跳过这步直接 scons,SConstruct 会当场报错并提示你去跑 set_env.bat——这不是 bug,是构建系统的"前门"。

最后认识一下 epdiy-epub/project/ 里的构建文件族:SConstruct 是整个构建的入口(相当于 Make 的 Makefile 顶层);SConscript 负责把 SDK、lib/src/font/ 各子工程拉进来;Kconfig.proj 是项目级配置(2.4 讲);还有一个容易被忽视的 rtconfig.py——芯片型号、核、目标名、编译器都声明在板级配置目录(如 sf32-oed-epd_v11/hcpu/rtconfig.py),构建时会被读进 SCons 环境。理解"配置放哪"比背命令重要得多。

角色本季用的工具它做什么
构建系统SCons调度编译:读 SConstruct / SConscript,按依赖关系编译、链接
编译器 / 链接器GCC 系(rtconfig.py 里声明).c/.cpp 编成机器码并链接成固件
固件开发框架SiFli SDK(基于 RT-Thread)芯片驱动、RTOS、中间件,类似 ESP-IDF 之于 ESP32
项目配置Kconfig + proj.conf / board.conf编译期的开关,决定编进哪些驱动与功能
构建入口project/SConstruct把 SDK + 库 + 应用 + 字体 + 文件系统镜像串成一条线
# Windows(在 SiFli-SDK 根目录执行;PowerShell 用 export.ps1)
set_env.bat

# Linux / macOS
source ./export.sh
# epdiy-epub/project/SConstruct(节选:先检查 SDK 环境变量)
SIFLI_SDK = os.getenv('SIFLI_SDK')
if not SIFLI_SDK:
    print("Please run set_env.bat in root folder of SIFLI SDK to set environment.")
    exit()
鲁班提示

别被 SCons 和 Make 的差别吓到——它们的核心概念完全一样:都有"入口脚本""目标""依赖"。你只要记住三个词:SConstruct(入口)、SConscript(子模块脚本)、rtconfig.py(工具链与芯片信息)。遇到不认识的 SCons 语法,去查 Python,因为 SCons 脚本就是 Python。

2.2 板级配置与编译:board、hcpu/lcpu 与多板

什么是板级配置?一句话:针对"某一块具体的开发板",把芯片引脚、外设、内存布局等硬件差异整理成一份配置,编译时据此生成合适的固件。芯片可以相同,板子却各有各的接法——就像同一颗 CPU,不同厂商的主板排线不同。本项目一口气带了 四套板级配置sf32-oed-epd_base(V1.1 与 V1.2 共用的公共配置)、sf32-oed-epd_v11(V1.1)、sf32-oed-epd_v12(V1.2)、sf32-oed-epd_v12_spi(V1.2 但墨水屏走 SPI 接口)。v11、v12 两套的 Kconfig.board 都只有一行 source "../sf32-oed-epd_base/Kconfig.board",把公共配置引进来;而 v12_spi 自成一派,不 source base,直接在自己的 Kconfig.board 里写死了触控、EPD 的各引脚号。

编译命令长这样,必须在 epdiy-epub/project 目录下执行

# 在 epdiy-epub/project 目录下
scons --board=sf32-oed-epd_v11 --board_search_path=.. -j8

拆开看三个参数:--board=sf32-oed-epd_v11 告诉 SCons"给哪块板子编",它会在搜索路径里找名为 sf32-oed-epd_v11 的板级配置目录;--board_search_path=.. 是搜索路径,指向上一级目录——也就是 epdiy-epub/ 根目录,四套板级配置都在那;-j8 是"8 个任务并行编译"(j 是 job 的缩写,跟 Make 的 -j 一个意思,能明显加速)。想给 V1.2 板编译,把 --board 换成 sf32-oed-epd_v12sf32-oed-epd_v12_spi 即可。编译产物会生成在 project/build_sf32-oed-epd_v11_hcpu/ 这类以 build_<板名>_hcpu 命名的目录里。

这里的 hcpu 后缀引出一个 SiFli 平台的招牌概念:双核(HCPU + LCPU)。HCPU(高性能核)是跑应用的主核——本项目的主循环、EPUB 解析、界面渲染都在它上面;LCPU(低功耗核)是贴心的协处理器,负责在系统休眠时用极低功耗盯着按键、定时器等,把 HCPU 放回睡眠。在 project/SConstruct 里有一行 AddLCPU(SIFLI_SDK, rtconfig.CHIP, "../../src/lcpu_img.c"),意思是"把 LCPU 的固件作为一个镜像编进主固件"——也就是说,你只需要 scons 一次,主核固件 + 协处理器镜像会一起产出,无需单独编两次。

看一眼 SConstruct 的完整流水线,就明白一次构建做了多少事:先 PrepareEnv 准备环境,AddBootLoader 加引导程序,AddLCPU 加协处理器镜像,PrepareBuilding 收集所有源码,DoBuilding 正式编译链接出主固件;随后 AddFTAB 生成分区表(ftab),FileSystemBuild("../disk", env)disk/ 里的样书打包成文件系统镜像 fs_root 烧进 Flash,若开了 EPD_WAVEFORM_USE_BIN 还会把 waveform/epd_waveform.bin 作为波形表镜像加进去;最后 GenDownloadScript 自动生成下载脚本。也就是说:样书、波形、分区表、引导程序,全部由一次 scons 打包进最终镜像——这是 SCons 帮这个项目省掉的最大工程量。

板级配置目录适用硬件与 base 的关系特点
sf32-oed-epd_baseV1.1 / V1.2 共用—(被引用方)bsp_init / bsp_lcd_tp / bsp_pinmux 等公共板级源码
sf32-oed-epd_v11SF32-OED-EPD V1.1source basehcpu / lcpu / ptab.json 齐全
sf32-oed-epd_v12SF32-OED-EPD V1.2source base与 v11 共用 base 板级源码
sf32-oed-epd_v12_spiV1.2 + SPI 墨水屏独立(不 source base)顶层自带 bsp_*.c,Kconfig.board 写死 EPD/触控引脚
# 编译与 menuconfig(在 epdiy-epub/project 下)
scons --board=sf32-oed-epd_v11 --board_search_path=.. -j8
scons --board=sf32-oed-epd_v11 --board_search_path=.. --menuconfig
# epdiy-epub/project/SConstruct(构建流水线,节选)
AddBootLoader(SIFLI_SDK, rtconfig.CHIP)                  # 引导程序
AddLCPU(SIFLI_SDK, rtconfig.CHIP, "../../src/lcpu_img.c") # LCPU 协处理器镜像
...
AddFTAB(SIFLI_SDK, rtconfig.CHIP)                        # 生成分区表 ftab
fs_bin = FileSystemBuild("../disk", env)              # disk/ 打包为文件系统镜像
AddCustomImg("fs_root", bin=[fs_bin])
GenDownloadScript(env)                                   # 生成 uart_download.bat
# epdiy-epub/sf32-oed-epd_v11/ 板级配置目录
sf32-oed-epd_v11/
  hcpu/          HCPU 主核:board.conf / Kconfig.board / rtconfig.py
  lcpu/          LCPU 低功耗协处理器:同结构配置
  ptab.json      分区表(2.4 详讲)
  Kconfig.board  source "../sf32-oed-epd_base/Kconfig.board"
  SConscript     本板驱动构建脚本
鲁班提示

看到 _hcpu 就联想到"双核",方向就对了:build_<板名>_hcpu 是主核固件的产物目录;你不需要为 LCPU 单独跑一次编译。想知道这块芯片平时怎么省电?答案就在 LCPU 上——第 6 章讲电池与低功耗时会把它揭个底朝天。

2.3 烧录:uart_download.bat

编译只是把固件做出来,真正让它跑起来还要烧录——把固件镜像通过某种通道写进芯片的 Flash(回忆第 1 章:Flash 是掉电不丢的存储区,固件就住在这里)。本项目官方推荐的通道是 UART 下载(UART 即串口,最简单的"一条收一条发"的通信方式;UART 下载就是给芯片装个"收件箱",用串口把固件一段段送进去)。上一节提到,SConstruct 末尾的 GenDownloadScript(env) 会自动生成下载脚本,它在编译产物目录里:

# README · 编译与烧录(在 epdiy-epub/project 下编译完成后)
build_sf32-oed-epd_v11_hcpu\uart_download.bat

注意它是个 Windows 批处理脚本(.bat),路径里的反斜杠是 Windows 风格——这个项目主要面向 Windows 开发环境(前面 set_env.bat、SCons 里的 mingw 工具集也都是 Windows 导向的)。运行它的步骤很简单:把开发板用 USB 连到电脑、装好串口驱动,然后执行这个脚本,按脚本提示输入开发板对应的串口号,脚本就会通过串口把固件烧进芯片。整个过程是"脚本问一句、你答一句"的交互式下载,不需要额外的烧录软件。

如果没连板子、或串口号输错,脚本会卡在等待握手或直接报错——这时先回设备管理器确认串口号(Windows 里 USB 转串口通常显示为 COM3COM5 之类的名字),再重试。除了 UART 下载,SDK 也支持用调试器下载:板级配置里那句 JLINK_DEVICE = 'SF32LB52X_NOR' 表明 J-Link(一款流行的调试器,不仅能烧录还能在线断点调试)可以直连芯片的 NOR Flash。对新手,先走通 uart_download.bat 即可;想进阶调试再上 J-Link。

烧录 / 调试方式怎么用优点缺点
UART 下载脚本运行 uart_download.bat,输入串口号零额外硬件,只需 USB 线,官方推荐只能下载,不能在线调试;依赖串口环境
J-Link 调试器硬件连接 SWD 接口(JLINK_DEVICE='SF32LB52X_NOR'可断点调试、单步、读寄存器需要额外硬件,上手门槛高
# 一次典型烧录交互(示意)
$ build_sf32-oed-epd_v11_hcpu\uart_download.bat
请输入串口号: 3        # 输入 COM3,脚本开始下载
Downloading firmware... OK
# 为什么脚本是"生成"出来的?因为 GenDownloadScript 会按当前配置动态生成
# epdiy-epub/project/SConstruct(节选)
GenDownloadScript(env)   # 在 build_<板名>_hcpu/ 下生成 uart_download.bat

# 板级 rtconfig.py 里声明了芯片型号,下载脚本据此定位目标芯片
CHIP = 'SF32LB52X'
鲁班提示

烧录失败九成是三个原因:串口号填错、串口被别的软件占用(比如串口监视器)、驱动没装好。排查顺序就按这个来。另外记住一句话:固件在哪、下载脚本就在哪——build_<板名>_hcpu/ 目录就是编译产物的"收件箱",凡是问"编出来的东西在哪个目录",答案都在这里。

2.4 menuconfig 与分区表:Kconfig.proj 与 ptab.json

最后一个工具是 menuconfig:一个字符界面的交互式配置菜单(Kconfig 是 Linux 内核首创的配置语法,menuconfig 是它的图形化操作界面)。它把所有编译期开关组织成树状菜单,你用方向键翻、回车选、空格切换勾选,最后保存生效——比手改配置头文件直观得多。运行方式就是上一节出现过的:

# 在 epdiy-epub/project 目录下
scons --board=sf32-oed-epd_v11 --board_search_path=.. --menuconfig

这些开关具体管什么?打开 project/Kconfig.proj(项目配置的"总目录"),能看到几类关键选择。一是帧缓冲色深EPD_EPUB_COLOR_DEPTH 在 1BPP(1 位色,纯黑白)和 4BPP(4 位色,16 级灰)之间选,SPI 小屏(OPM037A3)默认 1BPP,大屏默认 4BPP。二是屏幕驱动选择choice "Custom LCD driver" 里挂着四块屏(YZC085 V1.1 版、YZC085 V1.2 版、R7D005、OPM037A3 SPI 屏),并且按板子自动设默认——default LCD_USING_EPD_YZC085_V100 if BSP_USING_BOARD_SF32_OED_EPD_V11,意思就是"V1.1 板默认选这块屏"。三是每块屏配套的分辨率与 DPI 宏LCD_HOR_RES_MAX / LCD_VER_RES_MAX / LCD_DPI(DPI 是每英寸像素密度,墨水屏选字体和排版时要用)。

配置还有一个"优先级"规则值得记住:proj.conf 项目配置 > board.conf 板级配置 > Kconfig 默认值project/proj.conf 是一份项目级默认配置(比如 CONFIG_PKG_FREETYPE=y 打开 FreeType 字体库、CONFIG_BSP_USING_PM=y 打开电源管理、CONFIG_RT_MAIN_THREAD_STACK_SIZE=65536 给主线程 64 KB 栈);每个板级目录的 board.conf 再覆盖板子相关的(比如打开 GPIO/UART/SPI 等外设);最后才是 Kconfig 里写的默认值。menuconfig 里改的配置最终会写入 .config 并生成 rtconfig.h,被代码 #include 使用。

最后认识一下 ptab.json(分区表)——它是芯片 Flash/内存的"楼盘户型图",用 JSON 定义每块区域从哪里开始、多大、放什么。打开 sf32-oed-epd_v11/ptab.json 可以看到:主存储 flash20x12000000 开始,依次划出引导程序区(bootloader)、DFU 升级区、HCPU_FLASH_CODE 主固件区(0x200000 起、2 MB,标着 "img": "main",支持 XIP 原地执行)、BUILTIN_RESOURCE 内置资源区、fs_root 文件系统区(disk/ 里的样书就装这)以及 wave_table 波形表区;另有 psram1(外挂 PSRAM 数据区)、hpsys_ram(主核 RAM,含 HCPU↔LCPU 的邮箱 HCPU2LCPU_MB_CH1/CH2_BUF)和 lpsys_ram(低功耗系统 RAM)。看懂它,你就知道"哪本书烧在哪个地址、固件和资源怎么不打架"了。

分区(tags)起始地址 / 大小放什么
bootloader0x12010000 / 64 KB引导程序
DFU_FLASH_CODE0x12020000 / 384 KBDFU 升级代码
HCPU_FLASH_CODE0x12200000 / 2 MB主固件("img": "main",可 XIP 执行)
BUILTIN_RESOURCE0x12400000 / 10 MB内置资源
FS_REGIONfs_root0x12e00000 / 768 KBdisk/ 样书等文件系统镜像
CUSTOM_EPD_WAVE_TABLE0x12ec0000 / 128 KB墨水屏波形表 wave_table
PSRAM_DATA0x60000000 / 8 MB外挂 PSRAM 数据区
# epdiy-epub/project/Kconfig.proj(节选:屏幕驱动选择)
choice
    prompt "Custom LCD driver"
    default LCD_USING_EPD_YZC085_V100 if BSP_USING_BOARD_SF32_OED_EPD_V11
    default LCD_USING_EPD_YZC085_V100_V12 if BSP_USING_BOARD_SF32_OED_EPD_V12
    config LCD_USING_EPD_YZC085_V100
        bool "6.0 rect electronic paper display (EPD YZC085_V1.05 1032x758) for V1.1 board"
    config LCD_USING_EPD_OPMO37A3
        bool "3.7 rect electronic paper display SPI (OPM037A3 240x416)"
endchoice

config LCD_HOR_RES_MAX
    int
    default 1032 if LCD_USING_EPD_YZC085_V100
# epdiy-epub/sf32-oed-epd_v11/ptab.json(节选:主固件区)
{ "offset": "0x00200000", "max_size": "0x00200000",
  "tags": ["HCPU_FLASH_CODE"], "img": "main", "exec": "main" }
# epdiy-epub/project/proj.conf(项目级默认配置,节选)
CONFIG_PKG_FREETYPE=y               # 打开 FreeType 字体库
CONFIG_BSP_USING_PM=y               # 打开电源管理
CONFIG_RT_MAIN_THREAD_STACK_SIZE=65536 # 主线程栈 64 KB
鲁班提示

把三份配置文件当成"三层滤镜":proj.conf 管"这个项目要什么"(FreeType、电源管理、蓝牙),board.conf 管"这块板子有哪些外设"(GPIO、UART、SPI、触控),Kconfig 默认值管"厂里给的老底"(各宏的兜底)。改配置时先想清楚你要动的是哪一层——改错层,要么没效果,要么被更上层覆盖。

章末练习

练习 1:认识构建命令 入门

解释下面这条命令里每个参数的意思,并说明它应该在哪个目录下执行:

scons --board=sf32-oed-epd_v11 --board_search_path=.. -j8
提示

对照 2.2:--board 选板子,--board_search_path 指搜索路径,-j8 并行数;入口脚本 SConstructproject/ 里。

参考答案

epdiy-epub/project/ 目录下执行。--board=sf32-oed-epd_v11:为 V1.1 板生成固件;--board_search_path=..:到上一级(epdiy-epub 根)去找四套板级配置目录;-j8:8 个任务并行编译以加速。产物输出到 project/build_sf32-oed-epd_v11_hcpu/

练习 2:多板差异 进阶

打开 epdiy-epub/sf32-oed-epd_v11sf32-oed-epd_v12sf32-oed-epd_v12_spi 三个目录,指出:(a) 哪个目录的 Kconfig.boardsource base 的?(b) v12_spi 顶层比 v11 多了一堆 bsp_*.c 文件,这暗示什么?

提示

cat 看三个 Kconfig.board;对比 v12_spiv11 顶层文件列表。

参考答案

(a) v11v12Kconfig.board 都只有 source "../sf32-oed-epd_base/Kconfig.board",即共用 base 板级源码;v12_spi 不 source base,自己写死了引脚配置。(b) v12_spi 顶层带 bsp_board.h / bsp_init.c / bsp_lcd_tp.c / bsp_pinmux.c / bsp_power.c / battery_table.c 一套完整板级源码,说明它是"完全自洽"的一档配置——因为走 SPI 接口的墨水屏板,引脚与电源方案和 base 那套并行屏方案差异太大,干脆独立成板。这也解释了为什么移植新屏时 README 建议"复制已有屏驱配置并改名"。

练习 3:读分区表 进阶

打开 sf32-oed-epd_v11/ptab.json,回答:(a) 主固件区 HCPU_FLASH_CODE 的偏移与大小分别是多少?(b) 样书所在的文件系统区 fs_root 排在哪些分区之后?(c) HCPU2LCPU_MB_CH1_BUF 这类"邮箱"缓冲在哪个内存域里?

提示

offset 从小到大排;找 "img": "fs_root";邮箱在 hpsys_ram0x20000000 段内。

参考答案

(a) 偏移 0x00200000、大小 0x00200000(2 MB),"img": "main""exec": "main"(支持 XIP 原地执行)。(b) 在 bootloader、dfu、KVDB_DFU_REGION、KVDB_BLE_REGION 等之后,位于 0x00E00000,大小 0x00C0000(768 KB)。(c) HCPU2LCPU_MB_CH1_BUFHCPU2LCPU_MB_CH2_BUF 都位于 hpsys_ram(主核系统 RAM,基址 0x20000000)的顶部——这是 HCPU 与 LCPU 两个核之间互发消息用的邮箱缓冲区,正是 2.2 说的"双核协作"在内存上的落点。

练习 4:双核与配置优先级 挑战

(1) 用第一性原理解释:为什么 SiFli 要搞 HCPU + LCPU 双核,而不是让单个核直接休眠省电?提示:从"唤醒条件谁在盯着"和"休眠时谁还能干点轻活"两个角度想。(2) 若你在 project/proj.conf 里设了 CONFIG_BSP_USING_UART1=y,而板级 board.confCONFIG_BSP_USING_UART1=y 已经存在,最终配置以谁为准?为什么这么设计?

提示

想想"全部休眠"的代价:必须有个东西还在数秒、还在等按键电平跳变;配置优先级从"项目"到"板子"是收窄的,越靠内越接近硬件事实。

参考答案

(1) 如果连 HCPU 一起深度休眠,就必须让外部中断(比如按键 GPIO 中断)来唤醒它——但等待外部事件期间系统几乎"全盲",定时任务、传感器轮询都没人做,而且每次唤醒都要走完整的启动流程,又慢又费电。LCPU 作为低功耗核,能用微安级的电流持续盯着按键、定时器,需要时再叫醒 HCPU——"分工"带来的直接收益是:待机功耗大幅下降,唤醒又快又确定。这正是电子书阅读器"一页停几周"的物理基础。(2) 两处都是 =y,一致,没有冲突;真正的区别在"冲突时谁赢"——规则是 proj.conf 覆盖 board.conf,覆盖 Kconfig 默认值。这么设计是因为项目配置表达"这个应用要什么能力",板级配置表达"这块板子物理上有哪些外设",能力诉求应当优先于硬件清单——但前提是硬件得真有这个外设,否则就是编译期错误而非运行时灾难。理解这条优先级,menuconfig 里改来改去就不会懵。