1. 为什么每个和计算机打交道的人都绕不开数制这关我第一次被数制转换狠狠教育是在大学《计算机组成原理》的第一次实验课上。老师让我们用Verilog HDL写一个十六进制键盘扫描和编码器我对着键盘矩阵的电路图完全不知道该把扫描码编码成二进制、八进制还是十六进制传给下游模块。当时天真地以为“输出十六进制”就是直接写个16结果综合工具根本不认——寄存器里只能存0和1所谓的十六进制只是给人看的一种表示方式罢了。这件事后来一直跟着我。从C语言里用strstr()试图在二进制内存里找子串答案是不能遇到\0直接断到用HxD十六进制编辑器定位通达信本地数据文件的字段偏移再到CTF里对着rip寄存器偏移量推算返回地址甚至给CentOS部署Nginx二进制包时校验官方发布的SHA256十六进制摘要——所有环节都绕不开一个最底层的问题数据在计算机内部到底长什么样以及我们如何用不同的进制去描述它。所以这篇博文就干一件事把二进制、八进制、十进制、十六进制这四个老伙计的关系彻底讲透。我不会只给你一张“除2取余”的公式表那种东西教科书上到处都是。我要讲的是为什么补码要那样算为什么十进制小数转二进制永远转不干净为什么十六进制在内存调试里几乎成了默认语言以及在实际写代码、调硬件、做逆向的时候什么时候该用哪种进制、会遇到什么坑。不管你是刚学编程被进制转换作业卡住的学生还是工作几年每次遇到位运算都要临时翻文档的开发者或者是玩CTF、搞嵌入式、做安全分析需要经常和内存打交道的从业者这里的内容都能让你少走一些我当年走过的弯路。文章会从最核心的位权概念讲起一步步推到相互转换的手工算法、编程实现、精度陷阱和实战避坑经验。2. 四种数制的本质区别与适用场景拆解2.1 位权理解一切进制转换的钥匙数制这东西说穿了就一句话同一个符号在不同位置上代表不同的值这个“不同”就是位权。我们太习惯十进制了以至于觉得“123”里的2就是二十但在二进制里“10”是二在八进制里“10”是八在十六进制里“10”是十六。区别在哪区别只在底数。用生活类比来说你可以把每种进制想象成一种货币面额体系。十进制是1、10、100、1000的面额二进制只有1、2、4、8、16的面额十六进制则是1、16、256、4096的面额。你在某个位置上放一个数字d它实际贡献的值就是d乘以该位置的位权。用一个通用公式表达一个r进制数d_n d_{n-1} ... d_1 d_0的十进制值等于Σ d_i × r^i。整数部分从右往左数位权是r的0次方、1次方、2次方……小数部分从左往右数位权是r的-1次方、-2次方……就这么简单。为什么计算机选二进制不是因为它“先进”而是因为物理上最容易实现稳定的两种状态。晶体管导通或截止电压高或低磁畴方向朝上或朝下——只有两个稳定状态才能抵抗噪声和制造误差。八进制和十六进制本质上不是给机器用的是给人用的。因为二进制写起来太长太容易数错而8和16都是2的整数次幂可以无损压缩二进制的书写长度。每3位二进制正好对应1位八进制每4位二进制正好对应1位十六进制这种整齐的分组关系是它们存在的唯一理由。2.2 四种进制的性格对比与典型使用场合我在实际工作中有一个很深的体会每种进制都有它的“脾气”用对了场合事半功倍用错了就是给自己找麻烦。下面这张表是我根据自己的使用经验整理的你可以把它当成一个速查参考。进制基数数字符号每位的“信息容量”典型使用场合二进制20, 11 bit底层电路、位运算、掩码、状态标志八进制80-73 bitLinux文件权限chmod 755、某些老式编码十进制100-9约3.32 bit人类日常表述、业务逻辑中的数值十六进制160-9, A-F4 bit内存地址、颜色值、哈希摘要、字节流查看这张表里最容易被低估的是最后一列“典型使用场合”。我面试过一些应届生问他们“什么时候用十六进制”大部分人只能答出“表示颜色”和“表示地址”。其实远不止。比如你在Linux里执行chmod 755 file那个755就是八进制拆开来看是111 101 101分别对应owner、group、others的rwx权限位。用八进制是因为3位刚好覆盖rwx三种权限不多不少写起来比二进制短读起来比十进制直观十进制的493等于八进制的755你试试493能不能一眼看出权限。十六进制的4 bit对齐特性使得它成为查看字节流的默认选择。一个字节正好8位用两位十六进制就能完整表示从00到FF。HxD这类十六进制编辑器把每个字节显示为两个十六进制字符左边是偏移地址右边是ASCII预览这个布局能沿用几十年就是因为十六进制和字节之间的映射太自然了。2.3 为什么八进制没有像十六进制那样流行起来这个问题我琢磨过很久。按理说八进制也是2的幂次3位一组和十六进制的4位一组相比也没差到哪里去。但现实是十六进制几乎统治了底层调试、逆向工程和硬件描述领域八进制除了Linux权限之外很少露脸。我的理解是分组效率的问题。一个字节8位用十六进制刚好2位表示完整整齐齐。用八进制表示一个字节需要3位因为3×39比8多一位而且最高位永远浪费一部分信息。在处理16位字长、32位字长、64位字长的时候十六进制每4位对应的位数是16、32、64全部整除。八进制每3位对应的位数是12、24、48和主流的字长对不齐。这种不对齐在拼接多字节数据的时候会带来额外的移位和掩码操作在底层编程中是实实在在的麻烦。另外从历史沿革上看早期一些计算机确实用过八进制比如PDP-8的12位字长刚好可以用4位八进制表示但随着8位字节成为事实标准十六进制的优势就不可动摇了。Chmod 755这种用法更像是一个历史遗留的“特权保留地”在权限位这个小领域里3位一组确实比4位一组更贴合rwx的语义。3. 进制转换的手工算法与数学原理3.1 任意进制转十进制按权展开求和这是最基础的转换方向也是最符合直觉的。方法很简单把每一位数字乘以它对应的位权然后全部加起来。举个例子把二进制1101.101转成十进制。整数部分从右往左第0位是1位权2^01第1位是0位权2^12第2位是1位权2^24第3位是1位权2^38加起来就是104813。小数部分从左往右第-1位是1位权2^{-1}0.5第-2位是0位权2^{-2}0.25第-3位是1位权2^{-3}0.125加起来是0.500.1250.625。最终结果就是13.625。八进制转十进制同理。617转十进制6×8^2 1×8^1 7×8^0 384 8 7 399。十六进制转十进制也一样0x2F转十进制的计算是2×16^1 15×16^0 32 15 47。注意A-F分别代表10到15这是十六进制比前面几种多出来的一点点规则。提示按权展开法适合手算位数较少的数。如果位数很多逐项计算容易出错建议用下面的“分组转换法”作为中间跳板或者直接用计算器验证。3.2 十进制整数转任意进制除基取余逆序排列这个方法的数学本质是不断做带余除法把余数收集起来。以十进制转二进制为例把十进制数不断除以2记录每次的余数直到商为0然后把余数从下往上读。拿43来演示43÷221余121÷210余110÷25余05÷22余12÷21余01÷20余1。余数序列从下往上是101011所以43的二进制就是101011。验证一下3282143没错。转八进制就除以8转十六进制就除以16。这里有个容易踩的坑除基取余对整数永远成立但对小数不适用。很多人在手算的时候会把整数和小数一起处理结果就乱了。3.3 十进制小数转任意进制乘基取整顺序排列小数部分用的是完全不同的方法不断乘以目标基数记录每次的整数部分然后用剩下的小数部分继续乘。直到小数部分变成0或者达到你需要的精度位数。拿0.625转二进制举例0.625×21.25取整数部分1剩下0.250.25×20.5取整数部分0剩下0.50.5×21.0取整数部分1剩下0。乘完了整数部分按顺序读取就是101所以0.625的二进制是0.101。但这里有个致命问题不是所有十进制小数都能用有限位二进制精确表示。最经典的例子就是0.1。你试试看0.1×20.2取00.2×20.4取00.4×20.8取00.8×21.6取1剩0.60.6×21.2取1剩0.2……然后就开始循环了永远乘不到0。这就是为什么在编程中0.10.2不等于0.3的根本原因。注意十进制小数转二进制时如果存在精度限制比如float只有23位尾数double只有52位尾数就必须考虑舍入。IEEE 754标准定义了四种舍入模式但默认的是“向偶数舍入”round to nearest, ties to even。这不是随意选的而是为了在统计上减少累积误差。3.4 二进制与八进制、十六进制之间的快速分组转换这是实际工作中使用频率最高的转换方式因为绕过了十进制这个中间层直接在2的幂次进制之间做分组映射。二进制转八进制从右往左每3位一组不足3位在左边补0。比如11010110分组为011 010 110对应八进制326。二进制转十六进制从右往左每4位一组不足4位左边补0。同样11010110分组为1101 0110对应十六进制D6。反过来八进制转二进制就是每位拆成3位十六进制转二进制就是每位拆成4位。0x3A拆开就是0011 1010去掉前导0得到111010。这个方法为什么有效因为82³162⁴分组之后每组内部的位权关系不变而组与组之间恰好是基数倍的关系所以可以直接映射。这里有一个我在写Verilog和做位掩码时常用的技巧二进制扩展法。当你需要把一个较短的二进制数扩展到更长的位宽时比如把8位扩展到16位如果是有符号数高位补符号位如果是无符号数高位补0。这个操作在Verilog里用{{8{sign_bit}}, data}或者$signed()来实现在C语言里就是类型转换时编译器自动做的事情。理解了这个扩展规则才能真正理解补码运算中为什么负数的符号位可以一直往左延伸而不改变数值。4. 整数与负数在计算机中的真实面貌4.1 原码、反码、补码的演进逻辑要搞清楚负数在计算机里怎么表示得先明白为什么不能直接用“符号位绝对值”的方式。假设我们用最高位当符号位0表示正1表示负那5就是0101-5就是1101。看起来挺合理但做加法的时候就出问题了5 (-5)应该是0但0101 1101 10010如果只保留4位就是0010也就是2明显不对。为了解决这个问题历史上先出现了反码负数的反码是符号位不变其余位取反。-5的反码是1010。这样5 (-5)0101 1010 1111反码的1111对应负零。虽然结果对了但出现了“正零”和“负零”两个表示给判断和电路设计带来麻烦。最终胜出的是补码负数的补码等于反码加1。-5的补码是1011。5 (-5)0101 1011 10000丢掉最高位的进位剩下0000完美等于0。补码把减法统一成了加法硬件上只需要一套加法器电路就能处理加法和减法这是它成为现代计算机标准表示法的根本原因。4.2 负数二进制的手工计算方法我教别人的时候喜欢用一个更直观的口诀取反加一符号位不动。具体来说要得到-x的n位补码表示先把x写成n位二进制然后所有位包括符号位取反最后加1。拿-43的8位补码来演示。43的8位二进制是00101011。取反得到11010100加1得到11010101。验证一下最高位是1所以是负数取反加一还原绝对值11010101取反是00101010加1是00101011即43。没错。这里有个容易被问到的问题n位补码能表示的范围是什么。正数从0到2^{n-1}-1负数从-2^{n-1}到-1。8位补码范围是-128到127。为什么负的多一个因为10000000这个编码按照取反加一的规则还原不出来对应的正数取反得01111111加1得10000000又回来了它被规定为-128。这个规定不是随便定的而是补码算术的自然结果。4.3 补码运算中的溢出判断与位扩展在做嵌入式开发和逆向分析时溢出判断是个绕不过去的坎。两个8位数相加结果超出-128到127的范围就会溢出。但CPU的加法器只会闷头算不会告诉你溢出了。判断方法有两种。第一种是看符号两个正数相加得到负数或者两个负数相加得到正数就是溢出。第二种是看进位最高位的进位和次高位的进位不同就是溢出。第二种方法在硬件描述里更常用因为它可以直接用异或门实现overflow carry_out XOR carry_into_sign_bit。位扩展是另一个高频操作。把8位有符号数-4311010101扩展成16位符号位是1所以高位全部补1得到11111111 11010101。这个结果仍然是-43。如果你不小心补了0变成00000000 11010101也就是213数值就完全变了。这就是符号扩展的规则有符号数扩展补符号位无符号数扩展补0。实操心得在做协议解析时遇到多字节整数一定要先确认字节序大端还是小端再确认有无符号最后再做位扩展。这三步错任何一步解析出来的数值都是错的。5. 小数转换与精度陷阱的深度剖析5.1 为什么十进制小数转二进制经常“除不尽”前面已经提到了0.1的例子这里再深入说一下原因。十进制小数的分母是10的幂次而102×5。要把一个十进制小数转换成分母为2的幂次的二进制小数本质上就是看这个十进制小数的分数形式能否化简为分母只有2的因子的分数。0.5可以写成1/2分母只有2所以二进制是精确的0.1。0.625可以写成5/8分母是2³所以二进制是0.101。但0.1是1/10分母含有因子5没法化成只有2的幂次所以二进制表示是无限循环的。在计算机中float和double的尾数位数是有限的所以只能存一个近似值。float有23位尾数加上隐含的1位有效精度大约7位十进制数字。double有52位尾数有效精度大约15到16位十进制数字。超过这个精度的部分会被舍入。5.2 舍入模式对计算结果的实际影响IEEE 754定义了四种舍入模式向最近偶数舍入、向正无穷舍入、向负无穷舍入、向零舍入。默认是第一种也就是“四舍六入五成双”的二进制版本。为什么默认不是简单的“四舍五入”因为在大量累加运算中简单的四舍五入会引入统计偏差而“向最近偶数舍入”在统计上偏差最小而且不会产生系统性偏移。举个例子0.5在十进制里正好在两个整数中间向偶数舍入就是舍到最近的偶数。二进制里也一样当尾数正好处于两个可表示值的中间时选择尾数最低位为0的那个。这个规则对日常业务计算的影响是什么如果你在做财务系统用float累加一堆金额最后对账可能会差几分钱。解决办法是用整数表示最小货币单位比如用分而不是元或者用十进制浮点库比如Python的decimal模块。我见过一个真实案例某电商系统用float计算优惠券分摊结果每天有几毛钱的误差查了很久才定位到舍入问题上。5.3 浮点数在内存中的实际布局一个32位float的布局是1位符号位8位阶码偏移量12723位尾数。阶码决定了小数点移动的位数尾数决定了有效数字。把-12.5转成float来演示。先转成二进制12.5是1100.1。规格化后变成1.1001 × 2^3小数点移到第一个1后面指数为3。符号位是1。阶码是3127130二进制10000010。尾数是去掉隐含的前导1之后的1001后面补0到23位10010000000000000000000。拼起来就是1 10000010 10010000000000000000000转成十六进制是0xC1480000。用HxD打开一个存了float的文件你看到的字节流就是00 00 48 C1小端序。如果不知道这个布局你根本看不懂这串十六进制在说什么。提示Python里可以用struct.pack(f, -12.5).hex()得到c1480000用struct.unpack反向解析。这个技巧在分析二进制协议文件时非常有用。6. 编程实战中的进制处理与工具链6.1 C语言中的进制字面量与格式化输出C语言支持四种进制的字面量写法十进制直接写42八进制加前缀0比如052就是十进制的42十六进制加前缀0x比如0x2A就是42二进制在C23标准之前没有标准前缀GCC等编译器扩展支持0b前缀但可移植性差。输出的时候用printf的格式符控制%d十进制%o八进制%x或%X十六进制。注意%o输出的八进制没有前缀0%x输出的十六进制也没有前缀0x需要手动加。这是我在写日志系统时踩过的坑日志里的十六进制值没有前缀后来分析的时候全靠上下文猜是十进制还是十六进制。还有一个隐蔽的坑%d和%u的区别。用%d打印一个最高位为1的值会显示为负数用%u才会显示为无符号数。同样一串字节FF FF FF FF%d打印出-1%u打印出4294967295。在解析网络协议的首部长度字段时如果不注意符号问题可能会得到一个负数的长度值。#include stdio.h int main() { int n 42; printf(dec: %d\n, n); // 42 printf(oct: %o\n, n); // 52 printf(hex: %x\n, n); // 2a printf(HEX: %X\n, n); // 2A printf(with prefix: 0x%04X\n, n); // 0x002A return 0; }6.2 Python中的进制转换函数与字符串处理Python内置了bin()、oct()、hex()三个函数分别返回带前缀0b、0o、0x的字符串。解析的时候用int(s, base)可以处理任意进制int(2A, 16)得到42int(52, 8)也得到42。这里有一个我经常用的技巧格式化字符串里的进制控制。f{255:08b}得到11111111补全8位二进制f{255:04X}得到00FF补全4位十六进制。这个在生成Verilog初始化文件或者测试向量的时候特别方便不用手动补零。另外Python的bytes.fromhex()和bytes.hex()在二进制数据和十六进制字符串之间做批量转换配合struct模块可以解析任意二进制格式。我之前做一个嵌入式项目的日志分析日志文件是二进制格式我用struct.unpack按照格式字符串一次性把每条记录解析成结构化的字段效率比手动移位高得多。6.3 Verilog HDL中的进制表示与键盘扫描编码在Verilog里数字字面量的格式是位宽基数数值。比如8h2A表示8位十六进制2A4b1010表示4位二进制10103o7表示3位八进制7。设计十六进制键盘扫描和编码器的时候扫描部分产生行选信号读回列信号经过消抖后得到一个键位索引。编码器部分把这个索引映射成4位二进制键码。为什么用4位因为十六进制键盘有16个键2⁴刚好够用。一个常见的坑是位宽不匹配。如果键位索引是5位因为用了矩阵扫描可能有32个状态而键码表只有16个条目综合时可能会产生意想不到的截断。稳妥的做法是在映射模块里显式判断索引范围超出有效范围时输出一个默认值并拉高错误标志。// 4x4键盘扫描编码器核心逻辑简化版 module hex_encoder( input [3:0] row, input [3:0] col, output reg [3:0] key_code, output reg valid ); wire [7:0] key_index {row, col}; always (*) begin valid 1b1; case (key_index) 8b1110_1110: key_code 4h0; 8b1110_1101: key_code 4h1; 8b1110_1011: key_code 4h2; // ... 省略中间条目 8b0111_0111: key_code 4hF; default: begin key_code 4h0; valid 1b0; end endcase end endmodule6.4 十六进制编辑器在二进制分析中的实战用法HxD这类十六进制编辑器我用了很多年最开始只会看ASCII预览后来才慢慢摸出一些门道。看一个未知的二进制文件时我会先看头部几个字节是不是常见的文件签名50 4B是ZIP89 50 4E 47是PNG7F 45 4C 46是ELF4D 5A是Windows可执行文件。看内存dump的时候我最关注的是对齐。如果一段数据在偏移0x1000处有规律地出现那说明它是一个结构体或者页对齐的数据块。把数据类型切到float或者double有时候能直接读出有意义的数值。我把这个叫“十六进制考古”——用偏移量和值的变化规律去反推结构定义。通达信本地数据文件的解析就是一个典型例子。它的日线数据是按记录存储的每条记录有固定的字节数日期字段是4字节整数价格字段是4字节float。如果不清楚这个布局从十六进制上看就是一堆乱码。知道了布局之后用Python读出来转换成表格就一目了然了。import struct # 解析一条简化的通达信日线记录示意 def parse_record(data: bytes): if len(data) 16: return None date, open_p, high_p, low_p, close_p struct.unpack(Iffff, data[:20]) return { date: date, open: round(open_p, 2), high: round(high_p, 2), low: round(low_p, 2), close: round(close_p, 2), }7. 常见问题排查与避坑速查7.1 进制转换中的典型错误与排查方法我把这些年遇到的进制相关问题整理成了一张速查表大部分场景下按表排查能很快定位。现象可能原因排查方法二进制数转十进制结果偏小忘记加最高位位权倒着数一遍位权确认最高位指数等于位数-1十六进制转二进制多出位数忘记去掉前导0检查分组后最高组是否有多余前导0负数二进制还原为正数取了反码而不是补码确认操作是“取反加一”还是仅“取反”小数转二进制无限循环分母含因子5确认十进制小数是否为2的幂次分数float累加结果差几分钱精度丢失累积改用整数运算或decimal库%x打印结果和预期不符符号扩展问题检查变量是无符号还是有符号内存中的多字节值读反字节序搞错确认大端还是小端交换字节顺序试试CTF中rip偏移算错没算填充字节用cyclic pattern精确定位偏移量7.2 字节序一个比进制更容易翻车的地方字节序问题和进制问题经常缠在一起很多人分不清。进制是“怎么表示一个数”字节序是“怎么在内存中排列一个多字节数的各个字节”。大端序把最高有效字节放在最低地址。小端序把最低有效字节放在最低地址。x86和ARM默认小端网络协议默认大端一些嵌入式DSP默认大端。一个32位数0x12345678在小端内存里是78 56 34 12在大端内存里是12 34 56 78。排查字节序问题最简单的办法往内存里写一个你知道的、字节不对称的值比如0x0A0B0C0D然后用十六进制编辑器看字节顺序。HxD里显示的是从低地址到高地址的字节序列一眼就能看出来。7.3 浮点数精度问题的实战应对策略浮点精度问题在业务系统里比在底层系统里更常见因为底层系统处理的多是整数和位掩码业务系统才大量涉及小数金额。我的建议是按场景选方案。对金额、数量这类需要精确十进制表示的数据直接用整数存最小单位前端展示时除以10的幂次。对科学计算中可以接受微小误差的场景用float或double并配合容差比较比如abs(a-b) 1e-9。对需要严格控制舍入行为的场景用十进制浮点库或者定点数库。还有一个容易被忽视的点JavaScript的Number类型本质是64位双精度浮点所以0.10.2在浏览器控制台里输出0.30000000000000004。前端做金额计算时要么用整数分要么引入decimal.js之类的库不要直接拿Number做累加。7.4 位运算与掩码操作的高频错误位运算的错误往往很隐蔽因为结果看起来“差不多对”。常见的坑包括用和|时的优先级问题a b c实际是a (b c)用右移有符号负数时的算术移位行为高位补符号位而不是补0用~取反时位宽不明确导致的高位污染。我在调试一个标志位解析逻辑时遇到过这样一个问题从协议里读到一个字节0xF0想检查最高位是否为1。写的是if (byte 0x80 0x80)但实际执行的是byte (0x80 0x80)也就是byte 1结果自然时对时错。正确的写法是if ((byte 0x80) 0x80)。这个细节在C语言教材里可能一笔带过但在实际项目中造成的bug却不小。实操心得所有涉及位运算的表达式建议都加括号。可读性先不说至少能避免优先级踩坑。另外对负数做右移是平台相关行为如果业务需要逻辑右移先转成无符号数再移位。7.5 CTF与逆向场景中的进制速算技巧打CTF的时候速度很关键。我在二进制题目里最常用的几个速算转换十六进制转十进制记住0x1016, 0x2032, 0x4064, 0x80128, 0x100256, 0x10004096这些节点值然后按位组合。比如0x1A0 0x100 0xA0 256 160 416。或者用0x1A0 1×256 10×16 0 416。二进制补码速算看到一个负数补码先取反加一得到绝对值再加上负号。0xFFFFFFF6是32位取反得0x00000009加1得10所以是-10。这个技巧在分析栈帧偏移和系统调用参数时很好用。rip偏移计算在栈溢出题目里用pattern_create生成一个唯一字符串从崩溃时的rip值反查偏移。不要手算用pattern_offset直接查。如果环境里没有pwntools用Python脚本手动实现也行核心就是找子串位置。8. 个人实操体会与工具参考我这些年在这上面踩的坑归根结底是两个原因一是把“显示”和“存储”混为一谈二是忽略了不同进制之间的精度特性差异。一个最让我印象深刻的教训来自早期做网络协议解析的时候。协议文档里说“长度字段为2字节大端序单位是字节”我读出来放在一个uint16_t里然后直接拿去做索引。结果在某一台上位机上一切正常换了一台就偶尔越界。查了半天发现是那台机器的编译器默认把char当作有符号数而我读长度字段时用了char*缓冲区高位被符号扩展污染了。后来统一改成uint8_t*才稳定。这个教训是只要涉及二进制数据的读取所有缓冲区类型都显式用无符号类型不要给编译器留任何解释空间。另一个体会是关于工具的选择。HxD适合临时查看和修改几个字节但批量分析还是得靠Python脚本。我习惯用xxd配合grep做快速定位找到偏移后再用HxD精确定位和修改。Verilog仿真里看波形的时候把总线显示切换成十六进制或者二进制比十进制直观得多能直接看出哪些位在翻转。CentOS上部署Nginx二进制包时校验官方发布的SHA256十六进制摘要是必要步骤用sha256sum -c对比避免下载到损坏或被篡改的包。还有一个我经常推荐给新人的练习方法拿一个你知道格式的旧文件比如BMP图片或者WAV音频用十六进制编辑器打开对着格式规范一个字段一个字段地看。BMP的文件头是14字节包含42 4D签名、4字节文件大小、4字节保留字段、4字节数据偏移。看完文件头再看信息头再看像素数据。这个过程能把进制表示、字节序、字段对齐这几个概念一次串起来比做一百道进制转换题都管用。最后说一个Verilog键盘编码器项目里的小细节。做十六进制键盘的矩阵扫描时按键消抖的计数器位宽要算够。如果时钟是50MHz消抖时间需要10ms那计数器的最大值就是500000需要至少19位二进制2¹⁹524288。如果只给了16位最大65535消抖时间只有1.3ms按键会不稳定。这种参数计算看起来简单但我在实际调试中确实因为少算了一位而导致按下按键后偶尔多出几个字符。位宽计算这件事宁大勿小多出来的寄存器资源远比调试按键抖动要划算。