硬汉嵌入式论坛

 找回密码
 立即注册
查看: 1138|回复: 13
收起左侧

[技术讨论] SysTick 驱动定时任务随机出现延时不稳定

[复制链接]

1

主题

16

回帖

19

积分

新手上路

积分
19
发表于 2024-10-28 01:27:24 | 显示全部楼层 |阅读模式
本帖最后由 etberzin 于 2024-10-28 17:21 编辑

用HK32F030M 驱动数码管,5 位数码管用定时任务动态刷新,每4ms 切换一次位选引脚,一轮刷新周期20ms。SysTick 中断周期10ms,所以小于10ms 的零头部分是毫秒时间戳函数根据SysTick 当前计数值VAL 计算的。问题就是时间似乎会随机的波动一下,导致本来4ms 延时缩短到不足1ms,数码管就会闪一下。我拿逻辑分析仪记录了三个位选引脚的驱动波形:


下载14.png

第0 通道的低电平本来应该持续4ms,但是它很快就起来了,导致整个刷新周期从20ms 变成了16ms。定时任务原理:

1. 初次调用时记录当前时间为T0;
2. 之后每次调用,用当前时间t 和T0 做差得到ΔT;
3. 如果ΔT > 4ms,则切换位选引脚,并更新T0;

主函数里不停的调用定时任务,就这么循环。要是我把SysTick 中断周期调成1ms,去掉根据VAL 计算零头的部分,数码管显示就完全没问题了。所以定时任务的原理应该是没问题的,不会受到计时变量溢出影响,有问题的就是计算零头的代码。但是现象太随机了,比如下面的波形:

下载2.png

第一张图里是在正常的20ms 周期中间发生了一次异常,而这张图却是有两次连续的异常,我都怀疑是不是硬件BUG。所以现在一筹莫展,打算之后在其他开发板上慢慢调试计算零头的代码。另外,我之前是用github 上找的perf_counter 库来提供时间戳的,就是因为数码管会不时闪一下,我才决定自己写时间戳。而且那个库里用int64_t 当计时变量,感觉还是挺少见的,换了我自己的代码,程序尺寸减少了差不多700 字节。




以下是源码(C++):

【SysTick 配置和时间计算函数】:

[C] 纯文本查看 复制代码

#include <hk32f030m.h>
#include <hk32f030m_gpio.h>
#include <hk32f030m_rcc.h>

#include <adc/cm_hk030.hpp>           // 我自己写的ADC 驱动
#include <ioxx_button_trigger.hpp>    //  按键消抖
#include <lipid_filter.hpp>                  // ADC 滤波器
#include <pin/cm_hk030.hpp>            // GPIO 引脚驱动
#include <scheduler_basic.hpp>          // 定时任务调度器
#include <scheduler_tick.hpp>


/* ================================== Systick 配置 ======================================= */

namespace scheduler_basic {


// #define _SCHEDULER_TICK_SYS_TICK_CYCLE_1_MS


#ifdef _SCHEDULER_TICK_SYS_TICK_CYCLE_N_MS

    static_assert(_SCHEDULER_TICK_SYS_TICK_CYCLE_N_MS > 0);
    constexpr uint32_t SYS_TICK_CYCLE_N_MS = _SCHEDULER_TICK_SYS_TICK_CYCLE_N_MS;

#elif defined(_SCHEDULER_TICK_SYS_TICK_CYCLE_1_MS)

    constexpr uint32_t SYS_TICK_CYCLE_N_MS = 1;  // 中断周期10 毫秒

#elif defined(_SCHEDULER_TICK_SYS_TICK_CYCLE_100_MS)

    constexpr uint32_t SYS_TICK_CYCLE_N_MS = 100;  // 中断周期100 毫秒

#else

    constexpr uint32_t SYS_TICK_CYCLE_N_MS = 10;  // 默认中断周期10 毫秒

#endif

    // 每毫秒SysTick 计数值,调用setup_systick 更新
    extern uint32_t global_variable_ticks_per_milli_second;

    // 每微秒SysTick 计数值,调用setup_systick 更新
    extern uint32_t global_variable_ticks_per_micro_second;


    // 根据指定的中断中期配置SysTick,使能SysTick 中断,用CPU 时钟驱动计数
    int setup_systick() {
        global_variable_ticks_per_micro_second = SystemCoreClock / 1000'000;

        global_variable_ticks_per_milli_second = global_variable_ticks_per_micro_second * 1000;

        uint32_t ticks = global_variable_ticks_per_milli_second * SYS_TICK_CYCLE_N_MS;

        return SysTick_Config(ticks);
    }


    extern volatile uint32_t global_variable_ms_counter;

    extern volatile uint32_t global_variable_us_counter;


    /**
     * @brief 在SysTick 中断里调用,更新毫秒计数器及精度补偿变量
     *
     */
    void systick_interrupt_counter_inc() {
        // 毫秒和微秒用两个计数变量
        // systick 中断优先级最低,被打断可能造成奇怪的BUG,所以最好是先关掉中断
        __disable_irq();

        global_variable_ms_counter += SYS_TICK_CYCLE_N_MS;
        global_variable_us_counter += SYS_TICK_CYCLE_N_MS * 1000;

        __enable_irq();
    }


    /**
     * @brief 获取毫秒时间戳,可在中断函数中使用
     *
     */
    uint32_t clock_ms() {
        /**
         * SYS_TICK_CYCLE_N_MS > 1 时,需要用SysTick 当前值VAL 做计算,补全不足SYS_TICK_CYCLE_N_MS 的零头部分。
         * 由于SysTick 计数器持续运行,计算中可能出现的问题有:
         *
         * 1.
         *
         */

        if constexpr (SYS_TICK_CYCLE_N_MS > 1) {
            // SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;  // 暂停SysTick
            __disable_irq();

            uint32_t v0 = SysTick->VAL;
            uint32_t ms = global_variable_ms_counter;
            uint32_t v = SysTick->VAL;


            // uint32_t v1 = SysTick->VAL;

            // if(NVIC_GetPendingIRQ(SysTick_IRQn)) {
            //     ms +=SYS_TICK_CYCLE_N_MS;
            // }

            __enable_irq();

            if (v > v0) {
                ms += SYS_TICK_CYCLE_N_MS;
            }

            // uint32_t v1 = SysTick->VAL;

            // SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;  // 重开


            uint32_t ticks_per_cycle = SysTick->LOAD + 1;
            ms += (ticks_per_cycle - v) / global_variable_ticks_per_milli_second;
            return ms;
        }
        else {
            return global_variable_ms_counter;
        }
    }


