A parte anterior mostrou o protocolo utilizado pelo controle remoto e qual é a mensagem que faz a TV ligar e desligar. É possível usar vários meios para reproduzir esse sinal: Arduino, ESP8266 e Raspberry Pi são os mais comuns. Mas teoricamente qualquer microcontrolador com PWM poderia transmitir um sinal infravermelho semelhante a um controle remoto de TV.
Background sobre microcontroladores: Um microcontrolador é um circuito integrado com um processador, memória e “periféricos” que realizam funções de controle de tempo e comunicação em diferentes protocolos. São geralmente de baixo custo e desempenho, para aplicações em eletrodomésticos, medição de sensores e aparelhos com baterias. Arduino é uma plataforma baseada no microcontrolador Atmega328 e ESP8266 é outro microcontrolador que se tornou popular ao adotar a plataforma Arduino para programação. Além do Atmega328, outros microcontroladores populares são ATtiny85, Attiny13 e a linha STM32. Além desses, há muitas outras empresas que produzem microcontroladores, com popularidade variando por preço, função, estoques e documentação. Raspberry Pi não é um microcontrolador, mas um Single Board Computer (SBC) baseado em um processador e utilizando Linux. Vou ignorar ele para esse projeto, já que um controle remoto é algo simples que não necessita de um sistema operacional
O microcontrolador que decidi usar é o MSP430G2452. O motivo? Eu tenho vários dele sobrando e tenho também um debugger que posso utilizar para facilitar a programação. O debugger permite que eu pause a execução do código e veja valores de variáveis e registradores durante a execução, além de criar breakpoints que param a execução do programa em pontos determinados. É uma ferramenta maravilhosa para testar e resolver problemas no código. Se eu utilizasse um ATtiny13 ou ATtiny85, eu não teria essa facilidade, e encontrar problemas no código seria uma atividade bem mais demorada. Além disso, o debugger para o MSP430 também tem a função de medir o consumo de energia, o que é um bônus interessante para criar algo que utiliza baterias.
O MSP430G2452 possui um clock até 16MHz, mas sem a possibilidade de usar um cristal oscilador para maior precisão. Há vários modos de baixo consumo, então é possível personalizar bastante a economia de energia. Além disso, há outros recursos comuns a microcontroladores em geral: Flash, RAM, timers, GPIOs, ADCs e comunicação serial. A saída em PWM necessária para a modulação do sinal IR é feita pelos timers, que irei detalhar adiante.
O Trabalho de Outros
Mencionei o trabalho iniciado por Ken Shirriff, e aprimorado por outros, para criar uma versátil biblioteca para Arduino para transmissões em infravermelho. A biblioteca é bem completa, lidando com ambos transmissão e recepção. É compatível não somente com os arduinos baseados em Atmega328, mas também vários ATtiny, Teensy e ESP8266. Ela não inclui MSP430, mas não estou interessado em usar ela diretamente, apenas como referência de um projeto finalizado que funcionou.
Olhando o código no Github, cada protocolo possui um arquivo de código fonte próprio. Para o protocolo NEC, há um arquivo com algumas constantes declarada e duas funções: send e decode. A função send, que é a que mais interessa, chama duas funções mark() e space(), onde uma envia um sinal de nível lógico alto por uma certa duração e a outra envia um sinal de nível baixo por outra duração. A duração de cada um é o parâmetro da função. Essas funções são declaradas em um outro arquivo, que é comum entre todos os protocolos, mas a duração de cada sinal alto ou baixo está na série de #defines no arquivo específico do protocolo
A função mark() ativa a saída PWM (ENABLE_PWM) no pino utilizado para transmissão, seguido por um delay de acordo com o parâmetro da função. Esse delay é uma função própria da biblioteca, não o delay da plataforma Arduino, mas usa a função micros() que faz parte da plataforma. Essa função não existe por padrão ao programar um microcontrolador, então esse é o primeiro detalhe onde o código para MSP430 deve variar. A função space() é semelhante, mas desativa a saída PWM em vez de ativá-la.
É importante notar que a função não considera endereço e dado da mensagem a ser enviada, nem seu complemento para para garantir a integridade da mensagem. Essa tarefa deve ser feita pelo resto do programa, antes da função send() ser chamada. O tamanho máximo da mensagem a ser enviada por essa função é 32 bits (tamanho da variável tipo long do Arduino). Em 32 bits cabem um endereço e uma mensagem de 8 bits cada, além de seus complementos. Isso é apenas uma confirmação de que é esperado que a mensagem a ser transmitida chegue pronta à função.
Trabalho em Partes
A melhor forma de trabalhar em código é dividir as funções nas menores partes independentes. Em um microcontrolador onde os recursos são poucos e precisam ser compartilhados com cuidado, essa tarefa fica mais difícil
Sabendo o funcionamento geral de uma biblioteca funcional, é hora de criar as funções próprias para MSP430. Vou dividir o trabalho em 3 partes, de acordo com o funcionamento da biblioteca IR para Arduino:
- Controle PWM
- Funções mark() e space()
- Função send()
Essa ordem é de menor nível para maior, ou seja, de controle de registradores para funções de maior abstração.
Mesa de Trabalho
Antes de falar do funcionamento do microcontrolador e escrever o código, há algo entre essas duas partes: o ambiente de desenvolvimento. A Texas Instruments fornece gratuitamente a IDE (Integrated Development Environment), que é utilizada para facilitar o desenvolvimento. Ela inclui editor de texto, compilador, linker, debugger e outras ferramentas relacionadas com a criação de projetos em microcontroladores. A versão usada para esse projeto é a versão 8.
Tick Tock
Hora de trabalhar diretamente com o microcontrolador. A página do microcontrolador MSP430G2452 possui dois documentos importantes: o datasheet que indica as características do circuito integrado e os recursos presentes e o family user guide, que explica o funcionamento detalhado de cada parte do microcontrolador. O mais utilizado para a escrita do código é o user guide, mas o datasheet é necessário, pois nem todos os recursos mencionados pelo user guide estão presentes em todos os microcontroladores. Por exemplo, o documento menciona módulos Timer_A e Timer_B. Eles possuem funções semelhantes, mas com pequenas diferenças. Usar o Timer_B no microcontrolador MSP430G2452 não é possível, pois o datasheet mostra que ele possui apenas um módulo Timer_A. Microcontroladores mais avançados podem ter múltiplos de cada módulo Timer, mas os mais simples possuem apenas um módulo Timer.
Background sobre Timers: Um timer é um contador. Ele possui uma entrada que recebe pulsos constantes e a cada pulso o contador é incrementado (0..1..2..3..4..5…). Quando ele atinge o valor máximo, representado pelo número de bits do timer, o próximo valor do contador é zero. O uso prático desse módulo de microcontroladores é que há circuitos internos ao microcontrolador que interrompem o fluxo normal do código para executar funções específicas quando o contador atinge um valor específico e programável. E o processador não precisar fazer nada de processamento relacionado a contagem de tempo até que uma interrupção ocorra. Além de interrupções, o timer também pode controlar pinos sem intervenção do processador. Para controlar quão frequentemente essas interrupções ocorrem, dois parâmetros são alterados: a frequência dos pulsos que alimentam o contador e o valor do contador que ativa um interrupção. Por exemplo, para uma frequência de 1MHz, o contador incrementa a cada microsegundo. Se uma interrupção acontece quando o contador atinge o valor 1000, então haverá uma interrupção a cada 1000 microsegundos. Para interrupções frequentes, em microsegundos, são usados valores de contador baixos e frequências altas alimentado o Timer. Para interrupções a cada poucos minutos, são necessários valores altos no contador e baixas frequências.
Olhando o user guide, a primeira informação disponível é o diagrama de blocos do Timer, que certamente não dá a impressão de ser algo compreensível. Depois de um tempo, diagramas assim começam a fazer sentido e servem como um bom resumo do funcionamento geral de um periférico.

Resumindo o diagrama de blocos mostra que há um único contador, ligado à 3 submódulos, CCR0, CCR1 e CCR2. São esses submódulos que comparam a contagem com valores pré-definidos e disparam interrupções.
O contador pode operar em 3 modos:
- Up
- Continuous
- Up/Down
Cada um deles é um modo de como o timer se comporta quando atinge o valor máximo que pode contar. Para esse projeto, o modo up é simples e funcional. O valor de CCR0 define o período do PWM. Para uma frequência de 38kHz, o período é de aproximadamente 26,3μs. Entretanto, esse valor não pode ser colocado diretamente no registrador CCR0. O registrador contém o número de ciclos de clock a serem contados, não o número de segundos. Portanto, o valor de CCR0 depende do período desejado e do clock utilizado.
Cada microcontrolador possui diferentes fontes de clock (interno, externo e baixa ou alta frequência) que alimentam as diferentes partes do microcontrolador. Os periféricos não são alimentados diretamente pela fonte de clock, mas por sinais de clock, chamados de Master Clock (MCLK) e Sub-main Clock (SMCLK). A combinação entre fontes de clock e sinais de clock pode rapidamente se tornar complexa tão tantas partes móveis a serem consideradas. E isso antes de levar em consideração que o clock pode ser alterado durante o funcionamento do microcontrolador. Usar um diagrama, como o da figura a seguir, ajuda a identificar todas as partes do sistemas de clock sem precisar passar por várias páginas em um PDF. Vale notar que nem todos os elementos da imagem estão presentes em todos os microcontroladores, como é o caso do oscilador XT2 no MSP430G2452.

