【notes3】i2c,input,pwm,iio,dma
文章目录
1.协议

I2C读时序(i2cget):所有都是scl高时,sda才生效:一个主机可控制多个从机,使用7位地址,最多可使用128个唯一地址,如下发送数据是读出的值。
i2c没有像spi片选线,主机将从机地址发给每个从机比较(寻址),匹配则向主机发送低电平ACK,不匹配则SDA保持高。
如下A表示ACK,逻辑分析仪一个接SDA,一个接SCL,一个接地。
I2C写时序(i2cset):
1.1 i2ctools
SMBus是I2C的一个子集,在 I2C 上加了电压、时钟、超时、ACK、数据格式等限制,更适合系统管理场景:i2ctools提供i2c_smbus_read_block_data(这函数底层会加pec,前提是如下设备开启了pec,如果pec错了则该函数返回错误)。
// 手动计算一次 SMBus 读操作的 PEC 校验值。PEC 本质就是 CRC-8
typedef unsigned short u16;
typedef unsigned int u32;
typedef unsigned char u8;
#define POLY (0x1070U << 3) // CRC-8 的计算规则
static u8 crc8(u16 data) // 算一个字节
{
int i;
for (i = 0; i < 8; i++) { // CRC 是按 bit 算的。一个字节有 8 bit
if (data & 0x8000) // 判断当前最高位是不是 1
data = data ^ POLY;
data = data << 1; // 左移一位,处理下一个 bit
}
return (u8)(data >> 8); // 算完 8 个 bit 后,把高 8 位取出来,作为新的 CRC 值
}
static u8 i2c_smbus_pec(u8 crc, u8 *p, size_t count)
{
int i;
for (i = 0; i < count; i++) // 逐字节处理
crc = crc8((crc ^ p[i]) << 8);
return crc;
}
int main()
{
u8 pec;
char buf[12] = {0xcc}; // cc是8位即 高7位addr(0x66)+ 低1位写(0)
char buf0[12] = {0xcd}; // cd是8位即 高7位addr(0x66)+ 低1位读(1)
char buf1[12] = {0x88}; // 读的寄存器地址0x88
char buf2[12] = {0xc2,0xe0}; // 读出的数据
// S -> 0x66+W -> A -> 0x88 -> A -> Sr -> 0x66+R -> A -> 0xc2 -> A -> 0xe0 -> N -> PEC -> P
pec = i2c_smbus_pec(0, buf, 1);
pec = i2c_smbus_pec(pec, buf1, 1); // 这里不是buf0,因为要按照上面顺序
pec = i2c_smbus_pec(pec, buf0, 1);
pec = i2c_smbus_pec(pec, buf2, 2);
printf("test===0x%x\n",pec); // 正常解析前5个字节的crc8打印出0x3d
return 0;
// 主机想要的是0xc2 0xe0,但是由于干扰导致某个bit错了,主机采样成0xc2 0xf0,没有pec,主机就信了,有pec后,从设备最后发一个0x3d校验字节,主机收到数据后也自己算一次,如果不是0x3d,丢弃并retry。
}

--:地址被探测了,但没有芯片应答。 git://git.kernel.org/pub/scm/utils/i2c-tools/i2c-tools.git。UU:探测被跳过了(不能确定有没有芯片),这个地址目前正被驱动程序使用,驱动里配置了这个地址(0x14)。37:在0x37这个地址找到了芯片。
如下-f:force, -y:yes,i2cdetect -y -q 5,i2cdump。
i2ctransfer支持16位/32位寄存器的读写,w后面数字是设备/slave/器件即0x50后面的字节数:

2.模型

每次往client或driver链表里添加,都会与对方匹配调用match:
// i2c dev driver : i2c-dev.c :
open("/dev/i2c-0") 选择 I2C 控制器 (使用open函数,通过minor找到adapter,分配client,配置client的adapter,然后把client放入file中)
ioctl(I2C_SLAVE, address) 选择 I2C 从设备地址
ioctl(I2C_RDWR/I2C_SMBUS, ...) 真正传输数据 (根据I2C_RDWR或者I2C_SMBUS,来调用不同的i2c传输函数,发送和接受数据给i2c_adapter_driver)

