引言
在嵌入式开发中,当芯片自带的硬件SPI接口数量不足,或者需要适配非标准时钟极性与相位的SPI从设备时,利用TI C2000系列芯片(如F28P65x)强大的ePWM(增强型脉宽调制)模块来模拟SPI时钟是一种高效且灵活的替代方案。本文将详细介绍如何将ePWM配置为一个可精确控制频率和占空比的方波发生器,从而生成SPI从设备所需的串行时钟(SCLK),并提供完整的代码示例与调试建议。
为什么需要模拟?
应用场景分析
| 场景 | 说明 |
|---|---|
| SPI模块资源不足 | F28P65x最多提供4个高速(50MHz)SPI端口,若外接从设备超过此数量,可用ePWM扩展 |
| 非标准时序要求 | 某些SPI从设备需要特定的时钟极性与相位,通过ePWM可完全由软件自定义 |
| 节约外设资源 | 将硬件SPI保留给高速/关键设备,ePWM模拟用于低速或非关键设备 |
核心原理
ePWM模块内部包含时基(TB) 、计数比较(CC)、**动作限定(AQ)**等子模块。一个完整的PWM周期包含一个高电平和低电平,对应一个完整的SPI时钟周期。因此:
SPI比特率(SCLK频率)= ePWM开关频率
通过配置TBPRD(周期寄存器)和CMPA(比较寄存器),可生成50%占空比的方波,直接作为SPI时钟信号。
三步生成SPI时钟
第一步:计算频率
公式:
TBPRD = f_EPWMCLK / (2 * f_SPI_CLK)
参数说明:
TBPRD:ePWM时基周期寄存器值f_EPWMCLK:ePWM模块输入时钟频率(例如100MHz)f_SPI_CLK:目标SPI时钟频率
计算示例:
f_EPWMCLK = 100 MHz
f_SPI_CLK = 5 MHz
TBPRD = 100,000,000 / (2 * 5,000,000) = 10
第二步:生成波形
在向上计数模式下,设置 CMPA = TBPRD / 2 得到50%占空比。通过AQ子模块配置:
- 计数值等于
CMPA→ 引脚输出高电平 - 计数值等于
TBPRD→ 引脚输出低电平
生成的方波即为SPI_SCLK。
第三步:精确使能控制(使用TZ模块)
SPI从设备通常需要仅在数据传输期间有时钟 ,其余时间时钟应为空闲状态。利用ePWM的TZ(Trip Zone) 模块,结合外部使能信号(如GPIO或片选信号),可精确控制时钟的输出与关闭。
配置思路:
- 将TZ模块的
DCAEVT1或DCBEVT1事件配置为高电平有效 - 当使能信号为高时,触发TZ动作,将ePWM输出强制拉低(时钟停止)
- 通过反相逻辑或外部反相器,可实现"使能高有效→输出时钟"的正逻辑
简化方案: 若片选信号低有效,可直接配置TZ为低电平有效触发,使片选拉低时关闭时钟。
示例代码(基于DriverLib)
以下代码以F28P65x为例,配置ePWM1生成5MHz的SPI时钟,并演示TZ模块的基本使能控制(使用GPIO作为使能信号,高电平时关闭时钟)。
#include "driverlib.h"
#include "device.h"
void initEPWM_SPIClock(void)
{
// 1. 使能ePWM1模块时钟
SysCtl_enablePeripheral(SYSCTL_PERIPH_CLK_EPWM1);
// 2. 配置GPIO0为ePWM1A输出
GPIO_setPinConfig(GPIO_0_EPWM1A);
GPIO_setDirectionMode(0, GPIO_DIR_MODE_OUT);
// 3. 时基配置:向上计数,周期TBPRD=10(100MHz / (2*5MHz))
EPWM_setTimeBasePeriod(EPWM1_BASE, 10);
EPWM_setTimeBaseCounterMode(EPWM1_BASE, EPWM_COUNTER_MODE_UP);
EPWM_setTimeBaseClockDiv(EPWM1_BASE, EPWM_CLOCK_DIVIDER_1);
// 4. 比较值配置:CMPA = TBPRD/2 = 5
EPWM_setCounterCompareValue(EPWM1_BASE, EPWM_COUNTER_COMPARE_A, 5);
// 5. 动作配置:CMPA匹配时置高,PRD匹配时置低
EPWM_setActionQualifierAction(EPWM1_BASE,
EPWM_AQ_OUTPUT_A,
EPWM_AQ_OUTPUT_HIGH,
EPWM_AQ_OUTPUT_ON_TIMEBASE_UP_CMPA);
EPWM_setActionQualifierAction(EPWM1_BASE,
EPWM_AQ_OUTPUT_A,
EPWM_AQ_OUTPUT_LOW,
EPWM_AQ_OUTPUT_ON_TIMEBASE_PERIOD);
// 6. TZ模块配置:使能DCAEVT1事件,高电平时强制拉低输出(即关闭时钟)
EPWM_disableTripZone(EPWM1_BASE); // 先清零配置
EPWM_setTripZoneAction(EPWM1_BASE,
EPWM_TZ_ACTION_EVENT_DCAEVT1,
EPWM_TZ_ACTION_LOW); // 事件触发时拉低
EPWM_setDigitalCompareEventTrigger(EPWM1_BASE,
EPWM_DC_TYPE_DCAEVT1,
EPWM_DC_EVT_TRIG_HIGH); // 高电平有效
// 7. 启动ePWM
EPWM_setTimeBaseCounter(EPWM1_BASE, 0);
EPWM_enableADCTrigger(EPWM1_BASE, EPWM_SOC_A); // 可选,用于同步
}
注意: 上述示例中使能信号高电平时关闭时钟,如需反逻辑(使能高电平时输出时钟),可简单地将使能信号先反相输入到TZ引脚,或在软件中配置TZ事件极性(查阅具体芯片TRM)。
调试与验证
1. 示波器/逻辑分析仪
将探头连接到模拟时钟的GPIO引脚,观察波形:
- 频率是否等于目标SPI时钟
- 占空比是否为50%(或符合从设备要求)
- 使能信号控制下时钟是否正确启停
2. 代码级检查
- 确认ePWM模块时钟已使能:
SysCtl_enablePeripheral(SYSCTL_PERIPH_CLK_EPWM1) - 检查GPIO复用配置是否正确
- 在调试器中查看
TBPRD、CMPA、TZ相关寄存器值是否符合预期
3. 常见问题排查
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 无输出波形 | GPIO未配置为ePWM功能 | 检查GPIO_setPinConfig |
| 频率不正确 | TBPRD计算错误或时钟分频设置不对 | 核对ePWM输入时钟频率及分频系数 |
| 使能无法关闭时钟 | TZ事件极性配置错误 | 检查DCAEVT1触发条件及动作设置 |
扩展技巧
同步多路SPI时钟
若同一ePWM模块的A、B两路输出需作为两路独立SPI时钟,可将B通道配置为A通道的从同步模式,并设置相位偏移来控制两路时钟的延迟。
使用CLB(可配置逻辑模块)
对于极其复杂的时序要求(如带死区的时钟、多相时钟等),可考虑使用F28P65x内部的可配置逻辑块(CLB),实现更灵活的定制逻辑。
总结
方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 硬件SPI | 速度快、CPU负载低 | 数量有限、时序固定 |
| ePWM模拟 | 灵活、可扩展、节省外设 | 需占用ePWM资源、CPU参与数据收发 |
应用价值
使用ePWM模拟SPI时钟在资源紧张或需要高度定制时序的场景下极具价值。通过上述方法,您可以轻松生成精确的SPI时钟,并结合TZ模块实现精确的使能控制。配合GPIO模拟MOSI/MISO数据线,即可完整实现一个"软SPI"主机。
希望本文能帮助您在C2000平台上灵活应对SPI扩展需求。如有疑问,欢迎交流!
参考文献
- TI TMS320F28P65x Technical Reference Manual (TRM), Chapter: Enhanced Pulse Width Modulator (ePWM)
- TI C2000Ware DriverLib API Guide
========================================================================
明白了!您的实际需求是:SSI协议 (通常用于绝对编码器,如BISS、SSI),它没有片选信号,需要主机精确产生固定数量(32个)的时钟脉冲,然后停止,等待100μs后再产生下一组32个脉冲。即:帧周期100μs(10kHz),每帧32个时钟脉冲。
这比单纯的连续时钟复杂,核心挑战是如何让ePWM精确输出指定数量的脉冲后自动停止,并能周期性重启。
🎯 解决方案概览
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 方案一:ePWM单次触发 + 中断计数 | 用ePWM输出连续时钟,在中断中计数,达到32个后关闭ePWM;再用定时器重启 | 实现简单,灵活 | 中断响应有延迟,高速时可能丢脉冲 |
| 方案二:ePWM的"单发模式"+ 软件重触发 | 利用ePWM的EPWM_TB_COUNT_MODE_UP_ONESHOT模式(仅向上计数一次) |
硬件自动停止,精确控制脉冲数 | F28P65是否支持需要确认(C2000部分型号支持) |
| 方案三:ePWM + 事件触发计数器(ECAP/PCNT) | ePWM输出始终使能,但用外部计数器(或芯片内部ECAP)计数脉冲,达到32个后触发TZ关闭ePWM | 纯硬件,零CPU干预 | 需要额外资源 |
| 方案四:使用CLB(可配置逻辑块) | 用CLB搭建一个32脉冲计数器,自动门控时钟 | 最灵活,硬件自动 | 开发复杂 |
推荐方案一(中断计数+定时器重启):实现简单,在中等时钟频率(如2~5MHz)下完全可行,100μs的间隔足够CPU处理中断。
🔧 详细实现步骤(方案一)
整体思路
- 配置ePWM输出连续SPI时钟(例如5MHz)。
- 使能ePWM的周期中断 或比较中断,每输出一个时钟脉冲产生一次中断。
- 在中断服务函数中累计脉冲个数,当达到32个时,禁用ePWM模块的输出(或直接关闭时钟)。
- 配置一个CPU定时器(TIMER0/1/2) ,周期为100μs,定时器中断中重新使能ePWM输出并重置计数器,开始下一帧。
注意事项
- 中断优先级:定时器中断应高于ePWM中断,以确保帧周期精确。
- 关闭ePWM输出:可以用
EPWM_setOutputEnable()或直接禁止ePWM时钟。 - 为避免ePWM重新启动时相位不连续,可以同步重置时基计数器。
💻 代码示例(基于DriverLib)
以下代码实现:ePWM1输出5MHz连续时钟,每输出32个脉冲后自动停止,每隔100μs重新输出32个脉冲。
c
#include "driverlib.h"
#include "device.h"
// 全局变量
volatile uint16_t pulseCount = 0;
volatile uint16_t targetPulse = 32;
volatile uint8_t frameActive = 0; // 当前帧是否正在输出时钟
// 定时器中断周期 100us
#define TIMER_PERIOD_US 100
// 函数声明
void initEPWM_Continuous(void);
void initTimerForFrame(void);
__interrupt void epwm1ISR(void);
__interrupt void timer0ISR(void);
void main(void)
{
Device_init();
Device_initGPIO();
Interrupt_initModule();
Interrupt_initVectorTable();
initEPWM_Continuous();
initTimerForFrame();
// 使能中断
Interrupt_enable(INT_EPWM1);
Interrupt_enable(INT_TIMER0);
// 先启动第一帧:使能ePWM输出
EPWM_setOutputEnable(EPWM1_BASE, EPWM_OUT_A, true);
frameActive = 1;
while(1)
{
// 主循环可做其他事
}
}
// 配置ePWM1为连续5MHz时钟,使能周期中断(每个周期产生一次中断 = 一个时钟脉冲)
void initEPWM_Continuous(void)
{
SysCtl_enablePeripheral(SYSCTL_PERIPH_CLK_EPWM1);
// GPIO0 -> ePWM1A
GPIO_setPinConfig(GPIO_0_EPWM1A);
GPIO_setDirectionMode(0, GPIO_DIR_MODE_OUT);
// 100MHz EPWMCLK -> 5MHz PWM -> TBPRD = 10
EPWM_setTimeBasePeriod(EPWM1_BASE, 10);
EPWM_setTimeBaseCounterMode(EPWM1_BASE, EPWM_COUNTER_MODE_UP);
EPWM_setTimeBaseClockDiv(EPWM1_BASE, EPWM_CLOCK_DIVIDER_1);
EPWM_setCounterCompareValue(EPWM1_BASE, EPWM_COUNTER_COMPARE_A, 5);
// 动作:CMPA高,PRD低 -> 50%占空比
EPWM_setActionQualifierAction(EPWM1_BASE,
EPWM_AQ_OUTPUT_A,
EPWM_AQ_OUTPUT_HIGH,
EPWM_AQ_OUTPUT_ON_TIMEBASE_UP_CMPA);
EPWM_setActionQualifierAction(EPWM1_BASE,
EPWM_AQ_OUTPUT_A,
EPWM_AQ_OUTPUT_LOW,
EPWM_AQ_OUTPUT_ON_TIMEBASE_PERIOD);
// 使能周期中断(每个PWM周期触发一次 = 一个时钟脉冲)
EPWM_enablePeriodInt(EPWM1_BASE);
EPWM_setInterruptSource(EPWM1_BASE, EPWM_INT_TBCTR_U_PERIOD);
EPWM_setInterruptEventCount(EPWM1_BASE, 1); // 每次周期事件都触发中断
// 先禁止输出(由定时器启动)
EPWM_setOutputEnable(EPWM1_BASE, EPWM_OUT_A, false);
// 注册中断
Interrupt_register(INT_EPWM1, &epwm1ISR);
Interrupt_enable(INT_EPWM1);
// 启动ePWM时基(但无输出)
EPWM_setTimeBaseCounter(EPWM1_BASE, 0);
}
// 配置CPU定时器0,周期100us
void initTimerForFrame(void)
{
// 定时器时钟通常为 SYSCLK (200MHz 或 100MHz)
uint32_t timerClk = SysCtl_getClock(SYSCTL_DEVICE_OSCSRC) /
SYSCTL_SYSCLK_DIV_1;
uint32_t periodCycles = (timerClk / 1000000) * TIMER_PERIOD_US; // 100us
CPUTimer_setPeriod(CPUTIMER0_BASE, periodCycles);
CPUTimer_setPreScaler(CPUTIMER0_BASE, 0); // 1分频
CPUTimer_enableInt(CPUTIMER0_BASE);
CPUTimer_enable(CPUTIMER0_BASE);
Interrupt_register(INT_TIMER0, &timer0ISR);
Interrupt_enable(INT_TIMER0);
}
// ePWM中断:每输出一个时钟脉冲调用一次
__interrupt void epwm1ISR(void)
{
EPWM_clearEventTriggers(EPWM1_BASE);
EPWM_clearPeriodInt(EPWM1_BASE);
if(frameActive)
{
pulseCount++;
if(pulseCount >= targetPulse)
{
// 已经输出32个脉冲,立即关闭输出
EPWM_setOutputEnable(EPWM1_BASE, EPWM_OUT_A, false);
frameActive = 0;
pulseCount = 0;
}
}
else
{
// 未激活帧时,忽略多余中断(通常不会发生)
}
Interrupt_clearACKGroup(INTERRUPT_ACK_GROUP3);
}
// 定时器0中断:每100us触发一次,开始新的一帧
__interrupt void timer0ISR(void)
{
CPUTimer_clearInt(CPUTIMER0_BASE);
// 确保上一帧已关闭
EPWM_setOutputEnable(EPWM1_BASE, EPWM_OUT_A, false);
// 重置脉冲计数和状态
pulseCount = 0;
frameActive = 1;
// 可选:同步ePWM时基计数器,使每帧起始相位一致
EPWM_setTimeBaseCounter(EPWM1_BASE, 0);
// 使能输出,开始新帧
EPWM_setOutputEnable(EPWM1_BASE, EPWM_OUT_A, true);
Interrupt_clearACKGroup(INTERRUPT_ACK_GROUP1);
}
⚠️ 注意事项
- 中断延迟 :ePWM中断频率 = 5MHz = 每200ns一次中断!这对于CPU来说是极高的负载,且无法保证在下一个脉冲到来前完成中断处理。所以上述代码在高频时钟下不可行 。
➡️ 改进 :不要在每个时钟脉冲都进中断,而是利用ePWM的比较器在输出第32个脉冲时产生中断(例如设置CMPB = 32,然后使用"计数事件"触发中断)。但ePWM本身没有脉冲计数器,所以需要更巧妙的硬件方案。
🚀 更适合SSI的高效方案:使用ePWM的"单次触发"模式 + 硬件定时器
核心思路
利用ePWM的向上计数模式 + 软件强制同步 ,结合一个辅助定时器产生100μs的帧触发。但要求ePWM能输出恰好N个脉冲,最好的办法是:
使用ePWM的"单次触发"模式(One-shot mode) ,配置时基为向上计数,但设置TBCTL[FREE_SOFT] = 0,并在达到目标脉冲数后通过EPWM_forceSync()重新启动。不过F28P65的ePWM是否支持硬件单次触发,需要查阅TRM。
如果硬件不支持,可以用ePWM + ECAP的组合:
- ePWM始终输出连续时钟。
- ECAP模块捕获ePWM输出信号,配置为计数模式(捕捉每个上升沿),当计数值达到32时,产生中断,在中断中关闭ePWM输出。
- 定时器100μs中断中重置ECAP计数器并重新使能ePWM。
这样ePWM中断频率仍然是5MHz,但ECAP中断只在每32个脉冲后触发一次,大大降低CPU负载。
🧩 推荐最终方案:ePWM + ECAP 硬件计数
原理框图
定时器0 (100us) -> 清除ECAP计数值 -> 使能ePWM输出
ePWM输出 -> ECAP输入通道 -> ECAP计数模式 (上升沿) -> 计数值==32 -> ECAP中断 -> 关闭ePWM输出
关键配置
- ePWM:连续输出5MHz时钟(或您需要的任何频率),输出引脚同时连接到ECAP输入引脚(可通过内部连接或外部跳线)。
- ECAP :配置为绝对计数模式 (Capture mode),捕获上升沿,每次捕获后计数器加1,并可与预设值比较。或简单使用ECAP的"APWM模式"下的周期中断,但更简单的是:ECAP每次捕获产生中断,在中断中计数,达到32后关闭ePWM。这样仍然有中断,但频率降低为5MHz/32 ≈ 156kHz,仍较高。
更好的方法是:利用ECAP的多捕获模式,设置连续捕获32个事件后触发中断。查阅F28P65 TRM,ECAP支持"连续/单次捕获模式",可以设定捕获次数(如32),当达到设定次数后产生中断。这样中断频率 = 帧率10kHz,非常理想。
📝 最终代码框架(ECAP中断 + 定时器重启)
c
// 全局变量
volatile uint8_t frameActive = 0;
void initEPWM_5MHz(void)
{
// 同之前配置连续时钟,但不要使能ePWM中断
// 输出到GPIO,同时该GPIO也作为ECAP输入(内部路由)
}
void initECAP_PulseCounter(void)
{
SysCtl_enablePeripheral(SYSCTL_PERIPH_CLK_ECAP1);
// 将ePWM1A映射到ECAP1输入(通过GPIO输入交叉开关或直接内部连接,取决于芯片)
// 对于F28P65,可以使用Input X-BAR将GPIO0连接到ECAP1
InputXBAR_setInputPin(INPUT_XBAR_ECAP1, 0); // GPIO0
ECAP_enable(ECAP1_BASE);
ECAP_setCaptureMode(ECAP1_BASE, ECAP_ABSOLUTE_MODE);
// 配置为连续捕获32个事件后产生中断
ECAP_setCaptureEdge(ECAP1_BASE, ECAP_EVENT_1, ECAP_EDGE_RISING);
// ... 需查阅TRM设置捕获事件数量为32
ECAP_enableInterrupt(ECAP1_BASE, ECAP_INT_CEVT1); // 达到捕获次数中断
}
void initTimer_100us(void)
{
// 同前,定时器中断中重置ECAP计数器并启用ePWM
}
__interrupt void ecap1ISR(void)
{
ECAP_clearInterrupt(ECAP1_BASE, ECAP_INT_CEVT1);
// 已捕获32个脉冲,关闭ePWM输出
EPWM_setOutputEnable(EPWM1_BASE, EPWM_OUT_A, false);
frameActive = 0;
// 清除ECAP计数器,准备下一帧
ECAP_setCounter(ECAP1_BASE, 0);
}
__interrupt void timer0ISR(void)
{
// 开始新帧
ECAP_setCounter(ECAP1_BASE, 0);
EPWM_setOutputEnable(EPWM1_BASE, EPWM_OUT_A, true);
frameActive = 1;
// 清除定时器中断
}
✅ 总结
| 步骤 | 工具 | 作用 |
|---|---|---|
| 1 | ePWM | 产生连续的高频SPI时钟 |
| 2 | ECAP | 硬件计数时钟脉冲个数,达到32个后中断 |
| 3 | CPU定时器 | 每100μs启动新的一帧 |
这种方案将脉冲计数完全交给硬件(ECAP),CPU只需处理10kHz的帧中断和32脉冲完成中断,负载极低,且能精确控制32个时钟。
如需更具体的ECAP配置细节(如如何设置捕获32个事件),请查阅TMS320F28P65x TRM中"Enhanced Capture (ECAP)"章节的"连续捕获模式"说明。
🔧 详细实现步骤(方案三)
方案三:ePWM + 事件触发计数器(ECAP/PCNT)
除了用中断让CPU去关,利用eCAP直接输出硬件触发信号是更高效的选择。在F28P65x上,主要有两种不需要中断参与的硬件方法:利用eCAP的TRIP_OUT信号 和 将eCAP配置为APWM模式进行匹配触发。
⚙️ 方法一:利用eCAP的TRIP_OUT信号
这是最直接的硬件触发方式。eCAP模块内部有一个信号监控单元(Signal Monitoring Unit) ,专门用于检测输入信号的异常。你可以把它配置成一个计数器,当它检测到预设数量的脉冲后,就会输出一个TRIP_OUT信号。然后,只需在芯片内部通过X-BAR将TRIP_OUT连接到ePWM的TZ(Trip Zone)输入引脚,即可实现TZ硬件联动。
- 关键配置 :eCAP的
TRIP_OUT默认是使能的,你需要在X-BAR中选择正确的连接路径。这个方法优点是完全硬件实现,CPU零负载,触发延迟极低,非常适合对时序要求严苛的场景。 - 补充说明 :
TRIP_OUT是eCAP Type 1模块的新功能,由信号监控单元产生,相关文档建议在使用TRIP_OUT时,禁止ePWM的TRIPOUT反馈信号,避免可能发生的误触发。
⚙️ 方法二:配置为APWM模式,使用计数器匹配触发
这是方法一的变体。将eCAP模块配置为辅助PWM(APWM)模式。在这种模式下,它的行为就像一个简化的PWM发生器,内部有一个计数器,你可以设置一个比较值(Compare Value)。当计数器值等于这个比较值时,它会自动产生一个触发事件。如果把这个计数器"喂"上你想要的时钟信号(例如,配置eCAP去捕获你的SPI时钟),那么当它数到预设值时,就会触发动作。
- 注意事项 :此模式在eCAP作为捕获模式时不可用。而且,该触发事件通常还是需要中断来"响应",并非纯粹的硬件触发。
📊 两种硬件方案对比
下面表格清晰地展示了这两种硬件方案的区别:
| 对比维度 | eCAP的TRIP_OUT信号 | APWM模式计数器匹配 |
|---|---|---|
| 触发方式 | 硬件信号直接触发ePWM的TZ | 内部计数器匹配产生触发事件 |
| 是否需要中断 | 不需要 | 通常需要中断 |
| 触发源 | eCAP内部监控单元 | APWM模式下的计数器比较器 |
| 主要优点 | 完全硬件控制,CPU零负载,延迟低 | 配置相对简单 |
| 主要挑战 | 需要理解信号监控单元的配置 | 触发后仍需中断服务程序处理 |
| 适用场景 | 对实时性要求极高,希望完全解放CPU的SSI脉冲计数 | 更通用的触发场景,或eCAP未用于捕获时 |
💎 总结
- 首选方案 :追求最优解的话,方法一(使用eCAP的TRIP_OUT信号)是更彻底、更优雅的纯硬件解决方案,能实现完全的硬件自动化,最低的延迟和零CPU负载。
- 后备方案 :如果需要快速实现,可以评估APWM模式的可行性。但其"触发后仍需中断"的特性,意味着最终可能还是要回到你最初想避免的中断方案。