编程技术分享

分享编程知识,探讨技术创新

0%

简介:安信可科技AiPi-LoRaBoard AiPi-UNO-ET485扩展板,支持节点数据显示,支持LoRa 协议自组网。

好的,作为一名高级嵌入式软件开发工程师,我将针对安信可科技AiPi-LoRaBoard和AiPi-UNO-ET485扩展板项目,详细阐述最适合的代码设计架构,并提供具体的C代码实现。这个项目旨在构建一个可靠、高效、可扩展的嵌入式系统平台,用于LoRa自组网节点数据的采集、显示和管理。
关注微信公众号,提前获取相关推文

项目需求分析

在深入代码架构之前,我们首先要明确项目的需求,这对于后续的设计至关重要。基于项目简介和图片,我们可以总结出以下关键需求:

  1. 硬件平台:

    • 安信可科技AiPi-LoRaBoard (主控板,通常基于ESP32或其他MCU)
    • AiPi-UNO-ET485扩展板 (提供ET485接口,但主要功能是LoRa)
    • LCD 显示屏 (用于节点数据可视化)
    • LoRa 模块 (用于无线通信)
    • 可能包含传感器 (例如温度、湿度、PM2.5等,图中显示Temp和PM2.5)
    • 用户交互按钮 (图中按钮用于操作或配置)
  2. 功能需求:

    • 节点数据采集: 从本地传感器(或通过ET485扩展,但此处重点是LoRa节点)采集数据,例如温度、湿度、PM2.5等环境参数。
    • LoRa 自组网: 支持LoRa协议,实现设备自组网,形成一个无线传感器网络。
    • 节点数据传输: 通过LoRa网络将采集到的数据传输到中心节点(或网关),并能接收来自中心节点的指令。
    • 节点数据显示: 在本地LCD屏幕上实时显示采集到的节点数据,包括节点ID、传感器类型、数值等。
    • 用户交互: 通过按钮进行简单的用户交互,例如切换显示节点、配置参数等。
    • 系统管理: 具备基本的系统管理功能,例如节点注册、状态监控、错误处理、低功耗管理等。
    • 维护升级: 预留固件升级接口,方便后续维护和功能扩展。
  3. 性能需求:

    • 可靠性: 系统需要稳定可靠运行,数据传输准确无误,网络连接稳定。
    • 高效性: 代码执行效率高,资源占用低,响应速度快,尤其是在数据采集和LoRa通信方面。
    • 可扩展性: 系统架构应具有良好的可扩展性,方便后续增加节点数量、传感器类型、功能模块等。
    • 低功耗: 考虑到嵌入式设备通常采用电池供电,系统需要具备低功耗特性,延长续航时间。

代码设计架构:分层架构与模块化设计

为了满足上述需求,并构建一个可靠、高效、可扩展的系统平台,我推荐采用分层架构模块化设计相结合的代码架构。这种架构模式在嵌入式系统开发中非常成熟和有效,能够将复杂的系统分解为多个独立的、易于管理和维护的模块,同时提高代码的复用性和可移植性。

架构层次划分

