1

我正在尝试为 Gumstix Overo Fire 的 Angstrom Linux 2.6.36 编写一个 SPI 驱动程序。我的驱动程序在中断处理程序中不断崩溃。这是完整的代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/ioctl.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/errno.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/smp_lock.h>
#include <linux/cdev.h>
#include <linux/spi/spi.h>
#include <linux/string.h>
#include <asm/uaccess.h>
#include <linux/kernel.h>
#include <mach/gpio.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/hrtimer.h>

#define IRQ_PIN 10

#define SPI_BUFF_SIZE   4
#define USER_BUFF_SIZE  128

#define SPI_BUS 1
#define SPI_BUS_CS1 1
#define SPI_BUS_SPEED 1500000

unsigned char *buff_even = 0;
unsigned char *buff_odd = 0;
unsigned char *temp_buff = 0;
unsigned int sample_counter = 0;  
unsigned int buff_counter = 0;  
unsigned int current_buffer = 0;  
unsigned int local_current_buffer = 0;  
unsigned int local_sample_counter = 0;
unsigned int num_reads = 0;
unsigned int num_miss_samples = 0;
unsigned int regval = 0;

#define LIMIT (4000)
#define BUFF_SIZE (4*LIMIT)

#define MAJOR_NUM 100
#define READ_CURR_COUNTER _IOWR(MAJOR_NUM, 1, int)
#define READ_BUFF     _IOWR(MAJOR_NUM, 2, int)
#define READ_CURR_BUFF_NO _IOWR(MAJOR_NUM, 3, int)
#define READ_REGISTER _IOWR(MAJOR_NUM, 4, unsigned char)
#define WRITE_REGISTER _IOWR(MAJOR_NUM, 5, int)
#define START_READ _IOWR(MAJOR_NUM, 6, int)
#define STOP_READ _IOWR(MAJOR_NUM, 7, int)

const char this_driver_name[] = "adc";

static int running = 0;
static int resetting = 0;
static int reading = 0;

struct spike_control
{
    struct spi_message msg;
    struct spi_transfer transfer;
    u8 *tx_buff; 
    u8 *rx_buff;
};

static struct spike_control spike_ctl;

struct spike_dev
{
    struct semaphore spi_sem;
    struct semaphore fop_sem;
    dev_t devt;
    struct cdev cdev;
    struct class *class;
    struct spi_device *spi_device;
    char *user_buff;
    u8 test_data;   
    int irq;    
};

static struct spike_dev spike_dev;

static DEFINE_MUTEX(list_lock);
static DEFINE_MUTEX(count_lock);

static int status;

static void spike_completion_handler(void *arg)
{
    local_sample_counter++;

    if (sample_counter >= local_sample_counter + 1)
        num_miss_samples++;

    if (current_buffer == 0)
    {
        buff_even[buff_counter++] = spike_ctl.rx_buff[0];
        buff_even[buff_counter++] = spike_ctl.rx_buff[1];
        buff_even[buff_counter++] = spike_ctl.rx_buff[2];
        buff_even[buff_counter++] = spike_ctl.rx_buff[3];
    }
    else if (current_buffer == 1)
    {
        buff_odd[buff_counter++] = spike_ctl.rx_buff[0];
        buff_odd[buff_counter++] = spike_ctl.rx_buff[1];
        buff_odd[buff_counter++] = spike_ctl.rx_buff[2];
        buff_odd[buff_counter++] = spike_ctl.rx_buff[3];
    }

    memset(spike_ctl.rx_buff, 0, SPI_BUFF_SIZE);

    if (sample_counter == LIMIT)
    {
        buff_counter = 0;
        mutex_lock(&count_lock);
        if (current_buffer == 0)
            current_buffer = 1;
        else
            current_buffer = 0;
        sample_counter = 0; 
        mutex_unlock(&count_lock);
        local_sample_counter = 0;
    }

}

static irqreturn_t adc_handler(int irq, void *dev_id)
{
    sample_counter++;   

    spi_message_init(&spike_ctl.msg);   

    spike_ctl.msg.complete = spike_completion_handler;
    spike_ctl.msg.context = NULL;

    spike_ctl.transfer.tx_buf = NULL;
    spike_ctl.transfer.rx_buf = spike_ctl.rx_buff;
    spike_ctl.transfer.len = 4;

    spi_message_add_tail(&spike_ctl.transfer, &spike_ctl.msg);
    status = spi_async(spike_dev.spi_device, &spike_ctl.msg);

    return IRQ_HANDLED;
}

