sous-système Linux SPI

1. Vue d'ensemble

SPI est une abréviation de "Serial Peripheral Interface", et est une interface de communication série synchrone est un quatre fils, utilisé pour connecter des microcontrôleurs, des capteurs, des dispositifs de mémoire, le dispositif SPI est divisé en un dispositif maître et un dispositif esclave sont deux, pour la communication Les quatre lignes de contrôle et de contrôle sont:

  • Signal de sélection de puce CS
  • Signal d'horloge SCK
  • Entrée de données de l'appareil maître MISO , broche de sortie de données de l'appareil esclave
  • Sortie de données MOSI de l'appareil principal, à partir de la broche d'entrée de données de l'appareil
    car dans la plupart des cas, le côté CPU ou SOC fonctionne généralement en mode maître, par conséquent, les versions actuelles du noyau Linux, le moteur d'entraînement n'est que le cadre du mode maître .

Cet article est basé sur le noyau linux-3.0.35 et le matériel imx6q

2. Connexion matérielle

Écrivez la description de l'image ici

imx6q prend en charge quatre périphériques esclaves SPI externes Le SPI0 d'ECSPI prend en charge le mode salve, qui n'est pas implémenté par le noyau, donc si vous voulez utiliser ce mode, vous devez écrire le pilote vous-même.

Le périphérique esclave utilisé ici est sc16is752, une puce de spi à uart.

3. Séquence de travail

Selon la relation de phase entre le signal d'horloge et le signal de données, SPI dispose de 4 modes de synchronisation de travail:

Écrivez la description de l'image ici

Nous utilisons CPOL pour représenter l'état du niveau initial du signal d'horloge, un CPOL de 0 indique que l'état initial du signal d'horloge est bas et une valeur de 1 indique que le niveau initial du signal d'horloge est haut. De plus, nous utilisons l'ACSP pour indiquer que les données sont échantillonnées à ce front d'horloge. Un CPHA de 0 signifie que les données sont échantillonnées sur le premier front d'horloge, et un CPHA de 1 signifie que les données sont échantillonnées sur le second front d'horloge. . Utilisez la combinaison CPOL et CPHA pour représenter le mode de travail actuel requis par SPI:

  • CPOL = 0, CPHA = 1 Mode 0
  • CPOL = 0, CPHA = 1 Mode 1
  • CPOL = 1, CPHA = 0 Mode 2
  • CPOL = 1, CPHA = 1 Mode 3

sc16is752 prend uniquement en charge le mode 0

4. Synchronisation de l'opération sc16is752

Écrivez la description de l'image ici

5. Fichiers liés au noyau

Ici, nous utilisons un pilote spidev fourni avec le noyau Linux, et tous les fichiers impliqués sont les suivants:

conduire:

./drivers/spi/spidev.c
./drivers/spi/spidev.h

Sous-système:

./drivers/spi/spi.c
./include/linux/spi/spi.h

couche du milieu:

./drivers/spi/spi_bitbang.c
./drivers/spi/spi_bitbang.h

Bas:

./drivers/spi/spi_imx.c


Enregistrement des appareils de la plate-forme:

./arch/arm/plat-mxc/devices/platform-spi_imx.c

Fichiers au niveau de la carte:

./arch/arm/mach-mx6/board-mx6q_sabreauto.h
./arch/arm/mach-mx6/board-mx6q_sabreauto.c

5. Modification du noyau

Le sous-système SPI est basé sur la structure de périphérique de la plate - forme , de sorte que les noms du périphérique et du pilote doivent être cohérents pour correspondre avec succès.

Étape 1: Configurer le pad

La configuration d'imx6 est assez compliquée, une broche peut être configurée dans différentes fonctions via mux

static iomux_v3_cfg_t mx6q_sabreauto_pads[] = {
              .
              .
              .
        //ECSPI2
        MX6Q_PAD_DISP0_DAT19__ECSPI2_SCLK,
        MX6Q_PAD_DISP0_DAT17__ECSPI2_MISO,
        MX6Q_PAD_DISP0_DAT16__ECSPI2_MOSI,
        MX6Q_PAD_DISP0_DAT18__ECSPI2_SS0,
        MX6Q_PAD_DISP0_DAT22__GPIO_5_16, //这个引脚可以做为SC16IS752的中断输入(本文不用)
                .
                .
                .
                .

 };

