Seeed Studio XIAO nRF54L15 Sense 的 MicroPython
本教程旨在介绍如何在基于 XIAO nRF54L15 的 Thonny 上使用 MicroPython。
MicroPython 是一个具有部分本机代码编译功能的 Python 解释器。它提供了 Python 3.5 功能的子集,专为嵌入式处理器和受限系统实现。它与 CPython 不同,您可以在这里了解更多差异。
使用 Thonny IDE
准备一个 XIAO nRF54L15。
Seeed Studio XIAO nRF54L15 Sense |
---|
![]() |
安装 Thonny IDE
选择适合的版本进行安装。这里我在 Windows 系统上安装,所以选择了 Windows 版本。
按照所需 Python 版本的说明进行操作。

然后,只需按照默认步骤进行配置即可。
下载仓库
将其克隆到本地机器,然后记住存储此 XIAO nRF54L15 的 MicroPython 的路径。此路径稍后会用到。
git clone https://github.com/Seeed-Studio/micropython-seeed-boards.git
上传板文件
步骤 1. 为 XIAO nRF54L15 刷入 MicroPython 固件
-
下载固件包并将其解压到适当位置。然后点击 flash.bat,它会自动为您刷入固件。

此脚本已预配置刷写工具链命令。如果您是第一次使用,可能需要一点时间。
步骤 2. 打开 Thonny IDE,然后点击界面右下角配置解释器选项。选择 MicroPython (generic) 和端口

此固件已预设刷写工具链指令。如果这是您第一次使用,可能需要一点时间。
步骤 3. 上传板文件
- 打开视图,选择"文件",文件管理器路径将显示在左侧边栏。

- 打开克隆或下载文件的路径,并打开
micropython-seeed-boards\examples
- 选择"boards"文件夹并将其上传到闪存。然后,您将能够在 MicroPython 设备/闪存上看到上传的文件。

步骤 4. 点亮 LED
复制代码并按 F5
运行。
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)
结果如下:

数字
硬件
Seeed Studio XIAO nRF54L15 Sense | Seeed Studio XIAO 扩展板配 Grove OLED | Grove - 继电器 |
---|---|---|
![]() | ![]() | ![]() |
软件
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()
代码解释:
-
导入模块
time
导入时间模块Xiao Pin
从 boards.xiao 模块导入 Seeed Xiao 开发板的引脚控制类,用于操作板上的引脚。
-
定义引脚
button = "sw"
指定按钮连接到开发板的"sw"引脚(这里是 BOOT 引脚) -relay = 0
指定继电器连接到数字引脚 D0。
-
主逻辑(try 块)
- 当按钮被按下时(状态为 0)→ 继电器被激活(输出为 1)。
- 当按钮未被按下时(状态为 1)→ 继电器被停用(输出为 0)。
结果

模拟
硬件
Seeed Studio XIAO nRF54L15 Sense | Grove-Variable Color LED | Grove-Rotary Angle Sensor | Seeed Studio Grove Base for XIAO |
---|---|---|---|
![]() | ![]() | ![]() | ![]() |
软件
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()
代码说明:
-
导入依赖库
time
:用于添加延迟和控制程序的执行节奏。boards.xiao
:导入 Xiao 开发板的硬件控制类,包括:XiaoADC
:用于读取模拟信号(如电位器输出)。XiaoPWM
:用于生成 PWM 信号(控制 LED 亮度)。
-
定义硬件引脚
adc
对应开发板的 D0 引脚(用于连接电位器的输出),pwm
对应 D1 引脚(用于连接 LED)。
-
初始化硬件(try 块)
XiaoADC(adc)
:将 D0 引脚初始化为 ADC 输入模式,用于读取电位器的电压信号。XiaoPWM(pwm)
:将 D1 引脚初始化为 PWM 输出模式,用于控制 LED。PWM 参数
:频率为 1000Hz 意味着信号周期为 1 毫秒(1e6 纳秒)。占空比(高电平持续时间占周期的比例)决定 LED 亮度(占空比越高,LED 越亮)。
-
主循环(核心逻辑)
- 读取电压:通过 ADC 读取电位器输出的电压(转换单位为伏特)。
- 范围限制:确保电压在 0~3.3V 范围内(硬件安全范围)。
- 占空比计算:将电压线性转换为 0~1 的占空比(例如,1.65V 对应 50% 占空比)。
- 死区处理:忽略微小的电压波动(如旋钮的轻微抖动),防止 LED 频繁变化。
- 控制 LED:通过 PWM 占空比调节 LED 亮度(占空比越高,LED 越亮)。
结果

