Rick's blog

Using the CH32V003 as an IR transmitter

Background

For in my new car, I bought a cheap Android auto car radio. It has all features I need, and it fits into the single DIN-slot my VW Golf has. It even comes with a remote and something to attach it to the steering wheel.

The remote The remote this is all about

However, both the remote and this construction are kind of shitty. the attachment is done with an elastic band and the remote is a lot bigger than it should be, as well as being "besides" the steering wheel instead of actually "on" it.

I also bought the devkit for the CH32V003 a while ago, which I tried using, but disappeared in a closet by lack of a project and a halfway decent SDK. Both problems have been solved by now, since WCH released their Arduino SDK for the CH32V003 a while ago, which is very lacking in documentation, but has all the features you would have on a normal Arduino uno. Building this remote seems like a nice project for this microcontroller, since not a lot of processing power is needed and quite some GPIO.

First steps

The first steps to making the remote was to analyze the protocol being used by the official remote. I do not own a Flipper zero or any IR to USB-devices, so it was time to get out a ESP8266, a CHQ1838 and some jumper wires, which in conjunction with the IRrecvDumpV3 example sketch of the IRremoteESP8266 lent itself perfectly to decode some signals. The results looked as follows:

Vol+ 
Protocol  : NEC
Code      : 0xFFF20D (32 Bits)
uint16_t rawData[71] = {8936, 4504,  546, 592,  554, 590,  552, 590,  550, 590,  552, 592,  548, 594,  550, 592,  552, 596,  550, 1704,  546, 1706,  548, 1730,  522, 1706,  544, 1710,  542, 1704,  548, 1706,  546, 1710,  544, 1706,  548, 1706,  546, 1704,  548, 1704,  548, 590,  550, 592,  552, 1702,  550, 596,  548, 592,  550, 592,  552, 592,  550, 592,  548, 1706,  546, 1704,  550, 592,  552, 1728,  498, 39684,  8960, 2268,  546};  // NEC FFF20D
uint32_t address = 0x0;
uint32_t command = 0x4F;
uint64_t data = 0xFFF20D;

Here is where this blog becomes interesting, because for a AVR or ESP-based architecture, there are plenty IR libraries available that make it very easy to send commands once you have the data. However, those all use architecture-specific features such as timers and interrupts and aren't really portable because of that. Therefore, it was time to build our own.

The NEC protocol

There are multiple infrared protocols available, but for this remote, we are interested in the NEC protocol. Luckily, there is a very good writeup about this protocol available on Altium's website, which describes the pretty simple protocol.

Rick's blog A NEC message1.

As a start, let's look at the binary representation of the 0xFFF20D command, which is 0b111111111111001000001101. This is confusing for two reasons, being the following:

  1. The command is only 24 bits long, while we expect a 32-bit command
  2. The command should start with the address(0x00), while it starts with the inverse address.

I had a lot of thoughts about this, relating to the transmission being LSB-first and so on, but in the end, it turns out that the truth was a lot simpler: The leading zeroes were just removed. So, instead of 0xFFF20D, the actual command was 0x00FFF20D, which is 0b00000000111111111111001000001101 and denotes a valid NEC message.

First steps

With this information decoded, it was time to write a first implementation:

#define LED_PIN PD4

unsigned int message = 0x00FFF20D;

void setup() {
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  sendMessage(message);
  delay(500);
}

void sendMessage(unsigned int message) {
  sendHeader();
  for (byte i = 0; i < 32; i++){
    sendBit((message & 0x80000000 >> i) >> 31 - i);
  }
  sendFooter();
}
void sendBit(byte value) {
  digitalWrite(LED_PIN, HIGH);
  delayMicroseconds(562.5);
  digitalWrite(LED_PIN, LOW);
  if(value == 0) {
    delayMicroseconds(562.5);
  } else {
    delayMicroseconds(1687.5);
  }
}

void sendHeader() {
  digitalWrite(LED_PIN, HIGH);
  delayMicroseconds(9000);
  digitalWrite(LED_PIN, LOW);
  delayMicroseconds(4500);
}

