Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
A
arduino-esp32
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Analytics
Analytics
Repository
Value Stream
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Commits
Open sidebar
xpstem
arduino-esp32
Commits
5e7db8df
Unverified
Commit
5e7db8df
authored
Dec 14, 2021
by
P-R-O-C-H-Y
Committed by
GitHub
Dec 14, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Timer based od ESP-IDF (#5931)
parent
fb00b51f
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
151 additions
and
246 deletions
+151
-246
cores/esp32/esp32-hal-timer.c
cores/esp32/esp32-hal-timer.c
+147
-243
cores/esp32/esp32-hal-timer.h
cores/esp32/esp32-hal-timer.h
+4
-3
No files found.
cores/esp32/esp32-hal-timer.c
View file @
5e7db8df
...
...
@@ -13,42 +13,10 @@
// limitations under the License.
#include "esp32-hal-timer.h"
#include "freertos/FreeRTOS.h"
#ifndef CONFIG_IDF_TARGET_ESP32C3
#include "freertos/xtensa_api.h"
#include "soc/dport_reg.h"
#endif
#include "freertos/task.h"
#include "soc/timer_group_struct.h"
#include "esp_attr.h"
#include "driver/periph_ctrl.h"
#include "esp_system.h"
#ifdef ESP_IDF_VERSION_MAJOR // IDF 4+
#if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4
#include "esp32/rom/ets_sys.h"
#include "esp_intr_alloc.h"
#elif CONFIG_IDF_TARGET_ESP32S2
#include "esp32s2/rom/ets_sys.h"
#include "esp_intr_alloc.h"
#include "soc/periph_defs.h"
#elif CONFIG_IDF_TARGET_ESP32C3
#include "esp32c3/rom/ets_sys.h"
#include "esp_intr_alloc.h"
#include "soc/periph_defs.h"
#else
#error Target CONFIG_IDF_TARGET is not supported
#endif
#else // ESP32 Before IDF 4.0
#include "rom/ets_sys.h"
#include "esp_intr.h"
#endif
#define HWTIMER_LOCK() portENTER_CRITICAL(timer->lock)
#define HWTIMER_UNLOCK() portEXIT_CRITICAL(timer->lock)
typedef
volatile
struct
{
union
{
#include "driver/timer.h"
#include "soc/soc_caps.h"
typedef
union
{
struct
{
uint32_t
reserved0
:
10
;
uint32_t
alarm_en
:
1
;
/*When set alarm is enabled*/
...
...
@@ -60,288 +28,212 @@ typedef volatile struct {
uint32_t
enable
:
1
;
/*When set timer 0/1 time-base counter is enabled*/
};
uint32_t
val
;
}
config
;
uint32_t
cnt_low
;
/*Register to store timer 0/1 time-base counter current value lower 32 bits.*/
uint32_t
cnt_high
;
/*Register to store timer 0 time-base counter current value higher 32 bits.*/
uint32_t
update
;
/*Write any value will trigger a timer 0 time-base counter value update (timer 0 current value will be stored in registers above)*/
uint32_t
alarm_low
;
/*Timer 0 time-base counter value lower 32 bits that will trigger the alarm*/
uint32_t
alarm_high
;
/*Timer 0 time-base counter value higher 32 bits that will trigger the alarm*/
uint32_t
load_low
;
/*Lower 32 bits of the value that will load into timer 0 time-base counter*/
uint32_t
load_high
;
/*higher 32 bits of the value that will load into timer 0 time-base counter*/
uint32_t
reload
;
/*Write any value will trigger timer 0 time-base counter reload*/
}
hw_timer_reg_t
;
typedef
struct
hw_timer_s
{
hw_timer_reg_t
*
dev
;
uint8_t
num
;
}
timer_cfg_t
;
#define NUM_OF_TIMERS SOC_TIMER_GROUP_TOTAL_TIMERS
typedef
struct
{
int
timer_group
;
int
timer_idx
;
int
alarm_interval
;
bool
auto_reload
;
}
timer_info_t
;
typedef
struct
hw_timer_s
{
uint8_t
group
;
uint8_t
timer
;
portMUX_TYPE
lock
;
uint8_t
num
;
}
hw_timer_t
;
static
hw_timer_t
hw_timer
[
4
]
=
{
{(
hw_timer_reg_t
*
)(
DR_REG_TIMERGROUP0_BASE
),
0
,
0
,
0
,
portMUX_INITIALIZER_UNLOCKED
},
{(
hw_timer_reg_t
*
)(
DR_REG_TIMERGROUP0_BASE
+
0x0024
),
1
,
0
,
1
,
portMUX_INITIALIZER_UNLOCKED
},
{(
hw_timer_reg_t
*
)(
DR_REG_TIMERGROUP0_BASE
+
0x1000
),
2
,
1
,
0
,
portMUX_INITIALIZER_UNLOCKED
},
{(
hw_timer_reg_t
*
)(
DR_REG_TIMERGROUP0_BASE
+
0x1024
),
3
,
1
,
1
,
portMUX_INITIALIZER_UNLOCKED
}
// Works for all chips
static
hw_timer_t
timer_dev
[
4
]
=
{
{
0
,
0
},
{
1
,
0
},
{
1
,
0
},
{
1
,
1
}
};
typedef
void
(
*
voidFuncPtr
)(
void
);
static
voidFuncPtr
__timerInterruptHandlers
[
4
]
=
{
0
,
0
,
0
,
0
};
void
ARDUINO_ISR_ATTR
__timerISR
(
void
*
arg
){
uint32_t
s0
=
TIMERG0
.
int_st_timers
.
val
;
uint32_t
s1
=
TIMERG1
.
int_st_timers
.
val
;
TIMERG0
.
int_clr_timers
.
val
=
s0
;
TIMERG1
.
int_clr_timers
.
val
=
s1
;
uint8_t
status
=
(
s1
&
3
)
<<
2
|
(
s0
&
3
);
uint8_t
i
=
4
;
//restart the timers that should autoreload
while
(
i
--
){
hw_timer_reg_t
*
dev
=
hw_timer
[
i
].
dev
;
if
((
status
&
(
1
<<
i
))
&&
dev
->
config
.
autoreload
){
dev
->
config
.
alarm_en
=
1
;
}
}
i
=
4
;
//call callbacks
while
(
i
--
){
if
(
__timerInterruptHandlers
[
i
]
&&
(
status
&
(
1
<<
i
))){
__timerInterruptHandlers
[
i
]();
}
}
}
// NOTE: (in IDF 5.0 there wont be need to know groups/numbers
// timer_init() will list thru all timers and return free timer handle)
uint64_t
inline
timerRead
(
hw_timer_t
*
timer
){
timer
->
dev
->
update
=
1
;
while
(
timer
->
dev
->
update
)
{};
uint64_t
h
=
timer
->
dev
->
cnt_high
;
uint64_t
l
=
timer
->
dev
->
cnt_low
;
return
(
h
<<
32
)
|
l
;
uint64_t
value
;
timer_get_counter_value
(
timer
->
group
,
timer
->
num
,
&
value
);
return
value
;
}
uint64_t
timerAlarmRead
(
hw_timer_t
*
timer
){
uint64_t
h
=
timer
->
dev
->
alarm_high
;
uint64_t
l
=
timer
->
dev
->
alarm_low
;
return
(
h
<<
32
)
|
l
;
uint64_t
value
;
timer_get_alarm_value
(
timer
->
group
,
timer
->
num
,
&
value
)
;
return
value
;
}
void
timerWrite
(
hw_timer_t
*
timer
,
uint64_t
val
){
timer
->
dev
->
load_high
=
(
uint32_t
)
(
val
>>
32
);
timer
->
dev
->
load_low
=
(
uint32_t
)
(
val
);
timer
->
dev
->
reload
=
1
;
timer_set_counter_value
(
timer
->
group
,
timer
->
num
,
val
);
}
void
timerAlarmWrite
(
hw_timer_t
*
timer
,
uint64_t
alarm_value
,
bool
autoreload
){
timer
->
dev
->
alarm_high
=
(
uint32_t
)
(
alarm_value
>>
32
);
timer
->
dev
->
alarm_low
=
(
uint32_t
)
alarm_value
;
timer
->
dev
->
config
.
autoreload
=
autoreload
;
timer_set_alarm_value
(
timer
->
group
,
timer
->
num
,
alarm_value
);
timerSetAutoReload
(
timer
,
autoreload
);
}
void
timerSetConfig
(
hw_timer_t
*
timer
,
uint32_t
config
){
timer
->
dev
->
config
.
val
=
config
;
timer_cfg_t
cfg
;
cfg
.
val
=
config
;
timer_set_alarm
(
timer
->
group
,
timer
->
num
,
cfg
.
alarm_en
);
timerSetDivider
(
timer
,
cfg
.
divider
);
timerSetAutoReload
(
timer
,
cfg
.
autoreload
);
timerSetCountUp
(
timer
,
cfg
.
increase
);
if
(
cfg
.
enable
)
{
timerStart
(
timer
);
}
else
{
timerStop
(
timer
);
}
return
;
}
uint32_t
timerGetConfig
(
hw_timer_t
*
timer
){
return
timer
->
dev
->
config
.
val
;
timer_config_t
timer_cfg
;
timer_get_config
(
timer
->
group
,
timer
->
num
,
&
timer_cfg
);
//Translate to default uint32_t
timer_cfg_t
cfg
;
cfg
.
alarm_en
=
timer_cfg
.
alarm_en
;
cfg
.
autoreload
=
timer_cfg
.
auto_reload
;
cfg
.
divider
=
timer_cfg
.
divider
;
cfg
.
edge_int_en
=
timer_cfg
.
intr_type
;
cfg
.
level_int_en
=
!
timer_cfg
.
intr_type
;
cfg
.
enable
=
timer_cfg
.
counter_en
;
cfg
.
increase
=
timer_cfg
.
counter_dir
;
return
cfg
.
val
;
}
void
timerSetCountUp
(
hw_timer_t
*
timer
,
bool
countUp
){
timer
->
dev
->
config
.
increase
=
countUp
;
timer
_set_counter_mode
(
timer
->
group
,
timer
->
num
,
countUp
)
;
}
bool
timerGetCountUp
(
hw_timer_t
*
timer
){
return
timer
->
dev
->
config
.
increase
;
timer_cfg_t
config
;
config
.
val
=
timerGetConfig
(
timer
);
return
config
.
increase
;
}
void
timerSetAutoReload
(
hw_timer_t
*
timer
,
bool
autoreload
){
timer
->
dev
->
config
.
autoreload
=
autoreload
;
timer
_set_auto_reload
(
timer
->
group
,
timer
->
num
,
autoreload
)
;
}
bool
timerGetAutoReload
(
hw_timer_t
*
timer
){
return
timer
->
dev
->
config
.
autoreload
;
timer_cfg_t
config
;
config
.
val
=
timerGetConfig
(
timer
);
return
config
.
autoreload
;
}
void
timerSetDivider
(
hw_timer_t
*
timer
,
uint16_t
divider
){
//2 to 65536
if
(
!
divider
){
divider
=
0xFFFF
;
}
else
if
(
divider
==
1
){
divider
=
2
;
// Set divider from 2 to 65535
void
timerSetDivider
(
hw_timer_t
*
timer
,
uint16_t
divider
){
if
(
divider
<
2
)
{
log_e
(
"Timer divider must be set in range of 2 to 65535"
);
return
;
}
int
timer_en
=
timer
->
dev
->
config
.
enable
;
timer
->
dev
->
config
.
enable
=
0
;
timer
->
dev
->
config
.
divider
=
divider
;
timer
->
dev
->
config
.
enable
=
timer_en
;
timer_set_divider
(
timer
->
group
,
timer
->
num
,
divider
);
}
uint16_t
timerGetDivider
(
hw_timer_t
*
timer
){
return
timer
->
dev
->
config
.
divider
;
timer_cfg_t
config
;
config
.
val
=
timerGetConfig
(
timer
);
return
config
.
divider
;
}
void
timerStart
(
hw_timer_t
*
timer
){
timer
->
dev
->
config
.
enable
=
1
;
timer
_start
(
timer
->
group
,
timer
->
num
)
;
}
void
timerStop
(
hw_timer_t
*
timer
){
timer
->
dev
->
config
.
enable
=
0
;
timer
_pause
(
timer
->
group
,
timer
->
num
)
;
}
void
timerRestart
(
hw_timer_t
*
timer
){
timer
->
dev
->
config
.
enable
=
0
;
timer
->
dev
->
reload
=
1
;
timer
->
dev
->
config
.
enable
=
1
;
timerWrite
(
timer
,
0
);
}
bool
timerStarted
(
hw_timer_t
*
timer
){
return
timer
->
dev
->
config
.
enable
;
timer_cfg_t
config
;
config
.
val
=
timerGetConfig
(
timer
);
return
config
.
enable
;
}
void
timerAlarmEnable
(
hw_timer_t
*
timer
){
timer
->
dev
->
config
.
alarm_en
=
1
;
timer
_set_alarm
(
timer
->
group
,
timer
->
num
,
true
)
;
}
void
timerAlarmDisable
(
hw_timer_t
*
timer
){
timer
->
dev
->
config
.
alarm_en
=
0
;
timer
_set_alarm
(
timer
->
group
,
timer
->
num
,
false
)
;
}
bool
timerAlarmEnabled
(
hw_timer_t
*
timer
){
return
timer
->
dev
->
config
.
alarm_en
;
timer_cfg_t
config
;
config
.
val
=
timerGetConfig
(
timer
);
return
config
.
alarm_en
;
}
static
void
_on_apb_change
(
void
*
arg
,
apb_change_ev_t
ev_type
,
uint32_t
old_apb
,
uint32_t
new_apb
){
hw_timer_t
*
timer
=
(
hw_timer_t
*
)
arg
;
if
(
ev_type
==
APB_BEFORE_CHANGE
){
timer
->
dev
->
config
.
enable
=
0
;
timer
Stop
(
timer
)
;
}
else
{
old_apb
/=
1000000
;
new_apb
/=
1000000
;
timer
->
dev
->
config
.
divider
=
(
new_apb
*
timer
->
dev
->
config
.
divider
)
/
old_apb
;
timer
->
dev
->
config
.
enable
=
1
;
uint16_t
divider
=
(
new_apb
*
timerGetDivider
(
timer
))
/
old_apb
;
timerSetDivider
(
timer
,
divider
);
timerStart
(
timer
);
}
}
hw_timer_t
*
timerBegin
(
uint8_t
num
,
uint16_t
divider
,
bool
countUp
){
if
(
num
>
3
){
if
(
num
>=
NUM_OF_TIMERS
)
{
log_e
(
"Timer dont have that timer number."
);
return
NULL
;
}
hw_timer_t
*
timer
=
&
hw_timer
[
num
];
if
(
timer
->
group
)
{
periph_module_enable
(
PERIPH_TIMG1_MODULE
);
}
else
{
periph_module_enable
(
PERIPH_TIMG0_MODULE
);
}
timer
->
dev
->
config
.
enable
=
0
;
if
(
timer
->
group
)
{
#if CONFIG_IDF_TARGET_ESP32
TIMERG1
.
int_ena
.
val
&=
~
BIT
(
timer
->
timer
);
#else
TIMERG1
.
int_ena_timers
.
val
&=
~
BIT
(
timer
->
timer
);
#endif
TIMERG1
.
int_clr_timers
.
val
|=
BIT
(
timer
->
timer
);
}
else
{
#if CONFIG_IDF_TARGET_ESP32
TIMERG0
.
int_ena
.
val
&=
~
BIT
(
timer
->
timer
);
#else
TIMERG0
.
int_ena_timers
.
val
&=
~
BIT
(
timer
->
timer
);
#endif
TIMERG0
.
int_clr_timers
.
val
|=
BIT
(
timer
->
timer
);
}
#ifdef TIMER_GROUP_SUPPORTS_XTAL_CLOCK
timer
->
dev
->
config
.
use_xtal
=
0
;
#endif
timerSetDivider
(
timer
,
divider
);
timerSetCountUp
(
timer
,
countUp
);
timerSetAutoReload
(
timer
,
false
);
timerAttachInterrupt
(
timer
,
NULL
,
false
);
timerWrite
(
timer
,
0
);
timer
->
dev
->
config
.
enable
=
1
;
hw_timer_t
*
timer
=
&
timer_dev
[
num
];
//Get Timer group/num from 0-3 number
timer_config_t
config
=
{
.
divider
=
divider
,
.
counter_dir
=
countUp
,
.
counter_en
=
TIMER_PAUSE
,
.
alarm_en
=
TIMER_ALARM_DIS
,
.
auto_reload
=
false
,
};
timer_init
(
timer
->
group
,
timer
->
num
,
&
config
);
timer_set_counter_value
(
timer
->
group
,
timer
->
num
,
0
);
timerStart
(
timer
);
addApbChangeCallback
(
timer
,
_on_apb_change
);
return
timer
;
}
void
timerEnd
(
hw_timer_t
*
timer
){
timer
->
dev
->
config
.
enable
=
0
;
timerAttachInterrupt
(
timer
,
NULL
,
false
);
removeApbChangeCallback
(
timer
,
_on_apb_change
);
timer_deinit
(
timer
->
group
,
timer
->
num
);
}
void
timerAttachInterrupt
(
hw_timer_t
*
timer
,
void
(
*
fn
)(
void
),
bool
edge
){
#if CONFIG_IDF_TARGET_ESP32
if
(
edge
){
log_w
(
"EDGE timer interrupt
does not work properly on ESP32
! Setting to LEVEL..."
);
log_w
(
"EDGE timer interrupt
is not supported
! Setting to LEVEL..."
);
edge
=
false
;
}
#endif
static
bool
initialized
=
false
;
static
intr_handle_t
intr_handle
=
NULL
;
if
(
intr_handle
){
esp_intr_disable
(
intr_handle
);
}
if
(
fn
==
NULL
){
timer
->
dev
->
config
.
level_int_en
=
0
;
timer
->
dev
->
config
.
edge_int_en
=
0
;
timer
->
dev
->
config
.
alarm_en
=
0
;
if
(
timer
->
num
&
2
){
#if CONFIG_IDF_TARGET_ESP32
TIMERG1
.
int_ena
.
val
&=
~
BIT
(
timer
->
timer
);
#else
TIMERG1
.
int_ena_timers
.
val
&=
~
BIT
(
timer
->
timer
);
#endif
TIMERG1
.
int_clr_timers
.
val
|=
BIT
(
timer
->
timer
);
}
else
{
#if CONFIG_IDF_TARGET_ESP32
TIMERG0
.
int_ena
.
val
&=
~
BIT
(
timer
->
timer
);
#else
TIMERG0
.
int_ena_timers
.
val
&=
~
BIT
(
timer
->
timer
);
#endif
TIMERG0
.
int_clr_timers
.
val
|=
BIT
(
timer
->
timer
);
}
__timerInterruptHandlers
[
timer
->
num
]
=
NULL
;
}
else
{
__timerInterruptHandlers
[
timer
->
num
]
=
fn
;
timer
->
dev
->
config
.
level_int_en
=
edge
?
0
:
1
;
//When set, an alarm will generate a level type interrupt.
timer
->
dev
->
config
.
edge_int_en
=
edge
?
1
:
0
;
//When set, an alarm will generate an edge type interrupt.
int
intr_source
=
0
;
#ifndef CONFIG_IDF_TARGET_ESP32C3
if
(
!
edge
){
#endif
if
(
timer
->
group
){
intr_source
=
ETS_TG1_T0_LEVEL_INTR_SOURCE
+
timer
->
timer
;
}
else
{
intr_source
=
ETS_TG0_T0_LEVEL_INTR_SOURCE
+
timer
->
timer
;
}
#ifndef CONFIG_IDF_TARGET_ESP32C3
}
else
{
if
(
timer
->
group
){
intr_source
=
ETS_TG1_T0_EDGE_INTR_SOURCE
+
timer
->
timer
;
}
else
{
intr_source
=
ETS_TG0_T0_EDGE_INTR_SOURCE
+
timer
->
timer
;
}
}
#endif
if
(
!
initialized
){
initialized
=
true
;
esp_intr_alloc
(
intr_source
,
(
int
)(
ARDUINO_ISR_FLAG
|
ESP_INTR_FLAG_LOWMED
),
__timerISR
,
NULL
,
&
intr_handle
);
}
else
{
intr_matrix_set
(
esp_intr_get_cpu
(
intr_handle
),
intr_source
,
esp_intr_get_intno
(
intr_handle
));
}
if
(
timer
->
group
){
#if CONFIG_IDF_TARGET_ESP32
TIMERG1
.
int_ena
.
val
|=
BIT
(
timer
->
timer
);
#else
TIMERG1
.
int_ena_timers
.
val
|=
BIT
(
timer
->
timer
);
#endif
}
else
{
#if CONFIG_IDF_TARGET_ESP32
TIMERG0
.
int_ena
.
val
|=
BIT
(
timer
->
timer
);
#else
TIMERG0
.
int_ena_timers
.
val
|=
BIT
(
timer
->
timer
);
#endif
}
}
if
(
intr_handle
){
esp_intr_enable
(
intr_handle
);
}
timer_enable_intr
(
timer
->
group
,
timer
->
num
);
timer_info_t
*
timer_info
=
calloc
(
1
,
sizeof
(
timer_info_t
));
timer_info
->
timer_group
=
timer
->
group
;
timer_info
->
timer_idx
=
timer
->
num
;
timer_info
->
auto_reload
=
timerGetAutoReload
(
timer
);
timer_info
->
alarm_interval
=
timerAlarmRead
(
timer
);
timer_isr_callback_add
(
timer
->
group
,
timer
->
num
,
(
timer_isr_t
)
fn
,
timer_info
,
0
);
}
void
timerDetachInterrupt
(
hw_timer_t
*
timer
){
...
...
@@ -354,6 +246,12 @@ uint64_t timerReadMicros(hw_timer_t *timer){
return
timer_val
*
div
/
(
getApbFrequency
()
/
1000000
);
}
uint64_t
timerReadMilis
(
hw_timer_t
*
timer
){
uint64_t
timer_val
=
timerRead
(
timer
);
uint16_t
div
=
timerGetDivider
(
timer
);
return
timer_val
*
div
/
(
getApbFrequency
()
/
1000
);
}
double
timerReadSeconds
(
hw_timer_t
*
timer
){
uint64_t
timer_val
=
timerRead
(
timer
);
uint16_t
div
=
timerGetDivider
(
timer
);
...
...
@@ -366,6 +264,12 @@ uint64_t timerAlarmReadMicros(hw_timer_t *timer){
return
timer_val
*
div
/
(
getApbFrequency
()
/
1000000
);
}
uint64_t
timerAlarmReadMilis
(
hw_timer_t
*
timer
){
uint64_t
timer_val
=
timerAlarmRead
(
timer
);
uint16_t
div
=
timerGetDivider
(
timer
);
return
timer_val
*
div
/
(
getApbFrequency
()
/
1000
);
}
double
timerAlarmReadSeconds
(
hw_timer_t
*
timer
){
uint64_t
timer_val
=
timerAlarmRead
(
timer
);
uint16_t
div
=
timerGetDivider
(
timer
);
...
...
cores/esp32/esp32-hal-timer.h
View file @
5e7db8df
...
...
@@ -20,13 +20,13 @@
#ifndef MAIN_ESP32_HAL_TIMER_H_
#define MAIN_ESP32_HAL_TIMER_H_
#include "esp32-hal.h"
#include "freertos/FreeRTOS.h"
#ifdef __cplusplus
extern
"C"
{
#endif
#include "esp32-hal.h"
#include "freertos/FreeRTOS.h"
struct
hw_timer_s
;
typedef
struct
hw_timer_s
hw_timer_t
;
...
...
@@ -50,6 +50,7 @@ void timerSetAutoReload(hw_timer_t *timer, bool autoreload);
bool
timerStarted
(
hw_timer_t
*
timer
);
uint64_t
timerRead
(
hw_timer_t
*
timer
);
uint64_t
timerReadMicros
(
hw_timer_t
*
timer
);
uint64_t
timerReadMilis
(
hw_timer_t
*
timer
);
double
timerReadSeconds
(
hw_timer_t
*
timer
);
uint16_t
timerGetDivider
(
hw_timer_t
*
timer
);
bool
timerGetCountUp
(
hw_timer_t
*
timer
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment