STM32项目---畜牧定位器

1 项目简介

项目需求

畜牧定位器是智能农场项目的子项目, 目的在于定位农场中的畜牧牲畜. 由于牲畜的运动范围有限(农场中), 故定位频率不用太高, 一般小时级别的定位频率即可满足需求。该定位器也带有计步功能, 可以记录牲畜的运动步数。

功能描述

  • 定位器

牲畜身上绑定定位器,主控芯片为 STM32F103C8T6,定位器开发板上的主要功能模块是:LoRa模块, NB-IoT模块, GPS模块, 计步模块。

牲畜通过GPS模块获取自身的GPS定位信息,通过计步模块获取运动步数, 然后通过NB-IoT模块把这些发送到云端服务器. 云端服务器收到这些信息后,会自动存储到数据库. 其他应用会从数据库读取这些信息, 然后再根据需要进行各种分析.

如果NB-IoT出现无网络的情况, 则无法发送到云端服务器. 这时可以通过LoRa发送到LoRa网关, 再由网关统一把收到的定位和计步信息发送到云端服务器.

  • LoRa接收网关

只负责接收LoRa发来的数据, 然后通过网络发送到云端服务器. LoRa接收网关连接网络可以是有线也可以是Wifi无线连接.

总体设计

2 硬件架构

硬件选型

  • 定位器:

主控芯片:STM32F103C8T6。

LoRa模块: LoRa芯片:LLCC68。---- SPI1

NB-IOT: QS-100 ---- USART3

GPS模块:AT6558R-5N32 ---- USART2

计步模块: DS3553 ---- I2C1

  • LoRa网关

普通的服务器即可,能连接网络, 能接收LoRa. 暂时使用我们的STM32开发板来做LoRa网关.

原理图

  • GPS模块:
  • NB-IOT
  • 计步模块

3 软件架构及代码实现

  • 定位器:
  • LoRa网关

3.1 定位器

3.1.1 common ---- 公共层
调试模块

CubeMX配置:开USART1打印,默认波特率为115200即可

  • debug.h ---- 通用打印调试函数
c 复制代码
#ifndef __DEBUG_H
#define __DEBUG_H

#include "usart.h"
#include "stdarg.h"
#include "string.h"
#include "stdio.h"
/**
 * 使用条件定义来表示是否开启printf输出功能
 * 如果#define DEBUG  表名开启debug功能  后续统一使用宏定义debug_printf()来输出调试信息
 * 实际产品上线的时候  将#define DEBUG修改掉  else分支中会存在debug_printf()内容为空的宏定义
 * 之前调用的debug_printf()都会统一失效为空  
 */
#define DEBUG

#ifdef DEBUG

#define debug_init() Debug_Init()


// 拆分文件的路径加文件名称  只保留文件名称即可
// User\main.c  =>  main.c
#define _FILE_NAME   strrchr(__FILE__,'\\') ? strrchr(__FILE__,'\\') + 1 : __FILE__
#define FILE_NAME   strrchr(_FILE_NAME,'/') ? strrchr(_FILE_NAME,'/') + 1 : _FILE_NAME

// 在打印debug信息的时候 先输出处于哪个文件的哪一行
// 拼接前缀字符串"[%s:%d]"   表示对应的文件名和行号
#define debug_printf(format,...) printf("[%s:%d]" format ,FILE_NAME,__LINE__,##__VA_ARGS__)

#define debug_printfln(format,...) printf("[%s:%d]" format "\n",FILE_NAME,__LINE__,##__VA_ARGS__)

#else
#define debug_init() // 相同的宏定义名称  内容留空
#define debug_printf(format,...)  // 相同的宏定义名称  内容留空
#define debug_printfln(format,...) // 相同的宏定义名称 内容留空
#endif

void Debug_Init(void);

#endif
  • debug.c
c 复制代码
#include "Debug.h"

void Debug_Init(void)
{
    MX_USART1_UART_Init();
}

int fputc(int ch, FILE *file)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
  return ch;
}
工具函数
  • Tool.c ---- 编写延时、时间转换等函数
c 复制代码
#include "Tool.h"

void Delay_us(uint16_t us)
{
    // TODO
}
void Delay_ms(uint16_t ms)
{
    HAL_Delay(ms);
}
void Delay_s(uint16_t s)
{
    while (s--)
    {
        HAL_Delay(1000);
    }
}

