Configuração e inicialização de microcontroladores: Um estudo utilizando ARM Cortex-M0+

Por trás da função principal de um programa – main() – existe um conjunto de arquivos e configurações necessários para torná-la o ponto de entrada de um programa. Neste artigo serão apresentados os passos necessários para criar os arquivos de configuração e inicialização de microcontroladores, criando um setup básico para inicializar um microcontrolador (MKL25Z128) com arquitetura ARM® Cortex™-M0+. Para tal, as ferramentas disponibilizadas no GNU ARM Embedded Toolchain serão utilizadas.

Neste artigo a inicialização do sistema é definida como sendo a configuração básica para que o sistema possa operar em um estado conhecido. Tal condição é iniciada quando o microcontrolador é energizado, entrando no estado de Reset. Nesta condição, o processador inicia a execução do programa a partir de um endereço especificado numa região de memória chamada de Reset Vector. Considerando isso, cabe apresentar uma breve introdução à arquitetura do ARM® Cortex™-M0+.

Breve apresentação da Arquitetura

O ARM® Cortex™-M0+ faz parte da série de processadores Cortex-M aplicado em microcontroladores de 32 bits. Para este artigo é importante destacar as seguintes características: vetores de Interrupção e mapa de memória. Nesse sentido, as diferenças entre a linha M0 e M0+ não interferem nos temas destacados aqui. Para outras informações consulte o artigo escrito pelo Thiago e a documentação disponibilizada pela ARM.

A arquitetura ARM® Cortex™-M0+ apresenta vetores de interrupção com a disposição indicada na Figura 1. Os primeiros 16 endereços deste vetor possuem funções específicas, sendo o restante dependente do microcontrolador. A primeira posição é destinada para indicar o endereço da região de stack. A segunda, para indicar o ponto de entrada da aplicação.

Vetores de Interrupção para Inicialização de microcontroladores ARM
Figura 1: Vetores de Interrupção [1].

O mapa de memória do processador separa o espaço de endereçamento em múltiplas regiões, sendo representadas por diferentes tecnologias de memória e atributos. O mapa de memória padrão da arquitetura é mostrado na Figura 2. O espaço de endereçamento total é de 4 GB.

Mapa de Memória para Inicialização de microcontroladores ARM
Figura 2: Mapa de Memória [1].

Os endereços e descrições destas regiões são apresentados na Figura 3. Cabe ressaltar que as regiões CODE, SRAM e external RAM possibilitam armazenar programas.

Características das regiões de memória
Figura 3: Características das regiões de memória [1].

De Olho no Datasheet

Embora as regiões de endereçamento estejam delimitadas, cabe a a cada dispositivo indicar os elementos internos de cada região. Isto é, as tecnologias de memória e as funções de cada região. Por exemplo, analisando o datasheet do microcontrolador MKL25Z128 encontram-se as regiões indicadas na Figura 4.

Mapa de memória KL25Z
Figura 4: Mapa de memória KL25Z [2].

A memória flash tem capacidade de 128 KB e a memória RAM 16 KB. Embora a memória flash seja posicionada no endereço 0, os endereços iniciais são destinados aos vetores de interrupção. Além disso, a partir do endereço 0x0_0400 fica localizada uma região denominada Flash Configuration Fields, de 16 bytes.

Arquivo de inicialização

Um arquivo de inicialização básico consiste na declaração do vetor de interrupções e da definição do ponto de entrada (entry point), isto é, da primeira instrução executada.

    /*Diretiva utilizada para direcionar o código abaixo para a região de memória especificada*/
    .section .isr_vector, "a"

    /*Diretiva para indicar o alinhamento requerido*/
    .align 4

    /*Torna o simbolo visível para o linker*/
    .globl __isr_vector
__isr_vector:
    /*Long é um valor int*/
    .long   __StackTop     /* Top of Stack */
    .long   Reset_Handler  /* Reset Handler */
    .long   0              /* NMI Handler*/
    .long   0              /* Hard Fault Handler*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* SVCall Handler*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* PendSV Handler*/
    .long   0              /* SysTick Handler*/
    /*necessário adicionar os vetores correspondentes aos periféricos do fabricante*/
    /*...*/

    /*Diretiva utilizada para determinar o tamanho associado ao símbolo
    	.size expression
    	Obs: o ponto final é utilizado para obter o endereço atual

    	Determina o tamanho da região associada ao __isr_vector*/
    .size    __isr_vector, . - __isr_vector

Neste código, o ponto de entrada da aplicação é o Reset_Handler.

/* Reset Handler */
/*Necessário para criar um código com instruções ARM e thumb*/
    .thumb_func
    .align 2
    .globl   Reset_Handler
    /* Tipo do símbolo Reset_Handler*/
    .type    Reset_Handler, %function
Reset_Handler:	  /* código da rotina Reset_Handler*/
    cpsie   i     /* habilita interrupções */
    bl    main    /* chama função main*/

De modo geral, o arquivo de inicialização pode conter diversos estágios antes de realizar a chamada da função main. Por exemplo, realizar a configuração de alguns periféricos e do sistema de clock. O Arquivo completo é mostrado abaixo.

    /*Diretiva utilizada para direcionar o código abaixo para a região de memória especificada*/
    .section .isr_vector, "a"

    /*Diretiva para indicar o alinhamento requerido*/
    .align 4

    /*Torna o simbolo visível para o linker*/
    .globl __isr_vector
__isr_vector:
    /*Long é um valor int*/
    .long   __StackTop     /* Top of Stack */
    .long   Reset_Handler  /* Reset Handler */
    .long   0              /* NMI Handler*/
    .long   0              /* Hard Fault Handler*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* SVCall Handler*/
    .long   0              /* Reserved*/
    .long   0              /* Reserved*/
    .long   0              /* PendSV Handler*/
    .long   0              /* SysTick Handler*/
    /*necessário adicionar os vetores correspondentes aos periféricos do fabricante*/
    /*...*/

    /*Diretiva utilizada para determinar o tamanho associado ao símbolo
    	.size expression
    	Obs: o ponto final é utilizado para obter o endereço atual

    	Determina o tamanho da região associada ao __isr_vector*/
    .size    __isr_vector, . - __isr_vector

    .text
    .thumb

    /* Reset Handler */
    /*Necessário para criar um código com instruções ARM e thumb*/
    .thumb_func
    .align 2
    .globl   Reset_Handler
    /* Tipo do símbolo Reset_Handler*/
    .type    Reset_Handler, %function
Reset_Handler:	  /* código da rotina Reset_Handler*/
    cpsid   i     /* desabilita interrupções */
    bl SystemInit /* chama função SystemInit*/
    cpsie   i     /* habilita interrupções */
    bl    main    /* chama função main*/
    .pool

    /*Determina o tamanho da região associada ao Reset_Handler*/
    .size Reset_Handler, . - Reset_Handler

    /*Diretiva utilizada para indicar o fim do arquivo assembly*/
    .end

Na rotina Reset_Handler realizou-se a chamada da função SystemInit disponibilizada no SDK do fabricante. No entanto, esses procedimentos ficam a critério do desenvolvedor, pois os procedimentos podem ser realizados em outro ponto e de outra maneira. 

Link-edição

As informações sobre as regiões de memória destacadas anteriormente são utilizadas durante o processo de compilação em um estágio chamado link-edição. Especificamente, este processo é caracterizado por distribuir os objetos gerados na etapa de compilação nas regiões de memória indicadas no arquivo. Assim, deve-se especificar a localização da seção de código (.text), dados (.data), pilha (.stack), entre outras.

Antes disso, é necessário definir o ponto de entrada no vetor de interrupções usando a diretiva ENTRY.

ENTRY(endereço)

Em seguida são definidos os tipos de memória presentes no microcontrolador e o respectivo espaço de endereçamento. Isso é feito a partir da diretiva MEMORY.

MEMORY{
 nome da região de memória (permissão): ORIGIN = endereço inicial, LENGTH = tamanho em bytes
 ...
}

Após esta definição é necessário especificar ao linker quais seções serão combinadas nas regiões de memória indicadas. Para tal, utiliza-se a diretiva SECTION.

SECTIONS
{
 
  .nome da seção :
  {
    . = ALIGN(expressão);
    definição das seções
    . = ALIGN(expressão);
  } > nome da região de memória

}

A definição das regiões de memória para este projeto são apresentadas abaixo na Figura 5 e declaradas no script abaixo.

Regiões de memória indicadas no linker.
Figura 5: Regiões de memória indicadas no linker.
/* Ponto de entrada do programa */
ENTRY(Reset_Handler)

/* Tamanho da região stack*/
STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x0400;

