读取同一个传感器寄存器,用 UART 可能先发一段厂商命令,用 I²C 要先定位寄存器再切换读方向,用 SPI 则可能连续发送命令、地址和空时钟。协议名称只说明一部分传输规则,真正能读到数据还取决于设备命令格式、时序和电气连接。本篇沿一次传输展开三种接口,再计算有效吞吐。
| 接口 | 同步方式与常见信号 | 数据边界由谁定义 | 常被忽略的问题 |
| UART | 异步,TX/RX | 起始、数据、校验、停止位构成字符;消息另定 | 时钟误差、帧边界、接收溢出 |
| I²C | 同步,SCL/SDA | 地址、应答、START/STOP | 地址表示、重复起始、拉伸与上升时间 |
| SPI | 同步,SCK、数据线、CS | 设备规定的片选和时钟序列 | 模式、首位建立、dummy cycles、MISO 释放 |
表中不计电源、参考地和额外中断/流控引脚。串行描述按时间传位,不等于所有接口只有一根导线;SPI 有多根不同方向信号,QSPI 有多条数据线,高速串行也可使用多 lane。并行接口同样仍有用途,不能说现代高速接口全是串行。
UART:双方各自计时,在每个起始位重新对齐
常见非反相 UART 空闲为高,起始位拉低,随后按约定波特率发送数据位。8N1 表示 8 个数据位、无校验、1 个停止位;停止位为高。发送和接收方向可以独立工作,但单线或外接半双工收发器的实现不一定能同时收发。
0xA5 的二进制按高位书写是 10100101,UART 通常先发 D0,所以数据位顺序为 1、0、1、0、0、1、0、1。这里完整字符包括起始和停止,共 10 位。115200 bit/s 连续传输时理论有效数据率为 115200/10=11520 byte/s;100 字节需要约 8.681 ms,还没计消息头、校验、字节间隔和设备等待。
波特率误差是累计的,不能只看设置值相同
接收器识别起始位后,用本地时钟估计每个位中心。若接收与发送时钟相差 2%,到起始后约 9.5 个位时间处,理想化累计偏移约 0.19 位;再叠加起始检测误差、抖动与边沿畸变,可用窗口进一步缩小。两端都配置 115200,并不保证晶振和分频结果完全相同。
允许误差取决于数据/校验长度、过采样方式和具体接收器,不宜写成所有 UART 都适用的固定 ±2%。应把两端时钟精度、温漂与分频误差组合起来,查目标硬件的容差表。Microchip:异步接收工作范围
波形正确也可能丢数据:接收 FIFO 满而软件/DMA 未及时读取会 overrun;停止位位置不是有效高电平可能产生 framing error;奇偶校验只能检测部分错误,不能代替消息 CRC。UART 本身没有统一应用消息格式,可采用明确长度、分隔与转义、校验及超时;不能假设一次 read 就对应一条完整命令。
RTS/CTS 可为设备间提供硬件流控,但要考虑对方停止发送前仍在途的字符和 FIFO 容量。TX FIFO 空也不一定意味着最后停止位已从引脚发完,关闭发送器或撤销 RS-485 DE 应使用真正的发送完成条件。UART 可经 RS-485 等电气层参与多点系统,不能笼统限定为“一对一、低于 1 Mbit/s”。
采样点怎样逐位偏离中心?
正误差表示接收时钟更快,采样点向左漂;负误差向右漂。改变发送字节可看到:采样已偏离理想位中心,也未必马上产生错误,因为相邻位可能相同。因此单个字节成功不能证明整个误差预算合格。
Δtk=(k+1.5)Ttx(1+ε1−1)
正在呈现知识画面
采样点怎样逐位偏离中心?字符内的时钟误差逐位累计;同样的起始对齐不代表后面的采样仍在中心。
{"id":"uart-sampling-drift","title":"采样点怎样逐位偏离中心?","summary":"字符内的时钟误差逐位累计;同样的起始对齐不代表后面的采样仍在中心。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"byte\" data-i18n=\"byte\"></label><output for=\"byte\" id=\"byte-value\"></output><input id=\"byte\" type=\"range\" min=\"0\" max=\"255\" value=\"165\" step=\"1\"></div><div class=\"control\"><label for=\"error\" data-i18n=\"error\"></label><output for=\"error\" id=\"error-value\"></output><input id=\"error\" type=\"range\" min=\"-8\" max=\"8\" value=\"0\" step=\"0.1\"></div><div class=\"chart-wrap\"><div class=\"chart-label\" data-i18n=\"plot\"></div><svg id=\"plot\" class=\"curve\" viewBox=\"0 0 360 210\" role=\"img\" data-i18n-label=\"plot\"></svg></div><div class=\"track-row\"><span data-i18n=\"bits\"></span><div id=\"bits\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"decoded\"></span><strong id=\"decoded\"></strong></div><div><span data-i18n=\"stop\"></span><strong id=\"stop\"></strong></div><div><span data-i18n=\"drift\"></span><strong id=\"drift\"></strong></div></div><p id=\"eq\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:550;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:550 20px ui-monospace,monospace}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px ui-monospace,monospace;background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px ui-monospace,monospace}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 ui-monospace,monospace;white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:ui-monospace,monospace}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px ui-monospace,monospace;padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 ui-monospace,monospace;overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.chart-wrap{margin:18px 0}.chart-label{font-size:12px;color:var(--muted);min-height:3.2em}.curve{display:block;width:100%;height:auto;max-height:320px;overflow:visible}.curve text{font:11px ui-monospace,monospace;fill:var(--muted)}.gridline{stroke:var(--rule);stroke-width:1}.metrics strong{font-size:20px;min-height:3.2em}.formula{font:12px/1.8 ui-monospace,monospace;min-height:9em}.readout{min-height:8em}.presets button{flex-basis:30%}.scope p{overflow-wrap:anywhere} .unit-note{font-size:12px;color:var(--muted);margin:10px 0}\n\n.metrics span{word-break:break-all;line-break:strict}.metrics>div{padding-right:6px}\n\n","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';row.style.gridTemplateColumns='repeat('+r.length+',minmax(0,1fr))';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function plot(id,series,xr,yr,opts={}){const e=$('#'+id),X=x=>42+(x-xr[0])/(xr[1]-xr[0])*298,Y=y=>175-(y-yr[0])/(yr[1]-yr[0])*148;let out='<defs><clipPath id=\"clip-'+id+'\"><rect x=\"42\" y=\"27\" width=\"298\" height=\"148\"/></clipPath></defs>';for(let i=0;i<3;i++){let x=xr[0]+i*(xr[1]-xr[0])/2,y=yr[0]+i*(yr[1]-yr[0])/2;out+='<path class=\"gridline\" d=\"M '+X(x)+' 27V175M42 '+Y(y)+'H340\"/><text x=\"'+X(x)+'\" y=\"194\" text-anchor=\"middle\">'+esc(opts.xfmt?opts.xfmt(x):Number(x.toFixed(2)))+'</text><text x=\"36\" y=\"'+(Y(y)+4)+'\" text-anchor=\"end\">'+esc(opts.yfmt?opts.yfmt(y):Number(y.toFixed(2)))+'</text>';}out+='<g clip-path=\"url(#clip-'+id+')\">';for(const s of series){out+='<path fill=\"none\" stroke=\"'+(s.color||'var(--accent)')+'\" stroke-width=\"2.5\" '+(s.dash?'stroke-dasharray=\"5 4\"':'')+' d=\"'+s.pts.map((p,i)=>(i?'L':'M')+X(p[0]).toFixed(2)+','+Y(p[1]).toFixed(2)).join(' ')+'\"/>';}for(const p of opts.points||[])out+='<circle cx=\"'+X(p[0])+'\" cy=\"'+Y(p[1])+'\" r=\"4\" fill=\"var(--second)\" stroke=\"var(--paper)\" stroke-width=\"1.5\"/>';if(opts.cursor!==undefined)out+='<path d=\"M'+X(opts.cursor)+' 27V175\" stroke=\"var(--muted)\" stroke-dasharray=\"3 3\"/>';e.innerHTML=out+'</g>';e.dataset.curves=JSON.stringify(series.map(s=>s.pts));}const curve=(fn,lo,hi,n=120)=>Array.from({length:n+1},(_,i)=>{const x=lo+(hi-lo)*i/n;return[x,fn(x)]});function inputs(draw){document.querySelectorAll('input,select').forEach(e=>{e.addEventListener('input',draw);e.addEventListener('change',draw)});draw()}const n=id=>+$('#'+id).value;const show=(id,v,d=3)=>set(id,Number(v.toFixed(d)));function draw(){let byte=n('byte'),err=n('error')/100,r=1/(1+err),bits=[0,...Array.from({length:8},(_,i)=>(byte>>i)&1),1],at=t=>bits[Math.floor(t)%10],pts=[[0,0]];for(let k=0;k<12;k++){pts.push([k,at(k)],[k+1,at(k)])}const times=Array.from({length:9},(_,k)=>(k+1.5)*r),samples=times.map(at),value=samples.slice(0,8).reduce((s,x,i)=>s+(x<<i),0);show('byte-value',byte);show('error-value',100*err);plot('plot',[{pts}],[0,12],[-.1,1.1],{points:times.map((t,i)=>[t,samples[i]])});cells('bits',samples.slice(0,8).map((v,i)=>({text:String(v),cls:v===bits[i+1]?'on':'gold'})));set('decoded','0x'+value.toString(16).padStart(2,'0').toUpperCase());set('stop',samples[8]);show('drift',9.5*(r-1),4);set('eq','Trx/Ttx = 1/(1 + ε) = '+r.toFixed(5)+'\\ntsample,k = (k + 1.5)Trx, k=0…7\\nTX = 0x'+byte.toString(16).padStart(2,'0').toUpperCase());$('#decoded').dataset.value=value;}inputs(draw);","audio":false,"strings":{"bits":"D0…D7 的接收结果","byte":"发送字节(十进制)","decoded":"解码字节","drift":"停止位中心偏移(bit)","error":"接收时钟频率误差(%)","heading":"采样点怎样逐位偏离中心?","intro":"改变接收时钟误差,观察实际取到的八位与停止位。","plot":"横轴发送位时间;蓝:线上电平,金点:采样","reset":"重置","scope":"连续重复8N1字符,发送位时间归一化为1;理想起始检测、单点采样,无过采样、抖动与模拟边沿。","scopeLabel":"模型范围与约定","stop":"停止位采样"}}
I²C:总线控制、字节应答与设备寄存器是三层概念
在常见 Standard/Fast/Fast-mode Plus 双向模式中,SDA、SCL 使用开漏/开集形式和上拉。控制器安排时钟,但总线最终电平由所有连接设备决定;目标设备若支持时钟拉伸,可以在控制器释放 SCL 后继续拉低。控制器需观察真实 SCL,且驱动与设备都要支持所用特性。
普通数据位在 SCL 高期间保持稳定,SDA 在 SCL 高时下降定义 START,上升定义 STOP。每个 8 位字节之后有第 9 个应答时钟:发送方释放 SDA,由接收方拉低表示 ACK,保持高表示 NACK。NACK 不必然是故障,控制器读取最后一个字节后通常主动 NACK,表示不再接收后续字节。NXP:I²C 总线规范
连续读一次,先明确谁在驱动 SDA
假设一个教学设备使用 7 位地址 0x50、8 位寄存器地址,寄存器 0x10 当前值为 0x5A,要求用重复起始读出:
控制器先发送地址字节 (0x50<<1)|0=0xA0,目标 ACK;再发送寄存器指针 0x10,目标 ACK。控制器不释放总线,发送 repeated START,然后发读地址字节 0xA1。目标 ACK 后成为数据发送方,控制器读到 0x5A,在第 9 个时钟 NACK,最后 STOP。
这里有四个地址/数据字节,每个占 9 个时钟,共 36 个 SCL 周期。400 kHz 下仅这些时钟需 90 μs,此外还有 START/STOP、总线空闲、可能的拉伸和软件间隙。连续读 N 字节、设备支持地址自动递增时,可用 9(N+3) 个时钟摊薄前导开销;前 N−1 个数据字节由控制器 ACK,最后一个 NACK。
7 位设备地址 0x50 与上线路径中的 0xA0/0xA1 是两种表示。多数 API 接收 7 位地址,但也有库要求左移后的值,应查 API,不要连续左移两次。10 位寻址有专门头部格式,不能把 10 位值直接塞入一个字节。地址通常由器件固定或地址脚配置,并非天然全局唯一;同地址冲突需改地址、分段或使用多路复用器。
ACK 不是业务成功,仲裁不是软件锁
地址 NACK 可能表示无设备、地址错误、掉电或设备暂时不响应;数据 ACK 只表示本次字节被应答,并不一定表示 EEPROM 已完成内部编程。写后等待、忙状态或 ACK polling 必须按器件协议安排,不能凭总线 ACK 就断言非易失数据已落盘。
多控制器场景利用开漏总线逐位仲裁:发送逻辑 1 是释放 SDA,若实际读到 0,说明另一方正在发送优先的低电平,当前控制器应按规范退出。硬件是否完整支持多控制器、拉伸和仲裁,要看具体控制器与驱动;“总线规范允许”不等于每个外设实现都允许。
SDA 卡低时,不应无限阻塞系统。先超时确认故障,再按控制器/设备和总线状态执行恢复;规范提供 SDA 卡低时尝试时钟脉冲的 bus clear 方法,但 SCL 被持续拉低、设备掉电或硬件短路不能靠盲发九个时钟保证恢复。恢复后还应确认设备内部事务状态,避免把中断的写入当成完整成功。
上升时间按模式查,不能把频率变成固定电阻
| 常见模式 | 最高 SCL 频率 | 上升时间最大值,30%~70% |
| Standard-mode | 100 kHz | 1000 ns |
| Fast-mode | 400 kHz | 300 ns |
| Fast-mode Plus | 1 MHz | 120 ns |
高速度模式还有不同的进入流程和电气约束;上表并非 I²C 所有模式的全集。上升时间上限应与工作模式对应,不能只按时钟周期推算。Rp,max≈tr/(0.8473Cb),还要用 Rp,min≈(Vpullup−VOL,max)/IOL 限制灌电流。NXP:UM10204 时序表与上拉设计
例如 200 pF 总线用 4.7 kΩ,上升约 796 ns,可落在 Standard-mode 的上升时间要求内,却不满足 Fast-mode。400 kHz 时,单按上升时间算上限约 1.77 kΩ;还必须与灌电流下限形成可行区间。板上多个模块各自带上拉时,等效值是并联,电容也会增加,不应只看一块板的电阻。
SPI:共享时钟,仍必须给数据留出建立时间
典型四线 SPI 中,控制器输出 SCK、MOSI 和 CS_n,外设输出 MISO。共享总线时通常用独立 CS 选择外设,被取消选择的外设需按手册释放 MISO。也有三线半双工、只写、无独立片选、Dual/Quad 等变体,因此“四根线”只是常见结构。
| Mode | CPOL:空闲电平 | CPHA | 采样边沿 | 通常更新数据的边沿 |
| 0 | 0 | 0 | 上升 | 下降 |
| 1 | 0 | 1 | 下降 | 上升 |
| 2 | 1 | 0 | 下降 | 上升 |
| 3 | 1 | 1 | 上升 | 下降 |
CPHA=0 在第一个活动边沿采样,首位必须提前准备;CPHA=1 在第二个边沿采样,第一个边沿用于推出首位。CPOL 决定第一个活动边沿是上升还是下降。不能只记“上升沿读”而漏掉空闲极性和片选时序。Analog Devices:SPI 接口
假设 Mode 0 的半周期给外设输出提供 20 ns 预算,而外设时钟到 MISO 延迟为 12 ns、线路和板内传播共 4 ns、控制器建立要求 5 ns,总需求 21 ns,仍然不够。总时钟频率、采样延迟选项、负载和布线都要参与预算,不能因为双方标注支持同一 MHz 就忽略具体模式与时序条件。
有时钟就移位,但移进来的不一定是有效数据
假设设备定义一次读操作为 8 位命令、24 位地址、8 个 dummy 时钟,再返回 16 字节,整个事务需要 8+24+8+16×8=168 个时钟。10 MHz 时需 16.8 μs,有效载荷约 7.62 Mbit/s;即使没有 I²C 式地址阶段,SPI 设备自己的命令、地址和等待依然是开销。
读数据时控制器通常仍须发送占位字节以产生时钟;命令阶段同时收到的 MISO 位可能无效。要按设备定义决定何时丢弃、何时存入接收缓冲。CS_n 是否需跨所有字节持续为低、字节间是否允许暂停、末尾保持多久,均属于事务格式,不能简单把每个字节当成独立访问。
菊花链只有设备支持串接移位语义时才成立,不能把任意 SPI 外设的 MISO 接到下一块 MOSI。基础 SPI 没有统一 ACK 或流控,不代表设备不能提供忙位、READY 引脚、CRC 或重试规则。以太网芯片也要区分通过 SPI 访问的控制器与使用 MDIO/MDC 管理的 PHY,不能统称“以太网 PHY 都用 SPI”。
如何读一次异常传输
| 观察 | 优先核对 | 进一步定位 |
| UART 字符稳定但整体乱码 | 波特率、位数、校验、反相 | 原始字节与编码、消息格式 |
| UART 偶发丢段 | overrun、DMA/FIFO、流控 | 阻塞时间与消息校验 |
| I²C 地址后 NACK | 7 位/8 位地址、电源、地址脚 | 设备忙、上升沿、总线段 |
| I²C 永久等待 | SCL 拉伸或线路卡低 | 超时、恢复、器件状态 |
| SPI 数据整体移一位 | CPOL/CPHA、首位建立 | CS 时序、采样沿和传播延迟 |
| SPI 多设备一起接才失败 | MISO 高阻与片选 | 上拉、负载、冲突驱动 |
逻辑分析仪可解释位序和事务,但门限附近的慢沿、过冲、地弹和争用还需要模拟波形。解码器显示 ACK 或一个十六进制值,并不证明电气裕量充足。
接口选择最终取决于设备支持、吞吐、引脚预算、拓扑和功耗状态。I²C 空闲时上拉通常不耗持续直流,但总线低电平、设备静态功耗和通信占空比会改变能耗;不能把 I²C 直接列为所有电池设备的最低功耗方案。长线差分可选择合适物理层,但 RS-485 不限定只能承载 UART 字符格式。
复算帧与吞吐
from math import isclose
value = 0xA5
bits = [(value >> i) & 1 for i in range(8)]
assert bits == [1, 0, 1, 0, 0, 1, 0, 1]
assert sum(bit << i for i, bit in enumerate(bits)) == value
assert isclose(100*10/115200, 0.008680555555555556)
assert (0x50 << 1, (0x50 << 1) | 1) == (0xA0, 0xA1)
assert isclose(36/400000, 90e-6)
assert 0.8473*4700*200e-12 < 1000e-9
assert 0.8473*4700*200e-12 > 300e-9
assert 12+4+5 > 20
cycles = 8+24+8+16*8
assert cycles == 168
assert isclose(cycles/10e6, 16.8e-6)
assert isclose(128/(cycles/10e6), 7.619047619e6, rel_tol=1e-9)
print('UART 位序、I²C 地址/时钟及 SPI 事务吞吐检查通过')
这些检查复算教学事务,不代表已连接真实 UART、传感器或 SPI Flash。
继续查阅