My understanding of RT-Thread SPI

Created at 2020-12-10 16:57:17

Overview of the virtual bus and SPI-driven frameworks.
First of all, I would like to repeat introducing the concept of virtual bus. A bus typically refers to a channel between a processor and one or more devices. Why is it called a virtual bus, because it is not necessarily an objective hardware. Take the SPI as the example, a lot of microcontrollers do have physical controllers, but there are a lot of applications that are simulated with IO, so you can understand the word of "virtual". From the point of view of software structure, the purpose of proposing virtual bus is to abstract a model. A model that can be plugged in and used as a bridge for communication and management. With the model, we can easily layer the software and make the software structure more reasonable. The SPI driver framework has four files, spi_dev.c, spi_core.c and their header file spi.h, the driver stm32f20x_40x_spi.c for specific hardware.

I drawed a SPI-driven framework diagram to clearly specify the SPI


Let's look at this picture from top to bottom. The top layer is RT-Thread's device management, and we use SPI-driven processes to operate and communicate specific hardware through RT-Thread, a unified management interface.
Next is the core spi_core.c file, which defines the bus and device models, and enrolls both the bus and the device in RT-Thread device management. This spi_core.c also implements some special ways to send and receive data that can be called directly by looking up the device.
Between core and RT-Thread management, this spi_dev.c file simply provides some of the rule methods needed to enroll the bus and device, which is briefly described below.

The bottom layer is the hardware-specific driver code, as to what is implemented inside I will specify later. A bus can have only one device but more can mount (plug in) more devices, I represent here with an other device, each different hardware, or different format of sending and receiving hardware needs to be abstracted separately.
This diagram is a concept that helping to layer files and programs.

Important structural (class) interpretation
I'll start with the structures (classes) in the header file, because these structures describe some of the models in the program. Understanding these models is easier to understand when you read the program. My colleagues in software told me that when learning a new thing, it's all about looking at the relationships and interfaces between classes and inheritances, not rushing to read code. I think this advice is actually available to people who are transitioning from hardware to the software. Fortunately in our SPI-driven framework header file is very simple, is a spi.h file, we open the file one by one analysis.

A. Bus structure

struct rt_spi_bus
    struct rt_device parent;
    const struct rt_spi_ops *ops;

    struct rt_mutex lock;
    struct rt_spi_device *owner;

This structure defines the bus (BUS) in the drive frame, and I'll introduce the role of some of these members following my understanding.

  1. struct rt_device parent;
    RTT's device description, which contains some tags, types, callback function pointers, device operation function pointers, object basic information, and so on. You can register the bus as a device to take advantage of the RT-Thread device management framework. With this common management framework, you can use the system call rt_device_t rt_device_find provided by RT-Thread to find the device or bus. The interface is unified, simplifying the code for the SPI section (without having to write the code for the SPI bus management section separately).
  2. const struct rt_spi_ops s.ops;
    rt_spi_ops describes two of the most basic operations abstracted by the SPI bus: configuration and sending and receiving data, as defined below:
    struct rt_spi_ops
        rt_err_t (*configure)(struct rt_spi_device *device, struct rt_spi_configuration *configuration);
        rt_uint32_t (*xfer)(struct rt_spi_device *device, struct rt_spi_message *message);


Yes, the SPI-driven framework looks complex (for rookies like me) but actually it only needs 2 operations. SPI is a bus that sends and receives single-clock data at the same time, except that the configuration is to send and receive data. But here I have a little question, why is this method of operating on the bus not placed on each attached device on the bus, but registered on the bus? The configuration can be configured for the same controller can be a unified way to configure different parameters, but the send and receive strategy I think will vary depending on the device. Now that you're on the bus, writing this xfer function has to consider the sending and receiving of various configurations. For example, one byte to four bytes of bit width is different, with DMA or software to copy data and so on.

  1. struct rt_mutex lock;
    This is used to mutually exclusive multithreaded access bus. Multiple devices can be attached to a bus, so in a multithreaded environment there may be multiple processes competing for bus usage rights to access the devices they need to access.
  2. struct rt_spi_device *owner;
    From the name we can guess that the device is currently in operation by the bus. What's the point, as we said before, an SPI bus can hang multiple devices (distinguished by CS signal), different devices operating position width, speed, clock phase may be different. So this pointer represents a device that is currently getting bus operations. If the longer operation checks this pointer to find that the device did not need to configure the bus before using this SPI bus, the bus parameters will be reconfigured. The specific code can be spi_core many places in the code.c write:
if (device->bus->owner != device)   
        result = device->bus->ops->configure(device, &device->config);

B. The device structure

struct rt_spi_device
    struct rt_device parent;
    struct rt_spi_bus *bus;

    struct rt_spi_configuration config;

This structure defines the device in the drive framework and and I'll introduce the role of some of these members following my understanding.

  1. struct rt_device parent;
    RTT's device description, which contains some tags, types, callback function pointers, device operation function pointers, object basic information, and so on. Devices on the SPI bus can be individually enrolled in the RT-Thread device framework.
  2. struct rt_spi_bus s bus
    The device is attached to the (attach) bus to find the send and configure method on the bus.
  3. struct rt_spi_configuration config;
  struct rt_spi_configuration
        rt_uint8_t mode;
        rt_uint8_t data_width;
        rt_uint16_t reserved;
        rt_uint32_t max_hz;