static void resetbuffers(void)
{
    local_sample_counter = 0;
    sample_counter = 0;
    num_miss_samples = 0;
    current_buffer = 0;
    buff_counter = 0;   
    memset(spike_ctl.rx_buff, 0, SPI_BUFF_SIZE);
    memset(spike_ctl.tx_buff, 0, SPI_BUFF_SIZE);
    memset(buff_even, 0, BUFF_SIZE);
    memset(buff_odd, 0, BUFF_SIZE);
    memset(temp_buff, 0, BUFF_SIZE);
}

static int read_register(unsigned char addr)
{
    if (down_interruptible(&spike_dev.spi_sem))
        return -ERESTARTSYS;

    if (!spike_dev.spi_device)
    {
        up(&spike_dev.spi_sem);
        return -ENODEV;
    }

    memset(spike_ctl.rx_buff, 0, SPI_BUFF_SIZE);
    memset(spike_ctl.tx_buff, 0, SPI_BUFF_SIZE);

    spike_ctl.transfer.tx_buf = spike_ctl.tx_buff;
    spike_ctl.transfer.rx_buf = spike_ctl.rx_buff;
    spike_ctl.transfer.len = 1;

    spike_ctl.tx_buff[0] = 0x11;//Stop read data continuous
    spi_message_init(&spike_ctl.msg);   
    spi_message_add_tail(&spike_ctl.transfer, &spike_ctl.msg);
    status = spi_sync(spike_dev.spi_device, &spike_ctl.msg);
    __asm__ __volatile__("ldr r0,=0x2710\n\t"
                         ".LOOP1:\n\t"
                         "subs r0,r0,#1\n\t"
                         "bne .LOOP1\n\t");

    spike_ctl.tx_buff[0] = 0x20 + addr;//Address of register
    spi_message_init(&spike_ctl.msg);
    spi_message_add_tail(&spike_ctl.transfer, &spike_ctl.msg);
    status = spi_sync(spike_dev.spi_device, &spike_ctl.msg);
    __asm__ __volatile__("ldr r0,=0x2710\n\t"
                         ".LOOP2:\n\t"
                         "subs r0,r0,#1\n\t"
                         "bne .LOOP2\n\t");

    spike_ctl.tx_buff[0] = 0;//Number of registers to read minus 1
    spi_message_init(&spike_ctl.msg);
    spi_message_add_tail(&spike_ctl.transfer, &spike_ctl.msg);
    status = spi_sync(spike_dev.spi_device, &spike_ctl.msg);
    __asm__ __volatile__("ldr r0,=0x2710\n\t"
                         ".LOOP3:\n\t"
                         "subs r0,r0,#1\n\t"
                         "bne .LOOP3\n\t");

    spi_message_init(&spike_ctl.msg);
    spi_message_add_tail(&spike_ctl.transfer, &spike_ctl.msg);
    status = spi_sync(spike_dev.spi_device, &spike_ctl.msg);
    __asm__ __volatile__("ldr r0,=0x2710\n\t"
                         ".LOOP4:\n\t"
                         "subs r0,r0,#1\n\t"
                         "bne .LOOP4\n\t");

    printk(KERN_ALERT "%x\n",spike_ctl.rx_buff[0]);
    regval = spike_ctl.rx_buff[0];

//  spike_ctl.tx_buff[0] = 0x10;//Start read data continuous
//  spi_message_init(&spike_ctl.msg);   
//  spi_message_add_tail(&spike_ctl.transfer, &spike_ctl.msg);
//  status = spi_sync(spike_dev.spi_device, &spike_ctl.msg);

    up(&spike_dev.spi_sem);

    return status;
}

