VOGONS


First post, by clb

User metadata
Rank Oldbie
Rank
Oldbie

Hey all knowledged vogons veterans,

I am trying to read and write into a MMIO space of a PCI device from DOS.

Searching the web, I do find sources of how to read and write the PCI configuration space registers, e.g. https://github.com/TimmermanV/TweakPCI , but that is not what I'm looking to do.

Instead, what I would like to achieve is to write into a PCI device's memory mapped I/O space.

To my understanding, this I/O space is advertised by the PCI device's configuration space registers to have a physical starting address, defined by one or more of Base Address Registers:

The attachment Pci-config-space.svg.png is no longer available

I am already able to examine this configuration space to read the Base Address Register that I am interested in. In this case, it is physical address "0x6001". And I would like to access a byte in offset + 0xC0 to this base address, i.e. the physical address 0x60C1, to read and write the byte value located there.

My understanding is that this is something that would be easiest done via DJGPP in Protected Mode, it would somehow consist of mmap()ing this physical address into the virtual address space of the calling process, and then I would have a base pointer to that address 0x6001 and could do the interesting byte reads and writes. I tried to search for examples of this, but haven't found one so far.

Does anyone know if there might exist code examples of how to achieve this? Or possibly even ready-made programs/utilities? I could either write my own DJGPP program, or use an existing utility, doesn't matter in this case.

Also, if there was a way to shoehorn this kind of task into a real-mode program, that would simplify some things in my setup, since the rest of my utility is currently written in Borland Turbo C++ 3 real-mode (although I can imagine that could be tough, so not holding too much on this).

Thanks for any help!

Reply 1 of 13, by bakemono

User metadata
Rank Oldbie
Rank
Oldbie

How did you come up with this 0x6001 number? Seems like an unlikely address for a PCI memory region, considering it overlaps with conventional RAM.

PCI stuff usually gets mapped into the adapter area or after 2GB. Using 'unreal mode' might be an easier way to access high addresses, rather than having to go through a DOS extender.

GBAJAM 2024 submission on itch: https://90soft90.itch.io/wreckage

Reply 2 of 13, by jmarsh

User metadata
Rank Oldbie
Rank
Oldbie

bit 0 defines if the BAR is for mapped memory (value = 0) or a mapped I/O port (value = 1). So what you seem to have there is an IO port range at 0x6000. Try accessing it using IN/OUT.

Reply 3 of 13, by clb

User metadata
Rank Oldbie
Rank
Oldbie
jmarsh wrote on 2024-08-31, 13:15:

bit 0 defines if the BAR is for mapped memory (value = 0) or a mapped I/O port (value = 1). So what you seem to have there is an IO port range at 0x6000. Try accessing it using IN/OUT.

Oh indeed, that was the case!

I was able to access the data in the I/O port with inport() and outport().

However, now I'm met with a next issue. It seems that the PCI device in question (which is a Diamond Multimedia Rendition Verite V2200 card) either decodes 8-bit and 16-bit port writes incorrectly.

I find that

outport(0x60C2, inport(0x60C2));

"works" in the sense that it doesn't do anything (as one might expect from a well-defined hardware implementation of registers), but

outportb(0x60C2, inportb(0x60C2));

causes the high byte at 0x60C3 to be cleared.

Well, no big deal, I can just not do byte reads and writes.

But then I realize that 16-bit aligned reads and writes exhibit from the same problem. That is, if I do

outport(0x60C2, inport(0x60C2));

it actually clears the I/O data at 0x60C0 and 0x60C1. And if I do

outport(0x60C0, inport(0x60C0));

it seems to behave like it clears the I/O register data at 0x60C2 and 0x60C3. I infer that I need to do a single atomic 32-bit port write. Any chance there might be a way to hack that kind of thing into Borland C++ 3.0? I recall there were some asm prefix hacks to do 32-bit addressing or similar, or maybe some inline asm data bytes mechanism?

Reply 4 of 13, by jmarsh

User metadata
Rank Oldbie
Rank
Oldbie

