第四章第9节

  本节描述了一个简单多任务内核的设计和实现方法,这个内核包括两个特权级3的用户任务和一个系统调用中断过程。

本节给出的内核实例由两个文件构成。一个是使用as86语言编制的引导启动程序boot.s,用于在计算机加电时从启动盘上把内核代码加载到内存中;另一个是使用GUN as汇编语言编制的内核程序head.s,其中实现了2个运行在特权级3上的任务在时钟中断控制下相互切换运行,并且还实现了在屏幕上显示字符的一个系统调用。我们把这两个任务分别称为任务A和任务B,它们会分别调用这个系统调用在屏幕上输出字符'A'和'B',直到每隔10毫秒切换至另一个任务,任务A连续循环的调用系统调用在屏幕上输出'A',而任务B一直显示'B'。如要终止这个内核实例程序,则要重新启动机器,或者关闭运行的模拟PC运行环境软件。

  boot.s程序编绎出的代码共512字节,将被存放在软盘映像文件的第一个扇区中,PC在加电启动时,ROM BIOS中的程序会把启动盘第一个扇区加载到物理内存0X7C00(31kb)位置开始出,并把执行权限转移到0X7C00处开始执行boot程序代码。head.s程序运行在32位保护模式下,其功能主要包括:初始化设置代码、时钟中断0X08的过程代码、系统调用中断0X08的过程代码以及任务A和任务B等的代码和数据。初始化设置工作主要包括:1.重新设置GDT表  2.设置系统定时器芯片  3.重新设置IDT表并且设置时钟和系统调用中断门  4.移动到任务A中执行。

  由于特权级0的代码不能直接把控制权转移到特权级3的代码中去,但是中断操作是可以的,因此当初始化GDT,IDT和定时芯片结束后,我们就利用中断返回指令IRET来启动运行第一个任务。具体实现方法是在初始堆栈init_stack中人工设置一个返回环境,即把任务0的TSS段选择符加载到任务寄存器LTR中、LDT段选择符加载到LDTR中以后,把任务0的用户栈指针(0x17:init_stack)和代码指针(0x0f:task0)以及标志寄存器的值压入栈中,然后执行中断返回指令IRET。该指令会弹出堆栈上的堆栈指针作为任务0的用户栈指针恢复假设的任务0的标志寄存器的内容,并且弹出堆栈中的代码指针放入CS:EIP寄存器中,从而开始执行任务0的代码,完成了从特权级0到特权级3代码的控制转移。

  为了每隔10毫秒切换运行的任务,head.s程序中把定时器芯片8253的通道0设置成每隔10毫秒就向中断控制器8259A发送一个时钟中断请求信号,PC机的ROM BIOS开机时已经在8259A中把时钟中断请求信号设置成中断向量8,因此需要在中断8的处理过程中执行任务切换操作,任务切换的方法是查看current变量中当前运行的任务号,如果current是0,就利用任务1的TSS选择符作为操作数执行远跳转指令,从而切换到任务1中执行,否则反之。

  每个任务在执行时,会首先把一个字符的ACII码放入寄存器AL中,然后调用系统中断调用int 0x80,该系统调用处理过程则会调用一个简单的字符写屏子程序,把AL中的字符显示在屏幕上,同时把字符显示的屏幕的下一个位置记录下来,用于下一次显示字符。在显示过一个字符后,任务代码会使用循环语句延迟一段时间,然后又跳转到任务代码开始处继续循环执行,知道运行了10毫秒而发生了定时中断,切换到另一个任务中去执行。对于任务A,寄存器AL中始终存放字符'A',而任务B运行时AL中始终存放字符'B',因此在程序运行时我们会看到一连串的字符'A'和一连串的字符'B'不断的显示在屏幕上。

  下面给出boot.s和head.s程序的详细注释。有关这个简单内核实例的编译和运行方法参考最后一章“编译运行简单内核实例程序”一节的内容。

4.9.2 引导启动程序boot.s

  为了让程序尽量简单,这个引导扇区启动程序仅能够加载长度不超过16个扇区的head代码,并且直接使用了ROM BIOS默认设置的中断向量号,即定时中断请求处理的中断号仍然是8,这与linux系统中使用的不同。linux系统会在内核初始化时重新设置8259A中断控制芯片,并把时钟中断请求信号对应到中断0x20上,详细说明见“内核引导启动程序”一章内容。

