├── .gitignore ├── ARDUINO3V3.md ├── LICENSE ├── PocsagTranscoder.ino ├── README.md ├── RF7021TCXO.md ├── img ├── arduino3v3.jpg ├── grandcentral.jpg ├── rf7021tcxo1.jpg ├── rf7021tcxo2.jpg ├── rf7021tcxo3.jpg └── rf7021tcxo4.jpg └── src ├── PocsagEncoder.cpp ├── PocsagEncoder.h ├── Rf7021.cpp └── Rf7021.h /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !*.ino 4 | !*.cpp 5 | !*.h 6 | !*.md 7 | !*.jpg 8 | !*.png 9 | !*.gif 10 | !*/ -------------------------------------------------------------------------------- /ARDUINO3V3.md: -------------------------------------------------------------------------------- 1 | # Arduino Nano 3.3V conversion 2 | 3 | + Remove diode 4 | + Replace AMS1117-5.0 with AMS1117-3.3 5 | + Solder jumper wires as shown: 6 | 7 | ![Arduino](img/arduino3v3.jpg) 8 | 9 | + Make sure the Arduino is working properly and the maximum pin output voltage is below 3.3V -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 SinuX 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PocsagTranscoder.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * POCSAG transmitter example for Arduino Nano 3 | * 4 | * RF7021SE - Arduino wiring: 5 | * VCC - VCC (Don't forget to convert your Arduino to run on 3.3V first!) 6 | * CE - D6 7 | * SLE - D7 8 | * SDATA - D8 9 | * SREAD - D9 10 | * SCLK - D10 11 | * TxRxCLK - D11 12 | * TxRxData - D12 13 | * GND - GND 14 | * 15 | * Serial console commands: 16 | * d or D XXX - set frequency deviation in Hz (e.g. "d 4500" sets deviation to 4.5KHz) 17 | * f or F XXX - set frequency in Hz (e.g. "f 433920000" sets frequency to 433.920MHz) 18 | * i or I X - enable/disable data inversion (e.g. "i 0" turns inversion off, "i 1" - turns on) 19 | * r or R XXXX - set data rate (512, 1200 or 2400, e.g. "r 512" sets rate to 512kbps) 20 | * x or X - run transmitter test (by default transmit 0101... pattern on selected frequency, see RF_TEST_MODE and RF_TEST_DURATION) 21 | * c or C XXXX - set CAP code (e.g. "c 1234567") 22 | * s or S X - set message source code (0 to 3, e.g. "s 3") 23 | * e or E X - set encoding (0 to 3, e.g. "e 0" sets Latin Motorola encoding) 24 | * n or N XXXX - send numeric message (e.g. "n U *123*456*789") 25 | * m or M XXXX - send alphanumeric message (e.g. "m Hello World") 26 | * t or T - send tone message 27 | * 28 | * Copyright (c) 2021, SinuX. All rights reserved. 29 | * 30 | * This library is distributed "as is" without any warranty. 31 | * See MIT License for more details. 32 | */ 33 | #include "src/Rf7021.h" 34 | #include "src/PocsagEncoder.h" 35 | 36 | // RF7021 Wiring 37 | #define RF_CE_PIN 6 38 | #define RF_SLE_PIN 7 39 | #define RF_SDATA_PIN 8 40 | #define RF_SREAD_PIN 9 41 | #define RF_SCLK_PIN 10 42 | #define RF_TX_CLK_PIN 11 43 | #define RF_TX_DATA_PIN 12 44 | 45 | // RF7021 settings 46 | #define RF_TCXO 12288000UL // 12.288 MHz TCXO 47 | #define RF_PRINT_ADF_INFO // Read and print chip revision, temperature and voltage at startup 48 | #define RF_TEST_MODE 4 // Test mode - "010101..." pattern 49 | #define RF_TEST_DURATION 10000 // Test duration 10 seconds 50 | 51 | // Pocsag default settings 52 | #define PCSG_DEFAULT_DATA_RATE 1200 // 1200bps 53 | #define PCSG_DEFAULT_FREQ 433000000UL // 433.000 MHz 54 | #define PCSG_DEFAULT_FREQ_DEV 4500 // 4.5 KHz 55 | #define PCSG_DEFAULT_CAP_CODE 1234567 // 7th frame 56 | #define PCSG_DEFAULT_SOURCE 3 // Alphanumeric 57 | #define PCSG_DEFAULT_ENCODING 0 // Latin (Motorola) 58 | 59 | Rf7021 rf = Rf7021(); 60 | PocsagEncoder pocsagEncoder = PocsagEncoder(); 61 | 62 | void setup() { 63 | // Set transmitter control pins 64 | rf.setCEPin(RF_CE_PIN); 65 | rf.setSLEPin(RF_SLE_PIN); 66 | rf.setSDATAPin(RF_SDATA_PIN); 67 | rf.setSREADPin(RF_SREAD_PIN); 68 | rf.setSCLKPin(RF_SCLK_PIN); 69 | rf.setTxRxCLKPin(RF_TX_CLK_PIN); 70 | rf.setTxRxDataPin(RF_TX_DATA_PIN); 71 | 72 | Serial.begin(9600); 73 | 74 | #ifdef RF_PRINT_ADF_INFO 75 | // Print ADF readback info 76 | Serial.print(F("Silicon rev: ")); 77 | Serial.println(rf.getSiliconRev(), HEX); 78 | Serial.print(F("Temp °C: ")); 79 | Serial.println(rf.getTemp()); 80 | Serial.print(F("Voltage: ")); 81 | Serial.println(rf.getVoltage()); 82 | #endif 83 | 84 | // Apply default frequency and deviation 85 | rf.setXtalFrequency(RF_TCXO); 86 | rf.setHasExternalInductor(1); 87 | rf.setDataRate(PCSG_DEFAULT_DATA_RATE); 88 | rf.setFrequency(PCSG_DEFAULT_FREQ); 89 | rf.setFrequencyDeviation(PCSG_DEFAULT_FREQ_DEV); 90 | 91 | // Apply default encoder params 92 | pocsagEncoder.setCapCode(PCSG_DEFAULT_CAP_CODE); 93 | pocsagEncoder.setSource(PCSG_DEFAULT_SOURCE); 94 | pocsagEncoder.setEncodingId(PCSG_DEFAULT_ENCODING); 95 | //pocsagEncoder.setPrintMessage(1); 96 | } 97 | 98 | void loop() { 99 | if (Serial.available() > 0) { 100 | char command = Serial.read(); 101 | 102 | if (command == 'd' || command == 'D') { 103 | // d or D XXXX - set frequency deviation in Hz 104 | uint32_t freqDev = Serial.parseInt(); 105 | rf.setFrequencyDeviation(freqDev); 106 | Serial.print(F("Frequency Deviation: ")); 107 | Serial.print(freqDev / 1000.0, 2); 108 | Serial.println(F(" KHz")); 109 | } else if (command == 'f' || command == 'F') { 110 | // f or F XXXX - set frequency in Hz 111 | uint32_t freq = Serial.parseInt(); 112 | if (freq >= 80000000 && freq <= 950000000) { 113 | rf.setFrequency(freq); 114 | Serial.print(F("Frequency: ")); 115 | Serial.print(freq / 1000000.0, 3); 116 | Serial.println(F(" MHz")); 117 | } else { 118 | Serial.println(F("Wrong frequency, should be 80000000 to 950000000")); 119 | } 120 | } else if (command == 'i' || command == 'I') { 121 | // i or I X - set data inversion (0 - disabled, 1 - enabled) 122 | byte inv = Serial.parseInt(); 123 | rf.setDataInvertEnabled(inv ? 1 : 0); 124 | Serial.print(F("Inversion: ")); 125 | Serial.println(inv ? F("enabled") : F("disabled")); 126 | } else if (command == 'r' || command == 'R') { 127 | // r or R XXXX - set data rate (512, 1200 or 2400) 128 | word rate = Serial.parseInt(); 129 | if (rate == 512 || rate == 1200 || rate == 2400) { 130 | rf.setDataRate(rate); 131 | Serial.print(F("Data rate: ")); 132 | Serial.println(rate); 133 | } else { 134 | Serial.println(F("Wrong data rate, should be 512, 1200 or 2400")); 135 | } 136 | } else if (command == 'x' || command == 'X') { 137 | // x or X - run transmitter test 138 | Serial.println(F("Testing...")); 139 | rf.txTest(RF_TEST_MODE, RF_TEST_DURATION); 140 | Serial.println(F("Test completed")); 141 | } else if (command == 'c' || command == 'C') { 142 | // c or C XXXX - set CAP code 143 | uint32_t cap = Serial.parseInt(); 144 | pocsagEncoder.setCapCode(cap); 145 | Serial.print(F("CAP code: ")); 146 | Serial.println(cap); 147 | } else if (command == 's' || command == 'S') { 148 | // s or S X - set message source code (0 to 3) 149 | byte src = Serial.parseInt(); 150 | if (src <= 3) { 151 | pocsagEncoder.setSource(src); 152 | Serial.print(F("Source code: ")); 153 | Serial.println(src); 154 | } else { 155 | Serial.println(F("Wrong source code, should be 0, 1, 2 or 3")); 156 | } 157 | } else if (command == 'e' || command == 'E') { 158 | // e or E X - set encoding (0 to 3) 159 | byte enc = Serial.parseInt(); 160 | if (enc <= 3) { 161 | pocsagEncoder.setEncodingId(enc); 162 | Serial.print(F("Encoding: ")); 163 | Serial.print(enc); 164 | Serial.println(enc == 0 ? F(" (EN Motorola)") : enc == 1 ? F(" (EN/RU Motorola)") : enc == 2 ? F(" (EN/RU Philips)") : enc == 3 ? F(" (RU Motorola)") : F(" (Unknown)")); 165 | } else { 166 | Serial.println(F("Wrong encoding, should be 0, 1, 2 or 3")); 167 | } 168 | } else if (command == 'n' || command == 'N') { 169 | // n or N XXXX - send numeric message 170 | Serial.println(F("Message encoding...")); 171 | PocsagEncoder::PocsagMessage message = pocsagEncoder.encodeNumeric(Serial.readString()); 172 | Serial.println(F("Sending...")); 173 | rf.sendMessage(message.dataBytes, message.messageLength); 174 | Serial.println(F("Message sent")); 175 | } else if (command == 'm' || command == 'M') { 176 | // m or M XXXX - send alphanumeric message 177 | Serial.println(F("Message encoding...")); 178 | PocsagEncoder::PocsagMessage message = pocsagEncoder.encodeAlphanumeric(Serial.readString()); 179 | Serial.println(F("Sending...")); 180 | rf.sendMessage(message.dataBytes, message.messageLength); 181 | Serial.println(F("Message sent")); 182 | } else if (command == 't' || command == 'T') { 183 | // t or T - send tone message 184 | Serial.println(F("Message encoding...")); 185 | PocsagEncoder::PocsagMessage message = pocsagEncoder.encodeTone(); 186 | Serial.println(F("Sending...")); 187 | rf.sendMessage(message.dataBytes, message.messageLength); 188 | Serial.println(F("Message sent")); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arduino POCSAG Transcoder 2 | 3 | ![Pager](img/grandcentral.jpg) 4 | 5 | ## Features 6 | 7 | + Message types: `Tone`, `Numeric`, `Alphanumeric` 8 | + 4 encodings: `Latin`, `2xLatin/Cyrillic`, `Cyrillic` 9 | + Data rates: `512`, `1200`, `2400bps` 10 | + Data inversion 11 | + Frequency range: `80 – 650MHz`, `862 – 940MHz` (see ADF7021 datasheet) 12 | 13 | ## Required parts 14 | 15 | + Arduino Nano running at `3.3V` ([How to convert](ARDUINO3V3.md)) 16 | + RF7021SE module with `12.288Mhz` or `14.745MHz` TCXO ([How to install](RF7021TCXO.md)) 17 | 18 | ## Wiring 19 | 20 | | RF7021 | Arduino | 21 | | ------ | ------- | 22 | | VCC | VCC | 23 | | CE | D6 | 24 | | SLE | D7 | 25 | | SDATA | D8 | 26 | | SREAD | D9 | 27 | | SCLK | D10 | 28 | | TxRxCLK| D11 | 29 | | TxRxData| D12 | 30 | | GND | GND | 31 | 32 | ## Serial console commands 33 | 34 | | Command | Description | Example | 35 | | --- | --- | --- | 36 | | d or D`XXX` | Set frequency deviation in Hz | `d 4500` - set deviation to 4.5KHz | 37 | | f or F`XXX` | Set frequency in Hz | `f 433920000` - set freq to 433.920MHz | 38 | | i or I`X` | Enable/disable data inversion (`0` = OFF, `1` = ON) | `i 0` | 39 | | r or R`XXXX` | Set data rate (`512`, `1200` or `2400`) | `r 512` - set rate to 512kbps | 40 | | x or X | Run transmitter test (transmit 0101... pattern) | `x` | 41 | | c or C`XXXX` | Set CAP code | `c 1234567` | 42 | | s or S`X` | Set message source code (`0` to `3`) | `s 3` | 43 | | e or E`X` | Set encoding (`0` to `3`) | `e 0` - set Latin Motorola encoding | 44 | | n or N`XXXX` | Send numeric message | `n U *123*456*789` | 45 | | m or M`XXXX` | Send alphanumeric message | `m Hello World` | 46 | | t or T | Send tone message | `t` | 47 | 48 | ## Troubleshooting 49 | 50 | | Problem | Possible Solution | 51 | | --- | --- | 52 | | Nothing happens in Serial Console after Arduino connection or device getting stuck | Check Arduino - RF7021SE wiring, try to reset | 53 | | RF7021 doesn't transmit or emits on wrong frequency | Check TCXO connection and `RF_TCXO` value | 54 | | Pager doesn't receive messages | Check RF7021 emission, pager frequency, CAP code, data rate and inversion mode | -------------------------------------------------------------------------------- /RF7021TCXO.md: -------------------------------------------------------------------------------- 1 | # RF7021SE TCXO replacement 2 | 3 | + Find default `19.68MHz` TCXO on the PCB: 4 | 5 | ![RF7021](img/rf7021tcxo1.jpg) 6 | 7 | + And unsolder it: 8 | 9 | ![RF7021](img/rf7021tcxo2.jpg) 10 | 11 | + Replace with `12.288Mhz` or `14.745MHz`. Default footprint is 3225, but 5032 can be placed as well: 12 | 13 | ![RF7021](img/rf7021tcxo3.jpg) 14 | 15 | + To use 5032 chip you may have to use VCC workaround: 16 | 17 | ![RF7021](img/rf7021tcxo4.jpg) 18 | 19 | + Don't forget to change the `RF_TCXO` value to match your TCXO! -------------------------------------------------------------------------------- /img/arduino3v3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SinuXVR/arduino-pocsag-transcoder/3fa0326a501f68e39692683ae72fa0cab1e9c946/img/arduino3v3.jpg -------------------------------------------------------------------------------- /img/grandcentral.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SinuXVR/arduino-pocsag-transcoder/3fa0326a501f68e39692683ae72fa0cab1e9c946/img/grandcentral.jpg -------------------------------------------------------------------------------- /img/rf7021tcxo1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SinuXVR/arduino-pocsag-transcoder/3fa0326a501f68e39692683ae72fa0cab1e9c946/img/rf7021tcxo1.jpg -------------------------------------------------------------------------------- /img/rf7021tcxo2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SinuXVR/arduino-pocsag-transcoder/3fa0326a501f68e39692683ae72fa0cab1e9c946/img/rf7021tcxo2.jpg -------------------------------------------------------------------------------- /img/rf7021tcxo3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SinuXVR/arduino-pocsag-transcoder/3fa0326a501f68e39692683ae72fa0cab1e9c946/img/rf7021tcxo3.jpg -------------------------------------------------------------------------------- /img/rf7021tcxo4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SinuXVR/arduino-pocsag-transcoder/3fa0326a501f68e39692683ae72fa0cab1e9c946/img/rf7021tcxo4.jpg -------------------------------------------------------------------------------- /src/PocsagEncoder.cpp: -------------------------------------------------------------------------------- 1 | #include "PocsagEncoder.h" 2 | 3 | PocsagEncoder::PocsagEncoder() { 4 | } 5 | 6 | void PocsagEncoder::setCapCode(uint32_t capCode) { 7 | this->capCode = capCode; 8 | } 9 | 10 | void PocsagEncoder::setSource(byte source) { 11 | this->source = source; 12 | } 13 | 14 | void PocsagEncoder::setEncodingId(byte encodingId) { 15 | this->encodingId = encodingId; 16 | } 17 | 18 | void PocsagEncoder::setPrintMessage(byte printMessage) { 19 | this->printMessage = printMessage; 20 | } 21 | 22 | static word calculateBCH(uint32_t cw) { 23 | uint32_t generator = 0xED200000; 24 | uint32_t mask = 0x80000000; 25 | 26 | for (byte i = 0; i < 21; i++) { 27 | if (cw & mask) { 28 | cw ^= generator; 29 | } 30 | generator >>= 1; 31 | mask >>= 1; 32 | } 33 | 34 | return cw >> 1; 35 | } 36 | 37 | static byte calculateParity(uint32_t cw) { 38 | byte parity = 0; 39 | 40 | for (byte i = 0; i < 32; i++) { 41 | if (cw & 0x80000000) { 42 | parity ^= 1; 43 | } 44 | cw <<= 1; 45 | } 46 | 47 | return parity; 48 | } 49 | 50 | static void flipEndianess(uint32_t *value) { 51 | uint8_t *bytes = (uint8_t*) value; 52 | uint8_t tmp = bytes[0]; 53 | bytes[0] = bytes[3]; 54 | bytes[3] = tmp; 55 | tmp = bytes[1]; 56 | bytes[1] = bytes[2]; 57 | bytes[2] = tmp; 58 | } 59 | 60 | static void fillCW(PocsagEncoder::PocsagCW *codeWord, byte messageType, uint32_t payloadBits) { 61 | codeWord->data = 0; 62 | codeWord->messageType = messageType; 63 | codeWord->payloadBits = payloadBits; 64 | codeWord->crcBits = calculateBCH(codeWord->data); 65 | codeWord->parityBits = calculateParity(codeWord->data); 66 | flipEndianess(&codeWord->data); 67 | } 68 | 69 | static void fillAddressCW(PocsagEncoder::PocsagCW *codeWord, uint32_t capCode, byte source) { 70 | fillCW(codeWord, 0, ((capCode >> 3) << 2) | source); 71 | } 72 | 73 | static void fillMessageCW(PocsagEncoder::PocsagCW *codeWord, uint32_t messageBits) { 74 | fillCW(codeWord, 1, messageBits); 75 | } 76 | 77 | // Fill preamble bytes with 0xAA and frames with idle code words, fill address code word and return first frame number 78 | static byte prepareMessage(PocsagEncoder::PocsagMessage *message, uint32_t capCode, byte source) { 79 | // Fill preamble 80 | for (byte i = 0; i < PCSG_PREAMBLE_LENGTH; i++) { 81 | message->preamble[i] = 0xAA; 82 | } 83 | // Fill frames with idle code words 84 | for (byte i = 0; i < PCSG_BATCHES_COUNT; i++) { 85 | message->batches[i].fsc = 0xD814D27C; // 0x7CD214D8 86 | for (byte j = 0; j < 8; j++) { 87 | message->batches[i].frames[j].codeWords[0].data = 0x97C1897A; // 0x7A89C197 88 | message->batches[i].frames[j].codeWords[1].data = 0x97C1897A; // 0x7A89C197 89 | } 90 | } 91 | 92 | // Determine first frame number 93 | byte currentFrame = capCode & 0x7; 94 | 95 | // Fill address code word 96 | fillAddressCW(&message->batches[0].frames[currentFrame].codeWords[0], capCode, source); 97 | 98 | return currentFrame; 99 | } 100 | 101 | // Convert 1-byte ASCII character to 5-bit Pocsag digit 102 | byte PocsagEncoder::getPocsagNumericCharacter(byte asciiChar) { 103 | for (byte i = 0; i < 16; i++) { 104 | if (pgm_read_byte(&PCSG_ALPHABET[i][0]) == asciiChar) { 105 | return pgm_read_byte(&PCSG_ALPHABET[i][1]); 106 | } 107 | } 108 | 109 | return 0xC; // ' ' (space) - unknown digit 110 | } 111 | 112 | // Convert 2-byte Latin or Cyrillic UTF-8 character to 1-byte Windows-1251 113 | static inline byte utf8ToWindows1251(word character, byte flipCase) { 114 | // Latin 115 | if (character <= 0x7E) { 116 | if (flipCase && character >= 0x41 && character <= 0x7A) { 117 | return character ^ (1 << 5); 118 | } 119 | return character; 120 | } 121 | 122 | // Cyrillic 123 | // Special cyrillic characters workaround 124 | switch(character) { 125 | case 0xD081: if (flipCase) return 184; else return 168; // Ё/ё 126 | case 0xD191: if (flipCase) return 168; else return 184; // ё/Ё 127 | case 0xD083: if (flipCase) return 131; else return 129; // Ѓ/ѓ 128 | case 0xD193: if (flipCase) return 129; else return 131; // ѓ/Ѓ 129 | case 0xD084: if (flipCase) return 186; else return 170; // Є/є 130 | case 0xD194: if (flipCase) return 170; else return 186; // є/Є 131 | case 0xD087: if (flipCase) return 191; else return 175; // Ї/ї 132 | case 0xD197: if (flipCase) return 175; else return 191; // ї/Ї 133 | case 0xD08B: if (flipCase) return 158; else return 142; // Ћ/ћ 134 | case 0xD19B: if (flipCase) return 142; else return 158; // ћ/Ћ 135 | default: break; 136 | } 137 | 138 | // Main cyrillic characters 139 | if (character >= 0xD090 && character <= 0xD18F) { 140 | if (character >= 0xD090 && character <= 0xD0BF) { 141 | // 'А' - 'п' 142 | character -= 0xCFD0; 143 | } else if (character >= 0xD180 && character <= 0xD18F) { 144 | // 'р' - 'я' 145 | character -= 0xD090; 146 | } 147 | if (flipCase) { 148 | return character ^ (1 << 5); 149 | } 150 | return character; 151 | } 152 | 153 | return 0x3F; // '?' - unknown symbol; 154 | } 155 | 156 | // Convert 2-byte Latin or Cyrillic UTF-8 character to 7-bit Pocsag character 157 | byte PocsagEncoder::getPocsagAlphanumericCharacter(word utf8Char, byte encodingId) { 158 | byte winChar = utf8ToWindows1251(utf8Char, 0); 159 | if (winChar == 0x3F) { 160 | return 0x3F; 161 | } 162 | 163 | // Search in common section 164 | for (byte i = 16; i < 48; i++) { 165 | if (pgm_read_byte(&PCSG_ALPHABET[i][0]) == winChar) { 166 | return pgm_read_byte(&PCSG_ALPHABET[i][1]); 167 | } 168 | } 169 | 170 | // Search in current case 171 | for (word i = PCSG_ENCODINGS[encodingId].offset; i < PCSG_ENCODINGS[encodingId].offset + PCSG_ENCODINGS[encodingId].length; i++) { 172 | if (pgm_read_byte(&PCSG_ALPHABET[i][0]) == winChar) { 173 | return pgm_read_byte(&PCSG_ALPHABET[i][1]); 174 | } 175 | } 176 | 177 | // Search in flipped case: 178 | winChar = utf8ToWindows1251(utf8Char, 1); 179 | for (word i = PCSG_ENCODINGS[encodingId].offset; i < PCSG_ENCODINGS[encodingId].offset + PCSG_ENCODINGS[encodingId].length; i++) { 180 | if (pgm_read_byte(&PCSG_ALPHABET[i][0]) == winChar) { 181 | return pgm_read_byte(&PCSG_ALPHABET[i][1]); 182 | } 183 | } 184 | 185 | return 0x3F; // '?' - unknown symbol 186 | } 187 | 188 | static void printMsg(PocsagEncoder::PocsagMessage message) { 189 | byte byteCounter = 0; 190 | byte wordCounter = 0; 191 | uint32_t byteAcc = 0; 192 | for (word i = PCSG_PREAMBLE_LENGTH; i < message.messageLength; i++) { 193 | byteAcc <<= 8; 194 | byteAcc |= message.dataBytes[i]; 195 | if (++byteCounter >= 4) { 196 | Serial.print(byteAcc, HEX); 197 | if (++wordCounter % 8 == 1) { 198 | Serial.println(); 199 | } else { 200 | Serial.print(" "); 201 | if (wordCounter >= 18) { 202 | wordCounter = 1; 203 | Serial.println(); 204 | } 205 | } 206 | byteCounter = 0; 207 | byteAcc = 0; 208 | } 209 | } 210 | } 211 | 212 | static void incrementCounters(byte *currentBatch, byte *currentFrame, byte *currentCodeWord) { 213 | (*currentCodeWord)++; 214 | if (*currentCodeWord >= 2) { 215 | *currentCodeWord = 0; 216 | (*currentFrame)++; 217 | if (*currentFrame >= 8) { 218 | *currentFrame = 0; 219 | (*currentBatch)++; 220 | } 221 | } 222 | } 223 | 224 | static void pushPocsagChar(byte pocsagChar, byte charSize, uint32_t *messageBits, byte *messageBitsLength, 225 | PocsagEncoder::PocsagMessage *message, byte *currentBatch, byte *currentFrame, byte *currentCodeWord) { 226 | for (byte b = 0; b < charSize; b++) { 227 | *messageBits |= pocsagChar & 1; 228 | (*messageBitsLength)++; 229 | 230 | // Push message bits to code word 231 | if (*messageBitsLength >= 20) { 232 | *messageBitsLength = 0; 233 | fillMessageCW(&message->batches[*currentBatch].frames[*currentFrame].codeWords[*currentCodeWord], *messageBits); 234 | incrementCounters(currentBatch, currentFrame, currentCodeWord); 235 | } 236 | 237 | *messageBits <<= 1; 238 | pocsagChar >>= 1; 239 | } 240 | } 241 | 242 | PocsagEncoder::PocsagMessage PocsagEncoder::encodeNumeric(String messageText) { 243 | PocsagMessage message; 244 | 245 | // Prepare message and fill address CW 246 | byte currentFrame = prepareMessage(&message, capCode, source); 247 | byte currentBatch = 0; 248 | byte currentCodeWord = 1; 249 | uint32_t messageBits = 0; 250 | byte messageBitsLength = 0; 251 | 252 | // Convert and push all characters to message code words 253 | for (word i = 0; i < messageText.length(); i++) { 254 | byte nextChar = messageText.charAt(i); 255 | 256 | // Skip leading space and non-printable symbols 257 | if ((i == 0 && nextChar == 0x20) || nextChar < 0x20) { 258 | continue; 259 | } 260 | 261 | // Translate to pocsag digit 262 | byte pocsagChar = getPocsagNumericCharacter(nextChar); 263 | 264 | // Push next digit to message bits 265 | pushPocsagChar(pocsagChar, 4, &messageBits, &messageBitsLength, &message, ¤tBatch, ¤tFrame, ¤tCodeWord); 266 | } 267 | 268 | // Fill empty space of last message codeword with 0xC character 269 | if (messageBitsLength > 0 && messageBitsLength < 20) { 270 | byte freeSpace = 20 - messageBitsLength; 271 | for (byte i = 0; i < freeSpace / 4; i++) { 272 | pushPocsagChar(0xC, 4, &messageBits, &messageBitsLength, &message, ¤tBatch, ¤tFrame, ¤tCodeWord); 273 | } 274 | } 275 | 276 | message.messageLength = PCSG_PREAMBLE_LENGTH + 68 * (currentBatch + 1); 277 | 278 | if (printMessage) { 279 | printMsg(message); 280 | } 281 | 282 | return message; 283 | } 284 | 285 | PocsagEncoder::PocsagMessage PocsagEncoder::encodeAlphanumeric(String messageText) { 286 | PocsagMessage message; 287 | 288 | // Prepare message and fill address CW 289 | byte currentFrame = prepareMessage(&message, capCode, source); 290 | byte currentBatch = 0; 291 | byte currentCodeWord = 1; 292 | uint32_t messageBits = 0; 293 | byte messageBitsLength = 0; 294 | 295 | // Convert and push all characters to message code words 296 | for (word i = 0; i < messageText.length(); i++) { 297 | word nextChar = messageText.charAt(i); 298 | 299 | // Skip leading space and non-printable symbols 300 | if ((i == 0 && nextChar == 0x20) || nextChar < 0x20) { 301 | continue; 302 | } 303 | 304 | // 2-byte UTF-8 symbol - read second byte (workaround for Cyrillic characters) 305 | if (nextChar & 0x80) { 306 | i++; 307 | nextChar <<= 8; 308 | nextChar |= (byte) messageText.charAt(i); 309 | } 310 | 311 | // Translate to pocsag alphabet using current encoding 312 | byte pocsagChar = getPocsagAlphanumericCharacter(nextChar, encodingId); 313 | 314 | // Check if there is enough space to put next char in the last code word of the last batch 315 | // We must preserve at least 7 bits to put EOT/ETX character at the end of the message 316 | if (currentBatch == PCSG_BATCHES_COUNT - 1 && currentFrame == 7 && currentCodeWord == 1) { 317 | // Skip all remaining chars because they won't fit 318 | if (messageBitsLength >= 7) { 319 | break; 320 | } 321 | } 322 | 323 | // Push next character to message bits 324 | pushPocsagChar(pocsagChar, 7, &messageBits, &messageBitsLength, &message, ¤tBatch, ¤tFrame, ¤tCodeWord); 325 | } 326 | 327 | // Push EOT symbol to the end of the message 328 | byte eotChar = PCSG_EOT_CHAR; 329 | pushPocsagChar(eotChar, 7, &messageBits, &messageBitsLength, &message, ¤tBatch, ¤tFrame, ¤tCodeWord); 330 | 331 | // Fill last message code word 332 | if (messageBitsLength > 0 && messageBitsLength < 20) { 333 | // Try to push additional EOTs 334 | byte freeSpace = 20 - messageBitsLength; 335 | for (byte i = 0; i < freeSpace / 7; i++) { 336 | pushPocsagChar(eotChar, 7, &messageBits, &messageBitsLength, &message, ¤tBatch, ¤tFrame, ¤tCodeWord); 337 | } 338 | // Trying to fill left empty space with EOT bits partially 339 | for (byte b = 0; b < 19 - messageBitsLength; b++) { 340 | messageBits |= eotChar & 1; 341 | messageBits <<= 1; 342 | eotChar >>= 1; 343 | } 344 | fillMessageCW(&message.batches[currentBatch].frames[currentFrame].codeWords[currentCodeWord], messageBits); 345 | } 346 | 347 | message.messageLength = PCSG_PREAMBLE_LENGTH + 68 * (currentBatch + 1); 348 | 349 | if (printMessage) { 350 | printMsg(message); 351 | } 352 | 353 | return message; 354 | } 355 | 356 | PocsagEncoder::PocsagMessage PocsagEncoder::encodeTone() { 357 | PocsagMessage message; 358 | 359 | // Fill address CW only 360 | prepareMessage(&message, capCode, source); 361 | message.messageLength = PCSG_PREAMBLE_LENGTH + 68; 362 | 363 | if (printMessage) { 364 | printMsg(message); 365 | } 366 | 367 | return message; 368 | } 369 | -------------------------------------------------------------------------------- /src/PocsagEncoder.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Simple yet fully functional POCSAG encoder implementation capable 3 | * of generating any kind of message (tone, numeric or alphanumeric) 4 | * 5 | * Alphanumeric messages can be encoded using one of 4 encodings 6 | * 7 | * Copyright (c) 2021, SinuX. All rights reserved. 8 | * 9 | * This library is distributed "as is" without any warranty. 10 | * See MIT License for more details. 11 | */ 12 | 13 | #ifndef _POCSAGENCODER_H_ 14 | #define _POCSAGENCODER_H_ 15 | 16 | #define PCSG_PREAMBLE_LENGTH 128 // Preamble length in bytes (must be >= 72) 17 | #define PCSG_BATCHES_COUNT 5 // Batches count (5 batches can handle 185 to 225 alphanumeric symbols) 18 | #define PCSG_MESSAGE_LENGTH PCSG_PREAMBLE_LENGTH + 68 * PCSG_BATCHES_COUNT // Max message length in bytes including preamble and all batches 19 | #define PCSG_EOT_CHAR 0x04 // Alphanumeric message text termination character code 20 | 21 | #include "Arduino.h" 22 | 23 | class PocsagEncoder { 24 | 25 | public: 26 | PocsagEncoder(); 27 | 28 | void setCapCode(uint32_t capCode); 29 | void setSource(byte source); 30 | void setEncodingId(byte encodingId); 31 | void setPrintMessage(byte printMessage); 32 | 33 | typedef union { 34 | struct { 35 | byte parityBits : 1; 36 | word crcBits : 10; 37 | uint32_t payloadBits : 20; // 18 address + 2 source bits or 20 message bits 38 | byte messageType : 1; 39 | }; 40 | uint32_t data; 41 | } PocsagCW; 42 | 43 | typedef struct { 44 | PocsagCW codeWords[2]; 45 | } PocsagFrame; 46 | 47 | typedef struct { 48 | uint32_t fsc; 49 | PocsagFrame frames[8]; 50 | } PocsagBatch; 51 | 52 | typedef struct { 53 | word messageLength; 54 | union { 55 | struct { 56 | byte preamble[PCSG_PREAMBLE_LENGTH]; 57 | PocsagBatch batches[PCSG_BATCHES_COUNT]; 58 | }; 59 | byte dataBytes[PCSG_MESSAGE_LENGTH]; 60 | }; 61 | } PocsagMessage; 62 | 63 | PocsagMessage encodeNumeric(String messageText); 64 | PocsagMessage encodeAlphanumeric(String messageText); 65 | PocsagMessage encodeTone(); 66 | 67 | private: 68 | uint32_t capCode; 69 | byte source; 70 | byte encodingId; 71 | byte printMessage; 72 | 73 | byte getPocsagNumericCharacter(byte asciiChar); 74 | byte getPocsagAlphanumericCharacter(word utf8Char, byte encodingId); 75 | 76 | struct { 77 | word offset; 78 | byte length; 79 | } PCSG_ENCODINGS[4] = { 80 | {48, 63}, // 0 - Latin (Motorola Advisor) 81 | {111, 75}, // 1 - Latin/Cyrillic (Motorola Advisor Linguist, Scriptor LX2 Linguist, NEC Maxima, Optima, Truly Supervisor) 82 | {186, 65}, // 2 - Latin/Cyrillic (Philips PRG2220, PRG2310) 83 | {251, 67} // 3 - Cyrillic (Motorola Advisor, Scriptor LX1) 84 | }; 85 | }; 86 | 87 | static const byte PCSG_ALPHABET[318][2] PROGMEM = { 88 | // Numeric 89 | {'0', 0x0}, {'1', 0x1}, {'2', 0x2}, {'3', 0x3}, {'4', 0x4}, {'5', 0x5}, {'6', 0x6}, {'7', 0x7}, {'8', 0x8}, {'9', 0x9}, {'*', 0xA}, {'U', 0xB}, {' ', 0xC}, {'-', 0xD}, {')', 0xE}, {'(', 0xF}, 90 | 91 | // Alphanumeric common 92 | {' ', 0x20}, {'!', 0x21}, {'"', 0x22}, {'#', 0x23}, {'$', 0x24}, {'%', 0x25}, {'&', 0x26}, {'\'', 0x27}, {'(', 0x28}, {')', 0x29}, {'*', 0x2A}, {'+', 0x2B}, {',', 0x2C}, {'-', 0x2D}, {'.', 0x2E}, {'/', 0x2F}, 93 | {'0', 0x30}, {'1', 0x31}, {'2', 0x32}, {'3', 0x33}, {'4', 0x34}, {'5', 0x35}, {'6', 0x36}, {'7', 0x37}, {'8', 0x38}, {'9', 0x39}, {':', 0x3A}, {';', 0x3B}, {'<', 0x3C}, {'=', 0x3D}, {'>', 0x3E}, {'?', 0x3F}, 94 | 95 | // 0 - Alphanumeric Latin (Motorola Advisor) 96 | {'@', 0x40}, {'A', 0x41}, {'B', 0x42}, {'C', 0x43}, {'D', 0x44}, {'E', 0x45}, {'F', 0x46}, {'G', 0x47}, {'H', 0x48}, {'I', 0x49}, {'J', 0x4A}, {'K', 0x4B}, {'L', 0x4C}, {'M', 0x4D}, {'N', 0x4E}, {'O', 0x4F}, 97 | {'P', 0x50}, {'Q', 0x51}, {'R', 0x52}, {'S', 0x53}, {'T', 0x54}, {'U', 0x55}, {'V', 0x56}, {'W', 0x57}, {'X', 0x58}, {'Y', 0x59}, {'Z', 0x5A}, {'[', 0x5B}, {'\\', 0x5C}, {']', 0x5D}, {'^', 0x5E}, {'_', 0x5F}, 98 | {'`', 0x60}, {'a', 0x61}, {'b', 0x62}, {'c', 0x63}, {'d', 0x64}, {'e', 0x65}, {'f', 0x66}, {'g', 0x67}, {'h', 0x68}, {'i', 0x69}, {'j', 0x6A}, {'k', 0x6B}, {'l', 0x6C}, {'m', 0x6D}, {'n', 0x6E}, {'o', 0x6F}, 99 | {'p', 0x70}, {'q', 0x71}, {'r', 0x72}, {'s', 0x73}, {'t', 0x74}, {'u', 0x75}, {'v', 0x76}, {'w', 0x77}, {'x', 0x78}, {'y', 0x79}, {'z', 0x7A}, {'{', 0x7B}, {'|', 0x7C}, {'}', 0x7D}, {'~', 0x7E}, 100 | 101 | // 1 - Alphanumeric Latin/Cyrillic (Motorola Advisor Linguist, Scriptor LX2 Linguist, NEC Maxima, Optima, Truly Supervisor) 102 | {'@', 0x40}, {'A', 0x41}, {192, 0x41}, {'B', 0x42}, {194, 0x42}, {'C', 0x43}, {209, 0x43}, {'D', 0x44}, {'E', 0x45}, {197, 0x45}, {'F', 0x46}, {'G', 0x47}, {'H', 0x48}, {205, 0x48}, {'I', 0x49}, {'J', 0x4A}, {'K', 0x4B}, {202, 0x4B}, {'L', 0x4C}, {'M', 0x4D}, {204, 0x4D}, {'N', 0x4E}, {'O', 0x4F}, {206, 0x4F}, 103 | {'P', 0x50}, {208, 0x50}, {'Q', 0x51}, {'R', 0x52}, {'S', 0x53}, {'T', 0x54}, {210, 0x54}, {'U', 0x55}, {'V', 0x56}, {'W', 0x57}, {'X', 0x58}, {213, 0x58}, {'Y', 0x59}, {'Z', 0x5A}, {'[', 0x5B}, {'\\', 0x5C}, {']', 0x5D}, {'^', 0x5E}, {'_', 0x5F}, 104 | {'`', 0x60}, {193, 0x61}, {195, 0x62}, {129, 0x63}, {131, 0x63}, {196, 0x64}, {168, 0x65}, {198, 0x66}, {199, 0x67}, {200, 0x68}, {201, 0x69}, {203, 0x6A}, {207, 0x6B}, {211, 0x6C}, {212, 0x6D}, {214, 0x6E}, {215, 0x6F}, 105 | {216, 0x70}, {217, 0x71}, {218, 0x72}, {219, 0x73}, {220, 0x74}, {221, 0x75}, {222, 0x76}, {223, 0x77}, {142, 0x78}, {170, 0x79}, {175, 0x7A}, {'{', 0x7B}, {'|', 0x7C}, {'}', 0x7D}, {'~', 0x7E}, 106 | 107 | // 2 - Alphanumeric Latin/Cyrillic (Philips PRG2220, PRG2310) 108 | {'@', 0x40}, {'A', 0x41}, {'B', 0x42}, {'C', 0x43}, {'D', 0x44}, {'E', 0x45}, {'F', 0x46}, {'G', 0x47}, {'H', 0x48}, {'I', 0x49}, {'J', 0x4A}, {'K', 0x4B}, {'L', 0x4C}, {'M', 0x4D}, {'N', 0x4E}, {'O', 0x4F}, 109 | {'P', 0x50}, {'Q', 0x51}, {'R', 0x52}, {'S', 0x53}, {'T', 0x54}, {'U', 0x55}, {'V', 0x56}, {'W', 0x57}, {'X', 0x58}, {'Y', 0x59}, {'Z', 0x5A}, {'[', 0x5B}, {'\\', 0x5C}, {']', 0x5D}, {'^', 0x5E}, {'_', 0x5F}, 110 | {192, 0x60}, {193, 0x61}, {194, 0x62}, {195, 0x63}, {196, 0x64}, {197, 0x65}, {168, 0x65}, {198, 0x66}, {199, 0x67}, {200, 0x68}, {201, 0x69}, {202, 0x6A}, {203, 0x6B}, {204, 0x6C}, {205, 0x6D}, {206, 0x6E}, {207, 0x6F}, 111 | {208, 0x70}, {209, 0x71}, {210, 0x72}, {211, 0x73}, {212, 0x74}, {213, 0x75}, {214, 0x76}, {215, 0x77}, {216, 0x78}, {217, 0x79}, {218, 0x7A}, {219, 0x7B}, {220, 0x7C}, {221, 0x7D}, {222, 0x7E}, {223, 0x7F}, 112 | 113 | // 3 - Alphanumeric Cyrillic (Motorola Advisor, Scriptor LX1) 114 | {254, 0x40}, {224, 0x41}, {225, 0x42}, {246, 0x43}, {228, 0x44}, {229, 0x45}, {184, 0x45}, {244, 0x46}, {227, 0x47}, {245, 0x48}, {232, 0x49}, {233, 0x4A}, {234, 0x4B}, {235, 0x4C}, {236, 0x4D}, {237, 0x4E}, {238, 0x4F}, 115 | {239, 0x50}, {255, 0x51}, {240, 0x52}, {241, 0x53}, {242, 0x54}, {243, 0x55}, {230, 0x56}, {226, 0x57}, {250, 0x58}, {252, 0x58}, {251, 0x59}, {231, 0x5A}, {248, 0x5B}, {253, 0x5C}, {249, 0x5D}, {247, 0x5E}, {'_', 0x5F}, 116 | {222, 0x60}, {192, 0x61}, {193, 0x62}, {214, 0x63}, {196, 0x64}, {197, 0x65}, {168, 0x65}, {212, 0x66}, {195, 0x67}, {213, 0x68}, {200, 0x69}, {201, 0x6A}, {202, 0x6B}, {203, 0x6C}, {204, 0x6D}, {205, 0x6E}, {206, 0x6F}, 117 | {207, 0x70}, {223, 0x71}, {208, 0x72}, {209, 0x73}, {210, 0x74}, {211, 0x75}, {198, 0x76}, {194, 0x77}, {218, 0x78}, {220, 0x78}, {219, 0x79}, {199, 0x7A}, {216, 0x7B}, {221, 0x7C}, {217, 0x7D}, {215, 0x7E} 118 | }; 119 | 120 | #endif /* _POCSAGENCODER_H_ */ 121 | -------------------------------------------------------------------------------- /src/Rf7021.cpp: -------------------------------------------------------------------------------- 1 | #include "rf7021.h" 2 | 3 | Rf7021::Rf7021() { 4 | // Default settings 5 | rfConfig.xtalFrequency = 19680000UL; // Default 19.68 MHz TCXO 6 | rfConfig.hasExternalInductor = 0; // RF7021SE has no external inductor on L1-L2 pins by default 7 | rfConfig.xtalBias = 3; // 0 - 20uA, 1 - 25uA, 2 - 30uA, 3 - 35uA 8 | rfConfig.cpCurrent = 3; // 0 - 0.3mA, 1 - 0.9mA, 2 - 1.5mA, 3 - 2.1mA 9 | rfConfig.modulationScheme = 0; // 0 - FSK, 1 - GFSK, 2 - 3FSK, 3 - 4FSK, 4 - OFSK, 5 - RCFSK, 6 - RC3FSK, 7 - RC4FSK 10 | rfConfig.powerAmplifierEnabled = 1; // 0 - OFF, 1 - ON 11 | rfConfig.powerAmplifierRamping = 1; // 0 - OFF, 1 - LOWEST, ..., 7 - HIGHEST 12 | rfConfig.powerAmplifierBias = 3; // 0 - 5uA, 1 - 7uA, 2 - 9uA, 3 - 11 uA 13 | rfConfig.powerAmplifierPower = 63; // 0 - OFF, 1 - LOWEST, ..., 63 - HIGHEST 14 | rfConfig.dataInvertType = 2; // 0 - NONE, 1 - Invert CLK, 2 - Invert DATA, 3 - Invert CLK and DATA 15 | rfConfig.dataInvertEnabled = 0; // 0 - OFF, 1 - ON 16 | rfConfig.rCosineAlpha = 1; // 0 - 0.5, 1 - 0.7 17 | } 18 | 19 | void Rf7021::setTxRxDataPin(byte txRxDataPin) { 20 | rfConfig.txRxDataPin = txRxDataPin; 21 | pinMode(txRxDataPin, OUTPUT); 22 | } 23 | 24 | void Rf7021::setTxRxCLKPin(byte txRxCLKPin) { 25 | rfConfig.txRxCLKPin = txRxCLKPin; 26 | pinMode(txRxCLKPin, INPUT); 27 | } 28 | 29 | void Rf7021::setCEPin(byte cePin) { 30 | rfConfig.cePin = cePin; 31 | pinMode(cePin, OUTPUT); 32 | } 33 | 34 | void Rf7021::setSREADPin(byte sreadPin) { 35 | rfConfig.sreadPin = sreadPin; 36 | pinMode(sreadPin, INPUT); 37 | } 38 | 39 | void Rf7021::setSLEPin(byte slePin) { 40 | rfConfig.slePin = slePin; 41 | pinMode(slePin, OUTPUT); 42 | } 43 | 44 | void Rf7021::setSDATAPin(byte sdataPin) { 45 | rfConfig.sdataPin = sdataPin; 46 | pinMode(sdataPin, OUTPUT); 47 | } 48 | 49 | void Rf7021::setSCLKPin(byte sclkPin) { 50 | rfConfig.sclkPin = sclkPin; 51 | pinMode(sclkPin, OUTPUT); 52 | } 53 | 54 | void Rf7021::setXtalFrequency(uint32_t xtalFrequency) { 55 | rfConfig.xtalFrequency = xtalFrequency; 56 | } 57 | 58 | void Rf7021::setXtalBias(byte xtalBias) { 59 | rfConfig.xtalBias = xtalBias; 60 | } 61 | 62 | void Rf7021::setCpCurrent(byte cpCurrent) { 63 | rfConfig.cpCurrent = cpCurrent; 64 | } 65 | 66 | void Rf7021::setHasExternalInductor(byte hasExternalInductor) { 67 | rfConfig.hasExternalInductor = hasExternalInductor; 68 | } 69 | 70 | void Rf7021::setmModulationScheme(byte modulationScheme) { 71 | rfConfig.modulationScheme = modulationScheme; 72 | } 73 | 74 | void Rf7021::setPowerAmplifierEnabled(byte powerAmplifierEnabled) { 75 | rfConfig.powerAmplifierEnabled = powerAmplifierEnabled; 76 | } 77 | 78 | void Rf7021::setPowerAmplifierRamping(byte powerAmplifierRamping) { 79 | rfConfig.powerAmplifierRamping = powerAmplifierRamping; 80 | } 81 | 82 | void Rf7021::setPowerAmplifierBias(byte powerAmplifierBias) { 83 | rfConfig.powerAmplifierBias = powerAmplifierBias; 84 | } 85 | 86 | void Rf7021::setPowerAmplifierPower(byte powerAmplifierPower) { 87 | rfConfig.powerAmplifierPower = powerAmplifierPower; 88 | } 89 | 90 | void Rf7021::setDataInvertType(byte dataInvertType) { 91 | rfConfig.dataInvertType = dataInvertType; 92 | } 93 | 94 | void Rf7021::setRCosineAlpha(byte rCosineAlpha) { 95 | rfConfig.rCosineAlpha = rCosineAlpha; 96 | } 97 | 98 | void Rf7021::setDataRate(word dataRate) { 99 | rfConfig.dataRate = dataRate; 100 | } 101 | 102 | void Rf7021::setFrequency(uint32_t frequency) { 103 | rfConfig.frequency = frequency; 104 | } 105 | 106 | void Rf7021::setFrequencyDeviation(word frequencyDeviation) { 107 | rfConfig.frequencyDeviation = frequencyDeviation; 108 | } 109 | 110 | void Rf7021::setDataInvertEnabled(byte dataInvertEnabled) { 111 | rfConfig.dataInvertEnabled = dataInvertEnabled; 112 | } 113 | 114 | void Rf7021::writeReg(RfReg *reg) { 115 | digitalWrite(rfConfig.slePin, LOW); 116 | digitalWrite(rfConfig.sclkPin, LOW); 117 | 118 | for (byte i = 4; i > 0; i--) { 119 | byte buf = reg->dataBytes[i - 1]; 120 | for (byte j = 8; j > 0; j--) { 121 | digitalWrite(rfConfig.sclkPin, LOW); 122 | digitalWrite(rfConfig.sdataPin, (buf & 0x80) ? HIGH : LOW); 123 | digitalWrite(rfConfig.sclkPin, HIGH); 124 | buf <<= 1; 125 | } 126 | digitalWrite(rfConfig.sclkPin, LOW); 127 | } 128 | 129 | digitalWrite(rfConfig.slePin, HIGH); 130 | delay(1); 131 | digitalWrite(rfConfig.sdataPin, LOW); 132 | digitalWrite(rfConfig.slePin, LOW); 133 | } 134 | 135 | Rf7021::RfReg Rf7021::readReg(word readbackConfig) { 136 | RfReg reg; 137 | reg.data = ((readbackConfig & 0x1F) << 4); 138 | 139 | reg.data |= 7; 140 | writeReg(®); 141 | reg.data = 0; 142 | 143 | digitalWrite(rfConfig.sdataPin, LOW); 144 | digitalWrite(rfConfig.sclkPin, LOW); 145 | digitalWrite(rfConfig.slePin, HIGH); 146 | 147 | // Skip first DB16 byte 148 | digitalWrite(rfConfig.sclkPin, HIGH); 149 | digitalWrite(rfConfig.sclkPin, LOW); 150 | 151 | byte buf = 0; 152 | for (byte i = 2; i > 0; i--) { 153 | for (byte j = 8; j > 0; j--) { 154 | digitalWrite(rfConfig.sclkPin, HIGH); 155 | buf <<= 1; 156 | if (digitalRead(rfConfig.sreadPin)) { 157 | buf |= 1; 158 | } 159 | digitalWrite(rfConfig.sclkPin, LOW); 160 | } 161 | reg.dataBytes[i - 1] = buf; 162 | } 163 | 164 | digitalWrite(rfConfig.sclkPin, HIGH); 165 | digitalWrite(rfConfig.slePin, LOW); 166 | digitalWrite(rfConfig.sclkPin, LOW); 167 | 168 | return reg; 169 | } 170 | 171 | word Rf7021::getSiliconRev() { 172 | powerOn(); 173 | word value = readReg(0x1C).dataLower; 174 | powerOff(); 175 | return value; 176 | } 177 | 178 | float Rf7021::getTemp() { 179 | powerOn(); 180 | RfReg reg; 181 | reg.data = 8; 182 | reg.data |= 1 << 8; 183 | writeReg(®); 184 | reg = readReg(0x16); 185 | powerOff(); 186 | return 469.5 - (7.2 * (reg.dataBytes[0] & 0x7F)); 187 | } 188 | 189 | float Rf7021::getVoltage() { 190 | powerOn(); 191 | RfReg reg; 192 | reg.data = 8; 193 | reg.data |= 1 << 8; 194 | writeReg(®); 195 | reg = readReg(0x15); 196 | powerOff(); 197 | return (reg.dataBytes[0] & 0x7F) / 21.1; 198 | } 199 | 200 | void Rf7021::powerOn() { 201 | digitalWrite(rfConfig.cePin, HIGH); 202 | delay(1); 203 | 204 | // Prepare and push Register 1 205 | rfConfig.R1.addressBits = 1; 206 | rfConfig.R1.rCounter = 1; 207 | rfConfig.R1.clockoutDivide = 0; 208 | rfConfig.R1.xtalDoubler = 0; 209 | rfConfig.R1.xoscEnable = 1; 210 | rfConfig.R1.xtalBias = rfConfig.xtalBias; 211 | rfConfig.R1.cpCurrent = rfConfig.cpCurrent; 212 | rfConfig.R1.vcoEnable = 1; 213 | if (rfConfig.frequency >= 900000000UL && rfConfig.frequency <= 950000000UL) { 214 | // 900 - 950 MHz internal VCO 215 | rfConfig.R1.rfDivideBy2 = 0; 216 | rfConfig.R1.vcoBias = 8; 217 | rfConfig.R1.vcoAdjust = 3; 218 | rfConfig.R1.vcoInductor = 0; 219 | } else if (rfConfig.frequency >= 800000000UL && rfConfig.frequency < 900000000UL) { 220 | // 862 - 900 MHz internal VCO 221 | rfConfig.R1.rfDivideBy2 = 0; 222 | rfConfig.R1.vcoBias = 8; 223 | rfConfig.R1.vcoAdjust = 0; 224 | rfConfig.R1.vcoInductor = 0; 225 | } else if (rfConfig.frequency >= 450000000UL && rfConfig.frequency < 500000000UL) { 226 | // 450 - 500 MHz internal VCO 227 | rfConfig.R1.rfDivideBy2 = 1; 228 | rfConfig.R1.vcoBias = 8; 229 | rfConfig.R1.vcoAdjust = 3; 230 | rfConfig.R1.vcoInductor = 0; 231 | } else if (rfConfig.frequency >= 400000000UL && rfConfig.frequency < 450000000UL) { 232 | // 400 - 450 MHz internal VCO 233 | rfConfig.R1.rfDivideBy2 = 1; 234 | rfConfig.R1.vcoBias = 8; 235 | rfConfig.R1.vcoAdjust = 0; 236 | rfConfig.R1.vcoInductor = 0; 237 | } else if (rfConfig.hasExternalInductor && rfConfig.frequency >= 500000000UL && rfConfig.frequency <= 650000000UL) { 238 | // 500 - 650 MHz external VCO 239 | rfConfig.R1.rfDivideBy2 = 0; 240 | rfConfig.R1.vcoBias = 4; 241 | rfConfig.R1.vcoInductor = 1; 242 | } else if (rfConfig.hasExternalInductor && rfConfig.frequency >= 200000000UL && rfConfig.frequency < 400000000UL) { 243 | // 200 - 400 MHz external VCO 244 | rfConfig.R1.rfDivideBy2 = 0; 245 | rfConfig.R1.vcoBias = 3; 246 | rfConfig.R1.vcoInductor = 1; 247 | } else if (rfConfig.hasExternalInductor && rfConfig.frequency >= 80000000UL && rfConfig.frequency < 200000000UL) { 248 | // 80 - 200 MHz external VCO 249 | rfConfig.R1.rfDivideBy2 = 1; 250 | rfConfig.R1.vcoBias = 2; 251 | rfConfig.R1.vcoInductor = 1; 252 | } 253 | writeReg(&rfConfig.r1); 254 | 255 | // Prepare and push Register 15 (CLK_MUX = 0x0 to enable default mode) 256 | rfConfig.R15.addressBits = 15; 257 | rfConfig.R15.rxTestMode = 0; 258 | rfConfig.R15.txTestMode = 0; 259 | rfConfig.R15.sdTestMode = 0; 260 | rfConfig.R15.cpTestMode = 0; 261 | rfConfig.R15.clkMux = 0; 262 | rfConfig.R15.pllTestMode = 0; 263 | rfConfig.R15.analogTestMode = 0; 264 | rfConfig.R15.forceLdHigh = 0; 265 | rfConfig.R15.reg1Pd = 0; 266 | rfConfig.R15.calOverride = 0; 267 | writeReg(&rfConfig.r15); 268 | 269 | // Prepare and push Register 3 270 | rfConfig.R3.addressBits = 3; 271 | // Find best BBOS divider 272 | for (byte bbosDiv = 0; bbosDiv < 4; bbosDiv++) { 273 | uint32_t bbosClk = round(rfConfig.xtalFrequency / (2 << (bbosDiv + 1))); 274 | if (bbosClk >= 1000000UL && bbosClk <= 2000000UL) { 275 | rfConfig.R3.bbosClkDivide = bbosDiv; 276 | } 277 | } 278 | // Find best DEMOD and CDR dividers to achieve required data rate 279 | double rateDeltaMin = rfConfig.dataRate; 280 | for (byte dem = 1; dem < 15; dem++) { 281 | byte cdr = round(rfConfig.xtalFrequency / (dem * rfConfig.dataRate * 32.0)); 282 | if (cdr == 0) continue; 283 | word realRate = round(rfConfig.xtalFrequency / (dem * cdr * 32.0)); 284 | double delta = abs(realRate - rfConfig.dataRate); 285 | if (delta < rateDeltaMin) { 286 | rfConfig.R3.demClkDivide = dem; 287 | rfConfig.R3.cdrClkDivide = cdr; 288 | rateDeltaMin = delta; 289 | } 290 | } 291 | rfConfig.R3.seqClkDivide = round(rfConfig.xtalFrequency / 100000.0); 292 | rfConfig.R3.agcClkDivide = round((rfConfig.R3.seqClkDivide * rfConfig.xtalFrequency) / 10000.0); 293 | writeReg(&rfConfig.r3); 294 | 295 | // Prepare and push Register 0 296 | rfConfig.R0.addressBits = 0; 297 | double n = (rfConfig.frequency * rfConfig.R1.rCounter / (rfConfig.xtalFrequency * (rfConfig.R1.rfDivideBy2 ? 0.5 : 1.0))); 298 | uint32_t nInt = floor(n); 299 | uint32_t nFrac = round((n - floor(n)) * 32768); 300 | rfConfig.R0.fractionalN = nFrac; 301 | rfConfig.R0.integerN = nInt; 302 | rfConfig.R0.rxMode = 0; 303 | rfConfig.R0.uartMode = 0; 304 | rfConfig.R0.muxOut = 0; 305 | writeReg(&rfConfig.r0); 306 | 307 | // Prepare and push Register 2 308 | rfConfig.R2.addressBits = 2; 309 | rfConfig.R2.modulationScheme = rfConfig.modulationScheme; 310 | rfConfig.R2.paEnable = rfConfig.powerAmplifierEnabled; 311 | rfConfig.R2.paRamp = rfConfig.powerAmplifierRamping; 312 | rfConfig.R2.paBias = rfConfig.powerAmplifierBias; 313 | rfConfig.R2.powerAmplifier = rfConfig.powerAmplifierPower; 314 | rfConfig.R2.txFrequencyDeviation = round(rfConfig.frequencyDeviation * 65536.0 * (rfConfig.R1.rfDivideBy2 ? 2.0 : 1.0) / ((double) rfConfig.xtalFrequency / rfConfig.R1.rCounter)); 315 | rfConfig.R2.txDataInvert = rfConfig.dataInvertEnabled ? rfConfig.dataInvertType : 0; 316 | rfConfig.R2.rcosineAlpha = rfConfig.rCosineAlpha; 317 | writeReg(&rfConfig.r2); 318 | } 319 | 320 | void Rf7021::powerOff() { 321 | digitalWrite(rfConfig.cePin, LOW); 322 | delay(1); 323 | } 324 | 325 | void Rf7021::txTest(byte testMode, word duration) { 326 | powerOn(); 327 | 328 | rfConfig.R15.txTestMode = testMode; 329 | writeReg(&rfConfig.r15); 330 | 331 | delay(duration); 332 | 333 | powerOff(); 334 | } 335 | 336 | void Rf7021::sendMessage(byte *message, word messageLength) { 337 | powerOn(); 338 | 339 | byte bitCounter = 0; 340 | word byteCounter = 0; 341 | byte currentByte = message[0]; 342 | 343 | while (byteCounter < messageLength) { 344 | // Wait LOW on TxRxCLK 345 | while (digitalRead(rfConfig.txRxCLKPin)); 346 | 347 | // Push bit 348 | if (currentByte & 0x80) { 349 | digitalWrite(rfConfig.txRxDataPin, HIGH); 350 | } else { 351 | digitalWrite(rfConfig.txRxDataPin, LOW); 352 | } 353 | 354 | // Switch to next bit 355 | bitCounter++; 356 | if (bitCounter >= 8) { 357 | bitCounter = 0; 358 | byteCounter++; 359 | currentByte = message[byteCounter]; 360 | } else { 361 | currentByte <<= 1; 362 | } 363 | 364 | // Wait HIGH on TxRxCLK 365 | while (!digitalRead(rfConfig.txRxCLKPin)); 366 | } 367 | 368 | powerOff(); 369 | } 370 | -------------------------------------------------------------------------------- /src/Rf7021.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Simple library for using the RF7021SE (ADF7021) module to transfer binary data 3 | * 4 | * RF7021SE module pinout: 5 | * 6 | * | VCC | PAC (not used) | 7 | * | CE | SLE | 8 | * | SDATA | SREAD | 9 | * | NC (not used) | SCLK | 10 | * | TxRxData | SWD (not used) | 11 | * | MUX (not used) | TxRxCLK | 12 | * | GND | GND | 13 | * 14 | * Copyright (c) 2021, SinuX. All rights reserved. 15 | * 16 | * This library is distributed "as is" without any warranty. 17 | * See MIT License for more details. 18 | */ 19 | 20 | #ifndef _RF7021_H_ 21 | #define _RF7021_H_ 22 | 23 | #include "Arduino.h" 24 | 25 | class Rf7021 { 26 | 27 | public: 28 | Rf7021(); 29 | 30 | // Wiring 31 | void setTxRxDataPin(byte txRxDataPin); 32 | void setTxRxCLKPin(byte txRxCLKPin); 33 | void setCEPin(byte cePin); 34 | void setSREADPin(byte sreadPin); 35 | void setSLEPin(byte slePin); 36 | void setSDATAPin(byte sdataPin); 37 | void setSCLKPin(byte sclkPin); 38 | 39 | // Readback 40 | word getSiliconRev(); 41 | float getTemp(); 42 | float getVoltage(); 43 | 44 | // Setup 45 | void setXtalFrequency(uint32_t xtalFrequency); 46 | void setXtalBias(byte xtalBias); 47 | void setCpCurrent(byte cpCurrent); 48 | void setHasExternalInductor(byte hasExternalInductor); 49 | void setmModulationScheme(byte modulationScheme); 50 | void setPowerAmplifierEnabled(byte powerAmplifierEnabled); 51 | void setPowerAmplifierRamping(byte powerAmplifierRamping); 52 | void setPowerAmplifierBias(byte powerAmplifierBias); 53 | void setPowerAmplifierPower(byte powerAmplifierPower); 54 | void setDataInvertType(byte dataInvertType); 55 | void setRCosineAlpha(byte rCosineAlpha); 56 | void setDataRate(word dataRate); 57 | void setFrequency(uint32_t frequency); 58 | void setFrequencyDeviation(word frequencyDev); 59 | void setDataInvertEnabled(byte dataInvertEnabled); 60 | 61 | // Transmission 62 | void txTest(byte testMode, word duration); 63 | void sendMessage(byte *message, word messageLength); 64 | 65 | private: 66 | typedef union { 67 | uint32_t data; 68 | word dataLower; 69 | word dataUpper; 70 | byte dataBytes[4]; 71 | } RfReg; 72 | 73 | typedef struct { 74 | // Wiring 75 | byte txRxDataPin, txRxCLKPin; 76 | byte cePin, sreadPin, slePin, sdataPin, sclkPin; 77 | 78 | // Core settings 79 | uint32_t xtalFrequency; 80 | byte xtalBias; 81 | byte cpCurrent; 82 | byte hasExternalInductor; 83 | 84 | // Transmission settings 85 | byte modulationScheme; 86 | byte powerAmplifierEnabled; 87 | byte powerAmplifierRamping; 88 | byte powerAmplifierBias; 89 | byte powerAmplifierPower; 90 | byte dataInvertType; 91 | byte rCosineAlpha; 92 | word dataRate; 93 | uint32_t frequency; 94 | word frequencyDeviation; 95 | byte dataInvertEnabled; 96 | 97 | union { 98 | RfReg r0; 99 | struct { 100 | byte addressBits : 4; 101 | word fractionalN : 15; 102 | word integerN : 8; 103 | byte rxMode : 1; 104 | byte uartMode : 1; 105 | byte muxOut : 3; 106 | } R0; 107 | }; 108 | union { 109 | RfReg r1; 110 | struct { 111 | byte addressBits : 4; 112 | byte rCounter : 3; 113 | byte clockoutDivide : 4; 114 | byte xtalDoubler : 1; 115 | byte xoscEnable : 1; 116 | byte xtalBias : 2; 117 | byte cpCurrent : 2; 118 | byte vcoEnable : 1; 119 | byte rfDivideBy2 : 1; 120 | byte vcoBias : 4; 121 | byte vcoAdjust : 2; 122 | byte vcoInductor : 1; 123 | } R1; 124 | }; 125 | union { 126 | RfReg r2; 127 | struct { 128 | byte addressBits : 4; 129 | byte modulationScheme : 3; 130 | byte paEnable : 1; 131 | byte paRamp : 3; 132 | byte paBias : 2; 133 | byte powerAmplifier : 6; 134 | word txFrequencyDeviation : 9; 135 | byte txDataInvert : 2; 136 | byte rcosineAlpha : 1; 137 | } R2; 138 | }; 139 | union { 140 | RfReg r3; 141 | struct { 142 | byte addressBits : 4; 143 | byte bbosClkDivide: 2; 144 | byte demClkDivide : 4; 145 | byte cdrClkDivide : 8; 146 | byte seqClkDivide : 8; 147 | byte agcClkDivide : 6; 148 | } R3; 149 | }; 150 | union { 151 | RfReg r15; 152 | struct { 153 | byte addressBits : 4; 154 | byte rxTestMode : 4; 155 | byte txTestMode : 3; 156 | byte sdTestMode : 3; 157 | byte cpTestMode : 3; 158 | byte clkMux : 3; 159 | byte pllTestMode : 4; 160 | byte analogTestMode : 4; 161 | byte forceLdHigh : 1; 162 | byte reg1Pd : 1; 163 | byte calOverride : 2; 164 | } R15; 165 | }; 166 | } RfConfig; 167 | 168 | RfConfig rfConfig; 169 | 170 | void writeReg(RfReg *reg); 171 | RfReg readReg(word readbackConfig); 172 | 173 | void powerOn(); 174 | void powerOff(); 175 | }; 176 | 177 | 178 | #endif /* _RF7021_H_ */ 179 | --------------------------------------------------------------------------------