简介:
本文主要介绍在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_adapter和detach_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_open和oled_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
从上面可以看出主要做了两件事:
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程序