UEFI开发探索13 – 访问PCI/PCI-E设备1

我所用的测试卡是PCI-E设备,公司商用的产品也是PCI-E设备。所以,我很早就“被迫”去读那些PCI spec。

从软件工程师的角度,我觉得只要解决几个问题就行了,其余的细节不妨碍编程。

1)      PCI/PCI-E设备是如何定位的,也即程序如何找到设备;
2)      系统把它认作什么设备;
3)      如何访问设备的内部寄存器(一般要去读所使用的PCI-E芯片的资料);
4)      商用化的产品,需要考虑设备对ACPI的各种电源事件的处理。

前三个问题需要仔细研究下PCI设备配置空间:

图1 PCI配置空间(PCI spec2.3)

除了主PCI桥之外,其他PCI设备都应该实现PCI设备配置空间。配置空间包括一系列的PCI配置寄存器,其实现位置可以在PCI配置空间中,或者IO空间,也可以直接在申请的memory空间中实现。

一般情况下,操作系统使用这些配置寄存器的内容来决定为PCI设备加载什么驱动程序:供应商ID、设备ID、版本号、类别代码、子系统供应商、子系统ID。具体的含义可以参考PCI/PCI-E的Spec手册。

隔离卡/还原卡的开发中,最让人印象深刻的就是类别代码寄存器了。它规定了你的设备是大容量存储设备还是网络设备,或者其他设备。主板有时候会找不到你的设备,无法加载Option ROM代码,改一种类型就可以了。

这个章节主要实现列举PCI设备的代码。

实际上,uefi shell下已经有了类似功能的命令。之前开发的时候我并不清楚,是在写博客的时候慢慢了解到的,可见写博客真是种学习的好方法。

Shell下pci内置在运行环境中,是\ShellPkg\Library\UefiShellDebug1CommandsLib下pci.*编译出来的,在模拟环境中执行:

图2 Nt32模拟器中执行shell命令pci

很遗憾,无法执行。这也意味着我们编写的pci列举程序无法在nt32下执行,最好还是以x64方式编译,在实际环境中执行。

UEFI Spec中有两个protocol可以访问PCI设备,PCI Root Bridge I/O Protocol和PCI I/O Protocol。这两个Protocol有什么区别,到现在我理解得还是不深刻。

从实践中知道,PCI I/O Protocol可以访问改在在主板上的所有PCI设备,而PCI Root Bridge I/O Protocol不能访问PCI to PCI桥设备。两种Protocol都可以访问上一篇博客描述的测试板卡。

这次用PCI Root Bridge I/O Protocol来获取找到的PCI 设备的信息。

参照UEFI spec 2.8 page 649,了解PCI Root Bridege I/O Protocol的用法。描述太长了,就不贴图出来了。

代码仍旧放在了百度云上。核心函数为:

EFI_STATUS PciDevicePresent (
  IN  EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL     *PciRootBridgeIo,
  OUT PCI_TYPE00                          *Pci,
  IN  UINT8                               Bus,
  IN  UINT8                               Device,
  IN  UINT8                               Func
  )

代码中以此函数,获取所有能找到的PCI设备。不过Nt32中无法演示,旁边也没有机器可以测试,运行结果就不演示了。

百度云链接:https://pan.baidu.com/s/1gccSosw8_UAGTI5gZPnLCA
提取码:dx23
代码在 05 ListPCIMessage-01下。

6 total views, 6 views today

UEFI开发探索12 – Oprom测试板

今天拿到了测试板,可以把UEFI Oprom写到硬件里面去了。手有点痒,想写一个程序来试试。

测试用的板子非常简单,去除了所有实际的硬件控制元器件,只留下了WCH366和一个128K的ROM。不过按照沁恒电子的资料,WCH366只支持64K的寻址,而且还不能同时寻址,意味着我们只有32K的空间可以写代码。

做了十个,足够我折腾很长时间了。

图1 测试用的小板卡

测试用的板子非常简单,去除了所有实际的硬件控制元器件,只留下了WCH366和一个128K的ROM。不过按照沁恒电子的资料,WCH366只支持64K的寻址,而且还不能同时寻址,意味着我们只有32K的空间可以写代码。

图2 WCH366 spec

我直接在已经实现的Option ROM代码上修改了一下,用来演示。如果有读过Foxdisk代码的技术同好,可能会发现我的代码有点熟悉:

图3 代码列表

如图,实际上很多代码确实是从Foxdisk中移植过来的,特别是图形和文字显示部分。毕竟这是为了开发产品而做的,我力图做到屏蔽底层细节。也就是说,所有和硬件打交道的部分,都提供同样或者类似的接口,让用户层去调用。

这是很自然的做法,由此能保证以前的一些工具和函数能够共用。比如提取汉字、提取图形,调试smbus的工具等,都可以直接或者略微修改就可使用了。

在Foxdisk中介绍了一些这样的工具(那个系列的博客还没有写完,后续会继续写),这里就不讨论细节了。

如何编写Option ROM,要放到比较后面的章节来记录。在那之前,还有太多的程序需要写,包括PCIE设备的访问、汉字显示、BMP/PCX图形显示、SMBUS访问等。今天主要试一下到手的测试板卡。

编译好了之后,我准备将ROM代码写入板卡。用DOS下的一个工具来刷写,沁恒电子提供的。

我的测试平台是AMD A8,MSI的BIOS。测试中发现,这个平台不支持大容量存储设备的PCIE ROM,将PCI子设备改为网络设备后可以了。(修改inf文件)

图4 测试平台

写入固件代码:

图5 写入固件

写完后重启,最后的显示效果:

图6 显示效果

大功告成!

11 total views, 4 views today

UEFI开发探索11 – 鼠标前传

这不是写鼠标的历史,而是记录在很久以前,在Legacy BIOS下,我写鼠标驱动(BIOS/DOS下运行)的过程。

我对底层编写程序,从参加工作的时候就非常着迷。当时接手隔离卡5.0的开发维护,主体产品已经完成,我一直思考:怎么才能让产品和别家的不同呢?

介绍下背景知识:隔离卡V5.0是公司第一款采用PCI Oprom开发的产品,插上就可以出界面。省去了用户安装软件的过程,一推出就大受欢迎。我们采用的是沁恒电子的WCH365,代码也参考他们的DEMO开发的。很快,对手使用同样的芯片,有了同样的方案。

就是在这种情况下,我希望找到新的竞争点。突然想到,市场上所有的PCI Option ROM上的界面,都是支持键盘的,没有支持鼠标的。这不就是一个切入点吗?如图,这是当时的隔离卡。我工作的笔记本没带回来,图是从网上找到的,这么多年,没想到还能找到。

图1 易思克隔离卡v5.0

有了想法,执行起来也不容易。

问题1: 鼠标肯定是在图形模式下运行的,怎么无缝的将鼠标显示在目前的界面?
问题2: 针对鼠标的驱动代码量不能大,ROM只有32K,现在出货的产品已经有21.5K,留给鼠标驱动的就没有多少空间,必须控制代码量;
问题3: 我知道DOS下有int33h可以控制鼠标,也有大量的例子可以参考。可惜我们是在BIOS上运行的,无法调用;

第3个问题,实际可以通过BIOS提供的int 15h C2h的00~07功能来实现。这是支持PS/2鼠标的软中断(实际测试,USB有时候也支持),phoneix/award的BIOS手册上有相关描述。

图2 鼠标的bios中断

手册上只列出了中断的说明,如何运行却没有解释。

注意看AL=07的功能号,其中指明了ES:BX指向mouse driver。鼠标驱动是由用户自己编写,由BIOS来调用的。其原理如下:

1)      用户准备mouser driver,必须是FAR调用(跨段);
2)      当鼠标有任何动作时,比如移动、单击等,BIOS将会调用mouse driver,将鼠标的位置和状态传递给用户;
3)      每次的鼠标动作,都会触发BIOS调用三次mouse driver,每次只传送一个数据。传递的三个数据分别为鼠标状态、鼠标X坐标、鼠标Y坐标;
4)      BIOS每次的传递方法为:将数据压入堆栈,再调用mouse driver。

也即在鼠标有动作时,BIOS依次的动作为(发生三次):

Push mouse_status(2字节)
Call far ptr mouse_driver(4字节)

数据就存在sp+4处。

这些过程有点像windows驱动,都是由系统来调用程序,只不过这个比windows简单很多。

有了这些背景知识,就可以编程了。

Mouse driver的主要功能如下:

1)      根据传过来的数据,在新的位置绘制鼠标图案,并消除原位置的鼠标图案,同时将原位置鼠标图案下掩盖的图形还原;
2)      准备全局变量,将鼠标的状态传递出去,让使用mouser driver的程序可以根据鼠标动作进行事件处理。

更细节的处理可以看源码,仍旧放在百度云上,有兴趣可以看看。不过估计没有什么人还会对看汇编程序感兴趣,为了减少代码量,全部都是汇编写的。

我也只是因为,再不把这篇博文写完,以后估计连源代码都找不到了。以前没有养成如现在一样规范写日志的习惯,日志记录非常分散,有些已经找不到了。

程序使用borland 3.1中的tasm编译,如果用微软的masm(16位)编译,有些宏名需要修改。

在实际运用过程中,发现AMI的bios支持得不是很好;USB的鼠标也不是很理想。代码中加了判断,如果不支持的话,就不会启用鼠标驱动。虽然不完美,也帮助公司确立了产品技术领先的形象,是一段让我颇为自豪的经历。

虚拟机下是没法运行的,所以不截图了。就把编译过程截下来吧:

图3 编译mouse driver

回到UEFI 的鼠标支持吧,估计要再过几个博客才会写到。UEFI中似乎没有中断的概念了,取而代之的是事件方式。怎么构建出一个让主程序不需要管理、只需要处理关心的鼠标事件的代码,类似windows的事件处理,简洁而高效,是一个需要深入思考的问题。

研究技术真的是没有止境。

百度云链接:https://pan.baidu.com/s/1gccSosw8_UAGTI5gZPnLCA
提取码:dx23
代码在 X2 Mouse driver下。

46 total views, 5 views today

UEFI开发探索09 – 图形显示02

上次对UEFI的图形模式有了一定的了解,根据得到的信息,我准备用1024×768的分辨率来运行程序。

这个分辨率我比较熟悉,以前的代码(legacy bios下)可以复用。在Vesa标准中,有多个模式是这个分辨率的(我最常用的是105h)。如图:

图1 VESA graphics

那么UEFI的这个分辨率对应的是哪个模式呢(UEFI 下的模式索引值为3)?虽然都是从EDID中读出来的,不过经过了UEFI 的重组,我也搞不清到底是对应哪个。从blt的操作方式来看,可能是118h;不过似乎其他的模式也能类似操作。

算了,不纠结了,什么时候去跟下代码就知道了。

核心工作的API是Blt(),需要好好读一读。UEFI spec 2.8 page 70:

图2 bit() function

我把其内容翻译了一下:

有了这些信息,足够构建图形的基本函数了。

我的做法是,首先构建画点函数putpixel;然后根据采用不同的才做模式,将画竖线(HLine)、画横线(VLine)、画矩形(rectangle)、画矩形块(rectblock)以及画圆(circle)。其中,画圆函数使用了putpixel来实现,采用的是四象限画圆法。

以上的算法都是参照Foxdisk3.01的代码来构建的。

废话不多说,具体的可以看源代码。编译一下,来看看效果。

图4 程序运行效果

让程序测试了三个函数的运行效果,实际上包括设置显示模式、背景画图等都试了。

也就是说,如今我们有了一张1024×768的白纸,各种颜料,以及可以自动画各种形状的画笔,有什么画不出来呢?

百度云链接:https://pan.baidu.com/s/1gccSosw8_UAGTI5gZPnLCA
提取码:dx23
代码在 03 GraphicsOutput1下。

31 total views, 4 views today

UEFI开发探索08 – PE/COFF

前几天有人问起我,对UEFI的格式文件的理解。

嗯,我基本上快把PE格式忘光光了,那是很多年前,搞反汇编,混看雪学院时搞的事情。当时花了几个月,专门学习软件加密、加壳、扰乱反汇编等等。然后,又花了几周,破解了几个dll,扔给工程师去用,就再也没有接触过了。

闲言少叙。既然忘记了,就花点时间去回忆一下吧。上午的会议开完,下午有两个小时空档,找找以前的日志和资料。

1 DOS下的exe文件

这是PE格式的起源,有必要了解一下。我曾经做过一个小程序,为了直接加载exe,自己实现加载器。这是为了在Foxdisk中,能够复用我以前的exe程序而做的。

DOS下的exe结构非常简单:

图1 DOS下EXE文件结构

DOS下的加载,分为exe文件的加载和com文件的加载。它们加载后是这样的:

A  exe文件加载
1) DS 和 ES 指向 PSP 的段地址;
2) CS 指向代码段的绝对段址;
3) SS 指向堆栈段的绝对段址;
4) IP 指向代码段入口时第一条指令的偏移地址;
5) SP 指向堆栈段如口时深度,此值由文件头位移 10H 的字域决定;
6) BX, CX 是加载程序的字节长度。

B  com文件加载
1) CS,DS,ES 和 SS 指向 PSP 的段地址;
2) IP 固定为 100H;
3) SP 位 FFFEH ,并在栈顶出压入一全 0 字;
4) BX, CX 是 COM 文件的字节长度。

需要注意的是重定向的过程,加载程序会将重定向表的段和地址偏移加上起始段和偏移的,这个很好理解。这都是在实模式下处理,非常简单。保护模式下,有非常多的信息,GDT、LDT、IDT,虚拟地址和物理地址的换算,比这个复杂多了。

另外,DOS加载的时候会在程序段之前设置一个具有256字节的信息区,比如命令行参数就存在这个里面。我们自己写加载程序就不用管这些了,我主要的目的是让程序运行起来。当然,我的程序也不会去调用int 21h之类的dos中断,否则foxdisk加载起来也无法运行。

代码放在百度云上,有兴趣可以瞧瞧:
百度云链接:https://pan.baidu.com/s/1gccSosw8_UAGTI5gZPnLCA
提取码:dx23
代码在 X1 Run Dos exe下

2 PE/Coff

我手上有微软以前发布的《Microsoft Portable Executable and Common Object File Format Specification》Revision 8.2,2010年发布。最新的版本不知道是什么,估计变化不大,也不用找了

如何去理解PE结构,我以为,最好自己写一个PE格式分析软件,或者读一读别人写的分析软件。正好,以前看过的《软件加密技术内幕》(看雪学院),提供了这么一个工具:

图2 PEInfo

PE文件一个方便的特点是磁盘上的数据结构和加载到内存中的数据结构是相同的。加载一个可执行文件到内存中 (例如,通过调用LoadLibrary)主要就是映射一个PE文件中的几个确定的区域到地址空间中。这就使得分析起来很方便,都是些机械性的动作。

PE的结构如下图。

图3 PE文件架构

开始的时候,看得我有点糊涂。我的第一个反应就是,这些到底怎么定位?我用UE以bin文件的方式打开一个exe文件,对照书上的数据结构算了很久,才有点眉目。

记下我的一些感想:
1)      MS-DOS头部的偏移0x3c处的内容,为PE文件签名的偏移地址(也即PE文件头起始);
2)      PE文件头的结构由三个字段组成:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
对照上图可以有个直观的了解;
3)      OptionalHeader最后包含了16个数据目录表,以数组方式存储,第一项和第二项分别为输出表和输入表;
4)      紧随PE文件头的是区块表,其数目在PE文件头的FileHeader中给出了。我测试的这个程序其值为6;
5)      区块表规定了各个区块的信息,包括我很关心的大小和属性。区块表后面就是各种区块了;
6)      调试符号的位置和大小也在PE文件头的FileHeader中给出了(windbg用的pdb也在此处?)。

