Document preview

Embedded firmware technical documentation.

Browser-readable preview generated from the original Word document, including extracted drawings and images.

JG-01

Embedded Firmware Technical Documentation

ESP32-S3 • Murata SCH16T • BLE • OTA • Real-Time Steering Control

DocumentValue
Document IDJG01-FW-TD-001
Revision1.1
Source packagesrc(1).zip
Source dateJuly 2026
Document date18 July 2026
StatusCode-based technical description

Revision History

RevisionDateDescription
1.018 July 2026Initial architecture skeleton.
1.118 July 2026Expanded from full source-code review: organization, tasks, real-time processing, communications, settings, logging, and control behavior.

Document Basis and Limitations

This document is derived from the supplied firmware source archive. It describes the implementation as found in the code rather than an intended future architecture. Where comments and executable values disagree, the executable value is treated as authoritative and the discrepancy is called out explicitly.

Table of Contents

1. Introduction

2. Firmware Organization

3. System Architecture

4. Startup and Initialization

5. Real-Time Execution Model

6. Main Control Algorithm

7. Sensor Subsystem

8. Receiver Inputs

9. Servo Output and Dual-Servo Mixing

10. Settings, Profiles, and Persistence

11. BLE, JSON API, and OTA

12. Web/Settings Runtime

13. Logging Subsystem

14. LED and Operator Feedback

15. Concurrency and Shared State

16. Module Reference

17. Design Assessment and Observations

Appendix A. Parameter Catalogue

Appendix B. JSON Command Catalogue

Appendix C. Pin and Timing Summary

1. Introduction

1.1 Purpose

The JG-01 firmware implements a steering-assistance controller for a rear-wheel-drive RC drift vehicle. It acquires steering, gain, and throttle commands from the radio receiver; measures vehicle yaw rate and acceleration using a Murata SCH16T inertial sensor; calculates a corrective steering component; and drives one or two steering servos. The same firmware also provides configuration, profile storage, BLE communication, firmware update, web/settings functions, diagnostics, and SD-card logging.

1.2 Scope

Source-code structure and module responsibilities.

ESP32-S3 startup, FreeRTOS tasks, interrupt timing, and core assignment.

The complete 400 Hz control-cycle data path.

IMU, receiver, servo, profile, BLE, OTA, settings, and logging behavior.

Concurrency controls and implementation observations relevant to maintenance.

1.3 Key implementation facts

ItemImplementation
ControllerESP32-S3 using Arduino framework and FreeRTOS
Primary sensorMurata SCH16T-K01 over SPI
Control period2,500 µs, corresponding to 400 Hz
Receiver inputsThree PWM inputs or FlySky iBus
OutputsOne standard servo output or two independently mapped outputs
ConfigurationBLE JSON API and settings/web subsystem
PersistenceESP32 Preferences/NVS with four profiles
Firmware updateBLE OTA and web OTA support
LoggingBinary samples buffered through a FreeRTOS queue to SD card

2. Firmware Organization

The project is organized as one main Arduino sketch plus focused C++ modules. The main sketch owns the physical control path and top-level state. Supporting modules encapsulate sensor access, configuration transport, parameter validation, filtering, drift detection, return damping, receiver decoding, and logging.

Document drawing or diagram

Figure 1. Major firmware subsystems and their data relationships.

2.1 Source groups

