kset与热插拔中的uevent和call_usermodehelper

简介: kset与热插拔中的uevent和call_usermodehelper

kset

kset可以认为是一组kobject的集合,是kobject的容器。kset本身也是一个内核对象,所以需要内嵌一个kobject对象。其完整定义如下:

struct kset {
  struct list_head list;/*用来将其中的kobject对象构建成链表。*/
  spinlock_t list_lock;/*对kset上的list链表进行访问操作时用来作为互斥保护使用的自旋锁。*/
  struct kobject kobj;/*表当前kset内核对象的kobJect变量。*/
  const struct kset_uevent_ops *uevent_ops;/*定义了一组函数指针,当k中的某些kobj对象发生状态变化需要通知用户空间时,调用其中的函数来完成。*/
};

struct_kset_uevent_ops类型声明如下:

struct kset_uevent_ops {
  int (* const filter)(struct kset *kset, struct kobject *kobj);
  const char *(* const name)(struct kset *kset, struct kobject *kobj);
  int (* const uevent)(struct kset *kset, struct kobject *kobj,
          struct kobj_uevent_env *env);
};

kset上的一些主要操作有:

  • kset_init
    用来初始化一个kset对象,函数原型为:
void kset_init(struct kset *kset)
  • kset_register
    用来初始化并向系统注册一个kset对象,函数的实现如下:
int kset_register(struct kset *k)
{
  int err;
  if (!k)
    return -EINVAL;
  kset_init(k);
  err = kobject_add_internal(&k->kobj);
  if (err)
    return err;
  kobject_uevent(&k->kobj, KOBJ_ADD);
  return 0;
}

其中kset_init和kobject_add_intemal的功能都比较直观,分别用来初始化kset对象和向系统注册该kset对象,因为kset对象本身就是一个由kobject代表的内核对象,所以kobject_add_intemal函数会为代表该kset对象的k->kobJ在sysfs文件树中生成一个新目录,这个过程同前面谈到的kobject的操作是完全一样的。

kset对象与单个的kobJ对象不一样的地方在于,将一个kset对象向系统注册时,如果Linux内核编译时启用了CONFIG_HOTPLUG,那么需要将这一事件通知用户空间,这个过程由kobJect_uevent完成。如果一个kobject对象不属于任一kset,那么这个孤立的kobject对象将无法通过uevent机制向用户空间发送event消息。

图中,kobJ之间通过parent成员实现层次关系,如果某一kobJ的parent为NULL,那么在调用kobject_add函数将该kobj加入系统时,函数首先看kobj->kset是否为NULL,如果不为NULL,就会把kobJ->kset->kobj作为kobj的parent,否则系统中将产生一个孤立的kobject对象,该对象将无法通过uevent机制向用户空间发送event消息。kset将所有隶属于它的kobject对象放到一个链表中,同时可以看到kset的数据结构中内嵌了一个kobject成员,所以kset自身也是作为一个内核对象而存在。

  • kset_create_and_add
struct kset *kset_create_and_add(const char *name,
         const struct kset_uevent_ops *uevent_ops,
         struct kobject *parent_kobj)

主要作用是动态产生一kset对象然后将其加入到sys文件系统中。参数是创建的kset对象的名称,uevent_ops是新kt对象上用来处理用户空间event消息的操作集,parent_kobj是kset对象的上层(父级)的内核对象指针。

  • kset_unregister
void kset_unregister(struct kset *k)

用来将k指向的kset对象从系统中注销,完成的是kset_register的反向操作。

热插拔中的uevent和call_usermodehelper

这里的热插拔(hotplug)可以简单描述为,当一个设备动态加入系统时(典型地如用户将一个USB盘插到计算机上),设备驱动程序可以检查到这种设备状态的变化(加入或者移除),然后通过某种机制使得在用户空间找到该设备对应的驱动程序模块并加载之。在Linux系统上有两种机制可以在设备状态发生变化时,通知用户空间去加载或者卸载该设备所对应的驱动程序模块:一个是udev,另一个是/sbin/hotplug。在Linux发展的早期阶段,用户空间支持热插拔的唯一工具是/sbin/hotplug,它的幕后推手是call_usermodehelper函数,后者能够从内核空间启动一个用户空间的应用程序。随着内核的发展演进,后来又发展出了udev机制并逐渐取代了/sbin/hotplug,现在udev工具包己成为大多数Linux发行版本中首选的方法。udev的实现基于内核中的网络机制,它通过创建标准的socket接口来监听来自内核的网络广播包,并对接收到的包进行分析处理。

恰如刚才所提到的,两种机制都必须得到来自内核空间的支持才可以工作,接下来讨论的是设备驱动程序如何在内核空间对这些工具给予支持。

Linux设备模型中一个非常重要的功能便是对设备热插拔特性的支持,具体到底层的实现细节上,热插拔在内核中通过一个名为kobject_uevent的函数来实现。它通过发送一个uevent消息和调用call_usermodehelper来与用户空间进行沟通kobject_uevent所实现的功能和Linux系统中用以实现热插拔的特性息息相关,它是udev和/sbin/hotplug等工具赖以工作的基石。所以有足够的理由让我们用出一定的篇幅来仔细讨论一下kobject_uevent函数,该函数在内核中的实现为:

int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
  return kobject_uevent_env(kobj, action, NULL);
}

参数action是个枚举型变量,其类型定义为:

enum kobject_action {
  KOBJ_ADD,
  KOBJ_REMOVE,
  KOBJ_CHANGE,
  KOBJ_MOVE,
  KOBJ_ONLINE,
  KOBJ_OFFLINE,
  KOBJ_MAX
};

这些枚举数值定义了k姒对象的一些状态变化,此处使用的是KOBJ_ADD,表明将向系统添加一个kset对象。

kobject_uevent函数的主体功能是在kobject_uevent_env调用中完成的,这个函数的实现比较冗长:

int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
           char *envp_ext[])
{
  struct kobj_uevent_env *env;
  const char *action_string = kobject_actions[action];
  const char *devpath = NULL;
  const char *subsystem;
  struct kobject *top_kobj;
  struct kset *kset;
  const struct kset_uevent_ops *uevent_ops;
  int i = 0;
  int retval = 0;
#ifdef CONFIG_NET
  struct uevent_sock *ue_sk;
#endif
  pr_debug("kobject: '%s' (%p): %s\n",
     kobject_name(kobj), kobj, __func__);
  /* search the kset we belong to */
  top_kobj = kobj;//此处的while循环用来查找k所隶属的最頂层kset
  while (!top_kobj->kset && top_kobj->parent)
    top_kobj = top_kobj->parent;
  if (!top_kobj->kset) {//如果当前kobJ没有隶属的kset,那么它将不能使用uevent机制
    pr_debug("kobject: '%s' (%p): %s: attempted to send uevent "
       "without kset!\n", kobject_name(kobj), kobj,
       __func__);
    return -EINVAL;
  }
  kset = top_kobj->kset;//得到kobj所隶属的頂层kset的uevent操作集对象uevent-ops
  uevent_ops = kset->uevent_ops;
  /* skip the event, if uevent_suppress is set*/
  if (kobj->uevent_suppress) {//如果kobj->uevent_suppress=1,表明该kobJ不希望使用uevent机制
    pr_debug("kobject: '%s' (%p): %s: uevent_suppress "
         "caused the event to drop!\n",
         kobject_name(kobj), kobj, __func__);
    return 0;
  }
  /* skip the event, if the filter returns zero. */
  //首先调用filter函数,如果函数返回0,表明ko希望发送的event消息被頂层kset过滤掉了
  if (uevent_ops && uevent_ops->filter)
    if (!uevent_ops->filter(kset, kobj)) {
      pr_debug("kobject: '%s' (%p): %s: filter function "
         "caused the event to drop!\n",
         kobject_name(kobj), kobj, __func__);
      return 0;
    }
  /* originating subsystem */
  if (uevent_ops && uevent_ops->name)
    subsystem = uevent_ops->name(kset, kobj);
  else
    subsystem = kobject_name(&kset->kobj);
  if (!subsystem) {
    pr_debug("kobject: '%s' (%p): %s: unset subsystem caused the "
       "event to drop!\n", kobject_name(kobj), kobj,
       __func__);
    return 0;
  }
  /* environment buffer */
  //准备使用uevent机制向用户空间发送event消息,通过add_uevent_var添加环境变量信息
  env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);
  if (!env)
    return -ENOMEM;
  /* complete object path */
  devpath = kobject_get_path(kobj, GFP_KERNEL);
  if (!devpath) {
    retval = -ENOENT;
    goto exit;
  }
  /* default keys */
  //此处在向用户空间发送event消息之前,给kset最后一次机会以完成一些私人事情
  retval = add_uevent_var(env, "ACTION=%s", action_string);
  if (retval)
    goto exit;
  retval = add_uevent_var(env, "DEVPATH=%s", devpath);
  if (retval)
    goto exit;
  retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
  if (retval)
    goto exit;
  /* keys passed in from the caller */
  if (envp_ext) {
    for (i = 0; envp_ext[i]; i++) {
      retval = add_uevent_var(env, "%s", envp_ext[i]);
      if (retval)
        goto exit;
    }
  }
  /* let the kset specific function add its stuff */
  if (uevent_ops && uevent_ops->uevent) {
    retval = uevent_ops->uevent(kset, kobj, env);
    if (retval) {
      pr_debug("kobject: '%s' (%p): %s: uevent() returned "
         "%d\n", kobject_name(kobj), kobj,
         __func__, retval);
      goto exit;
    }
  }
  /*
   * Mark "add" and "remove" events in the object to ensure proper
   * events to userspace during automatic cleanup. If the object did
   * send an "add" event, "remove" will automatically generated by
   * the core, if not already done by the caller.
   */
  if (action == KOBJ_ADD)
    kobj->state_add_uevent_sent = 1;
  else if (action == KOBJ_REMOVE)
    kobj->state_remove_uevent_sent = 1;
  mutex_lock(&uevent_sock_mutex);
  /* we will send an event, so request a new sequence number */
  retval = add_uevent_var(env, "SEQNUM=%llu", (unsigned long long)++uevent_seqnum);
  if (retval) {
    mutex_unlock(&uevent_sock_mutex);
    goto exit;
  }
//如果配置了CONFIGNET宏,表明内核打算使用netlink机制实现uevent消息的发送
#if defined(CONFIG_NET)
  /* send netlink message */
  list_for_each_entry(ue_sk, &uevent_sock_list, list) {
    struct sock *uevent_sock = ue_sk->sk;
    struct sk_buff *skb;
    size_t len;
    if (!netlink_has_listeners(uevent_sock, 1))
      continue;
    /* allocate message with the maximum possible size */
    len = strlen(action_string) + strlen(devpath) + 2;
    skb = alloc_skb(len + env->buflen, GFP_KERNEL);
    if (skb) {
      char *scratch;
      /* add header */
      scratch = skb_put(skb, len);
      sprintf(scratch, "%s@%s", action_string, devpath);
      /* copy keys to our continuous event payload buffer */
      for (i = 0; i < env->envp_idx; i++) {
        len = strlen(env->envp[i]) + 1;
        scratch = skb_put(skb, len);
        strcpy(scratch, env->envp[i]);
      }
      NETLINK_CB(skb).dst_group = 1;
      retval = netlink_broadcast_filtered(uevent_sock, skb,
                  0, 1, GFP_KERNEL,
                  kobj_bcast_filter,
                  kobj);
      /* ENOBUFS should be handled in userspace */
      if (retval == -ENOBUFS || retval == -ESRCH)
        retval = 0;
    } else
      retval = -ENOMEM;
  }
#endif
  mutex_unlock(&uevent_sock_mutex);
#ifdef CONFIG_UEVENT_HELPER
  //使用uevent_helper机制实uevent
  /* call uevent_helper, usually only enabled during early boot */
  if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
    struct subprocess_info *info;
    retval = add_uevent_var(env, "HOME=/");
    if (retval)
      goto exit;
    retval = add_uevent_var(env,
          "PATH=/sbin:/bin:/usr/sbin:/usr/bin");
    if (retval)
      goto exit;
    retval = init_uevent_argv(env, subsystem);
    if (retval)
      goto exit;
    retval = -ENOMEM;
    info = call_usermodehelper_setup(env->argv[0], env->argv,
             env->envp, GFP_KERNEL,
             NULL, cleanup_uevent_env, env);
    if (info) {
      retval = call_usermodehelper_exec(info, UMH_NO_WAIT);
      env = NULL; /* freed by cleanup_uevent_env */
    }
  }
#endif
exit:
  kfree(devpath);
  kfree(env);
  return retval;
}

kobJect_uevent_env总体上可以分成三个功能部分,第一部分用到kset->uevent-ops,调用其中的filter函数,以决定kset对象当前状态的改变是否要通知到用户层,如果uevent_ops->filter(kset,kobj)返回0,将不再通知用户层。不同的kset对象拥有不同的uevent_ops对象,因此也意味着不同的kset都有自己独特的uevent_ops操作集,在后续使用到uevent_ops操作集的集体例子中将再来讨论此处的操作。总之,读者需要记住,一个kset对象状态的变化,将会首先调用隶属于该kset对象的uevent_ops操作集中的filter函数,以决定是否向用户层报告该事件。

如果filter函数通过了,换句话说,kset中发生的事件需要通知用户层,那么将进入第二部分。第二部分主要是完成环境变量的设置,在一开始先通过env=kmlloc(sizeof(struct kobj_uevent_env),GFPKERNEL)分配一个存储环境变量的空间对象env,接下来把用户空间程序可能需要的环境变量通过add_ueventvar函数加入到env中。如同第一部分的filter函数一样,第二部分在处理完环境变量之后,会调用kset对象的uevent_ops操作集中的uevent函数,这是内核赋予kset通过该函数完成自己特定功能的最后一次机会。

第三部分是kobject-ueventenv函数的亮点,也是最有趣的地方,主要用来和用户空间进程进行交互〈或者在内核空间启动执行一个用户空间的程序)。在Linux内核中,有两种方式完成这项任务,一个是代码中由CONFIGNET宏包含的部分,这部分代码通过netlink的方式向用户空间广播当前kset对象中的uevent消息。另一种方式是在内核空间启动一个用户空间的进程,通过给该进程传递内核设定的环境变量的方式来通知用户空间k对象中的uevent事件。虽然/sbin/hotplug方式已经逐渐被udev取代,但是因为/sbin/hotplug在内核中需要一个call_usermodehelper函数的支持,这是个比较有趣的函数,所以这里我们只讨论uevent_helper方式的实现。

uevent_helper方法通过调用call_usermodehelper来达到从内核空间运行一个用户空间进程的目的,用户空间进程的二进制文件的路径由ueventhelper提供,该变量是一字符数组,在内核源码中的定义为:

#ifdef CONFIG_UEVENT_HELPER
char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;
#endif

CONFIG_UEVEN——THELPERPATH是一内核编译阶段的配置宏,依赖于CONFIG_HOTPLUG,这意味着如果系统需要支持设备的热插拔等特性,则需要给出用户空间进程的文件路径信息。通常,CONFIG_UEVENT_HELPERPATH会指向/sbin/hotplug,后者用来处理系统中出现的热插拔事件,不过现在的Linux系统多半没有/sbin/hotplug这个文件。

下面讨论call_usermodehelper函数的内核实现。对内核空间如何运行一个用户空间的进程感兴趣的读者,或者想深入理解内核如何支持设备的hotplug特性的设备驱动程序员,也许都不应该错过这里讨论的内容。call_usermodehelper函数在Linux内核中的源码为:

int call_usermodehelper(char *path, char **argv, char **envp, int wait)
{
  struct subprocess_info *info;
  gfp_t gfp_mask = (wait == UMH_NO_WAIT) ? GFP_ATOMIC : GFP_KERNEL;
  info = call_usermodehelper_setup(path, argv, envp, gfp_mask,
           NULL, NULL, NULL);
  if (info == NULL)
    return -ENOMEM;
  return call_usermodehelper_exec(info, wait);
}

call_usermodehelper函数的设计思想是采用工作队列的方式,在call_usermodehelper_setup函数内部会初始化一个工作队列的节点:

INIT_WORK(&sub_info->work, __call_usermodehelper);

其中sub_info是一struct subprocess_info类型的变量,工作队列节点作为它的一个内嵌对象sub_info其他成员用来存储运行用户态进程的一些相关信息,主要是相关的环境变量。call_usermodehelper是该工作节点上的延迟执行的函数。

将call_usermodehelper_setup中建立的工作节点提交到工作队列的行为发生在call_usermodehelper_exec函数中,其定义如下:

int call_usermodehelper_exec(struct subprocess_info *sub_info, int wait)
{
  DECLARE_COMPLETION_ONSTACK(done);
  int retval = 0;
  if (!sub_info->path) {
    call_usermodehelper_freeinfo(sub_info);
    return -EINVAL;
  }
  helper_lock();
  if (!khelper_wq || usermodehelper_disabled) {
    retval = -EBUSY;
    goto out;
  }
  /*
   * Set the completion pointer only if there is a waiter.
   * This makes it possible to use umh_complete to free
   * the data structure in case of UMH_NO_WAIT.
   */
  sub_info->complete = (wait == UMH_NO_WAIT) ? NULL : &done;
  sub_info->wait = wait;
  queue_work(khelper_wq, &sub_info->work);
  if (wait == UMH_NO_WAIT)  /* task has freed sub_info */
    goto unlock;
  if (wait & UMH_KILLABLE) {
    retval = wait_for_completion_killable(&done);
    if (!retval)
      goto wait_done;
    /* umh_complete() will see NULL and free sub_info */
    if (xchg(&sub_info->complete, NULL))
      goto unlock;
    /* fallthrough, umh_complete() was already called */
  }
  wait_for_completion(&done);
wait_done:
  retval = sub_info->retval;
out:
  call_usermodehelper_freeinfo(sub_info);
unlock:
  helper_unlock();
  return retval;
}

该函数的逻辑功能很直观,也许有几个细节注意一下会对理解整个hotplug的机制会有所帮助。首先是khelper_wq,这是一个工作队列,其创建发生在Linux系统初始化阶段:

void __init usermodehelper_init(void)
{
  khelper_wq = create_singlethread_workqueue("khelper");
  BUG_ON(!khelper_wq);
}

其次,call_usermodehelper_exec函数通过引入一个completion变量done来实现和工作节点subinfo->work上的延迟函数__call_usermodehelper的同步:函数通过queuework(khelper_wq,

&sub_info->work)将工作节点提交到elwq队列之后,将等待在

wait_for_completion(&done)语句上。可以猜想当延迟函数call_usermodehelper执行完毕,会通过complete函数来唤醒睡眠的call_usermodehelper_exec函数。

最后,来看看__call_usemodehelper要完成的工作:

static void __call_usermodehelper(struct work_struct *work)
{
  struct subprocess_info *sub_info =
    container_of(work, struct subprocess_info, work);
  pid_t pid;
  if (sub_info->wait & UMH_WAIT_PROC)
    pid = kernel_thread(wait_for_helper, sub_info,
            CLONE_FS | CLONE_FILES | SIGCHLD);
  else
    pid = kernel_thread(____call_usermodehelper, sub_info,
            SIGCHLD);
  if (pid < 0) {
    sub_info->retval = pid;
    umh_complete(sub_info);
  }
}

该函数会通过kernel_thread来生成一个新的进程,kernel_thread的调用将会导致call_usermodehelper中出现两条执行路径,一是父进程,二是子进程,也就是新产生的进程。父进程在调用kernel_thread后会直接返回,而子进程则需要等到首次被调度的机会才会从kernel_thread返回,因此函数接下来出现了三个case来处理父子进程间的同步问题,不过这不是这里要重点关注的话题。

因为当初在调用call_usermodehelper函数时指定的wait参数是UMHWAITEXEC,所以下面先按照这个路径进行讨论。kernel_thread的具体实现是应该关心的事情,这里我们只要知道它会产生一个新的进程,然后当该进程被调度执行时,call_usermodehelper函数会被调用,传递给它的参数是sub_info,那里带有要执行的用户空间进程的路径及环境变量等信息。

注意这个函数最后的complete(sub_info->complete),它将会唤醒睡眠的call_usermodehelper_exec函数。

再给出____call_usermodehelper的完整代码:

static int ____call_usermodehelper(void *data)
{
  struct subprocess_info *sub_info = data;
  struct cred *new;
  int retval;
  spin_lock_irq(&current->sighand->siglock);
  flush_signal_handlers(current, 1);
  spin_unlock_irq(&current->sighand->siglock);
  /* We can run anywhere, unlike our parent keventd(). */
  set_cpus_allowed_ptr(current, cpu_all_mask);
  /*
   * Our parent is keventd, which runs with elevated scheduling priority.
   * Avoid propagating that into the userspace child.
   */
  set_user_nice(current, 0);
  retval = -ENOMEM;
  new = prepare_kernel_cred(current);
  if (!new)
    goto out;
  spin_lock(&umh_sysctl_lock);
  new->cap_bset = cap_intersect(usermodehelper_bset, new->cap_bset);
  new->cap_inheritable = cap_intersect(usermodehelper_inheritable,
               new->cap_inheritable);
  spin_unlock(&umh_sysctl_lock);
  if (sub_info->init) {
    retval = sub_info->init(sub_info, new);
    if (retval) {
      abort_creds(new);
      goto out;
    }
  }
  commit_creds(new);
  retval = do_execve(getname_kernel(sub_info->path),
         (const char __user *const __user *)sub_info->argv,
         (const char __user *const __user *)sub_info->envp);
out:
  sub_info->retval = retval;
  /* wait_for_helper() will call umh_complete if UHM_WAIT_PROC. */
  if (!(sub_info->wait & UMH_WAIT_PROC))
    umh_complete(sub_info);
  if (!retval)
    return 0;
  do_exit(0);
}

所以call_usermodehelper执行完毕后,所在的进程将会因为do_exit的调用而从系统中消失掉。读者估计已经猜到kernel_execve函数用来在内核空间运行一个用户空间的进程,该进程的路径存放在sub_info->path中,进程运行时的环境变量等信息由sub_info->argv和sub_info->envp来提供。kernel_execve是个体系架构相关的函数。

目录
相关文章
【PCIe 协议】听说你做 PCIe 很多年,还不知道 PCIe Hierarchy ID 是什么 ???
【PCIe 协议】听说你做 PCIe 很多年,还不知道 PCIe Hierarchy ID 是什么 ???
485 0
【PCIe 协议】听说你做 PCIe 很多年,还不知道 PCIe Hierarchy ID 是什么 ???
|
Linux 虚拟化 监控
PERF EVENT 硬件篇
简介 本文将通过以 X86 为例子介绍硬件 PMU 如何为 linux kernel perf_event 子系统提供硬件性能采集功能 理解硬件 MSR (Model Specify Register) 可以理解为CPU硬件的专用寄存器,下述的所有寄存器都是这个类型 汇编指令 rdmsr/wrm.
3684 0
|
4月前
|
存储 固态存储 API
spdk关于nvme模块的实例helloword代码
spdk关于nvme模块的实例helloword代码
|
4月前
|
固态存储 网络协议 Linux
SPDK NVMe-oF Target
SPDK NVMe-oF Target
SPDK NVMe-oF Target
|
NoSQL 安全 Linux
ARM深入理解-hypervisor调试方法一(异常寄存器分析)
ARM深入理解-hypervisor调试方法一(异常寄存器分析)
|
开发工具 git
UART子系统(十四)编写虚拟UART驱动程序\_实现uart_ops
UART子系统(十四)编写虚拟UART驱动程序\_实现uart_ops
106 0
UART子系统(十四)编写虚拟UART驱动程序\_实现uart_ops
|
IDE 开发工具 数据格式
【最新技术早知道】PCIe Gen5 还没用上,Gen6 就来了?PCIe 6.0 系列文章之:《PCIe 6.0,到底 6 在哪?》
【最新技术早知道】PCIe Gen5 还没用上,Gen6 就来了?PCIe 6.0 系列文章之:《PCIe 6.0,到底 6 在哪?》
1199 0
|
Linux
【PCIe 6.0】PCIe 6.0 新特性 - DMWr (Deferrable Memory Write) 详解
【PCIe 6.0】PCIe 6.0 新特性 - DMWr (Deferrable Memory Write) 详解
948 0
【PCIe 6.0】PCIe 6.0 新特性 - DMWr (Deferrable Memory Write) 详解
可编程USB转 UART/I2C /SMBusS/SPI/CAN/1 -Wire适配器USB2S 常见问题及注意事项
当使用导线连接外部设备或芯片时,导线不可过长,一般控制在 20CM 以内,IIC、SPI、UART 等数字接口数据线驱动能力有限,过长的导线会导致通讯波形迟缓。当导线确实无法缩短时,可通过降低通讯速率的方法来解决、缓解通讯异常问题。
可编程USB转 UART/I2C /SMBusS/SPI/CAN/1 -Wire适配器USB2S  常见问题及注意事项