QT上位机+STC单片机实现串口通信

要实现串口通信主要分为两个部分,下位机和上位机,下位机主要完成的功能是数据的生成和发送,上位机主要需要将传送来得数据进行存储和处理,这里分为这两部分分别来说

下位机

我这里要实现的功能是通过使用光敏传感器和热敏传感器完成对声、光的A/D采集,并且把这些数据封装成数据包发送到上位机上。

数据采集

寄存器初始设置

    P0M0 = 0xff;
	P0M1 = 0x00;
	P2M0 = 0x08;
	P2M1 = 0x00;
	SEL0 = 0;
	SEL1 = 0;
	SEL2 = 0;
void ADC_LightInit()
{
	P1ASF = 0xff;
	ADC_RES = 0;
	ADC_RESL = 0;
	ADC_CONTR = 0x8c;
	CLK_DIV = 0x20;
}

/*ADCTemp???*/
void ADC_TempInit()
{
	P1ASF = 0xff;
	ADC_RES = 0;
	ADC_RESL = 0;
	ADC_CONTR = 0x8b;
	CLK_DIV = 0x20;
}

注意到这里的温度和光照的ADC_CONTR设置是不同的,因为这两个AD采集需要不同的通道,所以选用的时候会将不同的寄存器值置为1,比如这里分别选择了P1^3和P1^4作为AD转换的通道

采集过程

void ADC_pcs() interrupt 5
{
	time++;
	if(time==2000)
	{
		time = 0;
		if(flag==1)
		{
			/*?*/
			light = (suml+l/2)/l;
			l=0;
		}
		if(flag==-1)
		{
			/*??*/
			temp = (sumt+t/2)/t;
			t = 0;
		}
	}
	if(flag==1)
	{
		l++;
		datal = ADC_RES*256+ADC_RESL;
		suml += datal;
	}
	if(flag==-1)
	{
		t++;
		datat = ADC_RES*256+ADC_RESL;
		sumt += datat;
	}
	ADC_CONTR&=~0X10;  					 //?????,ADC_FLAG??
	ADC_CONTR|=0X08;	 					 //?????,ADC_START?1
	IE = 0xb2;
}

这里使用了一个flag,每次根据这个flag来判断我们这次采集的是光照还是温度,这个值的改变是在定时器中断中完成的,同时也会重新调用上面的两个初始函数中的一个,完成AD通道的切换。这里使用了l和t来分别计算数据采集的次数并且取平均值,减小数据的波动。最后对寄存器的置一或者清零可以参考数据手册上对于每个值的解释:

当我们完成一次AD采集之后,需要软件清零的位置通过ADC_CONTR &= ~0x10完成,同时对于ADC_START置一,开始下一次的转换

封装进数据包

构建结构体,将所需要的数据存入,并依次发送

data struct 
{   unsigned char head1;
    unsigned char head2;
    unsigned char ch;
	unsigned int  X;
	unsigned int  Y;			
} txd_data={0xaa,0x55,1,0x1234,0x5678};

这里的值只是初始的,后面我们如果要将采集的数据装进来的话可以对其中某些数据进行直接修改

在前面的函数中已经完成了数据采集,分别为light和temp,这里将这两个数据装进来就可以了,所以通过一个setdata函数来实现

void setdata()
{
	txd_data.X = light;
	txd_data.Y = temp;
}

什么时候将我们调用的函数装进来呢,我这里是放在了定时器的中断中,同时定时器也承担这数码管动态扫描的任务,数据管显示的内容就是我们采集出来的数据

void timer0() interrupt 1
{
	display();
	setdata();
	TH0 = (65535-100)/256;
	TL0 = (65535-100)%256;
	count_1s++;
	if(count_1s==1000)
	{
		count_1s=0;
		flag_1s=1;
	}
	ADC_CONTR |= 0x80;
	IE = 0xb2;
	TR0 = 1;
}

display函数就是将采集出的AD值按十六进制分为三位一次显示,八位数码管,左面三位显示光照,右面三位显示温度,这里不再解释

串口通信——发送

寄存器初始

void UartInit(void)  /*********该初始化函数由"STC-ISP"软件生成*******/
{						//[email protected] 
	SCON = 0x50;		//8位数据,可变波特率
	AUXR |= 0x40;		//定时器1时钟为Fosc,即1T
	AUXR &= 0xFE;		//串口1选择定时器1为波特率发生器
	TMOD &= 0x0F;		//设定定时器1为16位自动重装方式
	TL1 = 0x00;		//设定定时初值
	TH1 = 0xF7;		//设定定时初值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1
}

发送数据

这里要说我之前的思路出现了一点问题,问题在于对于SBUF、TI、RI这几个寄存器的理解不够。SBUF用来承接下位机传过来的数据,大小是一个字节,每次发送完成会产生一个中断使得TI这个寄存器为1,而我之前的想法是用一个数组来保存数据,每次TI为1都将数组的值依次赋给SBUF,这是不对的,发送完第一个值就停止了。

