嵌入式Linux——IIC总线驱动(3):IIC驱动OLED外设

2023-05-16

简介:

        本文主要介绍在jz2440开发板上驱动OLED外设,使其显示我们在应用层输入的语句。而同时我将该文分成了两部分,第一部分讲解i2c总线的实现,而第二部分讲解在i2c总线实现后,我们使用字符设备驱动来实现对OLED的控制。

 

    Linux内核:linux-2.6.22.6

    所用开发板:JZ2440 V3

    所用OLED 屏幕:中景园电子0.96 寸OLED 显示屏12864液晶屏模块

    所用OLED 驱动芯片:SSD1306

声明:

        本文主要讲解在JZ2440上如何驱动OLED,因此我们更注重的是讲解如何实现这个功能,而关于i2c总线的相关原理,如果你不明白可以去看我的上一篇文章:嵌入式Linux——IIC驱动(2):i2c驱动框架分析 。而关于文中OLED方面的知识我也在:嵌入式Linux——IIC总线驱动(1):2440裸板驱动OLED 中做了介绍。在裸板中很多的函数我们会在这篇文章中用到,只是做了一些修改。所以如果你也要学习驱动OLED,同时也想知道这方面的原理。我建议你将上面两篇文章看完后再看本文,而如果你只是想在开发板上实现OLED驱动 ,你可以只看本文我会将各个函数的接口信息告诉你,以方便你的调用。同时本文为了顾及没有看过前面两篇文章的人,所以有部分内容与前面文章相同,敬请谅解。

第一部分:2c总线的实现

        i2c总线的实现其实就是申请,设置和注册i2c_driver结构体以及i2c_client结构体的过程。而说详细点就是使用i2c_driver结构体的attach_adapter函数来确定我们的设备驱动与外接的i2c设备是否匹配,如果匹配则调用相应的处理函数。在处理函数中我们实现对这个从机设备的注册,为他注册一个i2c_client结构体。然后就是我们第二部分要讲的使用字符驱动实现对OLED的控制。下面我们进入正题,直接进入程序,顺着程序慢慢了解驱动OLED的过程。

        我们先看入口函数:s3c_i2c_init

static int s3c_i2c_init(void)
{
	i2c_add_driver(&oled_driver);  /* 将i2c_driver结构体注册到内核 */
	return 0;
}

        从上面我们看出他主要就是将i2c_driver结构体注册进内核。而i2c_driver结构体是怎么设置的我们就要看继续分析了:

static struct i2c_driver oled_driver = {
	.driver = {
		.name = "oled" ,
	},
	.attach_adapter = oled_attach,
	.detach_client  = oled_detach,
};

        而在上面的程序中,我们可以看到驱动的名称,以及两个回调函数:attach_adapterdetach_client。而attach_adapter的作用是调用i2c_probe函数来测试从机地址与从机设备是否匹配。而detach_client的作用是卸载这个驱动后如果之前发现能够支持的设备则调用他来清理。由此可见这两个回调函数是很重要的。我们先按着attach_adapter函数这条线分析下去。

static int oled_attach(struct i2c_adapter *adapter)
{
	return i2c_probe(adapter, &addr_data, oled_detect);	
}

        从上面看,他确实只调用了i2c_probe函数,那么我们就要了解这个函数的功能了。从上面看我们知道他有三个参数,分别为:适配器,7位从机地址以及从机匹配成功后的处理函数(而该函数回调我们所写的函数)。而从这些参数我们不难猜出这个函数做了什么。他是将我们的从机地址与连接在适配器上的从机设备进行匹配,如果他们匹配成功,我们将调用第三个参数:处理函数。

        不过我们的第二个参数:从机地址也是有格式要求的:

struct i2c_client_address_data {
	unsigned short *normal_i2c;       /* 正常模式 */
	unsigned short *probe;
	unsigned short *ignore;           /* 忽略 */
	unsigned short **forces;          /* 强制模式 */
};

        我们主要介绍上面的两种模式:正常模式和强制模式。

正常模式:要发出S信号和地址信号并得到ACK信号才能确定是否存在这个设备

强制模式:强制认为存在这个设备

        我下面举个例子说一下这两种模式的不同,比如我们的设备的7位从机地址为0x3c,而在正常模式下,我们要发出S信号和地址信号并得到ACK信号才能确定是否存在这个设备,如果存在调用处理函数。而在强制模式是不过你从机地址与从机设备是否匹配我们都强制认为存在这个设备,并且匹配,而直接调用处理函数。而在本文中我们使用正常模式,程序为:

