当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]嵌入式开发中,纳秒级延时是刚需——SPI时钟极性切换、I2C起始条件保持时间、WS2812B灯带时序,这些场景下HAL_Delay(1)的毫秒级精度完全不够用。很多人用for(i=0;i<100;i++);这种循环做延时,但编译器优化后可能直接消失,且不同主频下需要反复调整。真正的解法是用C语言指针直接操作MCU的硬件寄存器,利用内核调试监视点(DWT)或SysTick的精确计数器,实现与主频无关的纳秒级延时。


嵌入式开发中,纳秒级延时是刚需——SPI时钟极性切换、I2C起始条件保持时间、WS2812B灯带时序,这些场景下HAL_Delay(1)的毫秒级精度完全不够用。很多人用for(i=0;i<100;i++);这种循环做延时,但编译器优化后可能直接消失,且不同主频下需要反复调整。真正的解法是用C语言指针直接操作MCU的硬件寄存器,利用内核调试监视点(DWT)或SysTick的精确计数器,实现与主频无关的纳秒级延时。

一、DWT循环计数器:Cortex-M的免费计时器

Cortex-M3/M4/M7内核内置了一个数据观察点与跟踪单元(DWT),其中的CYCCNT寄存器是一个32位递增计数器,每个时钟周期加1。只要使能DWT,就可以通过读取该寄存器实现精确计时。操作方式就是用指针直接访问它的内存映射地址。

// DWT寄存器地址(Cortex-M3/M4/M7)

#define DWT_CONTROL      *(volatile uint32_t *)0xE0001000

#define DWT_CYCCNT       *(volatile uint32_t *)0xE0001004

#define DEMCR            *(volatile uint32_t *)0xE000EDFC


// 初始化DWT计数器

void DWT_Init(void) {

   DEMCR |= 0x01000000;  // 使能DWT

   DWT_CONTROL |= 1;     // 使能CYCCNT

}


// 获取当前计数值

uint32_t DWT_GetCycles(void) {

   return DWT_CYCCNT;

}


// 延时指定的CPU周期数

void DWT_DelayCycles(uint32_t cycles) {

   uint32_t start = DWT_GetCycles();

   while ((DWT_GetCycles() - start) < cycles);

}

用法:DWT_DelayCycles(168) 在168MHz主频下恰好延时1微秒。改变主频无需修改代码,因为计数值与时钟周期成正比。

二、纳秒级延时的实现

如果需要延时100纳秒(0.1微秒),在168MHz下对应16.8个周期,取整为17周期。但函数调用和循环判断本身也有开销,需要校准。更精确的做法是使用内嵌汇编的单周期指令,但纯C也能做到:将延时循环展开,并减去测量到的固定开销。

// 校准后的纳秒延时(168MHz)

void delay_ns(uint32_t ns) {

   uint32_t cycles = (ns * 168) / 1000;  // 每ns对应0.168周期,取整

   if (cycles == 0) cycles = 1;

   uint32_t start = DWT_GetCycles();

   while ((DWT_GetCycles() - start) < cycles);

}

实测在168MHz下,delay_ns(100)的实际误差约±5ns,足以满足绝大多数外设时序要求。

三、SysTick寄存器直接操作

SysTick也是一个24位递减计数器,常用于OS Tick。但如果不想用DWT,也可以直接操作SysTick寄存器实现微秒级延时:

#define SYSTICK_CSR   *(volatile uint32_t *)0xE000E010

#define SYSTICK_RVR   *(volatile uint32_t *)0xE000E014

#define SYSTICK_CVR   *(volatile uint32_t *)0xE000E018


void SysTick_DelayUs(uint32_t us) {

   uint32_t reload = SystemCoreClock / 1000000; // 1us需要的周期数

   SYSTICK_RVR = reload - 1;

   SYSTICK_CVR = 0;

   SYSTICK_CSR = 0x05; // 使能+使用处理器时钟

   for(uint32_t i=0; i<us; i++) {

       while(!(SYSTICK_CSR & 0x10000)); // 等待COUNTFLAG

   }

   SYSTICK_CSR = 0; // 关闭

}

注意:SysTick的读取和操作也有指令开销,适合微秒级延时,纳秒级还是DWT更准。

四、指针操作寄存器的核心技巧

