Micropython nos permite usar a sintaxe de uma linguagem de script poderosa em uma placa com microcontrolador. Uma das principais vantagens é a implementação de programas complexos com um número menor de linhas de programação e menor investimento em tempo, se comparado com a programação em C/C++.
Uma possível aplicação de Micropython é o desenvolvimento rápido de protótipos de sistemas embarcados, ou o desenvolvimento de ferramentas/jigs de teste, mas há aplicações “sérias” (isto é, que podem se tornar produtos para o usuário final) em que seu uso pode ser mais que apropriado, desde que observadas as limitações e avaliados os riscos e benefícios dessa abordagem, da mesma forma que é feito em no desenvolvimento de produtos que usam a abordagem tradicional (isto é, linguagens compiladas como C/C++, RTOS ou bare-metal etc.).
Uma vantagem de Micropython é que boa parte do conhecimento de Python se aplica sem ou com poucas alterações. Outra vantagem é que há implementação dos drivers para vários periféricos das arquiteturas suportadas (UART, I2C, SPI, Timers, PWM, ADCs) de modo que o esforço requerido para produzir uma aplicação usando esses periféricos é pequeno, especialmente levando em conta a grande quantidade de tutoriais e código fonte disponíveis na Internet. E para os casos em que é necessário conectar algum periférico mais sofisticado, por exemplo, um display gráfico, ou algum circuito integrado mais complexo ou algum protocolo de comunicação, é bem provável que exista algum código fonte para esses dispositivos, permitindo que possamos nos concentrar na aplicação em si, em vez de escrever os drivers para esses dispositivos.
Apesar do aparente entusiasmo que os parágrafos acima sugerem, é preciso deixar claro que Micropython não é um substituto de C/C++ em todas as situações ou uma panaceia técnica: várias aplicações, especialmente as que exigem uma resposta muito rápida e determinística ou em que os recursos (especialmente memória) são limitados ainda vão precisar ser desenvolvidos em C ou C++ ou alguma outra linguagem compilada.
Quando os drivers não oferecem a funcionalidade necessária
As classes do Micropython que controlam os periféricos dos microcontroladores para os quais existem ports oferecem funcionalidades comuns que são suficientes para a maior parte das aplicações, por exemplo, para controlar a velocidade de um motor DC ou um servo motor usando PWM, comunicação com sensores via I2C ou SPI, aquisição de sinais analógicos com um ADC etc. Entretanto, algumas aplicações podem se beneficiar de funcionalidades disponíveis nos periféricos do microcontrolador hospedeiro, mas não disponíveis nas classes existentes. Nesses casos, seria necessário alterar o port que suporta o microcontrolador para adicionar as funcionalidades desejadas,[1] ou usar o inline assembler do Micropython[2] ou ainda escrever um módulo externo em C[3] , mas em todos esses casos seria preciso investir tempo aprendendo os conceitos necessários para implementar o acesso aos registradores, tempo esse que seria melhor usado desenvolvendo a aplicação em si.
Felizmente há alternativas nativas do próprio Micropython para acessar os registradores; elas serão apresentadas nas seções seguintes.
Acesso aos registradores usando o módulo machine
O módulo machine contém os tipos mem8, mem16 e mem32. O código abaixo, obtido da documentação do Micropython[4] , mostra como acessar os GPIOs de um microcontrolador STM32.
import machine
from micropython import const
GPIOA = const(0x48000000)
GPIO_BSRR = const(0x18)
GPIO_IDR = const(0x10)
# set PA2 high
machine.mem32[GPIOA + GPIO_BSRR] = 1 << 2
# read PA3
value = (machine.mem32[GPIOA + GPIO_IDR] >> 3) & 1
A documentação inclui um exemplo mais detalhado, para o ESP32.[5]
Como os nomes sugerem, esses tipos permitem acesso a palavras de um byte (mem8), dois bytes (mem16) ou quatro bytes (mem32). Embora em geral os microcontroladores suportados pelo Micropython sejam de 32 bits, é possível que a largura do barramento de periféricos seja diferente, por exemplo, de 16 bits. Portanto, é necessário verificar a documentação do microcontrolador usado no projeto, para determinar qual tipo usar.
Acesso aos registradores usando o Viper
Micropython, assim como Python, não interpreta diretamente o código do script durante a execução, em vez disso compilando-o no início da execução para gerar o bytecode que é executado pela máquina virtual (Micro)Python, permitindo uma performance muito superior à de uma linguagem de script puramente interpretada (isto é, que processa continuamente o texto do script).
O emissor de código Viper é uma extensão do Micropython que gera código de máquina nativo do processador hospedeiro em vez de gerar o bytecode, permitindo um melhor desempenho de execução. Abaixo, um excerto de código usando a extensão, extraído da documentação. [6]
@micropython.viper
def foo(self, arg: int) -> int:
# code
O Viper tem alguns tipos próprios, entre eles, ptr8, ptr16 e ptr32, que são ponteiros, respectivamente, para palavras de um byte, dois bytes (16 bits) e quatro bytes (32 bits) e, como no caso dos objetos mem*, será necessário consultar a documentação do microcontrolador usado no projeto, para determinar qual usar para acessar seus periféricos.
Aplicação: sincronizando as saídas PWM da Raspberry Pi Pico
A inspiração para este artigo surgiu da necessidade de se sincronizar as saídas PWM de uma placa Raspberry Pi Pico para uma aplicação que exigia esse tipo de sincronismo.
O processador RP2040 usado na Pico tem oito blocos PWM, cada um com dois canais, conforme mostrado na figura abaixo, extraída do data sheet.[7][8][9]
Os dois canais A e B são controlados pelo mesmo contador e, portanto, sincronizados. Mas entre os blocos, esse sincronismo inexiste, mesmo que todos os contadores dos blocos estejam programados para gerar a mesma frequência de saída, e não há uma forma direta de fazer essa sincronização atuando sobre algum bit de um registrador de controle ou configuração.
O código abaixo inicia os oito blocos e ativa as saídas A, gerando ondas quadradas de 10kHz nos pinos da lista pins.
from micropython import const
from machine import Pin, PWM
FREQ = const(10000)
DUTY = const(2**15)
pins = (0, 2, 4, 6, 8, 10, 12, 14)
pwm = [PWM(Pin(i)) for i in pins]
[i.freq(FREQ) for i in pwm]
[i.duty_u16(DUTY) for i in pwm]
A figura abaixo apresenta as saídas A de dois blocos diferentes, demonstrando a diferença de fase entre elas.
Além da falta de sincronismo em si, a própria diferença de fase varia entre execuções sucessivas do script.
O código abaixo emprega a classe mem32 para acessar os registradores internos (o código é para prova de conceito apenas, numa implementação real, a criação de um módulo com uma classe seria mais apropriada).
from micropython import const
from machine import Pin, PWM, mem32
FREQ = const(10000)
DUTY = const(2**15)
PWM_CTR = const((
0x40050008, 0x4005001c, 0x40050030, 0x40050044,
0x40050058, 0x4005006c, 0x40050080, 0x40050094
))
GLOBAL_EN = const(0x400500a0)
GLOBAL_EN_SET = const(0x400520a0)
GLOBAL_EN_CLR = const(0x400530a0)
PWM_SET = const(0xFF)
pins = (0, 2, 4, 6, 8, 10, 12, 14)
pwm = [PWM(Pin(i)) for i in pins]
[i.freq(FREQ) for i in pwm]
mem32[GLOBAL_EN_CLR] = PWM_SET
for i in PWM_CTR: mem32[i] = 0
mem32[GLOBAL_EN_SET] = PWM_SET
[i.duty_u16(DUTY) for i in pwm]
No início são definidas várias constantes para facilitar a legibilidade do código. Os blocos são iniciados como no caso anterior. A diferença é que, antes de configurar o duty cycle, três operações são acrescentadas (linhas 18, 19 e 20 no código acima). Na linha 18, todos os blocos são desabilitados, isto é, seus contadores param e as saídas mantêm o estado prévio à desabilitação. Na linha 19, todos os contadores são zerados. E na linha 20, todos os blocos são novamente habilitados e os contadores recomeçam as contagens sincronizados.
O RP2040 tem um registrador que permite habilitar ou desabilitar todos os blocos simultaneamente; no código, o registrador é representado pela constante GLOBAL_EN. Cada bit desse registrador habilita um bloco PWM; ao escrever 0 nesse registrador, todos os blocos PWM são desabilitados; ao escrever 1 nos bits do registrador, o bloco PWM correspondente é habilitado (no exemplo, escrevendo 0xFF, os 8 blocos). No código foram usados os registradores GLOBAL_EN_CLR e GLOBAL_EN_SET, que permitem setar ou resetar atomicamente os bits do registrador GLOBAL_EN por meio de uma máscara de bits.
As figuras abaixo mostram o resultado da execução do código acima: note que as duas saídas estão sincronizadas. De fato, as oito saídas A dos blocos PWM ficam perfeitamente sincronizadas.
O código abaixo usa o Viper para fazer as mesmas operações. O resultado é idêntico ao apresentado acima.
from micropython import const
from machine import Pin, PWM
FREQ = const(10000)
DUTY = const(2**15)
PWM_CTR = const((
0x40050008, 0x4005001c, 0x40050030, 0x40050044,
0x40050058, 0x4005006c, 0x40050080, 0x40050094
))
GLOBAL_EN = const(0x400500a0)
GLOBAL_EN_SET = const(0x400520a0)
GLOBAL_EN_CLR = const(0x400530a0)
PWM_SET = const(0xFF)
pins = (0, 2, 4, 6, 8, 10, 12, 14)
pwm = [PWM(Pin(i)) for i in pins]
@micropython.viper
def init (pwm):
[i.freq(FREQ) for i in pwm]
ptr32(GLOBAL_EN_CLR)[0] = PWM_SET
for i in PWM_CTR: ptr32(i)[0] = 0
ptr32(GLOBAL_EN_SET)[0] = PWM_SET
[i.duty_u16(DUTY) for i in pwm]
init(pwm)
Os testes foram feitos com uma Raspberry Pi Pico com Micropython v1.19.1 (data de compilação 2022-06-18). Também foi feito um teste rápido com uma ESP-WROOM-32 (placa DEVKITV1) com a mesma versão, apenas para verificar a funcionalidade da classe mem32.
Conclusão
Este artigo apresentou algumas formas de acessar os registradores internos do processador hospedeiro do Micropython, para contornar limitações da implementação que imponham alguma restrição à aplicação pretendida para ele. A vantagem dessa abordagem é o uso de recursos já presentes na linguagem, para rápida implementação das funcionalidades faltantes de modo a se poder concentrar no desenvolvimento da aplicação em si e não da infraestrutura necessária.
Saiba Mais
- Video aulas: Programação da Raspberry Pi Pico com MicroPython
- MicroPython agora faz parte do ecossistema Arduino
- MicroPython: Python para microcontrolador
Referências
- [1] https://docs.micropython.org/en/latest/develop/cmodules.html
- [2] https://docs.micropython.org/en/latest/reference/asm_thumb2_index.html
- [3] https://docs.micropython.org/en/latest/develop/natmod.html#natmod
- [4] https://docs.micropython.org/en/latest/library/machine.html. Ver também: https://docs.micropython.org/en/latest/reference/speed_python.html#accessing-hardware-directly
- [5] https://docs.micropython.org/en/latest/esp32/tutorial/peripheral_access.html
- [6] https://docs.micropython.org/en/latest/reference/speed_python.html#the-viper-code-emitter
- [7] https://www.raspberrypi.com/documentation/microcontrollers/rp2040.html
- [8] https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
- [9] https://embarcados.com.br/o-microcontrolador-rp2040/









Obrigado por compartilhar