# FreeRTOS_LoRa_Environment_Assistant

## 项目概述：
**该项目为裸机开发“LoRa 2Nodes Demo”的升级版。**
不仅移植了FreeRTOS操作系统，而且完善了传输协议。减少了远程操控延迟的同时，保证项目的稳定运行。基于 STM32F103C8T6 最小系统板与正点原子 ATK-LORA 模块开发智能环境监测系统，构建双 LoRa 节点网络，通过 I2C、SPI、单总线等常用协议采集温湿度等传感器数据；采用定点组网模式与 LoRa 网关通信，结合 Wi-Fi 模块将数据推送至基于 MQTT 协议的 EMQX Broker 服务器；采用 FreeRTOS 进行任务调度优化，保障多线程数据采集与传输；在每一个节点通过 LVGL 图形库添加可视化界面，通过 OLED  显示屏实时显示数据信息和可操控设备；通过开发 Android App，集成实时数据可视化、远程设备控制、安全监控及异常报警等功能。
## 项目实现：
### 一、使用FreeRTOS操作系统：
**优点及作用：** 
可保证每个任务不会因为延迟被阻塞，大幅度减小远程操控的延迟，提高系统运行的稳定性。<br>
#### 环境监测节点：
**1、创建任务：**
创建了LED状态灯、温湿度检测、向网关节点传输数据、从网关节点接收数据和检测执行器指令控制LED灯五个任务。<br>
**2、创建消息队列：**
温湿度检测任务将采集到的温湿度数据通过队列发送到传输数据任务，实现数据的保护。<br>
**3、创建中断触发消息队列：**
创建串口触发消息队列，当串口3接收到消息触发中断后，将数据包传送到消息队列，同时创建一个任务用于接收消息队列，并用于后续的检测接收到的数据包是否完整、是否有相关指令。<br>
**4、创建二值信号量：**
创建检测LED指令消息队列，调用LoRa.c中检测数据包的函数，检测消息队列是否有相关的控制指令，若有相关的控制指令，则触发信号量；在其他任务中判断该信号量是否被触发，若被触发则控制LED灯开启，反之关闭。<br>
#### 网关节点：
**1、创建任务：**
创建了LED状态灯、Wi-Fi初始化、向子节点传输数据、从子节点接收数据、MQTT上发、MQTT下发6个任务。<br>

### 二、自定义传输协议：
自定义传输协议仿照了ModBus传输协议，其完善性相比有欠缺，但是可满足非大型项目的基本使用。在自定义传输协议中，使用十六进制数字进行传输，分别由网关节点和子节点。<br>
**1、节点：**<br>
**规则：**
子节点的传输协议主要分为帧头、传感器ID、传感器数据、执行器ID、执行器指令和帧尾。其中，帧头（也是节点ID），用于声明自己是哪个节点上发的数据；传感器ID，用于声明自己是什么传感器；传感器数据，如DHT11温湿度数据；执行器ID也是同理，用于声明自己是什么执行器；执行器指令，一般是0或1，0为关、1为开，用于在执行网关节点下发的控制指令后的返回结果，告诉网关节点是开了还是关了，是否成功，若失败了是什么原因；最后一个是帧尾，为帧头倒写，添加帧头和帧尾的目的是在网关接收多个节点的数据是验证其数据是否完整，不会被其他上传的数据打断。<br>
**节点ID：**
D1为子节点1，D2为子节点2，以此类推。<br>
**传感器ID：**
EA为DHT11温湿度传感器，EB为MQ2烟雾浓度传感器、EC为火焰传感器、ED为光敏传感器。<br>
**传感器数据：**
如17、35：17为温度数据、35为湿度数据，将其转化为10进制，分别为23℃、湿度53%。<br>
**执行器ID：**
FA为LED灯、FB为小风扇、FC为加湿器、FD为蜂鸣器、FE为舵机、FF为加热片。<br>
**执行器指令**
0为关、1为开。<br>
**帧尾：**
D1倒过来就是1D，以此类推。<br>

| 目标地址 | 目标信道 | 帧头（也是节点ID） | 传感器ID | 传感器数据 | 执行器ID | 执行器指令 | 帧尾（为帧头倒写） |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| 03 E9 | 17 | D1 | EA | 17 35 | FA | 01 | 1D |

**2、网关：**
**规则：**
网关的传输协议主要分为帧头、执行器ID、执行器指令和帧尾。其中，帧头也是节点ID，用于声明控制的是哪个节点的设备；执行器ID，同理一般是0或1，用于声明控制的是哪个设备；执行器指令用于声明执行器是开还是关；最后一个是帧尾，为帧头倒写。*节点ID、执行器ID与上面相同。*<br>
**执行器指令：**
一共有两种，一种是常规的0和1，另外一种是温控模式，用于控制温度范围，若超过这个范围就会触发执行器的指令。温控模式默认常开，控制字为02，后面跟的是温控的数据，也就是限定的温湿度数值，如17 20 35 50，就是温度在23~26度之间，湿度在53%~80%之间。

