Peripheral Drivers in ESP-IDF(2)——GP Timer
这篇博客是ESP-IDF外设驱动系列的第二篇,这篇博客的研究对象是ESP32系列MCU中的定时器组,涉及到的和之前的思路类似,首先,随后深入研究定时器外设的使用方法和其驱动结构组织。
0.Briefly Speaking
这篇博客是ESP-IDF外设驱动系列的第二篇,这篇博客的研究对象是ESP32系列MCU中的定时器组,涉及到的主要参考资料和源代码内容如下:
- ESP32C6技术参考手册
- ESP-IDF编程指南——GPTimer
- esp-idf/components/esp-driver-gptimer (定时器驱动代码)
- esp-idf/examples/peripherals/timer_group/gptimer (使用示例)
和之前的思路类似,首先以一份使用示例代码作为引子,随后深入研究定时器外设的使用方法和其驱动结构组织。
引子——如何使用一个Timer
以下代码展示了如何使用一个定时器(节选自gptimer_example_main.c),大体可以总结为以下几个步骤,以下的行文按照这几个步骤作为子标题,深入研究ESP32系列MCU的计数器使用方法及其IDF驱动组织结构,可以点击下方目录实现直接跳转。
- 分配并初始化一个新的定时器(gptimer_new_timer)
- 设置计数器报警回调函数(gptimer_register_event_callbacks)
- 使能计数器(gptimer_enable)
- 设置警报行为(gptimer_set_alarm_action)
- 开启计数器(gptimer_start)
- 停止及回收计数器(gptimer_stop/disable/del)
void app_main(void)
{
example_queue_element_t ele;
QueueHandle_t queue = xQueueCreate(10, sizeof(example_queue_element_t));
if (!queue) {
ESP_LOGE(TAG, "Creating queue failed");
return;
}
ESP_LOGI(TAG, "Create timer handle");
gptimer_handle_t gptimer = NULL;
// 1.分配并初始化一个新的定时器(gptimer_new_timer)
gptimer_config_t timer_config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT,
.direction = GPTIMER_COUNT_UP,
.resolution_hz = 1000000, // 1MHz, 1 tick=1us
};
ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &gptimer));
// ...skip some code
// 2.设置计数器报警回调函数
cbs.on_alarm = example_timer_on_alarm_cb_v2;
ESP_ERROR_CHECK(gptimer_register_event_callbacks(gptimer, &cbs, queue));
ESP_LOGI(TAG, "Enable timer");
// 3.使能计数器
ESP_ERROR_CHECK(gptimer_enable(gptimer));
ESP_LOGI(TAG, "Start timer, auto-reload at alarm event");
// 4.设置警报行为
gptimer_alarm_config_t alarm_config2 = {
.reload_count = 0,
.alarm_count = 1000000, // period = 1s
.flags.auto_reload_on_alarm = true,
};
ESP_ERROR_CHECK(gptimer_set_alarm_action(gptimer, &alarm_config2));
// 5.开启定时器
ESP_ERROR_CHECK(gptimer_start(gptimer));
int record = 4;
while (record) {
if (xQueueReceive(queue, &ele, pdMS_TO_TICKS(2000))) {
ESP_LOGI(TAG, "Timer reloaded, count=%llu", ele.event_count);
record--;
} else {
ESP_LOGW(TAG, "Missed one count event");
}
}
// 跳过一些代码...
// 6.停止及回收计数器
ESP_LOGI(TAG, "Stop timer");
ESP_ERROR_CHECK(gptimer_stop(gptimer));
ESP_LOGI(TAG, "Disable timer");
ESP_ERROR_CHECK(gptimer_disable(gptimer));
ESP_LOGI(TAG, "Delete timer");
ESP_ERROR_CHECK(gptimer_del_timer(gptimer));
vQueueDelete(queue);
}
1 分配并初始化一个新的定时器
1.1 定时器结构体——gptimer_t
ESP-IDF中使用gptimer_t来抽象一个gptimer硬件实例,它记录了关于一个timer所有可用属性的抽象。
struct gptimer_t {
// 出于可扩展性,这里每个gptimer都记录着它属于哪个计时器组
// 同样的,组里也保留着指向每个timer实例的指针
// typedef struct gptimer_group_t {
// int group_id;
// portMUX_TYPE spinlock; // to protect per-group register level concurrent access
// gptimer_t *timers[SOC_TIMER_GROUP_TIMERS_PER_GROUP];
// } gptimer_group_t;
gptimer_group_t *group;
// 当前定时器的ID
int timer_id;
// 分辨率,越大表明计数越精细,指的是经过1s的时间需要时钟源发出多少个ticks
uint32_t resolution_hz;
// 如果开启自动装填功能,填充的数值是多少
uint64_t reload_count;
// 触发警报的时钟tick,达到警报值时将会触发回调函数on_alarm(见下)
uint64_t alarm_count;
// 计数方向,时钟tick到来时计数值递增还是递减
gptimer_count_direction_t direction;
// 计数器硬件实例
timer_hal_context_t hal;
// 当前timer的状态机,<*原子类型变量*>
_Atomic gptimer_fsm_t fsm;
// 中断优先级
int intr_priority;
// 中断向量信息
// typedef struct intr_handle_data_t {
// vector_desc_t *vector_desc;
// shared_vector_desc_t *shared_vector_desc;
// } intr_handle_data_t;
intr_handle_t intr;
// 自旋锁,在timer级别执行并发保护
portMUX_TYPE spinlock; // to protect per-timer resources concurrent accessed by task and ISR handler
// 到达报警值时触发的回调函数
gptimer_alarm_cb_t on_alarm;
void *user_ctx;
// 时钟源
// 对于C6而言,timer可用时钟源有以下几个(esp_clk_tree.c):
// typedef enum {
// GPTIMER_CLK_SRC_PLL_F80M = SOC_MOD_CLK_PLL_F80M, /*!< Select PLL_F80M as the source clock */
// GPTIMER_CLK_SRC_RC_FAST = SOC_MOD_CLK_RC_FAST, /*!< Select RC_FAST as the source clock */
// GPTIMER_CLK_SRC_XTAL = SOC_MOD_CLK_XTAL, /*!< Select XTAL as the source clock */
// GPTIMER_CLK_SRC_DEFAULT = SOC_MOD_CLK_PLL_F80M, /*!< Select PLL_F80M as the default choice */
//} soc_periph_gptimer_clk_src_t;
gptimer_clock_source_t clk_src;
esp_pm_lock_handle_t pm_lock; // power management lock
#if CONFIG_PM_ENABLE
char pm_lock_name[GPTIMER_PM_LOCK_NAME_LEN_MAX]; // pm lock name
#endif
// 一些属性标志标志
struct {
uint32_t intr_shared: 1; // timer中断是否是可共享
uint32_t auto_reload_on_alarm: 1; // 在系统进入睡眠模式时,此电源域是否可掉电
uint32_t alarm_en: 1;
} flags;
};
typedef gptimer_t* gptimer_t_handle;
1.2 定时器配置结构体——gptimer_config_t
ESP-IDF中使用gptimer_config_t结构体来指定一个计数器中可由用户配置的一些基本属性,因此它只是上述gptimer_t结构体所具有的一些属性的子集,因此具有相同的含义,这里不再展开介绍。
typedef struct {
gptimer_clock_source_t clk_src;
gptimer_count_direction_t direction;
uint32_t resolution_hz;
int intr_priority;
struct {
uint32_t intr_shared: 1;
uint32_t allow_pd: 1;
uint32_t backup_before_sleep: 1;
} flags;
} gptimer_config_t;
1.3 初始化一个新的定时器(gptimer_new_timer)
首先给出gptimer_new_timer函数的调用总览图,gptimer_new_timer完成的事情有以下几个方面,点击可跳转到对应的子标题:
- 新timer的注册,并关联到具体的硬件实例(gptimer_register_to_group)
- 重置timer硬件实例,使其恢复到初始状态(timer_hal_init)
- 设置定时器时钟源和分频(gptimer_select_periph_clock)
- 按照传入的配置,设定计时器的基本属性
这里只对其中一些比较重要的函数进行剖析,一些LL层设置寄存器的动作不再展开,相关操作流程可与TRM两相对照。
// gptimer_new_timer函数的调用关系图
gptimer_new_timer(const gptimer_config_t*, gptimer_handle_t*);
// 分配timer_id, 将timer实例记录到对应的组里
gptimer_register_to_group(gptimer_t*);
// 初始化一个gptimer group实例
gptimer_acquire_group_handle(int);
// 初始化timer硬件实例
timer_hal_init(timer_hal_context_t*, uint32_t, uint32_t);
// 关闭时基计数器、自动装填功能和alarm(注意虽然名字是enable,但传入的bool值是false)
timer_ll_enable_counter(timg_dev_t*, uint32_t, bool);
timer_ll_enable_auto_reload(timg_dev_t*, uint32_t, bool);
timer_ll_enable_alarm(timg_dev_t*, uint32_t, bool);
// 设置定时器时钟来源和分辨率
gptimer_select_periph_clock(gptimer_t*, gptimer_clock_source_t, uint32_t);
// 获取时钟源的主频
esp_clk_tree_src_get_freq_hz(soc_module_clk_t, esp_clk_tree_src_freq_precision_t, uint32_t *);
// 开启时钟源自身的门控(很多ESP32 MCU并没有时钟门控)
esp_clk_tree_enable_src(soc_module_clk_t, bool);
// 设置时钟源并打开时钟源到timer的门控
timer_ll_set_clock_source(timg_dev_t*, uint32_t, gptimer_clock_source_t);
timer_ll_enable_clock(timg_dev_t*, uint32_t, bool);
// 根据分辨率设定timer的预分频器
timer_ll_set_clock_prescale(timg_dev_t*, uint32_t, uint32_t);
// 设置当前计数值为0(这里是通过触发立即reload来实现的)
// 写入一个值 = 缓存旧的reload值 + 软件触发新的reload + 恢复旧的reload值
timer_hal_set_counter_value(timer_hal_context_t*, uint64_t);
// 设置timer计数方向
timer_ll_set_count_direction(timg_dev_t*, uint32_t, gptimer_count_direction_t);
// 关闭timer中断(同一个TimerGroup下的所有gptimer共享一个中断)
timer_ll_enable_intr(timg_dev_t*, uint32_t, bool);
// 清掉之前的中断标志
timer_ll_clear_intr_status(timg_dev_t*, uint32_t);
1.3.1 gptimer_register_to_group
此函数完成的主要功能是在timer_group(已有的或新申请的)中找出一个可用的gptimer硬件实例,并将其与传入的timer句柄关联起来,代码实现如下:
static esp_err_t gptimer_register_to_group(gptimer_t *timer)
{
gptimer_group_t *group = NULL;
int timer_id = -1;
for (int i = 0; i < SOC_TIMER_GROUPS; i++) {
// 找出对应group_id的组,若不存在,则新分配一个并注册到platform中
group = gptimer_acquire_group_handle(i);
ESP_RETURN_ON_FALSE(group, ESP_ERR_NO_MEM, TAG, "no mem for group (%d)", i);
// 寻找group中是否还有没有使用的timer实例
portENTER_CRITICAL(&group->spinlock);
for (int j = 0; j < SOC_TIMER_GROUP_TIMERS_PER_GROUP; j++) {
if (!group->timers[j]) {
timer_id = j;
group->timers[j] = timer;
break;
}
}
portEXIT_CRITICAL(&group->spinlock);
// 如果没有找到可用的定时器,则释放之前分配的group
if (timer_id < 0) {
gptimer_release_group_handle(group);
} else {
// 若找到,则将timer实例与此group关联起来
timer->timer_id = timer_id;
timer->group = group;
break;
}
}
ESP_RETURN_ON_FALSE(timer_id != -1, ESP_ERR_NOT_FOUND, TAG, "no free timer");
#if GPTIMER_USE_RETENTION_LINK
// 从略
#endif // GPTIMER_USE_RETENTION_LINK
return ESP_OK;
}
此函数中调用了gptimer_acquire_group_handle函数分配了一个新的group,并将其注册到s_platform中,并递增s_platform和全局变量ref_counts中对外设的引用计数。s_platform是ESP-IDF中对外设驱动最高层次的抽象,一个类型的外设仅对应到一个s_platform(因此是单例模式),而一个s_platform下辖多个组(group),一个组又下辖多个硬件实例(instance),所以IDF中形成了platform -> group -> instance的层次化驱动结构。
gptimer_group_t *gptimer_acquire_group_handle(int group_id)
{
bool new_group = false;
gptimer_group_t *group = NULL;
// s_platform是一个静态的单例变量,对于gptimer它的platform定义如下
// typedef struct gptimer_platform_t {
// _lock_t mutex; // platform level mutex lock
// gptimer_group_t *groups[SOC_TIMER_GROUPS]; // timer group pool
// int group_ref_counts[SOC_TIMER_GROUPS]; // reference count used to protect group install/uninstall
// } gptimer_platform_t;
// 防止多个任务同时注册group
_lock_acquire(&s_platform.mutex);
// 如果group还没有注册到platform
if (!s_platform.groups[group_id]) {
group = heap_caps_calloc(1, sizeof(gptimer_group_t), GPTIMER_MEM_ALLOC_CAPS);
if (group) {
// 则分配group,并填入一些基本信息
new_group = true;
s_platform.groups[group_id] = group;
group->group_id = group_id;
group->spinlock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED;
}
} else {
// 否则,这个组已经被注册过,直接取出
group = s_platform.groups[group_id];
}
// 递增s_platform中对group的引用计数
if (group) {
// someone acquired the group handle means we have a new object that refer to this group
s_platform.group_ref_counts[group_id]++;
}
_lock_release(&s_platform.mutex);
// 如果是新分配的组,还需要递增一个全局引用计数
// 并做一些硬件上的初始化动作
if (new_group) {
// !!! HARDWARE SHARED RESOURCE !!!
// the gptimer and watchdog reside in the same the timer group
// we need to increase/decrease the reference count before enable/disable/reset the peripheral
// 由于gptimer和watch dog timer在同一个timer group中
// 所以当使用其中任何一个时,都需要将这个组整体的时钟信号打开,并复位一些公有的寄存器
PERIPH_RCC_ACQUIRE_ATOMIC(timer_group_periph_signals.groups[group_id].module, ref_count) {
if (ref_count == 0) {
timer_ll_enable_bus_clock(group_id, true);
timer_ll_reset_register(group_id);
}
}
ESP_LOGD(TAG, "new group (%d) @%p", group_id, group);
}
return group;
}
Digression 1:关于并发控制的语法糖
上述代码中的并发控制中使用的语法糖非常有趣,在这里展开分析一下,请注意函数gptimer_acquire_group_handle中的这一段代码:
PERIPH_RCC_ACQUIRE_ATOMIC(timer_group_periph_signals.groups[group_id].module, ref_count) {
if (ref_count == 0) {
timer_ll_enable_bus_clock(group_id, true);
timer_ll_reset_register(group_id);
}
}
这段代码本质上完成了两件事情:
- 递增timer_group的全局引用计数(ref_count),并返回递增前的引用计数
- 如果是首次初始化(计数值为0),则使能timer group的时钟信号,并复位一些寄存器
首先来看PERIPH_RCC_ACQUIRE_ATOMIC这个宏,从名称上来看,它保证了访问的原子性,这一方面是通过自旋锁来保证的(对全局计数ref_count的访问),另一方面还使用了一个“有趣的检查机制”,保证硬件层面的操作也是串行化执行的,细节请看下面:
/**
* @brief Acquire the RCC lock for a peripheral module
*
* @note User code protected by this macro should be as short as possible, because it's a critical section
* @note This macro will increase the reference lock of that peripheral.
* You can get the value before the increment from the `rc_name` local variable
*/
// 可以看到PERIPH_RCC_ACQUIRE_ATOMIC本质上对应到一个for循环
#define PERIPH_RCC_ACQUIRE_ATOMIC(rc_periph, rc_name) \
// 声明了3个变量:rc_name、_rc_cnt = 1、__DECLARE_RCC_RC_ATOMIC_ENV
// rc是reference counter的缩写
// 请注意这里声明了__DECLARE_RCC_RC_ATOMIC_ENV变量,这是一个表明我们即将处于原子化执行的上下文的标志,此标志之后会被检查
for (uint8_t rc_name, _rc_cnt = 1, __DECLARE_RCC_RC_ATOMIC_ENV; \
// 这里用了逗号表达式,如果_rc_cnt不为0,则将rc_name的赋值为periph_rcc_acquire_enter(rc_periph)并返回1
// 否则,返回0结束循环,结合到上述的调用,这里将ref_count变量赋值为了periph_rcc_acquire_enter(rc_periph)
// 这个函数会获取自旋锁,并返回timer group的全局计数(ref_counts, defined in periph_ctrl.c)
_rc_cnt ? (rc_name = periph_rcc_acquire_enter(rc_periph), 1) : 0; \
// periph_rcc_acquire_exit(rc_periph, rc_name)将会递增循环计数
periph_rcc_acquire_exit(rc_periph, rc_name), _rc_cnt--)
这里一个非常重要的细节在于,由于循环条件会率先进行判断,所以返回到rc_name的计数值是被递增之前的,而循环迭代条件则会在循环体执行之后再执行,进而保证退出循环体时(periph_rcc_acquire_exit)引用计数会被正确递增。
接下来,循环体内调用了两个LL层次的函数,LL层次我们在上一篇博客中已经提及,它会操作目标相关的寄存器完成对应的功能设置,但值得注意的是这里的两个函数本质上也是受到保护的宏包装,如下所示:
/// use a macro to wrap the function, force the caller to use it in a critical section
/// the critical section needs to declare the __DECLARE_RCC_RC_ATOMIC_ENV variable in advance
// 这里在调用真实的LL函数_timer_ll_enable_bus_clock之前,会先声明一个变量__DECLARE_RCC_RC_ATOMIC_ENV
// 如果程序上下文中没有声明__DECLARE_RCC_RC_ATOMIC_ENV,编译器就会报错
// 提醒user这个操作没有在临界区中被调用
// 上述循环体开始时对__DECLARE_RCC_RC_ATOMIC_ENV的声明就是此意
#define timer_ll_enable_bus_clock(...) (void)__DECLARE_RCC_RC_ATOMIC_ENV; _timer_ll_enable_bus_clock(__VA_ARGS__)
1.3.2 gptimer_select_periph_clock
此函数按照定时器传入的配置,设定了计时器使用的时钟来源,并按照时钟源的真实频率传入的分辨率设定了预分频器(prescaler)的数值,这里涉及到一些获取时钟源频率的动作,暂时没有展开。
esp_err_t gptimer_select_periph_clock(gptimer_t *timer, gptimer_clock_source_t src_clk, uint32_t resolution_hz)
{
uint32_t counter_src_hz = 0;
int timer_id = timer->timer_id;
// TODO: [clk_tree] to use a generic clock enable/disable or acquire/release function for all clock source
#if SOC_TIMER_GROUP_SUPPORT_RC_FAST
// 从略
#endif // SOC_TIMER_GROUP_SUPPORT_RC_FAST
// 获取时钟源的频率
// TODO: 此函数用来获取指定时钟源的时钟频率,涉及到时钟校准(calibration)
ESP_RETURN_ON_ERROR(esp_clk_tree_src_get_freq_hz((soc_module_clk_t)src_clk,
ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED,
&counter_src_hz),
TAG, "get clock source frequency failed");
#if CONFIG_PM_ENABLE
// ...从略,这里是一些与功耗管理相关的设置
#endif // CONFIG_PM_ENABLE
// 打开时钟源一侧的门控
esp_clk_tree_enable_src((soc_module_clk_t)src_clk, true);
// 和上面同样的语法糖
// ESP32中有很多外设的时钟源选择都挤在一个寄存器中,可能存在并发问题
// !!! HARDWARE SHARED RESOURCE !!!
// on some ESP chip, different peripheral's clock source setting are mixed in the same register
// so we need to make this done in an atomic way
GPTIMER_CLOCK_SRC_ATOMIC() {
timer_ll_set_clock_source(timer->hal.dev, timer_id, src_clk);
timer_ll_enable_clock(timer->hal.dev, timer_id, true);
}
// 将时钟源设置到timer字段中
timer->clk_src = src_clk;
// 根据时钟源的频率,计算预分频器数值,并设置预分频器
uint32_t prescale = counter_src_hz / resolution_hz; // potential resolution loss here
timer_ll_set_clock_prescale(timer->hal.dev, timer_id, prescale);
// 精度可能有损失,这里将真实的分辨率设置到timer字段中
timer->resolution_hz = counter_src_hz / prescale; // this is the real resolution
if (timer->resolution_hz != resolution_hz) {
ESP_LOGW(TAG, "resolution lost, expect %"PRIu32", real %"PRIu32, resolution_hz, timer->resolution_hz);
}
return ESP_OK;
}
2 设置计数器报警回调函数
2.1 gptimer_register_event_callback
此函数主要为timer完成了CPU中断的分配,并将其ISR关联到gptimer_default_isr(gptimer_default_isr详解请点此),最后将cbs->on_alarm回调函数设置到了timer结构体中。请注意区分这两者,gptimer_default_isr是发生timer中断时CPU自动跳入的函数,而cbs->on_alarm会在ISR中被调用从而完成用户想完成的定时功能。
esp_err_t gptimer_register_event_callbacks(gptimer_handle_t timer, const gptimer_event_callbacks_t *cbs, void *user_data)
{
// 获取timer的group、group_id和timer_id
gptimer_group_t *group = NULL;
ESP_RETURN_ON_FALSE(timer && cbs, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
group = timer->group;
int group_id = group->group_id;
int timer_id = timer->timer_id;
// 一些当回调函数放置在SRAM时的合法性检查
#if CONFIG_GPTIMER_ISR_IRAM_SAFE
if (cbs->on_alarm) {
ESP_RETURN_ON_FALSE(esp_ptr_in_iram(cbs->on_alarm), ESP_ERR_INVALID_ARG, TAG, "on_alarm callback not in IRAM");
}
if (user_data) {
ESP_RETURN_ON_FALSE(esp_ptr_internal(user_data), ESP_ERR_INVALID_ARG, TAG, "user context not in internal RAM");
}
#endif
// 如果timer的中断描述符还没有分配
if (!timer->intr) {
// 验证一下当前FSM状态是否处于init
// TODO: FSM真的有必要吗
ESP_RETURN_ON_FALSE(atomic_load(&timer->fsm) == GPTIMER_FSM_INIT, ESP_ERR_INVALID_STATE, TAG, "timer not in init state");
// 分配中断标志
// GPTIMER_INTR_ALLOC_FLAGS本质上指向ESP_INTR_FLAG_INTRDISABLED
// 也就是分配完中断后要立即关闭这个中断
int isr_flags = timer->flags.intr_shared ?
ESP_INTR_FLAG_SHARED | GPTIMER_INTR_ALLOC_FLAGS :
GPTIMER_INTR_ALLOC_FLAGS;
if (timer->intr_priority) {
isr_flags |= 1 << (timer->intr_priority);
}
// 调用esp_intr_alloc_intrstatus函数完成CPU中断的分配
// 此函数详解请参见GPIO篇Digression3
// 中断处理函数为gptimer_default_isr
ESP_RETURN_ON_ERROR(esp_intr_alloc_intrstatus(
timer_group_periph_signals.groups[group_id].timer_irq_id[timer_id],
isr_flags,
(uint32_t)timer_ll_get_intr_status_reg(timer->hal.dev),
TIMER_LL_EVENT_ALARM(timer_id),
gptimer_default_isr,
timer,
&timer->intr), TAG, "install interrupt service failed");
}
// 打开timer中断
portENTER_CRITICAL(&group->spinlock);
timer_ll_enable_intr(timer->hal.dev, TIMER_LL_EVENT_ALARM(timer->timer_id), cbs->on_alarm != NULL); // enable timer interrupt
portEXIT_CRITICAL(&group->spinlock);
// 将on_alarm回调函数设置到timer结构体中
timer->on_alarm = cbs->on_alarm;
timer->user_ctx = user_data;
return ESP_OK;
}
2.2 gptimer_default_isr
这是timer注册到CPU的默认中断处理函数,它完成的逻辑相对也比较简单,清中断和手动触发alarm回调函数,如果计时器还打开了自动装填功能,则还需要再次手动打开alarm功能,因为当警报触发时alarm会被硬件自动关闭。
static void gptimer_default_isr(void *args)
{
// 取出timer所属组和回调函数
bool need_yield = false;
gptimer_t *timer = (gptimer_t *)args;
gptimer_group_t *group = timer->group;
gptimer_alarm_cb_t on_alarm_cb = timer->on_alarm;
uint32_t intr_status = timer_ll_get_intr_status(timer->hal.dev);
// 如果确定当前timer发生了中断
if (intr_status & TIMER_LL_EVENT_ALARM(timer->timer_id)) {
// 获取当前定时器的计数值与警报值
gptimer_alarm_event_data_t edata = {
.count_value = timer_hal_capture_and_get_counter_value(&timer->hal),
.alarm_value = timer->alarm_count,
};
// 清除对应的中断标记
portENTER_CRITICAL_ISR(&group->spinlock);
timer_ll_clear_intr_status(timer->hal.dev, TIMER_LL_EVENT_ALARM(timer->timer_id));
// 如果设定了发生警报时自动装填,则需要手动再次开启警报
// 因为发生警报后,硬件会自动关闭alarm
if (timer->flags.auto_reload_on_alarm) {
timer_ll_enable_alarm(timer->hal.dev, timer->timer_id, true);
}
portEXIT_CRITICAL_ISR(&group->spinlock);
// 触发alarm回调函数
if (on_alarm_cb) {
if (on_alarm_cb(timer, &edata, timer->user_ctx)) {
need_yield = true;
}
}
}
if (need_yield) {
portYIELD_FROM_ISR();
}
}
3 使能计数器(的中断)
其实,使能计数器这个叫法有些误导性,gptimer_enable函数的功能基本上只是完成了两件事,更新定时器状态机到GPTIMER_FSM_ENABLE,其次是使能gptimer对应的CPU中断。请注意这里的措辞是使能CPU中断,因为任何一个外设中断的处理,首先需要将其与某个CPU中断关联起来,这样外设中断才可以被转发到CPU的PLIC/CLINT中(以RISC-V处理器为例)。CPU跳入对应的中断处理函数后,再调用外设注册的回调函数,至此才算完成对外设中断的响应。
3.1 gptimer_enable
esp_err_t gptimer_enable(gptimer_handle_t timer)
{
ESP_RETURN_ON_FALSE(timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
gptimer_fsm_t expected_fsm = GPTIMER_FSM_INIT;
// 将timer状态机的状态变更为GPTIMER_FSM_ENABLE
// atomic_compare_exchange_strong是一个原子的CAS(Compare & Swap)函数
// atomic_compare_exchange_strong(A, B, C)的执行逻辑:
// if(A==B) A=C, return true;
// else B=A, return false;
ESP_RETURN_ON_FALSE(atomic_compare_exchange_strong(&timer->fsm, &expected_fsm, GPTIMER_FSM_ENABLE),
ESP_ERR_INVALID_STATE, TAG, "timer not in init state");
// 获取功耗管理锁
if (timer->pm_lock) {
ESP_RETURN_ON_ERROR(esp_pm_lock_acquire(timer->pm_lock), TAG, "acquire pm_lock failed");
}
// 使能CPU中断
// esp_intr_enable函数会区分两种情况:
// 1.如果是来自外设的中断(中断号 > 0),此中断需要经过中断交换矩阵,只需要将外部中断路由到CPU即可
// 2.如果是CPU保留的内部中断(中断号 < 0),此中断不需要经过中断交换矩阵,只需要将CPU对应的中断位打开即可
if (timer->intr) {
ESP_RETURN_ON_ERROR(esp_intr_enable(timer->intr), TAG, "enable interrupt service failed");
}
return ESP_OK;
}
4 设置报警行为
4.1 警报配置结构体——gptimer_alarm_config_t
ESP-IDF中将可以由用户配置的警报属性抽象出来作为一个独立结构体gptimer_alarm_config_t,其定义如下:
typedef struct {
// 警报计数值,达到此计数值后会触发中断
uint64_t alarm_count;
// 触发中断之后重新加载的计数值
uint64_t reload_count;
struct {
// 是否开启自动加载功能
// 中断触发后将会自动装载新值reload_count
uint32_t auto_reload_on_alarm: 1;
} flags;
} gptimer_alarm_config_t;
4.2 gptimer_set_alarm_action
这个函数完成的功能也比较直观,就是将用户传入的警报配置记录到timer结构体中,并设定警报值、重新装载值,并使能对应的功能。
esp_err_t gptimer_set_alarm_action(gptimer_handle_t timer, const gptimer_alarm_config_t *config)
{
ESP_RETURN_ON_FALSE_ISR(timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
// 如果传入了有效警报配置
if (config) {
// 如果配置参数位于SRAM时的合法性检查
#if CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM
ESP_RETURN_ON_FALSE_ISR(esp_ptr_internal(config), ESP_ERR_INVALID_ARG, TAG, "alarm config struct not in internal RAM");
#endif
// 报警值和重新装载值不应该相等,否则中断不会停
bool valid_auto_reload = !config->flags.auto_reload_on_alarm || config->alarm_count != config->reload_count;
ESP_RETURN_ON_FALSE_ISR(valid_auto_reload, ESP_ERR_INVALID_ARG, TAG, "reload count can't equal to alarm count");
// 将传入的参数配置设置到timer结构体中
portENTER_CRITICAL_SAFE(&timer->spinlock);
timer->reload_count = config->reload_count;
timer->alarm_count = config->alarm_count;
timer->flags.auto_reload_on_alarm = config->flags.auto_reload_on_alarm;
timer->flags.alarm_en = true;
// 设置重新装载值和警报值
timer_ll_set_reload_value(timer->hal.dev, timer->timer_id, config->reload_count);
timer_ll_set_alarm_value(timer->hal.dev, timer->timer_id, config->alarm_count);
portEXIT_CRITICAL_SAFE(&timer->spinlock);
} else {
// 如果没有传入有效配置
portENTER_CRITICAL_SAFE(&timer->spinlock);
timer->flags.auto_reload_on_alarm = false;
timer->flags.alarm_en = false;
portEXIT_CRITICAL_SAFE(&timer->spinlock);
}
// 按照传入的配置,选择打开或者关闭timer的警报功能和自动装载功能
portENTER_CRITICAL_SAFE(&timer->spinlock);
timer_ll_enable_auto_reload(timer->hal.dev, timer->timer_id, timer->flags.auto_reload_on_alarm);
timer_ll_enable_alarm(timer->hal.dev, timer->timer_id, timer->flags.alarm_en);
portEXIT_CRITICAL_SAFE(&timer->spinlock);
return ESP_OK;
}
5 开启计数器
5.1 gptimer_start
这个函数的作用就是更改timer的状态机为GPTIMER_FSM_RUN,且开启计数器计时,注意在实践时不要将计数器的警报值和起始计数值设置的过于接近,否则CPU会被快速且多次触发的ISR抢占,而导致CPU出现卡死的假象。
esp_err_t gptimer_start(gptimer_handle_t timer)
{
ESP_RETURN_ON_FALSE_ISR(timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
gptimer_fsm_t expected_fsm = GPTIMER_FSM_ENABLE;
// 将timer状态机设置为GPTIMER_FSM_RUN_WAIT
if (atomic_compare_exchange_strong(&timer->fsm, &expected_fsm, GPTIMER_FSM_RUN_WAIT)) {
portENTER_CRITICAL_SAFE(&timer->spinlock);
// 开启timer的警报功能(操作冗余?)
timer_ll_enable_alarm(timer->hal.dev, timer->timer_id, timer->flags.alarm_en);
// <!开启计时...>
// IDF注释:如果alarm值设置的reload值比较接近,则有可能立即触发中断(立即进入gptimer_default_isr)
// gptimer的警报值如果设置的太过于接近于reload值,且开启自动装载
// 则程序可能出现"假死",即ISR一直占用CPU而不让出,程序始终处于快速进出ISR的状态
// <测试: 对于resolution_hz为1000000的场景下,这个差值大约为5>
timer_ll_enable_counter(timer->hal.dev, timer->timer_id, true);
// 设置timer状态机为GPTIMER_FSM_RUN
atomic_store(&timer->fsm, GPTIMER_FSM_RUN);
portEXIT_CRITICAL_SAFE(&timer->spinlock);
// 如果timer状态不是GPTIMER_FSM_ENABLE,则报错
} else {
ESP_RETURN_ON_FALSE_ISR(false, ESP_ERR_INVALID_STATE, TAG, "timer is not ready for a new start");
}
return ESP_OK;
}
Digression 2: gptimer中的有限状态机
在上述的代码梳理中,一直潜藏着一条暗线,就是timer外设的有限状态机状态更迭。为了方便掌握当前timer处于哪个状态(执行到了哪个函数中),IDF引入了一个有限状态机(Finite State Machine, FSM)管理当前timer的状态,并对timer的状态进行必要的检查和断言,总共可用的FSM状态有以下几个:
// timer的有限状态机可能处于的状态
typedef enum {
GPTIMER_FSM_INIT, // timer已经被初始化,但还没有使能(gptimer_new_timer中会初始化)
GPTIMER_FSM_ENABLE, // timer已经使能,但还没有开始计时
GPTIMER_FSM_ENABLE_WAIT, // timer正处于从计时到停止的中间状态中
GPTIMER_FSM_RUN, // timer正在计时
GPTIMER_FSM_RUN_WAIT, // timer正处于使能到开始计时的中间状态中
} gptimer_fsm_t;
作为总结,这里对其状态变化做一个总结和梳理,变化条件为其函数调用:
FIXME: Timer状态机的约束范围(idf v5.4)
在Review上述Timer代码时,我对FSM的作用范围有一些疑问,因为存在如下的逻辑:
- FSM是线性的,本质上它对GPTimer的使用顺序做了约束,比如IDF期待用户按照gptimer_new_timer -> gptimer_enable -> gptimer_start的顺序来配置和开启timer计时。
- 因为gptimer_enable出现在FSM检查链条中,所以它必须被调用,但是gptimer_enable函数中存在如下逻辑:
esp_err_t gptimer_enable(gptimer_handle_t timer)
{
// 以上代码从略
// 打开timer中断,这个函数要想执行成功,timer->intr不能为空
// 这反向约束了gptimer_register_event_callbacks必须在之前被调用,否则此处失败
if (timer->intr) {
ESP_RETURN_ON_ERROR(esp_intr_enable(timer->intr), TAG, "enable interrupt service failed");
}
return ESP_OK;
}
3.然而,gptimer_set_alarm_action函数却没有加入FSM状态检查,这意味着我们甚至可以不调用这个函数直接开启计时。(已验证:ESP32C6上略去此步骤并不会触发报错,回调函数也不会被调用)。既然,回调函数在这种(特殊)情况下不会被调用,那么gptimer_enable中开中断又有什么意义?
所以,处于逻辑上的完整性,IDF可以考虑将gptimer_set_alarm_action加入检查链条,确保报警被正常设置。
更多推荐



所有评论(0)