void UTCTime_to_bjTime(uint8_t *utcTime, uint8_t *bjTime)
{
    // +8小时
    /*1. 解析UTC时间,转化为时间戳*/
    struct tm utc;

    int32_t year, month, day, hour, minute, second;
    // utcTime ==> 2024-10-25 06:07:00
    sscanf((char *)utcTime, "%d-%d-%d %d:%d:%d",
           &year, &month, &day, &hour, &minute, &second);

    utc.tm_year = year - 1900;
    utc.tm_mon = month - 1;
    utc.tm_mday = day;
    utc.tm_hour = hour;
    utc.tm_min = minute;
    utc.tm_sec = second;

    // 使用mktime 转化结构体到时间戳
    time_t timestamp = mktime(&utc);

    /*2. 对转化完成的时间戳 +8h*/
    timestamp += 8 * 3600;

    /*3. 将时间戳转化为北京时间*/
    // 使用localtime 转化时间戳到结构体
    struct tm *tm_bj_time = localtime(&timestamp);
    sprintf((char *)bjTime, "%04d-%02d-%02d %02d:%02d:%02d",
            tm_bj_time->tm_year + 1900,
            tm_bj_time->tm_mon + 1,
            tm_bj_time->tm_mday,
            tm_bj_time->tm_hour,
            tm_bj_time->tm_min,
            tm_bj_time->tm_sec);
}
  • config.h ---- 存放IP、端口号以及自定义的结构体
c 复制代码
#ifndef __COMMON_CONFIG_H
#define __COMMON_CONFIG_H

#include "stm32f1xx.h"


// IOT物联网芯片远程连接的云服务器地址
#define TCP_SERVER_IP "112.125.89.8"
#define TCP_SERVER_PORT 44445

/* 状态类型定义 */
//使用CommonStatus的返回值类型,判断当前代码执行是否成功
typedef enum
{
    COMMON_OK = 0,
    COMMON_ERROR,
    COMMON_OTHER
} CommonStatus;

/* 通过IoT和LoRa上传的数据的类型封装 */
typedef struct
{
    //牛马编号 ==> 使用物联网卡的id
    uint8_t uuid[33];  /* 使用16进制字符串形式保存 16 * 2 = 32*/
    /* gps信息 */
    //经度 0-180 东西 一共360
    double  lon;    /* 经度 */
    uint8_t lonDir[2]; /* 经度 方向 E-东 W-西*/

    //伟度 0-90 南北 一共是180
    double  lat;    /* 维度 */
    uint8_t latDir[2]; /* 维度方向 N-北 S-南 */

    // 速度
    double speed; /* 对地速度 单位位节*/

    uint8_t dateTime[21]; /* 定位时间 2024-12-12 11:11:11 */

    /* 计步信息 */
    uint16_t stepNum; /* 运动的步数 */

    /* 存储json格式的字符串, 用于上传到服务器*/
    uint8_t  data[256]; /* 缓冲区 */
    uint16_t dataLen;   /* 缓冲功区存储的实际的数据长度 */
} UploadDataType;
#endif
3.1.2 通信模块
NB-IoT模块

CubeMX配置:开USART3,用于NB-IOT驱动,波特率设置为9600。再配置PB13唤醒引脚,通用推挽输出,默认低电平,别名 IOT_WKUP。

  • Inf_QS100.c

AT+RB ---- 软重启

ATEx ---- 串口回显,ATE0:不回显;ATE1:需要回显

AT+CGATT? ---- 查询命令:返回所有当前设置的参数值。返回值+CGATT:;0为去附着,1为附着

c 复制代码
#include "Inf_QS100.h"

#define IOT_USART huart3
// 接收usart3的IOT芯片返回的消息

static uint8_t rx_buff[128];
static uint16_t rx_buff_size = 0;

static uint8_t response_buff[512];
static uint16_t response_size = 0;

void Inf_QS100_RecvCallback(uint16_t Size)
{
    /*1. 接收到IOT芯片返回的消息*/
    rx_buff_size = Size;
    /*2. 直接打印 ==> 这个方法不好,不要在中断处理中去执行时间太长的程序*/
    // debug_printfln("%s",rx_buff);

    /*3. HAL_UARTEx_ReceiveToIdle_IT 函数调用后只触发一次*/
    HAL_UARTEx_ReceiveToIdle_IT(&IOT_USART, rx_buff, sizeof(rx_buff));
}

void Inf_QS100_HandleResponse(void)
{
    /*1. 清空之前使用的缓存*/
    memset(response_buff, 0, sizeof(response_buff));
    response_size = 0;

    uint8_t count = 3;
    do
    {
        // 挂起等待接收一次数据
        uint32_t timeout = 0xffffff;
        while (rx_buff_size == 0 && timeout--)
            ;
        // 接收到一次数据
        memcpy(&response_buff[response_size], rx_buff, rx_buff_size);
        response_size += rx_buff_size;

        // 清空当前消息
        memset(rx_buff, 0, rx_buff_size);
        rx_buff_size = 0;

    } while (count-- && strstr((char *)response_buff, "OK") == NULL && strstr((char *)response_buff, "ERROR") == NULL);

    debug_printfln("%s", response_buff);
    debug_printfln("===========================");
}

