@@ -79,6 +79,16 @@ bool HardwareGPIO_FTDI::begin(int vendor_id, int product_id,
79
79
return false ;
80
80
}
81
81
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
+
82
92
is_open = true ;
83
93
Logger.info (" FTDI GPIO interface initialized successfully" );
84
94
return true ;
@@ -249,8 +259,9 @@ void HardwareGPIO_FTDI::analogWriteFrequency(pin_size_t pinNumber, uint32_t freq
249
259
return ;
250
260
}
251
261
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)" );
254
265
return ;
255
266
}
256
267
@@ -268,7 +279,7 @@ void HardwareGPIO_FTDI::analogWriteFrequency(pin_size_t pinNumber, uint32_t freq
268
279
// If PWM was active, recalculate timing
269
280
if (was_enabled) {
270
281
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 ();
272
283
Logger.debug (" Updated PWM frequency for active pin" );
273
284
}
274
285
}
@@ -377,7 +388,8 @@ void HardwareGPIO_FTDI::pwmThreadFunction() {
377
388
378
389
while (pwm_thread_running) {
379
390
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 ;
381
393
382
394
{
383
395
std::lock_guard<std::mutex> lock (pwm_mutex);
@@ -390,59 +402,97 @@ void HardwareGPIO_FTDI::pwmThreadFunction() {
390
402
if (!pwm.enabled ) continue ;
391
403
392
404
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
+ }
394
436
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 {
406
446
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);
407
452
} else {
408
453
pin_values_b &= ~(1 << bit_pos);
409
454
}
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 ;
431
456
}
432
457
}
433
458
}
434
459
}
435
460
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) {
439
463
updateGPIOState (0 );
464
+ }
465
+ if (channel_b_changed) {
440
466
updateGPIOState (1 );
441
467
}
442
468
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
+ }
446
496
}
447
497
448
498
Logger.info (" PWM thread stopped" );
@@ -453,6 +503,19 @@ void HardwareGPIO_FTDI::startPWMThread() {
453
503
454
504
pwm_thread_running = true ;
455
505
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
+
456
519
Logger.info (" PWM thread started" );
457
520
}
458
521
@@ -476,11 +539,29 @@ void HardwareGPIO_FTDI::updatePWMPin(pin_size_t pin, uint8_t duty_cycle, uint32_
476
539
pwm.period_us = 1000000 / frequency; // Convert Hz to microseconds
477
540
pwm.on_time_us = (pwm.period_us * duty_cycle) / 255 ; // Calculate on-time based on duty cycle
478
541
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 ;
480
546
481
547
Logger.debug (" PWM pin configured" );
482
548
}
483
549
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
+
484
565
void HardwareGPIO_FTDI::analogWriteResolution (uint8_t bits) {
485
566
// FTDI FT2232HL supports 8-bit PWM resolution (0-255)
486
567
// Log a warning if user tries to set different resolution
0 commit comments