一、等待队列

1.定义一个等待队列及初始化
1)动态初始化

wait_queue_head_t  wq;       //全局变量
init_waitqueue_head(&wq);   //安装模块时候执行了初始化

2)静态初始化

DECLARE_WAIT_QUEUE_HEAD(wq);    //这句等效于上面两句

功能:建立一个等待队列,并且初始化好
参数:
wq:是一个类型wait_queue_head_t的变量,就是驱动自己定义的等待队列头。

2.等待队列睡眠

wait_event(wq, condition)
//功能:建立不可以杀进程(信号不能唤醒,效果和msleep相同)。wait_event_interruptible(wq, condition)
//功能:它可以被信号唤醒。休眠过程中,进程可以接收信号,收到后不管条件如何,直接返回。wait_event_timeout(wq, condition, timeout)
//功能:休眠期间效果和 wait_event ,但是有一个超时时间 ,时间到不管条件如何,直接返回。wait_event_interruptible_timeout(wq, condition, timeout)
//功能:休眠期间效果和 wait_event_interruptible相同。区别是有超时功能,时间到不管条件如何,直接返回。

参数:
wq: 是一个类型wait_queue_head_t的变量,就是驱动自己定义的等待队列头。
Condition :可以为任何类型,通常定义为整形,值为0进入休眠,值为非0直接返回。
Timeout :超时时间。

3.等待队列的唤醒

wake_up(wq)  //常用
//功能:用于唤醒各种方式进入休眠的进程,只唤醒队列上的一个进程。wake_up_all(wq)
//功能:效果和wake_up相同,只是能唤醒队列上所有的进程。wake_up_interruptible(wq) //常用
//功能:只能用于唤醒一个 使用wait_event_interruptible*休眠的进程。wake_up_interruptible_all(wq)
//功能:能唤醒队列所有 使用wait_event_interruptible*休眠的进程。

参数:
wq: 是一个类型wait_queue_head_t指针。

