当前位置: 首页 > news >正文

石家庄建站工具网页设计基础教学设计及ppt

石家庄建站工具,网页设计基础教学设计及ppt,扁平化蓝色网站模板,旅游网站开发近五年参考文献目录前言字符设备驱动简介内核驱动操作函数集合(file_operations结构体)字符设备驱动开发步骤.ko驱动模块的加载和卸载(module_init驱动入口、insmod驱动加载)字符设备注册与注销到内核register_chrdev(设备号、设备名) -- 很少用了实现设备的具体操作函数添加LICENSE 和作者信… 目录前言字符设备驱动简介内核驱动操作函数集合(file_operations结构体)字符设备驱动开发步骤.ko驱动模块的加载和卸载(module_init驱动入口、insmod驱动加载)字符设备注册与注销到内核register_chrdev(设备号、设备名) -- 很少用了实现设备的具体操作函数添加LICENSE 和作者信息创建应用层设备节点文件(mknod应用程序通过设备节点操作具体设备)Linux 设备号设备号的组成(主、次设备号)设备号的分配chrdevbase字符设备驱动实验程序编写1、创建VSCode 工程2、VSCode添加头文件路径(重要)3、编写驱动程序(传递函数copy_to_user、copy_from_user)编写测试APP(1表示向驱动读数据、2表示向驱动写数据)Makefile编译驱动程序和测试APP1、编译驱动程序2、编译测试APP运行测试1、设置内核启动、复制.ko和app到指定目录、加载驱动模块2、创建设备节点文件(mknod应用程序通过设备节点操作具体设备)3、chrdevbase设备操作测试4、卸载驱动模块前言 【注意】驱动开发不要用正点原子提供的uboot和linux内核以及根文件系统因为里面所有驱动都写好了我们要使用恩智浦提供的。 Linux 中的三大类驱动字符设备驱动、块设备驱动和网络设备驱动。 字符设备驱动是占用篇幅最大的一类驱动因为字符设备最多从最简单的点灯到I2C、SPI、音频等都属于字符设备驱动的类型。 块设备和网络设备驱动要比字符设备驱动复杂就是因为其复杂所以半导体厂商一般都给我们编写好了大多数情况下都是直接可以使用的。 所谓的块设备驱动就是存储器设备的驱动比如EMMC、NAND、SD 卡和U 盘等存储设备因为这些存储设备的特点是以存储块为基础因此叫做块设备。 网络设备驱动就更好理解了就是网络驱动不管是有线的还是无线的都属于网络设备驱动的范畴。 一个设备可以属于多种设备驱动类型比如USB WIFI其使用USB 接口所以属于字符设备但是其又能上网所以也属于网络设备驱动。 本书使用的Linux 内核版本为4.1.15其支持设备树(Device tree)所以本篇所有例程均采用设备树。 本章会以一个虚拟的设备为例讲解如何进行字符设备驱动开发以及如何编写测试APP 来测试驱动工作是否正常为以后的学习打下坚实的基础。 字符设备驱动简介 字符设备是Linux 驱动中最基本的一类设备驱动字符设备就是一个一个字节按照字节流进行读写操作的设备读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPILCD 等等都是字符设备。 我们先来简单的了解一下Linux 下的应用程序是如何调用驱动程序的Linux 应用程序对驱动程序的调用如图40.1.1 所示 在Linux 中一切皆为文件驱动加载成功以后会在“/dev”目录下生成一个相应的文件应用程序通过对这个名为“/dev/xxx”(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。应用程序使用open 函数来打开文件/dev/led使用完成以后使用close 函数关闭/dev/led 这个文件。open 和close 就是打开和关闭led 驱动的函数如果要获取led 灯的状态就用read 函数从驱动中读取相应的状态。 应用程序运行在用户空间而Linux 驱动属于内核的一部分因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作比如使用open 函数打开/dev/led 这个驱动因为用户空间不能直接对内核进行操作因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间这样才能实现对底层驱动的操作。open、close、write 和read 等这些函数是由C 库提供的在Linux 系统中系统调用作为C 库的一部分。当我们调用open 函数的时候流程如图40.1.2 所示 其中关于C 库以及如何通过系统调用“陷入”到内核空间这个我们不用去管我们重点关注的是应用程序和具体的驱动应用程序使用到的函数在具体驱动程序中都有与之对应的函数比如应用程序中调用了open 这个函数那么在驱动程序中也得有一个名为open 的函数。每一个系统调用在驱动中都有与之对应的一个驱动函数在Linux内核文件include/linux/fs.h 中有个叫做file_operations 的结构体此结构体就是Linux 内核驱动操作函数集合内容如下所示 内核驱动操作函数集合(file_operations结构体) 1588 struct file_operations { 1589 struct module *owner; 1590 loff_t (*llseek) (struct file *, loff_t, int); 1591 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 1592 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); 1593 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); 1594 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); 1595 int (*iterate) (struct file *, struct dir_context *); 1596 unsigned int (*poll) (struct file *, struct poll_table_struct *); 1597 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); 1598 long (*compat_ioctl) (struct file *, unsigned int, unsigned long); 1599 int (*mmap) (struct file *, struct vm_area_struct *); 1600 int (*mremap)(struct file *, struct vm_area_struct *); 1601 int (*open) (struct inode *, struct file *); 1602 int (*flush) (struct file *, fl_owner_t id); 1603 int (*release) (struct inode *, struct file *); 1604 int (*fsync) (struct file *, loff_t, loff_t, int datasync); 1605 int (*aio_fsync) (struct kiocb *, int datasync); 1606 int (*fasync) (int, struct file *, int); 1607 int (*lock) (struct file *, int, struct file_lock *); 1608 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); 1609 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); 1610 int (*check_flags)(int); 1611 int (*flock) (struct file *, int, struct file_lock *); 1612 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); 1613 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); 1614 int (*setlease)(struct file *, long, struct file_lock **, void **); 1615 long (*fallocate)(struct file *file, int mode, loff_t offset, 1616 loff_t len); 1617 void (*show_fdinfo)(struct seq_file *m, struct file *f); 1618 #ifndef CONFIG_MMU 1619 unsigned (*mmap_capabilities)(struct file *); 1620 #endif 1621 };简单介绍一下file_operation 结构体中比较重要的、常用的函数 第1589 行owner 拥有该结构体的模块的指针一般设置为THIS_MODULE。第1590 行llseek 函数用于修改文件当前的读写位置。第1591 行read 函数用于读取设备文件。第1592 行write 函数用于向设备文件写入(发送)数据。第1596 行poll 是个轮询函数用于查询设备是否可以进行非阻塞的读写。第1597 行unlocked_ioctl 函数提供对于设备的控制功能与应用程序中的ioctl 函数对应。第1598 行compat_ioctl 函数与unlocked_ioctl 函数功能一样区别在于在64 位系统上32 位的应用程序调用将会使用此函数。在32 位的系统上运行32 位的应用程序调用的是unlocked_ioctl。第1599 行mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间)一般帧缓冲设备会使用此函数比如LCD 驱动的显存将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了这样就不用在用户空间和内核空间之间来回复制。第1601 行open 函数用于打开设备文件。第1603 行release 函数用于释放(关闭)设备文件与应用程序中的close 函数对应。第1604 行fasync 函数用于刷新待处理的数据用于将缓冲区中的数据刷新到磁盘中。第1605 行aio_fsync 函数与fasync 函数的功能类似只是aio_fsync 是异步刷新待处理的数据。 在字符设备驱动开发中最常用的就是上面这些函数我们在字符设备驱动开发中最主要的工作就是实现上面这些函数不一定全部都要实现但是像open、release、write、read 等都是需要实现的。 字符设备驱动开发步骤 .ko驱动模块的加载和卸载(module_init驱动入口、insmod驱动加载) Linux 驱动有两种运行方式 【编译进内核】第一种就是将驱动编译进Linux 内核中这样当Linux 内核启动的时候就会自动运行驱动程序【编译成.ko模块】第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko)在Linux 内核启动以后使用“insmod”命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块这样我们修改驱动以后只需要编译一下驱动代码即可不需要编译烧写整个Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可不需要重启整个系统。 总之将驱动编译为模块最大的好处就是方便开发当驱动开发完成确定没有问题以后就可以将驱动编译进Linux内核中当然也可以不编译进Linux 内核中具体看自己的需求。 模块有加载和卸载两种操作我们在编写驱动的时候需要注册这两种操作函数模块的加载和卸载注册函数如下 module_init(xxx_init); //注册模块加载函数 module_exit(xxx_exit); //注册模块卸载函数module_init 函数用来向Linux 内核注册一个模块加载函数参数xxx_init 就是需要注册的具体函数当使用“insmod”命令加载驱动的时候xxx_init 这个函数就会被调用。module_exit()函数用来向Linux 内核注册一个模块卸载函数参数xxx_exit 就是需要注册的具体函数当使用“rmmod”命令卸载具体驱动的时候xxx_exit 函数就会被调用。 字符设备驱动模块加载和卸载模板如下所示 1 /* 驱动入口函数*/ 2 static int __init xxx_init(void) 3 { 4 /* 入口函数具体内容*/ 5 return 0; 6 } 7 8 /* 驱动出口函数*/ 9 static void __exit xxx_exit(void) 10 { 11 /* 出口函数具体内容*/ 12 } 13 14 /* 将上面两个函数指定为驱动的入口和出口函数*/ 15 module_init(xxx_init); 16 module_exit(xxx_exit);第2 行定义了个名为xxx_init 的驱动入口函数并且使用了“__init”来修饰。 第9 行定义了个名为xxx_exit 的驱动出口函数并且使用了“__exit”来修饰。 第15 行调用函数module_init 来声明xxx_init 为驱动入口函数当加载驱动的时候xxx_init函数就会被调用。 第16 行调用函数module_exit 来声明xxx_exit 为驱动出口函数当卸载驱动的时候xxx_exit函数就会被调用。 驱动编译完成以后扩展名为.ko有两种命令可以加载驱动模块insmod 和modprobeinsmod是最简单的模块加载命令此命令用于加载指定的.ko 模块比如加载drv.ko 这个驱动模块命令如下 insmod drv.koinsmod 命令不能解决模块的依赖关系比如drv.ko 依赖first.ko 这个模块就必须先使用insmod 命令加载first.ko 这个模块然后再加载drv.ko 这个模块。但是modprobe 就不会存在这个问题modprobe 会分析模块的依赖关系然后会将所有的依赖模块都加载到内核中因此modprobe 命令相比insmod 要智能一些。modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能推荐使用modprobe 命令来加载驱动。modprobe 命令默认会去/lib/modules/ kernel-version目录中查找模块比如本书使用的Linux kernel 的版本号为4.1.15因此modprobe 命令默认会到/lib/modules/4.1.15 这个目录中查找相应的驱动模块一般自己制作的根文件系统中是不会有这个目录的所以需要自己手动创建然后将.ko文件放到这个文件目录即可。 加载成功以后可以使用lsmod命令查看一下。 驱动模块的卸载使用命令“rmmod”即可比如要卸载drv.ko使用如下命令即可 rmmod drv.ko也可以使用“modprobe -r”命令卸载驱动比如要卸载drv.ko命令如下 modprobe -r drv.ko使用modprobe 命令可以卸载掉驱动模块所依赖的其他模块前提是这些依赖模块已经没有被其他模块所使用否则就不能使用modprobe 来卸载驱动模块。所以对于模块的卸载还是推荐使用rmmod 命令。 字符设备注册与注销到内核register_chrdev(设备号、设备名) – 很少用了 这个函数现在已经很少用了后面会讲到有新的函数代替。它有两个缺点 把次设备号全占用了需要手动查看哪些设备号没占用不能自动申请 对于字符设备驱动而言当驱动模块加载成功以后需要注册字符设备同样卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示: static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops) static inline void unregister_chrdev(unsigned int major, const char *name)register_chrdev 函数用于注册字符设备此函数一共有三个参数这三个参数的含义如下 major主设备号Linux 下每个设备都有一个设备号设备号分为主设备号和次设备号两部分关于设备号后面会详细讲解。name设备名字指向一串字符串。fops结构体file_operations 类型指针指向设备的操作函数集合变量。unregister_chrdev 函数用户注销字符设备此函数有两个参数这两个参数含义如下 major要注销的设备对应的主设备号。 name要注销的设备对应的设备名。 一般字符设备的注册在驱动模块的入口函数xxx_init 中进行字符设备的注销在驱动模块的出口函数xxx_exit 中进行。在示例代码40.2.2.1 中字符设备的注册和注销内容如下所示 1 static struct file_operations test_fops; 2 3 /* 驱动入口函数*/ 4 static int __init xxx_init(void) 5 { 6 /* 入口函数具体内容*/ 7 int retvalue 0; 8 9 /* 注册字符设备驱动*/ 10 retvalue register_chrdev(200, chrtest, test_fops); 11 if(retvalue 0){ 12 /* 字符设备注册失败,自行处理*/ 13 } 14 return 0; 15 } 16 17 /* 驱动出口函数*/ 18 static void __exit xxx_exit(void) 19 { 20 /* 注销字符设备驱动*/ 21 unregister_chrdev(200, chrtest); 22 } 23 24 /* 将上面两个函数指定为驱动的入口和出口函数*/ 25 module_init(xxx_init); 26 module_exit(xxx_exit);第1 行定义了一个file_operations 结构体变量test_fopstest_fops 就是设备的操作函数集合只是此时我们还没有初始化test_fops 中的open、release 等这些成员变量所以这个操作函数集合还是空的。 第10 行调用函数register_chrdev 注册字符设备主设备号为200设备名字为“chrtest”设备操作函数集合就是第1 行定义的test_fops。 要注意的一点就是选择没有被使用的主设备号输入命令 cat /proc/devices可以查看当前已经被使用掉的设备号如图40.2.2.1 所示(限于篇幅原因只展示一部分) 在图40.2.2.1 中可以列出当前系统中所有的字符设备和块设备其中第1 列就是设备对应的主设备号。200 这个主设备号在我的开发板中并没有被使用所以我这里就用了200 这个主设备号。 第21 行调用函数unregister_chrdev 注销主设备号为200 的这个设备。 实现设备的具体操作函数 file_operations 结构体就是设备的具体操作函数在示例代码40.2.2.1 中我们定义了file_operations 结构体类型的变量test_fops但是还没对其进行初始化也就是初始化其中的open、release、read 和write 等具体的设备操作函数。本节小节我们就完成变量test_fops 的初始化设置好针对chrtest 设备的操作函数。在初始化test_fops 之前我们要分析一下需求也就是要对 chrtest 这个设备进行哪些操作只有确定了需求以后才知道我们应该实现哪些操作函数。假设对chrtest 这个设备有如下两个要求 1、能够对chrtest 进行打开和关闭操作 设备打开和关闭是最基本的要求几乎所有的设备都得提供打开和关闭的功能。因此我们需要实现file_operations 中的open 和release 这两个函数。 2、对chrtest 进行读写操作假设chrtest 这个设备控制着一段缓冲区(内存)应用程序需要通过read 和write 这两个函数对chrtest 的缓冲区进行读写操作。所以需要实现file_operations 中的read 和write 这两个函数。 需求很清晰了修改示例代码40.2.2.1在其中加入test_fops 这个结构体变量的初始化操作完成以后的内容如下所示 1 /* 打开设备*/ 2 static int chrtest_open(struct inode *inode, struct file *filp) 3 { 4 /* 用户实现具体功能*/ 5 return 0; 6 } 7 8 /* 从设备读取*/ 9 static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) 10 { 11 /* 用户实现具体功能*/ 12 return 0; 13 } 14 15 /* 向设备写数据*/ 16 static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) 17 { 18 /* 用户实现具体功能*/ 19 return 0; 20 } 21 22 /* 关闭/释放设备*/ 23 static int chrtest_release(struct inode *inode, struct file *filp) 24 { 25 /* 用户实现具体功能*/ 26 return 0; 27 } 28 29 static struct file_operations test_fops { 30 .owner THIS_MODULE, 31 .open chrtest_open, 32 .read chrtest_read, 33 .write chrtest_write, 34 .release chrtest_release, 35 }; 36 37 /* 驱动入口函数*/ 38 static int __init xxx_init(void) 39 { 40 /* 入口函数具体内容*/ 41 int retvalue 0; 42 43 /* 注册字符设备驱动*/ 44 retvalue register_chrdev(200, chrtest, test_fops); 45 if(retvalue 0){ 46 /* 字符设备注册失败,自行处理*/ 47 } 48 return 0; 49 } 50 51 /* 驱动出口函数*/ 52 static void __exit xxx_exit(void) 53 { 54 /* 注销字符设备驱动*/ 55 unregister_chrdev(200, chrtest); 56 } 57 58 /* 将上面两个函数指定为驱动的入口和出口函数*/ 59 module_init(xxx_init); 60 module_exit(xxx_exit); 在示例代码40.2.3.1 中我们一开始编写了四个函数chrtest_open、chrtest_read、chrtest_write和chrtest_release。这四个函数就是chrtest 设备的open、read、write 和release 操作函数。第29行~35 行初始化test_fops 的open、read、write 和release 这四个成员变量。 添加LICENSE 和作者信息 最后我们需要在驱动中加入LICENSE 信息和作者信息其中LICENSE 是必须添加的否则的话编译的时候会报错作者信息可以添加也可以不添加。LICENSE 和作者信息的添加使用如下两个函数 MODULE_LICENSE() //添加模块LICENSE 信息 MODULE_AUTHOR() //添加模块作者信息最后给示例代码40.2.3.1 加入LICENSE 和作者信息完成以后的内容如下 1 /* 打开设备*/ 2 static int chrtest_open(struct inode *inode, struct file *filp) 3 { 4 /* 用户实现具体功能*/ 5 return 0; 6 } ...... 57 58 /* 将上面两个函数指定为驱动的入口和出口函数*/ 59 module_init(xxx_init); 60 module_exit(xxx_exit); 61 62 MODULE_LICENSE(GPL); 63 MODULE_AUTHOR(zuozhongkai);第62 行LICENSE 采用GPL 协议。 第63 行添加作者名字。 创建应用层设备节点文件(mknod应用程序通过设备节点操作具体设备) 驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节点文件 mknod /dev/chrdevbase c 200 0其中 “mknod”是创建节点命令“/dev/chrdevbase”是要创建的节点文件“c”表示这是个字符设备“200”是设备的主设备号“0”是设备的次设备号。 创建完成以后就会存在/dev/chrdevbase 这个文件可以使用“ls /dev/chrdevbase -l”命令查看结果如图40.4.4.7 所示 如果chrdevbaseAPP 想要读写chrdevbase 设备直接对/dev/chrdevbase 进行读写操作即可相当于/dev/chrdevbase 这个文件是chrdevbase 设备在用户空间中的实现。Linux 下一切皆文件 至此字符设备驱动开发的完整步骤就讲解完了而且也编写好了一个完整的字符设备驱动模板以后字符设备驱动开发都可以在此模板上进行。 Linux 设备号 设备号的组成(主、次设备号) 为了方便管理Linux 中每个设备都有一个设备号设备号由主设备号和次设备号两部分组成 主设备号表示某一个具体的驱动(如IIC)次设备号表示使用这个驱动的各个设备(如好多个IIC设备)。 Linux 提供了一个名为dev_t 的数据类型表示设备号dev_t 定义在文件include/linux/types.h 里面定义如下 12 typedef __u32 __kernel_dev_t; ...... 15 typedef __kernel_dev_t dev_t;可以看出dev_t 是__u32 类型的而__u32 定义在文件include/uapi/asm-generic/int-ll64.h 里面定义如下 26 typedef unsigned int __u32;综上所述dev_t 其实就是unsigned int 类型是一个32 位的数据类型。这32 位的数据构成了主设备号和次设备号两部分其中高12 位为主设备号低20 位为次设备号。因此Linux系统中主设备号范围为0~4095 所以大家在选择主设备号的时候一定不要超过这个范围。 在文件include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏)如下所示 6 #define MINORBITS 20 7 #define MINORMASK ((1U MINORBITS) - 1) 8 9 #define MAJOR(dev) ((unsigned int) ((dev) MINORBITS)) 10 #define MINOR(dev) ((unsigned int) ((dev) MINORMASK)) 11 #define MKDEV(ma,mi) (((ma) MINORBITS) | (mi))第6 行宏MINORBITS 表示次设备号位数一共是20 位。 第7 行宏MINORMASK 表示次设备号掩码。 第9 行宏MAJOR 用于从dev_t 中获取主设备号将dev_t 右移20 位即可。 第10 行宏MINOR 用于从dev_t 中获取次设备号取dev_t 的低20 位的值即可。 第11 行宏MKDEV 用于将给定的主设备号和次设备号的值组合成dev_t 类型的设备号。 设备号的分配 1、静态分配设备号 本小节讲的设备号分配主要是主设备号的分配。前面讲解字符设备驱动的时候说过了注册字符设备的时候需要给设备指定一个设备号这个设备号可以是驱动开发者静态的指定一个设备号比如选择200 这个主设备号。有一些常用的设备号已经被Linux 内核开发者给分配掉了具体分配的内容可以查看文档Documentation/devices.txt。并不是说内核开发者已经分配掉的主设备号我们就不能用了具体能不能用还得看我们的硬件平台运行过程中有没有使用这个主设备号使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号。 2、动态分配设备号 静态分配设备号需要我们检查当前系统中所有被使用了的设备号然后挑选一个没有使用的。而且静态分配设备号很容易带来冲突问题Linux 社区推荐使用动态分配设备号在注册字符设备之前先申请一个设备号系统会自动给你一个没有被使用的设备号这样就避免了冲突。 卸载驱动的时候释放掉这个设备号即可设备号的申请函数如下 int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)函数alloc_chrdev_region 用于申请设备号此函数有4 个参数 dev保存申请到的设备号。 baseminor次设备号起始地址alloc_chrdev_region 可以申请一段连续的多个设备号这些设备号的主设备号一样但是次设备号不同次设备号以baseminor 为起始地址地址开始递增。一般baseminor 为0也就是说次设备号从0 开始。 count要申请的设备号数量。 name设备名字。 注销字符设备之后要释放掉设备号设备号释放函数如下 void unregister_chrdev_region(dev_t from, unsigned count)此函数有两个参数 from要释放的设备号。 count表示从from 开始要释放的设备号数量。 chrdevbase字符设备驱动实验程序编写 字符设备驱动开发的基本步骤我们已经了解了本节我们就以chrdevbase 这个虚拟设备为例完整的编写一个字符设备驱动模块。chrdevbase 不是实际存在的一个设备是笔者为了方便讲解字符设备的开发而引入的一个虚拟设备。chrdevbase 设备有两个缓冲区一个为读缓冲区一个为写缓冲区这两个缓冲区的大小都为100 字节。在应用程序中可以向chrdevbase 设备的写缓冲区中写入数据从读缓冲区中读取数据。chrdevbase 这个虚拟设备的功能很简单但是它包含了字符设备的最基本功能。 本实验对应的例程路径为开发板光盘- 2、Linux 驱动例程- 1_chrdevbase。 应用程序调用open 函数打开chrdevbase 这个设备打开以后可以使用write 函数向chrdevbase 的写缓冲区writebuf 中写入数据(不超过100 个字节)也可以使用read 函数读取读缓冲区readbuf 中的数据操作操作完成以后应用程序使用close 函数关闭chrdevbase 设备。 1、创建VSCode 工程 在Ubuntu 中创建一个目录用来存放Linux 驱动程序比如我创建了一个名为Linux_Drivers的目录来存放所有的Linux 驱动。在Linux_Drivers 目录下新建一个名为1_chrdevbase 的子目录来存放本实验所有文件如图40.4.1.1 所示 在1_chrdevbase 目录中新建VSCode 工程并且新建chrdevbase.c 文件完成以后1_chrdevbase 目录中的文件如图40.4.1.2 所示 2、VSCode添加头文件路径(重要) 因为是编写Linux 驱动因此会用到Linux 源码中的函数。我们需要在VSCode 中添加Linux源码中的头文件路径。打开VSCode按下“CrtlShiftP”打开VSCode 的控制台然后输入“C/C: Edit configurations(JSON) ”打开C/C编辑配置文件如图40.4.1.3 所示 打开以后会自动在.vscode 目录下生成一个名为c_cpp_properties.json 的文件此文件默认内容如下所示 1 { 2 configurations: [ 3 { 4 name: Linux, 5 includePath: [ 6 ${workspaceFolder}/**, 7 ], 8 defines: [], 9 compilerPath: /usr/bin/clang, 10 cStandard: c11, 11 cppStandard: c17, 12 intelliSenseMode: clang-x64 13 } 14 ], 15 version: 4 16 }第5 行的includePath 表示头文件路径需要将Linux 源码里面的头文件路径添加进来也就是我们前面移植的Linux 源码中的头文件路径。添加头文件路径以后的c_cpp_properties.json的文件内容如下所示 1 { 2 configurations: [ 3 { 4 name: Linux, 5 includePath: [ 6 ${workspaceFolder}/**, 7 /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include, 8 /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include, 9 /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/generated/ 10 ], 11 defines: [], ...... 16 } 17 ], 18 version: 4 19 } 第7~9 行就是添加好的Linux 头文件路径。分别是开发板所使用的Linux 源码下的include、arch/arm/include 和arch/arm/include/generated 这三个目录的路径注意这里使用了绝对路径。 3、编写驱动程序(传递函数copy_to_user、copy_from_user) 工程建立好以后就可以开始编写驱动程序了新建chrdevbase.c然后在里面输入如下内容 #include linux/types.h #include linux/kernel.h #include linux/delay.h #include linux/ide.h #include linux/init.h #include linux/module.h /*************************************************************** Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 文件名 : chrdevbase.c 作者 : 左忠凯 版本 : V1.0 描述 : chrdevbase驱动文件。 其他 : 无 论坛 : www.openedv.com 日志 : 初版V1.0 2019/1/30 左忠凯创建 ***************************************************************/#define CHRDEVBASE_MAJOR 200 /* 主设备号 */ #define CHRDEVBASE_NAME chrdevbase /* 设备名 */static char readbuf[100]; /* 读缓冲区 */ static char writebuf[100]; /* 写缓冲区 */ static char kerneldata[] {kernel data!};/** description : 打开设备* param - inode : 传递给驱动的inode* param - filp : 设备文件file结构体有个叫做private_data的成员变量* 一般在open的时候将private_data指向设备结构体。* return : 0 成功;其他 失败*/ static int chrdevbase_open(struct inode *inode, struct file *filp) {//printk(chrdevbase open!\r\n);return 0; }/** description : 从设备读取数据 * param - filp : 要打开的设备文件(文件描述符)* param - buf : 返回给用户空间的数据缓冲区* param - cnt : 要读取的数据长度* param - offt : 相对于文件首地址的偏移* return : 读取的字节数如果为负值表示读取失败*/ static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) {int retvalue 0;/* 向用户空间发送数据 */memcpy(readbuf, kerneldata, sizeof(kerneldata));retvalue copy_to_user(buf, readbuf, cnt);//cnt大小由应用程序指定if(retvalue 0){printk(kernel senddata ok!\r\n);}else{printk(kernel senddata failed!\r\n);}//printk(chrdevbase read!\r\n);return 0; }/** description : 向设备写数据 * param - filp : 设备文件表示打开的文件描述符* param - buf : 要写给设备写入的数据* param - cnt : 要写入的数据长度* param - offt : 相对于文件首地址的偏移* return : 写入的字节数如果为负值表示写入失败*/ static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) {int retvalue 0;/* 接收用户空间传递给内核的数据并且打印出来 */retvalue copy_from_user(writebuf, buf, cnt);if(retvalue 0){printk(kernel recevdata:%s\r\n, writebuf);}else{printk(kernel recevdata failed!\r\n);}//printk(chrdevbase write!\r\n);return 0; }/** description : 关闭/释放设备* param - filp : 要关闭的设备文件(文件描述符)* return : 0 成功;其他 失败*/ static int chrdevbase_release(struct inode *inode, struct file *filp) {//printk(chrdevbase release\r\n);return 0; }/** 设备操作函数结构体*/ static struct file_operations chrdevbase_fops {.owner THIS_MODULE, .open chrdevbase_open, //打开.read chrdevbase_read, //读.write chrdevbase_write, //写.release chrdevbase_release, //关闭 };/** description : 驱动入口函数 * param : 无* return : 0 成功;其他 失败*/ static int __init chrdevbase_init(void) {int retvalue 0;/* 注册字符设备驱动 */retvalue register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, chrdevbase_fops);if(retvalue 0){printk(chrdevbase driver register failed\r\n);}printk(chrdevbase init!\r\n);return 0; }/** description : 驱动出口函数* param : 无* return : 无*/ static void __exit chrdevbase_exit(void) {/* 注销字符设备驱动 */unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);printk(chrdevbase exit!\r\n); }/* * 将上面两个函数指定为驱动的入口和出口函数 */ module_init(chrdevbase_init); module_exit(chrdevbase_exit);/* * LICENSE和作者信息*/ MODULE_LICENSE(GPL); MODULE_AUTHOR(zuozhongkai);第32~36 行chrdevbase_open 函数当应用程序调用open 函数的时候此函数就会调用本例程中我们没有做任何工作只是输出一串字符用于调试。这里使用了printk 来输出信息而不是printf因为在Linux 内核中没有printf 这个函数printf运行在用户态printk 运行在内核态。 不同之处在于printk 可以根据日志级别对消息进行分类一共有8 个消息级别这8 个消息级别定义在文件include/linux/kern_levels.h 里面定义如下 #define KERN_SOH \001 #define KERN_EMERG KERN_SOH 0 /* 紧急事件一般是内核崩溃*/ #define KERN_ALERT KERN_SOH 1 /* 必须立即采取行动*/ #define KERN_CRIT KERN_SOH 2 /* 临界条件比如严重的软件或硬件错误*/ #define KERN_ERR KERN_SOH 3 /* 错误状态一般设备驱动程序中使用 KERN_ERR 报告硬件错误*/ #define KERN_WARNING KERN_SOH 4 /* 警告信息不会对系统造成严重影响*/ #define KERN_NOTICE KERN_SOH 5 /* 有必要进行提示的一些信息*/ #define KERN_INFO KERN_SOH 6 /* 提示性的信息*/ #define KERN_DEBUG KERN_SOH 7 /* 调试信息*/一共定义了8 个级别其中0 的优先级最高7 的优先级最低。如果要设置消息级别参考如下示例 printk(KERN_EMERG gsmi: Log Shutdown Reason\n);上述代码就是设置“gsmi: Log Shutdown Reason\n”这行消息的级别为KERN_EMERG。在具体的消息前面加上KERN_EMERG 就可以将这条消息的级别设置为KERN_EMERG。如果使用printk 的时候不显式的设置消息级别那么printk 将会采用默认级别MESSAGE_LOGLEVEL_DEFAULTMESSAGE_LOGLEVEL_DEFAULT 默认为4。 在include/linux/printk.h 中有个宏CONSOLE_LOGLEVEL_DEFAULT定义如下 #define CONSOLE_LOGLEVEL_DEFAULT 7CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上此宏默认为7意味着只有优先级高于7 的消息才能显示在控制台上。 这个就是printk 和printf 的最大区别可以通过消息级别来决定哪些消息可以显示在控制台上。默认消息级别为44 的级别比7 高所示直接使用printk 输出的信息是可以显示在控制台上的。 参数filp 有个叫做private_data 的成员变量private_data 是个void 指针一般在驱动中将private_data 指向设备结构体设备结构体会存放设备的一些属性。 第46~61 行chrdevbase_read 函数应用程序调用read 函数从设备中读取数据的时候此函数会执行。参数buf 是用户空间的内存读取到的数据存储在buf 中参数cnt 是要读取的字节数参数offt 是相对于文件首地址的偏移。kerneldata 里面保存着用户空间要读取的数据第51行先将kerneldata 数组中的数据拷贝到读缓冲区readbuf 中第52 行通过函数copy_to_user 将 readbuf 中的数据复制到参数buf 中。因为内核空间不能直接操作用户空间的内存因此需要借助copy_to_user 函数来完成内核空间的数据到用户空间的复制。copy_to_user 函数原型如下 static inline long copy_to_user(void __user *to, const void *from, unsigned long n)参数to 表示目的参数from 表示源参数n 表示要复制的数据长度。如果复制成功返回值为0如果复制失败则返回负数。 第71~84 行chrdevbase_write 函数应用程序调用write 函数向设备写数据的时候此函数就会执行。参数buf 就是应用程序要写入设备的数据也是用户空间的内存参数cnt 是要写入的数据长度参数offt 是相对文件首地址的偏移。 第75 行通过函数copy_from_user 将buf 中的数据复制到写缓冲区writebuf 中因为用户空间内存不能直接访问内核空间的内存所以需要借助函数copy_from_user 将用户空间的数据复制到writebuf 这个内核空间中。 第91~95 行chrdevbase_release 函数应用程序调用close 关闭设备文件的时候此函数会执行一般会在此函数里面执行一些释放操作。如果在open 函数中设置了filp 的private_data成员变量指向设备结构体那么在release 函数最终就要释放掉。 第100~106 行新建chrdevbase 的设备文件操作结构体chrdevbase_fops 初始化chrdevbase_fops。 第113~124 行驱动入口函数chrdevbase_init第118 行调用函数register_chrdev 来注册字符设备。 第131~136 行驱动出口函数chrdevbase_exit第134 行调用函数unregister_chrdev 来注销字符设备。 第141~142 行通过module_init 和module_exit 这两个函数来指定驱动的入口和出口函数。 第147~148 行添加LICENSE 和作者信息。 编写测试APP(1表示向驱动读数据、2表示向驱动写数据) 1、C 库文件操作基本函数 编写测试APP 就是编写Linux 应用需要用到C 库里面和文件操作有关的一些函数比如open、read、write 和close 这四个函数。 2、编写测试APP 程序 驱动编写好以后是需要测试的一般编写一个简单的测试APP测试APP 运行在用户空间。测试APP 很简单通过输入相应的指令来对chrdevbase 设备执行读或者写操作。在1_chrdevbase 目录中新建chrdevbaseApp.c 文件在此文件中输入如下内容 #include stdio.h #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.h #include stdlib.h #include string.h /*************************************************************** Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 文件名 : chrdevbaseApp.c 作者 : 左忠凯 版本 : V1.0 描述 : chrdevbase驱测试APP。 其他 : 使用方法./chrdevbase /dev/chrdevbase 1|2argv[2] 1:读文件argv[2] 2:写文件 论坛 : www.openedv.com 日志 : 初版V1.0 2019/1/30 左忠凯创建 ***************************************************************/static char usrdata[] {usr data!};/** description : main主程序* param - argc : argv数组元素个数* param - argv : 具体参数* return : 0 成功;其他 失败*/ int main(int argc, char *argv[]) {int fd, retvalue;char *filename;char readbuf[100], writebuf[100];if(argc ! 3){printf(Error Usage!\r\n);return -1;}filename argv[1];/* 打开驱动文件 */fd open(filename, O_RDWR);if(fd 0){printf(Cant open file %s\r\n, filename);return -1;}if(atoi(argv[2]) 1){ /* 从驱动文件读取数据 */retvalue read(fd, readbuf, 50);if(retvalue 0){printf(read file %s failed!\r\n, filename);}else{/* 读取成功打印出读取成功的数据 */printf(read data:%s\r\n,readbuf);}}if(atoi(argv[2]) 2){/* 向设备驱动写数据 */memcpy(writebuf, usrdata, sizeof(usrdata));retvalue write(fd, writebuf, 50);if(retvalue 0){printf(write file %s failed!\r\n, filename);}}/* 关闭设备 */retvalue close(fd);if(retvalue 0){printf(Cant close file %s\r\n, filename);return -1;}return 0; }第21 行数组usrdata 是测试APP 要向chrdevbase 设备写入的数据。 第35 行判断运行测试APP 的时候输入的参数是不是为3 个main 函数的argc 参数表示参数数量argv[]保存着具体的参数如果参数不为3 个的话就表示测试APP 用法错误。比如现在要从chrdevbase 设备中读取数据需要输入如下命令 ./chrdevbaseApp /dev/chrdevbase 1上述命令一共有三个参数“./chrdevbaseApp”、“/dev/chrdevbase”和“1”这三个参数分别对应argv[0]、argv[1]和argv[2]。第一个参数表示运行chrdevbaseAPP 这个软件第二个参数表示测试APP要打开/dev/chrdevbase 这个设备。第三个参数就是要执行的操作1 表示从chrdevbase中读取数据2 表示向chrdevbase 写数据。 第40 行获取要打开的设备文件名字argv[1]保存着设备名字。 第43 行调用C 库中的open 函数打开设备文件/dev/chrdevbase。 第49 行判断argv[2]参数的值是1 还是2因为输入命令的时候其参数都是字符串格式的因此需要借助atoi 函数将字符串格式的数字转换为真实的数字。 第50 行当argv[2]为1 的时候表示要从chrdevbase 设备中读取数据一共读取50 字节的数据读取到的数据保存在readbuf 中读取成功以后就在终端上打印出读取到的数据。 第59 行当argv[2]为2 的时候表示要向chrdevbase 设备写数据。 第69 行对chrdevbase 设备操作完成以后就关闭设备。 chrdevbaseApp.c 内容还是很简单的就是最普通的文件打开、关闭和读写操作。 Makefile编译驱动程序和测试APP 1、编译驱动程序 首先编译驱动程序也就是chrdevbase.c 这个文件我们需要将其编译为.ko 模块创建Makefile 文件然后在其中输入如下内容 KERNELDIR : /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek CURRENT_PATH : $(shell pwd) obj-m : chrdevbase.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M$(CURRENT_PATH) modules clean:$(MAKE) -C $(KERNELDIR) M$(CURRENT_PATH) clean第1 行KERNELDIR 表示开发板所使用的Linux 内核源码目录使用绝对路径大家根据自己的实际情况填写即可。 第2 行CURRENT_PATH 表示当前路径直接通过运行“pwd”命令来获取当前所处路径。 第3 行obj-m 表示将chrdevbase.c 这个文件编译为chrdevbase.ko 模块。 第8 行具体的编译命令后面的modules 表示编译模块-C 表示将当前的工作目录切换到指定目录中也就是KERNERLDIR 目录。M 表示模块源码目录“make modules”命令中加入Mdir 以后程序会自动到指定的dir 目录中读取模块的源码并将其编译为.ko 文件。 Makefile 编写好以后输入“make”命令编译驱动模块编译过程如图40.4.3.1 所示 编译成功以后就会生成一个叫做chrdevbaes.ko 的文件此文件就是chrdevbase 设备的驱动模块。至此chrdevbase 设备的驱动就编译成功。 2、编译测试APP 测试APP 比较简单只有一个文件因此就不需要编写Makefile 了直接输入命令编译。 因为测试APP 是要在ARM 开发板上运行的所以需要使用arm-linux-gnueabihf-gcc 来编译输入如下命令 arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp编译完成以后会生成一个叫做chrdevbaseApp 的可执行程序输入如下命令查看chrdevbaseAPP 这个程序的文件信息 file chrdevbaseApp结果如图40.4.3.2 所示 从图40.4.3.2 可以看出chrdevbaseAPP 这个可执行文件是32 位LSB 格式ARM 版本的因此chrdevbaseAPP 只能在ARM 芯片下运行。 运行测试 1、设置内核启动、复制.ko和app到指定目录、加载驱动模块 驱动模块chrdevbase.ko 和测试软件chrdevbaseAPP 都已经准备好了接下来就是运行测试。 为了方便测试Linux 系统选择通过TFTP 从网络启动并且使用NFS 挂载网络根文件系统确保uboot 中bootcmd 环境变量的值为 tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000bootargs 环境变量的值为 consolettymxc0,115200 root/dev/nfs rw nfsroot192.168.1.250:/home/zuozhongkai/linux/nfs/ rootfs ip192.168.1.251:192.168.1.250:192.168.1.1:255.255.255.0::eth0:off【前面提过】设置好以后启动Linux 系统检查开发板根文件系统中有没有“/lib/modules/4.1.15”这个目录如果没有的话自行创建。注意“/lib/modules/4.1.15”这个目录用来存放驱动模块使用modprobe 命令加载驱动模块的时候驱动模块要存放在此目录下。“/lib/modules”是通用的 不管你用的什么板子、什么内核这部分是一样的。不一样的是后面的“4.1.15”这里要根据你所使用的Linux 内核版本来设置比如ALPHA 开发板现在用的是4.1.15 版本的Linux 内核因此就是“/lib/modules/4.1.15”。如果你使用的其他版本内核比如5.14.31那么就应该创建 “/lib/modules/5.14.31”目录否则modprobe 命令无法加载驱动模块。 因为是通过NFS 将Ubuntu 中的rootfs(第三十八章制作好的根文件系统)目录挂载为根文件系统所以可以很方便的将chrdevbase.ko 和chrdevbaseAPP 复制到rootfs/lib/modules/4.1.15 目录中命令如下 sudo cp chrdevbase.ko chrdevbaseApp /home/zuozhongkai/linux/nfs/rootfs/lib/modules/4.1.15/ -f拷贝完成以后就会在开发板的/lib/modules/4.1.15 目录下存在chrdevbase.ko 和chrdevbaseAPP 这两个文件如图40.4.4.1 所示 输入如下命令加载chrdevbase.ko 驱动文件 insmod chrdevbase.ko或 modprobe chrdevbase.ko【注意】如果使用modprobe 加载驱动的话第一次可能会出现如图40.4.4.2 所示的提示 从图40.4.4.2 可以看出modprobe 提示无法打开“modules.dep”这个文件因此驱动挂载失败了。我们不用手动创建modules.dep 这个文件直接输入 depmod命令即可自动生成modules.dep有些根文件系统可能没有depmod 这个命令如果没有这个命令就只能重新配置busybox使能此命令然后重新编译busybox。输入“depmod”命令以后会自动生成modules.alias、 modules.symbols 和modules.dep 这三个文件如图40.4.4.3 所示 重新使用modprobe 加载chrdevbase.ko结果如图40.4.4.4 所示 从图40.4.4.4 可以看到“chrdevbase init”这一行这一行正是chrdevbase.c 中模块入口函数chrdevbase_init 输出的信息说明模块加载成功 输入“lsmod”命令即可查看当前系统中存在的模块结果如图40.4.4.5 所示 从图40.4.4.5 可以看出当前系统只有“chrdevbase”这一个模块。输入如下命令查看当前系统中有没有chrdevbase 这个设备 cat /proc/devices结果如图40.4.4.6 所示 从图40.4.4.6 可以看出当前系统存在chrdevbase 这个设备主设备号为200跟我们设置的主设备号一致。 2、创建设备节点文件(mknod应用程序通过设备节点操作具体设备) 驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节点文件 mknod /dev/chrdevbase c 200 0其中 “mknod”是创建节点命令“/dev/chrdevbase”是要创建的节点文件“c”表示这是个字符设备“200”是设备的主设备号“0”是设备的次设备号。 创建完成以后就会存在/dev/chrdevbase 这个文件可以使用“ls /dev/chrdevbase -l”命令查看结果如图40.4.4.7 所示 如果chrdevbaseAPP 想要读写chrdevbase 设备直接对/dev/chrdevbase 进行读写操作即可相当于/dev/chrdevbase 这个文件是chrdevbase 设备在用户空间中的实现。Linux 下一切皆文件 3、chrdevbase设备操作测试 使用chrdevbaseApp 软件操作chrdevbase 这个设备看看读写是否正常首先进行读操作输入如下命令 ./chrdevbaseApp /dev/chrdevbase 1结果如图40.4.4.8 所示 从图40.4.4.8 可以看出首先输出“kernel senddata ok!”这一行信息这是驱动程序中chrdevbase_read 函数输出的信息因为chrdevbaseAPP 使用read 函数从chrdevbase 设备读取数据因此chrdevbase_read 函数就会执行。chrdevbase_read 函数向chrdevbaseAPP 发送“kernel data!”数据chrdevbaseAPP 接收到以后就打印出来“read data:kernel data!”就是chrdevbaseAPP打印出来的接收到的数据。说明对chrdevbase 的读操作正常接下来测试对chrdevbase 设备的写操作输入如下命令 ./chrdevbaseApp /dev/chrdevbase 2结果如图40.4.4.9 所示 只有一行“kernel recevdata:usr data!”这个是驱动程序中的chrdevbase_write 函数输出的。 chrdevbaseAPP 使用write 函数向chrdevbase 设备写入数据“usr data!”。chrdevbase_write 函数接收到以后将其打印出来。说明对chrdevbase 的写操作正常既然读写都没问题说明我们编写的chrdevbase 驱动是没有问题的。 4、卸载驱动模块 如果不再使用某个设备的话可以将其驱动卸载掉比如输入如下命令卸载掉chrdevbase 这个设备 rmmod chrdevbase.ko卸载以后使用lsmod 命令查看chrdevbase 这个模块还存不存在结果如图40.4.4.10 所示 从图40.4.4.10 可以看出此时系统已经没有任何模块了chrdevbase 这个模块也不存在了说明模块卸载成功。 至此chrdevbase 这个设备的整个驱动就验证完成了。本章我们详细的讲解了字符设备驱动的开发步骤并且以一个虚拟的chrdevbase 设备为例完成了字符设备驱动的开发框架以及测试方法以后的字符设备驱动实验基本都以此为蓝本。
http://www.pierceye.com/news/857938/

