Linux设备驱动程式学习(5)-高级字符驱动程式操作[(2)阻塞型I/O和休眠]

本文深入探讨了Linux内核中的休眠机制,包括休眠的基本原则、如何安全地进入休眠状态、休眠的不同类型如简单休眠和高级休眠,以及如何使用内核提供的API来管理和唤醒休眠进程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 这一部分主要讨论:假如驱动程式无法立即满足请求,该如何响应?(65865346)
一、休眠
进程被置为休眠,意味着他被标识为处于一个特别的状态并且从调度器的运行队列中移走。这个进程将不被在任何 CPU 上调度,即将不会运行。 直到发生某些事情改变了那个状态。安全地进入休眠的两条规则:
(1) 永远不要在原子上下文中进入休眠,即当驱动在持有一个自旋锁、seqlock或 RCU 锁时不能睡眠;关闭中断也不能睡眠。持有一个信号量时休眠是合法的,但您应当仔细查看代码:假如代码在持有一个信号量时睡眠,任何其他的等待这个信号量的线程也会休眠。因此发生在持有信号量时的休眠必须短暂,而且决不能阻塞那个将最终唤醒您的进程。
(2)当进程被唤醒,他并不知道休眠了多长时间连同休眠时发生什么;也不知道是否另有进程也在休眠等待同一事件,且那个进程可能在他之前醒来并获取了所等待的资源。所以不能对唤醒后的系统状态做任何的假设,并必须重新检查等待条件来确保正确的响应
除非确信其他进程会在其他地方唤醒休眠的进程,否则也不能睡眠。使进程可被找到意味着:需要维护一个称为等待队列的数据结构。他是个进程链表,其中饱含了等待某个特定事件的任何进程。在 Linux 中, 一个等待队列由一个wait_queue_head_t 结构体来管理,其定义在中。wait_queue_head_t 类型的数据结构很简单:
struct __wait_queue_head {
    spinlock_t lock;
    struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
他包含一个自旋锁和一个链表。这个链表是个等待队列入口,他被声明做 wait_queue_t。wait_queue_head_t包含关于睡眠进程的信息和他想怎样被唤醒。
简单休眠(其实是高级休眠的宏)
Linux 内核中最简单的休眠方式是称为 wait_event的宏(及其变种),他实现了休眠和进程等待的条件的检查。形式如下:
wait_event(queue, condition)/*不可中断休眠,不推荐*/
wait_event_interruptible(queue, condition)/*推荐,返回非零值意味着休眠被中断,且驱动应返回 -ERESTARTSYS*/
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
/*有限的时间的休眠;若超时,则不管条件为何值返回0,*/
唤醒休眠进程的函数称为 wake_up,形式如下:
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
惯例:用 wake_up 唤醒 wait_event ;用 wake_up_interruptible 唤醒wait_event_interruptible。
简单休眠实验
模块程式链接:
sleepy
模块测试程式链接
sleepy-test
实验现象:
[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#insmod sleepy.ko
[Tekkaman2440@SBC2440V4]#cd /dev/
[Tekkaman2440@SBC2440V4]#cat /proc/devices
Character devices:
  1 mem
  2 pty
  3 ttyp
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
10 misc
13 input
14 sound
81 video4linux
89 i2c
90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
204 s3c2410_serial
252 sleepy
253 usb_endpoint
254 rtc
Block devices:
  1 ramdisk
256 rfd
  7 loop
31 mtdblock
93 nftl
96 inftl
179 mmc
[Tekkaman2440@SBC2440V4]#mknod -m 666 sleepy c 252 0
[Tekkaman2440@SBC2440V4]#cd /tmp/
[Tekkaman2440@SBC2440V4]#./sleepy_testr&
[Tekkaman2440@SBC2440V4]#./sleepy_testr&
[Tekkaman2440@SBC2440V4]#ps
  PID Uid VSZ Stat Command
    1 root 1744 S init
    2 root SW [kthreadd]
    3 root SWN [ksoftirqd/0]
    4 root SW [watchdog/0]
    5 root SW [events/0]
    6 root SW [khelper]
   59 root SW [kblockd/0]
   60 root SW [ksuspend_usbd]
   63 root SW [khubd]
   65 root SW [kseriod]
   77 root SW [pdflush]
   78 root SW [pdflush]
   79 root SW [kswapd0]
   80 root SW [aio/0]
  707 root SW [mtdblockd]
  708 root SW [nftld]
  709 root SW [inftld]
  710 root SW [rfdd]
  742 root SW [kpsmoused]
  751 root SW [kmmcd]
  769 root SW [rpciod/0]
  778 root 1752 S -sh
  779 root 1744 S init
  781 root 1744 S init
  783 root 1744 S init
  787 root 1744 S init
  799 root 1336 S ./sleepy_testr  800 root 1336 S ./sleepy_testr
  802 root 1744 R ps
[Tekkaman2440@SBC2440V4]#./sleepy_testw
read code=0
write code=0
[2] + Done ./sleepy_testr
[Tekkaman2440@SBC2440V4]#ps
  PID Uid VSZ Stat Command
    1 root 1744 S init
    2 root SW [kthreadd]
    3 root SWN [ksoftirqd/0]
    4 root SW [watchdog/0]
    5 root SW [events/0]
    6 root SW [khelper]
   59 root SW [kblockd/0]
   60 root SW [ksuspend_usbd]
   63 root SW [khubd]
   65 root SW [kseriod]
   77 root SW [pdflush]
   78 root SW [pdflush]
   79 root SW [kswapd0]
   80 root SW [aio/0]
  707 root SW [mtdblockd]
  708 root SW [nftld]
  709 root SW [inftld]
  710 root SW [rfdd]
  742 root SW [kpsmoused]
  751 root SW [kmmcd]
  769 root SW [rpciod/0]
  778 root 1752 S -sh
  779 root 1744 S init
  781 root 1744 S init
  783 root 1744 S init
  787 root 1744 S init
  799 root 1336 S ./sleepy_testr
  804 root 1744 R ps
[Tekkaman2440@SBC2440V4]#./sleepy_testw
write code=0
[Tekkaman2440@SBC2440V4]#read code=0
[1] + Done ./sleepy_testr
[Tekkaman2440@SBC2440V4]#ps
  PID Uid VSZ Stat Command
    1 root 1744 S init
    2 root SW [kthreadd]
    3 root SWN [ksoftirqd/0]
    4 root SW [watchdog/0]
    5 root SW [events/0]
    6 root SW [khelper]
   59 root SW [kblockd/0]
   60 root SW [ksuspend_usbd]
   63 root SW [khubd]
   65 root SW [kseriod]
   77 root SW [pdflush]
   78 root SW [pdflush]
   79 root SW [kswapd0]
   80 root SW [aio/0]
  707 root SW [mtdblockd]
  708 root SW [nftld]
  709 root SW [inftld]
  710 root SW [rfdd]
  742 root SW [kpsmoused]
  751 root SW [kmmcd]
  769 root SW [rpciod/0]
  778 root 1752 S -sh
  779 root 1744 S init
  781 root 1744 S init
  783 root 1744 S init
  787 root 1744 S init
  806 root 1744 R ps
阻塞和非阻塞操作
全功能的 read 和 write 方法涉及到进程能够决定是进行非阻塞 I/O还是阻塞 I/O操作。明确的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK 标志来指示(定义再 ,被 自动包含)。浏览源码,会发现O_NONBLOCK 的另一个名字:O_NDELAY ,这是为了兼容 System V 代码。O_NONBLOCK 标志缺省地被清除,因为等待数据的进程的正常行为只是睡眠.
其实不一定只有read 和 write 方法有阻塞操作,open也能够有阻塞操作。后面会见到。而我的项目有一个和CPLD的接口的驱动,我决定要在ioctl 中使用阻塞。
以下是后面的scullpipe实验的有关阻塞的代码,我觉得写得很好,先结合书上的介绍看看吧:
while (dev->rp == dev->wp)      { /* nothing to read */
     up(&dev->sem); /* release the lock */
     if (filp->f_flags & O_NONBLOCK)
         return -EAGAIN;
     PDEBUG("/"%s/" reading: going to sleep/n", current->comm);
     if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp)))
        return -ERESTARTSYS; /* signal: tell the fs layer to handle it */ /* otherwise loop, but first reacquire the lock */
     if (down_interruptible(&dev->sem))
        return -ERESTARTSYS;
}        /* ok, data is there, return something */
......
高级休眠
步骤:
(1)分配和初始化一个 wait_queue_t 结构, 随后将其添加到正确的等待队列。
(2)配置进程状态,标记为休眠。在  中定义有几个任务状态:TASK_RUNNING 意思是进程能够运行。有 2 个状态指示一个进程是在睡眠: TASK_INTERRUPTIBLE 和 TASK_UNTINTERRUPTIBLE。2.6 内核的驱动代码通常无需直接操作进程状态。但假如需要这样做使用的代码是:
void set_current_state(int new_state);
在老的代码中, 您常常见到如此的东西:current->state = TASK_INTERRUPTIBLE; 但是象这样直接改变 current 是不推荐的,当数据结构改变时这样的代码将会失效。通过改变 current 状态,只改变了调度器对待进程的方式,但进程还未让出处理器。
(3) 最后一步是放弃处理器。 但必须先检查进入休眠的条件。假如不做检查会引入竞态: 假如在忙于上面的这个过程时有其他的线程刚刚试图唤醒您,您可能错过唤醒且长时间休眠。因此典型的代码下:
if (!condition)
    schedule();