GroupPrincipal filesResponsibility
Main runtimerc_drift_gyro_01.inoPins, global runtime objects, startup, timer ISR, FreeRTOS tasks, control algorithm, servo writes, and profile switching.
Sensor driverSCH16T.cpp/.hRegister-level SCH16T initialization, diagnostics, raw data acquisition, conversion, filters, sensitivities, and serial number.
Settings and UIsettings.cpp/.hWeb/settings pages, Preferences access, profiles, EPA, dual-servo configuration, logging controls, web OTA, and websocket data.
Transport/APIBluetoothSettingsTransport.*, SettingsApi.*, SettingsService.*BLE GATT service, authentication, JSON command dispatch, OTA transaction, validation, and generic parameter access.
Control helperslpfilter.*, derivative.*, DerivativeN.h, driftdetector.*, ReturnDamping.*, RollingPeak.*, rolling_max_float.h, notchFilter.*, WobbleDetectorZC.h, AdaptiveGains.hSignal filtering, derivatives, drift classification, damping, rolling peaks, and experimental/available control utilities.
ReceiverIbusReceiver.*UART initialization, 32-byte frame collection, checksum validation, channel extraction, and diagnostics.
Logginglogging.*SD initialization, queue-based sample transfer, buffering, periodic flush, shutdown, and dropped-sample count.
ParametersControlParams.h, params_list.hSingle-source parameter definitions, defaults, typed storage, and runtime lookup.
Build metadatagenerated_version.hFirmware version, Git revision, branch, dirty flag, and build timestamp.

2.2 Main-sketch ownership

The main sketch is deliberately central. It creates the SCH16T object, servo objects, filters, detectors, task handles, hardware timer, receiver pulse state, live telemetry, profile state, and the active ControlParams instance. This makes the time-critical path easy to follow, but it also means rc_drift_gyro_01.ino is the primary integration point and carries substantial global state.

3. System Architecture

The firmware separates the high-rate steering loop from lower-rate management work. Core 1 is reserved for the timer-triggered control task. Core 0 runs status indication and the communications/settings task. Logging creates an additional task on a caller-selected core when recording begins.

Document drawing or diagram

Figure 2. FreeRTOS tasks, core placement, priorities, and principal responsibilities.

3.1 Data path

Receiver signals are captured either by GPIO edge interrupts or by iBus UART parsing.

The control task wakes from a hardware-timer notification every 2.5 ms.

The task snapshots current settings, reads inputs, and normalizes steering and throttle.

SCH16T data is acquired and transformed into the selected installation orientation.

Yaw-rate and acceleration signals pass through configurable filters and derivative processing.

The P+D control calculation is shaped by drift detection, steering priority, correction exponent, lateral-acceleration assistance, peak holding, low-pass filtering, and a delta limiter.

The correction is added to driver steering, constrained by EPA, filtered, optionally mapped to two servos, and written as PWM.

Every fourth control cycle a telemetry record is offered to the logging queue.

4. Startup and Initialization

Document drawing or diagram

Figure 3. Startup order implemented in setup().

4.1 Startup order

Start the serial port at 115200 baud and select slow LED blinking.

Initialize LED PWM immediately, then load stored settings.

Create ledTask on core 0 at priority 1.

Create wifiTask on core 0 at priority 4 before IMU startup. This intentionally keeps settings and OTA access available even if the IMU is absent or calibration fails.

Start SPI and configure SCH16T filter, sensitivity, and decimation structures from the loaded settings.

Attempt IMU initialization for up to 2 seconds. If initialization fails, setup() returns without starting the control task. Communications remain available because wifiTask is already running.

Initialize either iBus or three GPIO pulse inputs.

Run IMU calibration with a 5-second timeout. The control task is still permitted to start if calibration does not complete.

Configure two servo objects with 14-bit timer width and the validated servo frequency.

Reload settings, create controlTask on core 1, and start the periodic hardware timer.

Set LED brightness and indicate whether the base or secondary runtime profile is active.

4.2 Fault behavior during startup

ConditionBehavior
IMU initialization failureThe high-rate control task is not created. BLE/settings/OTA remain available, and the LED continues slow blinking.
IMU calibration failureA warning is printed; control starts without newly calculated offsets.
Receiver mode iBusUART-based iBus is initialized and channels 1, 3, and 4 are used.
Receiver mode PWMCHANGE interrupts are attached to steering, gain, and throttle GPIOs.

5. Real-Time Execution Model

5.1 Hardware timer and task notification

Timer 0 is configured with an 80:1 prescaler, producing a 1 MHz timer tick. Its auto-reload alarm is set to LOOP_PERIOD_US = 2500. The interrupt service routine calls vTaskNotifyGiveFromISR() for controlTask and requests a context switch when necessary. The task blocks indefinitely in ulTaskNotifyTake() and therefore consumes no CPU while waiting.

