Tutoriais

Introdução ao Rust Embarcado com STM32F103

Eletrogate 4 de julho de 2023

Introdução

Rust é uma linguagem focada em segurança e desempenho que vem crescendo muito nos últimos anos, principalmente nas comunidades de sistemas embarcados, como uma alternativa promissora a C++. Nesse post, vamos dar uma introdução sobre ela, criando um Blink para STM32 bluepill e mostrando as semelhanças e diferenças entre ela e a C++.


Necessidades de um Sistema Embarcado

Antes de falar de Rust, temos que entender quais as necessidades de um sistema embarcado e como isso se relaciona com o desenvolvimento. Existem alguns tópicos principais que um projeto de sistema embarcado precisa resolver:

Consumo de energia:

Muitos sistemas funcionam com baterias e/ou operam por 24 h ininterruptas. Um consumo de energia maior pode aumentar bastante os custos de manter um sistema desses. Uma linguagem com alta capacidade de controlar os recursos do hardware é essencial nesse caso.

Tempo de resposta:

Precisão de tempo é algo indispensável para a indústria. Atrasos de microssegundos podem gerar problemas enormes para automóveis, aviões, satélites e até linha de produção. Por isso, a linguagem usada nesses sistemas deve ter tempo de execução rápido e consistente.

Memoria:

Memoria limitada (memoria física e/ou RAM) é muito comum em microcontroladores. Adicionar mais memoria torna o produto mais caro, o que não é viável para controladores de baixo custo. Por isso, a linguagem deve ter um “footprint” (pegada) pequeno. “footprint” é a quantidade de recursos necessários para rodar um programa em determinada linguagem. Por exemplo, linguagens como Java ou Python usam um recurso chamado GC (Garbage collection) para gerenciar a memória automaticamente. Mas, para ter esse recurso, eles precisam reservar mais memoria e processamento e, por isso, têm um “footprint” maior.

Confiabilidade:

Um sistema tem que executar sua tarefa de forma consistente, independentemente do ambiente em que foi colocado. Isso é resultado direto da relação entre o hardware e o software. A linguagem influencia diretamente essa etapa.

Resistência a Falhas:

Mesmo um produto robusto e confiável pode passar por situações inesperadas. Portanto, é crucial que haja protocolos e procedimentos bem estabelecidos para lidar com emergências e garantir que os sistemas críticos permaneçam funcionando adequadamente, mesmo diante de falhas inesperadas.


Uso de C e C++

C e C++ são linguagens excelentes para sistemas embarcados: são leves, rápidas e com um ótimo controle de baixo nível. Mas essas linguagens têm problemas que podem dificultar a sua vida.

“C faz com que seja muito fácil atirar nos próprios pés. C++ faz com que isso se torne mais difícil, mas quando você consegue, destrói toda a perna.” – Bjarne Stroustrupcriador da C++

Essa frase, dita pelo criador da C++, faz referência a um dos maiores problemas de C/C++: o gerenciamento de memória. A linguagem C é feita para ser simples e ter poucas abstrações, o que faz ela ser eficiente. Mas não oferece nenhuma ajuda ao desenvolvedor. Em códigos grandes e complexos, fica muito fácil alguém cometer um erro que vai passar de forma despercebida.

(Curiosidade: os vetores de ataques mais utilizados por hackers para ter acesso a um dispositivo são gerados por problemas no gerenciamento de memória, uma das mais famosas é o buffer overflow)

C++ adiciona um recurso chamado “abstrações de custo zero” para ajudar no desenvolvimento do software sem perda de desempenho. Essas abstrações também são utilizadas na técnica de gerenciamento de memória chamado RAII, que permite gerenciar a memória automaticamente. Porém, C++ também tem problemas:

Undefined behavior (Comportamento indefinido):

Comportamento indefinido é um recurso para ajudar na flexibilidade da linguagem, permitindo que cada implementação decida qual a melhor forma de realizar certas tarefas. Mas isso gera um problema: é muito difícil prever como o código vai se comportar, o que pode gerar bugs difíceis de analisar e resolver. Essa inconsistência pode atrasar bastante o desenvolvimento de um produto.

