esp: Automatic Bootloader

下载模式、烧录模式、Boot模式、Boot Mode都是同样的意思!!!

概述

在对esp32开发板烧录固件时,每次都要先按住boot键不放,然后按住en键,此时才能进入烧录模式。感觉麻烦的,要是有一种自动下载模式就好了,无需开发者去操作按键,直接烧录就行。

image-20241117132345736

下载模式分为:自动下载模式 和 手动下载模式。但有一个前提,如何进入下载模式?

EN(也称为RST)上升沿(从低电平到高电平)时候,检测到GPIO0(也称为Boot)为低电平,进入下载模式。具体操作方式有两种:

  • 方式1:先按住Boot键不放,然后按住EN键
  • 方式2:先按住Boot键不放,然后断电和上电操作

注意:默认GPIO0为高电平,当按下Boot键后,GPIO0为低电平。可以查看开发板原理图得知

原理

那么开发板的自动下载模式的原理是什么呢?其实是通过电路模拟了进入Boot模式操作。

乐鑫解释了如何进入自动下载模式,如下:

esptool.py resets ESP32 automatically by asserting DTR and RTS control lines of the USB to serial converter chip, i.e., FTDI, CP210x, or CH340x. The DTR and RTS control lines are in turn connected to GPIO0 and EN (CHIP_PU) pins of ESP32, thus changes in the voltage levels of DTR and RTS will boot the ESP32 into Firmware Download mode.

Note:When developing esptool.py, keep in mind DTR and RTS are active low signals, i.e., True = pin @ 0V, False = pin @ VCC.

释义:esptool.py 通过置位 USB 转串口转换器芯片(即 FTDI、CP210x 或 CH340x)的 DTR 和 RTS 控制线来自动重置 ESP32。DTR 和 RTS 控制线依次连接到 ESP32 的 GPIO0 和 EN(CHIP_PU)引脚,因此 DTR 和 RTS 的电压变化将使 ESP32 进入固件下载模式。

注意:在开发 esptool.py 时,请记住 DTR 和 RTS 是低电平有效信号,即 True = 引脚 @ 0V,False = 引脚 @ VCC。

先来看开发板的原理图(依次为 图1 和 图2):

image-20241117173318335

image-20241117140224096

这里可能会有一个疑问:DTR 和 RTS 在哪里?这两个引脚在 CP2102N芯片上(USB-UART Bridge 电路)

串口流控机制:

  • DTR: Data Terminal Ready,数据终端准备好,低有效
  • RTS:Request To Send,请求发送,低有效

另外 “Auto program” 中是有规律的,规律如下:

  1. 当 DTR 和 RTS 同时为 0 或者同时为 1 时,三极管 Q1 和 Q2 均为截止状态,此时 EN 和 IO0 的状态由其他电路决定(内部/外部上拉电阻)。
  2. 当 DTR 和 RTS 不同时,EN = RTS, IO0 = DTR。

自动进入下载模式的顺序应该是这样的(芯片断电,然后上电):(EN=0, IO0=0)–>(EN=1,IO0=0),可是看上图的 “Auto program” 是没有(EN=0, IO0=0)这种逻辑的。如果没有这个逻辑,那么是无法进入下载模式的。(暂时先不管)

查看esptool.py关于自动下载模式的实现,在 esptool/reset.py 找到了具体逻辑实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
class ClassicReset(ResetStrategy):
"""
Classic reset sequence, sets DTR and RTS lines sequentially.
"""

def reset(self):
self._setDTR(False) # IO0=HIGH
self._setRTS(True) # EN=LOW, chip in reset (1)
time.sleep(0.1)
self._setDTR(True) # IO0=LOW (2)
self._setRTS(False) # EN=HIGH, chip out of reset (3)
time.sleep(self.reset_delay)
self._setDTR(False) # IO0=HIGH, done (4)

这里有两个疑惑:为什么在 (1) 和 (2)之间要sleep? 为甚么 (3) 和 (4) 之间要sleep?

在(1)之后,设置EN=0,由于充放电电路(图1)中的电容需要放电,所以EN并不是一下子从高电平1突变至低电平0,而是有一个放电时间。故这里 sleep 0.1秒,确保EN一定是低电平0。此时,芯片开始复位。

在(3)之后,设置EN=1,由于充放电电路(图1)中的电容需要充电,所以并不是一下从低电平0突变至高电平1,而是有一个充电时间。故这里 sleep reset_delay 秒,确保EN一定是高电平1。在EN从低电平到高电平(上升沿)时,检测到IO0为低电平0,芯片开始下载模式。

疑问

esptool 通过USB发送了什么数据给串口芯片的DTR和RTS呢?

先把USB转串口电路的原理看下:

image-20241119134619149

