Inter Intra #4.1 – Drivers, GPIO!

Olá pessoal! Como passaram por este ‘começo’ de ano? 🙈
 
Continuando a nossa série de artigos, hoje vamos elaborar o primeiro driver do nosso projeto, o driver de GPIO. Lembrando que na verdade não é um mas sim dois drivers, um para o STM32F10x e outro para o KL25!
Poxa Andre! Já estamos na metade do ano! 🙄
Até então falamos apenas sobre a arquitetura Inter e terminamos de preparar o nosso repositório. Vamos agora falar sobre os drivers e após terminarmos, poderemos entrar na tão aguardada arquitetura Intra!
 
O approach dos nossos artigos irá mudar um pouco, porque o conteúdo agora é mais prático. Vamos ter menos texto e vamos ter mais código… se prepare, gitHub! 😎

Atualizações no repositório!

Nosso repositório está carregado com todas as novidades para este artigo. As mudanças estão distribuídas em diversos commits, cada um contendo um pequeno grupo de entrega. Na última entrega coloquei a tag para este artigo, Inter_Intra_#4.1!
 
O que temos de novo:
  • Os drivers GPIO totalmente implementados.
  • Testes unitários para eles, rodando e passando.
  • Algumas pequenas novidades em alguns helpers.
 
Para a galera que está estudando o conteúdo mais minuciosamente, recomendo que clonem o repositório (caso ainda não o fizeram 🙃), estudem os arquivos, rodem os testes unitários, tentem entender o que foi feito e porque foi feito de tal maneira.
 
Quanto à galera que está apenas acompanhando os artigos, sem problemas, eu vou estar aqui passando pelos principais pontos dessas implementações e explicando os aspectos mais importantes! 👍
 
Vamos começar?

Nosso setup e as nossas necessidades

Vamos recapitular o que precisamos que os nossos drivers de GPIO façam.
 
Primeiramente, ainda não elaborei nada sobre o nosso setup do projeto, eu apenas mostrei algumas fotos no primeiro artigo. Deixe-me corrigir isso…
Nossas plaquinhas, montadas e aguardando o software.
Diagrama do nosso setup.
No diagrama acima estão identificados todos os pinos que vamos utilizar e quais funções de IO precisaremos de cada um deles. Basicamente utilizaremos os LEDs das próprias placas e apenas vamos colocar dois botões externos.
 
Com isso podemos ver que, no escopo dos GPIOs, vamos precisar de: 
  • Pino de LED, uma saída digital que pode ser nível alto ou baixo.
  • Pino de botão, uma entrada digital com um pull-up habilitado.
  • Pino de terra, uma saída digital, em nível baixo.
 
Relembrando a nossa filosofia de desenvolvimento: implementamos apenas o mínimo necessário, mas sempre sobre uma arquitetura que permite a adição de novas funcionalidades no futuro. 👍

SDKs são importantes!

Os drivers devem evitar ao máximo manipular diretamente os periféricos do microcontrolador mas sim utilizar de recursos fornecido pelo fabricante – um SDK, por exemplo. Vamos fazer exatamente isto aqui:
  • A STM fornece um ‘STM32F1xx_HAL_Driver‘.
  • A NXP possui um HAL, chamado por ela de FSL (aparentemente é uma abreviação de ‘Freescale’ mas divago👀)
 
As grandes vantagens de ir por este caminho:
  • Conseguimos colocar o driver para funcionar sem precisar saber e entender tanto do microcontrolador.
  • Os SDKs são feitas por equipes que manjam bastante dos seus dispositivos e já investiram boas horas trabalhando com eles. Assim, estas bibliotecas podem já contornar vários problemas conhecidos, itens de erratas, etc.
  • Reduzimos bastante a ‘escovação de bits’.
  • Para os testes unitários, é bem menos difícil 👀 fazer mocks de HALs/SDKs do que fazer dos periféricos do microcontrolador.
 
Eu já tinha anteriormente pego os arquivos destes SDKs e colocados eles em nosso repositório, aqui.

myDriverDefs.h

Uma coisa nova que fiz foi de adicionar um arquivo de cabeçalho novo nos diretórios de cada driver, que é chamado de myDriverDefs.h.
myDriverDefs.h no diretório do stm32f10x. No do KL25 também tem um!
A ideia ao criar este cabeçalho é de permitir que ele seja incluso por outras partes a fim de que suas definições sejam úteis para ‘amarrar algumas informações’.
 