所以我们需要一个变量来判断我们现在是不是已经将结构体中的值全发送完了,如果发送没发送完的话那么修改指针到下一个字节并赋值给SBUF,发送完成就将现在的状态修改为FREE,具体实现如下

void Uart1_isr() interrupt 4 		   //串口中断函数   
{	
	if (TI) 
	{ 
			TI = 0;                      // 如果上一个数据发完,TI = 0; 
		  if (txd_count >= txd_N)      // 如果数据包发送完毕	   
			  flag_txd=FREE;
		  else 
			{ 
				SBUF=*txd_point++;    // 否则发送数据包下一字节
		         txd_count++;		   // 发送计数++
			}					 
	}       
	else RI=0;						   // 接收中断
}
enum txd_status Uart1_print(void *pt, unsigned int num)	 //非阻塞串口发送函数(与Uart1_isr()配合)
{  
		if (flag_txd==BUSY) return(BUSY);
        txd_point=(char *)pt;	   //指定发送数据的起始指针
		txd_N=num;	               //指定发送数据的字节数,不含校验和字节
		txd_count=0;			   //发送字节计数清零
		flag_txd=BUSY;
		TI = 1;					   //启动发送
		return(OK);
}

上位机

上位机是用QT来实现的,主要是实现了一个图形界面,然后使用QT自带的串口功能读取出数据,并显示出来

图形界面:

可以看到这里涉及到串口号/波特率/数据位/校验位/停止位的设置,具体的设置都在QSerialPort的方法里:

        qsp.setPortName(ui->ckh->currentText());
        qsp.setBaudRate(QSerialPort::Baud1200);
        qsp.setParity(QSerialPort::NoParity);
        qsp.setDataBits(QSerialPort::Data8);
        qsp.setStopBits(QSerialPort::OneStop);
        qsp.setFlowControl(QSerialPort::NoFlowControl);

这里涉及到一个概念是当我们右键点击图形界面时,有一个转到槽选项,这个东西不是特别好理解,我们这里可以把它理解为为这个组件提供了一个方法,比如我们为“打开串口”这个按钮转到槽,选择onclick的方法,也就是我们点击到按钮就调用这个方法,这个方法做了什么呢?首先我这里对按钮的文本内容进行了判断,如果是打开串口,则像上面设定串口的一系列参数,然后判断QSerailPort的open是否返回为true,如果是则说明成功打开了串口,否则说明打开失败

qsp.open(QIODevice::ReadWrite)

这个方法判断了QT的IO端口的读写状态,在后面的方法中还会涉及

    QObject::connect(&qsp,&QSerialPort::readyRead,this,&MainWindow::readData);

这一句在验收实验的时候被学长问到了,怪自己当时没好好看代码,其实可以这样看,首先我们看一下这个方法内部是怎么定义的:

看到这有一种豁然开朗的感觉,sender是什么,可以看到对应了我们的串口对象,也就是数据的来源,这里把它叫做sender很形象,然后是一个方法,什么方法呢?对应到我们的代码中可以看到,是反应读写状态的方法,把它当做一个信号,也就是如果我们的读写状态正常的话就能正常地执行connect这个方法,connect这个名字也很形象,将串口的两端连接起来。后面的参数包括了接收方,也就是我们的上位机对应的对象,还需要一个方法来对数据有所处理,这里我们自己定义了一个方法叫做readData

数据处理

在前面定义的readData方法中,我们首先要将数据取出来,可以通过串口对象的readall方法将数据全部取出,这里使用了QBytearray进行存储,这里其实自己也没认真思考为什么,但是当我使用QString存储之后,发现输出的都是一些类似乱码的字符,突然想起来我们在下位机发送的时候是按照字节装填并且发送的,这里按照文本类型进行解析,把我们的字符解析成了乱码

同时如果使用isp软件的串口助手对应的文本模式的话,效果也是一样的,Hex模式就能正确读取出

 所以我们读取出来之后,将QBytearray中的内容调用toHex方法转换为16进制格式,再与原文本框中内容进行合并,实现数据的连续显示

QByteArray qba = qsp.readAll();
QString str = ui->AcptEdit->text();
str += qba.toHex();
ui->AcptEdit->setText(str);

完成了显示

出现的问题

下位机进行数据显示的时候,会有频闪的现象,是由于定时器中断时间选择过长造成的,修改中断时间变短,使得现象变得没那么明显了

从运行的结果可以看到我这里本来是想要弄一个折线图反应变化趋势的,但是遇到了一些麻烦,首先是数据类型的问题,我们将串口中数据取出之后转换为Hex格式,再调用方法转换为数字之后,测试的时候先将这个数输出,发现一直是0,意识到会不会是超过了int的范围,于是缩减,使用了这样的方法:

for(int i=0;i<qba.size();i++)
        std::cout<<qba[i];
    std::cout<<std::endl;

发现输出的内容和串口助手文本模式下的内容一样,对于数据的内容解析出了错误,这里还没想明白该怎么修改。

猜你喜欢

转载自blog.csdn.net/LieberVater/article/details/88971067