Conversor SATA-USB

Estou com alguns HDs velhos, alguns são externos (com case) e outros são internos (sem case), e quero fazer uma limpa neles. Eu queria algo prático e rápido de ligar os outros HDs no computador.

Os HDs externos já tem um conversor SATA-USB neles. Para os HDs internos, é possível comprar um case, porém não muito prático ficar trocando o HD do case, também não quero comprar um carrinho de mão deles… A boa é que existem conversores SATA-USB como o abaixo, que liga direto na porta SATA do HD.

Simples e prático.

Conversor SATA-USB Montado

Porém nada acontece ao ligar o HD no computador através deste conversor.

Com o comando abaixo tentei verificar os dispositivos conectados recentemente e nada.

$ ls -lrt /dev/

Ao desconectar tudo que percebi que o conversor estava razoavelmente quente.

Abri ele, conectei apenas o conversor no USB do computador e constatei que o CI U2 (o maior CI, no centro da imagem abaixo) está esquentando muito. Ele fica tão quente que não dá pra manter o dedo em cima dele.

CIs:

  • U1 P25Q21H – Puya Semiconductor – Serial Flash Memory
  • U2 VL711S-Q4 – VLI – USB 3.0 to SATA 6Gb/s Bridge Controller

Frente da Placa do Conversor SATA-USB

A pesar do problema citado, a placa parece bem desenhada. Pode-se ver pelo cuidado que tiveram ao criar as trilhas diferenciais, mesmo em uma placa dupla-face.

Porém a soldagem deixa um bocado a desejar:

Na frente da placa (acima) podemos ver pedaços longos de fio desencapados e tocados pelo ferro de solda. Estes podem causar curto-circuitos nos fios. No verso vemos o mesmo.

No verso da placa (abaixo) também podemos ver os dois pinos de suporte do conector SATA (em cima e embaixo) sem solda alguma.

Por último, porém não menos agonizante, apenas um dos três pintos do conector 12 V está bem soldado. O segundo faz conexão, porém a solda não fluiu por todo pino e o pino do meio está sem solda alguma.

Verso da Placa do Conversor SATA-USB

Testando se um aquivo é ASCII

Surgiu a necessidade de verificar se um arquivo tem apenas caracteres ASCII.

O código abaixo resolve esta problema. Para usá-lo é muito simples: basta passar o nome do arquivo que deseja testar.

  • teste_ascii.py README.md
  • teste_ascii.py graph.m
#!/usr/bin/env python3
# Arquivo: teste_ascii.py
import sys

if len(sys.argv) != 2:
    print(f"Uso: {sys.argv[0]} ARQUIVO", file=sys.stderr)
    sys.exit(1)

# Abre o arquivo passado na linha de comando em modo binário
# e lê o conteúdo do arquivo.
fp = open(sys.argv[1], 'rb')
x = fp.read()

# Decodifica o conteúdo do arquivo para ASCII.
# Se houver algum caractere que não é ascii uma exceção será
# lançada.
x.decode('ascii')

Como testar pendrive e cartão SD no Linux

Uma situação extremamente chata é comprar um pendrive ou cartão SD falsificado.

Você coloca arquivos nele e, ao ler os dados de volta, estes estão corrompidos.

Comprei 4 pendrives de 16 GB e, para evitar essa situação inconveniente, quero testar se eles realmente possuem esse tamanho ou se são falsificados.

Para esses testes, geralmente crio um arquivo grande com dados aleatórios (de 128 MB até 1 GB) e copio ele várias vezes para dentro do pendrive e em seguida verifico se a checksum de cada um dos arquivos bate com a original.

Como testar pendrive e cartão SD no Linux

Testando 4 pendrives de uma vez com ZFS

Só que como dessa vez eu tenho 4 pendrives para testar, optei por fazer um pouco diferente.

Para este teste resolvi usar o sistema de arquivos ZFS.

O ZFS automaticamente guarda checksums de todos os arquivos armazenados e, ao lê-los valida essas checksums. Além disso, há um comando que realiza a verificação de todos os dados gravados.

Então, testar usando o ZFS me poupa o trabalho de manualmente criar checksums e de manualmente verificá-las. Basta gravar arquivos até encher os pendrives e verificar os dados.

Muito cuidado: é possível corromper o seu disco principal com os comandos utilizados.

Instalando as dependências

