MicroPython para Seeed Studio XIAO nRF54L15 Sense
Este tutorial tiene como objetivo introducir cómo usar MicroPython en Thonny basado en XIAO nRF54L15.
MicroPython es un intérprete de Python con una función de compilación de código nativo parcial. Proporciona un subconjunto de características de Python 3.5, implementado para procesadores embebidos y sistemas restringidos. Es diferente de CPython y puedes leer más sobre las diferencias aquí.
Usar Thonny IDE
Prepara un XIAO nRF54L15.
Seeed Studio XIAO nRF54L15 Sense |
---|
![]() |
Instalar Thonny IDE
Elige la versión apropiada para la instalación. Aquí, lo estoy instalando en un sistema Windows, así que he seleccionado la versión de Windows.
Sigue las instrucciones para la versión de Python deseada.

Luego, simplemente sigue los pasos predeterminados para la configuración.
Descargar el repositorio
Clónalo en la máquina local, y luego recuerda la ruta donde está almacenado el MicroPython de este XIAO nRF54L15. Esta ruta se usará más tarde.
git clone https://github.com/Seeed-Studio/micropython-seeed-boards.git
Subir archivo de placa
Paso 1. Flashear el firmware de MicroPython para XIAO nRF54L15
-
Descarga el paquete de firmware y extráelo en la ubicación apropiada. Luego haz clic en flash.bat, y automáticamente flasheará el firmware por ti.
[Firmware] XIAO nRF54L15 MicroPython Firmware

Este script tiene comandos de cadena de herramientas de flasheo preconfigurados. Si lo estás usando por primera vez, puede tomar un poco de tiempo.
Paso 2. Abre Thonny IDE, luego haz clic en la esquina inferior derecha de la interfaz para configurar las opciones del intérprete. Selecciona MicroPython (generic) y Puerto

Este firmware tiene preestablecidas las instrucciones de la cadena de herramientas de flasheo. Si esta es la primera vez que lo usas, podría tomar un poco de tiempo.
Paso 3. Subir el archivo de placas
- Abre la vista, selecciona "File", y la ruta del administrador de archivos se mostrará en la barra lateral izquierda.

- Abre la ruta del archivo clonado o descargado, y abre
micropython-seeed-boards\examples
- Selecciona la carpeta "boards" y súbela al flash. Entonces, podrás ver el archivo subido en el dispositivo/flash de MicroPython.

Paso 4. Encender el LED
Copia el código y presiona F5
para ejecutarlo.
import time
from boards.xiao import XiaoPin
led = "led"
try:
# Initialize LED
led = XiaoPin(led, XiaoPin.OUT)
while True:
# LED 0.5 seconds on, 0.5 seconds off
led.value(1)
time.sleep(0.5)
led.value(0)
time.sleep(0.5)
except KeyboardInterrupt:
print("\nProgram interrupted by user")
except Exception as e:
print("\nError occurred: %s" % {e})
finally:
led.value(1)
El resultado es el siguiente:

Digital
Hardware
Seeed Studio XIAO nRF54L15 Sense | Seeed Studio Expansion Base for XIAO with Grove OLED | Grove - Relay |
---|---|---|
![]() | ![]() | ![]() |
Software
import time
from boards.xiao import XiaoPin
button = "sw"
relay = 0 #D0
try:
# Initialize button and relay
button = XiaoPin(button, XiaoPin.IN)
relay = XiaoPin(relay, XiaoPin.OUT)
relay.value(0)
while True:
# Read button state
button_state = button.value()
# Control relay based on button state
if button_state == 0:
relay.value(1)
else:
relay.value(0)
except KeyboardInterrupt:
print("\nProgram interrupted by user")
except Exception as e:
print("\nError occurred: %s" % {e})
finally:
relay.off()
Explicación del Código:
-
Importar Módulos
time
Importa el módulo timeXiao Pin
Importa la clase de control de pines para la placa de desarrollo Seeed Xiao desde el módulo boards.xiao, que se usa para operar los pines en la placa.
-
Definir Pines
button = "sw"
Especifica que el botón está conectado al pin "sw" de la placa de desarrollo (aquí, el pin BOOT) -relay = 0
Especifica que el relé está conectado al pin digital D0.
-
Lógica Principal (bloque try)
- Cuando se presiona el botón (estado es 0) → El relé se activa (salida es 1).
- Cuando no se presiona el botón (estado es 1) → El relé se desactiva (salida es 0).
Resultado

Analógico
Hardware
Seeed Studio XIAO nRF54L15 Sense | Grove-Variable Color LED | Grove-Rotary Angle Sensor | Seeed Studio Grove Base for XIAO |
---|---|---|---|
![]() | ![]() | ![]() | ![]() |
Software
import time
from boards.xiao import XiaoPin, XiaoADC, XiaoPWM
adc = 0 #D0
pwm = 1 #D1
try:
# Initialize ADC for potentiometer
adc = XiaoADC(adc)
# Initialize PWM for LED control
pwm = XiaoPWM(pwm)
FREQ = 1000
PERIOD_NS = 1000000
pwm.init(freq=FREQ, duty_ns=0)
# Potentiometer parameters
MIN_VOLTAGE = 0.0
MAX_VOLTAGE = 3.3
DEAD_ZONE = 0.05
last_duty = -1
while True:
# Read ADC voltage value
voltage = adc.read_uv() / 1000000
# Ensure voltage is within valid range
if voltage < MIN_VOLTAGE:
voltage = MIN_VOLTAGE
elif voltage > MAX_VOLTAGE:
voltage = MAX_VOLTAGE
duty_percent = (voltage - MIN_VOLTAGE) / (MAX_VOLTAGE - MIN_VOLTAGE)
# Apply dead zone to prevent tiny fluctuations
if abs(duty_percent - last_duty) < DEAD_ZONE / 100:
time.sleep(0.05)
continue
# Calculate duty cycle time (nanoseconds)
duty_ns = int(duty_percent * PERIOD_NS)
# Set PWM duty cycle
pwm.duty_ns(duty_ns)
# Print current status
print("Voltage: {:.2f}V, Duty Cycle: {:.1f}%".format(voltage, duty_percent * 100))
# Update last duty cycle value
last_duty = duty_percent
# Short delay
time.sleep(0.05)
except KeyboardInterrupt:
print("\nProgram interrupted by user")
except Exception as e:
print("\nError occurred: %s" % {e})
finally:
pwm.deinit()
Explicación del Código:
-
Importar Librerías Dependientes
time
: Se utiliza para agregar retrasos y controlar el ritmo de ejecución del programa.boards.xiao
: Importa las clases de control de hardware de la placa de desarrollo Xiao, incluyendo:XiaoADC
: Se utiliza para leer señales analógicas (como la salida del potenciómetro).XiaoPWM
: Se utiliza para generar señales PWM (para controlar el brillo del LED).
-
Definir Pines de Hardware
adc
corresponde al pin D0 de la placa de desarrollo (utilizado para conectar la salida del potenciómetro), ypwm
corresponde al pin D1 (utilizado para conectar el LED).
-
Inicializar Hardware (bloque try)
XiaoADC(adc)
: Inicializa el pin D0 como modo de entrada ADC para leer la señal de voltaje del potenciómetro.XiaoPWM(pwm)
: Inicializa el pin D1 como modo de salida PWM para controlar el LED.Parámetros PWM
: Una frecuencia de 1000Hz significa que el período de la señal es de 1 milisegundo (1e6 nanosegundos). El ciclo de trabajo (la proporción del período que dura el nivel alto) determina el brillo del LED (cuanto mayor sea el ciclo de trabajo, más brillante será el LED).
-
Bucle Principal (Lógica Central)
- Leer Voltaje: Lee el voltaje de salida del potenciómetro a través del ADC (convierte la unidad a voltios).
- Limitación de Rango: Asegura que el voltaje esté dentro de 0~3.3V (rango de seguridad del hardware).
- Cálculo del Ciclo de Trabajo: Convierte linealmente el voltaje a un ciclo de trabajo de 0~1 (por ejemplo, 1.65V corresponde a un ciclo de trabajo del 50%).
- Manejo de Zona Muerta: Ignora fluctuaciones menores de voltaje (como ligeras vibraciones de la perilla) para prevenir cambios frecuentes en el LED.
- Controlar LED: Ajusta el brillo del LED a través del ciclo de trabajo PWM (cuanto mayor sea el ciclo de trabajo, más brillante será el LED).
Resultado

