已阅读5页,还剩18页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
WIFI 分析SDIO_WIFI分析WIFI之我见作者:黄树新时间:2011-11-18地点:华清远见深圳分中心SDIO_WIFI整个设备分为两个部分,一个是SD卡,一个是WIFI。SD卡部分主要涉及的重点在与如何识别SD卡和支持热拔插,而WIFI部分主要的重点在于发送和接收数据,现在,由小弟带着大家走一遭,本文涉及内容完全属于个人对WIFI部分理解,属于个人观点,如有错误,欢迎指导纠正。WIFI驱动属于网络设备驱动,那么我们首先从它的结构出发,要了解它的结构,我们首先了解一般网络设备驱动的结构。下图1是LINUX下网络驱动程序的体系结构:数据包发送dev_queue_xmit()数据包接收netif_rx()数据包发送hard_start_xmit()中断处理(数据包接收)struct net_device网络物理设备媒介网络协议接口层网络设备接口层网络驱动功能层网络设备与媒介层图11、 网络协议接口层想网络层协议提供一个统一的数据包收发接口,不论是上层协议为APP还是IP,都通过dev_queue_xmit()函数发送数据,并通过netif_rx()函数接收数据。这一层的存在使得上层协议独立于具体的设备。2、 网络设备接口层向协议接口层提供统一的用于描述具体网络设备属性和操作的结构体net_device,该结构体是设备驱动功能层中各函数的容器。实际上,网络设备接口层从宏观上规划了具体操作硬件的设备驱动功能层的结构。3、 设备驱动功能层函数是网络设备接口层net_device数据结构的具体成员,是驱使网络设备硬件完成相应动作的程序,它通过hard_start_xmit()函数启动发送操作,并通过网络设备上的中断触发接收操作。4、 网络设备与媒介层是完成数据包发送和接收的物理实体,包括网络适配器和具体的传输媒介,网络适配器被设备驱动层中得函数物理上的驱动。对于LINUX系统来说,网络设备和媒介可以是虚拟的。在设计具体的网络设备驱动程序时,我们需要完成的主要工作是编写设备驱动功能层的相关函数以填充net_device数据结构的内容并将net_device注册入内核。在进入正式驱动之前,还是要给大家补充一点基础知识,就是如何管理总线,设备,驱动之间关系的,关于bus_type、device_driver、device这三个内核结构,在内核代码中可以找到。由于这三个结构的重要性,我们在这里先将它们贴出来,我会用红色的注释标注。我在笔记中将会提到的一些结构成员以其代表的意义,这样便于对照。(我不得不承认,这三个结构的代码枯燥复杂。如果我误导了你,请指正我,所以我的建议是不妨先看完了笔记再来看这些结构)。1、 设备结构的定义:struct device struct klist klist_children; struct klist_node knode_parent; /* node in sibling list */ struct klist_node knode_driver; struct klist_node knode_bus;struct device *parent; struct kobject kobj; /kobject结构,关于这个结构与kset结构以及subsystem结构,笔记中会有描述。 char bus_idBUS_ID_SIZE; /* position on parent bus */ struct device_type *type; unsigned is_registered:1; unsigned uevent_suppress:1; struct semaphore sem; /* semaphore to synchronize calls to* its driver.*/ struct bus_type * bus; /* type of bus device is on /这个设备挂接的总线的类型 struct device_driver *driver; /* which driver has allocated this device */ /这个设备挂接的驱动 void *driver_data; /* data private to the driver */ void *platform_data; /* Platform specific data, device core doesnt touch it */ struct dev_pm_info power;#ifdef CONFIG_NUMA int numa_node; /* NUMA node this device is close to */#endif u64 *dma_mask; /* dma mask (if dmaable device) */ u64 coherent_dma_mask;/* Like dma_mask, but for alloc_coherent mappings as not all hardware supports 64 bit addresses for consistent allocations such descriptors. */ struct list_head dma_pools; /* dma pools (if dmable) */ struct dma_coherent_mem *dma_mem; /* internal for coherent mem override */ /* arch specific additions */ struct dev_archdata archdata; spinlock_t devres_lock; struct list_head devres_head; /* class_device migration path */ struct list_head node; struct class *class; dev_t devt; /* dev_t, creates the sysfs dev */ struct attribute_group *groups; /* optional groups */ void (*release)(struct device * dev);2、 设备驱动的结构:struct device_driver const char * name; /设备驱动的名字 struct bus_type * bus; /设备驱动挂接的总线的类型 struct kobject kobj; /kobject结构 struct klist klist_devices; /这个驱动对应的设备的链表 struct klist_node knode_bus; struct module * owner; const char * mod_name; /* used for built-in modules */ struct module_kobject * mkobj; 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);3、 总线结构:struct bus_type const char * name; /总线的名字 struct module * owner; struct kset subsys; /与该总线相关的subsystemstruct kset drivers; /挂接在该总线上的驱动的集合 struct kset devices; /挂接在该总线上的设备的集合 struct klist klist_devices; struct klist klist_drivers; struct blocking_notifier_head bus_notifier; struct bus_attribute * bus_attrs; /总线属性 struct device_attribute * dev_attrs; /设备属性 struct driver_attribute * drv_attrs; /驱动属性 int (*match)(struct device * dev, struct device_driver * drv); int (*uevent)(struct device *dev, struct kobj_uevent_env *env); 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 (*suspend_late)(struct device * dev, pm_message_t state); int (*resume_early)(struct device * dev); int (*resume)(struct device * dev); unsigned int drivers_autoprobe:1;我们注意到只有在bus_type结构中有kset结构,其他两个结构中则没有,我们知道kset结构是用于存放相同类型的kobject的,这究竟是个什么意思呢?kset又是为什么而存在的呢?为什么不能就是kobject呢?(关于kobject结构,我们很难抽象的形容,尽管它就是一个抽象的概念,我们将留待看代码的时候介绍,这里可以将kobject看成一个基类,kset就是容器了)。首先不管是设备还是驱动,都是挂接在某条总线上的,也就是说我们根据总线类型的不同来区分各种设备和驱动。(但是我们也要注意到,一个设备和驱动是可以挂接在不同的总线上的,比如网卡可以挂接在pci和sdio总线上,但这并不是说在linux设备模型中就可以同时挂接在两个总线上,我们只能选择其中的一种挂接)。在内核代码中我们找到了bus_type结构,我们发现了它使用了三个kset结构,分别是: struct kset subsys 、struct kset drivers 、struct kset devices。我们先抛开subsys因为它是用来向上挂接的。这里我们先看drivers与devices两个kset结构的意义。我们从发现一个设备或者驱动说起吧,一个设备或者驱动向内核注册的时候(对于设备来说就是被插入了;对于驱动来说就是.ko模块被加载了),对于每一次设备注册或者驱动注册,我们都得分配一个device结构或者device_drive结构,每一次我们都需要将device结构挂入drivers或devices(kset结构)链表中,这样我们能通过总线找到挂接在这个总线上的所有设备和驱动。但是这里我们注意到仅仅将设备们和驱动们挂接在总线上并不能表明设备和驱动之间的关系,这样的处理仅仅表明了驱动、设备与总线的关系,它们申明了我现在挂接在这条总线下,以后操作我就请通过这条总线。那么设备如何认出驱动,或者驱动如何认出设备呢?是的,我们是使用的probe函数。那么需要完成哪些过程了?在这些结构总是如何关联的?这才是我们关心的问题。所以这里我们将不得不在说明一下klist结构的作用。在内核代码中我们再次找到了device结构和device_drive结构。我们注意到在device结构中存在一个struct device_driver *driver这样的声明,而在device_drive中却并没有同样的包含device结构。我们这样想就明白了:对于一个设备来说,我们只能绑定一个驱动;而对于一个驱动来说,我们是可以对应于多个设备的。 也就是说这里device中的driver指针将会指向其绑定的驱动。那么回到probe探测函数,在我们对一个设备驱动进行注册的过程中,我们会在其相应的总线(也就是其挂接的总线)上发出一个探测,这个探测会搜寻所有挂接在这个总线上的尚未被绑定的设备(也就是driver指针为NULL),然后将driver指针指向这个驱动的结构,同时将这个设备的device结构挂接在device_driver结构中的klist链表中。 另外要提及一点就是我居然发现在device结构中也发现了一个这样的结构struct klist_node knode_driver,它看起来跟klist有关,但是我得说我确实不认为device需要一个klist来挂上它能使用的driver。同样,当一个设备被注册时,它也会去寻找挂接在同一条总线上的驱动,并将自己与这个驱动联系起来。关于基础知识大致就这么些,如果需要知道的更细,请致电10086。现在,结合代码叙述注册设备,发现设备(对于驱动来说这里的工作其实很少,如果想知道内核做了些什么,就请跟着我一起看看内核的奥秘),以及数据传输的驱动代码。sdio_register_driver(&sdio) 函数被调用从而注册sdio驱动.这里已经进入内核部分代码,他存在于内核的/drivers/net/wireless/libertas /If_sdio.c文件中,特别说明,我用的是LINUX 2.6.38的内核。首先我们看看注册函数:int sdio_register_driver(struct sdio_driver *drv) = drv-name; /首先忽略下面两行,直接进入driver_register函数. drv-drv.bus = &sdio_bus_type; /实际上这行代码是关键,告诉内核这个设备是sdio_bus_type。而下面的函数中我们要找的仅仅是调用probe函数的地方而已,稍后分析 return driver_register(&drv-drv);大家来看driver_register函数的内容,由于其中涉及较多有关klist、kobject结构的内容,如果有不明白的地方,请查看设备驱动开发详解 第2版,因为有些我也不是很明白。接着我们再进入driver_register(&drv-drv)函数:int driver_register(struct device_driver *drv)int ret;struct device_driver *other;BUG_ON(!drv-bus-p);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 methodsn, drv-name);other = driver_find(drv-name, drv-bus); /在kobject结构组成的链表中查找是否已经存在这个驱动 ,驱动必然挂接在某个总线上,返回值是device_driver结构的指针,返回后再提示驱动已经安装。if (other) put_driver(other); /由于之前增加了引用计数,这里在减1printk(KERN_ERR Error: Driver %s is already registered, aborting.n, drv-name);return -EBUSY;ret = bus_add_driver(drv);/进来(把驱动加载到总线上,此处为sdio_bus_type) /此函数是重点!if (ret)return ret;ret = driver_add_groups(drv, drv-groups); /加载驱动到相应的组里,成功返回非0,不成功返回0 ?(不明白)if (ret)bus_remove_driver(drv); /删除驱动 ? ?(不明白)return ret;接着跟进bus_add_driver(drv)函数,它的作用是:如果驱动还未挂接在总线上,挂接它并且调用probe函数进行探测。看看重点代码:int bus_add_driver(struct device_driver *drv).error = driver_attach(drv);/*在driver_attach()函数里,会进行driver和device匹配,无论匹配成功与否都会原路返回,接着向下执行klist_add_tail(&priv-knode_bus, &bus-p-klist_drivers);语句,这条语句的作用就是将if_sdio_driver添加到klist_drivers上。*/.driver_attach函数中,很简单的一句话: int driver_attach(struct device_driver *drv) return bus_for_each_dev(drv-bus, NULL, drv, _driver_attach);这个函数会调用_driver_attach函数,我们已经接近目标了static int _driver_attach(struct device *dev, void *data)struct device_driver *drv = data;if (!driver_match_device(drv, dev)/*调用driver_match_device匹配如果匹配成功,则向下执行,否则原路返回*/return 0;if (dev-parent)/* Needed for USB */device_lock(dev-parent);device_lock(dev);if (!dev-driver)driver_probe_device(drv, dev); /走这里噢 这就是我们要找的入口device_unlock(dev);if (dev-parent)device_unlock(dev-parent);return 0;接着我们再探秘driver_probe_device(drv, dev)部分源码:int driver_probe_device(struct device_driver *drv, struct device *dev)int ret = 0;if (!device_is_registered(dev)/判断驱动是否加载return -ENODEV;pr_debug(bus: %s: %s: matched device %s with driver %sn, drv-bus-name, _func_, dev_name(dev), drv-name);pm_runtime_get_noresume(dev);pm_runtime_barrier(dev);ret = really_probe(dev, drv);/走这里噢pm_runtime_put_sync(dev);return ret;准备到最后了,大家请忍耐,我们进去really_probe(dev, drv)看看:static int really_probe(struct device *dev, struct device_driver *drv)int ret = 0;.if (dev-bus-probe) ret = dev-bus-probe(dev);if (ret)goto probe_failed; else if (drv-probe) ret = drv-probe(dev); /我们看到了这里会调用probe函数,就是我们驱动里面的那个相应的函数。.probe= if_sdio_probe.if (dev-bus-probe)这个表示是否注册了device结构,如果注册了并且给device结构挂接上了驱动和总线,那么调用挂接在device结构中的总线的probe函数。这里的device结构从哪里冒出来的?它在 bus_for_each_dev函数中:int bus_for_each_dev(struct bus_type *bus, struct device *start, void *data, int (*fn)(struct device *, void *) struct klist_iter i; struct device *dev; int error = 0; if (!bus) return -EINVAL; klist_iter_init_node(&bus-p-klist_devices, &i, (start ? &start-knode_bus : NULL); while(dev=next_device(&i)&!error) /查找每个挂接在sdio总线上的设备,看他们是否有注册,并调用相应的probe函数。 _driver_attach函数.实际上就是查找device结构. error=fn(dev,data); klist_iter_exit(&i); return error;probe函数的调用,就取决于你在你注册的device结构中挂接的总线的类型了.因为调用 dev-bus-probe(dev); 所以清查看一下你注册是挂接的总线的probe函数的参数即可. 一般来说,参数会是两个,因为一类总线上总是可以挂接多个设备,所以我们还需要一个device_id. 如果行到else部分: else if (drv-probe) .这里调用驱动的probe函数,由于我们注册的是sdio_driver结构.看一看sdio_driver结构:struct sdio_driver char *name;const struct sdio_device_id *id_table;int (*probe)(struct sdio_func *, const struct sdio_device_id *);void (*remove)(struct sdio_func *);struct device_driver drv;好了,大家跟我进去那么久,该出来歇息歇息了,现在我就把大概的注册框图贴出来,方便了解,图2 图2总的一句话就是,我们已经完成了设备驱动的注册,注册最后又返回执行if_sdio_probe()函数,那么,我们跟着if_sdio_probe()函数看看(部分代码):static int if_sdio_probe(struct sdio_func *func,const struct sdio_device_id *id)struct mmc_host *host = func-card-host;/分配了一个描述mmc_host的结构体/*struct mmc_host struct device*parent;struct deviceclass_dev;intindex;const struct mmc_host_ops *ops;.*/switch (card-model) case MODEL_8385:card-scratch_reg = IF_SDIO_SCRATCH_OLD;break;case MODEL_8686:card-scratch_reg = IF_SDIO_SCRATCH; /选择wifi的类型 我们用的是8686break;case MODEL_8688:default: /* for newer chipsets */card-scratch_reg = IF_SDIO_FW_STATUS;break;/*初始化一个工作队列,延迟执行*/INIT_WORK(&card-packet_worker, if_sdio_host_to_card_worker);/初步猜测if_sdio_host_to_card_worker的作用是启动固件 发送使用/接着就调用if_sdio_host_to_card 发送使用sdio_claim_host(func);/*检测当前是否被占用*/ret = sdio_enable_func(func); /使能sdio函数(可能跟SD卡驱动有关系)if (ret)goto release;ret = sdio_claim_irq(func, if_sdio_interrupt);/当接收到数据时会产生这个中断,中断函数if_sdio_interrupt(后面单独分析)priv = lbs_add_card(card, &func-dev); priv-hw_host_to_card = if_sdio_host_to_card;/当执行主线程priv-main_thread = kthread_run(lbs_thread, dev, lbs_main);/调用lbs_thread()后就会进入回调函数if_sdio_host_to_card(发送时用)priv-enter_deep_sleep = if_sdio_enter_deep_sleep;priv-exit_deep_sleep = if_sdio_exit_deep_sleep;priv-reset_deep_sleep_wakeup = if_sdio_reset_deep_sleep_wakeup;ret = lbs_start_card(priv);/有东西看 请进please*分析sdio_claim_irq(func, if_sdio_interrupt)*1、 ret = sdio_card_irq_get(func-card);2、 host-sdio_irq_thread =kthread_run(sdio_irq_thread, host, ksdioirqd/%s,mmc_hostname(host);/进入sdio_irq_thread3、 ret = process_sdio_pending_irqs(host-card);4、 终于进入我们想知道的函数中:static int process_sdio_pending_irqs(struct mmc_card *card)/遍历读取相关寄存器的数据,如果有中断,则返回执行驱动的中断处理函数count = 0;for (i = 1; i = 7; i+) if (pending & (1 sdio_funci - 1;if (!func) printk(KERN_WARNING %s: pending IRQ for non-existant functionn,mmc_card_id(card);ret = -EINVAL; else if (func-irq_handler) func-irq_handler(func);/调用SD卡驱动的中断处理函数count+; else *sdio_claim_irq分析完毕* lbs_start_card(priv)分析*struct lbs_private *lbs_add_card(void *card, struct device *dmdev)/*调用函数alloc_netdev()函数分配了一个mesh_dev网络设备*/dev = alloc_netdev(0, wlan%d, ether_setup);if (!dev) dev_err(dmdev, no memory for network device instancen);goto err_adapter;/*设置了该网卡设备的操作函数,如设置MAC地址等*/ dev-netdev_ops = &lbs_netdev_ops;lbs_deb_thread(Starting main thread.n);init_waitqueue_head(&priv-waitq);priv-main_thread = kthread_run(lbs_thread, dev, lbs_main);/进入线程lbs_thread 这个线程被添加到prv-waitq队列中,然后休眠/只有被唤醒才会被执行/*在main.c 和 mesh.c (不知道是那个)都注册了.ndo_start_xmit= lbs_hard_start_xmit当有数据发送时,执行dev_queue_xmit()函数,接着调用lbs_hard_start_xmit()函数唤醒主线程priv-main_thread(),进入lbs_thread()函数*/INIT_WORK(&priv-mcast_work, lbs_set_mcast_worker);/这个工作队列很让人纠结,据说是实现无线网卡的热点扫描,跟进去里面太复杂了,差点出不来此处执行完毕会进入线程lbs_thread,让我们接着进去:static int lbs_thread(void *data)/处理待处理的命令lbs_process_command_response(priv,priv-resp_bufresp_idx,priv-resp_lenresp_idx);spin_lock_irq(&priv-driver_lock);/处理硬件事件 如拔卡,在Cmdresp.c中处理命令lbs_process_event(priv, event);spin_lock_irq(&priv-driver_lock);/当有数据发送时,执行此函数发送数据int ret = priv-hw_host_to_card(priv, MVMS_DAT,priv-tx_pending_buf,priv-tx_pending_len);到此为止,bs_start_card(priv)分析完毕,估计看代码有点吃力,那么我就贴张美图给大家看看吧。图3图3这会感觉好多了吧。*bs_start_card(priv)分析结束* lbs_start_card(priv)分析*int lbs_start_card(struct lbs_private *priv)/* poke the firmware */刺探固件 估计是安装那两个 helper_sd.bin sd8686.bin固件ret = lbs_setup_firmware(priv);if (ret)goto done;lbs_update_channel(priv);/?lbs_init_mesh(priv);/进来lbs_debugfs_init_one(priv, dev);/?进入lbs_init_mesh(priv):int lbs_init_mesh(struct lbs_private *priv)if (MRVL_FW_MAJOR_REV(priv-fwrelease) = MRVL_FW_V10) &(priv-fwcapinfo & MESH_CAPINFO_ENABLE_MASK) /* 10.0.0.pXX new firmwares should succeed with TLV * 0x100+37; Do not invoke command with old TLV. */priv-mesh_tlv = TLV_TYPE_MESH_ID;if (lbs_mesh_config(priv, CMD_ACT_MESH_CONFIG_START, priv-channel) /配置meshpriv-mesh_tlv = 0;if (priv-mesh_tlv) sprintf(priv-mesh_ssid, mesh);priv-mesh_ssid_len = 4;lbs_add_mesh(priv);/再添加meshif (device_create_file(&dev-dev, &dev_attr_lbs_mesh)lbs_pr_err(cannot register lbs_mesh attributen);ret = 1;接着进入bs_add_mesh(priv):int lbs_add_mesh(struct lbs_private *priv)/郁闷 分配网络设备 接下来ether_setup()就对它各种赋值mesh_dev = alloc_netdev(0, msh%d, ether_setup);if (!mesh_dev) lbs_deb_mesh(init mshX device failedn);ret = -ENOMEM;goto done;/* Register virtual mesh interface */终于走到终点站(天后)向VFS注册网络设备 这样应用层才可以实现对网络设备的操作ret = register_netdev(mesh_dev); /对网络设备的注册if (ret) lbs_pr_err(cannot register mshX virtual interfacen);goto err_free;关于网络设备的注册,小弟跟进去了下:err = register_netdevice(dev)ret = netdev_register_kobject(dev)error = device_add(dev)error = device_private_init(dev)bus_probe_device(dev);ret = device_attach(dev)最后:ret = bus_for_each_drv(dev-bus, NULL, dev, _device_attach);原来它跟一般的平台Driver注册是一样,我。* lbs_start_card(priv)分析完毕*故事发展到这一拍,基本上已经把if_sdio_probe函数的故事讲完毕。那么来总一个小结:在这个if_sdio_probe()函数主要做了两件事(这也是我们的最终目的):1、 调用函数alloc_netdev()函数分配(mesh_dev = alloc_netdev(0, msh%d, ether_setup),)了一个mesh_dev网络设备。2、 对网络设备mesh_dev进行初始化,操作函数绑定之后,接着调用了register_netdev(mesh_dev)函数,将网络设备注入到内核。到这里可以在系统跑起来后用ifconfig命令来查看这个网络设备,还可以设置这个网络设备的ip地址,网关,子网掩码等。在这个probe函数中还创建了一个内核线程,用来管理这个网络设备的数据发送、事件的处理(卡的拔出)和一些命令的处理。还申请了一个接收数据中断,只要这个wifi模块接收到数据,就会发送一个中断,告诉sdi host我接收到了数据,然后,host就会发送读取命令,对其进行读取,将读取到的数据放到skbuf中递交给ip层。下面贴出整个if_sdio_probe函数的整体框架,欢迎指导:图4图4既然是属于网卡,那么它肯定具备发送和接收数据的功能,现在我们一一解说.数据接收:在上面的if_sdio_probe函数中我们已经申请了一个接收数据的中断if_sdio_interrupt(),当接收到数据,wifi模块通过产生中断来提醒host去读取数据,host通过发送cmd53命令将数据接收后放到skbuf中,然后调用ip层提供的接口(netif_rx())函数将数据递交到ip层。这里在数据的读取上,我只是到request这个函数,这个函数是在host的platformdriver中绑
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2026年新课标 II 卷高考政治押题冲刺压轴题含解析
- 2026年全国乙卷新高考生物压轴题训练含解析
- 自动相关监视系统机务员变更管理评优考核试卷含答案
- 计算机整机装配调试员诚信品质能力考核试卷含答案
- 面包师安全宣贯能力考核试卷含答案
- 经编机操作工安全实操水平考核试卷含答案
- 城市管理网格员岗前健康知识考核试卷含答案
- 绿氢产业政策支持 (课件)
- 污水处理安全培训
- 公司上市前职业规划指南
- 《电气主系统》课件-第六章 电气设备选择
- 校医岗前培训课件
- 毕业设计(论文)-自动饺子机设计
- 小学生天文知识科普
- 基建工程项目财务决算报告【模板范本】
- 《综合代维交付方案》课件
- SYT 6968-2021 油气输送管道工程水平定向钻穿越设计规范-PDF解密
- JG293-2010 压铸铝合金散热器
- 2023年资产负债表模板
- 国开计算机组网技术实训1:组建小型局域网
- dd5e人物卡可填充格式角色卡夜版
评论
0/150
提交评论