마이크로 컨트롤러는 어떻게 단계적으로 부팅 및 시작합니까?


17

C 코드를 작성, 컴파일 및 마이크로 컨트롤러에 업로드하면 마이크로 컨트롤러가 실행되기 시작합니다. 그러나이 업로드 및 시작 프로세스를 슬로우 모션으로 단계별로 가져 가면 실제로 MCU 내부에서 발생하는 상황 (메모리, CPU, 부트 로더)에 대해 약간의 혼란이 있습니다. 누군가가 나에게 물어 보면 내가 대답 할 내용은 다음과 같습니다.

  1. 컴파일 된 이진 코드는 USB를 통해 플래시 ROM (또는 EEPROM)에 기록됩니다
  2. 부트 로더는이 코드의 일부를 RAM에 복사합니다. 참이면 부트 로더는 무엇을 복사 할 것인지 (ROM의 어느 부분이 RAM에 복사 할 것인지) 어떻게 알 수 있습니까?
  3. CPU가 ROM 및 RAM에서 명령 및 코드 데이터 가져 오기를 시작합니다.

이것이 잘못 되었습니까?

이 부팅 및 시작 프로세스를 메모리, 부트 로더 및 CPU가이 단계에서 어떻게 상호 작용하는지에 대한 정보와 함께 요약 할 수 있습니까?

BIOS를 통해 PC를 부팅하는 방법에 대한 많은 기본 설명을 찾았습니다. 그러나 마이크로 컨트롤러 시작 프로세스에 갇혀 있습니다.

답변:


31

1) 컴파일 된 바이너리가 prom / flash에 기록됩니다. USB, 직렬, i2c, jtag 등은 부팅 프로세스를 이해하는 데 관계없이 해당 장치가 지원하는 장치에 따라 장치에 따라 다릅니다.

2) 이것은 일반적으로 마이크로 컨트롤러에게는 해당되지 않으며, 주요 사용 사례는 ROM / 플래시 명령과 램 데이터를 사용하는 것입니다. 어떤 아키텍처이든 상관 없습니다. 비 마이크로 컨트롤러, PC, 랩톱, 서버의 경우 프로그램은 비 휘발성 (디스크)에서 램으로 복사되어 실행됩니다. 일부 마이크로 컨트롤러에서는 정의를 위반 한 것으로 보이지만 하버드를 주장하는 램도 사용할 수 있습니다. 램을 명령어 측에 매핑하는 것을 방해하는 하버드에 대한 것은 없습니다. 전원이 켜진 후 명령어를 얻을 수있는 메커니즘이 필요합니다 (정의를 위반하지만 하버드 시스템은 다른 유용한 기능을 수행해야합니다) 마이크로 컨트롤러보다).

3) 종류.

각 CPU는 설계된대로 결정 론적으로 "부팅"합니다. 가장 일반적인 방법은 전원을 켠 후 첫 번째 명령어의 주소가 재설정 벡터에있는 벡터 테이블입니다.이 주소는 하드웨어가 읽은 다음 해당 주소를 사용하여 실행을 시작하는 주소입니다. 다른 일반적인 방법은 프로세서가 잘 알려진 주소에서 벡터 테이블없이 실행을 시작하도록하는 것입니다. 때때로 칩에는 "스트랩"이 있으며, 리셋을 해제하기 전에 높거나 낮게 묶을 수있는 일부 핀, 로직이 다른 방식으로 부팅하는 데 사용됩니다. CPU 자체, 프로세서 코어를 나머지 시스템과 분리해야합니다. CPU 작동 방식을 이해 한 다음 칩 / 시스템 설계자가 CPU 외부 주변에 주소 디코더를 설정하여 CPU 주소 공간의 일부가 플래시와 통신 할 수 있음을 이해합니다. 일부는 램이 있고 일부는 주변기기 (uart, i2c, spi, gpio 등)가 있습니다. 원하는 경우 동일한 CPU 코어를 사용하여 다르게 감쌀 수 있습니다. 이것은 팔이나 밉을 기반으로 무언가를 구입할 때 얻는 것입니다. 팔과 밉은 CPU 코어를 만듭니다. 칩 코어는 사람들이 자신의 물건을 사서 포장합니다. 여러 가지 이유로 그들은 그 물건을 브랜드에서 브랜드로 호환시키지 못합니다. 그렇기 때문에 핵심 이외의 모든 것에 대해 일반적인 암 질문을 거의 할 수 없습니다.

마이크로 컨트롤러는 칩의 시스템이 되려고 시도하므로 비 휘발성 메모리 (플래시 / 롬), 휘발성 (스램) 및 CPU는 모두 주변 장치와 함께 동일한 칩에 있습니다. 그러나 칩은 내부적으로 플래시가 해당 CPU의 부팅 특성과 일치하는 CPU의 주소 공간에 매핑되도록 설계되었습니다. 예를 들어 CPU에 주소 0xFFFC에 재설정 벡터가있는 경우 유용한 프로그램을 위해 주소 공간에 충분한 플래시 / ROM과 함께 1)을 통해 프로그래밍 할 수있는 해당 주소에 응답하는 플래시 / ROM이 있어야합니다. 칩 설계자는 이러한 요구 사항을 충족시키기 위해 0xF000에서 0x1000 바이트의 플래시를 시작하도록 선택할 수 있습니다. 그리고 아마도 그들은 약간의 램을 더 낮은 주소 또는 0x0000에, 주변 장치를 중간 어딘가에 넣었습니다.

