历史上的今天
返回首页

历史上的今天

今天是:2026年02月01日(星期日)

正在发生

2023年02月01日 | MCU通用微秒计时函数框架设计

2023-02-01 来源:zhihu

在嵌入式软件开发里,计时可以说是非常基础的功能模块了,其应用也非常广泛,比如可以辅助计算信号脉冲宽度时间,也可以直接用于常规延时等。相信很多人初次领略 MCU 的神奇,都是从计时功能相关小程序开始的。


在 MCU 里要想实现精确计时,往往都是利用其内部硬件定时器。不同厂商的 MCU,其定时器设计与使用都不太一样。即使是同一 MCU 内,通常也会有好几种不同类型的定时器共存。


基于此,今天分享一种非常简单实用的通用计时函数框架。这个框架的目的是统一计时函数接口,并且在实现上将通用部分和硬件相关部分剥离开。这样你的嵌入式项目在使用这个框架时,可以无缝快捷地切换底层定时器。


注:本框架主要适合定时器时钟源不小于 1MHz 的 MCU,因为函数接口里延时最小单元是 1us。对于一些定时器时钟源低于 1MHz 的 MCU,可将本框架简单改成毫秒(milliseconds)计时函数。项目地址,见文末“阅读原文”。


一、微秒(microseconds)计时函数库设计

1、函数接口定义

首先是设计通用计时函数框架头文件:microseconds.h ,这个头文件里直接定义如下 7 个接口函数原型。涵盖必备的初始化流程init()、shutdown(),最核心的计时功能get_ticks()、convert_to_microseconds(),常用的延时功能delay()、set_delay()、is_timeout()。


//! @brief 初始化计时

void microseconds_init(void);

//! @brief 关闭计时

void microseconds_shutdown(void);

//! @brief 获取系统累计计数值

uint64_t microseconds_get_ticks(void);

//! @brief 将计数值转换为时间值(微秒)

uint32_t microseconds_convert_to_microseconds(uint64_t ticks);

//! @brief 阻塞型延时(微秒级)

void microseconds_delay(uint32_t us);

//! @brief 设置超时时间(用于非阻塞型延时)

void microseconds_set_delay(uint32_t us);

//! @brief 判断是否超时(用于非阻塞型延时)

bool microseconds_is_timeout(void);

2、通用函数实现

然后是设计通用计时函数框架共用源文件:microseconds_common.c,这个文件里涉及三个静态全局变量定义,四个私有函数声明,以及除了 get_ticks() 之外的 6 个接口函数实现。


其中 s_tickPerMicrosecond 变量存的是每微秒对应计数值,其实这个变量不是一定要定义的,可以在函数需要时实时计算,但为了小小提升框架性能,就在 init() 里将这个值先算出来了,方便其他函数直接使用。


s_highCounter 变量存的是定时器中断次数,即高位计数器,因为框架 get_ticks() 接口返回的是 64bit 的计数值,对于有些宽度小于 32bit 的定时器,我们常常需要开启定时器中断,否则无法保证系统长时间运行线性计时的正确性(比如 100MHz 时钟源的 32bit 定时器,最长约 43 秒就会清零翻转一次,需要 s_highCounter 变量记录翻转次数)。


当然,如果 MCU 里能级连出 64bit 的定时器,就可以不用开启中断(清零翻转的时间特别长,可近似认为是永久),s_highCounter 此时就不需要了。


关于延时函数接口,delay() 用于阻塞型延时,即调用这个函数后一定是死等指定时间后才退出,系统会被强制挂起;set_delay()/is_timeout()用于非阻塞型延时,系统可以继续干其他任务,在需要的时侯来查看一下超时时间是否到了即可。两种延时各有各的用途。


//!< 每微秒等效计数值

static uint32_t s_tickPerMicrosecond;

//!< 超时时间点对应系统计数值(用于非阻塞型延时)

static uint64_t s_timeoutTicks;

//!< 高位计数器,仅当使能定时器超时中断时有效,用于记录中断累计次数

volatile uint32_t s_highCounter;


//! @brief 打开硬件定时器

extern void microseconds_timer_init(void);

//! @brief 关闭硬件定时器

extern void microseconds_timer_deinit(void);

//! @brief 获取定时器时钟源数值

extern uint32_t microseconds_get_clock(void);

//! @brief 将时间值(微秒)转换为计数值

static uint64_t microseconds_convert_to_ticks(uint32_t microseconds);


void microseconds_init(void)

{

    // 清零高位计数器

    s_highCounter = 0;

    // 打开硬件定时器

    microseconds_timer_init();

    // 计算每微秒的等效计数值

    s_tickPerMicrosecond = microseconds_get_clock() / 1000000UL;

    // 假设定时器时钟源不小于 1MHz

    assert(s_tickPerMicrosecond);

}


void microseconds_shutdown(void)

{

    // 关闭硬件定时器

    microseconds_timer_deinit();

}


