分类 C++ 下的文章

请输入图片描述

当教授链表这个知识点时,需要详细地解释链表的概念、种类、基本操作以及与数组的区别。下面详细展开讲解链表的每个知识点:

  1. 链表的概念:

链表是一种线性数据结构,由一系列节点组成,每个节点包含两部分:数据域和指针域。数据域用于存储数据,指针域指向下一个节点,通过这种指针关系连接起来。

  1. 链表的种类:

主要有单向链表、双向链表和循环链表。

单向链表:每个节点只有一个指针域,指向下一个节点。
双向链表:每个节点有两个指针域,分别指向前一个节点和后一个节点。
循环链表:最后一个节点的指针域指向第一个节点,形成一个循环。

单链表0.png
双向链表0.png
循环链表0.png

  1. 链表的基本操作:

    创建链表:动态分配内存空间创建节点,构建节点之间的指针关系。
    插入节点:在链表的指定位置插入一个新节点,需要调整指针关系。
    删除节点:从链表中删除指定位置的节点,需要调整指针关系,并释放节点的内存空间。
    查找节点:遍历链表,按照顺序查找或根据条件查找目标节点。

  2. 与数组的区别:

    链表的长度可以动态增长,而数组的长度固定。
    在链表中插入或删除节点的操作效率高,而在数组中这些操作可能涉及到大量的数据移动。
    链表不需要连续的内存空间,而数组需要。

详细展开讲解示例:

单向链表:
    从头节点开始,每个节点包含数据和指向下一个节点的指针。
    插入节点时,需要修改前一个节点的指针指向新节点,以及新节点的指针指向后一个节点。
    删除节点时,修改前一个节点的指针指向后一个节点,然后释放被删除节点的内存空间。

linked-list-add-node-at-end.PNG
linked-list-add-node-at-start.PNG
linked-list-insertat.PNG
linked-list-delete-first-node.PNG
linked-list-delete-last-node.PNG
linked-list-deleteat.PNG

双向链表:
    每个节点包含指向前一个节点和后一个节点的指针。
    插入节点时,需要修改前一个节点和后一个节点的指针指向新节点。
    删除节点时,需要修改前一个节点和后一个节点的指针指向,然后释放被删除节点的内存空间。

循环链表:
    最后一个节点的指针指向第一个节点,形成一个循环。
    插入和删除节点时,需要特别注意边界条件,以防止死循环或指针错误。

通过这些详细的讲解,学生可以更好地理解链表的结构、操作和应用场景,并能够编写简单的链表代码来解决问题。

单向链表应用示例:存储学生信息

单向链表可以用来存储学生信息,每个节点代表一个学生,包含学生的姓名、学号等信息。

#include <iostream>
#include <string>
using namespace std;

struct Student {
    string name;
    int id;
    Student* next;
};

int main() {
    // 创建链表节点
    Student* head = nullptr;
    head = new Student;
    head->name = "Alice";
    head->id = 1001;
    head->next = nullptr;

    // 添加新节点
    Student* newNode = new Student;
    newNode->name = "Bob";
    newNode->id = 1002;
    newNode->next = nullptr;
    head->next = newNode;

    // 遍历链表
    Student* current = head;
    while (current != nullptr) {
        cout << "姓名: " << current->name << ", 学号: " << current->id << endl;
        current = current->next;
    }

    // 释放内存
    while (head != nullptr) {
        Student* temp = head;
        head = head->next;
        delete temp;
    }

    return 0;
}

C++指针概念

指针是 C++
中一个非常重要的概念,它是一种特殊的变量类型,用于存储内存地址。通过指针,我们可以直接访问和操作内存中的数据,而不是通过变量名来操作。
指针的概念:

指针是一个变量,其值为另一个变量的内存地址。
指针存储的是内存地址,而不是实际的值。
指针可以指向任何类型的数据,包括基本数据类型、数组、结构体、函数等。

指针的基本操作:

取址运算符 &:用于获取变量的地址。
间接访问运算符 *:用于访问指针所指向的内存地址中存储的值。
指针赋值:将变量的地址赋给指针。
空指针:指向空地址的指针,表示不指向任何有效的内存地址。
指针运算:指针可以进行算术运算,如加法、减法等,以移动指针指向的内存位置。

指针的应用:

动态内存分配:通过 new 关键字分配动态内存,并返回指向该内存的指针。
数组和字符串操作:指针可以用于遍历数组和操作字符串。
函数参数传递:可以通过指针在函数之间传递参数,实现对变量的直接修改。
动态数据结构:如链表、树等数据结构的实现常常需要用到指针。
实现复杂数据结构:如图、图形、链表、树等复杂数据结构通常需要使用指针来建立节点之间的连接。
#include <iostream> using namespace std;

int main() {
    int num = 10; // 定义一个整数变量
    int* ptr; // 声明一个整型指针

    ptr = &num; // 将变量 num 的地址赋给指针 ptr

    cout << "变量 num 的值为: " << num << endl;
    cout << "变量 num 的地址为: " << &num << endl;
    cout << "指针 ptr 存储的地址为: " << ptr << endl;
    cout << "通过指针 ptr 访问 num 的值为: " << *ptr << endl;

    return 0; }

C++结构体自定义数据类型

在 C++ 中,struct
关键字用于定义一个结构体(structure),结构体是一种用户自定义的数据类型,用于组合多个不同类型的数据成员。结构体可以包含各种类型的数据,包括整数、浮点数、字符、字符串、指针等。

在给定的例子中,定义了一个名为 Student 的结构体,其中包含了三个数据成员:

string name:用于存储学生的姓名,类型为 string。
int id:用于存储学生的学号,类型为 int。
Student* next:用于指向下一个学生的指针,类型为 Student*,表示该成员存储的是指向 Student 结构体的指针。

这个结构体的定义描述了一个学生的基本信息以及指向下一个学生的指针,可以用于构建链表数据结构,其中每个节点表示一个学生,通过 next
指针连接起来,形成一个链表。

双向链表应用示例:实现浏览器的前进后退功能

双向链表可以用来实现浏览器的前进后退功能,每个节点代表一个网页,包含指向前一个网页和后一个网页的指针。

#include <iostream>
#include <string>
using namespace std;

struct WebPage {
    string url;
    WebPage* prev;
    WebPage* next;
};

int main() {
    // 创建链表节点
    WebPage* current = nullptr;
    current = new WebPage;
    current->url = "www.google.com";
    current->prev = nullptr;

    // 添加新节点
    WebPage* newPage = new WebPage;
    newPage->url = "www.example.com";
    newPage->prev = current;
    newPage->next = nullptr;
    current->next = newPage;

    // 后退到前一个网页
    if (current->prev != nullptr) {
        current = current->prev;
        cout << "当前网页: " << current->url << endl;
    }

    // 前进到下一个网页
    if (current->next != nullptr) {
        current = current->next;
        cout << "当前网页: " << current->url << endl;
    }

    // 释放内存
    delete newPage;
    delete current;

    return 0;
}
## 循环链表应用示例:实现游戏中的循环列表

循环链表可以用于实现游戏中的循环列表,比如玩家的回合顺序,最后一个节点的指针指向第一个节点,形成一个循环。

include

using namespace std;

struct Player {

string name;
Player* next;

};

int main() {

// 创建循环链表
Player* head = nullptr;
head = new Player;
head->name = "Player1";
head->next = nullptr;

Player* second = new Player;
second->name = "Player2";
second->next = nullptr;
head->next = second;

Player* third = new Player;
third->name = "Player3";
third->next = nullptr;
second->next = third;
third->next = head; // 最后一个节点指向头节点,形成循环

// 遍历循环链表
Player* current = head;
do {
    cout << "当前玩家: " << current->name << endl;
    current = current->next;
} while (current != head);

// 释放内存
delete head;
delete second;
delete third;

return 0;

}

## 将前面成绩管理系统,温度记录,电影票售卖系统使用链表
#### 成绩管理系统使用链表改写

include

include

using namespace std;

