写点什么

Linux 驱动开发 - 编写 OLED 显示屏驱动

作者:DS小龙哥
  • 2022 年 4 月 10 日
  • 本文字数:6420 字

    阅读完需:约 21 分钟

1. 前言

OLED 显示屏在是智能手环,智能手表上用的非常的多,功耗低,不刺眼,优点特别多。本篇文章就介绍,在 Linux 系统里如何使用 OLED 显示屏,要使用 OLED 显示屏,大致分为两步: (1) 针对 OLED 显示屏编写一个驱动 (2) 编写应用层程序进行测试。


采用的 OLED 显示屏是 0.96 寸 SPI 接口显示屏,分辨率是 128*64,比较便宜,淘宝上非常多。


测试开发板采用友善之臂 Tiny4412,三星的 EXYNOS-4412 芯片,4 核 1.5GHZ,板载 8G-EMMC,2G-DDR。

2. 硬件接线效果




3. 驱动代码

Linux 内核提供了标准 SPI 子系统框架,和前面介绍的 IIC 子系统框架使用类似,代码分为设备端和驱动端,Linux 内核提供子系统的目的就是为了统一驱动编写标准,提高驱动代码的移植性。


本篇文章代码没有采用 SPI 子系统框架,采用单片机惯用的模拟 SPI 时序,对入门而言,代码更容易理解。

3.1 oled.c 驱动示例代码

#include <linux/kernel.h>#include <linux/module.h>#include <linux/miscdevice.h> #include <linux/fs.h>#include <linux/uaccess.h>#include <linux/fb.h>#include <linux/io.h>#include <linux/mm.h>#include <linux/slab.h>#include <linux/gpio.h>#include <linux/delay.h>#include <mach/gpio.h>#include <plat/gpio-cfg.h>
/*定义OLED需要使用的寄存器*/static volatile unsigned int *GPB_CON=NULL;static volatile unsigned int *GPB_DAT=NULL;
//OLED屏幕底层接口#define OLED_SCK(x) if(x){*GPB_DAT|=1<<0;}else{*GPB_DAT&=~(1<<0);}#define OLED_MOSI(x) if(x){*GPB_DAT|=1<<3;}else{*GPB_DAT&=~(1<<3);}#define OLED_RES(x) if(x){*GPB_DAT|=1<<4;}else{*GPB_DAT&=~(1<<4);}#define OLED_DC(x) if(x){*GPB_DAT|=1<<5;}else{*GPB_DAT&=~(1<<5);}#define OLED_CS(x) if(x){*GPB_DAT|=1<<1;}else{*GPB_DAT&=~(1<<1);}
//命令与数据区分#define OLED_CMD 0#define OLED_DAT 1
//函数声明区域static void OLED_WriteOneByte(u8 data,u8 cmd);static u8 OLED_GPIO_Init(void);static void OLED_Init(void);static void OLED_Clear(u8 data);static void OLED_DrawPoint(u8 x,u8 y,u8 c);static void OLED_RefreshGRAM(void);

/*函数功能: OLED对应的GPIO口初始化硬件连接:OLED模块---Tiny4412开发板GND--------GNDVCC--------VCC(5V)D0---------SCL--------------GPB_0D1---------MOSI-------------GPB_3RES--------复位-------------GPB_4DC---------数据/命令--------GPB_5CS---------CS片选-----------GPB_1*/static u8 OLED_GPIO_Init(void){ /*1. 将物理地址转换为虚拟地址*/ GPB_CON=ioremap(0x11400040,4); GPB_DAT=ioremap(0x11400044,4); if(GPB_CON==NULL||GPB_DAT==NULL) { printk("物理地址转换为虚拟地址出现问题!\n"); return -1; } /*2. 配置GPIO口模式*/ *GPB_CON&=0xFF000F00; *GPB_CON|=0x00111011; /*3. 上拉GPIO口*/ OLED_CS(1); OLED_DC(1); OLED_MOSI(1); OLED_RES(1); OLED_SCK(1);}

