51 单片机 + ESP8266 实现互联网远程控制小灯

目录

1、准备工作

1.1、软件

1.2、硬件

2、硬件连线

3、C 语言程序

4、测试

 5、总结


1、准备工作

1.1、软件

  • keil 4
  • 串口调试工具 

1.2、硬件

  • STC89C516(扩展有外部RAM,貌似有 60K+,下面的程序没用到)
  • 跳线若干
  • ESP8266

我这里使用的 普中51-单核-A4 学习板,硬件电路已经基本都连接完了,而且它比较好的一点就是提供了 ttl 转 usb 的接口,可以用于配置 ESP8266,而且还提供了 3.3V & 5V 电源外接(注意: 烧录程序的时候还是不要外部供电,否则程序烧录不进去) 还是很贴心的,淘宝上价格 60 多块钱吧,作为我这种从事互联网纯软件开发过来试水51 单片机的程序员来说够用了。

2、硬件连线

硬件连线其实很简单,商家都会提供该款单片机学习板的电路原理图的,而且开发板都有预留单片机 I/O 的外部接口,只要依照这电路图找出 RXD/TXD I/O 口就行,普中51-单核-A4 这款单片机的 RXD、TXD 为 P30 和 P31 口,只要将 ESP8266 接通 3.3 V 电源,将 wifi 模块的 RX 和 TX 与单片机对应的口交叉相连就可以了,实际连线如下:

单片机左边连线从上到下分别是 3.3V 和 GND,右边连线从上到下依次是 P31(TXD)、P30(RXD) 口,具体查看手里单片机管脚图即可。

配置 ESP8266 我参考是 单片机+wifi 远程控制开关灯,讲得很详细,大家参考操作即可。

3、C 语言程序

#include <stdio.h>
#include <reg52.h>
#include <ctype.h>
#include <string.h>
#include <math.h>

#define uchar unsigned char
#define uint unsigned int

sbit LED1 = P2^0;
sbit LED2 = P2^1;
sbit LED3 = P2^2;
sbit LED4 = P2^3;
sbit LED5 = P2^4;
sbit LED6 = P2^5;
sbit LED7 = P2^6;
sbit LED8 = P2^7;

// 串口中断接收相关
uint scount = 0, maxLen = 25, tscount = 0;
uchar srdatas[25];
// 计时器 0, 多大代表有多少个 50ms(0 ~ 65535)
uint time0Count = 0;
// 串口接收数据已处理标志
bit sflag = 0;
// 校验连接发送标志(1-表示需要发送)
bit tsflag = 0;

void Delay2(unsigned long cnt);

void Tranfer(uchar *s);
void SysInit();
void SetWifi();
void dealReceiveData();
void dealReceiveLine(uchar* line, uint length);
void dealWifiConnectInfo();
// 10 进制 => 2 进制
//uchar* decimal2binary(uint val);
// 字符串转数字
// uint parseInt(uchar* str, uint len);

void Delay2(unsigned long cnt) {
	long i;
 	for(i=0;i<cnt*10;i++);
}

/*
uchar* decimal2binary(uint val) {
	uchar chs[8];
    uint i = 0, tmp = val;
	for(i = 0; i < 8; i++) {
		if(tmp == 0) {
			chs[i] = 0;
			continue;
		}
		chs[i] = (tmp % 2) + 48;
		tmp = tmp / 2;
	}
	
	for(i = 0; i < 4; i++) {
		tmp = chs[i];
		chs[i] = chs[8-i-1];
		chs[8-i-1] = tmp;	
	}

	return chs;
}
*/

/*
uint parseInt(uchar* str, uint len) {
	uint i = 0, resVal = 0;
	for(i = 0; i < len; i++) {
		resVal = resVal + ((str[i] - 0x30) * pow(10, len - i - 1)); 	
	}
	return resVal;
}
*/

