MibSPI寄存器深度解析:从引脚控制到多设备通信实战

MibSPI寄存器深度解析:从引脚控制到多设备通信实战

1. MibSPI控制寄存器:嵌入式通信的精密调谐器

在嵌入式系统开发中,SPI(串行外设接口)因其简单、高速和全双工的特性,成为了连接微控制器与传感器、存储器、显示屏等外设的首选协议之一。然而,当你从简单的标准SPI应用转向更复杂的多设备、高可靠性场景时,会发现底层硬件的配置能力直接决定了系统的上限。德州仪器的Multi-Buffered SPI模块,也就是我们常说的MibSPI,正是在这个背景下脱颖而出的利器。它不仅仅是一个通信接口,更是一个配备了精密控制寄存器的通信引擎。这些寄存器,如SPIPC4/5/6、SPIDAT0/1、SPIBUF和SPIEMU,就像是引擎上的各个调节阀和仪表盘,工程师通过它们可以精细地控制每一个通信脉冲的时序、电平和状态。理解并熟练配置这些寄存器,意味着你能从“让设备跑起来”进阶到“让设备跑得又快又稳”,尤其是在应对多从机切换、抗噪声干扰、实时状态监控以及调试等高级需求时。今天,我们就抛开手册式的罗列,从一线开发的视角,深入聊聊这些关键寄存器背后的设计逻辑、实战配置要点以及那些容易踩坑的细节。

2. 引脚控制三剑客:SPIPC4/5/6寄存器深度解析

在MibSPI的舞台上,SPI通信引脚(SIMO, SOMI, CLK, ENA, SCS)并非只能扮演通信角色。它们可以被配置为通用输入/输出引脚,这在系统初始化、引脚复用测试或特定硬件控制场景下非常有用。SPIPC4、SPIPC5和SPIPC6这组寄存器,就是专门用于管理这些引脚在GPIO输出模式下的行为的精密控制器。

2.1 SPIPC4与SPIPC5:输出电平的“置位”与“清零”双簧

SPIPC4和SPIPC5是一对功能对称的寄存器,分别用于将配置为GPIO输出的SPI引脚驱动为高电平和低电平。这种设计并非多余,而是出于对操作原子性和效率的考量。

SPIPC4 寄存器的核心功能是“置位”。当你向它的某个位写入1时,如果对应的引脚已被配置为GPIO输出模式(通过SPIPC1/2/3等方向控制寄存器设置),那么该引脚就会被驱动为逻辑高电平。例如,SOMISET位域控制着SOMIx引脚,SIMOSET控制SIMOx,CLKSET控制时钟线,ENASET控制使能线,而SCSSET则控制着最多8个片选线。这里有一个关键细节:对于SOMI0和SIMO0这两个引脚,存在位重叠现象。SOMISET0SIMOSET0分别与SOMISETSIMOSET位域的最低有效位功能重叠。手册明确指出,当进行32位写操作时,SOMISET0的优先级高于SOMISET[24]SIMOSET0的优先级高于SIMOSET[16]。这意味着,如果你想精确控制SOMI0引脚,直接操作SOMISET0是更安全、意图更明确的做法,可以避免因误写整个位域而引发的意外。

注意:优先级陷阱:在进行32位整体写入SPIPC4时,务必注意SOMISET0SIMOSET0的优先级覆盖规则。一个常见的错误是,开发者想通过写SOMISET位域来同时控制多个SOMI引脚,却意外地因为SOMISET0位的值而改变了SOMI0的状态。最佳实践是,对于SOMI0/SIMO0这类有独立控制位的引脚,尽量使用位操作(如|=&=)或字节/半字写入来单独设置,避免不必要的副作用。

SPIPC5 寄存器则是SPIPC4的“镜像清零器”。它的位域命名如SOMICLRSIMOCLR等,功能与SPIPC4一一对应,但作用相反:写入1会将对应的引脚输出驱动为逻辑低电平。这种“置位-清零”分离的架构,在嵌入式实时系统中非常实用。它允许软件通过一次写操作,原子性地将某个或某组引脚拉低,而不需要先读取当前状态、修改、再回写(读-改-写周期)。这在需要严格时序的控制中,例如快速拉低片选线以启动传输,可以避免因非原子操作带来的时序抖动或竞态条件。