struct Student {

string name;
int score;
Student* next;

};

int main() {

Student* head = nullptr;
Student* current = nullptr;

// 添加学生成绩
for (int i = 0; i < 5; ++i) {
    Student* newStudent = new Student;
    cout << "请输入第 " << i + 1 << " 个学生的姓名和成绩: ";
    cin >> newStudent->name >> newStudent->score;
    newStudent->next = nullptr;

    if (head == nullptr) {
        head = newStudent;
        current = head;
    } else {
        current->next = newStudent;
        current = newStudent;
    }
}

// 显示学生成绩
cout << "学生成绩列表:" << endl;
current = head;
while (current != nullptr) {
    cout << "姓名: " << current->name << ", 成绩: " << current->score << endl;
    current = current->next;
}

// 释放内存
current = head;
while (current != nullptr) {
    Student* temp = current;
    current = current->next;
    delete temp;
}

return 0;

}

温度记录器使用链表改写:

#include <iostream>
using namespace std;

struct Temperature {
    int temp;
    Temperature* next;
};

int main() {
    const int DAYS = 7;
    Temperature* head = nullptr;
    Temperature* current = nullptr;

    // 输入每天的温度
    for (int i = 0; i < DAYS; ++i) {
        Temperature* newTemp = new Temperature;
        cout << "请输入第 " << i + 1 << " 天的温度: ";
        cin >> newTemp->temp;
        newTemp->next = nullptr;

        if (head == nullptr) {
            head = newTemp;
            current = head;
        } else {
            current->next = newTemp;
            current = newTemp;
        }
    }

    // 显示每天的温度
    cout << "一周内的温度记录:" << endl;
    current = head;
    while (current != nullptr) {
        cout << current->temp << " ";
        current = current->next;
    }
    cout << endl;

    // 释放内存
    current = head;
    while (current != nullptr) {
        Temperature* temp = current;
        current = current->next;
        delete temp;
    }

    return 0;
}

电影票售卖系统使用链表改写:

#include <iostream>
#include <string>
using namespace std;

struct Movie {
    string name;
    int ticketSales;
    Movie* next;
};

int main() {
    const int NUM_MOVIES = 3;
    Movie* head = nullptr;
    Movie* current = nullptr;

    // 输入每部电影的售票数量
    for (int i = 0; i < NUM_MOVIES; ++i) {
        Movie* newMovie = new Movie;
        cout << "请输入电影 " << i + 1 << " 的名称和售票数量: ";
        cin >> newMovie->name >> newMovie->ticketSales;
        newMovie->next = nullptr;

        if (head == nullptr) {
            head = newMovie;
            current = head;
        } else {
            current->next = newMovie;
            current = newMovie;
        }
    }

    // 统计总票数和最畅销电影
    int totalSales = 0;
    string bestMovieName;
    int maxSales = 0;
    current = head;
    while (current != nullptr) {
        totalSales += current->ticketSales;
        if (current->ticketSales > maxSales) {
            maxSales = current->ticketSales;
            bestMovieName = current->name;
        }
        current = current->next;
    }

    // 显示统计结果
    cout << "总售票数量为: " << totalSales << endl;
    cout << "最畅销电影是: " << bestMovieName << endl;

    // 释放内存
    current = head;
    while (current != nullptr) {
        Movie* temp = current;
        current = current->next;
        delete temp;
    }

    return 0;
}
这些示例展示了如何使用链表数据结构来重新实现之前的成绩管理系统、温度记录器和电影票售卖系统。通过链表,我们可以动态地添加、删除和管理数据,使代码更加灵活和易于扩展。

请输入图片描述

在第一节中,我们将介绍数组的概念、声明和初始化方法,以及演示数组的基本操作。为了使教学内容更加生动有趣,我们可以通过一些比较强烈的例子来说明数组的应用。

教学内容:

#### 数组的概念和特点:

  • 数组是一种线性数据结构,由相同类型的元素组成,这些元素在内存中是连续存储的。
  • 数组的长度是固定的,在声明时就确定了,不能动态改变。