CPU의 다른 아키텍처는 주소 0에서 실행을 시작할 수 있으므로 반대의 작업을 수행하고 플래시를 배치하여 0 근처의 주소 범위에 응답합니다. 예를 들어 0x0000 ~ 0x0FFF라고 말하십시오. 램을 다른 곳에 두십시오.

칩 설계자는 CPU 부팅 방법을 알고 있으며 비 휘발성 스토리지를 플래시 (ROM / ROM)에 배치했습니다. 그런 다음 해당 CPU의 잘 알려진 동작과 일치하도록 부팅 코드를 작성하는 것은 소프트웨어 사용자의 몫입니다. 리셋 벡터 주소는 리셋 벡터에, 부트 코드는 리셋 벡터에 정의한 주소에 배치해야합니다. 툴체인은 여기서 크게 도움이 될 수 있습니다. 때로는 포인트 앤 클릭 아이디어 나 다른 샌드 박스를 사용하여 대부분의 작업을 수행 할 수 있습니다. 고급 언어 (C)로 api를 호출하기 만하면됩니다.

그러나 플래시 / ROM에로드 된 프로그램은 CPU의 유선 부팅 동작과 일치해야합니다. main () 프로그램의 C 부분을 시작하기 전에 main을 시작점으로 사용하려면 몇 가지 작업을 수행해야합니다. AC 프로그래머는 초기 값으로 변수를 선언 할 때 실제로 작동 할 것으로 예상합니다. 글쎄, const 이외의 변수는 램에 있지만 초기 값을 가진 변수가 있으면 초기 값은 비 휘발성 램에 있어야합니다. 따라서 이것은 .data 세그먼트이며 C 부트 스트랩은 .data 항목을 플래시에서 램으로 복사해야합니다 (일반적으로 툴 체인에 의해 결정됩니다). 초기 값없이 선언하는 전역 변수는 프로그램이 시작되기 전에 0으로 가정하지만 실제로는 일부 컴파일러가 초기화되지 않은 변수에 대해 경고하기 시작한다고 가정해서는 안됩니다. 이것은 .bss 세그먼트이며, 램에서 내용 인 0 인 C 부트 스트랩 제로는 비 휘발성 메모리에 저장 될 필요는 없지만 시작 주소와 그 양은 얼마입니까? 다시 한 번 툴체인이 크게 도움이됩니다. 마지막으로 최소한 C 프로그램은 로컬 변수를 가질 수 있고 다른 함수를 호출 할 수 있기 때문에 스택 포인터를 설정해야합니다. 그런 다음 다른 칩 관련 작업이 수행되거나 나머지 칩 관련 작업이 C에서 발생하도록 할 수 있습니다. 비 휘발성 메모리에 저장 될 필요는 없지만 시작 주소와 용량은 얼마입니까? 다시 한 번 툴체인이 크게 도움이됩니다. 마지막으로 최소한 C 프로그램은 로컬 변수를 가질 수 있고 다른 함수를 호출 할 수 있기 때문에 스택 포인터를 설정해야합니다. 그런 다음 다른 칩 관련 작업이 수행되거나 나머지 칩 관련 작업이 C에서 발생하도록 할 수 있습니다. 비 휘발성 메모리에 저장 될 필요는 없지만 시작 주소와 용량은 얼마입니까? 다시 한 번 툴체인이 크게 도움이됩니다. 마지막으로 최소한 C 프로그램은 로컬 변수를 가질 수 있고 다른 함수를 호출 할 수 있기 때문에 스택 포인터를 설정해야합니다. 그런 다음 다른 칩 관련 작업이 수행되거나 나머지 칩 관련 작업이 C에서 발생하도록 할 수 있습니다.

arm의 cortex-m 시리즈 코어 가이 작업을 수행합니다. 스택 포인터는 벡터 테이블에 있으며 재설정 후 코드를 가리 키도록 재설정 벡터가 있으므로 수행해야 할 작업 이외의 작업 벡터 테이블을 생성하기 위해 (어쨌든 보통 asm을 사용합니다) asm없이 순수한 C를 사용할 수 있습니다. 이제는 .data를 복사하거나 .bss를 0으로 만들지 않으므로 cortex-m 기반으로 asm없이 가려고하면 직접해야합니다. 더 큰 특징은 리셋 벡터가 아니라 하드웨어가 권장 C 호출 규칙을 따르고 레지스터를 유지하고 인터럽트 벡터를 올바르게 사용하여 각 핸들러 주위에 올바른 asm을 감쌀 필요가없는 인터럽트 벡터입니다 ( 또는 툴체인이 당신을 위해 그것을 감싸도록 타겟에 대한 툴체인 특정 지시문을 갖도록하십시오).

칩 관련 사항은 예를 들어, 마이크로 컨트롤러는 종종 배터리 기반 시스템에서 사용되므로 전력이 낮으므로 일부 주변 장치를 끈 상태에서 일부는 재설정되지 않으므로 이러한 하위 시스템을 켜야 사용할 수 있습니다. . Uarts, gpios 등 크리스탈이나 내부 발진기에서 곧바로 낮은 클럭 속도가 사용되는 경우가 많습니다. 그리고 시스템 설계에 더 빠른 시계가 필요하다는 것을 보여줄 수 있으므로 초기화하십시오. 시계가 플래시 또는 램에 비해 너무 빠를 수 있으므로 시계를 올리기 전에 대기 상태를 변경해야 할 수도 있습니다. uart 또는 USB 또는 기타 인터페이스를 설정해야 할 수도 있습니다. 그런 다음 응용 프로그램이 그 일을 할 수 있습니다.