假如代码只是从 schedule 返回,则进程处于TASK_RUNNING 状态。 假如不需睡眠而跳过对 schedule 的调用,必须将任务状态重置为 TASK_RUNNING,还必要从等待队列中去除这个进程,否则他可能被多次唤醒。
手工休眠
/* (1)创建和初始化一个等待队列。常由宏定义完成:*/
DEFINE_WAIT(my_wait);
/*name 是等待队列入口项的名字. 也能够用2步来做:*/
wait_queue_t my_wait;
init_wait(&my_wait);
/*常用的做法是放一个 DEFINE_WAIT 在循环的顶部,来实现休眠。*/
/* (2)添加等待队列入口到队列,并配置进程状态:*/
void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state);
/*queue 和 wait 分别地是等待队列头和进程入口。state 是进程的新状态:TASK_INTERRUPTIBLE(可中断休眠,推荐)或TASK_UNINTERRUPTIBLE(不可中断休眠,不推荐)。*/
/* (3)在检查确认仍然需要休眠之后调用 schedule*/
schedule();
/* (4)schedule 返回,就到了清理时间:*/
void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);
认真地看简单休眠中的 wait_event(queue, condition) wait_event_interruptible(queue, condition) 底层源码会发现,其实他们只是手工休眠中的函数的组合。所以怕麻烦的话还是用wait_event比较好。
独占等待
当一个进程调用 wake_up 在等待队列上,任何的在这个队列上等待的进程被置为可运行的。 这在许多情况下是正确的做法。但有时,可能只有一个被唤醒的进程将成功获得需要的资源,而其余的将再次休眠。这时假如等待队列中的进程数目大,这可能严重降低系统性能。为此,内核研发者增加了一个“独占等待”选项。他和一个正常的睡眠有 2 个重要的不同:
(1)当等待队列入口配置了 WQ_FLAG_EXCLUSEVE 标志,他被添加到等待队列的尾部;否则,添加到头部。
(2)当 wake_up 被在一个等待队列上调用, 他在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止唤醒.但内核仍然每次唤醒任何的非独占等待。
采用独占等待要满足 2 个条件:
(1)希望对资源进行有效竞争;
(2)当资源可用时,唤醒一个进程就足够来完全消耗资源。
使一个进程进入独占等待,可调用:
void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);

注意:无法使用 wait_event 和他的变体来进行独占等待.
唤醒的相关函数
很少会需要调用wake_up_interruptible 之外的唤醒函数,但为完整起见,这里是整个集合:
wake_up(wait_queue_head_t *queue);
wake_up_interruptible(wait_queue_head_t *queue);
/*wake_up 唤醒队列中的每个非独占等待进程和一个独占等待进程。wake_up_interruptible 同样, 除了他跳过处于不可中断休眠的进程。他们在返回之前, 使一个或多个进程被唤醒、被调度(假如他们被从一个原子上下文调用, 这就不会发生).*/
wake_up_nr(wait_queue_head_t *queue, int nr);
wake_up_interruptible_nr(wait_queue_head_t *queue, int nr);
/*这些函数类似 wake_up, 除了他们能够唤醒多达 nr 个独占等待者, 而不只是个. 注意传递 0 被解释为请求任何的互斥等待者都被唤醒*/
wake_up_all(wait_queue_head_t *queue);
wake_up_interruptible_all(wait_queue_head_t *queue);
/*这种 wake_up 唤醒任何的进程, 不管他们是否进行独占等待(可中断的类型仍然跳过在做不可中断等待的进程)*/
wake_up_interruptible_sync(wait_queue_head_t *queue);
/*一个被唤醒的进程可能抢占当前进程, 并且在 wake_up 返回之前被调度到处理器。 但是, 假如您需要不要被调度出处理器时,能够使用 wake_up_interruptible 的"同步"变体. 这个函数最常用在调用者首先要完成剩下的少量工作,且不希望被调度出处理器时。*/
poll 和 select
当应用程式需要进行对多文档读写时,若某个文档没有准备好,则系统会处于读写阻塞的状态,并影响了其他文档的读写。为了避免这种情况,在必须使用多输入输出流又不想阻塞在他们任何一个上的应用程式常将非阻塞 I/O 和 poll(System V)、select(BSD Unix)、 epoll(linux2.5.45开始)系统调用配合使用。当poll函数返回时,会给出一个文档是否可读写的标志,应用程式根据不同的标志读写相应的文档,实现非阻塞的读写。这些系统调用功能相同: 允许进程来决定他是否可读或写一个或多个文档而不阻塞。这些调用也可阻塞进程直到任何一个给定集合的文档描述符可用来读或写。这些调用都需要来自设备驱动中poll 方法的支持,poll返回不同的标志,告诉主进程文档是否能够读写,其原型(定义在 ):
unsigned int (*poll) (struct file *filp, poll_table *wait);
实现这个设备方法分两步:
1. 在一个或多个可指示查询状态变化的等待队列上调用 poll_wait. 假如没有文档描述符可用来执行 I/O, 内核使这个进程在等待队列上等待任何的传递给系统调用的文档描述符. 驱动通过调用函数 poll_wait增加一个等待队列到 poll_table 结构,原型:
void poll_wait (struct file *, wait_queue_head_t *, poll_table *);
2. 返回一个位掩码:描述可能不必阻塞就立即进行的操作,几个标志(通过  定义)用来指示可能的操作:
标志
含义
POLLIN
假如设备无阻塞的读,就返回该值
POLLRDNORM
通常的数据已准备好,能够读了,就返回该值。通常的做法是会返回(POLLLIN|POLLRDNORA)
POLLRDBAND
假如能够从设备读出带外数据,就返回该值,他只可在linux内核的某些网络代码中使用,通常不用在设备驱动程式中
POLLPRI
假如能够无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致select报告文档发生异常,以为select八带外数据当作异常处理
POLLHUP
当读设备的进程到达文档尾时,驱动程式必须返回该值,依照select的功能描述,调用select的进程被告知进程时可读的。
POLLERR
假如设备发生错误,就返回该值。
POLLOUT
假如设备能够无阻塞地些,就返回该值
POLLWRNORM
设备已准备好,能够写了,就返回该值。通常地做法是(POLLOUT|POLLNORM)
POLLWRBAND
于POLLRDBAND类似
考虑 poll 方法的 scullpipe 实现:
static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
        struct scull_pipe *dev = filp->private_data;
        unsigned int mask = 0;
        /*
        * The buffer is circular; it is considered full
        * if "wp" is right behind "rp" and empty if the
        * two are equal.
        */
        down(&dev->sem);
        poll_wait(filp, &dev->inq, wait);
        poll_wait(filp, &dev->outq, wait);
        if (dev->rp != dev->wp)
                mask |= POLLIN | POLLRDNORM; /* readable */
        if (spacefree(dev))
                mask |= POLLOUT | POLLWRNORM; /* writable */
        up(&dev->sem);
        return mask;
}
和read 和write 的交互
正确实现poll调用的规则:
从设备读取数据:
(1)假如在输入缓冲中有数据,read 调用应当立即返回,即便数据少于应用程式需要的,并确保其他的数据会很快到达。 假如方便,可一直返回小于请求的数据,但至少返回一个字节。在这个情况下,poll 应当返回 POLLIN|POLLRDNORM。
(2)假如在输入缓冲中无数据,read默认必须阻塞直到有一个字节。若O_NONBLOCK 被置位,read 立即返回 -EAGIN 。在这个情况下,poll 必须报告这个设备是不可读(清零POLLIN|POLLRDNORM)的直到至少一个字节到达。
(3)若处于文档尾,不管是否阻塞,read 应当立即返回0,且poll 应该返回POLLHUP。
向设备写数据
(1)若输出缓冲有空间,write 应立即返回。他可接受小于调用所请求的数据,但至少必须接受一个字节。在这个情况下,poll应返回 POLLOUT|POLLWRNORM。
(2)若输出缓冲是满的,write默认阻塞直到一些空间被释放。若 O_NOBLOCK 被配置,write 立即返回一个 -EAGAIN。在这些情况下, poll 应当报告文档是不可写的(清零POLLOUT|POLLWRNORM). 若设备不能接受任何多余数据, 不管是否配置了 O_NONBLOCK,write 应返回 -ENOSPC("设备上没有空间")。
(3)永远不要让write在返回前等待数据的传输结束,即使O_NONBLOCK 被清除。若程式想确保他加入到输出缓冲中的数据被真正传送, 驱动必须提供一个 fsync 方法。
刷新待处理输出
若一些应用程式需要确保数据被发送到设备,就实现必须fsync 方法。对 fsync 的调用只在设备被完全刷新时(即输出缓冲为空)才返回,不管 O_NONBLOCK 是否被配置,即便这需要一些时间。其原型是:
int (*fsync) (struct file *file, struct dentry *dentry, int datasync);
底层数据结构
只要用户应用程式调用 poll、select、或epoll_ctl,内核就会调用这个系统调用所引用的任何文档的 poll 方法,并向他们传递同一个poll_table。 poll_table 结构只是构成实际数据结构的简单封装:
struct poll_table_struct;
/*
* structures and helpers for f_op->poll implementations
*/
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);
typedef struct poll_table_struct {
    poll_queue_proc qproc;
} poll_table;
对于 poll和 select系统调用,poll_table 是个包含 poll_table_entry 结构内存页链表。
struct poll_table_entry {
    struct file * filp;
    wait_queue_t wait;
    wait_queue_head_t * wait_address;
};
对 poll_wait 的调用有时还会将进程添加到给定的等待队列。整个的结构必须由内核维护,在 poll 或 select 返回前,进程可从任何的队列中去除, .
假如被轮询的驱动没有一个驱动程式指明可进行非阻塞I/O,poll 调用会简单地睡眠,直到一个他所在的等待队列(可能许多)唤醒他.
当 poll 调用完成,poll_table 结构被重新分配, 任何的之前加入到 poll 表的等待队列入口都会从表和他们的等待队列中移出.
struct poll_wqueues {
    poll_table pt;
    struct poll_table_page * table;
    int error;
    int inline_index;
    struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};