Realizei o teste no Ubuntu Linux 22.04.

Acredito que a única dependência sejam os utilitários do ZFS.

sudo apt install zfsutils-linux

Formatando

Primeiro listamos os discos.

$ ls -1rt /dev/sd*
> /dev/sda
> /dev/sdb
> /dev/sdc
> /dev/sdd
> /dev/sde

Para descobrir quais dos discos são os pendrives listamos os discos com o programa parted, filtrando os dados de interesse com o comando grep.

$ sudo parted -l | grep '/dev/sd'
> Disk /dev/sda: 1000GB
> Disk /dev/sdb: 15,7GB
> Disk /dev/sdc: 15,7GB
> Disk /dev/sdd: 15,7GB
> Disk /dev/sde: 15,7GB

Dica: Fique atento a qual é o seu disco principal. No meu caso é /dev/sda.

Vamos usar o ZFS para criar uma ZPOOL (conjunto de discos). Criamos uma ZPOOL chamada "teste" com os 4 pendrives em stripes (listras?).

$ sudo zpool create teste sdb sdc sdd sde

Dica: Não adicione o seu disco principal na ZPOOL.

Os pendrives já estavam formatados com FAT, então o ZFS se negou a criar a ZPOOL, o que causaria a perda de dados. Bastou adicionar -f no comando para forçar a criação, pois não tenho nada gravado neles.

Para melhorar um pouco a performance do teste configuramos as seguintes opções:

$ sudo zpool trim teste --wait
$ sudo zfs set atime=off teste
$ sudo zfs set sync=disabled teste
$ sudo zfs set compression=off teste

Sobre as opções habilitadas

Executar um "trim" no sistema de arquivos avisa o pendrive de todo o espaço livre, permitindo utilizá-lo de forma mais efetiva. Esse passo vai ser repetido no fim do teste. É possível que a operação de trim não seja suportada pelo pendrive ou cartão SD.

Além do trim, a única opção que recomendo usar na prática com ZFS é atime=off. Isso evita atualizar a data de acesso a cada leitura. Não tem porque transformar uma leitura em uma leitura-e-escrita, por isso o uso dessa opção é comum e recomendada.

As outras duas opções é só pra deixar o teste mais rápido e podem ter consequências indesejadas.

A opção sync=disabled, do ponto de vista da aplicação, pode afetar seriamente a consistência dos dados gravados. A maioria das aplicações, precisam que todas as gravações sejam síncronas: "só quero continuar quando os dados estiverem efetivamente gravados no disco." Essa opção mente para estas aplicações, tornando todas as gravações assíncronas.

Na prática, recomendo usar sync=standard, que deixa a aplicação escolher entre síncrono e assíncrono.

Já a opção compression=off serve para não comprimir os dados no disco.

Sim, outra vantagem do ZFS é que ele pode comprimir dados, automaticamente economizando o precioso espaço do disco. Porém, neste teste, compactar seria contra-produtivo porque queremos preencher o disco completamente e então varrê-lo em busca de erros.

Na prática, recomendo usar sync=lz4, que compacta muito bem as sequências de zero, que são muito comuns, e arquivos de texto.

Gravando

Uma coisa muito legal do ZFS é que ele monta tudo automaticamente. Como acabamos de criar uma ZPOOL, é óbvio que vamos utilizá-la e por isso ela já é montada pelo próprio ZFS.

Então basta logar como root (usando sudo), entrar na pasta do ZPOOL e começar a escrever arquivos.

$ sudo -i
$ cd /teste

Os arquivos que vamos escrever são arquivos aleatórios (lidos de /dev/urandom). Criamos uma variável COUNT, que conta o número de arquivos criados e, dentro de um laço while criamos arquivos de 128 MB utilizando o comando dd. Caso o comando dd falhe (ex: por falta de espaço em disco) o comando break pára o laço e aguardamos o sistema operacional sincronizar com o comando sync. Por fim, também já iniciamos o scrub (varredura) nos discos.

# Execute como root
COUNT=0
while :; do
    dd if=/dev/urandom of=file-$COUNT.bin bs=1M count=128 || break
    COUNT="$(($COUNT + 1))"
done
sync
zpool scrub teste

Para ver o andamento da gravação podemos usar os comandos abaixo. Eu prefiro o penúltimo.

