☰
DSP串口通信实战:SCI即UART,从寄存器配置到调试避坑
2026/10/20 16:33:25 网站建设 项目流程

这个系列的学习记录写到第 12 篇,终于轮到 SCI 了。SCI 的全称是 Serial Communication Interface,说穿了就是 DSP 上的异步串行通信接口,也就是大家熟悉的 UART。它的典型用途非常直接:跟 PC 上位机收发调试数据、跟传感器模组通信、跟其它板卡对接。我在做某个电机控制项目时,最开始根本不需要通信功能,但一旦要调参数、看波形、做上位机交互,SCI 就成了逃不掉的第一道关卡。这篇文章不会只贴寄存器手册,而是把我从头配置 SCI 到调通收发、再到被各种坑折磨的过程完整记录下来。跟着文章走一遍,你就能在自家板子上把串口跑起来,知道每一行配置在干什么,也知道出问题时该从哪几个方向查。

1. SCI是个什么东西:先用串口思维把外设看明白

刚接触 DSP 的人容易把 SCI 当成一个"神秘的外设",其实它的本质就是标准的异步串行通信,和单片机上的 UART 是同一个物种:一条 TX 线发送,一条 RX 线接收,双方约定好波特率,不需要时钟线,不需要地址线,就能把数据从一个设备搬到另一个设备。

但 SCI 又不完全是 UART,它在 DSP 这类控制芯片上做了不少面向实时控制的增强。比如接收端有双缓冲,发送端有专门的移位寄存器,部分型号还带 FIFO 和自动波特率检测。理解这些增强点,对后面写收发代码很有帮助。

先看它在整个通信接口家族里的位置。我在学习时做过一个简单的对比表:

接口通信方式需要时钟线典型用途特点
SCI/UART异步串行否上位机通信、调试日志、低速模块对接简单可靠,但速度偏低
SPI同步串行是片外存储、ADC、DAC速度快,但接线稍多
I2C同步串行是传感器、EEPROM两线制,支持多设备
CAN异步差分否工业总线、车载通信抗干扰强,距离远
并行接口并行需要旧式存储、高速数据总线速度快但占用引脚多

我自己的经验是:DSP 项目里 SCI 往往承担"和人打交道"的角色,SPI 和 I2C 承担"和芯片打交道"的角色。所以疫情搞上位机交互,学会 SCI 基本是硬需求。

接下来是异步通信的帧格式。SCI 每传输一个字节,先发送一个起始位(逻辑低电平),然后依次发送数据位,通常低位在前,最后是可选的校验位和停止位(逻辑高电平)。接收端不在数据期间发时钟,它靠的是采样:双方约定波特率,接收方在每个位周期内多次采样电平,拼出完整的字节序列。这就像两个人约定好每隔一秒说一个字,只要能对上节拍,不需要有人举节拍器。

这里必须理解一个关键点:正是因为异步通信没有独立时钟线,所以双方的波特率必须尽可能一致。如果发送方波特率是 9600,接收方实际却是 9800,短帧可能没事,连续几十个字节之后就会累积出采样错位,出现乱码。这就是后面波特率计算要抠细节的根本原因。

2. 上电第一件事:管脚复用、外设时钟和字符格式

我调试某款 DSP 时,发现很多新手的通病是一上来就查 SCI 寄存器,却忘了芯片外设默认是不使能的。DSP 的外设引脚大多复用,某个引脚到底是 GPIO 还是 SCI 的 TX/RX,完全由管脚复用寄存器决定。不配好复用,后面怎么写寄存器都是白搭。

所以正确的初始化顺序应该是:

  1. 使能 SCI 外设时钟;
  2. 配置 GPIO 引脚复用为 SCI 功能;
  3. 把 SCI 模块置于软件复位状态;
  4. 设置通信帧格式;
  5. 计算并写入波特率;
  6. 退出复位,使能收发。

先说时钟。在某款常用 DSP 上,SCI 挂在低速外设时钟 LSPCLK 上。很多人会忽略这一步,结果代码明明照着例程写的,串口就是没反应,JP 在时钟。使能方法通常是把 PCLKCRX 寄存器里对应位写 1。这里有个小经验:单独配 SCI 的时候,最好把用到的外设时钟一并打开,不然调试时会陷入"寄存器好像没错但就是不工作"的困惑。

然后是 GPIO 复用。假设我用的是某个 100 引脚的型号,SCI-TX 对应某个 GPIO 口。配置分两步:第一步,把 GPxMUX 的对应位设为功能 1(复用为 SCI);第二步,把 GPxDIR 设置为输出(TX 作为输出,RX 作为输入)。引脚方向如果配反了,最典型的现象是发出去的数据对端收不到,或者对端发来的数据进不了接收移位寄存器。别小看这一步,我曾经因为 RX 引脚方向没配成输入,调试了整整一个下午。