(isso também existe em C, mas é mais problemático em linguagens maiores, como o C++)

Difícil atualização:

Na última década, houve uma necessidade muito grande de atualizar C++ para atender as novas demandas do mercado. Mas C++ é uma linguagem que existe a bastante tempo, vários sistemas dependem dele e sua modernização precisa ser compatível com códigos antigos, o que torna todo o processo mais lento a cada atualização.

Por fim, um problema que afeta tanto C quanto C++: seu gerenciamento de projeto. Essas linguagens dependem de ferramentas de terceiros para configurar pacotes, configurações, compilação, etc. Isso não é um grande problema, mas uma coisa a mais com a qual se preocupar.


A Linguagem Rust

Rust é uma linguagem de programação desenvolvida pela Mozilla em 2009, focada na eficiência, segurança e confiabilidade. Considerada a linguagem mais amada pela pesquisa da StackOverFlow por 7 anos seguidos, tem ganhado muita popularidade e está crescendo rápido na programação de sistemas. Rust já faz parte do Kernel Linux, Android e até a Espressif, fabricante dos famosos ESP32, já esta criando soluções com ela. Para entender o sucesso de Rust, é importante dar uma breve introdução sobre como ela funciona.

Rust é uma linguagem capaz de realizar todas as funcionalidades do C++, mas sem os problemas associados a ela. Além disso, Rust apresenta um conjunto de recursos modernos, com um dos seus principais objetivos sendo a segurança, especialmente no que se refere a erros comuns de programação que podem levar a vulnerabilidades como vazamento de memória, data-race, etc. Rust utiliza o conceito de Ownership, uma “versão mais sofisticada” do RAII em C++. Essa técnica é uma abstração de custo zero assim como o RAII.

Em Rust, as variáveis são imutáveis por padrão. Ou seja, elas não podem ser modificadas após a sua declaração. Quando um valor é atribuído a uma variável, ela se torna o dona (Owner) desse valor, e cada valor só pode ter um dono por vez. Quando o dono sai de escopo, o valor é automaticamente destruído. Esse valor pode ser emprestado para outros donos, mas apenas um de cada vez, garantindo que a memória seja gerenciada de forma segura. Para garantir essa segurança, Rust possui uma verificação chamada “borrow checker”, que verifica em tempo de compilação se há algum problema de propriedade ou empréstimo de valores. Esse recurso permite que Rust gerencie a memória de forma eficiente, sem custo adicional de desempenho.

Outra vantagem é o gerenciador de pacotes Cargo, que permite, com facilidade, a criação e o gerenciamento de projetos. Vamos falar mais sobre ele na hora de criar nosso projeto.

Rust em sistemas embarcados:

Além da segurança e performance, Rust também tem algumas vantagens para sistemas embarcados:

Se você se interessou pela linguagem e quer aprender mais sobre ela, aqui estão alguns links uteis:

(Todos os links são de materiais gratuitos disponibilizados pela comunidade de Rust)

Agora, chega de falar e vamos começar a programar!!!


Instalando as Dependências

Nesse projeto, vamos montar um ambiente de desenvolvimento de Rust embarcado. Para isso, vamos instalar os seguintes softwares:

  • Visual Studio Code
  • Rust
  • OpenOCD
  • ARM Toolchain

Instalando Visual Studio Code:

A instalação do Visual Studio Code foi ensinada passo a passo neste post: https://blog.eletrogate.com/linguagem-go-com-arduino/ no tópico: Instalação e Configuração do IDE. Eecomendo a leitura deste post se você tem interesse em alternativas a C++. Nele, é apresentada a linguagem Go e seu uso em Arduino.

Instalando Rust:

Se você é usuário de Windows, primeiro, você deve instalar ferramentas de build do Visual Studio.

Para isso, baixe o instalador pelo link https://visualstudio.microsoft.com/pt-br/visual-cpp-build-tools/

Após o download, execute o instalador e siga os seguintes passos:

Clique em continuar e ele vai começar o download do instalador. Após isso, ele vai abrir a tela de instalação. Nessa tela, selecione a opção “Desenvolvimento para desktop com C++” e verifique, nos detalhes de instalação, se as seguintes caixas estão marcadas:

Clique em “Instalar”. Após o fim da instalação, feche o instalador.

Para instalar a linguagem Rust, baixe o instalador “rustup” pelo link: https://www.rust-lang.org/pt-BR/tools/install 

Após o download, abra o instalador e essa tela vai aparecer:

Digite 1 e aparte ENTER. A instalação vai começar. Esse processo pode demorar. Não feche o terminal até o termino da instalação. Se tudo ocorrer bem, essa será a mensagem:

Aperte ENTER e o terminal vai fechar automaticamente.

Instalando ferramentas para Rust embarcado:

Abra outro terminal e digite:

rustup target install thumbv7m-none-eabi

Isso vai instalar o compilador para ARM thumb. Depois disso, execute:

cargo install cargo-embed

Isso vai instalar as ferramentas para upload de código para a placa. Isso pode demorar um pouco. Quando tudo acabar, pode fechar o terminal.

Instalando OpenOCD

OpenOCD é um software de código livre para On-Chip Debbuger. Vamos usar ele para fazer o upload e o debug de nosso código usando o ST-Link.

Nesse link: https://github.com/xpack-dev-tools/openocd-xpack/releases/download/v0.12.0-1/xpack-openocd-0.12.0-1-win32-x64.zip

Basta descompactar onde você desejar e adicionar ao path. Para isso, siga os seguintes passos:

Vá até a pasta onde você descompactou o openOCD e clique na pasta “bin”

Copie o caminho da pasta “bin” e siga os passos.

Abra o menu Iniciar e pesquise por “Variáveis de ambiente”

Abra e clique em “Variaveis de Ambiente”

Selecione “Path” e clique em editar

Clique em novo. Ele vai abrir uma caixa de texto. Cole o caminho da pasta bin do OpenOCD e aperte em “Ok”. Você vai voltar à segunda janela e clicar em “Ok” novamente. Essa janela também vai fechar. Na última, clique em Aplicar.

Instalando ARM toolchain:

Baixe o instalador no link: https://developer.arm.com/-/media/Files/downloads/gnu/12.2.mpacbti-rel1/binrel/arm-gnu-toolchain-12.2.mpacbti-rel1-mingw-w64-i686-arm-none-eabi.exe

Após abrir, siga os seguintes passos

Clique em “Ok”

Clique em “Próximo”

Clique em “Eu Concordo”

Clique em “Instalar”

Marque a caixa “Add path to environment variable” e clique em “concluir”.

ST-Link Drives:

Para usar o ST-link, é necessário instalar os drives. Esses drives vêm com o ambiente de desenvolvimento para STM32 “CubeIDE”. Veja esse outro post da Eletrogate que ensina passo a passo essa instalação: https://blog.eletrogate.com/bluepill-com-stm32cubeide/, no tópico: “Baixar e Instalar”

Pronto. Agora, temos tudo para iniciar o projeto.


Criando um Blink em Rust

Esquema:

(Download projeto completo: https://github.com/RecursiveError/rust-embarcado/releases/download/1.0.1/blink_teste.zip)

Primeiro, vamos criar uma pasta para nosso projeto. Nomes de pastas de projetos em Rust devem conter apenas letras minúsculas, números e underline (“_”). Aqui, vamos chamar de “blink_teste”.

Agora, vamos abrir o Visual Studio Code. A primeira coisa que vamos fazer é instalar a extensão “rust-analyzer”, seguindo os passos abaixo:

Agora, vamos abrir a pasta do projeto. Para isso, clique em “File” e, depois, “Open Folder”. Isso vai abrir o navegador de arquivos. Nele, selecione a pasta que você criou.

Abra um terminal clicando em “Terminal” e, depois, “New terminal”. Com o terminal aberto, digite: “cargo init –bin”. Isso vai iniciar nosso projeto em Rust.

Após isso, a seguinte estrutura de pastas será gerada:

Agora, temos que configurar o projeto para trabalhar com o STM32. As abstrações para Rust embarcado seguem o esquema:

(fonte: https://docs.rust-embedded.org/book/start/registers.html )

PAC é uma biblioteca para acesso de periféricos geradas a partir dos arquivos SVD. A junção disso com a biblioteca da arquitetura forma as HAL de Rust. Essas HAL são padronizadas pela embedded-hal, o que significa que uma biblioteca feita em cima da embdded-hal vai funcionar em qualquer HAL Rust, independente da arquitetura. Nesse projeto, vamos usar a stm32f1xx_hal.

Primeiro, vamos criar um arquivo chamado memory.x:

memory.x são semelhantes aos arquivos “.ld” usados com C++. Dentro de memory.x, cole o seguinte código:

/*----deixe apenas o linker referente a sua placa e remova o resto----*/

/* Linker script for the STM32F103C8xx*/
MEMORY
{
  FLASH : ORIGIN = 0x08000000, LENGTH = 64K
  RAM : ORIGIN = 0x20000000, LENGTH = 20K
}

/* Linker script for the STM32F103C6xx*/
MEMORY
{
  FLASH : ORIGIN = 0x08000000, LENGTH = 32K
  RAM : ORIGIN = 0x20000000, LENGTH = 10K
}

Não se preocupe em memorizar esse código. Ele é feito para ser copiado e colado, assim como os “.ld”. Para utilizar, remova o linker que não é referente a sua placa. Neste projeto, estamos usando o STM32F013C8. Então, no final, o arquivo deve ficar assim:

/* Linker script for the STM32F103C8xx*/
MEMORY
{
  FLASH : ORIGIN = 0x08000000, LENGTH = 64K
  RAM : ORIGIN = 0x20000000, LENGTH = 20K
}

(não esqueça de salva o arquivo após as mudanças)

Agora, vamos configurar o compilador para ARM thumb:

Crie uma nova pasta, chamada .cargo. Dentro desta, crie um arquivo chamado config.toml:

dentro de config.toml, cole o seguinte código:

[target.thumbv7m-none-eabi]
runner = 'arm-none-eabi-gdb'
rustflags = [
  "-C", "link-arg=-Tlink.x",
]

[build]
target = "thumbv7m-none-eabi"

Com isso, o ambiente para stm32 está finalizado.

Instalando as bibliotecas:

Para incluir bibliotecas num projeto Rust, basta adicionar o nome e a versão no arquivo “Cargo.toml” que foi gerado com o comando “cargo init”. Nesse projeto vamos usar as seguintes bibliotecas:

Cargo.toml

[package]
name = "blink_teste"
version = "0.1.0"
edition = "2021"

[dependencies]
embedded-hal = "0.2.7"
nb = "1"
cortex-m = "0.7.6"
cortex-m-rt = "0.7.1"
panic-halt = "0.2.0"

[dependencies.stm32f1xx-hal]
version = "0.10.0"

#remova '#' em 'features' selecionar a placa correta

#STM32F103C8xx
#features = ["rt", "stm32f103", "medium"]

#STM32F103C6xx
#features = ["rt", "stm32f103"]

Para utilizar, é necessário remover o ‘#’ localizado atrás de ‘features’ de acordo com sua placa (não esqueça de salva o arquivo após as mudanças). Essa seleção, feita no memory.x e cargo.toml, é referente à densidade de memória da placa. Caso não esteja usando uma placa STM32 bluepill, a identificação da densidade de memória pode ser feita da seguinte maneira:

Normalmente, o segundo caractere após o número do nome do dispositivo é referente à densidade de memória. Por exemplo, em “STM32F103C8T6”, o nome do dispositivo é SMT32F103. Então, o segundo caractere após isso é 8. Com isso, é só verificar, nessa tabela, a configuração correta no Cargo.toml:

  • 4, 6 → low density, no feature required
  • 8, B → medium feature
  • C, D, E → high feature
  • F, G → xl feature

(fonte: https://github.com/stm32-rs/stm32f1xx-hal#selecting-a-microcontroller)

O arquivo src/main.rs é onde vamos colocar o nosso código:

main.rs

// projeto bare-metal portanto sem std e sem main (codigo inicia no entry point)
#![no_std]
#![no_main]

use panic_halt as _; //panic handler padrão
use cortex_m_rt::entry;
use stm32f1xx_hal::{pac, prelude::*};

//declara nosso _start
#[entry]
fn main() -> !{
    let cp = cortex_m::Peripherals::take().unwrap(); //perifericos da arquitetura
    let dp = pac::Peripherals::take().unwrap(); //perifericos da placa

    //incia os perifericos
    let mut _afio = dp.AFIO.constrain();
    let mut flash = dp.FLASH.constrain();
    let rcc = dp.RCC.constrain();
    let clocks = rcc.cfgr.freeze(&mut flash.acr);

    // define o pino pb9 como saida
    let mut gpiob = dp.GPIOB.split();
    let mut led = gpiob.pb9.into_push_pull_output(&mut gpiob.crh);

    //cria um delay bloqueando com o clock da placa
    let mut delay = cp.SYST.delay(&clocks);

    loop{
        //pisca o led a cada 1seg
        led.toggle();
        delay.delay_ms(1000u16);
    }

}

pode acontecer de você se deparar com o seguinte erro:

Esse é um bug da extensão do Vscode “Rust-analyzer”. Ele não identifica que o target se trata de um ambiente bare-metal. Mas, não se preocupe: isso é apenas visual. O projeto ainda compila e funciona normalmente. Apenas ignore esta mensagem.

Para enviar para placa, abra o terminal (na pasta do projeto) e digite o comando:

cargo embed --chip STM32F103C8 --release

Se os drives do STlink e o openOCD foram instalados corretamente, ele vai identificar automaticamente a porta e enviar o código.

Resultado:

Esse código ainda contém pedaços que não são vistos em projetos comuns com C. Para explicar eles de forma correta, nada melhor que comparar lado a lado com C

A primeira coisa a explicar está logo no início do arquivo:

#![no_std] e #![no_main], essas linhas dizem ao compilador como queremos compilar esse arquivo. Nesse caso, sem a std e sem a main, porque é um código bare-metal, não tem sistema e começa de um endereço predefinido chamado “entry point”. Essas coisas também existem no C, mas, nela, você chama passando os argumentos para o compilador.

Seguindo até a “main”, podemos ver #[entry] e “!”. #[entry] marca a função em que o código inicia (entry point). Isso, em C, fica no arquivo de strartup. “!” indica que a função não pode retornar e pula direto para o panic_halt caso a função retorne.

Nas primeiras linhas da função, podemos ver a separação entre PAC(dp) e Cortex-M(cp) visto na imagem abstração, as linhas abaixo são apenas iniciação de periféricos.

Chegando na parte principal, podemos notar umas semelhanças. Vonfiguramos o clock RCC, definimos o funcionamento do GPIO pelo registrador CRx, criamos um delay baseado no SYST do NVIC, assim como em outras HAL, como a CubeMx, e, por fim, apenas invertemos o valor do GPIO a cada 1seg.


Debugando com GDB

Você pode fazer debug de seus projetos embarcados em Rust usando GDB. Primeiro, você precisa compilar o código para debug. Para isso, rode o comando:

cargo embed --chip STM32F103C8

Isso vai compilar e enviar o código de debug para a placa. Agora, no terminal do projeto, digite o comando

arm-none-eabi-gdb target/thumbv7m-none-eabi/debug/blink_teste  (blink_teste é o nome do projeto. Em Rust, os binários compilados pelo cargo têm o mesmo nome do projeto)

Agora, em outro terminal (esse não precisa estar na pasta do projeto), digite o seguinte comando:

openocd -f interface/stlink-v2.cfg -f target/stm32f1x.cfg

Isso vai fazer o openOCD se conectar com a placa. Então, ele vai abrir uma conexão para GDB na porta 3333

Agora, volte para o GDB e digite o seguinte comando:

target extended-remote localhost:3333

Isso vai fazer a conexão com o openOCD. Pronto! Agora, você tem o GDB para debug de Rust embarcado.

Para sair do GDB e do OpenOCD, digite Ctrl+C.

Usar no terminal pode ser difícil para novos usuário e, infelizmente, o terminal Windows não tem suporte para a interface TUI do GDB. Mas você sabia que nos podemos usar o Visual Studio Code como uma interface interativa para uso do GDB em projetos embarcados? Para isso, vá até a aba de extensões e “instale Cortex-Debug”:

(Download do projeto completo[incluindo config de Debug]: https://github.com/RecursiveError/rust-embarcado/releases/download/1.0.1/blink_teste.zip)

Após a instalação, volte para o projeto, crie uma pasta chamada “.vscode” e, dentro dela, dois arquivos chamados “tasks.json” e “launch.json”:

Dentro de tasks.json, coloque o seguinte código:

tasks.json

{
    "version": "2.0.0",
"tasks": [
    {
        "label": "Cargo build",
        "type": "shell",
        "command": "cargo",
        "args": ["build"],
        "problemMatcher": [
            "$rustc"
        ],
        "group": "build"
    },
    {
        "label": "Build binary",
        "type": "shell",
        "command": "arm-none-eabi-objcopy",
        "args": [
            "--output-target", "binary",
            "target/thumbv7m-none-eabi/debug/${workspaceFolderBasename}",
            "main.bin",
        ],
        "problemMatcher": [
            "$rustc"
        ],
        "group": {
            "kind": "build",
            "isDefault": true
        },
        "dependsOn": "Cargo build"
    }
]
}

e dentro launch.json:

launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug Rust",
            "request": "launch",
            "type": "cortex-debug",
            "cwd": "${workspaceRoot}",
            "executable": "target/thumbv7m-none-eabi/debug/${workspaceFolderBasename}",
            "svdFile": "STM32F103.svd",
            "servertype": "openocd",
            "configFiles": [
                "interface/stlink-v2.cfg",
                "target/stm32f1x.cfg"],
            "preLaunchTask": "Build binary",
            "preLaunchCommands": [
                "monitor init",
                "monitor reset init",
                "monitor halt",
                "monitor flash write_image erase main.bin 0x08000000"
            ],
            "postLaunchCommands": ["continue"]
        }
    ]
}

Você também vai ter que baixar o arquivo svd da placa e colocar na pasta do projeto. No nosso caso, o SMT32F103, você pode baixar por este link: https://github.com/RecursiveError/rust-embarcado/releases/download/1.0.0/STM32F103.svd

Para rodar, basta ir até a aba de debug do Vscode e selecionar a opção “Debug Rust” ou apertar “F5”:

Debug usando GDB:


Considerações Finais

Discutimos, anteriormente, as vantagens da linguagem. Mas e quanto às desvantagens? Embora Rust seja altamente eficiente e eficaz em sua função, há algumas considerações a se fazer. A linguagem é conhecida por ter uma curva de aprendizado difícil, o que pode ser desafiador no início, mas o investimento vale a pena. Além disso, o tempo de compilação é mais longo devido às verificações rigorosas da linguagem e o tamanho do binário pode ser ligeiramente maior, o que pode ser um problema em algumas situações.

Rust vai substituir C++?

Se você já pesquisou sobre Rust, provavelmente já se deparou com a pergunta frequente: ‘Com todas essas vantagens, Rust vai substituir C++?’. Embora seja impossível prever o futuro, a curto prazo, podemos afirmar que não. Apesar de seus problemas, C++ ainda apresenta suas vantagens. Rust reacendeu discussões sobre segurança sem perda de eficiência e, por isso, muitos desenvolvedores de C++ estão aplicando técnicas aprendidas com Rust para melhorar a qualidade de seus softwares.

Ainda tem muito o que falar sobre Rust, mas este post acaba aqui. Espero que tenham gostado desse grande pequena introdução à linguagem e até a próxima.


Sobre o Autor


Guilherme Schultz
LinkedIn

Apaixonado por tecnologia, Autodidata em eletrônica e desenvolvimento embarcado.


Eletrogate

4 de julho de 2023

A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.

Tenha a Metodologia Eletrogate dentro da sua Escola! Conheça nosso Programa de Robótica nas Escolas!

Eletrogate Robô

Cadastre-se e fique por
dentro de novidades!