/*Regiões de memória*/
MEMORY
{
  m_interrupts          (RX)  : ORIGIN = 0x00000000, LENGTH = 0x00000100
  m_text                (RX)  : ORIGIN = 0x00000410, LENGTH = 0x0001FBF0
  m_data                (RW)  : ORIGIN = 0x1FFFF000, LENGTH = 0x00004000
}

SECTIONS
{
  /*Vetores de interrupção*/
  .interrupts :
  {
    __VECTOR_TABLE = .;
    . = ALIGN(4);
    KEEP(*(.isr_vector))     /* Startup code */
    . = ALIGN(4);
  } > m_interrupts

  /* Programa (.text) e dados somente leitura (.rodata) */
  .text :
  {
    . = ALIGN(4);
    *(.text)                 
    *(.text*)                
    *(.rodata)               
    *(.rodata*)              
    . = ALIGN(4);
  } > m_text

  /*Seção de dados na memória ram*/
  .data :
  {
    . = ALIGN(4);
    *(.data)             
    *(.data*)            
    . = ALIGN(4);
  } > m_data


  /*Stack na memória ram, alinhamento definido conforme indicado no documento da arquitetura M0*/
  .stack :
  {
    . = ALIGN(8);
    . += STACK_SIZE;
  } > m_data

  /* Range da região stack */
  __StackTop   = ORIGIN(m_data) + LENGTH(m_data);
  __StackLimit = __StackTop - STACK_SIZE;
  PROVIDE(__stack = __StackTop);
}

Compilando o programa

Foi dito que o processo de compilação de um programa é realizado por etapas, sendo ao fim realizado o processo de link-edição. A seguir são apresentados os comandos para compilação e montagem dos objetos.

Para programas em C o seguinte procedimento é executado. O parâmetro ‘-c’ indica que o linker não será utilizado. O resultado disso é um arquivo objeto.

arm-none-eabi-gcc -c -o programa.c programa.o

Para programas em Assembly o seguinte procedimento é executado. O parâmetro ‘-c’ indica que o linker não será utilizado. O resultado disso é um arquivo objeto.

arm-none-eabi-as -c -o programa.S programa.o

Por fim, utiliza-se o utilitário ld para realizar a link-edição de todos os módulos e gerar o arquivo final.

arm-none-eabi-ld -T"LinkerFile.ld" -o Programa.elf Modulo1.o ModuloN.o

O resultado disso pode ser visto com o utilitário objdump.

Automação do processo com Makefiles

Antes de iniciar este tópico, cabe lembrar que um tutorial completo sobre Makefiles é apresentado pelo Lincoln Uehara neste artigo.

Este projeto foi criado na IDE Kinetis e possui a seguinte organização:

Estrutura do Projeto.
Figura 6: Estrutura do Projeto.

Os arquivos de include foram obtidos do pacote de software CMSIS. Os arquivos do microcontrolador MKL25Z4 foram obtidos no SDK do Kinetis. No entanto, os arquivos startup_MKL25Z4.S e o linker foram alterados para atender aos abjetivos deste artigo.

A automatização do processo de compilação é mostrada abaixo em que o caminho para o compilador é definido a partir de uma variável de ambiente. O toolchain (GNU ARM Embedded Toolchain) pode ser obtido neste link.

#ferramentas utilizadas
COMPILER = arm-none-eabi-gcc
ASSEMBLER = arm-none-eabi-as
LINKER = arm-none-eabi-ld

##############################
#	Arquivos C
##############################

#Fontes
C_SRCS = \
		../Sources/main.c \
		../Project_Settings/Startup_Code/system_MKL25Z4.c

#Objetos
C_OBJS = \
		./Sources/main.o\
		./Project_Settings/Startup_Code/system_MKL25Z4.o

#Lista de dependências
C_DEPS = \
		./Sources/main.d \
		./Project_Settings/Startup_Code/system_MKL25Z4.d
		
#Flags de compilação
C_FLAGS = -mcpu=cortex-m0plus -march=armv6-m -mthumb -O0 -fmessage-length=0 -fsigned-char -ffunction-sections -fdata-sections  -g3

##############################
#	Arquivos ASM
##############################

#Fontes
S_SRCS = \
		../Project_Settings/Startup_Code/startup_MKL25Z4.S

#Objetos
S_OBJS = \
		./Project_Settings/Startup_Code/startup_MKL25Z4.o