void Inf_QS100_SendCmd(uint8_t *cmd)
{
    // 直接使用usart3发送命令
    HAL_UART_Transmit(&IOT_USART, cmd, strlen((char *)cmd), 1000);

    // 挂起等待,处理返回消息
    Inf_QS100_HandleResponse();
}

void Inf_QS100_Init(void)
{
    /*1. 初始化底层使用的驱动*/
    // MX_USART3_UART_Init();

    /*2.调用hal库的usart3接收数据命令 */
    HAL_UARTEx_ReceiveToIdle_IT(&IOT_USART, rx_buff, sizeof(rx_buff));

    /*3. 重启-重置芯片 AT+RB*/
    debug_printfln("开始初始化IOT芯片");
    Inf_QS100_SendCmd("AT+RB\r\n");
    debug_printfln("重置IOT芯片命令已经发送");
    Delay_s(5);
    /*4. 添加回显*/
    Inf_QS100_SendCmd("ATE1\r\n");
    // Delay_s(2);
    Inf_QS100_SendCmd("ATE1\r\n");
}

CommonStatus Inf_QS100_GetIp(void)
{
    /*1. 拼接查询附着的命令*/
    uint8_t cmd_buff[30];
    sprintf((char *)cmd_buff, "AT+CGATT?\r\n");

    /*2. 发送查询命令*/
    Inf_QS100_SendCmd(cmd_buff);

    /*3. 根据返回的消息判断是否附着成功*/
    if (strstr((char *)response_buff, "+CGATT:1"))
    {
        // 3.1 附着成功
        return COMMON_OK;
    }
    // 3.2 没有成功
    return COMMON_ERROR;
}

uint8_t qs100_buff_cmd[128];

CommonStatus Inf_QS100_OpenSocket(uint8_t *socketID)
{
    /*1. 拼接需要发送的命令*/
    memset(qs100_buff_cmd, 0, sizeof(qs100_buff_cmd));
    sprintf((char *)qs100_buff_cmd, "AT+NSOCR=STREAM,6,0,0\r\n");
    /*2. 发送对应的指令*/
    Inf_QS100_SendCmd(qs100_buff_cmd);
    /*3. 处理返回信息*/
    if (strstr((char *)response_buff, "OK"))
    {
        // 接收对应的socket编号
        //  *socketID = response_buff[32] - 48;
        sscanf((char *)response_buff, "%*[^:]:%hhu", socketID);
        return COMMON_OK;
    }
    else
    {
        return COMMON_ERROR;
    }
}

CommonStatus Inf_QS100_ConnectTCPServer(uint8_t socketID, uint8_t serverIP[], uint16_t port)
{
    /*1. 拼接需要发送的命令*/
    memset(qs100_buff_cmd, 0, sizeof(qs100_buff_cmd));
    sprintf((char *)qs100_buff_cmd, "AT+NSOCO=%d,%s,%d\r\n", socketID, serverIP, port);
    /*2. 发送对应的指令*/
    Inf_QS100_SendCmd(qs100_buff_cmd);
    /*3. 处理返回信息*/
    if (strstr((char *)response_buff, "OK"))
    {
        return COMMON_OK;
    }
    else
    {
        return COMMON_ERROR;
    }
}

CommonStatus Inf_QS100_SendToTCPServer(uint8_t socketID, uint8_t sequence, uint8_t dataLen, uint8_t data[])
{
    /*0. 转换字节数组为16进制字符串*/
    // 0.1 创建接收数据
    uint8_t tmp_data[dataLen * 2 + 1];
    memset(tmp_data, 0, sizeof(tmp_data));

    // 0.2 转换16进制字符串
    for (uint8_t i = 0; i < dataLen; i++)
    {
        sprintf((char *)&tmp_data[i * 2], "%02X", data[i]);
    }

    /*1. 拼接需要发送的命令*/
    memset(qs100_buff_cmd, 0, sizeof(qs100_buff_cmd));
    sprintf((char *)qs100_buff_cmd, "AT+NSOSD=%d,%d,%s,0x200,%d\r\n", socketID, dataLen, tmp_data, sequence);
    /*2. 发送对应的指令*/
    Inf_QS100_SendCmd(qs100_buff_cmd);
    /*3. 处理返回信息*/
    if (strstr((char *)response_buff, "OK"))
    {
        // 发送出去了
        // 还要判断是否接收到数据
        do
        {
            // 发送查询状态的命令
            memset(qs100_buff_cmd, 0, sizeof(qs100_buff_cmd));
            sprintf((char *)qs100_buff_cmd, "AT+SEQUENCE=%d,%d\r\n", socketID, sequence);
            //当返回值为 2 说明还在发送中 不能确定成功与否
        } while (response_buff[19] == '2');
    }
    if (response_buff[19] == '1')
    {
        return COMMON_OK;
    }
    

        return COMMON_ERROR;
}