static int write_register(unsigned char addr, unsigned char val)
{
    if (down_interruptible(&spike_dev.spi_sem))
        return -ERESTARTSYS;

    if (!spike_dev.spi_device)
    {
        up(&spike_dev.spi_sem);
        return -ENODEV;
    }

    memset(spike_ctl.rx_buff, 0, SPI_BUFF_SIZE);
    memset(spike_ctl.tx_buff, 0, SPI_BUFF_SIZE);

    spike_ctl.transfer.tx_buf = spike_ctl.tx_buff;
    spike_ctl.transfer.rx_buf = spike_ctl.rx_buff;
    spike_ctl.transfer.len = 1;

    spike_ctl.tx_buff[0] = 0x11;//Stop read data continuous
    spi_message_init(&spike_ctl.msg);   
    spi_message_add_tail(&spike_ctl.transfer, &spike_ctl.msg);
    status = spi_sync(spike_dev.spi_device, &spike_ctl.msg);
    __asm__ __volatile__("ldr r0,=0x2710\n\t"
                         ".LOOP5:\n\t"
                         "subs r0,r0,#1\n\t"
                         "bne .LOOP5\n\t");

    spike_ctl.tx_buff[0] = 0x40 + addr;//Address of register
    spi_message_init(&spike_ctl.msg);
    spi_message_add_tail(&spike_ctl.transfer, &spike_ctl.msg);
    status = spi_sync(spike_dev.spi_device, &spike_ctl.msg);
    __asm__ __volatile__("ldr r0,=0x2710\n\t"
                         ".LOOP6:\n\t"
                         "subs r0,r0,#1\n\t"
                         "bne .LOOP6\n\t");

    spike_ctl.tx_buff[0] = 0;//Number of registers to read minus 1
    spi_message_init(&spike_ctl.msg);
    spi_message_add_tail(&spike_ctl.transfer, &spike_ctl.msg);
    status = spi_sync(spike_dev.spi_device, &spike_ctl.msg);
    __asm__ __volatile__("ldr r0,=0x2710\n\t"
                         ".LOOP7:\n\t"
                         "subs r0,r0,#1\n\t"
                         "bne .LOOP7\n\t");

    spike_ctl.tx_buff[0] = val;//Value to write
    spi_message_init(&spike_ctl.msg);
    spi_message_add_tail(&spike_ctl.transfer, &spike_ctl.msg);
    status = spi_sync(spike_dev.spi_device, &spike_ctl.msg);
    __asm__ __volatile__("ldr r0,=0x2710\n\t"
                         ".LOOP8:\n\t"
                         "subs r0,r0,#1\n\t"
                         "bne .LOOP8\n\t");

    spike_ctl.tx_buff[0] = 0x10;//Start read data continuous
    spi_message_init(&spike_ctl.msg);   
    spi_message_add_tail(&spike_ctl.transfer, &spike_ctl.msg);
    status = spi_sync(spike_dev.spi_device, &spike_ctl.msg);

//  spike_ctl.tx_buff[0] = 0x10;//Start read data continuous
//  spi_message_init(&spike_ctl.msg);   
//  spi_message_add_tail(&spike_ctl.transfer, &spike_ctl.msg);
//  status = spi_sync(spike_dev.spi_device, &spike_ctl.msg);

    up(&spike_dev.spi_sem);

    return status;
}

static long adc_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    unsigned char addr;
    switch(cmd) 
    {
    case READ_CURR_COUNTER: 
        mutex_lock(&count_lock);
        local_current_buffer = current_buffer;  
        local_sample_counter = sample_counter;
        mutex_unlock(&count_lock);
        copy_to_user((void *)arg, &local_sample_counter, sizeof(unsigned int));
        printk(KERN_ALERT "Read current counter %d\n",local_sample_counter);
        break;
    case READ_BUFF: 
        mutex_lock(&count_lock);
        local_current_buffer = current_buffer;  
        local_sample_counter = sample_counter;
        mutex_unlock(&count_lock);
        if (local_current_buffer == 0)
        {
            memcpy(temp_buff,buff_odd, BUFF_SIZE);
//          memset(temp_buff, 'Q', BUFF_SIZE);          
        }
        else
        {
            memcpy(temp_buff,buff_even, BUFF_SIZE);
//          memset(temp_buff, 'T', BUFF_SIZE);          
        }
        copy_to_user((void *)arg, temp_buff, BUFF_SIZE);
        num_reads++;
        break;
    case READ_CURR_BUFF_NO: 
        mutex_lock(&count_lock);
        local_current_buffer = current_buffer;  
        local_sample_counter = sample_counter;
        mutex_unlock(&count_lock);
        copy_to_user((void *)arg, &local_current_buffer, sizeof(int));
        break;
    case READ_REGISTER: 
        get_user(addr, (unsigned char *)arg);
        disable_irq(spike_dev.irq);
        read_register(addr);
        enable_irq(spike_dev.irq);
        copy_to_user((void *)arg, &regval, sizeof(int));
        break;
    case WRITE_REGISTER:    
        disable_irq(spike_dev.irq);

        enable_irq(spike_dev.irq);
        //copy_to_user((void *)arg, &local_current_buffer, sizeof(int));
        break;

    default:
        return -ENOTTY;
    }
    return 1;

}

