从毫秒到微秒:TinyML在嵌入式端的C语言加速秘籍,工程师都在偷偷用

第一章:从毫秒到微秒:TinyML推理加速的底层逻辑

在资源受限的嵌入式设备上运行机器学习模型,TinyML 的核心挑战在于如何将推理延迟从毫秒级压缩至微秒级。这不仅依赖于模型轻量化,更涉及对硬件架构、内存访问模式和计算流水线的深度优化。

模型与硬件的协同设计

TinyML 推理加速的关键在于打破传统“先训练后部署”的流程,转而采用硬件感知的模型设计策略。通过在训练阶段引入量化感知(Quantization-Aware Training, QAT),模型权重和激活值可被约束为低比特表示,显著降低计算复杂度。
  • 使用8位或更低精度整数替代浮点运算
  • 消除ReLU等非线性函数的高开销实现
  • 将卷积核重参数化以适配DSP指令集

内存层级的极致优化

嵌入式系统中,CPU与内存之间的带宽瓶颈远比算力限制更严重。因此,减少DRAM访问次数成为性能突破的重点。
访问类型延迟(典型值)优化策略
片外Flash读取80 ns权重预加载至SRAM
片内SRAM访问4 ns数据复用与缓存分块

基于CMSIS-NN的代码优化示例


// 使用ARM CMSIS-NN库执行量化卷积
arm_convolve_s8(&ctx,                  // 运行时上下文
                &input_tensor,         // 输入张量(int8)
                &filter_tensor,        // 滤波器(int8)
                &bias_tensor,          // 偏置(int32)
                &output_tensor,        // 输出(int8)
                &conv_params,          // 量化参数
                &quant_params,         // 乘法移位量化因子
                &cpu_buf,              // 临时CPU缓冲区
                NULL);                 // 可选DMA句柄
// 执行逻辑:该函数利用Cortex-M4/M55的SIMD指令(如SMLAD)
// 实现每周期多乘加操作,将3x3卷积延迟压至<10μs
graph LR A[输入特征图] --> B{是否在SRAM?} B -- 是 --> C[直接加载] B -- 否 --> D[从Flash预取] D --> C C --> E[执行SIMD卷积] E --> F[输出至下一层]

第二章:C语言层面的性能瓶颈分析与突破

2.1 数据类型选择对推理延迟的影响:理论与实测对比

在深度学习推理过程中,数据类型的选取直接影响计算效率与内存带宽占用。通常使用的数据类型包括 FP32、FP16 和 INT8,其精度与计算速度之间存在权衡。
常见数据类型对比
  • FP32:单精度浮点,提供高精度但计算开销大;
  • FP16:半精度浮点,减少内存占用并提升GPU利用率;
  • INT8:整型量化,显著加速推理,适合边缘设备。
实测延迟对比表
数据类型推理延迟(ms)模型大小(MB)
FP3248.2520
FP1632.1260
INT819.5130
量化代码示例

import torch
# 将模型从FP32转换为INT8
quantized_model = torch.quantization.quantize_dynamic(
    model, {torch.nn.Linear}, dtype=torch.qint8
)
该代码使用 PyTorch 的动态量化功能,仅对线性层进行 INT8 转换,降低模型体积并提升推理速度,适用于 CPU 部署场景。

2.2 函数调用开销优化:内联与宏定义的实战权衡

在性能敏感的代码路径中,函数调用带来的栈帧创建与参数传递开销不容忽视。通过内联函数和宏定义可有效消除此类开销,但二者在安全性与可维护性上存在显著差异。
内联函数:类型安全的优化选择
C++ 中的 `inline` 关键字建议编译器将函数体直接嵌入调用点,避免跳转开销:
inline int square(int x) {
    return x * x;
}
该方式保留类型检查与作用域规则,调试信息完整,适合复杂逻辑的小函数。
宏定义:高效但需谨慎使用
宏由预处理器展开,无类型约束,适用于泛型表达式:
#define SQUARE(x) ((x) * (x))
尽管性能极致,但缺乏作用域控制,易因副作用引发错误,如 `SQUARE(++x)` 会导致重复递增。
特性内联函数宏定义
类型安全✔️
调试支持✔️
执行效率极高
实际开发中应优先采用内联函数,仅在性能瓶颈且类型无关场景下考虑宏。

2.3 内存访问模式优化:缓存友好型数组布局设计

现代CPU通过多级缓存提升内存访问效率,而数据的存储布局直接影响缓存命中率。连续访问相邻内存地址可充分利用空间局部性,减少缓存行(Cache Line)未命中。
结构体数组 vs 数组结构体
在高频遍历场景中,应优先采用“结构体数组”(AoS)或“数组结构体”(SoA)中的SoA布局,以保证字段访问的连续性。

// AoS: 字段交错,遍历x时y/z也加载
struct Particle { float x, y, z; };
Particle particles[1024];

// SoA: 按字段分离,提升特定字段访问效率
float particle_x[1024], particle_y[1024], particle_z[1024];
上述SoA布局在仅更新位置x时,避免加载冗余数据,显著降低缓存带宽压力。
缓存行对齐优化
使用对齐属性确保关键数据按64字节(典型缓存行大小)对齐,防止伪共享:

alignas(64) float data[1024];
该声明确保data起始地址为64的倍数,提升SIMD指令与预取器效率。

2.4 浮点运算替代策略:定点化与查表法的实际应用

在资源受限的嵌入式系统中,浮点运算因性能开销大而常被规避。两种主流替代方案是定点化和查表法。
定点化:用整数模拟小数运算
通过缩放因子将浮点数转换为整数处理。例如,使用16位小数位的Q15格式表示[-1,1)范围的数值:

// Q15 定点乘法:0.5 * 0.25
int16_t a = 0x4000; // 0.5 in Q15
int16_t b = 0x2000; // 0.25 in Q15
int32_t temp = (int32_t)a * b; // 结果左移15位以归一化
int16_t result = (int16_t)(temp >> 15); // 得到0x1000 (0.125)
该方法避免FPU依赖,提升确定性执行时间。
查表法:预计算换取运行时效率
适用于周期性函数(如sin、log)。预先存储计算值,运行时直接索引:
  • 节省CPU周期,适合高频调用
  • 空间换时间,需权衡精度与内存占用

2.5 中断与上下文切换对实时推理的隐性损耗剖析

在实时推理系统中,中断处理和频繁的上下文切换会引入不可忽视的延迟抖动。硬件中断(如网络包到达)触发内核调度,可能导致推理任务被抢占,破坏时序确定性。
上下文切换开销量化
一次典型上下文切换耗时约2~10微秒,具体取决于CPU架构与缓存状态:
项目平均耗时(μs)
寄存器保存/恢复1.2
TLS与页表切换3.5
L1/L2缓存污染额外2~5
中断屏蔽策略示例

// 绑定线程至隔离CPU核心,并禁用本地中断
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(8, &set); // 使用保留核心8
sched_setaffinity(0, sizeof(set), &set);

// 在实时段关闭可屏蔽中断(需root权限)
__asm__ volatile("cli" ::: "memory");
上述代码将关键推理线程绑定至专用CPU核心,并通过 cli指令临时屏蔽外部中断,减少干扰源。此策略适用于硬实时场景,但需谨慎管理中断延迟累积问题。

第三章:模型部署前的代码级优化手段

3.1 算子融合与计算图简化在C代码中的实现路径

在高性能计算场景中,算子融合通过合并多个相邻运算操作,减少内存访问开销并提升缓存利用率。常见的实现方式是在C语言中利用函数指针与结构体封装基本算子。
融合策略设计
采用静态注册机制将常见算子(如ReLU、Add)进行模式匹配,识别可融合的连续节点。例如:

typedef struct {
    void (*compute)(float*, float*, int);
    int size;
} fused_op_t;

void fuse_relu_add(float* a, float* b, int n) {
    for (int i = 0; i < n; ++i)
        a[i] = fmaxf(0.0f, a[i] + b[i]); // 融合Add+ReLU
}
该函数将加法与激活函数合并为单一循环,避免中间结果写回内存。参数 `a` 和 `b` 为输入张量,`n` 表示向量长度,显著降低访存次数。
优化效果对比
方案内存访问次数执行周期
分离算子3850
融合算子1520

3.2 预计算与常量折叠:减少运行时负担的有效方法