Description of the configuration information for the SPI bus.
Mode selection for configuring clock levels, sampling edge polarity, highest or lowest bit pass first.
data_width data width, in bytes.
Reserved reservation.
max_hz the maximum bus clock.

C. The message structure

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;

This structure defines the message in the driver framework, a class (structure) that artificially abstracts data from an SPI, I'll introduce the role of some of these members following my understanding.

  1. const void *send_buf;
    Send a buffer pointer to point to the buffer where you want to send data.
  2. void *recv_buf;
    The receive buffer pointer, where the pointer type is void, indicates that the receiving data target is a piece of memory, not a fixed 8-bit or 16-bit wide array.
  3. rt_size_t length;
    The length of data sent and received.
  4. struct rt_spi_message *next;
    A pointer embedded in the next message can form a one-way list that strings messages together to send and receive them uniformly.
  5. unsigned cs_take: 1; and unsigned cs_release: 1;
    Literally, this variable is indicating the pull-up of CS and the pull-down of CS. Because there is no uniform standard for the length of sending and receiving data for SPI devices, you can send and receive any byte, not even CS. Therefore cs_take indicates whether the CS signal should be pulled down before the message is sent, cs_release indicates whether the CS signal should be pulled up after the message is sent. This allows you to form any frame-length SPI communication timing.

The program analysis and brief description

After introducing the spi.h header file, we're officially starting to analyze the code. The code for the SPI-driven spi_core consists of spi_dev.c and spi_core.c

A. Bus registration function

   1. rt_err_t rt_spi_bus_register(    struct rt_spi_bus *bus,
                                               const char *name,
                                               const struct rt_spi_ops *ops)

The core of the framework is the virtual SPI bus. So the first thing to build this framework is to create or register an SPI driver, which has three parameters.

  1. struct rt_spi_bus *bus: represents the object of the bus that you want to register.
  2. const char *name: The name of the bus, which is a string.
  3. const struct rt_spi_ops *ops: the method of operating the bus that we mentioned earlier.

    The following rt_spi_bus_register are performed in this function.

    1. Register this bus as an RT-Thread device for unified management.
    2. Initialize the amount of mutual exclusion in the bus object.
    3. Save the bus operation method in the parameter.
    4. Set the bus owner empty to indicate that it has not been acquired.

B. Device bound to bus functions

    rt_err_t rt_spi_bus_attach_device(struct rt_spi_device *device,
                                  struct rt_spi_device *device
                                  const char *bus_name,
                                  void *user_data)

This function is used to create (or bind) a device to the specified SPI bus. One device corresponds to one CS pin. Description of the parameters:

  1. struct rt_spi_device *device used to create (binding) a device with a pointer to an instantiated object that cannot be a temporary variable.
  2. struct rt_spi_device *device: the name of the device
  3. const char *bus_name used to bind to the bus.
  4. void *user_data is a user pointer, which is actually the information that is passed in to the CS pin.

    In this function:

    In this function:

    1. According to the string name, the bus to bind is found by the system call. Interested can see how to register and find the device.
    2. Save the bus to the device description.
    3. Register this device with the RT-Thread device management framework.
    4. Empty the configuration, because if this memory is allocated from the heap, it is likely to be a mess of data.
    5. Save the IO pin information for CS to rt_device data pointer in the file. Here I do not understand, the number of SPI devices certainly not many, 4 bytes of memory occupied why not save CS information in rt_spi_device.

C. Bus configuration function

    rt_err_t rt_spi_configure(struct rt_spi_device *device,
                          struct rt_spi_configuration *cfg)   

Based on the interface we judge that this function is to pass in configuration parameters and configure the bus.
The function device->bus->ops->configure (device, sed->config) called the configuration method.

Since then spi_core functions in the .c file is very simple to understand, so I will give a short introduction.
rt_spi_send_then_send - Send data after send_buf1 data is sent send_buf2.
rt_spi_send_then_recv - Receive data as soon as it is sent.
rt_spi_transfer - Send and receive simultaneously.
rt_spi_transfer_message - Send an SPI message data (simply send a piece of data that is shrapned according to the rules, the rules are set by the SPI framework, and the message data content is populated by itself)
The last gets the release bus and device functions that we don't need to care about.

D. Others
spi_dev.c file. This file is the interface between SPI drive and RT-Thread system device management.
Enrolling an RT-Thread device requires several standard methods, similar to the kind of on, off, read, write, and IO control of a linux character device. As I said above, the SPI drive framework registers both the bus and the device as RTT devices, so this file implements the method of enrolling RTT devices, and the method of enrolling.

Device-related SPI drivers
Here I take the example of stm32f20x_40x_spi.c that I modified and I put it to F20_40X this file for the purpose of deleting and porting it to F103. The core of this file does two things, implementing the function of xfer data sending and receiving and the function of configuring the bus, registering the bus.

Use Specifics
On the official website wiki there is such an article that introduces how to use SPI-driver, you can check it out after reading this article. Welcome to share your opinions with me.

0 Answer