This is a demonstration of controlling the stock fan. The demo is based on the DCB details pasted on the GPU Passthrough, and EDID access post.
Warning: Improperly programming a GPU, or a display, may cause harm to the devices.
The DCB: GPIO Assignment Table
The offset to the GPIO Assignment table itself lies at offset 0xa
from the start of the DCB.
The DCB starts at offset 0x8dd6
within my BIOS ROM.
$ hexdump -C nv.bios.rom
. . .
00008dd0 2f 21 2c 00 71 00 30 19 0a 08 3f 8e cb bd dc 4e |/!,.q.0...?....N|
00008de0 78 8e 6c 8e d6 8e e2 8e f0 8e 05 8f 31 51 59 00 |x.l.........1QY.|
. . .
The offset to the GPIO Assignment table is thus 0x8e78
.
$ hexdump -C nv.bios.rom
. . .
00008e70 0f 0f 0f 0f 0f 0f 0f 0f 31 06 10 02 9e 8e 00 71 |........1......q|
00008e80 e1 70 e0 07 e0 07 e0 07 85 20 a6 20 e0 07 e0 07 |.p....... . ....|
00008e90 29 89 e0 07 e0 07 e0 07 e0 07 e0 07 e0 07 30 04 |).............0.|
00008ea0 00 02 a6 8e cd 8e 30 07 10 02 00 00 00 00 00 00 |......0.........|
. . .
The table begins with a header:
Address | Value | Comment |
---|---|---|
0x8e78 |
0x31 |
Version of the GPIO Assignment table. |
0x8e79 |
0x6 |
Size of the Header, in bytes. The table entries immediately follow this header. |
0x8e7a |
0x10 |
Number of entries inside the table. |
0x8e7b |
0x2 |
Size of each entry, in bytes. |
0x8e7c |
0x8e9e |
Offset to the External GPIO Assignment Master table. It happens to contain no entries. See here for parsing details. |
Each entry is of 16 bits in size, with the following layout:
[15]: PWM
[14]: ON Dir
[13]: ON Data
[12]: OFF Dir
[11]: OFF Data
[10:5]: Function
[4:0]: Pin#
The description of each of the fields is found
here. The same page also lists the meanings of the values found
in the Function
field.
The 16 entries of the GPIO Assignment table are:
Address | Value | PWM | ON Dir | ON Data | OFF Dir | OFF Data | Function | Pin# | Comment |
---|---|---|---|---|---|---|---|---|---|
0x8e7e |
0x7100 |
0 |
1 |
1 |
1 |
0 |
8 |
0 |
See the table below. |
0x8e80 |
0x70e1 |
0 |
1 |
1 |
1 |
0 |
7 |
1 |
See the table below. |
0x8e82 |
0x7e0 |
0 |
0 |
0 |
0 |
0 |
0x3f |
0 |
Unused. |
0x8e84 |
0x7e0 |
0 |
0 |
0 |
0 |
0 |
0x3f |
0 |
Unused. |
0x8e86 |
0x7e0 |
0 |
0 |
0 |
0 |
0 |
0x3f |
0 |
Unused. |
0x8e88 |
0x2085 |
0 |
0 |
1 |
0 |
0 |
4 |
5 |
See the table below. |
0x8e8a |
0x20a6 |
0 |
0 |
1 |
0 |
0 |
5 |
6 |
See the table below. |
0x8e8c |
0x7e0 |
0 |
0 |
0 |
0 |
0 |
0x3f |
0 |
Unused. |
0x8e8e |
0x7e0 |
0 |
0 |
0 |
0 |
0 |
0x3f |
0 |
Unused. |
0x8e90 |
0x8929 |
1 |
0 |
0 |
0 |
1 |
9 |
9 |
See the table below. |
0x8e92 |
0x7e0 |
0 |
0 |
0 |
0 |
0 |
0x3f |
0 |
Unused. |
0x8e94 |
0x7e0 |
0 |
0 |
0 |
0 |
0 |
0x3f |
0 |
Unused. |
0x8e96 |
0x7e0 |
0 |
0 |
0 |
0 |
0 |
0x3f |
0 |
Unused. |
0x8e98 |
0x7e0 |
0 |
0 |
0 |
0 |
0 |
0x3f |
0 |
Unused. |
0x8e9a |
0x7e0 |
0 |
0 |
0 |
0 |
0 |
0x3f |
0 |
Unused. |
0x8e9c |
0x7e0 |
0 |
0 |
0 |
0 |
0 |
0x3f |
0 |
Unused. |
The entries that are linked to a valid function are:
Index | Value | PWM | ON Dir | ON Data | OFF Dir | OFF Data | Function | Pin# | Comment |
---|---|---|---|---|---|---|---|---|---|
0 |
0x7100 |
0 |
1 |
1 |
1 |
0 |
8 |
0 |
Hotplug B signal. |
1 |
0x70e1 |
0 |
1 |
1 |
1 |
0 |
7 |
1 |
Hotplug A signal. |
5 |
0x2085 |
0 |
0 |
1 |
0 |
0 |
4 |
5 |
Voltage Select (VID) Bit 0. |
6 |
0x20a6 |
0 |
0 |
1 |
0 |
0 |
5 |
6 |
Voltage Select (VID) Bit 1. |
9 |
0x8929 |
1 |
0 |
0 |
0 |
1 |
9 |
9 |
Fan Control. |
The speed of the fan is controlled by PWM signals.
The duty cycle of a PWM wave is the fraction of its period during which the signal is high; the fan is supposed to run for that fraction of the period. But notice (ON/OFF Data) that the polarity of the corresponding GPIO pin is reversed; the fan runs for that fraction of period when the signal is low. That is, the signal is treated as active low. Thus, to set a duty cycle of 31%, one must actually program the cycle value as 100 - 31 = 69%.
The values to feed into the controller registers depend on PWM divider value.
It is found by reading the Performance Table of the
BIOS Information Table Specification. See the function nvbios_perf_fan_parse
,
here. The value on my card happens to be 0x2b4 = 692
.
To set the duty cycle to 31%, the value that needs to be programmed is
692 - (31 * 692 + 99) / 100 = 477 = 0x1dd
, or 692 - ceil(0.31 * 692) = 477
,
or floor(0.69 * 692) = 477
.
To establish the duty cycle, set these values (the PWM divider and the duty
cycle), as shown below, and then initiate an Enable control signal to activate
the new duty cycle.
The addresses are relative to the BAR0
base address, and the assumption is
that the fan control is at GPIO pin #9.
// Set the values.
[0x15f8] = PWM Divider = 0x2b4
// Prepare the duty cycle field to receive the new setting.
t = [0x15f4]
t &= ~0x7fffffff
t |= 0x1dd
[0x15f4] = t
// Enable.
[0x15f4] |= 0x80000000
At boot, the PWM Divider register contains 0x2b4
, and the Duty Cycle register
contains 0x1dd
, i.e. 31% duty cycle.
The functions nvkm_therm_fan_set_defaults
and nvkm_fan_update
, both found
here, show how to gradually increase or decrease the fan speed over a
slow gradient.
The PWM divider value is 692. Assuming that PWM’s input clock is the 27MHz crystal found on the card, the divider value generates PWM waves at a frequency of 27000/692 ~ 39KHz. That is equivalent to a period of 25.641 usecs.