Étape 2: définir les informations de la plate-forme

static struct spi_board_info imx6_sabresd_spi_uart[] __initdata = {
    {
        .modalias = "sc16is752", //这个别名非常重要,必须要和spidev.c驱动里的名字保持一致
        .max_speed_hz = 1000000,  //最大的速度
        .bus_num = 1,            //这里我们用的ECSPI2,下标要注意
        .chip_select = 0,        //片选,是低电平有效
        .mode = SPI_MODE_0,      //使用的SPI模式0
//      .irq = gpio_to_irq(SPI_UART_IRQ),
    },
};

Étape 3: initialiser l'équipement de la plate-forme

static void spi_device_init(void)
{
    spi_register_board_info(imx6_sabresd_spi_uart, ARRAY_SIZE(imx6_sabresd_spi_uart));
}

static void __init mx6_board_init(void)
{

   。。。

       /* SPI */
     imx6q_add_ecspi(0, &mx6q_sabreauto_spi1_data);
     imx6q_add_ecspi(1, &mx6q_sabreauto_spi2_data);
     imx6q_add_ecspi(2, &mx6q_sabreauto_spi3_data);
     spi_device_init();

。。。。
}

Étape 4: Conduire les modifications

static struct spi_driver spidev_spi_driver = {
    .driver = {
        //.name =       "spidev",  
        .name =     "sc16is752", // 这里改为sc16is752,当然改spi_board_info 里的值也是一样的,只要一致
        .owner =    THIS_MODULE,
    },
    .probe =    spidev_probe,
    .remove =   __devexit_p(spidev_remove),

    /* NOTE:  suspend/resume methods are not necessary here.
     * We don't do anything except pass the requests to/from
     * the underlying controller.  The refrigerator handles
     * most issues; the controller driver handles the rest.
     */
};

Étape 5: test

Le code de test du pilote spidev est fourni dans le noyau, situé à

spidev_test.c dans le répertoire ./documentation/spi

Vous pouvez d'abord court-circuiter le MISO et le MOSI avec une pince à épiler.Si vous constatez que les données envoyées et reçues sont les mêmes, le pilote fonctionne normalement. Le reste est la configuration de SC16IS752.

6. Analyse de la structure des données clés

Si vous voulez comprendre l'analyse du sous-système, vous devez commencer par les structures suivantes:

6.1 、 spi_transfer et spi_message

La structure spi_transfer est un transfert spi, plusieurs spi_transfers forment un spi_message

struct spi_transfer {
    const void  *tx_buf;
    void        *rx_buf;
    unsigned    len;

    dma_addr_t  tx_dma;
    dma_addr_t  rx_dma;

    unsigned    cs_change:1;
    u8      bits_per_word;
    u16     delay_usecs;
    u32     speed_hz;

    struct list_head transfer_list;-------------------------|
};                                                          |
                                                            |
struct spi_message {                                        |
    struct list_head    transfers;--------------------------|

    struct spi_device   *spi;

    unsigned        is_dma_mapped:1;

    /* completion is reported through a callback */
    void            (*complete)(void *context);
    void            *context;
    unsigned        actual_length;
    int         status;

    struct list_head    queue;
    void            *state;
};

La relation entre spi_transfer et spi_message

Fonction d'interface d'opération:

static inline void spi_message_init(struct spi_message *m)
static inline void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
static inline void spi_transfer_del(struct spi_transfer *t)

6.2 、 ​​spi_master

Cette structure représente un bus SPI

struct spi_master {
    struct device   dev; //继承自device
    struct list_head list;  //spi_register_master 的时候会把spi_master注册到 spi_master_list这个双向链表上
    s16         bus_num; //第几根bus
    u16         num_chipselect; //这个spi master有几个片选信号
    u16         dma_alignment;
    u16         mode_bits;  //spi 协议的mode位
    u16         flags;

    /* lock and mutex for SPI bus locking */
    spinlock_t      bus_lock_spinlock;
    struct mutex        bus_lock_mutex;

    bool            bus_lock_flag;
    // 以下的三个接口都定义在spi_imx.c文件里
    //static int spi_imx_setup(struct spi_device *spi)
    //static int spi_imx_transfer(struct spi_device *spi,struct spi_transfer *transfer)
    //static void spi_imx_cleanup(struct spi_device *spi)
    int         (*setup)(struct spi_device *spi);
    int         (*transfer)(struct spi_device *spi,
                        struct spi_message *mesg);
    void            (*cleanup)(struct spi_device *spi);
};

