1.概述
本文介绍了一种基于 Android手机APP、STM32F103C8T6 微控制器和 ESP8266/HC-05 无线通信模块的 步进电机(28BYJ-48)控制系统。该系统支持 WiFi(TCP)和蓝牙(串口)双模通信,用户可通过手机APP实时调节电机的 转速 和 转向,同时 ESP8266 模块将电机运行数据通过 MQTT协议 上传至 公共服务器,实现远程监控。如果文中有不当的地方,还请各位大佬加以中指正,笔者一定会虚心求教。
大致架构:
A[Android App] -->|WiFi TCP| B[ESP8266] A -->|蓝牙串口| C[HC-05] B -->|UART| D[STM32F103C8T6] C -->|UART| D D -->|IIC|E[OLED显示屏] D --> F[ULN2003驱动] F --> G[28BYJ48步进电机] B -->|MQTT| H[公共服务器--broker.emqx.io]
2.硬件准备
2.1 STM32F10C8T6(主控芯片)
核心架构 | 存储资源 | 关键外设 |
ARM Cortex-M3,72MHz主频 | 64KB Flash + 20KB SRAM | USART(WiFi/蓝牙)、GPIO(驱动ULN2003和OLED)等 |
2.2 ESP8266-12E(WiFi模块)
通信协议 | 工作模式 | 数据传输 |
802.11 b/g/n(2.4GHz) | STA(连接热点)/AP(自建热点) | TCP/IP(与Android APP通信)、MQTT(数据上报云端)等 |
2.3 HC-05(蓝牙模块)
蓝牙版本 | 工作模式 | 通信方式 |
2.0+EDR(兼容Android/iOS) | 主从一体(默认从模式) | 串口透传 |
HC-05蓝牙模块可以通过上电前先按住蓝牙模块上的按键,接通电源,模块上的led灯进入慢闪后再松开按键,此时已经进入AT指令模式,可以进行AT指令设置,部分AT命令如下表所示。
常见AT命令 | 含义 |
AT+NAME = Yyuan | 设置蓝牙名称为Yyuan |
AT+ROLE=0/AT+ROLE=1 | 0为从模式 、1为主模式 |
AT+CMODE=0 | 蓝牙连接模式为任意地址连接模式 |
AT+PSWD=1234 | 蓝牙配对密码为1234 |
AT+UART=9600,0,0 | 蓝牙通信串口波特率为9600,停止位1位,无校验位 |
AT+RMAAD | 清空配对列表 |
注意:AT命令后面需要换行,然后点发送命令才有效,如果没有换行,发送命令,只会被当作是字符
2.4 OLED(显示屏)
接口 | 显示内容 |
IIC | 电机转速、接收控制命令次数、转向 |
2.5 ULN2003(电机驱动)
驱动类型 | 输入电压 | 输出电流 | 控制方式 |
达林顿晶体管阵列 | 5V(兼容STM32 GPIO) | 500mA/路(可驱动28BYJ-48) | 四相八拍/四相单四拍/四相双四拍 |
ULN2003芯片是大电流驱动阵列(放大电流来提高驱动能力),可直接用来驱动步进电机,其中芯片内有非门,会将输入电平倒置。详解:uln2003驱动电路_身在江湖的郭大侠的博客-CSDN博客_uln2003
控制方式中四相指步进电机有 4 组线圈(A、B、C、D 或 A+、A-、B+、B-),每组线圈为一个“相”,八拍指电机旋转一个齿距需要 8 个脉冲信号(即 8 步完成一个周期),四拍则是需4个脉冲信号一个周期。
IN1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
IN2 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
IN3 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 |
IN4 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
单四拍就是每次仅通电一相线圈。
IN1 | 1 | 0 | 0 | 0 |
IN2 | 0 | 1 | 0 | 0 |
IN3 | 0 | 0 | 1 | 0 |
IN4 | 0 | 0 | 0 | 1 |
双四拍则是每次通电两组线圈。
IN1 | 1 | 0 | 0 | 1 |
IN2 | 1 | 1 | 0 | 0 |
IN3 | 0 | 1 | 1 | 0 |
IN4 | 0 | 0 | 1 | 1 |
表中1-通电,0-断电,由于ULN2003公共端为高电平,输出端为低电平导通(输出端-->步进电机-->公共端),而ULN2003芯片会将输入电平倒置,则输入端为高电平。
2.6 28BYJ-48(步进电机)
型号 | 28BYJ-48 |
工作电压 | 5V |
直径 | 28mm |
减速比 | 1:64 |
驱动方式 | ULN2003 |
最小步进角度 | 5.625°/64(64步/圈,减速比1:64) |
其中减速比由减速齿轮决定,减速比大概是1:64
28BYJ-48步进电机内部实图与原理图右下图可知,转子由8对交替分布的N极与S极的永磁铁组成(共16块)。定子由两组线圈(上下两组),每组线圈两个线圈组成,共四个线圈。定子磁极依其形状称为爪极,由导磁钢板冲压成型,形成八个爪极,一个线圈有八个爪极,交错排列共32个。则使用四相四拍方式控制,步距角度:360°/(4*8)=11.25°则四相八拍,步距角度:360°/(8*8)=5.625°
步进电机原理详解可参考:
【STM32】步进电机及其驱动(ULN2003驱动28BYJ-48丨按键控制电机旋转)_Include everything的博客-CSDN博客_步进电机
3.通信协议
3.1 TCP/IP协议
TCP/IP(Transmission Control Protocol/Internet Protocol)是互联网最核心的通信协议族,定义了数据如何在网络中传输和路由。它不仅是现代互联网的基础,也是物联网(IoT)、云计算等技术的底层支撑。本项目通过TCP/IP协议进行手机app端与esp8266进行通信,以此来间接控制步进电机。
3.2 MQTT协议
MQTT(Message Queuing Telemetry Transport)是一种轻量级的 发布/订阅(Pub-Sub) 消息传输协议,专为 低带宽、高延迟或不可靠网络 环境设计,广泛应用于物联网(IoT)、远程传感器通信和移动设备场景。本项目通过MQTT协议将步进电机数据上传到公共服务器。
MQTT运行在TCP之上。MQTT协议属于应用层,TCP/IP协议属于传输层。
3.3 数据帧格式
数据头----'^'
数据尾----'$'
数据内容----Json格式字符串
显示上传数据格式如下
^{"Speed":"1.00RPM","Dir":"ClockWise"}$
控制命令格式如下
^{"Speed":"1","Dir":"1"}$
其中Json字符串中Speed键值为1--速度升1档,-1--降1档;Dir键值1--正转,0-反转
4.Android端实现
笔者在android端简单实现蓝牙通信、MQTT通信与TCP通信。其中蓝牙通信中添加bluetoothAdapter.disable()但是仍然通过app上按键关闭蓝牙,如果想要关掉可能需要手动关闭蓝牙。
源码如下:
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 兼容所有版本的蓝牙权限配置 -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" /> <!-- Android 11及以下 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- Android 12+ 新权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
MainActivity.java
package com.example.communication;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
Button mqttButton = findViewById(R.id.MQTT_button);
mqttButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this, MqttActivity.class);
startActivity(intent);
}
});
Button bluetoothButton = findViewById(R.id.Bluetooth_button);
bluetoothButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this, BluetoothActivity.class);
startActivity(intent);
}
});
Button tcpButton = findViewById(R.id.TCP_button);
tcpButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this, TcpClientActivity.class);
startActivity(intent);
}
});
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/main"
android:gravity="center"
android:padding="16dp"
tools:context=".MainActivity">
<!-- 第一个按钮 -->
<Button
android:id="@+id/MQTT_button"
android:layout_width="200dp"
android:layout_height="50dp"
android:text="MQTT连接"
android:textColor="#FFFFFF"
android:layout_marginBottom="16dp"/>
<!-- 第二个按钮 -->
<Button
android:id="@+id/TCP_button"
android:layout_width="200dp"
android:layout_height="50dp"
android:text="TCP连接"
android:textColor="#FFFFFF"
android:layout_marginBottom="16dp"/>
<!-- 第三个按钮 -->
<Button
android:id="@+id/Bluetooth_button"
android:layout_width="200dp"
android:layout_height="50dp"
android:text="Bluetooh连接"
android:textColor="#FFFFFF"
android:layout_marginBottom="16dp"/>
</LinearLayout>
MqttHandler.java
package com.example.communication;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
public class MqttHandler {
public interface MqttCallback {
void onConnected();
void onDisconnected();
void onMessageReceived(String topic, String message);
void onError(String error);
}
private MqttClient mqttClient;
private MqttCallback callback;
public void connect(String brokerUrl, String clientId, MqttCallback callback) {
this.callback = callback;
try {
mqttClient = new MqttClient(brokerUrl, clientId, new MemoryPersistence());
mqttClient.setCallback(new org.eclipse.paho.client.mqttv3.MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
callback.onDisconnected();
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
callback.onMessageReceived(topic, new String(message.getPayload()));
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
// 消息发布完成
}
});
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
mqttClient.connect(options);
callback.onConnected();
} catch (MqttException e) {
callback.onError(e.getMessage());
}
}
public void disconnect() {
if (mqttClient != null && mqttClient.isConnected()) {
try {
mqttClient.disconnect();
callback.onDisconnected();
} catch (MqttException e) {
callback.onError(e.getMessage());
}
}
}
public void subscribe(String topic) {
if (mqttClient != null && mqttClient.isConnected()) {
try {
mqttClient.subscribe(topic);
} catch (MqttException e) {
callback.onError(e.getMessage());
}
}
}
public void publish(String topic, String message) {
if (mqttClient != null && mqttClient.isConnected()) {
try {
MqttMessage mqttMessage = new MqttMessage(message.getBytes());
mqttClient.publish(topic, mqttMessage);
} catch (MqttException e) {
callback.onError(e.getMessage());
}
}
}
public boolean isConnected() {
return mqttClient != null && mqttClient.isConnected();
}
}
MqttActivity.java
package com.example.communication;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import org.json.JSONException;
public class MqttActivity extends AppCompatActivity {
private EditText etBrokerUrl, etClientId,etMqttMessage;
private AutoCompleteTextView etTopic;
private Button btnConnectMqtt, btnSubscribe, btnPublish;
private TextView tvMqttStatus, tvMqttMessages;
private MqttHandler mqttHandler;
private Button btnExpedite;
private Button btnSlowdown;
private Button btnClockwise;
private Button btnAnticlockwise;
ControlMotor controlMotor = new ControlMotor();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mqtt);
etBrokerUrl = findViewById(R.id.etBrokerUrl);
etClientId = findViewById(R.id.etClientId);
etTopic = findViewById(R.id.etTopic);
etMqttMessage = findViewById(R.id.etMqttMessage);
btnConnectMqtt = findViewById(R.id.btnConnectMqtt);
btnSubscribe = findViewById(R.id.btnSubscribe);
btnPublish = findViewById(R.id.btnPublish);
tvMqttStatus = findViewById(R.id.tvMqttStatus);
tvMqttMessages = findViewById(R.id.tvMqttMessages);
btnExpedite = findViewById(R.id.btnexpedite);
btnSlowdown = findViewById(R.id.btnslowdown);
btnClockwise = findViewById(R.id.btnclockwise);
btnAnticlockwise = findViewById(R.id.btnanticlockwise);
mqttHandler = new MqttHandler();
btnPublish.setEnabled(false);
btnSubscribe.setEnabled(false);
btnExpedite.setEnabled(false);
btnSlowdown.setEnabled(false);
btnClockwise.setEnabled(false);
btnAnticlockwise.setEnabled(false);
String[] suggestions = new String[]{"esp8266/status", "app/command"}; // 你的预设选项
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this,
android.R.layout.simple_dropdown_item_1line,
suggestions
);
etTopic.setAdapter(adapter);
btnExpedite.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
sendMessage(controlMotor.selectControl(1));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
});
btnSlowdown.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
sendMessage(controlMotor.selectControl(2));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
});
btnClockwise.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
sendMessage(controlMotor.selectControl(3));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
});
btnAnticlockwise.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
sendMessage(controlMotor.selectControl(4));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
});
btnConnectMqtt.setOnClickListener(v -> {
if (mqttHandler.isConnected()) {
mqttHandler.disconnect();
btnConnectMqtt.setText("连接");
tvMqttStatus.setText("状态: 未连接");
} else {
String brokerUrl = etBrokerUrl.getText().toString();
String clientId = etClientId.getText().toString();
mqttHandler.connect(brokerUrl, clientId, new MqttHandler.MqttCallback() {
@Override
public void onConnected() {
runOnUiThread(() -> {
btnConnectMqtt.setText("断开");
btnPublish.setEnabled(true);
btnSubscribe.setEnabled(true);
btnExpedite.setEnabled(true);
btnSlowdown.setEnabled(true);
btnClockwise.setEnabled(true);
btnAnticlockwise.setEnabled(true);
tvMqttStatus.setText("状态: 已连接");
});
}
@Override
public void onDisconnected() {
runOnUiThread(() -> {
btnConnectMqtt.setText("连接");
btnPublish.setEnabled(false);
btnSubscribe.setEnabled(false);
btnExpedite.setEnabled(false);
btnSlowdown.setEnabled(false);
btnClockwise.setEnabled(false);
btnAnticlockwise.setEnabled(false);
tvMqttStatus.setText("状态: 未连接");
});
}
@Override
public void onMessageReceived(String topic, String message) {
runOnUiThread(() -> {
tvMqttMessages.append("主题: " + topic + ", 消息: " + message + "\n");
});
}
@Override
public void onError(String error) {
runOnUiThread(() -> {
tvMqttStatus.setText("错误: " + error);
});
}
});
}
});
btnSubscribe.setOnClickListener(v -> {
if (mqttHandler.isConnected()) {
String topic = etTopic.getText().toString();
mqttHandler.subscribe(topic);
tvMqttMessages.append("已订阅主题: " + topic + "\n");
}
});
btnPublish.setOnClickListener(v -> {
String message = etMqttMessage.getText().toString();
sendMessage(message);
});
}
private void sendMessage(String message) {
if (mqttHandler.isConnected()) {
String topic = etTopic.getText().toString();
mqttHandler.publish(topic, message);
tvMqttMessages.append("发布到 " + topic + ": " + message + "\n");
etMqttMessage.setText("");
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mqttHandler != null && mqttHandler.isConnected()) {
mqttHandler.disconnect();
}
}
}
activity_mqtt.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/etBrokerUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="MQTT Broker URL"
android:inputType="textUri"
android:text="tcp://broker.emqx.io:1883"/>
<EditText
android:id="@+id/etClientId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="客户端ID"
android:inputType="text"
android:text="Yyuan"/>
<Button
android:id="@+id/btnConnectMqtt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="连接"/>
<!-- <EditText
android:id="@+id/etTopic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="主题"/>-->
<AutoCompleteTextView
android:id="@+id/etTopic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="主题"
android:text="esp8266/status"
android:completionThreshold="1"
android:inputType="text"/>
<Button
android:id="@+id/btnSubscribe"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="订阅"/>
<EditText
android:id="@+id/etMqttMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入消息"/>
<Button
android:id="@+id/btnPublish"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发布"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnexpedite"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="加速"/>
<Button
android:id="@+id/btnslowdown"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="减速"/>
<Button
android:id="@+id/btnclockwise"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="正转"/>
<Button
android:id="@+id/btnanticlockwise"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="反转"/>
</LinearLayout>
<TextView
android:id="@+id/tvMqttStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="状态: 未连接"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:id="@+id/tvMqttMessages"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</ScrollView>
</LinearLayout>
TcpClient.java
package com.example.communication;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class TcpClient {
public interface TcpListener {
void onConnected();
void onDisconnected();
void onMessageReceived(String message);
void onError(String error);
}
private String serverIp;
private int serverPort;
private TcpListener listener;
private Socket socket;
private PrintWriter out;
private BufferedReader in;
private boolean isConnected = false;
private final Handler mainHandler; // 添加主线程Handler
public TcpClient(String serverIp, int serverPort, TcpListener listener) {
this.serverIp = serverIp;
this.serverPort = serverPort;
this.listener = listener;
this.mainHandler = new Handler(Looper.getMainLooper()); // 初始化主线程Handler
}
public void connect() {
new Thread(() -> {
try {
socket = new Socket(serverIp, serverPort);
out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
isConnected = true;
listener.onConnected();
// 持续接收消息
String message;
while (isConnected && (message = in.readLine()) != null) {
listener.onMessageReceived(message);
}
} catch (IOException e) {
listener.onError(e.getMessage());
} finally {
disconnect();
}
}).start();
}
public void disconnect() {
isConnected = false;
try {
if (out != null) out.close();
if (in != null) in.close();
if (socket != null) socket.close();
listener.onDisconnected();
} catch (IOException e) {
listener.onError(e.getMessage());
}
}
private void runOnUiThread(Runnable action) {
mainHandler.post(action);
}
public void sendMessage(String message) {
if (!isConnected || out == null) {
runOnUiThread(() -> listener.onError("未连接或输出流不可用"));
return;
}
new Thread(() -> {
try {
out.print(message);
out.flush();
} catch (Exception e) {
runOnUiThread(() -> listener.onError("发送失败: " + e.getMessage()));
disconnect();
}
}).start();
}
public boolean isConnected() {
return isConnected;
}
}
TcpClientActivity.java
package com.example.communication;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import org.json.JSONException;
public class TcpClientActivity extends AppCompatActivity {
private EditText etServerIp, etServerPort, etMessage;
private Button btnConnect, btnSend;
private TextView tvStatus, tvReceived;
private TcpClient tcpClient;
private Button btnExpedite;
private Button btnSlowdown;
private Button btnClockwise;
private Button btnAnticlockwise;
ControlMotor controlMotor = new ControlMotor();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tcp_client);
etServerIp = findViewById(R.id.etServerIp);
etServerPort = findViewById(R.id.etServerPort);
etMessage = findViewById(R.id.etMessage);
btnConnect = findViewById(R.id.btnConnect);
btnSend = findViewById(R.id.btnSend);
tvStatus = findViewById(R.id.tvStatus);
tvReceived = findViewById(R.id.tvReceived);
btnExpedite = findViewById(R.id.btnexpedite);
btnSlowdown = findViewById(R.id.btnslowdown);
btnClockwise = findViewById(R.id.btnclockwise);
btnAnticlockwise = findViewById(R.id.btnanticlockwise);
btnExpedite.setEnabled(false);
btnSlowdown.setEnabled(false);
btnClockwise.setEnabled(false);
btnAnticlockwise.setEnabled(false);
btnSend.setEnabled(false);
btnExpedite.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
sendMessage(controlMotor.selectControl(1));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
});
btnSlowdown.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
sendMessage(controlMotor.selectControl(2));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
});
btnClockwise.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
sendMessage(controlMotor.selectControl(3));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
});
btnAnticlockwise.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
sendMessage(controlMotor.selectControl(4));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
});
btnConnect.setOnClickListener(v -> {
if (tcpClient != null && tcpClient.isConnected()) {
tcpClient.disconnect();
btnConnect.setText("连接");
tvStatus.setText("状态: 未连接");
btnExpedite.setEnabled(false);
btnSlowdown.setEnabled(false);
btnClockwise.setEnabled(false);
btnAnticlockwise.setEnabled(false);
btnSend.setEnabled(false);
} else {
String ip = etServerIp.getText().toString();
int port = Integer.parseInt(etServerPort.getText().toString());
tcpClient = new TcpClient(ip, port, new TcpClient.TcpListener() {
@Override
public void onConnected() {
runOnUiThread(() -> {
btnConnect.setText("断开");
tvStatus.setText("状态: 已连接");
btnExpedite.setEnabled(true);
btnSlowdown.setEnabled(true);
btnClockwise.setEnabled(true);
btnAnticlockwise.setEnabled(true);
btnSend.setEnabled(true);
});
}
@Override
public void onDisconnected() {
runOnUiThread(() -> {
btnConnect.setText("连接");
tvStatus.setText("状态: 未连接");
});
}
@Override
public void onMessageReceived(String message) {
runOnUiThread(() -> {
tvReceived.append("接收: " + message + "\n");
});
}
@Override
public void onError(String error) {
runOnUiThread(() -> {
tvStatus.setText("错误: " + error);
});
}
});
tcpClient.connect();
}
});
btnSend.setOnClickListener(v -> {
String message = etMessage.getText().toString();
sendMessage(message);
});
}
private void sendMessage(String message) {
if (tcpClient != null && tcpClient.isConnected()) {
if (!message.isEmpty()) {
tcpClient.sendMessage(message); // 现在这个方法内部已经处理了线程切换
runOnUiThread(() -> {
tvReceived.append("发送: " + message + "\n");
etMessage.setText("");
});
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (tcpClient != null) {
tcpClient.disconnect();
}
}
}
activity_tcp_client.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/etServerIp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="服务器IP"
android:inputType="text"
android:text="192.168.174.117"/>
<EditText
android:id="@+id/etServerPort"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="服务器端口"
android:inputType="number"
android:text="8080"/>
<Button
android:id="@+id/btnConnect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="连接"/>
<EditText
android:id="@+id/etMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入消息"/>
<Button
android:id="@+id/btnSend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发送"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnexpedite"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="加速"/>
<Button
android:id="@+id/btnslowdown"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="减速"/>
<Button
android:id="@+id/btnclockwise"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="正转"/>
<Button
android:id="@+id/btnanticlockwise"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="反转"/>
</LinearLayout>
<TextView
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="状态: 未连接"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:id="@+id/tvReceived"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</LinearLayout>
BluetoothActivity.java
package com.example.communication;
import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import com.example.communication.ControlMotor;
import org.json.JSONException;
public class BluetoothActivity extends AppCompatActivity {
private static final String TAG = "BluetoothActivity";
private static final int REQUEST_ENABLE_BT = 1;
private static final int REQUEST_PERMISSIONS = 2;
private static final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private BluetoothAdapter bluetoothAdapter;
private BluetoothSocket bluetoothSocket;
private BluetoothDevice connectedDevice;
private InputStream inputStream;
private OutputStream outputStream;
private ConnectedThread connectedThread;
private Button btnEnableBluetooth;
private Button btnDiscoverDevices;
private Button btnSendBluetooth;
private Button btnExpedite;
private Button btnSlowdown;
private Button btnClockwise;
private Button btnAnticlockwise;
ControlMotor controlMotor = new ControlMotor();
private ListView lvDevices;
private EditText etBluetoothMessage;
private TextView tvBluetoothStatus;
private TextView tvBluetoothMessages;
private ArrayList<BluetoothDevice> deviceList = new ArrayList<>();
private ArrayAdapter<String> deviceAdapter;
private ArrayList<String> deviceNames = new ArrayList<>();
private final Handler handler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bluetooth);
initViews();
initBluetoothAdapter();
checkAndRequestPermissions();
setupListeners();
registerBluetoothReceiver();
}
private void initViews() {
btnEnableBluetooth = findViewById(R.id.btnEnableBluetooth);
btnDiscoverDevices = findViewById(R.id.btnDiscoverDevices);
btnSendBluetooth = findViewById(R.id.btnSendBluetooth);
lvDevices = findViewById(R.id.lvDevices);
etBluetoothMessage = findViewById(R.id.etBluetoothMessage);
tvBluetoothStatus = findViewById(R.id.tvBluetoothStatus);
tvBluetoothMessages = findViewById(R.id.tvBluetoothMessages);
btnExpedite = findViewById(R.id.btnexpedite);
btnSlowdown = findViewById(R.id.btnslowdown);
btnClockwise = findViewById(R.id.btnclockwise);
btnAnticlockwise = findViewById(R.id.btnanticlockwise);
deviceAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, deviceNames);
lvDevices.setAdapter(deviceAdapter);
btnSendBluetooth.setEnabled(false);
btnExpedite.setEnabled(false);
btnSlowdown.setEnabled(false);
btnClockwise.setEnabled(false);
btnAnticlockwise.setEnabled(false);
}
private void initBluetoothAdapter() {
try {
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
tvBluetoothStatus.setText("状态: 设备不支持蓝牙");
btnEnableBluetooth.setEnabled(false);
return;
}
updateBluetoothStateUI(bluetoothAdapter.isEnabled());
} catch (SecurityException e) {
Log.e(TAG, "Bluetooth权限异常", e);
Toast.makeText(this, "无法访问蓝牙功能,请检查权限", Toast.LENGTH_SHORT).show();
}
}
private void checkAndRequestPermissions() {
List<String> permissionsNeeded = new ArrayList<>();
// 基本蓝牙权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH)
!= PackageManager.PERMISSION_GRANTED) {
permissionsNeeded.add(Manifest.permission.BLUETOOTH);
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN)
!= PackageManager.PERMISSION_GRANTED) {
permissionsNeeded.add(Manifest.permission.BLUETOOTH_ADMIN);
}
// Android 12+ 需要的新权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT)
!= PackageManager.PERMISSION_GRANTED) {
permissionsNeeded.add(Manifest.permission.BLUETOOTH_CONNECT);
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN)
!= PackageManager.PERMISSION_GRANTED) {
permissionsNeeded.add(Manifest.permission.BLUETOOTH_SCAN);
}
}
// 位置权限(用于设备发现)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
permissionsNeeded.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
if (!permissionsNeeded.isEmpty()) {
ActivityCompat.requestPermissions(this,
permissionsNeeded.toArray(new String[0]),
REQUEST_PERMISSIONS);
}
}
private void setupListeners() {
btnEnableBluetooth.setOnClickListener(v -> toggleBluetooth());
btnDiscoverDevices.setOnClickListener(v -> discoverDevices());
btnSendBluetooth.setOnClickListener(v -> sendCommonMessage());
btnExpedite.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
sendMessage(controlMotor.selectControl(1));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
});
btnSlowdown.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
sendMessage(controlMotor.selectControl(2));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
});
btnClockwise.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
sendMessage(controlMotor.selectControl(3));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
});
btnAnticlockwise.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
sendMessage(controlMotor.selectControl(4));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
});
lvDevices.setOnItemClickListener((parent, view, position, id) -> {
if (position < deviceList.size()) {
connectToDevice(deviceList.get(position));
}
});
}
private void registerBluetoothReceiver() {
try {
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_FOUND);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(bluetoothReceiver, filter);
} catch (SecurityException e) {
Log.e(TAG, "注册广播接收器失败", e);
}
}
private void toggleBluetooth() {
if (bluetoothAdapter == null) return;
try {
if (bluetoothAdapter.isEnabled()) {
bluetoothAdapter.disable();
Toast.makeText(this, "请手动关闭蓝牙", Toast.LENGTH_SHORT).show();
} else {
if (hasBluetoothConnectPermission()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
tvBluetoothStatus.setText("状态: 正在请求启用蓝牙...");
} else {
requestBluetoothPermissions();
}
}
} catch (SecurityException e) {
Log.e(TAG, "蓝牙操作权限异常", e);
Toast.makeText(this, "权限不足,无法操作蓝牙", Toast.LENGTH_SHORT).show();
}
}
private boolean hasBluetoothConnectPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return ContextCompat.checkSelfPermission(this,
Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED;
}
return true;
}
private void requestBluetoothPermissions() {
List<String> permissions = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
permissions.add(Manifest.permission.BLUETOOTH_SCAN);
}
permissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
ActivityCompat.requestPermissions(this,
permissions.toArray(new String[0]),
REQUEST_PERMISSIONS);
}
private void discoverDevices() {
if (!isBluetoothReady()) return;
try {
if (!hasDiscoveryPermissions()) {
requestBluetoothPermissions();
return;
}
deviceList.clear();
deviceNames.clear();
deviceAdapter.notifyDataSetChanged();
// 获取已配对设备
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
if (pairedDevices.size() > 0) {
for (BluetoothDevice device : pairedDevices) {
addDeviceToList(device, true);
}
}
if (bluetoothAdapter.isDiscovering()) {
bluetoothAdapter.cancelDiscovery();
}
bluetoothAdapter.startDiscovery();
tvBluetoothStatus.setText("状态: 正在搜索设备...");
} catch (SecurityException e) {
Log.e(TAG, "设备发现权限异常", e);
Toast.makeText(this, "权限不足,无法搜索设备", Toast.LENGTH_SHORT).show();
}
}
private boolean hasDiscoveryPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return ContextCompat.checkSelfPermission(this,
Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED;
}
return ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}
private void addDeviceToList(BluetoothDevice device, boolean isPaired) {
if (!deviceList.contains(device)) {
deviceList.add(device);
String deviceName;
try {
if (hasBluetoothConnectPermission()) {
deviceName = device.getName() != null ? device.getName() : "未知设备";
} else {
deviceName = device.getAddress(); // 没有权限时只显示地址
}
} catch (SecurityException e) {
deviceName = device.getAddress(); // 捕获安全异常,只显示地址
Log.w(TAG, "无法获取设备名称,权限不足", e);
}
String displayText = deviceName + "\n" + device.getAddress();
if (isPaired) {
displayText += " (已配对)";
}
if (deviceName.equals(device.getAddress())) {
displayText += "\n(无权限查看设备名称)";
}
deviceNames.add(displayText);
deviceAdapter.notifyDataSetChanged();
}
}
private boolean isBluetoothReady() {
if (bluetoothAdapter == null) {
tvBluetoothStatus.setText("状态: 设备不支持蓝牙");
return false;
}
if (!bluetoothAdapter.isEnabled()) {
tvBluetoothStatus.setText("状态: 蓝牙未启用");
return false;
}
return true;
}
private void connectToDevice(BluetoothDevice device) {
if (!isBluetoothReady() || !hasBluetoothConnectPermission()) {
requestBluetoothPermissions();
return;
}
try {
if (bluetoothAdapter.isDiscovering()) {
bluetoothAdapter.cancelDiscovery();
}
new Thread(() -> {
try {
bluetoothSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
bluetoothSocket.connect();
connectedDevice = device;
inputStream = bluetoothSocket.getInputStream();
outputStream = bluetoothSocket.getOutputStream();
runOnUiThread(() -> {
String name;
try {
name = hasBluetoothConnectPermission() ?
(device.getName() != null ? device.getName() : "未知设备") :
device.getAddress();
} catch (SecurityException e) {
name = device.getAddress();
}
tvBluetoothStatus.setText("状态: 已连接到 " + name);
btnSendBluetooth.setEnabled(true);
btnExpedite.setEnabled(true);
btnSlowdown.setEnabled(true);
btnClockwise.setEnabled(true);
btnAnticlockwise.setEnabled(true);
btnDiscoverDevices.setEnabled(true);
appendMessage("已连接到: " + name + "\n");
});
connectedThread = new ConnectedThread();
connectedThread.start();
} catch (IOException e) {
runOnUiThread(() -> {
tvBluetoothStatus.setText("状态: 连接失败");
Toast.makeText(BluetoothActivity.this,
"连接失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
});
closeSocket();
} catch (SecurityException e) {
runOnUiThread(() -> {
tvBluetoothStatus.setText("状态: 无连接权限");
Toast.makeText(BluetoothActivity.this,
"无蓝牙连接权限", Toast.LENGTH_SHORT).show();
});
}
}).start();
} catch (SecurityException e) {
Toast.makeText(this, "无权限取消发现或连接设备", Toast.LENGTH_SHORT).show();
}
}
private void sendCommonMessage() {
String message = etBluetoothMessage.getText().toString().trim();
sendMessage(message);
}
private void sendMessage(String message) {
if (message.isEmpty() || outputStream == null) return;
try {
if (!hasBluetoothConnectPermission()) {
requestBluetoothPermissions();
return;
}
outputStream.write(message.getBytes());
appendMessage("发送: " + message + "\n");
etBluetoothMessage.setText("");
} catch (IOException e) {
runOnUiThread(() -> {
tvBluetoothStatus.setText("状态: 发送失败");
Toast.makeText(BluetoothActivity.this, "发送失败", Toast.LENGTH_SHORT).show();
});
} catch (SecurityException e) {
Toast.makeText(this, "无蓝牙发送权限", Toast.LENGTH_SHORT).show();
}
}
private void appendMessage(String message) {
handler.post(() -> tvBluetoothMessages.append(message));
}
private void updateBluetoothStateUI(boolean isEnabled) {
btnEnableBluetooth.setText(isEnabled ? "禁用蓝牙" : "启用蓝牙");
btnDiscoverDevices.setEnabled(isEnabled);
if (!isEnabled) {
btnSendBluetooth.setEnabled(false);
btnExpedite.setEnabled(false);
btnSlowdown.setEnabled(false);
btnClockwise.setEnabled(false);
btnAnticlockwise.setEnabled(false);
}
}
private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device != null) {
addDeviceToList(device, false);
}
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
tvBluetoothStatus.setText("状态: 设备搜索完成");
} else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
runOnUiThread(() -> {
switch (state) {
case BluetoothAdapter.STATE_OFF:
tvBluetoothStatus.setText("状态: 蓝牙已禁用");
updateBluetoothStateUI(false);
break;
case BluetoothAdapter.STATE_TURNING_ON:
tvBluetoothStatus.setText("状态: 蓝牙正在开启...");
break;
case BluetoothAdapter.STATE_ON:
tvBluetoothStatus.setText("状态: 蓝牙已启用");
updateBluetoothStateUI(true);
break;
case BluetoothAdapter.STATE_TURNING_OFF:
tvBluetoothStatus.setText("状态: 蓝牙正在关闭...");
break;
}
});
}
}
};
private class ConnectedThread extends Thread {
@Override
public void run() {
byte[] buffer = new byte[1024];
int bytes;
while (!Thread.currentThread().isInterrupted()) {
try {
bytes = inputStream.read(buffer);
String receivedMessage = new String(buffer, 0, bytes);
appendMessage("接收: " + receivedMessage + "\n");
} catch (IOException e) {
runOnUiThread(() -> {
tvBluetoothStatus.setText("状态: 连接已断开");
btnSendBluetooth.setEnabled(false);
});
break;
}
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_ENABLE_BT) {
if (resultCode == RESULT_OK) {
tvBluetoothStatus.setText("状态: 蓝牙已启用");
updateBluetoothStateUI(true);
} else {
tvBluetoothStatus.setText("状态: 蓝牙启用被拒绝");
updateBluetoothStateUI(false);
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_PERMISSIONS) {
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (!allGranted) {
Toast.makeText(this, "部分功能需要权限才能使用", Toast.LENGTH_SHORT).show();
} else {
// 权限已授予,可以重新尝试之前的操作
if (bluetoothAdapter != null && !bluetoothAdapter.isEnabled()) {
toggleBluetooth();
}
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
try {
unregisterReceiver(bluetoothReceiver);
} catch (IllegalArgumentException e) {
Log.w(TAG, "广播接收器未注册或已注销");
}
closeSocket();
if (connectedThread != null) {
connectedThread.interrupt();
}
}
private void closeSocket() {
try {
if (bluetoothSocket != null) {
bluetoothSocket.close();
bluetoothSocket = null;
}
} catch (IOException e) {
Log.e(TAG, "关闭蓝牙socket时出错", e);
}
}
}
activity_bluetooth.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<Button
android:id="@+id/btnEnableBluetooth"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="启用蓝牙"/>
<Button
android:id="@+id/btnDiscoverDevices"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发现设备"/>
<ListView
android:id="@+id/lvDevices"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<EditText
android:id="@+id/etBluetoothMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入消息"/>
<Button
android:id="@+id/btnSendBluetooth"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发送"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnexpedite"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="加速"/>
<Button
android:id="@+id/btnslowdown"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="减速"/>
<Button
android:id="@+id/btnclockwise"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="正转"/>
<Button
android:id="@+id/btnanticlockwise"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="反转"/>
</LinearLayout>
<TextView
android:id="@+id/tvBluetoothStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="状态: 蓝牙未启用"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:id="@+id/tvBluetoothMessages"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</ScrollView>
</LinearLayout>
ControlMotor.java
package com.example.communication;
import org.json.JSONException;
import org.json.JSONObject;
public class ControlMotor {
String HEAD="^";
String TAIL="$";
public String selectControl(int control) throws JSONException {
JSONObject json = new JSONObject();
switch (control)
{
case 1://加速
json.put("Speed", "1");
break;
case 2://减速
json.put("Speed", "-1");
break;
case 3://正转
json.put("Dir", "1");
break;
case 4://反转
json.put("Dir", "0");
break;
default:
break;
}
String info=HEAD+json.toString()+TAIL;
return info;
}
}
5. STM32设计
笔者使用了cJSON库文件用来封装数据成json格式,可以在Github获取Github_cJSON步进电机采用简单的四相单四拍的方式来控制步进电机。
引脚配置
其中,蓝牙模块VCC端要连接在stm32f10的5v上,如果是3.3v,可能导致供电不足,步进电机也要连接在5v上。
源码如下:
main.c
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* <h2><center>© Copyright (c) 2025 STMicroelectronics.
* All rights reserved.</center></h2>
*
* This software component is licensed by ST under BSD 3-Clause license,
* the "License"; You may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
* opensource.org/licenses/BSD-3-Clause
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "i2c.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"
#include "string.h"
#include "stdio.h"
#include "OLED.h"
#include "stdbool.h"
#include "stdlib.h"
#include "cJSON.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
#define LENGTH 50
#define HEAD '^' // 数据帧头标识符
#define TAIL '$' // 数据帧尾标识符
/*
四项五线步进电机---28BYJ48
工作电压---5V
减速比---1:64
最小步进角度---360/8/8/64=5.625/64(8--八拍(八拍一个周期,八个周期一圈)64:1--减速比)
这里采用四相单四拍
*/
#define Get_Rotate_Speed(x) (1.0*60*1000/(8*4*64*x)) // 计算转速(RPM)的宏
uint8_t step=0; // 步进电机当前步数
uint32_t clock_5ms=0,count=0; // 5ms计时器和计数器
// 电机控制数据结构体
struct Data{
volatile int speed; // 电机速度参数
volatile bool direction; // 方向标志:true=正转,false=反转
}data;
bool is_handle=false,is_update_OLED=true; // 标志位:是否需要处理数据和更新OLED
// UART处理器结构体
typedef struct {
UART_HandleTypeDef *huart; // UART句柄指针
uint8_t ch; // 当前接收字符
uint8_t rxBuf[32]; // 接收缓冲区
uint8_t idx; // 缓冲区索引
bool is_begin_Rx; // 接收开始标志
} UART_Handler_t;
// 全局UART处理器实例
UART_Handler_t uart1_handler; // USART1处理器
UART_Handler_t uart3_handler; // USART3处理器
/* 初始化系统信息 */
void init_info()
{
data.speed=10; // 默认速度
data.direction=true; // 默认方向为正转
// 初始化USART1处理器
uart1_handler.huart=&huart1;
uart1_handler.idx=0;
uart1_handler.is_begin_Rx=false;
memset(uart1_handler.rxBuf,0,32);
// 初始化USART3处理器
uart3_handler.huart=&huart3;
uart3_handler.idx=0;
uart3_handler.is_begin_Rx=false;
memset(uart3_handler.rxBuf,0,32);
}
/* 重定向printf输出到USART1 */
int fputc(int ch,FILE*f)
{
HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,HAL_MAX_DELAY);
return ch;
}
/* 步进电机步进控制函数 */
void Motor_Step() {
step %= 4; // 步数取模,保持在0-3范围内
uint8_t phase = data.direction ?(3 - step):step; // 根据方向确定相位
// 先关闭所有相线
HAL_GPIO_WritePin(GPIOA, IN1_Pin|IN2_Pin|IN3_Pin|IN4_Pin, GPIO_PIN_RESET);
// 根据相位激活对应相线
switch(phase) {
case 0: HAL_GPIO_WritePin(GPIOA, IN1_Pin, GPIO_PIN_SET); break;
case 1: HAL_GPIO_WritePin(GPIOA, IN2_Pin, GPIO_PIN_SET); break;
case 2: HAL_GPIO_WritePin(GPIOA, IN3_Pin, GPIO_PIN_SET); break;
case 3: HAL_GPIO_WritePin(GPIOA, IN4_Pin, GPIO_PIN_SET); break;
}
step++; // 步数增加
}
/* OLED显示更新函数 */
void Display()
{
if(is_update_OLED) // 需要更新OLED显示
{
is_update_OLED=false;
char ch[LENGTH];
memset(ch,0,LENGTH);
// 显示速度信息
__disable_irq(); // 禁用中断保护数据
sprintf(ch,"Speed:%.2fRPM",(float)Get_Rotate_Speed(data.speed));
__enable_irq(); // 启用中断
OLED_ShowString(1,1,ch);
// 显示方向信息
memset(ch,0,LENGTH);
__disable_irq();
strcpy(ch, data.direction ? "ClockWise " : "AntiClockWise");
__enable_irq();
OLED_ShowString(3,1,"Dir:");
OLED_ShowString(4,2,ch);
// 显示计数器
OLED_ShowString(2,1,"Count:");
OLED_ShowNum(2,7,count,2);
}
if(is_handle) // 需要处理数据
{
is_handle=false;
char ch[LENGTH];
// 创建JSON对象
cJSON *root = cJSON_CreateObject();
// 添加速度信息到JSON
memset(ch,0,LENGTH);
__disable_irq();
sprintf(ch,"%.2fRPM",(float)Get_Rotate_Speed(data.speed));
__enable_irq();
cJSON_AddStringToObject(root, "Speed", ch);
// 添加方向信息到JSON
memset(ch,0,LENGTH);
__disable_irq();
strcpy(ch, data.direction ? "ClockWise" : "AntiClockWise");
__enable_irq();
cJSON_AddStringToObject(root, "Dir", ch);
// 打印JSON字符串
char *json = cJSON_Print(root);
printf("%c%s%c",HEAD,json,TAIL);
// 释放内存
free(json); // 释放cJSON_Print分配的内存
cJSON_Delete(root); // 释放整个JSON树
}
}
/* UART数据处理函数 */
void ProcessUARTData(UART_Handler_t *handler)
{
// 重新启用接收中断
HAL_UART_Receive_IT(handler->huart, &handler->ch, 1);
if(handler->ch == TAIL) { // 接收到帧尾
handler->rxBuf[handler->idx] = '\0'; // 字符串结束符
count++; // 计数器增加
// 解析JSON数据
cJSON *root = cJSON_Parse((char*)handler->rxBuf);
if(root==NULL) // 解析失败
{
printf("Fail Rec:%s\n", handler->rxBuf);
}
else // 解析成功
{
// 获取速度参数
cJSON *item1 = cJSON_GetObjectItem(root, "Speed");
// 获取方向参数
cJSON *item2 = cJSON_GetObjectItem(root, "Dir");
if(item1!=NULL) // 速度参数存在
{
int speed_adjust = 0;
// 检查是否为数字类型
if(cJSON_IsNumber(item1)) {
speed_adjust = cJSON_GetNumberValue(item1);
}
// 检查是否为字符串类型且可以转换为数字
else if(cJSON_IsString(item1)) {
char *endptr;
speed_adjust = strtod(item1->valuestring, &endptr);
// 检查转换是否成功
if(endptr == item1->valuestring) {
printf("Invalid number string: %s\n", item1->valuestring);
speed_adjust = 0; // 设置默认值
}
}
// 计算新速度(最小限制为5)
int speed_f = data.speed + (-5*speed_adjust);
data.speed = speed_f <= 5 ? 5 : speed_f;
}
if(item2!=NULL) // 方向参数存在
{
if(cJSON_IsString(item2)) {
data.direction = strcmp(item2->valuestring, "0") != 0;
}
else if(cJSON_IsNumber(item2)) {
data.direction = item2->valueint != 0;
}
}
cJSON_Delete(root); // 释放JSON树
}
// 重置接收状态
is_update_OLED = true; // 标记需要更新OLED
handler->idx = 0;
handler->is_begin_Rx = false;
memset(handler->rxBuf, 0, sizeof(handler->rxBuf));
}
else if(handler->ch == HEAD) { // 接收到帧头
handler->is_begin_Rx = true; // 开始接收数据
handler->idx=0; // 重置缓冲区索引
}
else if(handler->is_begin_Rx) { // 正在接收数据
handler->rxBuf[handler->idx++] = handler->ch; // 存储数据
if(handler->idx >= sizeof(handler->rxBuf)) { // 缓冲区溢出
handler->idx = 0;
handler->is_begin_Rx = false;
}
}
}
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
// 硬件初始化
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2C1_Init();
HAL_Delay(1000); // 延时等待蓝牙外设稳定
// 初始化USART1
MX_USART1_UART_Init();
HAL_UART_Receive_IT(&huart1,&uart1_handler.ch,1);
// 初始化TIM2和USART3
MX_TIM2_Init();
MX_USART3_UART_Init();
HAL_UART_Receive_IT(&huart3,&uart3_handler.ch,1);
// 初始化OLED和系统信息
OLED_Init();
HAL_TIM_Base_Start_IT(&htim2); // 启动定时器2
init_info();
// 主循环
while (1)
{
Display(); // 更新显示
}
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* 定时器中断回调函数 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM2) // TIM2中断
clock_5ms++; // 5ms计数器增加
if(clock_5ms%(data.speed/5)==0) // 根据速度控制电机步进
Motor_Step();
if(clock_5ms==2000) // 10秒周期(2000*5ms)
{
clock_5ms=0;
is_handle=true; // 标记需要处理数据
}
}
/* UART接收完成中断回调函数 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 根据UART调用对应的数据处理函数
if(huart->Instance == USART1) {
ProcessUARTData(&uart1_handler);
}
else if(huart->Instance == USART3) {
ProcessUARTData(&uart3_handler);
}
}
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
OLED.c (其中OLED_Font.h文件可以见帖子最下端源码链接)
#include "OLED.h"
#include "OLED_Font.h"
/**
* @brief OLED写命令
* @param Command 要写入的命令
* @retval 无
*/
void OLED_WriteCommand(uint8_t Command)
{
uint8_t buf[2];
buf[0] = 0x00; // 控制字节:Co=0, D/C#=0(命令)
buf[1] = Command;
HAL_I2C_Master_Transmit(&hi2c1, 0x78, buf, 2, 100); // 注意替换hi2c1为实际句柄
}
// 新版本OLED_WriteData(使用硬件I2C)
void OLED_WriteData(uint8_t Data)
{
uint8_t buf[2];
buf[0] = 0x40; // 控制字节:Co=0, D/C#=1(数据)
buf[1] = Data;
HAL_I2C_Master_Transmit(&hi2c1, 0x78, buf, 2, 100);
}
/**
* @brief OLED设置光标位置
* @param Y 以左上角为原点,向下方向的坐标,范围:0~7
* @param X 以左上角为原点,向右方向的坐标,范围:0~127
* @retval 无
*/
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
OLED_WriteCommand(0xB0 | Y); //设置Y位置
OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); //设置X位置高4位
OLED_WriteCommand(0x00 | (X & 0x0F)); //设置X位置低4位
}
/**
* @brief OLED清屏
* @param 无
* @retval 无
*/
void OLED_Clear(void)
{
uint8_t i, j;
for (j = 0; j < 8; j++)
{
OLED_SetCursor(j, 0);
for(i = 0; i < 128; i++)
{
OLED_WriteData(0x00);
}
}
}
/**
* @brief OLED显示一个字符
* @param Line 行位置,范围:1~4
* @param Column 列位置,范围:1~16
* @param Char 要显示的一个字符,范围:ASCII可见字符
* @retval 无
*/
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{
uint8_t i;
OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); //设置光标位置在上半部分
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i]); //显示上半部分内容
}
OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //设置光标位置在下半部分
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]); //显示下半部分内容
}
}
/**
* @brief OLED显示字符串
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串,范围:ASCII可见字符
* @retval 无
*/
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i++)
{
OLED_ShowChar(Line, Column + i, String[i]);
}
}
/**
* @brief OLED次方函数
* @retval 返回值等于X的Y次方
*/
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y--)
{
Result *= X;
}
return Result;
}
/**
* @brief OLED显示数字(十进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~4294967295
* @param Length 要显示数字的长度,范围:1~10
* @retval 无
*/
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i, Number / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
}
/**
* @brief OLED显示数字(十进制,带符号数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-2147483648~2147483647
* @param Length 要显示数字的长度,范围:1~10
* @retval 无
*/
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)
{
uint8_t i;
uint32_t Number1;
if (Number >= 0)
{
OLED_ShowChar(Line, Column, '+');
Number1 = Number;
}
else
{
OLED_ShowChar(Line, Column, '-');
Number1 = -Number;
}
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i + 1, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
}
/**
* @brief OLED显示数字(十六进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFFFFFF
* @param Length 要显示数字的长度,范围:1~8
* @retval 无
*/
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i, SingleNumber;
for (i = 0; i < Length; i++)
{
SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;
if (SingleNumber < 10)
{
OLED_ShowChar(Line, Column + i, SingleNumber + '0');
}
else
{
OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');
}
}
}
/**
* @brief OLED显示数字(二进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i, Number / OLED_Pow(2, Length - i - 1) % 2 + '0');
}
}
/**
* @brief OLED初始化
* @param 无
* @retval 无
*/
void OLED_Init(void)
{
uint32_t i, j;
for (i = 0; i < 1000; i++) //上电延时
{
for (j = 0; j < 1000; j++);
}
//端口初始化
OLED_WriteCommand(0xAE); //关闭显示
OLED_WriteCommand(0xD5); //设置显示时钟分频比/振荡器频率
OLED_WriteCommand(0x80);
OLED_WriteCommand(0xA8); //设置多路复用率
OLED_WriteCommand(0x3F);
OLED_WriteCommand(0xD3); //设置显示偏移
OLED_WriteCommand(0x00);
OLED_WriteCommand(0x40); //设置显示开始行
OLED_WriteCommand(0xA1); //设置左右方向,0xA1正常 0xA0左右反置
OLED_WriteCommand(0xC8); //设置上下方向,0xC8正常 0xC0上下反置
OLED_WriteCommand(0xDA); //设置COM引脚硬件配置
OLED_WriteCommand(0x12);
OLED_WriteCommand(0x81); //设置对比度控制
OLED_WriteCommand(0xCF);
OLED_WriteCommand(0xD9); //设置预充电周期
OLED_WriteCommand(0xF1);
OLED_WriteCommand(0xDB); //设置VCOMH取消选择级别
OLED_WriteCommand(0x30);
OLED_WriteCommand(0xA4); //设置整个显示打开/关闭
OLED_WriteCommand(0xA6); //设置正常/倒转显示
OLED_WriteCommand(0x8D); //设置充电泵
OLED_WriteCommand(0x14);
OLED_WriteCommand(0xAF); //开启显示
OLED_Clear(); //OLED清屏
}
OLED.h
#ifndef __OLED_H
#define __OLED_H
#include "math.h"
#include "i2c.h"
/*引脚配置*/
#define OLED_W_SCL(x) HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, (GPIO_PinState)(x))
#define OLED_W_SDA(x) HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, (GPIO_PinState)(x))
void OLED_Init(void);
void OLED_Clear(void);
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char);
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String);
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length);
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length);
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length);
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length);
#endif
6. ESP8266设计
ESP8266烧录可以直接使用数据线连接至电脑,选择开发板和com口进行烧录程序。
ESP8266运行时,与stm32进行串口通信,需要连接:
ESP8266 STM32F10 EN、VCC 3V3 GND、D8(GPIO15) GND TXD RX RXD TX D3(GPIO0)低电平---烧录模式,反之为运行模式
笔者使用Arduino用来编程ESP8266,其中MQTT协议需要导入PubSubClient库,工具->管理库->搜索“PubSubClient”->安装即可。
设计主要特点:1) 通过WiFi连接路由器并创建TCP服务器(8080端口);2) 接入MQTT协议与云端代理(broker.emqx.io)通信,支持esp8266/status主题发布和app/command主题订阅;3) 通过串口与STM32等设备通信,采用^$帧头帧尾协议解析数据;4) 同时处理TCP客户端连接请求,实现双向数据传输。
笔者本想采用线程来处理串口接收,MQTT和TCP,但是很容易导致栈崩溃,若只采用MQTT协议进行上传数据并与手机通信,但是通过手机订阅消息,笔者多次尝试容易产生延迟,实时性比较差。因此,才采用这种方式实现。
Arduino源码如下:
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
// 数据帧标识符
#define HEAD '^' // 数据帧起始标识
#define TAIL '$' // 数据帧结束标识
// WiFi配置
const char* ssid = "your_ssid"; // WiFi名称
const char* password = "your_password"; // WiFi密码
// MQTT配置
const char* mqtt_server = "broker.emqx.io"; // MQTT代理服务器地址
const char* mqtt_Pub_topic = "esp8266/status"; // 发布主题
const char* mqtt_Sub_topic = "app/command"; // 订阅主题
const int mqtt_port = 1883; // MQTT端口
// 服务器配置
WiFiServer server(8080); // 创建TCP服务器,监听8080端口
WiFiClient tcpclient; // TCP客户端对象
// MQTT客户端
WiFiClient espClient; // WiFi客户端
PubSubClient client(espClient); // MQTT客户端
volatile bool networkBusy = false; // 网络忙标志,防止重入
void setup() {
Serial.begin(115200); // 初始化串口,用于与STM32通信
delay(3000);//等待stm32串口初始化
setup_wifi(); // 连接WiFi
// 启动TCP服务器
server.begin();
Serial.println("服务器已启动");
// 打印网络信息
Serial.print("服务器IP地址: ");
Serial.println(WiFi.localIP());
Serial.print("服务器端口: ");
Serial.println(8080);
// 配置MQTT客户端
client.setServer(mqtt_server, mqtt_port); // 设置MQTT服务器
client.setCallback(callback);
}
/* WiFi连接函数 */
void setup_wifi() {
delay(10);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.mode(WIFI_STA); // 设置为站模式(连接路由器)
WiFi.begin(ssid, password); // 连接WiFi
// 等待连接成功
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// 连接成功打印信息
Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
/* MQTT消息回调函数 */
void callback(char* topic, byte* payload, unsigned int length) {
// 打印接收到的消息信息
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
// 提取消息内容
String jsonStr;
for (int i = 0; i < length; i++) {
jsonStr += (char)payload[i];
}
Serial.println(jsonStr); // 打印完整消息
}
/* MQTT重连函数 */
void reconnect() {
// 尝试重新连接MQTT服务器
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// 尝试连接
if (client.connect("ESP8266Client")) { // 客户端ID
Serial.println("connected");
// 连接成功后订阅主题
client.subscribe(mqtt_Sub_topic);
} else {
// 连接失败处理
Serial.print("failed, rc=");
Serial.print(client.state()); // 打印错误状态
Serial.println(" try again in 5 seconds");
delay(1000);
}
}
}
/* 串口数据处理函数 */
void handleSerialData() {
// 检查串口是否有数据且网络不忙
if (Serial.available() && !networkBusy) {
networkBusy = true; // 设置网络忙标志
String sh = ""; // 存储接收的数据
bool is_begin_rx = false; // 数据接收开始标志
// 处理所有可用数据
while (Serial.available()) {
char ch = Serial.read(); // 读取一个字符
if (ch == HEAD) { // 检测到帧头
is_begin_rx = true;
sh = ""; // 清空缓冲区
} else if (is_begin_rx && ch == TAIL) { // 检测到帧尾
is_begin_rx = false;
Serial.println("Publishing: " + sh); // 打印要发布的消息
// 确保MQTT连接正常
while(!client.connected()) {
reconnect();
}
// 发布消息到MQTT主题
if (!client.publish(mqtt_Pub_topic, sh.c_str())) {
Serial.println("Publish failed!"); // 发布失败处理
}
sh = ""; // 清空缓冲区
} else if (is_begin_rx) { // 正在接收数据
sh += ch; // 添加到缓冲区
}
delay(1); // 短暂延时
}
networkBusy = false; // 清除网络忙标志
}
}
void handleTCP()
{
// 检查TCP客户端连接
if (!tcpclient) {
// 没有客户端连接,等待新连接
tcpclient = server.available();
} else {
// 有客户端已连接
if (tcpclient.connected()) {
// 检查客户端是否有数据可读
if (tcpclient.available()) {
// 丢弃HEAD之前的所有内容
tcpclient.readStringUntil(HEAD);
// 读取直到TAIL的内容
String request = tcpclient.readStringUntil(TAIL);
Serial.print("收到消息: ");
Serial.println(request);
// 处理JSON数据(原handleJson函数未实现)
request=HEAD+request+TAIL; // 重新添加帧头帧尾
Serial.println(request);
// 向客户端发送响应
tcpclient.println("ESP8266服务器已收到你的消息: " + request);
}
} else {
// 客户端断开连接
tcpclient.stop();
Serial.println("客户端断开连接");
}
}
}
/* 主循环 */
void loop() {
handleSerialData(); // 处理串口数据
handleTCP();
delay(100); // 主循环延时
}
7. 效果演示
8.参考文献
[1] uln2003驱动电路_身在江湖的郭大侠的博客-CSDN博客_uln2003
[2]【STM32】步进电机及其驱动(ULN2003驱动28BYJ-48丨按键控制电机旋转)_Include everything的博客-CSDN博客_步进电机
[3] 28BY-J48步进电机工作原理_文析的博客-CSDN博客_28BY-J48
[4] HC-05蓝牙模块AT指令设置教程_LG小龙哥的博客-CSDN博客_HC-05
[5]【B站】28BYJ-48步进电机详解(五线四相 STM32)
[6]【B站】STM32入门教程-2023版 细致讲解 中文字幕_OLED显示屏
源码
链接: https://pan.baidu.com/s/1MdoTqNVrmLnncYsyUhNbRg?pwd=svc5 提取码: svc5