在Ubuntu上为Android系统编写Linux内核驱动程序(老罗学习笔记1)
这里,我们不会为真实的硬件设备编写内核驱动程序。为了方便描述为Android系统编写内核驱动程序的过程,我们使用一个虚拟的硬件设备,这个设备只有一个4字节的寄存器,它可读可写。想起我们第一次学习程序语言时,都喜欢用“Hello, World”作为例子,这里,我们就把这个虚拟的设备命名为“hello”,而这个内核驱动程序也命名为hello驱动程序。其实,Android内核驱动程序和一般Linux内核驱动程序的编写方法是一样的,都是以Linux模块的形式实现的,具体可参考前面Android学习启动篇一文中提到的Linux Device Drivers一书。不过,这里我们还是从Android系统的角度来描述Android内核驱动程序的编写和编译过程。
一. 参照前面两篇文章在Ubuntu上下载、编译和安装Android最新源代码和在Ubuntu上下载、编译和安装Android最新内核源代码(Linux Kernel)准备好Android内核驱动程序开发环境。
二. 进入到kernel/common/drivers目录,新建hello目录:
USER-NAME@MACHINE-NAME:~/Android$ cd kernel/common/drivers
USER-NAME@MACHINE-NAME:~/Android/kernel/common/drivers$ mkdir hello
三. 在hello目录中增加hello.h文件:
#ifndef _HELLO_ANDROID_H_
#define _HELLO_ANDROID_H_ #include <linux/cdev.h> //对字符设备结构cdev以及一系列的操作函数定义
#include <linux/semaphore.h> //信号量使能的头文件 #define HELLO_DEVICE_NODE_NAME "hello" //字节名字
#define HELLO_DEVICE_FILE_NAME "hello" //文件名
#define HELLO_DEVICE_PROC_NAME "hello" //任务名
#define HELLO_DEVICE_CLASS_NAME "hello" //类名 struct hello_android_dev { //驱动结构体定义(虚拟的硬件设备)
int val; //设备里面的寄存器地址
struct semaphore sem; //信号量,用于同步访问寄存器val
struct cdev dev; //dev成员变量是一个内嵌的字符设备,这个Linux驱动程序自定义字符设备结构体的标准方法
}; #endif
这个头文件定义了一些字符串常量宏,在后面我们要用到。此外,还定义了一个字符设备结构体hello_android_dev,这个就是我们虚拟的硬件设备了,val成员变量就代表设备里面的寄存器,它的类型为int,sem成员变量是一个信号量,是用同步访问寄存器val的,dev成员变量是一个内嵌的字符设备,这个Linux驱动程序自定义字符设备结构体的标准方法。
四.在hello目录中增加hello.c文件,这是驱动程序的实现部分。驱动程序的功能主要是向上层提供访问设备的寄存器的值,包括读和写。这里,提供了三种访问设备寄存器的方法,一是通过proc文件系统来访问,二是通过传统的设备文件的方法来访问,三是通过devfs文件系统来访问。下面分段描述该驱动程序的实现。
(size_t:在32位系统上 定义为 unsigned int 也就是说在32位系统上是32位无符号整形。在64位系统上定义为 unsigned long 也就是说在64位系统上是64位无符号整形。size_t一般用来表示一种计数,比如有多少东西被拷贝等。例如:sizeof操作符的结果类型是size_t,该类型保证能容纳实现所建立的最大对象的字节大小。 它的意义大致是“适于计量内存中可容纳的数据项目个数的无符号整数类型”。所以,它在数组下标和内存管理函数之类的地方广泛使用。而ssize_t这个数据类型用来表示可以被执行读写操作的数据块的大小.它和size_t类似,但必需是signed.意即:它表示的是signed size_t类型的。)
首先是包含必要的头文件和定义三种访问设备的方法:
/*---------------------------------------
(三)该文件包
---------------------------------------*/
#include <linux/init.h> //包含了模块的初始化的宏定义 以及一些其他函数的初始化函数
/*MODULE.H:写内核驱动的时候 必须加载这个头文件,作用是动态的将模块加载到内核中去,常用的宏定义如 MODULE_LICESENCE(),MODULE_AUTHOR(),等在此文件中
而且 kobject,kset结构体题及其操作函数也在这个结构体中
*/ #include <linux/module.h>
#include <linux/types.h> //原系统数据类型
#include <linux/fs.h> //file_operations、inode_operations、super_operations结构体(文件系统函数定义头文件)
#include <linux/proc_fs.h> /*proc 文件系统是由软件创建,被内核用来向外界报告信息的一个文件系统,,http://www.cnblogs.com/Ph-one/p/4399326.html */
#include <linux/device.h> //在这个头文件中包含了 bus的一些函数 和 drifver的一些函数,以及class_create()等函数
#include <asm/uaccess.h> /* copy_to_user和copy_from_user */
#include "hello.h" //自定义头文件 /*主设备和从设备号变量;设备号有什么意义,作用是什么,大小有无关系*/
static int hello_major = ; //主设备
static int hello_minor = /*设备类别和设备变量*/
static struct class* hello_class = NULL;
static struct hello_android_dev* hello_dev = NULL;
/*---------------------------------------
(二)
/*------------------------------------------------------------------------------------
-*/
/*传统的设备文件操作方法*/
static int hello_open(struct inode* inode, struct file* filp); //打开
static int hello_release(struct inode* inode, struct file* filp); //释放
static ssize_t hello_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos);
static ssize_t hello_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos); /*设备文件操作方法表 */
/* 第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针.
这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中,
它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏. */
static struct file_operations hello_fops = { /*对应用层出口*/
.owner = THIS_MODULE,
.open = hello_open, //尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.
.release = hello_release, //在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL
.read = hello_read,
.write = hello_write,
};
---------------------------------------*/
/*------------------------------------------------------------------------------------
sysfs访问设置设备属性-*/
/*访问设置属性方法 */
static ssize_t hello_val_show(struct device* dev, struct device_attribute* attr, char* buf);
static ssize_t hello_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count); /*定义设备属性*/
static DEVICE_ATTR(val, S_IRUGO | S_IWUSR, hello_val_show, hello_val_store);
/*----------------------------------------------------------------------------------*/
DEVICE_ATTR(_name, _mode, _show, _store)
_name:名称,也就是将在sys fs中生成的文件名称。
_mode:上述文件的访问权限,与普通文件相同,UGO的格式。
_show:显示函数,cat该文件时,此函数被调用。
_store:写函数,echo内容到该文件时,此函数被调用。
/*---------------------------------------
(一)
定义传统的设备文件访问方法,主要是定义hello_open、hello_release、hello_read和hello_write这四个打开、释放、读和写设备文件的方法:
/*打开设备方法*/
static int hello_open(struct inode* inode, struct file* filp) {
struct hello_android_dev* dev; /*将自定义设备结构体保存在文件指针的私有数据域中,以便访问设备时拿来用*/
dev = container_of(inode->i_cdev, struct hello_android_dev, dev); //container_of()的作用就是通过一个结构变量中一个成员的地址找到这个结构体变量的首地址
//struct cdev * :
filp->private_data = dev; //void * private_data: 该成员是系统调用时保存状态信息非常有用的资源 return ;
} /*设备文件释放时调用,空实现*/
static int hello_release(struct inode* inode, struct file* filp) {
return ; //函数返回零,就是释放,整个退出,好笑
} /*读取设备的寄存器val的值*/
static ssize_t hello_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos) {
ssize_t err = ;
struct hello_android_dev* dev = filp->private_data; //将系统保存状态信息(地址)传给Hello结构体首地址,,(hello_android_dev* dev:这样定义是不是有问题??没问题)
/*同步访问*/
if(down_interruptible(&(dev->sem))) {
/* 这个函数的功能就是获得信号量,如果得不到信号量就睡眠,此时没有信号打断,那么进入睡眠。但是在睡眠过程中可能被信号打断,打断之后返回-EINTR,主要用来进程间的互斥同步 */
return -ERESTARTSYS;
} if(count < sizeof(dev->val)) { //这一步的作用是做什么的呢?
goto out;
} /*将寄存器val的值拷贝到用户提供的缓冲区*/
if(copy_to_user(buf, &(dev->val), sizeof(dev->val))) { //拷贝成功返回‘1’;
/*copy_to_user :从内核区中读取数据到用户区
*/
err = -EFAULT;
goto out;
} err = sizeof(dev->val); out:
up(&(dev->sem));
return err;
} /*写设备的寄存器值val*/
static ssize_t hello_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos) {
struct hello_android_dev* dev = filp->private_data;
ssize_t err = ; /*同步访问*/
if(down_interruptible(&(dev->sem))) {
return -ERESTARTSYS;
} if(count != sizeof(dev->val)) {
goto out;
} /*将用户提供的缓冲区的值写到设备寄存器去,读成功返回‘0’,失败返回数据字节数*/
if(copy_from_user(&(dev->val), buf, count)) { //这个buf是个地址
err = -EFAULT;
goto out;
} err = sizeof(dev->val); out:
up(&(dev->sem));
return err;
}
---------------------------------------*/
定义通过sysfs文件系统访问方法,这里把设备的寄存器val看成是设备的一个属性,通过读写这个属性来对设备进行访问,主要是实现hello_val_show和hello_val_store两个方法,同时定义了两个内部使用的访问val值的方法__hello_get_val和__hello_set_val:
/*读取寄存器val的值到缓冲区buf中,内部使用*/
static ssize_t __hello_get_val(struct hello_android_dev* dev, char* buf) {
int val = ; /*同步访问*/
if(down_interruptible(&(dev->sem))) {
return -ERESTARTSYS; //函数直接跳出,没有执行读取任务
} val = dev->val;
up(&(dev->sem)); return snprintf(buf, PAGE_SIZE, "%d\n", val); //snprintf:将可变个参数(...)按照format格式化成字符串,然后将其复制到str中,不是太懂?答:snprintf有数据格式转换的功能
} /*把缓冲区buf的值写到设备寄存器val中去,内部使用*/
static ssize_t __hello_set_val(struct hello_android_dev* dev, const char* buf, size_t count) {
int val = ; /*将字符串转换成数字*/
/*
就是解析字符串cp 中 8,10,16 进制数字 ,返回值是解析的数字,endp 指向第一个不是数值的字符串起始处,base :进制
unsigned long long simple_strtoull(const char *cp, char **endp, unsigned int base)
*/
val = simple_strtol(buf, NULL, ); //将一个字符串转换成 sigend long型数据,十进制 /*同步访问*/
if(down_interruptible(&(dev->sem))) {
return -ERESTARTSYS;
} dev->val = val;
up(&(dev->sem)); return count;
} /*读取设备属性val*/
static ssize_t hello_val_show(struct device* dev, struct device_attribute* attr, char* buf) {
struct hello_android_dev* hdev = (struct hello_android_dev*)dev_get_drvdata(dev); //返回驱动数据的指针, 参数设备指针
return __hello_get_val(hdev, buf);
} /*写设备属性val*/
static ssize_t hello_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count) {
struct hello_android_dev* hdev = (struct hello_android_dev*return __hello_set_val(hdev, buf, count);
}
/*----------------------------------------------
device_attribute:
struct driver_attribute {
struct attribute attr;
ssize_t (*show)(struct device_driver *driver, char *buf);
ssize_t (*store)(struct device_driver *, const char * buf, size_t count);
};
Device drivers can export attributes via their sysfs directories.Drivers can declare attributes using a DRIVER_ATTR macro that works identically to the DEVICE_ATTR macro.实例:在GSENSOR 8452驱动中申明的属性,可以在ADB中查看到属性。
属性部分出口确实不太清楚
---------------------------------------------------*/
定义通过proc文件系统访问方法,主要实现了hello_proc_read和hello_proc_write两个方法,同时定义了在proc文件系统创建和删除文件的方法hello_create_proc和hello_remove_proc:
proc:在用户态检查内核状态的机制
/*读取设备寄存器val的值,保存在page缓冲区中*/
static ssize_t hello_proc_read(char* page, char** start, off_t off, int count, int* eof, void* data) {
/*off_t类型默认是32位的long int */
if(off > ) {
*eof = ;
return ;
} return __hello_get_val(hello_dev, page);
} /*把缓冲区的值buff保存到设备寄存器val中去*/
static ssize_t hello_proc_write(struct file* filp, const char __user *buff, unsigned long len, void* data) {
int err = ;
char* page = NULL; if(len > PAGE_SIZE) { //PAGE_SIZE这个如何定义,量多少?
printk(KERN_ALERT"The buff is too large: %lu.\n", len); //保存量超标
return -EFAULT;
} page = (char*)__get_free_page(GFP_KERNEL);
if(!page) {
/*没有足够的内存,你必须处理这种错误! */
printk(KERN_ALERT"Failed to alloc page.\n");
return -ENOMEM;
} /*先把用户提供的缓冲区值拷贝到内核缓冲区中去*/
if(copy_from_user(page, buff, len)) {
printk(KERN_ALERT"Failed to copy buff from user.\n");
err = -EFAULT;
goto out;
} err = __hello_set_val(hello_dev, page, len); out:
free_page((unsigned long)page);
return err; //为‘0’拷贝失败(与上面相反)
} /*创建/proc/hello文件*/
static void hello_create_proc(void) {
// proc_dir_entry: http://www.cnblogs.com/Ph-one/p/4411557.html struct proc_dir_entry* entry;
entry = create_proc_entry(HELLO_DEVICE_PROC_NAME, , NULL);
if(entry) {
entry->owner = THIS_MODULE;
entry->read_proc = hello_proc_read;
entry->write_proc = hello_proc_write;
}
} /*删除/proc/hello文件*/
static void hello_remove_proc(void) {
remove_proc_entry(HELLO_DEVICE_PROC_NAME, NULL);
}
/*--
struct file ──字符设备驱动相关重要结构
文件结构 代表一个打开的文件描述符,它不是专门给驱动程序使用的,系统中每一个打开的文件在内核中都有一个关联的struct file。它由内核在open时创建,并传递给在文件上操作的任何函数,知道最后关闭。当文件的所有实例都关闭之后,内核释放这个数据结构。
--*/
/*--
printk是在内核中运行的向控制台输出显示的函数,printk相当于printf的孪生姐妹.
--*/
最后,定义模块加载和卸载方法,这里只要是执行设备注册和初始化操作:
/*初始化设备*/
static int __hello_setup_dev(struct hello_android_dev* dev) {
int err;
dev_t devno = MKDEV(hello_major, hello_minor); //dev_t:unsigned int 类型,32位,用于在驱动程序中定义设备编号,高12位为主设备号,低20位为次设备号
//MKDEV:获取设备在设备表中的位置
memset(dev, , sizeof(struct hello_android_dev)); //清零 cdev_init(&(dev->dev), &hello_fops); //空间的申请,并且清零
dev->dev.owner = THIS_MODULE;
dev->dev.ops = &hello_fops; //动态内存定义初始化 /*注册字符设备,初始化后将它添加到系统中*/
err = cdev_add(&(dev->dev),devno, );
if(err) {
return err;
} /*初始化信号量和寄存器val的值*/
init_MUTEX(&(dev->sem));
dev->val = ; return ;
} /*模块加载方法*/
static int __init hello_init(void){
int err = -;
dev_t dev = ;
struct device* temp = NULL; printk(KERN_ALERT"Initializing hello device.\n"); /*动态分配主设备和从设备号*/
err = alloc_chrdev_region(&dev, , , HELLO_DEVICE_NODE_NAME);
if(err < ) {
printk(KERN_ALERT"Failed to alloc char dev region.\n");
goto fail;
} hello_major = MAJOR(dev);
hello_minor = MINOR(dev); /*分配hello设备结构体变量*/
hello_dev = kmalloc(sizeof(struct hello_android_dev), GFP_KERNEL);
if(!hello_dev) {
err = -ENOMEM;
printk(KERN_ALERT"Failed to alloc hello_dev.\n");
goto unregister;
} /*初始化设备*/
err = __hello_setup_dev(hello_dev);
if(err) {
printk(KERN_ALERT"Failed to setup dev: %d.\n", err);
goto cleanup;
} /*在/sys/class/目录下创建设备类别目录hello*/
hello_class = class_create(THIS_MODULE, HELLO_DEVICE_CLASS_NAME);
if(IS_ERR(hello_class)) { //IS_ERR检验函数,1:错,0:对
err = PTR_ERR(hello_class);
printk(KERN_ALERT"Failed to create hello class.\n");
goto destroy_cdev;
} /*在/dev/目录和/sys/class/hello目录下分别创建设备文件hello*/
temp = device_create(hello_class, NULL, dev, "%s", HELLO_DEVICE_FILE_NAME); //创建一个设备节点,节点名为:HELLO_DEVICE_FILE_NAME
if(IS_ERR(temp)) { // http://www.cnblogs.com/Ph-one/p/4414540.html
err = PTR_ERR(temp); //将指针转换成错误号
printk(KERN_ALERT"Failed to create hello device.");
goto destroy_class;
} /*在/sys/class/hello/hello目录下创建属性文件val*/
err = device_create_file(temp, &dev_attr_val);
if(err < ) {
printk(KERN_ALERT"Failed to create attribute val.");
goto destroy_device;
} dev_set_drvdata(temp, hello_dev); /*创建/proc/hello文件*/
hello_create_proc(); printk(KERN_ALERT"Succedded to initialize hello device.\n");
return ; destroy_device:
device_destroy(hello_class, dev);
destroy_class:
class_destroy(hello_class); //删class destroy_cdev:
cdev_del(&(hello_dev->dev)); //删 cleanup:
kfree(hello_dev); //kfree函数很简单,将Slab对象释放到阵列缓存中,如果缓存满了,则批量释放到Slab缓存中 unregister:
unregister_chrdev_region(MKDEV(hello_major, hello_minor), ); //释放hello_major在设备号,1个设备 fail:
return err;
} /*模块卸载方法*/
static void __exit hello_exit(void) { //__exit:标记退出代码,对于非模块无效
dev_t devno = MKDEV(hello_major, hello_minor);
/*dev_t:unsigned int 类型,32位,用于在驱动程序中定义设备编号,高12位为主设备号,低20位为次设备号*/ printk(KERN_ALERT"Destroy hello device.\n"); //驱动破坏 /*删除/proc/hello文件*/
hello_remove_proc(); /*销毁设备类别和设备*/
if(hello_class) {
device_destroy(hello_class, MKDEV(hello_major, hello_minor));
class_destroy(hello_class);
} /*删除字符设备和释放设备内存*/
if(hello_dev) {
cdev_del(&(hello_dev->dev)); //释放 cdev占用的内存
kfree(hello_dev);
} /*释放设备号*/
unregister_chrdev_region(devno, ); //当用到这个函数的时候,就不能用杂项设备misc,中的 misc_deregister(&tiny4412_led_dev);杂项设备相当于设备号为10的字符设备
} MODULE_LICENSE("GPL"); //模块许可证
MODULE_DESCRIPTION("First Android Driver"); //模块描述 module_init(hello_init); // module_init除了初始化加载之外,还有后期释放内存的作用
module_exit(hello_exit);
/*module_exit的参数卸载时同__init类似,如果驱动被编译进内核,则__exit宏会忽略清理函数,因为编译进内核的模块不需要做清理工作,显然__init和__exit对动态加载的模块是无效的,只支持完全编译进内核*/
/*--
/* 判断返回的指针是错误信息还是实际地址,即指针是否落在最后一页
是实际地址:落在最后一页,返回‘0’
不是实际地址:没有落在最后一页,返回‘1’
*/
static inline long IS_ERR(const void *ptr) //☆☆
{
return IS_ERR_VALUE((unsigned long)ptr);
}
--*/
/*--
--*/
以上是通过devfs和proc两种方式访问设备属性;
五.在hello目录中新增Kconfig和Makefile两个文件,其中Kconfig是在编译前执行配置命令 make menuconfig 时用到的,而Makefile是执行编译命令make是用到的:
Kconfig文件的内容
在Ubuntu上为Android系统编写Linux内核驱动程序(老罗学习笔记1)的更多相关文章
- 在Ubuntu上为Android系统编写Linux内核驱动程序
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6568411 在智能手机时代,每个品牌的手机都有 ...
- 在ubuntu上为android系统编写Linux驱动程序【转】
本文转载自:http://blog.csdn.net/luoshengyang/article/details/6568411 在智能手机时代,每个品牌的手机都有自己的个性特点.正是依靠这种与众不同的 ...
- 在Ubuntu上为Android系统内置C可执行程序测试Linux内核驱动程序(老罗学习笔记2)
在前一篇文章中,我们介绍了如何在Ubuntu上为Android系统编写Linux内核驱动程序.在这个名为hello的Linux内核驱动程序中,创建三个不同的文件节点来供用户空间访问,分别是传统的设备文 ...
- 在Ubuntu上为Android增加硬件抽象层(HAL)模块访问Linux内核驱动程序(老罗学习笔记3)
简单来说,硬件驱动程序一方面分布在Linux内核中,另一方面分布在用户空间的硬件抽象层中.接着,在Ubuntu上为Android系统编写Linux内核驱动程序(老罗学习笔记1)一文中举例子说明了如何在 ...
- 在Ubuntu上为Android系统内置Java应用程序测试Application Frameworks层的硬件服务(老罗学习笔记6)
一:Eclipse下 1.创建工程: ---- 2.创建后目录 3.添加java函数 4.在src下创建package,在package下创建file 5.res---layout下创建xml文件,命 ...
- linux内核分析第四周学习笔记
linux内核分析第四周学习笔记 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.co ...
- Linux内核分析第二周学习笔记
linux内核分析第二周学习笔记 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.co ...
- linux内核分析第一周学习笔记
linux内核分析第一周学习笔记 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.co ...
- 在Ubuntu为Android硬件抽象层(HAL)模块编写JNI方法提供Java访问硬件服务接口(老罗学习笔记4)
在上两篇文章中,我们介绍了如何为Android系统的硬件编写驱动程序,包括如何在Linux内核空间实现内核驱动程序和在用户空间实现硬件抽象层接口.实现这两者的目的是为了向更上一层提供硬件访问接口,即为 ...
随机推荐
- [Java][RCP] 引入第三方jar包时出错: XXXcannot be found XXX
为什么会这样? 下面的博客有介绍,不在累赘 http://dengmin.iteye.com/blog/260585 这些博客貌似忘掉了一点,或者是我本地的Eclipse新建的项目Version不够高 ...
- ibatis.net demo
1. download ibatis.nethttps://code.google.com/p/mybatisnet/ 2. add all dll as reference to your proj ...
- iOS 下拉菜单 FFDropDownMenu自定义下拉菜单样式实战-b
Demo地址:https://github.com/chenfanfang/CollectionsOfExampleFFDropDownMenu框架地址:https://github.com/chen ...
- 对frameset、frame、iframe的js操作
框架编程概述一个HTML页面可以有一个或多个子框架,这些子框架以<iframe>来标记,用来显示一个独立的HTML页面.这里所讲的框架编程包括框架的自我控制以及框架之间的互相访问,例如从一 ...
- 【转】android 选取图片
转自:http://www.cnblogs.com/top5/archive/2012/03/06/2381986.html 这几天 在学习并开发android系统的图片浏览 音频 视频 的浏览 由于 ...
- ffmpeg 打开视频流太慢(下)
前面的博文中已经交代过,ffmpeg打开视频慢主要是因为av_find_stream_info 耗时久.下面给出重写查找音视频stream info的一段代码,用来替代av_find_stream_i ...
- CSS3技巧:利用css3径向渐变做一张优惠券(转)
在很多购物网站上都能看到优惠券,代金券,什么什么的券,但基本都是图片直接放上去,那么你有没有想过css来做一个呢,反正我是这样想过.那么你怎么做呢,切图做背景平铺边缘,嗯,有这样想过,如今css3技术 ...
- TesserOCR训练
1.CMD命令行进入 图片目录.运行: tesseract.exe testcode.tif testcode batch.nochop makebox 注意:上面的 testcode 名称 必须保持 ...
- Eclipse plugin插件开发 NoClassDefFoundError
Eclipse的每一个plugin都有属于自己的类加载器,这是OSGI架构的基础,每一个plugin项目都是一个bundle,独立运行在各自的运行环境里面,这就造成了开发时和运行时的不同. Eclip ...
- 微软发布WP SDK8.0 新增语音、应用内支付等原生API
http://www.csdn.net/article/2012-10-31/2811338-windows-phone-8-sdk 京时间10月30日,微软在旧金山举行新一代手机操作系统Window ...