我们的系统可以划分为以下几个层次:

  1. 硬件抽象层 (HAL - Hardware Abstraction Layer):

    • 功能: 直接与硬件交互,提供统一的硬件接口,屏蔽底层硬件的差异性。
    • 模块: GPIO 驱动、SPI 驱动、I2C 驱动、UART 驱动、LoRa 模块驱动、LCD 驱动、传感器驱动、定时器驱动、ADC 驱动、电源管理驱动等。
    • 优点: 提高代码的可移植性,方便更换硬件平台,降低上层应用的开发难度。
  2. 板级支持包 (BSP - Board Support Package):

    • 功能: 在 HAL 层之上,提供特定硬件平台相关的初始化和配置,例如时钟配置、中断管理、内存管理等。
    • 模块: 系统初始化模块、时钟管理模块、中断管理模块、内存管理模块、启动代码等。
    • 优点: 针对特定硬件平台进行优化,确保系统能够正确运行,并提供必要的系统级服务。
  3. 操作系统层 (OSAL - Operating System Abstraction Layer) (可选,但推荐):

    • 功能: 如果使用 RTOS(实时操作系统),OSAL 层可以提供统一的操作系统接口,例如任务管理、线程同步、消息队列、定时器等,屏蔽不同 RTOS 的差异性。
    • 模块: 任务管理模块、线程同步模块 (互斥锁、信号量、事件标志组)、消息队列模块、定时器模块等。
    • 优点: 提高代码的跨 RTOS 可移植性,简化多任务并发编程,提高系统的实时性和响应速度。 如果项目简单,也可以不使用 RTOS,采用简单的事件循环或协作式调度。
  4. 通信协议层 (Communication Protocol Layer):

    • 功能: 负责处理 LoRa 通信协议,包括数据包的封装、解封装、加密、解密、网络管理、路由等。
    • 模块: LoRa 协议栈模块 (例如 LoRaWAN 或简化版 LoRa 协议)、数据链路层模块、网络层模块、安全模块等。
    • 优点: 实现设备之间的可靠通信,支持自组网功能,确保数据传输的完整性和安全性。
  5. 数据处理层 (Data Processing Layer):

    • 功能: 负责处理采集到的传感器数据,例如数据解析、数据校验、数据转换、数据滤波、数据存储等。
    • 模块: 传感器数据解析模块、数据校验模块、数据转换模块 (单位转换)、数据滤波模块 (例如移动平均滤波)、数据存储模块 (如果需要本地存储)。
    • 优点: 将原始传感器数据处理成可用的、有意义的信息,提高数据质量和应用价值。
  6. 应用逻辑层 (Application Logic Layer):

    • 功能: 实现系统的核心业务逻辑,例如节点数据采集、LoRa 数据发送和接收、节点数据管理、用户交互逻辑、系统配置管理、电源管理等。
    • 模块: 节点管理模块、数据采集任务模块、LoRa 通信任务模块、显示管理模块、用户界面模块、配置管理模块、电源管理模块、错误处理模块等。
    • 优点: 实现系统的具体功能,满足用户需求,是整个系统的核心部分。
  7. 表示层 (Presentation Layer) / 用户界面层 (UI Layer):

    • 功能: 负责用户界面的显示和交互,例如 LCD 屏幕的驱动、数据显示、菜单显示、按钮事件处理等。
    • 模块: LCD 驱动模块、UI 元素管理模块 (例如窗口、文本框、按钮)、数据显示模块、用户输入处理模块等。
    • 优点: 提供友好的用户界面,方便用户查看数据和操作系统。

模块化设计

在每个层次内部,我们还需要采用模块化设计,将功能进一步细分到更小的模块中。每个模块负责完成特定的功能,模块之间通过清晰定义的接口进行交互。模块化设计的优点包括:

  • 高内聚低耦合: 模块内部功能高度相关,模块之间依赖性低,方便模块的独立开发、测试和维护。
  • 代码复用性: 模块可以被多个项目或系统复用,减少重复开发工作。
  • 易于维护和升级: 修改或升级某个模块不会影响到其他模块,降低维护成本和风险。
  • 团队协作开发: 不同开发人员可以并行开发不同的模块,提高开发效率。

C 代码实现示例 (伪代码 + 关键代码片段,总计超过3000行代码框架)

为了详细说明架构和模块,并提供具体的C代码实现,以下将按照层次结构,给出每个层次和模块的伪代码框架和关键代码片段。由于实际项目代码量庞大,这里不可能提供完整的3000行可编译代码,但会尽可能详细地展示代码结构、接口定义和关键算法,帮助您理解代码架构和实现思路。

1. 硬件抽象层 (HAL)

  • hal_gpio.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef HAL_GPIO_H
#define HAL_GPIO_H

typedef enum {
GPIO_MODE_INPUT,
GPIO_MODE_OUTPUT
} gpio_mode_t;

typedef enum {
GPIO_LEVEL_LOW,
GPIO_LEVEL_HIGH
} gpio_level_t;

typedef uint32_t gpio_pin_t; // 例如,使用位掩码表示 GPIO 引脚

// 初始化 GPIO 引脚
void hal_gpio_init(gpio_pin_t pin, gpio_mode_t mode);

// 设置 GPIO 输出电平
void hal_gpio_write(gpio_pin_t pin, gpio_level_t level);

// 读取 GPIO 输入电平
gpio_level_t hal_gpio_read(gpio_pin_t pin);

#endif // HAL_GPIO_H
  • hal_gpio.c (示例,针对特定 MCU,例如 ESP32):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "hal_gpio.h"
#include "esp_gpio.h" // ESP-IDF 的 GPIO 驱动头文件 (假设使用 ESP32)

void hal_gpio_init(gpio_pin_t pin, gpio_mode_t mode) {
gpio_config_t io_conf;
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.pin_bit_mask = pin; // 使用位掩码
if (mode == GPIO_MODE_OUTPUT) {
io_conf.mode = GPIO_MODE_OUTPUT;
} else {
io_conf.mode = GPIO_MODE_INPUT;
}
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
gpio_config(&io_conf);
}

void hal_gpio_write(gpio_pin_t pin, gpio_level_t level) {
gpio_set_level(pin, (level == GPIO_LEVEL_HIGH) ? 1 : 0);
}

gpio_level_t hal_gpio_read(gpio_pin_t pin) {
return (gpio_get_level(pin) == 1) ? GPIO_LEVEL_HIGH : GPIO_LEVEL_LOW;
}
  • 类似地,需要实现 hal_spi.h/c, hal_i2c.h/c, hal_uart.h/c, hal_lora.h/c, hal_lcd.h/c, hal_sensor.h/c, hal_timer.h/c, hal_adc.h/c 等 HAL 模块,提供统一的硬件操作接口。 例如 hal_lora.h 可能会包含 LoRa 模块的初始化、发送数据、接收数据、设置频率、设置功率等函数。hal_lcd.h 包含 LCD 的初始化、清屏、显示字符、显示字符串、显示图形等函数。hal_sensor.h 包含各种传感器的初始化、读取数据函数。

2. 板级支持包 (BSP)

  • bsp.h:
1
2
3
4
5
6
7
8
#ifndef BSP_H
#define BSP_H

void bsp_init(); // 系统初始化
void bsp_delay_ms(uint32_t ms); // 毫秒级延时
uint32_t bsp_get_tick_ms(); // 获取系统运行时间 (毫秒)

#endif // BSP_H
  • bsp.c (示例,针对 AiPi-LoRaBoard,假设基于 ESP32):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "bsp.h"
#include "freertos/FreeRTOS.h" // 如果使用 FreeRTOS
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_spi_flash.h"
#include "nvs_flash.h"

void bsp_init() {
// 初始化 NVS (Non-Volatile Storage) 用于存储配置信息
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);

// 初始化其他系统资源,例如时钟、中断控制器等 (ESP-IDF 会在启动时自动初始化大部分)
// ...

// 初始化 HAL 层
// ... (例如初始化 GPIO 驱动、SPI 驱动等,根据实际硬件连接配置)
}

void bsp_delay_ms(uint32_t ms) {
vTaskDelay(ms / portTICK_PERIOD_MS); // 使用 FreeRTOS 延时
}

uint32_t bsp_get_tick_ms() {
return esp_timer_get_time() / 1000; // 使用 ESP-IDF 的高精度定时器
}

3. 操作系统层 (OSAL) (示例,简化版,不依赖特定 RTOS)

  • osal.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef OSAL_H
#define OSAL_H

typedef void (*osal_task_func_t)(void* param);

typedef struct {
osal_task_func_t task_func;
void* task_param;
uint32_t task_period_ms; // 任务执行周期 (毫秒)
uint32_t last_exec_time_ms; // 上次执行时间
} osal_task_t;

void osal_task_create(osal_task_t* task);
void osal_task_schedule(); // 任务调度器

#endif // OSAL_H
  • osal.c (简单协作式调度器示例):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "osal.h"
#include "bsp.h" // 使用 bsp_get_tick_ms 和 bsp_delay_ms

#define MAX_TASKS 10
osal_task_t tasks[MAX_TASKS];
uint8_t task_count = 0;

void osal_task_create(osal_task_t* task) {
if (task_count < MAX_TASKS) {
tasks[task_count++] = *task;
}
}

void osal_task_schedule() {
while (1) {
uint32_t current_time_ms = bsp_get_tick_ms();
for (int i = 0; i < task_count; i++) {
if (current_time_ms - tasks[i].last_exec_time_ms >= tasks[i].task_period_ms) {
tasks[i].task_func(tasks[i].task_param);
tasks[i].last_exec_time_ms = current_time_ms;
}
}
bsp_delay_ms(10); // 调度周期,例如 10ms
}
}

4. 通信协议层 (LoRa 协议层,简化示例)

  • lora_protocol.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#ifndef LORA_PROTOCOL_H
#define LORA_PROTOCOL_H

#include <stdint.h>

#define LORA_NODE_ID_LEN 2 // 节点 ID 长度 (字节)
#define LORA_PAYLOAD_MAX_LEN 64 // 最大 payload 长度

typedef struct {
uint8_t dest_addr[LORA_NODE_ID_LEN]; // 目的节点地址 (广播地址全 0)
uint8_t src_addr[LORA_NODE_ID_LEN]; // 源节点地址
uint8_t payload_len;
uint8_t payload[LORA_PAYLOAD_MAX_LEN];
} lora_packet_t;

// 初始化 LoRa 协议栈
void lora_protocol_init();

// 发送 LoRa 数据包
bool lora_protocol_send_packet(lora_packet_t* packet);

// 接收 LoRa 数据包 (非阻塞)
bool lora_protocol_receive_packet(lora_packet_t* packet);

// 设置节点 LoRa 地址
void lora_protocol_set_local_addr(const uint8_t addr[LORA_NODE_ID_LEN]);

// 获取节点 LoRa 地址
void lora_protocol_get_local_addr(uint8_t addr[LORA_NODE_ID_LEN]);

#endif // LORA_PROTOCOL_H
  • lora_protocol.c (简化 LoRa 协议实现,假设使用 SX1276/SX1278 LoRa 芯片):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include "lora_protocol.h"
#include "hal_lora.h" // HAL 层 LoRa 驱动
#include "bsp.h"

#define LOCAL_LORA_ADDR_KEY "lora_addr" // NVS 存储本地 LoRa 地址的 key

static uint8_t local_lora_addr[LORA_NODE_ID_LEN] = {0x00, 0x01}; // 默认地址

void lora_protocol_init() {
// 初始化 HAL 层 LoRa 驱动
hal_lora_init();

// 从 NVS 加载本地 LoRa 地址,如果不存在则使用默认地址并保存
// ... (使用 NVS 读取和保存地址的代码,此处省略,需要使用 ESP-IDF 的 NVS API)
// nvs_handle_t nvs_handle;
// esp_err_t err = nvs_open("config", NVS_READWRITE, &nvs_handle);
// if (err == ESP_OK) {
// size_t addr_len = LORA_NODE_ID_LEN;
// err = nvs_get_blob(nvs_handle, LOCAL_LORA_ADDR_KEY, local_lora_addr, &addr_len);
// if (err != ESP_OK) {
// // NVS 中不存在地址,保存默认地址
// err = nvs_set_blob(nvs_handle, LOCAL_LORA_ADDR_KEY, local_lora_addr, LORA_NODE_ID_LEN);
// ESP_ERROR_CHECK(err);
// err = nvs_commit(nvs_handle);
// ESP_ERROR_CHECK(err);
// }
// nvs_close(nvs_handle);
// }

hal_lora_set_frequency(433E6); // 设置 LoRa 频率 (例如 433MHz)
hal_lora_set_tx_power(20); // 设置发射功率 (例如 20dBm)
hal_lora_set_spreading_factor(7); // 设置扩频因子 (例如 7)
hal_lora_set_bandwidth(125E3); // 设置带宽 (例如 125kHz)
}

bool lora_protocol_send_packet(lora_packet_t* packet) {
// 填充源地址
memcpy(packet->src_addr, local_lora_addr, LORA_NODE_ID_LEN);

// 将数据包序列化为字节流 (简单的例子,实际应用中可能需要更复杂的封装)
uint8_t tx_buffer[LORA_PAYLOAD_MAX_LEN + LORA_NODE_ID_LEN * 2 + 1]; // 目的地址 + 源地址 + payload_len + payload
memcpy(tx_buffer, packet->dest_addr, LORA_NODE_ID_LEN);
memcpy(tx_buffer + LORA_NODE_ID_LEN, packet->src_addr, LORA_NODE_ID_LEN);
tx_buffer[LORA_NODE_ID_LEN * 2] = packet->payload_len;
memcpy(tx_buffer + LORA_NODE_ID_LEN * 2 + 1, packet->payload, packet->payload_len);

// 使用 HAL 层 LoRa 驱动发送数据
return hal_lora_send_data(tx_buffer, packet->payload_len + LORA_NODE_ID_LEN * 2 + 1);
}

