前言

本实验计划采用STM32作为终端控制器,搭载E22-400T22D LoRa模块,通过定点传输模式,将采集到的传感器数据发送至IMX6ULL主控端,并最终在前端网页上进行实时刷新与显示。

LORA介绍

E22-400T22D是一款基于SEMTECH LoRa扩频方案的UART串口无线模块,工作于410.125~493.125MHz频段(默认433.125MHz),发射功率达22dBm,理想通信距离可达5km。模块采用TTL电平输出,兼容3.3V与5V IO口电压,尺寸仅为21×36mm,重量约6.7g,适用于嵌入式设备集成。在本实验中,它作为数据链路的无线承载层,实现STM32终端与IMX6ULL网关之间的可靠数据传输。
在这里插入图片描述
E22-400T22D模块提供了四种工作模式,由硬件引脚 M1 和 M0 的高低电平组合来控制。您可以根据实际应用场景,在功耗、响应速度和配置灵活性之间做出选择。

模式 M1引脚 M0引脚 模式介绍
0 传输模式 0 0 串口打开,无线打开,透明传输
1 WOR模式 0 1 可定义为WOR发送方或WOR接收方,支持空中唤醒
2 配置模式 1 0 用户可通过串口访问寄存器,控制模块工作状态
3 深度休眠 1 1 模块进入休眠

E22-400T22D模块在传输模式(模式0,M1=0,M0=0)下,根据数据包的目标寻址方式,可进一步细分为三种传输方式:透传模式、定点传输和广播模式。其中,定点传输是本实验采用的方式,通过在数据包中指定目标模块的地址,实现精准寻址通信,确保传感器数据仅被IMX6ULL网关接收,有效避免同频干扰。以下是三种方式的对比:

传输方式 核心特点 适用场景
透传模式 串口收到的数据原封不动无线发出,接收端直接串口输出,不解析地址 简单的一对一通信,无需区分设备
定点传输 发送数据时携带目标模块地址,仅地址匹配的模块接收 多设备环境下的精准寻址通信(本实验采用)
广播模式 同信道内所有模块均可接收,无需指定目标地址 系统通知、参数同步等全局下发场景

要实现E22-400T22D模块的定点传输,发送端需要在普通数据前补全3个字节作为数据帧头,格式为:

[目标地址高字节] [目标地址低字节] [目标信道] [用户数据]

示意图如下图所示:
在这里插入图片描述
假设您的接收端(IMX6ULL侧)LoRa模块配置的地址为0x0000,信道为0x17,需要发送字符串"Hello"(十六进制:48 65 6C 6C 6F),那么完整的待发送数据帧为:00 00 17 48 65 6C 6C 6F

  • 00 00 → 目标地址高/低字节(指向地址为0x0000的接收端)
  • 17 → 目标信道(0x17)
  • 48 65 6C 6C 6F → 实际数据(“Hello”)

实验开始前,需对两端LoRa模块进行如下地址分配:IMX6ULL网关端的LoRa模块地址配置为 0x00,信道配置为 0x17;STM32终端端的LoRa模块地址配置为 0x01,信道配置为 0x18。后续数据交互中,STM32端向IMX6ULL发送数据时,需在帧头指定目标地址为 00、目标信道为 17;反之,IMX6ULL向STM32发送数据时,则需指定目标地址为 01、目标信道为 18。
具体上位机配置如下图所示:

STM32采集温湿度数据并通过LoRa定点上传

本次实验采用 STM32F103C8T6 作为终端控制器,搭载E22-400T22D LoRa模块,通过定点传输方式将采集到的温湿度传感器数据发送至IMX6ULL网关端。
LORA和单片机连接如图所示:
在这里插入图片描述
STM32分配的引脚如下表:

引脚功能 STM32端口 STM32引脚 功能说明
M0 GPIOB Pin 6 工作模式选择位0,与M1配合控制模块工作状态
M1 GPIOB Pin 7 工作模式选择位1,与M0配合控制模块工作状态
AUX GPIOB Pin 5 模块状态指示引脚(输入),用于判断模块是否忙碌或准备就绪
TXD GPIOA Pin 2 模块串口发送端,连接STM32的USART2_RX(PA3)
RXD GPIOA Pin 3 模块串口接收端,连接STM32的USART2_TX(PA2)

完成GPIO与USART2初始化后,即可调用自定义的定点传输函数 LORA_SendFixed(),该函数会自动在用户数据前封装目标地址与信道信息,并通过LoRa模块发送至指定终端。

// ============================================
// 定点传输发送
// 参数:
//   targetAddr:     目标设备地址(16位)
//   targetChannel:  目标设备信道(8位)
//   data:           要发送的数据指针
//   len:            数据长度
// ============================================
void LORA_SendFixed(uint16_t targetAddr, uint8_t targetChannel, uint8_t *data, uint16_t len)
{
    uint8_t header[3];
    
    // 构造头部:目标地址(高字节 + 低字节)+ 目标信道
    header[0] = (targetAddr >> 8) & 0xFF;   // 地址高字节
    header[1] = targetAddr & 0xFF;           // 地址低字节
    header[2] = targetChannel;               // 信道
    
    // 先发送 3 字节头部
    LORA_SendData(header, 3);
    
    // 再发送数据
    LORA_SendData(data, len);
}

然后在main函数中,定时2s传输一次数据。

int main(void)
{
    System_Init();
    
    while(1)
    {
        if (DHT11_Read_Data(&temperature, &humidity) == 0)
        {
            Display_Data();
            
            // ============================================
            // 使用定点传输发送温湿度数据
            // 目标地址:0x0003,目标信道:0x04
            // ============================================
            uint8_t txBuffer[32];
            uint16_t len;
            
            sprintf((char *)txBuffer, "T:%d.%d,H:%d.%d",
                    temperature / 10, temperature % 10,
                    humidity / 10, humidity % 10);
            
            len = strlen((char *)txBuffer);
            
            // 定点发送给地址 0x0003,信道 23 的设备
            LORA_SendFixed(0x0000, 23, txBuffer, len);
            
            // 同时OLED显示发送状态
            OLED_ShowString(3, 1, "Sent Fixed ");
        }
        else
        {
            OLED_ShowString(3, 1, "Read Error!");
        }
        
        delay_ms(2000);
    }
}

IMX6ULL接收温湿度数据

将带有USB转串口的LoRa模块插入IMX6ULL开发板的USB接口后,首先需确认Linux内核是否已内置CH340串口驱动,具体图形化配置的详细步骤可参考相关博客。
效果如下图所示:

在A7端创建独立线程,采用非阻塞I/O方式持续读取串口数据,具体实现可参考《正点原子Linux C应用编程》中串口编程相关内容。
将LoRa模块通过USB转串口接入IMX6ULL开发板后,系统自动识别为 /dev/ttyUSB0。

串口初始化需配置通信参数,包括波特率(与LoRa模块默认115200保持一致)、数据位(8位)、停止位(1位)及奇偶校验位(无校验)。通过 tcgetattr() 获取当前 termios 结构体,修改 c_cflag 寄存器完成参数设置,最后调用 tcsetattr() 使配置生效。具体实现如下:

int serial_init(const char *device, int baudrate)
{
    struct termios options = {0};

    // 1. 打开串口
    int fd = open(device, O_RDWR | O_NOCTTY | O_NDELAY);
    if (fd == -1)
    {
        perror("open serial port failed");
        return -1;
    }

    // 2. 保存旧配置
    if (tcgetattr(fd, &old_cfg) < 0)
    {
        fprintf(stderr, "tcgetattr error: %s\n", strerror(errno));
        close(fd);
        return -1;
    }

    // 3. 设置波特率
    speed_t speed;
    switch (baudrate)
    {
    case 9600:
        speed = B9600;
        break;
    case 19200:
        speed = B19200;
        break;
    case 38400:
        speed = B38400;
        break;
    case 57600:
        speed = B57600;
        break;
    case 115200:
        speed = B115200;
        break;
    case 230400:
        speed = B230400;
        break;
    default:
        speed = B115200;
        break;
    }

    // 4. 配置串口
    cfmakeraw(&options);
    cfsetispeed(&options, speed);
    cfsetospeed(&options, speed);

    options.c_cflag |= (CLOCAL | CREAD); // 本地连接,接收使能
    options.c_cflag &= ~CSIZE;           // 清除数据位
    options.c_cflag |= CS8;              // 8数据位
    options.c_cflag &= ~PARENB;          // 无校验
    options.c_cflag &= ~CSTOPB;          // 1停止位
    options.c_cflag &= ~CRTSCTS;         // 🔧 关闭硬件流控(明确添加)

    options.c_cc[VMIN] = 0;  // 不等待固定字节数
    options.c_cc[VTIME] = 0; // 立即返回

    // 5. 清空缓冲区
    tcflush(fd, TCIOFLUSH);

    // 6. 应用配置
    if (tcsetattr(fd, TCSANOW, &options) < 0)
    {
        fprintf(stderr, "tcsetattr error: %s\n", strerror(errno));
        close(fd);
        return -1;
    }

    return fd;
}

线程启动后首先调用 init_serial() 完成串口初始化,随后进入主循环,通过非阻塞方式不断轮询串口缓冲区,检测是否有新数据到达并读取。
整个pthread_transfer.c如下所示:

#include "data_global.h"
#include <termios.h>

static struct termios old_cfg; // 用于保存终端的配置参数
int serial_fd;
unsigned char rx_buffer[256];

// 函数声明
void file_env_info_struct(struct env_info_client_addr *client_addr, int home_id);

// ================================================================
// 解析 LoRa 数据并填充到结构体
// ================================================================
void parse_lora_data(const char *data, struct env_info_client_addr *client_addr, int home_id)
{
    float temp = 0, humi = 0;

    // 示例格式: "T:25.5,H:60.0"
    if (strstr(data, "T:") && strstr(data, "H:"))
    {
        sscanf(data, "T:%f,H:%f", &temp, &humi);

        client_addr->monitor_no[home_id].zigbee_data.tempature = temp;
        client_addr->monitor_no[home_id].zigbee_data.humidity = humi;
    }
}

void *pthread_transfer(void *arg)
{
    int home_id = 0; // 家庭ID,假设为1
    serial_fd = serial_init(ttyUSB, 115200);
    if (serial_fd < 0)
    {
        printf("Serial init failed!\n");
        return NULL;
    }
    while (1)
    {
        pthread_mutex_lock(&mutex_client_transfer);
        file_env_info_struct(&client_addr, home_id); // 填入虚拟环境信息
        pthread_mutex_unlock(&mutex_client_transfer);
        pthread_cond_signal(&cond_client_transfer); // 发送转发条件
        sleep(1);
    }
}
// 模拟数据上传
void file_env_info_struct(struct env_info_client_addr *client_addr, int home_id) // 填入虚拟环境信息
{
    int ret = serial_read(serial_fd, rx_buffer, sizeof(rx_buffer) - 1);
    if (ret > 0)
    {
        rx_buffer[ret] = '\0';
        parse_lora_data((char *)rx_buffer, client_addr, home_id);
    }
    client_addr->monitor_no[home_id].zigbee_data.humiMax = 60.0;
    client_addr->monitor_no[home_id].zigbee_data.humiMin = 40.0;
    client_addr->monitor_no[home_id].zigbee_data.tempMax = 30.0;
    client_addr->monitor_no[home_id].zigbee_data.tempMin = 20.0;

    client_addr->monitor_no[home_id].imx6ull_data.adc = 1023;
    client_addr->monitor_no[home_id].imx6ull_data.accx = 100;
    client_addr->monitor_no[home_id].imx6ull_data.accy = 200;
    client_addr->monitor_no[home_id].imx6ull_data.accz = 300;
    client_addr->monitor_no[home_id].imx6ull_data.gyrox = 10;
    client_addr->monitor_no[home_id].imx6ull_data.gyroy = 20;
    client_addr->monitor_no[home_id].imx6ull_data.gyroz = 30;
}

int serial_init(const char *device, int baudrate)
{
    struct termios options = {0};

    // 1. 打开串口
    int fd = open(device, O_RDWR | O_NOCTTY | O_NDELAY);
    if (fd == -1)
    {
        perror("open serial port failed");
        return -1;
    }

    // 2. 保存旧配置
    if (tcgetattr(fd, &old_cfg) < 0)
    {
        fprintf(stderr, "tcgetattr error: %s\n", strerror(errno));
        close(fd);
        return -1;
    }

    // 3. 设置波特率
    speed_t speed;
    switch (baudrate)
    {
    case 9600:
        speed = B9600;
        break;
    case 19200:
        speed = B19200;
        break;
    case 38400:
        speed = B38400;
        break;
    case 57600:
        speed = B57600;
        break;
    case 115200:
        speed = B115200;
        break;
    case 230400:
        speed = B230400;
        break;
    default:
        speed = B115200;
        break;
    }

    // 4. 配置串口
    cfmakeraw(&options);
    cfsetispeed(&options, speed);
    cfsetospeed(&options, speed);

    options.c_cflag |= (CLOCAL | CREAD); // 本地连接,接收使能
    options.c_cflag &= ~CSIZE;           // 清除数据位
    options.c_cflag |= CS8;              // 8数据位
    options.c_cflag &= ~PARENB;          // 无校验
    options.c_cflag &= ~CSTOPB;          // 1停止位
    options.c_cflag &= ~CRTSCTS;         // 🔧 关闭硬件流控(明确添加)

    options.c_cc[VMIN] = 0;  // 不等待固定字节数
    options.c_cc[VTIME] = 0; // 立即返回

    // 5. 清空缓冲区
    tcflush(fd, TCIOFLUSH);

    // 6. 应用配置
    if (tcsetattr(fd, TCSANOW, &options) < 0)
    {
        fprintf(stderr, "tcsetattr error: %s\n", strerror(errno));
        close(fd);
        return -1;
    }

    return fd;
}

int serial_write(int fd, const unsigned char *data, int len)
{
    int ret = write(fd, data, len);
    if (ret < 0)
    {
        perror("write error");
    }
    return ret;
}

int serial_read(int fd, unsigned char *buffer, int len)
{
    int ret = read(fd, buffer, len);
    if (ret < 0)
    {
        perror("read error");
    }
    return ret; // 返回实际读取的字节数
}

将数据填充结构体,然后交给刷新线程用于上传网页,之后的数据就和模拟数据上传一样,刷新现成一旦获取锁,就通过共享内存的方式将数据交给CGI进程,随后转发到HTML网页显示,如下图所示:
在这里插入图片描述

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