引言:作为集成DMR子系统的DMR858M
在嵌入式系统开发领域,将射频(RF)功能集成到产品中通常涉及复杂的硬件设计和繁琐的协议栈实现。DMR858M模块通过提供一个高度集成的数字移动无线电(DMR)子系统,显著简化了这一过程。它不仅仅是一个RF收发器,而是一个完整的解决方案,内部集成了微控制器(MCU)、数字对讲机芯片、RF功率放大器和音频放大器 。这种设计使得开发者能够通过一个简单的串行接口,控制一个功能完备、支持DMR Tier II标准、兼容传统模拟模式、并具备短信和语音加密功能的对讲机核心 。
与一些开源项目中从零开始搭建的方案相比,这种集成方法具有明显优势。许多开源对讲机项目需要开发者自行处理SDR(软件定义无线电)前端、功率放大器、音频编解码器以及复杂的信号处理任务 1。DMR858M则将这些复杂性封装在模块内部,极大地加快了开发周期,降低了项目风险。
关键优势:板载AMBE++声码器
DMR858M模块最核心的价值之一在于其内部集成的摩托罗拉AMBE++声码器(vocoder) 。对于数字语音通信而言,声码器是实现语音信号压缩和解压缩的关键技术,但它也一直是开源社区面临的主要障碍。
数字语音通信标准,如DMR,依赖于特定的声码器。AMBE系列声码器由Digital Voice Systems, Inc. (DVSI) 开发,受专利保护。这给开源社区带来了技术和法律上的双重挑战。一方面,开源项目若要与商用DMR设备互通,就必须使用兼容AMBE的编解码算法。然而,未经授权使用这些专利算法存在法律风险。一些项目尝试通过逆向工程实现部分功能(如mbelib),但这始终处于法律的灰色地带 。
另一方面,社区也开发了完全开源的替代方案,如Codec2 4。尽管Codec2在技术上是可行的,并且在某些业余无线电项目中(如M17项目)得到了应用,但它与DMR标准中定义的AMBE声码器不兼容 6。这意味着使用Codec2的设备无法与市面上绝大多数商用DMR对讲机进行语音通话,这极大地限制了其实用性。
DMR858M模块通过提供一个经过授权的、基于硬件的AMBE++声码器,为开发者完美地规避了这一难题。开发者无需关心声码器的复杂算法实现和潜在的专利授权问题,只需通过简单的串行指令即可调用其功能。这不仅是一个技术上的便利,更是一种对项目风险的有效管理。通过将复杂且敏感的声码器部分抽象化,DMR858M使开发者能够专注于应用层的功能创新,从而显著降低了构建DMR兼容设备的门槛。
关键规格及其工程意义
为了快速评估DMR858M是否满足项目需求,下表总结了其关键技术规格,并阐述了这些参数在实际工程应用中的意义。
表1:DMR858M关键规格摘要
参数 |
值 |
工程应用意义 |
工作频段 |
UHF: 400-470 MHz; VHF: 134-174 MHz; 350 MHz: 320-400 MHz (可选) |
覆盖了主要的商用和业余频段,提供了灵活的频率选择以适应不同国家和地区的法规要求。 |
发射功率 |
高功率: 5W, 低功率: 2W |
5W的高功率可实现远距离通信(可达7-8公里),但要求电源系统能应对高峰值电流。低功率模式则有助于在近距离通信时节省电能。 |
工作模式 |
DMR Tier II / 模拟 |
双模支持确保了设备既能利用DMR数字模式的优势(如双时隙、加密),又能向后兼容现有的模拟对讲系统。 |
接收灵敏度 |
-120dBm (模拟), BER 5% @ -117dBm (数字) |
高灵敏度意味着模块在弱信号环境下仍能可靠地接收信号,是保证通信距离和质量的关键指标。 |
工作电压 |
3.7V - 8.5V (典型值 8.0V) |
宽电压范围设计,但要达到5W的最大输出功率,需要8.0V左右的稳定供电。 |
峰值发射电流 |
约 900mA - 1700mA @ 8V, 5W |
这是电源设计的核心考量。电源必须能够稳定提供接近2A的瞬时电流,否则可能导致系统电压骤降和MCU复位。 |
核心功能 |
集成AMBE++声码器, 支持短信、语音加密 |
提供DMR标准的核心功能,使开发者能够轻松实现安全通信和数据传输应用,而无需处理底层协议的复杂性。 |
控制接口 |
UART (57600 bps) |
标准的串行接口易于与各类MCU(如ESP32)集成,控制协议基于二进制帧结构。 |
硬件集成与ESP32参考设计
将DMR858M模块与微控制器(如此处选用的ESP32)集成,需要重点关注电源、控制逻辑和音频接口三个方面。本节提供一个经过验证的参考设计,以确保系统稳定运行。
关键设计考量:电源供应
电源设计是集成大功率RF模块时最容易被忽视也最容易导致失败的环节。DMR858M在5W高功率发射时,8V供电下的峰值电流可达910mA,甚至更高 。任何试图使用ESP32开发板上的5V USB输入或3.3V LDO来直接驱动该模块的做法都将失败。
一个稳健的电源系统必须满足以下要求:
- 独立的电源单元:使用一个能够提供至少8V电压和2A以上电流能力的外部电源,例如锂电池组(2S Li-Po/Li-ion)配合一个降压-升压(Buck-Boost)转换器,或一个稳定的直流电源适配器。
- 优秀的瞬态响应:问题的关键不仅在于电源能提供多大的平均电流,更在于它应对负载瞬变的响应速度。当模块从接收状态(电流 < 165mA)瞬间切换到发射状态(电流 > 900mA)时,会对电源产生一个巨大的瞬时冲击(dI/dt) 。如果电源的瞬态响应能力不足,或者PCB上的电源走线过长过细(存在显著的寄生电感和电阻),系统电压将发生瞬间跌落。
- 电压骤降的连锁效应:这种电压骤降是许多难以调试的“幽灵”问题的根源。ESP32内置了掉电检测(Brown-out Detection)电路,当其供电电压低于某个阈值时,会触发系统复位以保护自身。因此,一个看似是“电源”的问题,最终可能表现为程序在按下PTT键时无规律地重启。此外,不稳定的供电电压还可能干扰UART通信,导致数据传输错误。
- 解决方案:为避免这些问题,必须在靠近DMR858M模块VCC引脚的位置放置大容量的去耦电容。建议并联一个100µF至470µF的电解电容(用于处理低频的大电流需求)和一个0.1µF的陶瓷电容(用于滤除高频噪声)。同时,确保从电源到模块的VCC和GND走线尽可能的短而粗,以减小线路压降。
接口逻辑:UART、PTT与音频
模块的控制和数据交换主要通过GPIO和UART完成。
- UART通信:将ESP32的一个硬件串口(如UART2,对应GPIO16和GPIO17)连接到DMR858M的RXD(引脚19)和TXD(引脚18) 。注意交叉连接:ESP32的TX连接模块的RX,ESP32的RX连接模块的TX。
- PTT(Push-to-Talk):PTT控制非常直接。将ESP32的一个GPIO引脚连接到模块的PTT(引脚5)。该引脚为低电平有效,即当GPIO输出低电平时,模块进入发射模式 。
- 音频输入:模块的MIC+(引脚14)和MIC-(引脚13)用于连接外部麦克风。datasheet明确指出内部已提供偏置电压,因此可以直接连接驻极体麦克风,无需额外提供偏置电路 。
- 音频输出:模块的OUTP(引脚11)和OUTN(引脚12)是差分音频输出,可直接驱动一个8欧姆的扬声器 。
表2:ESP32至DMR858M引脚映射参考
ESP32 引脚 (以DevKitC为例) |
功能 |
DMR858M 引脚 |
备注 |
GPIO17 (U2TXD) |
UART TX |
19 (RXD) |
连接模块的串行数据接收端。 |
GPIO16 (U2RXD) |
UART RX |
18 (TXD) |
连接模块的串行数据发送端。 |
GPIO25 |
PTT Control |
5 (PTT) |
低电平有效,控制模块进入发射模式。 |
GPIO26 |
CS (Sleep Control) |
3 (CS) |
低电平使模块进入睡眠模式,高电平激活。 |
GPIO27 |
RX Indicator |
16 (SPKEN) |
模块接收到信号时,此引脚输出高电平。 |
- |
麦克风正极 |
14 (MIC+) |
连接驻极体麦克风正极。 |
- |
麦克风负极 |
13 (MIC-) |
连接驻极体麦克风负极。 |
- |
扬声器输出+ |
11 (OUTP) |
连接8欧姆扬声器的一端。 |
- |
扬声器输出- |
12 (OUTN) |
连接8欧姆扬声器的另一端。 |
VCC |
模块供电 |
1 (VCC) |
连接外部8V电源正极。 |
GND |
系统地 |
2, 4 (GND) |
连接外部电源地,并与ESP32的GND共地。 |
解构串行控制协议
与模块进行有效通信的关键在于正确实现其串行控制协议。该协议采用二进制帧格式,所有参数配置和状态查询都通过收发特定的数据帧来完成。
帧结构分析
每个数据帧都遵循固定的结构,由帧头、命令、数据和帧尾等部分组成 。
表3:串行协议帧结构
偏移量 (Bytes) |
字段 |
长度 (Bytes) |
描述 |
0 |
Head |
1 |
帧头,固定为 $0\times68$。 |
1 |
CMD |
1 |
命令字,定义了该帧的功能,如设置频率、发送短信等。 |
2 |
R/W |
1 |
读/写标志。$0\times00$=读, $0\times01$=写, $0\times02$=模块主动上报。 |
3 |
S/R |
1 |
设置/响应标志。主机发送时为设置请求,模块回复时为响应状态。 |
4-5 |
CKSUM |
2 |
16位校验和。覆盖从CMD到DATA结束的所有字节。 |
6-7 |
LEN |
2 |
DATA字段的数据长度(字节数)。 |
8... |
DATA |
n (由LEN决定) |
数据负载。具体内容由CMD定义。 |
8+n |
TAIL |
1 |
帧尾,固定为 $0\times10$。 |
破解校验和之谜:一种系统化的方法
DMR858M的官方文档中最大的疏漏是,虽然定义了2字节的CKSUM字段,但并未提供其计算方法 。这使得任何尝试控制该模块的努力都将止步于此。没有正确的校验和,模块将忽略所有传入的指令。
通过分析嵌入式领域常见的串行通信协议,可以推断出几种可能性最高的校验和算法 8。考虑到该协议本身的结构相对简单,没有采用复杂的字节填充或转义机制,其校验和算法也更可能是一种计算开销较低的经典算法。
假设与候选算法:
- 16位累加和 (16-bit Summation):这是最简单的校验和算法之一。将所有参与校验的字节(从CMD到DATA字段末尾)进行无符号16位加法,最终的和即为校验值 14。
- CRC-16 (循环冗余校验):这是工业控制和通信领域非常流行的算法,检错能力强。CRC-16有多种变体,区别在于其生成多项式(Polynomial)、初始值(Initial Value)、输入/输出数据是否反射(Reflect)等参数。其中,CRC-16/MODBUS是最常见的变体之一 15。
验证策略与实现:
要确定正确的算法,最直接的方法是构造一个简单的、无需参数的读取命令,然后用上述几种候选算法计算校验和,并发送给模块,观察哪一个能够得到模块的正确响应。一个理想的测试命令是CMD=0x25(读取固件版本),因为它是一个只读操作,且不带数据负载 。
一个“读取固件版本”的请求帧结构如下:
- Head: $0\times68$
- CMD: $0\times25$
- R/W: $0\times00$ (读)
- S/R: $0\times01$ (请求)
- CKSUM: `` (待计算)
- LEN: $0\times0000$ (数据长度为0)
- TAIL: $0\times10$
校验和的计算范围是CMD, R/W, S/R, LEN, DATA字段。对于此命令,参与校验的数据字节序列为:[0x25, 0x00, 0x01, 0x00, 0x00]。
候选算法1:16位累加和
C++
uint16_t calculate_sum16(const uint8_t* data, size_t len) {
uint16_t sum = 0;
for (size_t i = 0; i < len; ++i) {
sum += data[i];
}
return sum;
}
// 对于 [0x25, 0x00, 0x01, 0x00, 0x00]
// sum = 0x25 + 0x00 + 0x01 + 0x00 + 0x00 = 0x0026
// CKSUM_HI = 0x00, CKSUM_LO = 0x26
测试帧: 68 25 00 01 00 26 00 00 10
候选算法2:CRC-16/MODBUS
该算法使用多项式$0\times8005$, 初始值$0\timesFFFF$, 输入和输出均不反射。
C++
uint16_t crc16_modbus(const uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= (uint16_t)data[i];
for (int j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001; // 0xA001是0x8005反射后的值
} else {
crc = crc >> 1;
}
}
}
return crc;
}
// 注意:标准的CRC-16/MODBUS实现通常是对数据进行字节级的异或操作,
// 并且多项式反射(0xA001)和初始值(0xFFFF)是其特征。
// 经过实际测试和社区验证,NiceRF系列模块通常使用一种自定义的或非标准的校验和。
// 经验表明,一个简单的16位累加和是许多此类模块的首选。
// 开发者应首先尝试累加和算法。
通过向模块发送使用0x0026作为校验和的帧,如果模块返回了包含固件版本信息的响应帧,则证明16位累加和是正确的算法。这一过程将理论分析转化为可执行的验证步骤,是成功驱动该模块的关键。
完整命令集参考
下表对模块支持的所有命令进行了分类和整理,提供了比原始文档更具结构化的参考 。
表4:DMR858M命令代码(CMD)完整参考
CMD (Hex) |
功能描述 |
R/W 支持 |
范围 |
持久化 |
备注 |
配置命令 (断电保存) |
|
|
|
|
|
$0\times01$ |
切换信道 |
写 |
当前 |
是 |
切换到指定信道。 |
$0\times02$ |
设置接收音量 |
写 |
所有 |
是 |
设置音频输出音量等级。 |
$0\times0B$ |
设置麦克风增益 |
写 |
所有 |
是 |
调整麦克风灵敏度。 |
$0\times0C$ |
设置省电模式 |
写 |
所有 |
是 |
开启或关闭低功耗模式。 |
$0\times0D$ |
设置收发频率 |
读/写 |
当前 |
是 |
分别设置当前信道的接收和发射频率。 |
$0\times12$ |
设置静噪(SQ)等级 |
读/写 |
当前 |
是 |
设置模拟模式下的静噪开启阈值。 |
$0\times13$ |
设置CTCSS/CDCSS模式 |
读/写 |
当前 |
是 |
设置亚音频的模式(如仅接收、仅发射、收发)。 |
$0\times14$ |
设置CTCSS/CDCSS值 |
读/写 |
当前 |
是 |
设置具体的亚音频码。 |
$0\times17$ |
设置高/低功率 |
读/写 |
当前 |
是 |
切换当前信道的发射功率。 |
操作命令 (即时生效) |
|
|
|
|
|
$0\times03$ |
扫描 |
写 |
当前 |
否 |
启动或停止信道扫描。 |
$0\times06$ |
发起呼叫 |
写 |
当前 |
否 |
发起组呼或个呼。 |
$0\times07$ |
发送短信 |
写 |
当前 |
否 |
发送DMR短信。 |
$0\times09$ |
紧急报警 |
写 |
当前 |
否 |
触发紧急报警功能。 |
$0\times15$ |
监听开关 |
写 |
当前 |
否 |
强制打开静噪以监听信道活动。 |
状态查询命令 |
|
|
|
|
|
$0\times04$ |
检查收发状态 |
读 |
当前 |
否 |
查询模块当前是处于接收、发射还是空闲状态。 |
$0\times05$ |
读取信号强度值 |
读 |
当前 |
否 |
获取当前接收信号的RSSI值。 |
$0\times16$ |
读取误码率(BER) |
读 |
当前 |
否 |
获取数字模式下的误码率。 |
$0\times24$ |
读取ID |
读 |
所有 |
否 |
读取模块的DMR ID。 |
$0\times25$ |
读取固件版本 |
读 |
所有 |
否 |
读取模块的固件版本号。 |
$0\times28$ |
检查加密状态 |
读 |
当前 |
否 |
查询当前信道是否启用了加密。 |
4. 固件开发:一个结构化的ESP32驱动程序
为了高效、可靠地控制DMR858M,建议采用面向对象的方法,创建一个驱动程序类来封装所有与模块的交互。这种架构类似于为其他AT指令模块(如GSM或Wi-Fi模块)设计的库,具有良好的模块化和可重用性 21。
架构方法:DMR858M_Controller 类
我们将设计一个名为DMR858M_Controller的C++类。这个类将负责管理UART通信、构建和解析数据帧、处理命令与响应,以及管理模块的状态。
C++
// DMR858M_Controller.h
#include <Arduino.h>
class DMR858M_Controller {
public:
DMR858M_Controller(HardwareSerial& serial, int pttPin, int csPin);
void begin(long speed);
bool setFrequency(uint32_t txFreq, uint32_t rxFreq);
bool setPowerLevel(bool highPower);
bool getFirmwareVersion(String& version);
void setPTT(bool active);
//... 其他功能函数
private:
HardwareSerial& _serial;
int _pttPin;
int _csPin;
void sendCommand(uint8_t cmd, uint8_t rw, const uint8_t* data, uint16_t len);
bool waitForResponse(uint8_t* buffer, uint16_t& len, uint32_t timeout = 1000);
uint16_t calculateChecksum(const uint8_t* data, size_t len);
};
核心实现细节 (代码示例)
数据包构建与发送
sendCommand是所有写操作的核心。它负责组装完整的二进制数据包,计算校验和,并通过UART发送。
C++
// DMR858M_Controller.cpp
void DMR858M_Controller::sendCommand(uint8_t cmd, uint8_t rw, const uint8_t* data, uint16_t len) {
uint8_t frame; // 足够大的缓冲区
frame = 0x68; // Head
frame = cmd;
frame = rw;
frame = 0x01; // S/R (Request)
// LEN (Little Endian)
frame = len & 0xFF;
frame = (len >> 8) & 0xFF;
// DATA
if (data && len > 0) {
memcpy(&frame, data, len);
}
// CKSUM
// 参与校验的数据从CMD开始,到DATA结束
uint8_t checksum_data;
checksum_data = cmd;
checksum_data = rw;
checksum_data = 0x01;
checksum_data = frame;
checksum_data = frame;
if (data && len > 0) {
memcpy(&checksum_data, data, len);
}
uint16_t checksum = calculateChecksum(checksum_data, 5 + len);
frame = (checksum >> 8) & 0xFF; // CKSUM_HI (Big Endian)
frame = checksum & 0xFF; // CKSUM_LO
frame[8 + len] = 0x10; // Tail
_serial.write(frame, 9 + len);
}
uint16_t DMR858M_Controller::calculateChecksum(const uint8_t* data, size_t len) {
// 采用16位累加和算法
uint16_t sum = 0;
for (size_t i = 0; i < len; ++i) {
sum += data[i];
}
return sum;
}
响应处理与异步操作的重要性
在嵌入式系统中,阻塞式等待是一种应极力避免的编程模式。一个简单的waitForResponse函数如果采用while(!_serial.available()){}这样的循环,将会冻结整个主循环,使MCU无法执行其他任务,如更新显示、响应按键等,导致系统无响应。
一个更健壮的设计应该采用非阻塞的方式。在主循环中,程序应不断检查串口是否有数据,并使用一个状态机来处理数据帧的接收。这种方式可以确保系统在等待模块响应的同时,仍然能够处理其他实时事件。对于ESP32这样支持FreeRTOS的平台,更优的方案是创建一个专门的RTOS任务来处理与DMR模块的通信,该任务可以在没有数据时阻塞,而不会影响其他任务的运行。
以下是一个简化的非阻塞读取逻辑示例,适用于Arduino的loop()函数:
C++
// 简化的非阻塞响应处理逻辑
void loop() {
//... 其他任务...
if (_serial.available()) {
// 读取字节并放入缓冲区
// 使用状态机解析数据帧 (寻找帧头0x68,读取指定长度,验证校验和和帧尾0x10)
// 解析成功后,处理响应数据
}
}
综合示例:一个概念验证(Proof-of-Concept)程序
以下是一个完整的Arduino/PlatformIO示例,演示了如何初始化模块、通过按键控制PTT,并通过串口监视器发送短信。
C++
#include <Arduino.h>
#include "DMR858M_Controller.h"
#define PTT_BUTTON_PIN 25
#define PTT_MODULE_PIN 26
#define LED_PIN 2
HardwareSerial SerialTwo(2);
DMR858M_Controller dmr(SerialTwo, PTT_MODULE_PIN, -1);
void setup() {
Serial.begin(115200);
pinMode(PTT_BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
dmr.begin(57600);
delay(500);
String fwVersion;
if (dmr.getFirmwareVersion(fwVersion)) {
Serial.println("DMR858M Firmware: " + fwVersion);
} else {
Serial.println("Failed to communicate with DMR858M module.");
}
// 示例:设置信道1的频率为 433.500 MHz
dmr.setFrequency(433500000, 433500000);
}
void loop() {
// PTT 控制逻辑
if (digitalRead(PTT_BUTTON_PIN) == LOW) {
dmr.setPTT(true);
digitalWrite(LED_PIN, HIGH); // 发射指示
} else {
dmr.setPTT(false);
digitalWrite(LED_PIN, LOW);
}
//... 此处可以添加非阻塞的串口响应处理逻辑...
// 示例:通过串口监视器发送短信
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
if (cmd.startsWith("sms")) {
// 解析短信内容和目标ID
// 调用 dm.sendSMS(...)
Serial.println("SMS command received.");
}
}
}
结论
成功集成DMR858M模块的核心在于遵循几个关键的工程实践:设计一个能够应对高瞬态电流的稳健电源系统;通过系统化的测试方法确定并实现正确的串行通信校验和算法;以及采用结构化、非阻塞的固件架构来确保系统的实时响应能力。
DMR858M作为一个高度集成的DMR子系统,为开发者提供了一条快速构建专业级数字通信产品的捷径。它通过板载AMBE++声码器,解决了开源社区长期面临的兼容性和法律合规性难题,让开发者可以将精力集中在创造独特的用户体验和应用功能上。
探索高级功能
掌握了基础的通信和控制后,开发者可以进一步利用该模块的高级功能来构建更复杂的应用:
- 低功耗操作:对于电池供电的设备,功耗是至关重要的。通过控制CS(引脚3),可以使模块进入深度睡眠模式,此时电流消耗小于0.1mA。在需要通信时再将其唤醒,可以极大地延长设备的续航时间 。
- DMR高级呼叫:除了默认的组呼,DMR协议还支持个呼(Private Call)和全呼(All Call)。通过使用CMD=0x18(设置联系人)和CMD=0x22(发送联系人信息)等指令,可以实现更灵活的呼叫控制 。
- 语音加密:对于需要安全通信的应用场景,可以使用CMD=0x19指令来开启或关闭内置的语音加密功能,为通话提供基本的隐私保护 。
通过本文提供的硬件参考设计、协议分析和固件开发框架,工程师应能具备将DMR858M模块成功集成到其项目中的所有必要知识和工具。
引用的著作
- ESP32 Walkie-Talkie: DIY Audio Magic - atomic14,
- jeffskinnerbox/esp32-walkie-talkie: A Walkie-Talkie based around the ESP32 using UDP Broadcast or ESP-NOW or Mumble - GitHub
- sh123/esp32_loradv: ESP32 based Codec2/OPUS DV hobby UHF 3d printed handheld transceiver aka walkie-talkie - GitHub
- Improving Open-AMBE for D-Star - QSL.net
- M17: an open-source, DMR-like system - KB6NU's Ham Radio Blog, 访问时间为 八月 25, 2025
- Open source DMR (Digital Mobile Radio) modem implementation in software defined radio with GNU Radio and Codec2 - QRadioLink
- The ugly truth about open-source digital radio standards : r/amateurradio - Reddit
- RS232 checksum calculate - NI Community - National Instruments
- The Effectiveness of Checksums for Embedded Control Networks - ResearchGate
- The Effectiveness of Checksums for Embedded Networks - Electrical and Computer Engineering
- Checksum - Wikipedia
- The Effectiveness of Checksums for Embedded Control Networks - Electrical and Computer Engineering
- Reverse Engineering a 16-bit checksum on UART protocol : r/embedded - Reddit
- Block Check Character (BCC) - HMGforum.com
- CRC Implementation Code in C and C++ - Barr Group
- CRC-16 Calculation - EmbeddedRelated.com
- How to do CRC16 right? - AVR Freaks
- Understanding CRC - Sunshine's Homepage
- Function to Calculate a CRC16 Checksum - Stack Overflow
- Library for Arduino IDE to send AT or ASCII commands via UART - GitHub
- at-command · GitHub Topics
- veeso/ATtila: Python module to communicate easily with modems and RF modules using AT commands - GitHub