视频:第23.1讲 Linux SPI驱动实验-SPI控制器驱动详解(上)_哔哩哔哩_bilibili
资料:《【正点原子】I.MX6U开发指南V1.81.pdf》六十二章


一、SPI主机驱动

        这部分只做了解即可,一般都由原厂提供。重点还是在SPI设备驱动

1.1 spi_master定义

        SPI驱动框架和I2C类似,都分主机控制器驱动和设备驱动。SPI主机驱动就是SOC的SPI控制器驱动,类似I2C驱动里面的适配器驱动。

        spi_master的定义如下。因为太长了,有删减,只保留了重要的部分。

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.
	 * 除非为负数(== 动态分配),否则bus_num完全由板级决定。
	 * 例子:某个SoC有三个SPI控制器,编号0 1 2,而某块板子的原理图可能使用SPI2,
     * 软件通常会为该控制器设置bus_num=2
	 */
	s16			bus_num;

	/* chipselects will be integral to many controllers; some others
	 * might use board-specific GPIOs.
	 * 许多控制器集成了片选;也有一些会使用与板级相关的GPIO
	 */
	u16			num_chipselect;

	/* some SPI controllers pose alignment requirements on DMAable
	 * buffers; let protocol drivers know about these requirements.
     * 某些SPI控制器对可用于DMA的缓冲区提出了对齐要求;用此字段让协议驱动知晓这些要求。
	 */
	u16			dma_alignment;

	/* spi_device.mode flags understood by this controller driver 
     * 该控制器驱动可识别的spi_device.mode标志 
     */
	u16			mode_bits;

	/* bitmask of supported bits_per_word for transfers 
     * 传输时所支持的bits_per_word的位掩码 */
	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 必须有接收rx*/
#define SPI_MASTER_MUST_TX      BIT(4)	/* requires tx 必须有发送tx*/

	/* lock and mutex for SPI bus locking 用于SPI总线加锁的自旋锁和互斥量*/
	spinlock_t		bus_lock_spinlock;
	struct mutex		bus_lock_mutex;

	/* flag indicating that the SPI bus is locked for exclusive use 
     * 指示SPI总线已被独占锁定的标志 */
	bool			bus_lock_flag;

	/* Setup mode and clock, etc (spi driver may call many times).
	 * 设置模式、时钟等(SPI设备驱动可能会多次调用)
     * 
	 * 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.
     * transfer()方法不能睡眠;其主要作用只是把消息加入队列
	 * + For now there's no remove-from-queue operation, or any other request management
     *   目前没有从队列中移除的操作 或任何其他请求管理
	 * + To a given spi_device, message queueing is pure fifo
	 *   对于某个给定的spi_device,消息排队方式是纯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)
     *   如果有多个spi_device子设备,I/O队列的仲裁算法未指定(轮询、FIFO、优先级、预留、抢占等)
	 *
	 * + Chipselect stays active during the entire message (unless modified by spi_transfer.cs_change != 0).
     *   在整个消息期间片选保持有效(除非被spi_transfer.cs_change != 0 修改)
	 * + The message transfers use clock and SPI mode parameters
	 *   previously established by setup() for this device
     *   消息传输使用先前由setup()为该设备建立的时钟和SPI模式参数
	 */
	int			(*transfer)(struct spi_device *spi,
						struct spi_message *mesg);

	/* called on release() to free memory provided by spi_master
     * 在release()时调用,用于释放由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.
	 * 用于启用核心对DMA处理的支持;如果实现了can_dma()并返回true,
     * 则会在调用transfer_one()之前对传输进行映射。驱动不应修改或保存xfer,
     * 并且在设备被准备好期间必须设置dma_tx和dma_rx
	 */
	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.
     * 以下hook用于驱动,其希望使用通用主控传输队列机制
	 * 若采用这些hook,驱动不得实现上面的transfer()函数。
     * 随着时间推移,预计SPI驱动会逐步迁移到该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.
	 * 这些hook用于给 (使用由核心提供的通用transfer_one_message()实现的驱动)使用。  好长的定从
	 */
	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 GPIO 片选 */
	int			*cs_gpios;

	/* DMA channels for use with core dmaengine helpers 
     * 与核心dmaengine配合使用的DMA通道 */
	struct dma_chan		*dma_tx;
	struct dma_chan		*dma_rx;

	/* dummy data for full duplex devices 用于全双工设备的占位数据 */
	void			*dummy_rx;
	void			*dummy_tx;
};

1.2 spi_master 申请/释放

struct spi_master *spi_alloc_master(struct device *dev, unsigned size)
// dev :该spi_master的父设备。一般是platform_device中的dev成员
// size:需要额外分配的私有数据区大小。
//       内核会一次性分配sizeof(struct spi_master) + size的空间
//       这段私有区size通常放驱动自己的状态,比如寄存器基址、时钟、中断号、FIFO长度等。
//       分配好后可以用函数spi_master_get_devdata(struct spi_master *master)取回这段私有数据的指针
//       再强转成我们定义的的私有结构体类型即可
// return:申请到的spi_master
static inline void spi_master_put(struct spi_master *master)
// master:要释放的spi_master

1.3 spi_master 注册/注销

int spi_register_master(struct spi_master *master)
// master:要注册的spi_master
// return:0成功;负值:错误代码
void spi_unregister_master(struct spi_master *master)
// master:要注销的spi_master

        这两个函数的注释中都指出“This must be called from context that can sleep.",调用这些函数时,当前执行环境允许阻塞/调度,即不能在原子上下文里调用。因为这些函数会进行创建/销毁设备、操作sysfs、获取mutex、分配内存等,这些动作都可能睡眠。因此不要在中断、软中断或持有自旋锁的原子上下文里调用。

二、SPI设备驱动

2.1 spi_device 定义

        也不是重点,因为与i2c_client类似。如果有设备树的话,就不需要我们手动创建注册spi_device 。在启动内核时会自动根据设备树节点解析出spi_device。

        spi_device用于在SPI从设备与CPU内存之间交换数据。

struct spi_device {
	struct device		dev;        // dev中的platform_data用于保存对该设备的协议驱动有意义、但对控制器没有意义的信息
                                    // 例如用于区分功能略有不同的芯片变体的标识;或者该电路板如何连接该芯片引脚的布线信息
	struct spi_master	*master;    // SPI控制器
	u32			max_speed_hz;       // 本板上与该芯片通信所能使用的最大时钟频率
                                    // 可使用设备驱动修改。每次传输由spi_transfer.speed_hz 覆盖该值
	u8			chip_select;        // 片选
	u8			bits_per_word;      // 数据传输由一个或多个“字”组成;常见字长如8位或12位。
                                    // 内存中的字长按2的幂字节对齐(例如20位采样用32位表示)
                                    // 可由设备驱动修改,或保持默认值0(表示协议字为8位字节)。
                                    // 每次传输可由 spi_transfer.bits_per_word 覆盖该值。
	u16			mode;               // mode的参数配置如下
#define	SPI_CPHA	0x01			/* 时钟相位 */
#define	SPI_CPOL	0x02			/* 时钟极性 */
#define	SPI_MODE_0	(0|0)			/* 原始MicroWire,MODE0,CPOL=0,CPHA=0 */
#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			/* 片选高电平有效 */
#define	SPI_LSB_FIRST	0x08			/* 每字 LSB 先传 */
#define	SPI_3WIRE	0x10			/* SI/SO 信号共用(3 线) */
#define	SPI_LOOP	0x20			/* 回环模式 */
#define	SPI_NO_CS	0x40			/* 每总线仅 1 设备,无片选 */
#define	SPI_READY	0x80			/* 从设备拉低表示暂停 */
#define	SPI_TX_DUAL	0x100			/* 使用 2 线发送(Dual) */
#define	SPI_TX_QUAD	0x200			/* 使用 4 线发送(Quad) */
#define	SPI_RX_DUAL	0x400			/* 使用 2 线接收(Dual) */
#define	SPI_RX_QUAD	0x800			/* 使用 4 线接收(Quad) */
	int		irq;                     // 负数,或传给request_irq()以接收该设备中断的编号
	void	*controller_state;       // 控制器的运行状态
	void	*controller_data;        // 控制器的板级特定定义,例如FIFO初始化参数;来源于 board_info.controller_data         
	char	modalias[SPI_NAME_SIZE]; // 用于该设备的驱动名称,或该名称的别名
                                     // 该名称会出现在用于驱动冷插的sysfs属性“modalias”中,以及用于热插的uevent中
	int		cs_gpio;	/* 片选使用的 GPIO */

	/*
	 * 可能还需要更多钩子来描述影响控制器与各芯片通信的协议选项,例如:
	 *  - 内存打包(如 12 位采样放在低位,其余位置清零)
	 *  - 优先级
	 *  - 每个字后释放片选
	 *  - 片选延时
	 *  - ...
	 */
};

将spi的成员配置好后,使用spi_setup完成初始化:

int spi_setup(struct spi_device *spi)

2.2 spi_driver定义

