@@ -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