Para o meu MSP430G2452, a única opção para um clock de alta frequência, na faixa de MHz, vem de um DCO (Digital Controlled Oscillator) interno. Ele possui baixa precisão, mas é prático não ter um cristal oscilador no circuito externo. Ele pode gerar um clock de pouco menos de 100kHz até pouco mais de 20MHz. Para simplificar o projeto, vou manter um clock fixo de 1MHz agora e modificar caso seja necessário. Esses 1MHz irá alimentar MCLK e SMCLK, e este por sua vez irá alimentar o módulo timer.
Multiplicando 1MHz por 26,3μs, o valor de CCR0 deve ser 26,3. Como CCR0 pode apenas conter números inteiros, é necessário arredondar para 26 ou 27. Usando o valor de 26, a frequência (teórica) do PWM é 38,4kHz, contra 37kHz para um valor de 27 em CCR0. Essa situação pode ser melhorada, mas é melhor ver os resultados antes de tentar otimizar.
É possível usar o timer dessa forma gerando uma interrupção e dentro dessa interrupção o estado do pino é alterado. Mas esse modo desperdiça ciclos da CPU para a interrupção. Por ser uma tarefa tão comum, a capacidade de gerar um sinal PWM sem intervenção da CPU é incluída nos circuitos do timer. No MSP430, isso funciona ao mudar o comportamento de um pino de GPIO para uma conexão com o Timer. O comportamento exato do pino em relação é definido pela escolha do modo de saída (output mode). Esse é o modo como é escolhido o duty cycle do PWM.
Para um transmissor com o protocolo NEC, o PWM deve ser 25% ou 33%. Se o período é controlador por CCR0, o duty cycle é controlado por CCR1 e a escolha do modo de saída. Por exemplo, no modo 1 a saída muda para um nível alto quando o timer é igual à CCR1 e permanece assim até que algo force a mudança. Esse é um modo não relacionado com PWM, então não é útil nesse caso. Para o PWM desejado, o modo escolhido é o 7. Conforme a imagem a seguir, ele gera uma saída baixa em CCR1 e uma saída alta em CCR0. Se CCR1 for ⅓ de CCR0, a saída passa ⅓ do tempo em nível alto e ⅔ em nível baixo, formando o duty cycle desejado. Novamente é preciso arredondar 26/3 para para 9. Ainda é preciso ver que efeito isso tem no sinal final.

Para configurar o clock para 1MHz, é possível usar valores calibrados de fábrica, com o código a seguir no início da função main().
BCSCTL1 = CALBC1_1MHZ; DCOCTL = CALDCO_1MHZ;
Isso configura o clock para todo o microcontrolador. Para configurar o timer para o PWM, eu crio uma função void IR_PWM_config(void). Como há apenas um configuração, não há necessidade de parâmetros para a função. O objetivo é simplesmente colocar todas as partes relacionadas com configuração do timer em um único lugar.
Essa função possui 4 partes mencionadas anteriormente:
- Configurar o pino como saída do Timer;
- Configurar o clock do timer e o modo de operação (Up);
- Configurar a saída do PWM
- Configurar o período do timer (CCR0 e CCR1)
O PWM inicia desativado, ou seja, o output mode é 0, onde o timer não influencia a saída. Para ativar o PWM, o output mode é mudado para 7 por uma função IR_PWM_enable() e mudado novamente para 0 por IR_PWM_disable(). Quando está desativado, o timer continua funcionando internamente e, portanto, consumindo energia. Ele apenas não está conectado à saída.
Hora de testar os resultados. Vou carregar o programa criado até agora para o microcontrolador e observar a saída PWM ativa. Nenhuma mensagem é transmitida. Estou apenas testando se o PWM funciona como esperado.

