BIOS实战:Super I/O逻辑设备读写与配置详解 BIOS 这层东西很多人一听到就头大觉得是汇编、寄存器、中断向量表堆起来的黑箱。但真要在底层做点事情比如在没有操作系统帮忙的情况下直接跟硬件打交道绕不开的一个环节就是逻辑设备SIOSuper I/O的读写。这篇东西就是我在实际调试一块老平台时把 SIO 读写这套逻辑从头到尾摸了一遍之后的记录。它讲的是BIOS 环境下怎么定位并操作 Super I/O 芯片、读写它的配置空间、把串口并口这些逻辑设备真正用起来。适合谁看适合已经能看懂基本 x86 汇编、知道端口 I/O 是什么、但一碰到 SIO 配置就发怵的底层开发者和固件爱好者。看完你至少能明白 SIO 的索引/数据双端口机制是怎么回事为什么进配置空间要敲那个 magic 序列以及怎么在不把机器搞挂的前提下安全地改一个逻辑设备的基址。先把话说在前头SIO 读写这件事坑不在“会不会写端口”而在“什么时候能写、写完什么时候生效、写错了怎么退回来”。我见过太多人一上来就out一个配置值结果机器直接黑屏连 BIOS 都进不去只能抠电池。所以这篇不会只给你几行代码而是把每一步背后的道理和退路都讲清楚。1. SIO 到底是个什么东西为什么 BIOS 阶段要单独伺候它1.1 从主板上一颗不起眼的小芯片说起Super I/O直译“超级输入输出”是主板上那颗常年被散热片和插槽挡住的方形小芯片。它干的活很杂串口、并口、软驱控制器、键盘鼠标控制器、硬件监控温度电压风扇转速、还有一些 GPIO 和看门狗。你可以把它理解成南桥的“外设管家”南桥负责高速总线SIO 负责那些慢速但必不可少的老接口。为什么叫“逻辑设备”因为这一颗物理芯片内部被划分成了好几个功能块每个功能块在配置空间里都有自己独立的寄存器组和使能位比如逻辑设备 0 是软驱、逻辑设备 1 是并口、逻辑设备 2 是串口 1、逻辑设备 3 是串口 2……每个逻辑设备可以单独使能、单独分配基址、单独配置中断。这种“一颗芯片、多个逻辑设备”的设计是 PC 体系里非常典型的资源复用思路。在操作系统起来之后这些配置通常由 ACPI 或者驱动去读但在 BIOS 阶段——尤其是早期初始化、POST 自检、或者你要在 bootloader 里临时用一下串口输出调试信息——操作系统还没影没人帮你配你就得自己动手。这就是“BIOS 实战之读写逻辑设备”这个标题的真实场景不是学术研究是没它不行。1.2 为什么不能像访问内存一样直接怼很多人第一反应是既然 SIO 有寄存器那我直接给它地址读写不就行了不行。SIO 的配置寄存器不在内存空间里也不在普通的 I/O 空间里直接暴露而是藏在一个间接访问机制后面。芯片只对外暴露两个端口一个叫索引端口Index Port一个叫数据端口Data Port。你要读某个配置寄存器得先把寄存器编号写到索引端口再从数据端口把值读出来写则是先把编号写索引端口再把值写数据端口。这个设计的好处是省端口资源——一颗芯片几十上百个配置寄存器如果每个都占一个 I/O 端口端口空间根本不够分。坏处就是访问变麻烦了而且这两个端口的地址本身也不是固定的不同厂商、不同芯片型号默认值不一样常见的有0x2E/0x2F、0x4E/0x4F、0x3F0/0x3F1这几组。你得先探测出这对端口在哪才能开始干活。提示索引/数据端口是成对出现的索引端口是偶数地址数据端口是它加一。探测的时候按对来试别单独试一个。1.3 逻辑设备读写的本质先选设备再选寄存器理解了双端口机制还要理解第二层间接逻辑设备选择。SIO 配置空间里有一个特殊的寄存器叫“逻辑设备选择寄存器”通常编号是0x07你往它里面写一个逻辑设备号后续对配置寄存器的读写就都作用在那个逻辑设备上。这就像你先按楼层按钮选逻辑设备再按房间号选寄存器最后才进房间拿东西。所以一次完整的逻辑设备寄存器读写其实是三步写0x07选设备 → 写目标寄存器编号到索引端口 → 读写数据端口。少一步都不行顺序错了读出来的就是别的设备的寄存器值这种 bug 特别隐蔽因为读出来的值往往“看起来挺正常”只是不对。2. 进配置空间的那道门magic 序列与解锁逻辑2.1 为什么要有“解锁”这一步SIO 的配置寄存器如果随便就能改那任何一个跑飞的程序都可能把串口基址改掉、把看门狗打开机器就废了。所以厂商设计了一个保护机制芯片上电后处于“锁定”状态配置寄存器不可写必须先发送一个特定的解锁序列unlock sequence芯片才会进入配置模式Config Mode这时候配置寄存器才可读写。退出时再发一个锁定序列回到正常模式。这个解锁序列就是所谓的 magic 序列通常是一串特定字节按顺序写到索引端口。不同厂商的序列不一样常见的是往索引端口连写两个特定值比如先写0x87再写0x87有些是0x55或者0xA5之类。这一步是硬编码在芯片手册里的没有商量余地写错一个字节就进不去。2.2 探测端口地址的稳妥做法在不知道端口地址的情况下怎么找我的做法是遍历候选端口对对每一对尝试发解锁序列然后读一个“身份寄存器”通常是0x20设备 ID 高字节、0x21版本号之类看读出来的值是否合理。合理的判断标准是不是0xFF说明端口没设备读回全 1也不是0x00说明短接或者没响应而且多次读结果稳定。这里有个细节很多人忽略发完解锁序列后要立刻读中间不要插入其他端口操作否则可能被别的设备干扰。另外探测完一对端口如果没命中记得发锁定序列再换下一对别让芯片停在配置模式里。; 伪代码示意探测 0x2E/0x2F 这对端口 mov dx, 0x2E ; 索引端口 mov al, 0x87 ; 解锁序列第一字节 out dx, al mov al, 0x87 ; 解锁序列第二字节 out dx, al mov dx, 0x2E mov al, 0x20 ; 设备 ID 寄存器 out dx, al mov dx, 0x2F ; 数据端口 in al, dx ; 读设备 ID cmp al, 0xFF je not_here ; 全 1没设备 cmp al, 0x00 je not_here ; 全 0异常 ; 命中继续读版本号确认上面这段是示意实际写的时候要注意in/out的端口宽度SIO 配置访问基本都是字节操作8 位别用 16 位或 32 位去怼会读到相邻端口的值。2.3 解锁失败的几种典型表现与排查解锁失败不是每次都表现为“读不到”有时候是“读得到但写不进”。我遇到过两种情况一种是设备 ID 读出来是0xFF说明端口根本没对上换端口另一种是设备 ID 正常但写配置寄存器后回读发现值没变说明解锁序列不对或者芯片处于某种保护状态比如被上一级锁了。排查顺序我一般这样走先确认端口对再确认解锁序列字节和顺序最后确认是不是有别的逻辑设备占用了同一个基址导致冲突。还有一种冷门情况某些芯片在低功耗状态下配置接口不响应需要先唤醒这个就得翻具体型号的手册了。注意解锁序列因厂商而异千万不要拿 A 厂商的序列去套 B 厂商的芯片轻则无效重则把芯片带进未定义状态。3. 逻辑设备寄存器的读写实操以串口为例3.1 选设备、读基址的完整链路假设我们已经进了配置模式现在要读串口 1逻辑设备号通常是0x02的基址。基址寄存器在 SIO 配置空间里通常是0x60高字节和0x61低字节这一对组合起来是一个 16 位 I/O 地址。完整链路是往索引端口写0x07往数据端口写0x02选中串口 1往索引端口写0x60从数据端口读高字节往索引端口写0x61从数据端口读低字节拼成(高 8) | 低得到基址。这里有个容易翻车的点读高字节和读低字节之间不能重新选设备因为选设备操作会改变当前逻辑设备上下文。如果你中间插了别的操作读出来的低字节可能属于别的设备。我一般把选设备和读两个字节写成连续的一段中间不夹任何其他端口访问。; 读串口 1 基址逻辑设备 0x02 mov dx, INDEX_PORT mov al, 0x07 out dx, al mov dx, DATA_PORT mov al, 0x02 ; 选中逻辑设备 2 out dx, al mov dx, INDEX_PORT mov al, 0x60 out dx, al mov dx, DATA_PORT in al, dx mov bh, al ; 高字节存 bh mov dx, INDEX_PORT mov al, 0x61 out dx, al mov dx, DATA_PORT in al, dx mov bl, al ; 低字节存 bl ; 此时 bx 就是基址3.2 使能位与基址的先后顺序读是一回事写是另一回事。要让串口真正工作光设基址不够还得打开逻辑设备的使能位。使能位通常在逻辑设备的“激活寄存器”里编号常见0x30写0x01表示激活写0x00表示禁用。顺序上我建议先设基址、再使能。原因很实在如果先使能再改基址中间会有一个短暂窗口设备已经激活但基址还是旧的某些芯片在这个窗口里可能产生异常中断或者总线冲突。先设好基址再一把使能干净利落。改基址之前如果设备已经使能稳妥做法是先禁用、改基址、再重新使能。3.3 中断与 DMA 通道的配置要点串口除了基址还涉及中断号IRQ配置寄存器通常在0x70附近低四位是 IRQ 号。并口还涉及 DMA 通道。这些配置的坑在于IRQ 号不是随便填的得跟系统里其他设备错开而且某些 IRQ 被固定分配给特定用途比如 IRQ 0 是定时器、IRQ 1 是键盘你填进去芯片可能接受但系统起来后会冲突。我的经验是在 BIOS 阶段临时用串口调试IRQ 可以先不配或者配一个明显空闲的号因为轮询方式发数据根本不需要中断。等真要上中断了再回头仔细规划。这个取舍很实际调试阶段求稳别为了“配全”引入不必要的风险。配置项常见寄存器说明踩坑提示逻辑设备选择0x07写入设备号切换上下文每次读写前都要确认当前设备设备 ID0x20读芯片身份用于探测端口是否命中基址高/低0x60/0x6116 位 I/O 基址两字节连续读中间别插操作激活0x300x01 使能0x00 禁用先设基址再使能中断号0x70低四位为 IRQ避开固定分配的中断4. 那些手册上不会写的坑时序、回读与恢复4.1 写完必须回读验证这是我最想强调的一条。SIO 配置寄存器写完不是立刻生效的有些芯片需要几个总线周期才稳定有些则要求写完后再读一次确认。我养成的习惯是每写一个关键寄存器立刻回读比对。如果回读值和写入值不一致说明要么没解锁、要么写保护、要么时序不对这时候停下来排查别继续往下写。回读还有一个好处是能发现“写进去被硬件改写”的情况。比如某些保留位你写 1 它可能强制回 0回读就能看出来避免你对寄存器布局产生错误认知。4.2 配置模式下的“禁区”进了配置模式芯片的某些正常功能是暂停的。这时候如果你去访问被 SIO 管理的设备比如往串口数据寄存器写数据行为是未定义的。所以配置操作要“快进快出”解锁、改配置、锁定中间不要夹带对逻辑设备的实际数据访问。另外配置模式下不要长时间停留。我见过有代码在配置模式里做延时等待结果看门狗被触发或者系统监控误判。改完立刻发锁定序列退出是基本纪律。4.3 改错了怎么退回来万一改错了导致设备不工作退路有两条。第一条是改回原值所以在改任何寄存器之前先把原值读出来存好这是铁律。第二条是整芯片复位某些平台可以通过特定的 GPIO 或者复位寄存器让 SIO 回到上电默认状态但这个操作风险高不到万不得已不用。我个人的做法是在动配置之前先把整个逻辑设备相关寄存器组 dump 一份到内存里改完出问题就整组写回。这比逐个回退可靠得多因为有些寄存器之间存在联动单独回退一个可能引入新的不一致。// 示意先备份再修改的思路 uint8_t backup[REG_COUNT]; for (int i 0; i REG_COUNT; i) { backup[i] sio_read(LDN_SERIAL1, reg_list[i]); } // ... 执行修改 ... // 出问题时 for (int i 0; i REG_COUNT; i) { sio_write(LDN_SERIAL1, reg_list[i], backup[i]); }4.4 多逻辑设备共用端口的冲突一颗 SIO 上多个逻辑设备共享同一对索引/数据端口这意味着你操作逻辑设备 A 的时候逻辑设备 B 的上下文是被切走的。如果代码里有中断或者并发访问BIOS 阶段虽然少见但不是没有就可能出现“我明明选了设备 A读出来却是设备 B 的值”。解决办法是临界区保护在选设备到读完寄存器这一小段里关中断或者用自旋锁如果有的话。在纯 BIOS 环境里关中断是最简单直接的手段。5. 从能读到能用把串口真正跑起来的验证方法5.1 最小验证闭环配置改完怎么确认真的生效了我的最小验证闭环是配好基址和使能然后往基址偏移的数据寄存器通常是基址 0写一个字节再读线路状态寄存器基址 5看发送保持寄存器是否为空。如果状态位正常翻转说明基址对了、设备激活了。这一步不需要接任何外部设备纯寄存器层面就能验证大半。再进一步把串口的波特率寄存器基址 0 配合线路控制寄存器的 DLAB 位配好往外发一个字符用另一台机器或者逻辑分析仪抓波形确认波特率准确。这一步能抓出“基址对了但时钟源配错”的问题。5.2 常见“配了但不工作”的原因清单基址设了但使能位没开最常见读寄存器正常但设备不响应使能了但基址和别的设备冲突表现为时好时坏波特率分频值算错发出来的数据全是乱码中断配了但没清中断标志一开中断就反复进中断配置模式没退出设备行为完全异常。这张清单我基本每次调试都会过一遍能省掉大量瞎猜的时间。5.3 用轮询代替中断做初期调试前面提过初期调试强烈建议用轮询。串口轮询发送的逻辑很简单查线路状态寄存器的发送保持寄存器空位空了就写数据没空就循环等。虽然效率低但没有中断配置的干扰能把“配置是否正确”和“中断是否正确”两个问题解耦。等轮询能稳定收发再上中断出问题时你就知道问题一定在中断配置而不是基址配置。这个“分阶段验证”的思路是我觉得比任何具体寄存器值都值钱的经验。底层调试最怕多个变量同时动一次只改一个东西改完验证验证通过再动下一个。6. 收尾的一点个人体会SIO 读写这套东西代码量其实很小几十行汇编就能覆盖核心逻辑真正难的是对“状态”和“时序”的把握。我踩过的坑里八成不是不会写端口而是没意识到芯片当前处于什么模式、上一个操作留下了什么副作用。所以我现在写这类代码习惯先把状态机画清楚锁定态、配置态、正常态每个态能做什么、怎么迁移想明白了再动手比边写边试快得多。另外就是备份意识。底层寄存器操作没有“撤销键”改之前先读、先存这个习惯救过我很多次。真到了要改一个不熟悉的寄存器宁可多花五分钟把相关寄存器组 dump 一遍也别赌自己记得住原值。这套方法放到别的底层外设上同样适用不只是 SIO。