代码例子:
驱动层;

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/gpio.h>
#include <asm/uaccess.h>
#include <linux/delay.h>      //msleep//1)第一步: 添加相关头文件
#include <linux/wait.h>        //等待队列相关数据结构及函数
#include <linux/sched.h>       //进程状态宏定义//2)定义一个等待队列头变量,并且初始化
static DECLARE_WAIT_QUEUE_HEAD(wq);//按键数量
#define BTN_SIZE   4
//按键缓冲区,'0'表示没有按键,'1'表示按下了
static char keybuf[] = {"0000"};
/*把一个当成一个对象来看待,方便编程,定义一个描述按键结构*/
struct button_desc {int  gpio;   //存放io口编号int  number; //存放按键编号,根据自己需要设计,char *name;  //按键名字,随便,但是要有意义
};
/* 定义4个按键的信息 */
static struct button_desc buttons[] = {{ EXYNOS4_GPX3(2), 0, "KEY0" },{ EXYNOS4_GPX3(3), 1, "KEY1" },{ EXYNOS4_GPX3(4), 2, "KEY2" },{ EXYNOS4_GPX3(5), 3, "KEY3" },
};
init_waitqueue_head/* 按键动作标志,在中断程序中置1,read成功复制数据后清0 */
int press = 0;//中断服务函数
irqreturn_t key_isr(int irq, void* dev)
{//存放按键状态int dn = 0;int index = 0;//这里进行还原原来的类型struct button_desc *bdata = (struct button_desc *)dev;//把读取到的结果取逻辑非,因为程序设计使用正逻辑。'1'表示按下。dn = !gpio_get_value(bdata->gpio);//取得当前中断对应 的按键编号index = bdata->number;//把按键状态更新到对应的按缓冲中keybuf[index] = dn + '0';//输出按键提示//  printk("%s %s\r\n", bdata->name, dn ? "down" : "up");//4) 在等待条件变成真的地方调用  wake_up*函数唤醒休眠的进程press = 1;wake_up_interruptible(&wq) ;//通知内核到wq这个队列头上的链表去查询每一个休眠的进程 的条件是否变成了真。//如果进程等待的条件还没有是真,则继续休眠。return IRQ_HANDLED;
}static ssize_t tiny4412_read (struct file *flp,char __user *buff,size_t count,loff_t * off)
{int ret = 0;//用户传递0,直接返回if(!count) {return 0;}//修正参数if(count > BTN_SIZE ) {count = BTN_SIZE;}/* 没有按键动作( 按下和松开时候 )*/if (!press) {if (flp->f_flags & O_NONBLOCK) { //调用open("/dev/button",flags) --》flags 存放在filp->f_flags = flagsreturn -EAGAIN;} else {//3)在需要休眠的地方使用wait_event*函数进行进行。//休眠,等待有按键动作唤醒进程。wait_event_interruptible(wq, press);}}/* 清标志 */press = 0;//复制数据到用户空间ret = copy_to_user(buff, keybuf, count);if(ret) {printk("error:copy_to_user\r\n");return -EFAULT;}return count;
}static const struct file_operations dev_fops = {.read   =   tiny4412_read,.owner  =   THIS_MODULE,
};#define LEDS_MAJOR  255   //255
#define DEVICE_NAME  "mybtn"
static struct miscdevice misc = {.minor = LEDS_MAJOR, //次设备号.name  = DEVICE_NAME,//设备名.fops  = &dev_fops,  //文件操作方法
};static int __init btn_init(void)
{int ret;int irq;int i;int flags;flags = IRQ_TYPE_EDGE_BOTH; //设置为双边触发for ( i = 0; i < 4 ; i++ ) {//得到中断号irq = gpio_to_irq(buttons[i].gpio); //keyX//注册中断ret = request_irq(irq, key_isr, flags, buttons[i].name, (void*)&buttons[i]);if(ret < 0) {break;}}//如果不是全部成功,则反向注销已经注册的中断if(ret < 0) {for ( --i; i; i-- ) {irq = gpio_to_irq(buttons[i].gpio); //keyXdisable_irq(irq);free_irq(irq, (void*)&buttons[i]);}return ret;}//注册杂项设备ret = misc_register(&misc);       //注册混杂设备return ret;
}static void __exit btn_exit(void)
{int i = 0;int irq;//注销中断for (i = 0; i < 4; i++) {irq = gpio_to_irq(buttons[i].gpio); //keyXdisable_irq(irq);free_irq(irq, (void*)&buttons[i]);}//注销杂项设备misc_deregister(&misc);
}module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");

应用层:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>    //lseek
#include <sys/ioctl.h> //ioctl
#include <poll.h>      //pollchar save_buf[10] = {"0000"};   //存放数据使用int main(void)
{int fd;                      //存放文件描述符号int ret;int i;struct pollfd fds[1];//以非阻塞方式打开//fd = open("/dev/mybtns", O_RDWR | O_NONBLOCK );//以读写方式进行打开,默认是阻塞方式打开的fd = open("/dev/mybtns", O_RDWR );if(fd < 0) {printf("open error\r\n");return -1;}//实际程序需要循环读取按键动作,然后根据动作完成不同事情while(1) {char cur_buf[10] = {0};   //临时存放数据使用fds[0].fd     = fd;fds[0].events = POLLIN;  //要监测读事件//ret = poll(fds, 1, -1);   //   永远阻塞直到有变化 //ret = poll(fds, 1, 0);      //   非阻塞ret = poll(fds, 1, 2000); //   2秒超时//判断查询结果if(ret < 0) {perror("poll");exit(0);} else if(ret == 0) {printf("timeout\r\n");continue;} else {//分别判断每个fdif(fds[0].revents & POLLIN) {//回读当前的4个灯状态read(fd, cur_buf, 4);for(i = 0; i < 4; i++) {if(cur_buf[i] != save_buf[i]) {save_buf[i] = cur_buf[i] ; //更新当前按键状态if(save_buf[i] == '1') {printf("K%d press\r\n", i + 1);} else {printf("K%d up\r\n", i + 1);}printf("keys:%s\r\n", save_buf);}}printf("keys:%s\r\n", save_buf);}}}//关闭文件close(fd);return 0;
}

二、poll接口

1.驱动层

void poll_wait(struct file * pfile, wait_queue_head_t * wait_address, poll_table *p)

参数:
pfile:由unsigned int xxxx_poll(struct file *filp,struct poll_table_struct *wait)第一个参数传递
wait_address:上面的定义并且初始化的等待队列头wq
p:由unsigned int xxxx_poll(struct file *filp,struct poll_table_struct *wait)第二个参数传递
返回值:成功:返回设备的状态掩码( 正数):可读,可写,出错掩码;失败:负数,错误码。

返回值掩码 含义
POLLIN 如果设备无阻塞的读,就返回该值
POLLRDNORM 通常的数据已经准备好,可以读了,就返回该值。
POLLERR 如果设备发生错误,就返回该值。
POLLOUT 如果设备可以无阻塞地写,就返回该值
POLLWRNORM 设备已经准备好,可以写了,就返回该值。

设备可读,通常返回: (POLLIN | POLLRDNORM)
设备可写,通常返回: (POLLOUT | POLLWRNORM)

2.应用层

int poll(struct pollfd fd[], nfds_t nfds, int timeout)

功能:可以阻塞/非阻塞地监测多个文件的可读、 可写、 错误事件发生。 poll 函数退出后, struct pollfd 变量的fd,events 值被清零,需要重新设置, revents 变量包含了监测结果。
参数:
fd:表示被监视的文件描述符(不用申明,需要定义和初始化赋值),结构如下:

struct pollfd {
int fd; //文件描述符
short events; //请求的事件
short revents; //返回的事件
};

fd:打开文件的文件描述符
events:传入需要监测事件
revents:传出返回的事件

nfds:要监视的文件描述符的数量。
timeout:大于0 :等待指定数目的毫秒数;0 :立即返回,不阻塞进程;-1 :永远等待, 直到有任何一个监测的文件描述符发生变化
返回值:
大于0 : fd 数组中准备好读,写或出错状态的那些文件描述符号的总数量( 我们要关心这种情况)
等于0 : 超时
小于0 : 调用函数失败

返回值 意义
POLLIN 普通或优先级带数据可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级数据可读
POLLOUT 普通数据可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件

注意:后三个只能作为描述字的返回结果存储在 revents 中,而不能作为测试条件用于 events 中。

三、select函数

1.应用层 (对应设备驱动的 poll接口)

int select(int nfds,fd_set *readset, fd_set *writeset,fd_set *exceptset, struct timeval*timeout)
//exceptset 三个集合中的 fd, 所以如果想检测它们, 则需要在返回后再次添加。

参数说明:
ndfs: select 监视的文件文件描述符中值最大值+1。
readset: select 监视的可读文件描述符集合。可以传入 NULL 值,表示不关心任何文件的读变化。
writeset: select 监视的可写文件描述符集合。可以传入 NULL 值,表示不关心任何文件的写变化。
exceptset: select 监视的异常文件描述符集合。
timeout:本次 select()的超时结束时间。
时间结构定义如下:

struct timeval{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
}

返回值:
大于 0:执行成功则返回文件描述符状态已改变的个数;
等于0:代表已超过 timeout 时间, 文件描述符状态还没有发生改变;
等于-1:函数有错误发生错误原因存于 errno,此时参数 readset, writeset, exceptset 和 timeout 的值变成不可预测。

