SPI通信 #

一、SPI协议基础 #

1.1 SPI概述 #

SPI(Serial Peripheral Interface)是一种高速全双工同步串行通信协议。

text
┌─────────────────────────────────────────────────────────┐
│                    SPI总线连接方式                       │
├─────────────────────────────────────────────────────────┤
│                                                         │
│     ┌─────────┐                                        │
│     │   主机  │                                        │
│     │ (Master)│                                        │
│     └────┬────┘                                        │
│          │                                             │
│    ┌─────┼─────┬─────┬─────┐                          │
│    │     │     │     │     │                          │
│   SCLK  MOSI  MISO  CS0  CS1                           │
│    │     │     │     │     │                          │
│    ├─────┼─────┼─────┤     │                          │
│    │     │     │     │     │                          │
│  ┌─┴──┐ ┌┴──┐ ┌┴──┐ ┌┴──┐ ┌┴──┐                      │
│  │SCLK│ │SI │ │SO │ │CS │ │CS │                      │
│  │    │ │   │ │   │ │   │ │   │                      │
│  │ 从机0 │ │   │ │   │ │   │ │   │                      │
│  └────┘ └───┘ └───┘ └───┘ └───┘                      │
│                                                         │
│  SCLK: 时钟线 (主机输出)                                │
│  MOSI: 主机输出从机输入                                 │
│  MISO: 主机输入从机输出                                 │
│  CS: 片选信号 (低电平有效)                              │
│                                                         │
└─────────────────────────────────────────────────────────┘

1.2 SPI特性 #

特性 说明
通信方式 全双工同步串行
线路数量 4线(SCLK、MOSI、MISO、CS)
时钟极性 CPOL (0或1)
时钟相位 CPHA (0或1)
传输速率 可达数MHz甚至数十MHz
多从机 通过多个CS线支持

1.3 SPI工作模式 #

模式 CPOL CPHA 说明
Mode 0 0 0 空闲低电平,第一边沿采样
Mode 1 0 1 空闲低电平,第二边沿采样
Mode 2 1 0 空闲高电平,第一边沿采样
Mode 3 1 1 空闲高电平,第二边沿采样
text
Mode 0 (CPOL=0, CPHA=0):
    ___     ___     ___     ___
___|   |___|   |___|   |___|   |___  SCLK
       ^       ^       ^       ^
      采样    采样    采样    采样

Mode 1 (CPOL=0, CPHA=1):
    ___     ___     ___     ___
___|   |___|   |___|   |___|   |___  SCLK
         ^       ^       ^       ^
        采样    采样    采样    采样

1.4 Raspberry Pi SPI引脚 #

text
    3.3V  ──[01] [02]──  5V
   GPIO2  ──[03] [04]──  5V
   GPIO3  ──[05] [06]──  GND
   GPIO4  ──[07] [08]──  GPIO14
     GND  ──[09] [10]──  GPIO15
  GPIO17  ──[11] [12]──  GPIO18
  GPIO27  ──[13] [14]──  GND
  GPIO22  ──[15] [16]──  GPIO23
    3.3V  ──[17] [18]──  GPIO24
   GPIO10  ──[19] [20]──  GND     GPIO10 = MOSI
    GPIO9  ──[21] [22]──  GPIO25   GPIO9  = MISO
   GPIO11  ──[23] [24]──  GPIO8    GPIO11 = SCLK
     GND  ──[25] [26]──  GPIO7     GPIO8  = CE0
                                   GPIO7  = CE1

二、启用SPI接口 #

2.1 系统配置 #

bash
# 启用SPI接口
sudo raspi-config
# 选择 Interface Options -> SPI -> Enable

# 或手动启用
sudo nano /boot/config.txt
# 添加或取消注释
dtparam=spi=on

# 重启
sudo reboot

2.2 验证SPI #

bash
# 检查SPI设备
ls /dev/spi*

# 输出示例:
# /dev/spidev0.0  /dev/spidev0.1

# 安装SPI工具
sudo apt install python3-spidev -y

三、Pi4J SPI编程 #

3.1 基本配置 #

java
package com.example.spi;

import com.pi4j.Pi4J;
import com.pi4j.io.spi.Spi;
import com.pi4j.io.spi.SpiConfig;
import com.pi4j.io.spi.SpiMode;

