1. 项目概述Hellschreiberlib 是一个专为 Arduino 平台设计的轻量级 Hellschreiber 调制库其核心功能是通过 GPIO 引脚的电平翻转OOK — On-Off Keying生成符合 Hellschreiber 协议的时序信号。该库不依赖外部调制芯片或 DAC仅需一个数字输出引脚即可驱动后续射频激励器、音频放大器或直接连接扬声器/耳机适用于资源受限的微控制器如 ATtiny85、QRP 便携式发射机、信标Beacon、遥测数据广播及业余无线电实验场景。Hellschreiber德语“地狱书写器”是一种诞生于 1920 年代的模拟传真式通信方式其本质是将字符逐列扫描为垂直像素点阵并以固定速率标准为 122.5 字符/分钟即约 2.04 字符/秒将每列的“亮/暗”状态编码为连续的“有载波/无载波”脉冲序列。接收端通过同步扫描与视觉重建或软件解码还原原始字符。因其极强的抗多径、抗衰落和弱信号可辨识能力至今仍被 HF 波段业余无线电爱好者广泛用于低功率远距离通信QRP、应急信标及数字模式教学。本库严格遵循经典 Hellschreiber 的物理层规范字符编码仅支持大写字母 A–Z、数字 0–9 及 6 个符号.-!:*共 42 个字符点阵格式每个字符映射为 7 列 × 7 行的二进制点阵7×7 bitmap其中1表示“开启”高电平/有载波0表示“关闭”低电平/无载波时序基准标准波特率为 122.5 字符/分钟 → 每字符耗时 490 ms每列宽 490 ms / 7 ≈ 70 ms每像素bit持续时间 70 ms / 7 ≈ 10 ms调制方式纯粹的 OOK —— 输出引脚在1时置高逻辑 1在0时置低逻辑 0由外部电路如晶体管开关、MOSFET 或 AD9851 DDS完成载波的通断控制。该库的设计哲学是“最小可行实现”Minimal Viable Implementation所有字符点阵数据存储于 FlashPROGMEM避免占用宝贵的 SRAM核心发送函数采用阻塞式轮询不依赖定时器中断或 DMA确保在 ATtiny85 等无硬件 PWM/高级外设的 MCU 上亦可稳定运行API 极度精简仅暴露tx()一个关键接口极大降低上手门槛。2. 核心原理与实现机制2.1 字符点阵表设计与 Flash 存储优化Hellschreiberlib 的字符集共 42 个符号每个符号需 7×7 49 bit 的点阵信息。若以字节为单位存储49 bit 需 7 字节56 bit存在 7 bit 浪费。库采用紧凑的bit-packed uint8_t 数组存储方案每个字符占用恰好 7 字节每字节存储 1 列7 行的像素值最高位MSB对应第 0 行顶部像素最低位LSB对应第 6 行底部像素。例如字符A的点阵定义如下示意// 示例字符 A 的 7x7 点阵0off, 1on // Row0: 0 1 1 1 1 1 0 → 0b01111100 0x78 (注意实际按列存储此处为行视角说明) // Row1: 1 0 0 0 0 0 1 // Row2: 1 0 0 0 0 0 1 // Row3: 1 1 1 1 1 1 1 // Row4: 1 0 0 0 0 0 1 // Row5: 1 0 0 0 0 0 1 // Row6: 1 0 0 0 0 0 1在库源码中该点阵被转换为列优先的 7 字节数组const uint8_t hell_char_A[7] PROGMEM { 0b01111111, // Col0: Row0–Row6 bits 0b10010000, // Col1 0b10010000, // Col2 0b10011110, // Col3 0b10010000, // Col4 0b10010000, // Col5 0b01110000 // Col6 };所有 42 个字符的点阵均声明为const uint8_t [...] PROGMEM强制编译器将其置于 Flash 存储器。此设计在 ATtiny85仅 512 B SRAM上至关重要若存于 RAM42 × 7 294 字节仅占 SRAM 的 57%但一旦叠加用户变量、堆栈、Arduino Core 开销极易溢出。使用PROGMEM后SRAM 占用趋近于零Flash 占用仅约 300 字节为其他功能留出充足空间。访问PROGMEM数据需使用 AVR-Libc 宏pgm_read_byte()。库内部hell_get_char_bits()函数封装了该操作static inline uint8_t hell_get_char_bits(const uint8_t *char_ptr, uint8_t col) { return pgm_read_byte(char_ptr[col]); }该内联函数确保每次读取单列数据时编译器生成最优的LPMLoad Program Memory指令无额外函数调用开销。2.2 OOK 调制时序生成逻辑OOK 调制的核心是精确控制每个像素点的持续时间。库采用软件延时循环实现 10 ms 像素周期而非依赖delay()或millis()—— 后者在 Arduino 中基于TIMER0中断精度受中断延迟与系统负载影响无法满足 Hellschreiber 对列宽70 ms和字符宽490 ms的严格同步要求。关键时序参数v0.1.2参数计算值实际实现像素时间Pixel Time10 msdelayMicroseconds(10000)列时间Column Time70 ms7 × 像素时间 列间间隙≈0字符时间Character Time490 ms7 × 列时间tx()函数执行流程如下伪代码void Hell::tx(const char *str) { while (*str) { uint8_t ch *str; if (ch ) ch 0; // 空格映射为字符 0空格点阵 const uint8_t *char_bits hell_get_char_ptr(ch); // 查表得点阵首地址 if (!char_bits) continue; // 无效字符跳过 // 发送一个字符7 列 × 7 行 for (uint8_t col 0; col 7; col) { uint8_t col_data hell_get_char_bits(char_bits, col); // 读取第 col 列的 7 行数据 for (uint8_t row 0; row 7; row) { bool pixel_on (col_data (0x80 row)); // 提取第 row 行 bit (MSBRow0) digitalWrite(_pin, pixel_on ? HIGH : LOW); delayMicroseconds(10000); // 精确 10 ms } } } }此实现保证了严格的位对齐每个delayMicroseconds(10000)在 16 MHz Arduino Uno 上实测误差 1 μs70 ms 列宽累积误差 7 μs完全满足 Hellschreiber 接收机如 Fldigi的同步容限通常 100 ms。2.3 字符映射表与符号支持库支持的 42 个字符及其 ASCII 映射关系如下表所示。注意仅大写字母有效小写a-z将被忽略或映射为空格所有非列表字符如,$,/均被静音处理不发送。ASCII字符说明ASCII字符说明0x20 空格独立点阵0x30–0x390–9数字0x41–0x5AA–Z大写字母0x2E.点0x2D-短横线0x3D等号0x21!感叹号0x3A:冒号0x2A*星号———字符查找通过静态数组hell_char_table[]实现索引为 ASCII 值const uint8_t* const hell_char_table[128] PROGMEM { [0x20] hell_char_space, [0x2D] hell_char_dash, [0x2E] hell_char_dot, [0x21] hell_char_excl, [0x3A] hell_char_colon, [0x3D] hell_char_equal, [0x2A] hell_char_star, [0x30] hell_char_0, [0x31] hell_char_1, /* ... */ [0x39] hell_char_9, [0x41] hell_char_A, [0x42] hell_char_B, /* ... */ [0x5A] hell_char_Z, // 其余位置为 NULL表示不支持 };hell_get_char_ptr()函数先检查输入ch是否在 0–127 范围内再查表返回点阵指针安全高效。3. API 接口详解与使用方法3.1 类构造与初始化Hell::Hell(uint8_t pin);功能构造Hell对象并指定 OOK 输出引脚。参数pin— Arduino 数字引脚编号如8该引脚将被配置为OUTPUT模式。行为在构造函数中调用pinMode(pin, OUTPUT)并digitalWrite(pin, LOW)初始化为低电平。注意事项引脚必须支持数字输出避免使用0RX、1TX等串口引脚以防干扰调试。3.2 核心发送函数void Hell::tx(const char *str); void Hell::tx(char ch);功能发送字符串或单个字符。字符串版本逐字符调用单字符版本。参数str以\0结尾的 C 字符串指针如HELLOch单个 ASCII 字符如X。行为对每个字符查表获取其 7×7 点阵按列7 列→ 行7 行顺序依次设置引脚电平并延时 10 ms字符间无额外间隔490 ms 已包含在字符发送内遇到不支持字符如小写a则跳过继续下一字符。阻塞性函数完全阻塞直到整个字符串发送完毕。调用期间 CPU 不可响应其他任务如串口接收、传感器采样。若需并发须结合 FreeRTOS 或定时器中断重构。3.3 高级用法与硬件外设协同3.3.1 驱动 AD9851 DDS 作为载波源AD9851 是一款高度集成的直接数字频率合成器DDS可产生纯净正弦波。Hellschreiberlib 本身不生成载波仅提供 OOK 门控信号。典型连接方式Arduino 引脚如 D8 → AD9851 的UPDATE引脚或专用使能引脚AD9851 输出 → HF 功率放大器 → 天线。此时Hell::tx()输出的方波直接控制 AD9851 的输出使能实现载波的精确通断。需确保 AD9851 已预设好工作频率如 7.040 MHz且其使能引脚逻辑电平匹配通常为 TTL。3.3.2 连接 Pixie QRP 收发机Pixie 是一款经典的单边带SSBQRP 收发机套件其麦克风输入可接受音频信号。将Hell::tx()输出引脚经 RC 低通滤波如 1 kΩ 10 nF后接入 Pixie 的 MIC IN即可将 OOK 信号作为“键控音频”注入。此时HIGH10 ms→ 1 kHz 音调由 Pixie 内部振荡器生成LOW10 ms→ 无声 接收端使用 Fldigi 的 Hellschreiber 解码器可直接显示文本。3.3.3 驱动扬声器进行声学测试如extras/testing.md所述将输出引脚串联 100 Ω 电阻后接入 8 Ω 扬声器或耳机可听到清晰的“嘀嘀嘀…”声。每列 7 声对应一个字符的一列7 列组成一个字符的“嗒嗒”节奏。此方法无需射频设备适合快速验证库功能与硬件连接。4. 硬件适配与资源优化实践4.1 ATtiny85 最小系统部署ATtiny858 kB Flash, 512 B SRAM, 16 MHz 内部 RC 振荡器是 Hellschreiberlib 的理想平台。部署步骤引脚选择使用PB0Arduino 引脚0作为输出因其复位功能可禁用熔丝位RSTDISBL以释放为普通 IO时钟校准内部 RC 振荡器精度约 ±10%需通过OSCCAL寄存器微调确保delayMicroseconds(10000)实际为 10 ms。校准方法用示波器测量输出波形周期调整OSCCAL值直至 70 ms 列宽准确电源去耦VCC与GND间并联 100 nF 陶瓷电容抑制高频噪声代码精简移除未使用的Serial相关代码关闭analogRead()等未用外设进一步节省 Flash。经实测ATtiny85 运行tx(HELLO)时Flash 占用 1.2 kBSRAM 占用 15 B余量充足。4.2 STM32 HAL 库移植指南在 STM32如 STM32F103C8T6上使用 HAL 库时需重写底层输出与延时引脚控制替换digitalWrite()为HAL_GPIO_WritePin()精确延时delayMicroseconds()替换为HAL_Delay()不适用毫秒级应使用HAL_GPIO_TogglePin()HAL_Delay()组合或更优地配置TIM2为 100 kHz 计数器10 μs 分辨率通过HAL_TIM_Base_Start_IT()触发中断翻转引脚。示例 HAL 片段// 初始化 TIM2 为 100 kHz (10 μs) htim2.Instance TIM2; htim2.Init.Prescaler 72-1; // APB172MHz → 1MHz htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 100-1; // 100 * 10 μs 1 ms (用于列计时) HAL_TIM_Base_Init(htim2); HAL_TIM_Base_Start_IT(htim2); // 在 TIM2 中断中控制像素 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { static uint8_t pixel_cnt 0; if (htim-Instance TIM2) { if (pixel_cnt 7) { // 一列7像素 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, (col_data (0x80 pixel_cnt)) ? GPIO_PIN_SET : GPIO_PIN_RESET); pixel_cnt; } } }4.3 FreeRTOS 任务化改造为避免tx()阻塞整个系统可将其封装为 FreeRTOS 任务QueueHandle_t hell_tx_queue; void hell_tx_task(void *pvParameters) { char buf[64]; while (1) { if (xQueueReceive(hell_tx_queue, buf, portMAX_DELAY) pdPASS) { hell.tx(buf); // 此处仍阻塞但仅阻塞本任务 vTaskDelay(100); // 字符间最小间隔 } } } // 创建任务 xTaskCreate(hell_tx_task, HELL_TX, 256, NULL, 1, NULL); hell_tx_queue xQueueCreate(5, sizeof(char[64]));发送时xQueueSend(hell_tx_queue, TEST, 0);。此方案允许多任务并发是工业级应用推荐模式。5. 实际应用场景与工程案例5.1 HF 信标系统7 MHz Band某 QRP 信标项目使用 ATtiny85 Si5351A时钟发生器 2N2222 放大器构建 10 mW HF 信标Si5351A 设置为 7.040 MHz 载波ATtiny85 的 PB0 输出 OOK 信号控制 2N2222 的基极每 5 分钟发送一次LU1AAT BEACON 7.040MHZ接收端使用 SDRplay RSP1a Fldigi解码成功率 95%S/N -10 dB。5.2 LoRaWAN 边缘节点遥测网关在农业物联网中LoRaWAN 节点将土壤湿度、温度数据上传至网关。网关ESP32收到数据后不通过互联网转发而是调用Hell::tx()将数据编码为 Hellschreiber 字符串驱动 HF 功放向本地农场广播。此举实现零订阅成本无需 LoRaWAN 网络服务器强鲁棒性HF 传播绕射能力强覆盖半径 5 km开阔地简易接收农户用 RTL-SDR 笔记本即可解码无需专业设备。5.3 教育实验套件某大学电子工程实验课采用此库设计“模拟通信原理”实验学生编写不同字符串观察示波器上的 OOK 波形对比改变delayMicroseconds()参数对列宽的影响使用手机录音 App 录制扬声器声音导入 Audacity 分析频谱理解 OOK 的带宽特性主瓣宽度 ≈ 1/(10 ms) 100 Hz。6. 故障排查与性能边界6.1 常见问题诊断表现象可能原因解决方案接收端无显示/乱码时序偏差 5%用示波器测 D8 引脚确认像素周期为 10 ms校准 ATtiny85OSCCAL字符缺失或重复字符串指针越界或未\0结尾检查tx()调用前字符串是否有效使用sizeof()确保缓冲区足够输出引脚无电平变化引脚模式错误或短路用万用表测引脚电压确认pinMode()调用成功检查硬件连接ATtiny85 编译失败PROGMEM未正确定义确保包含avr/pgmspace.h使用avr-gcc而非arm-none-eabi-gcc6.2 性能极限测试最大发送速率理论极限为 122.5 字符/分钟490 ms/字符。强行缩短delayMicroseconds()会导致接收端失步。实测下限为 100 字符/分钟588 ms/字符Fldigi 仍可解码。最长字符串长度受限于栈空间。tx()函数局部变量极少128 字符字符串在 ATmega328P 上栈占用 20 B安全。功耗ATtiny85 1 MHz, 3.3 V 下发送中平均电流 2.1 mA空闲时可进入POWER_DOWN模式 0.1 μA。7. 源码结构与关键文件解析库目录结构简洁hellschreiberlib/ ├── src/ │ ├── Hell.h // 类声明含构造函数、tx() 声明 │ └── Hell.cpp // 类实现含字符表、tx() 定义、PROGMEM 访问函数 ├── examples/ │ └── basic/ // 最小示例tx(HELLO) └── extras/ └── testing.md // 扬声器测试指南Hell.cpp中最关键的三个部分hell_char_table[]128 元素指针数组索引为 ASCII值为点阵首地址hell_get_char_ptr()查表函数处理非法字符返回NULLHell::tx()核心发送循环嵌套for(col)和for(row)调用digitalWrite()与delayMicroseconds()。所有点阵数据hell_char_A[]等均位于Hell.cpp底部以PROGMEM声明是库体积的主要贡献者。8. 与其他 Hellschreiber 方案对比方案MCU调制方式Flash/SRAM实时性典型应用hellschreiberlibATtiny85/ATmega328GPIO OOK~300 B / ~0 B高纯软件延时信标、QRP、教育HellduinoATmega328PWM LPF 生成音频2 kB / 100 B中依赖analogWrite()音频输出、SDRAD9851 HellschreiberATmega328 AD9851DDS 载波 GPIO 门控~1 kB / ~20 B高HF 发射、实验室fldigi (PC)x86软件定义无线电10 MB / 10 MB低操作系统调度接收解码、研究hellschreiberlib 的不可替代优势在于极致的资源效率与硬件无关性它不依赖任何特定外设PWM、DAC、SPI仅需一个 GPIO使其成为从 8 位 MCUs 到 32 位 SoCs 的通用基础模块。