2.1 I2C设备添加
用户空间注册
root@s430:/sys/bus/i2c/devices/i2c-89# echo 0x50 > delete_device
-sh: echo: write error: No such file or directory # delete_device删除设备只能删除在用户空间创建的i2c设备,dts里注册的不能删
root@bmc:/sys/bus/i2c/devices/i2c-0# echo 0xd > delete_device // 删除bus0上0x0d设备,里面0-000d文件夹被删除,这时i2cdetect -y看设备在不在
root@bmc:/sys/bus/i2c/devices/i2c-0# rmmod basecpld //驱动,这行lsmod查看不到,如果这行比上行先执行,0-000d文件夹还在,但里面节点没了
root@bmc:/sys/bus/i2c/devices/i2c-0# insmod ~/basecpld.ko // 驱动
root@bmc:/sys/bus/i2c/devices/i2c-0# echo basecpld 0xd > new_device // 设备,将ko放到对应路径(/lib/modules/4.1.51/extra/basecpld.ko),这行执行后,不用上行insmod

静态注册
1.没有dts:arm架构board-xxx-yyy.c(架构板级)文件或x86架构下xxx-yyy-init-zzz.c(初始化)文件。
static struct i2c_board_info my_tmp75_info = {
I2C_BOARD_INFO("my_tmp75", 0x48), // 设备名,设备地址
};
// info:i2c设备信息,就是上面的结构体
// n:info中有几个设备
// 如下函数在linux-aspeed/drivers/i2c/i2c-boardinfo.c
i2c_register_board_info(int busnum, struct i2c_board_info const * info, unsigned n)
{
devinfo->busnum = busnum; // busnum:哪一条总线,也就是选择哪一个i2c控制器(adapter)
devinfo->board_info = *info;
list_add_tail(&devinfo->list, &__i2c_board_list); /* 将上面定义的i2c设备信息添加进__i2c_board_list链表中 */
}
2.出现了dts之后将这些的板级信息全部都定义在设备树(arch/arm/boots/dts),linux在启动uboot的时会自动展开dts上的硬件信息,自动调用i2c_register_board_info函数将设备注册进设备链表中,i2c总线相对于芯片的cpu而言也是设备。
// 如上2种方式将硬件设备添加到设备链表之后,如下内核是怎么向i2c总线动态注册这些设备的:
// I2C controller driver probe
// v
// i2c_add_adapter(adap结构体)
// |-- 创建 /dev/i2c-0
// | 由 i2c-dev.c 监听 adapter 注册后创建
// |-- 解析设备树子节点
// tmp75@48 -> 创建真实 i2c_client 0-0048
3.i2c_new_device(不管i2c设备是否真的存在,都实例化i2c_client),i2c_new_probed_device(调用probe函数去探测i2c地址是否有ACK,存在则实例化i2c_client)。
static struct i2c_board_info sfe4001_hwmon_info = {
I2C_BOARD_INFO("max6647", 0x4e),
};
int sfe4001_init(struct efx_nic *efx)
{// 不需要知道busnum,网卡驱动初始化时,已经获得了自己的 I2C 控制器
efx->board_info.hwmon_client = i2c_new_device(&efx->i2c_adap, &sfe4001_hwmon_info);//使用i2c_adapter创建i2c_client
}
2.2 master driver

