Design Patterns no acesso ao hardware
A característica mais marcante dos sistemas embarcados é que eles precisam acessar o hardware diretamente., uma vez que o objetivo de um sistema embarcado é que o software esteja incorporado em um “dispositivo inteligente” que forneça algum tipo específico de aplicacao, e isso exige acesso ao hardware. De forma geral, o hardware acessível por software pode ser categorizado em quatro tipos: infraestrutura, comunicações, sensores e atuadores.
O hardware de infraestrutura refere-se à infraestrutura de computação e aos dispositivos nos quais o software está sendo executado. Isso inclui não apenas a CPU e a memória, mas também dispositivos de armazenamento, temporizadores, dispositivos de entrada como teclados, botões e botões giratórios, dispositivos de saída para o usuário, como impressoras, displays e luzes, portas e interrupções.
De modo geral, uma configuração pode envolver a manipulação de apenas um bit ou de um conjunto de bits.
É extremamente comum ter hardware que usa campos de bits (bit fields) para especificar comandos para o hardware ou para retornar dados. Um campo de bits é um bloco contínuo de um ou mais bits dentro de um elemento de memória endereçável (por exemplo, um byte ou palavra) que, em conjunto, possui algum significado semântico para o dispositivo de hardware.
Por exemplo, um byte de 8 bits pode ser mapeado para um dispositivo de hardware em quatro campos diferentes:
0 0 0000 00
Os bits representam as seguintes informações em relacao ao dispositivo de hardware mapeado em memória:
| Bit range | Access | Name | Description |
|---|---|---|---|
| 0 | Write only | Enable bit | 0 = disable device, 1 = enable device |
| 1 | Read only | Error status bit | 0 = no error, 1 = error present |
| 2-5 | Write only | Motor speed | range 0000 = no speed to 1111 (16d) top speed |
| 6-7 | Write only | LED color | 00 (0d) = OFF, 01 (1d) = GREEN, 10 (2d) = YELLOW, 11 (3d) = RED |
Campos de bits são manipulados com os operadores bit a bit da linguagem C
& (AND bit a bit), | (OR bit a bit), ~ (NOT bit a bit), ^ (XOR bit a bit), >> (deslocamento à direita) e << (deslocamento à esquerda), em uma técnica chamada Ler-Modificar-Escrever (Read-Modify-Write), que, para efeito deste texto, será referido pelo seu termo em ingles.
A operacao Read-Modify-Write
A operacao read-modify-write é uma classe de operações atômicas, tais como test-and-set, fetch-and-add e compare-and-swap, que leem uma posição de memória e escrevem um novo valor nela simultaneamente, seja com um valor completamente novo ou com alguma função do valor anterior.
Qualquer instrução que especifique um registrador de arquivo como parte da instrução executa uma operação de read-modify-write (RMW). O registrador é lido, os dados são modificados e o resultado é armazenado no Registrador de Trabalho (Working register) ou no registrador de arquivo de origem, dependendo do estado do designador de destino. Uma operação de leitura é realizada em um registrador mesmo que a instrução escreva nesse registrador.
A operação de read-modify-write garante que se modifique apenas os bits específicos em um registrador do sistema que deseja alterar.
Bits individuais em um registrador do sistema controlam diferentes funcionalidades do sistema. Modificar os bits errados em um registrador do sistema pode fazer com que seu programa se comporte incorretamente.
Usando os operadores bitwise para acessar bits
Para realizar uma operação de read-modify-write em um registrador de microcontrolador em C, deve-se deve primeiro ler o valor atual, depois modificá-lo usando operadores bit a bit como & (AND), | (OR) e ^ (XOR) para alterar bits específicos, e finalmente escrever o novo valor de volta no registrador. Essa sequência é crucial porque alguns registradores, especialmente portas de I/O, retornam um valor baseado no estado do pino, e não no último valor escrito; portanto, atribuir um novo valor diretamente pode não funcionar como esperado.
A maioria das máquinas oferece uma variedade de operações para manipular bits individuais de uma palavra ou outros elementos endereçáveis, frequentemente chamadas de “bit twiddling”, baseadas em operações booleanas.
Além das operações lógicas bit a bit, a maioria das máquinas oferece diversas funções de deslocamento e rotação. A operaçao mais básica é o deslocamento lógico, onde os bits de uma palavra são deslocados para a esquerda ou para a direita. Em uma extremidade, o bit deslocado para fora é perdido. Na outra, um zero é inserido. Deslocamentos lógicos são úteis principalmente para isolar campos dentro de uma palavra.
Quando se faz programação de baixo nível, muitas vezes é preciso armazenar informações como bits individuais ou coleções de bits. Usando os operadores bit a bit, pode-se extrair ou modificar dados armazenados em um pequeno número de bits.
O procedimento é então realizado da seguinte maneira:
Passo 1: Ler o valor atual
Significa obter o valor atual do registrador em uma variável temporária, por meio da criacao de um ponteiro para o endereço de memória do registrador e desreferenciando-o:
// Define the register address and type
volatile unsigned int *register_ptr = (volatile unsigned int *)0xXXXXXXXX; // Replace with the actual address
// Read the current value
unsigned int temp_value = *register_ptr;
Passo 2: Modificar o valor
Isso é feito utilizando uma das várias operações bit a bit em C.
Definindo um bit
Suponha que se queira definir o bit 4 em i (vamos assumir que o bit mais à esquerda — ou mais significativo — é numerado como 15, e o menos significativo como 0).
A forma mais simples de definir o bit 4 é fazer um OR do valor de i com a constante 0x0010 (uma “máscara” que contém um bit 1 na posição 4):
i = 0x000; /* is is 0000000000000000*/
i |= 0x0010; /* i is now 000000000010000*/
i |= 1 << j; /* sets bit j */
Zerando um bit
Para zerar o bit 4 de i, usamos uma máscara com um bit 0 na posição 4 e bits 1 em todas as outras posições:
i = 0x00ff; /* i is now 0000000011111111 */
i &= ~0x0010; /*i is now 0000000011101111 */
Usando a mesma ideia, podemos facilmente escrever uma instrução que limpa um bit cuja posição está armazenada em uma variável:
i &= ~(1 << j); /* clears bit j*/
Testando um bit
A instrução if testa se o bit 4 de i está definido (set):
if(i & 0x0010) /* tests bit 4 */
To test whether bit j is set, we’d use the following statement:
if(i & 1 << j) ... /* tests bit j */
Toggle um bit
Fazer o toggle de um bit significa alternar o seu valor, invertendo-o:
– Se o valor do bit é zero, seu valor se torna 1.
– Se o valor do bit é 1, ele se torna zero.
Em C, a operação usada para alternar (inverter) um bit é o XOR (^) com uma máscara:
// Define the mask for the bit you want to toggle (e.g., bit 3)
#define TOGGLE_MASK (1 << 3)
temp_value = temp_value ^ TOGGLE_MASK;
Passo 3: Escrever o novo valor
Escreve o valor modificado de volta ao mesmo registrador.
*register_ptr |= temp_value;
Um exemplo completo desses passos pode ser resumido no seguinte exemplo, onde o bit 5 de um registrador, cujo endereço é 0x40001000, é definido:
// Assume you want to set bit 5 of a register at address 0x40001000
volatile unsigned int *gpio_port_b = (volatile unsigned int *)0x40001000;
unsigned int mask = (1 << 5);
// Read, modify, and write
*gpio_port_b = *gpio_port_b | mask;
Um registrador mantém uma determinada configuração, e qualquer modificação resulta em uma nova configuração. Em termos gerais, esta é a técnica usual para manipular bits em um microcontrolador.
Registradores Read-only ou write-only
Alguns registradores, ou bits dentro de um registrador, podem ser apenas de leitura (read-only) ou apenas de escrita (write-only). Para registradores apenas de escrita, operações de read-modify-write, como |=, &= e ^=, não podem ser usadas. Nesse caso, uma cópia sombra (shadow copy) do conteúdo do registrador deve ser mantida em uma variável na RAM para preservar o estado atual do registrador apenas de escrita.
Exemplo de um registrador apenas de escrita usando uma shadow copy:
timerRegValue
que segue:
/* initiaise timer write-only register */
timerRegValue = TIMER_INTERRUPT;
*pTimerReg = timerRegValue
Após a shadow copy e o registrador do temporizador terem sido inicializados, as escritas subsequentes no registrador são realizadas primeiro modificando a shadow copy timerRegValue e, em seguida, escrevendo o novo valor no registrador. Por exemplo:
timerRegValue |= TIMER_ENABLE;
*pTimerReg = timerRegValue;
Bit fields
Um campo de bits (bit field) é um campo de um ou mais bits dentro de um valor inteiro maior. Campos de bits são úteis para manipulação de bits e são suportados dentro de uma struct pelos compiladores da linguagem C.
struct
{
uint8_t bit0 : 1;
uint8_t bit1 : 1;
uint8_t bit2 : 1;
uint8_t bit3 : 1;
uint8_t nibble : 4;
} foo;
Os bits dentro de um campo de bits (bitfield) podem ser individualmente definidos, testados, limpos e alternados sem afetar o estado dos outros bits fora do campo de bits.
Para testar bits usando o campo de bits, a estratégia é a seguinte:
if(foo.bit0)
{
//do something
}
if(foo.nibble == 0x03)
{
//do something else
}
Definir um bit usando o bitfield é feito da seguinte forma:
foo.bit1 = 1;
e a estratégia para definir múltiplos bits em um bitfield é a seguinte:
foo.nibble = 0xC;
Para zerar um bit usando o bitfield, é necessário fazer:
foo.bit2 = 0;
e para fazer o toggle de um bit usando o bitfield:
foo.bit3 = ~foo.bit3; // or !foo.bit3;
Existem alguns problemas com os bit fields em C. Primeiro, a ordem dos bits depende do compilador e do processador. Alguns compiladores começam pelo bit menos significativo, enquanto outros começam pelo bit mais significativo. Em alguns casos, o compilador pode exigir que o campo de bits seja incluído dentro de uma union; fazer isso torna o código de campos de bits portátil entre compiladores ANSI C. Além disso, o compilador pode aplicar regras de padding de bytes, o que significa que o mapeamento em um dispositivo mapeado em memória pode não ocorrer como esperado.
Pior ainda, como a maioria das CPUs precisa escrever um byte ou uma palavra por vez, campos de bits podem não ser escritos em um passo atômico, levando a problemas de thread safety se mutexes separados forem usados para diferentes campos de bits.
Outro problema potencial ao usar campos de bits é que é impossível fazer cast entre um escalar e uma struct definida pelo usuário. Portanto, o seguinte não é permitido:
typedef struct _statusBits
{
unsigned enable : 1;
unsigned errorStatus : 1;
unsigned motorSpeed : 4;
unsigned LEDColor: 2;
} statusBits;
statusBits status;
unsigned char f;
f = 0xF0;
status = (statusBits) f;
Máscaras de bits (bitmasks) são mais eficientes do que campos de bits (bitfields) em certas situações. Especificamente, uma máscara de bits geralmente é uma forma melhor de inicializar vários bits em um registrador.
Definir e limpar bits usando um campo de bits não é mais rápido do que usar uma máscara de bits; em alguns compiladores, pode ser até mais lento utilizar um campo de bits. Um benefício de usar campos de bits é que campos individuais podem ser declarados como volatile ou const. Isso é útil quando um registrador é gravável, mas contém alguns bits apenas de leitura.
Operacoes Bit-band
O bit-banding foi introduzido na arquitetura ARM Cortex-M principalmente para resolver desafios comuns em sistemas embarcados, onde desempenho em tempo real, eficiência de memória e acesso seguro a recursos compartilhados são críticos. Embora o bit-banding ofereça várias vantagens, ele também apresenta algumas desvantagens e limitações, especialmente em termos de flexibilidade e praticidade em certas situações.
Não há suporte nativo para operações de bit-band nas linguagens C/C++. Por exemplo, compiladores C não entendem que a mesma memória pode ser acessada usando dois endereços diferentes, e não sabem que acessos ao alias do bit-band irão acessar apenas o bit menos significativo (LSB) da posição de memória.
Para usar o recurso de bit-band em C, a solução mais simples é declarar separadamente o endereço original e o alias do bit-band de uma posição de memória.
Para modificar um bit pelo método de bit-banding, precisamos calcular o endereço do alias e o endereço do banding desse bit.
O bit-banding oferece uma solução permitindo a manipulação atômica de bits individuais sem a necessidade de copiar dados para registradores da CPU. Isso garante que operações em nível de bit sejam seguras, eficientes e determinísticas, algo essencial para desempenho em tempo real. Ele ajuda a prevenir condições de corrida que poderiam ocorrer quando múltiplas tarefas tentam modificar o mesmo bit simultaneamente.
Tradicionalmente, para garantir que modificações em nível de bit (como definir uma flag ou alternar um pino GPIO) sejam feitas de forma segura, sem interrupções (por exemplo, por uma interrupção ou outra tarefa), os desenvolvedores usavam seções críticas — desabilitando temporariamente interrupções ou bloqueando o acesso. No entanto, essa abordagem aumenta a complexidade e pode afetar o comportamento em tempo real, introduzindo atrasos ou inversões de prioridade.
O bit-banding elimina a necessidade de seções críticas para manipulações de bits, permitindo que bits individuais sejam definidos ou limpos de forma atômica em um único ciclo. Isso ajuda a manter o desempenho em tempo real sem os problemas de desabilitar interrupções ou usar locks.
Para usar bit-banding, é necessário calcular manualmente o endereço do alias de bit-band. Isso envolve aplicar uma fórmula específica para derivar o endereço do alias, o que pode adicionar complexidade e potencial para erros no código. Para aplicações simples, isso pode não ser um problema, mas em sistemas mais complexos, os cálculos podem se tornar trabalhosos, especialmente quando muitos bits diferentes precisam ser acessados em diversos endereços.
Bit-banding nem sempre é a solução mais eficiente. Para operações simples em bits individuais dentro de uma palavra, operações bit a bit tradicionais (como AND, OR, XOR) podem ser mais eficientes em termos de legibilidade do código, tamanho e desempenho. Por exemplo, limpar ou definir múltiplos bits de uma vez pode ser mais fácil e rápido usando uma operação bit a bit direta, evitando a necessidade de calcular e acessar múltiplos endereços de alias de bit-band. O bit-banding se destaca quando operações atômicas em um único bit são necessárias, mas se a atomicidade não for exigida, o overhead de usar bit-banding pode não ser justificado.
A vantagem da otimização de memória vem acompanhada das seguintes desvantagens: como o bit-banding não é diretamente suportado pela maioria das linguagens de alto nível como C ou C++, os desenvolvedores precisam calcular manualmente o endereço do alias ou usar macros personalizadas para realizar o bit-banding. Isso adiciona etapas extras ao processo de desenvolvimento, aumentando a complexidade do código e o esforço de manutenção.
Se o projeto envolve manipular muitos bits diferentes em diversas posições de memória, gerenciar todos os endereços de alias de bit-band pode se tornar um fardo. Cada bit requer um cálculo do endereço de alias, o que pode tornar o código mais confuso e dificultar a manutenção e depuração em aplicações grandes. Além disso, o bit-banding não fornece um mecanismo direto de feedback do hardware. Ele funciona silenciosamente, o que significa que depurar problemas relacionados ao bit-banding pode ser mais desafiador, já que não há um indicador de status incorporado.
Outra limitação é que o bit-banding não está presente em todas as arquiteturas ARM, sendo suportado apenas pelo Cortex-M3 e Cortex-M4. Isso limita a portabilidade do código para outras arquiteturas, reduzindo sua atratividade para desenvolvedores que trabalham em múltiplas plataformas e necessitam de portabilidade.
E o meu Arduino?
Como as placas Arduino são projetadas em torno de um microcontrolador bem estabelecido, seja das antigas versões da ATMEL ou dos fornecedores mais recentes como STM32 e Renesas, as mesmas regras descritas anteriormente se aplicam, especialmente quando o programador está escrevendo código bare-metal em uma das linguagens de programação orientadas a hardware, como C, C++ ou Assembly.
No entanto, a biblioteca Wiring fornece algumas funções que encapsulam essa complexidade.
Usando a função
bitSet()
Define (escreve um 1 em) um bit de uma variável numérica em uma posição específica. Útil ao fazer manipulação de bits de baixo nível, especialmente ao trabalhar com registradores de hardware, flags ou I/O mapeado na memória.
Sintaxe:
Use a seguinte função para definir o estado do bit na posição n da variável x:
bitSet(x, n)
onde os parametros sao:
x : the numeric variable whose bit to set.
n: which bit to set, starting at 0 for the least-significant (rightmost) bit.
e retorna o valor da variável numérica depois que o bit na posição n é definido.
Por trás dos panos, a função faz o mesmo truque de sempre:
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
Código de exemplo:
Modifique um determinado byte x transformando o seu 5º bit em 1:
uint8_t x = 0b10000001; // initial byte
void setup() {
Serial.begin(9600);
int index = 5; // index of the bit to modify
x = bitSet(x, index-1);
Serial.print("The resulting byte is: ");
Serial.println(x, BIN);
}
void loop() {
}
Por que usar operações bit a bit em vez de uma simples atribuição em C?
Usar operações bit a bit em vez de uma simples atribuição em C é essencial quando você trabalha com registradores de hardware, flags ou estruturas em que cada bit tem sua própria função.
Usar o operador de igualdade = sobrescreve todo o valor do registrador, o que significa que essa operação define todos os bits para os valores especificados na instrução. Isso inclui tanto os bits que precisam ser configurados intencionalmente quanto aqueles que não precisam ser alterados.
Você substitui completamente o conteúdo do registrador.
Isso é perigoso porque:
- Bits reservados devem manter valores específicos.
- Alguns bits têm semânticas especiais (W1C, W1S, etc.).
- Pode-se alterar acidentalmente flags que não pretendia tocar.
Se o valor escrito corresponder ao valor padrão documentado, isto é,
REGISTER = 0;
//se o valor default é zero
então esses bits efetivamente voltarão ao valor padrão.
O hardware não “retorna ao valor padrão por conta própria” só porque o operador de igualdade foi usado.
É essencial construir a máscara completa do registrador com todos os campos/bits configurados para os valores desejados (incluindo os “reservados”, conforme instruído pelo datasheet).
Operações bit a bit permitem que você altere apenas certos bits, por exemplo
REG |= MASK;
Define apenas os bits em 1 na máscara e mantém o restante do registrador intacto.
REG &= ~MASK;
Ele limpa apenas os bits especificados e não afeta os demais.
Isso é exatamente o que você quer quando um registrador possui múltiplos campos com funções diferentes.
Usar |= realiza uma operação OR cumulativa: define como 1 apenas os bits que você coloca como 1 na máscara e deixa todos os outros bits inalterados. Isso é útil para configurar bits sem afetar os demais e preserva os valores anteriores nos bits não mencionados.
Frequentemente, é necessário combiná-lo com &= ~mask para limpar bits específicos. O padrão seguro geralmente é o “read–modify–write”:
R = REG;
R &= ~MASK_CLEAR;
R |= MASK_SET;
REG = R.
Isso garante que:
- só se modifica os bits pretendidos;
- todos os bits reservados permanecem corretos;
- o comportamento documentado é respeitado.
Definir um bit de registrador usando = em vez de |= é útil quando:
- Se quer definir o estado inteiro do registrador de uma vez: – = sobrescreve todos os bits, enquanto |= apenas define bits específicos sem limpar os outros.
- Inicialização – Ao configurar um periférico pela primeira vez, o ideal é colocá-lo em um estado conhecido:
DDRD = 0x10; // Set PD4 as output, all others as input
- Operações atômicas em múltiplos bits – Quando se quer alterar vários bits simultaneamente:
PORTD = 0x18; // Set PD3 and PD4 high, clear all others
Exemplo: configuração e redefinição de bits no registrador BSRR em STM32.
- Limpar bits indesejados – Se outros bits puderem estar definidos e precisam ser limpos:
PORTD = 0x10; // Only PD4 high, ensures all others are low
- Desempenho – Uma única atribuição é mais rápida do que read-modify-write (|= exige a leitura do valor atual primeiro).
Use = quando precisar de controle completo sobre o estado do registrador ou não se importar com outros bits. Apenas tome cuidado para não limpar acidentalmente bits dos quais outras partes do seu código dependem!
Muitos registradores exigem padrões de escrita específicos. Em microcontroladores/SoCs, é comum ter:
- W1C (escrever 1 para limpar)
- W1S (escrever 1 para definir)
- RC (ler-limpar)
- WO (somente escrita)
- Bits que ignoram 0 ou 1, etc.
Uma atribuição simples (=) pode:
- limpar flags quando essa operacao não for a que se pretende,
- acionar operações não intencionais,
- causar comportamento indefinido.
O padrão seguro é quase sempre o read-modify-write.
Usar operações bit a bit é essencial porque elas:
- evitam sobrescrever bits indesejados
- respeitam semânticas de escrita especiais
- mantêm bits reservados intactos
- evitam comportamento indefinido
- implementam o padrão correto recomendado pelos fabricantes
Uma atribuição simples só deve ser usada quando se deseja escrever o registrador inteiro, com todos os bits definidos exatamente como especificado.
Referências
K.N. King
C Programming – A Modern Approach
Second edition
Stallings, William.
Computer Organization and Architecture – Designing for performance
Tenth edition
Douglass, Bruce Powel
Design Patterns for Embedded Systems in C – An Embedded Software Engineering Toolkit
Barr, Michael & Massa, Antony
Programming Embedded Systems
With C and GNU Development Tools
Second Edition
https://docs.arduino.cc/language-reference/en/functions/bits-and-bytes/bitSet/
https://bit-by-bit.gitbook.io/embedded-systems/perifericos-mapeados-em-memoria/configuracao-de-registradores
https://docs.arduino.cc/retired/hacking/software/PortManipulation/
https://developer.arm.com/documentation/dui0379/e/writing-arm-assembly-language/the-read-modify-write-operation
https://onlinedocs.microchip.com/oxy/GUID-08F34A7C-AE69-443E-863E-D5ED2FFA578F-en-US-4/GUID-30E8B6B3-92CB-46EB-ADE7-541724D6C1FF.html
https://medium.com/@levinet.nicolai/strengths-and-weaknesses-for-bit-banding-in-arm-cortex-m-58217cd12260
