4. ICU(输入捕获单元)模块配置与驱动

4.1 ICU功能概述

ICU,全称Input Capture Unit,说白了就是用来“抓”信号边沿的。你想想看,一个PWM信号过来了,高电平多久、低电平多久、周期多长——这些信息ICU都能帮你精确捕获。

我在项目中用过很多次ICU,最典型的场景就是测量舵机的PWM信号。舵机控制信号通常是50Hz的PWM,脉宽在1ms到2ms之间。用ICU来测,精度能到微秒级,完全够用。

ICU的核心功能其实就两个:

  • 测量频率:通过捕获两个上升沿之间的时间差,算出信号频率
  • 测量脉宽:捕获上升沿和下降沿之间的时间差,得到高电平宽度

嗯,这里要注意,ICU测量的是时间,不是直接测电压。它靠的是定时器的硬件捕获功能,信号边沿一来,硬件自动把当前计数值锁存到寄存器里。这样CPU就不用一直盯着信号看了,效率高很多。

关键点:ICU测量精度取决于定时器时钟频率。比如定时器时钟是80MHz,一个tick就是12.5ns,理论上能分辨12.5ns的变化。但实际应用中,受信号抖动和滤波影响,能做到1us精度就很不错了。

4.2 EB配置步骤

EB Tresos是AUTOSAR常用的配置工具。ICU模块的配置,我个人习惯按这个顺序来:

4.2.1 通道配置

首先得选好哪个通道用来捕获。每个ICU通道对应一个GPIO引脚,这个引脚必须支持输入捕获功能。

  • IcuChannel:指定通道编号,比如IcuChannel_0
  • IcuChannelPin:绑定到具体的GPIO引脚,比如PortPin_5
  • IcuActivationEdge:选择触发边沿,可以是上升沿、下降沿或双边沿

我记得有一次,客户要求同时测量三路PWM信号。我配了三个ICU通道,每个通道独立配置边沿检测,互不干扰。这点AUTOSAR做得挺好,通道之间完全解耦。

4.2.2 边沿检测配置

边沿检测是ICU的核心。你要测频率,就配成上升沿触发;要测脉宽,就得配成双边沿触发。

测量目标 边沿配置 捕获次数
频率 上升沿 2次(两个上升沿)
脉宽 上升沿+下降沿 2次(上升沿和下降沿)
占空比 双边沿 3次(上升沿、下降沿、上升沿)

为什么会这样?因为占空比需要同时知道周期和脉宽。先捕获一个上升沿,再捕获下降沿得到脉宽,再捕获下一个上升沿得到周期。嗯,逻辑上就是这么回事。

4.2.3 滤波配置

信号抖动是个头疼的问题。ICU模块一般都有硬件滤波功能,可以配置滤波时间和采样方式。

  • IcuFilterTime:设置滤波时间,单位是定时器tick
  • IcuFilterMode:选择滤波模式,比如连续采样N次都一致才认为是有效边沿

我的经验:滤波时间别设太大。我曾经在一个项目中把滤波设成了10us,结果舵机信号脉宽只有1ms,滤波吃掉10%的精度。后来改成2us,效果就好多了。

4.3 驱动API实现

配置完EB,接下来就是写驱动代码了。AUTOSAR的ICU驱动API其实不多,常用的就这几个:

4.3.1 测量频率

测量频率的思路很简单:捕获两次上升沿的时间戳,差值就是周期。频率就是周期的倒数。

/* 频率测量示例 */
uint32 Icu_GetFrequency(uint8 Channel)
{
    uint32 TimeStamp1, TimeStamp2;
    uint32 Period;
    uint32 Frequency;
    
    /* 等待第一个上升沿 */
    while(!Icu_GetCaptureStatus(Channel));
    TimeStamp1 = Icu_GetCaptureValue(Channel);
    
    /* 等待第二个上升沿 */
    while(!Icu_GetCaptureStatus(Channel));
    TimeStamp2 = Icu_GetCaptureValue(Channel);
    
    /* 计算周期,单位是定时器tick */
    Period = TimeStamp2 - TimeStamp1;
    
    /* 转换为频率,假设定时器时钟80MHz */
    Frequency = 80000000 / Period;
    
    return Frequency;
}

