IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> c# 与stm32之间结构体的收发 -> 正文阅读

[嵌入式]c# 与stm32之间结构体的收发

基础知识

大小端

我们都知道,对于一个超过一个字节的数据,其在计算机中的存储需要跨越字节。
某些机器选择在存储器中按照从最低为有效字节到最高有效字节的顺序存储对象,而另一些机器则按照从最高为有效字节到到最低为有效字节的顺序存储,
前一种存储方式被称为小端存储,后一种方式被称为大端存储。

举例:对于十六进制数0x01234567,其字节的存储顺序便依赖于机器,如下:也就是大端就是正常的顺序,高位在前。
在这里插入图片描述
stm32和PC都是大端 但需要注意的是他们之间都需要设置为1字节对齐(stm32默认用的是4字节对齐)

byte数组与结构体之间的转换

byte数组与结构体之间的转换 参考1
byte数组与结构体之间的转换 参考2
c#中采用内存方式转换字节数组与结构体需要使用一个单独的命名空间(海康提供的二次开发c#API里面也是这样操作的)

using System.Runtime.InteropServices;

1、结构体前面
[StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
固定这样写就好了

  • 这是C#引用非托管的C/C++的DLL的一种定义定义结构体的方式,主要是为了内存中排序
  • LayoutKind:有两个属性Sequential和Explicit,Sequential表示顺序存储,结构体内数据在内存中都是顺序存放的
  • CharSet=CharSet.Ansi:表示编码方式。这都是为了使用非托管的指针准备的,这两点大家记住就可以。
  • 需要注意的是 Pack = 1 这个特性:它代表了结构体的字节对齐方式,在实际开发中,C++开发环境开始默认是2字节对齐方式 ,拿上面报文包头结构体为例,char类型在虽然在内存中至占用一个字节,但在结构体转为字节数组时,系统会自动补齐两个字节,所以如果C#这面定义为Pack=1,C++默认为2字节对齐的话,双方结构体会出现长度不一致的情况,相互转换时必然会发生错位,所以需要大家都默认1字节对齐的方式,C#定义Pack=1,C++ 添加 #pragma pack 1,保证结构体中字节对齐方式一致。使用为1就行了

2、数组

  • 数组的定义,结构体中每个成员的长度都是需要明确的,因为内存需要根据这个分配空间,而C#结构体中数组是无法进行初始化的,这里我们需要在成员声明时进行定义;
  • 一些基本的数据类型,C#与C++都是可以匹配的,也就是不需要变动。

UnmanagedType 枚举:指定如何将参数或字段封送到非托管代码。

MarshalAsAttribute.ArraySubType:如果是数组的话,指定数组元素的类型
下面这句话的意思就是 下面是长度为12的里面装的是结构体的数组。有些就是里面装的都是浮点型啊 整形啥的

[MarshalAs(UnmanagedType.ByValArray, SizeConst = 12, ArraySubType = UnmanagedType.Struct)]

收发的转换

我觉得 发送数字那么传输的就是他们的值转成的二进制。而发送字符,则是发送字符的ASCII值。发送中文,那就需要看是具体用的什么编解码方式。

实验讲解

上位机发送 25 01 下位机发送结构体 上位机将结构体显示出来
添加监视后看到(这样看的更全面,需要打断点才能使用该功能的。也就是先打断点,然后运行程序,找到需要的,选择添加监视)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

上位机代码

Byte2Struct.cs

引用

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;

定义的结构体

    //结构体 一定要加public才能被调用
	/* 加速度信息结构体-XYZ三分量 */
	[StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
	struct CSModuleInfo_ACC
	{
        //一些基本的数据类型,C#与C++都是可以匹配的:也就是用到数组的时候用那些提示 其他不用
        public float _acc_X;
        public float _acc_Y;
        public float _acc_Z;
	}


	/* 经纬度信息结构体-经纬两分量 */
	[StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
	struct CSmouduleInfo_LL
	{
        //[MarshalAs(UnmanagedType.R4)] 不加也没事 加的话用这个样子的把 
        public float _latitude;
        //[MarshalAsAttribute(UnmanagedType.R4)]//R4 代表4字节浮点数。 也不知道与上面那个有什么区别
        public float _longitude;
	}


	/* 测控站信息结构体 */
	[StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]//这样写就可以了
	struct CSInfoStrcutre
	{
        /* 核心温度 MCU温度 */
        [MarshalAs(UnmanagedType.U1, SizeConst = 1)]//这里看出来 加了也没事
        public byte _temp_O_MCU;
        /* 气温 */
        public float _temp_env;
		/* 气压 */
		[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2, ArraySubType = UnmanagedType.U1)]//两字节数组 每个是1 字节无符号整数。
		public byte[] gp;
        /* 加速度 */
        public CSModuleInfo_ACC acc;
        /* 经纬度 */
        public CSmouduleInfo_LL ll;
	}

字节转结构体 结构体转字节函数

	class Byte2Struct
    {
         <summary>
        /// 结构体转byte数组
        /// </summary>
        /// <param name="structObj">要转换的结构体</param>
        /// <returns>转换后的byte数组</returns>
        public static byte[] StructToBytes(object structObj)
        {
            //得到结构体的大小
            int size = Marshal.SizeOf(structObj);
            //创建byte数组
            byte[] bytes = new byte[size];
            //分配结构体大小的内存空间
            IntPtr structPtr = Marshal.AllocHGlobal(size);
            //将结构体拷到分配好的内存空间
            Marshal.StructureToPtr(structObj, structPtr, false);
            //从内存空间拷到byte数组
            Marshal.Copy(structPtr, bytes, 0, size);
            //释放内存空间
            Marshal.FreeHGlobal(structPtr);
            //返回byte数组
            return bytes;
        }

        /// <summary>
        /// byte数组转结构体
        /// </summary>
        /// <param name="bytes">byte数组</param>
        /// <param name="type">结构体类型</param>
        /// <returns>转换后的结构体</returns>
        public static object BytesToStuct(byte[] bytes, Type type)
        {
            //得到结构体的大小
            int size = Marshal.SizeOf(type);
            //byte数组长度小于结构体的大小
            if (size > bytes.Length)
            {
                //返回空
                return null;
            }
            //分配结构体大小的内存空间
            IntPtr structPtr = Marshal.AllocHGlobal(size);
            //将byte数组拷到分配好的内存空间
            Marshal.Copy(bytes, 0, structPtr, size);
            //将内存空间转换为目标结构体
            object obj = Marshal.PtrToStructure(structPtr, type);
            //释放内存空间
            Marshal.FreeHGlobal(structPtr);
            //返回结构体
            return obj;
        }
    }

Form1.cs

引用

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Sunny.UI;
using System.Threading;

定义一个串口

public static SerialPort serialPort1 = new SerialPort();

核心代码(接收回调函数)

        int HEAD1 = 0x80;
        int HEAD2 = 0x81;
        int HEAD3 = 0x82;
        byte[] infoArray = new byte[27]; //需要接收到结构体的大小 根据需要调整
        
        private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
        {
            Thread.Sleep(40);//我猜应该是有一个数据就进来 然后这个是等数据变多了 在接受
            /*
            //字符串方式读缓冲区中的数据
            string str = serialPort1.ReadExisting();
            //把读取的数据添加至接收区。AppendText():向文本框的当前文本追加文本。
            String time = GetCurTime() + ":";
            this.Invoke((EventHandler)delegate
            {
                RecevieTextBox1.AppendText("\r\n"+"接收" + time + str);
            });  
            */

            int ptr=0;

            int len = serialPort1.BytesToRead;//一共接收到多少数据
            Byte[] readBuffer = new Byte[100];
            serialPort1.Read(readBuffer, 0, len);


            //获得一帧掐头去尾的数据
            for (ptr=0;ptr<len;ptr++)
            {
                if((readBuffer[ptr] == HEAD1) && (readBuffer[ptr + 1] == HEAD2) && (readBuffer[ptr+2]==HEAD3))
                {
                    ptr += 3;
                    for (int i=0;i<27;i++)//这样就把尾巴给去掉了
                    {
                        infoArray[i] = readBuffer[ptr+i];
                    }

                    CSInfoStrcutre fram = (CSInfoStrcutre)Byte2Struct.BytesToStuct(infoArray, typeof(CSInfoStrcutre));
                    this.Invoke((EventHandler)delegate
                    {
                        uiRichTextBox1.AppendText("\r\n" + "_temp_O_MCU:"+fram._temp_O_MCU+ "\r\n"+ "_temp_env:"+fram._temp_env+ "\r\n" + "gp[1]" + fram.gp[1] + "\r\n" + "ll._longitude"+fram.ll._longitude);
                    });

                }
            }

        }

Form1_Load

           try
            {
                Updata_Serialport_Name(ComComboBox1);  //调用更新可用串口函数,comboBox1为 端口 组合框名字
                BaudComboBox2.Items.Add(115200);
                BaudComboBox2.Items.Add(9600);
                BaudComboBox2.Text = "115200";

                
                StopComboBox3.Items.Add(System.IO.Ports.StopBits.One);
                StopComboBox3.Items.Add(System.IO.Ports.StopBits.OnePointFive);
                StopComboBox3.Items.Add(System.IO.Ports.StopBits.Two);
                StopComboBox3.Text = System.IO.Ports.StopBits.One.ToString();

                DataComboBox4.Items.Add(8);
                DataComboBox4.Text = "8";

                CheckComboBox5.Items.Add(System.IO.Ports.Parity.None);//在表单中显示的就是enum的名字 而不是一串数
                CheckComboBox5.Items.Add(System.IO.Ports.Parity.Odd);
                CheckComboBox5.Items.Add(System.IO.Ports.Parity.Even);
                CheckComboBox5.Text= System.IO.Ports.Parity.None.ToString();

            }
            catch (Exception ex)
            {

                MessageBox.Show("串口打开异常" + serialPort1.PortName);
            }

Updata_Serialport_Name

            //System.IO.Ports.SerialPort.GetPortNames()函数功能为获取计算机所有可用串口,以字符串数组形式输出
            string[] ArryPort = System.IO.Ports.SerialPort.GetPortNames(); //定义字符串数组,数组名为 ArryPort,将可用的串口信息存放在字符串中
            MycomboBox.Items.Clear();       //清除当前组合框下拉菜单内容                  
            for (int i = 0; i < ArryPort.Length; i++)
            {
                MycomboBox.Items.Add(ArryPort[i]);   //将所有的可用串口号添加到端口对应的组合框中
            }

ComComboBox1_Click

            ComComboBox1.Items.Clear();
            Updata_Serialport_Name(ComComboBox1);  //调用更新可用串口函数,comboBox1为 端口 组合框名字

打开串口

            try
            {
                //在打开串口之前获取串口号和波特率
                serialPort1.PortName = ComComboBox1.Text;
                //转换为10进制,10可省略
                serialPort1.BaudRate = Convert.ToInt32(BaudComboBox2.Text, 10);

                Parity NIParity = (Parity)Enum.Parse(typeof(Parity), CheckComboBox5.Text);//字符串转枚举
                serialPort1.Parity = NIParity;//这段再研究下 如何实现文本转enum中的某个值

                //MessageBox.Show(StopComboBox3.Text);这个是none
                //MessageBox.Show(StopComboBox3.SelectedText);这个打印出来是空白
                StopBits NIStopBits = (StopBits)Enum.Parse(typeof(StopBits), StopComboBox3.Text);//字符串转枚举
                                                                                                 //serialPort1.StopBits = System.IO.Ports.StopBits.One;
                serialPort1.StopBits = NIStopBits;

                serialPort1.DataBits = Convert.ToInt32(DataComboBox4.Text, 10);

                serialPort1.DataReceived += new System.IO.Ports.SerialDataReceivedEventHandler(serialPort1_DataReceived);

                serialPort1.ReceivedBytesThreshold = 1;

                //打开串口
                serialPort1.Open();
                //打开串口按钮不可用,变成灰色
                OpenuiButton2.Enabled = false;
                //关闭串口按钮可用。
                CloseuiButton3.Enabled = true;
            }
            catch (Exception ex)
            {
                 MessageBox.Show( ex.ToString());
            }

GetCurTime

            System.DateTime currentTime = new System.DateTime();
            currentTime = System.DateTime.Now;
            string res;
            res = (currentTime.Hour < 10 ? ("0" + currentTime.Hour) : currentTime.Hour + "") + ":" + (currentTime.Minute < 10 ? ("0" + currentTime.Minute) : currentTime.Minute + "") + ":" + (currentTime.Second < 10 ? ("0" + currentTime.Second) : currentTime.Second + "");//+ "-" + (currentTime.Millisecond).ToString();
            return res;

关闭串口

            try
            {
                //关闭串口
                serialPort1.Close();
                //打开串口按钮可用
                OpenuiButton2.Enabled = true;
                //关闭串口按钮不可用,变成灰色
                CloseuiButton3.Enabled = false;
            }
            catch
            {
                //一般关闭串口不出错,此处对程序不进行处理
            }

发送

            try
            {
                //将发送的数据存在于缓冲区
                serialPort1.WriteLine(NameTextBox2.Text);
                String time = GetCurTime() + ":";
                RecevieTextBox1.AppendText("\r\n"+"发送" +time + " " + NameTextBox2.Text);
            }
            catch (Exception err)
            {
                MessageBox.Show("串口数据写入错误", "发送错误");
            }

ClearuiButton1_Click

RecevieTextBox1.Clear();

STM32

main

//这两个是全局变量
uint8_t aRxBuffer;
char RxBuffer[255]={0};
HAL_UART_Receive_IT(&huart1,&aRxBuffer, 1);

中断回调函数

uint8_t Uart1_Rx_Cnt=0;
extern uint8_t aRxBuffer;
extern char RxBuffer[255];
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{ 
		if(Uart1_Rx_Cnt >= 255)  //溢出判断
		{	
			Uart1_Rx_Cnt = 0;
			memset(RxBuffer,0x00,strlen(RxBuffer));
			HAL_UART_Transmit(&huart1, (uint8_t *)"数据溢出", 10,0xFFFF);         
        
		}
		else
		{
			RxBuffer[Uart1_Rx_Cnt++] = aRxBuffer;   //接收数据转存
 
			//这样写的话需要发送16进制 2501其中2501中间即时隔了很多空格也没关系
			//if((RxBuffer[Uart1_Rx_Cnt-1] == 0x01)&&(RxBuffer[Uart1_Rx_Cnt-2] == 0x25)) //判断结束位
	
			//如果发送字符串判断 那就使用ASCII码 也就是2501对应的ASCII码 共四个 这个时候他们中间不能出现空格因为空格也代表了一个值
				if((RxBuffer[Uart1_Rx_Cnt-1] == 49)&&(RxBuffer[Uart1_Rx_Cnt-2] ==48)&&(RxBuffer[Uart1_Rx_Cnt-3] ==53&&(RxBuffer[Uart1_Rx_Cnt-4] ==50))) //判断结束位
			{
					//发送结构体
					CSInfoS ni;
					ni._temp_O_MCU=27;//1
					ni._temp_env=27.5;//4
					ni.gp[0]=100;//1
					ni.gp[1]=50;//1
					ni.acc._acc_X=21;
					ni.ll._latitude=11.1;
					ni.ll._longitude=12.2;
					//printf("接收到2501了");
					CSInfo_PackAndSend(&ni);
					Uart1_Rx_Cnt = 0;
				  //printf("qian%s",RxBuffer);
					memset(RxBuffer,0,strlen(RxBuffer)); //清空数组
					//printf("hou%s",RxBuffer);
					HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1);   //再开启接收中断
					Uart1_Rx_Cnt = 0;
		
			}
		}
	
			HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1);   //再开启接收中断
}