    /**
     * @brief 获取微秒时间戳
     *
     */
    uint32_t clock_us() {
        __disable_irq();

        uint32_t us = global_variable_us_counter;
        uint32_t v = SysTick->VAL;

        if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) {
            us += SYS_TICK_CYCLE_N_MS * 1000;
        }

        __enable_irq();

        uint32_t ticks_per_cycle = SysTick->LOAD + 1;
        us += (ticks_per_cycle - v) / global_variable_ticks_per_micro_second;
        return us;
    }


    /**
     * @brief 获取CPU 时钟周期数, 可在中断函数中使用
     *
     * 最大计时周期等于SysTick 中断周期
     *
     * @return uint32_t
     */
    uint32_t clock_ticks() {
        // 把Systick 计数变成从小到大增加的顺序,方便使用
        return SysTick->LOAD - SysTick->VAL;
    }


    uint32_t ticks_to_ms(uint32_t ticks) {
        return ticks / global_variable_ticks_per_milli_second;
    }


    uint32_t ticks_to_us(uint32_t ticks) {
        return ticks / global_variable_ticks_per_micro_second;
    }


    // 以下都是给定时调度器用的时钟源
    
    struct SysTickMsSource {
        inline static auto get_time() {
            return clock_ms();
        }

        using TimeType = uint32_t;
    };


    struct SysTickUsSource {
        inline static auto get_time() {
            return clock_us();
        }

        using TimeType = uint32_t;
    };


    struct SysTickTicksSource {
        inline static auto get_time() {
            return clock_ticks();
        }

        using TimeType = uint32_t;
    };

}  // namespace scheduler_basic




【SysTick 中断函数】:

[C] 纯文本查看 复制代码

extern "C" {
// 如果中断函数直接放在cpp 文件,由于C++ 编译器会修改函数名,就和中断向量表的命名不匹配了


__attribute__((used)) void SysTick_Handler() {
    // user_code_insert_to_systick_handler();
    scheduler_basic::systick_interrupt_counter_inc();
}
}




【main.cpp 全部代码,包含上面的SysTick 部分】:

[C] 纯文本查看 复制代码

#include <hk32f030m.h>
#include <hk32f030m_gpio.h>
#include <hk32f030m_rcc.h>

#include <adc/cm_hk030.hpp>
#include <ioxx_button_trigger.hpp>
#include <lipid_filter.hpp>
#include <pin/cm_hk030.hpp>
#include <scheduler_basic.hpp>
#include <scheduler_tick.hpp>


/* ================================== Systick 配置 ======================================= */

namespace scheduler_basic {


// #define _SCHEDULER_TICK_SYS_TICK_CYCLE_1_MS


#ifdef _SCHEDULER_TICK_SYS_TICK_CYCLE_N_MS

    static_assert(_SCHEDULER_TICK_SYS_TICK_CYCLE_N_MS > 0);
    constexpr uint32_t SYS_TICK_CYCLE_N_MS = _SCHEDULER_TICK_SYS_TICK_CYCLE_N_MS;

#elif defined(_SCHEDULER_TICK_SYS_TICK_CYCLE_1_MS)

    constexpr uint32_t SYS_TICK_CYCLE_N_MS = 1;  // 中断周期10 毫秒

#elif defined(_SCHEDULER_TICK_SYS_TICK_CYCLE_100_MS)

    constexpr uint32_t SYS_TICK_CYCLE_N_MS = 100;  // 中断周期100 毫秒

#else

    constexpr uint32_t SYS_TICK_CYCLE_N_MS = 10;  // 默认中断周期10 毫秒

#endif

    // 每毫秒SysTick 计数值,调用setup_systick 更新
    extern uint32_t global_variable_ticks_per_milli_second;

    // 每微秒SysTick 计数值,调用setup_systick 更新
    extern uint32_t global_variable_ticks_per_micro_second;


    // 根据指定的中断中期配置SysTick,使能SysTick 中断,用CPU 时钟驱动计数
    int setup_systick() {
        global_variable_ticks_per_micro_second = SystemCoreClock / 1000'000;

        global_variable_ticks_per_milli_second = global_variable_ticks_per_micro_second * 1000;

        uint32_t ticks = global_variable_ticks_per_milli_second * SYS_TICK_CYCLE_N_MS;

        return SysTick_Config(ticks);
    }


    extern volatile uint32_t global_variable_ms_counter;

    extern volatile uint32_t global_variable_us_counter;


    /**
     * @brief 在SysTick 中断里调用,更新毫秒计数器及精度补偿变量
     *
     */
    void systick_interrupt_counter_inc() {
        // 毫秒和微秒用两个计数变量
        // systick 中断优先级最低,被打断可能造成奇怪的BUG,所以最好是先关掉中断
        __disable_irq();

        global_variable_ms_counter += SYS_TICK_CYCLE_N_MS;
        global_variable_us_counter += SYS_TICK_CYCLE_N_MS * 1000;

        __enable_irq();
    }


    /**
     * @brief 获取毫秒时间戳,可在中断函数中使用
     *
     */
    uint32_t clock_ms() {
        /**
         * SYS_TICK_CYCLE_N_MS > 1 时,需要用SysTick 当前值VAL 做计算,补全不足SYS_TICK_CYCLE_N_MS 的零头部分。
         * 由于SysTick 计数器持续运行,计算中可能出现的问题有:
         *
         * 1.
         *
         */

        if constexpr (SYS_TICK_CYCLE_N_MS > 1) {
            // SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;  // 暂停SysTick
            __disable_irq();

            uint32_t v0 = SysTick->VAL;
            uint32_t ms = global_variable_ms_counter;
            uint32_t v = SysTick->VAL;


            // uint32_t v1 = SysTick->VAL;

            // if(NVIC_GetPendingIRQ(SysTick_IRQn)) {
            //     ms +=SYS_TICK_CYCLE_N_MS;
            // }

            __enable_irq();

            if (v > v0) {
                ms += SYS_TICK_CYCLE_N_MS;
            }

            // uint32_t v1 = SysTick->VAL;

            // SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;  // 重开


            uint32_t ticks_per_cycle = SysTick->LOAD + 1;
            ms += (ticks_per_cycle - v) / global_variable_ticks_per_milli_second;
            return ms;
        }
        else {
            return global_variable_ms_counter;
        }
    }


    /**
     * @brief 获取微秒时间戳
     *
     */
    uint32_t clock_us() {
        __disable_irq();

        uint32_t us = global_variable_us_counter;
        uint32_t v = SysTick->VAL;

        if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) {
            us += SYS_TICK_CYCLE_N_MS * 1000;
        }

        __enable_irq();

        uint32_t ticks_per_cycle = SysTick->LOAD + 1;
        us += (ticks_per_cycle - v) / global_variable_ticks_per_micro_second;
        return us;
    }


    /**
     * @brief 获取CPU 时钟周期数, 可在中断函数中使用
     *
     * 最大计时周期等于SysTick 中断周期
     *
     * @return uint32_t
     */
    uint32_t clock_ticks() {
        // 把Systick 计数变成从小到大增加的顺序,方便使用
        return SysTick->LOAD - SysTick->VAL;
    }


    uint32_t ticks_to_ms(uint32_t ticks) {
        return ticks / global_variable_ticks_per_milli_second;
    }


    uint32_t ticks_to_us(uint32_t ticks) {
        return ticks / global_variable_ticks_per_micro_second;
    }


    // 以下都是给定时调度器用的时钟源
    
    struct SysTickMsSource {
        inline static auto get_time() {
            return clock_ms();
        }

        using TimeType = uint32_t;
    };


    struct SysTickUsSource {
        inline static auto get_time() {
            return clock_us();
        }

        using TimeType = uint32_t;
    };


    struct SysTickTicksSource {
        inline static auto get_time() {
            return clock_ticks();
        }

        using TimeType = uint32_t;
    };

}  // namespace scheduler_basic


#include <cstdint>

namespace scheduler_basic {

    uint32_t global_variable_ticks_per_milli_second;

    uint32_t global_variable_ticks_per_micro_second;

    volatile uint32_t global_variable_ms_counter;

    volatile uint32_t global_variable_us_counter;
}  // namespace scheduler_basic


// using TimeSource = scheduler_basic::PerfCounterMsSource;
// using TimeType = TimeSource::TimeType;

using TimeSource = scheduler_basic::SysTickMsSource;
using TimeType = TimeSource::TimeType;

// 初始化定时调度器对象,最大任务数10
namespace sb = scheduler_basic;
sb::DelayCallback3<TimeSource, 10> dcall;


/* ================================== 数码管引脚定义 ======================================= */

constexpr size_t DIG_COUNT = 5;
constexpr size_t SEG_COUNT = 8;

// 数码管对应8 + 5 一共13 个引脚,全都是推挽输出模式,
// 用union 把seg 引脚和dig 引脚的数组连到一个长度13 的数组all_pin 里
// 方便批量设置GPIO 模式
const union {
    ioxx::Pin all_pin[SEG_COUNT + DIG_COUNT];

    struct {
        ioxx::Pin seg[SEG_COUNT];
        ioxx::Pin dig[DIG_COUNT];
    } pin;

} TUBE = {
    .pin = {

        // ======== 共阴数码管8 段引脚 ==========
        // 用一个数组把八个段引脚装一起方便处理
        // 8 段对应一个字节,按顺序,A 是LSB,DP 是MSB
        // 所以SEG_A 在数组里是是[0],
        .seg = {
            ioxx::Pin(GPIOD, 1),  // 数码管SEG A 引脚,连接到阳极
            ioxx::Pin(GPIOC, 7),  // SEG B
            ioxx::Pin(GPIOC, 6),  // SEG C
            ioxx::Pin(GPIOC, 5),  // SEG D
            ioxx::Pin(GPIOC, 4),  // SEG E
            ioxx::Pin(GPIOC, 3),  // SEG F
            ioxx::Pin(GPIOB, 4),  // SEG G
            ioxx::Pin(GPIOD, 6),  // 小数点 DP
        },

        // ======= 共阴数码管5 位COM 引脚 =============
        // 连接到数码管阴极,所以低电平有效
        // 同样装在一个数组里,[0] 对应左侧数码管第一位
        // DIG4 同时作为按键输入引脚,要设置为开漏输出,且启用内部下拉,以读取按键输入的高电平信号
        .dig = {
            ioxx::Pin(GPIOA, 3),  // DIG3
            ioxx::Pin(GPIOD, 4),  // DIG4

            ioxx::Pin(GPIOA, 0),  // DIG0
            ioxx::Pin(GPIOA, 1),  // DIG1
            ioxx::Pin(GPIOA, 2),  // DIG2
        }}};


constexpr size_t DIG4_KEY_POS = 1;  // DIG4 在dig 数组中的位置

/* =============================================== 数码管引脚操作 ============================================ */


/** 初始化所有数码管引脚为输出模式,并将位选引脚设为高电平,关闭数码管显示
 *
 * 引脚直接驱动数码管需要比较大的电流,驱动速度应该选比较高的等级
 */
void init_tube_pin() {
    using namespace ioxx;
    PinInit pi;
    pi.mode(mode::out).drive(drive::push_pull);

    // 设置所有引脚为推挽输出
    for (const Pin& p : TUBE.all_pin) {
        pi.init(p);

        // 拉高所有引脚,默认关闭数码管
        p.set();
    }

    // 单独设置DIG4 为开漏输出
    pi.drive(drive::open_drain).pull(pull::down);
    pi.init(TUBE.pin.dig[DIG4_KEY_POS]);
}


/** 用一个字节按顺序设置数组内引脚的输出电平,数组[0] 对应LSB
 */
void write_pins_by_byte(const ioxx::Pin* pins, size_t pins_count, uint8_t b) {
    for (; pins_count > 0; --pins_count) {
        if (0x01 & b) {
            pins->set();
        }
        else {
            pins->clr();
        }

        b = b >> 1;
        ++pins;
    }
}


/**
 * @brief 写入段选引脚,SEG A 对应[0]
 *
 * @param b
 */
void write_seg_by_byte(uint8_t b) {
    write_pins_by_byte(TUBE.pin.seg, SEG_COUNT, b);
}


/**
 * @brief 写入位选引脚,DIG0 对应[0]
 *
 * @param b
 */
void write_dig_by_byte(uint8_t b) {
    write_pins_by_byte(TUBE.pin.dig, DIG_COUNT, b);
}


void nrst_switch_to_pa0() {
    // 切换NRST 为PA0
    RCC->APB1ENR |= RCC_APB1ENR_IOMUXEN;
    GPIOMUX->NRST_PIN_KEY = 0x5AE1;
    GPIOMUX->NRST_PA0_SEL = 1;
}


/* ================================== 按键处理 ======================================= */


// 实现按键消抖和点击、长按逻辑
static ioxx::ButtonTrigger<ioxx::polarity::high> button_trigger;


/* ================================== 数码管显示操作 ======================================= */

// 共阴数码管码表:
//                                       0      1     2    3     4      5      6     7     8    9
constexpr static uint8_t TUBE_CODE[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};

