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.
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.
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.
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.
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.
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.
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:
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:
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.