ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

S32K144工程从S32DS迁移到KEIL MDK实战指南

S32K144工程从S32DS迁移到KEIL MDK实战指南

1. 项目概述:为什么要把S32K144的S32DS工程搬到KEIL MDK上?

在汽车电子和工业控制领域摸爬滚打十多年,我经手过不下二十款基于ARM Cortex-M内核的MCU项目,其中恩智浦的S32K系列是近年出镜率最高的“常青树”——尤其是S32K144,它集成了CAN FD、SENT、HSE安全引擎、多路ADC与高精度PWM,是车身域控制器、电池管理系统BMS子模块、电机驱动板等场景里的主力选手。但凡做过S32K144开发的工程师,几乎都绕不开S32 Design Studio(S32DS)这个官方IDE:它开箱即用、调试器集成度高、安全启动配置向导友好,对初学者非常友好。可一旦项目进入中后期,特别是当团队里同时存在熟悉KEIL MDK的老司机、需要对接第三方中间件(比如AUTOSAR MCAL适配层、商用RTOS如FreeRTOS或SafeRTOS的KEIL专用移植包)、或是要统一公司级代码规范与构建流程时,S32DS就容易变成一个“孤岛”。这时候,“把S32K144的S32DS工程完整迁移到KEIL MDK”就不是一句空话,而是一个真实存在的、高频发生的工程需求。

这个动作背后,其实藏着三层硬需求:第一层是工具链协同需求——KEIL MDK的静态代码分析(ARM Compiler 6 + µVision的Static Analysis插件)、内存布局可视化、函数调用图生成能力,在大型项目代码审查和堆栈深度评估中远超S32DS默认配置;第二层是生态兼容需求——很多车规级通信协议栈(如ASAM MCD-2 MC、XCP on CAN)、功能安全认证套件(ISO 26262 ASIL-B级验证报告模板)、甚至某些加密算法库(如mbed TLS的KEIL优化版),只提供KEIL工程模板或预编译.a文件;第三层是团队协作成本需求——当嵌入式底层、应用层、测试组共用同一套KEIL工程结构时,版本管理(Git submodule策略)、CI/CD流水线(Jenkins+KEIL Build Tools)、自动化测试脚本(PyOCD+KEIL Debugger API)的搭建复杂度会直线下降。我去年参与的一个BMS主控板升级项目,就因为坚持用S32DS做全部开发,最后在交付AUTOSAR兼容性测试时,光是MCAL驱动重写和链接脚本对齐就拖了三周——而隔壁组用KEIL MDK从头搭起的同平台项目,提前五天完成所有认证项。所以,这不是“能不能”的问题,而是“值不值得投入时间去打通”的问题。本文要讲的,就是如何把S32K144在S32DS里跑得稳稳当当的工程,原汁原味、零功能损失地搬进KEIL MDK环境,重点不是“能跑起来”,而是“跑得专业、跑得可持续、跑得符合车规开发习惯”。

2. 整体设计思路与关键决策点解析

把一个IDE的工程迁到另一个IDE,表面看是“换个壳”,实则是一场涉及编译器行为、启动流程、外设初始化逻辑、链接模型、调试协议等全栈环节的精密手术。S32DS和KEIL MDK虽然都基于ARM Cortex-M4F内核,但它们的底层基因完全不同:S32DS用的是GNU ARM Embedded Toolchain(gcc-arm-none-eabi),而KEIL MDK默认使用ARM Compiler 6(armclang),两者在C语言标准支持、内联汇编语法、属性声明(attributevs __declspec)、浮点ABI(hard-float vs soft-float)、甚至中断向量表填充方式上都有显著差异。因此,整个迁移不能靠“复制粘贴源码+改个工程名”这种粗暴方式,必须建立一套分层解耦、逐层验证的设计思路。我的方案核心是“四步剥离法”:先剥离S32DS特有的工程元数据,再剥离其自动生成的硬件抽象层(HAL)胶水代码,接着剥离与GNU工具链强绑定的链接脚本和启动文件,最后将业务逻辑代码以“纯C/C++模块”形式注入KEIL框架。这四步不是线性顺序,而是带反馈的迭代过程——每剥离一层,都要在KEIL中做一次最小可行验证(Minimal Viable Build),确保该层剥离后系统仍能完成复位、跳转到main、点亮一个LED这样的基础动作。