/**
 * 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;   // 该驱动支持的SPI设备列表
	int		(*probe)(struct spi_device *spi);  // probe
	int		(*remove)(struct spi_device *spi); // remove
	void	(*shutdown)(struct spi_device *spi); // 在系统状态转换(例如关机/停机/kexec)期间使用的标准关机回调
	struct device_driver	driver;   // driver需要初始化其name、owner字段,以及设备树匹配用的of_match_table
};

2.3 spi_driver 注册/注销

int spi_register_driver(struct spi_driver *sdrv)
// sdrv:要注册的spi_driver
// return::0成功;负值失败
void spi_unregister_driver(struct spi_driver *sdrv)
// sdrv:要注销的spi_driver

2.4 设备和驱动的匹配

        SPI设备和驱动的匹配也是用由SPI总线完成的,SPI总线的定义为:

struct bus_type spi_bus_type = {
	.name		= "spi",
	.dev_groups	= spi_dev_groups,    
	.match		= spi_match_device, // 这里就是匹配函数
	.uevent		= spi_uevent,
};

看一眼spi_match_device函数,四种匹配方式:

static int spi_match_device(struct device *dev, struct device_driver *drv)
{
	const struct spi_device	*spi = to_spi_device(dev);
	const struct spi_driver	*sdrv = to_spi_driver(drv);

	/* Attempt an OF style match 设备树匹配*/
	if (of_driver_match_device(dev, drv))
		return 1;

	/* Then try ACPI    ACPI匹配*/
	if (acpi_driver_match_device(dev, drv))
		return 1;

	if (sdrv->id_table) // id_table匹配
		return !!spi_match_id(sdrv->id_table, spi);

	return strcmp(spi->modalias, drv->name) == 0;  // name字段匹配(spi_driver中的device_driver中的name)
}

2.5 数据收发

        这里的结构体有两个:spi_transfer和spi_message

        spi_transfer表示一次具体的传输动作:要收发的数据、数据的长度等等。
        spi_message是一个传输序列容器,可以装很多个spi_transfer。每次收发信息都是直接收发spi_message

        具体流程为:
                先定义spi_transfer成员变量
                初始化spi_message
                将spi_transfer装入spi_message
                发送spi_message

        具体代码写法详见3.6部分。

2.5.1 spi_transfer 

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;  // 数据长度。因为SPI是全双工,因此接收长度=发送长度

	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;
};

2.5.2 spi_message

struct spi_message {
	struct list_head	transfers;
	struct spi_device	*spi;
	unsigned		is_dma_mapped:1;

	/* completion is reported through a callback */
	void			(*complete)(void *context);
	void			*context;
	unsigned		frame_length;
	unsigned		actual_length;
	int			status;

	struct list_head	queue;
	void			*state;
};

2.5.3 其他函数

初始化spi_message:

void spi_message_init(struct spi_message *m)
// m:要初始化的spi_message

将spi_transfer添加到spi_message中:

void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
// 将spi_transfer(t)添加到spi_message(m)中

传输数据

同步传输(使用互斥锁,阻塞当前进程,等待SPI数据传输完成)
int spi_sync(struct spi_device *spi, struct spi_message *message)
// spi:    要进行数据传输的spi_device
// message:要传输的spi_message

异步传输(不会阻塞)
异步传输就需要设置spi_message中的complete成员,这是一个回调函数,当SPI异步传输完成后会调用这个函数
int spi_async(struct spi_device *spi, struct spi_message *message)
// spi:    要进行数据传输的spi_device
// message:要传输的spi_message

2.5.4 常用函数

要是自己手搓上面那一大坨函数也墨迹了,而且还有全双工数据的问题、片选脚的问题,linux提供了几个现成的函数:

SPI 同步写
static inline int spi_write(struct spi_device *spi, const void *buf, size_t len)
// spi:spi设备
// buf:要写的数据
// len:数据长度
// return: 0成功,负值错误码
// 只能在可以睡眠的上下文中调用(如进程上下文),不能在中断上下文里用
先写后读的 SPI 同步操作

        常用于先发送目标寄存器地址、然后读取目标寄存器内容。具体详见3.6.

int spi_write_then_read(
        struct spi_device *spi,  // spi设备
		const void *txbuf,       // 要写的数据
        unsigned n_tx,           // 写数据的长度
		void *rxbuf,             // 保存要读取的数据
        unsigned n_rx)           // 读数据的长度
// return:0成功,负值错误码


只能在允许睡眠的上下文中调用

传给这个函数的参数都会先通过一个小缓冲区拷贝一遍
为了可移植性,建议单次调用传输的数据不要超过32字节

对性能敏感或者需要大量数据传输的代码,应该改用
spi_{async,sync}() 这类接口,并且使用DMA安全的缓冲区

三、icm20608驱动

        这里的驱动以IMX6ULL ALPHA板为例。
        因为我的mini板并没有icm20608,因此并不能保证代码能正确运行。为此我增加了第四部分0.96寸OLED的驱动代码。

3.1 修改设备树

3.1.1 配置引脚

        在“【正点原子】阿尔法Linux开发板(A盘)-基础资料/02、开发板原理图”里面的《IMX6ULL_ALPHA_V2.2(底板原理图).pdf》中搜索ICM20608,可以找到ICM20608在alpha板上的接线:

        由图可得:
                UART2_TXD脚复用为ECSPI3_SS0         接到  CS片选
                UART2_RXD脚复用为ECSPI3_SCLK      接到  SCL/SCLK
                UART2_RTS脚复用为ECSPI3_MISO       接到  AD0SDO
                UART2_CTS脚复用为ECSPI3_MOSI       接到  SDA/SDI

        但是我们这里不把UART2_TXD脚复用为ECSPI3_SS0,而是复用为GPIO,作为软件片选。因此在arch/arm/boot/dts/imx6ul-pinfunc.h中查找对应宏定义,可以定义rinctrl_ecspi3:

		// 2025/11/9 SPI
		pinctrl_ecspi3: icm20608{  // label : name
			fsl,pins = <
				MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20   0x10b0
				MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK  0x10b1
				MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI    0x10b1
				MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO	  0x10b1
			>;
		}

将这段代码增加到imx6ull-alientek-emmc.dts的&iomuxc下面:

        为了防止文件中还有其他代码将UART2的四个脚复用为其他功能,需要我们查找包含UART2_TXD、UART2_RXD、UART2_RTS、UART2_CTS的代码,将其注释掉。
        由于把UART2_TX复用为GPIO1_IO20,因此还需要查找是否包含 “gpio1_io20” 和 “<&gpio1 20……>” 的代码

        至于这个电气属性的配置,在“【正点原子】阿尔法Linux开发板(A盘)-基础资料/07、I.MX6U参考资料/01、I.MX6UL芯片资料”的《IMX6UL参考手册.pdf》查找对应寄存器即可。以第一行MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20   0x10b0为例,查找UART2_TX_DATA,可见:

        电气属性为0x10b0,二进制为 0001 0000 1011 0000,对应寄存器为:

寄存器名内容(二进制)含义
HYS0Hysteresis Disabled 关闭输入迟滞
PUS0100K Ohm Pull Down  100K欧姆下拉

PUE

0Keeper。选择Keeper而不是Pull。此时PUS配置的上下拉不生效
PKE1Pull/Keeper Enabled   使能Kepper/Pull。此时PUS和PUE才有效
ODE0Open Drain Disabled  关闭开漏
SPEED10medium(100 MHz)
DSE110R0/6
SRE0Slow Slew Rate   慢速边沿

3.1.2 配置设备节点

        imx6ull-alientek-emmc.dts下没有找到spi节点,我们直接自己写一个,放在imx6ull-alientek-emmc.dts的最后面。

        (这里直接给出节点配置。spi节点具体配置详见linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/Documentation/devicetree/bindings/spi/fsl-imx-cspi.txt,spi子节点配置详见同路径下的spi_bus.txt文件)

// 2025/11/9 新增
&ecspi3 {
	fsl,spi-num-chipselects = <1>;  // 1个片选 Contains the number of the chipselect
	cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>; // 使用gpio1_20做软件片选
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_ecspi1>;
	status = "okay";

	// 对应的spi芯片子节点
	spidev0: icm20608@0 { // @后面的0表示此SPI芯片接到哪个片选上,这里表示SS0
		// #address-cells = <1>;  spidev0下面没有子节点了,这两个cells可以删掉
		// #size-cells = <1>;
		compatible = "alientek,icm20608";
		spi-max-frequency = <8000000>; // SPI时钟最大频率
		reg = <0>;
	};
};

        这里片选引脚cs-gpios配置为gpio1_20,之后在使用的时候就不需要我们手动拉高拉低了,而是由SPI控制器自动控制。

编译、复制:

make dtbs
sudo cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /.../tftpboot/

看看有没有挂上:

ls /sys/bus/spi/devices/    # 应显示spi2.0。为什么不是3.0?
                            # 因为手册上是从1开始排序,而系统是从0开始排序,所以这里系统的spi2.0对应的就是手册的spi3
cd /sys/bus/spi/devices/spi2.0/of_node/
cat name    # 应能看到icm20608

 ls /sys/bus/spi/drivers/   # 应能看到icm20608

3.2 文件结构