UART
硬件
软件
代码
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()
-
导入模块
XiaoUART
从boards.xiao
模块导入适用于 Seeed Xiao 开发板的 UART 通信类,用于初始化和控制串行通信。time
导入时间模块以支持与时间相关的功能(虽然在这里没有直接使用,但为了潜在的未来使用或兼容性而导入)。math
导入坐标转换算法所需的数学函数(sin
、cos
、sqrt
、atan2
等)。
-
定义 UART 配置
uart = "uart1"
指定要使用的 UART 控制器实例——这里是uart1
。baudrate = 9600
将串行通信的波特率设置为 9600 bps。tx = 6
指定 UART 发送引脚(TX)连接到数字引脚 D6。rx = 7
指定 UART 接收引脚(RX)连接到数字引脚 D7。
-
定义数据结构
Coordinates
类:用于存储经度/纬度值作为浮点数的简单容器。GNRMC
类:表示从$GNRMC
NMEA 语句解析的 GPS 数据。包含:- 十进制度数的纬度/经度
- 半球指示符(
N/S
、E/W
) - 时间(小时、分钟、秒——调整为 GMT+8)
- 状态标志(1 = 有效定位,0 = 无定位)
-
坐标转换函数
transformLat(x, y)
和transformLon(x, y)
— 实现 WGS-84 → GCJ-02 转换算法部分的辅助函数(在中国用于地图混淆)。bd_encrypt(gg)
— 通过应用额外的偏移和旋转将 GCJ-02 坐标转换为百度的 BD-09 坐标系。transform(gps)
— 使用基于椭球地球模型的复杂三角函数公式将 WGS-84(原始 GPS)坐标转换为 GCJ-02 的主函数。L76X_Baidu_Coordinates(gps)
— 将原始 GPS(WGS-84)→ GCJ-02 → BD-09(百度地图格式)转换的包装器。L76X_Google_Coordinates(gps)
— 将原始 GPS(WGS-84)→ GCJ-02(中国的谷歌地图格式)转换的包装器。
-
解析 GNRMC 语句
parse_gnrmc(nmea_sentence)
— 将原始 NMEA$GNRMC
或$PNRMC
字符串解析为结构化的GNRMC
对象。- 提取时间(从 UTC 转换为 GMT+8)。
- 检查状态(
A
= 活动/有效定位,V
= 无效)。 - 从 DDMM.MMMMM 格式解析纬度/经度 → 十进制度数。
- 返回填充的
GNRMC
对象,如果解析失败则返回默认空对象。
-
显示格式化的 GPS 数据
print_gps_data(gps)
— 打印人类可读的 GPS 信息,包括:- 本地时间(GMT+8)
- 带半球的原始 WGS-84 坐标
- 转换后的 GCJ-02(谷歌兼容)和 BD-09(百度兼容)坐标
- 指示定位是否成功的状态消息
-
主逻辑(try 块)
- 使用指定参数初始化 UART 接口。
- 定义坐标数学所需的全局常量(
pi
、a
、ee
、x_pi
)——地球椭球参数和缩放因子。 - 进入无限循环以通过 UART 持续读取传入的 GPS 数据。
- 使用
buffer
累积部分消息,直到接收到完整行(以\n
结尾)。 - 当完整行到达时:
- 检查是否以
$GNRMC
或$PNRMC
开头 - 如果是,使用
parse_gnrmc()
解析 - 通过
print_gps_data()
显示格式化输出
- 检查是否以
- 使用
- 处理异常:
KeyboardInterrupt
:在 Ctrl+C 时优雅退出。- 一般
Exception
:捕获并打印任何意外错误。
- 最后,调用
uart.deinit()
在退出前清理 UART 资源。
结果

