Linux SPI Driver Experiment

In the previous chapter, we explained how to write the I2C device driver under Linux. SPI is also a very common serial communication protocol. In
this chapter, we will learn how to write the SPI device driver under Linux.
The ultimate goal of the experiment in this chapter is to drive the ICM-20608 six-axis sensor with SPI interface on the I.MX6U-ALPHA development board, and the original sensor data of ICM-20608 can be read in the application program.

Introduction to SPI driver framework under Linux

The SPI driver framework is very similar to I2C. They are divided into host controller drivers and device drivers. The host controller is also the
SPI controller interface of the SOC. For example, in "Chapter 27 SPI Experiment" in the bare metal chapter, we have written
two files, bsp_spi.c and bsp_spi.h, which are the SPI controller drivers of I.MX6U. We have written the SPI control After the device driver, it can
be used directly. No matter what SPI device is, the driver of the SPI controller part is the same. Our focus is on the
various SPI device drivers.

SPI host driver

The SPI host driver is the SPI controller driver of the SOC, similar to the adapter driver in the I2C driver. The Linux kernel
uses spi_master to represent the SPI host driver. spi_master is a structure defined in the include/linux/spi/spi.h file
, the content is as follows (with abbreviation):

315 struct spi_master {
    
    
316 struct device dev;
317
318 struct list_head list;
......
326 s16 bus_num;
327
328 /* chipselects will be integral to many controllers; some others
329 * might use board-specific GPIOs.
330 */
331 u16 num_chipselect;
332
333 /* some SPI controllers pose alignment requirements on DMAable
334 * buffers; let protocol drivers know about these requirements.
335 */
336 u16 dma_alignment;
337
338 /* spi_device.mode flags understood by this controller driver */
339 u16 mode_bits;
340
341 /* bitmask of supported bits_per_word for transfers */
342 u32 bits_per_word_mask;
......
347 /* limits on transfer speed */
348 u32 min_speed_hz;
349 u32 max_speed_hz;
350
351 /* other constraints relevant to this driver */
352 u16 flags;
359 /* lock and mutex for SPI bus locking */
360 spinlock_t bus_lock_spinlock;
361 struct mutex bus_lock_mutex;
362
363 /* flag indicating that the SPI bus is locked for exclusive use */
364 bool bus_lock_flag;
......
372 int (*setup)(struct spi_device *spi);
373
......
393 int (*transfer)(struct spi_device *spi,
394 struct spi_message *mesg);
......
434 int (*transfer_one_message)(struct spi_master *master,
435 struct spi_message *mesg);
......
462 };

......

Line 393, transfer function, same as master_xfer function in i2c_algorithm, controller data transfer function
.
Line 434, the transfer_one_message function is also used to send SPI data, used to send a spi_message, the
SPI data will be packaged into spi_message, and then sent out in a queue.
That is to say, the SPI host will eventually communicate with the SPI device through the transfer function, so for the driver
writer of the SPI host controller, the transfer function needs to be implemented, because different SOCs have different SPI controllers and different registers.
Sample. Like the I2C adapter driver, the SPI host driver is generally written by the SOC manufacturer, so we, as SOC
users, don’t have to worry about this part of the driver, unless you are working in the original SOC factory, and the content is to write the SPI host driver.
move.
The core of the SPI host driver is to apply for spi_master, then initialize spi_master, and finally register
spi_master with the Linux kernel.
1. spi_master application and release
The spi_alloc_master function is used to apply for spi_master. The function prototype is as follows:

struct spi_master *spi_alloc_master(struct device *dev,
unsigned size)

The meanings of function parameters and return values ​​are as follows:
dev: device, generally the dev member variable in platform_device.
size: private data size, which can be obtained through the spi_master_get_devdata function.
Return value: the applied spi_master.
The release of the spi_master is done through the spi_master_put function. When we delete a SPI host driver, we need to
release the previously applied spi_master. The prototype of the spi_master_put function is as follows:

void spi_master_put(struct spi_master *master)

The meanings of function parameters and return values ​​are as follows:
master: the spi_master to be released.
Return value: None.

2. Registration and deregistration of spi_master
When spi_master is initialized, it needs to be registered to the Linux kernel. The spi_master registration function is
spi_register_master, and the function prototype is as follows:

int spi_register_master(struct spi_master *master)

The meanings of function parameters and return values ​​are as follows:
master: the spi_master to be registered.
Return value: 0, success; negative value, failure.
The SPI host driver of I.MX6U will use the API function spi_bitbang_start to complete the registration of spi_master, and the
spi_bitbang_start function actually completes the registration of spi_master by calling the spi_register_master function.
If you want to unregister spi_master, you can use the spi_unregister_master function. The prototype of this function is:

void spi_unregister_master(struct spi_master *master)

The meanings of function parameters and return values ​​are as follows:
master: the spi_master to be unregistered.
Return value: None.
If you use spi_bitbang_start to register spi_master, you need to use spi_bitbang_stop to log off
spi_master.

SPI device driver

The spi device driver is also very similar to the i2c device driver. The Linux kernel uses the spi_driver structure to represent the spi device driver
. We need to implement spi_driver when writing the SPI device driver. The spi_driver structure is defined in
the include/linux/spi/spi.h file, and the structure content is as follows:
insert image description here
It can be seen that spi_driver is basically the same as i2c_driver and platform_driver, and
the probe function will be executed when the SPI device and driver are successfully matched.
Similarly, after the spi_driver is initialized, it needs to register with the Linux kernel. The spi_driver registration function is
spi_register_driver. The function prototype is as follows:

int spi_register_driver(struct spi_driver *sdrv)

The meanings of function parameters and return values ​​are as follows:
sdrv: the spi_driver to be registered.
Return value: 0, registration is successful; assignment, registration fails.
After unregistering the SPI device driver, the previously registered spi_driver also needs to be unregistered. Use the spi_unregister_driver function
to complete the unregistering of the spi_driver. The function prototype is as follows:

void spi_unregister_driver(struct spi_driver *sdrv)

The meanings of function parameters and return values ​​are as follows:
sdrv: the spi_driver to be unregistered.
Return value: None.
The sample program for spi_driver registration is as follows:

1 /* probe函数*/
2 static int xxx_probe(struct spi_device *spi)
3 {
    
    
4 /* 具体函数内容*/
5 return 0;
6 }
7
8 /* remove函数*/
9 static int xxx_remove(struct spi_device *spi)
10 {
    
    
11 /* 具体函数内容*/
12 return 0;
13 }
14 /* 传统匹配方式ID列表*/
15 static const struct spi_device_id xxx_id[] = {
    
    
16 {
    
    "xxx", 0},
17 {
    
    }
18 };
19
20 /* 设备树匹配列表*/
21 static const struct of_device_id xxx_of_match[] = {
    
    
22 {
    
     .compatible = "xxx" },
23 {
    
     /* Sentinel */ }
24 };
25
26 /* SPI驱动结构体*/
27 static struct spi_driver xxx_driver = {
    
    
28 .probe = xxx_probe,
29 .remove = xxx_remove,
30 .driver = {
    
    
31 .owner = THIS_MODULE,
32 .name = "xxx",
33 .of_match_table = xxx_of_match,
34 },
35 .id_table = xxx_id,
36 };
37
38 /* 驱动入口函数*/
39 static int __init xxx_init(void)
40 {
    
    
41 return spi_register_driver(&xxx_driver);
42 }
43
44 /* 驱动出口函数*/
45 static void __exit xxx_exit(void)
46 {
    
    
47 spi_unregister_driver(&xxx_driver);
48 }
49
50 module_init(xxx_init);
51 module_exit(xxx_exit);

Lines 1~36, spi_driver structure, need to be written by SPI device driver, including matching table, probe function, etc.
It is the same as i2c_driver and platform_driver, so I won't explain it in detail.
Lines 39~42, call spi_register_driver in the driver entry function to register spi_driver.
In line 45~48, call spi_unregister_driver in the driver exit function to unregister spi_driver.

SPI device and driver matching process

The matching process of SPI devices and drivers is done by the SPI bus, which is the same as platform, I2C and other drivers. The SPI
bus is spi_bus_type, which is defined in the drivers/spi/spi.c file, and the content is as follows:

131 struct bus_type spi_bus_type = {
    
    
132 .name = "spi",
133 .dev_groups = spi_dev_groups,
134 .match = spi_match_device,
135 .uevent = spi_uevent,
136 };

It can be seen that the matching function of the SPI device and driver is spi_match_device, and the content of the function is as follows:

99 static int spi_match_device(struct device *dev,
struct device_driver *drv)
100 {
    
    
101 const struct spi_device *spi = to_spi_device(dev);
102 const struct spi_driver *sdrv = to_spi_driver(drv);
103
104 /* Attempt an OF style match */
105 if (of_driver_match_device(dev, drv))
106 return 1;
107
108 /* Then try ACPI */
109 if (acpi_driver_match_device(dev, drv))
110 return 1;
111
112 if (sdrv->id_table)
113 return !!spi_match_id(sdrv->id_table, spi);
114
115 return strcmp(spi->modalias, drv->name) == 0;
116 }

The spi_match_device function and the i2c_match_device function are basically the same for the device and driver matching process.
On line 105, the of_driver_match_device function is used to complete device tree device and driver matching. Compare whether the compatible attribute of the SPI device node
is equal to the compatible attribute in of_device_id, and if so, it means that the SPI device
and the driver match.
On line 109, the acpi_driver_match_device function is used for ACPI-style matching.
On line 113, the spi_match_id function is used for the traditional, device-tree-less SPI device and driver matching process. Compare whether the SPI
device name is equal to the name field of spi_device_id. If they are equal, it means that the SPI device matches the driver.
Line 115 compares whether the modalias member variable in spi_device is equal to the name member variable in device_driver
.

I.MX6U SPI host driver analysis

Like the I2C adapter driver, the SPI host driver is generally written by the SOC manufacturer. Open the imx6ull.dtsi
file and find the following content:

示例代码62.2.1 imx6ull.dtsi 文件中的ecspi3 节点内容
1 ecspi3: ecspi@02010000 {
    
    
2 #address-cells = <1>;
3 #size-cells = <0>;
4 compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
5 reg = <0x02010000 0x4000>;
6 interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
7 clocks = <&clks IMX6UL_CLK_ECSPI3>,
8 <&clks IMX6UL_CLK_ECSPI3>;
9 clock-names = "ipg", "per";
10 dmas = <&sdma 7 7 1>, <&sdma 8 7 2>;
11 dma-names = "rx", "tx";
12 status = "disabled";
13 };

Focus on the compatible attribute value in line 4. The compatible attribute has two values ​​"fsl,imx6ul-ecspi" and
"fsl,imx51-ecspi". Search these two attribute values ​​in the Linux kernel source code to find I. The ECSPI (SPI)
host driver corresponding to MX6U . The ECSPI host driver file of I.MX6U is drivers/spi/spi-imx.c, and the following content is found in this file:

示例代码62.2.2 spi_imx_driver 结构体
694 static struct platform_device_id spi_imx_devtype[] = {
    
    
695 {
    
    
696 .name = "imx1-cspi",
697 .driver_data = (kernel_ulong_t) &imx1_cspi_devtype_data,
698 }, {
    
    
699 .name = "imx21-cspi",
700 .driver_data = (kernel_ulong_t) &imx21_cspi_devtype_data,
713 }, {
    
    
714 .name = "imx6ul-ecspi",
715 .driver_data = (kernel_ulong_t) &imx6ul_ecspi_devtype_data,
716 }, {
    
    
717 /* sentinel */
718 }
719 };
720
721 static const struct of_device_id spi_imx_dt_ids[] = {
    
    
722 {
    
     .compatible = "fsl,imx1-cspi", .data =
&imx1_cspi_devtype_data, },
......
728 {
    
     .compatible = "fsl,imx6ul-ecspi", .data =
&imx6ul_ecspi_devtype_data, },
729 {
    
     /* sentinel */ }
730 };
731 MODULE_DEVICE_TABLE(of, spi_imx_dt_ids);
......
1338 static struct platform_driver spi_imx_driver = {
    
    
1339 .driver = {
    
    
1340 .name = DRIVER_NAME,
1341 .of_match_table = spi_imx_dt_ids,
1342 .pm = IMX_SPI_PM,
1343 },
1344 .id_table = spi_imx_devtype,
1345 .probe = spi_imx_probe,
1346 .remove = spi_imx_remove,
1347 };
1348 module_platform_driver(spi_imx_driver);

......

Line 714, spi_imx_devtype is SPI no device tree matching table.
Line 721, spi_imx_dt_ids is the SPI device tree matching table.
Line 728, "fsl,imx6ul-ecspi" matches, so we know that the ECSPI driver of I.MX6U is the
file spi-imx.c.
Lines 1338~1347, the platform_driver driver framework is the same as the I2C adapter driver, and the SPI host driver uses
the platform driver framework. The spi_imx_probe function will be executed when the device and driver are successfully matched.
The spi_imx_probe function will read the corresponding node attribute value from the device tree, apply for and initialize the spi_master, and finally call the
spi_bitbang_start function (spi_bitbang_start will call the spi_register_master function) to register
the spi_master with the Linux kernel.
For I.MX6U, the final data sending and receiving function of the SPI host is spi_imx_transfer, and this function
finally realizes SPI data sending through the following layer-by-layer calls:

spi_imx_transfer
	-> spi_imx_pio_transfer
		-> spi_imx_push
			-> spi_imx->tx



