quarta-feira, 27 de setembro de 2023

LSM110A P2P - EMBASSY

  LSM110A P2P - EMBASSY


O objetivo deste BLOG é demonstrar como é possível utilizar a linguagem de programação RUST para programar o módulo LSM110A. O LSM110A foi montado no BREAKOUT da Smartcore. 


Sobre o Rust

Rust é uma nova linguagem de programação patrocinada pela Mozilla. Rust 1.0, a primeira versão estável, foi lançado em 15 de maio de 2015. Ele se concentra na segurança, simultaneidade e velocidade, ao mesmo tempo que fornece controle de baixo nível. Isso o torna adequado para desenvolvimento embarcado. Rust tem muitas semelhanças com C, mas também leva inspiração em linguagens funcionais como Haskell. Além disso, também tem muitas ferramentas inspiradas nas ferramentas de linguagens dinâmicas como Ruby, Python e JavaScript.

O Rust visa fornecer segurança à memória sem coleta de lixo. Esta segurança é alcançado usando dois conceitos chamados propriedade e tempo de vida. Quando um novo variável é criada, ela está ligada a um nome. Então dizemos que esse nome "possui" a variável. Mais tarde, a variável pode ser associada a um novo nome, e a propriedade é passada. O rastreamento de propriedade permite que RUST saiba quando uma variável não é mais necessário. Isso ocorre porque quando o proprietário sai do escopo, a variável não está mais vinculado a nenhum nome e, portanto, não pode ser acessado pelo código não mais. A variável pode, portanto, ser descartada com segurança e sua memória liberada. Se quisermos que outra pessoa use a variável por um tempo, por exemplo, uma função, nós pode deixar a função tomar emprestada a variável, dando-lhe uma referência à variável.
No entanto, para evitar condições de corrida, podemos distribuir vários referências que não podem alterar a variável, ou podemos apenas distribuir um único referência mutável. Dessa forma, o Rust é capaz de evitar leituras simultâneas e grava em uma variável, garantindo que ela sempre esteja em um estado consistente, além do mais para isso, rastreando o tempo de vida das ligações e empréstimos, ou seja, o tempo que um variável é emprestada, ou o escopo em que a vinculação ou empréstimo existe, Rust pode garantir que não haja vinculações a uma variável eliminada.

Como o Rust deve ser usado em todos os lugares, C é usado hoje; também tem mecanismos para trabalhar com bibliotecas escritas em C. Para chamar uma função escrito em C do Rust, há algumas coisas que precisam ser feitas: a função deve ser declarada como uma função externa usando o C ABI, e a biblioteca onde a função é definida deve ser vinculada. Se a função usar quaisquer tipos compostos, como structs ou enums, esses tipos também devem ser definidos em Rust. Além disso, como o compilador Rust não pode verificar se a função C mantém garantias de segurança da RUST, quando chamamos a função, ela deve estar dentro de um local inseguro. Ao fazer uma função Rust para chamar de C, também precisamos marcar que a função deve usar o C ABI. Além disso, precisamos dizer ao compilador Rust para não estragar o nome da função. Um exemplo de uso do a interface de função estrangeira é mostrada na listagens abaixo:

// File: library.c struct c_struct { int a, int b } // We must tell the C compiler what the Rust function looks like extern int rust_function(int a); int c_function(struct c_struct arg) { return rust_function(arg.a); }









// File: main.rs // We need to tell the Rust compiler to pack the C // struct in the same way that the C compiler would. #[repr(C)] struct c_struct { a: i32, 8 b: i32, } #[no_mangle] pub extern "C" fn rust_function(a: i32) -> i32 { a + 1 } // Inside this block we can define external functions // that must be called using the C ABI. extern "C" { fn c_function(arg: c_struct) -> i32; } fn main() { let foo = c_struct{a: 1, b: 2}; unsafe { println!("foo.a + 1: {}", c_function(foo)); } }





















