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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
|
//! POSIX serial port: raw mode, standard baud rates, and the DTR/RTS lines that put an Espressif
//! chip into download mode. No libc, no external tool - std.posix for termios, std.os.linux for
//! the two modem-control ioctls std does not wrap, std.Io.File for the byte traffic.
//!
//! On this board DTR drives the boot strap (GPIO35) and RTS drives CHIP_PU through a transistor
//! pair, which is why the reset sequence below needs no button press.
const std = @import("std");
const posix = std.posix;
const linux = std.os.linux;
/// Any rate the hardware can divide to, not just the historical enum values: the port is
/// configured through termios2, where the baud is a plain integer. The ESP32 ROM loader
/// auto-detects the host's rate from the 0x55 pattern in SYNC, so raising this needs no
/// CHANGE_BAUDRATE handshake.
pub const Baud = enum(u32) {
b115200 = 115200,
b230400 = 230400,
b460800 = 460800,
b921600 = 921600,
b1500000 = 1500000,
b2000000 = 2000000,
pub fn rate(b: Baud) u32 {
return @intFromEnum(b);
}
};
/// `struct termios2` as the Linux kernel defines it: 19 control characters, then the two integer
/// baud fields. std's `linux.termios2` uses NCCS = 32, which makes it 60 bytes instead of 44 and
/// therefore encodes TCGETS2/TCSETS2 with the wrong size field - the ioctl then fails with ENOTTY.
/// Declaring the struct here keeps the numbers right and the intent visible.
const Termios2 = extern struct {
iflag: u32,
oflag: u32,
cflag: u32,
lflag: u32,
line: u8,
cc: [19]u8,
ispeed: u32,
ospeed: u32,
/// _IOR('T', 0x2A, struct termios2) and _IOW('T', 0x2B, struct termios2) for a 44-byte struct.
const TCGETS2: u32 = 0x802C542A;
const TCSETS2: u32 = 0x402C542B;
/// CBAUD escape meaning "take the rate from ispeed/ospeed" (asm-generic/termbits.h).
const BOTHER: u32 = 0o010000;
const CBAUD: u32 = 0o010017;
const CS8: u32 = 0o000060;
const CREAD: u32 = 0o000200;
const CLOCAL: u32 = 0o004000;
};
pub const Port = struct {
file: std.Io.File,
io: std.Io,
saved: Termios2,
// std exposes neither these ioctl numbers nor the TIOCM bits.
const TIOCEXCL = 0x540C;
const TCFLSH = 0x540B;
const TCIFLUSH = 0;
const TIOCMGET = 0x5415;
const TIOCMSET = 0x5418;
const DTR: u32 = 0x002;
const RTS: u32 = 0x004;
pub fn open(path: []const u8, baud: Baud) !Port {
const fd = try posix.openat(posix.AT.FDCWD, path, .{
.ACCMODE = .RDWR,
.NOCTTY = true,
.CLOEXEC = true,
}, 0);
const file: std.Io.File = .{ .handle = fd, .flags = .{ .nonblocking = false } };
const io = std.Io.Threaded.global_single_threaded.io();
errdefer file.close(io);
// Configure through termios2: it is the only interface whose baud fields the kernel
// populates. `tcsetattr` uses TCSETS, whose struct has no ispeed/ospeed, so assigning
// those fields silently does nothing and leaves the port at whatever rate it had - which
// is exactly the bug that made a 1.5 KB flash take 230 ms instead of 60.
var saved: Termios2 = undefined;
if (@as(isize, @bitCast(linux.ioctl(fd, Termios2.TCGETS2, @intFromPtr(&saved)))) < 0)
return error.NotATerminal;
var raw = saved;
// A raw byte pipe: no canonical mode, no echo, no signals, no flow control, no CR/LF
// translation, 8N1, CLOCAL so a missing carrier-detect cannot block reads, and the baud
// taken from the integer fields.
raw.iflag = 0;
raw.oflag = 0;
raw.lflag = 0;
raw.cflag = Termios2.CS8 | Termios2.CREAD | Termios2.CLOCAL | Termios2.BOTHER;
raw.ispeed = baud.rate();
raw.ospeed = baud.rate();
@memset(&raw.cc, 0);
raw.cc[6] = 0; // VMIN: never block for a minimum count
raw.cc[5] = 0; // VTIME: poll() owns the timeouts
if (@as(isize, @bitCast(linux.ioctl(fd, Termios2.TCSETS2, @intFromPtr(&raw)))) < 0)
return error.SetAttrFailed;
// Claim the port exclusively, the way pyserial (and therefore esptool) does: otherwise a
// `zig build monitor` left running in another terminal splits the ROM's replies between
// two readers, and the failure looks like random SyncFailed.
if (@as(isize, @bitCast(linux.ioctl(fd, TIOCEXCL, 0))) < 0) return error.PortBusy;
// Drop anything the kernel captured at the previous line rate.
_ = linux.ioctl(fd, TCFLSH, TCIFLUSH);
return .{ .file = file, .io = io, .saved = saved };
}
pub fn close(p: *Port) void {
_ = linux.ioctl(p.file.handle, Termios2.TCSETS2, @intFromPtr(&p.saved));
p.file.close(p.io);
}
pub fn write(p: *Port, bytes: []const u8) !void {
try p.file.writeStreamingAll(p.io, bytes);
}
/// Read whatever is available, waiting at most `timeout_ms`. Returns 0 on timeout.
pub fn read(p: *Port, buf: []u8) !usize {
var pfd = [_]posix.pollfd{.{ .fd = p.file.handle, .events = posix.POLL.IN, .revents = 0 }};
const ready = try posix.poll(&pfd, 0);
if (ready == 0) return 0;
return p.file.readStreaming(p.io, &.{buf}) catch |err| switch (err) {
error.EndOfStream => 0,
else => err,
};
}
pub fn readTimeout(p: *Port, buf: []u8, timeout_ms: i32) !usize {
var pfd = [_]posix.pollfd{.{ .fd = p.file.handle, .events = posix.POLL.IN, .revents = 0 }};
const ready = try posix.poll(&pfd, timeout_ms);
if (ready == 0) return 0;
return p.file.readStreaming(p.io, &.{buf}) catch |err| switch (err) {
error.EndOfStream => 0,
else => err,
};
}
pub fn drain(p: *Port) void {
var scratch: [512]u8 = undefined;
while (true) {
const n = p.read(&scratch) catch return;
if (n == 0) return;
}
}
fn setLines(p: *Port, dtr: bool, rts: bool) !void {
var flags: u32 = 0;
if (linux.ioctl(p.file.handle, TIOCMGET, @intFromPtr(&flags)) != 0) return error.IoctlFailed;
flags = if (dtr) flags | DTR else flags & ~DTR;
flags = if (rts) flags | RTS else flags & ~RTS;
if (linux.ioctl(p.file.handle, TIOCMSET, @intFromPtr(&flags)) != 0) return error.IoctlFailed;
}
/// How long to hold each phase of the reset sequence. esptool uses 100/50/50 ms; the shorter
/// numbers below were measured on this board over repeated runs. Tunable because a different
/// carrier's RC network may need longer.
pub const ResetTiming = struct {
hold_reset_ms: i64 = 40,
strap_settle_ms: i64 = 25,
release_ms: i64 = 15,
};
/// Classic reset into the ROM download loader: hold the boot strap asserted across a reset
/// pulse. Both lines are inverted by the board's transistor pair, so an asserted RS-232 line
/// pulls its pin low.
pub fn resetToDownload(p: *Port, timing: ResetTiming) !void {
// Never leave the board held in reset because a modem-line ioctl failed halfway through.
errdefer p.setLines(false, false) catch {};
try p.setLines(false, true); // strap released, EN low: held in reset
p.sleepMs(timing.hold_reset_ms);
try p.setLines(true, false); // strap asserted, EN high: enters the ROM loader
p.sleepMs(timing.strap_settle_ms);
try p.setLines(false, false);
p.sleepMs(timing.release_ms);
p.drain();
}
/// Reset and let the flashed application run.
pub fn resetToRun(p: *Port, timing: ResetTiming) !void {
errdefer p.setLines(false, false) catch {};
try p.setLines(false, true);
p.sleepMs(timing.hold_reset_ms);
try p.setLines(false, false);
p.sleepMs(timing.release_ms);
}
fn sleepMs(p: *Port, ms: i64) void {
std.Io.sleep(p.io, .fromMilliseconds(ms), .boot) catch {};
}
/// Milliseconds on a monotonic clock, for deadlines.
pub fn nowMs(p: *Port) i64 {
return std.Io.Timestamp.now(p.io, .boot).toMilliseconds();
}
};
|