constexpr uint8_t TUBE_CODE_DP_MASK = 0x80;  // 添加小数点的bit MASK
constexpr uint8_t TUBE_CODE_NONE = 0x00;     // 无显示
constexpr uint8_t TUBE_CODE_MINUS = 0x40;    // 显示中间横杠,或负号
constexpr uint8_t TUBE_CODE_U = 0x3E;        // 显示字母U
constexpr uint8_t TUBE_CODE_J = 0x0E;        // 显示字母J
constexpr uint8_t TUBE_CODE_P = 0x73;
constexpr uint8_t TUBE_CODE_A = 0x77;
constexpr uint8_t TUBE_CODE_C = 0x39;
constexpr uint8_t TUBE_CODE_L = 0x38;


// 显示缓冲区
// 0, 1 对应左侧两位的数码管;2, 3, 4 对应右侧的三位
static uint8_t display_buffer[DIG_COUNT];

// 数码管单个位的刷新周期,如果周期是20ms,则每个位都以50Hz 频率闪烁
// 20ms 要平分给5 位数码管,所以每个位点亮4ms 然后就熄灭16ms,切换到其他位,所以位的切换周期是4ms,频率250Hz。
// 刷新周期太大会让数码管看起来有明显的闪烁,太小切换太频繁,又会占用过多CPU 时间,50Hz 一般应该没问题
constexpr TimeType DISPLAY_REFRESH_PERIOD = 20;

// 位选的切换周期,5 个位平分整个刷新周期,所以就是刷新周期除以5
constexpr TimeType DIG_SWITCH_PERIOD = DISPLAY_REFRESH_PERIOD / DIG_COUNT;

// 按键扫描周期大于等于7ms
constexpr TimeType KEY_SCAN_PERIOD = 9;


/** 定时刷新数码管的延时任务,同时负责定时读取按键
 *
 * 所有SEG 引脚经过数码管中的二极管和各个DIG 引脚连接,
 * 所以要从DIG4 读取按键输入,必须先将所有SEG 拉低,去除干扰
 *
 */
TimeType callback_task_cycle_display() {
    static size_t dig_index = 0;

    static bool is_time_to_key_scan = true;

    write_seg_by_byte(0x00);  // 先将SEG 全部拉低

    // 切换位选引脚,拉高上一个位选,之后再拉低当前位选
    TUBE.pin.dig[dig_index].set();

    ++dig_index;
    if (dig_index >= DIG_COUNT) {
        dig_index = 0;
    }

    // 到时间扫描按键
    if (is_time_to_key_scan) {
        is_time_to_key_scan = false;

        // 添加定时任务,到时间再通知扫描按键
        dcall.add_task(

            []() -> TimeType {
                is_time_to_key_scan = true;
                return 0;
            },

            KEY_SCAN_PERIOD);

        // 直接读取DIG4 的电平,因为此时所有DIG 引脚都是拉高的状态,
        // 不必判断当前是哪个DIG 引脚。开漏输出引脚拉高时可以直接读取输入电平
        button_trigger.feed(TUBE.pin.dig[DIG4_KEY_POS].get());
    }

    // 拉低当前位选
    TUBE.pin.dig[dig_index].clr();
    write_seg_by_byte(display_buffer[dig_index]);  // 写入显示值

    return DIG_SWITCH_PERIOD;
}


/* ================================== 显示数据操作 ======================================= */

enum class display_mode {
    u_j = 0,  // 左侧两位数码管显示U 表示电压,右侧三位显示J,表示电流
    u_p = 1,  // 两位显示电压U,三位显示功率P
    j_u = 2,  // 反过来,用三位显示电压,两位显示电流

    // TODO:
    cal = 3,  // 校准模式
};


constexpr int DISPLAY_MODE_NUM_MIN = 0;
constexpr int DISPLAY_MODE_NUM_MAX = 3;


display_mode current_display_mode = display_mode::u_j;


/**
 * @brief 存储4 个msg 对应的显示值的数组,内层数组顺序和display_buffer 相同
 */
constexpr static uint8_t MSG_DISPLAY_CODE[4][DIG_COUNT] = {
    {0X00, TUBE_CODE_U, 0x00, TUBE_CODE_J, 0X00},  // 0: U J
    {0X00, TUBE_CODE_U, 0X00, TUBE_CODE_P, 0X00},  // 1: U P
    {0X00, TUBE_CODE_J, 0X00, TUBE_CODE_U, 0X00},  // 2: J U

    {0X00, 0X00, TUBE_CODE_C, TUBE_CODE_A, TUBE_CODE_L},  // 3: CAL
};


// is_messaging 为true 时,表示数码管正用于显示静态的状态信息,一段时间内禁止更新显示缓冲区
// 测量数据会被丢弃,不显示
static bool is_messaging = false;


/**
 * @brief 将选定的msg 送进显示缓冲区,进入messaging 状态,一段时间内不会更新显示数据
 *
 * 如果在messaging 状态再次发出新的msg,则更新显示,倒计时重置
 *
 * @param t
 */
void show_mode_message() {
    // 数码管显示状态信息的时间,超过时间后,重新开始显示测量数据
    constexpr TimeType MSG_DURATION = 1000;

    static uint8_t task_index;

    auto msg_code = MSG_DISPLAY_CODE[static_cast<int>(current_display_mode)];

    for (int i = 0; i < DIG_COUNT; ++i) {
        display_buffer[i] = msg_code[i];
    }

    if (!is_messaging) {
        // 如果当前不是messaging 状态,则进入messaging
        is_messaging = true;

        /** 添加延时回调,定时退出messaging 状态
         */
        task_index = dcall.add_task(

            []() -> TimeType {
                is_messaging = false;
                return 0;
            },

            MSG_DURATION);
    }
    else {
        // 如果已经是messaging 状态,则重置任务的倒计时,
        // 避免重复多次添加任务把调度挤满
        dcall.reset_task(task_index, MSG_DURATION);
    }
}


// 进入U_P 模式前的状态
static display_mode last_mode_before_u_p;

/**
 * @brief 在U_J 和J_U 模式中切换,然后调用show_mode_message
 *
 * 如果当前模式是U_P 则切换为进入U_P 模式之前的状态
 *
 */
void cycle_u_j_mode() {
    if (current_display_mode == display_mode::u_p) {
        current_display_mode = last_mode_before_u_p;
    }
    else if (current_display_mode == display_mode::j_u) {
        current_display_mode = display_mode::u_j;
    }
    else {
        current_display_mode = display_mode::j_u;
    }

    show_mode_message();
}