The executable period is 2.5 ms, or 400 Hz. A nearby source comment says “5 ms = 200 Hz”; that comment is stale and does not match the constant or timer configuration.

5.2 Task inventory

Execution contextCorePriorityStackScheduling
Timer ISRInterruptHardware levelN/AEvery 2,500 µs; sends direct task notification.
controlTask1316,384 bytesEvent-driven by timer notification; runs one complete control cycle per wake-up.
wifiTask0412,288 bytesCalls setupSettings(), then repeatedly calls makeSettings() with a 50 ms delay while it returns true; deletes itself when finished.
ledTask014,096 bytesPermanent state machine; delay is 20 or 100 ms depending on mode.
LoggerTaskConfigured by caller316,384 bytesCreated only while logging is active; blocks on queue receive and periodically flushes SD writes.
GPIO receiver ISRsInterruptHardware levelN/ACapture rising/falling edges and update pulse-width snapshots.

5.3 Timing behavior

The control task calculates measured dt from micros() for manual dual-servo ramping, while filter/derivative objects also use the configured nominal loop period in several places.

No blocking delay appears in the main control cycle. SD writes are delegated through a non-blocking queue.

Serial debug printing is rate limited to approximately 5 Hz and additionally suppressed when receiver input activity is recent, reducing interference with pulse capture.

The Arduino loop() is intentionally idle and sleeps for one second; application work is task-driven.

5.4 Overrun considerations

A direct task notification acts as a counting semaphore. If a control iteration exceeds 2.5 ms, notifications can accumulate, but the task has no explicit deadline-miss counter or execution-time monitor. Consequently, a sustained overrun may cause back-to-back iterations instead of preserving evenly spaced control updates. Instrumenting cycle duration and notification backlog would improve observability.

6. Main Control Algorithm

Document drawing or diagram

Figure 4. Processing sequence performed by each controlTask iteration.

6.1 Settings synchronization

When settings_changed is set, the control task reloads the active profile. It then copies the global ControlParams object into a local stack variable while holding cp_mux. The local snapshot prevents individual fields from changing midway through a cycle. Profile-switch logic may update the runtime profile, after which the task snapshots ControlParams again.

6.2 Receiver acquisition and normalization

PWM mode reads volatile pulse-width snapshots under s_mux. iBus mode calls ibusUpdate() and maps channel 1 to steering, channel 3 to gain, and channel 4 to throttle. Steering can be reversed around the stored EPA center. Gain is mapped from nominal 1000-2000 µs into 0.0-1.0. Steering and throttle are low-pass filtered and normalized through SteeringMap objects.

6.3 IMU processing

The task requests raw SCH16T data and converts it into engineering units. applyImuOrientation() supports eight installation orientations by swapping and negating body axes. Body Z angular rate becomes gyrodps. Body X acceleration, after removal of the lateral setpoint, becomes the lateral-acceleration assistance input.

6.4 Yaw-rate P+D correction

Yaw rate is low-pass filtered to form the proportional signal. A derivative object calculates yaw acceleration and a second low-pass filter suppresses derivative noise. The core correction is:

gyro_correction = drift_multiplier × configured_gain × receiver_gain × (pid_p × filtered_yaw_rate + pid_d × filtered_yaw_derivative)

No integral term is used. This is consistent with a drift-assistance controller where the desired behavior is rate-sensitive support rather than elimination of a persistent heading error.

6.5 Drift-dependent gain

DriftDetector receives normalized steering, filtered yaw rate, and configured gain. Its output is converted into a multiplier of 1 + (drift_value / 10) × dd_multiplier. The intention is to increase correction when steering/yaw conditions indicate an established drift.

6.6 Steering priority

The derivative of normalized driver steering reduces gyro authority during rapid manual steering movement. The correction is divided by 1 + (|steering derivative| / 2) × steering_prio. This preserves driver authority during transitions and fast counter-steering inputs.

