Skip to content

Commit 357c5dd

Browse files
committed
ftdi: improve pwm
1 parent 10281cf commit 357c5dd

File tree

3 files changed

+195
-45
lines changed

3 files changed

+195
-45
lines changed

ArduinoCore-Linux/cores/ftdi/HardwareGPIO_FTDI.cpp

Lines changed: 125 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -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, &param) != 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+
484565
void 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

ArduinoCore-Linux/cores/ftdi/HardwareGPIO_FTDI.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,16 @@ class HardwareGPIO_FTDI : public HardwareGPIO {
174174
*/
175175
operator bool() { return is_open && ftdi_context != nullptr; }
176176

177+
/**
178+
* @brief Get PWM statistics for monitoring timing accuracy.
179+
* @param pin Pin number
180+
* @param cycles Total number of PWM cycles completed
181+
* @param max_jitter_us Maximum jitter observed in microseconds
182+
* @param avg_jitter_us Average jitter in microseconds
183+
*/
184+
void getPWMStatistics(pin_size_t pin, uint64_t& cycles,
185+
uint64_t& max_jitter_us, uint64_t& avg_jitter_us);
186+
177187
protected:
178188
struct ftdi_context* ftdi_context = nullptr;
179189
bool is_open = false;
@@ -194,8 +204,12 @@ class HardwareGPIO_FTDI : public HardwareGPIO {
194204
uint32_t frequency = 1000; // Hz
195205
uint32_t period_us = 1000; // Period in microseconds
196206
uint32_t on_time_us = 0; // On time in microseconds
197-
std::chrono::high_resolution_clock::time_point last_toggle;
207+
std::chrono::high_resolution_clock::time_point period_start; // Start of current period
198208
bool current_state = false;
209+
uint64_t cycle_count = 0; // Total cycles for drift correction
210+
// Statistics for monitoring
211+
uint64_t max_jitter_us = 0;
212+
uint64_t total_jitter_us = 0;
199213
};
200214

201215
std::map<pin_size_t, PWMPin> pwm_pins;

examples/pwm/pwm.ino

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
// GPIO 12 should work on a PI and with FTDI FT2232HL for PWM
55
const int LED1_PIN = 12; // GPIO 12
66

7+
unsigned long lastStatsTime = 0;
8+
const unsigned long STATS_INTERVAL = 5000; // Print stats every 5 seconds
9+
710
void setup() {
811
// Initialize serial communication
912
Serial.begin(115200);
@@ -12,8 +15,46 @@ void setup() {
1215
Serial.println("This example demonstrates software PWM on FTDI FT2232HL");
1316
Serial.println();
1417

18+
// Configure PWM frequency (optional, default is 1000 Hz)
19+
analogWriteFrequency(LED1_PIN, 1000);
20+
1521
// Configure pins as outputs
1622
pinMode(LED1_PIN, OUTPUT);
23+
24+
Serial.println("PWM configured at 1000 Hz");
25+
Serial.println("Statistics will be displayed every 5 seconds");
26+
Serial.println();
27+
}
28+
29+
void printPWMStatistics() {
30+
#ifdef USE_FTDI
31+
uint64_t cycles, max_jitter, avg_jitter;
32+
GPIO.getPWMStatistics(LED1_PIN, cycles, max_jitter, avg_jitter);
33+
34+
if (cycles > 0) {
35+
Serial.println("\n--- PWM Statistics ---");
36+
Serial.print("Total cycles: ");
37+
Serial.println((unsigned long)cycles);
38+
Serial.print("Max jitter: ");
39+
Serial.print((unsigned long)max_jitter);
40+
Serial.println(" µs");
41+
Serial.print("Avg jitter: ");
42+
Serial.print((unsigned long)avg_jitter);
43+
Serial.println(" µs");
44+
45+
// Warn if jitter is too high
46+
if (max_jitter > 100) {
47+
Serial.println("⚠️ WARNING: High jitter detected!");
48+
Serial.println(" Consider:");
49+
Serial.println(" - Running with elevated privileges for real-time priority");
50+
Serial.println(" - Reducing system load");
51+
Serial.println(" - Lowering PWM frequency");
52+
} else if (max_jitter < 50) {
53+
Serial.println("✓ Excellent PWM timing accuracy");
54+
}
55+
Serial.println("----------------------\n");
56+
}
57+
#endif
1758
}
1859

1960
void loop() {
@@ -30,6 +71,13 @@ void loop() {
3071
Serial.print((brightness * 100.0f) / 255.0f);
3172
Serial.println("%)");
3273

74+
// Check if it's time to print statistics
75+
unsigned long currentTime = millis();
76+
if (currentTime - lastStatsTime >= STATS_INTERVAL) {
77+
printPWMStatistics();
78+
lastStatsTime = currentTime;
79+
}
80+
3381
delay(100); // Small delay to see the fade effect
3482
}
3583

@@ -46,6 +94,13 @@ void loop() {
4694
Serial.print((brightness * 100.0f) / 255.0f);
4795
Serial.println("%)");
4896

97+
// Check if it's time to print statistics
98+
unsigned long currentTime = millis();
99+
if (currentTime - lastStatsTime >= STATS_INTERVAL) {
100+
printPWMStatistics();
101+
lastStatsTime = currentTime;
102+
}
103+
49104
delay(100);
50105
}
51106

0 commit comments

Comments
 (0)