// 接收到字符串出现 /r/n, 视为新行标志
void dealReceiveData() {
	uint t = 0;

	if(sflag == 1 || scount <= 0) {
		return;
	}

	// 延时之后依然没有数据, 既断定为串口有数据接收到
	tscount = scount;
	// 使用 Delay2 有问题
	// Delay2(100);
	t = time0Count;

	// 演示 50 ms
	while(abs(time0Count - t) <= 0);

	if(scount != tscount) {
	   return;
	}
   	
	// 数据处理期间暂停串口中断使能
	ES = 0;
	// 数组未溢出
	if(scount < maxLen) {
	  // 接收到第二个数据之后, 判断是否出现了结束符
	  if(scount > 2) {
	  	// 出现了换行符, 处理接收到的行的数据
	  	if(srdatas[scount - 2] == '\r' && srdatas[scount - 1] == '\n') {
		   dealReceiveLine(srdatas, scount - 2);
		   scount = 0;	
		}	
	  }
	}

	// 执行之后即为串口接收到的数据已经处理
	sflag = 1;
	ES = 1;
}

void dealWifiConnectInfo() {
	// 开始发送尝试重连
	 if(tsflag == 1) {
	 	ET0 = 0;
		printf("AT+CIPCLOSE=2\r\n");
		Delay2(5);
		printf("AT+CIPSTART=2,\"TCP\",\"115.29.109.104\",6520\r\n");
		Delay2(10);
		tsflag = 0;
		ET0 = 1;
	 }
}

void dealReceiveLine(uchar* line, uint length) {
	// bit hasCommand = 0;
	uint i = 0, t = 0;
	uchar command;
	uchar newLine[25];

	// 去除换行符
	if(length > 3) {
		for(i = 0; i < length; i++) {
			if(line[i] != '\r' && line[i] != '\n') {
			   newLine[t] = line[i];
			   t++; 
			}
			if(t >= 25) {
				length = t;
				break;
			}
		}	
	}
	
	if(length > 3) {
		// 处理开关控制指令
	 	if(
			newLine[0] == '+'
			&& newLine[1] == 'I'
			&& newLine[2] == 'P'
			&& newLine[3] == 'D'	
		) {
			i = 0;
			while(i < length && newLine[i] != ':') {
				i++;
			}
			// 存在有效位置
			if(i < length) {
			   t = 0;
			   for(i = i + 1; i < length; i++) {
			   		t++;
                    if(newLine[i] == '/') {
						continue;
					}
			   		command = newLine[i] - 0x30;
			   		switch(t) {
						case 1:
							LED1 = !command;
							break;
						case 2:
							LED2 = !command;
							break;
					    case 3:
							LED3 = !command;
							break;
						case 4:
							LED4 = !command;
							break;
						case 5:
							LED5 = !command;
							break;
						case 6:
							LED6 = !command;
							break;
						case 7:
							LED7 = !command;
							break;
						case 8:
							LED8 = !command;
							break;
					}
					
					if(t >= 8) {
						break;
					}	
			   }
			}	
		}

		/*
		LED6 = ~LED6;

		 // 处理定时轮询的远程连接状态响应数据
		 if(tsflag == 1) {
		   LED7 = ~LED7;
		   if(
			 	newLine[0] == 'A'
				&& newLine[1] == 'L'
				&& newLine[2] == 'R'
				&& newLine[3] == 'E'
				&& newLine[4] == 'A'
				&& newLine[5] == 'D'
				&& newLine[6] == 'Y'
			) {
			 	LED8 = 0;
			} else {
				LED8 = 1;
			}
			tsflag = 0;
		 }
		 */
	 }
}