Às vezes, as garantias de segurança de Rust são um pouco estritas demais, ou Rust pode ser incapaz para provar que algo é seguro e, portanto, não o permitirá, mesmo que o o programador pode saber que a operação é segura. Nestes casos, Rust fornece a palavra-chave unsafe. Dentro de um bloco de código marcado como unsafe, Rust nos permite
fazer algumas coisas normalmente não permitidas:

• Cancelar a referência de um ponteiro bruto
• Acessar ou modificar uma variável estática mutável
• Chame uma função ou método unsafe
• Implementar um traço unsafe

Chamar funções estrangeiras se enquadra na categoria “Chamar uma função ou método unsafe” ponto. Variáveis ​​estáticas em Rust são variáveis ​​que têm uma vida útil estática, ou seja, eles duram por todo o programa. Além disso, seu escopo é global (para o módulo onde estão definidos). Isso significa que eles, em teoria, podem ser acessado por vários threads. Portanto, se a variável estática for mutável, não é seguro acessá-lo, pois outro thread pode estar modificando-o ao mesmo tempo. Como parte da análise da vida útil do Rust, ele é capaz de impedir a leitura de memória não inicializada. Esse acesso à memória é considerado inseguro, pois a memória pode conter qualquer valor. A razão para este ser um problema torna-se clara se consideramos uma variável não inicializada do tipo bool. A memória que o bool é feito de só pode ter dois valores válidos: 0 e 1. No entanto, o não inicializado pedaço de memória pode ter qualquer valor, o que leva a um problema se tentarmos ler o valor do bool. Em código seguro (código que não está dentro de um bloco unsafe) isso não pode acontecer, pois Rust pode detectar que a variável não foi inicializada, e é impossível obter referências a variáveis ​​não inicializadas. No entanto, Rust também suporta ponteiros brutos (ponteiros C padrão). Como esses ponteiros podem ser definidos para apontam para qualquer endereço, Rust não pode garantir que eles sempre apontem para memória inicializada válida. Portanto, desreferenciar um ponteiro bruto é considerado inseguro, e deve ser feito dentro de um bloco inseguro.

Conjunto de ferramentas Rust e outras ferramentas úteis

O compilador "rustc" do Rust é baseado no back-end do compilador LLVM. O projeto LLVM consiste em vários subprojetos; os mais relevantes são o LLVM Core, Clang, compiler-rt e LLD. LLVM Core é um conjunto de bibliotecas que fornecem um otimizador independente de origem e destino e suporte à geração de código para muitos CPUs diferentes. Clang é um compilador C que usa LLVM como backend. Clang faz isso produzindo uma representação intermediária LLVM (LLVM IR), que é fornecido ao LLVM Core, que então gera o código binário final. Clang também pode ser usado como uma plataforma para fazer ferramentas de nível de origem. O LLVM IR contém alguns operações de alto nível que não são suportadas em todas as plataformas. Para essas plataformas, o compilador-rt fornece rotinas de implementos para essas operações. LLD é um novo linker, que pode ser usado como um substituto dos linkers do sistema. Tem sido usado como o vinculador padrão em alvos ARM Cortex-M desde 28 de agosto de 2018. Para ajudar a chamar o compilador e lidar com dependências Rust fornece cargo. Cargo é principalmente um gerenciador de pacotes que lida com as dependências de um projeto Rust, mas também pode ser usado para especificar como o projeto Rust deve ser construído. Isso inclui a construção e vinculação a bibliotecas C. Para fornecer este funcionalidade Cargo usa alguns arquivos de configuração e uma versão opcional roteiro. O primeiro arquivo de configuração, chamado de arquivo de manifesto, contém informações Sobre o projeto. Qual é o nome do projeto e que versão é? Quem escreveu? Ele também tem informações sobre quais dependências (Rust) são usadas e quais sinalizadores de recurso eles usam; quais outras bibliotecas devem ser vinculadas e onde encontrá-los. Ele também especifica como o próprio projeto é construído. Isso inclui opções como o tipo de biblioteca para a qual ela deve ser compilada (estática ou dinâmica), qual nível de otimização deve ser usado e como os pânicos (panic) devem ser tratados.
O outro arquivo de configuração “.cargo/config” e é usado para configurar o próprio Cargo.
Ele também pode ser usado para definir certos sinalizadores ao compilar para um destino específico e as opções neste arquivo podem ser espalhadas por vários arquivos, para cima na hierarquia de arquivos. Dessa forma, podemos especificar opções tanto globalmente quanto por projeto. O caminho final de influenciar a construção é um script de construção. Geralmente são chamados de “build.rs”, mas qualquer arquivo pode ser especificado no arquivo de manifesto. O arquivo de compilação contém um Rust programa que é executado antes da construção do projeto. Os casos de uso mais comuns para esses scripts de construção são para gerar código em tempo de compilação, código de construção escrito em outro idioma e para especificar como vincular aos idiomas nativos. A construção script pode passar comandos e opções para o Cargo usando sua saída padrão.

