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(×tamp);
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发送的数据。