Skip to content

前言 ​

广义的物联网概念最早于 1999 年由麻省理工学院提出其定义是"通过射频识别(RFID)、全球定位系统等信息传感设备,按约定的协议,把任何物品通过物联网域名相连接,进行信息交换和通信,以实现智能化识别、定位、跟踪、监控和管理的一种网络概念"。

物联网(IoT, Internet of Things)的本质是将各种嵌入式单片机相连,而这一概念将无线连接和各类智能传感器相结合并搭配低功耗的微控制器实现设备成本更低、方式更简单的联网。

而近些年,基于 MEMS(微机电系统)技术的传感器、MCU(微控制器)、 LPWAN(低功耗广域物联网)、云计算以及云储存等技术的快速发展,使物联网这一概念重新成为热门话题,而这一领域,也成为全球互联网巨头未来的重要战略布局。是未来最具有想象力的市场。

也正是基于物联网的前景和对高新技术的渴望,我投身到物联网的学习和开发当中,写下了物联网分支—— LoRa 的开发与应用这篇教程。在开始学习之前,希望读者有以下准备:

Win10 及其以上系统的电脑、MacOS 虚拟机(仅可编译文档中的代码);

一定的 C 语言编程基础、电路知识基础、如果有一定嵌入式基础更好;

一颗热爱学习的心。

本教程的初衷是希望读者能够最大程度从零基础入门学习 LoRa 的开发,但是从实际的学习中来看,我低估了学习LoRa的门槛。学科都是有相关性的,所以在开发 LoRa 的过程中不可避免会涉及到嵌入式底层、电路元器件的基本使用以及电脑的一些复杂操作等内容。但是我想我会尽最大可能用最浅显易懂的语言帮你们理解其中的原理,以便日后在你们走上开发这条道路能有所裨益。

以上的几点准备是本教程所要求的最低限制,我建议开发人员还是需要使用 Windows 电脑,MacOS 系统我也尝试过运行程序,但是配置虚拟机的阻碍也还是挺大的,并且最终是没有办法通过串口将所编写的程序实现烧录运行的操作,但对于理解和编程的过程问题不大。我自己使用的编程学习环境是基于最新的 Windows11 来完成本教程的编写,也希望这篇教程不会"老得这么快"。当然了,如果有条件的同学,我希望你们能够自己准备两块配有 LoRa 模块的开发板,一块作为主机,一块作为从机,这样你们就可以烧录程序到开发板上,观察真实的效果。开发板上,我希望能具备以下传感器模块:直流电机(风扇)、MPU6050 三轴传感器、DHT11 温湿度传感器。这样你就能完美实现第七部分的 LoRa 智慧牧场项目集成开发的项目了。

做好准备,我们就正式开始教程的内容。

LoRa 简介 ​

下一代生态网络 ​

过去 20 年的互联网是"人联网",未来 20 年的互联网是"物联网"。互联网的下半场是将整个物理世界数字化,零售、物流、制造、道路、汽车、森林、河流、厂房......甚至一个垃圾桶都会被抽象到数字世界,连到互联网上,实现"物""物"交流,"人""物"交互。

物联网是互联网的外延,互联网是通过电脑、移动终端等设备将人联网,所形成的一种全新的人与人的连接方式。而物联网是通过传感器、通信模组和智能芯片使物体联网。

人类每一次连接方式的改变,都会催生出一批巨无霸型的企业,在数十亿连接量的语音网时代,产生了 AT&T 这样的百亿美元规模的公司;在实现百亿连接的 PC /移动互联网时代,产生了如 IBM、微软、Google、BAT 这样的干亿美金级别企业;而物联网实现的千亿连接量一定会诞生出下一个巨无霸企业。

这印证了人类每一次连接方式的改变,都会诞生出一批伟大的公司,而伟大的公司会反过来更深刻地改变我们的生活,推动社会的进步。

图片

物联网为什么现在才开始爆发? ​

物联网概念于 1999 年被提出,但 2016 年却被称为物联网的元年,在这 17 年中,技术的进步是物联网从概念逐渐走向成熟的核心驱动力。

物联网的核心驱动力是通过多个维度的技术成熟而实现。

  • 过去 70% 的传感器及设备无法连接;

  • 2G/3G/4G、WIFI、ZigBee 只解决了视频、图像、语音的等方面应用;

  • 2G/3G/4G 功耗大、成本高、覆盖并不完善;

  • WIFI、 ZigBee、蓝牙距场短,很多应用场景无法满足;

  • 那些需要电池供电、广域覆盖、又需要长时闻低功耗待机的设备无法适应。

图片

LPWAN 技术 ​

在 LPWAN 技术出现以前,物联网网络难以平衡广覆盖和低功耗/低成本这对矛盾统一体。以目前物联网最常见的 GPRS (2G) 为例,技术存在功耗较高,每个扇区的连接量较低以及网络覆盖面积有限等缺陷,从而无法满足物联网在不同使用场景下的需求。

为了改进 GPRS 无法满足物联网连接需求的现状,业界提出了如 LoRa、SigFox 等工作于未授权频段的低功耗广域物联网(LPWAN)通信协议。与此同时,作为行业标准制定机构 3GPP 也提出了基于 LTE 发展而来的 eMTC 协议、基于 GSM 发展而来的 EC-GSM 协议和 NB-IoT协议。

相较于 3G/4G 蜂窝网络,LPWAN 具有成本低、能源消耗少、覆盖广等优势。

LPWAN:低功耗广域网络

优势:低带宽、低功耗、远距离、大量连接

LPWA 应用最广泛的三项技术:LoRa(美)、NB-IoT(华为参与)、sigfox(欧)

图片

LoRa 无线技术 ​

LoRa 作为低功耗广域网(LPWAN)中的一种无线技术,相对于其他无线技术(如 Sigfox、NB-IoT 等),LoRa 产业链较为成熟、商业化应用较早。基于 Sub-GHz 的频段使其更易以较低功耗远距离通信,可以使用电池供电或者其他能量收集的方式供电。

优势:

  • 164 dB 链路预算、距离 >15 km、快速;

  • 灵活的基础设施,易组网且投资成本较少;

  • LoRa节点模块仅用于通讯,电池寿命长达 10 年;

  • 免牌照的频段、网关/路由器建设和运营、节点/终端成本低。

图片

LoRa 开发环境搭建及驱动移植 ​

STM32CubeMX 安装及使用 ​

STM32CubeMX 简介 ​

市面上常见的 LoRa 模块都是搭配 STM32 芯片的单片机来使用的,所以使用 ST 公司的软件和产品是入门学习 LoRa 开发的必要一环。

图片

STM32 单片机是 ST(意法半导体)公司使用 ARM 公司的 Cortex-M3 为核心生产的 32 位系列的单片机。

STM32 字面含义:

  • ST ——意法半导体(一个公司名),即 SOC 厂商。

一个芯片比如 STM32 里面有内核(ARM),而内核 ARM 由 ARM 公司( IP 厂商)生产,外设由 ST 公司( SOC 厂商)生产,再此基础上添加各种外设,比如 GPIO, IIC 等。

  • M —— Microelectronics 的缩写即微控制器。

注意: 微处理器在微控制器的基础上有 MMU(内存管理单元),一般微控制器不跑系统,编写的程序为裸机例程。

  • 32 —— 32 bit,表示是一个 32 位微控制器。

单片机位数指 CPU 处理的数据的宽度,参与运算的寄存器的数据长度。32 位单片机的数据总线宽度为 32 位,通常可直接处理 8 位或 16 位或 32 位数据。

使用到 LoRa 的时候不可避免会用到 STM32 上面的串口、IIC、RTC 时钟等功能,所以我们会需要对这些外设进行初始化、配置等操作。传统的 STM32 配置和开发可以单独开一门课来讲了,难度比较大,所以我们需要借助 ST 公司的 STM32CubeMX 软件来简化配置的门槛。

STM32CubeMX 是意法半导体推出的图形化配置工具,通过傻瓜化的操作便能实现相关配置,最终能够生成 C 语言代码,支持多种工具链,比如 MDK、IAR For ARM、TrueStudio 等。尤其值得一提的是,TrueStudio 已经被 ST 收购,提供完全免费的版本,并且,通过插件式安装,可以将 STM32CubeMX 集成在一个 IDE,使用十分方便。

图片

直白点来说,我们可以直接通过图形化的方法,用 STM32CubeMX 这款软件完成一些外设的配置,它能够通过我们的选择,自动为我们生成配置外设所需的代码,大大提高了我们的效率。

STM32CubeMX 的优势:

  • 直观的选择 STM32 微控制器;

  • 微控制器图形化配置:

自动处理引脚冲突

动态设置确定的时钟树

可以动态确定参数设置的外围和中间件模式和初始化

功耗预测

  • C代码工程生成器覆盖了STM32微控制器初始化编译软件,如IAR、KEIL、GCC可以独立使用,作为Eclipse插件使用。

HAL 库与 STD 库 ​

库的概念大家应该不陌生,通常时候我们用到的一些传感器读取数据的函数,都是工程师们在库里面完成的底层代码编写。同样的,ST 的工程师们也为他们的 STM32 外设编写了不少外设库给我们调用,目前 ST 的库大致可以分为两大类:STD 库和 HAL 库。

  • STD 库\——标准外设库:寄存器操作,将一些基本的寄存器操作封装成函数;

  • HAL 库\——硬件抽象库:将这些象成了一个抽象层,从使用的角度来看,是与硬件无关的。

STD 库是早期 ST 推出的标准库,广泛应用在高校教育当中,库中对常见的一些寄存器功能进行了封装,成为了一个个独立的文件包。但是由于后来ST公司主推HAL库,STD 库已经缺少维护,并且文件需要自己去布局安排放置位置、使用门槛高,渐渐被HAL库所替代。

图片

HAL 库优势:

  • HAL 库是 ST 未来主推的库,从 2015 开始 ST 新出的芯片已经没有 STD 库;

  • HAL 库的处理机制比 STD 库好很多,HAL 库支持 STM32 全线产品;

  • HAL 库跨芯片的可移植性非常好。

STM32CubeMX 安装 ​