结构体处理函数.c

#include "StructSerialPort.h"

//这一段也是比较简单的 
//1、首先是将结构体 装入(uint8_t)类型的数组中
//2、加入包头和包尾
//3、一个一个将字节发送出去   HAL_UART_Transmit(&huart1,&byte,1, 1000);

/**
* @brief  将数据打包并发送到上位机
  * @param  
			ptrInfoStructure 指向一个装填好信息的infoStructure的指针
  * @retval 无
  */

void CSInfo_PackAndSend(ptrCSInfo ptrInfoStructure)
{
	uint8_t infoArray[27]={0};//因为一个这个类型的结构体就是27个字节
	uint8_t infoPackage[30]={0};//信息打包后共多少个字节 也就是加3个头
	CSInfo_2Array_uint8(ptrInfoStructure,infoArray);//重组为uint8
	CSInfo_Pack(infoPackage,infoArray,sizeof(infoArray));
	CSInfo_SendPack(infoPackage,sizeof(infoPackage));
}


/**
  * @brief  将数据段(CSInfoStructure)重组为uint8类型的数组
  * @param  
		传入一个结构体的指针,并传入一个对应大小(uint8_t)类型的数组,用来装结构体拆分出来的元素。
		infoSeg 指向一个CSInfoStructure的指针
	  infoArray 由数据段重组的uint8类型数组			
  * @retval 无
  */
