1. 开发环境介绍
1.1 野火的IMX.6ULL PRO开发板
移植了Linux5.4操作系统,所以本次的驱动是适用于该版本的,至于其他linux版本,有可能有些函数有差异,不过大部分都是一样的
1.2 ST7735S TFT 液晶屏
该液晶屏分辨率为128x128,使用SPI通信
2. 连线说明
从上一张图片中看出,我们需要使用该oled的VCC, GND, LED, CLK, SDI, RS, RST, CS总共8个引脚,每个引脚连接到linux开发板上。开发板有多个spi接口,我是用的spi1这个接口,而且在配置引脚复用的时候,配置的GPIO4.IO28(MISO), GPIO4.IO27(MOSI), GPIO4.IO25(CLK), GPIO4.IO26(SS)
3. 设备树节点
我的设备树默认并没有开启SPI,所以
&ecspi1 { fsl,spi-num-chipselects = <1>; cs-gpio = <&gpio4 26 GPIO_ACTIVE_LOW>; /* 设置片选引脚,供子节点使用 */ pinctrl-names = "default"; pinctrl-0 = <&pinctrl_ecspi1>; status = "okay"; #address-cells = <1>; #size-cells = <0>; /* 上面这几行是配置SPI,如果你的SPI已经配置好了就不需要上面这几行 */ oled: st7735s@0 { compatible = "fire,st7735s"; reg = <0>; /* reg = <index>; 指定片选引脚,就是父节点的cs-gpio中第index个 */ spi-max-frequency = <8000000>; /* 指定设备的最高速度 */ /* GPIO_ACTIVE_HIGH目的是指定有效电平, 使用gpiod_set_value()设置的是逻辑电平 */ dc-gpio = <&gpio4 23 GPIO_ACTIVE_HIGH>; /* 数据/命令配置引脚 */ reset-gpio = <&gpio4 24 GPIO_ACTIVE_HIGH>; /* 复位引脚 */ }; }; /* 配置引脚复用 */ &iomuxc { pinctrl_ecspi1:ecspi1grp { fsl,pins = < MX6UL_PAD_CSI_DATA05__ECSPI1_SS0 0x1a090 MX6UL_PAD_CSI_DATA04__ECSPI1_SCLK 0x11090 MX6UL_PAD_CSI_DATA06__ECSPI1_MOSI 0x11090 MX6UL_PAD_CSI_DATA07__ECSPI1_MISO 0x11090 >; }; };
3. 驱动程序编写步骤
3.1 在驱动入口注册spi_driver, 在出口注销spi_driver
static int __init oled_spi_init(void) { int ret; ret = spi_register_driver(&oled_spi_driver); return 0; } static void __exit oled_spi_exit(void) { spi_unregister_driver(&oled_spi_driver); } module_init(oled_spi_init); module_exit(oled_spi_exit); MODULE_LICENSE("GPL v2");
3.2 在probe函数内部初始化液晶屏硬件设备,注册fire_oparation结构体
static int oled_spi_probe(struct spi_device *spi) { printk("===========%s %d=============\n", __FUNCTION__, __LINE__); oled_dev = spi; // 从设备树获取资源 dc_pin = gpiod_get(&spi->dev, "dc", GPIOD_OUT_HIGH); reset_pin = gpiod_get(&spi->dev, "reset", GPIOD_OUT_HIGH); if (dc_pin == NULL || dc_pin == NULL) { printk("=========引脚错误=======\n"); return -1; } /* 注册字符设备 */ major = register_chrdev(0, "oled", &oled_fops); /* class_create */ oled_class = class_create(THIS_MODULE, "oled_class"); /* device_create */ oled_device = device_create(oled_class, NULL, MKDEV(major, 0), NULL, "myoled"); /* 初始化硬件 */ Oled_Init(); mdelay(100); //清屏 Oled_Clear(WHITE); // 分配空间, 在iotcl函数内会使用到 data_buf = vmalloc(128*128*2); if (data_buf == NULL) { printk("malloc error\n"); return -1; } return 0; }
3.3 编写基础的函数操作oled
包括写入8位指令,写入8位数据,oled复位等基础操作
//向液晶屏写一个8位指令 void Oled_WriteIndex(u8 cmd) { int ret = 0; gpiod_set_value(dc_pin, 0); ret = spi_write(oled_dev, &cmd, 1); } //向液晶屏写一个8位数据 void Oled_WriteData(u8 data) { gpiod_set_value(dc_pin, 1); spi_write(oled_dev, &data, 1); } // oled复位 void Oled_Reset(void) { gpiod_set_value(reset_pin, 0); mdelay(100); gpiod_set_value(reset_pin, 1); mdelay(50); }
3.4 完善ioctl函数
该函数的作用就是应用程序通过该函数来操作硬件。所以该函数应该根据应用程序的需要进行更改。
static long oled_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { void __user *from = (void __user *)arg; unsigned char param_buf[10]; int size; int ret = 0; int i = 0, j = 0; int m = 0; switch(cmd & 0xff) { case OLED_GET_SCREEN_X_PIXEL: { // 获取x_max return X_MAX_PIXEL; } case OLED_GET_SCREEN_Y_PIXEL: { // 获取y_max return Y_MAX_PIXEL; } case OLED_SET_REGION: // start_x, start_y, end_x, end_y { // 设置显示区域 ret = copy_from_user(param_buf, from, 4); //printk("start: (%d, %d), end: (%d, %d)\n", param_buf[0], param_buf[1], param_buf[2], param_buf[3]); Oled_SetRegion(param_buf[0], param_buf[1], param_buf[2], param_buf[3]); //Oled_WriteIndex(0x2C); return 0; } case OLED_WRITE_COLOR: { // 写入Color数据 size = cmd >> 8; //printk("size: %d\n", size); ret = copy_from_user(data_buf, from, size); for (m=0;m<size;m+=2) { Oled_WriteData_16Bit(data_buf[m] << 8 | data_buf[m+1]); } return 0; } case OLED_CEALR: { printk("clear screen use %lx\n", arg & 0xffff); // 清屏 Oled_Clear(arg); return 0; } } return -ENXIO; }
4. 将基础操作封装起来
如此封装的目的就是为了以后好操作,不需要去使用open, ioctl来操作,直接用封装好了的来操作oled
4.1 头文件
#ifndef __OLEDLIB_H #define __OLEDLIB_H #define BLACK 0x0000 #define WHITE 0xFFFF #define RED 0xF800 #define GREEN 0x07E0 #define BLUE 0x001F // 字体大小 #define ENGLISH_FONT_WIDTH 8 #define ENGLISH_FONT_HEIGHT 16 #define CHINESE_FONT_WIDTH 16 #define CHINESE_FONT_HEIGHT 16 // 文字间隙 #define INTERVAL_X 2 #define INTERVAL_Y 1 // ioctl 参数 #define OLED_SET_REGION 1 #define OLED_WRITE_COLOR 2 #define OLED_ALL_COLOR 3 #define OLED_CEALR 4 #define OLED_DEBUG 5 #define OLED_GET_SCREEN_X_PIXEL 6 #define OLED_GET_SCREEN_Y_PIXEL 7 // 设备分辨率 #define X_MAX_PIXEL 128 #define Y_MAX_PIXEL 128 // 提供的函数 int oled_open(char *devpath); // 打开设备 void oled_close(void); // 关闭设备 short rgb888_to_rgb565(int color); // rgb888---->rgb565 void oled_show_text(int start_x, int start_y, char *p); // 显示文字,可英文中文混合 void oled_set_font_color(unsigned short color); // 设置字体显示 void oled_set_font_back(unsigned short color); // 设置字体背景 int oled_clear_screen(unsigned short color); // 指定颜色刷屏 #define FONTDATAMAX 4096 // 英文字母(8x16) static const unsigned char english_fontdata[FONTDATAMAX] = { /* 0 0x00 '^@' */ 0x00, /* 00000000 */ 0x00, /* 00000000 */ 0x00, /* 00000000 */ 0x00, /* 00000000 */ 0x00, /* 00000000 */ 0x00, /* 00000000 */ 0x00, /* 00000000 */ ...... ...... }
4.2 主要的函数
oled_open函数主要用来打开文件,打开中文字库文件,并使用mmap映射到应用空间,方便其他函数的读取。
// 打开oled设备,使用的HZK16字库文件 int oled_open(char *devpath) { struct stat st; oled_fd = open(devpath, O_RDWR); if (oled_fd < 0) { printf("can't open /dev/myoled\n"); return -1; } // 获取设备分辨率 x_max = ioctl(oled_fd, OLED_GET_SCREEN_X_PIXEL); y_max = ioctl(oled_fd, OLED_GET_SCREEN_Y_PIXEL); printf("x_max: %d, y_max: %d\n", x_max, y_max); // 映射中文字库文件 if ((hzk_fd = open("./HZK16", O_RDONLY)) == -1) { perror("open error"); return -1; } fstat(hzk_fd, &st); hzkmem_size = st.st_size; hzkmem = (unsigned char *)mmap(NULL, hzkmem_size, PROT_READ, MAP_SHARED, hzk_fd, 0); if (hzkmem == (unsigned char *)-1) { printf("can't mmap\n"); return -1; } }
oled_show_ascii() 函数用于显示单个ASCII字符。通过ASCII字符的码值在数组english_fontdata找到头地址,然后遍历该字符的字模,每个像素点转为一个16位的565的颜色保存在data数组内部,转换完成后通过ioctl设置显示区域,然后即将data数组通过ioctl传递给驱动程序,驱动程序就能够将数据显示出来。
/* 显示单个英文字符 */ static void oled_show_ascii(int x, int y, unsigned char c, unsigned short color) { int ret = 0; int i = 0; int j = 0; int m = 0; int debug = 0; unsigned char params[10]; unsigned char temp = 0; unsigned char *dots = (unsigned char *)&english_fontdata[c*ENGLISH_FONT_HEIGHT]; params[0] = x & 0xff; // x_start params[1] = y & 0xff; // y_start params[2] = params[0] + ENGLISH_FONT_WIDTH - 1; // x_end params[3] = params[1] + ENGLISH_FONT_HEIGHT -1; // y_end // 必须提前把数据构造好,再一次性传给驱动程序 for (i=0;i<ENGLISH_FONT_HEIGHT*ENGLISH_FONT_WIDTH/8;i++) { temp = *(dots+i); //printf("0x%x\n", temp); for (j=7;j>=0;j--) { //printf("compare: %d\n", temp & (1 << j)); if (temp & (1 << j)) { //printf("1\n"); data[m] = color >> 8; data[m+1] = color & 0xff; } else { //printf("0\n"); data[m] = background_color >> 8; data[m+1] = background_color & 0xff; } m += 2; } } // 设置范围 ret = ioctl(oled_fd, OLED_SET_REGION, params); if (ret != 0) { perror("error: "); printf("%s %d: ioctl error\n", __FUNCTION__, __LINE__); return; } //printf("debug111111111\n"); ret = ioctl(oled_fd, (ENGLISH_FONT_HEIGHT*ENGLISH_FONT_WIDTH*2 << 8) | OLED_WRITE_COLOR, data); }
中文字符同理,不同的是本程序使用的是HZK字库,每个字符占用两个字节,根据GB2312的编码规则,第一个字节表明了该字符所在码区,第二个字节表明了该字符在该码区的那一个位置(就相当于,我这里有一栋楼,第一个字节表明在哪一层,第二个字节表明那一个房间)。
/* 显示单个中文字符 */ void oled_show_chinese_char(int x, int y, unsigned char *chinese_char, unsigned short color) { unsigned int area = chinese_char[0] - 0xA1; unsigned int where = chinese_char[1] - 0xA1; unsigned char *pos = &hzkmem[(area*94+where)*32]; unsigned char temp = 0; int i = 0; int j = 0; int m = 0; int ret = 0; unsigned char params[10]; params[0] = x & 0xff; // x_start params[1] = y & 0xff; // y_start params[2] = params[0] + CHINESE_FONT_WIDTH - 1; // x_end params[3] = params[1] + CHINESE_FONT_HEIGHT -1; // y_end // 构造显示该字符所需的数据 for(i=0;i<CHINESE_FONT_WIDTH*CHINESE_FONT_HEIGHT<<2;i++) { temp = pos[i]; for(j=7;j>=0;j--) { if (temp & (1<<j)) { data[m] = color >> 8; data[m+1] = color & 0xff; } else { data[m] = background_color >> 8; data[m+1] = background_color & 0xff; } m += 2; } } // 设置范围 ret = ioctl(oled_fd, OLED_SET_REGION, params); if (ret != 0) { perror("error: "); printf("%s %d: ioctl error\n", __FUNCTION__, __LINE__); return; } ret = ioctl(oled_fd, (CHINESE_FONT_WIDTH*CHINESE_FONT_HEIGHT*2 << 8) | OLED_WRITE_COLOR, data); }
英文字符串的显示只需要遍历字符串,调用oled_show_ascii即可。中文字符串的显示也是遍历字符串,每次循环去两个字节。而中英文混合就需要判断一下了。原理就是gb2312编码的两个字节都大于0xA1,设计之初就是为了兼容ASCII码的吧,只需要判断当前地址所指向的值是否大于0xA1, 如果大于,就是用oled_show_chinese_char处理接下来的两个字节,否则就用oled_show_ascii处理当前字节。
/* 显示字符串(可英文中文混合) * start_x: 起始位置(左上方的点基准点)的x * start_y: 起始位置(左上方的点基准点)的y * p: 要显示的字符串地址 */ void oled_show_text(int start_x, int start_y, char *p) { int i = 0; int current_x = start_x; int current_y = start_y; while (p[i] != '\0') { // 当前字符是字母 if (p[i] < 0xA1) { oled_show_ascii(current_x, current_y, *(p+i), font_color); current_x = current_x + ENGLISH_FONT_WIDTH + INTERVAL_X; //start_y = start_y + ENGLISH_FONT_HEIGHT + INTERVAL_Y; i += 1; // ascii字符占一个字节 } else { oled_show_chinese_char(current_x, current_y, (unsigned char *)(p+i), font_color); current_x = current_x + CHINESE_FONT_WIDTH + INTERVAL_X; //start_y = start_y + CHINESE_FONT_HEIGHT + INTERVAL_Y; i += 2; } } }
4. 效果
前面说的封装起来确实挺复杂的,不过使用起来也真的很香,看看下面的测试程序就知道了,注意: 在编译的时候需要指定输出文件的编码格式,因为我们使用的HZK字库文件是gb2312编码格式的,而默认会将其字符使用utf8来编码,每个字符在不同的编码规范里面的编码值是不一样的,所以在编译时需要使用-fexec-charset=GB2312指定编码格式,不然会出现莫名其妙的字符
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <string.h> #include "./oledlib.h" #define BLACK 0x0000 #define WHITE 0xFFFF #define RED 0xF800 #define GREEN 0x07E0 #define BLUE 0x001F int main(int argc, char **argv) { if (argc != 2) { printf("%s /dev/xxx\n", argv[0]); return -1; } oled_open(argv[1]); //oled_clear_screen(GREEN); oled_set_font_color(BLACK); oled_set_font_back(WHITE); oled_show_text(2, 3, "你好,我是不会学习的小菜鸡,我喜欢编程,也喜欢Linux,希望以后可以走嵌入式Linux这个方向。Good bye!"); oled_close(); return 0; }
运行效果