首先从USB到串口芯片CP2102N只有两根信号线:USB_DN 和 USB_DP。然后通过串口芯片CP2102N出来了4根信号线:RXD、TXD;DTR(Data Terminal Ready、RTS(Request to Send)。其中RXD和TXD连接到MCU的TXD0和RXD0,而DTR和RTS分别连接到Q1(NPN三极管)和Q2(NPN)的基级,用于间接控制MCU的EN和IO0。

esptool 使用了 pyserial 库,可以打开串口,然后直接控制 DTR 和 RTS,经过Q1 和 Q2 的电路特性间接控制MCU 的EN 和 IO0,只要控制 DTR 和 RTS 满足 MCU 进入下载模式的条件,即可使 MCU 进入下载模式。

编译 Python 程序,使用 ESP32 先进入烧录模式,然后重启 ESP32

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# 第一步:确定串口名,分为两种情况 
# 1)对于给定目标串口名(例如 /dev/ttyUSB0 or /dev/ttyACM0)
# 2)没有指定目标串口名,需要在计算机上编译所有的的串口,然后指定一个可用串口名
# 第二步:打开串口
# 第三步:使用串口Read or Write 数据
# 第四步:关闭串口

import serial #导入模块
import time
import threading


def open_serial_port(port, baudrate=9600):
"""
打开指定的串口。

:param port: 串口号,例如 'COM1' 或 '/dev/ttyUSB0'
:param baudrate: 波特率,默认为 9600
:return: Serial 对象
"""
try:
ser = serial.Serial(port, baudrate, timeout=1)
print(f"Open port {port} success")
return ser
except serial.SerialException as e:
print(f"Open port {port} fail: {e}")
return None

def read_data(ser):
"""
从串口读取数据。

:param ser: Serial 对象
:return: 读取的数据
"""
if ser is not None and ser.is_open:
# 读取一行数据
line = ser.readline()
if line:
print(f"Recv Data: {line.decode('utf-8').strip()}")
else:
print("Not Recv Data")
else:
print("serial not open")

def read_data_cb(ser, stop_event):
"""
从串口读取数据的函数,运行在一个单独的线程中。

:param ser: Serial 对象
"""
try:
received_data = []
while not stop_event.is_set():
if ser.in_waiting > 0:
line = ser.readline()
if line:
data = line.decode('utf-8').strip()
received_data.append(data)
print(f"Recv Data: {data}")
time.sleep(0.01) # 避免CPU占用过高
except KeyboardInterrupt:
print("Data reading stopped.")

def write_data(ser, data):
"""
向串口发送数据。

:param ser: Serial 对象
:param data: 要发送的数据
"""
if ser is not None and ser.is_open:
# 发送数据
ser.write(data.encode('utf-8'))
print(f"Send Data: {data.strip()}")
else:
print("Serial not open")

def ClassicReset(ser):
"""
Classic reset sequence, sets DTR and RTS lines sequentially.
"""
ser.dtr = False # IO0=HIGH
ser.rts = True # EN=LOW, chip in reset
time.sleep(0.1)
ser.dtr = True # IO0=LOW
ser.rts = False # EN=HIGH, chip out of reset
# default time (0.05) to wait before releasing boot pin after reset
time.sleep(0.05)
ser.dtr = False # IO0=HIGH, done

def HardReset(ser):
"""
Reset sequence for hard resetting the chip.
Can be used to reset out of the bootloader or to restart a running app.
"""
ser.dtr = False
ser.rts = True # EN->LOW
# Give the chip some time to come out of reset,
# to be able to handle further DTR/RTS transitions
time.sleep(0.2)
ser.rts = False
time.sleep(0.2)

def main():
# 设置串口参数
port = '/dev/ttyUSB0' # 根据你的设备修改此值
baudrate = 115200

# 打开串口
ser = open_serial_port(port, baudrate)

if ser is not None:
try:
# 创建一个线程来读取串口数据
stop_event = threading.Event()
read_thread = threading.Thread(target=read_data_cb, args=(ser, stop_event))
read_thread.start()

# 发送数据
print("--> ESP32 autoboot mode")
ClassicReset(ser) # 设置DTR 和 RTS,将ESP32进入烧录模式
time.sleep(3)

print("--> ESP32 reset")
HardReset(ser) # 设置RTS,复位ESP32
time.sleep(2)

while True:
# 输入数据
user_input = input("Please input send data:")
if user_input.lower() in ['q', 'quit', 'exit']:
stop_event.set() # 通知子线程结束
read_thread.join() # 等待子线程结束
break

# 发送数据
write_data(ser, user_input + '\n')
except KeyboardInterrupt:
print("Program is interrupted(Ctrl+C) by user")
stop_event.set() # 通知子线程结束
read_thread.join() # 等待子线程结束
finally:
# 关闭串口
ser.close()
print(f"Port {port} close")

if __name__ == "__main__":
main()

CP2102N

CP2102N 设备是 USBXpress 系列的一部分,旨在通过消除固件复杂性和缩短开发时间。

这些高度集成的 USB 转 UART 桥接控制器 为使用最少的元件和 PCB 空间将 RS-232 设计更新为 USB。CP2102N包括一个 USB 2.0 全速功能控制器、USB 收发器、振荡器和通用异步接收器/发射器 (UART),封装最小为 3mmx 3 mm。开发不需要其他外部 USB 组件。所有定制和配置选项都可以使用基于 GUI 的简单配置器进行选择。由于无需复杂的固件和驱动程序开发,CP2102N 设备能够以最少的开发工作量实现快速 USB 连接。

CP2102N 的应用场景:

  • POS终端
  • 医疗设备
  • USB dongle
  • 数据日志分析器
  • 游戏控制器

CP2102N 的硬件架构图:

image-20250102095449172

CP2102N(QFN28)引脚图定义:

image-20250102103247477

参考

ESP8266/ESP32自动下载电路原理分析

Boot Mode Selection

esptool

ESP32自动下载电路

ESP32自动下载电路分析