#### 声明和初始化数组:

  • 示范如何声明数组,并介绍如何初始化数组。
  • 例如:int scores[5]; 声明了一个包含5个整数的数组。
    #### 数组的基本操作示例:
  • 访问元素:通过索引访问数组中的元素。
  • 修改元素:可以直接通过索引修改数组中的元素。
  • 遍历数组:使用循环结构遍历数组中的所有元素。

应用例子:

成绩管理系统:

让学生设想一个成绩管理系统,其中需要存储学生的成绩数据。使用数组来存储学生的成绩,每个元素代表一个学生的成绩。
示范如何通过数组实现添加、删除、查找学生成绩的功能。

温度记录器:

想象一个温度记录器,记录每天的气温。使用数组来存储每天的温度数据,每个元素代表一天的温度。
示范如何使用数组计算一周内的平均温度、最高温度和最低温度。

电影票售卖系统:

设想一个电影票售卖系统,记录每天不同电影的售票情况。使用数组来存储每部电影的售票数量,每个元素代表一部电影的售票数量。
示范如何通过数组实现统计每部电影的总票数、最畅销电影等功能。

通过这些应用例子,学生可以更好地理解数组的概念和用途,同时激发他们对学习的兴趣。

成绩管理系统

参考代码
    #include <iostream>
    using namespace std;
    
    int main() {
        const int MAX_STUDENTS = 5;
        int scores[MAX_STUDENTS];
    
        // 添加学生成绩
        for (int i = 0; i < MAX_STUDENTS; ++i) {
            cout << "请输入第 " << i + 1 << " 个学生的成绩: ";
            cin >> scores[i];
        }
    
        // 显示学生成绩
        cout << "学生成绩列表:" << endl;
        for (int i = 0; i < MAX_STUDENTS; ++i) {
            cout << "第 " << i + 1 << " 个学生的成绩为: " << scores[i] << endl;
        }
    
        return 0;
    }

温度记录器

参考代码
    #include <iostream>
    using namespace std;
    
    int main() {
        const int DAYS = 7;
        int temperatures[DAYS];
        int sum = 0;
    
        // 输入每天的温度
        for (int i = 0; i < DAYS; ++i) {
            cout << "请输入第 " << i + 1 << " 天的温度: ";
            cin >> temperatures[i];
            sum += temperatures[i];
        }
    
        // 计算平均温度
        double average = static_cast<double>(sum) / DAYS;
        cout << "一周内的平均温度为: " << average << " 度" << endl;
    
        // 查找最高和最低温度
        int maxTemp = temperatures[0];
        int minTemp = temperatures[0];
        for (int i = 1; i < DAYS; ++i) {
            if (temperatures[i] > maxTemp) {
                maxTemp = temperatures[i];
            }
            if (temperatures[i] < minTemp) {
                minTemp = temperatures[i];
            }
        }
        cout << "最高温度为: " << maxTemp << " 度" << endl;
        cout << "最低温度为: " << minTemp << " 度" << endl;
    
        return 0;
    }

电影票售卖系统

参考代码
    #include <iostream>
    using namespace std;
    
    int main() {
        const int NUM_MOVIES = 3;
        int ticketSales[NUM_MOVIES] = {0};
    
        // 模拟售票过程
        for (int i = 0; i < NUM_MOVIES; ++i) {
            cout << "请输入电影 " << i + 1 << " 的售票数量: ";
            cin >> ticketSales[i];
        }
    
        // 统计总票数和最畅销电影
        int totalSales = 0;
        int bestMovieIndex = 0;
        for (int i = 0; i < NUM_MOVIES; ++i) {
            totalSales += ticketSales[i];
            if (ticketSales[i] > ticketSales[bestMovieIndex]) {
                bestMovieIndex = i;
            }
        }
    
        // 显示统计结果
        cout << "总售票数量为: " << totalSales << endl;
        cout << "最畅销电影是电影 " << bestMovieIndex + 1 << endl;
    
        return 0;
    }

请输入图片描述