第一步:STM32CubeMX 依赖 Java 环境,JRE 官方下载地址:[https://www.oracle.com/java/technologies/downloads/#java8]{.underline}。以本文开发环境为例,选择 Windows x64,特别注意,需要选择 Accept License Agreement 才可以下载。

(百度网盘下载链接:[https://pan.baidu.com/s/1bYMQAemDrOwA688S70o8LA]{.underline} 提取码: 1p7o)

图片

图片

图片

第二步:在 ST 官网选择对应版本的 STM32CubeMX 进行下载,下载时会要求你填邮箱信息,之后下载的链接会通过邮箱发送给你。在安装 SetupSTM32CubeMX.exe 时,最好不要有中文路径。

(百度网盘下载链接:[https://pan.baidu.com/s/1r0RK5mBP3gJjjIHpMWB9Cw]{.underline} 提取码: a9fm)

图片

图片

图片

图片

图片

图片

图片

图片

图片

第三步:STM32CubeMX 固件包导入

固件包就是要选择我们 LoRa 开发板对应的 STM32 芯片类型,只有加载了正确的固件包,软件才能显示正确的芯片供我们配置。

首先需要检查固件包的路径,是否是中文路径,如果是中文的路径,修改路径。路径同样不能包含中文,具体修改固件包路径操作如下图所示:

图片

图片

导入固件包有两种方式:

  • 方法一:在有网络的情况下,直接在线下载

我们的教程使用的是 STM32F051K8 这款芯片,所以下载的固件包是 STM32F0。

图片

图片

图片

  • 方法二:导入固件包文件

图片

图片

STM32CubeMX 创建工程 ​

万事俱备之后,我们就开始创建我们的配置工程,步骤可以参考下面的图片。

图片

图片

图片

选择好芯片之后,就可以看到芯片的图片显示在屏幕的中心位置。这个时候我们只需要通过简单地点击 GPIO 引脚,就可以将它配置成我们所需要的模式。

像在这里,我们就可以设置PA8引脚为输出模式:

图片

完成工程的创建工作之后,我们还需要对工程进行一些常规配置:

图片

图片

完成配置之后,我们点击右上角的 "GENERATE CODE" 即可生成代码文件。

图片

IAR 安装及使用 ​

IAR 介绍 ​

上一节内容带领大家尝试使用了一下 STM32CubeMX 来生成一个配置 PA8 引脚为输出模式的项目,但是这个项目我们应该使用什么软件来打开方便我们之后的开发步骤呢?在本篇教程中,我们选择的是 IAR 这款软件。

IAR Systems 是全球领先的嵌入式系统开发工具和服务的供应商。公司成立于 1983 年,提供的产品和服务涉及到嵌入式系统的设计、开发和测试的每一个阶段,包括:带有 C/C++ 编译器和调试器的集成开发环境(IDE)、实时操作系统和中间件、开发套件、硬件仿真器以及状态机建模工具。

图片

IAR 与 Keil

IAR 即 IAR Embedded Workbench,Keil 即 Keil MDK-ARM,这两款都是 ARM 开发工具。那么,IAR 与 Keil 两款开发工具区别在于哪里呢?

Keil 可以自动配置启动代码,集成 Flash 烧写模块,而且支持性能分析功能;

而 IAR 是一套用于编译和调试嵌入式系统应用程序开发工具,支持汇编、C 和 C++ 语言,为伙伴们提供了完整的集成开发环境,而且还包括管理器、编辑器等。

Keil 默认只创建工程,工作区是不会直接创建的。如果想多个工程聚合,则首先需要创建一个工作区,然后再添加相应的工程,相对比较繁琐。

IAR 默认是创建工程和工作区,如果想多个工程并存,直接添加即可。

Keil 编译时,只有 level 的选择;IAR 有 debug 和 Release 的快速选择。

默认状态,Keil 的工具栏功能比较多,有点繁杂;IAM 的比较简洁。但相对,也比较单薄。Keil 的程序文件,最后必须要有一个新的空行,否则会有编译警告。

IAR 安装 ​

第一步:从官网下载IAR软件([https://www.iar.com/products/architectures/arm/iar-embedded-workbench-for-arm/iar-embedded-workbench-for-arm---free-trial-version/]{.underline})

(百度网盘下载链接:[https://pan.baidu.com/s/1QKWNsdyseW59BIegrmDNlA]{.underline} 提取码: 0no0 )

图片

第二步:参考以下图片步骤完成软件的安装。

图片

图片

图片

图片

图片

图片

图片

附:破解方法参考([https://www.jb51.net/softs/767867.html]{.underline})

(破解工具百度网盘下载链接:[https://pan.baidu.com/s/1Sy-4_eGmwDOjgcSOA0Ra8g]{.underline} 提取码: 81e1 )

图片

IAR 使用 ​

我们先来记忆一些 IAR 的常用快捷键:

  • CTRL + B 括号匹配选择括号内的多行代码;

  • CTRL + T 自动缩进,格式化选中代码;

  • CTRL + K 快注释,屏蔽选中的代码;

  • CTRL + SHFT + K 取消快注释,取消屏蔽选中的代码。

这里我们使用在前面 STM32CubeMX 生成好的项目举例说明 IAR 软件的使用,打开 STM32CubeMX 生成好的工作区,首先启动 IAR 软件。

图片

生成的工程文件位置在 EWARM 文件夹下,后缀为 .eww 的文件。

图片

图片

打开工作区之后我们可以看一下工作区的目录以及文件。

图片

STM32CubeMX 生成的工作区文件夹分类相当清晰。目录树下分成了两个文件夹 Application 和 Drivers,将所有的库文件分成了应用和外设两个部分。对于 Application 文件夹的内容,一方面是启动芯片的配置库(在 EWARP 文件夹中),另一些就是用户的配置文件(在 Core 文件夹中)。用户配置的文件在图片当中就有 gpio.c(引脚配置)、main.c(主函数)、stm32f0xx_it.c(中断配置)等。

STM32F0xx_HAL_Driver 文件夹里面就是基于 HAL 库的外设配置代码,可以通过名称清晰地看到有 IIC、DMA、RCC 等的配置,当然了,由于我们刚才并未在 STM32CubeMX 中开启这些功能,所以部分内容可能是空白的。

在习惯上,我们打开工作区之后的第一步应该右击项目的名称,选择 "Make" 对工程进行编译,检查是否有错误。

图片

只有编译没有问题,我们才能继续之后的开发步骤。

图片

M0 工程建立 ​

到目前为止,我们已经完成了 STM32CubeMX 和 IAR 软件的一些了解学习,接下来我们就结合 LoRa 开发板来创建一个 M0 工程。M0 工程是所有硬件开发的第一步,是根据我们所需的硬件内容和需求做出来的第一个工程文件。以后的配置工作甚至是其他 LoRa 板型都可以在 M0 工程的基础之上完成,是名副其实的"万能模板"。

在这部分的内容当中,希望大家能够梳理清楚整个LoRa驱动配置的步骤和过程,明白其中的原理;同时学习到 STM32CubeMX 和 IAR 软件使用方法。

IO 端口配置 ​

从前面的学习中不难发现 STM32CubeMX 这个软件就是为了 STM32 芯片各个功能引脚配置和各种外设的开启功能配置的,所以在使用软件之前,我们得完成 STM32 的引脚配置,绘制出原理图。一般来讲这个工作是由硬件工程师来完成的,所以具体过程我们可以不用深究,拿到原理图我们就可以根据原理图来配置引脚的模式和功能了。

这里是我的 LoRa 开发板提供的芯片原理图。

图片

然后根据上面的原理图,建立 IO 功能映射表如下。

序号IO编号IO信号网络功能描述
0VSSGND电源负
1VDD3.3V电源正
2PF0-OSC-INOSC_IN外部8M晶振
3PF1-OSC-OUTOSC_OUT
4NRSTNRST外部复位
5VDDA3.3V电源正
6PA0BAT_ADC模拟量输入-电池电压
7PA1ADC_KEY模拟量输入-按键信息
8PA2DIO3数字量输入-LoRa数字IO3
9PA3DIO2数字量输入-LoRa数字IO2
10PA4A1数字量输入-传感器扩展接口1
11PA5ID_1NC
12PA6A2模拟量输入-传感器扩展接口2
13PA7NSS_LoRaLoRa模块片选接口
14PB0LED4数字输出-无线通信网络指示灯
15PB1LED3数字输出-无线通信发送指示灯
16PB2LED2数字输出-无线通信接收指示灯
17VDD3.3V电源正
18PA8D3&KEY数字量输入3-按键状态
19PA9U1 USART1_TX串行通信接口
20PA10U1 USART1_RX
21PA11DIO0数字量输入-LoRa数字IO0
22PA12DIO1数字量输入-LoRa数字IO1
23PA13SWDIOSWD调试烧写接口
24PA14SWCLK
25PA15NSS_LCDLCD片选接口
26PB3SCLKSPI时钟接口
27PB4MISOSPI主机输入从机输出接口
28PB5MOSISPI主机输出从机输入接口
29PB6LCDLCD背光控制接口

根据上面的映射表,我们通过 STM32CubeMX 配置每个 IO 的工作模式。配置的时候要留意晶振引脚 PIN2、PIN3 还有 SPI、USART、ADC、和系统时钟,它们需要单独配置。其他引脚根据实际的输入输出配置为常见的 GPIO_Output 或者 GPIO_Input 即可。

图片

时钟配置 ​

时钟系统是 CPU 的脉搏,就像人的心跳一样。所以时钟系统的重要性就不言而喻了。STM32有多个时钟来源的选择,采用一个系统时钟不是很简单吗?为什么 STM32 要有多个时钟源呢? 因为首先 STM32 本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率,比如看门狗以及 RTC 只需要几十 k 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁干扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。

对于 STM32F0 型号的芯片,我们固定选择时钟源,配置倍频,使能锁相环配置系统主时钟为48M。

图片

外设配置 ​

  • USART1 配置

USART 是一个全双工通用同步/异步串行收发模块,该接口是一个高度灵活的串行通信设备。配置开启 USART 是为了方便我们进行 LoRa 的调试和通信。我们在学习使用 Arduino 的时候,因为 Arduino IDE 没有逐步调试功能,所以我们常用串口输出来检查某些代码编写的功能是否如我们所期望的一样,在这里也是同理。

配置 USART 的基本原则:

→选择异步通信

→无硬件流控

→设置波特率 115200

→设置 DMA 接收

异步通信是一种很常用的通信方式。相对于同步通信,异步通信在发送字符时,所发送的字符之间的时隙可以是任意的,当然,接收端必须时刻做好接收的准备。发送端可以在任意时刻开始发送字符,因此必须在每一个字符的开始和结束的地方加上标志,即加上开始位和停止位,以便使接收端能够正确地将每一个字符接收下来。内部处理器在完成了相应的操作后,通过一个回调的机制,以便通知发送端发送的字符已经得到了回复。

DMA,全称为:Direct Memory Access,即直接存储器访问。DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路,能使 CPU 的效率大为提高。

图片

图片

图片

  • SPI 配置

配置 SPI 是为了方便我们使用 LoRa 和 LCD 等模块。SPI 是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为 PCB 的布局上节省空间,提供方便。在 LoRa 的开发当中,我们会用到各种传感器模块和 LCD 显示屏,这些可能都需要使用 SPI 进行通信,所以我们也配置开启 SPI 功能。

SPI 的配置原则:

→选择全双工主机模式

→硬件片选不使能

→设置波特率为 1.5MBit/s

→时钟极性为低电平驱动

→时钟相位为第一个边沿

图片

我们继续尚未完成的 STM32CubeMX 工程,完成配置之后点击生成代码。

图片

然后使用IAR打开工程,点击"make"编译,检查是否报错。

图片

printf() 函数重定向 ​

我们在学习 C 语言的第一课,就学会了使用 printf("hello world!") 来打印括号中的内容,printf() 函数的使用我们已经非常熟悉,没有想过它是怎么实现的甚至是怎么来的。

其实 printf() 函数是一个经过封装之后的函数,也就意味着它的底层还有一些汇编语言编写的代码段来帮助我们完成打印输出这一简单的功能。其实在C语言当中要想实现 printf() 函数的功能也并不是直接就能拿来用的,还需要在代码的最前面加上这么一句 #include <stdio.h>,这其实就是包含 printf() 函数的库。

所以在我们的 LoRa 代码开发中,如果想要使用 printf() 函数,并不能直接使用 printf() ,否则程序会报错,我们需要做一步操作叫"重定向"。

将重定向的代码复制到 main.c 最后面即可。

c
int fputc(int ch, FILE *f){
while((USART1->ISR&0X40) == 0);
USART1->TDR =(uint8_t)ch;
return ch;
}
```c

![图片](/images/lora-doc/image58.png)

![图片](/images/lora-doc/image59.png)

可以在初始化语句之后我们添加一段printf语句,结合开发板硬件,检查通信是否正常。

![图片](/images/lora-doc/image60.png)

![图片](/images/lora-doc/image61.png)

<img src="/images/lora-doc/image62.png" alt="图片" data-width="80%" style="width: 80%" data-align="center" />

到这里就已经完成了 M0 工程的生成,之后的项目开发都会基于此 M0 工程的内容,为了保证 M0 工程的完整性和正确性,大家可以使用 M0 工程的副本来完成接下来的开发步骤。

## LoRa 驱动源码分析

### 驱动源码文件说明

(LoRa 官方驱动固件库百度网盘链接: [\[https://pan.baidu.com/s/1Yf3wsaZsa8BWqXTGf-9L3g\]{.underline}](https://pan.baidu.com/s/1Yf3wsaZsa8BWqXTGf-9L3g) 提取码: 92ed)

拿到官方提供的 LoRa 驱动固件库,我们先来仔细研究一下驱动固件库文件夹的目录和作用,以便日后我们对驱动代码的修改有一个初步的了解。

![图片](/images/lora-doc/image63.png)

文件夹当中有四个子文件夹,分别是 doc、lst、obj、src。**doc 文件夹**存放的是关于此 LoRa 驱动库的官方说明;**lst 和 obj 文件夹**是程序编译之后自动生成的一些编译文件;**src 文件夹**则存放了 LoRa 驱动的所有源码。

![图片](/images/lora-doc/image64.png)

综上,我们的重点是要研究 src 文件夹的组成。src 文件夹当中又有两个子文件夹,分别是 **platform** 和 **radio**,还有一个 main.c 文件。

<strong>platform文件夹:</strong>存放的是 LoRa 硬件平台的驱动源码,由于其他平台的开发我们用不到,这里只需要关心子文件夹 **sx12xxEiger** 即可。

![图片](/images/lora-doc/image65.png)

**radio文件夹**:操作 LoRa 无线驱动的源码。这里只需要关心 SX1276 这款芯片即可( SX1276 芯片在美国使用,中国用 SX1278 芯片较多)。

![图片](/images/lora-doc/image66.png)

**main.c**:是主程序文件,这里的主程序是 LoRa 官方提供的示例代码,我们可以拿来参考学习,但是我们开发很少会直接拿来使用。

简单了解了一下 LoRa 驱动固件的目录树之后,我们再来看看下面的这个表格,看看驱动移植当中我们需要处理的是哪些文件,先有一个初步的印象。

![图片](/images/lora-doc/image67.png)

### 硬件抽象层分析

从表面看,终端驱动就是 MCU 通过读写 SX1278 的寄存器,实现射频收发功能。然而,一个优秀的驱动设计,至少满足以下设计目标。最具挑战的是,有些目标是相互抵触的。

提供机制:区分策略和机制,驱动仅提供机制,由用户进程实现策略;

接口简单:接口越简单,驱动越好使用,另外,更好实现"高内聚、低耦合";

提高效率:最大化硬件设备性能,是驱动的重要使命;

节能内存:内存复用和指针传递等方法可以节省 MCU 宝贵的内存;

易于移植:能在不同的 MCU 之间低成本移植,该驱动就越优异;

稳定可靠:驱动是硬件和系统软件的黏合层,不能有任何差错。

![图片](/images/lora-doc/image68.png)

**搭建的LoRa终端的总体思路与步骤:**

1\. 搭建的LoRa终端的系统需求和目标

2\. 设计LoRa终端的目标系统

3\. 设计LoRa终端的主机开发环境

4\. 设计LoRa终端的软件架构

5\. 构建LoRa终端的实际硬件开发平台

6\. 构建LoRa终端的实际软件开发平台

7\. 迭代式实现LoRa终端的软件功能

**硬件接口设计**

MCU 与 LoRa 硬件接口如图所示。MCU 通过 SPI 总线与 LoRa 进行通信,包括设置参数和读写 FIFO;当 LoRa 有异步事件发生时,它通过 6 根连接线 DIO0 ~ DIO5 中断 MCU;MCU 为判断接收和发送数据包是否超时需要设置 TIMER ,该资源 LoRa 不需要,仅被 MCU 所用。

![图片](/images/lora-doc/image69.png)

**硬件接口函数**

DIO 除初始化、函数化,还需要 DIO0 ~ DIO5 对应中断服务函数;SPI 需要初始化和输入、输出函数。

![图片](/images/lora-doc/image70.jpeg)

![图片](/images/lora-doc/image70.jpeg)

上述的函数都在 Hal.c 文件里面有他们的定义,不需要我们来重新写一遍这些功能,我们所需要做的,就是根据我们所选的 STM32 进行匹配,这也是接下来驱动移植所需要完成的主要任务。

![图片](/images/lora-doc/image71.png)

也就是说,在接下来的硬件驱动移植的时候我们就只需要关心 SPI 和 IO 的配置即可。

## LoRa驱动移植

由上面的描述可知,LoRa 官方已经给我们提供了所需要外设的函数定义和全部驱动固件,但是我们不能要求这套固件拿到手之后对所有的 MCU 都适用,LoRa 提供的模板我们还是需要根据选择的芯片型号进行适配之后才能够进行使用的,这就是驱动移植当中我们需要完成的任务——将 LoRa 驱动固件与 M0 工程融合,使 LoRa 能够在所选择的芯片上编译运行。

### 驱动文件移植

首先第一步,我们要将所需的文件从官方提供的固件库中复制出来,放在我们的 M0 文件夹中进行开发,我们尽量不要对官方固件进行修改。所需的文件以及放置位置可以参考下图。

![图片](/images/lora-doc/image72.jpeg)

实操如下:在 M0 副本里面新建一个文件夹,命名为 "sx1278"。

![图片](/images/lora-doc/image73.png)

然后我们找到官方固件库,将 platform 和 radio 文件夹全部复制到 sx1278 文件夹当中。

![图片](/images/lora-doc/image74.png)

### IAR 工程文件添加

所需的驱动文件已经复制到我们的项目文件当中了,但是并不代表项目中也加载了我们所需的代码。所以我们还需要在 IAR 软件当中也添加驱动文件,添加之后的目录树如下图所示。

<img src="/images/lora-doc/image75.png" alt="图片" data-width="80%" style="width: 80%" data-align="center" />

实操如下:打开 EWARM 文件夹下面的"Project.eww"文件。

![图片](/images/lora-doc/image76.png)

然后我们右键点击项目名称,添加项目组名称为"platform"、"radio"。

<img src="/images/lora-doc/image77.png" alt="图片" data-width="80%" style="width: 80%" data-align="center" />

分别为这两个项目组添加我们需要的源码文件:

<img src="/images/lora-doc/image78.png" alt="图片" data-width="80%" style="width: 80%" data-align="center" />

![图片](/images/lora-doc/image79.png)

![图片](/images/lora-doc/image80.png)

值得注意的是,这里添加的所有文件都需要从新拷贝过来的 platform 和 radio 文件夹里面去找,不要去官方固件库里面找,因为我们的原则是不破坏源代码,只对复制代码进行修改。

添加完成之后如下图所示:

![图片](/images/lora-doc/image81.png)

### 驱动源码修改

LoRa 官方固件库不支持 Hal 库;SPI 和 IO 口跟自己开发的接口不一样,不修改是无法正常运行的。

**第一步:修改硬件平台**

为了确保程序能够成功烧录匹配 SX1278 芯片,我们需要将 SX1278 硬件平台添加到 IAR 软件中。关于硬件平台的定义在 platform.h 文件里。

打开 platform.h 文件

![图片](/images/lora-doc/image82.png)

![图片](/images/lora-doc/image83.png)

复制下图所示第 40 行的部分代码。

![图片](/images/lora-doc/image84.png)

然后右击工程名称,选择 "**Options**",在图片指定位置添加宏定义 "PLATFORM=SX12xxEiger" 即完成硬件平台的添加。

![图片](/images/lora-doc/image85.png)

![图片](/images/lora-doc/image86.png)

**第二步:修改包含路径**

要想实现编译的时候软件能够自动检索 "#include" 包含的头文件和源文件,我们必须要在 IAR 设置当中将这些文件路径添加上,不然会导致软件找不到包含的文件而报错。

![图片](/images/lora-doc/image87.png)

**第三步:修改 sx12xxEiger.h 和 fifo.h**

在开头我们已经说明,官方固件不支持 Hal 库,对于 IO 引脚也不适配,所以接下来就是比较繁琐和复杂的匹配过程。

我们打开 sx12xxEiger.h 文件,先修改让它支持我们的 STM32。

![图片](/images/lora-doc/image88.png)

对代码的第 29 行开始进行修改,添加一个分支,内容为我们所选的芯片前缀型号。

![图片](/images/lora-doc/image89.png)

修改后:

```c
#elif defined( STM32F1XX )
  #include "stm32f10x.h"
#else
  #include "stm32f0xx.h"
```c

![图片](/images/lora-doc/image90.png)

除了这个文件有不支持的情况可能发生以外,可能还会有其他文件也存在同样的问题,那么我们现在可以编译文件查看错误,根据错误我们选择对应的文件进行相同的修改。

![图片](/images/lora-doc/image91.png)

找到这类错误然后,双击这个错误即可打开需要修改的代码文件。我们复制修改代码进行粘贴即可。

![图片](/images/lora-doc/image92.png)

修改后再次编译,确保编译错误中不再有 "cannot open source file 'stm32f10x.h'" 这个错误发生。

**第四步:修改 sx12xxEiger.c**

编译之后可以看见 sx12xxEiger.c 文件也有报错的情况发生,这里的错误因为缺少一些文件,而报错缺少的这些文件在我们的开发中并不需要,所以直接把代码 27 ~ 42 行注释(选中然后ctrl+K)即可。

![图片](/images/lora-doc/image93.png)

47 ~ 59 行同样也注释。

![图片](/images/lora-doc/image94.png)

在这里,所有关于初始化(Init)的函数也都需要注释,因为我们在使用 STM32CubeMX 生成项目的时候就已经使用 Hal 库完成初始化,不再需要 LoRa 官方的初始化步骤。

**\[(但要注意不要把函数名和大括号注释了,避免报错函数名字得留着!)\]{.mark}**

![图片](/images/lora-doc/image95.png)

**第五步:修改 led.h 和 led.c**

打开led.h文件进行修改。

![图片](/images/lora-doc/image96.png)

根据上面的"IO 功能映射表",确定 LED 的引脚号。可以看到 LED2 ~ LED4 对应的是 PB2 ~ PB0。

![图片](/images/lora-doc/image97.png)

然后我们不需要去关心 STM32F4 等其他的平台的源码,我们直接找到"#else"的部分(表示我们自己使用的平台 STM32F0 )在 68 行附近:

![图片](/images/lora-doc/image98.png)

将上图的引脚号改成我们自己指示灯的引脚标号(需要改成大写的原因是 Hal 库对于引脚的定义就是大写的)。

同时,在 Hal 库里面是不支持对时钟的定义的,所以我们还需要将引脚后面对时钟的定义全部注释。

![图片](/images/lora-doc/image99.png)

第一个灯 LED1_PIN 因为我们用不到,但是删掉可能会导致报错,所以这里我们随便定义一个引脚。别忘了注释时钟的定义。

![图片](/images/lora-doc/image100.png)

然后继续将剩余的一些与 Hal 库冲突的地方全部进行修改。

![图片](/images/lora-doc/image101.png)

然后我们切换到 led.c 文件,把 Hal 库不支持的时钟和初始化的相关内容进行修改。

![图片](/images/lora-doc/image102.png)

![图片](/images/lora-doc/image103.png)

最后把灯开关的操作改成使用 Hal 库里面的函数来完成。

![图片](/images/lora-doc/image104.png)

单击鼠标右键对 led.c 文件进行编译,检查是否无错。

![图片](/images/lora-doc/image105.png)

![图片](/images/lora-doc/image106.png)

补充一点:关于 Hal 库函数的介绍和使用可以参考 Hal 库的用户手册,下载地址在官网,选择你的芯片型号进行下载 [\[https://www.st.com/en/embedded-software/stm32cubef0.html#documentation\]{.underline}](https://www.st.com/en/embedded-software/stm32cubef0.html#documentation))

![图片](/images/lora-doc/image107.png)

**第六步:修改 sx1276-Hal.c(工作量最大)**

打开 sx1276-Hal.c,首先注释 35 ~ 44 行。

![图片](/images/lora-doc/image108.png)

片选部分我们在 else 当中修改为引脚 PA7 。(根据 IO 功能映射表)

![图片](/images/lora-doc/image109.png)

![图片](/images/lora-doc/image110.png)

修改 DIO 部分的引脚标号。

![图片](/images/lora-doc/image109.png)

![图片](/images/lora-doc/image111.png)

同时把其他芯片的内容注释。

![图片](/images/lora-doc/image112.png)

![图片](/images/lora-doc/image113.png)

![图片](/images/lora-doc/image114.png)

将所有的 GPIO_WriteBit() 改成 Hal() 库的写法: HAL_GPIO_WritePin()。

![图片](/images/lora-doc/image115.png)

将所有的 Bit_RESET() 和 Bit_SET() 全部改成 Hal 库的写法: GPIO_PIN_RESET() 和 GPIO_PIN_SET()。

![图片](/images/lora-doc/image116.png)

将所有的 GPIO_ReadInputDataBit() 改为 Hal 库的写法: HAL_GPIO_ReadPin()。

![图片](/images/lora-doc/image117.png)

由于 DIO3、DIO4、DIO5 我们不使用,所以在它们配置函数的内部直接设置为 return 1,避免报错。

![图片](/images/lora-doc/image118.png)

我们不需要通过硬件去进行发送和接收,所以最后的函数内容也注释。

![图片](/images/lora-doc/image119.png)

最后对 sx1276-Hal.c 文件进行编译,确保编译无错。

![图片](/images/lora-doc/image120.png)

**第七步:修改 sx1276-Hal.h**

打开 sx1276-Hal.h 文件,把 39 行的 TickCounter 改成 Hal 库的写法:HAL_GetTick()。

![图片](/images/lora-doc/image121.png)

**第八步:修改 spi.c**

打开 spi.c 文件,在用户配置代码的地方配置 SPI 的使用。

```c
uint8_t SpiInOut( uint8_t outData ){
uint8_t pData = 0;
if(HAL_SPI_TransmitReceive( &hspi1,&outData,&pData,1,0xffff) != HAL_OK)
  return ERROR;
else
  return pData;
}
```text

![图片](/images/lora-doc/image122.png)

**注意:**

HAL_SPI_TransmitReceive() 函数的第一个参数要参见 spi.c 开头 STM32CubeMX 默认分配的结构体指针;

![图片](/images/lora-doc/image123.png)

第二个参数是发送的数据指针,用我们定义的局部变量 outData 即可;

第三个参数是接收的数据指针,我们在函数内部定义的 pData 即作为接收数据指针;

第四、第五个参数是数据的长度和超时时间。

整个函数的设计逻辑就是,使用 HAL_SPI_TransmitReceive() 函数来查看返回值是否为定义好的 HAL_OK,如果是则代表 SPI 开启正常,则返回接收的数据,否则返回错误信息。

其他更多关于 HAL_SPI_TransmitReceive() 函数的使用可以参考 Hal 库的用户手册。

完成以上所有的修改之后,对整个项目进行编译,检查是否无错,到此,整个 LoRa 驱动移植步骤就顺利完成。

![图片](/images/lora-doc/image124.png)

### LoRa 模块上电自检

为了检查上面驱动移植的代码没有问题,能够成功驱动 LoRa 运行,我们需要给开发板上电,并且设计一个程序能够通过 SPI 总线来读取芯片版本,判断读取值是否为 0x12,并打印出模块版本号。

![图片](/images/lora-doc/image125.png)

首先我们来查找看一下通过 SPI 总线来读取芯片版本号这个功能是否有实现:

![图片](/images/lora-doc/image126.png)

搜索 "**RegVersion**":

![图片](/images/lora-doc/image127.png)

果然是能找到的,我们双击找到的位置,直接复制"SX1276Read( REG_LR_VERSION, &SX1276LR->RegVersion );",然后打开"main.c"文件,粘贴在初始化之后。

![图片](/images/lora-doc/image128.png)

当然了,别忘了添加我们所需要的头文件。

![图片](/images/lora-doc/image129.png)

新建一个变量 RegVersion,并且用它替换掉原来 SX1276Read() 函数的第二个参数——版本号。

![图片](/images/lora-doc/image130.png)

然后用 C 语言编写判断语句,判断芯片版本是否为 0x12。

![图片](/images/lora-doc/image131.png)

编译检查无错后,连接LoRa开发板,上电检查运行结果:

![图片](/images/lora-doc/image132.png)

通过串口助手可以看到,最终打印出来的结果是"LoRa read Ok!",表示这部分驱动移植和芯片版本读取的代码已经顺利实现其应有的功能效果。

# LoRa 人机界面开发

使用串口助手查看 LoRa 通信的内容始终不是长久之计,因为我们不可能将 LoRa 一直连着电脑来看数据信息。因此,合理利用 LoRa 开发板上面的显示屏进行数据的显示会让整个产品的用户体验变得更好。

## TFT 液晶屏技术原理

TFT 即薄膜场效应晶体管,它可以"主动地"对屏幕上的各个独立的像素进行控制,这样可以大大提高反应时间。一般 TFT 的反应时间比较快,约 80 毫秒,而且可视角度大,一般可达到 130 度左右,主要运用在高端产品。

![图片](/images/lora-doc/image133.png)

从而可以做到高速度、高亮度、高对比度显示屏幕信息。 TFT 属于有源矩阵液晶显示器,在技术上采用了"主动式矩阵"的方式来驱动,方法是利用薄膜技术所作成的电晶体电极,利用扫描的方法"主动拉"控制任意一个显示点的开与关,光源照射时先通过下偏光板向上透出,借助液晶分子传导光线,通过遮光和透光来达到显示的目的。

![图片](/images/lora-doc/image134.png)

![图片](/images/lora-doc/image135.png)

颜色深度:

Color 介绍 :

    ① R,G,B 三基色组合形成各种颜色。

    ② 能显示的颜色数由 RGB 的数字信号的位数来决定。(8bit 数字信号刚好能显示 16.7M 种颜色)

![图片](/images/lora-doc/image136.png)

以 3bit 为例数字信号为例:

![图片](/images/lora-doc/image137.png)

For 3 bit : 23(R) \* 23(G) \* 23(B) = 256 colors

For 6 bit : 26(R) \* 26(G) \* 26(B) = 262144 colors(242K)

For 8  bit: 28(R) \* 28(G) \* 28(B) = 16777216 colors(16.7M)

For 10  bit: 210(R) \* 210(G) \* 210(B) = 1073741824 colors(1 billion)

1.44寸TFT模块:

![图片](/images/lora-doc/image138.png)

TFT 驱动控制器:

![图片](/images/lora-doc/image139.png)

1.44 寸模块电路详解:

![图片](/images/lora-doc/image140.png)

## TFT 液晶屏驱动开发

### TFT 液晶屏硬件接口驱动开发

SPI、GPIO 初始化 STM32CubeMX 已经帮助我们完成代码的设计了,最后我们需要手动完成数据模式的初始化。根据硬件设计,LoRa 与 LCD 共用 SPI 总线,且 LCD_MISO 用于命令/数据模式切换控制。同时需要修改 GPIO 初始化源码,让片选接口拉高。(片选脚拉低可以进行写指令,片选脚拉高使能 LoRa 和 LCD)

![](/images/MzekPgCOxbv4.png)

驱动源码移植概览:

<img src="/images/lora-doc/image147.png" alt="图片" data-width="70%" style="width: 70%" data-align="center" />

**实践:上电后液晶屏显示黄色屏幕**

我们继续使用 LoRa 移植完成的项目工程文件,先按照上图将我们需要的代码文件放在对应的文件夹当中:

![图片](/images/lora-doc/image148.png)

![图片](/images/lora-doc/image149.png)

![图片](/images/lora-doc/image150.png)

(我们不难总结出来 Src 文件夹里面其实存放的都是 .c 源文件,Inc 里面存放的则是 .h 头文件,而在我们的 IAR 项目工程当中,导入的必须都是 .c 源文件)

然后打开 IAR 工程,把我们刚刚加的 lcd.c 文件添加进工程当中。

![图片](/images/lora-doc/image151.png)

编译完成之后就可以看到 lcd.c 文件会包含所有的头文件(.h文件)。

![图片](/images/lora-doc/image152.png)

编译之后可以看到有一处警告,双击进入警告的代码行,发现是HAL库没有定义的问题,我们对这个警告进行修正:

![图片](/images/lora-doc/image153.png)

把数字"1"改成 "GPIO_PIN_SET" 即可,重新编译则无错误无警告。

![图片](/images/lora-doc/image154.png)

然后我们需要将 gpio.c 文件当中初始化片选引脚设置为拉高,使能 LCD 和 LoRa,即把 GPIO_PIN_RESET 改为 GPIO_PIN_SET。

![图片](/images/lora-doc/image155.png)

接下来我们来实现显示屏的清屏操作,在 main.c 文件当中先添加 lcd 的头文件,然后调用 lcd 的初始化函数以及清屏的函数,把屏幕设置为黄色。

![图片](/images/lora-doc/image156.png)

![图片](/images/lora-doc/image157.png)

然后我们就可以看到运行的结果如下:

<img src="/images/lora-doc/image158.png" alt="图片" data-width="61%" style="width: 61%" data-align="center" />

### TFT 液晶屏取模方式

TFT显示屏取模方式有四种,分别是:列扫描、行扫描、列行扫描、行列扫描(后面两种会在到达屏幕中间部分的时候切换扫描方式)

![图片](/images/lora-doc/image159.png)

我们不可能手写完成所有图片或者文字的显示代码编写,所以我们会用到能够生成取模代码的软件。用到的取模软件有:文字取模和图片取模。

![图片](/images/lora-doc/image160.png)

![图片](/images/lora-doc/image161.png)

软件生成的内容以 logo.h 保存在 LoRa 人机界面开发的 Inc 文件夹当中,在之后的人机界面开发的实操中,我们会介绍 logo.h 的使用方法。

![图片](/images/lora-doc/image162.png)

### TFT 液晶屏显示字符串

要想让 TFT 显示屏显示字体,除了上述取模的方法以外(取模方法常用于显示占满整个屏幕的字体或者中文),我们还可以直接使用库函数 Gui_DrawFont_GBK16() 来完成英文字体的输出。这个库函数的工作原理如下图所示。

<img src="/images/lora-doc/image163.png" alt="图片" data-width="57%" style="width: 57%" data-align="center" />

![图片](/images/lora-doc/image164.png)

Gui_DrawFont_GBK16(uint16_t x, uint16_t y, uint16_t fc, uint16_t bc, uint8_t *s) 的五个参数分别是:显示的行地址、显示的列地址、字体颜色、背景色、要写的字符串(英文)。

## TFT 人机界面开发

### 开机界面设计

对于人机界面开发,我们的设计是:TFT 显示屏一开始显示公司的 logo 图标,然后显示出 LoRa 接收和发送的相关数据信息。

显示的 logo:

<img src="/images/lora-doc/image165.jpeg" alt="图片" data-width="53%" style="width: 53%" data-align="center" />

实现图像显示的最基本思路是模仿清屏函数 Lcd_Clear() 的代码。因为我们输入的数据设定为行扫描,并且显示设定的大小为 128*128,所以 for 循环当中循环的次数可以确定,同时低字节优先,高字节靠后。

```c
/*************************************************
函数名:showimage
功能:显示一副图片
入口参数:图片缓存
返回值:无
*************************************************/
//因为一副128*128的图片需要32768个8位数组成(数组元素有32768个),很显然数组太大,不能放到内存中,只能放在静态存储区,所以参数使用 const 修饰
void showimage(const unsigned char *p)
{
 unsigned int i;
 uint16_t HData,LData;
 Lcd_SetRegion(0,0,X_MAX_PIXEL-1,Y_MAX_PIXEL-1);
 Lcd_WriteIndex(0x2C);
 for(i = 0;i < 128*128;i++)//因为我们已经设置显示屏显示范围为128*128,所以写满一行会自动换行,不需单独写代码
 {
   LData = *(p+i*2);
   HData = *(p+i*2+1);
   LCD_WriteData_16Bit(HData<<8|LData);
 }
}
```text

![图片](/images/lora-doc/image166.png)

这个函数的声明在 lcd.h 文件已经准备好了。

![图片](/images/lora-doc/image167.png)

然后打开我们之前生成的 logo.h,进行宏处理:

![图片](/images/lora-doc/image168.png)

并且在 main.c 里面添加头文件

![图片](/images/lora-doc/image169.png)

之后即可调用 showimage() 函数来显示我们的 logo 图像。

![图片](/images/lora-doc/image170.png)

**小结**:从上述编写 showimage() 函数的步骤我们可以总结得到程序开发的一般过程。首先确定函数的功能,**在源程序文件(.c)完成函数功能代码的编写,然后在头文件(.h)声明函数**。最后在需要**使用到该函数的位置包含头文件才能正常使用**。

### 菜单界面设计

显示收发数据的内容其实更多是排版的问题,显示英文内容我们直接使用函数 Gui_DrawFont_GBK16() 即可。

<img src="/images/lora-doc/image171.png" alt="图片" data-width="56%" style="width: 56%" data-align="center" />

```c
showimage(gImage_logo);
HAL_Delay(500);
Lcd_Clear(YELLOW);
Gui_DrawFont_GBK16(0,0,RED,GREEN,"   LoRa Topology   ");
Gui_DrawFont_GBK16(0,16,RED,GREEN,"      Master      ");
Gui_DrawFont_GBK16(0,32,BLACK,YELLOW,"SSID:");
Gui_DrawFont_GBK16(64,32,BLACK,YELLOW,"30");
Gui_DrawFont_GBK16(0,48,BLACK,YELLOW,"RX:");
Gui_DrawFont_GBK16(64,48,BLACK,YELLOW,"255");
Gui_DrawFont_GBK16(0,64,BLACK,YELLOW,"TX");
Gui_DrawFont_GBK16(64,64,BLACK,YELLOW,"255");
```text

![图片](/images/lora-doc/image172.png)

# LoRa PingPang 系统设计

PingPang 是什么?其实大白话来讲就是收发数据的过程。在 LoRa 中,主机发送的数据叫Ping,从机发送的数据叫 Pang(也有地方用 Pong)。

LoRa 通信的整个过程就是 Master 主动发送 PING 数据,接收 PANG 数据,Slave 接收 PING 数据,回应 PANG 数据的过程,这个功能的设计就是 LoRa PingPang 系统的设计。

## 深入了解 LoRa 技术原理

### LoRa 扩频通信原理

无线电波指在自由空间传播的射频(RF)频段的电磁波,其基本原理是导体中电流强度的改变会产生无线电波。利用这一现象,通过调制可将信息加载于无线电波中。当电波通过空间传播到达接收方时,电波引起的电磁场变化又会在导体中产生电流。再通过解调将信息从电流变化中提取出来,即可实现信息传递。

模拟无线通信信号大概经历以下三大过程:**输入→检波→放大**。

![图片](/images/lora-doc/image173.png)

数字无线通讯中,调制指将输入信息变换为适于信道传输的形式。信号源信息通常包含直流分量和频率较低的频率分量,称为基带信号。基带信号一般不能直接用于传输,需变换为一个远高于基带频率的信号,即已调信号。

调制过程改变了高频载波即信息载体信号的幅度、相位或频率,使其随基带信号幅度变化而变化。解调过程则将基带信号从载波中提取出来。

常用调制方式有:

-   模拟调制(幅度调制、角度调制)

-   数字调制(ASK、FSK、PSK)

-   脉冲调制(指用脉冲序列作为载波,最常用的是脉码调制)

![图片](/images/lora-doc/image174.png)

无线通信传播方式有地波传播(低于 2MHz)、天波传播(2MHz~30MHz)和直线传播(30MHz 以上)。

![图片](/images/lora-doc/image175.png)

无线通信传播路径有反射、散射和衍射三种。这些都会造成信号在移动环境中的衰落。

![图片](/images/lora-doc/image176.png)

同时生活中还有许许多多无线通信噪声存在。

![图片](/images/lora-doc/image177.png)

综上,也就导致了信号可能出现的不稳定的情况。

还有传输过程中的损耗:

```c
衰减和衰减失真
自由空间损耗
噪声
大气吸收
多径
```text

扩频(Spread Spectrum,SS)是一种重要的通信技术。发送方输入的数据首先进入信道编码器,生成模拟信号,该模拟信号围绕某个中心频率具有相对较窄的带宽。然后使用扩频码或扩展序列进一步调制,通常扩频码由伪噪声或伪随机数产生器产生。调制后传输信号的带宽显著增加,即扩展了频谱。 接收方使用同一扩频码进行解扩。解扩后的信号通过信号解码器,最终还原为数据。

扩频通信技术的作用:从各种类型的噪声和多径失真中获得**免疫性**。

扩频通信算法:C 表示信号质量

![图片](/images/lora-doc/image178.png)

扩频通信原理:用户数据和扩频数据**异或**得到发送数据,增加了信号带宽,提高了信号质量。

![图片](/images/lora-doc/image179.png)

### LoRa 关键技术参数

-   **信号带宽(BW)**:

增加 BW,可以提高有效数据速率以缩短传输时间,但是以牺牲部分接受灵敏度为代价。对于 LoRa 芯片 SX127x,LoRa 带宽为双边带宽(全信道带宽),而 FSK 调制方式的 BW 是指单边带宽。

| 带宽(kHz) | 扩频因子 | 编码率 | 标称比特率(bps) |
| --- | --- | --- | --- |
| 7.8 | 12 | 4/5 | 18 |
| 10.4 | 12 | 4/5 | 24 |
| 15.6 | 12 | 4/5 | 37 |
| 20.8 | 12 | 4/5 | 49 |
| 31.2 | 12 | 4/5 | 73 |
| 41.7 | 12 | 4/5 | 98 |
| 62.5 | 12 | 4/5 | 146 |
| 125 | 12 | 4/5 | 293 |
| 250 | 12 | 4/5 | 586 |
| 500 | 12 | 4/5 | 1172 |

-   **扩频因子(SF)**:

原本使用 1 位来表示的信号变成多位来表示这个信号,提高信号的通讯质量。

LoRa 采用多个信息码片来代表有效负载信息的每个位,扩频信息的发送速度称为符号速率(Rs),而码片速率与标称的 Rs 比值即为扩频因子(SF,SpreadingFactor),表示了每个信息位发送的符号数量。(信噪比越小,信号质量越好)

| 扩频因子(RegModulationCfg) | 扩频因子(码片/符号) | LoRa 解调器信噪比(SNR) |
| --- | --- | --- |
| 6 | 64 | -5 dB |
| 7 | 128 | -7.5 dB |
| 8 | 256 | -10 dB |
| 9 | 512 | -12.5 dB |
| 10 | 1024 | -15 dB |
| 11 | 2048 | -17.5 dB |
| 12 | 4096 | -20 dB |

-   **编码率(CR)**:

提高信号质量的冗余,提高数据的可靠性。

编码率(或信息率)是数据流中有用部分(非冗余)的比例。也就是说,如果编码率是 k/n,则对每 k 位有用信息,编码器总共产生 n 位的数据,其中 n-k 是多余的。

LoRa 采用循环纠错编码进行前向错误检测与纠错。使用该方式会产生传输开销。

| 编码率(RegTxCfg1) | 循环编码率 | 开销比率 |
| --- | --- | --- |
| 1 | 4/5 | 1.25 |
| 2 | 4/6 | 1.5 |
| 3 | 4/7 | 1.75 |
| 4 | 4/8 | 2 |

---

LoRa 符号速率 Rs 计算:

![图片](/images/lora-doc/image180.png)

LoRa 数据速率 DR 计算:

![图片](/images/lora-doc/image181.png)

LoRaWAN 主要使用了 125kHz 信号带宽设置,但其他专用协议可以利用其他的信号带宽(BW)设置。改变 BW、SF 和 CR 也就改变了链路预算和传输时间,需要在电池寿命和距离上做个权衡。

配置 LoRa 收发数据需要设置的参数如下,分别是两个函数 SX1276LoRaInit() 和 LoRaSettings() 以及函数的若干事件。

![图片](/images/lora-doc/image182.png)

### LoRa 数据收发任务

LoRa 数据发送序列:

数据发送流程:(1)LoRa 模块标准模式 →(2)发送模式 →(3)将数据写入发送队列 →(4)发送数据 →(5)等待发送完成(判断数据是否发送完成,如果发送完成则再次进入标准模式,如果还有数据需要发送,则进入第三步)

![图片](/images/lora-doc/image183.png)

LoRa 数据接收序列:

数据接收流程分成两部分,分别是连续接收模式和单次接收模式。接收是通过等待接收中断来判断执行的。

数据接收流程:(1)等待接收中断 → (2)是否超时 → (3)读取数据 → (4)检查模式是否更换 → (5)等待下一次接收中断

![图片](/images/lora-doc/image184.png)

Radio 事件任务:(官方固件提供的事件处理的任务,这些事件都是需要配置的)

![图片](/images/lora-doc/image185.png)

我们只需要设置上面的这些工作状态,就可以实现 LoRa 数据的收发。这些事件任务都可以在 sx1276-LoRa.c 和 sx1276-LoRa.h 代码当中找到。

## LoRa PingPang 系统设计原理

### PingPang 系统设计需求

将 LoRa 终端定义成两种角色:Master(主机)和 Slave(从机),这也就决定了我们需要两块 LoRa 开发板才能完成收发数据的任务。

LoRa PingPang 系统设计需求:

-   Master 主动发送 PING 数据,接收 PANG 数据;

-   Slave 如果接收到 PING 数据,回应 PANG 数据。

-   终端在 LCD 屏幕上显示终端类型及收发数据包个数。

### PingPang 系统通信机制

![图片](/images/lora-doc/image186.png)

发送数据的流程跟上面讲的差不多,我们可以看到,主机发送 Ping 是使用事件 SetTxPacket、StartRx 来完成的,从机发送 Pang 是使用事件 StartRx、SetTxPacket 来完成。是否停止收发数据我们可以看到是通过使用事件 TIMEOUT 或者 DONE 来判断的。

### PingPang 系统业务流程

PingPang 系统的第一步,对所有使用到的外设部分先进行初始化。并且添加宏定义判断,判断烧录程序的当前开发板是主机还是从机,对应执行不同的事件任务(主机发送数据,从机等待接收数据)。

<img src="/images/lora-doc/image187.png" alt="图片" data-width="80%" style="width: 80%" data-align="center" />

PingPang 业务流程——Master 主机:

主机确定数据发送,则 LCD 屏幕显示发送的数据内容,发送数据完成后指示灯闪烁,并把主机从发送模式设置为接收模式,等待从机返回确认数据。

当主机处在接收模式时,等待接收中断,收到数据并需判断接收的数据是否为从机返回的 PANG 数据,如果是则接收指示灯闪烁,并且返回 PING 数据,最后更新 LCD 显示收到的数据内容。

<img src="/images/lora-doc/image188.png" alt="图片" data-width="70%" style="width: 70%" data-align="center" />

PingPang 业务流程——Slave:

<img src="/images/lora-doc/image189.png" alt="图片" data-width="80%" style="width: 80%" data-align="center" />

LoRa 参数设置一览:

<img src="/images/lora-doc/image190.png" alt="图片" data-width="80%" style="width: 80%" data-align="center" />

数据包结构:

![图片](/images/lora-doc/image191.png)

## LoRa PingPang 系统功能开发

### IAR 工程配置

从上面的过程我们知道,我们需要对不同的两片开发板运行不同的收发过程,一个作为主机,一个作为从机。当主机处于发送状态时,从机需要在等待接收模式下;当主机处于等待接收状态时,从机需要在发送模式下。所以第一步,我们对不同的开发板先进行主从机设置。

![图片](/images/lora-doc/image192.png)

![图片](/images/lora-doc/image193.png)

新建两个配置,一个是主机"Master",另一个是从机"Slave"。

![图片](/images/lora-doc/image194.png)

![图片](/images/lora-doc/image195.png)

![图片](/images/lora-doc/image196.png)

配置这段的意义是以后主机和从机的工程都是基于我们原来写好的 "learn" 的基础代码。

然后还需要添加主机和从机的宏定义,工作区先选择主机的配置:

![图片](/images/lora-doc/image197.png)

![图片](/images/lora-doc/image198.png)

![图片](/images/lora-doc/image199.png)

然后再切换到 Slave 的工作区里面,宏定义"SLAVE"。

![图片](/images/lora-doc/image200.png)

### 搭建框架

完成准备工作之后,我们分析一下需要在主程序中增加哪些函数功能来完成我们收发数据的任务。每个开发板至少应该有两个事件任务,则主从机应该一共配置四个函数。这两个事件函数一个负责 LCD 内容的显示,一个是收发数据的处理。

<img src="/images/lora-doc/image201.png" alt="图片" data-width="65%" style="width: 65%" data-align="center" />

我们把主机显示任务函数定义为 MLCD_Show(),从机显示任务定义为 SLCD_Show();主机无线任务定义为 Master_Task(),从机无线任务定义为 Slave_Task().

然后我们根据上面的功能函数框架,在 main.c 当中完成框架的搭建如下。

```c
/* main.c */
/* USER CODE BEGIN 0 */
//主机显示任务
void MLCD_Show(void){
}
//从机显示任务
void SLCD_Show(void){
}
//主机无线任务
void Master_Task(void){
}
//从机无线任务
void Slave_Task(void){
}
/* USER CODE END 0 */
```text

建立数据结构:声明全部变量、进行赋值初始化。

<img src="/images/lora-doc/image202.png" alt="图片" data-width="67%" style="width: 67%" data-align="center" />

接收的数据、发送的数据、接收的数据量、发送的数据量等都需要使用变量来存储,所以我们在程序的开始之初根据我们所需,先完成这些变量的赋值和初始化操作。

```c
/* main.c */
/* Private variables ——————————————----*/
#define BUFFERSIZE 4
uint8_t PingMsg[] = "PING";
uint8_t PingMsg[] = "PANG";
uint16_t BufferSize = BUFFERSIZE;
uint8_t Buffer[BUFFERSIZE];
#ifdef MASTER
uint8_t EnbleMaster = true;
#else
uint8_t EnbleMaster = false;
#endif
uint32_t Master_TxNumber = 0;
uint32_t Master_RxNumber = 0;
uint32_t Slave_TxNumber = 0;
uint32_t Slave_RxNumber = 0;
tRadioDriver *Radio = NULL;   //在最前面要包含库 radio.h
```text

### 编码

(1)功能函数编码

我们要实现主机和从机的显示任务,就要使用到人机交互界面设计当中已经完成的一些用于显示的代码,将他们重新规整到 MLCD_Show() 和 SLCD_Show() 函数中。同时在这两个函数中,由于我们用到了 LCD 显示和 SPI 传输数据,所以也别忘了对这两者进行初始化。大致代码如下:

```c
//主机显示任务
void MLCD_Show(void){
uint8_t str[20] = {0};
LCD_GPIO_Init();    //LCD初始化
sprintf((char*)str,"%d",Master_RxNumber);        //将接收的内容显示出来
Gui_DrawFont_GBK16(64,48,BLACK,YELLOW,str);
memset((char*)str,0,strlen((const char*)str));  //申请新的内存初始化
sprintf((char*)str,"%d",Master_TxNumber);        //将发送的东西显示出来
Gui_DrawFont_GBK16(64,64,BLACK,YELLOW,str);
//SPI初始化
HAL_SPI_DeInit(&hspi1);
MX_SPI1_Init();
}
//从机显示任务
void SLCD_Show(void){
uint8_t str[20] = {0};
LCD_GPIO_Init();    //LCD初始化
sprintf((char*)str,"%d",Slave_RxNumber);         //将接收的内容显示出来
Gui_DrawFont_GBK16(64,48,BLACK,YELLOW,str);
memset((char*)str,0,strlen((const char*)str));  //申请新的内存初始化
sprintf((char*)str,"%d",Slave_TxNumber);         //将发送的东西显示出来
Gui_DrawFont_GBK16(64,64,BLACK,YELLOW,str);
//SPI初始化
HAL_SPI_DeInit(&hspi1);
MX_SPI1_Init();
}
```text

完成显示任务之后,我们开始编写主从机的无线任务。无线任务的编写思路就是我们上面主从机业务流程图的整个过程。在编写这部分代码的时候,我们不需要盲目自己去写,要学会去找官方已经提供好的参数和定义,灵活去配置。

举个例子,像在这里我们想要找到 LoRa 的数据处理的内容结构体,我们可以右键这个表示 LoRa 数据处理的变量"Radio",找到关于它的定义在第 80 行,双击跳转。

![图片](/images/lora-doc/image203.png)

然后找到它原来宏定义的名字,再跳转到它的结构体。

<img src="/images/lora-doc/image204.png" alt="图片" data-width="70%" style="width: 70%" data-align="center" />

然后我们就能看到Radio的所有结构体了:

![图片](/images/lora-doc/image205.png)

这些结构体表示的意思和内容在之前分析的图片当中已经有详细的注释了,大家要是不太记得可以翻回去再去了解一下。我们开发需要做的,就是根据需要取出这些结构体来使用就能完成系统的开发了。

完整代码如下:

```c
//主机无线任务
void Master_Task(void){
switch(Radio->Process()){       //无线任务处理进程判断
  case RF_RX_DONE:              //接收完成
    Radio->GetRxPacket(Buffer,&BufferSize);  //获取接收数据
    if(strncmp((const char*)Buffer,(const char*)PangMsg,strlen((const char*)PangMsg)) == 0){  //判断是否为Pang数据
      LedToggle(LED_RX);        //接收指示灯闪烁
      Master_RxNumber++;        //统计接收数据量
      Radio->SetTxPacket(PingMsg, strlen((const char*)PingMsg));  //发送Ping数据
    }
  break;
  case RF_TX_DONE:              //发送完成
    LedToggle(LED_TX);          //发送指示灯闪烁
    Master_TxNumber++;          //统计发送数据量
    Radio->StartRx();           //设置为接收状态
  break;
  default:
  break;
}
}
//从机无线任务
void Slave_Task(void){
switch(Radio->Process()){       //无线任务处理进程判断
  case RF_RX_DONE:              //接收完成
    Radio->GetRxPacket(Buffer,&BufferSize);  //获取接收数据
    if(strncmp((const char*)Buffer,(const char*)PingMsg,strlen((const char*)PingMsg)) == 0){  //判断是否为Pang数据
      LedToggle(LED_RX);        //接收指示灯闪烁
      Slave_RxNumber++;         //统计接收数据量
      Radio->SetTxPacket(PingMsg, strlen((const char*)PangMsg));  //发送Ping数据
    }
  break;
  case RF_TX_DONE:             //发送完成
    LedToggle(LED_TX);         //发送指示灯闪烁
    Slave_TxNumber++;          //统计发送数据量
    Radio->StartRx();          //设置为接收状态
  break;
  default:
  break;
}
}
```text

(2)main 函数编码

功能函数已经编写完成,接下来我们就来编写 main 主函数,按照上面的业务流程图,将整个过程复现在主函数当中,实现 LoRa 的数据收发。

```c
/* main.c */
#ifdef MASTER
Radio->SetTxPacket(PingMsg, strlen((const char*)PangMsg));  //发送Ping数据
#else
Radio->StartRx();  //开启接收
#endif
while (1){
  if(EnbleMaster == true){
    MLCD_Show();       //主机显示
    Master_Task();     //主机无线任务
  }
  else{
    SLCD_Show();       //从机显示
    Slave_Task();      //从机无线任务
  }
}
```text

(3)LoRa驱动源码

完成主函数功能的编写之后,我们再对 LoRa 驱动部分进行参数的设置和内容的修改。

首先我们打开 sx1276-LoRa.c 文件,注释掉其中所有的 DIO2 部分,因为我们不使用。

![图片](/images/lora-doc/image206.png)

![图片](/images/lora-doc/image207.png)

然后我们就要在 sx1276-LoRa.c 文件当中进行 LoRa 参数的设置(参数设置在前面的 **PingPang 系统业务流程**也有图片参考):

![图片](/images/lora-doc/image208.png)

编写项目之后会出现程序报错,这是因为有一个代码文件还没有添加进来。

![图片](/images/lora-doc/image209.png)

![图片](/images/lora-doc/image210.png)

![图片](/images/lora-doc/image211.png)

添加这两个文件之后再编译,就没有报错了。

![图片](/images/lora-doc/image212.png)

那么到此,整个代码编写部分的任务就已经完成,LoRa PingPang 系统设计的整体思路也大致如此。

## LoRa PingPang 系统功能调试

为了验证我们程序编写的准确性,我们接下来进行功能调试环节。功能调试我们需要通信,所以需要准备好两块 LoRa 的开发板,一个作主机一个作从机,分别烧入不同的开发板当中。

![图片](/images/lora-doc/image213.png)

接下来我们要通过串口打印出来的信息来验证我们的代码是否像期望的一样正常运行。

在宏定义里面加 printf() 函数打印主机、从机的确认信息。

```c
#ifdef MASTER
Radio->SetTxPacket(PingMsg, strlen((const char*)PangMsg));  //发送Ping数据
printf("I am Master!");
#else
Radio->StartRx();  //开启接收
printf("I am Salve!");
#endif
```text

然后在主机和从机的任务函数当中添加 printf() 函数,打印出接收的数据信息是什么。

```c
//主机无线任务
void Master_Task(void){
switch(Radio->Process()){       //无线任务处理进程判断
  case RF_RX_DONE:              //接收完成
    Radio->GetRxPacket(Buffer,&BufferSize);  //获取接收数据
    printf("Master_Task:RX______%s\\n",Buffer);
    if(strncmp((const char*)Buffer,(const char*)PangMsg,strlen((const char*)PangMsg)) == 0){  //判断是否为Pang数据
      LedToggle(LED_RX);        //接收指示灯闪烁
      Master_RxNumber++;        //统计接收数据量
      Radio->SetTxPacket(PingMsg, strlen((const char*)PingMsg));  //发送Ping数据
    }
  break;
  case RF_TX_DONE:              //发送完成
    LedToggle(LED_TX);          //发送指示灯闪烁
    Master_TxNumber++;          //统计发送数据量
    Radio->StartRx();           //设置为接收状态
  break;
  default:
  break;
}
}
//从机无线任务
void Slave_Task(void){
switch(Radio->Process()){       //无线任务处理进程判断
  case RF_RX_DONE:              //接收完成
    Radio->GetRxPacket(Buffer,&BufferSize);  //获取接收数据
    printf("Slave_Task:RX______%s\\n",Buffer);
    if(strncmp((const char*)Buffer,(const char*)PingMsg,strlen((const char*)PingMsg)) == 0){  //判断是否为Pang数据
      LedToggle(LED_RX);        //接收指示灯闪烁
      Slave_RxNumber++;         //统计接收数据量
      Radio->SetTxPacket(PingMsg, strlen((const char*)PangMsg));  //发送Ping数据
    }
  break;
  case RF_TX_DONE:             //发送完成
    LedToggle(LED_TX);         //发送指示灯闪烁
    Slave_TxNumber++;          //统计发送数据量
    Radio->StartRx();          //设置为接收状态
  break;
  default:
  break;
}
}
```text

然后就可以编译烧写程序检验代码的正确性。

![图片](/images/lora-doc/image214.png)

![图片](/images/lora-doc/image215.png)

可以看到,通过两个不同的串口助手连接两个不同的 LoRa 开发板,主机和从机之间已经实现了数据的收发任务,正常运行。

# LoRa 串口透传开发

## LoRa 串口透传系统设计

为什么要进行串口透传开发?因为很多人并不具备 LoRa 模块的开发能力,很多供应商会选择替用户设计好直观的串口透传系统,解决用户使用 LoRa 门槛高的问题。

### 串口透传系统设计需求

![图片](/images/lora-doc/image216.png)

1、将 LoRa 终端定义成两种角色:Master(主机)和 Slave(从机);

2、一个模块发送任意字节长度(小于 128 Byte)数据,另一模块都可以接收到;

3、**PC 机上通过串口调试助手实现接收和发送**;

4、终端在 LCD 屏幕上显示终端类型及收发数据包个数;

LoRa 透传系统设计与 PingPang 系统设计最大的不同之处是后者没有完成自定义数据的发送和接收,我们之前 **PingPang 系统设计的数据接收和发送是固定数据**。

### 串口透传系统通信机制

透传机制如下图所示,主机通过串口助手发送数据 SetTxPacket() 到从机,从机 StartRx() 处于等待接收的状态,当从机接收到主机的数据之后,使用 printf() 将数据打印出来并返回主机,主机也通过 printf() 将数据打印出来。

![图片](/images/lora-doc/image217.png)

### 串口透传业务流程

| 初始化 | 主程序 | LCD任务 |
| --- | --- | --- |
|   |   |   |
| 串口接收任务 | 无线任务 |   |
|   |   |   |

## LoRa串口透传系统功能开发

### 串口功能开发

<img src="/images/lora-doc/image223.png" alt="图片" data-width="66%" style="width: 66%" data-align="center" />

对于串口的功能配置,我们在 STM32CubeMX 生成的代码 uart.c 中有 uart1 的接收中断初始化、设置中断优先级和打开全局中断,所以这一步不需要人为干预,我们只需要将启动串口,让串口进入空闲中断的代码添加在 main.c 中即可。

```c
/* main.c */
//启动串口1,使能串口空闲中断 。设置为空闲中断模式可以在整包之后才进行数据的收发,然后产生中断,避免重复单个数据的收发(减小开销)
__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);//注意:使能空闲中断后,不管有没有接收数据,会先触发一次中断,发送一次数据
HAL_UART_Receive_DMA(&huart1,a_Usart1_RxBuffer,RXLENGHT); //打开DMA接收,串口空闲中断模式配合DMA进行数据的接收
```text

![图片](/images/lora-doc/image224.png)

然后我们打开 stm32f0xx_it.c 来配置串口中断。

```c
/* stm32f0xx_it.c */
void USART1_IRQHandler(void){
uint32_t temp;
//判断是否产生空闲中断
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE)  != RESET ){
  //清除中断标志
 __HAL_UART_CLEAR_IDLEFLAG(&huart1);
 temp = huart1.Instance->ISR;  //读取寄存器的值
 temp = huart1.Instance->RDR;  //读取寄存器的值
 HAL_UART_DMAStop(&huart1);  //停用DMA  关键点1:以上两行代码用于清除DMA的接收中断(只需要读取一次ISR和RDR寄存器的值)
 temp  = hdma_usart1_rx.Instance->CNDTR;
 UsartType1.Usart_rx_len =  RXLENGHT - temp; /*CNDTR为DMA通道接收数据的计数器(注意是一个递减计数器,所以需要将DMA的缓存区的总长度减去该计数器的值才是DMA通道接收数据的长度)*/
 HAL_UART_RxCpltCallback(&huart1); //调用回调函数,在回调函数中具体实现数据的接收 关键点2
}
HAL_UART_IRQHandler(&huart1);
```text

在 usart.c 当中重写回调函数。回调函数是指使用者自己定义一个函数,实现这个函数的程序内容,然后把这个函数(入口地址)作为参数传入别人(或系统)的函数中,由别人(或系统)的函数在运行时来调用的函数。函数是你实现的,但由别人(或系统)的函数在运行时通过参数传递的方式调用,这就是所谓的回调函数。简单来说,就是由别人的函数运行期间来回调你实现的函数。

```c
/* usart.c */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle){
uint8_t Old_len = 0;
if(UartHandle->Instance == USART1){  //判断是否为USART1中断
  Old_len = strlen(UsartType1.usartDMA_rxBuf);//读取发送缓冲区UsartType1.usartDMA_rxBuf的长度(如果为0则表示没有数据)
  if(0 != Old_len){ /*如果发送缓冲区中还有数据,则将DMA中接收缓冲区a_Usart1_RxBuffer的数据拷贝到发送缓冲区的末尾
                            (不覆盖发送缓冲区前面的数据)*/
    memcpy(&UsartType1.usartDMA_rxBuf[Old_len],a_Usart1_RxBuffer,UsartType1.Usart_rx_len);
    Old_len=0;
  }
  else{  /*如果发送缓冲区UsartType1.usartDMA_rxBuf中没有数据,则直接将DMA中接收缓冲区a_Usart1_RxBuffer的数据
                    拷贝到发送缓冲区的起始位置*/
    memcpy(UsartType1.usartDMA_rxBuf,a_Usart1_RxBuffer,UsartType1.Usart_rx_len);
//      UsartxSendData_DMA(&huart1,a_Usart2_RxBuffer,UsartType2.Usart_rx_len);
    memset(a_Usart1_RxBuffer,0,UsartType1.Usart_rx_len);//清空DMA中接收缓冲区a_Usart1_RxBuffer的数据
    HAL_UART_Receive_DMA(&huart1,a_Usart1_RxBuffer,RXLENGHT);//继续打开DMA接收
    UsartType1.receive_flag =1; //接收标识位,置1表示发送缓冲区中有新的数据,提醒相关函数将数据发送出去
}
}
```text

在 main.c 里面编写函数功能——将发送缓冲区里面的数据发送出去。

```c
/* main.c */
//串口数据获取//串口数据获取
void UartDmaGet(void){ //串口接收任务处理函数
if(UsartType1.receive_flag == 1){ //如果发送缓冲区中有新的数据,则将其发送给sx1278
  //串口接收到的数据原封发给SX1278,sx1278通过无线将数据发送出去
  Radio->SetTxPacket(UsartType1.usartDMA_rxBuf, UsartType1.Usart_rx_len);
  memset(UsartType1.usartDMA_rxBuf,0,UsartType1.Usart_rx_len);
  UsartType1.receive_flag = 0; //接收数据标志清零
}
}
```text

数据结构:

<img src="/images/lora-doc/image225.png" alt="图片" data-width="80%" style="width: 80%" data-align="center" />

```c
/* uart.h */
#define RXLENGHT  128
#define RECEIVELEN 2048
#define USART_DMA_SENDING       1//发送未完成
#define USART_DMA_SENDOVER      0//发送完成
typedef struct
{
uint8_t receive_flag ;     //空闲接收标记
uint8_t dmaSend_flag ;     //发送完成标记
uint16_t Usart_rx_len;     //接收长度
uint8_t usartDMA_rxBuf[RECEIVELEN];   //无线发送缓存
}USART_RECEIVETYPE;
extern USART_RECEIVETYPE UsartType1;
extern USART_RECEIVETYPE UsartType2;
//局部变量
//extern uint8_t g_Usart1_RxBuffer[RXLENGHT];
//extern uint8_t g_Usart2_RxBuffer[RXLENGHT];
//全局变量
extern uint8_t a_Usart1_RxBuffer[RXLENGHT]; //DMA接收缓存
//extern uint8_t a_Usart2_RxBuffer[RXLENGHT];
/* USER CODE END Private defines */
```text

### LCD 功能开发

在串口透传系统设计需求当中,我们明确了终端在 LCD 屏幕上显示终端类型及收发数据包个数,所以设计 LCD 的事件任务如下图所示。

<img src="/images/lora-doc/image226.png" alt="图片" data-width="80%" style="width: 80%" data-align="center" />

<img src="/images/lora-doc/image227.png" alt="图片" data-width="80%" style="width: 80%" data-align="center" />

LED 闪烁代码:

```c
void LedBlink( tLed led )
{
  LedPort[led]->ODR ^= LedPin[led];
  HAL_Delay(50);
  LedPort[led]->ODR ^= LedPin[led];
}
```text

LCD 显示代码:

这部分代码的理解可以结合 PingPang 系统设计中的系统功能调试以及串口透传系统通信机制的流程框图来加以理解。这里主要是把 PingPang 系统功能调试的 printf() 改成了使用串口将数据打印出来,思路大体是一致的。

```c
//读取sx127x射频射频数据
void Sx127xDataGet(void){
switch(Radio->Process( )){ //无线任务处理进程判断
case RF_RX_TIMEOUT:     //接收超时
  printf("RF_RX_TIMEOUT\\n");
  break;
case RF_RX_DONE:        //接收完成
  Radio->GetRxPacket( Buffer, ( uint16_t* )&BufferSize );  //获取接收数据
  if(EnbleMaster == true)
    printf("master Rx__%s,__,%d,%d\\n",Buffer,strlen((char*)Buffer),BufferSize);
  else
    printf("slave Rx__%s,__,%d,%d\\n",Buffer,strlen((char*)Buffer),BufferSize);
  if( BufferSize > 0 ){ //&& (BufferSize == strlen((char*)Buffer)))
    //接收数据闪烁
    LedBlink( LED_RX );
    //计算接收数据的个数
    RxDataPacketNum();
    //清空sx127x接收缓冲区
    memset(Buffer,0,BufferSize );
  }
  break;
case RF_TX_DONE:
  //发送闪烁
  LedBlink( LED_TX );
  //计算发送数据的个数
  TxDataPacketNum();
  Radio->StartRx();//打开接收模式
  break;
case RF_TX_TIMEOUT:
  printf("RF_TX_TIMEOUT\\n");
  break;
default:
  break;
}
}
```text

### 无线收发功能开发

<img src="/images/lora-doc/image227.png" alt="图片" data-width="82%" style="width: 82%" data-align="center" />

无线收发任务跟前面的 PingPang 系统设计大致相同,这里不再进行赘述。

```c
/* main.c */
void RxDataPacketNum(void){  //接收数据包计数
if(EnbleMaster == true)
  Master_RxNumber++;
else
  Slave_RxNumber++;
}
void TxDataPacketNum(void){  //发送数据包计数
if(EnbleMaster == true)
  Master_TxNumber++;
else
  Slave_TxNumber++;
}
//主机显示任务
void MLCD_Show(void){
uint8_t str[20] = {0};
LCD_GPIO_Init();    //LCD初始化
sprintf((char*)str,"%d",Master_RxNumber);        //将接收的内容显示出来
Gui_DrawFont_GBK16(64,48,BLACK,YELLOW,str);
memset((char*)str,0,strlen((const char*)str));  //申请新的内存初始化
sprintf((char*)str,"%d",Master_TxNumber);        //将发送的东西显示出来
Gui_DrawFont_GBK16(64,64,BLACK,YELLOW,str);
//SPI初始化
HAL_SPI_DeInit(&hspi1);
MX_SPI1_Init();
}
```text

![图片](/images/lora-doc/image228.png)

# LoRa 自组网络设计

## 深入了解 LoRaWAN

### LoRaWAN 是什么?

按照 LoRa 联盟官方白皮书《what is LoRaWAN》的介绍,LoRaWAN 是为 LoRa 远距离通信网络设计的一套通讯协议和系统架构。

![图片](/images/lora-doc/image229.png)

可以看到一个 LoRaWAN 网络架构中包含了终端、基站、NS(网络服务器)、应用服务器这四个部分。基站和终端之间采用星型网络拓扑,由于 LoRa 的长距离特性,它们之间得以使用单跳传输。在终端部分官方列了 6 个典型应用,有个细节,你会发现终端节点可以同时发给多个基站。基站则对 NS 和终端之间的 LoRaWAN 协议数据做转发处理,将 LoRaWAN 数据分别承载在了 LoRa 射频传输和 TCP/IP 上。

### LoRaWAN 通信机制

![图片](/images/lora-doc/image230.png)

首先是传感器端,传感器收集到的数据会存储在 LoRaWAN 的从机,LoRaWAN 底层部分应当有用 HAL 库配置好的 SPI 接口来传输这些数据,数据传输给服务器物理层。然后通过 LoRa 的 FSK 无线传输技术将数据传输到网关的物理层进行数据处理,通过 Ethernet、3G 或者 WIFI 加密传输到网络终端上面。但在用户看来,传感器端是直接跟网络服务器通信的。

### LoRaWAN 与其他组网协议对比

![图片](/images/lora-doc/image231.png)

### LoRaWAN 通信协议

LoRaWAN 在协议和网络架构的设计上,充分考虑了节点功耗,网络容量,QoS,安全性和网络应用多样性等几个因素。

![图片](/images/lora-doc/image232.png)

协议中有规定 Class A/B/C 三类终端设备,这三类设备基本覆盖了物联网所有的应用场景。

![图片](/images/lora-doc/image233.png)

这是 Class A 上下行的时序图,目前接收窗口 RX1 一般是在上行后 1 秒开始,接收窗口 RX2 是在上行后 2 秒开始。(功耗低,但是实时性较差)

![图片](/images/lora-doc/image234.png)

Class B 的时隙则复杂一些,它有一个同步时隙 beacon,还有一个固定周期的接收窗口 ping 时隙。(实时性高)

![图片](/images/lora-doc/image235.png)

Class C 和 A 基本是相同的,只是在 Class A 休眠的期间,它都打开了接收窗口 RX2。(保证了实时性,但是功耗比 Class A 高)

![图片](/images/lora-doc/image236.png)

### LoRaWAN 服务器框架

下面 LoRaWAN 的架构,它是基于 Internet 建设物联网,Gateway 是 IP 设备(运行 IP 协议栈),而 End Node 运行的是 LoRaMac-node(没有运行 IP 协议栈)。

LoRaWAN Server 共有 4 种角色,包括:NS(Network Server,网络服务器)、AS(Application Server,应用服务器)、NC(Network Controller,网络控制服务器)和 CS(Customer Server,客户服务器)。

![图片](/images/lora-doc/image237.png)

### LoRaWAN 服务器通信接口

![图片](/images/lora-doc/image238.jpeg)

### LoRaWAN 服务器通信协议

它们之间的通信协议规律如下:

NS 和 Gateways 通过 JSON / GWMP / UDP / IP;

Command console 和 4 种服务器通过 JSON / UDP / IP;

4 种服务器之间通过 JSON / TCP / IP。

UDP 通信的优势:实时性

TCP 通信的优势:可靠性

![图片](/images/lora-doc/image239.png)

## LoRa 自组网架构设计

### MAC 协议重要性

MAC 协议全称是 medium access control(介质访问控制)。介质访问控制的内容就是,采取一定的措施,使得两对节点之间的通信不会发生互相干扰的情况。它主要用于解决信号冲突的问题、尽可能地节省电能、保证通信的健壮和稳定性。

在 LoRa 自组网络设计中,会有多个节点同时收发数据,所以我们需要 MAC 协议帮助我们确定什么时间接收什么节点的数据,使得多个终端设备共享信道资源,提高信道利用率。

![图片](/images/lora-doc/image240.png)

### MAC 协议的种类

(1)信道划分的 MAC 协议

-   时分(TDMA)、频分(FDMA)、码分(CDMA)划分

(2)随机访问 MAC 协议

-   ALOHA,S-ALOHA,CSMA,CSMA/CD

-   CSMA/CD 应用于以太网

-   CSMA/CA 应用于 802.11 无线局域网

(3)轮讯访问 MAC 协议

-   主节点轮询

-   工业Modbus通信协议

![图片](/images/lora-doc/image241.png)

### 常见的几种协议的优缺点分析

(1)时分复用

![图片](/images/lora-doc/image242.png)

**优点:**

节省电能、最大化使用带宽;

**缺点:**

所有节点需要精确的时钟源,并且需要周期性校时、向网络中添加和删除节点都要有时隙分配和回收算法。

(2)频分复用

![图片](/images/lora-doc/image243.png)

**优点:**

增加通信容量、提高通信可靠性;

**缺点:**

物理通道增加,成本增加。

(3)轮询访问

![图片](/images/lora-doc/image244.png)

**优点:**

协议简单,易开发;

**缺点:**

通讯效率低、网络规模小。

### 基于时分复用 LoRa 自组网设计

由于能力和条件的限制,我们这里的 LoRa 自组网络就采用比较简单但是效率较高的时分复用 MAC 协议来完成。根据上述时分复用的原理,我们制定如下图所示的入网机制。

![图片](/images/lora-doc/image245.png)

时分复用的基本原理是这样的,不同的节点随机等待一段时间进行数据的发送,假如当前的节点 i 等待时间到,且检测无其他节点正在与主机进行数据交换,则发送数据给主机,主机进行数据的接收。此时如果恰好节点 j 的等到时间也到了,则需继续等待一段随机时间,因为节点 i 正在传输数据,等到下一个时间片到达,且无其他节点发送数据,则节点 j 才可与主机通信。

![图片](/images/lora-doc/image246.png)

### LoRa 自组网协调器设计

![图片](/images/lora-doc/image247.png)

### LoRa自组网节点设计

![图片](/images/lora-doc/image248.png)

## LoRa 自组网集中器程序开发

### 通讯协议

以下是 LoRa 规定的节点数据帧格式,贯穿于整个自组网的开发过程。

| 从机->入网请求 |   |   |   |
| --- | --- | --- | --- |
| 名称 | 字节数 | 描述 | 举例 |
| 帧头 | 1 | 0x3C | 0x3C |
| 长度 | 1 | 最长 126 字节;长度范围内不检测包头;计算整个帧长度 | 0x0c |
| 网络类型 | 1 | 字符 代表入网请求 | J |
| 网络标识符 | 2 | PANID,用于网络区分,只有 PANID 一样才可以组网通信 | 0x0102 |
| 设备地址 | 2 | 设备唯一地址标识 | 0x1235 |
| CRC8 校验 | 1 | 数据包校验,整个数据包,除校验位 | 0x08 |

| 主机->入网应答 |   |   |   |
| --- | --- | --- | --- |
| 名称 | 字节数 | 描述 | 举例 |
| 帧头 | 1 | 0x3C | 0x3C |
| 长度 | 1 | 最长 126 字节;长度范围内不检测包头;计算整个帧长度 | 0x0c |
| 网络类型 | 1 | 字符 代表入网成功 | A |
| 网络标识符 | 2 | PANID,用于网络区分,只有 PANID 一样才可以组网通信 | 0x0102 |
| 设备地址 | 2 | 设备唯一地址标识 | 0x1235 |
| 设备序号 | 1 | 设备是第几个入网 | 1 |
| CRC8 校验 | 1 | 数据包校验,整个数据包,除校验位 | 0x08 |

| 主机->时间同步 |   |   |   |
| --- | --- | --- | --- |
| 名称 | 字节数 | 描述 | 举例 |
| 帧头 | 1 | 0x3C | 0x3C |
| 长度 | 1 | 最长126字节;长度范围内不检测包头;计算整个帧长度 | 0x0c |
| 网络类型 | 1 | 字符 代表入网请求 | T |
| 网络标识符 | 2 | PANID,用于网络区分,只有 PANID 一样才可以组网通信 | 0x0102 |
| 时 | 1 | RTC- 时 1-24 | 0x1235 |
| 分 | 1 | RTC- 分 1-60 | 12 |
| 秒 | 1 | RTC- 秒 1-60 | 12 |
| 亚秒 | 2 | RTC- 亚秒 1-1000 | 1000 |
| CRC8 校验 | 1 | 数据包校验,整个数据包,除校验位 | 0x08 |

| 网络数据包 |   |   |   |   |
| --- | --- | --- | --- | --- |
| 名称 | 字节数 | 描述 | 举例 |   |
| 帧头 | 1 | 字符 代表网络数据包 | N |   |
| 网络标识符 | 2 | 用于网络区分,只有 PANID 一样才可以组网通信 | 0x0102 |   |
| 数据包 | 包头 | 1 | 0x21 | 0x21 |
| 包长 | 1 | 数据包长度,代表数据域数据长度 | 1 |   |
| 数据类型 | 1 | 0x00:数据、0x01:命令 | 0x00 |   |
| 设备地址 | 2 | 设备标识符 | 0x0001 |   |
| 传感器类型 | 1 | 0x01:温湿度、0x02:三轴、0x03:水表、0x04:地磁、0x05:灌溉 | 0x01 |   |
| 数据 | 4 | 每种传感器数值用两个字节标识,比如温湿度占四个字节 | 0x01298113 |   |
| CRC8 校验 | 1 | 数据包校验,整个数据包,除校验位 | 0x08 |   |

### 工程修改

业务流程参见下图:

![图片](/images/lora-doc/image249.png)

根据协调器业务流程,需要在之前工程里添加两个外设的配置:RTC 和定时器。RTC 时钟是为了计算节点数据发送的时间而服务的。添加外设需要修改 STM32CubeMX 工程。

![图片](/images/lora-doc/image250.png)

双击打开 .ioc 文件进入 STM32CubeMX,在左侧的引脚中选择 "Timers" 下的 "RTC",并且打开 "Activate Clock Source"、"Activate Calendar"、"Internal Alarm A"。

![图片](/images/lora-doc/image251.png)

然后对 RTC 时钟进行一些简单的配置。

![图片](/images/lora-doc/image252.png)

![图片](/images/lora-doc/image253.png)

之后我们在 TIM2 中配置内部时钟。

![图片](/images/lora-doc/image254.png)

![图片](/images/lora-doc/image255.png)

![图片](/images/lora-doc/image256.png)

修改完成后点击"生成"即可:

![图片](/images/lora-doc/image257.png)

### 搭建框架

在 User 文件夹下面新建一个文件夹,然后将网络处理、协议解析的代码"dataprocess.c"、"netprocess.c"、"protocol.c"放在文件夹当中,方便多功能代码的开发,让结构更加清晰。

![图片](/images/lora-doc/image258.png)

框架的基本内容有:

RTC 任务——RTC 初始化、Alarm 中断任务;

```c
/* rtc.c */
//闹钟事件回调函数
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc){
}
//时分秒转换
void GetTimeHMS(uint32_t timeData,uint8_t *hours,uint8_t *minute,uint8_t *seconds,uint32_t *subSeconds) {
}
```text

定时器任务——定时器初始化、定时器中断任务;

```c
/* tim.c */
//定时器2溢出中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
}
```text

通信协议——CRC8 检验函数、协议数据结构;

```c
/* protocol.c */
/******************************************************************************
* Name:    CRC-8               x8+x2+x+1
* Poly:    0x07
* Init:    0x00
* Refin:   False
* Refout:  False
* Xorout:  0x00
* Note:
*****************************************************************************/
uint8_t crc8(uint8_t *data, uint8_t length){
uint8_t i;
uint8_t crc = 0;        // Initial value
while(length--){
  crc ^= *data++;        // crc ^= *data; data++;
  for ( i = 0; i < 8; i++ ){
    if ( crc & 0x80 )
      crc = (crc << 1) ^ 0x07;
    else
      crc <<= 1;
  }
}
return crc;
}
//CRC8校验
uint8_t DataCrcVerify(uint8_t * buff, uint8_t len){
  uint8_t Crc8Data = 0;
  //验证数据是否正确
  Crc8Data = crc8(buff, len - 1);
  if (Crc8Data == buff[len - 1]){
//      PRINTF1("CRC8 Success!\\n");
      return 1;
  }
  else{
//      PRINTF1("CRC8 Failed!\\n");
      return 0;
  }
}
```text

数据处理任务——串口任务(串口接收)、无线任务(无线接收、主机协议解析、网络数据包解析、入网请求解析)

```c
/* dataprocess.c */
extern uint16_t BufferSize;
extern uint8_t Buffer[BUFFER_SIZE];
#if defined(MASTER)
extern uint8_t EnableMaster;
#elif defined(SLAVE)
extern uint8_t EnableMaster;
#endif
extern tRadioDriver *Radio;
extern uint32_t Master_RxNumber;
extern uint32_t Master_TxNumber;
extern uint32_t Slave_RxNumber;
extern uint32_t Slave_TxNumber;
extern volatile uint8_t SendDataOkFlag;
uint8_t startUpDateHours = 0;
uint8_t startUpDateMinute = 0;
uint8_t startUpDateSeconds = 0;
uint16_t startUpDateSubSeconds = 0;
//Master存储入网的设备信息
SlaveInfo slaveNetInfo_t[NodeNumber];
//Salve入网信息包
SlaveJionNet jionPacke_t;
//Salve保存自己的地址
SlaveInfo slaveNativeInfo_t;
//节点数据
SlaveDataNet DataPacke_t;
//串口数据获取
void UartDmaGet(void){
}
//接收数据包计数
void RxDataPacketNum(void){
}
//发送数据包计数
void TxDataPacketNum(void){
}
//读取sx127x射频射频数据
uint8_t Sx127xDataGet(void){
}
//**************从**************//
//**************机**************//
//从机入网数据发送
void SendJionNetPacke(void){
}
//从机协议解析
uint8_t SlaveProtocolAnalysis(uint8_t *buff,uint8_t len){
}
//上传节点传感器数据
void SendSensorDataUP(void){
}
//**************主**************//
//**************机**************//
//主机协议解析
uint8_t MasterProtocolAnalysis(uint8_t *buff,uint8_t len){
}
//入网协议解析
uint8_t JionNetProtocolAnalysis(uint8_t *buff,uint8_t len){
}
//网络数据包解析
void NetDataProtocolAnalysis(uint8_t *buff,uint8_t len){
}
```text

网络处理任务——等待入网完成、主机发送时钟同步数据包

```c
/* netprocess.c */
//所有节点的更新周期(在Time内上传所有数据) 单位Ms
volatile uint32_t DataUpTimePeriod = 1000 *  60 * 1;    //1分钟
volatile static uint32_t currentTime = 0;
//当前加入设个的个数
volatile  uint16_t currentDeviceNumber = 0;
//保存当前加入节点
volatile  uint16_t oldNodeNumber = 0;
//节点时间片
volatile uint32_t DataUpTime = 0;
//节点入网状态
volatile DeviceJionFlag JionNodeTimeOutFlag = No_Node_Jion_Flag;
uint8_t startUpTimeHours = 0;
uint8_t startUpTimeMinute = 0;
uint8_t startUpTimeSeconds = 0;
uint32_t startUpTimeSubSeconds = 0;
uint8_t DataUpTimeHours = 0;
uint8_t DataUpTimeMinute = 0;
uint8_t DataUpTimeSeconds = 0;
uint32_t DataUpTimeSubSeconds = 0;
//时钟同步
SlaveRtcSync rtcSync_t;
//初始化网络状态
volatile DeviceJionStatus NetStatus = NO_JION;
extern tRadioDriver *Radio;
//**************从**************//
//**************机**************//
//生成随机数
uint16_t RandomNumber(void){
}
//从机加入网络
uint8_t SlaveJionNetFuction(void){
}
//节点获取时间片
void SlaveGetSendTime(void){
}
//**************主**************//
//**************机**************//
//等待入网完成
DeviceJionFlag WaitJionNetFinish(uint8_t timout){
}
//主机发送同步时钟
void MasterSendClockData(void){
}
```text

### 源码分析

![图片](/images/lora-doc/image249.png)

```c
/* main.c */
int main(void){
...
#if SLAVE
//获取随机入网时间
DelayTime = RandomNumber();
printf("JionTime = %d\\n",DelayTime);
HAL_Delay(DelayTime);
//等待入网成功
while (SlaveJionNetFuction());
//获取节点发送时间片
SlaveGetSendTime();
#else
  //主机直接初始化RTC
  MX_RTC_Init();
  HAL_TIM_Base_Start_IT(&htim2);
#endif
while (1){
  Sx127xDataGet();
#if SLAVE
  if(sendUpDataFlag == 1){
    SendSensorDataUP();
    sendUpDataFlag = 0;
  }
#else
  UartDmaGet();
  //等待节点入网
  if (JionDeviceStatu != Node_Jion_Finish_Flag){
    printf("main 等待加入网络\\n");
    JionDeviceStatu = WaitJionNetFinish(10);
  }
  /* 有新节点加入 */
  if (currentDeviceNumber != oldNodeNumber){
    printf("main 新节点加入网络\\n");
    HAL_TIM_Base_Start_IT(&htim2);
    JionDeviceStatu = New_Node_Jion_Flag;
    SendClockFlag = 0; //发送分时时间片
  }
  /* 有旧节点加入 */
  for (int i = 0; i < currentDeviceNumber;i++){
    /* 查询是否有旧节点重新加入*/
    if (slaveNetInfo_t[i].deviceNetStatus == AGAIN_JION_NET){
      printf("main 旧节点加入网络\\n");
      slaveNetInfo_t[i].deviceNetStatus = JIONDONE;
      JionDeviceStatu = New_Node_Jion_Flag;
      SendClockFlag = 0; //发送分时时间片
      HAL_TIM_Base_Start_IT(&htim2);
    }
  }
  /* 给从机分发时间片 */
  if ((JionDeviceStatu == Node_Jion_Finish_Flag)&&(SendClockFlag == 0)
      &&(currentDeviceNumber != 0)){
      if (SendDataOkFlag == 1) {
          SendDataOkFlag = 0;
                      printf("main 发送时钟同步\\n");
          //告诉所有节点开始上传数据
          MasterSendClockData();
          SendClockFlag = 1;
          while(!SendDataOkFlag){  //等待发送完成
              Sx127xDataGet();
          }
          SendDataOkFlag = 1;
      }
  }
#endif
  ...
}
}
```text

LoRa 启动完毕并自检 OK 之后,从机自动生成入网的时间,等待入网成功之后,获取节点发送时间片。主机处于一直等待节点入网的状态,当有节点入网时,判断是新节点入网还是旧节点入网,然后分别发送分时时间片。节点时间到,则开始上传数据,主机等待数据发送完成。

## LoRa 自组网节点程序开发

### 硬件准备

-   LoRa 设备 ×3

-   ST-Link ×1

-   USB mini 线 ×3

选择一块 LoRa 主板烧写一次 Master 主机程序。

烧写两次 Slave 程序(模拟多个节点)——需要配置从机设备地址(选择两个不同的地址即可),分别烧录。

![图片](/images/lora-doc/image259.png)

### 实验现象

从机(节点)入网请求,主机入网应答,从机 1 分钟定时上传数据。

![图片](/images/lora-doc/image260.png)

主机:

![图片](/images/lora-doc/image261.png)

从机:

![图片](/images/lora-doc/image262.png)

![图片](/images/lora-doc/image263.png)

# LoRa 智慧牧场项目集成开发

在前面的课程内容当中,我们已经学习完了一整套 LoRa 开发的过程,并且在前面一节实现了基于时分复用 MAC 协议的自组网络设计,可以说已经具备了 LoRa 集成开发的基础。为了更加巩固我们所学习的成果,我们就拿应用比较多的智慧牧场项目来真正将我们所学习的内容应用在实操上。

开始设计智慧牧场项目之前,我们回顾一下之前学习的 LoRa 自组网络设计的步骤。传输数据确定 → LoRa PingPang 系统 → LoRa 透传系统 → LoRa 自组网。我们可以模仿这个步骤,先规划一下智慧牧场项目我们大概需要以下几步:数据采集 → 数据发送 → 数据接收处理 → 闭环控制。

## 体征数据采集

### 需求分析及传感器原理

需求分析——如何获取奶牛计步信息?

![图片](/images/lora-doc/image264.png)

采集传感器——六轴运动处理传感器 MPU6050

三轴加速度测量原理:加速度测量计反应的加速向量与当前的受力方向是相反,单位为 g。

![图片](/images/lora-doc/image265.png)

三轴陀螺仪测量:陀螺仪,是用来测量角速度的,单位为度每秒(deg/s)。

![图片](/images/lora-doc/image266.png)

MPU6050 是使用 IIC 进行数据传输的,其初始化过程如下:

![图片](/images/lora-doc/image267.png)

读取加速度初始值过程如下:

![图片](/images/lora-doc/image268.png)

读取加速度数据过程如下:

![图片](/images/lora-doc/image269.png)

### 功能配置及源码分析

首先我们需要打开 .ioc 文件,对 STM32 芯片的 IIC 通信接口进行配置。查询原理图可知,芯片的 PB8(SCL)和 PB7(SDA)是 IIC 的数据引脚。

![图片](/images/lora-doc/image270.png)

然后生成代码即可。

在项目工程的 User 文件夹里面新建一个 Sensor 文件夹,将 MPU6050 传感器的配置代码放在里面。

![图片](/images/lora-doc/image271.png)

```c
/* mpu6050 */
#include "mpu6050.h"
#include "string.h"
#include "stdio.h"
#include "i2c.h"
int16_t Accx,Accy,Accz;
// 初始化MPU6050
void InitMpu6050(void){
uint8_t WriteCmd = 0;
//解除休眠状态
WriteCmd = 0x00;
HAL_I2C_Mem_Write(&hi2c1, ADDRESS_Write, PWR_MGMT_1, I2C_MEMADD_SIZE_8BIT, &WriteCmd, 1, 0x10);
//时钟速率0x06(1Khz)陀螺仪采样率0x07(125Hz)
WriteCmd = 0x07;
HAL_I2C_Mem_Write(&hi2c1, ADDRESS_Write, SMPLRT_DIV, I2C_MEMADD_SIZE_8BIT, &WriteCmd, 1, 0x10);
WriteCmd = 0x06;
HAL_I2C_Mem_Write(&hi2c1, ADDRESS_Write, CONFIG, I2C_MEMADD_SIZE_8BIT, &WriteCmd, 1, 0x10);
//不自检,2000deg/s
WriteCmd = 0x18;
HAL_I2C_Mem_Write(&hi2c1, ADDRESS_Write, GYRO_CONFIG, I2C_MEMADD_SIZE_8BIT, &WriteCmd, 1, 0x10);
//(不自检,2G,5Hz)
WriteCmd = 0x01;
HAL_I2C_Mem_Write(&hi2c1, ADDRESS_Write, ACCEL_CONFIG, I2C_MEMADD_SIZE_8BIT, &WriteCmd, 1, 0x10);
HAL_Delay(10);
mpu6050_verify(&Accx, &Accy, &Accz); //读取第一次的值
}
// MPU6050校验
void  mpu6050_verify(int16_t *x, int16_t *y, int16_t *z){
uint8_t ReadBuffer[10] = {0};
HAL_I2C_Mem_Read(&hi2c1, ADDRESS_Read, ACCEL_XOUT_L, I2C_MEMADD_SIZE_8BIT,&ReadBuffer[0],1, 0x10);
HAL_I2C_Mem_Read(&hi2c1, ADDRESS_Read, ACCEL_XOUT_H, I2C_MEMADD_SIZE_8BIT,&ReadBuffer[1],1, 0x10);
      *x = (ReadBuffer[1]<<8)+ReadBuffer[0];
HAL_I2C_Mem_Read(&hi2c1, ADDRESS_Read, ACCEL_YOUT_L, I2C_MEMADD_SIZE_8BIT,&ReadBuffer[0],1, 0x10);
HAL_I2C_Mem_Read(&hi2c1, ADDRESS_Read, ACCEL_YOUT_H, I2C_MEMADD_SIZE_8BIT,&ReadBuffer[1],1, 0x10);
      *y = (ReadBuffer[1]<<8)+ReadBuffer[0];
HAL_I2C_Mem_Read(&hi2c1, ADDRESS_Read, ACCEL_ZOUT_L, I2C_MEMADD_SIZE_8BIT,&ReadBuffer[0],1, 0x10);
HAL_I2C_Mem_Read(&hi2c1, ADDRESS_Read, ACCEL_ZOUT_H, I2C_MEMADD_SIZE_8BIT,&ReadBuffer[1],1, 0x10);
      *z = (ReadBuffer[1]<<8)+ReadBuffer[0];
}
// MPU6060获取三轴数据
void  mpu6050_ReadData(float *Mx, float *My, float *Mz){
int16_t x,y,z;
uint8_t ReadBuffer[10] = {0};
HAL_I2C_Mem_Read(&hi2c1, ADDRESS_Read, ACCEL_XOUT_L, I2C_MEMADD_SIZE_8BIT,&ReadBuffer[0],1, 0x10);
HAL_I2C_Mem_Read(&hi2c1, ADDRESS_Read, ACCEL_XOUT_H, I2C_MEMADD_SIZE_8BIT,&ReadBuffer[1],1, 0x10);
x = (ReadBuffer[1]<<8)+ReadBuffer[0];
x -= Accx;
*Mx = ((float)x)/16384;
HAL_I2C_Mem_Read(&hi2c1, ADDRESS_Read, ACCEL_YOUT_L, I2C_MEMADD_SIZE_8BIT,&ReadBuffer[0],1, 0x10);
HAL_I2C_Mem_Read(&hi2c1, ADDRESS_Read, ACCEL_YOUT_H, I2C_MEMADD_SIZE_8BIT,&ReadBuffer[1],1, 0x10);
y = (ReadBuffer[1]<<8)+ReadBuffer[0];
y -= Accy;
*My = ((float)y)/16384;
HAL_I2C_Mem_Read(&hi2c1, ADDRESS_Read, ACCEL_ZOUT_L, I2C_MEMADD_SIZE_8BIT,&ReadBuffer[0],1, 0x10);
HAL_I2C_Mem_Read(&hi2c1, ADDRESS_Read, ACCEL_ZOUT_H, I2C_MEMADD_SIZE_8BIT,&ReadBuffer[1],1, 0x10);
z = (ReadBuffer[1]<<8)+ReadBuffer[0];
z -= Accz;
*Mz = ((float)z)/16384;
}
```text

更多有关 MPU6050 的配置可以查阅数据手册来理解。

然后我们需要在 main.c 当中对数据读取进行配置:定义全局变量 Mx、My、Mz,初始化 IIC,初始化 MPU6050,然后每两秒钟读取一次加速度值即可。

```c
float Mx,My,Mz;
...
int main(void){
MX_I2C1_Init();
InitMpu6050();
...
while(1){
  ...
  mpu6050_ReadData(&Mx,&My,&Mz);
  printf("Mx = %.3f\\n",Mx);
  printf("My = %.3f\\n",My);
  printf("Mz = %.3f\\n",Mz);
  ...
}
...
}
```text

实验现象:

将传感器安装在奶牛腿上,其体征数据会每两秒返回一次,主机可获得接收并通过串口助手打印出来。

![图片](/images/lora-doc/image272.png)

## 饲养环境采集

### 需求分析及传感器原理

需求分析——如何获取牛舍温湿度信息?

![图片](/images/lora-doc/image273.png)

采集传感器——DHT11 数字式空气温湿度传感器

DHT11 数据传输原理:一次完整的数据传输为 40 bit,高位先出

8bit 湿度整数数据+ 8bit 湿度小数数据;

8bit 温度整数数据+ 8bit 温度小数数据;

8bit 校验和。

由于 DHT11 是单总线协议传输数据,所以是通过高低电平持续的时间长短来进行数据的传输。

![图片](/images/lora-doc/image274.png)

启动采集的过程:

![图片](/images/lora-doc/image275.png)

获取数据的过程:

![图片](/images/lora-doc/image276.png)

数字 0 信号的高低电平状态:

![图片](/images/lora-doc/image277.png)

数字 1 信号的高低电平状态:

![图片](/images/lora-doc/image278.png)

### 功能配置及源码分析

![图片](/images/lora-doc/image279.png)

由于我们这里 DHT11 接的是 PB8 引脚,所以需要对 PB8 的两种模式(输入、输出)进行初始化操作。

```c
/* gpio.c */
void D2_IN_GPIO_Init(void){
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.Pin = GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
void D2_OUT_GPIO_Init(void){
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.Pin = GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
DHT11 配置代码如下:
/* dht11.c */
#include "stdint.h"
#include "tim.h"
#include "gpio.h"
#include "dht11.h"
#include "delay.h"
//温湿度定义
uint8_t ucharT_data_H=0,ucharT_data_L=0,ucharRH_data_H=0,ucharRH_data_L=0,ucharcheckdata=0;
void DHT11_TEST(void){   //温湿传感启动
uint8_t ucharT_data_H_temp,ucharT_data_L_temp,ucharRH_data_H_humidity,ucharRH_data_L_humidity,ucharcheckdata_temp;
uint8_t ucharFLAG = 0,uchartemp=0;
uint8_t ucharcomdata;
uint8_t i;
D2_OUT_GPIO_Init();
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_RESET);
HAL_Delay_ms(18);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_SET);
D2_IN_GPIO_Init();
HAL_Delay_10us(4);
if(!HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8)){
  ucharFLAG=2;
  while((!HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))&&ucharFLAG++);
  ucharFLAG=2;
  while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8)&&ucharFLAG++);
  for(i=0;i<8;i++){
    ucharFLAG=2;
    while((!HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))&&ucharFLAG++);
    HAL_Delay_10us(3);
    uchartemp=0;
    if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))uchartemp=1;
    ucharFLAG=2;
    while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8)&&ucharFLAG++);//超时判断
    if(ucharFLAG==1)break;
    ucharcomdata<<=1;
    ucharcomdata|=uchartemp;
    }
  ucharRH_data_H_humidity = ucharcomdata;
  for(i=0;i<8;i++){
    ucharFLAG=2;
    while((!HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))&&ucharFLAG++);
    HAL_Delay_10us(3);
    uchartemp=0;
    if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))uchartemp=1;
    ucharFLAG=2;
    while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8)&&ucharFLAG++);
    if(ucharFLAG==1)break;
    ucharcomdata<<=1;
    ucharcomdata|=uchartemp;
    }
  ucharRH_data_L_humidity = ucharcomdata;
  for(i=0;i<8;i++){
    ucharFLAG=2;
    while((!HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))&&ucharFLAG++);
    HAL_Delay_10us(3);
    uchartemp=0;
    if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))uchartemp=1;
    ucharFLAG=2;
    while((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))&&ucharFLAG++);
    if(ucharFLAG==1)break;
    ucharcomdata<<=1;
    ucharcomdata|=uchartemp;
    }
  ucharT_data_H_temp = ucharcomdata;
  for(i=0;i<8;i++){
    ucharFLAG=2;
    while((!HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))&&ucharFLAG++);
    HAL_Delay_10us(3);
    uchartemp=0;
    if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))uchartemp=1;
    ucharFLAG=2;
    while((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))&&ucharFLAG++);
    if(ucharFLAG==1)break;
    ucharcomdata<<=1;
    ucharcomdata|=uchartemp;
    }
  ucharT_data_L_temp = ucharcomdata;
  for(i=0;i<8;i++){
    ucharFLAG=2;
    while((!HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))&&ucharFLAG++);
    HAL_Delay_10us(3);
    uchartemp=0;
    if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))uchartemp=1;
    ucharFLAG=2;
    while((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8))&&ucharFLAG++);
    if(ucharFLAG==1)break;
    ucharcomdata<<=1;
    ucharcomdata|=uchartemp;
    }
  ucharcheckdata_temp = ucharcomdata;