实战配置示例:假设我们需要在系统初始化时,将SPI的片选线SCS0和SCS1配置为GPIO输出,并初始化为高电平(无效状态),同时将SPIENA引脚也初始化为高电平。

// 假设寄存器基地址定义为 MIBSPI_BASE volatile uint32_t *spipc1 = (uint32_t*)(MIBSPI_BASE + 0x10); // SPIPC1 方向控制 volatile uint32_t *spipc4 = (uint32_t*)(MIBSPI_BASE + 0x24); // SPIPC4 置位寄存器 // 1. 配置引脚方向:将SCS0, SCS1, ENA 设置为GPIO输出模式 // 假设通过SPIPC1的相应位设置方向,这里简化表示 *spipc1 |= (1 << 0) | (1 << 1) | (1 << 8); // 设置SCS0, SCS1, ENA 为输出 // 2. 通过SPIPC4将输出初始化为高电平 // 设置SCSSET的bit0和bit1为1,对应SCS0和SCS1;设置ENASET为1 *spipc4 = (1 << 0) | (1 << 1) | (1 << 8); // 同时置位SCS0, SCS1, ENA

这段代码执行后,SCS0、SCS1和ENA引脚将被驱动至高电平。如果需要快速将SCS0拉低以选中从设备,则可以操作SPIPC5寄存器:*(volatile uint32_t*)(MIBSPI_BASE + 0x28) = (1 << 0);

2.2 SPIPC6:开漏输出模式的门卫

SPIPC6寄存器管理的是引脚的开漏输出使能。开漏输出是一种常见的输出结构,它只能将引脚拉低或置为高阻态,而不能主动驱动为高电平。高电平需要外部上拉电阻提供。这种模式常用于总线“线与”、电平转换或与不同电压域器件通信。

SPIPC6的每个使能位(如SOMIPDR,SIMOPDR,CLKPDR等)都有一个重要的前提条件:只有当对应的引脚同时满足以下两个条件时,设置开漏使能才会生效:

  1. 引脚被配置为GPIO输出模式(对应xxDIRx位为1)。
  2. 引脚的输出数据寄存器位(对应xxDOUTx位)为1,即逻辑上希望输出高电平。

只有在这两个条件都满足时,将SPIPC6的对应位置1,才会使引脚进入开漏模式(输出级关闭,呈高阻态)。如果xxDOUTx为0(希望输出低电平),那么无论SPIPC6如何设置,引脚都会被强驱动为低电平。这个逻辑确保了开漏模式只在需要输出“逻辑高”(实际为高阻)时启用,避免逻辑混乱。

实操心得:开漏配置顺序:配置一个引脚为开漏输出的正确顺序应该是:a) 配置为GPIO输出方向;b) 通过SPIPC4将输出数据设置为1(逻辑高);c) 最后再通过SPIPC6使能开漏模式。如果顺序颠倒,可能会在中间状态产生意外的强驱动输出,导致总线冲突。

3. 数据传输核心:SPIDAT0与SPIDAT1寄存器详解

数据寄存器是SPI通信的心脏。MibSPI提供了两个数据寄存器:SPIDAT0和SPIDAT1。它们并非简单的重复,而是承载着不同的设计意图和功能侧重。

3.1 SPIDAT0:兼容模式下的数据传输通道

SPIDAT0是一个相对“传统”的数据寄存器,其低16位TXDATA用于存放要发送的数据。它的主要特点是:

  • 仅用于兼容模式:在MibSPI强大的多缓冲器模式下,SPIDAT0是不可访问的。它主要用于维持与早期标准SPI模块的软件兼容性。
  • 数据搬运机制:写入SPIDAT0的数据,会首先尝试存入发送移位寄存器。如果移位寄存器正忙(正在发送数据),则数据会暂存于内部的TXBUF(发送缓冲器)中。这实现了一个简单的单级缓冲。
  • 数据对齐:无论字符长度配置如何,写入TXDATA的数据都必须右对齐。这是很多新手容易忽略的地方。如果字符长度是8位,那么数据应放在TXDATA[7:0];如果是12位,则应放在TXDATA[11:0],依此类推。硬件在发送时会根据数据格式寄存器(SPIFMTx)中配置的字符长度,提取有效位进行发送。
  • 格式选择:默认情况下,SPIDAT0使用SPIFMT0作为其数据格式控制寄存器。但是,你可以通过先配置SPIDAT1寄存器中的DFSEL字段,来为SPIDAT0选择不同的SPIFMTx寄存器。这提供了灵活性,但增加了配置的复杂性。

