• (原创)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>中的宏来构造。

     

  • 相关阅读:
    Informatica 常用组件Aggregator之三 使用排序输入
    Informatica 常用组件Aggregator之二 分组依据端口
    Informatica 常用组件Aggregator之一 聚合表达式
    Informatica 常用组件Filter之四 优化
    Informatica 常用组件Filter之三 创建FIL
    Informatica 常用组件Filter之二 过滤条件
    Informatica 常用组件Filter之一 概述
    Ubuntu 16.04 升级 PHP 版本至 7.1
    Socket远程调试日志之 SocketLog的简单实用
    Tesseract-ocr
  • 原文地址:https://www.cnblogs.com/yingfang18/p/1879586.html
Copyright © 2020-2023  润新知