컴퓨터 데스크탑, 랩톱, 서버 및 마이크로 컨트롤러는 부팅 / 작동 방식이 다르지 않습니다. 그것들이 대부분 하나의 칩에 있지는 않습니다. BIOS 프로그램은 종종 CPU와 별도의 칩 플래시 / ROM에 있습니다. 최근 x86 CPU가 칩을 지원하는 칩을 동일한 패키지 (pcie 컨트롤러 등)에 점점 더 많이 가져오고 있지만 여전히 램과 롬 오프 칩이 대부분이지만 여전히 시스템이며 여전히 작동합니다. 높은 수준에서 동일합니다. CPU 부팅 프로세스는 잘 알려져 있으며 보드 설계자는 플래시가 부팅되는 주소 공간에 플래시 / ROM을 배치합니다. 해당 프로그램 (x86 pc의 BIOS 부분)은 위에서 언급 한 모든 작업을 수행하고 다양한 주변 장치를 시작하고 dram을 초기화하고 pcie 버스를 열거하는 등의 작업을 수행합니다. 사용자가 BIOS 설정 또는 cmos 설정을 호출 할 때 사용한 기술을 기반으로 사용자가 구성 할 수있는 경우가 많았습니다. 당시 기술이 사용 되었기 때문입니다. 중요하지 않지만, BIOS 부팅 코드의 기능을 변경하는 방법을 알려주기 위해 변경할 수있는 사용자 설정이 있습니다.

다른 사람들은 다른 용어를 사용합니다. 칩 부트, 즉 첫 번째로 실행되는 코드입니다. 때때로 부트 스트랩이라고도합니다. 로더라는 단어가있는 부트 로더는 종종 방해 할 일이 없다면 일반 부팅에서 더 큰 응용 프로그램 또는 운영 체제로 부팅하는 부트 스트랩이라는 것을 의미합니다. 그러나 로더 부분은 부팅 프로세스를 중단 한 다음 다른 테스트 프로그램을로드 할 수 있음을 의미합니다. 예를 들어 임베디드 리눅스 시스템에서 uboot를 사용한 적이 있다면 키를 누르고 일반 부팅을 중지 한 다음 테스트 커널을 램으로 다운로드하여 플래시가 아닌 램으로 부팅하거나 다운로드 할 수 있습니다 자체 프로그램을 사용하거나 새 커널을 다운로드 한 다음 부트 로더가 플래시에 쓰도록하여 ​​다음에 부팅 할 때 새 항목을 실행할 수 있습니다.

CPU 자체까지는 주변 장치의 플래시에서 램을 모르는 코어 프로세서. 부트 로더, 운영 체제, 응용 프로그램에 대한 개념은 없습니다. 실행되는 CPU에 공급되는 일련의 명령입니다. 서로 다른 프로그래밍 작업을 구별하기위한 소프트웨어 용어입니다. 서로 소프트웨어 개념.

일부 마이크로 컨트롤러에는 칩 공급 업체가 제공하지 않는 별도의 부트 로더가 있으며 별도의 플래시 또는 별도의 플래시 영역에서 수정할 수 없습니다. 이 경우 종종 핀 또는 핀 세트 (스트랩이라고 부름)가 있습니다. 리셋이 해제되기 전에 핀을 높거나 낮게 묶는 경우 로직 및 / 또는 부트 로더에게 수행 할 작업을 알려줍니다 (예 : 하나의 스트랩 조합) 칩에 해당 부트 로더를 실행하도록 지시하고 플래시에 데이터가 프로그래밍 될 때까지 uart를 기다리십시오. 스트랩을 다른 방식으로 설정하면 칩 벤더 부트 로더가 아닌 프로그램이 부팅되므로 칩을 현장에서 프로그래밍하거나 프로그램 충돌을 복구 할 수 있습니다. 때로는 플래시를 프로그래밍 할 수있는 순수한 논리 일 수도 있습니다. 이것은 요즘 꽤 흔합니다.

대부분의 마이크로 컨트롤러가 램보다 훨씬 더 많은 플래시를 갖는 이유는 주요 사용 사례는 플래시에서 직접 프로그램을 실행하고 스택 및 변수를 포함하기에 충분한 램만 가지고 있기 때문입니다. 경우에 따라 램에서 프로그램을 실행하여 바로 컴파일하고 플래시에 저장 한 다음 호출하기 전에 복사해야합니다.

편집하다

플래시

.cpu cortex-m0
.thumb

.thumb_func
.global _start
_start:
stacktop: .word 0x20001000
.word reset
.word hang
.word hang
.word hang

.thumb_func
reset:
    bl notmain
    b hang

.thumb_func
hang:   b .

notmain.c

int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;

    return(0);
}

flash.ld

MEMORY
{
    bob : ORIGIN = 0x00000000, LENGTH = 0x1000
    ted : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > bob
    .rodata : { *(.rodata*) } > bob
    .bss : { *(.bss*) } > ted
    .data : { *(.bss*) } > ted AT > bob
}

따라서 이것은 cortex-m0의 예입니다. cortex-ms는이 예제가 진행되는 한 모두 동일하게 작동합니다. 이 예에서 특정 칩은 암 주소 공간의 주소 0x00000000에서 애플리케이션 플래시를, 0x20000000에서 램을 갖습니다.

