Inter Intra #4.2 – Drivers, Timers!

Vamos começar nosso último driver!

Na primeira parte deste capítulo vimos não só os drivers de GPIO mas também mostrei bastante coisa sobre outros detalhes de implementações dos drivers, tal como o uso dos SDKs, da estruturação de múltiplas instâncias e sobre qual é o maior problema de fazer testes unitários de drivers. 👀

Nesta segunda parte vamos ver o último driver necessário, o de timer. Vocês verão que, apesar da (óbvia 🙄) diferença de funções deste driver com o de GPIO, os detalhes da implementação são bem similares. Vai ser rapidinho!

Está na hora de montarmos nossos drivers de tempo! (que porcaria de trocadilho Andre 😆)
Qual a função de um driver timer? Basicamente, ele utiliza de algum periférico de tempo do microcontrolador para contar pedidos de tempo, avisando seus clientes quando esses períodos terminam. ⏱

Repositório atualizado!

Como sempre, temos novidades no nosso repositório, com o conteúdo fresquinho deste artigo, marcado na tag Inter_Intra_#4.2:
  • Os drivers de tempo totalmente implementados.
  • Testes unitários, rodando e passando.

O que nossos timers precisam suportar? 💭

Primeiramente vamos definir o que precisamos implementar para delimitarmos nosso trabalho para este projeto. Estes são os pontos importantes:
 

Quantidade de timers a serem suportados

Na segunda parte da nossa série definimos que o nosso projeto terá duas aplicações: uma para leitura do botão e outra para controle do LED.
 
Enquanto a primeira periodicamente vai ler uma entrada digital, a segunda irá periodicamente escrever em uma saída digital.
 
Assim, cada uma precisará de um timer distinto e, portanto, vamos precisar de dois timers. ✌
 

Contagem máxima

Quanto ao período de tempo, vamos nos lembrar que estamos desenvolvendo brinquedos com luzes piscantes. Elas não devem piscar muito rápido (alôoooo epilepsia 🙈) e muito menos devagar (alôoooo sono 😴).
 
Acho seguro estimar que estaremos geralmente trabalhando na casa de centenas de milissegundos. Portanto, vamos considerar que, no pior caso, nossos timers precisarão contar até 2000 milissegundos.
 
Usamos desta definição para definir, por exemplo, os valores de prescalers, a fim de que os contadores sejam grandes o bastante para suportar a contagem necessária.
 

Modo de operação

As operações das nossas aplicações serão periódicas, já que elas sempre estarão fazendo alguma atividade, de tanto em tanto tempo. Assim, nossos drivers também precisam apenas operar como Timers periódicos. Estes, depois de iniciados, irão repetidamente ficar contando o último pedido de tempo que receberam, sem precisar de mais nenhuma ação de seus clientes.
 
Também, notem que não precisaremos em nenhum momento parar estes timers, então não precisamos nos preocupar em implementar alguma rotina para isto agora. 🙏

Estes drivers são mais... 'chatinhos'! 😅

Estes drivers são bem mais complicados de implementar que GPIOs porque agora estamos entrando no domínio do tempo, com um pouco de real-time. Digo isso porque:
  • Os periféricos de tempo são complexos; eles suportam diferentes modos de uso e assim são mais difíceis de entender.
  • Tem uma certa matemática envolvida nestes drivers; é preciso calcular valores a serem colocados nos registros a fim de que a ‘tempística’ da operação seja a esperada.
  • Se paramos a execução de código (breakpoint, por exemplo) vamos afetar o resultado da operação. Assim, precisamos deixar a operação rodando e usar de algum instrumento (como um osciloscópio) para analisar as atividades…

Detalhes das implementações

Vamos para os detalhes! Como no outro artigo, abaixo os links para as implementações, para quem quiser ir acompanhando conforme explico:

Escolhendo os Includes

Não temos muito mistério nos includes, a mesma coisa dos drivers gpio: os cabeçalhos básicos, o projConfig e os cabeçalhos das APIs. 🙃

/*******************************************************************************
 *  INCLUDES
 ******************************************************************************/
#include "myTimer.h"
#include "projConfig.h"

#include "fsl_tpm.h"
#include "fsl_clock.h"
#include "fsl_common.h"
#include "myTimer_TPM.h"