Rust usa uma estrutura de módulo hierárquica, o que significa que a estrutura do módulo representa uma árvore, com um módulo de nível superior que contém módulos filhos e em breve. Qualquer função, tipo, característica ou submódulo (um item) definido em um módulo é privado por padrão, mas pode ser tornado público pela palavra-chave pub. A exceção é o nível superior da biblioteca (caixa em linguagem de Rust), que não exporta nada por padrão. Um item é sempre público para seu módulo pai imediato, e o módulo dos filhos dos pais. Um módulo pode ser escrito no mesmo arquivo que seu pai, mas geralmente recebem seus próprios arquivos. Para simplificar o procurar por esses arquivos, o nome do arquivo é decidido pelo compilador Rust. Um arquivo contendo um módulo deve ter o mesmo nome do módulo ou ser chamado mod.rs e existe em uma pasta com o mesmo nome do módulo. Por exemplo, um módulo chamado foo pode ser escrito no arquivo do módulo pai, em foo.rs ou mod/foo.rs. Da mesma forma, Cargo espera que o arquivo de nível superior em uma biblioteca ser chamado de lib.rs, e em um executável main.rs.
Como o rustc visa o LLVM em vez de uma arquitetura real, o Rust pode ser compilado para a maioria das plataformas para as quais o LLVM pode gerar código. Isso também significa que Rust é um compilador cruzado por padrão. Para compilar para um destino diferente, todos nós precisa fazer é obter a versão correta da biblioteca padrão e especificar quais alvo para o qual queremos compilar. No entanto, se quisermos usar a biblioteca padrão (ou partes dele) em um alvo que Rust não suporta, ou queremos mudar como a biblioteca padrão é construída, nós mesmos temos que construir a biblioteca padrão.
Isso pode ser feito com a ferramenta Xargo, que imita o comportamento do Cargo mas também constrói a biblioteca padrão. Xargo irá compilar a biblioteca padrão e chamar o compilador com os sinalizadores corretos para que a nova biblioteca padrão seja vinculados. O Xargo, por padrão, só compilará o núcleo independente de plataforma da biblioteca padrão, mas se outras partes da biblioteca forem necessárias, eles também podem ser compilado.

Para saber como compilar para um alvo específico, o rustc usa uma especificação de arquivo alvo. Esta abordagem foi sugerida pela primeira vez na RFC 131. Ter o alvo especificações escritas em seu próprio arquivo de configuração permitem que os usuários editem os alvos facilmente, e assim facilmente transportar Rust para novas plataformas. O arquivo de configuração consiste de um objeto JSON que especifica qual destino LLVM deve ser usado, junto com o endianness, a largura do ponteiro e do inteiro e o layout de dados do alvo. Dentro além disso, existem campos para especificar o sistema operacional, ambiente (qual libc), fornecedor e arquitetura. Essas opções são usadas para condicional compilação de Rust. Por fim, existem muitas configurações opcionais que podem ser conjunto. Entre eles estão qual linker usar e quais argumentos o linker deve ser usado, quais formatos de saída estão disponíveis e algumas configurações que controlam quais otimizações são aplicadas. Uma descrição completa da especificação alvo pode ser encontrado. Embora as especificações de destino possam ser escritas em JSON, os destinos suportados são construídos diretamente no rustc. Há duas razões para isso:

1. Facilita a distribuição, evitando a necessidade de arquivos de configuração;
2. As especificações tornam-se imutáveis ​​e podem, portanto, serem confiáveis.

No entanto, o rustc pode imprimir as especificações no formato JSON. A especificação do thumbv7em-none-eabi alvo, que é o alvo para um bare metal Cortex M4 sem uma unidade de ponto flutuante pode ser visto na listagem abaixo:

{ "abi-blacklist": [ "stdcall", "fastcall", "vectorcall", "thiscall", "win64", "sysv64" ], "arch": "arm", "data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64", "emit-debug-gdb-scripts": false, "env": "", "executables": true, "is-builtin": true, "linker": "rust-lld", "linker-flavor": "ld.lld", "llvm-target": "thumbv7em-none-eabi", "max-atomic-width": 32, "os": "none", "panic-strategy": "abort", "relocation-model": "static", "target-c-int-width": "32", "target-endian": "little", "target-pointer-width": "32", "vendor": "" }





















Outra ferramenta útil para desenvolvedores Rust é (Rust-) Bindgen. Tanto do trabalho com a chamada de funções C vem do fornecimento de protótipos C em Rust, Bindgen  foi feito para gerar essas ligações automaticamente. Bindgen usa libclang, um interface da biblioteca para o Clang. Dessa forma, o Bindgen pode gerar uma sintaxe abstrata árvore (AST) do código, e encontre todas as funções e definições de tipo que são usados. Ao usar libclang, o Bindgen também obtém o benefício de executar o pré-processador no código, então, se houver várias versões de uma função baseada em algumas opções de configuração, o Bindgen verá a correta.

Rust - EMBASSY

Embassy é a estrutura de nova geração para aplicativos embarcados. Escreva código embarcado seguro, correto e energeticamente eficiente mais rapidamente, usando a linguagem de programação Rust, suas instalações assíncronas e as bibliotecas da Embassy.

Instalando Rust e portado para o LSM110A, o qual é um STM32WL55JC

Baseado no BLOG


Foi testado no Prompt do WINDOWS


rustc -V
rustup target add thumbv7em-none-eabi rustup target add thumbv7em-none-eabihf sudo apt install build-essential cargo install cargo-binutils rustup component add llvm-tools-preview

O exemplo utilizado foi o para piscar um LED (BLINK) o qual foi testado na placa BREAKOUT.


Para gravar será utilizado o ST-LINK V2 


git clone https://github.com/embassy-rs/embassy.git
cd embassy
cargo install probe-run
cargo install probe-rs






cd ~/embassy/examples/stm32wl
cd .cargo
notepad config.toml

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# replace your chip as listed in `probe-rs chip list`
runner = "probe-rs run --chip STM32WLE5JBIx"

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

[env]
DEFMT_LOG = "trace"

cd ..
cargo build release
probe-rs run --chip STM32WLE5JBIx target\thumbv7em-none-eabi\debug\lora_p2p_send
grave no primeiro LSM110A

probe-rs run --chip STM32WLE5JBIx target\thumbv7em-none-eabi\debug\lora_p2p_receive
grave no segundo LSM110A


Exemplo de como Gravar

CÓDIGO ALTERADO PARA O lora_p2p_send

