鼠标控制与32位模式切换

在编写代码的时候可以对照书中的代码,以便及时找出自己的错误。

  • 鼠标控制与32位模式切换

    • 鼠标解读1
    • 整理
    • 鼠标解读2
    • 移动鼠标指针
    • 通往32位模式之路

1. 鼠标解读(1)

已经能从鼠标取得数据了,紧接着的问题是要解读这些数据,调查鼠标是怎么移动的,然后结合鼠标的动作,让鼠标指针相应地动起来。

首先对bootpack.c中的HariMain进行一些修改:

for (;;) {io_cli();if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {io_stihlt();} else {if (fifo8_status(&keyfifo) != 0) {i = fifo8_get(&keyfifo);io_sti();sprintf(s, "%02X", i);boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);} else if (fifo8_status(&mousefifo) != 0) {i = fifo8_get(&mousefifo);io_sti();if (mouse_phase == 0)  /*等待鼠标的0xfa的状态*/{if (i == 0xfa){mouse_phase = 1;}} else if (mouse_phase == 1) {/*等待鼠标的第一字节*/mouse_dbuf[0] = i;mouse_phase = 2;} else if (mouse_phase == 2) {/* 等待鼠标的第二字节 */mouse_dbuf[1] = i;mouse_phase = 3;} else if (mouse_phase == 3) {mouse_dbuf[2] = i;mouse_phase = 1;/*鼠标的三个字节都齐了,显示出来*/sprintf(s, "%02X %02X %02X", mouse_dbuf[0], mouse_dbuf[1], mouse_dbuf[2]);boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 8 * 8 - 1, 31);putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);}}}}

实际上是将HariMain中for循环部分进行修改,首先把最初读到的0xfa舍弃掉。之后,每次从鼠标那里送过来的数据都应该是3个字节一组的,所以每当数据累积到3个字节,就把他显示在屏幕上。

变量mouse_phase用来记住接受鼠标数据的工作进展到了什么阶段(phase)。接受到的数据放在mouse_dbuf[0~2]内。

        if (mouse_phase == 0)  /*等待鼠标的0xfa的状态*/{各种处理;             } else if (mouse_phase == 1) {/*等待鼠标的第一字节*/各种处理;} else if (mouse_phase == 2) {/* 等待鼠标的第二字节 */各种处理;} else if (mouse_phase == 3) {/*鼠标的三个字节都齐了,显示出来*/各种处理;}

这部分就是对于不同的mouse_phase值,相应地做各种不同的处理。显示结果如下(鼠标移动过):

屏幕上除了括号内的还有三字节数字,即mouse_dbuf[0],mouse_dbuf[1],mouse_dbuf[2]里的数据。”08”部分0会在0~3的范围内变化,”8”只有在点击鼠标时才会有变化,值在8~F之间。第二个字节与鼠标的左右移动有关,第三个字节与鼠标的上下移动有关。

2. 整理

在HariMain函数中出现的unsigned char mouse_dbuf[3], mouse_phase;声明可以放到函数前的结构体里:

struct MOUSE_DEC {unsigned char buf[3], phase;
};

并在这句话修改为:

struct MOUSE_DEC mdec;

创建的这个结构体MOUSE_DEC,DEC是decode的缩写,用这个结构日把鼠标所需要的变量都归总到一块儿。
然后将鼠标的解读从函数HariMain的接受信息处理中剥离出来,放到了mouse_decode函数。

3. 鼠标解读(2)