大致就是这样的框架,其余的细节可以参照spec一点点看。

需要注意的是,在可执行文件中,有许多地方需要指定内存中的地址。比如全局变量的地址,必须定位准确。PE有一个首选载入地址,但是它有可能载入到任何进程空间,所以不能依赖这个。

由此出现了相对虚拟地址(Relative Virtual Address, RVA)概念。这是一个偏移位置,方便exe载入的时候,从基地址加上它,把相应的磁盘文件载入。

图4 Stud_PE和PEInfo测试图

我找到了一个不错的,可以查看PE文件的软件Stud_PE。可惜的是它只能支持exe和dll文件名的载入。

PEInfo没有这个限制,可以看efi的信息。什么时候改造一下,符合我自己的使用习惯。

这篇博客我不是很满意,时间只有两个小时,很多细节不够深入。最好的方法就是仿照PEInfo,自己编写有个查看efi的程序,估计理解就深刻了。以后找时间重写一次。

今天就先这样吧。

36 total views, 6 views today

UEFI开发探索07 – 关于SMBUS的开发故事

这篇博客曾经发表在高端调试的论坛上,我把它转移到这里来了。UEFI访问SMBUS设备,是我计划中要写的,这篇就作为其前言吧,其中没有任何与UEFI相关的内容。

当时我已经开发完了C850F320上的smubs访问代码(作为smbus device)、Legacy BIOS下的smbus访问代码(在dos下调试,运行于OptionRom上),期望在windows上开发一个驱动,能通过smbus直接访问C850F320。这能让目前产品的价格降低20元,以当时的出货量,一年的成本可以节省40w,很可观的。

我把这篇博客拷贝过来了,绝不是因为今天上午要开会,没时间写了。说不是就不是!

以下为原文:


这是以失败告终的故事,这个故事中我学会了两件事情:1 选择剑走偏锋的技术方法的不是神人,走多了会变成神经,比如不才我;2 童话故事里规则多简单,坏人必定玩完,好人从此过着美好的生活。可惜,现实不是童话,所以主板上串口没了、SMBus的Pin也不连了。

以上纯粹是牢骚,发泄下我对现实的不满。在我做的项目中,需要在BIOS和单片机、OS和单片机间选择一种可以传输少量数据的方法。我选择的是SMBus,用单片机(C850F320)构建SMBus Device,以PCI/PCI-E板卡的形式呈现。大家知道,在PCI/PCI-E上提供了SMBus的时钟和数据引脚,这样使得不需要任何连接线,插上卡就可以让BIOS和OS访问做好的SMBus Device了。写下这篇是为了总结我走过的道路,希望可以让别人在遇到同样的问题时,少走一些弯路。

1 开发板的结构和SMBus设备的构建

图1 支持SMBUS总线通讯的卡

这是我使用的开发板,F320提供了不少功能,在这个开发板上可以去模拟USB设备,很有意思。板卡是PCI形式的,其他功能就不描述了,我的目标是使用F320模拟出一个SMBus device。

F320的开发环境中提供了一些SMBus的例子,可以对照参考。从芯片手册中知道,SMBus 接口可以被配置为工作在主方式和/或从方式。在任一时刻,它将工作在下述4 种方式之一:主发送器、主接收器、从发送器或从接收器。SMBus 在产生起始条件时进入主方式,并保持在该方式直到产生一个停止条件或在总线竞争中失败。SMBus 在每个字节帧结束后都产生一个中断;但作为接收器时中断在ACK 周期之前产生,作为发送器时中断在ACK周期之后产生。

我要求的设备很简单,从发送器和从接收器,可以进行字节的读写。因此必须实现主控制器读从设备和主控制器写从设备的功能。芯片手册中也给出了详细的时序图,可以参照编码。不想将这篇文章拉得太长,具体的实现就不讨论了,可以参考中国科技论文在线上我以前写的一篇论文《PC机与单片机的SMBus底层通信实现》,上面给出了代码的架构。

2 软件实现

2.1 第一条道路-SMBus driver

我很希望微软铺建好了一条康庄大道,可以让我在OS层轻松的访问做好的SMBus设备。就像访问串口、访问USB HID设备一样,不需要特别去为设备写驱动,应用层的API 就搞定了,这样的世界多美好啊。

怀着这样的想法去寻找,没有发现微软提供的应用层接口。不过找到这篇古老的文档《SMBus Device Driver External Architecture Specification》,1999年的。里面提到了一大堆的术语,相当高深,看起来是找对了。简而言之,其访问方法分为三步:1 Obtaining a Handle to the SMBus,找到SMBus设备驱动的上级领导;2 Enumerating SMBus Information,枚举设备信息;3 Initiating SMBus Requests,准备相应的Request,比如SMB_READ_BYTE。

到这儿为止,相当顺利,我看到这些命令之后,再对照 SMBus协议来看,觉得应该可以成功。所以,废寝忘食的编了两天,写好了一个调试用的驱动。文档中的代码有些小错误,不过很容易修改。我主要想看看其中的FindDevicesOnSMBus能运作的怎么样,是否能够得到Handle。

好,准备一台有串口的台式机,搭建调试环境。启动windbg,下断,运行,然后开始一步步跟踪。执行完IoGetDeviceInterfaces(&GUID_SMB, NULL, 0, &pInterfaceList)之后,发现pInterfaceList的值是空的!我的尝试失败了。

回头去分析,我一直认为GUID_SMB提供的是smbus设备上层驱动的对象,驱动获取到handle后,可以对自己的设备( 如我构建的设备地址为0xF0)发送读写命令。不过在windows的驱动库中,我没有发现smbhc.sys和smbclass.sys,我想这可能是其原因。微软对SMBus的支持并没有像文档中描述的那样运作。不过,我搞不清楚Smbattery是怎么运作的,微软甚至提供了一个例子来演示,因学识原因没有看明白,希望哪位同学可以详细的阐述一下。

第一条道路的尝试以失败告终。

2.2 第二条道路-I/O 访问

BIOS访问SMBus设备的时候,比如访问SPD,是通过南桥的寄存器来访问的。以读字节为例,基本的流程如下:

;1. Write Base+0 = FFh (Clear status)

;2. Read s = Base+0, if s AND 9Fh !=0 then goto step1

;3. Write Base+4 = x (x = (Device address << 1) + 1)

;4. Write Base+3 = y (y = Data area offset = 0..FFh)

;5. Write Base+2 = 48h (start byte read command)

;6. Read s = Base+0, if s AND 4 != 0 then ERROR, if s AND 2 == 0 then repeat step6

;7. Read d = Base+5 (d = Data read)

;8. repeat step1 to step7 to read all data

写字节的流程也差不多,以上的伪代码是从Rw-everything安装后产生的文件rw.ini中摘录出来的。主板南桥的技术文档中一般也有相应的读写流程,可以参考看看。

这就提供了另外一个思路,在OS层提供I/O端口操作的驱动,按照上述的流程直接去访问SMBus设备。SMBus本身有仲裁机制,不会发生设备独占的事情,理论上应该可行。

为了便于调试,我决定在DOS下去写代码。微软提供了READ_PORT_UCHAR和WRITE_PORT_UCHAR以允许对设备进行I/O访问,驱动中可以直接使用。我的代码在DOS下运行,为了便于移植,我自己在代码中实现了同名的函数,其他的函数如果要I/O访问设备的话,必须调用它们。这样,如果代码在 Dos下调试通过,直接拷贝到搭建好的windows 驱动中,就可以使用了,相当方便。