23_ICM20608 (工作区)
├── .vscode
│   ├── c_cpp_properties.json
│   └── settings.json
├── 22_icm20608.code-workspace
├── Makefile
├── icm20608reg.h
├── icm20608.c
└── icm20608APP.c

3.3  icm20608reg.h

        icm20608的寄存器地址在裸机部分教程已经写过,内容来自【正点原子】阿尔法Linux开发板(A盘)-基础资料\01、例程源码\01、裸机例程\18_spi\bsp\icm20608\bsp_icm20608.h,直接贴到icm20608reg.h,并做了部分删除,只留下寄存器相关宏定义:

#ifndef _BSP_ICM20608_H
#define _BSP_ICM20608_H
/***************************************************************
Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
文件名	: 	 bsp_icm20608.h
作者	   : 左忠凯
版本	   : V1.0
描述	   : ICM20608驱动文件。
其他	   : 无
论坛 	   : www.wtmembed.com
日志	   : 初版V1.0 2019/3/26 左忠凯创建
***************************************************************/

#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


3.5 驱动框架

        先写个框架,后面再填。

#include<linux/module.h>
#include<linux/input.h>
#include<linux/spi/spi.h>
#include<linux/cdev.h>
#include<linux/device.h>
#include<linux/fs.h>
#include<linux/uaccess.h>
#include<linux/string.h>
#include<linux/delay.h>

#define ICM20608_CNT 1
#define ICM20608_NAME "icm20608"

static int icm20608_probe(struct spi_device* spi){
    printk("probe\r\n");
    return 0;
}
static int icm20608_remove(struct spi_device* spi){
    return 0;
}

// 传统匹配
static const struct spi_device_id icm20608_id[] = {
    {"alientek,icm20608", 0},
    {  },
};
// 设备树匹配
static const struct of_device_id icm20608_of_match[] = {
    {.compatible = "alientek,icm20608", 0},
    {  },
};

struct spi_driver icm20608_driver = {
    .probe = icm20608_probe,
    .remove = icm20608_remove,
    .driver = {
        .name = "icm20608",
        .owner = THIS_MODULE,
        .of_match_table = icm20608_of_match,
    },
    .id_table = icm20608_id,
};

// 驱动入口
static int __init icm20608_init(void){
    int ret = 0;
    ret = spi_register_driver(&icm20608_driver);
    return ret;
}

// 驱动出口
static void __exit icm20608_exit(void){
    spi_unregister_driver(&icm20608_driver);
}

module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");

        测试一下,看看能否正确匹配:

# VSCODE终端
make
sudo cp icm20608.ko /.../rootfs/lib/modules/4.1.15/

# 串口
depmod
modprobe icm20608.ko  # 应打印"probe"

3.6 spi数据读写函数

        读不到数据,找了半天问题,突然想起来自己的mini版根本没有icm20608,气笑了

        这里一定要用spi_write_then_read函数发送,不要自己用spi_sync 像IIC那样分两段发送,否则在两段数据的中间 片选信号会被拉高,导致被当做两段数据,最后读写错误

// SPI读
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len){
    int ret;
    struct spi_device *spi = (struct spi_device *)dev->private_data;
    u8 tx = reg | 0x80;    // 读数据时,要将最高位置1

    ret = spi_write_then_read(spi, &tx, 1, buf, len);

    if (ret < 0)
        pr_err("icm20608: spi_read reg 0x%x failed %d\n", reg, ret);
    return ret;
}
// SPI写
static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, int len){
    int ret;
    struct spi_device *spi = (struct spi_device *)dev->private_data;
    u8 tx[len + 1];
    // 如果len长度太大,为了避免炸栈,更保险的写法是给txdata分配堆空间:
    // u8 *tx = kzalloc(len+1,GFP_KERNEL);

    tx[0] = reg & ~0x80;        // 写数据时,地址最高位置0
    memcpy(&tx[1], buf, len);

    ret = spi_write(spi, tx, len + 1);

    // 如果分了堆,记得free掉:
    // kfree(tx);

    if (ret < 0)
        pr_err("icm20608: spi_write reg 0x%x failed %d\n", reg, ret);
    return ret;
}

3.7 完整驱动代码

#include<linux/module.h>
#include<linux/input.h>
#include<linux/spi/spi.h>
#include<linux/cdev.h>
#include<linux/device.h>
#include<linux/fs.h>
#include<linux/uaccess.h>
#include<linux/string.h>
#include<linux/delay.h>
#include<linux/gpio.h>
#include<linux/of_gpio.h>
#include "icm20608reg.h"

#define ICM20608_CNT 1
#define ICM20608_NAME "icm20608"
///设备结构体/////////////////////////////////////////////////////////////////////////////////////

struct icm20608_dev{
    int major;
    int minor;
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    void *private_data;   // spi设备指针挂这里
    int cs_gpio;  // 片选引脚
    struct device_node *nd;  // 设备树节点

    signed int gyro_x;  // 陀螺仪三轴原始值
    signed int gyro_y;
    signed int gyro_z;
    signed int accel_x; // 加速度计三轴原始值
    signed int accel_y;
    signed int accel_z;
    signed int temp_adc;// 温度计原始值
};
static struct icm20608_dev icm20608dev;

// SPI读
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len){
    int ret;
    struct spi_device *spi = (struct spi_device *)dev->private_data;
    u8 tx = reg | 0x80;    // 最高位=1 表示读

    ret = spi_write_then_read(spi, &tx, 1, buf, len);

    if (ret < 0)
        pr_err("icm20608: spi_read reg 0x%x failed %d\n", reg, ret);
    return ret;
}

// SPI写
static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, int len){
    int ret;
    struct spi_device *spi = (struct spi_device *)dev->private_data;
    u8 *tx = kzalloc(len+1,GFP_KERNEL);

    tx[0] = reg & ~0x80;        // 写操作最高位为 0
    memcpy(&tx[1], buf, len);

    ret = spi_write(spi, tx, len + 1);
    kfree(tx);
    if (ret < 0)
        pr_err("icm20608: spi_write reg 0x%x failed %d\n", reg, ret);
    
    return ret;
}

// icm20608初始化
void icm20608_dev_init(struct icm20608_dev *dev){
    u8 value = 0;
    u8 t = 0x80;
    icm20608_write_regs(dev, ICM20_PWR_MGMT_1, &t, 1); // 复位。复位后会自动进入睡眠模式
    mdelay(50);
    t = 0x01;
    icm20608_write_regs(dev, ICM20_PWR_MGMT_1, &t,1); // 关闭睡眠模式,自动选择时钟
    mdelay(50);

    icm20608_read_regs(dev, ICM20_WHO_AM_I, &value, 1);
    printk("ICM20608 ID = %#x\r\n", value);

    t = 0x00;
    icm20608_write_regs(dev, ICM20_SMPLRT_DIV, &t, 1); // 输出速率=内部采样率
    t = 0x18;
    icm20608_write_regs(dev, ICM20_GYRO_CONFIG, &t, 1); // 陀螺仪±2000dps量程
    t = 0x18;
    icm20608_write_regs(dev, ICM20_ACCEL_CONFIG, &t, 1); // 加速度计±16g量程
    t = 0x04;
    icm20608_write_regs(dev, ICM20_CONFIG, &t, 1); // 陀螺仪低通滤波BW=20Hz
    t = 0x04;
    icm20608_write_regs(dev, ICM20_ACCEL_CONFIG2, &t, 1); // 加速度计低通滤波BW=21.2Hz
    t = 0x00;
    icm20608_write_regs(dev, ICM20_PWR_MGMT_2, &t, 1); // 打开加速度计和陀螺仪所有轴
    t = 0x00;
    icm20608_write_regs(dev, ICM20_LP_MODE_CFG, &t, 1); // 关闭低功耗
    t = 0x00;
    icm20608_write_regs(dev, ICM20_FIFO_EN, &t, 1); // 关闭FIFO


}

// 读取icm20608值
void icm20608_readdata(struct icm20608_dev *dev){
    unsigned char data[14]; // 7个寄存器,高低8位

    // 从ICM20_ACCEL_XOUT_H开始,连续读14个寄存器
    icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);

    dev->accel_x = (signed short)((data[0]<<8) | data[1]);
    dev->accel_y = (signed short)((data[0]<<8) | data[3]);
    dev->accel_z = (signed short)((data[0]<<8) | data[5]);
    dev->temp_adc= (signed short)((data[0]<<8) | data[7]);
    dev->gyro_x  = (signed short)((data[0]<<8) | data[9]);
    dev->gyro_y  = (signed short)((data[0]<<8) | data[11]);
    dev->gyro_z  = (signed short)((data[0]<<8) | data[13]);
}

static int icm20608_open(struct inode *inode, struct file* filp){
    filp->private_data = &icm20608dev; 
    return 0;
}
static ssize_t icm20608_read(struct file* filp, char __user *buf, size_t cnt, loff_t *off){
    signed int data[7];
    long err = 0;
    struct icm20608_dev *dev = (struct icm20608_dev *)filp->private_data;

    icm20608_dev_init(dev);
    data[0] = dev->accel_x;
    data[1] = dev->accel_y;
    data[2] = dev->accel_z;
    data[3] = dev->temp_adc;
    data[4] = dev->gyro_x;
    data[5] = dev->gyro_y;
    data[6] = dev->gyro_z;
    err = copy_to_user(buf, data, sizeof(data));
    return 0;
}
static int icm20608_release(struct inode *inode, struct file* filp){
    return 0;
}

