If the internal circuitry of your fans transforms the pwm-signal to an analog voltage
the pwm-frequency can be varied in some range. I guess most pwm-controlled devices do so.
You can test this by varying the pwm-frequency of your one-channel pwmcode
Under this pre-condition a different approach can be used:
using one timer-interrupt and create the pwm-signals in software.
Though on a Arduino Uno the maximum-frequency for this is limited.
When using timer2 I tested it to a maximum-frequency of 30 kHz as the base-frequency for the timer-interrupt.
Depending on the resolution you want to have for the pwm-signal the pwm-frequency is
base-frequency / resolution
example
basefrequency 30 kHz
resolution 10 (steps from 0% to 100% = 0,10,20,30,...100%)
pwm-frequency = 30 kHz / 10 = 3 kHz
basefrequency 30 kHz
resolution 100 (steps from 0% to 100% = 0,1,2,3,...100%)
pwm-frequency = 30 kHz / 100 = 300 Hz
I'm not very familiar with the counters inside arduinos
For other microcontrollers the reachable frequency will be higher but as they have a different hardware setting up the frequency is done differently.
Anyway I guess for any microcontroller like Seeeduino XIAO, all kinds of SMT-boards, SAMD21 SAMD91, nRF, ESP8266, ESP32 you will find demo-codes that show how to use a timer-interrupt.
So here is the demo-code for a 4 channel PWM created by a single timer-interrupt using timer2 on Arduino = microcontroller-type Atmega328P
// this demo-code belongs to the public domain
// this code demonstrates how to blink the onboard-LED
// of an Arduino-Uno (connected to IO-pin 13)
// in a nonblocking way using timing based on function millis()
// and creating a pwm-signal "in the backround"
// through setting up a timer-interrupt through configuring timer2
// A T T E N T I O N ! R E M A R K
// some other libraries that make use of timer2 may conflict with this
// start of macros dbg and dbgi
#define dbg(myFixedText, variableName) \
Serial.print( F(#myFixedText " " #variableName"=") ); \
Serial.println(variableName);
// usage: dbg("1:my fixed text",myVariable);
// myVariable can be any variable or expression that is defined in scope
#define dbgi(myFixedText, variableName,timeInterval) \
do { \
static unsigned long intervalStartTime; \
if ( millis() - intervalStartTime >= timeInterval ){ \
intervalStartTime = millis(); \
Serial.print( F(#myFixedText " " #variableName"=") ); \
Serial.println(variableName); \
} \
} while (false);
// usage: dbgi("2:my fixed text",myVariable,1000);
// myVariable can be any variable or expression that is defined in scope
// third parameter is the time in milliseconds that must pass by until the next time a
// Serial.print is executed
// end of macros dbg and dbgi
void PrintFileNameDateTime() {
Serial.println( F("Code running comes from file ") );
Serial.println( F(__FILE__));
Serial.print( F(" compiled ") );
Serial.print(F(__DATE__));
Serial.print( F(" ") );
Serial.println(F(__TIME__));
}
boolean TimePeriodIsOver (unsigned long &periodStartTime, unsigned long TimePeriod) {
unsigned long currentMillis = millis();
if ( currentMillis - periodStartTime >= TimePeriod ) {
periodStartTime = currentMillis; // set new expireTime
return true; // more time than TimePeriod) has elapsed since last time if-condition was true
}
else return false; // not expired
}
unsigned long MyTestTimer = 0; // variables MUST be of type unsigned long
const byte OnBoard_LED = 13;
void BlinkHeartBeatLED(int IO_Pin, int BlinkPeriod) {
static unsigned long MyBlinkTimer;
pinMode(IO_Pin, OUTPUT);
if ( TimePeriodIsOver(MyBlinkTimer, BlinkPeriod) ) {
digitalWrite(IO_Pin, !digitalRead(IO_Pin) );
}
}
const unsigned long pwmBaseFrequency = 30000;
const unsigned long resolution = 10;
const unsigned long pulseFreq = pwmBaseFrequency / resolution;
volatile unsigned long pwmCounter = 0;
volatile unsigned long periodCount = pwmBaseFrequency / pulseFreq;
const byte maxChannels = 4;
volatile byte dutyCh[maxChannels];
volatile unsigned long pwmOnTimeCnt[maxChannels];
const byte pwmPin[maxChannels] = {4, 5, 6, 7};
byte duty = resolution * 2;
void setupTimerInterrupt(unsigned long ISR_call_frequency) {
const byte Prescaler___8 = (1 << CS21);
const byte Prescaler__32 = (1 << CS21) + (1 << CS20);
const byte Prescaler__64 = (1 << CS22);
const byte Prescaler_128 = (1 << CS22) + (1 << CS20);
const byte Prescaler_256 = (1 << CS22) + (1 << CS21);
const byte Prescaler1024 = (1 << CS22) + (1 << CS21) + (1 << CS20);
const unsigned long CPU_Clock = 16000000;
const byte toggleFactor = 1;
unsigned long OCR2A_value;
cli();//stop interrupts
TCCR2A = 0;// set entire TCCR2A register to 0
TCCR2B = 0;// same for TCCR2B
TCNT2 = 0;//initialize counter value to 0
TCCR2A |= (1 << WGM21); // turn on CTC mode
TIMSK2 |= (1 << OCIE2A); // enable timer compare interrupt
// the prescaler must be setup to a value that the calculation
// of the value for OCR2A is below 256
TCCR2B = Prescaler___8;
OCR2A_value = (CPU_Clock / ( 8 * ISR_call_frequency * toggleFactor) ) - 1;
dbg("1 setup: timer ", OCR2A_value);
if (OCR2A_value > 256) { // if value too big
TCCR2B = Prescaler__32; // set higher prescaler
OCR2A_value = (CPU_Clock / ( 32 * ISR_call_frequency * toggleFactor) ) - 1;
dbg("setup: prescaler 32", OCR2A_value);
}
if (OCR2A_value > 256) { // if value too big
TCCR2B = Prescaler__64;// set higher prescaler
OCR2A_value = (CPU_Clock / ( 64 * ISR_call_frequency * toggleFactor) ) - 1;
dbg("setup: prescaler 64", OCR2A_value);
}
if (OCR2A_value > 256) { // if value too big
TCCR2B = Prescaler_128;// set higher prescaler
OCR2A_value = (CPU_Clock / ( 128 * ISR_call_frequency * toggleFactor) ) - 1;
dbg("setup: prescaler 128", OCR2A_value);
}
if (OCR2A_value > 256) { // if value too big
TCCR2B = Prescaler_256; // set higher prescaler
OCR2A_value = (CPU_Clock / ( 256 * ISR_call_frequency * toggleFactor) ) - 1;
dbg("setup: prescaler 256", OCR2A_value);
}
if (OCR2A_value > 256) { // if value too big
TCCR2B = Prescaler1024; // set higher prescaler
OCR2A_value = (CPU_Clock / ( 1024 * ISR_call_frequency * toggleFactor) ) - 1;
dbg("setup: prescaler 1024", OCR2A_value);
}
OCR2A = OCR2A_value; // finally set the value of OCR2A
sei();//allow interrupts
dbg("setup: timer done", OCR2A_value);
dbg("setup", periodCount);
}
unsigned long pwmCount = 0;
const byte Clock_Pin = 9;
void setup() {
Serial.begin(115200);
Serial.println( F("Setup-Start") );
PrintFileNameDateTime();
for (int i = 0; i < maxChannels; i++) {
pinMode(pwmPin[i], OUTPUT);
}
/*
pinMode(pwmPin[0], OUTPUT);
pinMode(pwmPin[1], OUTPUT);
pinMode(pwmPin[2], OUTPUT);
pinMode(pwmPin[3], OUTPUT);
*/
pinMode(Clock_Pin, OUTPUT);
setupTimerInterrupt(pwmBaseFrequency);
pwmCount = 0;
SetDuty(0,10);
SetDuty(1,25);
SetDuty(2,80);
SetDuty(3,100);
}
void createPWM() {
pwmCount++;
// for debugging purposes create a clock-signal
digitalWrite(Clock_Pin, !digitalRead(Clock_Pin) );
if (pwmCount == periodCount) { // if new period starts
pwmCount = 0; // reset counter
for (int i = 0; i < maxChannels; i++) {
if (dutyCh[i] > 0) { // if duty is above 0%
digitalWrite(pwmPin[i], HIGH); // switch channel ON
}
}
}
for (int i = 0; i < maxChannels; i++) {
if (pwmCount == pwmOnTimeCnt[i]) { // if ON-time is reached
digitalWrite(pwmPin[i], LOW); // switch channel OFF
}
}
}
void SetDuty(byte channel, byte duty) {
if (duty > 100) {
duty = 100;
}
if (channel < maxChannels) {
dutyCh[channel] = duty;
pwmOnTimeCnt[channel] = (periodCount * duty) / 100;
}
dbg("1:",channel);
dbg("2:",dutyCh[channel]);
dbg("3:",pwmOnTimeCnt[channel]);
dbg("IO-pin Nr.:",pwmPin[channel]);
}
ISR(TIMER2_COMPA_vect) {
createPWM();
}
void loop() {
BlinkHeartBeatLED(OnBoard_LED, 200);
if (TimePeriodIsOver(MyTestTimer,1000) ) {
// if 3000 milliseconds have passed by
duty = duty + resolution;
if (duty > 100) {
duty = 0;
}
SetDuty( 0,duty );
}
}
best regards Stefan