Hmm, o PWM funciona, mas a frequência está um pouco diferente do esperado. Isso provavelmente se deve à baixa precisão do oscilador interno. Alterando um pouco o valor de CCR0 e testando novamente, o valor de 25 em vez de 26 gera uma frequência mais próxima de 38kHz. Testando valores para CCR1, o valor permaneceu inalterado. Sem mais alterações no clock do microcontrolador, esse é o PWM utilizado para modulação.
Mensagem
Com o PWM funcionando corretamente, é hora de utilizá-lo para enviar uma mensagem. Conforme o post anterior, para enviar uma mensagem, o PWM deve ficar ligado por uma duração fixa e desligado por outra duração fixa para cada bit. Estou pegando a ideia do código de Ken Shirrif e colocando os valores em microsegundos no header, com #define. “MARK” significa que é a parte do sinal em nível lógico alto, enquanto “SPACE” é o nível lógico baixo. Repare que para enviar um bit há dois “SPACE”, mas apenas um “MARK”.
// Microssegundos de duracao para cada parte do sinal #define NEC_HDR_MARK 9000U #define NEC_HDR_SPACE 4500U #define NEC_BIT_MARK 560U #define NEC_ONE_SPACE 1690U #define NEC_ZERO_SPACE 560U
Como o clock é de 1MHz, um ciclo equivale a um microssegundo, o que significa que o número de ciclos e o número de microssegundos do delay é o mesmo valor.
Para enviar o cabeçalho inicial, duração de 9ms em nível lógico alto seguido de 4,5ms em nível lógico baixo, o código a seguir serve bem:
IR_PWM_enable(); _delay_cycles(NEC_HDR_MARK); IR_PWM_disable(); _delay_cycles(NEC_HDR_SPACE);
O uso de _delay_cycles() não é ideal, pois o valor do delay precisa ser conhecido no momento da compilação e mantém a CPU ocupada desnecessariamente. Um modo melhor de fazer esse delay seria usando um timer, mas como teste inicial esse método serve. Além disso, o argumento de _delay_cycles() também não pode ser uma variável, o que significa que não é possível criar funções mark() e space(). Os 3 passos descritos anteriormente são agora apenas 2.
Para enviar os bits da mensagem, é preciso tratar de cada bit individualmente e em ordem. O fluxograma a seguir mostra isso, embora de forma simplificada.

Para verificar se o bit é 0 ou 1, é usado uma “máscara” de bits para escolher um único bit para comparação.
Uma máscara é um byte onde os bits são escolhidos como 0 ou 1 para serem utilizados em uma operação lógica. Por exemplo, para transformar os 4 primeiros bits de uma variável em zeros, é possível usar a máscara 0b11110000. Realizando a operação lógica AND entre a máscara e a variável, os bits da variável na mesma posição dos bits 0 da máscara são modificados para 0, enquanto os bits na mesma posição dos bits 1 não são alterados.
Para selecionar um único bit, é usada a máscara 0b00000001, ou 0x01, a a operação LEFT SHIFT (operador << em C) , que desloca os bits para a esquerda e adiciona bits 0 à direita. Ou seja, para selecionar o primeiro bit é utilizada a máscara “0x01 << 0” e para selecionar o último bit é “0x01 << 7”. Para fazer com que a máscara percorra todos os bits, um loop for pode ser usado:
uint8_t mask;
for (mask = 1; mask; mask <<= 1){
//Código aqui
}
Você pode notar que não há uma condição nesse loop, apenas “mask”. Isso é um aproveitamento do fato de que C considera qualquer byte que não é zero como “TRUE”, enquanto um byte 0x00 é “FALSE”. Enquanto houver pelo menos um bit 1 em mask, o resultado é TRUE. Quando a operação << 8 é realizada, o bit 1 mais a esquerda é perdido, sobrando apenas bits 0, portanto o resultado é falso e o loop finaliza. É um modo um pouco mais obscuro de escrever código, mas funciona como “mask != 0”
Essa propriedade de TRUE e FALSE também é aproveitada em um condicional if. Utilizando a máscara, um byte a ser enviado e o operador AND:
if (message & mask){
// Envia bit 1
}
else{
// Envia bit 0
};
Esse processo de for e if com a máscara é repetido 4 vezes, para enviar o endereço, seu complemento, os dados e seu complemento.
Por fim é enviado o rodapé que indica que a mensagem acabou. Tudo isso está dentro de um única função send_NEC(uint8_t address, uint8_t data)
Hora de finalmente testar e ver qual o resultado desse trabalho. Crio um loop em main() que envia a mesma mensagem infinitamente, com um delay entre cada mensagem. Conecto o analisador lógico nos pinos, inicio o debugger e obtenho esse resultado:

O que deu errado?
Obviamente, esse não é o esperado. O analisador lógico reconheceu que uma mensagem está sendo enviada, mas não consegue decodificar todos os bits. Isso indica que o problema está na transmissão da mensagem em si, não do sinal PWM. Caso o problema fosse na modulação, nenhum bit seria reconhecido.
Verificando primeiro o cabeçalho, ele dura 9,035ms em vez de 9ms esperados, um desvio de 0,4%. Todas as repetições do sinal possuem um desvio semelhante. Será que um desvio tão pequeno é suficiente para causar problemas na recepção da mensagem? Verificando a pausa que segue o pulso inicial, o desvio é maior. O esperado é 4,5ms, mas foi recebido 4,652, um erro de pouco mais de 3%. Ainda assim é um erro pequeno.
Para visualizar melhor o problema, coloquei a duração de todos os trecho de uma mensagem em uma tabela, tanto para os dados do controle original LG quanto para os resultados do meu microcontrolador. Durante o processo, reparei que, contrário ao que eu esperava, a duração de cada parte de um bit, MARKs e SPACEs, variava muito. Entretanto, a duração total da transmissão de um bit era quase constante. Isso indica que o problema não é somente a precisão do clock do microcontrolador.
Os gráficos a seguir são referentes para a transmissão de bits 0, mas o mesmo processo se aplica para bits 1.


Essa é a duração total para transmitir cada bit. Os bits do controle LG são enviados com durações muito próxima, enquanto os do meu microcontrolador não somente possuem uma diferença maior, como o valor médio também é mais alto. Isso se mostra um ponto crucial a seguir.
Mostrando os valores separados entre MARKs e SPACEs, há algo bem interessante a ser visto. As durações de cada parte variam muito, mas de forma que o valor total seja praticamente constante. Quando há uma queda na duração do MARK, SPACE dura mais para compensar.

Comparando com a implementação no microcontrolador (imagem abaixo), a variação é menor, mas isso não ajuda a obter uma maior reliabilidade na transmissão de bits, pois é a duração total que importa.

Comparando a duração total em ambos os casos, na imagem a seguir, mostra que os bits reconhecidos corretamente foram os com menor duração. Todos os bits não reconhecidos, em vermelho, parecem estar acima de uma linha invisível. O bit não reconhecido em verde foi ignorado pelo analisador lógico e não foi feita uma tentativa de decodificar ele, embora a posição no gráfico indique que ele certamente não seria reconhecido.

De onde surgiu esse problema? Bom, certamente há o problema da precisão do clock, que faz com o que os delays não sejam exatos. Mas há outro problema. Entre cada delay, a mudança da saída não é instantânea. Há um processamento para determinar qual deve ser a próxima parte de transmissão e também para chamar as funções que altera a saída. Digamos que esse processo demore 20us, então 20us serão adicionados ao tempo da transmissão. Esse processamento é ainda maior quando há uma decisão (if) a ser feita.
Há dois modos simples de resolver esse problema. Um é aumentar o clock, de forma que o tempo de processamento entre cada delay seja desprezível comparado com a duração do delay. Para um clock de 1MHz, cada ciclo da CPU dura 1us. Cada operação da CPU leva entre 1 e 6 ciclos, logo entre 1 e 6us. Com apenas 10 instruções, essa já é uma diferença provavelmente maior que 30us. Se o clock fosse de 16MHz, por exemplo, essas mesmas instruções levariam menos de 2us.
Mudar o clock possui outras implicações, como maior consumo de energia, então irei primeiro testar apenas diminuir a duração dos delays, levando em conta o tempo de processamento da CPU. Esse é um modo de tentativa e erro, portanto deve ser utilizado principalmente para atividades simples, como essa. Testando com diferentes valores, uma diminuição entre 20 e 30 ciclos para cada MARK e SPACE é suficiente para que a transmissão ocorra corretamente. Para testar, eu deixei o analisador lógico realizar capturas por mais tempo que o normal, esperando encontrar um erro, mas todas as transmissões foram capturadas e decodificadas corretamente.
O código final completo pode ser encontrado no github, aqui, e o sinal transmitido corretamente está na figura abaixo.

Outros Problemas
Um problema que descobri apenas na hora final desse projeto foi que o resistor do emissor IR era de uma resistência diferente do ideal. O valor recomendado para o transmissor é de 220Ω, enquanto o resistor que eu estava utilizando era de 1kΩ. Embora a diferença seja pequena, era o suficiente para que o método de diminuição de delays não fosse suficiente para evitar erros na transmissão. Uma lição em usar os componentes recomendados e em checar o circuito antes de passar horas alterando o código.