//  humiture=1;
  uchartemp=(ucharT_data_H_temp+ucharT_data_L_temp+ucharRH_data_H_humidity+ucharRH_data_L_humidity);
  if(uchartemp==ucharcheckdata_temp){
    ucharT_data_H  = ucharT_data_H_temp;
    ucharT_data_L  = ucharT_data_L_temp;
    ucharRH_data_H = ucharRH_data_H_humidity;
    ucharRH_data_L = ucharRH_data_L_humidity;
    ucharcheckdata = ucharcheckdata_temp;
    }
}
else{ //没用成功读取,返回0
  ucharT_data_H  = 0;
  ucharT_data_L  = 0;
  ucharRH_data_H = 0;
  ucharRH_data_L = 0;
}
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000); //恢复1ms嘀嗒定时器
}
```text

main 函数配置读取温湿度数据代码如下:

```c
...
while(1){
#if defined(MPU6050)
  mpu6050_ReadData(&Mx,&My,&Mz);
  printf("Mx = %.3f\\n",Mx);
  printf("My = %.3f\\n",My);
  printf("Mz = %.3f\\n",Mz);
#elif defined(DHT11)
  DHT11_TEST();
  printf("TEMP = %d\\n",ucharT_data_H);
  printf("HUM = %d\\n",ucharRH_data_H);
#endif
  SLCD_Show();
  ...
}
...
```text

实验现象:

将温湿度传感器放置在牛舍环境下,每隔两秒钟主机会获取牧场温湿度值并且通过串口助手打印出来。

![图片](/images/lora-doc/image280.png)

## 饲养环境控制

### 需求分析及传感器原理

需求分析——如何驱动风扇启动关闭?

![图片](/images/lora-doc/image273.png)

风扇转动是通过直流电机来实现的,直流电机的原理图如下所示,T2 和 T3 为 P-Mos 管,T1 为 N-Mos 管。当 D1 输出高电平时,T2 阻断,风扇不转;当 D1 输出低电平时,T2 通路,风扇开始转动。

![图片](/images/lora-doc/image281.png)

### 功能配置及源码分析

配置 PB7 为输出模式,连接风扇直流电机。

```c
/* gpio.c */
void D1_OUT_GPIO_Init(void)
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.Pin = GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
```text

配置直流电机代码如下:

```c
#include "gpio.h"
#include "fan.h"
#include <stdbool.h>
static uint8_t FanStaus = false;
// 开启风扇
void FanOn(void){
  HAL_GPIO_WritePin( FAN_GPIO_PORT, FAN_PIN, FAN_ON );
  FanStaus = true;
}
// 关闭风扇
void FanOff(void){
  HAL_GPIO_WritePin( FAN_GPIO_PORT, FAN_PIN, FAN_OFF );
  FanStaus = false;
}
// 读取风扇状态
uint8_t FanReadStaus( void ){
  return FanStaus;
}
在 main.c 里面配置每隔五秒开启一次风扇。
/* main.c */
...
while(1){
  ...
#if defined(MPU6050)
  mpu6050_ReadData(&Mx,&My,&Mz);
  printf("Mx = %d\\n",Mx);
  printf("My = %d\\n",My);
  printf("Mz = %d\\n",Mz);
#elif defined(DHT11)
  DHT11_TEST();
  printf("TEMP = %d\\n",ucharT_data_H);
  printf("HUM = %d\\n",ucharRH_data_H);
#elif defined(FAN)
  FanOn();
  FanStaus = FanReadStaus();
  printf("Fan is On\\n");
  SLCD_Show();
  HAL_Delay(5000);
  FanOff();
  FanStaus = FanReadStaus();
  printf("Fan is Off\\n");
#endif
  ...
}
```text

实验现象:

每隔五秒风扇状态改变一次。(这里只是介绍了风扇驱动的基础功能,对于如何根据已有的温湿度数据甚至更多牧场环境数据来控制风扇开闭,大家可以根据实际需要完成功能的开发)

![图片](/images/lora-doc/image282.png)

## 项目集成开发

### 定时采集上传数据

第一步:修改 RTC 文件

在体征数据采集的步骤当中,我们使用 STM32cubeMX 开启了 IIC,这使得我们配置好的 RTC 时钟初始化被更改,所以在这里我们需要改回来。

```c
/* rtc.c */
#include "rtc.h"
#include "netprocess.h"
#include "string.h"
#include "dataprocess.h"
#define CLOCKHOURS 5
volatile uint8_t SendClockFlag = 0;
volatile uint8_t sendUpDataFlag = 0;
RTC_AlarmTypeDef gAlarm;
RTC_HandleTypeDef hrtc;
void MX_RTC_Init(void){
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
RTC_AlarmTypeDef sAlarm;
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = 125-1;
hrtc.Init.SynchPrediv = 2000-1;
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
if (HAL_RTC_Init(&hrtc) != HAL_OK){
  _Error_Handler(__FILE__, __LINE__);
}
sTime.Hours = startUpDateHours;
sTime.Minutes = startUpDateMinute;
sTime.Seconds = startUpDateSeconds;
sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
sTime.StoreOperation = RTC_STOREOPERATION_RESET;
if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BCD) != HAL_OK){
  _Error_Handler(__FILE__, __LINE__);
}
sDate.WeekDay = RTC_WEEKDAY_MONDAY;
sDate.Month = RTC_MONTH_APRIL;
sDate.Date = 0x1;
sDate.Year = 0x18;
if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BCD) != HAL_OK){
  _Error_Handler(__FILE__, __LINE__);
}
if(HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) != 0x32F2){
  HAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR0,0x32F2);
}
sAlarm.AlarmTime.Hours = DataUpTimeHours;
sAlarm.AlarmTime.Minutes = DataUpTimeMinute;
sAlarm.AlarmTime.Seconds = DataUpTimeSeconds;
sAlarm.AlarmTime.SubSeconds = DataUpTimeSubSeconds;
sAlarm.AlarmTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
sAlarm.AlarmTime.StoreOperation = RTC_STOREOPERATION_RESET;
sAlarm.AlarmMask = RTC_ALARMMASK_DATEWEEKDAY;
sAlarm.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_ALL;
sAlarm.AlarmDateWeekDaySel = RTC_ALARMDATEWEEKDAYSEL_DATE;
sAlarm.AlarmDateWeekDay = 0x1;
sAlarm.Alarm = RTC_ALARM_A;
memcpy(&gAlarm, &sAlarm, sizeof(sAlarm));
if (HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN) != HAL_OK){
  _Error_Handler(__FILE__, __LINE__);
}
}
void HAL_RTC_MspInit(RTC_HandleTypeDef* rtcHandle){
if(rtcHandle->Instance==RTC){
  __HAL_RCC_RTC_ENABLE();
  HAL_NVIC_SetPriority(RTC_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(RTC_IRQn);
}
}
void HAL_RTC_MspDeInit(RTC_HandleTypeDef* rtcHandle){
if(rtcHandle->Instance==RTC){
  __HAL_RCC_RTC_DISABLE();
  HAL_NVIC_DisableIRQ(RTC_IRQn);
}
}
//闹钟事件回调函数
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc){
RTC_TimeTypeDef masterTime;
RTC_TimeTypeDef SlaveTime;
RTC_DateTypeDef masterDate;
#if MASTER
//置位同步时钟标志
  SendClockFlag = 0;
//获取下次闹钟时间
  HAL_RTC_GetTime(hrtc, &masterTime, RTC_FORMAT_BIN);
  HAL_RTC_GetDate(hrtc, &masterDate, RTC_FORMAT_BIN);
  gAlarm.AlarmTime.Hours = masterTime.Hours + CLOCKHOURS;
  gAlarm.AlarmTime.Minutes = masterTime.Minutes;
  gAlarm.AlarmTime.Seconds = masterTime.Seconds;
  gAlarm.AlarmTime.SubSeconds = masterTime.SubSeconds;
#else
  sendUpDataFlag = 1;
  HAL_RTC_GetTime(hrtc, &SlaveTime, RTC_FORMAT_BIN);
  HAL_RTC_GetDate(hrtc, &masterDate, RTC_FORMAT_BIN);
  gAlarm.AlarmTime.Hours = SlaveTime.Hours + DataUpTimeHours;
  gAlarm.AlarmTime.Minutes = SlaveTime.Minutes + DataUpTimeMinute;
  gAlarm.AlarmTime.Seconds = SlaveTime.Seconds + DataUpTimeSeconds;
  gAlarm.AlarmTime.SubSeconds = SlaveTime.SubSeconds + DataUpTimeSubSeconds;
#endif
  if (gAlarm.AlarmTime.Seconds > 59){
     gAlarm.AlarmTime.Seconds -= 60;
     gAlarm.AlarmTime.Minutes += 1;
  }
 if ( gAlarm.AlarmTime.Minutes >59){
     gAlarm.AlarmTime.Minutes -= 60;
     gAlarm.AlarmTime.Hours += 1;
 }
 if (gAlarm.AlarmTime.Hours > 23){
     gAlarm.AlarmTime.Hours -= 24;
 }
 printf("RTC\\n");
//使能闹钟中断
 if (HAL_RTC_SetAlarm_IT(hrtc, &gAlarm, RTC_FORMAT_BIN) != HAL_OK){
    _Error_Handler(__FILE__, __LINE__);
  }
}
//时分秒转换
void GetTimeHMS(uint32_t timeData,uint8_t *hours,uint8_t *minute,uint8_t *seconds,uint32_t *subSeconds) {
  /* 获得亚秒 */
  *subSeconds = timeData % 1000;
  /* 获得秒钟*/
  timeData = timeData / 1000;
  *seconds = timeData % 60;
  /* 获得分钟*/
  timeData = timeData / 60;
  *minute = timeData % 60;
  /* 获得小时 */
  *hours = timeData / 60;
}
```text

第二步:修改传感器定时上传函数。

在之前完成的代码当中,我们发送的数据是固定的数据内容,这个函数是 dataprocess.c 文件当中的 SendSensorDataUP() 函数,现在我们需要将它修改为发送传感器的数据包。

这就涉及到网络数据包的解包过程了。

| 网络数据包 |   |   |   |   |
| --- | --- | --- | --- | --- |
| 名称 | 字节数 | 描述 | 举例 |   |
| 帧头 | 1 | 字符 代表网络数据包 | N |   |
| 网络标识符 | 2 | 用于网络区分,只有 PANID 一样才可以组网通信 | 0x0102 |   |
| 数据包 | 包头 | 1 | 0x21 | 0x21 |
| 包长 | 1 | 数据包长度,代表数据域数据长度 | 1 |   |
| 数据类型 | 1 | 0x00:数据、0x01:命令 | 0x00 |   |
| 设备地址 | 2 | 设备标识符 | 0x0001 |   |
| 传感器类型 | 1 | 0x01:温湿度、0x02:三轴、0x03:水表、0x04:地磁、0x05:灌溉 | 0x01 |   |
| 数据 | 4 | 每种传感器数值用两个字节标识,比如温湿度占四个字节 | 0x01298113 |   |
| CRC8 校验 | 1 | 数据包校验,整个数据包,除校验位 | 0x08 |   |

```c
/* dataprocess.c */
// 上传节点传感器数据
void SendSensorDataUP(void){
printf("SendSensorDataUP\\n");
#if defined(MPU6050)
  mpu6050_ReadData(&Mx,&My,&Mz);
  printf("Mx = %.3f\\n",Mx);
  printf("My = %3f\\n",My);
  printf("Mz = %3f\\n",Mz);
  DataPacke_t.netmsgHead = 'N';
  DataPacke_t.netPanid[0] = HI_UINT16(PAN_ID);
  DataPacke_t.netPanid[1] = LO_UINT16(PAN_ID);
  DataPacke_t.msgHead = 0x21;
  DataPacke_t.dataLength = 0x08;
  DataPacke_t.dataType = 0;
  DataPacke_t.deviceAddr[0] = HI_UINT16(ADDR);
  DataPacke_t.deviceAddr[1] = LO_UINT16(ADDR);
  DataPacke_t.sensorType  = 0x2;
  DataPacke_t.buff[0]  = (int8_t)(Mx*10);
  DataPacke_t.buff[1]  = (int8_t)(My*10);
  DataPacke_t.buff[2]  = (int8_t)(Mz*10);
  //校验码
  DataPacke_t.crcCheck = crc8((uint8_t *)&DataPacke_t,DataPacke_t.dataLength + 4);
  //发送数据包
  Radio->SetTxPacket((uint8_t *)&DataPacke_t, DataPacke_t.dataLength + 5);
#elif defined(DHT11)
  DHT11_TEST();
  printf("TEMP = %d\\n",ucharT_data_H);
  printf("HUM = %d\\n",ucharRH_data_H);
  DataPacke_t.netmsgHead = 'N';
  DataPacke_t.netPanid[0] = HI_UINT16(PAN_ID);
  DataPacke_t.netPanid[1] = LO_UINT16(PAN_ID);
  DataPacke_t.msgHead = 0x21;
  DataPacke_t.dataLength = 0x07;
  DataPacke_t.dataType = 0;
  DataPacke_t.deviceAddr[0] = HI_UINT16(ADDR);
  DataPacke_t.deviceAddr[1] = LO_UINT16(ADDR);
  DataPacke_t.sensorType  = 0x1;
  DataPacke_t.buff[0]  = ucharT_data_H;
  DataPacke_t.buff[1]  = ucharRH_data_H;
  //校验码
  DataPacke_t.crcCheck = crc8((uint8_t *)&DataPacke_t,DataPacke_t.dataLength + 4);
  //发送数据包
  Radio->SetTxPacket((uint8_t *)&DataPacke_t, DataPacke_t.dataLength + 5);
#elif defined(FAN)
  FanStaus = FanReadStaus();
  DataPacke_t.netmsgHead = 'N';
  DataPacke_t.netPanid[0] = HI_UINT16(PAN_ID);
  DataPacke_t.netPanid[1] = LO_UINT16(PAN_ID);
  DataPacke_t.msgHead = 0x21;
  DataPacke_t.dataLength = 0x06;
  DataPacke_t.dataType = 0;
  DataPacke_t.deviceAddr[0] = HI_UINT16(ADDR);
  DataPacke_t.deviceAddr[1] = LO_UINT16(ADDR);
  DataPacke_t.sensorType  = 0x3;
  DataPacke_t.buff[0]  = FanStaus;
  //校验码
  DataPacke_t.crcCheck = crc8((uint8_t *)&DataPacke_t,DataPacke_t.dataLength + 4);
  //发送数据包
  Radio->SetTxPacket((uint8_t *)&DataPacke_t, DataPacke_t.dataLength + 5);
#endif
}
```text

其基本的原理也并不复杂,就是根据上面的表格,按照数据包的每一帧进行判断,判断是什么数据对应实现不同的功能即可。例如返回的数据第九个数据帧是 0x02,根据表格,我们知道这个传感器是温湿度传感器的数据。

### 实时接收控制指令

第三步:修改从机协议解析函数。

在原来编写好的从机协议解析函数 SlaveProtocolAnalysis() 当中,我们实现了将收到的数据打印出来,同时也实现了对网络数据包前面几帧的解包操作。现在我们需要实现发送指定命令,就开启和关闭风扇的功能,那么这里同样涉及解包的代码操作。

```c
/* dataprocess.c */
//从机协议解析
uint8_t SlaveProtocolAnalysis(uint8_t *buff,uint8_t len){
uint8_t Crc8Data;
printf("SlaveProtocolAnalysis\\n");
for (int i = 0; i < len; i++){
  printf("0x%x  ",buff[i]);
}
printf("\\n");
if (buff[0] == NETDATA){
  if (buff[1] == HI_UINT16(PAN_ID) && buff[2] == LO_UINT16(PAN_ID)){
    Crc8Data = crc8(&buff[0], len - 1);
    if (Crc8Data != buff[len - 1]){
      memset(buff, 0, len);
      return 0;
    }
    if (buff[3] == 0x21){
      printf("Slave_NETDATA\\n");
      if(buff[5] == 0x1){
         if (buff[6] == HI_UINT16(ADDR) && buff[7] == LO_UINT16(ADDR)){
            if(buff[8] == 0x3){
#if defined(FAN)
              if(buff[9] == true){
                FanOn();
              }
              else{
                FanOff();
              }
#endif
            }
         }
      }
    }
    return 0;
  }
}
else if((buff[0] == 0x3C) && (buff[2] == 'A')){
  if (DataCrcVerify(buff, len) == 0){
    return 0;
  }
  if (buff[3] == HI_UINT16(PAN_ID) && buff[4] == LO_UINT16(PAN_ID)){
    if (buff[5] == jionPacke_t.deviceAddr[0] && buff[6] == jionPacke_t.deviceAddr[1]){
      slaveNativeInfo_t.deviceId = buff[7];
      printf("Slave_ACK\\n");
      return 0xFF;
    }
  }
}
else if((buff[0] == 0x3C) && (buff[2] == 'T')){
  if (DataCrcVerify(buff, len) == 0){
    return 0;
  }
  if (buff[3] == HI_UINT16(PAN_ID) && buff[4] == LO_UINT16(PAN_ID)){
    uint32_t alarmTime = 0;
    startUpTimeHours = buff[5];
    startUpTimeMinute = buff[6];
    startUpTimeSeconds = buff[7];
    startUpTimeSubSeconds = buff[8] <<8 | buff[9];
    printf("Slave_CLOCK\\n");
    printf("H:%d,M:%d,S:%d,SUB:%d\\n", startUpTimeHours, startUpTimeMinute, startUpTimeSeconds, startUpTimeSubSeconds);
    alarmTime = ((DataUpTimeHours * 60 + DataUpTimeMinute) * 60
                 + DataUpTimeSeconds) * 1000 + (DataUpTimeSubSeconds / 2) + DataUpTime;
    GetTimeHMS(alarmTime, &DataUpTimeHours, &DataUpTimeMinute, &DataUpTimeSeconds, &DataUpTimeSubSeconds);
    printf("DataUpTime->H:%d,M:%d,S:%d,SUB:%d\\n", DataUpTimeHours, DataUpTimeMinute, DataUpTimeSeconds, DataUpTimeSubSeconds);
    //使能RTC
    MX_RTC_Init();
   return 0xFF;
  }
}
return 1;
}

