1. 项目概述与核心价值在嵌入式开发领域尤其是基于瑞萨RL78系列微控制器的项目中Flash内存的在线编程是一个绕不开的核心话题。无论是为智能家电添加固件空中升级FOTA功能还是在工业传感器中实现关键参数的掉电保存都离不开对片上Flash存储器的直接操作。然而直接操作硬件寄存器不仅繁琐更充满了风险——一个时序错误或模式设置不当就可能导致芯片锁死或数据损坏让整个项目陷入僵局。这正是瑞萨官方提供的Renesas Flash Driver (RFD) RL78 Type 01库的价值所在。它并非一个简单的函数集合而是一套经过严格验证的、硬件抽象化的操作框架。它将底层复杂的Flash控制序列器Sequencer、状态寄存器、以及关键的中断管理机制封装成一系列标准化的API函数。对于开发者而言这意味着我们无需再深究FLPMC、FSSET、FLSIVC这些寄存器每一位的具体含义只需关注业务逻辑我要擦除哪一块、写入什么数据、如何保证操作期间系统稳定。RFD库特别是其Type 01版本为RL78/G23、G22、G24等型号提供了统一的编程接口极大地提升了开发效率与代码的可靠性。本文将深入解析RFD RL78 Type 01 API函数库的设计精髓与实战要点。我不会仅仅罗列函数原型而是会结合我多年在RL78平台上的开发经验拆解每个关键API背后的硬件原理、调用时机、以及那些手册上不会写明但实际项目中一定会遇到的“坑”。无论你是正在为产品添加自升级功能还是需要安全地管理数据Flash相信这份融合了官方规范与实践心得的指南都能为你提供清晰的路径。2. RFD RL78 Type 01 整体架构与设计思路要玩转RFD库首先得理解它的“游戏规则”。RFD库的设计核心是状态机和硬件序列器。它不是一个让你随意、随时读写Flash的“万能钥匙”而是一套需要严格遵守流程的“安全操作规程”。2.1 核心概念三种操作模式RFD库将Flash内存的操作抽象为三种模式这是所有操作的基石非编程模式 (Non-programmable Mode)微控制器的默认模式。在此模式下CPU可以正常执行代码访问Flash中的程序和数据但不能对Flash进行擦写。任何编程操作前都必须先切换到编程模式。代码Flash编程模式 (Code Flash Programming Mode)在此模式下可以对存放程序代码的Flash区域进行擦除和写入操作。切换到此模式后对代码Flash的访问会通过硬件序列器进行。数据Flash编程模式 (Data Flash Programming Mode)专用于对数据Flash区域通常容量较小用于存储参数进行操作的模式。需要特别注意访问数据Flash前必须通过特定API或寄存器位使其使能。关键理解模式切换并非软件意义上的一个标志位设置而是通过配置硬件的FLPMC (Flash Memory Programming Control Register)寄存器来实现的。R_RFD_SetFlashMemoryMode()函数就是帮你安全地完成这个硬件配置。绝对禁止在序列器正在执行命令如擦除中时进行模式切换这会导致不可预料的后果。2.2 硬件序列器异步操作的执行引擎RL78的Flash编程并非由CPU直接逐条指令完成而是由一个独立的硬件模块——Flash序列器来执行。你可以把它想象成一个专门负责“体力活”的协处理器。当你调用R_RFD_Erase()或R_RFD_Write()这类函数时API只是向序列器下达了命令设置好目标地址、命令码等然后立即返回。实际的擦除、写入等耗时操作则由序列器在后台异步完成。这就引出了两个至关重要的概念命令启动函数如R_RFD_Erase,R_RFD_Write它们负责配置参数并启动序列器。状态检查函数如R_RFD_CheckCFDFSeqEndStep1/2它们用于轮询序列器的工作状态判断操作是否完成。为什么需要两个步骤的检查Step1和Step2这是RFD库为确保状态检测绝对可靠而设计的双保险机制。Step1检查序列器是否报告“操作结束”SQEND/ESQEND标志Step2则在清除控制寄存器后再次确认所有操作真正完毕。跳过任何一步都可能因为状态残留导致后续操作误判。在我的项目中曾因偷懒只做了一次检查在连续快速操作时偶尔出现写入失败排查许久才发现是状态未完全同步的问题。2.3 中断管理编程期间的“安全气囊”Flash编程期间最危险的事情之一就是被中断打断。如果在一个擦除周期中间发生了中断CPU转去执行中断服务程序可能会访问正在被修改的Flash区域或者干扰序列器的时序轻则导致当前操作失败重则损坏Flash内容甚至锁死芯片。RFD库提供了优雅的解决方案中断向量重定向。通过R_RFD_ChangeInterruptVector()函数你可以将所有中断的入口地址临时指向RAM中的一个特定函数。这样当Flash编程期间发生中断时CPU会跳转到RAM中的这个“中转站”函数。在这个函数里你可以安全地判断中断源、保存上下文、甚至执行一些紧急任务因为RAM的访问不会干扰Flash序列器的工作。操作完成后再通过R_RFD_RestoreInterruptVector()切回原来的中断向量表。这个功能是实现在线编程IAP而不“变砖”的关键。但实现它需要一些技巧我们会在后续章节详细展开。3. 关键API函数深度解析与实战要点掌握了整体框架我们来逐一拆解那些最核心、也最容易出错的API函数。我会结合代码片段和实际场景说明“怎么用”以及“为什么这么用”。3.1 初始化与配置一切的开端3.1.1R_RFD_Init()– 设定心跳频率这是使用RFD库的第一个且必须调用的函数。e_rfd_ret_t ret; uint8_t cpu_freq_mhz 16; // 假设你的MCU运行在16MHz ret R_RFD_Init(cpu_freq_mhz); if (ret ! R_RFD_ENUM_RET_STS_OK) { // 处理错误频率参数非法 }核心作用该函数将你传入的CPU频率MHz设置到Flash序列器的定时器中。Flash的擦写操作对时序有极其严格的要求这个频率值决定了序列器内部计时基准直接影响编程脉冲的宽度。参数要点与巨坑参数范围对于RL78/G23/G22范围为1-32 MHz对于RL78/G24还支持48 MHz。必须根据实际MCU型号和运行频率准确填写。“向上取整”原则手册明确要求如果CPU实际运行频率不是整数例如4.5MHz参数必须向上取整填5。这是为了保证时序余量防止因频率估算不足导致编程电压或时间不够造成写入数据不可靠。我曾遇到过在4.8MHz下参数填4导致批量生产中有千分之几的产品数据保存期不达标问题就出在这里。频率一致性这里填写的频率必须是执行Flash编程操作时CPU实际运行的频率。如果你在初始化后改变了系统时钟例如从HOCO切换到PLL必须重新初始化RFD库或者确保编程期间频率与初始化时一致。3.1.2 用户定义宏区分芯片类别在编译前你必须根据目标芯片在编译器选项中定义对应的宏R_RFD_MCU_FLASH_T01_CATEGORY01用于RL78/G23 或 G22。R_RFD_MCU_FLASH_T01_CATEGORY02用于RL78/G24。为什么需要这个RL78/G24的Flash模块在时序和某些寄存器位上与G23/G22有差异。这个宏会引导RFD库内部选择正确的底层驱动代码。如果定义错误或未定义轻则编译报错重则运行时操作异常。我建议在项目的全局头文件或编译器预定义中明确设置并添加注释。// 在项目配置中例如IAR的Preprocessor Definitions #define R_RFD_MCU_FLASH_T01_CATEGORY01 // 或者 #define R_RFD_MCU_FLASH_T01_CATEGORY023.2 模式管理与数据Flash访问3.2.1R_RFD_SetDataFlashAccessMode()– 数据Flash的钥匙数据FlashData Flash通常与代码Flash物理隔离需要显式“使能”才能被CPU或序列器访问。// 在需要操作数据Flash之前 R_RFD_SetDataFlashAccessMode(R_RFD_ENUM_DF_ACCESS_ENABLE); // ... 执行擦除、写入等操作 ... // 操作完成后可以禁用访问非必须但有时为安全考虑 R_RFD_SetDataFlashAccessMode(R_RFD_ENUM_DF_ACCESS_DISABLE);关键细节该函数通过设置DFLCTL.DFLEN位来实现。使能后需要等待一段短暂的建立时间典型值250nsAPI内部已处理。必须在非编程模式下调用此函数。试图在编程模式中切换访问使能状态是未定义行为。一个常见的应用场景是在系统初始化时使能数据Flash读取保存的参数在参数更新时先使能然后切换至数据Flash编程模式进行操作。3.2.2R_RFD_SetFlashMemoryMode()– 模式切换指挥官这是进入和退出编程模式的唯一安全入口。e_rfd_ret_t ret; // 1. 从非编程模式切换到代码Flash编程模式 ret R_RFD_SetFlashMemoryMode(R_RFD_ENUM_FLASH_MODE_CODE_PROGRAMMING); if (ret ! R_RFD_ENUM_RET_STS_OK) { // 切换失败可能当前已在编程模式或序列器忙 } // ... 执行代码Flash的擦写操作 ... // 2. 操作完成后切换回非编程模式 ret R_RFD_SetFlashMemoryMode(R_RFD_ENUM_FLASH_MODE_UNPROGRAMMABLE);函数内部做了什么调用R_RFD_HOOK_EnterCriticalSection()进入临界区通常关中断。根据目标模式配置FLPMC寄存器例如代码编程模式设置FLSPM位。插入硬件要求的等待时间tMS。将之前由R_RFD_Init()设定的频率值写入FSSET寄存器。调用R_RFD_HOOK_ExitCriticalSection()退出临界区。致命注意事项顺序是铁律只能从非编程模式切换到代码或数据编程模式。不允许直接在两种编程模式间切换。必须先切回非编程模式。依赖初始化务必在调用任何R_RFD_SetFlashMemoryMode()之前成功调用R_RFD_Init()。否则频率未设置编程时序错乱数据无法保证。中断向量标志当切回非编程模式时函数会检查g_u08_change_interrupt_vector_flag标志。如果之前重定向过中断向量该标志为0x55它会将FLPMC.FWEDIS位清零确保中断继续指向RAM如果未重定向标志为0x00则设置FWEDIS1恢复指向ROM向量表。这意味着如果你手动修改了中断相关寄存器可能会破坏这个逻辑。3.3 中断向量重定向机制详解这是RFD库中最精妙也最需要小心处理的部分用于在Flash编程期间安全地处理中断。3.3.1R_RFD_ChangeInterruptVector()– 构建安全岛此函数将所有中断的入口地址临时修改到你指定的一个RAM中的函数地址。// 1. 在RAM中定义一个中断分发函数 #pragma interrupt (IntHandler_RAM) __far void IntHandler_RAM(void) { // 读取中断请求标志寄存器判断是哪个中断源 uint8_t ir_flag ISPR; // 示例实际寄存器需查手册 // 根据标志位调用对应的实际中断服务程序(ISR) if (ir_flag 0x01) { // 假设是定时器中断 Timer_ISR(); } else if (ir_flag 0x02) { // 假设是串口中断 UART_ISR(); } // ... 其他中断判断 // 注意必须手动清除已处理的中断请求标志 ISPR ir_flag; // 清除对应的位 } // 2. 在进入Flash编程前重定向中断向量 R_RFD_ChangeInterruptVector((uint32_t)((void (__far *)(void)) IntHandler_RAM));实现原理关中断通过Hook函数。清除FLPMC.FWEDIS位允许修改中断向量。将你提供的RAM函数地址写入FLSIVC0和FLSIVC1寄存器。设置VECTCTRL寄存器让所有中断都跳转到FLSIVC指定的地址。开中断通过Hook函数。你必须亲自动手的事编写RAM中断分发函数这个函数需要能识别所有可能发生的中断源。因为所有中断都涌向这里。手动清除中断标志在RAM中断函数里硬件不会自动清除中断请求标志IF位你必须用代码读取并清除它们否则会陷入无限中断。注意函数地址转换示例中(void (__far *)(void))的强制转换对于CC-RL编译器是必需的用于生成正确的远调用地址。IAR和LLVM的语法略有不同需参考手册。3.3.2R_RFD_RestoreInterruptVector()– 回归正轨编程操作结束后调用此函数恢复原状。// Flash编程操作全部完成后 R_RFD_RestoreInterruptVector();内部操作关中断。设置FLPMC.FWEDIS位禁止修改中断向量。清除VECTCTRL寄存器让中断向量表恢复为ROM中的地址。开中断。重要配合R_RFD_ChangeInterruptVector和R_RFD_RestoreInterruptVector必须成对调用并且调用之间不能嵌套。通常的编程流程是重定向 - 切换至编程模式 - 执行操作 - 切换回非编程模式 - 恢复向量。3.4 序列器状态检查与错误处理异步操作的核心在于“等待完成”和“处理异常”。3.4.1 状态检查双步曲CheckCFDFSeqEndStep1Step2以擦除代码Flash的一个块为例// 假设已切换到代码Flash编程模式并启动了擦除命令 R_RFD_Erase(some_address); // 第一步等待序列器报告操作结束 e_rfd_ret_t ret; do { ret R_RFD_CheckCFDFSeqEndStep1(); } while (ret R_RFD_ENUM_RET_STS_BUSY); // 忙则循环等待 // 第二步确认操作完全结束 if (ret R_RFD_ENUM_RET_STS_OK) { do { ret R_RFD_CheckCFDFSeqEndStep2(); } while (ret R_RFD_ENUM_RET_STS_BUSY); } if (ret R_RFD_ENUM_RET_STS_OK) { // 擦除成功可以执行后续操作如写入 } else { // 处理错误 }为什么需要两步Step1检查的是硬件序列器是否完成了它的物理操作SQEND标志。Step2是在软件清除序列器控制寄存器FSSQ后再次确认硬件状态已完全稳定没有残留操作。这能有效避免在极短时间内连续发起操作时因状态机未完全复位而导致的失败。超时机制是必须的上面的代码使用了死循环while(busy)在实际产品中这是危险的。一定要添加超时判断例如循环计数超过一个预期值根据擦除/写入时间计算后强制跳出并视为操作失败进行错误恢复。#define FLASH_OP_TIMEOUT_MS 100 uint32_t timeout_counter 0; do { ret R_RFD_CheckCFDFSeqEndStep1(); timeout_counter; if(timeout_counter (FLASH_OP_TIMEOUT_MS * 1000)) { // 粗略估算循环次数 // 超时处理记录错误尝试恢复或复位 break; } } while (ret R_RFD_ENUM_RET_STS_BUSY);3.4.2 错误状态获取R_RFD_GetSeqErrorStatus当Check...EndStep2返回错误或操作感觉异常时需要查询具体错误原因。uint8_t error_status 0; R_RFD_GetSeqErrorStatus(error_status); if (error_status 0x20) { // Bit5: Extra area sequencer error // 额外区域序列器错误 } if (error_status 0x10) { // Bit4: Code/data flash memory area sequencer error // 主序列器错误 } if (error_status 0x08) { // Bit3: Blank check command error // 空白检查失败要写入的区域未擦除干净 } if (error_status 0x02) { // Bit1: Write command error // 写入命令错误地址非法、数据错误等 } if (error_status 0x01) { // Bit0: Erase command error // 擦除命令错误电压不足、时序错误等 }注意此函数应在序列器空闲时调用。在忙状态时读取可能得到不准确的值。3.4.3 强制停止与清理R_RFD_ForceStopSeq()紧急制动按钮。仅在擦除或空白检查命令执行过程中且出现严重问题需要强行终止时使用。强制停止后目标Flash区块可能处于不确定状态必须重新擦除。切勿在写入操作时使用否则会导致写入数据损坏。R_RFD_ClearSeqRegister()操作后的清理工。在一次完整的编程操作如擦除-写入-校验流程结束后调用此函数来清除FLAPH、FLAPL等地址/数据寄存器为下一次操作准备一个干净的环境。务必在Check...EndStep2返回OK后调用。4. 完整实战流程与代码框架下面我将展示一个在RL78/G23上对数据Flash进行参数保存的典型流程。这个流程包含了从初始化到恢复的完整步骤并融入了错误处理和超时机制。/* 假设数据Flash起始地址为0x00100000我们要写入4字节数据 */ #define DATA_FLASH_BASE_ADDR 0x00100000 #define PARAMETER_SIZE 4 /* 1. 全局变量与Hook函数需用户实现 */ volatile uint8_t g_interrupt_state; void R_RFD_HOOK_EnterCriticalSection(void) { g_interrupt_state __get_interrupt_state(); // 伪代码获取中断状态 __disable_interrupt(); // 关中断 } void R_RFD_HOOK_ExitCriticalSection(void) { if (g_interrupt_state) { __enable_interrupt(); // 恢复中断 } } /* 2. RAM中的中断分发函数需根据实际中断源实现 */ #pragma interrupt (RAM_Interrupt_Handler) __far void RAM_Interrupt_Handler(void) { // 简化示例仅处理一个中断 if (IS_TAU0_INTERRUPT()) { // 假设的宏判断定时器中断 TAU0_ISR_Handler(); // 用户实际的定时器中断处理函数 CLEAR_TAU0_INTERRUPT_FLAG(); // 必须手动清标志 } // ... 其他中断判断 } /* 3. 主操作函数 */ e_rfd_ret_t Save_Parameter_To_DataFlash(uint32_t param) { e_rfd_ret_t ret R_RFD_ENUM_RET_STS_OK; uint32_t timeout; uint8_t error_status; /* 步骤A: 初始化与准备 */ ret R_RFD_Init(16); // CPU频率16MHz if (ret ! R_RFD_ENUM_RET_STS_OK) return ret; /* 步骤B: 重定向中断向量至RAM */ R_RFD_ChangeInterruptVector((uint32_t)((void (__far *)(void)) RAM_Interrupt_Handler)); /* 步骤C: 使能数据Flash访问 */ R_RFD_SetDataFlashAccessMode(R_RFD_ENUM_DF_ACCESS_ENABLE); /* 步骤D: 切换到数据Flash编程模式 */ ret R_RFD_SetFlashMemoryMode(R_RFD_ENUM_FLASH_MODE_DATA_PROGRAMMING); if (ret ! R_RFD_ENUM_RET_STS_OK) goto exit_restore; /* 步骤E: 执行擦除操作数据Flash通常按块/页擦除*/ ret R_RFD_Erase(DATA_FLASH_BASE_ADDR); if (ret ! R_RFD_ENUM_RET_STS_OK) goto exit_mode; timeout 0; do { ret R_RFD_CheckCFDFSeqEndStep1(); if (timeout 1000000UL) { // 超时判断 ret R_RFD_ENUM_RET_ERR_TIMEOUT; // 自定义超时错误码 break; } } while (ret R_RFD_ENUM_RET_STS_BUSY); if (ret R_RFD_ENUM_RET_STS_OK) { do { ret R_RFD_CheckCFDFSeqEndStep2(); } while (ret R_RFD_ENUM_RET_STS_BUSY); } if (ret ! R_RFD_ENUM_RET_STS_OK) { R_RFD_GetSeqErrorStatus(error_status); // 记录错误日志: error_status goto exit_clear; } /* 步骤F: 执行写入操作 */ ret R_RFD_Write(DATA_FLASH_BASE_ADDR, (uint8_t*)param, PARAMETER_SIZE); if (ret ! R_RFD_ENUM_RET_STS_OK) goto exit_clear; timeout 0; do { ret R_RFD_CheckCFDFSeqEndStep1(); if (timeout 500000UL) { // 写入超时通常比擦除短 ret R_RFD_ENUM_RET_ERR_TIMEOUT; break; } } while (ret R_RFD_ENUM_RET_STS_BUSY); if (ret R_RFD_ENUM_RET_STS_OK) { do { ret R_RFD_CheckCFDFSeqEndStep2(); } while (ret R_RFD_ENUM_RET_STS_BUSY); } if (ret ! R_RFD_ENUM_RET_STS_OK) { R_RFD_GetSeqErrorStatus(error_status); // 记录错误日志 } exit_clear: /* 步骤G: 清除序列器寄存器 */ R_RFD_ClearSeqRegister(); exit_mode: /* 步骤H: 切换回非编程模式 */ R_RFD_SetFlashMemoryMode(R_RFD_ENUM_FLASH_MODE_UNPROGRAMMABLE); exit_restore: /* 步骤I: 恢复中断向量 */ R_RFD_RestoreInterruptVector(); /* 步骤J: 禁用数据Flash访问可选 */ R_RFD_SetDataFlashAccessMode(R_RFD_ENUM_DF_ACCESS_DISABLE); return ret; }5. 常见问题排查与实战经验即使严格遵循手册在实际项目中依然会遇到各种问题。下面是我总结的一些典型故障场景和排查思路。5.1 编译与链接问题问题链接错误提示找不到R_RFD_xxx函数。排查首先确认RFD库文件.lib是否已正确添加到你的工程中。其次检查是否定义了正确的宏CATEGORY01或CATEGORY02。最后确认你调用的API函数名称与库版本完全一致瑞萨有时会在不同版本间微调函数名。问题使用R_RFD_ChangeInterruptVector时编译器报关于函数指针或段放置的警告尤其在IAR中。排查这是正常现象因为将中断函数强制放在RAM中与编译器的常规优化策略冲突。可以按照手册建议在工程选项的Extra Options中添加--diag_suppressTa030,Be006来抑制特定警告。但更推荐的做法是仔细检查你的RAM中断函数是否正确定义为__far或__interrupt类型以及地址强制转换是否正确。5.2 运行时操作失败问题R_RFD_Init()返回参数错误0x10。排查百分之百是传入的CPU频率值不对。用示波器或调试器确认你的系统时钟频率。如果使用了分频确认是CPU时钟频率不是振荡器频率。对于RL78/G24确认是否该传48。问题擦除或写入操作总是失败CheckCFDFSeqEndStep2返回错误或超时。排查清单模式检查操作前是否成功切换到了正确的编程模式代码/数据可以用R_RFD_CheckFlashMemoryMode()验证当前模式。地址对齐Flash操作通常有严格的地址对齐要求如256字节块擦除。确认你传入的地址是块起始地址。数据Flash访问使能如果是操作数据Flash是否在操作前调用了R_RFD_SetDataFlashAccessMode(R_RFD_ENUM_DF_ACCESS_ENABLE)电压与时钟Flash编程对供电电压和时钟稳定性有要求。确保在操作期间MCU电压在规格书要求范围内且高速片上振荡器HOCO稳定运行。不要在切换时钟源的过程中进行Flash编程。干扰中断是否在编程期间有未被重定向的中断发生确认R_RFD_ChangeInterruptVector和R_RFD_RestoreInterruptVector被正确配对调用且RAM中的中断分发函数正确清除了所有中断标志。问题操作成功后读取Flash发现数据不正确或部分正确。排查写入后延迟在写入操作完成CheckCFDFSeqEndStep2返回OK后不要立即读取。插入一个短暂的软件延时几个微秒到几十微秒等待Flash单元完全稳定。缓存问题有些RL78型号可能有指令缓存或预取指缓冲区。在写入代码Flash后如果立即从相同地址取指执行可能会读到旧数据。需要在写入后执行一条“刷新流水线”的操作例如跳转到一个绝对无关的地址或者使用__memory_changed()之类的编译器内置函数如果有。数据宽度确认你写入的数据类型和读取的数据类型一致。R_RFD_Write接收的是uint8_t指针确保你的数据源格式正确。5.3 系统稳定性问题问题在频繁进行Flash操作如记录日志后系统偶尔会跑飞或复位。排查Flash寿命Flash有擦写次数限制通常10万次。频繁擦写同一区块会使其提前失效。务必实现磨损均衡算法轮流使用不同的Flash扇区。电源完整性Flash编程瞬间电流较大。如果电源设计不佳可能导致电压跌落影响CPU稳定。确保电源路径足够宽并在MCU的Vdd引脚附近放置足够容量的去耦电容如10uF电解0.1uF陶瓷。看门狗Flash操作耗时较长毫秒级可能触发看门狗复位。在长耗时操作如全擦除中需要适时喂狗或者临时禁用看门狗需评估安全性。5.4 高级技巧与优化减少模式切换开销模式切换R_RFD_SetFlashMemoryMode涉及关中断和硬件等待有一定开销。如果业务逻辑允许尽量将多个Flash操作如连续写入多个参数集中在一次“进入编程模式-操作-退出编程模式”的周期内完成而不是每次操作都切换模式。RAM中断函数的效率RAM中的中断分发函数应尽可能短小精悍。它只负责判断中断源和清除标志然后调用位于ROM中的实际ISR。避免在RAM ISR中做复杂运算因为RAM访问速度可能慢于ROM且占用宝贵的RAM空间。Hook函数的实现R_RFD_HOOK_EnterCriticalSection和R_RFD_HOOK_ExitCriticalSection需要用户实现。最简单的实现就是直接关中断和开中断__disable_interrupt(),__enable_interrupt()。但在复杂RTOS环境中你可能需要与RTOS的临界区管理接口对接以保持调度器状态一致。