• RT-Thread 设备驱动SPI浅析及使用


    OS版本:RT-Thread 4.0.0

    测试BSP:STM32F407

    SPI简介

    SPI总线框架其实和I2C差不多,可以说都是总线设备+从设备,但SPI设备的通信时序配置并不固定,也就是说控制特定设备的总线需要单独配置;

    SPI的特性是工作方式众多,有标准SPI和QSPI

    QSPI: QSPI 是 Queued SPI 的简写,是 Motorola 公司推出的 SPI 接口的扩展,比 SPI 应用更加广泛。在 SPI 协议的基础上,Motorola 公司对其功能进行了增强,增加了队列传输机制,推出了队列串行外围接口协议(即 QSPI 协议)。使用该接口,用户可以一次性传输包含多达 16 个 8 位或 16 位数据的传输队列。一旦传输启动,直到传输结束,都不需要 CPU 干预,极大的提高了传输效率。与 SPI 相比,QSPI 的最大结构特点是以 80 字节的 RAM 代替了 SPI 的发送和接收数据寄存器。

    Dual SPI Flash: 对于 SPI Flash 而言全双工并不常用,可以发送一个命令字节进入 Dual 模式,让它工作在半双工模式,用以加倍数据传输。这样 MOSI 变成 SIO0(serial io 0),MISO 变成 SIO1(serial io 1),这样一个时钟周期内就能传输 2 个 bit 数据,加倍了数据传输。

    Quad SPI Flash: 与 Dual SPI 类似,Quad SPI Flash增加了两根 I/O 线(SIO2,SIO3),目的是一个时钟内传输 4 个 bit 数据。

    所以对于 SPI Flash,有标准 SPI Flash,Dual SPI Flash, Quad SPI Flash 三种类型。在相同时钟下,线数越多传输速率越高。

    SPI驱动分析

    RT-Thread将驱动层抽象成设备,应用只需熟悉设备接口即可,驱动的分析我们从其 设备类的实现来剖析;

    SPI的驱动里面主要包含两种设备 rt_spi_device(挂载SPI总线并配置了使能引脚和通信时序之后的设备) 和 rt_spi_bus(SPI总线、类似Linux的SPI适配器);

    rt_spi_bus 即 SPI 总线,rt_spi_device 是绑定 rt_spi_configuration 之后的设备

    struct rt_spi_device
    {
        struct rt_device parent;
        struct rt_spi_bus *bus;
    
        struct rt_spi_configuration config;
        void   *user_data;
    };
    
    struct rt_spi_bus
    {
        struct rt_device parent;
        rt_uint8_t mode;
        const struct rt_spi_ops *ops;
    
        struct rt_mutex lock;
        struct rt_spi_device *owner;
    };

    在使用 SPI 操作具体设备之前,需要 rt_hw_spi_device_attach 对对应设备的SPI时序配置进行绑定,官方的说法是将设备挂载到SPI总线;

    下面我们一步步来看 SPI 设备时怎么样初始化和注册设备的;

    其中 SPI 总线bus 在drv_spi.c 中的 rt_hw_spi_init(), 系统启动时进行了自动初始化

    int rt_hw_spi_init(void)
    {
        stm32_get_dma_info();
        return rt_hw_spi_bus_init();    //SPI-bus注册
    }
    INIT_BOARD_EXPORT(rt_hw_spi_init);

    而设备的挂载需要在用户程序实现,可以使用前挂载,也可以使用自动初始化实现

    // 自动初始化实现SPI设备挂载
    int w25q_spi_device_init()
    {
        __HAL_RCC_GPIOB_CLK_ENABLE();
        return rt_hw_spi_device_attach("spi1", "spi10", GPIOB, GPIO_PIN_14);  //设备挂载到SPI总线,抽象为 spi10 设备,同时使用时还需进行 rt_spi_configure
    } 
    INIT_DEVICE_EXPORT(w25q_spi_device_init);

    注意设备驱动在使用之前,需要对挂载的设备进行 rt_spi_configure,当然也可以在自动初始化中就配置

        spi_dev_w25q = (struct rt_spi_device *)rt_device_find(name);
        if (!spi_dev_w25q)
        {
            rt_kprintf("spi sample run failed! can't find %s device!
    ", name);
        }
        else
        {
            /* config spi */
            {
                struct rt_spi_configuration cfg;
                cfg.data_width = 8;
                cfg.mode = RT_SPI_MODE_0 | RT_SPI_MSB; /* SPI Compatible: Mode 0 and Mode 3 */
                cfg.max_hz = 50 * 1000 * 1000; /* 50M */
                rt_spi_configure(spi_dev_w25q, &cfg);
            }

    以上是设备句柄的实现流程

    SPI设备驱动

    在 spi_dev.c 中可以看出,SPI设备的主要操作没有主要使用 I/O 设备模型来操作;

    其 spi_device_ops 没有实现 contorl ,其读写则通过 rt_spi_transfer 实现;

    但是官方给出的SPI驱动主要接口为 下面两个,

    rt_spi_configure

    rt_spi_transfer_message

    主要是 rt_spi_transfer_message 可以更加灵活的适应各种SPI设备的通信协议

    当然还有其他数据传输接口,但都可以用 自定义传输 rt_spi_transfer_message 来实现,使用方式如下

            struct rt_spi_message msg1, msg2;
    
            msg1.send_buf   = &w25x_read_id;
            msg1.recv_buf   = RT_NULL;
            msg1.length     = 1;
            msg1.cs_take    = 1;
            msg1.cs_release = 0;
            msg1.next       = &msg2;
    
            msg2.send_buf   = RT_NULL;
            msg2.recv_buf   = id;
            msg2.length     = 5;
            msg2.cs_take    = 0;
            msg2.cs_release = 1;
            msg2.next       = RT_NULL;
    
            rt_spi_transfer_message(spi_dev_w25q, &msg1);
            rt_kprintf("use rt_spi_transfer_message() read w25q ID is:%x%x
    ", id[3], id[4]);
    
        // 其等同于下面的操作
         rt_spi_send_then_recv(spi_dev_w25q, &w25x_read_id, 1, id, 5);
            rt_kprintf("use rt_spi_send_then_recv() read w25q ID is:%x%x
    ", id[3], id[4]);

    spi传输的核心实现在 drv_spi.c 中的 spixfer() 函数,实现spi数据的收发

    先分析 spi 传输的消息体 

    struct rt_spi_message
    {
        const void *send_buf;        /** 发送缓冲区指针 */
        void *recv_buf;              /** 接收缓冲区指针 */
        rt_size_t length;            /** 发送 / 接收 数据字节数 */
        struct rt_spi_message *next; /** 指向继续发送的下一条消息的指针 */
    
        unsigned cs_take    : 1;    /** 片选选中 */
        unsigned cs_release : 1;    /** 片选释放 */
    };

    这样第一包数据

    msg1.cs_take = 1;
    msg1.cs_release = 0;

    中间包数据

    msgx.cs_take = 0;
    msgx.cs_release = 0;

    最后一包的数据使用

    msgn.cs_take = 0;
    msgn.cs_release = 1;

    同时应该指导 SPI 总线的工作原理,其在发送数据的同时也在接收数据,即发送数据时忽略了接收缓存,而接收数据也必须要发送数据来接收;

    spixfer 则调用Hal 库的 传输函数实现数据传输

    HAL_SPI_TransmitReceive_DMA  /  HAL_SPI_TransmitReceive

    HAL_SPI_Transmit_DMA  /  HAL_SPI_Transmit

    HAL_SPI_Receive_DMA  /  HAL_SPI_Receive

    这里我们注意 Hal 库的SPI传输支持 轮询、中断即DMA 三种方式,其中轮询支持超时检错,即数据传输完成、传输异常等可以较好发现,而DMA方式则需另外判断标志位处理,当然有出错回调处理;

    SPI驱动的具体使用

    修改 board 文件夹下的板级 Kconfig 文件,增加对 SPI 的支持

        menuconfig BSP_USING_SPI
            bool "Enable SPI BUS"
            default n
            select RT_USING_SPI
            if BSP_USING_SPI
                config BSP_USING_SPI1
                    bool "Enable SPI1 BUS"
                    default n
    
                config BSP_SPI1_TX_USING_DMA
                    bool "Enable SPI1 TX DMA"
                    depends on BSP_USING_SPI1
                    default n
            
                config BSP_SPI1_RX_USING_DMA
                    bool "Enable SPI1 RX DMA"
                    depends on BSP_USING_SPI1
                    select BSP_SPI1_TX_USING_DMA
                    default n
                
                config BSP_USING_SPI2
                    bool "Enable SPI2 BUS"
                    default n
                    
                config BSP_SPI2_TX_USING_DMA
                    bool "Enable SPI2 TX DMA"
                    depends on BSP_USING_SPI2
                    default n
            
                config BSP_SPI2_RX_USING_DMA
                    bool "Enable SPI2 RX DMA"
                    depends on BSP_USING_SPI2
                    select BSP_SPI2_TX_USING_DMA
                    default n

    进入env 使能 SPI, 另外 CubeMX 使能相应SPI外设 ,具体操作可参考上节

    接下来即可使用SPI设备驱动了,当然对应的拓展有 SPI-flash  及其引申出的 块设备文件系统,下次在单独描述。

    #if 1
    /*
     * 程序清单:这是一个 SPI 设备使用例程
     * 例程导出了 spi_w25q_sample 命令到控制终端
     * 命令调用格式:spi_w25q_sample spi10
     * 命令解释:命令第二个参数是要使用的SPI设备名称,为空则使用默认的SPI设备
     * 程序功能:通过SPI设备读取 w25q 的 ID 数据
    */
    #include "drv_spi.h"
    
    int w25q_spi_device_init()
    {
        __HAL_RCC_GPIOB_CLK_ENABLE();
        return rt_hw_spi_device_attach("spi1", "spi10", GPIOB, GPIO_PIN_14);
    }
    INIT_DEVICE_EXPORT(w25q_spi_device_init);
    
    
    #define W25Q_SPI_DEVICE_NAME     "spi10"
    
    static void spi_w25q_sample(int argc, char *argv[])
    {
        struct rt_spi_device *spi_dev_w25q;
        char name[RT_NAME_MAX];
        rt_uint8_t w25x_read_id = 0x90;
        rt_uint8_t id[5] = {0};
    
        if (argc == 2)
        {
            rt_strncpy(name, argv[1], RT_NAME_MAX);
        }
        else
        {
            rt_strncpy(name, W25Q_SPI_DEVICE_NAME, RT_NAME_MAX);
        }
        
    //    rt_hw_spi_device_attach("spi1", "spi10", GPIOB, GPIO_PIN_14);
        
        /* 查找 spi 设备获取设备句柄 */
        spi_dev_w25q = (struct rt_spi_device *)rt_device_find(name);
        if (!spi_dev_w25q)
        {
            rt_kprintf("spi sample run failed! can't find %s device!
    ", name);
        }
        else
        {
            /* config spi */
            {
                struct rt_spi_configuration cfg;
                cfg.data_width = 8;
                cfg.mode = RT_SPI_MODE_0 | RT_SPI_MSB; /* SPI Compatible: Mode 0 and Mode 3 */
                cfg.max_hz = 50 * 1000 * 1000; /* 50M */
                rt_spi_configure(spi_dev_w25q, &cfg);
            }
            
            /* 方式1:使用 rt_spi_send_then_recv()发送命令读取ID */
            rt_spi_send_then_recv(spi_dev_w25q, &w25x_read_id, 1, id, 5);
            rt_kprintf("use rt_spi_send_then_recv() read w25q ID is:%x%x
    ", id[3], id[4]);
    
            /* 方式2:使用 rt_spi_transfer_message()发送命令读取ID */
            struct rt_spi_message msg1, msg2;
    
            msg1.send_buf   = &w25x_read_id;
            msg1.recv_buf   = RT_NULL;
            msg1.length     = 1;
            msg1.cs_take    = 1;
            msg1.cs_release = 0;
            msg1.next       = &msg2;
    
            msg2.send_buf   = RT_NULL;
            msg2.recv_buf   = id;
            msg2.length     = 5;
            msg2.cs_take    = 0;
            msg2.cs_release = 1;
            msg2.next       = RT_NULL;
    
            rt_spi_transfer_message(spi_dev_w25q, &msg1);
            rt_kprintf("use rt_spi_transfer_message() read w25q ID is:%x%x
    ", id[3], id[4]);
    
        }
    }
    /* 导出到 msh 命令列表中 */
    MSH_CMD_EXPORT(spi_w25q_sample, spi en25q sample);
    #endif
  • 相关阅读:
    弹出窗口失败 Debug Assertion Failed!
    颜色设置 OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) 的用法
    启动项
    进程PK线程
    获取代码运行时间,获取当前系统时间,日期
    在WindowsPhone8中生成基于MVVM Light的LongListSelector拼音检索绑定
    微软认证考试Mcts70511 part1翻译_Part2_使用控件_ContentControl
    微软认证考试Mcts70511 part1翻译_Part1_考分分配
    ORA14452: attempt to create, alter or drop an index on temporary table already in use
    HPUX日常工作整理
  • 原文地址:https://www.cnblogs.com/silencehuan/p/10950973.html
Copyright © 2020-2023  润新知