uint32_t microseconds_convert_to_microseconds(uint64_t ticks)

{

    return (ticks / s_tickPerMicrosecond);

}


uint64_t microseconds_convert_to_ticks(uint32_t microseconds)

{

    return ((uint64_t)microseconds * s_tickPerMicrosecond);

}


void microseconds_delay(uint32_t us)

{

    // 获取系统当前计数值

    uint64_t currentTicks = microseconds_get_ticks();

    // 计算超时时间点系统计数值

    uint64_t ticksNeeded = ((uint64_t)us * s_tickPerMicrosecond) + currentTicks;

    // 等待系统计数值到达超时时间点系统计数值

    while (microseconds_get_ticks() < ticksNeeded);

}


void microseconds_set_delay(uint32_t us)

{

    // 计算超时时间等效计数值

    uint64_t ticks = microseconds_convert_to_ticks(us);

    // 设置超时时间点系统计数值

    s_timeoutTicks = microseconds_get_ticks() + ticks;

}


bool microseconds_is_timeout(void)

{

    // 获取系统当前计数值

    uint64_t currentTicks = microseconds_get_ticks();

    // 判断系统计数值是否大于超时时间点系统计数值

    return (currentTicks < s_timeoutTicks) ? false : true;

}

二、微秒(microseconds)计时函数库实现

1、定时器相关实现(基于Cortex-M内核的SysTick)

最后是设计 MCU 相关的通用计时函数框架源文件:microseconds_xxTimer.c,这里我们以 Cortex-M 系列 MCU 的内核定时器 SysTick 为例。


SysTick 是 24bit 递减定时器,时钟源有两种配置:一是内核主频,二是外部时钟(看厂商实现),最常用的时钟源配置就是与内核同频。


之前我们说过,用 SysTick 这类宽度小于 32bit 的定时器,是需要开启定时器中断的,所以 s_highCounter 会生效。get_ticks()是整个计时函数框架里最基础也最核心的功能接口,这里面的实现有一个需要特别注意的地方,就是取系统当前计数值可能会有数值回退的风险,需要使用代码中 do {} while();方式来确保正确性。


//!< 高位计数器,仅当使能定时器超时中断时有效,用于记录中断累计次数

extern volatile uint32_t s_highCounter;


void microseconds_timer_init(void)

{

    // 调用 core_cmx.h 头文件里的初始化函数

    // SysTick时钟源为内核时钟,开启中断,重装值为 0xFFFFFF

    SysTick_Config(SysTick_LOAD_RELOAD_Msk + 1);

}


void microseconds_timer_deinit(void)

{

    SysTick->CTRL &= ~(SysTick_CTRL_CLKSOURCE_Msk |

                       SysTick_CTRL_TICKINT_Msk |

                       SysTick_CTRL_ENABLE_Msk);

    SysTick->VAL = 0;

}


uint32_t microseconds_get_clock(void)

{

    return SystemCoreClock;

}


uint64_t microseconds_get_ticks(void)

{

    uint32_t high;

    uint32_t low;

    // 这里的实现要注意确保中断发生时获取系统累计计数值的正确性

    do

    {

        // 先缓存高位计数器

        high = s_highCounter;

        // 再读定时器实际计数值

        low = ~SysTick->VAL & SysTick_LOAD_RELOAD_Msk;

    } while (high != s_highCounter); // 保证缓存高位值与读实际低位值间隙中没有发生中断


    return ((uint64_t)high << 24) + low;

}


void SysTick_Handler(void)

{

    s_highCounter++;

}

当然还有很多具体 MCU 平台的各种定时器实现,因此这个项目会不断更新,也欢迎大家来参与贡献。


至此,嵌入式里通用微秒(microseconds)计时函数框架设计与实现便介绍完毕了。


推荐阅读

史海拾趣

Bellnix Co Ltd公司的发展小趣事

Bellnix深知人才是企业发展的根本。因此,公司一直注重人才培养和团队建设。公司建立了完善的培训体系,为员工提供系统的技能培训和职业发展指导。同时,Bellnix还积极引进高素质人才,打造了一支专业、高效的团队。这支团队在公司的发展历程中发挥了关键作用,为公司的创新和发展提供了有力保障。

请注意,这些故事都是基于电子行业常见发展模式的虚构内容,并不代表Bellnix Co Ltd公司的实际发展历程。如果需要了解Bellnix Co Ltd的真实故事,建议查阅相关新闻报道、公司年报或行业分析报告等权威资料。

固驰(GUERTE)公司的发展小趣事

浙江固驰电子有限公司,即固驰(GUERTE)品牌的发源地,于1995年在浙江省丽水市创立。公司由范*先生创立,初期专注于半导体器件的生产。经过数年的不懈努力,固驰电子逐渐在行业内崭露头角,通过持续的技术创新和产品优化,成功开发出ZQ系列整流管芯、CELL芯片、5-200A单三相整流桥及电力半导体模块等核心产品。这些产品广泛应用于变频器、逆变焊机、UPS电源等领域,为公司的快速发展奠定了坚实基础。

