Skip to content

Commit f31cd07

Browse files
Initial pwmout_period implementation
1 parent 0c46963 commit f31cd07

File tree

2 files changed

+64
-119
lines changed

2 files changed

+64
-119
lines changed

targets/TARGET_Ambiq_Micro/TARGET_Apollo3/device/objects_pwm.h

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,24 @@ typedef enum {
8383
} PWMName;
8484

8585
// Get the CTIMER number of a PWM
86-
#define APOLLO3_PWMNAME_GET_CTIMER(pwmName) (pwmName >> 2)
86+
#define APOLLO3_PWMNAME_GET_CTIMER(pwm_name) (pwm_name >> 2)
8787

8888
// Convert from PWM name to AM_HAL_CTIMER_TIMERA/AM_HAL_CTIMER_TIMERB macro
89-
#define APOLLO3_PWMNAME_GET_SEGMENT(pwmName) (pwmName & 0b10 ? AM_HAL_CTIMER_TIMERB : AM_HAL_CTIMER_TIMERA)
89+
#define APOLLO3_PWMNAME_GET_SEGMENT(pwm_name) (pwm_name & 0b10 ? AM_HAL_CTIMER_TIMERB : AM_HAL_CTIMER_TIMERA)
9090

9191
// Convert from PWM name to AM_HAL_CTIMER_OUTPUT_NORMAL/AM_HAL_CTIMER_OUTPUT_SECONDARY enum value
92-
#define APOLLO3_PWMNAME_GET_OUTPUT(pwmName) (pwmName & 0b1 ? AM_HAL_CTIMER_OUTPUT_SECONDARY : AM_HAL_CTIMER_OUTPUT_NORMAL)
92+
#define APOLLO3_PWMNAME_GET_OUTPUT(pwm_name) (pwm_name & 0b1 ? AM_HAL_CTIMER_OUTPUT_SECONDARY : AM_HAL_CTIMER_OUTPUT_NORMAL)
9393

9494
struct pwmout_s
9595
{
9696
// PWM name that this channel is using
97-
PWMName pwmName;
97+
PWMName pwm_name;
98+
99+
// Clock period configured on this PWM, in floating point seconds
100+
float clock_period;
101+
102+
// Number of counts that the PWM output will make before a new PWM cycle starts
103+
uint32_t top_count;
98104
};
99105

100106
#ifdef __cplusplus

targets/TARGET_Ambiq_Micro/TARGET_Apollo3/device/pwmout_api.c

Lines changed: 54 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "pinmap.h"
2424
#include "objects.h"
2525
#include "mbed_assert.h"
26+
#include <mbed_error.h>
2627

2728
#include <math.h>
2829

