1
- /*
2
- * Copyright (c) 2018 Nordic Semiconductor ASA
3
- * All rights reserved.
1
+ /* mbed Microcontroller Library
2
+ * Copyright (c) 2024, Arm Limited and affiliates.
3
+ * SPDX-License-Identifier: Apache-2.0
4
4
*
5
- * Redistribution and use in source and binary forms, with or without modification,
6
- * are permitted provided that the following conditions are met:
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
7
8
*
8
- * 1. Redistributions of source code must retain the above copyright notice, this list
9
- * of conditions and the following disclaimer.
10
- *
11
- * 2. Redistributions in binary form, except as embedded into a Nordic Semiconductor ASA
12
- * integrated circuit in a product or a software update for such product, must reproduce
13
- * the above copyright notice, this list of conditions and the following disclaimer in
14
- * the documentation and/or other materials provided with the distribution.
15
- *
16
- * 3. Neither the name of Nordic Semiconductor ASA nor the names of its contributors may be
17
- * used to endorse or promote products derived from this software without specific prior
18
- * written permission.
19
- *
20
- * 4. This software, with or without modification, must only be used with a
21
- * Nordic Semiconductor ASA integrated circuit.
22
- *
23
- * 5. Any software provided in binary or object form under this license must not be reverse
24
- * engineered, decompiled, modified and/or disassembled.
25
- *
26
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
27
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
28
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
29
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
30
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
31
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
32
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
33
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
34
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
9
+ * http://www.apache.org/licenses/LICENSE-2.0
36
10
*
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
37
16
*/
38
17
18
+
39
19
#if DEVICE_PWMOUT
40
20
41
21
#include "hal/pwmout_api.h"
45
25
#include "hardware/clocks.h"
46
26
#include "mbed_assert.h"
47
27
48
- const uint count_top = 1000 ;
28
+ #include <math.h>
29
+
30
+ // Change to 1 to enable debug prints of what's being calculated.
31
+ // Must comment out the critical section calls in PwmOut to use.
32
+ #define RP2040_PWMOUT_DEBUG 0
33
+
34
+ #if RP2040_PWMOUT_DEBUG
35
+ #include <stdio.h>
36
+ #include <inttypes.h>
37
+ #endif
38
+
39
+ /// Largest top count value supported by hardware. Using this value will provide the highest duty cycle resolution,
40
+ /// but will limit the period to a maximum of (1 / (125 MHz / (65534 + 1)) =) 524 us
41
+ const uint16_t MAX_TOP_COUNT = 65534 ;
42
+
43
+ /// Value for PWM_CHn_DIV register that produces a division of 1
44
+ const uint16_t PWM_CHn_DIV_1 = 0x010 ;
45
+
46
+ /// Calculate the effective PWM period (in floating point seconds) based on a divider and top_count value
47
+ static float calc_effective_pwm_period (float divider , uint16_t top_count )
48
+ {
49
+ // Note: The hardware counts to top_count *inclusively*, so we have to add 1
50
+ // to get the number of clock cycles that a given top_count value will produce
51
+ return 1.0f / ((clock_get_hz (clk_sys ) / divider ) / (top_count + 1 ));
52
+ }
53
+
54
+ /// Calculate the best possible top_count value (rounding up) for a divider and a desired pwm period
55
+ static uint16_t calc_top_count_for_period (float divider , float desired_pwm_period )
56
+ {
57
+ // Derivation:
58
+ // desired_pwm_period = 1.0f / ((clock_get_hz(clk_sys) / divider) / (top_count + 1))
59
+ // desired_pwm_period = (top_count + 1) / (clock_get_hz(clk_sys) / divider)
60
+ // desired_pwm_period * (clock_get_hz(clk_sys) / divider) - 1 = top_count
61
+
62
+ long top_count_float = lroundf (desired_pwm_period * (clock_get_hz (clk_sys ) / divider ) - 1 );
63
+ MBED_ASSERT (top_count_float <= MAX_TOP_COUNT );
64
+ return (uint16_t )top_count_float ;
65
+ }
66
+
67
+ /// Calculate the best possible floating point divider value for a desired pwm period.
68
+ /// This function assumes that top_count is set to MAX_TOP_COUNT.
69
+ static float calc_divider_for_period (float desired_pwm_period )
70
+ {
71
+ // Derivation:
72
+ // (desired_pwm_period * clock_get_hz(clk_sys)) / divider - 1 = top_count
73
+ // (desired_pwm_period * clock_get_hz(clk_sys)) / divider = top_count + 1
74
+ // divider = (desired_pwm_period * clock_get_hz(clk_sys)) / (top_count + 1)
75
+
76
+ return (desired_pwm_period * clock_get_hz (clk_sys )) / (MAX_TOP_COUNT + 1 );
77
+ }
78
+
79
+ /// Convert PWM divider from floating point to a fixed point number (rounding up).
80
+ /// The divider is returned as an 8.4 bit fixed point number, which is what the Pico registers use.
81
+ static uint16_t pwm_divider_float_to_fixed (float divider_float )
82
+ {
83
+ // To convert to a fixed point number, multiply by 16 and then round up
84
+ uint16_t divider_exact = ceil (divider_float * 16 );
85
+
86
+ // Largest supported divider is 255 and 15/16
87
+ if (divider_exact > 0xFFF )
88
+ {
89
+ divider_exact = 0xFFF ;
90
+ }
91
+ return divider_exact ;
92
+ }
93
+
94
+ /// Convert PWM divider from the fixed point hardware value (8.4 bits) to a float.
95
+ static float pwm_divider_fixed_to_float (uint16_t divider_fixed )
96
+ {
97
+ return divider_fixed / 16.0f ;
98
+ }
49
99
50
100
/** Initialize the pwm out peripheral and configure the pin
51
101
*
@@ -60,11 +110,10 @@ void pwmout_init(pwmout_t *obj, PinName pin)
60
110
obj -> slice = pwm_gpio_to_slice_num (pin );
61
111
obj -> channel = pwm_gpio_to_channel (pin );
62
112
obj -> pin = pin ;
63
- obj -> period = 0 ;
113
+ obj -> top_count = MAX_TOP_COUNT ;
64
114
obj -> percent = 0.5f ;
65
-
66
115
obj -> cfg = pwm_get_default_config ();
67
- pwm_config_set_wrap (& (obj -> cfg ), count_top );
116
+ pwm_config_set_wrap (& (obj -> cfg ), obj -> top_count );
68
117
69
118
pwm_init (obj -> slice , & (obj -> cfg ), false);
70
119
gpio_set_function (pin , GPIO_FUNC_PWM );
@@ -89,7 +138,25 @@ void pwmout_free(pwmout_t *obj)
89
138
void pwmout_write (pwmout_t * obj , float percent )
90
139
{
91
140
obj -> percent = percent ;
92
- pwm_set_gpio_level (obj -> pin , percent * (count_top + 1 ));
141
+
142
+ // Per datasheet section 4.5.2.2, a period value of top_count + 1 produces 100% duty cycle
143
+ int32_t new_reset_counts = lroundf ((obj -> top_count + 1 ) * percent );
144
+
145
+ // Clamp to valid values
146
+ if (new_reset_counts > obj -> top_count + 1 )
147
+ {
148
+ new_reset_counts = obj -> top_count + 1 ;
149
+ }
150
+ else if (new_reset_counts < 0 )
151
+ {
152
+ new_reset_counts = 0 ;
153
+ }
154
+
155
+ #if RP2040_PWMOUT_DEBUG
156
+ printf ("new_reset_counts: %" PRIu32 "\n" , new_reset_counts );
157
+ #endif
158
+
159
+ pwm_set_chan_level (obj -> slice , obj -> channel , new_reset_counts );
93
160
pwm_set_enabled (obj -> slice , true);
94
161
}
95
162
@@ -114,8 +181,61 @@ float pwmout_read(pwmout_t *obj)
114
181
*/
115
182
void pwmout_period (pwmout_t * obj , float period )
116
183
{
117
- /* Set new period. */
118
- pwmout_period_us (obj , period * 1000000 );
184
+ // Two possibilities here:
185
+ // - If the period is relatively short (< about 524 us), we want to keep the clock divider at 1
186
+ // and reduce top_count to match the period
187
+ // - If the period is larger than what we can achieve with a clock divider of 1, we need to
188
+ // use a higher clock divider, then recalculate the top_count to match
189
+
190
+ // Note: For math this complex, I wasn't able to avoid using floating point values.
191
+ // This function won't be too efficient, but for now I just want something that works and
192
+ // can access the full PWM range.
193
+
194
+ if (period <= calc_effective_pwm_period (1 , MAX_TOP_COUNT ))
195
+ {
196
+ // Short period. Leave divider at 1 and reduce top_count to match the expected period
197
+ obj -> clock_divider = 1.0f ;
198
+ obj -> cfg .div = PWM_CHn_DIV_1 ;
199
+ obj -> top_count = calc_top_count_for_period (obj -> clock_divider , period );
200
+ }
201
+ else
202
+ {
203
+ // Long period, need to use divider.
204
+
205
+ // Step 1: Calculate exact desired divider such that top_count would equal MAX_TOP_COUNT
206
+ float desired_divider = calc_divider_for_period (period );
207
+
208
+ // Step 2: Round desired divider upwards to the next value the hardware can do.
209
+ // We go upwards so that the top_count value can be trimmed downwards for the best period accuracy.
210
+ uint16_t divider_fixed_point = pwm_divider_float_to_fixed (desired_divider );
211
+ obj -> cfg .div = divider_fixed_point ;
212
+
213
+ // Step 3: Get the divider we'll actually be using as a float
214
+ obj -> clock_divider = pwm_divider_fixed_to_float (divider_fixed_point );
215
+
216
+ // Step 4: For best accuracy, recalculate the top_count value using the divider.
217
+ obj -> top_count = calc_top_count_for_period (obj -> clock_divider , period );
218
+
219
+ #if RP2040_PWMOUT_DEBUG
220
+ printf ("period = %f, desired_divider = %f\n" ,
221
+ period ,
222
+ desired_divider );
223
+ #endif
224
+ }
225
+
226
+ // Save period for later
227
+ obj -> period = period ;
228
+
229
+ #if RP2040_PWMOUT_DEBUG
230
+ printf ("obj->clock_divider = %f, obj->cfg.div = %" PRIu32 ", obj->top_count = %" PRIu16 "\n" ,
231
+ obj -> clock_divider ,
232
+ obj -> cfg .div ,
233
+ obj -> top_count );
234
+ #endif
235
+
236
+ // Set the new divider and top_count values.
237
+ pwm_config_set_wrap (& (obj -> cfg ), obj -> top_count );
238
+ pwm_init (obj -> slice , & (obj -> cfg ), false);
119
239
}
120
240
121
241
/** Set the PWM period specified in miliseconds, keeping the duty cycle the same
@@ -126,7 +246,7 @@ void pwmout_period(pwmout_t *obj, float period)
126
246
void pwmout_period_ms (pwmout_t * obj , int period )
127
247
{
128
248
/* Set new period. */
129
- pwmout_period_us (obj , period * 1000 );
249
+ pwmout_period (obj , period / 1000.0f );
130
250
}
131
251
132
252
/** Set the PWM period specified in microseconds, keeping the duty cycle the same
@@ -136,18 +256,18 @@ void pwmout_period_ms(pwmout_t *obj, int period)
136
256
*/
137
257
void pwmout_period_us (pwmout_t * obj , int period )
138
258
{
139
- obj -> period = period ;
140
-
141
- // min_period should be 8us
142
- uint32_t min_period = 1000000 * count_top / clock_get_hz (clk_sys );
143
-
144
- pwm_config_set_clkdiv (& (obj -> cfg ), (float )period / (float )min_period );
145
- pwm_init (obj -> slice , & (obj -> cfg ), false);
259
+ /* Set new period. */
260
+ pwmout_period (obj , period / 1000000.0f );
146
261
}
147
262
263
+ /** Read the PWM period specified in microseconds
264
+ *
265
+ * @param obj The pwmout object
266
+ * @return A int output period
267
+ */
148
268
int pwmout_read_period_us (pwmout_t * obj )
149
269
{
150
- return obj -> period ;
270
+ return lroundf ( 1000000 * calc_effective_pwm_period ( obj -> clock_divider , obj -> top_count )) ;
151
271
}
152
272
153
273
/** Set the PWM pulsewidth specified in seconds, keeping the period the same.
@@ -157,7 +277,7 @@ int pwmout_read_period_us(pwmout_t *obj)
157
277
*/
158
278
void pwmout_pulsewidth (pwmout_t * obj , float pulse )
159
279
{
160
- pwmout_pulsewidth_us (obj , pulse * 1000000 );
280
+ pwmout_write (obj , pulse / obj -> period );
161
281
}
162
282
163
283
/** Set the PWM pulsewidth specified in miliseconds, keeping the period the same.
@@ -167,7 +287,7 @@ void pwmout_pulsewidth(pwmout_t *obj, float pulse)
167
287
*/
168
288
void pwmout_pulsewidth_ms (pwmout_t * obj , int pulse )
169
289
{
170
- pwmout_pulsewidth_us (obj , pulse * 1000 );
290
+ pwmout_write (obj , ( pulse * .001f ) / obj -> period );
171
291
}
172
292
173
293
/** Set the PWM pulsewidth specified in microseconds, keeping the period the same.
@@ -177,19 +297,11 @@ void pwmout_pulsewidth_ms(pwmout_t *obj, int pulse)
177
297
*/
178
298
void pwmout_pulsewidth_us (pwmout_t * obj , int pulse )
179
299
{
180
- /* Cap pulsewidth to period. */
181
- if (pulse > obj -> period ) {
182
- pulse = obj -> period ;
183
- }
184
-
185
- obj -> percent = (float ) pulse / (float ) obj -> period ;
186
-
187
- /* Restart instance with new values. */
188
- pwmout_write (obj , obj -> percent );
300
+ pwmout_write (obj , (pulse * .000001f ) / obj -> period );
189
301
}
190
302
191
303
int pwmout_read_pulsewidth_us (pwmout_t * obj ) {
192
- return (obj -> period ) * ( obj -> percent );
304
+ return lroundf (obj -> period * obj -> percent * 1000000 );
193
305
}
194
306
195
307
const PinMap * pwmout_pinmap ()
0 commit comments