6.7 Nonlinear shaping and acceleration assistance

correction_exp applies a sign-preserving power function normalized around a correction magnitude of 150. Values above one suppress small corrections relative to large ones; values below one do the opposite. Filtered lateral acceleration is multiplied by acc_lat_multip, clamped to ±50 µs, and added to the gyro correction.

6.8 Peak hold, filtering, and slew limiting

RollingMaxCorr stores a recent maximum-magnitude correction. For the first 60% of the configured rollmaxcorr age window, that peak is held. During the remaining 40%, the output blends back toward the current correction. A correction low-pass filter follows. Finally, max_d_corr constrains the change relative to the previous cycle, acting as a global correction slew limiter.

6.9 Output construction

The final correction is added to the filtered receiver steering pulse. When EPA is enabled, the result is clamped to the stored low/high endpoints. A servo-output low-pass filter is applied before normal or dual-servo output mapping. A deadband-like condition attempts to retain the previous output when the change is below 2 µs; however, it appears before the new out value is calculated and should be reviewed because its effective behavior may not match the apparent intent.

6.10 Logging cadence

logging_counter causes one sample to be enqueued every fourth control iteration. At a 400 Hz control rate this gives a nominal logging rate of 100 Hz. The record includes filtered and raw gyro values, correction, input/output pulses, gain input, normalized throttle, receiver gain, and three raw acceleration axes.

7. Sensor Subsystem

7.1 SCH16T configuration

PropertyValue / source
SPI chip selectGPIO 10
ResetNot connected in software (−1)
Rate sensitivity3200 LSB/(°/s) for Rate1 and Rate2
Acceleration sensitivity3200 LSB/(m/s²) for Acc1, Acc2, and Acc3
Rate2 decimation4
Acc2 decimation4
Filter settingsLoaded from imu_r_filt_hz, imu_a12_filt_hz, and imu_a3_filt_hz; defaults are 68 Hz
Runtime signals usedRate1 Z for yaw; Acc1 X/Y/Z for acceleration and logging

7.2 Initialization and diagnostics

initializeImu() repeatedly calls imu.begin() until success or timeout. The SCH16T driver is significantly larger than the control modules and contains register-level startup, status checks, filter/sensitivity configuration, conversion, and device-identification logic. After initialization the firmware reads and prints the sensor serial number.

7.3 Calibration and orientation

Calibration establishes acceleration offsets and a lateral acceleration setpoint while the car is stationary. Eight orientation modes allow the PCB to be installed in multiple rotations and inverted configurations without rewriting the control algorithm. The orientation mapping is applied consistently before yaw and body acceleration are used.

8. Receiver Inputs

8.1 PWM mode

Steering, gain, and throttle use GPIO 3, 4, and 39. CHANGE interrupts measure high-pulse duration. Shared pulse variables are volatile and copied under s_mux in the control task. This keeps ISR work minimal and avoids disabling interrupts for longer than the snapshot operation.

8.2 iBus mode

IbusReceiver reads fixed 32-byte frames, validates the protocol checksum, extracts channel values, and maintains diagnostics including valid-frame count, checksum errors, frame errors, last-frame time, and raw-frame bytes. The normal control calculations are unchanged after channel values are selected.

8.3 Runtime profile switching

Each base profile can define a secondary profile and an iBus switch channel. The control loop evaluates the selected channel with a 100 ms debounce period and loads either the base or secondary runtime profile. The LED uses a distinct pattern while the secondary profile is active.

9. Servo Output and Dual-Servo Mixing

9.1 Servo generation

ESP32Servo is configured with 14-bit timer width. GPIO 5 drives servo 1 and GPIO 6 drives servo 2. servo_hz is validated before use; its default is 200 Hz. In single-servo mode only servo 1 is updated by the control task.

9.2 Nine-point mapping

