Zum Inhalt springen →

Mit dem Milk-V Duo einen PWM-Lüfter steuern

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.

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.

Pin Configuration

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.

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.

/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)


Ein Blick auf PulseView zeigt, dass exakt die 25kHZ mit einem Duty-Cycle von 50% am GP4 ankommen.

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.

Der Steckverbinder ist hier nur provisoprisch und wird noch durch das kleine Kabel ersetzt, dass ich von dem beiligenden Y-Adapter abgeschnitten habe.
Der Stromverbrauch von 244mA bei 100% Duty.Cycle kann sich sehen lassen.

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.

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.

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 ….

Veröffentlicht in Allgemein