Linux驱动开发笔记(二十)——SPI (ICM20608驱动+0.96寸OLED驱动)
视频:第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,对应寄存器为:
| 寄存器名 | 内容(二进制) | 含义 |
| HYS | 0 | Hysteresis Disabled 关闭输入迟滞 |
| PUS | 0 | 100K Ohm Pull Down 100K欧姆下拉 |
| PUE | 0 | Keeper。选择Keeper而不是Pull。此时PUS配置的上下拉不生效 |
| PKE | 1 | Pull/Keeper Enabled 使能Kepper/Pull。此时PUS和PUE才有效 |
| ODE | 0 | Open Drain Disabled 关闭开漏 |
| SPEED | 10 | medium(100 MHz) |
| DSE | 110 | R0/6 |
| SRE | 0 | Slow 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 | 时钟SCLK | UART2_RXD→ECSPI3_SCLK |
| D1 | MOSI | UART2_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

更多推荐

所有评论(0)