C语言&嵌入式物联之美-物联网专业

51单片机实战:物联网初步のESP8266无线网络模块

2017-02-10  本文已影响13674人  兔子泽

文章框架

文章框架

前言

扯单一周目BOSS出现!请速速讨伐!
ESP8266是我第一个,也是唯一一个接触过的无线网络模块,我非常喜欢!大部分网上的教程都是ARM架构单片机配用ESP8266。我比较抠门,给大家上51版的。
我记得当时和老板承诺可以做成无线通信的时候我也不是很确定,心里一直打鼓。好在最后做出来了,很有成就感!所以今天的代码案例是我把我之前在公司写的项目代码简化之后分享给大家的。虽然结构有点恶心,但应该还是入得了眼的。
如果有没看懂的地方,大家评论区告诉我。

需求

利用ESP8266芯片,通过无线网络同电脑建立TCP连接。接收电脑传来的消息并显示在LCD上。

每条信息以\结尾。
每条信息字符数不超过32。
信息内容只能是ASCII表内的字符。


清单

硬件

学习板
ESP8266
型号 主芯片 无线标准 工作电压 安全机制 支持模式
ESP8266-01 ESP8266 IEEE 802.11b/g/n 3.3V WEP/WPA-PSK/WPA2-PSK STA、AP、STA+AP

STA 模式:ESP8266模块通过路由器连接互联网,手机或电脑通过互联网实现对设备的远程控制。
AP 模式:ESP8266模块作为热点,实现手机或电脑直接与模块通信,实现局域网无线控制。
STA+AP 模式:两种模式的共存模式,即可以通过互联网控制可实现无缝切换,方便操作。

引脚图

上图对8个针脚进行说明。

UTXD GND CH_PD GPIO2 GPIO16 GPIO0 VCC URXD
发送 接地 高电平工作 |||电源 接收

本例只用到这五个引脚(画“\”的不用),其他引脚的说明资料请自行到淘宝链接处下载。

AT+CIOBAUD=<baudrate>,<databits>,<stopbits>,<parity>,<flow control>

如:

AT+CIOBAUD=9600,8,1,0,0  

软件

程序 说明 下载
UartAssist 串口调试助手,用来给单片机发送消息 度娘网盘
NetAssist 网络调试助手,在电脑端建立TCP连接与单片机的ESP8266进行通信 度娘网盘
STC-ISP STC单片机工具集,很强大,可烧录程序,可串口调试等等 度娘网盘
CH340驱动 CH340的USB驱动,如果没有这个驱动,你的电脑可能识别不到单片机(识别到CH340就相当于识别到单片机的串口。) 度娘网盘
51单片机波特率初值计算工具 用于计算在各种波特率和晶振频率等参数下计时器的初值,属于辅助工具,省的还得算。 度娘网盘

分析

调试

在开始用单片机直接和无线模块通信之前,首先要绕过单片机直接和无线模块通信以确定其可以使用和接入默认网络(接入一个热点后,模块每次断电后启动都会自动连接该热点),这样可以让单片机少做很多事情(要知道我们用的是51单片机,硬件资源极其有限,能省则省)。

实物图

这个图就很难看出线是怎么连的了,所以你要忍受我丑陋的画工。

?諄MEM CHECK FAIL!!!
d{$弬s
Ai-Thinker Technology Co. Ltd.

invalid

显示的信息类似上面,你可以先给个测试命令AT看看是否可以接受指令

注意!每个指令后要跟回车再发送!

如果返回OK则说明指令可以被接收并识别。

如果下面的指令都会返回ERROR(在没有给错指令的情况下),可以尝试AT+RST重启模块。

1. 更改模式
指令:AT+CWMODE?
一般情况下,第一次使用会返回2,也就是AP模式,我们不用它发热点,所以要改回Station模式(模式1)。

指令:AT+CWMODE?
一般情况下,第一次使用会返回2,也就是AP模式,我们不用它发热点,所以要改回Station模式(模式1)。

指令:AT+CWMODE=1
返回:OK则成功

2. 接入热点(连Wi-Fi)
指令:AT+CWJAP=<SSID>,<Password>
参数:<SSID>处填写热点名称,<Password>处填写密码。两者都要用双引号括起来。
例如:AT+CWJAP="CMCC","123456"

指令:AT+CWJAP=<SSID>,<Password>
参数:<SSID>处填写热点名称,<Password>处填写密码。两者都要用双引号括起来。
例如:AT+CWJAP="CMCC","123456"

返回:WIFI CONNECTED:连接到热点
返回:WIFI GOT IP:分配到IP,走到这一步,就算已经连入到热点了。

如果忘记SSID了,想看一下可以使用下面的指令,列出广播的SSID(隐藏的不会显示)。
指令:AT+CWLAP

3. 连接TCP服务器
首先打开NetAssist,设置TCP Server,然后建立连接(注意防火墙)。注意,Server必须在Client所在内网或其外网(我的是在同一个内网)。

网络调试助手

首先打开NetAssist,设置TCP Server,然后建立连接(注意防火墙)。注意,Server必须在Client所在内网或其外网(我的是在同一个内网)。


网络调试助手

指令:AT+CIPSTART=<Type>,<DomainName>,<Port>
参数:<Type>处写TCP或UDP,<DomainName>处写域名,<Port>处写端口号。
本例:AT+CIPSTART="TCP","192.168.1.110",1234
返回:

CONNECT

OK

说明连接成功。
在NetAssist中,数据接收框的下面有一个连接对象,点开后发现除了All Connections之外,多了一个客户,就确定客户连接到服务器了。

NetAssist
在下方文本框输入信息后发送,可在串口调试助手中看到ESP8266所接收到的信息
UartAssist收到的信息:+IPD,10:Hello 简书

到这里就说明网络连接及建立TCP都可以顺利完成,在下面的单片机操作中就会变得方便很多。

注意,ESP8266每次断电后重新上电,最多只会自动连到之前连接的热点,但不会自动连接到TCP服务器,所以,建立连接要交给单片机来做。

问题

到这里就做完了所有理论上的准备工作,也就是理论上我们已经可以实现这个程序了,下面代码实现。


代码

说下这次代码比较新颖的地方,一个是用到了.c.h文件组合的模块形式,另一个是用到了函数指针(函数名也被括起来的那个)。前者这种写法是为了将各子功能模块化,.h文件里的内容相当于面向对象编程里的public.c文件一个是实现.h内的函数,另一个就是隐藏函数和变量,相当于private。后者是用来充当高级语言中的“事件”,让函数调用变得更加灵活,其具体用法请自己查资料或者看相关C语言书籍。

总而言之,这次的代码是模块化事件化的,类似面向对象的编码风格。

#ifndef __ASCIIS__
#define __ASCIIS__

#define NUL 0x00 // NULL
#define SOH 0x01 // Start of Heading
#define STX 0x02 // Start of Text
#define ETX 0x03 // End of Text
#define EOT 0x04 // End of Transmission
#define ENQ 0x05 // Enquiry
#define ACK 0x06 // Acknowledge
#define BEL 0x07 // Bell
#define BS 0x08 // Backspace
#define HT 0x09 // Horizontal Tab
#define LF 0x0A // Line Feed
#define NL 0x0A // New Line
#define VT 0x0B // Vertical Tab
#define FF 0x0C // Form Feed
#define NP 0x0C // New Page
#define CR 0x0D // Carriage Return
#define SO 0x0E // Shift Out
#define SI 0x0F // Shift In
#define DLE 0x10 // Data Link Escape
#define DC1 0x11 // Device Control 1
#define DC2 0x12 // Device Control 2
#define DC3 0x13 // Device Control 3
#define DC4 0x14 // Device Control 4
#define NAK 0x15 // Negative Acknowledge
#define SYN 0x16 // Synchronous Idle
#define ETB 0x17 // End of Transmission Block
#define CAN 0x18 // Cancel
#define EM 0x19 // End of Medium
#define SUB 0x1A // Substitute
#define ESC 0x1B // Escape
#define FS 0x1C // File Separator
#define GS 0x1D // Group Separator
#define RS 0x1E // Record Separator
#define US 0x1F // Unit Separator
#define SP 0x20 // Space

#endif

lcd1602.h

#ifndef __LCD1602__
#define __LCD1602__

typedef bit BOOL;

void LCD_writeCmd(unsigned char cmd);  //写命令
void LCD_writeData(unsigned char dat);  //写数据
void LCD_writeLine(unsigned char *line);  //写行数据
void LCD_init();  //初始化
void delay(unsigned int z);  //粗略的延时器

#endif

lcd1602.c

#include <reg52.h>
#include "lcd1602.h"

#define LCD_CLEAR 0x01
#define LCD_Display_Mode 0X38

#define DISPLAY_OFF 0x08
#define DISPLAY_ON_NO_CURSOR 0x0c
#define DISPLAY_ON_WITH_CURSOR_NO_BLINK 0x0e
#define DISPLAY_ON_WITH_CURSOR_BLINK 0x0f

#define AUTO_BACK_STEP 0x04
#define AUTO_NEXT_STEP 0x06
#define AUTO_DISPLAY_MOVE_LEFT 0x07
#define AUTO_DISPLAY_MOVE_RIGHT 0x05

#define ALL_MOVE_LEFT 0x18
#define ALL_MOVE_RIGHT 0x1c
#define CURSOR_MOVE_LEFT 0x10
#define CURSOR_MOVE_RIGHT 0x14

#define FIRST_ROW 0x80
#define SECOND_ROW FIRST_ROW+0x40

sbit enable = P0^5;
sbit RS = P0^7;
sbit RW = P0^6;

void delay(unsigned int z)
{
    unsigned int x,y;
    for(x=z;x>0;x--)
        for(y=220;y>0;y--);
}

void LCD_writeCmd(unsigned char cmd){
    RS = 0;
    P2 = cmd;
    delay(5);
    enable = 1;
    delay(5);
    enable = 0;
}

void LCD_writeData(unsigned char dat)
{
    RS = 1;
    P2 = dat;
    delay(5);
    enable = 1;
    delay(5);
    enable = 0;
}

void LCD_writeLine(unsigned char *line){
    unsigned char i=0;
    BOOL flag = 0;
    LCD_writeCmd(LCD_CLEAR);  //每次送来信息都清屏,可以一直刷新显示送来的信息。
    LCD_writeCmd(FIRST_ROW);
    while(line[i] != '\0'){
        LCD_writeData(line[i++]);
        if(i>15 && flag == 0){
            LCD_writeCmd(SECOND_ROW);
            flag = 1;
        }
        delay(5);
    }
}

void LCD_init()
{
    RW = 0;
    enable = 0;
    LCD_writeCmd(LCD_Display_Mode);
    LCD_writeCmd(DISPLAY_ON_NO_CURSOR);
    LCD_writeCmd(AUTO_NEXT_STEP);
    LCD_writeCmd(LCD_CLEAR);
}

stc52ser.h

#ifndef __STC52_SER__
#define __STC52_SER__

extern void (*SerialPort_Event_ByteReceived)(unsigned char byte);  //事件:串口接收到字节

void SerialPort_Init_Low();  //初始化为11.0592MHz下的9600波特率
void SerialPort_Init_High();  //初始化为22.1184下的115200波特率
void SerialPort_SendByte(unsigned char byte);  //发送一个字节
void SerialPort_SendData(unsigned char* bytes);  //发送一组字节

#endif

stc52ser.c

#include <reg52.h>
#include "ASCIIS.h"
#include "stc52ser.h"

//Byte Received Event
void (*SerialPort_Event_ByteReceived)(unsigned char byte);

//initialize registers pertinent to serial port
void SerialPort_Init_Low(){
    //set and run Timer1
    //mode2: 8bit, auto reload initial value
    //9600bps and 11.0592MHz => 0xfd(initial value)
    TMOD = 0x20;
    TH1 = 0xfd;
    TL1 = 0xfd;
    TR1 = 1;
    
    //set serial port configuration and enable receive
    //mode1: asyc 10bit(8 data bit), alterable baud rate
    SM0 = 0;
    SM1 = 1;
    REN = 1;
    
    //set interruption
    //enable all and serial port interruption
    EA = 1;
    ES = 1;
}

//initialize registers pertinent to serial port for esp8266
void SerialPort_Init_High(){
    //SMOD = 1
    PCON |= 0x80;
    
    //set and run Timer1
    //mode2: 8bit, auto reload initial value
    //115200bps and 22.1184MHz => 0xfd(initial value)
    TMOD = 0x20;
    TH1 = 0xff;
    TL1 = 0xff;
    TR1 = 1;
    
    //set serial port configuration and enable receive
    //mode1: asyc 10bit(8 data bit), alterable baud rate
    SM0 = 0;
    SM1 = 1;
    REN = 1;
    
    //set interruption
    //enable all and serial port interruption
    EA = 1;
    ES = 1;
}

//Send a byte
void SerialPort_SendByte(unsigned char byte){
    ES = 0;
    SBUF = byte;
    while(!TI);
    // transmit interrupt
    TI = 0;
    ES = 1;
}

//Send a data of byte sequence end by 'EOT'
void SerialPort_SendData(unsigned char* bytes){
    int i = 0;
    while(bytes[i] != EOT){
        SerialPort_SendByte(bytes[i]);
        i++;
    }
}


//Occured when byte received
void receivedInterruped() interrupt 4 {
    TR0 = 0;
    (*SerialPort_Event_ByteReceived)(SBUF);
    while(!RI);
    RI = 0;
}

简单说一下,这里留了两个初始化函数,SerialPort_Init_Low()用于11.0592MHz下的9600波特率,SerialPort_Init_High()用于22.1184MHz下的115200波特率(此例用这个)。这样写只是为了以后可以重用(软工狗的矫情)。

esp8266.h

#ifndef __ESP8266__
#define __ESP8266__

extern void (*ESP01_Event_WifiConnected)();    //事件:Wi-Fi已连接
extern void (*ESP01_Event_IpGot)();    //事件:IP地址已获得
extern void (*ESP01_Event_TcpServerConnected)();    //事件:已连接到TCP服务器
extern void (*ESP01_Event_MsgReceived)(unsigned char* head);    //事件:已获得消息,head为消息数组头

void ESP01_Init();  //无线模块初始化
void ESP01_ConnectToTCPServer();  //连接TCP服务器

#endif

esp8266.c

#include <reg52.h>
#include "stc52ser.h"
#include "ASCIIS.h"
#include "esp8266.h"

#define BUFFER_MAX_SIZE 99  //缓冲区大小
unsigned char buffer[BUFFER_MAX_SIZE];  //缓冲区:用于存放从ESP8266接收来的各种信息

//连接到TCP服务器的指令:AT+CIPSTART="TCP","192.168.1.110",1234。后面的CR和NL是AT指令的固定结尾,EOT用于SerialPort_SendData发送时识别结尾。
code unsigned char cmd_connectToTCPServer[] = {0x41, 0x54, 0x2B, 0x43, 0x49, 0x50, 0x53, 0x54, 0x41, 0x52, 0x54, 0x3D, 0x22, 0x54, 0x43, 0x50, 0x22, 0x2C, 0x22, 0x31, 0x39, 0x32, 0x2E, 0x31, 0x36, 0x38, 0x2E, 0x31, 0x2E, 0x31, 0x31, 0x30, 0x22, 0x2C, 0x31, 0x32, 0x33, 0x34, CR, NL, EOT};
int counter = 0;    //用于ESP8266的执行步骤计数
int writeIndex = 0;    //缓冲区写索引

void (*ESP01_Event_WifiConnected)();
void (*ESP01_Event_IpGot)();
void (*ESP01_Event_TcpServerConnected)();
void (*ESP01_Event_MsgReceived)(unsigned char* head);

//注意:下面代码推荐从后往前看,从注释标"1. "处开始。

void prepareForData(unsigned char byte);    //因为第四步和第三步会相互调用,所以这里只是做了个声明(C语言的矫情点)。

//4. 将信息插入到缓冲区并送给单片机。
void insertDataIntoBuffer(unsigned char byte){
    if(byte == '\\'){
        //检测到'\'后,将信息送出到单片机
        buffer[writeIndex] = '\0';
        (*ESP01_Event_MsgReceived)(buffer);
        SerialPort_Event_ByteReceived = &prepareForData;    //回到第三步,准备接收下一条信息
        writeIndex = 0;
        return;
    }
    buffer[writeIndex++] = byte;
}

//3. 准备信息:这里是过度步骤,前面可以观察到,ESP8266在接收发来的信息时是有个头的,这里的作用就是去头。
void prepareForData(unsigned char byte){
    if(byte == ':'){
        SerialPort_Event_ByteReceived = &insertDataIntoBuffer;
        writeIndex = 0;
    }
}

//识别回馈指令:用于识别接收到的是WIFI CONNECTED(连上热点)还是WIFI IP GOT(获得IP)还是CONNECT(连上TCP服务器)
void parseCmd(){
    switch(counter){
        case 1:
            if(buffer[0] == 'W' && buffer[5] == 'C'){
                (*ESP01_Event_WifiConnected)();
                counter += 1;
            }
            break;
        case 2:
            if(buffer[0] == 'W' && buffer[5] == 'G'){
                (*ESP01_Event_IpGot)();
                counter += 1;
            }
            break;
        case 3:
            if(buffer[0] == 'A' && buffer[3] == 'C')
                counter += 1;
            break;
        case 4:
            if(buffer[0] == 'C' && buffer[3] == 'N' && buffer[6] == 'T'){
                (*ESP01_Event_TcpServerConnected)();
                SerialPort_Event_ByteReceived = &prepareForData;  //连接到TCP服务器后,进入第三步。
            }
    }
}

//2. 这里开始向缓冲区存储信息,用于识别。
void insertBuffer(unsigned char byte){
    if(byte == NL){
        //收到尾(NL)后,将缓冲区的回馈信息送去识别
        parseCmd();
        writeIndex = 0;
        return;
    }
    buffer[writeIndex++] = byte;
}

//1. 接收头:头是无用信息,但我们要通过头里面的一些字符,推算出什么时候到达第二步(WIFI CONNECTED)
void headerReceived(unsigned char byte){
    if(byte == NL){
        //头内有5个NL,只要数够5个,下一个就是第二步的内容了。
        if(++counter == 5){
            SerialPort_Event_ByteReceived = &insertBuffer;  //跳到第二步
            counter = 1;
        }
    }
}

//同.h中的声明
void ESP01_Init(){
    SerialPort_Init_High();
    SerialPort_Event_ByteReceived = &headerReceived;  //事件注册
}

//同.h中的声明
void ESP01_ConnectToTCPServer(){
    SerialPort_SendData(cmd_connectToTCPServer);
}

main.c

#include <reg52.h>
#include "lcd1602.h"
#include "esp8266.h"

//连接到Wi-Fi后,亮第一个灯
void EventHandler_WifiConnected(){
    P1 &= 0xFE;
}

//获得IP后,亮第二个灯
void EventHandler_IpGot(){
    P1 &= 0xFD;
    ESP01_ConnectToTCPServer();
}

//连接到TCP服务器后,亮第三个灯
void EventHandler_TcpServerConnected(){
    P1 &= 0xFB;
}

//将ESP8266送来的信息,送去LCD显示。
void EventHandler_MsgReceived(unsigned char* head){
    LCD_writeLine(head);
}

//初始化
void init(){
    ESP01_Event_WifiConnected = &EventHandler_WifiConnected;  //事件注册
    ESP01_Event_IpGot = &EventHandler_IpGot;  //事件注册
    ESP01_Event_TcpServerConnected = &EventHandler_TcpServerConnected;  //事件注册
    ESP01_Event_MsgReceived = &EventHandler_MsgReceived;  //事件注册
    ESP01_Init();
    LCD_init();
}

void main(){
    init();
    while(1);
}

效果

开始前请确定在同一个网络下,并且服务端已开启。

初始化效果

我只是把单片机连到移动电源上了。

服务端
为了能让1602第一行显示Hello,第二行显示World,中间可以留了11个空格。 客户端

结语

猴!到这里这个程序就算完成了。这个比那些用手机控制开关灯要复杂一些。所以你只要掌握了这个例子,那些都不在话下了。
扯单一周目BOSS正式刷完,这个系列的文章将会暂告一段落,因为接下来笔者又要去考试了,还有什么噼里啪啦科三学车,烦。消失一小阵子后我会再次诈尸的!

恭喜你获得一周目BOSS神装:物联网神技!

上一篇下一篇

猜你喜欢

热点阅读