.. image:: _images/fan2.png :align: center :width: 80% 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**. .. important:: **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: .. image:: _images/pinout.png :align: center :width: 60% | 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. .. code-block:: python :caption: 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 ") 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 Reemplaza: #. **** Por el canal PWM que quieres usar (0 o 1). Por ejemplo, un 0 para utilizar el canal PWM0 del GPIO_18. #. **** Por la frecuencia en hertzios que quieres te tenga la señal PWM. Por ejemplo, 25000 para 25Khz. #. **** 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: .. code-block:: python :caption: 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: .. code-block:: python :caption: 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. .. code-block:: python :caption: 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. .. code:: console 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. .. code:: powershell [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**. .. code:: console sudo nano /etc/systemd/system/temperature_pwm_controller.service El contenido del archivo debe ser el siguiente: .. code-block:: powershell # 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.