static const struct file_operations icm20608_fops = {
    .owner = THIS_MODULE,
    .open = icm20608_open,
    .read = icm20608_read,
    .release = icm20608_release,
};


///驱动结构体/////////////////////////////////////////////////////////////////////////////////////

static int icm20608_probe(struct spi_device* spi){
    int ret = 0;

    // 1、devid
    icm20608dev.major = 0;
    if(icm20608dev.major){ // 已给定主设备号
        icm20608dev.devid = MKDEV(icm20608dev.major,0);
        ret = register_chrdev_region(icm20608dev.devid, ICM20608_CNT, ICM20608_NAME);
    } else {  // 系统分配主设备号
        ret = alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT, ICM20608_NAME);
        icm20608dev.major = MAJOR(icm20608dev.devid);
        icm20608dev.minor = MINOR(icm20608dev.devid);
    }
    if(ret < 0){
        printk("chrdev region error!\r\n");
        goto fail_devid;
    }
    printk("major = %d, minor = %d\r\n", icm20608dev.major, icm20608dev.minor);

    // 2、 cdev
    icm20608dev.cdev.owner = THIS_MODULE;
    cdev_init(&icm20608dev.cdev, &icm20608_fops);
    ret = cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);
    if(ret<0){
        printk("cdev error!\r\n");
        goto fail_cdev;
    }

    // 3、class
    icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);
    if(IS_ERR(icm20608dev.class)){
        ret = PTR_ERR(icm20608dev.class);
        goto fail_class;
    }

    // 4、device
    icm20608dev.device = device_create(icm20608dev.class, NULL, icm20608dev.devid, NULL, ICM20608_NAME);
    if(IS_ERR(icm20608dev.device)){
        ret = PTR_ERR(icm20608dev.device);
        goto fail_device;
    }

    // 5、设置设备结构体的私有数据
    icm20608dev.private_data = spi;

    // 6、spi设备初始化
    spi->mode = SPI_MODE_0; // 原始MicroWire,MODE0,CPOL=0,CPHA=0
    spi_setup(spi);

    // 7、设备初始化
    icm20608_dev_init(&icm20608dev);

    return 0;

fail_device:
    class_destroy(icm20608dev.class);
fail_class:
    cdev_del(&icm20608dev.cdev);
fail_cdev:
    unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);
fail_devid:
    return ret;
}

static int icm20608_remove(struct spi_device* spi){
    int ret = 0;
    device_destroy(icm20608dev.class, icm20608dev.devid);
    class_destroy(icm20608dev.class);
    cdev_del(&icm20608dev.cdev);
    unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);

    return 0; // 懒得写错误处理了
}


// 传统匹配
static const struct spi_device_id icm20608_id[] = {
    {"alientek,icm20608", 0},
    {  },
};
// 设备树匹配
static const struct of_device_id icm20608_of_match[] = {
    {.compatible = "alientek,icm20608", 0},
    {  },
};

struct spi_driver icm20608_driver = {
    .probe = icm20608_probe,
    .remove = icm20608_remove,
    .driver = {
        .name = "icm20608",
        .owner = THIS_MODULE,
        .of_match_table = icm20608_of_match,
    },
    .id_table = icm20608_id,
};

////////////////////////////////////////////////////////////////////////////////////////
// 驱动入口
static int __init icm20608_init(void){
    int ret = 0;
    ret = spi_register_driver(&icm20608_driver);
    // printk("注册结果:%d", &ret);
    return ret;
}

// 驱动出口
static void __exit icm20608_exit(void){
    spi_unregister_driver(&icm20608_driver);
}

module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");

3.8 应用程序

直接从《指南》中62.5.3.1示例代码搬过来:

#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>

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;
}

3.9 测试

make
arm-linux-gnueabihf-gcc icm20608APP.c -o icm20608APP
sudo cp icm20608.ko icm20608APP /home/for/linux/nfs/rootfs/lib/modules/4.1.15/

四、0.96寸OLED驱动

        这部分代码以mini板为准,参考OLED屏实验 — [野火]嵌入式Linux驱动开发实战指南

        芯片数据参考0.96寸OLED显示模块 — 正点原子资料下载中心 1.0.0 文档中《SSD1306-Revision 1.1 (Charge Pump).pdf》

        使用7针引脚0.96寸OLED,SSD1306芯片(如果是6针的去掉CS脚的配置即可)

4.1 设备树

4.1.1 配置引脚

        首先确定要连哪些脚。SCLK、MOSI和片选与icm20608配置一样(我这里直接替换掉icm20608了。如果想同时使用icm20608,可以换个片选脚)。DC、RES直接使用GPIO控制。具体选择哪些脚复用为GPIO参考arch/arm/boot/dts/imx6ul-pinfunc.h的定义。

OLED引脚功能对应开发板引脚 / 复用
D0时钟SCLKUART2_RXD→ECSPI3_SCLK
D1MOSIUART2_CTS→ECSPI3_MOSI
CS片选UART2_TXD→GPIO1_IO20
DC

数据 / 命令控制引脚

拉低命令,拉高数据

UART2_RTS→GPIO1_IO23
RES

复位

拉低复位,拉高正常

UART3_TXD→GPIO1_IO24

在imx6ull-alientek-emmc.dts的&iomuxc下新增pinctrl_ecspi3: spi_oled:

		// 新增 2025/11/26 SPI-OLED
		pinctrl_ecspi3: spi_oled{
			fsl,pins = <
				MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20   0x10b0
				MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK  0x10b1
				MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI    0x10b1
				MX6UL_PAD_UART2_RTS_B__GPIO1_IO23	  0x10b0
				MX6UL_PAD_UART3_TX_DATA__GPIO1_IO24   0x10b0
			>;
		};

        查找包含UART2_TX_DATA、UART2_RX_DATA、UART2_RTS、UART2_CTS、UART3_TX_DATA的代码,将其注释掉。

4.1.2 配置设备节点

在imx6ull-alientek-emmc.dts最后面增加ecspi3部分:
(与icm20608的配置基本一致,只是增加了DC和RES的gpio,以及compatible)

// 2025/11/9 新增
&ecspi3 {
	fsl,spi-num-chipselects = <1>;  // 1个片选 Contains the number of the chipselect
	// cs-gpios = <&gpio4 9 0>;
	cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>; // 使用gpio1_20做软件片选
	dc-gpios = <&gpio1 23 GPIO_ACTIVE_LOW>; // gpio1_23  DC
	res-gpios= <&gpio1 24 GPIO_ACTIVE_LOW>; // gpio1_24  RES
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_ecspi3>;
	status = "okay";

	// 2025/11/26 新增
	spidev1: ecspi_oled@0 {
		// #address-cells = <1>;  spidev1下面没有子节点了,这两个cells可以删掉
		// #size-cells = <1>;
		compatible = "alientek,ecspi_oled";
		spi-max-frequency = <8000000>; // SPI时钟最大频率
		reg = <0>;
	};
};

编译、复制到tftp:

make dtbs
sudo cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb  .../tftpboot/

 4.2 文件结构

        与icm20608结构基本一致。可以直接在其基础上修改

23_OLED (工作区)
├── .vscode
│   ├── c_cpp_properties.json
│   └── settings.json
├── 23_oled.code-workspace
├── Makefile
├── oledreg.h
├── oled.c
└── oledAPP.c

4.3 SSD1306寻址方式

以下截图均来自《SSD1306-Revision 1.1 (Charge Pump).pdf》

        0.96寸OLED屏幕显示为128像素宽、64像素高的范围。如图8-13,将其水平分割为8个页,每一个页负责管理一个128像素宽、8像素高的范围。

        每个页中,一列是8个像素、共128列。一个列的8个像素刚好可以用一个字节的数据来表示,而数据都是以字节为最小单位来发送的,因此可以将一个页的最小单位理解为(PAGEx, COLx),也就是一个页中的一列。
        后面三种寻址方式都是以这么个基本单位进行的。也就是说,不论(PAGEx, COLx)外部/之间是水平方向写还是垂直方向写,(PAGEx, COLx)内部总是垂直方向写数据的。

4.3.1 页寻址方式

        使用列指针、页指针这两个指针pointer来确定要写哪一个(PAGEx,COLx)。
        在页寻址中,每写完一个(PAGEx,COLx),列指针会自动+1,变成(PAGEx,COLx+1)。如果列指针写完column end address,列指针会自动指向column start address,而页指针不会动。因此需要手动配置页指针+1。
        另外,column end address和column start address可以用命令自定义,包括后面两种寻址方式中的page end address和page start address也可以自定义,使得指针可以在用户指定的一小块区域内循环。