Dual-servo mode uses nine steering input points at −100, −80, −50, −20, 0, 20, 50, 80, and 100 percent. Independent left and right tables are linearly interpolated between adjacent points. Negative and positive sides are scaled relative to their respective endpoint magnitudes. Servo 2 uses the right-side curve and then reverses sign so the two physical steering actuators can work in opposition.

9.3 Manual test mode

The settings interface can take temporary control of the servos for calibration. The requested test input is ramped at 500 µs/s using the measured cycle dt, preventing an abrupt jump. Release-to-center ramps back to 1500 µs and automatically returns ownership to the normal control path after center is reached.

10. Settings, Profiles, and Persistence

10.1 Parameter model

params_list.h is an X-macro list and is the single place where regular ControlParams fields and defaults are declared. ControlParams.h includes the list three times to initialize defaults, build a name-to-pointer table, and declare typed members. This reduces duplication and lets the SettingsService access parameters by name.

10.2 Four-profile storage

The firmware supports four profiles. Each profile uses its own Preferences namespace (rc0 through rc3), while legacy values can be read from an older rc namespace. The active base profile, profile names, secondary profile, switch channel, EPA values, dual-servo map, and common settings are loaded and saved through settings.cpp and companion routines in the main sketch.

10.3 Runtime change model

SettingsService validates incoming text, writes the typed value into ControlParams, and marks settings_changed. The control task observes that flag and reloads its active settings. The cp_mux critical section protects snapshots and settings-sensitive shared values. Persistent commands save values to Preferences; temporary set operations can modify runtime behavior without necessarily committing it.

10.4 Validation

Murata filter values are restricted to supported rates.

Servo frequency is checked by isValidServoHz()/validatedServoHz().

Known parameters are parsed by type and rejected with an error string when invalid.

BLE authentication is enforced when device_pwd is non-empty.

11. BLE, JSON API, and OTA

11.1 GATT service

CharacteristicPropertiesPurpose
Settings TXRead + NotifyJSON replies and asynchronous text notifications.
Settings RXWrite + Write Without ResponseJSON settings commands from the client.
OTA ControlWrite + Write Without Responseota_begin, ota_status, ota_end, and ota_abort transaction control.
OTA DataWrite + Write Without ResponseRaw firmware binary chunks.

The peripheral advertises a custom settings service, uses an MTU of 185, high transmit power, and conservative 40-80 ms advertising intervals intended to improve compatibility with Windows/WinRT adapters.

11.2 Request handling

BLE write callbacks do not execute the complete command directly. They store a pending request, and BluetoothSettingsTransport::loop() later removes and processes it. Authentication is handled before SettingsApi dispatch. Responses are serialized as JSON and sent by notification.

11.3 JSON command groups

GroupCommands
Information and parametersget_info, list_params, get_param, set_param, set_param_save, set_params, save_params
Profilesget_profiles, get_profileinfo, load_profile, set_profile_name, set_profile_switch, copy_profile
Loggingget_logging, start_logging, stop_logging
EPAenter_epa, exit_epa, get_epa, capture_epa, save_epa
Dual servoget_dual_servo, set_dual_servo, set_dual_servo_options, dual_servo_go, dual_servo_live, dual_servo_center/release, save_dual_servo

11.4 BLE OTA transaction

ota_begin validates the declared image size and opens an ESP32 Update transaction. OTA Data writes raw bytes and tracks the number accepted. ota_status reports progress. ota_end verifies the completed image and schedules reboot. ota_abort terminates an active transaction. Progress notifications are intentionally not interleaved with the high-rate data callback because some Windows BLE stacks fail when notifications and firmware writes overlap.

11.5 Build identification

get_info returns firmware version, Git commit, branch, dirty-build flag, build date/time, and BLE device name. This makes field units traceable to the exact software build used.

12. Web/Settings Runtime

settings.cpp contains the browser-facing management implementation. It builds HTML pages, websocket telemetry, profile controls, logging pages, firmware-upload handlers, file download, EPA functions, dual-servo setup, Wi-Fi persistence, and settings save operations. setupSettings() initializes the subsystem; makeSettings() services it from wifiTask at approximately 20 Hz due to the 50 ms task delay.