实现效果:发送命令"4E 12 03 21 06 01 12 02 03 01 75",风扇运行;

发送命令"4E 12 03 21 06 01 12 02 03 00 72",风扇停止。

图片

图片

如何确定最后的校验位数字?由于使用的是 CRC8 校验,所以我们可以使用软件来生成 CRC 校验码。

图片

LoRa 物联网系统设计 ​

如何规划中小型 LoRa 物联网系统 ​

在上一节的课程当中,我们动手完成了智慧牧场项目的集成开发,相信大家对整个 LoRa 项目开发过程有了初步的了解,那么这一节内容,我们就来好好梳理一下一个中小型 LoRa 物联网系统的设计都需要做哪些准备、需要考虑哪些问题。

为什么我们思考的是规划中小型的 LoRa 物联网系统?因为基于时分复用的私有协议只能做中小型的 LoRa 物联网系统,大型的需要基于 LoRaWAN 协议。

通讯距离——最远的节点与网关之间的通信距离 ​

在发射功率、通信速率和天线相同的条件下,通信距离依赖于地形和环境的因素,所以在设计规划 LoRa 物联网系统的时候需要确定好通信所处的环境情况。

图片

高空气球通信距离可达 40KM

图片

铁塔通信距离可达 20KM

图片

空旷地通信距离 5KM