/*函数功能: OLED屏幕初始化*/static void OLED_Init(void){ /*1. 初始化配置GPIO口*/ OLED_GPIO_Init();
/*2. 执行OLED屏幕的初始化配置*/ OLED_RES(1); udelay(2000); OLED_RES(0); udelay(2000); OLED_RES(1); udelay(2000); OLED_WriteOneByte(0xAE,OLED_CMD); //0xAE表示关显示,0xAF表示开显示
OLED_WriteOneByte(0x00,OLED_CMD); OLED_WriteOneByte(0x10,OLED_CMD);
OLED_WriteOneByte(0x40,OLED_CMD);
OLED_WriteOneByte(0xB0,OLED_CMD);
OLED_WriteOneByte(0x81,OLED_CMD); OLED_WriteOneByte(0xCF,OLED_CMD);
OLED_WriteOneByte(0xA1,OLED_CMD);
OLED_WriteOneByte(0xA6,OLED_CMD);
OLED_WriteOneByte(0xA8,OLED_CMD); OLED_WriteOneByte(0x3F,OLED_CMD);
OLED_WriteOneByte(0xC8,OLED_CMD);
OLED_WriteOneByte(0xD3,OLED_CMD); OLED_WriteOneByte(0x00,OLED_CMD);
OLED_WriteOneByte(0xD5,OLED_CMD); OLED_WriteOneByte(0x80,OLED_CMD);
OLED_WriteOneByte(0xD9,OLED_CMD); OLED_WriteOneByte(0xF1,OLED_CMD);
OLED_WriteOneByte(0xDA,OLED_CMD); OLED_WriteOneByte(0x12,OLED_CMD);
OLED_WriteOneByte(0xDB,OLED_CMD); OLED_WriteOneByte(0x30,OLED_CMD);
OLED_WriteOneByte(0x8D,OLED_CMD); OLED_WriteOneByte(0x14,OLED_CMD);
OLED_WriteOneByte(0xAF,OLED_CMD); //正常模式}
/*函数功能: 写一个字节函数参数: cmd=0表示命令,cmd=1表示数据*/static void OLED_WriteOneByte(u8 data,u8 cmd){ u8 i; /*1. 区分发送数据是命令还是屏幕数据*/ if(cmd){OLED_DC(1);} else {OLED_DC(0);} udelay(2); /*2. 发送实际的数据*/ OLED_CS(0); //选中OLED for(i=0;i<8;i++) { udelay(2); OLED_SCK(0); //告诉从机,主机将要发送数据 if(data&0x80){OLED_MOSI(1);} //发送数据 else {OLED_MOSI(0);} udelay(2); OLED_SCK(1); //告诉从机,主机数据发送完毕 data<<=1; //继续发送下一位数据 } OLED_CS(1); //取消选中OLED OLED_SCK(1); //上拉时钟线,恢复空闲电平}

/*函数功能: 清屏 (开全部灯、关全部灯)*/static void OLED_Clear(u8 data){ u8 i,j; for(i=0;i<8;i++) { OLED_WriteOneByte(0xB0+i,OLED_CMD); //设置页地址 OLED_WriteOneByte(0x10,OLED_CMD); //设置列高起始地址(半字节) OLED_WriteOneByte(0x00,OLED_CMD); //设置列低起始地址(半字节) for(j=0;j<128;j++) { OLED_WriteOneByte(data,OLED_DAT); //写数据 } }}
/*定义显存数组: 8行,每行128列,与OLED屏幕对应*/static u8 OLED_GRAM[8][128];
/*函数功能: 画点函数 x: 横向坐标0~128 y: 纵坐标0~64 c: 1表示亮、0表示灭*/static void OLED_DrawPoint(u8 x,u8 y,u8 c){ u8 page; page=y/8; //得到当前点的页数0/8=0 1/8=0 y=y%8; //得到一列中点的位置。(0~7) //0%8=0 1%8=1 .....7%8=7 8%8=0 9%8=1 ...... if(c) OLED_GRAM[page][x]|=1<<y; else OLED_GRAM[page][x]&=~(1<<y);}
/*函数功能: 刷新数据到OLED显示屏*/static void OLED_RefreshGRAM(void){ u8 i,j; for(i=0;i<8;i++) { OLED_WriteOneByte(0xB0+i,OLED_CMD); //设置页地址 OLED_WriteOneByte(0x10,OLED_CMD); //设置列高起始地址(半字节) OLED_WriteOneByte(0x00,OLED_CMD); //设置列低起始地址(半字节) for(j=0;j<128;j++) { OLED_WriteOneByte(OLED_GRAM[i][j],OLED_DAT); //写数据 } }}
static u8 *mmap_buffer=NULL; /*指针-存放申请空间的首地址*/
static int lcd_open(struct fb_info *info, int user){ mmap_buffer=kmalloc(4096,GFP_ATOMIC); if(mmap_buffer==NULL) { printk("空间申请失败!\n"); return 0; } printk("lcd_open调用成功\n"); return 0;}

static int lcd_mmap(struct fb_info *info, struct vm_area_struct *vma){ vma->vm_flags |= VM_IO;//表示对设备 IO 空间的映射 vma->vm_flags |= VM_RESERVED;//标志该内存区不能被换出,在设备驱动中虚拟页和物理页的关系应该是长期的,应该保留起来,不能随便被别的虚拟页换出 if(remap_pfn_range(vma,//虚拟内存区域,即设备地址将要映射到这里 vma->vm_start,//虚拟空间的起始地址 virt_to_phys(mmap_buffer)>>PAGE_SHIFT,//与物理内存对应的页帧号,物理地址右移 12 位 vma->vm_end - vma->vm_start,//映射区域大小,一般是页大小的整数倍 vma->vm_page_prot))//保护属性, { return -EAGAIN; } printk("(drv)映射的长度:%d\n",vma->vm_end - vma->vm_start); printk("物理地址:0x%X\n",virt_to_phys(mmap_buffer)); /* 开发板的DDR容量: 1G 0x40000000 ~ 0x80000000 0x10000000=256M */ return 0;}
#define _OLED_RefreshGRAM 0x12345 /*将应用层的数据刷新到OLED屏幕上*/#define _OLED_ClearGRAM 0x45678 /*将应用层的数据刷新到OLED屏幕上*/static int lcd_ioctl(struct fb_info *info, unsigned int cmd,unsigned long arg){ switch(cmd) { case _OLED_RefreshGRAM: memcpy(OLED_GRAM,mmap_buffer,1024); //拷贝数据 OLED_RefreshGRAM(); /*刷新*/ break; case _OLED_ClearGRAM: /*清屏*/ OLED_Clear(0); break; } return 0;}
static int lcd_release(struct fb_info *info, int user){ if(mmap_buffer!=NULL) { kfree(mmap_buffer); } printk("lcd_release调用成功\n"); return 0;}

/*帧缓冲设备专用的文件操作接口*/static struct fb_ops fbops={ .fb_open=lcd_open, .fb_release=lcd_release, .fb_mmap=lcd_mmap, .fb_ioctl=lcd_ioctl};

/*帧缓冲的设备结构体*/static struct fb_info lcd_info={ .var= /*可变形参*/ { .xres=128, .yres=64, .bits_per_pixel=1 }, .fix= { .smem_len=4096, .line_length=128 }, .fbops=&fbops};
static int __init tiny4412_oled_init(void){ /*1. 初始化OLED屏幕*/ OLED_Init(); OLED_Clear(0);//清屏为黑色 /*2. 帧缓冲驱动注册*/ if(register_framebuffer(&lcd_info)!=0) { printk("提示: lcd驱动安装失败!\n"); return -1; } else { printk("提示: lcd驱动安装成功!\n"); } return 0;}

static void __exit tiny4412_oled_exit(void){ /*1. 帧缓冲驱动注销*/ if(unregister_framebuffer(&lcd_info)!=0) { printk("提示: lcd驱动卸载失败!\n"); return -1; } else { printk("提示: lcd驱动卸载成功!\n"); } /*2. 解除虚拟地址映射关系*/ iounmap(GPB_CON); iounmap(GPB_DAT);}
module_init(tiny4412_oled_init); /*指定驱动的入口函数*/module_exit(tiny4412_oled_exit); /*指定驱动的出口函数*/MODULE_LICENSE("GPL"); /*指定驱动许可证*/
复制代码

3.2 app.c 应用层代码

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <linux/fb.h>#include <sys/mman.h>#include <string.h>
unsigned char *lcd_mem=NULL; /*LCD的内存地址*/struct fb_fix_screeninfo finfo; /*固定形参*/struct fb_var_screeninfo vinfo; /*可变形参*/ unsigned char font[]={/*-- 文字: 国 --*//*-- 宋体42; 此字体下对应的点阵为:宽x高=56x56 宽/8*高*/0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0xC0,0x00,0x00,0x00,0x07,0x00,0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0xC0,0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0xE0,0x01,0xF0,0x00,0x00,0x00,0x07,0xC0,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x01,0x87,0x80,0x01,0xF0,0x00,0x00,0x03,0xC7,0x80,0x01,0xF7,0xFF,0xFF,0xFF,0xE7,0x80,0x01,0xF3,0xFF,0xFF,0xFF,0xF7,0x80,0x01,0xF1,0xC0,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x06,0x07,0x80,0x01,0xF0,0x00,0x7C,0x0F,0x07,0x80,0x01,0xF1,0xFF,0xFF,0xFF,0x87,0x80,0x01,0xF1,0xFF,0xFF,0xFF,0xC7,0x80,0x01,0xF0,0xF0,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7F,0xC0,0x07,0x80,0x01,0xF0,0x00,0x7D,0xF0,0x07,0x80,0x01,0xF0,0x00,0x7C,0xFC,0x07,0x80,0x01,0xF0,0x00,0x7C,0x7E,0x07,0x80,0x01,0xF0,0x00,0x7C,0x3F,0x07,0x80,0x01,0xF0,0x00,0x7C,0x3F,0x07,0x80,0x01,0xF0,0x00,0x7C,0x1F,0x07,0x80,0x01,0xF0,0x00,0x7C,0x0F,0x07,0x80,0x01,0xF0,0x00,0x7C,0x0E,0x07,0x80,0x01,0xF0,0x00,0x7C,0x07,0x87,0x80,0x01,0xF0,0x00,0x7C,0x03,0xC7,0x80,0x01,0xF0,0x00,0x7C,0x07,0xE7,0x80,0x01,0xFF,0xFF,0xFF,0xFF,0xF7,0x80,0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0x80,0x01,0xF7,0x80,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0x80,0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x00,0x01,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,};
/*定义显存数组: 8行,每行128列,与OLED屏幕对应*/static unsigned char OLED_GRAM[8][128];
/*函数功能: 画点函数 x: 横向坐标0~128 y: 纵坐标0~64 c: 1表示亮、0表示灭*/static void OLED_DrawPoint(unsigned char x,unsigned char y,unsigned char c){ unsigned char page; page=y/8; //得到当前点的页数0/8=0 1/8=0 y=y%8; //得到一列中点的位置。(0~7) //0%8=0 1%8=1 .....7%8=7 8%8=0 9%8=1 ...... if(c) OLED_GRAM[page][x]|=1<<y; else OLED_GRAM[page][x]&=~(1<<y); memcpy(lcd_mem,OLED_GRAM,1024);}

/*保证字体宽和高是一样的,而且必须是8的倍数。*/void ShowFont(int x,int y,int size,char *font){ int i,j,x0=x; unsigned char data; for(i=0;i<size/8*size;i++) { data=font[i]; for(j=0;j<8;j++) { //高位取模 if(data&0x80) { //字体颜色 OLED_DrawPoint(x0,y,1); } else { //背景颜色 OLED_DrawPoint(x0,y,0); } x0++; data<<=1; } if(x0-x==size) { x0=x; y++;//换行 } }}
#define OLED_RefreshGRAM 0x12345 /*将应用层的数据刷新到OLED屏幕上*/#define OLED_ClearGRAM 0x45678 /*将应用层的数据刷新到OLED屏幕上*/
int main(int argc,char **argv){ /*1.打开设备文件*/ int fd=open("/dev/fb5",O_RDWR); if(fd<0) { printf("/dev/fb5设备文件打开失败!\n"); return 0; } /*2. 读取LCD屏的参数*/ ioctl(fd,FBIOGET_FSCREENINFO,&finfo);//固定参数 printf("映射的长度:%d\n",finfo.smem_len); ioctl(fd,FBIOGET_VSCREENINFO,&vinfo);//可变参数,32位 printf("分辨率:%d*%d,%d\n",vinfo.xres,vinfo.yres,vinfo.bits_per_pixel);
/*3. 映射LCD的地址到进程空间*/ lcd_mem=mmap(NULL,finfo.smem_len,PROT_WRITE|PROT_READ,MAP_SHARED,fd,0); if(lcd_mem==NULL) { printf("lcd_mem映射失败!\n"); return -1; } //OLED清屏 ioctl(fd,OLED_ClearGRAM); /*4. 显示中文*/ ShowFont(0,0,56,font); ShowFont(56,0,56,font); ioctl(fd,OLED_RefreshGRAM); /*5. 关闭文件*/ munmap(lcd_mem,finfo.smem_len); close(fd); return 0;}
复制代码


发布于: 刚刚阅读数: 2
用户头像

DS小龙哥

关注

之所以觉得累,是因为说的比做的多。 2022.01.06 加入

熟悉C/C++、51单片机、STM32、Linux应用开发、Linux驱动开发、音视频开发、QT开发. 目前已经完成的项目涉及音视频、物联网、智能家居、工业控制领域

评论

发布
暂无评论
Linux驱动开发-编写OLED显示屏驱动_4月月更_DS小龙哥_InfoQ写作平台