*常规模式*
| 目标地址 | 目标信道 | 帧头（也是节点ID）| 执行器ID | 执行器指令 | 帧尾（为帧头倒写） |
| :---: | :---: | :---: | :---: | :---: | :---: |
| 03 E9 | 17 | D1 | FA | 01 | 1D |

*温控模式*

| 目标地址 | 目标信道 | 帧头（也是节点ID）| 执行器ID | 执行器指令 | 温控模式02 | 温控数据 | 帧尾（为帧头倒写） |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| 03 E9 | 17 | D1 | FA | 01 | 02 | 17 20 35 50 | 1D |

## 本系统在FreeRTOS中引用的api：
### 一、多任务创建与运行
**1、概念：**
通过定义任务句柄、名称、优先级，从而实现多任务同时在单核的单片机中以并发或抢占的形式运行。在FreeRTOS中，时钟源频率通常被设定为1kHz(#define configTICK_RATE_HZ ( ( TickType_t ) 1000 ))，时间片1tick也就是1ms。在任务优先级相同的情况下，多个任务按照一个时间片切换运行，也就是轮询式调度；当其中一个任务的优先级较高时（数值较高），该任务会打断其他任务的运行，也就是抢占式调度。在创建任务后，会产生任务控制块，简称TCB，TCB在切换任务的时候，会将创建任务的指针保存到创建的栈里，也就是保存任务现场，直到任务切换过来后再复原现场。FreeRTOS的任务分为4个状态，分别为就绪态、运行态、挂起态和阻塞态，当其中一个任务遇到延时等情况，会进入到阻塞态，会执行其他任务知道延时结束，低优先级任务被高优先级任务打断也同理。<br>
**2、使用到的api函数：**
- 任务函数：
```c
void vTask(void *<参数>);
```
- 创建任务函数：
```c
TaskHandle_t xTaskHandler;
xTaskCreate(
           (TaskFunction_t        ) vTask,
           (char *                ) "<定义一个任务名称>",
           (configSTACK_DEPTH_TYPE) 1024,
           (void *                ) NULL, （若有参数则写参数名称）
           (UBaseType_t           ) <优先级，数字越大优先级越高>
           (TaskHandle_t *        ) &xTaskHandler);
```
- 启动任务调度函数：
```c
vTaskStartScheduler();
```
### 二、消息队列：
**1、概念：**
在初始化消息队列函数种，有两个参数，第一个是队列的长度,也就是这个队列能存放多少个数据；第二个参数就是每个数据的大小，单位为字节，在stm32中uint8_t指针的长度一般是4字节。在多个任务需要共用一个变量值以传递数据的时候，使用消息队列可以很好地保护数据的传输不会被其他用到相关变量的任务干扰。消息队列通过发送队列和接收队列的函数，将不同的任务搭建起一个传递参数的桥梁，从而确保传递数据时不被其他任务干扰，造成数据的误差。如DHT11接收到温湿度数据，将温湿度数据传输到LoRa发送数据的任务中。<br>
**2、使用到的api函数：**
- 创建队列句柄：
```c
QueueHandle_t xQueueHandler;
```
- 创建队列：
```c
xQueueHandler = xQueueCreate(
                            (UBaseType_t) <队列的长度，单位字节>,
                            (UBaseType_t) <每个数据的大小>);
```
- 发送队列，也就是将数据存入队列，等待接收，接收时长从0到portMAX_DELAY，如下所示。
```c
xQueueSend(xQueueHandler, <要传递的变量，指向其地址>, pdMS_TO_TICKS(10));
```
- 在中断中发送队列（在串口初始化中设置中断抢占优先级配置拉满，且需要设置上下文转换以免造成阻塞）：
```c
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(xQueueUsart3IrqHdlr, &ulRxData, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
```
- 接收队列，可接收普通和中断的队列：
```c
xQueueReceive(xQueueHandler, <要传递的变量，指向其地址>, portMAX_DELAY);
```
### 三、信号量：
**1、概念：**
信号量一般用于任务管理。常用的分为二值信号量、计数型信号量和互斥信号量3种信号量,其中二值信号量全称为二进制信号量，用于任务同步，类似于状态机，而后两者分别用于多资源和单资源的管理，资源管理就相当于停车场，多资源管理相当于停车场的多个车位，若停车位停满车就不能停车了，单资源同理。这三种信号量都只有0或1两种值。使用信号量的过程，以二值信号量为例，为创建二值信号量->释放二值信号量->获取二值信号量。释放二值信号量共有两个函数，释放任务中信号量和释放中断中信号量。信号量和状态机类似，释放信号量就相当于改变标志位，而获取信号量相当于获取改变的标志位以决定是否改变执行器的状态。由于只有0和1两种值，因此适用于只有开和关两种状态的执行器。<br>
**2、使用到的api函数：**
- 动态创建二值信号量：
```c
SemaphoreHandle_t xSemaphoreHandler;
```
- 发送信号量：
```c
xSemaphoreGive(xSemaphoreHandler);
```
- 接收信号量：（接收句柄、等待时长）
```c
if (xSemaphoreTake(xSemaphoreHandler, pdMS_TO_TICKS(10)) == pdTRUE);
```