@@ -79,6 +79,16 @@ bool HardwareGPIO_FTDI::begin(int vendor_id, int product_id,
7979 return false ;
8080 }
8181
82+ // Set low latency timer for better PWM performance (default is 16ms, set to 1ms)
83+ ret = ftdi_set_latency_timer (ftdi_context, 1 );
84+ if (ret < 0 ) {
85+ Logger.warning (" Failed to set latency timer: %s" , ftdi_get_error_string (ftdi_context));
86+ }
87+
88+ // Enable USB transfer chunking for better performance
89+ ftdi_write_data_set_chunksize (ftdi_context, 256 );
90+ ftdi_read_data_set_chunksize (ftdi_context, 256 );
91+
8292 is_open = true ;
8393 Logger.info (" FTDI GPIO interface initialized successfully" );
8494 return true ;
@@ -249,8 +259,9 @@ void HardwareGPIO_FTDI::analogWriteFrequency(pin_size_t pinNumber, uint32_t freq
249259 return ;
250260 }
251261
252- if (frequency == 0 || frequency > 100000 ) { // Limit to reasonable range
253- Logger.error (" Invalid PWM frequency (valid range: 1-100000)" );
262+ // Limit to reasonable range - higher frequencies will have poor accuracy due to USB latency
263+ if (frequency == 0 || frequency > 10000 ) {
264+ Logger.error (" Invalid PWM frequency (valid range: 1-10000 Hz for reliable operation)" );
254265 return ;
255266 }
256267
@@ -268,7 +279,7 @@ void HardwareGPIO_FTDI::analogWriteFrequency(pin_size_t pinNumber, uint32_t freq
268279 // If PWM was active, recalculate timing
269280 if (was_enabled) {
270281 pwm.on_time_us = (pwm.period_us * current_duty) / 255 ;
271- pwm.last_toggle = std::chrono::high_resolution_clock::now ();
282+ pwm.period_start = std::chrono::high_resolution_clock::now ();
272283 Logger.debug (" Updated PWM frequency for active pin" );
273284 }
274285 }
@@ -377,7 +388,8 @@ void HardwareGPIO_FTDI::pwmThreadFunction() {
377388
378389 while (pwm_thread_running) {
379390 auto current_time = std::chrono::high_resolution_clock::now ();
380- bool state_changed = false ;
391+ bool channel_a_changed = false ;
392+ bool channel_b_changed = false ;
381393
382394 {
383395 std::lock_guard<std::mutex> lock (pwm_mutex);
@@ -390,59 +402,97 @@ void HardwareGPIO_FTDI::pwmThreadFunction() {
390402 if (!pwm.enabled ) continue ;
391403
392404 auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(
393- current_time - pwm.last_toggle ).count ();
405+ current_time - pwm.period_start ).count ();
406+
407+ bool new_state = false ;
408+ bool state_change = false ;
409+
410+ if (elapsed >= pwm.period_us ) {
411+ // New period starts
412+ // Calculate jitter for statistics
413+ uint64_t expected_period_time = pwm.cycle_count * pwm.period_us ;
414+ auto total_elapsed = std::chrono::duration_cast<std::chrono::microseconds>(
415+ current_time - pwm.period_start ).count ();
416+ uint64_t jitter = (total_elapsed > expected_period_time) ?
417+ (total_elapsed - expected_period_time) :
418+ (expected_period_time - total_elapsed);
419+
420+ pwm.total_jitter_us += jitter;
421+ if (jitter > pwm.max_jitter_us ) {
422+ pwm.max_jitter_us = jitter;
423+ }
424+
425+ pwm.period_start = current_time;
426+ pwm.cycle_count ++;
427+ new_state = (pwm.on_time_us > 0 );
428+ state_change = (new_state != pwm.current_state );
429+ } else if (pwm.current_state && elapsed >= pwm.on_time_us ) {
430+ // Transition HIGH to LOW
431+ new_state = false ;
432+ state_change = true ;
433+ } else {
434+ new_state = pwm.current_state ;
435+ }
394436
395- if (pwm.current_state ) {
396- // Pin is currently HIGH, check if it's time to go LOW
397- if (elapsed >= pwm.on_time_us ) {
398- pwm.current_state = false ;
399- pwm.last_toggle = current_time;
400-
401- // Update hardware pin state
402- int channel = getChannel (pin);
403- int bit_pos = getBitPosition (pin);
404-
405- if (channel == 0 ) {
437+ if (state_change) {
438+ pwm.current_state = new_state;
439+ int channel = getChannel (pin);
440+ int bit_pos = getBitPosition (pin);
441+
442+ if (channel == 0 ) {
443+ if (new_state) {
444+ pin_values_a |= (1 << bit_pos);
445+ } else {
406446 pin_values_a &= ~(1 << bit_pos);
447+ }
448+ channel_a_changed = true ;
449+ } else {
450+ if (new_state) {
451+ pin_values_b |= (1 << bit_pos);
407452 } else {
408453 pin_values_b &= ~(1 << bit_pos);
409454 }
410- state_changed = true ;
411- }
412- } else {
413- // Pin is currently LOW, check if it's time to go HIGH or start new period
414- uint32_t off_time_us = pwm.period_us - pwm.on_time_us ;
415- if (elapsed >= off_time_us) {
416- pwm.current_state = true ;
417- pwm.last_toggle = current_time;
418-
419- // Update hardware pin state (only if duty cycle > 0)
420- if (pwm.on_time_us > 0 ) {
421- int channel = getChannel (pin);
422- int bit_pos = getBitPosition (pin);
423-
424- if (channel == 0 ) {
425- pin_values_a |= (1 << bit_pos);
426- } else {
427- pin_values_b |= (1 << bit_pos);
428- }
429- state_changed = true ;
430- }
455+ channel_b_changed = true ;
431456 }
432457 }
433458 }
434459 }
435460
436- // Update hardware if any pin states changed
437- if (state_changed) {
438- // Update both channels - this could be optimized to only update changed channels
461+ // Update only changed channels to reduce USB overhead
462+ if (channel_a_changed) {
439463 updateGPIOState (0 );
464+ }
465+ if (channel_b_changed) {
440466 updateGPIOState (1 );
441467 }
442468
443- // Sleep for a short time to avoid excessive CPU usage
444- // PWM resolution is limited by this sleep time
445- std::this_thread::sleep_for (std::chrono::microseconds (10 ));
469+ // Dynamic sleep time based on active PWM frequencies
470+ // Sleep for a fraction of the minimum period to ensure responsive timing
471+ auto min_period = std::chrono::microseconds::max ();
472+ {
473+ std::lock_guard<std::mutex> lock (pwm_mutex);
474+ for (const auto & pair : pwm_pins) {
475+ if (pair.second .enabled ) {
476+ auto period = std::chrono::microseconds (pair.second .period_us );
477+ min_period = std::min (min_period, period);
478+ }
479+ }
480+ }
481+
482+ if (min_period != std::chrono::microseconds::max ()) {
483+ // Sleep for 1% of minimum period, but at least 1µs and at most 100µs
484+ auto sleep_time = std::max (
485+ std::chrono::microseconds (1 ),
486+ std::min (
487+ std::chrono::microseconds (100 ),
488+ min_period / 100
489+ )
490+ );
491+ std::this_thread::sleep_for (sleep_time);
492+ } else {
493+ // No active PWM pins, sleep longer
494+ std::this_thread::sleep_for (std::chrono::microseconds (100 ));
495+ }
446496 }
447497
448498 Logger.info (" PWM thread stopped" );
@@ -453,6 +503,19 @@ void HardwareGPIO_FTDI::startPWMThread() {
453503
454504 pwm_thread_running = true ;
455505 pwm_thread = std::thread (&HardwareGPIO_FTDI::pwmThreadFunction, this );
506+
507+ // Set real-time priority for better timing accuracy (Linux only)
508+ #ifdef __linux__
509+ struct sched_param param;
510+ param.sched_priority = sched_get_priority_max (SCHED_FIFO) - 1 ; // High but not max priority
511+ if (pthread_setschedparam (pwm_thread.native_handle (), SCHED_FIFO, ¶m) != 0 ) {
512+ Logger.warning (" Failed to set real-time priority for PWM thread (requires CAP_SYS_NICE capability or root)" );
513+ Logger.warning (" PWM timing may be less accurate. Consider running with elevated privileges for production use." );
514+ } else {
515+ Logger.info (" PWM thread running with real-time priority" );
516+ }
517+ #endif
518+
456519 Logger.info (" PWM thread started" );
457520}
458521
@@ -476,11 +539,29 @@ void HardwareGPIO_FTDI::updatePWMPin(pin_size_t pin, uint8_t duty_cycle, uint32_
476539 pwm.period_us = 1000000 / frequency; // Convert Hz to microseconds
477540 pwm.on_time_us = (pwm.period_us * duty_cycle) / 255 ; // Calculate on-time based on duty cycle
478541 pwm.current_state = false ;
479- pwm.last_toggle = std::chrono::high_resolution_clock::now ();
542+ pwm.period_start = std::chrono::high_resolution_clock::now ();
543+ pwm.cycle_count = 0 ;
544+ pwm.max_jitter_us = 0 ;
545+ pwm.total_jitter_us = 0 ;
480546
481547 Logger.debug (" PWM pin configured" );
482548}
483549
550+ void HardwareGPIO_FTDI::getPWMStatistics (pin_size_t pin, uint64_t & cycles,
551+ uint64_t & max_jitter_us, uint64_t & avg_jitter_us) {
552+ std::lock_guard<std::mutex> lock (pwm_mutex);
553+ auto it = pwm_pins.find (pin);
554+ if (it != pwm_pins.end () && it->second .enabled ) {
555+ cycles = it->second .cycle_count ;
556+ max_jitter_us = it->second .max_jitter_us ;
557+ avg_jitter_us = (cycles > 0 ) ? it->second .total_jitter_us / cycles : 0 ;
558+ } else {
559+ cycles = 0 ;
560+ max_jitter_us = 0 ;
561+ avg_jitter_us = 0 ;
562+ }
563+ }
564+
484565void HardwareGPIO_FTDI::analogWriteResolution (uint8_t bits) {
485566 // FTDI FT2232HL supports 8-bit PWM resolution (0-255)
486567 // Log a warning if user tries to set different resolution
0 commit comments