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的时钟分频配错了,导致测量值偏大。嗯,这种坑踩过一次就记住了。
实战步骤:
- 配置ICU通道:选择一个通道,绑定到舵机信号引脚
- 设置边沿检测:配成双边沿触发,因为要同时测脉宽和周期
- 配置滤波:设2us滤波,去掉毛刺
- 使能中断:每个边沿捕获都触发中断
- 编写中断服务函数:在中断里读取时间戳,计算脉宽和周期
中断服务函数的伪代码:
/* 舵机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模块的配置与驱动。