那么我们如何增加通信距离呢?

1、增加发射功率。就 SX1278 芯片来说,它最大支持 30dBm 的全功率运行。

2、降低通信速率。

3、更换高增益天线。较好的增益天线可以增加 3dBm。

4、增加网关、集中器。

节点功耗 ​

LoRa 节点一般用于户外,使用电池供电。针对电池供电的终端设备,我们需要通过它的工作模式、各个模式下的工作电流以及各个模式下的工作时长来合理计算 LoRa 节点的功耗情况,这样才能保证 LoRa 节点具备一定的使用寿命。

例:

LoRaWAN 终端,大约 10 分钟发送一次数据帧,约 1000ms;按协议,发送完毕后,1 秒内能唤醒,侦听时长为 160ms,接收时长为 1000ms;其他时间都处于 Sleep 休眠。以 10 分钟(600 秒)为单位。

工作模式工作电流(mA)发送周期(min)工作时长(s)功耗平均功耗(mA)电池容量(mA/H)使用天数使用年数
休眠0.001610600-1.6-1 = 597.40.0016×597.4 = 0.95584(0.95584+20.8+88)/600 = 0.1829220002000/0.18292/24 = 455.557455.557/365 = 1.2481
接收13-1.613×1.6 = 20.8----
发送88-188×1 = 88----

