LCD驱动移植在在mini2440(linux2.6.29)和FS4412(linux3.14.78)上实现对比(deep dive)


1.Linux帧缓冲子系统

  帧缓冲(FrameBuffer)是Linux为显示设备提供的一个接口,用户可以将帧缓冲看成是显示内存的一种映像,将其映射到进程地址空间之后,就可以直接进行读写操作,而写操作可以立即反映到屏幕上,这种操作是抽象和统一的,用户不必关心显存的位置、换页机制等具体细节,这些都是由FrameBuffer设备驱动来实现,帧缓冲把显示设备描述成一个缓冲区,允许应用程序通过帧缓冲定义好的接口访问这些图形设备,从而不用关心具体的硬件细节。个人感觉,更抽象一点,帧缓冲从本质上是图形设备的硬件抽象,对于我们开发者而言,帧缓冲是一种显示缓存,向显示缓存写入特定格式的数据就意味着向屏幕输出内容,通过不断向帧缓存中写入数据,显示控制器会自动从帧缓冲中取数据并显示出来。FrameBuffer的设备文件一般是dev/fb0、dev/fb1等,最多支持32个设备,FrameBuffer是个字符设备,主设备号是29,对应于/dev/fb%d设备文件,对于我们驱动工程师而言,FrameBuffer设备和其他的文件没有区别,可以通过配置对FrameBuffer设备文件完成对硬件的参数设置,Framebuffer对应的源文件在linux/drivers/video/目录下。总的抽象设备文件为fbcon.c,在这个目录下还有与各种显卡驱动相关的源文件。

  在应用程序中,一般通过将FrameBuffer设备映射到进程地址空间的方式使用,比如下面的程序就打开/dev/fb0设备,并通过mmap系统调用进行地址映射,随后用memset将屏幕清空(这里假设显示模式是1024x768-8位色模式,线性内存模式):

  int fb;

  unsigned char* fb_mem;

  fb = open ("/dev/fb0", O_RDWR);

  fb_mem = mmap (NULL, 1024*768, PROT_READ|PROT_WRITE,MAP_SHARED,fb,0);

  memset (fb_mem, 0, 1024*768); //这个命令应该只有在root可以执行

  Framebuffer驱动实现:

  一般来说,应用程序通过内核对Framebuffer的控制,主要有以下3种方式:

  1,读/写 /dev/fb相当于读/写屏幕缓冲区;

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

  3,I/O控制对于帧缓冲设备,设备文件的ioctl()函数可读取/设置显示设备及屏幕的参数,如分辨率、显示颜色数、屏幕大小等,ioctl()函数由底层的驱动程序完成;

  Linux帧缓冲子系统的层次结构图如下所示,由帧缓冲设备层和控制器驱动组成,帧缓冲设备层在drivers/video/fbmem.c中实现,向上给应用程序提供设备文件结构操作接口,向下提供硬件操作接口,硬件操作接口需要根据具体的LCD控制器硬件来实现,这也是控制器驱动要完成的工作,对比IIC和SPI的软件层次结构,相对较为简单,帧缓冲驱动程序主要依靠四个数据结构,分别是fb_info、fb_var_screeninfo、fb_fix_screeninfo和fb_monospecs,后3个数据结构可以在用户空间访问,数据结构fb_info只能在内核空间访问。

2.LCD工作时序分析:

(1)显示指针从矩形左上角的第一行第一个点开始,一个点一个点的在LCD上显示,在上面的时序图上用时间线表示就为VCLK,我们称之为像素时钟信号;
(2) 当显示指针一直显示到矩形的右边就结束这一行,那么这一行的动作在上面的时序图中就称之为1 Line;
(3)接下来显示指针又回到矩形的左边从第二行开始显示,注意,显示指针在从第一行的右边回到第二行的左边是需要一定的时间的,我们称之为行切换;
(4)如此类推,显示指针就这样一行一行的显示至矩形的右下角才把一副图显示完成。因此,这一行一行的显示在时间线上看,就是时序图上的HSYNC;
(5)然而,LCD的显示并不是对一副图像快速的显示一下,为了持续和稳定的在LCD上显示,就需要切换到另一幅图上(另一幅图可以和上一副图一样或者不一样,目的只是为了将图像持续的显示在LCD上)。那么这一副一副的图像就称之为帧,在时序图上就表示为1 Frame,因此从时序图上可以看出1 Line只是1 Frame中的一行;
(6)同样的,在帧与帧切换之间也是需要一定的时间的,我们称之为帧切换,那么LCD整个显示的过程在时间线上看,就可表示为时序图上的VSYNC。

参照下图:VSYNC/VFRAME/STV:垂直同步信号(TFT)/帧同步信号(STN)/SEC TFT信号; HSYNC/VLINE/CPV:水平同步信号(TFT)/行同步脉冲信号(STN)/SEC TFT信号; VCLK/LCD_HCLK:象素时钟信号(TFT/STN)/SEC 参照TFT信号; VD[23:0]:LCD像素数据输出端口(TFT/STN/SEC TFT); VDEN/VM/TP:数据使能信号(TFT)/LCD驱动交流偏置信号(STN)/SEC TFT 信号; LEND/STH:行结束信号(TFT)/SEC TFT信号; LCD_LPCOE:SEC TFT OE信号; LCD_LPCREV:SEC TFT REV信号; LCD_LPCREVB:SEC TFT REVB信号;

上面时序图中上各时钟延时参数的含义如下:(这些参数的值,LCD产生厂商会提供相应的数据手册):