$ ls -lh /teste
$ zfs list teste
$ zpool iostat teste 5
$ zpool iostat teste 5 -v

Verificando

Finalizada a gravação, o scrub (varredura) nos discos foi iniciado e o ZFS vai ler todos os dados, de todos os pendrives, e vai validar esses dados com as checksums.

No fim da gravação, logo após o comando sync já iniciamos o scrub, que pode ser iniciado novamente com o comando abaixo.

$ sudo zpool scrub teste

Podemos ver o andamento do scrub no status da ZPOOL.

$ zpool status teste
> ...
>   scan: scrub in progress since ...
>     14.0G scanned at 182M/s, 720M issued at 9.12M/s, 14.0G total
>     0B repaired, 5.01% done, no estimated completion time
> ...

Quando o scrub finalizar vemos o resultado também no status da ZPOOL.

$ zpool status teste
> ...
>   scan: scrub repaired 0B in 00:27:31 with 0 errors on ...
> ...
>     NAME      STATE     READ WRITE CKSUM
>     teste     ONLINE       0     0     0
>       sdb     ONLINE       0     0     0
>       sdc     ONLINE       0     0     0
>       sdd     ONLINE       0     0     0
>       sde     ONLINE       0     0     0
> ...

Ótimas notícias: Nenhum erro!

Limpando e exportando

Agora vamos apagar todos os arquivos e aplicar um "trim" no sistema de arquivos, instruindo ao pendrive que o espaço livre pode ser reutilizado, melhorando a longevidade do pendrive.

$ sudo rm -f /teste/file-*.bin
$ sudo zpool trim teste --wait

Estando tudo finalizado, podemos exportar (ejetar) a ZPOOL para podermos formatar os pendrives e usá-los normalmente.

$ sudo zpool export teste

Conclusão

Em 3 horas os 4 pendrives estavam testados e pude aproveitar boa parte do tempo para escrever este post.

Tamanha a conveniência, acredito que será meu novo método de testes de pendrives e cartões SD.

Minhas Preferências de Formatação

Abaixo estão algumas das minhas preferências e suas razões.

1. Endentação e aspecto geral

  • Endentar com 4 espaços;
  • Limite de 80 caracteres por linha; e
  • Tabulações proibidas.

Para mim 2 espaços é pouco e 8 espaços é muito na endentação. Quatro espaços parecem ser o meio termo adequado.

Linhas longas atrapalham a leitura do código: eu desejo um código tão legível quanto livros e jornais. Além disso, o limite de 80 caracteres permite abrir arquivos lado a lado sem necessitar de rolagem horizontal e ainda facilita revisão do código anterior e atual abertos lado a lado.

Com respeito a proibir tabulações, existe o argumento que tabulações são mais flexíveis, pois o programador pode escolher o tamanho de sua preferência. O argumento é válido, porém somente até se misturarem as tabulações e os espaços: nesse instante, a única pessoa que vê alinhado é o próprio autor do código; e todo o resto do mundo vê uma bagunça desalinhada. Usar apenas um tipo de espaço em branco elimina esse problema.

2. Alinhamento das chaves

Abertura de chaves na linha abaixo.

Isso ajuda a identificar onde acontece a abertura do bloco. Chave abrindo na própria linha permite determinar o início do bloco sem precisar procurar por ela no fim de alguma linha.

unsigned long int funcao_h(
    unsigned long int parametro1,
    unsigned long int parametro2,
    unsigned long int parametro3)
{
    return parametro1 + parametro2 - parametro3;
}

Compare abaixo a mesma função abrindo as chaves na linha. Não é tão óbvio como no caso acima.

unsigned long int funcao_h(
    unsigned long int parametro1,
    unsigned long int parametro2,
    unsigned long int parametro3) {
    return parametro1 + parametro2 - parametro3;
}

Abrindo as chaves na linha abaixo, fica óbvio onde os parâmetros terminam e onde inicia o corpo da função. O mesmo acontece em blocos de controle como if, for, etc.

Mesmo se eliminarmos qualquer informação sobre os caracteres, como é mostrado abaixo, facilmente identificamos a lista de parâmetros e o início do corpo da função.

XXXXXXXX XXXX XXX XXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
X
    XXXXXX XXXXXXXXXX X XXXXXXXXXX X XXXXXXXXXXX
X

