M4内核启动全过程(从零写代码,不用任何库,深入分析启动过程和函数调用规则)

news2024/12/29 8:48:32

引言

        玩过stm32的小伙伴,应该知道,在使用的keil工程里面有一个start.s的启动文件(网上关于这个启动文件的分析很多,本文不是讲解启动文件的文字,不打算具体讲解这个文件的内容)。start.s文件是芯片复位、启动要运行的一个代码文件(其实在之前还会运行其它的东西,在后面会介绍下),在复位中断里面会执行SystemInit初始化系统时钟,最后调用__main(keil C库的API),__main调用我们写的main函数,进入用户写的代码。同时在讲解的过程中,对函数调用的规则进行深入分析,C和汇编的调用规则进行理论和实践的讲解。

        本文将对上述过程进行分析,从芯片刚上电的时刻开始,芯片进行了哪些工作,最终启动完成,运行main,该部分的代码全部有博主手写,并未使用任何C库中的代码,希望读者对芯片的启动过程有一个更详细的了解,而非仅仅知道start.s文件中的那些内容。

        博主用的芯片是stm32F407(下面简称F4芯片、芯片等),启动地址(内部flash地址)在0x0800 0000,RAM在0x2000 0000位置,flash大小512KB,RAM 192KB(分为128KB+64KB,下文中为了省事,直接将这两个RAM放到一起了,不影响结论,且结论和RAM大小无关)。

        注:下文中的flash大小和ram大小可能会出现错误,这是因为博主写的时候,用了两个芯片做实验,flash、ram的起始地址和大小是不影响本文的结论。

一、芯片启动过程

1.1 内存映射

图1 部分内存映射

        在F4芯片中的部分内存映射如图1所示。在芯片刚上电的时候,PC指针实际的值是0x0000_0000,并非很多人理解的0x0800_0000(boot0为低电平),其执行的是Boot ROM中的代码,这部分代码是芯片厂商烧录进去,用户无法更改。在这部分代码中,芯片厂商可以在里面做一些定制性的操作。例如同样的芯片,配置不同(关掉某些功能),成为两个芯片,其也会去检测boot0、boot1引脚,在stm32中,若是boot0为0(低电平)、boot1任意电平,pc会赋值为0x0800_0000,执行内部的flash(若是boot1、boot1为1,执行SRAM中的程序)。在Boot ROM中,芯片厂商会进行一些定制的操作,具体与本文内容无关。在F4芯片中,boot为0(低电平)、boot1任意电平时,会跳转到0x0800_0000处,开始执行用户写的代码。

        在之前的文章中(keil下载程序具体过程),博主说明了一个keil编译生成的axf文件,是如何从在keil中点击download按钮之后下载到内部的flash中。感兴趣的看看博主的文章,在flash中的储存的是bin文件,我们先看下bin文件有什么。

1.2 bin文件

        bin文件是由axf文件转化过来的,axf文件ELF格式的二进制文件,里面除了包含bin文件的内容,还包含了调试信息、地址信息等。keil编译之后,产生的链接结果,博主设置了在编译之后生成bin文件。

图2 keil编译结果

我们看下bin文件的大小。

图3 bin文件大小

bin文件大小2780字节。由图2可知:

Code=2436 RO-data=340 RW-data=4 ZI-data=276。

        一个bin文件包含Code、RO、RW、ZI段的信息,keil工程中,代码部分放在Code中,只读数据放到RO中(如常量字符串、const修饰的变量等等),可读数据放到RW中(如已经初始化的全局变量),ZI段放的是未初始化的全局变量等定义了的变量,但未初始化,这样可以减少bin文件的大小。

        Code + RO + RW =   2436 + 340 + 4 = 2780 = bin文件大小,没有ZI段,ZI段只需要记录起始地址和长度,将该部分内存全部清零即可,故可知,bin的文件内容实际就是Code、RO、RW三部分。

        至于Code、RO、RW具体应该如何存放,放到哪个地址,需要看分散加载文件如何写。

        关于映像文件的介绍先到这里,只是简单的说了下映像文件,后面会出一篇文章专门介绍映像文件的组成。

1.3 分散加载文件

        分散加载文件(sct后缀的文件)是一个文本文件,通过编写一个分散加载文件来指定ARM连接器在生成映像文件时如何分配RO/RW/ZI等数据的存放地址,HTQ_MCU芯片的分散加载文件如下所示。