在现代编译优化中,预计算与常量折叠是提升程序执行效率的关键技术。它们通过在编译期求解表达式,将运行时的重复计算提前完成,从而降低CPU负载。
常量折叠的工作机制
当编译器检测到由字面量或常量构成的表达式时,会直接计算其结果并替换原表达式。例如:
int result = 5 * 1024 + 2048;
该表达式会被优化为:
int result = 7168;
此举消除了每次运行时的乘法和加法运算,显著减少指令数量。
优化带来的性能收益
  • 减少目标代码指令数,提升缓存命中率
  • 缩短程序启动时间,尤其在初始化阶段大量使用常量表达式时
  • 为后续优化(如常量传播)提供基础支持
优化前优化后
3 次算术运算0 次运算
运行时计算编译期完成

3.3 轻量化内存分配:静态缓冲区管理的最佳实践

在资源受限的嵌入式系统中,动态内存分配易引发碎片与不确定性。静态缓冲区管理通过预分配固定内存池,提升运行时稳定性。
静态缓冲区设计原则
  • 定长块分配:将缓冲区分成等长块,简化分配逻辑;
  • 编译期确定大小:避免运行时调整,降低开销;
  • 零释放开销:采用循环复用机制,无需显式释放。
代码实现示例

#define BUFFER_SIZE 256
#define BLOCK_COUNT 8
static uint8_t pool[BUFFER_SIZE * BLOCK_COUNT];
static uint8_t used[BLOCK_COUNT] = {0};

void* alloc_block() {
    for (int i = 0; i < BLOCK_COUNT; i++) {
        if (!used[i]) {
            used[i] = 1;
            return &pool[i * BUFFER_SIZE];
        }
    }
    return NULL; // 分配失败
}
该实现使用位图跟踪块状态, alloc_block 时间复杂度为 O(n),适合小规模场景。通过预分配 pool 数组,确保内存连续且无碎片。

第四章:编译器与硬件协同优化技巧

4.1 GCC编译优化选项深度解析:-O2、-Os与-mfpu的选择艺术

在嵌入式与高性能计算场景中,合理选择GCC优化选项对程序性能和资源占用至关重要。`-O2` 提供了良好的性能优化平衡,启用如循环展开、函数内联等增强技术。
常见优化级别对比
  • -O2:启用大部分安全优化,提升运行效率
  • -Os:在-O2基础上优化代码体积,适合存储受限环境
  • -O3:激进优化,可能增大代码尺寸
浮点运算单元的针对性优化
对于ARM架构,需结合硬件特性使用`-mfpu`指定FPU类型:
gcc -O2 -mfpu=neon -mfloat-abi=hard -o app app.c
该命令启用NEON SIMD扩展并使用硬浮点ABI,显著提升浮点密集型任务性能。忽略此配置可能导致软件模拟,性能下降达数倍。正确匹配目标平台FPU能力是实现高效编译的关键一步。

4.2 利用SIMD指令集加速向量运算:ARM CMSIS-DSP集成实战

ARM Cortex-M系列处理器通过SIMD(单指令多数据)指令集显著提升数字信号处理性能。CMSIS-DSP作为ARM官方提供的优化库,深度集成SIMD特性,适用于音频处理、传感器融合等高吞吐场景。
CMSIS-DSP向量加法示例
arm_add_q15(inputA, inputB, output, blockSize);
该函数执行两个Q15格式数组的并行加法。其中 inputAinputB为输入向量, output存储结果, blockSize表示元素数量。底层利用SMLAD等SIMD指令,单周期完成多组数据运算。
性能优势对比
运算类型传统C实现 (cycles)CMSIS-DSP SIMD (cycles)
128点Q15加法1280160
128点Q15乘法累加2560320
可见,借助SIMD并行处理,运算效率提升达8倍以上。

4.3 数据对齐与内存边界优化:提升总线传输效率的关键细节

现代处理器通过总线访问内存时,数据的存储位置直接影响读取效率。若数据未按内存边界对齐,可能引发多次内存访问,甚至触发硬件异常。
数据对齐的基本原理
数据对齐指变量的地址是其大小的整数倍。例如,4字节的 int32 应存放在地址能被4整除的位置。
  • 提高访问速度:对齐数据可减少内存访问周期
  • 避免原子性问题:某些架构要求锁操作对象必须对齐
  • 兼容SIMD指令:向量操作通常要求16/32字节对齐
代码示例:结构体对齐优化

struct Data {
    char a;     // 1字节
    // 填充3字节
    int b;      // 4字节,对齐到4字节边界
};
该结构体实际占用8字节而非5字节。编译器自动插入填充字节以满足 int 的对齐需求。通过调整成员顺序(如将 char 放在最后),可减少内存浪费。
类型大小对齐要求
char11
int44
double88

