动手写写Modbus-RTU协议
关注、星标嵌入式客栈,精彩及时送达
[导读] 大家好,我是逸珺。
前面聊了modbus的物理层,协议标准,今天来实现一下modbus-RTU,本文主要聊从设备的实现。
思路分析
前面聊modbus协议的时候,画了这张图modbus OSI分层模型图。OSI模型图是一种分层设计图。就好比建房子,那些搞建筑的绘制的设计图一样,所以为什么建筑师也叫Architect。嗨,跑偏了。
所以编码之前,这个协议照这个图的意思就是最好设计成三层,比如这样:
物理层与控制芯片采用UART与一个GPIO进行交互:
UART,一般的单片机/DSP/MPU都具有UART外设,其中TXD将二进制报文,逐字节按照UART规范,进行编码,一般一个字节需要11个bit在物理层编码,事实上很多时候也会使用10bit模式,无校验位,8个数据位,1个起始位,1个停止位。
DIR:利用一个芯片的GPIO,实现收发方向控制,DIR置低,为接收模式;DIR置高,为发送模式。
链路层
链路层的职责是:
实现报文的接收服务
实现介质的发送服务
介质管理
报文接收服务,芯片与物理层之间通信接口是UART,因此就是处理串口接收。先看看modbus报文的定义:
modbus报文,没有特殊帧头、帧尾,如何判别接收到一个完整的帧了呢?
modus 标准规定,帧间隔至少须3.5个字节时间,字节间隔不得大于1.5 字节时间,那反过来思考,只要3.5 字节时间内没有新收到数据就表示有可能接收到一帧。为什么是有可能呢?因为数据里还有可能有错误字节,如果加上CRC 校验通过这个条件,就可以判定数据帧收到了。 所以在上文中才会有这么一个状态机图:
收发状态机
这里的T3.5,T1.5字节时间怎么算呢?前面说了一个字节需要11个Bit表示,所以如果波特率是9600,就按照下面计算,如果是其他的波特率计算方式一样。
用个定时器就可以实现了。
#define MODBUS_BUF_SIZE 256Utypedef enum {E_MS_RECEVING,E_MS_PENDING, E_MS_SENDING
}E_MODBUS_STATE;typedef struct _T_MODBUS_LAYER2
{INT8U buffer[MODBUS_BUF_SIZE];INT16U index;INT16U txLength;E_MODBUS_STATE state; void (*SendDataToCom)(INT16U length);void (*InitiliseLayer2)(void);
} T_MODBUS_LAYER2;
extern volatile T_MODBUS_LAYER2 modbusLayer2;
为突出重点,接收控制中仅实现T3.5字节时间判定,T1.5字节时间要实现也非常容易,每接收一个字节就判定一下间隔时间即可,超过1.5字节时间,丢弃所有字节,重新开始接收就可以了。
//假定用P05脚控制收发方向
sbit MODBUS_COM1_CTL = P0^5;#define MODBUS_COM1_R_ENABLE MODBUS_COM1_CTL = 0
#define MODBUS_COM1_R_DISABLE MODBUS_COM1_CTL = 1#define FOSC 11059200
#define TIMER_CLK 921600
#define BAUD_9600 9600
//这里实现T3.5 如果波特率可修改,这里需要调整
#define COM1_T35_GAP_TIME ( (INT16U)(65536-(TIMER_CLK*10*3.5/BAUD_9600) ) )void ModbusCom1StartGapTimer(void)
{ET0 = 0;TR0 = 0;TMOD = 0X21;CKCON = 0x00;TH0 = (INT8U)((COM1_T35_GAP_TIME&0xff00)>>8);TL0 = (INT8U)(COM1_T35_GAP_TIME&0x00ff);ET0 = 1;TR0 = 1;
}void ModbusCom1StopGapTimer(void)
{ET0 = 0;TR0 = 0;
}void ModbusCom1InitLayer2_SRV(void)
{MODBUS_COM1_R_DISABLE;ModbusCom1InitUart((INT16U)BAUD_9600);modbusLayer2.state = E_MODBUS_STATE_RECEVING;modbusLayer2.index = 0;modbusLayer2.txLength = 0;modbusLayer2.SendDataToCom = ModbusCom1SendData;modbusLayer2.InitiliseLayer2 = ModbusCom1InitLayer2;MODBUS_COM1_R_ENABLE;
}void ModbusCom1InitLayer2(void)
{ES1 = 0;MODBUS_COM1_R_DISABLE;modbusLayer2[MODBUS_COM1].state = E_MODBUS_STATE_RECEVING;modbusLayer2[MODBUS_COM1].index = 0;modbusLayer2[MODBUS_COM1].txLength = 0;MODBUS_COM1_R_ENABLE;TI_1 = 0; RI_1 = 0;ES1 = 1; // 使能串口
}void TIMER0_INT_ISR(void) interrupt 1 using 1
{ MODBUS_COM1_R_DISABLE;ES1 = 0; ModbusCom1StopGapTimer(); modbusLayer2.state = E_MODBUS_STATE_PENDING; TF0 = 0;
}void ModbusCom1InitUart(INT16U baudrate)
{baudrate = 9600;T2CON &= 0XCF; // XXXX XXXX Timer 2 Control // |||| |||+- CP/RL2 Capture/Reload Select.// |||| ||| 0 = Auto-reloads will occur when Timer 2 overflows or// |||| ||| a falling edge is detected on T2EX if EXEN2 = 1.// |||| ||| 1 = Timer 2 captures when a falling edge is detected on T2EX if EXEN2 = 1.// |||| ||+-- Counter/Timer Select.// |||| || 0 = Timer 2 functions as a timer.// |||| || 1 = Timer 2 will count negative transitions on the T2 pin (P1.0).// |||| |+--- Timer 2 Run Control.// |||| | 0 = Timer 2 is halted.// |||| | 1 = Timer 2 is enabled.// |||| +---- Timer 2 External Enable.// |||| 0 = Timer 2 will ignore all external events at T2EX.// |||| 1 = Timer 2 will capture or reload a value if a negative transition is detected on the T2EX pin.// |||+------ Transmit Clock Flag.// ||| 0 = Timer 1 overflow is used to Tx baud rate for USART0.// ||| 1 = Timer 2 overflow is used to Tx baud rate for USART0.// ||+------- Receive Clock Flag// || 0 = Timer 1 overflow is used to Rx baud rate for USART0.// || 1 = Timer 2 overflow is used to Rx baud rate for USART0.// |+-------- Timer 2 External Flag.// | A negative transition on the T2EX pin (P1.1) will cause this flag // +--------- Timer 2 Overflow Flag.SMOD1 = 0; //baudrate //T0 T1 T2 12分频 00000000 CKCON = 0x00;// XXXX XXXX Clock Control // |||| |+++- Stretch MOVX Select 2:0.// |||| | 000~111= 2~9 Instruction Cycles// |||| +---- Timer 0 Clock Select.// |||| 0: Timer 0 uses a divide by 12 of the crystal frequency.// |||| 1: Timer 0 uses a divide by 4 of the crystal frequency.// |||+------ Timer 1 Clock Select. // ||| 0: Timer 1 uses a divide by 12 of the crystal frequency// ||| 1: Timer 1 uses a divide by 4 of the crystal frequency.// ||+------- Timer 2 Clock Select// || 0: Timer 2 uses a divide by 12 of the crystal frequency.// || 1: Timer 2 uses a divide by 4 of the crystal frequency. // ++-------- 00 reservedTCON = 0X40;// XXXX XXXX Timer/Counter Control// |||| |||+- Interrupt 0 Type Select.// |||| ||| 0: INT0 is level-triggered.// |||| ||| 1: INT0 is edge-triggered.// |||| ||+-- Interrupt 0 Edge Detect// |||| || If IT0 = 1, this bit will remain set until cleared in software // |||| || or the start of the External Interrupt 0 service routine// |||| || If IT0 = 0, this bit will inversely reflect the state of the INT0 pin.// |||| |+--- Interrupt 1 Type Select.// |||| | 0: INT1 is level-triggered.// |||| | 1: INT1 is edge-triggered.// |||| +---- Interrupt 1 Edge Detect. similar with Interrupt 0 Edge Detect// |||+------ Timer 0 Run Control.// ||| 0: Timer is halted// ||| 1: Timer is enabled.// ||+------- Timer 0 Overflow Flag.// || 0: No Timer 0 overflow has been detected.// || 1: Timer 0 has overflowed its maximum count.// |+-------- Timer 1 Run Control. // +--------- Timer 1 Overflow FlagSCON1 = 0x50;// XXXX XXXX Serial Port 0 Control // |||| |||+- Receiver Interrupt Flag// |||| ||+-- Transmitter Interrupt Flag// |||| |+--- 9th Received Bit State.// |||| +---- 9th Transmission Bit State// |||+------ Receive Enable.// ||| 0: Serial Port 0 reception disabled.// ||| 1: Serial Port 0 received enabled (modes 1,2,and 3). // +++------- Serial Port 0 Mode// 000 Synchronous 8bits 12 pCLK// 001 Synchronous 8bits 4 pCLK// 010 Asynchronous 10 bits,Timer 1 or 2 Baud Rate Equation// 011 Valid Stop Required,10 bits,Timer 1 Baud Rate Equation// 100 Asynchronous 11 bits 64 pCLK (SMOD = 0), 32 (SMOD = 1)// 101 Asynchronous with Multiprocessor Communication.11 bits// 110 Asynchronous 11bits Timer 1 or 2 Baud Rate Equation// 111 Asynchronous with Multiprocessor Communication.Timer 1 or 2 Baud Rate Equation//0010(T1) 0001(T0)TMOD = 0X21; //9600 8 N 1 BAUD=256-FOSC/384*BAUDRATETH1 = 253;P1DDRL = 0x71; //interrupt TI_1 = 0; RI_1 = 0;TR1 = 1; TR0 = 0;ES1 = 1;
}void USART1_ISR(void) interrupt 7 using 2
{ if(RI_1){if(modbusLayer2.state == E_MODBUS_STATE_RECEVING){ModbusCom1StartGapTimer(); if(modbusLayer2.index >= MODBUS_BUF_SIZE){modbusLayer2.buffer[0] = SBUF1;modbusLayer2.index = 0;}else {modbusLayer2.buffer[modbusLayer2.index++] = SBUF1; } } RI_1 = 0;}else if(TI_1){ if(modbusLayer2.state == E_MODBUS_STATE_SENDING){if(modbusLayer2.index < modbusLayer2.txLength){SBUF1 = modbusLayer2.buffer[modbusLayer2.index++];}else{ modbusLayer2.index = 0;modbusLayer2.txLength = 0; modbusLayer2.state = E_MODBUS_STATE_RECEVING; MODBUS_COM1_R_ENABLE;} }TI_1 = 0;}
}//发送一个字节,触发发送中断。
void ModbusCom1SendData(INT16U length)
{ MODBUS_COM1_R_DISABLE; ES1 = 1; modbusLayer2.txLength = length;modbusLayer2.state = E_MODBUS_STATE_SENDING;modbusLayer2.index = 1;SBUF1 = modbusLayer2.buffer[0];
}
这个底层是用51单片机实现的,如果是其他单片机,需要实现做些相应的修改就可以了,基本思路是一样的。
应用层
数据关联
回顾之前modbus协议,标准将用户应用数据规划为4张表:
本文以最为常用的0x03、0x10命令进行示例,使用后两种表就可以了。
看到这两条命令,是以16位地址进行索引的,而且有需要与用户应用数据进行关联,怎么做呢?可以设计这样一个数据表:
注:这个示例是很久以前用51单片机实现的,所以int的长度是16位。
typedef unsigned char BOOLEAN;
typedef unsigned char INT8U;
typedef signed char INT8S;
typedef unsigned int INT16U;
typedef signed int INT16S;
typedef unsigned long INT32U;
typedef signed long INT32S;
typedef float FP32; enum E_TYPES
{ET_U8, ET_U16, ET_U32, ET_FLOAT
};typedef struct
{INT16U address;INT16U index;E_TYPES type;
}MODBUS_REG_TABLE;
//输入只读寄存器
MODBUS_REG_TABLE code inputRegisterTable[]={{10000, IDX_unit, ET_U8},{10001, IDX_temperature, ET_FLOAT},{10003, IDX_adc, ET_U16}
};
//保持寄存器
MODBUS_REG_TABLE code holdingRegisterTable[]={{20000, IDX_data_4, ET_FLOAT},{20002, IDX_data_5, ET_U8},{20003, IDX_data_6, ET_U32}
};enum E_IDXS
{IDX_unit=0,IDX_temperature,IDX_upperRange,IDX_lowerRange,IDX_adc,IDX_dac_input,IDX_dac,IDX_dac_upperRange,IDX_dac_lowerRange
};
typedef struct _T_APP_DATA_TABLE
{void *pTarget;INT8U length;
}T_APP_DATA_TABLE;
//利用这个表,将分散的数据统一桥接映射
T_APP_DATA_TABLE code appDataTable[]={{ &tempMeasurent.unit, 1},{ &tempMeasurent.temperature, 4},{ &tempMeasurent.upperRange, 4},{ &tempMeasurent.lowerRange, 4}, { &tempMeasurent.adc, 2},{ &dacOutput.input, 4},{ &dacOutput.dac, 2},{ &dacOutput.upperRange, 4},{ &dacOutput.lowerRange, 4}
};
typedef struct _T_MEASURE
{ INT8U unit; FP32 temperature;FP32 upperRange;FP32 lowerRange;INT16U adc;
}T_MEASURE;typedef struct _T_DAC
{ FP32 input;INT16U dac;FP32 upperRange;FP32 lowerRange;
}T_DAC;
extern T_MEASURE xdata tempMeasurent;
extern T_DAC xdata dacOutput;
为了便于描述,假定有两个应用数据结构体,一个采样当前温度传感器,计算当前温度,并根据设定上下测量范围进行映射;另一个结构体假定需要对外输出一个DA模拟量给别的设备,将输入input值,按照设定范围,用DA通道输出。这些数据在本文中并无实际意义,为了方便描述假设一下。(注:文中关键字xdata,code等为keil C51关键字。忽略即可)
将上述代码,绘制成一张图来分析一下:
首先设计一个索引枚举E_IDXS,枚举值与appDataTable里存放的条目一一对应,appDataTable相当于一个字典,而枚举值则相对于appDataTable数组快速存取的下标。
利用T_APP_DATA_TABLE这个结构体,利用void指针,将任意类型的数据与长度抽象出来,其实这里甚至还可以放入自定义数据类型,比如某一个结构体,只要保证将结构体内存长度填对即可。我为什么这样设计这个结构体呢?因为应用层的模块可能有很多,不同的模块都会有自身的数据,利用这样一个索引字典,就将这些分散的应用数据,整合起来了。再接下来就是水到渠成的事情了,设计一个modbus寄存器表的结构体,其中第一个数据成员address,是modbus报文中的地址;第二个数据成员为index,是应用数据索引;第三个数据type,是该索引对应的数据类型。这样一来,就把modbus寄存器表与应用数据关联起来了。如此一来,要构建只读输入寄存器表,保持寄存器表,甚至什么线圈等表都变成统一模型了。
有了对数据的字典映射管理,只需要实现按寄存器表进行索引,根据不同类型的记录条目进行内存读写操作就可以了。所以需要实现两个读写接口,提供给modbus应用层访问:
static INT8U GetDataFromReg(INT8U * pBuf,INT16U startAddr,INT8U regsNum);
static INT8U StoreDataToReg(INT16U startAddr,INT8U* pBuf,INT8U regsNum,T_MODBUS_LAYER2 *pLayer2);
GetDataFromReg函数就是从寄存器表中,通过查字典,找到对应modbus地址对应的应用数据的内存地址,然后将应用数据从内存拷贝到pBuf所指向的缓冲区,这个缓冲区会进一步封装成应答报文。
StoreDataToReg函数则是从pLayer2中将接收到的报文通过传入寄存器起始地址,查询到寄存器表中的数据索引以及相应的数据类型,从而就只需要实现数据的搬运了。
这里需要注意modbus报文中,字节序是高字节在前,比如地址20000,对应16进制为0x4E20,那么在报文中0x4E先传,0x20后传。
应用框架
链路层本来需要实现帧校验,由于51单片机里资源有限,而接收又采用逐字节中断方式,所以把帧校验放在应用中处理了。先看看
INT8U ModBusLayer7_Interpreter( void )
{INT8U xdata frameCode;if( modbusLayer2.state == E_MS_PENDING){ frameCode = ModBusFrameTypeCheck((T_MODBUS_LAYER2 *)&modbusLayer2,systemPara.modbusAddr); switch( frameCode ){ case MODBUSRTU_F03:if(ModBusRTU_F03_Response((T_MODBUS_LAYER2 *)&modbusLayer2)!=OK)modbusLayer2[i].InitiliseLayer2();break;case MODBUSRTU_F10:if(ModBusRTU_F10_Response((T_MODBUS_LAYER2 *)&modbusLayer2)!=OK)modbusLayer2.InitiliseLayer2();break;//按照这个样式还可以实现其他命令default: modbusLayer2.InitiliseLayer2();break;} }return( ERR );
}INT8U ModBusRTU_F03_Response( T_MODBUS_LAYER2 *pLayer2)
{INT8U xdata regs,bytes;INT16U xdata regAddr,CRC16Value;regAddr = (INT16U)(pLayer2->buffer[2]<<8)+(INT16U)pLayer2->buffer[3]; regs = (INT16U)(pLayer2->buffer[4]<<8)+(INT16U)pLayer2->buffer[5];bytes = regs * 2; if( bytes == 0 ) {return( ERR );}if( bytes > sizeof(pLayer2->buffer) - 7 ) {return( ERR );}GetDataFromReg( &pLayer2->buffer[3], regAddr, regs ); pLayer2->buffer[2] = bytes;CRC16Value = CRC16( pLayer2->buffer, 3+bytes );pLayer2->buffer[3+bytes] = (INT8U)( CRC16Value >> 8 );pLayer2->buffer[4+bytes] = (INT8U)( CRC16Value );pLayer2->SendDataToCom(5+bytes);return OK;
}INT8U ModBusRTU_F10_Response( T_MODBUS_LAYER2 *pLayer2 )
{INT8U xdata bytes,regs;INT16U xdata CRC16Value,regAddr;regs = pLayer2->buffer[5];bytes = pLayer2->buffer[6]; regAddr = (INT16U)(pLayer2->buffer[2]<<8)+(INT16U)(pLayer2->buffer[3]);if( (bytes == 0) || ( bytes != (regs*2) ) )return( ERR );if(StoreDataToReg( regAddr, &(pLayer2->buffer[7]), regs,pLayer2)!=0){pLayer2->buffer[4] = 0x90;pLayer2->buffer[5] = 0x01;} CRC16Value = CRC16( pLayer2->buffer, 6 );pLayer2->buffer[6] = ( INT8U )( CRC16Value >> 8 );pLayer2->buffer[7] = ( INT8U )( CRC16Value );pLayer2->SendDataToCom(8);return OK;
}
基本思路就是,先判断layer2是否接收一个报文,然后在校验这个报文是否是一个正确的报文,如果是就先进行校验,校验返回值为命令码,再转入相应的命令进行后续处理。
数据校验
帧校验,需要校验CRC-16,这个是必须要做的。除此之外,还需要检查当前请求是否为设备所支持的命令,是否是对该设备的请求。本文没有关注广播报文。CRC-16校验算法就是前文中标准给出的算法。
INT8U ModBusFrameTypeCheck( T_MODBUS_LAYER2 *pLayer2,INT8U address)
{INT8U xdata funCode;INT8U xdata station;if(address!=pLayer2->buffer[0])return ERR;if( pLayer2->index < 8 )return( ERR ); station = pLayer2->buffer[0];funCode = pLayer2->buffer[1]; if(station != address){ return( ERR );}//0X03命令报文长度为8,//0x10命令,第6个字节为寄存器字节数,if( (pLayer2->index != 8) && (pLayer2->index != pLayer2->buffer[6]+9) && (pLayer2->index != (5+pLayer2->buffer[2])) ) return( ERR );if( CRC16(&(pLayer2->buffer[0]), pLayer2->index) != 0 ){ return( ERR );}switch( funCode ){ case 0x03: return( MODBUSRTU_F03 ); case 0x10: return( MODBUSRTU_F10 ); default: break;} return( ERR );
}
总结一下
很久以前用51单片机实现的modbus两条命令,要实现其他的命令或者广播处理,可以类似处理。主要聊一下整体思路。代码很多地方不是很严谨。有兴趣自己实现一下modbus命令,本文可以当个参考。
—END—
往期精彩推荐,点击即可阅读
▲万变不离其宗之单片机串口问题
▲万变不离其宗之I2C总线要点总结
▲万变不离其宗之SPI总线要点总结
▲长文图解工业HART总线协议
▲RS-485总线,这篇很详细
▲图文详解Modbus-RTU
动手写写Modbus-RTU协议相关推荐
- 安卓开发板之串口通信,通过modbus Rtu协议控制下位机
安卓开发板之串口通信,通过modbus Rtu协议控制下位机 1.环境准备 2.编写串口操作核心类 3.编写测试类 前言:因为公司最近有个人脸识别门禁的项目,这个项目主要业务是实现远程人脸注册,管理员 ...
- modbus RTU协议设备使用无线代替有线注意事项
1.设备有线连接 Modbus是由Modicon(现为施耐德电气公司的一个品牌)在1979年发明的,是全球第一个真正用于工业现场的总线协议.ModBus网络是一个工业通信系统,由带智能终端的可编程序控 ...
- 基于Modbus RTU协议的开关量控制采集简介
一.什么是开关量控制采集 所谓的开关量控制采集就是通过458/232接口发送控制命令,实现读取开关量输入或者控制开关量输出的通断. 二.开关量输入采集和开关量输出控制 1. 开关量输入采集就是将一个 ...
- 8数据提供什么掩膜产品_工业轨式1-8路RS485数据(MODBUS RTU协议)厂家产品说明...
产品描述 工业级数点对点光猫提供1-8路RS485(MODBUS RTU协议): 在光纤中传输,该产品突破了传统串行接口通讯距离与通讯速率的矛盾,同时,也解决了电磁干扰.地环干扰和雷电破坏的难题,大大 ...
- AIRIOT物联网低代码平台如何配置Modbus RTU协议?
MBRTU即MODBUS RTU的简称,MODBUS是OSI模型第7层上的应用层报文传输协议,它在连接至不同类型总线或网络的设备之间提供客户机/服务器通信.平台的MBRTU协议是建立在TCP协议之上的 ...
- FDX-B标签RFID读写器CK-LR12-AB之Modbus Rtu协议运用规则
1.1 Modbus Rtu 协议 1.1.1 寄存器定义表 寄存器地址 定义内容 寄存器地址 定义内容 0 从站地址 1 485速率 2 通信校验 3 读卡模式 4 系统状态 5 RSSI 6 Re ...
- RS232(Modbus RTU)+RS485(Modbus RTU)协议RFID识别磁导航AGV小车传感器|定位仪CK-GL16-AB的安装与磁处理方法
RS232(Modbus RTU)+RS485(Modbus RTU)协议RFID识别磁导航AGV小车传感器|定位仪CK-GL16-AB是一款面向AGV行业新推出的一款"跨界"传感 ...
- ubuntu16.04下使用Modbus RTU协议控制Robotiq
ubuntu16.04下使用Modbus RTU协议控制Robotiq 一.设备配置 二.创建工作空间 三.安装驱动 四.配置串口 五. ROS节点控制夹爪 六.RVIZ显示模型 一.设备配置 操作系 ...
- 树莓派4B、Python与三相四线多功能电力仪表通过RS485(modbus RTU协议)收发数据
树莓派4B+Python与三相四线多功能电力仪表通过RS485(modbus RTU协议)接口发送和接收数据 请耐心把下面的警告⚠️看完 开始之前需要注意以下点:一.那就是安全,生命为本,安全第一.因 ...
- 三菱FX3U与台达变频器通讯 采用485方式,modbus RTU协议,对台达变频器频率设定
三菱FX3U与台达变频器通讯器件:三菱FX3U PLC+FX3U 485BD板,台达VFD变频器,昆仑通态触摸屏 功能:采用485方式,modbus RTU协议,对台达变频器频率设定,正反转,点动控制 ...
最新文章
- python对于会计-会计转到数据分析值得吗?
- 数据库访问的性能问题与瓶颈问题【z】
- angularjs post返回html_Python 爬虫网页解析工具lxml.html(二)
- java 设计模式学习笔记十三 observer设计者模式
- vue 后台翻译_vue - 实战项目 - 在线翻译
- 通讯录系统课程设计——链表实现——c语言
- 【C++】模板(函数模板,类模板,模板的特化,模板的分离编译)
- 论文阅读--Emotion Recognition in Conversation: Research Challenges, Datasets, and Recent Advances
- Java 微信图片上传素材管理
- python批量改变图像大小
- 俄勒冈大学计算机科学专业,俄勒冈大学计算机
- 约束优化:约束优化的三种序列无约束优化方法
- MFC中dlg.DoModal()返回-1
- 随机字符串解决大问题之腾讯网如何实现手机扫描二维码登录qq功能的
- How much do you know in R language
- 华为——OSPF单区域实验配置,实验抓包分析,五种报文分析,六种LSA介绍,以及如何建立邻接关系的七种状态
- TextToSpeech文字转语音、文字转音频文件并播放
- 超大XML文件怎么打开
- 将Revit模型转入unity中
- SSM毕设项目电竞酒店管理o51zb(java+VUE+Mybatis+Maven+Mysql)