struct poll_table_page {
    struct poll_table_page * next;
    struct poll_table_entry * entry;
    struct poll_table_entry entries[0];
};



异步通知

通过使用异步通知,应用程式能够在数据可用时收到一个信号,而无需不停地轮询。
启用步骤:
(1)他们指定一个进程作为文档的拥有者:使用 fcntl 系统调用发出 F_SETOWN 命令,这个拥有者进程的 ID 被保存在 filp->f_owner。目的:让内核知道信号到达时该通知哪个进程。
(2)使用 fcntl 系统调用,通过 F_SETFL 命令配置 FASYNC 标志。
内核操作过程
1.F_SETOWN被调用时filp->f_owner被赋值。
2. 当 F_SETFL 被执行来打开 FASYNC, 驱动的 fasync 方法被调用.这个标志在文档被打开时缺省地被清除。
3. 当数据到达时,任何的注册异步通知的进程都会被发送一个 SIGIO 信号。
Linux 提供的通用方法是基于一个数据结构和两个函数,定义在。
数据结构:
struct fasync_struct {
    int    magic;
    int    fa_fd;
    struct    fasync_struct    *fa_next; /* singly linked list */
    struct    file         *fa_file;
};
驱动调用的两个函数的原型:
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
void kill_fasync(struct fasync_struct **fa, int sig, int band);
当一个打开的文档的FASYNC标志被修改时,调用fasync_helper 来从相关的进程列表中添加或去除文档。除了最后一个参数, 其他任何参数都时被提供给 fasync 方法的相同参数并被直接传递。 当数据到达时,kill_fasync 被用来通知相关的进程,他的参数是被传递的信号(常常是 SIGIO)和 band(几乎都是 POLL_IN)。
这是 scullpipe 实现 fasync 方法的:
static int scull_p_fasync(int fd, struct file *filp, int mode)
{
struct scull_pipe *dev = filp->private_data;
return fasync_helper(fd, filp, mode, &dev->async_queue);
}
当数据到达, 下面的语句必须被执行来通知异步读者. 因为对 sucllpipe 读者的新数据通过一个发出 write 的进程被产生, 这个语句出现在 scullpipe 的 write 方法中:
if (dev->async_queue)
kill_fasync(&dev->async_queue, SIGIO, POLL_IN); /* 注意, 一些设备也针对设备可写而实现了异步通知,在这个情况,kill_fasnyc 必须以 POLL_OUT 模式调用.*/
当文档被关闭时必须调用fasync 方法,来从活动的异步读取进程列表中删除该文档。尽管这个调用仅当 filp->f_flags 被配置为 FASYNC 时才需要,但不管什么情况,调用这个函数不会有问题,并且是普遍的实现方法。 以下是 scullpipe 的 release 方法的一部分:
/* remove this filp from the asynchronously notified filp's */
scull_p_fasync(-1, filp, 0);
异步通知使用的数据结构和 struct wait_queue 几乎相同,因为他们都涉及等待事件。区别异步通知用 struct file 替代 struct task_struct. 队列中的 file 用获取 f_owner, 一边给进程发送信号。
scullpipe的实验(poll和fasync方法的实现):