CommonStatus Inf_QS100_SendData(uint8_t serverIP[], uint16_t server_port, uint8_t data[], uint8_t data_len)
{
    /*1. 测试外网连接*/
    uint8_t count = 20;
    while (Inf_QS100_GetIp() != COMMON_OK && count)
    {
        Delay_ms(500);
        count--;
    }

    // 判断count的值
    if (count == 0)
    {
        return COMMON_ERROR;
    }
    debug_printfln("外网连接成功");

    /*2. 开发板实现客户端 ==> 打开一个socket套接字*/
    count = 20;
    uint8_t socketID;
    while (Inf_QS100_OpenSocket(&socketID) != COMMON_OK && count)
    {
        Delay_ms(500);
        count--;
    }
    // 判断count的值
    if (count == 0)
    {
        return COMMON_ERROR;
    }
    debug_printfln("创建socket成功");

    /*3. 创建TCP客户端 主动连接云服务器上的服务端*/
    count = 20;
    while (Inf_QS100_ConnectTCPServer(socketID, serverIP, server_port) != COMMON_OK && count)
    {
        Delay_ms(500);
        count--;
    }
    // 判断count的值
    if (count == 0)
    {
        return COMMON_ERROR;
    }

    debug_printfln("连接服务端成功");

    /*4. 发送对应的数据*/
    Inf_QS100_SendToTCPServer(socketID,5,data_len,data);
    return COMMON_OK;
}

void Inf_QS100_EnterLowPower(void)
{
    /*1. 直接进入低功耗*/
    debug_printfln("准备进入低功耗模式");
    Inf_QS100_SendCmd("AT+FASTOFF=0\r\n");
}

void Inf_QS100_ExitLowPower(void)
{
    debug_printfln("退出低功耗模式");

    HAL_GPIO_WritePin(IOT_WKUP_GPIO_Port,IOT_WKUP_Pin,GPIO_PIN_SET);
    Delay_s(2);
    HAL_GPIO_WritePin(IOT_WKUP_GPIO_Port,IOT_WKUP_Pin,GPIO_PIN_RESET);

}
  • 在main.c中测试

注:第三步后面加个1s左右的延时会比较稳定,没加延时之前测试云平台偶尔能收到一条数据,加了后能稳定接收数据。

c 复制代码
while (1)
  {
    // /*1. 退出低功耗*/
    debug_printfln("退出低功耗");
    Inf_QS100_ExitLowPower();
    // /*2. 执行初始化*/
    debug_printfln("准备上班");
    Inf_QS100_Init();
    // /*3. 发送数据*/
    debug_printfln("努力上班");
    Inf_QS100_SendData(TCP_SERVER_IP, TCP_SERVER_PORT, "HELLO", 5);
    Delay_s(1);
    // /*4. 进入低功耗*/
    debug_printfln("开始摸鱼");
    Inf_QS100_EnterLowPower();
    // /*5. 延时*/
    debug_printfln("接下来摸鱼3s");
    Delay_s(3);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
GPS模块

CubeMX配置:开USART2,用于GPS模块驱动,波特率设置为9600,再打开中断。设置GPS的电源引脚PB3,取别名为GPS_POWER.

NMEA协议:接收消息都是NMEA标准消息,第一条消息开头是GGA,最后一条消息开头是TXT

发消息都是自定义消息,CAS02:设置定位更新率。$PCAS02,1000*2E更新率为 1Hz,每秒输出 1 个定位点

​ CAS04:配置工作系统。$PCAS04,3*1A 北斗和 GPS 双模

其中:星号 * 后面的16进制数表示校验和, 和之间(不包括 和之间(不包括 和之间(不包括和)所有字符的异或结果

  • Inf_AT6558R.c
c 复制代码
#include "Inf_AT6558R.h"

#define GPS_UART huart2
#define GPS_FREQ_FULL "$PCAS02,1000*2E"
#define GPS_FREQ "PCAS02,1000"
#define GPS_MODE "PCAS04,3"
#define GPS_BEGIN "GGA"
#define GPS_END "TXT"

static uint8_t GPS_cmd_buff[50] = {0};
static uint8_t GPS_rx_buff[1024];
static uint16_t GPS_rx_size;

extern void Inf_AT6558R_RecvCallback(uint16_t Size)
{
    GPS_rx_size = Size;
    //再次调用接收方法 用于下一次接收数据
    HAL_UARTEx_ReceiveToIdle_IT(&GPS_UART,GPS_rx_buff,1024);

}

//发送的消息都是NMEA的自定义消息 前缀都是CAS
void Inf_AT6558R_SendCmd(uint8_t cmd[])
{   
    memset(GPS_cmd_buff,0,sizeof(GPS_cmd_buff));

    uint16_t tmp = cmd[0];
    for (uint8_t i = 1; cmd[i] != '\0'; i++)
    {
        tmp ^= cmd[i];
    }

    sprintf((char *)GPS_cmd_buff,"$%s*%02x\r\n",cmd,tmp);
    // debug_printfln("发送的命令为:%s",GPS_cmd_buff);
    HAL_UART_Transmit(&GPS_UART,GPS_cmd_buff,strlen((char *)GPS_cmd_buff),1000);
    
}

void Inf_AT6558R_Init(void)
{   
    /*1. 初始化底层驱动 USART2*/
    MX_USART2_UART_Init();

    /*2. 中断接收返回消息*/
    HAL_UARTEx_ReceiveToIdle_IT(&GPS_UART,GPS_rx_buff,1024);

    /*3. 开启电源*/
    HAL_GPIO_WritePin(GPS_POWER_GPIO_Port,GPS_POWER_Pin,GPIO_PIN_SET);

    /*4. 修改刷新频率CAS02 改为1s一次*/
    Inf_AT6558R_SendCmd(GPS_FREQ);

    /*5. 修改模式CAS04  */
    Inf_AT6558R_SendCmd(GPS_MODE);
}

void Inf_AT6558R_ReadData(uint8_t data_buff[],uint16_t *data_size)
{
    /*1. 清空缓存*/
    memset(data_buff,0,sizeof(data_buff));
    *data_size = 0;

    /*2. 判断此时接收数据是否为空*/
    if (GPS_rx_size ==  0)
    {
        return;
    }
    //每次中断返回信息只有一行 多次中断拼接数据 
    // memcpy(data_buff,GPS_rx_buff,GPS_rx_size);
    //最终在满足 合并的信息后再修改长度
    if (strstr((char *)GPS_rx_buff,GPS_BEGIN) && strstr((char *)GPS_rx_buff,GPS_END))
    {
        //此处的长度一定要用strlen算接收到的数组具体数据长度,因为GPS_rx_size不准确,或者把这行代码放在判断前,先复制再判断
        memcpy(data_buff,GPS_rx_buff,strlen((char *)GPS_rx_buff));
        *data_size = GPS_rx_size;
    }
    memset(GPS_rx_buff,0,sizeof(GPS_rx_buff));
    GPS_rx_size = 0;
}

void Inf_AT6558R_EnterLowPower(void)
{
    debug_printfln("切断电源");
    HAL_GPIO_WritePin(GPS_POWER_GPIO_Port,GPS_POWER_Pin,GPIO_PIN_RESET);
}

void Inf_AT6558R_ExtiLowPower(void)
{
    debug_printfln("恢复电源");
    HAL_GPIO_WritePin(GPS_POWER_GPIO_Port,GPS_POWER_Pin,GPIO_PIN_SET);
}
LoRa模块

LoRa模块分为定位器和网关

LoRa定位器:

CubeMX配置:使用的mcu为c8t6

开SPI1,设置16分频,波特率为4.5MBits/s。然后配置GPIO引脚并取别名。

GPIO引脚:PA4 ---- LORA_CS

​ PB0 ---- LORA_RST

​ PB1 ---- LORA_BUSY

​ PB2 ---- LORA_TXEN

​ PB12 ---- LORA_RXEN

LoRa的移植参考之前使用的Ebyte的官方移植代码。

其中ebyte_callback.c文件中有可改进的地方:

  • ebyte_callback.c

在实现自己逻辑业务时可以使用虚弱函数,以后需要时便可重写弱实现函数。

c 复制代码
__weak void LoRa_TransmitCallback(void)
{
    //弱实现中保持空定义
}
void Ebyte_Port_TransmitCallback( uint16e_t state )
{
    //1.简单的方式,直接写在回调中
    debug_printfln("发送的状态码为:%d",state);
    Ebyte_RF.EnterReceiveMode(0);
    /* 发送: 正常完成 */
    if( state & 0x0001 )
    {
        //To-do 实现自己的逻辑
        //2. 开放的方法 ==> 提供给外部使用
        LoRa_TransmitCallback();
    }
    ...
}

__weak void LoRa_ReceiveCallback(uint8e_t *buffer, uint8e_t length)
{
    //弱实现中保持空定义
}
void Ebyte_Port_ReceiveCallback(  uint16e_t state, uint8e_t *buffer, uint8e_t length )
{
    /* 接收: 正常 */
    if( state & 0x0002 )
    {
        //To-do 实现自己的逻辑
        LoRa_ReceiveCallback( buffer, length);
    }
   ...
}
  • Inf_lora.c
c 复制代码
#include "Inf_lora.h"

void Inf_LoRa_Init(void)
{
    MX_SPI1_Init();
    Ebyte_RF.Init();
}

void Inf_LoRa_Start(void)
{
    Ebyte_RF.StartPollTask();
}

void Inf_LoRa_SendData(uint8_t data[], uint16_t size)
{
    // 超时时间一定要写0
    Ebyte_RF.Send(data, size, 0);
}

//LoRa的收发数据也可以写在这里,不用在ebyte_callback.c里写
void LoRa_transmitCallback(void)
{
    debug_printfln("使用LoRa发送数据给网关");
}

void LoRa_ReceiveCallback(uint8e_t *buffer, uint8e_t length)
{
    debug_printfln("lora接收数据成功%d:%s", length, buffer);
}
LoRa网关:

实现LoRa网关需要新建一个工程。

CubeMX配置:与定位器配置一致,只是此次网关用的mcu为f103zet6,所以引脚名不同而已

开SPI1,设置16分频,波特率为4.5MBits/s。然后配置GPIO引脚并取别名。

GPIO引脚:PG14 ---- LORA_CS

​ PG13 ---- LORA_RST

​ PE2 ---- LORA_BUSY

​ PE6 ---- LORA_TXEN

​ PE5 ---- LORA_RXEN

网关的LoRa移植直接将之前的LoRa文件夹复制过来即可,注意实现SPI底层驱动的时候引脚名称是否一致。

3.1.3 计步模块

CubeMx配置:

I2C通信,这里我们使用I2C1,PB5作为它的片选线。

他的I2C读写地址:

命令 I2C 地址 读/写 I2C 地址+读/写
读命令 27H 1 4FH
写命令 27H 0 4EH

通信时序图:

  • inf_DS3553.c
c 复制代码
#include "Inf_DS3553.h"

void Inf_DS3553_Init(void)
{
    MX_I2C1_Init();
}

uint8_t Inf_DS3553_ReadReg(uint8_t reg_addr)
{
    uint8_t result = 0;
    /* 1. 拉低片选线 */
    CS_LOW;
    Delay_ms(5);

    /* 2. 读取对应数据 */
    HAL_I2C_Mem_Read(&hi2c1, DS3553_ADDR_W, reg_addr, I2C_MEMADD_SIZE_8BIT, &result, 1, 1000);
    // HAL_I2C_Master_Transmit();
    // HAL_I2C_Master_Receive();

    /* 拉高片选线 */
    CS_HIGH;
    Delay_ms(13);
    return result;
}

uint8_t Inf_DS3553_ReadId(void)
{
    return Inf_DS3553_ReadReg(DS3553_REG_ADDR_ID);
}

uint32_t Inf_DS3553_ReadStep(void)
{
    uint32_t step = 0;
    step |= Inf_DS3553_ReadReg(DS3553_REG_ADDR_STEP_L);
    step |= Inf_DS3553_ReadReg(DS3553_REG_ADDR_STEP_M) << 8;
    step |= Inf_DS3553_ReadReg(DS3553_REG_ADDR_STEP_H) << 16;
    return step;
}
3.1.4 应用层
计步模块:
  • App_stepCount.c
c 复制代码
#include "App_stepCount.h"


void App_step_count_Init(void)
{
    Inf_DS3553_Init();
}

void App_Get_Step_Count(UploadDataType *upload)
{
    uint32_t step = Inf_DS3553_ReadStep();
    upload->stepNum = step;
}
通信模块:
  • App_communication.c

转化为jSON数据需要先导入cjson工具文件,由于拼接后的数据太大,导致内存溢出,根本原因是HAL库默认给我们堆内存分配的堆内存太小了,需要将startup_stm32f103xb.s中的第43行代码 Heap_Size EQU 0x200 改为0x800。

c 复制代码
#include "App_communication.h"

void App_Communication_Init(void)
{
    Inf_AT6558R_Init();
    Inf_LoRa_Init();
    Inf_QS100_Init();
}
uint16_t data_size;
uint8_t data_buff[512];
void App_Communication_GetGpsInfo(UploadDataType *upload)
{
    /* 1. 读取需要的gps信息 */
    char *tmp_str;
    char ch;
    while (1)
    {
        Inf_AT6558R_ReadData(data_buff, &data_size);
        if (data_size > 0)
        {
            // tmp_str = strstr((char *)data_buff, "$GNRMC");
            tmp_str = "$GNRMC,070822.000,A,4006.81888,N,11621.89413,E,0.81,359.02,020624,,,A,V*02";
            sscanf(tmp_str, "%*[^AV]%c", &ch);
            if (ch == 'A')
            {
                debug_printfln("接收到有效的GPS信号");
                break;
            }
            else if (ch == 'V')
            {
                debug_printfln("接收到无效的GPS信号");
            }
        }
    }

    // 解析有效GPS信号中具体的内容
    // tmp_str: $GNRMC,070822.000,A,4006.81888,N,11621.89413,E,0.81,359.02,020624,,,A,V*02
    char time[6];
    char date[6];
    sscanf(tmp_str, "$GNRMC,%6c%*7c%lf,%c,%lf,%c,%lf,%*f,%6c",
           time,
           &upload->lat,
           (char *)&upload->latDir[0],
           &upload->lon,
           (char *)&upload->lonDir[0],
           &upload->speed,
           date);

    // 修正数据 => 将datetime修改为 yyyy-MM-dd HH:mm:ss
    sprintf((char *)upload->dateTime, "20%c%c-%c%c-%c%c %c%c:%c%c:%c%c",
            date[4], date[5], date[2], date[3], date[0], date[1],
            time[0], time[1], time[2], time[3], time[4], time[5]);
    debug_printfln("datetime:%s", upload->dateTime);

    // 修正经度和纬度  4006.81888 => 40 + (06.81888 / 60)
    double lat, lon;
    lat = (uint8_t)(upload->lat / 100) + (upload->lat - (uint16_t)(upload->lat / 100) * 100) / 60;
    lon = (uint8_t)(upload->lon / 100) + (upload->lon - (uint16_t)(upload->lon / 100) * 100) / 60;
    upload->lat = lat;
    upload->lon = lon;
    debug_printfln("time:%s,date:%s,lat:%lf,latDir:%c,lon:%lf,lonDir:%c,speed:%lf",
                   time, date, upload->lat, upload->latDir[0], upload->lon, upload->lonDir[0], upload->speed);
}

void App_Communication_uploadToJSON(UploadDataType *upload)
{
    // 1. 创建json对象
    cJSON *json_obj = cJSON_CreateObject();

    // 2. 添加json键值对
    cJSON_AddStringToObject(json_obj, "datetime", (char *)upload->dateTime);
    cJSON_AddNumberToObject(json_obj, "lat", upload->lat);
    cJSON_AddNumberToObject(json_obj, "lon", upload->lon);
    cJSON_AddStringToObject(json_obj, "lat_dir", (char *)upload->latDir);
    cJSON_AddStringToObject(json_obj, "lon_dir", (char *)upload->lonDir);
    cJSON_AddNumberToObject(json_obj, "speed", upload->speed);
    cJSON_AddStringToObject(json_obj, "uuid", (char *)upload->uuid);
    cJSON_AddNumberToObject(json_obj, "stepNum", upload->stepNum);

    // 3. 将json对象转换为json字符串
    char *json = cJSON_PrintUnformatted(json_obj);
    debug_printfln("%s", json);

    sprintf((char *)upload->data, "%s", json);
    upload->dataLen = strlen(json);

    // 4. 释放空间
    cJSON_Delete(json_obj);
}

CommonStatus App_Communication_SendDataByIOT(UploadDataType *upload)
{
    return Inf_QS100_SendData(TCP_SERVER_IP, TCP_SERVER_PORT, upload->data, upload->dataLen);
}

void App_Communication_SendDataByLoRa(UploadDataType *upload)
{
    Inf_LoRa_SendData(upload->data, upload->dataLen);
}

void App_Communication_SendData(UploadDataType *upload)
{
    // 补全设备ID  3个32位INT值    => 24位16进制数字
    sprintf((char *)upload->uuid, "%08x%08x%08x", HAL_GetUIDw2(), HAL_GetUIDw1(), HAL_GetUIDw0());

    // 将结构体转换为JSON  => 数据存到upload的data中
    App_Communication_uploadToJSON(upload);

    /* 1. 优先使用IOT将数据发送到云服务器 */
    debug_printfln("准备使用IOT发送数据");
    CommonStatus status = App_Communication_SendDataByIOT(upload);

    // 测试使用  发送一份lora的数据
    App_Communication_SendDataByLoRa(upload);

    /* 2. 再选择使用LoRa将数据发送给网关 */
    if (status != COMMON_OK)
    {
        debug_printfln("准备使用LoRa发送数据");
        App_Communication_SendDataByLoRa(upload);
    }

    debug_printfln("发送数据完成");
}
低功耗模块:

在CubeMX中打开RTC即可,注意我们使用的是内部低速时钟LSI40KHz。

  • App_lowPower.c
c 复制代码
#include "App_lowPower.h"

void App_EnterLowPower(void)
{
    // 关闭两个芯片
    Inf_AT6558R_EnterLowPower();
    Inf_QS100_EnterLowPower();

    // 准备进入到MCU的低功耗模式
    RTC_TimeTypeDef timeType;
    HAL_RTC_GetTime(&hrtc, &timeType, RTC_FORMAT_BIN);

    RTC_AlarmTypeDef alarmType;
    alarmType.AlarmTime.Hours = timeType.Hours;
    alarmType.AlarmTime.Minutes = timeType.Minutes;
    alarmType.AlarmTime.Seconds = timeType.Seconds + 20;
    HAL_RTC_SetAlarm(&hrtc, &alarmType, RTC_FORMAT_BIN);
    // 清除唤醒标签
    __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
    __HAL_PWR_CLEAR_FLAG(PWR_FLAG_SB);
    HAL_PWR_EnterSTANDBYMode();
}

void App_ExitLowPower(void)
{
    // 打开另外两个芯片
    Inf_QS100_ExitLowPower();
    Inf_AT6558R_ExitLowPower();
}
  • main.c
c 复制代码
/* USER CODE BEGIN 2 */
  debug_init();
  debug_printfln("准备退出待机模式  开始工作");

  App_ExitLowPower();

  App_step_count_Init();
  App_Communication_Init();

  App_Get_Step_Count(&upload);
  debug_printfln("计数器步数为 %d", upload.stepNum);

  App_Communication_GetGpsInfo(&upload);
  App_Communication_SendData(&upload);

  debug_printfln("准备进入待机模式 开始省电");
  Delay_s(3);
  App_EnterLowPower();

  /* USER CODE END 2 */

3.2 网关

网关逻辑很简单,就是LoRa接收到数据,然后通过以太网发送出去即可。

LoRa和ETH完全可以参考以前的代码,需要注意的是LoRa接收数据的长度会出错,但还是能完整接收到数据,所以在ebyte_callback.c中,接收的数据长度不能直接用他的,我们自己用函数计算下

  • ebyte_callback.c
c 复制代码
...
__weak void LoRa_ReceiveCallback(uint8e_t *buffer, uint16e_t length)
{
    // 保持为空
}

void Ebyte_Port_ReceiveCallback(uint16e_t state, uint8e_t *buffer, uint8e_t length)
{
    debug_printfln("接收的状态码为%d", state);
    /* 接收: 正常 */
    if (state & 0x0002)
    {
        // To-do 实现自己的逻辑
        LoRa_ReceiveCallback(buffer, strlen((char *)buffer));
    }
    ...
  • Inf_lora.c
c 复制代码
...
    extern uint8_t sn ;
void LoRa_ReceiveCallback(uint8e_t *buffer, uint16e_t length)
{
    debug_printfln("lora接收数据成功%d:%s", length, buffer);
    send(sn,buffer,length);
}

4 成果展示

  • 定位器:

定位器通过计步器记录步数,再通过GPS获取当前坐标信息,然后通过IOT将所有数据发送到云端,若物联卡信号不好,则由LoRa发送到网关。此次演示都发送。

  • 网关

网关接收到定位器发来的数据便通过以太网发送到服务端。

  • TCP服务端

接收网关发来的数据。

  • 云端

接收IOT发送的数据。

相关推荐
网易独家音乐人Mike Zhou2 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
搬砖的小码农_Sky5 小时前
C语言:数组
c语言·数据结构
PegasusYu5 小时前
STM32CUBEIDE FreeRTOS操作教程(九):eventgroup事件标志组
stm32·教程·rtos·stm32cubeide·free-rtos·eventgroup·时间标志组
2401_882727576 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架
ahadee9 小时前
蓝桥杯每日真题 - 第19天
c语言·vscode·算法·蓝桥杯
Theliars9 小时前
C语言之字符串
c语言·开发语言
畅联云平台9 小时前
美畅物联丨智能分析,安全管控:视频汇聚平台助力智慧工地建设
人工智能·物联网
Reese_Cool9 小时前
【数据结构与算法】排序
java·c语言·开发语言·数据结构·c++·算法·排序算法
lantiandianzi9 小时前
基于单片机的多功能跑步机控制系统
单片机·嵌入式硬件
文弱书生6569 小时前
输出比较简介
stm32