Tutorial: Burning ESP-AT Firmware into the OSOLINK WiFi/BLE I/O Shield v1.0 using an Arduino UNO

Applies to: OSOLINK WiFi/BLE I/O Shield v1.0 (Espressif ESP32-C3-MINI-1)

Flashing port: 4-pin ESP UART0 header (silkscreen: 3V3 / ESP_RX / ESP_TX / GND)

Tool needed: Arduino UNO (used as a USB-to-Serial adapter) + Espressif Flash Download Tool

Table of Contents

1Introduction – When Do You Need This?

The OSOLINK WiFi/BLE I/O Shield v1.0 ships with Espressif ESP-AT firmware already installed, so in normal use you do not need to flash anything — you simply send AT commands from your Arduino over UART.

However, you may want to reflash the ESP-AT firmware if:

  • The wireless module does not respond to AT commands (blank or garbage reply), which can happen if the module was shipped without pre-loaded firmware.
  • You accidentally overwrote the AT firmware with your own sketch or a different binary.
  • You want to upgrade to a newer ESP-AT release.

This guide shows how to reflash the firmware using only an Arduino UNO as the USB-to-Serial adapter — no dedicated USB-to-TTL module is required. The Arduino UNO’s on-board USB chip is repurposed as a transparent serial bridge between your PC and the shield’s ESP UART0 flashing header.

Tip: If you own a dedicated 3.3 V USB-to-TTL module, you can connect it directly to the ESP UART0 header instead (see the note in Section 4). The Arduino-UNO method described here is for makers who do not have a separate USB-to-TTL adapter on hand.

2What You Will Need

  • 1 × Windows PC (the Espressif Flash Download Tool is Windows-only).
  • 1 × Arduino UNO board + USB cable (used purely as a USB-to-Serial adapter — no sketch required).
  • 1 × OSOLINK WiFi/BLE I/O Shield v1.0 to be flashed.
  • A few female-to-female (or female-to-male) jumper wires, and optionally a 4-pin test clip if the ESP UART0 header is not soldered.
  • The Espressif Flash Download Tool (official).
  • The ESP32-C3 ESP-AT firmware package (download link in Section 5).
OSOLINK WiFi/BLE I/O Shield v1.0 - locating the ESP UART0, BOOT and EN
Fig. 1 – The shield. The 4-pin ESP UART0 header (3V3 / ESP_RX / ESP_TX / GND) is next to the module; the BOOT and EN buttons are at the top-right.

3Turn the Arduino UNO into a USB-to-Serial Adapter

An Arduino UNO already contains a USB-to-Serial converter that connects to the main microcontroller’s D0 (RX) and D1 (TX) pins. If we disable the main microcontroller, the USB converter becomes a transparent bridge between your PC and whatever you wire to D0/D1 — exactly what we need to talk to the shield.

To disable the main microcontroller, simply connect the RESET pin to a GND pin on the Arduino UNO with a jumper wire, and keep it connected for the whole flashing process:

Arduino UNO RESET → Arduino UNO GND (jumper wire, stays connected).

This holds the main chip in reset so it will not drive the serial lines. The Arduino UNO does not need any sketch uploaded — and you should not stack the shield on top of this Arduino. The UNO is used stand-alone, only as a USB-to-Serial adapter, wired to the shield by jumper wires.

Important: Do not plug the shield onto this Arduino UNO. Keep them separate and connect only the four wires described in Section 4. Stacking would connect the shield’s power and I/O pins to the disabled Arduino and prevent flashing.

4Hardware Wiring (ESP UART0 Header)

On the shield, locate the 4-pin header labeled ESP UART0 (silkscreen pins: 3V3, ESP_RX, ESP_TX, GND). If this header has no pins soldered, either solder a 2.54 mm header or press a 4-pin test clip onto the pads.

Connect the Arduino UNO to the shield’s ESP UART0 header as follows. Because the main chip is held in reset, the wiring is straight-through (RX → RX, TX → TX):

Arduino UNO Shield – ESP UART0 (silkscreen)
3.3V 3V3
D0 (RX0) ESP_RX
D1 (TX1) ESP_TX
GND GND