spi_imx is a mechanism pointer variable of type spi_imx_data, where the two member variables tx and rx are SPI data sending and receiving functions respectively . The I.MX6U SPI host driver will maintain a spi_imx_data type variable spi_imx, and
use the spi_imx_setupxfer function to set the tx and rx functions of spi_imx. According to the data bit width to be sent
, there are 8-bit, 16-bit and 32-bit sending functions, as follows:

spi_imx_buf_tx_u8
spi_imx_buf_tx_u16
spi_imx_buf_tx_u32

Similarly, there are also 8-bit, 16-bit and 32-bit data receiving functions, as follows:

spi_imx_buf_rx_u8
spi_imx_buf_rx_u16
spi_imx_buf_rx_u32

Let's take the function spi_imx_buf_tx_u8 as an example to see how to send one's own data.
Find the following content in the spi-imx.c file:

示例代码62.2.3 spi_imx_buf_tx_u8 函数
152 #define MXC_SPI_BUF_TX(type) \
153 static void spi_imx_buf_tx_##type(struct spi_imx_data *spi_imx) \
154 {
    
     \
155 type val = 0; \
156 \
157 if (spi_imx->tx_buf) {
    
     \
158 val = *(type *)spi_imx->tx_buf; \
159 spi_imx->tx_buf += sizeof(type); \
160 } \
161 \
162 spi_imx->count -= sizeof(type); \
163 \
164 writel(val, spi_imx->base + MXC_CSPITXDATA); \
165 }
166
167 MXC_SPI_BUF_RX(u8)
168 MXC_SPI_BUF_TX(u8)

It can be seen from the sample code 62.2.3 that the spi_imx_buf_tx_u8 function is implemented by the MXC_SPI_BUF_TX macro
. Line 164 is to write the data value to be sent into the TXDATA register of ECSPI, which is the same as our SPI bare metal
experiment method. Expanding MXC_SPI_BUF_TX(u8) on line 168 is the spi_imx_buf_tx_u8 function.
Other tx and rx functions are implemented in this way, so I won't introduce them here. The host driver of I.MX6U is explained
here, the basic routine is similar to the adapter driver of I2C.

SPI device driver writing process

SPI device information description

1. The creation and modification of pinctrl sub-nodes of IO
must first create or modify pinctrl sub-nodes according to the IO used. There is nothing to say about this. The only thing to pay attention to is to check whether the corresponding IO is used by other devices. If there is, it needs to be deleted!
2. Creation and modification of SPI device nodes
In the case of using the device tree, the SPI device information description is completed by creating the corresponding device sub-nodes. We can
open the device tree header file imx6qdl-sabresd.dtsi and find it in this file The content is as follows:

示例代码62.3.1.1 m25p80 设备节点
308 &ecspi1 {
    
    
309 fsl,spi-num-chipselects = <1>;
310 cs-gpios = <&gpio4 9 0>;
311 pinctrl-names = "default";
312 pinctrl-0 = <&pinctrl_ecspi1>;
313 status = "okay";
314
315 flash: m25p80@0 {
    
    
316 #address-cells = <1>;
317 #size-cells = <1>;
318 compatible = "st,m25p32";
319 spi-max-frequency = <20000000>;
320 reg = <0>;
321 };
322 };

The sample code 62.3.1.1 is a SPI device node on a board of I.MX6Q, and
an m25p80 is connected to the ECSPI interface of this board, which is a device of SPI interface.
Line 309, set the attribute "fsl,spi-num-chipselects" to 1, which means there is only one device.
Line 310, set the "cs-gpios" attribute, that is, the chip select signal is GPIO4_IO09.
Line 311, set the "pinctrl-names" attribute, which is the IO name used by the SPI device.
Line 312, set the "pinctrl-0" attribute, that is, the pinctrl node corresponding to the used IO.
On line 313, change the "status" attribute of the ecspi1 node to "okay".
Lines 315~320, m25p80 device information under ecspi1, each SPI device uses a child node to describe
its device information. The "0" after "m25p80@0" in line 315 indicates that m25p80 is connected
to channel 0 of ECSPI. This should be set according to your own specific hardware.
Line 318, the compatible attribute value of the SPI device is used to match the device driver.
Line 319, the attribute "spi-max-frequency" sets the maximum frequency of the SPI controller, which should be set according to the SPI device used
, for example, the maximum frequency of the SPI controller is set to 20MHz here.
Line 320, the reg attribute sets the ECSPI channel used by the m25p80 device, which is the
same as the "0" behind "m25p80@0".
We will refer to the content in the sample code 62.3.1.1 when we write the device tree node information of ICM20608 later
.

SPI device data sending and receiving processing flow

The core of the SPI device driver is spi_driver, which we have already talked about in Section 62.1.2. After we
successfully register the spi_driver with the Linux kernel, we can use the API functions provided by the SPI core layer to read and write the device.
The first is the spi_transfer structure, which is used to describe the SPI transfer information, and the content of the structure is as follows:

示例代码62.3.2.1 spi_transfer 结构体
603 struct spi_transfer {
    
    
604 /* it's ok if tx_buf == rx_buf (right?)
605 * for MicroWire, one buffer must be null
606 * buffers must work with dma_*map_single() calls, unless
607 * spi_message.is_dma_mapped reports a pre-existing mapping
608 */
609 const void *tx_buf;
610 void *rx_buf;
611 unsigned len;
612
613 dma_addr_t tx_dma;
614 dma_addr_t rx_dma;
615 struct sg_table tx_sg;
616 struct sg_table rx_sg;
617
618 unsigned cs_change:1;
619 unsigned tx_nbits:3;
620 unsigned rx_nbits:3;
621 #define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */
622 #define SPI_NBITS_DUAL 0x02 /* 2bits transfer */
623 #define SPI_NBITS_QUAD 0x04 /* 4bits transfer */
624 u8 bits_per_word;
625 u16 delay_usecs;
626 u32 speed_hz;
627
628 struct list_head transfer_list;
629 };

Line 609, tx_buf holds the data to be sent.
Line 610, rx_buf is used to save the received data.
In line 611, len is the length of the data to be transmitted, and SPI is full-duplex communication, so the
number of bytes sent and received in one communication is the same, so there is no difference between sending length and receiving length in spi_transfer .
spi_transfer needs to be organized into spi_message, spi_message is also a structure, the content is as follows:

示例代码62.3.2.2 spi_message 结构体
660 struct spi_message {
    
    
661 struct list_head transfers;
662
663 struct spi_device *spi;
664
665 unsigned is_dma_mapped:1;
......
678 /* completion is reported through a callback */
679 void (*complete)(void *context);
680 void *context;
681 unsigned frame_length;
682 unsigned actual_length;
683 int status;
684
685 /* for optional use by whatever driver currently owns the
686 * spi_message ... between calls to spi_async and then later
687 * complete(), that's the spi_master controller driver.
688 */
689 struct list_head queue;
690 void *state;
691 };