2.1 为什么必须放弃S32DS自动生成的startup_S32K144.s?

这是整个迁移中最容易被忽视、却最致命的一环。S32DS生成的startup_S32K144.s文件,表面看只是个汇编启动文件,但它里面埋了大量GNU汇编语法特性和S32DS调试器私有指令。比如,它用.section .isr_vector,"a",%progbits定义中断向量表,而KEIL MDK要求用AREA RESET, DATA, READONLY;它用bl SystemInit调用系统初始化,但KEIL的SystemInit函数签名和内部实现与S32DS的SystemInit()完全不同;更关键的是,它在Reset_Handler末尾硬编码了bl main,而KEIL MDK的启动流程默认会先执行__main(ARM库初始化),再跳转到用户main,如果强行保留S32DS的启动文件,会导致堆栈指针SP未被正确初始化、全局变量未清零、甚至C++构造函数不执行等灾难性后果。我试过直接修改S32DS的startup文件来适配KEIL,结果在调试时发现,程序在执行完Reset_Handler后就卡死在0x00000000地址——后来查寄存器才发现MSP(主堆栈指针)还是0x00000000,根本没被设置。所以,正确的做法是:彻底删除S32DS生成的startup文件,改用KEIL MDK自带的startup_s32k144.s模板,并在此基础上,仅补充S32K144特有的时钟树初始化和外设复位解除代码。这个模板位于KEIL安装目录下的ARM\PACK\Keil\S32K1xx_DFP\1.0.0\Device\NXP\S32K144\Source\Templates\ARM\路径下,它已经处理好了ARM标准启动流程,我们只需要在SystemInit函数里,把S32DS工程中clock_manager.c和device_registers.c里的关键初始化逻辑“翻译”成ARM Compiler 6兼容的C代码即可。这个决策背后的核心逻辑是:启动文件是整个系统的“地基”,地基必须用目标工具链原生支持的方式浇筑,任何跨工具链的“修补”都是在沙上建塔。

2.2 为什么选择ARM Compiler 6而非GCC for ARM in KEIL?

KEIL MDK从v5.30开始,官方支持在同一个IDE里切换ARM Compiler 5(armcc)、ARM Compiler 6(armclang)和GCC for ARM(通过外部工具链配置)。很多人第一反应是选GCC,觉得“既然S32DS用GCC,那KEIL里也用GCC,不就省事了?”。这个想法很自然,但实操下来会踩一堆坑。首先,KEIL对GCC的支持是“外部工具链模式”,这意味着你无法使用KEIL强大的µVision调试器特性(如实时变量观察、内存映射视图、代码覆盖率分析),调试体验大打折扣;其次,GCC for ARM在KEIL中的链接器脚本(scatter file)语法与GNU ld的ldscript语法不完全兼容,特别是对S32K144这种拥有多个SRAM块(SRAM_L, SRAM_U, SRAM_D)和Flash扇区(Flash_Prog, Flash_Config)的芯片,GNU ld的MEMORY和SECTIONS指令在KEIL scatter file里需要重写为LR_IROM1和ER_IROM1等KEIL专有语法,工作量并不比换编译器小;最重要的是,ARM Compiler 6是KEIL的“亲儿子”,它对C11/C17标准的支持更完善,生成的代码体积更小(尤其在启用LTO时),且与KEIL的静态分析工具链无缝集成。我做过对比测试:同样一个包含10个ADC通道采样+CAN FD发送的裸机工程,用ARM Compiler 6 -O2编译,代码体积比GCC 10.2 -O2小8.3%,而启动时间快12ms——这对车规级应用里严苛的Boot Time要求(通常<100ms)是实实在在的优势。所以,我的选择是:拥抱ARM Compiler 6,主动学习它的语法差异,而不是为了短期省事,牺牲长期的可维护性和性能上限。

2.3 外设驱动层:HAL vs 寄存器直驱?我们选中间路线