/**
 * @brief 切换为U_P 模式
 *
 */

void switch_to_u_p_mode() {
    if (current_display_mode != display_mode::u_p) {
        last_mode_before_u_p = current_display_mode;
        current_display_mode = display_mode::u_p;
    }

    show_mode_message();
}


/**
 * @brief 将mA ,mV ,mW数值转换成A ,V 或W 的显示数据,添加小数点
 *
 * 数值不能是负数,如果数值大于9V 或9A,则结果保留一位小数,否则保留两位小数。
 * 功率值有可能超过99W,没有小数部分
 * 计算得到三位数字结果后,再转换成数码管显示值,然后依次送进buf 指针指定显示缓冲区位置
 *
 * @param num
 * @param buf
 */
void milli_num_to_3_digit(uint32_t num, uint8_t* buf) {
    // 在messaging 状态不更新显示值
    if (is_messaging) {
        return;
    }

    uint8_t temp_buf[5] = {0};
    uint_fast8_t c;

    // 从十位数开始,将num 拆解成5 个显示值
    for (int_fast8_t i = 4; i >= 0; --i) {
        num /= 10;
        c = num % 10;
        temp_buf[i] = TUBE_CODE[c];
    }

    // 添加小数点
    temp_buf[2] |= TUBE_CODE_DP_MASK;

    // 找出不为0 的第一个数的位置, 带小数点的0 != 0
    uint8_t* start_pos = temp_buf;
    while (*start_pos == TUBE_CODE[0]) {
        ++start_pos;
    }

    for (uint_fast8_t i = 0; i < 3; ++i) {
        buf[i] = start_pos[i];
    }
}


/**
 * @brief 将数值送入显示缓冲区
 *
 * @param num 单位是mV,最大不超过99V
 */
void show_milli_volt_amp_watt(uint32_t volt, uint32_t amp) {
    // 如果是U_J 或U_P 模式,电压在左侧,显示两位数
    // 所以milli_num_to_3_digit 从显示缓冲区[0] 开始写入3 个转换值
    // 写在[2] 处的多余的1 个数在之后显示J 或P 时覆盖掉
    uint32_t left = volt;
    if (current_display_mode == display_mode::j_u) {
        left = amp;
    }

    milli_num_to_3_digit(left, display_buffer);

    uint32_t right = amp;
    if (current_display_mode == display_mode::j_u) {
        right = volt;
    }
    else if (current_display_mode == display_mode::u_p) {
        right = volt * amp / 1000;
    }

    milli_num_to_3_digit(right, &display_buffer[2]);
}


/* ==================================== ADC 输入 ========================================= */

// 电压引脚,连接分压器
const ioxx::Pin V_SENSE_PIN{GPIOD, 2};
constexpr auto V_SENSE_CHANNEL = adxx::channel::ain4;

// 电流引脚,连接运放
const ioxx::Pin I_SENSE_PIN{GPIOD, 3};
constexpr auto I_SENSE_CHANNEL = adxx::channel::ain3;


static adxx::Adc adcc;

void init_adc_pin() {
    using namespace ioxx;
    PinInit()
        .pull(pull::none)
        .mode(mode::analog)
        .init(I_SENSE_PIN)
        .init(V_SENSE_PIN);
}


void init_adc() {
    // 不连续模式
    adcc.init(adxx::adc_mode::discontinuous);

    // 使能ADC 通道
    // 反向转换时,次序是: 0 电压检测 1 电流检测
    adcc.set_channel({I_SENSE_CHANNEL, V_SENSE_CHANNEL});

    // 设置ADC 系数固定为3.3V / 4095
    // 用内置参考电压测出来的系数偏小,不如简单的用外部3.3V 电源
    adcc.ad_factor_by_full_scale(static_cast<uint32_t>(3.3E6));
}


/* ==================================== 数据处理 ========================================= */

using FilterValueType = uint32_t;
using FilterType = lipid::MovingAverageFilter<FilterValueType, FilterValueType, 16>;

// 电压、电流、参考电压采样值滑动平均滤波器,窗口尺寸 == 16
static FilterType adc_filter[2];

FilterType& v_filter = adc_filter[0];
FilterType& i_filter = adc_filter[1];


// 经过滤波后的ADC 采样原始值
static FilterValueType adc_avg[2] = {0};

FilterValueType& v_avg = adc_avg[0];
FilterValueType& i_avg = adc_avg[1];


// 显示数据更新周期设为300ms,毕竟数字变得太快了眼睛看不过来
// 这期间持续采样并滑动平均滤波

TimeType callback_task_adc_update() {
    // 每10 毫秒查询一次ADC 结果,3 个通道一共30 毫秒,足以在300ms 刷新周期内全部采样8 次
    constexpr TimeType ADC_UPDATE_PERIOD = 10;

    auto ch_index = adcc.completed();
    if (ch_index >= 0) {
        // 已转换完成,将ADC 值送进滤波器,获取滤波结果

        adc_avg[ch_index] = adc_filter[ch_index].feed(adcc.fetch_real());

        // 继续启动转换
        adcc.start();
    }

    return ADC_UPDATE_PERIOD;
}


TimeType callback_task_display_data_update() {
    constexpr TimeType DISPLAY_DATA_UPDATE_PERIOD = 300;

    uint32_t milli_v = v_avg / 1000 * 10;
    uint32_t milli_i = i_avg / 1000;

    milli_v += 200;  // 电压测量值似乎比实际值总是低0.2V(200mV),所以补偿一下

    // 电压测量电路有10 倍衰减,导致输入信号比较小,比如从5V 到9V,ADC 输入变化只有0.4V,
    // 所以电压测量精度比电流精度可能更低

    show_milli_volt_amp_watt(milli_v, milli_i);

    return DISPLAY_DATA_UPDATE_PERIOD;
}


/* ============================================ M  I  A  N  &#128064;&#128064;&#129781;&#128074;============================================ */

/** 初始化代码
 */
void hw_setup() {
    // "在使用进入 stop 低功耗模式时,一定要记得将 PWR 时钟打开"
    // "没有打开 PWR 时钟,则会功能异常,且不能再次烧录程序"
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);

    scheduler_basic::setup_systick();

    // 启动中断
    __enable_irq();

    // 配置GPIO
    ioxx::enable_gpio();
    nrst_switch_to_pa0();

    // 数码管引脚初始化
    init_tube_pin();

    // ADC 初始化
    init_adc_pin();
    init_adc();
}