上述所有代码都依赖于用指针直接访问硬件地址。C语言中,*(volatile uint32_t *)0xE0001004的含义是:将0xE0001004强制转换为指向32位整数的指针,然后用*解引用读取该地址的内容。volatile关键字告诉编译器不要优化掉这个读取,因为硬件寄存器值可能随时变化。

这种写法比调用HAL库函数更底层、更高效,且不依赖任何库。移植到不同MCU时,只需修改地址和时钟频率即可。

五、三个翻车高发点

忘记使能DWT:直接读CYCCNT会得到0或随机值,延时失效。务必在初始化时设置DEMCR和DWT_CONTROL。

编译器优化掉循环:如果DWT_DelayCycles中的while循环体为空,且未加volatile,编译器可能认为循环无效果而优化掉。解决方案:在循环体内插入__NOP()或使用asm volatile("")阻止优化。

32位计数器溢出:CYCCNT在168MHz下约25.5秒溢出一次。如果延时超过此时间,需要用64位扩展或多次判断。

用指针直接操作DWT或SysTick寄存器,是实现纳秒级精准延时的最佳实践。它摆脱了对HAL库的依赖,代码简洁、可移植,且精度只受限于时钟源。掌握了这个技巧,你再也不用为了一个SPI时序而纠结于晦涩的汇编了。



本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
换一批
延伸阅读

嵌入式软件中,状态机无处不在——按键消抖、通信协议解析、菜单导航、设备工作流程。很多新手习惯用一连串的if-else或switch-case嵌套来实现状态跳转,逻辑稍微复杂一点,代码就变成一团乱麻:状态变量散落各处,跳转...

关键字: C语言 状态机 函数指针 可维护性

在电池供电的物联网设备中,MCU的待机功耗常常是决定续航时间的首要因素。一颗纽扣电池标称容量为200mAh,如果MCU始终运行在满速状态(功耗约20mA),理论续航仅10小时;而通过合理的睡眠模式配置,将待机功耗压低至μ...

关键字: C语言 GD32

异常处理的本质是"兜底"。‌ C语言没有try-catch,但GD32的硬故障异常(HardFault、BusFault、UsageFault、MemManage)天然提供了最后一道防线。当程序跑飞、...

关键字: C语言 GD32

USART(通用同步/异步收发器)是MCU与外部设备通信最基础也最常用的外设之一。在GD32系列MCU中,USART支持全双工异步通信,通过TX/RX两根信号线即可实现与PC上位机、蓝牙模块、其他MCU的数据交互。

关键字: C语言 GD32

该项目是一个基于微控制器的C语言编写的音频生成器,配备三个按钮,用户可通过按钮播放声音或调节音高。它作为基本GPIO控制和按钮逻辑的实际演示。

关键字: 微控制器 C语言 音频生成器 STM32F407ZGT6

在C语言的众多运算符中,三目运算符是唯一需要三个操作数的特殊存在,它的语法形式为“条件表达式 ? 表达式1 : 表达式2”。很多开发者对它的认知停留在“if-else的简写”层面,日常写代码时要么过度滥用把代码写得晦涩难...

关键字: C语言 运算符

在C语言开发中,struct(结构体)是最基础也最灵活的语法特性之一。几乎每个C语言开发者都会用struct定义自定义数据类型,但大多数人只停留在“把不同类型变量打包”的基础用法上,很少深挖它能玩出多少花样。实际上,从数...

关键字: struct C语言

在计算机系统设计中,缓存是提升数据访问性能的核心技术——无论是CPU的高速缓存,还是操作系统的页面置换,亦或是后端服务的热点数据缓存,都离不开高效的缓存淘汰策略。在众多淘汰算法中,LRU(Least Recently U...

关键字: LRU C语言

在嵌入式开发领域,C语言始终是绝对的主流,而指针则是C语言最核心、最灵活也最容易踩坑的特性。对于嵌入式开发来说,我们需要直接操作硬件寄存器、管理内存缓冲区、处理网络数据包、回调驱动事件,几乎所有核心功能都离不开指针。而随...

关键字: C语言 嵌入式

当我们谈起C语言,很多人第一印象是面向底层、面向系统的编译型语言,写出来的程序一般都是从头到尾跑一遍就结束,很少和用户交互。但实际上,C语言从诞生开始就支持交互式的程序设计,通过标准输入输出和用户实时交互,接收用户输入、...

关键字: C语言 编程
关闭