first commit

This commit is contained in:
zhenai 2026-10-12 22:00:30 +08:00
commit f596912057
12 changed files with 672 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

11
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack",
"pioarduino.pioarduino-ide"
]
}

37
include/README Normal file
View File

@ -0,0 +1,37 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

46
lib/README Normal file
View File

@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into the executable file.
The source code of each library should be placed in a separate directory
("lib/your_library_name/[Code]").
For example, see the structure of the following example libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Example contents of `src/main.c` using Foo and Bar:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
The PlatformIO Library Dependency Finder will find automatically dependent
libraries by scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

21
platformio.ini Normal file
View File

@ -0,0 +1,21 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_compat_mode = soft
lib_deps =
jrowberg/I2Cdevlib-Core
jrowberg/I2Cdevlib-MPU6050
Adafruit_MPU6050
Adafruit_Sensor

View File

@ -0,0 +1,12 @@
{
"folders": [
{
"path": "../../../Arduino/sketch_oct8a"
},
{
"name": "TwoFootBot-arduion",
"path": "."
}
],
"settings": {}
}

81
src/imu/imu.cpp Normal file
View File

@ -0,0 +1,81 @@
#include "I2Cdev.h"
#include "MPU6050_6Axis_MotionApps20.h"
#include "Wire.h"
#include <MPU6050.h>
#include "imu.h"
MPU6050 mpu;
bool dmpReady = false;
uint8_t fifoBuffer[64];
Quaternion q;
VectorFloat gravity;
// ===== 初始化 =====
void imu_setup() {
Serial.begin(115200);
Wire.begin(21, 22);
mpu.initialize();
if (!mpu.testConnection()) {
Serial.println("MPU6050 连接失败");
while (1);
}
uint8_t devStatus = mpu.dmpInitialize();
if (devStatus == 0) {
mpu.CalibrateAccel(6);
mpu.CalibrateGyro(6);
mpu.setDMPEnabled(true);
dmpReady = true;
Serial.println("DMP 就绪");
} else {
Serial.print("DMP 初始化失败,错误码: ");
Serial.println(devStatus);
}
}
// ===== DMP 读取,返回 YPR(角度制)=====
YPR dmp() {
YPR out = {0, 0, 0};
if (!dmpReady) return out;
if (mpu.dmpGetCurrentFIFOPacket(fifoBuffer)) {
mpu.dmpGetQuaternion(&q, fifoBuffer);
mpu.dmpGetGravity(&gravity, &q);
float ypr[3];
mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
out.yaw = ypr[0] * 180.0f / M_PI;
out.pitch = ypr[1] * 180.0f / M_PI;
out.roll = ypr[2] * 180.0f / M_PI;
}
// Serial.print("Yaw: "); Serial.print(a.yaw);
// Serial.print(" Pitch: "); Serial.print(a.pitch);
// Serial.print(" Roll: "); Serial.println(a.roll);
return out;
}
// ===== 原始数据读取,返回 RawData =====
RawData raw() {
RawData r;
mpu.getMotion6(&r.ax, &r.ay, &r.az, &r.gx, &r.gy, &r.gz);
// ±2g 量程下 1g = 16384 LSB
// r.ax_g = r.ax / 16384.0f;
// r.ay_g = r.ay / 16384.0f;
// r.az_g = r.az / 16384.0f;
// // ±250°/s 量程下 1°/s = 131 LSB
// r.gx_dps = r.gx / 131.0f;
// r.gy_dps = r.gy / 131.0f;
// r.gz_dps = r.gz / 131.0f;
// Serial.print("Accel(g): X="); Serial.print(r.ax_g, 3);
// Serial.print(" Y="); Serial.print(r.ay_g, 3);
// Serial.print(" Z="); Serial.print(r.az_g, 3);
// Serial.print(" | Gyro(°/s): X="); Serial.print(r.gx_dps, 2);
// Serial.print(" Y="); Serial.print(r.gy_dps, 2);
// Serial.print(" Z="); Serial.println(r.gz_dps, 2);
return r;
}

27
src/imu/imu.h Normal file
View File

@ -0,0 +1,27 @@
#ifndef IMU_DATA_H
#define IMU_DATA_H
#include <Arduino.h>
// 返回姿态角(单位:度)
struct YPR {
float yaw;
float pitch;
float roll;
};
// 返回原始数据及换算值
struct RawData {
int16_t ax, ay, az;
int16_t gx, gy, gz;
// float ax_g, ay_g, az_g;
// float gx_dps, gy_dps, gz_dps;
//
};
// 函数声明
YPR dmp();
RawData raw();
void imu_setup();
#endif