bool lora_protocol_receive_packet(lora_packet_t* packet) {
uint8_t rx_buffer[LORA_PAYLOAD_MAX_LEN + LORA_NODE_ID_LEN * 2 + 1];
uint16_t rx_len;

if (hal_lora_receive_data_non_blocking(rx_buffer, &rx_len)) {
if (rx_len > LORA_NODE_ID_LEN * 2 + 1) { // 最小数据包长度
memcpy(packet->dest_addr, rx_buffer, LORA_NODE_ID_LEN);
memcpy(packet->src_addr, rx_buffer + LORA_NODE_ID_LEN, LORA_NODE_ID_LEN);
packet->payload_len = rx_buffer[LORA_NODE_ID_LEN * 2];
if (packet->payload_len <= LORA_PAYLOAD_MAX_LEN && packet->payload_len <= rx_len - (LORA_NODE_ID_LEN * 2 + 1)) {
memcpy(packet->payload, rx_buffer + LORA_NODE_ID_LEN * 2 + 1, packet->payload_len);
return true;
}
}
}
return false; // 没有收到有效数据包
}

void lora_protocol_set_local_addr(const uint8_t addr[LORA_NODE_ID_LEN]) {
memcpy(local_lora_addr, addr, LORA_NODE_ID_LEN);
// 保存到 NVS
// ... (使用 NVS 保存地址的代码)
}

void lora_protocol_get_local_addr(uint8_t addr[LORA_NODE_ID_LEN]) {
memcpy(addr, local_lora_addr, LORA_NODE_ID_LEN);
}

5. 数据处理层 (Data Processing Layer)

  • sensor_data_process.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef SENSOR_DATA_PROCESS_H
#define SENSOR_DATA_PROCESS_H

#include <stdint.h>

typedef struct {
float temperature; // 摄氏度
float humidity; // 百分比
float pm25; // μg/m³
} sensor_data_t;

// 解析传感器原始数据
bool sensor_data_parse(const uint8_t* raw_data, uint16_t raw_data_len, sensor_data_t* processed_data);

// 数据校验 (例如 CRC 校验,此处简化)
bool sensor_data_validate(const uint8_t* raw_data, uint16_t raw_data_len);

// 数据滤波 (例如移动平均滤波,此处简化)
void sensor_data_filter(sensor_data_t* data);

#endif // SENSOR_DATA_PROCESS_H
  • sensor_data_process.c (示例,假设传感器数据格式为:温度[2字节] + 湿度[2字节] + PM2.5[2字节] ):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "sensor_data_process.h"
#include <string.h>

bool sensor_data_parse(const uint8_t* raw_data, uint16_t raw_data_len, sensor_data_t* processed_data) {
if (raw_data_len != 6) { // 期望数据长度为 6 字节
return false; // 数据长度错误
}

// 假设温度和湿度是乘以 100 的整数值
int16_t temp_raw = (raw_data[0] << 8) | raw_data[1];
int16_t humidity_raw = (raw_data[2] << 8) | raw_data[3];
int16_t pm25_raw = (raw_data[4] << 8) | raw_data[5];

processed_data->temperature = (float)temp_raw / 100.0f;
processed_data->humidity = (float)humidity_raw / 100.0f;
processed_data->pm25 = (float)pm25_raw / 100.0f;

return true;
}

bool sensor_data_validate(const uint8_t* raw_data, uint16_t raw_data_len) {
// 简单的校验,例如检查数据长度,实际应用中可能需要 CRC 校验等
return raw_data_len > 0;
}

void sensor_data_filter(sensor_data_t* data) {
// 简单的滤波,例如移动平均滤波,此处省略具体实现
// ... (可以使用一个环形缓冲区存储历史数据,计算平均值)
}

6. 应用逻辑层 (Application Logic Layer)

  • node_manager.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#ifndef NODE_MANAGER_H
#define NODE_MANAGER_H

#include "lora_protocol.h"
#include "sensor_data_process.h"

#define MAX_NODES 6 // 最大节点数量 (图中显示 6 个节点)
#define NODE_DATA_UPDATE_INTERVAL_MS 5000 // 节点数据更新间隔 (5 秒)

typedef struct {
uint8_t node_addr[LORA_NODE_ID_LEN];
sensor_data_t last_sensor_data;
uint32_t last_update_time_ms;
bool online; // 节点是否在线
} node_info_t;

extern node_info_t node_list[MAX_NODES];
extern uint8_t node_count;

// 初始化节点管理器
void node_manager_init();

// 添加新节点
bool node_manager_add_node(const uint8_t node_addr[LORA_NODE_ID_LEN]);

// 更新节点数据
bool node_manager_update_node_data(const uint8_t node_addr[LORA_NODE_ID_LEN], const sensor_data_t* sensor_data);

// 获取节点信息
node_info_t* node_manager_get_node_info(const uint8_t node_addr[LORA_NODE_ID_LEN]);

// 节点管理任务 (定期检查节点状态,处理 LoRa 数据等)
void node_manager_task(void* param);

#endif // NODE_MANAGER_H
  • node_manager.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include "node_manager.h"
#include "osal.h"
#include "bsp.h"
#include <string.h>

node_info_t node_list[MAX_NODES];
uint8_t node_count = 0;
uint8_t local_node_addr[LORA_NODE_ID_LEN]; // 本地节点地址

void node_manager_init() {
lora_protocol_get_local_addr(local_node_addr); // 获取本地 LoRa 地址
memset(node_list, 0, sizeof(node_list));
node_count = 0;

// 创建节点管理任务
osal_task_t task;
task.task_func = node_manager_task;
task.task_param = NULL;
task.task_period_ms = 100; // 任务周期 100ms
osal_task_create(&task);
}

bool node_manager_add_node(const uint8_t node_addr[LORA_NODE_ID_LEN]) {
if (node_count < MAX_NODES) {
memcpy(node_list[node_count].node_addr, node_addr, LORA_NODE_ID_LEN);
node_list[node_count].online = false; // 初始状态为离线
node_count++;
return true;
}
return false; // 节点数量已满
}

bool node_manager_update_node_data(const uint8_t node_addr[LORA_NODE_ID_LEN], const sensor_data_t* sensor_data) {
for (int i = 0; i < node_count; i++) {
if (memcmp(node_list[i].node_addr, node_addr, LORA_NODE_ID_LEN) == 0) {
node_list[i].last_sensor_data = *sensor_data;
node_list[i].last_update_time_ms = bsp_get_tick_ms();
node_list[i].online = true; // 更新数据时标记为在线
return true;
}
}
return false; // 节点未找到
}

node_info_t* node_manager_get_node_info(const uint8_t node_addr[LORA_NODE_ID_LEN]) {
for (int i = 0; i < node_count; i++) {
if (memcmp(node_list[i].node_addr, node_addr, LORA_NODE_ID_LEN) == 0) {
return &node_list[i];
}
}
return NULL; // 节点未找到
}

void node_manager_task(void* param) {
lora_packet_t rx_packet;
while (1) {
// 接收 LoRa 数据包
if (lora_protocol_receive_packet(&rx_packet)) {
// 判断是否是数据包 (根据协议定义判断,此处简化)
if (rx_packet.payload_len > 0) {
sensor_data_t sensor_data;
if (sensor_data_parse(rx_packet.payload, rx_packet.payload_len, &sensor_data)) {
// 更新节点数据
node_manager_update_node_data(rx_packet.src_addr, &sensor_data);
}
}
}

// 定期检查节点在线状态 (例如,超过一定时间未收到数据则认为离线)
uint32_t current_time_ms = bsp_get_tick_ms();
for (int i = 0; i < node_count; i++) {
if (node_list[i].online && (current_time_ms - node_list[i].last_update_time_ms > NODE_DATA_UPDATE_INTERVAL_MS * 2)) {
node_list[i].online = false; // 超时未更新,标记为离线
}
}

bsp_delay_ms(100); // 任务周期 100ms
}
}
  • 类似地,需要实现 data_display.h/c, ui_manager.h/c, config_manager.h/c, power_manager.h/c, error_handler.h/c 等应用逻辑模块,实现系统的各种功能。 例如 data_display.c 负责将节点数据格式化后显示在 LCD 屏幕上,ui_manager.c 处理用户界面的逻辑,例如菜单切换、按钮事件响应等,config_manager.c 负责系统配置参数的加载和保存,power_manager.c 负责电源管理,例如低功耗模式切换等,error_handler.c 负责错误处理和日志记录。

7. 表示层 (UI Layer)

  • ui_display.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef UI_DISPLAY_H
#define UI_DISPLAY_H

#include "node_manager.h"

// 初始化 UI 显示
void ui_display_init();

// 更新节点数据显示
void ui_display_update_node_data(const node_info_t* node_info);

// 显示节点列表
void ui_display_show_node_list(const node_info_t* node_list, uint8_t node_count);

// 清屏
void ui_display_clear_screen();

// ... 其他 UI 显示函数,例如显示文本、显示图形等

#endif // UI_DISPLAY_H
  • ui_display.c (示例,假设使用 SPI 接口 LCD):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include "ui_display.h"
#include "hal_lcd.h" // HAL 层 LCD 驱动
#include <stdio.h>

#define NODE_DISPLAY_X_START 10
#define NODE_DISPLAY_Y_START 10
#define NODE_DISPLAY_WIDTH 80
#define NODE_DISPLAY_HEIGHT 60
#define NODE_DISPLAY_SPACING_X 10
#define NODE_DISPLAY_SPACING_Y 10

void ui_display_init() {
hal_lcd_init();
ui_display_clear_screen();
}

void ui_display_update_node_data(const node_info_t* node_info) {
// 计算节点显示位置 (假设节点 ID 从 1 开始)
uint8_t node_id = node_info->node_addr[1]; // 假设节点 ID 取地址的第二个字节
uint8_t row = (node_id - 1) / 3;
uint8_t col = (node_id - 1) % 3;
uint16_t x = NODE_DISPLAY_X_START + col * (NODE_DISPLAY_WIDTH + NODE_DISPLAY_SPACING_X);
uint16_t y = NODE_DISPLAY_Y_START + row * (NODE_DISPLAY_HEIGHT + NODE_DISPLAY_SPACING_Y);

char buffer[64];
sprintf(buffer, "Node %d\n", node_id);
hal_lcd_draw_string(x, y, buffer);

sprintf(buffer, "Temp: %.2fC\n", node_info->last_sensor_data.temperature);
hal_lcd_draw_string(x, y + 20, buffer);

sprintf(buffer, "PM2.5: %.2f\n", node_info->last_sensor_data.pm25);
hal_lcd_draw_string(x, y + 40, buffer);

if (!node_info->online) {
hal_lcd_draw_string(x, y + 50, "Offline");
}
}

void ui_display_show_node_list(const node_info_t* node_list, uint8_t node_count) {
ui_display_clear_screen();
for (int i = 0; i < node_count; i++) {
ui_display_update_node_data(&node_list[i]);
}
}

void ui_display_clear_screen() {
hal_lcd_clear_screen();
}

主程序 (main.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include "bsp.h"
#include "osal.h"
#include "lora_protocol.h"
#include "node_manager.h"
#include "ui_display.h"
#include "hal_sensor.h" // 假设有本地传感器驱动

void system_init_task(void* param); // 系统初始化任务
void sensor_collect_task(void* param); // 本地传感器数据采集任务
void ui_update_task(void* param); // UI 更新任务

void app_main() {
// 创建系统初始化任务
osal_task_t init_task;
init_task.task_func = system_init_task;
init_task.task_param = NULL;
init_task.task_period_ms = 0; // 只执行一次
osal_task_create(&init_task);

// 启动任务调度器
osal_task_schedule();
}

void system_init_task(void* param) {
bsp_init(); // 板级支持包初始化
lora_protocol_init(); // LoRa 协议栈初始化
node_manager_init(); // 节点管理器初始化
ui_display_init(); // UI 显示初始化
hal_sensor_init(); // 本地传感器初始化 (如果需要)

// 添加预定义的节点 (示例,实际应用中可能从配置读取或动态发现)
uint8_t node_addr1[LORA_NODE_ID_LEN] = {0x00, 0x02};
uint8_t node_addr2[LORA_NODE_ID_LEN] = {0x00, 0x03};
uint8_t node_addr3[LORA_NODE_ID_LEN] = {0x00, 0x04};
uint8_t node_addr4[LORA_NODE_ID_LEN] = {0x00, 0x05};
uint8_t node_addr5[LORA_NODE_ID_LEN] = {0x00, 0x06};
uint8_t node_addr6[LORA_NODE_ID_LEN] = {0x00, 0x07};
node_manager_add_node(node_addr1);
node_manager_add_node(node_addr2);
node_manager_add_node(node_addr3);
node_manager_add_node(node_addr4);
node_manager_add_node(node_addr5);
node_manager_add_node(node_addr6);

// 创建本地传感器数据采集任务 (如果需要本地节点采集数据并发送)
osal_task_t sensor_task;
sensor_task.task_func = sensor_collect_task;
sensor_task.task_param = NULL;
sensor_task.task_period_ms = 5000; // 每 5 秒采集一次数据
osal_task_create(&sensor_task);

// 创建 UI 更新任务
osal_task_t ui_task;
ui_task.task_func = ui_update_task;
ui_task.task_param = NULL;
ui_task.task_period_ms = 1000; // 每 1 秒更新 UI
osal_task_create(&ui_task);

vTaskDelete(NULL); // 删除初始化任务自身
}

void sensor_collect_task(void* param) {
sensor_data_t local_sensor_data;
uint8_t raw_sensor_data[6]; // 假设传感器返回 6 字节原始数据
uint16_t raw_data_len;

while (1) {
// 读取本地传感器数据 (假设 hal_sensor_read_data 函数返回原始数据)
if (hal_sensor_read_data(raw_sensor_data, &raw_data_len)) {
if (sensor_data_parse(raw_sensor_data, raw_data_len, &local_sensor_data)) {
// 将本地传感器数据通过 LoRa 发送出去 (如果需要作为本地节点发送数据)
lora_packet_t tx_packet;
memset(&tx_packet, 0, sizeof(tx_packet));
memcpy(tx_packet.dest_addr, "\x00\x00", LORA_NODE_ID_LEN); // 广播地址
tx_packet.payload_len = raw_data_len;
memcpy(tx_packet.payload, raw_sensor_data, raw_data_len);
lora_protocol_send_packet(&tx_packet);
}
}
bsp_delay_ms(5000); // 5 秒采集一次
}
}

void ui_update_task(void* param) {
while (1) {
// 获取节点列表和节点数量
extern node_info_t node_list[MAX_NODES];
extern uint8_t node_count;

// 更新 UI 显示
ui_display_show_node_list(node_list, node_count);

bsp_delay_ms(1000); // 1 秒更新一次 UI
}
}

测试验证和维护升级

  • 测试验证:

    • 单元测试: 针对每个模块进行单元测试,验证模块功能的正确性。
    • 集成测试: 将模块组合起来进行集成测试,验证模块之间的接口和协作是否正常。
    • 系统测试: 进行系统级别的测试,验证整个系统的功能、性能、可靠性是否满足需求。
    • 现场测试: 在实际应用环境中进行现场测试,验证系统的稳定性和适应性。
  • 维护升级:

    • 固件升级: 预留固件升级接口 (例如 OTA 无线升级或 UART 串口升级),方便后续功能升级和 bug 修复。
    • 模块化维护: 由于采用模块化设计,可以方便地对单个模块进行维护和升级,而不会影响到其他模块。
    • 日志记录和错误处理: 完善的日志记录和错误处理机制可以帮助快速定位和解决问题。

总结

以上代码架构和C代码示例提供了一个构建可靠、高效、可扩展的嵌入式 LoRa 自组网节点数据采集和显示系统的框架。这个架构基于分层和模块化设计,将系统分解为多个独立的层次和模块,每个模块负责特定的功能,层次之间和模块之间通过清晰定义的接口进行交互。这种架构具有良好的可移植性、可维护性和可扩展性,能够满足项目需求,并为后续的开发和维护奠定坚实的基础。

请注意,以上代码仅为示例和框架,实际项目中需要根据具体的硬件平台、传感器类型、LoRa 协议细节、UI 设计等进行详细的实现和优化。 实际的代码量肯定会超过3000行,因为包含了各个模块的完整实现,以及详细的注释、错误处理、边界条件考虑等等。 这个框架和示例代码可以作为您进行项目开发的起点和参考。

欢迎关注我的其它发布渠道