Skip to content
Travis Goodspeed edited this page May 11, 2019 · 3 revisions

NOTE WELL This documentation is a work in progress and the code isn't completed yet. NOTE WELL

Implementing POCSAG on the CC430 GoodWatch

Howdy y'all,

This article is a quick description of how I implemented a POCSAG receiver on the CC430F6137 or CC430F6147 of the GoodWatch, but register settings and many of the principles ought to translate well to the CC1101 and related radios.

My specific interest was for the DAPNET amateur paging networking that runs on 439.9875MHz in much of Germany and a few locations abroad. You can run one at home on an MMDVM hotspot, which is what I did for development.

If hams begin to carry these pagers, perhaps I could also transmit simplex to them from my watch, and if it fits within the power budget, my watch itself might become a useful pager. And if not, it will still be a nifty example for implementing other 2FSK protocols.

73 from East Tennessee,

--Travis KK4VCZ

The PHY of POCSAG

POCSAG is transmitted at 512, 1200, or 2400 baud 2FSK in many different frequencies.

Packets are sent in transmissions of multiple batches, with every batch having a sync word of 0x7CD215D8. Aside from the SYNC word, there will be no gap between batches of a transmission, and each batch contains exactly sixteen 32-bit codewords in eight pairs. Instead of having an address at the beginning of the transmission, it will be in the first word of a pair, specifying that either the second word is a numeric message or the following words are an alphanumeric message.

As DAPNET has standardized on 1200 baud at 439.9875MHz, that is the configuration that I will target first. This frequency is a bit above the filtered band of the GoodWatch22 and earlier devices, but might be close enough for short range work. The GoodWatch30 expands the filtering considerably, and will not attenuate this frequency.

One easy thing to miss is that POCSAG uses an inverted view of 2FSK, where the higher frequency is a zero and the lower frequency is a one. This is a very rare configuration, so we'll need to invert all of the bits in an incoming packet.

When implementing a new protocol, I like to begin with an existing implementation wherever possible, as a sort of sanity check. Sure enough, we find that a POCSAG transmission correctly decodes in Universal Radio Hacker with a bit period of 833µs, matching the expected 1200 baud. Because of bit inversions, the sync word of 0x7CD215D8 appears instead as 0x832dea27.

POCSAG has one last physical oddity, that you'll see on the screen but can ignore until power management brings it back. The transmission has a 576-bit preamble before its batches, which at 1200 baud is 480 millseconds, nearly half a second! The reason for this is that receivers cannot afford to listen all the time, so by waking every now and then and sampling raw radio bytes, they can look for the preamble pattern (AA or 55), going back to sleep immediately if they don't see it. This is how a pager with just one AA cell can outlive even a Nokia feature phone by months.

We'll come back to decoding the actual text of this message in a bit; for the moment, we're more interested in converting these physical parameters for use in the Chipcon CC1110 core of the CC430 chip in the GoodWatch. Then we'll get some packets, and then we'll deal with multi-batch transmissions and with waking on the long preamble.

Radio Configuration

The CC1110 core of the GoodWatch is configured by its own set of registers, which are documented in datasheets/cc1101.pdf of the repository. As it's only 105 pages, I like to keep a paper copy on hand, on which to make notes or spill coffee as I get frustrated.

While many of these registers are documented sufficiently for manual use, it is helpful to begin by having the SmartRF Studio tool from Texas Instruments calculate the expected set of values for you to use as a starting point.

In the tool's Expert mode, running without hardware under WINE, I set a base frequency of 439.987396, a data rate of 1.19948 kBaud, and 2-FSK modulation. Note that these numbers aren't exactly right, but that's something you'd better get used to in low-power radio. Deviation in SmartRF is twice the shift, so for the +/- 4.5kHz shift, I chose a 9.52kHz deviation. For the RX Filter BW, I figure I'll be off by some significant amount of drift, so 101kHz seemed reasonable.

Now that we have roughly correct radio settings, we can export them from SmartRF into a Python script and try to catch our first packet.

Receiving the First Raw Packets