void CSInfo_2Array_uint8(ptrCSInfo infoSeg,uint8_t* infoArray)
{
	//将结构体强制转换为uint8_t类型的指针
	//然后将值一个一个取出来装入数组中就行了
	int ptr=0;uint8_t 
	*infoElem=(uint8_t*)infoSeg;
	for(ptr=0;ptr<sizeof(CSInfoS);ptr++){
		infoArray[ptr] = (*(infoElem+ptr));
	}
}


/**
  * @brief  按协议打包
  * @param  
		package 打包结果,按协议结果为3+32+3=38字节 (38*8bit)一个float为8 我们前面给他改了一下 不是都是float
	    infoArray 由数据段重组的uint8类型数组 | 结果	
		infoSize 数据段的大小--占用内存字节数(协议规定为32Byte)
  * @retval 无
  */
void CSInfo_Pack(uint8_t* infopackage,uint8_t* infoArray,uint8_t infoSize)
{
	uint8_t ptr=0;
	infopackage[0] = HEAD1;//数据包的头
	infopackage[1] = HEAD2;
	infopackage[2] = HEAD3;
	//infopackage[3] = 0;
	//infopackage[4] = 27;
	
	/* 将信息封如入数据包中 */
	for(;ptr<infoSize;ptr++){
		infopackage[ptr+3] = infoArray[ptr];
	}
	//infopackage[ptr+5]=0xff;
}

