RFIN, RFOUT and NKM2401 protocol information

nick12ab

Senior Member
A short bit of background: over the years, I've made a few wireless temperature transmitter devices based around a PICAXE-14M2 microcontroller and 433MHz wireless module, and these last many years powered from 2xAA batteries. I've also made a number of different display devices to receive and display the temperatures including the PICAXE-based one below.

AP1GczOrovKUqp5cjYtTc2vaTNMwYJm2qAeta3wIX6DovExaU2eMLcaSTLhRiFruXG8bQeOKIB6RpUQ2IOOnsb0SodDnyUP-NZFlW2IFAdeE-3fnTELOgUvQ0T5yDWPG78WCnDc4XVN0vD59xYEwshy8Iyyv3g=w2001-h864-s-no


These devices all use the PICAXE rfout and rfin commands. I've also made some displays that aren't PICAXE and those use the NKM2401 for reception duties.

I wanted to start making some more advanced devices to fit in with this "ecosystem", starting with a transmitter with built in bare glass LCD display that would also be battery powered. To get the power consumption down to the lowest possible level, it wasn't practical to use a dedicated LCD driver plus PICAXE or an LCD-driving microcontroller plus NKM2401, so I started investigating how to replicate the protocol myself.

The details given in the NKM2401 datasheet and any PICAXE documentation is very vague and shows a "waveform" with 8 bytes along with mentions of Manchester encoding, a 200us period, and a "CRC".

Protocol reverse-engineering

I set up an NKM2401 on breadboard and sent a few data packets with the logic analyser connected to the serial input and Manchester data output.

AP1GczNmab9YYKo16i7ssX1Zt1rJHEi9-1Gn6orIukCZvGQKwb_QJYF4H6Bk8zhSTfwRE2Z-tH1gDr2aw3Ko6voGB8oNudSZdsZg-Ahap7hfCmnd_irmdL25Vp3R4qfx7jg4Q5SPNAEUySd33nrqkHCwwzbyLQ=w1611-h1057-s-no


From inspection of the waveform, we can see that each half bit is indeed exactly 200us and the packet format is as follows:
  1. One-byte preamble of 0x00
  2. Constant byte of 0x09
  3. 8 data bytes in the same order as the input bytes, with no processing, scrambling or further encoding
  4. CRC – and this is indeed a CRC (more on that next) not a simple checksum
The CRC needs to be replicated for the receiving NKM2401 or PICAXE rfin command to accept the packet. I first went about checking my test data against a few common 8-bit CRCs online but none of them matched. I downloaded the "CRC RevEng" program and gave it several sample packets from the NKM2401; this gave the following results:
  • Polynomial=0xb3
  • Initial value=0x00
  • Reflect input=false
  • Reflect output=false
  • Xorout=0x00
  • Check=0xdc
  • Residue=0x00
Using the polymonial found above, I calculated the CRCs for a few more test packets and they matched the CRC transmitted by the NKM2401.

With that confirmed, that's all the information needed to re-implement the packet.

Here is a clearer view of the packet. This one is actually generated by my re-implementation rather than the NKM2401. The yellow line shows the values of the data bytes and CRC.

AP1GczOkVce6p7uJ99eqhX1ngbhFwCZMjZAKgy2EQW_Q8kNHVovhrAqPfB65FochaROZS7I9j4L1AZd-R6z39MDiPllQLdZMyaOL2MvfaSV6oq1BivRqLndpQ8yMWKkwgFlmQK8q1qztNSjNnMwPm_ogkcv2Wg=w1854-h343-s-no


The re-implementation works and my finished PIC16LF1936-based transmitter project (photo attached) works with all the other receivers in the system. Idle current consumption is less than 10uA.