模块程式链接:
scullpipe
模块测试程式链接
scullpipe-test
ARM9实验板的实验现象是:
[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#insmod pipe.ko
[Tekkaman2440@SBC2440V4]#cat /proc/devices
Character devices:
  1 mem
  2 pty
  3 ttyp
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
10 misc
13 input
14 sound
81 video4linux
89 i2c
90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
204 s3c2410_serial
252 pipe
253 usb_endpoint
254 rtc
Block devices:
  1 ramdisk
256 rfd
  7 loop
31 mtdblock
93 nftl
96 inftl
179 mmc
[Tekkaman2440@SBC2440V4]#cd /dev/
[Tekkaman2440@SBC2440V4]#
[Tekkaman2440@SBC2440V4]#cd /tmp/
[Tekkaman2440@SBC2440V4]#./pipe_test &
[Tekkaman2440@SBC2440V4]#open scullpipe0 !
open scullpipe1 !
SCULL_P_IOCTSIZE : scull_p_buffer0=21 !
SCULL_P_IOCTSIZE : scull_p_buffer1=21 !
close pipetest0 !
close pipetest1 !
reopen scullpipe0 !
reopen scullpipe1 !
[Tekkaman2440@SBC2440V4]#echo 12345678901234567890 > /dev/scullpipe0
[Tekkaman2440@SBC2440V4]#read from pipetest0 code=20
[0]=1 [1]=2 [2]=3 [3]=4 [4]=5
[5]=6 [6]=7 [7]=8 [8]=9 [9]=0
[10]=1 [11]=2 [12]=3 [13]=4 [14]=5
[15]=6 [16]=7 [17]=8 [18]=9 [19]=0
read from pipetest0 code=1
[0]=
[1]=2 [2]=3 [3]=4 [4]=5
[5]=6 [6]=7 [7]=8 [8]=9 [9]=0
[10]=1 [11]=2 [12]=3 [13]=4 [14]=5
[15]=6 [16]=7 [17]=8 [18]=9 [19]=0
[Tekkaman2440@SBC2440V4]#echo 12345678901234 > /dev/scullpipe1
[Tekkaman2440@SBC2440V4]#read from pipetest1 code=15
[0]=1 [1]=2 [2]=3 [3]=4 [4]=5
[5]=6 [6]=7 [7]=8 [8]=9 [9]=0
[10]=1 [11]=2 [12]=3 [13]=4 [14]=
[15]=6 [16]=7 [17]=8 [18]=9 [19]=0
[Tekkaman2440@SBC2440V4]#ps
  PID Uid VSZ Stat Command
    1 root 1744 S init
    2 root SW [kthreadd]
    3 root SWN [ksoftirqd/0]
    4 root SW [watchdog/0]
    5 root SW [events/0]
    6 root SW [khelper]
   59 root SW [kblockd/0]
   60 root SW [ksuspend_usbd]
   63 root SW [khubd]
   65 root SW [kseriod]
   77 root SW [pdflush]
   78 root SW [pdflush]
   79 root SW [kswapd0]
   80 root SW [aio/0]
  707 root SW [mtdblockd]
  708 root SW [nftld]
  709 root SW [inftld]
  710 root SW [rfdd]
  742 root SW [kpsmoused]
  751 root SW [kmmcd]
  769 root SW [rpciod/0]
  778 root 1752 S -sh
  779 root 1744 S init
  781 root 1744 S init
  783 root 1744 S init
  785 root 1744 S init
  796 root 1344 S ./pipe_test
  797 root 1744 R ps
[Tekkaman2440@SBC2440V4]#./asynctest &
[Tekkaman2440@SBC2440V4]#echo 12345678901234 > /dev/scullpipe0
[Tekkaman2440@SBC2440V4]#12345678901234
close pipetest0 !
exit !
[2] + Done ./asynctest
[Tekkaman2440@SBC2440V4]#cat /proc/scullpipe
Default buffersize is 21
Device 0: c3e18494
   Queues: c04f1c34 c04f1c34
   Buffer: c3dc6c08 to c3dc6c1d (21 bytes)
   rp c3dc6c17 wp c3dc6c17
   readers 1 writers 0
Device 1: c3e18528
   Queues: c04f1c6c c04f1c6c
   Buffer: c3dc6b38 to c3dc6b4d (21 bytes)
   rp c3dc6b47 wp c3dc6b47
   readers 1 writers 0
Device 2: c3e185bc
   Queues: c3e185bc c3e185bc
   Buffer: 00000000 to 00000000 (0 bytes)
   rp 00000000 wp 00000000
   readers 0 writers 0
Device 3: c3e18650
   Queues: c3e18650 c3e18650
   Buffer: 00000000 to 00000000 (0 bytes)
   rp 00000000 wp 00000000
   readers 0 writers 0


本文来自ChinaUnix博客,假如查看原文请点:http://blog.chinaunix.net/u1/59291/showart_461599.html
import argparse import os import platform import sys import time from pathlib import Path import torch import cv2 import numpy as np import yaml import serial import traceback # 获取当前文件的绝对路径并解析根目录 FILE = Path(__file__).resolve() ROOT = FILE.parents[0] if str(ROOT) not in sys.path: sys.path.append(str(ROOT)) ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # 导入必要的模块 from models.common import DetectMultiBackend from utils.dataloaders import IMG_FORMATS, VID_FORMATS, LoadImages, LoadScreenshots, LoadStreams from utils.general import ( LOGGER, Profile, check_file, check_img_size, check_imshow, check_requirements, colorstr, increment_path, non_max_suppression, print_args, scale_boxes, strip_optimizer, xyxy2xywh ) from utils.plots import colors, save_one_box from utils.torch_utils import select_device, smart_inference_mode class Annotator: def __init__(self, im, line_width=None): # 初始化图像线条宽度 self.im = im self.lw = line_width or max(round(sum(im.shape) / 2 * 0.003), 2) def box_label(self, box, label='', color=(128, 128, 128)): # 在图像上绘制边界框标签 p1, p2 = (int(box[0]), int(box[1])), (int(box[2]), int(box[3])) cv2.rectangle(self.im, p1, p2, color, thickness=self.lw, lineType=cv2.LINE_AA) if label: tf = max(self.lw - 1, 1) w, h = cv2.getTextSize(label, 0, fontScale=self.lw / 3, thickness=tf)[0] outside = p1[1] - h >= 3 p2 = p1[0] + w, p1[1] - h - 3 if outside else p1[1] + h + 3 cv2.rectangle(self.im, p1, p2, color, -1, cv2.LINE_AA) cv2.putText(self.im, label, (p1[0], p1[1] - 2 if outside else p1[1] + h + 2), 0, self.lw / 3, (255, 255, 255), thickness=tf, lineType=cv2.LINE_AA) return self.im def result(self): # 返回标注后的图像 return self.im def read_config(config_path): try: # 读取配置文件 with open(config_path, 'r') as f: return yaml.safe_load(f) except FileNotFoundError: LOGGER.warning(f"未找到配置文件 '{config_path}',将使用默认值。") return {} def setup_serial(port, baudrate=115200): """直接尝试打开指定串口端口""" if not port: LOGGER.warning("未指定串口端口,跳过串口初始化") return None try: # 尝试打开串口 ser = serial.Serial(port, baudrate, timeout=1) LOGGER.info(f"成功打开串口 {port}") return ser except serial.SerialException as e: LOGGER.error(f"无法打开串口 {port}: {str(e)}") return None except Exception as e: LOGGER.error(f"串口初始化异常: {str(e)}") return None # 高精度测距类 class EnhancedDistanceCalculator: def __init__(self, focal_length, real_width): self.focal_length = focal_length self.real_width = real_width self.distance_history = [] self.stable_count = 0 self.stable_distance = 0.0 def calculate_distance(self, pixel_width, pixel_height): """改进的距离计算方法,考虑边界框的宽高比""" # 计算宽高比 aspect_ratio = pixel_width / pixel_height if pixel_height > 0 else 1.0 # 计算基础距离 base_distance = (self.real_width * self.focal_length) / pixel_width # 根据宽高比调整距离 # 当宽高比接近1时(球体正对相机),不需要调整 # 当宽高比偏离1时(球体倾斜),需要校正 aspect_factor = min(1.0, max(0.5, aspect_ratio)) # 限制在0.5-1.0之间 corrected_distance = base_distance * (1.0 + (1.0 - aspect_factor) * 0.3) # 使用历史数据平滑距离值 self.distance_history.append(corrected_distance) if len(self.distance_history) > 5: self.distance_history.pop(0) # 计算平均距离 avg_distance = sum(self.distance_history) / len(self.distance_history) # 稳定性检测 if abs(avg_distance - self.stable_distance) < 0.2: # 小于20cm变化认为是稳定的 self.stable_count += 1 else: self.stable_count = 0 self.stable_distance = avg_distance # 当连续稳定5帧以上才使用当前距离 if self.stable_count > 5: return self.stable_distance return avg_distance def calculate_angle(cx, cy, width, height): """计算物体中心点相对于图像中心的角度""" origin_x, origin_y = width // 2, height // 2 dx, dy = cx - origin_x, origin_y - cy # 计算角度(弧度) angle_rad = np.arctan2(dy, dx) # 转换为角度(0-360度) angle_deg = np.degrees(angle_rad) if angle_deg < 0: angle_deg += 360 return angle_deg @smart_inference_mode() def run( weights=ROOT / 'combined_model.pt', # 使用合并后的模 source=ROOT / 'data/images', data=ROOT / 'data/coco128.yaml', ball_diameter=9, imgsz=(1920, 1080), conf_thres=0.25, iou_thres=0.45, max_det=1000, device='CPU', view_img=True, save_txt=False, save_conf=False, save_crop=False, nosave=False, classes=None, agnostic_nms=False, augment=False, visualize=False, update=False, project=ROOT / 'runs/detect', name='exp', exist_ok=False, line_thickness=3, hide_labels=False, hide_conf=False, half=False, dnn=False, vid_stride=1, config_path='config.yaml', known_width=0.2, # 校准物体实际宽度 known_distance=2.0, # 校准物体已知距离 ref_pixel_width=100, # 校准物体像素宽度 ball_real_width=0.2, # 篮球实际宽度 hoop_real_width=1.0, # 篮筐实际宽度 serial_port=None, # 指定串口端口 serial_baud=115200, # 串口波特率 serial_interval=3, # 串口发送间隔(帧数) ): # 处理输入源 source = str(source) save_img = not nosave and not source.endswith('.txt') is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS) is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://')) webcam = source.isnumeric() or source.endswith('.txt') or (is_url and not is_file) screenshot = source.lower().startswith('screen') if is_url and is_file: source = check_file(source) # 创建保存目录 save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) if save_txt: (save_dir / 'labels').mkdir(parents=True, exist_ok=True) else: save_dir.mkdir(parents=True, exist_ok=True) # 选择设备并加载模 device = select_device(device) model = DetectMultiBackend(weights, device=device, dnn=dnn, data=data, fp16=half) stride, names, pt = model.stride, model.names, model.pt imgsz = check_img_size(imgsz, s=stride) # 计算焦距 (必须放在数据集初始化前) if known_width <= 0 or known_distance <= 0 or ref_pixel_width <= 0: raise ValueError("[ERROR] Calibration parameters must be positive values!") focal_length = (ref_pixel_width * known_distance) / known_width print(f"[Calibration] Focal Length = {focal_length:.2f} px·m") # ========== 焦距计算结束 ========== # 初始化高精度测距器 ball_distance_calculator = EnhancedDistanceCalculator(focal_length, ball_real_width) hoop_distance_calculator = EnhancedDistanceCalculator(focal_length, hoop_real_width) # 初始化数据集 - 增加视频流稳定性处理 bs = 1 retry_count = 0 max_retries = 5 dataset = None while retry_count < max_retries: try: if webcam: view_img = check_imshow(warn=True) dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt, vid_stride=vid_stride) bs = len(dataset) break # 如果成功则跳出循环 elif screenshot: dataset = LoadScreenshots(source, img_size=imgsz, stride=stride, auto=pt) bs = 1 break else: dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt, vid_stride=vid_stride) bs = 1 break except Exception as e: retry_count += 1 LOGGER.warning(f"视频流初始化失败,尝试重新连接 ({retry_count}/{max_retries})...") time.sleep(2) # 等待2秒后重试 if dataset is None: LOGGER.error(f"无法初始化视频流,请检查输入源: {source}") return vid_path, vid_writer = [None] * bs, [None] * bs # 打开串口 - 直接使用指定端口 ser = setup_serial(serial_port, serial_baud) if serial_port else None if ser and not ser.is_open: LOGGER.warning("串口未成功打开,继续无串口模式运行") ser = None elif ser: LOGGER.info(f"串口连接已建立: {serial_port}@{serial_baud}bps") # 模预热 model.warmup(imgsz=(1 if pt or model.triton else bs, 3, *imgsz)) seen, windows, dt = 0, [], (Profile(), Profile(), Profile()) # 存储检测到的篮球篮筐信息 detected_basketball = None detected_hoop = None # 帧计数器用于控制串口发送频率 frame_counter = 0 for path, im, im0s, vid_cap, s in dataset: frame_counter += 1 with dt[0]: # 预处理图像 im = torch.from_numpy(im).to(model.device).half() if model.fp16 else torch.from_numpy(im).to( model.device).float() im /= 255 if len(im.shape) == 3: im = im[None] with dt[1]: # 模推理 visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False pred = model(im, augment=augment, visualize=visualize) with dt[2]: # 非极大值抑制 # 确保结果在CPU上以便后续处理 pred = [x.cpu() for x in non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)] # 重置检测信息 detected_basketball = None detected_hoop = None for i, det in enumerate(pred): seen += 1 p, im0, frame = (path[i], im0s[i].copy(), dataset.count) if webcam else ( path, im0s.copy(), getattr(dataset, 'frame', 0)) p = Path(p) save_path = str(save_dir / p.name) txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') s += '%gx%g' % im.shape[2:] gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] imc = im0.copy() if save_crop else im0 annotator = Annotator(im0, line_width=line_thickness) # 获取图像尺寸 height, width = im0.shape[:2] if len(det): # 确保在CPU上 det = det.cpu() # 调整检测框坐标 det[:, :4] = scale_boxes(im.shape[2:], det[:, :4], im0.shape).round() s += ', '.join([f"{(det[:, 5] == c).sum()} {names[int(c)]}{'s' * ((det[:, 5] == c).sum() > 1)}" for c in det[:, 5].unique()]) # 遍历所有检测结果 for *xyxy, conf, cls in reversed(det): try: # 确保坐标是普通数值 xyxy = [x.item() for x in xyxy] # 计算像素宽度高度 pixel_width = xyxy[2] - xyxy[0] pixel_height = xyxy[3] - xyxy[1] # 获取类别信息 class_id = int(cls) class_name = names[class_id] # 根据类别选择测距器参数 if class_name == 'basketball': # 篮球 distance = ball_distance_calculator.calculate_distance(pixel_width, pixel_height) # 计算篮球中心位置 cx = (xyxy[0] + xyxy[2]) / 2 cy = (xyxy[1] + xyxy[3]) / 2 # 计算角度信息 angle_deg = calculate_angle(cx, cy, width, height) # 更新篮球信息 if detected_basketball is None or distance < detected_basketball['distance']: detected_basketball = { 'distance': distance, 'angle_deg': angle_deg, 'cx': cx, 'cy': cy } # 创建标签 label = f'篮球 {conf:.2f} {distance:.2f}m {angle_deg:.1f}°' # 在图像上标记篮球中心点 cv2.circle(im0, (int(cx), int(cy)), 5, (0, 0, 255), -1) elif class_name == 'hoop': # 篮筐 distance = hoop_distance_calculator.calculate_distance(pixel_width, pixel_height) # 计算篮筐中心位置 cx = (xyxy[0] + xyxy[2]) / 2 cy = (xyxy[1] + xyxy[3]) / 2 # 计算角度信息 angle_deg = calculate_angle(cx, cy, width, height) # 更新篮筐信息 if detected_hoop is None or distance < detected_hoop['distance']: detected_hoop = { 'distance': distance, 'angle_deg': angle_deg, 'cx': cx, 'cy': cy } # 创建标签 label = f'篮筐 {conf:.2f} {distance:.2f}m {angle_deg:.1f}°' # 在图像上标记篮筐中心点 cv2.circle(im0, (int(cx), int(cy)), 5, (0, 255, 0), -1) else: # 其他类别 distance = ball_distance_calculator.calculate_distance(pixel_width, pixel_height) cx = (xyxy[0] + xyxy[2]) / 2 cy = (xyxy[1] + xyxy[3]) / 2 angle_deg = calculate_angle(cx, cy, width, height) label = f'{class_name} {conf:.2f} {distance:.2f}m' # 绘制边界框 im0 = annotator.box_label(xyxy, label=label, color=colors(class_id, True)) # 在终端打印信息 print(f"检测到的 {class_name} - 距离: {distance:.2f}m, 角度: {angle_deg:.1f}°") except Exception as e: print(f"[ERROR] 处理检测结果失败: {e}") traceback.print_exc() # 串口通信 - 按照新数据格式发送 if frame_counter % serial_interval == 0: if ser and ser.is_open: try: # 发送篮球数据 (格式: "1,{angle:.2f},{distance:.2f}\n") if detected_basketball: ball_message = f"1,{detected_basketball['angle_deg']:.2f},{detected_basketball['distance']:.2f}\n" ser.write(ball_message.encode('ascii')) LOGGER.debug(f"📤 发送篮球数据: {ball_message.strip()}") # 发送篮筐数据 (格式: "0,{angle:.2f},{distance:.2f}\n") if detected_hoop: hoop_message = f"0,{detected_hoop['angle_deg']:.2f},{detected_hoop['distance']:.2f}\n" ser.write(hoop_message.encode('ascii')) LOGGER.debug(f"📤 发送篮筐数据: {hoop_message.strip()}") # 如果没有检测到任何物体,发送空行保持连接 if not detected_basketball and not detected_hoop: ser.write(b'\n') # 发送空行 LOGGER.debug("📤 发送空行") except serial.SerialException as e: LOGGER.error(f"🚨 串口发送失败: {str(e)}") ser.close() ser = None # 标记串口失效 # 在图像上绘制中心点 origin_x, origin_y = width // 2, height // 2 cv2.circle(im0, (origin_x, origin_y), 10, (255, 0, 0), -1) # 在图像上绘制十字线 cv2.line(im0, (0, origin_y), (width, origin_y), (0, 255, 0), 2) cv2.line(im0, (origin_x, 0), (origin_x, height), (0, 255, 0), 2) # 显示检测信息 info_y = 30 if detected_basketball: info_text = f"篮球: {detected_basketball['distance']:.2f}m, {detected_basketball['angle_deg']:.1f}°" cv2.putText(im0, info_text, (10, info_y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) info_y += 40 if detected_hoop: info_text = f"篮筐: {detected_hoop['distance']:.2f}m, {detected_hoop['angle_deg']:.1f}°" cv2.putText(im0, info_text, (10, info_y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) info_y += 40 if not detected_basketball and not detected_hoop: info_text = "未检测到目标" cv2.putText(im0, info_text, (10, info_y), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) im0 = annotator.result() if view_img: # 显示图像 if platform.system() == 'Linux' and p not in windows: windows.append(p) cv2.namedWindow(str(p), cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO) cv2.resizeWindow(str(p), im0.shape[1], im0.shape[0]) cv2.imshow(str(p), im0) if cv2.waitKey(1) == ord('q'): # 按q退出 break if save_img: # 保存图像或视频 if dataset.mode == 'image': cv2.imwrite(save_path, im0) else: if vid_path[i] != save_path: if isinstance(vid_writer[i], cv2.VideoWriter): vid_writer[i].release() fps, w, h = (vid_cap.get(cv2.CAP_PROP_FPS), int(vid_cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))) if vid_cap else ( 30, im0.shape[1], im0.shape[0]) save_path = str(Path(save_path).with_suffix('.mp4')) vid_writer[i] = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h)) vid_writer[i].write(im0) LOGGER.info(f"{s}{'' if len(det) else '(no detections), '}{dt[1].dt * 1E3:.1f}ms") t = tuple(x.t / seen * 1E3 for x in dt) LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {(1, 3, *imgsz)}' % t) if save_txt or save_img: s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else '' LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}") if update: strip_optimizer(weights[0]) if ser and ser.is_open: ser.close() LOGGER.info("🔌 串口连接已关闭") def parse_opt(): # 解析命令行参数 parser = argparse.ArgumentParser() parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'runs/train/exp33/weights/best.pt', help='模路径或triton URL') parser.add_argument('--source', type=str, default='2', help='文件/目录/URL/glob/屏幕/0(摄像头)') parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='(可选)数据集yaml路径') parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='推理尺寸h,w') parser.add_argument('--conf-thres', type=float, default=0.25, help='置信度阈值') parser.add_argument('--iou-thres', type=float, default=0.45, help='NMS IoU阈值') parser.add_argument('--max-det', type=int, default=1000, help='每张图像最大检测数') parser.add_argument('--device', default='cuda', help='cuda设备, 例如 0 或 0,1,2,3 或 cpu') parser.add_argument('--view-img', action='store_true', help='显示结果') parser.add_argument('--save-txt', action='store_true', help='保存结果到*.txt') parser.add_argument('--save-conf', action='store_true', help='在--save-txt标签中保存置信度') parser.add_argument('--save-crop', action='store_true', help='保存裁剪的预测框') parser.add_argument('--nosave', action='store_true', help='不保存图像/视频') parser.add_argument('--classes', nargs='+', type=int, help='按类别过滤: --classes 0, 或 --classes 0 2 3') parser.add_argument('--agnostic-nms', action='store_true', help='类别不可知的NMS') parser.add_argument('--augment', action='store_true', help='增强推理') parser.add_argument('--visualize', action='store_true', help='可视化特征') parser.add_argument('--update', action='store_true', help='更新所有模') parser.add_argument('--project', default=ROOT / 'runs/detect', help='保存结果到项目/名称') parser.add_argument('--name', default='exp', help='保存结果到项目/名称') parser.add_argument('--exist-ok', action='store_true', help='现有项目/名称可用,不增加') parser.add_argument('--line-thickness', default=3, type=int, help='边界框厚度(像素)') parser.add_argument('--hide-labels', default=False, action='store_true', help='隐藏标签') parser.add_argument('--hide-conf', default=False, action='store_true', help='隐藏置信度') parser.add_argument('--half', action='store_true', help='使用FP16半精度推理') parser.add_argument('--dnn', action='store_true', help='使用OpenCV DNN进行ONNX推理') parser.add_argument('--vid-stride', type=int, default=1, help='视频帧步长') parser.add_argument('--known-width', type=float, default=0.2, help='参考物体的已知宽度()') parser.add_argument('--known-distance', type=float, default=2.0, help='参考物体的已知距离()') parser.add_argument('--ref-pixel-width', type=float, default=100, help='图像中参考物体的像素宽度') parser.add_argument('--ball-real-width', type=float, default=0.2, help='篮球的实际宽度()') parser.add_argument('--hoop-real-width', type=float, default=1.0, help='篮筐的实际宽度()') # 串口参数 parser.add_argument('--serial-port', type=str, default=None, help='指定串口端口 (例如 COM3 或 /dev/ttyUSB0)') parser.add_argument('--serial-baud', type=int, default=115200, help='串口波特率 (默认: 115200)') parser.add_argument('--serial-interval', type=int, default=3, help='串口发送间隔 (帧数, 默认: 每3帧发送一次)') # ========== 参数添加结束 ========== opt = parser.parse_args() opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 print_args(vars(opt)) return opt def main(opt): # 主函数,调用run函数 run(**vars(opt)) if __name__ == "__main__": # 解析命令行参数并运行主函数 opt = parse_opt() main(opt) 检测到的 ball - 距离: 5.35m, 角度: 183. 99 16:01:42.188 0.00010520 python.exe IRP_MJ_WRITE COM7 SUCCESS Length: 1, Data: 0A 这些是我使用串口时的数据 很明显串口的发送有问题请你帮我改进,请不要增加代码的运算负担,增加不必要的功能
最新发布
07-09
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值