VBPD(vertical back porch):表示在一帧图像开始时,垂直同步信号以后的无效的行数,对应驱动中的upper_margin;
VFBD(vertical front porch):表示在一帧图像结束后,垂直同步信号以前的无效的行数,对应驱动中的lower_margin;
VSPW(vertical sync pulse width):表示垂直同步脉冲的宽度,用行数计算,对应驱动中的vsync_len;
HBPD(horizontal back porch):表示从水平同步信号开始到一行的有效数据开始之间的VCLK的个数,对应驱动中的left_margin;
HFPD(horizontal front porth):表示一行的有效数据结束到下一个水平同步信号开始之间的VCLK的个数,对应驱动中的right_margin;
HSPW(horizontal sync pulse width):表示水平同步信号的宽度,用VCLK计算,对应驱动中的hsync_len;

注意以下细节:(1)VSYNC信号有效时,表示一帧数据的开始
(2)VSPW表示VSYNC信号的脉冲宽度为(VSPW+1)个HSYNC信号周期,即(VSPW+1)行,这(VSPW+1)行的数据无效。
(3)VSYNC信号脉冲之后,还要经过(VBPD+1)个HSYNC信号周期,有效的行数据才出现。所以,在VSYNC信号有效后要经过(VSPW+1+VBPD+1)个无效的行,第一个有效行才出现,对应上边框。
(4)随后即连续发出(LINEVAL+1)行的有效数据。
(5)最后是(VFPD+1)个无效的行,它对应下边框,完整的一帧结束,紧接着就是下一帧数据了。

下面我们深入到一行中像素数据的传输过程,它与上面行数据的传输相似:
(1)HSYNC信号有效时,表示一行数据的开始
(2)HSPW表示HSYNC信号的脉冲宽度为(HSPW+1)个VCLK信号周期,即(HSPW+1)个像素,这(HSPW+1)个像素的数据无效。
(3)HSYNC信号脉冲之后,还要经过(HBPD+1)个VCLK信号周期,有效的像素数据才出现。所以,在HSYNC有效之后,总共要经过(HSPW+1+HBPD+1)个无效的像素,它对应左边框,第一个有效的像素才出现。
(4)随后即连续发出(HOZVAL+1)个像素的有效数据。
(5)最后是(HFPD+1)个无效的像素,它对应右边框,完整的一行结束

3.s3c2440和Exynos4412硬件资源分析

  s3c2440 LCD驱动采用Platform设备驱动模型实现,内核自带的LCD驱动模型代码路径是drivers/vide0/s3c2410fb.c,Exynos 4412的驱动代码里,framebuffer主要代码在driver/video/,文件名是s3c-fb.cExynos 4412显示控制器可以控制0~5个windows,代码中分给它们分别编号win0, win1,win2......,这里一个window就对应一个独立的驱动控制个体, 每个framebuffer有自己的一个FBI(fb_info)结构,显示控制器对应的抽象结构是s3c_fb结构体,window对应的抽象结构是s3c_fb_win结构体。s3c2440内部LCD控制器结构图如下,根据数据手册:

   1、LCD控制器由REGBANK、LCDCDMA、TIMEGEN、VIDPRCS寄存器组成;

    2、REGBANK由17个可编程的寄存器组和一块256*16的调色板内存组成,它们用来配置LCD控制器的;     3、LCDCDMA是一个专用的DMA,它能自动地把在侦内存中的视频数据传送到LCD驱动器,通过使用这个DMA通道,视频数据在不需要CPU的干预的情况下显示在LCD屏上;     4、VIDPRCS接收来自LCDCDMA的数据,将数据转换为合适的数据格式,比如说4/8位单扫,4位双扫显示模式,然后通过数据端口VD[23:0]传送视频数据到LCD驱动器;     5、TIMEGEN由可编程的逻辑组成,他生成LCD驱动器需要的控制信号,比如VSYNC、HSYNC、VCLK和LEND等等,而这些控制信号又与REGBANK寄存器组中的LCDCON1/2/3/4/5的配置密切相关,通过不同的配置,TIMEGEN就能产生这些信 号的不同形态,从而支持不同的LCD驱动器(即不同的STN/TFT屏)。

  Exynos4412内部集成了一个FIMD(Fully Interactive Mobile Display),在FS4412开发板上使用的是rgb接口连接外部的LCD屏,相关的电路原理图如下,

3.帧缓冲子系统中数据结构分析

  Fb_info:该结构体重要是用来描述帧缓冲设备的属性和操作的完整描述,包括了设备的设置参数,状态以及操作函数指针,每个缓冲设备都必须对应一个fb_info

/* struct fb_info 结构体 */
struct fb_info {
    int node;
    int flags;
    struct mutex lock;        /* 用于 open/release/ioctl的锁 */
    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;    /* 显示器标准 */
    struct work_struct queue;    /* Framebuffer event queue 帧缓冲事件队列 */
    struct fb_pixmap pixmap;    /* Image hardware mapper 图像硬件mapper */
    struct fb_pixmap sprite;    /* Cursor hardware mapper 光标硬件mapper */
    struct fb_cmap cmap;        /* Current cmap 目前颜色表 */
    struct list_head modelist;      /* mode list */
    struct fb_videomode *mode;    /* current mode 目前video模式  */
#ifdef 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 fb设备 */
    int class_flag;                    /* private sysfs flags */
#ifdef CONFIG_FB_TILEBLITTING
    struct fb_tile_ops *tileops;    /* Tile Blitting */
#endif
    char __iomem *screen_base;    /* Virtual address */
    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 similiar aperture base/size not
       smem_start/size as smem_start may just be an object
       allocated inside the aperture so may not actually overlap */
    resource_size_t aperture_base;
    resource_size_t aperture_size;
};

  Fb_ops:该结构体是fb_info中的成员变量,主要是用来为指向底层操作的函数的指针,fb_ops结构体中的成员函数fb_check_var是用来检查可以修改的屏幕参数并调整到最合适的值;成员函数fb_set_par是用来使得用户设置的屏幕参数可以在硬件上生效。