void setup() {
    hw_setup();

    // 启动ADC
    adcc.start();

    // 添加不会终止的定时任务
    // 调度器最大容量10 绰绰有余
    dcall.add_task(callback_task_cycle_display, 0);        // 数码管刷新任务
    dcall.add_task(callback_task_adc_update, 0);           // ADC 定时采样、滤波任务
    dcall.add_task(callback_task_display_data_update, 0);  // 显示数据刷新任务

    // 上电后默认先显示消息,指示当前模式
    show_mode_message();
    // TODO: 存储用户选择的模式
}

// 方便调试器查看时间变量
volatile uint32_t main_ms;
volatile uint32_t ms_diff = main_ms - scheduler_basic::global_variable_ms_counter;

int main(void) {
    setup();

    // sb::TimeCycle<TimeSource> c;
    while (1) {
        main_ms = scheduler_basic::clock_ms();

        dcall.tick();

        // ====== 处理按键 =======
        // 单击切换电压、电流显示位置
        if (button_trigger.clicked()) {
            cycle_u_j_mode();
        }

        // 长按显示功率
        if (button_trigger.long_pressed()) {
            switch_to_u_p_mode();
        }
    }
}


extern "C" {
// 如果中断函数直接放在cpp 文件,由于C++ 编译器会修改函数名,就和中断向量表的命名不匹配了


__attribute__((used)) void SysTick_Handler() {
    // user_code_insert_to_systick_handler();
    scheduler_basic::systick_interrupt_counter_inc();
}
}


#ifdef USE_FULL_ASSERT
/**
 * @brief  Reports the name of the source file and the source line number
 *         where the assert_param error has occurred.
 * @param  file: pointer to the source file name
 * @param  line: assert_param error line source number
 * @retval None
 */
void assert_failed(char* file, uint32_t line) {
    /* User can add his own implementation to report the file name and line number,
       tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
    /* Infinite loop */

    while (1) {
    }
}
#endif /* USE_FULL_ASSERT */


回复

使用道具 举报

1万

主题

7万

回帖

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
117512
QQ
发表于 2024-10-28 11:44:04 | 显示全部楼层
楼主的代码内容有点多,不太好分析。针对楼主的问题,下面几个方向查找过了没。
1、其他中断都不开,仅开这个滴答中断是否正常。
2、如果第1步已经排查了,得排查下你的代码实现了,包括编译器优化。
回复

使用道具 举报

1

主题

16

回帖

19

积分

新手上路

积分
19
 楼主| 发表于 2024-10-28 16:25:07 | 显示全部楼层
eric2013 发表于 2024-10-28 11:44
楼主的代码内容有点多,不太好分析。针对楼主的问题,下面几个方向查找过了没。
1、其他中断都不开,仅开 ...

涉及毫秒时间计算的只有clock_ms 里的这部分代码:

[C] 纯文本查看 复制代码
 

// 关中断读取当前计数值
 __disable_irq();

uint32_t v0 = SysTick->VAL;
uint32_t ms = global_variable_ms_counter;
uint32_t v = SysTick->VAL;

__enable_irq();

if (v > v0) {
ms += SYS_TICK_CYCLE_N_MS;   // SysTick 是递减计数,如果v 大于v0,说明计数发生了一次溢出,ms 加上一个计数周期
}


uint32_t ticks_per_cycle = SysTick->LOAD + 1;   // 根据SysTick 计数最大值计算零头
ms += (ticks_per_cycle - v) / global_variable_ticks_per_milli_second;
return ms;



另外,我加的波形图都没显示,哪里没整对吗?
回复

使用道具 举报

1

主题

16

回帖

19

积分

新手上路

积分
19
 楼主| 发表于 2024-10-28 17:01:36 | 显示全部楼层
本帖最后由 etberzin 于 2024-10-28 17:27 编辑
eric2013 发表于 2024-10-28 11:44
楼主的代码内容有点多,不太好分析。针对楼主的问题,下面几个方向查找过了没。
1、其他中断都不开,仅开 ...

似乎搞定了,把毫秒计算代码改成了这样:

[C] 纯文本查看 复制代码
            

            uint32_t ms = global_variable_ms_counter;
            uint32_t v = SysTick->VAL;


            // uint32_t v1 = SysTick->VAL;

            // if(NVIC_GetPendingIRQ(SysTick_IRQn)) {
            //     ms +=SYS_TICK_CYCLE_N_MS;
            // }

            // __enable_irq();

            if (v < 300) {
                // SysTick 是递减计数,如果v 大于v0,说明计数刚刚发生了一次溢出,ms 加上一个计数周期
                ms += SYS_TICK_CYCLE_N_MS;
            }
            else {
                // 否则让ms 加上零头
                uint32_t ticks_per_cycle = SysTick->LOAD + 1;
                ms += (ticks_per_cycle - v) / global_variable_ticks_per_milli_second;
            }



甚至不用关中断,反正就是不闪了,到底怎么样还得拿逻辑分析仪看看。思路是,如果v < 300,说明SysTick 马上就要溢出了。但是如果把300 改成200 就还是会闪,搞不懂原因,可能是和编译优化有关。这整个计算函数都被内联到了调用的地方,如果v < 200 ,反汇编窗口看到在CMP 之前少了一条LSRS 指令。总之是似乎能用了,但是不知道具体怎么就能用了……

【小于200 时】:

小于200.png

【小于500 时】:

小于500.png

回复

使用道具 举报

1

主题

16

回帖

19

积分

新手上路

积分
19
 楼主| 发表于 2024-10-28 19:15:21 | 显示全部楼层
不,还是不行,不动它的时候计时相当稳定,但是每次操作按键后就一定会闪几下,估计是和主程序的循环周期有关系,操作按键的时候主程序要花时间做点额外的工作
回复

使用道具 举报

1

主题

16

回帖

19

积分

新手上路

积分
19
 楼主| 发表于 2024-10-28 20:58:33 | 显示全部楼层
eric2013 发表于 2024-10-28 11:44
楼主的代码内容有点多,不太好分析。针对楼主的问题,下面几个方向查找过了没。
1、其他中断都不开,仅开 ...

W超,这波可能真的搞定了。在毫秒计算函数里增加一个静态变量,用来记录上一次调用时返回的时间戳:

[C] 纯文本查看 复制代码
    
            static uint32_t last_ms = 0;
            uint32_t ms = global_variable_ms_counter;
            uint32_t v = SysTick->VAL;

            uint32_t ticks_per_cycle = SysTick->LOAD + 1;
            ms += (ticks_per_cycle - v) / global_variable_ticks_per_milli_second;

            if(last_ms > ms && (last_ms - ms) < (SYS_TICK_CYCLE_N_MS)) {
                // 如果这次时间比上次小,返回上次时间
                // 如果ms 发生了溢出,那么
                ms = last_ms;
            }
            else {
                last_ms = ms;
            }

            return ms;



