esp_flash 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +84 -0
- data/lib/esp_flash/loader.rb +441 -0
- data/lib/esp_flash/monitor.rb +268 -0
- data/lib/esp_flash/serial_port.rb +348 -0
- data/lib/esp_flash/stub/LICENSE-APACHE +201 -0
- data/lib/esp_flash/stub/LICENSE-MIT +21 -0
- data/lib/esp_flash/stub/README.md +9 -0
- data/lib/esp_flash/stub/esp32.json +8 -0
- data/lib/esp_flash/stub/esp32c3.json +8 -0
- data/lib/esp_flash/stub/esp32c6.json +8 -0
- data/lib/esp_flash/stub/esp32h2.json +8 -0
- data/lib/esp_flash/stub/esp32s3.json +30 -0
- data/lib/esp_flash/version.rb +5 -0
- data/lib/esp_flash.rb +46 -0
- metadata +59 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 50f62f253aa6b84339218e9068ee71e6520c4aabab1ee0d3cf2bf0d620e79389
|
|
4
|
+
data.tar.gz: cd6dbc3b007949f43864d5711754c5dd83ce8bbe19992bb607ea3b493219e638
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0cfc138a83ea59bddc5f6b937a390948169c843b4bb592b9828a9672ef8c74a4b8d49268779f38c14f136d3c73d0cf050f7ebee01359de148ac8d1b381cb54e5
|
|
7
|
+
data.tar.gz: fdc1c0f052cd531009bf91a305252c2b630c04325b915ff1368ae3ee45b9c0157a52f85da43eb1bfc8cdccc6036fea9159722f2549aed416affacfa69ceb5101
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Okazaki Yuhei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# esp_flash
|
|
2
|
+
|
|
3
|
+
Flash and monitor ESP32-family chips from plain Ruby: a client of the
|
|
4
|
+
chip's ROM loader (running Espressif's flasher stub for speed) and a serial
|
|
5
|
+
monitor. No esptool, no Python, no C extension, no other gem; it needs only
|
|
6
|
+
Ruby's standard library.
|
|
7
|
+
|
|
8
|
+
Supported chips with the flasher stub: ESP32, ESP32-S3, ESP32-C3, ESP32-C6,
|
|
9
|
+
ESP32-H2. Others are written through the ROM loader. Serial ports work on
|
|
10
|
+
Linux, macOS and Windows, and `socket://HOST:PORT` connects to a TCP port
|
|
11
|
+
instead (QEMU's UART, for one).
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
gem install esp_flash
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
or, in a Gemfile:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem "esp_flash"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
Flashing the images of an ESP-IDF build, as `build/flasher_args.json`
|
|
28
|
+
lists them:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require "esp_flash"
|
|
32
|
+
require "json"
|
|
33
|
+
|
|
34
|
+
args = JSON.parse(File.read("build/flasher_args.json"))
|
|
35
|
+
port = EspFlash::SerialPort.resolve(ENV["PORT"]) # PORT, or the one connected board
|
|
36
|
+
|
|
37
|
+
loader = EspFlash::Loader.open(port, chip: args.dig("extra_esptool_args", "chip"),
|
|
38
|
+
baud: 460_800,
|
|
39
|
+
flash_size: EspFlash.parse_size(args.dig("flash_settings", "flash_size")))
|
|
40
|
+
begin
|
|
41
|
+
args["flash_files"].each do |addr, file|
|
|
42
|
+
loader.write_flash(Integer(addr, 16), File.binread(File.join("build", file)), name: file)
|
|
43
|
+
end
|
|
44
|
+
loader.hard_reset
|
|
45
|
+
ensure
|
|
46
|
+
loader.close
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`stub: false` writes through the ROM loader instead of the flasher stub.
|
|
51
|
+
|
|
52
|
+
Monitoring the console:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
EspFlash::Monitor.new(port, baud: 115_200, target: "esp32s3", elf: "build/app.elf").run
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
In the monitor, Ctrl-] quits, Ctrl-T Ctrl-R resets the chip and Ctrl-T
|
|
59
|
+
Ctrl-H lists the keys. It resets the chip first unless given `reset: false`.
|
|
60
|
+
With an ELF and the target's `addr2line` on `PATH`, a panic's backtrace is
|
|
61
|
+
decoded.
|
|
62
|
+
|
|
63
|
+
`EspFlash.merge_images` builds one flash image from `{ offset => data }`,
|
|
64
|
+
like `esptool merge_bin`. Errors are raised as `EspFlash::Error`.
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
bundle install
|
|
70
|
+
bundle exec rake test
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Releasing: bump `EspFlash::VERSION` in `lib/esp_flash/version.rb`, commit,
|
|
74
|
+
and run `bundle exec rake release`, which tags `v<version>`, pushes the tag
|
|
75
|
+
and pushes the gem to RubyGems.
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT (`LICENSE`). The flasher stubs in `lib/esp_flash/stub` are the binaries
|
|
80
|
+
of [esp-flasher-stub](https://github.com/espressif/esp-flasher-stub) v1.2.2,
|
|
81
|
+
under the Apache License 2.0 or the MIT license (see that directory).
|
|
82
|
+
|
|
83
|
+
This library started in [R2P2-ESP32](https://github.com/picoruby/R2P2-ESP32)
|
|
84
|
+
as `lib/esp_flash`.
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest/md5"
|
|
4
|
+
require "json"
|
|
5
|
+
require "zlib"
|
|
6
|
+
|
|
7
|
+
module EspFlash
|
|
8
|
+
# Writes images to the SPI flash through the serial bootloader in the chip's ROM (the protocol esptool
|
|
9
|
+
# speaks: https://docs.espressif.com/projects/esptool/en/latest/esp32/advanced-topics/serial-protocol.html).
|
|
10
|
+
#
|
|
11
|
+
# Once connected, the loader uploads the flasher stub of esp-flasher-stub (stub/, MIT or Apache-2.0) into
|
|
12
|
+
# the chip's RAM and talks to that instead, as esptool does: it takes 16 KB blocks and erases as it
|
|
13
|
+
# writes, which is much faster than the ROM loader. With stub: false (or when there is no stub for the
|
|
14
|
+
# chip, or over TCP to QEMU, which cannot run it) the ROM loader does the work itself.
|
|
15
|
+
# Images are written as they are; their headers are not patched for flash mode/size/frequency, since
|
|
16
|
+
# ESP-IDF builds them with the values from sdkconfig (the ones flasher_args.json gives to esptool).
|
|
17
|
+
class Loader
|
|
18
|
+
FLASH_BEGIN = 0x02
|
|
19
|
+
FLASH_DATA = 0x03
|
|
20
|
+
FLASH_END = 0x04
|
|
21
|
+
MEM_BEGIN = 0x05
|
|
22
|
+
MEM_END = 0x06
|
|
23
|
+
MEM_DATA = 0x07
|
|
24
|
+
SYNC = 0x08
|
|
25
|
+
WRITE_REG = 0x09
|
|
26
|
+
READ_REG = 0x0a
|
|
27
|
+
SPI_SET_PARAMS = 0x0b
|
|
28
|
+
SPI_ATTACH = 0x0d
|
|
29
|
+
CHANGE_BAUDRATE = 0x0f
|
|
30
|
+
FLASH_DEFL_BEGIN = 0x10
|
|
31
|
+
FLASH_DEFL_DATA = 0x11
|
|
32
|
+
FLASH_DEFL_END = 0x12
|
|
33
|
+
SPI_FLASH_MD5 = 0x13
|
|
34
|
+
GET_SECURITY_INFO = 0x14
|
|
35
|
+
|
|
36
|
+
ROM_BAUD = 115_200
|
|
37
|
+
BLOCK_SIZE = 0x400 # the ROM loader's FLASH_WRITE_SIZE
|
|
38
|
+
STUB_BLOCK_SIZE = 0x4000 # the stub's
|
|
39
|
+
RAM_BLOCK_SIZE = 0x1800
|
|
40
|
+
STUB_DIR = File.join(__dir__, "stub")
|
|
41
|
+
CHECKSUM_SEED = 0xef
|
|
42
|
+
CHIP_DETECT_MAGIC_REG = 0x40001000
|
|
43
|
+
|
|
44
|
+
# Chip => [image chip id (GET_SECURITY_INFO), values of CHIP_DETECT_MAGIC_REG]
|
|
45
|
+
CHIPS = {
|
|
46
|
+
"esp32" => [0, [0x00f01d83]],
|
|
47
|
+
"esp32s2" => [2, [0x000007c6]],
|
|
48
|
+
"esp32c3" => [5, [0x6921506f, 0x1b31506f, 0x4881606f, 0x4361606f]],
|
|
49
|
+
"esp32s3" => [9, [0x9]],
|
|
50
|
+
"esp32c2" => [12, [0x6f51306f, 0x7c41a06f]],
|
|
51
|
+
"esp32c6" => [13, [0x2ce0806f]],
|
|
52
|
+
"esp32h2" => [16, [0xd7b73e80]],
|
|
53
|
+
"esp32p4" => [18, [0x0, 0x0addbad0]],
|
|
54
|
+
"esp32c5" => [23, []],
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
# Chip => [RTC WDT write protect, RTC WDT config0, SWD write protect, SWD config, SWD auto feed bit, SWD key].
|
|
58
|
+
# Over the USB-Serial/JTAG nothing feeds these watchdogs while the chip waits in the download mode, and
|
|
59
|
+
# they can reset it in the middle of flashing; esptool disables them the same way.
|
|
60
|
+
WATCHDOGS = {
|
|
61
|
+
"esp32c3" => [0x600080a8, 0x60008090, 0x600080b0, 0x600080ac, 1 << 31, 0x8f1d312a],
|
|
62
|
+
"esp32s3" => [0x600080b0, 0x60008098, 0x600080b8, 0x600080b4, 1 << 31, 0x8f1d312a],
|
|
63
|
+
"esp32c6" => [0x600b1c18, 0x600b1c00, 0x600b1c20, 0x600b1c1c, 1 << 18, 0x50d83aa1],
|
|
64
|
+
"esp32h2" => [0x600b1c1c, 0x600b1c00, 0x600b1c24, 0x600b1c20, 1 << 18, 0x50d83aa1],
|
|
65
|
+
"esp32p4" => [0x50116018, 0x50116000, 0x50116020, 0x5011601c, 1 << 18, 0x50d83aa1],
|
|
66
|
+
}.freeze
|
|
67
|
+
WDT_KEY = 0x50d83aa1
|
|
68
|
+
|
|
69
|
+
ROM_ERRORS = {
|
|
70
|
+
0x05 => "received message is invalid",
|
|
71
|
+
0x06 => "failed to act on received message",
|
|
72
|
+
0x07 => "invalid CRC in message",
|
|
73
|
+
0x08 => "flash write error",
|
|
74
|
+
0x09 => "flash read error",
|
|
75
|
+
0x0a => "flash read length error",
|
|
76
|
+
0x0b => "deflate error",
|
|
77
|
+
}.freeze
|
|
78
|
+
|
|
79
|
+
attr_reader :chip
|
|
80
|
+
|
|
81
|
+
# Opens +port+, resets the chip into its ROM loader, syncs with it and starts the flasher stub (unless
|
|
82
|
+
# +stub+ is false). +chip+ (e.g. "esp32s3") is what the image was built for; flashing another chip is
|
|
83
|
+
# refused. +log+ receives progress messages.
|
|
84
|
+
def self.open(port, chip:, baud: 460_800, flash_size: nil, reset: true, stub: true, log: $stdout)
|
|
85
|
+
loader = new(SerialPort.open(port, ROM_BAUD), log: log)
|
|
86
|
+
begin
|
|
87
|
+
loader.connect(reset: reset)
|
|
88
|
+
loader.check_chip(chip)
|
|
89
|
+
loader.disable_watchdogs
|
|
90
|
+
loader.run_stub if stub
|
|
91
|
+
loader.attach_flash(flash_size)
|
|
92
|
+
loader.change_baud(baud) if baud && baud != ROM_BAUD
|
|
93
|
+
loader
|
|
94
|
+
rescue StandardError
|
|
95
|
+
loader.close
|
|
96
|
+
raise
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def initialize(port, log: $stdout)
|
|
101
|
+
@port = port
|
|
102
|
+
@log = log
|
|
103
|
+
@buffer = "".b
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Resets into the ROM loader (when the port can) and syncs, trying the reset sequences esptool uses.
|
|
107
|
+
def connect(reset: true, attempts: 7)
|
|
108
|
+
@log.print "Connecting to #{@port.path}..."
|
|
109
|
+
strategies = reset && @port.resettable? ? reset_strategies : [nil]
|
|
110
|
+
attempts.times do |i|
|
|
111
|
+
strategy = strategies[i % strategies.size]
|
|
112
|
+
send(*strategy) if strategy
|
|
113
|
+
@port.discard_input
|
|
114
|
+
@buffer.clear
|
|
115
|
+
if sync
|
|
116
|
+
@log.puts " connected."
|
|
117
|
+
return
|
|
118
|
+
end
|
|
119
|
+
@log.print "."
|
|
120
|
+
end
|
|
121
|
+
@log.puts
|
|
122
|
+
raise Error, "Failed to connect to the chip on #{@port.path}. " \
|
|
123
|
+
"Hold BOOT (GPIO0) while pressing RESET to enter the download mode, and try again."
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The chip the ROM reports, checked against what the image was built for.
|
|
127
|
+
def check_chip(expected)
|
|
128
|
+
@chip = detect_chip
|
|
129
|
+
@log.puts "Chip is #{@chip || "unknown"}"
|
|
130
|
+
return if expected.nil? || @chip.nil? || @chip == expected
|
|
131
|
+
|
|
132
|
+
raise Error, "This image is for #{expected}, but the connected chip is #{@chip}."
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def disable_watchdogs
|
|
136
|
+
return unless @port.usb_jtag_serial? != false && (regs = WATCHDOGS[@chip])
|
|
137
|
+
|
|
138
|
+
wdt_protect, wdt_config, swd_protect, swd_config, swd_auto_feed, swd_key = regs
|
|
139
|
+
write_reg(wdt_protect, WDT_KEY)
|
|
140
|
+
write_reg(wdt_config, 0)
|
|
141
|
+
write_reg(wdt_protect, 0)
|
|
142
|
+
write_reg(swd_protect, swd_key)
|
|
143
|
+
write_reg(swd_config, read_reg(swd_config) | swd_auto_feed)
|
|
144
|
+
write_reg(swd_protect, 0)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Uploads the flasher stub for the chip into its RAM and runs it. Nothing happens when there is no stub
|
|
148
|
+
# for the chip, or the port is a TCP socket (QEMU, which cannot run the stub).
|
|
149
|
+
def run_stub
|
|
150
|
+
return if @stub || !@port.resettable? || @chip.nil?
|
|
151
|
+
|
|
152
|
+
file = File.join(STUB_DIR, "#{@chip}.json")
|
|
153
|
+
return unless File.exist?(file)
|
|
154
|
+
|
|
155
|
+
stub = JSON.parse(File.read(file))
|
|
156
|
+
%w[text data].each do |segment|
|
|
157
|
+
next unless stub[segment]
|
|
158
|
+
|
|
159
|
+
upload_to_ram(stub[segment].unpack1("m"), stub["#{segment}_start"])
|
|
160
|
+
end
|
|
161
|
+
# The response to MEM_END may be lost as the stub takes over the UART; its greeting is what counts.
|
|
162
|
+
send_packet(MEM_END, [0, stub["entry"]].pack("VV"))
|
|
163
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 3
|
|
164
|
+
loop do
|
|
165
|
+
break if read_packet(deadline) == "OHAI"
|
|
166
|
+
end
|
|
167
|
+
@stub = true
|
|
168
|
+
@log.puts "Stub flasher running."
|
|
169
|
+
rescue TimeoutError
|
|
170
|
+
raise Error, "The flasher stub did not start. Try again with NO_STUB=1 (writes through the ROM loader)."
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Enables the default SPI flash pins and tells the flash geometry.
|
|
174
|
+
def attach_flash(flash_size)
|
|
175
|
+
# The ROM takes an extra "is legacy" word
|
|
176
|
+
command(SPI_ATTACH, @stub ? [0].pack("V") : [0, 0].pack("VV"))
|
|
177
|
+
size = flash_size || 16 * 1024 * 1024
|
|
178
|
+
command(SPI_SET_PARAMS, [0, size, 64 * 1024, 4 * 1024, 256, 0xffff].pack("V6"))
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def change_baud(baud)
|
|
182
|
+
return unless @port.resettable? # a TCP socket has no baud rate
|
|
183
|
+
|
|
184
|
+
# The stub also takes the current baud rate
|
|
185
|
+
command(CHANGE_BAUDRATE, [baud, @stub ? ROM_BAUD : 0].pack("VV"))
|
|
186
|
+
@port.baud = baud
|
|
187
|
+
sleep 0.05
|
|
188
|
+
@port.discard_input
|
|
189
|
+
@buffer.clear
|
|
190
|
+
@log.puts "Changed baud rate to #{baud}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Writes +data+ at +offset+ compressed, then checks it against the MD5 the chip computes.
|
|
194
|
+
def write_flash(offset, data, name: nil)
|
|
195
|
+
data = data.b
|
|
196
|
+
data += "\xff".b * (-data.bytesize % 4)
|
|
197
|
+
compressed = Zlib::Deflate.deflate(data, Zlib::BEST_COMPRESSION)
|
|
198
|
+
block_size = @stub ? STUB_BLOCK_SIZE : BLOCK_SIZE
|
|
199
|
+
blocks = (compressed.bytesize + block_size - 1) / block_size
|
|
200
|
+
label = format("0x%08x", offset)
|
|
201
|
+
label += " (#{name})" if name
|
|
202
|
+
|
|
203
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
204
|
+
if @stub
|
|
205
|
+
# The stub erases as it writes
|
|
206
|
+
command(FLASH_DEFL_BEGIN, [data.bytesize, blocks, block_size, offset, 0].pack("V5"))
|
|
207
|
+
else
|
|
208
|
+
# The ROM erases the whole region first
|
|
209
|
+
erase_size = (data.bytesize + block_size - 1) / block_size * block_size
|
|
210
|
+
params = [erase_size, blocks, block_size, offset].pack("V4")
|
|
211
|
+
params += [0].pack("V") if encrypted_flag?
|
|
212
|
+
command(FLASH_DEFL_BEGIN, params, timeout: timeout_per_mb(30, erase_size))
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
blocks.times do |seq|
|
|
216
|
+
chunk = compressed.byteslice(seq * block_size, block_size)
|
|
217
|
+
command(FLASH_DEFL_DATA, [chunk.bytesize, seq, 0, 0].pack("V4") + chunk,
|
|
218
|
+
checksum: checksum(chunk), timeout: 10)
|
|
219
|
+
progress(label, seq + 1, blocks)
|
|
220
|
+
end
|
|
221
|
+
# The stub acknowledges a block before writing it; this returns once the last one is written.
|
|
222
|
+
command(FLASH_DEFL_END, [1].pack("V"), timeout: 10) if @stub
|
|
223
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
224
|
+
@log.puts format("%sWrote %d bytes (%d compressed) at %s in %.1f s", tty? ? "\r" : "", data.bytesize,
|
|
225
|
+
compressed.bytesize, label, elapsed)
|
|
226
|
+
|
|
227
|
+
verify(offset, data)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Resets the chip into the application (the ROM loader is left as it is when the port cannot reset).
|
|
231
|
+
def hard_reset
|
|
232
|
+
return unless @port.resettable?
|
|
233
|
+
|
|
234
|
+
@log.puts "Hard resetting via RTS pin..."
|
|
235
|
+
EspFlash.hard_reset(@port)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def close
|
|
239
|
+
@port.close
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
private
|
|
243
|
+
|
|
244
|
+
def reset_strategies
|
|
245
|
+
usb_jtag = @port.usb_jtag_serial?
|
|
246
|
+
return [[:usb_jtag_reset]] if usb_jtag
|
|
247
|
+
|
|
248
|
+
tight = @port.is_a?(SerialPort::PosixPort)
|
|
249
|
+
list = [0.05, 0.55].flat_map do |delay|
|
|
250
|
+
[([:tight_reset, delay] if tight), [:classic_reset, delay]].compact
|
|
251
|
+
end
|
|
252
|
+
usb_jtag.nil? ? list + [[:usb_jtag_reset]] : list
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# The reset sequences of esptool (esptool/reset.py). On a board's auto-reset circuit DTR drives
|
|
256
|
+
# GPIO0 and RTS drives EN; "true" asserts a line, which pulls the pin low.
|
|
257
|
+
def classic_reset(delay)
|
|
258
|
+
@port.set_lines(dtr: false)
|
|
259
|
+
@port.set_lines(rts: true) # EN low: in reset
|
|
260
|
+
sleep 0.1
|
|
261
|
+
@port.set_lines(dtr: true) # GPIO0 low
|
|
262
|
+
@port.set_lines(rts: false) # EN high: out of reset, into the download mode
|
|
263
|
+
sleep delay
|
|
264
|
+
@port.set_lines(dtr: false)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Like classic_reset, with both lines changed at once (some USB-UART bridges need that).
|
|
268
|
+
def tight_reset(delay)
|
|
269
|
+
@port.set_lines(dtr: false, rts: false)
|
|
270
|
+
@port.set_lines(dtr: true, rts: true)
|
|
271
|
+
@port.set_lines(dtr: false, rts: true)
|
|
272
|
+
sleep 0.1
|
|
273
|
+
@port.set_lines(dtr: true, rts: false)
|
|
274
|
+
sleep delay
|
|
275
|
+
@port.set_lines(dtr: false, rts: false)
|
|
276
|
+
@port.set_lines(dtr: false)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# For the USB-Serial/JTAG controller, which must go through (1, 1) instead of (0, 0).
|
|
280
|
+
def usb_jtag_reset
|
|
281
|
+
@port.set_lines(rts: false)
|
|
282
|
+
@port.set_lines(dtr: false)
|
|
283
|
+
sleep 0.1
|
|
284
|
+
@port.set_lines(dtr: true)
|
|
285
|
+
@port.set_lines(rts: false)
|
|
286
|
+
sleep 0.1
|
|
287
|
+
@port.set_lines(rts: true)
|
|
288
|
+
@port.set_lines(dtr: false)
|
|
289
|
+
@port.set_lines(rts: true)
|
|
290
|
+
sleep 0.1
|
|
291
|
+
@port.set_lines(dtr: false)
|
|
292
|
+
@port.set_lines(rts: false)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def sync
|
|
296
|
+
payload = [0x07, 0x07, 0x12, 0x20].pack("C4") + ("\x55".b * 32)
|
|
297
|
+
5.times do
|
|
298
|
+
begin
|
|
299
|
+
command(SYNC, payload, timeout: 0.1)
|
|
300
|
+
# The ROM answers a SYNC several times; drop the rest.
|
|
301
|
+
sleep 0.05
|
|
302
|
+
@port.discard_input
|
|
303
|
+
@buffer.clear
|
|
304
|
+
return true
|
|
305
|
+
rescue Error
|
|
306
|
+
next
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
false
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def detect_chip
|
|
313
|
+
info = command(GET_SECURITY_INFO, "", timeout: 1)
|
|
314
|
+
if info.bytesize >= 20
|
|
315
|
+
chip_id = info.byteslice(12, 4).unpack1("V")
|
|
316
|
+
found = CHIPS.find { |_name, (id, _magic)| id == chip_id }
|
|
317
|
+
return found.first if found
|
|
318
|
+
end
|
|
319
|
+
magic_chip
|
|
320
|
+
rescue Error
|
|
321
|
+
magic_chip # the ESP32 ROM does not know GET_SECURITY_INFO
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def magic_chip
|
|
325
|
+
magic = read_reg(CHIP_DETECT_MAGIC_REG)
|
|
326
|
+
CHIPS.find { |_name, (_id, values)| values.include?(magic) }&.first
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def read_reg(address)
|
|
330
|
+
command(READ_REG, [address].pack("V"), want_value: true)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def write_reg(address, value)
|
|
334
|
+
command(WRITE_REG, [address, value, 0xffffffff, 0].pack("V4"))
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def upload_to_ram(data, address)
|
|
338
|
+
blocks = (data.bytesize + RAM_BLOCK_SIZE - 1) / RAM_BLOCK_SIZE
|
|
339
|
+
command(MEM_BEGIN, [data.bytesize, blocks, RAM_BLOCK_SIZE, address].pack("V4"))
|
|
340
|
+
blocks.times do |seq|
|
|
341
|
+
chunk = data.byteslice(seq * RAM_BLOCK_SIZE, RAM_BLOCK_SIZE)
|
|
342
|
+
command(MEM_DATA, [chunk.bytesize, seq, 0, 0].pack("V4") + chunk, checksum: checksum(chunk))
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# The ROM loader ends every response with 4 status bytes, the stub with 2.
|
|
347
|
+
def status_bytes
|
|
348
|
+
@stub ? 2 : 4
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def encrypted_flag?
|
|
352
|
+
# Every ROM loader after the ESP32's takes an extra "encrypted" word in FLASH_(DEFL_)BEGIN.
|
|
353
|
+
@chip != "esp32"
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def verify(offset, data)
|
|
357
|
+
response = command(SPI_FLASH_MD5, [offset, data.bytesize, 0, 0].pack("V4"),
|
|
358
|
+
timeout: timeout_per_mb(8, data.bytesize))
|
|
359
|
+
# The stub answers with the 16 bytes of the MD5, the ROM with them in hex
|
|
360
|
+
actual = @stub ? response.byteslice(0, 16).unpack1("H*") : response.byteslice(0, 32).downcase
|
|
361
|
+
expected = Digest::MD5.hexdigest(data)
|
|
362
|
+
raise Error, format("Verification failed at 0x%08x: flash has MD5 %s, expected %s", offset, actual, expected) \
|
|
363
|
+
unless actual == expected
|
|
364
|
+
|
|
365
|
+
@log.puts "Hash of data verified."
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def checksum(data)
|
|
369
|
+
data.each_byte.reduce(CHECKSUM_SEED) { |sum, byte| sum ^ byte }
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def timeout_per_mb(seconds, size)
|
|
373
|
+
[3.0, seconds * size / 1_000_000.0].max
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def tty?
|
|
377
|
+
@log.respond_to?(:tty?) && @log.tty?
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def progress(label, done, total)
|
|
381
|
+
return unless tty?
|
|
382
|
+
return unless done == total || (done % 16).zero?
|
|
383
|
+
|
|
384
|
+
@log.print format("\rWriting at %s... (%d %%)", label, done * 100 / total)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Sends a command and returns the data of its response (or its 32-bit value when +want_value+).
|
|
388
|
+
def command(op, data, checksum: 0, timeout: 3, want_value: false)
|
|
389
|
+
send_packet(op, data, checksum)
|
|
390
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
391
|
+
loop do
|
|
392
|
+
packet = read_packet(deadline)
|
|
393
|
+
next if packet.bytesize < 8
|
|
394
|
+
|
|
395
|
+
direction, resp_op, _size, value = packet.unpack("CCvV")
|
|
396
|
+
next unless direction == 0x01 && resp_op == op
|
|
397
|
+
|
|
398
|
+
body = packet.byteslice(8..)
|
|
399
|
+
status = body.byteslice(-status_bytes, status_bytes) if body.bytesize >= status_bytes
|
|
400
|
+
raise Error, format("Command 0x%02x: no status in the response", op) unless status
|
|
401
|
+
if status.getbyte(0) != 0
|
|
402
|
+
code = status.getbyte(1)
|
|
403
|
+
raise Error, format("Command 0x%02x failed: %s (0x%02x)", op, ROM_ERRORS.fetch(code, "unknown error"), code)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
return want_value ? value : body.byteslice(0, body.bytesize - status_bytes)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def send_packet(op, data, checksum = 0)
|
|
411
|
+
@port.write(slip_encode([0x00, op, data.bytesize, checksum].pack("CCvV") + data))
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def slip_encode(packet)
|
|
415
|
+
"\xc0".b + packet.b.gsub("\xdb".b, "\xdb\xdd".b).gsub("\xc0".b, "\xdb\xdc".b) + "\xc0".b
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# The next SLIP frame, skipping anything outside frames (such as the boot log).
|
|
419
|
+
def read_packet(deadline)
|
|
420
|
+
loop do
|
|
421
|
+
if (start = @buffer.index("\xc0".b))
|
|
422
|
+
if (stop = @buffer.index("\xc0".b, start + 1))
|
|
423
|
+
frame = @buffer.byteslice(start + 1, stop - start - 1)
|
|
424
|
+
@buffer = @buffer.byteslice(stop..) # the closing 0xc0 may open the next frame
|
|
425
|
+
next if frame.empty?
|
|
426
|
+
|
|
427
|
+
return frame.gsub(/\xdb[\xdc\xdd]/n) { |escape| escape.getbyte(1) == 0xdc ? "\xc0".b : "\xdb".b }
|
|
428
|
+
end
|
|
429
|
+
@buffer = @buffer.byteslice(start..)
|
|
430
|
+
else
|
|
431
|
+
@buffer.clear
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
435
|
+
raise TimeoutError, "Timed out waiting for a response from the chip" if remaining <= 0
|
|
436
|
+
|
|
437
|
+
@buffer << @port.read(4096, remaining)
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|