struct MOUSE_DEC {unsigned char buf[3], phase;int x, y ,btn;
};int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat)
{if (mdec->phase == 0) {/* 等待鼠标的0xfa的阶段 */if (dat == 0xfa) {mdec->phase = 1;}return 0;}if (mdec->phase == 1) {/* 等待鼠标第一字节的阶段 */if ((dat & 0xc8) == 0x08) {/*如果第一字节正确*/mdec->buf[0] = dat;mdec->phase = 2;}return 0;}if (mdec->phase == 2) {/* 等待鼠标第二字节的阶段 */mdec->buf[1] = dat;mdec->phase = 3;return 0;}if (mdec->phase == 3) {/* 等待鼠标第三字节的阶段 */mdec->buf[2] = dat;mdec->phase = 1;mdec->btn = mdec->buf[0] & 0x07;mdec->x = mdec->buf[1];mdec->y = mdec->buf[2];if ((mdec->buf[0] & 0x10) != 0){mdec->x |= 0xffffff00;}if ((mdec->buf[0] & 0x20) != 0){mdec->y |= 0xffffff00;}mdec->y = - mdec->y;  /*鼠标的y方向与画面符号相反*/return 1;}return -1; /* 应该不可能到这里来 */
}

结构体里增加的几个变量用于存放解读结果,这几个变量是x、y和btn,分别用于存放移动信息和鼠标按键状态。

if (mdec->phase == 1)这个语句用于判断第一字节对移动有反应的部分是否在0~3的范围内;同时还要判断第一字节对点击有反应的部分是否在8~F的范围内,如果不在以上数据范围内就被舍去。这样做是因为鼠标连线可能会由接触不良,这样产生的数据就有错位,不能顺利解读。

if (mdec->phase == 3)语句是解读处理的核心。鼠标键的状态放在buf[0]的低3位,我们只取出这3位。十六进制的0x07相当于二进制的0000 0111,通过与运算(&)取出低3位。

x,y基本上直接使用buf[1]和buf[2],但是需要使用第一字节中对鼠标移动有反应的几位,将x和y的第8位及第8位以后全部都设成1,或全部都保留为0,就能正确解读x和y。

解读最后对y符号进行了取反操作是因为鼠标与屏幕的y方向正好相反,为了配合画面方向,就对y符号进行了取反操作。

鼠标数据解读完成之后接下来修改显示部分:

      else if (fifo8_status(&mousefifo) != 0) {i = fifo8_get(&mousefifo);io_sti();if (mouse_decode(&mdec, i) != 0)  /*3字节都凑齐了,所以把它们显示出来*/{sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y);if ((mdec.btn & 0x01) != 0) {s[1] = 'L';}if ((mdec.btn & 0x02) != 0) {s[3] = 'R';}if ((mdec.btn & 0x04) != 0) {s[2] = 'C';}boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 15 * 8 - 1, 31);putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);}}

如果mdec.btn的最低位是1,就把s的第2个字符(第一个字符是s[0])换成‘L’。执行结果如下:

最左边的字符是因为有按键。

4. 移动鼠标指针

现在就是让鼠标指针在屏幕上动起来啦,感觉好激动,终于能动了。

    else if (fifo8_status(&mousefifo) != 0) {i = fifo8_get(&mousefifo);io_sti();if (mouse_decode(&mdec, i) != 0)  /*3字节都凑齐了,所以把它们显示出来*/{sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y);if ((mdec.btn & 0x01) != 0) {s[1] = 'L';}if ((mdec.btn & 0x02) != 0) {s[3] = 'R';}if ((mdec.btn & 0x04) != 0) {s[2] = 'C';}boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 15 * 8 - 1, 31);putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);/*鼠标指针的移动*/boxfill8(binfo->vram, binfo->scrnx, COL8_008484, mx, my, mx + 15 * 8 - 1, my + 15);   /*隐藏鼠标*/mx += mdec.x;my += mdec.y;if (mx < x){mx = 0;}if (my < 0){my = 0;}if (mx > binfo->scrnx - 16){mx = binfo->scrnx - 16;}if (my > binfo->scrny - 16){my = binfo->scrny - 16;}sprintf(s, "(%sd, %3d)", mx, my);boxfill8(binfo->vram, binfo->scrnx, COL8_008484,  0, 0, 79, 15);  /*隐藏坐标*/putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s);  /*显示坐标*/putfonts8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);  /*描画鼠标*/}

先隐藏到鼠标指针,然后根据取得的鼠标数据解读得到的位移量,让鼠标显示在屏幕上。

mx += mdec.x;
my += mdec.y;

是为了防止鼠标指针跑到屏幕外进行的调整。运行看看,鼠标能动啦!

