• i.MX 6ULL 驱动开发 二十二:SPI(SPI子系统+MISC子系统)


    一、概述

    SPI 子系统:作用是完成驱动和设备的匹配。

    MISC 子系统:作用是简化字符设备注册过程。

    二、SPI 协议

    一文搞懂SPI通信协议_不脱发的程序猿的博客-CSDN博客_spi通讯流程

    三、Linux SPI 驱动框架中重要对象

    1、SPI 总线

    struct bus_type spi_bus_type = {
    	.name		= "spi",
    	.dev_groups	= spi_dev_groups,
    	.match		= spi_match_device,
    	.uevent		= spi_uevent,
    };
    EXPORT_SYMBOL_GPL(spi_bus_type);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    SPI 总线完成 SPI 设备和 SPI 驱动的匹配,主要作用是设备和驱动分离。

    2、SPI 控制器驱动

    /**
     * struct spi_master - interface to SPI master controller
     * @dev: device interface to this driver
     * @list: link with the global spi_master list
     * @bus_num: board-specific (and often SOC-specific) identifier for a
     *	given SPI controller.
     * @num_chipselect: chipselects are used to distinguish individual
     *	SPI slaves, and are numbered from zero to num_chipselects.
     *	each slave has a chipselect signal, but it's common that not
     *	every chipselect is connected to a slave.
     * @dma_alignment: SPI controller constraint on DMA buffers alignment.
     * @mode_bits: flags understood by this controller driver
     * @bits_per_word_mask: A mask indicating which values of bits_per_word are
     *	supported by the driver. Bit n indicates that a bits_per_word n+1 is
     *	supported. If set, the SPI core will reject any transfer with an
     *	unsupported bits_per_word. If not set, this value is simply ignored,
     *	and it's up to the individual driver to perform any validation.
     * @min_speed_hz: Lowest supported transfer speed
     * @max_speed_hz: Highest supported transfer speed
     * @flags: other constraints relevant to this driver
     * @bus_lock_spinlock: spinlock for SPI bus locking
     * @bus_lock_mutex: mutex for SPI bus locking
     * @bus_lock_flag: indicates that the SPI bus is locked for exclusive use
     * @setup: updates the device mode and clocking records used by a
     *	device's SPI controller; protocol code may call this.  This
     *	must fail if an unrecognized or unsupported mode is requested.
     *	It's always safe to call this unless transfers are pending on
     *	the device whose settings are being modified.
     * @transfer: adds a message to the controller's transfer queue.
     * @cleanup: frees controller-specific state
     * @can_dma: determine whether this master supports DMA
     * @queued: whether this master is providing an internal message queue
     * @kworker: thread struct for message pump
     * @kworker_task: pointer to task for message pump kworker thread
     * @pump_messages: work struct for scheduling work to the message pump
     * @queue_lock: spinlock to syncronise access to message queue
     * @queue: message queue
     * @idling: the device is entering idle state
     * @cur_msg: the currently in-flight message
     * @cur_msg_prepared: spi_prepare_message was called for the currently
     *                    in-flight message
     * @cur_msg_mapped: message has been mapped for DMA
     * @xfer_completion: used by core transfer_one_message()
     * @busy: message pump is busy
     * @running: message pump is running
     * @rt: whether this queue is set to run as a realtime task
     * @auto_runtime_pm: the core should ensure a runtime PM reference is held
     *                   while the hardware is prepared, using the parent
     *                   device for the spidev
     * @max_dma_len: Maximum length of a DMA transfer for the device.
     * @prepare_transfer_hardware: a message will soon arrive from the queue
     *	so the subsystem requests the driver to prepare the transfer hardware
     *	by issuing this call
     * @transfer_one_message: the subsystem calls the driver to transfer a single
     *	message while queuing transfers that arrive in the meantime. When the
     *	driver is finished with this message, it must call
     *	spi_finalize_current_message() so the subsystem can issue the next
     *	message
     * @unprepare_transfer_hardware: there are currently no more messages on the
     *	queue so the subsystem notifies the driver that it may relax the
     *	hardware by issuing this call
     * @set_cs: set the logic level of the chip select line.  May be called
     *          from interrupt context.
     * @prepare_message: set up the controller to transfer a single message,
     *                   for example doing DMA mapping.  Called from threaded
     *                   context.
     * @transfer_one: transfer a single spi_transfer.
     *                  - return 0 if the transfer is finished,
     *                  - return 1 if the transfer is still in progress. When
     *                    the driver is finished with this transfer it must
     *                    call spi_finalize_current_transfer() so the subsystem
     *                    can issue the next transfer. Note: transfer_one and
     *                    transfer_one_message are mutually exclusive; when both
     *                    are set, the generic subsystem does not call your
     *                    transfer_one callback.
     * @handle_err: the subsystem calls the driver to handle an error that occurs
     *		in the generic implementation of transfer_one_message().
     * @unprepare_message: undo any work done by prepare_message().
     * @cs_gpios: Array of GPIOs to use as chip select lines; one per CS
     *	number. Any individual value may be -ENOENT for CS lines that
     *	are not GPIOs (driven by the SPI controller itself).
     * @dma_tx: DMA transmit channel
     * @dma_rx: DMA receive channel
     * @dummy_rx: dummy receive buffer for full-duplex devices
     * @dummy_tx: dummy transmit buffer for full-duplex devices
     *
     * Each SPI master controller can communicate with one or more @spi_device
     * children.  These make a small bus, sharing MOSI, MISO and SCK signals
     * but not chip select signals.  Each device may be configured to use a
     * different clock rate, since those shared signals are ignored unless
     * the chip is selected.
     *
     * The driver for an SPI controller manages access to those devices through
     * a queue of spi_message transactions, copying data between CPU memory and
     * an SPI slave device.  For each such message it queues, it calls the
     * message's completion function when the transaction completes.
     */
    struct spi_master {
    	struct device	dev;
    
    	struct list_head list;
    
    	/* other than negative (== assign one dynamically), bus_num is fully
    	 * board-specific.  usually that simplifies to being SOC-specific.
    	 * example:  one SOC has three SPI controllers, numbered 0..2,
    	 * and one board's schematics might show it using SPI-2.  software
    	 * would normally use bus_num=2 for that controller.
    	 */
    	s16			bus_num;
    
    	/* chipselects will be integral to many controllers; some others
    	 * might use board-specific GPIOs.
    	 */
    	u16			num_chipselect;
    
    	/* some SPI controllers pose alignment requirements on DMAable
    	 * buffers; let protocol drivers know about these requirements.
    	 */
    	u16			dma_alignment;
    
    	/* spi_device.mode flags understood by this controller driver */
    	u16			mode_bits;
    
    	/* bitmask of supported bits_per_word for transfers */
    	u32			bits_per_word_mask;
    #define SPI_BPW_MASK(bits) BIT((bits) - 1)
    #define SPI_BIT_MASK(bits) (((bits) == 32) ? ~0U : (BIT(bits) - 1))
    #define SPI_BPW_RANGE_MASK(min, max) (SPI_BIT_MASK(max) - SPI_BIT_MASK(min - 1))
    
    	/* limits on transfer speed */
    	u32			min_speed_hz;
    	u32			max_speed_hz;
    
    	/* other constraints relevant to this driver */
    	u16			flags;
    #define SPI_MASTER_HALF_DUPLEX	BIT(0)		/* can't do full duplex */
    #define SPI_MASTER_NO_RX	BIT(1)		/* can't do buffer read */
    #define SPI_MASTER_NO_TX	BIT(2)		/* can't do buffer write */
    #define SPI_MASTER_MUST_RX      BIT(3)		/* requires rx */
    #define SPI_MASTER_MUST_TX      BIT(4)		/* requires tx */
    
    	/* lock and mutex for SPI bus locking */
    	spinlock_t		bus_lock_spinlock;
    	struct mutex		bus_lock_mutex;
    
    	/* flag indicating that the SPI bus is locked for exclusive use */
    	bool			bus_lock_flag;
    
    	/* Setup mode and clock, etc (spi driver may call many times).
    	 *
    	 * IMPORTANT:  this may be called when transfers to another
    	 * device are active.  DO NOT UPDATE SHARED REGISTERS in ways
    	 * which could break those transfers.
    	 */
    	int			(*setup)(struct spi_device *spi);
    
    	/* bidirectional bulk transfers
    	 *
    	 * + The transfer() method may not sleep; its main role is
    	 *   just to add the message to the queue.
    	 * + For now there's no remove-from-queue operation, or
    	 *   any other request management
    	 * + To a given spi_device, message queueing is pure fifo
    	 *
    	 * + The master's main job is to process its message queue,
    	 *   selecting a chip then transferring data
    	 * + If there are multiple spi_device children, the i/o queue
    	 *   arbitration algorithm is unspecified (round robin, fifo,
    	 *   priority, reservations, preemption, etc)
    	 *
    	 * + Chipselect stays active during the entire message
    	 *   (unless modified by spi_transfer.cs_change != 0).
    	 * + The message transfers use clock and SPI mode parameters
    	 *   previously established by setup() for this device
    	 */
    	int			(*transfer)(struct spi_device *spi,
    						struct spi_message *mesg);
    
    	/* called on release() to free memory provided by spi_master */
    	void			(*cleanup)(struct spi_device *spi);
    
    	/*
    	 * Used to enable core support for DMA handling, if can_dma()
    	 * exists and returns true then the transfer will be mapped
    	 * prior to transfer_one() being called.  The driver should
    	 * not modify or store xfer and dma_tx and dma_rx must be set
    	 * while the device is prepared.
    	 */
    	bool			(*can_dma)(struct spi_master *master,
    					   struct spi_device *spi,
    					   struct spi_transfer *xfer);
    
    	/*
    	 * These hooks are for drivers that want to use the generic
    	 * master transfer queueing mechanism. If these are used, the
    	 * transfer() function above must NOT be specified by the driver.
    	 * Over time we expect SPI drivers to be phased over to this API.
    	 */
    	bool				queued;
    	struct kthread_worker		kworker;
    	struct task_struct		*kworker_task;
    	struct kthread_work		pump_messages;
    	spinlock_t			queue_lock;
    	struct list_head		queue;
    	struct spi_message		*cur_msg;
    	bool				idling;
    	bool				busy;
    	bool				running;
    	bool				rt;
    	bool				auto_runtime_pm;
    	bool                            cur_msg_prepared;
    	bool				cur_msg_mapped;
    	struct completion               xfer_completion;
    	size_t				max_dma_len;
    
    	int (*prepare_transfer_hardware)(struct spi_master *master);
    	int (*transfer_one_message)(struct spi_master *master,
    				    struct spi_message *mesg);
    	int (*unprepare_transfer_hardware)(struct spi_master *master);
    	int (*prepare_message)(struct spi_master *master,
    			       struct spi_message *message);
    	int (*unprepare_message)(struct spi_master *master,
    				 struct spi_message *message);
    
    	/*
    	 * These hooks are for drivers that use a generic implementation
    	 * of transfer_one_message() provied by the core.
    	 */
    	void (*set_cs)(struct spi_device *spi, bool enable);
    	int (*transfer_one)(struct spi_master *master, struct spi_device *spi,
    			    struct spi_transfer *transfer);
    	void (*handle_err)(struct spi_master *master,
    			   struct spi_message *message);
    
    	/* gpio chip select */
    	int			*cs_gpios;
    
    	/* DMA channels for use with core dmaengine helpers */
    	struct dma_chan		*dma_tx;
    	struct dma_chan		*dma_rx;
    
    	/* dummy data for full duplex devices */
    	void			*dummy_rx;
    	void			*dummy_tx;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245

    struct spi_master 是对 SOC 集成的 SPI 控制器的驱动抽象,不同 SOCSPI 控制器驱动实现有所不同。

    3、SPI 驱动

    /**
     * struct spi_driver - Host side "protocol" driver
     * @id_table: List of SPI devices supported by this driver
     * @probe: Binds this driver to the spi device.  Drivers can verify
     *	that the device is actually present, and may need to configure
     *	characteristics (such as bits_per_word) which weren't needed for
     *	the initial configuration done during system setup.
     * @remove: Unbinds this driver from the spi device
     * @shutdown: Standard shutdown callback used during system state
     *	transitions such as powerdown/halt and kexec
     * @driver: SPI device drivers should initialize the name and owner
     *	field of this structure.
     *
     * This represents the kind of device driver that uses SPI messages to
     * interact with the hardware at the other end of a SPI link.  It's called
     * a "protocol" driver because it works through messages rather than talking
     * directly to SPI hardware (which is what the underlying SPI controller
     * driver does to pass those messages).  These protocols are defined in the
     * specification for the device(s) supported by the driver.
     *
     * As a rule, those device protocols represent the lowest level interface
     * supported by a driver, and it will support upper level interfaces too.
     * Examples of such upper levels include frameworks like MTD, networking,
     * MMC, RTC, filesystem character device nodes, and hardware monitoring.
     */
    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);
    	struct device_driver	driver;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    struct spi_driver 是对具体设备驱动的抽象,用于控制 SPI 控制器上连接的设备。例如,控制 icm20608 设备。

    4、SPI 设备

    /**
     * struct spi_device - Master side proxy for an SPI slave device
     * @dev: Driver model representation of the device.
     * @master: SPI controller used with the device.
     * @max_speed_hz: Maximum clock rate to be used with this chip
     *	(on this board); may be changed by the device's driver.
     *	The spi_transfer.speed_hz can override this for each transfer.
     * @chip_select: Chipselect, distinguishing chips handled by @master.
     * @mode: The spi mode defines how data is clocked out and in.
     *	This may be changed by the device's driver.
     *	The "active low" default for chipselect mode can be overridden
     *	(by specifying SPI_CS_HIGH) as can the "MSB first" default for
     *	each word in a transfer (by specifying SPI_LSB_FIRST).
     * @bits_per_word: Data transfers involve one or more words; word sizes
     *	like eight or 12 bits are common.  In-memory wordsizes are
     *	powers of two bytes (e.g. 20 bit samples use 32 bits).
     *	This may be changed by the device's driver, or left at the
     *	default (0) indicating protocol words are eight bit bytes.
     *	The spi_transfer.bits_per_word can override this for each transfer.
     * @irq: Negative, or the number passed to request_irq() to receive
     *	interrupts from this device.
     * @controller_state: Controller's runtime state
     * @controller_data: Board-specific definitions for controller, such as
     *	FIFO initialization parameters; from board_info.controller_data
     * @modalias: Name of the driver to use with this device, or an alias
     *	for that name.  This appears in the sysfs "modalias" attribute
     *	for driver coldplugging, and in uevents used for hotplugging
     * @cs_gpio: gpio number of the chipselect line (optional, -ENOENT when
     *	when not using a GPIO line)
     *
     * A @spi_device is used to interchange data between an SPI slave
     * (usually a discrete chip) and CPU memory.
     *
     * In @dev, the platform_data is used to hold information about this
     * device that's meaningful to the device's protocol driver, but not
     * to its controller.  One example might be an identifier for a chip
     * variant with slightly different functionality; another might be
     * information about how this particular board wires the chip's pins.
     */
    struct spi_device {
    	struct device		dev;
    	struct spi_master	*master;
    	u32			max_speed_hz;
    	u8			chip_select;
    	u8			bits_per_word;
    	u16			mode;
    #define	SPI_CPHA	0x01			/* clock phase */
    #define	SPI_CPOL	0x02			/* clock polarity */
    #define	SPI_MODE_0	(0|0)			/* (original MicroWire) */
    #define	SPI_MODE_1	(0|SPI_CPHA)
    #define	SPI_MODE_2	(SPI_CPOL|0)
    #define	SPI_MODE_3	(SPI_CPOL|SPI_CPHA)
    #define	SPI_CS_HIGH	0x04			/* chipselect active high? */
    #define	SPI_LSB_FIRST	0x08			/* per-word bits-on-wire */
    #define	SPI_3WIRE	0x10			/* SI/SO signals shared */
    #define	SPI_LOOP	0x20			/* loopback mode */
    #define	SPI_NO_CS	0x40			/* 1 dev/bus, no chipselect */
    #define	SPI_READY	0x80			/* slave pulls low to pause */
    #define	SPI_TX_DUAL	0x100			/* transmit with 2 wires */
    #define	SPI_TX_QUAD	0x200			/* transmit with 4 wires */
    #define	SPI_RX_DUAL	0x400			/* receive with 2 wires */
    #define	SPI_RX_QUAD	0x800			/* receive with 4 wires */
    	int			irq;
    	void			*controller_state;
    	void			*controller_data;
    	char			modalias[SPI_NAME_SIZE];
    	int			cs_gpio;	/* chip select gpio */
    
    	/*
    	 * likely need more hooks for more protocol options affecting how
    	 * the controller talks to each chip, like:
    	 *  - memory packing (12 bit samples into low bits, others zeroed)
    	 *  - priority
    	 *  - drop chipselect after each word
    	 *  - chipselect delays
    	 *  - ...
    	 */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    struct spi_deviceLinux 内核对 SPI 设备的抽象,

    5、SPI 一次数据传输

    /**
     * struct spi_transfer - a read/write buffer pair
     * @tx_buf: data to be written (dma-safe memory), or NULL
     * @rx_buf: data to be read (dma-safe memory), or NULL
     * @tx_dma: DMA address of tx_buf, if @spi_message.is_dma_mapped
     * @rx_dma: DMA address of rx_buf, if @spi_message.is_dma_mapped
     * @tx_nbits: number of bits used for writing. If 0 the default
     *      (SPI_NBITS_SINGLE) is used.
     * @rx_nbits: number of bits used for reading. If 0 the default
     *      (SPI_NBITS_SINGLE) is used.
     * @len: size of rx and tx buffers (in bytes)
     * @speed_hz: Select a speed other than the device default for this
     *      transfer. If 0 the default (from @spi_device) is used.
     * @bits_per_word: select a bits_per_word other than the device default
     *      for this transfer. If 0 the default (from @spi_device) is used.
     * @cs_change: affects chipselect after this transfer completes
     * @delay_usecs: microseconds to delay after this transfer before
     *	(optionally) changing the chipselect status, then starting
     *	the next transfer or completing this @spi_message.
     * @transfer_list: transfers are sequenced through @spi_message.transfers
     * @tx_sg: Scatterlist for transmit, currently not for client use
     * @rx_sg: Scatterlist for receive, currently not for client use
     *
     * SPI transfers always write the same number of bytes as they read.
     * Protocol drivers should always provide @rx_buf and/or @tx_buf.
     * In some cases, they may also want to provide DMA addresses for
     * the data being transferred; that may reduce overhead, when the
     * underlying driver uses dma.
     *
     * If the transmit buffer is null, zeroes will be shifted out
     * while filling @rx_buf.  If the receive buffer is null, the data
     * shifted in will be discarded.  Only "len" bytes shift out (or in).
     * It's an error to try to shift out a partial word.  (For example, by
     * shifting out three bytes with word size of sixteen or twenty bits;
     * the former uses two bytes per word, the latter uses four bytes.)
     *
     * In-memory data values are always in native CPU byte order, translated
     * from the wire byte order (big-endian except with SPI_LSB_FIRST).  So
     * for example when bits_per_word is sixteen, buffers are 2N bytes long
     * (@len = 2N) and hold N sixteen bit words in CPU byte order.
     *
     * When the word size of the SPI transfer is not a power-of-two multiple
     * of eight bits, those in-memory words include extra bits.  In-memory
     * words are always seen by protocol drivers as right-justified, so the
     * undefined (rx) or unused (tx) bits are always the most significant bits.
     *
     * All SPI transfers start with the relevant chipselect active.  Normally
     * it stays selected until after the last transfer in a message.  Drivers
     * can affect the chipselect signal using cs_change.
     *
     * (i) If the transfer isn't the last one in the message, this flag is
     * used to make the chipselect briefly go inactive in the middle of the
     * message.  Toggling chipselect in this way may be needed to terminate
     * a chip command, letting a single spi_message perform all of group of
     * chip transactions together.
     *
     * (ii) When the transfer is the last one in the message, the chip may
     * stay selected until the next transfer.  On multi-device SPI busses
     * with nothing blocking messages going to other devices, this is just
     * a performance hint; starting a message to another device deselects
     * this one.  But in other cases, this can be used to ensure correctness.
     * Some devices need protocol transactions to be built from a series of
     * spi_message submissions, where the content of one message is determined
     * by the results of previous messages and where the whole transaction
     * ends when the chipselect goes intactive.
     *
     * When SPI can transfer in 1x,2x or 4x. It can get this transfer information
     * from device through @tx_nbits and @rx_nbits. In Bi-direction, these
     * two should both be set. User can set transfer mode with SPI_NBITS_SINGLE(1x)
     * SPI_NBITS_DUAL(2x) and SPI_NBITS_QUAD(4x) to support these three transfer.
     *
     * The code that submits an spi_message (and its spi_transfers)
     * to the lower layers is responsible for managing its memory.
     * Zero-initialize every field you don't set up explicitly, to
     * insulate against future API updates.  After you submit a message
     * and its transfers, ignore them until its completion callback.
     */
    struct spi_transfer {
    	/* it's ok if tx_buf == rx_buf (right?)
    	 * for MicroWire, one buffer must be null
    	 * buffers must work with dma_*map_single() calls, unless
    	 *   spi_message.is_dma_mapped reports a pre-existing mapping
    	 */
    	const void	*tx_buf;
    	void		*rx_buf;
    	unsigned	len;
    
    	dma_addr_t	tx_dma;
    	dma_addr_t	rx_dma;
    	struct sg_table tx_sg;
    	struct sg_table rx_sg;
    
    	unsigned	cs_change:1;
    	unsigned	tx_nbits:3;
    	unsigned	rx_nbits:3;
    #define	SPI_NBITS_SINGLE	0x01 /* 1bit transfer */
    #define	SPI_NBITS_DUAL		0x02 /* 2bits transfer */
    #define	SPI_NBITS_QUAD		0x04 /* 4bits transfer */
    	u8		bits_per_word;
    	u16		delay_usecs;
    	u32		speed_hz;
    
    	struct list_head transfer_list;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104

    struct spi_transferlinux 内核对 SPI 一次数据传输的抽象。

    6、SPI 事物(多个数据传输)

    /**
     * struct spi_message - one multi-segment SPI transaction
     * @transfers: list of transfer segments in this transaction
     * @spi: SPI device to which the transaction is queued
     * @is_dma_mapped: if true, the caller provided both dma and cpu virtual
     *	addresses for each transfer buffer
     * @complete: called to report transaction completions
     * @context: the argument to complete() when it's called
     * @frame_length: the total number of bytes in the message
     * @actual_length: the total number of bytes that were transferred in all
     *	successful segments
     * @status: zero for success, else negative errno
     * @queue: for use by whichever driver currently owns the message
     * @state: for use by whichever driver currently owns the message
     *
     * A @spi_message is used to execute an atomic sequence of data transfers,
     * each represented by a struct spi_transfer.  The sequence is "atomic"
     * in the sense that no other spi_message may use that SPI bus until that
     * sequence completes.  On some systems, many such sequences can execute as
     * as single programmed DMA transfer.  On all systems, these messages are
     * queued, and might complete after transactions to other devices.  Messages
     * sent to a given spi_device are always executed in FIFO order.
     *
     * The code that submits an spi_message (and its spi_transfers)
     * to the lower layers is responsible for managing its memory.
     * Zero-initialize every field you don't set up explicitly, to
     * insulate against future API updates.  After you submit a message
     * and its transfers, ignore them until its completion callback.
     */
    struct spi_message {
    	struct list_head	transfers;
    
    	struct spi_device	*spi;
    
    	unsigned		is_dma_mapped:1;
    
    	/* REVISIT:  we might want a flag affecting the behavior of the
    	 * last transfer ... allowing things like "read 16 bit length L"
    	 * immediately followed by "read L bytes".  Basically imposing
    	 * a specific message scheduling algorithm.
    	 *
    	 * Some controller drivers (message-at-a-time queue processing)
    	 * could provide that as their default scheduling algorithm.  But
    	 * others (with multi-message pipelines) could need a flag to
    	 * tell them about such special cases.
    	 */
    
    	/* completion is reported through a callback */
    	void			(*complete)(void *context);
    	void			*context;
    	unsigned		frame_length;
    	unsigned		actual_length;
    	int			status;
    
    	/* for optional use by whatever driver currently owns the
    	 * spi_message ...  between calls to spi_async and then later
    	 * complete(), that's the spi_master controller driver.
    	 */
    	struct list_head	queue;
    	void			*state;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    struct spi_message 是对多个 spi_transfer 的封装。spi_message 用来原子的执行 spi_transfer 表示的一串数组传输请求。这个传输队列是原子的,这意味着在这个消息完成之前不会有其它消息占用总线消息的执行总是按照 FIFO 的顺序。

    四、SPI 子系统相关 API

    SPI 相关 API 请参考 linux-4.1.15\include\linux\spi\spi.h 文件。

    五、ICM-20608 使用教程

    ICM-20608InvenSense 出品的一款 6MEMS 传感器,包括 3 轴加速度和 3 轴陀螺仪。

    1、ICM-20608 框图

    在这里插入图片描述

    2、ICM-20608 寄存器说明

    参考 ICM-20608 数据手册。

    六、ICM-20608 驱动编写思路

    1、熟悉 AP3216C 使用。

    2、添加 SPI 总线驱动框架

    #include "linux/init.h"
    #include "linux/module.h"
    #include "linux/spi/spi.h"
    
    /*
     * @description     : spi驱动的probe函数,当驱动与
     *                    设备匹配以后此函数就会执行
     * @param - client  : spi设备
     * @param - id      : spi设备ID
     * 
     */	
    static int icm20608_probe(struct spi_device *spi)
    {
        int ret = 0;
        return ret;
    }
    
    /*
     * @description     : spi驱动的remove函数,移除spi驱动的时候此函数会执行
     * @param - client 	: spi设备
     * @return          : 0,成功;其他负值,失败
     */
    static int icm20608_remove(struct spi_device *spi)
    {
        int ret = 0;
        return ret;
    }
    
    /* 传统匹配方式ID列表 */
    static const struct spi_device_id icm20608_id[] = {
        {"invensense,icm20608", 0},  
        {}
    };
    
    /* 设备树匹配列表 */
    static const struct of_device_id icm20608_of_match[] = {
        { .compatible = "invensense,icm20608" },
        { /* Sentinel */ }
    };
    
    /* SPI驱动结构体 */	
    static struct spi_driver icm20608_driver = {
        .probe = icm20608_probe,
        .remove = icm20608_remove,
        .driver = {
            .owner = THIS_MODULE,
            .name = "icm20608",
            .of_match_table = icm20608_of_match, 
        },
        .id_table = icm20608_id,
    };
    
    /*
     * @description	: 驱动入口函数
     * @param 		: 无
     * @return 		: 无
     */
    static int __init icm20608_init(void)
    {
        int ret = 0;
        ret = spi_register_driver(&icm20608_driver);
        return ret;
    }
    
    /*
     * @description	: 驱动出口函数
     * @param 		: 无
     * @return 		: 无
     */
    static void __exit icm20608_exit(void)
    {
        spi_unregister_driver(&icm20608_driver);
    }
    
    /* module_i2c_driver(ap3216c_driver) */
    
    module_init(icm20608_init);
    module_exit(icm20608_exit);
    MODULE_LICENSE("GPL");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

    3、使用 MISC 注册字符设备驱动

    #include "linux/init.h"
    #include "linux/module.h"
    #include "linux/spi/spi.h"
    #include "linux/miscdevice.h"
    #include "linux/fs.h"
    #include "linux/printk.h"
    
    #define NEWCHRDEV_MINOR 255         /* 次设备号(让MISC自动分配) */
    #define NEWCHRDEV_NAME  "icm20608"   /* 名子 */
    
    typedef struct{
    }newchrdev_t;
    newchrdev_t newchrdev;
    
    /*
     * @description		: 打开设备
     * @param - inode 	: 传递给驱动的inode
     * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
     * 					  一般在open的时候将private_data指向设备结构体。
     * @return 			: 0 成功;其他 失败
     */
    static int icm20608_open(struct inode *inode, struct file *filp)
    {
        filp->private_data = &newchrdev; /* 设置私有数据 */
        return 0;
    }
    
    /*
     * @description		: 从设备读取数据 
     * @param - filp 	: 要打开的设备文件(文件描述符)
     * @param - buf 	: 返回给用户空间的数据缓冲区
     * @param - cnt 	: 要读取的数据长度
     * @param - offt 	: 相对于文件首地址的偏移
     * @return 			: 读取的字节数,如果为负值,表示读取失败
     */
    static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
    {
        return 0;
    }
    
    /*
     * @description		: 关闭/释放设备
     * @param - filp 	: 要关闭的设备文件(文件描述符)
     * @return 			: 0 成功;其他 失败
     */
    static int icm20608_release(struct inode *inode, struct file *filp)
    {
        return 0;
    }
    
    static const struct file_operations icm20608_ops = {
        .owner   = THIS_MODULE,
        .open = icm20608_open,
    	.read = icm20608_read,
    	.release = icm20608_release,
    };
    
    /* MISC设备结构体 */
    static struct miscdevice icm20608_miscdev = {
    	.minor = NEWCHRDEV_MINOR,
    	.name = NEWCHRDEV_NAME,
    	.fops = &icm20608_ops,
    };
    
    /*
     * @description     : spi驱动的probe函数,当驱动与
     *                    设备匹配以后此函数就会执行
     * @param - client  : spi设备
     * @param - id      : spi设备ID
     * 
     */	
    static int icm20608_probe(struct spi_device *spi)
    {
        int ret = 0;
        /* 使用 MISC 注册字符设备驱动 */
        ret = misc_register(&icm20608_miscdev);
        if(ret < 0){
            printk("icm20608 misc device register failed!\r\n");
            ret = -EFAULT;
        }
        return ret;
    }
    
    /*
     * @description     : spi驱动的remove函数,移除spi驱动的时候此函数会执行
     * @param - client 	: spi设备
     * @return          : 0,成功;其他负值,失败
     */
    static int icm20608_remove(struct spi_device *spi)
    {
        int ret = 0;
        /* MISC 驱动框架卸载 */
        misc_deregister(&icm20608_miscdev);
        return ret;
    }
    
    /* 传统匹配方式ID列表 */
    static const struct spi_device_id icm20608_id[] = {
        {"invensense,icm20608", 0},  
        {}
    };
    
    /* 设备树匹配列表 */
    static const struct of_device_id icm20608_of_match[] = {
        { .compatible = "invensense,icm20608" },
        { /* Sentinel */ }
    };
    
    /* SPI驱动结构体 */	
    static struct spi_driver icm20608_driver = {
        .probe = icm20608_probe,
        .remove = icm20608_remove,
        .driver = {
            .owner = THIS_MODULE,
            .name = "icm20608",
            .of_match_table = icm20608_of_match, 
        },
        .id_table = icm20608_id,
    };
    
    /*
     * @description	: 驱动入口函数
     * @param 		: 无
     * @return 		: 无
     */
    static int __init icm20608_init(void)
    {
        int ret = 0;
        ret = spi_register_driver(&icm20608_driver);
        return ret;
    }
    
    /*
     * @description	: 驱动出口函数
     * @param 		: 无
     * @return 		: 无
     */
    static void __exit icm20608_exit(void)
    {
        spi_unregister_driver(&icm20608_driver);
    }
    
    /* module_i2c_driver(ap3216c_driver) */
    
    module_init(icm20608_init);
    module_exit(icm20608_exit);
    MODULE_LICENSE("GPL");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147

    4、添加ap3216c操作

    1、初始化 icm20608 设备。

    2、获取 icm20608 传感器相关数据。

    七、添加设备树

    1、imx6ull 中 SPI 控制器设备树添加规则

    文档路径:Documentation\devicetree\bindings\spi\fsl-imx-cspi.txt

    必需属性:

    • compatible:匹配字符串。
    • reg:设备寄存器集的偏移量和长度。
    • interrupts:中断配置。
    • fsl,spi-num-chipselectsSPI 片选引脚数量。
    • cs-gpios:片选引脚配置。
    • clocks:时钟配置。
    • clock-names:时钟名字。
    • dmasDMA 配置。
    • dma-namesDMA 名字。

    2、imx6ull 中 SPI 具体设备设备树添加规则

    文档路径:Documentation/devicetree/bindings/spi/spi-bus.txt

    • compatible:匹配字符串。
    • reg:芯片选择地址的设备。
    • spi-max-frequency:设备最大 SPI 时钟速度(以 Hz 为单位)。

    3、确定 ICM-20608 使用引脚

    UART2_TX_DATAICM20608 片选引脚。

    UART2_RX_DATA:时钟引脚。

    UART2_RTS_BMISO 引脚。

    UART2_CTS_BMOSI 引脚。

    4、查看引脚是否冲突

    5、添加 pinctrl 子系统相关配置

    pinctrl_ecspi3: icm20608 {
    	fsl,pins = < 
    		MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20		0x10b0	/* CS */
    		MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK	0x10b1	/* SCLK */
    		MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO		0x10b1	/* MISO */
    		MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI		0x10b1	/* MOSI */
    	>;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    6、在 ecspi3 节点下追加 icm20608 子节点

    &ecspi3 {
    	fsl,spi-num-chipselects = <1>;
    	cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;
    	pinctrl-names = "default";
    	pinctrl-0 = <&pinctrl_ecspi3>;
    	status = "okay";
    
    	spidev: icm20608@0 {
    		compatible = "invensense,icm20608";
    		spi-max-frequency = <8000000>;
    		reg = <0>;
    	};	
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    7、设备树编译

    onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$ make dtbs
      CHK     include/config/kernel.release
      CHK     include/generated/uapi/linux/version.h
      CHK     include/generated/utsrelease.h
    make[1]: 'include/generated/mach-types.h' is up to date.
      CHK     include/generated/bounds.h
      CHK     include/generated/asm-offsets.h
      CALL    scripts/checksyscalls.sh
      DTC     arch/arm/boot/dts/imx6ull-alientek-emmc.dtb
      DTC     arch/arm/boot/dts/imx6ull-alientek-nand.dtb
    onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$ 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    8、测试

    # pwd
    /proc/device-tree/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02010000
    # ls
    #address-cells           dma-names                pinctrl-0
    #size-cells              dmas                     pinctrl-names
    clock-names              fsl,spi-num-chipselects  reg
    clocks                   icm20608@0               status
    compatible               interrupts
    cs-gpio                  name
    # cat compatible
    fsl,imx6ul-ecspifsl,imx51-ecspi#
    #
    #
    # cd icm20608@0/
    # ls
    compatible         name               reg                spi-max-frequency
    # cat compatible
    invensense,icm20608#
    #
    # cat name
    icm20608#
    #
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    八、驱动编写

    #include "linux/init.h"
    #include "linux/module.h"
    #include "linux/spi/spi.h"
    #include "linux/miscdevice.h"
    #include "linux/fs.h"
    #include "linux/printk.h"
    #include "linux/delay.h"
    #include "asm-generic/uaccess.h"
    
    #define ICM20608G_ID			0XAF	/* ID值 */
    #define ICM20608D_ID			0XAE	/* ID值 */
    
    /* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
    #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
    
    #define NEWCHRDEV_MINOR 255         /* 次设备号(让MISC自动分配) */
    #define NEWCHRDEV_NAME  "icm20608"   /* 名子 */
    
    typedef struct{
        void *private_data;			/* 私有数据(保存SPI设备指针)*/
        signed int gyro_x_adc;		/* 陀螺仪X轴原始值 	 */
        signed int gyro_y_adc;		/* 陀螺仪Y轴原始值		*/
        signed int gyro_z_adc;		/* 陀螺仪Z轴原始值 		*/
        signed int accel_x_adc;		/* 加速度计X轴原始值 	*/
        signed int accel_y_adc;		/* 加速度计Y轴原始值	*/
        signed int accel_z_adc;		/* 加速度计Z轴原始值 	*/
        signed int temp_adc;		/* 温度原始值 			*/
    }newchrdev_t;
    newchrdev_t newchrdev;
    
    /*
     * @description	: 从icm20608读取多个寄存器数据
     * @param - dev:  icm20608设备
     * @param - reg:  要读取的寄存器首地址
     * @param - val:  读取到的数据
     * @param - len:  要读取的数据长度
     * @return 		: 操作结果
     * 说明:icm20608_read_regs函数内部操作SPI子系统已经封装,封装好的函数为spi_read
     */
    static int icm20608_read_regs(newchrdev_t *dev, unsigned char reg, void *buf, int len)
    {
    	int ret = -1;
    	unsigned char txdata[1];
    	unsigned char * rxdata;
    	struct spi_message m;
    	struct spi_transfer *t;
    	struct spi_device *spi = (struct spi_device *)dev->private_data;
        
    	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);	/* 申请内存 */
    	if(!t) {
    		return -ENOMEM;
    	}
    
    	rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL);	/* 申请内存 */
    	if(!rxdata) {
    		goto out1;
    	}
    
    	/* 一共发送len+1个字节的数据,第一个字节为
    	寄存器首地址,一共要读取len个字节长度的数据,*/
    	txdata[0] = reg | 0x80;		/* 写数据的时候首寄存器地址bit8要置1 */			
    	t->tx_buf = txdata;			/* 要发送的数据 */
        t->rx_buf = rxdata;			/* 要读取的数据 */
    	t->len = len+1;				/* t->len=发送的长度+读取的长度 */
    	spi_message_init(&m);		/* 初始化spi_message */
    	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
    	ret = spi_sync(spi, &m);	/* 同步发送 */
    	if(ret) {
    		goto out2;
    	}
    	
        memcpy(buf , rxdata+1, len);  /* 只需要读取的数据 */
    
    out2:
    	kfree(rxdata);					/* 释放内存 */
    out1:	
    	kfree(t);						/* 释放内存 */
    	
    	return ret;
    }
    
    /*
     * @description	: 向icm20608多个寄存器写入数据
     * @param - dev:  icm20608设备
     * @param - reg:  要写入的寄存器首地址
     * @param - val:  要写入的数据缓冲区
     * @param - len:  要写入的数据长度
     * @return 	  :   操作结果
     * 说明:icm20608_write_regs函数内部操作SPI子系统已经封装,封装好的函数为spi_write
     */
    static signed int icm20608_write_regs(newchrdev_t *dev, unsigned char reg, unsigned char *buf, unsigned char len)
    {
    	int ret = -1;
    	unsigned char *txdata;
    	struct spi_message m;
    	struct spi_transfer *t;
    	struct spi_device *spi = (struct spi_device *)dev->private_data;
    	
    	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);	/* 申请内存 */
    	if(!t) {
    		return -ENOMEM;
    	}
    	
    	txdata = kzalloc(sizeof(char)+len, GFP_KERNEL);
    	if(!txdata) {
    		goto out1;
    	}
    	
    	/* 一共发送len+1个字节的数据,第一个字节为
    	寄存器首地址,len为要写入的寄存器的集合,*/
    	*txdata = reg & ~0x80;	/* 写数据的时候首寄存器地址bit8要清零 */
        memcpy(txdata+1, buf, len);	/* 把len个寄存器拷贝到txdata里,等待发送 */
    	t->tx_buf = txdata;			/* 要发送的数据 */
    	t->len = len+1;				/* t->len=发送的长度+读取的长度 */
    	spi_message_init(&m);		/* 初始化spi_message */
    	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
    	ret = spi_sync(spi, &m);	/* 同步发送 */
        if(ret) {
            goto out2;
        }
    	
    out2:
    	kfree(txdata);				/* 释放内存 */
    out1:
    	kfree(t);					/* 释放内存 */
    	return ret;
    }
    
    /*
     * @description	: 读取icm20608指定寄存器值,读取一个寄存器
     * @param - dev:  icm20608设备
     * @param - reg:  要读取的寄存器
     * @return 	  :   读取到的寄存器值
     */
    static unsigned char icm20608_read_onereg(newchrdev_t *dev, unsigned char reg)
    {
    	unsigned char data = 0;
    	icm20608_read_regs(dev, reg, &data, 1);
    	return data;
    }
    
    /*
     * @description	: 向icm20608指定寄存器写入指定的值,写一个寄存器
     * @param - dev:  icm20608设备
     * @param - reg:  要写的寄存器
     * @param - data: 要写入的值
     * @return   :    无
     */	
    
    static void icm20608_write_onereg(newchrdev_t *dev, unsigned char reg, unsigned char value)
    {
    	unsigned char buf = value;
    	icm20608_write_regs(dev, reg, &buf, 1);
    }
    
    /*
     * ICM20608内部寄存器初始化函数 
     * @param  	: 无
     * @return 	: 无
     */
    void icm20608_reginit(void)
    {
    	unsigned char value = 0;
    	
        icm20608_write_onereg(&newchrdev, ICM20_PWR_MGMT_1, 0x80);
    	mdelay(50);
    	icm20608_write_onereg(&newchrdev, ICM20_PWR_MGMT_1, 0x01);
    	mdelay(50);
    
    	value = icm20608_read_onereg(&newchrdev, ICM20_WHO_AM_I);
    	printk("ICM20608 ID = %#X\r\n", value);	
    
    	icm20608_write_onereg(&newchrdev, ICM20_SMPLRT_DIV, 0x00); 	/* 输出速率是内部采样率					*/
    	icm20608_write_onereg(&newchrdev, ICM20_GYRO_CONFIG, 0x18); 	/* 陀螺仪±2000dps量程 				*/
    	icm20608_write_onereg(&newchrdev, ICM20_ACCEL_CONFIG, 0x18); 	/* 加速度计±16G量程 					*/
    	icm20608_write_onereg(&newchrdev, ICM20_CONFIG, 0x04); 		/* 陀螺仪低通滤波BW=20Hz 				*/
    	icm20608_write_onereg(&newchrdev, ICM20_ACCEL_CONFIG2, 0x04); /* 加速度计低通滤波BW=21.2Hz 			*/
    	icm20608_write_onereg(&newchrdev, ICM20_PWR_MGMT_2, 0x00); 	/* 打开加速度计和陀螺仪所有轴 				*/
    	icm20608_write_onereg(&newchrdev, ICM20_LP_MODE_CFG, 0x00); 	/* 关闭低功耗 						*/
    	icm20608_write_onereg(&newchrdev, ICM20_FIFO_EN, 0x00);		/* 关闭FIFO						*/
    }
    
    /*
     * @description	: 读取ICM20608的数据,读取原始数据,包括三轴陀螺仪、
     * 				: 三轴加速度计和内部温度。
     * @param - dev	: ICM20608设备
     * @return 		: 无。
     */
    void icm20608_readdata(newchrdev_t *dev)
    {
    	unsigned char data[14] = "";
    	icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);
    
    	dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]); 
    	dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]); 
    	dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]); 
    	dev->temp_adc    = (signed short)((data[6] << 8) | data[7]); 
    	dev->gyro_x_adc  = (signed short)((data[8] << 8) | data[9]); 
    	dev->gyro_y_adc  = (signed short)((data[10] << 8) | data[11]);
    	dev->gyro_z_adc  = (signed short)((data[12] << 8) | data[13]);
    }
    
    /*
     * @description		: 打开设备
     * @param - inode 	: 传递给驱动的inode
     * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
     * 					  一般在open的时候将private_data指向设备结构体。
     * @return 			: 0 成功;其他 失败
     */
    static int icm20608_open(struct inode *inode, struct file *filp)
    {
        filp->private_data = &newchrdev; /* 设置私有数据 */
        return 0;
    }
    
    /*
     * @description		: 从设备读取数据 
     * @param - filp 	: 要打开的设备文件(文件描述符)
     * @param - buf 	: 返回给用户空间的数据缓冲区
     * @param - cnt 	: 要读取的数据长度
     * @param - offt 	: 相对于文件首地址的偏移
     * @return 			: 读取的字节数,如果为负值,表示读取失败
     */
    static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
    {
        newchrdev_t *dev = (newchrdev_t *)filp->private_data;
        signed int data[7];
    	long err = 0;
        icm20608_readdata(dev);
        data[0] = dev->gyro_x_adc;
    	data[1] = dev->gyro_y_adc;
    	data[2] = dev->gyro_z_adc;
    	data[3] = dev->accel_x_adc;
    	data[4] = dev->accel_y_adc;
    	data[5] = dev->accel_z_adc;
    	data[6] = dev->temp_adc;
        printk("data[0] = %d data[1] = %d data[2] = %d data[3] = %d data[4] = %d data[5] = %d\r\n",data[0],data[1],data[2],data[3],data[4],data[5]);
        err = copy_to_user(buf, data, sizeof(data));
        return 0;
    }
    
    /*
     * @description		: 关闭/释放设备
     * @param - filp 	: 要关闭的设备文件(文件描述符)
     * @return 			: 0 成功;其他 失败
     */
    static int icm20608_release(struct inode *inode, struct file *filp)
    {
        return 0;
    }
    
    static const struct file_operations icm20608_ops = {
        .owner   = THIS_MODULE,
        .open = icm20608_open,
    	.read = icm20608_read,
    	.release = icm20608_release,
    };
    
    /* MISC设备结构体 */
    static struct miscdevice icm20608_miscdev = {
    	.minor = NEWCHRDEV_MINOR,
    	.name = NEWCHRDEV_NAME,
    	.fops = &icm20608_ops,
    };
    
    /*
     * @description     : spi驱动的probe函数,当驱动与
     *                    设备匹配以后此函数就会执行
     * @param - client  : spi设备
     * @param - id      : spi设备ID
     * 
     */	
    static int icm20608_probe(struct spi_device *spi)
    {
        int ret = 0;
        /* 使用 MISC 注册字符设备驱动 */
        ret = misc_register(&icm20608_miscdev);
        if(ret < 0){
            printk("icm20608 misc device register failed!\r\n");
            ret = -EFAULT;
        }
        newchrdev.private_data = spi; /* 设置私有数据 */
        icm20608_reginit();
        return ret;
    }
    
    /*
     * @description     : spi驱动的remove函数,移除spi驱动的时候此函数会执行
     * @param - client 	: spi设备
     * @return          : 0,成功;其他负值,失败
     */
    static int icm20608_remove(struct spi_device *spi)
    {
        int ret = 0;
        /* MISC 驱动框架卸载 */
        misc_deregister(&icm20608_miscdev);
        return ret;
    }
    
    /* 传统匹配方式ID列表 */
    static const struct spi_device_id icm20608_id[] = {
        {"invensense,icm20608", 0},  
        {}
    };
    
    /* 设备树匹配列表 */
    static const struct of_device_id icm20608_of_match[] = {
        { .compatible = "invensense,icm20608" },
        { /* Sentinel */ }
    };
    
    /* SPI驱动结构体 */	
    static struct spi_driver icm20608_driver = {
        .probe = icm20608_probe,
        .remove = icm20608_remove,
        .driver = {
            .owner = THIS_MODULE,
            .name = "icm20608",
            .of_match_table = icm20608_of_match, 
        },
        .id_table = icm20608_id,
    };
    
    /*
     * @description	: 驱动入口函数
     * @param 		: 无
     * @return 		: 无
     */
    static int __init icm20608_init(void)
    {
        int ret = 0;
        ret = spi_register_driver(&icm20608_driver);
        return ret;
    }
    
    /*
     * @description	: 驱动出口函数
     * @param 		: 无
     * @return 		: 无
     */
    static void __exit icm20608_exit(void)
    {
        spi_unregister_driver(&icm20608_driver);
    }
    
    /* module_i2c_driver(ap3216c_driver) */
    
    module_init(icm20608_init);
    module_exit(icm20608_exit);
    MODULE_LICENSE("GPL");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349
    • 350
    • 351
    • 352
    • 353
    • 354
    • 355
    • 356
    • 357
    • 358
    • 359
    • 360
    • 361
    • 362
    • 363
    • 364
    • 365
    • 366
    • 367
    • 368
    • 369
    • 370
    • 371
    • 372
    • 373
    • 374
    • 375
    • 376
    • 377
    • 378
    • 379
    • 380
    • 381
    • 382
    • 383
    • 384
    • 385
    • 386
    • 387
    • 388
    • 389
    • 390
    • 391
    • 392
    • 393
    • 394
    • 395
    • 396
    • 397
    • 398
    • 399
    • 400
    • 401
    • 402
    • 403
    • 404
    • 405
    • 406
    • 407
    • 408
    • 409
    • 410
    • 411
    • 412
    • 413
    • 414

    九、应用程序编写

    #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 
    #include 
    #include 
    #include 
    #include 
    
    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);
    		}
    		sleep(1); /*100ms */
    	}
    	close(fd);	/* 关闭文件 */	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    特别说明:

    编译时需要使能浮点计算单元。编译命令如下:

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

    十、测试

    # ls
    icm20608.ko   icm20608_app
    # ls -l /dev/icm20608
    ls: /dev/icm20608: No such file or directory
    #
    # insmod icm20608.ko
    ICM20608 ID = 0XAE
    # ls -l /dev/icm20608
    crw-rw----    1 root     root       10,  56 Jan  1 03:27 /dev/icm20608
    #
    # rmmod icm20608.ko
    # ls -l /dev/icm20608
    ls: /dev/icm20608: No such file or directory
    #
    # insmod icm20608.ko
    ICM20608 ID = 0XAE
    # ls -l /dev/icm20608
    crw-rw----    1 root     root       10,  56 Jan  1 03:27 /dev/icm20608
    #
    # ./icm20608_app /dev/icm20608
    data[0] = 6 data[1] = 12 data[2] = -1 data[3] = 62 data[4] = 27 data[5] = 2059
    
    原始值:
    gx = 6, gy = 12, gz = -1
    ax = 62, ay = 27, az = 2059
    temp = 2453
    实际值:act gx = 0.37°/S, act gy = 0.73°/S, act gz = -0.06°/S
    act ax = 0.03g, act ay = 0.01g, act az = 1.01g
    act temp = 32.43°C
    data[0] = 7 data[1] = 12 data[2] = 0 data[3] = 54 data[4] = 26 data[5] = 2061
    
    原始值:
    gx = 7, gy = 12, gz = 0
    ax = 54, ay = 26, az = 2061
    temp = 2457
    实际值:act gx = 0.43°/S, act gy = 0.73°/S, act gz = 0.00°/S
    act ax = 0.03g, act ay = 0.01g, act az = 1.01g
    act temp = 32.44°C
    data[0] = 7 data[1] = 11 data[2] = -1 data[3] = 59 data[4] = 30 data[5] = 2059
    
    原始值:
    gx = 7, gy = 11, gz = -1
    ax = 59, ay = 30, az = 2059
    temp = 2458
    实际值:act gx = 0.43°/S, act gy = 0.67°/S, act gz = -0.06°/S
    act ax = 0.03g, act ay = 0.01g, act az = 1.01g
    act temp = 32.44°C
    data[0] = 8 data[1] = 12 data[2] = 0 data[3] = 59 data[4] = 28 data[5] = 2060
    
    原始值:
    gx = 8, gy = 12, gz = 0
    ax = 59, ay = 28, az = 2060
    temp = 2455
    实际值:act gx = 0.49°/S, act gy = 0.73°/S, act gz = 0.00°/S
    act ax = 0.03g, act ay = 0.01g, act az = 1.01g
    act temp = 32.44°C
    ^C
    #
    # rmmod icm20608.ko
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
  • 相关阅读:
    quickapp_快应用_tabBar
    npm发布自己的插件包:新手教程
    【无标题】
    深度学习——使用块的网络VGG(笔记)
    LiteFlow 开源编排规则引擎
    2023年JCR影响因子正式发布,点击查看能源与燃料领域期刊变化【持续更新02】
    深度探索:智能家居背后的科技力量与伦理思考
    联想混合云Lenovo xCloud:4大产品线+IT服务门户
    学习笔记之——视觉三维重建(colmap)
    Shiro【散列算法、Shiro会话、退出登录 、权限表设计、注解配置鉴权 】(五)-全面详解(学习总结---从入门到深化)
  • 原文地址:https://blog.csdn.net/OnlyLove_/article/details/127855331