接下来是帧格式。SCICCR 寄存器是 SCI 的"性格设置中心",我要配置的位包括:

位域作用我的常用配置
SCICHAR数据位长度8 位
PARITY_ENA是否启用奇偶校验通常关闭
PARITY偶校验还是奇校验无关
STOP_BITS停止位长度1 位
ADDR/IDLE MODE地址模式还是空闲线模式空闲线模式
LOOPBKENA回环模式调试时开启

实际里我给某个温控模块写驱动,就是 8 数据位、无校验、1 停止位,也就是最常见的 8N1 格式。这个格式和大多数 USB 转串口工具默认配置一致,省去一堆麻烦。

帧格式配好之后,还有一个容易忽略的步骤:软件复位。SCICTL1 寄存器的 SWRESET 位控制 SCI 模块是否能工作。初始化期间应该先把 SWRESET 置 0,完成所有配置后再置 1,让模块正式进入工作状态。如果不按这个顺序,有些配置可能不会正确生效,而且状态寄存器里会挂着脏数据。

下面贴一段我实际用过的初始化代码框架,寄存器名以某系列 DSP 为准:

void SCI_Init(void) { // 1. 使能 SCI-A 外设时钟 // PCLKCR1.SCI_A_ENCLK = 1; // 2. 配置 GPIO:TX 复用、RX 复用,TX 为输出 // GPAMUX1 相应位置 1; // GPADIR 相应位 TX=1 输出,RX=0 输入。 // 3. 进入软件复位 SCIA.SCICTL1.bit.SWRESET = 0; // 4. 配置帧格式:8位数据、无校验、1停止位、空闲线模式 SCIA.SCICCR.all = 0x0007; // 5. 关闭环回模式(调试时可临时开启) SCIA.SCICCR.bit.LOOPBKENA = 0; // 6. 使能发送和接收 SCIA.SCICTL1.bit.TXENA = 1; SCIA.SCICTL1.bit.RXENA = 1; // 7. 波特率由 SCIAHBAUD、SCIALBAUD 决定,见下一节 // 8. 退出复位 SCIA.SCICTL1.bit.SWRESET = 1; }

这段代码的关键不在于逐行抄,而在于理解背后三个原则:先开时钟再动引脚,配置期间保持软件复位,全部设好再一次性唤醒模块。这样做可以避免很多随机性的寄存器错乱问题。

3. 波特率公式:算一次就忘不掉的配置流程

波特率是 SCI 配置里最需要动手算的部分,也是最能体现"为什么"的地方。DSP 计算波特率的公式很统一:

BRR = (LSPCLK / (波特率 × 8)) - 1

其中 BRR 是从 1 到 65535 的 16 位值,LSPCLK 是低速外设时钟频率。算出的 BRR 分别写入 SCIHBAUD(高8位)和 SCILBAUD(低8位)。

当 BRR 算出 0 或者波特率要求极高时,波特率直接就等于 LSPCLK / 16。这是一个特例,但大多数低速场景用不到,普通 9600 到 115200 的波特率基本都要走公式。

为什么是 8 倍?因为 SCI 在硬件上采用过采样机制,接收端在每个数据位时间内采样 8 次或 16 次,取多数派电平作为最终判定,提高抗干扰能力。8 倍系数的存在意味着 BRR 并不总是波特率的整数分频,算出来经常带小数。

举一个实际例子。假设 LSPCLK 是 120 MHz,目标波特率是 9600,代入公式:

BRR = (120000000 / (9600 × 8)) - 1 = 1562.5 - 1 = 1561.5

BRR 只能写整数,所以取 1561 或 1562。如果取 1561,实际波特率约等于 120000000 / ((1561+1) × 8) = 9600.77,误差不到 0.01%,完全没问题。如果目标波特率是 115200,那就有点意思了:

BRR = (120000000 / (115200 × 8)) - 1 = 129.21

取 129,实际波特率约等于 120000000 / ((129+1) × 8) = 115384.61,误差约 0.16%。这个误差通常可以接受,但如果对端设备对波特率精度格外敏感,或者传输超长帧,就有可能出现偶发乱码。

为了方便以后配置,我把常用波特率在某几个 LSPCLK 下的计算情况整理成表:

LSPCLK目标波特率BRR计算值取整BRR实际波特率误差
120 MHz96001561.515619600.77<0.01%
120 MHz115200129.21129115384.610.16%
120 MHz19200780.2578019200.64<0.01%
100 MHz115200107.51107115740.740.47%
150 MHz115200161.77162114930.29-0.23%
150 MHz96001952.2519529600.25<0.01%

从这个表能看到一个规律:目标波特率越高,BRR 越小,小数值取整带来的误差就越明显。比如 100 MHz 下尝试 115200,误差 0.47%,短帧可能没事,但如果是连续几十字节的报文,就可能出故障。

我的建议是:如果误差超过 1%,最好换一个 LSPCLK 分频,或者换一个能用 BRR 整数除尽的波特率。嵌入式领域 9600 和 19200 这种"低速整数友好"波特率,在 DSP 上反而常常是最稳的选择。

波特率还有一个隐蔽的坑:SCIHBAUD 和 SCILBAUD 必须一起写,并且要先写低字节后写高字节,或者用 16 位连续赋值,否则中间时刻可能出现一个错误的临时波特率。我在某次调试里因为分两条语句写,结果发送端在运行中莫名发错波形,找了大半天才发现是两个寄存器写入顺序的问题。

4. 数据收发:轮询、中断和FIFO的取舍

配置好引脚、帧格式和波特率之后,SCI 模块就处于可收可发状态。但真正让数据流动起来,还需要处理收发逻辑。我刚开始学习时用的是最简单的轮询方式,逻辑很直白:不断查询状态位,准备好就发、收到就取。

先说发送。要发送一个字节,把数据写入发送数据缓冲寄存器 SCITXBUF,硬件会自动把这个值移到内部的发送移位寄存器,然后逐位发送到 TX 引脚。发送状态有两个关键位:SCICTL2 里的 TXRDY 表示发送缓冲是否为空,TX EMPTY 表示内部移位寄存器是否也已发完。我犯过一次错:只查 TXRDY 就开始写下一个字节,实际上前一帧可能还没完全发完,导致两帧数据粘连。

循环发送的效果是:数据从缓冲寄存器搬到移位寄存器时,TXRDY 置 1,我们马上写下一个字节,这样只要查询速度够快,数据流能基本保持连续,性能还不错。

接收就更简单:硬件把 RX 引脚收到的数据装到接收移位寄存器,再转存到 SCIRXBUF,同时置 RXRDY 位。我们读 SCIRXBUF 就能拿到数据。但接收端如果数据来得比处理速度快,新的数据会覆盖旧数据,此时就会出现溢出错误(OE)。读取 SCIRXST 寄存器可以获得详细的错误标志,比如帧错误 FE 和奇偶校验错误 PE。

轮询代码的优点是简单、可控,缺点是 CPU 会被一直占着。如果主程序还要跑 PID 环路、状态机、采样控制,轮询串口简直就是资源灾难。所以实际项目里我更推荐用中断收发。

在 DSP 上使用中断,除了 SCI 模块自身要配置使能中断,还要经过一层 PIE 中断管理。很多人第一次写 SCI 中断时,模块里的中断使能位明明开了,中断也触发不了,就是因为遗漏了 PIE 这一层。需要做三件事:

  1. 在 PIE 控制寄存器里使能对应的中断组;
  2. 在 CPU 中断寄存器里使能对应的中断;
  3. 打开 CPU 的全局中断开关。

中断服务函数内部还要记得清标志,否则中断会反复进入。SCI 的中断标志机制和普通单片机略有不同:发送中断在 TXRDY 从 0 变 1 时触发,接收中断在 RXRDY 从 0 变 1 时触发,如果不清标志,CPU 会被打断到天荒地老。

在支持 FIFO 的 DSP 增强型 SCI 上,处理方式又能更进一步。SCIFFTX 和 SCIFFRX 寄存器可以配置发送和接收 FIFO 的触发深度,比如接收 FIFO 收到 4 个字节才触发一次中断。这样高频数据的处理压力会小很多,CPU 也不用为每个字节都被打断一次。部分型号还有自动波特率检测功能,通过 SCIFFCT 寄存器开启后,能根据接收到的特定字符自动校准波特率,调试时确实省心,但我觉得生产环境下还是手动固定波特率更可靠。

这里给一个带中断接收、轮询发送的最小框架,方便理解整个流程配合:

__interrupt void SCI_RX_ISR(void) { uint8_t rx_data = SCIA.SCIRXBUF.bit.SAR; // 读取数据 HandleCommand(rx_data); // 处理收到的东西 // 清接收中断标志,等下一帧 } void SCI_SendByte(uint8_t ch) { while (SCIA.SCICTL2.bit.TXRDY == 0) { } // 等待空闲 SCIA.SCITXBUF.bit.TXDT = ch; // 写数据 }

这种"中断接收入队,主循环集中处理"的结构,是我在 DSP 项目里最常用的套路。它比纯轮询灵活,又比 DMA 省心。如果数据吞吐量要求再上一个台阶,我再考虑把收发都转到 DMA,但目前的大多数场景用不上。

5. 调试实录:这块板子上最容易踩的一堆坑

这部分才是整个学习过程里最有血有肉的地方。我把这次学习 SCI 踩过的坑按"症状、排查路径、根因、解决方式"列出来,给后来人当排雷图。

坑一:波特率"看起来对",实际对不上。症状是接收端经常收到 0x00 或乱码,死磕寄存器半天找不到原因。后来我用示波器量 TX 引脚,发现一个位的实际时长和理论值有明显偏差。一查才知道,LSPCLK 的时钟源不是我想当然的那个频率,我被手册默认值误导了,实际经过分频器后变成了另一条频率,波特率自然全错。解决办法是先把时钟树完整画出来,确认 LSPCLK 的实际频率,再套用公式算 BRR。时钟树上一步没理清,后面全盘皆输。

坑二:TX 和 RX 接反。这个坑说出去很丢人,但真的很容易犯。尤其是用杜邦线临时接线的时候,我自己很容易把"TX 接对方 TX"当成直觉,结果双方谁都不理谁。正确的规则是 TX 接对方 RX,RX 接对方 TX,而且必须共地。如果目标板用独立电源供电,不共地的话,两边电平基准不一致,也会出现"发出去了但对方听不见"的现象。这三条基本错误(反接、不共地、单片机与对方电压不兼容)占了我早期串口故障的八成。

坑三:中断进不去。症状是仿真器单步运行时,手动给 RX 引脚置低电平模拟起始位,中断却没有触发。排查链路是这样的:先检查 SCI 模块接收使能,再检查 PIE 中断组,最后检查 CPU 控制寄存器。最后发现是 PIE 里对应中断组的使能位没置 1。这类问题在大型 DSP 上尤其常见,因为外设中断都要经过 PIE 控制器映射到 CPU,初学者很容易把这层和单片机混淆。记住一句话:模块使能只是必要条件,PIE 使能和 CPU 使能才是充分条件。

坑四:SWRESET 复位顺序造成的行为怪异。我曾在初始化代码里先退出复位,再设置帧格式和波特率,结果现象是第一次发送正常,第二次发送就卡死或者数据错位。多次尝试后发现,必须在 SWRESET 为 0 期间把所有通信参数都写好,最后再置 1 唤醒。硬件手册上的时序图不是画着玩的,这条顺序对 SCI 来说几乎强制性。

坑五:回环测试好用,但对端连不上。区别在于,把 TX 和 RX 短接做回环测试时,SCI 模块内部收发逻辑完好,不代表外部电平转换芯片和连接器没问题。我遇到过一块板子的 SCI 引脚经过电平转换芯片后,TX 通道虚焊,内部回环全通,外部上电就是不通。排查时可以用示波器分别打两种测试点:芯片引出脚和连接器外壳,对比信号在哪个环节消失,就能定位到具体故障点。

调试工具方面,我强烈建议在代码里预留一个"回环自检"模式。把 SCI 的 LOOPBKENA 位置 1,硬件会自动把发送数据环回到接收端,不经过外部引脚,用来验证模块本身是否正常。这个开关配合仿真器在线读寄存器,基本能把软件问题和硬件问题快速切分开。

另外,调试串口时逻辑分析仪是极好的伙伴。即使不带协议解析,单单看 TX 线上每一帧的起始位和停止位波形,就能判断波特率对不对、帧格式对不对。如果没有逻辑分析仪,也可以用示波器量位宽,算一算一个数据位的时间是否等于 1/波特率。我在没有逻辑分析仪的那段时间,就是靠这个笨办法排除了一大批问题。

最后再给一个实用建议:当你的 SCI 数据收发出现问题时,先怀疑电平,再怀疑时钟,其次怀疑接线,最后才是寄存器。这个排查顺序不是我拍脑袋想出来的,而是根据我多次调试经历提炼出来的规律。绝大多数所谓"玄学问题",最后都能归结到这四类原因之一。搞定了 SCI,后面再学 SPI 和 CAN 时你会发现,DSP 外设的学习路径其实是相似的:先时钟,再引脚,然后协议寄存器,最后中断。这一套流程走通一次,整个通信类外设都能触类旁通。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询