《Arduino 手册(思路与案例)》栏目介绍:
在电子制作与智能控制的应用领域:广泛涉及了Arduino BLDC、Arduino CNC、Arduino ESP32 SPP、Arduino FreeRTOS、Arduino FOC、Arduino GRBL、Arduino HTTP、Arduino HUB75、Arduino IoT Cloud、Arduino JSON、Arduino LCD、Arduino OLED、Arduino LVGL、Arduino PID 及 Arduino TFT 等方面的相关拓展思路和众多参考案例。本专栏目前博客近2300篇。
https://blog.csdn.net/weixin_41659040/category_12422453.html
Arduino是一个开放源码的电子原型平台,它可以让你用简单的硬件和软件来创建各种互动的项目。Arduino的核心是一个微控制器板,它可以通过一系列的引脚来连接各种传感器、执行器、显示器等外部设备。Arduino的编程是基于C/C++语言的,你可以使用Arduino IDE(集成开发环境)来编写、编译和上传代码到Arduino板上。Arduino还有一个丰富的库和社区,你可以利用它们来扩展Arduino的功能和学习Arduino的知识。
Arduino的特点是:
1、开放源码:Arduino的硬件和软件都是开放源码的,你可以自由地修改、复制和分享它们。
2、易用:Arduino的硬件和软件都是为初学者和非专业人士设计的,你可以轻松地上手和使用它们。
3、便宜:Arduino的硬件和软件都是非常经济的,你可以用很低的成本来实现你的想法。
4、多样:Arduino有多种型号和版本,你可以根据你的需要和喜好来选择合适的Arduino板。
5、创新:Arduino可以让你用电子的方式来表达你的创意和想象,你可以用Arduino来制作各种有趣和有用的项目,如机器人、智能家居、艺术装置等。
一个简单的 Web 控制 ESP32 机器人,带有摄像头。由 LiPo 电池供电,使用 4 个 n20 电机。
用品与材料
1 个 ESP32-CAM 微控制器
4x N20 电机 (60RPM)(全球速卖通))
2x MX 1508 有刷直流电机驱动器(有时也称为迷你 L298 驱动器)(Amazon UK)(全球速卖通))
1x 7.4v Lipo 电池(亚马逊英国))
1 个 JST 连接器(Amazon UK)
1x Mini3060 DC-DC 降压模块(Amazon UK)(全球速卖通))
1 个 160 度 OVO2640 75mm 长的模块。(亚马逊英国)
1x 60x80mm 原型 / pef 板(我用的是双面,但单面应该可以)(全球速卖通)
一个 40 针的公板接头条。
母跳线选择。(提供 40 个每种的套件应该足够了,因为有些会被切成两半)
2 个 8 针母板接头(用于插入 esp32)
2 针摇杆开关(内部尺寸应约为 18.6 毫米 x 11.8 毫米,如果您的开关需要更大的孔,您可以将开口锉磨或打磨得更大或调整印刷品)(亚马逊英国))
12 个 M3x8 螺栓
4 个 M2x6 螺栓(如果您没有 2 个就足够了)
带有外露端的跳线,用于在配电板上创建连接
预计建造成本(2025 年 4 月):35-45 英镑/40-50 欧元/50-60 美元/60-80元人民币(但由于关税,可能会更多),但是您可能需要购买 2、5、10 等套装的某些零件,这意味着它可能会花费更多,但您有备件,甚至可能有足够的零件来建造 2 个!您可能会在其他地方找到更便宜的零件。
第 1 步:3D 打印组件
在 PLA 中打印 Wheels、bottom、mid、top、shell 部分。这总共需要大约 7-8 小时,但可能需要更长的时间,具体取决于您的打印机。我建议打印底部、车轮、轮胎和 N20 电机外壳,以便在打印外壳和上部时更快地开始项目。
使用 95A TPU 打印轮胎(确保在正确的温度下打印以减少任何瑕疵)
第 2 步:焊接电机连接
您的电机可能带有预焊接的电线,但我发现它们可能很弱并且几乎不费吹灰之力就会折断。我将其中一根母对母跨接电缆切成两半,将非连接器端剥去约 5-10 毫米,然后将其焊接到电机上。
焊接到电机引脚时要小心,不要焊接得太热,否则可能会损坏电机外壳和连接点。对所有 4 个电机执行此作。我发现最好用黑色和红色电缆焊接它们,并将它们全部焊接到相同的方向(你可以通过电机上的齿轮看出)。
第 3 步:安装电机 + 轮子
在底壳上,您会看到中间有 8 个螺丝孔,这是 4 个电机外壳拧入到位的位置。
首先将每个电机插入 n20 电机外壳杆端,然后将背面卡入到位。
对于 4 个轮子将轮胎重叠在一个车轮上,它应该很紧,几乎看起来不合适,但 95a 的伸展性刚好可以放在车轮上。这样做是为了确保它保持原状。现在将每个轮子连接到 N20 电机。如果您查看 4 个部分之一的车轮,它会有一个轻微的颠簸,这是 N20 轴的平坦部分应该面向的一侧,并将车轮推到电机上。根据你的打印方式,有些可能几乎没有力量继续,但有些可能需要更多的鼓励。如果它太容易滑入和滑出,那么您可能需要检查您的打印设置。
现在拧入每个外壳,并将轮子连接到底座上。外壳应与中间部分的螺丝柱位于同一侧。
第 4 步:将接头焊接到电机驱动器上
您可以直接在电机和驱动器之间焊接电线,但在尝试解决问题时可能会导致问题。我发现只需焊接一些接头就更容易了,这样我就可以连接或断开跳线以帮助发现任何问题。焊接所有 9 个接头。您应该能够为每个 2 针孔一组 2 个来执行它们。同样,不要焊接得太热,否则可能会熔化将引脚固定到位的塑料。
第 5 步:焊接配电板
图为配电板上需要如何连接。我的焊接不是最好的,但应该让您大致了解它们应该如何协同工作。lipo 需要为电机驱动器的 2 个电源连接和直流降压转换器的连接供电。转换器的另一侧需要连接到 ESP32 上的 5V 和 GND。然后将 8 个接头(2 行,每行 4 行)连接到 GPIO。
第 6 步:连接所有内容
一旦打印出中间部分,您就可以将每个电机的电缆向上穿过提供的小孔,如图所示插入电机驱动器,并将电池放在中间的部分…确保您也拧紧了中段。拧紧电源板,使连接点朝上。将电源和接地连接到每个电机控制器,然后连接到每根控制电缆。
第 7 步:对 ESP32 CAM 进行编程
将代码上传到开发板,但将您的 SSID 名称和密码插入相关部分。加载后,使用 115200 波特率的串行监视器查看设备将使用的 IP。(如果未在路由器中分配静态 IP,这可能会发生变化,但您也可以通过查看连接的设备来检查路由器上的 IP)
//Adapted Code based on project by NT Tronix https://nabatechblog.com/esp32-cam-wifi-camera-with-pan-tilt-control-diy-smart-surveillance/
#include "esp_camera.h"
#include <WiFi.h>
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "fb_gfx.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "esp_http_server.h"
// Replace with your network credentials
const char* ssid = "INSERTSSIDHERE";
const char* password = "INSERTPASSWORDHERE";
#define PART_BOUNDARY "123456789000000000000987654321"
#define CAMERA_MODEL_AI_THINKER
#if defined(CAMERA_MODEL_AI_THINKER)
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
#else
#error "Camera model not selected"
#endif
// Motor control pins
#define MOTOR_LEFT_IN1 14
#define MOTOR_LEFT_IN2 15
#define MOTOR_RIGHT_IN1 12
#define MOTOR_RIGHT_IN2 13
// Streaming constants
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
httpd_handle_t camera_httpd = NULL;
httpd_handle_t stream_httpd = NULL;
// Simple web UI
static const char PROGMEM INDEX_HTML[] = R"rawliteral(
<html>
<head><title>ESP32-CAM Remote Robot</title>
<style>
body { font-family: Arial; text-align: center; }
.button {
background-color: #2f4468; border: none; color: white;
padding: 10px 20px; font-size: 18px; margin: 6px 3px;
cursor: pointer; touch-action: manipulation;
}
</style>
</head>
<body>
<h1>ESP32-CAM Motor Control</h1>
<img src="" id="photo">
<div>
<button class="button" ontouchstart="send('up')" ontouchend="send('stop')"
onmousedown="send('up')" onmouseup="send('stop')">Up</button><br>
<button class="button" ontouchstart="send('left')" ontouchend="send('stop')"
onmousedown="send('left')" onmouseup="send('stop')">Left</button>
<button class="button" ontouchstart="send('stop')" ontouchend="send('stop')"
onmousedown="send('stop')" onmouseup="send('stop')">Stop</button><br>
<button class="button" ontouchstart="send('right')" ontouchend="send('stop')"
onmousedown="send('right')" onmouseup="send('stop')">Right</button><br>
<button class="button" ontouchstart="send('down')" ontouchend="send('stop')"
onmousedown="send('down')" onmouseup="send('stop')">Down</button>
</div>
<script>
function send(cmd) {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/action?go=" + cmd, true);
xhr.send();
}
window.onload = () => document.getElementById("photo").src = window.location.href.slice(0, -1) + ":81/stream";
</script>
</body>
</html>
)rawliteral";
// Motor control helper
void stopMotors() {
digitalWrite(MOTOR_LEFT_IN1, LOW);
digitalWrite(MOTOR_LEFT_IN2, LOW);
digitalWrite(MOTOR_RIGHT_IN1, LOW);
digitalWrite(MOTOR_RIGHT_IN2, LOW);
}
void moveBackward() {
digitalWrite(MOTOR_LEFT_IN1, HIGH);
digitalWrite(MOTOR_LEFT_IN2, LOW);
digitalWrite(MOTOR_RIGHT_IN1, HIGH);
digitalWrite(MOTOR_RIGHT_IN2, LOW);
}
void moveForward() {
digitalWrite(MOTOR_LEFT_IN1, LOW);
digitalWrite(MOTOR_LEFT_IN2, HIGH);
digitalWrite(MOTOR_RIGHT_IN1, LOW);
digitalWrite(MOTOR_RIGHT_IN2, HIGH);
}
void turnRight() {
digitalWrite(MOTOR_LEFT_IN1, LOW);
digitalWrite(MOTOR_LEFT_IN2, HIGH);
digitalWrite(MOTOR_RIGHT_IN1, HIGH);
digitalWrite(MOTOR_RIGHT_IN2, LOW);
}
void turnLeft() {
digitalWrite(MOTOR_LEFT_IN1, HIGH);
digitalWrite(MOTOR_LEFT_IN2, LOW);
digitalWrite(MOTOR_RIGHT_IN1, LOW);
digitalWrite(MOTOR_RIGHT_IN2, HIGH);
}
static esp_err_t index_handler(httpd_req_t *req) {
httpd_resp_set_type(req, "text/html");
return httpd_resp_send(req, (const char *)INDEX_HTML, strlen(INDEX_HTML));
}
static esp_err_t stream_handler(httpd_req_t *req){
camera_fb_t * fb = NULL;
esp_err_t res = ESP_OK;
size_t _jpg_buf_len = 0;
uint8_t * _jpg_buf = NULL;
char * part_buf[64];
res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
if(res != ESP_OK){
return res;
}
while(true){
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed");
res = ESP_FAIL;
} else {
if(fb->format != PIXFORMAT_JPEG){
bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
esp_camera_fb_return(fb);
if(!jpeg_converted){
Serial.println("JPEG compression failed");
res = ESP_FAIL;
}
} else {
_jpg_buf_len = fb->len;
_jpg_buf = fb->buf;
}
}
if(res == ESP_OK){
size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
}
if(res == ESP_OK){
res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
}
if(res == ESP_OK){
res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
}
if(fb){
esp_camera_fb_return(fb);
}
if(res != ESP_OK){
break;
}
}
return res;
}
static esp_err_t cmd_handler(httpd_req_t *req){
char* buf;
size_t buf_len;
char variable[32] = {0,};
buf_len = httpd_req_get_url_query_len(req) + 1;
if (buf_len > 1) {
buf = (char*)malloc(buf_len);
if(!buf){
httpd_resp_send_500(req);
return ESP_FAIL;
}
if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
httpd_query_key_value(buf, "go", variable, sizeof(variable));
}
free(buf);
} else {
httpd_resp_send_404(req);
return ESP_FAIL;
}
if(!strcmp(variable, "up")) {
moveForward();
} else if(!strcmp(variable, "down")) {
moveBackward();
} else if(!strcmp(variable, "left")) {
turnLeft();
} else if(!strcmp(variable, "right")) {
turnRight();
} else if(!strcmp(variable, "stop")) {
stopMotors();
} else {
httpd_resp_send_404(req);
return ESP_FAIL;
}
return httpd_resp_send(req, NULL, 0);
}
void startCameraServer(){
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = 80;
httpd_uri_t index_uri = { .uri = "/", .method = HTTP_GET, .handler = index_handler };
httpd_uri_t cmd_uri = { .uri = "/action", .method = HTTP_GET, .handler = cmd_handler };
httpd_uri_t stream_uri = { .uri = "/stream", .method = HTTP_GET, .handler = stream_handler };
if (httpd_start(&camera_httpd, &config) == ESP_OK) {
httpd_register_uri_handler(camera_httpd, &index_uri);
httpd_register_uri_handler(camera_httpd, &cmd_uri);
}
config.server_port += 1;
config.ctrl_port += 1;
if (httpd_start(&stream_httpd, &config) == ESP_OK) {
httpd_register_uri_handler(stream_httpd, &stream_uri);
}
}
void setup() {
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
Serial.begin(115200);
// Motor pins
pinMode(MOTOR_LEFT_IN1, OUTPUT);
pinMode(MOTOR_LEFT_IN2, OUTPUT);
pinMode(MOTOR_RIGHT_IN1, OUTPUT);
pinMode(MOTOR_RIGHT_IN2, OUTPUT);
stopMotors();
WiFi.hostname("ESP32-MiniBot");
// Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected");
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_VGA;
config.jpeg_quality = 10;
config.fb_count = 1;
if (esp_camera_init(&config) != ESP_OK) {
Serial.println("Camera init failed");
return;
}
Serial.println("Camera ready at:");
Serial.println(WiFi.localIP());
Serial.printf("PSRAM size: %d bytes\n", ESP.getPsramSize());
Serial.printf("Free PSRAM: %d bytes\n", ESP.getFreePsram());
startCameraServer();
}
void loop() {}
第 8 步:测试它有效
在下一步切割电池连接器之前,将电池连接到电源板上,电缆完好无损,并测试 esp32 是否上电。
第 9 步:添加 Power 按钮并关闭 shell
第 10 步:控制机器人
打开电源开关,然后在您的 PC、Mac 或 Phone 上,通过步骤 7 中所述的 IP 地址连接到浏览器中的设备。(您的机器人本地 IP 可能与我的不同)。您应该会看到一个摄像头源和 4 个按钮来控制。在移动设备上,按钮有时会卡住,因此在台式机/笔记本电脑浏览器上通常控制得更好。那应该是它,如果您发现您的电机没有正确运行,那么您可能需要检查您的接线并重新组织它,直到电机都正常工作。
为了给电池充电,我还没有在这个项目中加入 lipo 充电器,所以你需要拧下顶部,断开电池并将其连接到充电器(通常随电池一起提供)。但是,它可能是您融入自己的升级点!
第 11 步:未来版本
正如您会注意到的那样,在摄像头正下方的外壳中,有一个足够大的空间可以容纳 15 毫米扬声器,而在中段框架上,则有空间可以连接MAX98357A。将来,我计划更新项目以包含此内容,但请随时自行添加!esp32 上应该有足够的引脚供您添加它。只需调整配电板即可从 ESP32-CAM 进行连接。
我计划在今年晚些时候发布这个机器人的第 2 版,希望它能包括音频,甚至可能还包括麦克风功能,这样它就可以用作远程控制安全摄像头。
附录
项目链接:https://www.instructables.com/ESP32-CAM-Web-Controlled-Car/
项目作者:英国 Kent(BeepBoopPrints)
项目代码:https://github.com/BeepBoopindie/ESP32CAMBOT/blob/main/ESP32CAMBOT.ino
3D打印文件:https://content.instructables.com/FKL/O6ST/M9PNR6FM/FKLO6STM9PNR6FM._3mf