0.Briefly Speaking

这篇博客是ESP-IDF外设驱动系列的第二篇,这篇博客的研究对象是ESP32系列MCU中的定时器组,涉及到的主要参考资料和源代码内容如下

和之前的思路类似,首先以一份使用示例代码作为引子,随后深入研究定时器外设的使用方法和其驱动结构组织。

引子——如何使用一个Timer

以下代码展示了如何使用一个定时器(节选自gptimer_example_main.c),大体可以总结为以下几个步骤,以下的行文按照这几个步骤作为子标题,深入研究ESP32系列MCU的计数器使用方法及其IDF驱动组织结构,可以点击下方目录实现直接跳转。

  1. 分配并初始化一个新的定时器(gptimer_new_timer)
  2. 设置计数器报警回调函数(gptimer_register_event_callbacks)
  3. 使能计数器(gptimer_enable)
  4. 设置警报行为(gptimer_set_alarm_action)
  5. 开启计数器(gptimer_start)
  6. 停止及回收计数器(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完成的事情有以下几个方面,点击可跳转到对应的子标题:

这里只对其中一些比较重要的函数进行剖析,一些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的作用范围有一些疑问,因为存在如下的逻辑

  1. FSM是线性的,本质上它对GPTimer的使用顺序做了约束,比如IDF期待用户按照gptimer_new_timer -> gptimer_enable -> gptimer_start的顺序来配置和开启timer计时。
  2. 因为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加入检查链条,确保报警被正常设置。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