12.1 Separation from the control loop

The communications/settings task is pinned to core 0, while the control task is pinned to core 1. This reduces the risk that HTTP, BLE, HTML construction, flash writes, or OTA processing directly delay the 400 Hz steering loop. Shared state is exchanged through snapshots, flags, and critical sections rather than by invoking browser logic from the control task.

12.2 EPA mode

EPA mode exposes live receiver pulses, captures low/center/high steering and throttle points, saves them to non-volatile storage, and can temporarily disable normal endpoint limiting. The LED blinks fast while endpoint protection is disabled, providing a visible warning.

13. Logging Subsystem

13.1 Producer-consumer design

The control task is the producer and never writes the SD card directly. loggingEnqueue() calls xQueueSend() with zero wait. If the queue is full, the sample is dropped and a counter is incremented rather than blocking control execution. LoggerTask is the consumer and performs buffered writes and file flushes.

13.2 Structures and buffering

ItemValue
Queue length256 LogSample records
Record payloaduint32 timestamp plus eleven float channels
Write buffer11 samples
Nominal producer rate100 Hz (every fourth 400 Hz cycle)
Logger priority3
Logger stack16,384 bytes
Stop timeout2 seconds while waiting for task exit

13.3 Logged channels

FieldControl-loop value
t_usElapsed logging time divided by 1000; despite the name, stored unit is milliseconds.
p1Filtered yaw rate
p2Raw converted yaw rate
p3Final gyro correction
p4Filtered steering input pulse
p5Combined steering output before dual-servo mapping
p6Raw gain input pulse
p7Normalized throttle
p8Mapped receiver gain 0-1
p9-p11Raw converted Acc1 X, Y, and Z values

The timestamp member name t_us is misleading because the assignment divides microseconds by 1000. Documentation and downstream log readers should treat it as elapsed milliseconds unless the source is changed.

14. LED and Operator Feedback

ledTask is an independent low-priority state machine using ESP32 LEDC PWM. LED_OFF and LED_ON update every 100 ms. Fast blink uses a 200 ms full period; slow blink uses 800 ms. Secondary-profile mode stays on for 800 ms and off for 200 ms. Brightness and command are copied under led_mux.

LED stateMeaning in current implementation
Slow blinkStartup or IMU initialization failure; management interfaces remain available.
OnBase profile active and normal operation.
Secondary patternRuntime secondary profile active.
Fast blinkEPA limiting disabled / calibration-related warning state.
OffExplicit status command.

15. Concurrency and Shared State

15.1 Critical sections

LockProtected data / use
s_muxPWM receiver pulse snapshots and ISR-updated values.
cp_muxControlParams snapshots and selected control/settings flags.
dual_servo_manual_muxManual test ownership, target, current position, and release state.
led_muxLED command and brightness.
Interrupt disable in BLE transportPending String request handoff between callback and transport loop.

15.2 Communication patterns

Direct-to-task notification: timer ISR to controlTask.

Queue: controlTask to LoggerTask.

Volatile snapshots + critical sections: receiver ISRs to controlTask.

Flag + reload: SettingsService to controlTask.

Pending request buffer: BLE callback to transport loop.

Local copy of ControlParams: stable settings for each high-rate cycle.

15.3 Risks to review

String operations and noInterrupts()/interrupts() in the BLE pending-request handoff should remain short; a queue would provide a more explicit thread-safe boundary.

settings_changed is a shared flag; making it atomic or consistently protecting it would clarify memory ordering.

Some control code uses cp directly instead of cp_local, notably drift calculation fields, which can undermine the otherwise careful per-cycle snapshot model.

There is no explicit watchdog, control-cycle deadline monitor, or receiver failsafe behavior described in the supplied main path.

LoggerTask and controlTask both use priority 3. Core selection for logging should avoid competing with control on core 1.

16. Module Reference