Keep the RESETGND jumper from Section 3 in place. After all wires are connected, plug the Arduino UNO’s USB cable into the PC. The shield’s power indicator (PWR LED) should light up, powered by the 3.3 V line.

Warning: Power the shield only from the Arduino UNO’s 3.3 V pin for flashing. Do not also power the shield from another source (USB, battery via VIN, or the ON/OFF switch) at the same time — connecting two power sources can damage the board.
Alternative (dedicated USB-to-TTL module): If you use a 3.3 V USB-to-TTL adapter instead of the Arduino UNO, the RX/TX lines are crossed in that case: adapter 3.3V → 3V3, TXD → ESP_RX, RXD → ESP_TX, GND → GND.

5Download the Flash Tool & AT Firmware

  • Flash Download Tool – download the official Espressif tool (direct link): dl.espressif.com/public/flash_download_tool.zip. Unzip it anywhere; no installation is required.
  • ESP32-C3 ESP-AT firmware V3.2.0.0 – download the pre-built ESP32-C3-MINI-1 AT binary (direct link): ESP32-C3-MINI-1-AT-V3.2.0.0.zip. Unzip the package. You will use the file at:
    ESP32-C3-MINI-1-AT-V3.2.0.0\ESP32-C3-MINI-1-V3.2.0.0\factory\factory_MINI-1.bin
Note: Always use the MINI-1 factory binary that matches the ESP32-C3-MINI-1 module on this shield. The single factory_MINI-1.bin already contains the bootloader, partition table and AT application combined, so it is flashed to address 0x0 in one step.

6Configure the Flash Download Tool

6.1  Run flash_download_tool.exe. In the first dialog, set:

  • ChipType: ESP32-C3
  • WorkMode: Factory
  • LoadMode: UART

Click OK.

6.2  In the main window, configure it as shown below:

Step Setting Value / Action
a LockSettings Un-check it first so the fields below become editable.
b SPI Flash Config Keep the defaults: 40MHz, DIO.
c Firmware row checkbox Tick the checkbox on the first firmware row to enable it.
d Firmware file Click ... and select ...\factory\factory_MINI-1.bin from the unzipped AT package.
e Flash address Enter 0x0 in the address box next to the firmware file.
f LockSettings Re-check it to lock the settings.
g COM Select the COM port of your Arduino UNO (check Windows Device Manager if unsure).
h BAUD Set 115200 for the Arduino-UNO method (reliable). A dedicated USB-to-TTL module can use a higher rate for faster flashing.

7Enter Download Mode & Flash

The ESP32-C3 must be put into download (bootloader) mode using the shield’s own BOOT and EN buttons. Follow this sequence:

  1. Click START in the Flash Download Tool.
  2. Press and hold the BOOT button on the shield.
  3. While still holding BOOT, press the EN button once and release it.
  4. Release the BOOT button. The ESP32-C3 is now in download mode, and a progress bar appears at the bottom of the tool.
  5. Wait for the flash to complete. The tool shows FINISH when done.

After it finishes, press the EN button once more (or unplug and replug USB) to reboot the module and run the new AT firmware.

Batch flashing: You can flash several boards one after another with the same setup — just make sure the correct COM port is selected each time you connect a different board.

8Verify the AT Firmware (Functional Test)

Important – two separate interfaces: The ESP UART0 header (3V3 / ESP_RX / ESP_TX / GND) used above is only for firmware flashing. The AT command interface that the Arduino host talks to is a different port, brought out on the shield’s ATRX / ATTX pins (routed to Arduino D5 / D4 through the UART Selector, which is the factory-default setting). Therefore, do not test AT commands through the flashing port. Instead, mount the shield on an Arduino UNO and run the test below.

First, disconnect all flashing wires and remove the RESETGND jumper from Section 3.

8.1  Power-on test. Mount the shield onto an Arduino UNO — align the headers carefully and seat them fully without bending any pins. Connect the UNO to the PC with a USB cable. The shield’s on-board power indicator LED should stay on. If it does, this test passes.