/* fb_ops 结构体 */
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);
    /* 对于非线性布局的/常规内存映射无法工作的帧缓冲设备*/
    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);
    /* 调整可变参数,并调整到支持的值 */
    int (*fb_check_var)(struct fb_var_screeninfo *var, struct fb_info *info);
    /* 根据info->var设置video模式 */
    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);
    /* 批量设置color寄存器,设置颜色表 */
    int (*fb_setcmap)(struct fb_cmap *cmap, struct fb_info *info);
    /* 显示空白 */
    int (*fb_blank)(int blank, struct fb_info *info);
    /* pan display */
    int (*fb_pan_display)(struct fb_var_screeninfo *var, struct fb_info *info);
    /* 矩形填充 */
    void (*fb_fillrect) (struct fb_info *info, const struct fb_fillrect *rect);
    /* 数据复制 */
    void (*fb_copyarea) (struct fb_info *info, const struct fb_copyarea *region);
    /* 图形填充 */
    void (*fb_imageblit) (struct fb_info *info, const struct fb_image *image);
    /* 绘制光标 */
    int (*fb_cursor) (struct fb_info *info, struct fb_cursor *cursor);
    /* 旋转显示 */
    void (*fb_rotate)(struct fb_info *info, int angle);
    /* 等待blit空闲(可选) */
    int (*fb_sync)(struct fb_info *info);
    /* fb特定的ioctl(可选) */
    int (*fb_ioctl)(struct fb_info *info, unsigned int cmd,unsigned long arg);
    /* 处理32位的compat ioctl(可选) */
    int (*fb_compat_ioctl)(struct fb_info *info, unsigned cmd,unsigned long arg);
    /* fb特定的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);
};

  Fb_var_screeninfo:该结构体是用来记录用户可以修改的显示控制器参数,包括屏幕分辨率和每个像素点的比特数。该结构体中的xres定义屏幕一行中有多少个点,yres是用来定义一列中有几个点,bit_per_pixel定义每个点用多少字节表示;

/* fb_var_screeninfo 结构体 */
struct fb_var_screeninfo {
    __u32 xres;            /* visible resolution  可见度解析      */
    __u32 yres;
    __u32 xres_virtual;        /* virtual resolution  虚拟度解析      */
    __u32 yres_virtual;
    __u32 xoffset;            /* offset from virtual to visible 虚拟到可见之间的偏移 */
    __u32 yoffset;            /* resolution            */
    __u32 bits_per_pixel;        /* guess what            */
    __u32 grayscale;        /* 非0时指灰度 */
    /* fb缓存的R/G/B位域 */
    struct fb_bitfield red;        /* bitfield in fb mem if true color, */
    struct fb_bitfield green;    
    struct fb_bitfield blue;
    struct fb_bitfield transp;    /* transparency 透明度           */    
    __u32 nonstd;            /* != 0 非标准像素格式 */
    __u32 activate;            /* see FB_ACTIVATE_*        */
    __u32 height;            /* height of picture in mm    */
    __u32 width;            /* width of picture in mm     */
    __u32 accel_flags;        /* (OBSOLETE) see fb_info.flags */
    /* 定时:除了pixclock本身外,其他的都以像素时钟为单位 */
    __u32 pixclock;            /* 像素时钟(皮秒) */
    __u32 left_margin;        /* 行切换:从同步到绘图之间的延迟    */
    __u32 right_margin;        /* 行切换:从绘图到同步之间的延迟    */
    __u32 upper_margin;        /* 帧切换:从同步到绘图之间的延迟    */
    __u32 lower_margin;        /* 帧切换:从绘图到同步之间的延迟    */   
    __u32 hsync_len;        /*   水平同步的长度  */
    __u32 vsync_len;        /* 垂直同步的长度  */
    __u32 sync;            /* see FB_SYNC_*        */
    __u32 vmode;            /* see FB_VMODE_*        */
    __u32 rotate;            /* 顺时针旋转的角度 */
    __u32 reserved[5];        /* Reserved for future compatibility */
};

  Fb_fix_screeninfo:该结构体是用来记录用户不能修改的显示控制器的参数,如果屏幕缓冲区的物理地址,长度。当对帧缓冲区进行映射操作时,就是从fb_fix_screeninfo中取得帧缓冲区的物理地址。

