|
| 1 | +/* |
| 2 | + * Copyright (c) 2024 Espressif Systems (Shanghai) Co., Ltd. |
| 3 | + * |
| 4 | + * SPDX-License-Identifier: Apache-2.0 |
| 5 | + */ |
| 6 | + |
| 7 | +/** |
| 8 | + * @file |
| 9 | + * @brief Generate PWM signals in different configurations and use a GPIO |
| 10 | + * input pin to check the programmed timing. This test uses the systimer as |
| 11 | + * benchmark, so it assumes the system tick is verified and precise. |
| 12 | + */ |
| 13 | + |
| 14 | +#include <zephyr/device.h> |
| 15 | +#include <zephyr/devicetree.h> |
| 16 | +#include <zephyr/drivers/pwm.h> |
| 17 | +#include <zephyr/kernel.h> |
| 18 | +#include <zephyr/ztest.h> |
| 19 | +#include <zephyr/drivers/gpio.h> |
| 20 | +#include <stdlib.h> |
| 21 | +#include <math.h> |
| 22 | + |
| 23 | +static struct gpio_callback gpio_cb; |
| 24 | + |
| 25 | +#define PWM_COUNT DT_PROP_LEN(DT_PATH(zephyr_user), pwms) |
| 26 | +#define PWM_CONFIG_ENTRY(idx, node_id) PWM_DT_SPEC_GET_BY_IDX(node_id, idx) |
| 27 | +#define PWM_CONFIG_ARRAY(node_id) \ |
| 28 | + { \ |
| 29 | + LISTIFY(PWM_COUNT, PWM_CONFIG_ENTRY, (,), node_id) \ |
| 30 | + } |
| 31 | + |
| 32 | +#define GPIO_COUNT DT_PROP_LEN(DT_PATH(zephyr_user), gpios) |
| 33 | +#define GPIO_CONFIG_ENTRY(idx, node_id) GPIO_DT_SPEC_GET_BY_IDX(node_id, gpios, idx) |
| 34 | +#define GPIO_CONFIG_ARRAY(node_id) \ |
| 35 | + { \ |
| 36 | + LISTIFY(GPIO_COUNT, GPIO_CONFIG_ENTRY, (,), node_id) \ |
| 37 | + } |
| 38 | + |
| 39 | +static const struct pwm_dt_spec pwms_dt[] = PWM_CONFIG_ARRAY(DT_PATH(zephyr_user)); |
| 40 | +static const struct gpio_dt_spec gpios_dt[] = GPIO_CONFIG_ARRAY(DT_PATH(zephyr_user)); |
| 41 | + |
| 42 | +static struct test_context { |
| 43 | + uint32_t last_edge_time; |
| 44 | + uint32_t high_time; |
| 45 | + uint32_t low_time; |
| 46 | + bool sampling_done; |
| 47 | + uint8_t skip_cnt; |
| 48 | +} ctx; |
| 49 | + |
| 50 | +static void gpio_edge_isr(const struct device *dev, struct gpio_callback *cb, uint32_t pins) |
| 51 | +{ |
| 52 | + int pin_state; |
| 53 | + uint32_t current_time = k_cycle_get_32(); |
| 54 | + |
| 55 | + if (ctx.sampling_done || ++ctx.skip_cnt < CONFIG_SKIP_EDGE_NUM) { |
| 56 | + return; |
| 57 | + } |
| 58 | + |
| 59 | + if (!ctx.last_edge_time) { |
| 60 | + /* init last_edge_time for first delta */ |
| 61 | + ctx.last_edge_time = current_time; |
| 62 | + return; |
| 63 | + } |
| 64 | + |
| 65 | + uint32_t elapsed_time = current_time - ctx.last_edge_time; |
| 66 | + |
| 67 | + int pin = __builtin_ffs(pins) - 1; |
| 68 | + |
| 69 | + if (pin >= 0) { |
| 70 | + pin_state = gpio_pin_get(dev, pin); |
| 71 | + } else { |
| 72 | + return; |
| 73 | + } |
| 74 | + |
| 75 | + if (pin_state) { |
| 76 | + ctx.low_time = elapsed_time; |
| 77 | + } else { |
| 78 | + ctx.high_time = elapsed_time; |
| 79 | + } |
| 80 | + |
| 81 | + /* sampling is done when both high and low times were stored */ |
| 82 | + if (ctx.high_time && ctx.low_time) { |
| 83 | + ctx.sampling_done = true; |
| 84 | + } |
| 85 | + |
| 86 | + ctx.last_edge_time = current_time; |
| 87 | +} |
| 88 | + |
| 89 | +static void setup_edge_detect(void) |
| 90 | +{ |
| 91 | + ctx.last_edge_time = 0; |
| 92 | + ctx.high_time = 0; |
| 93 | + ctx.low_time = 0; |
| 94 | + ctx.sampling_done = false; |
| 95 | + ctx.skip_cnt = 0; |
| 96 | +} |
| 97 | + |
| 98 | +static void config_gpio(const struct gpio_dt_spec *gpio_dt) |
| 99 | +{ |
| 100 | + /* Configure GPIO pin for edge detection */ |
| 101 | + gpio_pin_configure_dt(gpio_dt, GPIO_INPUT); |
| 102 | + |
| 103 | + gpio_cb.pin_mask = BIT(gpio_dt->pin); |
| 104 | + |
| 105 | + gpio_init_callback(&gpio_cb, gpio_edge_isr, gpio_cb.pin_mask); |
| 106 | + gpio_add_callback(gpio_dt->port, &gpio_cb); |
| 107 | + gpio_pin_interrupt_configure(gpio_dt->port, gpio_dt->pin, GPIO_INT_EDGE_BOTH); |
| 108 | +} |
| 109 | + |
| 110 | +static void unconfig_gpio(const struct gpio_dt_spec *gpio_dt) |
| 111 | +{ |
| 112 | + /* Disable interrupt for already tested channel */ |
| 113 | + gpio_pin_interrupt_configure(gpio_dt->port, gpio_dt->pin, GPIO_INT_DISABLE); |
| 114 | + |
| 115 | + gpio_cb.pin_mask &= ~BIT(gpio_dt->pin); |
| 116 | +} |
| 117 | + |
| 118 | +static bool check_range(float refval, float measval) |
| 119 | +{ |
| 120 | + float delta = fabsf(refval - measval); |
| 121 | + float allowed_deviation = (refval * (float)CONFIG_ALLOWED_DEVIATION) / 100; |
| 122 | + |
| 123 | + return delta <= allowed_deviation; |
| 124 | +} |
| 125 | + |
| 126 | +static int check_timing(const struct pwm_dt_spec *pwm_dt, const struct gpio_dt_spec *gpio_dt, |
| 127 | + uint8_t duty) |
| 128 | +{ |
| 129 | + uint64_t cycles_s_sys, cycles_s_pwm; |
| 130 | + int pin_state; |
| 131 | + bool inverted = (pwm_dt->flags & PWM_POLARITY_INVERTED) ? true : false; |
| 132 | + |
| 133 | + /* reset parameters for edge detection */ |
| 134 | + setup_edge_detect(); |
| 135 | + |
| 136 | + /* wait for sampling */ |
| 137 | + k_sleep(K_MSEC(CONFIG_SAMPLING_TIME)); |
| 138 | + |
| 139 | + /* store pin state for duty == 100% or 0% checks */ |
| 140 | + pin_state = gpio_pin_get_dt(gpio_dt); |
| 141 | + |
| 142 | + if (inverted) { |
| 143 | + pin_state = !pin_state; |
| 144 | + } |
| 145 | + |
| 146 | + cycles_s_sys = (uint64_t)sys_clock_hw_cycles_per_sec(); |
| 147 | + pwm_get_cycles_per_sec(pwm_dt->dev, pwm_dt->channel, &cycles_s_pwm); |
| 148 | + |
| 149 | + /* sampling_done should be false for 0 and 100% duty (no switching) */ |
| 150 | + TC_PRINT("Sampling done: %s\n", ctx.sampling_done ? "true" : "false"); |
| 151 | + |
| 152 | + if (duty == 100) { |
| 153 | + if ((pin_state == 1) && !ctx.sampling_done) { |
| 154 | + return TC_PASS; |
| 155 | + } else { |
| 156 | + return TC_FAIL; |
| 157 | + } |
| 158 | + } else if (duty == 0) { |
| 159 | + if ((pin_state == 0) && !ctx.sampling_done) { |
| 160 | + return TC_PASS; |
| 161 | + } else { |
| 162 | + return TC_FAIL; |
| 163 | + } |
| 164 | + } else { |
| 165 | + uint32_t measured_period = ctx.high_time + ctx.low_time; |
| 166 | + uint32_t measured_period_ns = (measured_period * 1e9) / cycles_s_sys; |
| 167 | + uint32_t pulse_time = inverted ? ctx.low_time : ctx.high_time; |
| 168 | + float measured_duty = (pulse_time * 100.0f) / measured_period; |
| 169 | + uint32_t measured_duty_2p = (uint32_t)(measured_duty * 100); |
| 170 | + uint32_t period_deviation_2p = |
| 171 | + (uint64_t)10000 * abs(measured_period_ns - pwm_dt->period) / pwm_dt->period; |
| 172 | + uint32_t duty_deviation_2p = |
| 173 | + (uint32_t)10000 * fabs(measured_duty - (float)duty) / duty; |
| 174 | + |
| 175 | + TC_PRINT("Measured period: %u cycles, high: %u, low: %u [unit: systimer ticks]\n", |
| 176 | + measured_period, ctx.high_time, ctx.low_time); |
| 177 | + TC_PRINT("Measured period: %u ns, deviation: %d.%d%%\n", measured_period_ns, |
| 178 | + period_deviation_2p / 100, period_deviation_2p % 100); |
| 179 | + TC_PRINT("Measured duty: %d.%d%%, deviation: %d.%d%%\n", measured_duty_2p / 100, |
| 180 | + measured_duty_2p % 100, duty_deviation_2p / 100, duty_deviation_2p % 100); |
| 181 | + |
| 182 | + /* Compare measured values with expected ones */ |
| 183 | + if (check_range(measured_period_ns, pwm_dt->period) && |
| 184 | + check_range(measured_duty, duty)) { |
| 185 | + TC_PRINT("PWM output matches the programmed values\n"); |
| 186 | + return TC_PASS; |
| 187 | + } |
| 188 | + |
| 189 | + TC_PRINT("PWM output does NOT match the programmed values\n"); |
| 190 | + return TC_FAIL; |
| 191 | + } |
| 192 | +} |
| 193 | + |
| 194 | +static void test_run(const struct pwm_dt_spec *pwm_dt, const struct gpio_dt_spec *gpio_dt, |
| 195 | + uint8_t duty, bool set_channel) |
| 196 | +{ |
| 197 | + int result; |
| 198 | + uint32_t pulse = (uint32_t)((pwm_dt->period * duty) / 100); |
| 199 | + bool inverted = (pwm_dt->flags & PWM_POLARITY_INVERTED) ? true : false; |
| 200 | + |
| 201 | + TC_PRINT("Test case: [Channel: %" PRIu32 "] [Period: %" PRIu32 "] [Pulse: %" PRIu32 |
| 202 | + "] [Inverted: %s]\n", |
| 203 | + pwm_dt->channel, pwm_dt->period, pulse, inverted ? "Yes" : "No"); |
| 204 | + |
| 205 | + if (set_channel) { |
| 206 | + result = pwm_set_dt(pwm_dt, pwm_dt->period, pulse); |
| 207 | + zassert_false(result, "Failed on pwm_set() call"); |
| 208 | + } |
| 209 | + |
| 210 | + config_gpio(gpio_dt); |
| 211 | + |
| 212 | + result = check_timing(pwm_dt, gpio_dt, duty); |
| 213 | + |
| 214 | + unconfig_gpio(gpio_dt); |
| 215 | + |
| 216 | + zassert_equal(result, TC_PASS, "Test case failed"); |
| 217 | +} |
| 218 | + |
| 219 | +ZTEST(pwm_gpio_loopback, test_pwm) |
| 220 | +{ |
| 221 | + for (int i = 0; i < PWM_COUNT; i++) { |
| 222 | + zassert_true(device_is_ready(pwms_dt[i].dev), "PWM device is not ready"); |
| 223 | + zassert_true(device_is_ready(gpios_dt[i].port), "GPIO device is not ready"); |
| 224 | + |
| 225 | + /* Test case: [Duty: 25%] */ |
| 226 | + test_run(&pwms_dt[i], &gpios_dt[i], 25, true); |
| 227 | + |
| 228 | + /* Test case: [Duty: 100%] */ |
| 229 | + test_run(&pwms_dt[i], &gpios_dt[i], 100, true); |
| 230 | + |
| 231 | + /* Test case: [Duty: 0%] */ |
| 232 | + test_run(&pwms_dt[i], &gpios_dt[i], 0, true); |
| 233 | + |
| 234 | + /* Test case: [Duty: 80%] */ |
| 235 | + test_run(&pwms_dt[i], &gpios_dt[i], 80, true); |
| 236 | + } |
| 237 | +} |
| 238 | + |
| 239 | +ZTEST(pwm_gpio_loopback, test_pwm_cross) |
| 240 | +{ |
| 241 | + for (int i = 0; i < PWM_COUNT; i++) { |
| 242 | + /* Test case: [Duty: 40%] */ |
| 243 | + test_run(&pwms_dt[i], &gpios_dt[i], 40, true); |
| 244 | + } |
| 245 | + |
| 246 | + /* Set all channels and check if they retain the original |
| 247 | + * configuration without calling pwm_set again |
| 248 | + */ |
| 249 | + for (int i = 0; i < PWM_COUNT; i++) { |
| 250 | + test_run(&pwms_dt[i], &gpios_dt[i], 40, false); |
| 251 | + } |
| 252 | +} |
| 253 | + |
| 254 | +ZTEST_SUITE(pwm_gpio_loopback, NULL, NULL, NULL, NULL, NULL); |
0 commit comments