1 /*
2  * Copyright (c) 2024 Nordic Semiconductor ASA
3  *
4  * SPDX-License-Identifier: Apache-2.0
5  */
6 
7 #include <stdlib.h>
8 #include <zephyr/logging/log.h>
9 #include "feedback.h"
10 
11 #include <nrfx_dppi.h>
12 #include <nrfx_gpiote.h>
13 #include <nrfx_timer.h>
14 #include <hal/nrf_gpio.h>
15 #include <hal/nrf_usbd.h>
16 #include <hal/nrf_i2s.h>
17 #include <helpers/nrfx_gppi.h>
18 
19 LOG_MODULE_REGISTER(feedback, LOG_LEVEL_INF);
20 
21 static const nrfx_gpiote_t gpiote = NRFX_GPIOTE_INSTANCE(0);
22 
23 #define FEEDBACK_PIN NRF_GPIO_PIN_MAP(1, 9)
24 #define FEEDBACK_TIMER_INSTANCE_NUMBER 2
25 #define FEEDBACK_TIMER_USBD_SOF_CAPTURE 0
26 #define FEEDBACK_TIMER_I2S_FRAMESTART_CAPTURE 1
27 
28 static const nrfx_timer_t feedback_timer_instance =
29 	NRFX_TIMER_INSTANCE(FEEDBACK_TIMER_INSTANCE_NUMBER);
30 
31 /* See 5.12.4.2 Feedback in Universal Serial Bus Specification Revision 2.0 for
32  * more information about the feedback. There is a direct implementation of the
33  * specification where P=1 when @kconfig{CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER}
34  * is enabled, because I2S LRCLK edges (and not the clock) are being counted by
35  * a timer. Otherwise, when @kconfig{CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER} is
36  * disabled, we are faking P=5 value using indirect offset measurements and
37  * we use such estimate on PI controller updated on every SOF.
38  *
39  * While it might be possible to determine I2S FRAMESTART to USB SOF offset
40  * entirely in software, the I2S API lacks appropriate timestamping. Therefore
41  * this sample uses target-specific code to perform the measurements. Note that
42  * the use of dedicated target-specific peripheral essentially eliminates
43  * software scheduling jitter and it is likely that a pure software only
44  * solution would require additional filtering in indirect offset measurements.
45  *
46  * Full-Speed isochronous feedback is Q10.10 unsigned integer left-justified in
47  * the 24-bits so it has Q10.14 format. This sample application puts zeroes to
48  * the 4 least significant bits (does not use the bits for extra precision).
49  */
50 #define FEEDBACK_K		10
51 #if defined(CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER)
52 #define FEEDBACK_P		1
53 #else
54 #define FEEDBACK_P		5
55 #endif
56 
57 #define FEEDBACK_FS_SHIFT	4
58 
59 static struct feedback_ctx {
60 	uint32_t fb_value;
61 	int32_t rel_sof_offset;
62 	int32_t base_sof_offset;
63 	union {
64 		/* For edge counting */
65 		struct {
66 			uint32_t fb_counter;
67 			uint16_t fb_periods;
68 		};
69 		/* For PI controller */
70 		int32_t integrator;
71 	};
72 } fb_ctx;
73 
feedback_edge_counter_setup(void)74 static nrfx_err_t feedback_edge_counter_setup(void)
75 {
76 	nrfx_err_t err;
77 	uint8_t feedback_gpiote_channel;
78 	uint8_t feedback_gppi_channel;
79 	nrfx_gpiote_trigger_config_t trigger_config = {
80 		.trigger = NRFX_GPIOTE_TRIGGER_TOGGLE,
81 		.p_in_channel = &feedback_gpiote_channel,
82 	};
83 	nrfx_gpiote_input_pin_config_t input_pin_config = {
84 		.p_trigger_config = &trigger_config,
85 	};
86 
87 	/* App core is using feedback pin */
88 	nrf_gpio_pin_control_select(FEEDBACK_PIN, NRF_GPIO_PIN_SEL_APP);
89 
90 	err = nrfx_gpiote_channel_alloc(&gpiote, &feedback_gpiote_channel);
91 	if (err != NRFX_SUCCESS) {
92 		return err;
93 	}
94 
95 	nrfx_gpiote_input_configure(&gpiote, FEEDBACK_PIN, &input_pin_config);
96 	nrfx_gpiote_trigger_enable(&gpiote, FEEDBACK_PIN, false);
97 
98 	/* Configure TIMER in COUNTER mode */
99 	const nrfx_timer_config_t cfg = {
100 		.frequency = NRFX_MHZ_TO_HZ(1UL),
101 		.mode = NRF_TIMER_MODE_COUNTER,
102 		.bit_width = NRF_TIMER_BIT_WIDTH_32,
103 		.interrupt_priority = NRFX_TIMER_DEFAULT_CONFIG_IRQ_PRIORITY,
104 		.p_context = NULL,
105 	};
106 
107 	err = nrfx_timer_init(&feedback_timer_instance, &cfg, NULL);
108 	if (err != NRFX_SUCCESS) {
109 		LOG_ERR("nrfx timer init error (sample clk feedback) - Return value: %d", err);
110 		return err;
111 	}
112 
113 	/* Subscribe TIMER COUNT task to GPIOTE IN event */
114 	err = nrfx_gppi_channel_alloc(&feedback_gppi_channel);
115 	if (err != NRFX_SUCCESS) {
116 		LOG_ERR("gppi_channel_alloc failed with: %d\n", err);
117 		return err;
118 	}
119 
120 	nrfx_gppi_channel_endpoints_setup(feedback_gppi_channel,
121 		nrfx_gpiote_in_event_address_get(&gpiote, FEEDBACK_PIN),
122 		nrfx_timer_task_address_get(&feedback_timer_instance, NRF_TIMER_TASK_COUNT));
123 
124 	nrfx_gppi_channels_enable(BIT(feedback_gppi_channel));
125 
126 	return NRFX_SUCCESS;
127 }
128 
feedback_relative_timer_setup(void)129 static nrfx_err_t feedback_relative_timer_setup(void)
130 {
131 	nrfx_err_t err;
132 	const nrfx_timer_config_t cfg = {
133 		.frequency = NRFX_MHZ_TO_HZ(16UL),
134 		.mode = NRF_TIMER_MODE_TIMER,
135 		.bit_width = NRF_TIMER_BIT_WIDTH_32,
136 		.interrupt_priority = NRFX_TIMER_DEFAULT_CONFIG_IRQ_PRIORITY,
137 		.p_context = NULL,
138 	};
139 
140 	err = nrfx_timer_init(&feedback_timer_instance, &cfg, NULL);
141 	if (err != NRFX_SUCCESS) {
142 		LOG_ERR("nrfx timer init error (relative timer) - Return value: %d", err);
143 	}
144 
145 	return err;
146 }
147 
feedback_init(void)148 struct feedback_ctx *feedback_init(void)
149 {
150 	nrfx_err_t err;
151 	uint8_t usbd_sof_gppi_channel;
152 	uint8_t i2s_framestart_gppi_channel;
153 
154 	feedback_reset_ctx(&fb_ctx);
155 
156 	if (IS_ENABLED(CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER)) {
157 		err = feedback_edge_counter_setup();
158 	} else {
159 		err = feedback_relative_timer_setup();
160 	}
161 
162 	if (err != NRFX_SUCCESS) {
163 		return &fb_ctx;
164 	}
165 
166 	/* Subscribe TIMER CAPTURE task to USBD SOF event */
167 	err = nrfx_gppi_channel_alloc(&usbd_sof_gppi_channel);
168 	if (err != NRFX_SUCCESS) {
169 		LOG_ERR("gppi_channel_alloc failed with: %d\n", err);
170 		return &fb_ctx;
171 	}
172 
173 	nrfx_gppi_channel_endpoints_setup(usbd_sof_gppi_channel,
174 		nrf_usbd_event_address_get(NRF_USBD, NRF_USBD_EVENT_SOF),
175 		nrfx_timer_capture_task_address_get(&feedback_timer_instance,
176 			FEEDBACK_TIMER_USBD_SOF_CAPTURE));
177 	nrfx_gppi_fork_endpoint_setup(usbd_sof_gppi_channel,
178 		nrfx_timer_task_address_get(&feedback_timer_instance,
179 			NRF_TIMER_TASK_CLEAR));
180 
181 	nrfx_gppi_channels_enable(BIT(usbd_sof_gppi_channel));
182 
183 	/* Subscribe TIMER CAPTURE task to I2S FRAMESTART event */
184 	err = nrfx_gppi_channel_alloc(&i2s_framestart_gppi_channel);
185 	if (err != NRFX_SUCCESS) {
186 		LOG_ERR("gppi_channel_alloc failed with: %d\n", err);
187 		return &fb_ctx;
188 	}
189 
190 	nrfx_gppi_channel_endpoints_setup(i2s_framestart_gppi_channel,
191 		nrf_i2s_event_address_get(NRF_I2S0, NRF_I2S_EVENT_FRAMESTART),
192 		nrfx_timer_capture_task_address_get(&feedback_timer_instance,
193 			FEEDBACK_TIMER_I2S_FRAMESTART_CAPTURE));
194 
195 	nrfx_gppi_channels_enable(BIT(i2s_framestart_gppi_channel));
196 
197 	/* Enable feedback timer */
198 	nrfx_timer_enable(&feedback_timer_instance);
199 
200 	return &fb_ctx;
201 }
202 
update_sof_offset(struct feedback_ctx * ctx,uint32_t sof_cc,uint32_t framestart_cc)203 static void update_sof_offset(struct feedback_ctx *ctx, uint32_t sof_cc,
204 			      uint32_t framestart_cc)
205 {
206 	int sof_offset;
207 
208 	if (!IS_ENABLED(CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER)) {
209 		uint32_t clks_per_edge;
210 
211 		/* Convert timer clock (independent from both Audio clock and
212 		 * USB host SOF clock) to fake sample clock shifted by P values.
213 		 * This works fine because the regulator cares only about error
214 		 * (SOF offset is both error and regulator input) and achieves
215 		 * its goal by adjusting feedback value. SOF offset is around 0
216 		 * when regulated and therefore the relative clock frequency
217 		 * discrepancies are essentially negligible.
218 		 */
219 		clks_per_edge = sof_cc / (SAMPLES_PER_SOF << FEEDBACK_P);
220 		sof_cc /= MAX(clks_per_edge, 1);
221 		framestart_cc /= MAX(clks_per_edge, 1);
222 	}
223 
224 	/* /2 because we treat the middle as a turning point from being
225 	 * "too late" to "too early".
226 	 */
227 	if (framestart_cc > (SAMPLES_PER_SOF << FEEDBACK_P)/2) {
228 		sof_offset = framestart_cc - (SAMPLES_PER_SOF << FEEDBACK_P);
229 	} else {
230 		sof_offset = framestart_cc;
231 	}
232 
233 	/* The heuristic above is not enough when the offset gets too large.
234 	 * If the sign of the simple heuristic changes, check whether the offset
235 	 * crossed through the zero or the outer bound.
236 	 */
237 	if ((ctx->rel_sof_offset >= 0) != (sof_offset >= 0)) {
238 		uint32_t abs_diff;
239 		int32_t base_change;
240 
241 		if (sof_offset >= 0) {
242 			abs_diff = sof_offset - ctx->rel_sof_offset;
243 			base_change = -(SAMPLES_PER_SOF << FEEDBACK_P);
244 		} else {
245 			abs_diff = ctx->rel_sof_offset - sof_offset;
246 			base_change = SAMPLES_PER_SOF << FEEDBACK_P;
247 		}
248 
249 		/* Adjust base offset only if the change happened through the
250 		 * outer bound. The actual changes should be significantly lower
251 		 * than the threshold here.
252 		 */
253 		if (abs_diff > (SAMPLES_PER_SOF << FEEDBACK_P)/2) {
254 			ctx->base_sof_offset += base_change;
255 		}
256 	}
257 
258 	ctx->rel_sof_offset = sof_offset;
259 }
260 
offset_to_correction(int32_t offset)261 static inline int32_t offset_to_correction(int32_t offset)
262 {
263 	return -(offset / BIT(FEEDBACK_P)) * BIT(FEEDBACK_FS_SHIFT);
264 }
265 
pi_update(struct feedback_ctx * ctx)266 static int32_t pi_update(struct feedback_ctx *ctx)
267 {
268 	int32_t sof_offset = ctx->rel_sof_offset + ctx->base_sof_offset;
269 	/* SOF offset is measured in pow(2, -FEEDBACK_P) samples, i.e. when
270 	 * FEEDBACK_P is 0, offset is in samples, and for 1 -> half-samples,
271 	 * 2 -> quarter-samples, 3 -> eightth-samples and so on.
272 	 * In order to simplify the PI controller description here, normalize
273 	 * the offset to 1/1024 samples (alternatively it can be treated as
274 	 * samples in Q10 fixed point format) and use it as Process Variable.
275 	 */
276 	int32_t PV = BIT(10 - FEEDBACK_P) * sof_offset;
277 	/* The control goal is to keep I2S FRAMESTART as close as possible to
278 	 * USB SOF and therefore Set Point is 0.
279 	 */
280 	int32_t SP = 0;
281 	int32_t error = SP - PV;
282 
283 	/*
284 	 * With above normalization at Full-Speed, when data received during
285 	 * SOF n appears on I2S during SOF n+3, the Ziegler Nichols Ultimate
286 	 * Gain is around 1.15 and the oscillation period is around 90 SOF.
287 	 * (much nicer oscillations with 204.8 SOF period can be observed with
288 	 * gain 0.5 when the delay is not n+3, but n+33 - surprisingly the
289 	 * resulting PI coefficients after power of two rounding are the same).
290 	 *
291 	 * Ziegler-Nichols rule with applied stability margin of 2 results in:
292 	 *   Kc = 0.22 * Ku = 0.22 * 1.15 = 0.253
293 	 *   Ti = 0.83 * tu = 0.83 * 80 = 66.4
294 	 *
295 	 * Converting the rules above to parallel PI gives:
296 	 *   Kp = Kc = 0.253
297 	 *   Ki = Kc/Ti = 0.254/66.4 ~= 0.0038253
298 	 *
299 	 * Because we want fixed-point optimized non-tunable implementation,
300 	 * the parameters can be conveniently expressed with power of two:
301 	 *   Kp ~= pow(2, -2) = 0.25    (divide by 4)
302 	 *   Ki ~= pow(2, -8) = 0.0039  (divide by 256)
303 	 *
304 	 * This can be implemented as:
305 	 *   ctx->integrator += error;
306 	 *   return (error + (ctx->integrator / 64)) / 4;
307 	 * but unfortunately such regulator is pretty aggressive and keeps
308 	 * oscillating rather quickly around the setpoint (within +-1 sample).
309 	 *
310 	 * Manually tweaking the constants so the regulator output is shifted
311 	 * down by 4 bits (i.e. change /64 to /2048 and /4 to /128) yields
312 	 * really good results (the outcome is similar, even slightly better,
313 	 * than using I2S LRCLK edge counting directly).
314 	 */
315 	ctx->integrator += error;
316 	return (error + (ctx->integrator / 2048)) / 128;
317 }
318 
feedback_process(struct feedback_ctx * ctx)319 void feedback_process(struct feedback_ctx *ctx)
320 {
321 	uint32_t sof_cc;
322 	uint32_t framestart_cc;
323 	uint32_t fb;
324 
325 	sof_cc = nrfx_timer_capture_get(&feedback_timer_instance,
326 		FEEDBACK_TIMER_USBD_SOF_CAPTURE);
327 	framestart_cc = nrfx_timer_capture_get(&feedback_timer_instance,
328 		FEEDBACK_TIMER_I2S_FRAMESTART_CAPTURE);
329 
330 	update_sof_offset(ctx, sof_cc, framestart_cc);
331 
332 	if (IS_ENABLED(CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER)) {
333 		int32_t offset = ctx->rel_sof_offset + ctx->base_sof_offset;
334 
335 		ctx->fb_counter += sof_cc;
336 		ctx->fb_periods++;
337 
338 		if (ctx->fb_periods == BIT(FEEDBACK_K - FEEDBACK_P)) {
339 
340 			/* fb_counter holds Q10.10 value, left-justify it */
341 			fb = ctx->fb_counter << FEEDBACK_FS_SHIFT;
342 
343 			/* Align I2S FRAMESTART to USB SOF by adjusting reported
344 			 * feedback value. This is endpoint specific correction
345 			 * mentioned but not specified in USB 2.0 Specification.
346 			 */
347 			if (abs(offset) > BIT(FEEDBACK_P)) {
348 				fb += offset_to_correction(offset);
349 			}
350 
351 			ctx->fb_value = fb;
352 			ctx->fb_counter = 0;
353 			ctx->fb_periods = 0;
354 		}
355 	} else {
356 		/* Use PI controller to generate required feedback deviation
357 		 * from nominal feedback value.
358 		 */
359 		fb = SAMPLES_PER_SOF << (FEEDBACK_K + FEEDBACK_FS_SHIFT);
360 		/* Clear the additional LSB bits in feedback value, i.e. do not
361 		 * use the optional extra resolution.
362 		 */
363 		fb += pi_update(ctx) & ~0xF;
364 		ctx->fb_value = fb;
365 	}
366 }
367 
feedback_reset_ctx(struct feedback_ctx * ctx)368 void feedback_reset_ctx(struct feedback_ctx *ctx)
369 {
370 	/* Reset feedback to nominal value */
371 	ctx->fb_value = SAMPLES_PER_SOF << (FEEDBACK_K + FEEDBACK_FS_SHIFT);
372 	if (IS_ENABLED(CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER)) {
373 		ctx->fb_counter = 0;
374 		ctx->fb_periods = 0;
375 	} else {
376 		ctx->integrator = 0;
377 	}
378 }
379 
feedback_start(struct feedback_ctx * ctx,int i2s_blocks_queued)380 void feedback_start(struct feedback_ctx *ctx, int i2s_blocks_queued)
381 {
382 	/* I2S data was supposed to go out at SOF, but it is inevitably
383 	 * delayed due to triggering I2S start by software. Set relative
384 	 * SOF offset value in a way that ensures that values past "half
385 	 * frame" are treated as "too late" instead of "too early"
386 	 */
387 	ctx->rel_sof_offset = (SAMPLES_PER_SOF << FEEDBACK_P) / 2;
388 	/* If there are more than 2 I2S blocks queued, use feedback regulator
389 	 * to correct the situation.
390 	 */
391 	ctx->base_sof_offset = (i2s_blocks_queued - 2) *
392 		(SAMPLES_PER_SOF << FEEDBACK_P);
393 }
394 
feedback_value(struct feedback_ctx * ctx)395 uint32_t feedback_value(struct feedback_ctx *ctx)
396 {
397 	return ctx->fb_value;
398 }
399