/ {
i2c-bus-virtual { // 创建一个platform device
compatible = "100ask,i2c-bus-virtual"; //使用字符串匹配i2c_adpter_drv
};
};
// i2c_adapter_drv.c:
static struct i2c_adapter *g_adapter;
static unsigned char eeprom_buffer[512]; // 模拟一个 512 字节 EEPROM
static int eeprom_cur_addr = 0; // 当前读写地址
static void eeprom_emulate_xfer(struct i2c_adapter *i2c_adap, struct i2c_msg *msg)
{
int i;
if(msg->flags & I2C_M_RD) { // 读
for(i=0;i<msg->len;i++) {
msg->buf[i] = eeprom_buffer[eeprom_cur_addr++];
if(eeprom_cur_addr == 512)
eeprom_cur_addr = 0;
}
} else {
// i2cset -f -y 4 0x50 0 0x55 : i2c-tools调用ioctl(fd,I2C_RDWR)进入i2c-dev.c进行下面赋值
// msg.addr = 0x50;
// msg.flags = 0; // 写
// msg.len = 2;
// msg.buf[0] = 0x00; // 寄存器地址
// msg.buf[1] = 0x55; // 要写的数据
if(msg->len >= 1) { // 如上进入
eeprom_cur_addr = msg->buf[0];
for(i=1;i<msg->len;i++) {
eeprom_buffer[eeprom_cur_addr++] = msg->buf[i]; // eeprom_buffer[0] = 0x55;
if(eeprom_cur_addr == 512)
eeprom_cur_addr = 0;
}
}
}
}
static int i2c_bus_virtual_xfer(struct i2c_adapter *i2c_adap, struct i2c_msg msgs[], int num) // i2cget/i2cset/i2cdetect 都会进去该函数
{
int i;
for(i=0;i<num;i++) {
if(msgs[i].addr == 0x50) {
// 所以 i2cdetect 扫描时,只有 0x50 有响应:
// 50: 50 -- -- -- -- ...其他地址失败,所以显示 --
eeprom_emulate_xfer(i2c_adap, &msgs[i]);
} else {
i = -EIO;
break;
}
}
return i;
}
// 如下:i2cdetect -F 4
// Functionalities implemented by /dev/i2c-4:
// I2C yes
// SMBus Quick Command yes
// SMBus Send Byte yes ....
static u32 i2c_bus_virtual_func(struct i2c_adapter *adap)
{
return (I2C_FUNC_I2C | I2C_FUNC_NOSTART | I2C_FUNC_SMBUS_EMUL |
I2C_FUNC_SMBUS_READ_BLOCK_DATA |
I2C_FUNC_SMBUS_BLOCK_PROC_CALL |
I2C_FUNC_PROTOCOL_MANGLING);
}
const struct i2c_algorithm i2c_bus_virtual_algo = {
.master_xfer = i2c_bus_virtual_xfer,
.functionality = i2c_bus_virtual_func,
};
static int i2c_bus_virtual_probe(struct platform_device *pdev)
{
g_adapter = kzalloc(sizeof(*g_adapter), GFP_KERNEL); // 创建i2c_adapter
g_adapter->owner = THIS_MODULE;
g_adapter->class = I2C_CLASS_HWMON | I2C_CLASS_SPD;
g_adapter->nr = -1; // 让内核动态分配 I2C bus 编号
snprintf(g_adapter->name, sizeof(g_adapter->name), "i2c-bus-virtual");
g_adapter->algo = &i2c_bus_virtual_algo;
i2c_add_adapter(g_adapter); // 注册i2c_adapter
return 0;
}
///////////////////////////////////////////////////////////////////////////////////
static int i2c_bus_virtual_remove(struct platform_device *dev)
{
i2c_del_adapter(g_adapter);
return 0;
}
static const struct of_device_id i2c_bus_virtual_ids[] = {
{ .compatible = "100ask,i2c-bus-virtual", },
{ /* end of list */ }
};
struct platform_driver i2c_bus_virtual_driver = {
.driver = {
.name = "i2c-bus-virtual",
.of_match_table = i2c_bus_virtual_ids,
},
.probe = i2c_bus_virtual_probe,
.remove = i2c_bus_virtual_remove,
};
static int i2c_bus_virtual_init(void)
{
int ret= platform_driver_register(&i2c_bus_virtual_driver); // 把i2c_bus_virtual_driver注册进platform总线
return ret;
}
static void i2c_bus_virtual_exit(void)
{
platform_driver_unregister(&i2c_bus_virtual_driver);
}
module_init(i2c_bus_virtual_init);
module_exit(i2c_bus_virtual_exit); ...
// 复制dtb文件到tftp目录
cp arch/arm/boot/dts/imx6ull-14x14-ebf.dtb ~/tftpboot/
// 复制ko文件到文件目录
// insmod i2c_adapter_drv.ko (先移进来)
// i2cdetect -y -a -l
i2c-4 i2c i2c-bus-virtual I2C adapter
i2c挂死如clock线一直被拉低,i2cdetect扫出来很慢,且没有设备,量波形发给厂商,一般换设备FW。一般954x的dev&driver probe后不需要手动切(i2ctransfer -f -y 6 w1@0x70 0x10),如果954x切不动,一般后面设备挂死。cpu要上电(dmesg | grep probe):有/sys/bus/i2c/devices/6710-0074,但是i2cdetect -y 6710没有0x74。
接了一个gpu卡,gpu升温快最长3秒,获取温度给风扇调速进程pid算法算风扇转速,如果温度获取慢,影响散热策略:接了一张raid卡(后面接了几十块硬盘),数据交互量非常大,i2c一个字节一个字节传输慢,要获取每个硬盘温度,如果把raid卡数据访问完,肯定超过3秒,再去访问gpu,gpu肯定超温了,常见解决方法给gpu单独开一个线程,和其他进程争抢访问i2c bus锁。
3.input子系统
cat /proc/bus/input/devices # 查看具体设备,包含上面event0...
I: Bus=0011 Vendor=0001 Product=0001 Version=ab41: 这一行显示了设备的总线类型、供应商 ID、产品 ID 和固件版本。在这个例子中,该设备的总线类型为 0011,供应商 ID 为 0001, 产品 ID 为 0001,固件版本为 ab41。
N: Name="AT Translated Set 2 keyboard": 这一行显示了设备的名称。在这个例子中,该设备的名称为"AT Translated Set 2 keyboard"。
P: Phys=isa0060/serio0/input0: 这一行显示了设备的物理位置。在这个例子中,该设备的物理位置是 isa0060/serio0/input0。
S: Sysfs=/devices/platform/i8042/serio0/input/input1: 这一行显示了设备在 sysfs 文件系统中的路径。在这个例子中,该设备的路径是/devices/platform/i8042/serio0/input/input1。
U: Uniq=: 这一行显示了设备的唯一标识符。在这个例子中,唯一标识符为空。
H: Handlers=sysrq kbd event1 leds: 这一行显示了设备的处理程序。它指示了处理设备输入事件的程序或模块。在这个例子中,设备有 sysrq、kbd、event1 和 leds 这些处理程序,其中 event1 就表示设备节点为/dev/input/event1。
B: PROP=0: 这一行显示了设备的属性。在这个例子中,设备的属性值为 0。
B: EV=120013: 这一行显示了设备支持的事件类型。在这个例子中,设备支持 EV_SYN、 EV_KEY、EV_MSC 和 EV_LED 这些事件类型。
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe: 这一行显示了设备支持的按键。每个按键对应一个位,1 表示按键被按下,0 表示按键未被按下。该行显示了按键的状态,以 16 进制表示。
B: MSC=10: 这一行显示了设备支持的杂项事件。在这个例子中,设备支持 MSC_SCAN事件。
B: LED=7: 这一行显示了设备支持的 LED 灯。在这个例子中,设备支持 3 个 LED 灯,使用一个 7 位的二进制数表示灯的状态。
struct input_dev {
unsigned long evbit : 事件分类 # define EV_KEY 0x01 键盘...
...keybit : 支持的键值 # define KEY_1 2 如短按是KEY_1 ,长按3s是KEY_2...在中断子系统中判断按键时间,再input子系统给input_dev赋值填充,最后调input_event完成事件发送
}