以下是一个简单的教学计划,旨在向初中生介绍基础的数据结构概念,并使用C++语言进行说明和示范。这个计划将包括讲解线性数据结构(数组、链表)和非线性数据结构(栈、队列)。

教学计划

第一节:数组(Array)

  • 介绍数组的概念和特点。
  • 讲解如何声明和初始化数组。
  • 演示数组的基本操作,如访问元素、修改元素和遍历数组。
  • 讨论数组的优缺点及应用场景。

第二节:链表(Linked List)

  • 介绍链表的概念和基本类型(单链表、双链表)。
  • 演示如何定义和操作单链表。
  • 讨论链表与数组的区别,以及链表的优势。
  • 分析链表的插入、删除和查找操作的时间复杂度。

第三节:栈(Stack)

  • 介绍栈的概念和特点。
  • 演示如何使用栈实现简单的表达式求值和括号匹配。
  • 讨论栈的应用场景,如函数调用栈和浏览器的前进后退功能。

第四节:队列(Queue)

  • 介绍队列的概念和特点。
  • 演示如何使用队列解决问题,如模拟排队和广度优先搜索(BFS)算法。
  • 讨论队列的应用场景,如任务调度和消息传递。

辅助资源和活动

  • 提供课堂演示和实践练习,让学生动手实践数据结构的基本操作。
  • 组织小组讨论,让学生分享自己对数据结构的理解和应用场景。
  • 分发资料和练习题,供学生课后复习和巩固知识。

评估方式

  • 课堂参与度:评估学生在课堂上的提问和回答情况。
  • 实践练习:评估学生对数据结构基本操作的掌握程度。
  • 课后作业:布置相关的编程作业,检验学生对所学内容的理解和应用能力。

结语

通过这个教学计划,学生将能够了解基本的数据结构概念,并通过C++语言实践掌握数组、链表、栈和队列等常用数据结构。同时,他们也将学会如何分析和应用这些数据结构解决实际问题。

开源硬件Arduino社群各种创意应用层出不穷,学习门槛越来越地,今天我们就尝试从小白视角,不求甚解动手构建一个自平衡机器人

请输入图片描述

材料收集

开源社区这类资料特别丰富,可以网络搜索从扫盲到实际动手操作都有很详细的讯息
从小白的角度,我们透过各种搜索,阅读,首先需要知道做这样的一个自平衡机器人,需要哪些材料? =》这样好从淘宝购买一些物美价廉的东西:)

经过若干次网络搜索,阅读,基本了解构建这样的一个自平衡机器人需要的材料如下:

  • Arduino控制板这里选用Uno or Nano,代码一模一样,唯一差异就是pin接口,以及连线;淘宝选国产的,不贵,才十几块
  • 陀螺仪MPU6050,物美价廉,淘宝才几块钱
  • 双轴减速电机,淘宝上一大堆,黄色的那个电机,我选择1:48减速比的电机
  • 轮子,一般和上面的双轴电机配套一起买,一两块钱
  • L298N电机驱动模块,可以驱动2个直流电机,同时可以输出5V电压给Arduino控制板供电;这样使用一路供电(比如2节或者3节16850电池),给电机与控制板同时供电
  • 16850锂电池(2节,3节,甚至4节也可以)
  • 16850电池盒(最好买带开关的盒子)
  • 框架材料,我通常选择雪弗板(一般购买20cm*20cm大小,厚度>5mm,一般选8mm或者10mm)便于美工到切割
  • 热熔胶
  • 电烙铁可能需要(焊接)
  • 美工刀

需要的软件

陀螺仪MPU6050
mpu6050
带有3轴加速度计以及三轴陀螺仪的模块,以及能够处理复杂的运动融合算法的数字运动处理器(DMP)

L298N电机驱动模块
电机驱动模块

双轴减速电机
电机

16850锂电池
电池

这个Arduino机器人DIY经常使用的电池,价格不贵,容量一般2000多毫安, 使用该电池可以非常方便满足3.7伏要求,一般我使用2节
可以考虑购买一个带开关的电池盒,方便重复使用,免得焊线

