The Pedigree Project 0.1
AhciPort.cc
1/*
2 * Copyright (c) 2026, Pedigree Developers
3 * SPDX-License-Identifier: ISC
4 *
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16#include "AhciPort.h"
17#include "pedigree/kernel/LockGuard.h"
18#include "pedigree/kernel/Log.h"
19#include "pedigree/kernel/TargetInfo.h"
20#include "pedigree/kernel/panic.h"
21#include "pedigree/kernel/process/Scheduler.h"
22#include "pedigree/kernel/process/TerminationDeferral.h"
23#include "pedigree/kernel/process/Thread.h"
24#include "pedigree/kernel/processor/IoBase.h"
25#include "pedigree/kernel/processor/PhysicalMemoryManager.h"
26#include "pedigree/kernel/processor/Processor.h"
27#include "pedigree/kernel/processor/ProcessorInformation.h"
28#include "pedigree/kernel/processor/VirtualAddressSpace.h"
29#include "pedigree/kernel/time/Time.h"
30#include "pedigree/kernel/utilities/utility.h"
31
32#include "Registers.h"
33
34using namespace Ahci;
35
36AhciPort::AhciPort(IoBase* registers, size_t port)
37 : m_Registers(registers),
38 m_Port(port),
39 m_Control("AHCI command storage"),
40 m_Online(false),
41 m_Active(0),
42 m_Queued(0),
43 m_SlotCount(1),
44 m_QueueDepth(0),
45 m_SectorBytes(512),
46 m_SupportsNcq(false),
47 m_AddressesInstalled(false),
48 m_PolledInterrupt(false),
49 m_InterruptCompletions(0),
50 m_MaximumOutstanding(0),
51 m_Outstanding(0) {}
52AhciPort::~AhciPort() {
53 shutdown();
54}
55uint32_t AhciPort::read(size_t reg) const {
56 return m_Registers->read32(PortBase + m_Port * PortStride + reg);
57}
58void AhciPort::write(size_t reg, uint32_t value) {
59 m_Registers->write32(value, PortBase + m_Port * PortStride + reg);
60}
61void AhciPort::waitForProgress() {
62 Thread* thread = Processor::information().getCurrentThread();
63 if (!thread || !Processor::getInterrupts()) {
65 } else if (thread->eventsDeferred()) {
66 // Mmap faults suppress timeout events along with other event callbacks.
68 } else {
69 Time::delay(Time::Multiplier::Millisecond);
70 }
71}
72bool AhciPort::waitClear(size_t reg, uint32_t bits, size_t milliseconds) {
73 const auto deadline = Time::getTicks() + milliseconds * Time::Multiplier::Millisecond;
74 do {
75 if (!(read(reg) & bits))
76 return true;
77 waitForProgress();
78 } while (Time::getTicks() < deadline);
79 return !(read(reg) & bits);
80}
81bool AhciPort::stopEngines() {
82 // AHCI 10.3: FRE must remain set until the command-list engine has stopped.
83 write(Cmd, read(Cmd) & ~Start);
84 if (!waitClear(Cmd, CommandRunning, 500))
85 return false;
86 write(Cmd, read(Cmd) & ~FisEnable);
87 return waitClear(Cmd, FisRunning, 500);
88}
89void AhciPort::acknowledge(uint32_t status) {
90 // Some port status bits are backed by SError diagnostic bits.
91 const uint32_t error = read(Serr);
92 if (error)
93 write(Serr, error);
94 if (status)
95 write(PortIs, status);
96}
97bool AhciPort::initialise(uint32_t capabilities, uint32_t version, uint32_t extendedCapabilities) {
98 write(PortIe, 0);
99 if (!stopEngines()) {
100 ERROR("AHCI: port " << m_Port << " firmware engines did not stop");
101 return false;
102 }
103 const size_t pageSize = TargetInfo::getPageSize();
104 m_SlotCount = ((capabilities >> 8) & 31U) + 1;
105 m_SupportsNcq = capabilities & (1U << 30);
106 const size_t controlBytes = TableOffset + m_SlotCount * TableStride;
107 if (pageSize < 4096 || pageSize > MaxTransfer || (MaxTransfer % pageSize))
108 return false;
109 auto& memory = PhysicalMemoryManager::instance();
112 if (!memory.allocateRegion(m_Control, (controlBytes + pageSize - 1) / pageSize, constraints,
113 flags))
114 return false;
115 ByteSet(m_Control.virtualAddress(), 0, m_Control.size());
116 for (size_t i = 0; i < m_SlotCount; ++i) {
117 // Contiguous allocations currently consume the scarce ISA DMA pool.
118 // PRDs allow each bounce page to come from ordinary memory below 4 GiB.
119 if (!memory.allocateRegion(m_Slots[i].data, MaxTransfer / pageSize,
121 return false;
122 ByteSet(m_Slots[i].data.virtualAddress(), 0, m_Slots[i].data.size());
123 for (size_t page = 0; page < MaxTransfer / pageSize; ++page) {
124 size_t mappingFlags = 0;
126 static_cast<uint8_t*>(m_Slots[i].data.virtualAddress()) + page * pageSize,
127 m_Slots[i].pages[page], mappingFlags);
128 if (m_Slots[i].pages[page] >= (uint64_t{1} << 32))
129 return false;
130 }
131 }
132 FENCE();
133 write(Clb, static_cast<uint32_t>(m_Control.physicalAddress()));
134 write(Clbu, 0);
135 write(Fb, static_cast<uint32_t>(m_Control.physicalAddress() + FisOffset));
136 write(Fbu, 0);
137 m_AddressesInstalled = true;
138
139 uint32_t cmd = read(Cmd) & ~(Atapi | AggressivePower | IccMask);
140 if (cmd & ColdPresence)
141 cmd |= PowerOn;
142 if (capabilities & StaggeredSpinup)
143 cmd |= Spinup;
144 // Disable automatic device sleep only when its register is implemented.
145 if (version >= 0x00010300 && (extendedCapabilities & (1U << 3)))
146 write(Devslp, read(Devslp) & ~1U);
147 write(Cmd, cmd | FisEnable);
148 uint32_t control = read(Sctl) & 0xf0U; // Retain firmware's speed restriction.
149 control |= (version >= 0x00010300 ? 7U : 3U) << 8;
150 write(Serr, 0xffffffffU);
151 write(Sctl, control | 1U);
152 const auto resetUntil = Time::getTicks() + Time::Multiplier::Millisecond;
153 while (Time::getTicks() < resetUntil)
154 waitForProgress();
155 write(Sctl, control);
156 const auto linkDeadline = Time::getTicks() + Time::Multiplier::Second;
157 while ((read(Ssts) & 15U) != 3U && Time::getTicks() < linkDeadline)
158 waitForProgress();
159 write(Serr, 0xffffffffU);
160 if ((read(Ssts) & 15U) != 3U)
161 return false;
162 write(Cmd, (read(Cmd) & ~IccMask) | IccActive);
163 if (!waitClear(Tfd, Busy | DataRequest, 30000)) {
164 WARNING("AHCI: port " << m_Port << " device not ready after COMRESET");
165 return false;
166 }
167 if (read(Sig) != SataDisk) {
168 NOTICE("AHCI: port " << m_Port << " unsupported signature " << Hex << read(Sig));
169 return false;
170 }
171 if ((read(Cmd) & CommandRunning) || read(Ci) || read(Sact))
172 return false;
173 acknowledge(read(PortIs));
174 write(Cmd, read(Cmd) | Start);
175 (void)read(Cmd);
176 m_Online = true;
177 return true;
178}
179void AhciPort::enableInterrupts() {
180 LockGuard<Mutex> state(m_StateLock);
181 acknowledge(read(PortIs));
182 if (m_Online)
183 write(PortIe, PortInterrupts);
184}
185void AhciPort::configureDisk(size_t sectorBytes, size_t queueDepth) {
186 m_SectorBytes = sectorBytes;
187 m_QueueDepth = m_SupportsNcq ? (queueDepth < m_SlotCount ? queueDepth : m_SlotCount) : 0;
188 NOTICE("AHCI: port " << m_Port << " NCQ depth " << Dec << m_QueueDepth << Hex);
189}
190void AhciPort::observe(uint32_t status, bool fromInterrupt) {
191 if (!m_Active)
192 return;
193 uint32_t errors = status & PortErrors;
194 if (read(Tfd) & (TaskError | DeviceFault))
195 errors |= TaskFileError;
196 if ((read(Ssts) & 15U) != 3U)
197 errors |= 1U << 22;
198 // Without READ LOG EXT attribution, no outstanding tag can be trusted after
199 // an NCQ error. Stop admission before any slot is retired or reused.
200 if (errors) {
201 m_Online = false;
202 write(PortIe, 0);
203 }
204 const uint32_t pending = (read(Sact) & m_Queued) | (read(Ci) & ~m_Queued);
205 for (size_t i = 0; i < m_SlotCount; ++i) {
206 if (!(m_Active & (1U << i)))
207 continue;
208 Slot& slot = m_Slots[i];
209 slot.errors |= errors;
210 if (!slot.done && (slot.errors || !(pending & (1U << i)))) {
211 slot.done = true;
212 --m_Outstanding;
213 if (fromInterrupt)
214 ++m_InterruptCompletions;
215 slot.completion.release();
216 }
217 }
218}
219void AhciPort::pollCompletions(bool interrupts) {
220 const uint32_t status = read(PortIs);
221 // Polling can withdraw INTx before its already-dispatched IRQ worker runs.
222 // Capture enabled causes before observe() can disable a failed port's IRQs.
223 if (interrupts && (status & read(PortIe)))
224 m_PolledInterrupt = true;
225 observe(status, false);
226 acknowledge(status);
227}
228bool AhciPort::interrupt(bool pending) {
229 LockGuard<Mutex> state(m_StateLock);
230 const bool credited = m_PolledInterrupt;
231 m_PolledInterrupt = false;
232 const uint32_t status = pending ? read(PortIs) : 0;
233 if (!status)
234 return credited;
235 observe(status, true);
236 acknowledge(status);
237 return true;
238}
239bool AhciPort::chooseSlot(bool queued, size_t& index) {
240 LockGuard<Mutex> state(m_StateLock);
241 index = 32;
242 if (!m_Online)
243 return false;
244 if (queued || !m_Active) {
245 const size_t count = queued ? m_QueueDepth : 1;
246 for (size_t i = 0; i < count; ++i) {
247 if (!(m_Active & (1U << i))) {
248 index = i;
249 break;
250 }
251 }
252 }
253 return true;
254}
255
256bool AhciPort::issueCommand(size_t index, uint8_t opcode, uint64_t lba, uint16_t sectors,
257 void* buffer, size_t bytes, bool writing, bool queued,
258 bool interrupts) {
259 Slot& slot = m_Slots[index];
260 const uint32_t mask = 1U << index;
261 auto* header = static_cast<CommandHeader*>(m_Control.virtualAddress()) + index;
262 const size_t tableOffset = TableOffset + index * TableStride;
263 auto* table = reinterpret_cast<CommandTable*>(
264 reinterpret_cast<uintptr_t>(m_Control.virtualAddress()) + tableOffset);
265 {
266 LockGuard<Mutex> state(m_StateLock);
267 if (!m_Online || (!queued && (read(Ci) || read(Sact) || (read(Tfd) & (Busy | DataRequest)))))
268 return false;
269 ByteSet(header, 0, sizeof(*header));
270 ByteSet(table, 0, sizeof(*table));
271 if (writing && bytes)
272 MemoryCopy(slot.data.virtualAddress(), buffer, bytes);
273 const size_t pageSize = TargetInfo::getPageSize();
274 const size_t prds = (bytes + pageSize - 1) / pageSize;
275 header->flags = 5U | (writing ? 1U << 6 : 0U) | (prds << 16);
276 header->table = static_cast<uint32_t>(m_Control.physicalAddress() + tableOffset);
277 table->fis[0] = 0x27;
278 table->fis[1] = 0x80;
279 table->fis[2] = opcode;
280 if (queued || opcode == 0x25 || opcode == 0x35)
281 table->fis[7] = 0x40;
282 for (size_t i = 0; i < 3; ++i) {
283 table->fis[4 + i] = static_cast<uint8_t>(lba >> (i * 8));
284 table->fis[8 + i] = static_cast<uint8_t>(lba >> ((i + 3) * 8));
285 }
286 if (queued) {
287 table->fis[3] = static_cast<uint8_t>(sectors);
288 table->fis[11] = static_cast<uint8_t>(sectors >> 8);
289 table->fis[12] = static_cast<uint8_t>(index << 3);
290 } else {
291 table->fis[12] = static_cast<uint8_t>(sectors);
292 table->fis[13] = static_cast<uint8_t>(sectors >> 8);
293 }
294 for (size_t page = 0; page < prds; ++page) {
295 const size_t remaining = bytes - page * pageSize;
296 const size_t count = remaining < pageSize ? remaining : pageSize;
297 table->data[page].address = static_cast<uint32_t>(slot.pages[page]);
298 table->data[page].byteCount = static_cast<uint32_t>(count - 1);
299 }
300 // Observe previous commands before acknowledging shared port status.
301 pollCompletions(interrupts);
302 if (!m_Online)
303 return false;
304 [[maybe_unused]] const size_t drained = slot.completion.drainAvailable();
305 slot.errors = 0;
306 slot.done = false;
307 m_Active |= mask;
308 ++m_Outstanding;
309 if (m_Outstanding > m_MaximumOutstanding)
310 m_MaximumOutstanding = m_Outstanding;
311 if (queued)
312 m_Queued |= mask;
313 FENCE();
314 if (queued)
315 write(Sact, mask);
316 const size_t timeoutSeconds = (opcode == 0xe7 || opcode == 0xea) ? 120 : 30;
317 slot.deadline = Time::getTicks() + timeoutSeconds * Time::Multiplier::Second;
318 write(Ci, mask);
319 (void)read(Ci);
320 }
321 return true;
322}
323
324bool AhciPort::reapCommand(size_t index, uint8_t opcode, void* buffer, size_t bytes, bool writing,
325 bool queued, bool interrupts, bool interruptProbe) {
326 Slot& slot = m_Slots[index];
327 const uint32_t mask = 1U << index;
328 auto* header = static_cast<CommandHeader*>(m_Control.virtualAddress()) + index;
329 bool success = false;
330 bool interruptGrace = interruptProbe;
331 for (;;) {
332 {
333 LockGuard<Mutex> state(m_StateLock);
334 // The readiness probe must observe a real IRQ before polling can
335 // consume its completion; ordinary owners check hardware before sleeping.
336 if (!slot.done && !interruptGrace) {
337 pollCompletions(interrupts);
338 }
339 if (slot.done || Time::getTicks() >= slot.deadline) {
340 FENCE();
341 // AHCI 5.4.1: PRDBC is not defined for native queued commands.
342 success = slot.done && !slot.errors && m_Online && !(read(queued ? Sact : Ci) & mask) &&
343 (queued || header->transferred == bytes);
344 if (!success) {
345 ERROR("AHCI: port " << m_Port << " command " << Hex << opcode << " tag " << index
346 << " failed, CI=" << read(Ci) << " SACT=" << read(Sact)
347 << " TFD=" << read(Tfd) << " errors=" << slot.errors);
348 m_Online = false;
349 write(PortIe, 0);
350 // Mark all owners failed before stopping engines clears hardware bits.
351 for (size_t i = 0; i < m_SlotCount; ++i) {
352 if (m_Active & (1U << i)) {
353 m_Slots[i].errors |= TaskFileError;
354 if (!m_Slots[i].done) {
355 m_Slots[i].done = true;
356 --m_Outstanding;
357 m_Slots[i].completion.release();
358 }
359 }
360 }
361 if (!stopEngines())
362 panic("AHCI: cannot stop failed port DMA; refusing to release memory");
363 } else if (!writing && bytes) {
364 MemoryCopy(buffer, slot.data.virtualAddress(), bytes);
365 }
366 m_Active &= ~mask;
367 m_Queued &= ~mask;
368 break;
369 }
370 }
371 Thread* thread = Processor::information().getCurrentThread();
372 if (interrupts && thread && Processor::getInterrupts() && !thread->eventsDeferred()) {
373 const bool acquired = slot.completion.acquireForCompletion(1, interruptGrace ? 1 : 0,
374 interruptGrace ? 0 : 10000);
375 (void)acquired;
376 } else {
377 waitForProgress();
378 }
379 interruptGrace = false;
380 }
381 return success;
382}
383
384bool AhciPort::command(uint8_t opcode, uint64_t lba, uint16_t sectors, void* buffer, size_t bytes,
385 bool writing, bool interrupts, bool interruptProbe) {
386 if (bytes > MaxTransfer || (bytes && (!buffer || (bytes & 1U))) || (lba >> 48))
387 return false;
388 TerminationDeferral lifetime;
389 LockGuard<Mutex> command(m_CommandLock);
390 const bool queued = m_QueueDepth && (opcode == 0x25 || opcode == 0x35);
391 if (queued)
392 opcode = writing ? 0x61 : 0x60;
393 const auto admissionDeadline = Time::getTicks() + 120 * Time::Multiplier::Second;
394 size_t index = 32;
395 while (true) {
396 if (!chooseSlot(queued, index))
397 return false;
398 if (index != 32)
399 break;
400 if (Time::getTicks() >= admissionDeadline)
401 return false;
402 waitForProgress();
403 }
404 if (!issueCommand(index, opcode, lba, sectors, buffer, bytes, writing, queued, interrupts))
405 return false;
406 if (queued) {
407 m_CommandLock.release();
408 command.disown();
409 }
410 return reapCommand(index, opcode, buffer, bytes, writing, queued, interrupts, interruptProbe);
411}
412
413bool AhciPort::readBatch(Disk::ReadBuffer* buffers, size_t count, bool interrupts) {
414 return transferBatch(buffers, count, interrupts, false);
415}
416
417bool AhciPort::writeBatch(Disk::WriteBuffer* buffers, size_t count, bool interrupts) {
418 static_assert(Disk::MaxWriteBuffers <= Disk::MaxReadBuffers);
419 if (count > Disk::MaxWriteBuffers || (count && !buffers))
420 return false;
421 Disk::ReadBuffer transfers[Disk::MaxWriteBuffers];
422 for (size_t i = 0; i < count; ++i) {
423 buffers[i].complete = false;
424 transfers[i] = {buffers[i].location, const_cast<void*>(buffers[i].buffer), buffers[i].length,
425 false};
426 }
427 const bool success = transferBatch(transfers, count, interrupts, true);
428 for (size_t i = 0; i < count; ++i)
429 buffers[i].complete = transfers[i].complete;
430 return success;
431}
432
433bool AhciPort::transferBatch(Disk::ReadBuffer* buffers, size_t count, bool interrupts,
434 bool writing) {
435 if (count > Disk::MaxReadBuffers || (count && !buffers))
436 return false;
437 for (size_t i = 0; i < count; ++i)
438 buffers[i].complete = false;
439 for (size_t i = 0; i < count; ++i) {
440 const auto& buffer = buffers[i];
441 if (!buffer.buffer || !buffer.length || buffer.length > TargetInfo::getPageSize() ||
442 buffer.length > MaxTransfer || !m_SectorBytes || buffer.location % m_SectorBytes ||
443 buffer.length % m_SectorBytes || buffer.location / m_SectorBytes >= (1ULL << 48) ||
444 buffer.length / m_SectorBytes > (1ULL << 48) - buffer.location / m_SectorBytes)
445 return false;
446 }
447 TerminationDeferral lifetime;
448 if (!m_QueueDepth) {
449 for (size_t i = 0; i < count; ++i) {
450 auto& buffer = buffers[i];
451 buffer.complete =
452 command(writing ? 0x35 : 0x25, buffer.location / m_SectorBytes,
453 buffer.length / m_SectorBytes, buffer.buffer, buffer.length, writing, interrupts);
454 if (!buffer.complete)
455 return false;
456 }
457 return true;
458 }
459
460 size_t next = 0;
461 while (next < count) {
462 size_t slots[Disk::MaxReadBuffers];
463 const size_t first = next;
464 size_t issued = 0;
465 bool admitted = true;
466 {
467 // A flush cannot overtake this wave. Reaping never requires this gate.
468 LockGuard<Mutex> command(m_CommandLock);
469 const auto admissionDeadline = Time::getTicks() + 120 * Time::Multiplier::Second;
470 while (next < count) {
471 size_t index = 32;
472 if (!chooseSlot(true, index)) {
473 admitted = false;
474 break;
475 }
476 if (index == 32) {
477 // Completed tags remain owned until reaped. Waiting here while owning
478 // tags could prevent this very batch from freeing the next slot.
479 if (issued)
480 break;
481 if (Time::getTicks() >= admissionDeadline) {
482 admitted = false;
483 break;
484 }
485 waitForProgress();
486 continue;
487 }
488 auto& buffer = buffers[next];
489 if (!issueCommand(index, writing ? 0x61 : 0x60, buffer.location / m_SectorBytes,
490 buffer.length / m_SectorBytes, buffer.buffer, buffer.length, writing,
491 true, interrupts)) {
492 admitted = false;
493 break;
494 }
495 slots[issued++] = index;
496 ++next;
497 }
498 }
499 bool succeeded = admitted;
500 for (size_t i = 0; i < issued; ++i) {
501 auto& buffer = buffers[first + i];
502 buffer.complete = reapCommand(slots[i], writing ? 0x61 : 0x60, buffer.buffer, buffer.length,
503 writing, true, interrupts, false);
504 succeeded = buffer.complete && succeeded;
505 }
506 if (!succeeded)
507 return false;
508 }
509 return true;
510}
511
512void AhciPort::shutdown() {
513 LockGuard<Mutex> command(m_CommandLock);
514 if (!m_AddressesInstalled)
515 return;
516 const auto deadline = Time::getTicks() + 120 * Time::Multiplier::Second;
517 for (;;) {
518 {
519 LockGuard<Mutex> state(m_StateLock);
520 if (!m_Active)
521 break;
522 }
523 if (Time::getTicks() >= deadline)
524 panic("AHCI: command owners did not drain during shutdown");
525 waitForProgress();
526 }
527 {
528 LockGuard<Mutex> state(m_StateLock);
529 m_Online = false;
530 write(PortIe, 0);
531 }
532 if (!stopEngines())
533 panic("AHCI: cannot stop port DMA during shutdown");
534 write(Clb, 0);
535 write(Clbu, 0);
536 write(Fb, 0);
537 write(Fbu, 0);
538 acknowledge(read(PortIs));
539 (void)read(Cmd);
540 m_AddressesInstalled = false;
541}
542size_t AhciPort::interruptCompletions() const {
543 LockGuard<Mutex> state(m_StateLock);
544 return m_InterruptCompletions;
545}
546
547size_t AhciPort::maximumOutstanding() const {
548 LockGuard<Mutex> state(m_StateLock);
549 return m_MaximumOutstanding;
550}
Abstrace base class for hardware I/O capabilities.
Definition IoBase.h:32
virtual void write32(uint32_t value, size_t offset=0)=0
virtual uint32_t read32(size_t offset=0)=0
void * virtualAddress() const
physical_uintptr_t physicalAddress() const
size_t size() const
static PhysicalMemoryManager & instance()
static bool getInterrupts()
static ProcessorInformation & information()
static void pause()
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:226
void release(size_t n=1)
Definition Semaphore.cc:546
static constexpr size_t getPageSize() noexcept
Definition TargetInfo.h:40
bool eventsDeferred() const
Definition Thread.cc:3180
virtual bool getMapping(void *virtualAddress, physical_uintptr_t &physicalAddress, size_t &flags)=0
static EXPORTED_PUBLIC VirtualAddressSpace & getKernelAddressSpace()
void EXPORTED_PUBLIC panic(const char *msg) NORETURN
Definition panic.cc:117
@ Dec
Definition Log.h:144
@ Hex
Definition Log.h:142
Definition cmd.h:30