很久之前就想学习如何在uClinux下控制硬件的工作,于是在WIKILCD16207网找到了LCD16207的操作说明,刚开始很开心,可是呢,做着做着发现结果出不来,因为刚开始接触uClinux,所以很多东西就不是很清楚,也没有办法找到错误,结果其中就耽误了很多时间,最后终于在Altera论坛上关于LCD16207找到了问题的答案。
实验目的:在uClinux下加载DE2上LCD16207的驱动,通过软件方式控制LCD的显示
开发板:DE2
开发软件:Quartus9.1 + Ubuntu + uClinux
第一、硬件设计
由于本文主要是想在uClinux下通过软件控制LCD,所以这里就没有在硬件利用Verilog进行什么设计,为了设计方便和准确性,我用了DE2自带的工程DE2_NIOS_HOST_MOUSE_VGA,只是重新在9.1的版本里重新编译了一遍,所以硬件设计就不多说了。
下面就重点来谈谈uClinux的软件设计。
第二、软件设计
首先是uClinux的内核移植工作,其实我写过一篇博文,是在qq空间上,转不过来,悲剧,过段时间再写一篇关于DE2上uClinux的移植工作,这里就从移植成功之后讲起吧!
1、在移植成功之后,首先按照WIKILCD16207上面的要求,下载lcd16207-kernel.zip和lcd16207_example.zip这两个源代码,前面的是内核驱动代码,后面是用户应用程序代码。
2、将内核LCD16207驱动代码拷贝到指定位置,在这里我想说明的是WIKILCD16207这个网上的一个错误,
下面是错误的原文:
Copy the kernel driver (lcd_16207.c, lcd_16207.h) to uClinux-dist/linux-2.6.x/drivers/char.
我们从上面可以看到是要拷贝到uClinux-dist/linux-2.6.x/drivers/char这个路径下,其实是不对的,可能是作者疏忽的错误吧,这个没什么,应该是拷贝到nios2-linux/linux-2.6/drivers/char这里面才是真正的内核源码。
3、修改Kconfig和MakeFile文件,
MakeFile文件修改如下图所示:
Kconfig文件的修改如下图所示:
注:只有在nios2-linux/linux-2.6/drivers/char这个路径下才有Kconfig和Makefile这两个文件,论坛中很多人说自己没有这两个文件,其实不是他的移植不成功,只是作者的错误导致的。也验证了上面的一个错误。
4、经过上面的步骤,uClinux下LCD16207的驱动就算移植成功,下面将要做的是应用程序的编译。
nios2-linux-uclibc-gcc -O -s -elf2flt='-s 16000' -I./ -I../uClinux-dist/linux-2.6.x/include -c lcd16207.c
nios2-linux-uclibc-gcc -O -s -elf2flt='-s 16000' -lm -I./ -I../uClinux-dist/linux-2.6.x/include -o lcd16207 lcd16207.o
nios2-linux-uclibc-gcc -O -s -elf2flt='-s 16000' -I./ -I../uClinux-dist/linux-2.6.x/include -c lcdtime.c
nios2-linux-uclibc-gcc -O -s -elf2flt='-s 16000' -lm -I./ -I../uClinux-dist/linux-2.6.x/include -o lcdtime lcdtime.o
nios2-linux-uclibc-gcc -O -s -elf2flt='-s 16000' -I../uClinux-dist/linux-2.6.x/include -c writef.c
nios2-linux-uclibc-gcc -O -s -elf2flt='-s 16000' -I../uClinux-dist/linux-2.6.x/include -o writef writef.o
nios2-linux-uclibc-gcc -O -s -elf2flt='-s 16000' -I../uClinux-dist/linux-2.6.x/include -c readf.c
nios2-linux-uclibc-gcc -O -s -elf2flt='-s 16000' -I../uClinux-dist/linux-2.6.x/include -o readf readf.o
上面就是编译的命令,需要我们将工作目录放在nios2-linux/uClinux-dist/linux-2.6.x/include下,即cd /usr/local/src/nios2-linux/uClinux-dist/linux-2.6.x/include,上面的省略号就是你自身的的绝对路径,我这里是usr/local/src,这里命令的具体参数如果想具体了解,可以自己查找一下,这里就不多加叙述。
5、经过上面编译命令之后,就会分别生成lcd16207、lcdtime、writef、readf,这些就是可执行程序。这里,我们将上面几个要拷贝到romfs下面,最终就能下载到板子上去,所以,将上面几个文件复制到romfs/lcd下,如下图所示:
6、通过命令方式.
nios2-configure-sof DE2_NIOS_HOST_MOUSE_VGA.sof下载硬件配置;
nios2-download -g zImage 将zImage镜像下载到板子上去;
nios2-terminal 启动uClinux内核,如下图所示如果你成功的话,上面会出现,
注:如果你LCD16207驱动加载成功,将会在内核启动的时候出现上面信息。
7、执行程序
cd lcd
./lcd16207 hello
./writef
./readf
./lcdtime 12345
执行第一个命令结果如下所示,同时LCD上滚动显示hello
这里,第一个命令是在LCD上滚动显示hello,第二个就是随意向LCD上写入一个字符串,第三个就是读LCD的值,第四个就是设置LCD的显示等待时间。
程序错误分析:
1、从上面看来,似乎所有的问题按照WIKILCD16207上面的要求就可以顺利解决了,其实不是这样的,首先遇到最大的问题就是,按照上面的做法做了之后,内核启动之后也出现了 Device /dev/lcd16207 registered,应用程序也跑起来了,可是硬件上什么也不显示,这就相当于白做了,毕竟你是在控制硬件工作,硬件没跑起来,说明你的工作是没有意义的,最终是在Altera论坛上关于LCD16207上找到了问题解决的方案,驱动的头文件需要增加一个地址偏移量,如下图所示
这里增加了一个LCD地址的偏移量,为什么是这样呢,这里论坛上达人给我的解释是由于non-MMU的nios把地址的最高位bit31置1,在I/O的的操作时就没有缓存,这里似乎有点明白,但是又不全懂。因为我之前也自己写过LCD的驱动(是在IDE里面),也没有特意更改LCD在NIOS II里面的地址啊!难道是uClinux操作系统的原因吧,太深入了,不好理解!不过好消息就是LCD16207的内核驱动好用了。
2、第二个问题就是LCD的显示出现了问题,不是按照程序上的意思,滚动显示的,而是一跳一跳的,这里,通过我仔细阅读应用程序代码和驱动代码,终于让我找到问题发生的原因了。 问题就出现在内核驱动的API函数上,在ssize_t device_write()这个函数中,就是上面的图,是将字符显示到LCD的函数,函数的执行,大家可以清楚看到,这里,应该是在赋值好Message_ptr_Line1和
Message_ptr_Line2之后,再执行LcdWriteLines()这个函数,实际上他执行了两次,这就导致了执行一次写操作,然而却写了两次到LCD上,从而出现一跳一跳的现象,只要将上面的while循环的后面那个大括号放在WaitNios(Display_Wait)前面就可以了。
3、在执行./writef这个命令的时候,没看到执行的前后结果,让我很诧异,我仔细又阅读了一下这个写命令函数,发现,length大小不能超过32,要不然就会出现问题。这样,我增加了一行语句,解决了这个问题,如下图所示:因为LCD只能显示32个字符,所以呢,如果你要显示的字符超过32的话,就要舍掉超过的部分。
经过上面的步骤,你已经基本上成功实现了在uClinux下对LCD的控制,这里,让我们来分析一下这个LCD的驱动,这样让我们更深入的了解LCD。
1、让我们先看看驱动的lcd16207.h文件,
#define MAJOR_NUM 250
#define ADR_LCD_COMMAND na_lcd_16207_0+0x80000000
#define ADR_LCD_READY (na_lcd_16207_0 +0x80000000+4)
#define ADR_LCD_DATA (na_lcd_16207_0 + 0x80000000+8)
#define ADR_LCD_READ (na_lcd_16207_0 + 0x80000000+12)
#define ADR_LCD_LINE1 0x80 + 0x00
#define ADR_LCD_LINE2 0x80 + 0x40
#define BUF_LCD_LINE 16
#define BUF_LCD_ROWS 2
#define BUF_LCD_CHARS BUF_LCD_LINE * BUF_LCD_ROWS
由于篇幅有些长,这里就列出几项,有LCD的主设备号,和一些宏,都是关于LCD的,如果想深入了解LCD的工作原理,大家应该查找更相关的文章,我前段时间也做过一些,还没来得及总结,如果有时间,我也总结一下。
#define IOCTL_SET_MSG _IOR(MAJOR_NUM, 0, char *)
上面是一个宏IOCTL_SET_MSG的定义,是将这个宏定义成IO read这里是相对于操作系统来说的,是操作系统从用户空间读取数据,再向LCD里面写数据,所以这里就定义为_IOR的原因。
static void WriteNios(unsigned long addr, unsigned long value);//向Avalon总线写数据
static unsigned long ReadNios(unsigned long addr);//从Avalon总写读数据
static void WaitNios(unsigned long us);//Nios的等待时间
void LcdWriteLines(void);//想LCD里面写一行数据
static void LcdReadLines(void);//从LCD上读一行数据
这是在lcd16207.c里面要用到的API函数
2、lcd16207.c
2.1首先来看几个简单的函数实现:
static void WriteNios(unsigned long addr, unsigned long value)
{
(* (volatile unsigned long *)(addr))=value;
}
static unsigned long ReadNios(unsigned long addr)
{
return (unsigned long)(* (volatile unsigned long *)(addr));
}
注:这两个函数就是将数据传递给Avalon上的地址线,有数据线也有命令行线,用宏就能解决这个问题。
再看看写入行数据的API函数:
//write all chars to the LCD
static void LcdWriteLines(void)
{
int i;
WriteNios(ADR_LCD_COMMAND,0x80);
udelay(50);
Message_Ptr_Write = Message_Ptr_Line1;
for (i = 0; i < BUF_LCD_LINE; i++)
{
WriteNios(ADR_LCD_DATA, (unsigned long)*(Message_Ptr_Write+i) );
udelay(50);
}
WriteNios(ADR_LCD_COMMAND,0x80 + 0x40);
udelay(50);
Message_Ptr_Write = Message_Ptr_Line2;
for (i = 0; i < BUF_LCD_LINE; i++)
{
WriteNios(ADR_LCD_DATA, (unsigned long)*(Message_Ptr_Write+i) );
udelay(50);
}
}
//read all chars from the LCD
static void LcdReadLines(void)
{
int i;
WriteNios(ADR_LCD_COMMAND,0x80);
udelay(50);
Message_Ptr_Write = Message_Ptr_Line1;
for (i = 0; i < BUF_LCD_LINE; i++)
{
*(Message_Ptr_Write+i)=ReadNios(ADR_LCD_READ);
udelay(50);
}
WriteNios(ADR_LCD_COMMAND,0x80 + 0x40);
udelay(50);
Message_Ptr_Write = Message_Ptr_Line2;
for (i = 0; i < BUF_LCD_LINE; i++)
{
*(Message_Ptr_Write+i)=ReadNios(ADR_LCD_READ);
udelay(50);
}
}
就分析一些static void LcdWriteLines(void)这个函数,首先向ADR_LCD_COMMAND地址线上写入80,表示要写入数据,分别有两个char指针,一个指向第一行,一个指向第二行,利用for循环,进行,没写入一个数据,就udelay(50),这是硬件规定的。
2.2、驱动注册、卸载,设备打开和释放API函数
init_module()和cleanup_module()这两个是驱动的注册和卸载程序,在init_module里面完成LCD的简单初始化工作。另外device_open()和device_release()完成设备的打开和释放工作,也不需要多讲。不明白看linux内核驱动程序。
2.3、设备读写操作
ssize_t device_read()和ssize_t device_write()API函数,这里就分析写操作!
static ssize_t device_write(struct file *file,
const char __user * buffer, size_t length, loff_t * offset)
{
int ii;
#ifdef DEBUG
printk(KERN_INFO "device_write(%p,%s,%d);\n", file, buffer, length);
#endif
ii=0;
if(length>BUF_LCD_CHARS)
length=BUF_LCD_CHARS;
while(ii
{
strncpy(Message_Ptr_Line1, Message_Ptr_Line2, BUF_LCD_LINE);
Message_Ptr=Message;
if ( (length-ii) > BUF_LCD_LINE)
{
copy_from_user(Message_Ptr_Line2, buffer+ii, BUF_LCD_LINE);
ii=ii+BUF_LCD_LINE;
}
else
{
copy_from_user(Message_Ptr_Line2, buffer+ii, length-ii);
memset(Message_Ptr_Line2+(length-ii),32,BUF_LCD_LINE-(length-ii));
ii=length;
}
}
WaitNios(Display_Wait);
LcdWriteLines();
#ifdef DEBUG
printk(KERN_INFO "Message:%s:End\n",Message);
#endif
return length;
}
这个API函数的作用就是将用户空间buffer里的数据拷贝到两个指针中去,之后,调用lcdWriteLines()写LCD函数,达到写LCD的目的。其中的API函数就不需要多讲了吧!
2.4、设备的ioctr()操作
static int device_ioctl(struct inode *inode,
struct file *file,
unsigned int ioctl_num,
unsigned long ioctl_param)
{
int i;
char *temp;
char ch;
switch (ioctl_num) {
case IOCTL_SET_MSG:
temp = (char *)ioctl_param;
get_user(ch, temp);
if (ch=='\0')
break;
for (i = 0; ch!='\0' ; i++, temp++)
get_user(ch, temp);
device_write(file, (char *)ioctl_param, i-1, 0);
break;
case IOCTL_SET_DISP_WAIT:
Display_Wait=(unsigned long)ioctl_param;
#ifdef DEBUG
printk(KERN_INFO "Display wait set to hex X\n", (unsigned int) Display_Wait);
#endif
break;
case IOCTL_GET_MSG:
i = device_read(file, (char *)ioctl_param, BUF_LCD_CHARS+1, 0);
break;
//not tested - still todo
case IOCTL_GET_NTH_BYTE:
return Message[ioctl_param];
break;
}
return SUCCESS;
}
这里用到了一个case语句,ioctr操作主要就是设置设备的一些参数,用到了一个case语句。
当ioctl_num=IOCTL_SET_MSG时,表示的是向LCD中写入数据,在这里,获取数据的长度,直接条用device_write()函数到达写数据的目的;
当ioctl_num=IOCTL_SET_DISP_WAIT时,表示设置LCD的显示等待时间,这里直接将传入的参数赋值即可;
当ioctl_num=IOCTL_GET_MSG时,表示读取LCD的数据,调用device_read()实现;
当ioctr_num=IOCTL_GET_NTH_BYTE,表示获取第ioctl_param的Message数据,就是显示的数据,一个字节一个字节的显示出来。
总结一下:
一、在驱动程序里面,有两个写操作,最底层的就是将数据直接写到Nios II的Avalon总线上,由于要用到操作系统,这里我们就需要另外一个写操作,将用户空间即是应用程序的数据拷贝出来,再调用上面的写函数,达到用户空间的数据显示到硬件上的目的。
二、然而在实际的应用程序编写的时候,是不用到具体的device_write函数的,所以这里面又定义了一个ioctr()函数提供给应用程式使用,里面用到case语句,这样应用程序就用到统一的接口,比较方便。
三、在这里的驱动程序中,ioctr里面没有涉及到向LCD写命令等操作,这里你完全可以利用WriteNios这个函数来实现,或者你再增加一个ioctr函数,不过,大部分情况下,不需要你去更改LCD的显示方式。所以这里就没有增加。
四、最后想说的是#define IOCTL_SET_MSG _IOR(MAJOR_NUM, 0, char *)这句话,是使用系统的宏来构造ioctl的命令号IOCTL_SET_MSG,命令号应该在系统中是唯一的,所以必须用