static unsigned short ignore[] = {I2C_CLIENT_END};
static unsigned short normal_addr[] = {0x3c,I2C_CLIENT_END};  /* 地址值为7位,如果将0x50改为0x60,
		                                                *由于不存在设备地址为0x60的设备
								*所以oled_detect不会被调用 
								*/
static struct i2c_client_address_data addr_data = {
	.normal_i2c = normal_addr,  /* 要发出S信号和地址信号并得到ACK信号才能确定是否存在这个设备 */
	.probe      = ignore,
	.ignore     = ignore,
	//.forces   = forces,  /* 强制认为存在这个设备 */
};

        下面我们顺着程序继续讲,当我们的从机地址和从机设备匹配后,会调用我们的处理函数:oled_detect,而在我们的处理函数中做了什么那?

static int oled_detect (struct i2c_adapter *adapter, int address, int kind)
{
	/* 构造一个i2c_client结构体:以后收发数据时会用到它 */
	new_client = kzalloc(sizeof(struct i2c_client),GFP_KERNEL);
	new_client->addr    = address;       /* 从机地址 */
	new_client->adapter = adapter;       /* 适配器 */
	new_client->driver  = &oled_driver;   /* i2c_driver结构体 */
	strcpy(new_client->name,"oled");
	i2c_attach_client(new_client);
        /* 注册字符设备驱动 */
	auto_major = register_chrdev(0,"oled",&oled_fops);
	cls = class_create(THIS_MODULE,"oled");
	class_device_create(cls,NULL,MKDEV(auto_major,0),NULL,"oled");
		
	return 0;
}

        我们主要就是做了两件事:

1. 构造一个i2c_client结构体:以后收发数据时会用到它
2. 注册字符设备驱动

        我们知道我们从机地址和外设从机匹配后我们就会用一个i2c_client结构体来记录这个从机设备并将它注册到内核,而上面程序中我们做的第一件事就是记录这个从机设备,这其中包括他的从机地址,所属适配器以及所调用的i2c_driver结构体

 

第二部分:使用字符设备驱动实现OLED的控制

        而从设置字符驱动我们就进入了第二部分,而这部分也是我们的重点了,因为上面讲解的这部分代码是我借鉴他人的,而你也可以在其他的i2c驱动中看到相似的代码。而下面这部分代码才是真正我自己所写。

        而要了解字符设备驱动我们就要看他的file_operations结构体,通过他我们就知道字符设备做了什么。

static struct file_operations oled_fops = {
	.owner = THIS_MODULE,
	.write = oled_write,
	.open  = oled_open,
};

        从上面我们可以看出他主要有两个函数:oled_openoled_write,而oled_open的工作就是打开这个设备文件时对OLED进行初始化,而oled_write的工作是从客户端获得数据,并将其显示到OLED上。

我们先看open函数

int oled_open (struct inode * inode, struct file *file)
{
	int data_len;
	int i;
	unsigned char j,n;	

	/* 初始化OLED */
	unsigned char data[] ={0xAE,0x00,0x10,0x40,0xB0,0x81,0xFF,0xA1,0xA6,0xA8,0x3F,0xC8,0xD3,0x00,0xD5,0x80,0xD8,0x05,0xD9,0xF1,0xDA,0x12,0xDB,0x30,0x8D,0x14,0xAF};
	data_len = sizeof(data)/sizeof(data[0]);
	for(i=0;i<data_len;i++){
		oled_write_1bit_cmd(data[i]);
	} 

	/* 清屏 */
	for(j=0;j<8;j++)  
	{  
		oled_write_1bit_cmd(0xb0+j);		//page0-page7
		oled_write_1bit_cmd(0x00);		//low column start address
		oled_write_1bit_cmd(0x10);		//high column start address
		
		for(n=0;n<128;n++){
			oled_write_1bit_dat(0);
		}

	} //更新显示
	return 0;
}

        从上面可以看出主要做了两件事:

1.初始化OLED

2. 清屏OLED

        而初始化OLED就是使用oled_write_1bit_cmd函数将芯片SSD1306中所对应的命令依次写入SSD1306芯片中。而关于SSD1306芯片中具体的命令就要各位读者自己查了。而清屏就是使用oled_write_1bit_dat函数将数据“0”写入SSD1306芯片数据存储器的每一页。而具体oled_write_1bit_cmd函数oled_write_1bit_dat函数是如何实现的,我们看下面:

/********************************************  
*	oled_write_1bit_cmd
*	写一字节命令
********************************************/
int oled_write_1bit_cmd(unsigned char cmd)
{
		
	struct i2c_msg msg[1];
	unsigned char cmds[2];
	int ret;
	
	cmds[0] = 0x00;
	cmds[1] = cmd;
	
	/* 数据传输三要素:源,目的,长度 */
	msg[0].addr = new_client->addr; 		  /* 目的 */
	msg[0].buf	= cmds;				/* 源 */
	msg[0].len	= 2;				/* 长度为两byte =  数据 + 地址 */
	msg[0].flags= 0;				/* 0表示写 */	

	ret = i2c_transfer(new_client->adapter,msg,1);
	if(ret == 1)
		return 2;
	else
		return -EIO;
}

/********************************************  
*	oled_write_1bit_dat
*	写一字节数据
********************************************/
int oled_write_1bit_dat(unsigned char data)
{
	struct i2c_msg msg[1];
	unsigned char datas[2];
	int ret;
	
	datas[0] = 0x40;
	datas[1] = data;

	/* 数据传输三要素:源,目的,长度 */
	msg[0].addr = new_client->addr; 		  /* 目的 */
	msg[0].buf	= datas;			/* 源 */
	msg[0].len	= 2;				/* 长度为两byte =  数据 + 地址 */
	msg[0].flags= 0;				/* 0表示写 */	

	ret = i2c_transfer(new_client->adapter,msg,1);
	if(ret == 1)
		return 2;
	else
		return -EIO;

}

        为什么将这两个函数放到一起看那?因为他们太相似了,他们唯一的区别就是命令的第一个字符为0x00,而数据的第一个字符为0x40.而这个差别是由于SSD1306芯片确定的,看下图:

        从第二个红框可以看出,0x00表示命令而0x40表示数据

        而在上面两个函数中都用到了一个结构体:i2c_msg,他的作用是作为信息的载体,通过调用i2c_transfer函数,实现我们向i2c从机读写数据。

struct i2c_msg {
	__u16 addr;	/* 从机地址 */
	__u16 flags;    /* 传输标记位 */
	__u16 len;	/* 数据长度 */
	__u8 *buf;	/* 指向数据的指针 */
};

         我们知道数据传输的三要素:源,目的和长度而当标志为0时表示写,那么addr为目的(向从机写)而buf为源(从主机出)。而当标志为1时表示读,那么addr为源(从从机读)而buf为目的(读往主机)。

         而上面讲的i2c_transfer函数的作用就是将数据传到从机,对从机进行相应的操作,而他的调用关系为:

i2c_transfer(adapter, msg, num) < 0)  
  adap->algo->master_xfer(adap,msgs,num);  
    .master_xfer= s3c24xx_i2c_xfer,  
            s3c24xx_i2c_doxfer(i2c, msgs, num);  
                        i2c->msg     = msgs;  
                        i2c->msg_num = num;  
                        i2c->msg_ptr = 0;  
                        i2c->msg_idx = 0;  
                        i2c->state   = STATE_START;  
                        s3c24xx_i2c_enable_irq(i2c);  
                        s3c24xx_i2c_message_start(i2c, msgs);  
                            iiccon = readl(i2c->regs + S3C2410_IICCON);  
                            writel(stat, i2c->regs + S3C2410_IICSTAT);  
                            writeb(addr, i2c->regs + S3C2410_IICDS);  

        从中我们可以看出,他最后会落实到对主机的寄存器进行操作,进而控制从机。

        有了上面写一个字节的命令或者写一个字节的数据函数。我们就可以做很多关于OLED设置的事情了,其中包括:

/******************************************** 
*	OLED_WR_Byte
*	OLED写字节,当cmd为0时表示写命令
*				当cmd为1时表示写数据
********************************************/
void OLED_WR_Byte(unsigned dat,unsigned cmd)
{
	if(cmd)
	{
		oled_write_1bit_dat(dat);
	}
	else 
	{
		oled_write_1bit_cmd(dat);	
	}
}

/******************************************** 
*	OLED_Set_Pos
*	坐标设置
* 参数:
*	x:表示横向的起始地址,0~127
*	y:表示纵向的起始地址,0~63
********************************************/
void OLED_Set_Pos(unsigned char x, unsigned char y) 
{ 	
	OLED_WR_Byte(0xb0+y,OLED_CMD);
	OLED_WR_Byte(((x&0xf0)>>4)|0x10,OLED_CMD);
	OLED_WR_Byte((x&0x0f),OLED_CMD); 
}