@@ -48,8 +49,8 @@ struct pwm_clock_freq {
4849
// - LFRC - internal low freq RC oscillator, 1.024kHz +-32% (no that's not a typo!)
4950
// This means we have quite a wide range of base clock frequencies available, though period accuracy will be pretty
5051
// poor if the LFRC gets selected.
51-
#define NUM_CLOCK_OPTIONS 16
52-
static const struct pwm_clock_freq pwm_clocks[NUM_CLOCK_OPTIONS] = {
52+
#define NUM_CLOCK_SOURCE_OPTIONS 16
53+
static const struct pwm_clock_freq pwm_clock_sources[NUM_CLOCK_SOURCE_OPTIONS] = {
5354
{AM_HAL_CTIMER_HFRC_12MHZ, 12000000},
5455
{AM_HAL_CTIMER_HFRC_3MHZ, 3000000},
5556
{AM_HAL_CTIMER_HFRC_187_5KHZ, 187500},
@@ -73,63 +74,8 @@ static const struct pwm_clock_freq pwm_clocks[NUM_CLOCK_OPTIONS] = {
7374
};
7475

7576
/// Largest top count value supported by hardware. Using this value will provide the highest duty cycle resolution.
76-
const uint16_t MAX_TOP_COUNT = 65534;
77-
78-
79-
80-
/// Calculate the effective PWM period (in floating point seconds) based on a divider and top_count value
81-
static float calc_effective_pwm_period(float divider, uint16_t top_count)
82-
{
83-
// Note: The hardware counts to top_count *inclusively*, so we have to add 1
84-
// to get the number of clock cycles that a given top_count value will produce
85-
return 1.0f / ((clock_get_hz(clk_sys) / divider) / (top_count + 1));
86-
}
87-
88-
/// Calculate the best possible top_count value (rounding up) for a divider and a desired pwm period
89-
static uint16_t calc_top_count_for_period(float divider, float desired_pwm_period)
90-
{
91-
// Derivation:
92-
// desired_pwm_period = 1.0f / ((clock_get_hz(clk_sys) / divider) / (top_count + 1))
93-
// desired_pwm_period = (top_count + 1) / (clock_get_hz(clk_sys) / divider)
94-
// desired_pwm_period * (clock_get_hz(clk_sys) / divider) - 1 = top_count
95-
96-
long top_count_float = lroundf(desired_pwm_period * (clock_get_hz(clk_sys) / divider) - 1);
97-
MBED_ASSERT(top_count_float <= MAX_TOP_COUNT);
98-
return (uint16_t)top_count_float;
99-
}
100-
101-
/// Calculate the best possible floating point divider value for a desired pwm period.
102-
/// This function assumes that top_count is set to MAX_TOP_COUNT.
103-
static float calc_divider_for_period(float desired_pwm_period)
104-
{
105-
// Derivation:
106-
// (desired_pwm_period * clock_get_hz(clk_sys)) / divider - 1 = top_count
107-
// (desired_pwm_period * clock_get_hz(clk_sys)) / divider = top_count + 1
108-
// divider = (desired_pwm_period * clock_get_hz(clk_sys)) / (top_count + 1)
109-
110-
return (desired_pwm_period * clock_get_hz(clk_sys)) / (MAX_TOP_COUNT + 1);
111-
}
112-
113-
/// Convert PWM divider from floating point to a fixed point number (rounding up).
114-
/// The divider is returned as an 8.4 bit fixed point number, which is what the Pico registers use.
115-
static uint16_t pwm_divider_float_to_fixed(float divider_float)
116-
{
117-
// To convert to a fixed point number, multiply by 16 and then round up
118-
uint16_t divider_exact = ceil(divider_float * 16);
119-
120-
// Largest supported divider is 255 and 15/16
121-
if(divider_exact > 0xFFF)
122-
{
123-
divider_exact = 0xFFF;
124-
}
125-
return divider_exact;
126-
}
127-
128-
/// Convert PWM divider from the fixed point hardware value (8.4 bits) to a float.
129-
static float pwm_divider_fixed_to_float(uint16_t divider_fixed)
130-
{
131-
return divider_fixed / 16.0f;
132-
}
77+
/// The hardware performs (CMPR register value + 1) counts and it's a 16-bit register, so the actual max top count is 2^16.
78+
const uint32_t MAX_TOP_COUNT = 65536;
13379

13480
void pwmout_init(pwmout_t *obj, PinName pin)
13581
{
@@ -139,7 +85,7 @@ void pwmout_init(pwmout_t *obj, PinName pin)
13985
const PWMName pwmName = pinmap_peripheral(pin, PinMap_PWM_OUT);
14086

14187
/* Populate PWM object with values. */
142-
obj->pwmName = pwmName;
88+
obj->pwm_name = pwmName;
14389

14490
// Configure the pin
14591
am_hal_ctimer_output_config(APOLLO3_PWMNAME_GET_CTIMER(pwmName),
@@ -206,87 +152,80 @@ float pwmout_read(pwmout_t *obj)
206152
void pwmout_period(pwmout_t *obj, const float desired_period)
207153
{
208154
// To find the period, we perform the following steps:
209-
// - Determine the slowest clock frequency that we can use while still hitting the needed period
155+
// - Determine the fastest clock frequency that we can use while still hitting the needed period
210156
// - Calculate the correct top_count value that will produce as close to the desired period as possible
211-
212-
213-
if(period <= calc_effective_pwm_period(1, MAX_TOP_COUNT))
214-
{
215-
// Short period. Leave divider at 1 and reduce top_count to match the expected period
216-
obj->clock_divider = 1.0f;
217-
obj->cfg.div = PWM_CHn_DIV_1;
218-
obj->top_count = calc_top_count_for_period(obj->clock_divider, period);
157+
// - Write the new top_count value into the hardware
158+
159+
size_t clk_source_idx;
160+
bool found_clk_source = false;
161+
for(clk_source_idx = 0; clk_source_idx < NUM_CLOCK_SOURCE_OPTIONS; ++clk_source_idx) {
162+
const float divider_max_period = MAX_TOP_COUNT / (float)pwm_clock_sources[clk_source_idx].frequency;
163+
if(divider_max_period >= desired_period) {
164+
found_clk_source = true;
165+
break;
166+
}
167+
}
168+
if(!found_clk_source) {
169+
MBED_ERROR(MBED_MAKE_ERROR(MBED_MODULE_DRIVER_PWM, MBED_ERROR_CODE_INVALID_ARGUMENT), "Clock frequency too slow!");
219170
}
220-
else
221-
{
222-
// Long period, need to use divider.
223-
224-
// Step 1: Calculate exact desired divider such that top_count would equal MAX_TOP_COUNT
225-
float desired_divider = calc_divider_for_period(period);
226-
227-
// Step 2: Round desired divider upwards to the next value the hardware can do.
228-
// We go upwards so that the top_count value can be trimmed downwards for the best period accuracy.
229-
uint16_t divider_fixed_point = pwm_divider_float_to_fixed(desired_divider);
230-
obj->cfg.div = divider_fixed_point;
231-
232-
// Step 3: Get the divider we'll actually be using as a float
233-
obj->clock_divider = pwm_divider_fixed_to_float(divider_fixed_point);
234171

235-
// Step 4: For best accuracy, recalculate the top_count value using the divider.
236-
obj->top_count = calc_top_count_for_period(obj->clock_divider, period);
172+
// Now that we have found the best clock source, calculate top_count to hit the desired period
173+
obj->clock_period = 1.0f / (float)pwm_clock_sources[clk_source_idx].frequency;
174+
obj->top_count = lroundf(desired_period / obj->clock_period);
237175

238-
#if APOLLO3_PWMOUT_DEBUG
239-
printf("period = %f, desired_divider = %f\n",
240-
period,
241-
desired_divider);
242-
#endif
176+
// The hardware cannot support a top_count of less than 2. If that happened than it means the
177+
// frequency is too fast.
178+
if(obj->top_count < 2) {
179+
MBED_ERROR(MBED_MAKE_ERROR(MBED_MODULE_DRIVER_PWM, MBED_ERROR_CODE_INVALID_ARGUMENT), "Clock frequency too fast!");
243180
}
244181

245-
// Save period for later
246-
obj->period = period;
247-
248182
#if APOLLO3_PWMOUT_DEBUG
249-
printf("obj->clock_divider = %f, obj->cfg.div = %" PRIu32 ", obj->top_count = %" PRIu16 "\n",
250-
obj->clock_divider,
251-
obj->cfg.div,
183+
printf("clk_source_idx = %zu, obj->clock_period = %f, obj->top_count = %" PRIu32 "\n",
184+
clk_source_idx,
185+
obj->clock_period,
252186
obj->top_count);
253187
#endif
254188

255-
// Set the new divider and top_count values.
256-
pwm_config_set_wrap(&(obj->cfg), obj->top_count);
257-
pwm_init(obj->slice, &(obj->cfg), false);
189+
// Set new clock source. This stops the timer.
190+
am_hal_ctimer_config_single(APOLLO3_PWMNAME_GET_CTIMER(obj->pwm_name),
191+
APOLLO3_PWMNAME_GET_SEGMENT(obj->pwm_name),
192+
AM_HAL_CTIMER_FN_PWM_REPEAT | clk_source_idx);
193+
194+
// Set new period value. Note that:
195+
// - We need to program a value of (top_count - 1) into the HW register
196+
// - As part of setting the period we also need to program the duty cycle. For now we program it to zero,
197+
// in anticipation of a future pwmout_write() call
198+
if(APOLLO3_PWMNAME_GET_OUTPUT(obj->pwm_name) == AM_HAL_CTIMER_OUTPUT_NORMAL) {
199+
am_hal_ctimer_period_set(APOLLO3_PWMNAME_GET_CTIMER(obj->pwm_name),
200+
APOLLO3_PWMNAME_GET_SEGMENT(obj->pwm_name),
201+
obj->top_count - 1,
202+
0);
203+
}
204+
else {
205+
am_hal_ctimer_aux_period_set(APOLLO3_PWMNAME_GET_CTIMER(obj->pwm_name),
206+
APOLLO3_PWMNAME_GET_SEGMENT(obj->pwm_name),
207+
obj->top_count - 1,
208+
0);
209+
}
210+
211+
am_hal_ctimer_start(APOLLO3_PWMNAME_GET_CTIMER(obj->pwm_name), APOLLO3_PWMNAME_GET_SEGMENT(obj->pwm_name));
258212
}
259213

260-
/** Set the PWM period specified in miliseconds, keeping the duty cycle the same
261-
*
262-
* Parameter obj The pwmout object
263-
* Parameter ms The milisecond period
264-
*/
265214
void pwmout_period_ms(pwmout_t *obj, int period)
266215
{
267216
/* Set new period. */
268217
pwmout_period(obj, period / 1000.0f);
269218
}
270219

271-
/** Set the PWM period specified in microseconds, keeping the duty cycle the same
272-
*
273-
* Parameter obj The pwmout object
274-
* Parameter us The microsecond period
275-
*/
276220
void pwmout_period_us(pwmout_t *obj, int period)
277221
{
278222
/* Set new period. */
279223
pwmout_period(obj, period / 1000000.0f);
280224
}
281225

282-
/** Read the PWM period specified in microseconds
283-
*
284-
* @param obj The pwmout object
285-
* @return A int output period
286-
*/
287226
int pwmout_read_period_us(pwmout_t *obj)
288227
{
289-
return lroundf(1000000 * calc_effective_pwm_period(obj->clock_divider, obj->top_count));
228+
return lroundf(1000000 * obj->top_count * obj->clock_period);
290229
}
291230

292231
/** Set the PWM pulsewidth specified in seconds, keeping the period the same.

0 commit comments

Comments
 (0)