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_timer.h>
13 #include <hal/nrf_usbd.h>
14 #include <hal/nrf_i2s.h>
15 #include <helpers/nrfx_gppi.h>
16 
17 LOG_MODULE_REGISTER(feedback, LOG_LEVEL_INF);
18 
19 #define FEEDBACK_TIMER_INSTANCE_NUMBER 2
20 #define FEEDBACK_TIMER_USBD_SOF_CAPTURE 0
21 #define FEEDBACK_TIMER_I2S_FRAMESTART_CAPTURE 1
22 
23 static const nrfx_timer_t feedback_timer_instance =
24 	NRFX_TIMER_INSTANCE(FEEDBACK_TIMER_INSTANCE_NUMBER);
25 
26 /* While it might be possible to determine I2S FRAMESTART to USB SOF offset
27  * entirely in software, the I2S API lacks appropriate timestamping. Therefore
28  * this sample uses target-specific code to perform the measurements. Note that
29  * the use of dedicated target-specific peripheral essentially eliminates
30  * software scheduling jitter and it is likely that a pure software only
31  * solution would require additional filtering in indirect offset measurements.
32  *
33  * Use timer clock (independent from both Audio clock and USB host SOF clock)
34  * values directly to determine samples offset. This works fine because the
35  * regulator cares only about error (SOF offset is both error and regulator
36  * input) and achieves its goal by sending nominal + 1 or nominal - 1 samples.
37  * SOF offset is around 0 when regulated and therefore the relative clock
38  * frequency discrepancies are essentially negligible.
39  */
40 #define CLKS_PER_SAMPLE	(16000000 / (SAMPLES_PER_SOF * 1000))
41 
42 static struct feedback_ctx {
43 	int32_t rel_sof_offset;
44 	int32_t base_sof_offset;
45 } fb_ctx;
46 
feedback_init(void)47 struct feedback_ctx *feedback_init(void)
48 {
49 	nrfx_err_t err;
50 	uint8_t usbd_sof_gppi_channel;
51 	uint8_t i2s_framestart_gppi_channel;
52 	const nrfx_timer_config_t cfg = {
53 		.frequency = NRFX_MHZ_TO_HZ(16UL),
54 		.mode = NRF_TIMER_MODE_TIMER,
55 		.bit_width = NRF_TIMER_BIT_WIDTH_32,
56 		.interrupt_priority = NRFX_TIMER_DEFAULT_CONFIG_IRQ_PRIORITY,
57 		.p_context = NULL,
58 	};
59 
60 	feedback_reset_ctx(&fb_ctx);
61 
62 	err = nrfx_timer_init(&feedback_timer_instance, &cfg, NULL);
63 	if (err != NRFX_SUCCESS) {
64 		LOG_ERR("nrfx timer init error - Return value: %d", err);
65 		return &fb_ctx;
66 	}
67 
68 	/* Subscribe TIMER CAPTURE task to USBD SOF event */
69 	err = nrfx_gppi_channel_alloc(&usbd_sof_gppi_channel);
70 	if (err != NRFX_SUCCESS) {
71 		LOG_ERR("gppi_channel_alloc failed with: %d\n", err);
72 		return &fb_ctx;
73 	}
74 
75 	nrfx_gppi_channel_endpoints_setup(usbd_sof_gppi_channel,
76 		nrf_usbd_event_address_get(NRF_USBD, NRF_USBD_EVENT_SOF),
77 		nrfx_timer_capture_task_address_get(&feedback_timer_instance,
78 			FEEDBACK_TIMER_USBD_SOF_CAPTURE));
79 	nrfx_gppi_fork_endpoint_setup(usbd_sof_gppi_channel,
80 		nrfx_timer_task_address_get(&feedback_timer_instance,
81 			NRF_TIMER_TASK_CLEAR));
82 
83 	nrfx_gppi_channels_enable(BIT(usbd_sof_gppi_channel));
84 
85 	/* Subscribe TIMER CAPTURE task to I2S FRAMESTART event */
86 	err = nrfx_gppi_channel_alloc(&i2s_framestart_gppi_channel);
87 	if (err != NRFX_SUCCESS) {
88 		LOG_ERR("gppi_channel_alloc failed with: %d\n", err);
89 		return &fb_ctx;
90 	}
91 
92 	nrfx_gppi_channel_endpoints_setup(i2s_framestart_gppi_channel,
93 		nrf_i2s_event_address_get(NRF_I2S0, NRF_I2S_EVENT_FRAMESTART),
94 		nrfx_timer_capture_task_address_get(&feedback_timer_instance,
95 			FEEDBACK_TIMER_I2S_FRAMESTART_CAPTURE));
96 
97 	nrfx_gppi_channels_enable(BIT(i2s_framestart_gppi_channel));
98 
99 	/* Enable feedback timer */
100 	nrfx_timer_enable(&feedback_timer_instance);
101 
102 	return &fb_ctx;
103 }
104 
update_sof_offset(struct feedback_ctx * ctx,uint32_t sof_cc,uint32_t framestart_cc)105 static void update_sof_offset(struct feedback_ctx *ctx, uint32_t sof_cc,
106 			      uint32_t framestart_cc)
107 {
108 	int sof_offset;
109 
110 	/* /2 because we treat the middle as a turning point from being
111 	 * "too late" to "too early".
112 	 */
113 	if (framestart_cc > (SAMPLES_PER_SOF * CLKS_PER_SAMPLE)/2) {
114 		sof_offset = framestart_cc - SAMPLES_PER_SOF * CLKS_PER_SAMPLE;
115 	} else {
116 		sof_offset = framestart_cc;
117 	}
118 
119 	/* The heuristic above is not enough when the offset gets too large.
120 	 * If the sign of the simple heuristic changes, check whether the offset
121 	 * crossed through the zero or the outer bound.
122 	 */
123 	if ((ctx->rel_sof_offset >= 0) != (sof_offset >= 0)) {
124 		uint32_t abs_diff;
125 		int32_t base_change;
126 
127 		if (sof_offset >= 0) {
128 			abs_diff = sof_offset - ctx->rel_sof_offset;
129 			base_change = -(SAMPLES_PER_SOF * CLKS_PER_SAMPLE);
130 		} else {
131 			abs_diff = ctx->rel_sof_offset - sof_offset;
132 			base_change = SAMPLES_PER_SOF * CLKS_PER_SAMPLE;
133 		}
134 
135 		/* Adjust base offset only if the change happened through the
136 		 * outer bound. The actual changes should be significantly lower
137 		 * than the threshold here.
138 		 */
139 		if (abs_diff > (SAMPLES_PER_SOF * CLKS_PER_SAMPLE)/2) {
140 			ctx->base_sof_offset += base_change;
141 		}
142 	}
143 
144 	ctx->rel_sof_offset = sof_offset;
145 }
146 
feedback_process(struct feedback_ctx * ctx)147 void feedback_process(struct feedback_ctx *ctx)
148 {
149 	uint32_t sof_cc;
150 	uint32_t framestart_cc;
151 
152 	sof_cc = nrfx_timer_capture_get(&feedback_timer_instance,
153 		FEEDBACK_TIMER_USBD_SOF_CAPTURE);
154 	framestart_cc = nrfx_timer_capture_get(&feedback_timer_instance,
155 		FEEDBACK_TIMER_I2S_FRAMESTART_CAPTURE);
156 
157 	update_sof_offset(ctx, sof_cc, framestart_cc);
158 }
159 
feedback_reset_ctx(struct feedback_ctx * ctx)160 void feedback_reset_ctx(struct feedback_ctx *ctx)
161 {
162 	ARG_UNUSED(ctx);
163 }
164 
feedback_start(struct feedback_ctx * ctx,int i2s_blocks_queued)165 void feedback_start(struct feedback_ctx *ctx, int i2s_blocks_queued)
166 {
167 	/* I2S data was supposed to go out at SOF, but it is inevitably
168 	 * delayed due to triggering I2S start by software. Set relative
169 	 * SOF offset value in a way that ensures that values past "half
170 	 * frame" are treated as "too late" instead of "too early"
171 	 */
172 	ctx->rel_sof_offset = (SAMPLES_PER_SOF * CLKS_PER_SAMPLE) / 2;
173 	/* If there are more than 2 I2S TX blocks queued, use feedback regulator
174 	 * to correct the situation.
175 	 */
176 	ctx->base_sof_offset = (i2s_blocks_queued - 2) *
177 		(SAMPLES_PER_SOF * CLKS_PER_SAMPLE);
178 }
179 
feedback_samples_offset(struct feedback_ctx * ctx)180 int feedback_samples_offset(struct feedback_ctx *ctx)
181 {
182 	int32_t offset = ctx->rel_sof_offset + ctx->base_sof_offset;
183 
184 	return offset / CLKS_PER_SAMPLE;
185 }
186