3.2 SPIDAT1:增强型控制与数据一体化寄存器

SPIDAT1才是MibSPI模块的“完全体”数据寄存器,它集数据与控制于一身,是实现自动片选、多格式传输等高级功能的关键。

寄存器结构剖析: SPIDAT1是一个32位寄存器,其字段可以清晰地分为控制字段数据字段两大部分。

  • 高16位(控制字段):
    • CSHOLD:片选保持位。此位置1时,在一次传输结束后,片选信号将保持有效,直到新的数据和片选信息被写入SPIDAT1。这对于需要连续向同一从设备发送多组数据的场景非常有用,可以避免片选信号的频繁翻转,节省时间并减少噪声。如果CSHOLD=0,则每次传输结束后,片选信号会在T2CDELAY时间后自动无效。
    • WDEL:等待延迟使能位。主模式下,此位置1会在当前事务结束后,插入一个由对应数据格式寄存器中WDELAY值定义的时钟延迟。在此期间,SPI总线暂停,片选信号保持无效。这用于满足某些慢速从设备对片选无效时间的最小要求。
    • DFSEL:数据格式选择位。这是一个2位字段,用于选择本次传输使用四个SPIFMTx格式寄存器中的哪一个。这允许主设备在与不同从设备通信时,动态切换数据长度、时钟极性和相位等参数,而无需重新配置整个SPI模块。
    • CSNR:片选编号。这个8位字段直接决定了在传输期间,哪个(或哪几个,如果支持)片选线被激活。写入SPIDAT1会立即将CSNR的值驱动到SPISCS引脚上(如果它们被配置为功能引脚)。这是实现自动片选的核心。
  • 低16位(数据字段):
    • TXDATA:与SPIDAT0中的TXDATA功能相同,用于存放发送数据,也必须右对齐。

高级功能应用场景: 假设系统连接了三个SPI从设备:一个ADC(使用SPIFMT0格式,片选CS0),一个DAC(使用SPIFMT1格式,片选CS1),和一个EEPROM(使用SPIFMT2格式,片选CS2)。传统方式需要频繁地重配置格式寄存器并手动控制GPIO来切换片选。而使用SPIDAT1,可以这样操作:

// 发送数据到ADC (CS0, 格式0) *(volatile uint32_t*)(MIBSPI_BASE + 0x3C) = (0 << 24) | (0x00 << 16) | (0x00 << 8) | adc_data; // 发送数据到DAC (CS1, 格式1),并保持片选以发送第二条命令 *(volatile uint32_t*)(MIBSPI_BASE + 0x3C) = (1 << 28) | (1 << 24) | (0x01 << 16) | (0x00 << 8) | dac_cmd; *(volatile uint32_t*)(MIBSPI_BASE + 0x3C) = (1 << 28) | (1 << 24) | (0x01 << 16) | (0x00 << 8) | dac_data; // CS1因CSHOLD仍有效 // 发送数据到EEPROM (CS2, 格式2),并在传输后插入延迟 *(volatile uint32_t*)(MIBSPI_BASE + 0x3C) = (1 << 26) | (2 << 24) | (0x02 << 16) | (0x00 << 8) | eeprom_data;

通过一次32位写操作,同时指定了数据、目标从设备(CSNR)、数据格式(DFSEL)以及传输行为(CSHOLD, WDEL),极大简化了软件流程并提高了效率。

注意事项:SPIDAT1的写入时机:手册强调,仅写入控制字段(例如只更新DFSELCSNR)不会在主模式下启动SPI传输。传输的启动是由写入TXDATA字段触发的。这个特性可以被巧妙利用:你可以在实际传输开始前,先通过字节写入操作预设好DFSEL,从而提前选定时钟极性和相位,确保第一次传输的时序就是正确的。

4. 状态监控与数据获取:SPIBUF与SPIEMU寄存器

通信过程中,及时获取接收数据和了解模块状态至关重要。SPIBUF和SPIEMU这对“孪生”寄存器,就是为这个目的服务的,但它们在使用上有着微妙的、至关重要的区别。

4.1 SPIBUF:动态的状态与数据窗口

SPIBUF是一个读操作具有“副作用”的关键寄存器。读取它的数据部分(RXDATA)不仅会返回接收到的数据,还会自动清除RXEMPTY标志,并可能清除如BITERRPARITYERR等错误标志。这是软件判断数据是否就绪并进行读取的标准流程的核心。

关键状态位解析

  1. RXEMPTY:接收缓冲区空标志。为1表示自上次读取SPIBUF后没有新数据;为0表示有新数据已就绪。读取RXDATA字段后,此位自动置1
  2. RXOVR:接收溢出标志。当一次传输完成,新数据要存入接收缓冲器RXBUF,但RXBUF已满时,此位置1。这里有个重要机制:溢出发生在RXBUF,而不是SPIBUF。SPIBUF中的数据在被读取前是安全的。发生溢出后,需要两次读取SPIBUF才能读到溢出时接收的数据。第一次读取出SPIBUF中旧的数据,第二次读取才会将RXBUF中(可能已被覆盖)的数据搬移到SPIBUF。
  3. TXFULL:发送缓冲区满标志。当TXBUF和发送移位寄存器都满时,此位置1,提示CPU暂停写入。
  4. 错误标志群:包括BITERR(位错误,主设备回读发送数据不一致)、DESYNC(从设备失步)、PARITYERR(奇偶校验错)、TIMEOUT(ENA超时)和DLENERR(数据长度错)。这些标志通常在读取RXDATA时被清除,为下一次传输的错误判断做准备。
  5. LCSNR:上一次使用的片选编号。这是从SPIDAT1的CSNR字段锁存过来的值,用于在中断服务程序中确认是哪个从设备传来的数据,无需软件额外记录。
  6. RXDATA:接收到的数据,总是右对齐存储,无论发送时的移位方向。

标准数据读取流程

// 轮询方式读取数据 volatile uint32_t *spibuf = (uint32_t*)(MIBSPI_BASE + 0x40); uint32_t buf_value; // 1. 等待接收数据就绪 (RXEMPTY 为 0) while ((*spibuf & (1 << 31)) != 0) { // 检查RXEMPTY位 (bit 31) // 等待或进行任务切换 } // 2. 读取整个SPIBUF寄存器 buf_value = *spibuf; // 3. 提取状态和数据 uint8_t last_cs = (buf_value >> 16) & 0xFF; // 提取LCSNR uint16_t received_data = buf_value & 0xFFFF; // 提取RXDATA bool has_error = (buf_value & 0x7F000000) != 0; // 检查错误标志位(bit 24-30) // 4. 根据状态处理 if (has_error) { // 处理具体错误,例如检查BITERR, PARITYERR等 handle_spi_error(buf_value); } // 使用 received_data 和 last_cs...

这个流程中,第2步的读取操作会自动将RXEMPTY置1,并清除可清除的错误标志,为下一次接收做好准备。

4.2 SPIEMU:静态的调试快照

SPIEMU在物理位域上与SPIBUF完全一致,包含相同的状态位和数据字段。但它有一个根本性的不同:对SPIEMU的读操作是“无害”的,不会改变任何状态标志

这个设计专为调试和仿真而生。当你在调试器中单步执行代码,或者在一个复杂的中断服务程序中,需要观察SPI的实时状态而又不希望因读取操作而破坏RXEMPTY等标志时,SPIEMU就是你的最佳选择。你可以随意读取SPIEMU来查看RXDATA内容和所有状态标志,而系统的实际状态流(由SPIBUF管理)不会受到任何干扰。

避坑指南:调试与运行时的寄存器选择:在编写调试日志函数或在线诊断代码时,务必使用SPIEMU来获取状态。如果错误地使用了SPIBUF,可能会在诊断过程中意外清除了错误标志或RXEMPTY位,使得真正的应用程序逻辑无法检测到本应发生的状态变化,导致数据丢失或错误无法上报。一个良好的编程习惯是,将用于调试和监控的代码路径与用于实际数据处理的代码路径严格分开,并明确注释所使用的寄存器。

5. 寄存器配置实战与高级技巧

理解了各个寄存器的功能后,如何将它们有机组合起来,构建一个稳定可靠的SPI通信驱动,是考验工程师功力的地方。下面以一个主设备与多个具有不同时序要求的从设备通信为例,展示配置流程和高级技巧。