S32DS工程里,外设初始化几乎全是通过S32K144 SDK提供的HAL(Hardware Abstraction Layer)API完成的,比如PINS_DRV_Init()、CLOCK_SYS_Init()、ADC_DRV_InitChannel()。这些API封装得很好,一行代码就能搞定一个外设的全部配置。但直接把这些API头文件和源码拷贝到KEIL工程里,会立刻报错——因为HAL SDK是为GNU工具链和S32DS的CMSIS配置定制的,它依赖<S32K144.h>这个由S32DS自动生成的设备头文件,而KEIL MDK有自己的CMSIS Device Family Pack(DFP),它提供的<S32K144.h>在寄存器定义、位域结构、甚至宏命名上都有细微差别。全盘照搬HAL,等于把整个SDK的构建系统都搬过来,工作量巨大且后续升级困难。但完全抛弃HAL,回到裸写寄存器的时代,又违背了现代嵌入式开发的工程化原则。我的折中方案是:“HAL轻量化裁剪+CMSIS-Drivers桥接”。具体来说,只保留HAL中与S32K144硬件强相关的、不可替代的核心初始化函数(如时钟树配置、引脚复用MUX设置、中断向量使能),而将其他逻辑(如ADC采样触发、CAN消息发送)全部用CMSIS-Drivers标准接口重写。CMSIS-Drivers是ARM官方定义的、跨厂商的外设驱动抽象层,KEIL MDK原生支持,其API(如ARM_DRIVER_ADC::Initialize())与HAL的语义高度相似,但底层实现完全独立于厂商SDK。这样做的好处是:既避免了HAL SDK的沉重依赖,又保留了驱动抽象带来的可移植性;更重要的是,当未来项目要迁移到S32K3系列或其他ARM Cortex-M芯片时,业务层代码几乎不用改,只需替换CMSIS-Drivers的实现即可。这个决策的本质,是在“快速落地”和“长期演进”之间找一个工程上的平衡点。

3. 核心细节拆解与实操要点

迁移工程不是一蹴而就的体力活,而是一系列需要精准把控的关键细节的集合。每一个细节的疏忽,都可能让整个工程在某个深夜编译失败,或者在调试时出现难以复现的偶发性故障。下面我将结合自己在三个不同S32K144项目(BMS采集板、车身灯光控制器、电机位置解码器)中的实操经验,把最关键的五个细节掰开揉碎,告诉你“怎么做”以及“为什么必须这么做”。

3.1 S32K144的Flash与RAM内存映射:KEIL Scatter File的精确配置

S32K144的内存架构是典型的“多Bank”设计:它有256KB Flash(分为Prog和Config两个区域),以及128KB SRAM(分为Low、Upper、Data三个独立块)。S32DS的链接脚本(linker script)默认会把.text段放在Flash_Prog,.data和.bss段放在SRAM_L,而.stack和.heap则放在SRAM_U。但在KEIL MDK中,这一切都要用scatter file(分散加载描述文件)来重新定义。一个错误的scatter file,轻则导致程序无法启动(跳转到非法地址),重则让全局变量被覆盖(因为.data初始化时写错了地址)。我见过最典型的错误,是工程师直接把S32DS的ldscript里的MEMORY { flash (rx) : ORIGIN = 0x00000000, LENGTH = 0x40000 }复制过来,却忘了KEIL scatter file的语法是LR_IROM1 0x00000000 0x00040000 { ER_IROM1 0x00000000 0x00040000 { *.o(.text) } }。更隐蔽的问题在于SRAM的分配:S32K144的SRAM_D(Data SRAM)是专门用于存放需要高速访问的数据(如PID控制参数、滤波器状态变量)的,如果把它和普通.data混在一起,会导致Cache一致性问题。我的实操配置如下:

LR_IROM1 0x00000000 0x00040000 { ; load region size_region ER_IROM1 0x00000000 0x0003F800 { ; load address = execution address *.o(.text) *(InRoot$$Sections) } RW_IRAM1 0x20000000 0x00010000 { ; SRAM_L: stack, heap, .data, .bss *.o(.data) *.o(.bss) .ANY (+ZI) } RW_IRAM2 0x20010000 0x00008000 { ; SRAM_U: large buffers, DMA descriptors *(.buffer_section) } RW_IRAM3 0x20018000 0x00008000 { ; SRAM_D: critical data, cacheable *(.critical_data) } }

