Week 4. Embedded Programming¶
Group Assignment¶
This week, our group assignment was very interesting. With the help of Maxime, we explored several microcontrollers:
- XIAO RP2040 (Datasheet)
- XIAO ESP32-S3 (Datasheet)
- ATtiny1614 (Datasheet)
Using two types of programming languages, C and MicroPython, we compared the speed of execution. We measured everything using an oscilloscope.
Zhirayr did an execution speed test on the XIAO RP2040, and I did it on the XIAO ESP32-S3.
Frequency of ESP32-S3 with MicroPython.
Frequency of ESP32-S3 with C++ Arduino framework.
I want to mention that it was very weird for us that the ESP32 runs MicroPython slower (76.8 kHz) than the RP2040 (82.8 kHz), even though the ESP32 runs C++ with the Arduino framework much faster (785.3 kHz) than the RP2040 (610.2 kHz).
I think the reason is that the RP2040 is more optimized for MicroPython than the ESP32.
Microcontroller | MicroPython Speed | C++ with Arduino Speed |
---|---|---|
ESP32 | 76.8 kHz | 785.3 kHz |
RP2040 | 82.8 kHz | 610.2 kHz |
Which Microcontroller did I use?¶
Since my submarine should have a camera, I decided to try programming on the ESP32 microcontroller, which already has a camera.
We have ESP32-CAM (ESP32-S) and XIAO-ESP32-S3 in our lab, so I will try to program on both.
More about ESP32.¶
What is ESP32?¶
I asked Copilot to introduce me more about ESP32. Here is the prompt.
What is ESP32-S3 , specs
Here is the answer.
The ESP32 is a series of low-cost, low-power system-on-chip microcontrollers with integrated Wi-Fi and dual-mode Bluetooth. Developed by >>Espressif Systems, the ESP32 series is a successor to the ESP8266 microcontroller.
This is the specs for ESP32-S3:
Specification Description Processor Xtensa dual-core 32-bit LX7 microprocessor, operating at up to 240 MHz Memory 512 KiB SRAM, 384 KiB ROM Wireless Connectivity Wi-Fi (802.11 b/g/n) and Bluetooth (v5.0 BR/EDR and BLE) Peripheral Interfaces 45 programmable GPIOs, 14 touch sensors, 2 × 12-bit SAR ADCs, 2 × 8-bit DAC, and more Security Secure boot, flash encryption, cryptographic hardware acceleration Power Management Integrated low-dropout regulator, deep sleep current of 5 μA
Versions¶
Here are the main versions of the ESP32:
ESP32: The original version with a dual-core Xtensa LX6 microprocessor. It’s known for its robust features and versatility.
ESP32-S: A versatile microcontroller with a dual-core 32-bit Xtensa LX6 microprocessor, operating at 160 or 240 MHz. It supports Wi-Fi and Bluetooth, and has extensive peripheral interfaces, making it suitable for a wide range of applications from simple DIY projects to complex IoT developments.
ESP32-S2: This version features a single-core Xtensa LX7 microprocessor. It offers improved security and a higher number of GPIOs compared to the original ESP32. It was launched in 2020.
ESP32-C3: This version is equipped with a single-core RISC-V microprocessor, providing enhanced performance and efficiency. It was also launched in 2020 and is designed for low-power IoT applications.
ESP32-S3: Features a dual-core Xtensa LX7 microprocessor, offering higher processing power and more GPIOs. It is suitable for AI and machine learning applications.
ESP32-H2: This version integrates Thread and Zigbee, making it ideal for smart home applications and providing better connectivity options.
Datasheets and Pinouts of ESP32-S and XIAO ESP32-S3¶
I found the pinouts of ESP32-S and XIAO ESP32-S3 on the Last Minute Engineers website and there is a lot of very useful information.
ESP32-S
ESP32-S3
Then I asked Copilot about pinouts and which pins I should use. Here is the prompt.
Tell me a about the pinouts of ESP32. What are GPIO, ADC, SPI, and Touch? I will be programming through Arduino Uno in Arduino IDE, which pinouts do I need to use?
Here is the answer:
Pinouts of ESP32
GPIO (General Purpose Input/Output): These are versatile pins used for both input and output of signals. They can connect various peripherals and devices.
ADC (Analog-to-Digital Converter): This converts analog signals into digital values. ADC pins allow you to read analog inputs, like voltage levels.
SPI (Serial Peripheral Interface): An interface used for communication with peripherals such as sensors and displays. It uses multiple pins for data exchange.
Touch: These pins can detect touch inputs or capacitive touch signals.
Programming ESP32 with Arduino Uno in Arduino IDE
To program the ESP32 using Arduino Uno in Arduino IDE, you’ll need to use specific pins for different functionalities:
GPIO Pins: For general-purpose input/output (e.g., GPIO 0, 1, 2, etc.)
ADC Pins: For analog-to-digital conversion (e.g., ADC1_CHANNEL_0 - GPIO 36, ADC1_CHANNEL_1 - GPIO 37, etc.)
SPI Pins: For SPI communication (e.g., GPIO 18 - SCK, GPIO 23 - MOSI, GPIO 19 - MISO, GPIO 5 - SS)
Touch Pins: For touch input (e.g., T0 - GPIO 4, T1 - GPIO 0, etc.)
For programming through Arduino UNO using Arduino IDE, you’ll need to connect the ESP32 to the Arduino UNO using the following pins:
VCC to 3.3V
GND to GND
TXD to RX
RXD to TX
EN to EN
Simulating on Wokwi¶
There are two ways to simulate it with Wokwi. One is on the Wokwi website, and the second option is to add an extension on VS Code. I prefer to do it on web site.
I watched a video on how to write code for a servo motor, and I modified it by adding servo.write
to the loop so that it keeps repeating constantly.
Here is the code:
#include <ESP32Servo.h>
Servo myServo;
const int servoPin = 2;
void setup() {
myServo.attach(servoPin);
}
void loop() {
myServo.write(90);
delay(1000);
myServo.write(0);
delay(1000);
}
For including library I need to go Library Manager and add it there.
After adding components and connecting wires in simulator I pressed the Start Simulation button and it runs good.
❗Note. Keep in mind that compilation on Wokwi is quite long, and you might see the message ‘servers are busy’ several times. I managed to compile and start the simulation on the 6th try.
Programming¶
Wiring the device¶
After Simulation I tried to program on real ESP32-S3 and Servo motor. For this I have used these components:
-
ESP32-S3
-
Servomotor CS-939MG
-
Breadboard
-
Jump wires
-
Type-C cable
I found the outputs of servomotor and usually the output is the same with the same colors.
After I connected the servo motor to ESP32-S3 using breadboard.
I want to share with you how to conveniently connect a breadboard without soldering using jumper wires.
Coding¶
Running Servomotor¶
For coding I used Arduino IDE.
For using Servomotor I need to install library ESP32Servo.
I found it in search bar and installed.
I need also to install ESP32 board from Boards Manager.
Here is the right board.
After installing I put my code to Arduino IDE, then select the board and settings.
Then I uploaded the code to ESP32 using ctrl + u
hotkey.
❗Note. After uploading the code I’ll need to reset the board to start the code execution by pressing b button on device. This will put the board into the programming mode. Once the upload is complete, release the “B” button. By Pressing the “R” (Reset) button to reset the board and start running the uploaded code.
After uploading i saw writing lines in log, which is good , and it means that code is uploading.
Then I pressed the R button on device and Servo start rotating.
Using Camera on ESP32CAM.¶
Since I don’t have many programming skills, I decided to use AI to help me with the code. I studied many websites and videos to achieve this, and after spending 3 DAYS, I finally got what I needed!
Uploading Example of CameraWeServer¶
In Autodesk Instructables site I found how to connect and program ESP32CAM with arduino Uno.
Here is the connection.
After I tried to upload example of WebServer for Streaming.
Before uploading I uncommented the camera model , and write the wifi SSID and password.
Here is the parameters for uploading data.
❗Note. Compilation is not fast! in my case, it takes about 2 minutes. After compilation, you must press the button on the ESP32-CAM to enter boot mode so the writing process can start.
If you see this red line logs(writing at....) it’s mean that everything is good.
After uploading I take off the jumper between GND and GPIO 0 , open Serial Monitor (ctrl + shift + m
) and push the button on ESP32CAM.
ESP32CAM is connected to Network, I open the address in browser and Voilà!!
Coding with AI.¶
I really enjoyed experimenting with the camera, and ChatGPT showed me the possibilities of getting information using a Telegram bot. So, I started learning all about it.
I watched several video tutorials on Youtube how to do all this, but I needed something different…
So I started talking with ChatGPT from this prompt:
I need code for ESP32-CAM where the camera connects through Wi-Fi and sends its IP address to a Telegram bot for live streaming.It should also have commands Capture – takes a photo and saves it to the SD card and Record – records a video and saves it to the SD card.
And everything started from here…
As you can see I have very long conversations with ChatGPT and finally
So, I started sending code to Copilot, hoping it would try to fix it. Then DeepSeek), then DeepAI, I talked to all AIs that are available and free.
I thought this would never end, and I wouldn’t be able to generate the code. But in the end, I came across another AI, Claude, who finally gave me the code that worked the way I wanted. However, before that, I spent a very long time talking to it, trying to explain what I needed and sending the code that other AIs had generated..
Here is the working Final Code:
#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"
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <ArduinoJson.h>
#include "SD_MMC.h"
#include "FS.h"
// Telegram settings
#define BOTtoken "#######################" //Bot token
// Array of chat IDs
const String CHAT_IDS[] = {
"************", // Chat IDs
"*************",
"THIRD_CHAT_ID",
"FOURTH_CHAT_ID",
"FIFTH_CHAT_ID"
};
const int NUM_CHAT_IDS = 5;
const char* ssid = "*************"; // wifi settings
const char* password = "***********";
#define FLASH_LED_PIN 4
#define PART_BOUNDARY "123456789000000000000987654321"
// Variables
bool isRecording = false;
File videoFile;
// Telegram
WiFiClientSecure clientTCP;
UniversalTelegramBot bot(BOTtoken, clientTCP);
// Camera pins
#define 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
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 stream_httpd = NULL;
// Function to broadcast message to all chat IDs
void broadcastMessage(const String& message) {
for (int i = 0; i < NUM_CHAT_IDS; i++) {
bot.sendMessage(CHAT_IDS[i], message, "");
delay(100); // Small delay between messages to prevent flooding
}
}
bool initSDCard() {
if(!SD_MMC.begin("/sdcard", true)) {
Serial.println("SD Card Mount Failed");
return false;
}
uint8_t cardType = SD_MMC.cardType();
if(cardType == CARD_NONE) {
Serial.println("No SD Card attached");
return false;
}
return true;
}
void showMenu(String chat_id) {
String welcome = "ESP32-CAM Bot Commands:\n\n";
welcome += "/start : Show this menu\n";
welcome += "/capture : Capture and save an image\n"; // Новая команда
welcome += "/record : Start recording (with flash)\n";
welcome += "/stoprecord : Stop recording\n";
bot.sendMessage(chat_id, welcome, "");
}
// Функция для захвата и сохранения снимка
void captureImage(String chat_id) {
if (!initSDCard()) {
broadcastMessage("Failed to initialize SD Card");
return;
}
camera_fb_t * fb = esp_camera_fb_get(); // Получаем кадр с камеры
if (!fb) {
broadcastMessage("Camera capture failed");
return;
}
String filename = "/capture_" + String(millis()) + ".jpg"; // Имя файла
File file = SD_MMC.open(filename, FILE_WRITE);
if (!file) {
broadcastMessage("Failed to create file");
esp_camera_fb_return(fb); // Освобождаем буфер камеры
return;
}
file.write(fb->buf, fb->len); // Записываем данные в файл
file.close();
esp_camera_fb_return(fb); // Освобождаем буфер камеры
broadcastMessage("Capture saved: " + filename);
}
void startRecording(String chat_id) {
if (!initSDCard()) {
broadcastMessage("Failed to initialize SD Card");
return;
}
String filename = "/capture_" + String(millis()) + ".mjpeg";
videoFile = SD_MMC.open(filename, FILE_WRITE);
if (!videoFile) {
broadcastMessage("Failed to create file");
return;
}
isRecording = true;
digitalWrite(FLASH_LED_PIN, HIGH); // Turn on flash
broadcastMessage("Recording started with flash. File: " + filename);
}
void stopRecording(String chat_id) {
if (isRecording) {
videoFile.close();
isRecording = false;
digitalWrite(FLASH_LED_PIN, LOW); // Turn off flash
broadcastMessage("Recording stopped and saved");
}
}
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->width > 400) {
if(fb->format != PIXFORMAT_JPEG) {
bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
esp_camera_fb_return(fb);
fb = NULL;
if(!jpeg_converted) {
Serial.println("JPEG compression failed");
res = ESP_FAIL;
}
} else {
_jpg_buf_len = fb->len;
_jpg_buf = fb->buf;
}
}
if (isRecording && videoFile) {
videoFile.write(_jpg_buf, _jpg_buf_len);
}
}
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);
fb = NULL;
_jpg_buf = NULL;
} else if(_jpg_buf) {
free(_jpg_buf);
_jpg_buf = NULL;
}
if(res != ESP_OK) {
break;
}
}
return res;
}
void startCameraServer() {
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = 80;
httpd_uri_t index_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = stream_handler,
.user_ctx = NULL
};
if (httpd_start(&stream_httpd, &config) == ESP_OK) {
httpd_register_uri_handler(stream_httpd, &index_uri);
}
}
void handleNewMessages(int numNewMessages) {
for (int i = 0; i < numNewMessages; i++) {
String chat_id = bot.messages[i].chat_id;
String text = bot.messages[i].text;
// Check if the sender is authorized
bool isAuthorized = false;
for (int j = 0; j < NUM_CHAT_IDS; j++) {
if (chat_id == CHAT_IDS[j]) {
isAuthorized = true;
break;
}
}
if (!isAuthorized) {
bot.sendMessage(chat_id, "Unauthorized user", "");
continue;
}
if (text == "/start") {
showMenu(chat_id);
String ipAddress = WiFi.localIP().toString();
bot.sendMessage(chat_id, "Stream available at: http://" + ipAddress, "");
}
else if (text == "/capture") { // Обработка команды /capture
captureImage(chat_id);
}
else if (text == "/record") {
if (!isRecording) {
startRecording(chat_id);
} else {
bot.sendMessage(chat_id, "Already recording", "");
}
}
else if (text == "/stoprecord") {
if (isRecording) {
stopRecording(chat_id);
} else {
bot.sendMessage(chat_id, "Not recording", "");
}
}
}
}
void setup() {
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
Serial.begin(115200);
Serial.setDebugOutput(false);
pinMode(FLASH_LED_PIN, OUTPUT);
digitalWrite(FLASH_LED_PIN, LOW);
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;
if(psramFound()) {
config.frame_size = FRAMESIZE_VGA;
config.jpeg_quality = 18;
config.fb_count = 2;
} else {
config.frame_size = FRAMESIZE_VGA;
config.jpeg_quality = 18;
config.fb_count = 1;
}
// Camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
// Camera settings
sensor_t * s = esp_camera_sensor_get();
s->set_brightness(s, 0);
s->set_contrast(s, 0);
s->set_saturation(s, 0);
s->set_special_effect(s, 0);
s->set_whitebal(s, 1);
s->set_awb_gain(s, 1);
s->set_wb_mode(s, 0);
s->set_exposure_ctrl(s, 1);
s->set_aec2(s, 0);
s->set_ae_level(s, 0);
s->set_aec_value(s, 300);
s->set_gain_ctrl(s, 1);
s->set_agc_gain(s, 0);
s->set_gainceiling(s, (gainceiling_t)0);
s->set_bpc(s, 0);
s->set_wpc(s, 1);
s->set_raw_gma(s, 1);
s->set_lenc(s, 1);
s->set_hmirror(s, 0);
s->set_vflip(s, 1);
s->set_dcw(s, 1);
s->set_colorbar(s, 0);
// Connect to WiFi
WiFi.begin(ssid, password);
clientTCP.setCACert(TELEGRAM_CERTIFICATE_ROOT);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
// Start web server
startCameraServer();
// Send initial message with menu
String ipAddress = WiFi.localIP().toString();
String welcome = "ESP32-CAM Started!\n\n";
welcome += "Stream available at: http://" + ipAddress + "\n\n";
welcome += "Commands:\n";
welcome += "/start : Show menu\n";
welcome += "/capture : Capture and save an image\n";
welcome += "/record : Start recording (with flash)\n";
welcome += "/stoprecord : Stop recording";
broadcastMessage(welcome);
}
void loop() {
int numNewMessages = bot.getUpdates(bot.last_message_received + 1);
if (numNewMessages > 0) {
handleNewMessages(numNewMessages);
}
delay(100);
}
Here is the result:
Flash is turning on, when I’m sending /record
command.
Here is Live Streaming.
Conclusion¶
This week, I learned a lot. Although I didn’t write the final code for the submarine myself, I realized that to get the code you need from AI, you have to talk to it for a long time. It’s very complex code for me, and I’m still not very good at programming devices, but I will keep learning programming because it’s really interesting. What amazed me the most was discovering that different programming languages have different execution speeds.