Uma grande variedade de respostas aqui ... abordando principalmente o problema de várias maneiras.
Escrevo software e firmware de baixo nível embutidos há mais de 25 anos em uma variedade de idiomas - principalmente C (mas com desvios para Ada, Occam2, PL / M e vários montadores ao longo do caminho).
Após um longo período de reflexão, tentativa e erro, estabeleci um método que obtém resultados rapidamente e é fácil criar invólucros e chicotes de teste (onde eles ADICIONAM VALOR!)
O método é mais ou menos assim:
Escreva uma unidade de código de abstração de driver ou hardware para cada periférico principal que você deseja usar. Escreva também um para inicializar o processador e configurar tudo (isso torna o ambiente amigável). Normalmente, em pequenos processadores embarcados - o seu AVR é um exemplo - pode haver de 10 a 20 unidades, todas pequenas. Podem ser unidades para inicializar, conversão A / D para buffers de memória não escalonados, saída bit a bit, entrada de botão (sem rebaixamento apenas amostrado), drivers de modulação de largura de pulso, drivers UART / serial simples que o uso interrompe e buffers de E / S pequenos. Pode haver mais alguns - por exemplo, drivers I2C ou SPI para EEPROM, EPROM ou outros dispositivos I2C / SPI.
Para cada uma das unidades de abstração de hardware (HAL) / driver, escrevo um programa de teste. Isso depende de uma porta serial (UART) e de um processador init - portanto, o primeiro programa de teste usa apenas essas duas unidades e faz apenas algumas entradas e saídas básicas. Isso me permite testar se posso iniciar o processador e se tenho E / S serial de suporte básico a depuração. Depois que isso funciona (e somente então), eu desenvolvo os outros programas de teste HAL, construindo-os sobre as boas unidades UART e INIT conhecidas. Portanto, posso ter programas de teste para ler as entradas bit a bit e exibi-las de uma forma agradável (hexadecimal, decimal, qualquer que seja) no meu terminal de depuração serial. Posso então mudar para coisas maiores e mais complexas, como programas de teste EEPROM ou EPROM - eu faço a maioria desses menus direcionados para que eu possa selecionar um teste para executar, executá-lo e ver o resultado. Não consigo escrever, mas geralmente não
Depois de ter todo o meu HAL em execução, encontro uma maneira de obter uma marcação regular do timer. Isso geralmente ocorre a uma taxa entre 4 e 20 ms. Isso deve ser regular, gerado em uma interrupção. A sobreposição / excesso de contadores geralmente é como isso pode ser feito. O manipulador de interrupção aumenta então um tamanho de byte "semáforo". Nesse ponto, você também pode mexer com o gerenciamento de energia, se necessário. A idéia do semáforo é que, se seu valor for> 0, você precisará executar o "loop principal".
O EXECUTIVO executa o loop principal. Ele simplesmente espera que o semáforo se torne não-0 (eu abstraio esse detalhe). Nesse ponto, você pode brincar com os contadores para contar esses ticks (porque você conhece a taxa de ticks) e, portanto, pode definir sinalizadores mostrando se o tick atual do executivo é por um intervalo de 1 segundo, 1 minuto e outros intervalos comuns. pode querer usar. Uma vez que o executivo saiba que o semáforo é> 0, ele executa uma única passagem por todas as funções de "atualização" dos processos "aplicativos".
Os processos do aplicativo efetivamente ficam lado a lado e são executados regularmente por uma marca de "atualização". Esta é apenas uma função chamada pelo executivo. Isso é efetivamente multitarefa para homens pobres com um RTOS caseiro simples que depende de todos os aplicativos que entram, realizando um pequeno trabalho e saindo. Os aplicativos precisam manter suas próprias variáveis de estado e não podem fazer cálculos de longa duração, porque não existe um sistema operacional preventivo para forçar a justiça. Obviamente, o tempo de execução dos aplicativos (cumulativamente) deve ser menor que o período do tick principal.
A abordagem acima é facilmente estendida para que você possa adicionar coisas como pilhas de comunicação executadas de forma assíncrona e as mensagens de comunicação podem ser entregues aos aplicativos (você adiciona uma nova função a cada uma que é o "rx_message_handler" e escreve um despachante de mensagens que indica para qual aplicativo enviar).
Essa abordagem funciona para praticamente qualquer sistema de comunicação que você queira nomear - ela pode (e já fez) funcionar para muitos sistemas proprietários, sistemas de comunicação de padrões abertos e até para pilhas TCP / IP.
Ele também tem a vantagem de ser construído em peças modulares com interfaces bem definidas. Você pode puxar e retirar peças a qualquer momento, substituindo peças diferentes. Em cada ponto do caminho, você pode adicionar equipamentos de teste ou manipuladores que se baseiam nas boas partes da camada inferior conhecidas (as coisas abaixo). Descobri que cerca de 30% a 50% de um projeto pode se beneficiar da adição de testes de unidade especialmente escritos, que geralmente são facilmente adicionados.
Eu levei isso um passo adiante (uma idéia que tirei de outra pessoa que fez isso) e substitui a camada HAL por uma equivalente para PC. Por exemplo, você pode usar C / C ++ e winforms ou similar em um PC e escrevendo o código com ATENÇÃO, você pode emular cada interface (por exemplo, EEPROM = um arquivo de disco lido na memória do PC) e executar o aplicativo incorporado inteiro em um PC. A capacidade de usar um ambiente de depuração amigável pode economizar uma grande quantidade de tempo e esforço. Somente projetos realmente grandes costumam justificar essa quantidade de esforço.
A descrição acima é algo que não é exclusivo de como faço as coisas em plataformas embarcadas - deparei-me com inúmeras organizações comerciais que fazem similar. A maneira como isso é feito é geralmente muito diferente na implementação, mas os princípios geralmente são os mesmos.
Espero que o exposto dê um pouco de sabor ... essa abordagem funciona para pequenos sistemas embarcados que rodam em alguns kB com gerenciamento agressivo de bateria até monstros de 100K ou mais linhas de origem que funcionam permanentemente. Se você executar "incorporado" em um grande sistema operacional como o Windows CE ou mais, tudo isso é completamente imaterial. Mas isso não é programação embarcada REAL, de qualquer maneira.