XXXXXXXX XXXX XXX XXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX X
    XXXXXX XXXXXXXXXX X XXXXXXXXXX X XXXXXXXXXXX
X

A maior parte do tempo que passamos programando é utilizada, na verdade, lendo o código. Esta pequena escolha ajuda a encontrar a parte que importa no momento.

3. Parâmetros de funções

A lista de parâmetros de funções e a lista de argumentos de chamada de uma função devem estar todos em uma linha ou cada um em sua linha.

Dessa forma, a lista de parâmetros (ou argumentos) realmente se parece com uma lista. É fácil de contar os parâmetros e é fácil de encontrá-los.

Nesse ponto, há três possibilidades avaliadas na ordem seguinte:

  • Protótipo todo na mesma linha;
  • Lista de parâmetros toda na segunda linha; e
  • Lista de parâmetros com um parâmetro por linha.

Exemplos abaixo:

/* Protótipo inteiro cabe em uma linha: */

int funcao_f(int parametro1, int parametro2)
{
    return parametro1 + parametro2;
}

/*
 * Protótipo não cabe em uma única linha, porém
 * a lista de parâmetros cabe inteira na segunda linha:
 */

unsigned long int funcao_g(
    unsigned long int parametro1, unsigned long int parametro2)
{
    return parametro1 + parametro2;
}

/*
 * Parâmetros não cabem todos na segunda linha, então
 * coloca-se um parâmetro por linha:
 */

unsigned long int funcao_h(
    unsigned long int parametro1,
    unsigned long int parametro2,
    unsigned long int parametro3)
{
    return parametro1 + parametro2 - parametro3;
}

4. Comentários

  • Usar o mínimo de comentários possível; e
  • Comentários multilinha devem ter um caractere inicial de alinhamento.

Comentários não são analisados pelo compilador e não precisam fazer sentido para o código funcionar. Por isso não são atualizados em conjunto com o código. Consequentemente, com o passar do tempo a semântica (o significado) do código se desvia dos comentários presentes.

Na maioria dos casos é possível fazer um código autoexplicativo através da refatoração e da melhoria de nomes de funções e nomes de variáveis.

/* executa_loop1 */

void executa_loop1(void)
{
    // Lê o valor do sinal do sensor
    int16_t val = ADC0;

    // Calcula tensão do sensor em volts
    // a partir do offset e do ganho
    float valf = (val - 512) * (24.0 / 512.0);

    // Escreve na serial
    Serial_EscreveFloat(valf);
}

/* ---------------------------------------- */

/* executa_loop2 */

#define SENSOR_OFFSET 512
#define SENSOR_GANHO  (24.0 / 512.0)

float TensaoSensor_Volts(void);

void executa_loop2(void)
{
    Serial_EscreveFloat(TensaoSensor_Volts());
}

int16_t SinalSensor_ADC(void)
{
    return ADC0;
}

float TensaoSensor_Volts(void)
{
    return (SinalSensor_ADC() - SENSOR_OFFSET) * SENSOR_GANHO;
}
  • O primeiro comentário é removido com uma chamada da função SinalSensor_ADC().
  • O segundo comentário é removido com uma chamada da função TensaoSensor_Volts() e pelo uso das constantes SENSOR_OFFSET e SENSOR_GANHO.
  • O terceiro comentário é redundante e desnecessário.

Qual versão da função executa_loopX() você prefere ler?

Alguns comentários, por sua vez, são impossíveis de serem transformados em código. Por exemplo, o comentário a seguir mostra um diagrama de um LCD 16×2 que mostra a tensão elétrica e a corrente medida.

/*
 *   0123456789012345
 * 0 Tensão   XX,XX V 0
 * 1 Corrente Y,YYY A 1
 *   0123456789012345
 */

Sobre o caractere inicial dos comentários multilinha, alguns editores e formatadores eliminam espaços em branco iniciais, alinhando o primeiro caractere não branco de cada linha.

Dessa forma, todo o esforço de alinhamento abaixo é perdido em um piscar de olhos:

/*  0123456789012345
  0 Display 16x2     0
  1 Linha 2          1
    0123456789012345 */

O comentário acima acaba por se transformar em alguma dessas atrocidades:

/* 0123456789012345
   0 Display 16x2     0
   1 Linha 2          1
   0123456789012345 */

/* 0123456789012345
0 Display 16x2     0
1 Linha 2          1
0123456789012345 */

