当前仓库属于暂停状态,部分功能使用受限,详情请查阅 仓库状态说明
1 Star 0 Fork 59

DONNUTS / netbox
暂停

forked from SpunkyWX / netbox
暂停
 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
README.md 29.09 KB
一键复制 编辑 原始数据 按行查看 历史
SpunkyWX 提交于 2019-12-03 23:34 . 修改README文件

【RT-Thread软件包赛】通用物联网系统平台

项目简介

当前国内互联网的发展迅猛,物联网云、边缘计算等技术也从概念技术逐步变为可靠的工程应用技术,加之国民对智能化生活的强烈需求,市场上涌现出许多的物联网设备需求,如智能售货柜、智能洗车机、智能储物箱等等。然而这些物联网设备与具体应用有关,不同的个体应用案例,对物联网设备的功能要求差异也较大。同时为了满足市场的商机,要求物联网设备能够快速开发出来,并且稳定可靠。因此为了避免重复开发打造一款通用的物联网平台非常必要。

物联网系统平台包含多个终端设备、后台服务器和人机设备(如手机、平板等),因此打造一套通用的物联网系统平台需要首先确定整个物联网系统的拓扑架构。

物联网系统拓扑图

  • 物联网系统的后台服务器的功能就是负责与成千上万的终端设备进行数据通讯交互、同时还需具备消息转发和存储等功能,高可靠性,高并发性也是对这个后台服务器的基本要求。在这种情况下肯定采用商用的云服务器比较稳妥,市面上的物联网服务器有很多,在本系统中选择使用阿里物联网云服务器,没有什么特殊原因,就是老板定的:)。
  • 物联网设备终端我们的详细介绍在后面的硬件章节,为了打造通用的物联网平台硬件,在通讯接口上必须丰富以满足不同的应用场景,目前支持GPRS(没有用4G主要是因为成本原因),以太网,RS485(MODBUS协议扩展子模块)。综上需求,我们选择STM32F4x7作为主控MCU,内部资源丰富无需扩展,而且成本价格相对合理。
  • 人机设备主要指的是手机和平板电脑一类终端设备,它们通过运行专用的APP或者B/S模式的形态存在,负责与后台云服务器进行通讯,从而与物联网设备进行交互。这部分不属于本次作品的范畴

实现功能说明

物联网终端设备硬件框图如下:

物联网终端设备硬件框图

实现功能说明:

  • GPRS/以太网通讯功能:负责通过设备与阿里物联网服务器进行数据交付;
  • 通讯接口冗余功能:自动检测GPRS和以太网连接情况,优先使用以太网,在以太网失效或者无法连接时,自动切换至GPRS网络;
  • RS485功能:通过承载MODBUS协议,与不同的子功能设备进行通讯,实现不同的应用功能;
  • MQTT+TLS功能:使用MQTT+TLS与阿里物联网服务器进行数据通讯;
  • OTA+TLS功能:使用OTA+TLS与阿里物联网服务器进行固件更新,OTA部分使用HTTPS协议;
  • 文件系统:设备使用W25Q128作为存储盘实现FAT文件系统,同时也作为固件区、参数区的存储空间;
  • 调试功能:ADBD负责作为shell调试和文件传输接口

使用的RTT软件包说明:

  • 阿里物联网SDK包:ali-iotkit-latest;
  • ADBD软件包:adbd-latest;
  • AT设备软件包:at_device-latest;
  • EasyFlash软件包:EasyFlash-latest;
  • FLASH分区管理包:fal-latest;
  • MODBUS软件包:libmodbus-latest;
  • TLS软件包:mbedtls-latest;
  • JSON软件包:cJSON-latest;
  • 网络工具包: netutils-latest;
  • OTA软件包:ota_download-latest;
  • LED软件包:SignalLed-latest;
  • 加密软件包:tinycryp;

软件框架说明

设备使用RT-Thrad 4.0.X作为操作系统,采用RTT官方的目录结构和框架:

  • 目录结构

目录结构

  • application: 应用代码存放;
  • board: 设备驱动存放,使用ST官方的CubeMX进行创建;
  • build: 编译文件存放;
  • figure: README.md的图片存放;
  • libraries:RTT和ST提供的针对STM32F4xx的库文件;
  • packages: RTT软件包存放;
  • rt-thread: 操作系统存放;
  • si-prj.si4project: SourceInsight工程文件存放;
  • 软件框架

使用RT-Thread系统,其软件框架就已经基本构架完成,采用compention方式进行自动初始化相关例程。这里我以智能存储柜应用为例子。

智能储存柜应用对终端设备的主要需求如下:

  1. 智能存储柜建立与阿里物联网平台的通讯连接,并订阅和发布相关主题;
  2. 智能存储柜通过订阅主题接收终端用户通过微信小程序向阿里物联网服务器发送相关命令,并执行相关操作;
  3. 智能存储柜具备远程设备更新能力;
  4. 智能存储柜具备通过MAC地址,自动在服务提供商的服务器进行注册并获得相关阿里物联网平台设备三元素信息;
  5. 智能存储柜实时监控将对撬门、关门超时或者其他违法操作进行本地和远程报警;
  6. 智能存储柜通过RS485总线承载标准的MODBUS协议与不同的功能子模块进行通讯,实现不同的应用功能;

智能存储柜应用设备使用了多个RTT软件包,这里不再详细叙述每个软件包的使用方法,请自行看代码和软件包的说明即可。以下详细阐述下应用业务层面的逻辑框架。应用层代码全部放置于/application文件夹中,其主要的实现文件就3个:mqtt-device.c,mqtt-ota.c,dev_modbus_rtu.c

MQTT框架逻辑

mqtt-device.c中主要实现了两个线程:mqtt.main和mqtt.chk:

static int ali_mqtt_init(void)
{
	rt_thread_t tid;
	
	/* 建立mqtt.main线程 */    
    tid = rt_thread_create("mqtt.main", mqtt_thread_main_thread, RT_NULL, 6 * 1024, RT_THREAD_PRIORITY_MAX / 2, 10);
    if (tid != RT_NULL)
        rt_thread_startup(tid);
	
	/* 建立mqtt.chk线程 */
	tid = rt_thread_create("mqtt.chk", mqtt_connect_check_thread, RT_NULL, 512, RT_THREAD_PRIORITY_MAX / 2 + 1, 10);
    if (tid != RT_NULL)
        rt_thread_startup(tid);

    return 0;
}
INIT_APP_EXPORT(ali_mqtt_init);

1.负责与阿里物联网服务器的通讯建立

if (topic_buff != RT_NULL)
{
	rt_free(topic_buff);
	topic_buff = RT_NULL;
}

/* mqtt_check_load_topic()函数是应用需要订阅和发布的主题全部组合起来,存储于topic_buff缓冲区 */
topic_buff = mqtt_check_load_topic();
if (topic_buff == RT_NULL)
{
	LOG_D("Load MQTT Topic failed!");
	return;
}

while (is_mqtt_exit == 0)
{		
	iotx_http_region_types_t region = IOTX_HTTP_REGION_SHANGHAI;
	IOT_Ioctl(IOTX_IOCTL_SET_REGION, (void *)&region);
	
	/* Initialize MQTT parameter */
	iotx_mqtt_param_t mqtt_params;
    rt_memset(&mqtt_params, 0x0, sizeof(mqtt_params));
	
	mqtt_params.customize_info = MQTT_MAN_INFO_STRING;
	
    /* timeout of request. uint: ms */
    mqtt_params.request_timeout_ms = MQTT_REQUEST_TIMEOUT;	    
    /* internal of keepalive checking: 60s~300s */
    mqtt_params.keepalive_interval_ms = MQTT_KEEPALIVE_INTERNAL * 1000; 
	/* default is 0 */
	mqtt_params.clean_session = 0;		
	/* MQTT read/write buffer size */
    mqtt_params.read_buf_size  = MQTT_MSGLEN;
    mqtt_params.write_buf_size = MQTT_MSGLEN;
    /* configure handle of event */
    mqtt_params.handle_event.h_fp     = ali_mqtt_event_handle;
    mqtt_params.handle_event.pcontext = RT_NULL;             

	/* construct a MQTT device with specify parameter */
    mqtt_client_hd = IOT_MQTT_Construct(&mqtt_params);
    if (RT_NULL == mqtt_client_hd) 
    {
        LOG_D("construct MQTT failed!");
        rt_thread_mdelay(rt_tick_from_millisecond(RT_TICK_PER_SECOND));
		continue;
    }		          
    
    /* 实现消息的订阅 */
    for (int i = 0; i < (sizeof(mqtt_sub_item) / sizeof(mqtt_subscribe_item)); i++)
    {	
    	if (mqtt_sub_item[i].topic_handle_func == RT_NULL)
    		continue;
				
        if (IOT_MQTT_Subscribe(mqtt_client_hd, &topic_buff[i * 128], mqtt_sub_item[i].qos, mqtt_sub_item[i].topic_handle_func, mqtt_sub_item[i].pcontext) < 0)
        {
            LOG_D("IOT_MQTT_Subscribe() failed, topic = %s", &topic_buff[i * 128]);
            goto __do_main_release;
        }         
    }

2.负责执行周期性任务

	IOT_MQTT_Yield(mqtt_client_hd, 200);
	is_mqtt_disconnect = 0;

	/* 每次连接成功后发送一次设备标签信息 */
	mqtt_devtag_task();
	
	/* 只要通讯不出错就一直循环下去 */
    while (is_mqtt_exit == 0)
    {
        /* handle the MQTT packet received from TCP or SSL connection */
        IOT_MQTT_Yield(mqtt_client_hd, 200);

		/* 每10s执行一次周期任务,执行消息的订阅和发布操作 */
		if ((mqtt_period_cnt % 50) == 0)
			mqtt_period_task();

		/* OTA周期执行,检查平台是否下发固件更新指令 */
		extern rt_err_t mqtt_ota(void *mqtt_ota_hd);
		mqtt_ota(mqtt_client_hd);
		
        mqtt_period_cnt++;        
    }
	
    IOT_MQTT_Yield(mqtt_client_hd, 200);
	is_mqtt_disconnect = 1;

	/* OTA模块释放 */
	extern void mqtt_ota_deinit(void);
	mqtt_ota_deinit();

mqtt_period_task() 函数是周期检查订阅的消息的来到,并发送消息队列给modbus线程执行相应动作;

mqtt_ota() 函数是周期检查平台下发的固件升级消息,并执行相应的固件下载存储工作;

dev _ modbus _ rtu.c中主要实现modbus线程:

modbus的实现使用libmodbus软件包,其初始化非常简单:

    modbus_t *ctx;
    ctx = modbus_new_rtu(MODBUS_SERIAL_DEV, MODBUS_SERIAL_BANDRATE, 'N', 8, 1);
	RT_ASSERT(ctx != RT_NULL);

	/* 初始化RS485方向控制引脚 */
#ifdef BSP_USING_SUB_CTRL
    rt_pin_mode(BSP_RS485_DIR_PIN, PIN_MODE_OUTPUT);
    modbus_rtu_set_serial_mode(ctx, BSP_RS485_DIR_PIN);
    modbus_rtu_set_rts(ctx, BSP_RS485_DIR_PIN, MODBUS_RTU_RTS_UP);
#endif

	/* 初始化先设置为广播地址, 从机地址最大不能超过247 */
    modbus_set_slave(ctx, 0);
	/* 打开串口操作,包括波特率设置和串口初始化等 */
    modbus_connect(ctx);
	/* 初始化设置超时时间为1s */
    modbus_set_response_timeout(ctx, 1, 0);

#ifdef DEV_MODBUS_DEBUG
    modbus_set_debug(ctx, RT_TRUE);
#else
	modbus_set_debug(ctx, RT_FALSE);
#endif

modbus是国际标准协议,采用一主多从的架构。平台设备硬件是主节点,所有的事件均由主节点发起,采取一问一答的方式进行通讯。因此modbus线程首先是接收到mqtt.main线程发送的消息队列信号,才开始执行相应的动作:

	err = rt_mq_recv(msg_queue, msg_buff, THE_MAX_QUEUE_MSG_SIZE, THE_QUEUE_TIMEOUT);
	if (err == RT_EOK)		
	{	
		/* 指令消息格式:id=123;door_idx=1,2,3,4,5,6,....,MAXDOOR */
		char *str_id, *str_cmd, *str_val;
		
		/* 任何消息都应该带有id=XXX,解析id */
		str_id = rt_strstr(msg_buff, "=");
		if (str_id == RT_NULL)
			continue;				
		str_val = rt_strstr(msg_buff, ";");
		if (str_val == RT_NULL)
			continue;
		str_id = str_id + 1;
		*str_val = '\0';
		if (str_id == str_val)
			continue;
		
		/* 解析具体命令 */
		str_cmd = str_val + 1;
		str_val = rt_strstr(str_cmd, "=");
		if (str_val)
		{
			extern void mqtt_service_reply_pub(const char *topic_idx, const char *id, const char *code, const char *data);
			
			*str_val = '\0'; str_val++;
			if (!rt_strcasecmp(str_cmd, DOOR_CTRL_CMD))
			{
				rt_uint8_t door_idx, last_door_idx = 0;
				/* 1个字节代表1个门,首先全部初始化为不控制开门:THE_DOOR_CTRL_CLOSE */
				rt_memset(door_coil_buff, THE_DOOR_CTRL_CLOSE, device_chn_num * device_num);
				/* 1个字节代表1个门,首先全部初始化为不控制开门:THE_DOOR_IS_CLOSE */
				rt_memset(door_status_buff, THE_DOOR_IS_CLOSE, device_chn_num * device_num);
				/* 解析消息数据 */
				while (1)
				{
					str_cmd = rt_strstr(str_val, ",");
					if (str_cmd)
					{
						*str_cmd = '\0'; 
						
						door_idx = atoi(str_val);
						if ((door_idx > 0) && (door_idx > last_door_idx) && (door_idx <= (device_chn_num * device_num)))
							door_coil_buff[door_idx - 1] = THE_DOOR_CTRL_OPEN;
						else
							LOG_D("door control message have some error to igiore.");

						str_val = str_cmd + 1;
						last_door_idx = door_idx;
					}
					else
					{
						door_idx = atoi(str_val);
						if ((door_idx > 0) && (door_idx > last_door_idx) && (door_idx <= (device_chn_num * device_num)))
							door_coil_buff[door_idx - 1] = THE_DOOR_CTRL_OPEN;
						else
							LOG_D("door control message have some error to igiore.");
							
						break;
					}
				}

				/* 消息解析完毕进行数据发送 */
				for (int i = 0; i < device_num; i++)
				{
                    /* 设置从机地址,从机地址从1开始 */
					modbus_set_slave(ctx, i + 1);
                    
                    uint32_t timeout_cnt = 0;                                                                                   
                    for (int j = 0; j < device_chn_num; j++)
                    {
                        if (door_coil_buff[i * device_chn_num + j])
                            timeout_cnt++;
                    }
                    if (timeout_cnt)
                        /* 设置超时时间,按照1个门1s超时时间计算 */
                        modbus_set_response_timeout(ctx, timeout_cnt, 0);
                    else
                        /* 当前设备没有需要打开的门,直接跳过 */
                        continue;
                    
					/* 通过MODBUS发送数据,等待回应 */
					if (modbus_write_bits(ctx, 0, device_chn_num, &door_coil_buff[i * device_chn_num]) > 0)
					{
						LOG_D("modbus device[%d] write success.", i + 1);
						THE_DEVICE_COMM_CLEAN(dev_error_buff[i]);
					}
					else
					{
						LOG_D("modbus device[%d] write failed.", i + 1);
						THE_DEVICE_COMM_ERROR(dev_error_buff[i]);
					}
				}

				/* 等待分板执行 */
				rt_thread_mdelay(RT_TICK_PER_SECOND);

				rt_memset(door_fail_buff, 0, device_num * device_chn_num);
				/* 读取门状态值,查看是否打开成功 */
				for (int i = 0; i < device_num; i++)
				{
					/* 设置从机地址,从机地址从1开始 */
					modbus_set_slave(ctx, i + 1);                                                
					/* 设置单个设备的超时时间: 1s */
					modbus_set_response_timeout(ctx, 1, 0);
					/* 通过MODBUS发送数据,等待回应 */
					if (modbus_read_input_bits(ctx, 0, device_chn_num, &door_status_buff[i * device_chn_num]) > 0)
					{
						LOG_D("modbus device[%d] read success.", i + 1);
						THE_DEVICE_COMM_CLEAN(dev_error_buff[i]);
						
						for (int j = 0; j < device_chn_num; j++)
						{
							if ((door_coil_buff[i * device_chn_num + j] == THE_DOOR_IS_OPEN) && (door_coil_buff[i * device_chn_num + j] != door_status_buff[i * device_chn_num + j]))
							{
								LOG_D("door[%d] open failed.", (i * device_chn_num) + j + 1);
								door_fail_buff[i * device_chn_num + j] = 1;
							}
						}
					}
					else
					{
						LOG_D("modbus device[%d] read failed.", i + 1);                            
						THE_DEVICE_COMM_ERROR(dev_error_buff[i]);      
                        
                        for (int j = 0; j < device_chn_num; j++)
						{
							if ((door_coil_buff[i * device_chn_num + j] == THE_DOOR_IS_OPEN) && (door_coil_buff[i * device_chn_num + j] != door_status_buff[i * device_chn_num + j]))
								door_fail_buff[i * device_chn_num + j] = 1;
						}
					}
				}

				/* 构造门打开失败的topic_data */					
				char topic_data[256];
				rt_memset(topic_data, 0, sizeof(topic_data));

				int err_cnt = 0;
				int pos = 0;
				pos += rt_snprintf(&topic_data[pos], sizeof(topic_data) - pos - 1, "%s", "\"open_fail\":\"");
				for (int i = 0; (i < (device_num * device_chn_num)) && (pos < sizeof(topic_data)); i++)
				{
					if (door_fail_buff[i])
					{
						pos += rt_snprintf(&topic_data[pos], sizeof(topic_data) - pos - 1, "%d,", i + 1);
						err_cnt++;
					}
				}

				if (err_cnt > 0)
                {
                    topic_data[rt_strlen(topic_data) - 1] = '\"';
					topic_data[rt_strlen(topic_data)] = '\0';
					mqtt_service_reply_pub(ALI_SERVICE_DOOR_CTRL_REPLY_PUB, str_id, ALI_CODE_DOOR_CTRL_FAIL, topic_data);
                }
				else
					mqtt_service_reply_pub(ALI_SERVICE_DOOR_CTRL_REPLY_PUB, str_id, ALI_CODE_OK, RT_NULL);
			}
			/* 指令消息格式:id=123;ctrl_cmd=xxx;ctrl_para=xxx */
			else if (!rt_strstr(str_cmd, DEV_CTRL_CMD) && !rt_strstr(str_cmd, DEV_CTRL_PARA))
			{											
				str_cmd = str_val + 1;
				str_val = rt_strstr(str_cmd, ";");
				if (str_val == RT_NULL)
					continue;
				str_val = '\0';
				if (!rt_strcasecmp(str_cmd, DEV_CTRL_CMD_OPENTIME))
				{
					str_cmd = str_val + 1;
					str_val = rt_strstr(str_cmd, "=");
					if (str_val == RT_NULL)
						continue;
					str_cmd = str_val + 1;
					rt_uint16_t open_time = atoi(str_cmd);
					LOG_I("set door max open time: %dsec", open_time);

					rt_bool_t is_set_ok = RT_TRUE;
					/* 设置单个设备超时时间: 1s */
					modbus_set_response_timeout(ctx, 1, 0);
					for (int i = 0; i < device_num; i++)
					{
						/* 设置从机地址,从机地址从1开始 */
						modbus_set_slave(ctx, i + 1);									
						/* 写最大开门报警时间参数 */
						if (modbus_write_registers(ctx, MODBUS_MAX_OPEN_TIME_ADDR, MODBUS_MAX_OPEN_TIME_NUM, &open_time) > 0)
						{
							THE_DEVICE_COMM_CLEAN(dev_error_buff[i]);								
						}
						else
						{
							LOG_D("modbus device[%d] door max open time write failed.", i + 1);
							THE_DEVICE_COMM_ERROR(dev_error_buff[i]);

							is_set_ok = RT_FALSE;								
						}
					}

					if (is_set_ok == RT_TRUE)
						mqtt_service_reply_pub(ALI_SERVICE_DEVICE_CTRL_REPLY_PUB, str_id, ALI_CODE_OK, RT_NULL);
					else
						mqtt_service_reply_pub(ALI_SERVICE_DEVICE_CTRL_REPLY_PUB, str_id, ALI_CODE_DEVICE_CTRL_ERROR, RT_NULL);
				}
				else if (!rt_strcasecmp(str_cmd, DEV_CTRL_CMD_POWERTIME))
				{
					str_cmd = str_val + 1;
					str_val = rt_strstr(str_cmd, "=");
					if (str_val == RT_NULL)
						continue;
					str_cmd = str_val + 1;
					rt_uint16_t power_time = atoi(str_cmd);
					LOG_I("set door max power time: %dsec", power_time);

					rt_bool_t is_set_ok = RT_TRUE;
					/* 设置单个设备超时时间: 1s */
					modbus_set_response_timeout(ctx, 1, 0);
					for (int i = 0; i < device_num; i++)
					{
						/* 设置从机地址,从机地址从1开始 */
						modbus_set_slave(ctx, i + 1);									
						/* 写最大开门报警时间参数 */
						if (modbus_write_registers(ctx, MODBUS_MAX_POWER_TIME_ADDR, MODBUS_MAX_POWER_TIME_NUM, &power_time) > 0)
						{
							THE_DEVICE_COMM_CLEAN(dev_error_buff[i]);
						}
						else
						{
							LOG_D("modbus device[%d] door max power time write failed.", i + 1);
							THE_DEVICE_COMM_ERROR(dev_error_buff[i]);

							is_set_ok = RT_FALSE;
						}
					}

					if (is_set_ok == RT_TRUE)
						mqtt_service_reply_pub(ALI_SERVICE_DEVICE_CTRL_REPLY_PUB, str_id, ALI_CODE_OK, RT_NULL);
					else
						mqtt_service_reply_pub(ALI_SERVICE_DEVICE_CTRL_REPLY_PUB, str_id, ALI_CODE_DEVICE_CTRL_ERROR, RT_NULL);
				}
				else
				{
					mqtt_service_reply_pub(ALI_SERVICE_DEVICE_CTRL_REPLY_PUB, str_id, ALI_CODE_DEVICE_CTRL_ERROR, RT_NULL);
				}
			}
		}
	}

消息队列传递的信息包括各种控制命令及相关参数,为了满足不同的平台要求,并且这些内容必须是可变长度且方便协议的增减。权衡利弊因此采用ASCII方式,接收到一帧消息队列信号,首先进行命令和参数解析,然后再执行相应动作。

消息队列的接收超时作为modbus的周期检查:

	else if (err == -RT_ETIMEOUT)	/* 超时代表定时扫描时间到 */
	{
		/* 设置单个设备超时时间: 1s */
		modbus_set_response_timeout(ctx, 1, 0);
		for (int i = 0; i < device_num; i++)
		{
			/* 设置从机地址,从机地址从1开始 */
			modbus_set_slave(ctx, i + 1);				
			/* 读门状态 */
			if (modbus_read_input_bits(ctx, 0, device_chn_num, &door_status_buff[i * device_chn_num]) > 0)
			{
				THE_DEVICE_COMM_CLEAN(dev_error_buff[i]);
			}
			else
			{
				LOG_D("modbus device[%d] door status read failed.", i + 1);
				THE_DEVICE_COMM_ERROR(dev_error_buff[i]);
			}
			/* 读报警状态 */
			if (modbus_read_registers(ctx, MODBUS_REGS_ADDR, MODBUS_ALARM_NUMS, &door_alarm_buff[i * MODBUS_ALARM_NUMS]) > 0)
			{
				THE_DEVICE_COMM_CLEAN(dev_error_buff[i]);
			}
			else
			{
				LOG_D("modbus device[%d] door alarm read failed.", i + 1);
				THE_DEVICE_COMM_ERROR(dev_error_buff[i]);
			}
		}
	}

mqtt-ota.c中主要实现ota的实现:

通过阿里物联网平台可以下载固件,固件下载的状态信息通过MQTT通道,而固件数据的下载使用HTTPS/HTTPS通道。因此如果采用TLS方式,在OTA执行后将增加一倍的内存使用量。

由于使用了FAL组件,将设备板上所有存储区器划分为几个区域:

/* ===================== Flash device Configuration ========================= */
extern const struct fal_flash_dev stm32f4_onchip_flash;
extern struct fal_flash_dev nor_flash0;

/* flash device table */
#define FAL_FLASH_DEV_TABLE                                          \
{                                                                    \
    &stm32f4_onchip_flash,                                           \
    &nor_flash0,                                                     \
}
/* ====================== Partition Configuration ========================== */
#ifdef FAL_PART_HAS_TABLE_CFG
/* partition table */
#define FAL_PART_TABLE                                                               \
{                                                                                    \
    {FAL_PART_MAGIC_WROD,        "app",    "onchip_flash",                           			   0,      1024 * 1024, 0}, \
    {FAL_PART_MAGIC_WROD,    "dl-area",    FAL_USING_NOR_FLASH_DEV_NAME,                           0,      1024 * 1024, 0}, \
    {FAL_PART_MAGIC_WROD,    "df-area",    FAL_USING_NOR_FLASH_DEV_NAME,                 1024 * 1024,      1024 * 1024, 0}, \
    {FAL_PART_MAGIC_WROD,    "kv-area",    FAL_USING_NOR_FLASH_DEV_NAME,        (1024 + 1024) * 1024,      1024 * 1024, 0}, \
    {FAL_PART_MAGIC_WROD,      "elmfs",    FAL_USING_NOR_FLASH_DEV_NAME, (1024 + 1024 + 1024) * 1024, 13 * 1024 * 1024, 0}, \
}
#endif /* FAL_PART_HAS_TABLE_CFG */

存储设备有两个:STM32内部FLASH和W25Q128,其中:

app区域:STM32内部FLASH;

dl-area区域:固件存储区;

df-area区域:出厂固件存储区;

kv-area区域:参数存储区;

elmfs区域:文件系统区;

通过阿里物联网平台下载的代码首先存入dl-area中,然后通过bootloader程序对固件进行判断和搬运;

OTA部分的实现相对比较简单,阿里物联网SDK例程已经给出实例,在mqtt.main线程中周期性调用mqtt_ota()即可:

rt_err_t mqtt_ota(void *mqtt_ota_hd)
{
	rt_err_t result = RT_ERROR;

	if (mqtt_ota_init(mqtt_ota_hd) != RT_EOK)
        goto __mqtt_ota_exit;  
	
	if (IOT_OTA_IsFetching(ota_hd))
	{
		char fm_ver[MQTT_OTA_VERSION_MAXLEN], md5sum[33];		
        IOT_OTA_Ioctl(ota_hd, IOT_OTAG_VERSION, fm_ver, MQTT_OTA_VERSION_MAXLEN);
		IOT_OTA_Ioctl(ota_hd, IOT_OTAG_MD5SUM, md5sum, 33);

		if (!rt_strcasecmp(firmware_verison, fm_ver))
		{
			IOT_OTA_ReportVersion(ota_hd, fm_ver);
			result = RT_EOK;
			goto __mqtt_ota_exit;
		}
		
		const struct fal_partition *dl_partition;
		dl_partition = fal_partition_find(MQTT_OTA_DOWNLOAD_PARTITION_NAME);
		if (dl_partition == RT_NULL)
		{
			LOG_I("can not find %s partition", MQTT_OTA_DOWNLOAD_PARTITION_NAME);
			goto __mqtt_ota_exit;
		}

		if (fal_partition_erase_all(dl_partition) < 0)
		{
			LOG_I("can not erase %s partition", dl_partition->name);
			goto __mqtt_ota_exit;
		}

		mqtt_stop_period_timer();

		int fetch_len;
		rt_uint32_t last_percent = 0, percent = 0;
		rt_uint32_t size_of_download = 0, size_of_file;
		rt_uint32_t content_pos = 0, content_write_sz;
		rt_uint32_t update_grade = 0;;

		/* 循环条件:未下载完成or设备在线 */
		while (!IOT_OTA_IsFetchFinish(ota_hd))
		{			
			fetch_len = IOT_OTA_FetchYield(ota_hd, ota_recv_buff, MQTT_OTA_RECV_BUFF_LEN, 1);						
			if (fetch_len > 0)
			{			
				content_write_sz = fal_partition_write(dl_partition, content_pos, (uint8_t *)ota_recv_buff, fetch_len);
				if (content_write_sz !=  fetch_len)
				{
					LOG_I("Write OTA data to file failed");
					
					IOT_OTA_ReportProgress(ota_hd, IOT_OTAP_BURN_FAILED, RT_NULL);	
					mqtt_ota_deinit();

					mqtt_start_period_timer();
					goto __mqtt_ota_exit;
				}
				else
				{				
					content_pos = content_pos + fetch_len;
					LOG_I("receive %d bytes, total recieve: %d bytes", content_pos, size_of_file);
				}
			}
			else 
			{
				LOG_I("ota fetch failed.");				
				IOT_OTA_ReportProgress(ota_hd, IOT_OTAP_FETCH_FAILED, NULL);	

				if (fetch_len < 0)
				{                		                
	                mqtt_ota_deinit();
					mqtt_start_period_timer();
					goto __mqtt_ota_exit;
				}				
            }			

			/* get OTA information */
            IOT_OTA_Ioctl(ota_hd, IOT_OTAG_FETCHED_SIZE, &size_of_download, 4);
            IOT_OTA_Ioctl(ota_hd, IOT_OTAG_FILE_SIZE, &size_of_file, 4);

			last_percent = percent;
            percent = (size_of_download * 100) / size_of_file;
            if (percent - last_percent > 0) 
			{
				/* 每下载400K上报一次进度 */
				update_grade = (update_grade + 1) % (((MQTT_OTA_RECV_BUFF_LEN - 1) / 1024) * 50);
				if (update_grade == 0)
                	IOT_OTA_ReportProgress(ota_hd, (IOT_OTA_Progress_t)percent, RT_NULL);
            }
			
            IOT_MQTT_Yield(mqtt_ota_hd, 100);
		}
		
		IOT_OTA_Ioctl(ota_hd, IOT_OTAG_MD5SUM, md5sum, 33);
        IOT_OTA_Ioctl(ota_hd, IOT_OTAG_VERSION, fm_ver, MQTT_OTA_VERSION_MAXLEN);

		uint32_t firmware_valid;
		IOT_OTA_Ioctl(ota_hd, IOT_OTAG_CHECK_FIRMWARE, &firmware_valid, 4);
        if ((firmware_valid) && (size_of_download == size_of_file) && (size_of_file > 0))
		{
            LOG_D("The firmware is valid!  Download firmware successfully.");

            LOG_D("OTA FW version: %s", fm_ver);
			LOG_D("OTA FW MD5 Sum: %s", md5sum);

			ef_set_env_blob(MQTT_OTA_FIRMWARE_VERSION, fm_ver, rt_strlen(fm_ver));
			IOT_OTA_ReportVersion(ota_hd, fm_ver);
			result = RT_EOK;			
        }

		mqtt_ota_deinit();
		mqtt_start_period_timer();
	}
	
__mqtt_ota_exit:
	return result;
}

软件包使用说明

本次使用的软件包比较多,分类如下:

  • MQTT和OTA使用的软件包:

    ali-iotkit: RTT官方移植的阿里物联网SDK,使用的最新的V3.0.2版本;

    cJSON:用于解析MQTT的JSON消息包;

    mbedtls:用于MQTT和OTA通讯的TLS加密;

    at_device:GPRS模块AT命令包;

  • 子模块通讯使用的软件包:

    libmodbus:modbus软件包,可以实现主从节点的modbus协议;

    SignalLed:用于通讯LED的闪烁控制;

  • 存储使用的软件包:

    FAL:用于存储区域划分;

    EasyFlash:用于KV参数的存储,可以实现平衡擦除和断电备份,同时也可以实现二进制存储,使用非常方便;

  • 调试使用的软件包:

    adbd:主要用于shell调试和文件的传输;

    ota_downloader:ota _downloader包含HTTP和ymodem协议的软件包,这里我们只用于测试和生产下线使用;

    nettutils:网络工具包,用于网络相关测试使用;

演示效果展示

硬件设备展示

设备样品图

智能柜主设备的虚拟串口连接

虚拟串口图

由于使用了虚拟串口,上电延时后基本就看不到RTT的开机画面,直接显示MQTT的连接信息

MQTT连接信息图

为了方便调试和文件传送添加使用ADBD包,该软件包使用非常方便

ADBD图

智能主设备通过GPRS/以太网连接阿里物联网平台

MQTT平台连接图 MQTT平台连接图

智能柜主设备向阿里物联网平台订阅和发布的TOPIC

MQTT平台TOPIC图

智能主设备使用阿里物联网平台进行OTA操作

MQTT-OTA连接图

在阿里物联网平台上进行设备功能调试

MQTT平台调试图 MQTT平台调试图

代码地址

https://gitee.com/spunky_973/netbox

注意事项

  • 本次参赛意在感谢对RT-Thread操作系统的感谢,能够为RT-Thread系统做宣传和推广;
  • 本次参赛作品是在成熟产品上进行裁剪后的实例,虽然水平不及RT-Thread官方的代码,但我相信仍然对年轻的嵌入式工程师有帮助;
  • 本项目的OTA部分的bootloader已经开源:https://gitee.com/spunky_973/rt-fota,只需要将此代码工程的链接文件和向量偏移地址进行修改即可,具体参照RT-FOTA的README文件。

联系人信息

维护人: 王希

C
1
https://gitee.com/donsi/netbox.git
git@gitee.com:donsi/netbox.git
donsi
netbox
netbox
dev

搜索帮助