ModuleRole
rc_drift_gyro_01.inoTop-level integration: pins, settings/profile loading, receiver ISRs, IMU initialization/calibration, FreeRTOS task creation, timer ISR, LED task, dual-servo mixer, and 400 Hz control loop.
SCH16T.cpp/.hMurata sensor driver with device startup, register transactions, diagnostic validation, raw acquisition, unit conversion, filter/sensitivity/decimation setup, and serial-number access.
BluetoothSettingsTransport.cpp/.hBLE server, advertised name, authentication, request buffering, response notification, four GATT characteristics, and transactional BLE OTA.
SettingsApi.cpp/.hJSON command router and response construction for information, parameters, profiles, logging, EPA, and dual-servo functions.
SettingsService.cpp/.hTyped parameter read/write adapter, value validation, and settings-change notification.
settings.cpp/.hWeb/UI service, NVS persistence, profile administration, websocket handling, SD/log controls, web OTA, EPA and dual-servo settings.
ControlParams.h + params_list.hCompile-time construction of typed parameter storage and defaults from a single X-macro list.
IbusReceiver.cpp/.hNon-blocking iBus frame parser, checksum verification, 14-channel extraction, and debug counters.
logging.cpp/.hAsynchronous SD logger using a FreeRTOS queue and dedicated task.
lpfilter.cpp/.hFirst-order low-pass filter with configurable cutoff and loop time.
derivative.cpp/.h + DerivativeN.hSingle-step and multi-sample derivative estimators.
driftdetector.cpp/.hComputes a drift likelihood/value from steering and yaw-rate conditions.
ReturnDamping.cpp/.hCalculates damping behavior intended to control correction return.
RollingPeak.cpp/.h + rolling_max_float.hRecent peak and age tracking used for correction holding/fading.
notchFilter.cpp, WobbleDetectorZC.h, AdaptiveGains.hAdditional oscillation-detection, notch, and adaptive-gain utilities; some are instantiated or available but not central to the active correction path in this revision.
SteeringMap.hMaps pulse widths to normalized steering/throttle values using stored endpoints.

17. Design Assessment and Observations

17.1 Strengths

The control task is isolated on core 1 and triggered by a hardware timer rather than Arduino loop timing.

Control-cycle SD work is non-blocking and queue based.

Management/OTA is started before the IMU, allowing recovery from a sensor fault.

Settings are snapshotted for deterministic per-cycle use.

Receiver abstraction allows PWM and iBus without changing downstream control logic.

BLE separates JSON control traffic from raw OTA data.

Profiles, secondary-profile switching, EPA, and dual-servo mapping are integrated into the runtime rather than bolted onto the mobile app.

Build metadata and dropped-log-sample counters support field diagnostics.

17.2 Important code observations

ObservationImpact / recommendation
LOOP_PERIOD_US is 2500 while its comment says 5 ms / 200 Hz.Actual rate is 400 Hz. Correct the comment to avoid tuning and documentation errors.
LogSample.t_us stores (micros-start)/1000.Value is milliseconds. Rename to t_ms or store true microseconds.
Small-output hold test occurs before out is recalculated.Review ordering to verify intended servo deadband behavior.
return_damping is calculated but not visibly applied to corr in the active path.Either integrate the value or remove dead code to avoid false assumptions.
cp_local is used for many settings, but some expressions reference global cp.Use one immutable cycle snapshot consistently.
No deadline/overrun counter.Measure cycle execution time and missed timer notifications.
No obvious input timeout/failsafe in the reviewed control path.Define behavior for stale PWM/iBus frames and invalid pulse values.
Main sketch contains extensive global state.Future refactoring could isolate ReceiverService, ControlLoop, ImuService, ServoService, and ProfileManager without changing behavior.

17.3 Recommended verification tests

Measure controlTask execution-time distribution at 400 Hz with BLE active, web UI active, and logging active.

Verify receiver loss behavior separately for PWM and iBus.

Confirm correction sign for all eight IMU orientation modes.

Sweep yaw-rate and steering inputs to validate steering-priority and nonlinear exponent behavior.