O primeiro uso que temos para eles é de dar nomes para as portas e pinos de seus microcontroladores. Com isso, conseguimos nomear os pinos que as nossas aplicações vão usar para acionar os LEDs e lerem os botões. 
/** 
 * @brief Type that names the io ports that the device has. 
 */ 
typedef enum 
{ 
  myDriverPort_PA = 0, 
  myDriverPort_PB, 
  myDriverPort_PC, 
  myDriverPort_PD, 
  myDriverPort_PE, 
} myDriverPort_t;

/** 
 * @brief Type that names the io pins that the device has. 
 */ 
typedef enum 
{ 
  myDriverPin_00 = 0, 
  myDriverPin_01, 
  myDriverPin_02, 
  myDriverPin_03, 
  myDriverPin_04, 
  myDriverPin_05, 
  myDriverPin_06, 
  myDriverPin_07, 
  myDriverPin_08, 
  myDriverPin_09, 
  myDriverPin_10, 
  myDriverPin_11, 
  myDriverPin_12, 
  myDriverPin_13, 
  myDriverPin_14, 
  myDriverPin_15, 
} myDriverPin_t;
/** 
 * @brief Type that names the io ports that the device has. 
 */ 
typedef enum 
{ 
  myDriverPort_PTA = 0, 
  myDriverPort_PTB, 
  myDriverPort_PTC, 
  myDriverPort_PTD, 
  myDriverPort_PTE, 
} myDriverPort_t;

/** 
 * @brief Type that names the io pins that the device has. 
 */ 
typedef enum 
{ 
  myDriverPin_00 = 0, 
  myDriverPin_01, 
  myDriverPin_02, 
  myDriverPin_03, 
  myDriverPin_04, 
  myDriverPin_05, 
  myDriverPin_06, 
  myDriverPin_07, 
  myDriverPin_08, 
  myDriverPin_09, 
  myDriverPin_10, 
  myDriverPin_11, 
  myDriverPin_12, 
  myDriverPin_13, 
  myDriverPin_14, 
  myDriverPin_15, 
  myDriverPin_16, 
  myDriverPin_17, 
  myDriverPin_18, 
  myDriverPin_19, 
  myDriverPin_20, 
  myDriverPin_21, 
  myDriverPin_22, 
  myDriverPin_23, 
  myDriverPin_24, 
  myDriverPin_25, 
  myDriverPin_26, 
  myDriverPin_27, 
  myDriverPin_28, 
  myDriverPin_29, 
  myDriverPin_30, 
  myDriverPin_31, 
} myDriverPin_t;
Eu receio que isto deve ter ficado um pouco vago e até mesmo confuso para muitos de vocês mas prometo que vai ficar mais claro nos próximos artigos, quando formos elaborar os módulos das nossas aplicações. 🙏 Mas precisamos já disto pronto…

Detalhes das implementações

Vamos agora falar sobre o conteúdo dos arquivos dos drivers. Abaixo o link deles no repositório, caso queiram ir acompanhando:

Escolhendo os Includes

O primeiro ponto de atenção que gostaria de dar para as implementações é para a lista de includes:
/******************************************************************************* 
 *  INCLUDES 
 ******************************************************************************/ 
#include "myGpio.h" 
#include "myDriverDefs.h" 
#include "projConfig.h"

#include "stm32f1xx_hal.h"

#include "myAssert.h"

Lista de includes no STM32f10x


 

/******************************************************************************* 
 *  INCLUDES 
 ******************************************************************************/ 
#include "myGpio.h" 
#include "myDriverDefs.h" 
#include "projConfig.h"

#include "fsl_gpio.h" 
#include "fsl_port.h" 
#include "fsl_clock.h" 
#include "fsl_common.h"

#include "myMacros.h" 
#include "myAssert.h"
Lista de includes no KL25
Nos dois casos a lista se resume em três grupos:
  • Cabeçalhos básicos da nossa arquitetura.
  • Cabeçalhos dos SDKs (no STM temos apenas um, no KL temos quatro)
  • projConfig.h
 
Prestem atenção no projConfig.h. Este cabeçalho permitirá que a gente faça alguns ajustes no nosso driver dependendo do produto/projeto que vamos usar ele.
 
