Memory Manager internals¶
How BC-250 Memory Manager reads and writes the VRAM split in CMOS, and the board dumps the layout was checked against. The user's side is in its chapter.
Where the split is stored¶
The firmware (AMI Aptio, AMD's ABL for the memory setup) keeps the memory configuration in CMOS bank 2: the
battery-backed RAM behind I/O ports 0x72 (index) and 0x73 (data). Bank 1 (0x70/0x71) is the RTC and the
usual CMOS settings; the kernel's cmos_nvram0 device only covers bank 1, so bank 2 is reached through
/dev/port, where file offset N is I/O port N.
The memory block is 28 bytes at 0x90-0xAB, little-endian:
| Offset | Size | Field | Allowed range |
|---|---|---|---|
| 0x90 | 4 | Signature | see below |
| 0x94 | 2 | Checksum: 16-bit sum of the bytes 0x96-0xAB | |
| 0x96 | 2 | ClockSpeed (MHz) | 450-1750 |
| 0x98 | 1 | tCL | 8-33 |
| 0x99 | 1 | tRAS | 21-58 |
| 0x9A | 1 | tRCDRD | 8-27 |
| 0x9B | 1 | tRCDWR | 8-27 |
| 0x9C | 1 | tRCAb | 40-90 |
| 0x9D | 1 | tRCPb | 0-11 |
| 0x9E | 1 | tRPAb | 8-27 |
| 0x9F | 1 | tRPPb | 0-11 |
| 0xA0 | 1 | tRRDS | 4-12 |
| 0xA1 | 1 | tRRDL | 4-12 |
| 0xA2 | 1 | tRTP | 0-14 |
| 0xA3 | 1 | tFAW | 4-34 |
| 0xA4 | 2 | tREF | any |
| 0xA6 | 2 | RFCPb | any |
| 0xA8 | 2 | tRFC | any |
| 0xAA | 2 | UMA_SIZE: the split in MB, in 16 MB steps | 256-16383 |
Signatures:
| Value | Bytes | Name | Meaning |
|---|---|---|---|
| 0x4C424124 | 24 41 42 4C ("$ABL") |
ABL | written by the firmware |
| 0x42435041 | 41 50 43 42 |
LINUX_TOOL | written by a Linux tool (bc250_memcfg, this app) |
| 0x42534D43 | CMOS_BAD | error codes the firmware leaves after a failed boot | |
| 0x46544457 | WATCH_DOG_TIMER_FIRED | ||
| 0x454B4843 | CHECKSUM_ERROR | ||
| 0x45474953 | SIGNATURE_ERROR |
The layout, the ranges and the signatures come from fanoush/bc250_memcfg (MIT), which credits an earlier tool by ethkey.
The layout on two boards¶
Bank 2 was read on two BC-250 boards (BIOS P5.00 05/03/2022, kernel lockdown off, both with a 6 GB split). The reads only set the index port and read the data port.
The memory block is byte for byte the same on both:
090 24 41 42 4C 10 04 D6 06 18 34 1B 13 4E 00 1A 00
0A0 08 08 02 20 F7 26 D2 00 18 01 00 18
| Field | Board 1 | Board 2 |
|---|---|---|
| Signature | 0x4C424124 (ABL) | 0x4C424124 (ABL) |
| Checksum stored / computed | 0x0410 / 0x0410 | 0x0410 / 0x0410 |
| ClockSpeed | 1750 MHz | 1750 MHz |
| tCL, tRAS, tRCDRD, tRCDWR, tRCAb | 24, 52, 27, 19, 78 | 24, 52, 27, 19, 78 |
| tRCPb, tRPAb, tRPPb | 0, 26, 0 | 0, 26, 0 |
| tRRDS, tRRDL, tRTP, tFAW | 8, 8, 2, 32 | 8, 8, 2, 32 |
| tREF, RFCPb, tRFC | 9975, 210, 280 | 9975, 210, 280 |
| UMA_SIZE | 6144 MB | 6144 MB |
mem_info_vram_total |
6442450944 (6 GiB) | 6442450944 (6 GiB) |
Every timing is inside its range, and UMA_SIZE matches the split the kernel got.
The rest of bank 2:
- 0x00-0x0F and 0x40-0x46 are the same on both boards (
FF 00 00 00 00 00 00 00 98 42 EE 7D FF 30 3F 00and00 00 01 02 56 90 F0): the firmware keeps something else there. - 0x47-0x7F is zero on both.
- 0x10-0x3F, 0x80-0x8F and 0xAC-0xFF are mostly
FFwith single bits cleared in different places on each board: unused memory that was never written.
So nothing outside 0x90-0xAB belongs to the memory configuration, and the app never writes there. The tests use
both reads as fixtures (bc250-bazzite/apps/vram/tests/dumps.py).
Reading and writing¶
The GUI runs as the user. Everything that touches CMOS is in vram_manager/cmos.py, which pkexec runs as
root with the system Python in isolated mode:
pkexec /usr/bin/python3 -I .../vram_manager/cmos.py --read
pkexec /usr/bin/python3 -I .../vram_manager/cmos.py --set-uma 6144
-I keeps the user's environment and the current folder out of the root process; the helper only needs the
standard library. It prints one JSON object: {"ok": true, ...} or {"ok": false, "error": "..."}.
Before it opens /dev/port, the helper checks that the DMI board name is AMD BC-250 and that kernel lockdown is
off.
--set-uma MB:
- Checks the split: a multiple of 16 MB, from 512 to 10240 MB. Above 10 GB the system keeps less than 6 GB.
- Reads the block and checks that it can be trusted: signature ABL or LINUX_TOOL, a matching checksum, every timing in range, UMA_SIZE 256-16383. If not, it writes nothing. bc250_memcfg writes back whatever it read, so a damaged block would reach the firmware with its wrong timings.
- Builds the new block: the same timings, the new UMA_SIZE, the LINUX_TOOL signature and a new checksum.
- Writes the whole block from 0x90 upwards, as bc250_memcfg does, so the bytes the firmware finds are the same as with that tool.
- Reads the block back and compares all 28 bytes.
It answers with the block before and after; the GUI saves both in backups/cmos-<time>.json. --read answers
with the decoded block and all 256 bytes of bank 2, which the GUI shows and saves as backups/cmos-dump-<time>.txt.
After a reboot¶
The firmware reads the block at boot. The GUI keeps the split it wrote with the boot ID it wrote it in
(/proc/sys/kernel/random/boot_id). At the next boot it compares mem_info_vram_total with that split and says
whether the kernel got it. Undo writes back the UMA_SIZE of the block before the last change.
The figures without root¶
vram_manager/system.py reads, without root:
| Figure | Source |
|---|---|
| Split | /sys/class/drm/card*/device/mem_info_vram_total |
| GTT | /sys/class/drm/card*/device/mem_info_gtt_total |
| System RAM | MemTotal in /proc/meminfo |
| TTM limit | /sys/module/ttm/parameters/pages_limit (4 KiB pages), and whether it is on /proc/cmdline |
| Swap | /proc/swaps (zram and disk swap) and /sys/module/zswap/parameters/enabled, as Bazzite Test's test 14 |
| Swappiness | /proc/sys/vm/swappiness |
amdgpu sizes GTT from the TTM limit and caps it at the RAM on this APU (amdgpu_ttm_init: "Capping GTT to …M to
not exceed available system memory"). When the split is smaller than GTT, amdgpu also prefers GTT for the GPU's
allocations (apu_prefer_gtt).