我做电机控制这行已经快十年了,从最开始的PIC到后来的STM32,再到这几年一头扎进TI的C2000系列,说实话,TMS320F28034这颗芯片在我手里至少量产过三个项目。每次和同行聊起这颗芯片,大家第一反应都是“CLA协处理器怎么用”“ADC触发怎么同步”,反而很少有人认真聊聊最基础的SCI串口。但恰恰是这个不起眼的SCI,在调试和产测环节卡住过无数人。今天我就把基于TMS320F28034的SCI技术从原理、寄存器到实际排查,完整梳理一遍,希望能帮正在被串口折腾的朋友少走弯路。
1. 项目整体设计与硬件平台理解
1.1 为什么是TMS320F28034
TMS320F28034属于TI C2000 Piccolo系列,主频最高跑到60MHz,C28x内核加上控制律加速器CLA,专门为实时控制场景设计。这颗芯片的定位很明确:电机控制、数字电源、逆变器这类对PWM精度和ADC采样时序要求极高的应用。我最早接触它是因为一个永磁同步电机项目,当时需要在极短的电流环周期内完成采样、运算和占空比更新,C2000的HRPWM高分辨率模块和12位ADC的硬件触发链是我选择它的核心原因。
但一个完整的控制系统,CPU再强也离不开与外界的沟通。调试阶段要看内部变量,产测阶段要校准参数,运行阶段要上报状态,这些都依赖UART串口。通信方案选型上,SPI速率高但引脚占用多且没有标准协议兜底,CAN抗干扰强但需要外加收发器和隔离,成本直接翻倍;相比之下SCI作为一颗芯片自带的UART外设,两根线就能解决90%的调试和通信需求,这也是它在工业控制板上出场率极高的原因。
1.2 SCI在项目中的角色定位
很多初学者把SCI单纯理解成“往寄存器里丢数据”,这其实低估了它在工程项目里的作用。在我做过的F28034项目里,SCI承担了三个层次的工作:第一层是调试打印,通过串口定时输出转速、电流环ID/IQ值、母线电压这些关键状态量;第二层是参数配置,上位机通过自定义协议修改PID系数、电流限幅等运行参数;第三层是产测通信,配合工装完成出厂校准数据的烧写和校验。
这三层需求对SCI提出了一些有意思的约束。调试打印要求每个周期至少要能输出几十个字节不丢帧;参数配置要求数据帧有明确的起始和结束标识,避免误触发;产测通信则对波特率稳定性和长时间运行的可靠性要求很高。这些约束直接决定了后面我们要怎么做FIFO配置、中断设计和协议封装。
1.3 本文的适用范围
如果你正在使用TMS320F2803x系列的任意一颗芯片,比如F28034、F28035、F28033,本文的代码和原理都直接通用,因为它们的SCI模块寄存器完全一致。如果你用的是F2806x、F2833x甚至更新的F2837x,寄存器结构大体相同,但外设时钟分频和引脚复用配置会有差异,原理部分依然有参考价值。哪怕你只是想把UART彻底搞明白,这篇也能给你提供一个从硬件到代码的完整视角。
2. SCI核心原理拆解
2.1 从UART角度理解SCI的本质
SCI全称是Serial Communications Interface,串行通信接口,本质上就是一颗增强型的UART。UART的全称是Universal Asynchronous Receiver/Transmitter,异步收发器,“异步”两个字是理解它的钥匙。所谓异步,就是通信双方没有共享时钟线,接收端靠什么知道数据从哪里开始?答案是靠波特率和起始位。
打个比方:两个人约定好每隔一秒钟说一个字,说话的人先咳嗽一声作为开始信号,然后每个字都严格按一秒的节奏输出。听话的人掐着表,听到咳嗽声就知道第一个字来了,之后每隔一秒再听下一个。这里“每隔一秒”就是波特率,而“咳嗽声”就是UART帧里的起始位。只要双方约定的节奏一致,即使没有时钟线也能准确通信。
SCI每次发送的最小单位是“一帧”,典型帧结构包含1个起始位(低电平)、5到8个数据位(低位在前)、可选的奇偶校验位、1到2个停止位(高电平)。F28034的SCI支持空闲线模式和地址位模式两种多机通信协议,绝大多数点对点通信场景用默认的空闲线模式就够了。
2.2 波特率计算与误差分析
波特率是SCI通信里最容易出问题的地方。F28034的SCI时钟来自LSPCLK,默认情况下LSPCLK等于SYSCLKOUT,也就是60MHz。波特率寄存器的计算公式在参考手册里写得很清楚:
BRR = (LSPCLK / (波特率 × 8)) - 1
以60MHz系统时钟、115200bps为例:60000000 / (115200 × 8) = 65.104,减去1后取整得到BRR = 64,十六进制就是0x0040,高8位写入SCIHBAUD为0x00,低8位写入SCILBAUD为0x40。实际波特率是60000000 / ((64 + 1) × 8) = 115384bps,误差大约0.16%,在可接受范围内。
再算一个9600bps的例子:60000000 / (9600 × 8) = 781.25,BRR = 780,十六进制0x030C,实际波特率约9603bps,误差不到0.04%。看到规律了吗?LSPCLK和波特率之间整除关系越好,误差越小。当BRR算出来等于0时,SCI会自动切换成16倍过采样模式,这时实际分频值是16而不是8,这个细节很多新手不知道,会导致低速通信时波特率算错。
我在项目里通常优先选择115200而非9600,除了传输速度快之外还有一个原因:115200在60MHz时钟下误差足够小,而且USB转串口芯片对115200的兼容性最好。如果你的系统时钟不是60MHz,比如用了外部晶振做PLL后得到50MHz或80MHz,一定要重新计算BRR,不要照搬例程。
2.3 发送和接收的底层数据流
理解了波特率,我们再看数据在SCI内部怎么流动。发送路径是这样:软件把要发的一个字节写入SCITXBUF发送缓冲寄存器,当发送移位寄存器SCITXSHF为空时,这个数据会被自动装入移位寄存器,然后按波特率一位一位从SCITXDA引脚移出去。发送状态寄存器里的TXRDY位是标志位,当SCITXBUF空闲时置1,告诉软件“你可以发下一个字节了”。
接收路径方向相反:外部信号从SCIRXDA引脚进入,接收移位寄存器SCIRXSHF按波特率把每一位装进来,完成一帧后把数据搬到SCIRXBUF接收缓冲寄存器,同时置位RXRDY。软件读取SCIRXBUF后,硬件自动清除RXRDY。这里有个隐藏的坑:如果软件读数据的速度太慢,新来的数据会覆盖缓冲区内还没被读走的数据,触发溢出错误。
SCIRXST寄存器里还汇总了各种错误标志,包括帧错误FE、溢出错误OE、奇偶校验错误PE和接收中断检测BRKDT,任何错误都会把RXERROR位置1。工程项目里不能只读数据不看错误标志,否则通信出问题的时候你根本不知道是硬件不稳定还是软件处理不过来。
3. 寄存器级配置与代码实现
3.1 关键寄存器功能对照
F28034的SCI寄存器虽然不少,但配置高频使用的就那么几个。SCICCR控制字符格式,包括数据位长度、奇偶校验方式、停止位个数和通信模式;SCICTL1是主控制寄存器,里面最关键的三个位是SWRESET软复位位、TXENA发送使能位和RXENA接收使能位;SCICTL2负责中断使能和状态标志;SCIHBAUD和SCILBAUD合起来构成16位波特率寄存器;SCIRXST是接收状态寄存器;SCITXBUF和SCIRXBUF是数据通道。
我在初始化SCI时有个习惯:先关全局中断,然后按顺序做——先复位SCI外设,再配置帧格式,再使能收发,最后写波特率,全部完成后再置位SWRESET释放复位。这个顺序不是随便定的,TI官方手册明确要求SWRESET必须在所有配置完成后再拉高,否则部分配置位可能在复位期间被覆盖。
3.2 标准初始化代码实现
下面这段代码是我项目里的SCI初始化函数,基于TI的ControlSuite库函数风格编写,在CCS环境下直接编译运行。
void SciA_Init(Uint32 baud) { // 1. 使能SCIA模块时钟 SysCtrlRegs.PCLKCR0.bit.SCIENCLKA = 1; // 2. 配置GPIO复用:使用GPIO28作为SCITXDA,GPIO29作为SCIRXDA GpioCtrlRegs.GPAMUX2.bit.GPIO28 = 1; // 推挽输出,SCI发送 GpioCtrlRegs.GPAMUX2.bit.GPIO29 = 1; // 输入,SCI接收 GpioCtrlRegs.GPADIR.bit.GPIO28 = 0; // 由外设控制方向 GpioCtrlRegs.GPADIR.bit.GPIO29 = 0; // 3. 外部引脚上拉,防止浮空误触发接收 GpioCtrlRegs.GPAPUD.bit.GPIO28 = 0; GpioCtrlRegs.GPAPUD.bit.GPIO29 = 0; // 4. 复位SCI,准备配置 SciaRegs.SCICTL1.bit.SWRESET = 0; // 5. 帧格式:8位数据,无校验,1个停止位,空闲线模式 SciaRegs.SCICCR.all = 0x0007; // 6. 使能发送和接收 SciaRegs.SCICTL1.all = 0x0003; // 7. 写入波特率寄存器 Uint32 brr = (SystemClock / (baud * 8)) - 1; SciaRegs.SCIHBAUD.all = (brr >> 8) & 0x00FF; SciaRegs.SCILBAUD.all = brr & 0x00FF; // 8. 释放复位,SCI开始工作 SciaRegs.SCICTL1.bit.SWRESET = 1; }这里SystemClock是宏定义,在项目里我统一定义为60L,对应60MHz。需要注意,brr可能超过16位范围吗?以最低常见波特率1200计算,60000000 / (1200 × 8) = 6250,远小于65535,所以Uint32定义是足够的。但如果你把LSPCLK配得很高又把波特率设得很低,理论上有溢出风险,建议计算时始终用32位变量。
3.3 查询方式收发一字节
初始化完成后,最直接的收发方式是查询标志位。发送一个字节前,先循环等待TXRDY为1,确保上一字节已经移出,然后写入SCITXBUF。接收一个字节时,循环等待RXRDY为1,读到数据后从SCIRXBUF取走。
void SciA_SendByte(Uint16 ch) { while (SciaRegs.SCICTL2.bit.TXRDY == 0); // 等待发送缓冲空闲 SciaRegs.SCITXBUF = ch; } Uint16 SciA_RecvByte(void) { while (SciaRegs.SCIRXST.bit.RXRDY == 0); // 等待接收数据就绪 return SciaRegs.SCIRXBUF.all & 0x00FF; }这种查询方式结构简单,适合逻辑不复杂的场景。但要注意,查询接收会死等,如果长时间没有数据进来,CPU就被这个循环占死了。我在电机控制主循环里绝不这么做,因为电流环和速度环的计算优先级高于串口,等不到数据可不能把控制任务卡死。查询方式我一般只用在初始化自检环节,比如上电后向上位机发送版本号。
3.4 中断方式收发
真正的工程实现必须用中断。F28034的SCI中断分成发送和接收两路,接收中断尤其重要,因为他能及时把这个字节处理的时机告诉软件。配置中断的步骤包括:设置PIE向量表中的SCI接收中断向量,使能PIE级中断,最后在SCICTL2里置位RXINTENA。
// 在main函数里执行中断注册 EALLOW; PieVectTable.SCIRXINTA = &SciA_RxISR; PieCtrlRegs.PIEIER9.bit.INTx9 = 1; // SCIRXINTA对应PIE第9组的第9个通道 EDIS; IER |= M_INT9; EINT; // 在初始化中使能接收中断 SciaRegs.SCICTL2.bit.RXINTENA = 1;接收中断服务函数里,核心任务是快速把SCIRXBUF的数据读走并存到环形缓冲区,避免溢出。环形缓冲区的设计有一个关键点:ISR只写入索引,主循环只读取索引,两者各自维护自己的读、写位置,不共享同一个变量,这样即使主循环在处理其他任务,缓冲区也不会被ISR写乱。
// 简单环形缓冲区 #define RX_BUFFER_SIZE 256 volatile Uint16 rxBuffer[RX_BUFFER_SIZE]; volatile Uint16 rxHead = 0; volatile Uint16 rxTail = 0; interrupt void SciA_RxISR(void) { Uint16 data = SciaRegs.SCIRXBUF.all & 0x00FF; Uint16 nextHead = (rxHead + 1) % RX_BUFFER_SIZE; if (nextHead != rxTail) // 缓冲区未满 { rxBuffer[rxHead] = data; rxHead = nextHead; } else { // 缓冲区满,可以选择丢弃或置溢出标志 rxOverflowFlag = 1; } PieCtrlRegs.PIEACK.all = PIEACK_GROUP9; // 应答PIE中断 }发送中断的使用场景不太一样。调试打印时经常要连续发一串字符串,如果每发一个字节都查询等待,CPU占用高。更好的做法是:先把整包数据放入发送缓冲区,使能发送中断TXINTENA,ISR里从缓冲区取下一个字节写入SCITXBUF,发完缓冲区最后一个字节后关闭发送中断并置一个完成标志。这样CPU在等待移位寄存器空闲的时间里可以继续跑控制算法,实测对电机控制计算周期的影响几乎可以忽略。
3.5 FIFO模式提升通信稳定性
F28034的SCI还有一个容易被忽略的增强功能:内置FIFO。默认情况下FIFO不使能,每一字节都直接触发中断。使能FIFO后,接收端可以积累到设定的阈值才触发中断,比如设置成一次收到8个字节才唤醒CPU,批量处理,这样能显著减少中断次数。
FIFO相关的寄存器有三个:SCIFFTX、SCIFFRX和SCIFFCT。SCIFFTX控制发送FIFO、接收FIFO复位和中断使能;SCIFFRX里的RXFFIL4-0位设置接收中断触发阈值;SCIFFCT用于设置字符延迟,这个参数的意义是:收到一个字节后,等待指定时间再去检查FIFO里有没有更多数据,适合不定长数据帧的解析。
以接收不定长协议帧为例,我的做法是启用FIFO,阈值设成1,同时把SCIFFCT设一个字符时间的延迟。这样每个字节到达后,SCI会先等一小段时间,如果后续字节快速到达就一起收进FIFO,如果一帧结束,主循环就会因为超时判定帧尾。配合接收FIFO的中断和主循环协议解析,整体通信效率比逐字节查询高一个量级。FIFO模式的初始化代码如下:
SciaRegs.SCIFFTX.all = 0xE040; // 使能发送FIFO和接收FIFO,设置发送中断触发级别为4 SciaRegs.SCIFFRX.all = 0x2040; // 使能接收FIFO,触发级别为4 SciaRegs.SCIFFCT.all = 0x0000; // 波特率校验,缺省为零即可4. 调试实录与常见问题排查
4.1 乱码问题:波特率不符是最常见的锅
串口调试遇到的第一个坎几乎都是乱码。我排过无数个乱码问题,归纳起来无非三类:波特率不匹配、时钟配置不一致、电平不兼容。
波特率不匹配的典型现象是:上位机设置115200,芯片实际跑的可能是9600或者别的速率,收到的全是乱码。排查方法很简单:用示波器或者逻辑分析仪抓MCU发送引脚的波形,看一个字节的实际位宽。比如115200下1位的时间大约是8.68微秒,如果实测位宽是104微秒,那实际波特率就是9600,问题出在BRR计算或LSPCLK配置上。
还有一个隐蔽的坑:有些项目在启动代码里把外设时钟做了分频,LSPCLK不再是60MHz而是30MHz或15MHz。这时候你即使套用标准公式也会算出错误的结果。排查时一定要先在调试器里查看系统时钟配置,确认LSPCLK的真实值,再决定BRR用多少。
电平不兼容也很容易被忽略。F28034的IO电平取决于电源域,一般是3.3V,但很多USB转串口模块是5V的TTL电平,直接连接不仅会产生乱码,长期运行还可能烧坏GPIO。正确处理是电平转换或用支持3.3V的USB转串口模块。模块输出如果是RS232电平,还需要MAX232做电平转换,切不可直接连。
4.2 收不到数据但发送正常
这是一个非常典型的不对称故障:MCU发数据到上位机完全正常,但上位机发数据给MCU,程序怎么都收不到。我从硬件到软件逐个排查,最常见的三个原因:
第一个是GPIO方向配置错误。SCIRXDA引脚虽然由外设接管,但GPIO的输入方向必须确认正确,有些引脚复用模式下还需要手动配置成输入;方向配错会导致外部信号根本进不到SCI模块。
第二个是引脚没有使能内部上拉。SCI空闲时RX线应该是高电平,如果线路上没有上拉电阻且引脚内部上拉又没打开,悬空的RX引脚容易受电磁干扰,产生一堆虚假的起始位,把状态机搞乱,导致真正的外部数据到达时SCI模块处于错误状态。
第三个是接收中断被别的中断长时间阻塞。PIE中断系统里,如果高优先级中断的服务函数执行时间过长,SCI接收中断就一直得不到响应,RXBUF被新数据覆盖,数据就丢了。排查方法是在ISR入口置一个GPIO,用示波器看它何时被拉高,对比SCI数据到达的实际时间,就能判断出中断响应是否延迟。
4.3 数据帧错位和粘包
当通信双方已经能收发数据,但应用层的协议解析总出问题,比如帧错位、粘包、半包,这就不是SCI外设本身的锅,而是协议设计不合理。很多新手刚上手时会犯一个错误:收到一个字节就立刻按协议解析,结果一帧数据还没收全,解出来的自然是乱的。
我的习惯做法是用“字节流+状态机”解析模式。主循环里不断从环形缓冲区取字节,通过一个状态机识别帧头、帧尾、长度和校验字段。帧头帧尾不是随便选的,最好用0xAA、0x55这种具有鲜明电平特征的值,可以自动消除上电瞬间的毛刺干扰。校验建议至少用累加和,关键通信场合用CRC16。
发送和接收共用一套帧结构时,还要设计好应答机制。工程里最常用的是:发送方发出请求帧,接收方处理完后应答,发送方收到应答后再发下一包。这个方法虽然降低了通信吞吐率,但极大提高了协议态的确定性,工业现场噪声多的环境下,宁可慢也不能错。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查步骤 | 解决方向 |
|---|---|---|---|
| 上电后全乱码 | 波特率不匹配 | 量位宽、看LSPCLK | 重算BRR |
| 只收不发 | GPIO方向错误 | 查GPAMUX和GPADIR | 配置复用方向 |
| 只发不收 | 接收数据线浮空 | 查上拉电阻 | 打开内部上拉 |
| 间歇性丢字节 | 接收FIFO溢出 | 查RXFFOVF标志 | 增大FIFO阈值或提速 |
| 协议解析错乱 | 帧格式设计不合理 | 打印原始字节流 | 改状态机、加校验 |
| 中断久了程序主循环卡死 | 查询等待阻塞 | 查看死循环位置 | 改用中断+环形缓冲 |
| 与5V设备连后损坏 | 电平不兼容 | 量IO电压 | 加电平转换 |
这张表里的每一项我都实际踩过。比如间歇性丢字节那个问题,我当时查了很久,最后发现是主循环里某个计算分支耗时太长,导致接收ISR被长时间占用,接收FIFO溢出。后来我把FIFO阈值从1调高到8,接收ISR就降到原来的八分之一,问题才缓解。所以排查串口问题时,不只盯着SCI本身,还要看看主循环的时间预算。
4.5 一个实用技巧:用SCI回环模式做快速自检
最后分享一个调试技巧。F28034的SCI有一个内置的数字回环测试模式,把SCICCR的SCICHAR位上面的回环使能位置1后,发送数据会直接在模块内部绕回来到接收端,不需要外部接线,就能验证SCI硬件是否正常。这个功能特别适合板卡贴片回来先跑“冒烟测试”的阶段。
回环模式代码很简单:
SciaRegs.SCICCR.bit.LOOPBKENA = 1; // 使能数字回环 SciA_SendByte(0x5A); Uint16 rxData = SciA_RecvByte(); if (rxData == 0x5A) { // SCI收发通路正常 } else { // SCI硬件存在问题 }注意回环模式下SCITXDA引脚不会输出信号,SCIRXDA引脚也不要接外部数据,否则回环数据和外部数据会叠加在一起。自检通过后,一定要把LOOPBKENA清回0,否则后续正常通信怎么都调不通。
5. 基于SCI的常用协议实践
5.1 命令响应式协议设计
有了稳定的SCI通信基础,具体做什么协议就是发挥空间了。我的电机控制器上位机协议是一个典型的命令响应式协议。帧格式定义如下:
帧头两个字节固定为0xAA 0x55,后跟1字节长度字段表示数据区长度,然后是命令字、数据区和两字节CRC16校验。长度上限设为128字节,防止缓冲区溢出。命令字分三大类:0x01命令表示读参数,0x02命令表示写参数,0x03命令表示控制电机启停。
协议解析在主循环里完成,每收到一帧合法数据,就解析命令并执行相关操作。执行结果通过应答帧返回给上位机,应答帧包含原命令字、执行状态和数据。这个设计保证了上位机和下位机的状态始终是同步的,即使中途发生通信错误,上位机重发命令即可。
我在实际项目里还加了一层防抖机制,同一命令连续三次失败就自动断开通信并报警,避免错误参数写入电机控制器导致危险。这个机制在产测环节挽救了无数个不明故障的板卡,强烈建议在控制类产品上加上。
5.2 多字节数据的字节序约定
SCI是字节流传输,但控制参数经常是多字节的,比如PID系数是float,速度值是int16。这里必须约定字节序,否则上位机和下位机各自解析的数值会南辕北辙。我在项目里统一使用小端序,也就是低位字节先发。理由很简单:F28034的C28x内核本身就是小端模式,直接取内存字节发出去就行,不用额外转换。
处理浮点数也踩过一个坑:有些编译器会把float在内存里排成大端,有些是小端,协议文档里必须写清楚。我的办法是所有浮点参数统一放大1000倍转成int32传输,接收端再除以1000恢复。虽然损失了一点精度,但彻底规避了字节序和浮点表示差异带来的坑,工业现场里一致性比极致精度重要得多。
5.3 用SCI实现简单的BOOTLOADER思路
SCI还可以扩展出在线升级功能,这个能力在产测和后期维护中非常省事。基本思路是:芯片上电后先运行一小段引导程序,等待一定时间,如果收到上位机的升级帧就进入下载模式,否则跳转到应用程序入口。
F28034的Flash操作需要调用TI的Flash API库,擦除和编程过程比较复杂,但整体框架并不神秘。下载过程一般采用块传输:上位机按每块512字节发送固件镜像,每块发完芯片回一个ACK,上位机收到ACK再发下一块。传输完成后,芯片跳转到应用程序入口。
这里必须提一个教训:升级过程中绝对不能掉电或断线,否则固件可能写成“半残”状态。我做的控制器在升级前会先把整个固件存入一个临时Flash分区,全部校验通过后才覆盖正式分区,升级失败还能回退到旧版本。这个双分区方案让在线升级的可靠性高了一个档次。
6. 最后一些实战体会
写到这里,SCI的基本原理和实操方法已经全部覆盖了。我想特别强调一个观点:串口技术本身不复杂,但它和你的系统时钟、中断优先级、协议状态机、甚至板级电平设计都强相关,任何一个环节出问题都会表现为“串口不好用”。所以遇到串口故障时,别只盯着SCIRXBUF和SCITXBUF这俩寄存器,把视野放宽到整个信号链路去排查。
以我个人的经验,调试SCI最快的路径是:先确认硬件接线和电平,再确认时钟和波特率,然后用回环模式验证外设本身,接着用逻辑分析仪看波形,最后再看软件逻辑。按这个顺序,90%的问题都能够在十分钟内定位。
如果后续大家感兴趣,我可以再写一篇如何基于F28034的SCI实现Modbus RTU从站协议的具体代码,那个在工控场景里应用非常广,我们下回再聊。