#include "myAssert.h"
#include "myMacros.h"

Lista de cabeçalhos no driver KL25.

 

/*******************************************************************************
 *  INCLUDES
 ******************************************************************************/
#include "myTimer.h"
#include "projConfig.h"

#include "stm32f1xx_hal.h"

#include "myAssert.h"

Cabeçalhos do driver STM32F103.

 

Periféricos e dados dos usuários

Repare que estes drivers, diferentemente dos GPIOs, não permitem configurar o limite de clientes. Neste caso, os timers precisarão ser feitos de tal forma que cada cliente precisará ter seu próprio periférico de contagem de tempo, exclusivo. Assim, quantos clientes cada driver suportará dependerá de quantos periféricos estão disponíveis:
/*******************************************************************************
 *  PRIVATE DEFINITIONS
 ******************************************************************************/
typedef enum
{
  myTimer_TPM0 = 0,
  myTimer_TPM1,
  myTimer_TPM2,
  myTimer_TPM_Count, /* Not an item! For counting only.                       */
} myTimerTPMs_t;

/*******************************************************************************
 *  PRIVATE VARIABLES
 ******************************************************************************/
static myTimerStruct_t myTimer_Struct[myTimer_TPM_Count];
static myTimerTPMs_t myTimer_NextTPM = myTimer_TPM0;

Declarando periféricos disponíveis no KL25.

/*******************************************************************************
 *  PRIVATE DEFINITIONS
 ******************************************************************************/
typedef enum 
{
  myTimer_TIM3 = 0,
  myTimer_TIM4,
  myTimer_TIM_Count, /* Not an item! For counting only.                       */
} myTimerTIMs_t;

/*******************************************************************************
 *  PRIVATE VARIABLES
 ******************************************************************************/
static myTimerStruct_t myTimer_Struct[myTimer_TIM_Count];
static myTimerTIMs_t myTimer_NextTIM = myTimer_TIM3;

Declarando periféricos disponíveis no STM32F103 (temos dois, ufa! 😅).

Tem hora que o SDK não é suficiente...

Se olharem com mais cuidado a rotina myTimer_Start da implementação KL25, vocês verão que temos um impostor. Uma das rotinas aqui parece ser do SDK, mas não é! 🙈
myRet_t myTimer_Start(myTimer_t timer, uint32_t period, myCbk_t cbk)
{
  myRet_t result = myRet_Fail;

  if((timer != NULL) && (period != 0) && (cbk != NULL))
  {
    const uint32_t freq = CLOCK_GetOsc0ErClkFreq() / 128;
    const uint32_t counts = MSEC_TO_COUNT(period, freq);
    myTimerStruct_t * strc = (myTimerStruct_t *) timer;
    TPM_Type * const periph = strc->TPM;

    strc->cbk = cbk;

    TPM_StopTimer(periph);
    TPM_ClearCounter(periph);
    TPM_SetTimerPeriod(periph, counts);
    TPM_StartTimer(periph, kTPM_SystemClock);

    result = myRet_OK;
  }

  return result;
}
A rotina TPM_ClearCounter não existe no SDK, na verdade eu que fiz ela. Porque? 🤔
 
Se o timer já estiver trabalhando em uma contagem de tempo e o cliente vier e pedir uma nova contagem – um novo período de tempo, no caso – então é importante que o contador do timer recomece a contar a partir de zero, que é o valor inicial.
 
Eu analisei o código fonte do SDK e nenhuma das rotinas que eu utilizei (e nenhuma das outras disponíveis) fazia esta ‘limpeza do contador’, então eu precisei fazer isso na mão, com o nosso próprio driver fazendo este housekeeping:
void TPM_ClearCounter(TPM_Type * base) 
{ 
  myASSERT(base != NULL); 
  base->CNT = 0; 
}
Criei um novo submódulo, myTimer_TPM e coloquei esta rotina nele. Lembram do cabeçalho estranho na lista de includes? Olha ele aqui! 👀
 
Pontos importantes:
  • Eu propositalmente coloquei o nome da rotina seguindo o padrão do HAL para que na lógica principal os chamados continuem coerentes.
  • Coloquei esta nova rotina em um submódulo separado porque assim podemos fazer o mock dele quando for testar.
  • Também, desta maneira, em nenhum momento no código principal os periféricos dos TPMs são acessados.