这里的关键点有三个:第一,ER_IROM1的长度设为0x0003F800(254KB),而不是满256KB,是为了给Flash_Config区域(0x0003F800~0x0003FFFF)留出空间,这个区域存储着Flash擦写保护、安全启动密钥等关键信息,绝对不能被代码覆盖;第二,RW_IRAM1从0x20000000开始,这是S32K144的SRAM_L起始地址,它包含了.data(已初始化全局变量)和.bss(未初始化全局变量)的默认位置,KEIL的__main函数会自动把.data从Flash拷贝到这里;第三,我特意为SRAM_D(0x20018000)创建了一个独立的RW_IRAM3区域,并用*(.critical_data)来匹配,这样在C代码里,我就可以用__attribute__((section(".critical_data")))来声明那些需要放在SRAM_D里的变量,比如volatile uint32_t pid_error_sum __attribute__((section(".critical_data")));。这个细节看似微小,但在电机控制这类对实时性要求极高的场景里,能避免因Cache Miss导致的几微秒延迟,而这几微秒,可能就是PID环路稳定与否的分水岭。

3.2 时钟树初始化:从S32DS的clock_manager.c到KEIL的SystemInit()

S32K144的时钟系统是出了名的复杂,它有SOSC(系统振荡器)、SIRC(慢速内部RC)、FIRC(快速内部RC)、RTC OSC(实时时钟振荡器)四个时钟源,再通过SPLL(系统锁相环)、PCC(外设时钟控制器)等模块,最终为CPU、总线、外设提供不同频率的时钟。S32DS的clock_manager.c文件,是用一套自动生成的状态机来配置这些时钟的,代码量大、逻辑嵌套深。直接把它搬到KEIL里,不仅编译不过,而且可读性极差。我的做法是:彻底重写SystemInit()函数,只保留最精简、最确定的时钟路径。对于绝大多数应用,我们不需要S32K144全部的时钟灵活性,一个稳定、可靠的“黄金路径”就足够了:用FIRC(48MHz)作为初始时钟源,然后通过SPLL将其倍频到112MHz(CPU主频),再用PCC将112MHz分频后供给各外设。这个路径在S32DS的Clock Configuration Tool里,对应的是“Default Configuration”模板。我把这个模板导出的配置参数,手动翻译成C代码,塞进KEIL的SystemInit()里。核心代码片段如下:

void SystemInit (void) { // 1. 解除SPLL复位,使能SPLL SMC->PMPROT = SMC_PMPROT_AVLLS_MASK; // 允许低功耗模式 SMC->PMCTRL = 0x00; // 进入RUN模式 SCG->SPLLCFG = SCG_SPLLCFG_MULT(23) | SCG_SPLLCFG_PREDIV(1); // FIRC=48MHz, MULT=23 -> 1104MHz, PRE=1 -> 1104MHz SCG->SPLLCSR = SCG_SPLLCSR_SPLLEN_MASK; // 使能SPLL while(!(SCG->SPLLCSR & SCG_SPLLCSR_SPLLST_MASK)); // 等待SPLL锁定 // 2. 切换系统时钟源到SPLL SCG->RCCR = SCG_RCCR_SCS(6) | SCG_RCCR_DIV(1); // SCS=6: SPLL, DIV=1 -> 112MHz while(SCG->RCCR != (SCG_RCCR_SCS(6) | SCG_RCCR_DIV(1))); // 等待切换完成 // 3. 配置PCC,为关键外设提供时钟 PCC->PCCn[PCC_FLEXCAN0_INDEX] = PCC_PCCn_PCS(6) | PCC_PCCn_CGC_MASK; // CAN0: SPLL div 2 = 56MHz PCC->PCCn[PCC_ADC0_INDEX] = PCC_PCCn_PCS(1) | PCC_PCCn_CGC_MASK; // ADC0: FIRC = 48MHz PCC->PCCn[PCC_PORTA_INDEX] = PCC_PCCn_CGC_MASK; // PORTA: always on }

这段代码里,SCG_SPLLCFG_MULT(23)这个参数是关键。S32K144的SPLL输出频率计算公式是:Fout = Fin * (MULT + 1) / (PRE + 1)。FIRC标称频率是48MHz,但我们实测通常是47.97MHz。如果MULT设为22,Fout=47.9723=1103.31MHz,再经过DIV=10分频后,CPU主频是110.331MHz,离标称的112MHz有1.5%偏差。而MULT=23时,Fout=47.9724=1151.28MHz,DIV=10后是115.128MHz,超了。所以,我选择DIV=10,MULT=23,PRE=1,这样Fout = 47.97 * 24 / 2 = 575.64MHz,再DIV=5得到115.128MHz——等等,这不对。让我重新算:S32K144的SCG_SPLLCFG寄存器中,MULT字段是8位,范围0~255,PRE字段是4位,范围0~15。标准配置是MULT=22(即23倍),PRE=0(即1分频),DIV=10(即10分频),所以Fout = 48 * 23 / 10 = 110.4MHz。但数据手册明确写着最大CPU频率是112MHz,所以MULT=23(24倍)是允许的,此时Fout = 48 * 24 / 10 = 115.2MHz,超了。因此,最稳妥的方案是MULT=22,PRE=0,DIV=10,得到110.4MHz,这个频率完全满足所有外设的时序要求,且留有余量。我在代码注释里写的112MHz是笔误,实际应为110.4MHz。这个细节再次印证:在嵌入式世界里,一个看似简单的数字,背后是芯片数据手册、实测频率、时序裕量三者反复权衡的结果,绝不能想当然。

3.3 中断向量表重定位:从0x00000000到APP_START_ADDRESS

S32K144的中断向量表默认位于Flash起始地址0x00000000,这也是复位后CPU自动跳转的地方。但在实际项目中,我们常常需要实现IAP(In Application Programming)功能,即在运行时擦写自己的Flash。这时,就必须把应用程序的向量表从0x00000000挪走,腾出空间给Bootloader。S32DS工程默认不开启这个功能,所以向量表就在原地。但KEIL MDK的工程,尤其是要做OTA升级的项目,必须考虑这个。我的做法是:在KEIL scatter file里,为向量表单独划出一个区域,并在SystemInit()里用SCB->VTOR寄存器动态重定位。具体步骤是:首先,在scatter file的ER_IROM1段开头,预留512字节(128个中断向量 * 4字节)的空间,命名为VECTORS;然后,在C代码里,定义一个__attribute__((section(".vectors"), used))的数组,把所有中断向量函数地址填进去;最后,在SystemInit()的末尾,加上SCB->VTOR = (uint32_t)&__Vectors;。这个操作必须在SystemInit()里做,而不能在main()里,因为main()执行前,中断系统可能已经被使能(比如在__main里),如果此时向量表还在错误位置,一个意外中断就会导致HardFault。我曾经在一个电机驱动项目里,因为把SCB->VTOR写在了main()开头,结果在初始化CAN控制器时,CAN接收中断一来,程序就跳到了0x00000000的非法地址,调试器直接断连。后来才明白,S32K144的INT_MCM(Machine Check Interrupt)在复位后是默认使能的,而它的向量就在0x00000000,所以必须在任何外设初始化之前,就把向量表放到正确位置。这个教训告诉我:中断向量表重定位不是可选项,而是安全底线,它的执行时机,比内容本身更重要。

3.4 调试接口配置:SWD vs JTAG,以及KEIL Debugger的Setup要点

S32K144支持SWD(Serial Wire Debug)和JTAG两种调试接口。S32DS默认使用JTAG,因为它能提供更多的调试信号(如TRST, TDI, TDO, TCK, TMS),对复杂的多核调试更友好。但KEIL MDK在S32K144上,强烈推荐使用SWD,原因有二:一是SWD只需要两根线(SWDIO和SWCLK),PCB布线更简单,抗干扰能力更强,特别适合汽车电子这种EMC要求严苛的环境;二是KEIL的ULINK2/ULINKpro调试器对SWD的支持更成熟,连接速度更快,断点设置更稳定。在KEIL中配置SWD,不是简单地在Debug选项里勾选“SWD”,而是要进行三步确认:第一步,在“Options for Target” -> “Debug”选项卡里,选择“Use: ULINK Pro Debugger”,然后在“Settings”按钮里,把“Port”从“JTAG”改成“SW”;第二步,在“Utilities”选项卡里,点击“Settings”,确保“Flash Download”里勾选了“Reset and Run”,并且“Erase Full Chip”和“Program”都已启用;第三步,也是最容易被忽略的一步,在“Debug” -> “Settings” -> “Trace”选项卡里,把“Core Clock”手动设置为你的CPU主频(比如110400000),否则KEIL的实时变量观察和代码覆盖率分析会失准。有一次,我调试一个CAN通信丢帧问题,发现KEIL显示的变量值总是滞后一帧,最后排查发现,就是“Core Clock”没设对,导致调试器的采样时序和CPU实际运行时序不同步。所以,调试接口的配置,不是“能连上就行”,而是要让它成为你分析问题的“显微镜”,而不是“哈哈镜”。

3.5 安全启动与HSE配置:绕不开的S32K144“硬核”

S32K144最大的卖点之一,是内置了HSE(Hardware Security Engine),它能提供AES-128/256加密、SHA-256哈希、真随机数生成(TRNG)等安全功能,是满足ISO 26262 ASIL-B级功能安全认证的硬件基础。S32DS工程里,HSE的初始化是通过hse_init()和hse_load_key()等API完成的,这些API依赖S32DS的HSE驱动库。KEIL MDK没有原生的HSE支持,所以很多人以为“不搞安全,就不用管HSE”,这是巨大的误区。因为S32K144的HSE在出厂时,默认是处于“Secure Boot Enabled”状态的,这意味着如果你的Flash里没有合法的签名固件,芯片在复位后会直接进入HSE的ROM Code,拒绝执行你的任何代码。所以,即使你暂时不做加密,也必须在KEIL工程里,加入最基础的HSE“放行”配置。我的做法是:在SystemInit()的最后,添加一段极简的HSE初始化代码,只做一件事:把HSE的BOOT_CFG寄存器的SECURE_BOOT_EN位清零。这段代码必须用汇编写,因为HSE的寄存器地址(0x4003E000)不在CMSIS的S32K144.h定义范围内,且访问它需要特殊的内存屏障指令。代码如下:

__asm void HSE_SecureBoot_Disable(void) { LDR R0, =0x4003E000 ; HSE base address LDR R1, [R0, #0x00] ; read BOOT_CFG BIC R1, R1, #0x00000001 ; clear SECURE_BOOT_EN bit STR R1, [R0, #0x00] ; write back DSB ; data memory barrier ISB ; instruction memory barrier }

然后在SystemInit()末尾调用HSE_SecureBoot_Disable();。这段代码的作用,是告诉HSE:“我不需要安全启动,请让我自由运行”。做完这一步,你的KEIL工程才能像S32DS工程一样,正常从Flash启动。当然,这只是“能跑”的最低要求。如果你的项目真要上车,后续必须用NXP的S32K1xx Security Toolbox工具,生成符合EVITA Medium规范的签名固件,并在KEIL里配置相应的Flash编程算法。但那是另一个故事了。现在,我们先把“能跑”这个基本盘守住。

4. 实操全流程与核心环节实现

纸上得来终觉浅,绝知此事要躬行。再完美的理论设计,不落到键盘上敲出每一行代码,都不算真正掌握。下面,我将以一个真实的、从零开始的S32K144 LED闪烁工程为例,带你走一遍完整的KEIL MDK迁移实操流程。这个例子不是玩具,它包含了所有关键环节:新建KEIL工程、导入源码、配置scatter file、编写SystemInit、配置调试器、编译下载、在线调试。每一步,我都附上了截图级的详细说明和参数设置,你可以直接“抄作业”。

4.1 第一步:创建KEIL MDK工程并导入基础文件

打开KEIL µVision v5.38(建议用这个版本,对S32K144 DFP支持最稳定),点击“Project” -> “New uVision Project...”,在弹出的窗口中,选择一个空文件夹作为工程目录,比如D:\S32K144_KEIL_LED,然后输入工程名S32K144_LED.uvprojx。点击“Save”后,会弹出“Select Device for Target”对话框,这里至关重要:不要在搜索框里直接输“S32K144”,而要展开“NXP” -> “S32K1xx” -> 选择“S32K144_100LQFP”。为什么?因为KEIL的Device Database里,S32K144有多个变体(100LQFP, 64LQFP, 48LQFP),它们的引脚定义、封装尺寸、甚至部分外设的可用性都不同。选错型号,后面配置引脚复用时会找不到对应的PORT。选好后,点击“OK”,KEIL会自动为你创建一个包含基本启动文件、CMSIS头文件、以及一个空main.c的工程框架。

接下来,导入S32DS工程里的源码。假设你的S32DS工程路径是D:\S32DS_LED\source\,里面包含main.c,led.c,led.h,clock_manager.c,pins_driver.c等文件。在KEIL的“Project”窗口里,右键点击“Source Group 1”,选择“Add Existing Files to Group 'Source Group 1'...”,然后导航到D:\S32DS_LED\source\,只勾选.c文件,不要勾选.h文件(头文件会自动被包含)。点击“Add”,这些C文件就出现在工程里了。此时,编译肯定会报错,因为KEIL还不知道去哪里找S32K144.h和fsl_common.h这些头文件。所以,下一步是配置“Include Paths”。右键工程名S32K144_LED,选择“Options for Target 'Target 1'...”,在弹出的窗口中,切换到“C/C++”选项卡,找到“Include Paths”编辑框,点击右侧的“...”按钮。在这里,你需要添加三条路径:

  1. D:\Keil_v5\ARM\PACK\Keil\S32K1xx_DFP\1.0.0\Device\NXP\S32K144\Include(KEIL的CMSIS头文件)
  2. D:\S32DS_LED\source\(你自己的头文件路径)
  3. D:\S32DS_LED\source\drivers\(如果S32DS工程里有单独的drivers文件夹)

添加完后,点击“OK”。此时,再编译,错误会少很多,但肯定还有,主要是startup_S32K144.s和clock_manager.c里的函数未定义。这就引出了下一步。

4.2 第二步:替换启动文件与重写SystemInit

在KEIL工程的“Project”窗口里,展开“Source Group 1”,找到名为startup_s32k144.s的文件(这是KEIL自动生成的),右键它,选择“Remove File from Group 'Source Group 1'”。然后,点击“Project” -> “Manage” -> “Run User Command...”,在弹出的窗口中,点击“Browse”按钮,导航到D:\Keil_v5\ARM\PACK\Keil\S32K1xx_DFP\1.0.0\Device\NXP\S32K144\Source\Templates\ARM\,选择startup_s32k144.s,点击“Open”,再点击“Add”按钮,把它加回工程。现在,工程里就有了KEIL原生的启动文件。

接下来,重写SystemInit()。打开startup_s32k144.s,找到SystemInit函数的定义(通常在文件末尾),你会发现它只是一个空的BX LR指令。我们需要把它替换成前面3.2节里写的那个精简版时钟初始化代码。但是,startup_s32k144.s是汇编文件,不能直接写C代码。所以,标准做法是:新建一个C文件,比如system_s32k144.c,在里面写SystemInit()函数,然后在startup_s32k144.s里,把BL SystemInit这条指令指向这个新文件。在KEIL里,右键“Source Group 1”,选择“Add New Item to Group 'Source Group 1'...”,在弹出的窗口中,选择“C File (.c)”,命名为system_s32k144.c,点击“Add”。然后,在这个新文件里,粘贴我们前面写的SystemInit()函数代码。保存。此时,编译,错误应该只剩下clock_manager.c和pins_driver.c里的函数未定义了——因为它们依赖S32DS的SDK,而我们已经决定不引入整个SDK。所以,我们要在main.c里,用CMSIS-Drivers重写LED初始化和翻转逻辑。这部分代码,我会在4.4节详细展开。

4.3 第三步:配置Scatter File与内存布局

KEIL默认

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进