//! This example runs on a STM32WL board, which has a builtin Semtech Sx1262 radio. //! It demonstrates LORA P2P send functionality. #![no_std] #![no_main] #![macro_use] #![feature(type_alias_impl_trait, async_fn_in_trait)] use defmt::info; use embassy_executor::Spawner; use embassy_lora::iv::{InterruptHandler, Stm32wlInterfaceVariant}; use embassy_stm32::bind_interrupts; use embassy_stm32::gpio::{Level, Output, Pin, Speed}; use embassy_stm32::spi::Spi; use embassy_time::Delay; use lora_phy::mod_params::*; use lora_phy::sx1261_2::SX1261_2; use lora_phy::LoRa; use {defmt_rtt as _, panic_probe as _}; const LORA_FREQUENCY_IN_HZ: u32 = 926_600_000; // warning: set this appropriately for the region bind_interrupts!(struct Irqs{ SUBGHZ_RADIO => InterruptHandler; }); #[embassy_executor::main] async fn main(_spawner: Spawner) { let mut config = embassy_stm32::Config::default(); config.rcc.mux = embassy_stm32::rcc::ClockSrc::HSE32; let p = embassy_stm32::init(config); let spi = Spi::new_subghz(p.SUBGHZSPI, p.DMA1_CH1, p.DMA1_CH2); // Set CTRL1 and CTRL3 for high-power transmission, while CTRL2 acts as an RF switch between tx and rx let _ctrl1 = Output::new(p.PB12.degrade(), Level::Low, Speed::High); let ctrl2 = Output::new(p.PC13.degrade(), Level::High, Speed::High); let _ctrl3 = Output::new(p.PC3.degrade(), Level::High, Speed::High); let iv = Stm32wlInterfaceVariant::new(Irqs, None, Some(ctrl2)).unwrap(); let mut delay = Delay; let mut lora = { match LoRa::new(SX1261_2::new(BoardType::Stm32wlSx1262, spi, iv), false, &mut delay).await { Ok(l) => l, Err(err) => { info!("Radio error = {}", err); return; } } }; let mdltn_params = { match lora.create_modulation_params( SpreadingFactor::_10, Bandwidth::_125KHz, CodingRate::_4_5, LORA_FREQUENCY_IN_HZ, ) { Ok(mp) => mp, Err(err) => { info!("Radio error = {}", err); return; } } }; let mut tx_pkt_params = { match lora.create_tx_packet_params(4, false, true, false, &mdltn_params) { Ok(pp) => pp, Err(err) => { info!("Radio error = {}", err); return; } } }; match lora.prepare_for_tx(&mdltn_params, 20, false).await { Ok(()) => {} Err(err) => { info!("Radio error = {}", err); return; } }; let buffer = [0x01u8, 0x02u8, 0x03u8]; match lora.tx(&mdltn_params, &mut tx_pkt_params, &buffer, 0xffffff).await { Ok(()) => { info!("TX DONE"); } Err(err) => { info!("Radio error = {}", err); return; } }; match lora.sleep(&mut delay).await { Ok(()) => info!("Sleep successful"), Err(err) => info!("Sleep unsuccessful = {}", err), } }

CÓDIGO ALTERADO PARA O lora_p2p_receive

