Linux驱动基于SPI子系统的GC9A01驱动程序
前言
为了巩固之前学习的SPI子系统本人写了一个GC9A01的屏幕驱动,现记录一下,以供大家参考
一、添加设备树节点
设备树部分比较简单,大家如果想正确编写出设备树,主要是要参考datasheet来进行配置,
对于GC9A01,它的硬件分布为如下:
引脚名称 | 引脚功能 |
---|---|
GND | 电源负,地 |
VCC | 电源正,3.3 - 5V,需要与通信电平一致 |
SCL | SPI时钟信号输入端口 |
SDA | SPI数据输入端口 |
DC | 数据/命令选择,低电平命令,高电平数据 |
CS | 片选,低电平使能 |
BLK | 背光,悬空使能接地关闭,默认上拉至3.3V |
在知道引脚功能后我们应该在对开发板的引脚进行分析,搞清楚我们外设要接在哪个引脚,用那些引脚进行我们的SPI通讯,下面的lubancat2的外置引脚引出图:
然后我们需要查看datasheet来确定SPI的一些模式:
此处的数据手册来源:技术手册
然后在根据SPI的模式和引脚编写我们的设备树:
&spi3{
status = "okay";
pinctrl-names = "default", "high_speed";
pinctrl-0 = <&spi3m1_cs0 &spi3m1_pins>;
pinctrl-1 = <&spi3m1_cs0 &spi3m1_pins_hs>;
cs-gpios = <&gpio4 RK_PC6 GPIO_ACTIVE_LOW>;
spi_gc9a01:spi_gc9a01@0 {
compatible = "ailun,gc9a01";
//片选和时钟最大频率必须添加否则会进不去probe函数
reg = <0>; //选择片选0
//spi-cpha; //添加这一项表示cpha设置为1,不添加则默认为0
//spi-cpol; //添加这一项表示cpol设置为1,不添加则默认为0
//spi-lsb-first; //添加这一项表示从低位开始传输数据,不添加则默认从高位开始传输
//spi-cs-high //添加这一项表示片选高电平选中,默认低电平选中
spi-max-frequency = <45000000>; //最大时钟频率12MHZ ,RK3568最大不能超过50MHZ
dc_control_pin = <&gpio3 RK_PA7 GPIO_ACTIVE_HIGH>;
res_control_pin = <&gpio3 RK_PA6 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&spi_oled_dc_pin>;
pinctrl-1 = <&spi_oled_res_pin>;
status = "okay";
};
};
&pinctrl {
spi_oled {
spi_oled_dc_pin: spi_oled_dc_pin {
rockchip,pins = <3 RK_PA7 RK_FUNC_GPIO &pcfg_pull_none>;
};
spi_oled_res_pin: spi_oled_res_pin {
rockchip,pins = <3 RK_PA6 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
};
编写完毕后使用命令编译设备树文件。(此处我使用的lubancat2的设备树插件功能编写的设备树节点,不知道的可以查看我的上一篇文章,如果你是其他开发板根据自己开发板的加载设备树的方式进行编译即可)
编译完加载到开发板后,我们会在这个节点下看到自己的节点:
二、编写驱动部分
想写好这部分驱动我们先需要了解SPI子系统的框架:
对于SPI子系统来说,主要的四个结构体为:
- spi_master(spi_controller):对soc的SPI控制器的抽象
- spi_bus_type: spi的bus_type,代表了硬件上的SPI Bus
- spi_device :spi 从设备
- spi_driver: spi具体设备的驱动
(关于这几个结构体我只做了简单的介绍对于内部实现还需要去理解下)
对于这几个结构体的实现方式同样是遵循的Linux设备模型(LDM数据结构)进行实现的
除此之外我们还需要了解SPI数据传输的关键结构体:
spi的数据传输主要使用了spi_transfer和spi_message结构,多个spi_transfer构成一个spi_message。
- spi_transfer :填充要发送的数据
- spi_message:发送数据出去
在你了解这几部分后我们就可以正式开始编写驱动了!
1,首先编写一个发送命令的函数
注意:(此处我直接从发送数据开始讲起,如果你不熟悉SPI控制器的注册流程请翻看我的其他文章)
/*
* 函数名:lcd_send_command
* 描述:向LCD发送命令
* 参数:
* spi_device:指向SPI设备结构体的指针
* command:指向命令数据的指针
* lenght:命令数据的长度
* value:DC引脚的电平值
* 返回值:
* 错误代码,0表示成功,-1表示失败
*/
static int lcd_send_command(struct spi_device *spi_device, uint8_t *command, u16 lenght, int value)
{
int error = 0;
struct spi_message *message; // 定义SPI消息结构体指针
struct spi_transfer *transfer; // 定义SPI传输结构体指针
/* 申请空间 */
message = kzalloc(sizeof(struct spi_message), GFP_KERNEL); // 从内核堆中分配内存给SPI消息结构体
transfer = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); // 从内核堆中分配内存给SPI传输结构体
/* 设置DC引脚为低电平,准备发送消息 */
gpio_direction_output(gc9a01_gpio.dc_control_pin_number, value);
/* 填充message和transfer */
transfer->tx_buf = command; // 设置传输的命令数据缓冲区
transfer->len = lenght; // 设置传输的命令数据长度
spi_message_init(message); // 初始化SPI消息结构体
spi_message_add_tail(transfer, message); // 将传输结构体添加到消息结构体尾部
/* 同步发送SPI消息 */
error = spi_sync(spi_device, message); // 发送SPI消息,并返回错误代码
kfree(message); // 释放SPI消息结构体所占用的内存
kfree(transfer); // 释放SPI传输结构体所占用的内存
if (error != 0) {
printk("spi_sync error!\n"); // 如果发送消息失败,打印错误信息
return -1; // 返回错误代码
}
return error; // 返回错误代码,0表示成功,-1表示失败
}
然后继续编写出发送一个指令的函数,此处我是防照STM32驱动GC9A01来编写的:
int value_command=0;//用于表示是发指令
int value_data=1; //用于表示是发数据
void LCD_WR_REG(uint8_t data,int value)
{
uint8_t out_buf[1] = {data};
lcd_send_command(oled_spi_device, out_buf, sizeof(out_buf),value);
}
接下来就是常见的一些列枯燥的发送指令和数据了,大家可以仿照STM32的驱动方式进行编写:

三、编写用户应用程序测试
此处我使用的是ioctl方式对屏幕进行控制的,来选择要发送什么数据内容:
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
// 文件名
#define DEVICE_NAME "/dev/GC9A01"
// LCD显示参数
typedef struct {
unsigned int x; // x坐标
unsigned int y; // y坐标
unsigned char size; // 字符大小
unsigned char mode; // 显示模式
char dis_char[255]; // 显示内容
} image_t;
// 定义读写的请求指令
#define UACCESS_STRUCT _IOWR('b',0,image_t)
// 使用方法和stm32的基本显示函数一样
void LCD_ShowString(int fd, unsigned int x, unsigned int y, unsigned char size, char src_array[255], unsigned char mode) {
image_t Lcd_fops;
Lcd_fops.x = x;
Lcd_fops.y = y;
Lcd_fops.size = size;
strcpy(Lcd_fops.dis_char, src_array);
Lcd_fops.mode = mode;
ioctl(fd, UACCESS_STRUCT, &Lcd_fops);
}
// 清空输入缓冲区 因为读取输入流后会留下一个换行符残留是你输入确认后留下的
void clear_input_buffer() {
int c;
while ((c = getchar()) != '\n' && c != EOF) {}
}
int main(int argc, const char *argv[]) {
int fd = -1;
fd = open(DEVICE_NAME, O_RDWR);
if (-1 == fd) {
perror("open is error");
exit(1);
}
image_t Lcd_display = {0};
while (1) {
printf("请输入显示内容:");
fgets(Lcd_display.dis_char, sizeof(Lcd_display.dis_char), stdin); // 从标准输入流中读取一行字符
clear_input_buffer(); // 清空输入缓冲区
printf("请输入x坐标:");
scanf("%d", &Lcd_display.x);
clear_input_buffer(); // 清空输入缓冲区
printf("请输入y坐标:");
scanf("%d", &Lcd_display.y);
clear_input_buffer(); // 清空输入缓冲区
printf("请输入显示大小:");
scanf("%hhu", &Lcd_display.size);
clear_input_buffer(); // 清空输入缓冲区
printf("请输入显示模式:");
scanf("%hhu", &Lcd_display.mode);
clear_input_buffer(); // 清空输入缓冲区
LCD_ShowString(fd, Lcd_display.x, Lcd_display.y, Lcd_display.size, Lcd_display.dis_char, Lcd_display.mode);
// printf("你输入的是 = %s\n",str);
}
close(fd);
return 0;
}