Não sou novo em programação e já trabalhei com C e ASM de baixo nível no AVR, mas realmente não consigo entender um projeto C incorporado em maior escala.
Sendo degenerado pela filosofia de Ruby do TDD / BDD, não consigo entender como as pessoas escrevem e testam códigos como este. Não estou dizendo que é um código incorreto, apenas não entendo como isso pode funcionar.
Eu queria aprender mais sobre programação de baixo nível, mas realmente não tenho idéia de como abordar isso, pois parece uma mentalidade completamente diferente à qual estou acostumado. Não tenho problemas para entender a aritmética dos ponteiros ou como a alocação de memória funciona, mas quando vejo como o código C / C ++ parece complexo em comparação com o Ruby, parece incrivelmente difícil.
Como já me encomendei uma placa Arduino, eu adoraria entrar mais em C de baixo nível e realmente entender como fazer as coisas corretamente, mas parece que nenhuma das regras de idiomas de alto nível se aplica.
É ainda possível fazer TDD em dispositivos incorporados ou ao desenvolver drivers ou coisas como gerenciador de inicialização personalizado, etc.?
fonte
Respostas:
Primeiro, você deve saber que tentar entender o código que você não escreveu é 5x mais difícil do que escrever você mesmo. Você pode aprender C lendo o código de produção, mas vai demorar muito mais do que aprender fazendo.
É uma habilidade; você fica melhor nisso. A maioria dos programadores em C não entende como as pessoas usam Ruby, mas isso não significa que não possam.
Bem, existem livros sobre o assunto:
Se um zangão pode fazê-lo, você também pode!
Lembre-se de que a aplicação de práticas de outros idiomas geralmente não funciona. O TDD é bastante universal.
fonte
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.
fonte
Código com um longo histórico de desenvolvimento incremental e otimizações para várias plataformas, como os exemplos que você escolheu, geralmente é mais difícil de ler.
O problema do C é que ele é realmente capaz de abranger plataformas em uma enorme variedade de riqueza de API e desempenho de hardware (e na falta dela). O MacVim executou responsivamente em máquinas com desempenho de memória e processador 1000 vezes menor que o de um smartphone comum atualmente. Pode seu código Ruby? Essa é uma das razões pelas quais pode parecer mais simples do que os exemplos C maduros que você escolheu.
fonte
Estou na posição inversa de ter passado a maior parte dos últimos 9 anos como programador C, e recentemente trabalhando em alguns front-ends do Ruby on Rails.
As coisas em que trabalho em C são principalmente sistemas personalizados de tamanho médio para controlar armazéns automatizados (custo típico de algumas centenas de milhares de libras, até alguns milhões). A funcionalidade de exemplo é um banco de dados personalizado na memória, interface com máquinas com alguns requisitos de tempo de resposta curtos e gerenciamento de nível superior do fluxo de trabalho do armazém.
Posso dizer, antes de tudo, que não fazemos TDD. Eu tentei em várias ocasiões introduzir testes de unidade, mas em C é mais problemático do que vale a pena - pelo menos ao desenvolver software personalizado. Mas eu diria que TDD é muito menos necessário em C do que Ruby. Principalmente, isso ocorre porque o C é compilado e, se for compilado sem avisos, você já fez uma quantidade bastante semelhante de testes aos testes de andaimes gerados automaticamente pelo rspec no Rails. Ruby sem testes de unidade não é viável.
Mas o que eu diria é que C não precisa ser tão difícil quanto algumas pessoas fazem. Grande parte da biblioteca padrão C é uma bagunça de nomes de funções incompreensíveis e muitos programas em C seguem esta convenção. Fico feliz em dizer que não temos, e, de fato, temos muitos wrappers para a funcionalidade de biblioteca padrão (ST_Copy em vez de strncpy, ST_PatternMatch em vez de regcomp / regexec, CHARSET_Convert em vez de iconv_open / iconv / iconv_close e assim por diante). Nosso código C interno é melhor para mim do que para a maioria das outras coisas que eu já li.
Mas quando você diz que regras de outras línguas de nível superior parecem não se aplicar, eu discordo. Muitos bons códigos C 'parecem' orientados a objetos. Você costuma ver um padrão de inicializar um identificador para um recurso, chamando algumas funções passando o identificador como argumento e, eventualmente, liberando o recurso. De fato, os princípios de design da programação orientada a objetos vieram em grande parte das coisas boas que as pessoas estavam fazendo nas linguagens procedurais.
Os momentos em que o C fica realmente complicado costumam fazer coisas como drivers de dispositivo e kernels do SO, que são basicamente de nível muito baixo. Ao escrever um sistema de nível superior, você também pode usar os recursos de nível superior do C e evitar a complexidade de baixo nível.
Uma coisa muito interessante que você pode querer dar uma olhada é o código-fonte C para Ruby. Nos documentos da API Ruby (http://www.ruby-doc.org/core-1.9.3/), você pode clicar e ver o código fonte dos vários métodos. O interessante é que esse código parece bastante agradável e elegante - não parece tão complexo quanto você imagina.
fonte
O que fiz foi separar o código dependente do dispositivo do código independente do dispositivo e, em seguida, testar o código independente do dispositivo. Com boa modularidade e disciplina, você vai acabar com uma principalmente base de código bem testado.
fonte
Não há razão para que você não possa. O problema é que pode não haver estruturas de teste de unidade prontas para uso, como você tem em outros tipos de desenvolvimento. Isso está ok. Significa apenas que você precisa adotar uma abordagem "faça você mesmo" para testar.
Por exemplo, pode ser necessário programar a instrumentação para produzir "entradas falsas" para os conversores A / D ou talvez você precise gerar um fluxo de "dados falsos" para o seu dispositivo incorporado responder.
Se você encontrar resistência ao usar a palavra "TDD", chame-a de "DVT" (teste de verificação do projeto) que tornará os EE mais confortáveis com a idéia.
fonte
É ainda possível fazer TDD em dispositivos incorporados ou ao desenvolver drivers ou coisas como gerenciador de inicialização personalizado, etc.?
Algum tempo atrás, eu precisava escrever um gerenciador de inicialização de primeiro nível para uma CPU ARM. Na verdade, existe um dos caras que vendem esta CPU. E usamos um esquema em que o gerenciador de inicialização inicializa o nosso. Mas isso foi lento, pois precisávamos fazer o flash de dois arquivos no flash NOR em vez de um, precisávamos criar o tamanho do nosso gerenciador de inicialização no primeiro, e reconstruí-lo sempre que alterávamos o gerenciador de inicialização e assim por diante.
Então, decidi integrar as funções do gerenciador de inicialização no nosso. Por ser um código comercial, eu tinha que ter certeza de que tudo funcionava conforme o esperado. Então, modifiquei o QEMU para emular os blocos IP dessa CPU (não todos, apenas aqueles que tocam no gerenciador de inicialização) e adiciono código ao QEMU para "printf" todos de leitura / gravação em registros que controlam coisas como PLL, UART, SRAM e em breve. Depois atualizei nosso gerenciador de inicialização para oferecer suporte a essa CPU e, depois disso, comparamos a saída que fornece nosso gerenciador de inicialização e seu emulador, isso me ajuda a detectar vários bugs. Foi escrito em parte no ARM assembler, em parte C. Também depois que o QEMU modificado me ajudou a detectar um bug, não consegui capturar usando JTAG e uma CPU ARM real.
Assim, mesmo com C e assembler, você pode usar testes.
fonte
Sim, é possível fazer TDD em software incorporado. As pessoas que dizem que não é possível, não são relevantes ou não são aplicáveis não estão corretas. Há um grande valor a ser obtido com o TDD, incorporado como em qualquer software.
Porém, a melhor maneira de fazer isso não é executar seus testes no destino, mas abstrair as dependências de hardware, compilar e executar no PC host.
Ao fazer TDD, você estará criando e executando muitos testes. Você precisa de um software para ajudá-lo a fazer isso. Você deseja uma estrutura de teste que torne isso fácil e rápido, com descoberta automática de testes e geração simulada.
A melhor opção para C agora é Ceedling. Aqui está um post sobre o que eu escrevi sobre isso:
http://www.electronvector.com/blog/try-embedded-test-driven-development-right-now-with-ceedling
E é construído em Ruby! Você não precisa conhecer nenhum Ruby para usá-lo.
fonte