Test rollmaxcorr peak hold and fade at boundary values 0, 1, and typical window sizes.

Verify servo update frequency and pulse timing for both single- and dual-servo operation.

Stress BLE OTA on Windows, Android, and iOS while monitoring control-task timing.

Fill the logging queue deliberately and verify dropped-sample reporting and safe stop.

Appendix A. Parameter Catalogue

NameTypeDefaultPurpose
gainfloat0.0Master configured gyro gain.
gyro_avgint0Legacy/available gyro averaging control.
steering_priofloat0.0Reduces gyro correction during fast driver steering.
debug_serialint0Enables rate-limited serial control diagnostics.
correction_expfloat1.0Nonlinear correction exponent.
gyro_lp_hzint20Yaw-rate low-pass cutoff.
derivative_lp_hzint20Yaw derivative low-pass cutoff.
steer_in_lp_hzint20Receiver steering low-pass cutoff.
steer_out_lp_hzint20Servo command low-pass cutoff.
pid_pfloat5.0Yaw-rate proportional coefficient.
pid_dfloat1.0Yaw-acceleration derivative coefficient.
wobble_det_afloat0.2Wobble detector parameter.
dd_min_steerfloat0.05Minimum steering magnitude for drift detection.
dd_min_yawfloat10Minimum yaw rate for drift detection.
dd_multiplierfloat1Drift-dependent gain multiplier.
max_d_corrfloat0.05Maximum correction change per cycle.
corr_lp_hzfloat5.0Final correction low-pass cutoff.
acc_lat_multipfloat10Lateral acceleration assistance gain.
imu_orientationint0One of eight sensor mounting transformations.
imu_r_filt_hzint68SCH16T rate filter selection.
imu_a12_filt_hzint68SCH16T Acc1/2 filter selection.
imu_a3_filt_hzint68SCH16T Acc3 filter selection.
rollmaxcorrint10Rolling correction peak-hold/fade window.
dual_servo_enabledint0Enables two-servo output mapping.
receiver_input_modeint00 = PWM, 1 = iBus.
servo_hzint200Servo PWM refresh frequency.
profile_secondaryint1Secondary profile index.
profile_switch_channelint0iBus channel used for profile switching.
device_ssidstringJASAGYRO-01Device network/advertised configuration name.
device_pwdstring12345678Configuration/BLE authentication password.

Appendix B. JSON Command Catalogue

CommandPurpose
get_infoRead build and BLE identity.
list_paramsReturn all parameter metadata and values.
get_paramRead one parameter by name.
set_paramChange one runtime parameter.
set_param_saveChange and persist one parameter.
set_paramsChange multiple parameters.
save_paramsPersist current parameter set.
get_profiles / get_profileinfoRead profile names and selection metadata.
load_profileSelect and load a profile.
set_profile_nameRename a profile.
set_profile_switchSet secondary profile and switch channel.
copy_profileCopy active profile to another slot.
get_logging / start_logging / stop_loggingInspect and control SD logging.
enter_epa / exit_epa / get_epaManage EPA calibration mode.
capture_epa / save_epaCapture and persist endpoint values.
get_dual_servoRead map, options, and live values.
set_dual_servoStage/apply full nine-point map.
set_dual_servo_optionsImmediately update enable/reverse options.
dual_servo_go / dual_servo_liveTemporary calibration positioning.
dual_servo_centerCenter and release manual test.
save_dual_servoPersist the dual-servo configuration.
ota_begin / ota_status / ota_end / ota_abortBLE firmware-update transaction control.

Appendix C. Pin and Timing Summary

SignalGPIO / value
Steering PWM input3
Gain PWM input4
Throttle PWM input39
Servo 1 output5
Servo 2 output6
Status LED7 / PIN_LED_CTRL in active LED implementation
SCH16T chip select10
Control timer period2,500 µs
Control frequency400 Hz
Nominal log frequency100 Hz
Profile-switch debounce100 ms
Manual servo test ramp500 µs/s
BLE local MTU185
BLE advertising interval40-80 ms