5. 通往32位模式之路

这里讲解了asmhead.nas中的程序。

; PIC关闭一切中断
;   根据AT兼容机的规格,如果要初始化PIC,
;   必须在CLI之前进行,否则有时会挂起,
;   随后进行PIC的初始化MOV     AL,0xffOUT     0x21,ALNOP                     ; 如果连续执行OUT指令,有些机种会无法正常运行OUT     0xa1,ALCLI                     ; 禁止CPU级别的中断

这段程序等同于一下内容的C程序。

io_out8(PIC0_IMR,  0xff  ); /* 禁止主PIC的全部中断 */
io_out8(PIC1_IMR,  0xff  ); /* 禁止从PIC的全部中断 */
Io_cli(); /*禁止CPU级别的中断*/

为了让CPU能够访问1MB以上的内存空间,设定A20GATE

        CALL    waitkbdoutMOV     AL,0xd1OUT     0x64,ALCALL    waitkbdoutMOV     AL,0xdf         ; enable A20OUT     0x60,ALCALL    waitkbdout

这里的waitbdout等同于wait_KBC_sendread,等同于C语言中的:

#define KEYCMD_WRITE_OUTPORT        0xd1
#define KBC_OUTPORT_A20G_ENABLE  0xdf/* A20GATE的设定*/Wait_KBC_sendready();Io_out8(PORT_KEYCMD, KEYCMD_WRITE_OUTPORT);Waite_KBC_sendready();Io_out8(PORT_KEYDATA, KBC_OUTPORT_A20G_ENABLE);Waite_KBC_sendready();     /*这句话是为了等待完成执行指令*/

程序的基本结构与init_keyboard完全相同,功能仅仅是往键盘控制电路发送指令。这里发送的指令,是指令键盘控制电路的附属端口输出0xdf。这个附属端口,连接着主板上的很多地方,通过这个端口发送不同的指令,就可以实现各种各样的控制功能。

这次输出0xdf所要完成的功能,是让A20GATE信号线变成ON的状态。这条信号线的作用是使内存的1MB以上的部分变成可使用状态。Waite_KBC_sendready();是多余的,在此之后,虽然不会往键盘送命令,但仍然要等到下一个命令能够送来为止。这是为了等待A20GATE的处理切实完成。

; 切换到保护模式[INSTRSET "i486p"]              ; “想要使用486指令”的叙述LGDT    [GDTR0]         ; 设定临时GDTMOV     EAX,CR0AND     EAX,0x7fffffff  ; 设bit31为0(为了禁止颁)OR      EAX,0x00000001  ; 设bit0为1(为了切换到保护模式)MOV     CR0,EAXJMP     pipelineflush
pipelineflush:MOV     AX,1*8          ;  可读写的段 32bitMOV     DS,AXMOV     ES,AXMOV     FS,AXMOV     GS,AXMOV     SS,AX

INSTRSET指令,是为了能够使用386以后的LGDT,EAX, CR0等关键字。LGDT指令将暂定的GDT读进来。然后将CR0这一特殊的32寄存器的值带入EAX,并将最高位置为0,最低位置为1,再将这个值返回给CR0寄存器。这样就完成了模式转换,进入到不用颁的保护模式。CR0,也就是control register 0,是一个非常重要的寄存器,只有操作系统才能操作它。

保护模式是指操作系统受到CPU的保护,应用程序既不能随便改变段的设定,又不能使用操作系统专用的段。

通过带入CR0而切换到保护模式时,要马上执行JMP指令。因为变成保护模式后,机器语言的解释要发生变化。CPU为了加快指令的执行速度而使用了管道(piprline)这一机制。也就是说,前一条指令还在执行的时候就开始解释下一条甚至再下一条指令。因为模式变了,就要重新解释一遍,所以加入了JMP指令。

并且在进入保湿模式以后,段寄存器的意思也变了(不再是乘以16后再加算的意思了),除了CS以外所有段寄存器的值都从0x0000变成了0x0008.CS保持原状是因为如果CS也变了,会造成混乱,所以只有CS要放到后面再处理。0x0008,相当于“gdt+1”的段。