LR_IROM1 0x08000000 0x00100000  {    ; load region size_region
  ER_IROM1 0x08000000 0x00100000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
   .ANY (+XO)
  }
  RW_IRAM1 0x20000000 0x00020000  {  ; RW data
   .ANY (+RW +ZI)
  }
}

分散加载文件的区域分两类:

加载区:该映像文件开始运行前存放的区域,即当系统启动或加载时应用程序存放的区域(本文的Flash部分)。
执行区:映像文件运行时的区域,即系统启动后,应用程序进行执行和数据访问的存储器区域,系统在实时运行是可以有一个或多个执行块(本文也是指Flash部分)。

        上述分散链接文件定义了一个区域ER_IROM1(起始地址为0x08000000,大小为0x00100000,为内部的Flash映射地址),在ER_IROM1中存放bin,也在这个上面执行。有的时候,存放的bin位置和执行地址,二者可能不一致,需要修改分散链接文件。定义了区域RW_IRAM1,起始地址为0x20000000,大小0x00020000 ,为SRAM映射地址。

        在分散链接文件中,只有RO、RW属性,并没有Code属性。其实,在分散链接文件看来,Code本身也是一种RO数据,因此,

Code=2436 RO-data=340 RW-data=4 ZI-data=276。

Code和RO在bin中放到一起,叫做RO段,RW放到RW段 。      

        由上述可知,我们的bin存放在0x08000000处,该处存放的是RO段(Code部分和RO部分),紧接着存放的是RW段,这一点,我们在后面可以得到验证。

1.4 链接符号

        在keil中,我们可以使用类似于下面这样的链接符号,在代码中获得分散链接文件中定义的区域相关信息,还可以更具体的获得RO段、RW段、ZI段的信息。在本文中,我们使用如下链接符号获得bin中的信息。

extern int Image$$ER_IROM1$$Base;       //RO段的起始地址
extern int Image$$ER_IROM1$$Limit;      //RO段的结束地址

extern int Image$$RW_IRAM1$$RW$$Base;   //RW段的起始地址,加载地址
extern int Image$$RW_IRAM1$$RW$$Limit;  //RW段的结束地址,加载地址

extern int Load$$RW_IRAM1$$RW$$Base;    //RW段的起始地址,执行地址
extern int Load$$RW_IRAM1$$RW$$Limit;   //RW段的结束地址,执行地址

extern int Image$$RW_IRAM1$$ZI$$Base;   //ZI段的起始地址,执行地址
extern int Image$$RW_IRAM1$$ZI$$Limit;  //ZI段的结束地址,执行地址

        RO段的数据,并不需要改动,因此存放在0x0800 0000处不动。RW段数据需要从加载地址拷贝到执行地址处运行,ZI段都是零,因此,只需要知道执行地址即可(bin中也没有ZI段,只有ZI段的起始地址和结束地址信息)。

1.5 start.s文件简述

        在stm32f407中,使用的是startup_stm32f40_41xxx.s作为启动文件。下面的代码做了简化。

Stack_Size      EQU     0x00001000

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp

Heap_Size       EQU     0x00000200

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

                PRESERVE8
                THUMB

; Vector Table Mapped to Address 0 at Reset
                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size
__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
                DCD     0                          ; Reserved
            ......
__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors

                AREA    |.text|, CODE, READONLY

; Reset handler
Reset_Handler    PROC
                 EXPORT  Reset_Handler             [WEAK]
        IMPORT  SystemInit
        IMPORT  __main

                 LDR     R0, =SystemInit
                 BLX     R0
                 LDR     R0, =__main
                 BX      R0
                 ENDP

        最开始,设置了栈、堆的大小,之后是中断向量表,这部分代码存放在bin文件的最开头,bin的前两个字就是stack和pc的值,对应的就是__initial_sp、Reset_Handler。我们的代码就进入到了Reset_Handler即复位中断,在这里面,调用SystemInit初始化系统的时钟,调用__main,完成一些环境的初始化(后面具体展开),最后进入main(应用程序的入口)函数,即我们写的代码,控制权来到了程序员手中。

       打开汇编代码的时候,我们可以看到如下的代码。

LDR R0, =__main,之后会跳转到这里

这里会多出来__scatterload、__rt_entry函数,这些都是C库里面的函数,进行一些初始化的工作。

截取了一些代码放这里。有兴趣的自己看下里面的汇编代码具体这么做的,这里总结下里面做的内容。

__scatterload:将RORW段从加载域复制到运行域,并完成ZI段初始化。

__rt_entry:完成库函数的初始化工作,如果采用分散装载技术,还必须实现__user_initial_stackheap函数,重新定义堆栈和堆空间,最后自动跳转向main()函数。

在__rt_entry函数里面,使用__rt_lib_init完成库的初始化工作,调用__rt_stackheap_init()设置stack和heap,最后进入main函数。main函数的返回值由__rt_exit函数处理。

        __main是C/C++应用程序的入口点,main是用户应用程序入口点。简单的总结下__main函数做了哪些工作:

1、将非固定(nonroot)的执行代码域(region)从加载地址空间搬运到运行地址空间

2、将ZI域清零

3、跳转到__rt_entry()

       __rt_entry函数做的工作:

1、调用__rt_stackheap_init()简历数据栈和数据堆

2、调用__rt_lib_init()初始化应用程序用到的C运行时库

3、调用main函数,main是用户程序的入口点,在main函数中,可以调用C运行时库的相应函数

4、调用exit()函数,退出应用程序

        既然想用全部手写代码实现启动代码的过程,我们在上面的start.s的基础上,将__main函数改成我们自己的代码就可以完成。__main函数主要完成3个功能。__rt_entry中的初始化C库的部分,由于我们未用到C库,可以去掉,只保留初始化堆栈和进入main的功能。__scatterload部分我们手动实现它。

二、实现__main函数

        在实现__main的时候,博主将其功能分为如下几类:

1、初始化RW:将RW段由加载域搬运到运行域。由于RO段是不动的,因此,RO段搬运的代码未实现。

2、初始化ZI:ZI段都是0,因此,只是将某一块内容清空即可。

3、跳转main:考虑汇编的难读性,博主实现的代码均是使用C完成,因此,在start.s中直接跳转到main.s了。

画个简图表示上面实现的关系。

2.1 加载域和运行域的关系

       添加一张ARM给的图

   图片来源于《Introductiontothe  Armv8-M  Architecture  and its ProgrammersModel》

        在1.3中已经说明了加载域和运行域是什么含义,下面用图表示下二者中的具体含义。

左边是flash部分,从0x0800  0000地址开始,bin文件里面内容摆放的位置,从低地址开始,分别是RO、RW、ZI段的信息,code存放的位置也是RO段,RO段不需要动。RW段的内容,需要从flash搬运到sram中,在flash和sram中存放的起始地址、结束地址如下:

flash:Image$$RW_IRAM1$$RW$$Base   -----    Image$$RW_IRAM1$$RW$$Limit
sram:   Load$$RW_IRAM1$$RW$$Base    -----    Load$$RW_IRAM1$$RW$$Limit

上面的链接符合在1.4节有说明。

2.2 RW段拷贝

        既然知道了RW段的存放位置和目的地址,因此,只需要写一个简单的函数将里面的内容全部copy到sram即可。函数如下:

void rw_section_init(uint32_t src_addr, uint32_t dst_addr, uint32_t length)
{
    uint32_t * psrc, *pdst, *pend;
    
    psrc = (uint32_t*)src_addr;
    pdst = (uint32_t*)dst_addr;
    pend = (uint32_t*)(src_addr + length);
    
    while(psrc < pend){
        *pdst++ = *psrc++;
    }
}

函数很简单,src_addr表示起始地址,为flash中bin文件的RW段起始地址,dst_addr表示目的地址,为sram中的RW段起始地址,length表示copy字节长度。具体的值为2.1小节的flash起始地址和sram的起始地址。

2.3 ZI段清零 

        ZI段的内容在flash中只保留了起始地址,长度等信息,并无实际内容,在sram直接清空即可。这里说一下,在实际代码中,当一个变量小于8 Byte时,哪怕未赋值,实际存放的也是RW段,而非ZI段。读者可以自行试试。

        ZI段初始化代码很简单。