Este cabeçalho sempre deve estar disponível nos diretórios dos projetos dos produtos:
projConfig no Blinky, projeto st_bluepill.
projConfig no Blinky, projeto nxp_frdmkl25z.
Como usamos eles? Implementando o nosso driver de tal forma que algumas coisas sejam ajustáveis por #defines. Aí podemos declarar eles dentro dos projConfigs!
 
Por exemplo, estes nossos drivers vão possuir limite de quantos ‘clientes’ (pinos) eles vão suportar. Na seção de declarações privadas fazemos do jeito mostrado abaixo, criando uma definição DRIVER_GPIO_PIN_AMOUNT e deixando com um valor padrão.
 
Se existir o define no projConfig, usa o valor dado. Senão, usa este valor padrão (quatro, no caso):
/*******************************************************************************
 *  INCLUDES 
 ******************************************************************************/
#include "projConfig.h"

/*******************************************************************************
 *  PRIVATE DEFINITIONS
 ******************************************************************************/
/* Set below the maximum amount of pins that the driver can handle.           */
#ifndef DRIVER_GPIO_PIN_AMOUNT
  #define DRIVER_GPIO_PIN_AMOUNT                                               4
#endif
DRIVER_GPIO_PIN_AMOUNT só será declarado se ele não tiver sido antes.

Criando múltiplas instâncias

Os nossos drivers vão precisar gerenciar diferentes pinos, utilizados por diferentes clientes. Como fazemos essa organização? 🤔
 
Fazemos isso montando o driver com suporte para múltiplas instâncias, aonde cada instância é um cliente/pino. 👀
 
Primeiramente, identificamos quais dados vão ser específicos de cada instância e colocamos eles em uma estrutura:
/*******************************************************************************
 *  PRIVATE DEFINITIONS
 ******************************************************************************/
/* The structure below holds all the items related to a gpio pin instance.    */
typedef struct
{
  GPIO_TypeDef * GPIO;
  uint16_t pinMask;
} myGpioPinStruct_t;
Estrutura de instanciação no driver STM32F10x
/*******************************************************************************
 *  PRIVATE DEFINITIONS
 ******************************************************************************/
/* The structure below holds all the items related to a gpio pin instance.    */
typedef struct
{
  GPIO_Type * GPIO;
  uint32_t pin;
} myGpioPinStruct_t;
Estrutura de instanciação no driver KL25
Reparem que o conteúdo da estrutura é diferente para cada driver. 👍
 
A seguir, criamos um vetor desta estrutura, aonde cada posição armazenará os dados de uma instância. Adicionamos também uma variável contadora, para controlar qual é a próxima instância ‘vazia’:
/*******************************************************************************
 *  PRIVATE VARIABLES
 ******************************************************************************/
static myGpioPinStruct_t myGpio_Struct[DRIVER_GPIO_PIN_AMOUNT];
static uint32_t myGpio_NextPin = 0;
myGpio_Struct armazena os dados de cada instância
Reparem que aqui usamos aquela definição que mencionei antes, DRIVER_GPIO_PIN_AMOUNT. Vê como que ela limita quantos itens do vetor podemos ter e, assim, limita a capacidade do driver?
 
Quando myGpio_Init for chamado por um cliente:
  1. …uma posição deste vetor será alocada para ele,
  2. …seus dados serão escritos nesta posição,
  3. o endereço desta posição é a instância devolvida para o cliente! 👀👀👀
 
Na imagem abaixo destaco cada um destes momentos, no driver do KL25:
Criando uma instância no driver do KL25, myGpio_Init
No driver do STM32F10x o processo é basicamente o mesmo. Você consegue abrir o seu arquivo e identificar isto acontecendo? 💭

Usando dos SDKs

Vimos como o driver é configurado e como ele faz para suportar múltiplas instâncias. Para fechar este tópico, vamos ver como ele faz o que ele foi feito para fazer, a manipulação dos GPIOs dos microcontroladores.
 
Ele faz isso usando chamando as rotinas dos SDKs, enviando informações para elas e processando as suas respostas. No meio deste processo, o driver é encarregado de fazer as conversões de dados necessárias.
 