cortex-m이 부팅되는 방식은 주소 0x0000의 32 비트 워드가 스택 포인터를 초기화하는 주소입니다. 이 예제에는 많은 스택이 필요하지 않으므로 0x20001000으로 충분할 것입니다. 분명히 그 주소 아래에 램이 있어야합니다 (팔이 밀리는 방식, 먼저 빼는 것이므로 0x20001000을 설정하면 스택의 첫 번째 항목은 주소 0x2000FFFC입니다) 0x2000FFFC를 사용할 필요는 없습니다). 주소 0x0004의 32 비트 워드는 재설정 핸들러의 주소이며 기본적으로 재설정 후 실행되는 첫 번째 코드입니다. 그런 다음 해당 cortex m 코어 및 칩, 128 또는 256만큼 많은 인터럽트 및 이벤트 핸들러가 있습니다. 사용하지 않으면 테이블을 설정할 필요가 없습니다. 시연을 위해 몇 가지를 던졌습니다. 목적.

이 예제에서는 .data 또는 .bss를 처리 할 필요가 없습니다. 코드를 보면 해당 세그먼트에 아무것도 없다는 것을 알고 있기 때문입니다. 내가 그것을 처리한다면, 그리고 잠시 후에 것입니다.

따라서 스택은 설정, 확인, .data 처리, 확인, .bss, 확인이므로 C 부트 스트랩 작업이 완료되고 C에 대한 진입 함수로 분기 될 수 있습니다. 일부 컴파일러는 함수를 볼 때 추가 정크를 추가하기 때문에 main () 및 main으로가는 길에 정확한 이름을 사용하지 않습니다. 여기서 C 항목으로 notmain ()을 사용했습니다. 따라서 리셋 핸들러는 notmain ()을 호출 한 다음 notmain ()이 반환 할 때 무한 루프 인 멈춤으로 이름이 잘못 지정 될 수 있습니다.

나는 많은 사람들이 도구를 마스터하는 것을 굳게 믿지만, 당신이 찾을 수있는 것은 앱이나 웹 페이지를 만드는 것처럼 원격으로 제한되지 않은 거의 완전한 자유 때문에 각각의 베어 메탈 개발자가 자신의 일을한다는 것입니다. . 그들은 다시 자신의 일을합니다. 나는 나만의 부트 스트랩 코드와 링커 스크립트를 선호한다. 다른 사람들은 툴체인에 의존하거나 대부분의 작업이 다른 사람에 의해 수행되는 공급 업체 샌드 박스에서 게임을합니다.

따라서 gnu 도구를 사용하여 조립, 컴파일 및 연결합니다.

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

그렇다면 부트 로더는 물건이 어디에 있는지 어떻게 알 수 있습니까? 컴파일러가 작업을 수행했기 때문입니다. 첫 번째 경우 어셈블러는 flash.s에 대한 코드를 생성했으며 그렇게하면 레이블이 어디에 있는지 (레이블은 함수 이름이나 변수 이름과 같은 주소 일뿐입니다) 바이트를 계산하고 벡터를 채울 필요가 없습니다. 테이블을 수동으로 레이블 이름을 사용하고 어셈블러가 대신했습니다. 이제 reset이 주소 0x14인지 어셈블러가 벡터 테이블에 0x15를 넣은 이유는 무엇입니까? 글쎄, 이것은 cortex-m이며 부팅되고 엄지 모드에서만 실행됩니다. Thumb 모드로 분기하는 경우 주소로 분기 할 때 ARM을 사용하면 arm 모드가 재설정 된 경우 lsbit를 설정해야합니다. 따라서 항상 해당 비트 세트가 필요합니다. 도구를 알고 레이블이 벡터 테이블에 그대로 사용되거나 분기 또는 기타로 사용되는 경우 레이블 앞에 .thumb_func를 넣어서 알고 있습니다. 툴체인은 lsbit을 설정하는 것을 알고 있습니다. 여기 0x14 | 1 = 0x15가 있습니다. 행 아웃도 마찬가지입니다. 이제 디스어셈블러는 notmain () 호출에 대해 0x1D를 표시하지 않지만 도구가 명령을 올바르게 작성했는지 걱정하지 마십시오.

이제 코드가 메인이 아니고 해당 로컬 변수가 사용되지 않고 죽은 코드입니다. 컴파일러는 y가 설정되었지만 사용되지 않았다고 말함으로써 그 사실에 대해 언급합니다.

주소 공간을 주목하십시오.이 모든 것은 주소 0x0000에서 시작하여 거기에서 벡터 테이블이 올바르게 배치되고 .text 또는 프로그램 공간이 올바르게 배치됩니다. 도구를 알면 일반적인 실수는 바로 그 권리를 얻지 못하고 추락하고 열심히 태워 버리는 것입니다. IMO는 처음 부팅하기 직전에 물건을 놓기 위해 분해해야합니다. 적절한 장소에 물건이 있으면 매번 점검 할 필요는 없습니다. 새 프로젝트 나 중단 된 경우에만 해당됩니다.

이제 일부 사람들에게 놀라운 것은 두 컴파일러가 동일한 입력에서 동일한 출력을 생성 할 것으로 기대할 이유가 없다는 것입니다. 또는 다른 설정을 가진 동일한 컴파일러조차도. clang을 사용하여 llvm 컴파일러를 사용하여 최적화 여부에 관계 없이이 두 가지 출력을 얻습니다.

llvm / clang 최적화

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

