
简介一套基于TI TMS320C28377D浮点DSP的AD/DA转换与PWM波形生成基础示例工程面向嵌入式开发、电机控制与电源管理方向的初学者和工程师。压缩包内含完整CCS工程共73个文件以obj、d、mk等编译中间文件及ccsproject、cproject等工程配置为主同时保留blinky_dc_cpu01.c与blinky_dc_cpu02.c双核源码支持直接导入Code Composer Studio完成编译、烧录与在线调试map、out、launch等文件便于核对存储映射、烧写镜像和启动仿真器。整个压缩包仅417KB结构紧凑已有146人学习下载。借助工程可快速掌握28377D的外设初始化、模拟信号采集与输出、PWM时序控制并理解DSP双核协同工作的基本流程为后续工业控制或实时信号处理系统开发提供可复用的工程模板。1. 拿到 blinky_dc 这类 28377D 工程先分清 AD、DA 和点灯各自的位置解压blinky_dc.rar之前需要先理解这个包名在说什么28377D 是 TI C2000 系列里的双核 DSPTMS320F28377Dblinky 说明里面一定有个 GPIO 点灯程序dc 多半指直流偏置而 ad da 再把 ADC 采集和 DAC 输出这两条链路补上。换句话说这个工程要解决的事情很典型ADC 采一个直流电压DSP 处理后从 DAC 输出同时 GPIO 翻转做运行指示最后在 CCS 的 Watch 窗口里盯住某个引脚或变量。很多人卡住不是因为不会点灯而是这三条线在 28377D 上要共享中断优先级和触发链采样窗口配错一个数DAC 输出就会带毛刺。这篇内容适合正在调 C2000 采集回环的工程师也适合把旧工程往 28377D 迁移时做外设自检下面按外设配置、工程改造、调试观察和校准验收四条线展开。2. 28377D 的 AD/DA 外设配置采样窗口、触发源与输出缓冲是关键28377D 的模拟外设和普通 MCU 最大的区别在于触发方式。ADC 不是靠软件随便一读就能用的它的每一次转换都由 SOCStart of Conversion控制SOC 里同时绑定了通道、触发源和采样窗口DAC 则是带输出缓冲的电压型输出基准源决定满量程。这两个外设一旦配合不好最常见的现象是 ADC 读数跳动、DAC 输出电压偏低或者回环波形上出现周期性毛刺。2.1 12 位 ADC 的 SOC 与采样窗口参数要一起看28377D 内部有 ADCA、ADCB、ADCC、ADCD 四组独立的 12 位 ADC每组的转换逻辑都由最多 16 个 SOC 控制。每个 SOC 配置里包含三样东西要采哪个输入通道ADCIN0 到 ADCIN15、用什么事件触发、以及采样窗口ACQPS有多长。采样窗口这里先说结论硬件上决定 ADC 前端采样电容充电时间的不是转换位数而是 ACQPS。ADCCLK 由系统时钟分频而来SOC 里的 ACQPS 字段每增加 1采样时间大约多一个 ADCCLK 周期。SYSCLK 在 200MHz 附近时ACQPS 取 10 到 20 之间是比较稳妥的区间太小了高阻抗信号源来不及充电读数会偏低太大了又会挤占转换周期影响最高采样率。ADC 模块分辨率SOC 数量输入范围关键配置项ADCA / ADCB / ADCC / ADCD12 位每组 16 个0 ~ 3.3V 单端SOC 通道选择、触发源、ACQPSDAC 模块分辨率通道数输出范围关键配置项DACA / DACB / DACC12 位3 个独立通道0 ~ 基准电压DACVAL、基准源、加载模式触发源这里建议优先用 EPWM 事件而不是软件触发。软件触发每次都要往寄存器写同一个位多写几次可能因标志位未及时清除而丢失触发导致 ISR 里读到的还是旧数据。用 EPWM1 的周期事件做 ADC 触发采样频率由 PWM 周期决定天然稳定调试时改频率只需改一个寄存器。2.2 DAC 缓冲输出与基准不要为了省事用 PWM 滤波替代28377D 的 DAC 是真正的缓冲电压输出不是 PWM 加 RC 滤波。它内部有 12 位 DAC 内核和输出运放可以直接驱动 ADC 输入或者后级放大电路。DACVAL 寄存器写入 0 到 4095输出范围由基准电压决定默认接 3.3V 时理论 LSB 约为 0.8mV。需要特别注意 DAC 输出引脚上的负载电容。缓冲运放带宽有限如果直接在引脚上并联一个较大的滤波电容比如 1μF会在 DAC 输出电压跳变时出现明显过冲甚至引起运放自激。常见做法是在 DAC 输出和电容之间串一个几十欧姆的电阻形成一个钝化网络既滤波又不影响驱动能力。DAC 还有一个容易踩的坑是加载模式。部分 C2000 的 DAC 支持立即加载和同步加载两种模式回环场景里用立即加载即可让 DAC 值一旦写入立刻反映到引脚上只有在需要多通道绝对同步输出时才考虑用同步加载那时需要额外触发一次加载事件。2.3 用 ADC 中断把采集和 DA 输出固定在同一个事件序列采样和输出要同步最直接的办法是让 ADC 转换完成后触发中断在中断服务函数里同时做“读结果、写 DAC、清标志”。这样每次 EPWM 周期事件到来ADC 采一次、DAC 更新一次时序误差被压缩到一个固定延迟内。下面用 C2000 driverlib 风格写最小回环ADC 采 ADCA 的通道 0结果直接写进 DACA#include driverlib.h #include device.h #define ADC_SOC_NUM 0 // 使用 SOC0 #define ADC_CH_NUM 0 // 采样 ADCINA0 #define DAC_MODULE DACA_BASE volatile uint16_t adc_result; __interrupt void adca1_isr(void) { // 读取本次 ADC 转换结果 adc_result ADC_readResult(ADCA_BASE, ADC_SOC_NUM); // 把采集值直接写到 DAC形成回环 DAC_setAnalogValue(DAC_MODULE, adc_result); // 清中断标志并应答中断组 ADC_clearInterruptStatus(ADCA_BASE, ADC_INT1); INTERRUPT_CLEAR_FLAG(INTERRUPT_ACK_GROUP1); } void init_epwm_trigger(void) { // EPWM1 周期事件触发 ADCA 的 SOC0 EPWM_setClockPrescaler(EPWM1_BASE, EPWM_CLOCK_DIVIDER_1, EPWM_HSCLOCK_DIVIDER_1); EPWM_setTimeBasePeriod(EPWM1_BASE, 2000); // 200MHz/2000 100kHz EPWM_setTimeBaseCounterMode(EPWM1_BASE, EPWM_COUNTER_MODE_UP); EPWM_enableADCTrigger(EPWM1_BASE, EPWM_SOC_A); EPWM_setADCTriggerSource(EPWM1_BASE, EPWM_SOC_A, EPWM_SOC_TBCTR_ZERO); EPWM_setADCTriggerEventPrescale(EPWM1_BASE, EPWM_SOC_A, 1); } void main(void) { DEVICE_INIT(); // 注册并启用 ADCA 中断 1 INTERRUPT_REGISTER(INT_ADCA1, adca1_isr); INTERRUPT_ENABLE(INT_ADCA1); INTERRUPT_ENABLE(INTERRUPT_ACK_GROUP1); // ADC 配置 ADC_setMode(ADCA_BASE, ADC_RESOLUTION_12BIT, ADC_MODE_SINGLE_ENDED); ADC_setVREF(ADCA_BASE, ADC_REFERENCE_3_3V); ADC_setSOCConfig(ADCA_BASE, ADC_SOC_NUM, ADC_TRIGGER_EPWM1_SOCA, ADC_CH_NUM); ADC_setSampleWindow(ADCA_BASE, ADC_SOC_NUM, 12); // ACQPS12 // DAC 配置 DAC_setReferenceVoltage(DAC_MODULE, DAC_REF_ADC_VREFHI); DAC_setLoadMode(DAC_MODULE, DAC_LOAD_MODE_IMMEDIATE); DAC_enableOutput(DAC_MODULE); DAC_setAnalogValue(DAC_MODULE, 2048); // 初始输出中点电压 init_epwm_trigger(); // 开启 ADC 转换完成中断 ADC_enableInterrupt(ADCA_BASE, ADC_INT1); ADC_enableInterruptSignal(ADCA_BASE, ADC_INT1); ADC_setInterruptPulseMode(ADCA_BASE, ADC_PULSE_END_OF_CONV); ADC_clearInterruptStatus(ADCA_BASE, ADC_INT1); // GPIO5 即 PA5配置为输出用于点灯 GPIO_setPinConfig(GPIO_5_GPIO5); GPIO_setDirectionMode(5, GPIO_DIR_MODE_OUT); GPIO_setPadConfig(5, GPIO_PIN_TYPE_STD); while(1) { GPIO_togglePin(5); DEVICE_DELAY_US(100000); // 100ms 翻转一次 } }代码里几个参数需要说明。ACQPS12对应采样窗口约 1.5μs这个值对绝大多数信号源都足够如果信号源内阻偏高可以提高到 16EPWM_setTimeBasePeriod(EPWM1_BASE, 2000)决定了回环的更新频率改大这个值可以降低触发频率比如改成 20000 就是 10kHz 的回环便于观察波形INTERRUPT_CLEAR_FLAG(INTERRUPT_ACK_GROUP1)是 C2000 中断嵌套必需的应答操作漏掉会导致后续中断进不来。3. 把 blinky_dc 例程改造成 AD 采集、DA 回环、GPIO 翻转同时跑的最小工程压缩包里的原始工程和上一章的最小代码有差距直接烧写到 28377D 上大概率编译报错或跑不起来。这里要解决的三个问题是CCS 工程文件结构是否完整、链接脚本是否匹配 CPU1/CPU2 的内存布局、以及编译选项是否和驱动库版本冲突。3.1 解压 blinky_dc 后先确认工程文件和 cmd 链接脚本导入 CCS 的路径是 Project - Import CCS Projects选择解压后的目录CCS 会识别.project和.cproject文件。如果 CCS 版本比工程创建时高会弹出 migration 提示这时建议选择不自动迁移保留原编译选项避免编译器版本跨越导致 driverlib 头文件不兼容。导入后优先看两个东西一个是工程目录下的.cmd文件另一个是是否包含全局变量定义文件比如dsp2833x_globalvariabledefs.c这类28377D 工程里通常对应device.c或DSP2837xD_GlobalVariableDefs.c。链接脚本决定代码段和变量段放在哪块内存28377D 是双核芯片CPU1 和 CPU2 的 RAM 地址空间重叠如果工程文件同时包含 CPU2 的链接脚本而你又只在 CPU1 上调试必须把 CPU2 那段从编译列表里移除否则链接器会报重名或地址越界。工程文件清单通常是这样的文件类型典型文件名作用主程序main.c / blinky_dc.c外设初始化和主循环驱动库driverlib.lib 或 driverlib 源码ADC、DAC、GPIO 等外设驱动链接脚本2837xD_FLASH_lnk_cpu1.cmd定义内存段和目标段分配系统初始化device.c / sysctrl.c时钟、看门狗、片上外设时钟使能中断向量pie.c / 中断表定义PIE 中断向量映射cmd 文件里最常见的错误是栈大小设置太小。28377D 的 ADC 中断服务函数如果用了浮点运算或者较多局部变量栈需求会明显上升建议把stack_size从默认的 0x400 改到 0x800 或更大否则跑一段时间后会出现神秘的跑飞现象。3.2 最小改动让 AD、DA、GPIO 三条线在同一工程里运行原始 blinky 工程里通常只有 GPIO 闪烁改造成本最小的方法是保留原工程的初始化框架把上一章的 ADC 和 DAC 配置代码替换进去然后确认三件事ADC 输入引脚没有和点灯 GPIO 复用、DAC 输出引脚没有悬空、中断向量表里 ADCA1 的入口和中断服务函数名一致。让 PA5 对应的 GPIO5 做点灯输出同时让 ADC 采一个不冲突的模拟通道。28377D 的 GPIO5 在默认状态下是 GPIO 功能可以配置成输出ADC 通道则从 ADCINA0 接入物理引脚上二者互不影响。如果目标板把 PA5 引到了 LED 上可以在主循环里翻转 PA5每个周期翻转一次刚好作为运行指示灯。主循环里的延时要注意喂狗问题。很多 C2000 例程会开启看门狗DEVICE_INIT()里可能已经把看门狗关掉了但如果你在拷贝代码时把DEVICE_INIT()精简掉了主循环里长时间延时会导致看门狗复位。解决办法有两个要么保留DEVICE_INIT()要么在DEVICE_DELAY_US()的循环里定时喂狗推荐前者因为DEVICE_INIT()还负责把 CPU1 和 CPU2 的时钟配置统一。3.3 编译链接遇到的三个典型报错与对应处理第一个常见报错是unresolved symbols remain这种问题百分之九十出在链接脚本或全局变量定义文件没包含。打开工程属性里的 Build Settings在 Linker 的 File Search Path 里确认 include 路径和库路径都对再把DSP2837xD_GlobalVariableDefs.c加进编译列表。第二个报错是中断向量入口找不到比如adca1_isr没有被链接进最终镜像。C2000 的中断注册要么用INTERRUPT_REGISTER宏要么在 PIE 向量表里手动填地址如果用了宏但宏引用的函数被编译器优化掉了比如定义为 static 且未使用就会报向量缺失。解决办法是把中断服务函数定义成非 static并在调用的地方用INTERRUPT_REGISTER显式注册。第三个报错是内存地址重叠现象是链接器提示section .text cant be allocated。这多数是因为把 CPU1 的链接脚本和 CPU2 的链接脚本同时加入了编译。28377D 的 CPU1 和 CPU2 各自有独立的程序空间共用一份.cmd 会导致地址分配错乱确保编译列表里只保留当前核的链接脚本。编译通过后烧写时还要注意仿真器连接速度。有些 XDS 类仿真器在 5MHz 以下最稳定如果连接时频繁掉线可以把仿真器时钟降下来再试。这一步不解决后面 Watch 窗口和数据观察都会断断续续。4. 用 CCS 的 Watch 窗口盯着 PA5 引脚定位 AD/DA 数据异常dsp_watchpa5这个名字里的 watchpa5我理解就是在 CCS 调试器里把 PA5 相关的信号加进 Watch 窗口观察。PA5 对应的是 GPIO5在 28377D 上属于 Port A 的第 5 位这个引脚既可以做普通 GPIO也可以复用成其他外设功能。调试 AD/DA 回环时盯着 PA5是为了确认点灯翻转没有干扰 ADC 的采样时序同时也能借助点灯频率直观判断主循环是否还在跑。4.1 把 ADC 结果和 PA5 电平一起加进 Watch 表达式代码里定义好的adc_result变量在调试界面右键选择 Add Watch Expression就能实时看到 ADC 采集值。这个变量的地址在调试会话开始后会被自动解析数值以无符号整数显示。如果想让 Watch 窗口直接读寄存器而不依赖 C 变量可以写表达式((unsigned int*)0x0000A000)[0]这种地址强转但不同工程里 ADC 结果寄存器基址可能不一样建议直接用变量名加断点更稳妥。Watch 窗口的刷新模式建议这样设置右键表达式选择 Continuous Refresh 并把刷新间隔设到 500ms 到 1s。间隔太短会占用仿真器带宽导致程序执行变慢点灯频率肉眼可见地下降间隔太长又看不出 ADC 值随输入电压的实时变化。同时把 PA5 的电平也加进 Watch。GPIO 数据寄存器会同时反映一组引脚的电平如果想单独看 PA5表达式可以写成读取 GPIO 数据寄存器后按位与的运算CCS 的 Watch 窗口支持直接写表达式例如GPIO_readPin(5)。4.2 用断点核对 PA5 的复用状态和电平翻转如果 Watch 窗口里 PA5 的电平一直没变化第一步不是怀疑引脚坏了而是确认引脚复用寄存器。28377D 的 GPIO 有 PINMUX 寄存器组GPIO5 可以被复用为 EPWM 或 SCI 等功能。.c文件里如果调用了GPIO_setPinConfig(GPIO_5_GPIO5)就明确把 GPIO5 配成了普通 GPIO如果没调默认状态下它可能仍处于 GPIO 模式但也可能被之前的初始化代码设成了别的功能。检查办法是在调试会话里打开 Registers 窗口找到 GPIO 相关的 PINMUX 寄存器核对 bit 段的值。点灯逻辑放在主循环里时可以用断点确认程序有没有进主循环。在GPIO_togglePin(5)那一行打一个断点全速运行后如果断点命中说明主循环在跑PA5 没翻转是别的原因如果断点不命中说明程序卡在某个中断里或者跑飞了。这个排除法比直接看波形更快。PA5 还有一个作用是可以用来测量 ISR 的执行时间。把 PA5 的输出在中断服务函数里翻转一次用示波器看这个引脚的脉冲宽度就能估算出 ADC 读取加 DAC 写入的总耗时。如果脉冲宽度相比 EPWM 触发周期过大说明中断里做了太多事应该把不需要的操作移出去。4.3 用 graph 工具把 AD-DA 回环波形画出来Watch 窗口适合看单个数值但要看回环是否线性建议用 CCS 自带的 graph 工具。Tools - Graph - Dual Time Graph把显示数据地址指向adc_result变量Buffer Size 设成 256 或 512数据格式选 Unsigned 16-bit采样率设成和 EPWM 触发频率一致就能看到一段连续的 ADC 采样曲线。在adc_result前面加一个大数组作为环形缓冲区中断里往数组里写数据graph 工具直接画这个数组效果等同于一个简易示波器。这个方法特别适合看 DAC 输出回灌到 ADC 时是否有毛刺或者台阶。如果波形在某几个码值上出现周期性跳变优先怀疑 EPWM 触发和 ADC 中断之间的相位问题常见做法是把ADC_setInterruptPulseMode从ADC_PULSE_END_OF_CONV改成ADC_PULSE_END_OF_ACQ让中断在采样结束而不是转换结束时报出减少信号源充电对结果的影响。观察完波形之后把点灯函数从主循环挪到 ISR 里或者反过来观察 PA5 的翻转频率和 ADC 波形是否出现关联性抖动。如果点灯翻转瞬间 ADC 波形变差说明电源或地平面被 LED 电流干扰这时需要检查板级的电源去耦电容位置。5. 把 AD-DA 回环误差压到 1% 以内零漂与增益校准的具体做法回环能跑通只是第一步DA 输出端接回 AD 输入端时理想情况下 ADC 读到的值应该等于 DAC 写入的值。实际读数和写入值之间会有零漂和增益误差零漂来源是 ADC 的偏移误差和 DAC 输出级的小幅失调增益误差则与基准电压的精确值有关。解决方法是做一次简单的两点校准。校准流程分成两步。第一步把 DAC 输出写到 0等输出稳定后读 ADC记录为offset第二步把 DAC 输出写到 4095读 ADC 记录为gain。之后每次读数都通过线性映射转到 DAC 码值uint16_t adc_to_dac_calibrated(uint16_t raw, int16_t offset, int16_t gain) { int32_t val (int32_t)raw - offset; if(val 0) { val 0; } val val * 4095L; val val / (int32_t)(gain - offset); return (uint16_t)val; }这个函数把 ADC 读数先减去零漂再按增益差缩放回 12 位范围。注意乘法用int32_t避免溢出因为 raw 最大 4095减完 offset 后乘以 4095 最大约 1600 万16 位整数不够用。两个校准点之间是纯线性近似实际 DAC 和 ADC 的传输曲线在中段线性度很好误差通常在几个 LSB 以内。校准完成后做三档验收DAC 写入值理论输出电压ADC 回读值判定标准00Voffset 附近偏差小于 10 LSB20481.65V2048 附近偏差小于 20 LSB40953.3Vgain 附近偏差小于 20 LSB三档全部通过回环误差就能控制在 1% 以内。如果第二档偏差明显大于第一档和第三档说明线性度问题比增益问题严重建议检查 DAC 输出引脚上的负载是否过重以及 ADC 采样窗口是否过短。顺便把 PA5 的点灯节奏从 100ms 改成 500ms用示波器数脉冲频率就能验证主循环没有因为校准函数里的浮点运算而变慢。本文还有配套的精品资源点击获取