`
fanrey
  • 浏览: 255036 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

网络相关驱动点滴

 
阅读更多
1. proto_register:
rc = proto_register(&proto, 1);
把proto加到一个prot_list中,proto_list是一个全局的静态链表,inet域支持的所有协议全部在这个链表中,但这个链表在协议栈中并没有太大用途,它只是用于在/proc/net/protocols文件中输出当前系统所支持的所有协议。
这个函数是在 linux/net/core/sock.c 中定义的,除了可以将这个协议添加到活动协议列表中之外,如果需要,该函数还可以选择分配一到多个 slab 缓存.

2. sock_register函数将某项协议注册到协议族数组net_families[]中
/* sock_register - add a socket protocol handler
* @ops: description of protocol
*
* This function is called by a protocol handler that wants to
* advertise its address family, and have it linked into the
* socket interface.The value ops->family coresponds to the
* socket system call protocol family.
*/
int sock_register(const struct net_proto_family *ops)
{
int err;

if (ops->family >= NPROTO) {
  printk(KERN_CRIT "protocol %d >= NPROTO(%d)\n", ops->family,
         NPROTO);
  return -ENOBUFS;
}

spin_lock(&net_family_lock);
if (net_families[ops->family])
  err = -EEXIST;
else {
  net_families[ops->family] = ops;
  err = 0;
}
spin_unlock(&net_family_lock);

printk(KERN_INFO "NET: Registered protocol family %d\n", ops->family);
return err;
}


struct net_proto_family {
int  family;
int  (*create)(struct net *net, struct socket *sock, int protocol);
struct module *owner;
};

3. 在/proc/net/下创建我们需要的文件 ,这里procfs已经给出了接口(all the APIs are defined in linux/include/linux/proc_fs.h):
linux\fs\proc\proc_net.c
extern struct proc_dir_entry *proc_net_fops_create(struct net *net,
const char *name, mode_t mode, const struct file_operations *fops);
extern void proc_net_remove(struct net *net, const char *name);
extern struct proc_dir_entry *proc_net_mkdir(struct net *net, const char *name,
struct proc_dir_entry *parent);

4. Seq文件接口:
当故障的原因很难确定时,监控和分析由procfs提供的数据可能会提供帮助。
但是,当数据量很大时,procfs的read()实现变得很复杂。Seq 文件接口是一种内核
提供的帮助机制用来简化这样的实现。Seq文件接口使得procfs操作干净又整洁。

让我们先介绍一个逐步变得复杂的procfs read()实现,然后看看Seq文件接口如何将
这个复杂的实现变得优雅。我们也会逐步跟新2.6内核中还没有采用seq file的驱动.


Seq 文件接口的优点

通过一个例子来看看Seq的优点吧。同通常的设备驱动一样,假设你有某一个数据结构的链表,
每一个节点都包含一个字符串域(称为info)。一下代码C1通过一个/proc/readme文件将其输出
到用户空间。当用户读这个文件,procfs的read()方法的具体实现,readme_proc将被调用,它将
遍历链表,将所有节点的info域传递给文件系统缓冲区。
C1:

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
MODULE_DESCRIPTION("test proc");
MODULE_LICENSE("GPL");

/* Private Data structure */
struct _mydrv_struct
{
    /* ... */
    struct list_head list; /* Link to the next node */
    char info[10];          /* Info to pass via the procfs file */
    /* ... */
};
static LIST_HEAD(mydrv_list); /* List Head */

static int readme_proc(char *page, char **start, off_t offset, int count, int *eof, void *data)
{

off_t thischunk_len = 0;
struct _mydrv_struct *p;

printk( KERN_DEBUG "offset is %d\n",offset );
/* Traverse the list and copy info into the supplied buffer */
list_for_each_entry(p, &mydrv_list, list)
{
    thischunk_len += sprintf(page+thischunk_len, p->info);
}
*eof = 1; /* Indicate completion */
return thischunk_len;
}


static int test_proc_init_module(void)
{
int i;
static struct proc_dir_entry *entry = NULL ;
struct _mydrv_struct * mydrv_new;


/* Create /proc/readme */
entry = create_proc_entry("readme", S_IWUSR, NULL);
if (entry)
    {
      entry->read_proc = readme_proc;
    }
/* Handcraft mydrv_list for testing purpose.
In the real world, device driver logic
maintains the list and populates the 'info' field */
    for (i=0;i<100;i++)
    {
     mydrv_new = kmalloc(sizeof(struct _mydrv_struct), GFP_ATOMIC);
     sprintf(mydrv_new->info, "Node No: %d\n", i);
     list_add_tail(&mydrv_new->list, &mydrv_list);
    }
return 0;
}

static void test_proc_exit_module(void)
{
remove_proc_entry("readme",0);
printk( KERN_DEBUG "Module test_proc exit\n" );
}

module_init(test_proc_init_module);
module_exit(test_proc_exit_module);


使用如下命令
bash> cat /proc/readme
Node No: 0
Node No: 1
...
Node No: 99

当read()方法被调用,它将提供一页的内存(译者注:通常是4k)用来传递信息到用户空间。
正如你所看到的,readme_proc的第一个参数是一个指向1页大小的缓冲区的指针。
第二个参数start用于当数据量超过1页时read方法的实现,在例子C2中我们可以明确它的使用方法。
接下来的两个参数非别用于指定当前读请求的偏移和要读的byte数量
*eof用来告诉调用者是否还需要读更多的数据。如果你注释掉了上面对*eof置一的语句,
readme_proc将被再次调用,且offset被指定为1190(节点0-节点99中info域最终显示的字符串数量)
readme_proc返回此次写入缓冲区的有效字符数。

在例子C1里,procfs read方法的返回被限定在1页以内。如果你将节点数量从100增加到500,将看到以下输出:

bash> cat /proc/readmeNode No: 0Node No: 1...Node No: 322proc_file_read: Apparent buffer overflow!
正如你所看到的,当超过4096byte的ascii字符被制造出来,产生了一个缓冲区溢出
为了解决这个问题,你需要修改c1的代码,使用以前提到的start变量
这将导致代码有点复杂,这样的实现基于以下原理:
1、readme_proc被调用多次,每次调用都将改变两个参数:每次要地区的最大数据量count和相对于
文件开始的偏移。每次的count值必须小于一页
2、每次调用结束后,offset增加readme_proc的返回值的大小
3、readme_proc的eof信号仅仅当总共产生的数据量小于等于本次请求的count+offset才发出。
如果eof没有被设置,readme_proc将再次被调用,传递给它的offset将增加上一次调用readme_proc的
返回值
4、每次调用结束后,只有start指向的数据是有效的并被传递给调用者

为了更好地理解操作过程,
C2代码将打印每次被调用的参数: start、offset、count、page
通过以下修改,你的代码可以摆脱限制,输出大量的信息

bash> cat /proc/readmeNode No: 0Node No: 1...Node No: 499
C2:

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
MODULE_DESCRIPTION("test proc");
MODULE_AUTHOR("root ()");
MODULE_LICENSE("GPL");

/* Private Data structure */
struct _mydrv_struct
{
    /* ... */
    struct list_head list; /* Link to the next node */
    char info[10];          /* Info to pass via the procfs file */
    /* ... */
};
static LIST_HEAD(mydrv_list); /* List Head */

static int readme_proc(char *page, char **start, off_t offset, int count, int *eof, void *data)
{

int i = 0;
off_t thischunk_start = 0;
off_t thischunk_len = 0;
struct _mydrv_struct *p;
/* Loop thru the list collecting device info */
list_for_each_entry(p, &mydrv_list, list) {
   thischunk_len += sprintf(page+thischunk_len, p->info);

    /* Advance thischunk_start only to the extent that the next
   * read will not result in total bytes more than (offset+count)
    */
   if (thischunk_start + thischunk_len < offset) {
    thischunk_start += thischunk_len;
    thischunk_len = 0;
   } else if (thischunk_start + thischunk_len > offset+count) {
    break;
   } else {
    continue;
   }
}

/* Actual start */
*start = page + (offset - thischunk_start);

/* Calculate number of written bytes */
thischunk_len -= (offset - thischunk_start);
if (thischunk_len > count) {
   thischunk_len = count;
} else {
   *eof = 1;
}

return thischunk_len;

}


static int test_proc_init_module(void)
{
int i;
static struct proc_dir_entry *entry = NULL ;
struct _mydrv_struct * mydrv_new;


/* Create /proc/readme */
entry = create_proc_entry("readme", S_IWUSR, NULL);
if (entry)
    {
      entry->read_proc = readme_proc;
    }
/* Handcraft mydrv_list for testing purpose.
In the real world, device driver logic
maintains the list and populates the 'info' field */
    for (i=0;i<500;i++)
    {
     mydrv_new = kmalloc(sizeof(struct _mydrv_struct), GFP_ATOMIC);
     sprintf(mydrv_new->info, "Node No: %d\n", i);
     list_add_tail(&mydrv_new->list, &mydrv_list);
    }
return 0;
}

static void test_proc_exit_module(void)
{
remove_proc_entry("readme",0);
printk( KERN_DEBUG "Module test_proc exit\n" );
}

module_init(test_proc_init_module);
module_exit(test_proc_exit_module);


当你面对C2中复杂的read实现感到前途黯淡时,Seq 文件接口前来救驾。
顾名思义,seq file interface 将procfs文件内容看做是序列化的对象
(Seq)编程接口提供的是这个序列化对象的迭代器
因此你的Seq代码必须实现以下的迭代器方法
1、start()将被seq interface首先调用,它初始化迭代子对象的位置并且返回插入的第一个迭代子对象
2、next()增加迭代器的位置,返回指向下一个迭代子的指针。这个函数不知道迭代子的内核结构,
将其看做黑盒
3、show()当用户读procfs的文件时,解释传递给它的迭代子,转换成要输出给用户的字符串。
这个方法充分利用了内核提供的 seq_printf(), seq_putc(), and seq_puts() 来格式化输出
4、stop()最后被调用用来清理

响应用户对相关proc文件的操作时,seq file interface会自动调用以上迭代器方法去产生输出。
你不用再去担心页大小和eof信号

C3


#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>

MODULE_DESCRIPTION("test proc");
MODULE_AUTHOR("root ()");
MODULE_LICENSE("GPL");

/* Private Data structure */
struct _mydrv_struct
{
    /* ... */
    struct list_head list; /* Link to the next node */
    char info[10];          /* Info to pass via the procfs file */
    /* ... */
};
static LIST_HEAD(mydrv_list); /* List Head */

/* start() method */
static void *
   mydrv_seq_start(struct seq_file *seq, loff_t *pos)
{
struct _mydrv_struct *p;
loff_t off = 0;

/* The iterator at the requested offset */
list_for_each_entry(p, &mydrv_list, list) {
   if (*pos == off++) return p;
}
return NULL;
}

/* next() method */
static void *
   mydrv_seq_next(struct seq_file *seq, void *v, loff_t *pos)
{
/* 'v' is a pointer to the iterator returned by start() or
by the previous invocation of next() */
struct list_head *n = ((struct _mydrv_struct *)v)->list.next;

++*pos; /* Advance position */
/* Return the next iterator, which is the next node in the list */
return(n != &mydrv_list) ?
    list_entry(n, struct _mydrv_struct, list) : NULL;
}

/* show() method */
static int
   mydrv_seq_show(struct seq_file *seq, void *v)
{
const struct _mydrv_struct *p = v;

/* Interpret the iterator, 'v' */
seq_printf(seq, p->info);
return 0;
}

/* stop() method */
static void mydrv_seq_stop(struct seq_file *seq, void *v)
{
/* No cleanup needed in this example */
}

/* Define iterator operations */
static struct seq_operations mydrv_seq_ops = {
.start = mydrv_seq_start,
.next = mydrv_seq_next,
.stop = mydrv_seq_stop,
.show = mydrv_seq_show,
};

static int
   mydrv_seq_open(struct inode *inode, struct file *file)
{
/* Register the operators */
return seq_open(file, &mydrv_seq_ops);
}

static struct file_operations mydrv_proc_fops = {
.owner   = THIS_MODULE,
.open    = mydrv_seq_open, /* User supplied */
.read    = seq_read,       /* Built-in helper function */
.llseek = seq_lseek,      /* Built-in helper function */
.release = seq_release,    /* Built-in helper funciton */
};


static int test_proc_init_module(void)
{

int i;
static struct proc_dir_entry *entry = NULL ;
struct _mydrv_struct * mydrv_new;


/* Create /proc/readme */
entry = create_proc_entry("readme", S_IWUSR, NULL);
if (entry)
{
   entry->proc_fops = &mydrv_proc_fops;
}
/* Handcraft mydrv_list for testing purpose.
In the real world, device driver logic
maintains the list and populates the 'info' field */
for (i=0;i<500;i++)
{
   mydrv_new = kmalloc(sizeof(struct _mydrv_struct), GFP_ATOMIC);
   sprintf(mydrv_new->info, "Node No: %d\n", i);
   list_add_tail(&mydrv_new->list, &mydrv_list);
}
return 0;
}

static void test_proc_exit_module(void)
{
remove_proc_entry("readme",0);
printk( KERN_DEBUG "Module test_proc exit\n" );
}

module_init(test_proc_init_module);
module_exit(test_proc_exit_module);

5. 网络协议的初始化dev_add_pack

在数据包的处理函数netif_receive_skb中,会先看ptype_all中是否有注册的协议,如果有,则调用相应的处理函数,然后再到ptype_base中,找到合适的协议,将skb发送到相关协议的处理函数.比如ip协议(ip_rcv)或者arp(arp_rcv)等等.此篇笔记讲的是有关ptype_all和ptype_base的相关知识点. 
 
ptype_base和ptype_all在内核中存储的情况如下图: 
 
 
 
可以看到,ptype_base为一个hash表,而ptype_all为一个双向链表.每一个里面注册的协议都用一个struct packet_type表示. 
 
struct packet_type  

    unsigned short        type;    /*协议类型*/ 
    struct net_device     *dev;    
    int            (*func) (struct sk_buff *, struct net_device *, 
                     struct packet_type *); 
    void            *data;    /* Private to the packet type        */ 
    struct packet_type    *next; 
}; 
其中需要注意的是dev参数,此参数表明了协议只处理来自dev指向device的数据,当dev=NULL时,表示该协议处理来自所有device的数据.这样,当注册自己的协议时,就可以指定自己想要监听或者接收的device. 
其中注册和注销协议的函数为: 
dev_add_pack(...)和dev_remove_pack(...) 
这两个函数很简单,分别如下: 
void dev_add_pack(struct packet_type *pt) 

    int hash; 
    br_write_lock_bh(BR_NETPROTO_LOCK); 
#ifdef CONFIG_NET_FASTROUTE  
    /* Hack to detect packet socket */ 
    if ((pt->data) && ((int)(pt->data)!=1)) { 
        netdev_fastroute_obstacles++; 
        dev_clear_fastroute(pt->dev); 
    } 
#endif  
    if (pt->type == htons(ETH_P_ALL)) { 
        netdev_nit++; 
        pt->next=ptype_all; 
        ptype_all=pt; 
    } else { 
        hash=ntohs(pt->type)&15; 
        pt->next = ptype_base[hash]; 
        ptype_base[hash] = pt; 
    } 
    br_write_unlock_bh(BR_NETPROTO_LOCK); 

此函数判断协议类型,然后加到ptype_base或者ptype_all中. 
void dev_remove_pack(struct packet_type *pt) 

    struct packet_type **pt1; 
    br_write_lock_bh(BR_NETPROTO_LOCK); 
    if (pt->type == htons(ETH_P_ALL)) { 
        netdev_nit--; 
        pt1=&ptype_all; 
    } else { 
        pt1=&ptype_base[ntohs(pt->type)&15]; 
    } 
    for (; (*pt1) != NULL; pt1 = &((*pt1)->next)) { 
        if (pt == (*pt1)) { 
            *pt1 = pt->next; 
#ifdef CONFIG_NET_FASTROUTE  
            if (pt->data) 
                netdev_fastroute_obstacles--; 
#endif  
            br_write_unlock_bh(BR_NETPROTO_LOCK); 
            return; 
        } 
    } 
    br_write_unlock_bh(BR_NETPROTO_LOCK); 
    printk(KERN_WARNING "dev_remove_pack: %p not found.\n", pt); 

此函数也很简单,只是把协议从相关的链表中移除. 
下面以ip协议为例子来看看相关的实现: 
ip协议结构体的定义如下: 
static struct packet_type ip_packet_type = 

    __constant_htons(ETH_P_IP), 
    NULL,    /* All devices */ 
    ip_rcv, 
    (void*)1, 
    NULL, 
}; 
当ipv4协议栈初始化时,会调用ip_init.之后,所有协议类型为ETH_P_IP的包都会交由ip_rcv处理.代码如下: 
void __init ip_init(void) 

    dev_add_pack(&ip_packet_type); 
    ip_rt_init(); 
    inet_initpeers(); 
#ifdef CONFIG_IP_MULTICAST  
    proc_net_create("igmp", 0, ip_mc_procinfo); 
#endif  

这样在系统启动之后,ip协议便被注册到ptype_base链表中,相应的处理函数为ip_rcv.


6. 内核中的Notification Chains的分析 Linux内核中有许多子系统,他们之间有着非常多的依赖与交互关系,当某一个子系统中有事件发生时,就会影响到其他子系统的工作,比如说网卡ip地址的改变,设备的热插拔等等。Linux内核中使用了Notification Chains的方法来处理这类事件,顾名思义,它是一个链表的数据结构,其中链表的表元由回调函数,priority和next指针组成。
35 struct notifier_block { 36         int (*notifier_call)(struct notifier_block *, unsigned long, void *); 37         struct notifier_block *next; 38         int priority; 39 };      当我们初始化子系统的时候,应该知道自己到底对哪些事件比较敏感,也就是说知道哪些事件的发生会对自己的功能发生影响,如果有的话,就注册自己到这个特定的notification chain里面去,我们举一个例子来看看:

1242 void __init arp_init(void)1243 {1244         neigh_table_init(&arp_tbl);12451246         dev_add_pack(&arp_packet_type);1247         arp_proc_init();1248 #ifdef CONFIG_SYSCTL1249         neigh_sysctl_register(NULL, &arp_tbl.parms, NET_IPV4,1250                               NET_IPV4_NEIGH, "ipv4", NULL, NULL);1251 #endif1252         register_netdevice_notifier(&arp_netdev_notifier);1253 }      这个取自arp的初始化,我们注意最后一行的函数register_netdevice_notifier,这个就是注册函数,当然它还会有多层的调用,而arp_netdev_notifier就是一个notifier_block的结构,其中回调函数初始化为arp_netdev_event。我们先来看一下这个回调函数的实现

1201 static int arp_netdev_event(struct notifier_block *this, unsigned long event, void *ptr)1202 {1203         struct net_device *dev = ptr;12041205         switch (event) {1206         case NETDEV_CHANGEADDR:1207                 neigh_changeaddr(&arp_tbl, dev);1208                 rt_cache_flush(0);1209                 break;1210         default:1211                 break;1212         }12131214         return NOTIFY_DONE;1215 }      我们看到了,它只关心一种event,那就是NETDEV_CHANGEADDR,其他的事件发生它就并不在意了,当网络设备发生任何已经定义好的事件时,会遍历notification chain来进行遍历通知,如果是我们关心的事件发生,我们就作出相应的反应进行处理,如果不是,那么就当什么都没有发生。
      注册函数有一个通用的类型函数,上文所见到的register_netdevice_notifier经过二层调用之后就会遇到,这个函数就是
105 static int notifier_chain_register(struct notifier_block **nl,106                 struct notifier_block *n)107 {108         while ((*nl) != NULL) {109                 if (n->priority > (*nl)->priority)110                         break;111                 nl = &((*nl)->next);112         }113         n->next = *nl;114         rcu_assign_pointer(*nl, n);115         return 0;116 }      这个函数的作用就是把一个notifier_block加入到chain中去,不过这个链表的插入方法有点诡异,因为它直接利用了二级指针的一个巧妙应用,一般我们插入链表都需要有一个指针来保存前一个位置,然后再插入表元,而在这里,它只利用了一个指针就完成了这些工作,也就是把这个二级指针保存了前一个表元的next指针。
      最后我们来看一下那个事件发生时通知在通知链上的表元的函数

131 static int __kprobes notifier_call_chain(struct notifier_block **nl,132                 unsigned long val, void *v)133 {134         int ret = NOTIFY_DONE;135         struct notifier_block *nb;136137         nb = rcu_dereference(*nl);138         while (nb) {139                 ret = nb->notifier_call(nb, val, v);140                 if ((ret & NOTIFY_STOP_MASK) == NOTIFY_STOP_MASK)141                         break;142                 nb = rcu_dereference(nb->next);143         }144         return ret;145 }      这个函数应该很容易理解,就是调用回调函数进行处理。
      Notification Chains最重要的好处是每个子系统自己来注册到底需要听哪些事件,而不是事件的发生方来遍历各个子系统来问我的改变你们是不是需要进行相应的对策处理,这样效率就提高了,也就是所谓的publish-and-subscribe model。
 
 
7. dev_hold 和 dev_put, 可以用来增加和递减net_device 的使用计数的.

linux可以使用dev_get_by_name函数取得设备指针,但是使用是需要注意,使用过dev_get_by_name函数后一定要使用dev_put(pDev)函数释放设备引用,不然可能导致GET的设备无法正常卸载。

8. dev_queue_xmit 
dev_queue_xmit是有网络设备无关层调用的函数,调用对象调用该函数之后,函数会判断skb中的dev字段,根据这个字段指示的设备调用该设备的发送函数hard_start_xmit来对skb进行转发。
上层在需要发送skb的时候会选择调用dev_queue_xmit,那么至于下层是怎么传递该skb的,上层根本就不用关心,这就是所谓的各层的独立性原理。所以对skb具体的发送处理过程,可以由下层网络接口的hard_queue_xmit来处理。比如说上层需要发送一个广播帧,那么它就将skb->pkt_type赋值为PACKET_BROADCAST,然后调用dev_queue_xmit将其发送出去之后就不管下层是否将这个广播帧真的放到网络中进行广播。而下层如果是一个与上层绑定好了的虚拟网络设备的话,它可以在自己的hard_start_xmit中对skb->pkt_type字段为PACKET_BROADCAST的skb进行特定的处理,这里指的特定就是说,不一定非要将这个skb放到网络中进行广播。

9. sk_alloc: 生成一个sock结构体
/**
* sk_alloc - All socket objects are allocated here
* @net: the applicable net namespace
* @family: protocol family
* @priority: for allocation (%GFP_KERNEL, %GFP_ATOMIC, etc)
* @prot: struct proto associated with this new sock instance
*/
struct sock *sk_alloc(struct net *net, int family, gfp_t priority,
      struct proto *prot)
{
struct sock *sk;

sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family);
if (sk) {
sk->sk_family = family;
/*
* See comment in struct sock definition to understand
* why we need sk_prot_creator -acme
*/
sk->sk_prot = sk->sk_prot_creator = prot;
sock_lock_init(sk);
sock_net_set(sk, get_net(net));
atomic_set(&sk->sk_wmem_alloc, 1);

sock_update_classid(sk);
}

return sk;
}
分享到:
评论

相关推荐

    液体点滴速度监控系统设计

    此外,系统可能还需要具备一定的扩展性,比如支持多点滴同时监控,或者通过网络连接成为医院信息系统的一部分,实现远程监控和报警功能。这样的系统不仅提高了医疗护理的效率,也降低了人为错误的风险,对于提升医疗...

    基于89C2051的步进电机驱动系统设计

    原型号的医用点滴泵采用恒压驱动,虽然成本低,但在高流量条件下存在整机发热的问题。通过采用本设计的步进电机驱动方案,提高了高频力矩,降低了低频功耗,使系统在不同的工作频率段均能有效地控制流速,同时避免了...

    自动点滴管理系统(.NET 3.5框架的WCF技术实现)

    也包括对ASP.NET MVC的支持,便于创建模式驱动的Web应用程序。 WCF的核心概念包括服务、终结点、绑定和协定。服务是提供功能的实体,终结点是服务与外界交互的入口,绑定定义了服务如何与客户端通信的细节,而协定...

    ARM硬件高手的点滴

    1. 硬件层:这是嵌入式系统的基础,包括单片机、接口设计以及相关的C和汇编编程。适合电子、通信、自动化等专业的人员从事,需要掌握单片机原理、微机接口技术及C语言。 2. 驱动层:驱动工程师需具备读取电路图、...

    Nodejs学习点滴

    这篇名为“Nodejs学习点滴”的博文,结合了“源码”和“工具”两个标签,暗示了作者可能分享了关于Node.js核心原理以及相关开发工具的见解和经验。接下来,我们将深入探讨Node.js的核心特性,以及`build.js`、`Dom...

    薄膜点滴填料混装的横流冷却塔行业(2021-2026)企业市场突围战略分析与建议.docx

    在瞬息万变的市场环境中,企业需明确自身的战略目标,以创新为驱动,不断适应并引领行业发展趋势。这意味着企业不仅要在技术研发上保持领先地位,还要在市场策略上做出独特且有远见的决策。 其次,企业应精准定位...

    SD卡协议学习点滴.pdf

    发送CMD0命令可将SD卡置于IDLE状态,此时CMD线为输入模式,卡的相对地址为0x0000,驱动寄存器设定为最低速度,最大驱动电流能力。复位过程是整个通信的基础,确保SD卡准备好接收后续指令。 3. **工作条件检测** ...

    新人教统编版九年级上册道德与法治 第二课 创新驱动发展 教学课件.pptx

    创新无处不在,无论是科技领域的发明创造,还是日常生活的点滴改进,都可以被视为创新。 2. **创新的价值与作用**: - 生活中的创新:创新使生活变得更加丰富多彩,例如用废旧物品创造出新的实用品,既环保又有...

    单片机与DSP中的基于89C51的液体点滴速度监控系统设计

    此外,系统利用串行通信接口建立主站与从站之间的有线监控网络,主站采用VB编程的用户界面,提供直观的人机交互体验,能够实时显示点滴速度并根据需要调整。 系统设计包括两部分:从站和主站。从站需在滴斗处检测...

    全国大学生电子设计竞赛论文.doc

    - LCD显示:主机上的液晶显示屏提供友好的用户界面,显示当前点滴速度和其他相关信息。 - LED显示:可能用于显示系统状态或报警信息。 - 光电传感器:用于检测液滴和液面,采用光电效应原理,通过光敏元件接收...

    基于STM32的输液监控系统设计与实现

    整体来看,基于STM32的输液监控系统实现了静脉输液的自动化和网络化,不仅提高了患者治疗的安全性和舒适度,还减轻了护理人员的工作负担,对于现代医疗体系有着显著的改进作用。随着物联网技术的发展,类似的智能...

    2019年考研政治必备.doc

    在实现中国梦的过程中,他们需要将个人理想与国家的未来、民族的命运紧密结合,以实际行动参与社会进步,脚踏实地地从点滴做起,勇担时代使命。 3. 创新创业的挑战与坚韧精神: 创新创业充满困难和挑战,理想的...

    大视野 用户体验至上

    当前,随着新一轮信息化浪潮的到来,用户体验已经逐渐成为行业发展的核心驱动力,标志着信息化时代更加繁荣的未来。 用户体验的概念,是指用户在使用产品或服务时的直接感受,包括对产品易用性、功能性、愉悦性等...

    家校交流与合作-能力提升工程2.0信息化教学实践新模式和任务驱动形式下的感想和转变.docx

    无论是学校的大型活动还是课堂上的点滴进步,都可以通过照片、视频等形式记录并分享,让家长能够看到孩子在学校的生活,增强家校间的互动,激发家长参与学校活动的积极性。 接着,微信群也是教育资源的共享空间。...

    unix大全(涵盖了UNIX学习的方方面面)

    18.性能相关的几个核心参数(CPU,I/O,MEMORY) 19.SCO NFS详解 20.SCO命令--df 21.SCO OpenServer 5.0.5中DHCP如何配置 ? 22.ESQL编程使用说明 23.UnixWare 7 root口令遗失的解决方法 24.SCO UNIX 讲座 25.Sco open...

    4.3信息交流

    - **个人社交媒体**:如QQ空间、微博、微信朋友圈等,个人可以在此平台上分享生活点滴,表达个人观点。 #### 网络上的信息发布 - **个人网站或网页**:通过搭建个人网站或网页,发布个人作品、研究资料等。 - **...

    提出问题与解决问题的辩证关系-日常工作中的创新与方法论归类.pdf

    爱因斯坦的观点强调了提出问题的重要性,因为这需要创新思维和创造性的想象力,是科学进步的驱动力。在科技创新领域,明确问题的存在是研究的方向标,而提出新问题意味着对既有知识体系的挑战和突破。 然而,在实际...

    twitter 运营

    通过对Twitter内部社交互动的研究发现,用户的实际使用行为是由一个稀疏且隐藏的连接网络驱动的,这一网络位于他们公开声明的朋友和关注者列表之下。具体而言: 1. **注意力稀缺性**:在Twitter这样的平台上,由于...

    博客空间Sablog-X v1.6 正式版 Build 20070820-sablog-x1.6.zip

    博客空间Sablog-X v1.6 正式版 Build 20070820-sablog-x1.6.zip是一款基于PHP语言开发的个人博客系统,它为用户提供了一个功能强大、易于使用的平台来发布自己的思想、见解和生活点滴。这款博客软件在2007年8月20日...

Global site tag (gtag.js) - Google Analytics