简介:本资源为TI官方TL16C754C四通道UART收发器的完整数据手册PDF,面向嵌入式硬件工程师、通信协议开发者及需要选型或调试多串口方案的工程师,主要解决高速多通道串行通信中数据缓冲、流控与中断管理的设计问题。内容包括64字节收发FIFO、自动RTS/CTS流控、可编程Xon/Xoff字符、DMA触发阈值配置、Modem控制信号及IrDA红外通信支持,并涵盖1.8V至5V宽电压工作与最高3Mbps波特率实现方法。资源为单个PDF文件,约1.9MB,便于直接查阅参数、引脚定义、寄存器说明与应用电路参考,也可作为ST16C654/654D兼容替代设计的规格依据。目前已有217人学习下载,适合在嵌入式UART扩展、工业通信接口或多串口服务器等项目中作为官方设计参考资料使用。
1. UART收发器不是「串口转接器」:64字节FIFO背后的背压机制
UART收发器看起来是一个早已没有秘密的老外设:发送端把并行字节变成串行位流,接收端再做反变换,协议层几十年没怎么变过。但在真实系统里,串口丢字节、数据断流、DMA被频繁打断的问题从来不少,根因往往不在协议,而在缓冲与背压机制的配合上。TL16C754C这类四通道UART给每个通道配备了64字节FIFO,把触发级别做成可编程,还加入了自动RTS/CTS与Xon/Xoff软件流控——这些设计不是为了把缓冲区「做大」,而是为了在软件来不及响应时由硬件自己踩刹车。搞懂FIFO水位、触发阈值和流控闸门如何联动,才算真正会用多通道UART,而不是靠玄学调参。
2. 四通道架构与FIFO设计:共享总线为何能维持四路独立
数据手册里有一句话是理解整个芯片的钥匙:“The four UARTs share only the data bus interface and clock source, otherwise they operate independently.”四路UART共享数据总线和时钟源,但每一路都有自己的寄存器组、FIFO、波特率生成器和modem控制逻辑。这句话说起来简单,落到芯片内部其实牵涉到两级译码、FIFO存储结构和触发阈值三层设计。
2.1 独立ACE与「片选+地址」的两级译码
芯片内部实际上是四个完整的ACE(Asynchronous Communications Element,异步通信单元),每个ACE都是独立的收发状态机。它们唯一共享的是外部8位数据总线和XTAL1时钟输入,除此之外,每一路的中断、流控、休眠、IrDA配置全部独立。从CPU视角看,访问某个具体寄存器要经过两级译码:第一级用CSA、CSB、CSC、CSD片选挑中哪一路,第二级用A0、A1、A2三个地址位在该通道的寄存器窗口里挑具体寄存器。引脚分配很直观:CSA在7脚、CSB在11脚、CSC在38脚、CSD在42脚,地址位A0到A2分别落在24、23、22脚,读选通IOR在40脚、写选通IOW在9脚。一次IOR或IOW周期只能命中一个通道的一个寄存器。
这种结构的设计动机很清晰。把四个ACE做成完全独立的状态机,可以杜绝通道间状态串扰——A通道配置成IrDA模式不会影响B通道的RS-485模式,每个通道的FIFO水位各自独立计算。共享数据总线则是典型的引脚经济学:8位数据总线加3位地址加4位片选,十几个引脚就能挂四路串口。相比用四颗单通道UART外扩,PCB布线面积、片选译码逻辑和软件驱动复杂度都大幅下降。
不过要注意,别把「共享总线」理解成某种并行处理能力。一次总线周期只能操作一个寄存器,软件要轮询四路状态就得分四次访问。频率一高,「状态轮询开销」就成了压在软件头上的实际成本。这正是FIFORDY寄存器存在的原因——它把四路的FIFO状态压缩成一次读,等读到状态字之后,再按需逐路搬运数据。FIFO越大、数据率越高,这种访问粒度的差异就越明显。
2.2 64字节FIFO的真实存储结构:每字节额外携带3位错误标志
很多开发者把64字节FIFO想象成「一个能装64个字节的数组」,实际上接收FIFO的存储结构要更讲究。数据手册明确写道:接收FIFO最多存64字节,且每字节都附带3位错误状态。也就是说,FIFO的每个条目不是8位宽,而是11位宽——8位数据加3位伴随的错误标志。
常见的实现思路是把FIFO做成64×11的寄存器堆或SRAM阵列,写入时在同一个周期把「数据+当时算出的错误状态」作为一个整体压入;读取时这11位一起弹出。错误标志和字节强绑定,而不是和「位置」绑定。这一点直接决定了软件读LSR(Line Status Register,线路状态寄存器)的方式:LSR当前显示的错误位,对应的是FIFO头部那一个字节的错误状态;头部字节被弹出后,LSR里的错误信息立即换成下一个字节的。如果你先读了数据寄存器再读LSR,拿到的就是错位的现场。
与错误标志绑定存储配套的,是溢出检测。当64字节已满、移位器里又收到一个完整字符时,硬件置溢出错误。这个错误不针对任何具体字节,而是针对「来不及取走」这一事件本身。所以芯片把错误分成两类:字节级错误(奇偶错、帧错、break)走3位伴随标志;事件级错误(FIFO overflow)由状态寄存器单独暴露。这种拆分让软件既能按字节精确归因,又能感知整体溢出风险。
2.3 触发级别:从复位后1字节到FCR/TLR两级可配
FIFO深度只是容量,触发级别才决定行为。复位之后,发送和接收FIFO都处于禁用状态,等效触发级别为1字节——也就是说你写了第一个字节就会触发「发送缓冲空」中断,完全看不出64字节容量的存在。之后软件通过FCR选择固定触发档位,或通过TLR寄存器做可编程触发。
这里有两个不同层次的「水位」概念很容易混。FCR/TLR决定的是「何时产生中断或DMA请求」,面向的是CPU和DMA控制器;后面流控章节要讲的TCR阈值决定的则是「何时由硬件自动刹停收发」,面向的是对端设备。同一个FIFO,一套水位通知本地软件来搬数据,另一套水位通知对端设备暂停发送,两套机制独立配置、协同工作。
FCR固定档位适合快速上手,选一个接近DMA burst长度的档位,中断频率和响应延迟大致可控。TLR可编程触发则适合精确对齐——比如DMA每次搬运16字节,就把接收触发点设成16,让一次DMA请求正好搬完一批数据。64字节深度在这里的真正价值,是把触发点抬高之后仍然留有余量:触发点太高,溢出窗口小;触发点太低,中断太频繁。深度给了水位调整的空间,而触发级别才是把这个空间用好的旋钮。
3. 时钟链与波特率生成:极限速率是怎样被算出来的
「最高3Mbps」是这颗芯片最显眼的宣传点,但这个数字不是白给的,它由输入时钟、预分频选择、除数锁存器、16倍过采样四段链路共同决定。实际调波特率翻车的人,多半是只算了除数,没算整条时钟链。
3.1 从XTAL1到采样时钟:16分频与三次取样投票
时钟链路是这么走的:XTAL1(25脚)接晶振或外部时钟 → 内部振荡器缓冲 → CLKSEL选择的÷1或÷4预分频 → 得到参考时钟fref → 16位除数锁存器(DLL/DLM)做整数分频 → 得到波特率时钟 → 内部再÷16 → 得到每位数据的采样时钟。因此波特率公式是:
baud = fref / (divisor × 16)
其中fref = fXTAL / prescaler。给一段C语言风格的初始化计算:
/* 假设 XTAL1 输入 24 MHz,预分频为 1,目标波特率 115200 */ #define XTAL_HZ 24000000UL #define PRESCALER 1 #define TARGET_BAUD 115200UL uint32_t fref = XTAL_HZ / PRESCALER; /* 参考时钟 */ uint32_t divisor = fref / (16UL * TARGET_BAUD); /* 整数除数 */ /* divisor ≈ 13.02,向下取整得到 13 */ write_reg(DLL, divisor & 0xFF); /* 低 8 位 */ write_reg(DLM, (divisor >> 8) & 0xFF); /* 高 8 位 */这段代码里的关键点是除数锁存器是16位的,低8位写DLL、高8位写DLM,二者合起来才是完整的除数。整数除法必然带来速率误差:用13做除数时,实际波特率是24MHz/(16×13)≈115384,相对115200的误差约0.16%,常规串口链路±2%到±3%的容错范围完全接得住。但如果目标速率很高或参考时钟偏低,误差会迅速放大,这时候应该调整参考时钟而不是硬凑除数。
采样侧的细节同样值得注意。数据手册提到内部投票逻辑会对RX线每个位做三次采样,按多数投票判定逻辑电平。16倍过采样提供了「在一个位周期内选三个采样点」的窗口,多数投票机制让短毛刺很难翻转位判决。这个设计和假起始位检测配合:RX线从高到低的跳变如果没有持续够一定的采样窗口,就被当作假起始位丢弃,而不是立刻进入字符接收流程。
3.2 CLKSEL与MCR[7]:复位时的引脚锁存如何被软件覆盖
预分频选择有一个反直觉的机制。CLKSEL(21脚)在复位期间被采样,结果锁存进MCR[7]:CLKSEL为高时锁存0(选÷1),为低时锁存1(选÷4)。复位结束之后,CLKSEL引脚本身不再起作用,真正决定预分频的是MCR[7]这个寄存器位,软件随时可以改写。
这样设计的好处是硬件上用一个引脚就能决定复位后的默认分频,适合「不同板卡配不同晶振」的生产场景;软件启动后再接管,又能在不改硬件的情况下切换时钟方案。代价是极其容易踩坑——如果板卡上CLKSEL硬接地,而初始化代码假设预分频是÷1,那么复位后实际运行在÷4,所有波特率都会变成预期值的四分之一。反过来,改了MCR[7]却忘了重算除数,速率也会立刻翻倍或减半。时钟链上的每一段改动,都必须向后传导到除数。
3.3 工作电压与速率上限:为什么5V才能跑满3Mbps
数据手册按工作电压给出了不同的输入时钟和速率上限:
| 工作电压 | 推荐输入时钟 | 最大数据速率 |
|---|---|---|
| 5V | 48 MHz 振荡器 | 3 Mbps |
| 3.3V | 32 MHz 振荡器 | 2 Mbps |
| 2.5V | 24 MHz 晶体 | 1.5 Mbps |
| 1.8V | 16 MHz 振荡器 | 1 Mbps |
这个表格背后的机制是:低电压下IO驱动能力和片内逻辑翻转速度同时下降,时钟周期缩短后,采样点的建立保持时间不够,位判决可靠性就会恶化。所以「最高速率」不是一个恒定值,而是电压、时钟源类型、温度等级共同约束的结果。设计系统时钟时应该先定电压轨,再选输入时钟,最后反推波特率;反过来先定3Mbps再去找5V供电,很可能在项目后期发现整板电源方案都跟着被动。
4. 流控机制:硬件RTS/CTS与软件Xon/Xoff的背压差异
有了FIFO还不够。FIFO只能缓冲,不能阻止对端继续发数据;真正防止溢出的,是流控。TL16C754C同时支持硬件流控(Auto-RTS/Auto-CTS)和软件流控(Xon/Xoff),两条路径都依赖可编程的阈值寄存器,但两者的背压语义完全不同。
4.1 Auto-RTS/Auto-CTS:以FIFO水位为闸门的收发控制
先看硬件流控。CTS输入(CTSA在2脚、CTSB在16脚、CTSC在33脚、CTSD在47脚)只有在EFR[7]使能auto-CTS时才真正影响发送;RTS输出只有在EFR[6]使能auto-RTS时才由硬件自动控制。在没有使能之前,CTS只是MSR[4]里一个可读的状态位,RTS也只是MCR[1]手动控制的普通输出。
背后的机制是典型的水位闸门。使能auto-RTS之后,接收FIFO的水位由TCR寄存器里的阈值决定:水位一达到阈值,硬件自动拉高RTS,对端看到RTS无效就会停止发送;水位降到阈值以下,硬件自动拉低RTS,对端恢复发送。发送方向对称:auto-CTS使能后,本地发送器在CTS无效时发完当前字节就停,CTS恢复有效再继续。这里的「当前字节」很关键——不是立刻掐断,而是保证已经开始的字节完整发完,避免在字节中间切断线路造成帧错误。
这里有个容易被忽略的点:FCR/TLR的触发阈值是给CPU和DMA看的,TCR的流控阈值是给对端设备看的,两套水位可以独立设置。比如接收触发点设16字节触发DMA搬运,TCR水位设48字节拉高RTS。这样即使DMA还没来得及搬完,硬件也已经把闸门关了,FIFO永远到不了真正满的状态。两级背压的配合,才是「64字节FIFO不丢数据」这句话的完整含义。
4.2 Xon/Xoff与Xon Any:把控制信息编码进数据通道
软件流控解决的是没有额外信号线的问题:整个链路只有TX和RX两根数据线,控制信息必须编码进数据流。Xoff字符表示「请暂停」,Xon字符表示「请恢复」,字符值全部可编程。接收方向收到Xoff后,发送器暂停从TX FIFO取数;收到Xon后恢复。收发双方各自维护一套暂停/恢复状态机,不需要任何硬件引脚参与。
Xon Any是数据手册里一个容易被忽略的增强项:即便发送器是因为硬件流控处于暂停状态,只要收到Xon字符,数据流也能恢复。「Any」的含义是恢复条件不限于软件流控自己造成的暂停,而是对所有制停原因生效。在混合使用软硬流控的系统里,这个特性给了软件一个「强制解冻」的手段。
软件流控的代价也很实际:控制字符占用了数据通道,数据内容里一旦出现与Xon/Xoff相同的字节,就可能误触发暂停或恢复。常见的实现思路是把Xon/Xoff设成协议约定范围内很少出现的值,或者在发送侧做转义处理。这也是硬件流控在可靠性和延迟上优于软件流控的原因——专用信号线即时响应、不存在字符内容冲突;而软件流控在只有两根线的场景(比如部分IrDA红外链路)下几乎是唯一选择。
4.3 FIFORDY与TCR:一次总线访问读取四个端口的FIFO状态
多通道UART的软件开销很大一部分来自轮询。每路TX/RX状态都要单独读寄存器,四路就得读四次;如果其中三路没有数据,这三读就全是浪费。FIFORDY寄存器把四路的TXRDY/RXRDY状态压进一个寄存器:TXRDY为低表示有「触发级别数量」的空位可用,TXRDY为高表示四路发送FIFO都满了;RXRDY为低表示接收触发级别已达到或发生了超时中断。
配合状态字做调度的代码框架大致是这样:
uint8_t frdy = read_reg(FIFORDY); for (int port = 0; port < 4; port++) { if (frdy & (RXRDY_MASK << (port * 2))) { /* 该路接收 FIFO 达到触发级别,搬走一批数据 */ drain_rx_fifo(port); } if (frdy & (TXRDY_MASK << (port * 2))) { /* 该路发送 FIFO 有空位,补充待发数据 */ feed_tx_fifo(port); } }逻辑说明:这个循环把「哪一路需要处理」的判断压缩成一次寄存器读,之后每个活跃端口只做一次数据搬移。RXRDY_MASK和TXRDY_MASK分别对应状态字里接收与发送的位域,每路占两个位,用移位取到对应通道的状态。TCR寄存器在这一章里承担的是流控阈值角色——它不产生中断,但决定硬件何时拉高或拉低RTS,和4.1里auto-RTS的水位逻辑直接挂钩。读完FIFORDY再逐路处理,比「每路都读一次状态寄存器、大部分读了白读」要省得多,这在四路同时跑高波特率时差别很明显。
5. 避坑排查:预分频、触发级别与PM封装踩过的五个坑
下面五条是调板子时容易反复出现的问题,按「现象→原因→解决」的顺序讲清楚,每一条都对应前面章节里讲过的某个机制。
5.1 坑位一:PM封装下RXRDY/TXRDY引脚完全没有信号
现象:硬件按照RXRDY/TXRDY引脚接了DMA请求线,上电后发现请求信号一直不来,DMA通道完全不动,但用示波器量INTA-D中断引脚却很正常。
原因:TL16C754CPM(64引脚TQFP的PM封装)明确不支持RXRDY/TXRDY功能,数据手册在引脚描述中专门做了标注,这是封装级的功能裁剪,不是配置能救回来的。RXRDY和TXRDY是芯片级的「线或」输出,在PM封装里这两条信号通路根本没被引出来。
解决:如果DMA请求必须走硬件引脚,就换支持该功能的型号变体;如果封装已经定死,改用INTA-D中断做搬运触发。FIFORDY寄存器在PM封装里依然可读,把它当状态源,软件轮询一次就能拿到四路状态,基本可以替代引脚中断做DMA搬运调度。
5.2 坑位二:MCR[7]改完波特率立刻翻倍或减半
现象:初始化代码里把MCR[7]从0改成1(或反向),之后串口数据全部乱码;用示波器测TXD波形,发现实际速率和预期差2倍甚至4倍。
原因:MCR[7]控制的是预分频÷1还是÷4,改它等于直接改变参考时钟fref。DLL/DLM里存的是按旧fref算出的除数,fref一变,波特率就成倍偏离。很多人以为CLKSEL引脚的逻辑电平会实时决定分频,实际它在复位结束后就失去作用,后续全看MCR[7]。
解决:把时钟链当整体对待。每次修改MCR[7]都必须重新计算并写入DLL/DLM;调试时先读一次MCR[7]确认当前分频值,再反推预期波特率。计算公式就是第三章的baud = fref / (divisor × 16),先确认fref再谈除数。
5.3 坑位三:Auto-RTS开了仍然溢出,阈值没留制动距离
现象:EFR[6]使能了auto-RTS,TCR阈值也设了,但大流量通信时接收FIFO的溢出错误还是被置位,RS-485链路上偶发丢包。
原因:auto-RTS的响应不是瞬时的。从本地拉高RTS,到对端芯片收到RTS无效、停止发送、线缆残留数据全部进入本地FIFO,中间存在一段延迟。TCR阈值如果设得接近64,这段延迟里涌进来的数据根本没有地方放。
解决:阈值要留「制动距离」。估算方法是用示波器量RTS拉高沿到RX数据完全停止的延迟δ,需要的缓冲空间约等于δ × 波特率 / 8(换算成字节)。比如δ=1ms、115200bps时约需14.4字节的余量,TCR水位至少要比64低这么多。工程上通常把接收触发阈值和TCR阈值都往下调,宁可中断多几次,也不能冒溢出风险。
5.4 坑位四:LSR读出的错误标志和字节对不上
现象:连续接收数据时中间有一帧奇偶错误,但读LSR时错误位出现的位置比预期晚;或者先读了RBR再读LSR,发现错误标志已经变成下一个字节的了。
原因:接收FIFO的每个条目是「8位数据+3位错误状态」绑定存储的,LSR反映的永远只是当前FIFO头部那个字节的错误状态。头部字节弹出后,LSR立即变成下一个字节的状态。读顺序反了,错误信息就和字节错位。
解决:严格遵守「先读LSR、再读RBR」的消费顺序,让错误标志和它对应的数据在同一次访问序列里配对。需要逐字节检查错误时,每个字节都重复这个顺序,不要图省事先批量读数据再回头查状态。
5.5 坑位五:复位后64字节FIFO缩水成1字节
现象:上电复位后往发送FIFO写数据,刚写完第一个字节就触发了发送缓冲空中断,完全不像64字节FIFO该有的表现。
原因:复位后发送和接收FIFO默认是禁用的,触发级别等效1字节。数据手册原话是“After reset, both transmitter and receiver FIFOs are disabled and so, in effect, the trigger level is the default value of one byte”。没有显式使能FIFO,64字节容量只是个摆设。
解决:初始化时明确写FCR选择触发档位并恢复FIFO使能,需要精确阈值时再用TLR编程。初始化完成后读一下FIFO状态寄存器确认模式已生效,不要假设复位后FIFO是默认打开的。这个坑最容易发生在「代码是从单字节UART驱动移植过来」的项目里。
6. 设计取舍:为什么是64字节FIFO与可编程触发,而不是更大缓冲
这颗芯片最有意思的设计取舍,不是「能做四路」,而是「为什么停在64字节」。FIFO不是越大越好。深度加大带来两个隐性成本:一是延迟,数据进了FIFO要等触发点到了才被软件或DMA取走,深度越大、触发点越高,单字节在缓冲里停留的时间就越长,对实时性敏感的数据反而是灾难;二是面积与功耗,64×11位的接收存储阵列已经不小,翻倍到128深度,面积几乎翻倍,收益却只是把中断频率再降一半。64字节恰好是「一次常见DMA burst、一条典型协议帧、半程modem数据流」的公约数。
可编程触发比固定档位更值得琢磨。固定档位解决80%的通用场景,TLR解决剩下的20%:DMA burst长度不对齐时、协议帧边界正好落在非标准深度时、多路公平轮询需要把各通道触发点错开时,软件有权限把水位调到任意想要的位置。设计者把选择权留给软件,而不是替用户把所有场景写死。
RS-485模式体现了同样的取舍逻辑。数据手册让DTR引脚兼任外部RS-485收发器的方向控制,省掉一根专用GPIO,代价是软件必须理解「DTR此刻的语义是方向使能,不是数据终端就绪」。在引脚预算紧张的封装里,这种复用是典型的空间换语义。
我自己的教训是:以前调串口丢数据,第一反应就是加延时、降波特率,后来才意识到问题几乎都出在时钟链、FIFO水位、流控阈值这三者的配合上。从那以后,每次写UART驱动我都强制先画一张「分频器→除数→触发阈值→流控水位」的状态表,确认每一段链路的数字都对,再上示波器验证RTS与RX的时序关系。这套习惯救过我好几次,希望帮到你。
本文还有配套的精品资源,点击获取