UART
Hardware
Seeed Studio XIAO nRF54L15 Sense | L76K GNSS Module for Seeed Studio XIAO |
---|---|
![]() | ![]() |
Software
código
from boards.xiao import XiaoUART
import time
import math
uart = "uart1"
baudrate = 9600
tx = 6 # D6
rx = 7 # D7
# Coordinate structure
class Coordinates:
def __init__(self, Lon=0.0, Lat=0.0):
self.Lon = Lon
self.Lat = Lat
# GPS data structure
class GNRMC:
def __init__(self):
self.Lon = 0.0 # GPS Longitude
self.Lat = 0.0 # GPS Latitude
self.Lon_area = '' # E or W
self.Lat_area = '' # N or S
self.Time_H = 0 # Time Hour
self.Time_M = 0 # Time Minute
self.Time_S = 0 # Time Second
self.Status = 0 # 1: Successful positioning, 0: Positioning failed
# Convert WGS-84 to GCJ-02
def transformLat(x, y):
ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * math.sqrt(abs(x))
ret += (20.0 * math.sin(6.0 * x * pi) + 20.0 * math.sin(2.0 * x * pi)) * 2.0 / 3.0
ret += (20.0 * math.sin(y * pi) + 40.0 * math.sin(y / 3.0 * pi)) * 2.0 / 3.0
ret += (160.0 * math.sin(y / 12.0 * pi) + 320 * math.sin(y * pi / 30.0)) * 2.0 / 3.0
return ret
# Convert WGS-84 to GCJ-02
def transformLon(x, y):
ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * math.sqrt(abs(x))
ret += (20.0 * math.sin(6.0 * x * pi) + 20.0 * math.sin(2.0 * x * pi)) * 2.0 / 3.0
ret += (20.0 * math.sin(x * pi) + 40.0 * math.sin(x / 3.0 * pi)) * 2.0 / 3.0
ret += (150.0 * math.sin(x / 12.0 * pi) + 300.0 * math.sin(x / 30.0 * pi)) * 2.0 / 3.0
return ret
# Convert GCJ-02 to BD-09
def bd_encrypt(gg):
bd = Coordinates()
x = gg.Lon
y = gg.Lat
z = math.sqrt(x * x + y * y) + 0.00002 * math.sin(y * x_pi)
theta = math.atan2(y, x) + 0.000003 * math.cos(x * x_pi)
bd.Lon = z * math.cos(theta) + 0.0065
bd.Lat = z * math.sin(theta) + 0.006
return bd
# Convert WGS-84 to GCJ-02
def transform(gps):
gg = Coordinates()
dLat = transformLat(gps.Lon - 105.0, gps.Lat - 35.0)
dLon = transformLon(gps.Lon - 105.0, gps.Lat - 35.0)
radLat = gps.Lat / 180.0 * pi
magic = math.sin(radLat)
magic = 1 - ee * magic * magic
sqrtMagic = math.sqrt(magic)
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi)
dLon = (dLon * 180.0) / (a / sqrtMagic * math.cos(radLat) * pi)
gg.Lat = gps.Lat + dLat
gg.Lon = gps.Lon + dLon
return gg
# Convert to Baidu coordinates (BD-09)
def L76X_Baidu_Coordinates(gps):
wgs84_coords = Coordinates(gps.Lon, gps.Lat)
gcj02_coords = transform(wgs84_coords)
bd09_coords = bd_encrypt(gcj02_coords)
return bd09_coords
# Convert to Google coordinates (GCJ-02)
def L76X_Google_Coordinates(gps):
wgs84_coords = Coordinates(gps.Lon, gps.Lat)
gcj02_coords = transform(wgs84_coords)
return gcj02_coords
# Parse GNRMC NMEA sentence
def parse_gnrmc(nmea_sentence):
gps = GNRMC()
if not nmea_sentence.startswith(b'$GNRMC') and not nmea_sentence.startswith(b'$PNRMC'):
return gps
try:
# Convert to string and split by commas
sentence_str = nmea_sentence.decode('ascii', 'ignore')
fields = sentence_str.split(',')
if len(fields) < 12:
return gps
# Parse time field (HHMMSS.sss)
if fields[1]:
time_str = fields[1]
if '.' in time_str:
time_str = time_str.split('.')[0]
if len(time_str) >= 6:
gps.Time_H = int(time_str[0:2]) + 8 # GMT+8
gps.Time_M = int(time_str[2:4])
gps.Time_S = int(time_str[4:6])
if gps.Time_H >= 24:
gps.Time_H -= 24
# Parse status
gps.Status = 1 if fields[2] == 'A' else 0
if gps.Status == 1:
# Parse latitude (DDMM.MMMMM)
if fields[3] and fields[4]:
lat_str = fields[3]
if '.' in lat_str:
degrees = float(lat_str[0:2])
minutes = float(lat_str[2:])
gps.Lat = degrees + minutes / 60.0
gps.Lat_area = fields[4]
# Parse longitude (DDDMM.MMMMM)
if fields[5] and fields[6]:
lon_str = fields[5]
if '.' in lon_str:
degrees = float(lon_str[0:3])
minutes = float(lon_str[3:])
gps.Lon = degrees + minutes / 60.0
gps.Lon_area = fields[6]
except Exception as e:
print("Parse error:", e)
return gps
# Print formatted GPS data
def print_gps_data(gps):
print("\n--- GPS Data ---")
print("Time (GMT+8): {:02d}:{:02d}:{:02d}".format(gps.Time_H, gps.Time_M, gps.Time_S))
if gps.Status == 1:
print("Latitude (WGS-84): {:.6f} {}".format(gps.Lat, gps.Lat_area))
print("Longitude (WGS-84): {:.6f} {}".format(gps.Lon, gps.Lon_area))
# Coordinate conversion
baidu_coords = L76X_Baidu_Coordinates(gps)
google_coords = L76X_Google_Coordinates(gps)
print("Baidu Latitude: {:.6f}".format(baidu_coords.Lat))
print("Baidu Longitude: {:.6f}".format(baidu_coords.Lon))
print("Google Latitude: {:.6f}".format(google_coords.Lat))
print("Google Longitude: {:.6f}".format(google_coords.Lon))
print("GPS positioning successful.")
else:
print("GPS positioning failed or no valid data.")
try:
uart = XiaoUART(uart, baudrate, tx, rx)
# Initialize UART
uart.init(9600, bits=8, parity=None, stop=1)
# Buffer to accumulate complete messages
buffer = bytearray()
# Constants for coordinate transformation
pi = 3.14159265358979324
a = 6378245.0
ee = 0.00669342162296594323
x_pi = 3.14159265358979324 * 3000.0 / 180.0
while True:
available = uart.any()
if available > 0:
# Read all available bytes
data = uart.read(available)
buffer.extend(data)
# Check if we have a complete line (ends with newline)
if b'\n' in buffer:
# Find the newline position
newline_pos = buffer.find(b'\n')
# Extract the complete message
complete_message = buffer[:newline_pos + 1]
# Remove the processed part from buffer
buffer = buffer[newline_pos + 1:]
# Parse GNRMC sentences
if complete_message.startswith(b'$GNRMC') or complete_message.startswith(b'$PNRMC'):
gps_data = parse_gnrmc(complete_message)
print_gps_data(gps_data)
except KeyboardInterrupt:
print("\nProgram interrupted by user")
except Exception as e:
print("\nError occurred: %s" % {e})
finally:
uart.deinit()
-
Importar Módulos
XiaoUART
Importa la clase de comunicación UART para la placa de desarrollo Seeed Xiao desde el móduloboards.xiao
, utilizada para inicializar y controlar la comunicación serie.time
Importa el módulo de tiempo para soportar funciones relacionadas con el tiempo (aunque no se usa directamente aquí, se importa para uso futuro potencial o compatibilidad).math
Importa funciones matemáticas (sin
,cos
,sqrt
,atan2
, etc.) requeridas para algoritmos de transformación de coordenadas.
-
Definir Configuración UART
uart = "uart1"
Especifica la instancia del controlador UART a usar — aquí,uart1
.baudrate = 9600
Establece la velocidad de baudios para la comunicación serie a 9600 bps.tx = 6
Especifica que el pin de transmisión UART (TX) está conectado al pin digital D6.rx = 7
Especifica que el pin de recepción UART (RX) está conectado al pin digital D7.
-
Definir Estructuras de Datos
- Clase
Coordinates
: Un contenedor simple para almacenar valores de longitud/latitud como números de punto flotante. - Clase
GNRMC
: Representa datos GPS analizados de una sentencia NMEA$GNRMC
. Contiene:- Latitud/Longitud en grados decimales
- Indicadores de hemisferio (
N/S
,E/W
) - Tiempo (hora, minuto, segundo — ajustado a GMT+8)
- Bandera de estado (1 = posición válida, 0 = sin posición)
- Clase
-
Funciones de Transformación de Coordenadas
transformLat(x, y)
&transformLon(x, y)
— Funciones auxiliares que implementan parte del algoritmo de conversión WGS-84 → GCJ-02 (usado en China para ofuscación de mapas).bd_encrypt(gg)
— Convierte coordenadas GCJ-02 al sistema de coordenadas BD-09 de Baidu aplicando desplazamiento y rotación adicionales.transform(gps)
— Función principal que convierte coordenadas WGS-84 (GPS crudo) a GCJ-02 usando fórmulas trigonométricas complejas basadas en el modelo elíptico de la Tierra.L76X_Baidu_Coordinates(gps)
— Envoltorio que convierte GPS crudo (WGS-84) → GCJ-02 → BD-09 (formato de Mapas Baidu).L76X_Google_Coordinates(gps)
— Envoltorio que convierte GPS crudo (WGS-84) → GCJ-02 (formato de Google Maps en China).
-
Analizar Sentencia GNRMC
parse_gnrmc(nmea_sentence)
— Analiza una cadena NMEA$GNRMC
o$PNRMC
cruda en un objetoGNRMC
estructurado.- Extrae tiempo (convierte de UTC a GMT+8).
- Verifica estado (
A
= activo/posición válida,V
= inválido). - Analiza latitud/longitud desde formato DDMM.MMMMM → grados decimales.
- Devuelve objeto
GNRMC
poblado o uno vacío por defecto si el análisis falla.
-
Mostrar Datos GPS Formateados
print_gps_data(gps)
— Imprime información GPS legible incluyendo:- Tiempo local (GMT+8)
- Coordenadas WGS-84 crudas con hemisferio
- Coordenadas convertidas GCJ-02 (compatible con Google) y BD-09 (compatible con Baidu)
- Mensaje de estado indicando si el posicionamiento tuvo éxito
-
Lógica Principal (bloque try)
- Inicializa la interfaz UART con parámetros especificados.
- Define constantes globales necesarias para matemáticas de coordenadas (
pi
,a
,ee
,x_pi
) — parámetros del elipsoide terrestre y factores de escala. - Entra en bucle infinito para leer continuamente datos GPS entrantes vía UART.
- Usa
buffer
para acumular mensajes parciales hasta que se recibe una línea completa (terminando con\n
). - Cuando llega una línea completa:
- Verifica si comienza con
$GNRMC
o$PNRMC
- Si es así, la analiza usando
parse_gnrmc()
- Muestra salida formateada vía
print_gps_data()
- Verifica si comienza con
- Usa
- Maneja excepciones:
KeyboardInterrupt
: Sale elegantemente con Ctrl+C.Exception
general: Captura e imprime cualquier error inesperado.
- Finalmente, llama a
uart.deinit()
para limpiar recursos UART antes de salir.
Resultado