Eventually I settled on these settings, which I defined in goodwatch.py for prototyping a receiver though a GoodWatch over its UART and an FTDI cable. I started with SmartRF's suggestions, then checked each register in the CC1101 programmer's guide until (1) what I transmitted looked roughly like my MMDVM's POCSAG transmissions and finally (2) some real packets came back!

# 1200 Baud POCSAG for DAPNET
pocsagconfig=[
    MDMCFG4, 0xF5,      #  Modem Configuration, wide BW
    #MDMCFG4, 0xC5,      #  Modem Configuration, narrow BW
    MDMCFG3, 0x83,      #  Modem Configuration
    MDMCFG2, 0x82,      #  2-FSK, current optimized, 16/16 sync
    MDMCFG1, 0x72,      #  Long preamble.
    # FREND0 , 0x11,      #  Front End TX Configuration

    # #DEVIATN, 0x24,      # 9.5 kHz
    DEVIATN, 0x31,      # 15 kHz
    
    FSCAL3 , 0xE9,      #  Frequency Synthesizer Calibration
    FSCAL2 , 0x2A,      #  Frequency Synthesizer Calibration
    FSCAL1 , 0x00,      #  Frequency Synthesizer Calibration
    FSCAL0 , 0x1F,      #  Frequency Synthesizer Calibration
    
    PKTCTRL0, 0x00,     #  Packet automation control, fixed length without CRC.
    PKTLEN,  60,        #  PKTLEN    Packet length.

    SYNC1,   0x83,  # 832d first
    SYNC0,   0x2d,
    ADDR,    0xea,  # ea27 next, but we can only match one piece of it.

    TEST2,   0x81, #Who knows?
    TEST1,   0x35,
    TEST0,   0x09,

    MCSM1,   0x30,   # MCSM1, return to IDLE after packet.  Or with 2 for TX carrier tes.
    MCSM0,   0x10,   # MCSM0     Main Radio Control State Machine configuration.
    IOCFG2,  0x29,   # IOCFG2    GDO2 output pin configuration.
    IOCFG0,  0x06,   # IOCFG0    GDO0 output pin configuration.
    
    FIFOTHR,  0x47,  # RX FIFO and TX FIFO Thresholds
    #PKTCTRL1, 0x00,  # No address check, no status.
    PKTCTRL1, 0x01,  # Exact address check, no status.
    
    0, 0
];

My early reception code looked roughly like this. The CC1101 can only match the first two bytes of the SYNC/SFD field, so I let the hardware match the first two bytes (0x832d) with the SYNC1 and SYNC0 registers, then manually verify that the first two bytes of my packet are what follow.

    if args.pocsag!=None:
        time.sleep(1);
        goodwatch.radioonoff(1);
        print "Configuring radio.";
        goodwatch.radioconfig(beaconconfig);
        goodwatch.radioconfig(pocsagconfig);
        #Standard DAPNET frequency.
        goodwatch.radiofreq(439.988);
        while 1:
            pkt=goodwatch.radiorx();
            if len(pkt)>1 and pkt[0]=='\xea' and pkt[1]=='\x27':
                print pkt.encode('hex');
            time.sleep(0.1);

And hey, my simple receiver gets some frames!

nuc16% ./goodwatch.py --pocsag
Configuring radio.
ea27fffca65f72d4d8d4132e43ee365ceb6c172e56114f9d2f67658f2c5054d932852e4366e05ce9743d2e54f8fa1d2e58450ffffd7c85763e6885760dff
ea27ffd8dac83b6df8797e663a502febb3db85763e6885763e6885763e6885763e6885763e6885763e6885763e6885763e6885763e6885763e68857611ff
ea27fffcc3b872d4d8d4132e43ee2e58ea111b2e535a4f9d2f67658f2c5054d932852e42e20f58e9b7aa2e54f8fa1d2e58450ffffd7c85763e68857610ff
ea27fffc80b33264c9de1926491c5bb770786ec9933b31a34b7c658f9fdf52e548e73963a1fb6cf9f3e83ffffb2385763e6885763e6885763e68857610ff

Decoding a POCSAG Batch by Hand

So now we can grab POCSAG batches from the air, but we need to decode them before they will be of any use. I recommend that you follow step by step when writing your own decoder, making sure that you understand each stage before moving on to the next.

First we will need to flip the bits, because POCSAG defines a 1 as the lower FSK frequency but our radio uses the opposite convention. We will then need to parse through the eight frames (16 blocks) of each batch to identify the address, data and idle blocks.

So given a packet from the air, let's break it into codewords.

ea2785763e6885763e6885763e6885763e6885763e6885763e6885763e6885763e68f705a1d4162da038651ea64b547ed514609ffa8d85763e6885760fff

First, we invert the bits, toss the first two bytes (the last two of the FCS), and break the remainder into 32-bit words. From the POCSAG standard, we know that the 32-bit codewords are data if they begin with a 1 and addresses if they begin with a 0, and that the 7a89c197 codewords are idle and represent no data.

FCS | IDLE   | IDLE   |  IDLE  | IDLE   |  IDLE  |  IDLE  |  IDLE  |  IDLE  |  ADR   | DATA   | DATA   | DATA   | DATA   | IDLE   | DAMAGE
15d8 7a89c197 7a89c197 7a89c197 7a89c197 7a89c197 7a89c197 7a89c197 7a89c197 08fa5e2b e9d25fc7 9ae159b4 ab812aeb 9f600572 7a89c197 7a89f000

So why does the transmitter waste eight idle words before our target address? It's a fancy little trick of POCSAG, as you'll see in a moment, that the least significant three bits of the address are encoded as the position within the frame! This feels wasteful for alphanumeric pages, but it presumably made sense on congested national networks when the standard was first proposed and alphanumeric paging a rare luxury.

Our recipient's address is thus contained in the ninth block, 08fa5e2b. Address blocks take the following format.

Decoding of 08fa5e2b.

MSBIT                                                              LSBIT
 0 | 18-bit Address          | 2-bit Function | 10-bit BCH Check | Parity
 0 | 000 1000 1111 1010 010  |           1 1  |     110 0010 101 |   1

The first bit is zero by necessity, because this is an Address block. The Function code of 0b11 implies that an alphanumeric message follows in the upcoming data blocks.

The upper eighteen bits of the 21-bit address are 000 1000 1111 1010 010, and we are in the fifth block pair of the batch, so the destination is address is (0b000100011111010010<<3)|4 or 147092. (The block pair index is four, not five, because of zero indexing.)

The BCH check is a nifty little error correction field that can correct one or even two bit errors, but unlike the forward error correction of other protocols, can you ignore this field as a receiver with good signal strength. Except for your radio link budget, it's entirely optional!

The final bit acts as a poor man's sanity check of even parity. Even if you don't bother to perform the BCH correction, you should check this bit to know that the receiver hasn't gone completely off the rails.

So now we know that the recipient is 147092, it's time to decode the body of the message, which stretches across the data frames e9d25fc7, 9ae159b4, ab812aeb, and 9f600572.

We'll begin with e9d25fc7,

Decoding of e9d25fc7, 9ae159b4, ab812aeb, 9f600572.

1 | 20-bit Data               | 10-bit BCH   | Parity
1 | 110 1001 1101 0010 0101 1 | 111 1100 011 | 1
1 | 001 1010 1110 0001 0101 1 | 001 1011 010 | 0
1 | 010 1011 1000 0001 0010 1 | 010 1110 101 | 1
1 | 001 1111 0110 0000 0000 0 | 101 0111 001 | 0

So how in hell do we get text out of twenty bytes? Each letter is ASCII encoded, but the least-significant bit comes first as in RS232. The twenty bits in a code word contain just one bit less than three letters.

So concatenating the data and then chopping it apart, we see that the first seven bits are 1101001. If we flip this around and decode that, we find that chr(0b1001011)=='K', the first letter of our message! The second letter is also a K, and you can figure out the rest of the message yourself with pen and paper.