E quando o tempo acabar?

Interrupções! 🙂
 
Quando os periféricos terminarem de contar o tempo eles irão levantar interrupções, aonde podemos tratar este eventos.
 
No caso do KL25 a operação é bem straightforward, basta descobrir quem que terminou de contar – basta ver qual interrupção que foi acionada – e chamar o seu callback.
/*******************************************************************************
 *  PRIVATE FUNCTIONS / ROUTINES
 ******************************************************************************/
static void myTimer_Interrupt(myTimerTPMs_t source)
{
  myTimerStruct_t * strc = &myTimer_Struct[source];
  const myCbk_t cbk = strc->cbk;

  TPM_ClearStatusFlags(strc->TPM, kTPM_TimeOverflowFlag);

  if(cbk != NULL) { cbk(); }
}

/*******************************************************************************
 *  INTERRUPT ROUTINES
 ******************************************************************************/
void TPM0_IRQHandler(void)
{
  myTimer_Interrupt(myTimer_TPM0);
}

void TPM1_IRQHandler(void)
{
  myTimer_Interrupt(myTimer_TPM1);
}

void TPM2_IRQHandler(void)
{
  myTimer_Interrupt(myTimer_TPM2);
}
Já no caso do STM32F103, precisamos chamar uma rotina de tratamento do HAL e esta que irá chamar a nossa lógica. 

Aqui não temos muito como escapar, temos que varrer os timers até encontrar qual que é o cliente relacionado:
/*******************************************************************************
 *  CALLBACK ROUTINES
 ******************************************************************************/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  myTimerTIMs_t thisTIM;
  bool foundTIM = false;

  for(thisTIM = 0; thisTIM < myTimer_TIM_Count; thisTIM++)
  {
    if(&myTimer_handle[thisTIM] == htim)
    {
      myTimerStruct_t * strc = &myTimer_Struct[thisTIM];
      const myCbk_t cbk = strc->cbk;

      foundTIM = true;

      if(cbk != NULL) { cbk(); }
      break;
    }
  }

  myASSERT(foundTIM);
}

/*******************************************************************************
 *  INTERRUPT ROUTINES
 ******************************************************************************/
void TIM3_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&myTimer_handle[myTimer_TIM3]);
}

void TIM4_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&myTimer_handle[myTimer_TIM4]);
}

Testes Unitários

Aqui o diretório de testes para cada um dos drivers:
 
Não vamos ter muito mistério:
  • Assim como no driver GPIO, o difícil aqui é preparar os arquivos de suporte a fim de que possamos fazer os mocks da API para testarmos os arquivos principais, mas…
  • … para os timers, eu já tinha colocado no repositório estes arquivos, já preparados! 😎
 
Desta maneira, basta escrever os arquivos de teste. Estão no repositório três deles para cada driver, aonde é testado a inicialização, o início da contagem dos tempos e o evento de fim de contagem dos timers.
/**
 * @file test_myTimer_Init.c
 * @brief Test file for testing timer driver logic, operation when
 *          myTimer_Init is called.
 */

/*******************************************************************************
 *  INCLUDES
 ******************************************************************************/
#include "unity.h"
#include "myTestDefs.h"

#include "myTimer.h"
#include "myDriverDefs.h"

#include "mock_fsl_tpm.h"
#include "mock_fsl_clock.h"
#include "mock_fsl_common.h"
#include "mock_myTimer_TPM.h"
Olha só o nosso impostor sendo mockado nos testes do KL25…

Terminamos os drivers! 🥳

Com o fim deste artigo temos os drivers prontos!
 
Agora, não só podemos contar tempo mas também conseguimos atuar nos pinos dos nossos microcontroladores conforme as necessidades das nossas aplicações.
 
Tudo está preparado para podermos implementar as aplicações dos nossos brinquedos – as suas regras de negócio – e seguirmos neste trabalho usando dos princípios da arquitetura Intra. ✌
Está na hora de falarmos da Intra!

Até a próxima! 👊

1 comentário em “Inter Intra #4.2 – Drivers, Timers!”

  1. Perfeito André, excelente conteudo que sigo acompanhando desde o inicio, aprendí demais e estou na expectativa da continuação abordando a Intra. Abs, Wagner

Não é possível comentar.