! boot.s程序
! 首先利用BIOS中断把内核代码(head.s)加载到内存0x10000处,然后移动到内存0处
! 最后进入保护模式,并跳转到内存0(head.s)开始出继续运行。
BOOTSEG = 0X07C0            !引导扇区(本程序)被BIOS加载到内存0X7C00处
SYSSEG = 0X1000                !内核(head)先加载到0X10000处,然后移动到0X0处
SYSLEN = 17                    !内核占用的最大磁盘扇区数
entry start
start:jmpi    go,#BOOTSEG        !段间跳转至0x7c0:go处。当本程序刚运行时所有段寄存器的值均为0.该!跳转语句会把CS寄存器加载为0x7c0
go: mov        ax,cs             !让DS和SS都指向0X7C0段mov        ds,axmov        ss,axmov        sp,#0x400        !设置临时栈指针,其值需大于程序末端并有一定的空间即可!加载内核代码到内存0x10000开始处
load_system:mov     dx,#0x0000           !利用BIOS中断int 0x13功能2从启动盘读取head代码。mov        cx,#0x0002           ! DH - 磁头号;DL - 驱动器号; CH - 10位磁道号低8位;mov        ax,#SYSSEG           !CL - 位7,6是磁道号高2位,位5-0是起始扇区号(从1记).mov     es,ax               !ES:BX - 读入缓冲区位置(0x1000:0x0000)。xor        bx,bx             mov     ax,#0x200+SYSLEN   !AH - 读扇区功能号;AL - 需读的扇区数(17)int     0x13 jnc        ok_load                !若没有发生错误则跳转继续运行,否则死循环
die:jmp     die
!把内核代码移动到内存0开始出,共移动8KB字节(内核长度不超过8KB)
ok_load:cli                            ! 关中断mov     ax, #SYSSEG            !移动开始位置 DS:SI = 0X1000:0 目的位置ES:DI=0:0.mov        ds, axxor        ax, axmov        es, axmov     cx, #0X1000sub        si, sisub     di, direp        movw                ! 执行重复移动指令
! 加载 IDT 和 GDT基地址寄存器 IDTR 和 GDTRmov     ax, #BOOTSEGmov     ds, ax                 ! 让DS重新指向 0x7c0段lidt    idt_48                ! 加载IDTR.6字节操作数,2字节表长度,4字节线性基地址lgdt    gdt_48                ! 加载GDTR.6字节操作数,2字节表长度,4字节线性基地址。! 设置控制寄存器CR0(即及其状态字),进入保护模式。段选择符8对应GDT表中第2个段描述符mov     ax, #0x0001            ! 在CR0中设置保护模式标志PE(位0)lmsw    ax                    jmpi    0,8                    ! 然后跳转至段选择符指定的段中,偏移0处。! 注意此时段值已是段选择符,该段的线性基地址是0! 下面是全局描述符表GDT的内容,其中包含3个段描述符。第一个不用,第二个是代码和数据段描述
! 符
gdt:.word    0,0,0,0             ! 段描述符0,不用,每个描述符占8个字节.word    0x07FF                ! 段描述符1. 8MB  段限长=2047(2048*4096=8MB).word     0X0000                 ! 段基地址=0x00000.word     0X9A00                ! 是代码段,可读/执行.word     0X00C0                ! 段属性颗粒度=4KB, 80386.word    0x07FF                 !段描述符2.8MB  段限长值=2047 (2048*4096=8MB).word     0x0000                 ! 段基地址=0x00000.word     0x9200                 ! 是数据段,可读写.word     0x00c0                 ! 段属性科类度=4KB,80386! 下面分别是LIDT和LGDT指令的6字节操作数
idt_48:.word    0                     ! IDT表长度是0.word     0,0                 ! IDT表的线性基地址也是0
gdt_48:.word     0x7ff                 ! GDT 表长度是2048字节,可容纳256个描述符项.word     0x7c00+gdt, 0       ! GDT 表的线性基地址在0x7c0段的偏移gdt处
.org 510                !.org命令的作用等同于给'.'赋值,即是使当前程序定位在510字节处.word     0XAA55                 ! 引导扇区有效标志,必须处于引导扇区最后2字节处

 4.9.3 多任务内核程序 head.s

   在进入保护模式后,head.s重新建立和设置IDT、GDT表的主要原因是为了让程序在结构上比较清晰,也为了与后面linux 0.11内核源代码中这两个表的设置方式保持一致。