I2C
硬件
软件
代码
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)
代码说明:
-
导入模块
time
导入时间模块以启用与时间相关的功能,如延迟。XiaoI2C
从boards.xiao
模块导入适用于 Seeed Xiao 开发板的 I2C 通信类,用于初始化和控制 I2C 外设。
-
定义 I2C 配置
sda = 4
指定 I2C 总线的 SDA(数据)线连接到数字引脚 D4。scl = 5
指定 I2C 总线的 SCL(时钟)线连接到数字引脚 D5。i2c = "i2c0"
指定要使用的 I2C 控制器实例——这里是i2c0
。frq = 400000
将 I2C 总线频率设置为 400 kHz(标准快速模式)。i2c = XiaoI2C(i2c, sda, scl, frq)
使用指定参数初始化 I2C 接口。
-
定义 SSD1306 常量
SSD1306_I2C_ADDR = 0x3C
SSD1306 OLED 显示屏的默认 I2C 地址。- 各种命令常量(
SSD1306_SET_CONTRAST
、SSD1306_DISPLAY_ON
等)定义了用于配置和控制显示硬件的控制命令。 SSD1306_WIDTH = 128
、SSD1306_HEIGHT = 64
、SSD1306_PAGES = 8
定义显示分辨率和页面结构(每页高 8 行)。
-
定义字体数据
font_data
一个将 ASCII 字符映射到其 8x8 像素位图表示的字典。每个字符表示为 8 个字节的列表,其中每个字节对应一行像素(LSB = 最左侧像素)。
-
辅助函数
ssd1306_write_command(cmd)
通过 I2C 使用控制字节0x00
向 SSD1306 发送单个命令字节。ssd1306_write_commands(cmds)
在一次事务中发送多个命令字节。ssd1306_write_data(data)
使用控制字节0x40
(数据模式)向 SSD1306 发送显示数据字节。ssd1306_clear()
通过向所有页面和列写入零字节来清除整个显示。ssd1306_init()
使用推荐设置初始化 SSD1306 显示,包括对比度、多路复用比、内存模式和打开显示。ssd1306_draw_text(text, x, y)
从列x
和页面y
开始绘制文本。它将每个字符转换为其 8x8 字体位图,顺时针旋转 90°(以匹配显示方向),并将像素数据写入显示缓冲区。
-
主逻辑(初始化和显示)
i2c.scan()
扫描 I2C 总线以检测连接的设备。- 如果在地址
0x3C
处未找到 SSD1306,则抛出异常;否则打印成功消息。 ssd1306_init()
初始化显示硬件。ssd1306_draw_text("NRF54L15", 30, 2)
从列 30、页面 2(≈ 第 16 行)开始绘制字符串 "NRF54L15"。ssd1306_draw_text("HELLO WORLD", 20, 4)
从列 20、页面 4(≈ 第 32 行)开始绘制字符串 "HELLO WORLD"。
结果

SPI
硬件
软件
代码
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()
代码说明:
-
模块导入
time
:启用时间相关功能,如延时。XiaoPin 和 XiaoSPI
:从boards.xiao
导入;XiaoPin 用于控制 GPIO 引脚,而 XiaoSPI 处理 SPI 通信。
-
引脚和 SPI 配置
- 定义了特定引脚:复位 (RST)、片选 (CS)、数据/命令 (DC) 和忙碌 (BUSY)。
- 配置了 SPI 相关引脚 (SCK、MOSI、MISO) 和 SPI 控制器。
- 初始化了所有 GPIO 引脚的工作模式(输入/输出)。
- 创建了一个频率设置为 20 MHz 的 SPI 实例。
-
ePaper 基本功能
reset()
:对显示器执行硬件复位操作。send_command(cmd)
:传输单字节命令。send_data(data)
:发送数据,可以是单字节或多字节。wait_until_idle()
:等待显示器进入空闲状态(目前通过简单延时实现)。init_display()
:执行显示器的初始化程序。clear_screen()
:清除屏幕,将其设置为全白状态。
-
显示参数
WIDTH, HEIGHT = 800, 480
:指定显示器的分辨率。BYTES_PER_ROW
:表示每行像素所需的字节数。linebuf
:临时存储单行像素数据的行缓冲区。
-
字体系统
- 定义了一个简单的 5x7 像素字体,存储在
G
字典中。 glyph(ch)
:检索给定字符对应的像素数据。text_size()
:计算文本在指定缩放比例下显示时的尺寸。text_pixel()
:确定特定位置是否应绘制像素(用于文本渲染)。
- 定义了一个简单的 5x7 像素字体,存储在
-
显示更新
- epaper_update_lines(lines):更新显示器的核心功能。
- 首先,发送数据设置全白背景。
- 然后,逐行计算并传输新的图像数据。
- 最后,触发显示刷新以显示新内容。
- 支持多行文本显示,每行可以有不同的位置和缩放比例。
-
main() 函数
- 初始化显示器。
- 计算文本的居中位置。
- 创建文本行的配置列表。
- 调用
epaper_update_lines()
更新显示内容。 - 进入无限睡眠循环。
结果

自动执行程序
如果您希望您的程序能够自动执行,可以按照以下步骤操作:
步骤 1. 创建一个新的程序文件,使用 Ctrl + S 将其保存到 MicroPython 设备的闪存中,并命名为 main.py。
这里以闪烁程序为例

然后它将显示在 MicroPython 设备/闪存部分下。

步骤 2. 通过按下板载复位按钮,可以实现自动执行效果。
效果:

技术支持与产品讨论
感谢您选择我们的产品!我们在这里为您提供不同的支持,以确保您使用我们产品的体验尽可能顺畅。我们提供多种沟通渠道,以满足不同的偏好和需求。