代码例子:
驱动层代码:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/gpio.h>
#include <asm/uaccess.h>
#include <linux/delay.h>      //msleep
#include <linux/poll.h>        //poll接口的相关定义及函数//1)第一步: 添加相关头文件
#include <linux/wait.h>        //等待队列相关数据结构及函数
#include <linux/sched.h>       //进程状态宏定义//2)定义一个等待队列头变量,并且初始化
static DECLARE_WAIT_QUEUE_HEAD(wq);//按键数量
#define BTN_SIZE   4//按键缓冲区,'0'表示没有按键,'1'表示按下了
static char keybuf[] = {"0000"};/*把一个当成一个对象来看待,方便编程,定义一个描述按键结构*/
struct button_desc {int  gpio;   //存放io口编号int  number; //存放按键编号,根据自己需要设计,char *name;  //按键名字,随便,但是要有意义
};/* 定义4个按键的信息 */
static struct button_desc buttons[] = {{ EXYNOS4_GPX3(2), 0, "KEY0" },{ EXYNOS4_GPX3(3), 1, "KEY1" },{ EXYNOS4_GPX3(4), 2, "KEY2" },{ EXYNOS4_GPX3(5), 3, "KEY3" },
};/* 按键动作标志,在中断程序中置1,read成功复制数据后清0 */
int press = 0;//中断服务函数
irqreturn_t key_isr(int irq, void* dev)
{//存放按键状态int dn = 0;int index = 0;//这里进行还原原来的类型struct button_desc *bdata = (struct button_desc *)dev;//把读取到的结果取逻辑非,因为程序设计使用正逻辑。'1'表示按下。dn = !gpio_get_value(bdata->gpio);//取得当前中断对应 的按键编号index = bdata->number;//把按键状态更新到对应的按缓冲中keybuf[index] = dn + '0';//输出按键提示//  printk("%s %s\r\n", bdata->name, dn ? "down" : "up");//4) 在等待条件变成真的地方调用  wake_up*函数唤醒休眠的进程press = 1;wake_up_interruptible(&wq) ;//通知内核到wq这个队列头上的链表去查询每一个休眠的进程 的条件是否变成了真。//如果进程等待的条件还没有是真,则继续休眠。return IRQ_HANDLED;
}static ssize_t tiny4412_read (struct file *flp, char __user *buff,size_t count,loff_t * off)
{int ret = 0;//用户传递0,直接返回if(!count) {return 0;}//修正参数if(count > BTN_SIZE ) {count = BTN_SIZE;}/* 没有按键动作( 按下和松开时候 )*/if (!press) {if (flp->f_flags & O_NONBLOCK) { //调用open("/dev/button",flags) --》flags 存放在filp->f_flags = flagsreturn -EAGAIN;} else {//  while(press == 0) {  //这个循环中不能是独占类型代码,否则进程会死循环。 不能是while(press==0);//      msleep(5);       //休眠,进程放弃CPU,这种休眠后不可被信号中断。// }//3)在需要休眠的地方使用wait_event*函数进行进行。//休眠,等待有按键动作唤醒进程。wait_event_interruptible(wq, press);}}/* 清标志 */press = 0;//复制数据到用户空间ret = copy_to_user(buff, keybuf, count);if(ret) {printk("error:copy_to_user\r\n");return -EFAULT;}return count;
}
//轮询接口
//这个函数执行时候不会引起阻塞,
unsigned int tiny4412_poll (struct file *pfile, struct poll_table_struct *wait)
{unsigned int mask = 0;       //一定要初始化为0,因为mask是局部变量//1)调用 poll_wait 把当前进程添加到等待队列中poll_wait(pfile, &wq, wait); //这个函数不会引起进程的阻塞//2)返回设备状态掩码(是否可读可写标志)if(press) {mask = POLLIN | POLLRDNORM;}return mask;
}static const struct file_operations dev_fops = {.read   =   tiny4412_read,.poll   =   tiny4412_poll,.owner  =   THIS_MODULE,
};#define LEDS_MAJOR  255   //255
#define DEVICE_NAME  "mybtns"static struct miscdevice misc = {.minor = LEDS_MAJOR, //次设备号.name  = DEVICE_NAME,//设备名.fops  = &dev_fops,  //文件操作方法
};static int __init btn_init(void)
{int ret;int irq;int i;int flags;flags = IRQ_TYPE_EDGE_BOTH; //设置为双边触发for ( i = 0; i < 4 ; i++ ) {//得到中断号irq = gpio_to_irq(buttons[i].gpio); //keyX//注册中断ret = request_irq(irq, key_isr, flags, buttons[i].name, (void*)&buttons[i]);if(ret < 0) {break;}}//如果不是全部成功,则反向注销已经注册的中断if(ret < 0) {for ( --i; i; i-- ) {irq = gpio_to_irq(buttons[i].gpio); //keyXdisable_irq(irq);free_irq(irq, (void*)&buttons[i]);}return ret;}//注册杂项设备ret = misc_register(&misc);       //注册混杂设备return ret;
}static void __exit btn_exit(void)
{int i = 0;int irq;//注销中断for (i = 0; i < 4; i++) {irq = gpio_to_irq(buttons[i].gpio); //keyXdisable_irq(irq);free_irq(irq, (void*)&buttons[i]);}//注销杂项设备misc_deregister(&misc);
}module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");

