| // SPDX-License-Identifier: GPL-2.0+ |
| /* Microchip Sparx5 Switch driver |
| * |
| * Copyright (c) 2021 Microchip Technology Inc. and its subsidiaries. |
| */ |
| |
| #include <linux/module.h> |
| #include <linux/device.h> |
| |
| #include "sparx5_main_regs.h" |
| #include "sparx5_main.h" |
| |
| /* QSYS calendar information */ |
| #define SPX5_PORTS_PER_CALREG 10 /* Ports mapped in a calendar register */ |
| #define SPX5_CALBITS_PER_PORT 3 /* Bit per port in calendar register */ |
| |
| /* DSM calendar information */ |
| #define SPX5_DSM_CAL_LEN 64 |
| #define SPX5_DSM_CAL_EMPTY 0xFFFF |
| #define SPX5_DSM_CAL_MAX_DEVS_PER_TAXI 13 |
| #define SPX5_DSM_CAL_TAXIS 8 |
| #define SPX5_DSM_CAL_BW_LOSS 553 |
| |
| #define SPX5_TAXI_PORT_MAX 70 |
| |
| #define SPEED_12500 12500 |
| |
| /* Maps from taxis to port numbers */ |
| static u32 sparx5_taxi_ports[SPX5_DSM_CAL_TAXIS][SPX5_DSM_CAL_MAX_DEVS_PER_TAXI] = { |
| {57, 12, 0, 1, 2, 16, 17, 18, 19, 20, 21, 22, 23}, |
| {58, 13, 3, 4, 5, 24, 25, 26, 27, 28, 29, 30, 31}, |
| {59, 14, 6, 7, 8, 32, 33, 34, 35, 36, 37, 38, 39}, |
| {60, 15, 9, 10, 11, 40, 41, 42, 43, 44, 45, 46, 47}, |
| {61, 48, 49, 50, 99, 99, 99, 99, 99, 99, 99, 99, 99}, |
| {62, 51, 52, 53, 99, 99, 99, 99, 99, 99, 99, 99, 99}, |
| {56, 63, 54, 55, 99, 99, 99, 99, 99, 99, 99, 99, 99}, |
| {64, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99}, |
| }; |
| |
| struct sparx5_calendar_data { |
| u32 schedule[SPX5_DSM_CAL_LEN]; |
| u32 avg_dist[SPX5_DSM_CAL_MAX_DEVS_PER_TAXI]; |
| u32 taxi_ports[SPX5_DSM_CAL_MAX_DEVS_PER_TAXI]; |
| u32 taxi_speeds[SPX5_DSM_CAL_MAX_DEVS_PER_TAXI]; |
| u32 dev_slots[SPX5_DSM_CAL_MAX_DEVS_PER_TAXI]; |
| u32 new_slots[SPX5_DSM_CAL_LEN]; |
| u32 temp_sched[SPX5_DSM_CAL_LEN]; |
| u32 indices[SPX5_DSM_CAL_LEN]; |
| u32 short_list[SPX5_DSM_CAL_LEN]; |
| u32 long_list[SPX5_DSM_CAL_LEN]; |
| }; |
| |
| static u32 sparx5_target_bandwidth(struct sparx5 *sparx5) |
| { |
| switch (sparx5->target_ct) { |
| case SPX5_TARGET_CT_7546: |
| case SPX5_TARGET_CT_7546TSN: |
| return 65000; |
| case SPX5_TARGET_CT_7549: |
| case SPX5_TARGET_CT_7549TSN: |
| return 91000; |
| case SPX5_TARGET_CT_7552: |
| case SPX5_TARGET_CT_7552TSN: |
| return 129000; |
| case SPX5_TARGET_CT_7556: |
| case SPX5_TARGET_CT_7556TSN: |
| return 161000; |
| case SPX5_TARGET_CT_7558: |
| case SPX5_TARGET_CT_7558TSN: |
| return 201000; |
| default: |
| return 0; |
| } |
| } |
| |
| /* This is used in calendar configuration */ |
| enum sparx5_cal_bw { |
| SPX5_CAL_SPEED_NONE = 0, |
| SPX5_CAL_SPEED_1G = 1, |
| SPX5_CAL_SPEED_2G5 = 2, |
| SPX5_CAL_SPEED_5G = 3, |
| SPX5_CAL_SPEED_10G = 4, |
| SPX5_CAL_SPEED_25G = 5, |
| SPX5_CAL_SPEED_0G5 = 6, |
| SPX5_CAL_SPEED_12G5 = 7 |
| }; |
| |
| static u32 sparx5_clk_to_bandwidth(enum sparx5_core_clockfreq cclock) |
| { |
| switch (cclock) { |
| case SPX5_CORE_CLOCK_250MHZ: return 83000; /* 250000 / 3 */ |
| case SPX5_CORE_CLOCK_500MHZ: return 166000; /* 500000 / 3 */ |
| case SPX5_CORE_CLOCK_625MHZ: return 208000; /* 625000 / 3 */ |
| default: return 0; |
| } |
| return 0; |
| } |
| |
| static u32 sparx5_cal_speed_to_value(enum sparx5_cal_bw speed) |
| { |
| switch (speed) { |
| case SPX5_CAL_SPEED_1G: return 1000; |
| case SPX5_CAL_SPEED_2G5: return 2500; |
| case SPX5_CAL_SPEED_5G: return 5000; |
| case SPX5_CAL_SPEED_10G: return 10000; |
| case SPX5_CAL_SPEED_25G: return 25000; |
| case SPX5_CAL_SPEED_0G5: return 500; |
| case SPX5_CAL_SPEED_12G5: return 12500; |
| default: return 0; |
| } |
| } |
| |
| static u32 sparx5_bandwidth_to_calendar(u32 bw) |
| { |
| switch (bw) { |
| case SPEED_10: return SPX5_CAL_SPEED_0G5; |
| case SPEED_100: return SPX5_CAL_SPEED_0G5; |
| case SPEED_1000: return SPX5_CAL_SPEED_1G; |
| case SPEED_2500: return SPX5_CAL_SPEED_2G5; |
| case SPEED_5000: return SPX5_CAL_SPEED_5G; |
| case SPEED_10000: return SPX5_CAL_SPEED_10G; |
| case SPEED_12500: return SPX5_CAL_SPEED_12G5; |
| case SPEED_25000: return SPX5_CAL_SPEED_25G; |
| case SPEED_UNKNOWN: return SPX5_CAL_SPEED_1G; |
| default: return SPX5_CAL_SPEED_NONE; |
| } |
| } |
| |
| static enum sparx5_cal_bw sparx5_get_port_cal_speed(struct sparx5 *sparx5, |
| u32 portno) |
| { |
| struct sparx5_port *port; |
| |
| if (portno >= SPX5_PORTS) { |
| /* Internal ports */ |
| if (portno == SPX5_PORT_CPU_0 || portno == SPX5_PORT_CPU_1) { |
| /* Equals 1.25G */ |
| return SPX5_CAL_SPEED_2G5; |
| } else if (portno == SPX5_PORT_VD0) { |
| /* IPMC only idle BW */ |
| return SPX5_CAL_SPEED_NONE; |
| } else if (portno == SPX5_PORT_VD1) { |
| /* OAM only idle BW */ |
| return SPX5_CAL_SPEED_NONE; |
| } else if (portno == SPX5_PORT_VD2) { |
| /* IPinIP gets only idle BW */ |
| return SPX5_CAL_SPEED_NONE; |
| } |
| /* not in port map */ |
| return SPX5_CAL_SPEED_NONE; |
| } |
| /* Front ports - may be used */ |
| port = sparx5->ports[portno]; |
| if (!port) |
| return SPX5_CAL_SPEED_NONE; |
| return sparx5_bandwidth_to_calendar(port->conf.bandwidth); |
| } |
| |
| /* Auto configure the QSYS calendar based on port configuration */ |
| int sparx5_config_auto_calendar(struct sparx5 *sparx5) |
| { |
| u32 cal[7], value, idx, portno; |
| u32 max_core_bw; |
| u32 total_bw = 0, used_port_bw = 0; |
| int err = 0; |
| enum sparx5_cal_bw spd; |
| |
| memset(cal, 0, sizeof(cal)); |
| |
| max_core_bw = sparx5_clk_to_bandwidth(sparx5->coreclock); |
| if (max_core_bw == 0) { |
| dev_err(sparx5->dev, "Core clock not supported"); |
| return -EINVAL; |
| } |
| |
| /* Setup the calendar with the bandwidth to each port */ |
| for (portno = 0; portno < SPX5_PORTS_ALL; portno++) { |
| u64 reg, offset, this_bw; |
| |
| spd = sparx5_get_port_cal_speed(sparx5, portno); |
| if (spd == SPX5_CAL_SPEED_NONE) |
| continue; |
| |
| this_bw = sparx5_cal_speed_to_value(spd); |
| if (portno < SPX5_PORTS) |
| used_port_bw += this_bw; |
| else |
| /* Internal ports are granted half the value */ |
| this_bw = this_bw / 2; |
| total_bw += this_bw; |
| reg = portno; |
| offset = do_div(reg, SPX5_PORTS_PER_CALREG); |
| cal[reg] |= spd << (offset * SPX5_CALBITS_PER_PORT); |
| } |
| |
| if (used_port_bw > sparx5_target_bandwidth(sparx5)) { |
| dev_err(sparx5->dev, |
| "Port BW %u above target BW %u\n", |
| used_port_bw, sparx5_target_bandwidth(sparx5)); |
| return -EINVAL; |
| } |
| |
| if (total_bw > max_core_bw) { |
| dev_err(sparx5->dev, |
| "Total BW %u above switch core BW %u\n", |
| total_bw, max_core_bw); |
| return -EINVAL; |
| } |
| |
| /* Halt the calendar while changing it */ |
| spx5_rmw(QSYS_CAL_CTRL_CAL_MODE_SET(10), |
| QSYS_CAL_CTRL_CAL_MODE, |
| sparx5, QSYS_CAL_CTRL); |
| |
| /* Assign port bandwidth to auto calendar */ |
| for (idx = 0; idx < ARRAY_SIZE(cal); idx++) |
| spx5_wr(cal[idx], sparx5, QSYS_CAL_AUTO(idx)); |
| |
| /* Increase grant rate of all ports to account for |
| * core clock ppm deviations |
| */ |
| spx5_rmw(QSYS_CAL_CTRL_CAL_AUTO_GRANT_RATE_SET(671), /* 672->671 */ |
| QSYS_CAL_CTRL_CAL_AUTO_GRANT_RATE, |
| sparx5, |
| QSYS_CAL_CTRL); |
| |
| /* Grant idle usage to VD 0-2 */ |
| for (idx = 2; idx < 5; idx++) |
| spx5_wr(HSCH_OUTB_SHARE_ENA_OUTB_SHARE_ENA_SET(12), |
| sparx5, |
| HSCH_OUTB_SHARE_ENA(idx)); |
| |
| /* Enable Auto mode */ |
| spx5_rmw(QSYS_CAL_CTRL_CAL_MODE_SET(8), |
| QSYS_CAL_CTRL_CAL_MODE, |
| sparx5, QSYS_CAL_CTRL); |
| |
| /* Verify successful calendar config */ |
| value = spx5_rd(sparx5, QSYS_CAL_CTRL); |
| if (QSYS_CAL_CTRL_CAL_AUTO_ERROR_GET(value)) { |
| dev_err(sparx5->dev, "QSYS calendar error\n"); |
| err = -EINVAL; |
| } |
| return err; |
| } |
| |
| static u32 sparx5_dsm_exb_gcd(u32 a, u32 b) |
| { |
| if (b == 0) |
| return a; |
| return sparx5_dsm_exb_gcd(b, a % b); |
| } |
| |
| static u32 sparx5_dsm_cal_len(u32 *cal) |
| { |
| u32 idx = 0, len = 0; |
| |
| while (idx < SPX5_DSM_CAL_LEN) { |
| if (cal[idx] != SPX5_DSM_CAL_EMPTY) |
| len++; |
| idx++; |
| } |
| return len; |
| } |
| |
| static u32 sparx5_dsm_cp_cal(u32 *sched) |
| { |
| u32 idx = 0, tmp; |
| |
| while (idx < SPX5_DSM_CAL_LEN) { |
| if (sched[idx] != SPX5_DSM_CAL_EMPTY) { |
| tmp = sched[idx]; |
| sched[idx] = SPX5_DSM_CAL_EMPTY; |
| return tmp; |
| } |
| idx++; |
| } |
| return SPX5_DSM_CAL_EMPTY; |
| } |
| |
| static int sparx5_dsm_calendar_calc(struct sparx5 *sparx5, u32 taxi, |
| struct sparx5_calendar_data *data) |
| { |
| bool slow_mode; |
| u32 gcd, idx, sum, min, factor; |
| u32 num_of_slots, slot_spd, empty_slots; |
| u32 taxi_bw, clk_period_ps; |
| |
| clk_period_ps = sparx5_clk_period(sparx5->coreclock); |
| taxi_bw = 128 * 1000000 / clk_period_ps; |
| slow_mode = !!(clk_period_ps > 2000); |
| memcpy(data->taxi_ports, &sparx5_taxi_ports[taxi], |
| sizeof(data->taxi_ports)); |
| |
| for (idx = 0; idx < SPX5_DSM_CAL_LEN; idx++) { |
| data->new_slots[idx] = SPX5_DSM_CAL_EMPTY; |
| data->schedule[idx] = SPX5_DSM_CAL_EMPTY; |
| data->temp_sched[idx] = SPX5_DSM_CAL_EMPTY; |
| } |
| /* Default empty calendar */ |
| data->schedule[0] = SPX5_DSM_CAL_MAX_DEVS_PER_TAXI; |
| |
| /* Map ports to taxi positions */ |
| for (idx = 0; idx < SPX5_DSM_CAL_MAX_DEVS_PER_TAXI; idx++) { |
| u32 portno = data->taxi_ports[idx]; |
| |
| if (portno < SPX5_TAXI_PORT_MAX) { |
| data->taxi_speeds[idx] = sparx5_cal_speed_to_value |
| (sparx5_get_port_cal_speed(sparx5, portno)); |
| } else { |
| data->taxi_speeds[idx] = 0; |
| } |
| } |
| |
| sum = 0; |
| min = 25000; |
| for (idx = 0; idx < ARRAY_SIZE(data->taxi_speeds); idx++) { |
| u32 jdx; |
| |
| sum += data->taxi_speeds[idx]; |
| if (data->taxi_speeds[idx] && data->taxi_speeds[idx] < min) |
| min = data->taxi_speeds[idx]; |
| gcd = min; |
| for (jdx = 0; jdx < ARRAY_SIZE(data->taxi_speeds); jdx++) |
| gcd = sparx5_dsm_exb_gcd(gcd, data->taxi_speeds[jdx]); |
| } |
| if (sum == 0) /* Empty calendar */ |
| return 0; |
| /* Make room for overhead traffic */ |
| factor = 100 * 100 * 1000 / (100 * 100 - SPX5_DSM_CAL_BW_LOSS); |
| |
| if (sum * factor > (taxi_bw * 1000)) { |
| dev_err(sparx5->dev, |
| "Taxi %u, Requested BW %u above available BW %u\n", |
| taxi, sum, taxi_bw); |
| return -EINVAL; |
| } |
| for (idx = 0; idx < 4; idx++) { |
| u32 raw_spd; |
| |
| if (idx == 0) |
| raw_spd = gcd / 5; |
| else if (idx == 1) |
| raw_spd = gcd / 2; |
| else if (idx == 2) |
| raw_spd = gcd; |
| else |
| raw_spd = min; |
| slot_spd = raw_spd * factor / 1000; |
| num_of_slots = taxi_bw / slot_spd; |
| if (num_of_slots <= 64) |
| break; |
| } |
| |
| num_of_slots = num_of_slots > 64 ? 64 : num_of_slots; |
| slot_spd = taxi_bw / num_of_slots; |
| |
| sum = 0; |
| for (idx = 0; idx < ARRAY_SIZE(data->taxi_speeds); idx++) { |
| u32 spd = data->taxi_speeds[idx]; |
| u32 adjusted_speed = data->taxi_speeds[idx] * factor / 1000; |
| |
| if (adjusted_speed > 0) { |
| data->avg_dist[idx] = (128 * 1000000 * 10) / |
| (adjusted_speed * clk_period_ps); |
| } else { |
| data->avg_dist[idx] = -1; |
| } |
| data->dev_slots[idx] = ((spd * factor / slot_spd) + 999) / 1000; |
| if (spd != 25000 && (spd != 10000 || !slow_mode)) { |
| if (num_of_slots < (5 * data->dev_slots[idx])) { |
| dev_err(sparx5->dev, |
| "Taxi %u, speed %u, Low slot sep.\n", |
| taxi, spd); |
| return -EINVAL; |
| } |
| } |
| sum += data->dev_slots[idx]; |
| if (sum > num_of_slots) { |
| dev_err(sparx5->dev, |
| "Taxi %u with overhead factor %u\n", |
| taxi, factor); |
| return -EINVAL; |
| } |
| } |
| |
| empty_slots = num_of_slots - sum; |
| |
| for (idx = 0; idx < empty_slots; idx++) |
| data->schedule[idx] = SPX5_DSM_CAL_MAX_DEVS_PER_TAXI; |
| |
| for (idx = 1; idx < num_of_slots; idx++) { |
| u32 indices_len = 0; |
| u32 slot, jdx, kdx, ts; |
| s32 cnt; |
| u32 num_of_old_slots, num_of_new_slots, tgt_score; |
| |
| for (slot = 0; slot < ARRAY_SIZE(data->dev_slots); slot++) { |
| if (data->dev_slots[slot] == idx) { |
| data->indices[indices_len] = slot; |
| indices_len++; |
| } |
| } |
| if (indices_len == 0) |
| continue; |
| kdx = 0; |
| for (slot = 0; slot < idx; slot++) { |
| for (jdx = 0; jdx < indices_len; jdx++, kdx++) |
| data->new_slots[kdx] = data->indices[jdx]; |
| } |
| |
| for (slot = 0; slot < SPX5_DSM_CAL_LEN; slot++) { |
| if (data->schedule[slot] == SPX5_DSM_CAL_EMPTY) |
| break; |
| } |
| |
| num_of_old_slots = slot; |
| num_of_new_slots = kdx; |
| cnt = 0; |
| ts = 0; |
| |
| if (num_of_new_slots > num_of_old_slots) { |
| memcpy(data->short_list, data->schedule, |
| sizeof(data->short_list)); |
| memcpy(data->long_list, data->new_slots, |
| sizeof(data->long_list)); |
| tgt_score = 100000 * num_of_old_slots / |
| num_of_new_slots; |
| } else { |
| memcpy(data->short_list, data->new_slots, |
| sizeof(data->short_list)); |
| memcpy(data->long_list, data->schedule, |
| sizeof(data->long_list)); |
| tgt_score = 100000 * num_of_new_slots / |
| num_of_old_slots; |
| } |
| |
| while (sparx5_dsm_cal_len(data->short_list) > 0 || |
| sparx5_dsm_cal_len(data->long_list) > 0) { |
| u32 act = 0; |
| |
| if (sparx5_dsm_cal_len(data->short_list) > 0) { |
| data->temp_sched[ts] = |
| sparx5_dsm_cp_cal(data->short_list); |
| ts++; |
| cnt += 100000; |
| act = 1; |
| } |
| while (sparx5_dsm_cal_len(data->long_list) > 0 && |
| cnt > 0) { |
| data->temp_sched[ts] = |
| sparx5_dsm_cp_cal(data->long_list); |
| ts++; |
| cnt -= tgt_score; |
| act = 1; |
| } |
| if (act == 0) { |
| dev_err(sparx5->dev, |
| "Error in DSM calendar calculation\n"); |
| return -EINVAL; |
| } |
| } |
| |
| for (slot = 0; slot < SPX5_DSM_CAL_LEN; slot++) { |
| if (data->temp_sched[slot] == SPX5_DSM_CAL_EMPTY) |
| break; |
| } |
| for (slot = 0; slot < SPX5_DSM_CAL_LEN; slot++) { |
| data->schedule[slot] = data->temp_sched[slot]; |
| data->temp_sched[slot] = SPX5_DSM_CAL_EMPTY; |
| data->new_slots[slot] = SPX5_DSM_CAL_EMPTY; |
| } |
| } |
| return 0; |
| } |
| |
| static int sparx5_dsm_calendar_check(struct sparx5 *sparx5, |
| struct sparx5_calendar_data *data) |
| { |
| u32 num_of_slots, idx, port; |
| int cnt, max_dist; |
| u32 slot_indices[SPX5_DSM_CAL_LEN], distances[SPX5_DSM_CAL_LEN]; |
| u32 cal_length = sparx5_dsm_cal_len(data->schedule); |
| |
| for (port = 0; port < SPX5_DSM_CAL_MAX_DEVS_PER_TAXI; port++) { |
| num_of_slots = 0; |
| max_dist = data->avg_dist[port]; |
| for (idx = 0; idx < SPX5_DSM_CAL_LEN; idx++) { |
| slot_indices[idx] = SPX5_DSM_CAL_EMPTY; |
| distances[idx] = SPX5_DSM_CAL_EMPTY; |
| } |
| |
| for (idx = 0; idx < cal_length; idx++) { |
| if (data->schedule[idx] == port) { |
| slot_indices[num_of_slots] = idx; |
| num_of_slots++; |
| } |
| } |
| |
| slot_indices[num_of_slots] = slot_indices[0] + cal_length; |
| |
| for (idx = 0; idx < num_of_slots; idx++) { |
| distances[idx] = (slot_indices[idx + 1] - |
| slot_indices[idx]) * 10; |
| } |
| |
| for (idx = 0; idx < num_of_slots; idx++) { |
| u32 jdx, kdx; |
| |
| cnt = distances[idx] - max_dist; |
| if (cnt < 0) |
| cnt = -cnt; |
| kdx = 0; |
| for (jdx = (idx + 1) % num_of_slots; |
| jdx != idx; |
| jdx = (jdx + 1) % num_of_slots, kdx++) { |
| cnt = cnt + distances[jdx] - max_dist; |
| if (cnt < 0) |
| cnt = -cnt; |
| if (cnt > max_dist) |
| goto check_err; |
| } |
| } |
| } |
| return 0; |
| check_err: |
| dev_err(sparx5->dev, |
| "Port %u: distance %u above limit %d\n", |
| port, cnt, max_dist); |
| return -EINVAL; |
| } |
| |
| static int sparx5_dsm_calendar_update(struct sparx5 *sparx5, u32 taxi, |
| struct sparx5_calendar_data *data) |
| { |
| u32 idx; |
| u32 cal_len = sparx5_dsm_cal_len(data->schedule), len; |
| |
| spx5_wr(DSM_TAXI_CAL_CFG_CAL_PGM_ENA_SET(1), |
| sparx5, |
| DSM_TAXI_CAL_CFG(taxi)); |
| for (idx = 0; idx < cal_len; idx++) { |
| spx5_rmw(DSM_TAXI_CAL_CFG_CAL_IDX_SET(idx), |
| DSM_TAXI_CAL_CFG_CAL_IDX, |
| sparx5, |
| DSM_TAXI_CAL_CFG(taxi)); |
| spx5_rmw(DSM_TAXI_CAL_CFG_CAL_PGM_VAL_SET(data->schedule[idx]), |
| DSM_TAXI_CAL_CFG_CAL_PGM_VAL, |
| sparx5, |
| DSM_TAXI_CAL_CFG(taxi)); |
| } |
| spx5_wr(DSM_TAXI_CAL_CFG_CAL_PGM_ENA_SET(0), |
| sparx5, |
| DSM_TAXI_CAL_CFG(taxi)); |
| len = DSM_TAXI_CAL_CFG_CAL_CUR_LEN_GET(spx5_rd(sparx5, |
| DSM_TAXI_CAL_CFG(taxi))); |
| if (len != cal_len - 1) |
| goto update_err; |
| return 0; |
| update_err: |
| dev_err(sparx5->dev, "Incorrect calendar length: %u\n", len); |
| return -EINVAL; |
| } |
| |
| /* Configure the DSM calendar based on port configuration */ |
| int sparx5_config_dsm_calendar(struct sparx5 *sparx5) |
| { |
| int taxi; |
| struct sparx5_calendar_data *data; |
| int err = 0; |
| |
| data = kzalloc(sizeof(*data), GFP_KERNEL); |
| if (!data) |
| return -ENOMEM; |
| |
| for (taxi = 0; taxi < SPX5_DSM_CAL_TAXIS; ++taxi) { |
| err = sparx5_dsm_calendar_calc(sparx5, taxi, data); |
| if (err) { |
| dev_err(sparx5->dev, "DSM calendar calculation failed\n"); |
| goto cal_out; |
| } |
| err = sparx5_dsm_calendar_check(sparx5, data); |
| if (err) { |
| dev_err(sparx5->dev, "DSM calendar check failed\n"); |
| goto cal_out; |
| } |
| err = sparx5_dsm_calendar_update(sparx5, taxi, data); |
| if (err) { |
| dev_err(sparx5->dev, "DSM calendar update failed\n"); |
| goto cal_out; |
| } |
| } |
| cal_out: |
| kfree(data); |
| return err; |
| } |