8.2  Upload the test sketch. Use the OSOYOO functional-test sketch (shown below) to have the Arduino talk to the ESP32-C3 over the AT interface (ATRX / ATTX → D5 / D4), run a sequence of AT commands, and scan for nearby Wi-Fi:

  1. Open the Arduino IDE and open the test sketch.
  2. Select the correct Port and set the board type to Arduino UNO.
  3. Click Upload and wait for it to finish.
  4. Open the Serial Monitor and set the baud rate to 9600 (line ending: Newline / NL).

Test sketch – copy the whole sketch below into a new Arduino IDE window, then upload it:

/*
 * ESP32-C3 WiFi/BLE Shield -- production-line test + factory restore
 * (for QC use; the board can be shipped directly after the test)
 * Hardware: Arduino UNO + ESP32-C3 Shield (AT firmware, SoftwareSerial D4/D5)
 *
 * Flow (all done automatically in setup(); press the UNO reset button to test the next board):
 *   STEP A  AT+RESTORE factory restore (covers both 115200 and 9600 as the current baud; module ends at factory 115200)
 *   STEP B  Temporarily drop the baud to 9600 (AT+UART_CUR, not written to flash, this session only) for stable SoftwareSerial comms
 *   STEP 1~5  Basic AT / firmware version / Station mode / scan config / WiFi scan -- PASS/FAIL per step
 *   STEP D  AT+RESTORE again to wipe the temporary config; module ends at [factory default 115200], ready to ship
 *
 * Serial Monitor: baud 9600, line ending "Newline (NL)".
 */

#include 
SoftwareSerial esp(4, 5);   // D4=RX, D5=TX

// Forward declarations
bool   testAT(const char* title, String cmd, String expect, int timeout);
String readFor(int timeout);
void   scanWiFi();
void   printAp(int n, String line);
String ecnToStr(int ecn);
void   factoryRestore();
void   flushEsp();

int passCount = 0, failCount = 0;

void setup() {
  Serial.begin(9600);
  delay(300);
  Serial.println(F("\n=========================================="));
  Serial.println(F("   ESP32-C3 Shield production test + factory restore"));
  Serial.println(F("=========================================="));

  // ---------- STEP A: factory restore ----------
  Serial.println(F("\nSTEP A: Factory restore (AT+RESTORE) ..."));
  factoryRestore();     // handles unknown baud; module ends at factory default 115200
  Serial.println(F("  Factory restored; module should now be at 115200"));

  // ---------- STEP B: temporarily drop to 9600 (not written to flash) ----------
  Serial.println(F("\nSTEP B: Temporarily switch to 9600 for testing (AT+UART_CUR) ..."));
  esp.begin(115200);
  delay(400);
  esp.println("AT+UART_CUR=9600,8,1,0,0");   // this session only; lost after power-off / RESTORE
  delay(800);
  esp.end();
  esp.begin(9600);
  delay(400);
  flushEsp();

  // ---------- STEP 1~4 ----------
  bool ok1 = testAT("STEP 1: Basic AT communication", "AT", "OK", 1500);
  if (!ok1) {
    Serial.println(F("  !! No AT response, please check:"));
    Serial.println(F("     - Whether the UART Selector jumper is set to D4/D5"));
    Serial.println(F("     - Module power / whether it is seated properly"));
    Serial.println(F("     - If the STEP B baud command was not received, press UNO reset and retry"));
  }
  testAT("STEP 2: Read firmware version (AT+GMR)", "AT+GMR",           "OK", 2500);
  testAT("STEP 3: Set Station mode",               "AT+CWMODE=1",      "OK", 1500);
  testAT("STEP 4: Configure scan output (sort/compact)", "AT+CWLAPOPT=1,23", "OK", 1000);

  // ---------- STEP 5: scan WiFi ----------
  Serial.println(F("\nSTEP 5: Scan nearby WiFi (AT+CWLAP) ..."));
  scanWiFi();

  // ---------- summary ----------
  Serial.println(F("\n------------------------------------------"));
  Serial.print(F("Result: PASS ")); Serial.print(passCount);
  Serial.print(F(", FAIL "));       Serial.println(failCount);
  if (failCount == 0) Serial.println(F("*** TEST PASSED ***"));
  else                Serial.println(F("!!! Some items failed, please check !!!"));
  Serial.println(F("------------------------------------------"));

  // ---------- STEP D: restore factory again after test; keep clean before shipping ----------
  Serial.println(F("\nSTEP D: Factory restore for direct shipping ..."));
  flushEsp();
  esp.println("AT+RESTORE");    // currently 9600; wipe temp config and reboot to factory 115200
  delay(3000);
  Serial.println(F("* Module restored to factory default (115200, no leftover config)"));
  Serial.println(F("* Ready to ship; no re-flash needed."));
  Serial.println(F("=========================================="));
  Serial.println(F("(Next board: swap in a new Shield and press the UNO reset button)"));
}