I'm not familiar with that compiler so I can't say. All I know is that adding an operand prefix (byte 0x66) to the IN/OUT opcode will make it perform 32-bit access (using EAX instead of just AX) if the current code segment size is 16 bits.

Reply 5 of 13, by clb

User metadata
Rank Oldbie
Rank
Oldbie

Thanks jmarsh, you're so awesomely helpful, as always!

Reply 6 of 13, by clb

User metadata
Rank Oldbie
Rank
Oldbie

I wrote up a gist code example to do 32-bit I/O port accesses from Borland Turbo C++: https://gist.github.com/juj/84f6c680939d1da7686db890b69954a0

Even though I ended up not needing to do it, to answer my original question of "how to write to memory-mapped address space from DOS", I believe the scenario would be two-fold:

1) if the memory-mapped address space resides below the 1MB region, then one should be able to in Borland Turbo C++ just MK_FP(segment, offset) the 32-bit linear address given from PCI configuration register space and access it directly.
2) if the memory-mapped address space is above the 1MB region, then using protected mode compiler makes life easier. In DJGPP, I believe that this page https://www.delorie.com/djgpp/v2faq/faq18_7.html shows how to map the physical PCI memory BAR to the calling processes address space. Glad I didn't need to go down that route. Today at least.. maybe I'll run into this issue another day.

Reply 7 of 13, by jakethompson1

User metadata
Rank Oldbie
Rank
Oldbie

Nice. If you did need above-1MB memory access, I think a DOS extender really is the way to go, otherwise (unreal mode?), you'd produce a program that doesn't run under EMM386.
Any late DOS SVGA game that supported linear framebuffer would have to deal with this.

Reply 8 of 13, by sndtst

User metadata
Rank Newbie
Rank
Newbie

Over Thanksgiving weekend I dusted off Borland's Turbo C++ and took a stab at writing a program that can poke around Voodoo2's RAM chips. Inspired by the Witchery project but running on DOS directly. I managed to get up to the point where I can see how many Voodoo2 cards are installed and to get the BAR0 addresses for each ( checking with the output of MOJO to see that they match) . Where I am currently stuck is getting any attempt to actually write to this address to return anything other than `FFFFFFFF`.

Anyone who understands this better than I do have any ideas what I can try next?

https://gist.github.com/jasonsperske/35606048 … bc258f27e391957

A Sound Test for as many games as I can find at SNDTST.com

Reply 9 of 13, by sndtst

User metadata
Rank Newbie
Rank
Newbie