制作框架
对于这个项目,我使用雪弗板切割而成,加工起来也比较方便,强度对于这小车来说基本足够
车身
我看到网上很多教程建议将电池,电机等重物保持再底部,有一定道理,不过我把电池(最终的部分)放置在最上面,依然保持平衡很好(关键是控制参数调试的恰当)

电路图

截图 2023-12-15 16-35-00.png

该机器人的接线很简单, mpu6050测量运动方向上的倾斜,Arduino使用L298N电机驱动设置电机速度以克服倾斜

这里我们使用的mpu6050传感器,经济可靠。 其精度以及同时测量所有三轴旋转和运动的能力时其非常适合用于自平衡机器人。 L298N也是一款非常适合在此使用的低成本双H桥电机驱动器

MPU6050我放置在顶部,其Z轴朝向地球,当然我们可以将该传感器放置在框架中的任何位置。方向可以根据框架和设计进行修改。为此需要更改代码

由于我们使用L298N作为电机驱动器,同时也作为Arduino的电源,所以放置电源跳线,这样我们就可以使用+5v引脚作为电源。

Arduino代码

需要安装依赖的库

  • PID_v1
  • LMotorController
  • I2Cdev
  • MPU6050

完整代码:

/*
* Arduino Uno
* L298N
* HC-06 BT module
* MPU6050
* by 麦克不能飞, admin@dazuicheng.com
* www.dazuicheng.com
* 1. 如何可视化PID tuning??
* 2.勉强可以工作,但是抗扰动能力稍微有点弱?
*/
#include <PID_v1.h>
#include <LMotorController.h>
#include "I2Cdev.h"
#include "MPU6050_6Axis_MotionApps20.h"
#include <SoftwareSerial.h>


SoftwareSerial bluetoothSerial(4, 3); // RX, TX for bluetooth module control(HC-06 here)4和3分别表示蓝牙模块的rx,tx

#define MIN_ABS_SPEED 20

MPU6050 mpu;

// MPU control/status vars
bool dmpReady = false; // set true if DMP init was successful
uint8_t mpuIntStatus; // holds actual interrupt status byte from MPU
uint8_t devStatus; // return status after each device operation (0 = success, !0 = error)
uint16_t packetSize; // expected DMP packet size (default is 42 bytes)
uint16_t fifoCount; // count of all bytes currently in FIFO
uint8_t fifoBuffer[64]; // FIFO storage buffer

// orientation/motion vars
Quaternion q; // [w, x, y, z] quaternion container
VectorFloat gravity; // [x, y, z] gravity vector
float ypr[3]; // [yaw, pitch, roll] yaw/pitch/roll container and gravity vector

//PID,
double originalSetpoint = 179.60;
double setpoint = originalSetpoint;
double movingAngleOffset = 0.1;
double input, output;

//my setting, cost a lot of time to fine tuning those parameters,adjust to fit your own design
double Kp = 39; //45; 地毯上可以工作,地板上有点jitter   
double Kd = 1.6; //1.8
double Ki = 400; //400;这貌似可以不用这么大,还可以优化?
PID pid(&input, &output, &setpoint, Kp, Ki, Kd, DIRECT);

double motorSpeedFactorLeft = 0.8;
double motorSpeedFactorRight = 0.7;
//MOTOR CONTROLLER,因为可能电机+-接线弄反了,所这里IN1,IN2定义与实际接线故意反了(实际是IN1->6, IN2->7)
int ENA = 5;
int IN1 = 7;
int IN2 = 6;
int IN3 = 9;
int IN4 = 8;
int ENB = 10;
LMotorController motorController(ENA, IN1, IN2, ENB, IN3, IN4, motorSpeedFactorLeft, motorSpeedFactorRight);

volatile bool mpuInterrupt = false; // indicates whether MPU interrupt pin has gone high
void dmpDataReady()
{
  mpuInterrupt = true;
}