/******************************************** 
*	OLED_ShowChar
*	在指定位置显示一个字符,包括部分字符
* 参数:
*	x:表示横向的起始地址,0~127
*	y:表示纵向的起始地址,0~63
*	chr:表示用户输入字符
********************************************/
void OLED_ShowChar(unsigned     char x,unsigned char y,unsigned char chr)
{      	
	unsigned char c=0,i=0;	
	c=chr-' ';//得到偏移后的值			
	if(x>128-1){x=0;y=y+2;}
	
	OLED_Set_Pos(x,y);	
	for(i=0;i<8;i++)
	OLED_WR_Byte(F8X16[c*16+i],OLED_DATA);
	OLED_Set_Pos(x,y+1);
	for(i=0;i<8;i++)
	OLED_WR_Byte(F8X16[c*16+i+8],OLED_DATA);
}

/******************************************** 
*	OLED_ShowString
*	显示一个字符号串
* 参数:
*	x:表示横向的起始地址
*	y:表示纵向的起始地址
*	chr:表示用户输入字符串
*注:函数中说明,当一行数据写满是会自动跳到下一行显示
********************************************/
void OLED_ShowString(unsigned     char x,unsigned char y,unsigned char *chr)
{
	unsigned char j=0;
	while (chr[j]!='\0')
	{		OLED_ShowChar(x,y,chr[j]);
			x+=8;
		if(x>120){x=0;y+=2;}
			j++;
	}
}

 

        上面就是对OLED一些功能的设置,其中包括OLED写字节坐标设置在指定位置显示一个字符以及显示一个字符串。而我们主要做的就是显示一个字符串,所以我先将要用到的数组写下来:

 

const unsigned char F8X16[]=	  
{
  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
·····················································································
······················································································
  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
};

        由于这个数组比较长,我这里做了相应的省略,但大致我们知道,我们键盘上所有的字母在这个数组中都可以找到。

 

        有了上面的介绍,我们现在讲解如何在write函数中实现将用户程序所输的字符串显示到OLED。

static ssize_t oled_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	unsigned char val[64];
	copy_from_user(val,buf,64);
	OLED_ShowString(0,0,val,16);

	return 0;
}

        其实你看后会觉得很简单,就是通过copy_from_user函数从用户程序获得字符串,然后通过函数OLED_ShowString将该字符串显示到OLED。而关于OLED_ShowString函数我在上面已经介绍了。

        现在我们的驱动程序就写完了,同时我还写了一个测试程序用它来测试这个驱动程序。我们看他主要做了什么:

int main(int argc,char **argv)
{
	int fd; 
	unsigned char buf[64] = {0};
	int i;
	fd = open("/dev/oled",O_RDWR);   /* 打开字符设备/dev/oled */
	if(fd < 0){
		printf(" can't open /dev/oled \n\r");
		return -1;
	}

	write(fd,argv[1],64);        /* 将接收的第二个字符串传入内核的字符设备 */

	return 0;

}

而测试的效果图为:

而相应的OLED显示为:

 

而详细代码在:用i2c总线驱动OLED程序

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

嵌入式Linux——IIC总线驱动(3):IIC驱动OLED外设 的相关文章