/**
* @brief  将数据包发送到上位机
  * @param  
			infoPackage 数据包
			packSize 数据包的大小--占用内存字节数(协议规定为38Byte)
  * @retval 无
  */
void CSInfo_SendPack(uint8_t* infoPackage,uint8_t packSize)
{
	int ptr=0;
	for(ptr=0;ptr<packSize;ptr++){
		USART_SendByte(infoPackage[ptr]);
	}
}

 /**
  * @brief  通过USART通道向上位机发送一个字节(8bit)的数据
  * @param  byte 要发送的8位数据
  * @retval 无
  */
void USART_SendByte(uint8_t byte)
{
	/* 发送一个字节数据到串口 */
	HAL_UART_Transmit(&huart1,&byte,1, 1000);
	/* 等待发送完毕 */
	//while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);		
}

结构体处理函数.h

#ifndef StructSerialPort
#define StructSerialPort


#include "main.h"
#include "usart.h"


#define HEAD1 0x80
#define HEAD2 0x81
#define HEAD3 0x82

#define TAIL1 0xFF
#define TAIL2 0xFF
#define TAIL3 0xFF

#define BYTE_ALIGN   __attribute__ ((packed))//stm32默认是4字节对齐,给他转成1字节对齐
/**
	@part 通信数据结构
*/