I dug deeper and found a few obviously wrong things, my FBI_INIT* address were all off (fixed from values) found in the voodoo2 spec PDF. I also found some code in the Base-Metal video (https://www.youtube.com/live/LDT6KlfOG2k?si=1 … 1dpzDPdk&t=3748) that shows a method of Blanking the Voodoo by writing the FBI_INIT0, 1 and 2 with some bitwise operators. I incorporated these changes into https://github.com/jasonsperske/witcheroo but I still feel like I am missing some obvious thing that is preventing my writes and reads to the Frame Buffer.

A Sound Test for as many games as I can find at SNDTST.com

Reply 10 of 13, by Disruptor

User metadata
Rank Oldbie
Rank
Oldbie

You may find some useful code in mkarcher's dostools: https://github.com/karcherm/dostools

Reply 11 of 13, by Vort

User metadata
Rank Newbie
Rank
Newbie
sndtst wrote on 2024-12-03, 07:41:

... but I still feel like I am missing some obvious thing that is preventing my writes and reads to the Frame Buffer.

Looks like you are confusing I/O space and memory space.
You figured out how to make 32 bit accesses for I/O space (outportl and inportl), but for linear frame buffer you need 32 bit access to memory space.

I like the idea of using DOS for poking video cards, using Linux for such simple task looks like overcomplication.
I'm trying to figure out how to make similar thing right now, and it looks like links to djgpp/dpmi stuff above point to the right direction.

Reply 12 of 13, by megatron-uk

User metadata
Rank l33t
Rank
l33t

I wrote some protected mode C code a few years ago to write to the linear framebuffer on the NEC PC-98 series of computers, whilst the hardware is different to IBM PC compatibles, the code is more or less identical.

Have a look here: https://www.target-earth.net/wiki/doku.php?id … r_to_dos_memory

It's targetting GCC/DJGPP/DPMI, so should be pretty much drag and drop for DOS/IBM PC - just changing a few defines for framebuffer location. You *can* then poke pixels directly in the VGA framebuffer, but it's probably best to doublebuffer between system ram and vram and pageflip when needed.

Of course you're going to have to find the linear framebuffer address first... and I guess that's a VGA-chip specific lookup; whereas on the PC-9821 it always lives in one of two locations (15-16MB, or up at 4095MB),

My collection database and technical wiki:
https://www.target-earth.net

Reply 13 of 13, by Vort

User metadata
Rank Newbie
Rank
Newbie

My video card, GeForce 3 Ti 500, has a straps register, located in MMIO space.
Card allows to override it, and this is what video BIOS is doing during card initialization.
I was able to read modified value of this register with AIDA64, but I wanted to know its original value.
To do this, register should be reset by writing zero to it and be read afterwards.

By combining code from @sndtst, @megatron-uk and official DJGPP documentation, I was able to write this simple program:

#include <pc.h>
#include <dpmi.h>
#include <stdio.h>
#include <stdint.h>
#include <sys/farptr.h>

int selector;

uint8_t bus;
uint8_t dev;
uint8_t func = 0x00;

uint32_t pci_config_read32(uint8_t offset)
{
uint32_t address = 0x80000000 | bus << 16 | dev << 11 | func << 8 | offset;
outportl(0x0cf8, address);
return inportl(0x0cfc);
}

void pci_config_write32(uint8_t offset, uint32_t value)
{
uint32_t address = 0x80000000 | bus << 16 | dev << 11 | func << 8 | offset;
outportl(0x0cf8, address);
outportl(0x0cfc, value);
}

bool find_video_card(uint32_t pci_id)
{
for (bus = 0; bus < 255; bus++)
for (dev = 0; dev < 32; dev++)
if (pci_config_read32(0x00) == pci_id)
return true;
return false;
}

void initialize_mapping(uint32_t base, uint32_t size)
{
__dpmi_meminfo mi;
mi.address = base;
mi.size = size;
__dpmi_physical_address_mapping(&mi);

selector = __dpmi_allocate_ldt_descriptors(1);
__dpmi_set_segment_base_address(selector, mi.address);
__dpmi_set_segment_limit(selector, mi.size - 1);
}

uint32_t register_read32(uint32_t address)
{
return _farpeekl(selector, address);
}

void register_write32(uint32_t address, uint32_t value)
{
return _farpokel(selector, address, value);
}

int main()
{
printf("Searching for video card... ");
Show last 26 lines
  if (find_video_card(0x020210de))
printf("Found at %02d:%02d.%d\n", bus, dev, func);
else
{
printf("Not found, exiting\n");
return -1;
}

uint32_t bar0 = pci_config_read32(0x10);
uint32_t bar0_base = bar0 & 0xfffffff0;
pci_config_write32(0x10, 0xffffffff);
uint32_t bar0_size = ~(pci_config_read32(0x10) & 0xfffffff0) + 1;
pci_config_write32(0x10, bar0);

printf("BAR0 base: 0x%08x, size:0x%08x\n", bar0_base, bar0_size);

initialize_mapping(bar0_base, bar0_size);

printf("Card id: 0x%08x\n", register_read32(0x00000000));
printf("Overridden straps: 0x%08x\n", register_read32(0x00101000));
register_write32(0x00101000, 0x00000000);
printf("Original straps: 0x%08x\n", register_read32(0x00101000));

return 0;
}

Running it with real hardware revealed that BIOS didn't actually changed this register except for enabling override bit. I wonder why it was needed.

Searching for video card... Found at 01:00.0
BAR0 base: 0xd4000000, size:0x01000000
Card id: 0x020200a5
Overridden straps: 0xfff86c6b
Original straps: 0x7ff86c6b