我的目标是在所有主板上都可以访问SMBus设备,而每个厂家的SMBus的bus  number、device number、function number都不相同,所以在这条道路上,兼容性实现是个很大的问题。

很幸运,在Intel的主板上没有遇到什么问题,代码运行良好,不管是按字节读设备还是按字节写设备都没有问题。先后尝试了915、G31、Q45几款主板,DOS下读写设备,基本上没有遇到问题。将代码移植到驱动中,也很容易就编译通过了。

使用DriverMonitor加载驱动,运行测试代码,在XP下去读写上述做好的SMBus设备,运行良好。随后在Win7下也做了一些测试,没有问题。至此为止,一切似乎很美好。

我美滋滋的搭建了一个AMD的平台,南桥是ATI的SB800。将代码中的SMBus Base Address的获取位置修改为AMD的(Intel的是bus 0,dev 0x1f,fun 0x3,offset 0x20;AMD的为bus 0,dev 0x14,fun 0x0,offset 0x90)。访问结果:设备错误,我傻眼了。代码修改为读取SPD,幸好,SPD的数据可以正常读取,看来代码没有问题。

我开始冷静下来,在另外一块VIA主板上去做类似的实验,结果相同。到底是怎么回事呢?主板上有若干组SMBus,难道每组的访问方法不同?

找来硬件工程师,一起来做了一些实验。

首先用示波器去量信号:使用示波器去采集信息,将单片机的两个引脚连到示波器上。在Intel主板上采集,不管是访问spd还是访问控制卡,都有波形出现。但是在amd主板上,一直没有波形出现。开始怀疑是否pci 上的smbdat和smbclk是否连接到南桥了。在amd的pci/pci-e上分别去量smbus的引脚,在上电的时候,pci上的这两个引脚没有电,pci-e上的倒是有。

为了验证想法,将内存DIMM的SDA 和SCL直接连接到PCI控制卡对应的引脚上,再用原来的代码去访问。(AMD平台上做的实验)在插上内存后,访问是成功的(我的主板如果没有内存在DIMM槽内,似乎SMBus没有使能,也就无法访问设备了)。

得出的结论是:使用内存这组SMBus,访问我自己做的设备是成功的。因此,要么是访问PCI/PCI-E上的SMBus设备,需要做一些配置,要么硬件上根本没有将PCI/PCI-E的两个smbus pin连接。

其后是问遍所有我认识的主板工程师和BIOS工程师,不厌其烦的将上面的问题一遍遍的重复,耗费若干口水和脑细胞之后,上述想法得到了一些证实。确实存在硬件不连的可能性,但是在AMD的平台上,也确实有些主板需要配置后才能访问PCI/PIC-E的SMBus设备。如何去做,我已经没有精力再去研究了。如果你知道的话,望不吝赐教^^。

感谢开发过程中张老师、晓文、志坚、Jerry、Kevin、Terry、Frank、Fly给予的无私帮助,和你们的技术探讨是我最喜欢的事情。

24 total views, 4 views today

UEFI开发探索06 – 图形显示01

到了我最喜欢的环节了,图形显示!

我曾经写过不少关于图形显示的博客和论文,特别是在Legacy BIOS以及嵌入式设备中的显示。PC的显示一直都遵循着VESA的标准,在底层的访问还是比较一致。虽然在多年的开发中,也在一些主板上出现过奇怪的现象(一次而已。Award某款主板,设置显示模式的时候,花屏。在调用设置显示模式的函数前,多压几次堆栈,问题又消失了。)。但当年写的核心显示代码,运行了近10年了,也没出现过显示的问题。

这是我曾翻烂过的参考书:

图1 参考书

还有这些…

图2 参考书

以及《最新VESA SVGA图形图像编程秘技》 李军著、《C语言游戏编程从入门到精通》,还有图像格式,主要是BMP、PCX和jpeg。哦,还有一本雷军(没错,我就是当年用过这本书,才成为米粉的)和求伯君的《深入DOS编程》。

写得最扎实的是《IBM PC的原理及应用》以及《PC技术内幕》,想了解底层显示原理的可以参考一下。虽然我觉得这些知识有点过时了,但对理解还是有帮助的。

在Foxdisk的博客中,曾经简略的提到了如何进行底层的显示编程,三个步骤:

1)      设置显示模式(如0x103为 800×600 256色的配置);
2)      设置颜色寄存器;
3)      按照图形显示的原理编写画点、画线、画圆等基本函数。

详细的显示原理以及编程没有去讨论,后面会陆续在Foxdisk的系列博客中谈到的。

回到UEFI的显示上,基本的显示原理不会有什么变化,intel也没有重新造一个新的显示模型出来。我觉得UEFI应该还是如上述三个步骤一样执行。无非还是显示模式如何设置,怎么画点,以及如何减少显存换页。查看UEFI spec(对照UEFI spec 2.8),有个关于显示的PROTOCOL,截图如下(uefi spec 2.8 page 448):

图3 EFI_GRAPHICS_OUTPUT_PROTOCOL

开始写代码。

第一个问题就是SetMode。在Spec中提供了两个SetMode函数,一个是EFI_GRAPHICS_OUTPUT_PROTOCOL的,另外一个是EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL的,这让我很困惑。猜测后一个是用来转换Text mode和Graphic mode的。

以前在调试汇编代码时候,int 10h设置显示模式为3,就是进入了Text mode。调试显示程序时,这是常用的代码。因为进入Graphic mode后,debug.exe的信息很多时候是没法显示的,必须回到Text mode才能看到。因此我有上述的猜想。

在调试中,我参照了aptio写的代码,并且对照调试结果,收集了一些信息。未来可能用得上,记录如下。

1.      UGA window模拟环境中,text mode有三种,graphic mode有五种;
2. 以前的代码(aptio)使用了gEfiConsoleControlProtocolGuid来获取protocol,但是在uefi spec2.3.1中已经输出了ConOut了,我以为没有必要再这样使用。怀疑与版本兼容有关,是否EDK1中是这样实现的?
3. GOP在后续版本中替代了UGA。see page 2065 in UEFI_Spec_2_3_1;
4. \EdkCompatibilityPkg\Foundation\Library\Dxe\Graphics,包含了一些使用方法;
5. \EdkCompatibilityPkg\Foundation\Protocol\ConsoleControl,包含了另外一些使用方法;
6. Text mode的第二个模式,columns和rows都是0,怀疑就是EfiConsoleControlScreenGraphics,      转换为graphic mode所要用到的。

照旧将代码放到百度云上了。这次的代码主要是收集信息,加深对UEFI图形显示的理解。将代码编译,显示的信息如下。

图4 代码运行输出

可以看到各种显示信息:Text Mode有三种,Mode 0应该就是我们常用的80×25的字符模式;Graphic则有5种,分辨率分别为800×600、640×480、720×400、1024×768、1280×1024。对照VESA的标准,能体会不少事情。奇怪的是为什么不是顺序安排分辨率的,有什么深意吗,还是EDK的开发者就是这么随意?

百度云链接:https://pan.baidu.com/s/1gccSosw8_UAGTI5gZPnLCA
提取码:dx23
代码在 02 GraphicsOutput下

66 total views, 3 views today

UEFI开发探索05 – 搭环境遇到了问题

这两天妻子去阜宁,孩子没有带过去。我只能在陪孩子之外,腾出一点点时间,来搭建UDK2017/UDK2018的编译环境。

大部分的时间都在装VS,试了两个版本,VS2013和VS2015。我不知道是不是我的版本有问题,总是不成功。Ubuntu16.04的也没有安装成功,让人颇为恼火。总计来回折腾了12个虚拟机,真是不甘心。

下周开始又没有太多时间了,几个案子都在催着完成,有大量的工作要去做。两个专利也还没写,前期的资料都还没收集完。思来想去,我仍旧以UDK2010来编译这些代码吧。主要的目的是熟悉这些API和UEFI的架构,我目前没有开发BIOS,太细节的暂时不必理会。