; bootpack的传送MOV     ESI,bootpack    ; 传送源MOV     EDI,BOTPAK      ; 传送目的地MOV     ECX,512*1024/4CALL    memcpy; 磁盘数据最终转送到它本来的位置去; 首先从启动扇区开始MOV     ESI,0x7c00      ; 传送源MOV     EDI,DSKCAC      ; 传送目的地MOV     ECX,512/4CALL    memcpy; 所有剩下的MOV     ESI,DSKCAC0+512 ; 传送源MOV     EDI,DSKCAC+512  ; 传送目的地MOV     ECX,0MOV     CL,BYTE [CYLS]IMUL    ECX,512*18*2/4  ; 从柱面数变换为字节数/4SUB     ECX,512/4       ; 减去IPLCALL    memcpy

这部分程序只是在调用memcpy函数,同样可以理解成C语言形式(写法可能不正确,但中心思想是类似的):

memcpy(bootpack,          BOTPAK,          512*1024/4);
memcpy(0x7c00,              DSKCAC,          512/4           );
memcpy(DSKCAC0+512, DSKCAC+512, cyls*512*18*2/4 - 512/4);

函数mencpy是赋值内存的函数,语法如下:
memcpy(转送源地址, 转送目的地址, 转送数据的大小);
转送数据大小是以双字节位单位的,所以数据大小用字节数除以4来指定。
memcpy(0x7c00, DSKCAC, 512/4 );
DSKCAC是0x00100000,所以上面这句话就是从0x7c00复制512字节到0x00100000。这正好是将启动扇区复制到1MB以后的内存去。
memcpy(DSKCAC0+512, DSKCAC+512, cyls*512*18*2/4 - 512/4);
它的意思是将始于0x00008200的磁盘内容,复制到0x00100200那里。

上文中“转送数据大小”的计算有点复杂,因为它是以柱面数来计算的,所以需要减去启动区的那一部分长苏。IMUL是乘法运算,SUB是减法运算。

bootpack是asmhead,nas的最后一个标签,haribote.hrb连接起来而生成的,所以asmhead结束的地方,紧接着串联着bootpack.hrb最前面的部分。

memcpy(bootpack, BOTPAK, 512*1024/4);
→从bootpack的地址开始的512KB内容复制到0x00280000号地址去

; 必须有asmhead来完成的工作,至此全部完毕
;   以后就变由bootpack来完成; bootpack的启动MOV     EBX,BOTPAKMOV     ECX,[EBX+16]ADD     ECX,3           ; ECX += 3;SHR     ECX,2           ; ECX /= 4;JZ      skip            ; 没有要转送的东西时MOV     ESI,[EBX+20]    ; 转送源ADD     ESI,EBXMOV     EDI,[EBX+12]    ; 转送目的地CALL    memcpy
skip:MOV     ESP,[EBX+12]    ; 栈初始值JMP     DWORD 2*8:0x0000001b

这里仍然是在做memcpy,它对bootpack.hrb的header进行解析,将执行必需的数据传送过去。EBX里带入的是BOTPAK,所以值如下:

[EBX + 16]……bootpack.hrb之后的第16号地址。值是0x11a8
[EBX + 20]……bootpack.hrb之后的第20号地址。值是0x10c8
[EBX + 12]……bootpack.hrb之后的第12号地址。值是0x00310000

上面这些值是通过二进制编辑器,打开harib05d的bootpack.hrb后确认的。这些值因harib的版本不同而有所变化。

SHA指令是向右移位指令,相当于“ECX>>=2;”,JZ时条件转移指令,来自英文jump if zero,根据前一个计算结果是否为0来决定是否跳转。

最终这个memcpy的作用是将bootpack.hrb第0x10c8字节开始的0x11a8字节复制到0x00310000号地址去。最后将0x310000代入到ESP里,然后用一个特别的JMP指令,将2*8代入到CS里,同时移动到0x1b号。这里的0x1b号地址是指第2个段的0x1b号地址。第2个段的基地址是0x280000,所以实际上是从0x28001b开始执行的。也就是bootpack.hrb的0x1b号地址。