Just as in the address packets, the BCH error correction can be ignored if you signal is strong.

Long Frames

Just a few problems remain, but the worst of them is that POCSAG can send many groups in a single transmission! So even though a batch is limited to 64 bytes (16 codewords of 32 bytes apiece), there might very well be many batches in a transmission.

And as luck would have it, the radio core of the CC430 only has 64 bytes of storage, two of which are taken by the end of the FCS synchronization word!

Luckily, we can receive very long packets by fetching bytes out of the 64-byte ring buffer in the CPU as the radio is populating that same buffer. This is described in Section 25.3.3.4.6 in SLAU259E, the CC430 Family Guide. In implementing the long frame, it is best to trigger a callback for each 32-bit codeword, dropping the reception when the expected FCS of the next batch is missed.

FINISHME

Decode POCSAG Frames in C Firmware

So now that we can receive frames, chop them apart, and read their data, it's time to port this code to run in C on the device firmware.

As always, working in firmware is a pain that we'd rather separate from testing, so let's write a POCSAG library in C that can be tested on the host and only later deployed to the device. We also need to keep in mind that there will be a lot of bit shuffling, as our pen and paper examples were big endian and the MSP430 is little endian.

Let's begin with some ground rules: The code must compile to both AMD64 for testing and MSP430 for deployment. It must be small, and it must be legible. You will find it in the code repo as firmware/libs/pocsag.c.

Given a 32-bit codeword, we can parse it easily enough.

void pocsag_handleword(uint32_t word){
  if(word==0x7a89c197){         //IDLE
    /* Just ignore idle frames, but don't yet return because we need
       to count them.
     */
  }else if(word&0x80000000){    //DATA
    pocsag_handledataword(word);
  }else if(!(word&0x80000000)){ //ADDRESS
    /* 18 bits of the address come from the address word's payload,
       but the lowest three bits come from the frame count, which is
       half of the word count.
     */
    pocsag_lastid=((word>>10)&0x1ffff8) | ((wordcount>>1)&7);
  }

  /* Increment the word count, because we need it to decode the
     address.
   */
  wordcount++;
}

Data words are a bit trickier to parse, but only because the bits come in an awkward order. If we had support for error correction, we could apply it before these parser functions.

//! Local function to handle alphanumeric data payloads.
static void pocsag_handledataword(uint32_t word){
  //Twenty bits of this word.
  uint32_t bits=((word&0x7FFFFFFF)>>11);
  //Index within those bits.
  int i;
  //Latest bit that we're sampling.
  int newbit;

  for(i=19; i>=0; i--){
    //This grabs the bits in line order. As in RS232, the least
    //significant bit comes first.
    newbit=((bits>>i) & 1) ? 1 : 0;
    bitcount++;
    
    //Shift that into the top of the new char.
    newchar = newchar>>1;
    newchar|=(newbit<<6);
    
    //When the character is complete, it's already been rearranged to
    //the right order.
    if(bitcount==7){
      //Record the character.
      pocsag_buffer[bytecount++]=newchar;
      //Safety first.
      if(bytecount>MAXPAGELEN-1)
        bytecount=0;
      //Clear our counts to start again.
      bitcount=0;
      newchar=0;
    }
  }
}

As C code is portable, the last thing we'd want to do it restrict our parsing library to only run within the watch. Instead, we can have a nice little test case that runs in Unix, with assert() bailing out when anything code wrong.

#ifdef STANDALONE

#include<stdio.h>
#include<assert.h>

//! Unix command-line tool for testing.
int main(){
  int i;
  
  //Initialize a new batch.
  pocsag_newbatch();
  //Run eight IDLE words.
  for(i=0; i<8; i++){
    //Wordcount must be right or the address will be wrong.
    assert(wordcount==i);
    pocsag_handleword(0x7a89c197);
  }
  
  //Now we provide the address word, and check the ID and function.
  pocsag_handleword(0x08fa5e2b);
  //Is the ID correctly identified?
  assert(pocsag_lastid==147092);

  //This will populate the string with the incoming message.
  pocsag_handleword(0xe9d25fc7);
  pocsag_handleword(0x9ae159b4);
  pocsag_handleword(0xab812aeb);
  pocsag_handleword(0x9f600572);

  //Idle as we're done.
  pocsag_handleword(0x7a89c197);

  //Did we get the right message?
  assert(!strcmp(pocsag_buffer,"KK4VCZ: Jo"));

  printf("%d: %s\n",
         pocsag_lastid,
         pocsag_buffer);
  
  //Damaged frame.
  pocsag_handleword(0x7a89f000);
  
  return 0; //Doesn't work yet.
}

