Este é o segundo artigo da série onde aprenderemos como desenvolver projetos com STM32 sem depender de IDEs gráficas ou ecossistemas como o STM32Cube. Neste artigo, você aprenderá a montar a estrutura de diretórios manualmente e a escrever o código para o seu primeiro exemplo: um programa blink.
Montando estrutura de diretórios
- Em seu sistema, busque pelo diretório do linux: “\\wsl.localhost\Ubuntu\home\user”.
- Crie uma pasta para o projeto, aqui vou chamá-la de hello-world.
- Dentro desta pasta crie as seguintes subpastas: build, inc, src, stm
- Entre em https://github.com/STMicroelectronics/STM32CubeC0/tree/main, faça o clone do repositório e inicie os submodulos.
git clone git@github.com:STMicroelectronics/STM32CubeC0.gitgit submodule update --init --recursive - Acesse o diretório \STM32CubeC0\Drivers\CMSIS\Device\ST\STM32C0xx\Source\Templates e copie o arquivo system_stm32c0xx.c (responsável pela inicialização e configuração do clock) para a subpasta src criada dentro da pasta do seu projeto.
- Em seguida, abra \STM32CubeC0\Drivers\CMSIS\Device\ST\STM32C0xx\Source\Templates\gcc e copie o arquivo startup_stm32c011xx.s (responsável por definir a vector table) para a subpasta src riada dentro da pasta do seu projeto.
- Depois, localize algum projeto criado no STM32CubeIDE para a Franzininho C0 e copie o arquivo STM32C011F6PX_FLASH.ld (script de ligação responsável por gerar o binário, especificar a localização da vector table e configurar os endereços relativos para chamadas de função e acesso a dados) para a pasta raiz do seu projeto. Caso você não possua esse arquivo, crie um novo arquivo com o nome STM32C011F6PX_FLASH.ld e insira o seguinte código:
/*
******************************************************************************
**
** @file : LinkerScript.ld
**
** @author : Auto-generated by STM32CubeIDE
**
** @brief : Linker script for STM32C011F6Px Device from STM32C0 series
** 32KBytes FLASH
** 6KBytes RAM
**
** Set heap size, stack size and stack location according
** to application requirements.
**
** Set memory bank area and size if external memory is used
**
** Target : STMicroelectronics STM32
**
** Distribution: The file is distributed as is, without any warranty
** of any kind.
**
******************************************************************************
** @attention
**
** Copyright (c) 2024 STMicroelectronics.
** All rights reserved.
**
** This software is licensed under terms that can be found in the LICENSE file
** in the root directory of this software component.
** If no LICENSE file comes with this software, it is provided AS-IS.
**
******************************************************************************
*/
/* Entry Point */
ENTRY(Reset_Handler)
/* Highest address of the user mode stack */
_estack = ORIGIN(RAM) + LENGTH(RAM); /* end of "RAM" Ram type memory */
_Min_Heap_Size = 0x200; /* required amount of heap */
_Min_Stack_Size = 0x400; /* required amount of stack */
/* Memories definition */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 6K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 32K
}
/* Sections */
SECTIONS
{
/* The startup code into "FLASH" Rom type memory */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH
/* The program code and other data into "FLASH" Rom type memory */
.text :
{
. = ALIGN(4);
*(.text) /* .text sections (code) */
*(.text*) /* .text* sections (code) */
*(.glue_7) /* glue arm to thumb code */
*(.glue_7t) /* glue thumb to arm code */
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .; /* define a global symbols at end of code */
} >FLASH
/* Constant data into "FLASH" Rom type memory */
.rodata :
{
. = ALIGN(4);
*(.rodata) /* .rodata sections (constants, strings, etc.) */
*(.rodata*) /* .rodata* sections (constants, strings, etc.) */
. = ALIGN(4);
} >FLASH
.ARM.extab (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */
{
. = ALIGN(4);
*(.ARM.extab* .gnu.linkonce.armextab.*)
. = ALIGN(4);
} >FLASH
.ARM (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */
{
. = ALIGN(4);
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
. = ALIGN(4);
} >FLASH
.preinit_array (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */
{
. = ALIGN(4);
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array*))
PROVIDE_HIDDEN (__preinit_array_end = .);
. = ALIGN(4);
} >FLASH
.init_array (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */
{
. = ALIGN(4);
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
. = ALIGN(4);
} >FLASH
.fini_array (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */
{
. = ALIGN(4);
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT(.fini_array.*)))
KEEP (*(.fini_array*))
PROVIDE_HIDDEN (__fini_array_end = .);
. = ALIGN(4);
} >FLASH
/* Used by the startup to initialize data */
_sidata = LOADADDR(.data);
/* Initialized data sections into "RAM" Ram type memory */
.data :
{
. = ALIGN(4);
_sdata = .; /* create a global symbol at data start */
*(.data) /* .data sections */
*(.data*) /* .data* sections */
*(.RamFunc) /* .RamFunc sections */
*(.RamFunc*) /* .RamFunc* sections */
. = ALIGN(4);
_edata = .; /* define a global symbol at data end */
} >RAM AT> FLASH
/* Uninitialized data section into "RAM" Ram type memory */
. = ALIGN(4);
.bss :
{
/* This is used by the startup in order to initialize the .bss section */
_sbss = .; /* define a global symbol at bss start */
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .; /* define a global symbol at bss end */
__bss_end__ = _ebss;
} >RAM
/* User_heap_stack section, used to check that there is enough "RAM" Ram type memory left */
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
/* Remove information from the compiler libraries */
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
.ARM.attributes 0 : { *(.ARM.attributes) }
}
- Por último, acesse \STM32CubeC0\Drivers\ e copie as pastas CMIS e da HAL_C0 para a sua subpasta stm. Também copie o arquivo “stm32c0xx_hal_conf_template.h” para sua pasta include.
É importante destacar que o diretório e o template referentes à HAL não serão utilizados neste projeto, portanto, sua inclusão é opcional. No entanto, estou apresentando essa etapa para que você tenha a estrutura preparada caso decida programar utilizando os drivers HAL no futuro, sem a necessidade de abrir o STM32CubeIDE.
Primeiro Programa: Blink
Entre na pasta do seu projeto pelo terminal Ubuntu e digite code . para abrir no VSCode.
Em src crie um arquivo main.c
A Franzininho C0 possui um LED conectado ao pino PB6, e nosso objetivo é fazê-lo piscar.
Os passos que devemos seguir para implementação do código são:
- Habilitar clock no GPIO
- Configurar o GPIO como saída na porta desejada
- Configurar o GPIO no modo pushpull
- Configurar a velocidade no GPIO
- Escrever 1 no registrador de saída para ligar o LED
- Escrever 0 no registrador de saída para ligar o LED
No entanto, antes de começar a codificação, é preciso compreender alguns conceitos sobre registradores. Para isso, consulte o manual de referência do STM32C0:: https://www.st.com/resource/en/reference_manual/rm0490-stm32c0-series-advanced-armbased-32bit-mcus-stmicroelectronics.pdf
Habilitar o Clock no GPIO
No manual de referência encontre o RCC_IOPENR. Esse registrador é responsável por habilitar o clock do GPIO.
Configurações GPIO
Buscando por General-purpose I/Os (GPIO) no manual de referência teremos a descrição sobre os GPIOs.Cada porta de E/S de uso geral possui quatro registradores de configuração de 32 bits (GPIOx_MODER, GPIOx_OTYPER, GPIOx_OSPEEDR e GPIOx_PUPDR), dois registradores de dados de 32 bits (GPIOx_IDR e GPIOx_ODR) e um registrador de configuração de bits de set/reset de 32 bits (GPIOx_BSRR). Além disso, todas as GPIOs possuem um registrador de bloqueio de 32 bits (GPIOx_LCKR) e dois registradores de seleção de função alternativa de 32 bits (GPIOx_AFRH e GPIOx_AFRL).
Estrutura básica GPIOs
Tabela de configuração de bits de porta
Detalhamento Registradores
- GPIOx_MODER define o modo de operação do GPIO. Ele tem 2 bits dedicados para cada pino.
- 00: Entrada digital
- 01: Saída digital
- 10: Função alternativa (para periféricos como SPI, UART, etc.)
- 11: Modo analógico (para ADC ou DAC).
No nosso exemplo, vamos usar o pino PB6, como cada pino possui 2 bits dedicados, o 6 estará na posição 12 e 13 e para o pino atuar como saída será necessário que neles fique 0 na posição 13 e 1 na posição 12.
- GPIOx_OTYPER configura o GPIO em output push-pull (reset state) quando usado o bit 0 e quando usado bit 1 configura como Output open-drain.
No nosso exemplo, na posição 6 deveremos colocar o bit 0.
- GPIOx_OSPEEDR define a velocidade no registrador. É reservado 2 bits para essa configuração.
- 00: Velocidade muito baixa
- 01: Baixa velocidade
- 10: Alta velocidade
- 11: Velocidade muito alta
Novamente, em nosso exemplo vamos utilizar o PB6, portanto nas posições 12 e 13 ajustaremos com os bits 00 para configurar em velocidade muito baixa.
- GPIOx_ODR é utilizado para manipular saídas. Esse registrador é dividido em bits individuais, com cada bit correspondendo a um pino da porta GPIO, em que se bit definido como:
- 0 – pino é colocado em nível lógico baixo
- 1 – pino é colocado em nível lógico alto
Em nosso exemplo, basicamente vamos deslocar para o pino na posição 6 e setar 1 para nível lógico alto.
Código
Por fim, agora que entendemos os conceitos básicos sobre registradores, podemos avançar para a implementação do código com auxílio das macros da CMSIS. No arquivo main.c adicione o seguinte código:
#include "stm32c011xx.h"
void delay(unsigned long delay) {
for (unsigned long i = 0; i < delay * 1000; i++) {
__asm("nop");
}
}
void gpio_init(void) {
// Habilitar o clock para o GPIOB
RCC->IOPENR |= RCC_IOPENR_GPIOBEN;
// Configurar PB6 como saída
GPIOB->MODER &= ~(3U << (6 * 2)); // Limpa os bits correspondentes ao pino 6
GPIOB->MODER |= (1U << (6 * 2)); // Configura PB6 como saída (01)
// Configurar PB6 como push-pull
GPIOB->OTYPER &= ~(1U << 6); // 0 = Saída push-pull
// Configurar PB6 para baixa velocidade
GPIOB->OSPEEDR &= ~(3U << (6 * 2)); // 00 = Baixa velocidade
}
void gpio_set_high(void) {
// Configurar PB6 em nível lógico alto usando ODR
GPIOB->ODR |= (1U << 6);
}
void gpio_set_low(void) {
// Configurar PB6 em nível lógico baixo usando ODR
GPIOB->ODR &= ~(1U << 6);
}
int main(void) {
gpio_init();
while (1) {
gpio_set_high(); // PB6 em nível alto
delay(1000); // Aguarda
gpio_set_low(); // PB6 em nível baixo
delay(1000); // Aguarda
}
return 0;
}
Caso você queira visualizar as macros declaradas, acesse o diretório stm/CMSIS/Device/ST/STM32C0xx/Include/stm32c011xx.h.
Conclusão
Neste segundo artigo, você aprendeu a montar a estrutura de diretórios e a escrever seu primeiro código utilizando as macros da CMSIS e manipulando registradores. No próximo artigo, vamos gerar o arquivo .elf e gravá-lo na placa. Fique atento aqui no Portal!