Before using spi_message, it needs to be initialized. The spi_message initialization function is spi_message_init.
The function prototype is as follows:

void spi_message_init(struct spi_message *m)

The meanings of function parameters and return value are as follows:
m: spi_message to be initialized.
Return value: None.
After the initialization of spi_message is completed, spi_transfer needs to be added to the spi_message queue. Here we need to use
the spi_message_add_tail function. The prototype of this function is as follows:

void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)

The meanings of function parameters and return values ​​are as follows:
t: spi_transfer to be added to the queue.
m: spi_message to be added by spi_transfer.
Return value: None.
After spi_message is ready, data transmission can be carried out. Data transmission is divided into synchronous transmission and asynchronous transmission. Synchronous
transmission will block and wait for the completion of SPI data transmission. The synchronous transmission function is spi_sync, and the function prototype is as follows:

int spi_sync(struct spi_device *spi, struct spi_message *message)

The meanings of function parameters and return values ​​are as follows:
spi: spi_device for data transmission.
message: The spi_message to transmit.
Return value: None.
Asynchronous transmission will not block until the SPI data transmission is completed. Asynchronous transmission needs to set the complete member variable in spi_message
. complete is a callback function. This function will be called when the SPI asynchronous transmission is completed. The SPI asynchronous transfer
function is spi_async, and the function prototype is as follows:

int spi_async(struct spi_device *spi, struct spi_message *message)

The meanings of function parameters and return values ​​are as follows:
spi: spi_device for data transmission.
message: The spi_message to transmit.
Return value: None.
In the experiments in this chapter, we use the synchronous transmission method to complete the transmission of SPI data, that is, the spi_sync function.
To sum up, the steps of SPI data transmission are as follows:
① Apply for and initialize spi_transfer, set the tx_buf member variable of spi_transfer, tx_buf is the data to be sent
. Then set the rx_buf member variable, rx_buf saves the received data. Finally, set the len member variable, which is
the length of the data communication.
②. Use the spi_message_init function to initialize spi_message.
③. Use the spi_message_add_tail function to add the previously set spi_transfer to the spi_message queue.
④. Use the spi_sync function to complete the SPI data synchronous transmission.
The sample code for sending and receiving n bytes of data via SPI is as follows:

示例代码62.3.2.3 SPI 数据读写操作
    /* SPI多字节发送*/
static int spi_send(struct spi_device * spi, u8 * buf, int len) {
    
    
        int ret;
        struct spi_message m;
        struct spi_transfer t = {
    
    
            .tx_buf = buf,
                .len = len,
        };
        spi_message_init( & m); /* 初始化spi_message */
        spi_message_add_tail(t, & m); /* 将spi_transfer添加到spi_message队列*/
        ret = spi_sync(spi, & m); /* 同步传输*/
        return ret;
    }
    /* SPI多字节接收*/
static int spi_receive(struct spi_device * spi, u8 * buf, int len) {
    
    
    int ret;
    struct spi_message m;
    struct spi_transfer t = {
    
    
        .rx_buf = buf,
            .len = len,
    };
    spi_message_init( & m); /* 初始化spi_message */
    spi_message_add_tail(t, & m); /* 将spi_transfer添加到spi_message队列*/
    ret = spi_sync(spi, & m); /* 同步传输*/
    return ret;
}

Hardware Schematic Analysis

For the schematic diagram of the experimental hardware in this chapter, please refer to Section 26.2.

Test program writing

The routine path corresponding to this experiment is: development board CD -> 2, Linux driver routine -> 22_spi.

Modify the device tree

1. Add the IO used by ICM20608
First add the IO information used by ICM20608 in the imx6ull-alientek-emmc.dts file,
add a new child node in the iomuxc node to describe the SPI pin used by ICM20608, the name of the child node For pinctrl_ecspi3,
the content of the node is as follows:

示例代码62.5.1.1 icm20608 IO 节点信息
1 pinctrl_ecspi3: icm20608 {
    
    
2 fsl,pins = <
3 MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20 0x10b0 /* CS */
4 MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 0x10b1 /* SCLK */
5 MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO 0x10b1 /* MISO */
6 MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 0x10b1 /* MOSI */
7 >;
8 };

The UART2_TX_DATA IO is the chip select signal of ICM20608, here we do not multiplex it as the
SS0 signal of ECSPI3, but multiplex it as a common GPIO. Because we need to control the chip select signal by ourselves, it is multiplexed
as a common GPIO.
2. Add icm20608 sub-node to ecspi3 node
There is no code to add content to ecspi3 node in the imx6ull-alientek-emmc.dts file, because
the NXP official 6ULL ​​EVK development board is not connected to the SPI device.
Add the following content at the end of the imx6ull-alientek-emmc.dts file :

示例代码62.5.1.2 向ecspi3 节点加入icm20608 信息
1 &ecspi3 {
    
    
2 fsl,spi-num-chipselects = <1>;
3 cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;
4 pinctrl-names = "default";
5 pinctrl-0 = <&pinctrl_ecspi3>;
6 status = "okay";
7
8 spidev: icm20608@0 {
    
    
9 compatible = "alientek,icm20608";
10 spi-max-frequency = <8000000>;
11 reg = <0>;
12 };
13 };

In line 2, set the current number of chip selections to 1, because only one ICM20608 is connected.
In line 3, be sure to use the "cs-gpios" attribute to describe the chip select pin, and the SPI host driver will control the chip select pin.
Line 5 sets the pinctrl child node to be used by IO, which is the
pinctrl_ecspi3 we created in the sample code 62.5.1.1.
Line 6, in the imx6ull.dtsi file, the ecspi3 node status (status) is set to “disable” by default, here we need to
change it to “okay”.
Lines 8~12, icm20608 device child node, because icm20608 is connected to the 0th channel of ECSPI3, so
@ is followed by 0. Line 9 sets the node attribute compatible value to "alientek,icm20608", and line 10 sets the maximum SPI clock frequency
to 8MHz, which is the maximum clock frequency that the SPI interface of ICM20608 can support. In line 11, icm20608 is connected
on channel 0, so reg is 0.
Recompile the imx6ull-alientek-emmc.dts file after modification to get a new dtb file, and use the new
dtb to start the Linux system.

Write ICM20608 driver

Create a new folder named "22_spi", then create a vscode project in the 22_spi folder, and name the workspace
"spi". After the project is created, create two new files, icm20608.c and icm20608reg.h. icm20608.c is the
driver code of ICM20608, and icm20608reg.h is the header file of ICM20608 registers. First define the ICM20608
registers in icm20608reg.h, and enter the following content (some are omitted, please refer to the example for the complete content):