최적화되지 않은

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   b082        sub sp, #8
  1e:   2001        movs    r0, #1
  20:   9001        str r0, [sp, #4]
  22:   2002        movs    r0, #2
  24:   9000        str r0, [sp, #0]
  26:   2000        movs    r0, #0
  28:   b002        add sp, #8
  2a:   4770        bx  lr

컴파일러가 덧셈을 최적화했다는 거짓말이지만 변수에 대해 스택에 두 항목을 할당했습니다. 이것은 로컬 변수이기 때문에 램에 있지만 고정 주소가 아닌 스택에 있으면 전역에서 볼 수 있습니다. 변화. 그러나 컴파일러는 컴파일 타임에 y를 계산할 수 있고 런타임에 계산할 이유가 없으므로 x에 할당 된 스택 공간에 1을, y에 할당 된 스택 공간에 2를 간단히 배치했습니다. 컴파일러는 변수 y의 경우 stack plus 0을, 변수 x의 경우 stack plus 4를 선언하는 내부 테이블로이 공간을 "할당"합니다. 컴파일러는 구현하는 코드가 C 표준 또는 C 프로그래머의 표준을 준수하는 한 원하는 모든 작업을 수행 할 수 있습니다. 컴파일러가 함수 기간 동안 x를 스택 + 4에 두어야 할 이유가 없습니다.

어셈블러에 함수 더미를 추가하면

.thumb_func
.globl dummy
dummy:
    bx lr

그리고 전화 해

void dummy ( unsigned int );
int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;
    dummy(y);
    return(0);
}

출력 변경

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f804   bl  20 <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <dummy>:
  1c:   4770        bx  lr
    ...

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

이제 중첩 함수가 있으므로 notmain 함수는 리턴 주소를 보존해야 중첩 호출의 리턴 주소를 파악할 수 있습니다. 팔이 x86 또는 다른 것들과 같은 스택을 잘 사용하면 반환을 위해 레지스터를 사용하기 때문입니다 ... 아직 스택을 사용하지만 다르게 사용합니다. 이제 왜 r4를 눌렀습니까? 글쎄, 호출 규칙은 오래 전에 스택을 하나의 워드 경계인 32 비트 대신 64 비트 (2 워드) 경계에 정렬하도록 변경했습니다. 따라서 스택 정렬을 유지하기 위해 무언가를 밀어야하므로 컴파일러는 어떤 이유로 든 r4를 임의로 선택했지만 이유는 중요하지 않습니다. 이 대상에 대한 호출 규칙에 따라 r4에 튀기는 것은 버그 일 것입니다. 함수 호출에서 r4를 클로버하지 말고 r0에서 r3까지 클로버 할 수 있습니다. r0은 반환 값입니다. 꼬리 최적화를하고있는 것 같습니다.

그러나 우리는 x와 y 수학이 더미 함수에 전달되는 2의 하드 코딩 된 값에 최적화되어 있음을 알 수 있습니다 (더미는 별도의 파일,이 경우 asm으로 코딩되어 컴파일러가 함수 호출을 완전히 최적화하지 못합니다) notmain.c에서 C로 반환 된 더미 함수가있는 경우 옵티마이 저는 x, y 및 더미 함수 호출을 모두 제거 / 쓸모없는 코드이기 때문에 제거했을 것입니다).

또한 flash.s 코드가 커졌기 때문에 notmain은 그렇지 않으며 툴 체인은 우리를 위해 모든 주소를 패치하여 처리하므로 수동으로 수행하지 않아도됩니다.