5.1 多从设备通信的完整配置流程

假设我们需要驱动一个SPI Flash(模式0,8位数据,CS0)、一个ADC(模式3,16位数据,CS1)和一个数字电位器(模式1,12位数据,CS2)。

步骤1:全局初始化与引脚功能配置

void MibSPI_Init(void) { // 1. 使能MibSPI模块时钟(此步骤依赖具体MCU的时钟系统) Enable_Peripheral_Clock(MIBSPI_MODULE); // 2. 配置SPIPC1/2/3,将SIMO, SOMI, CLK, ENA, SCS[2:0]引脚设置为SPI功能(非GPIO) // 通常是将对应引脚控制寄存器的“功能选择”位设置为SPI,而非设置方向。 // 假设通过PINMUX寄存器配置,这里简化表示。 CONFIG_PIN_AS_SPI(SIMO_PIN); CONFIG_PIN_AS_SPI(SOMI_PIN); CONFIG_PIN_AS_SPI(CLK_PIN); CONFIG_PIN_AS_SPI(ENA_PIN); CONFIG_PIN_AS_SPI(CS0_PIN); CONFIG_PIN_AS_SPI(CS1_PIN); CONFIG_PIN_AS_SPI(CS2_PIN); // 3. 通过SPIPC4将所有的CS和ENA引脚初始输出为高电平(无效) // 注意:此时它们还是GPIO模式,但先设置好默认电平可以避免总线冲突 volatile uint32_t *spipc4 = (uint32_t*)(MIBSPI_BASE + 0x24); *spipc4 = (1 << 8) | (1 << 0) | (1 << 1) | (1 << 2); // 置位ENA, CS0, CS1, CS2 // 4. 配置SPIGCR0/1等全局控制寄存器,复位并使能MibSPI模块,选择主模式等 // 此处省略具体位操作,假设函数调用 MibSPI_Global_Enable(); }

步骤2:配置多组数据格式寄存器

void Configure_SPI_Formats(void) { volatile uint32_t *spifmt0 = (uint32_t*)(MIBSPI_BASE + 0x60); // SPIFMT0 volatile uint32_t *spifmt1 = (uint32_t*)(MIBSPI_BASE + 0x64); // SPIFMT1 volatile uint32_t *spifmt2 = (uint32_t*)(MIBSPI_BASE + 0x68); // SPIFMT2 // 配置SPIFMT0: 用于SPI Flash, 模式0 (CPOL=0, CPHA=0), 8位数据 *spifmt0 = (0 << 20) | // SHIFTDIR: MSB先发 (0 << 17) | // PARITYENA: 无奇偶校验 (8 << 8) | // CHARLEN: 字符长度8位 (0 << 6) | // PRESCALE: 预分频,具体值根据时钟计算 (0 << 4) | // PHASE: CPHA=0 (0 << 3); // POLARITY: CPOL=0 // 配置SPIFMT1: 用于ADC, 模式3 (CPOL=1, CPHA=1), 16位数据 *spifmt1 = (0 << 20) | // MSB先发 (0 << 17) | // 无奇偶校验 (16 << 8) | // 字符长度16位 (0 << 6) | // 预分频 (1 << 4) | // CPHA=1 (1 << 3); // CPOL=1 // 配置SPIFMT2: 用于数字电位器, 模式1 (CPOL=0, CPHA=1), 12位数据 *spifmt2 = (0 << 20) | // MSB先发 (0 << 17) | // 无奇偶校验 (12 << 8) | // 字符长度12位 (0 << 6) | // 预分频 (1 << 4) | // CPHA=1 (0 << 3); // CPOL=0 }

步骤3:实现基于SPIDAT1的智能传输函数

uint16_t MibSPI_Transmit(uint8_t cs_num, uint8_t fmt_sel, uint16_t data, bool hold_cs, bool enable_delay) { volatile uint32_t *spidat1 = (uint32_t*)(MIBSPI_BASE + 0x3C); volatile uint32_t *spibuf = (uint32_t*)(MIBSPI_BASE + 0x40); uint32_t control_field = 0; uint32_t tx_packet; // 1. 组装控制字段 control_field |= (cs_num & 0xFF) << 16; // CSNR control_field |= (fmt_sel & 0x3) << 24; // DFSEL if (hold_cs) { control_field |= (1 << 28); // CSHOLD } if (enable_delay) { control_field |= (1 << 26); // WDEL } // 2. 组装完整的32位发送数据包 (控制字段 + 右对齐的数据字段) tx_packet = control_field | (data & 0xFFFF); // 数据自动右对齐 // 3. 等待发送缓冲区就绪 (检查SPIBUF的TXFULL位或使用中断) while ((*spibuf & (1 << 29)) != 0) { // 等待TXFULL为0 // 超时处理可以加在这里 } // 4. 写入SPIDAT1,启动传输 *spidat1 = tx_packet; // 5. 等待接收完成 (轮询RXEMPTY,或使用接收中断) while ((*spibuf & (1 << 31)) != 0) { // 等待RXEMPTY为0 } // 6. 读取SPIBUF,获取接收数据和状态 uint32_t rx_packet = *spibuf; // 此读取会清除RXEMPTY和某些错误标志 // 7. 错误检查 if (rx_packet & 0x7F000000) { // 检查所有错误位 // 错误处理... return 0xFFFF; // 返回错误值 } // 8. 返回接收到的数据 return (uint16_t)(rx_packet & 0xFFFF); } // 使用示例 uint16_t read_adc_value(void) { // 读取ADC (CS1, 使用格式1,不保持CS,不插入延迟) return MibSPI_Transmit(1, 1, 0x8000, false, false); // 假设0x8000是ADC的读命令 } void write_flash_page(uint16_t addr, uint16_t data) { // 写Flash (CS0, 使用格式0),先发送写使能命令(需要保持CS) MibSPI_Transmit(0, 0, 0x0600, true, false); // WREN命令,CS保持 // 然后发送写命令和地址数据(CS继续保持) MibSPI_Transmit(0, 0, 0x0200, true, false); // PAGE WRITE命令 MibSPI_Transmit(0, 0, addr, false, true); // 地址,释放CS并插入延迟满足Flash的tCSH时间 // 实际数据写入...(通常需要轮询Flash忙状态) }

5.2 关键配置陷阱与优化策略

  1. NUM_CS_PINS参数的影响:在芯片设计时,通过NUM_CS_PINS这个参数决定了实际可用的片选线数量。在SPIPC4/5/6的SCSSET/SCSCLR/SCSPDR字段,以及SPIDAT1的CSNR和SPIBUF的LCSNR字段中,只有低NUM_CS_PINS位是有效的。高位读始终为0,写无效。在编写通用驱动时,最好通过宏或配置表来管理实际可用的CS数量,避免操作不存在的位。

  2. SPIDAT0在Multi-Buffer模式下的访问:务必记住,在MibSPI的多缓冲器模式下,SPIDAT0寄存器是不可访问的。如果你的应用使用了多缓冲器功能(用于DMA或更高效的数据流管理),所有数据传输都必须通过SPIDAT1或特定的缓冲器控制结构进行。试图访问SPIDAT0可能会导致硬件错误或未定义行为。

  3. 错误处理与状态清除的时序:错误标志(如BITERR,PARITYERR)通常在读取RXDATA时被清除。这意味着,如果你在中断服务程序中只读取了状态字而没有读取数据,错误标志将不会被清除,可能导致中断持续触发或错误状态被误判为持续存在。安全的做法是,在错误处理例程中,也执行一次对SPIBUF的完整读取(即使你可能不关心数据),以确保状态被正确清除。

  4. 利用SPIEMU进行非侵入式调试:在系统运行时,如果需要定期监控SPI总线状态(例如在RTOS的任务中),务必使用SPIEMU寄存器。你可以安全地读取它来获取TXFULLRXEMPTY、错误标志以及最新的RXDATA,而不会干扰主通信流程。这对于实现通信质量监控、负载统计或早期故障检测非常有用。

  5. 开漏模式与总线竞争:当配置SPI引脚为开漏模式并与外部上拉电阻一起使用时,需要特别注意总线竞争问题。如果多个设备(包括主机)同时试图驱动总线为低,没有问题。但如果一个设备输出高(实际为高阻),而另一个设备错误地配置为推挽输出并驱动为低,就会产生短路,可能损坏器件。因此,在共享总线上使用开漏模式时,必须严格管理各个设备的输出使能和方向控制。