public class SPIBasicDemo {

    public static void main(String[] args) {
        var pi4j = Pi4J.newAutoContext();
        
        SpiConfig config = Spi.newConfigBuilder(pi4j)
                .id("spi-device")
                .name("SPI Device")
                .bus(0)
                .chipSelect(0)
                .mode(SpiMode.MODE_0)
                .baud(1000000)
                .build();
        
        Spi spi = pi4j.create(config);
        
        System.out.println("SPI设备已配置");
        System.out.println("总线: " + config.bus());
        System.out.println("片选: " + config.chipSelect());
        System.out.println("模式: " + config.mode());
        System.out.println("波特率: " + config.baud());
        
        pi4j.shutdown();
    }
}

3.2 数据传输 #

java
package com.example.spi;

import com.pi4j.Pi4J;
import com.pi4j.io.spi.Spi;
import com.pi4j.io.spi.SpiConfig;
import com.pi4j.io.spi.SpiMode;

public class SPITransferDemo {

    public static void main(String[] args) {
        var pi4j = Pi4J.newAutoContext();
        
        Spi spi = pi4j.create(Spi.newConfigBuilder(pi4j)
                .id("spi-demo")
                .name("SPI Demo")
                .bus(0)
                .chipSelect(0)
                .mode(SpiMode.MODE_0)
                .baud(1000000)
                .build());
        
        byte[] txData = {0x01, 0x02, 0x03, 0x04};
        System.out.print("发送数据: ");
        for (byte b : txData) {
            System.out.printf("0x%02X ", b);
        }
        System.out.println();
        
        byte[] rxData = spi.transfer(txData);
        
        System.out.print("接收数据: ");
        for (byte b : rxData) {
            System.out.printf("0x%02X ", b);
        }
        System.out.println();
        
        byte singleByte = spi.transfer((byte) 0xAA);
        System.out.printf("单字节传输: 发送 0xAA, 接收 0x%02X%n", singleByte);
        
        pi4j.shutdown();
    }
}

3.3 多从机控制 #

java
package com.example.spi;

import com.pi4j.Pi4J;
import com.pi4j.io.spi.Spi;
import com.pi4j.io.spi.SpiConfig;
import com.pi4j.io.spi.SpiMode;
import java.util.HashMap;
import java.util.Map;

public class MultiSlaveSPI {

    private final var pi4j;
    private final Map<String, Spi> devices = new HashMap<>();
    
    public MultiSlaveSPI() {
        this.pi4j = Pi4J.newAutoContext();
    }
    
    public void addDevice(String name, int bus, int chipSelect, 
                         SpiMode mode, int baud) {
        SpiConfig config = Spi.newConfigBuilder(pi4j)
                .id(name)
                .name(name)
                .bus(bus)
                .chipSelect(chipSelect)
                .mode(mode)
                .baud(baud)
                .build();
        
        devices.put(name, pi4j.create(config));
    }
    
    public byte[] transfer(String deviceName, byte[] data) {
        Spi device = devices.get(deviceName);
        if (device != null) {
            return device.transfer(data);
        }
        return null;
    }
    
    public void shutdown() {
        pi4j.shutdown();
    }
    
    public static void main(String[] args) {
        MultiSlaveSPI spi = new MultiSlaveSPI();
        
        spi.addDevice("sensor1", 0, 0, SpiMode.MODE_0, 1000000);
        spi.addDevice("sensor2", 0, 1, SpiMode.MODE_1, 2000000);
        
        byte[] response1 = spi.transfer("sensor1", new byte[]{0x01, 0x02});
        byte[] response2 = spi.transfer("sensor2", new byte[]{0x03, 0x04});
        
        spi.shutdown();
    }
}

四、实际设备驱动开发 #

4.1 MCP3008 ADC驱动 #

java
package com.example.spi.device;

import com.pi4j.Pi4J;
import com.pi4j.io.spi.Spi;
import com.pi4j.io.spi.SpiConfig;
import com.pi4j.io.spi.SpiMode;

public class MCP3008ADC {

    private static final int NUM_CHANNELS = 8;
    private static final int RESOLUTION = 1024;
    private static final double VREF = 3.3;
    
    private final Spi spi;
    
