在之前的教程中,我们学习了 ESP6266 的原理,并动手写了驱动,实现了串口的通讯和 STA、AP、STA+AP 三种模式。本次我们就来教大家如何使用 ESP8266 控制灯。这是一个简单的示例,展示了如何将 WIFI 通信与硬件控制相结合,实现远程控制的功能。你也可以扩展这个示例,添加更多的指令和功能,以满足自己的需求。
本文首发 良许嵌入式网 :https://www.lxlinux.net/e/ ,欢迎关注!
本文所涉及的源码及安装包如下(由于平台限制,请点击以下链接阅读原文下载):
https://www.lxlinux.net/e/stm32/esp8266-control-led.html
如果你是嵌入式开发小白,那么建议你先读读下面几篇文章。
前期教程,没看过的小伙伴可以先看下。
作者简介 |
---|
大家好,我是良许,博客里所有的文章皆为我的原创。 下面是我的一些个人介绍,欢迎交个朋友: · 211工科硕士,国家奖学金获得者; · 深耕嵌入式11年,前世界500强外企高级嵌入式工程师; · 书籍《速学Linux作者》,机械工业出版社专家委员会成员; · 全网60W粉丝,博客分享大量原创成体系文章,全网阅读量累计超4000万; · 靠自媒体连续年入百万,靠自己买房买车。 |
我本科及硕士都是学机械,通过自学成功进入世界500强外企。我已经将自己的学习经验写成了一本电子书,超千人通过此书学习并转行成功。现在将这本电子书免费分享给大家,希望对你们有帮助:
电子书链接:https://www.lxlinux.net/1024.html
实现目标是我们有一个三色 LED 灯,电脑上使用网络调试助手向 WIFI 模块发送关键词「green」绿灯亮,再次发送「green」绿灯灭,黄灯和红灯的关键词是「yellow」、「red」,效果一样。
本教程使用的硬件如下:
ESP8266 | LED | STM32 | USB 转 TTL |
---|---|---|---|
3V3 | 3.3 | ||
RX | A2 | ||
TX | A3 | ||
GND | G | ||
R | A5 | ||
Y | A6 | ||
G | A7 | ||
GND | G | ||
A10 | TX | ||
A9 | RX | ||
G | GND |
烧录的时候接线如下表,如果不会烧录的话可以看我之前的文章【STM32下载程序的五种方法:https://www.lxlinux.net/e/stm32/five-ways-to-flash-program-to-stm32.html】。
ST-Link V2 | STM32 |
---|---|
SWCLK | SWCLK |
SWDIO | SWDIO |
GND | GND |
3.3V | 3V3 |
接好如下图:
LED 灯的代码简简单单,只要进行一下三个灯的初始化就行。
void led_init(void) { GPIO_InitTypeDef gpio_init_struct; LED1_GPIO_CLK_ENABLE(); /* LED1时钟使能 */ LED2_GPIO_CLK_ENABLE(); /* LED2时钟使能 */ LED3_GPIO_CLK_ENABLE(); /* LED3时钟使能 */ gpio_init_struct.Pin = LED1_GPIO_PIN; /* LED1引脚 */ gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */ gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */ gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */ HAL_GPIO_Init(LED1_GPIO_PORT, &gpio_init_struct); /* 初始化LED1引脚 */ gpio_init_struct.Pin = LED2_GPIO_PIN; /* LED2引脚 */ HAL_GPIO_Init(LED2_GPIO_PORT, &gpio_init_struct); /* 初始化LED2引脚 */ gpio_init_struct.Pin = LED3_GPIO_PIN; /* LED3引脚 */ HAL_GPIO_Init(LED3_GPIO_PORT, &gpio_init_struct); /* 初始化LED3引脚 */ LED1(0); /* 关闭 LED1 */ LED2(0); /* 关闭 LED2 */ LED3(0); /* 关闭 LED3 */ }
LED 的 .h文件:
#ifndef _LED_H #define _LED_H #include "sys.h" /******************************************************************************************/ /* 引脚 定义 */ #define LED1_GPIO_PORT GPIOA #define LED1_GPIO_PIN GPIO_PIN_7 #define LED1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */ #define LED2_GPIO_PORT GPIOA #define LED2_GPIO_PIN GPIO_PIN_6 #define LED2_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */ #define LED3_GPIO_PORT GPIOA #define LED3_GPIO_PIN GPIO_PIN_5 #define LED3_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PB口时钟使能 */ /******************************************************************************************/ /* LED端口定义 */ #define LED1(x) do{ x ? \ HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_SET) : \ HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_RESET); \ }while(0) #define LED2(x) do{ x ? \ HAL_GPIO_WritePin(LED2_GPIO_PORT, LED2_GPIO_PIN, GPIO_PIN_SET) : \ HAL_GPIO_WritePin(LED2_GPIO_PORT, LED2_GPIO_PIN, GPIO_PIN_RESET); \ }while(0) #define LED3(x) do{ x ? \ HAL_GPIO_WritePin(LED3_GPIO_PORT, LED3_GPIO_PIN, GPIO_PIN_SET) : \ HAL_GPIO_WritePin(LED3_GPIO_PORT, LED3_GPIO_PIN, GPIO_PIN_RESET); \ }while(0) /* LED取反定义 */ #define LED1_TOGGLE() do{ HAL_GPIO_TogglePin(LED1_GPIO_PORT, LED1_GPIO_PIN); }while(0) /* 翻转LED1 */ #define LED2_TOGGLE() do{ HAL_GPIO_TogglePin(LED2_GPIO_PORT, LED2_GPIO_PIN); }while(0) /* 翻转LED2 */ #define LED3_TOGGLE() do{ HAL_GPIO_TogglePin(LED3_GPIO_PORT, LED3_GPIO_PIN); }while(0) /* 翻转LED3 */ /******************************************************************************************/ /* 外部接口函数*/ void led_init(void); /* LED初始化 */ #endif
WIFI 串口通讯我们在【手把手教你玩转WIFI模块(原理+驱动):https://www.lxlinux.net/e/stm32/esp8266-tutorial.html】有详细教程,在这里就简单带过+浅浅复习下,没看过或者忘记了的小伙伴可以点击链接看看。
实现向串口发送数据,并等待返回值。ESP8266 的 TX 和 RX 定义在串口2。
g_uart_handle
,并调用 HAL_UART_Init
进行初始化。UART_HandleTypeDef g_uart_handle; void esp8266_uart_init(uint32_t baudrate) { g_uart_handle.Instance = ESP8266_UART_INTERFACE; /* ESP8266 UART */ g_uart_handle.Init.BaudRate = baudrate; /* 波特率 */ g_uart_handle.Init.WordLength = UART_WORDLENGTH_8B; /* 数据位 */ g_uart_handle.Init.StopBits = UART_STOPBITS_1; /* 停止位 */ g_uart_handle.Init.Parity = UART_PARITY_NONE; /* 校验位 */ g_uart_handle.Init.Mode = UART_MODE_TX_RX; /* 收发模式 */ g_uart_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */ g_uart_handle.Init.OverSampling = UART_OVERSAMPLING_16; /* 过采样 */ HAL_UART_Init(&g_uart_handle); /* 使能ESP8266 UART */ }
其中,ESP8266_UART_INTERFACE
是宏定义,指代的就是 USART2
。
传入参数 baudrate
可以定义该串口的波特率。
HAL_UART_MspInit
函数。注意最后一行,需要调用 __HAL_UART_ENABLE_IT
函数使能接收中断。
void HAL_UART_MspInit(UART_HandleTypeDef *huart) { GPIO_InitTypeDef gpio_init_struct; if (huart->Instance == USART_UX) /* 如果是串口1,进行串口1 MSP初始化 */ { .... // 省略串口1相关代码 .... } else if (huart->Instance == ESP8266_UART_INTERFACE) /* 如果是ESP8266 UART */ { ESP8266_UART_TX_GPIO_CLK_ENABLE(); /* 使能UART TX引脚时钟 */ ESP8266_UART_RX_GPIO_CLK_ENABLE(); /* 使能UART RX引脚时钟 */ ESP8266_UART_CLK_ENABLE(); /* 使能UART时钟 */ gpio_init_struct.Pin = ESP8266_UART_TX_GPIO_PIN; /* UART TX引脚 */ gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */ gpio_init_struct.Pull = GPIO_NOPULL; /* 无上下拉 */ gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */ HAL_GPIO_Init(ESP8266_UART_TX_GPIO_PORT, &gpio_init_struct); /* 初始化UART TX引脚 */ gpio_init_struct.Pin = ESP8266_UART_RX_GPIO_PIN; /* UART RX引脚 */ gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入 */ gpio_init_struct.Pull = GPIO_NOPULL; /* 无上下拉 */ gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */ HAL_GPIO_Init(ESP8266_UART_RX_GPIO_PORT, &gpio_init_struct); /* 初始化UART RX引脚 */ HAL_NVIC_SetPriority(ESP8266_UART_IRQn, 0, 0); /* 抢占优先级0,子优先级0 */ HAL_NVIC_EnableIRQ(ESP8266_UART_IRQn); /* 使能UART中断通道 */ __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE); /* 使能UART接收中断 */ } }
由于 ESP8266 通过串口返回的数据长度不固定,所以我们可以使用接收中断+超时判断的方法完成数据接收。具体方法可以参考下文:
【STM32串口接收不定长数据(接收中断+超时判断):https://www.lxlinux.net/e/stm32/stm32-usart-receive-data-using-rxne-time-out.html】
在串口中断服务函数里,我们可以将接收到的字符保存在接收缓冲区里 g_uart_rx_buf
,并定义一个变量 esp8266_cnt
计算总共收到了多少个字符。
void ESP8266_UART_IRQHandler(void) { uint8_t receive_data = 0; if(__HAL_UART_GET_FLAG(&g_uart_handle, UART_FLAG_RXNE) != RESET){ if(esp8266_cnt >= sizeof(g_uart_rx_buf)) esp8266_cnt = 0; //防止串口被刷爆 HAL_UART_Receive(&g_uart_handle, &receive_data, 1, 1000);//串口2接收1位数据 g_uart_rx_buf[esp8266_cnt++] = receive_data; } HAL_UART_IRQHandler(&g_uart_handle); }
假如一帧的数据接收完成了,那么 esp8266_cnt
变量的值应该维持不变。
我们通过 while 死循环不停计算当前收到多少个字符,当前统计的值计算在 esp8266_cnt
变量里,定义另一个变量 esp8266_cntPre
,用于记录上一次统计接收到的数据的长度,如果本次统计数据长度跟上一次一样的话就说明数据接收完成了。代码如下:
uint8_t esp8266_wait_receive(void) { if(esp8266_cnt == 0) //如果接收计数为0 则说明没有处于接收数据中,所以直接跳出,结束函数 return ESP8266_ERROR; if(esp8266_cnt == esp8266_cntPre) { //如果上一次的值和这次相同,则说明接收完毕 esp8266_cnt = 0; //清0接收计数 return ESP8266_EOK; //返回接收完成标志 } esp8266_cntPre = esp8266_cnt; //置为相同 return ESP8266_ERROR; //返回接收未完成标志 }
当然,接收到的数据使用完成之后,我们就应该清空接收缓冲区,并将计数器置 0 ,方便下一次接收。
void esp8266_clear(void) { memset(g_uart_rx_buf, 0, sizeof(g_uart_rx_buf)); esp8266_cnt = 0; }
接下来,就是最关键的一个函数了。我们使用这个函数通过串口向 ESP8266 发送一个字符串,并循环等待我们所期待得到的字符串。
在下面这个函数里,cmd
变量是我们向 ESP8266 发送的字符串,res
变量是我们期待得到的回复。
比如,我们向 ESP8266 发送 AT
这个字符串,那么 ESP8266 如果正常的话应该会回复 OK
。此时,cmd
就是 AT
,而 res
就是 OK
。
uint8_t esp8266_send_command(char *cmd, char *res) { uint8_t timeOut = 250; esp8266_clear(); HAL_UART_Transmit(&g_uart_handle, (unsigned char *)cmd, strlen((const char *)cmd), 100); while(timeOut--) { if(esp8266_wait_receive() == ESP8266_EOK) { //如果收到数据 if(strstr((const char *)g_uart_rx_buf, res) != NULL) //如果检索到关键词 return ESP8266_EOK; } delay_ms(10); } return ESP8266_ERROR; }
接下来,我们就可以使用 esp8266_send_command
发送 AT 指令并确定 ESP8266 回复是否正确。以代码方式实现各个 AT 指令,例子如下:
uint8_t esp8266_at_test(void) { return esp8266_send_command("AT\r\n", "OK"); } uint8_t esp8266_sw_reset(void) { return esp8266_send_command("AT+RST\r\n", "OK"); } uint8_t esp8266_set_mode(uint8_t mode) { switch (mode) { case ESP8266_STA_MODE: return esp8266_send_command("AT+CWMODE=1\r\n", "OK"); /* Station模式 */ case ESP8266_AP_MODE: return esp8266_send_command("AT+CWMODE=2\r\n", "OK"); /* AP模式 */ case ESP8266_STA_AP_MODE: return esp8266_send_command("AT+CWMODE=3\r\n", "OK"); /* AP+Station模式 */ default: return ESP8266_EINVAL; } }
由于 AT 指令有很多,这里只截取了其中的一部分,完整的可以参考我提供的代码。
STA 模式实现思路如下:
uint8_t esp8266_single_connection(void) { return esp8266_send_command("AT+CIPMUX=0\r\n", "OK"); } uint8_t esp8266_join_ap(char *ssid, char *pwd) { char cmd[64]; sprintf(cmd, "AT+CWJAP=\"%s\",\"%s\"\r\n", ssid, pwd); return esp8266_send_command(cmd, "WIFI GOT IP"); } uint8_t esp8266_get_ip(char *buf) { char *p_start; char *p_end; if (esp8266_send_command("AT+CIFSR\r\n", "STAIP") != ESP8266_EOK) return ESP8266_ERROR; p_start = strstr((const char *)g_uart_rx_buf, "\""); p_end = strstr(p_start + 1, "\""); *p_end = '\0'; sprintf(buf, "%s", p_start + 1); return ESP8266_EOK; } uint8_t esp8266_connect_tcp_server(char *server_ip, char *server_port) { char cmd[64]; sprintf(cmd, "AT+CIPSTART=\"TCP\",\"%s\",%s\r\n", server_ip, server_port); return esp8266_send_command(cmd, "CONNECT"); } uint8_t esp8266_enter_unvarnished(void) { uint8_t ret; ret = esp8266_send_command("AT+CIPMODE=1\r\n", "OK"); ret += esp8266_send_command("AT+CIPSEND\r\n", ">"); if (ret == ESP8266_EOK) return ESP8266_EOK; else return ESP8266_ERROR; } /** * @brief ESP8266初始化 * @param baudrate: ESP8266 UART通讯波特率 * @retval ESP8266_EOK : ESP8266初始化成功,函数执行成功 * ESP8266_ERROR: ESP8266初始化失败,函数执行失败 */ uint8_t esp8266_init(uint32_t baudrate) { char ip_buf[16]; esp8266_uart_init(baudrate); /* ESP8266 UART初始化 */ /* 让WIFI退出透传模式 */ while(esp8266_exit_unvarnished()) delay_ms(500); printf("1.AT\r\n"); while(esp8266_at_test()) delay_ms(500); printf("2.RST\r\n"); while(esp8266_sw_reset()) delay_ms(500); while(esp8266_disconnect_tcp_server()) delay_ms(500); printf("3.CWMODE\r\n"); while(esp8266_set_mode(ESP8266_STA_MODE)) delay_ms(500); printf("4.AT+CIPMUX\r\n"); //设置单路连接模式,透传只能使用此模式 while(esp8266_multi_connection()) delay_ms(500); printf("5.CWJAP\r\n"); //连接WIFI printf("%s\r\n",WIFI_SSID); while(esp8266_join_ap(WIFI_SSID, WIFI_PWD)) delay_ms(1000); printf("6.CIFSR\r\n"); while(esp8266_get_ip(ip_buf)) delay_ms(500); printf("ESP8266 IP: %s\r\n", ip_buf); printf("7.CIPSTART\r\n"); while(esp8266_connect_tcp_server(TCP_SERVER_IP, TCP_SERVER_PORT)) delay_ms(500); printf("8.CIPMODE\r\n"); while(esp8266_enter_unvarnished()) delay_ms(500); printf("ESP8266初始化完成\r\n"); return ESP8266_EOK; }
检测WIFI串口是否接收到 LED 关键词,如果有就反转 LED 灯状态。
void control_led() { if(strstr((const char *)bt_uart_rx_buf, "green") != NULL) //如果接收到关键词"green" LED1_TOGGLE(); // 翻转LED1 if(strstr((const char *)bt_uart_rx_buf, "yellow") != NULL) //如果接收到关键词"yellow" LED2_TOGGLE(); // 翻转LED2 if(strstr((const char *)bt_uart_rx_buf, "red") != NULL) //如果接收到关键词"red" LED3_TOGGLE(); // 翻转LED3 }
在 main 函数里,我们先调用 led_init()
函数和 esp8266_init(115200)
函数进行 LED 和ESP8266 的初始化,然后在原有通讯的基础上检测 LED 关键词。
int main(void) { HAL_Init(); /* 初始化HAL库 */ sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟,72M */ delay_init(72); /* 初始化延时函数 */ usart_init(115200); /* 波特率设为115200 */ led_init(); esp8266_init(115200); while(1) { printf("receive: %s\r\n", g_uart_rx_buf); control_led(); esp8266_clear(); esp8266_uart_printf("from client\r\n"); HAL_Delay(1000); } }
这个串口工具接收的是 MCU 串口 1 的数据,也就是 log 。WIFI接收到数据后,我们使用串口 1 打印到下面的串口助手里。
烧录代码,串口输出如下。
我们给 WIFI 模块发送数据「green」、「yellow」、「red」。
可以看到串口助手成功接收到了 「green」、「yellow」、「red」这些数据。
我们的三个小灯也打开了。(我的小绿灯不是很亮,用旧了,嘻嘻)
再次发送关键词即可关对应的灯。当然,一次发送 “green yellow red”,就可以控制三个小灯一起反转。
另外,想进大厂的同学,一定要好好学算法,这是面试必备的。这里准备了一份 BAT 大佬总结的 LeetCode 刷题宝典,很多人靠它们进了大厂。
刷题 | LeetCode算法刷题神器,看完 BAT 随你挑!
推荐阅读:
欢迎关注我的博客:良许嵌入式教程网,满满都是干货!