基本问题就是在读这两个值之间:

[C] 纯文本查看 复制代码
            

uint32_t ms = global_variable_ms_counter;
uint32_t v = SysTick->VAL;



有可能VAL 已经归零并重装,于是后面计算出来的零头也变成0,导致前一次调用时间可能是19,后一次就变成10,于是做差得到的ΔT = 10 - 19,溢出以后变成很大的值,延时任务就提前执行了。这个原理我一开始就想到了,之前的几个版本代码都是要解决这个问题,试了各种方法检测SysTick->VAL 有没有重装,但是检查NVIC 的pending bit 或者检查SysTick->CTRL 里的COUNTFLAG 都没用,比如:

[C] 纯文本查看 复制代码
 

           if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) {
                ms += SYS_TICK_CYCLE_N_MS;
            }
            else {
                // 否则让ms 加上零头
                uint32_t ticks_per_cycle = SysTick->LOAD + 1;
                ms += (ticks_per_cycle - v) / global_variable_ticks_per_milli_second;
            }



闪还是闪,不知道是硬件标志位有延迟还是怎样。于是最后改成记录上一次时间戳last_ms,用VAL 计算零头加到ms 以后,如果ms 还比last_ms 小,比如10 < 19,那就说明VAL 重装了,暂时让ms 不变,与last_ms 相同,返回19,避免出现10 - 19 的情况。

如果ms 在加上零头以后发生溢出,也会导致last_ms > ms,所以要增加个条件,要求last_ms - ms 小于SysTick 一个中断周期的毫秒数,也就是零头计算结果的最大值。这样如果是溢出了,只要上一次给last_ms 赋值的的间隔不太久,last_ms 肯定也接近溢出,也就是个很大的值,last-ms - ms 肯定比中断周期要大。而如果last_ms 是很久以前赋值的,也不会发生10-19 的情况。

如果 ms 是在加零头之前已经溢出了,也就是毫秒计数变量global_variable_ms_counter 发生了溢出。同样是两种情况:1. last_ms 是很久以前赋值;2. last_ms 刚刚赋值。刚刚赋值的情况和之前一样,不会有问题;很久以前赋值的话,感觉可能有BUG,还没想好。

回复

使用道具 举报

1万

主题

7万

回帖

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
117512
QQ
发表于 2024-10-29 09:41:54 | 显示全部楼层
etberzin 发表于 2024-10-28 20:58
W超,这波可能真的搞定了。在毫秒计算函数里增加一个静态变量,用来记录上一次调用时返回的时间戳:

[ ...

总的来说你的是溢出问题没有处理好吧,如果是这个没有处理好,使用我这个微秒延迟的思路解决就行,核心下就是:两个32位无符号数相减,获取的结果再赋值给32位无符号数依然可以正确的获取差值

[C] 纯文本查看 复制代码
/*
*********************************************************************************************************
*   函 数 名: bsp_DelayUS
*   功能说明: 这里的延时采用CPU的内部计数实现,32位计数器
*               OSSchedLock(&err);
*               bsp_DelayUS(5);
*               OSSchedUnlock(&err); 根据实际情况看看是否需要加调度锁或选择关中断
*   形    参: _ulDelayTime  延迟长度,单位1 us
*   返 回 值: 无
*   说    明: 1. 主频168MHz的情况下,32位计数器计满是2^32/168000000 = 25.565秒
*                建议使用本函数做延迟的话,延迟在1秒以下。  
*             2. 实际通过示波器测试,微妙延迟函数比实际设置实际多运行0.25us左右的时间。
*             下面数据测试条件:
*             (1). MDK5.15,优化等级0, 不同的MDK优化等级对其没有影响。
*             (2). STM32F407IGT6
*             (3). 测试方法:
*                GPIOI->BSRRL = GPIO_Pin_8;
*                bsp_DelayUS(10);
*                GPIOI->BSRRH = GPIO_Pin_8;
*             -------------------------------------------
*                测试                 实际执行
*             bsp_DelayUS(1)          1.2360us
*             bsp_DelayUS(2)          2.256us
*             bsp_DelayUS(3)          3.256us
*             bsp_DelayUS(4)          4.256us
*             bsp_DelayUS(5)          5.276us
*             bsp_DelayUS(6)          6.276us
*             bsp_DelayUS(7)          7.276us
*             bsp_DelayUS(8)          8.276us
*             bsp_DelayUS(9)          9.276us
*             bsp_DelayUS(10)         10.28us
*            3. 两个32位无符号数相减,获取的结果再赋值给32位无符号数依然可以正确的获取差值。
*              假如A,B,C都是32位无符号数。
*              如果A > B  那么A - B = C,这个很好理解,完全没有问题
*              如果A < B  那么A - B = C, C的数值就是0xFFFFFFFF - B + A + 1。这一点要特别注意,正好用于本函数。
*********************************************************************************************************
*/
void bsp_DelayUS(uint32_t _ulDelayTime)
{
    uint32_t tCnt, tDelayCnt;
    uint32_t tStart;
         
    tStart = DWT_CYCCNT;                                     /* 刚进入时的计数器值 */
    tCnt = 0;
    tDelayCnt = _ulDelayTime * (SystemCoreClock / 1000000);  /* 需要的节拍数 */            
 
    while(tCnt < tDelayCnt)
    {
        tCnt = DWT_CYCCNT - tStart; /* 求减过程中,如果发生第一次32位计数器重新计数,依然可以正确计算 */ 
    }
}

回复

使用道具 举报

1

主题

16

回帖

19

积分

新手上路

积分
19
 楼主| 发表于 2024-10-29 16:41:14 | 显示全部楼层
eric2013 发表于 2024-10-29 09:41
总的来说你的是溢出问题没有处理好吧,如果是这个没有处理好,使用我这个微秒延迟的思路解决就行,核心下 ...

这个溢出处理是在延时任务里做的,提供系统时间戳的函数没办法这么整吧。问题也不是溢出,而是缺少可靠的手段检测SysTick->VAL 有没有重置。那个计算零头的函数其实和取模类似,如果没有发生SysTick 中断,全局ms_counter 不更新,那么计算出来的时间就只能跟着SysTick->VAL 周期性改变,类似这样:
无标题.png
这样就会发生我说的10-19 的情况,就因为延时任务里有应对溢出的处理,才会在这种情况出问题。如果前一刻是19,后一刻是10,一做差就像后一刻计时变量已经溢出了一样,算出来的时间差是个很大值,所以延时任务会立即执行,它误以为已经过去很长时间了。

反正我是很不理解为什么那么难检测SysTick 溢出,你可以看一下stm32duino 的微秒时间戳计算函数:

[C] 纯文本查看 复制代码

/**
  * @brief  Function called to read the current micro second
  * @param  None
  * @retval None
  */
uint32_t getCurrentMicros(void)
{
  uint32_t m0 = HAL_GetTick();
  __IO uint32_t u0 = SysTick->VAL;
  uint32_t m1 = HAL_GetTick();
  __IO uint32_t u1 = SysTick->VAL;
  const uint32_t tms = SysTick->LOAD + 1;

  if (m1 != m0) {
    return (m1 * 1000 + ((tms - u1) * 1000) / tms);
  } else {
    return (m0 * 1000 + ((tms - u0) * 1000) / tms);
  }
}

/**
  * @brief  Function called wto read the current millisecond
  * @param  None
  * @retval None
  */
uint32_t getCurrentMillis(void)
{
  return HAL_GetTick();
}



HAL_GetTick(); 返回的就是全局的毫秒计数变量。计算零头的代码和我那个差不多,所以他是想通过两次GetTick 来判断SysTick->VAL 有没有重置,但是这就要求SysTick->VAL 重置以后立即触发SysTick 中断更新全局计数器,我估计这代码是用不了的,毕竟我一开始就试过直接检测NVIC 中断标志来判断VAL 重置,没用。
回复

使用道具 举报

1

主题

16

回帖

19

积分

新手上路

积分
19
 楼主| 发表于 2024-10-29 16:55:29 | 显示全部楼层
eric2013 发表于 2024-10-29 09:41
总的来说你的是溢出问题没有处理好吧,如果是这个没有处理好,使用我这个微秒延迟的思路解决就行,核心下 ...

这是我写的非阻塞延时函数,和你的延时处理是一样,都是用时间差。定时任务使用相同的方式实现的,所以问题不在这里。
[C] 纯文本查看 复制代码

namespace scheduler_basic {

    /**
     * @brief 用来方便实现简单的轮询延时操作
     *
     * @tparam TimeSource
     */
    template <typename TimeSource>
    class TimeCycle {
       public:
        using TimeType = typename TimeSource::TimeType;

       private:
        TimeType _last_time;

       public:
        TimeCycle() {
            _last_time = TimeSource::get_time();
        }


        void reset() {
            _last_time = TimeSource::get_time();
        }


        /**
         * @brief 周期性延时指定的时间
         *
         * 每次延时结束时返回true,同时自动更新参考时间,从而开启下一个延时周期。
         *
         * @param duration 时间
         * @return true    一个延时周期结束
         * @return false   当前延时周期未结束
         */
        bool cycle(TimeType duration) {
            if (this->delay(duration)) {
                _last_time = TimeSource::get_time();
                return true;
            }

            return false;
        }


        /**
         * @brief 非阻塞延时指定的时间
         *
         * 延时结束不自动更新参考时间,一次延时结束后,返回值保持为true。
         *
         * @param duration 时间
         * @return true    已经过了指定的时间
         * @return false   延时尚未结束
         */
        bool delay(TimeType duration) {
            return (TimeSource::get_time() - _last_time) > duration;
        }
    };
}  // namespace scheduler_basic

回复

使用道具 举报

1万

主题

7万

回帖

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
117512
QQ
发表于 2024-10-30 11:03:36 | 显示全部楼层
etberzin 发表于 2024-10-29 16:55
这是我写的非阻塞延时函数,和你的延时处理是一样,都是用时间差。定时任务使用相同的方式实现的,所以问 ...

那确实不是这个问题了
回复

使用道具 举报

102

主题

573

回帖

894

积分

金牌会员

积分
894
QQ
发表于 2024-10-30 13:47:40 | 显示全部楼层
[C] 纯文本查看 复制代码
/**
 * @brief 获取微秒时间戳
 *
 */
uint32_t clock_us() {
        __disable_irq();

        uint32_t us = global_variable_us_counter;
        uint32_t v = SysTick->VAL;

        if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) {
                us += SYS_TICK_CYCLE_N_MS * 1000;
        }

        __enable_irq();

        uint32_t ticks_per_cycle = SysTick->LOAD + 1;
        us += (ticks_per_cycle - v) / global_variable_ticks_per_micro_second;
        return us;
}


这个函数是什么原理?
回复

使用道具 举报

1

主题

16

回帖

19

积分

新手上路

积分
19
 楼主| 发表于 2024-10-30 22:02:54 | 显示全部楼层
会飞的猪_2020 发表于 2024-10-30 13:47
[mw_shl_code=c,true]/**
* @brief 获取微秒时间戳
*

和clock_ms 一样,每次SysTick 中断会更新全局微秒计数器,在两次中断之间调用clock_us 时,获取SysTick->VAL,根据每微秒SysTick 的计数值,计算从上次中断开始的微秒数,也就是我说的零头,然后和全局计数器相加,就是微秒时间戳了。

这个后来和clock_ms 改成一样了,我一开始是想用SysTick->CTRL 寄存器COUNTFLAG 标志位检测VAL 溢出,但是经过clock_ms 的测试,这个原理行不通。
回复

使用道具 举报

102

主题

573

回帖

894

积分

金牌会员

积分
894
QQ
发表于 2024-10-31 16:37:33 | 显示全部楼层
etberzin 发表于 2024-10-30 22:02
和clock_ms 一样,每次SysTick 中断会更新全局微秒计数器,在两次中断之间调用clock_us 时,获取SysTick- ...

这个函数会溢出吗?用uint32表示us的话。
你逻辑里面这个us的值是不是肯定是不会超过65535的?
回复

使用道具 举报

1

主题

16

回帖

19

积分

新手上路

积分
19
 楼主| 发表于 2024-10-31 17:25:11 | 显示全部楼层
会飞的猪_2020 发表于 2024-10-31 16:37
这个函数会溢出吗?用uint32表示us的话。
你逻辑里面这个us的值是不是肯定是不会超过65535的?

正常的计数溢出没关系,只是限制了单次延时不能超过一个溢出周期,另外uint32 的最大值是4 个G。我以前写过一点计时溢出的处理方法:https://blog.csdn.net/Etberzin/article/details/130913841
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|小黑屋|Archiver|手机版|硬汉嵌入式论坛

GMT+8, 2025-8-12 06:27 , Processed in 0.058336 second(s), 28 queries .

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2023, Tencent Cloud.

快速回复 返回顶部 返回列表