    public MCP3008ADC(var pi4j, int bus, int chipSelect) {
        this.spi = pi4j.create(Spi.newConfigBuilder(pi4j)
                .id("mcp3008")
                .name("MCP3008 ADC")
                .bus(bus)
                .chipSelect(chipSelect)
                .mode(SpiMode.MODE_0)
                .baud(1000000)
                .build());
    }
    
    public int readRaw(int channel) {
        if (channel < 0 || channel >= NUM_CHANNELS) {
            throw new IllegalArgumentException("Invalid channel: " + channel);
        }
        
        byte[] txData = {
            (byte) (0x01),
            (byte) ((channel + 8) << 4),
            0x00
        };
        
        byte[] rxData = spi.transfer(txData);
        
        int result = ((rxData[1] & 0x03) << 8) | (rxData[2] & 0xFF);
        return result;
    }
    
    public double readVoltage(int channel) {
        int raw = readRaw(channel);
        return (raw * VREF) / (RESOLUTION - 1);
    }
    
    public double readPercentage(int channel) {
        int raw = readRaw(channel);
        return (raw * 100.0) / (RESOLUTION - 1);
    }
    
    public int[] readAllChannels() {
        int[] values = new int[NUM_CHANNELS];
        for (int i = 0; i < NUM_CHANNELS; i++) {
            values[i] = readRaw(i);
        }
        return values;
    }
    
    public static void main(String[] args) throws InterruptedException {
        var pi4j = Pi4J.newAutoContext();
        
        MCP3008ADC adc = new MCP3008ADC(pi4j, 0, 0);
        
        System.out.println("MCP3008 ADC测试");
        System.out.println("参考电压: " + VREF + "V");
        System.out.println();
        
        while (true) {
            System.out.println("=== 通道读数 ===");
            for (int i = 0; i < NUM_CHANNELS; i++) {
                int raw = adc.readRaw(i);
                double voltage = adc.readVoltage(i);
                System.out.printf("通道%d: 原始值=%4d, 电压=%.3fV%n", 
                    i, raw, voltage);
            }
            System.out.println();
            Thread.sleep(1000);
        }
    }
}

4.2 NRF24L01无线模块驱动 #

java
package com.example.spi.device;

import com.pi4j.Pi4J;
import com.pi4j.io.spi.Spi;
import com.pi4j.io.spi.SpiConfig;
import com.pi4j.io.spi.SpiMode;
import com.pi4j.io.gpio.digital.*;

public class NRF24L01 {

    private static final int REGISTER_CONFIG = 0x00;
    private static final int REGISTER_EN_AA = 0x01;
    private static final int REGISTER_EN_RXADDR = 0x02;
    private static final int REGISTER_SETUP_AW = 0x03;
    private static final int REGISTER_SETUP_RETR = 0x04;
    private static final int REGISTER_RF_CH = 0x05;
    private static final int REGISTER_RF_SETUP = 0x06;
    private static final int REGISTER_STATUS = 0x07;
    private static final int REGISTER_RX_ADDR_P0 = 0x0A;
    private static final int REGISTER_TX_ADDR = 0x10;
    private static final int REGISTER_RX_PW_P0 = 0x11;
    private static final int REGISTER_FIFO_STATUS = 0x17;
    
    private static final byte COMMAND_R_REGISTER = 0x00;
    private static final byte COMMAND_W_REGISTER = 0x20;
    private static final byte COMMAND_R_RX_PAYLOAD = 0x61;
    private static final byte COMMAND_W_TX_PAYLOAD = (byte) 0xA0;
    private static final byte COMMAND_FLUSH_TX = (byte) 0xE1;
    private static final byte COMMAND_FLUSH_RX = (byte) 0xE2;
    private static final byte COMMAND_NOP = (byte) 0xFF;
    
    private final Spi spi;
    private final DigitalOutput ce;
    
    public NRF24L01(var pi4j, int spiBus, int spiCs, int cePin) {
        this.spi = pi4j.create(Spi.newConfigBuilder(pi4j)
                .id("nrf24l01")
                .name("NRF24L01 Wireless")
                .bus(spiBus)
                .chipSelect(spiCs)
                .mode(SpiMode.MODE_0)
                .baud(8000000)
                .build());
        
        this.ce = pi4j.create(DigitalOutput.newConfigBuilder(pi4j)
                .id("nrf-ce")
                .name("NRF CE")
                .address(cePin)
                .shutdown(DigitalState.LOW)
                .initial(DigitalState.LOW)
                .provider("pigpio-digital-output"));
    }
    
    public void initialize() {
        ce.low();
        
        writeRegister(REGISTER_CONFIG, (byte) 0x0C);
        writeRegister(REGISTER_EN_AA, (byte) 0x01);
        writeRegister(REGISTER_EN_RXADDR, (byte) 0x01);
        writeRegister(REGISTER_SETUP_AW, (byte) 0x03);
        writeRegister(REGISTER_SETUP_RETR, (byte) 0x5F);
        writeRegister(REGISTER_RF_CH, (byte) 0x4C);
        writeRegister(REGISTER_RF_SETUP, (byte) 0x07);
        
        flushTX();
        flushRX();
        
        writeRegister(REGISTER_STATUS, (byte) 0x70);
    }
    
    public void setTXAddress(byte[] address) {
        writeRegisterMulti(REGISTER_TX_ADDR, address);
        writeRegisterMulti(REGISTER_RX_ADDR_P0, address);
    }
    
    public void setRXAddress(int pipe, byte[] address) {
        writeRegisterMulti(REGISTER_RX_ADDR_P0 + pipe, address);
    }
    
    public void setPayloadSize(int size) {
        writeRegister(REGISTER_RX_PW_P0, (byte) size);
    }
    
    public void powerUpTX() {
        byte config = readRegister(REGISTER_CONFIG);
        config = (byte) ((config & ~0x01) | 0x02);
        writeRegister(REGISTER_CONFIG, config);
        ce.high();
    }
    
    public void powerUpRX() {
        byte config = readRegister(REGISTER_CONFIG);
        config = (byte) (config | 0x03);
        writeRegister(REGISTER_CONFIG, config);
        ce.high();
    }
    
    public void powerDown() {
        ce.low();
        byte config = readRegister(REGISTER_CONFIG);
        config = (byte) (config & ~0x02);
        writeRegister(REGISTER_CONFIG, config);
    }
    
    public boolean transmit(byte[] data) {
        flushTX();
        
        byte[] txData = new byte[data.length + 1];
        txData[0] = COMMAND_W_TX_PAYLOAD;
        System.arraycopy(data, 0, txData, 1, data.length);
        spi.transfer(txData);
        
        ce.high();
        try { Thread.sleep(1); } catch (InterruptedException e) {}
        ce.low();
        
        byte status;
        int timeout = 100;
        do {
            status = readRegister(REGISTER_STATUS);
            try { Thread.sleep(1); } catch (InterruptedException e) {}
        } while ((status & 0x30) == 0 && --timeout > 0);
        
        writeRegister(REGISTER_STATUS, (byte) 0x30);
        
        return (status & 0x20) != 0;
    }
    
    public byte[] receive(int length) {
        byte status = readRegister(REGISTER_STATUS);
        
        if ((status & 0x40) == 0) {
            return null;
        }
        
        writeRegister(REGISTER_STATUS, (byte) 0x40);
        
        byte[] txData = new byte[length + 1];
        txData[0] = COMMAND_R_RX_PAYLOAD;
        byte[] rxData = spi.transfer(txData);
        
        byte[] payload = new byte[length];
        System.arraycopy(rxData, 1, payload, 0, length);
        
        return payload;
    }
    
    public boolean dataAvailable() {
        byte status = readRegister(REGISTER_STATUS);
        return (status & 0x40) != 0;
    }
    
    private byte readRegister(int register) {
        byte[] txData = {(byte) (COMMAND_R_REGISTER | register), COMMAND_NOP};
        byte[] rxData = spi.transfer(txData);
        return rxData[1];
    }
    
    private void writeRegister(int register, byte value) {
        byte[] txData = {(byte) (COMMAND_W_REGISTER | register), value};
        spi.transfer(txData);
    }
    
    private void writeRegisterMulti(int register, byte[] data) {
        byte[] txData = new byte[data.length + 1];
        txData[0] = (byte) (COMMAND_W_REGISTER | register);
        System.arraycopy(data, 0, txData, 1, data.length);
        spi.transfer(txData);
    }
    
    private void flushTX() {
        spi.transfer(new byte[]{COMMAND_FLUSH_TX});
    }
    
