概述
中断驱动学习(1)
几个简单的中断API
【入门】
在编写中断驱动之前先熟悉一下可能会用到的一些接口吧。
1、中断申请
点击(此处)折叠或打开
- int request_irq(
- unsigned int irq, //中断号
- irq_handler_t handler, //中断处理函数
- unsigned long flags, //中断标志:触发方式[#define IRQF_*]
- const char *name, //中断名称,通常是设备驱动程序的名称
- //使用cat /proc/interrupts可以查看
- void *dev_id); //一般设置为设备结构体,或者不使用时为NULL
返回值:
a) 返回0表示成功;
b) 返回-EINVAL表示无效的参数,如果返回这个值,应该看看传递给
request_irq()的参数是否正确;
c) 返回-EBUSY表示中断已经被占用且不能共享;
d) 返回ENOMEM表示内存不足。嵌入式系统由于内存资源有限,经常
会发生这样的错误。
中断处理函数说明:[这是一个回调函数]
点击(此处)折叠或打开
- typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);
2、中断注销
点击(此处)折叠或打开
- void free_irq(
- unsigned int irq, //中断号
- void *dev_id); //对应request_irq()参数中的dev
3、开启中断
点击(此处)折叠或打开
- void enable_irq(unsigned int irq); //中断号
4、关闭中断
点击(此处)折叠或打开
- void disable_irq(unsigned int irq); //中断号
由于在disable_irq() 中会调用synchronize_irq() 函数等待中断返回,所以在中断处理程序中不能使用disable_irq,否则会导致cpu被synchronize_irq() 独占而发生系统崩溃.
【扩展】
(1)请求中断(request_irq)与开启中断(enable_irq)的区别?
追根溯源:
用source insight跟踪request_irq() 函数可以发现,实际上 request_irq() 调用的是 request_threaded_irq() 这个函数,而在 request_threaded_irq() 中调用的最后一个函数就是 enable_irq()。
由此,我们可以做个比喻:
request_irq() 是一个戴着假面的温柔绅士,他在试图占用你(中断)之前会先询问你的意见(实际上是中断控制器管的),如果你不愿意,他就会默默离开,继续等待下一个目标;如果你愿意,他就露出 enable_irq() 的本性了。
再看 enable_irq() ,enable_irq() 是一个崇尚武力征服的蛮族(比如说:罗马、日本),如果他看中了你,不会屁颠屁颠地跑到你面前问你愿不愿意,而是直截了当地闯入你的领地,我们把 enable_irq() 这种强悍的仅供方式称为:单刀直入、直捣黄龙。
同理,free_irq()中也应该包含disable_irq(),不信?自己找找看吧。
【参考】
《Linux中断处理驱动程序编写 》
http://blog.csdn.net/gotosola/article/details/7422072中断驱动学习(2)
基于Timer0中断控制的led流水灯驱动
实现平台:Ubuntu 14.04 + TQ2440
实现工具:arm-linux-gcc + SecureCRT + Samba
实现内容:
用户空间 内核空间 实现结果
open pwm_irq_open pwm初始化和irq请求
close pwm_irq_close 停止pwm输出
ioctl pwm_led_ioctl 启动或关闭定时器
实现过程:
[1] 原材料:头文件
点击(此处)折叠或打开
- #ifndef _PWM_LED_
- #define _PWM_LED_
-
- #include <linux/kernel.h>
- #include <linux/init.h>
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/types.h>
- #include <linux/cdev.h>
- #include <linux/uaccess.h>
- #include <mach/gpio-nrs.h>
- #include <mach/regs-gpio.h>
- #include <linux/clk.h>
- #include <linux/interrupt.h>
- #include <asm/irq.h>
- #include <mach/hardware.h>
- #include <plat/regs-timer.h>
- #include <mach/regs-irq.h>
- #include <asm/mach/time.h>
- #include <linux/irq.h>
- #include <mach/hardware.h>
- #include <linux/platform_device.h>
- #include <asm/io.h>
-
- //#define DEVICE_MAJOR 250
- //#define DEVICE_MINOR 0
- #define DEVICE_NAME "PWM_LED"
- #define LED_OFF 0 //led灭
- #define LED_ON 1 //led亮
-
- //led端口查询表
- static unsigned long led_table[] =
- {
- S3C2410_GPB5,
- S3C2410_GPB6,
- S3C2410_GPB7,
- S3C2410_GPB8,
- };
-
- //led输出端口表
- static unsigned int led_cfg_out[] =
- {
- S3C2410_GPB5_OUTP,
- S3C2410_GPB6_OUTP,
- S3C2410_GPB7_OUTP,
- S3C2410_GPB8_OUTP,
- };
-
- //定时器相关寄存器和参数
- volatile unsigned long *tcfg0 = NULL; //用来设置预分频
- volatile unsigned long *tcfg1 = NULL; //用来设置分频
- volatile unsigned long *tcon = NULL; //定时器控制器
- volatile unsigned long *tcntb0 = NULL; //计数缓冲寄存器
- volatile unsigned long *tcmpb0 = NULL; //比较缓冲寄存器
- volatile unsigned long *tcnto0 = NULL; //计数观察寄存器
-
- struct clk *clk_p;
- unsigned long pclk;
-
- //设备号和设备结构
- dev_t dev;
- int g_nMajor = 0;
- int g_nMinor = 0;
- struct cdev *pwm_cdev = NULL;
- static struct class *pwm_class;
-
- //定时器中断描述
- struct pwm_irq_desc
- {
- unsigned int irq_nb;
- unsigned long irq_flag;
- char *irq_name;
- };
-
- //定时器中断描述符,为其他定时器预留,这里只测试TIMER0
- static struct pwm_irq_desc pwm_irqs[] =
- {
- {IRQ_TIMER0, IRQF_TRIGGER_FALLING, "PWM_TIMER0" },
- {IRQ_TIMER1, IRQF_TRIGGER_FALLING, "PWM_TIMER1"},
- {IRQ_TIMER2, IRQF_TRIGGER_FALLING, "PWM_TIMER2"},
- {IRQ_TIMER3, IRQF_TRIGGER_FALLING, "PWM_TIMER3"},
- {IRQ_TIMER4, IRQF_TRIGGER_FALLING, "PWM_TIMER4"},
- };
- // IRQF_TRIGGER_FALLING 也可以改为定时器专用的 IRQF_TIMER
-
- /********************************************************************
- *函数接口声明
- ********************************************************************/
- static irqreturn_t irq_interrupt(int irq, void *dev_id);
- static int pwm_irq_close(struct inode *inode, struct file *file);
- static int pwm_irq_open(struct inode *inode, struct file *file);
- static int pwm_led_ioctl(struct inode *inode, struct file *file,
- unsigned int cmd, unsigned long arg);
-
- /********************************************************************
- *设备文件操作接口
- ********************************************************************/
- static struct file_operations dev_fops = {
- .owner = THIS_MODULE,
- .open = pwm_irq_open,
- .release = pwm_irq_close,
- .ioctl = pwm_led_ioctl,
- };
- #endif // _PWM_LED_
[2] 模块框架
点击(此处)折叠或打开
- static int __init pwm_module_init(void)
- {
- ;初始化led
- ;注册设备{这部分在《基于TQ2440的led字符设备驱动》中分解的很详细了}
- }
-
- static void __init pwm_module_exit(void)
- {
- ;注销设备{这部分在《基于TQ2440的led字符设备驱动》中分解的很详细了}
- }
-
- module_init(pwm_module_init);
- module_exit(pwm_module_exit);
-
- MODULE_LICENSE("Dual BSD/GPL");
- MODULE_DESCRIPTION("PWM Led Test");
- MODULE_AUTHOR("Reyn-2014.05.27");
[3] 操作接口
由于我们实际上要的效果是流水灯,所以只要实现两个或者三个接口即可。而open和close肯定是需要设计的,那么如果你嫌麻烦,可以直接在open中实现流水灯;如果不怕麻烦,就增加一个ioctl也不为过吧。
本文加上了ioctl,总共三个操作接口,在头文件中的dev_fops中有声明。
下面逐一介绍。
[4] 逐个介绍
[+] pwm_open() 主要的工作是请求定时器0中断,在中断中实现流水灯,流水灯我们在接触ARM阶段的时候应该都有写过,就不需要特别说明了,现在感觉写驱动好像也不那么难吧?笔者倒是觉得,比起ARM阶段把工作手册DataSheet翻来翻去找寄存器的时候更加难受。申请完irq之后,还要一个操作,那就是初始化pwm。所以,这个过程除了对寄存器的操作有点技巧之外,并不难实现,下面贴出代码:
点击(此处)折叠或打开
- /********************************************************************
- *函数名称: pwm_irq_open
- *函数描述: 申请定时器中断
- *返回值 : request_irq的处理结果
- ********************************************************************/
- static int pwm_irq_open(struct inode *inode, struct file *file)
- {
- int nRet = 0;
- printk(KERN_INFO "[Kernel] try pwm_irq_open, we'll request irq.n");
- printk(KERN_INFO "[Kernel] irq_nb[%d],irq_flag[%d],irq_name[%s].n",
- IRQ_TIMER0, IRQ_TYPE_EDGE_FALLING/*IRQF_TIMER*/, "PWM_LED");
- //请求Timer0中断
- nRet = request_irq(IRQ_TIMER0, irq_interrupt, IRQ_TYPE_EDGE_FALLING,
- "PWM_LED", NULL);
- printk(KERN_INFO "[Kernel] request irq and returns %d.n",nRet);
- //如果请求失败,则禁止并释放该中断
- if(nRet < 0)
- {
- disable_irq(IRQ_TIMER0);
- free_irq(IRQ_TIMER0, NULL);
- printk(KERN_INFO "[Error] request_irq fail.n");
- return -1;
- }
-
- //初始化pwm
- pwm_init();
-
- return nRet;
- }
-
- 然后是中断注册函数:
- /********************************************************************
- *函数名称: irq_interrupt
- *函数描述: 定时器中断执行函数
- *返回值 : IRQ_NONE / IRQ_HANDLED / IRQ_WAKE_THREAD
- ********************************************************************/
- static irqreturn_t irq_interrupt(int irq, void *dev_id)
- {
- static int nLedNb = 0;
- static int irq_count = 0;
-
- switch(nLedNb%4)
- {
- case 0:
- {
- s3c2410_gpio_setpin(S3C2410_GPB5, ~(LED_ON));
- s3c2410_gpio_setpin(S3C2410_GPB6, ~(LED_OFF));
- s3c2410_gpio_setpin(S3C2410_GPB7, ~(LED_OFF));
- s3c2410_gpio_setpin(S3C2410_GPB8, ~(LED_OFF));
- printk(KERN_INFO "[Kernel] led1 on. ");
- nLedNb++;
- }
- break;
-
- case 1:
- {
- s3c2410_gpio_setpin(S3C2410_GPB5, ~(LED_OFF));
- s3c2410_gpio_setpin(S3C2410_GPB6, ~(LED_ON));
- s3c2410_gpio_setpin(S3C2410_GPB7, ~(LED_OFF));
- s3c2410_gpio_setpin(S3C2410_GPB8, ~(LED_OFF));
- printk(KERN_INFO "[Kernel] led2 on. ");
- nLedNb++;
- }
- break;
-
- case 2:
- {
- s3c2410_gpio_setpin(S3C2410_GPB5, ~(LED_OFF));
- s3c2410_gpio_setpin(S3C2410_GPB6, ~(LED_OFF));
- s3c2410_gpio_setpin(S3C2410_GPB7, ~(LED_ON));
- s3c2410_gpio_setpin(S3C2410_GPB8, ~(LED_OFF));
- printk(KERN_INFO "[Kernel] led3 on. ");
- nLedNb++;
- }
- break;
-
- case 3:
- {
- s3c2410_gpio_setpin(S3C2410_GPB5, ~(LED_OFF));
- s3c2410_gpio_setpin(S3C2410_GPB6, ~(LED_OFF));
- s3c2410_gpio_setpin(S3C2410_GPB7, ~(LED_OFF));
- s3c2410_gpio_setpin(S3C2410_GPB8, ~(LED_ON));
- printk(KERN_INFO "[Kernel] led4 on. ");
- nLedNb++;
- }
- break;
-
- default:
- printk(KERN_INFO "[Kernel] unrecognize led number. ");
- break;
- }
-
- irq_count++;
- printk(KERN_INFO "[IRQ] counts %d.n", irq_count);
-
- return IRQ_RETVAL(IRQ_HANDLED);
- }
-
- 最后是初始化pwm的代码:
- /********************************************************************
- *函数名称: pwm_init
- *函数描述: 定时器初始化
- *返回值 : void
- ********************************************************************/
- static void pwm_init(void)
- {
- //对寄存器的操作可以使用io端口映射——ioremap
- //实际上是因为笔者在内核目录下找不到针对tcntb0/tcnto0的宏操作函数
- //如果你们找到了,麻烦请通知我一声
- tcfg0 = (volatile unsigned long *)ioremap(0x51000000, 4);
- tcfg1 = (volatile unsigned long *)ioremap(0x51000004, 4);
- tcon = (volatile unsigned long *)ioremap(0x51000008, 4);
- tcntb0 = (volatile unsigned long *)ioremap(0x5100000c, 4);
- tcnto0 = (volatile unsigned long *)ioremap(0x51000014, 4);
-
- printk(KERN_INFO "[Kernel] pwm_init.n");
- //定时器时钟频率=pclk /( 预分频器的值+1) / 分频器
- *tcfg0 &= ~0xff; //定时器0预分配清零
- *tcfg0 |=(157-1); //预分频
- *tcfg1 &= ~0xf; //定时器0 mux 输入分频清零
- *tcfg1 |=3; //mux 分频 16
-
- clk_p = clk_get(NULL, "pclk");
- pclk = clk_get_rate(clk_p); //get pclk,实际得到的pclk=50000000
- printk("pclk = %ldn",pclk);
-
- *tcntb0 &=0x0000; //计数缓冲器,先清零
- *tcntb0 |=20003;
- printk("tcntb0=%ldn", *tcntb0);
- }
[+] pwm_irq_close () ,顾名思义,就是清理中断了,另外还要停止pwm输出,代码实现起来也是很简单的:
点击(此处)折叠或打开
- /********************************************************************
- *函数名称: pwm_irq_close
- *函数描述: 注销定时器中断
- *返回值 : 成功返回0,失败返回其他值
- ********************************************************************/
- static int pwm_irq_close(struct inode *inode, struct file *file)
- {
- pwm_stop();
- free_irq(IRQ_TIMER0, NULL);
- printk(KERN_INFO "[Kernel] free irq done.n");
- return 0;
- }
-
- pwm_stop()的实现如下:
- /********************************************************************
- *函数名称: pwm_stop
- *函数描述: 停止定时器
- *返回值 : void
- ********************************************************************/
- static void pwm_stop(void)
- {
- *tcon &= ~1;
- //__raw_writel(tcon, S3C2410_TCON);
- //对寄存器的写操作也可以使用__raw_writel
- printk(KERN_INFO "[Kernel] pwm stop now!n");
- }
[+] pwm_led_ioctl(),就是控制定时器的开启和关闭了,这里的实现略微人性化一点,用户空间向内核空间发送指令(开启和关闭定时器),若符合,则进行相应操作;若不符合,则点亮第一盏led等作为错误表示,详细见代码:
点击(此处)折叠或打开
- /********************************************************************
- *函数名称: pwm_led_ioctl
- *函数描述: 定时器初始化
- * cmd: 0代表开启定时器,1代表停止定时器
- * 若cmd不符合,则点亮led1以示错误
- *返回值 : 成功返回0,失败返回其他值
- ********************************************************************/
- static int pwm_led_ioctl(struct inode *inode, struct file *file,
- unsigned int cmd, unsigned long arg)
- {
- printk(KERN_INFO "[Kernel] do operations with pwm_led_ioctl.n");
- if(cmd < 0 || cmd > 1 )
- {
- s3c2410_gpio_setpin(led_table[0], ~(LED_ON));
- s3c2410_gpio_setpin(led_table[1], ~(LED_OFF));
- s3c2410_gpio_setpin(led_table[2], ~(LED_OFF));
- s3c2410_gpio_setpin(led_table[3], ~(LED_OFF));
- //对GPIO的操作也可以使用ioremap
- printk(KERN_INFO "[Error] unrecognize cmd, led1 on.n");
- return -1;
- }
-
- switch(cmd)
- {
- case 0: pwm_start();break;
- case 1: pwm_stop();break;
- default:break;
- }
- return 0;
- }
- /********************************************************************
- *函数名称: pwm_start
- *函数描述: 启动定时器
- *返回值 : void
- ********************************************************************/
- static void pwm_start(void)
- {
- *tcon &= ~0x1f;
- *tcon |= 0xb; //start timer
- //__raw_writel(tcon, S3C2410_TCON);
-
- *tcon &= ~2;
- //__raw_writel(tcon, S3C2410_TCON);
-
- printk(KERN_INFO "[Kernel] pwm start now.n");
- }
[5] 测试运行
又到了Makefile时间,其实对于驱动的Makefile而言,基本上一个Makefile就是一个框架,写完一个,就基本可以无限套用了。还是贴出来吧:
点击(此处)折叠或打开
- # PWM_LED Module Makefile
-
- OBJ := pwm_led.o
- KERNDIR := /opt/EmbedSky/linux-2.6.30.4/
- INSTALL := /home/reyn/share/out/
- CC := arm-linux-gcc
-
- obj-m := $(OBJ)
-
- all:
- $(MAKE) -C $(KERNDIR) M=$(PWD) modules
- cp *.ko $(INSTALL)
-
- modules:
- $(MAKE) -C $(KERNDIR) M=$(PWD) modules
-
- install:
- cp *.ko $(INSTALL)
-
- .PHONY:modules clean
- clean:
- rm *.mod.c *.o *.order *.ko *.symvers *~
- rm $(INSTALL)*.ko
-
-
- 终端输入make编译:
-
- reyn@ubuntu:pwm_led$ make
- make -C /opt/EmbedSky/linux-2.6.30.4/ M=/home/reyn/share/drivers/pwm_led modules
- make[1]: 正在进入目录 `/opt/EmbedSky/linux-2.6.30.4'
- CC [M] /home/reyn/share/drivers/pwm_led/pwm_led.o
- /home/reyn/share/drivers/pwm_led/pwm_led.h:76: warning: 'pwm_irqs' defined but not used
- Building modules, stage 2.
- MODPOST 1 modules
- LD [M] /home/reyn/share/drivers/pwm_led/pwm_led.ko
- make[1]:正在离开目录 `/opt/EmbedSky/linux-2.6.30.4'
- cp *.ko /home/reyn/share/out/
未发现任何错误,继续,写个测试程序验证一下效果:
点击(此处)折叠或打开
- reyn@ubuntu:test$ gedit test_pwm.c &
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <malloc.h>
- #include <sys/ioctl.h>
-
- int main(int argc, char **argv)
- {
- int nFd = 0;
- int ch = 0;
- int cmd = 0;
- unsigned long arg = 0;
-
- printf("[User] open pwm, it will request irq.n");
- nFd = open("/dev/PWM_LED", O_RDWR);
- if(nFd < 0)
- {
- printf("[User] Can't open %sn", "/dev/PWM_LED");
- return -1;
- }
-
- ioctl:
- printf("[User] Ready for ioctl.n");
- while((ch = getchar()) != 'n');
- ioctl(nFd, cmd);
- printf("[User] ioctl%ld cmd:%d arg:%ld.n", arg, cmd, arg);
- arg++;
- while((ch = getchar()) != 'n');
- cmd = !(cmd);//令cmd反转,可以实现控制定时器开关的效果,真棒!
- goto ioctl;
-
- close(nFd);
- }
测试程序的编译,咱就不说了吧。
废话不说,上板测试:
[$] 第一步,加载模块:
点击(此处)折叠或打开
- [root@EmbedSky /]# insmod pwm_led.ko
- [Kernel] led init start.
- [Kernel] led init done, all should be off.
- [Kernel] request memory for pwm_cdev now.
- [Kernel] request dev number now.
- [Kernel] [Device:PWM_LED][major:252][minor:0].
- [Kernel] add operation interfaces to device.
- [Kernel] auto create device node.
- [Kernel] succedd to insert PWM_LED module.
[$] 第二步:运行测试程序:
首先修改权限为777:
chmod 777 test_pwm
然后运行之:
点击(此处)折叠或打开
- [root@EmbedSky /]# ./test_pwm
- [User] open pwm, it will request irq.
- [Kernel] try pwm_irq_open, we'll request irq.
- [Kernel] irq_nb[26],irq_flag[2],irq_name[PWM_LED].
- [Kernel] request irq and returns 0.
- [Kernel] pwm_init.
- pclk = 50000000
- tcntb0=20003
- [User] Ready for ioctl.
- | //这里在等待用户输入,以进行下一步操作,当输入回车之后,定时器开启
- [Kernel] do operations with pwm_led_ioctl.
- [Kernel] pwm start now.
- [User] ioctl0 cmd:0 arg:0.
- [Kernel] led1 on. <6>[IRQ] counts 1. //进入irq中断,开始中断次数计数
- [Kenerl] led2 on. <6>[IRQ] counts 2.
- [Kernel] led3 on. <6>[IRQ] counts 3.
- [Kernel] led4 on. <6>[IRQ] counts 4.
- [Kernel] led1 on. <6>[IRQ] counts 5.
- [Kenerl] led2 on. <6>[IRQ] counts 6.
- [Kernel] led3 on. <6>[IRQ] counts 7.
- [Kernel] led4 on. <6>[IRQ] counts 8.
- [Kernel] led1 on. <6>[IRQ] counts 9.
- [Kenerl] led2 on. <6>[IRQ] counts 10.
- //再按回车,等待用户输入,这时pwm并没有暂停,因此仍然会继续输出
- [User] Ready for ioctl.
- [Kernel] led1 on. <6>[IRQ] counts 61.
- [Kenerl] led2 on. <6>[IRQ] counts 62.
- [Kernel] led3 on. <6>[IRQ] counts 63.
- [Kernel] led4 on. <6>[IRQ] counts 64.
- [Kernel] led1 on. <6>[IRQ] counts 65.
- [Kenerl] led2 on. <6>[IRQ] counts 66.
- [Kernel] led3 on. <6>[IRQ] counts 67.
- //再按一次,pwm暂停了,led停止,irq计数也停止了
- [Kernel] do operations with pwm_led_ioctl.
- [Kernel] pwm stop
- [User] ioctl1 cmd:1 arg:1.
- //多按两次,又开始运行了
- [User] Ready for ioctl.
-
- [Kernel] do operations with pwm_led_ioctl.
- [Kernel] pwm start now.
- [User] ioctl2 cmd:0 arg:2.
- [Kernel] led4 on. <6>[IRQ] counts 68.
- [Kernel] led1 on. <6>[IRQ] counts 69.
- [Kenerl] led2 on. <6>[IRQ] counts 70.
- [Kernel] led3 on. <6>[IRQ] counts 71.
- [Kernel] led4 on. <6>[IRQ] counts 72.
- [Kernel] led1 on. <6>[IRQ] counts 73.
- //发送终止进程信号,调用close接口
- ^C[Kernel] pwm stop
- [Kernel] free irq done.
- //进程退出了,可以看到流水灯也随之停止,说明测试成功
[6] 总结陈词
《基于TQ2440的led字符设备驱动》 描述了字符设备驱动编写的整体框架,其中包括一些常用接口的实现,如:open / close / read / write / ioctl ,本文在其基础上增加了定时器0中断,相比内容的丰富度,远不如上一篇,但是在笔者写代码的过程中,也掌握了很多技巧,比如ioremap的使用,goto用来调试也是不错的。所以,套用一句许多高大上的程序猿经常挂在嘴边的一句话(类似的话):代码究竟是人写出来的,难的并非代码本身,而是你能否打开自己的思想!
上一篇:[个人]Ubuntu编程环境配置
下一篇:Qt 错误: 无法运行 release 下的可执行文件
- 欧冠尤文解签 遇两弱旅晋级无...
- 二叉树的遍历 递归非递归 思路...
- linux 内核软中断详解
- expect用法
- 高效清除系统缓存有妙招...
- shell中字符串操作
- shell中的特殊字符
- stagefright与opencore对比
- linux守护进程的几个关键地方...
- UBOOT移植详细 很全面
最后
以上就是还单身夏天为你收集整理的中断驱动学习与实例——定时器0中断实现led流水灯 中断驱动学习(1) 中断驱动学习(2) 的全部内容,希望文章能够帮你解决中断驱动学习与实例——定时器0中断实现led流水灯 中断驱动学习(1) 中断驱动学习(2) 所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复