#head.s 包含32位保护模式初始化设置代码、时钟中断代码、系统调用中断代码和两个任务的代码
#在初始化完成之后程序移动到任务0开始执行,并在时钟中断控制下进行任务0和任务1之间的切换操作
LATCH  = 11930                #定时器出事计数值,即每隔10毫秒发送一次中断请求
SCRN_SEL = 0X18                #屏幕显示内存段选择符
TSS0_SEL = 0X20                #任务0的TSS段选择符
LDT0_SEL = 0X28                #任务0的LDT段选择符
TSS1_SEL = 0X30                #任务1的TSS段选择符
LDT1_SEL = 0X38                #任务1的LDT段选择符
.text
startup_32:
#首先加载数据段寄存器DS、堆栈寄存器SS和堆栈指针ESP。所有段的线性基地址都是0movl $0x10, %eax        #0x10是GDT中数据段选择符mov  %ax, %dslss  init_stack, %esp    #lss命令同时给SS和ESP赋值,高16位赋给SS,低16位赋给ESP
#在新的位置重新设置IDT和GDT表call setup_idt            #设置IDT,先把256个中断门都填默认处理过程的描述符call setup_gdt            #设置GDTmovl $0x10, %eax        #在改变了GDT之后重新加载所有段寄存器mov  %ax,%dsmov  %ax,%esmov  %ax,%fsmov  %ax,%gslss  init_stack,%esp
#设置8253定时芯片,把计数器通道0设置成每隔10户毫秒向中断控制器发送一个中断请信号movb $0x36, %al         #控制字:设置通道0工作在方式3,计数初值采用二进制movl $0x43, %edx        #8253芯片控制字寄存器写端口outb %al, %dxmovl $LATCH, %eax        #初始计数值设置为LATCH(1193180/100),即频率100HZmovl $0x40, %edx        #通道0的端口outb %al, %dx            #分两次把初始计数值写入通道0movb %ah, %aloutb %al, %dx
#在IDT表第8和第128项处分别设置定时中断门描述符和系统调用陷阱门描述符movl  $0x00080000, %eax        #中断程序属内核,即EAX高字是内核代码选择符0x0008movw  $timer_interrupt, %ax    #设置定时中断们描述符,取定时中断处理程序地址movw  $0x8e00, %dx            #中断门类型是14(屏蔽中断),特权级0或硬件使用movl  $0x08, %ecx            #开机时BIOS设置的时钟中断向量号8,这里直接使用它lea  idt(,%ecx,8), %esi        #把IDT描述符0x08地址放入ESI中,然后设置该描述符movl %eax, (%esi)             movl %edx, 4(%esi)movw $system_interrupt, %ax #设置系统调用先进门描述符,取系统调用处理程序地址movw $0xef00, %dx            #陷进门类型是15,特权级3的程序可执行movl $0x80, %ecx            #系统调用向量号的0x80lea  idt(,%ecx,8), %esi     #把IDT描述符项0x80地址放入ESI中,然后设置该描述符movl %eax,(%esi)movl $edx, 4(%esi)# 现在我们为移动到任务0(任务A)中执行来操作堆栈内容,在堆栈中人工建立中断返回时的场景pushfl                        #复位标志寄存器EFLAGS中的嵌套任务标志andl  $0xffffbfff, (%esp)popf1movl $TSS0_SEL, %eax         #把任务0的TSS段选择符加载到任务寄存器TRltr  %axmovl $LDT0_SEL, %eax         #把任务0的LDT段选择符加载到局部描述符表寄存器LDTRlldt %ax                    #TR和LDTR只需人工加载一次,以后CPU会自动处理movl $0, current            #把当前任务号0保存在current变量中sti                         #现在开启中断,并在栈中营造中断返回时的场景pushl $0x17                    #把任务0当前局部空间数据段(堆栈段)选择符入栈pushl $init_stack            #把堆栈指针入栈(也可以直接把ESP入栈)pushfl                        #把标志寄存器入栈pushl $0x0f                 #把当前局部空间代码选择符入栈pushl $task0                #把代码指针入栈iret                         #执行中断返回指令,从而切换到特权级3的任务0中执行#以下是设置GDT和IDT中描述符项的子程序
setup_gdt:                        #使用6字节操作数lgdt_opcode设置GDT表位置和长度lgdt lgdt_opcoderet#这段代码暂时设置IDT表中所有256个中断门描述符都为同一个默认值,均使用默认的中断处理过程ignore_int。
#设置的具体方法是:首先在EAX和EDX寄存器中分别设置好默认中断门描述符的0-3字节和4-7字节的内容,然后
#利用该寄存器对循环往IDT表中填充默认中断门描述符的内容
setup_idt:                    #把所有256个中断门描述符设置为使用默认处理过程lea ignore_int , %eax    #设置方法与设置定时中断门描述符的方法一样movl $0x00080000, %eax    #选择符为0x0008movw %dx,%axmovw $0x8e00, %dx        #中断门类型,特权级为0lea idt, %edimov $256, %ecx            #循环设置所有256个门描述符项
rp_idt:movl %eax, (%edi)movl %edx, 4(%edi)addl $8, %edidec %ecxjne rp_idtlidt lidt_opcode        #最后用6字节操作数加载IDTR寄存器ret#显示字符子程序。取当前光标位置并把AL中的字符显示在屏幕上,整屏可显示80x25个字符
write_char:push  %gs                #首先保存要用到的寄存器,EAX由调用者负责保存pushl %ebx                mov  $SCRN_SEL, %ebx    #然后让GS指向显示内存段(0xb8000)mov  %bx, %gsmovl scr_loc, %bx        #再从变量scr_loc中取目前字符显示位置值shl  $1, %ebx            #因为在屏幕上每个字符还有一个属性字节,因此字符movb %al, %gs:(%ebx)     #实际显示位置对应的显示内存偏移地址要乘2shr  $1, %ebx             #把字符放到显示内存后把位置值除2加1,此时位置值对incl  %ebx                #应下一个显示位置,如果该位置大于2000,则复位成0cmpl  $2000, %ebxjb    lfmovl  $0, %ebx
l:movl  %ebx, scr_loc        #最后把这个位置值保存起来(scr_loc)popl  %ebx                #并弹出保存的寄存器内容,返回pop   %gsret#以下是3个中断处理程序:默认中断、定时中断和系统调用中断
#ignore_int是默认的中断处理程序,若系统产生了其它中断,则会在屏幕上显示一个字符“C”
.align 2
ignore_int:push %dspushl %eax     movl $0x10, %eax         #首先让DS指向内核数据段,因为中断程序属于内核mov %ax,  %dsmovl $67, %eax             #在AL中存放字符C的代码,调用显示程序显示在屏幕上call  write_charpopl  %eaxpopl  %dsiret#这是定时中断处理程序。其中主要执行任务切换操作
.align 2
timer_interrupt:push %dspushl %eaxmovl $0x10, %eax         #首先让DS指向内核数据段mov  %ax, %dsmovb $0x20, %al         #然后立刻允许其他硬件中断,即向8259A发送EOI命令outb %al, $0x20movl $1, %eax             #接着判断当前任务,若是任务1则去执行任务0,或反之cmpl %eax, currentje 1fmovl %eax, current         #若当前任务是1,则把0存入current,并跳转到任务0ljmp $TSS0_SEL, $0         #去执行popl %eax pop  %ds iret#系统调用中断int 0x80处理程序。该示例只有一个显示字符功能
.align 2
system_interrupt:push  %dspushl %edxpushl %ecxpushl %ebxpushl %eaxmovl $0x10, %edx        #首先让DS指向内核数据段mov  %dx, %dscall write_char         #然后调用显示字符子程序write_char,显示AL中的字符。popl %eax pop1 %ebxpopl %ecxpopl %edxpop %dsiret##############****************************************###############
current:.long 0                         #当前任务号(0或1)
scr_loc:.long 0                         #屏幕当前显示位置。从左上角到右下角顺序显示.align 2
lidt_opcode:.word 256 * 8 - 1                     #加载IDTR寄存器的6字节操作数:表长度和基地址.long idt
lgdt_opcode:.word (end_gdt-gdt)-1                 #加载GDTR寄存器的6字节操作数:表长度和基地址.long gdt.align 3
idt:.fill 256,8,0                         #IDT空间。共256个门描述符,每个8字节,共占用2KBgdt:.quad 0x0000000000000000             #GDT表,第1个描述符不用.quad 0x00c09a00000007ff             #第2个是内核代码段描述符,其选择符是0x08.quad 0x00c09200000007ff             #第3个是内核数据段描述符,其选择符是0x10.quad 0x00c0920b80000002             #第4个是显示内存段描述符,其选择符是0x18.word 0x68, tss0, 0xe900, 0x0         #第5个是TSS0段的描述符,其选择符是0x20.word 0x40, ldt0, 0xe200, 0x0          #第6个是LDT0段的描述符。其选择符是0x28.word 0x68, tss1, 0xe900, 0x0         #第7个是TSS1段的描述符。其选择符是0x30.word 0x40, ldt1, 0xe200, 0x0         #第8个是LDT1段的描述符。其选择符是0x38
end_gdt:.fill 128,4,0                         #初始内核堆栈空间
init_stack:                                #刚进入保护模式时用于加载SS:ESP堆栈指针值.long init_stack                    #堆栈段偏移位置.word 0x10                             #堆栈段同内核数据段#下面是任务0的LDT表段中的局部段描述符
.align 3
ldt0:.quad 0x0000000000000000             #第1个描述符,不用。.quad 0x00c0fa00000003ff             #第2个局部代码段描述符,对应选择符是0x0f.quad 0x00c0f200000003ff             #第3个局部数据段描述符,对应选择符是0x17#下面是任务0的TSS段的内容。注意其中标号等字段在任务切换时不会改变。
tss0:.long 0                     /*back link*/.long krn_stk0, 0x10         /*esp0,ss0*/.long 0, 0, 0, 0, 0         /*esp1, ss1, esp2, ss2, cr3*/.long 0, 0, 0, 0, 0         /*eip, eflags, eax, ecx, edx*/.long 0, 0, 0, 0, 0         /*ebx, esp, ebp, esi, edi */.long 0, 0, 0, 0, 0, 0         /*es, cs, ss, ds, fs, gs*/.long LDT0_SEL, 0x8000000     /*ldt, trace bitmap*/.fill 128,4,0                 #这是任务0的内核栈空间
krn_stk0:
#下面是任务1的LDT表段内容和TSS段内容
.align 3
ldt1:.quad 0x0000000000000000     #第1个描述符,不用。.quad 0x00c0fa00000003ff     #选择符是0x0f,基地址=0x00000.quad 0x00c0f200000003ff     #选择符是0x17, 基地址=0x00000tss1:.long 0                                 /*back link */.long krn_stk1, 0x10                     /*esp0, sss0*/.long 0,0,0,0,0                         /*esp1, ss1,esp2,ss2,cr3*/.long task1, 0x200                         /*eip, eflags */.long 0,0,0,0                             /* eax, ecx , edx, ebx */.long usr_stk1, 0, 0, 0                 /* esp, ebp, esi, edi */.long 0x17,0x0f,0x17,0x17,0x17,0x17        /* es,cs,ss,ds,fs,gs*/.long LDT1_SEL, 0X8000000                 /* ldt, tarce bitmap */.fill 128,4,0                     #这是任务1的内核空间。其用户栈直接使用初始栈空间
krn_stk1:#下面是任务0和任务1的程序,它们分别循环显示字符'A'和'B'。
task0:movl $0x17, %eax             #首先让DS指向任务的局部数据,所以这两句可省略movw %ax, %ds                 #因为任务没有使用局部数据,所以这两句可省略movl $65, %al                 #把需要显示的字符'A'放入AL寄存器中int $0x80                     #执行系统调用,显示字符movl $0xfff, %ecx            #执行循环,起延时作用
1:loop 1bjmp  task0                     #跳转到任务代码开始处继续显示字符
task1:movl $66, %al                 #把需要显示的字符'B'放入AL寄存器中int  $0x80                     #执行系统调用,显示字符movl $0xfff, %ecx             #延时一段时间,并跳转到开始处继续循环显示
1:loop 1bjmp  task1.fill 128,4,0                 #这是任务1的用户栈空间
usr_stk1:

  保护模式详解------http://baike.baidu.com/link?url=BwqoEM95JB15Q2Xl3-UEuEozXNToviyZ66qtEZFKSMU-XZDX-mNXO8L2mW4JwPqV