void zi_section_init(uint32_t src_addr, uint32_t length)
{
    uint8_t * psrc, *pend;
    
    psrc = (uint8_t*)src_addr;
    pend = (uint8_t*)(src_addr + length - 16); 
    while(psrc < pend){
        *psrc++ = 0;
    }
}

src_addr的值为Image**RW_IRAM1**ZI**Base,length为(Image**RW_IRAM1**ZI**Limit - Image**RW_IRAM1**ZI**Base)。注:*表示的是$,CSDN上两个$显示的有问题。

        这里解释下为什么这行代码里面有一个 “ - 16 ”的操作:

pend = (uint8_t*)(src_addr + length - 16)

         我们都知道ZI段里面的值都是0,包含所有未初始化的变量、数组、堆、栈等等。在博主写的代码里,堆的内存大小设置为0(为了简化代码),栈的大小设置为200 Byte(大小无所谓),ZI段一共276 Byte。用一个图来表示ZI段的内容。

  

        在sram中的ZI段大概类似于上图所示。在我们的代码里面,应该将从ZI**Base开始到ZI**Limit中的所有数据都清空,设置为0。但博主在写的时候,用的纯C,使用C就必须设置好sp指针,sp指针设置的位置为ZI**Limit。在执行LDR     R0, =main(在start.s中)时,sp指针会向下移动4B,在main和zi_section_init中又定义了一些局部变量,因此又要去掉一些空间。因此必须给留出一些空间。接下来的分析将和编译器有关,因此,若是对该部分不是很熟悉的小伙伴,看看即可。        

三、ATPCS

3.1 ATPCS

        在解释之前,先补充点额外的知识,与ARM链接器相关,ATPCS相关知识。ATPCS是ARM程序和Thumb程序中子程序调用的基本规则(博主发的文章里面有讲解ATPCS)。在ATPCS中有如下两条规则:

1、子程序间通过R0--R3传递参数,被调用的子程序在返回前无需恢复寄存器R0--R3的内容。

2、在子程序中,使用R4--R11保存局部变量,如果在子程序中使用到了寄存器R4--R11中的某些寄存器,子程序进入前需要保存这些寄存器的值,在返回时需要恢复。未用到,不处理。在Thumb程序中,通常只能使用寄存器R4--R7来保存局部变量。

        ATPCS部分,博主会抽时间写一篇文章专门讲解ATPCS规则(纯理论的文章,估计很多人不太喜欢看^_^)本小节的内容可以看作ATPCS规则的实战部分讲解了。

        keil编译出来的各个段大小,RO段=Code+RO-data,RW段=RW-data,ZI段=ZI-data=276=0x114Byte。

Program Size: Code=2508 RO-data=340 RW-data=4 ZI-data=276  

 3.2 函数调用详细分析

在start.s中还未进main函数时,芯片的现场环境如下:

从这里可以看出一些信息:

1)博主在进入main之前设置RO--R11分别为0--11,为了后面调试追踪寄存器入栈。

2)栈顶指针为0x118,而我们设置的ZI段为0x114,这是因为sp指针指向栈最后一个单元的后一个单元,即如图所示:

故sp的值为0x118 = 0x114 + 4。接下来进入main函数。

先上main的代码(无用的代码已删去)

int main()
{
    uint32_t image_rw_start_addr = (uint32_t)&Image$$RW_IRAM1$$RW$$Base; 
    uint32_t image_rw_length = (uint32_t)&Image$$RW_IRAM1$$RW$$Limit - (uint32_t)&Image$$RW_IRAM1$$RW$$Base; 
    
    uint32_t load_rw_start_addr = (uint32_t)&Load$$RW_IRAM1$$RW$$Base; 

    uint32_t image_zi_start_addr = (uint32_t)&Image$$RW_IRAM1$$ZI$$Base; 
    uint32_t image_zi_length = (uint32_t)&Image$$RW_IRAM1$$ZI$$Limit - (uint32_t)&Image$$RW_IRAM1$$ZI$$Base;  

    rw_section_init(load_rw_start_addr, image_rw_start_addr, image_rw_length);

    zi_section_init(image_zi_start_addr, image_zi_length);
}

        首先,结合zi_section_init函数,先计算下main和zi_section_init函数一共有多少局部变量:

main:5个,分别是image_rw_start_addr、image_rw_length、load_rw_start_addr、image_zi_start_addr、image_zi_length

zi_section_init:2个,分别是psrc、pend

刚进入main的时候

 栈指针为0x118。

进入rw_section_init函数之前,寄存器的值和监视的变量。

栈大小为0x110,PC为0x10000A1A。

再看看各个变量的值以及寄存器里面的值

        具体的就不分析了,可以自己对照上面红色方框里面的值,说下结论:Watch 1里面监视变量在R8、R9、R10、R4、R6中储存,其余未用到的寄存器都是保存的开始设置的值,如R5保存的值为5,R7为7。

进入rw_section_init函数之后

        进入函数之后,栈值为0x110。R0--R2保存的是rw_section_init,和load_rw_start_addr, image_rw_start_addr, image_rw_length值相等(肯定的)。这一步是传递参数,使用LR保存PC寄存器的值,但LR的值为0x0x10000A25,不是0x10000A1A。

        重新来看下,在进入函数之前的rw_section_init汇编代码,可以看到,红框部分有四条汇编代码,故LR的值应该为0x0x10000A20+4,至于为0x0x10000A25而非A24,这是因为在ARMv7之前,指令集分为ARM和Thumb,而进入到Cortext-M3之后,使用的是Thumb2,不再区分ARM和Thumb状态,但依然保留某些时候会将PC的最低为置1(具体什么时候博主也忘了,在《Cortex-M3权威指南》中博主曾经看到过一眼,具体在哪里就不太清楚了)。

        刚进入rw_section_init函数,sp还未变动,只是R0--R2变化。

再运行一步,看看栈是如何变化的。

        栈减少了0x10 B,说明入栈了4个字(word)。 看看栈的变化,再四个红色框框里面,0x2000 00FC地址是不不变化的,但后面四个字就变成了0x2000 0004、0x0000 0005、

0x0000 0114、0x1000 0A25。 0x1000 0A25是PC的值,最先入栈(栈是倒着增长),依次入栈    0x0000 0114、  0x0000 0005、0x2000 0004。这三个是寄存器R6、R5、R4的值(如此看来寄存器应该是高寄存器先入栈),按照ATPCS规则,R4--R11是可以保存局部变量的,而rw_section_init函数刚好三个局部变量,看看汇编代码。

        确实跟分析的一样。接下来就是将R0保存到R3,R0、R4、R5分别保存psrc, pdst, pend的值(准确的说,在C中的局部变量psrc, pdst, pend实际就是R0、R4、R5)。

看看具体的汇编代码。

对照着C的循环代码。

可以看到,R6实际上是赋值数据的中间变量作用。最后弹出提前保存的R4--R6、LR

直接将lr的值弹出到pc中,完成pc的赋值。

        函数调用的具体过程、栈的变化、寄存器和变量的分配使用等在这里进行了较深入的介绍。这里虽然没有直接解释为什么“- 16”,但进行了间接的解释,相信不难理解,进入到zi_section_init函数时,要对栈的内容进行保护(“- 16”的操作)。具体的不分析了,这个函数比较简单,简单的看下即可。

这里栈为0x20000108,最开始栈为0x0x20000118,相差0x10即16B,符合前面的操作。

四、题外话

        关于__main函数的分析,暂时就到这里了。里面有很多内容可以深挖下去,以后若是继续进行研究再来补充。博主在写代码时,执行zi_section_init函数之前的内容完全没问题,就是执行zi_section_init函数时,程序老是跑飞,到HardFault_Handler里面。一开始百思不得其解,但我将pend的值设置的小一些(上面“-16”的操作部分),发现程序可以正常运行完成了。16这个值最开始是博主试验出来的(^_^),后面根据ATPCS规则,想通了这部分的代码为啥是16了(当然,不排除想错了的可能.....)。

        总结一下芯片的启动过程:

1、读取Boot ROM(芯片厂商的代码)

2、执行start.s(设置堆栈大小、中断向量表、初始化系统时钟、初始化堆栈、初始化C库,进入main)

3、执行用户的main,控制权回到程序员手中。

        这里面牵扯到很多的技术,博主知道的就有:CPU启动、编译器、ARM汇编、C库函数相关,硬件体系等等,每一部分都有很多知识点,有很多规范。博主也只是刚刚进入计算机的世界,以后还要继续学习,争取早日能在计算机的世界中做更深入的工作。