void setup()
{
  Serial.begin(115200);
  bluetoothSerial.begin(9600);

  // join I2C bus (I2Cdev library doesn't do this automatically)
  #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
  Wire.begin();
  TWBR = 24; // 400kHz I2C clock (200kHz if CPU is 8MHz)
  #elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
  Fastwire::setup(400, true);
  #endif

  mpu.initialize();

  devStatus = mpu.dmpInitialize();
  // 这部分偏移,只需要校正一次,使用mpu6050库里面的例子:IMU_Zero校正一下,记录这些偏移,靠不到这里
  mpu.setXAccelOffset(314);//-4259
  mpu.setYAccelOffset(1177);//749
  mpu.setZAccelOffset(931); // 1151,1688 factory default for my test chip
  mpu.setXGyroOffset(-12);//31
  mpu.setYGyroOffset(184);//557
  mpu.setZGyroOffset(-1);//34

  // make sure it worked (returns 0 if so)
  if (devStatus == 0) {
    // turn on the DMP, now that it's ready
    mpu.setDMPEnabled(true);

    // enable Arduino interrupt detection
    attachInterrupt(0, dmpDataReady, RISING);
    mpuIntStatus = mpu.getIntStatus();

    // set our DMP Ready flag so the main loop() function knows it's okay to use it
    dmpReady = true;

    // get expected DMP packet size for later comparison
    packetSize = mpu.dmpGetFIFOPacketSize();

    //setup PID
    pid.SetMode(AUTOMATIC);
    pid.SetSampleTime(10);
    pid.SetOutputLimits(-255, 255); 
  } else {
    // ERROR!
    // 1 = initial memory load failed
    // 2 = DMP configuration updates failed
    // (if it's going to break, usually the code will be 1)
    Serial.print(F("DMP Initialization failed (code "));
    Serial.print(devStatus);
    Serial.println(F(")"));
  }
}



void loop()
{
  //randomSeed(analogRead(0)); // 使用模拟引脚0的读取值作为随机数种子

  String received_from_bt = "";
  while (bluetoothSerial.available()) {
    char data = bluetoothSerial.read();
    received_from_bt += data;  
    // 在这里可以添加处理接收到的数据的代码
  }
  if (received_from_bt.length() > 0) {
    Serial.print("Received from Bluetooth: ");
    Serial.println(received_from_bt);
  }
  // if programming failed, don't try to do anything
  if (!dmpReady) return;
  //Serial.println(setpoint);

  // wait for MPU interrupt or extra packet(s) available
  while (!mpuInterrupt && fifoCount < packetSize) {
    //no mpu data - performing PID calculations and output to motors 
    pid.Compute();
    Serial.print(setpoint); Serial.print(",");Serial.print(input); Serial.print(","); Serial.println(output);
    motorController.move(output, MIN_ABS_SPEED);
}

  // reset interrupt flag and get INT_STATUS byte
  mpuInterrupt = false;
  mpuIntStatus = mpu.getIntStatus();

  // get current FIFO count
  fifoCount = mpu.getFIFOCount();

  // check for overflow (this should never happen unless our code is too inefficient)
  if ((mpuIntStatus & 0x10) || fifoCount == 1024) {
    // reset so we can continue cleanly
    mpu.resetFIFO();
    Serial.println(F("FIFO overflow!"));
    // otherwise, check for DMP data ready interrupt (this should happen frequently)
  } else if (mpuIntStatus & 0x02) {
    // wait for correct available data length, should be a VERY short wait
    while (fifoCount < packetSize) fifoCount = mpu.getFIFOCount();
    // read a packet from FIFO
    mpu.getFIFOBytes(fifoBuffer, packetSize);
    // track FIFO count here in case there is > 1 packet available
    // (this lets us immediately read more without waiting for an interrupt)
    fifoCount -= packetSize;
    mpu.dmpGetQuaternion(&q, fifoBuffer);
    mpu.dmpGetGravity(&gravity, &q);
    mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
    input = ypr[1] * 180/M_PI + 180;
  }
}

平衡测试与调整

这种类型的自平衡机器人是倒立摆,类似于在手指上平衡一根棍子。在这里,车轮的旋转提供了防止跌落的平衡力。为了平衡这个机器人,你必须配置 PID 值,直到机器人达到稳定的位置和恢复能力。