LoRa 官方也提供给我们计算 LoRa 节点功耗的软件,方便我们在设计之初对 LoRa 的一些参数的配置:

图片

在扩频因子设置为 12,负载长度为 8 字节,中心频率设置为 470000000(SX1278 芯片的固定频率),发射功率调整为 20dBm。计算后可以得到 8 个字节发送数据的时间为 190.46ms,发送电流是 125mA,接收电流是 13mA,睡眠电流为 100nA。

图片

在通信距离可靠的情况下,我们把发射功率改为 18,可以发现信号强度降低了 1dBm,发送电流也降低了 20mA,说明功耗降低的同时,信号强度也会削弱。

图片

这时候我们把扩频因子调整为 6。可以看到发送数据的时间大大缩短,同时信号的强度也较大程度地被削弱。也正是因为扩频因子越小,信号增益越差,而发送的数据量越少。

图片

网络规模 ​

网络规模与通信速率有直接关系。

按照 LoRaWan class A 计算
网络发送周期(min)单个节点发送时长(s)单个节点接收时长(s)节点数量
10.111min / (0.1s+1s) = 54.54545455

假设单个网关每天最多可以接收 a 个数据包,每个节点的应用发包频率是每小时 b 个数据包的话,那么,单个网关最多可以容纳的节点的数目的理论值的计算式如下:

