两年前实习时的文档——MMC学习总结
1概述
驱动程序实际上是硬件与应用程序之间的中间层。在Linux操作系统中,设备驱动程序对各种不同的设备提供了一致的訪问接口,把设备映射成一个特殊的设备文件,用户程序能够像其它文件一样对设备文件进行操作。
Linux2.6引入了新的设备管理机制kobject,通过这个数据结构使全部设备在底层都具有统一的接口,kobject提供主要的对象管理,是构成Linux2.6设备模型的核心结构,它与sysfs文件系统紧密联系,每一个在内核中注冊的kobject对象都相应于sysfs文件系统中的一个文件夹。
在这些内核对象机制的基础上,Linux的设备模型包含设备结构device、驱动程序driver、总线bus和设备类结构class几个关键组件。
一个现实的linux设备和驱动通常都须要挂接在一种总线上,比較常见的总线有USB、PCI总线等。可是,在嵌入式系统里面,SoC系统中集成的独立的外设控制器、挂接在SoC内存空间的外设却不依附于此类总线。基于这种背景下,2.6内核增加了platform虚拟总线。Platform机制将设备本身的资源注冊进内核,由内核统一管理,在驱动程序使用这些资源时使用统一的接口,这样提高了程序的可移植性。
Platform设备概念的引入是可以更好地描写叙述设备的资源信息。Platform设备是系统中自治的实体,包含基于port的设备、外围总线和集成入片上系统平台的大多数控制器,它们通常直接通过CPU的总线寻址。每一个platform设备被赋予一个名称,并分配一定数量的资源。
Platform总线对增加到该总线的设备和驱动分别封装了结构体——platform_device和platform_driver而且提供了相应的注冊函数。
图1 Platform虚拟总线
由上图可知,在platform虚拟总线上我们分别对device和driver进行注冊,这样我们可以更加方便的进行驱动设备的管理。这样当有总线或者设备注冊到该虚拟总线上时,内核自己主动的调用platform_match函数将platform_device绑定到platform_driver上。
2 SDIO启动过程
在kernel启动时,内核会自己主动的调用MODULE_INIT宏对模块进行载入,MODULE_INIT声明了模块的入口函数。在MMC中我们模块的运行顺序例如以下图所看到的:
图2 SDIO启动过程
在这个过程中内核首先调用xxx_init函数,init函数对device进行注冊,不管什么设备在内核中都会调用driver_register函数,driver_register函数经过一系列的调用,终于会调用探測函数probe(后面具体解说)。probe函数会对模块进行探測工作,不断的对SD/MMC/SDIO卡进行扫描。
3 platform相关
3.1 platform数据结构
Linux在启动的时候就注冊了platform总线,看内核源代码:
struct bus_type platform_bus_type = {
.name ="platform",
.dev_attrs = platform_dev_attrs,
.match =platform_match,
.uevent =platform_uevent,
.pm =&platform_dev_pm_ops,
};
能够看到,总线中定义了.match函数,当有总线或者设备注冊到platform总线时,内核自己主动调用.match函数,推断device和driver的name是否一致。
platform_device的结构体定义例如以下:
struct platform_device {
constchar * name;//设备名字,这将取代device->dev_id,用作sys/device下显示文件夹名
int id;//设备ID,用于给插入该总线而且具有同样name的设备编号,假设仅仅有一个设备的话填-1
structdevice dev;//结构体中内嵌的device结构体
u32 num_resources;//资源数
structresource * resource;//用于存放资源的数组
conststruct platform_device_id *id_entry;
/*arch specific additions */
structpdev_archdata archdata;
};
能够看出,在platform_device中定义了name,而且内嵌了structdevice结构体。另外,包括的structresource例如以下:
struct resource {
resource_size_tstart;
resource_size_tend;
constchar *name;
unsignedlong flags;
structresource *parent, *sibling, *child;
};
platform_driver的结构体定义例如以下:
struct platform_driver {
int(*probe)(struct platform_device *);
int(*remove)(struct platform_device *);
void(*shutdown)(struct platform_device *);
int(*suspend)(struct platform_device *, pm_message_t state);
int(*resume)(struct platform_device *);
structdevice_driver driver;
conststruct platform_device_id *id_table;
};
能够看出,在platform_driver中内嵌了structdevice_driver,以及一些回调函数。structdevice_driver的详细定义例如以下:
struct device_driver {
constchar *name;
int(*probe) (struct device *dev);
int(*remove) (struct device *dev);
void(*shutdown) (struct device *dev);
int(*suspend) (struct device *dev, pm_message_t state);
int(*resume) (struct device *dev);
conststruct attribute_group **groups;
conststruct dev_pm_ops *pm;
structdriver_private *p;
};
此处定义了name用于和platform_device匹配。由于platform_device和platform_driver的匹配就是通过内嵌的structdevice和structdevice_driver的name进行匹配的。
static int platform_match(structdevice *dev, struct device_driver *drv)
{
structplatform_device *pdev = to_platform_device(dev);
structplatform_driver *pdrv = to_platform_driver(drv);
/*match against the id table first */
if(pdrv->id_table)
returnplatform_match_id(pdrv->id_table, pdev) != NULL;
/*fall-back to driver name match */
return(strcmp(pdev->name, drv->name) == 0);
}
在数据结构设计上,总线、设备及驱动三者相互关联platform device包括device,依据device能够获得对应的bus及driver。
设备加入到总线上之后形成一个双向循环链表,依据总线能够获得其上挂接的全部device,进而获得了platform device。依据device也能够获得驱动该总线上全部设备对应的driver。Platform包括driver,依据driver获得对应的bus,进而获得bus上全部的device,进一步获得platformdevice。依据name对driver和platform device进行匹配,匹配成功后将device与对应的driver关联起来,即实现了platformdevice与platformdriver的关联。
匹配成功后调用driver的probe进而调用platformdriver的probe,在probe里实现驱动特定的功能。
Match函数仅仅是简单的进行字符串匹配,这就是强调platformdevice和platform driver中那么属性须要一致的原因。
3.2 platform device
3.2.1 register
注冊一个platform层的设备。注冊后,会在sys/device文件夹下创建一个以name命名的文件夹,而且创建软连接到/sys/bus/platform/device下。
int platform_device_register(structplatform_device *pdev)
{
device_initialize(&pdev->dev);
returnplatform_device_add(pdev);
}
当中,第一步device_initialize(在kernel目录下)用于初始化一个structdevice。函数的定义例如以下:
void device_initialize(struct device*dev)
{
dev->kobj.kset= devices_kset;
kobject_init(&dev->kobj,&device_ktype);
INIT_LIST_HEAD(&dev->dma_pools);
mutex_init(&dev->mutex);
lockdep_set_novalidate_class(&dev->mutex);
spin_lock_init(&dev->devres_lock);
INIT_LIST_HEAD(&dev->devres_head);
device_pm_init(dev);
set_dev_node(dev,-1);
}
第二步,加入一个platform device到device层 。函数的定义例如以下:
int platform_device_add(structplatform_device *pdev)
{
inti, ret = 0;
if(!pdev)
return -EINVAL;
if(!pdev->dev.parent)
pdev->dev.parent= &platform_bus;//假设p->dev.parent不存在则赋值&platform_bus
pdev->dev.bus= &platform_bus_type;//设置pdev->dev.bus的bus类型
if(pdev->id != -1) //pdev->id!=-1说明存在多于一个的设备
dev_set_name(&pdev->dev,"%s.%d", pdev->name, pdev->id);
Else //否则对唯一的设备进行命名
dev_set_name(&pdev->dev,"%s", pdev->name);
for(i = 0; i < pdev->num_resources; i++) {
//遍历资源而且资源增加到资源数组中
struct resource *p, *r =&pdev->resource[i];
if (r->name == NULL)
r->name= dev_name(&pdev->dev);
p = r->parent;
if (!p) {
if(resource_type(r) == IORESOURCE_MEM)
p = &iomem_resource;
elseif (resource_type(r) == IORESOURCE_IO)
p = &ioport_resource;
}
if (p && insert_resource(p, r)) {
printk(KERN_ERR
"%s: failed to claim resource%d\n",
dev_name(&pdev->dev), i);
ret= -EBUSY;
gotofailed;
}
}
pr_debug("Registeringplatform device '%s'. Parent at %s\n",
dev_name(&pdev->dev),dev_name(pdev->dev.parent));
ret= device_add(&pdev->dev);//(在core/core.c中定义)
if(ret == 0)
return ret;
failed:
while(--i >= 0) {
struct resource *r =&pdev->resource[i];
unsigned long type = resource_type(r);
if (type == IORESOURCE_MEM || type ==IORESOURCE_IO)
release_resource(r);
}
returnret;
}
3.2.2 unregister
platform_device_unregister用于注销一个platform-leveldevice。函数的定义例如以下:
voidplatform_device_unregister(struct platform_device *pdev)
{
platform_device_del(pdev);
platform_device_put(pdev);
}
在注销一个设备时,第一步调用platform_device_del函数。此函数的定义例如以下:
void platform_device_del(struct platform_device*pdev)
{
inti;
if(pdev) {
device_del(&pdev->dev);
//遍历全部的resource假设是IORESOURCE_MEM或者IORESOURCE_IO类型则调用release_resource函数释放掉resource。
for (i = 0; i <pdev->num_resources; i++) {
structresource *r = &pdev->resource[i];
unsignedlong type = resource_type(r);
if(type == IORESOURCE_MEM || type == IORESOURCE_IO)
release_resource(r);
}
}
}
此函数的作用是:移除一个platform-leveldevice。当中的IOSOURCE_MEM和IORESOURCE_IR是CPU对外设I/O端口物理地址的两种编制方式。Resource是一个指向platform资源数组的指针,该数组有num_resource个资源,以下是资源结构体的定义(linux/ioport.h):
struct resource {
resource_size_tstart; //起始地址
resource_size_tend; //终止地址
constchar *name; //名称
unsignedlong flags; //标志
structresource *parent, *sibling, *child;
};
在structplatform_device中能够设置多种资源信息。资源的flags标志包含:
#define IORESOURCE_IO 0x00000100 //IO资源
#define IORESOURCE_MEM 0x00000200 //内存资源
#define IORESOURCE_IRQ 0x00000400 //中断资源
#define IORESOURCE_DMA 0x00000800 //DMA资源
第二步,调用platform_device_put函数。
void platform_device_put(structplatform_device *pdev)
{
if(pdev)
put_device(&pdev->dev);
}
销毁一个platformdevice,而且释放全部与这个platformdevice相关的内存。
3.3 platform driver
3.3.1 register
通过调用函数platform_driver_register实现为platformlevel的设备注冊一个驱动。注冊成功后,内核会在/sys/bus/platform/driver/文件夹下创建一个名字为driver->name的文件夹。详细的函数定义例如以下:
int platform_driver_register(structplatform_driver *drv)
{
drv->driver.bus= &platform_bus_type;
if(drv->probe)
drv->driver.probe =platform_drv_probe;
if(drv->remove)
drv->driver.remove =platform_drv_remove;
if(drv->shutdown)
drv->driver.shutdown =platform_drv_shutdown;
returndriver_register(&drv->driver);
}
此函数首先对struct platform_driver变量的driver进行赋值。然后调用driver_register而且返回。driver_register函数的定义例如以下:
int driver_register(structdevice_driver *drv)
{
intret;
structdevice_driver *other;
BUG_ON(!drv->bus->p);
//假设driver的方法和总线上的方法不能匹配则驱动的名称须要更新
if((drv->bus->probe && drv->probe) ||
(drv->bus->remove &&drv->remove) ||
(drv->bus->shutdown &&drv->shutdown))
printk(KERN_WARNING "Driver '%s'needs updating - please use "
"bus_typemethods\n", drv->name);
other= driver_find(drv->name, drv->bus);
if(other) {
put_driver(other);
printk(KERN_ERR "Error: Driver '%s'is already registered, "
"aborting...\n",drv->name);
return -EBUSY;
}
ret= bus_add_driver(drv);//将驱动载入到总线上,假设成功就返回
if(ret)
return ret;
ret= driver_add_groups(drv, drv->groups);
if(ret)
bus_remove_driver(drv);
returnret;
}
3.3.2 unregister
通过调用函数platform_driver_unregister函数实现platform_driver级设备的注销。
void platform_driver_unregister(structplatform_driver *drv)
{
driver_unregister(&drv->driver);
}
在此函数中调用了driver_unregister函数,将驱动从系统中移除。函数的详细定义例如以下:
void driver_unregister(structdevice_driver *drv)
{
if(!drv || !drv->p) {
WARN(1, "Unexpected driverunregister!\n");
return;
}
driver_remove_groups(drv,drv->groups);
bus_remove_driver(drv);
}
分两步走,第一步调用driver_remove_groups。函数的详细定义例如以下:
static voiddriver_remove_groups(struct device_driver *drv,
const struct attribute_group **groups)
{
inti;
if(groups)
for (i = 0; groups[i]; i++)
sysfs_remove_group(&drv->p->kobj,groups[i]);
}
当中,调用了
static inline voidsysfs_remove_group(struct kobject *kobj,
const struct attribute_group *grp)
{
}
第二步从总线中将驱动删除。调用例如以下函数:
void bus_remove_driver(structdevice_driver *drv)
{
if(!drv->bus)
return;
if(!drv->suppress_bind_attrs)
remove_bind_files(drv);
driver_remove_attrs(drv->bus,drv);
driver_remove_file(drv,&driver_attr_uevent);
klist_remove(&drv->p->knode_bus);
pr_debug("bus:'%s': remove driver %s\n", drv->bus->name, drv->name);
driver_detach(drv);
module_remove_driver(drv);
kobject_put(&drv->p->kobj);
bus_put(drv->bus);
}
此函数的作用是:将驱动从它控制的设备中卸载,而且从驱动的总线链表中将它移除。
4probe函数
在kernel载入模块的时候启动了设备注冊函数,由于全部的设备的driver都继承自device_driver,所以从driver_register看起,函数的源代码例如以下:
int driver_register(structdevice_driver * drv)
{
if ((drv->bus->probe &&drv->probe) ||
(drv->bus->remove &&drv->remove) ||
(drv->bus->shutdown &&drv->shutdown)) {
printk(KERN_WARNING "Driver '%s'needs updating - please use bus_type methods\n", drv->name);
}
klist_init(&drv->klist_devices,NULL, NULL);
return bus_add_driver(drv);
}
klist_init不相关,不用管他,详细再去看bus_add_driver:
int bus_add_driver(structdevice_driver *drv)
{
1.先kobject_set_name(&drv->kobj,"%s", drv->name);
2.再kobject_register(&drv->kobj)
3.然后调用了:driver_attach(drv)
}
int driver_attach(struct device_driver * drv)
{
return bus_for_each_dev(drv->bus,NULL, drv, __driver_attach);
}
真正起作用的是__driver_attach:
static int __driver_attach(structdevice * dev, void * data)
{
……
if (!dev->driver)
driver_probe_device(drv, dev);
……
}
int driver_probe_device(struct device_driver * drv, struct device * dev)
{
……
//1.先是推断bus是否match:
if (drv->bus->match &&!drv->bus->match(dev, drv))
goto done;
//2.再详细运行probe:
ret = really_probe(dev, drv);
……
}
really_probe才是我们要找的函数:
static int really_probe(struct device *dev, struct device_driver *drv)
{
……
//1.先是调用的驱动所属总线的probe函数:
if (dev->bus->probe) {
ret = dev->bus->probe(dev);
if (ret)
goto probe_failed;
} else if (drv->probe) {
//2.再调用你的驱动中的probe函数:
ret = drv->probe(dev);
if (ret)
goto probe_failed;
}
……
}
当中,drv->probe(dev),才是真正调用的驱动实现的详细的probe函数。从此出開始probe函数正式被调用。
至此,platform成功挂接到platform bus上了,并与特定的设备实现了绑定,并对设备进行了probe处理。
5request函数
当SDIO设备启动之后,probe函数会调用mmc_alloc_host函数不断的检測连接的MMC/SD/SDIO卡,而且通过device_add完毕对设备的加入,当设备加入完毕之后,会调用mmc_blk_probe完毕驱动定义的特定功能。函数的调用关系图例如以下:
图3 request调用过程
Request函数的定义例如以下:
static void
v8sdio_request( struct mmc_host *mmc,struct mmc_request *mrq )
{
unsignedlong iflags = 0;
structv8sdio_host *host = mmc_priv( mmc );//首先将struct mmc_host设备转化成
//struct v8sdio_host
#if IRQ_STAT_DBG
if(host->id == DEBUG_CHN )
{
do_gettimeofday( &tv_now );//获取时间
timersub( &tv_now, &tv_last,&tv_delta );
if( tv_delta.tv_sec >= 5 )
{
u32i = 0;
tv_last.tv_sec = tv_now.tv_sec;
// tv_last.tv_usec= tv_now.tv_usec;
printk("\n" );
printk("[sdio%d_irq]: irq_ALL = %u\n", host->id, host->irq_cnt[0] );
printk("[sdio%d_irq]: irq_ERR = %u\n", host->id, host->irq_cnt[1] );
printk("[sdio%d_irq]: irq_DMA = %u\n", host->id, host->irq_cnt[2] );
printk("[sdio%d_irq]: irq_CMD = %u\n", host->id, host->irq_cnt[3] );
printk("[sdio%d_irq]: irq_TRN = %u\n", host->id, host->irq_cnt[4] );
for(i = 0; i <= MAX_OPCODE; i++ )
{
if( cmd_stats[i] )
{
printk( "[sdio%d_cmd]: cmd[%02u] =%u\n", host->id, i, cmd_stats[i] );
}
}
}
}
#endif
V8LOGV(V8TAG_SDIO, "" );
if(host->mrq )
V8LOGW( V8TAG_SDIO, "[Ch%d]host->mrq is NOT NULL.", host->id );
clk_enable(host->aclk ); //使能主机aclk
host->mrq= mrq; //请求队列赋值
local_irq_save(iflags); //保存本地中断标志
v8sdio_prepare_data(host, mrq ); //开启DMA通道,而且填充lli
v8sdio_start_command(host, mrq->cmd, mrq->data ); //開始运行命令
local_irq_restore(iflags); //又一次保存本地中断标志
}
6rescan过程
内核通过mmc_rescan(drivers/mmc/core/core.c)不断扫描MMC/SD卡:
void mmc_rescan(struct work_struct*work)
{
structmmc_host *host =
container_of(work, struct mmc_host,detect.work);
u32ocr;
interr;
unsignedlong flags;
intextend_wakelock = 0;
spin_lock_irqsave(&host->lock,flags);
if(host->rescan_disable) {
spin_unlock_irqrestore(&host->lock,flags);
return;
}
spin_unlock_irqrestore(&host->lock,flags);
mmc_bus_get(host); //取得总线
/*假设是个已经注冊过的卡, 检查它是否存在 */
if((host->bus_ops != NULL) && host->bus_ops->detect &&!host->bus_dead)
host->bus_ops->detect(host);
/*假设卡已经被移除,总线将被标记为死卡
* —声明唤醒锁
* 使得用户空间可以对应*/
if(host->bus_dead)
extend_wakelock = 1;
mmc_bus_put(host);
mmc_bus_get(host);
/*假设当前还有卡,将它停止*/
if(host->bus_ops != NULL) {
mmc_bus_put(host);
goto out;
}
/*检查新插入的卡 */
/*
* 仅仅有我们可以加入新的处理器, 所以在这里释放锁是安全的。
*/
mmc_bus_put(host);
if(host->ops->get_cd && host->ops->get_cd(host) == 0)
goto out;
mmc_claim_host(host);
mmc_power_up(host);
#ifdef CONFIG_MMC_VC088X
if(host->caps & MMC_CAP_SDIO_IRQ){
sdio_reset(host);
}
#else
sdio_reset(host);
#endif
mmc_go_idle(host);//发送CMD0使卡进入IDLE状态
mmc_send_if_cond(host,host->ocr_avail);
/*
* 首先我们搜索SDIO...
*/
err= mmc_send_io_op_cond(host, 0, &ocr);
if(!err) {
if (mmc_attach_sdio(host, ocr))
mmc_power_off(host);
extend_wakelock = 1;
goto out;
}
/*
* ...然后普通的SD...
*/
err= mmc_send_app_op_cond(host, 0, &ocr);
if(!err) {
if (mmc_attach_sd(host, ocr))
mmc_power_off(host);
extend_wakelock = 1;
goto out;
}
/*
* ...最后MMC.
*/
err= mmc_send_op_cond(host, 0, &ocr);
if(!err) {
if (mmc_attach_mmc(host, ocr))
mmc_power_off(host);
extend_wakelock = 1;
goto out;
}
mmc_release_host(host);
mmc_power_off(host);
out:
if(extend_wakelock)
wake_lock_timeout(&mmc_delayed_work_wake_lock,HZ / 2);
else
wake_unlock(&mmc_delayed_work_wake_lock);
if(host->caps & MMC_CAP_NEEDS_POLL)
mmc_schedule_delayed_work(&host->detect,HZ);
}
在设置完每一个卡进入空状态,而无论当前卡是存在于何种状态之后调用了mmc_send_if_cond命令,此命令的定义例如以下:
int mmc_send_if_cond(struct mmc_host*host, u32 ocr)
{
structmmc_command cmd;
interr;
staticconst u8 test_pattern = 0xAA;
u8result_pattern;
/*
* To support SD 2.0 cards, we must alwaysinvoke SD_SEND_IF_COND
* before SD_APP_OP_COND. This command willharmlessly fail for
* SD 1.0 cards.
*/
cmd.opcode= SD_SEND_IF_COND;
cmd.arg= ((ocr & 0xFF8000) != 0) << 8 | test_pattern;
cmd.flags= MMC_RSP_SPI_R7 | MMC_RSP_R7 | MMC_CMD_BCR;
err= mmc_wait_for_cmd(host, &cmd, 0);
if(err)
return err;
if(mmc_host_is_spi(host))
result_pattern = cmd.resp[1] & 0xFF;
else
result_pattern = cmd.resp[0] & 0xFF;
if(result_pattern != test_pattern)
return -EIO;
return0;
}
流程图例如以下:
图7 SD卡的状态图
⑴取得总线
⑵检查总线操作结构指针bus_ops,假设为空,则又一次利用各总线对port进行扫描,检測顺序依次为:SDIO、NormalSD、MMC。当检測到相应的卡类型后,就使用mmc_attach_bus()把相相应的总线操作与host连接起来。
voidmmc_attach_bus(struct mmc_host *host, const struct mmc_bus_ops *ops)
{
...
host->bus_ops = ops;
...
}
⑶初始化卡按下面流程初始化:
①发送CMD0使卡进入IDLE状态
②发送CMD8,检查卡是否SD2.0。SD1.1是不支持CMD8的,因此在SD2.0Spec中提出了先发送CMD8,如响应为无效命令,则卡为SD1.1,否则就是SD2.0(请參考SD2.0Spec)。
③发送CMD5读取OCR寄存器。
④发送ACMD55、CMD41,使卡进入工作状态。MMC卡并不支持ACMD55、CMD41,假设这步通过了,则证明这张卡是SD卡。
⑤假设d步骤错误,则发送CMD1推断卡是否为MMC。SD卡不支持CMD1,而MMC卡支持,这就是SD和MMC类型的推断根据。
⑥假设ACMD41和CMD1都不能通过,那这张卡恐怕就是无效卡了,初始化失败。
两年前实习时的文档——MMC学习总结的更多相关文章
- 两年前实习时的文档——Platform学习总结
1 概述 驱动程序实际上是硬件与应用程序之间的中间层.在Linux操作系统中,设备驱动程序对各种不同的设备提供了一致的訪问接口,把设备映射成一个特殊的设备文件,用户程序能够像其它文件一样对设备文件进 ...
- webpack搭建vue项目开发环境【文档向学习】
为何有这篇文章 各个社区已经有无数篇帖子介绍如何使用webpack搭建前端项目,但无论是出于学习webpack的目的还是为了解决工作实际需要都面临着一个现实问题,那就是版本更新.别人的帖子可能刚写好版 ...
- 使用Git Wiki 管理文档时,文档编写的基本用法
自己初次接触GitLab,通过百度和自己查找资料,了解了一部分.在自己的工作中,主要用到GitLab的Wiki文档版本管理能力.我总结了一小部分文本编辑需要用到的东西. 一.文本的排版 为了让文本/文 ...
- [知识积累]python3使用xlwt时写入文档字体颜色和边框样式
可借鉴的网址:https://www.programcreek.com/python/example/39979/xlwt.Alignment 可以直接通过pip安装xlwt 个人理解: xlwt中对 ...
- 如何在使用itext生成pdf文档时给文档添加背景图片
这个问题我在网上搜了很久,没有找到什么解决方案,需求其实很简单,就是添加背景图片.在解决这个问题之前,我们需要了解什么是背景图片?背景图片就是位于文档最底层的图片,文字和其他内容可以浮在它的上面.这又 ...
- C# 动态生成word文档 [C#学习笔记3]关于Main(string[ ] args)中args命令行参数 实现DataTables搜索框查询结果高亮显示 二维码神器QRCoder Asp.net MVC 中 CodeFirst 开发模式实例
C# 动态生成word文档 本文以一个简单的小例子,简述利用C#语言开发word表格相关的知识,仅供学习分享使用,如有不足之处,还请指正. 在工程中引用word的动态库 在项目中,点击项目名称右键-- ...
- Unity shader 官网文档全方位学习(一)
转载:https://my.oschina.net/u/138823/blog/181131 摘要: 这篇文章主要介绍Surface Shaders基础及Examples详尽解析 What?? Sha ...
- bootstrap2文档的学习
就像刚开始的 优雅,直观,强大的前端框架,让web开发更快,更容易,bootstrap给我的感觉就是把常用的布局,组件(导航,列表,按钮,表格),还有规范化颜色等等,同时它的遍历不至于此,他还支持了自 ...
- bash帮助文档简单学习;bash手册翻译
关于bash的四种工作方式的不同,可以参考:http://feihu.me/blog/2014/env-problem-when-ssh-executing-command-on-remote/,但是 ...
随机推荐
- HDU 2504 又见GCD
又见GCD Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Total Subm ...
- PageRank算法MapReduce实现
如果你现在需要计算网页的排名只有4一:数据如下面的: baidu 10.00 google,sina,nefu google 10.00 baidu sina 10.00 google nefu 10 ...
- Red Gate系列之五 .NET Reflector 7.6.1.824 Edition .NET程序反编译神器(附插件安装教程2012-10-13更新) 完全破解+使用教程
原文:Red Gate系列之五 .NET Reflector 7.6.1.824 Edition .NET程序反编译神器(附插件安装教程2012-10-13更新) 完全破解+使用教程 Red Gate ...
- SplitContainer如何实现左侧导航,正确显示和导航内容
关于这种类型的设计有很多的实现,这样,我首先解释一下我使用: 我的原则是实现方式,将form嵌panel在,作为一个子窗口. 如下面的代码细节: Form form = new DataSelect( ...
- java实现xml文件CRUD
java删除xml多个节点: 方案1.你直接改动了nodeList.这一般在做循环时是不同意直接这么做的. 你能够尝试在遍历一个list时,在循环体同一时候删除list里的内容,你会得到一个异常.建议 ...
- UVa 825 - Walking on the Safe Side
题目:在一个N*M的网格中,从左上角走到右下角,有一些点不能经过,求最短路的条数. 分析:dp,帕斯卡三角.每一个点最短的就是走N条向下,M条向右的路. 到达每一个点的路径条数为左边和上面的路径之和. ...
- AutoFac使用方法总结:Part III
生命周期 AutoFac中的生命周期概念非常重要,AutoFac也提供了强大的生命周期管理的能力. AutoFac定义了三种生命周期: Per Dependency Single Instance P ...
- 利用Pattern和Mather来禁止特殊字符的输入
String regEx="[`~!@#$%^&*()+=|{}':;',\\[\\].<>/?~!@#¥%……&*()——+|{}[]‘::”“’.,.?]&q ...
- 三种方式上传文件-Java
前言:负责,因为该项目他(jetty嵌入式开始SpringMvc)实现文件上传的必要性,并拥有java文件上传这一块还没有被曝光.并 Http 更多晦涩协议.因此,这种渐进的方式来学习和实践上载文件的 ...
- 【课程分享】基于plusgantt的项目管理系统实战开发(Spring3+JDBC+RMI的架构、自己定义工作流)
基于plusgantt的项目管理系统实战开发(Spring3+JDBC+RMI的架构.自己定义工作流) 课程讲师:张弘 课程分类:Java 适合人群:中级 课时数量:37课时 用到技术:Spring ...