6.3 、 spi_driver et spi_device

struct spi_device {
    struct device       dev;  //继承自device
    struct spi_master   *master;
    u32         max_speed_hz;
    u8          chip_select;
    u8          mode;
    u8          bits_per_word; //一个word多少个bit
    int         irq; 
    void            *controller_state;
    void            *controller_data;
    char            modalias[SPI_NAME_SIZE]; //这个会从spi_board_info的modalias域拷贝过来,驱动与设备匹配的时候会比较
}

//板级初始化的时候会根据spi_board_info ,new一个spi_device出来
struct spi_device *spi_new_device(struct spi_master *master,
                  struct spi_board_info *chip)
{
    struct spi_device   *proxy;
    int         status;

    proxy = spi_alloc_device(master);
    if (!proxy)
        return NULL;

    WARN_ON(strlen(chip->modalias) >= sizeof(proxy->modalias));

    proxy->chip_select = chip->chip_select;//片选,是低有效还是高有效
    proxy->max_speed_hz = chip->max_speed_hz; //最大频率
    proxy->mode = chip->mode; //spi操作模式
    proxy->irq = chip->irq;//中断号,我这里没有使用
    strlcpy(proxy->modalias, chip->modalias, sizeof(proxy->modalias));//拷贝别名
    proxy->dev.platform_data = (void *) chip->platform_data; //其它的数据可以通过这个指针取到
    proxy->controller_data = chip->controller_data;
    proxy->controller_state = NULL;

    status = spi_add_device(proxy);
    if (status < 0) {
        spi_dev_put(proxy);
        return NULL;
    }

    return proxy;
}

Le processus de correspondance de spi_driver & spi_device:

Comment correspondent-ils? ————— Lorsque le pilote de périphérique de plate-forme de spi_imx.c est enregistré

struct spi_driver {
    const struct spi_device_id *id_table;
    int         (*probe)(struct spi_device *spi); //匹配成功后会调用的函数
    int         (*remove)(struct spi_device *spi);
    void            (*shutdown)(struct spi_device *spi);
    int         (*suspend)(struct spi_device *spi, pm_message_t mesg);
    int         (*resume)(struct spi_device *spi);
    struct device_driver    driver;
};

// Initialisation du pilote de plate-forme:

static struct platform_driver spi_imx_driver = {
    .driver = {
           .name = DRIVER_NAME,
           .owner = THIS_MODULE,
           },
    .id_table = spi_imx_devtype,
    .probe = spi_imx_probe,
    .remove = __devexit_p(spi_imx_remove),
};

static int __init spi_imx_init(void)
{
    return platform_driver_register(&spi_imx_driver);
}

//platform_driver_register 会调用 bus_add_driver
int bus_add_driver(struct device_driver *drv)
{
        .
        .
        .
    if (drv->bus->p->drivers_autoprobe) {
        error = driver_attach(drv); //here
        if (error)
            goto out_unregister;         
    }
        .
        .
        .
}

static int __driver_attach(struct device *dev, void *data)
{
            .
            .
            .

    if (!driver_match_device(drv, dev)) //在这里会进行匹配
        return 0;

    if (dev->parent)    /* Needed for USB */
        device_lock(dev->parent);
    device_lock(dev);
    if (!dev->driver)
        driver_probe_device(drv, dev); //匹配成功了之后再probe
    device_unlock(dev);
    if (dev->parent)
        device_unlock(dev->parent);

    return 0;
}

static int spi_match_device(struct device *dev, struct device_driver *drv)
{
    const struct spi_device *spi = to_spi_device(dev);
    const struct spi_driver *sdrv = to_spi_driver(drv);

    /* Attempt an OF style match */
    if (of_driver_match_device(dev, drv))
        return 1;

    if (sdrv->id_table)
        return !!spi_match_id(sdrv->id_table, spi);

    return strcmp(spi->modalias, drv->name) == 0; //这里最终是匹配的名字
}


int driver_probe_device(struct device_driver *drv, struct device *dev)
{
        .
        .
        .
    pm_runtime_get_noresume(dev);
    pm_runtime_barrier(dev);
    ret = really_probe(dev, drv); //here
    pm_runtime_put_sync(dev);

    return ret;
}