随机推荐

  • react资源

    MUI The React UI library for faster and easier web development React Table Lightweight and extensible data tables for Re
  • gitbook之node版本问题

    执行gitbook build出现错误 xff0c 执行gitbook serve也是会出错 错误如下 xff1a gitbook build usr local lib node modules gitbook cli node modu
  • Arduino Uno安装设备时,出现了一个错误,这个INF中的服务安装段落无效

    问题 xff1a 在windows系统下 xff0c Uno安装设备时 xff0c 出现了一个错误 xff0c 这个INF中的服务安装段落无效 原因 xff1a 缺少系统文件 解决方案 xff1a 1 先下载 xff1a usbser zi
  • Vmware Unity模式

    ubuntu 12 04 之Vmware Unity模式 安装VMware Toolsudo add apt repository ppa gnome3 team gnome3sudo apt get install gnome shell
  • 状态机实现的三种方法-C语言

    1 参考 xff1a https www cnblogs com aaronLinux p 5705457 html 2 转载 xff1a http kb cnblogs com page 528972 3 参考 xff1a FSM TCP
  • 错误:try using -rpath or -rpath-link

    在使用到动态库的时候 xff0c 出现错误如下 xff1a arm linux bin span class hljs keyword ld span warning libssl span class hljs preprocessor
  • git push不用输入密码(方法一)-git-credentials

    install git credentials sh 命令步骤 xff1a touch git credentialsecho 34 http username password 64 localhost 34 gt gt git cred
  • 十、如何使用perf工具分析java程序

    问题 1 xff1a 使用 perf 工具时 xff0c 看到的是 16 进制地址而不是函数名 在 CentOS 系统中 xff0c 使用 perf 工具看不到函数名 xff0c 只能看到一些 16 进制格式的函数地址 只要你观察一下 pe
  • vins环境配置(一):kalibr标定工具箱安装

    相机和IMU标定 xff1a kalibr工具包 xff0c r os环境 Indigo xff0c 在jade版本中未曾编译通过 xff0c 应该是自己的方式不对 xff0c 有待继续尝试 xff09 在github中查找kalibr项目
  • msckf_vio使用记录

    使用环境 xff1a ubuntu14 04 indigo indigo版本的ros默认支持的是opencv2 4 8 xff0c 其带的库cv bridge依赖于opencv2 但是 xff0c msckf vio使用的是Ubuntu 1
  • 无人机姿态解算:四元数及其与欧拉角的转换

    无人机姿态解算 xff1a 四元数及其与欧拉角的转换 引言 xff1a 获得无人机飞行时的飞行姿态对于无人机稳定控制来说至关重要 无人机主要通过传感器数据融合来进行状态估计 xff0c 常用于无人机的传感器包括 xff1a MPU xff0
  • (纯干货)备战“金3银4”,拿下这101道面试题“10拿9稳””!

    前言 xff1a 马上就到了 金9银10的 面试季 xff0c 在这里作者给各位整理了一些大厂常用的面试题 xff0c 希望能对各位有所帮助 正文 xff1a 1 问 xff1a 你在测试中发现了一个bug xff0c 但是开发经理认为这不
  • 用Excel分析音视频同步

    声明 xff1a 这里主要介绍如何运用Excel来分析音视频是否同步 xff0c 希望可以对大家有所帮助 介绍 xff1a 学习音视频就一定要知道做音视频同步 xff0c 而现在我们来分析音视频同步的工具也是有的 xff0c 比如easyI
  • 使用VLC 在PC端搭建RTP环境

    声明 xff1a 本文是我在工作中遇到的关于环境搭建问题后的一些总结 xff0c 希望可以对你有所帮助 介绍 xff1a 搭建方法 xff1a 1 xff09 搭建 VLC 软件 xff0c 点击媒体菜单 xff0c 选择 流 选项 xff
  • 使用VLC 在PC端搭建RTSP环境

    声明 xff1a 本文是我在工作中遇到的关于环境搭建问题后的一些总结 xff0c 希望可以对你有所帮助 介绍 xff1a 搭建方法 xff1a 1 xff09 搭建 VLC 软件 xff0c 点击媒体菜单 xff0c 选择 流 选项 xff
  • 2020总结

    2020年可以说是我开始工作的第二年了 在第一年中我可以说了解了工作和组里的基本情况 xff0c 而这第二年就开始真正的加深学习了 还是以时间线一点点的慢慢讲吧 春节前我们一边工作一边准备年会 我们公司有个惯例那就是让每年最新入职的员工来主
  • 嵌入式Linux——网卡驱动(1):网卡驱动框架介绍

    声明 xff1a 文本是看完韦东山老师的视频和看了一些文章后 xff0c 所写的总结 我会尽力将自己所了解的知识写出来 xff0c 但由于自己感觉并没有学的很好 xff0c 所以文中可能有错的地方敬请指出 xff0c 谢谢 在介绍本文之前
  • ROS发布订阅的消息的种类及使用

    1 消息 std msgs 的种类 在 opt ros melodic include std msgs文件夹中查询 或参考 xff1a https www itdaan com tw b30f2309fe4b1b2d5993c8ea0cb
  • SSD1306部分命令

    命令描述 1 基础命令 1 1 为页地址模式设置列低半字节的开始地址 xff08 00h 0Fh xff09 这个命令专门为8位列地址的低半字节设置以通过页地址模式显示RAM中的数据 而每一个数据使用后列地址会自动增加 请参考表格9 1的部
  • 嵌入式Linux——IIC总线驱动(3):IIC驱动OLED外设

    简介 xff1a 本文主要介绍在jz2440开发板上驱动OLED外设 xff0c 使其显示我们在应用层输入的语句 而同时我将该文分成了两部分 xff0c 第一部分讲解i2c总线的实现 xff0c 而第二部分讲解在i2c总线实现后 xff0c