static int spike_open(struct inode *inode, struct file *filp)
{   
    int status = 0;

    if (down_interruptible(&spike_dev.fop_sem)) 
        return -ERESTARTSYS;

    if (!spike_dev.user_buff) {
        spike_dev.user_buff = kmalloc(USER_BUFF_SIZE, GFP_KERNEL);
        if (!spike_dev.user_buff) 
            status = -ENOMEM;
    }   

    up(&spike_dev.fop_sem);

    return status;
}

static int spike_probe(struct spi_device *spi_device)
{
    if (down_interruptible(&spike_dev.spi_sem))
        return -EBUSY;

    spike_dev.spi_device = spi_device;

    printk(KERN_ALERT "SPI[%d] max_speed_hz %d Hz\n", spi_device->chip_select, spi_device->max_speed_hz);

    up(&spike_dev.spi_sem);

    return 0;
}

static int spike_remove(struct spi_device *spi_device)
{
    if (down_interruptible(&spike_dev.spi_sem))
        return -EBUSY;

    spike_dev.spi_device = NULL;

    up(&spike_dev.spi_sem);

    return 0;
}

static int __init add_spike_device_to_bus(void)
{
    struct spi_master *spi_master;
    struct spi_device *spi_device;
    struct device *pdev;
    char buff[64];
    int status = 0;

    spi_master = spi_busnum_to_master(SPI_BUS);
    if (!spi_master)
    {
        printk(KERN_ALERT "spi_busnum_to_master(%d) returned NULL\n",
            SPI_BUS);
        printk(KERN_ALERT "Missing modprobe omap2_mcspi?\n");
        return -1;
    }

    spi_device = spi_alloc_device(spi_master);
    if (!spi_device)
    {
        put_device(&spi_master->dev);
        printk(KERN_ALERT "spi_alloc_device() failed\n");
        return -1;
    }

    spi_device->chip_select = SPI_BUS_CS1;

    /* Check whether this SPI bus.cs is already claimed */
    snprintf(buff, sizeof(buff), "%s.%u", 
            dev_name(&spi_device->master->dev),
            spi_device->chip_select);

    pdev = bus_find_device_by_name(spi_device->dev.bus, NULL, buff);
    if (pdev)
    {
        /* We are not going to use this spi_device, so free it */ 
        spi_dev_put(spi_device);

        /* 
         * There is already a device configured for this bus.cs  
         * It is okay if it us, otherwise complain and fail.
         */
        if (pdev->driver && pdev->driver->name && strcmp(this_driver_name, pdev->driver->name))
        {
            printk(KERN_ALERT 
                "Driver [%s] already registered for %s\n",
                pdev->driver->name, buff);
            status = -1;
        } 
    }
    else
    {
        spi_device->max_speed_hz = SPI_BUS_SPEED;
        spi_device->mode = SPI_MODE_0;
        spi_device->bits_per_word = 8;
        spi_device->irq = -1;
        spi_device->controller_state = NULL;
        spi_device->controller_data = NULL;
        strlcpy(spi_device->modalias, this_driver_name, SPI_NAME_SIZE);

        status = spi_add_device(spi_device);        
        if (status < 0)
        {   
            spi_dev_put(spi_device);
            printk(KERN_ALERT "spi_add_device() failed: %d\n", 
                status);        
        }               
    }

    put_device(&spi_master->dev);

    return status;
}

static struct spi_driver spike_driver = {
    .driver = {
        .name = this_driver_name,
        .owner = THIS_MODULE,
    },
    .probe = spike_probe,
    .remove = __devexit_p(spike_remove),    
};

static int __init spike_init_spi(void)
{
    int error;

    spike_ctl.tx_buff = kmalloc(SPI_BUFF_SIZE, GFP_KERNEL | GFP_DMA);
    if (!spike_ctl.tx_buff)
    {
        error = -ENOMEM;
        goto spike_init_error;
    }

    spike_ctl.rx_buff = kmalloc(SPI_BUFF_SIZE, GFP_KERNEL | GFP_DMA);
    if (!spike_ctl.rx_buff)
    {
        error = -ENOMEM;
        goto spike_init_error;
    }

    error = spi_register_driver(&spike_driver);
    if (error < 0)
    {
        printk(KERN_ALERT "spi_register_driver() failed %d\n", error);
        goto spike_init_error;
    }

    error = add_spike_device_to_bus();
    if (error < 0)
    {
        printk(KERN_ALERT "add_spike_to_bus() failed\n");
        spi_unregister_driver(&spike_driver);
        goto spike_init_error;  
    }

    spike_dev.irq = OMAP_GPIO_IRQ(IRQ_PIN);

    return 0;

spike_init_error:

    if (spike_ctl.tx_buff) {
        kfree(spike_ctl.tx_buff);
        spike_ctl.tx_buff = 0;
    }

    if (spike_ctl.rx_buff) {
        kfree(spike_ctl.rx_buff);
        spike_ctl.rx_buff = 0;
    }

    return error;
}

static ssize_t spike_read(struct file *filp, char __user *buff, size_t count, loff_t *offp)
{
    size_t len;
    ssize_t status = 0;

    if (!buff) 
        return -EFAULT;

    if (*offp > 0) 
        return 0;

    if (down_interruptible(&spike_dev.fop_sem)) 
        return -ERESTARTSYS;

    printk(KERN_ALERT "Interrupt triggered %d Missed packet %d\n", sample_counter, num_miss_samples);

    up(&spike_dev.fop_sem);

    return status;  
}

static ssize_t spike_write(struct file *filp, const char __user *buff, size_t count, loff_t *f_pos)
{
    size_t len; 
    ssize_t status = 0;

    if (down_interruptible(&spike_dev.fop_sem))
        return -ERESTARTSYS;

    memset(spike_dev.user_buff, 0, 16);
    len = count > 8 ? 8 : count;

    if (copy_from_user(spike_dev.user_buff, buff, len))
    {
        status = -EFAULT;
        goto spike_write_done;
    }

    /* we'll act as if we looked at all the data */
    status = count;

    /* but we only care about the first 5 characters */
    if (!strnicmp(spike_dev.user_buff, "inc", 3))
    {
        disable_irq(spike_dev.irq);
        write_register(1,0x62);
        resetbuffers();
        printk(KERN_ALERT "4000 samples per second\n");
        enable_irq(spike_dev.irq);
    } 

    if (!strnicmp(spike_dev.user_buff, "dec", 3))
    {
        disable_irq(spike_dev.irq);
        write_register(1,0x52);
        resetbuffers();
        printk(KERN_ALERT "1000 samples per second\n");
        enable_irq(spike_dev.irq);
    } 

    if (!strnicmp(spike_dev.user_buff, "stop", 4))
    {
        disable_irq(spike_dev.irq);
        resetbuffers();
        printk(KERN_ALERT "Driver stopped\n");
    } 

spike_write_done:

    up(&spike_dev.fop_sem);

    return status;
}

static const struct file_operations spike_fops = {
    .owner          = THIS_MODULE,
    .open           = spike_open,
    .read           = spike_read,
    .write          = spike_write,
    .unlocked_ioctl = adc_ioctl,    
};

static int __init spike_init_cdev(void)
{
    int error;

    spike_dev.devt = MKDEV(0, 0);

    error = alloc_chrdev_region(&spike_dev.devt, 0, 1, this_driver_name);
    if (error < 0)
    {
        printk(KERN_ALERT "alloc_chrdev_region() failed: %d \n", 
            error);
        return -1;
    }

    cdev_init(&spike_dev.cdev, &spike_fops);
    spike_dev.cdev.owner = THIS_MODULE;

    error = cdev_add(&spike_dev.cdev, spike_dev.devt, 1);
    if (error)
    {
        printk(KERN_ALERT "cdev_add() failed: %d\n", error);
        unregister_chrdev_region(spike_dev.devt, 1);
        return -1;
    }   

    return 0;
}