中断函数里要用如下结构体第一行读取电平,第二行上报按键。第三行获取设备资源,第四行申请中断。如果用全局变量,多个按键会覆盖。
按下按键进入到中断程序里,黄色是上报按键事件到用户空间。
最重要的probe函数中创建i_dev并填充,并赋给私有数据button_data:
如上从设备树button-gpios拿到gpio,获取中断号并注册中断。
如下是应用:循环读,监控事件发生。
input_report_key会填充struct input_event event:event.type(对应evbit)= EV_KEY; event.code(对应keybit)= BTN_0; event.value = 1;
read(hexdump /dev/input/eventX : 十六进制就是input_event结构体成员值)
▲
evdev(Handler有很多,类似dev dirver):evdev.c(类似spidev.c/i2c-dev.c): evdev_connect()创建/dev/input/event4
▲
Input Core (drivers/input/input.c) :当input_register_device(i_dev)时将input_dev加入input_device_list,然后遍历input_handler_list看谁可以处理它?根据EV_KEY找到上面evdev
▲
input_dev

4.pwm
tach:风扇给bmc。pwm:bmc给风扇。参考【bmc4】ltpi。
// 占空比(0.5ms)、周期(20ms)
// 0.5ms————0度;
// 1.0ms————45度;
// 1.5ms————90度;
// 2.0ms————135度;
// 2.5ms————180度;
/ {
hc_sg90 {
compatible = "hc-sg90";
pwms = <&pwm9(引用PWM controller lable) 0 20000000(周期20ms) 0(上面polarity)>;
status = "okay";
};
};
static struct pwm_device *pwm_test;
static int sg90_probe(struct platform_device *pdev)
{
struct device_node *node = pdev->dev.of_node; // 获取设备树节点hc_sg90{...}(因为已经compatible匹配到)
pwm_test = devm_of_pwm_get(&pdev->dev, node, NULL); // 从子节点中获取pwms
if (IS_ERR(pwm_test)){return -1;}
pwm_config(pwm_test, 1500000, 20000000); /* 配置PWM:duty 1.5ms,90度,周期:20000000ns = 20ms */
pwm_set_polarity(pwm_test, PWM_POLARITY_NORMAL); /* 设置输出极性:占空比duty为高电平 */
pwm_enable(pwm_test); /* 使能PWM输出 */
return 0;
}
static ssize_t sg90_write (struct file *filp, const char __user *buf, size_t size, loff_t *offset)
{
unsigned char data[1];
if(size != 1)
return 1;
int res = copy_from_user(data, buf, size);
pwm_config(pwm_test, data[0], 20000000); // data[0]同上
return 1;
} ...
4.1 sysfs接口操作pwm
cat /sys/kernel/debug/pwm,pwmchip1类似gpiochip1里有很多pin,
npwm是该组(控制器)支持的pin个数,period周期。

rockchip_pwm_probe()
▼
pwmchip_add()
▼
pwmchip_sysfs_export()
▼
device_create("pwmchip0")
▼
/sys/class/pwm/pwmchip0
┌─────────┴─────────┐
export unexport
│
echo 0 > export
▼
export_store()
▼
pwm_export_child()
▼
device_register("pwm0")
▼
/sys/class/pwm/pwmchip0/pwm0
┌──────┼─────────────┬────────────┐
│ │ │ │
period duty_cycle enable polarity ...
│
echo 20000000 > period
▼
period_store()
▼
pwm_apply_state()
▼
chip->ops->apply()
▼
rockchip_pwm_apply()
▼
writel()
▼
PWM硬件寄存器
▼
PWM输出发生变化

4.2 高精度定时器模拟软件pwm

leds {
compatible = "pwm-leds";
led-gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH>;
};
hrtimer 就像一个每 1ms 响一次的闹钟,闹钟响一次就执行一次回调函数;回调函数里根据计数值决定 GPIO 是拉高还是拉低。这样反复执行,就用普通 GPIO 模拟出了 PWM
用户写 /dev/pwm-gpio
▼
cdev_test_write()
▼
修改 sum_count(周期) / high_count(高电平)
▼
hrtimer 每 1ms 触发一次
▼
pwm_timer_func()
▼
根据 timer_count 判断拉高还是拉低 GPIO
▼
GPIO RK_PB7 输出近似 PWM 波形
5.iio
iio是专用于模数转换器(ADC)和数模转换器(DAC)的内核子系统.。
for i in /sys/class/hwmon/hwmon*/name; do echo -n "${i%/*}: "; cat "$i" 2>/dev/null || echo "N/A"; done;
for h in /sys/class/hwmon/hwmon15 /sys/class/hwmon/hwmon16; do
readlink -f "$h/device"
echo "=== $h: $(cat "$h/name") ==="
for f in "$h"/fan*_input; do
[ -f "$f" ] && echo "$(basename "$f")=$(cat "$f")"
done
done
Trigger (CONFIG_SENSORS_IIO_HWMON)
↓
ADC采样
↓
Buffer缓存
↓
应用程序读取Buffer
1.sysfs Trigger(软件触发)
echo 2 > /sys/bus/iio/devices/iio_sysfs_trigger/add_trigger
/sys/bus/iio/devices/trigger2/ 里有 name(sysfstrig2) 和 trigger_now:
echo sysfstrig2 > /sys/bus/iio/devices/iio:device0/trigger/current_trigger // 绑定,也可以指向下面irqtrig85
echo 1 > trigger_now // 软件模拟了一次中断触发一次采样
2.IRQ Trigger
如按键按下触发设备树中interrupts=<30 ...> gpio30触发中断采样
3.hrtimer Trigger(定时器触发)
openbmc不会去读../iio:device0/..,只认/sys/class/hwmon/..,通过dts iio-hwmon联系起来
6.dma