PS:写在最后,这篇文章从开始到结束跨度时间有点长,里面的内容应该有不少纰漏之处,博主做实验的时候,芯片用了两款,还不是一家的产品,但文章中对芯片的分析是没啥问题的,结论也没啥问题,只有一些关于地址、储存器大小相关部分的数据有些问题,写到最后已经没法改动了,还请见谅。

代码中包含不少调试信息,博主就不删了。链接:https://gitee.com/zichuanning520/htq_library/tree/master/%E5%8F%82%E8%80%83%E4%BB%A3%E7%A0%81/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1120656.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微信小程序-微信授权登录

前言 小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识&#xff0c;快速建立小程序内的用户体系 一.微信授权登录工作流程 1.理论叙述 触发授权登录: 用户在小程序中触发登录操作&#xff0c;通常通过点击登录按钮或执行相关操作。 授权弹窗: 小程序弹…

python树结构包treelib入门及其计算应用

树是计算机科学中重要的数据结构。例如决策树等机器学习算法设计、文件系统索引等。创建treelib包是为了在Python中提供树数据结构的有效实现。 Treelib的主要特点包括&#xff1a; 节点搜索的高效操作。支持常见的树操作&#xff0c;如遍历、插入、删除、节点移动、浅/深复制…

【Ascend C算子开发(入门)】——Ascend C编程模式与范式

Ascend C编程模型与范式 1.并行计算架构抽象 Ascend C编程开发的算子是运行在AI Core上的&#xff0c;所以我们需要了解一下AI Core的结构。AI Core主要包括计算单元、存储单元、搬运单元。 计算单元包括了三种计算资源&#xff1a;Scalar计算单元&#xff08;执行标量计算&…

登上抖音同城热搜榜:如何让你的短视频成为焦点?

登上抖音同城热搜榜的首要前提是紧跟潮流&#xff0c;捕捉热点。热点通常具有时效性、话题性和关注度&#xff0c;能够迅速吸引人们的注意力。要想捕捉热点&#xff0c;你需要关注新闻、社交媒体和抖音热门话题&#xff0c;时刻关注大众关心的问题。例如&#xff0c;近期热门的…

rabbitMQ(3)

RabbitMq 交换机 文章目录 1. 交换机的介绍2. 交换机的类型3. 临时队列4. 绑定 (bindings)5. 扇形交换机&#xff08;Fanout &#xff09; 演示6. 直接交换机 Direct exchange6.1 多重绑定6.2 direct 代码案例 7. 主题交换机7.1 Topic 匹配案例7.2 Topic 代码案例 8. headers 头…

【力扣每日一题】2023.10.22 做菜顺序

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 给我们一个数组表示每个菜的满意度&#xff0c;我们可以指定做哪些菜以及做的顺序&#xff0c;需要我们凑到一个系数的最大值&#xff0c…

第三章 内存管理 十五、内存映射文件

目录 一、传统的文件访问方式 二、内存映射文件 1、方便文件的访问 2、实现文件数据的共享 三、总结 一、传统的文件访问方式 二、内存映射文件 1、方便文件的访问 2、实现文件数据的共享 三、总结

教你注册chrome开发者账号,并发布chrome浏览器插件。

本篇文章主要讲解&#xff0c;注册chrome开发者账号&#xff0c;及发布chrome浏览器插件的流程。包含插件的打包和上传。 日期&#xff1a;2023年10月22日 作者&#xff1a;任聪聪 一、前提准备&#xff1a;注册chrome开发者账号 说明&#xff1a;注册需要5美元&#xff0c;一…

Qt界面容器:Widget、 Frame、分组框、滚动区、工具箱、选项卡小部件、堆叠小部件控件精讲

​Qt 界面设计容器简介 Qt中常用的容器控件, 包括: Widget, Frame, Group Box, Scroll Area, Tool Box, Tab Widget, Stacked Widget。 QWidget 这个类是所有窗口类的父类, 可以作为独立窗口使用, 也可以内嵌到其它窗口中使用。 头文件: #include <QWidget> qmake: QT…

按键控制LED灯亮灭

