_images/fan2.png

Generación de una señal hardware PWM

Con objeto de controlar la velocidad de un ventilador instalado sobre un disipador, podemos generar una señal PWM en un pin de una Raspberry Pi 4, de una forma muy fácil, utilizando Python y la biblioteca rpi-hardware-pwm.

La solución estaba en utilizar otra biblioteca, en lugar de la RPi.GPIO. Una que soporte la generación de la señal PWM por hardware.

Importante

Elección del PIN GPIO:

Todos los pines de salida de la Raspberry Pi 4 pueden sacar una señal PWM, pero hay que tener en cuenta que no todos los pines son iguales, a la hora de generar una señal PWM.

  • Pines PWM software: Generarán la señal por software. Será la CPU de la Raspberry Pi 4 la encargada de generar la señal y consumirá recursos de CPU, como cualquier otro programa.

  • Pines PWM hardware: Generarán la señal por hardware. La CPU de la Raspberry Pi 4 solo tendrá que pedirle a uno de sus «generadores hardware PWM» que genere la señal deseada y podrá desentenderse. El hardware especializado en generar señales PWM se ocupará de su generación y la CPU no tendrá que hacer nada, por lo que no consumirá recursos de la CPU dejando toda su capacidad y potencia para otros programas.

Los pines 12/13 y 18/19 generan la señal PWM por hardware, mientras que todos los demás pines generan la señal PWM por software.

Lo primero es preparar la Raspberry Pi para que sea capaz de utilizar los dos canales PWM hardware de los que dispone.

Tienes que editar el fichero /boot/firmware/config.txt e incluir la línea dtoverlay=pwm-2chan

Para ello, desde la consola ejecuta:

sudo nano /boot/firmware/config.txt

Y tras la última línea que empiece por dtoverlay (que posiblemente esté comentada, con una almohadilla ‘#’), incluye la línea:

dtoverlay=pwm-2chan

Esto habilitará el PWM por hardware en los GPIO por defecto: GPIO_18 para PWM0 y GPIO_19 para PWM1. Podemos localizar la posición de los pines en el siguiente esquema:

_images/pinout.png

Opcionalmente, puedes utilizar (en vez de la línea anterior), la línea:

dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4

Esto habilitará el PWM por hardware en los GPIO alternativos: GPIO_12 para PWM0 y GPIO_13 para PWM1.

Ahora reinicia la Raspberry Pi, para que los cambios tengan efecto, con:

sudo reboot

Cuando vuelva a arrancar la Raspberry Pi, entra de nuevo en la consola e instala la biblioteca rpi-hardware-pwm con:

sudo apt-get update
sudo pip3 install rpi-hardware-pwm

(*) si el segundo comando te avisa que no encuentra el paquete, lo instalas con:

sudo pip3 install rpi-hardware-pwm --break-system-packages

Para comprobar que el paquete rpi-hardware-pwm se ha instalado correctamente, ejecutamos:

lsmod | grep pwm

Debemos de obtener una respuesta con un número, similar a esta:

pwm_bcm2835            12288  1

Programa de prueba y calibración de la señal PWM

La Raspberry Pi ya está preparada para generar una señal PWM por hardware.

Ahora necesitamos un programa, que nos permita:

  • Probar de una forma fácil que, efectivamente, funciona.

  • Averiguar los ciclos de trabajo mínimos para que nuestro ventilador no se pare concreto (cada uno es diferente).

Para ello existe script en el lenguaje de programación Python, hardware_pwm_generator.py, que nos permite llamarlo con tres parámetros en la línea de comandos de la Raspberry Pi:

  1. Un 0 para utilizar el canal PWM0 en GPIO_18 PWM0 y PWM1 en GPIO_19 (o PWM0 en GPIO_12 PWM0 y PWM1 en GPIO_13, si has elegido los pines alternativos en el fichero /boot/firmware/config.txt.

  2. La frecuencia en hertzios. Por ejemplo, 25000 para 25Khz.

  3. El ciclo de trabajo (un número entre 0 y 100) que es en definitiva el porcentaje de velocidad del ventilador.

hardware_pwm_generator.py
import sys
from rpi_hardware_pwm import HardwarePWM

def set_pwm(pwm_channel, frequency, duty_cycle):
    pwm = HardwarePWM(pwm_channel, frequency)
    pwm.start(duty_cycle)
    input("Presiona Enter para detener el PWM...")
    pwm.stop()

if __name__ == "__main__":
    if len(sys.argv) != 4:
        print("Uso: python pwm_generator.py <canal_pwm> <frecuencia> <ciclo_de_trabajo>")
        sys.exit(1)

    pwm_channel = int(sys.argv[1])
    frequency = float(sys.argv[2])
    duty_cycle = float(sys.argv[3])

    try:
        set_pwm(pwm_channel, frequency, duty_cycle)
    except KeyboardInterrupt:
        pass

Puedes ejecutar el programa con el siguiente comando:

python hardware_pwm_generator.py <canal_pwm> <frecuencia> <ciclo_de_trabajo>

Reemplaza:

  1. <canal_pwm> Por el canal PWM que quieres usar (0 o 1). Por ejemplo, un 0 para utilizar el canal PWM0 del GPIO_18.

  2. <frecuencia> Por la frecuencia en hertzios que quieres te tenga la señal PWM. Por ejemplo, 25000 para 25Khz.

  3. <ciclo_de_trabajo> Por el ciclo de trabajo que quieres, por ejemplo, «50» para un ciclo de trabajo del 50%.

Por ejemplo, para generar en el canal 0 de PWM (PWM0) en el GPIO_18 una señal a 25Khz con un ciclo de trabajo del 80%, tienes que ejecutar en la consola:

python hardware_pwm_generator.py 0 25000 80

Para salir y detener el PWM, solo tienes que pulsar [Enter].

También puedes interrumpir y salir del programa con Ctrl-C. En este caso la señal PWM no se detendrá.

Lectura de la temperatura de la CPU

Para leer la temperatura de la CPU de una Raspberry Pi puedes utilizar la biblioteca psutil.

Asegúrate de tener la biblioteca psutil instalada. Puedes instalarla con el siguiente comando:

sudo apt-get update

sudo pip install psutil
ó
sudo pip install psutil --break-system-packages

Ahora que tenemos la biblioteca instalada, podemos ver la temperatura ejecutando la siguiente línea:

vcgencmd measure_temp

Aquí tienes un programa simple en Python temperature_reader.py que utiliza la biblioteca psutil para leer la temperatura de la CPU y la imprime en la consola cada 5 segundos:

temperature_reader.py
import psutil
import time

def get_cpu_temperature():
    try:
        temperature = psutil.sensors_temperatures()['cpu_thermal'][0].current
        return temperature
    except Exception as e:
        print(f"Error al obtener la temperatura de la CPU: {e}")
        return None

def main():
    try:
        while True:
            temperature = get_cpu_temperature()

            if temperature is not None:
                print(f"Temperatura de la CPU: {temperature}°C")

            time.sleep(5)

    except KeyboardInterrupt:
        pass

if __name__ == "__main__":
    main()

Diferentes versiones de Raspberry Pi y de Linux pueden llamar al sensor de temperatura con un nombre diferente a cpu_thermal.

Ejecútalo desde la consola con la línea:

python temperature_reader.py

Si te da un error parecido a «Error al obtener la temperatura de la CPU: ‘cpu_thermal’» seguramente sea porque tu sensor no se llama ‘cpu_thermal’ y tendrás que averiguar su nombre.

Una vez sepas el nombre, tendrás que sustituir en la línea 6 el nombre ‘cpu_thermal’ por el nombre de tu sensor.

Por ejemplo, tanto en la Raspberry Pi 3 con la que empecé a hacer las pruebas como en la Raspberry Pi, este sensor se llamaba ‘cpu_thermal’.

Puedes utilizar este pequeño código para averiguarlo imprimiendo la lista de todos los sensores disponibles para entender qué nombres están presentes en tu sistema:

print-sensors.py
import psutil
import time

def get_cpu_temperature():
    try:
        sensors_data = psutil.sensors_temperatures()
        if 'coretemp' in sensors_data:
            temperature = sensors_data['cpu_thermal'][0].current
            return temperature
        else:
            print("No se encontraron datos del sensor 'cpu_thermal'. Sensores disponibles:", sensors_data.keys())
            return None
    except Exception as e:
        print(f"Error al obtener la temperatura de la CPU: {e}")
        return None

def main():
    try:
        while True:
            temperature = get_cpu_temperature()

            if temperature is not None:
                print(f"Temperatura de la CPU: {temperature}°C")

            time.sleep(5)

    except KeyboardInterrupt:
        pass

if __name__ == "__main__":
    main()

Este script imprimirá la lista de sensores disponibles en tu sistema si no puede encontrar el sensor cpu_thermal. Al ejecutar el script, podrás ver qué nombres de sensores están presentes, y podrás ajustar el código en consecuencia para leer la temperatura desde el sensor correcto.

Lectura de la temperatura y ajuste de la velocidad del ventilador

Para leer la temperatura de la CPU de una Raspberry Pi 4 y generar una señal PWM con un ciclo de trabajo proporcional a la temperatura de la CPU, utilizaremos, como hemos visto antes, la biblioteca psutil para obtener la temperatura de la CPU y, de nuevo, la biblioteca rpi-hardware-pwm para generar la señal PWM.

temperature_pwm_controller.py
#!/usr/bin/env python3

import configparser
from rpi_hardware_pwm import HardwarePWM
import psutil
import time
import sys
import atexit
import syslog
import os
import signal

#Inicializa las variables
canal_pwm = 0
frecuencia = 25000
ciclo_de_trabajo_anterior = 0
temperatura_anterior = 0
tiempo = 6
ciclo_de_trabajo = 0

pwm = HardwarePWM(canal_pwm, frecuencia)

def print_debug(mensaje):
    if debug and not as_a_service:
        print(mensaje)
    if debug and as_a_service:
        syslog.syslog(syslog.LOG_INFO, mensaje)

def inicializar_configuracion():
    # Valores predeterminados
    defaults = {
        'intervalo_de_prueba': 5,
        'canal_pwm': 0,
        'frecuencia': 25000,
        'temp_min': 45,
        'temp_max': 65,
        'ciclo_min': 60,
        'ciclo_max': 100,
        'histeresis': 2,
        'debug': False,
        'as_a_service': True
    }

    try:
        # Cargar configuración desde el archivo INI
        config = configparser.ConfigParser()
        config.read("temperature_pwm_controller.ini")

        # Desempaquetar la configuración o usar valores predeterminados
        intervalo_de_prueba = config.getint("config", "intervalo_de_prueba", fallback=defaults['intervalo_de_prueba'])
        canal_pwm = config.getint("config", "canal_pwm", fallback=defaults['canal_pwm'])
        frecuencia = config.getint("config", "frecuencia", fallback=defaults['frecuencia'])
        temp_min = config.getint("config", "temp_min", fallback=defaults['temp_min'])
        temp_max = config.getint("config", "temp_max", fallback=defaults['temp_max'])
        ciclo_min = config.getint("config", "ciclo_min", fallback=defaults['ciclo_min'])
        ciclo_max = config.getint("config", "ciclo_max", fallback=defaults['ciclo_max'])
        histeresis = config.getint("config", "histeresis", fallback=defaults['histeresis'])
        debug = config.getboolean("config", "debug", fallback=defaults['debug'])
        as_a_service = config.getboolean("config", "as_a_service", fallback=defaults['as_a_service'])

        return (intervalo_de_prueba, canal_pwm, frecuencia, temp_min, temp_max, ciclo_min, ciclo_max, histeresis, debug, as_a_service)

    except Exception as e:
        print(f"Error al cargar la configuración: {str(e)}")
        syslog.syslog(syslog.LOG_ERR, f"Error al cargar la configuración: {str(e)}")
        # Utiliza los valores predeterminados si hay un error al cargar la configuración
        return tuple(defaults.values())

def calcular_ciclo_de_trabajo(temp, temp_min, temp_max, ciclo_min, ciclo_max, ciclo_actual, hysteresis):
    try:
        if temp < temp_min:
            return 0
        elif temp > temp_max:
            return 100
        elif temp_min <= temp:
            if (temp > (temperatura_anterior + hysteresis)) or (temp < (temperatura_anterior - hysteresis)):
                print_debug(f"***Superada histéresis. Nueva temperatura: {temp:.2f}ºC. Nuevo ciclo de trabajo: {ciclo_de_trabajo:.0f}%. La temperatura anterior era {temperatura_anterior:.2f} °C. El ciclo anterior era de  {ciclo_de_trabajo_anterior:.0f}%")
                ciclo = (temp - temp_min) * (ciclo_max - ciclo_min) / (temp_max - temp_min) + ciclo_min
                ciclo = max(ciclo_min, min(ciclo, ciclo_max))
                return round(ciclo)
        return round(ciclo_actual)
    except Exception as e:
        print_debug(f"Error al calcular el ciclo de trabajo: {str(e)}")
        return round(ciclo_actual)

def parar_ventilador(signum, frame):
    try:
        syslog.syslog(syslog.LOG_INFO, "Parando salida PWM")
        pwm.stop();
        time.sleep(1)
        # Asegurarse de que todos los buffers estén vacíos antes de salir
        sys.stdout.flush()
        sys.stderr.flush()
        sys.exit(0)
    except Exception as e:
        print_debug(f"Error al parar el servicio: {str(e)}")

try:
    # Llama a la función para inicializar la configuración
    (intervalo_de_prueba, canal_pwm, frecuencia, temp_min, temp_max, ciclo_min, ciclo_max, histeresis, debug, as_a_service) = inicializar_configuracion()

    script_name = os.path.splitext(os.path.basename(sys.argv[0]))[0]
    syslog.openlog(ident=script_name, facility=syslog.LOG_USER)
    syslog.syslog(syslog.LOG_INFO, f"El servicio {script_name} se está ejecutando.")

    # Para que se detenga el ventilador al para el script
    signal.signal(signal.SIGTERM, parar_ventilador)

    pwm.start(100)
    time.sleep(1)
    pwm.change_duty_cycle(0)

    while True:
        temp = psutil.sensors_temperatures()['cpu_thermal'][0].current
        ciclo_de_trabajo = calcular_ciclo_de_trabajo(temp, temp_min, temp_max, ciclo_min, ciclo_max, ciclo_de_trabajo, histeresis)

        if tiempo >= intervalo_de_prueba and debug is True:
            mensaje = f"La temperatura de la CPU es {temp:.2f} °C. El ciclo de trabajo es de {ciclo_de_trabajo:.0f}%"
            print_debug(mensaje)
            mensaje = f"La temperatura anterior era {temperatura_anterior:.2f} °C. El ciclo anterior era de  {ciclo_de_trabajo_anterior:.0f}%"
            print_debug(mensaje)
            mensaje = f"--------------------------------------------------------------------------------------------"
            print_debug(mensaje)
            tiempo = 0
        else:
            tiempo += 1

        if ciclo_de_trabajo_anterior == 0 and ciclo_de_trabajo != 0:
            print_debug(f"Arrancando ventilador... (temperatura {temp:.2f}ºC)")
            pwm.change_duty_cycle(100)
            ciclo_de_trabajo_anterior = 100
            time.sleep(1)

        if ciclo_de_trabajo != ciclo_de_trabajo_anterior:
            pwm.change_duty_cycle(ciclo_de_trabajo)
            print_debug(f"Nueva temperatura: {temp:.2f}ºC. Nuevo ciclo de trabajo: {ciclo_de_trabajo:.0f}%")
            print_debug(f"Temperatura cambio anterior: {temperatura_anterior:.2f}ºC. Ciclo de trabajo anterior: {ciclo_de_trabajo_anterior:.0f}%")
            ciclo_de_trabajo_anterior = ciclo_de_trabajo
            temperatura_anterior = temp

        time.sleep(intervalo_de_prueba)

except Exception as e:
    print(f"Error general: {str(e)}")
    syslog.syslog(syslog.LOG_ERR, f"Error general: {str(e)}")

Puedes grabar el programa en un fichero con nombre temperature_pwm_controller.py mediante:

sudo nano temperature_pwm_controller.py

Este programa lee la temperatura de la CPU cada 5 segundos y ajusta el ciclo de trabajo del PWM de acuerdo con la temperatura.

Hay varios parámetros que puedes ajustar según tus necesidades y preferencias.

  • canal_pwm = 0: Te permite elegir el canal PWM que quieres utilizar.

  • frecuencia = 25000: La frecuencia de la señal PWM. Ten en cuenta que una señal de una frecuencia más baja provocará un ruido audible que puede ser molesto.

  • temp_min = 45: La temperatura por debajo de la cual el ventilador estará parado.

  • temp_max = 60: La temperatura que no quieres que se sobrepase. A partir de esta temperatura el ciclo de trabajo de la señal PWM será del 100%.

  • ciclo_min = 55: Ciclo de la señal PWM para tu ventilador y preferencias. No pongas una señal demasiado baja que pueda hacer que el ventilador no gire.

  • ciclo_max = 100: El ciclo de trabajo máximo al que quieres que se genere la señal PWM.

  • histeresis = 2: Para que el ventilador no esté continuamente parándose y poniéndose en marcha, este es el valor de «zona gris». La temperatura tendrá que variar más que este valor para que el ventilador cambie entre movimiento y parado.

python temperature_pwm_controller.py

El script leerá la temperatura y ajustará la señal PWM periódicamente.

Pulsa Ctrl-C para interrumpirlo.

Configuración del script

Si lo deseas puedes crear un fichero de configuración para que te resulte más fácil adaptar el programa a tus preferencias sin tener que modificar el script.

Este fichero es completamente opcional y si no lo creas (o falta cualquier línea) el script utilizará los valores por defecto que están definidos en el código.

Solo tienes que crear un fichero de texto, llamado temperature_pwm_controller.ini en la misma carpeta donde tengas el script (fichero temperature_pwm_controller.py):

sudo nano temperature_pwm_controller.ini

Dentro del archivo, solo tienes que incluir una primera línea, a modo de cabecera, con el texto [config] y a continuación incluir las líneas que desees de entre las que figuran arriba con los valores que quieras.

[config]
intervalo_de_prueba = 5
canal_pwm = 0
frecuencia = 25000
temp_min = 46
temp_max = 65
ciclo_min = 60
ciclo_max = 100
hysteresis = 2
debug = False

Instalar el script desde GitHub

Puedes encontrar todo código actualizado en este repositorio de GitHub y lo puedes descargar a tu Raspberry Pi directamente desde allí.

Una vez en la consola te recomiendo que crees un directorio en el que clonar el repositorio, y lo clones:

git clone https://github.com/melkati/raspberry-fan.git

Ten en cuenta que tendrás que hacer los ajustes necesarios porque los scripts no estarán en /home/pi sino en /home/pi/raspberry-fan

El archivo temperature_pwm_controller.service estará ya configurado para funcionar desde /home/pi/raspberry-fan

Inicio automático del script

No tendría mucho sentido que cada vez que arrancásemos la Raspberry Pi tuviéramos que entrar en la consola y ejecutar un programa, de manera que lo vamos a automatizar.

Para que el script se ejecute automáticamente al arrancar hay varias formas de hacerlo. Aquí vamos a ver cómo se hace con systemctl.

Ejecución automática con systemctl

En este ejemplo el nombre del script es temperature_pwm_controller.py y la ruta completa donde está es /home/pi/:

Asegúrate de que el script sea ejecutable:

sudo chmod +x /home/pi/temperature_pwm_controller.py

Crea un archivo de servicio en la ubicación /etc/systemd/system/. Puedes nombrarlo, por ejemplo, temperature_pwm_controller.service.

sudo nano /etc/systemd/system/temperature_pwm_controller.service

El contenido del archivo debe ser el siguiente:

# Este es un archivo de servicio para el controlador PWM basado en temperatura
# que se ejecutará automáticamente al arrancar la Raspberry Pi.

[Unit]
Description=Controlador de PWM basado en temperatura
After=multi-user.target

[Service]
ExecStartPre=sleep 30
ExecStart=/usr/bin/python3 /home/pi/temperature_pwm_controller.py
WorkingDirectory=/home/pi
StandardOutput=syslog
StandardError=syslog
Restart=always
User=pi

[Install]
WantedBy=multi-user.target

Puedes crear el archivo con el siguiente comando:

sudo nano /etc/systemd/system/temperature_pwm_controller.service

Una vez creado, asegurate de que tiene los permisos correctos. Puedes darle los permisos con la siguiente línea:

sudo chmod 777 /etc/systemd/system/temperature_pwm_controller.service

Asegúrate de reemplazar /home/pi/temperature_pwm_controller.py con la ruta completa de tu script de Python, si lo has modificado.

Recarga systemd:

sudo systemctl daemon-reload

Habilita e inicia el servicio:

sudo systemctl enable temperature_pwm_controller.service
sudo systemctl start temperature_pwm_controller.service

El servicio tiene un retraso antes de ejecutarse de 30 segundos para asegurarse de que la Raspberry Pi se haya iniciado por completo antes de poner en marcha el ventilador, por lo que tardará un tiempo en hacerlo.

Si en este punto reinicias la Raspberry Pi, el servicio arrancará automáticamente.

Puedes hacerlo desde la consola con la línea:

sudo reboot

Verifica el estado del servicio:

sudo systemctl status temperature_pwm_controller.service

Puedes parar el servicio con:

sudo systemctl stop temperature_pwm_controller.service

Puedes ejecutar el servicio con:

sudo systemctl start temperature_pwm_controller.service

Puedes ver el estado del servicio con:

sudo systemctl status temperature_pwm_controller.service

Puedes ver el log del servicio con:

sudo journalctl -f -u temperature_pwm_controller.service

Fíjate en que no es necesario que ejecutemos el script cada quince segundos ya que realmente está siempre funcionando, realizando su trabajo cada 15 segundos.

Estos pasos deberían configurar tu script temperature_pwm_controller.py para ejecutarse automáticamente en el arranque de tu Raspberry Pi utilizando systemd. Asegúrate de ajustar las rutas y nombres de archivo según tus necesidades específicas.