Android arm linux kernel启动流程二

发布者:知识智慧最新更新时间:2016-07-01 来源: eefocus关键字:Android  arm  linux  kernel  启动流程 手机看文章 扫描二维码
随时随地手机看文章
写这个总结的时候咱的心情是沉重的,因为还有好多东西没弄明白。。。感叹自己的知识还是浅薄得很,前途钱途漫漫阿~~不过基本脉络是清楚的,具体的细节只能留在以后有时间再啃了。这里的第二部分启动流程指的是解压后kernel开始执行的一部分代码,这部分代码和ARM体系结构是紧密联系在一起的,所以最好是将ARM ARCHITECTURE REFERENCE MANUL仔细读读,尤其里面关于控制寄存器啊,MMU方面的内容~

      前面说过解压以后,代码会跳到解压完成以后的vmlinux开始执行,具体从什么地方开始执行我们可以看看生成的vmlinux.lds(arch/arm/kernel/)这个文件:

      view plaincopy to clipboardprint?
OUTPUT_ARCH(arm)   
ENTRY(stext)   
jiffies = jiffies_64;   
SECTIONS   
{   
 . = 0x80000000 + 0x00008000;   
 .text.head : {    
  _stext = .;   
  _sinittext = .;   
  *(.text.h  
OUTPUT_ARCH(arm)
ENTRY(stext)
jiffies = jiffies_64;
SECTIONS
{
 . = 0x80000000 + 0x00008000;
 .text.head : { 
  _stext = .;
  _sinittext = .;
  *(.text.h

      很明显我们的vmlinx最开头的section是.text.head,这里我们不能看ENTRY的内容,以为这时候我们没有操作系统,根本不知道如何来解析这里的入口地址,我们只能来分析他的section(不过一般来说这里的ENTRY和我们从seciton分析的结果是一样的),这里的.text.head section我们很容易就能在arch/arm/kernel/head.S里面找到,而且它里面的第一个符号就是我们的stext:

      view plaincopy to clipboardprint?
.section ".text.head", "ax"  
Y(stext)   
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode   
                    @ and irqs disabled   
mrc p15, 0, r9, c0, c0      @ get processor id   
bl  __lookup_processor_type     @ r5=procinfo r9=cpuid  
    .section ".text.head", "ax"
ENTRY(stext)
    msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
                        @ and irqs disabled
    mrc p15, 0, r9, c0, c0      @ get processor id
    bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
 

      这里的ENTRY这个宏实际我们可以在include/linux/linkage.h里面找到,可以看到他实际上就是声明一个GLOBAL Symbol,后面的ENDPROC和END唯一的区别是前面的声明了一个函数,可以在c里面被调用。

      view plaincopy to clipboardprint?
#ifndef ENTRY  
#define ENTRY(name) /   
  .globl name; /   
  ALIGN; /   
  name:  
#endif  
#ifndef WEAK  
#define WEAK(name)     /   
    .weak name;    /   
    name:  
#endif  
#ifndef END  
#define END(name) /   
  .size name, .-name  
#endif   
/* If symbol 'name' is treated as a subroutine (gets called, and returns)  
 * then please use ENDPROC to mark 'name' as STT_FUNC for the benefit of  
 * static analysis tools such as stack depth analyzer.  
 */ 
#ifndef ENDPROC  
#define ENDPROC(name) /   
  .type name, @function; /   
  END(name)  
#endif  
#ifndef ENTRY
#define ENTRY(name) /
  .globl name; /
  ALIGN; /
  name:
#endif
#ifndef WEAK
#define WEAK(name)     /
    .weak name;    /
    name:
#endif
#ifndef END
#define END(name) /
  .size name, .-name
#endif
/* If symbol 'name' is treated as a subroutine (gets called, and returns)
 * then please use ENDPROC to mark 'name' as STT_FUNC for the benefit of
 * static analysis tools such as stack depth analyzer.
 */
#ifndef ENDPROC
#define ENDPROC(name) /
  .type name, @function; /
  END(name)
#endif

      找到了vmlinux的起始代码我们就来进行分析了,先总体概括一下这部分代码所完成的功能,head.S会首先检查proc和arch以及atag的有效性,然后会建立初始化页表,并进行CPU必要的处理以后打开MMU,并跳转到start_kernel这个symbol开始执行后面的C代码。这里有很多变量都是我们进行kernel移植时需要特别注意的,下面会一一讲到。

      在这里我们首先看看这段汇编开始跑的时候的寄存器信息,这里的寄存器内容实际上是同bootloader跳转到解压代码是一样的,就是r1=arch  r2=atag addr。下面我们就具体来看看这个head.S跑的过程:

      view plaincopy to clipboardprint?
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode   
                    @ and irqs disabled   
mrc p15, 0, r9, c0, c0      @ get processor id  
    msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
                        @ and irqs disabled
    mrc p15, 0, r9, c0, c0      @ get processor id
 

      首先进入SVC模式并关闭所有中断,并从arm协处理器里面读到CPU ID,这里的CPU主要是指arm架构相关的CPU型号,比如ARM9,ARM11等等。

      view plaincopy to clipboardprint?
  
 

       然后跳转到__lookup_processor_type,这个函数定义在head-common.S里面,这里的bl指令会保存当前的pc在lr里面,最后__lookup_processor_type会从这个函数返回,我们具体看看这个函数:     

       view plaincopy to clipboardprint?
__lookup_processor_type:   
    adr r3, 3f   
    ldmda   r3, {r5 - r7}   
    sub r3, r3, r7          @ get offset between virt&phys   
    add r5, r5, r3          @ convert virt addresses to   
    add r6, r6, r3          @ physical address space   
1:  ldmia   r5, {r3, r4}            @ value, mask   
    and r4, r4, r9          @ mask wanted bits   
    teq r3, r4   
    beq 2f   
    add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)   
    cmp r5, r6   
    blo 1b   
    mov r5, #0              @ unknown processor   
2:  mov pc, lr   
ENDPROC(__lookup_processor_type)  
__lookup_processor_type:
    adr r3, 3f
    ldmda   r3, {r5 - r7}
    sub r3, r3, r7          @ get offset between virt&phys
    add r5, r5, r3          @ convert virt addresses to
    add r6, r6, r3          @ physical address space
1:  ldmia   r5, {r3, r4}            @ value, mask
    and r4, r4, r9          @ mask wanted bits
    teq r3, r4
    beq 2f
    add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)
    cmp r5, r6
    blo 1b
    mov r5, #0              @ unknown processor
2:  mov pc, lr
ENDPROC(__lookup_processor_type)
 

       他这里的执行过程其实比较简单就是在__proc_info_begin和__proc_info_end这个段里面里面去读取我们注册在里面的proc_info_list这个结构体,这个结构体的定义在arch/arm/include/asm/procinfo.h,具体实现根据你使用的cpu的架构在arch/arm/mm/里面找到具体的实现,这里我们使用的ARM11是proc-v6.S,我们可以看看这个结构体:

       view plaincopy to clipboardprint?
.section ".proc.info.init", #alloc, #execinstr   
/*   
 * Match any ARMv6 processor core.  
 */  
.type   __v6_proc_info, #object  
_proc_info:   
.long   0x0007b000   
.long   0x0007f000   
.long   PMD_TYPE_SECT | /   
    PMD_SECT_BUFFERABLE | /           
    PMD_SECT_CACHEABLE | /   
    PMD_SECT_AP_WRITE | /   
    PMD_SECT_AP_READ   
.long   PMD_TYPE_SECT | /   
    PMD_SECT_XN | /   
    PMD_SECT_AP_WRITE | /   
    PMD_SECT_AP_READ   
b   __v6_setup   
.long   cpu_arch_name   
.long   cpu_elf_name   
.long   HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP|HWCAP_JAVA   
.long   cpu_v6_name   
.long   v6_processor_functions    
.long   v6wbi_tlb_fns   
.long   v6_user_fns   
.long   v6_cache_fns   
.size   __v6_proc_info, . - __v6_proc_info  
    .section ".proc.info.init", #alloc, #execinstr
    /* 
     * Match any ARMv6 processor core.
     */
    .type   __v6_proc_info, #object
__v6_proc_info:
    .long   0x0007b000
    .long   0x0007f000
    .long   PMD_TYPE_SECT | /
        PMD_SECT_BUFFERABLE | /        
        PMD_SECT_CACHEABLE | /
        PMD_SECT_AP_WRITE | /
        PMD_SECT_AP_READ
    .long   PMD_TYPE_SECT | /
        PMD_SECT_XN | /
        PMD_SECT_AP_WRITE | /
        PMD_SECT_AP_READ
    b   __v6_setup
    .long   cpu_arch_name
    .long   cpu_elf_name
    .long   HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP|HWCAP_JAVA
    .long   cpu_v6_name
    .long   v6_processor_functions 
    .long   v6wbi_tlb_fns
    .long   v6_user_fns
    .long   v6_cache_fns
    .size   __v6_proc_info, . - __v6_proc_info

       对着.h我们就知道各个成员变量的含义了,他这里lookup的过程实际上是先求出这个proc_info_list的实际物理地址,并将其内容读出,然后将其中的mask也就是我们这里的0x007f000与寄存器与之后与0x007b00进行比较,如果一样的话呢就校验成功了,如果不一样呢就会读下一个proc_info的信息,因为proc一般都是只有一个的,所以这里一般不会循环,如果检测正确寄存器就会将正确的proc_info_list的物理地址赋给寄存器,如果检测不到就会将寄存器值赋0,然后通过LR返回。

        view plaincopy to clipboardprint?
bl  __lookup_machine_type       @ r5=machinfo   
movs    r8, r5              @ invalid machine (r5=0)?         
beq __error_a           @ yes, error 'a'  
    bl  __lookup_machine_type       @ r5=machinfo
    movs    r8, r5              @ invalid machine (r5=0)?      
    beq __error_a           @ yes, error 'a'

       检测完proc_info_list以后就开始检测machine_type了,这个函数的实现也在head-common.S里面,我们看看它具体的实现:

        view plaincopy to clipboardprint?
__lookup_machine_type:   
    adr r3, 3b   
    ldmia   r3, {r4, r5, r6}   
    sub r3, r3, r4          @ get offset between virt&phys    
    add r5, r5, r3          @ convert virt addresses to       
    add r6, r6, r3          @ physical address space          
1:  ldr r3, [r5, #MACHINFO_TYPE]    @ get machine type   
    teq r3, r1              @ matches loader number?          
    beq 2f              @ found       
    add r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc   
    cmp r5, r6   
    blo 1b   
    mov r5, #0              @ unknown machine   
2:  mov pc, lr   
ENDPROC(__lookup_machine_type)  
__lookup_machine_type:
    adr r3, 3b
    ldmia   r3, {r4, r5, r6}
    sub r3, r3, r4          @ get offset between virt&phys 
    add r5, r5, r3          @ convert virt addresses to    
    add r6, r6, r3          @ physical address space       
1:  ldr r3, [r5, #MACHINFO_TYPE]    @ get machine type
    teq r3, r1              @ matches loader number?       
    beq 2f              @ found    
    add r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
    cmp r5, r6
    blo 1b
    mov r5, #0              @ unknown machine
2:  mov pc, lr
ENDPROC(__lookup_machine_type)
 

        这里的过程基本上是同proc的检查是一样的,这里主要检查芯片的类型,比如我们现在的芯片是MSM7X27FFA,这也是一个结构体,它的头文件在arch/arm/include/asm/arch/arch.h里面(machine_desc),它具体的实现根据你对芯片类型的选择而不同,这里我们使用的是高通的7x27,具体实现在arch/arm/mach-msm/board-msm7x27.c里面,这些结构体最后都会注册到_arch_info_begin和_arch_info_end段里面,具体的大家可以看看vmlinux.lds或者system.map,这里的lookup会根据bootloader传过来的nr来在__arch_info里面的相匹配的类型,没有的话就寻找下一个machin_desk结构体,直到找到相应的结构体,并会将结构体的地址赋值给寄存器,如果没有的话就会赋值为0的。一般来说这里的machine_type会有好几个,因为不同的芯片类型可能使用的都是同一个cpu架构。

       对processor和machine的检查完以后就会检查atags parameter的有效性,关于这个atag具体的定义我们可以在./include/asm/setup.h里面看到,它实际是一个结构体和一个联合体构成的结合体,里面的size都是以字来计算的。这里的atags param是bootloader创建的,里面包含了ramdisk以及其他memory分配的一些信息,存储在boot.img头部结构体定义的地址中,具体的大家可以看咱以后对bootloader的分析~

       view plaincopy to clipboardprint?
__vet_atags:   
    tst r2, #0x3            @ aligned?    
    bne 1f   
    ldr r5, [r2, #0]            @ is first tag ATAG_CORE?         
    cmp r5, #ATAG_CORE_SIZE   
    cmpne   r5, #ATAG_CORE_SIZE_EMPTY   
    bne 1f   
    ldr r5, [r2, #4]   
    ldr r6, =ATAG_CORE   
    cmp r5, r6   
    bne 1f   
    mov pc, lr              @ atag pointer is ok              
1:  mov r2, #0   
    mov pc, lr   
ENDPROC(__vet_atags)  
__vet_atags:
    tst r2, #0x3            @ aligned? 
    bne 1f
    ldr r5, [r2, #0]            @ is first tag ATAG_CORE?      
    cmp r5, #ATAG_CORE_SIZE
    cmpne   r5, #ATAG_CORE_SIZE_EMPTY
    bne 1f
    ldr r5, [r2, #4]
    ldr r6, =ATAG_CORE
    cmp r5, r6
    bne 1f
    mov pc, lr              @ atag pointer is ok           
1:  mov r2, #0
    mov pc, lr
ENDPROC(__vet_atags)
 

       这里对atag的检查主要检查其是不是以ATAG_CORE开头,size对不对,基本没什么好分析的,代码也比较好看~ 下面我们来看后面一个重头戏,就是创建初始化页表,说实话这段内容我没弄清楚,它需要对ARM VIRT MMU具有相当的理解,这里我没有太多的时间去分析spec,只是粗略了翻了ARM V7的manu,知道这里建立的页表是arm的secition页表,完成内存开始1m内存的映射,这个页表建立在kernel和atag paramert之间,一般是4000-8000之间~具体的代码和过程我这里就不贴了,大家可以看看参考的链接,看看其他大虾的分析,我还没怎么看明白,等以后仔细研究ARM MMU的时候再回头来仔细研究了,不过代码虽然不分析,这里有几个重要的地址需要特别分析下~

      这几个地址都定义在arch/arm/include/asm/memory.h,我们来稍微分析下这个头文件,首先它包含了arch/memory.h,我们来看看arch/arm/mach-msm/include/mach/memory.h,在这个里面定义了#define PHYS_OFFSET     UL(0x00200000) 这个实际上是memory的物理内存初始地址,这个地址和我们以前在boardconfig.h里面定义的是一致的。然后我们再看asm/memory.h,他里面定义了我们的memory虚拟地址的首地址#define PAGE_OFFSET     UL(CONFIG_PAGE_OFFSET)。     

      另外我们在head.S里面看到kernel的物理或者虚拟地址的定义都有一个偏移,这个偏移又是从哪来的呢,实际我们可以从arch/arm/Makefile里面找到:textofs-y   := 0x00008000     TEXT_OFFSET := $(textofs-y) 这样我们再看kernel启动时候的物理地址和链接地址,实际上它和我们前面在boardconfig.h和Makefile.boot里面定义的都是一致的~

      建立初始化页表以后,会首先将__switch_data这个symbol的链接地址放在sp里面,然后获得__enable_mmu的物理地址,然后会跳到__proc_info_list里面的INITFUNC执行,这个偏移是定义在arch/arm/kernel/asm-offset.c里面,实际上就是取得__proc_info_list里面的__cpu_flush这个函数执行。

      view plaincopy to clipboardprint?
ldr r13, __switch_data      @ address to jump to after        
                    @ mmu has been enabled            
adr lr, __enable_mmu        @ return (PIC) address   
add pc, r10, #PROCINFO_INITFUNC  
    ldr r13, __switch_data      @ address to jump to after     
                        @ mmu has been enabled         
    adr lr, __enable_mmu        @ return (PIC) address
    add pc, r10, #PROCINFO_INITFUNC
 

      这个__cpu_flush在这里就是我们proc-v6.S里面的__v6_setup函数了,具体它的实现我就不分析了,都是对arm控制寄存器的操作,这里转一下它对这部分操作的注释,看完之后就基本知道它完成的功能了。

 /*

 *  __v6_setup

 *

 *  Initialise TLB, Caches, and MMU state ready to switch the MMU

 *  on.  Return in r0 the new CP15 C1 control register setting.

 *

 *  We automatically detect if we have a Harvard cache, and use the

 *  Harvard cache control instructions insead of the unified cache

 *  control instructions.

 *

 *  This should be able to cover all ARMv6 cores.

 *

 *  It is assumed that:      

 *  - cache type register is implemented

 */   

        完成这部分关于CPU的操作以后,下面就是打开MMU了,这部分内容也没什么好说的,也是对arm控制寄存器的操作,打开MMU以后我们就可以使用虚拟地址了,而不需要我们自己来进行地址的重定位,ARM硬件会完成这部分的工作。打开MMU以后,会将SP的值赋给PC,这样代码就会跳到__switch_data来运行,这个__switch_data是一个定义在head-common.S里面的结构体,我们实际上是跳到它地一个函数指针__mmap_switched处执行的。

        这个switch的执行过程我们只是简单看一下,前面的copy data_loc段以及清空.bss段就不用说了,它后面会将proc的信息和machine的信息保存在__switch_data这个结构体里面,而这个结构体将来会在start_kernel的setup_arch里面被使用到。这个在后面的对start_kernel的详细分析中会讲到。另外这个switch还涉及到控制寄存器的一些操作,这里我不没仔细研究spec,不懂也就不说了~

        好啦,switch操作完成以后就会b start_kernel了~ 这样就进入了c代码的运行了,下一篇文章仔细研究这个start_kernel的函数~~

关键字:Android  arm  linux  kernel  启动流程 引用地址:Android arm linux kernel启动流程二

上一篇:Android arm linux kernel启动流程一
下一篇:Kinect移植到嵌入式ARM平台上面

推荐阅读最新更新时间:2024-03-16 14:59

基于S3C2440的嵌入式Linux驱动——SPI子系统解读(二)
本文属于第二部分。 4. 主控制器驱动程序 4.1 定义 platform device 下列数据结构位于arch/arm/plat-s3c24XX/devs.c /* SPI (0) */ static struct resource s3c_spi0_resource = { = { .start = S3C24XX_PA_SPI, .end = S3C24XX_PA_SPI + 0x1f, .flags = IORESOURCE_MEM, }, = { .start = IRQ_SPI0, .end = IRQ_S
[单片机]
基于S3C2440的嵌入式<font color='red'>Linux</font>驱动——SPI子系统解读(二)
STM32高级开发(5)-gcc-arm-none-eabi
在完成对ubuntu的基本操作和指令的学习后,我们下面正式的进入有关于我们stm32/ARM单片机的软件安装过程。首先我们就要介绍这个会贯穿我们整个开发过程中的软件gcc-arm-none-eabi。 gcc-arm-none-eabi是什么 最直接的当我说出这个软件的时候,大部分童鞋可能会奇怪他到底是个什么呢?首先他是个软件这就不用多说了,做什么的软件呢?编译软件,或是准确点叫工具链。那么听起来是不是和keil、IAR感觉一样呢?我们是不是安装好他以后,打开获得一个界面导入代码就可以编译hex文件呢?答案是否定的。 又我们国内Keil+IAR环境教育出来的童靴们,很少会理解IDE其本质的含义。也许查询百度,他会告诉你ID
[单片机]
STM32高级开发(5)-gcc-<font color='red'>arm</font>-none-eabi
ARM中断向量表重定位到片外RAM方法
由于ARM CPU产生中断或者异常后,PC指针自动跳转到0x00地址执行(同时执行一些CPSR寄存器的保存、运行模式的转换等),所以要在0x00地址处存放中断向量表。而如果我们想将中断向量表重定位到片外ram的 话, 有2中方法: 1、启用MMU 将片外RAM空间隐射到0x00处 2、在0x00(片内RAM)地址处存放一份和片外RAM一模一样的中断向量表 标准做法是将程序存放在NAND FLASH里面,S3C2440 CPU启动后,会将程序复制到片内RAM里面,此时中断向量表也复制到了IRAM里 3、有部分CPU支持设置中断向量表的寄存器 这样也可以实现重定位
[单片机]
OK6410A 开发板 (八) 34 linux-5.11 OK6410A 内存管理第二阶段
B __turn_mmu_on符号 - setup_arch- paging_init- bootmem_init- memblock_allow_resize返回 ----此时memblock初始化完成,开启了基于虚拟内时代的 memblock内存管理器时代 流程 __turn_mmu_on mcr p15, 0, r0, c1, c0, 0 @ write control reg // 内存管理相关1 // 上句执行之后,mmu开启 ret r3 // 调用到 __mmap_switched __mmap_switched adr r4, __mmap_switched_data /
[单片机]
浅析Arm Linux操作系统调用流程详细解析 .
系统调用是os操作系统提供的服务,用户程序通过各种系统调用,来引用内核提供的各种服务,系统调用的执行让用户程序陷入内核,该陷入动作由swi软中断完成. At91rm9200处理器对应的linux2.4.19内核系统调用对应的软中断定义如下: #if defined(__thumb__) //thumb模式 #define __syscall(name)/ push {r7}/n/t / mov r7, # __sys1(__NR_##name) /n/t / swi 0/n/t / pop {r7} #else //arm模式 #define __syscal
[单片机]
11-S3C2440驱动学习(八)嵌入式linux-块设备驱动程序
一、回顾字符设备驱动 主要分为简单字符设备驱动程序,和复杂字符设备驱动 1、简单字符设备驱动 对于简单的字符设备驱动,不需要采用分离分层的思想,主要包括以下几个部分。 2、复杂字符设备驱动 对于复杂的字符设备驱动,采用分离分层的思想,内核中已经实现好了核心层部分,我们只需要实现与硬件相关的部分就可以,最后形成一个总体。这样就是一个通用的字符驱动框架。如LCD驱动、V4L2驱动,当然有时候我们可以选择不采用分离分层的思想,按简单驱动程序的框架来实现一个驱动。 3、字符设备驱动常用技巧 (1) 查询方式 (2) 休眠唤醒,APP-read drv_read (3) poll机制 (4) 异步通知发信号
[单片机]
11-S3C2440驱动学习(八)嵌入式<font color='red'>linux</font>-块设备驱动程序
移植linux-2.6.30.4到S3C2440
一、下载linux-2.6.30.4源码,并解压 ftp://ftp.kernel.org/pub/linux/kernel/v2.6/linux-2.6.30.4.tar.gz tar zxvf linux-2.6.30.4.tar.gz 二、在系统中添加对ARM的支持 $vim Makefile 193#ARCH ?= $(SUBARCH) 194#CROSS_COMPILE ?= 195 ARCH=arm 196 CROSS_COMPILE=arm-linux- 三、修改系统时钟 $vim arch/arm/mach-s3c2440/mach-smdk2440.c 系统的外部时钟为12MHz 160static
[单片机]
ARM-Intel大战即将上演,好戏还会在后头
  “消费计算器件”这个词涵盖的范围很广,但是大多指笔记本电脑和手机。具体怎么称呼手机和电脑这两类器件,业界目前存在很大争议。“移动互连设备(MID)”一词在业界通常指手机一类器件,但OEM(如DELL)趋向于用“移动互连设备(MID)”一词指笔记本电脑这一类设备。而Qualcomm(高通)则采用完全不同的术语:用“个人计算装置”(personal computing device,PCD)定义手机产品,用“移动计算装置(mobile computing device ,MCD)”来定义笔记本电脑产品。为了避免混淆,文中采用“手机类设备” 和“笔记本电脑设备”这两个更直观的术语。   Intel当前推出的Atom Centrin
[工业控制]
小广播
添点儿料...
无论热点新闻、行业分析、技术干货……
热门活动
换一批
更多
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

更多精选电路图
换一换 更多 相关热搜器件
更多每日新闻
随便看看
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved