* upgrade to IDF v5.1.1
* add new synchronization implementation, use sample stuffing / removal to keep up sync
* use big DMA buffer for I2S and improve sync
* Add DAC TAS5805M as custom board
* add wifi credential reset
o press reset button (nRESET pin) 3 times
but wait about 1s between button presses
the button press counter is reset 5s after boot
* Add support for PT8211 DAC (#78)
* upgrade ethernet interface to IDF v5 (#84)
* port official example of ethernet for IDF v5.x
* Fix cmake if guard for ethernet
Signed-off-by: Karl Osterseher <karli_o@gmx.at>
Co-authored-by: DerPicknicker <64746593+DerPicknicker@users.noreply.github.com>
Co-authored-by: whc2001 <ianwang0122@outlook.com>
460 lines
16 KiB
C
460 lines
16 KiB
C
/*
|
|
* ESPRESSIF MIT License
|
|
*
|
|
* Copyright (c) 2018 <ESPRESSIF SYSTEMS (SHANGHAI) PTE LTD>
|
|
*
|
|
* Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in
|
|
* which case, it is free of charge, to any person obtaining a copy of this
|
|
* software and associated documentation files (the "Software"), to deal in the
|
|
* Software without restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
* the Software, and to permit persons to whom the Software is furnished to do
|
|
* so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*
|
|
*/
|
|
|
|
#include "periph_is31fl3216.h"
|
|
|
|
#include <inttypes.h>
|
|
#include <string.h>
|
|
|
|
#include "IS31FL3216.h"
|
|
#include "audio_mem.h"
|
|
#include "esp_log.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/queue.h"
|
|
#include "freertos/task.h"
|
|
|
|
#define IS31FL3216_TASK_STACK_SIZE (2048 + 1024)
|
|
#define IS31FL3216_TASK_PRIORITY 3
|
|
|
|
#define ONE_FRAME_BYTE_SIZE 18
|
|
#define DEFAULT_FLASH_STEP 2
|
|
|
|
static const char *TAG = "PERIPH_IS31";
|
|
static const int DESTROY_BIT = BIT0;
|
|
|
|
#define VALIDATE_IS31FL3216(periph, ret) \
|
|
if (!(periph && esp_periph_get_id(periph) == PERIPH_ID_IS31FL3216)) { \
|
|
ESP_LOGE(TAG, "Invalid is31fl3216 periph, at line %d", __LINE__); \
|
|
return ret; \
|
|
}
|
|
|
|
static const uint8_t light_audio_frames[8][ONE_FRAME_BYTE_SIZE] = {
|
|
{0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
|
{0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
|
{0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
|
{0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0xff},
|
|
{0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
0xFF, 0x00, 0x00, 0x00, 0xff, 0xff},
|
|
{0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
0xFF, 0xFF, 0x00, 0xff, 0xff, 0xff},
|
|
{0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
0xFF, 0xFF, 0xFF, 0xff, 0xff, 0xff},
|
|
{0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xff, 0xFF, 0xff, 0xff, 0xff,
|
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
|
|
};
|
|
|
|
typedef enum {
|
|
PERIPH_IS31_CMD_CHG_STATE,
|
|
PERIPH_IS31_CMD_QUIT,
|
|
} periph_is31_cmd_t;
|
|
|
|
typedef struct {
|
|
uint16_t max_light_num; // Maximum light number
|
|
uint16_t light_num; // Working lights number
|
|
uint16_t light_mask; // Light bits mask
|
|
int interval_time; // Interval working time
|
|
uint16_t act_time; // Action times
|
|
uint8_t duty_step; // Duty step
|
|
periph_is31_shift_mode_t shift_mode; // Shift mode step
|
|
} periph_is31_arg_t;
|
|
|
|
typedef struct {
|
|
periph_is31_arg_t *arg;
|
|
uint8_t duty[IS31FL3216_CH_NUM]; // Duty of lights
|
|
is31fl3216_handle_t handle;
|
|
periph_is31fl3216_state_t cur_state;
|
|
QueueHandle_t evt;
|
|
EventGroupHandle_t g_event_bit;
|
|
} periph_is31fl3216_t;
|
|
|
|
typedef struct {
|
|
periph_is31_cmd_t type;
|
|
uint32_t data;
|
|
} periph_is31_msg_t;
|
|
|
|
static esp_err_t is31_leds_ctrl(is31fl3216_handle_t *handle, uint16_t mask) {
|
|
esp_err_t ret = ESP_OK;
|
|
for (int i = 0; i < IS31FL3216_CH_NUM; i++) {
|
|
if (mask & (1UL << i)) {
|
|
ret |= is31fl3216_ch_enable(handle, 1UL << i);
|
|
} else {
|
|
ret |= is31fl3216_ch_disable(handle, 1UL << i);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static esp_err_t is31_leds_duty(is31fl3216_handle_t *handle, int duty,
|
|
uint16_t mask) {
|
|
esp_err_t ret = ESP_OK;
|
|
for (int i = 0; i < IS31FL3216_CH_NUM; i++) {
|
|
if (mask & (1UL << i))
|
|
ret |= is31fl3216_ch_duty_set(handle, 1UL << i, duty);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static void is31_evt_send(void *que, periph_is31_cmd_t type, uint32_t data,
|
|
int dir) {
|
|
periph_is31_msg_t evt = {0};
|
|
evt.type = type;
|
|
evt.data = data;
|
|
if (dir) {
|
|
xQueueSendToFront(que, &evt, 0);
|
|
} else {
|
|
xQueueSend(que, &evt, 0);
|
|
}
|
|
}
|
|
static esp_err_t is31_change_state(periph_is31fl3216_t *is31, int state,
|
|
periph_is31_arg_t *arg) {
|
|
esp_err_t ret = ESP_OK;
|
|
switch (state) {
|
|
case IS31FL3216_STATE_OFF:
|
|
ret |= is31fl3216_ch_disable(is31->handle, arg->light_mask);
|
|
arg->interval_time = portMAX_DELAY;
|
|
is31->cur_state = IS31FL3216_STATE_OFF;
|
|
break;
|
|
case IS31FL3216_STATE_ON:
|
|
if (is31->cur_state == IS31FL3216_STATE_BY_AUDIO) {
|
|
ret |= is31fl3216_work_mode_set(is31->handle, IS31FL3216_MODE_PWM);
|
|
is31_leds_duty(is31->handle, IS31FL3216_DUTY_MAX, arg->light_mask);
|
|
}
|
|
is31_leds_ctrl(is31->handle, arg->light_mask);
|
|
arg->interval_time = portMAX_DELAY;
|
|
is31->cur_state = IS31FL3216_STATE_ON;
|
|
break;
|
|
case IS31FL3216_STATE_FLASH:
|
|
if (is31->cur_state == IS31FL3216_STATE_BY_AUDIO) {
|
|
ret |= is31fl3216_work_mode_set(is31->handle, IS31FL3216_MODE_PWM);
|
|
}
|
|
is31->cur_state = IS31FL3216_STATE_FLASH;
|
|
break;
|
|
case IS31FL3216_STATE_SHIFT:
|
|
if (is31->cur_state == IS31FL3216_STATE_BY_AUDIO) {
|
|
ret |= is31fl3216_work_mode_set(is31->handle, IS31FL3216_MODE_PWM);
|
|
}
|
|
is31->cur_state = IS31FL3216_STATE_SHIFT;
|
|
break;
|
|
case IS31FL3216_STATE_BY_AUDIO:
|
|
is31fl3216_reset(is31->handle);
|
|
is31fl3216_work_mode_set(is31->handle, IS31FL3216_MODE_FRAME);
|
|
is31fl3216_sample_rate_set(is31->handle, 0xB4); // Set adc sample rate
|
|
is31fl3216_frame_value_set(is31->handle, 1,
|
|
(uint8_t *)&light_audio_frames,
|
|
sizeof(light_audio_frames));
|
|
is31fl3216_first_frame_set(is31->handle, 0);
|
|
is31->cur_state = IS31FL3216_STATE_BY_AUDIO;
|
|
arg->interval_time = portMAX_DELAY;
|
|
break;
|
|
default:
|
|
ESP_LOGE(TAG, "State %d is not supported", state);
|
|
break;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static void is31fl3216_run_task(void *Para) {
|
|
esp_periph_handle_t periph = (esp_periph_handle_t)Para;
|
|
periph_is31fl3216_t *is31 = esp_periph_get_data(periph);
|
|
periph_is31_arg_t is31_arg = {
|
|
.max_light_num = IS31FL3216_CH_NUM,
|
|
.light_num = 1,
|
|
.light_mask = 1,
|
|
.interval_time = 1000,
|
|
.act_time = 0,
|
|
.duty_step = DEFAULT_FLASH_STEP,
|
|
.shift_mode = 0,
|
|
};
|
|
periph_is31_msg_t msg = {0};
|
|
int wait_time_ms = portMAX_DELAY;
|
|
bool task_run = true;
|
|
xEventGroupClearBits(is31->g_event_bit, DESTROY_BIT);
|
|
int cur_duty = 0;
|
|
int sig = 2;
|
|
int cur_bits_mask = 0;
|
|
int i = 0;
|
|
uint16_t act_times = 0;
|
|
while (task_run) {
|
|
if (xQueueReceive(is31->evt, &msg, (wait_time_ms / portTICK_PERIOD_MS))) {
|
|
ESP_LOGD(TAG, "cmd:%d, data:%" PRIu32, msg.type, msg.data);
|
|
switch (msg.type) {
|
|
case PERIPH_IS31_CMD_CHG_STATE:
|
|
memcpy(&is31_arg, is31->arg, sizeof(periph_is31_arg_t));
|
|
wait_time_ms = is31->arg->interval_time;
|
|
|
|
memset(is31->arg, 0, sizeof(periph_is31_arg_t));
|
|
is31->arg->interval_time = portMAX_DELAY;
|
|
is31->arg->max_light_num = IS31FL3216_CH_NUM;
|
|
is31->arg->duty_step = DEFAULT_FLASH_STEP;
|
|
is31_change_state(is31, msg.data, &is31_arg);
|
|
|
|
if (IS31FL3216_STATE_FLASH == msg.data) {
|
|
sig = is31_arg.duty_step;
|
|
}
|
|
if (is31_arg.act_time && wait_time_ms) {
|
|
act_times = is31_arg.act_time / wait_time_ms;
|
|
} else {
|
|
act_times = 0;
|
|
}
|
|
break;
|
|
|
|
case PERIPH_IS31_CMD_QUIT:
|
|
task_run = false;
|
|
if (is31->g_event_bit) {
|
|
xEventGroupSetBits(is31->g_event_bit, DESTROY_BIT);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (task_run == false) {
|
|
ESP_LOGW(TAG, "Quit is31fl3216 task ...");
|
|
break;
|
|
}
|
|
}
|
|
switch (is31->cur_state) {
|
|
case IS31FL3216_STATE_FLASH: {
|
|
is31_leds_duty(is31->handle, cur_duty, is31_arg.light_mask);
|
|
is31_leds_ctrl(is31->handle, is31_arg.light_mask);
|
|
cur_duty += sig;
|
|
if (cur_duty > IS31FL3216_DUTY_MAX) {
|
|
cur_duty = IS31FL3216_DUTY_MAX;
|
|
sig = -(is31_arg.duty_step);
|
|
}
|
|
if (cur_duty < 0) {
|
|
cur_duty = 0;
|
|
sig = (is31_arg.duty_step);
|
|
}
|
|
}
|
|
if (is31_arg.act_time == 0) {
|
|
act_times = 0;
|
|
break;
|
|
}
|
|
act_times--;
|
|
if (act_times == 0) {
|
|
wait_time_ms = portMAX_DELAY;
|
|
is31->cur_state = IS31FL3216_STATE_UNKNOWN;
|
|
is31_leds_ctrl(is31->handle, 0);
|
|
}
|
|
break;
|
|
case IS31FL3216_STATE_SHIFT:
|
|
if (is31_arg.shift_mode == PERIPH_IS31_SHIFT_MODE_SINGLE) {
|
|
cur_bits_mask = ((1UL << is31_arg.light_num) - 1) << (i++);
|
|
if (i == (is31_arg.max_light_num - is31_arg.light_num + 1)) {
|
|
i = 0;
|
|
}
|
|
} else if (is31_arg.shift_mode == PERIPH_IS31_SHIFT_MODE_ACC) {
|
|
cur_bits_mask = (1UL << (is31_arg.light_num * ((i++) + 1))) - 1;
|
|
if ((cur_bits_mask >> is31_arg.max_light_num) & 0x01) {
|
|
cur_bits_mask = 0;
|
|
i = 0;
|
|
}
|
|
}
|
|
is31_leds_duty(is31->handle, IS31FL3216_DUTY_MAX, cur_bits_mask);
|
|
is31_leds_ctrl(is31->handle, cur_bits_mask);
|
|
|
|
ESP_LOGD(TAG, "Mask:%08x, %d", cur_bits_mask, wait_time_ms);
|
|
if (is31_arg.act_time == 0) {
|
|
act_times = 0;
|
|
break;
|
|
}
|
|
act_times--;
|
|
if (act_times == 0) {
|
|
wait_time_ms = portMAX_DELAY;
|
|
is31->cur_state = IS31FL3216_STATE_UNKNOWN;
|
|
is31_leds_ctrl(is31->handle, 0);
|
|
}
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
vTaskDelete(NULL);
|
|
}
|
|
|
|
esp_err_t periph_is31fl3216_set_state(esp_periph_handle_t periph,
|
|
periph_is31fl3216_state_t state) {
|
|
periph_is31fl3216_t *is31fl3216 = esp_periph_get_data(periph);
|
|
is31_evt_send(is31fl3216->evt, PERIPH_IS31_CMD_CHG_STATE, state, 0);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t periph_is31fl3216_set_blink_pattern(esp_periph_handle_t periph,
|
|
uint16_t blink_pattern) {
|
|
periph_is31fl3216_t *is31fl3216 = esp_periph_get_data(periph);
|
|
is31fl3216->arg->light_mask = blink_pattern;
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t periph_is31fl3216_set_duty(esp_periph_handle_t periph, uint8_t index,
|
|
uint8_t value) {
|
|
periph_is31fl3216_t *is31fl3216 = esp_periph_get_data(periph);
|
|
is31fl3216->duty[index] = value;
|
|
is31fl3216_ch_duty_set(is31fl3216->handle, 1UL << index,
|
|
is31fl3216->duty[index]);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t periph_is31fl3216_set_duty_step(esp_periph_handle_t periph,
|
|
uint8_t step) {
|
|
periph_is31fl3216_t *is31fl3216 = esp_periph_get_data(periph);
|
|
is31fl3216->arg->duty_step = step;
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t periph_is31fl3216_set_interval(esp_periph_handle_t periph,
|
|
uint16_t interval_ms) {
|
|
periph_is31fl3216_t *is31fl3216 = esp_periph_get_data(periph);
|
|
is31fl3216->arg->interval_time = interval_ms;
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t periph_is31fl3216_set_shift_mode(esp_periph_handle_t periph,
|
|
periph_is31_shift_mode_t mode) {
|
|
periph_is31fl3216_t *is31fl3216 = esp_periph_get_data(periph);
|
|
is31fl3216->arg->shift_mode = mode;
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t periph_is31fl3216_set_light_on_num(esp_periph_handle_t periph,
|
|
uint16_t light_on_num,
|
|
uint16_t max_light_num) {
|
|
periph_is31fl3216_t *is31fl3216 = esp_periph_get_data(periph);
|
|
is31fl3216->arg->max_light_num = max_light_num;
|
|
is31fl3216->arg->light_num = light_on_num;
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t periph_is31fl3216_set_act_time(esp_periph_handle_t periph,
|
|
uint16_t act_ms) {
|
|
periph_is31fl3216_t *is31fl3216 = esp_periph_get_data(periph);
|
|
is31fl3216->arg->act_time = act_ms;
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t _is31fl3216_init(esp_periph_handle_t self) {
|
|
periph_is31fl3216_t *is31fl3216 = esp_periph_get_data(self);
|
|
esp_err_t ret = ESP_OK;
|
|
is31fl3216_ch_disable(is31fl3216->handle, IS31FL3216_CH_ALL);
|
|
is31_leds_duty(is31fl3216->handle, 0, IS31FL3216_CH_ALL);
|
|
xTaskCreate(is31fl3216_run_task, "is31fl3216_run_task",
|
|
IS31FL3216_TASK_STACK_SIZE, (void *)self,
|
|
IS31FL3216_TASK_PRIORITY, NULL);
|
|
if (ret) {
|
|
ESP_LOGE(TAG, "Failed to initialize is31fl3216");
|
|
return ESP_FAIL;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t _is31fl3216_destroy(esp_periph_handle_t self) {
|
|
VALIDATE_IS31FL3216(self, ESP_FAIL);
|
|
periph_is31fl3216_t *is31fl3216 = esp_periph_get_data(self);
|
|
is31_evt_send(is31fl3216->evt, PERIPH_IS31_CMD_QUIT, 0, 0);
|
|
if (is31fl3216->g_event_bit) {
|
|
xEventGroupWaitBits(is31fl3216->g_event_bit, DESTROY_BIT, pdTRUE, pdFALSE,
|
|
portMAX_DELAY);
|
|
vEventGroupDelete(is31fl3216->g_event_bit);
|
|
is31fl3216->g_event_bit = NULL;
|
|
}
|
|
esp_err_t ret = ESP_OK;
|
|
ret |= is31fl3216_ch_disable(is31fl3216->handle, IS31FL3216_CH_ALL);
|
|
ret |= is31fl3216_deinit(is31fl3216->handle);
|
|
audio_free(is31fl3216->arg);
|
|
vQueueDelete(is31fl3216->evt);
|
|
audio_free(is31fl3216);
|
|
if (ret) {
|
|
ESP_LOGE(TAG, "Error occurred when stopping the is31fl3216");
|
|
return ESP_FAIL;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_periph_handle_t periph_is31fl3216_init(
|
|
periph_is31fl3216_cfg_t *is31fl3216_config) {
|
|
esp_periph_handle_t periph =
|
|
esp_periph_create(PERIPH_ID_IS31FL3216, "periph_is31fl3216");
|
|
AUDIO_MEM_CHECK(TAG, periph, return NULL);
|
|
|
|
periph_is31fl3216_t *is31fl3216 =
|
|
audio_calloc(1, sizeof(periph_is31fl3216_t));
|
|
AUDIO_MEM_CHECK(TAG, is31fl3216, {
|
|
audio_free(periph);
|
|
return NULL;
|
|
});
|
|
|
|
is31fl3216->g_event_bit = xEventGroupCreate();
|
|
AUDIO_NULL_CHECK(TAG, is31fl3216->g_event_bit, {
|
|
audio_free(periph);
|
|
audio_free(is31fl3216);
|
|
});
|
|
|
|
is31fl3216->evt = xQueueCreate(2, sizeof(periph_is31_msg_t));
|
|
AUDIO_MEM_CHECK(TAG, is31fl3216->evt, {
|
|
audio_free(periph);
|
|
vEventGroupDelete(is31fl3216->g_event_bit);
|
|
audio_free(is31fl3216);
|
|
return NULL;
|
|
});
|
|
is31fl3216->arg = audio_calloc(1, sizeof(periph_is31_arg_t));
|
|
AUDIO_MEM_CHECK(TAG, is31fl3216->arg, {
|
|
vQueueDelete(is31fl3216->evt);
|
|
vEventGroupDelete(is31fl3216->g_event_bit);
|
|
audio_free(periph);
|
|
audio_free(is31fl3216);
|
|
return NULL;
|
|
});
|
|
is31fl3216->arg->max_light_num = IS31FL3216_CH_NUM;
|
|
is31fl3216->arg->light_num = 0;
|
|
is31fl3216->arg->light_mask = 0;
|
|
is31fl3216->arg->interval_time = 1000;
|
|
is31fl3216->arg->act_time = 0;
|
|
is31fl3216->arg->duty_step = DEFAULT_FLASH_STEP;
|
|
is31fl3216->arg->shift_mode = PERIPH_IS31_SHIFT_MODE_ACC;
|
|
|
|
for (int i = 0; i < IS31FL3216_CH_NUM; i++) {
|
|
is31fl3216->duty[i] = is31fl3216_config->duty[i];
|
|
}
|
|
is31fl3216->handle = is31fl3216_init();
|
|
AUDIO_MEM_CHECK(TAG, is31fl3216, {
|
|
audio_free(is31fl3216->arg);
|
|
vQueueDelete(is31fl3216->evt);
|
|
audio_free(periph);
|
|
vEventGroupDelete(is31fl3216->g_event_bit);
|
|
audio_free(is31fl3216);
|
|
return NULL;
|
|
});
|
|
|
|
esp_periph_set_data(periph, is31fl3216);
|
|
esp_periph_set_function(periph, _is31fl3216_init, NULL, _is31fl3216_destroy);
|
|
return periph;
|
|
}
|