static int really_probe(struct device *dev, struct device_driver *drv)
{
        .
        .
        .

    if (dev->bus->probe) {
        ret = dev->bus->probe(dev);
        if (ret)
            goto probe_failed;
    } else if (drv->probe) {
        ret = drv->probe(dev); //here
        if (ret)
            goto probe_failed;
    }

        .
        .
        .
}

La fonction de sonde pilotée par la plateforme imx spi:

static int __devinit spi_imx_probe(struct platform_device *pdev)
{
    struct spi_imx_master *mxc_platform_info;
    struct spi_master *master;
    struct spi_imx_data *spi_imx;
    struct resource *res;
    int i, ret;

    mxc_platform_info = dev_get_platdata(&pdev->dev);
    if (!mxc_platform_info) {
        dev_err(&pdev->dev, "can't get the platform data\n");
        return -EINVAL;
    }

    master = spi_alloc_master(&pdev->dev, sizeof(struct spi_imx_data));
    if (!master)
        return -ENOMEM;

    platform_set_drvdata(pdev, master);

    master->bus_num = pdev->id;
    master->num_chipselect = mxc_platform_info->num_chipselect;

    spi_imx = spi_master_get_devdata(master);
    spi_imx->bitbang.master = spi_master_get(master);
    spi_imx->chipselect = mxc_platform_info->chipselect;

    for (i = 0; i < master->num_chipselect; i++) {
        if (spi_imx->chipselect[i] < 0)
            continue;
        ret = gpio_request(spi_imx->chipselect[i], DRIVER_NAME);
        if (ret) {
            while (i > 0) {
                i--;
                if (spi_imx->chipselect[i] >= 0)
                    gpio_free(spi_imx->chipselect[i]);
            }
            dev_err(&pdev->dev, "can't get cs gpios\n");
            goto out_master_put;
        }
    }

    //这里的函数指针初始化比较重要
    spi_imx->bitbang.chipselect = spi_imx_chipselect;
    spi_imx->bitbang.setup_transfer = spi_imx_setupxfer;
    spi_imx->bitbang.txrx_bufs = spi_imx_transfer;
    spi_imx->bitbang.master->setup = spi_imx_setup;
    spi_imx->bitbang.master->cleanup = spi_imx_cleanup;
    spi_imx->bitbang.master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;

    init_completion(&spi_imx->xfer_done);

    spi_imx->devtype_data =
        spi_imx_devtype_data[pdev->id_entry->driver_data];

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(&pdev->dev, "can't get platform resource\n");
        ret = -ENOMEM;
        goto out_gpio_free;
    }

    if (!request_mem_region(res->start, resource_size(res), pdev->name)) {
        dev_err(&pdev->dev, "request_mem_region failed\n");
        ret = -EBUSY;
        goto out_gpio_free;
    }

    spi_imx->base = ioremap(res->start, resource_size(res));
    if (!spi_imx->base) {
        ret = -EINVAL;
        goto out_release_mem;
    }

    spi_imx->irq = platform_get_irq(pdev, 0);
    if (spi_imx->irq < 0) {
        ret = -EINVAL;
        goto out_iounmap;
    }

    /*上面我说没有用到中断,那这个中断什么鬼呢?这个中断是收发数据的SPI中断,上面说的中断是外接的从设备当数据准备好或是其它的情况的时候向IMX6输入的一个电平。*/
    ret = request_irq(spi_imx->irq, spi_imx_isr, 0, DRIVER_NAME, spi_imx);
    if (ret) {
        dev_err(&pdev->dev, "can't get irq%d: %d\n", spi_imx->irq, ret);
        goto out_iounmap;
    }

    spi_imx->clk = clk_get(&pdev->dev, NULL);
    if (IS_ERR(spi_imx->clk)) {
        dev_err(&pdev->dev, "unable to get clock\n");
        ret = PTR_ERR(spi_imx->clk);
        goto out_free_irq;
    }

    clk_enable(spi_imx->clk);
    spi_imx->spi_clk = clk_get_rate(spi_imx->clk);

    spi_imx->devtype_data.reset(spi_imx);

    spi_imx->devtype_data.intctrl(spi_imx, 0);
    ret = spi_bitbang_start(&spi_imx->bitbang);
    if (ret) {
        dev_err(&pdev->dev, "bitbang start failed with %d\n", ret);
        goto out_clk_put;
    }
    clk_disable(spi_imx->clk);

    //最后打印到这里
    dev_info(&pdev->dev, "probed\n");

    return ret;