void sendFooter() {
  digitalWrite(LED_PIN, HIGH);
  delayMicroseconds(562.5);
  digitalWrite(LED_PIN, LOW);
  delayMicroseconds(562.5);
}

When connecting an oscilloscope to pin PD4 of the CH32V003 dev board, the result is the following:

Rick's blog You see the header of 9ms high and 4.5 ms low, then you see the 8 zeroes of the address bits and it's inverse and after that the command(0b11110010) and its inverse(0b00001101), with a final 562.5 μs burst to end the command.

Real life use

With this output ready, I connected an IR led, fired up the radio, and hoped to see the volume go up, which of course did not happen. As a first debugging step, a phone camera was used to detect IR light. This detected IR light in a pulsed fashion, so light was actually being transmitted. Then, as a second step, the ESP8266-based IR reciever was used to detect what data is being transmitted, but this did not yield anything, so a closer look was taken at the documentation:

The NEC IR transmission protocol uses pulse distance encoding of the message bits. Each pulse burst (mark – RC transmitter ON) is 562.5µs in length, at a carrier frequency of 38kHz (26.3µs).

So there it was: The 562.5μs pulses should be 562.5μs pulse bursts at 38khz(26.3µs per pulse, 13.15µs on and 13.15µs off). This means that the header should start with 340 pulses and then 4500 µs off. For the normal bits this means that it should be 22 pulses. In practice, it turns out that the amount of pulses should be halved, and when zooming in on the oscilloscope, it becomes apparent why: Waveform zoomed in Despite it not being a nice square wave, the modulation is good enough and we can send signals to our ESP8266-based reciever. The recieved message looks as follows:

Timestamp : 000959.544
Library   : v2.8.6

Protocol  : NEC
Code      : 0xFFF20D (32 Bits)
uint16_t rawData[67] = {8800, 4380,  702, 454,  600, 522,  576, 546,  578, 544,  600, 524,  576, 546,  576, 546,  576, 512,  634, 1642,  650, 1566,  680, 1568,  708, 1542,  678, 1570,  700, 1578,  606, 1610,  614, 1636,  676, 1600,  670, 1542,  682, 1570,  612, 1668,  646, 446,  610, 546,  600, 1610,  706, 452,  576, 546,  578, 544,  578, 546,  576, 548,  572, 1670,  646, 1572,  700, 456,  578, 1634,  700};  // NEC FFF20D
uint32_t address = 0x0;
uint32_t command = 0x4F;
uint64_t data = 0xFFF20D;

The address is right, the command is right and the data is exactly what we transmitted. However...it still doesn't work. This also cost some thinking, but when looking at rawData it was clear pretty quickly.

Weird implementations

The command from the remote has length 71, while our transmitted command has length 67. That means that there are 4 bursts missing, which we can find at the bottom of the specification under "repeat codes":

If the key on the remote controller is kept depressed, a repeat code will be issued, typically around 40ms after the pulse burst that signified the end of the message.

Looking at the final 4 bits of the remote's rawData, we see that they are 39684, 8960, 2268, 546: 40000 μs wait, 9000 μs leading pulse burst, 2250 μs space and a 562.5 μs pulse burst to mark the end of the space. Why the decision has been made to implement the protocol this way is unclear, but it is apparently necessary. Therefore, let's implement it:

void sendRepeatSequence() {
  delayMicroseconds(40000);
  for (int i = 0; i < 171; i++) {
    sendPulse();
  }
  delayMicroseconds(2500);
  sendEndSequence();
}

And with this added to the total message sent, we get a working message! The volume can be controlled by sending signals through an IR LED. It also works for all other commands on the remote, which are the following:

Button Data
Vol + 0xFFF20D
Vol - 0xFFAA55
Sel 0xFFEA15
Back 0xFF6A95
Forward 0xFF9A65
Mod 0xFF2AD5
Menu 0xFFCA35
BND 0xFFDA25
Hang up 0xFFFA05
Accept 0xFF8A75
Mute 0xFF827D

The full code, wrapped in a nice little library can be found on GitHub. Be aware that this is very alpha software, it for example only supports 32 bit unsigned integers as data. This will of course be expanded upon.

This was the first part of a series, the next part will be about implementing this code in an actual remote control.

  1. Altium tech docs