插一句,为了方便调试,我请同事做了一些带有PCI-E扩展ROM的卡,主芯片是CH366,下周应该可以到我手上了。对于进入操作系统之前,可以跑自己的程序,我一直很热衷:总想脱离操作系统做点什么坏事。→_→

比如针对操作系统核心的几个文件ntldr.dll,gdi32.dll等,使用Oprom对其认证,防止篡改。或者和杀毒软件配合,进入OS前做些事情。整个硬盘都可以控制,可以预先做很多动作。

又扯远了,回到搭建编译环境的事情吧。

我主要使用Vmware搭建虚拟机,开发用的操作系统为win10。中间的各种小问题就不说了,基本上都解决了。主要是两个问题,没有找到解决办法。

1. VS2013+UDK2017,VS2013全部安装,WDK8.0和WDK8.1的目录都有。编译Nt32Pkg无法通过。如图1。

图1 UDK2017+VS2013,编译NT32Pkg报错

2. VS2015+UDK2018,Nt32Pkg编译通过,AppPkg编译不通过。本系列Blog中的“UEFI开发探索03-环境搭建2”曾经描述过安装过程。编译AppPkg的报错如下:

图2 UDK2018+VS2015 编译AppPkg报错

这段时间以把程序编译运行为主,就用UDK2010,尽快把博客写完。和张佩去年就约定要把Foxdisk和UEFI的开发记录下来,一直到现在还没搞定,甚是惭愧。

搭建环境的问题,留待日后吧,看是否能凑些边角料的时间来找找原因。O__O

如果哪位朋友知道怎么回事,也望不吝赐教,感激不尽。:)

32 total views, 4 views today

UEFI开发探索04 – 与键盘的互动

我当年学习UEFI的最终目标是实现UEFI Oprom,写入PCI-E ROM,作为隔离卡的底层软件运行。中间的一个目标是编写UEFI driver/application,与bios一起编译,同样是作为底层软件,与另外一款卡通讯。当时是与联想共同开发了一款安全隔离计算机,中间目标就是为这款产品而设的。

最后两个目标都实现了,当年刚做的时候还是比较辛苦的。第一个面临的问题就是使用哪种程序入口。有三种可选:UefiMain,main,ShellAppMain。

参照当时能找到的资料,《Beyond BIOS》、《Harnessing the UEFI Shell》以及UEFI spec(当年看的是2.3,现在都到2.9了),还有bios之家、网友的博客,终于摸到了一些门径。

图1 曾经使用过的参考资料

当年的我并不了解这些程序入口的区别,是最近写博客的时候看《UEFI原理与编程》时才有所了解(终于有本中文版的参考资料了!感谢作者戴正华,有机会真想拜见)。

我的理解,main和ShellAppMain肯定是不能用在Option ROM上的。那么多的链接库,一是没法放进去,二是即便可以放进去,工作量也太大了。简单来说,就如同我之前开发的Legacy Option ROM一样,只能调用int 10h,int 13h,int 16h等这些bios中断,想调用int 33h这些DOS中断,你只能把整个DOS弄进去(更别提那个时候可能BIOS都还没准备好,放进去也运行不了)。

不过,使用main的程序入口,可以很方便的进行Shell下的测试。所以在实现这第一个程序的时候,我选择了这种编译方式。

如图,使用的是以下的API(UEFI spec 2.8 Page431):

图2 访问键盘所用API

API还是很好理解的,看EFI_KEY_DATA的数据结构,与int 16h 0号功能返回的是一样的。我不知道UEFI是怎么处理硬件中断9的,估计也是封装在了这些API中。

程序编写相对比较容易,gBS和gST是预置的,直接使用就可以了。(代码在文末提供下载地址)根据API的说明,我编写了一个获取键盘输入的函数GetKeyEx,将用户输入的键盘信息保存下来。

上述的工作相对比较简单,有点麻烦的是准备编译。使用main函数的工程模块需要在AppPkg环境下才能成功编译。

为方便后续的开发,我在根目录下建立了一个\_LuoApp的文件夹。在\AppPkg\AppPkg.dsc中添加相应的inf文件,指明要编译的代码,如图所示。

图3 建立编译用的目录

(20190512 20:13 Robin: 这次的编译仍旧使用了UDK2010的环境。我试图搭建UDK2017或UDK2018,都不成功。有的NT32Pkg可以编译,但是AppPkg无法编译通过;有的则连Nt32Pkg都编译不通过。VS2013和VS2015都试过了,怀疑是我VS版本有些问题。)

修改target.txt,设置为如下参数:

ACTIVE_PLATFORM       = AppPkg/AppPkg.dsc
TARGET_ARCH           = IA32
TOOL_CHAIN_TAG        = VS2008

打开Visual Studio 2008 command,编译:

C:\MyEorkSPace>edksetup.bat
C:\MyEorkSPace>build
或者build -p AppPkg\AppPkg.dsc -a IA32 -t VS2008 (不需要修改target.txt了)。

生成的文件为c:\myWorkSpace\Build\AppPkg\DEBUG_VS2008\IA32\_LuoApp\Luo2\Luo2\OUTPUT\Luo2.efi

将其拷贝至之前编译好的NT32下(主要是可以使用SecMain.exe):

c:\myWorkSpace\Build\NT32\DEBUG_VS2008\IA32\

运行SecMain.exe,测试Luo2.exe:

图4 编译好的键盘处理程序

代码放在百度云上,对照看下,逻辑还是比较简单的,将用户的按键信息打印出来。这个程序是以main入口函数来编写的,另外也编写了一个UefiMain入口函数的程序,功能一样,也在云端。

至此,实现了键盘的访问。如需要在实际机器上运行,只要修改编译参数即可。

百度云链接:https://pan.baidu.com/s/1gccSosw8_UAGTI5gZPnLCA
提取码:dx23

键盘访问的代码在 01 ReadKeyEx下。

22 total views, 3 views today

UEFI开发探索03 – 环境搭建2

从上次写完博客,过去了一个多月。中间到深圳出差,解决一个硬盘加密的项目,时间就这么过去了。等回过神来,发现写博客的计划又往后拖了。

下定决心,我想在这周搞定编译环境。

以前我是在XP下,使用UDK2010来编译的,比如这个:

图1 VS2008+UDK2010开发的uefi application

我原本的计划是用Ubuntu 16来搭建环境。最近正习惯在上面用vim和vscode编程,戒掉用了多年的UE和sublime。尝试了一天,总是遇到各种问题,特别是到现在也没有也没搞明白,GCC在安装过程中不知怎么变为了7.x。

这么下去,这周的计划又完成不了。我回到了用windows安装的老路,预备安装UDK2018。

还是折腾了一天,搞定了。把安装步骤和中途遇到的问题记录下来吧。

这是参考用的几个网址:

https://github.com/tianocore/edk2/releases

https://github.com/tianocore/tianocore.github.io/wiki/UDK2018-How-to-Build#how-to-build-windows-system

先吐槽一下,文档写得并不友好,我的时间全花在了编译Base Toosls上。实际上可以不用自己去编译Base Tools,直接下载就可以了。地址如下:

https://github.com/tianocore/edk2-BaseTools-win32

我是在读UDK2017的文档UDK2017 How to Build时发现的。

安装步骤(Win10下安装):

1. 安装 VS2015。我安装了图中所示的项,Python Tools for Visual Studio我觉得应该是不需要的,当时顺手选上了。

图2 VS2015的安装

2. 安装Python 2.7.16,同时选择将Python加入Path(安装的时候需要选择);

3. 安装Nasm 2.12.01,注意选择安装路径为C:\Nasm。在我的电脑-属性-高级系统设置-高级-环境变量…-系统变量-Path中添加此路径;

4. 安装Openssl工具和ASL工具(应该不需要,这是用来生成Base Tools用的);

5. 解压edk2-vUDK2018.zip到C:\MyWorkspace;

6. 添加系统变量PYTHON_HOME=c:\Python27;(补充一句,添加编译工具路径以及系统变量,可以在edksetup.bat中添加,比如:
Path = c:\nasm;%path%
Set PYTHON_HOME=c:\python27
这样就不需要像步骤3和6一样操作了,对我这种强迫症比较合适);

7. 打开“VS2015 x64 Native Tools Command Prompt”,运行edksetup.bat –nt32,会生成/Conf下所需的一些文件; 8. 执行build -p NT32Pkg\Nt32Pkg.dsc -a IA32 -t VS2015x86 run,启动SecMain.exe。

8. 执行build -p NT32Pkg\Nt32Pkg.dsc -a IA32 -t VS2015x86 run,启动SecMain.exe。

图3 UDK2018的模拟器

我的安装过程并不顺利,遇到了两个问题,记录如下:

问题1:python编译中找不到cxFreeze的module,无法通过编译。

解决:cmd打开,输入pip install cx_freeze。(注意Path中要包含Python的路径,否则无法执行)。

问题2:提示 File “GenFds\GenFds.py”, line 24, in <module>
ValueError: Attempted relative import in non-package

解决:将路径BaseTools\Bin\Win32下GenFds.exe更名为GenFds.LABZ,并将BaseTools\BinWrappers\WindowsLike;添加到Path路径中。
(此问题的解决参照http://www.lab-z.com/udk2018coming/https://github.com/tzz1996/Blog/blob/master/compile_udk2018_in_windows.md,感谢!)

至此,终于可以继续探索UEFI了。明天开始将之前写过的例子一个个编译运行,加深理解。至于Ubuntu的开发环境,找时间再做吧。毕竟ubuntu更轻量级点,windows上光VS2015就占了12G。我都是在虚拟机下开发,这么多开发环境,空间严重不足了。

(20190512 20:40 Robin: Nt32Pkg是可以编译,可是AppPkg没有编译成功,目前没有找到原因,编译的问题在 UEFI开发探索05中列出,后面找时间再解决。暂时还是以我之前的UDK2010来编译吧)

28 total views, 3 views today

UEFI开发探索02 – 环境搭建1

开发初期的目的就是做出可以在pci rom上跑的Oprom,当然是在uefi bios下。我的计划大致如下:

1 搭建完整的编译环境,了解使用哪些库进行编译;

2 我的设备最常用的几种总线通讯方式:PCI总线、smbus总线和usb总线,如何通过这些总线访问设备,必须要解决;

3 图形显示,我最感兴趣的,首要解决的是如何画点。PC的架构还是一样的,硬件原理差不多,还是有页、显示模式等概念,不过uefi api把很多细节屏蔽了;

4 键盘输入,必不可少。鼠标的功能,试试看吧;

5 uefi下可以使用网络的功能,这很吸引人。以前为了加快产品测试,在dos下开发网络通信功能,最头疼的是要去找各种网卡的dos驱动,新的网卡基本都不怎么提供了。写这篇博客的时候,我还没去实现过,后面尝试一下;

6 硬盘访问;

7 编译一个可以放在pci rom上的oprom。以前的还原卡、硬盘加密等,都可以用这种方式来实现了。

计划大概就是如此,主线是做Oprom,支线是如何访问各种设备,以应用为目的,对uefi进行探索。

先看下我常用的两款设备,用来运行实际代码的。

图1 带有smbus与usb hid通讯功能的切换卡
图2 带PCI-E扩展ROM的切换卡

图1的卡带有smbus通讯和usb hid通讯功能,是在与联想、方正合作项目中,开发双网安全隔离计算机所用的卡。图2 是公司目前在销售的产品,带有PCI-E扩展ROM,是一种用来隔离硬盘和网络的产品。

第一款设备没有扩展ROM,我是以uefi driver的方式提供oprom,bios工程师将oprom编译在bios一起,访问设备的。第二款是将oprom代码直接写入rom中,不需要bios工程师的配合,做起实验来比较方便。

两个设备上有大量与我们实验无关的元器件,大部分是用来切换网络、硬盘、USB的,以及供电电路。最近正在考虑把这两种设备合在一起,做一个实验板,不知道有没有人有需求。

啰啰嗦嗦交待了一大堆的前言,回到搭建软件的编译环境。

使用intel提供的UDK来搭建即可。以前是在sourceforge.net上下载UDK,地址:https://sourceforge.net/projects/edk2/files/。我当时用的是UDK2010,刚才查看了一下,sourceforge上已经有了UDK2015,而在github上已经有了UDK2017了。

嗯,周末我再研究一下,看看选择哪个环境比较合适。

Foxdisk11-无字库显示汉字2

刚从大学毕业那会,对操作系统极其入迷,总想搞清楚底层是怎么运行的。其中最感兴趣的是图形的显示,BIOS对硬件的控制等。找了很多资料看,正好公司的一些项目上也需要用到,就这么磕磕碰碰的实现了各种显示的代码。

Foxdisk的代码中,Vesa.c和Vesa.h就包含了显示的所有信息。主要是遵循了vesa的标准进行显示,采用C嵌汇编的方式来编写。目前,几乎所有的机器都支持VESA标准,而在X86平台下编程就是基于此标准的。VESA标准在原有BIOS提供的API的基础上,提供了一组扩展BIOS功能调用。对于标准VGA下的模式,仍然可以使用基本BIOS调用,但对于扩展模式,其许多操作如模式设置及视频缓冲读写等,则只能通过VESA标准提供的扩展BIOS来实现。

在VESA标准下,视频缓冲是以分页映射的方式进行操作。其基窗口一般为在0xA0000处,每页为64K。整个视频缓冲都通过映射到此窗口而得以直接存取。一般的图形编程步骤如下:

  • 设置显示模式(如0x103为 800×600 256色的配置)
  • 设置颜色寄存器
  • 按照图形显示的原理编写画点、画线、画圆等基本函数。

过多的细节不用纠结,Vesa.c中提供了画点函数putpixel256()。把屏幕当做一个大画布,设置模式的时候知道这块画布多大,用画点函数绘制即可。Foxdisk使用的是模式0x105,也即1024×768,256色。想像一下,有张宽1024像素点,长768像素点的画布,有支可在画布任何坐标处画点的笔,应该是什么画都可以画出来了,包括汉字。

Foxdisk中显示汉字,大致可以分为两步:

  1. 提取需要的汉字字模,保存到程序内部;
  2. 调用Font.c中显示汉字的函数,将需要的汉字显示在屏幕上。

Vesa.c和Font.c中屏蔽了很多显示的细节,特别是对各种字体及字体大小的处理。在设计之初,考虑去兼容各种不同的字体,比如黑体、楷体、宋体等等。代码中针对不同的字体,提供了各种编译用的开关。实际程序中,主要用了16×16宋体和24×24楷体,从提取汉字库的批处理命令可以看出来。

在博客“Foxdisk09-工具篇”中,已经初略的介绍了提取汉字库的工具了。我是为了能自动针对所有用到汉字的代码,自动提取汉字字模,开发的这些小工具。下面具体介绍下如何提取字库。

所谓无字库技术,就是在程序中建立一个类似字库的字库数组来代替字库。程序中建立的字库某种意义上独立于程序中显示程序。也就是存在这样的可能:单独对字库数组进行压缩,在使用的时候重新解压。这个特性在嵌入式的应用中非常有用,可以节约不少的程序空间。

一般说来,提取汉字字库的步骤应该是这样的:

1) 对包含需要提取汉字的源文件进行文法分析,析取出需要提取字模的汉字

2) 对提取出的汉字再次分析,并去除重复的汉字打开相应的汉字库,提取需要的字模,并按规则形成新的字库数组

3) 对照上一篇博客的说明,设计16×16及24×24的汉字字模结构体。

struct       hzk16_typ{         /*  汉字字模结构体  */