Colocando os asteriscos de alinhamento o problema deixa de existir.

/*
 *   0123456789012345
 * 0 Display 16x2     0
 * 1 Linha 2          1
 *   0123456789012345
 */

Considerações finais

Essas são as algumas das minhas preferências, ao menos as que eu acho mais importantes.

Todas essas já mudaram algumas vezes desde que comecei a programar e, com a experiência, são as que me parecem mais naturais e que facilitam a leitura do código.

Além disso, a maioria delas (exceto as sobre comentários), não me dão trabalho algum, pois utilizo um formatador automático. Assim posso focar no código em si e em como melhorá-lo para eliminar comentários.

Formatando o Código

Quando desenvolvido em grupo ou time, a formatação do código de um projeto é um assunto que cria polêmicas com frequência. Cada pessoa tem suas preferências e isso gera conflitos durante as revisões.

Para minimizar esse problema há três pontos que podem ser adotados pelo projeto:

  • Deve haver algum padrão de formatação para o projeto;
  • Dar preferência para formatação automatizada; e
  • Documentar como configurar a formatação automatizada.

Tenha algum padrão de formatação

Para o projeto em si, não importa qual é o padrão estético do código, desde que o código funcione. Os programadores, no entanto, possuem preferências com respeito à estética do código.

Esse primeiro ponto reforça que algum padrão exista. Não importa qual seja o padrão, desde que ele exista e seja seguido. Dessa forma, essas discussões nas revisões deixam de focar em critérios pessoais e passam a usar um critério objetivo: estar ou não de acordo com o padrão.

Com o padrão definido, as discussões sobre preferência pessoal são minimizadas e, caso haja algum ponto adicional que o padrão não abrange, faz-se uma reunião para definir o padrão para tal caso.

Automatize a formatação

O segundo ponto, por sua vez, elimina o trabalho manual de formatar e consequentemente elimina apontamentos de erro nas revisões por conta de erros de formatação.

Também são eliminadas algumas reuniões sobre preferências pessoais não abrangidas pelo padrão, porque as decisões precisam ser mantidas dentro das opções fornecidas pelo formatador automático.

Além disso, são menos regras para se lembrar na hora de escrever código.

Só há uma regra: execute o formatador nos arquivos modificados.

Existe um projeto chamado pre-commit que realiza tarefas logo antes de realizar um commit com o git. O tipo de tarefa mais comum é formatar o código com alguma ferramenta externa, como o clang-format (para C/C++).

Ainda melhor: alguns editores podem ser configurados para formatar o código sempre que o arquivo é salvo.

Documente o processo de automação

É importante documentar como abaixar, instalar e configurar essas ferramentas, de forma que todos possam aderir sem dificuldades.

Crie um link com a informação no LEIAME (README) do próprio projeto ou adicione a informação diretamente nele.

Essa documentação só precisa ser feita uma vez e outros projetos podem simplesmente copiar e adaptar para as suas necessidades.

Links úteis

Considerações finais

Definir um padrão para o código é importante para evitar discussões sem fim sobre a estética do código.

Ferramentas de formatação automática são a melhor opção para eliminar essas discussões e manter o código formatado consistentemente.

Documentar o processo de configuração das ferramentas é importante para os desenvolvedores fazerem uso delas.

Qual é motivo do reset do microcontrolador STM32F

No post anterior vê-se como configurar os dois Watchdogs do microcontrolador STM32F.

Porém, é possível saber qual o motivo que causou o reset? O microcontrolador ligou? Foi pressionado o botão de reset? Ou foi algum dos Watchdogs?

A resposta é: sim, é possível determinar a causa do reset.

Para determinar o motivo do reset, basta avaliar o valor lido no registrador RCC_CSR. Os seis bits mais significativos são utilizados para determinar a fonte do reset:

BitValorMotivo
310x80000000Low-power reset
Reset de baixo-consumo
300x40000000Window watchdog
Watchdog de janela
290x20000000Independent watchdog
Watchdog independente
280x10000000Software reset
Reset por software
270x08000000POR/PDR reset
Reset de energização
260x04000000Pin reset
Reset pelo pino

Abaixo segue o código que lê a fonte do reset, limpa o registrador e determina a fonte, executando blocos de acordo. Com isso é possível determinar tomar ações diferenciadas para cada um dos casos. Note que é usada a operação lógica & (E bit-a-bit).

