前言 / 导语

b站详细视频教程:红外传感器——AD模块

若打不开复制BV码到b站搜索即可:BV1ceoDBHE2Q

在上一篇《电机PWM驱动模块》中,我们通过 ePWM 外设为智能小车装上了“双腿”,实现了丝滑的差速调速。但一辆闭着眼睛狂奔的小车注定会撞墙,今天,我们要为小车装上“眼睛”——红外循迹与 ADC(模数转换器)模块。

本篇我们将直击底层痛点:彻底抛弃极其消耗 CPU 算力的 while(1) 软件轮询机制,手把手教你配置 TI C2000 芯片真正的工业级杀手锏——ePWM 硬件级 SOC (Start of Conversion) 触发 ADC 同步采样!


一、 传感器原理:世界是模拟的,单片机是数字的

智能车循迹常用的反射式红外传感器,核心由红外发射管和接收管组成,协同实现“光信号 - 电信号”的转换。

  • 红外发射管:工作时发射特定波长(940nm)的红外光,抗环境光干扰能力强。

  • 红外接收管:核心特性是“红外光照射时导通,无红外光时截止”。

物理工作逻辑:

  1. 对准白地(反射):白色大量反射红外光,接收管导通,输出微弱的低电平(接近 0V)。

  2. 对准黑线(吸收):黑色赛道吸收红外光,接收管截止,输出高电平(接近 3.3V)。

思考 Tips:为什么不用普通 GPIO 读取?

很多新手会问:“既然黑白对应高低电压,直接用单片机的普通引脚读 0 和 1 不就行了?”

在极其理想的实验室里可以,但在实际赛道上,场馆灯光干扰、传感器老化、甚至小车“压住一半黑线一半白地”时,电压往往徘徊在 1.5V 左右。用 GPIO 会导致极其严重的误判。

引入 ADC(模数转换器),单片机读到的就不再是死板的 0 和 1,而是一个连续的数值! F28027 搭载的是 12位 ADC,它能将 0~3.3V 的电压,精细地切分成 $2^{12} = 4096$ 份(即 0~4095)。这为我们后续编写 PID 巡线算法提供了完美的平滑误差数据。


二、 核心痛点:告别 low 爆的死循环轮询

知道了要用 ADC,下一个问题是:什么时候去读 ADC?

最简单粗暴的做法是在 main 函数里写个死循环,或者开个软件定时器疯狂读取。千万别这么干! 当你为了追求极低谐波,将底层的 PWM 载波频率强行拉高到 50kHz 时,硬件电路每秒钟要开关 5 万次。此时 CPU 的算力极其宝贵,根本没有时间去处理软延时和毫无节律的轮询。

工业级解法:硬件级联发令枪(SOC - Start of Conversion)

我们要让控制电机的 ePWM 模块跨界指挥 ADC!让底层 PWM 发波到谷底或顶点时,纯硬件触发 ADC 采样,全程 0 软件延迟,0 CPU 资源占用。


三、 核心代码逐块精讲:ADC同步采样的“铁三角”

要实现硬件级同步,我们需要打通三个部门:发令枪(PWM)、执行者(ADC)和收信员(PIE中断)。

1. 发令枪:PWM 事件配置 (Motor_eventConfigure)

在这里,我们赋予 PWM 模块唤醒 ADC 的特权。

void Motor_eventConfigure(void)
{
    // 1. 设置触发源:当 PWM 计数器等于周期值时(波形的顶点),准备开枪
    PWM_setSocAPulseSrc(myPwm3, PWM_SocPulseSrc_CounterEqualPeriod);

    // 2. 设置触发频率:FirstEvent 意味着每一个 PWM 周期都开一枪
    PWM_setSocAPeriod(myPwm3, PWM_SocPeriod_FirstEvent);

    // 3. 拔掉保险栓:正式启用 SOCA (Start of Conversion A) 脉冲输出
    PWM_enableSocAPulse(myPwm3);
}

2. 执行者:ADC 功能配置 (myAdc_functionConfigure)

ADC 模块必须提前设置好,告诉它“听到枪声后,去读哪个引脚的数据”。

void myAdc_functionConfigure(void)
{
    // 1. 配置采样窗口时长(Acquisition Window)
    // 给电容充放电留足时间,数值越大采样越准,但耗时越长
    ADC_setSocSampleWindow(myAdc, ADC_SocNumber_0, ADC_SocSampleWindow_7_cycles);

    // 2. 绑定通道:告诉 SOC0 听命于哪个引脚(比如红外传感器接在 ADCINA7)
    ADC_setSocChanNumber(myAdc, ADC_SocNumber_0, ADC_SocChanNumber_A7);

    // 3. 核心绑定:指定 SOC0 的触发源为 ePWM3 的 SOCA 信号!
    ADC_setSocTrigSrc(myAdc, ADC_SocNumber_0, ADC_SocTrigSrc_EPWM3_ADCSOCA);
}

3. 收信员:PIE 中断路由 (User_Pie_functionConfigure)

数据采好了,怎么通知大脑(CPU)?这就需要配置 PIE 中断中心,围绕“注册、使能”来打通路线。

void User_Pie_functionConfigure(void)
{
    // 【核心注册】:把我们写好的 ADC 中断服务函数地址,挂到 PIE 报警墙的第 10 组第 1 号通道上
    PIE_registerPieIntHandler(myPie, PIE_GroupNumber_10, PIE_SubGroupNumber_1, (intVec_t) &myAdc_ADCINT_isr);
}

void User_Pie_eventConfigure(void)
{
    // 1. 启用 ADCINT1 外设级中断通道
    ADC_enableInt(myAdc, ADC_IntNumber_1);

    // 2. 开启 PIE 控制器对应的组别开关
    PIE_enableInt(myPie, PIE_GroupNumber_10, PIE_InterruptSource_ADCINT_10_1);
}

四、 💡 避坑指南:中断只能进一次?

在开发期间,你极大概率会遇到这个诡异的现象:小车刚上电时,能成功打印出一次 ADC 读数,然后程序就像卡死了一样,再也不进中断了!

【终极解法】:

单片机是一个非常有规矩的系统。消防队(中断服务函数)出警结束后,必须向上级汇报“火已扑灭”,否则调度中心(PIE)会认为该区域还在火灾中,拒绝派发新的任务!

在你的 myAdc_ADCINT_isr 中断函数的最后一行,必须、一定、千万要加上清除标志位和应答的代码:

TARGET_EXT interrupt void myAdc_ADCINT_isr(void)
{
    // 1. 读取 ADC 结果寄存器,拿到红外循迹的数据
    uint16_t sensor_val = ADC_readResult(myAdc, ADC_ResultNumber_0);

    // (这里写你的巡线逻辑)...

    // 2. 清除 ADC 模块内部的中断标志(扑灭外围火情)
    ADC_clearIntFlag(myAdc, ADC_IntNumber_1);

    // 3. 向 PIE 中心发送 ACK 应答,告知第 10 组中断处理完毕!(解除封锁)
    PIE_clearInt(myPie, PIE_GroupNumber_10);
}

我们可以替换局部变量为全局变量,在观察窗口查看数值;也可以直接查看寄存器。

 

更多推荐