• linux驱动移植LCD驱动


    一、LCD基础知识

    1.1 LCD硬件原理

    Mini2440裸机开发之LCD基础我们介绍了LCD的硬件原理,有兴趣的可以去看看,这里我们仅仅简述一下LCD的原理。

    下图是LCD示意图,里面的每个点就是一个像素点。它里面有一个电子枪,一边移动,一边发出各种颜色的光。用动态图表示如下:

    电子枪是如何移动的?

    • 有一条CLK时钟线与LCD相连,每发出一次CLK(高低电平),电子枪就移动一个像素。

    颜色如何确定?

    • 由连接LCD的三组线RGB三原色混合而成:R(Red)、G(Green)、B(Blue)确定。

    电子枪如何得知应跳到下一行?

    • 有一条HSYNC信号线与LCD相连,每发出一次脉冲(高低电平),电子枪就跳到下一行,该信号叫做行同步信号。

    电子枪如何得知应跳到原点?

    • 有一条VSYNC信号线与LCD相连,每发出一次脉冲(高低电平),电子枪就跳到原点,该信号叫做帧同步信号。

    RGB线上的数据从何而来?

    • 内存里面划分一块显存(framebuffer),里面存放了要显示的数据,LCD控制器从里面将数据读出来,通过RGB三组线传给电子枪,电子枪再依次打到显示屏上。

    前面的信号由谁发给LCD?

    • 有S3C2440里面的LCD控制器来控制发出信号。

    工作原理:

    • LCD屏可以看作是由许多象素构成的,比如320*240就是由320*240个象素构成的,每个象素由RGB三色调和,每种颜色又由多个位组成。比如我们的开发板上的LCD,有320*240个象素,每个象素由RGB三色调和,RGB三色位数分别为:565。
    • S3C2440内集成了LCD控制器,LCD控制器外接LCD,每来一个VLCK,就会从左到右在LCD屏幕上显示一个象素的颜色,而这一个个象素的颜色就存放在显存里,在嵌入式领域,一般不会佩戴专门的显存,而是从内存SDRAM中划分出一部分充当显存;
    • HSYNC引脚每发出一个脉冲,表示一行的数据开始发送;
    • VSYNC引脚每发出一个冒充,表示一帧的数据开始发送。

    1.2  frambuffer设备

    我们在Mini2440裸机开发之LCD编程(GB2312、ASCII字库制作) 中介绍了如何在LCD显示屏中显示一张图片,其核心步骤就是向framebuffer中写入图片数据。

    在linux中,如果我们的系统想使用GUI(图形界面接口),这时LCD设备驱动程序就应该编写成frambuffer接口,而不是像裸机中那样只编写操作底层的LCD控制器接口。

    framebuffer是linux系统为显示设备提供的一个用户接口,它将显示缓冲区抽象,屏蔽图像硬件的底层差异,允许上层应用程序在图形模式下直接对显示缓冲区进行操作,用户应用程序可以通过framebuffer透明地访问不同类型的显示设备。

    linux抽象出framebuffer这个帧缓冲区可以供用户应用程序直接读写,通过更改framebuffer中的内容,就可以立刻显示在LCD显示屏上。

    framebuffer是一个标准的字符设备,主设备号是29,次设备号根据缓冲区的数目而定。framebuffer对应/dev/fb%d设备文件。

    对用户程序而言,framebuffer设备它和/dev下面的其它设备没有什么区别,用户可以把frameBuffer看成一块内存,既可以写,又可以读。显示器将根据内存数据显示对应的图像界面,这一切都由framebuffer设备驱动来完成。

    注意:如果“framebuffer设备”你理解起来比较变扭,你就把它当做LCD设备即可,实际上我认为他们就是一个意思,只是叫法不一样。

    1.3 实现思路

    我们在介绍linux framebuffer驱动实现框架之前,我们试想一下,如果让我们按照之前所学的知识来实现framebuffer设备驱动我们会怎么做呢?

    • 首先我们会动态注册设备编号;
    • 初始化一个字符设备,并注册设备的file_operations;
    • 创建字符设备节点;
    • 编写file_operations成员函数open、write等函数;

    当然这么做可以么,当然也是可以的,不过这么存在一个问题,如果我们需要同时注册多个framebuffer设备,并且每个framebuffer设备具有很多共性的东西,他们的设备操作函数基本相同,只是LCD的屏幕尺寸、LCD控制器时序参数等信息存在差异,那我们应该如何进行抽象,屏蔽不同framebuffer之间的差异呢。

    实际上如果采用面向对象编程的话,我们可以这么干,我们抽象出frambebufer设备接口,然后实现一个抽象类继承该接口,在抽象类中实现一个些共性属性和方法,大致如下:

    实际上linux内核也是这么干的,只不过由于C不支持面向对象,在linux系统中,当系统接入若干个不同型号的LCD设备时,linux内核做了一层抽象,使用fb_info结构体来表示每一个LCD设备的信息以及设备操作方法。

    二、framebuffer设备驱动框架图

    framebuffer设备驱动在linux系统框架如下图:

    从上图可以看出frambebuffer设备驱动主要由两部分组成:

    • fbmem.c:为应用程序提供了对frambebuffer设备操作的系统调用,同时为硬件层提供了注册framebuffer设备的接口,比如register_frambebuffer;
    • xxxfb.c:主要实现了framebuffer设备的注册,实际上就是填充fb_info结构体,关于该结构的含义我们后面介绍。

    通过引入fb_info的形式,将硬件相关的部分与文件设备操作分离开,增加了内核代码的稳定性。我们只需调用register_framebuffer函数注册一个新的fb_info结构体,即可向内核新增一个framebuffer设备。

    三、基础数据结构

    3.1 fb_info结构体

    struct fb_info定义在include/linux/fb.h文件中,用于保存我们framebuffer设备信息,其内部提供了对framebuffer设备操作的函数指针:

    struct fb_info {
            atomic_t count;
            int node;
            int flags;
            /*
             * -1 by default, set to a FB_ROTATE_* value by the driver, if it knows
             * a lcd is not mounted upright and fbcon should rotate to compensate.
             */
            int fbcon_rotate_hint;
            struct mutex lock;              /* Lock for open/release/ioctl funcs */
            struct mutex mm_lock;           /* Lock for fb_mmap and smem_* fields */
            struct fb_var_screeninfo var;   /* Current var */
            struct fb_fix_screeninfo fix;   /* Current fix */
            struct fb_monspecs monspecs;    /* Current Monitor specs */
            struct work_struct queue;       /* Framebuffer event queue */
            struct fb_pixmap pixmap;        /* Image hardware mapper */
            struct fb_pixmap sprite;        /* Cursor hardware mapper */
            struct fb_cmap cmap;            /* Current cmap */
            struct list_head modelist;      /* mode list */
            struct fb_videomode *mode;      /* current mode */
    
    #if IS_ENABLED(CONFIG_FB_BACKLIGHT)
            /* assigned backlight device */
            /* set before framebuffer registration,
               remove after unregister */
            struct backlight_device *bl_dev;
    
            /* Backlight level curve */
            struct mutex bl_curve_mutex;
            u8 bl_curve[FB_BACKLIGHT_LEVELS];
    #endif
    #ifdef CONFIG_FB_DEFERRED_IO
            struct delayed_work deferred_work;
            struct fb_deferred_io *fbdefio;
    #endif
    
            struct fb_ops *fbops;
            struct device *device;          /* This is the parent */
            struct device *dev;             /* This is this fb device */
            int class_flag;                    /* private sysfs flags */
    #ifdef CONFIG_FB_TILEBLITTING
            struct fb_tile_ops *tileops;    /* Tile Blitting */
    #endif
            union {
                    char __iomem *screen_base;      /* Virtual address */
                    char *screen_buffer;
            };
            unsigned long screen_size;      /* Amount of ioremapped VRAM or 0 */
            void *pseudo_palette;           /* Fake palette of 16 colors */
    #define FBINFO_STATE_RUNNING    0
    #define FBINFO_STATE_SUSPENDED  1
            u32 state;                      /* Hardware state i.e suspend */
            void *fbcon_par;                /* fbcon use-only private area */
            /* From here on everything is device dependent */
            void *par;
            /* we need the PCI or similar aperture base/size not
               smem_start/size as smem_start may just be an object
               allocated inside the aperture so may not actually overlap */
            struct apertures_struct {
                    unsigned int count;
                    struct aperture {
                            resource_size_t base;
                            resource_size_t size;
                    } ranges[0];
            } *apertures;
    
            bool skip_vt_switch; /* no VT switch on suspend/resume required */
    };

    部分参数含义如下:

    • count:fb_info的引用计数,fb_open时使其+1,release时使其-1,为0时销毁;
    • node:全局变量registered_fb中的索引值,注册的时候分配,通过node可以索引fb_info;
    • flags:一些标志位,有关于硬件加速的,大小端,fb的内存位置(设备或者内存),具体硬件加速的方法,表明哪个使用了硬件加速;
    • var:描述的是LCD屏幕的可变参数,包括可见的分辨率,Bpp(bits_per_pixel),还有具体的时钟信号,包括bp,fp,vsync,hsync等,可以通过应用层设置也可以驱动层配置,相关设置时序的工具有fbset,还有相关的一些调色板配置;
    • fix:描述的是LCD屏幕的不可参数,不能在用户层更改。包括framebuffer缓冲区的区里起始位置(一般是显示控制器DMA起始地址,smem_start),framebuffer的长度(单位为字节,smem_len);
    • monspecs:描述的是显示器的一些参数,时序,生产日期等,一般这种信息描述在显示器中的EDID中,通过解析EDID来填充此参数;
    • queue:事件队列;
    • pixmap,sprite(光标)都是像素图,注册framebuffer的时候会默认申请;
    • cmap:设备独立的 colormap 信息,可以通过 ioctl 的 FBIOGETCMAP 和 FBIOPUTCMAP 命令设置 colormap;
    • modelist:将var参数转化成video mode,然后存入这个链表;
    • mode:一些时序,刷新率扫描方式(vmode)(隔行,逐行),极性(sync);
    • CONFIG_FB_BACKLIGHT:有关于背光曲线以及背光设备注册,需要注意的是需要在注册framebuffer之前就对其初始化;
    • CONFIG_FB_DEFERRED_IO:延迟IO,使用缺页中断的原理操作,减少FBIOPAN_DISPLAY带来的系统调用开支;
    • screen_base/screen_buffer:framebuffer缓冲区虚拟地址,u8类型,对应的物理地址保存在fix.smem_start中;
    • screen_size:framebuffer缓冲区大小;
    • fbops:提供具体的fb操作函数,主要是通过fbmem.c中提供的文件操作函数,间接调用fb_ops,主要的操作有fb_check_var,fb_pan_display,fb_mmap,等,以下三个函数提供了绘图的操作,可以使用系统中的绘图函数,也可以重写硬件加速的绘图函数;
    • device:fb_info的设备父节点,对应即/sys/device/xxx/fb_info;
    • dev:设备指针,注册framebuffer时创建;
    • pseudo_palette:伪调色板;
    • state:硬件状态,在fbmem中会设置成suspend以及resume;
    • skip_vt_switch:关于VT switch,是与console切换以及PM相关的;

    3.2 fb_info标志位

    fb_info标志位定义如下:

    /* FBINFO_* = fb_info.flags bit flags */
    #define FBINFO_DEFAULT          0
    #define FBINFO_HWACCEL_DISABLED 0x0002
            /* When FBINFO_HWACCEL_DISABLED is set:
             *  Hardware acceleration is turned off.  Software implementations
             *  of required functions (copyarea(), fillrect(), and imageblit())
             *  takes over; acceleration engine should be in a quiescent state */
    
    /* hints */
    #define FBINFO_VIRTFB           0x0004 /* FB is System RAM, not device. */
    #define FBINFO_PARTIAL_PAN_OK   0x0040 /* otw use pan only for double-buffering */
    #define FBINFO_READS_FAST       0x0080 /* soft-copy faster than rendering */
    
    /* hardware supported ops */
    /*  semantics: when a bit is set, it indicates that the operation is
     *   accelerated by hardware.
     *  required functions will still work even if the bit is not set.
     *  optional functions may not even exist if the flag bit is not set.
     */
    #define FBINFO_HWACCEL_NONE             0x0000
    #define FBINFO_HWACCEL_COPYAREA         0x0100 /* required */
    #define FBINFO_HWACCEL_FILLRECT         0x0200 /* required */
    #define FBINFO_HWACCEL_IMAGEBLIT        0x0400 /* required */
    #define FBINFO_HWACCEL_ROTATE           0x0800 /* optional */
    #define FBINFO_HWACCEL_XPAN             0x1000 /* optional */
    #define FBINFO_HWACCEL_YPAN             0x2000 /* optional */
    #define FBINFO_HWACCEL_YWRAP            0x4000 /* optional */
    
    #define FBINFO_MISC_USEREVENT          0x10000 /* event request
                                                      from userspace */
    #define FBINFO_MISC_TILEBLITTING       0x20000 /* use tile blitting */
    
    /* A driver may set this flag to indicate that it does want a set_par to be
     * called every time when fbcon_switch is executed. The advantage is that with
     * this flag set you can really be sure that set_par is always called before
     * any of the functions dependent on the correct hardware state or altering
     * that state, even if you are using some broken X releases. The disadvantage
     * is that it introduces unwanted delays to every console switch if set_par
     * is slow. It is a good idea to try this flag in the drivers initialization
     * code whenever there is a bug report related to switching between X and the
     * framebuffer console.
     */
    #define FBINFO_MISC_ALWAYS_SETPAR   0x40000
    
    /* where the fb is a firmware driver, and can be replaced with a proper one */
    #define FBINFO_MISC_FIRMWARE        0x80000
    /*
     * Host and GPU endianness differ.
     */
    #define FBINFO_FOREIGN_ENDIAN   0x100000
    /*
     * Big endian math. This is the same flags as above, but with different
     * meaning, it is set by the fb subsystem depending FOREIGN_ENDIAN flag
     * and host endianness. Drivers should not use this flag.
     */
    #define FBINFO_BE_MATH  0x100000
    /*
     * Hide smem_start in the FBIOGET_FSCREENINFO IOCTL. This is used by modern DRM
     * drivers to stop userspace from trying to share buffers behind the kernel's
     * back. Instead dma-buf based buffer sharing should be used.
     */
    #define FBINFO_HIDE_SMEM_START  0x200000

    3.3 fb_ops 

    fb_ops里存放时的framebuffer设备操作函数:

    /*
     * Frame buffer operations
     *
     * LOCKING NOTE: those functions must _ALL_ be called with the console
     * semaphore held, this is the only suitable locking mechanism we have
     * in 2.6. Some may be called at interrupt time at this point though.
     *
     * The exception to this is the debug related hooks.  Putting the fb
     * into a debug state (e.g. flipping to the kernel console) and restoring
     * it must be done in a lock-free manner, so low level drivers should
     * keep track of the initial console (if applicable) and may need to
     * perform direct, unlocked hardware writes in these hooks.
     */
    
    struct fb_ops {
            /* open/release and usage marking */
            struct module *owner;
            int (*fb_open)(struct fb_info *info, int user);
            int (*fb_release)(struct fb_info *info, int user);
    
            /* For framebuffers with strange non linear layouts or that do not
             * work with normal memory mapped access
             */
            ssize_t (*fb_read)(struct fb_info *info, char __user *buf,
                               size_t count, loff_t *ppos);
            ssize_t (*fb_write)(struct fb_info *info, const char __user *buf,
                                size_t count, loff_t *ppos);
    
            /* checks var and eventually tweaks it to something supported,
             * DO NOT MODIFY PAR */
            int (*fb_check_var)(struct fb_var_screeninfo *var, struct fb_info *info);
    
            /* set the video mode according to info->var */
            int (*fb_set_par)(struct fb_info *info);
    
            /* set color register */
            int (*fb_setcolreg)(unsigned regno, unsigned red, unsigned green,
                                unsigned blue, unsigned transp, struct fb_info *info);
    
            /* set color registers in batch */
            int (*fb_setcmap)(struct fb_cmap *cmap, struct fb_info *info);
    
            /* blank display */
            int (*fb_blank)(int blank, struct fb_info *info);
    
            /* pan display */
            int (*fb_pan_display)(struct fb_var_screeninfo *var, struct fb_info *info);
    
            /* Draws a rectangle */
            void (*fb_fillrect) (struct fb_info *info, const struct fb_fillrect *rect);
            /* Copy data from area to another */
            void (*fb_copyarea) (struct fb_info *info, const struct fb_copyarea *region);
            /* Draws a image to the display */
            void (*fb_imageblit) (struct fb_info *info, const struct fb_image *image);
    
            /* Draws cursor */
            int (*fb_cursor) (struct fb_info *info, struct fb_cursor *cursor);
    
            /* wait for blit idle, optional */
            int (*fb_sync)(struct fb_info *info);
    
            /* perform fb specific ioctl (optional) */
            int (*fb_ioctl)(struct fb_info *info, unsigned int cmd,
                            unsigned long arg);
    
            /* Handle 32bit compat ioctl (optional) */
            int (*fb_compat_ioctl)(struct fb_info *info, unsigned cmd,
                            unsigned long arg);
    
            /* perform fb specific mmap */
            int (*fb_mmap)(struct fb_info *info, struct vm_area_struct *vma);
    
            /* get capability given var */
            void (*fb_get_caps)(struct fb_info *info, struct fb_blit_caps *caps,
                                struct fb_var_screeninfo *var);
    
            /* teardown any resources to do with this framebuffer */
            void (*fb_destroy)(struct fb_info *info);
    
            /* called at KDB enter and leave time to prepare the console */
            int (*fb_debug_enter)(struct fb_info *info);
            int (*fb_debug_leave)(struct fb_info *info);
    };

    四、fbmem.c源码分析

    fbmem.c是framebuffer设备驱动的核心,它向上给应用程序提供了系统调用的接口,向下对特定的硬件提供了设备注册接口。

    fbmem.c位于drivers/video/fbdev/core路径下。我们可以在该文件定位到驱动模块的入口和出口:

    module_init(fbmem_init);
    module_exit(fbmem_exit);

    4.1 入口函数

    我们定位到fbmem.c的入口函数,也就是fbmem_init:

    /**
     *      fbmem_init - init frame buffer subsystem
     *
     *      Initialize the frame buffer subsystem.
     *
     *      NOTE: This function is _only_ to be called by drivers/char/mem.c.
     *
     */
    
    static int __init
    fbmem_init(void)
    {
            int ret;
    
            if (!proc_create_seq("fb", 0, NULL, &proc_fb_seq_ops))
                    return -ENOMEM;
    
            ret = register_chrdev(FB_MAJOR, "fb", &fb_fops);
            if (ret) {
                    printk("unable to get major %d for fb devs\n", FB_MAJOR);
                    goto err_chrdev;
            }
    
            fb_class = class_create(THIS_MODULE, "graphics");
            if (IS_ERR(fb_class)) {
                    ret = PTR_ERR(fb_class);
                    pr_warn("Unable to create fb class; errno = %d\n", ret);
                    fb_class = NULL;
                    goto err_class;
            }
    
            fb_console_init();
    
            return 0;
    
    err_class:
            unregister_chrdev(FB_MAJOR, "fb");
    err_chrdev:
            remove_proc_entry("fb", NULL);
            return ret;
    }

    简要分析一下该函数执行流程:

    • 创建/proc/fb文件;

    • 创建字符设备fb,主设备编号为FB_MAJOR(29),注册file_operations结构体fb_fops;

    • 调用class_create在/sys/class目录下创建graphics这个类,但是此时并没有调用device_create在/dev下创建设备节点 ;

    可以通过如下命令查看字符设备:

    root@zhengyang:/work/sambashare/linux-5.2.8# cat /proc/devices
    Character devices:
      1 mem
      4 /dev/vc/0
      4 tty
      4 ttyS
      5 /dev/tty
      5 /dev/console
      5 /dev/ptmx
      5 ttyprintk
      6 lp
      7 vcs
     10 misc
     13 input
     14 sound/midi
     14 sound/dmmidi
     21 sg
     29 fb  // 这个名字来自register_chrdev函数第二个参数
     89 i2c

    可以看到,确实是创建了主设备号为29的"fb"字符设备,而这里还没有创建设备节点,后面会提到,内核将该工作放到register_framebuffer函数里了。

    4.2 fb_fops

    我们再来看看file_operations结构体fb_fops:

    static const struct file_operations fb_fops = {
            .owner =        THIS_MODULE,
            .read =         fb_read,
            .write =        fb_write,
            .unlocked_ioctl = fb_ioctl,
    #ifdef CONFIG_COMPAT
            .compat_ioctl = fb_compat_ioctl,
    #endif
            .mmap =         fb_mmap,
            .open =         fb_open,
            .release =      fb_release,
    #if defined(HAVE_ARCH_FB_UNMAPPED_AREA) || \
            (defined(CONFIG_FB_PROVIDE_GET_FB_UNMAPPED_AREA) && \
             !defined(CONFIG_MMU))
            .get_unmapped_area = get_fb_unmapped_area,
    #endif
    #ifdef CONFIG_FB_DEFERRED_IO
            .fsync =        fb_deferred_io_fsync,
    #endif
            .llseek =       default_llseek,
    };

    下面我们来一一分析这些成员函数。

    4.3 fb_open

    static int
    fb_open(struct inode *inode, struct file *file)
    __acquires(&info->lock)
    __releases(&info->lock)
    {
            int fbidx = iminor(inode);       // 获取设备节点的次设备号
            struct fb_info *info;            // 定义fb_info指针
            int res = 0;
    
            info = get_fb_info(fbidx);       // 根据次设备编号获取fb_info
            if (!info) {
                    request_module("fb%d", fbidx);
                    info = get_fb_info(fbidx);
                    if (!info)
                            return -ENODEV;
            }
            if (IS_ERR(info))
                    return PTR_ERR(info);
    
            mutex_lock(&info->lock);               // 获取互斥锁
            if (!try_module_get(info->fbops->owner)) {
                    res = -ENODEV;
                    goto out;
            }
            file->private_data = info;
            if (info->fbops->fb_open) {
                    res = info->fbops->fb_open(info,1);   
                    if (res)
                            module_put(info->fbops->owner);
            }
    #ifdef CONFIG_FB_DEFERRED_IO
            if (info->fbdefio)
                    fb_deferred_io_open(info, inode, file);
    #endif
    out:
            mutex_unlock(&info->lock);             // 释放互斥锁  
            if (res)
                    put_fb_info(info);
            return res;
    }

    这里我们来看一下get_fb_info函数的实现:

    static struct fb_info *get_fb_info(unsigned int idx)
    {
            struct fb_info *fb_info;
    
            if (idx >= FB_MAX)
                    return ERR_PTR(-ENODEV);
    
            mutex_lock(&registration_lock);
            fb_info = registered_fb[idx];
            if (fb_info)
                    atomic_inc(&fb_info->count);
            mutex_unlock(&registration_lock);
    
            return fb_info;
    }

    可以看到get_fb_info函数将registered_fb数组的第idx个元素赋值给了fb_info,registered_fb是一个struct fb_info结构类型的全局数组:

    struct fb_info *registered_fb[FB_MAX] __read_mostly;

    这和数组会在register_framebuffer函数中赋值。

    经过分析,我们最终会发现fb_open执行的是fbops的操作函数中的fbopen函数。

    4.4 fb_read

    static ssize_t
    fb_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
    {
            unsigned long p = *ppos;                    // 读取起始位置
            struct fb_info *info = file_fb_info(file);  // 获取fb_info
            u8 *buffer, *dst;
            u8 __iomem *src;
            int c, cnt = 0, err = 0;
            unsigned long total_size;
    
            if (!info || ! info->screen_base)
                    return -ENODEV;
    
            if (info->state != FBINFO_STATE_RUNNING)
                    return -EPERM;
    
            if (info->fbops->fb_read)
                    return info->fbops->fb_read(info, buf, count, ppos); //执行fbops操作函数里的fb_read函数
    
            total_size = info->screen_size;       // 屏幕尺寸   假设屏幕大小240*320,每个像素占n字节数 则屏幕尺寸为240*320*n
    
            if (total_size == 0)
                    total_size = info->fix.smem_len;    //frmebuffer缓冲区大小
    
            if (p >= total_size)
                    return 0;
    
            if (count >= total_size)          // 最多把整个屏幕数据全部读取了
                    count = total_size;
    
            if (count + p > total_size)
                    count = total_size - p;
    
            buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count,  // 分配缓冲器 大于一页的话,按页大小读取
                             GFP_KERNEL);
            if (!buffer)
                    return -ENOMEM;
    
            src = (u8 __iomem *) (info->screen_base + p);  // 获取读取起始地址(虚拟地址)
    
            if (info->fbops->fb_sync)
                    info->fbops->fb_sync(info);
    
            while (count) {
                    c  = (count > PAGE_SIZE) ? PAGE_SIZE : count;  
                    dst = buffer;
                    fb_memcpy_fromfb(dst, src, c);    // 一次从src读取c个字节到dst
                    dst += c;
                    src += c;
    
                    if (copy_to_user(buf, buffer, c)) {   // 写回用户空间buf中,长度为c
                            err = -EFAULT;
                            break;
                    }
                    *ppos += c;
                    buf += c;
                    cnt += c;
                    count -= c;
            }
    
            kfree(buffer); // 释放内存
    
            return (err) ? err : cnt;
    }

    可以看到如果提供了fbops操作函数里的fb_read函数,则直接调用info->fbops->fb_read(info, buf, count, ppos)从framebuff缓冲区读取数据。

    否则直接从info->screen_base + ppos地址读取count字节数据到buf缓冲区。

    4.5 fb_write

    static ssize_t
    fb_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
    {
            unsigned long p = *ppos;   // 写入起始位置
            struct fb_info *info = file_fb_info(file); // 获取fb_info
            u8 *buffer, *src;
            u8 __iomem *dst;
            int c, cnt = 0, err = 0;
            unsigned long total_size;
    
            if (!info || !info->screen_base)
                    return -ENODEV;
    
            if (info->state != FBINFO_STATE_RUNNING)
                    return -EPERM;
    
            if (info->fbops->fb_write)
                    return info->fbops->fb_write(info, buf, count, ppos); // 执行fbops操作函数里的fb_write函数
    
            total_size = info->screen_size;  // 屏幕尺寸   假设屏幕大小240*320,每个像素占n字节数 则屏幕尺寸为240*320*n
    
            if (total_size == 0)
                    total_size = info->fix.smem_len; //frmebuffer缓冲区大小
    
            if (p > total_size)
                    return -EFBIG;
    
            if (count > total_size) {
                    err = -EFBIG;
                    count = total_size;
            }
    
            if (count + p > total_size) {
                    if (!err)
                            err = -ENOSPC;
    
                    count = total_size - p;
            }
    
            buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count,
                             GFP_KERNEL); // 分配缓冲器 大于一页的话,按页大小写入
            if (!buffer)
                    return -ENOMEM;
    
            dst = (u8 __iomem *) (info->screen_base + p); // 获取写入起始地址(虚拟地址)
    
            if (info->fbops->fb_sync)
                    info->fbops->fb_sync(info);
    
            while (count) {
                    c = (count > PAGE_SIZE) ? PAGE_SIZE : count;
                    src = buffer;
    
                    if (copy_from_user(src, buf, c)) {  // 从用户空间获取数据到内容
                            err = -EFAULT;
                            break;
                    }
    
                    fb_memcpy_tofb(dst, src, c);  // 一次将src中c个字节写到dst
                    dst += c;
                    src += c;
                    *ppos += c;
                    buf += c;
                    cnt += c;
                    count -= c;
            }
    
            kfree(buffer); // 释放内存
    
            return (cnt) ? cnt : err;
    }

    可以看到如果提供了fbops操作函数里的fb_write函数,则直接调用iinfo->fbops->fb_write(info, buf, count, ppos)向framebuffer缓冲区写入数据。

    否则直接向从buf中读取count个字节写入nfo->screen_base + ppos地址处。

    4.6 fb_mmap

    static int
    fb_mmap(struct file *file, struct vm_area_struct * vma)
    {
            struct fb_info *info = file_fb_info(file);  // 获取fb_info
            struct fb_ops *fb;
            unsigned long mmio_pgoff;
            unsigned long start;
            u32 len;
    
            if (!info)
                    return -ENODEV;
            fb = info->fbops;
            if (!fb)
                    return -ENODEV;
            mutex_lock(&info->mm_lock);
            if (fb->fb_mmap) {
                    int res;
    
                    /*
                     * The framebuffer needs to be accessed decrypted, be sure
                     * SME protection is removed ahead of the call
                     */
                    vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot);
                    res = fb->fb_mmap(info, vma);
                    mutex_unlock(&info->mm_lock);
                    return res;
            }
    
            /*
             * Ugh. This can be either the frame buffer mapping, or
             * if pgoff points past it, the mmio mapping.
             */
            start = info->fix.smem_start;  // framebuffer缓冲区起始地址(物理地址)
            len = info->fix.smem_len;      //  framebuffer缓冲区大小
            mmio_pgoff = PAGE_ALIGN((start & ~PAGE_MASK) + len) >> PAGE_SHIFT;
            if (vma->vm_pgoff >= mmio_pgoff) {
                    if (info->var.accel_flags) {
                            mutex_unlock(&info->mm_lock);
                            return -EINVAL;
                    }
    
                    vma->vm_pgoff -= mmio_pgoff;
                    start = info->fix.mmio_start;
                    len = info->fix.mmio_len;
            }
            mutex_unlock(&info->mm_lock);
    
            vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
            /*
             * The framebuffer needs to be accessed decrypted, be sure
             * SME protection is removed
             */
            vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot);
            fb_pgprotect(file, vma, start);
    
            return vm_iomap_memory(vma, start, len);
    }

    framebuffer的显示缓冲区位于linux的内核态地址空间。而在linux中,每个应用程序都有自己的虚拟地址空间,在应用程序中是不能直接访问物理缓冲区的。

    为此,linux在文件操作file_operations结构中提供了mmap函数,可将文件的内容映射到用户空间。

    对应帧缓冲设备,则可以通过映射操作,将屏幕缓冲区的物理地址映射到用户空间的一段虚拟地址中,之后用户就可以通过读写这段虚拟地址访问屏幕缓冲区,在屏幕上绘图。

    4.7 register_framebuffer

    register_framebuffer函数用于向内核注册framebuffer设备:

    /**
     *      register_framebuffer - registers a frame buffer device
     *      @fb_info: frame buffer info structure
     *
     *      Registers a frame buffer device @fb_info.
     *
     *      Returns negative errno on error, or zero for success.
     *
     */
    int
    register_framebuffer(struct fb_info *fb_info)
    {
            int ret;
    
            mutex_lock(&registration_lock);
            ret = do_register_framebuffer(fb_info);
            mutex_unlock(&registration_lock);
    
            return ret;
    }

    在do_register_farmebuffer之前前后加入了互斥锁,可以判断出该操作是线程安全的,我们定位到 do_register_framebuffer函数:

    static int do_register_framebuffer(struct fb_info *fb_info)
    {
            int i, ret;
            struct fb_event event;
            struct fb_videomode mode;
    
            if (fb_check_foreignness(fb_info))
                    return -ENOSYS;
    
            ret = do_remove_conflicting_framebuffers(fb_info->apertures,
                                                     fb_info->fix.id,
                                                     fb_is_primary_device(fb_info));
            if (ret)
                    return ret;
    
            if (num_registered_fb == FB_MAX)   // 已达到最大注册设备数
                    return -ENXIO;
    
            num_registered_fb++;               // 已注册设备计数+1
            for (i = 0 ; i < FB_MAX; i++)      // 查找空的数组项 
                    if (!registered_fb[i])
                            break;
            fb_info->node = i;               // 设置fb_info在registered_fb数组中的索引号 
            atomic_set(&fb_info->count, 1);  // 引用计数设置为1 
            mutex_init(&fb_info->lock);      // 初始化互斥锁
            mutex_init(&fb_info->mm_lock);   // 初始化互斥锁  
    
            fb_info->dev = device_create(fb_class, fb_info->device,  // 设备创建是在这里完成的,设备名称为fb%d 可以在/dev下看到fb%d设备 次设备号为i
                                         MKDEV(FB_MAJOR, i), NULL, "fb%d", i);
            if (IS_ERR(fb_info->dev)) {
                    /* Not fatal */
                    printk(KERN_WARNING "Unable to create device for framebuffer %d; errno = %ld\n", i, PTR_ERR(fb_info->dev));
                    fb_info->dev = NULL;
            } else
                    fb_init_device(fb_info);  // 初始化fb_info部分参数
    
            if (fb_info->pixmap.addr == NULL) {
                    fb_info->pixmap.addr = kmalloc(FBPIXMAPSIZE, GFP_KERNEL);
                    if (fb_info->pixmap.addr) {
                            fb_info->pixmap.size = FBPIXMAPSIZE;
                            fb_info->pixmap.buf_align = 1;
                            fb_info->pixmap.scan_align = 1;
                            fb_info->pixmap.access_align = 32;
                            fb_info->pixmap.flags = FB_PIXMAP_DEFAULT;
                    }
            }
            fb_info->pixmap.offset = 0;
    
            if (!fb_info->pixmap.blit_x)
                    fb_info->pixmap.blit_x = ~(u32)0;
    
            if (!fb_info->pixmap.blit_y)
                    fb_info->pixmap.blit_y = ~(u32)0;
    
            if (!fb_info->modelist.prev || !fb_info->modelist.next)
                    INIT_LIST_HEAD(&fb_info->modelist);              // 初始化双向链表
    
            if (fb_info->skip_vt_switch)
                    pm_vt_switch_required(fb_info->dev, false);
            else
                    pm_vt_switch_required(fb_info->dev, true);
    
            fb_var_to_videomode(&mode, &fb_info->var);
            fb_add_videomode(&mode, &fb_info->modelist);
            registered_fb[i] = fb_info;                            // 设置数组第i个元素
    
            event.info = fb_info;
            if (!lockless_register_fb)
                    console_lock();
            else
                    atomic_inc(&ignore_console_lock_warning);
            if (!lock_fb_info(fb_info)) {
                    ret = -ENODEV;
                    goto unlock_console;
            }
            ret = 0;
    
            fb_notifier_call_chain(FB_EVENT_FB_REGISTERED, &event);
            unlock_fb_info(fb_info);
    unlock_console:
            if (!lockless_register_fb)
                    console_unlock();
            else
                    atomic_dec(&ignore_console_lock_warning);
            return ret;
    }

    do_register_farmebuffer函数首先从registered_fb数组中查找空的数组项,然后填充fb_info结构体,赋给这个空的数组项中。在这里还创建了设备节点(前面创建字符设备未完成的工作)。

    从这里我们可以看出,register_framebuffer()函数通过注册各种各样的fb_info,来让内核支持多种framebuffer设备,并且以/dev/fb%d的形式命名。

    五、platform设备注册(s3c2410-lcd)

    5.1 LCD相关结构体

    我们定位到arch/arm/plat-samsung/include/plat/fb-s3c2410.h头文件:

    struct s3c2410fb_hw {
            unsigned long   lcdcon1;
            unsigned long   lcdcon2;
            unsigned long   lcdcon3;
            unsigned long   lcdcon4;
            unsigned long   lcdcon5;
    };
    
    /* LCD description */
    struct s3c2410fb_display {
            /* LCD type */
            unsigned type;
    
            /* Screen size */
            unsigned short width;
            unsigned short height;
    
            /* Screen info */
            unsigned short xres;
            unsigned short yres;
            unsigned short bpp;
    
            unsigned pixclock;              /* pixclock in picoseconds */
            unsigned short left_margin;  /* value in pixels (TFT) or HCLKs (STN) */
            unsigned short right_margin; /* value in pixels (TFT) or HCLKs (STN) */
            unsigned short hsync_len;    /* value in pixels (TFT) or HCLKs (STN) */
            unsigned short upper_margin;    /* value in lines (TFT) or 0 (STN) */
            unsigned short lower_margin;    /* value in lines (TFT) or 0 (STN) */
            unsigned short vsync_len;       /* value in lines (TFT) or 0 (STN) */
    
            /* lcd configuration registers */
            unsigned long   lcdcon5;
    };
    
    struct s3c2410fb_mach_info {
    
            struct s3c2410fb_display *displays;     /* attached displays info */
            unsigned num_displays;                  /* number of defined displays */
            unsigned default_display;
    
            /* GPIOs */
    
            unsigned long   gpcup;
            unsigned long   gpcup_mask;
            unsigned long   gpccon;
            unsigned long   gpccon_mask;
            unsigned long   gpdup;
            unsigned long   gpdup_mask;
            unsigned long   gpdcon;
            unsigned long   gpdcon_mask;
    
            /* lpc3600 control register */
            unsigned long   lpcsel;
    };

    这里定义了s3c24xx系列SOC关于LCD相关配置的结构体:

    • s3c2410fb_hw中定义了LCD控制寄存器
    • s3c2410fb_display:定义了LCD相关信息
    • s3c2410fb_mach_info :包含了LCD以及GPIO相关信息;

    5.2  结构体全局变量

    我们定位到 arch/arm/mach-s3c24xx/mach-smdk2440.c文件,在这个里面我们可以看到LCD相关的信息定义:

    /* LCD driver info */
    
    static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = {
    
            .lcdcon5        = S3C2410_LCDCON5_FRM565 |
                              S3C2410_LCDCON5_INVVLINE |
                              S3C2410_LCDCON5_INVVFRAME |
                              S3C2410_LCDCON5_PWREN |
                              S3C2410_LCDCON5_HWSWP,
    
            .type           = S3C2410_LCDCON1_TFT,
    
            .width          = 240,
            .height         = 320,
    
            .pixclock       = 166667, /* 每个像素时长,10^12/VCLK */
            .xres           = 240,
            .yres           = 320,
            .bpp            = 16,
            .left_margin    = 20,
            .right_margin   = 8,
            .hsync_len      = 4,
            .upper_margin   = 8,
            .lower_margin   = 7,
            .vsync_len      = 4,
    };
    
    static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = {
            .displays       = &smdk2440_lcd_cfg,
            .num_displays   = 1,
            .default_display = 0,
    
    #if 0
            /* currently setup by downloader */
            .gpccon         = 0xaa940659,
            .gpccon_mask    = 0xffffffff,
            .gpcup          = 0x0000ffff,
            .gpcup_mask     = 0xffffffff,
            .gpdcon         = 0xaa84aaa0,
            .gpdcon_mask    = 0xffffffff,
            .gpdup          = 0x0000faff,
            .gpdup_mask     = 0xffffffff,
    #endif
    
            .lpcsel         = ((0xCE6) & ~7) | 1<<4,
    };

    可以看到这里声明了全局变量smdk2440_lcd_cfg、smdk2440_fb_info并进行了初始化,如果我们想支持我们LCD的话,实际上这要修改这些配置信息即可。

    5.3  smdk2440_machine_init

    linux内核启动的时候会根据uboot中设置的机器id执行相应的初始化工作,比如.init_machine、.init_irq:

    static void __init smdk2440_machine_init(void)
    {
            s3c24xx_fb_set_platdata(&smdk2440_fb_info);
            s3c_i2c0_set_platdata(NULL);
    
            platform_add_devices(smdk2440_devices, ARRAY_SIZE(smdk2440_devices));
            smdk_machine_init();
    }
    
    MACHINE_START(S3C2440, "SMDK2440")
            /* Maintainer: Ben Dooks <ben-linux@fluff.org> */
            .atag_offset    = 0x100,
    
            .init_irq       = s3c2440_init_irq,
            .map_io         = smdk2440_map_io,
            .init_machine   = smdk2440_machine_init,
            .init_time      = smdk2440_init_time,
    MACHINE_END

    这里我们只关注smdk2440_fb_info相关的代码,我们定位到s3c24xx_fb_set_platdata函数,位于 arch/arm/plat-samsung/devs.c文件中,实际上在这个文件里根据我们内核编译配置的宏,注册不同的platform设备,比如这里我们注册了名字为"s3c2410-lcd"的platform设备:

    /* LCD Controller */
    
    #ifdef CONFIG_PLAT_S3C24XX
    static struct resource s3c_lcd_resource[] = {
            [0] = DEFINE_RES_MEM(S3C24XX_PA_LCD, S3C24XX_SZ_LCD),  // 定义起始地址资源 0x4D000000(LCD相关寄存器基地址)、大小为1M
            [1] = DEFINE_RES_IRQ(IRQ_LCD),
    };
    
    struct platform_device s3c_device_lcd = {
            .name           = "s3c2410-lcd",
            .id             = -1,
            .num_resources  = ARRAY_SIZE(s3c_lcd_resource),
            .resource       = s3c_lcd_resource,
            .dev            = {
                    .dma_mask               = &samsung_device_dma_mask,
                    .coherent_dma_mask      = DMA_BIT_MASK(32),
            }
    };
    
    void __init s3c24xx_fb_set_platdata(struct s3c2410fb_mach_info *pd) // pd = smdk2440_fb_info
    {
            struct s3c2410fb_mach_info *npd;
    
            npd = s3c_set_platdata(pd, sizeof(*npd), &s3c_device_lcd);
            if (npd) {
                    npd->displays = kmemdup(pd->displays,
                            sizeof(struct s3c2410fb_display) * npd->num_displays,
                            GFP_KERNEL);
                    if (!npd->displays)
                            printk(KERN_ERR "no memory for LCD display data\n");
            } else {
                    printk(KERN_ERR "no memory for LCD platform data\n");
            }
    }
    #endif /* CONFIG_PLAT_S3C24XX */

    这里我们调用了s3c_set_platdata函数,该函数设置s3c_device_lcd->dev.platform_data=smdk2440_fb_info。

    5.4 s3c_set_platdata

    s3c_set_platdata定义在arch/arm/plat-samsung/platformdata.c文件中:

    void __init *s3c_set_platdata(void *pd, size_t pdsize,   // pd = smdk2440_fb_info , pdev = s3c_device_lcd
                                  struct platform_device *pdev)
    {
            void *npd;
    
            if (!pd) {
                    /* too early to use dev_name(), may not be registered */
                    printk(KERN_ERR "%s: no platform data supplied\n", pdev->name);
                    return NULL;
            }
    
            npd = kmemdup(pd, pdsize, GFP_KERNEL);
            if (!npd)
                    return NULL;
    
            pdev->dev.platform_data = npd;
            return npd;
    }

    这个函数主要是用来设置pdev->dev的platform_data成员,是个void *类型,可以给平台driver提供各种数据(比如:GPIO引脚等等)。

    5.5 platform设备注册

    到了这里我们定义了LCD相关的platform_device设备,并进行了初始化,那platform设备啥时候注册的呢?

    我们定位到smdk2440_machine_init中的如下函数:

     platform_add_devices(smdk2440_devices, ARRAY_SIZE(smdk2440_devices));

    这里利用platform_add_devices进行若干个platform设备的注册,该函数还是通过调用platform_device_register实现platform设备注册:

    /**
     * platform_add_devices - add a numbers of platform devices
     * @devs: array of platform devices to add
     * @num: number of platform devices in array
     */
    int platform_add_devices(struct platform_device **devs, int num)
    {
            int i, ret = 0;
    
            for (i = 0; i < num; i++) {
                    ret = platform_device_register(devs[i]);
                    if (ret) {
                            while (--i >= 0)
                                    platform_device_unregister(devs[i]);
                            break;
                    }
            }
    
            return ret;
    }

    smdk2440_devices中就包含了s3c_device_lcd:

    static struct platform_device *smdk2440_devices[] __initdata = {
            &s3c_device_ohci,
            &s3c_device_lcd,
            &s3c_device_wdt,
            &s3c_device_i2c0,
            &s3c_device_iis,
            &smdk2440_device_eth,
    };

    六、platform驱动注册(s3c2410-lcd)

    既然注册了名字为"s3c2410-lcd"的platform设备,那么名字为"s3c2410-lcd"的platform驱动在哪里注册的呢?

    其相关代码为drivers/video/fbdev/s3c2410fb.c,在该文件里构建了fb_info结构体。我们可以在该文件定位到驱动模块的入口和出口:

    module_init(s3c2410fb_init);
    module_exit(s3c2410fb_cleanup);

    6.1 入口函数

    我们定位到fbmem.c的入口函数,也就是s3c2410fb_init:

    int __init s3c2410fb_init(void)
    {
            int ret = platform_driver_register(&s3c2410fb_driver);
    
            if (ret == 0)
                    ret = platform_driver_register(&s3c2412fb_driver);
    
            return ret;
    }

    看到这里是不是有点意外,这里是通过platform_driver_register函数注册了一个platform驱动。

    在plaftrom总线设备驱动模型中,我们知道当内核中有platform设备的.name名称和platform驱动s3c2410fb_driver里driver的name相同,会调用到platform_driver里的成员.probe,在这里就是s3c2410fb_probe函数。

    static struct platform_driver s3c2410fb_driver = {
            .probe          = s3c2410fb_probe,
            .remove         = s3c2410fb_remove,
            .suspend        = s3c2410fb_suspend,
            .resume         = s3c2410fb_resume,
            .driver         = {
                    .name   = "s3c2410-lcd",
            },
    };

    6.2 s3c2410fb_probe

    static int s3c2410fb_probe(struct platform_device *pdev)
    {
            return s3c24xxfb_probe(pdev, DRV_S3C2410);
    }

    定位到s3c24xxfb_probe:smdk2440_fb_info

    static int s3c24xxfb_probe(struct platform_device *pdev,
                               enum s3c_drv_type drv_type)
    {
            struct s3c2410fb_info *info;
            struct s3c2410fb_display *display;
            struct fb_info *fbinfo;
            struct s3c2410fb_mach_info *mach_info;
            struct resource *res;
            int ret;
            int irq;
            int i;
            int size;
            u32 lcdcon1;
    
            mach_info = dev_get_platdata(&pdev->dev);      // 这里实际获取到的就是smdk2440_fb_info,类型为s3c2440fb_mach_info
            if (mach_info == NULL) {
                    dev_err(&pdev->dev,
                            "no platform data for lcd, cannot attach\n");
                    return -EINVAL;
            }
    
            if (mach_info->default_display >= mach_info->num_displays) { // 默认使用的LCD索引号 >= 支持的LCD总数
                    dev_err(&pdev->dev, "default is %d but only %d displays\n",
                            mach_info->default_display, mach_info->num_displays);
                    return -EINVAL;
            }
    
            display = mach_info->displays + mach_info->default_display;   // 获取使用的LCD描述信息
    
            irq = platform_get_irq(pdev, 0);
            if (irq < 0) {
                    dev_err(&pdev->dev, "no irq for device\n");
                    return -ENOENT;
            }
    
            fbinfo = framebuffer_alloc(sizeof(struct s3c2410fb_info), &pdev->dev);  // 分配一个fb_info结构体,额外分配s3c2410fb_info大小的内存,初始化fbinfo->device = &pdev->dev
            if (!fbinfo)
                    return -ENOMEM;
    
            platform_set_drvdata(pdev, fbinfo);             // 设置pdev->dev.driver_data = fbinfo
    
            info = fbinfo->par;                         // 获取成员par
            info->dev = &pdev->dev;                     // 初始化par成员
            info->drv_type = drv_type;
    
            res = platform_get_resource(pdev, IORESOURCE_MEM, 0);  // 获取第一个地址内存设备资源,地址范围0x4D000000~(0x4D000000+1MB)
            if (res == NULL) {
                    dev_err(&pdev->dev, "failed to get memory registers\n");
                    ret = -ENXIO;
                    goto dealloc_fb;
            }
            size = resource_size(res);           // 1MB
            info->mem = request_mem_region(res->start, size, pdev->name); // 请求内存区域
            if (info->mem == NULL) {
                    dev_err(&pdev->dev, "failed to get memory region\n");
                    ret = -ENOENT;
                    goto dealloc_fb;
            }
    
            info->io = ioremap(res->start, size);   // 将LCD相关寄存器起始物理地址映射到虚拟地址,并返回虚拟地址
            if (info->io == NULL) {
                    dev_err(&pdev->dev, "ioremap() of registers failed\n");
                    ret = -ENXIO;
                    goto release_mem;
            }
    
            if (drv_type == DRV_S3C2412)
                    info->irq_base = info->io + S3C2412_LCDINTBASE;
            else
                    info->irq_base = info->io + S3C2410_LCDINTBASE;
    
            dprintk("devinit\n");
    
            strcpy(fbinfo->fix.id, driver_name);  // 设置fb_info成员id为s3c2410fb
    
            /* Stop the video */
            lcdcon1 = readl(info->io + S3C2410_LCDCON1);   // S3C2410_LCDCON1=0,从而得到LCDCON1寄存器地址 读取寄存器值
            writel(lcdcon1 & ~S3C2410_LCDCON1_ENVID, info->io + S3C2410_LCDCON1); // 输出使能位设置为禁止
    
            // 设置LCD不可变参数
            fbinfo->fix.type            = FB_TYPE_PACKED_PIXELS;       
            fbinfo->fix.type_aux        = 0;
            fbinfo->fix.xpanstep        = 0;
            fbinfo->fix.ypanstep        = 0;
            fbinfo->fix.ywrapstep       = 0;
            fbinfo->fix.accel           = FB_ACCEL_NONE;
    
           // 设置LCD可变参数
            fbinfo->var.nonstd          = 0;
            fbinfo->var.activate        = FB_ACTIVATE_NOW;
            fbinfo->var.accel_flags     = 0;
            fbinfo->var.vmode           = FB_VMODE_NONINTERLACED;
    
         // 设置LCD操作函数
            fbinfo->fbops               = &s3c2410fb_ops;
            fbinfo->flags               = FBINFO_FLAG_DEFAULT;
            fbinfo->pseudo_palette      = &info->pseudo_pal;
    
         // 清空调色板数组
            for (i = 0; i < 256; i++)
                    info->palette_buffer[i] = PALETTE_BUFF_CLEAR;
    
            ret = request_irq(irq, s3c2410fb_irq, 0, pdev->name, info);    // 设置中断
            if (ret) {
                    dev_err(&pdev->dev, "cannot get irq %d - err %d\n", irq, ret);
                    ret = -EBUSY;
                    goto release_regs;
            }
    
            // 时钟相关 
            clk_prepare_enable(info->clk);
            dprintk("got and enabled clock\n");
    
            usleep_range(1000, 1100);
    
            info->clk_rate = clk_get_rate(info->clk);   
    
            /* find maximum required memory size for display */
            for (i = 0; i < mach_info->num_displays; i++) {
                    unsigned long smem_len = mach_info->displays[i].xres;
    
                    smem_len *= mach_info->displays[i].yres;
                    smem_len *= mach_info->displays[i].bpp;
                    smem_len >>= 3;
                    if (fbinfo->fix.smem_len < smem_len)
                            fbinfo->fix.smem_len = smem_len;
            }
    
            /* Initialize video memory */
            ret = s3c2410fb_map_video_memory(fbinfo);   //为framgebuffer缓冲区动态申请内存空间,物理地址为fbinfo->fix.smem_start,虚拟地址为fbinfo->screen_base,大小为页对齐(fbinbfo->fix.smem_len)
            if (ret) {
                    dev_err(&pdev->dev, "Failed to allocate video RAM: %d\n", ret);
                    ret = -ENOMEM;
                    goto release_clock;
            }
    
            dprintk("got video memory\n");
    
            fbinfo->var.xres = display->xres;
            fbinfo->var.yres = display->yres;
            fbinfo->var.bits_per_pixel = display->bpp;
    
            s3c2410fb_init_registers(fbinfo);  // 设置GPIO,配置GPCUP、GPCCON、GPDUP、GPDCON为LCD功能  实际上就是给寄存器赋值,值来自smdk2440_fb_info中设置的值
    
            s3c2410fb_check_var(&fbinfo->var, fbinfo);
    
            ret = s3c2410fb_cpufreq_register(info);   // 根据LCD可变参数、lefrt_margin、right_margin、以及LCD控制器时钟频率(HCLK),计算LCD控制器时序参数,并设置相应控制器 s3c2410fb_calculate_tft_lcd_regs
            if (ret < 0) {
                    dev_err(&pdev->dev, "Failed to register cpufreq\n");
                    goto free_video_memory;
            }
    
            ret = register_framebuffer(fbinfo); // 注册设备
            if (ret < 0) {
                    dev_err(&pdev->dev, "Failed to register framebuffer device: %d\n",
                            ret);
                    goto free_cpufreq;
            }
    
            /* create device files */
            ret = device_create_file(&pdev->dev, &dev_attr_debug);
            if (ret)
                    dev_err(&pdev->dev, "failed to add debug attribute\n");
    
            dev_info(&pdev->dev, "fb%d: %s frame buffer device\n",
                    fbinfo->node, fbinfo->fix.id);
    
            return 0;
    
     free_cpufreq:
            s3c2410fb_cpufreq_deregister(info);
    free_video_memory:
            s3c2410fb_unmap_video_memory(fbinfo);
    release_clock:
            clk_disable_unprepare(info->clk);
            clk_put(info->clk);
    release_irq:
            free_irq(irq, info);
    release_regs:
            iounmap(info->io);
    release_mem:
            release_mem_region(res->start, size);
    dealloc_fb:
            framebuffer_release(fbinfo);
            return ret;
    }

    这段代码是在太长了,我直接挑重点说:

    • 分配一个fb_info结构体;
    • 设置fb_info:
      • 设置LCD不可变参数;
      • 设置LCD可变参数;
    • 硬件相关的操作
      • 设置中断:通过request_irq注册中断;
      • 设置framebuffer缓冲器地址:通过s3c2410fb_map_video_memory申请framebuffer显存,然后设置LCDSADDR1、LCDSADDR2、LCDSADDR3寄存器;
      • 配置引脚:通过s3c2410fb_init_registers设置GPIO端口C和GPIO端口D用于LCD;
      • 设置LCD控制器时序参数:通过s3c2410fb_cpufreq_register设置LCDCON1、LCDCON2、LCDCON3、LCDCON4、LCDCON5寄存器;
    • 注册fb_info结构体;

    需要注意的是这里并没有打开背光灯,开不开背光灯影响不大。

    6.3 s3c2410fb_cpufreq_register

    该函数主要用来初始化LCD控制器时序参数:

    static inline int s3c2410fb_cpufreq_register(struct s3c2410fb_info *info)
    {
            info->freq_transition.notifier_call = s3c2410fb_cpufreq_transition;
    
            return cpufreq_register_notifier(&info->freq_transition,
                                             CPUFREQ_TRANSITION_NOTIFIER);
    }

    定位到函数s3c2410fb_cpufreq_transition:

    static int s3c2410fb_cpufreq_transition(struct notifier_block *nb,
                                            unsigned long val, void *data)
    {
            struct s3c2410fb_info *info;
            struct fb_info *fbinfo;
            long delta_f;
    
            info = container_of(nb, struct s3c2410fb_info, freq_transition);
            fbinfo = dev_get_drvdata(info->dev);
    
            /* work out change, <0 for speed-up */
            delta_f = info->clk_rate - clk_get_rate(info->clk);
    
            if ((val == CPUFREQ_POSTCHANGE && delta_f > 0) ||
                (val == CPUFREQ_PRECHANGE && delta_f < 0)) {
                    info->clk_rate = clk_get_rate(info->clk);
                    s3c2410fb_activate_var(fbinfo);
            }
    
            return 0;
    }

    最终定位到s3c2410fb_activate_var:

    /* s3c2410fb_activate_var
     *
     * activate (set) the controller from the given framebuffer
     * information
     */
    static void s3c2410fb_activate_var(struct fb_info *info)
    {
            struct s3c2410fb_info *fbi = info->par;
            void __iomem *regs = fbi->io;
            int type = fbi->regs.lcdcon1 & S3C2410_LCDCON1_TFT;
            struct fb_var_screeninfo *var = &info->var;
            int clkdiv;
    
            clkdiv = DIV_ROUND_UP(s3c2410fb_calc_pixclk(fbi, var->pixclock), 2);  
    
            dprintk("%s: var->xres  = %d\n", __func__, var->xres);
            dprintk("%s: var->yres  = %d\n", __func__, var->yres);
            dprintk("%s: var->bpp   = %d\n", __func__, var->bits_per_pixel);
    
            if (type == S3C2410_LCDCON1_TFT) {   // 走这里 TFT真彩
                    s3c2410fb_calculate_tft_lcd_regs(info, &fbi->regs);
                    --clkdiv;
                    if (clkdiv < 0)
                            clkdiv = 0;
            } else {
                    s3c2410fb_calculate_stn_lcd_regs(info, &fbi->regs);
                    if (clkdiv < 2)
                            clkdiv = 2;
            }
    
            fbi->regs.lcdcon1 |=  S3C2410_LCDCON1_CLKVAL(clkdiv);
    
            /* write new registers */
    
            dprintk("new register set:\n");
            dprintk("lcdcon[1] = 0x%08lx\n", fbi->regs.lcdcon1);
            dprintk("lcdcon[2] = 0x%08lx\n", fbi->regs.lcdcon2);
            dprintk("lcdcon[3] = 0x%08lx\n", fbi->regs.lcdcon3);
            dprintk("lcdcon[4] = 0x%08lx\n", fbi->regs.lcdcon4);
            dprintk("lcdcon[5] = 0x%08lx\n", fbi->regs.lcdcon5);
    
            writel(fbi->regs.lcdcon1 & ~S3C2410_LCDCON1_ENVID,
                    regs + S3C2410_LCDCON1);
            writel(fbi->regs.lcdcon2, regs + S3C2410_LCDCON2);
            writel(fbi->regs.lcdcon3, regs + S3C2410_LCDCON3);
            writel(fbi->regs.lcdcon4, regs + S3C2410_LCDCON4);
            writel(fbi->regs.lcdcon5, regs + S3C2410_LCDCON5);
    
            /* set lcd address pointers */
            s3c2410fb_set_lcdaddr(info);
    
            fbi->regs.lcdcon1 |= S3C2410_LCDCON1_ENVID,    // 使能LCD
            writel(fbi->regs.lcdcon1, regs + S3C2410_LCDCON1);
    }

    这里我们简单介绍一下CLKVAL的计算方法,这里计算方式如下:

    $$clkdiv =\frac{clk\_rate * pixclock} {2^{12}*2}-1 $$

    clk_rate为HCLK频率,也就是100HMz,而 pixclock为每个像素时长,由于我们采用T35屏幕像素时钟信号$VCLK=6.4MHz$,对应每像素时长1/(6.4*10^6)*10^12=156250皮秒。

    所以:

    $$VCLK=\frac{10^{12}}{pixclock}$$

    因此可以计算出:

    $$CLKVAL=HCLK/VCLK/2−1=\frac{HCLK*pixclock}{2*10^{12}}-1$$

    6.4 s3c2410fb_calculate_tft_lcd_regs

    /* s3c2410fb_calculate_tft_lcd_regs
     *
     * calculate register values from var settings
     */
    static void s3c2410fb_calculate_tft_lcd_regs(const struct fb_info *info,
                                                 struct s3c2410fb_hw *regs)
    {
            const struct s3c2410fb_info *fbi = info->par;
            const struct fb_var_screeninfo *var = &info->var;
    
            switch (var->bits_per_pixel) {
            case 1:
                    regs->lcdcon1 |= S3C2410_LCDCON1_TFT1BPP;
                    break;
            case 2:
                    regs->lcdcon1 |= S3C2410_LCDCON1_TFT2BPP;
                    break;
            case 4:
                    regs->lcdcon1 |= S3C2410_LCDCON1_TFT4BPP;
                    break;
            case 8:
                    regs->lcdcon1 |= S3C2410_LCDCON1_TFT8BPP;
                    regs->lcdcon5 |= S3C2410_LCDCON5_BSWP |
                                     S3C2410_LCDCON5_FRM565;
                    regs->lcdcon5 &= ~S3C2410_LCDCON5_HWSWP;
                    break;
            case 16:
                    regs->lcdcon1 |= S3C2410_LCDCON1_TFT16BPP;
                    regs->lcdcon5 &= ~S3C2410_LCDCON5_BSWP;
                    regs->lcdcon5 |= S3C2410_LCDCON5_HWSWP;
                    break;
            case 32:
                    regs->lcdcon1 |= S3C2410_LCDCON1_TFT24BPP;
                    regs->lcdcon5 &= ~(S3C2410_LCDCON5_BSWP |
                                       S3C2410_LCDCON5_HWSWP |
                                       S3C2410_LCDCON5_BPP24BL);
                    break;
            default:
                    /* invalid pixel depth */
                    dev_err(fbi->dev, "invalid bpp %d\n",
                            var->bits_per_pixel);
            }
            /* update X/Y info */
            dprintk("setting vert: up=%d, low=%d, sync=%d\n",
                    var->upper_margin, var->lower_margin, var->vsync_len);
    
            dprintk("setting horz: lft=%d, rt=%d, sync=%d\n",
                    var->left_margin, var->right_margin, var->hsync_len);
    
            regs->lcdcon2 = S3C2410_LCDCON2_LINEVAL(var->yres - 1) |
                            S3C2410_LCDCON2_VBPD(var->upper_margin - 1) |
                            S3C2410_LCDCON2_VFPD(var->lower_margin - 1) |
                            S3C2410_LCDCON2_VSPW(var->vsync_len - 1);
    
            regs->lcdcon3 = S3C2410_LCDCON3_HBPD(var->right_margin - 1) |
                            S3C2410_LCDCON3_HFPD(var->left_margin - 1) |
                            S3C2410_LCDCON3_HOZVAL(var->xres - 1);
    
            regs->lcdcon4 = S3C2410_LCDCON4_HSPW(var->hsync_len - 1);
    }

    七、platform总线设备驱动(s3c2410-lcd)

    我们已经介绍了:

    • name为s3c2410-lcd的platform设备的注册;
    • name为s3c2410-lcd的platform驱动的注册;

    我们把这两部分代码整理成如下框框,方便查看:

    从这张图我们可以学习linux内核platform总线设备驱动的编写,以及如何将各部分代码进行拆分放到linux各个目录结构下。

    八、修改内核自带的LCD驱动

    8.1 修改smdk2440_lcd_cfg

    这里主要就是设置LCD时序相关参数,我们在Mini2440裸机开发之LCD基础中介绍过型号为LCD-P35(LQ035Q1DG04和ZQ3506_V0手册通用)的时序参数设置。这里我们以另外一款LCD为例,型号为LCD-T35(TD035STEB4),参考TD035STEB4 LCD数据手册上的参数性能,见下表:

    LCD 大小为240×320,16BPP数据格式,则:$$HOZVAL=240-1,LINEVAL=240-1$$
    水平同步信号的脉宽、前肩和后肩分别取10、10和20,则:$$HSPW=10-1=9,HFPD=10−1=9,HBPD=20−1=19$$
    垂直同步信号的脉宽、前肩和后肩分别取2、2和2,则:$$VSPW=2-1=1,VFPD=2−1=1,VBPD=2−1=1$$
    HCLK的频率为100MHz,要想驱动像素时钟信号为6.4MHz的LCD屏,则通过上式计算CLKVAL值:
    $$CLKVAL=HCLK/VCLK/2−1=100/6.4/2−1=6.8$$

    结果CLKVAL为6.8MHZ,取整后(值为7)放入寄存器LCDCON1中相应的位置即可。由于CLKVAL进行了取整,因此我们把取整后的值代入上式,重新计算VCLK,得到$VCLK=6.25MHz$。
    修改arch/arm/mach-s3c24xx/mach-smdk2440.c文件,定义宏:

    /*  LCD T35参数设定 */
    #define     LCD_WIDTH   240             /* LCD面板的行宽 */
    #define     LCD_HEIGHT  320             /* LCD面板的列宽 */
    
    #define     VSPW        1                /* 通过计算无效行数垂直同步脉冲宽度决定VSYNC脉冲的高电平宽度 */
    #define     VBPD        1                /* 垂直同步周期后的无效行数 */
    #define     LINEVAL     (LCD_HEIGHT-1)  /* LCD的垂直宽度-1 */
    #define     VFPD        1                /* 垂直同步周期前的的无效行数 */
    
    #define     CLKVAL      7              /* VCLK = HCLK / [(CLKVAL  + 1)  × 2] */
    
    #define     HSPW        9                /* 通过计算VCLK的数水平同步脉冲宽度决定HSYNC脉冲的高电平宽度 */
    #define     HBPD        19               /* 描述水平后沿为HSYNC的下降沿与有效数据的开始之间的VCLK周期数 */
    #define     HOZVAL      (LCD_WIDTH-1)    /* LCD的水平宽度-1 */
    #define     HFPD        9                /* 水平后沿为有效数据的结束与HSYNC的上升沿之间的VCLK周期数 */

    修改中smdk2440_lcd_cfg:

    static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = {
    
            .lcdcon5        = S3C2410_LCDCON5_FRM565 |
                              S3C2410_LCDCON5_INVVLINE |
                              S3C2410_LCDCON5_INVVFRAME |
                              S3C2410_LCDCON5_PWREN |
                              S3C2410_LCDCON5_HWSWP,
    
            .type           = S3C2410_LCDCON1_TFT,
    
            .width          = LCD_WIDTH,
            .height         = LCD_HEIGHT,
    
            .pixclock       = 156250, /* 每个像素时长,10^12/VCLK */
            .xres           = LCD_WIDTH,
            .yres           = LCD_HEIGHT,
            .bpp            = 16,
            .left_margin    = HFPD,      // HFPD
            .right_margin   = HBPD,      // HBPD   
            .hsync_len      = HSPW,      // HSPW 
            .upper_margin   = VBPD,      // VBPD
            .lower_margin   = VFPD,      // VFPD
            .vsync_len      = VSPW,      // VSPW
    };

    8.2 修改smdk2440_fb_info 

    修改arch/arm/mach-s3c24xx/mach-smdk2440.c中smdk2440_lcd_cfg:

    static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = {
            .displays       = &smdk2440_lcd_cfg,
            .num_displays   = 1,
            .default_display = 0,
    
    #if 1
            /* currently setup by downloader */
            .gpccon         = 0xaaaaaaaa,
            .gpccon_mask    = 0xffffffff,
            .gpcup          = 0xffffffff,
            .gpcup_mask     = 0xffffffff,
            .gpdcon         = 0xaaaaaaaa,
            .gpdcon_mask    = 0xffffffff,
            .gpdup          = 0xffffffff,
            .gpdup_mask     = 0xffffffff,
    #endif
    
            .lpcsel         = ((0xCE6) & ~7) | 1<<1,   // 第一位设置为1 选择输出分片率类型0:320 * 240  1:240*320
    };

    8.3 配置启动logo

    配置内核,显示启动logo:

    root@zhengyang:/work/sambashare/linux-5.2.8# make menuconfig

     Device Drivers  --->

    • Graphics support  --->
      • <*> Bootup logo
      • Frame buffer Devices
        • <*>Support for frame buffer devices --->
          •  <*> S3C2410 LCD framebuffer support
          •  <*> Samsung S3C framebuffer support

    保存文件,输入文件名s3c2440_defconfig,在当前路径下生成s3c2440_defconfig:存档:

    mv s3c2440_defconfig ./arch/arm/configs/

    8.4 编译内核

    此时重新执行:

    make distclean
    make s3c2440_defconfig    
    make uImage V=1

    将uImage复制到tftp服务器路径下:

     cp /work/sambashare/linux-5.2.8/arch/arm/boot/uImage /work/tftpboot/

    8.5 烧录内核

    开发板uboot启动完成后,内核启动前,按下任意键,进入uboot,可以通过print查看uboot中已经设置的环境变量。

    设置开发板ip地址,从而可以使用网络服务:

    SMDK2440 # set ipaddr 192.168.0.105
    SMDK2440 # save
    Saving Environment to NAND...
    Erasing NAND...
    
    Erasing at 0x40000 -- 100% complete.
    Writing to NAND... OK
    SMDK2440 # ping 192.168.0.200
    dm9000 i/o: 0x20000000, id: 0x90000a46 
    DM9000: running in 16 bit mode
    MAC: 08:00:3e:26:0a:5b
    operating at unknown: 0 mode
    Using dm9000 device
    host 192.168.0.200 is alive

    设置tftp服务器地址,也就是我们ubuntu服务器地址:

    set serverip 192.168.0.200
    save

    下载内核到内存,并写NAND FLASH:

    tftp 30000000 uImage
    nand erase.part kernel
    nand write 30000000 kernel

    运行结果如下:

    SMDK2440 # tftp 30000000 uImage
    dm9000 i/o: 0x20000000, id: 0x90000a46 
    DM9000: running in 16 bit mode
    MAC: 08:00:3e:26:0a:5b
    operating at unknown: 0 mode
    Using dm9000 device
    TFTP from server 192.168.0.200; our IP address is 192.168.0.188
    Filename 'uImage'.
    Load address: 0x30000000
    Loading: *#################################################################
         #################################################################
         #################################################################
         ##############################################################
         429.7 KiB/s
    done
    Bytes transferred = 3766128 (397770 hex)
    SMDK2440 # nand erase.part kernel
    
    NAND erase.part: device 0 offset 0x60000, size 0x400000
    
    Erasing at 0x60000 --   3% complete.
    Erasing at 0x80000 --   6% complete.
    Erasing at 0xa0000 --   9% complete.
    Erasing at 0xc0000 --  12% complete.
    Erasing at 0xe0000 --  15% complete.
    Erasing at 0x100000 --  18% complete.
    Erasing at 0x120000 --  21% complete.
    Erasing at 0x140000 --  25% complete.
    Erasing at 0x160000 --  28% complete.
    Erasing at 0x180000 --  31% complete.
    Erasing at 0x1a0000 --  34% complete.
    Erasing at 0x1c0000 --  37% complete.
    Erasing at 0x1e0000 --  40% complete.
    Erasing at 0x200000 --  43% complete.
    Erasing at 0x220000 --  46% complete.
    Erasing at 0x240000 --  50% complete.
    Erasing at 0x260000 --  53% complete.
    Erasing at 0x280000 --  56% complete.
    Erasing at 0x2a0000 --  59% complete.
    Erasing at 0x2c0000 --  62% complete.
    Erasing at 0x2e0000 --  65% complete.
    Erasing at 0x300000 --  68% complete.
    Erasing at 0x320000 --  71% complete.
    Erasing at 0x340000 --  75% complete.
    Erasing at 0x360000 --  78% complete.
    Erasing at 0x380000 --  81% complete.
    Erasing at 0x3a0000 --  84% complete.
    Erasing at 0x3c0000 --  87% complete.
    Erasing at 0x3e0000 --  90% complete.
    Erasing at 0x400000 --  93% complete.
    Erasing at 0x420000 --  96% complete.
    Erasing at 0x440000 -- 100% complete.
    OK
    SMDK2440 # nand write 30000000 kernel
    
    NAND write: device 0 offset 0x60000, size 0x400000
     4194304 bytes written: OK
    下载完成后,重启开发板,内核启动完成后会在显示屏上看到启动logo:

    在LCD参数设置过程中,为了查看寄存器设置的参数,我在drivers/video/fbdev/s3c2410fb.c中多处地方输出了寄存器的值信息,这样就可以在linux启动过程中看到寄存器的值,从而知道寄存器值有没有设置成功.

    比如在s3c24xxfb_probe函数最后加入:

      printk("lcdcon[1] = 0x%08lx\n", info->regs.lcdcon1);
      printk("lcdcon[2] = 0x%08lx\n", info->regs.lcdcon2);
      printk("lcdcon[3] = 0x%08lx\n", info->regs.lcdcon3);
      printk("lcdcon[4] = 0x%08lx\n", info->regs.lcdcon4);
      printk("lcdcon[5] = 0x%08lx\n", info->regs.lcdcon5);

    截取部分内核启动输出信息如下:

    map_video_memory: clear (ptrval):00026000
    map_video_memory: dma=339c0000 cpu=(ptrval) size=00026000
    got video memory
    gpcup     = 0xffffffff
    gpcup     = 0xaaaaaaaa
    gpcup     = 0xffffffff
    gpcup     = 0xaaaaaaaa
    gpcup     = 0xffffffff
    gpccon    = 0xaaaaaaaa
    cpdup     = 0xffffffff
    gpdcon    = 0xaaaaaaaa
    s3c2410fb_activate_var: var->xres  = 240
    s3c2410fb_activate_var: var->yres  = 320
    s3c2410fb_activate_var: var->bpp   = 16
    LCDSADDR1 = 0x19ce0000
    LCDSADDR2 = 0x19cf2c00
    LCDSADDR3 = 0x000000f0
    Console: switching to colour frame buffer device 30x40
    s3c2410-lcd s3c2410-lcd: fb0: s3c2410fb frame buffer device
    lcdcon[1] = 0x00000779
    lcdcon[2] = 0x014fc041
    lcdcon[3] = 0x0098ef09
    lcdcon[4] = 0x00000009
    lcdcon[5] = 0x00000f09 

    8.6 演示

    修改根文件系统inittab文件:

    root@zhengyang:/work/nfs_root/rootfs# cd /work/nfs_root/rootfs
    root@zhengyang:/work/nfs_root/rootfs# vim etc/inittab

    添加如下代码:

    tty1::askfirst:-/bin/sh  #重启一个sh终端,并将信息输出到tty1设备

    重新启动开发板,LCD被点亮,并有“Please press Enter to activate this console.”提示字样。

    运行如下命令:

     echo hello > /dev/tty1 

    此时在LCD可以看到由hello显示出来。

    安装linux驱动移植-输入子系统示例中介绍的案例驱动:

    [root@zy:/]# insmod button_dev.ko
    button_dev: loading out-of-tree module taints kernel.
    button driver init
    input: Unspecified device as /devices/virtual/input/input0
    register irq

    随便按下K1、K2、...,可以在LCD看到输出信息:

    此外我们可以重定位控制台到LCD设备,重新启动开发板,在uboot运行过程中按下任意键,然后设定启动参数:

    set bootargs "noinitrd console=tty1 root=/dev/nfs rw nfsroot=192.168.0.200:/work/nfs_root/rootfs ip=192.168.0.105:192.168.0.200:192.168.0.1:255.255.255.0::eth0:off"
    save

    重启开发板,此时启动输出信息就会输出到LCD显示屏上:

    参考文章

    [1]十二、Linux驱动之LCD驱动

    [2]15.linux-LCD层次分析(详解)

    [3]Linux LCD Frambuffer 基础介绍和使用(1)

    [4]Linux驱动开发 (framebuffer驱动)

    [5]Linux-FrameBuffer fb_info结构体解析申请以及注册

  • 相关阅读:
    docker镜像构建之docker commit(七)
    docker常用容器命令(五)
    docker常用镜像命令(四)
    如何查看systemctl启动服务的日志
    window server 2012 无法安装.NET framework 3.5 service pack 1
    SpringBoot第六篇-SpringBoot+Mybatis+SpringSecurity+JWT整合,开发工具IDea
    SpringBoot第五篇SpringSecurity 认证机制
    SpringBoot第四篇常见问题
    SpringBoot第三篇SpringBoo和Mybatis整合
    SpringBoot第二章拦截器
  • 原文地址:https://www.cnblogs.com/zyly/p/16146712.html
Copyright © 2020-2023  润新知