This post isn't meant to have detailed information about implementing the protocol on specific microcontrollers since this is the PICAXE Forum after all. I might come back and add another post with more implementation tips later, but the main things to keep in mind are:
  • The 200us half-bit interval should be recreated exactly (I haven't investigated exactly how tolerant the NKM2401 and PICAXE are to any deviations)
  • Use a logic analyser to check your timings as C compiler bloat (and MPLAB Melody delay function bloat) will easily add significant delays between each bit and each byte, and you'll need to tweak your timings to suit
  • The transmission and CRC routines are probably best written in assembly if using an 8-bit PIC. The '
    Code:
    rlf
    ' instruction (rotate left through carry) is very useful for both and there's no nice C equivalent as far as I know
Re-implementing reception is more of a challenge if it is to be done in a non-blocking manner. I haven't done this yet. The RMT peripheral on the ESP32 might be suitable. Ultra-low power consumption for a receiver has not been a requirement yet so it's always been easier to just use an NKM2401 then it can be received in the background using a standard UART.
 

Attachments

  • picaxe-wireless-temperature-display.jpg
    picaxe-wireless-temperature-display.jpg
    848.8 KB · Views: 3
  • nkm2401-waveforms.png
    nkm2401-waveforms.png
    77.9 KB · Views: 1
  • pic-recreated-nkm2401-waveform.png
    pic-recreated-nkm2401-waveform.png
    72.6 KB · Views: 2
  • wireless-temperature-transmitter.jpg
    wireless-temperature-transmitter.jpg
    215 KB · Views: 3
For this second post, I've performed a disassembly on the nkm2401.hex file which is the firmware on the NKM2401 which Rev-Ed made available for "educational purposes".

I've gone through it and comprehensively commented the important parts which I've attached and which I'll explain below. I mainly focused on the TX part as that is the part relevant to my project.

Point 1 - The CRC implementation (line 106)

If you Google "how to implement a CRC in C" you get a daft example with a big lookup table. Fortunately, they did not follow such a daft example and instead used the rlf instruction to implement this as I alluded to before. The polynomial of 0xB3 is clearly visible in the code. The succession of RLF statements implement a shift register, followed by an XOR which is a fundamental part of a CRC.

A quick explanation of how RLF (Rotate left through carry) works.

Carry is a bit in the PIC's status register.

Let's say we have register 0x15, which is 8 bits: (I'm using letters to represent the bits so you can see where each one moves to, but they can really only be either 0 or 1)
0x15ABCDEFGH
0x16KLMNPQRS
And the current value of Carry: J

Now we run the instruction "rlf 0x15, f" and the registers now contain:
0x15BCDEFGHJ
0x16KLMNPQRS
Carry now contains: A

Now we run the instruction "rlf 0x16, f" and the registers now contain:
0x15BCDEFGHJ
0x16LMNPQRSA
Carry now contains: K

As you can see, by stacking as many rlf instructions as you like, you can easily create a shift register of any size with fast execution speed.

As the C programming language lacks the carry feature, implementing it this way in C is much more cumbersome, so if implementing this in a PIC, I would suggest doing it using inline assembly and 'rlf' like in the NKM2401 firmware, which is what I did on my PIC16LF1936-based transmitter project.

Point 2 - Transmission procedure (line 177)

They've used the rlf instruction again here. The main advantage of using it here over other approaches such as a 'for' loop in C to iterate through the bytes and bits in nested loops is that the execution speed is consistent. The PIC only runs at 4MHz (1 million instructions per second) or 200 instructions per data half-bit so if you implement the transmission using nested for loops then you will see the variance in execution time between bits and bytes unless you compensate your delays accordingly.

Point 3 - the No Connect pin on the NKM2401

Looks like this is just briefly pulsed at key parts of the program, presumably for triggering an oscilloscope during development of the NKM2401 firmware.

Point 4 - "Test $ " string

If the test pin is asserted, the serial reception procedure is skipped and this string is loaded straight into the transmit buffer. The CRC is calculated on this buffer as usual rather than hardcoding the CRC for the string, then the sequence is transmitted using the usual procedure.

Others

I still don't fully understand how the receive process works. I understand the bit about checking the CRC and resetting if the CRC is incorrect but not so much the Manchester reception part.

The code generally looks very efficient so a lot of it would have been originally written in assembly. I'm not sure that all of it was though, as there's a small number of inefficient bits and seemingly redundant 'retlw's where the returned value doesn't seem to be used. Not sure why timer0 is used for one of the delays only but maybe there's a more complex explanation for that.
 

Attachments

Back
Top