简介:SMPTE 274M-2008 是电影与电视工程师协会发布的高清电视标准,针对 1920×1080 图像规定了样本结构、数字表示方式以及多种帧率下的数字定时参考序列,适用于高清摄像机、监视器、切换台及编码器之间的信号互通,也是视频信号测试与标准符合性验证的重要依据。这份完整英文电子版共 35 页,除 Scope、Normative References 等基础章节外,还涵盖 Image Structure、System Colorimetry、Raster Structure、Digital Picture Representation 等核心定义,并给出 SAV、EAV 定时参考序列的具体格式,以及模拟同步、模拟接口、逐行分段帧接口、辅助数据、比特并行接口等配套内容,从像素排列到接口电气特性形成了完整链条。资源仅含 1 个 PDF 文件,压缩包大小为 509KB,轻量便于按目录快速查询。目前已有 110 人学习/下载,适合广电设备研发、视频编解码调试、系统集成及标准合规测试的相关人员;对于需要核对 1080i/1080p 时序参数、落实 SMPTE 274M 条文要求的工程师,可直接作为原文依据和设计参考。
1. 为什么现在还要读SMPTE 274M-2008:从HD-SDI采样流识别1080p的坐标系
有一次我在调试一块采集卡,HD-SDI 接上一台摄像机,画面总是间歇性抽动。无论从软件里看分辨率还是帧率,都是标准的 1920×1080,但画面就是不对。最后把采样流落到文件里一查,才发现数据流里根本没有按 SMPTE 274M-2008 的规定插入 Digital Timing Reference Sequences,接收端的行场同步完全靠猜。SMPTE 274M-2008 不是一份只有信号发生器才需要读的规范,它定义了 1920×1080 图像的 Image Sample Structure、Digital Representation 以及 EAV/SAV 时序基准。做采集、编码、FPGA 视频通路或者视频质量分析的工程师,迟早要回到这份文档。
2. SMPTE 274M-2008的Image Sample Structure:1920×1080不是一张没有边框的图
2.1 有效样本只有1920×1080,但一行实际传输有2200或2640个样点
很多人第一次看 SMPTE 274M 会误以为一行只有 1920 个样本。其实 1920×1080 是有效图像区域,不是传输区域。标准里给每一行安排了一个水平消隐区,给每一帧安排了垂直消隐区。在 YCbCr 4:2:2 数字分量流中,有效行每行传输 1920 个亮度样本和对应的 960 个 Cb、960 个 Cr 样值,这是显示设备真正用来成像的部分。而数字接口为了给后续处理留出时间,会在每行前后各保留一段不显示的样值。
SMPTE 274M-2008 把这一整套排列统称为 Image Sample Structure。对于逐行格式,每帧总行数为 1125 行,其中有 1080 行是有效图像;每一行有效区为 1920 个亮度样点,一行总长度可能是 2200 或 2640 个亮度样点周期。最常被拿来做系统设计的几个 Picture Rates 参数如下表所示。
| Picture Rate | Total Samples per Line | Total Lines per Frame | 参考时钟(近似值) |
|---|---|---|---|
| 1080p60 | 2200 | 1125 | 148.5 MHz |
| 1080p59.94 | 2200 | 1125 | 148.5 / 1.001 MHz |
| 1080p50 | 2640 | 1125 | 148.5 MHz |
| 1080p30 | 2200 | 1125 | 74.25 MHz |
| 1080p29.97 | 2200 | 1125 | 74.25 / 1.001 MHz |
| 1080p25 | 2640 | 1125 | 74.25 MHz |
| 1080p24 | 2200 | 1125 | 74.25 MHz |
| 1080p23.98 | 2200 | 1125 | 74.25 / 1.001 MHz |
这张表把 Multiple Picture Rates 的核心差异压缩成了三个数。注意 1080p50 和 1080p60 一样使用 148.5 MHz 参考时钟,但一行总样本不同:60Hz 用 2200 个亮度样点,50Hz 则用 2640 个。这样行频上 60 系大约是 67.5 kHz,50 系大约是 56.25 kHz,都能落在同一套时钟体系的整数分频关系附近。如果你在 FPGA 里只实现了固定 2200 总样本,却要支持 50Hz 输入,就会直接错位。
在并行接口实现时还要注意计数口径。YCbCr 4:2:2 的数据线上每个亮度样本周期会出现两个复用字,比如 Cb/ Y / Cr / Y 交替;标准里说的 2200 是亮度样本周期数,不是复用字通道的字数。如果你像数普通总线数据那样按每个复用的 10bit 字去数长 FIFO,一行会长达 4400 个字,拿这个数去和参数表比对会直接翻倍。
2.2 Digital Representation里的量化范围:黑电平不是0,白电平不是1023
SMPTE 274M-2008 中的 Digital Representation 规定了 8bit 和 10bit 两套代码映射。以工程中最常见的 10bit 为例,亮度信号 E'Y 并不把代码空间 0 到 1023 全部用于图像。参考黑电平映射到代码 64,参考白电平映射到代码 940,0 到 64 留给消隐和同步信号,940 到 1023 作为过冲保护。色差信号 E'CB 和 E'CR 以 512 为零中心,有效范围大约从 64 到 960。
| 视频信号 | 10bit 代码值 | 含义 |
|---|---|---|
| E'Y 黑电平 | 64 | 视频参考黑 |
| E'Y 白电平 | 940 | 视频参考白 |
| E'CB / E'CR 无彩色 | 512 | 色差零中心 |
| E'CB / E'CR 范围 | 64 ~ 960 | 最大色差范围 |
这段范围设计不是随意定的。模拟时代遗留的下冲、上冲要求数字编码留出安全距离;数字链路做色彩空间转换时,如果直接把 0x000 当作黑电平,暗部会被错误压缩。常见的做法是先减去 64,再除以 940 - 64 = 876,把亮度归一化到 0~1,而不是除以 1023。8bit 系统对应的是 16 到 235,缩放关系一致。很多软件处理模块没有保留消隐区,因此会先把代码平移到 64 ~ 940,再进入变增益模块,防止在统一矩阵运算时把暗部削掉。
2.3 用Python算一遍1920×1080各种Picture Rates的参考时钟
在实际做设备选型时,我经常需要确认某个帧率的标准时钟。前面的表格可以直接查,但用一段简单的 Python 代码可以避免手算,也能在代码注释里保留计算依据。
formats = [ ("1080p60", 2200, 1125, 60), ("1080p59.94", 2200, 1125, 60 / 1.001), ("1080p50", 2640, 1125, 50), ("1080p30", 2200, 1125, 30), ("1080p29.97", 2200, 1125, 30 / 1.001), ("1080p25", 2640, 1125, 25), ("1080p24", 2200, 1125, 24), ("1080p23.98", 2200, 1125, 24 / 1.001), ] for name, h_total, v_total, frame_rate in formats: # 参考时钟 = 每行亮度样点总数 * 每帧总行数 * 每秒帧数 clock = h_total * v_total * frame_rate print(f"{name}: {clock / 1e6:.6f} MHz")代码里 h_total 是每行亮度样点周期数,v_total 是每帧总行数,frame_rate 是每秒完整帧数。相乘后得到的就是 SMPTE 274M-2008 对应的参考时钟频率。输出结果里 1080p60 会显示为 148.500000 MHz,1080p59.94 会显示为 148.351648 MHz,1080p50 同样是 148.500000 MHz。看到两个不同帧率共用同一个参考时钟,就自然理解了前文说的“50Hz 拉长一行总样本”的原因。如果你的系统里参考时钟精度不够,比如用 148.35MHz 去锁 59.94Hz,长期运行会积累行偏移,最后表现为周期性撕裂。
3. Digital Timing Reference Sequences:在数据流里找回行和场的边界
3.1 EAV/SAV前导结构:三项固定词加一个XYZ
即使把 2200、1125 这些参数背下来,也仍然无法直接判断一段裸数据流从哪里切开是一行。SMPTE 274M-2008 在每一行有效视频开头和结尾各放了一个 Digital Timing Reference Sequence,简称 TRS。行开头的叫 SAV(Start of Active Video),行结尾的叫 EAV(End of Active Video)。接收端只要找到这两个序列,就能确定有效样本的起止位置,再结合垂直方向标志,就能知道当前在第几行、属于哪一帧。
每个 TRS 在 10bit 数据流里由四个字组成:前导是 0x3FF、0x000、0x000,随后跟一个 XYZ 字。在 8bit 系统里前导是 0xFF、0x00、0x00。不能把前导记成“三个 0xFF”,正确理解是“一个最大值字加两个零字”。EAV 和 SAV 的差别不在前导,而在 XYZ 字里的 H 位。
| TRS部分 | 8bit示例 | 10bit示例 | 作用 |
|---|---|---|---|
| 前导字1 | 0xFF | 0x3FF | 标识TRS开始 |
| 前导字2 | 0x00 | 0x000 | 固定零 |
| 前导字3 | 0x00 | 0x000 | 固定零 |
| XYZ字 | 0xXY | 0xXYZ | 携带F/V/H标志与保护位 |
XYZ 字里面包含三个关键标志位:F 是隔行/逐行场标志,V 是垂直消隐标志,H 表示当前是 EAV 还是 SAV。H=1 时该 TRS 是 EAV,H=0 时是 SAV。在整帧结构里,垂直消隐期间的每一行同样有 EAV 和 SAV,但有效区内容不再是图像数据,而是消隐数据。所以只看 V 标志可以判断当前行是否落在有效图像区。
为了在物理链路上防止数据错误,SMPTE 274M-2008 还给 XYZ 增加了四位保护位 P0 到 P3,由 F/V/H 按异或关系生成。下表是通用位定义。
| 位 | 名称 | 含义或生成式 |
|---|---|---|
| bit0 | F | 场/帧标志 |
| bit1 | V | 垂直消隐标志 |
| bit2 | H | 0=SAV,1=EAV |
| bit3 | P0 | V XOR H |
| bit4 | P1 | F XOR H |
| bit5 | P2 | F XOR V |
| bit6 | P3 | F XOR V XOR H |
这里以通用的 8bit/10bit 定义为例,实际标准中高位还有保护/保留位。接收机在校验时先取出 F/V/H,再按同样方式算出 P0 到 P3,和收到的 XYZ 比对,一致才认为这个 TRS 有效。这个步骤能大幅减少因为视频内容里恰好出现 0xFF 0x00 0x00 而产生的误同步。
3.2 用Python从原始采样流中定位SAV/EAV
拿到一串未拆分的 10bit 采样数据后,定位 TRS 的最小实现并不复杂。下面这段代码会扫描一个以整数表示的采样数组,返回所有候选 TRS 的位置、F/V/H 标志,并用保护位过滤明显错误的位置。
def scan_trs(samples, bit_depth=10): if bit_depth == 10: preamble = (0x3FF, 0x000, 0x000) else: preamble = (0xFF, 0x00, 0x00) def check_xyz(xyz): f = (xyz >> 0) & 1 v = (xyz >> 1) & 1 h = (xyz >> 2) & 1 p0 = (xyz >> 3) & 1 p1 = (xyz >> 4) & 1 p2 = (xyz >> 5) & 1 p3 = (xyz >> 6) & 1 # P0~P3 由 F/V/H 异或生成,用于校验 TRS 是否完整 if p0 != (v ^ h): return None if p1 != (f ^ h): return None if p2 != (f ^ v): return None if p3 != (f ^ v ^ h): return None return f, v, h results = [] i = 0 while i < len(samples) - 4: if tuple(samples[i:i + 3]) == preamble: xyz = samples[i + 3] flags = check_xyz(xyz) if flags is not None: f, v, h = flags results.append((i, f, v, h)) # i是TRS前导起始索引 i += 4 continue i += 1 return results函数参数 samples 是连续采样数组,元素为 0~1023 的整数,bit_depth 控制前导字宽度。check_xyz 里的移位位置与上一小节的表格对应,p0 到 p3 必须满足异或关系,否则认为是数据内容偶然出现的前导。找到有效 TRS 后,直接跳过四个字,从下一个可能位置继续扫描,避免把 XYZ 字又当作前导的一部分。
这段代码只做“找位置”,不做行缓存。实际工程里,接收机应该一路跟踪 H 标志的状态机:期待下一个 TRS 是 SAV,找到 SAV 后开始计数有效样本,数到 1920 再期待 EAV,EAV 后进入消隐区。如果连续多个 TRS 都满足保护位校验,才认为进入锁定状态。纯 Python 扫描在大规模验证里会有些慢,但拿来解析一帧采集数据足够快。
3.3 10bit/8bit通用位定义、隔行F位和伪TRS的三个边界
使用上面的扫描器时,有几个边界容易踩。第一个是位宽。如果数据其实是 10bit 但前导写成了 0xFF 0x00 0x00,永远找不到;如果数据是 8bit 但按 10bit 位宽对齐,也会在相邻两个字之间拼出伪前导。所以在调用扫描器前,先确认采样数组的 bit_depth 是真实位宽,而不是显示位宽。第二个是 F 位。在隔行格式里,F 会在每场之间翻转,V 标志也会在垂直消隐期间置 1;逐行格式虽然不是隔行,但 F 位也参与保护位计算,不能简单置 0,否则 P1、P2、P3 会全部误判。第三个是伪 TRS。有效图像内可能出现和 TRS 一样的前导,但概率较低;真正让人头疼的是消隐区里的辅助数据,某些辅助数据帧头看起来像 0xFF 0x00 0x00,所以一定要把 XYZ 保护位校验放进同步逻辑,而不能只看前导三个字。
4. 用TRS测量结果反推1920×1080的Multiple Picture Rates
4.1 通过相邻EAV的样本距离得到每行总样本数
一旦 TRS 被正确定位,就能开始测量。最常见的测量目标是确认“我现在收到的是 60Hz 系还是 50Hz 系”。做法是先找到同一个信号源中两个连续 EAV 的起始索引,计算差值,这个差值就是每行总样本数。如果平均在 2200 附近,属于 60/30/24 系;如果在 2640 附近,属于 50/25 系。
eav_positions = [p for p, f, v, h in events if h == 1] if len(eav_positions) >= 2: line_len = eav_positions[1] - eav_positions[0] print(f"line total samples: {line_len}")代码选自前一节的 events 数据结构。由于一帧里行与行之间的消隐区可能有辅助数据写入,单个相邻差会偶尔偏大或偏小,所以更稳的做法是统计几十个差值的众数或中位数,而不是直接用第一对。得到 line_len 后,再统计垂直消隐区间内 EAV 的个数,可以得到每帧总行数。一个完整 SMPTE 274M-2008 的 1080p 帧应该数出 1125 行。行数和总样本数组合起来,就能确认信号没有丢失消隐区。
4.2 一个简单的格式判定函数:line_len、total_lines和参考时钟
只有 line_len 和 total_lines 还不能区分 1080p60 与 1080p59.94,因为两者的总样本数和总行数完全一样,差异只体现在参考时钟的微小偏差上。想要区分,需要在接收端测量采样时钟频率,或者用锁相后的时钟计数。下面这个函数把三种信息组合起来,返回完整的格式描述。
def identify_1080_format(line_len, total_lines, clock_hz=None): if total_lines != 1125: return "not SMPTE 274M 1080-line format" family = {2200: "60/30/24", 2640: "50/25"}.get(line_len, "unknown") if clock_hz is not None: # frame_rate = 参考时钟 / (每行总样本 * 每帧总行数) frame_rate = clock_hz / (line_len * total_lines) return (f"1920x1080 line_len={line_len}, family={family}, " f"measured frame_rate={frame_rate:.4f}") return f"1920x1080 line_len={line_len}, family={family}, frame_rate needs clock"clock_hz 是采样位同步恢复出的像素时钟频率,可以从硬件 PLL 的回读寄存器拿。函数先把时钟除以水平总样本数和垂直总行数,得到的就是每秒帧数。对 60Hz 系,通常得到 60.000 或 59.940;对 50Hz 系,得到 50.000。这个函数用在实际调试中可以快速回答“数据是不是 1080p60”的问题。如果 clock_hz 给的是一个源端标称频率,而 line_len 是 2640,即使时钟显示 148.5MHz,也要按 50Hz 系来理解,因为 148.5MHz 出现在两个不同帧率族里。
4.3 三个真实调试里常见的坑
第一个坑是拿 1920 当一行总长度。很多送给 FPGA 的有效流已经裁剪掉了 EAV/SAV 和消隐区,只剩 1920 个亮度样本;这种流里没有 Digital Timing Reference Sequences,不能再靠 TRS 判断帧率,只有加上“已经裁剪”这个前提才能用。第二个坑是把有效视频结束后的 EAV 当成下一个行的 SAV。EAV 和 SAV 在 XYZ 字的 H 位上正好相反,如果只看前导不看 H 标志,会将消隐长度计算错误。第三个坑是辅助数据对测量的影响。水平消隐区不是空白,可能写入音频、时间码等辅助数据,它们会占用样本位置,但因为不影响每行总样本数,只要计数区间是从 EAV 到 EAV,一般不会影响结果;真正影响的是在事件列表里混入伪 TRS,所以 4.1 的统计建议用众数,而不是均值。
5. 在FPGA里用计数器生成SMPTE 274M-2008的TRS,而不是逐字写入
5.1 按计数器插入EAV/SAV,最稳
标准文档读到最后,最实用的技巧是在产生信号时不把 TRS 当作普通数据写入,而是用行、场计数器在特定位置输出固定字。假设你在 FPGA 里生成 1080p60 的 YCbCr 4:2:2 输出流,一个亮度样点周期对应两个复用字通道,但我们在逻辑里仍然用一个行计数器 hcnt 表示亮度样点序号。每行从 SAV 开始,有效视频持续 1920 个亮度样点,之后输出 EAV,剩下的样点就是水平消隐区。这样不管消隐区内容怎么变化,EAV/SAV 的位置都不会漂移。
// 以亮度样点周期计数,一行共 H_TOTAL 个周期 localparam H_TOTAL = 2200; localparam H_ACTIVE = 1920; reg [11:0] hcnt; reg [10:0] vcnt; wire sav = (hcnt < 4); // 行起始的4个样点 wire active = (hcnt >= 4 && hcnt < H_ACTIVE + 4); wire eav = (hcnt >= H_ACTIVE + 4 && hcnt < H_ACTIVE + 8);上面这段不是完整输出状态机,但给出了核心位置关系:SAV 占 4 个样点,有效区占 1920 个样点,EAV 占 4 个样点,余下 H_TOTAL - 1928 个样点是水平消隐。实际设计中,hcnt 在每行末尾清零,vcnt 在每帧末尾清零。V 标志则根据 vcnt 落在有效行区间还是消隐行区间来决定。使用计数器而不是逐字去写,好处是修改 Picture Rate 时只需要改 H_TOTAL,不需要改输出端口逻辑。
5.2 把Picture Rates参数表直接写成localparam列表
第 2.1 节的参数表可以落到 RTL 参数里。常见做法是准备一组 localparam,通过顶层寄存器选择当前要输出的 Picture Rate,再让 hcnt 的计数值和 vcnt 的边界跟随选择。这种结构在显示控制器、信号发生器中非常通用,也便于在验证环境里对照 SMPTE 274M-2008 的时序基准序列做断言检查。验证时,可以运行一段前仿真的采样流,再调用之前的 scan_trs 函数,检查 EAV/SAV 的位置是否严格按照 H_TOTAL 分布。
提示:真正在硬件里做 TRS 生成时,还要处理 8bit 与 10bit 两种输出位宽、YCbCr 与 RGB 双通道布局、以及 SAV 前是否需要插入额外对齐字。先按计数器确定位置,再让数据选择器输出固定常数,这一层加上去,工程上反而比“改常数块”更容易排查。
把标准里的 2200、1125、1920 这些数字做成参数之后,换一个 Picture Rate 就只是换一张配置表,而不是改一版逻辑。这个习惯,比背住标准里的每个数值更能让你用好 SMPTE 274M-2008。
本文还有配套的精品资源,点击获取