应用层代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>    //lseek
#include <sys/ioctl.h> //ioctl
#include <poll.h>      //poll//select
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>#define   poll_or_select    0//poll:1;select:0char save_buf[10] = {"0000"};   //存放数据使用int main(void)
{int fd;                      //存放文件描述符号int ret;int i;
#if      poll_or_selectstruct pollfd fds[1];
#elsefd_set readset ;             //定义监测读集合struct timeval timeout;
#endif    //以非阻塞方式打开//fd = open("/dev/mybtns", O_RDWR | O_NONBLOCK );//以读写方式进行打开,默认是阻塞方式打开的fd = open("/dev/mybtns", O_RDWR );if(fd < 0) {printf("open error\r\n");return -1;}//实际程序需要循环读取按键动作,然后根据动作完成不同事情while(1) {char cur_buf[10] = {0};   //临时存放数据使用
#if  poll_or_select    fds[0].fd     = fd;fds[0].events = POLLIN;  //要监测读事件//ret = poll(fds, 1, -1);   //   永远阻塞直到有变化 //ret = poll(fds, 1, 0);      //   非阻塞ret = poll(fds, 1, 2000); //   2秒超时
#else//必须每重新设备,因为每次select返回时。timeout变成值会清成为0timeout.tv_sec   = 2;timeout.tv_usec  = 0;//一般在重新添加监测对象时候前需要前0全部fdFD_ZERO(&readset);        //清集合//必须每次重新添加要监测的对象,因为每次select返回时。readset变成部分值会清成为0FD_SET(fd, &readset);     //添加监测对象到集合//永远阻塞直到有文件状态发生变化ret = select(fd+1,&readset,NULL,NULL,NULL);//2秒超时//ret = select(fd + 1, &readset, NULL, NULL, &timeout);
#endif        //判断查询结果if(ret < 0) {perror("poll"); perror("select");exit(0);} else if(ret == 0) {printf("timeout\r\n");continue;} else {//分别判断每个fd
#if  poll_or_select              if(fds[0].revents & POLLIN)
#else                if(FD_ISSET(fd, &readset))
#endif                 {//回读当前的4个灯状态read(fd, cur_buf, 4);for(i = 0; i < 4; i++) {if(cur_buf[i] != save_buf[i]) {save_buf[i] = cur_buf[i] ; //更新当前按键状态if(save_buf[i] == '1') {printf("K%d press\r\n", i + 1);} else {printf("K%d up\r\n", i + 1);}printf("keys:%s\r\n", save_buf);}}printf("keys:%s\r\n", save_buf);}}}//关闭文件close(fd);return 0;
}