void SysInit() {
  	// 初始化定时器1, 配置波特率发生器
	TH1 = 0xFD;	 //晶振11.0592mhz 波特率设为9600
	TL1 = TH1;
	TMOD |= 0x20;	 //定时器1方式2
	SCON = 0x50;	 //串口接收使能
	ES = 1;			 //串口中断使能
	TR1 = 1;		 //定时器1使能
	TI = 1;			 //发送中断标记位,必须设置

	// 初始化定时器0, 做系统定时任务(11.0592MHz, 定时 50ms)
	/* */
	TH0 = 0x4C;
	TL0 = 0x00;
	TMOD |= 0x01; // 工作在方式2
	TR0 = 1; // 定时器0使能
	ET0 = 1;
	
	
	//REN = 0; // 禁止串口接收数据
	printf("begin init wifi...\r\n");
	SetWifi();
	printf("wifi inited...\r\n");
	//REN = 1;
	EA=1;
}


void SetWifi() {
   Delay2(1000);		
   printf("AT+CIPMUX=1\r\n");
   
   Delay2(1000);		
   printf("AT+CIPSERVER=1\r\n");

   Delay2(1000);
   printf("AT+CIPCLOSE=2\r\n");

   Delay2(2000);
   printf("AT+CIPSTART=2,\"TCP\",\"115.29.109.104\",6520\r\n");

   Delay2(2000);
}

/**/
void Timer0() interrupt 1 {
	uint t = 0;
	ET0 = 0;
	// 继续下一轮定时
	TH0 = 0x4C;
	TL0 = 0x00;
	time0Count = (time0Count + 1) % 1200;
	// 大概 30s 判断一下连接是否断开, 断开之后需要重连
	if(time0Count % 600 == 0) {
		tsflag = 1;
	}
	ET0 = 1;
}


void Usart() interrupt 4 {
    if(RI == 1)
    {
		RI = 0;
		srdatas[scount] = SBUF;
		scount += 1;
		if(scount >= maxLen) {
			scount = 0;	
		}
		sflag = 0;
	}
}

void main() {
	SysInit();
	while(1) {
		dealReceiveData();
		dealWifiConnectInfo();	
	}
}

上述程序编写经过实际测试,没有问题,具体细节如下:

  • void Uart() interrupt 4 {...}
    • 此方法是串口接收数据方法,在接收的时候,接收是一个一个字节接收的,而且在接收的时候发送端是连续发送的,并不是要等到接收端的 RI = 0(接收完成标志) 或者 ES=1(串口中断使能) 的时候才会发送下一个字节。比如现在发送字符串 "abc" 给我们,波特率是 9600,一秒就能传送 9600/8=1.2KB,大约 0.83ms 就会传输一个字母,如果我们在 Uart 中加入过多的处理逻辑,很可能就会导致传输数据的不完整(这个问题让我纠结了好久,纯软件的通病,接口传数据都是调用别人写好的API,谁会在意这些细节)!所以这里我的处理逻辑是定义一个长度为 25 的 uchar 数组缓存串口接收到的数据,超过之后数据位图标归 0,sflag 置为 0,表示接收到了新数据新数据,等待处理。
  • void Time0() interrupt 1 {...}
    • 这个是定时器 0 的中断函数,用于系统中其他定时任务定时使用,间隔大约是 50ms。
  • void SetWifi() {...}
    • wifi 模块已经事先配置好连接的 AP 信息,所以开始的时候需要延时一下,保证 wifi 连接成功。
    • AT+CIPMUX=1 => 允许多连接。
    • AT+CIPSERVER=1 => 开启服务器模式。
    • AT+CIPCLOSE=2 => 先关闭一下 TCP 连接,复位的时候有用。
    • AT+CIPSTART=2,"TCP","115.29.109.104",6520 => 重新连接 TCP,115.29.109.104:6520 是一个公开的 TCP Server,类似一个聊天室的功能,比如 A、B、C 都连接到该 TCP Server,则 A 发送的数据能够被 B、C 收到,其他的也是如此,这里就不费功夫自己写一个了,有兴趣的朋友可以自己写一下。
  • void SysInit() { ... }
    • 初始化定时器1 作为波特率发生器, 波特率为9600。
    • 开启串口。
    • 开启中断。
    • 初始化定时器 0 做其他定时任务。
    • 初始化 wifi,注意中断使能要在初始化之后开启。
  • void dealReceiveData() { ... }
    • 判断串口是否接收完一行命令,逻辑就是每隔 50ms 判断一下当前接受数据的长度有没有变化,如果变化了,并且当前接收到的数据最后两个字符为 '/r'、'/n'(16进制就是 0x0D、0x0A),如果是,交给 dealReceiveLine 函数处理。
  • void dealReceiveLine(...) { ... }
    • 处理逻辑如下
      1. 去除接收到的行的前后换行符。
      2. 判断处理后的结果是否以 +IPD 开头。
      3. 找到 : 后的 8 个字符,依次控制 LED1 ~ LED8 的开关状态,1 为开启,0 为关闭。
    • 开关字符串例子
      1. +IPD,2,2:111 => LED1:开启,LED2:开启,LED3: 开启(之后没有数据,则跳过控制之后的小灯,因此要控制之后的小灯需要将前面的小灯控制指令也加上,当然,如果对应位置开关指令是 '/',则会跳过该开关指令)
  • void dealWifiConnectInfo( ... ) { ... }
    • 间隔 30s 左右重连一次 TCP Server(因为这个测试 TCP Server 连接几分钟之后未接收或者发送数据,就会被服务端抛弃)。

dealReceiveData 方法中延时使用的是定时器,没有用  Delay2(),因为如果在延时期间发生了中断,则 Delay2() 会死循环,具体原因不详,有知道的小伙伴麻烦留言解释一下,不胜感谢,其他需要注意的就是在处理串口接收到的数据的时候需要将串口关闭,处理完成之后开启,为什么呢?因为串口接收数据缓存是互斥量,不能在读取的数据处理的时候再改变该数据,其实就是一个变相加锁。在读取接收缓存的时候,不允许更改接收缓存的数据。

4、测试

写了个 java 程序,每隔 1S 向 TCP Server 发送 0 ~ 255 的 二进制数据,以此控制小灯,程序采用 Java 编写,如下:

pulbic class LEDTests {
    
    @Test
    public void ledControlTest() throws InterruptedException {
        try {
            //创建Socket对象
            Socket socket = new Socket("115.29.109.104", 6520);
            //获取一个输出流,向服务端发送信息
            OutputStream outputStream = socket.getOutputStream();
            //将输出流包装成打印流
            PrintWriter printWriter = new PrintWriter(outputStream);
            char chs[] = { '0', '0', '0', '0', '0', '0', '0', '0' };
            int t;

            //根据输入输出流和服务端连接
            for(int i = 0; i < 10000; i++) {
                String binaryStr = Integer.toBinaryString((i + 1) % 256);
                char[] tchs = binaryStr.toCharArray();
                t = 0;

                for(int j = 8 - tchs.length; j < 8; j++) {
                    if(j < (8 - tchs.length)) {
                        chs[j] = '0';
                    } else {
                        chs[j] = tchs[t++];
                    }
                }

                StringBuffer sb = new StringBuffer();
                for (char ch : chs) {
                    sb.append(ch);
                }
                binaryStr = sb.toString();
                // 发送数据后需要加上换行符
                printWriter.print(binaryStr + "\r\n");
                Thread.sleep(500);
                printWriter.flush();
                System.out.println("=> " + binaryStr);
            }

            //关闭输出流
            socket.shutdownOutput();
            printWriter.close();
            outputStream.close();
            socket.close();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }    

}

实际效果如下:

 5、总结

本来想扩展很多功能,比如说设备可以使用红外遥控器配置热点连接信息,超时重连时间,向服务器上传温湿度,传输信息加密,单片机间隔一段时间发送心跳包给服务器,点对点控制等等功能,不过想想挺麻烦的,哈市不做了!!!听说 arduino 、STM32 的 RAM 方便很多,等后面学了这些单片机再做这些功能吧,对于 51 这种微控制器还是不想折腾这么多功能了。

 

  • 25
    点赞
  • 193
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值