4.3.2 水平寻址

        在页寻址的基础上,增加了页指针自动+1的设定。每写一个字节的数据,列指针自动+1,如果列指针达到column end address,列指针自动置为column start address,且页指针自动+1。

        同时如果写完最后一个(PAGEx,COLx),也就是列指针和页指针都分别到达page和column的edd address,即给定范围的右下角区域,就会自动返回给定范围的左上角区域(page和column的start address)。

4.3.3 垂直寻址

        与水平寻址类似,页指针和列指针都能自动+1、到右下角自动返回。但是垂直寻址时竖着写的,优先给页指针+1,页指针到达page end address后才会给列指针+1。

4.4 OLED相关函数

4.4.1 命令/数据发送函数

发送命令时,OLED不需要指定寄存器地址,拉低DC直接发送命令即可。

// is_data为False发送命令,为True发送数据
static int oled_send(struct oled_dev *dev, u8 buf, int len, bool is_data){
    int ret = 0;
    struct spi_device *spi = (struct spi_device *)dev->private_data;

    // 拉低DC脚发送命令,拉高DC发送数据
    gpio_direction_output(oleddev.dc_gpio, is_data ? 1 : 0);

    ret = spi_write(spi, &buf, len);

    if (ret != 0){
        pr_err("oled_send error! \r\n");
        return -1;
    }
    return ret;
}

4.4.2 OLED初始化函数

        照着这个流程图以及正点原子下载的原厂芯片手册《SSD1306-Revision 1.1 (Charge Pump).pdf》第10部分即可。

// 初始化
void oled_dev_init(void){
    // 具体的配置直接在原厂芯片手册pdf中搜索“命令+h”即可,如搜索“AEh”即可找到开关显示的部分

    gpio_direction_output(oleddev.res_gpio, 0); mdelay(50);// RES拉低
    gpio_direction_output(oleddev.res_gpio, 1); mdelay(50);// RES拉高

    oled_send(&oleddev, 0xAE, 1, false);  mdelay(10);// 关显示

    oled_send(&oleddev, 0xD5, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x80, 1, false);mdelay(10);// 设置显示时钟分频比/震荡频率

    oled_send(&oleddev, 0xA8, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x3F, 1, false);mdelay(10);// 复用比

    oled_send(&oleddev, 0xD3, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x00, 1, false);mdelay(10);// 设置显示偏移(不便宜)
    
    oled_send(&oleddev, 0x40, 1, false);mdelay(10);// 设置起始行(0行)

    oled_send(&oleddev, 0x8D, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x14, 1, false);mdelay(10);// 打开电荷泵

    oled_send(&oleddev, 0xA1, 1, false);mdelay(10);// 设置段重映射(用于左右翻转)
    oled_send(&oleddev, 0xC8, 1, false);mdelay(10);// 设置COM扫描方向(用于上下翻转)

    oled_send(&oleddev, 0xDA, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x12, 1, false);mdelay(10);// 设置COM引脚硬件配置

    oled_send(&oleddev, 0x81, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x66, 1, false);mdelay(10);// 设置对比度(推荐值66,嫌不够亮可以调高,最大0xFF)

    oled_send(&oleddev, 0xD9, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x22, 1, false);mdelay(10);// 预充电时间(像素从上一帧状态变成下一帧状态用几个时钟周期)

    oled_send(&oleddev, 0xDB, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x30, 1, false);mdelay(10);// 设置 VCOMH 取消选通电平

    oled_send(&oleddev, 0xA4, 1, false);mdelay(10);// 取消 整屏点亮(若为0xA5,则忽略RAM内容,整屏点亮)(详见原厂芯片手册10.1.9)

    oled_send(&oleddev, 0xA6, 1, false);mdelay(10);// 正常(A6)/反相(A7)显示

    oled_send(&oleddev, 0x20, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x00, 1, false);mdelay(10);// 设置水平寻址(默认是页寻址)(详见原厂芯片手册10.1.3)

    oled_send(&oleddev, 0x21, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x00, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x7F, 1, false);mdelay(10);// 设置列地址范围0~127(详见原厂芯片手册10.1.4)

    oled_send(&oleddev, 0x22, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x00, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x07, 1, false);mdelay(10);// 设置页地址范围0~7(详见原厂芯片手册10.1.5)

    oled_send(&oleddev, 0xAF, 1, false);mdelay(100);// 开启显示

    // oled_send(&oleddev, 0xA5, 1, false);mdelay(1000); // 调试用的
    // oled_send(&oleddev, 0xA4, 1, false);mdelay(10);

    oled_clear();

    oled_show_string8x16(20,16,"hello world");

    pr_info("OLED init finished!\r\n");
}

4.4.3 字库

        从某个N年前的stm32文件里掏出来的8*16字库,应该是C51+阴码+逐列式。放到oledreg.h文件中。