#ifndef ICM20608_H
#define ICM20608_H
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: icm20608reg.h
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: ICM20608寄存器地址描述头文件
其他	   	: 无
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/9/2 左忠凯创建
***************************************************************/
#define ICM20608G_ID			0XAF	/* ID值 */
#define ICM20608D_ID			0XAE	/* ID值 */

/* ICM20608寄存器 
 *复位后所有寄存器地址都为0,除了
 *Register 107(0X6B) Power Management 1 	= 0x40
 *Register 117(0X75) WHO_AM_I 				= 0xAF或0xAE
 */
/* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
#define	ICM20_SELF_TEST_X_GYRO		0x00
#define	ICM20_SELF_TEST_Y_GYRO		0x01
#define	ICM20_SELF_TEST_Z_GYRO		0x02
#define	ICM20_SELF_TEST_X_ACCEL		0x0D
#define	ICM20_SELF_TEST_Y_ACCEL		0x0E
#define	ICM20_SELF_TEST_Z_ACCEL		0x0F

/* 陀螺仪静态偏移 */
#define	ICM20_XG_OFFS_USRH			0x13
#define	ICM20_XG_OFFS_USRL			0x14
#define	ICM20_YG_OFFS_USRH			0x15
#define	ICM20_YG_OFFS_USRL			0x16
#define	ICM20_ZG_OFFS_USRH			0x17
#define	ICM20_ZG_OFFS_USRL			0x18

#define	ICM20_SMPLRT_DIV			0x19
#define	ICM20_CONFIG				0x1A
#define	ICM20_GYRO_CONFIG			0x1B
#define	ICM20_ACCEL_CONFIG			0x1C
#define	ICM20_ACCEL_CONFIG2			0x1D
#define	ICM20_LP_MODE_CFG			0x1E
#define	ICM20_ACCEL_WOM_THR			0x1F
#define	ICM20_FIFO_EN				0x23
#define	ICM20_FSYNC_INT				0x36
#define	ICM20_INT_PIN_CFG			0x37
#define	ICM20_INT_ENABLE			0x38
#define	ICM20_INT_STATUS			0x3A

/* 加速度输出 */
#define	ICM20_ACCEL_XOUT_H			0x3B
#define	ICM20_ACCEL_XOUT_L			0x3C
#define	ICM20_ACCEL_YOUT_H			0x3D
#define	ICM20_ACCEL_YOUT_L			0x3E
#define	ICM20_ACCEL_ZOUT_H			0x3F
#define	ICM20_ACCEL_ZOUT_L			0x40

/* 温度输出 */
#define	ICM20_TEMP_OUT_H			0x41
#define	ICM20_TEMP_OUT_L			0x42

/* 陀螺仪输出 */
#define	ICM20_GYRO_XOUT_H			0x43
#define	ICM20_GYRO_XOUT_L			0x44
#define	ICM20_GYRO_YOUT_H			0x45
#define	ICM20_GYRO_YOUT_L			0x46
#define	ICM20_GYRO_ZOUT_H			0x47
#define	ICM20_GYRO_ZOUT_L			0x48

#define	ICM20_SIGNAL_PATH_RESET		0x68
#define	ICM20_ACCEL_INTEL_CTRL 		0x69
#define	ICM20_USER_CTRL				0x6A
#define	ICM20_PWR_MGMT_1			0x6B
#define	ICM20_PWR_MGMT_2			0x6C
#define	ICM20_FIFO_COUNTH			0x72
#define	ICM20_FIFO_COUNTL			0x73
#define	ICM20_FIFO_R_W				0x74
#define	ICM20_WHO_AM_I 				0x75

/* 加速度静态偏移 */
#define	ICM20_XA_OFFSET_H			0x77
#define	ICM20_XA_OFFSET_L			0x78
#define	ICM20_YA_OFFSET_H			0x7A
#define	ICM20_YA_OFFSET_L			0x7B
#define	ICM20_ZA_OFFSET_H			0x7D
#define	ICM20_ZA_OFFSET_L 			0x7E


#endif


Next, continue to write the icm20608.c file, because the content of the icm20608.c file is relatively long, so it will be
explained separately here.
1. Creation of icm20608 device structure
First create an icm20608 device structure, as shown below:

示例代码62.5.2.2 icm20608 设备结构体创建
1 #include <linux/types.h>
2 #include <linux/kernel.h>
3 #include <linux/delay.h>
......
22 #include <asm/io.h>
23 #include "icm20608reg.h"
24 /***************************************************************
25 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
26 文件名: icm20608.c
27 作者: 左忠凯
28 版本: V1.0
29 描述: ICM20608 SPI驱动程序
30 其他: 无
31 论坛: www.openedv.com
32 日志: 初版V1.0 2019/9/2 左忠凯创建
33 ***************************************************************/
34 #define ICM20608_CNT 1
35 #define ICM20608_NAME "icm20608"
36
37 struct icm20608_dev {
    
    
38 dev_t devid; /* 设备号*/
39 struct cdev cdev; /* cdev */
40 struct class *class; /* 类*/
41 struct device *device; /* 设备*/
42 struct device_node *nd; /* 设备节点*/
43 int major; /* 主设备号*/
44 void *private_data; /* 私有数据*/
45 int cs_gpio; /* 片选所使用的GPIO编号*/
46 signed int gyro_x_adc; /* 陀螺仪X轴原始值*/
47 signed int gyro_y_adc; /* 陀螺仪Y轴原始值*/
48 signed int gyro_z_adc; /* 陀螺仪Z轴原始值*/
49 signed int accel_x_adc; /* 加速度计X轴原始值*/
50 signed int accel_y_adc; /* 加速度计Y轴原始值*/
51 signed int accel_z_adc; /* 加速度计Z轴原始值*/
52 signed int temp_adc; /* 温度原始值*/
53 };
54
55 static struct icm20608_dev icm20608dev;

The device structure icm20608_dev of icm20608 has nothing to say, focus on the private_data in line 44,
the core of the SPI device driver is spi_device. The probe function will provide the driver with the spi_device corresponding to the current SPI device
, so set private_data in the probe function as the spi_device parameter passed in by the probe function.
2. Registration and deregistration of spi_driver of icm20608
For SPI device driver, the first thing is to initialize and register spi_driver with the system. The codes of spi_driver initialization,
registration and deregistration of icm20608 are as follows:

示例代码62.5.2.3 icm20608 的spi_driver 初始化、注册与注销
1 /* 传统匹配方式ID列表*/
2 static const struct spi_device_id icm20608_id[] = {
    
    
3 {
    
    "alientek,icm20608", 0},
4 {
    
    }
5 };
6
7 /* 设备树匹配列表*/
8 static const struct of_device_id icm20608_of_match[] = {
    
    
9 {
    
     .compatible = "alientek,icm20608" },
10 {
    
     /* Sentinel */ }
11 };
12
13 /* SPI驱动结构体*/
14 static struct spi_driver icm20608_driver = {
    
    
15 .probe = icm20608_probe,
16 .remove = icm20608_remove,
17 .driver = {
    
    
18 .owner = THIS_MODULE,
19 .name = "icm20608",
20 .of_match_table = icm20608_of_match,
21 },
22 .id_table = icm20608_id,
23 };
24
25 /*
26 * @description : 驱动入口函数
27 * @param : 无
28 * @return : 无
29 */
30 static int __init icm20608_init(void)
31 {
    
    
32 return spi_register_driver(&icm20608_driver);
33 }
34
35 /*
36 * @description : 驱动出口函数
37 * @param : 无
38 * @return : 无
39 */
40 static void __exit icm20608_exit(void)
41 {
    
    
42 spi_unregister_driver(&icm20608_driver);
43 }
44
45 module_init(icm20608_init);
46 module_exit(icm20608_exit);
47 MODULE_LICENSE("GPL");
48 MODULE_AUTHOR("zuozhongkai");

Lines 2~5, the traditional device and driver matching table.
Lines 8~11, the device and driver matching table of the device tree, there is only one matching item here: "alientek,icm20608".
Lines 14~23, the spi_driver structure variable of icm20608, when the icm20608 device and this driver match successfully, the
icm20608_probe function in line 15 will be executed. Similarly, the icm20608_remove function will be executed when the driver is unregistered
.
Line 30~33, icm20608_init function is the driver entry function of icm20608, use spi_register_driver in this function to
register icm20608_driver defined above to Linux system.
In line 40~43, the icm20608_exit function is the driver exit function of icm20608. In this function,
spi_unregister_driver is used to unregister the previously registered icm20608_driver.
3. Probe&remove function
The contents of probe and remove functions in icm20608_driver are as follows:

示例代码62.5.2.4 probe 和remove 函数
1 /*
2 * @description : spi驱动的probe函数,当驱动与
3 * 设备匹配以后此函数就会执行
4 * @param - client : i2c设备
5 * @param - id : i2c设备ID
6 *
7 */
8 static int icm20608_probe(struct spi_device *spi)
9 {
    
    
10 /* 1、构建设备号*/
11 if (icm20608dev.major) {
    
    
12 icm20608dev.devid = MKDEV(icm20608dev.major, 0);
13 register_chrdev_region(icm20608dev.devid, ICM20608_CNT,
ICM20608_NAME);
14 } else {
    
    
15 alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT,
ICM20608_NAME);
16 icm20608dev.major = MAJOR(icm20608dev.devid);
17 }
18
19 /* 2、注册设备*/
20 cdev_init(&icm20608dev.cdev, &icm20608_ops);
21 cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);
22
23 /* 3、创建类*/
24 icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);
25 if (IS_ERR(icm20608dev.class)) {
    
    
26 return PTR_ERR(icm20608dev.class);
27 }
28
29 /* 4、创建设备*/
30 icm20608dev.device = device_create(icm20608dev.class, NULL,
icm20608dev.devid, NULL, ICM20608_NAME);
31 if (IS_ERR(icm20608dev.device)) {
    
    
32 return PTR_ERR(icm20608dev.device);
33 }
34
35 /*初始化spi_device */
36 spi->mode = SPI_MODE_0; /*MODE0,CPOL=0,CPHA=0*/
37 spi_setup(spi);
38 icm20608dev.private_data = spi; /* 设置私有数据*/
39
40 /* 初始化ICM20608内部寄存器*/
41 icm20608_reginit();
42 return 0;
43 }
44
45 /*
46 * @description : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行
47 * @param - client : i2c设备
48 * @return : 0,成功;其他负值,失败
49 */
50 static int icm20608_remove(struct spi_device *spi)
51 {
    
    
52 /* 删除设备*/
53 cdev_del(&icm20608dev.cdev);
54 unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);
55
56 /* 注销掉类和设备*/
57 device_destroy(icm20608dev.class, icm20608dev.devid);
58 class_destroy(icm20608dev.class);
59 return 0;
60 }

Lines 8~43 are the probe function. This function will be executed when the device and driver are successfully matched. Lines 10~33 are standard
registered character device drivers.
Line 36, set SPI to mode 0, that is, CPOL=0, CPHA=0.
Line 37, after setting spi_device, you need to use spi_setup to configure it.
Line 38, set the private_data member variable of icm20608dev to spi_device.
Line 41, call the icm20608_reginit function to initialize ICM20608, mainly to initialize the designated register of ICM20608
.
Lines 50~60, icm20608_remove function, this function will be executed when the driver is canceled.
4. icm20608 register reading and writing and initialization
The SPI driver is finally realized by reading and writing icm20608 registers, so it is necessary to write corresponding register reading and writing functions
, and use these reading and writing functions to complete the initialization of icm20608. The register reading and writing and initialization
code of icm20608 are as follows:

示例代码62.5.2.5 icm20608 寄存器读写以及出初始化
1 /*
2 * @description : 从icm20608读取多个寄存器数据
3 * @param – dev : icm20608设备
4 * @param – reg : 要读取的寄存器首地址
5 * @param – val : 读取到的数据
6 * @param – len : 要读取的数据长度
7 * @return : 操作结果
8 */
9 static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg,
void *buf, int len)
10 {
    
    
11
12 int ret = -1;
13 unsigned char txdata[1];
14 unsigned char * rxdata;
15 struct spi_message m;
16 struct spi_transfer *t;
17 struct spi_device *spi = (struct spi_device *)dev->private_data;
18
19 t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
20 if(!t) {
    
    
21 return -ENOMEM;
22 }
23
24 rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL); /* 申请内存*/
25 if(!rxdata) {
    
    
26 goto out1;
27 }
28
29 /* 一共发送len+1个字节的数据,第一个字节为
30 寄存器首地址,一共要读取len个字节长度的数据,*/
31 txdata[0] = reg | 0x80; /* 写数据的时候首寄存器地址bit8要置1 */
32 t->tx_buf = txdata; /* 要发送的数据*/
33 t->rx_buf = rxdata; /* 要读取的数据*/
34 t->len = len+1; /* t->len=发送的长度+读取的长度*/
35 spi_message_init(&m); /* 初始化spi_message */
36 spi_message_add_tail(t, &m);
37 ret = spi_sync(spi, &m); /* 同步发送*/
38 if(ret) {
    
    
39 goto out2;
40 }
41
42 memcpy(buf , rxdata+1, len); /* 只需要读取的数据*/
43
44 out2:
45 kfree(rxdata); /* 释放内存*/
46 out1:
47 kfree(t); /* 释放内存*/
48
49 return ret;
50 }
51
52 /*
53 * @description : 向icm20608多个寄存器写入数据
54 * @param – dev : icm20608设备
55 * @param – reg : 要写入的寄存器首地址
56 * @param – val : 要写入的数据缓冲区
57 * @param – len : 要写入的数据长度
58 * @return : 操作结果
59 */
60 static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg,
u8 *buf, u8 len)
61 {
    
    
62 int ret = -1;
63 unsigned char *txdata;
64 struct spi_message m;
65 struct spi_transfer *t;
66 struct spi_device *spi = (struct spi_device *)dev->private_data;
67
68 t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
69 if(!t) {
    
    
70 return -ENOMEM;
71 }
72
73 txdata = kzalloc(sizeof(char)+len, GFP_KERNEL);
74 if(!txdata) {
    
    
75 goto out1;
76 }
77
78 /* 一共发送len+1个字节的数据,第一个字节为
79 寄存器首地址,len为要写入的寄存器的集合,*/
80 *txdata = reg & ~0x80; /* 写数据的时候首寄存器地址bit8要清零*/
81 memcpy(txdata+1, buf, len); /* 把len个寄存器拷贝到txdata里*/
82 t->tx_buf = txdata; /* 要发送的数据*/
83 t->len = len+1; /* t->len=发送的长度+读取的长度*/
84 spi_message_init(&m); /* 初始化spi_message */
85 spi_message_add_tail(t, &m);
86 ret = spi_sync(spi, &m); /* 同步发送*/
87 if(ret) {
    
    
88 goto out2;
89 }
90
91 out2:
92 kfree(txdata); /* 释放内存*/
93 out1:
94 kfree(t); /* 释放内存*/
95 return ret;
96 }
97
98 /*
99 * @description : 读取icm20608指定寄存器值,读取一个寄存器
100 * @param – dev : icm20608设备
101 * @param – reg : 要读取的寄存器
102 * @return : 读取到的寄存器值
103 */
104 static unsigned char icm20608_read_onereg(struct icm20608_dev *dev,
u8 reg)
105 {
    
    
106 u8 data = 0;
107 icm20608_read_regs(dev, reg, &data, 1);
108 return data;
109 }
110
111 /*
112 * @description : 向icm20608指定寄存器写入指定的值,写一个寄存器
113 * @param – dev : icm20608设备
114 * @param – reg : 要写的寄存器
115 * @param – data : 要写入的值
116 * @return : 无
117 */
118
119 static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
120 {
    
    
121 u8 buf = value;
122 icm20608_write_regs(dev, reg, &buf, 1);
123 }
124
125 /*
126 * @description : 读取ICM20608的数据,读取原始数据,包括三轴陀螺仪、
127 * : 三轴加速度计和内部温度。
128 * @param - dev : ICM20608设备
129 * @return : 无。
130 */
131 void icm20608_readdata(struct icm20608_dev *dev)
132 {
    
    
133 unsigned char data[14] = {
    
     0 };
134 icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);
135
136 dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]);
137 dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]);
138 dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]);
139 dev->temp_adc = (signed short)((data[6] << 8) | data[7]);
140 dev->gyro_x_adc = (signed short)((data[8] << 8) | data[9]);
141 dev->gyro_y_adc = (signed short)((data[10] << 8) | data[11]);
142 dev->gyro_z_adc = (signed short)((data[12] << 8) | data[13]);
143 }
144
145 /*
146 * ICM20608内部寄存器初始化函数
147 * @param : 无
148 * @return : 无
149 */
150 void icm20608_reginit(void)
151 {
    
    
152 u8 value = 0;
153
154 icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x80);
155 mdelay(50);
156 icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x01);
157 mdelay(50);
158
159 value = icm20608_read_onereg(&icm20608dev, ICM20_WHO_AM_I);
160 printk("ICM20608 ID = %#X\r\n", value);
161
162 icm20608_write_onereg(&icm20608dev, ICM20_SMPLRT_DIV, 0x00);
163 icm20608_write_onereg(&icm20608dev, ICM20_GYRO_CONFIG, 0x18);
164 icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG, 0x18);
165 icm20608_write_onereg(&icm20608dev, ICM20_CONFIG, 0x04);
166 icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG2, 0x04);
167 icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_2, 0x00);
168 icm20608_write_onereg(&icm20608dev, ICM20_LP_MODE_CFG, 0x00);
169 icm20608_write_onereg(&icm20608dev, ICM20_FIFO_EN, 0x00);
170 }

Lines 9~50, the icm20608_read_regs function, reads multiple consecutive register data from the icm20608; Note:
In this experiment, SPI is full-duplex communication without the so-called length of sending and receiving. To read or send N bytes
, N+1 bytes must be encapsulated. The first byte tells the device whether we want to read or write, and the next N bytes are the
data we want to read or send. Because it is a read operation, set the first data bit7 to 1 in line 31, indicating a read operation.
Lines 60~96, icm20608_write_regs function, continuously write multiple register data to icm20608. There is little difference between this function and
icm20608_read_regs function.
Lines 104~109, icm20608_read_onereg function, reads the specified register data of icm20608.
Lines 119~123, icm20608_write_onereg function, write data to the specified register of icm20608.
Lines 131~143, the icm20608_readdata function, reads the original data values ​​of the icm20608 six-axis sensor and the temperature sensor
, and the raw data of these sensors will be reported to the application program when the application program reads the icm20608.
Lines 150~170, the icm20608_reginit function, initializes icm20608, which is the same as the initialization process in our spi bare metal experiment
.
5. Character device driver framework
The character device driver framework of icm20608 is as follows:

示例代码62.5.2.6 icm20608 字符设备驱动
1 /*
2 * @description : 打开设备
3 * @param – inode : 传递给驱动的inode
4 * @param - filp : 设备文件,file结构体有个叫做pr似有ate_data的成员变量
5 * 一般在open的时候将private_data似有向设备结构体。
6 * @return : 0 成功;其他失败
7 */
8 static int icm20608_open(struct inode *inode, struct file *filp)
9 {
    
    
10 filp->private_data = &icm20608dev; /* 设置私有数据*/
11 return 0;
12 }
13
14 /*
15 * @description : 从设备读取数据
16 * @param - filp : 要打开的设备文件(文件描述符)
17 * @param - buf : 返回给用户空间的数据缓冲区
18 * @param - cnt : 要读取的数据长度
19 * @param - offt : 相对于文件首地址的偏移
20 * @return : 读取的字节数,如果为负值,表示读取失败
21 */
22 static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
23 {
    
    
24 signed int data[7];
25 long err = 0;
26 struct icm20608_dev *dev = (struct icm20608_dev *
)filp->private_data;
27
28 icm20608_readdata(dev);
29 data[0] = dev->gyro_x_adc;
30 data[1] = dev->gyro_y_adc;
31 data[2] = dev->gyro_z_adc;
32 data[3] = dev->accel_x_adc;
33 data[4] = dev->accel_y_adc;
34 data[5] = dev->accel_z_adc;
35 data[6] = dev->temp_adc;
36 err = copy_to_user(buf, data, sizeof(data));
37 return 0;
38 }
39
40 /*
41 * @description : 关闭/释放设备
42 * @param - filp : 要关闭的设备文件(文件描述符)
43 * @return : 0 成功;其他失败
44 */
45 static int icm20608_release(struct inode *inode, struct file *filp)
46 {
    
    
47 return 0;
48 }
49
50 /* icm20608操作函数*/
51 static const struct file_operations icm20608_ops = {
    
    
52 .owner = THIS_MODULE,
53 .open = icm20608_open,
54 .read = icm20608_read,
55 .release = icm20608_release,
56 };