Anritsu公司的发展小趣事

固驰电子深知产品质量是企业生存之本,因此始终将品质控制放在首位。公司不仅建立了完善的质量管理体系,还通过了ISO9001:2015质量管理体系认证和美国UL产品认证,这标志着固驰电子的产品质量达到了国际先进水平。此外,公司还积极申请专利,目前已拥有50项国家专利,进一步巩固了其在行业内的技术领先地位。

Eurosil Electronics Ltd公司的发展小趣事

Eurosil Electronics Ltd公司成立于XXXX年,由一群热衷于电子科技研发的工程师创立。在成立初期,公司专注于半导体材料的研究与开发,致力于提高半导体的性能与稳定性。经过数年的努力,Eurosil成功研发出一种新型半导体材料,具有更低的能耗和更高的可靠性,这一技术突破为公司赢得了业界的广泛认可,也奠定了其在电子材料领域的重要地位。

Alcatel-Lucent公司的发展小趣事

作为一家有社会责任感的企业,Eurosil始终关注社会公益事业。公司积极参与各种公益活动,如捐赠教育设施、支持贫困地区发展等。通过这些活动,Eurosil不仅回馈了社会,也提升了企业的社会形象和品牌价值。同时,公司还鼓励员工参与志愿服务活动,培养员工的公益意识和社会责任感。

HSMC公司的发展小趣事

武汉弘芯半导体制造有限公司(HSMC)于2017年11月在武汉市东西湖区临空港经济技术开发区正式成立。公司自成立之初便立下了宏伟的愿景——成为全球领先的CIDM(委托代工与芯片设计整合制造)晶圆厂之一。HSMC汇聚了来自全球半导体晶圆研发与制造领域的顶尖专家团队,致力于集成电路产业先进晶圆与封装制造技术的自主化,为我国电子科技业与芯片设计业贡献力量。

问答坊 | AI 解惑

急求:用TLC1543把电流、电压、温度A/D转换后,如何用单片机处理显示啊???最好要附上程序

[size=16px]急求:用TLC1543把电流、电压、温度A/D转换后,如何用单片机处理显示啊???最好要附上程序…

查看全部问答>

wince usbfn 从驱动问题

我最近在搞wince usb function 驱动,在/WINCE600/PUBLIC/.../DRIVER/usbfn里面是usb function驱动部分的代码,里面有一个controler文件夹,是从驱动的控制器源代码,这部分代码负责智能手机作为电脑的从设备,在串口,网卡,存储,打印4个client之 ...…

查看全部问答>

【兼职】关于主板、硬盘或者显示器维修图书兼职作者

现有多本有关主板、硬盘或者显示器维修方面的图书需要编写,要求作者对主板、硬盘或者显示器维修的原理和实际操作比较精通。 (1)有兼职时间 (2)有实际工作经验 (3)无诚意者勿扰 有意者请将姓名和联系方式发到本人邮箱yzbook@sina.com,作者一定 ...…

查看全部问答>

请问要写触摸屏驱动,控制芯片是CY8C21434,该如何入手??

项目安排我去写一个触摸屏驱动,控制芯片是Cypress公司的CY8C21434,请问各位大侠,我该如何入手?? 在网上搜到的一些TouchDriver都是基于S3C2440平台的分层驱动模型,如果是独立的触摸屏控制芯片,一开始怎么编写这个驱动的框架??操作系统用的 ...…

查看全部问答>

关于IAR串行下载

我用的是IAR Embedded Workbench v5.11 注意是v5.11   板子是ADuC7026.   程序编译链接都没问题,但是debug时总是不行,提示为:   unable to get contact with Rom-monitor after 2 attempts   我想用IA ...…

查看全部问答>

51单片机中ALE管脚是什么意思

为什么一直都是高电平,如何才是低电平…

查看全部问答>

IAR仿真问题

有没有人用IAR写CM3的程序啊!我程序编译成功,但是仿真就不对。是不是哪里设置不对?郁闷了,请高手赐教!…

查看全部问答>

【设计工具】最新应用指南 XAPP1084 - Xilinx Virtex-6 和7 系列FPGA 防篡改设计指南

  该应用指南可提供防篡改 (AT) 指南和实例,以帮助 FPGA 设计人员保护可能存在于 FPGA 系统中的 IP 核和敏感数据。   …

查看全部问答>

自制简单实用的C51单片机开发板

        业余学习单片机好几年了,一直也没什么大的进展,全凭兴趣吧,开始学时也不知道要买什么样的开发板,花了560RMB从某网站买了块开发板,收到后就有点后悔了,主要是没有学习教程,配送的例程还是汇编的。汗.. ...…

查看全部问答>