目錄
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(...) { ... }
- 處理邏輯如下
- 去除接收到的行的前後換行符。
- 判斷處理後的結果是否以 +IPD 開頭。
- 找到 : 後的 8 個字符,依次控制 LED1 ~ LED8 的開關狀態,1 爲開啓,0 爲關閉。
- 開關字符串例子
- +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 這種微控制器還是不想折騰這麼多功能了。