25
src/main.cpp Normal file
View File

@ -0,0 +1,25 @@
#include "imu/imu.h"
#include "wifi/wifi-sg90.h"
#include <esp32-hal.h>
void setup() {
// 串口初始化等公共配置
Serial.begin(115200);
// 调用子模块的初始化函数
imu_setup();
wifi_setup();
}
void loop() {
static unsigned long lastTelemetry = 0;
unsigned long now = millis();
//unsigned long now = micros(); // 改用微秒级计时
if (now - lastTelemetry >= 300) {
lastTelemetry = now;
YPR a = dmp();
RawData r = raw();
sendLineData(a, r);
}
}

15
src/wifi/wifi-sg90.h Normal file
View File

@ -0,0 +1,15 @@
#pragma once
#ifndef WIFI_SG90_H
#define WIFI_SG90_H
#include "../imu/imu.h"
void sg90();
void wifi_setup();
// TCP IMU telemetry: 22-byte frame: A5 5A, then 9 signed big-endian int16
// values (yaw/pitch/roll in centidegrees, then ax/ay/az/gx/gy/gz), then
// CRC-16/CCITT-FALSE over the first 20 bytes. Text command replies remain lines.
void sendLineData(YPR a, RawData r);
#endif

381
src/wifi/wifi.cpp Normal file
View File