#Lista de dependências
S_DEPS = \
		./Project_Settings/Startup_Code/startup_MKL25Z4.d

#Flags de montagem
S_FLAGS = -mcpu=cortex-m0plus -march=armv6-m -g3


##############################
#	Arquivos de cabeçalho
##############################
I_DIR = \
		-I"../Includes" \
		-I"../Sources"

##############################
#	Parâmetros do linker
##############################

LINKER_OPT = \
			-T"../Project_Settings/Linker_Files/MKL25Z128xxx4_flash.ld"\
			-L"indicar o diretório /lib do compilador GNU Tools ARM Embedded/7 2017-q4-major/lib/gcc/arm-none-eabi/7.2.1/"

#bibliotecas para compilação
LIBS = -lgcc
#objetos que serão gerados
OBJS = $(C_OBJS) $(S_OBJS)
#listas de dependências
DEPS = $(C_DEPS) $(S_DEPS)

##############################
#	Make
##############################
all: Projeto.elf

#compila os arquivos armazenados no diretório Sources:
# * as flags de compilação são indicadas por C_FLAGS.
# * é gerado arquivo com a lista de dependências.
Sources/%.o: ../Sources/%.c
	@echo 'Compilando o arquivo $@'
	$(COMPILER) $(C_FLAGS) $(I_DIR) -std=c99 -MMD -MP -MF"$(@:%.o=%.d)" -MT"$(@:%.o=%.o)" -c -o "$@" "$<"

#compila os arquivos armazenados no diretório Project_Settings/Startup_Code:
# * as flags de compilação são indicadas por C_FLAGS.
# * é gerado arquivo com a lista de dependências.
Project_Settings/Startup_Code/%.o: ../Project_Settings/Startup_Code/%.c
	@echo 'Compilando o arquivo $@'
	$(COMPILER) $(C_FLAGS) $(I_DIR) -std=c99 -MMD -MP -MF"$(@:%.o=%.d)" -MT"$(@:%.o=%.o)" -c -o "$@" "$<"

#monta os arquivos ASM armazenados no diretório Project_Settings/Startup_Code:
# * as flags de compilação são indicadas por S_FLAGS.
# * é gerado arquivo com a lista de dependências.
Project_Settings/Startup_Code/%.o: ../Project_Settings/Startup_Code/%.S
	@echo 'Montando o arquivo $@'
	$(ASSEMBLER) $(S_FLAGS) -o "$@" "$<"

#faz o processo de linkedição e gera os arquivos .elf e .bin
Projeto.elf: $(OBJS)
	@echo 'Criando arquivo ELF'
	$(LINKER) $(LINKER_OPT) $(OBJS) $(LIBS) -o "$@"


##############################
#	Clean
##############################
clean:
	-$(RM) $(OBJS) $(DEPS) Projeto.elf

O procedimento executado também pode ser visto na Figura 7. Cada arquivo fonte passa por um procedimento, gerando um arquivo objeto. Por fim, os arquivos objetos são transformados em uma imagem final.

Procedimento para gerar o programa.
Figura 7: Procedimento para gerar o programa.

Conclusão

Neste artigo foram apresentadas as etapas para construção de uma aplicação simples, considerando a construção do arquivo utilizado pelo linker e a definição de uma rotina de inicialização. Com essas ferramentas é possível alterar outras configurações de memória e inicialização do dispositivo. Outra aplicação pode ser a criação de sistemas de bootloader como mostrado no artigo do Marcelo Jo.

Saiba mais

ARM Cortex M0+

Introdução ao Makefile

GNU ARM Cross-toolchain – Compilação e OpenOCD

GNU Cross-toolchain – Processo de build

Referências

[1] Cortex-M0+ Technical Reference Manual;

[2] KL25 Sub-Family Reference Manual.

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
2 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Marcelo Silveira
Marcelo Silveira
22/06/2020 18:37

Parabéns. Esse artigo foi muito esclarecedor.

Xavier Albornoz
Xavier Albornoz
10/10/2018 21:01

Parabéns Fernando !!! Vc fez um “tutorial” exatamente como um dia planejo fazer para o mundo KEIL … voltado para os NXPs da vida. PA-RA-BÉNS !!!

Home » Software » Configuração e inicialização de microcontroladores: Um estudo utilizando ARM Cortex-M0+

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: