安防监控项目---Cortex-A9和zigbee传感器数据上传至网页

文章目录

  • 前言
  • 一、A9平台数据的采集与上传
  • 二、共享内存上传数据到CGI
  • 三、设备代码
  • 总结


前言

书接上期,我们大概来梳理一下,已经完成的需求有哪些了,从html下发指令控制Cortex-A9平台硬件,其中主要实现的有控制LED,蜂鸣器,LED模拟数码管,这些是已经实现的硬件设备,实现了html和Cortex-A9平台的通信,是不是主要通过boa服务器呢,仔细想一下,是不是主要依赖于boa服务器搭载了CGI接口呢,那在CGI中我们进行了CGI编程,使用接口库函数进行网页(form表单)数据的接收,并且呢使用消息队列进行应用层的进程间通信,进而主框架进行客户端请求的处理,判断消息类型,唤醒相应等待线程!这样梳理下来思路是不是非常清晰了;那之前还完成了什么呢,是不是也完成了通过Cortex-A9平台控制zigbee硬件设备,这块需要注意的是linux下的串口编程,其实只要大家经常使用串口调试助手,那么这块的代码理解肯定不是问题,只不过这里把图形化的窗口进行了具体的软件实现,其实本质上来讲,就算是Windows下的UI界面,也是通过软件编写来实现的,所以原理都是一样的哈!
那么接下来要实现的是什么操作呢,接下来还需要将采集到的数据上传到网页对不对,所以本期我们的分享呢主要是实现安防监控项目中的环境数据的采集和上传!接下来带大家看一下吧!


一、A9平台数据的采集与上传

首先呢A9平台上需要上传的数据有两个,一个是MPU6050姿态传感器的加速度和角速度,另一个呢是板载的ADC模块,将采集到的数据转化为具体的电压值在网页端进行显示;
这一部分呢主要是应用层实现的事情,接下来先从应用层看起,先来看一下接收zigbee数据和采集A9平台传感器的数据,这个功能是通过pthread_transfer.c线程来实现的,具体来分析一下:

#include "data_global.h"
#include "common.h"
#include "mpu6050.h"
#include 

//接收ZigBee的数据和采集的A9平台的传感器数据
int adc_fd;
int mpu_fd;

extern pthread_cond_t cond_transfer;
extern pthread_mutex_t mutex_transfer;
extern struct env_info_client_addr  sm_all_env_info;
//调试代码,可以进行随意赋值,但是网页实时数据肯定不会进行变化
int file_env_info_a9_zigbee_debug(struct env_info_client_addr *rt_status,int home_id);
//真实硬件采集代码,数据实时采集
int file_env_info_a9_zigbee_stm32(struct env_info_client_addr *rt_status,int home_id);
//打印传感器的数据,开发人员可以再终端调试使用
int printf_sensor_info_debug(struct env_info_client_addr  *sm_all_env_info,int home_id);

//数据采集线程
void *pthread_transfer(void *arg)
{
	int home_id = 1;
	adc_fd = open(ADC_DEV,O_RDWR);				//打开ADC设备节点
	mpu_fd = open(MPU6050_DEV,O_RDWR);			//打开MPU6050设备节点
	if((adc_fd == -1) || (mpu_fd == -1)){
		printf("open adc or mpu device failed.\n");
	}
	while(1){
		pthread_mutex_lock(&mutex_transfer);					//上锁
		pthread_cond_wait(&cond_transfer,&mutex_transfer);		//线程睡眠等待被唤醒
//		printf("pthread_analysis and tranfer.\n");
#if 1
		//进行真实数据上传
		file_env_info_a9_zigbee_stm32(&sm_all_env_info,home_id);
#else	
		//调试使用代码
		file_env_info_a9_zigbee_debug(&sm_all_env_info,home_id);
#endif
		发送完毕后解锁
		pthread_mutex_unlock(&mutex_transfer);
		sleep(1);
	}
	close(adc_fd);
	close(mpu_fd);
}
//测试用例,可以看出都是常量赋值
int file_env_info_a9_zigbee_debug(struct env_info_client_addr *rt_status,int home_id)
{
	static int temp = 0;
	int  env_info_size = sizeof(struct env_info_client_addr);
	//	printf("env_info_size = %d.\n",env_info_size);
	
	rt_status->monitor_no[home_id].zigbee_info.temperature = 10.0;
	rt_status->monitor_no[home_id].zigbee_info.tempMIN = 2.0;
	rt_status->monitor_no[home_id].zigbee_info.tempMAX = 20.0;
	rt_status->monitor_no[home_id].zigbee_info.humidity  = 20.0;
	rt_status->monitor_no[home_id].zigbee_info.humidityMIN  = 10.0;
	rt_status->monitor_no[home_id].zigbee_info.humidityMAX  = 30.0;
	rt_status->monitor_no[home_id].zigbee_info.reserved[0]  = 0.01;
	rt_status->monitor_no[home_id].zigbee_info.reserved[1]  = -0.01;
	
	//模拟ADC数据
	temp ++;
	rt_status->monitor_no[home_id].a9_info.adc  = temp;
	rt_status->monitor_no[home_id].a9_info.gyrox  = -14.0;
	rt_status->monitor_no[home_id].a9_info.gyroy  = 20.0;
	rt_status->monitor_no[home_id].a9_info.gyroz  = 40.0;
	rt_status->monitor_no[home_id].a9_info.aacx  = 642.0;
	rt_status->monitor_no[home_id].a9_info.aacy  = -34.0;
	rt_status->monitor_no[home_id].a9_info.aacz  = 5002.0;
	rt_status->monitor_no[home_id].a9_info.reserved[0]  = 0.01;
	rt_status->monitor_no[home_id].a9_info.reserved[1]  = -0.01;
//	printf_sensor_info_debug(rt_status,home_id);
	//添加stm32部分的数据、arduino数据,
	return 0;
}
//此函数未用到
#if 0
int	get_sensor_data_from_a9(struct makeru_a9_info* a9_sensor_data)
{
	int adc_sensor_data;
	struct mpu6050_data data;

	/*get adc sensor data*/
	read(adc_fd,&adc_sensor_data,4); 
	printf("adc value :%0.2fV.\n",(1.8*adc_sensor_data)/4096);  

	/* get mpu6050 sensor data*/
	ioctl(mpu_fd,MPU6050_GYRO,&data);
	printf("gyro data: x = %05d, y = %05d, z = %05d\n", data.gyro.x,data.gyro.y,data.gyro.z);
	ioctl(mpu_fd,MPU6050_ACCEL,&data);
	printf("accel data: x = %05d, y = %05d, z = %05d\n", data.accel.x,data.accel.y,data.accel.z);

	/*预填充,有点浪费空间,大家可以优化一下*/
	a9_sensor_data->adc = (1.8 * adc_sensor_data)/4096 * 100; //定义未int32,应该是float,放大100倍,保护小数位
	a9_sensor_data->gyrox = data.gyro.x;
	a9_sensor_data->gyroy = data.gyro.y;
	a9_sensor_data->gyroz = data.gyro.z;
	a9_sensor_data->aacx  = data.accel.x;
	a9_sensor_data->aacy  = data.accel.y;
	a9_sensor_data->aacz  = data.accel.z;

	return 0;
}
#endif 

下面这个函数至关重要,可以看到不仅有硬件的真实数据赋值,也有模拟数据的赋值,当然最后的目标还是要把所有的真实数据都赋值

int file_env_info_a9_zigbee_stm32(struct env_info_client_addr *rt_status,int home_id)
{
	int  env_info_size = sizeof(struct env_info_client_addr);
//	printf("env_info_size = %d.\n",env_info_size);

	rt_status->monitor_no[home_id].zigbee_info.head[0]  = 'm';
	rt_status->monitor_no[home_id].zigbee_info.head[1]  = 's';
	rt_status->monitor_no[home_id].zigbee_info.head[2]  = 'm';		//前三个字符是表示为表示安防监控
	rt_status->monitor_no[home_id].zigbee_info.head[3]  = 'z';		//'z'表示zigbee,'a'表示A9
	rt_status->monitor_no[home_id].zigbee_info.temperature = 10.0;
	rt_status->monitor_no[home_id].zigbee_info.tempMIN = 2.0;
	rt_status->monitor_no[home_id].zigbee_info.tempMAX = 20.0;
	rt_status->monitor_no[home_id].zigbee_info.humidity  = 20.0;
	rt_status->monitor_no[home_id].zigbee_info.humidityMIN  = 10.0;
	rt_status->monitor_no[home_id].zigbee_info.humidityMAX  = 30.0;
	rt_status->monitor_no[home_id].zigbee_info.reserved[0]  = 0.01;
	rt_status->monitor_no[home_id].zigbee_info.reserved[1]  = -0.01;

	//获取数据     
	int adc_sensor_data;
	struct mpu6050_data data;
	/*get adc sensor data*/
	read(adc_fd,&adc_sensor_data,4); 
//	printf("adc value :%0.2fV.\n",(1.8*adc_sensor_data)/4096);  
	rt_status->monitor_no[home_id].a9_info.adc    = (float)((1.8*adc_sensor_data)/4096);
	
	/* get mpu6050 sensor data*/
	ioctl(mpu_fd,MPU6050_GYRO,&data);
//	printf("gyro data: x = %d, y = %d, z = %d\n", data.gyro.x,data.gyro.y,data.gyro.z);
	ioctl(mpu_fd,MPU6050_ACCEL,&data);
//	printf("accel data: x = %d, y = %d, z = %d\n", data.accel.x,data.accel.y,data.accel.z);
	
	rt_status->monitor_no[home_id].a9_info.head[0]  = 'm';
	rt_status->monitor_no[home_id].a9_info.head[1]  = 's';
	rt_status->monitor_no[home_id].a9_info.head[2]  = 'm';
	rt_status->monitor_no[home_id].a9_info.head[3]  = 'a';

	rt_status->monitor_no[home_id].a9_info.gyrox  =  (short)data.gyro.x; //获取角速度
	rt_status->monitor_no[home_id].a9_info.gyroy  =  (short)data.gyro.y;
	rt_status->monitor_no[home_id].a9_info.gyroz  =  (short)data.gyro.z;

	rt_status->monitor_no[home_id].a9_info.aacx   =  (short)data.accel.x; //获取加速度
	rt_status->monitor_no[home_id].a9_info.aacy   =  (short)data.accel.y;
	rt_status->monitor_no[home_id].a9_info.aacz   =  (short)data.accel.z;
	rt_status->monitor_no[home_id].a9_info.reserved[0]  = 0.01;
	rt_status->monitor_no[home_id].a9_info.reserved[1]  = -0.01;
	
	//printf_sensor_info_debug(rt_status,home_id);
	//添加stm32部分的数据、arduino数据
	return 0;
}

说到这里呢,直接给大家展示一下图片,现象更加直观一点;

大家可以看到温度和湿度都是模拟数据,这个是我的原因哈,还没实现,但是ADC和MPU6050的数据是OK的,下面我还需要继续努力,搞好了给大家分享出来;

//打印采集的传感器数据
int printf_sensor_info_debug(struct env_info_client_addr  *sm_all_env_info,int home_id)
{
	printf("a9_info.adc  : %f.\n",sm_all_env_info->monitor_no[home_id].a9_info.adc  );
	printf("a9_info.gyrox: %d.\n",sm_all_env_info->monitor_no[home_id].a9_info.gyrox);
	printf("a9_info.gyroy: %d.\n",sm_all_env_info->monitor_no[home_id].a9_info.gyroy);
	printf("a9_info.gyroz: %d.\n",sm_all_env_info->monitor_no[home_id].a9_info.gyroz);
	printf("a9_info.aacx : %d.\n",sm_all_env_info->monitor_no[home_id].a9_info.aacx );
	printf("a9_info.aacy : %d.\n",sm_all_env_info->monitor_no[home_id].a9_info.aacy );
	printf("a9_info.aacz : %d.\n",sm_all_env_info->monitor_no[home_id].a9_info.aacz );
	printf("a9_info.reserved[0]: %d.\n",sm_all_env_info->monitor_no[home_id].a9_info.reserved[0] );
	printf("a9_info.reserved[1]: %d.\n",sm_all_env_info->monitor_no[home_id].a9_info.reserved[1] );
	return 0;
}

大家千万不要单独只看代码,一定得看我们前期的通信结构体设计的部分,才能够知道每一个结构体包括结构体成员表示的什么意义,结合注释理解,肯定是能够拿下这部分的;

二、共享内存上传数据到CGI

下面带大家来看一下这个数据刷新线程,究竟接收到的数据是如何一步步上传到网页的,数据刷新线程肯定离不开数据接收线程,所以它们之间应该存在先后逻辑顺序,各位小伙伴们能想到什么呢,是不是条件变量实现同步,哈哈哈,来看下这个数据刷新线程吧!
这里呢我先把上面数据采集线程的核心代码拿下来,大家对比着看;
安防监控项目---Cortex-A9和zigbee传感器数据上传至网页_第1张图片
上图线程中while循环中先上锁,睡眠等待被唤醒,接下来咱这个数据刷新线程就是把它唤醒的线程:

首先是共享内存消息队列和信号量的id号,条件变量和互斥锁数据结构体以及函数声明;

#include "data_global.h"
#include "sem.h"

#define N 1024  //for share memory

extern int shmid;
extern int msgid;
extern int semid;

extern key_t shm_key;
extern key_t sem_key;
extern key_t key; //msg_key

extern pthread_mutex_t mutex_client_request,
	   mutex_refresh,
	   mutex_sqlite,
	   mutex_transfer,
	   mutex_analysis,
	   mutex_sms,
	   mutex_buzzer,
	   mutex_led,
	   mutex_camera;

extern pthread_cond_t  cond_client_request,
	   cond_refresh,
	   cond_sqlite,
	   cond_transfer,
	   cond_analysis,
	   cond_sms,
	   cond_buzzer,
	   cond_led,
	   cond_camera;
extern struct env_info_client_addr  sm_all_env_info;

struct shm_addr
{
	char shm_status;   //shm_status可以等于home_id,用来区分共享内存数据
	struct env_info_client_addr  sm_all_env_info;
};
struct shm_addr *shm_buf;

int file_env_info_struct(struct env_info_client_addr  *rt_status,int home_id);//模拟数据刷新的函数

下面这部分就是核心代码,大家一定要结合上面的数据采集线程一起理解,加深条件变量和互斥锁的使用;

void *pthread_refresh(void *arg)
{
	//semaphore for access to resource limits 创建IPC对象
	if((sem_key = ftok("/tmp",'i')) < 0){
		perror("ftok failed .\n");
		exit(-1);
	}
	
	//创建信号量集
	semid = semget(sem_key,1,IPC_CREAT|IPC_EXCL|0666);
	if(semid == -1)	{					//创建失败
		if(errno == EEXIST){			//信号量集和已经存在
			semid = semget(sem_key,1,0777);
		}else{
			perror("fail to semget");
			exit(1);
		}
	}else{
		init_sem (semid, 0, 1);		//创建成功则初始化
	}

	//share memory for env_info refresh config
	if((shm_key = ftok("/tmp",'i')) < 0){
		perror("ftok failed .\n");
		exit(-1);
	}

	shmid = shmget(shm_key,N,IPC_CREAT|IPC_EXCL|0666);
	if(shmid == -1)	{
		if(errno == EEXIST){
			shmid = shmget(key,N,0777);
		}else{
			perror("fail to shmget");
			exit(1);
		}
	}

	//share memap
	if((shm_buf = (struct shm_addr *)shmat(shmid,NULL,0)) == (void *)-1)
	{
		perror("fail to shmat");
		exit(1);
	}
	printf("pthread_refresh ......>>>>>>>\n");
	bzero (shm_buf, sizeof (struct shm_addr));
	while(1){
		sem_p(semid,0); //P操作
		shm_buf->shm_status = 1;
		int home_id = 1;
#if 1
		shm_buf->sm_all_env_info.monitor_no[home_id] = sm_all_env_info.monitor_no[home_id];  //真实数据上传
#else
		file_env_info_struct(&shm_buf->sm_all_env_info,shm_buf->shm_status); //模拟数据上传
#endif 
		sleep(1);
		sem_v(semid,0); //v操作
		pthread_cond_signal(&cond_transfer);			//真实的数据上传完成后,释放信号量唤醒数据采集线程进行数据采集赋值,紧接着进行第二轮的数据刷新(这个操作就是实现了同步)
	}
}
//模拟数据填充
int file_env_info_struct(struct env_info_client_addr *rt_status,int home_id)
{
	int  env_info_size = sizeof(struct env_info_client_addr);
	//	printf("env_info_size = %d.\n",env_info_size);

	rt_status->monitor_no[home_id].zigbee_info.temperature = 10.0;
	rt_status->monitor_no[home_id].zigbee_info.tempMIN = 2.0;
	rt_status->monitor_no[home_id].zigbee_info.tempMAX = 20.0;
	rt_status->monitor_no[home_id].zigbee_info.humidity  = 20.0;
	rt_status->monitor_no[home_id].zigbee_info.humidityMIN  = 10.0;
	rt_status->monitor_no[home_id].zigbee_info.humidityMAX  = 30.0;
	rt_status->monitor_no[home_id].zigbee_info.reserved[0]  = 0.01;
	rt_status->monitor_no[home_id].zigbee_info.reserved[1]  = -0.01;


	rt_status->monitor_no[home_id].a9_info.adc  = 9.0;
	rt_status->monitor_no[home_id].a9_info.gyrox  = -14.0;
	rt_status->monitor_no[home_id].a9_info.gyroy  = 20.0;
	rt_status->monitor_no[home_id].a9_info.gyroz  = 40.0;
	rt_status->monitor_no[home_id].a9_info.aacx  = 642.0;
	rt_status->monitor_no[home_id].a9_info.aacy  = -34.0;
	rt_status->monitor_no[home_id].a9_info.aacz  = 5002.0;
	rt_status->monitor_no[home_id].a9_info.reserved[0]  = 0.01;
	rt_status->monitor_no[home_id].a9_info.reserved[1]  = -0.01;

	//添加stm32部分的数据、arduino数据,

	return 0;
}

三、设备代码

这里和大家也分享一下MPU6050的寄存器文件,大家可以看到对硬件的读取都是通过加载驱动进而通过ioctl实现的,这样将功能模块化,也是我们的良好习惯;

#ifndef __MPU6050_H
#define __MPU6050_H

//	 
//#define MPU_ACCEL_OFFS_REG		0X06	//accel_offs寄存器,可读取版本号,寄存器手册未提到
//#define MPU_PROD_ID_REG			0X0C	//prod id寄存器,在寄存器手册未提到
#define MPU_SELF_TESTX_REG		0X0D	//自检寄存器X
#define MPU_SELF_TESTY_REG		0X0E	//自检寄存器Y
#define MPU_SELF_TESTZ_REG		0X0F	//自检寄存器Z
#define MPU_SELF_TESTA_REG		0X10	//自检寄存器A
#define MPU_SAMPLE_RATE_REG		0X19	//采样频率分频器
#define MPU_CFG_REG				0X1A	//配置寄存器
#define MPU_GYRO_CFG_REG		0X1B	//陀螺仪配置寄存器
#define MPU_ACCEL_CFG_REG		0X1C	//加速度计配置寄存器
#define MPU_MOTION_DET_REG		0X1F	//运动检测阀值设置寄存器
#define MPU_FIFO_EN_REG			0X23	//FIFO使能寄存器
#define MPU_I2CMST_CTRL_REG		0X24	//IIC主机控制寄存器
#define MPU_I2CSLV0_ADDR_REG	0X25	//IIC从机0器件地址寄存器
#define MPU_I2CSLV0_REG			0X26	//IIC从机0数据地址寄存器
#define MPU_I2CSLV0_CTRL_REG	0X27	//IIC从机0控制寄存器
#define MPU_I2CSLV1_ADDR_REG	0X28	//IIC从机1器件地址寄存器
#define MPU_I2CSLV1_REG			0X29	//IIC从机1数据地址寄存器
#define MPU_I2CSLV1_CTRL_REG	0X2A	//IIC从机1控制寄存器
#define MPU_I2CSLV2_ADDR_REG	0X2B	//IIC从机2器件地址寄存器
#define MPU_I2CSLV2_REG			0X2C	//IIC从机2数据地址寄存器
#define MPU_I2CSLV2_CTRL_REG	0X2D	//IIC从机2控制寄存器
#define MPU_I2CSLV3_ADDR_REG	0X2E	//IIC从机3器件地址寄存器
#define MPU_I2CSLV3_REG			0X2F	//IIC从机3数据地址寄存器
#define MPU_I2CSLV3_CTRL_REG	0X30	//IIC从机3控制寄存器
#define MPU_I2CSLV4_ADDR_REG	0X31	//IIC从机4器件地址寄存器
#define MPU_I2CSLV4_REG			0X32	//IIC从机4数据地址寄存器
#define MPU_I2CSLV4_DO_REG		0X33	//IIC从机4写数据寄存器
#define MPU_I2CSLV4_CTRL_REG	0X34	//IIC从机4控制寄存器
#define MPU_I2CSLV4_DI_REG		0X35	//IIC从机4读数据寄存器

#define MPU_I2CMST_STA_REG		0X36	//IIC主机状态寄存器
#define MPU_INTBP_CFG_REG		0X37	//中断/旁路设置寄存器
#define MPU_INT_EN_REG			0X38	//中断使能寄存器
#define MPU_INT_STA_REG			0X3A	//中断状态寄存器

#define MPU_ACCEL_XOUTH_REG		0X3B	//加速度值,X轴高8位寄存器
#define MPU_ACCEL_XOUTL_REG		0X3C	//加速度值,X轴低8位寄存器
#define MPU_ACCEL_YOUTH_REG		0X3D	//加速度值,Y轴高8位寄存器
#define MPU_ACCEL_YOUTL_REG		0X3E	//加速度值,Y轴低8位寄存器
#define MPU_ACCEL_ZOUTH_REG		0X3F	//加速度值,Z轴高8位寄存器
#define MPU_ACCEL_ZOUTL_REG		0X40	//加速度值,Z轴低8位寄存器

#define MPU_TEMP_OUTH_REG		0X41	//温度值高八位寄存器
#define MPU_TEMP_OUTL_REG		0X42	//温度值低8位寄存器

#define MPU_GYRO_XOUTH_REG		0X43	//陀螺仪值,X轴高8位寄存器
#define MPU_GYRO_XOUTL_REG		0X44	//陀螺仪值,X轴低8位寄存器
#define MPU_GYRO_YOUTH_REG		0X45	//陀螺仪值,Y轴高8位寄存器
#define MPU_GYRO_YOUTL_REG		0X46	//陀螺仪值,Y轴低8位寄存器
#define MPU_GYRO_ZOUTH_REG		0X47	//陀螺仪值,Z轴高8位寄存器
#define MPU_GYRO_ZOUTL_REG		0X48	//陀螺仪值,Z轴低8位寄存器

#define MPU_I2CSLV0_DO_REG		0X63	//IIC从机0数据寄存器
#define MPU_I2CSLV1_DO_REG		0X64	//IIC从机1数据寄存器
#define MPU_I2CSLV2_DO_REG		0X65	//IIC从机2数据寄存器
#define MPU_I2CSLV3_DO_REG		0X66	//IIC从机3数据寄存器

#define MPU_I2CMST_DELAY_REG	0X67	//IIC主机延时管理寄存器
#define MPU_SIGPATH_RST_REG		0X68	//信号通道复位寄存器
#define MPU_MDETECT_CTRL_REG	0X69	//运动检测控制寄存器
#define MPU_USER_CTRL_REG		0X6A	//用户控制寄存器
#define MPU_PWR_MGMT1_REG		0X6B	//电源管理寄存器1
#define MPU_PWR_MGMT2_REG		0X6C	//电源管理寄存器2 
#define MPU_FIFO_CNTH_REG		0X72	//FIFO计数寄存器高八位
#define MPU_FIFO_CNTL_REG		0X73	//FIFO计数寄存器低八位
#define MPU_FIFO_RW_REG			0X74	//FIFO读写寄存器
#define MPU_DEVICE_ID_REG		0X75	//器件ID寄存器
 
//如果AD0脚(9脚)接地,IIC地址为0X68(不包含最低位).
//如果接V3.3,则IIC地址为0X69(不包含最低位).
#define MPU_ADDR				0X68

因为开发板接GND,所以转为读写地址后,为0XD1和0XD0(如果接GND,则为0XD3和0XD2)  
//#define MPU_READ    0XD1
//#define MPU_WRITE   0XD0
#endif

上述就是应用层的全部实现过程了,还是那句话,驱动一定必须加载起来哦!


总结

到这里呢是不是发现这个项目做的东西已经很多了,又要移植boa,又要移植CGI,还要CGI编程,还要实现网页和A9平台的通信,还得实现A9与html的数据上传,确实挺麻烦的,这些其实都是最基础的,后面我们还需要实现GPRS报警,并且实现视频数据流的上传,这样才能彻底的安防监控!最后,各位小伙伴们如果有收获,可以点赞收藏哦,你们的认可是我创作的动力,一起加油!

你可能感兴趣的:(安防监控项目,单片机,嵌入式硬件,linux,arm开发,安防监控,Crotex-A9)