void loop() {
  // After factory restore the module is back at 115200; this program no longer talks to it, just idles.
}

// ---- factory restore: cover both possible current baud rates 115200 and 9600 ----
void factoryRestore() {
  long bauds[2] = {115200, 9600};
  for (int i = 0; i < 2; i++) { esp.end(); esp.begin(bauds[i]); delay(300); esp.print("+++"); // exit any passthrough mode (no newline, whitespace around) delay(1000); esp.println("AT+RESTORE"); // restore factory and reboot delay(2500); Serial.print(F(" Sent +++ / AT+RESTORE at ")); Serial.print(bauds[i]); Serial.println(F(" baud")); } // after RESTORE the module returns to factory default 115200 } // ---- send an AT command, check whether the response contains the expected keyword, print PASS/FAIL ---- bool testAT(const char* title, String cmd, String expect, int timeout) { Serial.println(); Serial.println(title); Serial.println(" > " + cmd);
  flushEsp();
  esp.println(cmd);
  String resp = readFor(timeout);

  resp.trim();
  if (resp.length()) {
    Serial.print("  < ");
    for (unsigned int i = 0; i < resp.length(); i++) {
      char c = resp[i];
      Serial.write(c);
      if (c == '\n') Serial.print("    ");
    }
    Serial.println();
  } else {
    Serial.println("  < (no response)");
  }

  bool ok = resp.indexOf(expect) != -1;
  Serial.println(ok ? "  [PASS]" : "  [FAIL]");
  if (ok) passCount++; else failCount++;
  return ok;
}

// ---- collect the module response within timeout (refresh the timeout on new data) ----
String readFor(int timeout) {
  String r = "";
  unsigned long t = millis();
  while (millis() - t < (unsigned long)timeout) { while (esp.available()) { r += (char)esp.read(); t = millis(); if (r.length() > 250) return r;   // limited UNO RAM; avoid an oversized String
    }
  }
  return r;
}

// ---- scan WiFi: parse and print each line as it arrives; still counts on timeout ----
void scanWiFi() {
  flushEsp();
  esp.println("AT+CWLAP");

  char buf[90];
  byte idx = 0;
  int count = 0;
  bool done = false;
  unsigned long t = millis();

  while (millis() - t < 9000 && !done) {
    while (esp.available()) {
      char c = esp.read();
      t = millis();
      if (c == '\r') continue;
      if (c == '\n') {
        buf[idx] = '\0';
        String line = String(buf);
        idx = 0;
        if (line.indexOf("+CWLAP:") != -1)      { count++; printAp(count, line); }
        else if (line.indexOf("OK") != -1)      { done = true; break; }
        else if (line.indexOf("ERROR") != -1)   { Serial.println(F("  Returned ERROR")); done = true; break; }
      } else if (idx < sizeof(buf) - 1) { buf[idx++] = c; } } } Serial.print(F(" Found ")); Serial.print(count); Serial.println(F(" hotspots")); if (count > 0) { Serial.println(F("  [PASS] RF and scan function OK")); passCount++; }
  else {
    Serial.println(F("  [FAIL] No hotspots found (check mode / wiring / antenna)"));
    failCount++;
  }
}

// ---- parse and print one +CWLAP line ----
void printAp(int n, String line) {
  int lp = line.indexOf('(');
  if (lp == -1) return;

  int c1  = line.indexOf(',', lp);
  int ecn = line.substring(lp + 1, c1).toInt();

  int q1 = line.indexOf('"', c1);
  int q2 = line.indexOf('"', q1 + 1);
  String ssid = (q1 != -1 && q2 != -1) ? line.substring(q1 + 1, q2) : "";
  if (ssid.length() == 0) ssid = "(hidden SSID)";

  int c2   = line.indexOf(',', q2);
  int c3   = line.indexOf(',', c2 + 1);
  int rssi = line.substring(c2 + 1, c3).toInt();

  int rp = line.lastIndexOf(')');
  int lc = line.lastIndexOf(',');
  int ch = line.substring(lc + 1, rp).toInt();

  Serial.print(F("  "));   Serial.print(n);   Serial.print(F(". SSID="));
  Serial.print(ssid);
  Serial.print(F("  RSSI=")); Serial.print(rssi); Serial.print(F("dBm"));
  Serial.print(F("  CH=")); Serial.print(ch);
  Serial.print(F("  ENC=")); Serial.println(ecnToStr(ecn));
}

String ecnToStr(int ecn) {
  switch (ecn) {
    case 0: return "OPEN";
    case 1: return "WEP";
    case 2: return "WPA-PSK";
    case 3: return "WPA2-PSK";
    case 4: return "WPA/WPA2-PSK";
    case 5: return "WPA2-Enterprise";
    case 6: return "WPA3-PSK";
    case 7: return "WPA2/WPA3-PSK";
    default: return "?";
  }
}

void flushEsp() {
  while (esp.available()) esp.read();
}
Note: The AT interface is on D5 / D4 by factory default (not D0 / D1), so uploading the sketch over USB and communicating with the ESP32-C3 do not conflict — both work at the same time with the shield mounted, and no jumper change is needed.

8.3  Expected result. If the firmware is working, the Serial Monitor prints the AT firmware version, the result of each AT step, and a list of nearby Wi-Fi hotspots, then restores the module to factory defaults. A representative output looks like this:

STEP 1: Basic AT communication
> AT
< OK  [PASS]

STEP 2: Read firmware version (AT+GMR)
AT version:3.2.0.0 …  Bin version:v3.2.0.0(MINI-1)
[PASS]

STEP 3: Set Station mode (AT+CWMODE=1)  [PASS]
STEP 4: Configure scan output (AT+CWLAPOPT)  [PASS]

STEP 5: Scan nearby Wi-Fi (AT+CWLAP) …
1. SSID=TP-LINK*****  RSSI=-40dBm  CH=4
2. SSID=NVDA  RSSI=-43dBm  CH=6
…  15 hotspots found  [PASS]

——————————————
Result: PASS 5, FAIL 0   ★★★ TEST PASSED ★★★
★ Module restored to factory defaults (115200, no leftover config)
★ Ready to ship — no re-flash needed.

If you see the firmware version, a list of nearby Wi-Fi SSIDs, and a PASS summary, the ESP-AT firmware is installed and working correctly. To test the next board, swap in a new shield and press the UNO’s reset button.

9Troubleshooting & Notes

1. The tool cannot connect / “Failed to connect”.
Re-do the boot sequence: click START first, then hold BOOT, tap EN, release BOOT. Double-check that the RESETGND jumper on the Arduino UNO is in place (main chip disabled), and that D0→ESP_RX and D1→ESP_TX are not swapped.

2. No COM port shown.
Install the Arduino UNO’s USB-serial driver and confirm the port appears in Windows Device Manager. Try a different USB cable/port — some cables are power-only.

3. Flashing starts but fails partway.
Lower the BAUD to 115200, keep wires short, and make sure the shield is powered only from the Arduino UNO 3.3 V pin. Poor jumper/test-clip contact is the most common cause of mid-flash errors.

4. The functional test returns no AT response.
Remember the AT interface is on ATRX / ATTX (D5 / D4), not the flashing port. Make sure the shield is fully seated on the Arduino UNO, the UART Selector is at its factory-default D5 / D4 position, the Serial Monitor is set to 9600 baud, and no other program is holding the COM port. Press the UNO reset button to re-run the test.

Reminder: Use the MINI-1 factory binary for this shield and flash it to address 0x0. Using a binary for a different module variant will result in a non-responsive module.

© 2026 OSOYOO | www.osoyoo.com