Para obter apenas o motivo do último reset é necessário realizar a limpeza do registrador. Isso é necessário porque quando ocorre um reset, os bits do registrador não são limpados para zero, mas apenas setados para um, fazendo-os acumular cada uma das fontes de reset que ocorreram.

  uint32_t reset_source = 0;

  // Lê a fonte do reset
  reset_source = RCC->CSR;

  // Limpa o registrador
  __HAL_RCC_CLEAR_RESET_FLAGS();

  if (reset_source & RCC_CSR_LPWRRSTF) {
    // 0x80000000 Low-power reset
  }

  if (reset_source & RCC_CSR_WWDGRSTF) {
    // 0x40000000 Window watchdog reset
  }

  if (reset_source & RCC_CSR_IWDGRSTF) {
    // 0x20000000 Independent watchdog reset
  }

  if (reset_source & RCC_CSR_SFTRSTF) {
    // 0x10000000 Software reset
  }

  if (reset_source & RCC_CSR_PORRSTF) {
    // 0x08000000 POR/PDR reset
  }

  if (reset_source & RCC_CSR_PINRSTF) {
    // 0x04000000 Pin reset
  }

Veja abaixo dois casos:

  • Quando o debugger grava e inicia o debug do programa, obtém-se o valor 0x14000000, indicando que houve dois resets:
    • Reset por software; e
    • Reset pelo pino.
  • Quando o reset é causado ao pressionar o pino de reset, obtém-se o valor 0x04000000.

Conclusão

Portanto, podemos verificar qual a fonte de um reset, ou seja, qual o seu motivo. Dessa forma, podemos determinar se houve um reset por algum Watchdog, pino, energização, etc. Com isso podemos ter uma dica do que pode ter acontecido logo antes do reset.

Os Dois Watchdogs do microcontrolador STM32F

No post anterior foi visto como configurar o clock do microcontrolador STM32F para rodar a 72 MHz, baseado no cristal externo.

Neste post serão explicados pra que são e como configurar os dois Watchdogs do STM32F (IWDG e WWDG).

O que é um Watchdog

O Watchdog (cão de guarda) é um periférico interno do microcontrolador que serve para identificar mal funcionamento do programa e reiniciar o sistema a partir de um reset.

É preciso recarregar/alimentar (reload/feed) periodicamente o Watchdog para que ele não cause um reset.

Então, quando um programa entra em um laço infinito ou entra em algum estado inconsistente que evita que o Watchdog seja adequadamente recarregado, o sistema vai resetar em um tempo máximo estipulado pela configuração do Watchdog.

IWDG – Independent Watchdog

O IWDG (Watchdog Independente) é chamado assim porque ele utiliza uma fonte de clock independente do clock do sistema.

Para habilitar o IWDG:

  • Abra o arquivo IOC do projeto, entrar em “Pinout & Configuration > IWDG” e então habilitá-lo em “Activated”.
  • O valor do prescaler (divisor de clock) e o valor de reload (recarregamento) podem ser configurados para se obter um tempo máximo de reset com precisão.
  • Note que o clock usado pelo IWDG vem de um oscilador RC interno de 40 kHz, o que pode ser visto em “Clock Configuration”.

O IWDG pode ser recarregado a qualquer momento, independente do valor do seu contador. Ele vai causar um reset quando seu contador chegar até zero.

Com um clock de 40 kHz, prescaler igual a 16 e valor de recarregamento 4095, o IWDG causará um reset em de 1,6 segundos.

T = 16*(4095+1)/40000

Para evitar isso, chamamos a função que reseta o IWDG no fim do laço while(1){…}, o qual demora 1.0 segundo para ser executado:

int main(void) {
  // ...
  while (1)
  {
    // ...

    // Recarrega o IWDG
    HAL_IWDG_Refresh(&hiwdg);

    // ...
  }
   // ...
}

Note que, para poder debugar o microcontrolador com o IWDG ativo, é necessário configurá-lo para que pare a contagem quando o processador pára em um breakpoint.

Na função MX_IWDG_Init() adicione as linhas a seguir:

static void MX_IWDG_Init(void)
{
  // ...

  // Necessário para parar o IWDG quando
  // o programa para em um breakpoint
  __HAL_DBGMCU_FREEZE_IWDG();

  // ...
}

WWDG – Window Watchdog