Dando um rápido exemplo, veja como que o driver do STM32F10x usa do HAL para mudar o nível de um pino:
myRet_t myGpio_Set(myGpioPin_t pin, myGpioLvl_t lvl)
{
  myRet_t result = myRet_Fail;
  myASSERT(pin != NULL);
  myASSERT(lvl <= myGpioLvl_Hi);

  if(pin != NULL)
  {
    myGpioPinStruct_t * strc = (myGpioPinStruct_t *) pin;
    GPIO_PinState state;

    if(lvl == myGpioLvl_Lo) { state = GPIO_PIN_RESET; }
    else                    { state = GPIO_PIN_SET;   }

    HAL_GPIO_WritePin(strc->GPIO, strc->pinMask, state);
    result = myRet_OK;
  }

  return result;
}
myGpio_Set, STM32F10x. Repare no HAL_GPIO_WritePin

Testes Unitários

Aqui entramos na parte mais complicada de drivers, a parte de… elaborar seus testes. 🙈
 
Aqui o diretório de testes para cada um dos drivers:
 
Os testes estão implementados e estão todos passando. Todos eles são relativamente simples e fáceis de entender. Assim não vou entrar em detalhes deles per si. Mas caso tenha alguma dúvida, entrem em contato comigo! ✌

Peraí Andre, mas você não disse que eles são complicados? 🤦‍♂️

Sim… eles são complicados, mas o difícil não são os testes, mas sim a preparação dos setups de testes.
 
Sabe por que isso? Dê uma passeada no conteúdo dos arquivos de um dos SDKs que usamos (belos exemplos aqui e aqui). 👀
 
Vai lá dar uma olhada, eu aguardo! 😅
 
Foi lá ver? Pois é, são de arquivos como estes que nossos drivers dependem para fazer os seus trabalhos. Para testar os drivers, precisamos fazer os mocks desses arquivos! 😯
 
Destacando alguns problemas deles:
  • A organização de suas declarações é [meio] bagunçada;
  • A cadeia de dependências é bem longa, alcançando diversos arquivos diferentes.
  • Algumas macros são… trechos de códigos inlined! Veja a macro abaixo, por exemplo, que usamos no nosso driver:
#define __HAL_RCC_GPIOA_CLK_ENABLE()   do { \ 
                                        __IO uint32_t tmpreg; \ 
                                        SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);\ 
                                        /* Delay after an RCC peripheral clock enabling */\ 
                                        tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);\ 
                                        UNUSED(tmpreg); \ 
                                      } while(0U)
Como testar que o nosso código chama algo assim? Lembrando que, para testar chamadas, precisamos de protótipos de funções… Da maneira que está, não dá!

Socorro! O que fazemos? 😣

É importante que nos lembremos que o nosso objetivo aqui é de apenas permitir que os nossos arquivos dos drivers sejam testáveis. Para isso, precisamos apenas de definições simples e de protótipos para as rotinas dos SDKs que nossas lógicas chamam. Todo o resto é descartável. Podemos usar de arquivos de SDK… ‘falsos‘. 💡
 
Assim, o que eu faço é o seguinte:
 
  1. Faço um trabalho exploratório, identificando todos os cabeçalhos do SDK que são realmente usados e o que do conteúdo deles realmente é necessário pelo driver.
  2. Faço uma cópia destes arquivos para o diretório de support dos testes unitários.
  3. Nestas cópias eu faço uma edição pesada, limpando tudo que não é usado pelo driver, removendo dependências que podem ser simplificadas, transformando algumas macros em protótipo de funções, etc.
 
As atividades acima tomam um bom tempo, já adianto! 😅 O lado bom é que, depois de feitas, se no futuro o driver precisar de algo novo do SDK, basta incrementar estes arquivos com a novidade e tudo deve ser bem rápido!
 
Enfim, este é um assunto muito muito muito longo ⏱ e definitivamente está fora do contexto desta série. Para os mais curiosos, os arquivos de SDK ‘falsos’ que fiz para o nosso projeto estão a seguir:
 
Estou à disposição para tirar quaisquer dúvidas e, se acharem interessante, posso elaborar algum material sobre isso. Aguardo seu feedback! 🙂

E com isso... temos os nossos primeiros drivers!

Neste artigo detalhei um pouco mais do nosso projeto e mostrei como montar alguns drivers de GPIO que, apesarem de serem básicos, atendem o nosso cabeçalho comum e possuem todas as propriedades de robustez que vamos precisar.
Estas implementações serão mais que suficientes para controlar as luzes dos nossos brinquedos e deixar os nossos pequenos clientes felizes!
No nosso próximo artigo vamos trabalhar em um driver um pouco mais complicado, o driver de timer!
 
Eae, o que acharam? Participem, entrem em contato, comentem, façam perguntas! Até a próxima! 👊