out_clk_put:
    clk_disable(spi_imx->clk);
    clk_put(spi_imx->clk);
out_free_irq:
    free_irq(spi_imx->irq, spi_imx);
out_iounmap:
    iounmap(spi_imx->base);
out_release_mem:
    release_mem_region(res->start, resource_size(res));
out_gpio_free:
    for (i = 0; i < master->num_chipselect; i++)
        if (spi_imx->chipselect[i] >= 0)
            gpio_free(spi_imx->chipselect[i]);
out_master_put:
    spi_master_put(master);
    kfree(master);
    platform_set_drvdata(pdev, NULL);
    return ret;
}

Écrivez la description de l'image ici

7. Analyse des flux de données

Analyser de haut en bas comment une donnée est transférée de la couche application vers le registre, voici pour emprunter le pilote spidev apporté par le noyau lui-même

7.1 、 spidev_test.c

Écrivez la description de l'image ici

7,1 、 spidev.c

static int spidev_message(struct spidev_data *spidev,
        struct spi_ioc_transfer *u_xfers, unsigned n_xfers)
{
    struct spi_message  msg;
    struct spi_transfer *k_xfers;
    struct spi_transfer *k_tmp;
    struct spi_ioc_transfer *u_tmp;
    unsigned        n, total;
    u8          *buf;
    int         status = -EFAULT;

    spi_message_init(&msg); //message 初始化
    k_xfers = kcalloc(n_xfers, sizeof(*k_tmp), GFP_KERNEL);
    if (k_xfers == NULL)
        return -ENOMEM;

    buf = spidev->buffer;
    total = 0;
    for (n = n_xfers, k_tmp = k_xfers, u_tmp = u_xfers;
            n;
            n--, k_tmp++, u_tmp++) {
        k_tmp->len = u_tmp->len;

        total += k_tmp->len;
        if (total > bufsiz) {
            status = -EMSGSIZE;
            goto done;
        }

        if (u_tmp->rx_buf) {
            k_tmp->rx_buf = buf;
            if (!access_ok(VERIFY_WRITE, (u8 __user *)
                        (uintptr_t) u_tmp->rx_buf,
                        u_tmp->len))
                goto done;
        }
        if (u_tmp->tx_buf) {
            k_tmp->tx_buf = buf;
            if (copy_from_user(buf, (const u8 __user *)
                        (uintptr_t) u_tmp->tx_buf,
                    u_tmp->len))
                goto done;
        }
        buf += k_tmp->len;

        k_tmp->cs_change = !!u_tmp->cs_change;
        k_tmp->bits_per_word = u_tmp->bits_per_word;
        k_tmp->delay_usecs = u_tmp->delay_usecs;
        k_tmp->speed_hz = u_tmp->speed_hz;

        // 把transfer添加到message的链表上
        spi_message_add_tail(k_tmp, &msg);
    }
     //发送  
    status = spidev_sync(spidev, &msg);
    if (status < 0)
        goto done;

    /* copy any rx data out of bounce buffer */
    buf = spidev->buffer;
    for (n = n_xfers, u_tmp = u_xfers; n; n--, u_tmp++) {
        if (u_tmp->rx_buf) {
            if (__copy_to_user((u8 __user *)
                    (uintptr_t) u_tmp->rx_buf, buf,
                    u_tmp->len)) {
                status = -EFAULT;
                goto done;
            }
        }
        buf += u_tmp->len;
    }
    status = total;

done:
    kfree(k_xfers);
    return status;
}

7.1 、 spi.c et spi_bitbang.c