//! This example runs on the STM32WL board, which has a builtin Semtech Sx1262 radio. //! It demonstrates LORA P2P receive functionality in conjunction with the lora_p2p_send example. #![no_std] #![no_main] #![macro_use] #![feature(type_alias_impl_trait, async_fn_in_trait)] use defmt::info; use embassy_executor::Spawner; use embassy_lora::iv::{InterruptHandler, Stm32wlInterfaceVariant}; use embassy_stm32::bind_interrupts; use embassy_stm32::gpio::{Level, Output, Pin, Speed}; use embassy_stm32::spi::Spi; use embassy_time::{Delay, Duration, Timer}; use lora_phy::mod_params::*; use lora_phy::sx1261_2::SX1261_2; use lora_phy::LoRa; use {defmt_rtt as _, panic_probe as _}; const LORA_FREQUENCY_IN_HZ: u32 = 926_600_000; // warning: set this appropriately for the region bind_interrupts!(struct Irqs{ SUBGHZ_RADIO => InterruptHandler; }); #[embassy_executor::main] async fn main(_spawner: Spawner) { let mut config = embassy_stm32::Config::default(); config.rcc.mux = embassy_stm32::rcc::ClockSrc::HSE32; let p = embassy_stm32::init(config); let spi = Spi::new_subghz(p.SUBGHZSPI, p.DMA1_CH1, p.DMA1_CH2); // Set CTRL1 and CTRL3 for high-power transmission, while CTRL2 acts as an RF switch between tx and rx let _ctrl1 = Output::new(p.PB12.degrade(), Level::Low, Speed::High); let ctrl2 = Output::new(p.PC13.degrade(), Level::High, Speed::High); let _ctrl3 = Output::new(p.PC3.degrade(), Level::High, Speed::High); let iv = Stm32wlInterfaceVariant::new(Irqs, None, Some(ctrl2)).unwrap(); let mut delay = Delay; let mut lora = { match LoRa::new(SX1261_2::new(BoardType::Stm32wlSx1262, spi, iv), false, &mut delay).await { Ok(l) => l, Err(err) => { info!("Radio error = {}", err); return; } } }; let mut debug_indicator = Output::new(p.PB9, Level::Low, Speed::Low); let mut start_indicator = Output::new(p.PB15, Level::Low, Speed::Low); start_indicator.set_high(); Timer::after(Duration::from_secs(5)).await; start_indicator.set_low(); let mut receiving_buffer = [00u8; 100]; let mdltn_params = { match lora.create_modulation_params( SpreadingFactor::_10, Bandwidth::_125KHz, CodingRate::_4_5, LORA_FREQUENCY_IN_HZ, ) { Ok(mp) => mp, Err(err) => { info!("Radio error = {}", err); return; } } }; let rx_pkt_params = { match lora.create_rx_packet_params(4, false, receiving_buffer.len() as u8, true, false, &mdltn_params) { Ok(pp) => pp, Err(err) => { info!("Radio error = {}", err); return; } } }; match lora .prepare_for_rx(&mdltn_params, &rx_pkt_params, None, true, false, 0, 0x00ffffffu32) .await { Ok(()) => {} Err(err) => { info!("Radio error = {}", err); return; } }; loop { receiving_buffer = [00u8; 100]; match lora.rx(&rx_pkt_params, &mut receiving_buffer).await { Ok((received_len, _rx_pkt_status)) => { if (received_len == 3) && (receiving_buffer[0] == 0x01u8) && (receiving_buffer[1] == 0x02u8) && (receiving_buffer[2] == 0x03u8) { info!("rx successful"); debug_indicator.set_high(); Timer::after(Duration::from_secs(5)).await; debug_indicator.set_low(); } else { } } Err(err) => info!("rx unsuccessful = {}", err), } } }

Assustadores os programas, sim, um novo paradigma!

#![no_std]
O atributo #! [No_std] indica que o programa não fará uso da biblioteca padrão, a caixa std. Em vez disso, ele usará a biblioteca central, um subconjunto da biblioteca padrão que não depende de um sistema operacional (SO) subjacente.

#![no_main]
O atributo #! [No_main] indica que o programa usará um ponto de entrada personalizado em vez do padrão fn main () {..}.

#[entry]
O atributo # [entrada] declara o ponto de entrada personalizado do programa. O ponto de entrada deve ser uma função divergente cujo tipo de retorno é o tipo nunca!. A função não pode retornar; portanto, o programa não pode ser encerrado.

MÓDULOS


Execução

Ao resetar o LSM110a com o programa lora_p2p_send, veremos no LSM110A com o programa lora_p2p_receive a mensagem

rx successful

Confirmando o recebimento do pacote

0x01u8, 0x02u8, 0x03u8

Dúvidas:

suporte@smartcore.com.br

Referências:

Sobre a SMARTCORE

A SmartCore fornece módulos para comunicação wireless, biometria, conectividade, rastreamento e automação.
Nosso portifólio inclui modem 2G/3G/4G/NB-IoT/Cat.M, satelital, módulos WiFi, Bluetooth, GNSS / GPS, Sigfox, LoRa, leitor de cartão, leitor QR code, mecanismo de impressão, mini-board PC, antena, pigtail, LCD, bateria, repetidor GPS e sensores.

Mais detalhes em www.smartcore.com.br