相关文章:

  • 南充哪里做网站太原关键词优化公司
  • 哪个网站做的ppt模板好投放广告网站
  • 公司网站中新闻中心怎样做优化百度浏览器电脑版
  • 厦门网站建设 九来外国做视频在线观看网站
  • 用.net做购物网站山东建筑公司实力排名
  • 做百度推广网站找谁好宁夏省建筑信息平台
  • phpcmsv9手机网站源码网站开发ide php
  • 学校网站建设成功案例微信公众号网站导航怎么做
  • 重庆汽车网站建设建立大安全大应急框架
  • 重庆模板建站定制网站做企业网站到哪里找
  • asp技术做网站网站建设推广是什么工作室
  • 运营公众号还是做网站室内装修设计软件哪个好用
  • 卖营销软件的网站如何利用站群做网站
  • 网站空间价格怎么算网站制作与发布
  • 做网站的动态图片廊坊做网站哪家好
  • 建设企业网站报价怎么做网站logo
  • 企业类网站谷歌seo招聘
  • asp.net网站安装顺序idc 公司网站模板
  • 新手学做网站优化2022app分类排行
  • 微信微网站制作公司2008年做的网站
  • 网站建设柒首先金手指1男孩做网站
  • 葫芦岛市建设局网站网页制作与网站发布
  • 企业网站首页布局尺寸营销网站建设哪家便宜
  • 专题网站建设策划郑州市做网站的公
  • wordpress網頁版天津百度网站排名优化
  • 做网站建设销售工资代做电大网站ui作业
  • DMZ做网站wordpress 小工具 创建
  • 宠物网站建设方案外贸网站建设公司服务
  • 玉林网站建设学校门户网站建设的意义
  • 湖南营销型网站建设 在线磐石网络现在出入河南最新规定