Skip to main content

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

Thonny IDE

选择适合的版本进行安装。这里我在 Windows 系统上安装,所以选择了 Windows 版本。

按照所需 Python 版本的说明进行操作。

然后,只需按照默认步骤进行配置即可。

下载仓库


将其克隆到本地机器,然后记住存储此 XIAO nRF54L15 的 MicroPython 的路径。此路径稍后会用到。

git clone https://github.com/Seeed-Studio/micropython-seeed-boards.git

上传板文件

步骤 1. 为 XIAO nRF54L15 刷入 MicroPython 固件

tip

此脚本已预配置刷写工具链命令。如果您是第一次使用,可能需要一点时间。

步骤 2. 打开 Thonny IDE,然后点击界面右下角配置解释器选项。选择 MicroPython (generic) 和端口

tip

此固件已预设刷写工具链指令。如果这是您第一次使用,可能需要一点时间。

步骤 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 SenseSeeed Studio XIAO 扩展板配 Grove OLEDGrove - 继电器

软件


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 SenseGrove-Variable Color LEDGrove-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

硬件

Seeed Studio XIAO nRF54L15 SenseL76K GNSS Module for Seeed Studio XIAO

软件


代码
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()
  • 导入模块

    • XiaoUARTboards.xiao 模块导入适用于 Seeed Xiao 开发板的 UART 通信类,用于初始化和控制串行通信。
    • time 导入时间模块以支持与时间相关的功能(虽然在这里没有直接使用,但为了潜在的未来使用或兼容性而导入)。
    • math 导入坐标转换算法所需的数学函数(sincossqrtatan2 等)。
  • 定义 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/SE/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 接口。
    • 定义坐标数学所需的全局常量(piaeex_pi)——地球椭球参数和缩放因子。
    • 进入无限循环以通过 UART 持续读取传入的 GPS 数据。
      • 使用 buffer 累积部分消息,直到接收到完整行(以 \n 结尾)。
      • 当完整行到达时:
        • 检查是否以 $GNRMC$PNRMC 开头
        • 如果是,使用 parse_gnrmc() 解析
        • 通过 print_gps_data() 显示格式化输出
    • 处理异常:
      • KeyboardInterrupt:在 Ctrl+C 时优雅退出。
      • 一般 Exception:捕获并打印任何意外错误。
    • 最后,调用 uart.deinit() 在退出前清理 UART 资源。

结果

I2C

硬件

Seeed Studio XIAO nRF54L15 SenseSeeed Studio XIAO 扩展底板

软件


代码
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 导入时间模块以启用与时间相关的功能,如延迟。
    • XiaoI2Cboards.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_CONTRASTSSD1306_DISPLAY_ON 等)定义了用于配置和控制显示硬件的控制命令。
    • SSD1306_WIDTH = 128SSD1306_HEIGHT = 64SSD1306_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

硬件

Seeed Studio XIAO nRF54L15 SenseePaper Driver Board for Seeed Studio XIAO

软件


代码
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():确定特定位置是否应绘制像素(用于文本渲染)。
  • 显示更新

    • epaper_update_lines(lines):更新显示器的核心功能。
    • 首先,发送数据设置全白背景。
    • 然后,逐行计算并传输新的图像数据。
    • 最后,触发显示刷新以显示新内容。
    • 支持多行文本显示,每行可以有不同的位置和缩放比例。
  • main() 函数

    • 初始化显示器。
    • 计算文本的居中位置。
    • 创建文本行的配置列表。
    • 调用 epaper_update_lines() 更新显示内容。
    • 进入无限睡眠循环。

结果

自动执行程序

如果您希望您的程序能够自动执行,可以按照以下步骤操作:

步骤 1. 创建一个新的程序文件,使用 Ctrl + S 将其保存到 MicroPython 设备的闪存中,并命名为 main.py。

这里以闪烁程序为例

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

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

效果:

技术支持与产品讨论

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

Loading Comments...