图片

我们可以看到,设计中小型的 LoRa 物联网系统,我们主要关注三个点:通讯距离、节点功耗以及网络规模。接下来我们结合上面的理论知识,再来分析一些生活中常见的 LoRa 物联网系统设计的案例。

LoRa 智能抄表系统开发 ​

智能抄表解决方案 ​

智能抄表系统架构图如下图所示。基本流程是:LoRa 水表收集数据,上传到 LoRa 网关,然后 LoRa 网关将数据信息上传到 LoRa 云,最后到用户的应用层面。

图片

水表工作原理 ​

水流从表壳进水口切向冲击叶轮使之旋转,然后通过齿轮减速机构连续记录叶轮的转数,从而记录流经水表的累积流量。

旋翼式多流束水表的工作原理与单流束水表基本相同,它是通过叶轮盒的分配作用,将多束水流从叶轮盒的进水口切向冲击叶轮,使水流对叶轮的轴向冲击力得到平衡,减少了叶轮支承部分的磨损,并从结构上减少水表安装、结垢对水表误差的影响,总体性能明显高于单流束水表。

图片

常见的 LoRa 水表会在传统的水表指针上安装一个贴片,这可以是干簧管或者是霍尔传感器。将水流信号转换成电信号的原理就是通过指针上面的磁力让传感器接收到高低电平的信号。