There is nothing to say about the character device driver framework. The key point is the icm20608_read function in lines 22~38.
This function will be executed when the application program calls the read function to read the icm20608 device file.
This function calls the icm20608_readdata function written above to read the original data of icm20608 and report it to the application program. Attention everyone,
try not to use floating-point calculations in the kernel, so don't convert the original value of icm20608 to the corresponding actual value in the driver, because
floating-point calculations will be involved.

Write a test APP

Create a new icm20608App.c file, and enter the following content in it:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: icm20608App.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: icm20608设备测试APP。
其他	   	: 无
使用方法	 :./icm20608App /dev/icm20608
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/9/20 左忠凯创建
***************************************************************/

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
    
    
	int fd;
	char *filename;
	signed int databuf[7];
	unsigned char data[14];
	signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;
	signed int accel_x_adc, accel_y_adc, accel_z_adc;
	signed int temp_adc;

	float gyro_x_act, gyro_y_act, gyro_z_act;
	float accel_x_act, accel_y_act, accel_z_act;
	float temp_act;

	int ret = 0;

	if (argc != 2) {
    
    
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];
	fd = open(filename, O_RDWR);
	if(fd < 0) {
    
    
		printf("can't open file %s\r\n", filename);
		return -1;
	}

	while (1) {
    
    
		ret = read(fd, databuf, sizeof(databuf));
		if(ret == 0) {
    
     			/* 数据读取成功 */
			gyro_x_adc = databuf[0];
			gyro_y_adc = databuf[1];
			gyro_z_adc = databuf[2];
			accel_x_adc = databuf[3];
			accel_y_adc = databuf[4];
			accel_z_adc = databuf[5];
			temp_adc = databuf[6];

			/* 计算实际值 */
			gyro_x_act = (float)(gyro_x_adc)  / 16.4;
			gyro_y_act = (float)(gyro_y_adc)  / 16.4;
			gyro_z_act = (float)(gyro_z_adc)  / 16.4;
			accel_x_act = (float)(accel_x_adc) / 2048;
			accel_y_act = (float)(accel_y_adc) / 2048;
			accel_z_act = (float)(accel_z_adc) / 2048;
			temp_act = ((float)(temp_adc) - 25 ) / 326.8 + 25;


			printf("\r\n原始值:\r\n");
			printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc, gyro_y_adc, gyro_z_adc);
			printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc, accel_y_adc, accel_z_adc);
			printf("temp = %d\r\n", temp_adc);
			printf("实际值:");
			printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", gyro_x_act, gyro_y_act, gyro_z_act);
			printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", accel_x_act, accel_y_act, accel_z_act);
			printf("act temp = %.2f°C\r\n", temp_act);
		}
		usleep(100000); /*100ms */
	}
	close(fd);	/* 关闭文件 */	
	return 0;
}


Lines 60~91, in the while loop, read data from icm20608 every 100ms, and convert the original data of icm20608
into actual values, such as the angular velocity of the gyroscope, and the g value of the accelerometer. Note that we
have set the measurement ranges of the gyroscope and accelerometer to the maximum in the icm20608 driver, which are ±2000 and ±16g respectively.
So, when calculating the actual value, use 16.4 for the gyroscope and 2048 for the accelerometer. Finally, the sensor raw data and the obtained
actual value are displayed on the terminal.

run test

Compile the driver and test the APP

1. Compile the driver and
write the Makefile. The Makefile of the experiment in this chapter is basically the same as the experiment in Chapter 40, except that
the value of the obj-m variable is changed to "icm20608.o". The content of the Makefile is as follows:

示例代码62.6.1.1 Makefile 文件
1 KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
......
4 obj-m := icm20608.o
......
11 clean:
12 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

Line 4, set the value of the obj-m variable to "icm20608.o".
Enter the following command to compile the driver module file:

make -j32

After the compilation is successful, a driver module file named "icm20608.ko" will be generated.
2. Compile and test APP
In the test APP of icm20608App.c, we use floating-point calculation, and I.MX6U supports hardware floating-point,
so we can enable hardware floating-point when compiling icm20608App.c, so that Accelerated floating-point calculations. It is very simple to enable hardware
floating point, just add the following parameters when compiling:

-march-armv7-a -mfpu-neon -mfloat=hard

Enter the following command to enable hardware floating point to compile the test program icm20608App.c:

arm-linux-gnueabihf-gcc -march=armv7-a -mfpu=neon -mfloat-abi=hard icm20608App.c -o
icm20608App

After the compilation is successful, the application program icm20608App will be generated, so is there any use of hardware floating point? Use
arm-linux-gnueabihf-readelf to check the compiled icm20608App and you will know, enter the following command:

arm-linux-gnueabihf-readelf -A icm20608App

The result is shown in Figure 62.6.1.1:
insert image description here
From Figure 62.6.1.1 we can see that the FPU architecture is VFPv3, SIMD uses NEON, and uses SP and DP,
indicating that the application program icm20608App uses hardware floating point.

run test

Copy the two files icm20608.ko and icm20608App compiled in the previous section to
the rootfs/lib/modules/4.1.15 directory, restart the development board, and enter the directory lib/modules/4.1.15. Enter the following command
to load the driver module icm20608.ko.

depmod //第一次加载驱动的时候需要运行此命令
modprobe icm20608.ko //加载驱动模块

After the driver module is loaded successfully, use icm20608App to test, and enter the following command:

./icm20608App /dev/icm20608

The test APP will continuously read data from ICM20608, and then output it to the terminal, as shown in Figure 62.6.2.1:
insert image description here
It can be seen that when the development board is in a static state, the acceleration in the Z-axis direction is about 1g, which is the acceleration of gravity. For
the gyroscope, the angular velocity of the three axes should be around 0°/S in the static state. The temperature collected by the temperature sensor inside the ICM20608
is about 30 degrees. You can shake the development board, and the values ​​of the gyroscope and accelerometer will change at this time.

Guess you like

Origin blog.csdn.net/zhuguanlin121/article/details/130630012