/* fb_fix_screeninfo 结构体*/
struct fb_fix_screeninfo {
    char id[16];            /* identification string eg "TT Builtin" */
    unsigned long smem_start;    /* fb缓存的开始位置(物理地址) */
    __u32 smem_len;            /* fb缓存的长度 */
    __u32 type;            /* see FB_TYPE_*        */
    __u32 type_aux;            /* Interleave for interleaved Planes */
    /* visual:记录屏幕使用的色彩模式 ①Monochrome(FB_VISUAL_MON01、FB_VISUAL_MON10) 每个像素都是黑或白
         ②Pseudo color(FB_VISUAL_PSEDOCOLOR、FB_VISUAL_STATIC_PSEUDOCOLOR), 即伪彩色,采用索引颜色显示
         ③True color(FB_VISUAL_TRUECOLOR),即彩色,分成红绿蓝三基色
         ④Direct color(FB_VISUAL_DIRECTCOLOR)每个像素偃师市也是由红绿蓝组成,不过每个颜色值是个索引,需要查表
         ⑤Grayscale displays,灰度显示,红、绿、蓝的值都一样
     */
    __u32 visual;            /* see FB_VISUAL_* 记录屏幕使用的色彩模式 */ 
    __u16 xpanstep;            /* zero if no hardware panning  */
    __u16 ypanstep;            /* zero if no hardware panning  */
    __u16 ywrapstep;        /* zero if no hardware ywrap    */
    __u32 line_length;        /* 1行的字节数    */
    unsigned long mmio_start;    /*  内存映射IO长度 */
    __u32 mmio_len;            /* 内存映射IO长度 */
    __u32 accel;            /* Indicate to driver which    */
    __u16 reserved[3];        /* Reserved for future compatibility */
};

  Fb_bitfield:该结构体是fb_var_screeninfo中的成员函数,主要是用来记录藐视每一像素显示缓冲区的组织方式,包括位域偏移,位域长度,和MSB(最高有效位)指示。

/* fb_bitfield 结构体 */
struct fb_bitfield {
    __u32 offset;            /* beginning of bitfield    */
    __u32 length;            /* length of bitfield        */
    __u32 msb_right;        /* != 0 : Most significant bit MSB在右边 */ 
};

  Fb_cmap:该结构体重要是用来记录设备无关的颜色值信息,用户空间通过ioctl()的FBIOGETMAP和FBIOSETMAP命令来读取或设定颜色值(FBI代表fb_info)

/* fb_cmap 结构体 */
struct fb_cmap {
    __u32 start;            /* First entry  第一个元素入口  */
    __u32 len;            /* Number of entries 原色数量 */
    __u16 *red;            /* Red values  RGB透明度  */
    __u16 *green;
    __u16 *blue;
    __u16 *transp;            /* transparency, can be NULL */
};

3.帧缓冲字符设备接口

  操作帧缓冲设备的字符设备接口在fbmem.c中实现,下面主要分析fb_write、fb_mmap和fb_ioctl方法,帧缓冲设备的mmap()操作函数比较重要,顾名思义其任务是完成设备到系统内存(虚拟地址)之间的映射,在多数情况下,访问帧缓冲设备不是通过其读写方法,而是通过mmap()系统调用将帧缓冲映射到用户空间直接访问,这样做不仅可以省去一次用户空间与内和空间的数据复制,而且操作起来也更加方便快捷,这里的映射行为发生在驱动程序初始化阶段。请注意,这些行为是否被囊括在一个名为mmap的函数中并不是问题的关键,我们在开发中真正应该关心的是映射所需要的前提条件如设备物理地址的提取,映射长度的确认以及实际的映射操作。只要在驱动程序的初始化中完成了上述动作,那就算是成功了。因此,不少显卡的驱动程序里是找不到mmap这个函数的,但它们一样工作得很好,原因就是它们已经完成了实际的映射操作。

4.帧缓冲驱动实现

  针对platform驱动模型,帧缓冲驱动需要完成的核心任务包括两点,首先实现镇缓冲设备操作集合fb_ops,然后分配fb_info结构、填充其成员,并调用register_framebuffer()向系统注册;帧缓冲设备作为平台设备:在S3C2440中,LCD控制器是片上资源,采用了platform驱动模型,在drivers/video/s3c2410fb.c中,模块初始化函数s3c2410fb_init()试图对两个platform驱动注册,名称分别时“s3c2410-lcd”和“s3c2412-lcd”,相应的平台设备在/arch/arm/plat-s3c24xx/devs.c中定义,初始名称是"s3c2410-lcd",由于S3C2440和S3C2410的LCD驱动器相同,因此只要在机器配置文件中将相应的平台设备注册进内核,适合于S3C2440 LCD控制器的平台驱动“s3c2410-lcd”就能顺利加载。除此之外,Linux还在/arch/arm/mach-s3c2410/include/mach/fb.h中为LCD平台设备定义了一个 s3c2410fb_mach_info结构体,该结构体主要是记录LCD的硬件参数信息(比如该结构体的s3c2410fb_display成员结构中就用于记录LCD的屏幕尺寸、屏幕信息、可变的屏幕参数、LCD配置寄存器等),这样在写驱动的时候就直接使用这个结构体,针对EXYNOS4412,Linux,在/arch/arm/mach-exynos/include/mach/s3cfb.h中为LCD平台设备定义了一个s3cfb_lcd结构体,该结构体主要是记录LCD的硬件参数信息(比如LCD的屏幕尺寸、 屏幕信息、 可变的屏幕参数、 LCD配置寄存器等),这样在写驱动的时候就直接使用这个结构体,对比可知,两者的驱动架构是相同的。

  具体移植步骤,首先在板级初始化文件中添加对帧缓冲的支持,需要在板级初始化文件中为帧缓冲的平台设备类型为s3c2410fb_mach_info的平台数据,并向系统中添加该平台设备,而针对EXYNOS4412通过设备树来加载初始化硬件信息,以后汇详细讲解,下面第一图是开发板原理图的LCD控制器部分,第二图是S3c2440数据手册中IO端口C和IO端口D控制器部分。原理图中使用了 GPC8-15和GPD0-15来用做LCD控制器VD0-VD23的数据端口,又分别使用GPC0、GPC1端口用做LCD控制器的LEND和VCLK 信号,对于GPC2-7则是用做STN屏或者三星专业TFT屏的相关信号。然而,S3C2440的各个IO口并不是单一的功能,都是复用端口,要使用他们 首先要对他们进行配置。所以代码中加粗部分的参数就是把GPC和GPD的部分端口配置成LCD控制功能模式。因此需要在mach-smdk2410.c中添加如下内容:

/* LCD driver info */
#if defined(CONFIG_FB_S3C2410_N240320)
#define LCD_WIDTH 240
#define LCD_HEIGHT 320
#define LCD_PIXCLOCK 100000
#define LCD_RIGHT_MARGIN 36
#define LCD_LEFT_MARGIN 19
#define LCD_HSYNC_LEN 5
#define LCD_UPPER_MARGIN 1
#define LCD_LOWER_MARGIN 5
#define LCD_VSYNC_LEN 1
#elif defined(CONFIG_FB_S3C2410_W320240)
#define LCD_WIDTH 320
#define LCD_HEIGHT 240
#define LCD_PIXCLOCK 170000
#define LCD_RIGHT_MARGIN 0x44
#define LCD_LEFT_MARGIN 0x04
#define LCD_HSYNC_LEN 0x01
#define LCD_UPPER_MARGIN 10
#define LCD_LOWER_MARGIN 4
#define LCD_VSYNC_LEN 1
#define LCD_CON5 (S3C2410_LCDCON5_FRM565 | S3C2410_LCDCON5_INVVFRAME | \
                  S3C2410_LCDCON5_INVVLINE | S3C2410_LCDCON5_HWSWP ) 
#elif defined(CONFIG_FB_S3C2410_N480272)
#define LCD_WIDTH 480
#define LCD_HEIGHT 272
#define LCD_PIXCLOCK 100000
#define LCD_RIGHT_MARGIN 36
#define LCD_LEFT_MARGIN 19
#define LCD_HSYNC_LEN 5
#define LCD_UPPER_MARGIN 1
#define LCD_LOWER_MARGIN 5
#define LCD_VSYNC_LEN 1
#elif defined(CONFIG_FB_S3C2410_TFT640480)
#define LCD_WIDTH 640
#define LCD_HEIGHT 480
#define LCD_PIXCLOCK 40000
#define LCD_RIGHT_MARGIN 67 
#define LCD_LEFT_MARGIN 40
#define LCD_HSYNC_LEN 31
#define LCD_UPPER_MARGIN 5
#define LCD_LOWER_MARGIN 25
#define LCD_VSYNC_LEN 1
#elif defined(CONFIG_FB_S3C2410_T240320)
#define LCD_WIDTH 240
#define LCD_HEIGHT 320
#define LCD_PIXCLOCK 170000
#define LCD_RIGHT_MARGIN 25
#define LCD_LEFT_MARGIN 0
#define LCD_HSYNC_LEN 4
#define LCD_UPPER_MARGIN 1
#define LCD_LOWER_MARGIN 4
#define LCD_VSYNC_LEN 1
#define LCD_CON5 (S3C2410_LCDCON5_FRM565 | S3C2410_LCDCON5_INVVDEN | \
                  S3C2410_LCDCON5_INVVFRAME | S3C2410_LCDCON5_INVVLINE | \
                  S3C2410_LCDCON5_INVVCLK | S3C2410_LCDCON5_HWSWP ) 
#elif defined(CONFIG_FB_S3C2410_X240320)
#define LCD_WIDTH 240
#define LCD_HEIGHT 320
#define LCD_PIXCLOCK 170000
#define LCD_RIGHT_MARGIN 25
#define LCD_LEFT_MARGIN 0
#define LCD_HSYNC_LEN 4
#define LCD_UPPER_MARGIN 0
#define LCD_LOWER_MARGIN 4
#define LCD_VSYNC_LEN 9
#define LCD_CON5 (S3C2410_LCDCON5_FRM565 | S3C2410_LCDCON5_INVVDEN | \
                  S3C2410_LCDCON5_INVVFRAME | S3C2410_LCDCON5_INVVLINE | \
                  S3C2410_LCDCON5_INVVCLK | S3C2410_LCDCON5_HWSWP ) 