참조를 위해 최적화되지 않은 클랑

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   b082        sub sp, #8
  26:   2001        movs    r0, #1
  28:   9001        str r0, [sp, #4]
  2a:   2002        movs    r0, #2
  2c:   9000        str r0, [sp, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   b002        add sp, #8
  36:   bd80        pop {r7, pc}

최적화 된 클랑

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   2002        movs    r0, #2
  26:   f7ff fff9   bl  1c <dummy>
  2a:   2000        movs    r0, #0
  2c:   bd80        pop {r7, pc}

컴파일러 작성자는 스택을 정렬하기 위해 더미 변수로 r7을 사용하기로 선택했으며 스택 프레임에 아무것도없는 경우에도 r7을 사용하여 프레임 포인터를 만듭니다. 기본적으로 교육은 최적화되었을 수 있습니다. 그러나 팝을 사용하여 세 가지 명령을 반환하지 않았습니다. 아마도 올바른 명령 행 옵션 (프로세서 지정)으로 gcc를 수행 할 수있을 것입니다.

이것은 대부분 나머지 질문에 답해야합니다.

void dummy ( unsigned int );
unsigned int x=1;
unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

나는 지금 세계를 가지고있다. 최적화되지 않으면 .data 또는 .bss로 이동합니다.

최종 출력을보기 전에 itermediate 객체를 볼 수 있습니다

00000000 <notmain>:
   0:   b510        push    {r4, lr}
   2:   4b05        ldr r3, [pc, #20]   ; (18 <notmain+0x18>)
   4:   6818        ldr r0, [r3, #0]
   6:   4b05        ldr r3, [pc, #20]   ; (1c <notmain+0x1c>)
   8:   3001        adds    r0, #1
   a:   6018        str r0, [r3, #0]
   c:   f7ff fffe   bl  0 <dummy>
  10:   2000        movs    r0, #0
  12:   bc10        pop {r4}
  14:   bc02        pop {r1}
  16:   4708        bx  r1
    ...

Disassembly of section .data:
00000000 <x>:
   0:   00000001    andeq   r0, r0, r1

이제 이것에서 누락 된 정보가 있지만 무슨 일이 일어나고 있는지에 대한 아이디어를 제공합니다. 링커는 객체를 가져 와서 제공된 정보 (이 경우 flash.ld)와 함께 연결하여 .text 및. 데이터 등이 있습니다. 컴파일러는 그러한 것들을 알지 못하며, 제시된 코드에만 집중할 수 있으며 외부는 링커가 연결을 채우기 위해 구멍을 남겨 두어야합니다. 모든 데이터는 그러한 것들을 함께 연결하는 방법을 남겨 두어야하므로 컴파일러와 디스어셈블러가 알지 못하기 때문에 모든 주소는 여기서 0을 기준으로합니다. 링커가 물건을 배치하는 데 사용하는 다른 정보는 여기에 표시되지 않습니다. 여기의 코드는 위치 독립적이므로 링커가 작업을 수행 할 수 있습니다.

그런 다음 연결된 출력의 분해를 확인하십시오.

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   4b05        ldr r3, [pc, #20]   ; (38 <notmain+0x18>)
  24:   6818        ldr r0, [r3, #0]
  26:   4b05        ldr r3, [pc, #20]   ; (3c <notmain+0x1c>)
  28:   3001        adds    r0, #1
  2a:   6018        str r0, [r3, #0]
  2c:   f7ff fff6   bl  1c <dummy>
  30:   2000        movs    r0, #0
  32:   bc10        pop {r4}
  34:   bc02        pop {r1}
  36:   4708        bx  r1
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

Disassembly of section .bss:

20000000 <y>:
20000000:   00000000    andeq   r0, r0, r0

Disassembly of section .data:

20000004 <x>:
20000004:   00000001    andeq   r0, r0, r1

컴파일러는 기본적으로 램에 두 개의 32 비트 변수를 요청했습니다. 하나는 초기화하지 않았기 때문에 .bss에 있으므로 0으로 초기화한다고 가정합니다. 다른 하나는 선언시 초기화했기 때문에 .data입니다.

이제는 전역 변수이기 때문에 다른 함수가 변수를 수정할 수 있다고 가정합니다. 컴파일러는 notmain이 호출 될 수있는 시점에 대해 가정하지 않으므로 y = x + 1 수학으로 볼 수있는 것과 최적화 할 수 없으므로 런타임을 수행해야합니다. 램에서 두 변수를 추가하고 다시 저장해야합니다.

이제 분명히이 코드가 작동하지 않습니다. 왜? 여기에 표시된 내 부트 스트랩은 notmain을 호출하기 전에 램을 준비하지 않기 때문에 칩이 깨어 났을 때 0x20000000 및 0x20000004에있는 쓰레기는 y와 x에 사용될 것입니다.

여기에 표시하지 않습니다. .data 및 .bss에 대한 더 긴 바람이 부딪 치는 부분을 읽을 수 있으며 베어 메탈 코드에서 왜 필요하지 않은지 알 수 있지만 다른 사람이 올바르게하기를 기대하기보다는 도구를 마스터해야한다고 생각하는 경우 .. .

https://github.com/dwelch67/raspberrypi/tree/master/bssdata

링커 스크립트와 부트 스트랩은 다소 컴파일러마다 다르므로 한 컴파일러의 한 버전에 대해 배우는 모든 것이 다음 버전이나 다른 컴파일러와 관련이있을 수 있지만 .data 및 .bss 준비에 많은 노력을 기울이지 않는 또 다른 이유 이 게으른 사람이 되려면 :

unsigned int x=1;

나는 오히려 오히려 이것을 할 것입니다

unsigned int x;
...
x = 1;

컴파일러가 .text에 넣도록하십시오. 때로는 플래시를 절약하여 때로는 더 많이 태우기도합니다. 툴체인 버전이나 한 컴파일러에서 다른 컴파일러로 프로그래밍하고 포팅하는 것이 가장 쉽습니다. 훨씬 더 안정적이며 오류가 적습니다. 그러나 C 표준을 준수하지 않습니다.

이 정적 글로벌을 만들면 어떻게 될까요?

void dummy ( unsigned int );
static unsigned int x=1;
static unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

분명히 이러한 변수는 다른 코드로 수정할 수 없으므로 컴파일러는 컴파일 타임에 이전과 마찬가지로 죽은 코드를 최적화 할 수 있습니다.

최적화되지 않은

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   4804        ldr r0, [pc, #16]   ; (38 <notmain+0x18>)
  26:   6800        ldr r0, [r0, #0]
  28:   1c40        adds    r0, r0, #1
  2a:   4904        ldr r1, [pc, #16]   ; (3c <notmain+0x1c>)
  2c:   6008        str r0, [r1, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   bd80        pop {r7, pc}
  36:   46c0        nop         ; (mov r8, r8)
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

로컬에 스택을 사용하는이 컴파일러는 이제 전역에 램을 사용하며 작성된 .data 또는 .bss를 올바르게 처리하지 않아 작성된 코드가 손상되었습니다.

그리고 우리가 분해에서 볼 수없는 마지막 것.

:1000000000100020150000001B0000001B00000075
:100010001B00000000F004F8FFE7FEE77047000057
:1000200080B500AF04480068401C04490860FFF731
:10003000F5FF002080BDC046040000200000002025
:08004000E0FFFF7F010000005A
:0400480078563412A0
:00000001FF

x를 0x12345678으로 사전 초기화하도록 변경했습니다. 내 링커 스크립트 (이것은 gnu ld 용)는 bob 일에 이것을 가지고 있습니다. 링커에게 최종 장소가 ted 주소 공간에 있기를 원하지만 바이너리 공간에 ted 주소 공간에 저장하면 누군가가 당신을 위해 이동할 것입니다. 그리고 우리는 그것이 일어난 것을 볼 수 있습니다. 이것은 인텔 16 진 형식입니다. 우리는 0x12345678을 볼 수 있습니다

:0400480078563412A0

이진의 플래시 주소 공간에 있습니다.

readelf도 이것을 보여줍니다

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  EXIDX          0x010040 0x00000040 0x00000040 0x00008 0x00008 R   0x4
  LOAD           0x010000 0x00000000 0x00000000 0x00048 0x00048 R E 0x10000
  LOAD           0x020004 0x20000004 0x00000048 0x00004 0x00004 RW  0x10000
  LOAD           0x030000 0x20000000 0x20000000 0x00000 0x00004 RW  0x10000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10

가상 주소가 0x20000004이고 실제가 0x48 인 LOAD 라인


맨 처음에 두 가지 흐림 사진이 있습니다 :
user16307

1.) "1 차 유스 케이스는 ROM / 플래시 및 램에 데이터를 지시하는 것입니다." "RAM에있는 데이터"라고 말하면 프로그램의 과정에서 생성 된 데이터를 의미합니까? 또는 초기화 된 데이터도 포함합니까? ROM에 코드를 업로드하면 코드에 이미 초기화 된 데이터가 있습니다. 예를 들어 다음과 같은 경우 oode에서 : int x = 1; int y = x +1; 위의 코드에는 명령어가 있으며 초기 데이터는 1입니다 (x = 1). 이 데이터는 RAM에 복사되거나 ROM에만 유지됩니다.
user16307

13
아, 이제 스택 교환 응답의 문자 제한을 알고 있습니다!
old_timer

2
U는 초보자에게 그러한 개념을 설명하는 책을 써야합니다. "github에서
훌륭한

1
난 그냥했다. 유용한 것은 아니지만 여전히 마이크로 컨트롤러 용 코드의 예입니다. 그리고 내가 공유하거나 좋거나 나쁘거나 다른 방법을 찾을 수있는 github 링크를 넣었습니다.
old_timer

8

이 답변은 부팅 프로세스에 더 초점을 맞출 것입니다. 먼저, 수정-플래시 쓰기는 MCU (또는 적어도 일부)가 이미 시작된 후에 수행됩니다. 일부 MCU (대개 고급 프로세서)에서는 CPU 자체가 직렬 포트를 작동하고 플래시 레지스터에 쓸 수 있습니다. 따라서 프로그램 작성 및 실행은 다른 프로세스입니다. 프로그램이 이미 플래시로 작성되었다고 가정합니다.

기본 부팅 과정은 다음과 같습니다. 몇 가지 일반적인 변형의 이름을 지정하지만 대부분 간단하게 유지합니다.

  1. 재설정 : 두 가지 기본 유형이 있습니다. 첫 번째는 파워 온 리셋이며 전원 전압이 상승하는 동안 내부적으로 생성됩니다. 두 번째는 외부 핀 토글입니다. 어쨌든, 리셋은 MCU 내의 모든 플립 플롭을 미리 결정된 상태로 강제한다.

  2. 추가 하드웨어 초기화 : CPU 실행을 시작하기 전에 추가 시간 및 / 또는 클럭주기가 필요할 수 있습니다. 예를 들어, 내가 작업하는 TI MCU에는로드되는 내부 구성 스캔 체인이 있습니다.

  3. CPU 부팅 : CPU는 재설정 벡터 라는 특수 주소에서 첫 번째 명령어를 가져옵니다 . 이 주소는 CPU가 설계 될 때 결정됩니다. 거기에서 그것은 단지 정상적인 프로그램 실행입니다.

    CPU는 세 가지 기본 단계를 반복해서 반복합니다.

    • 가져 오기 : 프로그램 카운터 (PC) 레지스터에 저장된 주소에서 명령어 (8, 16 또는 32 비트 값)를 읽은 다음 PC를 증가시킵니다.
    • 디코드 : 이진 명령어를 CPU의 내부 제어 신호에 대한 값 세트로 변환합니다.
    • 실행 : 명령을 수행합니다. 두 개의 레지스터를 추가하거나, 메모리에서 읽거나 쓰거나, 분기 (PC 변경) 또는 기타 무엇이든 추가합니다.

    (실제로는 이것보다 더 복잡합니다. CPU는 일반적으로 파이프 라인 이므로 서로 다른 명령에서 동시에 위의 각 단계를 수행 할 수 있습니다. 위의 각 단계에는 여러 파이프 라인 단계가있을 수 있습니다. 그런 다음 병렬 파이프 라인, 분기 예측이 있습니다. 인텔 CPU가 설계하는 데 10 억 개의 트랜지스터가 필요하게하는 모든 멋진 컴퓨터 아키텍처가 있습니다.)

    가져 오기가 어떻게 작동하는지 궁금 할 것입니다. CPU에는 주소 (out) 및 데이터 (in / out) 신호로 구성된 버스 가 있습니다. 페치를 수행하기 위해 CPU는 주소 라인을 프로그램 카운터의 값으로 설정 한 다음 버스를 통해 클럭을 보냅니다. 메모리를 활성화하기 위해 주소가 디코딩됩니다. 메모리는 클럭과 주소를 수신하고 해당 주소의 값을 데이터 라인에 넣습니다. CPU는이 값을받습니다. 주소는 PC가 아닌 명령어 또는 범용 레지스터의 값에서 나온다는 점을 제외하면 데이터 읽기 및 쓰기가 비슷합니다.

    von Neumann 아키텍처를 사용하는 CPU에는 명령과 데이터 모두에 사용되는 단일 버스가 있습니다. 하버드 아키텍처의 CPU에는 명령 용 버스와 데이터 용 버스가 하나씩 있습니다. 실제 MCU에서이 두 버스는 동일한 메모리에 연결될 수 있으므로 걱정할 필요가없는 경우가 많습니다 (항상 그런 것은 아님).

    부팅 과정으로 돌아갑니다. 재설정 후 PC에는 재설정 벡터 라는 시작 값이로드됩니다 . 하드웨어에 내장되거나 (ARM Cortex-M CPU에서) 자동으로 메모리에서 읽을 수 있습니다. CPU는 리셋 벡터에서 명령어를 가져오고 위 단계를 반복합니다. 이 시점에서 CPU가 정상적으로 실행되고 있습니다.

  4. 부트 로더 : MCU의 나머지 부분을 작동시키기 위해 수행해야 할 저수준 설정이 종종 있습니다. 여기에는 RAM 지우기 및 아날로그 구성 요소의 제조 트림 설정로드와 같은 것들이 포함될 수 있습니다. 직렬 포트 또는 외부 메모리와 같은 외부 소스에서 코드를로드하는 옵션이있을 수도 있습니다. MCU에는 이러한 작업을 수행하는 작은 프로그램 이 포함 된 부팅 ROM 이 포함될 수 있습니다 . 이 경우 CPU 재설정 벡터는 부팅 ROM의 주소 공간을 가리 킵니다. 이것은 기본적으로 정상적인 코드이며 제조업체가 제공 한 것이므로 직접 작성할 필요가 없습니다. :-) PC에서 BIOS는 부팅 ROM과 같습니다.

  5. C 환경 설정 : C는 스택 (함수 호출 중에 상태를 저장하기위한 RAM 영역)과 전역 변수에 대해 초기화 된 메모리 위치 를 가질 것으로 예상 합니다. 이들은 Dwelch가 이야기하고있는 .stack, .data 및 .bss 섹션입니다. 초기화 된 전역 변수는이 단계에서 초기화 값이 플래시에서 RAM으로 복사됩니다. 초기화되지 않은 전역 변수에는 서로 가까운 RAM 주소가 있으므로 전체 메모리 블록을 매우 쉽게 0으로 초기화 할 수 있습니다. 스택을 초기화 할 필요는 없지만 (할 수는 있지만) 실제로 필요한 것은 CPU의 스택 포인터 레지스터를 설정하여 RAM의 할당 된 영역을 가리 키는 것입니다.

  6. 주요 기능 : C 환경이 설정되면 C 로더는 main () 함수를 호출합니다. 응용 프로그램 코드가 정상적으로 시작되는 곳입니다. 원하는 경우 표준 라이브러리를 생략하고 C 환경 설정을 건너 뛰고 main ()을 호출하는 고유 코드를 작성할 수 있습니다. 일부 MCU를 사용하면 자체 부트 로더를 작성할 수 있으며 저수준 설정을 모두 직접 수행 할 수 있습니다.

기타 사항 : 많은 MCU에서 성능 향상을 위해 RAM에서 코드를 실행할 수 있습니다. 이것은 일반적으로 링커 구성에서 설정됩니다. 링커는 모든 함수에 두 개의 주소 , 즉 코드가 처음 저장되는 위치 (일반적으로 플래시)와 실행 주소 (기능을 실행하기 위해 PC에로드되는 주소)를 지정합니다 (플래시 또는 RAM). RAM에서 코드를 실행하려면 CPU가 플래시의로드 주소에서 RAM의 실행 주소로 함수 코드를 복사 한 다음 실행 주소에서 함수를 호출하도록 코드를 작성합니다. 링커는이를 돕기 위해 전역 변수를 정의 할 수 있습니다. 그러나 MCU에서 RAM으로 코드를 실행하는 것은 선택 사항입니다. 실제로 고성능이 필요하거나 플래시를 다시 작성하려는 경우에만 수행합니다.


1

귀하의 요약은 Von Neumann 아키텍처에 거의 맞습니다 . 초기 코드는 일반적으로 부트 로더를 통해 RAM에로드되지만 일반적으로이 용어가 일반적으로 사용하는 소프트웨어 부트 로더는 아닙니다. 이것은 일반적으로 '실리콘에 구운'행동입니다. 이 아키텍쳐에서의 코드 실행은 종종 프로세서가 코드를 실행하는 시간을 최대화하고 코드가 RAM에로드되기를 기다리지 않는 방식으로 ROM으로부터의 명령 캐싱을 포함한다. MSP430이이 아키텍처의 예라는 것을 읽었습니다.

A의 하바드 아키텍쳐 데이터 메모리 (RAM)를 별도의 버스를 통해 접속되는 동안 디바이스 명령어는 ROM에서 직접 실행된다. 이 아키텍처에서 코드는 단순히 리셋 벡터로부터 실행을 시작합니다. PIC24 및 dsPIC33이이 아키텍처의 예입니다.

이러한 프로세스를 시작하는 비트의 실제 플립 핑은 장치마다 다를 수 있으며 디버거, JTAG, 독점적 방법 등이 포함될 수 있습니다.


그러나 당신은 몇 가지 포인트를 빨리 건너 뛰고 있습니다. 슬로우 모션을 봅시다. 이진 코드 "first"가 ROM에 기록되었다고 가정하겠습니다. Ok .. 그 후에 "데이터 메모리에 액세스했습니다."라고 쓴다. 그러나 "RAM에 대한"데이터는 처음에 어디에서 오는가? ROM에서 다시 나오나요? 그렇다면 부트 로더는 ROM의 어느 부분이 처음에 RAM에 쓰여질 지 어떻게 알 수 있습니까?
user16307

당신은 맞습니다, 나는 많은 걸 뛰어 넘었습니다. 다른 사람들은 더 나은 답변을 가지고 있습니다. 찾고 계신 것을 기쁘게 생각합니다.
약간
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.