这段代码看起来简单,但实际项目中要注意溢出问题。如果信号频率很低,周期很大,32位变量可能存不下。我建议用64位变量来存时间戳差值。

4.3.2 测量占空比

占空比测量需要捕获三个边沿:上升沿、下降沿、下一个上升沿。

/* 占空比测量示例 */
uint8 Icu_GetDutyCycle(uint8 Channel)
{
    uint32 Rise1, Fall, Rise2;
    uint32 PulseWidth, Period;
    uint8 DutyCycle;
    
    /* 捕获上升沿 */
    while(!Icu_GetCaptureStatus(Channel));
    Rise1 = Icu_GetCaptureValue(Channel);
    
    /* 捕获下降沿 */
    while(!Icu_GetCaptureStatus(Channel));
    Fall = Icu_GetCaptureValue(Channel);
    
    /* 捕获下一个上升沿 */
    while(!Icu_GetCaptureStatus(Channel));
    Rise2 = Icu_GetCaptureValue(Channel);
    
    /* 计算脉宽和周期 */
    PulseWidth = Fall - Rise1;
    Period = Rise2 - Rise1;
    
    /* 计算占空比百分比 */
    DutyCycle = (PulseWidth * 100) / Period;
    
    return DutyCycle;
}

注意:上面的代码是轮询方式,会阻塞CPU。实际项目中建议用中断方式。配置ICU中断,边沿捕获时触发中断,在中断服务函数里读取时间戳。这样CPU可以干别的事。

4.4 实战:测量舵机PWM信号

好了,理论讲完了,咱们来点实战。舵机PWM信号的标准参数是:

  • 频率:50Hz(周期20ms)
  • 脉宽:1ms(0度)到2ms(180度)
  • 占空比:5%到10%

我曾经调试过一个舵机项目,信号死活不对。后来发现是ICU的时钟分频配错了,导致测量值偏大。嗯,这种坑踩过一次就记住了。

实战步骤:

  1. 配置ICU通道:选择一个通道,绑定到舵机信号引脚
  2. 设置边沿检测:配成双边沿触发,因为要同时测脉宽和周期
  3. 配置滤波:设2us滤波,去掉毛刺
  4. 使能中断:每个边沿捕获都触发中断
  5. 编写中断服务函数:在中断里读取时间戳,计算脉宽和周期

中断服务函数的伪代码:

/* 舵机PWM测量中断服务函数 */
void Icu_IrqHandler(void)
{
    static uint8 EdgeCount = 0;
    static uint32 TimeStamps[3];
    
    /* 读取当前捕获值 */
    TimeStamps[EdgeCount] = Icu_GetCaptureValue(IcuChannel_0);
    EdgeCount++;
    
    if(EdgeCount == 3)
    {
        /* 三个边沿都捕获到了,计算脉宽和周期 */
        uint32 PulseWidth = TimeStamps[1] - TimeStamps[0];
        uint32 Period = TimeStamps[2] - TimeStamps[0];
        
        /* 转换为微秒 */
        PulseWidth_us = PulseWidth / 80;  /* 80MHz时钟,1us=80tick */
        Period_us = Period / 80;
        
        /* 计算舵机角度 */
        /* 1ms对应0度,2ms对应180度 */
        Angle = (PulseWidth_us - 1000) * 180 / 1000;
        
        /* 重置边沿计数 */
        EdgeCount = 0;
    }
}

避坑指南:我曾经遇到过一个问题,舵机信号偶尔会丢失一个边沿,导致EdgeCount永远到不了3。后来我加了一个超时机制,如果超过30ms还没捕获到完整信号,就强制重置。这样系统就不会卡死了。

最后说一句,ICU模块的调试其实不难。关键是把定时器时钟、滤波时间、边沿配置这几个参数搞对。只要这些配好了,测量结果基本不会出问题。嗯,今天就讲到这里,下一章咱们聊聊PWM模块的配置与驱动。