I2C
Hardware
Seeed Studio XIAO nRF54L15 Sense | Seeed Studio Expansion Board Base for XIAO |
---|---|
![]() | ![]() |
Software
Código
import time
from boards.xiao import XiaoI2C
sda = 4 #D4
scl = 5 #D5
i2c = "i2c0"
frq = 400000
i2c = XiaoI2C(i2c, sda, scl, frq)
# --- SSD1306 I2C address and command definitions ---
SSD1306_I2C_ADDR = 0x3C
SSD1306_SET_CONTRAST = 0x81
SSD1306_DISPLAY_ALL_ON_RESUME = 0xA4
SSD1306_DISPLAY_ALL_ON = 0xA5
SSD1306_NORMAL_DISPLAY = 0xA6
SSD1306_INVERT_DISPLAY = 0xA7
SSD1306_DISPLAY_OFF = 0xAE
SSD1306_DISPLAY_ON = 0xAF
SSD1306_SET_DISPLAY_OFFSET = 0xD3
SSD1306_SET_COM_PINS = 0xDA
SSD1306_SET_VCOM_DETECT = 0xDB
SSD1306_SET_DISPLAY_CLOCK_DIV = 0xD5
SSD1306_SET_PRECHARGE = 0xD9
SSD1306_SET_MULTIPLEX = 0xA8
SSD1306_SET_LOW_COLUMN = 0x00
SSD1306_SET_HIGH_COLUMN = 0x10
SSD1306_SET_START_LINE = 0x40
SSD1306_MEMORY_MODE = 0x20
SSD1306_COLUMN_ADDR = 0x21
SSD1306_PAGE_ADDR = 0x22
SSD1306_COM_SCAN_INC = 0xC0
SSD1306_COM_SCAN_DEC = 0xC8
SSD1306_SEG_REMAP = 0xA0
SSD1306_CHARGE_PUMP = 0x8D
# Display dimensions
SSD1306_WIDTH = 128
SSD1306_HEIGHT = 64
SSD1306_PAGES = 8
# Basic 8x8 font
font_data = {
' ': [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00],
'A': [0x18,0x24,0x42,0x7E,0x42,0x42,0x42,0x00],
'B': [0x7C,0x42,0x42,0x7C,0x42,0x42,0x7C,0x00],
'C': [0x3C,0x42,0x40,0x40,0x40,0x42,0x3C,0x00],
'D': [0x78,0x44,0x42,0x42,0x42,0x44,0x78,0x00],
'E': [0x7C,0x40,0x40,0x78,0x40,0x40,0x7C,0x00],
'F': [0x7C,0x40,0x40,0x78,0x40,0x40,0x40,0x00],
'G': [0x3C,0x42,0x40,0x4E,0x42,0x42,0x3C,0x00],
'H': [0x44,0x44,0x44,0x7C,0x44,0x44,0x44,0x00],
'I': [0x38,0x10,0x10,0x10,0x10,0x10,0x38,0x00],
'J': [0x1C,0x08,0x08,0x08,0x08,0x48,0x30,0x00],
'K': [0x44,0x48,0x50,0x60,0x50,0x48,0x44,0x00],
'L': [0x40,0x40,0x40,0x40,0x40,0x40,0x7C,0x00],
'M': [0x42,0x66,0x5A,0x42,0x42,0x42,0x42,0x00],
'N': [0x42,0x62,0x52,0x4A,0x46,0x42,0x42,0x00],
'O': [0x3C,0x42,0x42,0x42,0x42,0x42,0x3C,0x00],
'P': [0x7C,0x42,0x42,0x7C,0x40,0x40,0x40,0x00],
'Q': [0x3C,0x42,0x42,0x42,0x4A,0x44,0x3A,0x00],
'R': [0x7C,0x42,0x42,0x7C,0x48,0x44,0x42,0x00],
'S': [0x3C,0x42,0x40,0x3C,0x02,0x42,0x3C,0x00],
'T': [0x7C,0x10,0x10,0x10,0x10,0x10,0x10,0x00],
'U': [0x42,0x42,0x42,0x42,0x42,0x42,0x3C,0x00],
'V': [0x42,0x42,0x42,0x42,0x42,0x24,0x18,0x00],
'W': [0x42,0x42,0x42,0x42,0x5A,0x66,0x42,0x00],
'X': [0x42,0x24,0x18,0x18,0x18,0x24,0x42,0x00],
'Y': [0x44,0x44,0x28,0x10,0x10,0x10,0x10,0x00],
'Z': [0x7E,0x04,0x08,0x10,0x20,0x40,0x7E,0x00],
'0': [0x3C,0x42,0x46,0x4A,0x52,0x62,0x3C,0x00],
'1': [0x10,0x30,0x10,0x10,0x10,0x10,0x38,0x00],
'2': [0x3C,0x42,0x02,0x0C,0x30,0x40,0x7E,0x00],
'3': [0x3C,0x42,0x02,0x1C,0x02,0x42,0x3C,0x00],
'4': [0x08,0x18,0x28,0x48,0x7E,0x08,0x08,0x00],
'5': [0x7E,0x40,0x7C,0x02,0x02,0x42,0x3C,0x00],
'6': [0x1C,0x20,0x40,0x7C,0x42,0x42,0x3C,0x00],
'7': [0x7E,0x42,0x04,0x08,0x10,0x10,0x10,0x00],
'8': [0x3C,0x42,0x42,0x3C,0x42,0x42,0x3C,0x00],
'9': [0x3C,0x42,0x42,0x3E,0x02,0x04,0x38,0x00],
'!': [0x10,0x10,0x10,0x10,0x10,0x00,0x10,0x00],
'?': [0x3C,0x42,0x02,0x0C,0x10,0x00,0x10,0x00],
'.': [0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00],
',': [0x00,0x00,0x00,0x00,0x00,0x10,0x10,0x20],
':': [0x00,0x10,0x00,0x00,0x00,0x10,0x00,0x00],
';': [0x00,0x10,0x00,0x00,0x00,0x10,0x10,0x20],
'-': [0x00,0x00,0x00,0x7C,0x00,0x00,0x00,0x00],
'_': [0x00,0x00,0x00,0x00,0x00,0x00,0x7E,0x00],
'+': [0x00,0x10,0x10,0x7C,0x10,0x10,0x00,0x00],
'*': [0x00,0x24,0x18,0x7E,0x18,0x24,0x00,0x00],
'/': [0x02,0x04,0x08,0x10,0x20,0x40,0x00,0x00],
'\\': [0x40,0x20,0x10,0x08,0x04,0x02,0x00,0x00],
'=': [0x00,0x00,0x7E,0x00,0x7E,0x00,0x00,0x00],
'\'': [0x10,0x10,0x20,0x00,0x00,0x00,0x00,0x00],
'"': [0x24,0x24,0x00,0x00,0x00,0x00,0x00,0x00],
'(': [0x08,0x10,0x20,0x20,0x20,0x10,0x08,0x00],
')': [0x20,0x10,0x08,0x08,0x08,0x10,0x20,0x00],
'[': [0x1C,0x10,0x10,0x10,0x10,0x10,0x1C,0x00],
']': [0x38,0x08,0x08,0x08,0x08,0x08,0x38,0x00],
'{': [0x0C,0x10,0x10,0x60,0x10,0x10,0x0C,0x00],
'}': [0x30,0x08,0x08,0x06,0x08,0x08,0x30,0x00],
'<': [0x08,0x10,0x20,0x40,0x20,0x10,0x08,0x00],
'>': [0x20,0x10,0x08,0x04,0x08,0x10,0x20,0x00],
'|': [0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x00],
'@': [0x3C,0x42,0x5A,0x5A,0x5C,0x40,0x3C,0x00],
'#': [0x24,0x24,0x7E,0x24,0x7E,0x24,0x24,0x00],
'$': [0x10,0x3C,0x50,0x3C,0x12,0x3C,0x10,0x00],
'%': [0x62,0x64,0x08,0x10,0x26,0x46,0x00,0x00],
'^': [0x10,0x28,0x44,0x00,0x00,0x00,0x00,0x00],
'&': [0x30,0x48,0x50,0x20,0x54,0x48,0x34,0x00],
'~': [0x00,0x00,0x34,0x4C,0x00,0x00,0x00,0x00]
}
# --- Helper functions ---
# Write a single command byte to SSD1306 via I2C
def ssd1306_write_command(cmd):
i2c.writeto(SSD1306_I2C_ADDR, bytes([0x00, cmd]))
# Write multiple command bytes to SSD1306 via I2C
def ssd1306_write_commands(cmds):
data = bytearray([0x00] + list(cmds))
i2c.writeto(SSD1306_I2C_ADDR, data)
# Write display data bytes to SSD1306 via I2C
def ssd1306_write_data(data):
buffer = bytearray(len(data) + 1)
buffer[0] = 0x40
buffer[1:] = data
i2c.writeto(SSD1306_I2C_ADDR, buffer)
# Clear the entire SSD1306 display
def ssd1306_clear():
ssd1306_write_commands(bytearray([SSD1306_COLUMN_ADDR, 0, SSD1306_WIDTH - 1]))
ssd1306_write_commands(bytearray([SSD1306_PAGE_ADDR, 0, SSD1306_PAGES - 1]))
empty_data = bytearray(SSD1306_WIDTH)
for _ in range(SSD1306_PAGES):
ssd1306_write_data(empty_data)
ssd1306_write_commands([SSD1306_COLUMN_ADDR, 0, SSD1306_WIDTH - 1])
# Initialize SSD1306 display with recommended settings
def ssd1306_init():
commands = [
bytearray([SSD1306_DISPLAY_OFF]),
bytearray([SSD1306_SET_DISPLAY_CLOCK_DIV, 0x80]),
bytearray([SSD1306_SET_MULTIPLEX, SSD1306_HEIGHT - 1]),
bytearray([SSD1306_SET_DISPLAY_OFFSET, 0x00]),
bytearray([SSD1306_SET_START_LINE | 0x00]),
bytearray([SSD1306_CHARGE_PUMP, 0x14]),
bytearray([SSD1306_MEMORY_MODE, 0x00]),
bytearray([SSD1306_SEG_REMAP | 0x01]),
bytearray([SSD1306_COM_SCAN_DEC]),
bytearray([SSD1306_SET_COM_PINS, 0x12]),
bytearray([SSD1306_SET_CONTRAST, 0xCF]),
bytearray([SSD1306_SET_PRECHARGE, 0xF1]),
bytearray([SSD1306_SET_VCOM_DETECT, 0x40]),
bytearray([SSD1306_DISPLAY_ALL_ON_RESUME]),
bytearray([SSD1306_NORMAL_DISPLAY]),
bytearray([SSD1306_DISPLAY_ON])
]
for cmd in commands:
ssd1306_write_commands(cmd)
ssd1306_clear()
print("SSD1306 initialized successfully.")
ssd1306_write_commands([SSD1306_COLUMN_ADDR, 0, SSD1306_WIDTH - 1])
# Draw a string of text at specified column and page (row) on SSD1306
def ssd1306_draw_text(text, x, y):
ssd1306_write_commands(bytearray([SSD1306_COLUMN_ADDR, x, x + len(text) * 8 - 1]))
ssd1306_write_commands(bytearray([SSD1306_PAGE_ADDR, y, y + 0]))
display_data = bytearray()
for char in text:
font_bytes = font_data.get(char.upper(), font_data[' '])
for col in range(7, -1, -1):
val = 0
for row in range(8):
if font_bytes[row] & (1 << col):
val |= (1 << row)
display_data.append(val)
ssd1306_write_data(display_data)
i2c_addr = i2c.scan()
if SSD1306_I2C_ADDR not in i2c_addr:
raise Exception("SSD1306 not found on I2C bus")
else:
print("SSD1306 found on I2C bus: 0x{:02X}".format(SSD1306_I2C_ADDR))
# Initialize display
ssd1306_init()
ssd1306_draw_text("NRF54L15", 30, 2)
ssd1306_draw_text("HELLO WORLD", 20, 4)
Explicación del Código:
-
Importar Módulos
time
Importa el módulo time para habilitar funciones relacionadas con el tiempo como retrasos.XiaoI2C
Importa la clase de comunicación I2C para la placa de desarrollo Seeed Xiao desde el móduloboards.xiao
, utilizada para inicializar y controlar periféricos I2C.
-
Definir Configuración I2C
sda = 4
Especifica que la línea SDA (datos) del bus I2C está conectada al pin digital D4.scl = 5
Especifica que la línea SCL (reloj) del bus I2C está conectada al pin digital D5.i2c = "i2c0"
Especifica la instancia del controlador I2C a usar — aquí, esi2c0
.frq = 400000
Establece la frecuencia del bus I2C a 400 kHz (modo rápido estándar).i2c = XiaoI2C(i2c, sda, scl, frq)
Inicializa la interfaz I2C con los parámetros especificados.
-
Definir Constantes SSD1306
SSD1306_I2C_ADDR = 0x3C
La dirección I2C predeterminada de la pantalla OLED SSD1306.- Varias constantes de comando (
SSD1306_SET_CONTRAST
,SSD1306_DISPLAY_ON
, etc.) definen comandos de control para configurar y controlar el hardware de la pantalla. SSD1306_WIDTH = 128
,SSD1306_HEIGHT = 64
,SSD1306_PAGES = 8
Definen la resolución de la pantalla y la estructura de páginas (cada página tiene 8 filas de altura).
-
Definir Datos de Fuente
font_data
Un diccionario que mapea caracteres ASCII a sus representaciones de mapa de bits de píxeles de 8x8. Cada carácter se representa como una lista de 8 bytes, donde cada byte corresponde a una fila de píxeles (LSB = píxel más a la izquierda).
-
Funciones Auxiliares
ssd1306_write_command(cmd)
Envía un solo byte de comando al SSD1306 vía I2C usando el byte de control0x00
.ssd1306_write_commands(cmds)
Envía múltiples bytes de comando en una transacción.ssd1306_write_data(data)
Envía bytes de datos de pantalla al SSD1306 usando el byte de control0x40
(modo de datos).ssd1306_clear()
Limpia toda la pantalla escribiendo bytes cero a todas las páginas y columnas.ssd1306_init()
Inicializa la pantalla SSD1306 con configuraciones recomendadas incluyendo contraste, relación multiplex, modo de memoria, y encender la pantalla.ssd1306_draw_text(text, x, y)
Dibuja texto comenzando en la columnax
y páginay
. Convierte cada carácter a su mapa de bits de fuente 8x8, lo rota 90° en sentido horario (para coincidir con la orientación de la pantalla), y escribe los datos de píxeles al buffer de la pantalla.
-
Lógica Principal (Inicialización y Pantalla)
i2c.scan()
Escanea el bus I2C para detectar dispositivos conectados.- Si el SSD1306 no se encuentra en la dirección
0x3C
, se genera una excepción; de lo contrario, se imprime un mensaje de éxito. ssd1306_init()
Inicializa el hardware de la pantalla.ssd1306_draw_text("NRF54L15", 30, 2)
Dibuja la cadena "NRF54L15" comenzando en la columna 30, página 2 (≈ fila 16).ssd1306_draw_text("HELLO WORLD", 20, 4)
Dibuja la cadena "HELLO WORLD" comenzando en la columna 20, página 4 (≈ fila 32).
Resultado