//16*16 ASCII字符集点阵
const unsigned char asc2_1608[][16]={	  
    {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*" ",0*/
    {0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x33,0x30,0x00,0x00,0x00},/*"!",1*/
    {0x00,0x10,0x0C,0x06,0x10,0x0C,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*""",2*/
    {0x40,0xC0,0x78,0x40,0xC0,0x78,0x40,0x00,0x04,0x3F,0x04,0x04,0x3F,0x04,0x04,0x00},/*"#",3*/
    {0x00,0x70,0x88,0xFC,0x08,0x30,0x00,0x00,0x00,0x18,0x20,0xFF,0x21,0x1E,0x00,0x00},/*"$",4*/
    {0xF0,0x08,0xF0,0x00,0xE0,0x18,0x00,0x00,0x00,0x21,0x1C,0x03,0x1E,0x21,0x1E,0x00},/*"%",5*/
    {0x00,0xF0,0x08,0x88,0x70,0x00,0x00,0x00,0x1E,0x21,0x23,0x24,0x19,0x27,0x21,0x10},/*"&",6*/
    {0x10,0x16,0x0E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"'",7*/
    {0x00,0x00,0x00,0xE0,0x18,0x04,0x02,0x00,0x00,0x00,0x00,0x07,0x18,0x20,0x40,0x00},/*"(",8*/
    {0x00,0x02,0x04,0x18,0xE0,0x00,0x00,0x00,0x00,0x40,0x20,0x18,0x07,0x00,0x00,0x00},/*")",9*/
    {0x40,0x40,0x80,0xF0,0x80,0x40,0x40,0x00,0x02,0x02,0x01,0x0F,0x01,0x02,0x02,0x00},/*"*",10*/
    {0x00,0x00,0x00,0xF0,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x1F,0x01,0x01,0x01,0x00},/*"+",11*/
    {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0xB0,0x70,0x00,0x00,0x00,0x00,0x00},/*",",12*/
    {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01},/*"-",13*/
    {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00,0x00,0x00},/*".",14*/
    {0x00,0x00,0x00,0x00,0x80,0x60,0x18,0x04,0x00,0x60,0x18,0x06,0x01,0x00,0x00,0x00},/*"/",15 */
    {0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00},/*"0",16*/
    {0x00,0x10,0x10,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},/*"1",17*/
    {0x00,0x70,0x08,0x08,0x08,0x88,0x70,0x00,0x00,0x30,0x28,0x24,0x22,0x21,0x30,0x00},/*"2",18*/
    {0x00,0x30,0x08,0x88,0x88,0x48,0x30,0x00,0x00,0x18,0x20,0x20,0x20,0x11,0x0E,0x00},/*"3",19*/
    {0x00,0x00,0xC0,0x20,0x10,0xF8,0x00,0x00,0x00,0x07,0x04,0x24,0x24,0x3F,0x24,0x00},/*"4",20*/
    {0x00,0xF8,0x08,0x88,0x88,0x08,0x08,0x00,0x00,0x19,0x21,0x20,0x20,0x11,0x0E,0x00},/*"5",21*/
    {0x00,0xE0,0x10,0x88,0x88,0x18,0x00,0x00,0x00,0x0F,0x11,0x20,0x20,0x11,0x0E,0x00},/*"6",22*/
    {0x00,0x38,0x08,0x08,0xC8,0x38,0x08,0x00,0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x00},/*"7",23*/
    {0x00,0x70,0x88,0x08,0x08,0x88,0x70,0x00,0x00,0x1C,0x22,0x21,0x21,0x22,0x1C,0x00},/*"8",24*/
    {0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,0x00,0x00,0x31,0x22,0x22,0x11,0x0F,0x00},/*"9",25*/
    {0x00,0x00,0x00,0xC0,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00},/*":",26*/
    {0x00,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x60,0x00,0x00,0x00,0x00},/*";",27*/
    {0x00,0x00,0x80,0x40,0x20,0x10,0x08,0x00,0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x00},/*"<",28*/
    {0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x00,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00},/*"=",29*/
    {0x00,0x08,0x10,0x20,0x40,0x80,0x00,0x00,0x00,0x20,0x10,0x08,0x04,0x02,0x01,0x00},/*">",30*/
    {0x00,0x70,0x48,0x08,0x08,0x08,0xF0,0x00,0x00,0x00,0x00,0x30,0x36,0x01,0x00,0x00},/*"?",31*/
    {0xC0,0x30,0xC8,0x28,0xE8,0x10,0xE0,0x00,0x07,0x18,0x27,0x24,0x23,0x14,0x0B,0x00},/*"@",32*/
    {0x00,0x00,0xC0,0x38,0xE0,0x00,0x00,0x00,0x20,0x3C,0x23,0x02,0x02,0x27,0x38,0x20},/*"A",33*/
    {0x08,0xF8,0x88,0x88,0x88,0x70,0x00,0x00,0x20,0x3F,0x20,0x20,0x20,0x11,0x0E,0x00},/*"B",34*/
    {0xC0,0x30,0x08,0x08,0x08,0x08,0x38,0x00,0x07,0x18,0x20,0x20,0x20,0x10,0x08,0x00},/*"C",35*/
    {0x08,0xF8,0x08,0x08,0x08,0x10,0xE0,0x00,0x20,0x3F,0x20,0x20,0x20,0x10,0x0F,0x00},/*"D",36*/
    {0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,0x20,0x3F,0x20,0x20,0x23,0x20,0x18,0x00},/*"E",37*/
    {0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,0x20,0x3F,0x20,0x00,0x03,0x00,0x00,0x00},/*"F",38*/
    {0xC0,0x30,0x08,0x08,0x08,0x38,0x00,0x00,0x07,0x18,0x20,0x20,0x22,0x1E,0x02,0x00},/*"G",39*/
    {0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,0x20,0x3F,0x21,0x01,0x01,0x21,0x3F,0x20},/*"H",40*/
    {0x00,0x08,0x08,0xF8,0x08,0x08,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},/*"I",41*/
    {0x00,0x00,0x08,0x08,0xF8,0x08,0x08,0x00,0xC0,0x80,0x80,0x80,0x7F,0x00,0x00,0x00},/*"J",42*/
    {0x08,0xF8,0x88,0xC0,0x28,0x18,0x08,0x00,0x20,0x3F,0x20,0x01,0x26,0x38,0x20,0x00},/*"K",43*/
    {0x08,0xF8,0x08,0x00,0x00,0x00,0x00,0x00,0x20,0x3F,0x20,0x20,0x20,0x20,0x30,0x00},/*"L",44*/
    {0x08,0xF8,0xF8,0x00,0xF8,0xF8,0x08,0x00,0x20,0x3F,0x00,0x3F,0x00,0x3F,0x20,0x00},/*"M",45*/
    {0x08,0xF8,0x30,0xC0,0x00,0x08,0xF8,0x08,0x20,0x3F,0x20,0x00,0x07,0x18,0x3F,0x00},/*"N",46*/
    {0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,0x0F,0x10,0x20,0x20,0x20,0x10,0x0F,0x00},/*"O",47*/
    {0x08,0xF8,0x08,0x08,0x08,0x08,0xF0,0x00,0x20,0x3F,0x21,0x01,0x01,0x01,0x00,0x00},/*"P",48*/
    {0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,0x0F,0x18,0x24,0x24,0x38,0x50,0x4F,0x00},/*"Q",49*/
    {0x08,0xF8,0x88,0x88,0x88,0x88,0x70,0x00,0x20,0x3F,0x20,0x00,0x03,0x0C,0x30,0x20},/*"R",50*/
    {0x00,0x70,0x88,0x08,0x08,0x08,0x38,0x00,0x00,0x38,0x20,0x21,0x21,0x22,0x1C,0x00},/*"S",51*/
    {0x18,0x08,0x08,0xF8,0x08,0x08,0x18,0x00,0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00},/*"T",52*/
    {0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00},/*"U",53*/
    {0x08,0x78,0x88,0x00,0x00,0xC8,0x38,0x08,0x00,0x00,0x07,0x38,0x0E,0x01,0x00,0x00},/*"V",54*/
    {0xF8,0x08,0x00,0xF8,0x00,0x08,0xF8,0x00,0x03,0x3C,0x07,0x00,0x07,0x3C,0x03,0x00},/*"W",55*/
    {0x08,0x18,0x68,0x80,0x80,0x68,0x18,0x08,0x20,0x30,0x2C,0x03,0x03,0x2C,0x30,0x20},/*"X",56*/
    {0x08,0x38,0xC8,0x00,0xC8,0x38,0x08,0x00,0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00},/*"Y",57*/
    {0x10,0x08,0x08,0x08,0xC8,0x38,0x08,0x00,0x20,0x38,0x26,0x21,0x20,0x20,0x18,0x00},/*"Z",58*/
    {0x00,0x00,0x00,0xFE,0x02,0x02,0x02,0x00,0x00,0x00,0x00,0x7F,0x40,0x40,0x40,0x00},/*"[",59*/
    {0x00,0x0C,0x30,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x06,0x38,0xC0,0x00},/*"\",60*/
    {0x00,0x02,0x02,0x02,0xFE,0x00,0x00,0x00,0x00,0x40,0x40,0x40,0x7F,0x00,0x00,0x00},/*"]",61*/
    {0x00,0x00,0x04,0x02,0x02,0x02,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"^",62*/
    {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80},/*"_",63*/
    {0x00,0x02,0x02,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"`",64*/
    {0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x19,0x24,0x22,0x22,0x22,0x3F,0x20},/*"a",65*/
    {0x08,0xF8,0x00,0x80,0x80,0x00,0x00,0x00,0x00,0x3F,0x11,0x20,0x20,0x11,0x0E,0x00},/*"b",66*/
    {0x00,0x00,0x00,0x80,0x80,0x80,0x00,0x00,0x00,0x0E,0x11,0x20,0x20,0x20,0x11,0x00},/*"c",67*/
    {0x00,0x00,0x00,0x80,0x80,0x88,0xF8,0x00,0x00,0x0E,0x11,0x20,0x20,0x10,0x3F,0x20},/*"d",68*/
    {0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x1F,0x22,0x22,0x22,0x22,0x13,0x00},/*"e",69*/
    {0x00,0x80,0x80,0xF0,0x88,0x88,0x88,0x18,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},/*"f",70*/
    {0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x6B,0x94,0x94,0x94,0x93,0x60,0x00},/*"g",71*/
    {0x08,0xF8,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20},/*"h",72*/
    {0x00,0x80,0x98,0x98,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},/*"i",73*/
    {0x00,0x00,0x00,0x80,0x98,0x98,0x00,0x00,0x00,0xC0,0x80,0x80,0x80,0x7F,0x00,0x00},/*"j",74*/
    {0x08,0xF8,0x00,0x00,0x80,0x80,0x80,0x00,0x20,0x3F,0x24,0x02,0x2D,0x30,0x20,0x00},/*"k",75*/
    {0x00,0x08,0x08,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},/*"l",76*/
    {0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x00,0x20,0x3F,0x20,0x00,0x3F,0x20,0x00,0x3F},/*"m",77*/
    {0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20},/*"n",78*/
    {0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00},/*"o",79*/
    {0x80,0x80,0x00,0x80,0x80,0x00,0x00,0x00,0x80,0xFF,0xA1,0x20,0x20,0x11,0x0E,0x00},/*"p",80*/
    {0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x0E,0x11,0x20,0x20,0xA0,0xFF,0x80},/*"q",81*/
    {0x80,0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x20,0x20,0x3F,0x21,0x20,0x00,0x01,0x00},/*"r",82*/
    {0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x33,0x24,0x24,0x24,0x24,0x19,0x00},/*"s",83*/
    {0x00,0x80,0x80,0xE0,0x80,0x80,0x00,0x00,0x00,0x00,0x00,0x1F,0x20,0x20,0x00,0x00},/*"t",84*/
    {0x80,0x80,0x00,0x00,0x00,0x80,0x80,0x00,0x00,0x1F,0x20,0x20,0x20,0x10,0x3F,0x20},/*"u",85*/
    {0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,0x00,0x01,0x0E,0x30,0x08,0x06,0x01,0x00},/*"v",86*/
    {0x80,0x80,0x00,0x80,0x00,0x80,0x80,0x80,0x0F,0x30,0x0C,0x03,0x0C,0x30,0x0F,0x00},/*"w",87*/
    {0x00,0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x31,0x2E,0x0E,0x31,0x20,0x00},/*"x",88*/
    {0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,0x80,0x81,0x8E,0x70,0x18,0x06,0x01,0x00},/*"y",89*/
    {0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x21,0x30,0x2C,0x22,0x21,0x30,0x00},/*"z",90*/
    {0x00,0x00,0x00,0x00,0x80,0x7C,0x02,0x02,0x00,0x00,0x00,0x00,0x00,0x3F,0x40,0x40},/*"{",91*/
    {0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00},/*"|",92*/
    {0x00,0x02,0x02,0x7C,0x80,0x00,0x00,0x00,0x00,0x40,0x40,0x3F,0x00,0x00,0x00,0x00},/*"}",93*/
    {0x00,0x06,0x01,0x01,0x02,0x02,0x04,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"~",94*/
};

4.4.4 字符显示&字符串显示&清屏函数

        已经忘了stm32的时候官方驱动是怎么写的了(好像根本就没看过)
        这个函数比较简单,只能按照页的排版显示(y必须是8的倍数)

// 页寻址,列范围0~127,页范围0~7。
// 字符为8*16字符
// xy为左上角坐标
void oled_show_char8x16(uint8_t x, uint8_t y, char ch){
    uint8_t page;
    const unsigned char *pfont;
    int i;

    if (x > 120)return;   // x最大=128-8
    if (y > 48)return;    // y最大=64-16
    if (y % 8 != 0)return;// 只支持按页对齐。也就是字符必须在两个页中,不能穿过3个页

    page = y / 8;

    if (ch < ' ' || ch > '~') // 字库只有这些字符
        ch = ' ';   // 如果是未定义字符,直接输出空格
    pfont = asc2_1608[(uint8_t)ch - ' '];//在字库数组的下标索引

    // 画上半部分(上8行)========================================
    oled_send(&oleddev, 0xB0 + page, 1, false);              // 设置页地址
    oled_send(&oleddev, 0x00 + (x & 0x0F), 1, false);        // 设置列低4位
    oled_send(&oleddev, 0x10 + ((x >> 4) & 0x0F), 1, false); // 设置列高4位

    for (i = 0; i < 8; i++) {
        oled_send(&oleddev, pfont[i], 1, true);
    }

    // 画下半部分(下8行)========================================
    oled_send(&oleddev, 0xB0 + page + 1, 1, false);
    oled_send(&oleddev, 0x00 + (x & 0x0F), 1, false);
    oled_send(&oleddev, 0x10 + ((x >> 4) & 0x0F), 1, false);

    for (i = 0; i < 8; i++) {
        oled_send(&oleddev, pfont[i + 8], 1, true);
    }
}
void oled_show_string8x16(uint8_t x, uint8_t y, const char *str){
    oled_send(&oleddev, 0xA4, 1, false);// 取消 整屏点亮
    oled_send(&oleddev, 0x20, 1, false);// 
    oled_send(&oleddev, 0x02, 1, false);// 设置为页寻址
    
    while (*str) {
        // 如果已经到屏幕右边了,显示不下,退出
        if (x > 120) {
            break;
        }
        oled_show_char8x16(x, y, *str);
        x += 8;
        str++;
    }
}
void oled_clear(void){
    int page, col;

    oled_send(&oleddev, 0xA4, 1, false);// 取消 整屏点亮

    oled_send(&oleddev, 0x20, 1, false);// 
    oled_send(&oleddev, 0x02, 1, false);// 设置为页寻址

    for (page = 0; page < 8; page++) {
        oled_send(&oleddev, 0xB0 + page, 1, false); // 选择当前页.0xB0~0xB7表示8个PAGE
        oled_send(&oleddev, 0x00, 1, false);        // 设置列地址低4位为0
        oled_send(&oleddev, 0x10, 1, false);        // 设置列地址高4位为0,即从第0列开始

        for (col = 0; col < 128; col++) {
            oled_send(&oleddev, 0x00, 1, true);     // 当前页的128列全部写0
        }
    }

    // 清屏结束后,把页地址和列地址恢复到左上角
    oled_send(&oleddev, 0xB0, 1, false);            // 回到第0页
    oled_send(&oleddev, 0x00, 1, false);            // 列低4位清0
    oled_send(&oleddev, 0x10, 1, false);            // 列高4位清0
}

4.5 完整驱动代码

#include<linux/module.h>
#include<linux/input.h>
#include<linux/spi/spi.h>
#include<linux/cdev.h>
#include<linux/device.h>
#include<linux/fs.h>
#include<linux/uaccess.h>
#include<linux/string.h>
#include<linux/delay.h>
#include<linux/gpio.h>
#include<linux/of_gpio.h>
#include "oledreg.h"

#define OLED_CNT 1
#define OLED_NAME "ecspi_oled"

struct oled_dev{
    int major;
    int minor;
    dev_t id;
    struct cdev cdev;
    struct class *class;
    struct device *device;   
    void *private_data;      // spi设备指针
    struct device_node *nd;  // 设备树节点

    int res_gpio;
    int dc_gpio;
};
static struct oled_dev oleddev;

//// OLED操作函数 /////////////////////////////////////////////////////////////////////////////

// is_data为False发送命令,为True发送数据
static int oled_send(struct oled_dev *dev, u8 buf, int len, bool is_data){
    int ret = 0;
    struct spi_device *spi = (struct spi_device *)dev->private_data;

    // 拉低DC脚发送命令,拉高DC发送数据
    gpio_direction_output(oleddev.dc_gpio,is_data ? 1 : 0);

    ret = spi_write(spi, &buf, len);

    if (ret != 0){
        pr_err("oled_send error! \r\n");
        return -1;
    }
    return ret;
}
//清屏
void oled_clear(void){
    int i = 0;
    oled_send(&oleddev, 0xA4, 1, false);mdelay(10);// 取消 整屏点亮

    oled_send(&oleddev, 0x20, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x00, 1, false);mdelay(10);// 设置水平寻址(默认是页寻址)(详见原厂芯片手册10.1.3)

    oled_send(&oleddev, 0x21, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x00, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x7F, 1, false);mdelay(10);// 设置列地址范围0~127(详见原厂芯片手册10.1.4)

    oled_send(&oleddev, 0x22, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x00, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x07, 1, false);mdelay(10);// 设置页地址范围0~7(详见原厂芯片手册10.1.5)

    for (i = 0; i < 128 * 8; ++i) {
        oled_send(&oleddev, 0x00, 1, true);
        // mdelay(10);
    }
}


// 水平寻址,列范围0~127,页范围0~7。
// 字符为8*16字符
// xy为左上角坐标
void oled_show_char8x16(uint8_t x, uint8_t y, char ch){
    uint8_t page;
    const unsigned char *pfont;
    int i;

    // 检查坐标,避免越界
    if (x > 120)   // x最大=128-8
        return;
    if (y > 48)    // y最大=64-16
        return;
    if (y % 8 != 0) {
        // 现在只能8个对齐,
        return;
    }
    page = y / 8;

    // 字库我只放了这些字符
    if (ch < ' ' || ch > '~')
        ch = ' ';
    pfont = asc2_1608[(uint8_t)ch - ' '];

    // 画上半部分(上8行)========================================
    // 设置列地址范围x~x+7
    oled_send(&oleddev, 0x21, 1, false);
    oled_send(&oleddev, x,   1, false);
    oled_send(&oleddev, x+7, 1, false);

    // 设置页地址范围page~page(只画这一页)
    oled_send(&oleddev, 0x22, 1, false);
    oled_send(&oleddev, page, 1, false);
    oled_send(&oleddev, page, 1, false);

    for (i = 0; i < 8; i++) {
        uint8_t data = pfont[i];
        oled_send(&oleddev, data, 1, true);   // 发送数据
    }

    // 画下半部分(下8行)========================================
    // oled_send(&oleddev, 0x21, 1, false);
    // oled_send(&oleddev, x,   1, false);
    // oled_send(&oleddev, x+7, 1, false);

    // 设置页地址范围page+1~page+1
    oled_send(&oleddev, 0x22, 1, false);
    oled_send(&oleddev, page+1, 1, false);
    oled_send(&oleddev, page+1, 1, false);

    for (i = 0; i < 8; i++) {
        uint8_t data = pfont[i + 8];  // 后 8 字节是下半部分
        oled_send(&oleddev, data, 1, true);
    }
}

void oled_show_string8x16(uint8_t x, uint8_t y, const char *str){
    while (*str) {
        // 如果已经到屏幕右边了,显示不下,退出
        if (x > 120) {
            break;
        }
        oled_show_char8x16(x, y, *str);
        x += 8;
        str++;
    }
}

// 初始化
void oled_dev_init(void){
    // 具体的配置直接在原厂芯片手册pdf中搜索“命令+h”即可,如搜索“AEh”即可找到开关显示的部分

    gpio_direction_output(oleddev.res_gpio, 0); mdelay(50);// RES拉低
    gpio_direction_output(oleddev.res_gpio, 1); mdelay(50);// RES拉高

    oled_send(&oleddev, 0xAE, 1, false);  mdelay(10);// 关显示

    oled_send(&oleddev, 0xD5, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x80, 1, false);mdelay(10);// 设置显示时钟分频比/震荡频率

    oled_send(&oleddev, 0xA8, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x3F, 1, false);mdelay(10);// 复用比

    oled_send(&oleddev, 0xD3, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x00, 1, false);mdelay(10);// 设置显示偏移(不便宜)
    
    oled_send(&oleddev, 0x40, 1, false);mdelay(10);// 设置起始行(0行)

    oled_send(&oleddev, 0x8D, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x14, 1, false);mdelay(10);// 打开电荷泵

    oled_send(&oleddev, 0xA1, 1, false);mdelay(10);// 设置段重映射(用于左右翻转)
    oled_send(&oleddev, 0xC8, 1, false);mdelay(10);// 设置COM扫描方向(用于上下翻转)

    oled_send(&oleddev, 0xDA, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x12, 1, false);mdelay(10);// 设置COM引脚硬件配置

    oled_send(&oleddev, 0x81, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x66, 1, false);mdelay(10);// 设置对比度(推荐值66,嫌不够亮可以调高,最大0xFF)

    oled_send(&oleddev, 0xD9, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x22, 1, false);mdelay(10);// 预充电时间(像素从上一帧状态变成下一帧状态用几个时钟周期)

    oled_send(&oleddev, 0xDB, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x30, 1, false);mdelay(10);// 设置 VCOMH 取消选通电平

    oled_send(&oleddev, 0xA4, 1, false);mdelay(10);// 取消 整屏点亮(若为0xA5,则忽略RAM内容,整屏点亮)(详见原厂芯片手册10.1.9)

    oled_send(&oleddev, 0xA6, 1, false);mdelay(10);// 正常(A6)/反相(A7)显示

    oled_send(&oleddev, 0x20, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x00, 1, false);mdelay(10);// 设置水平寻址(默认是页寻址)(详见原厂芯片手册10.1.3)

    oled_send(&oleddev, 0x21, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x00, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x7F, 1, false);mdelay(10);// 设置列地址范围0~127(详见原厂芯片手册10.1.4)

    oled_send(&oleddev, 0x22, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x00, 1, false);mdelay(10);// 
    oled_send(&oleddev, 0x07, 1, false);mdelay(10);// 设置页地址范围0~7(详见原厂芯片手册10.1.5)

    oled_send(&oleddev, 0xAF, 1, false);mdelay(100);// 开启显示

    // oled_send(&oleddev, 0xA5, 1, false);mdelay(1000); // 调试用的
    // oled_send(&oleddev, 0xA4, 1, false);mdelay(10);

    oled_clear();

    oled_show_string8x16(20,16,"hello world");

    pr_info("OLED init finished!\r\n");
}



//// 设备操作集函数 /////////////////////////////////////////////////////////////////////////////

static int oled_open(struct inode *inode, struct file* filp){
    filp->private_data = &oleddev;
    return 0;
}
static int oled_release(struct inode *inode, struct file* filp){
    return 0;
}
static ssize_t oled_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off){
    struct oled_dev *dev = (struct oled_dev*)filp->private_data;
    return 0;
}

static ssize_t oled_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){
    char k[233];
    size_t len;
    if(count >= sizeof(k)) len = sizeof(k)-1;
    else len = count;


    if (copy_from_user(k, buf, len))
        return -EFAULT;
    k[len] = '\0';

    oled_clear();

    oled_show_string8x16(0,0,k);

    return len;
}
static const struct file_operations oled_fops = {
    .owner = THIS_MODULE,
    .open = oled_open,
    .release = oled_release,
    .read = oled_read,
    .write = oled_write,
};







//spi驱动///////////////////////////////////////////////

static int oled_probe(struct spi_device* spi){
    pr_info("probe\r\n");

    int ret = 0;
    // 1、devid
    oleddev.major = 0;
    if(oleddev.major){ // 已给定主设备号
        oleddev.id = MKDEV(oleddev.major,0);
        ret = register_chrdev_region(oleddev.id, OLED_CNT, OLED_NAME);
    } else {  // 系统分配主设备号
        ret = alloc_chrdev_region(&oleddev.id, 0, OLED_CNT, OLED_NAME);
        oleddev.major = MAJOR(oleddev.id);
        oleddev.minor = MINOR(oleddev.id);
    }
    if(ret < 0){
        pr_err("chrdev region error!\r\n");
        goto fail_devid;
    }
    pr_info("major = %d, minor = %d\r\n", oleddev.major, oleddev.minor);

    // 2、cdev
    oleddev.cdev.owner = THIS_MODULE;
    cdev_init(&oleddev.cdev, &oled_fops);
    ret = cdev_add(&oleddev.cdev, oleddev.id, OLED_CNT);
    if(ret < 0){
        pr_err("cdev error!\r\n");
        goto fail_cdev;
    }

    // 3、class
    oleddev.class = class_create(THIS_MODULE, OLED_NAME);
    if(IS_ERR(oleddev.class)){
        pr_err("class error!\r\n");
        goto fail_class;
    }

    // 4、device
    oleddev.device = device_create(oleddev.class, NULL, oleddev.id, NULL, OLED_NAME);
    if(IS_ERR(oleddev.device)){
        pr_err("device error!\r\n");
        goto fail_device;
    }

    // 5、设备结构体私有数据
    oleddev.private_data = spi;

    // 6、spi设备初始化
    spi->mode = SPI_MODE_0;
    spi_setup(spi);

    // 7、申请GPIO
    // 先找device node
    // 然后获取GPIO号

    // 在imx6ull.dtsi中,可以找到ecspi3的位置为:
    // /soc/aips-bus@02000000/spba-bus@02000000/下有ecspi3: ecspi@02010000
    // 使用命令ls /sys/firmware/devicetree/base/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02010000
    // 也可以看到我们刚才新增的dc-gpios和res-gpios
    // 因此可以确定path路径如下:
    oleddev.nd = of_find_node_by_path("/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02010000");
    // oleddev.nd = of_get_parent(spi->dev.of_node) // 也可用of_get_parent
    if (!oleddev.nd) {
        pr_err("ecspi3 node not found\r\n");
        goto fail_find_node;
    }

    // dc gpio
    oleddev.dc_gpio = of_get_named_gpio(oleddev.nd, "dc-gpios", 0);
    if (!gpio_is_valid(oleddev.dc_gpio)) {
        pr_err("dc-gpios invalid: %d\r\n", oleddev.dc_gpio);
        ret = oleddev.dc_gpio;
        goto fail_get_dc_gpio;
    }
    ret = gpio_request(oleddev.dc_gpio, "dc_gpio");
    if(ret < 0){
        pr_err("dc-gpios request error!\r\b");
        goto fail_request_dc_gpio;
    }

    // res gpio
    oleddev.res_gpio = of_get_named_gpio(oleddev.nd, "res-gpios", 0);
    if (!gpio_is_valid(oleddev.res_gpio)) {
        pr_err("dc-gpios invalid: %d\r\n", oleddev.res_gpio);
        ret = oleddev.res_gpio;
        goto fail_get_res_gpio;
    }
    ret = gpio_request(oleddev.res_gpio, "dc_gpio");
    if(ret < 0){
        pr_err("dc-gpios request error!\r\b");
        goto fail_request_res_gpio;
    }


    // 8、oled初始化
    oled_dev_init();

    return 0;




fail_request_res_gpio:
fail_get_res_gpio:
    gpio_free(oleddev.dc_gpio);
fail_request_dc_gpio:
fail_get_dc_gpio:

fail_find_node:
    device_destroy(oleddev.class, oleddev.id);
fail_device:
    class_destroy(oleddev.class);
fail_class:
    cdev_del(&oleddev.cdev);
fail_cdev:
    unregister_chrdev_region(oleddev.id, OLED_CNT);
fail_devid:
    return ret;
}

static int oled_remove(struct spi_device* spi){
    oled_clear();
    oled_send(&oleddev, 0xAE, 1, false);  mdelay(10); // display off
    gpio_free(oleddev.dc_gpio);
    gpio_free(oleddev.res_gpio);
    device_destroy(oleddev.class, oleddev.id);
    class_destroy(oleddev.class);
    cdev_del(&oleddev.cdev);
    unregister_chrdev_region(oleddev.id, OLED_CNT);
    return 0;
}

// 传统匹配
static const struct spi_device_id oled_id[] = {
    {"alientek,ecspi_oled", 0},
    {  },
};
// 设备树匹配
static const struct of_device_id oled_of_match[] = {
    {.compatible = "alientek,ecspi_oled", 0},
    {  },
};

struct spi_driver oled_driver = {
    .probe = oled_probe,
    .remove = oled_remove,
    .driver = {
        .name = "ecspi_oled",
        .owner = THIS_MODULE,
        .of_match_table = oled_of_match,
    },
    .id_table = oled_id,
};

//////////////////////////////////////////////////

// 驱动入口
static int __init oled_init(void){
    int ret = 0;
    pr_info("oled init\r\n");
    ret = spi_register_driver(&oled_driver);
    return ret;
}

// 驱动出口
static void __exit oled_exit(void){
    pr_info("oled exit\r\n");
    spi_unregister_driver(&oled_driver);
}

module_init(oled_init);
module_exit(oled_exit);
MODULE_LICENSE("GPL");

4.6 应用程序

#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>

/* 
 * @description    : main主程序 
 * @param - argc   : argv数组元素个数 
 * @param - argv   : 具体参数 
 * @return         : 0 成功; else失败
 * 调用  ./ecspi_oled /dev/ecspi_oled string
 */

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("Error Usage!\r\n");
        return -1;
    }

    char *filename = argv[1];
    int fd = open(filename, O_RDWR);

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

    write(fd, argv[2], strlen(argv[2]));


    close(fd); /* 关闭文件 */ 
    return 0;
}


4.7 测试

# VSCODE
make
arm-linux-gnueabihf-gcc oledAPP.c -o oledAPP
sudo cp oled.ko oledAPP /home/for/linux/nfs/rootfs/lib/modules/4.1.15/

# 串口
cd /lib/modules/4.1.15/
depmod
modprobe oled.ko
./ecspi_oled /dev/ecspi_oled "test"  # 此时屏幕上应显示test

更多推荐