转载于:https://www.cnblogs.com/coderCaoyu/p/3601740.html

4.9一个简单的多任务内核实例相关推荐

  1. 一个简单的多任务内核实例

    多任务程序结构和工作原理 本文给出的内核文件由两个文件构成.一个使用as86语言编制而成的引导启动程序boot.s,用于在计算机上电时从启动盘上把内核代码加载到内存中:另一个是使用GNU as 汇编语 ...

  2. 一个简单的EJB-Session Bean实例

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 一个简单 ...

  3. 一个简单的百度爬虫实例

    一个简单的百度爬虫实例 最近在百度aistdio自学课程,看到一个基础课程的作业是爬取百度上<青春有你>选手信息,索性就跟着爬了一下,复习一下自己去年自学的已经忘得差不多的爬虫. 直接上代 ...

  4. 一个简单使用html的实例

    一个简单使用html的实例 起始网页 点击上个网页的连接,跳转到 代码如下 网页1的代码 <html> <head> <title>一个实例</title&g ...

  5. 用javascript进行一个简单的机器学习小实例

    虽然它可能不是机器学习传统选择的开发语言,但是JavaScript正在证明有能力完成这样的工作--即使它目前还不能与主要的机器学习语言Python竞争.在进一步学习之前,让我们做一下机器学习的介绍. ...

  6. swt包下载,swt包引入(一个简单的SWT程序实例及详解)

    让我们从简单的 HelloWorld 应用程序开始. swt包下载可以在eclipse网站上下 进到下面这个地址里 http://www.eclipse.org/downloads/download. ...

  7. 一个简单的TTS文语转换实例

    Text-to-Speech Tutorial 原文:来自SAPI5.1 文档 翻译:seasun / openpaper / google 论坛 日期:2007-10-21 本文介绍一个最基本的文语 ...

  8. src获取同级目录中的图片_一个简单的Python爬虫实例:百度贴吧页面下载图片

    本文主要实现一个简单的爬虫,目的是从一个百度贴吧页面下载图片. 1. 概述 本文主要实现一个简单的爬虫,目的是从一个百度贴吧页面下载图片.下载图片的步骤如下: 获取网页html文本内容: 分析html ...

  9. 一步步学习操作系统(1)——参照ucos,在STM32上实现一个简单的多任务(“啰里啰嗦版”)...

    该篇为"啰里啰嗦版",另有相应的"精简版"供参考 "不到长城非好汉:不做OS,枉为程序员" OS之于程序员,如同梵蒂冈之于天主教徒,那永远都 ...

