Successful (?) Read from the PCI Bus Configuration

Trying to access PCI bus configuration on Lenovo T60 laptop from BYOK Forth

It looks like if I’m going to get the AHCI SATA access I want, then I’m going to have to learn how to interact with the PCI bus. And I wanted to interact with the bus from Forth itself, rather than having to debug C code. First baby step: to see if I could pull any information from a PCI bus configuration register.

This seems to be fairly simple in principle: configuration access is port-based, with a 32 bit address memory location (CONFIG ADDRESS), and a 32 bit data memory location (CONFIG DATA). Put the correct request information in CA, and pull the data from CD.

The tricky part: since this is I/O memory, you have to access it using the Pentium INL and OUTL instructions. BYOK had implemented INB and OUTB, but these are not quite the instructions we wanted, and also they weren’t mapped to Forth words. But thankfully nice examples are available in a gcc library. So, I expanded BYOK’s io.h to…

static inline unsigned char inportb (uint16_t port)
    unsigned char rv;
    __asm__ __volatile__ ("inb %1, %0" : "=a" (rv) : "dN" (port));
    return rv;

static inline void outportb (uint16_t port, unsigned char data)
    __asm__ __volatile__ ("outb %1, %0" : : "dN" (port), "a" (data));

/* inport, outportl added by Christopher Howard 2020 */

static inline unsigned int inportl (unsigned short int port)
    unsigned int rv;
    __asm__ __volatile__ ("inl %w1, %0" : "=a" (rv) : "dN" (port));
    return rv;

static inline void outportl (unsigned int value, unsigned short int port)
    __asm__ __volatile__ ("outl %0, %w1" : : "a" (value), "dN" (port));

So, then I need to map this to Forth words, and I wasn’t sure where to do that. So, I added them to BYOK’s primitive word list. I wasn’t sure if the author of BYOK would have approved of that particular approach, but better to ask forgiveness later. Inserted the following into io.c:

state_t __INL(context_t *ctx)
    unsigned int port;
    if (popnum(ctx->ds, &port))
        port = inportl (port);
        pushnum(ctx->ds, port);
        return OK;
        return stack_underflow(ctx);

state_t __OUTL(context_t *ctx)
    unsigned short int port;
    unsigned int value;
    if (popnum(ctx->ds, &port) && popnum(ctx->ds, &value))
        outportl (value, port);
        return OK;
        return stack_underflow(ctx);

void init_io_words(context_t *ctx)
    hashtable_t *htbl = ctx->exe_tok;
    add_primitive(htbl, ".",      __DOT,    "( n -- )", "convert signed number n to string of digits, and output.");
    add_primitive(htbl, "INL",   __INL,   "( port -- )", "x86 32-bit read from i/o port");
    add_primitive(htbl, "OUTL",   __OUTL,   "( value port -- )", "x86 32-bit output to i/o port");

To my surprise, the code compiled. Now, I needed a Forth function to figure out what is the correct data to put into CA. This is the part I currently am manually typing into the laptop after boot:


cf8 constant CA
cfc constant CD

: pci-cfg-word ( bus slot func offset -- u )
    swap fc and or
    swap 8 lshift or
    swap 11 lshift or
    swap 16 lshift or ;


In the laptop terminal I entered the following to get the (hopefully) correct address for CA on the stack, which is intended to pull the vendor code from the PCI bus:

0 0 0 0 pci-cfg-word

Then load and read:

ca outl
cd inl

Result in hexadecimal:

hex .s

Is that information, or garbage? I found the PCI ID Repository on the Internet. If I make the reasonable assumption that the vendor code is in the 16 LSBs, that gives vendor code 0x8086, which matches to “Intel Corporation”. That sounds plausible!

Intel Corporation listing in The PCI ID Repository

Fun stuff!

cpupower utility

I think this command-line utility is nifty:

christopher@nightshade ~ [env]$ guix show cpupower
name: cpupower
version: 5.8.12
outputs: out
systems: x86_64-linux i686-linux
dependencies: gettext-minimal@0.20.1 pciutils@3.6.4
location: gnu/packages/linux.scm:5458:2
license: GPL 2
synopsis: CPU frequency and voltage scaling tools for Linux  
description: cpupower is a set of user-space tools that use the cpufreq feature of the Linux kernel to retrieve and control processor features related to
+ power saving, such as frequency and voltage scaling.

This command shows you what cpu governor you are using, and the current cpu frequency:

christopher@nightshade ~ [env]$ sudo cpupower frequency-info
analyzing CPU 0:
  driver: acpi-cpufreq
  CPUs which run at the same hardware frequency: 0
  CPUs which need to have their frequency coordinated by software: 0
  maximum transition latency: 4.0 us
  hardware limits: 800 MHz - 3.30 GHz
  available frequency steps:  3.30 GHz, 2.60 GHz, 2.10 GHz, 800 MHz
  available cpufreq governors: conservative ondemand userspace powersave performance schedutil
  current policy: frequency should be within 800 MHz and 3.30 GHz.
                  The governor "userspace" may decide which speed to use
                  within this range.
  current CPU frequency: 800 MHz (asserted by call to hardware)
  boost state support:
    Supported: no
    Active: no
    Boost States: 0
    Total States: 4
    Pstate-P0:  3300MHz
    Pstate-P1:  2600MHz
    Pstate-P2:  2100MHz
    Pstate-P3:  800MHz

And this sets the cpu frequency:

christopher@nightshade ~ [env]$ sudo cpupower frequency-set -f 800Mhz
Setting cpu: 0
Setting cpu: 1
Setting cpu: 2

A lower frequency is better when you aren’t busy using the computer, as that saves electricity and generates less heat. Of course, you might want to explorer the different governors as well.

BYOK: Bare Metal Forth

BYOK forth running on i386 qemu VM
BYOK Forth running on bare metal x86(-64) desktop computer

Forth is a language that was designed to be run on bare-metal – without an underlying operating system. Some interesting quotes from Chuck Moore:

The operating system is another concept that is curious. Operating systems are dauntingly complex and totally unnecessary. It’s a brilliant thing that Bill Gates has done in selling the world on the notion of operating systems. It’s probably the greatest con game the world has ever seen.

Lisp did not address I/O. In fact, C did not address I/O and because it didn’t, it needed an operating system. Forth addressed I/O from the very beginning. I don’t believe in the most common denominator. I think that if you go to a new machine, the only reason it’s a new machine is because it’s different in some way and you want to take advantage of those differences. So, you want to be there at the input-output level so you can do that.


So, I was certainly interested in the idea of Forth running on bare-metal. What I found quickly was byok.

I was able to get it compiled fairly easily using the pre-built toolchain provided by the author. However, I had to delete two lines in the kernel directory Makefile:

diff --git a/kernel/Makefile b/kernel/Makefile
index b54cfb0..4ed0dc3 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -49,9 +49,7 @@ $(CRTN_OBJ) \
 $(CRTI_OBJ) \
 $(OBJS) \
 $(CRTN_OBJ) \
 all: byok.kernel

The system boots up using Grub. The words display as expected.

words display in BYOK

I was able to create variables and store/pull data from them. However, a slight oddity is that the @ symbol and the ” symbol are swapped around on the keyboard, which had me confused for about 10 minutes. But I was fine after figuring that out.

working with variables in BYOK

The system comes with a nice block editor, for saving a program to block memory, though I think actual disk I/O is not coded yet.

block editor in BYOK
loading code from block memory

And the dump word is available:

hex dump which appears after running v cell dump

I’m definitely interested in playing around with this some more, and exploring what x86 architecture functionality is accessible with memory reads and writes.