static int __init spike_init_class(void)
{
    spike_dev.class = class_create(THIS_MODULE, this_driver_name);

    if (!spike_dev.class)
    {
        printk(KERN_ALERT "class_create() failed\n");
        return -1;
    }

    if (!device_create(spike_dev.class, NULL, spike_dev.devt, NULL, this_driver_name))
    {
        printk(KERN_ALERT "device_create(..., %s) failed\n",
            this_driver_name);
        class_destroy(spike_dev.class);
        return -1;
    }

    return 0;
}

static int __init spike_init(void)
{

    int result;
    memset(&spike_dev, 0, sizeof(spike_dev));
    memset(&spike_ctl, 0, sizeof(spike_ctl));

    sema_init(&spike_dev.spi_sem, 1);
    sema_init(&spike_dev.fop_sem, 1);

    buff_even = kmalloc(BUFF_SIZE, GFP_KERNEL);
    buff_odd  = kmalloc(BUFF_SIZE, GFP_KERNEL);
    temp_buff = kmalloc(BUFF_SIZE, GFP_KERNEL);

    if ( buff_even == 0 )
        printk(KERN_ALERT "Failed to allocate buffer even\n");

    if ( buff_odd == 0 )
        printk(KERN_ALERT "Failed to allocate buffer odd\n");

    if ( temp_buff == 0 )
        printk(KERN_ALERT "Failed to temp buffer\n");

    if (spike_init_cdev() < 0) 
        goto fail_1;

    if (spike_init_class() < 0)  
        goto fail_2;

    if (spike_init_spi() < 0) 
        goto fail_3;

    result = request_irq(spike_dev.irq, adc_handler, IRQF_TRIGGER_RISING, "adc", &spike_dev);

    if (result < 0)
    {
                printk(KERN_ALERT "request_irq failed: %d\n", result);
                return -1;
    }

    return 0;

fail_3:
    device_destroy(spike_dev.class, spike_dev.devt);
    class_destroy(spike_dev.class);

fail_2:
    cdev_del(&spike_dev.cdev);
    unregister_chrdev_region(spike_dev.devt, 1);

fail_1:
    return -1;
}
module_init(spike_init);

static void __exit spike_exit(void)
{

    disable_irq(spike_dev.irq);
    free_irq(spike_dev.irq, &spike_dev);
    gpio_free(IRQ_PIN);

    spi_unregister_device(spike_dev.spi_device);
    spi_unregister_driver(&spike_driver);

    device_destroy(spike_dev.class, spike_dev.devt);
    class_destroy(spike_dev.class);

    cdev_del(&spike_dev.cdev);
    unregister_chrdev_region(spike_dev.devt, 1);

    if (spike_ctl.tx_buff)
        kfree(spike_ctl.tx_buff);

    if (spike_ctl.rx_buff)
        kfree(spike_ctl.rx_buff);

    if (spike_dev.user_buff)
        kfree(spike_dev.user_buff);

    if ( buff_even != 0 )
        kfree(buff_even);

    if ( buff_odd != 0 )
        kfree(buff_odd);

    if ( temp_buff != 0 )
        kfree(temp_buff);

    printk(KERN_ALERT "Interrupt triggered %d Missed packet %d\n", sample_counter, num_miss_samples);

}
module_exit(spike_exit);

中断处理程序崩溃在线

status = spi_async(spike_dev.spi_device, &spike_ctl.msg);

如果我注释掉这一行,一切运行正常,但当然没有从 SPI 读取数据。

4

1 回答 1

1

穆罕默德,也许你到时候解决了这个问题。

但是,我看到的是您在 SPI 完成处理程序 (spike_completion_handler) 中使用互斥锁,它在无法休眠的上下文中执行。因此,我建议改用自旋锁。这是安全的,因为 count_lock 总是保护不休眠的代码。

这可能不是您的问题的原因(没有阅读整个代码),但最好还是这样做。

于 2012-11-20T08:46:51.950 回答