(原创)uClinux下控制LCD16207等字符设备显示

  很久之前就想学习如何在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.ziplcd16207_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<length)

    {

      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,命令号应该在系统中是唯一的,所以必须用<asm/ioctl.h>中的宏来构造。

 

原文地址:https://www.cnblogs.com/yingfang18/p/1879586.html