    private void flushRX() {
        spi.transfer(new byte[]{COMMAND_FLUSH_RX});
    }
    
    public static void main(String[] args) throws InterruptedException {
        var pi4j = Pi4J.newAutoContext();
        
        NRF24L01 nrf = new NRF24L01(pi4j, 0, 0, 25);
        nrf.initialize();
        
        byte[] address = {0xE7, 0xE7, 0xE7, 0xE7, 0xE7};
        nrf.setTXAddress(address);
        nrf.setPayloadSize(32);
        
        System.out.println("NRF24L01测试");
        
        nrf.powerUpTX();
        
        byte[] message = "Hello NRF24L01!".getBytes();
        boolean success = nrf.transmit(message);
        
        System.out.println("发送结果: " + (success ? "成功" : "失败"));
        
        pi4j.shutdown();
    }
}

4.3 SD卡读写 #

java
package com.example.spi.device;

import com.pi4j.Pi4J;
import com.pi4j.io.spi.Spi;
import com.pi4j.io.spi.SpiConfig;
import com.pi4j.io.spi.SpiMode;

public class SDCardDriver {

    private static final byte CMD0 = 0x40;
    private static final byte CMD8 = 0x48;
    private static final byte CMD55 = 0x77;
    private static final byte CMD41 = 0x69;
    private static final byte CMD17 = 0x51;
    private static final byte CMD24 = 0x58;
    
    private static final byte R1_IDLE = 0x01;
    private static final byte R1_READY = 0x00;
    private static final byte DATA_START = (byte) 0xFE;
    
    private final Spi spi;
    private boolean initialized = false;
    
    public SDCardDriver(var pi4j, int bus, int chipSelect) {
        this.spi = pi4j.create(Spi.newConfigBuilder(pi4j)
                .id("sdcard")
                .name("SD Card")
                .bus(bus)
                .chipSelect(chipSelect)
                .mode(SpiMode.MODE_0)
                .baud(500000)
                .build());
    }
    
    public boolean initialize() {
        for (int i = 0; i < 10; i++) {
            spi.transfer((byte) 0xFF);
        }
        
        byte response = sendCommand(CMD0, 0);
        if (response != R1_IDLE) {
            return false;
        }
        
        response = sendCommand(CMD8, 0x000001AA);
        
        long timeout = System.currentTimeMillis() + 1000;
        while (System.currentTimeMillis() < timeout) {
            response = sendCommand(CMD55, 0);
            response = sendCommand(CMD41, 0x40000000);
            
            if (response == R1_READY) {
                initialized = true;
                return true;
            }
            
            try { Thread.sleep(10); } catch (InterruptedException e) {}
        }
        
        return false;
    }
    
    public byte[] readBlock(long blockAddress) {
        if (!initialized) return null;
        
        byte response = sendCommand(CMD17, blockAddress);
        if (response != R1_READY) {
            return null;
        }
        
        long timeout = System.currentTimeMillis() + 1000;
        byte token;
        do {
            token = spi.transfer((byte) 0xFF);
            if (System.currentTimeMillis() > timeout) {
                return null;
            }
        } while (token != DATA_START);
        
        byte[] data = new byte[512];
        for (int i = 0; i < 512; i++) {
            data[i] = spi.transfer((byte) 0xFF);
        }
        
        spi.transfer((byte) 0xFF);
        spi.transfer((byte) 0xFF);
        
        return data;
    }
    
    public boolean writeBlock(long blockAddress, byte[] data) {
        if (!initialized || data.length != 512) return false;
        
        byte response = sendCommand(CMD24, blockAddress);
        if (response != R1_READY) {
            return false;
        }
        
        spi.transfer(DATA_START);
        
        for (byte b : data) {
            spi.transfer(b);
        }
        
        spi.transfer((byte) 0xFF);
        spi.transfer((byte) 0xFF);
        
        byte status = spi.transfer((byte) 0xFF);
        return (status & 0x0F) == 0x05;
    }
    
    private byte sendCommand(byte command, long argument) {
        spi.transfer((byte) 0xFF);
        
        spi.transfer(command);
        spi.transfer((byte) (argument >> 24));
        spi.transfer((byte) (argument >> 16));
        spi.transfer((byte) (argument >> 8));
        spi.transfer((byte) argument);
        
        byte crc = (command == CMD0) ? (byte) 0x95 : (byte) 0x01;
        spi.transfer(crc);
        
        byte response;
        int attempts = 0;
        do {
            response = spi.transfer((byte) 0xFF);
            attempts++;
        } while ((response & 0x80) != 0 && attempts < 10);
        
        return response;
    }
    