然后是这个我们制作的这个“纸娃娃系统”的内存分布图:

0x00000000 - 0x000fffff : 虽然在启动中会多次使用,但之后就变空。(1MB)
0x00100000 - 0x00267fff : 用于保存软盘的内容。(1440KB)
0x00268000 - 0x0026f7ff : 空(30KB)
0x0026f800 - 0x0026ffff : IDT(64KB)
0x00270000 - 0x0027ffff : GDT(64KB)
0x00280000 - 0x002fffff : bootpack.hrb(512KB)
0x00300000 - 0x003ffff : 栈及其他(1MB)
0x00400000 -                   : 空
waitkbdout:IN       AL,0x64AND      AL,0x02IN       AL,0x60        ; 空读(为了清空数据接收缓冲区中的垃圾数据)JNZ     waitkbdout      ; AND的结果如果不是0,就跳到waitkbdoutRET

waitbdout与wait_KBC_sendready相同,但也添加了部分处理,就是从0x60号设备进行IN的处理。如果控制器里有键盘代码,或者已经积累了鼠标数据,就顺便把它们读取出来。JNC与JZ相反,意思是“jump if not zero”

memcpy:MOV     EAX,[ESI]ADD     ESI,4MOV     [EDI],EAXADD     EDI,4SUB     ECX,1JNZ     memcpy          ; 减法运算的结果如果不是0,就跳转到memcpyRET

复制内存的程序。

ALIGNB  16
GDT0:
RESB    8               ; NULL selector
DW      0xffff,0x0000,0x9200,0x00cf ; 可以读写的段(segment)32bit
DW      0xffff,0x0000,0x9a28,0x0047 ; 可以执行的段(segment)32bit(bootpack用)DW      0
GDTR0:
DW      8*3-1
DD      GDT0ALIGNB  16
bootpack:

ALIGNB指令的意思是一直添加DBO,直到时机合适的时候为止,即最初的地址能被16整除。如果标签GDT0的地址不是8的整数倍,想段寄存器复制的MOV指令就会慢一些。所以插入了ALIGNB指令。

GDT0也是一种特定的GDT,0号是空区域(null sector),不能够在那里定义段。1号和2号分别由下式设定:

Set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, AR_DATA32_RW);
Set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);

GDTR0时LGDT指令,意思是通知GDT0说“有了GDT哟”。在GDT0里,写入了16位的段上限,和32位的段起始地址。

最初状态时,GDT在asmhead.nas里,并不在0x00270000~0x0027ffff的范围里。IDT连设定都没设定,所以扔处于中断禁止的状态。应当趁着硬件上积累过多数据而产生误动作之前,尽快开放终端,接收数据。因此,在bootpack.c的HariMain里,应该在进行调色板(palette)的初始化以及画面的准备之前,先赶紧重新创建GDT和IDT,初始化PIC,并执行”io_sti();”。