  unsigned int code;

  unsigned int array[16];

};

struct       hzk24_typ{         /*  汉字字模结构体  */

  unsigned int code;

  unsigned char array[3*24];

};

最终会将字模以上述数据结构的方式存储,Foxdisk中存储生成的文件为HZTABLE.H和HZK24.H。

具体的提取代码可以参照ehz24.c和etrhz.c,这两个文件的代码量都在400行左右。核心工作在于分析指定的文件,从中提取出需要转换字模的汉字。

为了便于分析,也为了程序编写简单,对指定的文件是有要求的。必须保证在提取的文件中字符串中没有//和/* 字符,否则可能会提取错误。提取程序并没有去进行字符串内部的语义分析,否则要处理的情况太多了。 代码就不贴出来了,放不下。对照编程的想法,以及代码,还是比较容易看明白运作的机制的。

UEFI开发探索01-起篇

2013年的时候,和联想的双网安全隔离计算机项目进入了平稳阶段。经过了前今年的合作开发,几代不同的产品的磨合,Q45、G41、H61等,整个方案已经成熟。再开案的话,我觉得大部分问题都会集中在硬件方面,软件问题不会太多。

不过,当时实际上软件还留下一个问题需要想办法解决,那就是Option ROM对UEFI的支持。在12年出货的产品中,因为时间比较紧,为了把Option ROM移植到UEFI下(联想2011年左右基本都转换为UEFI BIOS),我采用了一种比较省事的方法,用AMI的VBE来开发。主要是考虑到有问题可以去请教联想的BIOS工程师,让我这个虽然号称底层比较熟悉,但没有实操过BIOS开发的人,这是种比较保险的做法。

因此,即便我了解intel提供了UDK可以直接进行开发,我仍旧走了上述的路线。当时的开发很难,从VBE新手到开发出符合要求的UEFI driver(实际上就是个option rom),花了我三个礼拜,像打了鸡血一样不眠不休的完成了。在此要感谢当时还在Intel的Raymond,张银奎老师,帮我向intel bios部门咨询解决了显示的一个问题。以及我刚出生没多久的宝贝,因为她我才在北京待不住,用了最快的速度解决了这个项目问题,就为了早一刻回南京抱她。

所以,2013年的时候,摆在我面前的问题是,如何用intel UDK把Option ROM开发出来,一方面用在联想的合作项目上;另外一方面,把它用在公司的另外一个产品-隔离卡上。随着UEFI越来越普及,未来肯定会只支持UEFI,当时隔离卡上面的Legacy oprom会无法运行的。几年过去了,这个论断终于实现了。现在是2019年,公司就是因为当时的这个技术设计,今年3月还没过完,半个月的销量,超过了去年同期一个月的销量了。我甚是欣慰。

当时的时间比较充裕,毕竟和联想的案子已经有一个可用的软件版本了,而隔离卡这边的需求还没到时候。仿照之前学习底层开发的过程,我制定了一个小计划,从控制键盘、图形显示、鼠标显示、访问PCI端口、SMBUS通讯、USB访问、网络访问….这样的顺序开始尝试写代码。虽然因为各种原因,在完成了最初的目标(支持公司产品在UDK下的开发)后,有些工作没有继续了。但我觉得还是基本上把UEFI下的开发实现了,即便要开发其他的产品,现有的代码也能非常好的用上。

图1 robin’s Uefi 代码列表

在最早开始写博客的时,我就计划把这段历程写下来。今天终于如愿开始了,记录我挥汗编程的日日夜夜,也纪念我当时用来开发的华硕945主板的机器,跟了我那么多年,终于没有机会再使用它了。

是为开篇。

Foxdisk10-小字库显示汉字1

在以前的博客中曾经讨论过,设计的Foxdisk代码段和数据段总共只能128K。可是随便哪个汉字库轻轻松松都超过了100K,大的有近500K(我设计的时候只用了24×24以及16×16的字模),无论如何是不能包含在程序中的。唯一的办法,是将需要用的汉字字模提取出来,实现小字库的汉字显示。

当然,如果改变Foxdisk的设计方法,比如采用很久以前用过的一种技术:在硬盘的末端存一个裁剪的DOS或者linux,用OS来支持。现在遇到的存储空间不够啊、想实现的功能难以实现啊,都能解决。并且还能复用大量的DOS/Linux的软件,还是很不错的。不过,这就失去了我当时设计的初衷了,本来就是用来学习底层知识的一个小项目,用这种方法则屏蔽了大量的实现细节,所以我就放弃了。不过,在公司早期的一款产品中(卖了好几年,技术支持巨难,后来被我停掉了)是用了这种方法的,有机会也开个博客仔细探讨下。

文字显示主要包括文字的读取和显示两个步骤。西方文字显示完全可以集成在一个ROM内,但是中文字太多,通常都要用到字库。中文字库有两大类型:点阵式字库和矢量字库。

点阵式字库是通过将中文字看成一个个点组成的二维阵列来实现显示;矢量字库则通过对文字每个笔划的起始点和结束点的记录来完成文字显示。很明显,点阵字如果要放大文字将会出现明显的不平滑现象;而矢量字无论字的大小都可以保证字体圆滑。但是由于点阵字理解简单、便于编程等原因,在很多领域点阵字库成为了主流。

当前只针对点阵字库进行讨论研究,并针对研究的各字体库进行分析,阐述其在编程上的一些区别。在此基础上,提供了无字库方案的实现过程及代码。 点阵字的显示原理其实很简单。在规定大小的区域格内,如16×16、24×24等,将需要显示的字的对应格填充即可。字库内对应的格则以1表示,没有填充的以0表示。如图所示:

图1 英文字模图样

这是英文字模的图样,而中文字模的图样是这样的:

图2 中文字模的图样

图中的字模信息就存储在相应的字库中,而程序员就可以按照显示原理在显示器上以点位的方式显示字符。

每个英文字母都是由一个ASCII码组成的,而中文字是扩展ASCII码组成的。也就是说一个中文字是由左半边和右半边两部分的信息组成的。字库中的文字一般是按区位码的形式存放的,大体原理相同,但是在实际组织的时候有些细微的差别。[1]

本文所讨论的字库有HZK16(16×16汉字库)、HZK24S(24×24宋体)、HZK24H(24×24黑体)、HZK24F(24×24仿宋)、HZK24K(24×24楷体)、HZK24T(24×24汉字全角字符)。

对于24×24以上的汉字字库,比如48×48汉字字库,其存储方式与24×24 的相同,只是大小不同,简单修改就可适用,以下将作详细说明。

HZK16的中汉字存储可以用公式表示如下:

  • 区码 = 文字左半边信息(扩展ASCII码) – 0xA0
  • 位码 = 文字右半边信息 – 0xA0
  • 在字库中的地址 = [(区码-1) *94 + (位码-1)] * 32

其中1、2步减去0xA0的原因是中文字符用到的扩展ASCII码是从0xA0之后开始计算的,94的来源是字库中每个区最多存放94个中文字点阵。另外,因为HZK16是16×16字体,每个字由32个字节组成(16位x16)。[1]

24×24字体库中文字库与 16×16字体库的组织有些不同。HZK24T中已经存放了汉字的全角字符,其余的各种字体的汉字库16区之前是独立在此的。也就是说,对HZK24S、HZK24H、HZK24F和HZK24K,公式如下:

  • 区码 = 文字左半边信息(扩展ASCII码) – 0xA0
  • 位码 = 文字右半边信息 – 0xA0
  • 在字库中的地址 = [(区码-1- 15) *94 + (位码-1)] * 72

注意在第三步要减去15,前15区的字符存放在HZK24T中了。72表示24×24字库中汉字是按72字节存储的(24位x24)。

对于更大的字体,如32×32、48×48等,也是按照这样的方式存储。只是在第3步的计算中,32×32字体库中汉字占用 128字节,48×48字体库中汉字占用288字节。