O WWDG (Watchdog de Janela) é chamado assim porque com ele é possível configurar uma janela de tempo onde ele deve ser recarregado, de forma a não gerar um reset do microcontrolador. Se recarregar antes do tempo ou depois do tempo o reset ocorre.

Em outras palavras, o WWDG pode ser configurado para exigir um tempo mínimo e máximo entre os recarregamentos, causando um reset caso seja recarregado antes do tempo mínimo ou caso não seja recarregado no tempo máximo.

Este Watchdog funciona de forma um pouco diferente:

  • Você configura um valor de recarregamento, por exemplo 127 (0x7F) a partir do qual o contador é decrementado. Se o valor do contador é reduzido a 63 (0x3F) ocorre o reset.
  • Você também configura um valor de janela, o valor a partir do qual é permitido recarregar o WWDG. Se, quando recarregado, o contador é maior que este valor, o WWDG causa um reset. Se é menor ou igual, o WWDG é recarregado com sucesso, sem gerar um reset.
    • Se você configurar este valor como sendo igual ao valor de recarregamento, o WWDG funciona como um Watchgod normal, sem o tempo mínimo.
  • Você pode habilitar a interrução do WWDG, que ocorre quando o o valor do contador é reduzido a 64 (0x40).

Para habilitar o WWDG:

  • Abra o arquivo IOC do projeto, entrar em “Pinout & Configuration > WWDG” e então habilitá-lo em “Activated”.
  • Configure em “prescaler” o valor do divisor de clock.
  • Configure em “window value” o valor da janela (início da janela).
  • Configure em “down-counter” o valor do recarregamento (início da contagem).
  • Habilite a interrupção do WWDG e, na aba “NVIC Settings” habilite a interrupção.

Com um clock de 72 MHz, portanto 36 MHz em APB1, prescaler igual a 8 e valor de recarregamento igual a 127, o WWDG causará um reset em de 58,2 milissegundos após o o recarregamento anterior.

T1 = 8*4096*(127-63)/36000000

Com o valor da janela igual a 64, a janela de recarregamento inicia em 57,3 milissegundos após o recarregamento anterior.

T2 = 8*4096*(127-64)/36000000

Dessa forma há uma janela de tempo de 0,091 milissegundos para que o recarregamento ocorra: depois de T2 e antes de T1.

ΔT = T1-T2 = 8*4096*(64-63)/36000000

+--------------------------+
| Recarrega          Reset |
| |-------------|----|     |
| T0            T2   T1    |
|                <-->      |
|                Janela    |
+--------------------------+

Para evitar isso, no arquivo main.c (ou em qualquer arquivo) criamos a função HAL_WWDG_EarlyWakeupCallback(), a qual chama a função de reset do WWDG.

Essa função criada é automaticamente chamada pelo tratamento da interrupção do WWDG.

void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef *hwwdg) {
  // Recarrega o WWDG
  HAL_WWDG_Refresh(hwwdg);
}

De forma semelhante ao IWDG, para que seja possível debugar o microcontrolador com o WWDG ativo, é necessário configurá-lo para que pare a contagem quando a execução é pausada em um breakpoint.

Na função MX_WWDG_Init() adicione as linhas a seguir:

static void MX_WWDG_Init(void)
{
  // ...

  // Necessário para parar o WWDG quando
  // o programa para em um breakpoint
  __HAL_DBGMCU_FREEZE_WWDG();

  // ...
}

Por que dois Watchdogs?

Você pode se perguntar porque seriam necessários dois Watchdogs?

Caso o clock principal do sistema falhe, o que pode acontecer por exemplo escrevendo indevidamente em certos registradores, o WWDG vai parar de contar e, portanto, não causará reset. Nesse caso o IWDG entrará em ação, reiniciando o microcontrolador.

Além disso, como foi feito neste exemplo, ambos podem cuidar de dois escopos diferentes:

  • IWDG pode ser recarregado a cada 1 segundo, garantindo que o laço de execução principal está sendo executado; e
  • WWDG pode ser recarregado pela sua interrupção, garantindo que as interrupções não ficam desabilitadas por períodos muito longos.

Conclusão

Assim, com o uso de ambos os Watchdogs do STM32F, podemos obter uma garantia de que o programa está operando de forma correta: executando todas as suas tarefas e mantendo interrupções habilitadas.