@ -0,0 +1,381 @@
#include <WiFi.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <math.h>
#include "wifi/wifi-sg90.h"
// ==================== WiFi / TCP 配置 ====================
const char* AP_SSID = "ESP32_AP";
const char* AP_PASS = "12345678";
const int TCP_PORT = 3333;
// ==================== 舵机配置 ====================
const uint8_t NUM_SERVOS = 8;
// 注意:避开 GPIO 6~11(Flash)、34~39(仅输入)、2/15(启动脚)
const uint8_t SERVO_PINS[NUM_SERVOS] = {13, 14, 25, 26, 27, 32, 33, 4};
const uint16_t PULSE_MIN_US = 500; // 0 rad
const uint16_t PULSE_MAX_US = 2400; // π rad
const uint16_t PERIOD_US = 20000; // 50Hz = 20ms
const float RAD_MIN = 0.0f;
const float RAD_MAX = PI;
// ==================== 状态 ====================
struct ServoState {
uint8_t pin;
float targetRad;
unsigned long lastCycle;
};
ServoState g_servos[NUM_SERVOS];
WiFiServer tcpServer(TCP_PORT);
WiFiClient client;
struct TelemetryData {
YPR attitude;
RawData raw;
};
QueueHandle_t telemetryQueue = nullptr;
char commandBuffer[128];
size_t commandLength = 0;
bool commandOverflow = false;
// ==================== 前向声明 ====================
bool isNumeric(const String& s);
uint8_t parseFloatList(const String& s, float* out, uint8_t maxCount);
void sendLine(const String& msg);
void sg90Task(void* parameter);
void sendQueuedTelemetry();
int16_t encodeAngle(float degrees);
void writeInt16BE(uint8_t* buffer, size_t offset, int16_t value);
uint16_t calculateCrc16(const uint8_t* data, size_t length);
// ==================== 舵机初始化 / 更新 ====================
void servoInit() {
for (uint8_t i = 0; i < NUM_SERVOS; i++) {
g_servos[i].pin = SERVO_PINS[i];
g_servos[i].targetRad = PI / 2.0f; // 默认 90°
// 相位错开:第 i 个舵机在 20ms 周期内的 (i * 2.5ms) 处触发
g_servos[i].lastCycle = micros() -
(unsigned long)i * (PERIOD_US / NUM_SERVOS);
pinMode(SERVO_PINS[i], OUTPUT);
digitalWrite(SERVO_PINS[i], LOW);
}
}
/**
* @brief 更新 8 个舵机的 PWM,由 SG90 通讯任务周期调用
*/
void servoUpdateAll() {
unsigned long now = micros();
for (uint8_t i = 0; i < NUM_SERVOS; i++) {
ServoState& s = g_servos[i];
if ((unsigned long)(now - s.lastCycle) >= PERIOD_US) {
s.lastCycle += PERIOD_US;
// 若长时间滞后(例如遇到耗时长的指令),重新对齐到当前时刻
if ((unsigned long)(now - s.lastCycle) >= PERIOD_US) {
s.lastCycle = now;
}
// 弧度 -> 脉宽(µs)
float rad = s.targetRad;
if (rad < RAD_MIN) rad = RAD_MIN;
if (rad > RAD_MAX) rad = RAD_MAX;
uint16_t pulse = PULSE_MIN_US +
(uint16_t)((PULSE_MAX_US - PULSE_MIN_US) *
(rad / PI) + 0.5f);
digitalWrite(s.pin, HIGH);
delayMicroseconds(pulse);
digitalWrite(s.pin, LOW);
}
}
}
// ==================== WiFi / TCP ====================
void initWiFiAP() {
WiFi.mode(WIFI_AP);
WiFi.softAP(AP_SSID, AP_PASS);
Serial.println("==== WiFi AP 已启动 ====");
Serial.print("SSID : "); Serial.println(AP_SSID);
Serial.print("Password: "); Serial.println(AP_PASS);
Serial.print("AP IP : "); Serial.println(WiFi.softAPIP());
}
void initTcpServer() {
tcpServer.begin();
tcpServer.setNoDelay(true);
Serial.printf("TCP 服务器已启动,端口: %d\n", TCP_PORT);
}
void sendLine(const String& msg) {
if (client && client.connected()) {
client.println(msg);
}
}
void acceptClientIfNeeded() {
if (!client || !client.connected()) {
client = tcpServer.available();
if (client) {
commandLength = 0;
commandOverflow = false;
Serial.print("上位机已连接: ");
Serial.println(client.remoteIP());
sendLine("ESP32: 已连接。请发送 8 个角度,例如 "
"RAD:1.57,1.57,1.57,1.57,1.57,1.57,1.57,1.57");
} else {
commandLength = 0;
commandOverflow = false;
}
}
}
// ==================== 解析工具 ====================
bool isNumeric(const String& s) {
if (s.length() == 0) return false;
bool dotSeen = false;
for (size_t i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '-' || c == '+') {
if (i != 0) return false;
} else if (c == '.') {
if (dotSeen) return false;
dotSeen = true;
} else if (!isDigit(c)) {
return false;
}
}
return true;
}
/**
* @brief 解析逗号分隔的浮点列表
* @return 成功解析出的元素个数(遇到非法 token 立即停止)
*/
uint8_t parseFloatList(const String& s, float* out, uint8_t maxCount) {
uint8_t count = 0;
int start = 0;
int len = s.length();
while (start < len && count < maxCount) {
int comma = s.indexOf(',', start);
String token;
if (comma < 0) {
token = s.substring(start);
start = len;
} else {
token = s.substring(start, comma);
start = comma + 1;
}
token.trim();
if (token.length() == 0) continue;
if (!isNumeric(token)) break; // 非法 token -> 放弃后续
out[count++] = token.toFloat();
}
return count;
}
// ==================== 指令处理 ====================
void sendCurrentState() {
String msg = "ESP32:";
for (uint8_t i = 0; i < NUM_SERVOS; i++) {
msg += " S";
msg += String(i);
msg += "=";
msg += String(g_servos[i].targetRad, 4);
}
sendLine(msg);
}
/**
* @brief 支持指令:
* RAD:r0,r1,...,r7 弧度值列表
* DEG:d0,d1,...,d7 角度值列表(内部转弧度)
* r0,r1,...,r7 纯数字按弧度处理
* GET 查询当前 8 路角度
* PING 心跳
*/
void processCommand(const String& raw) {
String cmd = raw;
cmd.trim();
if (cmd.length() == 0) return;
if (cmd == "PING") { sendLine("ESP32: PONG"); return; }
if (cmd == "GET") { sendCurrentState(); return; }
float values[NUM_SERVOS];
uint8_t n = 0;
bool inDeg = false;
if (cmd.startsWith("RAD:")) {
n = parseFloatList(cmd.substring(4), values, NUM_SERVOS);
} else if (cmd.startsWith("DEG:")) {
n = parseFloatList(cmd.substring(4), values, NUM_SERVOS);
inDeg = true;
} else if (cmd.startsWith("SET:")) {
n = parseFloatList(cmd.substring(4), values, NUM_SERVOS);
} else {
n = parseFloatList(cmd, values, NUM_SERVOS);
}
if (n == 0) {
sendLine("ESP32: 无法解析 -> " + cmd);
return;
}
// 写入目标角度
for (uint8_t i = 0; i < n && i < NUM_SERVOS; i++) {
float rad = inDeg ? (values[i] * PI / 180.0f) : values[i];
if (rad < RAD_MIN) rad = RAD_MIN;
if (rad > RAD_MAX) rad = RAD_MAX;
g_servos[i].targetRad = rad;
}
// 简单日志(不要频繁打印,避免影响时序)
Serial.printf("SET %u servo(s)\n", n);
sendLine("ESP32: OK 设置 " + String(n) + " 个舵机");
}
void handleClientCommand() {
if (!client || !client.connected()) return;
uint16_t bytesRead = 0;
while (client.available() && bytesRead < sizeof(commandBuffer)) {
int incoming = client.read();
if (incoming < 0) break;
bytesRead++;
if (incoming == '\n') {
if (commandOverflow) {
sendLine("ESP32: 指令过长");
} else {
commandBuffer[commandLength] = '\0';
processCommand(String(commandBuffer));
}
commandLength = 0;
commandOverflow = false;
} else if (!commandOverflow) {
if (commandLength < sizeof(commandBuffer) - 1) {
commandBuffer[commandLength++] = static_cast<char>(incoming);
} else {
commandOverflow = true;
}
}
}
}
void sendQueuedTelemetry() {
if (!client || !client.connected()) {
return;
}
TelemetryData data;
if (telemetryQueue == nullptr ||
xQueueReceive(telemetryQueue, &data, 0) != pdTRUE) {
return;
}
// Frame: A5 5A, 3 angles (centidegrees), 6 raw int16 values, CRC16-CCITT.
uint8_t packet[22] = {0xA5, 0x5A};
writeInt16BE(packet, 2, encodeAngle(data.attitude.yaw));
writeInt16BE(packet, 4, encodeAngle(data.attitude.pitch));
writeInt16BE(packet, 6, encodeAngle(data.attitude.roll));
writeInt16BE(packet, 8, data.raw.ax);
writeInt16BE(packet, 10, data.raw.ay);
writeInt16BE(packet, 12, data.raw.az);
writeInt16BE(packet, 14, data.raw.gx);
writeInt16BE(packet, 16, data.raw.gy);
writeInt16BE(packet, 18, data.raw.gz);
uint16_t crc = calculateCrc16(packet, 20);
packet[20] = static_cast<uint8_t>(crc >> 8);
packet[21] = static_cast<uint8_t>(crc);
if (client.write(packet, sizeof(packet)) != sizeof(packet)) {
Serial.println("警告:IMU 二进制数据发送不完整");
}
}
int16_t encodeAngle(float degrees) {
long scaled = lroundf(degrees * 100.0f);
if (scaled > 32767L) scaled = 32767L;
if (scaled < -32768L) scaled = -32768L;
return static_cast<int16_t>(scaled);
}
void writeInt16BE(uint8_t* buffer, size_t offset, int16_t value) {
uint16_t encoded = static_cast<uint16_t>(value);
buffer[offset] = static_cast<uint8_t>(encoded >> 8);
buffer[offset + 1] = static_cast<uint8_t>(encoded);
}
uint16_t calculateCrc16(const uint8_t* data, size_t length) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < length; i++) {
crc ^= static_cast<uint16_t>(data[i]) << 8;
for (uint8_t bit = 0; bit < 8; bit++) {
crc = (crc & 0x8000)
? static_cast<uint16_t>((crc << 1) ^ 0x1021)
: static_cast<uint16_t>(crc << 1);
}
}
return crc;
}
void sg90Task(void* parameter) {
(void)parameter;
for (;;) {
sg90();
sendQueuedTelemetry();
vTaskDelay(1);
}
}
void sendLineData(YPR a, RawData r) {
if (telemetryQueue == nullptr) {
return;
}
TelemetryData data = {a, r};
xQueueOverwrite(telemetryQueue, &data);
}
// ==================== Arduino 主入口 ====================
void wifi_setup() {
Serial.begin(115200);
servoInit();
initWiFiAP();
initTcpServer();
telemetryQueue = xQueueCreate(1, sizeof(TelemetryData));
if (telemetryQueue == nullptr) {
Serial.println("错误:无法创建 IMU 遥测队列");
return;
}
BaseType_t taskCreated = xTaskCreatePinnedToCore(
sg90Task, "sg90", 4096, nullptr, 2, nullptr, 1);
if (taskCreated != pdPASS) {
Serial.println("错误:无法创建 SG90 通讯任务");
vQueueDelete(telemetryQueue);
telemetryQueue = nullptr;
}
}
void sg90() {
acceptClientIfNeeded(); // 1) 接入客户端
handleClientCommand(); // 2) 解析上位机指令
servoUpdateAll(); // 3) 非阻塞更新 8 路 PWM
}

11
test/README Normal file
View File

@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html