#endif

An Applet to Receiver Frames

Tossing this into an applet isn't much trouble, and you can find that code in the repo as applets/pager.c. In this abbreviated version of pager_packetrx(), we see how the incoming bytes of the packet are chopped up and converted from big endian for pocsag_handleword().

//! Handle an incoming packet.
void pager_packetrx(uint8_t *packet, int len){
  /* When the packet arrives, we need to chunk it into the pocsag
     library. */
  int i;
  uint32_t *words;
  
  /* See pocsag.c for decoder info, but the jist is that the first two
     bytes are the last two bytes of the FCS, and following words come
     in 4-byte words.  The pocsag library expects these words to have
     already been bitflipped (^=0xFFFFFFFF) and loaded as 32-bit *BIG
     ENDIAN* words.  (The MSP430 is little endian.  Sorry.)

     /FCS\ /--word0--\ /--word1--\ /--word2--\ ..
     ea 27 ff d8 da c8 3a ee f9 6c 7e 66 3a 50 ..
  */

  /* Forgive this pointer arithmetic, but in my own head, it really
     makes more sense this way.  First we skip the first two bytes (ea
     27) to get an array of the packet words.
   */
  words=(uint32_t*) (packet+2);

  /* We've triggered at the beginning of the batch, so we inform the
     pocsag library of that and then have it handle each word in
     sequence.  __builtin_bswap32() is a GCC primitive to swap a
     32-bit word, much like htonl() would do.
  */
  pocsag_newbatch();
  for(i=0;i<16;i++){
    pocsag_handleword(__builtin_bswap32(words[i])^0xFFFFFFFF);
  }

  ...

Finally, the received messages are in globals pocsag_lastid and pocsag_buffer for placing on the LCD. The moment of when to sample them is a bit tricky, though; other users might have incoming messages in the same batch. One method might be to copy out the message after each word in which the recipient RIC matches.

Power Budget

But now that we are receiving messages, we stumble into a pretty big problem. The radio draw 16mA when actively receiving, nearly five thousand times the idle current draw of the watch!

If we don't want our receiver to die in mere hours, we need some sort of trick, and thankfully--thank goodness!--POCSAG provides one in the form of a 480ms preamble. The idea is that the transmitter will waste nearly half a second sending nothing but alternating ones and zeroes, so that the receivers can wake slightly more than once a second to look for it. At the very least, we can drop from RX mode (16mA) to IDLE mode (1mA), averaging 4mA of consumption and giving us 24 hours of battery life.

To detect the preamble, simply drop the packet length to be very short and set the SYNC fields to AA. This way, the radio will trigger on the preamble long before the real start of the packet, and you can know to stay awake and in RX mode until the packet arrives.

/* Settings to match on the preamble, for waking up.
 */
static const uint8_t pocsag_settings_preamble[]={
  PKTLEN,  3,        // PKTLEN    Packet length.
  
  SYNC1, 0xAA,       // Triggers an early match if the preamble is heard.
  SYNC0, 0xAA,
  ADDR,  0xAA,
  
  0, 0
};

Another power management trick is to exit the pager application when out of range. By default the GoodWatch returns to the clock application if app_cleartimer() isn't called for three minutes, so by calling it from our packet handler function, we can have the pager application exit when out of range for a few minutes.

Checksums and Error Correction

POCSAG happily allows for receivers that don't bother with error correction, but if we know that the packet is ours and we also know that it's damaged, we ought to attempt to recover it.

FINISHME