    public static void main(String[] args) {
        var pi4j = Pi4J.newAutoContext();
        
        SDCardDriver sd = new SDCardDriver(pi4j, 0, 0);
        
        System.out.println("初始化SD卡...");
        if (sd.initialize()) {
            System.out.println("SD卡初始化成功");
            
            byte[] data = sd.readBlock(0);
            if (data != null) {
                System.out.println("读取块0成功,前16字节:");
                for (int i = 0; i < 16; i++) {
                    System.out.printf("%02X ", data[i]);
                }
                System.out.println();
            }
        } else {
            System.out.println("SD卡初始化失败");
        }
        
        pi4j.shutdown();
    }
}

五、SPI工具类封装 #

java
package com.example.spi;

import com.pi4j.Pi4J;
import com.pi4j.io.spi.Spi;
import com.pi4j.io.spi.SpiConfig;
import com.pi4j.io.spi.SpiMode;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class SPIUtils {

    public static class SPIDevice {
        private final Spi spi;
        
        public SPIDevice(Spi spi) {
            this.spi = spi;
        }
        
        public byte transferByte(byte data) {
            return spi.transfer(data);
        }
        
        public byte[] transferBytes(byte[] data) {
            return spi.transfer(data);
        }
        
        public short transferShort(short data, ByteOrder order) {
            ByteBuffer txBuffer = ByteBuffer.allocate(2).order(order);
            txBuffer.putShort(data);
            byte[] rxData = spi.transfer(txBuffer.array());
            return ByteBuffer.wrap(rxData).order(order).getShort();
        }
        
        public int transferInt(int data, ByteOrder order) {
            ByteBuffer txBuffer = ByteBuffer.allocate(4).order(order);
            txBuffer.putInt(data);
            byte[] rxData = spi.transfer(txBuffer.array());
            return ByteBuffer.wrap(rxData).order(order).getInt();
        }
        
        public void writeCommand(byte command, byte[] data) {
            byte[] txData = new byte[data.length + 1];
            txData[0] = command;
            System.arraycopy(data, 0, txData, 1, data.length);
            spi.transfer(txData);
        }
        
        public byte[] readCommand(byte command, int length) {
            byte[] txData = new byte[length + 1];
            txData[0] = command;
            byte[] rxData = spi.transfer(txData);
            byte[] result = new byte[length];
            System.arraycopy(rxData, 1, result, 0, length);
            return result;
        }
    }
    
    public static SPIDevice createDevice(var pi4j, int bus, int chipSelect,
                                         SpiMode mode, int baud) {
        SpiConfig config = Spi.newConfigBuilder(pi4j)
                .id("spi-" + bus + "-" + chipSelect)
                .name("SPI Device")
                .bus(bus)
                .chipSelect(chipSelect)
                .mode(mode)
                .baud(baud)
                .build();
        
        return new SPIDevice(pi4j.create(config));
    }
}

六、SPI调试技巧 #

6.1 常用调试方法 #

bash
# 检查SPI设备
ls -la /dev/spi*

# 使用Python测试SPI
python3 -c "
import spidev
spi = spidev.SpiDev()
spi.open(0, 0)
spi.max_speed_hz = 1000000
data = spi.xfer([0x01, 0x02, 0x03])
print('Received:', [hex(b) for b in data])
spi.close()
"

6.2 常见问题排查 #

问题 可能原因 解决方案
通信失败 模式不匹配 检查设备SPI模式
数据错误 时序问题 降低波特率
无法检测 连接问题 检查接线、片选信号
丢包 缓冲区溢出 增加延时或使用DMA

七、总结 #

SPI通信要点:

  1. 工作模式:理解CPOL和CPHA配置
  2. 全双工通信:同时发送和接收数据
  3. 多从机支持:通过片选信号控制
  4. 高速传输:适合大数据量传输场景
  5. 设备驱动:理解设备命令格式和时序要求

下一章我们将学习UART串口通信,实现设备间的异步数据传输。

最后更新:2026-03-27