Skip to content

Commit 28359cd

Browse files
committed
rp2/machine_pwm: Switch to more accurate frequency formula; use rounding for consistent duty cycle calculations.
1 parent 7fe7c55 commit 28359cd

File tree

1 file changed

+28
-22
lines changed

1 file changed

+28
-22
lines changed

ports/rp2/machine_pwm.c

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -113,30 +113,31 @@ STATIC void mp_machine_pwm_freq_set(machine_pwm_obj_t *self, mp_int_t freq) {
113113
// Maximum "top" is set at 65534 to be able to achieve 100% duty with 65535.
114114
#define TOP_MAX 65534
115115
uint32_t source_hz = clock_get_hz(clk_sys);
116-
uint32_t div16_top = 16 * source_hz / freq;
117-
uint32_t top = 1;
118-
for (;;) {
119-
// Try a few small prime factors to get close to the desired frequency.
120-
if (div16_top >= 16 * 5 && div16_top % 5 == 0 && top * 5 <= TOP_MAX) {
121-
div16_top /= 5;
122-
top *= 5;
123-
} else if (div16_top >= 16 * 3 && div16_top % 3 == 0 && top * 3 <= TOP_MAX) {
124-
div16_top /= 3;
125-
top *= 3;
126-
} else if (div16_top >= 16 * 2 && top * 2 <= TOP_MAX) {
127-
div16_top /= 2;
128-
top *= 2;
129-
} else {
130-
break;
131-
}
116+
uint32_t div16;
117+
118+
if ((source_hz + freq / 2) / freq < TOP_MAX) {
119+
// If possible (based on the formula for TOP below), use a DIV of 1.
120+
// This also prevents overflow in the DIV calculation.
121+
div16 = 16;
122+
} else {
123+
// Otherwise, choose the smallest possible DIV for maximum
124+
// duty cycle resolution.
125+
// Constraint: 16*F/(div16*freq) < TOP_MAX
126+
// So: div16 = ceil(16*F/(TOP_MAX*freq))
127+
128+
div16 = (16 * source_hz + TOP_MAX * freq - 1) / (TOP_MAX * freq);
132129
}
133-
if (div16_top < 16) {
130+
131+
// Set TOP as accurately as possible using rounding.
132+
uint32_t top = (16 * source_hz + div16 * freq / 2) / (div16 * freq) - 1;
133+
134+
if (div16 < 16) {
134135
mp_raise_ValueError(MP_ERROR_TEXT("freq too large"));
135-
} else if (div16_top >= 256 * 16) {
136+
} else if (div16 >= 256 * 16) {
136137
mp_raise_ValueError(MP_ERROR_TEXT("freq too small"));
137138
}
138-
pwm_hw->slice[self->slice].div = div16_top;
139-
pwm_hw->slice[self->slice].top = top - 1;
139+
pwm_hw->slice[self->slice].div = div16;
140+
pwm_hw->slice[self->slice].top = top;
140141
if (self->duty_type == DUTY_U16) {
141142
mp_machine_pwm_duty_set_u16(self, self->duty);
142143
} else if (self->duty_type == DUTY_NS) {
@@ -148,12 +149,17 @@ STATIC mp_obj_t mp_machine_pwm_duty_get_u16(machine_pwm_obj_t *self) {
148149
uint32_t top = pwm_hw->slice[self->slice].top;
149150
uint32_t cc = pwm_hw->slice[self->slice].cc;
150151
cc = (cc >> (self->channel ? PWM_CH0_CC_B_LSB : PWM_CH0_CC_A_LSB)) & 0xffff;
151-
return MP_OBJ_NEW_SMALL_INT(cc * 65535 / (top + 1));
152+
153+
// Use rounding (instead of flooring) here to give as accurate an
154+
// estimate as possible.
155+
return MP_OBJ_NEW_SMALL_INT((cc * 65535 + (top + 1) / 2) / (top + 1));
152156
}
153157

154158
STATIC void mp_machine_pwm_duty_set_u16(machine_pwm_obj_t *self, mp_int_t duty_u16) {
155159
uint32_t top = pwm_hw->slice[self->slice].top;
156-
uint32_t cc = duty_u16 * (top + 1) / 65535;
160+
161+
// Use rounding here to set it as accurately as possible.
162+
uint32_t cc = (duty_u16 * (top + 1) + 65535 / 2) / 65535;
157163
pwm_set_chan_level(self->slice, self->channel, cc);
158164
pwm_set_enabled(self->slice, true);
159165
self->duty = duty_u16;

0 commit comments

Comments
 (0)