6.1 为dma client driver提供api
你的公司(Client Driver): 一家需要频繁运输货物的“SPI设备制造商”。
物流公司(DMA Controller): 瑞芯微物流(RK3568 DMA)。
运输车队(DMA Channel): 物流公司旗下的一个车队。
货物(Data): 需要传输的数据。
Client Driver(SPI/I2S/UART...)
│ dma_request_chan()
▼
DMAEngine Framework
│ 找到对应 DMA Controller Driver
▼
DMA Controller Driver
│ 配置 DMA Controller
▼
DMA硬件
# include <linux/dmaengine.h>

6.2 uart dma

// 在 RK3568 这类 SoC 上,让 UART 不再完全靠 CPU 中断搬数据,而是借助 DMA 自动搬运 UART TX/RX 数据 :以UART1为例
dmac0: dma-controller@fe530000 {
compatible = "rockchip,rk3568-dmac", "snps,dw-ahb-dmac"; .....
}
&uart1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart1m0_xfer>; // 选择正确的引脚复用
// --- DMA 配置开始 ---
dmas = <&dmac0 2>, <&dmac0 3>; // 2 对应 TX 请求线,3 对应 RX 请求线 , 参考上图
dma-names = "tx", "rx";
// --- DMA 配置结束 ---
// 示例:连接一个蓝牙设备
bluetooth {
compatible = "brcm,bcm43438-bt";
max-speed = <1500000>; // 高速蓝牙需要高波特率,DMA 尤其重要
// ... 其他蓝牙配置
};
};
用户程序 read()/write()
↓
TTY 子系统
↓
serial_core.c // 通用串口核心
↓
8250_core.c // 8250串口框架与端口注册
↓
8250 端口操作
├── 8250_dma.c // DMA收发
└── PIO/中断收发
↑
8250_dw.c // UART控制器, 步骤2制定工作手册
↓
UART + DMA 硬件
// 8250_dw.c :
static int dw8250_probe(struct platform_device *pdev)
{
// ... 标准 UART 初始化
// 设置 DMA
if (has_dma) {
dw8250_dma_setup(dev, uart);
}
// ... 注册 UART 端口
}
// dw8250_dma_setup中 :从设备树获取 DMA 通道
dma->txchan = dma_request_slave_channel(dev, "tx"); // 步骤1
dma->rxchan = dma_request_slave_channel(dev, "rx");
TX发送流程:
1. 用户调用 write() 写入数据
2. 驱动将数据拷贝到 DMA 缓冲区
3. 启动 DMA 传输:
dmaengine_prep_slave_single // 步骤3
desc->callback = dma->tx_rx; // 传输完成回调
dmaengine_submit(desc); // 步骤4
dma_async_issue_pending(dma->txchan); // 步骤5
4. DMA控制器自动将数据从内存传输到UART TX FIFO
5. 传输完成后产生中断,调用dw8250_dma_tx_complete()清理资源
RX 接收流程:
6. 驱动预先分配 RX DMA 缓冲区并启动接收:
dmaengine_prep_slave_single
desc->callback = dma->rx_rx; // 接收完成回调
dmaengine_submit(desc);
dma_async_issue_pending(dma->rxchan);
7. 当 UART 接收到数据时,自动通过 DMA 传输到内存缓冲区
8. 达到预设的触发级别或超时后产生中断
9. 调用 dw8250_dma_rx_complete() 处理接收到的数据
// 查看串口是否成功获取了 DMA 通道 , CONFIG_DEBUG_FS
cat /sys/kernel/debug/dmaengine/summary
// 或查看具体串口设备
ls /sys/class/tty/ttyS1/device/dma/
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)