汉字的点阵结构介绍完了,后面就面临着怎么提取字模以及显示的问题。实际上,在工具篇中已经有所涉及提取的问题了,下面两篇博客中详细描述。

Foxdisk09-工具篇

题外话,大概是2017年底,开始开发DTU,用来采集光伏逆变器的数据,通过GPRS发送给后端服务器。一个不大的物联网设备,针对客户的需求做了三种不同的形态。采用的是STM32F103C8T6+SIM800C的硬件架构,软件则用FreeRTOS,方便后续的扩展。2018年4月份,国家政策一变,原计划的50K的产品计划也逐渐泡汤。我写博客前,刚好看到文件夹中满满的资料,不禁有些惆怅。先定个小计划,Foxdisk的博客写完后,也针对这个项目写写历程吧,从前期找客户、定方案、打样到量产,甚至包括采购物料、包材、后期的款项等等,可以吐槽的地方太多了。

回到正题。发现之前提供的Foxdisk的下载链接没法用了,同时也发现小工具的代码没有放上去。什么时候有人需要,我再整整吧,这段时间有点忙,有点顾不上。

Foxdisk的编译过程中,有两个小工具需要使用,EHZ24.exe和ETRHZ.exe,名字有点怪,我随便取的。EHZ24.exe是用来提取汉字字模的工具,即将需要的汉字的点阵图提取出来,方便程序去打印到屏幕上。其主要功能如下:

  1. 可以针对多个源文件进行处理,提取C语言源代码中需要显示的汉字;
  2. 可指定提取的字模为楷体、黑体或者仿宋,字模为24×24点阵字;
  3. 提取出来的字模,自动生成一个.h的头文件,并在头文件中定义了字模的结构体。

源代码中HZK24K.h就是由这个工具自动生成的。 ETRHZ.exe提取的是16×16的点阵字,其功能与EHZ24.exe差不多,HZTABLE.H由其提取。理论上可以把这两个程序合成一个,只是因为这两个程序是我以前开发隔离卡产品时写的,当时就这么分开的,稍微改改就拿来用了,也没兴趣去合一了。如图,两个工具简陋的命令行帮助文档。

图1 汉字提取工具ETRHZ和EHZ24

这两个工具其实没什么可以说的,主要就是分析源文件,将程序中需要显示的汉字取出来,然后在相应的汉字库中,将对应的汉字字模提取,最后写入到生成的文件中。如图2,从生成的文件中,也能了解到其大致功能。

图2 ETRHZ提取的文件截图

这两个工具的源代码比较简单,有兴趣的可以看看,过段时间我上传到博客上。源代码不难看懂,主要是要了解汉字提取的原理。

为什么要提取汉字?那是因为汉字库比较大(一般200K左右,大的400多K,Foxdisk才100多K,没法放下),我们是在无操作系统的情况下工作,程序越小越好。具体的汉字显示原理,我在接下来的几篇中记录。

Foxdisk08-如何调试2

有段时间一直沉迷于裸机上跑程序,学习怎么让一个操作系统跑起来。使用过VirtualPC(现在被微软收购了),VMware,VirtualBOX,Bochs,最终还是觉得Bochs比较好用。它的最大好处是集合了完整的调试环境,对指令是软件解析的,速度虽然慢,但对程序员来说,一步步看代码是如何执行的,非常契合需求。

Bochs的文档非常的完整,使用起来也比较方便。我只针对如何调试Foxdisk,以及如何下断点进行记录,其他的可以查其自带的帮助文档。我使用的是Bochs2.2.6,帮助文档在Bochs-2.2.6\docs目录下,目前已经到了版本2.6.9,估计也差不多。

第一步是制作磁盘镜像。Bochs本身提供了镜像制作工具bximage,使用起来也比较简单。当然,也可以使用其他工具制作,比如windows下的winimage或者Linux下直接用dd命令。以制作一个4G虚拟硬盘文件为例,命令如下:

..\bximage.exe -hd -size=4096 -mode=growing -q myhd.img

Bochs的菜单栏中(我使用的是windows)提供了Disk Image Creation Tool的快捷菜单,其实就是bximage。

具体还可以参考帮助文档中Bochs User Manual-8.1 How to make a simple disk image。

第二步是编辑bochsrc.bxrc文件,指定需要启动的设备。可以启动虚拟软盘、虚拟光盘或者虚拟硬盘。找个例子看一下,修改为自己想启动的设备即可。比如虚拟硬盘,boot: c即可启动。需要提醒的是,在Foxdisk中,为了尽可能的模拟真实硬件,磁盘的参数要注意修改,一般spt=63,heads=16即可。很早以前的版本中,我的代码中没有主要到这些参数是可变的,虚拟机调试就出问题。现在的版本中,硬盘驱动的代码已经修补了这个问题,如果不去调试硬盘驱动,不必理会这些参数。双击bochsrc.bxrc即可启动,如图1,启动了一个DOS环境下的软件,当年和联想开发的一款安全计算机的界面。

图1 Bochs调试DOS程序

另外,还有一个问题,如何把需要使用的文件拷贝到虚拟硬盘中呢?我的方法比较简单,我做了一个虚拟软盘,使用winimage将需要的文件拷贝到虚拟软盘中。启动虚拟硬盘的时候,虚拟软盘作为一个挂接设备存在(注意修改bochsrc.bxrc,比如 floppya: 1_44=LBDEBUG.IMG, status=inserted  ),从软盘中将文件拷贝过去即可。

一切准备就绪后,就可调试了。最后一步,启动bochsdbg.exe,命令行如下:

…\Bochs-2.2.6\bochsdbg.exe -q -f bochsrc.bxrc。断在了bios最早的位置f000:fff0上,这时就可以进入调试了。如图2所示。

图2  bochsdbg界面

如何在bochs中对Foxdisk进行调试呢?最常用的是定位在mbr区,跟踪Foxdisk的运行。即利用bochs可以对内存下断点,定位想调试的位置,进行跟踪。典型的代码运行位置,一个是mbr的0x7c00,以及Foxdisk的Code_Seg(也即0x7000处)。以mbr定位为例:

<bochs:1> lb 0x7c00     //下断点

<bochs:2>c            //继续执行直到遇到断点

参考Foxdisk的代码,对照跟踪,很容易理解系统的运作原理。

总觉得bochs的调试工具和linux下gdb很类似,为便于查询,将常用的一些命令记录下来,详细的仍可参考其官方文档。

执行控制命令
c 继续执行,遇到断点将停止
step [count] 执行count条指令, 默认为1条
s [count] step的缩写
Ctrl-C 停止执行,返回命令行
Ctrl-D 执行完所有命令后,退出
q(quit) 退出调试器

设置断点

vb(vbreak) seg:off 在指定的虚拟地址(段+偏移)设置断点,在保护模式下也可以使用
lb(lbreak) addr 在一个线性地址设置断点
pb(pbreak) [*] addr 在一个物理地址设置断点
info break 显示所有断点状态
d(delete) n 删除一个断点

查看内存

x /nuf addr 查看一个线性地址的内存
xp /nuf addr 查看一个物理地址的内存
    n 显示多少个单位的内存
    u 内存单位大小,可以是
       b: 字节;  h: 字(2个字节);  w: 双字(4个字节);  g : 4字(8字节)
f 打印格式,可以是
    x: 16进制格式打印;  d: 10进制格式打印;  u: 无符号10进制格式打印;
o: 8进制格式打印;   t: 2进制格式打印

寄存器操作

Info registers  列举CPU整型寄存器内容
dump_cpu    查看所有与CPU相关的寄存器状态
where        打印当前call stack
set $reg=val   改变寄存器的内容,如set $edx=25

最奇怪的是,在安装Foxdisk的时候,颜色会不对,到现在我也没搞清楚怎么回事。

图3 Bochs安装Foxdisk-变色了