PlatformIO package of the Teensy core framework compatible with GCC 10 & C++20
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

465 line
16KB

  1. // RH_CC110.cpp
  2. //
  3. // Driver for Texas Instruments CC110L transceiver.
  4. //
  5. // Copyright (C) 2016 Mike McCauley
  6. // $Id: RH_CC110.cpp,v 1.4 2016/01/02 01:46:34 mikem Exp $
  7. #include <RH_CC110.h>
  8. // Interrupt vectors for the 3 Arduino interrupt pins
  9. // Each interrupt can be handled by a different instance of RH_CC110, allowing you to have
  10. // 2 or more LORAs per Arduino
  11. RH_CC110* RH_CC110::_deviceForInterrupt[RH_CC110_NUM_INTERRUPTS] = {0, 0, 0};
  12. uint8_t RH_CC110::_interruptCount = 0; // Index into _deviceForInterrupt for next device
  13. // We need 2 tables of modem configuration registers, since some values change depending on the Xtal frequency
  14. // These are indexed by the values of ModemConfigChoice
  15. // Canned modem configurations generated with the TI SmartRF Studio v7 version 2.3.0 on boodgie
  16. // based on the sample 'Typical settings'
  17. // Stored in flash (program) memory to save SRAM
  18. // For 26MHz crystals
  19. PROGMEM static const RH_CC110::ModemConfig MODEM_CONFIG_TABLE_26MHZ[] =
  20. {
  21. // 0B 0C 10 11 12 15 19 1A 1B 1C 1D 21 22 23 24 25 26 2C 2D 2E
  22. {0x06, 0x00, 0xf5, 0x83, 0x13, 0x15, 0x16, 0x6c, 0x03, 0x40, 0x91, 0x56, 0x10, 0xe9, 0x2a, 0x00, 0x1f, 0x81, 0x35, 0x09}, // GFSK_Rb1_2Fd5_2
  23. {0x06, 0x00, 0xf6, 0x83, 0x13, 0x15, 0x16, 0x6c, 0x03, 0x40, 0x91, 0x56, 0x10, 0xe9, 0x2a, 0x00, 0x1f, 0x81, 0x35, 0x09}, // GFSK_Rb2_4Fd5_2
  24. {0x06, 0x00, 0xc7, 0x83, 0x13, 0x40, 0x16, 0x6c, 0x43, 0x40, 0x91, 0x56, 0x10, 0xe9, 0x2a, 0x00, 0x1f, 0x81, 0x35, 0x09}, // GFSK_Rb4_8Fd25_4
  25. {0x06, 0x00, 0xc8, 0x93, 0x13, 0x34, 0x16, 0x6c, 0x43, 0x40, 0x91, 0x56, 0x10, 0xe9, 0x2a, 0x00, 0x1f, 0x81, 0x35, 0x09}, // GFSK_Rb10Fd19
  26. {0x06, 0x00, 0xca, 0x83, 0x13, 0x35, 0x16, 0x6c, 0x43, 0x40, 0x91, 0x56, 0x10, 0xe9, 0x2a, 0x00, 0x1f, 0x81, 0x35, 0x09}, // GFSK_Rb38_4Fd20
  27. {0x08, 0x00, 0x7b, 0x83, 0x13, 0x42, 0x1d, 0x1c, 0xc7, 0x00, 0xb2, 0xb6, 0x10, 0xea, 0x2a, 0x00, 0x1f, 0x81, 0x35, 0x09}, // GFSK_Rb76_8Fd32
  28. {0x08, 0x00, 0x5b, 0xf8, 0x13, 0x47, 0x1d, 0x1c, 0xc7, 0x00, 0xb2, 0xb6, 0x10, 0xea, 0x2a, 0x00, 0x1f, 0x81, 0x31, 0x09}, // GFSK_Rb100Fd47
  29. {0x0c, 0x00, 0x2d, 0x3b, 0x13, 0x62, 0x1d, 0x1c, 0xc7, 0x00, 0xb0, 0xb6, 0x10, 0xea, 0x2a, 0x00, 0x1f, 0x88, 0x31, 0x09}, // GFSK_Rb250Fd127
  30. };
  31. // For 27MHz crystals
  32. PROGMEM static const RH_CC110::ModemConfig MODEM_CONFIG_TABLE_27MHZ[] =
  33. {
  34. // 0B 0C 10 11 12 15 19 1A 1B 1C 1D 21 22 23 24 25 26 2C 2D 2E
  35. {0x06, 0x00, 0xf5, 0x75, 0x13, 0x14, 0x16, 0x6c, 0x03, 0x40, 0x91, 0x56, 0x10, 0xe9, 0x2a, 0x00, 0x1f, 0x81, 0x35, 0x09}, // GFSK_Rb1_2Fd5_2
  36. {0x06, 0x00, 0xf6, 0x75, 0x13, 0x14, 0x16, 0x6c, 0x03, 0x40, 0x91, 0x56, 0x10, 0xe9, 0x2a, 0x00, 0x1f, 0x81, 0x35, 0x09}, // GFSK_Rb2_4Fd5_2
  37. {0x06, 0x00, 0xc7, 0x75, 0x13, 0x37, 0x16, 0x6c, 0x43, 0x40, 0x91, 0x56, 0x10, 0xe9, 0x2a, 0x00, 0x1f, 0x81, 0x35, 0x09}, // GFSK_Rb4_8Fd25_4
  38. {0x06, 0x00, 0xc8, 0x84, 0x13, 0x33, 0x16, 0x6c, 0x43, 0x40, 0x91, 0x56, 0x10, 0xe9, 0x2a, 0x00, 0x1f, 0x81, 0x35, 0x09}, // GFSK_Rb10Fd19
  39. {0x06, 0x00, 0xca, 0x75, 0x13, 0x34, 0x16, 0x6c, 0x43, 0x40, 0x91, 0x56, 0x10, 0xe9, 0x2a, 0x00, 0x1f, 0x81, 0x35, 0x09}, // GFSK_Rb38_4Fd20
  40. {0x08, 0x00, 0x7b, 0x75, 0x13, 0x42, 0x1d, 0x1c, 0xc7, 0x00, 0xb2, 0xb6, 0x10, 0xea, 0x2a, 0x00, 0x1f, 0x81, 0x35, 0x09}, // GFSK_Rb76_8Fd32
  41. {0x08, 0x00, 0x5b, 0xf8, 0x13, 0x47, 0x1d, 0x1c, 0xc7, 0x00, 0xb2, 0xb6, 0x10, 0xea, 0x2a, 0x00, 0x1f, 0x81, 0x31, 0x09}, // GFSK_Rb100Fd47
  42. {0x0c, 0x00, 0x2d, 0x2f, 0x13, 0x62, 0x1d, 0x1c, 0xc7, 0x00, 0xb0, 0xb6, 0x10, 0xea, 0x2a, 0x00, 0x1f, 0x88, 0x31, 0x09}, // GFSK_Rb250Fd127
  43. };
  44. // These power outputs are based on the suggested optimum values for
  45. // multilayer inductors in the 915MHz frequency band. Per table 5-15.
  46. // Yes these are not linear.
  47. // Caution: this table is indexed by the values of enum TransmitPower
  48. // Do not change one without changing the other.
  49. // If you do not like these values, use setPaTable() directly.
  50. PROGMEM static const uint8_t paPowerValues[] =
  51. {
  52. 0x03, // -30dBm
  53. 0x0e, // -20dBm
  54. 0x1e, // -15dBm
  55. 0x27, // -10dBm
  56. 0x8e, // 0dBm
  57. 0xcd, // 5dBm
  58. 0xc7, // 7dBm
  59. 0xc0, // 10dBm
  60. };
  61. RH_CC110::RH_CC110(uint8_t slaveSelectPin, uint8_t interruptPin, bool is27MHz, RHGenericSPI& spi)
  62. :
  63. RHNRFSPIDriver(slaveSelectPin, spi),
  64. _rxBufValid(false),
  65. _is27MHz(is27MHz)
  66. {
  67. _interruptPin = interruptPin;
  68. _myInterruptIndex = 0xff; // Not allocated yet
  69. }
  70. bool RH_CC110::init()
  71. {
  72. if (!RHNRFSPIDriver::init())
  73. return false;
  74. // Determine the interrupt number that corresponds to the interruptPin
  75. int interruptNumber = digitalPinToInterrupt(_interruptPin);
  76. if (interruptNumber == NOT_AN_INTERRUPT)
  77. return false;
  78. #ifdef RH_ATTACHINTERRUPT_TAKES_PIN_NUMBER
  79. interruptNumber = _interruptPin;
  80. #endif
  81. // Reset the chip
  82. // Strobe the reset
  83. uint8_t val = spiCommand(RH_CC110_STROBE_30_SRES); // Reset
  84. delay(100);
  85. val = spiCommand(RH_CC110_STROBE_36_SIDLE); // IDLE
  86. if (val != 0x0f)
  87. return false; // No chip there or reset failed.
  88. // Add by Adrien van den Bossche <vandenbo@univ-tlse2.fr> for Teensy
  89. // ARM M4 requires the below. else pin interrupt doesn't work properly.
  90. // On all other platforms, its innocuous, belt and braces
  91. pinMode(_interruptPin, INPUT);
  92. // Set up interrupt handler
  93. // Since there are a limited number of interrupt glue functions isr*() available,
  94. // we can only support a limited number of devices simultaneously
  95. // ON some devices, notably most Arduinos, the interrupt pin passed in is actuallt the
  96. // interrupt number. You have to figure out the interruptnumber-to-interruptpin mapping
  97. // yourself based on knwledge of what Arduino board you are running on.
  98. if (_myInterruptIndex == 0xff)
  99. {
  100. // First run, no interrupt allocated yet
  101. if (_interruptCount <= RH_CC110_NUM_INTERRUPTS)
  102. _myInterruptIndex = _interruptCount++;
  103. else
  104. return false; // Too many devices, not enough interrupt vectors
  105. }
  106. _deviceForInterrupt[_myInterruptIndex] = this;
  107. if (_myInterruptIndex == 0)
  108. attachInterrupt(interruptNumber, isr0, RISING);
  109. else if (_myInterruptIndex == 1)
  110. attachInterrupt(interruptNumber, isr1, RISING);
  111. else if (_myInterruptIndex == 2)
  112. attachInterrupt(interruptNumber, isr2, RISING);
  113. else
  114. return false; // Too many devices, not enough interrupt vectors
  115. spiWriteRegister(RH_CC110_REG_02_IOCFG0, RH_CC110_GDO_CFG_CRC_OK_AUTORESET); // gdo0 interrupt on CRC_OK
  116. spiWriteRegister(RH_CC110_REG_06_PKTLEN, RH_CC110_MAX_PAYLOAD_LEN); // max packet length
  117. spiWriteRegister(RH_CC110_REG_07_PKTCTRL1, RH_CC110_CRC_AUTOFLUSH); // no append status, crc autoflush, no addr check
  118. spiWriteRegister(RH_CC110_REG_08_PKTCTRL0, RH_CC110_PKT_FORMAT_NORMAL | RH_CC110_CRC_EN | RH_CC110_LENGTH_CONFIG_VARIABLE);
  119. spiWriteRegister(RH_CC110_REG_13_MDMCFG1, RH_CC110_NUM_PREAMBLE_4); // 4 preamble bytes, chan spacing not used
  120. spiWriteRegister(RH_CC110_REG_17_MCSM1, RH_CC110_CCA_MODE_RSSI_PACKET | RH_CC110_RXOFF_MODE_RX | RH_CC110_TXOFF_MODE_IDLE);
  121. spiWriteRegister(RH_CC110_REG_18_MCSM0, RH_CC110_FS_AUTOCAL_FROM_IDLE | RH_CC110_PO_TIMEOUT_64); // cal when going to tx or rx
  122. spiWriteRegister(RH_CC110_REG_20_WORCTRL, 0xfb); // from smartrf
  123. spiWriteRegister(RH_CC110_REG_29_FSTEST, 0x59); // from smartrf
  124. spiWriteRegister(RH_CC110_REG_2A_PTEST, 0x7f); // from smartrf
  125. spiWriteRegister(RH_CC110_REG_2B_AGCTEST, 0x3f); // from smartrf
  126. // Set some reasonable default values
  127. uint8_t syncWords[] = { 0xd3, 0x91 };
  128. setSyncWords(syncWords, sizeof(syncWords));
  129. setTxPower(TransmitPower5dBm);
  130. setFrequency(915.0);
  131. setModemConfig(GFSK_Rb1_2Fd5_2);
  132. return true;
  133. }
  134. void RH_CC110::setIs27MHz(bool is27MHz)
  135. {
  136. _is27MHz = is27MHz;
  137. }
  138. // C++ level interrupt handler for this instance
  139. // We use this to get RxDone and TxDone interrupts
  140. void RH_CC110::handleInterrupt()
  141. {
  142. // Serial.println("I");
  143. if (_mode == RHModeRx)
  144. {
  145. // Radio is confgigured to stay in RX until we move it to IDLE after a CRC_OK message for us
  146. // We only get interrupts in RX mode, on CRC_OK
  147. // CRC OK
  148. _lastRssi = spiBurstReadRegister(RH_CC110_REG_34_RSSI); // Was set when sync word was detected
  149. _bufLen = spiReadRegister(RH_CC110_REG_3F_FIFO);
  150. if (_bufLen < 4)
  151. {
  152. // Something wrong there, flush the FIFO
  153. spiCommand(RH_CC110_STROBE_3A_SFRX);
  154. clearRxBuf();
  155. return;
  156. }
  157. spiBurstRead(RH_CC110_REG_3F_FIFO | RH_CC110_SPI_BURST_MASK | RH_CC110_SPI_READ_MASK, _buf, _bufLen);
  158. // All good so far. See if its for us
  159. validateRxBuf();
  160. if (_rxBufValid)
  161. setModeIdle(); // Done
  162. }
  163. }
  164. // These are low level functions that call the interrupt handler for the correct
  165. // instance of RH_CC110.
  166. // 3 interrupts allows us to have 3 different devices
  167. void RH_CC110::isr0()
  168. {
  169. if (_deviceForInterrupt[0])
  170. _deviceForInterrupt[0]->handleInterrupt();
  171. }
  172. void RH_CC110::isr1()
  173. {
  174. if (_deviceForInterrupt[1])
  175. _deviceForInterrupt[1]->handleInterrupt();
  176. }
  177. void RH_CC110::isr2()
  178. {
  179. if (_deviceForInterrupt[2])
  180. _deviceForInterrupt[2]->handleInterrupt();
  181. }
  182. uint8_t RH_CC110::spiReadRegister(uint8_t reg)
  183. {
  184. return spiRead((reg & 0x3f) | RH_CC110_SPI_READ_MASK);
  185. }
  186. uint8_t RH_CC110::spiBurstReadRegister(uint8_t reg)
  187. {
  188. return spiRead((reg & 0x3f) | RH_CC110_SPI_READ_MASK | RH_CC110_SPI_BURST_MASK);
  189. }
  190. uint8_t RH_CC110::spiWriteRegister(uint8_t reg, uint8_t val)
  191. {
  192. return spiWrite((reg & 0x3f), val);
  193. }
  194. uint8_t RH_CC110::spiBurstWriteRegister(uint8_t reg, const uint8_t* src, uint8_t len)
  195. {
  196. return spiBurstWrite((reg & 0x3f) | RH_CC110_SPI_BURST_MASK, src, len);
  197. }
  198. bool RH_CC110::printRegisters()
  199. {
  200. #ifdef RH_HAVE_SERIAL
  201. uint8_t i;
  202. for (i = 0; i <= 0x2f; i++)
  203. {
  204. Serial.print(i, HEX);
  205. Serial.print(": ");
  206. Serial.println(spiReadRegister(i), HEX);
  207. }
  208. // Burst registers
  209. for (i = 0x30; i <= 0x3e; i++)
  210. {
  211. Serial.print(i, HEX);
  212. Serial.print(": ");
  213. Serial.println(spiBurstReadRegister(i), HEX);
  214. }
  215. #endif
  216. return true;
  217. }
  218. // Check whether the latest received message is complete and uncorrupted
  219. void RH_CC110::validateRxBuf()
  220. {
  221. if (_bufLen < 4)
  222. return; // Too short to be a real message
  223. // Extract the 4 headers
  224. _rxHeaderTo = _buf[0];
  225. _rxHeaderFrom = _buf[1];
  226. _rxHeaderId = _buf[2];
  227. _rxHeaderFlags = _buf[3];
  228. if (_promiscuous ||
  229. _rxHeaderTo == _thisAddress ||
  230. _rxHeaderTo == RH_BROADCAST_ADDRESS)
  231. {
  232. _rxGood++;
  233. _rxBufValid = true;
  234. }
  235. }
  236. bool RH_CC110::available()
  237. {
  238. if (_mode == RHModeTx)
  239. return false;
  240. if (_rxBufValid) // Will be set by the interrupt handler when a good message is received
  241. return true;
  242. setModeRx(); // Make sure we are receiving
  243. return false; // Nothing yet
  244. }
  245. void RH_CC110::clearRxBuf()
  246. {
  247. ATOMIC_BLOCK_START;
  248. _rxBufValid = false;
  249. _bufLen = 0;
  250. ATOMIC_BLOCK_END;
  251. }
  252. bool RH_CC110::recv(uint8_t* buf, uint8_t* len)
  253. {
  254. if (!available())
  255. return false;
  256. if (buf && len)
  257. {
  258. ATOMIC_BLOCK_START;
  259. // Skip the 4 headers that are at the beginning of the rxBuf
  260. if (*len > _bufLen - RH_CC110_HEADER_LEN)
  261. *len = _bufLen - RH_CC110_HEADER_LEN;
  262. memcpy(buf, _buf + RH_CC110_HEADER_LEN, *len);
  263. ATOMIC_BLOCK_END;
  264. }
  265. clearRxBuf(); // This message accepted and cleared
  266. return true;
  267. }
  268. bool RH_CC110::send(const uint8_t* data, uint8_t len)
  269. {
  270. if (len > RH_CC110_MAX_MESSAGE_LEN)
  271. return false;
  272. waitPacketSent(); // Make sure we dont interrupt an outgoing message
  273. setModeIdle();
  274. spiWriteRegister(RH_CC110_REG_3F_FIFO, len + RH_CC110_HEADER_LEN);
  275. spiWriteRegister(RH_CC110_REG_3F_FIFO,_txHeaderTo);
  276. spiWriteRegister(RH_CC110_REG_3F_FIFO,_txHeaderFrom);
  277. spiWriteRegister(RH_CC110_REG_3F_FIFO,_txHeaderId);
  278. spiWriteRegister(RH_CC110_REG_3F_FIFO,_txHeaderFlags);
  279. spiBurstWriteRegister(RH_CC110_REG_3F_FIFO, data, len);
  280. // Radio returns to Idle when TX is finished
  281. // need waitPacketSent() to detect change of _mode and TX completion
  282. setModeTx();
  283. return true;
  284. }
  285. uint8_t RH_CC110::maxMessageLength()
  286. {
  287. return RH_CC110_MAX_MESSAGE_LEN;
  288. }
  289. void RH_CC110::setModeIdle()
  290. {
  291. if (_mode != RHModeIdle)
  292. {
  293. spiCommand(RH_CC110_STROBE_36_SIDLE);
  294. _mode = RHModeIdle;
  295. }
  296. }
  297. bool RH_CC110::sleep()
  298. {
  299. if (_mode != RHModeSleep)
  300. {
  301. spiCommand(RH_CC110_STROBE_39_SPWD);
  302. _mode = RHModeSleep;
  303. }
  304. return true;
  305. }
  306. void RH_CC110::setModeRx()
  307. {
  308. if (_mode != RHModeRx)
  309. {
  310. // Radio is configuewd to stay in RX mode
  311. // only receipt of a CRC_OK wil cause us to return it to IDLE
  312. spiCommand(RH_CC110_STROBE_34_SRX);
  313. _mode = RHModeRx;
  314. }
  315. }
  316. void RH_CC110::setModeTx()
  317. {
  318. if (_mode != RHModeTx)
  319. {
  320. spiCommand(RH_CC110_STROBE_35_STX);
  321. _mode = RHModeTx;
  322. }
  323. }
  324. uint8_t RH_CC110::statusRead()
  325. {
  326. return spiCommand(RH_CC110_STROBE_3D_SNOP);
  327. }
  328. // Sigh, this chip has no TXDONE type interrupt, so we have to poll
  329. bool RH_CC110::waitPacketSent()
  330. {
  331. // If we are not currently in transmit mode, there is no packet to wait for
  332. if (_mode != RHModeTx)
  333. return false;
  334. // Caution: may transition through CALIBRATE
  335. while ((statusRead() & RH_CC110_STATUS_STATE) != RH_CC110_STATUS_IDLE)
  336. YIELD;
  337. _mode = RHModeIdle;
  338. return true;
  339. }
  340. bool RH_CC110::setTxPower(TransmitPower power)
  341. {
  342. if (power > sizeof(paPowerValues))
  343. return false;
  344. uint8_t patable[2];
  345. memcpy_P(&patable[0], (void*)&paPowerValues[power], sizeof(uint8_t));
  346. patable[1] = 0x00;
  347. setPaTable(patable, sizeof(patable));
  348. return true;
  349. }
  350. void RH_CC110::setPaTable(uint8_t* patable, uint8_t patablesize)
  351. {
  352. spiBurstWriteRegister(RH_CC110_REG_3E_PATABLE, patable, patablesize);
  353. }
  354. bool RH_CC110::setFrequency(float centre)
  355. {
  356. // From section 5.21: fcarrier = fxosc / 2^16 * FREQ
  357. uint32_t FREQ;
  358. float fxosc = _is27MHz ? 27.0 : 26.0;
  359. FREQ = (uint32_t)(centre * 65536 / fxosc);
  360. // Some trivial checks
  361. if (FREQ & 0xff000000)
  362. return false;
  363. spiWriteRegister(RH_CC110_REG_0D_FREQ2, (FREQ >> 16) & 0xff);
  364. spiWriteRegister(RH_CC110_REG_0E_FREQ1, (FREQ >> 8) & 0xff);
  365. spiWriteRegister(RH_CC110_REG_0F_FREQ0, FREQ & 0xff);
  366. // Radio is configured to calibrate automatically whenever it enters RX or TX mode
  367. // so no need to check for PLL lock here
  368. return true;
  369. }
  370. // Sets registers from a canned modem configuration structure
  371. void RH_CC110::setModemRegisters(const ModemConfig* config)
  372. {
  373. spiWriteRegister(RH_CC110_REG_0B_FSCTRL1, config->reg_0b);
  374. spiWriteRegister(RH_CC110_REG_0C_FSCTRL0, config->reg_0c);
  375. spiWriteRegister(RH_CC110_REG_10_MDMCFG4, config->reg_10);
  376. spiWriteRegister(RH_CC110_REG_11_MDMCFG3, config->reg_11);
  377. spiWriteRegister(RH_CC110_REG_12_MDMCFG2, config->reg_12);
  378. spiWriteRegister(RH_CC110_REG_15_DEVIATN, config->reg_15);
  379. spiWriteRegister(RH_CC110_REG_19_FOCCFG, config->reg_19);
  380. spiWriteRegister(RH_CC110_REG_1A_BSCFG, config->reg_1a);
  381. spiWriteRegister(RH_CC110_REG_1B_AGCCTRL2, config->reg_1b);
  382. spiWriteRegister(RH_CC110_REG_1C_AGCCTRL1, config->reg_1c);
  383. spiWriteRegister(RH_CC110_REG_1D_AGCCTRL0, config->reg_1d);
  384. spiWriteRegister(RH_CC110_REG_21_FREND1, config->reg_21);
  385. spiWriteRegister(RH_CC110_REG_22_FREND0, config->reg_22);
  386. spiWriteRegister(RH_CC110_REG_23_FSCAL3, config->reg_23);
  387. spiWriteRegister(RH_CC110_REG_24_FSCAL2, config->reg_24);
  388. spiWriteRegister(RH_CC110_REG_25_FSCAL1, config->reg_25);
  389. spiWriteRegister(RH_CC110_REG_26_FSCAL0, config->reg_26);
  390. spiWriteRegister(RH_CC110_REG_2C_TEST2, config->reg_2c);
  391. spiWriteRegister(RH_CC110_REG_2D_TEST1, config->reg_2d);
  392. spiWriteRegister(RH_CC110_REG_2E_TEST0, config->reg_2e);
  393. }
  394. // Set one of the canned Modem configs
  395. // Returns true if its a valid choice
  396. bool RH_CC110::setModemConfig(ModemConfigChoice index)
  397. {
  398. if (index > (signed int)(sizeof(MODEM_CONFIG_TABLE_27MHZ) / sizeof(ModemConfig)))
  399. return false;
  400. const RH_CC110::ModemConfig *p = _is27MHz ? MODEM_CONFIG_TABLE_27MHZ : MODEM_CONFIG_TABLE_26MHZ ;
  401. RH_CC110::ModemConfig cfg;
  402. memcpy_P(&cfg, p + index, sizeof(RH_CC110::ModemConfig));
  403. setModemRegisters(&cfg);
  404. return true;
  405. }
  406. void RH_CC110::setSyncWords(const uint8_t* syncWords, uint8_t len)
  407. {
  408. if (!syncWords || len != 2)
  409. return; // Only 2 byte sync words are supported
  410. spiWriteRegister(RH_CC110_REG_04_SYNC1, syncWords[0]);
  411. spiWriteRegister(RH_CC110_REG_05_SYNC0, syncWords[1]);
  412. }