| name | driver-review |
| description | Review and implement hardware driver code — DMA safety, interrupt correctness, timing constraints, peripheral register usage, channel drivers, and peripheral mock implementations. Use when writing, modifying, or reviewing LED drivers, SPI/I2S/RMT/UART/PARLIO/LCD_CAM peripherals, GPIO configuration, or peripheral mock code. |
| disable-model-invocation | true |
Hardware Driver Review & Implementation Guide
Review and implement hardware driver code changes for embedded-specific safety and correctness issues.
Your Task
- Run
git diff --cached and git diff to see all changes
- Identify files that are hardware driver code (see "What Counts as Driver Code" below)
- For reviews: Check ALL driver code changes against the Review Rules below
- For implementations: Follow the Implementation Guide below
- Fix straightforward violations directly
- Report summary of all findings
What Counts as Driver Code
Files matching these patterns:
src/platforms/** — Platform-specific implementations
src/fl/channels/** — LED channel engine and DMA pipeline
**/drivers/** — Hardware driver implementations
- Files containing: DMA buffers, SPI/I2S/RMT/UART/PARLIO peripheral access, GPIO configuration, interrupt handlers, timer configuration
Part 1: Review Rules
1. DMA Safety
2. Interrupt Safety
3. Peripheral Register Access
4. Timing Constraints
5. Memory Safety
6. Channel Engine Patterns (FastLED-specific)
7. Peripheral Mock Rules (CRITICAL)
8. Power and Reset
9. Multi-Platform Considerations
Part 2: Implementation Guide
Architecture
FastLED's driver stack has three layers:
IChannelDriver (driver.h — show/poll state machine)
└─ ChannelEngine* (groups channels by timing, iterates chipset groups)
└─ IPeripheral (virtual interface — real HW or mock)
├─ PeripheralEsp (real ESP-IDF calls)
└─ PeripheralMock (synchronous test simulation)
File Structure for New Peripheral foo
src/platforms/esp/32/drivers/foo/
├─ ifoo_peripheral.h # Virtual interface (no ESP-IDF types)
├─ foo_peripheral_esp.h # Real hardware implementation
├─ foo_peripheral_mock.h # Mock class declaration
├─ foo_peripheral_mock.cpp.hpp # Mock implementation (synchronous)
└─ channel_driver_foo.cpp.hpp # Channel driver using IFooPeripheral
tests/platforms/esp/32/drivers/foo/
├─ foo_peripheral_mock.cpp # Mock peripheral unit tests
└─ channel_driver_foo.cpp # Driver integration tests
Reference implementations:
- I2S:
src/platforms/esp/32/drivers/i2s/
- LCD_CAM:
src/platforms/esp/32/drivers/lcd_cam/
- PARLIO:
src/platforms/esp/32/drivers/parlio/
Peripheral Interface
Define the virtual interface in ifoo_peripheral.h:
- No ESP-IDF types — use
void*, u16*, basic types only
- All methods
FL_NOEXCEPT override
- Buffer management with 64-byte alignment (DMA requirement)
- Time simulation:
getMicroseconds(), delay(ms)
- Callback registration:
registerCallback(void* fn, void* ctx)
Peripheral Mock Implementation
Required Members
bool mInitialized, mEnabled, mBusy;
size_t mTransmitCount;
FooConfig mConfig;
void* mCallback;
void* mUserCtx;
u32 mTransmitDelayUs;
bool mTransmitDelayForced;
bool mShouldFailTransmit;
fl::vector<TransmitRecord> mHistory;
size_t mPendingTransmits;
u64 mSimulatedTimeUs;
bool mFiringCallbacks;
size_t mDeferredCallbackCount;
Core Pattern: transmit() → pump → fireCallback()
transmit() — Queue + pump:
bool transmit(const u16* buffer, size_t size_bytes) {
if (!mInitialized || mShouldFailTransmit) return false;
TransmitRecord record;
record.buffer_copy.resize(size_bytes / 2);
fl::memcpy(record.buffer_copy.data(), buffer, size_bytes);
record.size_bytes = size_bytes;
record.timestamp_us = mSimulatedTimeUs;
mHistory.push_back(fl::move(record));
mTransmitCount++;
mBusy = true;
mPendingTransmits++;
mDeferredCallbackCount++;
pumpDeferredCallbacks();
return true;
}
waitDone() — Instant check, never polls:
bool waitDone(u32 timeout_ms) {
if (!mInitialized) return false;
(void)timeout_ms;
if (mPendingTransmits == 0) { mBusy = false; return true; }
return false;
}
pumpDeferredCallbacks() — Re-entrant safe:
void pumpDeferredCallbacks() {
if (mFiringCallbacks) return;
mFiringCallbacks = true;
while (mDeferredCallbackCount > 0) {
mDeferredCallbackCount--;
fireCallback();
}
mFiringCallbacks = false;
}
fireCallback() — One callback at a time:
void fireCallback() {
if (mPendingTransmits > 0) mPendingTransmits--;
if (mPendingTransmits == 0) mBusy = false;
if (mCallback != nullptr) {
using CallbackType = bool (*)(void*, const void*, void*);
auto fn = reinterpret_cast<CallbackType>(mCallback);
fn(nullptr, nullptr, mUserCtx);
}
}
Time simulation:
u64 getMicroseconds() { return mSimulatedTimeUs; }
void delay(u32 ms) { mSimulatedTimeUs += static_cast<u64>(ms) * 1000; }
reset() — Full state reset:
void reset() {
mInitialized = mEnabled = mBusy = false;
mTransmitCount = 0;
mConfig = FooConfig();
mCallback = nullptr; mUserCtx = nullptr;
mTransmitDelayUs = 0; mTransmitDelayForced = false; mShouldFailTransmit = false;
mHistory.clear(); mPendingTransmits = 0;
mSimulatedTimeUs = 0; mFiringCallbacks = false; mDeferredCallbackCount = 0;
}
Mock-Specific Test API (required)
void simulateTransmitComplete();
void setTransmitFailure(bool should_fail);
void setTransmitDelay(u32 microseconds);
const fl::vector<TransmitRecord>& getTransmitHistory() const;
fl::span<const u16> getLastTransmitData() const;
size_t getTransmitCount() const;
bool isEnabled() const;
void clearTransmitHistory();
void reset();
Channel Driver
Implement IChannelDriver. State machine:
READY → (enqueue) → READY → (show) → BUSY → (poll) → DRAINING → (poll) → READY
Constructor pattern:
ChannelDriverFoo();
ChannelDriverFoo(fl::shared_ptr<IFooPeripheral> peripheral);
Chipset grouping: Channels with different timing (T0H, T1H, T0L, T1L) must be transmitted in separate groups. Sort by transmission time.
Tests
namespace {
void resetFooMockState() {
auto& mock = FooPeripheralMock::instance();
mock.reset();
}
}
FL_TEST_CASE("FooPeripheralMock - basic transmit") {
resetFooMockState();
auto& mock = FooPeripheralMock::instance();
FooConfig config;
config.num_lanes = 4;
config.pclk_hz = 3200000;
FL_REQUIRE(mock.initialize(config));
u16* buffer = mock.allocateBuffer(1024);
FL_REQUIRE(buffer != nullptr);
FL_CHECK(mock.transmit(buffer, 1024));
FL_CHECK(mock.waitDone(100));
FL_CHECK(mock.getTransmitCount() == 1);
mock.freeBuffer(buffer);
}
Driver Registration Priority
In channel_manager_esp32.cpp.hpp:
- PARLIO: 4 (highest)
- LCD_RGB: 3
- RMT: 2
- I2S: 1
- SPI: 0
- UART: -1
Buffer Alignment (64-byte for DMA)
u16* allocateBuffer(size_t size_bytes) {
size_t aligned = ((size_bytes + 63) / 64) * 64;
#ifdef FL_IS_WIN
return static_cast<u16*>(_aligned_malloc(aligned, 64));
#else
return static_cast<u16*>(aligned_alloc(64, aligned));
#endif
}
void freeBuffer(u16* buffer) {
if (!buffer) return;
#ifdef FL_IS_WIN
_aligned_free(buffer);
#else
fl::free(buffer);
#endif
}
Output Format (for reviews)
## Hardware Driver Review Results
### File-by-file Analysis
- **src/platforms/esp/32/drivers/spi/channel_engine_spi.cpp.hpp**: [findings]
### Findings by Category
- **DMA Safety**: N issues
- **Interrupt Safety**: N issues
- **Mock Rules**: N issues
- **Timing Constraints**: N issues
### Summary
- Files reviewed: N
- Violations found: N
- Violations fixed: N
Instructions
- Focus on driver/platform code — skip application-level changes
- Be thorough on DMA and interrupt safety (these cause hard-to-debug crashes)
- Mock violations are P0 — async mocks with threads/wall-clock = flaky tests
- Reference
agents/docs/cpp-standards.md for general C++ rules
- Make corrections directly when safe
- Ask for user confirmation on significant changes