#elif defined(CONFIG_FB_S3C2410_TFT800480)
#define LCD_WIDTH 800
#define LCD_HEIGHT 480
#define LCD_PIXCLOCK 40000
#define LCD_RIGHT_MARGIN 67
#define LCD_LEFT_MARGIN 40
#define LCD_HSYNC_LEN 31
#define LCD_UPPER_MARGIN 25
#define LCD_LOWER_MARGIN 5
#define LCD_VSYNC_LEN 1
#elif defined(CONFIG_FB_S3C2410_VGA1024768)
#define LCD_WIDTH 1024
#define LCD_HEIGHT 768
#define LCD_PIXCLOCK 80000
#define LCD_RIGHT_MARGIN 15
#define LCD_LEFT_MARGIN 199
#define LCD_HSYNC_LEN 15
#define LCD_UPPER_MARGIN 1
#define LCD_LOWER_MARGIN 1
#define LCD_VSYNC_LEN 1
#define LCD_CON5 (S3C2410_LCDCON5_FRM565 | S3C2410_LCDCON5_HWSWP)
#endif
#if defined (LCD_WIDTH)
static struct s3c2410fb_display My2440_lcd_cfg __initdata = {
#if !defined (LCD_CON5)
    .lcdcon5    = S3C2410_LCDCON5_FRM565 |
              S3C2410_LCDCON5_INVVLINE |
              S3C2410_LCDCON5_INVVFRAME |
              S3C2410_LCDCON5_PWREN |
              S3C2410_LCDCON5_HWSWP,
#else
    .lcdcon5    = LCD_CON5,
#endif
    .type        = S3C2410_LCDCON1_TFT,
    .width        = LCD_WIDTH,
    .height        = LCD_HEIGHT,
    .pixclock    = LCD_PIXCLOCK, /* HCLK 60 MHz, divisor 10 */
    .xres        = LCD_WIDTH,
    .yres        = LCD_HEIGHT,
    .bpp        = 16,
    .left_margin    = LCD_LEFT_MARGIN + 1,
    .right_margin    = LCD_RIGHT_MARGIN + 1,
    .hsync_len    = LCD_HSYNC_LEN + 1,
    .upper_margin    = LCD_UPPER_MARGIN + 1,
    .lower_margin    = LCD_LOWER_MARGIN + 1,
    .vsync_len    = LCD_VSYNC_LEN + 1,
};
#define S3C2410_GPCCON_MASK(x)    (3 << ((x) * 2))
#define S3C2410_GPDCON_MASK(x)    (3 << ((x) * 2))
static struct s3c2410fb_mach_info My2440_fb_info __initdata = {
    .displays    = &My2440_lcd_cfg,
    .num_displays    = 1,
    .default_display = 0,
    .gpcup        = (0xf << 1) | (0x3f << 10),
    .gpccon        = (S3C2410_GPC1_VCLK   | S3C2410_GPC2_VLINE |
               S3C2410_GPC3_VFRAME | S3C2410_GPC4_VM |
               S3C2410_GPC10_VD2   | S3C2410_GPC11_VD3 |
               S3C2410_GPC12_VD4   | S3C2410_GPC13_VD5 |
               S3C2410_GPC14_VD6   | S3C2410_GPC15_VD7),
    .gpccon_mask    = (S3C2410_GPCCON_MASK(1)  | S3C2410_GPCCON_MASK(2)  |
               S3C2410_GPCCON_MASK(3)  | S3C2410_GPCCON_MASK(4)  |
               S3C2410_GPCCON_MASK(10) | S3C2410_GPCCON_MASK(11) |
               S3C2410_GPCCON_MASK(12) | S3C2410_GPCCON_MASK(13) |
               S3C2410_GPCCON_MASK(14) | S3C2410_GPCCON_MASK(15)),
    .gpdup        = (0x3f << 2) | (0x3f << 10),
    .gpdcon        = (S3C2410_GPD2_VD10  | S3C2410_GPD3_VD11 |
               S3C2410_GPD4_VD12  | S3C2410_GPD5_VD13 |
               S3C2410_GPD6_VD14  | S3C2410_GPD7_VD15 |
               S3C2410_GPD10_VD18 | S3C2410_GPD11_VD19 |
               S3C2410_GPD12_VD20 | S3C2410_GPD13_VD21 |
               S3C2410_GPD14_VD22 | S3C2410_GPD15_VD23),
    .gpdcon_mask    = (S3C2410_GPDCON_MASK(2)  | S3C2410_GPDCON_MASK(3) |
               S3C2410_GPDCON_MASK(4)  | S3C2410_GPDCON_MASK(5) |
               S3C2410_GPDCON_MASK(6)  | S3C2410_GPDCON_MASK(7) |
               S3C2410_GPDCON_MASK(10) | S3C2410_GPDCON_MASK(11)|
               S3C2410_GPDCON_MASK(12) | S3C2410_GPDCON_MASK(13)|
               S3C2410_GPDCON_MASK(14) | S3C2410_GPDCON_MASK(15)),
};

#endif

static void __init My2440_machine_init(void)
{
s3c24xx_fb_set_platdata(&My2440_fb_info);
s3c_i2c0_set_platdata(NULL);
i2c_register_board_info(0, i2c_devices, ARRAY_SIZE(i2c_devices));
s3c_device_spi0.dev.platform_data= &s3c2410_spi0_platdata;
spi_register_board_info(s3c2410_spi0_board, ARRAY_SIZE(s3c2410_spi0_board));
s3c_device_spi1.dev.platform_data= &s3c2410_spi1_platdata;
spi_register_board_info(s3c2410_spi1_board, ARRAY_SIZE(s3c2410_spi1_board));
s3c_device_nand.dev.platform_data = &My2440_nand_info;
s3c_device_sdi.dev.platform_data = &My2440_mmc_cfg;

platform_add_devices(My2440_devices, ARRAY_SIZE(My2440_devices));
}

  对于驱动使用platform模型的编写,probe函数是平台设备和平台驱动匹配上后第一个调用的函数,它主要完成工作如下:

  1、分配核心结构体struct fb_info空间和私有数据空间;

  2、获取平台资源和平台私有数据,把获取到的信息保存到struct fb_info结构的fb_var_screeninfo var和struct fb_fix_screeninfo fix结构空间中,并保存一些必要的设备私有数据,对于内存资源,获取后要进行申请-映射为虚拟地址-使用映射后的虚拟地址对硬件寄存器初始化,对于中断资源,则进行对中断资源函数的注册;

  3、struct fb_ops结构是操作硬件的函数,所以这部分是在probe函数外部实现,但是把已经实现好的struct fb_ops *fops结构填充到struct fb_info结构中也是在探测函数中实现的;

  4、在探测函数中使用register_framebuffer函数注册已经填充好的struct fb_info结构,详尽代码如下:

注意,s3c2440调用的是s3c24xxfb_probe(),而Exynos4412通过调用s3cfb_probe()来实现;

static int s3c24xxfb_probe(struct platform_device *pdev,
               enum s3c_drv_type drv_type)
{
    struct s3c2410fb_info *info;
    struct s3c2410fb_display *display;  //LCD屏的配置信息结构体,该结构体在mach-s3c2410/include/mach/fb.h中
    struct fb_info *fbinfo;  //FrameBuffer驱动所对应的fb_info结构体
    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); //获取硬件平台信息
    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) {
        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;
    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);
    if (!fbinfo)
        return -ENOMEM;
    platform_set_drvdata(pdev, fbinfo);
    info = fbinfo->par;
    info->dev = &pdev->dev;  //把全局的平台设备变量pdev中的dev成员存放到私有数据成员中
    info->drv_type = drv_type;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (res == NULL) {
        dev_err(&pdev->dev, "failed to get memory registers\n");
        ret = -ENXIO;
        goto dealloc_fb;
    }
    size = resource_size(res);
    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);  //映射内存为虚拟地址空间
    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);
    /* Stop the video */
    lcdcon1 = readl(info->io + S3C2410_LCDCON1);
    writel(lcdcon1 & ~S3C2410_LCDCON1_ENVID, info->io + S3C2410_LCDCON1);
    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;
    fbinfo->var.nonstd        = 0;
    fbinfo->var.activate        = FB_ACTIVATE_NOW;
    fbinfo->var.accel_flags     = 0;
    fbinfo->var.vmode        = FB_VMODE_NONINTERLACED;
    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;
    }
    info->clk = clk_get(NULL, "lcd");
    if (IS_ERR(info->clk)) {
        dev_err(&pdev->dev, "failed to get lcd clock source\n");
        ret = PTR_ERR(info->clk);
        goto release_irq;
    }
    clk_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);
    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);
    s3c2410fb_check_var(&fbinfo->var, fbinfo);
    ret = s3c2410fb_cpufreq_register(info);
    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(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;
}

5.修改Makefile和Kconfig,并在内核中添加对帧缓冲驱动的支持

  首先,drivers/video/Kconfig中添加下面配置:

choice
    prompt "LCD select"
    depends on FB_S3C2410
    help
       S3C24x0 LCD size select

config FB_S3C2410_W320240
    boolean "3.5 inch 320X240 TFT Landscape LCD"
    depends on FB_S3C2410
    help
      3.5 inch 320X240 TFT Landscape LCD

config FB_S3C2410_T240320
    boolean "3.5 inch 240X320 Toppoly LCD"
    depends on FB_S3C2410
    help
      3.5 inch 240X320 Toppoly LCD

config FB_S3C2410_X240320
    boolean "3.5 inch 240X320 LCD(ACX502BMU)"
    depends on FB_S3C2410
    help
      3.5 inch 240X320 LCD(ACX502BMU)

config FB_S3C2410_N240320
    boolean "3.5 inch 240X320 NEC LCD"
    depends on FB_S3C2410
    help
       3.5 inch 240x320 NEC LCD

config FB_S3C2410_N480272
    boolean "4.3 inch 480X272 NEC LCD"
    depends on FB_S3C2410
    help
       4.3 inch 480x272 NEC LCD

config FB_S3C2410_TFT640480
    boolean "8 inch 640X480 L80 LCD"
    depends on FB_S3C2410
    help
       8 inch 640X480 LCD

config FB_S3C2410_TFT800480
    boolean "7 inch 800x480 TFT LCD"
    depends on FB_S3C2410
    help
       7 inch 800x480 TFT LCD

config FB_S3C2410_VGA1024768
    boolean "VGA 1024x768"
    depends on FB_S3C2410
    help
       VGA 1024x768

endchoice

config BACKLIGHT_MY2440
    tristate "Backlight support for My2440"
    depends on MACH_MY2440 && FB_S3C2410
    help
      backlight driver for MY2440

  其次,make menuconfig, 见下图,加进所需要配置的选项:

6.帧缓冲驱动测试

  针对2440,arm-linux-gcc bmplib.c fbtest.c -o test , 然后./test logo.bmp,将在显示屏上看到企鹅图像。