驱动等待队列,poll和select编程相关推荐

  1. (十二)Linux内核驱动之poll和select

    使用非阻塞 I/O 的应用程序常常使用 poll, select, 每个允许一个进程来决定它是否可读或者写一个或多个文件而不阻塞. 这些调用也可阻塞进程直到任何一个给定集合的文件描述符可用来读或写.  ...

  2. V4L2视频驱动程序开发之驱动方法poll 和 应用程序select

    V4L2视频驱动程序开发已经进入尾声,本次视频支持多个通道的stream同时传输,即有多个设备文件关联到驱动.最高支持48个stream同时输入. 应用程序在获取stream的时候,需要用到selec ...

  3. Java网络编程与NIO详解13:epoll、poll、select面试题汇总

    文章目录 一.文件描述符与IO模型 二.端口和地址复用 三.select 四.poll 五.epoll 六.相关面试题 1.epoll读到一半又有新事件来了怎么办? 一.文件描述符与IO模型   文件 ...

  4. linux 内核驱动的poll,Linux驱动基石之POLL机制

    来源:百问网 作者:韦东山 本文字数:2344,阅读时长:4分钟 1.适用场景 在前面引入中断时,我们曾经举过一个例子: 妈妈怎么知道卧室里小孩醒了? 时不时进房间看一下:查询方式 简单,但是累 进去 ...

  5. poll和select

    一.概述 应用程序可以使用poll,select,epoll三种形式,其中poll和select由两个不同的Unix团队分别实现的:select在BSD Unix中引入,而poll由System V引 ...

  6. linux 内核驱动的poll,详细解读Linux内核的poll机制

    所有的系统调用,基于都可以在它的名字前加上"sys_"前缀,这就是它在内核中对应的函数.比如系统调用open.read.write.poll,与之对应的内核函数为:sys_open ...

  7. epoll 的实现原理以及与poll,select 的对比

    最近面试的时候 被问到epoll的问题,就下来查一查,看到有篇文章不错,就记录下来,供大家参考学习. 以一个生活中的例子来解释. 假设你在大学中读书,要等待一个朋友来访,而这个朋友只知道你在A号楼,但 ...

  8. linux 内核驱动的poll,嵌入式Linux驱动开发(五)——poll机制原理以及驱动实现...

    前情回顾: 再开始今天的内容之前,先简单review一下,我们都用了什么方案来获取按键值,他们的特点都是什么.只有不断地理清了思路,我们才能够更好的理解,为何会出现如此多的解决方案,当遇到问题的时候, ...

  9. 微软视窗驱动模型(WDM)编程指南

    第一章           开始一个驱动项目 在本章中我将对驱动开发的历史作一个简要的回顾.从80年代中期起,我开始涉足个人计算领域,那时也正是IBM刚刚开始出售搭配着MS-DOS操作系统的个人电脑. ...

最新文章

  1. Servlet基础:接口、类、请求响应、配置、会话追踪、上下文、协作、异常
  2. 【手把手教你全文检索】Apache Lucene初探
  3. mysql怎样循环插入数据_你向 Mysql 数据库插入 100w 条数据用了多久?
  4. WordPress 多媒体库添加分类和标签支持
  5. java中的构造方法与代码块
  6. 前端:JS/24/BOM和DOM简介,for...in循环遍历,window对象的属性和方法,延时器,定时器,screen屏幕对象,location地址栏对象,history历史记录对象
  7. 编程函数c语言,C语言编程(练习1:函数 )
  8. 【ElasticSearch】es 线程池 ThreadPool 的封装
  9. 开源搜索服务 Apache Solr 出现多个高危漏洞
  10. Snake算法与遥感影像应用,python matlab对比
  11. 基于特征点匹配的车辆跟踪
  12. Unity3D应用防外挂与防破解
  13. 038--想和权证恋个爱
  14. 苏州大学计算机科学与技术研究生院,苏州大学计算机科学与技术学院第十四届研究生代表大会...
  15. 解决谷歌浏览器打不开
  16. 学大伟业 Day 3 培训总结
  17. 拓尔思信息科技股份有限公司2019校园春季招聘
  18. macbook linux 双系统,MacOS+Ubuntu双系统,原来MacBook安装linux也简单!
  19. Android factory reset 流程
  20. 联想(Lenovo) 小新M7268W 黑白激光无线WiFi打印多功能一体机 出现:打印机故障:显示扫描单元未找到初始位置 或者 扫描单元马达故障 解决办法

热门文章

  1. OSPF外部路由汇总
  2. 第五周课程总结试验报告三
  3. HDU 6356.Glad You Came-线段树(区间更新+剪枝) (2018 Multi-University Training Contest 5 1007)...
  4. thinkphp 学习_4中URL模式
  5. oc_转_类的数组的实现和操作
  6. 由浅至深 谈谈.NET混淆原理(三)-- 流程混淆
  7. Java实现阶乘的和
  8. 数据库-linux安装mysql
  9. 三相pmsm矢量控制仿真模型_实时控制系统的时序模型及其在AUTOSAR系统仿真监控中的应用-Foundations4.1控制理论...
  10. 5个让前端代码变得简洁的最佳实践