按键原理图&#xff1a;按键选用PE4 创建两个文件一个.c文件一个.h文件来写按键的代码 .c文件 #include "Key.h"void Key_Init(void) {RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,ENABLE);GPIO_InitTypeDef GPIO_InitStruct;GPIO_InitStruct.GPIO_Mode GPIO_M…

python astra相机驱动问题

报错问题&#xff1a; openni.utils.OpenNIError: (OniStatus.ONI_STATUS_ERROR, bDeviceOpen using default: no devices found, None) 解决办法&#xff1a; 1、从sdk中拷贝文件 2、修改openni源码 3、执行测试程序 from openni import openni2 import numpy as np impor…

基于驾驶训练优化的BP神经网络(分类应用) - 附代码

基于驾驶训练优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于驾驶训练优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.驾驶训练优化BP神经网络3.1 BP神经网络参数设置3.2 驾驶训练算法应用 4.测试结果…

【C/C++笔试练习】初始化列表、构造函数、析构函数、两种排序方法、求最小公倍数

文章目录 C/C笔试练习1. 初始化列表&#xff08;1&#xff09;只能在列表初始化的变量 2.构造函数&#xff08;2&#xff09;函数体赋值&#xff08;3&#xff09;构造函数的概念&#xff08;4&#xff09;构造函数调用次数&#xff08;5&#xff09;构造函数调用次数&#xff…

自然语言处理---RNN经典案例之使用seq2seq实现英译法

1 seq2seq介绍 1.1 seq2seq模型架构 seq2seq模型架构分析&#xff1a; seq2seq模型架构&#xff0c;包括两部分分别是encoder(编码器)和decoder(解码器)&#xff0c;编码器和解码器的内部实现都使用了GRU模型&#xff0c;这里它要完成的是一个中文到英文的翻译&#xff1a;欢迎…

数据库MongoDB

MongoDB记录是一个文档&#xff0c;由一个字段和值对组成的数据结构&#xff0c;文档类似于JSON对象。 一个文档认为就是一个对象&#xff0c;字段的数据类型是字符型&#xff0c;值除了使用基本类型外&#xff0c;还可以包括其他文档&#xff0c;普通数组和文档数组。 一、…

Python —— UI自动化之使用JavaScript进行元素点亮、修改、点击元素

1、JavaScript点亮元素 在控制台通过JavaScript语言中对元素点亮效果如下&#xff1a; 将这个语句和UI自动化结合&#xff0c;代码如下&#xff1a; locator (By.ID,"kw") # 是元组类型 web_element WebDriverWait(driver,5,0.5).until(EC.visibility_of_eleme…

Windows安装virtualenv虚拟环境

需要先安装好python环境 1 创建虚拟环境目录 还是在D:\Program\ 的文件夹新建 .env 目录&#xff08;你也可以不叫这个名字&#xff0c;一般命名为 .env 或者 .virtualenv &#xff0c;你也可以在其他目录中创建&#xff09; 2 配置虚拟环境目录的环境变量 3 安装虚拟环境 进…

网络原理之UDP协议

文章目录 前言应用层协议常见的几种数据格式1. xml2. JSON3. protobuffer 端口号传输层UDP 报文协议格式源端口号和目的端口号UDP 长度校验和 前言 前面我们学习了如何使用 UDP 数据报 和 TCP 流实现网络编程一个回显服务器&#xff0c;在知道了 UDP 和 TCP 协议的基本原理之后…

Arduino驱动BMA220三轴加速度传感器(惯性测量传感器篇)

目录 1、传感器特性 2、硬件原理图 3、驱动程序 BMA220的三轴加速度计是一款具有I2C接口的超小型三轴低g加速度传感器断路器,面向低功耗消费市场应用。它可以测量3个垂直轴的加速度,从而在手机、手持设备、计算机外围设备、人机界面、虚拟现实功能和游戏控制器中感知倾斜、…

MYSQL第五章节有关约束操作详解(附代码,例题详解)这一篇就够了

c知识点合集已经完成欢迎前往主页查看&#xff0c;点点赞点点关注不迷路哦 点我进入c第一章知识点合集 MYSQL第一章节DDL数据定义语言的操作----点我进入 MYSQL第二章节DDL-数据库操作语言 DQL-数据查询语言----点我进入 MYSQL第三章节DCL-管理用户&#xff0c;控制权限----点我…