Mein Jetson Orin hat seit einigen Monaten seinen Platz bei mir gefunden und läuft aktuell quasi permanent auf 65W und macht das Finetuning für diverse LLMs dabei wird er in dem Schrank leider sehr warm und die Wärme muss irgendwie aus dem Schrank raus.
Noctua NF-A6x25 5V PWM-Lüfter
Also habe ich mir einen Noctua NF-A6x25 5V PWM-Lüfter bestellt, um die Luft im Schrank etwas abzukühlen. Das Ziel ist es, die RPM des Lüfters über das PWM-Signal des Milk-V Duo zu steuern.
![](https://www.jentsch.io/wp-content/uploads/2023/11/image-7.png)
Lüfter
Der Noctua NF-A6x25 5V PWM-Lüfter ist ein 5Volt Lüfter, der auch bei hohen Drehzahlen noch sehr leise ist (19,3 dB(A)) und relativ wenig Strom (260mA) benötigt. Laut des WhitePapers kann man die Drehzahl über ein PWM-Signal steuern und so z.B. abhängig von Temperatur und Uhrzeit die Drehzahl regulieren.
https://noctua.at/pub/media/wysiwyg/Noctua_PWM_specifications_white_paper.pdf
Der Noctua NF-A6x25 5V hat 4 Anschlüsse (+5V, GND, RPM und PWM). Das RPM-Signal aus dem Lüfter kann man dazu verwenden, die aktuelle Drehzahl auszulesen. Hier kommen nur die 3 Anschlüsse +5V, GND und RPM zum Einsatz. Die aktuelle Drehzahl wird nicht ausgelesen.
![](https://www.jentsch.io/wp-content/uploads/2023/11/image-4.png)
Milk-V Duo zur PWM-Steuerung des Lüfters
Laut https://milkv.io/docs/duo/overview kann der Milk-V Duo bis zu 7x PWM GPIOS.
![](https://www.jentsch.io/wp-content/uploads/2023/11/image-2.png)
Allerdings sagen die Bezeichnungen „PWM4, PWM5, PWM6, PWM7, PWM8, PWM9, PWM10, PWM11“, dass es 8 PWM GPIOs gibt – komisch? Mit Hilfe von lsmod kann man sich die geladenen Module anzeigen lassen. Da sollte auch ein PWM-Modul dabei sein – evtl. kommt man damit weiter …?
[root@milkv-duo]/mnt/system# lsmod Module Size Used by Tainted: GF cv180x_pwm 6983 1 cvi_vc_driver 879138 0 [permanent] cv180x_jpeg 25220 1 cvi_vc_driver,[permanent] cv180x_vcodec 28451 2 cvi_vc_driver,cv180x_jpeg,[permanent] cv180x_tpu 32041 0 [permanent] cv180x_clock_cooling 5953 0 [permanent] cv180x_thermal 3404 0 cv180x_rgn 100809 0 [permanent] cv180x_dwa 48669 0 [permanent] cv180x_vpss 280938 0 [permanent] cv180x_vi 338826 0 [permanent] snsr_i2c 9341 0 [permanent] cvi_mipi_rx 54306 0 [permanent] cv180x_fast_image 32955 0 [permanent] cv180x_rtos_cmdqu 25922 1 cv180x_fast_image,[permanent] cv180x_base 96472 8 cvi_vc_driver,cv180x_rgn,cv180x_dwa,cv180x_vpss,cv180x_vi,snsr_i2c,cvi_mipi_rx,cv180x_rtos_cmdqu,[permanent] cv180x_sys 64161 7 cvi_vc_driver,cv180x_rgn,cv180x_dwa,cv180x_vpss,cv180x_vi,cv180x_fast_image,cv180x_base,[permanent]
OK, cv180x_pwm ist geladen :-). Eine Doku zu dem Modul kann man unter „https://doc.sophgo.com/cvitek-develop-docs/master/docs_latest_release/CV180x_CV181x/en/01.software/OSDRV/Peripheral_Driver_Operation_Guide/build/html/10_PWM_Operation_Guide.html“ finden. Hier kann man folgendes nachlesen:
Cv180X/CV181X verfügt über 4 PWM-IPs (pwmchip0/ pwmchip4/ pwmchip8/ pwmchip12), jede IP steuert 4 Kanäle und kann insgesamt 16 Signale steuern.
Mit dem Befehl „ls -altr /sys/class/pwm/“ kann man sich die 4 PWM-IPs ausgeben lassen.
![](https://www.jentsch.io/wp-content/uploads/2023/11/image-3.png)
/sys/class/pwm/pwmchip0 /sys/class/pwm/pwmchip4 /sys/class/pwm/pwmchip8 /sys/class/pwm/pwmchip12
Jeder dieser 4 PWM-IPs kann also jeweils 4 PWM GPIOs steuern. Für die Lüfter-Steuerung des Noctua NF-A6x25 5V PWM-Lüfters benötig man nur einen PWM-Anschluss. Also mal sehen, was wie angesteuert werden kann.
Unter https://docs.khadas.com/products/sbc/common/applications/gpio/pwm habe ich ein paar Hinweise zur Steuerung der PWM-Anschlüsse gefunden, aber welcher GPIO zu welchem pwmchipXY gehört habe ich noch nicht herausbekommen.
Mit duo-pinmux -l kann man sich die Funktionen der GPIO-Pins ausgeben lassen. Das Ergebnis sieht dann wie folgt aus:
[root@milkv-duo]~/pwm# duo-pinmux -l GP0 function: [ ] JTAG_TDI [ ] UART1_TX [ ] UART2_TX [ ] GP0 [v] IIC0_SCL [ ] WG0_D0 [ ] DBG_10 GP1 function: [ ] JTAG_TDO [ ] UART1_RX [ ] UART2_RX [ ] GP1 [v] IIC0_SDA [ ] WG0_D1 [ ] WG1_D0 [ ] DBG_11 GP2 function: [v] UART4_TX [ ] GP2 [ ] PWM_10 GP3 function: [v] UART4_RX [ ] GP3 [ ] PWM_11 GP4 function: [ ] PWR_SD1_D2 [ ] IIC1_SCL [ ] UART2_TX [ ] GP4 [ ] CAM_MCLK0 [ ] UART3_TX [ ] PWR_SPINOR1_HOLD_X [v] PWM_5 GP5 function: [ ] PWR_SD1_D1 [ ] IIC1_SDA [ ] UART2_RX [ ] GP5 [ ] CAM_MCLK1 [ ] UART3_RX [ ] PWR_SPINOR1_WP_X [v] PWM_6 GP6 function: [ ] PWR_SD1_CLK [v] SPI2_SCK [ ] IIC3_SDA [ ] GP6 [ ] CAM_HS0 [ ] EPHY_SPD_LED [ ] PWR_SPINOR1_SCK [ ] PWM_9 GP7 function: [ ] PWR_SD1_CMD [v] SPI2_SDO [ ] IIC3_SCL [ ] GP7 [ ] CAM_VS0 [ ] EPHY_LNK_LED [ ] PWR_SPINOR1_MOSI [ ] PWM_8 GP8 function: [ ] PWR_SD1_D0 [v] SPI2_SDI [ ] IIC1_SDA [ ] GP8 [ ] CAM_MCLK1 [ ] UART3_RTS [ ] PWR_SPINOR1_MISO [ ] PWM_7 GP9 function: [ ] PWR_SD1_D3 [v] SPI2_CS_X [ ] IIC1_SCL [ ] GP9 [ ] CAM_MCLK0 [ ] UART3_CTS [ ] PWR_SPINOR1_CS_X [ ] PWM_4 GP10 function: [ ] VI0_D_6 [ ] GP10 [v] IIC1_SDA [ ] KEY_ROW2 [ ] DBG_9 GP11 function: [ ] VI0_D_7 [ ] GP11 [v] IIC1_SCL [ ] CAM_MCLK1 [ ] DBG_10 GP12 function: [v] UART0_TX [ ] CAM_MCLK1 [ ] PWM_4 [ ] GP12 [ ] UART1_TX [ ] AUX1 [ ] JTAG_TMS [ ] DBG_6 GP13 function: [v] UART0_RX [ ] CAM_MCLK0 [ ] PWM_5 [ ] GP13 [ ] UART1_RX [ ] AUX0 [ ] JTAG_TCK [ ] DBG_7 GP14 function: [ ] SDIO0_PWR_EN [v] GP14 GP15 function: [v] GP15 GP16 function: [ ] SPINOR_MISO [ ] SPINAND_MISO [v] GP16 GP17 function: [ ] SPINOR_CS_X [ ] SPINAND_CS [v] GP17 GP18 function: [ ] SPINOR_SCK [ ] SPINAND_CLK [v] GP18 GP19 function: [ ] SPINOR_MOSI [ ] SPINAND_MOSI [v] GP19 GP20 function: [ ] SPINOR_WP_X [ ] SPINAND_WP [v] GP20 GP21 function: [ ] SPINOR_HOLD_X [ ] SPINAND_HOLD [v] GP21 GP22 function: [ ] PWR_SEQ2 [v] GP22 GP26 function: [v] GP26 [ ] KEY_COL2 [ ] PWM_3 GP27 function: [ ] USB_VBUS_DET [v] GP27 [ ] CAM_MCLK0 [ ] CAM_MCLK1 [ ] PWM_4 GP25 function: [v] GP25 [ ] IIS1_DI [ ] IIS2_DO [ ] IIS1_DO
Das ist auch nicht viel aufschlussreicher. Aber immerhin werden hier die Daten des Milk-V Pinouts bestätigt.
- GP26/PWM_3
- GP27/PWM_4
- GP9/PWM_4
- GP12/PWM_4
- GP13/PWM_5
- GP4/PWM_5
- GP5/PWM_6
- GP8/PWM_7
- GP7/PWM_8
- GP6/PWM_9
- GP2/PWM_10
- GP3/PWM_11
Wie es scheint, hilft nur messen. Also nehme ich meinen 8-Kanal Logic Analyzer und checke mal die GPIOs 26 (PWM3), 9 (PWM4), 13 (PWM5), 5 (PWM6), 8 (PWM7), 7 (PWM8), 6 (PWM9), 2 (PWM10), 3 (PWM11).
Also aktiviere ich erst mal mit dem duo-pinmux alle PWM GPIOs.
#!/bin/sh
set -x
duo-pinmux -w GP26/PWM_3
duo-pinmux -w GP26/PWM_3
duo-pinmux -w GP27/PWM_4
duo-pinmux -w GP9/PWM_4
duo-pinmux -w GP12/PWM_4
duo-pinmux -w GP13/PWM_5
duo-pinmux -w GP4/PWM_5
duo-pinmux -w GP5/PWM_6
duo-pinmux -w GP8/PWM_7
duo-pinmux -w GP7/PWM_8
duo-pinmux -w GP6/PWM_9
duo-pinmux -w GP2/PWM_10
duo-pinmux -w GP3/PWM_11
[root@milkv-duo]~/pwm# ./pwm-enable.sh + duo-pinmux -w GP26/PWM_3 pin GP26 func PWM_3 register: 30010a8 value: 6 + duo-pinmux -w GP26/PWM_3 pin GP26 func PWM_3 register: 30010a8 value: 6 + duo-pinmux -w GP27/PWM_4 pin GP27 func PWM_4 register: 30010ac value: 6 + duo-pinmux -w GP9/PWM_4 pin GP9 func PWM_4 register: 300108c value: 7 + duo-pinmux -w GP12/PWM_4 pin GP12 func PWM_4 register: 3001024 value: 2 + duo-pinmux -w GP13/PWM_5 pin GP13 func PWM_5 register: 3001028 value: 2 + duo-pinmux -w GP4/PWM_5 pin GP4 func PWM_5 register: 3001090 value: 7 + duo-pinmux -w GP5/PWM_6 pin GP5 func PWM_6 register: 3001094 value: 7 + duo-pinmux -w GP8/PWM_7 pin GP8 func PWM_7 register: 3001098 value: 7 + duo-pinmux -w GP7/PWM_8 pin GP7 func PWM_8 register: 300109c value: 7 + duo-pinmux -w GP6/PWM_9 pin GP6 func PWM_9 register: 30010a0 value: 7 + duo-pinmux -w GP2/PWM_10 pin GP2 func PWM_10 register: 3001084 value: 7 + duo-pinmux -w GP3/PWM_11 pin GP3 func PWM_11 register: 3001088 value: 7
Mit einem weiteren kleinen Script prüfe ich dann die PWM GPIOs.
#!/bin/sh
# pwm-check.sh
duo-pinmux -r GP26
duo-pinmux -r GP26
duo-pinmux -r GP27
duo-pinmux -r GP9
duo-pinmux -r GP12
duo-pinmux -r GP13
duo-pinmux -r GP4
duo-pinmux -r GP5
duo-pinmux -r GP8
duo-pinmux -r GP7
duo-pinmux -r GP6
duo-pinmux -r GP2
duo-pinmux -r GP3
Sieht aus, als hätte es funktioniert:
GP26 function: [ ] GP26 [ ] KEY_COL2 [v] PWM_3 register: 0x30010a8 value: 6 GP26 function: [ ] GP26 [ ] KEY_COL2 [v] PWM_3 register: 0x30010a8 value: 6 GP27 function: [ ] USB_VBUS_DET [ ] GP27 [ ] CAM_MCLK0 [ ] CAM_MCLK1 [v] PWM_4 register: 0x30010ac value: 6 GP9 function: [ ] PWR_SD1_D3 [ ] SPI2_CS_X [ ] IIC1_SCL [ ] GP9 [ ] CAM_MCLK0 [ ] UART3_CTS [ ] PWR_SPINOR1_CS_X [v] PWM_4 register: 0x300108c value: 7 GP12 function: [ ] UART0_TX [ ] CAM_MCLK1 [v] PWM_4 [ ] GP12 [ ] UART1_TX [ ] AUX1 [ ] JTAG_TMS [ ] DBG_6 register: 0x3001024 value: 2 GP13 function: [ ] UART0_RX [ ] CAM_MCLK0 [v] PWM_5 [ ] GP13 [ ] UART1_RX [ ] AUX0 [ ] JTAG_TCK [ ] DBG_7 register: 0x3001028 value: 2 GP4 function: [ ] PWR_SD1_D2 [ ] IIC1_SCL [ ] UART2_TX [ ] GP4 [ ] CAM_MCLK0 [ ] UART3_TX [ ] PWR_SPINOR1_HOLD_X [v] PWM_5 register: 0x3001090 value: 7 GP5 function: [ ] PWR_SD1_D1 [ ] IIC1_SDA [ ] UART2_RX [ ] GP5 [ ] CAM_MCLK1 [ ] UART3_RX [ ] PWR_SPINOR1_WP_X [v] PWM_6 register: 0x3001094 value: 7 GP8 function: [ ] PWR_SD1_D0 [ ] SPI2_SDI [ ] IIC1_SDA [ ] GP8 [ ] CAM_MCLK1 [ ] UART3_RTS [ ] PWR_SPINOR1_MISO [v] PWM_7 register: 0x3001098 value: 7 GP7 function: [ ] PWR_SD1_CMD [ ] SPI2_SDO [ ] IIC3_SCL [ ] GP7 [ ] CAM_VS0 [ ] EPHY_LNK_LED [ ] PWR_SPINOR1_MOSI [v] PWM_8 register: 0x300109c value: 7 GP6 function: [ ] PWR_SD1_CLK [ ] SPI2_SCK [ ] IIC3_SDA [ ] GP6 [ ] CAM_HS0 [ ] EPHY_SPD_LED [ ] PWR_SPINOR1_SCK [v] PWM_9 register: 0x30010a0 value: 7 GP2 function: [ ] UART4_TX [ ] GP2 [v] PWM_10 register: 0x3001084 value: 7 GP3 function: [ ] UART4_RX [ ] GP3 [v] PWM_11 register: 0x3001088 value: 7
Mit einem kleinen Test Skript werde ich mal einen PWM GPIO aktivieren. Dabei übernehme ich die Target Frequenz von 25kHz aus der 4-Wire Pulse Width Modulation (PWM) Controlled Fans Specification auf die in dem Noctua NF-A6x25 5V PWM Lüfter Whitepaper verwiesen wird.
Die 25kHZ entsprechen 40000ns bei einem Duty-Cycle von 50% sieht, dass test Script dann z.B. für pwmchip4 / pwm1 wie folgt aus.
#!/bin/sh
# set -x
duo-pinmux -w GP4/PWM_5
echo 1 > /sys/class/pwm/pwmchip4/export
echo 40000 > /sys/class/pwm/pwmchip4/pwm1/period
echo 20000 > /sys/class/pwm/pwmchip4/pwm1/duty_cycle
echo 1 > /sys/class/pwm/pwmchip4/pwm1/enable
Nach ein wenig herumprobieren und habe ich mich dann für GP4/PWM_5 entschieden. (Siehe oben)
![](https://www.jentsch.io/wp-content/uploads/2023/11/image-5.png)
Ein Blick auf PulseView zeigt, dass exakt die 25kHZ mit einem Duty-Cycle von 50% am GP4 ankommen.
![](https://www.jentsch.io/wp-content/uploads/2023/11/image-6.png)
So weit so gut.
Milk-V Duo Temperatur abfragen
Mit dem folgenden einfachen Befehl lässt sich einfach die Temperatur des RISV-V Chips auslesen.
[root@milkv-duo]~/adc# cat /sys/class/thermal/thermal_zone0/temp
32558
Die Zahl entspricht dann der Temperatur in °C * 1000. Mit folgendem kleinen Python Skript lässt sich die Temperatur dann auch etwas besser darstellen.
#!/usr/bin/python3
with open('/sys/class/thermal/thermal_zone0/temp') as temp:
curCtemp = float(temp.read()) / 1000
curFtemp = ((curCtemp / 5) * 9) + 32
print ("C:", curCtemp, " F:", curFtemp)
[root@milkv-duo]~/temp# python temp.py C: 38.152 F: 100.6736
Abhängig von der Temperatur kann ich also den Lüfter über das PWM-Signal steuern. Das ist zwar eigentlich die Temperatur des Chips auf dem Milk-V Duo, aber das reicht mir für den Moment.
Elektronik zusammenbauen
Nach dem Motto „Erst Coden, dann Löten“ habe ich nun die wichtigsten Coding-Details geklärt und löte erst mal alles zusammen.
![](https://www.jentsch.io/wp-content/uploads/2023/11/image-8.png)
![](https://www.jentsch.io/wp-content/uploads/2023/11/image-9.png)
![](https://www.jentsch.io/wp-content/uploads/2023/11/image-10.png)
Montage im Schrank
Der Schrank hat auf der Rückseite ein paar Öffnungen für Kabel, etc. die einen Durchmesser von ca. 60mmm haben. Eine dieser Öffnungen kann ich nutzen, um den Lüfter zu montieren.
Da ich nichts schrauben möchte erstelle ich mir einen Adapter, mit dem ich den Lüfter einfach in eines der Öffnungen einsetzen kann.
![](https://www.jentsch.io/wp-content/uploads/2023/11/unnamed.png)
Die STL und die OpenSCAD Datei kann unter https://www.thingiverse.com/thing:6300854/files heruntergeladen werden.
Einrichtung
Nachdem alles passt und der Lüfter per USB an den Jetson Orin angeschlossen ist, wird das kleine Shell-Skript, mit dem der Lüfter gesteuert wird in den init.d eingetragen.
1. Erstellen der Datei /mnt/system/fan.sh
#!/bin/sh
# /mnt/system/fan.sh
# Lüftersteuerung
/usr/bin/duo-pinmux -w GP4/PWM_5
echo 1 > /sys/class/pwm/pwmchip4/export
while :
do
TEMP=`/bin/cat /sys/class/thermal/thermal_zone0/temp`
if [ $TEMP -gt 40000 ]
then
VAL=40000
else
VAL=$TEMP
fi
# echo $TEMP $VAL
TIME=$(/bin/date +%H)
if [[ $TIME -gt 21 ]] || [[ $TIME -lt 06 ]]; then
VAL=10000
fi
# echo $TIME $VAL
DATE=$(/bin/date)
PERCENT=`echo $VAL/400 | /usr/bin/bc`
TMP=`echo $TEMP/1000 | /usr/bin/bc`
# echo "$DATE $TMP°C $PERCEN$T - $VAL"
echo 40000 > /sys/class/pwm/pwmchip4/pwm1/period
echo $VAL > /sys/class/pwm/pwmchip4/pwm1/duty_cycle
echo 1 > /sys/class/pwm/pwmchip4/pwm1/enable
echo $VAL > /mnt/system/fan.log
sleep 10
done
2. Editieren der Datei /etc/init.d/S99user. Folgende Zeilen werden hinter dem blink.sh Skript eingefügt.
if [ -f $SYSTEMPATH/fan.sh ]; then
. $SYSTEMPATH/fan.sh &
fi
Nun noch ein reboot und schon läuft der Lüfter.
Allerdings gibt es noch ein Problem. Der Lüfter läuft nur mit 25% Duty-Cycle. Das liegt daran, dass der Milk-V Duo die Uhrzeit nicht kennt und nach dem Booten immer Mitternacht ist. Also muss nach jedem Reboot mit folgendem kleinem Befehl die korrekte Uhrzeit gesetzt werden.
date -s 202311051942
Setzt das Datum auf 05.11.2023 und die Uhrzeit auf 19:42.
In der Datei /mnt/system/fan.log kann man sich nun jederzeit die Temperatur (* 1000) anzeigen lassen.
![](https://www.jentsch.io/wp-content/uploads/2023/11/image-11.png)
Bei mir im Schrank ist es gerade nur 35,7°C. Das ist doch schon viel besser als die 80°C, die ich noch vor ein paar Tagen gemessen habe ….