Écrivez la description de l'image ici

 static void bitbang_work(struct work_struct *work)
{
    struct spi_bitbang  *bitbang =
        container_of(work, struct spi_bitbang, work);
    unsigned long       flags;

    spin_lock_irqsave(&bitbang->lock, flags);
    bitbang->busy = 1;
    while (!list_empty(&bitbang->queue)) { //遍历bitbang的队列
        struct spi_message  *m;
        struct spi_device   *spi;
        unsigned        nsecs;
        struct spi_transfer *t = NULL;
        unsigned        tmp;
        unsigned        cs_change;
        int         status;
        int         do_setup = -1;

        m = container_of(bitbang->queue.next, struct spi_message,
                queue);
        list_del_init(&m->queue);
        spin_unlock_irqrestore(&bitbang->lock, flags);

        /* FIXME this is made-up ... the correct value is known to
         * word-at-a-time bitbang code, and presumably chipselect()
         * should enforce these requirements too?
         */
        nsecs = 100;

        spi = m->spi;
        tmp = 0;
        cs_change = 1;
        status = 0;

        list_for_each_entry (t, &m->transfers, transfer_list) { //遍历message 链表上的所有transfer

            /* override speed or wordsize? */
            if (t->speed_hz || t->bits_per_word)
                do_setup = 1;

            /* init (-1) or override (1) transfer params */
            if (do_setup != 0) {
                status = bitbang->setup_transfer(spi, t); //这里对spi的接口进行配置,因为每个transfer都可以设置 bits_per_word
                if (status < 0)
                    break;
                if (do_setup == -1)
                    do_setup = 0;
            }

            /* set up default clock polarity, and activate chip;
             * this implicitly updates clock and spi modes as
             * previously recorded for this device via setup().
             * (and also deselects any other chip that might be
             * selected ...)
             */
            if (cs_change) {
                bitbang->chipselect(spi, BITBANG_CS_ACTIVE); //这里在imx里没有什么毛线用,拉低电平是通过芯片内部的硬件实现的
                ndelay(nsecs); //这个也没有用
            }
            cs_change = t->cs_change;
            if (!t->tx_buf && !t->rx_buf && t->len) {
                status = -EINVAL;
                break;
            }

            /* transfer data.  the lower level code handles any
             * new dma mappings it needs. our caller always gave
             * us dma-safe buffers.
             */
            if (t->len) {
                /* REVISIT dma API still needs a designated
                 * DMA_ADDR_INVALID; ~0 might be better.
                 */
                if (!m->is_dma_mapped)
                    t->rx_dma = t->tx_dma = 0;
                status = bitbang->txrx_bufs(spi, t); //在这里把数据发出去了,最终调用了static int spi_imx_transfer(struct spi_device *spi,
                struct spi_transfer *transfer)
            }
            if (status > 0)
                m->actual_length += status;
            if (status != t->len) {
                /* always report some kind of error */
                if (status >= 0)
                    status = -EREMOTEIO;
                break;
            }
            status = 0;

            /* protocol tweaks before next transfer */
            if (t->delay_usecs)
                udelay(t->delay_usecs);

            if (!cs_change)
                continue;
            if (t->transfer_list.next == &m->transfers)
                break;

            /* sometimes a short mid-message deselect of the chip
             * may be needed to terminate a mode or command
             */
            ndelay(nsecs);
            bitbang->chipselect(spi, BITBANG_CS_INACTIVE);
            ndelay(nsecs);
        }

        m->status = status;
        m->complete(m->context);

        /* normally deactivate chipselect ... unless no error and
         * cs_change has hinted that the next message will probably
         * be for this chip too.
         */
        if (!(status == 0 && cs_change)) {
            ndelay(nsecs);
            bitbang->chipselect(spi, BITBANG_CS_INACTIVE);
            ndelay(nsecs);
        }

        spin_lock_irqsave(&bitbang->lock, flags);
    }
    bitbang->busy = 0;
    spin_unlock_irqrestore(&bitbang->lock, flags);
}

Délai entre la sélection de la puce et la transmission des données:

Écrivez la description de l'image ici

En fin de compte, cela est réalisé en définissant le champ CSD CTL du registre ECSPIx_PERIODREG. Voir les exigences de synchronisation de SC16IS752. Il n'est pas nécessaire de le définir ici. Nous sommes également des dizaines.

Écrivez la description de l'image ici

Le push dans la dernière étape est plus compliqué et générera une interruption. Une fois l'interruption générée, puis

Ceci est lié au processus de fonctionnement de l'ECSPI

Écrivez la description de l'image ici

référence

https://blog.csdn.net/droidphone/article/details/24663659

Je suppose que tu aimes

Origine blog.csdn.net/amwha/article/details/80126842
conseillé
Classement