
简介这是一套基于STM32F103C8、LoRa模组与RS485通信实现数据透传的嵌入式工程源码包面向物联网应用开发者和嵌入式学习者重点解决现场RS485设备数据远程无线回传的工程化需求。工程将LoRa远距离低功耗传输、STM32外设配置以及RS485多节点总线通信整合在一起实现了有线和无线链路的透明转发可直接用于远程传感器数据采集和设备控制场景。资源压缩包共234个文件大小5.46MB包含c/h源文件、Keil工程文件uvprojx/uvoptx、编译中间产物o/axf/hex以及map等工程结构完整可直接打开编译并烧录验证。目前已有344人学习下载。通过阅读源码和配置可掌握LoRa模组驱动、RS485收发控制、USART串口数据流转发等关键实现同时涉及的定时器、ADC、CAN等标准外设配置也有参考价值适合作为嵌入式通信项目的二次开发模板也便于理解工业现场数据远程联网的系统搭建思路。1. 一个串口总线和一个射频口STM32F103把两端拼接起来RS485总线在工业现场能拉1200米这个长度看着够用真到了过马路、跨厂房的场景穿管布线照样让人头疼。LoRa无线恰好补这个短板几公里内不用布线功耗还低。这个STM32F103工程干的事很直白USART1接RS485收发器USART3接LoRa模组收到哪边的数据就原样丢到另一边。所谓数据透传就是对Modbus RTU这类应用层协议完全不做解析帧是什么样远端收到的就是什么样。工程目录名叫“lander_wifi”实际上没有WiFi模块真正干活的是LoRa这种命名在真实工程里很常见不用纠结。适合正在做远程数据采集、分布式控制以及想搞明白RS485方向控制和串口中断怎么配合的人往下读。2. 硬件链路设计RS485方向控制、LoRa空口参数与引脚分配2.1 引脚分配为什么USART1给RS485USART3给LoRa外设分配要先于代码。RS485收发器占用一个方向控制脚LoRa模组通常也有M0/M1两个模式脚把这几个GPIO一起规划好后面写逻辑才不会乱。我常用的分配表如下功能引脚外设RS485收发PA9 / PA10USART1_TX / USART1_RXRS485方向控制 DE/REPB12GPIO输出高电平发送LoRa模组串口PB10 / PB11USART3_TX / USART3_RXLoRa模式选择 M0/M1PB13 / PB14GPIO输出组合决定配置/透传模式把USART1留给RS485是因为USART1挂在APB2总线上时钟来自72MHz波特率分频更精细USART3挂在APB1上36MHz时钟和USART2共用总线。两个半双工链路放进不同时钟域排查波特率问题时能少一个变量。若是项目里还要接第二路RS485用USART2的PA2/PA3即可代码结构完全一样。STM32F103最小系统本身没什么特殊8MHz晶振加两个20pF负载电容BOOT0接10k下拉到地复位脚RC复位电路用10k电阻加100nF电容每个VDD脚旁放一个100nF去耦电容。若用USB转TTL供电在VDD对地再加一个100uF电解电容避免LoRa发射瞬间压降导致复位。2.2 RS485收发方向切换与TC标志的坑RS485是半双工A/B差分线分时收发。方向控制脚一般接收发器的DE和RE合并端高电平让驱动器使能、接收器禁用低电平则反过来。很多开发者图省事用一个三极管把TX信号反相后直接驱动DE/RE做成所谓自动收发电路。这种电路在9600bps下能用但波特率超过19200后发送最后一位时DE已经被拉低总线上的末尾电平被截断对端收到的帧末尾会多一个0xFF或直接丢帧。批量产品不建议走这条路。用MCU控制方向最稳。发送前拉高DE最后一个字节完全移出移位寄存器后再拉低这个时序差经常出问题#define RS485_DIR_TX() GPIO_SetBits(GPIOB, GPIO_Pin_12) #define RS485_DIR_RX() GPIO_ResetBits(GPIOB, GPIO_Pin_12) void RS485_SendBytes(USART1_TypeDef *USARTx, uint8_t *buf, uint16_t len) { RS485_DIR_TX(); // DE1进入发送状态 for (uint16_t i 0; i len; i) { while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) RESET); USART_SendData(USARTx, buf[i]); } // 必须等TC不能只等TXE while (USART_GetFlagStatus(USARTx, USART_FLAG_TC) RESET); RS485_DIR_RX(); // 全部发完再切回接收 }TC与TXE的区别很关键TXE表示数据从软件写入移位寄存器完成但这一位还在线上跑TC才是移位寄存器彻底送完、总线空闲。只看TXE就切换方向等于把最后一个字节拦腰砍断。在9600bps下这个时间差约1msModbus主站超时一般给100ms以上所以不容易暴露一旦把波特率提到115200问题就频繁出现。总线侧建议加SMBJ6.5CA双向TVS、共模电感和120欧姆终端电阻AB两端各加390欧姆偏置电阻这套EMC电路是RS485接口的标准配置能显著减少现场变频器干扰。2.3 LoRa调制方式与空口参数怎么定LoRa用的是扩频调制同样一颗射频芯片上还有FSK模式。两者都能跑但LoRa把灵敏度做到了-137dBm级别比FSK高约20dB代价是空中速率低。混合模式确实存在同一个芯片按需切换LoRa和FSK但透传终端固定用LoRa就够了没必要在协议层引入切换逻辑。搜资料时注意区分射频领域的LoRa和AI微调里的LoRA是两回事。LoRa模组透传前我一般先用厂家上位机把频点、空速、地址配置好然后切到透传模式MCU侧完全无感。推荐参数如下参数推荐值说明工作频段470-510MHz 或 868/915MHz按当地无线电法规选择扩频因子 SF7-10SF7速率高SF10距离远信号带宽 BW125/250kHzBW越大速率越大灵敏度略降编码率 CR4/5默认值干扰大再调4/8串口波特率9600或115200与MCU串口参数一致即可配置时注意LoRa模组的M0/M1引脚组合决定工作模式配置模式下串口波特率固定透传模式下才是目标波特率。很多用户改完参数没断电重启或者M0/M1电平没稳定就发指令导致配置不生效这类故障多数不是代码问题而是操作时序问题。3. USART1则/USART3的差异与DMA空闲中断接收通路3.1 APB1/APB2时钟差异和重映射STM32F103的三路USART不在同一条总线上初始化时很容易踩坑串口挂载总线时钟引脚映射USART1APB272MHz默认PA9/PA10重映射到PB6/PB7USART2APB136MHzPA2/PA3USART3APB136MHzPB10/PB11重映射到PC10/PC11USART1的时钟来自APB2USART2/3来自APB1使能时钟用的是两套APIRCC_APB2PeriphClockCmd和RCC_APB1PeriphClockCmd。我把USART1接RS485、USART3接LoRa也是因为USART1和USART3的DMA通道没有重叠USART1_RX对应DMA1_Channel5USART3_RX对应DMA1_Channel3两个收发链路互不干扰。波特率计算公式为baud clock / (16 * USARTDIV)USART1在115200下USARTDIV约39.06USART3在9600下约234.375分频值的精度差异在高波特率下才明显。3.2 DMAIDLE接收变长帧LoRa模组输出数据是变长包如果逐字节进中断每个字节都打断主循环1200bps的空中速率还好115200波特率下约87us一个中断主循环基本被淹没。用DMA把串口数据搬到内存空闲中断只在一包数据结束后触发一次是标准做法#define UART3_RX_BUF_SIZE 512 volatile uint8_t uart3_rxbuf[UART3_RX_BUF_SIZE]; void USART3_DMA_Rx_Init(void) { DMA_InitTypeDef dma; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel3); dma.DMA_PeripheralBaseAddr (uint32_t)USART3-DR; dma.DMA_MemoryBaseAddr (uint32_t)uart3_rxbuf; dma.DMA_DIR DMA_DIR_PeripheralSRC; dma.DMA_BufferSize UART3_RX_BUF_SIZE; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; dma.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; dma.DMA_Mode DMA_Mode_Circular; // 循环模式DMA自动回绕 dma.DMA_Priority DMA_Priority_High; dma.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel3, dma); DMA_Cmd(DMA1_Channel3, ENABLE); USART_DMACmd(USART3, USART_DMAReq_Rx, ENABLE); USART_ITConfig(USART3, USART_IT_IDLE, ENABLE); }DMA配置里最关键的是DMA_Mode_Circular它让DMA搬完512字节后自动回到起始地址接着搬不用软件干预。缓冲大小必须是2的幂方便后续用掩码取余。使能DMA后要等它运行时才能发数据中途不能用DMA_SetCurrDataCounter改长度否则DMA会停在那等新计数。IDLE中断处理逻辑在USART3的IRQHandler里void USART3_IRQHandler(void) { if (USART_GetITStatus(USART3, USART_IT_IDLE) ! RESET) { // 先读SR再读DR硬件才会清IDLE标志 (void)USART3-SR; (void)USART3-DR; uint16_t len UART3_RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel3); if (len 0 len 256) { rb_write(lora_rxq, (uint8_t *)uart3_rxbuf, len); } // 清零计数器准备接收下一帧 DMA_Cmd(DMA1_Channel3, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel3, UART3_RX_BUF_SIZE); DMA_Cmd(DMA1_Channel3, ENABLE); } }IDLE标志的清除顺序很多人记错必须软件先读SR再读DR硬件才能复位IDLE。只读DR或者只读SR下一次空闲中断不会再触发表现就是LoRa模组那边发得热闹MCU这边一台一帧也收不到。DMA_GetCurrDataCounter返回的是DMA还没搬完的字节数用缓冲区总长减掉这个值才是本次空闲之前真正收到的数据长度。这个值可能小于一帧因为DMA是字节流搬运如果模组在包间停了很久才发下一包IDLE会拆出多个分段所以在写入环形缓冲区时不需要额外加帧头主循环按分段转发即可。3.3 包模式还是连续模式LoRa模组内部有FIFO配置成包模式发射时一包数据会连续从串口输出空闲中断一次就能收完整包如果模组被设置成连续模式或透明大包模式串口输出节奏受空中速率影响可能一包被拆成几段IDLE中断会触发多次。判断方法很简单在串口助手里看两包数据之间的间隔是否稳定稳定说明包模式正常抖动大就要回到模组配置里把包模式打开。这个参数直接影响上层帧完整性值得在联调前先确认。4. 双向透传实现环形缓冲区、半双工方向切换与主循环转发4.1 数据流向与RS485一主多从下的总线占用系统有两条独立数据通路RS485侧收到数据转发到LoRaLoRa侧收到数据转发到RS485。RS485总线挂多个从站时主站发起的读指令被LoRa网关送到远端远端从站的响应再原路返回整个过程中应用层协议完全不感知无线链路的存在这就是透传的价值。两组转发在同一颗MCU里跑必须保证同一时刻只有一个方向在操作USART1否则RS485方向脚会被两个逻辑反复拉高拉低总线冲突。4.2 环形缓冲区实现主循环和中断之间需要一个生产者-消费者队列。最轻的方案是环形缓冲区不需要动态内存读写都O(1)。这里用自然溢出配合掩码实现取模#define RING_SIZE 512 #define RING_MASK (RING_SIZE - 1) typedef struct { uint8_t buf[RING_SIZE]; volatile uint16_t head; // 写指针 volatile uint16_t tail; // 读指针 } ringbuf_t; // 生产者中断里把DMA收到的一包数据写入 uint16_t rb_write(ringbuf_t *q, const uint8_t *src, uint16_t len) { for (uint16_t i 0; i len; i) { q-buf[q-head RING_MASK] src[i]; // 掩码取余等价于 % RING_SIZE q-head; } return len; } // 消费者主循环读走全部或部分数据 uint16_t rb_read(ringbuf_t *q, uint8_t *dst, uint16_t maxlen) { uint16_t n (q-head - q-tail) RING_MASK; // 已有数据量 if (n maxlen) n maxlen; for (uint16_t i 0; i n; i) { dst[i] q-buf[q-tail RING_MASK]; q-tail; } return n; }head和tail都是uint16_t持续增长到65535会自然回绕成0因为缓冲区大小是512只要读写速度平均相等head减tail就是有效数据长度。 RING_MASK利用了512是2的9次幂这个性质比% RING_SIZE快得多。这个队列不需要加锁中断里只写head主循环里只读tail只要主循环消费速度不低于中断生产速度就不会出现读写同时改同一个变量的情况。如果LoRa一包超过512字节RB_WRITE会覆盖旧数据所以入中断前要做长度判断本项目中一帧控制在256字节以内。4.3 主循环转发逻辑两路转发用同一个主循环轮询代码很简短int main(void) { // 时钟、GPIO、USART1/3、DMA初始化略 uint8_t tmp[128]; while (1) { // 通路1RS485 - LoRa uint16_t n rb_read(rs485_rxq, tmp, sizeof(tmp)); if (n 0) { UART3_SendBytes(tmp, n); // 向LoRa模组发阻塞轮询 } // 通路2LoRa - RS485 n rb_read(lora_rxq, tmp, sizeof(tmp)); if (n 0) { RS485_SendBytes(USART1, tmp, n); } } }这里有个细节RF收发是串行的UART3_SendBytes虽然是往串口写但LoRa模组内部还要做变频、前导码、CRC一包数据从串口灌进去到真正发出去需要几十毫秒。如果RS485侧连续涌入几十帧LoRa链路来不及发缓冲区会持续积压。解决思路是压RS485侧的轮询频率主站轮询同一台LoRa网关下的从站群时间隔不要小于100ms尤其是一帧超过64字节时更要注意。空速只有几kbps不能让9600bps的RS485满速灌它。RS485发送函数前面已实现方向切换。还要补一个防溢出处理当LoRa_RS485方向在发送时USART1接收端的RXNE标志可能因为总线上的残余反射置位如果不清理下一条转发进来时会误读一个旧字节。所以RS485方向切回接收后立即读一次DR清掉ORE错误标志这个操作往往被忽略。5. 验收与排错回环测试、空中速率匹配和硬件级坑5.1 分层回环验证先把LoRa模组拆出来单独验证不接MCU。两块LoRa模组分别接USB转串口在PC上用串口助手互发数据确认射频链路本身是通的再做板级联调故障就能切开。RS485侧用USB转485接PC跑Modbus Poll模拟主站远端挂Modbus Slave模拟从站数据能通就说明透传链路完整。这个方法能覆盖九成问题射频不通调频点串口不通查波特率RS485不通查方向脚。5.2 空中速率与RS485吞吐估算LoRa名义速率可以用一个快速估算BW125、SF7、CR4/5时约5.47kbps扣掉前导码、CRC、FIFO操作开销用户实际吞吐约700字节每秒。RS485在9600bps下理论满速是960字节每秒已经超过LoRa的承载能力。所以两个硬指标要守RS485侧一帧别超过128字节主站轮询间隔不低于100ms。若确实需要连续大数据流把LoRa模组的SF降到7、BW提升到250kHz吞吐可以翻倍但接收灵敏度和距离都会下降。5.3 下载、启动和接线类坑现象常见原因处理DAP下载失败boot1跳线影响启动模式BOOT01且BOOT1悬空时可能进SRAM启动BOOT0加10k下拉BOOT1不用时也下拉到地RS485一发就报错终端电阻缺失或AB反接总线两端各接120欧姆A对A、B对BLoRa距离远低于标称天线没拧紧、天线区域有金属遮挡检查SMA头天线竖直摆放远离地平面串口3收乱码APB1总线上USART3波特率分频跑偏核对USARTDIV计算值用示波器测TX脚波形最后一类坑值得单独说STM32F103在DAP下载失败时别急着怀疑芯片坏了先检查BOOT0和BOOT1的组合是否被跳线帽意外短接。BOOT01、BOOT10从系统存储器启动BOOT01、BOOT11从SRAM启动只有BOOT00才从主闪存正常跑程序。很多开发板出厂把BOOT0引脚用一个跳线帽预留调试时误插到1上程序下得进去但就是不跑查半天往往是这个原因。排查顺序建议先用回环测试确认射频和串口各自正常再检查方向切换时序最后看空中速率是否被RS485灌爆逐层剥开问题通常不会同时出现在两个层面。本文还有配套的精品资源点击获取