4.4 链接脚本调优:将关键代码段搬至高速SRAM运行

在嵌入式系统中,将频繁执行的关键代码(如中断服务程序或信号处理函数)从Flash迁移至高速SRAM,可显著提升执行效率。这一优化依赖于链接脚本对内存布局的精细控制。
内存区域定义
通过链接脚本明确划分内存区域,确保SRAM具备足够的保留空间:

MEMORY
{
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
    SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
此处定义了可读写执行的SRAM区域,为代码搬运提供物理基础。
代码段重定向
使用SECTIONS指令将特定函数放入SRAM:

.text.fast_code :
{
    *(.text.fast_code)
} > SRAM
配合源码中的 __attribute__((section(".text.fast_code"))),实现函数级精准调度。 该策略常用于实时性要求严苛的场景,需注意同步初始化流程以保证代码正确加载。

第五章:未来趋势与极致低延迟的可能性探索

量子网络通信的初步实践
量子纠缠态传输为跨洲际数据同步提供了理论基础。在实验性金融交易系统中,利用量子密钥分发(QKD)实现加密信令的亚微秒级验证,显著降低安全握手延迟。
  • 中国科技大学“京沪干线”已实现1200公里QKD链路
  • 瑞士ID Quantique部署日内瓦证券交易所低延迟加密通道
边缘智能推理优化
通过在基站侧部署轻量化模型,将AI预测前移至用户50ms可达范围内。某CDN厂商采用该方案,在直播弹幕场景中实现端到端延迟压降至83ms。
// 边缘节点动态负载均衡策略
func RouteToNearestEdge(userLoc Coordinate) *EdgeNode {
    nodes := GetAvailableNodes()
    sort.Slice(nodes, func(i, j int) bool {
        return Distance(userLoc, nodes[i].Location) < 
               Distance(userLoc, nodes[j].Location)
    })
    return &nodes[0] // 返回地理最近节点
}
新型协议栈设计方向
传统TCP/IP在高频交互中暴露头部开销过大问题。基于UDP增强的QUIC+自定义时序控制层已在部分AR远程协作平台落地。
协议类型平均RTT(ms)适用场景
TCP45Web浏览
QUIC+TCPLite28实时协同编辑
[终端] → (时间敏感调度) → [边缘AI] → {压缩编码} → [光缆骨干] ↑ ↓ [状态预测缓存] ← (反馈校正)
<think> 我们是在嵌入式系统中,通常可以使用系统提供的定时器或硬件计数器来获取高精度时间。 常见的嵌入式系统可能有不同的硬件和操作系统支持,这里假设是一个基于ARM Cortex-M系列,运行FreeRTOS或类似RTOS的嵌入式环境。 我们可以使用系统滴答定时器(SysTick)或者其他的硬件定时器来获取毫秒级时间。 注意:由于嵌入式环境差异较大,以下代码需要根据具体硬件平台进行调整。这里以Cortex-M的SysTick为例,SysTick通常配置为每毫秒中断一次,并有一个计数器记录自启动以来的毫秒数。 如果系统使用了FreeRTOS,也可以使用FreeRTOS提供的`xTaskGetTickCount()`,但注意它返回的是系统节拍数,需要知道每秒钟的节拍数(configTICK_RATE_HZ)来转换为毫秒。 以下是一个通用的方法,假设系统有一个全局变量`sys_tick`,在SysTick中断中每次递增(1ms中断一次)。 步骤: 1. 在SysTick中断服务函数中维护一个全局变量(如`uwTick`)。 2. 编写一个函数,返回当前的`uwTick`值(即系统启动以来的毫秒数)。 如果没有操作系统,我们可以这样实现: 首先,在初始化SysTick定时器时设置为1ms中断一次,并维护计数器: volatile unsigned long long uwTick = 0; // 64位防止长时间运行溢出 然后,在SysTick_Handler中断服务函数中: void SysTick_Handler(void) { uwTick++; } 接着,我们可以编写获取毫秒时间的函数: 注意:由于uwTick在中断中会被修改,我们在读取时应该禁止中断,以保证读取的原子性。 但是,如果系统支持32位或64位的原子读取,且编译器保证了对volatile变量的读取是原子的(例如32位系统上32位数据是原子操作),则可以不禁止中断。但为了安全,特别是当变量长度超过机器字长时(如64位在32位系统上),需要采取保护措施。 这里我们假设是32位系统,使用32位计数器,则最大计数4294967295毫秒(约49.7天)会溢出。如果系统需要长时间运行,建议使用64位计数器,但32位系统上64位操作不是原子的,所以需要临界区保护。 以下代码以32位计数器为例(注意溢出问题),并使用临界区保护: 在FreeRTOS中,我们可以使用任务调度器挂起/恢复来实现临界区,或者使用中断屏蔽。这里使用FreeRTOS的临界区保护。 如果没有操作系统,可以使用关中断和开中断(如__disable_irq和__enable_irq,具体函数由编译器提供)。 这里我们写一个通用的,假设没有操作系统,使用CMSIS提供的函数: 注意:以下代码为示例,需要根据具体平台调整。 由于问题中没有指定具体硬件和操作系统,我们写一个基于Cortex-M和CMSIS的函数,并假设有`__disable_irq`和`__enable_irq`函数(或者使用`__set_PRIMASK`等)。 我们使用一个32位的计数器,并考虑临界区保护: 定义全局变量: volatile uint32_t uwTick = 0; SysTick中断服务函数: void SysTick_Handler(void) { uwTick++; } 获取毫秒时间的函数: uint32_t GetTick_ms(void) { uint32_t tick; // 进入临界区(关中断) __disable_irq(); tick = uwTick; __enable_irq(); // 开中断 return tick; } 但是,这种关中断的方式会影响中断响应,如果系统对中断响应要求高,可以尝试使用原子读取(但32位变量在32位系统上读取是原子的,因此也可以不用临界区,但考虑到编译器优化和变量可见性,使用volatile通常足够,不过为了安全,我们加上临界区)。 然而,在Cortex-M3及以上,可以使用更高效的临界区保护方法:使用LDREX/STREX指令,但这里我们使用简单的关中断。 但是,关中断时间很短,通常可以接受。 另一种方法:由于SysTick是一个递减计数器,我们可以通过读取当前SysTick的计数值来计算更精确的时间(包括毫秒内的时间),但这里我们只要求毫秒级,所以用上面的方法即可。 如果系统没有SysTick,而是使用其他定时器,方法类似,在定时器中断中累加计数器,然后提供读取函数。 下面是一个不使用操作系统的实现示例代码: ```c #include <stdint.h> volatile uint32_t uwTick = 0; // 全局计数器,每毫秒递增 // 假设系统时钟为72MHz,设置SysTick为1ms中断一次 void SysTick_Init(void) { SysTick_Config(SystemCoreClock / 1000); // SystemCoreClock为系统主频,如72MHz } // SysTick中断服务函数 void SysTick_Handler(void) { uwTick++; } // 获取毫秒计数器 uint32_t GetTick_ms(void) { uint32_t tick; // 关中断 uint32_t primask = __get_PRIMASK(); __disable_irq(); tick = uwTick; __set_PRIMASK(primask); return tick; } ``` 注意:`__get_PRIMASK`和`__set_PRIMASK`是CMSIS函数,用于保存和恢复中断状态,这样可以在退出时恢复之前的中断使能状态。 如果不使用CMSIS,可以使用以下方式: ```c #define ENTER_CRITICAL() uint32_t primask = __get_PRIMASK(); __disable_irq() #define EXIT_CRITICAL() __set_PRIMASK(primask) uint32_t GetTick_ms(void) { uint32_t tick; ENTER_CRITICAL(); tick = uwTick; EXIT_CRITICAL(); return tick; } ``` 如果系统运行时间很长,32位计数器会在大约49天后溢出,如果系统需要长期运行,可以考虑使用64位计数器,但读取64位变量在32位系统上不是原子的,需要临界区保护。 使用64位计数器: ```c volatile uint64_t uwTick = 0; void SysTick_Handler(void) { uwTick++; } uint64_t GetTick_ms(void) { uint64_t tick; ENTER_CRITICAL(); tick = uwTick; EXIT_CRITICAL(); return tick; } ``` 这样,溢出时间会非常长,可以忽略。 如果是在FreeRTOS中,我们可以直接使用`xTaskGetTickCount()`,然后根据`configTICK_RATE_HZ`转换为毫秒。例如,如果`configTICK_RATE_HZ=1000`,那么每个tick就是1毫秒,直接返回`xTaskGetTickCount()`即可。如果不是1000,则需要转换: TickType_t tick_count = xTaskGetTickCount(); uint32_t ms = (tick_count * 1000) / configTICK_RATE_HZ; 但是注意这样计算可能会溢出,所以建议在系统设计时设置`configTICK_RATE_HZ`为1000,这样每个tick就是1毫秒,无需转换。 总结:根据不同的嵌入式环境,选择合适的方法。以上提供了两种常见场景(裸机与FreeRTOS)的示例。 注意:以上代码需要根据实际平台调整,比如中断函数名、临界区保护方法等。 §§相关问题§§ 1. 在嵌入式系统中,如何实现微秒级延时函数? 2. 使用SysTick定时器时,如何避免32位计数器溢出导致的问题? 3. 在无操作系统的嵌入式环境下,如何实现多任务共享的时间戳获取? 4. FreeRTOS中如何配置系统节拍(Tick Rate)以优化时间精度? 5. 除了SysTick,还有哪些硬件定时器可以用于高精度时间测量? </think>### 嵌入式系统中获取毫秒级时间的 C 语言实现 在嵌入式系统中获取毫秒级时间,通常需要利用硬件定时器。以下是基于 ARM Cortex-M 架构的通用实现(适用于 STM32、GD32 等芯片),使用 SysTick 定时器实现高精度计时: ```c #include <stdint.h> // 全局变量存储系统启动后的毫秒数 static volatile uint64_t system_ms = 0; /** * @brief 初始化 SysTick 定时器为 1ms 中断 * @param 无 * @retval 无 */ void Init_SysTick(void) { // 设置 SysTick 重装载值为系统时钟频率/1000 - 1 SysTick->LOAD = (SystemCoreClock / 1000) - 1; // 设置时钟源为处理器时钟 SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk; // 使能 SysTick 中断和计数器 SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk; } /** * @brief SysTick 中断处理函数 * @note 每毫秒自动调用一次 */ void SysTick_Handler(void) { system_ms++; // 毫秒计数器递增 } /** * @brief 获取系统启动后的毫秒数 * @param 无 * @retval 当前毫秒计数值 */ uint64_t Get_Milliseconds(void) { return system_ms; } /** * @brief 计算时间间隔(毫秒) * @param start: 起始时间点(由 Get_Milliseconds 获取) * @retval 从 start 到当前的时间间隔(毫秒) */ uint32_t Get_Elapsed_Time(uint64_t start) { uint64_t current = Get_Milliseconds(); return (uint32_t)(current - start); } ``` ### 使用示例 ```c int main(void) { // 初始化硬件 SystemInit(); Init_SysTick(); uint64_t start_time = Get_Milliseconds(); // 执行耗时操作 Process_Data(); // 计算耗时 uint32_t elapsed = Get_Elapsed_Time(start_time); printf("操作耗时: %lu 毫秒\n", elapsed); while(1); } ``` ### 关键实现细节 1. **SysTick 配置**: - 设置重装载值为 `(系统时钟频率/1000) - 1` - 使能中断和计数器 - `SystemCoreClock` 需替换为实际的系统时钟频率 2. **中断处理**: - 在 `SysTick_Handler()` 中断服务程序中递增计数器 - 使用 `volatile` 确保多线程/中断环境下的正确访问 3. **64位计数器**: - 使用 `uint64_t` 存储毫秒数,可支持约 5.8 亿年不溢出 - 32位系统需确保 64 位操作的原子性 4. **时间间隔计算**: - `Get_Elapsed_Time()` 处理计数器回绕问题 - 返回 32 位无符号整数,支持最长约 49 天的间隔测量 ### 替代方案 1. **使用硬件定时器(如 TIM2)**: ```c // 配置 TIM2 为 1MHz 时钟 TIM2->PSC = SystemCoreClock/1000000 - 1; TIM2->ARR = 0xFFFF; // 自动重装载值 TIM2->CR1 |= TIM_CR1_CEN; // 启动定时器 uint32_t Get_Microseconds(void) { return TIM2->CNT; // 返回微秒计数值 } ``` 2. **FreeRTOS 系统**: ```c #include "FreeRTOS.h" #include "task.h" uint32_t Get_Milliseconds(void) { return xTaskGetTickCount() * portTICK_PERIOD_MS; } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值