/* 加速度信息结构体-XYZ三分量 */
typedef struct CSModuleInfo_ACC{
	float _acc_X;
	float _acc_Y;	
	float _acc_Z;
} BYTE_ALIGN CSInfo_Acc;

/* 经纬度信息结构体-经纬两分量 */
typedef struct CSmouduleInfo_LL{
	float _latitude;
	float _longitude;
}BYTE_ALIGN CSInfo_LL;

/* 测控站信息结构体 */
typedef struct CSInfoStrcutre CSInfoS;
typedef struct CSInfoStrcutre{
	/* 核心温度 MCU温度 */
	uint8_t _temp_O_MCU;
	/* 气温 */
	float _temp_env;
	/* 气压 */
	uint8_t gp[2];
	/* 加速度 */
	CSInfo_Acc acc;
	/* 经纬度 */
	CSInfo_LL ll;
}BYTE_ALIGN * ptrCSInfo;//这个应该是用ptrCSInfo代表声明一个结构体指针


//将数据打包并发送到上位机
void CSInfo_PackAndSend(ptrCSInfo ptrInfoStructure);
//传入一个结构体的指针,并传入一个对应大小(uint8_t)类型的数组,用来装结构体拆分出来的元素。
void CSInfo_2Array_uint8(ptrCSInfo infoSeg,uint8_t* infoArray);
void CSInfo_Pack(uint8_t* infopackage,uint8_t* infoArray,uint8_t infoSize);
void CSInfo_SendPack(uint8_t* infoPackage,uint8_t packSize);
void USART_SendByte(uint8_t byte);
#endif

问题

字节对齐问题

问题:发送结构体的时候中间总是出现很多0;
解答:stm32字节对齐问题,默认是4字节对齐的。
stm32做串口或网络传输数据时,经常需要用结构体定义帧格式。如果按照keil默认的对齐方式(4字节对齐),经常会出现结构体中补零的问题,造成帧格式错误。所以,在定义结构体类型时,最好把结构体对齐方式改为1字节对齐,防止出错。

#define BYTE_ALIGN   __attribute__ ((packed))
 
typedef struct
{
    u8 node;
    u8 cmd;
    u8 bytelen;
    u16 data;
    u16 crc;
}BYTE_ALIGN mdb_send_t;

参考

byte数组与结构体之间的转换 参考1

byte数组与结构体之间的转换 参考2

UnmanagedType 枚举:指定如何将参数或字段封送到非托管代码。

MarshalAsAttribute.ArraySubType:如果是数组的话,指定数组元素的类型

  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2022-09-25 23:18:15  更:2022-09-25 23:18:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 19:45:05-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码