SPI
Hardware
Seeed Studio XIAO nRF54L15 Sense | Placa Controladora ePaper para Seeed Studio XIAO |
---|---|
![]() | ![]() |
Software
Código
import time
from boards.xiao import XiaoPin, XiaoSPI
# -------- Pins & SPI --------
RST = 0; CS = 1; DC = 3; BUSY = 5
sck = 9; mosi = 10; miso = 8; spi_id = "spi0"
RST = XiaoPin(RST, XiaoPin.OUT)
CS = XiaoPin(CS, XiaoPin.OUT)
DC = XiaoPin(DC, XiaoPin.OUT)
BUSY = XiaoPin(BUSY, XiaoPin.IN, XiaoPin.PULL_UP)
spi = XiaoSPI(spi_id, 20_000_000, sck, mosi, miso)
# -------- ePaper basics --------
def reset():
RST.value(0); time.sleep_ms(10)
RST.value(1); time.sleep_ms(10)
def send_command(cmd):
DC.value(0); CS.value(0)
spi.write(bytearray([cmd & 0xFF]))
CS.value(1)
def send_data(data):
DC.value(1); CS.value(0)
if isinstance(data, int):
spi.write(bytearray([data & 0xFF]))
else:
spi.write(data)
CS.value(1)
def wait_until_idle():
# If BUSY = 0, it indicates that the device is busy. You can then switch back to polling.
# while BUSY.value() == 0: time.sleep_ms(1)
time.sleep_ms(1)
def init_display():
reset()
send_command(0x00); send_data(0x1F)
send_command(0x04); time.sleep_ms(100); wait_until_idle()
send_command(0x50); send_data(0x21); send_data(0x07)
def clear_screen():
CS.value(0)
DC.value(0); spi.write(b'\x10'); DC.value(1)
for _ in range(48000): spi.write(b'\xFF')
DC.value(0); spi.write(b'\x13'); DC.value(1)
for _ in range(48000): spi.write(b'\xFF')
DC.value(0); spi.write(b'\x12'); CS.value(1)
wait_until_idle()
# -------- Geometry --------
WIDTH, HEIGHT = 800, 480
BYTES_PER_ROW = WIDTH // 8
linebuf = bytearray(BYTES_PER_ROW)
# -------- Minimal 5x7 glyphs (columns, LSB=top) --------
FONT_W, FONT_H = 5, 7
G = {
' ':[0x00,0x00,0x00,0x00,0x00],
# Digits
'0':[0x3E,0x51,0x49,0x45,0x3E],
'1':[0x00,0x42,0x7F,0x40,0x00],
'2':[0x42,0x61,0x51,0x49,0x46],
'3':[0x21,0x41,0x45,0x4B,0x31],
'4':[0x18,0x14,0x12,0x7F,0x10],
'5':[0x27,0x45,0x45,0x45,0x39],
'6':[0x3C,0x4A,0x49,0x49,0x30],
'7':[0x01,0x71,0x09,0x05,0x03],
'8':[0x36,0x49,0x49,0x49,0x36],
'9':[0x06,0x49,0x49,0x29,0x1E],
# Uppercase
'A':[0x7E,0x11,0x11,0x11,0x7E],
'F':[0x7F,0x09,0x09,0x09,0x01],
'H':[0x7F,0x08,0x08,0x08,0x7F],
'I':[0x00,0x41,0x7F,0x41,0x00],
'L':[0x7F,0x40,0x40,0x40,0x40],
'M':[0x7F,0x02,0x0C,0x02,0x7F],
'O':[0x3E,0x41,0x41,0x41,0x3E],
'P':[0x7F,0x09,0x09,0x09,0x06],
'R':[0x7F,0x09,0x19,0x29,0x46],
'T':[0x01,0x01,0x7F,0x01,0x01],
'X':[0x63,0x14,0x08,0x14,0x63],
'Y':[0x07,0x08,0x70,0x08,0x07],
# Lowercase
'a':[0x20,0x54,0x54,0x54,0x78],
'c':[0x38,0x44,0x44,0x44,0x20],
'e':[0x38,0x54,0x54,0x54,0x18],
'h':[0x7F,0x08,0x04,0x04,0x78],
'i':[0x00,0x44,0x7D,0x40,0x00],
'l':[0x00,0x41,0x7F,0x40,0x00],
'n':[0x7C,0x08,0x04,0x04,0x78],
'o':[0x38,0x44,0x44,0x44,0x38],
'p':[0x7C,0x14,0x14,0x14,0x08],
'r':[0x7C,0x08,0x04,0x04,0x08],
't':[0x04,0x3F,0x44,0x40,0x20],
'y':[0x0C,0x50,0x50,0x50,0x3C],
}
def glyph(ch):
return G.get(ch, G[' '])
# -------- Text helpers --------
def text_size(text, scale=1, spacing=1):
w = 0
for _ in text:
w += (FONT_W * scale + spacing)
if w: w -= spacing
return w, FONT_H * scale
def text_pixel(x, y, text, sx, sy, scale=1, spacing=1):
# Return 0 = Black, 1 = White
if y < sy or y >= sy + FONT_H * scale:
return 1
lx = x - sx
if lx < 0:
return 1
cursor = 0
for ch in text:
cw = FONT_W * scale
if cursor <= lx < cursor + cw:
cx_scaled = lx - cursor
cy_scaled = y - sy
cx = cx_scaled // scale
cy = cy_scaled // scale
col = glyph(ch)[cx]
bit = (col >> cy) & 1
return 0 if bit else 1
cursor += cw + spacing
return 1
# -------- Stream update --------
def epaper_update_lines(lines):
CS.value(0)
# The old picture is completely white.
DC.value(0); spi.write(b'\x10'); DC.value(1)
for _ in range(HEIGHT * BYTES_PER_ROW):
spi.write(b'\xFF')
# New image: Generated row by row
DC.value(0); spi.write(b'\x13'); DC.value(1)
for y in range(HEIGHT):
bi = 0; bitpos = 7; linebuf[:] = b'\x00' * BYTES_PER_ROW
for x in range(WIDTH):
val = 1 # Default white
for (txt, tx, ty, scale) in lines:
if text_pixel(x, y, txt, tx, ty, scale) == 0:
val = 0
break
if val:
linebuf[bi] |= (1 << bitpos) # 1 = white
bitpos -= 1
if bitpos < 0:
bitpos = 7; bi += 1
spi.write(linebuf)
# Redresh
DC.value(0); spi.write(b'\x12'); CS.value(1)
wait_until_idle()
# -------- Main --------
LINE1 = "XIAO nRF541L15"
LINE2 = "Hello MicroPython"
SCALE1 = 3
SCALE2 = 3
def main():
init_display()
clear_screen()
# Centered layout
w1, h1 = text_size(LINE1, SCALE1)
w2, h2 = text_size(LINE2, SCALE2)
total_h = h1 + 12 + h2 # Line spacing: 12 px
y0 = (HEIGHT - total_h) // 2
x1 = (WIDTH - w1) // 2
x2 = (WIDTH - w2) // 2
y1 = y0
y2 = y0 + h1 + 12
lines = [
(LINE1, x1, y1, SCALE1),
(LINE2, x2, y2, SCALE2),
]
epaper_update_lines(lines)
while True:
time.sleep(1_000_000)
if __name__ == "__main__":
main()
Explicación del Código:
-
Importación de Módulos
time
: Habilita funciones relacionadas con el tiempo como retrasos.XiaoPin y XiaoSPI
: Importados desdeboards.xiao
; XiaoPin se usa para controlar pines GPIO, mientras que XiaoSPI maneja la comunicación SPI.
-
Configuración de Pines y SPI
- Definió pines específicos: Reset (RST), Chip Select (CS), Data/Command (DC), y Busy (BUSY).
- Configuró pines relacionados con SPI (SCK, MOSI, MISO) y el controlador SPI.
- Inicializó el modo de trabajo (entrada/salida) para todos los pines GPIO.
- Creó una instancia SPI con una frecuencia establecida de 20 MHz.
-
Funciones Básicas de ePaper
reset()
: Ejecuta una operación de reinicio por hardware en la pantalla.send_command(cmd)
: Transmite un comando de un solo byte.send_data(data)
: Envía datos, que pueden ser un solo byte o múltiples bytes.wait_until_idle()
: Espera a que la pantalla entre en estado inactivo (actualmente implementado con un retraso simple).init_display()
: Realiza procedimientos de inicialización para la pantalla.clear_screen()
: Limpia la pantalla, estableciéndola en un estado completamente blanco.
-
Parámetros de Pantalla
WIDTH, HEIGHT = 800, 480
: Especifica la resolución de la pantalla.BYTES_PER_ROW
: Indica el número de bytes necesarios para cada fila de píxeles.linebuf
: Un búfer de línea que almacena temporalmente datos de píxeles para una sola fila.
-
Sistema de Fuentes
- Definió una fuente simple de píxeles 5x7, almacenada en el diccionario
G
. glyph(ch)
: Recupera los datos de píxeles correspondientes a un carácter dado.text_size()
: Calcula las dimensiones del texto cuando se muestra a una relación de escala especificada.text_pixel()
: Determina si un píxel debe dibujarse en una posición específica (usado en renderizado de texto).
- Definió una fuente simple de píxeles 5x7, almacenada en el diccionario
-
Actualización de Pantalla
- epaper_update_lines(lines): La función principal para actualizar la pantalla.
- Primero, envía datos para establecer un fondo completamente blanco.
- Luego, calcula y transmite nuevos datos de imagen fila por fila.
- Finalmente, activa una actualización de pantalla para mostrar el nuevo contenido.
- Soporta visualización de texto multilínea, donde cada línea puede tener posiciones y relaciones de escala distintas.
-
función main()
- Inicializa la pantalla.
- Calcula la posición centrada para el texto.
- Crea una lista de configuración para las líneas de texto.
- Llama a
epaper_update_lines()
para actualizar el contenido de la pantalla. - Entra en un bucle de suspensión infinito.
Resultado

Ejecutar automáticamente el programa
Si desea que su programa de abreviación pueda ejecutarse automáticamente, puede seguir estos pasos:
Paso 1. Cree un nuevo archivo de programa y use Ctrl + S para guardarlo en la memoria flash del dispositivo MicroPython, y nómbrelo main.py.
Tomemos el programa de parpadeo como ejemplo aquí

Luego se mostrará bajo la sección dispositivo MicroPython/flash.

Paso 2. Presionando el botón Reset integrado, se puede lograr el efecto de ejecución automática.
efecto:

Soporte Técnico y Discusión de Productos
¡Gracias por elegir nuestros productos! Estamos aquí para brindarle diferentes tipos de soporte para asegurar que su experiencia con nuestros productos sea lo más fluida posible. Ofrecemos varios canales de comunicación para satisfacer diferentes preferencias y necesidades.