/*bmplib.c*/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include "bmplib.h"
int bmp_open(bmp_t *bmp, char *bmpn)
{
    bmp_file_header_t fhr;
    bmp_info_header_t ihr;    
    if (-1 == (bmp->fd=open(bmpn, O_RDONLY))) {
        printf("Error: cannot open bmp file.\n");
        _exit(EXIT_FAILURE);
    }    
    read(bmp->fd, &fhr, sizeof(bmp_file_header_t));
    read(bmp->fd, &ihr, sizeof(bmp_info_header_t));
    bmp->width  = char_to_int(ihr.width);
    bmp->height = char_to_int(ihr.height);
    bmp->bitcount = char_to_int(ihr.bitcount); 
    bmp->siz = (bmp->width * bmp->height * bmp->bitcount)/8; 
    printf("bmp->width = %d\n", bmp->width);
    printf("bmp->height = %d\n", bmp->height);
    printf("bmp->bitcount = %d\n", bmp->bitcount);
    printf("bmp->siz = %d\n", bmp->siz);
    bmp->data = malloc(bmp->siz);
    int ret;
    ret = read(bmp->fd, bmp->data, bmp->siz);
    bmp->curp = (rgb_32_t*)bmp->data;    
    return 0;
}
int bmp_close(bmp_t *bmp)
{
    close(bmp->fd);
    free(bmp->data);
}
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include "bmplib.h"
#define FB_DEV_NAME    "/dev/fb0"
#define RED_COLOR565    0x0F100
#define GREEN_COLOR565  0x007E0
#define BLUE_COLOR565   0x0001F
#define BLACK_COLOR565  0x0
typedef struct fb_dev {
    int fd;
    void *pfb;
    int xres, yres, siz;
    int bpp;
} fb_dev_t;
int fb_open(fb_dev_t *fbd, char *fbn)
{
    struct fb_var_screeninfo vinfo;
    if (-1 == (fbd->fd=open(fbn, O_RDWR))) {
        perror("open fb.");
        _exit(EXIT_FAILURE);
    }
    // Get variable screen information
    ioctl(fbd->fd, FBIOGET_VSCREENINFO, &vinfo);   
    fbd->xres = vinfo.xres;
    fbd->yres = vinfo.yres;
    fbd->bpp  = vinfo.bits_per_pixel;
    printf("vinfo.red.offset = %d, vinfo.red.length = %d\n", vinfo.red.offset, vinfo.red.length);
    printf("vinfo.green.offset = %d, vinfo.green.length = %d\n", vinfo.green.offset, vinfo.green.length);
    printf("vinfo.blue.offset = %d, vinfo.blue.length = %d\n", vinfo.blue.offset, vinfo.blue.length);
    if (fbd->bpp != 16) {
        ioctl(fbd->fd, FBIOPUT_VSCREENINFO, &vinfo);
        fbd->bpp  = vinfo.bits_per_pixel;
    }
    // Figure out the size of the screen in bytes
    fbd->siz = fbd->xres * fbd->yres * fbd->bpp / 8;
    printf("%dx%d, %dbpp, screensize = %d\n", fbd->xres, fbd->yres,
            fbd->bpp, fbd->siz );
    // Map the device to memory
    fbd->pfb = mmap(0, fbd->siz, PROT_READ | PROT_WRITE, MAP_SHARED, fbd->fd, 0);
    if ((int)fbd->pfb == -1) {
        printf("Error: failed to map framebuffer device to memory.\n");
        _exit(EXIT_FAILURE);
    }
    printf("fbd->pfb = %p\n", fbd->pfb);
    return 0;
}
int fb_close(fb_dev_t *fbd)
{
    munmap(fbd->pfb, fbd->siz);
    close(fbd->fd);
}
int fb_drawrect(fb_dev_t *fbd, int x0, int y0, int w, int h, int color)
{
    int x, y;
    for(y = y0; y < y0+h;  y++) {
        for(x = x0; x < x0+w ; x++) 
            *((short*)(fbd->pfb) + y*fbd->xres + x) = color;
    }
    return 0;
}
int fb_drawbmp(fb_dev_t *fbd, int x0, int y0, char *bmpn)
{
    int x, y, x1, y1;
    bmp_t bmp;

    bmp_open(&bmp, bmpn);
    if (x0 < 0) 
        x0 = (fbd->xres - bmp.width) / 2;
    if (y0 < 0) 
        y0 = (fbd->yres - bmp.height) / 2;
    
    x1 = x0+bmp.width;
    y1 = y0+bmp.height;

    for(y = y1; y > y0;  y--) {
        for(x = x0; x < x1; x++) {
            if (x > fbd->xres || y > fbd->yres) {
                bmp_next_pixel(&bmp);
                continue;
            }
            *((short*)(fbd->pfb) + y*fbd->xres + x) = bmp_get_pixel_16bit(&bmp);
            bmp_next_pixel(&bmp);
        }
    }
    bmp_close(&bmp);    
    return 0;
}
int main(int argc, char **argv)
{
    fb_dev_t *fbd;
    fbd = (fb_dev_t*)malloc(sizeof(fb_dev_t));

    fb_open(fbd, FB_DEV_NAME);
        
    if(fbd->bpp == 16) {
        printf("Red/Green/Blue Screen\n");
        fb_drawrect(fbd, 0, 0, fbd->xres, fbd->yres/3, RED_COLOR565);
        fb_drawrect(fbd, 0, fbd->yres/3, fbd->xres, fbd->yres/3, GREEN_COLOR565);
        fb_drawrect(fbd, 0, fbd->yres*2/3, fbd->xres, fbd->yres/3, BLUE_COLOR565);
        //fb_drawrect(fbd, 0, 0, fbd->xres, fbd->yres, BLACK_COLOR565);
        
        //fb_drawbmp(fbd, 0, 0, argv[1]);
        fb_drawbmp(fbd, -1, -1, argv[1]);
    } else
        printf("16 bits only, (bpp = %d)!\n", fbd->bpp);
    fb_close(fbd);    
    return 0;
}

参考文献1.《深入Linux内核架构》 2.《Linux设备驱动开发详解》

优质内容筛选与推荐>>
1、优秀测试博主
2、HDU3157:Crazy Circuits——题解
3、.Net软件测试化之道 [James D.MCCaffrey]
4、CF1100E Andrew and Taxi 二分答案+拓扑排序
5、字符串训练之一


长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号





    联系我们

    欢迎来到TinyMind。

    关于TinyMind的内容或商务合作、网站建议,举报不良信息等均可联系我们。

    TinyMind客服邮箱:support@tinymind.net.cn