RS232串口通信

1. 介绍

         RS232是一种全双工异步的总线,PC端使用如下的DB-9接口。其中有三个脚比较重要,分别为Pin2:Received Data(RXD)、Pin3:Transmit Data(TXD)、Pin5:Ground(GND)。


RS232是一种端到端的通信方式,在开始通信前,需要手动的确定两个RS232的端口通信参数是一致的。由于使用的习惯,这里只说明RS232串口的8-N-1,即8bit数据,无奇偶校验,1位停止位。

         RS232串口的TXD线在空闲时,电平为高;当开始发送字节之前,发送一个低电平的起始位;发送起始位后,发送8bit的数据,此处数据的LSB先发送,MSB后发送;发送8bit数据后,发送高电平的结束位。

         下图是发送0x55数据时候的波形:


下图是发送0xC4数据时候的波形:

RS232的速度用波特率来表示,波特率是每秒钟发送码元的个数,由于是二进制系统,所以1个码元由1bit表示,故波特率与比特率相同。在RS232串口中常用的波特率由9600,、115200等。波特率为115200是,1bit数据持续的时间为(1/115200)≈8.7us,发送1Byte就需要69.6us。由于串口发送1Byte的数据还需要辅助bit,例如开始位,结束位,故发送1Byte数据就需要8.7*10=87us;在1s时间间隔内,可以发送11.22KB的有效数据。

在RS232的标准中,使用-5V~-15V的表示逻辑高电平,使用5V~15V表示逻辑低电平。由于存在Max3232这类的电平转换芯片,所以我们只需要利用TTL电平。

2. 波特率的产生

(1)原理分析

波特率为115200的串口通信中,至少需要115200Hz的时钟产生波特率。此时,我们假设系统时钟比波特率时钟高16倍,则需要115200 Hz × 16 = 1.8432 MHz。系统时钟带有小数部分,不是常用的时钟。我们用2MHz的系统时钟替代1.8432MHz的系统时钟,则2MHz分频到115200Hz的分频比为(Clk / Baundrate)= 17.361111111...。在FPGA中,为了达到小数的分频比,可以利用一个10bit的寄存机,按59累加,溢出脉冲就是时钟分频后的波特率时钟,分频比为(2^累加器位宽 / 累加步长)=(2^10 / 59)= 17.3559,此刻的分频比的误差率是0.03%。

      有时候,我们提供的时钟Clk,并不是2MHz,波特率也可能不是115200,那么需要怎么通过我时钟和波特率得到累加步长呢?根据上面的分析,可知不管是时钟除还是累加器与累加步长相除,都是为了得到相同的分频比,故列出分频比的等式为:

(Clk / Baundrate)=(2^累加器位宽 / 累加步长)

故得到累加步长的计算公式:

          累加步长=(Baundrate / Clk)*2^累加器位宽=Baundrate * 2^累加器位宽 / Clk

在程序中,左位移N位,就是该数乘2的N次方,而累加步长为:

         累加步长 = (Baundrate <<累加器位宽)/Clk

由于FPGA的数据处理位宽不会大于32bit,但有时候Baundrate <<累加器位宽的数据比较大,使得累加器数据无法计算。为此上下同时除以16,减小中间过程的数值。当然可以除以其他数值,但可以是其他2的幂次数。

         累加步长 =(Baundrate<<(累加器位宽-4))/ (Clk>>4)

除法会产生小数,需要对结果进行四舍五入,在结果上加上1/2后,达到四舍五入的效果。

累加步长 =((Baundrate<<(累加器位宽-4))+ Clk>>5)/ (Clk>>4)

(2)波特率模块FPGA实现

波特率模块可以通过两个时钟使能管脚,分别产生数据发送时钟和串口接收时钟。

累加步长的计算如下。pBaudGeneratorInc是发送时钟的累加字;pBaudGeneratorInc8x是接收采样时钟的累加字,它是发送时钟的8倍,用来对1bit做8次采样。

localparam pBaudGeneratorInc = ((pBaud<<(pBaudGeneratorAccWidth-4))+(pClkFre>>5))/(pClkFre>>4);
localparam pBaudGeneratorInc8x = pBaudGeneratorInc<<3;

发送时钟产生

//Txd的波特率发射脉冲
always @ (posedge iClk )
begin
	if(iTxdEnable == 1'b0)
		rvBaudGeneratorAcc <= 0;
	else
		rvBaudGeneratorAcc <= rvBaudGeneratorAcc[pBaudGeneratorAccWidth-1:0]+pBaudGeneratorInc[pBaudGeneratorAccWidth:0];
end

assign oTxdBaudTick = rvBaudGeneratorAcc[pBaudGeneratorAccWidth];
接收时钟产生

wire [pBaudGeneratorAccWidth:0] wvConstValue = 2;
//Rxd的波特率采样时钟
always @ (posedge iClk )
begin
	if(iRxdEnable == 1'b0)
		rvBaudGeneratorAcc8x <= wvConstValue<<(pBaudGeneratorAccWidth-2);
	else
		rvBaudGeneratorAcc8x <= rvBaudGeneratorAcc8x[pBaudGeneratorAccWidth-1:0]+pBaudGeneratorInc8x[pBaudGeneratorAccWidth:0];
end

assign oRxdBaudTick = rvBaudGeneratorAcc8x[pBaudGeneratorAccWidth];

这里复位时,将累加器的数值设置成慢数值的一半。该设置的目的是确保在串口的1个bit有8个接收脉冲。

采用wvConstValue来移位是为了减少默认32bit的常数被被赋值给低数值而产生warning。

(3)完整代码与仿真

Verilog代码

//================================================================  
//  Filename      : BaudGenerator.v  
//  Created On    : 2017-05-16 09:44:30  
//  Last Modified : 2017-05-16 09:55:14  
//  Author        : ChrisHuang  
//  Description   :   
//================================================================

module BaudGenerator
#(parameter pClkFre = 25000000,
parameter pBaud = 115200,
parameter pBaudGeneratorAccWidth = 16)(
	input iClk,
	input iTxdEnable,
	input iRxdEnable,
	output oTxdBaudTick,
	output oRxdBaudTick
);

localparam pBaudGeneratorInc = ((pBaud<<(pBaudGeneratorAccWidth-4))+(pClkFre>>5))/(pClkFre>>4);
localparam pBaudGeneratorInc8x = pBaudGeneratorInc<<3;

reg [pBaudGeneratorAccWidth:0] rvBaudGeneratorAcc;
reg [pBaudGeneratorAccWidth:0] rvBaudGeneratorAcc8x;

//Txd的波特率发射脉冲
always @ (posedge iClk )
begin
	if(iTxdEnable == 1'b0)
		rvBaudGeneratorAcc <= 0;
	else
		rvBaudGeneratorAcc <= rvBaudGeneratorAcc[pBaudGeneratorAccWidth-1:0]+pBaudGeneratorInc[pBaudGeneratorAccWidth:0];
end

assign oTxdBaudTick = rvBaudGeneratorAcc[pBaudGeneratorAccWidth];

wire [pBaudGeneratorAccWidth:0] wvConstValue = 2;
//Rxd的波特率采样时钟
always @ (posedge iClk )
begin
	if(iRxdEnable == 1'b0)
		rvBaudGeneratorAcc8x <= wvConstValue<<(pBaudGeneratorAccWidth-2);
	else
		rvBaudGeneratorAcc8x <= rvBaudGeneratorAcc8x[pBaudGeneratorAccWidth-1:0]+pBaudGeneratorInc8x[pBaudGeneratorAccWidth:0];
end

assign oRxdBaudTick = rvBaudGeneratorAcc8x[pBaudGeneratorAccWidth];

endmodule

Testbench代码

//================================================================  
//  Filename      : BaudGenerator_tb.v  
//  Created On    : 2017-05-16 09:44:30  
//  Last Modified : 2017-05-16 09:55:14  
//  Author        : ChrisHuang  
//  Description   :   
//================================================================

`timescale 1ns / 1ps

module BaudGenerator_tb();
reg rClk;
reg rEnable;

wire wClk;
wire wCLK8x;

initial
begin
	rClk=0;
	rEnable = 1;
	rEnable = 0;
	#100 rEnable = 1;
	#100000 $stop;
end

always #10 rClk = ~rClk;

BaudGenerator #(50000000,115200,16) i1(
	.iClk				(rClk		),
	.iTxdEnable		(rEnable	),
	.iRxdEnable		(rEnable	),
	.oTxdBaudTick	(wClk		),
	.oRxdBaudTick	(wCLK8x	)
);

endmodule

仿真结果

3. 数据发送

         发送模块的开头如下图,除时钟和复位外,输入部分有开始发送信号、波特率脉冲,发送的数据,输出部分由TXD管脚和繁忙管脚。

         单状态机处在非IDLE状态的时候,就是发送模块在发送的时刻,因此处在繁忙状态。

module Rs232Transmit(
	input iClk,
	input iRstN,
	input iStart,
	input iBaudTick,
	input [7:0] ivData,
	output oTxd,
	output oBusy
);

//发送状态机
reg [3:0] rvState;
wire wReady = (rvState == 4'd0);
assign oBusy = ~wReady;
当开始脉冲发送来时,状态机进入开始阶段,然后再在波特率脉冲时刻,进入下一状态。由上文可知,当波特率为115200时这个间隔为8.7us左右。状态码的bit3为1时候,表示为数据状态,低3位则表示数据的次序。状态码的bit0的情况下,剩下采用独热码的状态码,即三个状态用三个bit。
		case(rvState)
			4'b0000:if( iStart )		rvState <= 4'b0100;	//idle, 		1
			4'b0100:if( iBaudTick )	rvState <= 4'b1000;	//start, 	0
			4'b1000:if( iBaudTick )	rvState <= 4'b1001;	//bit0, 		x
			4'b1001:if( iBaudTick )	rvState <= 4'b1010;	//bit1, 		x
			4'b1010:if( iBaudTick )	rvState <= 4'b1011;	//bit2, 		x
			4'b1011:if( iBaudTick )	rvState <= 4'b1100;	//bit3, 		x
			4'b1100:if( iBaudTick )	rvState <= 4'b1101;	//bit4, 		x
			4'b1101:if( iBaudTick )	rvState <= 4'b1110;	//bit5, 		x
			4'b1110:if( iBaudTick )	rvState <= 4'b1111;	//bit6, 		x
			4'b1111:if( iBaudTick )	rvState <= 4'b0001;	//bit7, 		x
			4'b0001:if( iBaudTick )	rvState <= 4'b0010;	//stop1, 	1
			4'b0010:if( iBaudTick )	rvState <= 4'b0000;	//stop2, 	1
			default:						rvState <= 4'b0000;
		endcase
当输入数据后,一个开始脉冲信号将需要发送的数据被锁存;只有当一个波特率脉冲发送来,并且状态处在数据的阶段,锁存数据的寄存器会右移一位。此处可以开出,锁存寄存器的bit0是用于发送的bit数据。
		if( iStart && wReady )
		begin
			rvData <= ivData;
		end
		else if( iBaudTick && rvState[3] )
		begin
			rvData <= rvData>>1;
		end
数据的输出,通过一个assign语句的输出。当非数据域时候,按位或的右边一定为0,所以通过rvState的数值判断发送1还是0。当发送数据域的时候,按位或左边一定为0,可以通过锁存寄存器的bit0获得发送的高低电平。

assign oTxd = (rvState<4) | (rvState[3]&&rvData[0]);

4. 接收模块

         接收模块的输入有RXD、采样时钟、采用时钟使能脚,数据接收完成管脚、数据管脚。
module Rs232Receive(
	input iClk,
	input iRstN,
	input iRxd,
	input iBaudTick,
	
	output oBaudEnable,
	output oRxdDataReady,
	output [7:0] ovRxdData
);
由于RXD是外部的管脚,所以需要通过两个D触发器实现时钟域同步,减小亚稳态的概率。当第1级输入为低,第2级为高,说明输入时钟有一个下降边沿。则开始采集数据。
//时钟域同步
reg [1:0] rRxdSync;
always @ (posedge iClk or negedge iRstN)
begin
	if(!iRstN)
		rRxdSync <= 2'b11;
	else
		rRxdSync <= {rRxdSync[0], iRxd};
end
//下降边沿检测
wire wRxdStart = (rRxdSync == 2'b10);	
当状态机在非idle状态的时候,波特率使能管脚置位,通知波特率产生模块输出比波特率脉冲高8倍的采样时钟。
reg [3:0] rState;
assign oBaudEnable = (rState!=4'b0000);						//波特率使能
采样时钟是波特率的8倍,所以在采样时钟过来是,采样一次,采用获得次数多出现的情况为bit的电平,减小数据错误的概率。
//数据过滤
reg [1:0] rvDataFilter;
always @ (posedge iClk or negedge iRstN)
begin
	if(!iRstN)
	begin
		rvDataFilter <= 2'b11;
	end
	else
	begin
		if(iBaudTick)
		begin
			if(rRxdSync[1] && rvDataFilter!=2'b11)
				rvDataFilter <= rvDataFilter+1'b1;
			else if(~rRxdSync[1] && rvDataFilter!=2'b00)
				rvDataFilter <= rvDataFilter-1'b1;
		end
	end
end
该部分指示除一个bit采样结束。
//数据采样个数计数
reg [2:0] rTickCnt;
always @(posedge iClk or negedge iRstN)
begin
	if(!iRstN)
		rTickCnt <= 3'd0;
	else if(iBaudTick)
		rTickCnt <= (rState==4'b0000)?1'd0:(rTickCnt + 1'b1);
end
wire wSamplingNow = (rTickCnt == 3'd7) && iBaudTick;
当状态机处在idle的状态,则一个开始信号,进入采集模式。第1个必须是起始位,如果不是低电平,则说明是误触发,回到idle状态。
always @ (posedge iClk or negedge iRstN)
begin
	if(!iRstN)
		rState <= 4'b0000;
	else
	begin
		if( rState==4'b0000 && wRxdStart)
			rState <= 4'b0100;													//启动接收
		else if(iBaudTick && wSamplingNow)
		begin
			case(rState)
				4'b0100: if( rvDataFilter==2'b00) rState <= 4'b1000;	//起始位, 确保不是扰动
							else rState <=4'b0000;
				4'b1000: rState <= 4'b1001;									//bit0
				4'b1001: rState <= 4'b1010;									//bit1
				4'b1010: rState <= 4'b1011;									//bit2
				4'b1011: rState <= 4'b1100;									//bit3
				4'b1100: rState <= 4'b1101;									//bit4
				4'b1101: rState <= 4'b1110;									//bit5
				4'b1110: rState <= 4'b1111;									//bit6
				4'b1111: rState <= 4'b0001;									//bit7
				4'b0001: rState <= 4'b0000;									//stop bit
				default: rState <= 4'b0000;
			endcase
		end
	end
end
当处在数据阶段且有一个采集结束的脉冲,则把数据存储下来。
reg [7:0] rvRxdData;
always @ (posedge iClk or negedge iRstN)
begin
	if(!iRstN)
		rvRxdData = 8'd0;
	else if(wSamplingNow && rState[3])
		rvRxdData <= {wBitValue, rvRxdData[7:1]};
end

assign ovRxdData = rvRxdData;
当处在结束位、有一个采集脉冲、采集的是逻辑高则则通知外部模块取数据。
assign oRxdDataReady = (wSamplingNow && rState==4'b0001 && wBitValue);	//确保停止位为逻辑高,ready脉冲持续时间为波特率脉冲的16倍
4. 完整代码
(1)发送器
//================================================================  
//  Filename      : Rs232Transmit.v  
//  Created On    : 2017-05-16 17:10:30  
//  Last Modified : 2017-05-16 17:55:14  
//  Author        : ChrisHuang  
//  Description   :   
//================================================================

module Rs232Transmit(
	input iClk,
	input iRstN,
	input iStart,
	input iBaudTick,
	input [7:0] ivData,
	output oTxd,
	output oBusy
);

//发送状态机
reg [3:0] rvState;
wire wReady = (rvState == 4'd0);
assign oBusy = ~wReady;

reg [7:0] rvData;

always @ (posedge iClk or negedge iRstN)
begin
	if( !iRstN )
	begin
		rvData <= 8'd0;
		rvState <= 4'd0;
	end
	else
	begin
	
		if( iStart && wReady )
		begin
			rvData <= ivData;
		end
		else if( iBaudTick && rvState[3] )
		begin
			rvData <= rvData>>1;
		end
		
		case(rvState)
			4'b0000:if( iStart )		rvState <= 4'b0100;	//idle, 		1
			4'b0100:if( iBaudTick )	rvState <= 4'b1000;	//start, 	0
			4'b1000:if( iBaudTick )	rvState <= 4'b1001;	//bit0, 		x
			4'b1001:if( iBaudTick )	rvState <= 4'b1010;	//bit1, 		x
			4'b1010:if( iBaudTick )	rvState <= 4'b1011;	//bit2, 		x
			4'b1011:if( iBaudTick )	rvState <= 4'b1100;	//bit3, 		x
			4'b1100:if( iBaudTick )	rvState <= 4'b1101;	//bit4, 		x
			4'b1101:if( iBaudTick )	rvState <= 4'b1110;	//bit5, 		x
			4'b1110:if( iBaudTick )	rvState <= 4'b1111;	//bit6, 		x
			4'b1111:if( iBaudTick )	rvState <= 4'b0001;	//bit7, 		x
			4'b0001:if( iBaudTick )	rvState <= 4'b0010;	//stop1, 	1
			4'b0010:if( iBaudTick )	rvState <= 4'b0000;	//stop2, 	1
			default:						rvState <= 4'b0000;
		endcase
		
	end
end

assign oTxd = (rvState<4) | (rvState[3]&&rvData[0]);


endmodule
(2)接收器
//================================================================  
//  Filename      : Rs232Receive.v  
//  Created On    : 2017-05-16 21:30:30  
//  Last Modified : 2017-05-16 21:55:14  
//  Author        : ChrisHuang  
//  Description   :   
//================================================================

module Rs232Receive(
	input iClk,
	input iRstN,
	input iRxd,
	input iBaudTick,
	
	output oBaudEnable,
	output oRxdDataReady,
	output [7:0] ovRxdData
);

//时钟域同步
reg [1:0] rRxdSync;
always @ (posedge iClk or negedge iRstN)
begin
	if(!iRstN)
		rRxdSync <= 2'b11;
	else
		rRxdSync <= {rRxdSync[0], iRxd};
end
//下降边沿检测
wire wRxdStart = (rRxdSync == 2'b10);	

//数据过滤
reg [1:0] rvDataFilter;
always @ (posedge iClk or negedge iRstN)
begin
	if(!iRstN)
	begin
		rvDataFilter <= 2'b11;
	end
	else
	begin
		if(iBaudTick)
		begin
			if(rRxdSync[1] && rvDataFilter!=2'b11)
				rvDataFilter <= rvDataFilter+1'b1;
			else if(~rRxdSync[1] && rvDataFilter!=2'b00)
				rvDataFilter <= rvDataFilter-1'b1;
		end
	end
end

wire wBitValue = rvDataFilter[1];

reg [3:0] rState;
assign oBaudEnable = (rState!=4'b0000);						//波特率使能

//数据采样个数计数
reg [2:0] rTickCnt;
always @(posedge iClk or negedge iRstN)
begin
	if(!iRstN)
		rTickCnt <= 3'd0;
	else if(iBaudTick)
		rTickCnt <= (rState==4'b0000)?1'd0:(rTickCnt + 1'b1);
end
wire wSamplingNow = (rTickCnt == 3'd7) && iBaudTick;

always @ (posedge iClk or negedge iRstN)
begin
	if(!iRstN)
		rState <= 4'b0000;
	else
	begin
		if( rState==4'b0000 && wRxdStart)
			rState <= 4'b0100;													//启动接收
		else if(iBaudTick && wSamplingNow)
		begin
			case(rState)
				4'b0100: if( rvDataFilter==2'b00) rState <= 4'b1000;	//起始位, 确保不是扰动
							else rState <=4'b0000;
				4'b1000: rState <= 4'b1001;									//bit0
				4'b1001: rState <= 4'b1010;									//bit1
				4'b1010: rState <= 4'b1011;									//bit2
				4'b1011: rState <= 4'b1100;									//bit3
				4'b1100: rState <= 4'b1101;									//bit4
				4'b1101: rState <= 4'b1110;									//bit5
				4'b1110: rState <= 4'b1111;									//bit6
				4'b1111: rState <= 4'b0001;									//bit7
				4'b0001: rState <= 4'b0000;									//stop bit
				default: rState <= 4'b0000;
			endcase
		end
	end
end

reg [7:0] rvRxdData;
always @ (posedge iClk or negedge iRstN)
begin
	if(!iRstN)
		rvRxdData = 8'd0;
	else if(wSamplingNow && rState[3])
		rvRxdData <= {wBitValue, rvRxdData[7:1]};
end

assign ovRxdData = rvRxdData;

assign oRxdDataReady = (wSamplingNow && rState==4'b0001 && wBitValue);	//确保停止位为逻辑高,ready脉冲持续时间为波特率脉冲的16倍

endmodule
(3)顶层文件
//================================================================  
//  Filename      : UsartModule.v  
//  Created On    : 2017-05-16 17:54:30  
//  Last Modified : 2017-05-16 17:55:14  
//  Author        : ChrisHuang  
//  Description   :   
//================================================================

module UsartModule
#(parameter pClkFre = 50000000,
parameter pBaud = 115200,
parameter pBaudGeneratorAccWidth = 16)(
	input iClk,
	input iRstN,
	
	input iTxdStart,
	input [7:0] ivTxdData,
	
	input iRxd,
	
	output oTxd,
	output oTxdBusy,
	
	output oRxdDataReady,
	output [7:0] ovRxdData
	
);

wire wTxdTick;
wire wRxdTick;
wire wRxdBaudEnable;

BaudGenerator #(pClkFre,pBaud,pBaudGeneratorAccWidth) BaudTick(
	.iClk				(iClk					),
	.iTxdEnable		(oTxdBusy			),			//当发送时候,处于繁忙
	.iRxdEnable		(wRxdBaudEnable	),
	
	.oTxdBaudTick	(wTxdTick			),
	.oRxdBaudTick	(wRxdTick			)
);

Rs232Transmit Transmit(
	.iClk			(iClk			),
	.iRstN		(iRstN		),
	.iStart		(iTxdStart	),
	.iBaudTick	(wTxdTick	),
	.ivData		(ivTxdData	),
	
	.oTxd			(oTxd			),
	.oBusy		(oTxdBusy	)
);

Rs232Receive Receive(
	.iClk					(iClk					),
	.iRstN				(iRstN				),
	.iRxd					(iRxd					),
	.iBaudTick			(wRxdTick			),
	
	.oBaudEnable		(wRxdBaudEnable	),
	.oRxdDataReady		(oRxdDataReady		),
	.ovRxdData			(ovRxdData			)
);


endmodule
(4)测试代码
//================================================================  
//  Filename      : UsartModuleTxd_tb.v  
//  Created On    : 2017-05-16 09:44:30  
//  Last Modified : 2017-05-16 09:55:14  
//  Author        : ChrisHuang  
//  Description   :   
//================================================================

module UsartModuleTxd_tb();

reg rClk;
reg rRstN;
reg rTxdStart;
reg [7:0] rvTxdData;

//reg rRxd;

wire wTxd;
wire wTxdBusy;

wire wRxdDataReady;
wire [7:0] wvRxdData;


initial
begin
	rClk = 0;
	rRstN = 1;
	
	#10 rRstN = 0;
	#10 rRstN = 1;
	
	#1000000 $stop;
end

always #10 rClk = ~rClk;

UsartModule i1(
	.iClk				(rClk				),
	.iRstN			(rRstN			),
	
	.iTxdStart		(rTxdStart		),
	.ivTxdData		(rvTxdData		),
	
	.iRxd				(wTxd),
	
	.oTxd				(wTxd				),
	.oTxdBusy		(wTxdBusy		),
	
	.oRxdDataReady	(wRxdDataReady	),
	.ovRxdData		(wvRxdData		)
);

reg [1:0]rCnt;
always @ (posedge rClk or negedge rRstN)
begin
	if(~rRstN)
	begin
		rvTxdData <= 8'h0;
		rCnt <= 2'd0;
		rTxdStart <= 1'b0;
	end
	else if(wTxdBusy == 1'b0)
	begin
		case( rCnt )
			2'b00:rvTxdData <= 8'h55;
			2'b01:rvTxdData <= 8'h5A;
			2'b10:rvTxdData <= 8'hA5;
			2'b11:rvTxdData <= 8'hAA;
			default:rvTxdData <= 8'h55;
		endcase
		rTxdStart <= 1'b1;
	end
	else
	begin
		if(rTxdStart == 1'b1)	//rTxdStart置位后,wTxdBusy会迟一个时钟置位
			rCnt <= rCnt+1'b1;
		rvTxdData <= 8'h0;
		rTxdStart <= 1'b0;
	end
end


endmodule
(5)测试结果


  • 2
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值