538 lines
17 KiB
C
538 lines
17 KiB
C
#ifdef SUPPORT_MS_EXTENSIONS
|
|
#pragma bss_seg(".a2dp_streamctrl.data.bss")
|
|
#pragma data_seg(".a2dp_streamctrl.data")
|
|
#pragma const_seg(".a2dp_streamctrl.text.const")
|
|
#pragma code_seg(".a2dp_streamctrl.text")
|
|
#endif
|
|
/*************************************************************************************************/
|
|
/*!
|
|
* \file a2dp_streamctrl.c
|
|
*
|
|
* \brief a2dp plug stream control file.
|
|
*
|
|
* Copyright (c) 2011-2022 ZhuHai Jieli Technology Co.,Ltd.
|
|
*
|
|
*/
|
|
/*************************************************************************************************/
|
|
#include "btstack/a2dp_media_codec.h"
|
|
#include "sync/audio_syncts.h"
|
|
#include "system/timer.h"
|
|
#include "media/audio_base.h"
|
|
#include "source_node.h"
|
|
#include "jiffies.h"
|
|
#include "a2dp_streamctrl.h"
|
|
|
|
extern const int CONFIG_A2DP_DELAY_TIME_AAC;
|
|
extern const int CONFIG_A2DP_DELAY_TIME_SBC;
|
|
extern const int CONFIG_A2DP_ADAPTIVE_MAX_LATENCY;
|
|
extern const int CONFIG_A2DP_DELAY_TIME_AAC_LO;
|
|
extern const int CONFIG_A2DP_DELAY_TIME_SBC_LO;
|
|
extern const int CONFIG_JL_DONGLE_PLAYBACK_LATENCY;
|
|
extern const int CONFIG_DONGLE_SPEAK_ENABLE;
|
|
extern const int CONFIG_A2DP_MAX_BUF_SIZE;
|
|
extern u32 bt_audio_reference_clock_time(u8 network);
|
|
|
|
#define A2DP_FLUENT_DETECT_INTERVAL 90000//ms 流畅播放延时检测时长
|
|
#define JL_DONGLE_FLUENT_DETECT_INTERVAL 30000//ms
|
|
#define A2DP_ADAPTIVE_DELAY_ENABLE 1
|
|
|
|
#define A2DP_STREAM_NO_ERR 0
|
|
#define A2DP_STREAM_UNDERRUN 1
|
|
#define A2DP_STREAM_OVERRUN 2
|
|
#define A2DP_STREAM_MISSED 3
|
|
#define A2DP_STREAM_DECODE_ERR 4
|
|
#define A2DP_STREAM_LOW_UNDERRUN 5
|
|
|
|
#define RB16(b) (u16)(((u8 *)b)[0] << 8 | (((u8 *)b))[1])
|
|
#define bt_time_to_msecs(clk) (((clk) * 625) / 1000)
|
|
|
|
#define DELAY_INCREMENT 150
|
|
#define LOW_LATENCY_DELAY_INCREMENT 50
|
|
|
|
#define DELAY_DECREMENT 50
|
|
#define LOW_LATENCY_DELAY_DECREMENT 10
|
|
|
|
#define MAX_DELAY_INCREMENT 150
|
|
struct a2dp_stream_control {
|
|
u8 plan;
|
|
u8 stream_error;
|
|
u8 frame_free;
|
|
u8 first_in;
|
|
u8 low_latency;
|
|
u8 jl_dongle;
|
|
u16 timer;
|
|
u16 seqn;
|
|
u16 overrun_seqn;
|
|
u16 missed_num;
|
|
s16 repair_frames;
|
|
s16 initial_latency;
|
|
s16 adaptive_latency;
|
|
s16 adaptive_max_latency;
|
|
s16 max_rx_interval;
|
|
u32 detect_timeout;
|
|
u32 codec_type;
|
|
u32 frame_time;
|
|
struct a2dp_media_frame frame;
|
|
int frame_len;
|
|
void *stream;
|
|
void *sample_detect;
|
|
u32 next_timestamp;
|
|
void *underrun_signal;
|
|
void (*underrun_callback)(void *);
|
|
};
|
|
|
|
|
|
void a2dp_stream_mark_next_timestamp(void *_ctrl, u32 next_timestamp)
|
|
{
|
|
struct a2dp_stream_control *ctrl = (struct a2dp_stream_control *)_ctrl;
|
|
if (ctrl) {
|
|
ctrl->next_timestamp = next_timestamp;
|
|
}
|
|
}
|
|
|
|
void a2dp_stream_bandwidth_detect_handler(void *_ctrl, int pcm_frames, int sample_rate)
|
|
{
|
|
struct a2dp_stream_control *ctrl = (struct a2dp_stream_control *)_ctrl;
|
|
int max_latency = 0;
|
|
|
|
if (ctrl->low_latency) {
|
|
return;
|
|
}
|
|
|
|
if (ctrl->frame_len) {
|
|
max_latency = (CONFIG_A2DP_MAX_BUF_SIZE * pcm_frames / ctrl->frame_len) * 1000 / sample_rate * 9 / 10;
|
|
}
|
|
|
|
if (!max_latency) {
|
|
return;
|
|
}
|
|
|
|
if (max_latency < ctrl->adaptive_max_latency) {
|
|
ctrl->adaptive_max_latency = max_latency;
|
|
}
|
|
|
|
if (ctrl->adaptive_latency > ctrl->adaptive_max_latency) {
|
|
ctrl->adaptive_latency = ctrl->adaptive_max_latency;
|
|
a2dp_media_update_delay_report_time(ctrl->stream, ctrl->adaptive_latency);
|
|
}
|
|
}
|
|
|
|
static void a2dp_stream_underrun_adaptive_handler(struct a2dp_stream_control *ctrl)
|
|
{
|
|
#if A2DP_ADAPTIVE_DELAY_ENABLE
|
|
ctrl->detect_timeout = jiffies + msecs_to_jiffies(ctrl->jl_dongle ? JL_DONGLE_FLUENT_DETECT_INTERVAL : A2DP_FLUENT_DETECT_INTERVAL);
|
|
|
|
if (!ctrl->low_latency) {
|
|
ctrl->adaptive_latency = ctrl->adaptive_max_latency;
|
|
} else {
|
|
ctrl->adaptive_latency += LOW_LATENCY_DELAY_INCREMENT;
|
|
|
|
if (ctrl->adaptive_latency < ctrl->max_rx_interval) {
|
|
ctrl->adaptive_latency = ctrl->max_rx_interval;
|
|
}
|
|
|
|
if (ctrl->adaptive_latency > ctrl->adaptive_max_latency) {
|
|
ctrl->adaptive_latency = ctrl->adaptive_max_latency;
|
|
}
|
|
}
|
|
a2dp_media_update_delay_report_time(ctrl->stream, ctrl->adaptive_latency);
|
|
/*printf("---underrun, adaptive : %dms---\n", ctrl->adaptive_latency);*/
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* 自适应延时策略
|
|
*/
|
|
static void a2dp_stream_adaptive_detect_handler(struct a2dp_stream_control *ctrl, u8 new_frame, u32 new_frame_time)
|
|
{
|
|
#if A2DP_ADAPTIVE_DELAY_ENABLE
|
|
if (ctrl->stream_error || !new_frame) {
|
|
return;
|
|
}
|
|
int rx_interval = 0;
|
|
if (ctrl->frame_time) {
|
|
rx_interval = bt_time_to_msecs((new_frame_time - ctrl->frame_time) & 0x7ffffff) + 1;
|
|
}
|
|
ctrl->frame_time = new_frame_time;
|
|
|
|
if (rx_interval > ctrl->max_rx_interval) {
|
|
if (CONFIG_DONGLE_SPEAK_ENABLE && ctrl->jl_dongle) {
|
|
return;
|
|
}
|
|
ctrl->max_rx_interval = rx_interval;
|
|
if (ctrl->max_rx_interval > ctrl->initial_latency + MAX_DELAY_INCREMENT) {
|
|
ctrl->max_rx_interval = ctrl->initial_latency + MAX_DELAY_INCREMENT;
|
|
}
|
|
if (ctrl->adaptive_latency < ctrl->max_rx_interval) {
|
|
ctrl->adaptive_latency = ctrl->max_rx_interval;
|
|
a2dp_media_update_delay_report_time(ctrl->stream, ctrl->adaptive_latency);
|
|
ctrl->detect_timeout = jiffies + msecs_to_jiffies(A2DP_FLUENT_DETECT_INTERVAL);
|
|
ctrl->max_rx_interval = ctrl->initial_latency;
|
|
return;
|
|
}
|
|
/*printf("---rx interval, adaptive : %dms, %dms---\n", ctrl->adaptive_latency, ctrl->max_rx_interval);*/
|
|
}
|
|
|
|
if (time_after(jiffies, ctrl->detect_timeout)) {
|
|
ctrl->adaptive_latency -= DELAY_DECREMENT;
|
|
if (ctrl->adaptive_latency < ctrl->max_rx_interval) {
|
|
ctrl->adaptive_latency = ctrl->max_rx_interval;
|
|
}
|
|
|
|
if (ctrl->adaptive_latency < ctrl->initial_latency) {
|
|
ctrl->adaptive_latency = ctrl->initial_latency;
|
|
}
|
|
/*printf("---adaptive detect : %dms, %dms---\n", ctrl->adaptive_latency, ctrl->max_rx_interval);*/
|
|
if (ctrl->low_latency) {
|
|
ctrl->max_rx_interval -= 10;//ctrl->initial_latency;
|
|
} else {
|
|
ctrl->max_rx_interval = ctrl->initial_latency;
|
|
}
|
|
ctrl->detect_timeout = jiffies + msecs_to_jiffies(A2DP_FLUENT_DETECT_INTERVAL);
|
|
a2dp_media_update_delay_report_time(ctrl->stream, ctrl->adaptive_latency);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
void *a2dp_stream_control_plan_select(void *stream, int low_latency, u32 codec_type, u8 plan)
|
|
{
|
|
struct a2dp_stream_control *ctrl = (struct a2dp_stream_control *)zalloc(sizeof(struct a2dp_stream_control));
|
|
|
|
if (!ctrl) {
|
|
return NULL;
|
|
}
|
|
|
|
switch (plan) {
|
|
|
|
case A2DP_STREAM_JL_DONGLE_CONTROL:
|
|
if (CONFIG_DONGLE_SPEAK_ENABLE) {
|
|
ctrl->initial_latency = CONFIG_JL_DONGLE_PLAYBACK_LATENCY;
|
|
ctrl->adaptive_latency = ctrl->initial_latency;
|
|
ctrl->max_rx_interval = ctrl->initial_latency;
|
|
ctrl->adaptive_max_latency = low_latency ? (ctrl->initial_latency + MAX_DELAY_INCREMENT) : CONFIG_A2DP_ADAPTIVE_MAX_LATENCY;
|
|
ctrl->detect_timeout = jiffies + msecs_to_jiffies(A2DP_FLUENT_DETECT_INTERVAL);
|
|
ctrl->jl_dongle = 1;
|
|
}
|
|
break;
|
|
default:
|
|
if (low_latency) {
|
|
ctrl->initial_latency = codec_type == A2DP_CODEC_MPEG24 ? CONFIG_A2DP_DELAY_TIME_AAC_LO : CONFIG_A2DP_DELAY_TIME_SBC_LO;
|
|
} else {
|
|
ctrl->initial_latency = codec_type == A2DP_CODEC_MPEG24 ? CONFIG_A2DP_DELAY_TIME_AAC : CONFIG_A2DP_DELAY_TIME_SBC;
|
|
}
|
|
ctrl->adaptive_max_latency = low_latency ? (ctrl->initial_latency + MAX_DELAY_INCREMENT) : CONFIG_A2DP_ADAPTIVE_MAX_LATENCY;
|
|
ctrl->adaptive_latency = ctrl->initial_latency;
|
|
ctrl->max_rx_interval = ctrl->initial_latency;
|
|
ctrl->detect_timeout = jiffies + msecs_to_jiffies(A2DP_FLUENT_DETECT_INTERVAL);
|
|
break;
|
|
}
|
|
|
|
ctrl->low_latency = low_latency;
|
|
ctrl->first_in = 1;
|
|
ctrl->codec_type = codec_type;
|
|
ctrl->plan = plan;
|
|
ctrl->stream = stream;
|
|
a2dp_media_update_delay_report_time(ctrl->stream, ctrl->adaptive_latency);
|
|
return ctrl;
|
|
}
|
|
|
|
static void a2dp_stream_underrun_signal(void *arg)
|
|
{
|
|
struct a2dp_stream_control *ctrl = (struct a2dp_stream_control *)arg;
|
|
|
|
local_irq_disable();
|
|
if (ctrl->underrun_callback) {
|
|
ctrl->underrun_callback(ctrl->underrun_signal);
|
|
}
|
|
ctrl->timer = 0;
|
|
local_irq_enable();
|
|
}
|
|
|
|
static int a2dp_audio_is_underrun(struct a2dp_stream_control *ctrl)
|
|
{
|
|
int underrun_time = ctrl->low_latency ? 2 : 30;
|
|
if (ctrl->next_timestamp) {
|
|
u32 reference_clock = bt_audio_reference_clock_time(0);
|
|
if (reference_clock == (u32) - 1) {
|
|
return true;
|
|
}
|
|
u32 reference_time = reference_clock * 625 * TIMESTAMP_US_DENOMINATOR;
|
|
int distance_time = ctrl->next_timestamp - reference_time;
|
|
if (distance_time > 67108863L || distance_time < -67108863L) {
|
|
if (ctrl->next_timestamp > reference_time) {
|
|
distance_time = ctrl->next_timestamp - 0xffffffff - reference_time;
|
|
} else {
|
|
distance_time = 0xffffffff - reference_time + ctrl->next_timestamp;
|
|
}
|
|
}
|
|
distance_time = distance_time / 1000 / TIMESTAMP_US_DENOMINATOR;
|
|
/*printf("distance_time %d %u %u\n", distance_time, ctrl->next_timestamp, reference_time);*/
|
|
if (distance_time < underrun_time) {
|
|
return true;
|
|
}
|
|
|
|
local_irq_disable();
|
|
if (ctrl->timer) {
|
|
sys_hi_timeout_del(ctrl->timer);
|
|
ctrl->timer = 0;
|
|
}
|
|
ctrl->timer = sys_hi_timeout_add(ctrl, a2dp_stream_underrun_signal, distance_time - underrun_time);
|
|
local_irq_enable();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int a2dp_stream_underrun_handler(struct a2dp_stream_control *ctrl, struct a2dp_media_frame *frame)
|
|
{
|
|
if (!a2dp_audio_is_underrun(ctrl)) {
|
|
/*putchar('x');*/
|
|
return 0;
|
|
}
|
|
putchar('X');
|
|
|
|
a2dp_stream_underrun_adaptive_handler(ctrl);
|
|
if (ctrl->stream_error != A2DP_STREAM_UNDERRUN) {
|
|
if (!ctrl->stream_error) {
|
|
|
|
}
|
|
ctrl->stream_error = A2DP_STREAM_UNDERRUN;
|
|
}
|
|
memcpy(frame, &ctrl->frame, sizeof(ctrl->frame));
|
|
ctrl->repair_frames++;
|
|
|
|
return ctrl->frame_len;
|
|
}
|
|
|
|
static void a2dp_stream_control_free_frames(struct a2dp_stream_control *ctrl, struct a2dp_media_frame *frame)
|
|
{
|
|
if (frame && frame->packet) {
|
|
a2dp_media_free_packet(ctrl->stream, frame->packet);
|
|
if ((void *)frame->packet == (void *)ctrl->frame.packet) {
|
|
ctrl->frame.packet = NULL;
|
|
}
|
|
}
|
|
|
|
if (ctrl->frame.packet) {
|
|
a2dp_media_free_packet(ctrl->stream, ctrl->frame.packet);
|
|
ctrl->frame.packet = NULL;
|
|
}
|
|
ctrl->frame_len = 0;
|
|
}
|
|
|
|
|
|
static int a2dp_stream_overrun_handler(struct a2dp_stream_control *ctrl, struct a2dp_media_frame *frame, int *len)
|
|
{
|
|
while (1) {
|
|
if (1) {//!ctrl->low_latency) {
|
|
int msecs = a2dp_media_get_remain_play_time(ctrl->stream, 1);
|
|
if (msecs < ctrl->adaptive_latency) {
|
|
/*printf("adaptive latency %d, msecs %d\n", ctrl->adaptive_latency, msecs);*/
|
|
break;
|
|
}
|
|
}
|
|
|
|
int rlen = a2dp_media_try_get_packet(ctrl->stream, frame);
|
|
if (rlen <= 0) {
|
|
break;
|
|
}
|
|
a2dp_stream_control_free_frames(ctrl, NULL);
|
|
memcpy(&ctrl->frame, frame, sizeof(ctrl->frame));
|
|
ctrl->frame_len = rlen;
|
|
*len = rlen;
|
|
putchar('n');
|
|
return 1;
|
|
}
|
|
|
|
memcpy(frame, &ctrl->frame, sizeof(ctrl->frame));
|
|
*len = ctrl->frame_len;
|
|
putchar('o');
|
|
return 0;
|
|
}
|
|
|
|
static int a2dp_stream_missed_handler(struct a2dp_stream_control *ctrl, struct a2dp_media_frame *frame, int *len)
|
|
{
|
|
memcpy(frame, &ctrl->frame, sizeof(ctrl->frame));
|
|
*len = ctrl->frame_len;
|
|
if (--ctrl->missed_num == 0) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int a2dp_stream_error_filter(struct a2dp_stream_control *ctrl, struct a2dp_media_frame *frame, int len)
|
|
{
|
|
int err = 0;
|
|
|
|
int header_len = a2dp_media_get_rtp_header_len(ctrl->codec_type, frame->packet, len);
|
|
if (header_len >= len) {
|
|
printf("##A2DP header error : %d\n", header_len);
|
|
a2dp_stream_control_free_frames(ctrl, frame);
|
|
return -EFAULT;
|
|
}
|
|
|
|
u16 seqn = RB16(frame->packet + 2);
|
|
if (ctrl->first_in) {
|
|
ctrl->first_in = 0;
|
|
goto __exit;
|
|
}
|
|
if (seqn != ctrl->seqn) {
|
|
if (ctrl->stream_error == A2DP_STREAM_UNDERRUN) {
|
|
int missed_frames = (u16)(seqn - ctrl->seqn) - 1;
|
|
if (missed_frames > ctrl->repair_frames) {
|
|
ctrl->stream_error = A2DP_STREAM_MISSED;
|
|
ctrl->missed_num = 2;//missed_frames - ctrl->repair_frames + 1;
|
|
/*printf("case 0 : %d, %d\n", missed_frames, ctrl->repair_frames);*/
|
|
err = -EAGAIN;
|
|
} else if (missed_frames < ctrl->repair_frames) {
|
|
ctrl->stream_error = A2DP_STREAM_OVERRUN;
|
|
ctrl->overrun_seqn = seqn + ctrl->repair_frames - missed_frames;
|
|
/*printf("case 1 : %d, %d, seqn : %d, %d\n", missed_frames, ctrl->repair_frames, seqn, ctrl->overrun_seqn);*/
|
|
err = -EAGAIN;
|
|
}
|
|
} else if (!ctrl->stream_error && (u16)(seqn - ctrl->seqn) > 1) {
|
|
err = -EAGAIN;
|
|
if ((u16)(seqn - ctrl->seqn) > 32768) {
|
|
a2dp_stream_control_free_frames(ctrl, frame);
|
|
return err;
|
|
}
|
|
ctrl->stream_error = A2DP_STREAM_MISSED;
|
|
ctrl->missed_num = 2;//(u16)(seqn - ctrl->seqn);
|
|
/*printf("case 2 : %d, %d, %d\n", seqn, ctrl->seqn, ctrl->missed_num); */
|
|
}
|
|
ctrl->repair_frames = 0;
|
|
}
|
|
|
|
__exit:
|
|
ctrl->seqn = seqn;
|
|
memcpy(&ctrl->frame, frame, sizeof(ctrl->frame));
|
|
ctrl->frame_len = len;
|
|
return err;
|
|
}
|
|
|
|
static int a2dp_get_frame_and_check_errors(struct a2dp_stream_control *ctrl, struct a2dp_media_frame *frame, int *len)
|
|
{
|
|
int rlen = 0;
|
|
int err = 0;
|
|
u8 new_packet = 0;
|
|
|
|
try_again:
|
|
switch (ctrl->stream_error) {
|
|
case A2DP_STREAM_OVERRUN:
|
|
new_packet = a2dp_stream_overrun_handler(ctrl, frame, len);
|
|
break;
|
|
case A2DP_STREAM_MISSED:
|
|
new_packet = a2dp_stream_missed_handler(ctrl, frame, len);
|
|
if (frame->packet) {
|
|
break;
|
|
}
|
|
//注意:这里不break是因为很有可能由于位流的错误导致补包无法再正常补上
|
|
default:
|
|
rlen = a2dp_media_try_get_packet(ctrl->stream, frame);
|
|
if (rlen <= 0) {
|
|
rlen = a2dp_stream_underrun_handler(ctrl, frame);
|
|
} else {
|
|
if (ctrl->frame.packet) {
|
|
a2dp_media_free_packet(ctrl->stream, ctrl->frame.packet);
|
|
ctrl->frame.packet = NULL;
|
|
}
|
|
ctrl->frame_len = 0;
|
|
new_packet = 1;
|
|
}
|
|
*len = rlen;
|
|
break;
|
|
}
|
|
|
|
if (*len <= 0) {
|
|
return 0;
|
|
}
|
|
err = a2dp_stream_error_filter(ctrl, frame, *len);
|
|
if (err) {
|
|
if (-err == EAGAIN) {
|
|
goto try_again;
|
|
}
|
|
*len = 0;
|
|
return 0;
|
|
}
|
|
|
|
a2dp_stream_adaptive_detect_handler(ctrl, new_packet, frame->clkn);
|
|
|
|
if (ctrl->stream_error) {
|
|
if (new_packet) {
|
|
ctrl->stream_error = 0;
|
|
return FRAME_FLAG_RESET_TIMESTAMP_BIT;
|
|
}
|
|
return FRAME_FLAG_FILL_PACKET;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int a2dp_stream_control_pull_frame(void *_ctrl, struct a2dp_media_frame *frame, int *len)
|
|
{
|
|
struct a2dp_stream_control *ctrl = (struct a2dp_stream_control *)_ctrl;
|
|
|
|
if (!ctrl) {
|
|
*len = 0;
|
|
return 0;
|
|
}
|
|
|
|
switch (ctrl->plan) {
|
|
default:
|
|
return a2dp_get_frame_and_check_errors(ctrl, frame, len);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void a2dp_stream_control_free_frame(void *_ctrl, struct a2dp_media_frame *frame)
|
|
{
|
|
struct a2dp_stream_control *ctrl = (struct a2dp_stream_control *)_ctrl;
|
|
|
|
switch (ctrl->plan) {
|
|
default:
|
|
if (ctrl->frame.packet == frame->packet) {
|
|
ctrl->frame_free = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
void a2dp_stream_control_set_underrun_callback(void *_ctrl, void *priv, void (*callback)(void *priv))
|
|
{
|
|
struct a2dp_stream_control *ctrl = (struct a2dp_stream_control *)_ctrl;
|
|
|
|
ctrl->underrun_signal = priv;
|
|
ctrl->underrun_callback = callback;
|
|
}
|
|
|
|
int a2dp_stream_control_delay_time(void *_ctrl)
|
|
{
|
|
struct a2dp_stream_control *ctrl = (struct a2dp_stream_control *)_ctrl;
|
|
|
|
if (ctrl) {
|
|
return ctrl->adaptive_latency;
|
|
}
|
|
|
|
return CONFIG_A2DP_DELAY_TIME_AAC;
|
|
}
|
|
|
|
void a2dp_stream_control_free(void *_ctrl)
|
|
{
|
|
struct a2dp_stream_control *ctrl = (struct a2dp_stream_control *)_ctrl;
|
|
|
|
if (!ctrl) {
|
|
return;
|
|
}
|
|
|
|
a2dp_stream_control_free_frames(ctrl, NULL);
|
|
|
|
local_irq_disable();
|
|
if (ctrl->timer) {
|
|
sys_hi_timeout_del(ctrl->timer);
|
|
ctrl->timer = 0;
|
|
}
|
|
local_irq_enable();
|
|
free(ctrl);
|
|
}
|