51单片机驱动LoRa模块实现远程无线通信实战 简介这份资源面向51单片机与LoRa无线通信的入门及进阶开发者聚焦如何借助经典51单片机驱动LoRa模块完成远距离串口数据收发解决嵌入式物联网场景中低功耗远程通信的落地问题。压缩包共30个文件约47KB以C语言源码、Keil工程文件uvproj、uvopt、启动汇编STARTUP.A51、编译中间文件obj、lst、m51及hex烧录文件为主并区分发送端与接收端两套程序便于对照调试。内容涉及51单片机串口寄存器配置、SPI时序、LoRa扩频参数设置、CRC校验与应答机制等关键环节代码结构清晰可直接用于实验复现与二次开发。目前已有950人学习下载适合希望快速搭建LoRa点对点通信原型、理解底层驱动流程的开发者参考。1. 两块 51 单片机隔空对话LoRa 远程通信到底能解决什么问题去年帮一个做农业监测的朋友搭远程数据采集现场离机房两公里没网线、没WiFi手机信号也时有时无。他一开始想用蓝牙结果传不过一百米想用4G模块每年流量费又觉得不划算。最后我用两块51单片机各挂一个LoRa模块一端接温湿度传感器一端接电脑串口两公里外数据稳稳传回来成本不到五十块。这就是LoRa在低速远距离场景里的价值——它不追求带宽追求的是穿墙、绕射和低功耗特别适合传感器数据回传、远程开关控制、小型物联网节点这类活儿。这个项目要讲的就是怎么用最经典的51单片机通过串口去驱动LoRa模块实现两块板子之间的无线数据收发。你手里如果有STC89C52或者AT89S52这类开发板再配上一对常见的LoRa串口模块比如基于SX1278芯片的就能跟着跑通。整套东西不依赖操作系统纯裸机串口收发代码量小逻辑清晰新手拿来练手串口通信和无线模块驱动非常合适熟手也能从中看到中断收发、环形缓冲、协议帧设计这些工程细节。2. 硬件选型与接线从单片机串口到LoRa模块的物理链路2.1 为什么选51单片机配串口LoRa模块51单片机虽然老但它的UART是标准外设几乎每颗芯片都带而且寄存器少、时序简单用来驱动“串口透传型”LoRa模块再合适不过。市面上常见的LoRa模块分两种一种是SPI接口的裸射频芯片需要自己写寄存器配置另一种是串口透传模块内部已经跑好了LoRa协议栈你只要通过UART发数据它就自动打包发出去收到数据也会从UART吐出来。这个项目用的是后者因为对51来说SPI驱动射频芯片太吃资源而串口透传把复杂度降到了最低。选型时注意几个参数频率要选合法频段比如470MHz或433MHz发射功率一般模块可调空中速率建议从2.4kbps起步距离和抗干扰会好一些。模块的供电通常是3.3V而51开发板多是5V所以电平匹配必须处理否则要么收不到要么烧模块。2.2 接线表与电平匹配的坑下面是我实际用的一套接线两块板子完全对称一块作发送端一块作接收端。51单片机引脚LoRa模块引脚说明P3.0 (RXD)TXD单片机接收接模块发送P3.1 (TXD)RXD单片机发送接模块接收5VVCC (经AMS1117-3.3)模块供电3.3V不能直接接5VGNDGND共地P1.0AUX可选用于检测模块忙闲P1.1M0模式控制一般拉低P1.2M1模式控制一般拉低这里最容易翻车的是供电。很多新手看模块上标了“3.3V-5V”就直接怼5V结果通信距离骤降甚至模块发烫。正确做法是如果模块明确是3.3V版就用AMS1117或者LM1117把5V降到3.3V再供。另外51的TXD是5V输出接到模块RXD时模块内部如果没做电平兼容长期可能损坏。稳妥点加一个电阻分压或者电平转换芯片我一般串一个1k电阻再并一个3.3V稳压管简单有效。2.3 模块工作模式与配置串口LoRa模块通常有几种模式透明传输、定点发射、广播发射。透明传输就是“串口进什么空中出什么”接收端原样吐出最适合快速验证。配置时一般用AT指令或者上位机软件改波特率、信道、地址、空中速率。我一般把模块波特率设成9600和51的UART一致减少一次波特率换算。信道和地址两端必须完全一样否则收不到。空中速率如果设得太高距离会明显缩短2.4kbps在开阔地两公里没问题城里穿墙大概几百米。配置完记得把模块切回透明传输模式M0、M1都拉低然后断电重启让参数生效。这一步不做后面代码怎么写都收不到数据。3. 串口通信核心代码51单片机UART收发与环形缓冲3.1 UART初始化与波特率计算51的UART工作方式1是8位数据、可变波特率用定时器1做波特率发生器。假设晶振是11.0592MHz要得到9600bps定时器初值这样算#include reg52.h // 串口初始化9600bps 11.0592MHz void UART_Init(void) { SCON 0x50; // 方式18位数据允许接收 TMOD 0x0F; // 清除定时器1模式位 TMOD | 0x20; // 定时器1方式28位自动重装 TH1 0xFD; // 波特率9600SMOD0时初值0xFD TL1 0xFD; TR1 1; // 启动定时器1 ES 1; // 打开串口中断 EA 1; // 打开总中断 }逻辑说明SCON的SM0、SM1设为01即方式1REN置1允许接收。TMOD的低4位保留高4位设为0010让定时器1工作在方式2自动重装这样波特率稳定。TH1和TL1都填0xFD对应9600bps。如果晶振是12MHz这个初值会有误差通信会丢包所以强烈建议用11.0592MHz晶振这是51串口的血泪经验。参数怎么改要改波特率查手册里的初值表。比如4800bps对应0xFA19200对应0xFD但SMOD要置1。改完记得同步改LoRa模块的波特率否则两边对不上。3.2 环形缓冲区设计直接在中断里处理数据是大忌因为中断要快进快出。我一般用一个环形缓冲把收到的字节存起来主循环慢慢解析。#define BUF_SIZE 64 typedef struct { unsigned char data[BUF_SIZE]; unsigned char head; // 写指针中断里用 unsigned char tail; // 读指针主循环用 } RingBuf; RingBuf rxBuf; // 中断接收每来一个字节存进环形缓冲 void UART_ISR(void) interrupt 4 { if (RI) { RI 0; // 清除接收中断标志 unsigned char next (rxBuf.head 1) % BUF_SIZE; if (next ! rxBuf.tail) { // 缓冲区未满 rxBuf.data[rxBuf.head] SBUF; rxBuf.head next; } // 满了就丢弃避免覆盖未读数据 } if (TI) { TI 0; // 发送中断本项目用查询发送这里只清标志 } } // 从环形缓冲取一个字节返回0表示无数据 unsigned char RingBuf_Get(unsigned char *byte) { if (rxBuf.head rxBuf.tail) { return 0; } *byte rxBuf.data[rxBuf.tail]; rxBuf.tail (rxBuf.tail 1) % BUF_SIZE; return 1; }逻辑说明head是中断写位置tail是主循环读位置。当head等于tail时缓冲区空当head的下一个位置等于tail时缓冲区满。满的时候直接丢弃新数据而不是覆盖旧数据这样保证主循环读到的每一帧是完整的。BUF_SIZE取64对传感器数据足够如果传长包可以加到128或256但51的RAM有限别超过256。参数说明BUF_SIZE必须是2的幂或者至少能被整除这里用取模运算51做除法慢如果追求效率可以改成if判断回绕。中断里只做存字节不做解析这是保证不丢数据的关键。3.3 发送函数与超时处理发送我一般用查询方式简单可靠但必须加超时否则模块忙的时候会卡死主循环。// 发送一个字节带超时返回1成功0失败 unsigned char UART_SendByte(unsigned char byte) { unsigned int timeout 5000; TI 0; SBUF byte; while (!TI timeout--) { // 等待发送完成 } if (timeout 0) { return 0; // 超时模块可能没响应 } TI 0; return 1; } // 发送一个字符串 void UART_SendString(unsigned char *str) { while (*str) { if (!UART_SendByte(*str)) { break; // 超时则中止避免死等 } } }逻辑说明SBUF写入后硬件开始发送TI在发送完成时由硬件置1。timeout递减防止模块掉线时死循环。如果返回0主循环可以做重发或者报警。发送字符串时逐个字节发遇到超时就退出不阻塞后续逻辑。参数说明timeout的值根据主频调整11.0592MHz下5000次循环大概几毫秒足够发一个字节。如果发现经常超时先查接线和模块供电再查波特率是否一致。4. 数据帧协议与收发联调让两块板子真正对话4.1 自定义帧格式透明传输虽然简单但实际用的时候不能裸发数据否则接收端不知道包从哪开始、到哪结束。我一般定义一个最小帧帧头0xAA、0x55然后长度、命令字、数据区、校验和。// 帧格式AA 55 LEN CMD DATA... CHECKSUM #define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 #define MAX_DATA_LEN 32 typedef struct { unsigned char len; // 数据区长度 unsigned char cmd; // 命令字 unsigned char data[MAX_DATA_LEN]; unsigned char checksum; // 从LEN到DATA的累加和 } Frame; // 打包一帧 unsigned char Frame_Pack(Frame *f, unsigned char *out) { unsigned char i, idx 0; unsigned char sum 0; out[idx] FRAME_HEAD1; out[idx] FRAME_HEAD2; out[idx] f-len; sum f-len; out[idx] f-cmd; sum f-cmd; for (i 0; i f-len; i) { out[idx] f-data[i]; sum f-data[i]; } out[idx] sum; // 校验和 return idx; // 返回帧总长度 }逻辑说明帧头两个字节用来同步接收端状态机先找0xAA再找0x55。LEN表示后面数据区有多少字节CMD区分是传感器数据还是控制命令。校验和用累加和简单但够用能挡住大部分传输错误。如果环境干扰大可以换成CRC16但51算CRC会慢一些。参数说明MAX_DATA_LEN根据实际数据量定传温湿度一般4字节就够传字符串可以到32。帧总长度别超过环形缓冲大小否则一帧都存不下。4.2 接收状态机解析接收端不能假设一次中断就能收完一整帧必须用状态机逐字节推进。typedef enum { STATE_HEAD1, STATE_HEAD2, STATE_LEN, STATE_CMD, STATE_DATA, STATE_CHECK } RxState; RxState state STATE_HEAD1; Frame rxFrame; unsigned char dataCnt 0; unsigned char sum 0; // 在主循环里调用处理环形缓冲里的字节 void Frame_Parse(void) { unsigned char byte; while (RingBuf_Get(byte)) { switch (state) { case STATE_HEAD1: if (byte FRAME_HEAD1) state STATE_HEAD2; break; case STATE_HEAD2: state (byte FRAME_HEAD2) ? STATE_LEN : STATE_HEAD1; break; case STATE_LEN: rxFrame.len byte; sum byte; dataCnt 0; state (byte MAX_DATA_LEN) ? STATE_CMD : STATE_HEAD1; break; case STATE_CMD: rxFrame.cmd byte; sum byte; state STATE_DATA; break; case STATE_DATA: rxFrame.data[dataCnt] byte; sum byte; if (dataCnt rxFrame.len) state STATE_CHECK; break; case STATE_CHECK: if (byte sum) { // 校验通过处理rxFrame Frame_Handle(rxFrame); } state STATE_HEAD1; break; } } }逻辑说明状态机从HEAD1开始只有收到0xAA才进入HEAD2收到0x55才继续。LEN阶段如果长度超过MAX_DATA_LEN就丢弃重来防止缓冲区溢出。DATA阶段逐个存存满len个后进入CHECK校验和一致才交给Frame_Handle处理。这样即使中间丢了一个字节状态机也能在下一帧重新同步。参数说明Frame_Handle是业务处理函数收到数据后可以点灯、回发确认、或者通过串口转发到电脑。如果校验失败直接丢弃不回复让发送端超时重发。4.3 联调步骤与现象判断先烧两块板子一块发一块收。发送端主循环里每隔一秒发一帧温湿度模拟数据接收端收到后如果校验通过就让一个LED翻转。上电后观察接收端LED一秒闪一次说明链路通。LED不闪先看发送端串口有没有输出用USB转TTL接电脑串口助手看。发送端有输出但接收端没反应查两块模块的信道、地址、空中速率是否一致。都一致还不行把两块板子靠近到一米内排除距离和天线问题。近距离也不行查供电和电平模块VCC必须是3.3V。我一般还会在接收端加一句“收到帧就通过串口打印”这样电脑上能直接看到解析结果比只看LED直观得多。5. 避坑与常见问题排查5.1 现象模块发烫通信距离只有几米原因供电电压接错5V直接灌进3.3V模块内部LDO或射频芯片过压。解决立刻断电改用AMS1117-3.3降压测模块VCC引脚对地电压必须是3.3V左右。如果模块已经发烫很久基本报废换新的。5.2 现象串口助手能收到发送端数据但接收端收不到原因两块LoRa模块的参数不一致最常见的是信道和地址。解决用上位机软件分别读两块模块的配置确保频率、信道、地址、空中速率、波特率五项完全一样。改完参数必须断电重启很多模块不重启不生效。5.3 现象数据偶尔丢包校验经常失败原因空中速率设得太高或者天线没接好。解决把空中速率降到2.4kbps或1.2kbps距离和稳定性会明显提升。天线必须接匹配的频段470MHz模块接433MHz天线效果会差很多。另外发送端两帧之间加50ms以上间隔给模块留出发射时间。5.4 现象51单片机程序跑飞串口无输出原因中断里处理太多逻辑或者环形缓冲溢出导致指针错乱。解决中断里只做存字节解析全部放主循环。检查RingBuf的head和tail是否用unsigned char取模时别溢出。如果RAM紧张把BUF_SIZE降到32。5.5 现象接收端LED闪但数据不对原因帧头同步错误或者校验和计算范围不一致。解决发送端和接收端的校验和必须都从LEN开始累加到DATA最后一个字节不包括帧头。用串口助手把原始字节打出来逐字节对很快能找到偏差。6. 二次开发改造从点灯到传感器组网这套代码跑通之后最直接的改造就是接真实传感器。比如把发送端的模拟数据换成DHT11的温湿度接收端通过串口转发给电脑用串口助手或者Python脚本存成CSV。DHT11是单总线时序要求微秒级51跑11.0592MHz刚好能凑出来但读取时记得关中断否则时序会被串口中断打断。再进一步可以加一个命令字实现双向控制。接收端收到CMD0x01就回一帧确认发送端收到确认再发下一包这样就有了简单的应答机制。如果要做多点组网给每个发送端分配不同地址接收端用地址过滤只处理发给自己的帧。LoRa模块本身支持定点发射配置好目标地址后空中就只发给指定模块软件层可以省掉地址判断。我一般还会留一个调试口把接收到的每一帧原样通过另一个串口打印出来格式是“AA 55 04 01 1E 2D 00 00 7A”这样用串口助手一看就知道帧结构对不对。这个习惯帮我省了很多猜的时间。改造时注意RAM用量。51的idata只有128字节xdata可能也没有所以Frame结构体别开太大MAX_DATA_LEN控制在16以内比较稳。如果数据量大考虑换STC12或STC15系列它们有更多RAM和独立波特率发生器串口更稳。最后说一个我踩过的坑有次为了省事把LoRa模块的AUX引脚悬空了结果模块在发射时单片机还在往串口写数据导致数据被吞。后来我把AUX接回P1.0发送前先判断AUX是否为高高才发低就等再也没丢过包。从那以后我每次接串口模块都强制走一遍“供电-电平-参数-AUX”四步检查希望帮到你。本文还有配套的精品资源点击获取