wifi控制舵机
一:简介
通过ESP8266模块进行通信
一个ESP8266模块作为服务器:创建TCP服务器
一个ESP8266模块作为客户端:连接TCP服务器
通过客户端将数据传递给服务器,服务器进行解析,解析是否有客户端连接,解析传递的角度数据,再调用这个角度数据,进行控制舵机。
客户端就是通过AD转换读取电位器的数据,再进行一阶滤波,转换为角度,动态步长平滑控制算法,最后调整当前角度,将得到的数据以“@150.0#*”这种数据格式发送给服务器。
二:服务器
ESP8266 工作在AP 模式 + TCP 服务器模式,实现三大核心功能:
- ESP8266 服务器初始化:创建 WiFi 热点、启动 TCP 服务器(端口 8080),支持多客户端连接;
- 数据解析与舵机控制:通过串口中断解析客户端发送的
@角度值#*
格式数据,转换为浮点数后控制舵机转动; - 状态显示与指令下发:OLED 显示客户端连接 / 断开状态、当前舵机角度;按键触发向指定客户端发送字符
1
(触发客户端上传数据)。
软件流程:
ESP8266TCP通信服务器模块:
ESP8266 通过 AT 指令配置工作模式。关键步骤:
- 发送
AT\r\n
测试模块是否正常(响应OK
); - 配置为 AP 模式(
AT+CWMODE=2
),让客户端直接连接 ESP8266 的热点; - 设置 AP 热点参数(名称
ESP8266
、密码12345678
、信道 5、加密方式 WPA2-PSK); - 启用多连接模式(
AT+CIPMUX=1
),支持最多 4 个客户端连接; - 启动 TCP 服务器(
AT+CIPSERVER=1,8080
),端口 8080。
#define BUFFER_SIZE 100
char receiveBuffer[BUFFER_SIZE]; // 串口接收缓冲区
uint8_t receiveIndex = 0, copyreceive = 0;/*** 发送AT指令并等待指定响应*/
void ESP8266_SendAT(char *p, char *q) {USART_SendString(USART1, p); // 发送AT指令uint32_t timeout = 0; // 超时计数器(单位:10us)while (1) {// 条件1:收到期望响应(取缓冲区最后3个字符匹配,避免前导字符干扰)// 条件2:超时(300000 * 10us = 3s,超过则视为配置失败)if ((receiveBuffer[copyreceive - 3] == *q && receiveBuffer[copyreceive - 2] == *(q + 1)) || timeout > 300000) {break;}timeout++;Delay_us(10); // 微秒级延时,保证响应检测精度}memset(receiveBuffer, 0, BUFFER_SIZE); // 清空缓冲区,避免下次解析干扰
}/*** ESP8266 TCP服务器初始化(完整流程)*/
void ESP8266_Init() {ESP8266_SendAT("AT\r\n", "OK"); // 1. 测试模块通信ESP8266_SendAT("AT+CWMODE=2\r\n", "OK"); // 2. 配置为AP模式// 3. 配置AP热点(名称:ESP8266,密码:12345678,信道5,加密3=WPA2-PSK)ESP8266_SendAT("AT+CWSAP=\"ESP8266\",\"12345678\",5,3\r\n", "OK");ESP8266_SendAT("AT+CIPMUX=1\r\n", "OK"); // 4. 启用多连接模式ESP8266_SendAT("AT+CIPSERVER=1,8080\r\n", "OK"); // 5. 启动TCP服务器(端口8080)
}
串口中断与角度数据解析模块:
客户端发送的角度数据格式为@xx.x#*
(如@90.5#*
),需解决两个核心问题:
- 数据帧边界识别:区分有效数据(
@
开头、*
结尾)与无效数据(如 ESP8266 的连接状态提示); - 容错处理:防止数据溢出(如角度值过长)、格式错误(如缺少
#
)导致解析失败。
通过枚举定义 4 个状态,按 “起始符→数值→分隔符→结束符” 的顺序解析数据,流程如下:
- STATE_WAIT_AT:等待起始符
@
,收到后清空角度缓存,切换到接收数值状态; - STATE_RECEIVE_VALUE:接收数字(0-9)或小数点(.),遇到
#
则切换到等待结束符状态; - STATE_WAIT_STAR:等待结束符
*
,收到后保存角度值,触发 OLED 显示与舵机控制; - 任何状态下遇到无效字符,立即重置为
STATE_WAIT_AT。
// 角度解析状态枚举
typedef enum {STATE_WAIT_AT, // 等待起始符'@'STATE_RECEIVE_VALUE, // 接收角度值(数字/小数点)STATE_WAIT_STAR // 等待结束符'*'
} Angle_Receive_State;Angle_Receive_State angle_recv_state = STATE_WAIT_AT; // 初始状态
char angle_buf[10] = {0}; // 角度缓存(最多存9个字符,避免溢出)
uint8_t angle_idx = 0; // 角度缓存索引
char latest_angle[20] = {0}; // 存储解析后的完整角度字符串(如"103.3")
char show = 0; // 显示触发标志(1=连接状态,2=角度数据)/*** USART1中断服务函数:解析角度数据+处理连接状态*/
void USART1_IRQHandler(void) {char receivedChar = 0;if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {receivedChar = USART_ReceiveData(USART1); // 读取接收的1个字符USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除中断标志// 核心:角度数据解析状态机switch (angle_recv_state) {case STATE_WAIT_AT:// 收到'@',开始接收角度值if (receivedChar == '@') {angle_recv_state = STATE_RECEIVE_VALUE;memset(angle_buf, 0, sizeof(angle_buf)); // 清空缓存angle_idx = 0;}break;case STATE_RECEIVE_VALUE:// 接收有效字符(数字或小数点)if ((receivedChar >= '0' && receivedChar <= '9') || receivedChar == '.') {if (angle_idx < sizeof(angle_buf) - 1) { // 预留1个字节存'\0',防止溢出angle_buf[angle_idx++] = receivedChar;} else {// 缓存溢出,重置状态angle_recv_state = STATE_WAIT_AT;memset(angle_buf, 0, sizeof(angle_buf));angle_idx = 0;}}// 收到'#',说明数值部分结束,等待'*'else if (receivedChar == '#') {angle_recv_state = STATE_WAIT_STAR;}// 收到无效字符,重置else {angle_recv_state = STATE_WAIT_AT;memset(angle_buf, 0, sizeof(angle_buf));angle_idx = 0;}break;case STATE_WAIT_STAR:// 收到'*',解析完成if (receivedChar == '*') {strcpy(latest_angle, angle_buf); // 保存角度值show = 2; // 触发OLED显示角度+舵机控制}// 无论是否收到'*',都重置为等待下一个帧angle_recv_state = STATE_WAIT_AT;break;default:angle_recv_state = STATE_WAIT_AT;break;}// 处理客户端连接/断开状态(仅在等待'@'时处理,避免与角度解析冲突)if (angle_recv_state == STATE_WAIT_AT) {// 存储非换行符的字符(ESP8266的状态提示以'\n'结尾)if (receivedChar != '\n' && receiveIndex < BUFFER_SIZE - 1) {receiveBuffer[receiveIndex++] = receivedChar;} else {receiveBuffer[receiveIndex] = '\0'; // 字符串结尾加'\0'copyreceive = receiveIndex;receiveIndex = 0;// 检测连接(CONNECT)或断开(DISCONNECT)指令if (strstr(receiveBuffer, "CONNECT") != NULL || strstr(receiveBuffer, "DISCONNECT") != NULL) {show = 1; // 触发OLED显示连接状态}}}}
}
完整代码:
#include "stm32f10x.h"
#include "OLED.h"
#include "Delay.h"
#include "usart.h"
#include "string.h"
#include "EXTI_KEY.h"
#include "stdio.h"
#include "servo.h"
#include "stdlib.h"#define BUFFER_SIZE 100 // 缓冲区大小
char receiveBuffer[BUFFER_SIZE]; // 原始数据缓冲区
uint8_t receiveIndex = 0, copyreceive = 0; // 接收索引
char receivedChar = 0;char server_ok = 0, show = 0;
char kehu = 0, clear = 0;
static uint8_t last_state = 0xFF;
char latest_angle[20] = {0}; // 存储完整角度数据(如"103.3")// -------------------------- 角度数据接收状态与缓存 --------------------------
typedef enum {STATE_WAIT_AT, // 等待起始符'@'STATE_RECEIVE_VALUE, // 接收角度值(数字和小数点)STATE_WAIT_HASH, // 等待分隔符'#'STATE_WAIT_STAR // 等待结束符'*'
} Angle_Receive_State;Angle_Receive_State angle_recv_state = STATE_WAIT_AT; // 初始状态
char angle_buf[10] = {0}; // 缓存角度数值
uint8_t angle_idx = 0; // 角度缓存索引
// -----------------------------------------------------------------------------/*** 发送AT指令并等待响应(增加超时机制,防止死等)*/
void ESP8266_SendAT(char *p, char *q) {USART_SendString(USART1, p);uint32_t timeout = 0;while (1) {// 等待响应或超时(约3秒)if ((receiveBuffer[copyreceive - 3] == *q && receiveBuffer[copyreceive - 2] == *(q + 1)) || timeout > 300000) {break;}timeout++;Delay_us(10);}memset(receiveBuffer, 0, BUFFER_SIZE); // 清空缓冲区(用0而非32,避免乱码)
}/*** 初始化ESP8266为TCP服务器*/
void ESP8266_Init() {ESP8266_SendAT("AT\r\n", "OK"); // 测试模块ESP8266_SendAT("AT+CWMODE=2\r\n", "OK"); // AP模式ESP8266_SendAT("AT+CWSAP=\"ESP8266\",\"12345678\",5,3\r\n", "OK"); // AP配置ESP8266_SendAT("AT+CIPMUX=1\r\n", "OK"); // 多连接模式ESP8266_SendAT("AT+CIPSERVER=1,8080\r\n", "OK"); // 启动服务器
}int main() {char *customer;float target_angle = 0.0f;// 硬件初始化EXTI_PE_Configuration();OLED_Init();USART1_Config();Servo_Init();ESP8266_Init();OLED_Clear();OLED_ShowString(1, 1, "Server Ready");while (1) {// 模式1:显示客户端连接状态if (show == 1) {OLED_ShowString(1, 1, "Wait Connect");customer = receiveBuffer + 2; // 跳过前缀if (strncmp(customer, "CONNECT", 7) == 0) {uint8_t current_customer = *(customer - 2) - '0';if (last_state != current_customer) {OLED_Clear();OLED_ShowString(1, 1, "Connected");switch (current_customer) {case 0: OLED_ShowString(2, 1, "Client 0"); break;case 1: OLED_ShowString(2, 1, "Client 1"); break;case 2: OLED_ShowString(2, 1, "Client 2"); break;case 3: OLED_ShowString(2, 1, "Client 3"); break;default: OLED_ShowString(2, 1, "Unknown"); break;}OLED_ShowString(3, 1, "Status: On");}show = 0;} else if (strncmp(customer, "DISCONNECT", 10) == 0) {OLED_Clear();OLED_ShowString(1, 1, "Disconnected");OLED_ShowString(2, 1, "No Client");OLED_ShowString(3, 1, "Status: Off");last_state = 0xFF;show = 0;} else {OLED_ShowString(1, 1, "Unknown Resp");OLED_ShowString(2, 1, receiveBuffer);show = 0;}}// 模式2:显示并处理角度数据if (show == 2) {OLED_Clear();OLED_ShowString(1, 1, "Angle Data");// 显示当前角度并控制舵机OLED_ShowString(2, 1, "Current: ");OLED_ShowString(2, 9, latest_angle);target_angle = atof(latest_angle); // 字符串转浮点数if (target_angle >= 0 && target_angle <= 180) {Servo_SetAngle(target_angle); // 控制舵机OLED_ShowString(3, 1, "Servo OK");} else {OLED_ShowString(3, 1, "Angle Err");}show = 0;}}
}/*** 串口1中断服务函数:解析@xx.x#*格式数据*/
void USART1_IRQHandler(void) {if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {receivedChar = USART_ReceiveData(USART1);USART_ClearITPendingBit(USART1, USART_IT_RXNE);// 角度数据解析状态机(核心逻辑)switch (angle_recv_state) {case STATE_WAIT_AT:// 等待起始符'@',收到后切换状态if (receivedChar == '@') {angle_recv_state = STATE_RECEIVE_VALUE;memset(angle_buf, 0, sizeof(angle_buf)); // 清空角度缓存angle_idx = 0;}break;case STATE_RECEIVE_VALUE:// 接收角度值(数字0-9或小数点.)if ((receivedChar >= '0' && receivedChar <= '9') || receivedChar == '.') {if (angle_idx < sizeof(angle_buf) - 1) { // 防止溢出angle_buf[angle_idx++] = receivedChar;} else {// 缓存溢出,重置状态angle_recv_state = STATE_WAIT_AT;memset(angle_buf, 0, sizeof(angle_buf));angle_idx = 0;}}// 收到'#'说明数值部分结束,切换状态else if (receivedChar == '#') {angle_recv_state = STATE_WAIT_STAR;}// 收到其他字符,视为无效数据else {angle_recv_state = STATE_WAIT_AT;memset(angle_buf, 0, sizeof(angle_buf));angle_idx = 0;}break;case STATE_WAIT_HASH:// 理论上不会进入此状态,容错处理angle_recv_state = STATE_WAIT_AT;break;case STATE_WAIT_STAR:// 收到'*'说明数据结束,处理完整角度值if (receivedChar == '*') {strcpy(latest_angle, angle_buf); // 保存角度值show = 2; // 触发显示和控制}// 无论是否收到'*',都重置为等待下一个'@'angle_recv_state = STATE_WAIT_AT;break;default:angle_recv_state = STATE_WAIT_AT;break;}// 处理客户端连接状态(仅在等待'@'时处理,避免冲突)if (angle_recv_state == STATE_WAIT_AT) {if (receivedChar != '\n' && receiveIndex < BUFFER_SIZE - 1) {receiveBuffer[receiveIndex++] = receivedChar;} else {receiveBuffer[receiveIndex] = '\0';copyreceive = receiveIndex;receiveIndex = 0;// 检测连接状态指令if (strstr(receiveBuffer, "CONNECT") != NULL || strstr(receiveBuffer, "DISCONNECT") != NULL) {show = 1;}}}}
}/*** 按键中断:向客户端发送数据*/
void EXTI0_IRQHandler(void) {if (EXTI_GetITStatus(EXTI_Line0) != RESET) {USART_SendString(USART1, "AT+CIPSEND=0,1\r\n");Delay_ms(100);USART_SendData(USART1, '1');Delay_ms(100);EXTI_ClearITPendingBit(EXTI_Line0);}
}
三:客户端
软件流程:
一阶滤波:
AD 模块采集的原始数据会受电路噪声、电源波动影响,导致舵机角度频繁抖动,因此需要滤波处理,同时保证数据响应速度(避免滤波后延迟过大)。
// 一阶滤波函数:平衡响应速度与滤波效果
uint16_t AD_Filter(uint16_t rawValue)
{static uint16_t filteredAD = 0; // 静态变量:保存上一次滤波结果float a = 0.6f; // 滤波系数(0<a<1,a越大响应越快,滤波效果越差)// 滤波公式:当前滤波值 = a*原始值 + (1-a)*上一次滤波值filteredAD = (uint16_t)(a * rawValue + (1 - a) * filteredAD);return filteredAD;
}
选用0.6,就是为了响应迅速
动态步长平滑控制算法:
// 全局变量定义
float TargetAngle; // 目标角度(AD值转换后)
float CurrentAngle; // 当前舵机角度
float AngleStep; // 动态步长
float MinStep = 0.5f; // 最小步长(角度差<10°时使用,避免抖动)
float MaxStep = 10.0f;// 最大步长(角度差>90°时使用,加快响应)// 动态步长计算与角度更新
float diff = TargetAngle - CurrentAngle;
float angleDiff = fabs(diff); // 角度差(取绝对值)// 1. 根据角度差确定步长
if (angleDiff < 10.0f)
{AngleStep = MinStep; // 近距离:小步慢走,避免抖动
}
else if (angleDiff > 90.0f)
{AngleStep = MaxStep; // 远距离:大步快走,加快响应
}
else
{// 中间距离:线性插值(步长随角度差递增)AngleStep = MinStep + (MaxStep - MinStep) * (angleDiff - 10.0f) / 80.0f;
}// 2. 更新当前角度(逼近目标角度)
if (TargetAngle > CurrentAngle + AngleStep)
{CurrentAngle += AngleStep;
}
else if (TargetAngle < CurrentAngle - AngleStep)
{CurrentAngle -= AngleStep;
}
else
{CurrentAngle = TargetAngle; // 误差小于步长时,直接对齐目标
}
当电位器缓慢转动(角度差 < 10°):舵机以 0.5°/ 次的步长转动,无明显抖动;
当电位器快速转动(角度差 > 90°):舵机以 10°/ 次的步长转动,1 秒内可从 0° 转到 180°;
当角度差在 10°-90° 之间:步长线性递增,兼顾速度与平滑度。
ESP8266 TCP 通信模块:
ESP8266 工作在透传模式:MCU 通过 USART1 向 ESP8266 发送数据,ESP8266 直接转发到 TCP 服务器;反之,服务器发送的指令也通过 ESP8266 透传给 MCU。
数据协议:
MCU 发送角度数据:格式为@角度值#*
(如@90.0#*
表示当前角度 90.0°),便于服务器解析;
服务器发送指令:仅需发送字符1
,MCU 收到后触发角度发送(避免频繁发送浪费带宽)。
// 1. 发送角度数据到服务器(按需发送,避免冗余)
void ESP_SendData(float current)
{char sendBuf[100];static float LastSentAngle = -1.0f; // 上一次发送的角度(初始为无效值)// 仅当角度变化≥0.5°或首次发送时,才发送数据(减少带宽占用)if (fabs(current - LastSentAngle) >= 0.5f || LastSentAngle < 0){sprintf(sendBuf, "@%.1f#*", current); // 格式化数据USART_SendString(USART1, sendBuf); // 发送到ESP8266LastSentAngle = current; // 更新上一次发送的角度}
}// 2. 接收服务器指令(需在USART1中断服务函数中实现)
void USART1_IRQHandler(void)
{static char recvBuf[10];static uint8_t recvLen = 0;if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET){char data = USART_ReceiveData(USART1); // 读取接收数据// 假设指令为单个字符(如'1'),可根据实际需求扩展if (data == '1'){instruction = '1'; // 标记指令:触发角度发送show = 1; // 标记OLED需要刷新指令}USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除中断标志}
}
完整代码:
#include "ESPClient.h"
#include "ad.h"
#include <stdio.h>
#include <math.h>uint16_t ADvalue;
uint16_t ADValueFiltered; // 滤波后的AD值
float LastSentAngle = -1.0f; //无效的值
float TargetAngle; // 目标角度(由AD值计算得出)
float CurrentAngle; // 当前舵机角度(用于平滑过渡)
float AngleStep ; // 角度变化动态步长(控制转动平滑度)
float MinStep = 0.5f; // 最小步长(小幅调节时使用)
float MaxStep = 10.0f; // 最大步长(大幅调节时使用)/*
ready 0 ESP8266Init没有初始化时为0,用来接收ok。
ready 1 ESP8266Init初始化完成后为1,用来接收服务器的数据。
show 0 无新指令需显示 / 已完成显示
show 1 当收到服务器的数据时,show变为1,进行刷新数据
*/
char i=0,ready=0,show=0;
uint16_t instruction=0;//接收服务器发送的指令//一阶滤波
uint16_t AD_Filter(uint16_t rawValue)
{static uint16_t filteredAD=0;float a=0.6;//a=0.6 响应较快,同时能过滤大部分高频噪声,平衡效果好。//α×当前值 + (1-α)×上次结果filteredAD=(uint16_t)(a*rawValue+(1-a)*filteredAD);return filteredAD;
}void ESP_SendData(float current)
{char sendBuf[100]; // 数据缓冲区(足够存储各类数据)//大于0.5°,或者错误时,进行发送if (fabs(current - LastSentAngle) >= 0.5f || LastSentAngle < 0){sprintf(sendBuf, "@%.1f#*", current); LastSentAngle = current; // 新增:更新上一次发送的角度// 通过USART1发送到ESP8266,由ESP8266透传给客户端USART_SendString(USART1, sendBuf);}}int main()
{OLED_Init();USART1_Config();ESP8266_Init();ready=1;AD_Init();OLED_Clear();OLED_ShowString(1,1,"wait server");OLED_ShowString(2,1,"Angle:");OLED_ShowString(3,1,"AD:");while(1){ADvalue=AD_GetValue();ADValueFiltered=AD_Filter(ADvalue);//目标角度TargetAngle = (float)ADValueFiltered / 4095 * 180;//动态步长平滑控制算法float diff = TargetAngle - CurrentAngle;float angleDiff = diff > 0 ? diff : -diff;if (angleDiff < 10.0f) {AngleStep = MinStep;} else if (angleDiff > 90.0f) {AngleStep = MaxStep;} else {AngleStep = MinStep + (MaxStep - MinStep) * (angleDiff - 10.0f) / 80.0f;}//调整当前角度if(TargetAngle>CurrentAngle+AngleStep){CurrentAngle+=AngleStep;}else if(TargetAngle<CurrentAngle-AngleStep){CurrentAngle-=AngleStep;}else{CurrentAngle = TargetAngle;}// 实时显示AD值和角度(OLED部分优化)OLED_ShowNum(2, 7, (uint16_t)CurrentAngle, 3); // 显示舵机角度OLED_ShowNum(3, 4, ADValueFiltered, 4); // 显示滤波后的AD值//收到数据进行刷新if(show==1){OLED_ShowNum(4,1,instruction,2);show=0;}//客户端收到1,发送数据if(instruction=='1'){ESP_SendData(CurrentAngle); }}}