为了使此任务变得简单,代码使用了 Arduino PID 的现成库。要根据您的设计调整 PID,您必须手动输入 P、I、D 值,并通过将代码上传到 Arduino 来检查稳定性(如上图中代码的以下部分)。

方法是先将Ki、Kd都设置为0,然后输入Kp的值,然后上传代码。经过几次尝试后,机器人在一段时间内保持平衡,但滚动时出现过度振荡,此时必须在不干扰 Kp 值的情况下设置 Kd 值。设置 Kd 以便发生最小振荡,这与其他两个一样手动调整 Ki。

此外,您还可以设置上图所示角度的设定值和偏移。但在许多情况下并不需要它,并且仅当陀螺仪有偏移时才进行设置。

经过这个反复试验阶段后,您将为您的机器人找到合适的值。这个过程有点棘手,如果您是控制系统的初学者和新手,有时需要几个小时才能找到完美的值。

8-BT7Tz6YA20nvp0M.png

> Bubble Sort

    #include <iostream>
    
    void swap(int &a, int &b) {
        int temp = a;
        a = b;
        b = temp;
    }
    
    void bubbleSort(int arr[], int size) {
        for (int i = 0; i < size - 1; ++i) {
            for (int j = 0; j < size - i - 1; ++j) {
                // 如果相邻元素逆序,则交换它们
                if (arr[j] > arr[j + 1]) {
                    swap(arr[j], arr[j + 1]);
                }
            }
        }
    }
    
    void printArray(int arr[], int size) {
        for (int i = 0; i < size; ++i) {
            std::cout << arr[i] << " ";
        }
        std::cout << std::endl;
    }
    
    int main() {
        const int size = 6;
        int arr[size] = {64, 25, 12, 22, 11, 1};
    
        std::cout << "原始数组: ";
        printArray(arr, size);
    
        bubbleSort(arr, size);
    
        std::cout << "排序后数组: ";
        printArray(arr, size);
    
        return 0;
    }

## > Insert Sort

#include <iostream>

void insertionSort(int arr[], int size) {
    for (int i = 1; i < size; ++i) {
        int key = arr[i];
        int j = i - 1;

        // 将 arr[0..i-1] 中大于 key 的元素向右移动
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            --j;
        }

        // 将 key 插入到正确的位置
        arr[j + 1] = key;
    }
}

void printArray(int arr[], int size) {
    for (int i = 0; i < size; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

int main() {
    const int size = 6;
    int arr[size] = {64, 25, 12, 22, 11, 1};

    std::cout << "原始数组: ";
    printArray(arr, size);

    insertionSort(arr, size);

    std::cout << "排序后数组: ";
    printArray(arr, size);

    return 0;
}

> Quick Sort

#include <iostream>

void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}

int partition(int arr[], int low, int high) {
    // 选择最右边的元素作为基准
    int pivot = arr[high];
    int i = low - 1;  // 小于等于基准的元素的最右索引

    for (int j = low; j < high; ++j) {
        // 如果当前元素小于或等于基准,则将其交换到最左边
        if (arr[j] <= pivot) {
            ++i;
            swap(arr[i], arr[j]);
        }
    }

    // 将基准元素放到正确的位置
    swap(arr[i + 1], arr[high]);
    return i + 1;  // 返回基准元素的索引
}

void quickSort(int arr[], int low, int high) {
    if (low < high) {
        // 对数组进行分割,获取基准元素的索引
        int pivotIndex = partition(arr, low, high);

        // 递归地对左右子数组进行排序
        quickSort(arr, low, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, high);
    }
}

void printArray(int arr[], int size) {
    for (int i = 0; i < size; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

int main() {
    const int size = 6;
    int arr[size] = {64, 25, 12, 22, 11, 1};

    std::cout << "原始数组: ";
    printArray(arr, size);

    quickSort(arr, 0, size - 1);

    std::cout << "排序后数组: ";
    printArray(arr, size);

    return 0;
}