最新文章

  1. 7个建议帮你完成更多的工作
  2. Scala方法定义,方法和函数的区别,将方法转换成函数
  3. go详解bufio包
  4. HDU ACM 3986 Harry Potter and the Final Battle(邻接表实现最短路dijkstra堆优化记录路径 + 枚举最短路上每条边)...
  5. 骨牌铺方格的3种做法
  6. 微服务技术图片资源汇总
  7. 谷歌pixel3axl开发者模式_谷歌推出新一代安卓系统“Android 10”
  8. 2.6.4 调色板技术
  9. 电脑系统常用的10个快捷键,适合电脑新用户看看
  10. 马云:眼光有多远 未来就有多远【2014世界互联网大会】
  11. 14Python爬虫---爬虫伪装浏览器
  12. Google浏览器插件推荐
  13. 电脑垃圾太多?这几个清理电脑的软件来看看吗?
  14. 银河麒麟高级服务器操作系统V10下载安装及安装docker
  15. poj 1703 poj 2492 并查集 桥梁判AB组
  16. 手机CPU天梯图2021年6月版
  17. php pecl_http,安装 PHP 的 PECL HTTP 扩展
  18. 甘特图制定项目计划,提高项目管理效率
  19. 项目管理如何与甲方沟通
  20. Installation silently blocked for package name fix

热门文章

  1. 移动端网站页面优化的关键点
  2. 网站前中期外链优化少不了这三点!
  3. 数据处理_流数据处理利器
  4. 关闭串口_USART串口通信,DMA方式,一分钟从入门到大师
  5. wifi linux 驱动分析,Linux 下wifi 驱动开发(二)—— WiFi模块浅析
  6. svn 不支持http 客户端_Xversion for mac(SVN客户端)
  7. 如何两个电脑共享文件实现多人编辑_四款花钱都难买到的良心软件,每一款都是电脑必备...
  8. append函数_高质量python代码:考虑用生成器来改写直接返回列表的函数
  9. ajax提交form表单数据_[基础编程学习] [PHP7数组详解]:第2章 (1)从表单提交数据说起...
  10. 重构-改善既有代码的设计 读书笔记