1. 项目概述在嵌入式系统开发尤其是基于TI C2000系列MCU的工业控制、电机驱动或数字电源项目中你是否曾遇到过这样的困惑为什么我的ePWM模块明明配置了却没有输出为什么系统功耗总是居高不下或者在项目交付前如何确保核心算法代码不被轻易读取或篡改这些问题的答案往往都隐藏在芯片最底层的系统控制与安全模块寄存器之中。对于许多开发者来说数据手册中关于系统控制寄存器的章节常常是“最熟悉的陌生人”。我们可能知道PCLKCR寄存器能开关外设时钟CSM模块能保护代码但面对密密麻麻的位域描述却不知从何下手更不清楚这些配置背后深刻的系统级考量。实际上这些寄存器是连接硬件资源与软件逻辑的桥梁它们的配置直接决定了系统的性能、功耗和安全性。理解并熟练配置它们是从“能用”到“用好”C2000 MCU的关键一步。本文将以一个资深嵌入式工程师的视角带你深入TI C2000系列MCU的系统控制与安全模块。我们将不仅仅停留在寄存器位域的简单翻译上而是结合真实的项目开发场景拆解外设时钟控制寄存器PCLKCRx和代码安全模块CSM寄存器的工作原理、配置策略以及那些数据手册上不会写的“避坑指南”。无论你是正在调试一个复杂的电机驱动板还是为你的电源设计寻求最优的功耗与安全方案这篇文章都将为你提供一套可直接参考、复现的底层配置方法论。2. 系统控制模块的整体架构与设计哲学在深入每一个比特位之前我们有必要先站在系统架构师的角度理解TI C2000系列MCU设计系统控制模块的初衷。这绝非简单的功能堆砌其背后是一套严谨的、服务于高性能实时控制系统的设计哲学。2.1 时钟树一切性能与功耗的根源C2000 MCU的核心是一个高性能的C28x DSP内核但其周围环绕着数十个功能各异的外设如ePWM、eCAP、eQEP、SCI、SPI等。这些外设对时钟频率和精度的需求各不相同。例如ePWM模块需要高精度、高稳定性的时钟来生成精准的PWM波形而用于调试信息输出的SCI串口对时钟精度的要求则相对宽松。如果让所有外设都运行在最高的系统时钟SYSCLKOUT下无疑是巨大的功耗浪费。因此C2000引入了分层的时钟树结构并由系统控制模块中的几个关键寄存器进行管理。SYSCLKOUT是内核及部分高速外设的时钟源。通过高速外设时钟预分频器HISPCP和低速外设时钟预分频器LOSPCP可以分别生成HSPCLK和LSPCLK。通常ePWM、HRPWM等对时序要求苛刻的模块会挂载在HSPCLK域而SCI、SPI、I2C等通信接口则挂载在LSPCLK域。这种设计允许开发者根据实际需求独立调整不同时钟域的速度在满足性能的前提下实现功耗的最优化。实操心得在项目初期进行系统设计时不要一上来就把所有时钟预分频器设为最小值即最高频率。应先分析每个外设的任务周期和实时性要求。例如一个用于电机控制的ePWM模块可能需要150MHz的HSPCLK来获得高分辨率而一个仅用于偶尔传输配置参数的SCI模块其LSPCLK降到10MHz可能也绰绰有余。合理的时钟分配是低功耗设计的基石。2.2 外设时钟门控精细化的功耗管理单元有了分层的时钟树下一步就是如何将时钟精准地“投喂”给需要工作的外设并在其空闲时彻底“关闸”。这就是**外设时钟控制寄存器PCLKCR0, PCLKCR1, PCLKCR2, PCLKCR3**的核心职责。你可以把这些寄存器想象成一系列电灯开关每个开关控制着一个或一组外设模块的时钟输入。当某个外设不被使用时将其对应的时钟使能位清零该模块内部的时钟网络就会停止翻转其动态功耗会降至几乎为零。这与简单地将外设置于复位状态有本质区别。复位会清零外设的配置寄存器而仅关闭时钟则能保持其配置不变在需要时能快速唤醒非常适合间歇性工作的外设。这种设计带来了两个直接好处第一是显著的动态功耗节约在电池供电或对效率要求极高的场合如太阳能逆变器价值巨大第二是降低系统噪声减少不必要的时钟信号对模拟电路如ADC可能造成的干扰。2.3 安全模块CSM知识产权与系统可靠性的守护者对于工业产品固件就是核心资产。C2000的**代码安全模块CSM**提供了一套基于密码学原理的硬件级保护机制。它的核心思想是将Flash和RAM存储空间划分为不同的安全区域Zone并通过128位或256位的密码进行锁定。CSM模块相关的寄存器如Zx_CSMKEY,Zx_CSMCR,Zx_GRABSECTR等共同构成了一套完整的“安全协议”。上电后受保护的区域处于“锁定”状态无法通过调试器如JTAG读取或修改也无法被DMA等主机非法访问。只有当软件在运行时按照严格的流程向CSMKEY寄存器写入正确的密码后对应的区域才会临时“解锁”。更重要的是CSM与**执行保护EXEONLY**功能相结合可以实现更细粒度的保护。即使一个扇区被解锁也可以将其设置为“仅执行”模式这意味着CPU可以从该扇区取指运行代码但无法通过数据访问指令读取其中的内容。这有效防止了通过内存dump方式窃取算法代码。理解这套机制不仅是为了保护代码也是为了构建更可靠的系统。例如你可以将引导加载程序Bootloader放在安全区域防止被意外擦写将关键参数表设置为只读防止运行时被异常修改。3. 核心寄存器详解与配置策略了解了顶层设计我们现在深入到寄存器层面。数据手册的表格告诉了我们“是什么”而这里我们要探讨的是“为什么这么设计”以及“在实际项目中如何配置”。3.1 外设时钟控制寄存器PCLKCRx实战解析PCLKCR寄存器组是开发中最常打交道的一类。以你提供的资料中的PCLKCR0和PCLKCR1为例我们来看如何正确使用它们。PCLKCR0主要控制一些独立的外设模块时钟如McBSP-A、SCI-A、SPI-A、I2C-A和HRPWM。每个使能位通常只控制一个外设实例。// 假设我们要启用SCI-A和SPI-A模块进行通信 EALLOW; // 解除对受保护寄存器的写保护这是操作PCLKCR等系统寄存器必需的步骤 SysCtrlRegs.PCLKCR0.bit.SCIAENCLK 1; // 使能SCI-A时钟 SysCtrlRegs.PCLKCR0.bit.SPIAENCLK 1; // 使能SPI-A时钟 EDIS; // 重新启用写保护 // 在不需要时关闭时钟以省电 EALLOW; SysCtrlRegs.PCLKCR0.bit.SCIAENCLK 0; SysCtrlRegs.PCLKCR0.bit.SPIAENCLK 0; EDIS;PCLKCR1则更多地以“组”的形式控制外设特别是ePWM、eCAP和eQEP这类在电机控制中大量使用的模块。它们通常有多个实例如ePWM1-ePWM8。// 在电机控制应用中我们可能需要同时启用多个ePWM和eQEP模块 EALLOW; // 使能ePWM1, ePWM2, ePWM3 用于三相逆变器驱动 SysCtrlRegs.PCLKCR1.bit.EPWM1ENCLK 1; SysCtrlRegs.PCLKCR1.bit.EPWM2ENCLK 1; SysCtrlRegs.PCLKCR1.bit.EPWM3ENCLK 1; // 使能eQEP1模块用于连接光电编码器 SysCtrlRegs.PCLKCR1.bit.EQEP1ENCLK 1; EDIS;关键注意事项操作顺序务必在初始化外设配置其功能寄存器之前先使能其时钟。试图配置一个没有时钟的模写入操作可能无效或导致不可预知的行为。EALLOW/EDIS保护对PCLKCR等系统控制寄存器的写操作必须包裹在EALLOW和EDIS汇编指令宏中。这是C2000的一种安全机制防止代码跑飞后意外修改关键系统配置。TI的库函数通常已经封装好了这些宏。TBCLKSYNC位在PCLKCR0中这是一个非常特殊且重要的位。当使能多个ePWM模块时为了确保所有PWM的时间基准计数器TBCTR完全同步启动需要在配置完所有ePWM模块后将此位置1。这能保证多个桥臂的PWM波严格对齐对于电机和电源控制至关重要。功耗权衡在低功耗模式下如STANDBYCPU时钟可能停止但某些外设时钟如果被使能可能由独立的时钟源驱动而继续运行消耗功耗。进入低功耗模式前务必检查并关闭所有不必要的外设时钟。3.2 时钟预分频器HISPCP, LOSPCP配置计算HISPCP和LOSPCP寄存器决定了HSPCLK和LSPCLK相对于SYSCLKOUT的分频比。配置它们不仅仅是填写一个值更需要计算。假设你的系统主频SYSCLKOUT 200 MHz。HISPCP寄存器是一个3位字段其值HSPCLKDIV与分频比的对应关系为HSPCLK SYSCLKOUT / (2 * HSPCLKDIV)当HSPCLKDIV0时分频比为1。如果你需要HSPCLK 100 MHz供ePWM使用那么计算如下 所需分频比 200 MHz / 100 MHz 2。 根据公式HSPCLK SYSCLKOUT / (2 * HSPCLKDIV)即100 200 / (2 * HSPCLKDIV)解得HSPCLKDIV 1。 查看寄存器描述HISPCP.HSPCLK 001b对应分频比为/2这与我们的计算一致注意这里数据手册表格的“Value”列是二进制位值而“Description”列是分频结果有时需要对照公式理解。// 配置高速外设时钟为100MHz (假设SYSCLKOUT200MHz) EALLOW; SysCtrlRegs.HISPCP.all 0x0001; // 二进制001即HSPCLKDIV1 EDIS;LOSPCP的配置逻辑类似但其分频选项更多最高/14。对于低速异步串口SCI过高的时钟频率可能导致波特率误差增大通常选择一个适中的分频比如SYSCLKOUT/8或/10。3.3 代码安全模块CSM寄存器深度剖析CSM的配置流程相对复杂且一旦操作不当可能导致芯片永久锁死因此必须透彻理解。其核心寄存器可分为几类密码寄存器CSMPSWDx这不是一个可随意访问的寄存器而是Flash中的特定地址密码存储位置PWL。在芯片出厂或第一次编程时你需要通过编程器将128位4x32位密码写入这些固定地址。切记永远不要将密码写成全0或全1。全0意味着未受保护但根据CSM流程可能无法解锁全1是TI的出厂默认值表示芯片处于“擦除”状态安全模块被禁用。一旦你写入了非全1的密码安全机制即被激活。密钥寄存器Zx_CSMKEYx这是解锁操作的“钥匙孔”。当你的程序需要访问受保护的Flash区域例如从安全区域跳转到非安全区域执行函数或需要更新安全区域的参数时必须执行一个“解锁序列”。这个序列包括先对Flash中的密码地址CSMPSWDx进行一次虚读dummy read。这个操作会“武装”CSM设置CSM_ARMED位。然后将同样的密码值写入对应的Zx_CSMKEYx寄存器。如果密码匹配CSM_MATCH位会被置1该安全区域被解锁。状态与控制寄存器Zx_CSMCR这是了解当前安全状态和进行强制操作的窗口。CSM_ARMED和ECSL_ARMED指示是否已执行虚读操作是解锁流程的必要状态。CSM_MATCH和ECSL_MATCH指示密码匹配是否成功即区域是否已解锁。CSM_ALLZERO/ALLONE和ECSL_ALLZERO/ALLONE反映Flash中密码的实际内容用于判断安全状态全1为未安全全0为安全但密码为0其他为已安全且密码有效。FORCESEC位这是一个“紧急按钮”。向此位写1会立即清除所有KEY寄存器将区域重新锁定。此操作不可逆必须在确认无需再次访问安全区域后使用。资源分配寄存器Zx_GRABSECTR, Zx_GRABRAMR这些寄存器定义了Flash扇区和RAM块的安全属性及归属属于Zone1还是Zone2。其值是从Flash中的特定配置位置加载而来的通常在链接器命令文件.cmd中定义。例如你可以将包含核心算法的Flash扇区B、C分配给Zone1并设置为安全而将通信协议栈放在扇区D并分配给Zone2。仅执行控制寄存器Zx_EXEONLYR这是第二道防线。即使一个扇区通过密码解锁你也可以通过设置对应的EXEONLY_SECTx位为0使其处于“仅执行”模式。在此模式下CPU可以正常取指运行该区域的代码但任何通过数据地址访问该区域的指令如MOVL ARx, #addr都将被阻止返回无效数据。这能有效防止通过调试器或恶意代码读取机器码。严重警告与避坑指南密码备份将你设置的CSM密码物理离线备份例如写在纸上并存放在安全的地方。一旦忘记密码芯片将无法通过调试器连接也无法更新受保护区域的固件相当于一块“砖头”。解锁流程的原子性解锁操作虚读写KEY必须在中断禁用的情况下完成且中间不能插入任何对Flash的访问操作否则可能导致状态机错误解锁失败。测试流程在最终产品使能CSM前务必进行充分测试。可以先在代码中实现解锁函数但不调用确保所有功能正常。然后使能CSM测试解锁函数是否能正常工作。最后再在真实产品中启用。FORCESEC的使用除非确定在产品的整个生命周期内都不再需要更新或读取安全区域否则不要轻易使用FORCESEC。在产品出厂前的最后一道工序中使用它。4. 系统初始化与安全启动的完整实操流程理论需要与实践结合。下面我将勾勒出一个典型的C2000项目系统初始化与安全启动的代码框架并穿插关键步骤的解析。4.1 系统初始化阶段时钟与外设使能系统上电或复位后首先执行的是InitSysCtrl()函数或类似名称的初始化函数。这个函数通常由TI提供的库函数实现但理解其内部过程至关重要。void InitSysCtrl(void) { // 1. 禁用看门狗避免在初始化过程中复位 DisableDog(); // 2. 初始化PLL将输入时钟倍频到目标系统频率如200MHz // 这涉及配置PLL控制寄存器PLLCR并等待PLL锁定PLLSTS InitPll(DSP28_PLLCR, DSP28_DIVSEL); // 示例函数 // 3. 初始化外设时钟预分频器 EALLOW; // 配置高速外设时钟预分频例如分频为系统时钟的1/2 SysCtrlRegs.HISPCP.all 0x0001; // HSPCLK SYSCLKOUT/2 // 配置低速外设时钟预分频例如分频为系统时钟的1/4 SysCtrlRegs.LOSPCP.all 0x0002; // LSPCLK SYSCLKOUT/4 EDIS; // 4. 使能本项目需要使用的外设时钟 // 注意这里只使能必要的未使用的外设时钟保持关闭以节省功耗 EALLOW; // 假设项目需要ePWM1,2,3, eQEP1, SCI-A, SPI-A SysCtrlRegs.PCLKCR0.bit.SCIAENCLK 1; SysCtrlRegs.PCLKCR0.bit.SPIAENCLK 1; SysCtrlRegs.PCLKCR1.bit.EPWM1ENCLK 1; SysCtrlRegs.PCLKCR1.bit.EPWM2ENCLK 1; SysCtrlRegs.PCLKCR1.bit.EPWM3ENCLK 1; SysCtrlRegs.PCLKCR1.bit.EQEP1ENCLK 1; // 使能CPU定时器常用于系统节拍 SysCtrlRegs.PCLKCR3.bit.CPUTIMER0ENCLK 1; EDIS; // 5. 初始化其他系统模块如GPIO复用、中断控制器等 InitGpio(); InitPieCtrl(); // ... 其他初始化 }4.2 安全模块初始化与解锁流程如果项目中使用了CSM保护在main()函数的开始可能需要执行解锁操作。以下是一个简化的解锁Zone1的示例流程// 假设密码定义在头文件中且已编程到Flash的PWL位置 #define CSM_PWD0 0xAAAAAAAA #define CSM_PWD1 0x55555555 #define CSM_PWD2 0x12345678 #define CSM_PWD3 0x9ABCDEF0 void UnlockZone1(void) { volatile Uint32* PWL_Ptr; Uint16 IsUnlocked; // 步骤1检查当前是否已解锁避免重复操作 // 读取CSM状态寄存器检查CSM_MATCH位 IsUnlocked (CsmRegs.Z1_CSMCR.bit.CSM_MATCH 1); if(!IsUnlocked) { // 步骤2执行虚读Dummy Read以“武装”CSM // 指向Flash中密码存储位置的指针地址需根据具体型号查手册 PWL_Ptr (volatile Uint32 *)0x33FFF8; // 示例地址Zone1密码起始地址 // 连续读取4个32位密码字 *PWL_Ptr; // 读PWD0 *(PWL_Ptr 1); // 读PWD1 *(PWL_Ptr 2); // 读PWD2 *(PWL_Ptr 3); // 读PWD3 // 注意这些读取操作本身没有将数据赋值给变量目的是触发硬件状态机 // 步骤3禁用中断确保解锁过程的原子性 DINT; // 禁用全局中断 // 步骤4将密码写入KEY寄存器 EALLOW; CsmRegs.Z1_CSMKEY0 CSM_PWD0; CsmRegs.Z1_CSMKEY1 CSM_PWD1; CsmRegs.Z1_CSMKEY2 CSM_PWD2; CsmRegs.Z1_CSMKEY3 CSM_PWD3; EDIS; // 步骤5重新使能中断 EINT; // 使能全局中断 // 步骤6验证解锁是否成功 while(CsmRegs.Z1_CSMCR.bit.CSM_MATCH ! 1) { // 解锁失败处理可能是密码错误或流程错误 // 在实际产品中这里可能需要系统复位或进入错误处理循环 } } // 解锁成功或原本已解锁可以继续执行 }在main()函数中调用顺序如下void main(void) { // 阶段一基础系统初始化此时可能无法访问受保护的Flash InitSysCtrl(); // 初始化时钟、GPIO等 // 阶段二解锁安全区域如果需要调用位于安全区域的函数或数据 UnlockZone1(); // 阶段三初始化使用安全区域代码/数据的模块 InitAlgorithmModule(); // 假设此函数代码在安全Flash中 LoadSecureParameters(); // 假设关键参数存储在安全Flash中 // 阶段四主循环 for(;;) { RunApplication(); } }4.3 链接器命令文件.cmd中的安全区域划分寄存器的配置需要与内存布局相匹配。这通常在链接器命令文件中完成。以下是一个简化的示例展示了如何将代码和数据分配到不同的安全区域。MEMORY { PAGE 0: /* 程序存储器 */ ZONE1_SECURE (RWIX) : origin 0x3F0000, length 0x008000 /* 安全Zone1 32K */ ZONE2_NONSECURE (RWIX) : origin 0x3F8000, length 0x008000 /* 非安全Zone2 32K */ ... PAGE 1: /* 数据存储器 */ ... } SECTIONS { /* 将核心算法代码段(.secureCode)分配到安全Zone1 */ .secureCode : ZONE1_SECURE, PAGE 0 { -l secure_library.libalgorithm.obj (.text) *(.secureFunc) } /* 将密码存储位置PWL分配到特定的固定地址 */ .csm_pswd : CSM_PWL, PAGE 0 { -l my_password.obj (.csm_pswd) } /* 主程序和其他非敏感代码分配到非安全Zone2 */ .text : ZONE2_NONSECURE, PAGE 0 /* 数据段分配 */ ... }5. 常见问题排查与调试技巧实录即使理解了原理和流程在实际开发中依然会遇到各种问题。下面是我在多年项目中总结的一些典型问题及其排查思路。5.1 外设时钟使能了但外设不工作症状配置了ePWM或SCI的寄存器但无输出或无法通信。排查步骤确认时钟已使能在调试器中直接查看PCLKCR0、PCLKCR1等寄存器的值确认对应外设的使能位是否为1。这是最常被忽略的一步。检查时钟源频率确认HISPCP和LOSPCP的配置是否符合预期。例如如果你为SCI配置了基于LSPCLK的波特率但LSPCLK被意外分频得很低会导致实际波特率错误。检查外设复位状态有些外设有独立的复位寄存器如SOFTRESET。确保外设不在复位状态。检查GPIO复用外设功能信号如PWM输出、串口TX需要通过GPIO MUX寄存器映射到具体的物理引脚上。确认MUX配置正确。使用示波器用示波器测量外设时钟引脚如XCLKOUT如果使能的话或外设输出引脚直接观察是否有信号。这是最直接的硬件验证手段。5.2 CSM解锁失败芯片无法调试症状使能CSM并编程后调试器CCS无法连接芯片提示“找不到设备”或“安全区域被锁定”。排查思路检查密码这是首要原因。确认编程到Flash PWL位置的密码与代码中UnlockZone1()函数里写入CSMKEY寄存器的密码完全一致包括大小端格式。一个字符的错误就会导致失败。检查解锁代码位置你的解锁代码UnlockZone1函数必须位于非安全区域例如RAM中或未受保护的Flash扇区。如果解锁代码本身就在安全区域那么CPU在尝试取指执行它时就会被阻止形成死锁。检查解锁流程确保严格按照“虚读 - 写KEY”的流程并且在写KEY期间禁用了中断。流程被打断是常见失败原因。检查GRABSECTR配置确认你试图访问的Flash扇区在GRABSECTR寄存器中的配置是正确的例如已分配给正确的Zone且不是“Invalid”状态。后备方案——解锁引导模式一些C2000型号提供了“密码恢复”或“安全引导”模式可以通过特定的GPIO引脚上电序列强制从非安全区域如SARAM启动一个引导加载程序从而有机会修复密码或更新固件。但这需要硬件设计上的支持。5.3 系统功耗高于预期症状测量芯片的供电电流发现即使在空闲状态也远高于数据手册中给出的典型低功耗值。排查与优化审计PCLKCR寄存器进入低功耗模式如IDLE或STANDBY前使用调试器或代码打印所有PCLKCR寄存器的值。查找是否有本该关闭的外设时钟仍然处于使能状态。特别注意PCLKCR3中的CPUTIMER和DMA时钟它们常被忘记关闭。检查未使用的模拟模块ADC、比较器、DAC等模拟模块即使时钟关闭其模拟电路部分也可能消耗静态电流。查看相应的电源控制寄存器如ADCCTL1中的ADCPWDN位将其关闭。GPIO引脚泄漏配置为输入的悬空GPIO引脚可能会因电平不定而产生漏电流。将其配置为带上拉或下拉的输出模式或者设置为模拟输入如果支持。时钟门控与频率缩放在运行模式下如果CPU负载不高可以考虑动态降低系统时钟频率通过调整PLL或使用内部振荡器。C2000支持多种低功耗模式需要配合LPMCR寄存器正确配置。5.4 寄存器配置后不生效症状代码中写了寄存器但读取回来发现值没变或者外设行为不符合配置。排查步骤EALLOW/EDIS对于所有受保护的寄存器包括系统控制、GPIO MUX、PIE等写操作必须放EALLOW和EDIS之间。忘记加是新手最常见的错误。寄存器映射与位域确认你操作的寄存器位域名称与芯片型号完全匹配。不同型号的C2000寄存器位域可能有细微差别。使用TI提供的对应型号的头文件如DSP2833x_GlobalVariableDefs.c可以最大程度避免此问题。写时序某些寄存器有特殊的写时序要求。例如对PLL控制寄存器PLLCR的修改需要等待PLL锁定PLLSTS寄存器中的PLLLOCKS位后才能生效。操作ADC校准寄存器也有严格的顺序。内存访问宽度C2000是32位架构对寄存器的访问建议使用32位访问uint32_t。虽然它也支持16位访问但不当的访问方式可能导致数据错误。使用TI定义的结构体和位域是最安全的方式。5.5 调试安全代码的实用技巧调试涉及CSM的代码非常棘手因为调试器无法直接读取安全区域。以下是一些变通方法分阶段开发先在不使能CSM的情况下完成所有功能开发和测试。确认一切正常后再最后集成和测试CSM解锁代码。使用RAM调试将需要调试的安全区域代码临时链接到RAM中运行。RAM通常是非安全的调试器可以正常访问。但这要求代码尺寸不能超过可用RAM。利用串口打印在非安全区域编写一个通过SCI输出调试信息的函数。在安全区域的代码中通过函数调用将关键变量如寄存器值、状态标志传递出来打印。这是最常用的“盲调”手段。硬件调试接口一些高端调试探针如XDS560支持“安全身份验证”功能可以在提供密码后访问安全区域但这通常需要额外的许可和配置。深入理解并熟练运用TI C2000的系统控制与安全模块寄存器是迈向高级嵌入式开发者的必经之路。它要求我们不仅关注功能的实现更要理解芯片内部的资源调度机制、功耗构成和安全边界。从精细的时钟门控到严谨的密码学保护每一处设计都体现了对可靠性、效率和资产保护的极致追求。希望本文的拆解和实录能帮助你将这些看似枯燥的寄存器位转化为构建稳定、高效、安全嵌入式系统的有力工具。记住最好的代码是那些充分理解了硬件特性的代码。