《30天自制操作系统》 day8 小结相关推荐

  1. 30天自制操作系统Day8

    一.鼠标解读 鼠标收到激活指令后,发送的第一个按键编码是0xfa,之后,每次从鼠标发送过来的数据都是3个字节一组的.   移动鼠标时,第一个字节的高四位会在0-3的范围内变化,也就是说第七位和第八位始 ...

  2. 为什么《30天自制操作系统》封面中的猫是两只尾巴

    刚刚在一社区,发了一贴,被指出一问题,询一高人,得一答案.这便是我没有关注到的封面上的那只猫,我想这也是很多读者没有关注到的.因为在我微博的200转发贴中,并没有人提到封面中的猫为何有两只尾巴.于是咨 ...

  3. 发布在《30天自制操作系统》之前的帮助阅读贴

    说明:这是8月15日即将上市的一本新书,本文的摘选也可以命名为<30天自制操作系统>上市之前必读.本书幽默,有趣,可以说是技术书里的幽默书,让您读起来绝对不会感到乏味.在本书上市之前,您一 ...

  4. 《30天自制操作系统》笔记(01)——hello bitzhuwei’s OS!

    <30天自制操作系统>笔记(01)--hello bitzhuwei's OS! 最初的OS代码 1 ; hello-os 2 ; TAB=4 3 4 ORG 0x7c00 ; 指明程序的 ...

  5. 写在《30天自制操作系统》上市之前

       这本<30天自制操作系统>马上就要在各大书店和网上商城全面上架了,作为本书的4位译者之一,我负责翻译了本书约三分之二的内容.这是我参与翻译的第一本译著,我感到很激动也很紧张,因为我知 ...

  6. 《30天自制操作系统》笔记(04)——显示器256色

    <30天自制操作系统>笔记(04)--显示器256色 进度回顾 从最开始的(01)篇到上一篇为止,已经解决了开发环境问题和OS项目的顶层设计问题. 本篇做一个小练习:设置显卡显示256色. ...

  7. 《30天自制操作系统》学习笔记--第好多天

    之前看<30天自制操作系统>,参考而成,和书中系统并不完全一致,是在原有基础上按照自己的习惯而成,由于水平和工作原因,未完成内存管理和文件系统,有兴趣者可以通过以下网址https://gi ...

  8. 由《30天自制操作系统》引发的漫画创作

    大家可还记得<30天自制操作系统>的封面上的那只猫吗?记得当时,在果壳网有人问,为何这只猫长了两只尾巴呢,延着这条线,我把这本书捧上了展示的舞台.事隔四个多月,我又重提此书. 这本经我手宣 ...

  9. 30天自制操作系统——第二十三天窗口操作

    窗口及输入切换 我们先来实现用键盘切换窗口,按下F11键,将最下面的窗口移动到最上面,这里F11按键的编码为0x57. bootpack.c节选: void HariMain(void) {(略)fo ...

  10. 30天自制操作系统-初体验

    最近在图书馆翻阅关于操作系统的书籍,看到川和秀实的自制操作系统决定也动手尝试一下,这本书书名就叫做30天自制操作系统.首先还是附上光盘镜像的获取地址吧.30天自制操作系统光盘镜像ISO完整版下载 - ...

最新文章

  1. java对mysql排序_MySQL 排序
  2. Python- 反射 及部份内置属性方法
  3. Java ASM与Javassit
  4. uboot支持S3C6410的SD启动
  5. hadoop jar包_计算机毕业设计中hadoop上运行Java程序
  6. 质数因子 java_质数因子
  7. [插头DP自我总结]
  8. 编历修改工作表中的控件属性(更新条形码)
  9. UCan下午茶武汉站,为你全面挖宝分布式存储
  10. GitHub的安装与配置
  11. iOS 应用下载链接获取
  12. 【贪玩巴斯】数字图像处理基础课堂笔记(二)——Matlab基础「下」与 Matlab编程基础 2021-09-26
  13. ping 不通 华为三层交换机vlan_华为三层交换机VLAN间路由怎么设置
  14. 理解 Rack 应用及其中间件
  15. css动画走马灯5秒,用animation制作走马灯
  16. 加速信创落地,最新国产身份目录服务首发
  17. 水平拉滑轮组计算机械效率的题,中考典型题:滑轮组机械效率分类计算(1).doc...
  18. python 3.6 盲水印脚本安装说明
  19. 物联网技术部培训---贪心算法
  20. 如何查询相关基因及其相关的全部信息

热门文章

  1. 惠普z800工作站bios设置_HP工作站BIOS说明书适用Z228Z440Z230Z640Z840Z800Z620Z420Z820主板设置.doc...
  2. TeeChart控件
  3. vb adodb mysql_PHP_ADODB类使用,MySQL的例子PHP中最通用的数据 - phpStudy
  4. 网络+系统+ITSM工具Network+system+ITSM tool
  5. 计算机360u盘删除,如何删除U盘在电脑中的使用痕迹
  6. chromium 安装flash player
  7. “超详细” 山特UPS C3KR 的驱动安装教程
  8. 点击按钮复制指定代码
  9. 手把手从零开始学习树莓派教程
  10. 锋利的jQuery总结(三)