图片

信号输出——每 10 升(或 100 升)发一次信号

瞬时流量——单位时间内流经封闭管道有效截面的流体量,单位:m³/h

累计流量——从水表开始计量的一直累计的值,单位:m³

智能抄表功能开发 ​

硬件设计:LoRa 水表的两端输出接开发板的 GPIO 引脚,引脚每次检测到由高电平到低电平跳跃,说明就有 10 升(或 100 升)水通过。可以通过模拟按键功能来实现。

图片

GPIO初始化

→配置引脚为输入模式

→配置中断源为上升沿(或下降沿)触发

→编写中断回调函数

瞬时流量计量

→计算两次上升沿(或下降沿)中断时长(s)

→转换为瞬时流量(m³/h)

累计流量

→全局记录中断出发次数,转换为m³

LoRa 智能停车系统开发 ​

智能停车解决方案 ​

智能停车系统架构图如下图所示。基本流程是:LoRa 地磁传感器收集数据,上传到 LoRa 网关,然后 LoRa 网关将数据信息上传到 LoRa 云,最后到用户的手机 APP 界面。

图片

地磁传感器工作原理 ​

地磁传感器可用于检测车辆的存在和车型识别。数据采集系统在交通监控系统中起着非常重要的作用,地磁传感器是数据采集系统的关键部分,传感器的性能对数据采集系统的准确性起决定作用。

图片

各向异性磁阻传感器由薄膜合金(透磁合金)制成,利用载流磁性材料在外部磁场存在时电阻特性将会改变的基本原理进行磁场变化的测量。当传感器接通以后,假设没有任何外部磁场,薄膜合金会有一个平行于电流方向的内部磁化矢量。

图片

常用的地磁传感器为飞思卡尔的 MAG3110,MAG3110 是一款小型,低功耗数字 3D 磁性传感器,具有宽动态范围,可在具有高外部磁场的 PCB 中运行。具有如下特性:

  • 测量局部磁场的成分,地磁场的总和以及电路板上的成分所产生的磁场;

  • 可以与 3 轴加速度计结合使用,从而可以获得与方向无关的精确指南针航向信息;

  • 具有标准的 I²C 串行接口,能够测量高达 10 高斯的局部磁场,输出数据速率高达 80 Hz。

图片

智能停车功能开发 ​

通过检测引脚是否为高电平判断车辆是否入驻。

GPIO初始化

  • 配置 D3&KEY 为输入模式

车辆状态读取

  • 读取 GPIO 状态,判断车辆入驻信息

LoRa 智能灌溉系统开发 ​

智能灌溉解决方案 ​

智能灌溉系统架构图如下图所示。

图片

电磁阀工作原理 ​

电磁阀从原理上分为三大类:直动式、分步直动式、先导式。而从阀瓣结构和材料上的不同与原理上的区别又分为六个分支小类:直动膜片结构、分步直动膜片结构、先导膜片结构、直动活塞结构、分步直动活塞结构、先导活塞结构。

直动式电磁阀

原理:通电时,电磁阀线圈产生电磁力把关闭件从阀座上提起,阀门打开; 断电时,电磁力消失,弹簧力把关闭件压在阀座上,阀门关闭。特点:在真空、 负压、零压时能正常工作,但通径一般不超过 25mm。

图片

分步直动式电磁阀

原理:它是一种直动和先导式相结合的原理,当入口与出口没有压差时,通电后,电磁力直接把先导小阀和主阀关闭件依次向上提起,阀门打开。当入口与出口达到启动压差时,通电后,电磁力先打开先导小阀,主阀下压力上升,上腔压力下降,从而利用弹簧力或介质压力推动关闭件,向下移动,使阀门关闭。特点:在零压或真空、高压时亦能可靠动作,但功率较大,要求必须水平安装。

图片

先导式电磁阀

原理:通电时,电磁力把先导孔打开,上腔室压力迅速下降,在关闭件周围形成上低下高的压差,流体压力推动关闭件移动,阀门打开;断电时,弹簧力把先导孔关闭,入口压力通过旁通孔迅速进入上腔室在关阀件周围形成下低上高的压差,流体压力推动关闭件向下移动,关闭阀门。特点:流体压力范围上限较高,可任意安装(需定制)但必须满足流体压差条件。

图片

智能灌溉功能开发 ​

D1 高电平,三极管导通,线圈通电,常开触点变常闭触点。

图片

总结 ​

开发物联网终端产品的关键要素:

  • 传感器开发

  • 无线模块开发

  • 单片机开发

  • 通信协议

  • 人机交互

Powered by VitePress