No último artigo foram descritos os procedimentos para criar módulos em linguagem C para encapsular as operações de acesso ao hardware. Mesmo com módulos simples, a aplicação fica estruturada, sendo cada módulo responsável por uma função específica.
A discussão levantada no final do último artigo foi em relação à portabilidade. Uma vez que a aplicação está dividida em módulos, por mais que a interface do módulo seja suficiente para abstrair as operações realizadas, ainda assim, existe a dependência do microcontrolador.
Neste artigo serão demonstrados alguns procedimentos que podem ser utilizados para tornar um módulo portável. O objetivo é estabelecer uma interface com funções bem definidas que abstrai o acesso ao hardware. A diferença em relação ao que foi demonstrado está justamente na definição da interface, que será estruturada de tal forma que a aplicação principal não necessitará conhecer detalhes dos recursos de hardware e periféricos que são utilizados no microcontrolador. Para tal, é indispensável conhecer as Técnicas de Mapeamento de Memória em Linguagem C.
Configurações do módulo
Primeiro passo
Para abstração do hardware é necessário criar os atributos de configuração do módulo. É importante ressaltar que essas características são dependentes do microcontrolador. No entanto, se você analisar as configurações de periféricos de diferentes microcontroladores certamente encontrará configurações em comum. Por exemplo, os terminais podem ser configurados na função de entrada ou saída digital. Funções específicas são adicionadas como extensão do módulo.
O trecho de código a seguir define as propriedades de um terminal do microcontrolador. A inclusão do arquivo dio_cfg_target expande as funções de configuração. Para tal, retomaremos o exemplo do último artigo que realizada a configuração do pino PB1 do microcontrolador ATtiny85 como saída digital.
#ifndef DIO_CFG_H_
#define DIO_CFG_H_
#include "dio_cfg_target.h"
/**
* Configurações do terminal.
*/
typedef enum
{
DIO_OUTPUT, /*< entrada */
DIO_INPUT, /*< saída */
DIO_PIN_DIR_MAX
}DioDirection_t;
/**
* Nível lógico do terminal para função de entrada ou saída digital.
*/
typedef enum
{
DIO_LOW, /**< Alto */
DIO_HIGH, /**< Baixo */
DIO_PIN_STATE_MAX
}DioPinState_t;
/**
* Configuração de um pino do microcontrolador
*/
typedef struct
{
DioDirection_t Direction; /**< Função do pino*/
DioPinState_t Data; /**< Estado*/
DioConfig_Target_t TargetDependent; /**< Configurações específicas*/
}DioConfig_t;
#ifdef __cplusplus
extern "C"{
#endif
const DioConfig_t * const Dio_GetConfig(void);
#ifdef __cplusplus
} // extern "C"
#endif
#endif /*DIO_CFG_H_*/
/*** End of File **************************************************************/
A implementação da função Dio_GetConfig será apresentada em breve. No momento é importante saber que tal função retorna a configuração dos pinos utilizados na aplicação.
Segundo Passo
Com os atributos definidos, o próximo passo é desenvolver a interface de acesso ao módulo, definindo as funções para utilização do recurso (periférico ou conjunto de periféricos).
#ifndef DIO_H_
#define DIO_H_
#include "target_cfg.h" /*Características dependentes do microcontrolador*/
#include "dio_cfg.h" /*Atributos do módulo DIO*/
#ifdef __cplusplus
extern "C"{
#endif
void Dio_Init(const DioConfig_t * const Config);
DioPinState_t Dio_ChannelRead(DioChannel_t Channel);
void Dio_ChannelWrite(DioChannel_t Channel, DioPinState_t State);
void Dio_ChannelDirectionSet(DioChannel_t Channel, DioDirection_t Mode);
#ifdef __cplusplus
} // extern "C"
#endif
#endif /*DIO_H_*/
/*** End of File **************************************************************/
Terceiro Passo
Os dois módulos apresentados são independentes do microcontrolador utilizado. Somente a implementação dessas funções é dependente.
As configurações específicas do módulo são mostradas abaixo.
#ifndef DIO_CFG_TARGET_H_
#define DIO_CFG_TARGET_H_
#include "target_cfg.h"
/**
* Capacidade do registrador
*/
#define MCU_DATA_TYPE uint8_t
/**
* Número de terminais por porta.
*/
#define NUMBER_OF_CHANNELS_PER_PORT 5U
/**
* Número de portas.
*/
#define NUMBER_OF_PORTS 1U
/**
* Quantidade de pinos que serão configurados
*/
#define NUMBER_OF_PINS 1
/**
* Macro para encontrar a porta que um pino pertence e a posição do pino no registrador.
*/
#define PORT_IDX(CHANNEL) (CHANNEL/NUMBER_OF_CHANNELS_PER_PORT)
#define PIN_IDX(CHANNEL) (CHANNEL%NUMBER_OF_CHANNELS_PER_PORT)
/**
* Lista de pinos do microcontrolador.
*/
typedef enum
{
PORTB_0,
PORTB_1,
PORTB_2,
PORTB_3,
PORTB_4,
PORTB_5,
DIO_MAX_PIN_NUMBER
}DioChannel_t;
/**
* Configurações específicas adicionadas no módulo principal
*/
typedef struct
{
DioChannel_t Channel; /**< ID do terminal */
}DioConfig_Target_t;
#ifdef __cplusplus
extern "C"{
#endif
#ifdef __cplusplus
} // extern "C"
#endif
#endif /*DIO_CFG_TARGET_H_*/
/*** End of File **************************************************************/
Quarto Passo
Na implementação do arquivo de configuração do módulo é criada uma tabela de configuração dos pinos utilizados. Essa tabela é utilizada na função de inicialização do módulo.
#include "dio_cfg.h"
static const DioConfig_t DioConfig[NUMBER_OF_PINS] =
{
{
.Direction = DIO_OUTPUT,
.Data = DIO_HIGH,
.TargetDependent = {
.Channel = PORTB_1
}
}
};
const DioConfig_t * const Dio_GetConfig(void)
{
/*retorna endereço da tabela de configuração dos pinos*/
return (const DioConfig_t * const)&DioConfig[0];*/
}
Quinto Passo
A última etapa corresponde à implementação das funções definidas na interface do módulo. Antes disso, cabe rever o conceito de Mapeamento de memória usando vetor de ponteiros.
Mapeamento de memória usando vetor de ponteiros
É uma técnica utilizada para agrupar um conjunto de registradores, relacionando-os a partir de um nome comum. Além de referenciá-los da mesma maneira, tais registradores também são agrupados por função. De modo geral, um vetor de ponteiros é criado para armazenar o endereço de cada registrador do módulo.
Por exemplo, todos os registradores de saída (no ATtiny85, PORTB) podem ser agrupados em um vetor. Uma vez que os registradores estão agrupados por funções, o código pode ser estruturado para criar módulos.
Por exemplo, no trecho de código abaixo os registradores foram agrupados em duas categorias, direção e valor de saída.
static MCU_DATA_TYPE volatile * const DataDirectionRegister[NUMBER_OF_PORTS] =
{
(MCU_DATA_TYPE * const)0x37, /*DDRB*/
};
static MCU_DATA_TYPE volatile * const DataRegister[NUMBER_OF_PORTS] =
{
(MCU_DATA_TYPE * const)0x38, /*PORTB*/
};
#define SET_BIT(REG, BIT) ((REG) |= (1U << BIT))
#define CLEAR_BIT(REG, BIT) ((REG) &= ~(1U << BIT))
#define TOGGLE_BIT(REG, BIT) ((REG) ^= (1U << BIT))
A função de inicialização configura todos os elementos definidos na tabela com base no registradores declarados dentro do módulo.
#include "dio.h"
void Dio_Init(const DioConfig_t * Config)
{
uint8_t i = 0;
for(i=0; i < NUMBER_OF_PINS; i++){
Dio_ChannelDirectionSet(Config[i].TargetDependent.Channel, Config[i].Direction);
Dio_ChannelWrite(Config[i].TargetDependent.Channel, Config[i].Data);
}
}
A função de inicialização utiliza a própria interface para configurar os pinos.
void Dio_ChannelWrite(DioChannel_t Channel, DioPinState_t State)
{
/*acessa registrador específico e configura o terminal indicado*/
if (State == DIO_HIGH)
{
SET_BIT(*DataOut[PORT_IDX(Channel)], PIN_IDX(Channel));
}
else
{
CLEAR_BIT(*DataOut[PORT_IDX(Channel)], PIN_IDX(Channel));
}
}
void Dio_ChannelDirectionSet(DioChannel_t Channel, DioDirection_t Mode)
{
/*acessa registrador específico e configura o terminal indicado*/
if(Mode == DIO_OUTPUT)
{
SET_BIT(*DataDirection[PORT_IDX(Channel)], PIN_IDX(Channel));
}
else
{
CLEAR_BIT(*DataDirection[PORT_IDX(Channel)], PIN_IDX(Channel));
}
}
Somente esta parte de código acessa o hardware. Isto é, as funções definem uma interface de acesso para um conjunto de recursos, já os recursos são configurados conforme o dispositivo.
A utilização dessa biblioteca é mostrada a seguir.
#include "dio_cfg.h"
#include "dio.h"
#define PIN_LED PORTB_1
int main()
{
const DioPortConfig_t *DioPortConfig = Dio_GetPortConfig();
Dio_InitPort(DioPortConfig);
while(1)
{
Dio_ChannelWrite(PIN_LED, DIO_LOW);
//delay...
Dio_ChannelWrite(PIN_LED, DIO_LOW);
//delay
}
}
Conclusão
Existem diversas técnicas que podem ser empregadas para fazer o mapeamento de memória. De modo geral, essas técnicas diferem nos seguintes aspectos [1]: densidade do código, tempo de execução, eficiência, portabilidade e grau de configuração.
A técnica apresentada neste artigo tem alguns impactos: diminui a densidade do código (aumenta o número de instruções) e acresce o tempo de execução [1]. No entanto, a portabilidade e grau de configuração dos periféricos aumentam substancialmente [1].
A aplicação destas técnicas dependerá do dispositivo utilizado, pois em alguns casos a quantidade de memória disponível é baixa. Assim, deve-se considerar esses fatores, balanceando portabilidade do código, facilidade de configuração (abstração do hardware) e dos recursos disponíveis.
Nesse exemplo, o módulo permitiu criar uma instância de cada pino, mas isso tem impacto na memória utilizada. É importante também citar que o módulo não fica limitado somente a essas funções. Operações para acessar o port completo e permitir operações mais rápidas (fast gpio) podem ser estendidas.
Por fim, as interfaces de acesso ao hardware podem ser utilizadas para implementação de drivers. Devido à interface fornecida pelo módulo, o driver pode ser criado e mantido para várias aplicações. Já a aplicação pode ser executada em microcontroladores diferentes, tendo que alterar somente a parte de acesso ao hardware.
Saiba mais
Criando uma biblioteca de acesso aos IOs da KL05
Técnicas de Mapeamento de Memória em Linguagem C
Princípio da Responsabilidade Única em Firmwares
Referências
[1] BENINGO, J. Reusable Firmware Development: A Practical Approach to APIs, HALs, and Drivers.Crédito da Imagem Destacada.










