简介:《阿里平头哥剑池CDK快速上手指南》是一份面向物联网与嵌入式开发者的PDF手册,重点服务使用平头哥C-SKY、RISC-V架构芯片并希望快速掌握剑池CDK集成开发环境的人群。内容围绕“1天上手、5天出原型、20天出产品”的1520理念展开,帮助读者理解CDK的Packages、Editor、Debugger、Analyzer四大模块及端云一体开发方式,并系统讲解组件化SDK工程的创建、链接脚本调整、Flash算法文件实现与配置、开发板与芯片组件更新、solution工程调试以及SDK离线/在线发布流程。资源包仅含1个PDF文件,大小约3.18MB,单文件结构便于按章节查阅;目录从工具介绍、初始工程搭建到SDK发布与常见问题解答层层递进,适合作为自定义平台SDK制作时的随查手册。已有1028人学习,对需要缩短物联网产品开发周期的初中级嵌入式开发者具有参考价值。
1. 剑池 CDK 到底解决什么:把散装 SDK 拆成能复用的组件
手里拿到一块新的 RISC-V 物联网开发板,翻出来的 SDK 又是一堆散落的 .c 和 .h 文件,换个芯片型号就得重新拷一遍、改一通宏定义,这种麻烦做嵌入式的基本都碰过。剑池 CDK 走的是另一条路:把整套东西拆成 solution、SDK、chip、board、common 几类组件,工程文件里只存依赖关系,源码按组件归位,换平台时改的是组件,而不是满工程 grep 找路径。它同时支持 C-SKY 与 RISC-V 两套指令集,编辑器、调试器、分析器挂在同一个图形界面里,还能直接从芯片开放平台拉取网络组件、协议栈和外设驱动,省掉不少攒轮子的时间。下面这套流程适合两类人:刚接触这套工具、想知道工程骨架怎么搭的新手,和已经能跑通 demo、但要给团队交付一份可复用 SDK 的老手。
2. 组件化 SDK 工程怎么搭:从 New SOC Project 到链接脚本对齐
2.1 四块功能划分决定了你后面会卡在哪一步
剑池 CDK 把功能拆成 Packages、Editor、Debugger、Analyzer 四块,这个划分不是说明书里的噱头,它直接对应你做 SDK 时会卡住的位置。
Packages 是组件化模块,也是整件事的骨架。solution、SDK、chip、board 这几类组件都以 package 的形式存在,工程文件里只记依赖,源码落在新建工程时指定的 Package Path 目录下。Editor 是图形化的代码编辑入口,但它同时承担了组件配置窗口、编译选项、链接脚本图形编辑这些关键入口,很多人以为自己只用它写代码,实际上后面调 LD、配算法文件全在这里点。Debugger 负责图形化调试,Flash 算法工程那一整套调试逻辑也走它,这也是第 3 章要单独起一个工程的原因。Analyzer 偏性能与体积分析,工程定稿之后再回头看更划算。
我一般的顺序是先把 Packages 这条线走通,再碰其它三块。原因是组件路径和依赖一旦填错,编辑器里能打开、编译器也可能过,但烧写和调试阶段会冒出一堆看起来毫不相关的怪问题,排查成本极高。
还有一层值得提前知道:这套工具与芯片开放平台的组件超市是打通的,蓝牙、语音、网络类组件可以直接拉。这意味着你做的 SDK 如果按组件规范拆好,别人导入之后还能继续复用平台上的组件,而不是把一坨源码拷来拷去。端云一体这个词听起来虚,落到操作上就是“组件来源可以是本地目录,也可以是平台”。
2.2 建骨架:Solution Package 类型与 Package Path
菜单 Project -> New SOC Project 打开新建窗口,填工程名,Project Type 选 Solution Package,再指定 Package Path。这两个参数最容易被随手填错:
| 参数 | 该填什么 | 填错的后果 |
|---|---|---|
| Project Type | Solution Package | 选成其它类型,工程视图里不会自动生成 sdk 与 chip 组件,后面要手工补依赖 |
| Package Path | 组件源码要落盘的目录 | 路径带中文、空格或过长,编译阶段不一定报错,链接时找不到头文件和库 |
| 工程名 | 与 solution 业务相关的短名 | 与已有工程重名,组件目录会被覆盖 |
建完之后工程视图是三层结构,理解这三层比记住按钮位置重要得多:
- my_solution:solution 工程本体,含 main 文件,以及可配置的调试初始化脚本和 Flash 初始化脚本。
- __SDK_CK802:sdk 类型组件,依赖一个 chip 组件,源码保存在 Package Path 指定的目录。
- __Chip_CK802:chip 类型组件,包含 CPU 的初始化启动代码和链接脚本文件,同样落在 Package Path。
可以把它们理解成三层责任:solution 管应用和配置,sdk 管平台无关的封装,chip 贴着硅。后面换板子,动的主要是 chip 和 board,solution 里的业务代码基本不动,这才是组件化真正省钱的地方。很多人第一次用会把业务代码直接塞进 solution 根目录,结果换芯片时又要全量搬一遍,等于白拆。
建完骨架后先别急着写代码,做一次空跑验证:什么都不改,直接编译整个 solution。能过,说明组件依赖和路径是对的;过不了,先把依赖关系理清楚,别往里面塞业务代码,否则后面分不清是环境问题还是代码问题。
2.3 链接脚本:五个符号不能随手改
默认的 LD 文件,CDK 提供了图形化文本修改方式,存储布局在两段 Flash、两段 RAM 以内,直接改图形配置就够了。超过这个复杂度,比如 Flash 分了三段、RAM 有多个不连续区间,就得按 GCC 的语法手写 LD。
手写之前,先认清楚默认文件里这五个符号:
| 符号 | 作用 |
|---|---|
| __s_ram_data_1 | 程序数据区起始位置 |
| __e_ram_data_1 | 程序数据区结束位置 |
| __s_ram_bss_1 | BSS 区起始位置 |
| __e_ram_bss_1 | BSS 区结束位置 |
| __e_rom | 程序只读数据区之后 4 字节对齐的位置 |
这五个符号在 chip 组件的 src/ck802.c 里被直接引用,用来完成 C 运行时初始化里的数据段搬运和 BSS 清零。改 LD 时把它们删掉、改名,或者让它们指向的区间互相重叠,编译和链接都不会报错,但程序跑起来会随机崩。这类问题的血泪之处在于:崩的位置和根因隔着好几层,你会一直怀疑业务代码。
下面是一段 MEMORY + 段对齐的写法示例,地址和长度按自己板子的实际参数改:
/* 存储区间声明,ORIGIN / LENGTH 必须与板子实际可用空间一致 */ MEMORY { ROM (rx) : ORIGIN = 0x00000000, LENGTH = 512K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 96K } SECTIONS { .text : { *(.text*) *(.rodata*) . = ALIGN(4); __e_rom = .; /* 只读区之后 4 字节对齐点,ck802.c 要用到 */ } > ROM .data : { __s_ram_data_1 = .; /* 数据区起点,启动代码从这里开始搬 */ *(.data*) __e_ram_data_1 = .; /* 数据区终点,决定搬运长度 */ } > RAM AT> ROM .bss : { __s_ram_bss_1 = .; /* BSS 起点 */ *(.bss*) *(COMMON) __e_ram_bss_1 = .; /* BSS 终点,启动代码按这个长度清零 */ } > RAM }逻辑说明:.data用AT> ROM把初值放在 ROM 里,运行时由启动代码搬到 RAM;__s_ram_data_1与__e_ram_data_1之间就是搬运区间的边界,两者一错,全局变量的初值全是随机数。__e_rom强调 4 字节对齐,是因为紧随其后的可能是按字访问的表结构或跳转表,不对齐会直接触发总线异常。
参数说明:LENGTH 要比芯片手册标称的可用空间略小一点,给栈和调试期临时缓冲留余量;两段 RAM 的板子再开一个 RAM2 区域并写对应的> RAM2,但别把 BSS 拆到两段上,启动代码不会替你处理这种布局。改完 LD 一定要回到图形配置里核对一遍,图形视图和文本视图不一致时,以最终参与编译的那份为准。
2.4 建自己的平台组件:Create a new SDK 与 Create a new package
右键 my_solution 节点,选 Create a new SDK,填自己的 SDK 名,基础组件选 __SDK_CK802;然后右键工程视图里的 SDK 组件,选 Create a new package,填芯片组件名(比如 SmartL_Chip),基础组件同样选 __SDK_CK802。
这两步的本质是“复制一份默认组件当模板,再改内容”。保留模板的好处是启动文件、链接脚本、编译选项的位置都是对的;坏处是残留的默认配置很容易被忽略,比如 chip 组件里的宏开关还指着默认型号,编译能过,跑起来外设全无响应。我一般建完组件后立刻做三件事:把组件配置窗口里的宏定义逐条过一遍、把 LD 文件里的地址区间换成板子的实际值、把启动文件里的时钟初始化改掉。三件事做完再编译,能省掉后面大量的“明明编译过了却不工作”。
组件创建完成后,工程视图里的节点是可以直接拖动和重命名之外的拖拽复用的,但依赖关系不要随便改。sdk 依赖 chip,solution 依赖 sdk,这个方向反了,CDK 在链接阶段会给出很难读的错误。
3. Flash 算法文件怎么写:FlashDev.c 描述器件、Driver.c 实现接口、main.c 做调试闭环
3.1 为什么烧写要单独起一个 Flash 工程
CDK 支持 Flash 烧写,但不同厂家的 Flash 器件编程时序、命令字、扇区划分都不一样,工具不可能内置所有型号。它的做法是:允许开发者提供一个 Flash 算法文件(最终产物是 .elf),烧写时 CDK 把这个算法下载到芯片 RAM 里跑起来,再通过全局变量给算法下命令,由算法自己去操作 Flash 控制器。
这个机制决定了两件事:一是算法工程和业务 SDK 是两个独立工程,CPU 型号必须完全一致,否则下载进去跑飞;二是算法工程里不能依赖业务 SDK 的任何代码,它得足够小、足够独立,能在 RAM 里跑完。
新建入口同样是 New SOC Project,Project Type 选 Flash,RAM1 里填板子的 RAM 区域,CPU 与目标 SDK 的 CPU 必须一模一样。建完之后工程里有三个文件承担不同职责:FlashDev.c 描述器件、Driver.c 实现接口、main.c 做主控逻辑。
3.2 FlashDev.c:把器件参数填对
FlashDev.c 里是一个固定结构的描述体,字段顺序不能调,CDK 按这个结构解析:
/* 器件基本信息,字段顺序与含义固定,不要自行增删 */ struct FlashDevice const FlashDevicesINDEVSECTION = { 6, // 保留的版本描述,不要改 "SmartL_Flash", // 算法名,工程配置里显示的就是它 "ck802", // CPU 名,全小写,必须与 SDK 的 CPU 一致 0x123456, // Flash ID "NorFlash", // 器件类型 512 * 1024, // 保留字段,按默认填 1, // 是否可直接访问,1 表示可在 Flash 上直接取指 1, // RangeNumbers,下面地址表的条目数 { {0x0, 0x80000, 0x200} // {起始地址, 容量, 扇区大小} } };参数说明:第三个字段 CPU 名必须全小写,写成 CK802 或 C-SKY 都会导致算法加载失败;最后一个结构里的扇区大小 0x200 必须和 Driver.c 中 flashErase 的实现对齐,两处不一致的典型现象是“擦除返回成功但数据没变”。如果板子上挂了两片 Flash,把 RangeNumbers 改成 2 并补一行地址范围,擦除接口会按传入地址自动落到对应区域,不需要在驱动里再写分支。
3.3 Driver.c:八个接口的语义与返回值
Driver.c 是真正干活的地方,要按硬件实现下面这些接口:
| 接口 | 语义 | 返回 |
|---|---|---|
| flashInit | 编程开始前的初始化,运行 Flash 程序时最先执行 | 0 成功,非 0 失败 |
| flashUnInit | 编程结束后的收尾,CDK 在操作完成后默认调用 | 0 成功 |
| flashID | 读取器件 ID,通过参数带回 | 0 成功 |
| flashProgram | 向目标地址写数据 | 0 成功 |
| flashRead | 读取数据,非直读型 Flash 才会被调用 | 0 成功 |
| flashErase | 擦除指定空间,地址按 PageSize 对齐、长度为 PageSize 的整数倍 | 0 成功 |
| flashChipErase | 整片擦除 | 0 成功 |
| flashChecksum | 烧写镜像的快速校验,与镜像里的 checksum 比对 | 0 成功 |
实现时有几个容易写错的地方值得提前说清楚。flashErase 的地址参数一定是对齐过的,但很多人会在里面再做一次“向下对齐”,结果擦掉了不该擦的区域;正确做法是直接信任 CDK 传进来的地址,只在内部做范围校验。flashProgram 不要假设一次写入的长度是页大小的整数倍,末尾不满一页的部分要么用缓冲补齐,要么拆成单字节写,具体取决于器件手册。flashChecksum 如果不实现,返回 0 也能跑通,但校验环节就形同虚设,量产阶段出问题会很难定位。
还有一个细节:这些接口的返回值不是给业务代码看的,是给 CDK 判断流程用的。返回非 0 时 CDK 会中止烧写并报错,所以哪怕只是参数越界,也老老实实返回错误码,别为了“让它跑完”强行返回 0。
3.4 调试闭环:-O0 编译、Watch 改变量、把 PC 拉到 lr
算法工程写完不能直接拿去烧,得先在 RAM 里把它调通。整套调试手法有个固定套路,第一次用会觉得别扭,用两次就顺了。
第一步,右击算法工程打开 Compiler 配置,把 Optimization 改成 -O0,重新编译。这一步是为了让单步调试和变量观察不被优化打乱,-O2 下很多局部变量会被优化掉,Watch 窗口里看不到值。
第二步,双击工程视图里的算法工程节点,设为 active project,点调试按钮启动。程序跑到 main 函数后全速运行,会停在__bkpt_label()这一行。这个函数就是软断点,CDK 把算法下载到 RAM 之后停在这里等你下发命令。
第三步,在 Watch 窗口里设置全局变量,验证某个接口。以 flashProgram 为例:把 g_func 设为 1,g_dstAddress 设为要编程的 Flash 地址,g_length 设为数据长度,g_rwBuffer 里填上特征数据。调试时不要拿几 KB 去试,十几个字节足够验证时序和地址映射,数据量大了反而容易掩盖边界问题。
第四步,把 PC 的值设成当前 lr 的值,然后单步运行。这一步是从断点函数返回到 main 的命令分发处,PC 不拉回来,你会一直卡在__bkpt_label里出不去,这是第一次用最容易卡住的地方。
第五步,单步进入 flashProgram,跑完整个函数,再回到 Watch 窗口核对目标地址的数据。按同样的方式依次验证 flashID、flashRead、flashErase、flashChipErase、flashChecksum,每个接口单独过一遍。全部通过之后,把 Compiler 选项从 -O0 改回 -O2 重新编译,产物才是能进 SDK 的版本。
3.5 算法文件挂进 SDK:两种路径
调通之后,把算法工程根目录下 Obj/ 里生成的 SmartL_Flash.elf 拷到 SDK 工程这边,挂载方式分两种情况:
- 情况 1:SDK 使用了芯片组件(正常路径)。把 .elf 拷到 SmartL_Chip 组件目录,右键组件节点选 Open Containing Folder 可以直接打开目录,然后在组件配置窗口里把 SmartL_Flash.elf 设为算法文件。
- 情况 2:没有使用芯片组件,或者用的是旧版本组件。把 .elf 拷到 my_solution 工程根路径下,与 .ckdproj 文件同目录,然后在工程配置的 Flash 标签页里选择这个算法文件。
两种路径的区别在于配置生效的范围:前者跟着组件走,组件被别人复用时算法一起被带走;后者只对当前工程生效。给团队交付 SDK 一定要走情况 1,否则别人导入你的工程后,烧写配置是空的,还得再来问你一遍。
4. 把初始工程改造成平台 SDK:开发板组件、芯片组件与 common 组件的收口
4.1 开发板组件:用 Add Source Folder 导入已有代码
骨架建好之后,my_solution 里还是默认的模板内容,接下来要把它换成 SmartL 平台的真实代码。第一步是在 SDK 组件节点下新增一个开发板组件 SmartL_Board,然后右键该节点选 Add Source Folder,选中已有的代码目录,确认导入。
这一步导入的是目录引用关系,不是文件拷贝,所以原目录后续被删掉或移动,工程立刻红叉。我一般会先把要导入的目录整理成一个固定的板级目录结构,再执行导入,避免导到一半发现目录里混着别的平台的代码。
导入之后必须配编译选项:在 Compiler 的 Global Config 里配置开发板组件要暴露给外部的头文件搜索路径,然后 Debug Level 也要选带调试信息的选项。头文件路径这一项漏配的典型现象是“组件自己能编译,依赖它的 solution 报找不到 xxx.h”。Debug Level 不配,调试时变量看不到值、断点位置漂移,排查起来很浪费时间。
4.2 芯片组件更新:启动代码与链接脚本要一起改
芯片组件是贴着硅的那层,换平台时改动最集中。需要同步的有三块内容:
| 内容 | 改什么 | 漏改的后果 |
|---|---|---|
| 启动代码 | 时钟、看门狗、中断向量表、栈顶地址 | 上电就跑飞或反复复位 |
| 链接脚本 | MEMORY 区间与 2.3 节提到的那五个符号 | 全局变量初值丢失、BSS 清零越界 |
| 编译宏 | 芯片型号、外设开关、Flash 容量 | 编译能过,运行时外设无响应或越界访问 |
这三块里最容易被单独改掉的是链接脚本——因为启动代码改完能跑,大家就以为完事了,结果换个优化等级或者加个全局数组就崩。我的习惯是把启动文件和 LD 文件放在同一个组件里、同一次提交里改,并且在组件说明里写清楚两者的对应关系,后面接手的人不至于只改一半。
4.3 common 组件:把硬件无关的那部分单独拎出来
平台代码里总有一批与硬件无关的东西:环形缓冲区、协议解析、状态机、日志格式化。这些东西放在开发板组件里不会报错,但会让组件职责变糊,别人复用时得连板子一起搬。
正确做法是新增两个硬件无关的 common 组件,把这类代码挪进去,然后让 solution 同时依赖 sdk、board 和 common。拆分时注意依赖方向:common 不能反向依赖 board,否则组件图里会出现环,CDK 解析依赖时直接失败。
拆完之后的好处很直接:换一块新板子,common 原样复用,你只需要重新做 chip 和 board,工作量从“重写一遍”降到“改配置”。
4.4 solution 收口:调试配置与 Flash 配置一起验
前面几步做完,solution 工程要做最后的收口。调试初始化脚本要指向新板子的 RAM 初始化流程,Flash 初始化脚本要和第 3 章做的算法文件对应上,然后在工程配置里检查编译产物路径、算法文件路径、调试器型号这三项。
验证顺序建议固定下来:先编译,再下载到 RAM 调试,最后烧到 Flash 冷启动。跳过中间那步直接烧 Flash,一旦启动代码有问题,你会分不清是算法的问题还是代码的问题。冷启动通过之后,把 solution 完整编译一遍并记录产物大小,这个数字是后面发布 SDK 时的基线,涨得离谱就说明有组件被重复链接了。
5. 避坑排查:链接脚本、算法文件、组件路径上的五个翻车点
5.1 编译链接都过,一上电就随机崩
现象:工程编译零告警,下载到 RAM 调试也正常,烧到 Flash 冷启动后跑几秒就崩,崩的位置每次都不一样。
原因:链接脚本里的__e_rom、__s_ram_bss_1这类符号被改过名字或者指向的区间重叠,启动代码按错误的长度搬运数据段、按错误的边界清 BSS,把不该清的 RAM 清了。这类错误编译期发现不了,因为符号本身是合法的。
解决:把默认 LD 文件里的五个保留符号原样保留,只改它们所在的地址区间,不改名字、不改相对顺序。改完用调试器在启动代码前后各打一个断点,核对 RAM 里的数据段内容与 ROM 中的初值是否一致,不一致就说明搬运长度算错了。
5.2 擦除返回成功,但写进去的数据读出来是旧值
现象:flashErase 返回 0,flashProgram 也返回 0,但 flashRead 读回来的还是上一次的数据。
原因:FlashDev.c 里描述的扇区大小与 Driver.c 中 flashErase 实际操作的扇区大小不一致。CDK 按描述体里的 PageSize 对齐地址传给你,驱动内部又按自己的页大小二次对齐,两次对齐叠加,实际擦掉的是另一块区域。
解决:把 FlashDev.c 最后一个结构里的扇区大小与驱动里的页大小宏统一成同一个值,最好只在一处定义、另一处引用。改完用 3.4 节的方法单独验证一次 flashErase:擦之前先读一遍,擦之后再读一遍,两次数值必须不同。
5.3 换台电脑打开工程,组件节点全是红叉
现象:在自己机器上好好的工程,拷给同事或者换台机器打开,组件节点全部红叉,编译报找不到源文件。
原因:Package Path 用的是绝对路径,或者组件导入时用的是目录引用而不是相对路径。换机器后路径不存在,组件自然找不到。
解决:新建工程时 Package Path 尽量选在工程目录内或者用相对路径,导入已有代码目录时确认是相对引用。已经出问题的工程可以在组件配置窗口里重新指定路径,重新指定后记得清理一次编译产物再全量编译,否则旧的中间文件会继续生效。
5.4 算法工程 -O0 调通了,改 -O2 之后烧写失败
现象:-O0 下每个接口都验证通过,改成 -O2 重新编译、挂进 SDK 后烧写报错,或者烧写成功但校验失败。
原因:-O2 下编译器会重排或消除部分全局变量访问,如果 main 里的全局变量没有加 volatile 或者被优化进了寄存器,CDK 在外部修改这些变量后算法读不到新值。
解决:把 g_func、g_dstAddress、g_length、g_checksum、g_rwBuffer 这几个被外部修改的全局变量加上 volatile 修饰,并把命令分发逻辑放在一个不会被优化掉的循环里。改完用 -O2 重新跑一遍 3.4 节的调试流程,确认每个接口在优化后依然能正确响应。
5.5 发布出去的 SDK,别人导入后头文件找不到
现象:自己工程里一切正常,把 SDK 发布给同事,对方导入后编译报找不到头文件,或者链接时报未定义符号。
原因:组件的 Global Config 里配的头文件搜索路径是本地绝对路径,或者发布时漏掉了某些组件目录,依赖关系在对方机器上解析不到。
解决:发布前把所有组件的头文件路径改成相对组件目录的写法,导出一份完整组件清单,逐条核对依赖。接收方导入后先做一次空编译,编译不过就是路径问题,别急着往下调业务。这个检查我一般会做成一个固定动作,每次发布都走一遍。
6. 进阶:SDK 的离线与在线发布,以及交付前的验收清单
SDK 做完只是自己能用,要给别人用还得走发布。CDK 支持两种方式:离线发布是把整个 solution 连同依赖组件打包成一个可分发的工程包,对方解压后直接导入;在线发布是把组件推到芯片开放平台上,别人通过组件超市拉取。两种方式的选择标准很简单:只给内部几个人用、或者网络环境受限,走离线;要给不特定开发者复用、希望跟着平台版本走,走在线。
在线发布前,组件的元信息要补齐:组件名、版本号、依赖列表、支持的芯片型号、暴露的头文件路径。这几项漏一个,别人拉下来就得手工改配置。版本号建议按语义化规则来,接口不兼容的改动必须升主版本,否则老工程拉新组件会编译不过。
交付前我会固定走一遍下面这张表,任何一项没过就不发:
| 检查项 | 通过标准 |
|---|---|
| 空工程编译 | 新建 solution 只依赖发布的组件,能零告警编译通过 |
| RAM 调试 | 下载到 RAM 能跑,断点命中正常,变量可观察 |
| Flash 冷启动 | 断电重启后能正常启动,不依赖调试器 |
| 算法文件 | 烧写、读取、擦除、校验四项全部通过 |
| 组件路径 | 全部为相对路径,换机器打开无红叉 |
| 代码体积 | 与上一版本对比无明显异常增长 |
真正让我养成这套习惯的是一次很尴尬的交付:SDK 在自己机器上验证得挺顺,打包发出去之后对方导入编译不过,查了半天是某个组件的头文件路径写成了本地绝对路径。从那以后我每次发布前都强制走一遍上面的清单,尤其是“换台机器空工程编译”这一项,它能把路径类问题一次性暴露干净。希望帮到你。
本文还有配套的精品资源,点击获取